code: purgatorio

Download patch

ref: a600cca5ff367ca7e7c64a14a8e87ba376780f6d
parent: d990c25d5795b16c181e875bf2f55aa06c2f75f9
author: henesy <devnull@localhost>
date: Sun Nov 4 12:16:24 EST 2018

init 3

diff: cannot open b/appl/acme/acme/acid/src//null: file does not exist: 'b/appl/acme/acme/acid/src//null' diff: cannot open b/appl/acme/acme/acid//null: file does not exist: 'b/appl/acme/acme/acid//null' diff: cannot open b/appl/acme/acme/bin/src//null: file does not exist: 'b/appl/acme/acme/bin/src//null' diff: cannot open b/appl/acme/acme/bin//null: file does not exist: 'b/appl/acme/acme/bin//null' diff: cannot open b/appl/acme/acme/edit/src//null: file does not exist: 'b/appl/acme/acme/edit/src//null' diff: cannot open b/appl/acme/acme/edit//null: file does not exist: 'b/appl/acme/acme/edit//null' diff: cannot open b/appl/acme/acme/mail/src//null: file does not exist: 'b/appl/acme/acme/mail/src//null' diff: cannot open b/appl/acme/acme/mail//null: file does not exist: 'b/appl/acme/acme/mail//null' diff: cannot open b/appl/acme/acme//null: file does not exist: 'b/appl/acme/acme//null' diff: cannot open b/appl/acme//null: file does not exist: 'b/appl/acme//null' diff: cannot open b/appl/alphabet/abc//null: file does not exist: 'b/appl/alphabet/abc//null' diff: cannot open b/appl/alphabet/auxi//null: file does not exist: 'b/appl/alphabet/auxi//null' diff: cannot open b/appl/alphabet/fs//null: file does not exist: 'b/appl/alphabet/fs//null' diff: cannot open b/appl/alphabet/grid//null: file does not exist: 'b/appl/alphabet/grid//null' diff: cannot open b/appl/alphabet/main//null: file does not exist: 'b/appl/alphabet/main//null' diff: cannot open b/appl/alphabet/typesets//null: file does not exist: 'b/appl/alphabet/typesets//null' diff: cannot open b/appl/alphabet//null: file does not exist: 'b/appl/alphabet//null' diff: cannot open b/appl/charon//null: file does not exist: 'b/appl/charon//null' diff: cannot open b/appl/cmd/asm//null: file does not exist: 'b/appl/cmd/asm//null' diff: cannot open b/appl/cmd/auth/factotum/proto//null: file does not exist: 'b/appl/cmd/auth/factotum/proto//null' diff: cannot open b/appl/cmd/auth/factotum//null: file does not exist: 'b/appl/cmd/auth/factotum//null' diff: cannot open b/appl/cmd/auth//null: file does not exist: 'b/appl/cmd/auth//null' diff: cannot open b/appl/cmd/auxi//null: file does not exist: 'b/appl/cmd/auxi//null' diff: cannot open b/appl/cmd/avr//null: file does not exist: 'b/appl/cmd/avr//null' diff: cannot open b/appl/cmd/dbm//null: file does not exist: 'b/appl/cmd/dbm//null' diff: cannot open b/appl/cmd/fs//null: file does not exist: 'b/appl/cmd/fs//null' diff: cannot open b/appl/cmd/install//null: file does not exist: 'b/appl/cmd/install//null' diff: cannot open b/appl/cmd/ip/nppp//null: file does not exist: 'b/appl/cmd/ip/nppp//null' diff: cannot open b/appl/cmd/ip/ppp//null: file does not exist: 'b/appl/cmd/ip/ppp//null' diff: cannot open b/appl/cmd/ip//null: file does not exist: 'b/appl/cmd/ip//null' diff: cannot open b/appl/cmd/lego//null: file does not exist: 'b/appl/cmd/lego//null' diff: cannot open b/appl/cmd/limbo//null: file does not exist: 'b/appl/cmd/limbo//null' diff: cannot open b/appl/cmd/mash//null: file does not exist: 'b/appl/cmd/mash//null' diff: cannot open b/appl/cmd/mk//null: file does not exist: 'b/appl/cmd/mk//null' diff: cannot open b/appl/cmd/mpc//null: file does not exist: 'b/appl/cmd/mpc//null' diff: cannot open b/appl/cmd/ndb//null: file does not exist: 'b/appl/cmd/ndb//null' diff: cannot open b/appl/cmd/palm//null: file does not exist: 'b/appl/cmd/palm//null' diff: cannot open b/appl/cmd/sh/doc//null: file does not exist: 'b/appl/cmd/sh/doc//null' diff: cannot open b/appl/cmd/sh//null: file does not exist: 'b/appl/cmd/sh//null' diff: cannot open b/appl/cmd/spki//null: file does not exist: 'b/appl/cmd/spki//null' diff: cannot open b/appl/cmd/usb//null: file does not exist: 'b/appl/cmd/usb//null' diff: cannot open b/appl/cmd//null: file does not exist: 'b/appl/cmd//null' diff: cannot open b/appl/collab/clients//null: file does not exist: 'b/appl/collab/clients//null' diff: cannot open b/appl/collab/lib//null: file does not exist: 'b/appl/collab/lib//null' diff: cannot open b/appl/collab/servers//null: file does not exist: 'b/appl/collab/servers//null' diff: cannot open b/appl/collab//null: file does not exist: 'b/appl/collab//null' diff: cannot open b/appl/demo/camera//null: file does not exist: 'b/appl/demo/camera//null' diff: cannot open b/appl/demo/chat//null: file does not exist: 'b/appl/demo/chat//null' diff: cannot open b/appl/demo/cpupool//null: file does not exist: 'b/appl/demo/cpupool//null' diff: cannot open b/appl/demo/lego//null: file does not exist: 'b/appl/demo/lego//null' diff: cannot open b/appl/demo/ns//null: file does not exist: 'b/appl/demo/ns//null' diff: cannot open b/appl/demo/odbc//null: file does not exist: 'b/appl/demo/odbc//null' diff: cannot open b/appl/demo/spree//null: file does not exist: 'b/appl/demo/spree//null' diff: cannot open b/appl/demo/whiteboard//null: file does not exist: 'b/appl/demo/whiteboard//null' diff: cannot open b/appl/demo//null: file does not exist: 'b/appl/demo//null' diff: cannot open b/appl/ebook/dtd//null: file does not exist: 'b/appl/ebook/dtd//null' diff: cannot open b/appl/ebook//null: file does not exist: 'b/appl/ebook//null' diff: cannot open b/appl/examples/minitel//null: file does not exist: 'b/appl/examples/minitel//null' diff: cannot open b/appl/examples//null: file does not exist: 'b/appl/examples//null' diff: cannot open b/appl/grid/demo//null: file does not exist: 'b/appl/grid/demo//null' diff: cannot open b/appl/grid/lib//null: file does not exist: 'b/appl/grid/lib//null' diff: cannot open b/appl/grid//null: file does not exist: 'b/appl/grid//null' diff: cannot open b/appl/lib/convcs//null: file does not exist: 'b/appl/lib/convcs//null' diff: cannot open b/appl/lib/crypt//null: file does not exist: 'b/appl/lib/crypt//null' diff: cannot open b/appl/lib/ecmascript//null: file does not exist: 'b/appl/lib/ecmascript//null' diff: cannot open b/appl/lib/encoding//null: file does not exist: 'b/appl/lib/encoding//null' diff: cannot open b/appl/lib/ida//null: file does not exist: 'b/appl/lib/ida//null' diff: cannot open b/appl/lib/print//null: file does not exist: 'b/appl/lib/print//null' diff: cannot open b/appl/lib/spki//null: file does not exist: 'b/appl/lib/spki//null' diff: cannot open b/appl/lib/strokes//null: file does not exist: 'b/appl/lib/strokes//null' diff: cannot open b/appl/lib/styxconv//null: file does not exist: 'b/appl/lib/styxconv//null' diff: cannot open b/appl/lib/usb//null: file does not exist: 'b/appl/lib/usb//null' diff: cannot open b/appl/lib/w3c//null: file does not exist: 'b/appl/lib/w3c//null' diff: cannot open b/appl/lib//null: file does not exist: 'b/appl/lib//null' diff: cannot open b/appl/math//null: file does not exist: 'b/appl/math//null' diff: cannot open b/appl/spree/clients//null: file does not exist: 'b/appl/spree/clients//null' diff: cannot open b/appl/spree/engines//null: file does not exist: 'b/appl/spree/engines//null' diff: cannot open b/appl/spree/lib//null: file does not exist: 'b/appl/spree/lib//null' diff: cannot open b/appl/spree/other//null: file does not exist: 'b/appl/spree/other//null' diff: cannot open b/appl/spree//null: file does not exist: 'b/appl/spree//null' diff: cannot open b/appl/svc/httpd//null: file does not exist: 'b/appl/svc/httpd//null' diff: cannot open b/appl/svc/webget//null: file does not exist: 'b/appl/svc/webget//null' diff: cannot open b/appl/svc//null: file does not exist: 'b/appl/svc//null' diff: cannot open b/appl/tiny//null: file does not exist: 'b/appl/tiny//null' diff: cannot open b/appl/wm/brutus//null: file does not exist: 'b/appl/wm/brutus//null' diff: cannot open b/appl/wm/drawmux//null: file does not exist: 'b/appl/wm/drawmux//null' diff: cannot open b/appl/wm/ftree//null: file does not exist: 'b/appl/wm/ftree//null' diff: cannot open b/appl/wm/mpeg//null: file does not exist: 'b/appl/wm/mpeg//null' diff: cannot open b/appl/wm//null: file does not exist: 'b/appl/wm//null' diff: cannot open b/appl//null: file does not exist: 'b/appl//null'
--- /dev/null
+++ b/appl/NOTICE
@@ -1,0 +1,25 @@
+This copyright NOTICE applies to all files in this directory and
+subdirectories, unless another copyright notice appears in a given
+file or subdirectory.  If you take substantial code from this software to use in
+other programs, you must somehow include with it an appropriate
+copyright notice that includes the copyright notice and the other
+notices below.  It is fine (and often tidier) to do that in a separate
+file such as NOTICE, LICENCE or COPYING.
+
+Copyright © 1995-1999 Lucent Technologies Inc.
+Portions Copyright © 1997-2000 Vita Nuova Limited
+Portions Copyright © 2000-2010 Vita Nuova Holdings Limited
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
--- /dev/null
+++ b/appl/acme/acme.b
@@ -1,0 +1,1125 @@
+implement Acme;
+
+include "common.m";
+
+sys : Sys;
+bufio : Bufio;
+workdir : Workdir;
+drawm : Draw;
+styx : Styx;
+acme : Acme;
+gui : Gui;
+graph : Graph;
+dat : Dat;
+framem : Framem;
+utils : Utils;
+regx : Regx;
+scrl : Scroll;
+textm : Textm;
+filem : Filem;
+windowm : Windowm;
+rowm : Rowm;
+columnm : Columnm;
+bufferm : Bufferm;
+diskm : Diskm;
+exec : Exec;
+look : Look;
+timerm : Timerm;
+fsys : Fsys;
+xfidm : Xfidm;
+plumbmsg : Plumbmsg;
+editm: Edit;
+editlog: Editlog;
+editcmd: Editcmd;
+styxaux: Styxaux;
+
+sprint : import sys;
+BACK, HIGH, BORD, TEXT, HTEXT, NCOL : import Framem;
+Point, Rect, Font, Image, Display, Pointer: import drawm;
+TRUE, FALSE, maxtab : import dat;
+Ref, Reffont, Command, Timer, Lock, Cursor : import dat;
+row, reffont, activecol, mouse, typetext, mousetext, barttext, argtext, seltext, button, modbutton, colbutton, arrowcursor, boxcursor, plumbed : import dat;
+Xfid : import xfidm;
+cmouse, ckeyboard, cwait, ccommand, ckill, cxfidalloc, cxfidfree, cerr, cplumb, cedit : import dat;
+font, bflush, balloc, draw : import graph;
+Arg, PNPROC, PNGROUP : import utils;
+arginit, argopt, argf, error, warning, postnote : import utils;
+yellow, green, red, blue, black, white, mainwin, display : import gui;
+Disk : import diskm;
+Row : import rowm;
+Column : import columnm;
+Window : import windowm;
+Text, Tag, Body, Columntag : import textm;
+Buffer : import bufferm;
+snarfbuf : import exec;
+Msg : import plumbmsg;
+
+tfd : ref Sys->FD;
+lasttime : int;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	acmectxt = ctxt;
+
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	{
+		# tfd = sys->create("./time", Sys->OWRITE, 8r600);
+		# lasttime = sys->millisec();
+		bufio = load Bufio Bufio->PATH;
+		workdir = load Workdir Workdir->PATH;
+		drawm = load Draw Draw->PATH;
+	
+		styx = load Styx Styx->PATH;
+	
+		acme = load Acme SELF;
+	
+		gui = load Gui path(Gui->PATH);
+		graph = load Graph path(Graph->PATH);
+		dat = load Dat path(Dat->PATH);
+		framem = load Framem path(Framem->PATH);
+		utils = load Utils path(Utils->PATH);
+		regx = load Regx path(Regx->PATH);
+		scrl = load Scroll path(Scroll->PATH);
+		textm = load Textm path(Textm->PATH);
+		filem = load Filem path(Filem->PATH);
+		windowm = load Windowm path(Windowm->PATH);
+		rowm = load Rowm path(Rowm->PATH);
+		columnm = load Columnm path(Columnm->PATH);
+		bufferm = load Bufferm path(Bufferm->PATH);
+		diskm = load Diskm path(Diskm->PATH);
+		exec = load Exec path(Exec->PATH);
+		look = load Look path(Look->PATH);
+		timerm = load Timerm path(Timerm->PATH);
+		fsys = load Fsys path(Fsys->PATH);
+		xfidm = load Xfidm path(Xfidm->PATH);
+		plumbmsg = load Plumbmsg Plumbmsg->PATH;
+		editm = load Edit path(Edit->PATH);
+		editlog = load Editlog path(Editlog->PATH);
+		editcmd = load Editcmd path(Editcmd->PATH);
+		styxaux = load Styxaux path(Styxaux->PATH);
+		
+		mods := ref Dat->Mods(sys, bufio, drawm, styx, styxaux,
+						acme, gui, graph, dat, framem,
+						utils, regx, scrl,
+						textm, filem, windowm, rowm, columnm,
+						bufferm, diskm, exec, look, timerm,
+						fsys, xfidm, plumbmsg, editm, editlog, editcmd);
+	
+		styx->init();
+		styxaux->init();
+	
+		utils->init(mods);
+		gui->init(mods);
+		graph->init(mods);
+		dat->init(mods);
+		framem->init(mods);
+		regx->init(mods);
+		scrl->init(mods);
+		textm->init(mods);
+		filem->init(mods);
+		windowm->init(mods);
+		rowm->init(mods);
+		columnm->init(mods);
+		bufferm->init(mods);
+		diskm->init(mods);
+		exec->init(mods);
+		look->init(mods);
+		timerm->init(mods);
+		fsys->init(mods);
+		xfidm->init(mods);
+		editm->init(mods);
+		editlog->init(mods);
+		editcmd->init(mods);
+	
+		utils->debuginit();
+	
+		if (plumbmsg->init(1, "edit", Dat->PLUMBSIZE) >= 0)
+			plumbed = 1;
+	
+		main(argl);
+	
+	}
+#	exception{
+#		* =>
+#			sys->fprint(sys->fildes(2), "acme: fatal: %s\n", utils->getexc());
+#			sys->print("acme: fatal: %s\n", utils->getexc());
+#			shutdown("error");
+#	}
+}
+
+timing(s : string)
+{
+	thistime := sys->millisec();
+	sys->fprint(tfd, "%s	%d\n", s, thistime-lasttime);
+	lasttime = thistime;
+}
+
+path(p : string) : string
+{
+	if (RELEASECOPY)
+		return p;
+	else {
+		# inlined strrchr since not loaded yet
+		 for (n := len p - 1; n >= 0; n--)
+			if (p[n] == '/')
+				break;
+		 if (n >= 0)
+			p = p[n+1:];
+		 return "/usr/jrf/acme/" + p;
+	}
+}
+
+waitpid0, waitpid1 : int;
+mainpid : int;
+
+fontcache : array of ref Reffont;
+nfontcache : int;
+reffonts : array of ref Reffont;
+deffontnames := array[2] of {
+	"/fonts/lucidasans/euro.8.font",
+	"/fonts/lucm/unicode.9.font",
+};
+
+command : ref Command;
+
+WPERCOL : con 8;
+
+NSnarf : con 32;
+snarfrune : ref Dat->Astring;
+
+main(argl : list of string)
+{
+	i, ac : int;
+	loadfile : string;
+	p : int;
+	c : ref Column;
+	arg : ref Arg;
+	ncol : int;
+
+	ncol = -1;
+
+	mainpid = sys->pctl(0, nil);
+	loadfile = nil;
+	fontnames = array[2] of string;
+	fontnames[0:] = deffontnames[0:2];
+	f := utils->getenv("acme-font");
+	if (f != nil)
+		fontnames[0] = f;
+	f = utils->getenv("acme-Font");
+	if (f != nil)
+		fontnames[1] = f;
+	arg = arginit(argl);
+	while(ac = argopt(arg)) case(ac){
+	'b' =>
+		dat->bartflag = TRUE;
+	'c' =>
+		ncol = int argf(arg);
+	'f' =>
+		fontnames[0] = argf(arg);
+	'F' =>
+		fontnames[1] = argf(arg);
+	'l' =>
+		loadfile = argf(arg);
+	}
+
+	dat->home = utils->getenv("home");
+	if (dat->home == nil)
+		dat->home = utils->gethome(utils->getuser());
+	ts := utils->getenv("tabstop");
+	if (ts != nil)
+		maxtab = int ts;
+	if (maxtab <= 0)
+		maxtab = 4;
+	snarfrune = utils->stralloc(NSnarf);
+	sys->pctl(Sys->FORKNS|Sys->FORKENV, nil);
+	utils->setenv("font", fontnames[0]);
+	sys->bind("/acme/dis", "/dis", Sys->MBEFORE);
+	wdir = workdir->init();
+	if (wdir == nil)
+		wdir = ".";
+	workdir = nil;
+
+	graph->binit();
+	font = Font.open(display, fontnames[0]);
+	if(font == nil){
+		fontnames[0] = deffontnames[0];
+		font = Font.open(display, fontnames[0]);
+		if (font == nil) {
+			warning(nil, sprint("can't open font file %s: %r\n", fontnames[0]));
+			return;
+		}
+	}
+	reffont = ref Reffont;
+	reffont.r = Ref.init();
+	reffont.f = font;
+	reffonts = array[2] of ref Reffont;
+	reffonts[0] = reffont;
+	reffont.r.inc();	# one to hold up 'font' variable 
+	reffont.r.inc();	# one to hold up reffonts[0] 
+	fontcache = array[1] of ref Reffont;
+	nfontcache = 1;
+	fontcache[0] = reffont;
+
+	iconinit();
+	usercolinit();
+	timerm->timerinit();
+	regx->rxinit();
+
+	cwait = chan of string;
+	ccommand = chan of ref Command;
+	ckill = chan of string;
+	cxfidalloc = chan of ref Xfid;
+	cxfidfree = chan of ref Xfid;
+	cerr = chan of string;
+	cplumb = chan of ref Msg;
+	cedit = chan of int;
+
+	gui->spawnprocs();
+	# spawn keyboardproc();
+	# spawn mouseproc();
+	sync := chan of int;
+	spawn waitproc(sys->pctl(0, nil), sync);
+	<- sync;
+	spawn plumbproc();
+
+	fsys->fsysinit();
+	dat->disk = (dat->disk).init();
+	row = rowm->newrow();
+	if(loadfile != nil) {
+		row.qlock.lock();	# tasks->procs now 
+		row.loadx(loadfile, TRUE);
+		row.qlock.unlock();
+	}
+	else{
+		row.init(mainwin.clipr);
+		if(ncol < 0){
+			if(arg.av == nil)
+				ncol = 2;
+			else{
+				ncol = (len arg.av+(WPERCOL-1))/WPERCOL;
+				if(ncol < 2)
+					ncol = 2;
+			}
+		}
+		if(ncol == 0)
+			ncol = 2;
+		for(i=0; i<ncol; i++){
+			c = row.add(nil, -1);
+			if(c==nil && i==0)
+				error("initializing columns");
+		}
+		c = row.col[row.ncol-1];
+		if(arg.av == nil)
+			readfile(c, wdir);
+		else
+			i = 0;
+			for( ; arg.av != nil; arg.av = tl arg.av){
+				filen := hd arg.av;
+				p = utils->strrchr(filen, '/');
+				if((p>=0 && filen[p:] == "/guide") || i/WPERCOL>=row.ncol)
+					readfile(c, filen);
+				else
+					readfile(row.col[i/WPERCOL], filen);
+				i++;
+			}
+	}
+	bflush();
+
+	spawn keyboardtask();
+	spawn mousetask();
+	spawn waittask();
+	spawn xfidalloctask();
+
+	# notify(shutdown);
+	# waitc := chan of int;
+	# <-waitc;
+	# killprocs();
+	exit;
+}
+
+readfile(c : ref Column, s : string)
+{
+	w : ref Window;
+	r : string;
+	nr : int;
+
+	w = c.add(nil, nil, -1);
+	(r, nr) = look->cleanname(s, len s);
+	w.setname(r, nr);
+	w.body.loadx(0, s, 1);
+	w.body.file.mod = FALSE;
+	w.dirty = FALSE;
+	w.settag();
+	scrl->scrdraw(w.body);
+	w.tag.setselect(w.tag.file.buf.nc, w.tag.file.buf.nc);
+}
+
+oknotes := array[6] of {
+	"delete",
+	"hangup",
+	"kill",
+	"exit",
+	"error",
+	nil
+};
+
+dumping : int;
+
+shutdown(msg : string)
+{
+	i : int;
+
+	# notify(nil);
+	if(!dumping && msg != "kill" && msg != "exit" && (1 || sys->pctl(0, nil)==mainpid) && row != nil){
+		dumping = TRUE;
+		row.dump(nil);
+	}
+	for(i=0; oknotes[i] != nil; i++)
+		if(utils->strncmp(oknotes[i], msg, len oknotes[i]) == 0) {
+			killprocs();
+			exit;
+		}
+	# killprocs();
+	sys->fprint(sys->fildes(2), "acme: %s\n", msg);
+	sys->print("acme: %s\n", msg);
+	# exit;
+}
+
+acmeexit(err: string)
+{
+	if(err != nil)
+		shutdown(err);
+	graph->cursorswitch(nil);
+	if (plumbed)
+		plumbmsg->shutdown();
+	killprocs();
+	gui->killwins();
+	exit;
+}
+
+killprocs()
+{
+	c : ref Command;
+	kill := "kill";
+	thispid := sys->pctl(0, nil);
+	fsys->fsysclose();
+
+	postnote(PNPROC, thispid, mousepid, kill);
+	postnote(PNPROC, thispid, keyboardpid, kill);
+	postnote(PNPROC, thispid, timerpid, kill);
+	postnote(PNPROC, thispid, waitpid0, kill);
+	postnote(PNPROC, thispid, waitpid1, kill);
+	postnote(PNPROC, thispid, fsyspid, kill);
+	postnote(PNPROC, thispid, mainpid, kill);
+	postnote(PNPROC, thispid, keytid, kill);
+	postnote(PNPROC, thispid, mousetid, kill);
+	postnote(PNPROC, thispid, waittid, kill);
+	postnote(PNPROC, thispid, xfidalloctid, kill);
+	# postnote(PNPROC, thispid, lockpid, kill);
+	postnote(PNPROC, thispid, plumbpid, kill);
+
+	# draw(mainwin, mainwin.r, white, nil, mainwin.r.min);
+
+	for(c=command; c != nil; c=c.next)
+		postnote(PNGROUP, thispid, c.pid, "kill");
+
+	xfidm->xfidkill();
+}
+
+keytid : int;
+mousetid : int;
+waittid : int;
+xfidalloctid : int;
+
+keyboardtask()
+{
+	r : int;
+	timer : ref Timer;
+	null : ref Timer;
+	t : ref Text;
+
+	{
+		keytid = sys->pctl(0, nil);
+		null = ref Timer;
+		null.c = chan of int;
+		timer = null;
+		typetext = nil;
+		for(;;){
+			alt{
+			<-(timer.c) =>
+				timerm->timerstop(timer);
+				t = typetext;
+				if(t!=nil && t.what==Tag && !t.w.qlock.locked()){
+					t.w.lock('K');
+					t.w.commit(t);
+					t.w.unlock();
+					bflush();
+				}
+				timer = null;
+			r = <-ckeyboard =>
+				gotkey := 1;
+				while (gotkey) {
+					typetext = row.typex(r, mouse.xy);
+					t = typetext;
+					if(t!=nil && t.col!=nil)
+						activecol = t.col;
+					if(t!=nil && t.w!=nil)
+						t.w.body.file.curtext = t.w.body;
+					if(timer != null)
+						spawn timerm->timerwaittask(timer);
+					if(t!=nil && t.what==Tag)
+						timer = timerm->timerstart(500);
+					else
+						timer = null;
+					alt {
+						r = <- ckeyboard =>
+							gotkey = 1;	# do this case again
+						* =>
+							gotkey = 0;
+					}
+					bflush();
+				}
+			}
+		}
+	}
+	exception{
+		* =>
+			shutdown(utils->getexc());
+			raise;
+			# acmeexit(nil);
+	}
+}
+
+mousetask()
+{
+	t, argt : ref Text;
+	but, ok : int;
+	q0, q1 : int;
+	w : ref Window;
+	m : ref Msg;
+
+	{
+		mousetid = sys->pctl(0, nil);
+		sync := chan of int;
+		spawn waitproc(mousetid, sync);
+		<- sync;
+		for(;;){
+			alt{
+			*mouse = *<-cmouse =>
+				row.qlock.lock();
+				if (mouse.buttons & M_QUIT) {
+					if (row.clean(TRUE))
+						acmeexit(nil);
+					# shutdown("kill");
+					row.qlock.unlock();
+					break;
+				}
+				if (mouse.buttons & M_HELP) {
+					warning(nil, "no help provided (yet)");
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(mouse.buttons & M_RESIZE){
+					draw(mainwin, mainwin.r, white, nil, mainwin.r.min);
+					scrl->scrresize();
+					row.reshape(mainwin.clipr);
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				t = row.which(mouse.xy);
+				if(t!=mousetext && mousetext!=nil && mousetext.w!=nil){
+					mousetext.w.lock('M');
+					mousetext.eq0 = ~0;
+					mousetext.w.commit(mousetext);
+					mousetext.w.unlock();
+				}
+				mousetext = t;
+				if(t == nil) {
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				w = t.w;
+				if(t==nil || mouse.buttons==0) {
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(w != nil)
+					w.body.file.curtext = w.body;
+				but = 0;
+				if(mouse.buttons == 1)
+					but = 1;
+				else if(mouse.buttons == 2)
+					but = 2;
+				else if(mouse.buttons == 4)
+					but = 3;
+				barttext = t;
+				if(t.what==Body && mouse.xy.in(t.scrollr)){
+					if(but){
+						w.lock('M');
+						t.eq0 = ~0;
+						scrl->scroll(t, but);
+						t.w.unlock();
+					}
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(w != nil && (mouse.buttons &(8|16))){
+					if(mouse.buttons & 8)
+						but = Dat->Kscrollup;
+					else
+						but = Dat->Kscrolldown;
+					w.lock('M');
+					t.eq0 = ~0;
+					t.typex(but, 0);
+					w.unlock();
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(mouse.xy.in(t.scrollr)){
+					if(but){
+						if(t.what == Columntag)
+							row.dragcol(t.col);
+						else if(t.what == Tag){
+							t.col.dragwin(t.w, but);
+							if(t.w != nil)
+								barttext = t.w.body;
+						}
+						if(t.col != nil)
+							activecol = t.col;
+					}
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+				if(mouse.buttons){
+					if(w != nil)
+						w.lock('M');
+					t.eq0 = ~0;
+					if(w != nil)
+						w.commit(t);
+					else
+						t.commit(TRUE);
+					if(mouse.buttons & 1){
+						t.select(0);
+						if(w != nil)
+							w.settag();
+						argtext = t;
+						seltext = t;
+						if(t.col != nil)
+							activecol = t.col;	# button 1 only 
+						if(t.w != nil && t == t.w.body)
+							dat->activewin = t.w;
+					}else if(mouse.buttons & 2){
+						(ok, argt, q0, q1) = t.select2(q0, q1);
+						if(ok)
+							exec->execute(t, q0, q1, FALSE, argt);
+					}else if(mouse.buttons & 4){
+						(ok, q0, q1) = t.select3(q0, q1);
+						if(ok)
+							look->look3(t, q0, q1, FALSE);
+					}
+					if(w != nil)
+						w.unlock();
+					bflush();
+					row.qlock.unlock();
+					break;
+				}
+			m = <- cplumb =>
+				if (m.kind == "text") {
+					attrs := plumbmsg->string2attrs(m.attr);
+					(found, act) := plumbmsg->lookup(attrs, "action");
+					if (!found || act == nil || act == "showfile")
+						look->plumblook(m);
+					else if (act == "showdata")
+						look->plumbshow(m);
+				}
+				bflush();
+			}
+		}
+	}
+	exception{
+		* =>
+			shutdown(utils->getexc());
+			raise;
+			# acmeexit(nil);
+	}
+}
+
+# list of processes that have exited but we have not heard of yet
+Pid : adt {
+	pid : int;
+	msg : string;
+	next : cyclic ref Pid;
+};
+
+waittask()
+{
+	status : string;
+	c, lc : ref Command;
+	pid : int;
+	found : int;
+	cmd : string;
+	err : string;
+	t : ref Text;
+	pids : ref Pid;
+
+	waittid = sys->pctl(0, nil);
+	command = nil;
+	for(;;){
+		alt{
+		err = <-cerr =>
+			row.qlock.lock();
+			warning(nil, err);
+			err = nil;
+			bflush();
+			row.qlock.unlock();
+			break;
+		cmd = <-ckill =>
+			found = FALSE;
+			for(c=command; c != nil; c=c.next){
+				# -1 for blank 
+				if(c.name[0:len c.name - 1] == cmd){
+					if(postnote(PNGROUP, waittid, c.pid, "kill") < 0)
+						warning(nil, sprint("kill %s: %r\n", cmd));
+					found = TRUE;
+				}
+			}
+			if(!found)
+				warning(nil, sprint("Kill: no process %s\n", cmd));
+			cmd = nil;
+			break;
+		status = <-cwait =>
+			pid = int status;
+			lc = nil;
+			for(c=command; c != nil; c=c.next){
+				if(c.pid == pid){
+					if(lc != nil)
+						lc.next = c.next;
+					else
+						command = c.next;
+					break;
+				}
+				lc = c;
+			}
+			row.qlock.lock();
+			t = row.tag;
+			t.commit(TRUE);
+			if(c == nil){
+				# warning(nil, sprint("unknown child pid %d\n", pid));
+				p := ref Pid;
+				p.pid = pid;
+				p.msg = status;
+				p.next = pids;
+				pids = p;
+			}
+			else{
+				if(look->search(t, c.name, len c.name)){
+					t.delete(t.q0, t.q1, TRUE);
+					t.setselect(0, 0);
+				}
+				if(status[len status - 1] != ':')
+					warning(c.md, sprint("%s\n", status));
+				bflush();
+			}
+			row.qlock.unlock();
+			if(c != nil){
+				if(c.iseditcmd)
+					cedit <- = 0;
+				fsys->fsysdelid(c.md);
+				c = nil;
+			}
+			break;
+		c = <-ccommand =>
+			lastp : ref Pid = nil;
+			for(p := pids; p != nil; p = p.next){
+				if(p.pid == c.pid){
+					status = p.msg;
+					if(status[len status - 1] != ':')
+						warning(c.md, sprint("%s\n", status));
+					if(lastp == nil)
+						pids = p.next;
+					else
+						lastp.next = p.next;
+					if(c.iseditcmd)
+						cedit <- = 0;
+					fsys->fsysdelid(c.md);
+					c = nil;
+					break;
+				}
+				lastp = p;
+			}	
+			c.next = command;
+			command = c;
+			row.qlock.lock();
+			t = row.tag;
+			t.commit(TRUE);
+			t.insert(0, c.name, len c.name, TRUE, 0);
+			t.setselect(0, 0);
+			bflush();
+			row.qlock.unlock();
+			break;
+		}
+	}
+}
+
+xfidalloctask()
+{
+	xfree, x : ref Xfid;
+
+	xfidalloctid = sys->pctl(0, nil);
+	xfree = nil;
+	for(;;){
+		alt{
+		<-cxfidalloc =>
+			x = xfree;
+			if(x != nil)
+				xfree = x.next;
+			else{
+				x = xfidm->newxfid();
+				x.c = chan of int;
+				spawn x.ctl();
+			}
+			cxfidalloc <-= x;
+			break;
+		x = <-cxfidfree =>
+			x.next = xfree;
+			xfree = x;
+			break;
+		}
+	}
+}
+
+frgetmouse()
+{
+	bflush();
+	*mouse = *<-cmouse;
+}
+
+waitproc(pid : int, sync: chan of int)
+{
+	fd : ref Sys->FD;
+	n : int;
+
+	if (waitpid0 == 0)
+		waitpid0 = sys->pctl(0, nil);
+	else
+		waitpid1 = sys->pctl(0, nil);
+	sys->pctl(Sys->FORKFD, nil);
+	# w := sprint("/prog/%d/wait", pid);
+	w := sprint("#p/%d/wait", pid);
+	fd = sys->open(w, Sys->OREAD);
+	if (fd == nil)
+		error("fd == nil in waitproc");
+	sync <-= 0;
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;){
+		if ((n = sys->read(fd, buf, len buf))<0)
+			error("bad read in waitproc");
+		status = string buf[0:n];
+		cwait <-= status;
+	}
+}
+
+get(fix : int, save : int, setfont : int, name : string) : ref Reffont
+{
+	r : ref Reffont;
+	f : ref Font;
+	i : int;
+
+	r = nil;
+	if(name == nil){
+		name = fontnames[fix];
+		r = reffonts[fix];
+	}
+	if(r == nil){
+		for(i=0; i<nfontcache; i++)
+			if(name ==  fontcache[i].f.name){
+				r = fontcache[i];
+				break;
+			}
+		if (i >= nfontcache) {
+			f = Font.open(display, name);
+			if(f == nil){
+				warning(nil, sprint("can't open font file %s: %r\n", name));
+				return nil;
+			}
+			r = ref Reffont;
+			r.r = Ref.init();
+			r.f = f;
+			ofc := fontcache;
+			fontcache = array[nfontcache+1] of ref Reffont;
+			fontcache[0:] = ofc[0:nfontcache];
+			ofc = nil;
+			fontcache[nfontcache++] = r;
+		}
+	}
+	if(save){
+		r.r.inc();
+		if(reffonts[fix] != nil)
+			reffonts[fix].close();
+		reffonts[fix] = r;
+		fontnames[fix] = name;
+	}
+	if(setfont){
+		reffont.f = r.f;
+		r.r.inc();
+		reffonts[0].close();
+		font = r.f;
+		reffonts[0] = r;
+		r.r.inc();
+		iconinit();
+	}
+	r.r.inc();
+	return r;
+}
+
+close(r : ref Reffont)
+{
+	i : int;
+
+	if(r.r.dec() == 0){
+		for(i=0; i<nfontcache; i++)
+			if(r == fontcache[i])
+				break;
+		if(i >= nfontcache)
+			warning(nil, "internal error: can't find font in cache\n");
+		else{
+			fontcache[i:] = fontcache[i+1:nfontcache];
+			nfontcache--;
+		}
+		r.f = nil;
+		r = nil;
+	}
+}
+
+arrowbits := array[64] of {
+	 byte 16rFF, byte 16rE0, byte 16rFF, byte 16rE0,
+	 byte 16rFF, byte 16rC0, byte 16rFF, byte 16r00,
+	 byte 16rFF, byte 16r00, byte 16rFF, byte 16r80,
+	 byte 16rFF, byte 16rC0, byte 16rFF, byte 16rE0,
+	 byte 16rE7, byte 16rF0, byte 16rE3, byte 16rF8,
+	 byte 16rC1, byte 16rFC, byte 16r00, byte 16rFE,
+	 byte 16r00, byte 16r7F, byte 16r00, byte 16r3E,
+	 byte 16r00, byte 16r1C, byte 16r00, byte 16r08,
+
+	 byte 16r00, byte 16r00, byte 16r7F, byte 16rC0,
+	 byte 16r7F, byte 16r00, byte 16r7C, byte 16r00,
+	 byte 16r7E, byte 16r00, byte 16r7F, byte 16r00,
+	 byte 16r6F, byte 16r80, byte 16r67, byte 16rC0,
+	 byte 16r43, byte 16rE0, byte 16r41, byte 16rF0,
+	 byte 16r00, byte 16rF8, byte 16r00, byte 16r7C,
+	 byte 16r00, byte 16r3E, byte 16r00, byte 16r1C,
+	 byte 16r00, byte 16r08, byte 16r00, byte 16r00,
+};
+
+# outer boundary of width 1 is white
+# next  boundary of width 3 is black
+# next  boundary of width 1 is white
+# inner boundary of width 4 is transparent
+boxbits := array[64] of {
+	 byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF, 
+	 byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF,
+	 byte 16rFF, byte 16rFF, byte 16rF8, byte 16r1F,
+	 byte 16rF8, byte 16r1F, byte 16rF8, byte 16r1F,
+	 byte 16rF8, byte 16r1F, byte 16rF8, byte 16r1F,
+	 byte 16rF8, byte 16r1F, byte 16rFF, byte 16rFF,
+	 byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF,
+	 byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF,
+
+
+	 byte 16r00, byte 16r00, byte 16r7F, byte 16rFE,
+	 byte 16r7F, byte 16rFE, byte 16r7F, byte 16rFE,
+	 byte 16r70, byte 16r0E, byte 16r70, byte 16r0E,
+	 byte 16r70, byte 16r0E, byte 16r70, byte 16r0E,
+	 byte 16r70, byte 16r0E, byte 16r70, byte 16r0E,
+	 byte 16r70, byte 16r0E, byte 16r70, byte 16r0E,
+	 byte 16r7F, byte 16rFE, byte 16r7F, byte 16rFE,
+	 byte 16r7F, byte 16rFE, byte 16r00, byte 16r00,
+};
+
+iconinit()
+{
+	r : Rect;
+
+	# Blue
+	tagcols = array[NCOL] of ref Draw->Image;
+	tagcols[BACK] = display.colormix(Draw->Palebluegreen, Draw->White);
+	tagcols[HIGH] = display.color(Draw->Palegreygreen);
+	tagcols[BORD] = display.color(Draw->Purpleblue);
+	tagcols[TEXT] = black;
+	tagcols[HTEXT] = black;
+
+	# Yellow
+	textcols = array[NCOL] of ref Draw->Image;
+	textcols[BACK] = display.colormix(Draw->Paleyellow, Draw->White);
+	textcols[HIGH] = display.color(Draw->Darkyellow);
+	textcols[BORD] = display.color(Draw->Yellowgreen); 
+	textcols[TEXT] = black;
+	textcols[HTEXT] = black;
+
+	if(button != nil)
+		button = modbutton = colbutton = nil;
+
+	r = ((0, 0), (Dat->Scrollwid+2, font.height+1));
+	button = balloc(r, mainwin.chans, Draw->White);
+	draw(button, r, tagcols[BACK], nil, r.min);
+	r.max.x -= 2;
+	draw(button, r, tagcols[BORD], nil, (0, 0));   
+	r = r.inset(2);
+	draw(button, r, tagcols[BACK], nil, (0, 0));
+
+	r = button.r;
+	modbutton = balloc(r, mainwin.chans, Draw->White);
+	draw(modbutton, r, tagcols[BACK], nil, r.min);
+	r.max.x -= 2;
+	draw(modbutton, r, tagcols[BORD], nil, (0, 0));
+	r = r.inset(2);
+	draw(modbutton, r, display.rgb(16r00, 16r00, 16r99), nil, (0, 0));	# was DMedblue
+
+	r = button.r;
+	colbutton = balloc(r, mainwin.chans, Draw->White);
+	draw(colbutton, r, tagcols[BACK], nil, r.min);
+	r.max.x -= 2;
+	draw(colbutton, r, tagcols[BORD], nil, (0, 0));
+
+#	arrowcursor = ref Cursor((-1, -1), (16, 32), arrowbits);
+	boxcursor = ref Cursor((-7, -7), (16, 32), boxbits);
+
+	but2col = display.rgb(16raa, 16r00, 16r00);
+	but3col = display.rgb(16r00, 16r66, 16r00);
+	but2colt = white;
+	but3colt = white;
+
+	graph->cursorswitch(arrowcursor);
+}
+
+colrec : adt {
+	name : string;
+	image : ref Image;
+};
+
+coltab : array of colrec;
+
+cinit() 
+{
+	coltab = array[6] of colrec;
+	coltab[0].name = "yellow"; coltab[0].image = yellow;
+	coltab[1].name = "green"; coltab[1].image = green;
+	coltab[2].name = "red"; coltab[2].image = red;
+	coltab[3].name = "blue"; coltab[3].image = blue;
+	coltab[4].name = "black"; coltab[4].image = black;
+	coltab[5].name = "white"; coltab[5].image = white;
+}
+
+col(s : string, n : int) : int
+{
+	return ((s[n]-'0') << 4) | (s[n+1]-'0');
+}
+
+rgb(s : string, n : int) : (int, int, int)
+{
+	return (col(s, n), col(s, n+2), col(s, n+4));
+}
+
+cenv(s : string, t : string, but : int, i : ref Image) : ref Image
+{
+	c := utils->getenv("acme-" + s + "-" + t + "-" + string but);
+	if (c == nil)
+		c = utils->getenv("acme-" + s + "-" + string but);
+	if (c == nil && but != 0)
+		c = utils->getenv("acme-" + s);
+	if (c != nil) {
+		if (c[0] == '#' && len c >= 7) {
+			(r1, g1, b1) := rgb(c, 1);
+			if (len c >= 15 && c[7] == '/' && c[8] == '#') {
+				(r2, g2, b2) := rgb(c, 9);
+				return display.colormix((r1<<24)|(g1<<16)|(b1<<8)|16rFF, (r2<<24)|(g2<<16)|(b2<<8)|16rFF);
+			}
+			return display.color((r1<<24)|(g1<<16)|(b1<<8)|16rFF);
+		}
+		for (j := 0; j < len c; j++)
+			if (c[j] >= 'A' && c[j] <= 'Z')
+				c[j] += 'a'-'A';
+		for (j = 0; j < len coltab; j++)
+			if (c == coltab[j].name)
+				return coltab[j].image;
+	}
+	return i;
+}
+
+usercolinit()
+{
+	cinit();
+	textcols[TEXT] = cenv("fg", "text", 0, textcols[TEXT]);
+	textcols[BACK] = cenv("bg", "text", 0, textcols[BACK]);
+	textcols[HTEXT] = cenv("fg", "text", 1, textcols[HTEXT]);
+	textcols[HIGH] = cenv("bg", "text", 1, textcols[HIGH]);
+	but2colt= cenv("fg", "text", 2, but2colt);
+	but2col = cenv("bg", "text", 2, but2col);
+	but3colt = cenv("fg", "text", 3, but3colt);
+	but3col = cenv("bg", "text", 3, but3col);
+	tagcols[TEXT] = cenv("fg", "tag", 0, tagcols[TEXT]);
+	tagcols[BACK] = cenv("bg", "tag", 0, tagcols[BACK]);
+	tagcols[HTEXT] = cenv("fg", "tag", 1, tagcols[HTEXT]);
+	tagcols[HIGH] = cenv("bg", "tag", 1, tagcols[HIGH]);
+}
+
+getsnarf()
+{
+	# return;
+	fd := sys->open("/chan/snarf", sys->OREAD);
+	if(fd == nil)
+		return;
+	snarfbuf.reset();
+	snarfbuf.loadx(0, fd);
+}
+
+putsnarf()
+{
+	n : int;
+
+	# return;
+	if(snarfbuf.nc == 0)
+		return;
+	fd := sys->open("/chan/snarf", sys->OWRITE);
+	if(fd == nil)
+		return;
+  	for(i:=0; i<snarfbuf.nc; i+=n){
+		n = snarfbuf.nc-i;
+		if(n >= NSnarf)
+			n = NSnarf;
+		snarfbuf.read(i, snarfrune, 0, n);
+		sys->fprint(fd, "%s", snarfrune.s[0:n]);
+	}
+}
+
+plumbpid : int;
+
+plumbproc()
+{
+	plumbpid = sys->pctl(0, nil);
+	for(;;){
+		msg := Msg.recv();
+		if(msg == nil){
+			sys->print("Acme: can't read /chan/plumb.edit: %r\n");
+			plumbpid = 0;
+			plumbed = 0;
+			return;
+		}
+		if(msg.kind != "text"){
+			sys->print("Acme: can't interpret '%s' kind of message\n", msg.kind);
+			continue;
+		}
+# sys->print("msg %s\n", string msg.data);
+		cplumb <-= msg;
+	}
+}
--- /dev/null
+++ b/appl/acme/acme.m
@@ -1,0 +1,32 @@
+Acme : module {
+	PATH : con "/dis/acme.dis";
+
+	RELEASECOPY : con 1;
+
+	M_LBUT : con 1;
+	M_MBUT : con 2;
+	M_RBUT : con 4;
+	M_TBS : con 8;
+	M_PLUMB : con 16;
+	M_QUIT : con 32;
+	M_HELP : con 64;
+	M_RESIZE : con 128;
+	M_DOUBLE : con 256;
+
+	textcols, tagcols : array of ref Draw->Image;
+	but2col, but3col, but2colt, but3colt : ref Draw->Image;
+
+	acmectxt : ref Draw->Context;
+	keyboardpid, mousepid, timerpid, fsyspid : int;
+	fontnames : array of string;
+	wdir : string;
+
+	init : fn(ctxt : ref Draw->Context, argv : list of string);
+	timing : fn(s : string);
+	frgetmouse : fn();
+	get : fn(p, q, r : int, b : string) : ref Dat->Reffont;
+	close : fn(r : ref Dat->Reffont);
+	acmeexit : fn(err : string);
+	getsnarf : fn(); 
+	putsnarf : fn();
+};
--- /dev/null
+++ b/appl/acme/acme/acid/guide
@@ -1,0 +1,2 @@
+Acid pid
+Acid -l alef -l symsfile pid
--- /dev/null
+++ b/appl/acme/acme/acid/mkfile
@@ -1,0 +1,24 @@
+<../../../../mkconfig
+
+BIN=$ROOT/acme/acid
+
+DIRS=\
+	src\
+
+TARG=\
+	guide\
+	readme\
+
+BINTARG=${TARG:%=$BIN/%}
+
+all:V:		$TARG
+
+install:V:	$BINTARG
+
+$BIN/guide : guide
+	rm -f $BIN/guide && cp guide $BIN/guide
+
+$BIN/readme : readme
+	rm -f $BIN/readme && cp readme $BIN/readme
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/acme/acid/readme
@@ -1,0 +1,12 @@
+Capital A Acid is a rudimentary acme interface to the debugger acid.
+It uses a win to provide an interactive window for acid.  In that window,
+a couple of extra acme-specific features are enabled:
+
+w(command)
+	runs the command and places its output in a new window.
+	e.g. w(lstk()) places the stack trace in a distinct window.
+
+Also, in any such window, text executed with button 2 is
+presented as input to acid in the main Acid window.  Thus, for
+example, one may evaluate variables presented in a stack trace
+by `executing' it with button 2.
--- /dev/null
+++ b/appl/acme/acme/acid/src/Acid.b
@@ -1,0 +1,31 @@
+implement Acid;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+
+Acid : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys := load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	if (len argl < 2) {
+		sys->fprint(stderr, "usage : Acid pid\n");
+		return;
+	}
+	cmd := "/acme/dis/win";
+	file := cmd + ".dis";
+	c := load Command file;
+	if(c == nil) {
+		sys->fprint(stderr, "%s: %r\n", cmd);
+		return;
+	}
+	argl = "-l" :: argl;
+	argl = "acid" :: argl;
+	argl = "/acme/dis/Acid0" :: argl;
+	argl = cmd :: argl;
+	c->init(ctxt, argl);
+}
--- /dev/null
+++ b/appl/acme/acme/acid/src/Acid0.b
@@ -1,0 +1,86 @@
+implement Acidb;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+FD : import sys;
+Iobuf : import bufio;
+
+Acidb : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(nil : ref Draw->Context, nil : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	# TBS main(argl);
+}
+
+False : con 0;
+True : con 1;
+
+EVENTSIZE : con 256;
+
+Event : adt {
+	c1, c2, q0, q1, flag, nb, nr : int;
+	b : array of byte;
+	r : array of int;
+	# TBS byte	b[EVENTSIZE*UTFmax+1];
+	# TBS Rune	r[EVENTSIZE+1];
+};
+
+Win : adt {
+		winid : int;
+		addr : int;
+		body : ref Iobuf;
+		ctl : int;
+		data : int;
+		event : int;
+		buf : array of byte;
+		# TBS byte	buf[512];
+		bufp : int;
+		nbuf : int;
+
+		wnew : fn(w : ref Win);
+		wwritebody : fn(w : ref Win, s : array of byte, n : int);
+		wread : fn(w : ref Win, m : int, n : int, s : array of byte);
+		wclean : fn(w : ref Win);
+		wname : fn(w : ref Win, s : array of byte);
+		wdormant : fn(w : ref Win);
+		wevent : fn(w : ref Win, e : ref Event);
+		wtagwrite : fn(w : ref Win, s : array of byte, n : int);
+		wwriteevent : fn(w : ref Win, e : ref Event);
+		wslave : fn(w : ref Win, c : chan of Event);
+		wreplace : fn(w : ref Win, s : array of byte, b : array of byte, n : int);
+		wselect : fn(w : ref Win, s : array of byte);
+		wdel : fn(w : ref Win, n : int) : int;
+		wreadall : fn(w : ref Win) : (int, array of byte);
+
+		ctlwrite : fn(w : ref Win, s : array of byte);
+		getec : fn(w : ref Win) : int;
+		geten : fn(w : ref Win) : int;
+		geter : fn(w : ref Win, s : array of byte, r : array of int) : int;
+		openfile : fn(w : ref Win, b : array of byte) : int;
+		openbody : fn(w : ref Win, n : int);
+};
+
+Awin : adt {
+	w : Win;
+
+	slave : fn(w : ref Awin, s : array of byte, c : chan of int);
+	new : fn(w : ref Awin, s : array of byte);
+	command : fn(w : ref Awin, s : array of byte) : int;
+	send : fn(w : ref Awin, m : int, s : array of byte, n : int);
+};
+
+srvfd : ref FD;
+stdin : ref FD;
+srvenv : array of byte;
+# TBS byte	srvenv[64];
+
+srvc : chan of array of byte;
--- /dev/null
+++ b/appl/acme/acme/acid/src/mkfile
@@ -1,0 +1,20 @@
+<../../../../../mkconfig
+
+TARG=\
+	Acid.dis\
+	Acid0.dis\
+#	acid.dis\
+#	awin.dis\
+#	util.dis\
+#	win.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	sh.m\
+	sys.m\
+	draw.m\
+
+DISBIN=$ROOT/acme/acid
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/acme/acme/bin/guide
@@ -1,0 +1,5 @@
+win
+new command ...
+aspell file
+adiff file1 file2
+adict -d oed
--- /dev/null
+++ b/appl/acme/acme/bin/mkfile
@@ -1,0 +1,24 @@
+<../../../../mkconfig
+
+BIN=$ROOT/acme/bin
+
+DIRS=\
+	src\
+
+TARG=\
+	guide\
+	readme\
+
+BINTARG=${TARG:%=$BIN/%}
+
+all:V:		$TARG
+
+install:V:	$BINTARG
+
+$BIN/guide : guide
+	rm -f $BIN/guide && cp guide $BIN/guide
+
+$BIN/readme : readme
+	rm -f $BIN/readme && cp readme $BIN/readme
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/acme/bin/readme
@@ -1,0 +1,25 @@
+This directory and its subdirectory $cputype are always mounted at
+the end of /bin for programs run from acme.  They hold a collection
+of small acme-specific applications:
+
+win [command]
+	Create an acme window to serve as a terminal, analogous
+	to xterm.  By default, it runs the shell, rc, but it works with
+	any interactive program, e.g. hoc.  Within the window,
+	commands executed with button 2 are 'executed' by sending
+	their text to the standard input of the command, appending
+	a newline if necessary.
+new command
+	Run the non-interactive command, placing its standard and
+	diagnostic output in a new window.
+aspell file
+	Run spell on the file, labeling the output with addresses so
+	misspelled words can be found in context using button 3.
+adiff file1 file2
+	Run diff on the files, labeling the output with addresses so
+	changes can be found in context using button 3.
+adict
+	Interactive version of dict(1).  Button 3 looks up words and
+	may be applied to any word in any adict window.
+	When a word has multiple definitions, indicate the number
+	(as in acme Mail) to disambiguate.
--- /dev/null
+++ b/appl/acme/acme/bin/src/adiff.b
@@ -1,0 +1,155 @@
+implement Adiff;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+include "workdir.m";
+include "bufio.m";
+
+Adiff : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+
+Context : import Draw;
+OREAD, OWRITE, QTDIR, FD, FORKFD, open, read, write, sprint, fprint, stat, fildes, dup, pctl : import sys;
+
+init(ctxt : ref Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir := load Workdir Workdir->PATH;
+	stderr := fildes(2);
+	if (len argl != 3) {
+		fprint(stderr, "usage: adiff file1 file2\n");
+		return;
+	}
+	ncfd := open("/chan/new/ctl", OREAD);
+	if (ncfd == nil) {
+		fprint(stderr, "cannot open ctl file\n");
+		return;
+	}
+	b := array[128] of byte;
+	n := read(ncfd, b, len b);
+	id := string int string b[0:n];
+	f1 := hd tl argl;
+	f2 := hd tl tl argl;
+	(ok1, d1) := stat(f1);
+	if (ok1 < 0) {
+		fprint(stderr, "cannot stat %s\n", f1);
+		return;
+	}
+	(ok2, d2) := stat(f2);
+	if (ok2 < 0) {
+		fprint(stderr, "cannot stat %s\n", f2);
+		return;
+	}
+	if (d1.qid.qtype & QTDIR)
+		f1 = f1 + "/" + basename(f2);
+	else if (d2.qid.qtype & QTDIR)
+		f2 = f2 + "/" + basename(f1);
+	buf := "/chan/" + id + "/ctl";
+	icfd := open(buf, OWRITE);
+	if (icfd == nil) {
+		fprint(stderr, "cannot open control file\n");
+		return;
+	}
+	buf = "name " + workdir->init() + "/-diff-" + f1 + "\n";
+	b = array of byte buf;
+	write(icfd, b, len b);
+
+	fds := array[2] of ref FD;
+	if (sys->pipe(fds) < 0) {
+		fprint(stderr, "can't pipe\n");
+		return;
+	}
+	buf = "/chan/" + id + "/body";
+	bfd := open(buf, OWRITE);
+	if (bfd == nil) {
+		fprint(stderr, "cannot open body file\n");
+		return;
+	}
+	spawn diff(fds[1], f1, f2, ctxt);
+	fds[1] = nil;
+	awk(fds[0], bfd, f1, f2);
+	b = array of byte "clean\n";
+	write(icfd, b, len b);
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+	
+strchrs(s, pat : string) : int
+{
+	for (i := 0; i < len s; i++)
+		if (strchr(pat, s[i]) >= 0)
+			return i;
+	return -1;
+}
+
+awk(ifd, ofd : ref FD, f1, f2 : string)
+{
+	bufio := load Bufio Bufio->PATH;
+	Iobuf : import bufio;
+	b := bufio->fopen(ifd, OREAD);
+	while ((s := b.gets('\n')) != nil) {
+		if (s[0] >= '1' && s[0] <= '9') {
+			if ((n := strchrs(s, "acd")) >= 0)
+				s = f1 + ":" + s[0:n] + " " + s[n:n+1] + " " + f2 + ":" + s[n+1:];
+		}
+		fprint(ofd, "%s", s);
+	}
+}
+
+diff(ofd : ref FD, f1, f2 : string, ctxt : ref Context)
+{
+	args : list of string;
+
+	pctl(FORKFD, nil);
+	fd := open("/dev/null", OREAD);
+	dup(fd.fd, 0);
+	fd = nil;
+	dup(ofd.fd, 1);
+	dup(1, 2);
+	ofd = nil;
+	args = nil;
+	args = f2 :: args;
+	args = f1 :: args;
+	args = "diff" :: args;
+	exec("diff", args, ctxt);
+	exit;
+}
+
+exec(cmd : string, argl : list of string, ctxt : ref Context)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(fildes(2), "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(ctxt, argl);
+}
+
+basename(s : string) : string
+{
+	for (i := len s -1; i >= 0; --i)
+		if (s[i] == '/')
+			return s[i+1:];
+	return s;
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/agrep.b
@@ -1,0 +1,46 @@
+implement Agrep;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+
+Agrep : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys := load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	cmd := "grep";
+	file := cmd + ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	argl = tl argl;
+	argl = rev(argl);
+	argl = "/dev/null" :: argl;
+	argl = rev(argl);
+	argl = "-n" :: argl;
+	argl = cmd :: argl;
+	c->init(ctxt, argl);
+}
+
+rev(a : list of string) : list of string
+{
+	b : list of string;
+
+	for ( ; a != nil; a = tl a)
+		b = hd a :: b;
+	return b;
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/awd.b
@@ -1,0 +1,45 @@
+implement Awd;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+
+Awd : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+workdir : Workdir;
+
+FD, OWRITE, open, write : import sys;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	n : int;
+	fd : ref FD;
+	buf, dir, str : string;
+	ab : array of byte;
+
+	sys = load Sys Sys->PATH;
+	workdir = load Workdir Workdir->PATH;
+	fd = open("/dev/acme/ctl", OWRITE);
+	if(fd == nil)
+		exit;
+	dir = workdir->init();
+	buf = "name " + dir;
+	n = len buf;
+	if(n>0 && buf[n-1] !='/')
+		buf[n++] = '/';
+	buf[n++] = '-';
+	if(tl argl != nil)
+		str = hd tl argl;
+	else
+		str = "rc";
+	buf += str + "\n";
+	ab = array of byte buf;
+	write(fd, ab, len ab);
+	buf = "dumpdir " + dir + "\n";
+	ab = array of byte buf;
+	write(fd, ab, len ab);
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/cd.b
@@ -1,0 +1,70 @@
+implement Cd;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+
+Cd : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+workdir : Workdir;
+
+FD, OREAD, OWRITE, open, read, write, chdir, fildes, fprint : import sys;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	n : int;
+	fd, stderr : ref FD;
+	buf, dir, str : string;
+	ab : array of byte;
+
+	sys = load Sys Sys->PATH;
+	stderr = fildes(2);
+	argl = tl argl;
+	if (argl == nil)
+		argl = "/usr/" + user() :: nil;
+	if (tl argl != nil) {
+		fprint(stderr, "Usage: cd [directory]\n");
+		exit;
+	}
+	if (chdir(hd argl) < 0) {
+		fprint(stderr, "cd: %s: %r\n", hd argl);
+		exit;
+	}
+
+	workdir = load Workdir Workdir->PATH;
+	fd = open("/dev/acme/ctl", OWRITE);
+	if(fd == nil)
+		exit;
+	dir = workdir->init();
+	buf = "name " + dir;
+	n = len buf;
+	if(n>0 && buf[n-1] !='/')
+		buf[n++] = '/';
+	buf[n++] = '-';
+	if(tl argl != nil)
+		str = hd tl argl;
+	else
+		str = "sh";
+	buf += str + "\n";
+	ab = array of byte buf;
+	write(fd, ab, len ab);
+	buf = "dumpdir " + dir + "\n";
+	ab = array of byte buf;
+	write(fd, ab, len ab);
+	exit;
+}
+
+user(): string
+{
+	fd := open("/dev/user", OREAD);
+	if(fd == nil)
+		return "inferno";
+	buf := array[Sys->NAMEMAX] of byte;
+	n := read(fd, buf, len buf);
+	if(n <= 0)
+		return "inferno";
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/mkfile
@@ -1,0 +1,22 @@
+<../../../../../mkconfig
+
+TARG=\
+	win.dis\
+	winm.dis\
+	adiff.dis\
+	agrep.dis\
+	new.dis\
+	spout.dis\
+	awd.dis\
+	cd.dis\
+
+MODULES=\
+	
+SYSMODULES=\
+	sh.m\
+	sys.m\
+	draw.m\
+
+DISBIN=$ROOT/acme/dis
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/acme/acme/bin/src/new.b
@@ -1,0 +1,70 @@
+implement New;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+include "workdir.m";
+
+New : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir := load Workdir Workdir->PATH;
+	if (len argl <= 1)
+		return;
+	ncfd := sys->open("/mnt/acme/new/ctl", Sys->OREAD);
+	if (ncfd == nil)
+		return;
+	b := array[128] of byte;
+	n := sys->read(ncfd, b, len b);
+	id := string int string b[0:n];
+	buf := "/mnt/acme/" + id + "/ctl";
+	icfd := sys->open(buf, Sys->OWRITE);
+	if (icfd == nil)
+		return;
+	base := hd tl argl;
+	for (i := len base - 1; i >= 0; --i)
+		if (base[i] == '/') {
+			base = base[i+1:];
+			break;
+	}
+	buf = "name " + workdir->init() + "/-" + base + "\n";
+	b = array of byte buf;
+	sys->write(icfd, b, len b);
+	buf = "/mnt/acme/" + id + "/body";
+	bfd := sys->open(buf, Sys->OWRITE);
+	if (bfd == nil)
+		return;
+	sys->dup(bfd.fd, 1);
+	sys->dup(1, 2);
+	spawn exec(hd tl argl, tl argl, ctxt);
+	b = array of byte "clean\n";
+	sys->write(icfd, b, len b);
+}
+
+exec(cmd : string, argl : list of string, ctxt : ref Draw->Context)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(sys->fildes(2), "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(ctxt, argl);
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/spout.b
@@ -1,0 +1,143 @@
+implement Spout;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+Spout : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+
+OREAD, OWRITE, ORDWR, FORKNS, FORKFD, NEWPGRP, MREPL, FD, UTFmax, pctl, open, read, write, fprint, sprint, fildes, bind, dup, byte2char, utfbytes : import sys;
+Iobuf : import bufio;
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);	
+}
+
+bout : ref Iobuf;
+
+main(argv : list of string)
+{
+	fd : ref FD;
+
+	bout = bufio->fopen(stdout, OWRITE);
+	if(len argv == 1)
+		spout(stdin, "");
+	else
+		for(argv = tl argv; argv != nil; argv = tl argv){
+			fd = open(hd argv, OREAD);
+			if(fd == nil){
+				fprint(stderr, "spell: can't open %s: %r\n", hd argv);
+				continue;
+			}
+			spout(fd, hd argv);
+			fd = nil;
+		}
+	exit;
+}
+
+alpha(c : int) : int
+{
+	return ('a'<=(c) && (c)<='z') || ('A'<=(c) && (c)<='Z');
+}
+
+b : ref Iobuf;
+
+spout(fd : ref FD, name : string)
+{
+	s, buf : string;
+	t, w : int;
+	inword, wordchar : int;
+	n, wn, c, m : int;
+
+	b = bufio->fopen(fd, OREAD);
+	n = 0;
+	wn = 0;
+	while((s = b.gets('\n')) != nil){
+		if(s[len s-1] != '\n')
+			s[len s] = '\n';
+		if(s[0] == '.') {
+			for(c=0; c<3 && c < len s && s[c]>' '; c++)
+				n++;
+			s = s[c:];
+		}
+		inword = 0;
+		w = 0;
+		t = 0;
+		do{
+			c = s[t];
+			wordchar = 0;
+			if(alpha(c))
+				wordchar = 1;
+			if(inword && !wordchar){
+				if(c=='\'' && alpha(s[t+1])) {
+					n++;
+					t++;
+					continue;
+				}
+				m = t-w;
+				if(m > 1){
+					buf = s[w:w+m];
+					bout.puts(sprint("%s:#%d,#%d:%s\n", name, wn, n, buf));
+				}
+				inword = 0;
+			}else if(!inword && wordchar){
+				wn = n;
+				w = t;
+				inword = 1;
+			}
+			if(c=='\\' && (alpha(s[t+1]) || s[t+1]=='(')){
+				case(s[t+1]){
+				'(' =>
+					m = 4;
+					break;
+				'f' =>
+					if(s[t+2] == '(')
+						m = 5;
+					else
+						m = 3;
+					break;
+				's' =>
+					if(s[t+2] == '+' || s[t+2]=='-'){
+						if(s[t+3] == '(')
+							m = 6;
+						else
+							m = 4;
+					}else{
+						if(s[t+2] == '(')
+							m = 5;
+						else if(s[t+2]=='1' || s[t+2]=='2' || s[t+2]=='3')
+							m = 4;
+						else
+							m = 3;
+					}
+					break;
+				* =>
+					m = 2;
+				}
+				while(m-- > 0){
+					if(s[t] == '\n')
+						break;
+					n++;
+					t++;
+				}
+				continue;
+			}
+			n++;
+			t ++;
+		}while(c != '\n');
+	}
+	bout.flush();
+}
--- /dev/null
+++ b/appl/acme/acme/bin/src/win.b
@@ -1,0 +1,770 @@
+implement Win;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+include "sh.m";
+include "env.m";
+
+sys : Sys;
+workdir : Workdir;
+env: Env;
+
+OREAD, OWRITE, ORDWR, FORKNS, FORKENV, FORKFD, NEWPGRP, MREPL, FD, UTFmax, pctl, open, read, write, fprint, sprint, fildes, bind, dup, byte2char, utfbytes : import sys;
+
+Win : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+Runeself : con 16r80;
+
+PNPROC, PNGROUP : con iota;
+
+stdout, stderr : ref FD;
+
+drawctxt : ref Draw->Context;
+finish : chan of int;
+
+Lock : adt {
+		c : chan of int;
+
+		init : fn() : ref Lock;
+		lock : fn(l : self ref Lock);
+		unlock : fn(l : self ref Lock);
+};
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir = load Workdir Workdir->PATH;
+	env = load Env Env->PATH;
+	drawctxt = ctxt;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	debuginit();
+	finish = chan[1] of int;
+	spawn main(argl);
+	<-finish;
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(chan[1] of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	l.c <-= 1;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	<-l.c;
+}
+
+dlock : ref Lock;
+debfd : ref Sys->FD;
+
+debuginit()
+{
+	# debfd = sys->create("./debugwin", Sys->OWRITE, 8r600);
+	# dlock = Lock.init();
+}
+
+debugpr(nil : string)
+{
+	# fprint(debfd, "%s", s);
+}
+
+debug(nil : string)
+{
+	# dlock.lock();
+	# fprint(debfd, "%s", s);	
+	# dlock.unlock();
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(drawctxt, argl);
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	# fd := open("/prog/" + string pid + "/ctl", OWRITE);
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+
+	fd = nil;
+	return 0;
+}
+
+sysname(): string
+{
+	fd := sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return nil;
+	return string buf[0:n];
+}
+
+EVENTSIZE : con	256;
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+blank : ref Event;
+
+pid : int;
+# pgrpfd : ref FD;
+parentpid : int;
+
+typing : array of byte;
+ntypeb : int;
+ntyper : int;
+ntypebreak : int;
+
+Q : adt {
+	l : ref Lock;
+	p : int;
+	k : int;
+};
+
+q : Q;
+
+newevent(n : int) : ref Event
+{
+	e := ref Event;
+	e.b = array[n*UTFmax+1] of byte;
+	e.r = array[n+1] of int;
+	return e;
+}
+
+main(argv : list of string)
+{
+	program : list of string;
+	fd, ctlfd, eventfd, addrfd, datafd : ref FD;
+	id : int;
+	c : chan of int;
+	name : string;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	q.l = Lock.init();
+	blank = newevent(2);
+	blank.c1 = 'M';
+	blank.c2 = 'X';
+	blank.q0 = blank.q1 = blank.flag = 0;
+	blank.nb = blank.nr = 1;
+	blank.b[0] = byte ' ';
+	blank.b[1] = byte 0;
+	blank.r[0] = ' ';
+	blank.r[1] = 0;
+	pctl(FORKNS|NEWPGRP, nil);
+	parentpid = pctl(0, nil);
+	program = nil;
+	if(tl argv != nil)
+		program = tl argv;
+	name = nil;
+	if(program == nil){
+		# program = "-i" :: program;
+		program = "sh" :: program;
+		name = sysname();
+	}
+	if(name == nil){
+		prog := hd program;
+		for (n := len prog - 1; n >= 0; n--)
+			if (prog[n] == '/')
+				break;
+		if(n >= 0)
+			name = prog[n+1:];
+		else
+			name = prog;
+		argl := tl argv;
+		if (argl != nil) {
+			for(argl = tl argl; argl != nil && len(name)+1+len(hd argl)<16; argl = tl argl)
+				name += "_" + hd argl;
+		}
+	}
+	if(bind("#|", "/dev/acme", MREPL) < 0)
+		error("pipe");
+	ctlfd = open("/chan/new/ctl", ORDWR);
+	buf := array[12] of byte;
+	if(ctlfd==nil || read(ctlfd, buf, 12)!=12)
+		error("ctl");
+	id = int string buf;
+	buf = nil;
+	env->setenv("acmewin", string id);
+	b := sprint("/chan/%d/tag", id);
+	fd = open(b, OWRITE);
+	write(fd, array of byte " Send Delete", 12);
+	fd = nil;
+	b = sprint("/chan/%d/event", id);
+	eventfd = open(b, ORDWR);
+	b = sprint("/chan/%d/addr", id);
+	addrfd = open(b, ORDWR);
+	b = sprint("/chan/%d/data", id);
+	datafd = open(b, ORDWR); # OCEXEC
+	if(eventfd==nil || addrfd==nil || datafd==nil)
+		error("data files");
+	c = chan of int;
+	spawn run(program, id, c);
+	pid = <-c;
+	# b = sprint("/prog/%d/notepg", pid);
+	# pgrpfd = open(b, OWRITE); # OCEXEC
+	# if(pgrpfd == nil)
+	#	fprint(stdout, "warning: win can't open notepg: %r\n");
+	c <-= 1;
+	fd = open("/dev/acme/data", ORDWR);
+	if(fd == nil)
+		error("/dev/acme/data");
+	wd  := workdir->init();
+	# b = sprint("name %s/-%s\n0\n", wd, name);
+	b = sprint("name %s/-%s\n", wd, name);
+	ab := array of byte b;
+	write(ctlfd, ab, len ab);
+	b = sprint("dumpdir %s/\n", wd);
+	ab = array of byte b;
+	write(ctlfd, ab, len ab);
+	b = sprint("dump %s\n", onestring(argv));
+	ab = array of byte b;
+	write(ctlfd, ab, len ab);
+	ab = nil;
+	spawn stdinx(fd, ctlfd, eventfd, addrfd, datafd);
+	stdoutx(fd, addrfd, datafd);
+}
+
+run(argv : list of string, id : int, c : chan of int)
+{
+	fd0, fd1 : ref FD;
+
+	pctl(FORKENV|FORKFD|NEWPGRP, nil);	# had RFMEM
+	c <-= pctl(0, nil);
+	<-c;
+	pctl(FORKNS, nil);
+	if(bind("/dev/acme/data1", "/dev/cons", MREPL) < 0){
+		fprint(stderr, "can't bind /dev/cons: %r\n");
+		exit;
+	}
+	fd0 = open("/dev/cons", OREAD);
+	fd1 = open("/dev/cons", OWRITE);
+	if(fd0==nil || fd1==nil){
+		fprint(stderr, "can't open /dev/cons: %r\n");
+		exit;
+	}
+	dup(fd0.fd, 0);
+	dup(fd1.fd, 1);
+	dup(fd1.fd, 2);
+	fd0 = fd1 = nil;
+	b := sprint("/chan/%d", id);
+	if(bind(b, "/dev/acme", MREPL) < 0)
+		error("bind /dev/acme");
+	if(bind(sprint("/chan/%d/consctl", id), "/dev/consctl", MREPL) < 0)
+	 	error("bind /dev/consctl");
+	exec(hd argv, argv);
+	exit;
+}
+
+killing : int = 0;
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "win: %s: %r\n", s);
+	if (killing)
+		return;
+	killing = 1;
+	s = "kill";
+	if(pid)
+		postnote(PNGROUP, pid, s);
+		# write(pgrpfd, array of byte "hangup", 6);
+	postnote(PNGROUP, parentpid, s);
+	finish <-= 1;
+	exit;
+}
+
+buff := array[8192] of byte;
+bufp : int;
+nbuf : int;
+
+onestring(argv : list of string) : string
+{
+	s : string;
+
+	if(argv == nil)
+		return "";
+	for( ; argv != nil; argv = tl argv){
+		s += hd argv;
+		if (tl argv != nil)
+			s += " ";
+	}
+	return s;
+}
+
+getec(efd : ref FD) : int
+{
+	if(nbuf == 0){
+		nbuf = read(efd, buff, len buff);
+		if(nbuf <= 0)
+			error(nil);
+		bufp = 0;
+	}
+	--nbuf;
+	return int buff[bufp++];
+}
+
+geten(efd : ref FD) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=getec(efd)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+geter(efd : ref FD, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = getec(efd);
+	buf[0] = byte r;
+	n = 1;
+	if(r < Runeself)
+		return (r, n);
+	for (;;) {
+		(r, m, ok) = byte2char(buf[0:n], 0);
+		if (m > 0)
+			return (r, n);
+		buf[n++] = byte getec(efd);
+	}
+	return (0, 0);
+}
+
+gete(efd : ref FD, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = getec(efd);
+	e.c2 = getec(efd);
+	e.q0 = geten(efd);
+	e.q1 = geten(efd);
+	e.flag = geten(efd);
+	e.nr = geten(efd);
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = geter(efd, e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	if(getec(efd) != '\n')
+		error("event syntax 2");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n, r, b, ok : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) = byte2char(s, i);
+		if (b == 0)
+			error("not full string in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+stdinx(fd0 : ref FD, cfd : ref FD, efd : ref FD, afd : ref FD, dfd : ref FD)
+{
+	e, e2, e3, e4 : ref Event;
+
+	e = newevent(EVENTSIZE);
+	e2 = newevent(EVENTSIZE);
+	e3 = newevent(EVENTSIZE);
+	e4 = newevent(EVENTSIZE);
+	for(;;){
+		gete(efd, e);
+		q.l.lock();
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>
+			case(e.c2){
+			'R' =>
+				addtype(' ', ntyper, e.b, e.nb, e.nr);
+				sendtype(fd0, 1);
+				break;
+			'I' =>
+				if(e.q0 < q.p)
+					q.p += e.q1-e.q0;
+				else if(e.q0 <= q.p+ntyper)
+					typex(e, fd0, afd, dfd);
+				break;
+			'D' =>
+				q.p -= delete(e);
+				break;
+			'x' or 'X' =>
+				if(e.flag & 2)
+					gete(efd, e2);
+				if(e.flag & 8){
+					gete(efd, e3);
+					gete(efd, e4);
+				}
+				if(e.flag&1 || (e.c2=='x' && e.nr==0 && e2.nr==0)){
+					# send it straight back 
+					fprint(efd, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+					break;
+				}
+				if(e.q0==e.q1 && (e.flag&2)){
+					e2.flag = e.flag;
+					*e = *e2;
+				}
+				if(e.flag & 8){
+					if(e.q1 != e.q0){
+						send(e, fd0, cfd, afd, dfd, 0);
+						send(blank, fd0, cfd, afd, dfd, 0);
+					}
+					send(e3, fd0, cfd, afd, dfd, 1);
+				}else	 if(e.q1 != e.q0)
+					send(e, fd0, cfd, afd, dfd, 1);
+				break;
+			'l' or 'L' =>
+				# just send it back 
+				if(e.flag & 2)
+					gete(efd, e2);
+				fprint(efd, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+		q.l.unlock();
+	}
+}
+
+stdoutx(fd1 : ref FD, afd : ref FD, dfd : ref FD)
+{
+	n, m, w, npart : int;
+	s, t : int;
+	buf, hold, x : array of byte;
+	r, ok : int;
+
+	buf = array[8192+UTFmax+1] of byte;
+	hold = array[UTFmax] of byte;
+	npart = 0;
+	for(;;){
+		n = read(fd1, buf[npart:], 8192);
+		if(n < 0)
+			error(nil);
+		if(n == 0)
+			continue;
+
+		# squash NULs 
+		for (s = 0; s < n; s++)
+			if (buf[npart+s] == byte 0)
+				break;
+		if(s < n){
+			for(t=s; s<n; s++)
+				if(buf[npart+t] == buf[npart+s])	# assign = 
+					t++;
+			n = t;
+		}
+
+		n += npart;
+
+		# hold on to final partial rune 
+		npart = 0;
+		while(n>0 && (int buf[n-1]&16rC0)){
+			--n;
+			npart++;
+			if((int buf[n]&16rC0)!=16r80){
+				if(utfbytes(buf[n:], npart) > 0){
+					(r, w, ok) = byte2char(buf, n);
+					n += w;
+					npart -= w;
+				}
+				break;
+			}
+		}
+		if(n > 0){
+			hold[0:] = buf[n:n+npart];
+			buf[n] = byte 0;
+			q.l.lock();
+			str := sprint("#%d", q.p);
+			x = array of byte str;
+			m = len x;
+			if(write(afd, x, m) != m)
+				error("stdout writing address");
+			x = nil;
+			if(write(dfd, buf, n) != n)
+				error("stdout writing body");
+			q.p += nrunes(buf, n);
+			q.l.unlock();
+			buf[0:] = hold[0:npart];
+		}
+	}
+}
+
+delete(e : ref Event) : int
+{
+	q0, q1 : int;
+	deltap : int;
+
+	q0 = e.q0;
+	q1 = e.q1;
+	if(q1 <= q.p)
+		return e.q1-e.q0;
+	if(q0 >= q.p+ntyper)
+		return 0;
+	deltap = 0;
+	if(q0 < q.p){
+		deltap = q.p-q0;
+		q0 = 0;
+	}else
+		q0 -= q.p;
+	if(q1 > q.p+ntyper)
+		q1 = ntyper;
+	else
+		q1 -= q.p;
+	deltype(q0, q1);
+	return deltap;
+}
+
+addtype(c : int, p0 : int, b : array of byte, nb : int, nr : int)
+{
+	i, w : int;
+	r, ok : int;
+	p : int;
+	b0 : int;
+
+	for(i=0; i<nb; i+=w){
+		(r, w, ok) = byte2char(b, i);
+		if(r==16r7F && c=='K'){
+			if (pid)
+				postnote(PNGROUP, pid, "kill");
+				# write(pgrpfd, array of byte "interrupt", 9);
+			# toss all typing 
+			q.p += ntyper+nr;
+			ntypebreak = 0;
+			ntypeb = 0;
+			ntyper = 0;
+			# buglet:  more than one delete ignored 
+			return;
+		}
+		if(r=='\n' || r==16r04)
+			ntypebreak++;
+	}
+	ot := typing;
+	typing = array[ntypeb+nb] of byte;
+	if(typing == nil)
+		error("realloc");
+	if (ot != nil)
+		typing[0:] = ot[0:ntypeb];
+	ot = nil;
+	if(p0 == ntyper)
+		typing[ntypeb:] = b[0:nb];
+	else{
+		b0 = 0;
+		for(p=0; p<p0 && b0<ntypeb; p++){
+			(r, w, ok) = byte2char(typing[b0:], i);
+			b0 += w;
+		}
+		if(p != p0)
+			error("typing: findrune");
+		typing[b0+nb:] = typing[b0:ntypeb];
+		typing[b0:] = b[0:nb];
+	}
+	ntypeb += nb;
+	ntyper += nr;
+}
+
+sendtype(fd0 : ref FD, raw : int)
+{
+	while(ntypebreak){
+		brkc := 0;
+		i := 0;
+		while(i<ntypeb){
+			if(typing[i]==byte '\n' || typing[i]==byte 16r04){
+				n := i + (typing[i] == byte '\n');
+				i++;
+				if(write(fd0, typing, n) != n)
+					error("sending to program");
+				nr := nrunes(typing, i);
+				if (!raw)
+					q.p += nr;
+				ntyper -= nr;
+				ntypeb -= i;
+				typing[0:] = typing[i:i+ntypeb];
+				i = 0;
+				ntypebreak--;
+				brkc = 1;
+			}else
+				i++;
+		}
+		if (!brkc) {
+			fprint(stdout, "no breakchar\n");
+			ntypebreak = 0;
+		}
+	}
+}
+
+deltype(p0 : int, p1 : int)
+{
+	w : int;
+	p, b0, b1 : int;
+	r, ok : int;
+
+	# advance to p0 
+	b0 = 0;
+	for(p=0; p<p0 && b0<ntypeb; p++){
+		(r, w, ok) = byte2char(typing, b0);
+		b0 += w;
+	}
+	if(p != p0)
+		error("deltype 1");
+	# advance to p1 
+	b1 = b0;
+	for(; p<p1 && b1<ntypeb; p++){
+		(r, w, ok) = byte2char(typing, b1);
+		b1 += w;
+		if(r=='\n' || r==16r04)
+			ntypebreak--;
+	}
+	if(p != p1)
+		error("deltype 2");
+	typing[b0:] = typing[b1:ntypeb];
+	ntypeb -= b1-b0;
+	ntyper -= p1-p0;
+}
+
+typex(e : ref Event, fd0 : ref FD, afd : ref FD, dfd : ref FD)
+{
+	m, n, nr : int;
+	buf : array of byte;
+
+	if(e.nr > 0)
+		addtype(e.c1, e.q0-q.p, e.b, e.nb, e.nr);
+	else{
+		buf = array[128] of byte;
+		m = e.q0;
+		while(m < e.q1){
+			str := sprint("#%d", m);
+			b := array of byte str;
+			n = len b;
+			write(afd, b, n);
+			b = nil;
+			n = read(dfd, buf, len buf);
+			nr = nrunes(buf, n);
+			while(m+nr > e.q1){
+				do; while(n>0 && (int buf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			addtype(e.c1, m-q.p, buf, n, nr);
+			m += nr;
+		}
+	}
+	buf = nil;
+	sendtype(fd0, 0);
+}
+
+send(e : ref Event, fd0 : ref FD, cfd : ref FD, afd : ref FD, dfd : ref FD, donl : int)
+{
+	l, m, n, nr, lastc, end : int;
+	abuf, buf : array of byte;
+
+	buf = array[128] of byte;
+	end = q.p+ntyper;
+	str := sprint("#%d", end);
+	abuf = array of byte str;
+	l = len abuf;
+	write(afd, abuf, l);
+	abuf = nil;
+	if(e.nr > 0){
+		write(dfd, e.b, e.nb);
+		addtype(e.c1, ntyper, e.b, e.nb, e.nr);
+		lastc = e.r[e.nr-1];
+	}else{
+		m = e.q0;
+		lastc = 0;
+		while(m < e.q1){
+			str = sprint("#%d", m);
+			abuf = array of byte str;
+			n = len abuf;
+			write(afd, abuf, n);
+			abuf = nil;
+			n = read(dfd, buf, len buf);
+			nr = nrunes(buf, n);
+			while(m+nr > e.q1){
+				do; while(n>0 && (int buf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			str = sprint("#%d", end);
+			abuf = array of byte str;
+			l = len abuf;
+			write(afd, abuf, l);
+			abuf = nil;
+			write(dfd, buf, n);
+			addtype(e.c1, ntyper, buf, n, nr);
+			lastc = int buf[n-1];
+			m += nr;
+			end += nr;
+		}
+	}
+	if(donl && lastc!='\n'){
+		write(dfd, array of byte "\n", 1);
+		addtype(e.c1, ntyper, array of byte "\n", 1, 1);
+	}
+	write(cfd, array of byte "dot=addr", 8);
+	sendtype(fd0, 0);
+	buf = nil;
+}
+
--- /dev/null
+++ b/appl/acme/acme/bin/src/winm.b
@@ -1,0 +1,791 @@
+implement Win;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+include "sh.m";
+
+sys : Sys;
+workdir : Workdir;
+
+OREAD, OWRITE, ORDWR, FORKNS, FORKENV, FORKFD, NEWPGRP, MREPL, FD, UTFmax, pctl, open, read, write, fprint, sprint, fildes, bind, dup, byte2char, utfbytes : import sys;
+
+Win : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+Runeself : con 16r80;
+
+PNPROC, PNGROUP : con iota;
+
+stdout, stderr : ref FD;
+
+drawctxt : ref Draw->Context;
+
+Lock : adt {
+		cnt : int;
+		chann : chan of int;
+
+		init : fn() : ref Lock;
+		lock : fn(l : self ref Lock);
+		unlock : fn(l : self ref Lock);
+};
+
+lc, uc : chan of ref Lock;
+lockpid : int;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir = load Workdir Workdir->PATH;
+	drawctxt = ctxt;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	lc = chan of ref Lock;
+	uc = chan of ref Lock;
+	spawn lockmgr();
+	debuginit();
+	main(argl);
+}
+
+lockmgr()
+{
+	l : ref Lock;
+
+	lockpid = pctl(0, nil);
+	for (;;) {
+		alt {
+			l = <- lc =>
+				if (l.cnt++ == 0)
+					l.chann <-= 1;
+			l = <- uc =>
+				if (--l.cnt > 0)
+					l.chann <-= 1;
+		}
+	}
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	lc <-= l;
+	<- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	uc <-= l;
+}
+
+dlock : ref Lock;
+debfd : ref Sys->FD;
+
+debuginit()
+{
+	# debfd = sys->create("./debugwin", Sys->OWRITE, 8r600);
+	# dlock = Lock.init();
+}
+
+debugpr(nil : string)
+{
+	# fprint(debfd, "%s", s);
+}
+
+debug(nil : string)
+{
+	# dlock.lock();
+	# fprint(debfd, "%s", s);	
+	# dlock.unlock();
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(drawctxt, argl);
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	# fd := open("/prog/" + string pid + "/ctl", OWRITE);
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+
+	fd = nil;
+	return 0;
+}
+
+sysname(): string
+{
+	fd := sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return nil;
+	return string buf[0:n];
+}
+
+EVENTSIZE : con	256;
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+blank : ref Event;
+
+pid : int;
+# pgrpfd : ref FD;
+parentpid : int;
+
+typing : array of byte;
+ntypeb : int;
+ntyper : int;
+ntypebreak : int;
+
+Q : adt {
+	l : ref Lock;
+	p : int;
+	k : int;
+};
+
+q : Q;
+
+newevent(n : int) : ref Event
+{
+	e := ref Event;
+	e.b = array[n*UTFmax+1] of byte;
+	e.r = array[n+1] of int;
+	return e;
+}
+
+main(argv : list of string)
+{
+	spawn main1(argv);
+	exit;
+}
+
+main1(argv : list of string)
+{
+	program : list of string;
+	fd, ctlfd, eventfd, addrfd, datafd : ref FD;
+	id : int;
+	c : chan of int;
+	name : string;
+
+	q.l = Lock.init();
+	blank = newevent(2);
+	blank.c1 = 'M';
+	blank.c2 = 'X';
+	blank.q0 = blank.q1 = blank.flag = 0;
+	blank.nb = blank.nr = 1;
+	blank.b[0] = byte ' ';
+	blank.b[1] = byte 0;
+	blank.r[0] = ' ';
+	blank.r[1] = 0;
+	pctl(FORKNS|NEWPGRP, nil);
+	parentpid = pctl(0, nil);
+	program = nil;
+	if(tl argv != nil)
+		program = tl argv;
+	name = nil;
+	if(program == nil){
+		# program = "-i" :: program;
+		program = "sh" :: program;
+		name = sysname();
+	}
+	if(name == nil){
+		prog := hd program;
+		for (n := len prog - 1; n >= 0; n--)
+			if (prog[n] == '/')
+				break;
+		if(n >= 0)
+			name = prog[n+1:];
+		else
+			name = prog;
+		argl := tl argv;
+		if (argl != nil) {
+			for(argl = tl argl; argl != nil && len(name)+1+len(hd argl)<16; argl = tl argl)
+				name += "_" + hd argl;
+		}
+	}
+	if(bind("#|", "/dev/acme", MREPL) < 0)
+		error("pipe");
+	ctlfd = open("/chan/new/ctl", ORDWR);
+	buf := array[12] of byte;
+	if(ctlfd==nil || read(ctlfd, buf, 12)!=12)
+		error("ctl");
+	id = int string buf;
+	buf = nil;
+	b := sprint("/chan/%d/tag", id);
+	fd = open(b, OWRITE);
+	write(fd, array of byte " Send Delete", 12);
+	fd = nil;
+	b = sprint("/chan/%d/event", id);
+	eventfd = open(b, ORDWR);
+	b = sprint("/chan/%d/addr", id);
+	addrfd = open(b, ORDWR);
+	b = sprint("/chan/%d/data", id);
+	datafd = open(b, ORDWR); # OCEXEC
+	if(eventfd==nil || addrfd==nil || datafd==nil)
+		error("data files");
+	c = chan of int;
+	spawn run(program, id, c);
+	pid = <-c;
+	# b = sprint("/prog/%d/notepg", pid);
+	# pgrpfd = open(b, OWRITE); # OCEXEC
+	# if(pgrpfd == nil)
+	#	fprint(stdout, "warning: win can't open notepg: %r\n");
+	c <-= 1;
+	fd = open("/dev/acme/data", ORDWR);
+	if(fd == nil)
+		error("/dev/acme/data");
+	wd  := workdir->init();
+	# b = sprint("name %s/-%s\n0\n", wd, name);
+	b = sprint("name %s/-%s\n", wd, name);
+	ab := array of byte b;
+	write(ctlfd, ab, len ab);
+	b = sprint("dumpdir %s/\n", wd);
+	ab = array of byte b;
+	write(ctlfd, ab, len ab);
+	b = sprint("dump %s\n", onestring(argv));
+	ab = array of byte b;
+	write(ctlfd, ab, len ab);
+	ab = nil;
+	spawn stdinx(fd, ctlfd, eventfd, addrfd, datafd);
+	stdoutx(fd, addrfd, datafd);
+}
+
+run(argv : list of string, id : int, c : chan of int)
+{
+	fd0, fd1 : ref FD;
+
+	pctl(FORKENV|FORKFD|NEWPGRP, nil);	# had RFMEM
+	c <-= pctl(0, nil);
+	<-c;
+	pctl(FORKNS, nil);
+	if(bind("/dev/acme/data1", "/dev/cons", MREPL) < 0){
+		fprint(stderr, "can't bind /dev/cons: %r\n");
+		exit;
+	}
+	fd0 = open("/dev/cons", OREAD);
+	fd1 = open("/dev/cons", OWRITE);
+	if(fd0==nil || fd1==nil){
+		fprint(stderr, "can't open /dev/cons: %r\n");
+		exit;
+	}
+	dup(fd0.fd, 0);
+	dup(fd1.fd, 1);
+	dup(fd1.fd, 2);
+	fd0 = fd1 = nil;
+	b := sprint("/chan/%d", id);
+	if(bind(b, "/dev/acme", MREPL) < 0)
+		error("bind /dev/acme");
+	if(bind(sprint("/chan/%d/consctl", id), "/dev/consctl", MREPL) < 0)
+	 	error("bind /dev/consctl");
+	exec(hd argv, argv);
+	exit;
+}
+
+killing : int = 0;
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "win: %s: %r\n", s);
+	if (killing)
+		return;
+	killing = 1;
+	s = "kill";
+	if(pid)
+		postnote(PNGROUP, pid, s);
+		# write(pgrpfd, array of byte "hangup", 6);
+	postnote(PNPROC, lockpid, s);
+	postnote(PNGROUP, parentpid, s);
+	exit;
+}
+
+buff := array[8192] of byte;
+bufp : int;
+nbuf : int;
+
+onestring(argv : list of string) : string
+{
+	s : string;
+
+	if(argv == nil)
+		return "";
+	for( ; argv != nil; argv = tl argv){
+		s += hd argv;
+		if (tl argv != nil)
+			s += " ";
+	}
+	return s;
+}
+
+getec(efd : ref FD) : int
+{
+	if(nbuf == 0){
+		nbuf = read(efd, buff, len buff);
+		if(nbuf <= 0)
+			error(nil);
+		bufp = 0;
+	}
+	--nbuf;
+	return int buff[bufp++];
+}
+
+geten(efd : ref FD) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=getec(efd)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+geter(efd : ref FD, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = getec(efd);
+	buf[0] = byte r;
+	n = 1;
+	if(r < Runeself)
+		return (r, n);
+	for (;;) {
+		(r, m, ok) = byte2char(buf[0:n], 0);
+		if (m > 0)
+			return (r, n);
+		buf[n++] = byte getec(efd);
+	}
+	return (0, 0);
+}
+
+gete(efd : ref FD, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = getec(efd);
+	e.c2 = getec(efd);
+	e.q0 = geten(efd);
+	e.q1 = geten(efd);
+	e.flag = geten(efd);
+	e.nr = geten(efd);
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = geter(efd, e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	if(getec(efd) != '\n')
+		error("event syntax 2");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n, r, b, ok : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) = byte2char(s, i);
+		if (b == 0)
+			error("not full string in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+stdinx(fd0 : ref FD, cfd : ref FD, efd : ref FD, afd : ref FD, dfd : ref FD)
+{
+	e, e2, e3, e4 : ref Event;
+
+	e = newevent(EVENTSIZE);
+	e2 = newevent(EVENTSIZE);
+	e3 = newevent(EVENTSIZE);
+	e4 = newevent(EVENTSIZE);
+	for(;;){
+		gete(efd, e);
+		q.l.lock();
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>
+			case(e.c2){
+			'R' =>
+				addtype(' ', ntyper, e.b, e.nb, e.nr);
+				sendtype(fd0, 1);
+				break;
+			'I' =>
+				if(e.q0 < q.p)
+					q.p += e.q1-e.q0;
+				else if(e.q0 <= q.p+ntyper)
+					typex(e, fd0, afd, dfd);
+				break;
+			'D' =>
+				q.p -= delete(e);
+				break;
+			'x' or 'X' =>
+				if(e.flag & 2)
+					gete(efd, e2);
+				if(e.flag & 8){
+					gete(efd, e3);
+					gete(efd, e4);
+				}
+				if(e.flag&1 || (e.c2=='x' && e.nr==0 && e2.nr==0)){
+					# send it straight back 
+					fprint(efd, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+					break;
+				}
+				if(e.q0==e.q1 && (e.flag&2)){
+					e2.flag = e.flag;
+					*e = *e2;
+				}
+				if(e.flag & 8){
+					if(e.q1 != e.q0){
+						send(e, fd0, cfd, afd, dfd, 0);
+						send(blank, fd0, cfd, afd, dfd, 0);
+					}
+					send(e3, fd0, cfd, afd, dfd, 1);
+				}else	 if(e.q1 != e.q0)
+					send(e, fd0, cfd, afd, dfd, 1);
+				break;
+			'l' or 'L' =>
+				# just send it back 
+				if(e.flag & 2)
+					gete(efd, e2);
+				fprint(efd, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+		q.l.unlock();
+	}
+}
+
+stdoutx(fd1 : ref FD, afd : ref FD, dfd : ref FD)
+{
+	n, m, w, npart : int;
+	s, t : int;
+	buf, hold, x : array of byte;
+	r, ok : int;
+
+	buf = array[8192+UTFmax+1] of byte;
+	hold = array[UTFmax] of byte;
+	npart = 0;
+	for(;;){
+		n = read(fd1, buf[npart:], 8192);
+		if(n < 0)
+			error(nil);
+		if(n == 0)
+			continue;
+
+		# squash NULs 
+		for (s = 0; s < n; s++)
+			if (buf[npart+s] == byte 0)
+				break;
+		if(s < n){
+			for(t=s; s<n; s++)
+				if(buf[npart+t] == buf[npart+s])	# assign = 
+					t++;
+			n = t;
+		}
+
+		n += npart;
+
+		# hold on to final partial rune 
+		npart = 0;
+		while(n>0 && (int buf[n-1]&16rC0)){
+			--n;
+			npart++;
+			if((int buf[n]&16rC0)!=16r80){
+				if(utfbytes(buf[n:], npart) > 0){
+					(r, w, ok) = byte2char(buf, n);
+					n += w;
+					npart -= w;
+				}
+				break;
+			}
+		}
+		if(n > 0){
+			hold[0:] = buf[n:n+npart];
+			buf[n] = byte 0;
+			q.l.lock();
+			str := sprint("#%d", q.p);
+			x = array of byte str;
+			m = len x;
+			if(write(afd, x, m) != m)
+				error("stdout writing address");
+			x = nil;
+			if(write(dfd, buf, n) != n)
+				error("stdout writing body");
+			q.p += nrunes(buf, n);
+			q.l.unlock();
+			buf[0:] = hold[0:npart];
+		}
+	}
+}
+
+delete(e : ref Event) : int
+{
+	q0, q1 : int;
+	deltap : int;
+
+	q0 = e.q0;
+	q1 = e.q1;
+	if(q1 <= q.p)
+		return e.q1-e.q0;
+	if(q0 >= q.p+ntyper)
+		return 0;
+	deltap = 0;
+	if(q0 < q.p){
+		deltap = q.p-q0;
+		q0 = 0;
+	}else
+		q0 -= q.p;
+	if(q1 > q.p+ntyper)
+		q1 = ntyper;
+	else
+		q1 -= q.p;
+	deltype(q0, q1);
+	return deltap;
+}
+
+addtype(c : int, p0 : int, b : array of byte, nb : int, nr : int)
+{
+	i, w : int;
+	r, ok : int;
+	p : int;
+	b0 : int;
+
+	for(i=0; i<nb; i+=w){
+		(r, w, ok) = byte2char(b, i);
+		if(r==16r7F && c=='K'){
+			if (pid)
+				postnote(PNGROUP, pid, "kill");
+				# write(pgrpfd, array of byte "interrupt", 9);
+			# toss all typing 
+			q.p += ntyper+nr;
+			ntypebreak = 0;
+			ntypeb = 0;
+			ntyper = 0;
+			# buglet:  more than one delete ignored 
+			return;
+		}
+		if(r=='\n' || r==16r04)
+			ntypebreak++;
+	}
+	ot := typing;
+	typing = array[ntypeb+nb] of byte;
+	if(typing == nil)
+		error("realloc");
+	if (ot != nil)
+		typing[0:] = ot[0:ntypeb];
+	ot = nil;
+	if(p0 == ntyper)
+		typing[ntypeb:] = b[0:nb];
+	else{
+		b0 = 0;
+		for(p=0; p<p0 && b0<ntypeb; p++){
+			(r, w, ok) = byte2char(typing[b0:], i);
+			b0 += w;
+		}
+		if(p != p0)
+			error("typing: findrune");
+		typing[b0+nb:] = typing[b0:ntypeb];
+		typing[b0:] = b[0:nb];
+	}
+	ntypeb += nb;
+	ntyper += nr;
+}
+
+sendtype(fd0 : ref FD, raw : int)
+{
+	i, n, nr : int;
+
+	while(ntypebreak){
+		brkc := 0;
+		for(i=0; i<ntypeb; i++)
+			if(typing[i]==byte '\n' || typing[i]==byte 16r04){
+				n = i + (typing[i] == byte '\n');
+				i++;
+				if(write(fd0, typing, n) != n)
+					error("sending to program");
+				nr = nrunes(typing, i);
+				if (!raw)
+					q.p += nr;
+				ntyper -= nr;
+				ntypeb -= i;
+				typing[0:] = typing[i:i+ntypeb];
+				ntypebreak--;
+				brkc = 1;
+			}
+		if (!brkc) {
+			fprint(stdout, "no breakchar\n");
+			ntypebreak = 0;
+		}
+	}
+}
+
+deltype(p0 : int, p1 : int)
+{
+	w : int;
+	p, b0, b1 : int;
+	r, ok : int;
+
+	# advance to p0 
+	b0 = 0;
+	for(p=0; p<p0 && b0<ntypeb; p++){
+		(r, w, ok) = byte2char(typing, b0);
+		b0 += w;
+	}
+	if(p != p0)
+		error("deltype 1");
+	# advance to p1 
+	b1 = b0;
+	for(; p<p1 && b1<ntypeb; p++){
+		(r, w, ok) = byte2char(typing, b1);
+		b1 += w;
+		if(r=='\n' || r==16r04)
+			ntypebreak--;
+	}
+	if(p != p1)
+		error("deltype 2");
+	typing[b0:] = typing[b1:ntypeb];
+	ntypeb -= b1-b0;
+	ntyper -= p1-p0;
+}
+
+typex(e : ref Event, fd0 : ref FD, afd : ref FD, dfd : ref FD)
+{
+	m, n, nr : int;
+	buf : array of byte;
+
+	if(e.nr > 0)
+		addtype(e.c1, e.q0-q.p, e.b, e.nb, e.nr);
+	else{
+		buf = array[128] of byte;
+		m = e.q0;
+		while(m < e.q1){
+			str := sprint("#%d", m);
+			b := array of byte str;
+			n = len b;
+			write(afd, b, n);
+			b = nil;
+			n = read(dfd, buf, len buf);
+			nr = nrunes(buf, n);
+			while(m+nr > e.q1){
+				do; while(n>0 && (int buf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			addtype(e.c1, m-q.p, buf, n, nr);
+			m += nr;
+		}
+	}
+	buf = nil;
+	sendtype(fd0, 0);
+}
+
+send(e : ref Event, fd0 : ref FD, cfd : ref FD, afd : ref FD, dfd : ref FD, donl : int)
+{
+	l, m, n, nr, lastc, end : int;
+	abuf, buf : array of byte;
+
+	buf = array[128] of byte;
+	end = q.p+ntyper;
+	str := sprint("#%d", end);
+	abuf = array of byte str;
+	l = len abuf;
+	write(afd, abuf, l);
+	abuf = nil;
+	if(e.nr > 0){
+		write(dfd, e.b, e.nb);
+		addtype(e.c1, ntyper, e.b, e.nb, e.nr);
+		lastc = e.r[e.nr-1];
+	}else{
+		m = e.q0;
+		lastc = 0;
+		while(m < e.q1){
+			str = sprint("#%d", m);
+			abuf = array of byte str;
+			n = len abuf;
+			write(afd, abuf, n);
+			abuf = nil;
+			n = read(dfd, buf, len buf);
+			nr = nrunes(buf, n);
+			while(m+nr > e.q1){
+				do; while(n>0 && (int buf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			str = sprint("#%d", end);
+			abuf = array of byte str;
+			l = len abuf;
+			write(afd, abuf, l);
+			abuf = nil;
+			write(dfd, buf, n);
+			addtype(e.c1, ntyper, buf, n, nr);
+			lastc = int buf[n-1];
+			m += nr;
+			end += nr;
+		}
+	}
+	if(donl && lastc!='\n'){
+		write(dfd, array of byte "\n", 1);
+		addtype(e.c1, ntyper, array of byte "\n", 1, 1);
+	}
+	write(cfd, array of byte "dot=addr", 8);
+	sendtype(fd0, 0);
+	buf = nil;
+}
+
--- /dev/null
+++ b/appl/acme/acme/edit/guide
@@ -1,0 +1,4 @@
+e file | x '/regexp/' | c 'replacement'
+e 'file:0,$' | x '/.*word.*\n/' | p -n
+e file | pipe command args ...
+New /absolute/file/name
--- /dev/null
+++ b/appl/acme/acme/edit/mkfile
@@ -1,0 +1,24 @@
+<../../../../mkconfig
+
+BIN=$ROOT/acme/edit
+
+DIRS=\
+	src\
+
+TARG=\
+	guide\
+	readme\
+
+BINTARG=${TARG:%=$BIN/%}
+
+all:V:		$TARG
+
+install:V:	$BINTARG
+
+$BIN/guide : guide
+	rm -f $BIN/guide && cp guide $BIN/guide
+
+$BIN/readme : readme
+	rm -f $BIN/readme && cp readme $BIN/readme
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/acme/edit/readme
@@ -1,0 +1,31 @@
+The programs collected in /acme/edit offer a sam-like command interface
+to acme windows.  The guide file
+	/acme/edit/guide
+holds templates for several editing operations implemented
+by external programs.  These programs, composed in
+a pipeline, refine the sections of a file to be modified.
+Thus in sam when one says
+	x/.*\n/ g/foo/ p
+in /acme/edit one runs
+	x '/.*\n/' | g '/foo/' | p
+The e command, unrelated to e in sam, disambiguates file names, collects
+lists of names, etc., and produces input suitable for the other tools.
+For example:
+	e '/usr/rob/acme:0,$' | x /oldname/ | c /newname/
+changes oldname to newname in all the files loaded in acme whose names match
+the literal text /usr/rob/acme.
+
+The commands in /acme/edit are
+	e
+	x
+	g
+	c
+	d
+	p
+	pipe	(like sam's | , which can't be used for syntactic reasons)
+
+p takes a -n flag analogous to grep's -n.  There is no s command.
+e has a -l flag to produce line numbers instead of the default character numbers.
+Its implementation is poor but sufficient for the mundane job of recreating
+the occasional line number for tools like acid; its use with the other commands
+in this directory is discouraged.
--- /dev/null
+++ b/appl/acme/acme/edit/src/a.b
@@ -1,0 +1,89 @@
+implement Aa;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Aa : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "a";
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id : int;
+	nf, n, seq, rlen : int;
+	f, tf : array of File;
+	buf, s : string;
+
+	if(len argv != 2){
+		fprint(stderr, "usage: %s 'replacement'\n", prog);
+		exit;
+	}
+
+include "input.b";
+
+	# sort back to original order, backwards
+	qsort(f, nf, BSCMP);
+
+	# change
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	ab := array of byte hd tl argv;
+	rlen = len ab;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = nil;
+				cfd = nil;
+				dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			if(write(cfd, array of byte "mark\nnomark\n", 12) != 12)
+				rerror("setting nomark");
+		}
+		if(fprint(afd, "#%d", f[i].q1) < 0)
+			rerror("writing address");
+		if(write(dfd, ab, rlen) != rlen)
+			rerror("writing replacement");
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/c.b
@@ -1,0 +1,104 @@
+implement Cc;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Cc : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stderr = fildes(2);
+	main(argl);
+}
+
+prog := "c";
+
+include "findfile.b";
+
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+ 	i, j, id : int;
+	r : string;
+	nf, n, seq, rlen : int;
+	f, tf : array of File;
+	buf, s : string;
+
+	if(len argv != 2){
+		fprint(stderr, "usage: %s 'replacement'\n", prog);
+		exit;
+	}
+
+include "input.b";
+
+	# sort back to original order, backwards
+	qsort(f, nf, BSCMP);
+
+	# change
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	argv1 := hd tl argv;
+	rlen = len argv1;
+	r = argv1;
+	i = 0;
+	for(j=0; j<rlen; j++){
+		r[i] = argv1[j];
+		if(i>0 && r[i-1]=='\\'){
+			if(r[i] == 'n')
+				r[--i] = '\n';
+			else if(r[i]=='\\')
+				r[--i] = '\\';
+		}
+		i++;
+	}
+	rlen = i;
+	ab := array of byte r[0:rlen];
+	rlen = len ab;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			if(write(cfd, array of byte "mark\nnomark\n", 12) != 12)
+				rerror("setting nomark");
+		}
+		if(fprint(afd, "#%d,#%d", f[i].q0, f[i].q1) < 0)
+			rerror("writing address");
+		if(write(dfd, ab, rlen) != rlen)
+			rerror("writing replacement");
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/d.b
@@ -1,0 +1,30 @@
+implement Dd;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+
+Dd : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys := load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	if (len argl != 1) {
+		sys->fprint(stderr, "usage : d\n");
+		return;
+	}
+	cmd := "/acme/edit/c";
+	file := cmd + ".dis";
+	c := load Command file;
+	if(c == nil) {
+		sys->fprint(stderr, "%s: %r\n", cmd);
+		return;
+	}
+	argl = nil;
+	argl = "" :: argl;
+	argl = cmd :: argl;
+	c->init(ctxt, argl);
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/e.b
@@ -1,0 +1,154 @@
+implement Ee;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Ee : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "e";
+
+main(argv : list of string)
+{
+	afd, cfd : ref FD;
+	i, id : int;
+	buf : string;
+	nf, n, lines, l0, l1 : int;
+	f, tf : array of File;
+
+	lines = 0;
+	if(len argv>1 && hd tl argv == "-l"){
+		lines = 1;
+		argv = tl argv;
+	}
+	if(len argv < 2){
+		fprint(stderr, "usage: %s 'file[:address]' ...\n", prog);
+		exit;
+	}
+	nf = 0;
+	f = nil;
+	for(argv = tl argv; argv != nil; argv = tl argv){
+		(n, tf) = findfile(hd argv);
+		if(n == 0)
+			errors("no files match pattern", hd argv);
+		oldf := f;
+		f = array[n+nf] of File;
+		if(f == nil)
+			rerror("out of memory");
+		if (oldf != nil) {
+			f[0:] = oldf[0:nf];
+			oldf = nil;
+		}
+		f[nf:] = tf[0:n];
+		nf += n;
+		tf = nil;
+	}
+
+	# convert to character positions
+	for(i=0; i<nf; i++){
+		id = f[i].id;
+		buf = sprint("/mnt/acme/%d/addr", id);
+		afd = open(buf, ORDWR);
+		if(afd == nil)
+			rerror(buf);
+		buf = sprint("/mnt/acme/%d/ctl", id);
+		cfd = open(buf, ORDWR);
+		if(cfd == nil)
+			rerror(buf);
+		if(write(cfd, array of byte "addr=dot\n", 9) != 9)
+			rerror("setting address to dot");
+		ab := array of byte f[i].addr;
+		if(write(afd, ab, len ab) != len ab){
+			fprint(stderr, "%s: %s:%s is invalid address\n", prog, f[i].name, f[i].addr);
+			f[i].ok = 0;
+			afd = nil;
+			cfd = nil;
+			continue;
+		}
+		seek(afd, big 0, 0);
+		ab = array[24] of byte;
+		if(read(afd, ab, len ab) != 2*12)
+			rerror("reading address");
+		afd = nil;
+		cfd = nil;
+		buf = string ab;
+		ab = nil;
+		f[i].q0 = int buf;
+		f[i].q1 = int buf[12:];
+		f[i].ok = 1;
+	}
+
+	# sort
+	qsort(f, nf, FCMP);
+
+	# print
+	for(i=0; i<nf; i++){
+		if(f[i].ok)
+			if(lines){
+				(l0, l1) = lineno(f[i]);
+				if(l1 > l0)
+					fprint(stdout, "%s:%d,%d\n", f[i].name, l0, l1);
+				else
+					fprint(stdout, "%s:%d\n", f[i].name, l0);
+			}else{
+				if(f[i].q1 > f[i].q0)
+					fprint(stdout, "%s:#%d,#%d\n", f[i].name, f[i].q0, f[i].q1);
+				else
+					fprint(stdout, "%s:#%d\n", f[i].name, f[i].q0);
+			}
+	}
+	exit;
+}
+
+lineno(f : File) : (int, int)
+{
+	b : ref Iobuf;
+	n0, n1, q, r : int;
+	buf : string;
+
+	buf = sprint("/mnt/acme/%d/body", f.id);
+	b = bufio->open(buf, bufio->OREAD);
+	if(b == nil){
+		fprint(stderr, "%s: can't open %s: %r\n", prog, buf);
+		exit;
+	}
+	n0 = 1;
+	n1 = 1;
+	for(q=0; q<f.q1; q++){
+		r = b.getc();
+		if(r == bufio->EOF){
+			fprint(stderr, "%s: early EOF on %s\n", prog, buf);
+			exit;
+		}
+		if(r=='\n'){
+			if(q < f.q0)
+				n0++;
+			if(q+1 < f.q1)
+				n1++;
+		}
+	}
+	b.close();
+	return (n0, n1);
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/findfile.b
@@ -1,0 +1,210 @@
+File : adt {
+	id : int;
+	seq : int;
+	ok : int;
+	q0, q1 : int;
+	name : string;
+	addr : string;
+};
+
+BSCMP, SCMP, NCMP, FCMP : con iota;
+
+indexfile := "/mnt/acme/index";
+
+dfd: ref Sys->FD;
+debug(s : string)
+{
+	if (dfd == nil)
+		dfd = sys->create("/usr/jrf/acme/debugedit", Sys->OWRITE, 8r600);
+	sys->fprint(dfd, "%s", s);
+}
+
+error(s : string)
+{
+	fprint(stderr, "%s: %s\n", prog, s);
+	exit;
+}
+
+errors(s, t : string)
+{
+	fprint(stderr, "%s: %s %s\n", prog, s, t);
+	exit;
+}
+
+rerror(s : string)
+{
+	fprint(stderr, "%s: %s: %r\n", prog, s);
+	exit;
+}
+
+strcmp(s, t : string) : int
+{
+	if (s < t) return -1;
+	if (s > t) return 1;
+	return 0;
+}
+
+strstr(s, t : string) : int
+{
+	if (t == nil)
+		return 0;
+	n := len t;
+	if (n > len s)
+		return -1;
+	e := len s - n;
+	for (p := 0; p <= e; p++)
+		if (s[p:p+n] == t)
+			return p;
+	return -1;
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n, r, b, ok : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) = byte2char(s, i);
+		i += b;
+	}
+	return n;
+}
+
+index : ref Iobuf;
+
+findfile(pat : string) : (int, array of File)
+{
+	line, pat1, pat2 : string;
+	colon, blank : int;
+	n : int;
+	f : array of File;
+
+	if(index == nil)
+		index = bufio->open(indexfile, bufio->OREAD);
+	else
+		index.seek(big 0, 0);
+	if(index == nil)
+		rerror(indexfile);
+	for(colon=0; colon < len pat && pat[colon]!=':'; colon++)
+		;
+	if (colon == len pat) {
+		pat1 = pat;
+		pat2 = ".";
+	}
+	else {
+		pat1 = pat[0:colon];
+		pat2 = pat[colon+1:];
+	}
+	n = 0;
+	f = nil;
+	while((line=index.gets('\n')) != nil){
+		if(len line < 5*12)
+			rerror("bad index file format");
+		line = line[0:len line - 1];
+		for(blank=5*12; blank < len line && line[blank]!=' '; blank++)
+			;
+		if (blank < len line)
+			line = line[0:blank];
+		if(strcmp(line[5*12:], pat1) == 0){
+			# exact match: take that
+			f = nil;	# should also free t->addr's
+			f = array[1] of File;
+			if(f == nil)
+				rerror("out of memory");
+			f[0].id = int line;
+			f[0].name = line[5*12:];
+			f[0].addr = pat2;
+			n = 1;
+			break;
+		}
+		if(strstr(line[5*12:], pat1) >= 0){
+			# partial match: add to list
+			off := f;
+			f = array[n+1] of File;
+			if(f == nil)
+				rerror("out of memory");
+			f[0:] = off[0:n];
+			off = nil;
+			f[n].id = int line;
+			f[n].name = line[5*12:];
+			f[n].addr = pat2;
+			n++;
+		}
+	}
+	return (n, f);
+}
+
+bscmp(a : File, b : File) : int
+{
+	return b.seq - a.seq;
+}
+
+scmp(a : File, b : File) : int
+{
+	return a.seq - b.seq;
+}
+
+ncmp(a : File, b : File) : int
+{
+	return strcmp(a.name, b.name);
+}
+
+fcmp(a : File, b : File) : int
+{
+	x : int;
+
+	if (a.name < b.name)
+		return -1;
+	if (a.name > b.name)
+		return 1;
+	x = a.q0 - b.q0;
+	if(x != 0)
+		return x;
+	return a.q1-b.q1;
+}
+
+gencmp(a : File, b : File, c : int) : int
+{
+	if (c == BSCMP)
+		return bscmp(a, b);
+	if (c == SCMP)
+		return scmp(a, b);
+	if (c == NCMP)
+		return ncmp(a, b);
+	if (c == FCMP)
+		return fcmp(a, b);
+	return 0;
+}
+
+qsort(a : array of File, n : int, c : int)
+{
+	i, j : int;
+	t : File;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && gencmp(a[i], a[0], c) < 0);
+			do
+				j--;
+			while(j > 0 && gencmp(a[j], a[0], c) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j, c);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n, c);
+			n = j;
+		}
+	}
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/g.b
@@ -1,0 +1,95 @@
+implement Gg;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Gg : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+prog := "g";
+
+include "findfile.b";
+
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id, seq : int;
+	nf, n, plen : int;
+	f, tf : array of File;
+	buf, s : string;
+
+	if(len argv!=2 || len hd tl argv==0 || (hd tl argv)[0]!='/'){
+		fprint(stderr, "usage: %s '/regexp/'\n", prog);
+		exit;
+	}
+
+include "input.b";
+
+	# execute regexp
+	id = -1;
+	afd = nil;
+	dfd = nil;
+	cfd = nil;
+	bufb := array of byte hd tl argv;
+	plen = len bufb;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+		}
+		ab := array of byte f[i].addr;
+		n = len ab;
+		if(write(afd, ab, n)!=n || fprint(cfd, "limit=addr\n")<0){
+			buf = sprint("%s:%s is invalid limit", f[i].name, f[i].addr);
+			rerror(buf);
+		}
+		if(fprint(afd, "#%d", f[i].q0) < 0)
+			rerror("can't set dot");
+		# look for match
+		if(write(afd, bufb, plen) == plen){
+			if(f[i].q0 == f[i].q1)
+				fprint(stdout, "%s:#%d\n", f[i].name, f[i].q0);
+			else
+				fprint(stdout, "%s:#%d,#%d\n", f[i].name, f[i].q0, f[i].q1);
+		}
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/i.b
@@ -1,0 +1,88 @@
+implement Ii;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Ii : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stderr = fildes(2);
+	main(argl);
+}
+
+prog := "i";
+
+include "findfile.b";
+
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id : int;
+	nf, n, seq, rlen : int;
+	f, tf : array of File;
+	s, buf : string;
+
+	if(len argv != 2){
+		fprint(stderr, "usage: %s 'replacement'\n", prog);
+		exit;
+	}
+	
+include "input.b";
+
+	# sort back to original order, backwards
+	qsort(f, nf, BSCMP);
+
+	# change 
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	ab := array of byte hd tl argv;
+	rlen = len ab;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			if(write(cfd, array of byte "mark\nnomark\n", 12) != 12)
+				rerror("setting nomark");
+		}
+		if(fprint(afd, "#%d", f[i].q0) < 0)
+			rerror("writing address");
+		if(write(dfd, ab, rlen) != rlen)
+			rerror("writing replacement");
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/input.b
@@ -1,0 +1,84 @@
+	bin = bufio->fopen(stdin, bufio->OREAD);
+	seq = 0;
+	nf = 0;
+	f = nil;
+	while((s=bin.gets('\n')) != nil){
+		s = s[0:len s - 1];
+		(n, tf) = findfile(s);
+		if(n == 0)
+			errors("no files match input", s);
+		for(i=0; i<n; i++)
+			tf[i].seq = seq++;
+		off := f;
+		f = array[n+nf] of File;
+		if(f == nil)
+			rerror("out of memory");
+		if (off != nil) {
+			f[0:] = off[0:nf];
+			off = nil;
+		}
+		f[nf:] = tf[0:n];
+		nf += n;
+		tf = nil;
+	}
+
+	# sort by file name
+	qsort(f, nf, NCMP);
+
+	# convert to character positions if necessary
+	for(i=0; i<nf; i++){
+		f[i].ok = 1;
+		# see if it's easy
+		s = f[i].addr;
+		if(s[0]=='#'){
+			s = s[1:];
+			n = 0;
+			while(len s > 0 && '0'<=s[0] && s[0]<='9'){
+				n = n*10+(s[0]-'0');
+				s = s[1:];
+			}
+			f[i].q0 = n;
+			if(len s == 0){
+				f[i].q1 = n;
+				continue;
+			}
+			if(s[0] == ',') {
+				s = s[1:];
+				n = 0;
+				while(len s > 0 && '0'<=s[0] && s[0]<='9'){
+					n = n*10+(s[0]-'0');
+					s = s[1:];
+				}
+				f[i].q1 = n;
+				if(len s == 0)
+					continue;
+			}
+		}
+		id = f[i].id;
+		buf = sprint("/chan/%d/addr", id);
+		afd = open(buf, ORDWR);
+		if(afd == nil)
+			rerror(buf);
+		buf = sprint("/chan/%d/ctl", id);
+		cfd = open(buf, ORDWR);
+		if(cfd == nil)
+			rerror(buf);
+		if(write(cfd, array of byte "addr=dot\n", 9) != 9)
+			rerror("setting address to dot");
+		ab := array of byte f[i].addr;
+		if(write(afd, ab, len ab) != len ab){
+			fprint(stderr, "%s: %s:%s is invalid address\n", prog, f[i].name, f[i].addr);
+			f[i].ok = 0;
+			afd = cfd = nil;
+			continue;
+		}
+		seek(afd, big 0, 0);
+		bbuf := array[2*12] of byte;
+		if(read(afd, bbuf, len bbuf) != 2*12)
+			rerror("reading address");
+		afd = cfd = nil;
+		buf = string bbuf;
+		bbuf = nil;
+		f[i].q0 = int buf;
+		f[i].q1 = int buf[12:];
+	}
--- /dev/null
+++ b/appl/acme/acme/edit/src/mkfile
@@ -1,0 +1,23 @@
+<../../../../../mkconfig
+
+TARG=\
+	a.dis\
+	c.dis\
+	d.dis\
+	e.dis\
+	g.dis\
+	i.dis\
+	p.dis\
+	x.dis\
+	pipe.dis\
+
+MODULES=\
+	
+SYSMODULES=\
+	sh.m\
+	sys.m\
+	draw.m\
+
+DISBIN=$ROOT/acme/edit
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/acme/acme/edit/src/p.b
@@ -1,0 +1,109 @@
+implement Pp;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Pp : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "p";
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id : int;
+	m, nr, nf, n, nflag, seq : int;
+	f, tf : array of File;
+	buf, s : string;
+
+	nflag = 0;
+	if(len argv==2 && hd tl argv == "-n"){
+		argv = tl argv;
+		nflag = 1;
+	}
+	if(len argv != 1){
+		fprint(stderr, "usage: %s [-n]\n", prog);
+		exit;
+	}
+	
+include "input.b";
+
+	# sort back to original order
+	qsort(f, nf, SCMP);
+
+	# print
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+		}
+		if(nflag){
+			if(f[i].q1 > f[i].q0)
+				fprint(stdout, "%s:#%d,#%d: ", f[i].name, f[i].q0, f[i].q1);
+			else
+				fprint(stdout, "%s:#%d: ", f[i].name, f[i].q0);
+		}
+		m = f[i].q0;
+		while(m < f[i].q1){
+			if(fprint(afd, "#%d", m) < 0){
+				fprint(stderr, "%s: %s:%s is invalid address\n", prog, f[i].name, f[i].addr);
+				continue;
+			}
+			bbuf := array[512] of byte;
+			n = read(dfd, bbuf, len buf);
+			nr = nrunes(bbuf, n);
+			while(m+nr > f[i].q1){
+				do; while(n>0 && (int bbuf[--n]&16rC0)==16r80);
+				--nr;
+			}
+			if(n == 0)
+				break;
+			write(stdout, bbuf, n);
+			m += nr;
+		}
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/pipe.b
@@ -1,0 +1,212 @@
+implement Pipe;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "sh.m";
+
+sys : Sys;
+bufio : Bufio;
+
+UTFmax, ORDWR, NEWFD, FD, open, read, write, seek, sprint, fprint, fildes, byte2char, pipe, dup, pctl : import sys;
+Iobuf : import bufio;
+
+Pipe : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stderr : ref FD;
+pipectxt : ref Draw->Context;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	pipectxt = ctxt;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "pipe";
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, dfd, cfd : ref FD;
+	nf, nc, nr, npart : int;
+	p1, p2 : array of ref FD;
+	i, n, id, seq : int;
+	buf : string;
+	tmp, data : array of byte;
+	s : string;
+	r, s0 : int;
+	f, tf : array of File;
+	q, q0, q1 : int;
+	cpid : chan of int;
+	w, ok : int;
+
+	if(len argv < 2){
+		fprint(stderr, "usage: pipe command\n");
+		exit;
+	}
+
+include "input.b";
+
+	# sort back to original order
+	qsort(f, nf, SCMP);
+
+	# pipe
+	id = -1;
+	afd = nil;
+	cfd = nil;
+	dfd = nil;
+	tmp = array[8192+UTFmax] of byte;
+	if(tmp == nil)
+		error("malloc");
+	cpid = chan of int;
+	for(i=0; i<nf; i++){
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			if(write(cfd, array of byte "mark\nnomark\n", 12) != 12)
+				rerror("setting nomark");
+		}
+
+		if(fprint(afd, "#%ud", f[i].q0) < 0)
+			rerror("writing address");
+
+		q0 = f[i].q0;
+		q1 = f[i].q1;
+		# suck up data
+		data = array[(q1-q0)*UTFmax+1] of byte;
+		if(data == nil)
+			error("malloc failed\n");
+		s0 = 0;
+		q = q0;
+		bbuf := array[12] of byte;
+		while(q < q1){
+			nc = read(dfd, data[s0:], (q1-q)*UTFmax);
+			if(nc <= 0)
+				error("read error from acme");
+			seek(afd, big 0, 0);
+			if(read(afd, bbuf, 12) != 12)
+				rerror("reading address");
+			q = int string bbuf;
+			s0 += nc;
+		}
+		bbuf = nil;
+		s0 = 0;
+		for(nr=0; nr<q1-q0; nr++) {
+			(r, w, ok) = byte2char(data, s0);
+			s0 += w;
+		}
+
+		p1 = array[2] of ref FD;
+		p2 = array[2] of ref FD;
+		if(pipe(p1)<0 || pipe(p2)<0)
+			error("pipe");
+
+		spawn run(tl argv, p1[0], p2[1], cpid);
+		<-cpid;
+		p1[0] = nil;
+		p2[1] = nil;
+
+		spawn send(data, s0, p1[1]);
+		p1[1] = nil;
+
+		# put back data
+		if(fprint(afd, "#%d,#%d", q0, q1) < 0)
+			rerror("writing address");
+
+		npart = 0;
+		q1 = q0;
+		while((nc = read(p2[0], tmp[npart:], 8192)) > 0){
+			nc += npart;
+			s0 = 0;
+			while(s0 <= nc-UTFmax){
+				(r, w, ok) = byte2char(tmp, s0);
+				s0 += w;
+				q1++;
+			}
+			if(s0 > 0)
+				if(write(dfd, tmp, s0) != s0)
+					error("write error to acme");
+			npart = nc - s0;
+			tmp[0:] = tmp[s0:s0+npart];
+		}
+		p2[0] = nil;
+		if(npart){
+			s0 = 0;
+			while(s0 < npart){
+				(r, w, ok) = byte2char(tmp, s0);
+				s0 += w;
+				q1++;
+			}
+			if(write(dfd, tmp, npart) != npart)
+				error("write error to acme");
+		}
+		if(fprint(afd, "#%d,#%d", q0, q1) < 0)
+			rerror("writing address");
+		if(fprint(cfd, "dot=addr\n") < 0)
+			rerror("writing dot");
+		data = nil;
+	}
+}
+
+run(argv : list of string, p1, p2 : ref FD, c : chan of int)
+{
+	pctl(NEWFD, 0::1::2::p1.fd::p2.fd::nil);
+	dup(p1.fd, 0);
+	dup(p2.fd, 1);
+	c <-= pctl(0, nil);
+	exec(hd argv, argv);
+	fprint(stderr, "can't exec");
+	exit;
+}
+
+send(buf : array of byte, nbuf : int, fd : ref FD)
+{
+	if(write(fd, buf, nbuf) != nbuf)
+		error("write error to process");
+	fd = nil;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+
+	c->init(pipectxt, argl);
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/x.b
@@ -1,0 +1,123 @@
+implement Xx;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Xx : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+include "findfile.b";
+
+prog := "x";
+bin : ref Iobuf;
+
+main(argv : list of string)
+{
+	afd, cfd, dfd : ref FD;
+	i, id, seq : int;
+	nf, n, plen : int;
+	addr, aq0, aq1, matched : int;
+	f, tf : array of File;
+	buf, s : string;
+	bbuf0 : array of byte;
+
+	if(len argv!=2 || len hd tl argv==0 || (hd tl argv)[0]!='/'){
+		fprint(stderr, "usage: %s '/regexp/'\n", prog);
+		exit;
+	}
+
+include "input.b";
+
+	# execute regexp
+	id = -1;
+	afd = nil;
+	dfd = nil;
+	cfd = nil;
+	for(i=0; i<nf; i++){
+		if(f[i].ok == 0)
+			continue;
+		if(f[i].id != id){
+			if(id > 0){
+				afd = cfd = dfd = nil;
+			}
+			id = f[i].id;
+			buf = sprint("/mnt/acme/%d/addr", id);
+			afd = open(buf, ORDWR);
+			if(afd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/ctl", id);
+			cfd = open(buf, ORDWR);
+			if(cfd == nil)
+				rerror(buf);
+			buf = sprint("/mnt/acme/%d/data", id);
+			dfd = open(buf, ORDWR);
+			if(dfd == nil)
+				rerror(buf);
+		}
+		bbuf0 = array of byte f[i].addr;
+		n = len bbuf0;
+		if(write(afd, bbuf0, n)!=n || fprint(cfd, "limit=addr\n")<0){
+			buf = sprint("%s:%s is invalid limit", f[i].name, f[i].addr);
+			rerror(buf);
+		}
+		if(fprint(afd, "#%d", f[i].q0) < 0)
+			rerror("can't set address");
+		if(fprint(cfd, "dot=addr") < 0)
+			rerror("can't unset dot");
+		addr = f[i].q0-1;
+		bbuf := array of byte hd tl argv;
+		plen = len bbuf;
+		matched = 0;
+		# scan for matches
+		for(;;){
+			if(write(afd, bbuf, plen) != plen)
+				break;
+			seek(afd, big 0, 0);
+			bbuf0 = array[2*12] of byte;
+			if(read(afd, bbuf0, len bbuf0) != 2*12)
+				rerror("reading address");
+			buf = string bbuf0;
+			bbuf0 = nil;
+			aq0 = int buf;
+			aq1 = int buf[12:];
+			if(matched && aq1==aq0 && addr==aq1){	# repeated null match; advance
+				matched = 0;
+				addr++;
+				if(addr > f[i].q1)
+					break;
+				if(fprint(afd, "#%d", addr) < 0)
+					rerror("writing address");
+				continue;
+			}
+			matched = 1;
+			if(aq0<addr || aq0>=f[i].q1 || aq1>f[i].q1)
+				break;
+			addr = aq1;
+			if(aq0 == aq1)
+				fprint(stdout, "%s:#%d\n", f[i].name, aq0);
+			else
+				fprint(stdout, "%s:#%d,#%d\n", f[i].name, aq0, aq1);
+		}
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/edit/src/xxx.b
@@ -1,0 +1,327 @@
+implement Ee;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+sys : Sys;
+bufio : Bufio;
+
+ORDWR, FD, open, read, write, seek, sprint, fprint, fildes, byte2char : import sys;
+Iobuf : import bufio;
+
+Ee : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+stdin, stdout, stderr : ref FD;
+
+init(ctxt : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdin = fildes(0);
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+File : adt {
+	id : int;
+	seq : int;
+	ok : int;
+	q0, q1 : int;
+	name : string;
+	addr : string;
+};
+
+BSCMP, SCMP, NCMP, FCMP : con iota;
+
+indexfile := "/usr/jrf/tmp/index";
+
+dfd: ref Sys->FD;
+debug(s : string)
+{
+	if (dfd == nil)
+		dfd = sys->create("/usr/jrf/acme/debugedit", Sys->OWRITE, 8r600);
+	sys->fprint(dfd, "%s", s);
+}
+
+error(s : string)
+{
+debug(sys->sprint("error %s\n", s));
+	fprint(stderr, "%s: %s\n", prog, s);
+	exit;
+}
+
+errors(s, t : string)
+{
+debug(sys->sprint("errors %s %s\n", s, t));
+	fprint(stderr, "%s: %s %s\n", prog, s, t);
+	exit;
+}
+
+rerror(s : string)
+{
+debug(sys->sprint("rerror %s\n", s));
+	fprint(stderr, "%s: %s: %r\n", prog, s);
+	exit;
+}
+
+strcmp(s, t : string) : int
+{
+	if (s < t) return -1;
+	if (s > t) return 1;
+	return 0;
+}
+
+strstr(s, t : string) : int
+{
+	if (t == nil)
+		return 0;
+	n := len t;
+	if (n > len s)
+		return -1;
+	e := len s - n;
+	for (p := 0; p <= e; p++)
+		if (s[p:p+n] == t)
+			return p;
+	return -1;
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n, r, b, ok : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) = byte2char(s, i);
+		i += b;
+	}
+	return n;
+}
+
+index : ref Iobuf;
+
+findfile(pat : string) : (int, array of File)
+{
+	line, pat1, pat2 : string;
+	colon, blank : int;
+	n : int;
+	f : array of File;
+
+	if(index == nil)
+		index = bufio->open(indexfile, bufio->OREAD);
+	else
+		index.seek(0, 0);
+	if(index == nil)
+		rerror(indexfile);
+	for(colon=0; colon < len pat && pat[colon]!=':'; colon++)
+		;
+	if (colon == len pat) {
+		pat1 = pat;
+		pat2 = ".";
+	}
+	else {
+		pat1 = pat[0:colon];
+		pat2 = pat[colon+1:];
+	}
+	n = 0;
+	f = nil;
+	while((line=index.gets('\n')) != nil){
+		if(len line < 5*12)
+			rerror("bad index file format");
+		line = line[0:len line - 1];
+		for(blank=5*12; blank < len line && line[blank]!=' '; blank++)
+			;
+		if (blank < len line)
+			line = line[0:blank];
+		if(strcmp(line[5*12:], pat1) == 0){
+			# exact match: take that
+			f = nil;	# should also free t->addr's
+			f = array[1] of File;
+			if(f == nil)
+				rerror("out of memory");
+			f[0].id = int line;
+			f[0].name = line[5*12:];
+			f[0].addr = pat2;
+			n = 1;
+			break;
+		}
+		if(strstr(line[5*12:], pat1) >= 0){
+			# partial match: add to list
+			off := f;
+			f = array[n+1] of File;
+			if(f == nil)
+				rerror("out of memory");
+			f[0:] = off[0:n];
+			off = nil;
+			f[n].id = int line;
+			f[n].name = line[5*12:];
+			f[n].addr = pat2;
+			n++;
+		}
+	}
+	return (n, f);
+}
+
+bscmp(a : File, b : File) : int
+{
+	return b.seq - a.seq;
+}
+
+scmp(a : File, b : File) : int
+{
+	return a.seq - b.seq;
+}
+
+ncmp(a : File, b : File) : int
+{
+	return strcmp(a.name, b.name);
+}
+
+fcmp(a : File, b : File) : int
+{
+	x : int;
+
+	if (a.name < b.name)
+		return -1;
+	if (a.name > b.name)
+		return 1;
+	x = a.q0 - b.q0;
+	if(x != 0)
+		return x;
+	return a.q1-b.q1;
+}
+
+gencmp(a : File, b : File, c : int) : int
+{
+	if (c == BSCMP)
+		return bscmp(a, b);
+	if (c == SCMP)
+		return scmp(a, b);
+	if (c == NCMP)
+		return ncmp(a, b);
+	if (c == FCMP)
+		return fcmp(a, b);
+	return 0;
+}
+
+qsort(a : array of File, n : int, c : int)
+{
+	i, j : int;
+	t : File;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && gencmp(a[i], a[0], c) < 0);
+			do
+				j--;
+			while(j > 0 && gencmp(a[j], a[0], c) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j, c);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n, c);
+			n = j;
+		}
+	}
+}
+
+prog := "e";
+
+main(argv : list of string)
+{
+	afd, cfd : ref FD;
+	i, id : int;
+	buf : string;
+	nf, n, lines, l0, l1 : int;
+	f, tf : array of File;
+
+	if(len argv < 2){
+debug(sys->sprint("usage\n"));
+		fprint(stderr, "usage: %s 'file[:address]' ...\n", prog);
+		exit;
+	}
+	nf = 0;
+	f = nil;
+	for(argv = tl argv; argv != nil; argv = tl argv){
+		(n, tf) = findfile(hd argv);
+		if(n == 0)
+			errors("no files match pattern", hd argv);
+		oldf := f;
+		f = array[n+nf] of File;
+		if(f == nil)
+			rerror("out of memory");
+		if (oldf != nil) {
+			f[0:] = oldf[0:nf];
+			oldf = nil;
+		}
+		f[nf:] = tf[0:n];
+		nf += n;
+		tf = nil;
+	}
+debug(sys->sprint("nf=%d\n", nf));
+	# convert to character positions
+	for(i=0; i<nf; i++){
+		id = f[i].id;
+		buf = sprint("/mnt/acme/%d/addr", id);
+		# afd = open(buf, ORDWR);
+		# if(afd == nil)
+			# rerror(buf);
+		buf = sprint("/mnt/acme/%d/ctl", id);
+		# cfd = open(buf, ORDWR);
+		# if(cfd == nil)
+			# rerror(buf);
+		if(0 && write(cfd, array of byte "addr=dot\n", 9) != 9)
+			rerror("setting address to dot");
+		ab := array of byte f[i].addr;
+		if(0 && write(afd, ab, len ab) != len ab){
+			fprint(stderr, "%s: %s:%s is invalid address\n", prog, f[i].name, f[i].addr);
+			f[i].ok = 0;
+			afd = nil;
+			cfd = nil;
+			continue;
+		}
+		# seek(afd, 0, 0);
+		ab = array[24] of byte;
+		if(0 && read(afd, ab, len ab) != 2*12)
+			rerror("reading address");
+		afd = nil;
+		cfd = nil;
+		# buf = string ab;
+		ab = nil;
+		f[i].q0 = 0; 	# int buf;
+		f[i].q1 = 5;		# int buf[12:];
+		f[i].ok = 1;
+debug(sys->sprint("q0=%d q1=%d\n", f[i].q0, f[i].q1));
+	}
+
+	# sort
+	# qsort(f, nf, FCMP);
+
+	# print
+	for(i=0; i<nf; i++){
+		if(f[i].ok)
+			{
+				if(f[i].q1 > f[i].q0)
+					fprint(stdout, "%s:#%d,#%d\n", f[i].name, f[i].q0, f[i].q1);
+				else
+					fprint(stdout, "%s:#%d\n", f[i].name, f[i].q0);
+			}
+	}
+debug("e exiting\n");
+	exit;
+}
--- /dev/null
+++ b/appl/acme/acme/mail/guide
@@ -1,0 +1,5 @@
+Mail /mail/box/$user/stored
+Mail
+Mailpop3
+mkbox /mail/box/$user/new_box
+mail -'x' someaddress
--- /dev/null
+++ b/appl/acme/acme/mail/mkbox.b
@@ -1,0 +1,25 @@
+implement Mkbox;
+
+include "sys.m";
+include "draw.m";
+
+sys : Sys;
+
+FD : import sys;
+
+Mkbox : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+init(nil : ref Draw->Context, argl : list of string)
+{
+	sys = load Sys Sys->PATH;
+	for (argl = tl argl; argl != nil; argl = tl argl) {
+		nm := hd argl;
+		(ok, dir) := sys->stat(nm);
+		if (ok < 0) {
+			fd := sys->create(nm, Sys->OREAD, 8r600);
+			fd = nil;
+		}
+	}
+}
--- /dev/null
+++ b/appl/acme/acme/mail/mkfile
@@ -1,0 +1,34 @@
+<../../../../mkconfig
+
+BIN=$ROOT/acme/mail
+
+DIRS=\
+	src\
+
+TARG=\
+	guide\
+	readme\
+	mkbox.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	sys.m\
+	draw.m\
+
+BINTARG=${TARG:%=$BIN/%}
+
+DISBIN=$ROOT/acme/mail
+
+all:V:		$TARG
+
+install:V:	$BINTARG
+
+$BIN/guide : guide
+	rm -f $BIN/guide && cp guide $BIN/guide
+
+$BIN/readme : readme
+	rm -f $BIN/readme && cp readme $BIN/readme
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/acme/mail/readme
@@ -1,0 +1,29 @@
+Mail is the single program in this directory.  Its argument specifies
+the mail box to read, default /mail/box/$user/mbox.
+For example, running
+	Mail /mail/box/$user/stored
+(a line in the guide file) looks at saved mail.
+
+Mail maintains a window containing headers for all the
+messages in the mailbox and monitors the mailbox for new messages.
+Using button 3 to indicate a message number opens
+a window on that message.   commands in the mailbox window are
+	Put		Write the mailbox back to the file (never done automatically)
+	Mail		Make a new message window ready to mail someone.
+			Takes argument names analogously to acme's New.
+	Del		Exit Mail, after checking that mailbox isn't modified.
+New messages appear at the top of the window and are highlighted upon arrival.
+(The messages are numbered oldest to newest, the opposite of regular mail.)
+
+Message windows have a simple format: the first line, up to the first tab or newline,
+holds the sender or, when sending, the addressee.  Edit the line to change who the
+message goes to.  Message windows contain the commands
+	Reply	Make a new window to compose a reply to this message
+	Delmesg	Delete the message from the screen and from the mailbox
+	Del		Delete the window, leaving the message in the mailbox
+	Post		Send the message to the addressee
+	Save		Save to the named mailbox, default/mail/box/$user/stored
+Save takes a full file name; if that name has no slashes, the file is taken
+to be in /mail/box/$user and must already exist. Use mkbox in the guide to
+create target mailboxes in /mail/box/$user.
+Reply and mail windows contain an obvious subset of the commands.
--- /dev/null
+++ b/appl/acme/acme/mail/src/Mail.b
@@ -1,0 +1,1715 @@
+implement mail;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "daytime.m";
+include "sh.m";
+
+mail : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+daytime : Daytime;
+
+OREAD, OWRITE, ORDWR, NEWFD, FORKFD, FORKENV, NEWPGRP, UTFmax : import Sys;
+FD, Dir : import sys;
+fprint, sprint, sleep, create, open, read, write, remove, stat, fstat, fwstat, fildes, pctl, pipe, dup, byte2char : import sys;
+Context : import Draw;
+EOF : import Bufio;
+Iobuf : import bufio;
+time : import daytime;
+
+DIRLEN : con 116;
+PNPROC, PNGROUP : con iota;
+False : con 0;
+True : con 1;
+EVENTSIZE : con 256;
+Runeself : con 16r80;
+OCEXEC : con 0;
+CHEXCL : con 0; # 16r20000000;	# for the moment
+CHAPPEND : con 0; # 16r40000000;
+
+MAILDIR : con "/mail";
+
+Win : adt {
+	winid : int;
+	addr : ref FD;
+	body : ref Iobuf;
+	ctl : ref FD;
+	data : ref FD;
+	event : ref FD;
+	buf : array of byte;
+	bufp : int;
+	nbuf : int;
+
+	wnew : fn() : ref Win;
+	wwritebody : fn(w : self ref Win, s : string);
+	wread : fn(w : self ref Win, m : int, n : int) : string;
+	wclean : fn(w : self ref Win);
+	wname : fn(w : self ref Win, s : string);
+	wdormant : fn(w : self ref Win);
+	wevent : fn(w : self ref Win, e : ref Event);
+	wshow : fn(w : self ref Win);
+	wtagwrite : fn(w : self ref Win, s : string);
+	wwriteevent : fn(w : self ref Win, e : ref Event);
+	wslave : fn(w : self ref Win, c : chan of Event);
+	wreplace : fn(w : self ref Win, s : string, t : string);
+	wselect : fn(w : self ref Win, s : string);
+	wsetdump : fn(w : self ref Win, s : string, t : string);
+	wdel : fn(w : self ref Win, n : int) : int;
+	wreadall : fn(w : self ref Win) : string;
+
+ 	ctlwrite : fn(w : self ref Win, s : string);
+ 	getec : fn(w : self ref Win) : int;
+ 	geten : fn(w : self ref Win) : int;
+ 	geter : fn(w : self ref Win, s : array of byte) : (int, int);
+ 	openfile : fn(w : self ref Win, s : string) : ref FD;
+ 	openbody : fn(w : self ref Win, n : int);
+};
+
+Mesg : adt {
+	w : ref Win;
+	id : int;
+	hdr : string;
+	realhdr : string;
+	replyto : string;
+	text : string;
+	subj : string;
+	next : cyclic ref Mesg;
+ 	lline1 : int;
+	box : cyclic ref Box;
+	isopen : int;
+	posted : int;
+
+	read : fn(b : ref Box) : ref Mesg;
+	open : fn(m : self ref Mesg);
+	slave : fn(m : self ref Mesg);
+	free : fn(m : self ref Mesg);
+	save : fn(m : self ref Mesg, s : string);
+	mkreply : fn(m : self ref Mesg);
+	mkmail : fn(b : ref Box, s : string);
+	putpost : fn(m : self ref Mesg, e : ref Event);
+
+ 	command : fn(m : self ref Mesg, s : string) : int;
+ 	send : fn(m : self ref Mesg);
+};
+
+Box : adt {
+	w : ref Win;
+	nm : int;
+	readonly : int;
+	m : cyclic ref Mesg;
+	file : string;
+	io : ref Iobuf;
+	clean : int;
+ 	leng : big;
+ 	cdel : chan of ref Mesg;
+	cevent : chan of Event;
+	cmore : chan of int;
+	
+	line : string;
+	peekline : string;
+
+	read : fn(s : string, n : int) : ref Box;
+	readmore : fn(b : self ref Box);
+	readline : fn(b : self ref Box) : string;
+	unreadline : fn(b : self ref Box);
+	slave : fn(b : self ref Box);
+	mopen : fn(b : self ref Box, n : int);
+	rewrite : fn(b : self ref Box);
+	mdel : fn(b : self ref Box, m : ref Mesg);
+	event : fn(b : self ref Box, e : ref Event);
+
+	command : fn(b : self ref Box, s : string) : int;
+};
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+Lock : adt {
+	cnt : int;
+	chann : chan of int;
+
+	init : fn() : ref Lock;
+	lock : fn(l : self ref Lock);
+	unlock : fn(l : self ref Lock);
+};
+
+Ref : adt {
+	l : ref Lock;
+	cnt : int;
+
+	init : fn() : ref Ref;
+	inc : fn(r : self ref Ref) : int;
+};
+
+mbox : ref Box;
+mboxfile : string;
+usermboxfile : string;
+usermboxdir : string;
+lockfile : string;
+user : string;
+date : string;
+mailctxt : ref Context;
+stdout, stderr : ref FD;
+
+killing : int = 0;
+
+init(ctxt : ref Context, argl : list of string)
+{
+	mailctxt = ctxt;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main(argl);
+}
+
+dlock : ref Lock;
+dfd : ref Sys->FD;
+
+debug(s : string)
+{
+	if (dfd == nil) {
+		dfd = sys->create("/usr/jrf/acme/debugmail", Sys->OWRITE, 8r600);
+		dlock = Lock.init();
+	}
+	if (dfd == nil)
+		return;
+	dlock.lock();
+	sys->fprint(dfd, "%s", s);	
+	dlock.unlock();
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+	fd = nil;
+	return 0;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(mailctxt, argl);
+}
+
+swrite(fd : ref FD, s : string) : int
+{
+	ab := array of byte s;
+	m := len ab;
+	p := write(fd, ab, m);
+	if (p == m)
+		return len s;
+	if (p <= 0)
+		return p;
+	return 0;
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+} 
+
+strrchr(s : string, c : int) : int
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+strtoi(s : string) : (int, int)
+{
+	m := 0;
+	neg := 0;
+	t := 0;
+	ls := len s;
+	while (t < ls && (s[t] == ' ' || s[t] == '\t'))
+		t++;
+	if (t < ls && s[t] == '+')
+		t++;
+	else if (t < ls && s[t] == '-') {
+		neg = 1;
+		t++;
+	}
+	while (t < ls && (s[t] >= '0' && s[t] <= '9')) {
+		m = 10*m + s[t]-'0';
+		t++;
+	}
+	if (neg)
+		m = -m;
+	return (m, t);	
+}
+
+access(s : string) : int
+{
+	fd := open(s, 0);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+newevent() : ref Event
+{
+	e := ref Event;
+	e.b = array[EVENTSIZE*UTFmax+1] of byte;
+	e.r = array[EVENTSIZE+1] of int;
+	return e;
+}	
+
+newmesg() : ref Mesg
+{
+	m := ref Mesg;
+	m.id = m.lline1 = m.isopen = m.posted = 0;
+	return m;
+}
+
+lc, uc : chan of ref Lock;
+
+initlock()
+{
+	lc = chan of ref Lock;
+	uc = chan of ref Lock;
+	spawn lockmgr();
+}
+
+lockmgr()
+{
+	l : ref Lock;
+
+	for (;;) {
+		alt {
+			l = <- lc =>
+				if (l.cnt++ == 0)
+					l.chann <-= 1;
+			l = <- uc =>
+				if (--l.cnt > 0)
+					l.chann <-= 1;
+		}
+	}
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	lc <-= l;
+	<- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	uc <-= l;
+}
+
+Ref.init() : ref Ref
+{
+	r := ref Ref;
+	r.l = Lock.init();
+	r.cnt = 0;
+	return r;
+}
+
+Ref.inc(r : self ref Ref) : int
+{
+	r.l.lock();
+	i := r.cnt;
+	r.cnt++;
+	r.l.unlock();
+	return i;
+}
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "mail: %s\n", s);
+	rmlockfile();
+# debug(sprint("error %s\n", s));
+	postnote(PNGROUP, pctl(0, nil), "kill");
+	killing = 1;
+	exit;
+}
+
+rmlockfile()
+{
+	if (lockfile != nil) {
+		remove(lockfile);
+		lockfile = nil;
+	}
+}
+
+#
+#  try opening a lock file.  If it doesn't exist try creating it.
+#
+openlockfile(path : string) : ref FD
+{
+	try : int;
+	fd : ref FD;
+
+	try = 0;
+	for(;;){
+		# fd = open(path, OWRITE);
+		# if(fd!=nil || ++try>3)
+		# 	return fd;
+		if(++try > 3)
+			return fd;
+		(ok, d) := stat(path);
+		if(ok >= 0)
+			sleep(1000);
+		else{
+			fd = create(path, OWRITE, CHEXCL|8r666);
+			if(fd != nil){
+				(ok, d) = fstat(fd);
+				if(ok >= 0){
+					d.mode |= CHEXCL|8r666;
+					fwstat(fd, d);
+				}
+				return fd;
+			}
+			break;
+		}
+	}
+	return nil;
+}
+
+tryopen(s : string, mode : int) : ref FD
+{
+	fd : ref FD;
+	try : int;
+
+	for(try=0; try<3; try++){
+		fd = open(s, mode);
+		if(fd != nil)
+			return fd;
+		sleep(1000);
+	}
+	return nil;
+}
+
+run(argv : list of string, c : chan of int, p0 : ref FD)
+{
+	# pctl(FORKFD|NEWPGRP, nil);	# had RFMEM
+	pctl(FORKENV|NEWFD|NEWPGRP, 0::1::2::p0.fd::nil);
+	c <-= pctl(0, nil);
+	dup(p0.fd, 0);
+	p0 = nil;
+	exec(hd argv, argv);
+	exit;
+}
+
+getuser() : string
+{
+  	fd := open("/dev/user", OREAD);
+  	if(fd == nil)
+    		return "";
+  	buf := array[128] of byte;
+  	n := read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+  	return string buf[0:n];	
+}
+
+main(argv : list of string)
+{
+	fd : ref FD;
+	readonly : int;
+	buf : string;
+
+	initlock();
+	initreply();
+	date = time();
+	if(date==nil)
+		error("can't get current time");
+	user = getuser();
+	if(user == nil)
+		user = "Wile.E.Coyote";
+	usermboxdir = MAILDIR + "/box/" + user + "/";
+	usermboxfile = MAILDIR + "/box/" + user + "/mbox";
+	if(len argv > 1)
+		mboxfile = hd tl argv;
+	else
+		mboxfile = usermboxfile;
+
+	fd = nil;
+	readonly = False;
+	if(mboxfile == usermboxfile){
+		buf = MAILDIR + "/box/" + user + "/L.reading";
+		fd = openlockfile(buf);
+		if(fd == nil){
+			fprint(stderr, "Mail: %s in use; opened read-only\n", mboxfile);
+			readonly = True;
+		}
+		else
+			lockfile = buf;
+	}
+	mbox = mbox.read(mboxfile, readonly);
+	spawn timeslave(fd, mbox, mbox.cmore);
+	mbox.slave();
+	error(nil);
+}
+
+timeslave(rlock : ref FD, b : ref Box, c : chan of int)
+{
+	buf := array[DIRLEN] of byte;
+	for(;;){
+		sleep(30*1000);
+		if(rlock != nil && write(rlock, buf, 0)<0)
+			error("can't maintain L.reading: %r");
+		(ok, d) := stat(mboxfile);
+		if (ok >= 0 && d.length > b.leng)
+			c <-= 0;
+	}
+}
+
+Win.wnew() : ref Win
+{
+	w := ref Win;
+	buf := array[12] of byte;
+	w.ctl = open("/chan/new/ctl", ORDWR);
+	if(w.ctl==nil || read(w.ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	w.ctlwrite("noscroll\n");
+	w.winid = int string buf;
+	w.event = w.openfile("event");
+	w.addr = nil;	# will be opened when needed
+	w.body = nil;
+	w.data = nil;
+	w.bufp = w.nbuf = 0;
+	w.buf = array[512] of byte;
+	return w;
+}
+
+Win.openfile(w : self ref Win, f : string) : ref FD
+{
+	buf := sprint("/chan/%d/%s", w.winid, f);
+	fd := open(buf, ORDWR|OCEXEC);
+	if(fd == nil)
+		error(sprint("can't open window %s file: %r", f));
+	return fd;
+}
+
+Win.openbody(w : self ref Win, mode : int)
+{
+	buf := sprint("/chan/%d/body", w.winid);
+	w.body = bufio->open(buf, mode|OCEXEC);
+	if(w.body == nil)
+		error("can't open window body file: %r");
+}
+
+Win.wwritebody(w : self ref Win, s : string)
+{
+	n := len s;
+	if(w.body == nil)
+		w.openbody(OWRITE);
+	if(w.body.puts(s) != n)
+		error("write error to window: %r");
+}
+
+Win.wreplace(w : self ref Win, addr : string, repl : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	if(swrite(w.addr, addr) < 0){
+		fprint(stderr, "mail: warning: bad address %s:%r\n", addr);
+		return;
+	}
+	if(swrite(w.data, repl) != len repl)
+		error("writing data: %r");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) := byte2char(s, i);
+		if (!ok)
+			error("help needed in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+Win.wread(w : self ref Win, q0 : int, q1 : int) : string
+{
+	m, n, nr : int;
+	s, buf : string;
+	b : array of byte;
+
+	b = array[256] of byte;
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	s = nil;
+	m = q0;
+	while(m < q1){
+		buf = sprint("#%d", m);
+		if(swrite(w.addr, buf) != len buf)
+			error("writing addr: %r");
+		n = read(w.data, b, len b);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(b, n);
+		while(m+nr >q1){
+			do; while(n>0 && (int b[--n]&16rC0)==16r80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		s += string b[0:n];
+		m += nr;
+	}
+	return s;
+}
+
+Win.wshow(w : self ref Win)
+{
+	w.ctlwrite("show\n");
+}
+
+Win.wsetdump(w : self ref Win, dir : string, cmd : string)
+{
+	t : string;
+
+	if(dir != nil){
+		t = "dumpdir " + dir + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+	if(cmd != nil){
+		t = "dump " + cmd + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+}
+
+Win.wselect(w : self ref Win, addr : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(swrite(w.addr, addr) < 0)
+		error("writing addr");
+	w.ctlwrite("dot=addr\n");
+}
+
+Win.wtagwrite(w : self ref Win, s : string)
+{
+	fd : ref FD;
+
+	fd = w.openfile("tag");
+	if(swrite(fd, s) != len s)
+		error("tag write: %r");
+	fd = nil;
+}
+
+Win.ctlwrite(w : self ref Win, s : string)
+{
+	if(swrite(w.ctl, s) != len s)
+		error("write error to ctl file: %r");
+}
+
+Win.wdel(w : self ref Win, sure : int) : int
+{
+	if (w == nil)
+		return False;
+	if(sure)
+		swrite(w.ctl, "delete\n");
+	else if(swrite(w.ctl, "del\n") != 4)
+		return False;
+	w.wdormant();
+	w.ctl = nil;
+	w.event = nil;
+	return True;
+}
+
+Win.wname(w : self ref Win, s : string)
+{
+	w.ctlwrite("name " + s + "\n");
+}
+
+Win.wclean(w : self ref Win)
+{
+	if(w.body != nil)
+		w.body.flush();
+	w.ctlwrite("clean\n");
+}
+
+Win.wdormant(w : self ref Win)
+{
+	w.addr = nil;
+	if(w.body != nil){
+		w.body.close();
+		w.body = nil;
+	}
+	w.data = nil;
+}
+
+Win.getec(w : self ref Win) : int
+{
+	if(w.nbuf == 0){
+		w.nbuf = read(w.event, w.buf, len w.buf);
+		if(w.nbuf <= 0 && !killing) {
+			error("event read error: %r");
+		}
+		w.bufp = 0;
+	}
+	w.nbuf--;
+	return int w.buf[w.bufp++];
+}
+
+Win.geten(w : self ref Win) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=w.getec()) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+Win.geter(w : self ref Win, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = w.getec();
+	buf[0] = byte r;
+	n = 1;
+	if(r >= Runeself) {
+		for (;;) {
+			(r, m, ok) = byte2char(buf[0:n], 0);
+			if (m > 0)
+				return (r, n);
+			buf[n++] = byte w.getec();
+		}
+	}
+	return (r, n);
+}
+
+Win.wevent(w : self ref Win, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = w.getec();
+	e.c2 = w.getec();
+	e.q0 = w.geten();
+	e.q1 = w.geten();
+	e.flag = w.geten();
+	e.nr = w.geten();
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = w.geter(e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	c := w.getec();
+	if(c != '\n')
+		error("event syntax 2");
+}
+
+Win.wslave(w : self ref Win, ce : chan of Event)
+{
+	e : ref Event;
+
+	e = newevent();
+	for(;;){
+		w.wevent(e);
+		ce <-= *e;
+	}
+}
+
+Win.wwriteevent(w : self ref Win, e : ref Event)
+{
+	fprint(w.event, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+}
+
+Win.wreadall(w : self ref Win) : string
+{
+	s, t : string;
+
+	if(w.body != nil)
+		w.body.close();
+	w.openbody(OREAD);
+	s = nil;
+	while ((t = w.body.gets('\n')) != nil)
+		s += t;
+	w.body.close();
+	w.body = nil;
+	return s;
+}
+
+ignored : int;
+
+None,Unknown,Ignore,CC,From,ReplyTo,Sender,Subject,Re,To, Date : con iota;
+NHeaders : con 200;
+
+Hdrs : adt {
+	name : string;
+	typex : int;
+};
+
+
+hdrs := array[NHeaders+1] of {
+	Hdrs ( "CC:",				CC ),
+	Hdrs ( "From:",				From ),
+	Hdrs ( "Reply-To:",			ReplyTo ),
+	Hdrs ( "Sender:",			Sender ),
+	Hdrs ( "Subject:",			Subject ),
+	Hdrs ( "Re:",				Re ),
+	Hdrs ( "To:",				To ),
+	Hdrs ( "Date:",				Date),
+ * => Hdrs ( "",					0 ),
+};
+
+StRnCmP(s : string, t : string, n : int) : int
+{
+	c, d, i, j : int;
+
+	i = j = 0;
+	if (len s < n || len t < n)
+		return -1;
+	while(n > 0){
+		c = s[i++];
+		d = t[j++];
+		--n;
+		if(c != d){
+			if('a'<=c && c<='z')
+				c -= 'a'-'A';
+			if('a'<=d && d<='z')
+				d -= 'a'-'A';
+			if(c != d)
+				return c-d;
+		}
+	}
+	return 0;
+}
+
+ignore()
+{
+	b : ref Iobuf;
+	s : string;
+	i : int;
+
+	ignored = True;
+	b = bufio->open(MAILDIR + "/lib/ignore", OREAD);
+	if(b == nil)
+		return;
+	for(i=0; hdrs[i].name != nil; i++)
+		;
+	while((s = b.gets('\n')) != nil){
+		s = s[0:len s - 1];
+		hdrs[i].name = s;
+		hdrs[i].typex = Ignore;
+		if(++i >= NHeaders){
+			fprint(stderr, "%s/lib/ignore has more than %d headers\n", MAILDIR, NHeaders);
+			break;
+		}
+	}
+	b.close();
+}
+
+readhdr(b : ref Box) : (string, int)
+{
+	i, j, n, m, typex : int;
+	s, t : string;
+
+	{
+		if(!ignored)
+			ignore();
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			raise("e");
+		for(i=0; i<n; i++){
+			j = s[i];
+			if(i>0 && j == ':')
+				break;
+			if(j<'!' || '~'<j){
+				b.unreadline();
+				raise("e");
+			}
+		}
+		typex = Unknown;
+		for(i=0; hdrs[i].name != nil; i++){
+			j = len hdrs[i].name;
+			if(StRnCmP(hdrs[i].name, s, j) == 0){
+				typex = hdrs[i].typex;
+				break;
+			}
+		}
+		# scan for multiple sublines 
+		for(;;){
+			t = b.readline();
+			m = len t;
+			if(m<=0 || (t[0]!=' ' && t[0]!='\t')){
+				b.unreadline();
+				break;
+			}
+			# absorb 
+			s += t;
+		}
+		return(s, typex);
+	}
+	exception{
+		"*" =>
+			return (nil, None);
+	}
+}
+
+Mesg.read(b : ref Box) : ref Mesg
+{
+	m : ref Mesg;
+	s : string;
+	n, typex : int;
+
+	s = b.readline();
+	n = len s;
+	if(n <= 0)
+		return nil;
+
+{
+	if(n < 5 || s[0:5] !="From ")
+		raise("e");
+	m = newmesg();
+	m.realhdr = s;
+	# toss 'From ' 
+	s = s[5:];
+	n -= 5;
+	# toss spaces/tabs
+	while (n > 0 && (s[0] == ' ' || s[0] == '\t')) {
+		s = s[1:];
+		n--;
+	}
+	m.hdr = s;
+	# convert first blank to tab 
+	s0 := strchr(m.hdr, ' ');
+	if(s0 >= 0){
+		m.hdr[s0] = '\t';
+		# drop trailing seconds, time zone, and year if match local year 
+		t := n-6;
+		if(t <= 0)
+			raise("e");
+		if(m.hdr[t:n-1] == date[23:]){
+			m.hdr = m.hdr[0:t] + "\n";	# drop year for sure
+			t = -1;
+			s1 := strchr(m.hdr[s0:], ':');
+			if(s1 >= 0)
+				t = strchr(m.hdr[s0+s1+1:], ':');
+			if(t >= 0)	# drop seconds and time zone 
+				m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			else{	# drop time zone 
+				t = strchr(m.hdr[s0+s1+1:], ' ');
+				if(t >= 0)
+					m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			}
+			n = len m.hdr;
+		}
+	}
+	m.lline1 = n;
+	m.text = nil;
+	# read header 
+loop:
+	for(;;){
+		(s, typex) = readhdr(b);
+		case(typex){
+		None =>
+			break loop;
+		ReplyTo =>
+			m.replyto = s[9:];
+			break;
+		From =>
+			if(m.replyto == nil)
+				m.replyto = s[5:];
+			break;
+		Subject =>
+			m.subj = s[8:];
+			break;
+		Re =>
+			m.subj = s[3:];
+			break;
+		Date =>
+			break;
+		}
+		m.realhdr += s;
+		if(typex != Ignore)
+			m.hdr += s;
+	}
+	# read body 
+	for(;;){
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			break;
+		if(len s >= 5 && s[0:5] == "From "){
+			b.unreadline();
+			break;
+		}
+		m.text += s;
+	}
+	# remove trailing "morF\n" 
+	l := len m.text;
+	if(l>6 && m.text[l-6:] == "\nmorF\n")
+		m.text = m.text[0:l-5];
+	m.box = b;
+	return m;
+}
+exception{
+	"*" =>
+		error("malformed header " + s);
+		return nil;
+}
+}
+
+Mesg.mkmail(b : ref Box, hdr : string)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	r.hdr = hdr + "\n";
+	r.lline1 = len r.hdr;
+	r.text = nil;
+	r.box = b;
+	r.open();
+	r.w.wdormant();
+}
+
+replyaddr(r : string) : string
+{
+	p, q, rr : int;
+
+	rr = 0;
+	while(r[rr]==' ' || r[rr]=='\t')
+		rr++;
+	r = r[rr:];
+	p = strchr(r, '<');
+	if(p >= 0){
+		q = strchr(r[p+1:], '>');
+		if(q < 0)
+			r = r[p+1:];
+		else
+			r = r[p+1:p+q] + "\n";
+		return r;
+	}
+	p = strchr(r, '(');
+	if(p >= 0){
+		q = strchr(r[p:], ')');
+		if(q < 0)
+			r = r[0:p];
+		else
+			r = r[0:p] + r[p+q+1:];
+	}
+	return r;
+}
+
+Mesg.mkreply(m : self ref Mesg)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	if(m.replyto != nil){
+		r.hdr = replyaddr(m.replyto);
+		r.lline1 = len r.hdr;
+	}else{
+		r.hdr = m.hdr[0:m.lline1];
+		r.lline1 = m.lline1;	# was len m.hdr;
+	}
+	if(m.subj != nil){
+		if(StRnCmP(m.subj, "re:", 3)==0 || StRnCmP(m.subj, " re:", 4)==0)
+			r.text = "Subject:" + m.subj + "\n";
+		else
+			r.text = "Subject: Re:" + m.subj + "\n";
+	}
+	else
+		r.text = nil;
+	r.box = m.box;
+	r.open();
+	r.w.wselect("$");
+	r.w.wdormant();
+}
+
+Mesg.free(m : self ref Mesg)
+{
+	m.text = nil;
+	m.hdr = nil;
+	m.subj = nil;
+	m.realhdr = nil;
+	m.replyto = nil;
+	m = nil;
+}
+
+replyid : ref Ref;
+
+initreply()
+{
+	replyid = Ref.init();
+}
+
+Mesg.open(m : self ref Mesg)
+{
+	buf: string;
+
+	if(m.isopen)
+		return;
+	m.w = Win.wnew();
+	if(m.id != 0)
+		m.w.wwritebody("From ");
+	m.w.wwritebody(m.hdr);
+	m.w.wwritebody(m.text);
+	if(m.id){
+		buf = sprint("%s/%d", m.box.file , m.id);
+		m.w.wtagwrite("Reply Delmesg Save");
+	}else{
+		buf = sprint("%s/Reply%d", m.box.file, replyid.inc());
+		m.w.wtagwrite("Post");
+	}
+	m.w.wname(buf);
+	m.w.wclean();
+	m.w.wselect("0");
+	m.isopen = True;
+	m.posted = False;
+	spawn m.slave();
+}
+
+Mesg.putpost(m : self ref Mesg, e : ref Event)
+{
+	if(m.posted || m.id==0)
+		return;
+	if(e.q0 >= len m.hdr+5)	# include "From " 
+		return;
+	m.w.wtagwrite(" Post");
+	m.posted = True;
+	return;
+}
+
+Mesg.slave(m : self ref Mesg)
+{
+	e, e2, ea, etoss, eq : ref Event;
+	s : string;
+	na : int;
+
+	e = newevent();
+	e2 = newevent();
+	ea = newevent();
+	etoss = newevent();
+	for(;;){
+		m.w.wevent(e);
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>	# type away; we don't care 
+			case(e.c2){
+			'x' or 'X' =>	# mouse only 
+				eq = e;
+				if(e.flag & 2){
+					m.w.wevent(e2);
+					eq = e2;
+				}
+				if(e.flag & 8){
+					m.w.wevent(ea);
+					m.w.wevent(etoss);
+					na = ea.nb;
+				}else
+					na = 0;
+				if(eq.q1>eq.q0 && eq.nb==0)
+					s = m.w.wread(eq.q0, eq.q1);
+				else
+					s = string eq.b[0:eq.nb];
+				if(na)
+					s = s + " " + string ea.b[0:ea.nb];
+				if(!m.command(s))	# send it back 
+					m.w.wwriteevent(e);
+				s = nil;
+				break;
+			'l' or 'L' =>	# mouse only 
+				if(e.flag & 2)
+					m.w.wevent(e2);
+				# just send it back 
+				m.w.wwriteevent(e);
+				break;
+			'I' or 'D' =>	# modify away; we don't care 
+				m.putpost(e);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	}
+}
+
+Mesg.command(m : self ref Mesg, s : string) : int
+{
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(s == "Post"){
+		m.send();
+		return True;
+	}
+	if(len s >= 4 && s[0:4] == "Save"){
+		s = s[4:];
+		while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+			s = s[1:];
+		if(s == nil)
+			m.save("stored");
+		else{
+			ss := 0;
+			while(ss < len s && s[ss]!=' ' && s[ss]!='\t' && s[ss]!='\n')
+				ss++;
+			m.save(s[0:ss]);
+		}
+		return True;
+	}
+	if(s == "Reply"){
+		m.mkreply();
+		return True;
+	}
+	if(s == "Del"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			exit;
+		}
+		return True;
+	}
+	if(s == "Delmesg"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			m.box.cdel <-= m;
+			exit;
+		}
+		return True;
+	}
+	return False;
+}
+
+Mesg.save(m : self ref Mesg, base : string)
+{
+	s, buf : string;
+	n : int;
+	fd : ref FD;
+	b : ref Iobuf;
+
+	if(m.id <= 0){
+		fprint(stderr, "can't save reply message; mail it to yourself\n");
+		return;
+	}
+	buf = nil;
+	if(strchr(base, '/') >= 0)
+		s = base;
+	else
+		s = usermboxdir + base;
+	{
+		if(access(s) < 0)
+			raise("e");
+		fd = tryopen(s, OWRITE);
+		if(fd == nil)
+			raise("e");
+		buf = nil;
+		b = bufio->fopen(fd, OWRITE);
+		# seek to end in case file isn't append-only 
+		b.seek(big 0, 2);
+		# use edited headers: first line of real header followed by remainder of selected ones 
+		for(n=0; n<len m.realhdr && m.realhdr[n++]!='\n'; )
+			;
+		b.puts(m.realhdr[0:n]);
+		b.puts(m.hdr[m.lline1:]);
+		b.puts(m.text);
+		b.close();
+		b = nil;
+		fd = nil;
+	}
+	exception{
+		"*" =>
+			buf = nil;
+			fprint(stderr, "mail: can't open %s: %r\n", base);
+			return;
+	}
+}
+
+Mesg.send(m : self ref Mesg)
+{
+	s, buf : string;
+	t, u : int;
+	a, b : list of string;
+	n : int;
+	p : array of ref FD;
+	c : chan of int;
+
+	p = array[2] of ref FD;
+	s = m.w.wreadall();
+	a = "sendmail" :: nil;
+	if(len s >= 5 && s[0:5] == "From ")
+		s = s[5:];
+	for(t=0; t < len s && s[t]!='\n' && s[t]!='\t';){
+		while(t < len s && (s[t]==' ' || s[t]==','))
+			t++;
+		u = t;
+		while(t < len s && s[t]!=' ' && s[t]!=',' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		if(t == u)
+			break;
+		a = s[u:t] :: a;
+	}
+	b = nil;
+	for ( ; a != nil; a = tl a)
+		b = hd a :: b;
+	a = b;
+	while(t < len s && s[t]!='\n')
+		t++;
+	if(s[t] == '\n')
+		t++;
+	if(pipe(p) < 0)
+		error("can't pipe: %r");
+	c = chan of int;
+	spawn run(a, c, p[0]);
+	<-c;
+	c = nil;
+	p[0] = nil;
+	n = len s - t;
+	if(swrite(p[1], s[t:]) != n)
+		fprint(stderr, "write to pipe failed: %r\n");
+	p[1] = nil;
+	# run() frees the arg list 
+	buf = sprint("%s/%d-R", m.box.file, m.id);
+	m.w.wname(buf);
+	m.w.wclean();
+}
+
+Box.read(f : string, readonly : int) : ref Box
+{
+	b : ref Box;
+	m : ref Mesg;
+	s, buf : string;
+
+	b = ref Box;
+	b.nm = 0;
+	b.readonly = readonly;
+	b.file = f;
+	b.io = bufio->open(f, OREAD|OCEXEC);
+	if(b.io == nil)
+		error(sprint("can't open %s: %r", f));
+	b.w = Win.wnew();
+	if (!readonly)
+		spawn lockfilemon(b.w.winid);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		m.id = b.nm;
+	}
+	b.leng = b.io.offset();
+	b.io.close();
+	b.io = nil;
+	# b.w = Win.wnew();
+	for(m=b.m; m != nil; m=m.next){
+		if(m.subj != nil)
+			buf = sprint("%d\t%s\t %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wwritebody(buf);
+	}
+	buf = sprint("Mail/%s/", s);
+	b.w.wname(f);
+	if(b.readonly)
+		b.w.wtagwrite("Mail");
+	else
+		b.w.wtagwrite("Put Mail");
+	buf = "Mail " + f; 
+	b.w.wsetdump("/acme/mail", buf);
+	b.w.wclean();
+	b.w.wselect("0");
+	b.w.wdormant();
+	b.cdel= chan of ref Mesg;
+	b.cevent = chan of Event;
+	b.cmore = chan of int;
+	spawn b.w.wslave(b.cevent);
+	b.clean = True;
+	return b;
+}
+
+Box.readmore(b : self ref Box)
+{
+	m : ref Mesg;
+	new : int;
+	buf : string;
+	doclose : int;
+
+	doclose = False;
+	if(b.io == nil){
+		b.io = bufio->open(b.file, OREAD|OCEXEC);
+		if(b.io == nil)
+			error(sprint("can't open %s: %r", b.file));
+		b.io.seek(b.leng, 0);
+		doclose = True;
+	}
+	new = False;
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		m.id = b.nm;
+		if(m.subj != nil)
+			buf  = sprint("%d\t%s\t  %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wreplace("0", buf);
+		new = True;
+	}
+	b.leng = b.io.offset();
+	if(doclose){
+		b.io.close();
+		b.io = nil;
+	}
+	if(new){
+		if(b.clean)
+			b.w.wclean();
+		b.w.wselect("0;/.*(\\n[ \t].*)*");
+		b.w.wshow();
+	}
+	b.w.wdormant();
+}
+
+Box.readline(b : self ref Box) : string
+{
+    	for (;;) {
+		if(b.peekline != nil){
+			b.line = b.peekline;
+			b.peekline = nil;
+		}else
+			b.line = b.io.gets('\n');
+		# nulls appear in mailboxes! 
+		if(b.line != nil && strchr(b.line, 0) >= 0)
+			;
+		else
+			break;
+	}
+	return b.line;
+}
+
+Box.unreadline(b : self ref Box)
+{
+	b.peekline = b.line;
+}
+
+Box.slave(b : self ref Box)
+{
+	e : ref Event;
+	m : ref Mesg;
+
+	e = newevent();
+	for(;;){
+		alt{
+		*e = <-b.cevent =>
+			b.event(e);
+			break;
+		<-b.cmore =>
+			b.readmore();
+			break;
+		m = <-b.cdel =>
+			b.mdel(m);
+			break;
+		}
+	}
+}
+
+Box.event(b : self ref Box, e : ref Event)
+{
+	e2, ea, eq : ref Event;
+	s : string;
+	t : int;
+	n, na, nopen : int;
+
+	e2 = newevent();
+	ea = newevent();
+	case(e.c1){
+	'E' =>	# write to body; can't affect us 
+		break;
+	'F' =>	# generated by our actions; ignore 
+		break;
+	'K' =>	# type away; we don't care 
+		break;
+	'M' =>
+		case(e.c2){
+		'x' or 'X' =>
+			if(e.flag & 2)
+				*e2 = <-b.cevent;
+			if(e.flag & 8){
+				*ea = <-b.cevent;
+				na = ea.nb;
+				<- b.cevent;
+			}else
+				na = 0;
+			s = string e.b[0:e.nb];
+			# if it's a known command, do it 
+			if((e.flag&2) && e.nb==0)
+				s = string e2.b[0:e2.nb];
+			if(na)
+				s = sprint("%s %s", s, string ea.b[0:ea.nb]);
+			# if it's a long message, it can't be for us anyway 
+			if(!b.command(s))	# send it back 
+				b.w.wwriteevent(e);
+			if(na)
+				s = nil;
+			break;
+		'l' or 'L' =>
+			eq = e;
+			if(e.flag & 2){
+				*e2 = <-b.cevent;
+				eq = e2;
+			}
+			s = string eq.b[0:eq.nb];
+			if(eq.q1>eq.q0 && eq.nb==0)
+				s = b.w.wread(eq.q0, eq.q1);
+			nopen = 0;
+			do{
+				t = 0;
+				(n, t) = strtoi(s);
+				if(n>0 && (t == len s || s[t]==' ' || s[t]=='\t' || s[t]=='\n')){
+					b.mopen(n);
+					nopen++;
+					s = s[t:];
+				}
+				while(s != nil && s[0]!='\n')
+					s = s[1:];
+			}while(s != nil);
+			if(nopen == 0)	# send it back 
+				b.w.wwriteevent(e);
+			break;
+		'I' or 'D' or 'd' or 'i' =>	# modify away; we don't care 
+			break;
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	* =>
+		fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+		break;
+	}
+}
+
+Box.mopen(b : self ref Box, id : int)
+{
+	m : ref Mesg;
+
+	for(m=b.m; m != nil; m=m.next)
+		if(m.id == id){
+			m.open();
+			break;
+		}
+}
+
+Box.mdel(b : self ref Box, dm : ref Mesg)
+{
+	prev, m : ref Mesg;
+	buf : string;
+
+	if(dm.id){
+		prev = nil;
+		for(m=b.m; m!=nil && m!=dm; m=m.next)
+			prev = m;
+		if(m == nil)
+			error(sprint("message %d not found", dm.id));
+		if(prev == nil)
+			b.m = m.next;
+		else
+			prev.next = m.next;
+		# remove from screen: use acme to help 
+		buf = sprint("/^%d	.*\\n(^[ \t].*\\n)*/", m.id);
+		b.w.wreplace(buf, "");
+	}
+	dm.free();
+	b.clean = False;
+}
+
+Box.command(b : self ref Box, s : string) : int
+{
+	t : int;
+	m : ref Mesg;
+
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(len s >= 4 && s[0:4] == "Mail"){
+		s = s[4:];
+		while(s != nil && (s[0]==' ' || s[0]=='\t' || s[0]=='\n'))
+			s = s[1:];
+		t = 0;
+		while(t < len s && s[t] && s[t]!=' ' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		m = b.m;		# avoid warning message on b.m.mkmail(...)
+		m.mkmail(b, s[0:t]);
+		return True;
+	}
+	if(s == "Del"){
+
+		if(!b.clean){
+			b.clean = True;
+			fprint(stderr, "mail: mailbox not written\n");
+			return True;
+		}
+		rmlockfile();
+		postnote(PNGROUP, pctl(0, nil), "kill");
+		killing = 1;
+		pctl(NEWPGRP, nil);
+		b.w.wdel(True);
+		for(m=b.m; m != nil; m=m.next)
+			m.w.wdel(False);
+		exit;
+		return True;
+	}
+	if(s == "Put"){
+		if(b.readonly)
+			fprint(stderr, "Mail: %s is read-only\n", b.file);
+		else
+			b.rewrite();
+		return True;
+	}
+	return False;
+}
+
+Box.rewrite(b : self ref Box)
+{
+	mbox, Lmbox, mboxtmp : ref FD;
+	i, t, ok : int;
+	buf : string;
+	s : string;
+	m : ref Mesg;
+	d : Dir;
+	Lmboxs : string = nil;
+
+	if(b.clean){
+		b.w.wclean();
+		return;
+	}
+	t = strrchr(b.file, '/');
+	if(t >= 0)
+		s = b.file[t+1:];
+	else
+		s = b.file;
+	if(mboxfile == usermboxfile){
+		buf = sprint("%sL.%s", b.file[0:t+1], s);
+		Lmbox = openlockfile(buf);
+		if(Lmbox == nil)
+			error(sprint("can't open lock file %s: %r", buf));
+		else
+			Lmboxs = buf;
+	}else
+		Lmbox = nil;
+	buf = sprint("%s.tmp", b.file);
+	mbox = tryopen(mboxfile, OREAD);
+	if(mbox != nil){
+		b.io = bufio->fopen(mbox, OREAD);
+		b.io.seek(b.leng, 0);
+		b.readmore();
+	}else if(access(buf)){
+		fprint(stderr, "mail: mailbox missing; using %s\n", buf);
+		mboxtmp = tryopen(buf, ORDWR);
+		b.io = bufio->fopen(mboxtmp, OREAD);
+		b.readmore();
+		b.io.close();
+	}else
+		error(sprint("can't open %s to rewrite: %r", s));
+	remove(buf);
+	mboxtmp = create(buf, OWRITE, 0622|CHAPPEND|CHEXCL);
+	if(mboxtmp == nil)
+			error(sprint("can't create %s: %r", buf));
+	(ok, d) = fstat(mboxtmp);
+	if(ok < 0)
+		error(sprint("can't fstat %s: %r", buf));
+	d.mode |= 0622;
+	if(fwstat(mboxtmp, d) < 0)
+		error(sprint("can't change mode of %s: %r", buf));
+	b.io = bufio->fopen(mboxtmp, OWRITE);
+	# write it backwards: stupid code 
+	for(i=1; i<=b.nm; i++){
+		for(m=b.m; m!=nil && m.id!=i; m=m.next)
+			;
+		if(m != nil){
+			b.io.puts(m.realhdr);
+			b.io.puts(m.text);
+		}
+	}
+	if(remove(mboxfile) < 0)
+		error(sprint("can't unlink %s: %r", mboxfile));
+	d.name = s;
+	if(fwstat(mboxtmp, d) < 0)
+		error(sprint("can't change name of %s: %r", buf));
+	b.leng = b.io.offset();
+	b.io.close();
+	mboxtmp = nil;
+	b.io = nil;
+	Lmbox = nil;
+	if (Lmboxs != nil)
+		remove(Lmboxs);
+	b.w.wclean();
+	b.clean = True;
+}
+
+lockfilemon(id : int)
+{
+	pctl(NEWPGRP, nil);
+	p := "/chan/" + string id;
+	for (;;) {
+		if (lockfile == nil)
+			error(nil);
+		sleep(60*1000);
+		(ok, d) := stat(p);
+		if (ok < 0)
+			error(nil);
+	}
+}
--- /dev/null
+++ b/appl/acme/acme/mail/src/Mailp.b
@@ -1,0 +1,1684 @@
+implement mailpop3;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "daytime.m";
+include "sh.m";
+include "pop3.m";
+
+mailpop3 : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+daytime : Daytime;
+pop3 : Pop3;
+
+OREAD, OWRITE, ORDWR, NEWFD, FORKENV, FORKFD, NEWPGRP, UTFmax, EXCEPTION, ONCE : import Sys;
+FD, Dir, Exception : import sys;
+fprint, sprint, sleep, create, open, read, write, remove, stat, fstat, fwstat, fildes, pctl, pipe, dup, byte2char : import sys;
+Context : import Draw;
+EOF : import Bufio;
+Iobuf : import bufio;
+time : import daytime;
+
+DIRLEN : con 116;
+PNPROC, PNGROUP : con iota;
+False : con 0;
+True : con 1;
+EVENTSIZE : con 256;
+Runeself : con 16r80;
+OCEXEC : con 0;
+CHEXCL : con 0; # 16r20000000;
+CHAPPEND : con 0; # 16r40000000;
+
+Win : adt {
+	winid : int;
+	addr : ref FD;
+	body : ref Iobuf;
+	ctl : ref FD;
+	data : ref FD;
+	event : ref FD;
+	buf : array of byte;
+	bufp : int;
+	nbuf : int;
+
+	wnew : fn() : ref Win;
+	wwritebody : fn(w : self ref Win, s : string);
+	wread : fn(w : self ref Win, m : int, n : int) : string;
+	wclean : fn(w : self ref Win);
+	wname : fn(w : self ref Win, s : string);
+	wdormant : fn(w : self ref Win);
+	wevent : fn(w : self ref Win, e : ref Event);
+	wshow : fn(w : self ref Win);
+	wtagwrite : fn(w : self ref Win, s : string);
+	wwriteevent : fn(w : self ref Win, e : ref Event);
+	wslave : fn(w : self ref Win, c : chan of Event);
+	wreplace : fn(w : self ref Win, s : string, t : string);
+	wselect : fn(w : self ref Win, s : string);
+	wsetdump : fn(w : self ref Win, s : string, t : string);
+	wdel : fn(w : self ref Win, n : int) : int;
+	wreadall : fn(w : self ref Win) : string;
+
+ 	ctlwrite : fn(w : self ref Win, s : string);
+ 	getec : fn(w : self ref Win) : int;
+ 	geten : fn(w : self ref Win) : int;
+ 	geter : fn(w : self ref Win, s : array of byte) : (int, int);
+ 	openfile : fn(w : self ref Win, s : string) : ref FD;
+ 	openbody : fn(w : self ref Win, n : int);
+};
+
+Mesg : adt {
+	w : ref Win;
+	id : int;
+	popno : int;
+	hdr : string;
+	realhdr : string;
+	replyto : string;
+	text : string;
+	subj : string;
+	next : cyclic ref Mesg;
+ 	lline1 : int;
+	box : cyclic ref Box;
+	isopen : int;
+	posted : int;
+	deleted : int;
+
+	read : fn(b : ref Box) : ref Mesg;
+	open : fn(m : self ref Mesg);
+	slave : fn(m : self ref Mesg);
+	free : fn(m : self ref Mesg);
+	save : fn(m : self ref Mesg, s : string);
+	mkreply : fn(m : self ref Mesg);
+	mkmail : fn(b : ref Box, s : string);
+	putpost : fn(m : self ref Mesg, e : ref Event);
+
+ 	command : fn(m : self ref Mesg, s : string) : int;
+ 	send : fn(m : self ref Mesg);
+};
+
+Box : adt {
+	w : ref Win;
+	nm : int;
+	readonly : int;
+	m : cyclic ref Mesg;
+#	io : ref Iobuf;
+	clean : int;
+ 	leng : int;
+ 	cdel : chan of ref Mesg;
+	cevent : chan of Event;
+	cmore : chan of int;
+	lst : list of int;
+	s : string;
+	
+	line : string;
+	popno : int;
+	peekline : string;
+
+	read : fn(n : int) : ref Box;
+	readmore : fn(b : self ref Box);
+	readline : fn(b : self ref Box) : string;
+	unreadline : fn(b : self ref Box);
+	slave : fn(b : self ref Box);
+	mopen : fn(b : self ref Box, n : int);
+	rewrite : fn(b : self ref Box);
+	mdel : fn(b : self ref Box, m : ref Mesg);
+	event : fn(b : self ref Box, e : ref Event);
+
+	command : fn(b : self ref Box, s : string) : int;
+};
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+Lock : adt {
+	cnt : int;
+	chann : chan of int;
+
+	init : fn() : ref Lock;
+	lock : fn(l : self ref Lock);
+	unlock : fn(l : self ref Lock);
+};
+
+Ref : adt {
+	l : ref Lock;
+	cnt : int;
+
+	init : fn() : ref Ref;
+	inc : fn(r : self ref Ref) : int;
+};
+
+mbox : ref Box;
+user : string;
+date : string;
+mailctxt : ref Context;
+stdout, stderr : ref FD;
+
+killing : int = 0;
+
+init(ctxt : ref Context, argl : list of string)
+{
+	mailctxt = ctxt;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	pop3 = load Pop3 Pop3->PATH;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main();
+}
+
+dlock : ref Lock;
+dfd : ref Sys->FD;
+
+debug(s : string)
+{
+	if (dfd == nil) {
+		dfd = sys->create("/usr/jrf/acme/debugmail", Sys->OWRITE, 8r600);
+		dlock = Lock.init();
+	}
+	if (dfd == nil)
+		return;
+	dlock.lock();
+	sys->fprint(dfd, "%s", s);	
+	dlock.unlock();
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+	fd = nil;
+	return 0;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(mailctxt, argl);
+}
+
+swrite(fd : ref FD, s : string) : int
+{
+	ab := array of byte s;
+	m := len ab;
+	p := write(fd, ab, m);
+	if (p == m)
+		return len s;
+	if (p <= 0)
+		return p;
+	return 0;
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+} 
+
+strrchr(s : string, c : int) : int
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+strtoi(s : string) : (int, int)
+{
+	m := 0;
+	neg := 0;
+	t := 0;
+	ls := len s;
+	while (t < ls && (s[t] == ' ' || s[t] == '\t'))
+		t++;
+	if (t < ls && s[t] == '+')
+		t++;
+	else if (t < ls && s[t] == '-') {
+		neg = 1;
+		t++;
+	}
+	while (t < ls && (s[t] >= '0' && s[t] <= '9')) {
+		m = 10*m + s[t]-'0';
+		t++;
+	}
+	if (neg)
+		m = -m;
+	return (m, t);	
+}
+
+access(s : string) : int
+{
+	fd := open(s, 0);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+newevent() : ref Event
+{
+	e := ref Event;
+	e.b = array[EVENTSIZE*UTFmax+1] of byte;
+	e.r = array[EVENTSIZE+1] of int;
+	return e;
+}	
+
+newmesg() : ref Mesg
+{
+	m := ref Mesg;
+	m.id = m.lline1 = m.isopen = m.posted = m.deleted = 0;
+	return m;
+}
+
+lc, uc : chan of ref Lock;
+
+initlock()
+{
+	lc = chan of ref Lock;
+	uc = chan of ref Lock;
+	spawn lockmgr();
+}
+
+lockmgr()
+{
+	l : ref Lock;
+
+	for (;;) {
+		alt {
+			l = <- lc =>
+				if (l.cnt++ == 0)
+					l.chann <-= 1;
+			l = <- uc =>
+				if (--l.cnt > 0)
+					l.chann <-= 1;
+		}
+	}
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	lc <-= l;
+	<- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	uc <-= l;
+}
+
+Ref.init() : ref Ref
+{
+	r := ref Ref;
+	r.l = Lock.init();
+	r.cnt = 0;
+	return r;
+}
+
+Ref.inc(r : self ref Ref) : int
+{
+	r.l.lock();
+	i := r.cnt;
+	r.cnt++;
+	r.l.unlock();
+	return i;
+}
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "mail: %s\n", s);
+	postnote(PNGROUP, pctl(0, nil), "kill");
+	killing = 1;
+	exit;
+}
+
+tryopen(s : string, mode : int) : ref FD
+{
+	fd : ref FD;
+	try : int;
+
+	for(try=0; try<3; try++){
+		fd = open(s, mode);
+		if(fd != nil)
+			return fd;
+		sleep(1000);
+	}
+	return nil;
+}
+
+run(argv : list of string, c : chan of int, p0 : ref FD)
+{
+	# pctl(FORKFD|NEWPGRP, nil);	# had RFMEM
+	pctl(FORKENV|NEWFD|NEWPGRP, 0::1::2::p0.fd::nil);
+	c <-= pctl(0, nil);
+	dup(p0.fd, 0);
+	p0 = nil;
+	exec(hd argv, argv);
+	exit;
+}
+
+getuser() : string
+{
+  	fd := open("/dev/user", OREAD);
+  	if(fd == nil)
+    		return "";
+  	buf := array[128] of byte;
+  	n := read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+  	return string buf[0:n];	
+}
+
+pop3conn : int = 0;
+pop3bad : int = 0;
+pop3lock : ref Lock;
+
+pop3open()
+{
+	pop3lock.lock();
+	if (!pop3conn) {
+		(ok, s) := pop3->open(user, "********", nil);	# password now got from user in Mailpop3.b
+		if (ok < 0) {
+			if (!pop3bad) {
+				fprint(stderr, "mail: could not connect to POP3 mail server : %s\n", s);
+				pop3bad = 1;
+			}
+			return;
+		}
+	}
+	pop3conn = 1;
+	pop3bad = 0;
+}
+
+pop3close()
+{
+	if (pop3conn) {
+		(ok, s) := pop3->close();
+		if (ok < 0) {
+			fprint(stderr, "mail: could not close POP3 connection : %s\n", s);
+			pop3lock.unlock();
+			return;
+		}
+	}
+	pop3conn = 0;
+	pop3lock.unlock();
+}
+
+pop3stat(b : ref Box) : int
+{
+	(ok, s, nm, nil) := pop3->stat();
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not stat POP3 server : %s\n", s);
+		return b.leng;
+	}
+	return nm;
+}
+
+pop3list() : list of int
+{
+	(ok, s, l) := pop3->msgnolist();
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not get list from POP3 server : %s\n", s);
+		return nil;
+	}
+	return l;
+}
+
+pop3mesg(mno : int) : string
+{
+	(ok, s, msg) := pop3->get(mno);
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not retrieve a message from server : %s\n", s);
+		return "Acme Mail : FAILED TO RETRIEVE MESSAGE\n";
+	}
+	return msg;
+}
+
+pop3del(mno : int) : int
+{
+	(ok, s) := pop3->delete(mno);
+	if (ok < 0) 
+		fprint(stderr, "mail: could not delete message : %s\n", s);
+	return ok;
+}
+
+pop3init(b : ref Box)
+{
+	b.leng = pop3stat(b);
+	b.lst = pop3list();
+	b.s = nil;
+	b.popno = 0;
+}
+
+pop3more(b : ref Box)
+{
+	nl : list of int;
+
+	leng := b.leng;
+	b.leng = pop3stat(b);
+	b.lst = pop3list();
+	b.s = nil;
+	b.popno = 0;
+	if (len b.lst != b.leng || b.leng <= leng)
+		error("bad lengths in pop3more()");
+	# is this ok ?
+	nl = nil;
+	for (i := 0; i < leng; i++) {
+		nl = hd b.lst :: nl;
+		b.lst = tl b.lst;
+	}
+	# now update pop nos.
+	for (m := b.m; m != nil; m = m.next) {
+		# opopno := m.popno;
+		if (nl == nil)
+			error("message list too big");
+		m.popno = hd nl;
+		nl = tl nl;
+		# debug(sys->sprint("%d : popno from %d to %d\n", m.id, opopno, m.popno));
+	}
+	if (nl != nil)
+		error("message list too small");
+}
+
+pop3next(b : ref Box) : string
+{
+	mno : int = 0;
+	r : string;
+
+	if (b.s == nil) {
+		if (b.lst == nil)
+			return nil;	# end of box
+		first := b.popno == 0;
+		mno = hd b.lst;
+		b.lst = tl b.lst;
+		b.s = pop3mesg(mno);
+		b.popno = mno;
+		if (!first)
+			return nil;	# end of message
+	}
+	t := strchr(b.s, '\n');
+	if (t >= 0) {
+		r = b.s[0:t+1];
+		b.s = b.s[t+1:];
+	}
+	else {
+		r = b.s;
+		b.s = nil;
+	}
+	return r;
+}
+
+main()
+{
+	readonly : int;
+
+	initlock();
+	initreply();
+	date = time();
+	if(date==nil)
+		error("can't get current time");
+	user = getuser();
+	if(user == nil)
+		user = "Wile.E.Coyote";
+	readonly = False;
+	pop3lock = Lock.init();
+	mbox = mbox.read(readonly);
+	spawn timeslave(mbox, mbox.cmore);
+	mbox.slave();
+	error(nil);
+}
+
+timeslave(b : ref Box, c : chan of int)
+{
+	for(;;){
+		sleep(30*1000);
+		pop3open();
+		leng := pop3stat(b);
+		pop3close();
+		if (leng > b.leng)
+			c <-= 0;
+	}
+}
+
+Win.wnew() : ref Win
+{
+	w := ref Win;
+	buf := array[12] of byte;
+	w.ctl = open("/chan/new/ctl", ORDWR);
+	if(w.ctl==nil || read(w.ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	w.ctlwrite("noscroll\n");
+	w.winid = int string buf;
+	w.event = w.openfile("event");
+	w.addr = nil;	# will be opened when needed
+	w.body = nil;
+	w.data = nil;
+	w.bufp = w.nbuf = 0;
+	w.buf = array[512] of byte;
+	return w;
+}
+
+Win.openfile(w : self ref Win, f : string) : ref FD
+{
+	buf := sprint("/chan/%d/%s", w.winid, f);
+	fd := open(buf, ORDWR|OCEXEC);
+	if(fd == nil)
+		error(sprint("can't open window %s file: %r", f));
+	return fd;
+}
+
+Win.openbody(w : self ref Win, mode : int)
+{
+	buf := sprint("/chan/%d/body", w.winid);
+	w.body = bufio->open(buf, mode|OCEXEC);
+	if(w.body == nil)
+		error("can't open window body file: %r");
+}
+
+Win.wwritebody(w : self ref Win, s : string)
+{
+	n := len s;
+	if(w.body == nil)
+		w.openbody(OWRITE);
+	if(w.body.puts(s) != n)
+		error("write error to window: %r");
+}
+
+Win.wreplace(w : self ref Win, addr : string, repl : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	if(swrite(w.addr, addr) < 0){
+		fprint(stderr, "mail: warning: bad address %s:%r\n", addr);
+		return;
+	}
+	if(swrite(w.data, repl) != len repl)
+		error("writing data: %r");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(r, b, ok) := byte2char(s, i);
+		if (!ok)
+			error("help needed in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+Win.wread(w : self ref Win, q0 : int, q1 : int) : string
+{
+	m, n, nr : int;
+	s, buf : string;
+	b : array of byte;
+
+	b = array[256] of byte;
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	s = nil;
+	m = q0;
+	while(m < q1){
+		buf = sprint("#%d", m);
+		if(swrite(w.addr, buf) != len buf)
+			error("writing addr: %r");
+		n = read(w.data, b, len b);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(b, n);
+		while(m+nr >q1){
+			do; while(n>0 && (int b[--n]&16rC0)==16r80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		s += string b[0:n];
+		m += nr;
+	}
+	return s;
+}
+
+Win.wshow(w : self ref Win)
+{
+	w.ctlwrite("show\n");
+}
+
+Win.wsetdump(w : self ref Win, dir : string, cmd : string)
+{
+	t : string;
+
+	if(dir != nil){
+		t = "dumpdir " + dir + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+	if(cmd != nil){
+		t = "dump " + cmd + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+}
+
+Win.wselect(w : self ref Win, addr : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(swrite(w.addr, addr) < 0)
+		error("writing addr");
+	w.ctlwrite("dot=addr\n");
+}
+
+Win.wtagwrite(w : self ref Win, s : string)
+{
+	fd : ref FD;
+
+	fd = w.openfile("tag");
+	if(swrite(fd, s) != len s)
+		error("tag write: %r");
+	fd = nil;
+}
+
+Win.ctlwrite(w : self ref Win, s : string)
+{
+	if(swrite(w.ctl, s) != len s)
+		error("write error to ctl file: %r");
+}
+
+Win.wdel(w : self ref Win, sure : int) : int
+{
+	if (w == nil)
+		return False;
+	if(sure)
+		swrite(w.ctl, "delete\n");
+	else if(swrite(w.ctl, "del\n") != 4)
+		return False;
+	w.wdormant();
+	w.ctl = nil;
+	w.event = nil;
+	return True;
+}
+
+Win.wname(w : self ref Win, s : string)
+{
+	w.ctlwrite("name " + s + "\n");
+}
+
+Win.wclean(w : self ref Win)
+{
+	if(w.body != nil)
+		w.body.flush();
+	w.ctlwrite("clean\n");
+}
+
+Win.wdormant(w : self ref Win)
+{
+	w.addr = nil;
+	if(w.body != nil){
+		w.body.close();
+		w.body = nil;
+	}
+	w.data = nil;
+}
+
+Win.getec(w : self ref Win) : int
+{
+	if(w.nbuf == 0){
+		w.nbuf = read(w.event, w.buf, len w.buf);
+		if(w.nbuf <= 0 && !killing) {
+			error("event read error: %r");
+		}
+		w.bufp = 0;
+	}
+	w.nbuf--;
+	return int w.buf[w.bufp++];
+}
+
+Win.geten(w : self ref Win) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=w.getec()) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+Win.geter(w : self ref Win, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = w.getec();
+	buf[0] = byte r;
+	n = 1;
+	if(r >= Runeself) {
+		for (;;) {
+			(r, m, ok) = byte2char(buf[0:n], 0);
+			if (m > 0)
+				return (r, n);
+			buf[n++] = byte w.getec();
+		}
+	}
+	return (r, n);
+}
+
+Win.wevent(w : self ref Win, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = w.getec();
+	e.c2 = w.getec();
+	e.q0 = w.geten();
+	e.q1 = w.geten();
+	e.flag = w.geten();
+	e.nr = w.geten();
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = w.geter(e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	c := w.getec();
+	if(c != '\n')
+		error("event syntax 2");
+}
+
+Win.wslave(w : self ref Win, ce : chan of Event)
+{
+	e : ref Event;
+
+	e = newevent();
+	for(;;){
+		w.wevent(e);
+		ce <-= *e;
+	}
+}
+
+Win.wwriteevent(w : self ref Win, e : ref Event)
+{
+	fprint(w.event, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+}
+
+Win.wreadall(w : self ref Win) : string
+{
+	s, t : string;
+
+	if(w.body != nil)
+		w.body.close();
+	w.openbody(OREAD);
+	s = nil;
+	while ((t = w.body.gets('\n')) != nil)
+		s += t;
+	w.body.close();
+	w.body = nil;
+	return s;
+}
+
+None,Unknown,Ignore,CC,From,ReplyTo,Sender,Subject,Re,To, Date : con iota;
+NHeaders : con 200;
+
+Hdrs : adt {
+	name : string;
+	typex : int;
+};
+
+
+hdrs := array[NHeaders+1] of {
+	Hdrs ( "CC:",				CC ),
+	Hdrs ( "From:",				From ),
+	Hdrs ( "Reply-To:",			ReplyTo ),
+	Hdrs ( "Sender:",			Sender ),
+	Hdrs ( "Subject:",			Subject ),
+	Hdrs ( "Re:",				Re ),
+	Hdrs ( "To:",				To ),
+	Hdrs ( "Date:",				Date),
+ * => Hdrs ( "",					0 ),
+};
+
+StRnCmP(s : string, t : string, n : int) : int
+{
+	c, d, i, j : int;
+
+	i = j = 0;
+	if (len s < n || len t < n)
+		return -1;
+	while(n > 0){
+		c = s[i++];
+		d = t[j++];
+		--n;
+		if(c != d){
+			if('a'<=c && c<='z')
+				c -= 'a'-'A';
+			if('a'<=d && d<='z')
+				d -= 'a'-'A';
+			if(c != d)
+				return c-d;
+		}
+	}
+	return 0;
+}
+
+readhdr(b : ref Box) : (string, int)
+{
+	i, j, n, m, typex : int;
+	s, t : string;
+
+{
+	s = b.readline();
+	n = len s;
+	if(n <= 0) {
+		b.unreadline();
+		raise("e");
+	}
+	for(i=0; i<n; i++){
+		j = s[i];
+		if(i>0 && j == ':')
+			break;
+		if(j<'!' || '~'<j){
+			b.unreadline();
+			raise("e");
+		}
+	}
+	typex = Unknown;
+	for(i=0; hdrs[i].name != nil; i++){
+		j = len hdrs[i].name;
+		if(StRnCmP(hdrs[i].name, s, j) == 0){
+			typex = hdrs[i].typex;
+			break;
+		}
+	}
+	# scan for multiple sublines 
+	for(;;){
+		t = b.readline();
+		m = len t;
+		if(m<=0 || (t[0]!=' ' && t[0]!='\t')){
+			b.unreadline();
+			break;
+		}
+		# absorb 
+		s += t;
+	}
+	return(s, typex);
+}
+exception{
+	"*" =>
+		return (nil, None);
+}
+}
+
+Mesg.read(b : ref Box) : ref Mesg
+{
+	m : ref Mesg;
+	s : string;
+	n, typex : int;
+
+	s = b.readline();
+	n = len s;
+	if(n <= 0)
+		return nil;
+	
+{
+	if(n < 5 || (s[0:5] !="From " && s[0:5] != "From:"))
+		raise("e");
+	m = newmesg();
+	m.popno = b.popno;
+	if (m.popno == 0)
+		error("bad pop3 id");
+	m.realhdr = s;
+	# toss 'From ' 
+	s = s[5:];
+	n -= 5;
+	# toss spaces/tabs
+	while (n > 0 && (s[0] == ' ' || s[0] == '\t')) {
+		s = s[1:];
+		n--;
+	}
+	m.hdr = s;
+	# convert first blank to tab 
+	s0 := strchr(m.hdr, ' ');
+	if(s0 >= 0){
+		m.hdr[s0] = '\t';
+		# drop trailing seconds, time zone, and year if match local year 
+		t := n-6;
+		if(t <= 0)
+			raise("e");
+		if(m.hdr[t:n-1] == date[23:]){
+			m.hdr = m.hdr[0:t] + "\n";	# drop year for sure
+			t = -1;
+			s1 := strchr(m.hdr[s0:], ':');
+			if(s1 >= 0)
+				t = strchr(m.hdr[s0+s1+1:], ':');
+			if(t >= 0)	# drop seconds and time zone 
+				m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			else{	# drop time zone 
+				t = strchr(m.hdr[s0+s1+1:], ' ');
+				if(t >= 0)
+					m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			}
+			n = len m.hdr;
+		}
+	}
+	m.lline1 = n;
+	m.text = nil;
+	# read header 
+loop:
+	for(;;){
+		(s, typex) = readhdr(b);
+		case(typex){
+		None =>
+			break loop;
+		ReplyTo =>
+			m.replyto = s[9:];
+			break;
+		From =>
+			if(m.replyto == nil)
+				m.replyto = s[5:];
+			break;
+		Subject =>
+			m.subj = s[8:];
+			break;
+		Re =>
+			m.subj = s[3:];
+			break;
+		Date =>
+			break;
+		}
+		m.realhdr += s;
+		if(typex != Ignore)
+			m.hdr += s;
+	}
+	# read body 
+	for(;;){
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			break;
+#		if(len s >= 5 && (s[0:5] == "From " || s[0:5] == "From:")){
+#			b.unreadline();
+#			break;
+#		}
+		m.text += s;
+	}
+	# remove trailing "morF\n" 
+	l := len m.text;
+	if(l>6 && m.text[l-6:] == "\nmorF\n")
+		m.text = m.text[0:l-5];
+	m.box = b;
+	return m;
+}
+exception{
+	"*" =>
+		error("malformed header " + s);
+		return nil;
+}
+}
+
+Mesg.mkmail(b : ref Box, hdr : string)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	r.hdr = hdr + "\n";
+	r.lline1 = len r.hdr;
+	r.text = nil;
+	r.box = b;
+	r.open();
+	r.w.wdormant();
+}
+
+replyaddr(r : string) : string
+{
+	p, q, rr : int;
+
+	rr = 0;
+	while(r[rr]==' ' || r[rr]=='\t')
+		rr++;
+	r = r[rr:];
+	p = strchr(r, '<');
+	if(p >= 0){
+		q = strchr(r[p+1:], '>');
+		if(q < 0)
+			r = r[p+1:];
+		else
+			r = r[p+1:p+q] + "\n";
+		return r;
+	}
+	p = strchr(r, '(');
+	if(p >= 0){
+		q = strchr(r[p:], ')');
+		if(q < 0)
+			r = r[0:p];
+		else
+			r = r[0:p] + r[p+q+1:];
+	}
+	return r;
+}
+
+Mesg.mkreply(m : self ref Mesg)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	if(m.replyto != nil){
+		r.hdr = replyaddr(m.replyto);
+		r.lline1 = len r.hdr;
+	}else{
+		r.hdr = m.hdr[0:m.lline1];
+		r.lline1 = m.lline1;	# was len m.hdr;
+	}
+	if(m.subj != nil){
+		if(StRnCmP(m.subj, "re:", 3)==0 || StRnCmP(m.subj, " re:", 4)==0)
+			r.text = "Subject:" + m.subj + "\n";
+		else
+			r.text = "Subject: Re:" + m.subj + "\n";
+	}
+	else
+		r.text = nil;
+	r.box = m.box;
+	r.open();
+	r.w.wselect("$");
+	r.w.wdormant();
+}
+
+Mesg.free(m : self ref Mesg)
+{
+	m.text = nil;
+	m.hdr = nil;
+	m.subj = nil;
+	m.realhdr = nil;
+	m.replyto = nil;
+	m = nil;
+}
+
+replyid : ref Ref;
+
+initreply()
+{
+	replyid = Ref.init();
+}
+
+Mesg.open(m : self ref Mesg)
+{
+	buf, s : string;
+
+	if(m.isopen)
+		return;
+	m.w = Win.wnew();
+	if(m.id != 0)
+		m.w.wwritebody("From ");
+	m.w.wwritebody(m.hdr);
+	m.w.wwritebody(m.text);
+	if(m.id){
+		buf = sprint("Mail/box/%d", m.id);
+		m.w.wtagwrite("Reply Delmesg Save");
+	}else{
+		buf = sprint("Mail/%s/Reply%d", s, replyid.inc());
+		m.w.wtagwrite("Post");
+	}
+	m.w.wname(buf);
+	m.w.wclean();
+	m.w.wselect("0");
+	m.isopen = True;
+	m.posted = False;
+	spawn m.slave();
+}
+
+Mesg.putpost(m : self ref Mesg, e : ref Event)
+{
+	if(m.posted || m.id==0)
+		return;
+	if(e.q0 >= len m.hdr+5)	# include "From " 
+		return;
+	m.w.wtagwrite(" Post");
+	m.posted = True;
+	return;
+}
+
+Mesg.slave(m : self ref Mesg)
+{
+	e, e2, ea, etoss, eq : ref Event;
+	s : string;
+	na : int;
+
+	e = newevent();
+	e2 = newevent();
+	ea = newevent();
+	etoss = newevent();
+	for(;;){
+		m.w.wevent(e);
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>	# type away; we don't care 
+			case(e.c2){
+			'x' or 'X' =>	# mouse only 
+				eq = e;
+				if(e.flag & 2){
+					m.w.wevent(e2);
+					eq = e2;
+				}
+				if(e.flag & 8){
+					m.w.wevent(ea);
+					m.w.wevent(etoss);
+					na = ea.nb;
+				}else
+					na = 0;
+				if(eq.q1>eq.q0 && eq.nb==0)
+					s = m.w.wread(eq.q0, eq.q1);
+				else
+					s = string eq.b[0:eq.nb];
+				if(na)
+					s = s + " " + string ea.b[0:ea.nb];
+				if(!m.command(s))	# send it back 
+					m.w.wwriteevent(e);
+				s = nil;
+				break;
+			'l' or 'L' =>	# mouse only 
+				if(e.flag & 2)
+					m.w.wevent(e2);
+				# just send it back 
+				m.w.wwriteevent(e);
+				break;
+			'I' or 'D' =>	# modify away; we don't care 
+				m.putpost(e);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	}
+}
+
+Mesg.command(m : self ref Mesg, s : string) : int
+{
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(s == "Post"){
+		m.send();
+		return True;
+	}
+	if(len s >= 4 && s[0:4] == "Save"){
+		s = s[4:];
+		while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+			s = s[1:];
+		if(s == nil)
+			m.save("stored");
+		else{
+			ss := 0;
+			while(ss < len s && s[ss]!=' ' && s[ss]!='\t' && s[ss]!='\n')
+				ss++;
+			m.save(s[0:ss]);
+		}
+		return True;
+	}
+	if(s == "Reply"){
+		m.mkreply();
+		return True;
+	}
+	if(s == "Del"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			exit;
+		}
+		return True;
+	}
+	if(s == "Delmesg"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			m.box.cdel <-= m;
+			exit;
+		}
+		return True;
+	}
+	return False;
+}
+
+Mesg.save(m : self ref Mesg, base : string)
+{
+	s, buf : string;
+	n : int;
+	fd : ref FD;
+	b : ref Iobuf;
+
+	if(m.id <= 0){
+		fprint(stderr, "can't save reply message; mail it to yourself\n");
+		return;
+	}
+	buf = nil;
+	s = base;
+{
+	if(access(s) < 0)
+		raise("e");
+	fd = tryopen(s, OWRITE);
+	if(fd == nil)
+		raise("e");
+	buf = nil;
+	b = bufio->fopen(fd, OWRITE);
+	# seek to end in case file isn't append-only 
+	b.seek(big 0, 2);
+	# use edited headers: first line of real header followed by remainder of selected ones 
+	for(n=0; n<len m.realhdr && m.realhdr[n++]!='\n'; )
+		;
+	b.puts(m.realhdr[0:n]);
+	b.puts(m.hdr[m.lline1:]);
+	b.puts(m.text);
+	b.close();
+	b = nil;
+	fd = nil;
+}
+exception{
+	"*" =>
+		buf = nil;
+		fprint(stderr, "mail: can't open %s: %r\n", base);
+		return;
+}
+}
+
+Mesg.send(m : self ref Mesg)
+{
+	s, buf : string;
+	t, u : int;
+	a, b : list of string;
+	n : int;
+	p : array of ref FD;
+	c : chan of int;
+
+	p = array[2] of ref FD;
+	s = m.w.wreadall();
+	a = "sendmail" :: nil;
+	if(len s >= 5 && (s[0:5] == "From " || s[0:5] == "From:"))
+		s = s[5:];
+	for(t=0; t < len s && s[t]!='\n' && s[t]!='\t';){
+		while(t < len s && (s[t]==' ' || s[t]==','))
+			t++;
+		u = t;
+		while(t < len s && s[t]!=' ' && s[t]!=',' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		if(t == u)
+			break;
+		a = s[u:t] :: a;
+	}
+	b = nil;
+	for ( ; a != nil; a = tl a)
+		b = hd a :: b;
+	a = b;
+	while(t < len s && s[t]!='\n')
+		t++;
+	if(s[t] == '\n')
+		t++;
+	if(pipe(p) < 0)
+		error("can't pipe: %r");
+	c = chan of int;
+	spawn run(a, c, p[0]);
+	<-c;
+	c = nil;
+	p[0] = nil;
+	n = len s - t;
+	if(swrite(p[1], s[t:]) != n)
+		fprint(stderr, "write to pipe failed: %r\n");
+	p[1] = nil;
+	# run() frees the arg list 
+	buf = sprint("Mail/box/%d-R", m.id);
+	m.w.wname(buf);
+	m.w.wclean();
+}
+
+Box.read(readonly : int) : ref Box
+{
+	b : ref Box;
+	m : ref Mesg;
+	buf : string;
+
+	b = ref Box;
+	b.nm = 0;
+	b.leng = 0;
+	b.readonly = readonly;
+	pop3open();
+	pop3init(b);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		m.id = b.nm;
+	}
+	pop3close();
+	if (b.leng != b.nm)
+		error("bad message count in Box.read()");
+	b.w = Win.wnew();
+	for(m=b.m; m != nil; m=m.next){
+		if(m.subj != nil)
+			buf = sprint("%d\t%s\t %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wwritebody(buf);
+	}
+	buf = sprint("Mail/box/");
+	b.w.wname(buf);
+	if(b.readonly)
+		b.w.wtagwrite("Mail");
+	else
+		b.w.wtagwrite("Put Mail");
+	buf = "Mail " + "box"; 
+	b.w.wsetdump("/acme/mail", buf);
+	b.w.wclean();
+	b.w.wselect("0");
+	b.w.wdormant();
+	b.cdel= chan of ref Mesg;
+	b.cevent = chan of Event;
+	b.cmore = chan of int;
+	spawn b.w.wslave(b.cevent);
+	b.clean = True;
+	return b;
+}
+
+Box.readmore(b : self ref Box)
+{
+	m : ref Mesg;
+	new : int;
+	buf : string;
+
+	new = False;
+	leng := b.leng;
+	n := 0;
+	pop3open();
+	pop3more(b);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		n++;
+		m.id = b.nm;
+		if(m.subj != nil)
+			buf  = sprint("%d\t%s\t  %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wreplace("0", buf);
+		new = True;
+	}
+	pop3close();
+	if (b.leng != leng+n)
+		error("bad message count in Box.readmore()");
+	if(new){
+		if(b.clean)
+			b.w.wclean();
+		b.w.wselect("0;/.*(\\n[ \t].*)*");
+		b.w.wshow();
+	}
+	b.w.wdormant();
+}
+
+Box.readline(b : self ref Box) : string
+{
+    	for (;;) {
+		if(b.peekline != nil){
+			b.line = b.peekline;
+			b.peekline = nil;
+		}else
+			b.line = pop3next(b);
+		# nulls appear in mailboxes! 
+		if(b.line != nil && strchr(b.line, 0) >= 0)
+			;
+		else
+			break;
+	}
+	return b.line;
+}
+
+Box.unreadline(b : self ref Box)
+{
+	b.peekline = b.line;
+}
+
+Box.slave(b : self ref Box)
+{
+	e : ref Event;
+	m : ref Mesg;
+
+	e = newevent();
+	for(;;){
+		alt{
+		*e = <-b.cevent =>
+			b.event(e);
+			break;
+		<-b.cmore =>
+			b.readmore();
+			break;
+		m = <-b.cdel =>
+			b.mdel(m);
+			break;
+		}
+	}
+}
+
+Box.event(b : self ref Box, e : ref Event)
+{
+	e2, ea, eq : ref Event;
+	s : string;
+	t : int;
+	n, na, nopen : int;
+
+	e2 = newevent();
+	ea = newevent();
+	case(e.c1){
+	'E' =>	# write to body; can't affect us 
+		break;
+	'F' =>	# generated by our actions; ignore 
+		break;
+	'K' =>	# type away; we don't care 
+		break;
+	'M' =>
+		case(e.c2){
+		'x' or 'X' =>
+			if(e.flag & 2)
+				*e2 = <-b.cevent;
+			if(e.flag & 8){
+				*ea = <-b.cevent;
+				na = ea.nb;
+				<- b.cevent;
+			}else
+				na = 0;
+			s = string e.b[0:e.nb];
+			# if it's a known command, do it 
+			if((e.flag&2) && e.nb==0)
+				s = string e2.b[0:e2.nb];
+			if(na)
+				s = sprint("%s %s", s, string ea.b[0:ea.nb]);
+			# if it's a long message, it can't be for us anyway 
+			if(!b.command(s))	# send it back 
+				b.w.wwriteevent(e);
+			if(na)
+				s = nil;
+			break;
+		'l' or 'L' =>
+			eq = e;
+			if(e.flag & 2){
+				*e2 = <-b.cevent;
+				eq = e2;
+			}
+			s = string eq.b[0:eq.nb];
+			if(eq.q1>eq.q0 && eq.nb==0)
+				s = b.w.wread(eq.q0, eq.q1);
+			nopen = 0;
+			do{
+				t = 0;
+				(n, t) = strtoi(s);
+				if(n>0 && (t == len s || s[t]==' ' || s[t]=='\t' || s[t]=='\n')){
+					b.mopen(n);
+					nopen++;
+					s = s[t:];
+				}
+				while(s != nil && s[0]!='\n')
+					s = s[1:];
+			}while(s != nil);
+			if(nopen == 0)	# send it back 
+				b.w.wwriteevent(e);
+			break;
+		'I' or 'D' or 'd' or 'i' =>	# modify away; we don't care 
+			break;
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	* =>
+		fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+		break;
+	}
+}
+
+Box.mopen(b : self ref Box, id : int)
+{
+	m : ref Mesg;
+
+	for(m=b.m; m != nil; m=m.next)
+		if(m.id == id){
+			m.open();
+			break;
+		}
+}
+
+Box.mdel(b : self ref Box, dm : ref Mesg)
+{
+	m : ref Mesg;
+	buf : string;
+
+	if(dm.id){
+		for(m=b.m; m!=nil && m!=dm; m=m.next)
+			;
+		if(m == nil)
+			error(sprint("message %d not found", dm.id));
+		m.deleted = 1;
+		# remove from screen: use acme to help 
+		buf = sprint("/^%d	.*\\n(^[ \t].*\\n)*/", m.id);
+		b.w.wreplace(buf, "");
+	}
+	dm.free();
+	b.clean = False;
+}
+
+Box.command(b : self ref Box, s : string) : int
+{
+	t : int;
+	m : ref Mesg;
+
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(len s >= 4 && s[0:4] == "Mail"){
+		s = s[4:];
+		while(s != nil && (s[0]==' ' || s[0]=='\t' || s[0]=='\n'))
+			s = s[1:];
+		t = 0;
+		while(t < len s && s[t] && s[t]!=' ' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		m = b.m;		# avoid warning message on b.m.mkmail(...)
+		m.mkmail(b, s[0:t]);
+		return True;
+	}
+	if(s == "Del"){
+
+		if(!b.clean){
+			b.clean = True;
+			fprint(stderr, "mail: mailbox not written\n");
+			return True;
+		}
+		postnote(PNGROUP, pctl(0, nil), "kill");
+		killing = 1;
+		pctl(NEWPGRP, nil);
+		b.w.wdel(True);
+		for(m=b.m; m != nil; m=m.next)
+			m.w.wdel(False);
+		exit;
+		return True;
+	}
+	if(s == "Put"){
+		if(b.readonly)
+			fprint(stderr, "Mail is read-only\n");
+		else
+			b.rewrite();
+		return True;
+	}
+	return False;
+}
+
+Box.rewrite(b : self ref Box)
+{
+	prev, m : ref Mesg;
+
+	if(b.clean){
+		b.w.wclean();
+		return;
+	}
+	prev = nil;
+	pop3open();
+	for(m=b.m; m!=nil; m=m.next) {
+		if (m.deleted && pop3del(m.popno) >= 0) {
+			b.leng--;
+			if (prev == nil)
+				b.m=m.next;
+			else
+				prev.next=m.next;
+		}
+		else
+			prev = m;
+	}
+	pop3close();
+	b.w.wclean();
+	b.clean = True;
+}
--- /dev/null
+++ b/appl/acme/acme/mail/src/Mailpop3.b
@@ -1,0 +1,1720 @@
+implement Mailpop3;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "daytime.m";
+include "sh.m";
+include "pop3.m";
+
+Mailpop3 : module {
+	init : fn(ctxt : ref Draw->Context, argl : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+daytime : Daytime;
+pop3 : Pop3;
+
+OREAD, OWRITE, ORDWR, FORKENV, NEWFD, FORKFD, NEWPGRP, UTFmax : import Sys;
+FD, Dir : import sys;
+fprint, sprint, sleep, create, open, read, write, remove, stat, fstat, fwstat, fildes, pctl, pipe, dup, byte2char : import sys;
+Context : import Draw;
+EOF : import Bufio;
+Iobuf : import bufio;
+time : import daytime;
+
+DIRLEN : con 116;
+PNPROC, PNGROUP : con iota;
+False : con 0;
+True : con 1;
+EVENTSIZE : con 256;
+Runeself : con 16r80;
+OCEXEC : con 0;
+CHEXCL : con 0; # 16r20000000;
+CHAPPEND : con 0; # 16r40000000;
+
+Win : adt {
+	winid : int;
+	addr : ref FD;
+	body : ref Iobuf;
+	ctl : ref FD;
+	data : ref FD;
+	event : ref FD;
+	buf : array of byte;
+	bufp : int;
+	nbuf : int;
+
+	wnew : fn() : ref Win;
+	wwritebody : fn(w : self ref Win, s : string);
+	wread : fn(w : self ref Win, m : int, n : int) : string;
+	wclean : fn(w : self ref Win);
+	wname : fn(w : self ref Win, s : string);
+	wdormant : fn(w : self ref Win);
+	wevent : fn(w : self ref Win, e : ref Event);
+	wshow : fn(w : self ref Win);
+	wtagwrite : fn(w : self ref Win, s : string);
+	wwriteevent : fn(w : self ref Win, e : ref Event);
+	wslave : fn(w : self ref Win, c : chan of Event);
+	wreplace : fn(w : self ref Win, s : string, t : string);
+	wselect : fn(w : self ref Win, s : string);
+	wsetdump : fn(w : self ref Win, s : string, t : string);
+	wdel : fn(w : self ref Win, n : int) : int;
+	wreadall : fn(w : self ref Win) : string;
+
+ 	ctlwrite : fn(w : self ref Win, s : string);
+ 	getec : fn(w : self ref Win) : int;
+ 	geten : fn(w : self ref Win) : int;
+ 	geter : fn(w : self ref Win, s : array of byte) : (int, int);
+ 	openfile : fn(w : self ref Win, s : string) : ref FD;
+ 	openbody : fn(w : self ref Win, n : int);
+};
+
+Mesg : adt {
+	w : ref Win;
+	id : int;
+	popno : int;
+	hdr : string;
+	realhdr : string;
+	replyto : string;
+	text : string;
+	subj : string;
+	next : cyclic ref Mesg;
+ 	lline1 : int;
+	box : cyclic ref Box;
+	isopen : int;
+	posted : int;
+	deleted : int;
+
+	read : fn(b : ref Box) : ref Mesg;
+	open : fn(m : self ref Mesg);
+	slave : fn(m : self ref Mesg);
+	free : fn(m : self ref Mesg);
+	save : fn(m : self ref Mesg, s : string);
+	mkreply : fn(m : self ref Mesg);
+	mkmail : fn(b : ref Box, s : string);
+	putpost : fn(m : self ref Mesg, e : ref Event);
+
+ 	command : fn(m : self ref Mesg, s : string) : int;
+ 	send : fn(m : self ref Mesg);
+};
+
+Box : adt {
+	w : ref Win;
+	nm : int;
+	readonly : int;
+	m : cyclic ref Mesg;
+#	io : ref Iobuf;
+	clean : int;
+ 	leng : int;
+ 	cdel : chan of ref Mesg;
+	cevent : chan of Event;
+	cmore : chan of int;
+	lst : list of int;
+	s : string;
+	
+	line : string;
+	popno : int;
+	peekline : string;
+
+	read : fn(n : int) : ref Box;
+	readmore : fn(b : self ref Box, lck : int);
+	readline : fn(b : self ref Box) : string;
+	unreadline : fn(b : self ref Box);
+	slave : fn(b : self ref Box);
+	mopen : fn(b : self ref Box, n : int);
+	rewrite : fn(b : self ref Box);
+	mdel : fn(b : self ref Box, m : ref Mesg);
+	event : fn(b : self ref Box, e : ref Event);
+
+	command : fn(b : self ref Box, s : string) : int;
+};
+
+Event : adt {
+	c1 : int;
+	c2 : int;
+	q0 : int;
+	q1 : int;
+	flag : int;
+	nb : int;
+	nr : int;
+	b : array of byte;
+	r : array of int;
+};
+
+Lock : adt {
+	cnt : int;
+	chann : chan of int;
+
+	init : fn() : ref Lock;
+	lock : fn(l : self ref Lock);
+	unlock : fn(l : self ref Lock);
+};
+
+Ref : adt {
+	l : ref Lock;
+	cnt : int;
+
+	init : fn() : ref Ref;
+	inc : fn(r : self ref Ref) : int;
+};
+
+mbox : ref Box;
+user : string;
+pwd : string;
+date : string;
+mailctxt : ref Context;
+stdout, stderr : ref FD;
+
+killing : int = 0;
+
+init(ctxt : ref Context, nil : list of string)
+{
+	mailctxt = ctxt;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	pop3 = load Pop3 Pop3->PATH;
+	stdout = fildes(1);
+	stderr = fildes(2);
+	main();
+}
+
+dlock : ref Lock;
+dfd : ref Sys->FD;
+
+debug(s : string)
+{
+	if (dfd == nil) {
+		dfd = sys->create("/usr/jrf/acme/debugmail", Sys->OWRITE, 8r600);
+		dlock = Lock.init();
+	}
+	if (dfd == nil)
+		return;
+	dlock.lock();
+	sys->fprint(dfd, "%s", s);	
+	dlock.unlock();
+}
+
+postnote(t : int, pid : int, note : string) : int
+{
+	fd := open("#p/" + string pid + "/ctl", OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	fprint(fd, "%s", note);
+	fd = nil;
+	return 0;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sprint("%r");
+		}
+		if(c == nil){
+			fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(mailctxt, argl);
+}
+
+swrite(fd : ref FD, s : string) : int
+{
+	ab := array of byte s;
+	m := len ab;
+	p := write(fd, ab, m);
+	if (p == m)
+		return len s;
+	if (p <= 0)
+		return p;
+	return 0;
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+} 
+
+strrchr(s : string, c : int) : int
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+strtoi(s : string) : (int, int)
+{
+	m := 0;
+	neg := 0;
+	t := 0;
+	ls := len s;
+	while (t < ls && (s[t] == ' ' || s[t] == '\t'))
+		t++;
+	if (t < ls && s[t] == '+')
+		t++;
+	else if (t < ls && s[t] == '-') {
+		neg = 1;
+		t++;
+	}
+	while (t < ls && (s[t] >= '0' && s[t] <= '9')) {
+		m = 10*m + s[t]-'0';
+		t++;
+	}
+	if (neg)
+		m = -m;
+	return (m, t);	
+}
+
+access(s : string) : int
+{
+	fd := open(s, 0);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+newevent() : ref Event
+{
+	e := ref Event;
+	e.b = array[EVENTSIZE*UTFmax+1] of byte;
+	e.r = array[EVENTSIZE+1] of int;
+	return e;
+}	
+
+newmesg() : ref Mesg
+{
+	m := ref Mesg;
+	m.id = m.lline1 = m.isopen = m.posted = m.deleted = 0;
+	return m;
+}
+
+lc, uc : chan of ref Lock;
+
+initlock()
+{
+	lc = chan of ref Lock;
+	uc = chan of ref Lock;
+	spawn lockmgr();
+}
+
+lockmgr()
+{
+	l : ref Lock;
+
+	for (;;) {
+		alt {
+			l = <- lc =>
+				if (l.cnt++ == 0)
+					l.chann <-= 1;
+			l = <- uc =>
+				if (--l.cnt > 0)
+					l.chann <-= 1;
+		}
+	}
+}
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	lc <-= l;
+	<- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	uc <-= l;
+}
+
+Ref.init() : ref Ref
+{
+	r := ref Ref;
+	r.l = Lock.init();
+	r.cnt = 0;
+	return r;
+}
+
+Ref.inc(r : self ref Ref) : int
+{
+	r.l.lock();
+	i := r.cnt;
+	r.cnt++;
+	r.l.unlock();
+	return i;
+}
+
+error(s : string)
+{
+	if(s != nil)
+		fprint(stderr, "mail: %s\n", s);
+	postnote(PNGROUP, pctl(0, nil), "kill");
+	killing = 1;
+	exit;
+}
+
+tryopen(s : string, mode : int) : ref FD
+{
+	fd : ref FD;
+	try : int;
+
+	for(try=0; try<3; try++){
+		fd = open(s, mode);
+		if(fd != nil)
+			return fd;
+		sleep(1000);
+	}
+	return nil;
+}
+
+run(argv : list of string, c : chan of int, p0 : ref FD)
+{
+	# pctl(FORKFD|NEWPGRP, nil);	# had RFMEM
+	pctl(FORKENV|NEWFD|NEWPGRP, 0::1::2::p0.fd::nil);
+	c <-= pctl(0, nil);
+	dup(p0.fd, 0);
+	p0 = nil;
+	exec(hd argv, argv);
+	exit;
+}
+
+getuser() : string
+{
+  	fd := open("/dev/user", OREAD);
+  	if(fd == nil)
+    		return "";
+  	buf := array[128] of byte;
+  	n := read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+  	return string buf[0:n];	
+}
+
+pop3conn : int = 0;
+pop3bad : int = 0;
+pop3lock : ref Lock;
+
+pop3open(lck : int)
+{
+	if (lck)
+		pop3lock.lock();
+	if (!pop3conn) {
+		(ok, s) := pop3->open(user, pwd, nil);
+		if (ok < 0) {
+			if (!pop3bad) {
+				fprint(stderr, "mail: could not connect to POP3 mail server : %s\n", s);
+				pop3bad = 1;
+			}
+			return;
+		}
+	}
+	pop3conn = 1;
+	pop3bad = 0;
+}
+
+pop3close(unlck : int)
+{
+	if (pop3conn) {
+		(ok, s) := pop3->close();
+		if (ok < 0) {
+			fprint(stderr, "mail: could not close POP3 connection : %s\n", s);
+			pop3lock.unlock();
+			return;
+		}
+	}
+	pop3conn = 0;
+	if (unlck)
+		pop3lock.unlock();
+}
+
+pop3stat(b : ref Box) : int
+{
+	(ok, s, nm, nil) := pop3->stat();
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not stat POP3 server : %s\n", s);
+		return b.leng;
+	}
+	return nm;
+}
+
+pop3list() : list of int
+{
+	(ok, s, l) := pop3->msgnolist();
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not get list from POP3 server : %s\n", s);
+		return nil;
+	}
+	return l;
+}
+
+pop3mesg(mno : int) : string
+{
+	(ok, s, msg) := pop3->get(mno);
+	if (ok < 0 && pop3conn) {
+		fprint(stderr, "mail: could not retrieve a message from server : %s\n", s);
+		return "Acme Mail : FAILED TO RETRIEVE MESSAGE\n";
+	}
+	return msg;
+}
+
+pop3del(mno : int) : int
+{
+	(ok, s) := pop3->delete(mno);
+	if (ok < 0) 
+		fprint(stderr, "mail: could not delete message : %s\n", s);
+	return ok;
+}
+
+pop3init(b : ref Box)
+{
+	b.leng = pop3stat(b);
+	b.lst = pop3list();
+	b.s = nil;
+	b.popno = 0;
+	if (len b.lst != b.leng)
+		error("bad lengths in pop3init()");
+}
+
+pop3more(b : ref Box)
+{
+	nl : list of int;
+
+	leng := b.leng;
+	b.leng = pop3stat(b);
+	b.lst = pop3list();
+	b.s = nil;
+	b.popno = 0;
+	if (len b.lst != b.leng || b.leng < leng)
+		error("bad lengths in pop3more()");
+	# is this ok ?
+	nl = nil;
+	for (i := 0; i < leng; i++) {
+		nl = hd b.lst :: nl;
+		b.lst = tl b.lst;
+	}
+	# now update pop nos.
+	for (m := b.m; m != nil; m = m.next) {
+		# opopno := m.popno;
+		if (nl == nil)
+			error("message list too big");
+		m.popno = hd nl;
+		nl = tl nl;
+		# debug(sys->sprint("%d : popno from %d to %d\n", m.id, opopno, m.popno));
+	}
+	if (nl != nil)
+		error("message list too small");
+}
+
+pop3next(b : ref Box) : string
+{
+	mno : int = 0;
+	r : string;
+
+	if (b.s == nil) {
+		if (b.lst == nil)
+			return nil;	# end of box
+		first := b.popno == 0;
+		mno = hd b.lst;
+		b.lst = tl b.lst;
+		b.s = pop3mesg(mno);
+		b.popno = mno;
+		if (!first)
+			return nil;	# end of message
+	}
+	t := strchr(b.s, '\n');
+	if (t >= 0) {
+		r = b.s[0:t+1];
+		b.s = b.s[t+1:];
+	}
+	else {
+		r = b.s;
+		b.s = nil;
+	}
+	return r;
+}
+
+main()
+{
+	readonly : int;
+
+	initlock();
+	initreply();
+	date = time();
+	if(date==nil)
+		error("can't get current time");
+	user = getuser();
+	if(user == nil)
+		user = "Wile.E.Coyote";
+	readonly = False;
+	pop3lock = Lock.init();
+	mbox = mbox.read(readonly);
+	spawn timeslave(mbox, mbox.cmore);
+	mbox.slave();
+	error(nil);
+}
+
+timeslave(b : ref Box, c : chan of int)
+{
+	for(;;){
+		sleep(30*1000);
+		pop3open(1);
+		leng := pop3stat(b);
+		pop3close(1);
+		if (leng > b.leng)
+			c <-= 0;
+	}
+}
+
+Win.wnew() : ref Win
+{
+	w := ref Win;
+	buf := array[12] of byte;
+	w.ctl = open("/chan/new/ctl", ORDWR);
+	if(w.ctl==nil || read(w.ctl, buf, 12)!=12)
+		error("can't open window ctl file: %r");
+	w.ctlwrite("noscroll\n");
+	w.winid = int string buf;
+	w.event = w.openfile("event");
+	w.addr = nil;	# will be opened when needed
+	w.body = nil;
+	w.data = nil;
+	w.bufp = w.nbuf = 0;
+	w.buf = array[512] of byte;
+	return w;
+}
+
+Win.openfile(w : self ref Win, f : string) : ref FD
+{
+	buf := sprint("/chan/%d/%s", w.winid, f);
+	fd := open(buf, ORDWR|OCEXEC);
+	if(fd == nil)
+		error(sprint("can't open window %s file: %r", f));
+	return fd;
+}
+
+Win.openbody(w : self ref Win, mode : int)
+{
+	buf := sprint("/chan/%d/body", w.winid);
+	w.body = bufio->open(buf, mode|OCEXEC);
+	if(w.body == nil)
+		error("can't open window body file: %r");
+}
+
+Win.wwritebody(w : self ref Win, s : string)
+{
+	n := len s;
+	if(w.body == nil)
+		w.openbody(OWRITE);
+	if(w.body.puts(s) != n)
+		error("write error to window: %r");
+}
+
+Win.wreplace(w : self ref Win, addr : string, repl : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	if(swrite(w.addr, addr) < 0){
+		fprint(stderr, "mail: warning: bad address %s:%r\n", addr);
+		return;
+	}
+	if(swrite(w.data, repl) != len repl)
+		error("writing data: %r");
+}
+
+nrunes(s : array of byte, nb : int) : int
+{
+	i, n : int;
+
+	n = 0;
+	for(i=0; i<nb; n++) {
+		(nil, b, ok) := byte2char(s, i);
+		if (!ok)
+			error("help needed in nrunes()");
+		i += b;
+	}
+	return n;
+}
+
+Win.wread(w : self ref Win, q0 : int, q1 : int) : string
+{
+	m, n, nr : int;
+	s, buf : string;
+	b : array of byte;
+
+	b = array[256] of byte;
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(w.data == nil)
+		w.data = w.openfile("data");
+	s = nil;
+	m = q0;
+	while(m < q1){
+		buf = sprint("#%d", m);
+		if(swrite(w.addr, buf) != len buf)
+			error("writing addr: %r");
+		n = read(w.data, b, len b);
+		if(n <= 0)
+			error("reading data: %r");
+		nr = nrunes(b, n);
+		while(m+nr >q1){
+			do; while(n>0 && (int b[--n]&16rC0)==16r80);
+			--nr;
+		}
+		if(n == 0)
+			break;
+		s += string b[0:n];
+		m += nr;
+	}
+	return s;
+}
+
+Win.wshow(w : self ref Win)
+{
+	w.ctlwrite("show\n");
+}
+
+Win.wsetdump(w : self ref Win, dir : string, cmd : string)
+{
+	t : string;
+
+	if(dir != nil){
+		t = "dumpdir " + dir + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+	if(cmd != nil){
+		t = "dump " + cmd + "\n";
+		w.ctlwrite(t);
+		t = nil;
+	}
+}
+
+Win.wselect(w : self ref Win, addr : string)
+{
+	if(w.addr == nil)
+		w.addr = w.openfile("addr");
+	if(swrite(w.addr, addr) < 0)
+		error("writing addr");
+	w.ctlwrite("dot=addr\n");
+}
+
+Win.wtagwrite(w : self ref Win, s : string)
+{
+	fd : ref FD;
+
+	fd = w.openfile("tag");
+	if(swrite(fd, s) != len s)
+		error("tag write: %r");
+	fd = nil;
+}
+
+Win.ctlwrite(w : self ref Win, s : string)
+{
+	if(swrite(w.ctl, s) != len s)
+		error("write error to ctl file: %r");
+}
+
+Win.wdel(w : self ref Win, sure : int) : int
+{
+	if (w == nil)
+		return False;
+	if(sure)
+		swrite(w.ctl, "delete\n");
+	else if(swrite(w.ctl, "del\n") != 4)
+		return False;
+	w.wdormant();
+	w.ctl = nil;
+	w.event = nil;
+	return True;
+}
+
+Win.wname(w : self ref Win, s : string)
+{
+	w.ctlwrite("name " + s + "\n");
+}
+
+Win.wclean(w : self ref Win)
+{
+	if(w.body != nil)
+		w.body.flush();
+	w.ctlwrite("clean\n");
+}
+
+Win.wdormant(w : self ref Win)
+{
+	w.addr = nil;
+	if(w.body != nil){
+		w.body.close();
+		w.body = nil;
+	}
+	w.data = nil;
+}
+
+Win.getec(w : self ref Win) : int
+{
+	if(w.nbuf == 0){
+		w.nbuf = read(w.event, w.buf, len w.buf);
+		if(w.nbuf <= 0 && !killing) {
+			error("event read error: %r");
+		}
+		w.bufp = 0;
+	}
+	w.nbuf--;
+	return int w.buf[w.bufp++];
+}
+
+Win.geten(w : self ref Win) : int
+{
+	n, c : int;
+
+	n = 0;
+	while('0'<=(c=w.getec()) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' ')
+		error("event number syntax");
+	return n;
+}
+
+Win.geter(w : self ref Win, buf : array of byte) : (int, int)
+{
+	r, m, n, ok : int;
+
+	r = w.getec();
+	buf[0] = byte r;
+	n = 1;
+	if(r >= Runeself) {
+		for (;;) {
+			(r, m, ok) = byte2char(buf[0:n], 0);
+			if (m > 0)
+				return (r, n);
+			buf[n++] = byte w.getec();
+		}
+	}
+	return (r, n);
+}
+
+Win.wevent(w : self ref Win, e : ref Event)
+{
+	i, nb : int;
+
+	e.c1 = w.getec();
+	e.c2 = w.getec();
+	e.q0 = w.geten();
+	e.q1 = w.geten();
+	e.flag = w.geten();
+	e.nr = w.geten();
+	if(e.nr > EVENTSIZE)
+		error("event string too long");
+	e.nb = 0;
+	for(i=0; i<e.nr; i++){
+		(e.r[i], nb) = w.geter(e.b[e.nb:]);
+		e.nb += nb;
+	}
+	e.r[e.nr] = 0;
+	e.b[e.nb] = byte 0;
+	c := w.getec();
+	if(c != '\n')
+		error("event syntax 2");
+}
+
+Win.wslave(w : self ref Win, ce : chan of Event)
+{
+	e : ref Event;
+
+	e = newevent();
+	for(;;){
+		w.wevent(e);
+		ce <-= *e;
+	}
+}
+
+Win.wwriteevent(w : self ref Win, e : ref Event)
+{
+	fprint(w.event, "%c%c%d %d\n", e.c1, e.c2, e.q0, e.q1);
+}
+
+Win.wreadall(w : self ref Win) : string
+{
+	s, t : string;
+
+	if(w.body != nil)
+		w.body.close();
+	w.openbody(OREAD);
+	s = nil;
+	while ((t = w.body.gets('\n')) != nil)
+		s += t;
+	w.body.close();
+	w.body = nil;
+	return s;
+}
+
+None, Unknown, Ignore, CC, From, ReplyTo, Sender, Subject, Re, To, Date, Received : con iota;
+NHeaders : con 200;
+
+Hdrs : adt {
+	name : string;
+	typex : int;
+};
+
+
+hdrs := array[NHeaders+1] of {
+	Hdrs ( "CC:",				CC ),
+	Hdrs ( "From:",				From ),
+	Hdrs ( "Reply-To:",			ReplyTo ),
+	Hdrs ( "Sender:",			Sender ),
+	Hdrs ( "Subject:",			Subject ),
+	Hdrs ( "Re:",				Re ),
+	Hdrs ( "To:",				To ),
+	Hdrs ( "Date:",				Date),
+	Hdrs ( "Received:",			Received),
+ * => Hdrs ( "",					0 ),
+};
+
+StRnCmP(s : string, t : string, n : int) : int
+{
+	c, d, i, j : int;
+
+	i = j = 0;
+	if (len s < n || len t < n)
+		return -1;
+	while(n > 0){
+		c = s[i++];
+		d = t[j++];
+		--n;
+		if(c != d){
+			if('a'<=c && c<='z')
+				c -= 'a'-'A';
+			if('a'<=d && d<='z')
+				d -= 'a'-'A';
+			if(c != d)
+				return c-d;
+		}
+	}
+	return 0;
+}
+
+readhdr(b : ref Box) : (string, int)
+{
+	i, j, n, m, typex : int;
+	s, t : string;
+
+{
+	s = b.readline();
+	n = len s;
+	if(n <= 0) {
+		b.unreadline();
+		raise("e");
+	}
+	for(i=0; i<n; i++){
+		j = s[i];
+		if(i>0 && j == ':')
+			break;
+		if(j<'!' || '~'<j){
+			b.unreadline();
+			raise("e");
+		}
+	}
+	typex = Unknown;
+	for(i=0; hdrs[i].name != nil; i++){
+		j = len hdrs[i].name;
+		if(StRnCmP(hdrs[i].name, s, j) == 0){
+			typex = hdrs[i].typex;
+			break;
+		}
+	}
+	# scan for multiple sublines 
+	for(;;){
+		t = b.readline();
+		m = len t;
+		if(m<=0 || (t[0]!=' ' && t[0]!='\t')){
+			b.unreadline();
+			break;
+		}
+		# absorb 
+		s += t;
+	}
+	return(s, typex);
+}
+exception{
+	"*" =>
+		return (nil, None);
+}
+}
+
+Mesg.read(b : ref Box) : ref Mesg
+{
+	m : ref Mesg;
+	s : string;
+	n, typex : int;
+
+	for(;;){
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			return nil;
+		if(n >= 5 && (s[0:5] == "From:" || s[0:5] == "From "))
+			break;
+	}
+{
+	if(n < 5 || (s[0:5] !="From " && s[0:5] != "From:"))
+		raise("e");
+	m = newmesg();
+	m.popno = b.popno;
+	if (m.popno == 0)
+		error("bad pop3 id");
+	m.realhdr = s;
+	# toss 'From ' 
+	s = s[5:];
+	n -= 5;
+	# toss spaces/tabs
+	while (n > 0 && (s[0] == ' ' || s[0] == '\t')) {
+		s = s[1:];
+		n--;
+	}
+	m.hdr = s;
+	# convert first blank to tab 
+	s0 := strchr(m.hdr, ' ');
+	if(s0 >= 0){
+		m.hdr[s0] = '\t';
+		# drop trailing seconds, time zone, and year if match local year 
+		t := n-6;
+		if(t <= 0)
+			raise("e");
+		if(m.hdr[t:n-1] == date[23:]){
+			m.hdr = m.hdr[0:t] + "\n";	# drop year for sure
+			t = -1;
+			s1 := strchr(m.hdr[s0:], ':');
+			if(s1 >= 0)
+				t = strchr(m.hdr[s0+s1+1:], ':');
+			if(t >= 0)	# drop seconds and time zone 
+				m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			else{	# drop time zone 
+				t = strchr(m.hdr[s0+s1+1:], ' ');
+				if(t >= 0)
+					m.hdr = m.hdr[0:s0+s1+t+1] + "\n";
+			}
+			n = len m.hdr;
+		}
+	}
+	m.lline1 = n;
+	m.text = nil;
+	# read header 
+loop:
+	for(;;){
+		(s, typex) = readhdr(b);
+		case(typex){
+		None =>
+			break loop;
+		ReplyTo =>
+			m.replyto = s[9:];
+			break;
+		From =>
+			if(m.replyto == nil)
+				m.replyto = s[5:];
+			break;
+		Subject =>
+			m.subj = s[8:];
+			break;
+		Re =>
+			m.subj = s[3:];
+			break;
+		Date =>
+			break;
+		}
+		m.realhdr += s;
+		if(typex != Ignore)
+			m.hdr += s;
+	}
+	# read body 
+	for(;;){
+		s = b.readline();
+		n = len s;
+		if(n <= 0)
+			break;
+#		if(len s >= 5 && (s[0:5] == "From " || s[0:5] == "From:")){
+#			b.unreadline();
+#			break;
+#		}
+		m.text += s;
+	}
+	# remove trailing "morF\n" 
+	l := len m.text;
+	if(l>6 && m.text[l-6:] == "\nmorF\n")
+		m.text = m.text[0:l-5];
+	m.box = b;
+	return m;
+}
+exception{
+	"*" =>
+		error("malformed header " + s);
+		return nil;
+}
+}
+
+Mesg.mkmail(b : ref Box, hdr : string)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	r.hdr = hdr + "\n";
+	r.lline1 = len r.hdr;
+	r.text = nil;
+	r.box = b;
+	r.open();
+	r.w.wdormant();
+}
+
+replyaddr(r : string) : string
+{
+	p, q, rr : int;
+
+	rr = 0;
+	while(r[rr]==' ' || r[rr]=='\t')
+		rr++;
+	r = r[rr:];
+	p = strchr(r, '<');
+	if(p >= 0){
+		q = strchr(r[p+1:], '>');
+		if(q < 0)
+			r = r[p+1:];
+		else
+			r = r[p+1:p+q] + "\n";
+		return r;
+	}
+	p = strchr(r, '(');
+	if(p >= 0){
+		q = strchr(r[p:], ')');
+		if(q < 0)
+			r = r[0:p];
+		else
+			r = r[0:p] + r[p+q+1:];
+	}
+	return r;
+}
+
+Mesg.mkreply(m : self ref Mesg)
+{
+	r : ref Mesg;
+
+	r = newmesg();
+	if(m.replyto != nil){
+		r.hdr = replyaddr(m.replyto);
+		r.lline1 = len r.hdr;
+	}else{
+		r.hdr = m.hdr[0:m.lline1];
+		r.lline1 = m.lline1;	# was len m.hdr;
+	}
+	if(m.subj != nil){
+		if(StRnCmP(m.subj, "re:", 3)==0 || StRnCmP(m.subj, " re:", 4)==0)
+			r.text = "Subject:" + m.subj + "\n";
+		else
+			r.text = "Subject: Re:" + m.subj + "\n";
+	}
+	else
+		r.text = nil;
+	r.box = m.box;
+	r.open();
+	r.w.wselect("$");
+	r.w.wdormant();
+}
+
+Mesg.free(m : self ref Mesg)
+{
+	m.text = nil;
+	m.hdr = nil;
+	m.subj = nil;
+	m.realhdr = nil;
+	m.replyto = nil;
+	m = nil;
+}
+
+replyid : ref Ref;
+
+initreply()
+{
+	replyid = Ref.init();
+}
+
+Mesg.open(m : self ref Mesg)
+{
+	buf, s : string;
+
+	if(m.isopen)
+		return;
+	m.w = Win.wnew();
+	if(m.id != 0)
+		m.w.wwritebody("From ");
+	m.w.wwritebody(m.hdr);
+	m.w.wwritebody(m.text);
+	if(m.id){
+		buf = sprint("Mail/box/%d", m.id);
+		m.w.wtagwrite("Reply Delmesg Save");
+	}else{
+		buf = sprint("Mail/%s/Reply%d", s, replyid.inc());
+		m.w.wtagwrite("Post");
+	}
+	m.w.wname(buf);
+	m.w.wclean();
+	m.w.wselect("0");
+	m.isopen = True;
+	m.posted = False;
+	spawn m.slave();
+}
+
+Mesg.putpost(m : self ref Mesg, e : ref Event)
+{
+	if(m.posted || m.id==0)
+		return;
+	if(e.q0 >= len m.hdr+5)	# include "From " 
+		return;
+	m.w.wtagwrite(" Post");
+	m.posted = True;
+	return;
+}
+
+Mesg.slave(m : self ref Mesg)
+{
+	e, e2, ea, etoss, eq : ref Event;
+	s : string;
+	na : int;
+
+	e = newevent();
+	e2 = newevent();
+	ea = newevent();
+	etoss = newevent();
+	for(;;){
+		m.w.wevent(e);
+		case(e.c1){
+		'E' =>	# write to body; can't affect us 
+			break;
+		'F' =>	# generated by our actions; ignore 
+			break;
+		'K' or 'M' =>	# type away; we don't care 
+			case(e.c2){
+			'x' or 'X' =>	# mouse only 
+				eq = e;
+				if(e.flag & 2){
+					m.w.wevent(e2);
+					eq = e2;
+				}
+				if(e.flag & 8){
+					m.w.wevent(ea);
+					m.w.wevent(etoss);
+					na = ea.nb;
+				}else
+					na = 0;
+				if(eq.q1>eq.q0 && eq.nb==0)
+					s = m.w.wread(eq.q0, eq.q1);
+				else
+					s = string eq.b[0:eq.nb];
+				if(na)
+					s = s + " " + string ea.b[0:ea.nb];
+				if(!m.command(s))	# send it back 
+					m.w.wwriteevent(e);
+				s = nil;
+				break;
+			'l' or 'L' =>	# mouse only 
+				if(e.flag & 2)
+					m.w.wevent(e2);
+				# just send it back 
+				m.w.wwriteevent(e);
+				break;
+			'I' or 'D' =>	# modify away; we don't care 
+				m.putpost(e);
+				break;
+			'd' or 'i' =>
+				break;
+			* =>
+				fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+				break;
+			}
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	}
+}
+
+Mesg.command(m : self ref Mesg, s : string) : int
+{
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(s == "Post"){
+		m.send();
+		return True;
+	}
+	if(len s >= 4 && s[0:4] == "Save"){
+		s = s[4:];
+		while(len s > 0 && (s[0]==' ' || s[0]=='\t' || s[0]=='\n'))
+			s = s[1:];
+		if(s == nil)
+			m.save("stored");
+		else{
+			ss := 0;
+			while(ss < len s && s[ss]!=' ' && s[ss]!='\t' && s[ss]!='\n')
+				ss++;
+			m.save(s[0:ss]);
+		}
+		return True;
+	}
+	if(s == "Reply"){
+		m.mkreply();
+		return True;
+	}
+	if(s == "Del"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			exit;
+		}
+		return True;
+	}
+	if(s == "Delmesg"){
+		if(m.w.wdel(False)){
+			m.isopen = False;
+			m.box.cdel <-= m;
+			exit;
+		}
+		return True;
+	}
+	return False;
+}
+
+Mesg.save(m : self ref Mesg, base : string)
+{
+	s, buf : string;
+	n : int;
+	fd : ref FD;
+	b : ref Iobuf;
+
+	if(m.id <= 0){
+		fprint(stderr, "can't save reply message; mail it to yourself\n");
+		return;
+	}
+	buf = nil;
+	s = base;
+{
+	if(access(s) < 0)
+		raise("e");
+	fd = tryopen(s, OWRITE);
+	if(fd == nil)
+		raise("e");
+	buf = nil;
+	b = bufio->fopen(fd, OWRITE);
+	# seek to end in case file isn't append-only 
+	b.seek(big 0, 2);
+	# use edited headers: first line of real header followed by remainder of selected ones 
+	for(n=0; n<len m.realhdr && m.realhdr[n++]!='\n'; )
+		;
+	b.puts(m.realhdr[0:n]);
+	b.puts(m.hdr[m.lline1:]);
+	b.puts(m.text);
+	b.close();
+	b = nil;
+	fd = nil;
+}
+exception{
+	"*" =>
+		buf = nil;
+		fprint(stderr, "mail: can't open %s: %r\n", base);
+		return;
+}
+}
+
+Mesg.send(m : self ref Mesg)
+{
+	s, buf : string;
+	t, u : int;
+	a, b : list of string;
+	n : int;
+	p : array of ref FD;
+	c : chan of int;
+
+	p = array[2] of ref FD;
+	s = m.w.wreadall();
+	a = "sendmail" :: nil;
+	if(len s >= 5 && (s[0:5] == "From " || s[0:5] == "From:"))
+		s = s[5:];
+	for(t=0; t < len s && s[t]!='\n' && s[t]!='\t';){
+		while(t < len s && (s[t]==' ' || s[t]==','))
+			t++;
+		u = t;
+		while(t < len s && s[t]!=' ' && s[t]!=',' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		if(t == u)
+			break;
+		a = s[u:t] :: a;
+	}
+	b = nil;
+	for ( ; a != nil; a = tl a)
+		b = hd a :: b;
+	a = b;
+	while(t < len s && s[t]!='\n')
+		t++;
+	if(s[t] == '\n')
+		t++;
+	if(pipe(p) < 0)
+		error("can't pipe: %r");
+	c = chan of int;
+	spawn run(a, c, p[0]);
+	<-c;
+	c = nil;
+	p[0] = nil;
+	n = len s - t;
+	if(swrite(p[1], s[t:]) != n)
+		fprint(stderr, "write to pipe failed: %r\n");
+	p[1] = nil;
+	# run() frees the arg list 
+	buf = sprint("Mail/box/%d-R", m.id);
+	m.w.wname(buf);
+	m.w.wclean();
+}
+
+Box.read(readonly : int) : ref Box
+{
+	b : ref Box;
+	m : ref Mesg;
+	buf : string;
+
+	b = ref Box;
+	b.nm = 0;
+	b.leng = 0;
+	b.readonly = readonly;
+	b.w = Win.wnew();
+	b.w.wwritebody("Password:");
+	b.w.wname("Mail/box/");
+	b.w.wclean();
+	b.w.wselect("$");
+	b.w.ctlwrite("noecho\n");
+	b.cevent = chan of Event;
+	spawn b.w.wslave(b.cevent);
+	e := ref Event;
+	for (;;) {
+		sleep(1000);
+		s := b.w.wreadall();
+		lens := len s;
+		if (lens >= 10 && s[0:9] == "Password:" && s[lens-1] == '\n') {
+			pwd = s[9:lens-1];
+			for (i := 0; i < lens; i++)
+				s[i] = '\b';
+			b.w.wwritebody(s);
+			break;
+		}
+		alt {
+			*e = <-b.cevent =>
+				b.event(e);
+				break;
+			* =>
+				break;
+		}
+	}
+	b.w.ctlwrite("echo\n");
+	pop3open(1);
+	pop3init(b);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		m.id = b.nm;
+	}
+	pop3close(1);
+	if (b.leng != b.nm)
+		error("bad message count in Box.read()");
+	# b.w = Win.wnew();
+	for(m=b.m; m != nil; m=m.next){
+		if(m.subj != nil)
+			buf = sprint("%d\t%s\t %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wwritebody(buf);
+	}
+	# b.w.wname("Mail/box/");
+	if(b.readonly)
+		b.w.wtagwrite("Mail");
+	else
+		b.w.wtagwrite("Put Mail");
+	b.w.wsetdump("/acme/mail", "Mail box");
+	b.w.wclean();
+	b.w.wselect("0");
+	b.w.wdormant();
+	b.cdel= chan of ref Mesg;
+	b.cmore = chan of int;
+	b.clean = True;
+	return b;
+}
+
+Box.readmore(b : self ref Box, lck : int)
+{
+	m : ref Mesg;
+	new : int;
+	buf : string;
+
+	new = False;
+	leng := b.leng;
+	n := 0;
+	pop3open(lck);
+	pop3more(b);
+	while((m = m.read(b)) != nil){
+		m.next = b.m;
+		b.m = m;
+		b.nm++;
+		n++;
+		m.id = b.nm;
+		if(m.subj != nil)
+			buf  = sprint("%d\t%s\t  %s", m.id, m.hdr[0:m.lline1], m.subj);
+		else
+			buf = sprint("%d\t%s", m.id, m.hdr[0:m.lline1]);
+		b.w.wreplace("0", buf);
+		new = True;
+	}
+	pop3close(1);
+	if (b.leng != leng+n)
+		error("bad message count in Box.readmore()");
+	if(new){
+		if(b.clean)
+			b.w.wclean();
+		b.w.wselect("0;/.*(\\n[ \t].*)*");
+		b.w.wshow();
+	}
+	b.w.wdormant();
+}
+
+Box.readline(b : self ref Box) : string
+{
+    	for (;;) {
+		if(b.peekline != nil){
+			b.line = b.peekline;
+			b.peekline = nil;
+		}else
+			b.line = pop3next(b);
+		# nulls appear in mailboxes! 
+		if(b.line != nil && strchr(b.line, 0) >= 0)
+			;
+		else
+			break;
+	}
+	return b.line;
+}
+
+Box.unreadline(b : self ref Box)
+{
+	b.peekline = b.line;
+}
+
+Box.slave(b : self ref Box)
+{
+	e : ref Event;
+	m : ref Mesg;
+
+	e = newevent();
+	for(;;){
+		alt{
+		*e = <-b.cevent =>
+			b.event(e);
+			break;
+		<-b.cmore =>
+			b.readmore(1);
+			break;
+		m = <-b.cdel =>
+			b.mdel(m);
+			break;
+		}
+	}
+}
+
+Box.event(b : self ref Box, e : ref Event)
+{
+	e2, ea, eq : ref Event;
+	s : string;
+	t : int;
+	n, na, nopen : int;
+
+	e2 = newevent();
+	ea = newevent();
+	case(e.c1){
+	'E' =>	# write to body; can't affect us 
+		break;
+	'F' =>	# generated by our actions; ignore 
+		break;
+	'K' =>	# type away; we don't care 
+		break;
+	'M' =>
+		case(e.c2){
+		'x' or 'X' =>
+			if(e.flag & 2)
+				*e2 = <-b.cevent;
+			if(e.flag & 8){
+				*ea = <-b.cevent;
+				na = ea.nb;
+				<- b.cevent;
+			}else
+				na = 0;
+			s = string e.b[0:e.nb];
+			# if it's a known command, do it 
+			if((e.flag&2) && e.nb==0)
+				s = string e2.b[0:e2.nb];
+			if(na)
+				s = sprint("%s %s", s, string ea.b[0:ea.nb]);
+			# if it's a long message, it can't be for us anyway 
+			if(!b.command(s))	# send it back 
+				b.w.wwriteevent(e);
+			if(na)
+				s = nil;
+			break;
+		'l' or 'L' =>
+			eq = e;
+			if(e.flag & 2){
+				*e2 = <-b.cevent;
+				eq = e2;
+			}
+			s = string eq.b[0:eq.nb];
+			if(eq.q1>eq.q0 && eq.nb==0)
+				s = b.w.wread(eq.q0, eq.q1);
+			nopen = 0;
+			do{
+				t = 0;
+				(n, t) = strtoi(s);
+				if(n>0 && (t == len s || s[t]==' ' || s[t]=='\t' || s[t]=='\n')){
+					b.mopen(n);
+					nopen++;
+					s = s[t:];
+				}
+				while(s != nil && s[0]!='\n')
+					s = s[1:];
+			}while(s != nil);
+			if(nopen == 0)	# send it back 
+				b.w.wwriteevent(e);
+			break;
+		'I' or 'D' or 'd' or 'i' =>	# modify away; we don't care 
+			break;
+		* =>
+			fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+			break;
+		}
+	* =>
+		fprint(stdout, "unknown message %c%c\n", e.c1, e.c2);
+		break;
+	}
+}
+
+Box.mopen(b : self ref Box, id : int)
+{
+	m : ref Mesg;
+
+	for(m=b.m; m != nil; m=m.next)
+		if(m.id == id){
+			m.open();
+			break;
+		}
+}
+
+Box.mdel(b : self ref Box, dm : ref Mesg)
+{
+	m : ref Mesg;
+	buf : string;
+
+	if(dm.id){
+		for(m=b.m; m!=nil && m!=dm; m=m.next)
+			;
+		if(m == nil)
+			error(sprint("message %d not found", dm.id));
+		m.deleted = 1;
+		# remove from screen: use acme to help 
+		buf = sprint("/^%d	.*\\n(^[ \t].*\\n)*/", m.id);
+		b.w.wreplace(buf, "");
+	}
+	dm.free();
+	b.clean = False;
+}
+
+Box.command(b : self ref Box, s : string) : int
+{
+	t : int;
+	m : ref Mesg;
+
+	while(s[0]==' ' || s[0]=='\t' || s[0]=='\n')
+		s = s[1:];
+	if(len s >= 4 && s[0:4] == "Mail"){
+		s = s[4:];
+		while(s != nil && (s[0]==' ' || s[0]=='\t' || s[0]=='\n'))
+			s = s[1:];
+		t = 0;
+		while(t < len s && s[t] && s[t]!=' ' && s[t]!='\t' && s[t]!='\n')
+			t++;
+		m = b.m;		# avoid warning message on b.m.mkmail(...)
+		m.mkmail(b, s[0:t]);
+		return True;
+	}
+	if(s == "Del"){
+
+		if(!b.clean){
+			b.clean = True;
+			fprint(stderr, "mail: mailbox not written\n");
+			return True;
+		}
+		postnote(PNGROUP, pctl(0, nil), "kill");
+		killing = 1;
+		pctl(NEWPGRP, nil);
+		b.w.wdel(True);
+		for(m=b.m; m != nil; m=m.next)
+			m.w.wdel(False);
+		exit;
+		return True;
+	}
+	if(s == "Put"){
+		if(b.readonly)
+			fprint(stderr, "Mail is read-only\n");
+		else
+			b.rewrite();
+		return True;
+	}
+	return False;
+}
+
+Box.rewrite(b : self ref Box)
+{
+	prev, m : ref Mesg;
+
+	if(b.clean){
+		b.w.wclean();
+		return;
+	}
+	prev = nil;
+	pop3open(1);
+	for(m=b.m; m!=nil; m=m.next) {
+		if (m.deleted && pop3del(m.popno) >= 0) {
+			b.leng--;
+			if (prev == nil)
+				b.m=m.next;
+			else
+				prev.next=m.next;
+		}
+		else
+			prev = m;
+	}
+	# must update pop nos now so don't unlock pop3
+	pop3close(0);
+	b.w.wclean();
+	b.clean = True;
+	b.readmore(0);	# updates pop nos
+}
--- /dev/null
+++ b/appl/acme/acme/mail/src/mashfile
@@ -1,0 +1,18 @@
+make -clear;
+
+MOD=module;
+DISBIN=/acme/mail;
+LFLAGS=-I/$MOD -gw;
+
+fn lcom {
+	limbo $LFLAGS $args;
+};
+
+TARG=mail.dis;
+
+*.dis 			:~ $1.b 		{ lcom $1.b };
+$DISBIN/*.dis 	:~ $1.dis 		{ cp $1.dis $DISBIN };
+
+default : $TARG {};
+all : $TARG {};
+install : $DISBIN/*.dis {};
--- /dev/null
+++ b/appl/acme/acme/mail/src/mkfile
@@ -1,0 +1,16 @@
+<../../../../../mkconfig
+
+TARG=\
+	Mail.dis\
+	Mailpop3.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	sh.m\
+	sys.m\
+	draw.m\
+
+DISBIN=$ROOT/acme/mail
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/acme/acme/mkfile
@@ -1,0 +1,9 @@
+<../../../mkconfig
+
+DIRS=\
+	acid\
+	bin\
+	edit\
+	mail\
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/acme/buff.b
@@ -1,0 +1,380 @@
+implement Bufferm;
+
+include "common.m";
+
+sys : Sys;
+dat : Dat;
+utils : Utils;
+diskm : Diskm;
+ecmd: Editcmd;
+
+FALSE, TRUE, XXX, Maxblock, Astring : import Dat;
+Block : import Dat;
+disk : import dat;
+Disk : import diskm;
+File: import Filem;
+error, warning, min : import utils;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	diskm = mods.diskm;
+	ecmd = mods.editcmd;
+}
+
+nullbuffer : Buffer;
+
+newbuffer() : ref Buffer
+{
+	b := ref nullbuffer;
+	return b;
+}
+
+Slop : con 100;	# room to grow with reallocation
+
+Buffer.sizecache(b : self ref Buffer, n : int)
+{
+	if(n <= b.cmax)
+		return;
+	b.cmax = n+Slop;
+	os := b.c;
+	b.c = utils->stralloc(b.cmax);
+	if (os != nil) {
+		loss := len os.s;
+		c := b.c;
+		oss := os.s;
+		for (i := 0; i < loss && i < b.cmax; i++)
+			c.s[i] = oss[i];
+		utils->strfree(os);
+	}
+}
+
+#
+# Move cache so b.cq <= q0 < b.cq+b.cnc.
+# If at very end, q0 will fall on end of cache block.
+#
+
+Buffer.flush(b : self ref Buffer)
+{
+	if(b.cdirty || b.cnc==0){
+		if(b.cnc == 0)
+			b.delblock(b.cbi);
+		else
+			b.bl[b.cbi] = disk.write(b.bl[b.cbi], b.c.s, b.cnc);
+		b.cdirty = FALSE;
+	}
+}
+
+Buffer.setcache(b : self ref Buffer, q0 : int)
+{
+	blp, bl : ref Block;
+	i, q : int;
+
+	if (q0 > b.nc)
+		error("bad assert in setcache");
+
+	# flush and reload if q0 is not in cache.
+	 
+	if(b.nc == 0 || (b.cq<=q0 && q0<b.cq+b.cnc))
+		return;
+
+	# if q0 is at end of file and end of cache, continue to grow this block
+	 
+	if(q0==b.nc && q0==b.cq+b.cnc && b.cnc<Maxblock)
+		return;
+	b.flush();
+	# find block 
+	if(q0 < b.cq){
+		q = 0;
+		i = 0;
+	}else{
+		q = b.cq;
+		i = b.cbi;
+	}
+	blp = b.bl[i];
+	while(q+blp.n <= q0 && q+blp.n < b.nc){
+		q += blp.n;
+		i++;
+		blp = b.bl[i];
+		if(i >= b.nbl)
+			error("block not found");
+	}
+	bl = blp;
+	# remember position 
+	b.cbi = i;
+	b.cq = q;
+	b.sizecache(bl.n);
+	b.cnc = bl.n;
+	#read block
+	disk.read(bl, b.c, b.cnc);
+}
+
+Buffer.addblock(b : self ref Buffer, i : int, n : int)
+{
+	if (i > b.nbl)
+		error("bad assert in addblock");
+
+	obl := b.bl;
+	b.bl = array[b.nbl+1] of ref Block;
+	b.bl[0:] = obl[0:i];
+	if(i < b.nbl)
+		b.bl[i+1:] = obl[i:b.nbl];
+	b.bl[i] = disk.new(n);
+	b.nbl++;
+	obl = nil;
+}
+
+Buffer.delblock(b : self ref Buffer, i : int)
+{
+	if (i >= b.nbl)
+		error("bad assert in delblock");
+
+	disk.release(b.bl[i]);
+	obl := b.bl;
+	b.bl = array[b.nbl-1] of ref Block;
+	b.bl[0:] = obl[0:i];
+	if(i < b.nbl-1)
+		b.bl[i:] = obl[i+1:b.nbl];
+	b.nbl--;
+	obl = nil;
+}
+
+Buffer.insert(b : self ref Buffer, q0 : int, s : string, n : int)
+{
+	i, j,  m, t, off, p : int;
+
+	if (q0>b.nc)
+		error("bad assert in insert");
+	p = 0;
+	while(n > 0){
+		b.setcache(q0);
+		off = q0-b.cq;
+		if(b.cnc+n <= Maxblock){
+			# Everything fits in one block. 
+			t = b.cnc+n;
+			m = n;
+			if(b.bl == nil){	# allocate 
+				if (b.cnc != 0)
+					error("bad assert in insert");
+				b.addblock(0, t);
+				b.cbi = 0;
+			}
+			b.sizecache(t);
+			c := b.c;
+			# cs := c.s;
+			for (j = b.cnc-1; j >= off; j--)
+				c.s[j+m] = c.s[j];
+			for (j = 0; j < m; j++)
+				c.s[off+j] = s[p+j];
+			b.cnc = t;
+		}
+		#
+		# We must make a new block.  If q0 is at
+		# the very beginning or end of this block,
+		# just make a new block and fill it.
+		#
+		else if(q0==b.cq || q0==b.cq+b.cnc){
+			if(b.cdirty)
+				b.flush();
+			m = min(n, Maxblock);
+			if(b.bl == nil){	# allocate 
+				if (b.cnc != 0)
+					error("bad assert in insert");
+				i = 0;
+			}else{
+				i = b.cbi;
+				if(q0 > b.cq)
+					i++;
+			}
+			b.addblock(i, m);
+			b.sizecache(m);
+			c := b.c;
+			for (j = 0; j < m; j++)
+				c.s[j] = s[p+j];
+			b.cq = q0;
+			b.cbi = i;
+			b.cnc = m;
+		}
+		else {
+			#
+		 	# Split the block; cut off the right side and
+		 	# let go of it.
+			#
+		 
+			m = b.cnc-off;
+			if(m > 0){
+				i = b.cbi+1;
+				b.addblock(i, m);
+				b.bl[i] = disk.write(b.bl[i], b.c.s[off:], m);
+				b.cnc -= m;
+			}
+			#
+			# Now at end of block.  Take as much input
+			# as possible and tack it on end of block.
+			#
+		 
+			m = min(n, Maxblock-b.cnc);
+			b.sizecache(b.cnc+m);
+			c := b.c;
+			for (j = 0; j < m; j++)
+				c.s[j+b.cnc] = s[p+j];
+			b.cnc += m;
+		}
+		b.nc += m;
+		q0 += m;
+		p += m;
+		n -= m;
+		b.cdirty = TRUE;
+	}
+}
+
+Buffer.delete(b : self ref Buffer, q0 : int, q1 : int)
+{
+	m, n, off : int;
+
+	if (q0>q1 || q0>b.nc || q1>b.nc)
+		error("bad assert in delete");
+
+	while(q1 > q0){
+		b.setcache(q0);
+		off = q0-b.cq;
+		if(q1 > b.cq+b.cnc)
+			n = b.cnc - off;
+		else
+			n = q1-q0;
+		m = b.cnc - (off+n);
+		if(m > 0) {
+			c := b.c;
+			# cs := c.s;
+			p := m+off;
+			for (j := off; j < p; j++)
+				c.s[j] = c.s[j+n];
+		}
+		b.cnc -= n;
+		b.cdirty = TRUE;
+		q1 -= n;
+		b.nc -= n;
+	}
+}
+
+# Buffer.replace(b: self ref Buffer, q0: int, q1: int, s: string, n: int)
+# {
+#	if(q0>q1 || q0>b.nc || q1>b.nc || n != q1-q0)
+#		error("bad assert in replace");
+#	p := 0;
+#	while(q1 > q0){
+#		b.setcache(q0);
+#		off := q0-b.cq;
+#		if(q1 > b.cq+b.cnc)
+#			n = b.cnc-off;
+#		else
+#			n = q1-q0;
+#		c := b.c;
+#		for(i := 0; i < n; i++)
+#			c.s[i+off] = s[i+p];
+#		b.cdirty = TRUE;
+#		q0 += n;
+#		p += n;
+#	}	
+# }
+
+pbuf : array of byte;
+
+bufloader(b: ref Buffer, q0: int, r: string, nr: int): int
+{
+	b.insert(q0, r, nr);
+	return nr;
+}
+
+loadfile(fd: ref Sys->FD, q0: int, fun: int, b: ref Buffer, f: ref File): int
+{
+	p : array of byte;
+	r : string;
+	m, n, nb, nr : int;
+	q1 : int;
+
+	if (pbuf == nil)
+		pbuf = array[Maxblock+Sys->UTFmax] of byte;
+	p = pbuf;
+	m = 0;
+	n = 1;
+	q1 = q0;
+	#
+	# At top of loop, may have m bytes left over from
+	# last pass, possibly representing a partial rune.
+	#	 
+	while(n > 0){
+		n = sys->read(fd, p[m:], Maxblock);
+		if(n < 0){
+			warning(nil, "read error in Buffer.load");
+			break;
+		}
+		m += n;
+		nb = sys->utfbytes(p, m);
+		r = string p[0:nb];
+		p[0:] = p[nb:m];
+		m -= nb;
+		nr = len r;
+		if(fun == Dat->BUFL)
+			q1 += bufloader(b, q1, r, nr);
+		else
+			q1 += ecmd->readloader(f, q1, r, nr);
+	}
+	p = nil;
+	r = nil;
+	return q1-q0;
+}
+
+Buffer.loadx(b : self ref Buffer, q0 : int, fd : ref Sys->FD) : int
+{
+	if (q0>b.nc)
+		error("bad assert in load");
+	return loadfile(fd, q0, Dat->BUFL, b, nil);
+}
+
+Buffer.read(b : self ref Buffer, q0 : int, s : ref Astring, p : int, n : int)
+{
+	m : int;
+
+	if (q0>b.nc || q0+n>b.nc)
+		error("bad assert in read");
+	while(n > 0){
+		b.setcache(q0);
+		m = min(n, b.cnc-(q0-b.cq));
+		c := b.c;
+		cs := c.s;
+		for (j := 0; j < m; j++)
+			s.s[p+j] = cs[j+q0-b.cq];
+		q0 += m;
+		p += m;
+		n -= m;
+	}
+}
+
+Buffer.reset(b : self ref Buffer)
+{
+	i : int;
+
+	b.nc = 0;
+	b.cnc = 0;
+	b.cq = 0;
+	b.cdirty = 0;
+	b.cbi = 0;
+	# delete backwards to avoid n² behavior 
+	for(i=b.nbl-1; --i>=0; )
+		b.delblock(i);
+}
+
+Buffer.close(b : self ref Buffer)
+{
+	b.reset();
+	if (b.c != nil) {
+		utils->strfree(b.c);
+		b.c = nil;
+	}
+	b.cnc = 0;
+	b.bl = nil;
+	b.nbl = 0;
+}
--- /dev/null
+++ b/appl/acme/buff.m
@@ -1,0 +1,34 @@
+Bufferm : module {
+	PATH : con "/dis/acme/buff.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	newbuffer : fn() : ref Buffer;
+
+	Buffer : adt {
+		nc : int;
+		c : ref Dat->Astring;		# cache
+		cnc : int;		# bytes in cache
+		cmax : int;	# size of allocated cache
+		cq : int;		# position of cache
+		cdirty : int;	# cache needs to be written
+		cbi : int;		# index of cache Block
+		bl : array of ref Dat->Block;	# array of blocks
+		nbl : int;		# number of blocks
+
+		insert : fn(b : self ref Buffer, n : int, s : string, m : int);
+		delete : fn(b : self ref Buffer, n : int, m : int);
+		# replace : fn(b : self ref Buffer, q0 : int, q1 : int, s : string, n : int);
+		loadx : fn(b : self ref Buffer, n : int, fd : ref Sys->FD) : int;
+		read : fn(b : self ref Buffer, n : int, s : ref Dat->Astring, p, m : int);
+		close : fn(b : self ref Buffer);
+		reset : fn(b : self ref Buffer);
+		sizecache : fn(b : self ref Buffer, n : int);
+		flush : fn(b : self ref Buffer);
+		setcache : fn(b : self ref Buffer, n : int);
+		addblock : fn(b : self ref Buffer, n : int, m : int);
+		delblock : fn(b : self ref Buffer, n : int);
+	};
+
+	loadfile: fn(fd: ref Sys->FD, q1: int, fun: int, b: ref Bufferm->Buffer, f: ref Filem->File): int;
+};
--- /dev/null
+++ b/appl/acme/col.b
@@ -1,0 +1,610 @@
+implement Columnm;
+
+include "common.m";
+
+sys : Sys;
+utils : Utils;
+drawm : Draw;
+acme : Acme;
+graph : Graph;
+gui : Gui;
+dat : Dat;
+textm : Textm;
+rowm : Rowm;
+filem : Filem;
+windowm : Windowm;
+
+FALSE, TRUE, XXX : import Dat;
+Border : import Dat;
+mouse, colbutton : import dat;
+Point, Rect, Image : import drawm;
+draw : import graph;
+min, max, abs, error, clearmouse : import utils;
+black, white, mainwin : import gui;
+Text : import textm;
+Row : import rowm;
+Window : import windowm;
+File : import filem;
+Columntag : import Textm;
+BACK : import Framem;
+tagcols, textcols : import acme;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	drawm = mods.draw;
+	acme = mods.acme;
+	graph = mods.graph;
+	gui = mods.gui;
+	textm = mods.textm;
+	rowm = mods.rowm;
+	filem = mods.filem;
+	windowm = mods.windowm;
+}
+
+Column.init(c : self ref Column, r : Rect)
+{
+	r1 : Rect;
+	t : ref Text;
+	dummy : ref File = nil;
+
+	draw(mainwin, r, white, nil, (0, 0));
+	c.r = r;
+	c.row = nil;
+	c.w = nil;
+	c.nw = 0;
+	c.tag = textm->newtext();
+	t = c.tag;
+	t.w = nil;
+	t.col = c;
+	r1 = r;
+	r1.max.y = r1.min.y + (graph->font).height;
+	t.init(dummy.addtext(t), r1, dat->reffont, tagcols);
+	t.what = Columntag;
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(mainwin, r1, black, nil, (0, 0));
+	t.insert(0, "New Cut Paste Snarf Sort Zerox Delcol ", 38, TRUE, 0);
+	t.setselect(t.file.buf.nc, t.file.buf.nc);
+	draw(mainwin, t.scrollr, colbutton, nil, colbutton.r.min);
+	c.safe = TRUE;
+}
+
+Column.add(c : self ref Column, w : ref Window, clone : ref Window, y : int) : ref Window
+{
+	r, r1 : Rect;
+	v : ref Window;
+	i, t : int;
+
+	v = nil;
+	r = c.r;
+	r.min.y = c.tag.frame.r.max.y+Border;
+	if(y<r.min.y && c.nw>0){	# steal half of last window by default 
+		v = c.w[c.nw-1];
+		y = v.body.frame.r.min.y+v.body.frame.r.dy()/2;
+	}
+	# look for window we'll land on 
+	for(i=0; i<c.nw; i++){
+		v = c.w[i];
+		if(y < v.r.max.y)
+			break;
+	}
+	if(c.nw > 0){
+		if(i < c.nw)
+			i++;	# new window will go after v 
+		#
+		# if v's too small, grow it first.
+		#
+		 
+		if(!c.safe || v.body.frame.maxlines<=3){
+			c.grow(v, 1, 1);
+			y = v.body.frame.r.min.y+v.body.frame.r.dy()/2;
+		}
+		r = v.r;
+		if(i == c.nw)
+			t = c.r.max.y;
+		else
+			t = c.w[i].r.min.y-Border;
+		r.max.y = t;
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		r1 = r;
+		y = min(y, t-(v.tag.frame.font.height+v.body.frame.font.height+Border+1));
+		r1.max.y = min(y, v.body.frame.r.min.y+v.body.frame.nlines*v.body.frame.font.height);
+		r1.min.y = v.reshape(r1, FALSE);
+		r1.max.y = r1.min.y+Border;
+		draw(mainwin, r1, black, nil, (0, 0));
+		r.min.y = r1.max.y;
+	}
+	if(w == nil){
+		w = ref Window;
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		w.col = c;
+		w.init(clone, r);
+	}else{
+		w.col = c;
+		w.reshape(r, FALSE);
+	}
+	w.tag.col = c;
+	w.tag.row = c.row;
+	w.body.col = c;
+	w.body.row = c.row;
+	ocw := c.w;
+	c.w = array[c.nw+1] of ref Window;
+	c.w[0:] = ocw[0:i];
+	c.w[i+1:] = ocw[i:c.nw];
+	ocw = nil;
+	c.nw++;
+	c.w[i] = w;
+	utils->savemouse(w);
+	# near but not on the button 
+	graph->cursorset(w.tag.scrollr.max.add(Point(3, 3)));
+	dat->barttext = w.body;
+	c.safe = TRUE;
+	return w;
+}
+
+Column.close(c : self ref Column, w : ref Window, dofree : int)
+{
+	r : Rect;
+	i : int;
+
+	# w is locked 
+	if(!c.safe)
+		c.grow(w, 1, 1);
+	for(i=0; i<c.nw; i++)
+		if(c.w[i] == w)
+			break;
+	if (i == c.nw)
+		error("can't find window");
+	r = w.r;
+	w.tag.col = nil;
+	w.body.col = nil;
+	w.col = nil;
+	utils->restoremouse(w);
+	if(dofree){
+		w.delete();
+		w.close();
+	}
+	ocw := c.w;
+	c.w = array[c.nw-1] of ref Window;
+	c.w[0:] = ocw[0:i];
+	c.w[i:] = ocw[i+1:c.nw];
+	ocw = nil;
+	c.nw--;
+	if(c.nw == 0){
+		draw(mainwin, r, white, nil, (0, 0));
+		return;
+	}
+	if(i == c.nw){		# extend last window down 
+		w = c.w[i-1];
+		r.min.y = w.r.min.y;
+		r.max.y = c.r.max.y;
+	}else{			# extend next window up 
+		w = c.w[i];
+		r.max.y = w.r.max.y;
+	}
+	draw(mainwin, r, textcols[BACK], nil, (0, 0));
+	if(c.safe)
+		w.reshape(r, FALSE);
+}
+
+Column.closeall(c : self ref Column)
+{
+	i : int;
+	w : ref Window;
+
+	if(c == dat->activecol)
+		dat->activecol = nil;
+	c.tag.close();
+	for(i=0; i<c.nw; i++){
+		w = c.w[i];
+		w.close();
+	}
+	c.nw = 0;
+	c.w = nil;
+	c = nil;
+	clearmouse();
+}
+
+Column.mousebut(c : self ref Column)
+{
+	graph->cursorset(c.tag.scrollr.min.add(c.tag.scrollr.max).div(2));
+}
+
+Column.reshape(c : self ref Column, r : Rect)
+{
+	i : int;
+	r1, r2 : Rect;
+	w : ref Window;
+
+	clearmouse();
+	r1 = r;
+	r1.max.y = r1.min.y + c.tag.frame.font.height;
+	c.tag.reshape(r1);
+	draw(mainwin, c.tag.scrollr, colbutton, nil, colbutton.r.min);
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(mainwin, r1, black, nil, (0, 0));
+	r1.max.y = r.max.y;
+	for(i=0; i<c.nw; i++){
+		w = c.w[i];
+		w.maxlines = 0;
+		if(i == c.nw-1)
+			r1.max.y = r.max.y;
+		else
+			r1.max.y = r1.min.y+(w.r.dy()+Border)*r.dy()/c.r.dy();
+		r2 = r1;
+		r2.max.y = r2.min.y+Border;
+		draw(mainwin, r2, black, nil, (0, 0));
+		r1.min.y = r2.max.y;
+		r1.min.y = w.reshape(r1, FALSE);
+	}
+	c.r = r;
+}
+
+colcmp(a : ref Window, b : ref Window) : int
+{
+	r1, r2 : string;
+
+	r1 = a.body.file.name;
+	r2 = b.body.file.name;
+	if (r1 < r2)
+		return -1;
+	if (r1 > r2)
+		return 1;
+	return 0;
+}
+
+qsort(a : array of ref Window, n : int)
+{
+	i, j : int;
+	t : ref Window;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && colcmp(a[i], a[0]) < 0);
+			do
+				j--;
+			while(j > 0 && colcmp(a[j], a[0]) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+Column.sort(c : self ref Column)
+{
+	i, y : int;
+	r, r1 : Rect;
+	rp : array of Rect;
+	w : ref Window;
+	wp : array of ref Window;
+
+	if(c.nw == 0)
+		return;
+	clearmouse();
+	rp = array[c.nw] of Rect;
+	wp = array[c.nw] of ref Window;
+	wp[0:] = c.w[0:c.nw];
+	qsort(wp, c.nw);
+	for(i=0; i<c.nw; i++)
+		rp[i] = wp[i].r;
+	r = c.r;
+	r.min.y = c.tag.frame.r.max.y;
+	draw(mainwin, r, textcols[BACK], nil, (0, 0));
+	y = r.min.y;
+	for(i=0; i<c.nw; i++){
+		w = wp[i];
+		r.min.y = y;
+		if(i == c.nw-1)
+			r.max.y = c.r.max.y;
+		else
+			r.max.y = r.min.y+w.r.dy()+Border;
+		r1 = r;
+		r1.max.y = r1.min.y+Border;
+		draw(mainwin, r1, black, nil, (0, 0));
+		r.min.y = r1.max.y;
+		y = w.reshape(r, FALSE);
+	}
+	rp = nil;
+	c.w = wp;
+}
+
+Column.grow(c : self ref Column, w : ref Window, but : int, mv : int)
+{
+	r, cr : Rect;
+	i, j, k, l, y1, y2, tot, nnl, onl, dnl, h : int;
+	nl, ny : array of int;
+	v : ref Window;
+
+	for(i=0; i<c.nw; i++)
+		if(c.w[i] == w)
+			break;
+	if (i == c.nw)
+		error("can't find window");
+
+	cr = c.r;
+	if(but < 0){	# make sure window fills its own space properly 
+		r = w.r;
+		if(i == c.nw-1)
+			r.max.y = cr.max.y;
+		else
+			r.max.y = c.w[i+1].r.min.y;
+		w.reshape(r, FALSE);
+		return;
+	}
+	cr.min.y = c.w[0].r.min.y;
+	if(but == 3){	# full size 
+		if(i != 0){
+			v = c.w[0];
+			c.w[0] = w;
+			c.w[i] = v;
+		}
+		draw(mainwin, cr, textcols[BACK], nil, (0, 0));
+		w.reshape(cr, FALSE);
+		for(i=1; i<c.nw; i++)
+			c.w[i].body.frame.maxlines = 0;
+		c.safe = FALSE;
+		return;
+	}
+	# store old #lines for each window 
+	onl = w.body.frame.maxlines;
+	nl = array[c.nw] of int;
+	ny = array[c.nw] of int;
+	tot = 0;
+	for(j=0; j<c.nw; j++){
+		l = c.w[j].body.frame.maxlines;
+		nl[j] = l;
+		tot += l;
+	}
+	# approximate new #lines for this window 
+	if(but == 2){	# as big as can be 
+		for (j = 0; j < c.nw; j++)
+			nl[j] = 0;
+		nl[i] = tot;
+	}
+	else {
+		nnl = min(onl + max(min(5, w.maxlines), onl/2), tot);
+		if(nnl < w.maxlines)
+			nnl = (w.maxlines+nnl)/2;
+		if(nnl == 0)
+			nnl = 2;
+		dnl = nnl - onl;
+		# compute new #lines for each window 
+		for(k=1; k<c.nw; k++){
+			# prune from later window 
+			j = i+k;
+			if(j<c.nw && nl[j]){
+				l = min(dnl, max(1, nl[j]/2));
+				nl[j] -= l;
+				nl[i] += l;
+				dnl -= l;
+			}
+			# prune from earlier window 
+			j = i-k;
+			if(j>=0 && nl[j]){
+				l = min(dnl, max(1, nl[j]/2));
+				nl[j] -= l;
+				nl[i] += l;
+				dnl -= l;
+			}
+		}
+	}
+	# pack everyone above 
+	y1 = cr.min.y;
+	for(j=0; j<i; j++){
+		v = c.w[j];
+		r = v.r;
+		r.min.y = y1;
+		r.max.y = y1+v.tag.all.dy();
+		if(nl[j])
+			r.max.y += 1 + nl[j]*v.body.frame.font.height;
+		if(!c.safe || !v.r.eq(r)){
+			draw(mainwin, r, textcols[BACK], nil, (0, 0));
+			v.reshape(r, c.safe);
+		}
+		r.min.y = v.r.max.y;
+		r.max.y += Border;
+		draw(mainwin, r, black, nil, (0, 0));
+		y1 = r.max.y;
+	}
+	# scan to see new size of everyone below 
+	y2 = c.r.max.y;
+	for(j=c.nw-1; j>i; j--){
+		v = c.w[j];
+		r = v.r;
+		r.min.y = y2-v.tag.all.dy();
+		if(nl[j])
+			r.min.y -= 1 + nl[j]*v.body.frame.font.height;
+		r.min.y -= Border;
+		ny[j] = r.min.y;
+		y2 = r.min.y;
+	}
+	# compute new size of window 
+	r = w.r;
+	r.min.y = y1;
+	r.max.y = r.min.y+w.tag.all.dy();
+	h = w.body.frame.font.height;
+	if(y2-r.max.y >= 1+h+Border){
+		r.max.y += 1;
+		r.max.y += h*((y2-r.max.y)/h);
+	}
+	# draw window 
+	if(!c.safe || !w.r.eq(r)){
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		w.reshape(r, c.safe);
+	}
+	if(i < c.nw-1){
+		r.min.y = r.max.y;
+		r.max.y += Border;
+		draw(mainwin, r, black, nil, (0, 0));
+		for(j=i+1; j<c.nw; j++)
+			ny[j] -= (y2-r.max.y);
+	}
+	# pack everyone below 
+	y1 = r.max.y;
+	for(j=i+1; j<c.nw; j++){
+		v = c.w[j];
+		r = v.r;
+		r.min.y = y1;
+		r.max.y = y1+v.tag.all.dy();
+		if(nl[j])
+			r.max.y += 1 + nl[j]*v.body.frame.font.height;
+		if(!c.safe || !v.r.eq(r)){
+			draw(mainwin, r, textcols[BACK], nil, (0, 0));
+			v.reshape(r, c.safe);
+		}
+		if(j < c.nw-1){	# no border on last window 
+			r.min.y = v.r.max.y;
+			r.max.y += Border;
+			draw(mainwin, r, black, nil, (0, 0));
+		}
+		y1 = r.max.y;
+	}
+	r = w.r;
+	r.min.y = y1;
+	r.max.y = c.r.max.y;
+	draw(mainwin, r, textcols[BACK], nil, (0, 0));
+	nl = nil;
+	ny = nil;
+	c.safe = TRUE;
+	if (mv)
+		w.mousebut();
+}
+
+Column.dragwin(c : self ref Column, w : ref Window, but : int)
+{
+	r : Rect;
+	i, b : int;
+	p, op : Point;
+	v : ref Window;
+	nc : ref Column;
+
+	clearmouse();
+	graph->cursorswitch(dat->boxcursor);
+	b = mouse.buttons;
+	op = mouse.xy;
+	while(mouse.buttons == b)
+		acme->frgetmouse();
+	graph->cursorswitch(dat->arrowcursor);
+	if(mouse.buttons){
+		while(mouse.buttons)
+			acme->frgetmouse();
+		return;
+	}
+
+	for(i=0; i<c.nw; i++)
+		if(c.w[i] == w)
+			break;
+	if (i == c.nw)
+		error("can't find window");
+
+	p = mouse.xy;
+	if(abs(p.x-op.x)<5 && abs(p.y-op.y)<5){
+		c.grow(w, but, 1);
+		w.mousebut();
+		return;
+	}
+	# is it a flick to the right? 
+	if(abs(p.y-op.y)<10 && p.x>op.x+30 && c.row.whichcol(p) == c)
+		p.x += w.r.dx();	# yes: toss to next column 
+	nc = c.row.whichcol(p);
+	if(nc!=nil && nc!=c){
+		c.close(w, FALSE);
+		nc.add(w, nil, p.y);
+		w.mousebut();
+		return;
+	}
+	if(i==0 && c.nw==1)
+		return;			# can't do it 
+	if((i>0 && p.y<c.w[i-1].r.min.y) || (i<c.nw-1 && p.y>w.r.max.y)
+	|| (i==0 && p.y>w.r.max.y)){
+		# shuffle 
+		c.close(w, FALSE);
+		c.add(w, nil, p.y);
+		w.mousebut();
+		return;
+	}
+	if(i == 0)
+		return;
+	v = c.w[i-1];
+	if(p.y < v.tag.all.max.y)
+		p.y = v.tag.all.max.y;
+	if(p.y > w.r.max.y-w.tag.all.dy()-Border)
+		p.y = w.r.max.y-w.tag.all.dy()-Border;
+	r = v.r;
+	r.max.y = p.y;
+	if(r.max.y > v.body.frame.r.min.y){
+		r.max.y -= (r.max.y-v.body.frame.r.min.y)%v.body.frame.font.height;
+		if(v.body.frame.r.min.y == v.body.frame.r.max.y)
+			r.max.y++;
+	}
+	if(!r.eq(v.r)){
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		v.reshape(r, c.safe);
+	}
+	r.min.y = v.r.max.y;
+	r.max.y = r.min.y+Border;
+	draw(mainwin, r, black, nil, (0, 0));
+	r.min.y = r.max.y;
+	if(i == c.nw-1)
+		r.max.y = c.r.max.y;
+	else
+		r.max.y = c.w[i+1].r.min.y-Border;
+	# r.max.y = w.r.max.y;
+	if(!r.eq(w.r)){
+		draw(mainwin, r, textcols[BACK], nil, (0, 0));
+		w.reshape(r, c.safe);
+	}
+	c.safe = TRUE;
+    	w.mousebut();
+}
+
+Column.which(c : self ref Column, p : Point) : ref Text
+{
+	i : int;
+	w : ref Window;
+
+	if(!p.in(c.r))
+		return nil;
+	if(p.in(c.tag.all))
+		return c.tag;
+	for(i=0; i<c.nw; i++){
+		w = c.w[i];
+		if(p.in(w.r)){
+			if(p.in(w.tag.all))
+				return w.tag;
+			return w.body;
+		}
+	}
+	return nil;
+}
+
+Column.clean(c : self ref Column, exiting : int) : int
+{
+	clean : int;
+	i : int;
+
+	clean = TRUE;
+	for(i=0; i<c.nw; i++)
+		clean &= c.w[i].clean(TRUE, exiting);
+	return clean;
+}
--- /dev/null
+++ b/appl/acme/col.m
@@ -1,0 +1,26 @@
+Columnm : module {
+	PATH : con "/dis/acme/col.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	Column : adt {
+		r : Draw->Rect;
+		tag : cyclic ref Textm->Text;
+		row : cyclic ref Rowm->Row;
+		w : cyclic array of ref Windowm->Window;
+		nw : int;
+		safe : int;
+
+		init : fn (c : self ref Column, r : Draw->Rect);
+		add : fn (c : self ref Column, w : ref Windowm->Window, w0 : ref Windowm->Window, n : int) : ref Windowm->Window;
+		close : fn (c : self ref Column, w : ref Windowm->Window, n : int);
+		closeall : fn (c : self ref Column);
+		reshape : fn (c : self ref Column, r : Draw->Rect);
+		which : fn (c : self ref Column, p : Draw->Point) : ref Textm->Text;
+		dragwin : fn (c : self ref Column, w : ref Windowm->Window, n : int);
+		grow : fn (c : self ref Column, w : ref Windowm->Window, m, n : int);
+		clean : fn (c : self ref Column, exiting : int) : int;
+		sort : fn (c : self ref Column);
+		mousebut : fn (c : self ref Column);
+	};
+};
--- /dev/null
+++ b/appl/acme/common.m
@@ -1,0 +1,30 @@
+include "sys.m";
+include "bufio.m";
+include "plumbmsg.m";
+include "workdir.m";
+include "draw.m";
+include "styx.m";
+include "acme.m";
+include "dat.m";
+include "gui.m";
+include "graph.m";
+include "frame.m";
+include "util.m";
+include "regx.m";
+include "text.m";
+include "file.m";
+include "wind.m";
+include "row.m";
+include "col.m";
+include "buff.m";
+include "disk.m";
+include "xfid.m";
+include "exec.m";
+include "look.m";
+include "time.m";
+include "scrl.m";
+include "fsys.m";
+include "edit.m";
+include "elog.m";
+include "ecmd.m";
+include "styxaux.m";
--- /dev/null
+++ b/appl/acme/dat.b
@@ -1,0 +1,107 @@
+implement Dat;
+
+include "common.m";
+
+sys : Sys;
+acme : Acme;
+utils : Utils;
+
+# lc, uc : chan of ref Lock;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	acme = mods.acme;
+	utils = mods.utils;
+
+	mouse = ref Draw->Pointer;
+	mouse.buttons = mouse.msec = 0;
+	mouse.xy = (0, 0);
+	# lc = chan of ref Lock;
+	# uc = chan of ref Lock;
+	# spawn lockmgr();
+}
+
+# lockmgr()
+# {
+# 	l : ref Lock;
+# 
+# 	acme->lockpid = sys->pctl(0, nil);
+# 	for (;;) {
+# 		alt {
+# 			l = <- lc =>
+# 				if (l.cnt++ == 0)
+# 					l.chann <-= 1;
+# 			l = <- uc =>
+# 				if (--l.cnt > 0)
+# 					l.chann <-= 1;
+# 		}
+# 	}
+# }
+
+Lock.init() : ref Lock
+{
+	return ref Lock(0, chan[1] of int);
+	# return ref Lock(0, chan of int);
+}
+
+Lock.lock(l : self ref Lock)
+{
+	l.cnt++;
+	l.chann <-= 0;
+	# lc <-= l;
+	# <- l.chann;
+}
+
+Lock.unlock(l : self ref Lock)
+{
+	<-l.chann;
+	l.cnt--;
+	# uc <-= l;
+}
+
+Lock.locked(l : self ref Lock) : int
+{
+	return l.cnt > 0;
+}
+
+Ref.init() : ref Ref
+{
+	r := ref Ref;
+	r.l = Lock.init();
+	r.cnt = 0;
+	return r;
+}
+
+Ref.inc(r : self ref Ref) : int
+{
+	r.l.lock();
+	i := r.cnt;
+	r.cnt++;
+	r.l.unlock();
+	return i;
+}
+
+Ref.dec(r : self ref Ref) : int
+{
+	r.l.lock();
+	r.cnt--;
+	i := r.cnt;
+	r.l.unlock();
+	return i;
+}
+
+Ref.refx(r : self ref Ref) : int
+{
+	return r.cnt;
+}
+
+Reffont.get(p, q, r : int, b : string) : ref Reffont
+{
+	return acme->get(p, q, r, b);
+}
+
+Reffont.close(r : self ref Reffont)
+{
+	return acme->close(r);
+}
--- /dev/null
+++ b/appl/acme/dat.m
@@ -1,0 +1,283 @@
+Dat : module {
+	PATH : con "/dis/acme/dat.dis";
+
+	init : fn(mods : ref Mods);
+
+	Mods : adt {
+		sys : Sys;
+		bufio : Bufio;
+		draw : Draw;
+		styx : Styx;
+		styxaux : Styxaux;
+		acme : Acme;
+		gui : Gui;
+		graph : Graph;
+		dat : Dat;
+		framem : Framem;
+		utils : Utils;
+		regx : Regx;
+		scroll : Scroll;
+		textm : Textm;
+		filem : Filem;
+		windowm : Windowm;
+		rowm : Rowm;
+		columnm : Columnm;
+		bufferm : Bufferm;
+		diskm : Diskm;
+		exec : Exec;
+		look : Look;
+		timerm : Timerm;
+		fsys : Fsys;
+		xfidm : Xfidm;
+		plumbmsg : Plumbmsg;
+		edit: Edit;
+		editlog: Editlog;
+		editcmd: Editcmd;
+	};
+
+	SZSHORT : con 2;
+	SZINT : con 4;
+
+	FALSE, TRUE, XXX : con iota;
+
+	EM_NORMAL, EM_RAW, EM_MASK : con iota;
+
+	Qdir,Qacme,Qcons,Qconsctl,Qdraw,Qeditout,Qindex,Qlabel,Qnew,QWaddr,QWbody,QWconsctl,QWctl,QWdata,QWeditout,QWevent,QWrdsel,QWwrsel,QWtag,QMAX : con iota;
+
+	Blockincr : con 256;
+	Maxblock : con 8*1024;
+	NRange : con 10;
+	Infinity : con 16r7fffffff; 	# huge value for regexp address
+
+	# fbufalloc() guarantees room off end of BUFSIZE
+	MAXRPC : con 8192+Styx->IOHDRSZ;
+	BUFSIZE : con MAXRPC;
+	EVENTSIZE : con 256;
+	PLUMBSIZE : con 1024;
+	Scrollwid : con 12;	# width of scroll bar
+	Scrollgap : con 4;	# gap right of scroll bar
+	Margin : con 4;		# margin around text
+	Border : con 2;		#  line between rows, cols, windows
+	Maxtab : con 4;		# size of a tab, in units of the '0' character
+	
+	Empty: con 0;
+	Null : con '-';
+	Delete : con 'd';
+	Insert : con 'i';
+	Replace: con 'r';
+	Filename : con 'f';
+
+	# editing
+	Inactive, Inserting, Collecting: con iota;
+
+	# alphabets
+	ALPHA_LATIN: con '\0';
+	ALPHA_GREEK: con '*';
+	ALPHA_CYRILLIC: con '@';
+
+	Kscrollup: con 16re050;
+	Kscrolldown: con 16re051;
+
+	Astring : adt {
+		s : string;
+	};
+
+	Lock : adt {
+		cnt : int;
+		chann : chan of int;
+
+		init : fn() : ref Lock;
+		lock : fn(l : self ref Lock);
+		unlock : fn(l : self ref Lock);
+		locked : fn(l : self ref Lock) : int;
+	};
+
+# 	Lockx : adt {
+#		sem : ref Lock->Semaphore;
+#
+#		init : fn() : ref Lockx;
+#		lock : fn(l : self ref Lockx);
+#		unlock : fn(l : self ref Lockx);
+#	};
+
+	Ref : adt {
+		l : ref Lock;
+		cnt : int;
+
+		init : fn() : ref Ref;
+		inc : fn(r : self ref Ref) : int;
+		dec : fn(r : self ref Ref) : int;
+		refx : fn(r : self ref Ref) : int;
+	};
+
+	Runestr : adt {
+		r: string;
+		nr: int;
+	};
+
+	Range : adt {
+		q0 : int;
+		q1 : int;
+	};
+
+	Block : adt {
+		addr : int;			# disk address in bytes
+		n : int;			# number of used runes in block
+		next : cyclic ref Block;	# pointer to next in free list
+	};
+
+	Timer : adt {
+		dt : int;
+		c : chan of int;
+		next : cyclic ref Timer;
+	};
+
+	Command : adt {
+		pid : int;
+		name : string;
+		text : string;
+		av : list of string;
+		iseditcmd: int;
+		md : ref Mntdir;
+		next : cyclic ref Command;
+	};
+
+	Dirtab : adt {
+		name : string;
+		qtype : int;
+		qid : int;
+		perm : int;
+	};
+
+	Mntdir : adt {
+		id : int;
+		refs : int;
+		dir : string;
+		ndir : int;
+		next : cyclic ref Mntdir;
+		nincl : int;
+		incl : array of string;
+	};
+
+	Fid : adt {
+		fid : int;
+		busy : int;
+		open : int;
+		qid : Sys->Qid;
+		w : cyclic ref Windowm->Window;
+		dir : array of Dirtab;
+		next : cyclic ref Fid;
+		mntdir : ref Mntdir;
+		nrpart : int;
+		rpart : array of byte;
+	};
+
+	Rangeset : type array of Range;
+
+	Expand : adt {
+		q0 : int;
+		q1 : int;
+		name : string;
+		bname : string;
+		jump : int;
+		at : ref Textm->Text;
+		ar : string;
+		a0 : int;
+		a1 : int;
+	};
+
+	Dirlist : adt {
+		r : string;
+		wid : int;
+	};
+
+	Reffont : adt {
+		r : ref Ref;
+		f : ref Draw->Font;
+
+		get : fn(p : int, q : int, r : int, b : string) : ref Reffont;
+		close : fn(r : self ref Reffont);
+	};
+
+	Cursor : adt {
+		hot : Draw->Point;
+		size : Draw->Point;
+		bits : array of byte;
+	};
+
+	Smsg0 : adt {
+		msize : int;
+		version : string;
+		iounit: int;
+		qid : Sys->Qid;
+		count : int;
+		data : array of byte;
+		stat : Sys->Dir;
+		qids: array of Sys->Qid;
+	};
+
+	# loadfile function ptr
+
+	BUFL, READL: con iota;
+
+	# allwindows pick type
+
+	Looper: adt{
+		cp: ref Edit->Cmd;
+		XY: int;
+		w: array of ref Windowm->Window;
+		nw: int;
+	};	# only one; X and Y can't nest
+
+	Tofile: adt {
+		f: ref Filem->File;
+		r: ref Edit->String;
+	};
+
+	Filecheck: adt{
+		f: ref Filem->File;
+		r: string;
+		nr: int;
+	};
+
+	Allwin: adt{
+		pick{
+			LP => lp: ref Looper;
+			FF => ff: ref Tofile;
+			FC => fc: ref Filecheck;
+		}
+	};
+
+	seq : int;
+	maxtab : int;
+	mouse : ref Draw->Pointer;
+	reffont : ref Reffont;
+	modbutton : ref Draw->Image;
+	colbutton : ref Draw->Image;
+	button : ref Draw->Image;
+	arrowcursor, boxcursor : ref Cursor;
+	row : ref Rowm->Row;
+	disk : ref Diskm->Disk;
+	seltext : ref Textm->Text;
+	argtext : ref Textm->Text;
+	mousetext : ref Textm->Text; 	# global because Text.close needs to clear it
+	typetext : ref Textm->Text;		# ditto
+	barttext : ref Textm->Text;		# shared between mousetask and keyboardtask
+	bartflag : int;
+	activewin : ref Windowm->Window;
+	activecol : ref Columnm->Column;
+	nullrect : Draw->Rect;
+	home : string;
+	plumbed : int;
+
+	ckeyboard : chan of int;
+	cmouse : chan of ref Draw->Pointer;
+	cwait : chan of string;
+	ccommand : chan of ref Command;
+	ckill : chan of string;
+	cxfidalloc : chan of ref Xfidm->Xfid;
+	cxfidfree : chan of ref Xfidm->Xfid;
+	cerr : chan of string;
+	cplumb : chan of ref Plumbmsg->Msg;
+	cedit: chan of int;
+};
--- /dev/null
+++ b/appl/acme/ecmd.b
@@ -1,0 +1,1350 @@
+implement Editcmd;
+
+include "common.m";
+
+sys: Sys;
+utils: Utils;
+edit: Edit;
+editlog: Editlog;
+windowm: Windowm;
+look: Look;
+columnm: Columnm;
+bufferm: Bufferm;
+exec: Exec;
+dat: Dat;
+textm: Textm;
+regx: Regx;
+filem: Filem;
+rowm: Rowm;
+
+Dir: import Sys;
+Allwin, Filecheck, Tofile, Looper, Astring: import Dat;
+aNo, aDot, aAll: import Edit;
+C_nl, C_a, C_b, C_c, C_d, C_B, C_D, C_e, C_f, C_g, C_i, C_k, C_m, C_n, C_p, C_s, C_u, C_w, C_x, C_X, C_pipe, C_eq: import Edit;
+TRUE, FALSE: import Dat;
+Inactive, Inserting, Collecting: import Dat;
+BUFSIZE, Runestr: import Dat;
+Addr, Address, String, Cmd: import Edit;
+Window: import windowm;
+File: import filem;
+NRange, Range, Rangeset: import Dat;
+Text: import textm;
+Column: import columnm;
+Buffer: import bufferm;
+
+sprint: import sys;
+elogterm, elogclose, eloginsert, elogdelete, elogreplace, elogapply: import editlog;
+cmdtab, allocstring, freestring, Straddc, curtext, editing, newaddr, cmdlookup, editerror: import edit;
+error, stralloc, strfree, warning, skipbl, findbl: import utils;
+lookfile, cleanname, dirname: import look;
+undo, run: import exec;
+Ref, Lock, row, cedit: import dat;
+rxcompile, rxexecute, rxbexecute: import regx;
+allwindows: import rowm;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	utils = mods.utils;
+	edit = mods.edit;
+	editlog = mods.editlog;
+	windowm = mods.windowm;
+	look = mods.look;
+	columnm = mods.columnm;
+	bufferm = mods.bufferm;
+	exec = mods.exec;
+	dat = mods.dat;
+	textm = mods.textm;
+	regx = mods.regx;
+	filem = mods.filem;
+	rowm = mods.rowm;
+
+	none.r.q0 = none.r.q1 = 0;
+	none.f = nil;
+}
+
+cmdtabexec(i: int, t: ref Text, cp: ref Cmd): int
+{
+	case (cmdtab[i].fnc){
+		C_nl	=> i = nl_cmd(t, cp);
+		C_a 	=> i = a_cmd(t, cp);
+		C_b	=> i = b_cmd(t, cp);
+		C_c	=> i = c_cmd(t, cp);
+		C_d	=> i = d_cmd(t, cp);
+		C_e	=> i = e_cmd(t, cp);
+		C_f	=> i = f_cmd(t, cp);
+		C_g	=> i = g_cmd(t, cp);
+		C_i	=> i = i_cmd(t, cp);
+		C_m	=> i = m_cmd(t, cp);
+		C_p	=> i = p_cmd(t, cp);
+		C_s	=> i = s_cmd(t, cp);
+		C_u	=> i = u_cmd(t, cp);
+		C_w	=> i = w_cmd(t, cp);
+		C_x	=> i = x_cmd(t, cp);
+		C_eq => i = eq_cmd(t, cp);
+		C_B	=> i = B_cmd(t, cp);
+		C_D	=> i = D_cmd(t, cp);
+		C_X	=> i = X_cmd(t, cp);
+		C_pipe	=> i = pipe_cmd(t, cp);
+		* =>	error("bad case in cmdtabexec");
+	}
+	return i;
+}
+
+Glooping: int;
+nest: int;
+Enoname := "no file name given";
+
+addr: Address;
+menu: ref File;
+sel: Rangeset;
+collection: string;
+ncollection: int;
+
+clearcollection()
+{
+	collection = nil;
+	ncollection = 0;
+}
+
+resetxec()
+{
+	Glooping = nest = 0;
+	clearcollection();
+}
+
+mkaddr(f: ref File): Address
+{
+	a: Address;
+
+	a.r.q0 = f.curtext.q0;
+	a.r.q1 = f.curtext.q1;
+	a.f = f;
+	return a;
+}
+
+none: Address;
+
+cmdexec(t: ref Text, cp: ref Cmd): int
+{
+	i: int;
+	ap: ref Addr;
+	f: ref File;
+	w: ref Window;
+	dot: Address;
+
+	if(t == nil)
+		w = nil;
+	else
+		w = t.w;
+	if(w==nil && (cp.addr==nil || cp.addr.typex!='"') &&
+	    utils->strchr("bBnqUXY!", cp.cmdc) < 0&&
+	    !(cp.cmdc=='D' && cp.text!=nil))
+		editerror("no current window");
+	i = cmdlookup(cp.cmdc);	# will be -1 for '{' 
+	f = nil;
+	if(t!=nil && t.w!=nil){
+		t = t.w.body;
+		f = t.file;
+		f.curtext = t;
+	}
+	if(i>=0 && cmdtab[i].defaddr != aNo){
+		if((ap=cp.addr)==nil && cp.cmdc!='\n'){
+			cp.addr = ap = newaddr();
+			ap.typex = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap.typex = '*';
+		}else if(ap!=nil && ap.typex=='"' && ap.next==nil && cp.cmdc!='\n'){
+			ap.next = newaddr();
+			ap.next.typex = '.';
+			if(cmdtab[i].defaddr == aAll)
+				ap.next.typex = '*';
+		}
+		if(cp.addr!=nil){	# may be false for '\n' (only)
+			if(f!=nil){
+				dot = mkaddr(f);
+				addr = cmdaddress(ap, dot, 0);
+			}else	# a "
+				addr = cmdaddress(ap, none, 0);
+			f = addr.f;
+			t = f.curtext;
+		}
+	}
+	case(cp.cmdc){
+	'{' =>
+		dot = mkaddr(f);
+		if(cp.addr != nil)
+			dot = cmdaddress(cp.addr, dot, 0);
+		for(cp = cp.cmd; cp!=nil; cp = cp.next){
+			t.q0 = dot.r.q0;
+			t.q1 = dot.r.q1;
+			cmdexec(t, cp);
+		}
+		break;
+	* =>
+		if(i < 0)
+			editerror(sprint("unknown command %c in cmdexec", cp.cmdc));
+		i = cmdtabexec(i, t, cp);
+		return i;
+	}
+	return 1;
+}
+
+edittext(f: ref File, q: int, r: string, nr: int): string
+{
+	case(editing){
+	Inactive =>
+		return "permission denied";
+	Inserting =>
+		eloginsert(f, q, r, nr);
+		return nil;
+	Collecting =>
+		collection += r[0: nr];
+		ncollection += nr;
+		return nil;
+	* =>
+		return "unknown state in edittext";
+	}
+}
+
+# string is known to be NUL-terminated
+filelist(t: ref Text, r: string, nr: int): string
+{
+	if(nr == 0)
+		return nil;
+	(r, nr) = skipbl(r, nr);
+	if(r[0] != '<')
+		return r;
+	# use < command to collect text 
+	clearcollection();
+	runpipe(t, '<', r[1:], nr-1, Collecting);
+	return collection;
+}
+
+a_cmd(t: ref Text, cp: ref Cmd): int
+{
+	return append(t.file, cp, addr.r.q1);
+}
+
+b_cmd(nil: ref Text, cp: ref Cmd): int
+{
+	f: ref File;
+
+	f = tofile(cp.text);
+	if(nest == 0)
+		pfilename(f);
+	curtext = f.curtext;
+	return TRUE;
+}
+
+B_cmd(t: ref Text, cp: ref Cmd): int
+{
+	listx, r, s: string;
+	nr: int;
+
+	listx = filelist(t, cp.text.r, cp.text.n);
+	if(listx == nil)
+		editerror(Enoname);
+	r = listx;
+	nr = len r;
+	(r, nr) = skipbl(r, nr);
+	if(nr == 0)
+		look->new(t, t, nil, 0, 0, r, 0);
+	else while(nr > 0){
+		(s, nr) = findbl(r, nr);
+		look->new(t, t, nil, 0, 0, r, len r);
+		if(nr > 0)
+			(r, nr) = skipbl(s[1:], nr-1);
+	}
+	clearcollection();
+	return TRUE;
+}
+
+c_cmd(t: ref Text, cp: ref Cmd): int
+{
+	elogreplace(t.file, addr.r.q0, addr.r.q1, cp.text.r, cp.text.n);
+	return TRUE;
+}
+
+d_cmd(t: ref Text, nil: ref Cmd): int
+{
+	if(addr.r.q1 > addr.r.q0)
+		elogdelete(t.file, addr.r.q0, addr.r.q1);
+	return TRUE;
+}
+
+D1(t: ref Text)
+{
+	if(t.w.body.file.ntext>1 || t.w.clean(FALSE, FALSE))
+		t.col.close(t.w, TRUE);
+}
+
+D_cmd(t: ref Text, cp: ref Cmd): int
+{
+	listx, r, s, n: string;
+	nr, nn: int;
+	w: ref Window;
+	dir, rs: Runestr;
+	buf: string;
+
+	listx = filelist(t, cp.text.r, cp.text.n);
+	if(listx == nil){
+		D1(t);
+		return TRUE;
+	}
+	dir = dirname(t, nil, 0);
+	r = listx;
+	nr = len r;
+	(r, nr) = skipbl(r, nr);
+	do{
+		(s, nr) = findbl(r, nr);
+		# first time through, could be empty string, meaning delete file empty name
+		nn = len r;
+		if(r[0]=='/' || nn==0 || dir.nr==0){
+			rs.r = r;
+			rs.nr = nn;
+		}else{
+			n = dir.r + "/" + r;
+			rs = cleanname(n, dir.nr+1+nn);
+		}
+		w = lookfile(rs.r, rs.nr);
+		if(w == nil){
+			buf = sprint("no such file %s", rs.r);
+			rs.r = nil;
+			editerror(buf);
+		}
+		rs.r = nil;
+		D1(w.body);
+		if(nr > 0)
+			(r, nr) = skipbl(s[1:], nr-1);
+	}while(nr > 0);
+	clearcollection();
+	dir.r = nil;
+	return TRUE;
+}
+
+readloader(f: ref File, q0: int, r: string, nr: int): int
+{
+	if(nr > 0)
+		eloginsert(f, q0, r, nr);
+	return 0;
+}
+
+e_cmd(t: ref Text , cp: ref Cmd): int
+{
+	name: string;
+	f: ref File;
+	i, q0, q1, nulls, samename, allreplaced, ok: int;
+	fd: ref Sys->FD;
+	s, tmp: string;
+	d: Dir;
+
+	f = t.file;
+	q0 = addr.r.q0;
+	q1 = addr.r.q1;
+	if(cp.cmdc == 'e'){
+		if(t.w.clean(TRUE, FALSE)==FALSE)
+			editerror("");	# winclean generated message already 
+		q0 = 0;
+		q1 = f.buf.nc;
+	}
+	allreplaced = (q0==0 && q1==f.buf.nc);
+	name = cmdname(f, cp.text, cp.cmdc=='e');
+	if(name == nil)
+		editerror(Enoname);
+	i = len name;
+	samename = name == t.file.name;
+	s = name;
+	name = nil;
+	fd = sys->open(s, Sys->OREAD);
+	if(fd == nil){
+		tmp = sprint("can't open %s: %r", s);
+		s = nil;
+		editerror(tmp);
+	}
+	(ok, d) = sys->fstat(fd);
+	if(ok >=0 && (d.mode&Sys->DMDIR)){
+		fd = nil;
+		tmp = sprint("%s is a directory", s);
+		s = nil;
+		editerror(tmp);
+	}
+	elogdelete(f, q0, q1);
+	nulls = 0;
+	bufferm->loadfile(fd, q1, Dat->READL, nil, f);
+	s = nil;
+	fd = nil;
+	if(nulls)
+		warning(nil, sprint("%s: NUL bytes elided\n", s));
+	else if(allreplaced && samename)
+		f.editclean = TRUE;
+	return TRUE;
+}
+
+f_cmd(t: ref Text, cp: ref Cmd): int
+{
+	name: string;
+
+	name = cmdname(t.file, cp.text, TRUE);
+	name = nil;
+	pfilename(t.file);
+	return TRUE;
+}
+
+g_cmd(t: ref Text, cp: ref Cmd): int
+{
+	ok: int;
+
+	if(t.file != addr.f){
+		warning(nil, "internal error: g_cmd f!=addr.f\n");
+		return FALSE;
+	}
+	if(rxcompile(cp.re.r) == FALSE)
+		editerror("bad regexp in g command");
+	(ok, sel) = rxexecute(t, nil, addr.r.q0, addr.r.q1);
+	if(ok ^ cp.cmdc=='v'){
+		t.q0 = addr.r.q0;
+		t.q1 = addr.r.q1;
+		return cmdexec(t, cp.cmd);
+	}
+	return TRUE;
+}
+
+i_cmd(t: ref Text, cp: ref Cmd): int
+{
+	return append(t.file, cp, addr.r.q0);
+}
+
+# int
+# k_cmd(File *f, Cmd *cp)
+# {
+# 	USED(cp);
+#	f->mark = addr.r;
+#	return TRUE;
+# }
+
+copy(f: ref File, addr2: Address)
+{
+	p: int;
+	ni: int;
+	buf: ref Astring;
+
+	buf = stralloc(BUFSIZE);
+	for(p=addr.r.q0; p<addr.r.q1; p+=ni){
+		ni = addr.r.q1-p;
+		if(ni > BUFSIZE)
+			ni = BUFSIZE;
+		f.buf.read(p, buf, 0, ni);
+		eloginsert(addr2.f, addr2.r.q1, buf.s, ni);
+	}
+	strfree(buf);
+}
+
+move(f: ref File, addr2: Address)
+{
+	if(addr.f!=addr2.f || addr.r.q1<=addr2.r.q0){
+		elogdelete(f, addr.r.q0, addr.r.q1);
+		copy(f, addr2);
+	}else if(addr.r.q0 >= addr2.r.q1){
+		copy(f, addr2);
+		elogdelete(f, addr.r.q0, addr.r.q1);
+	}else if(addr.r.q0==addr2.r.q0 && addr.r.q1==addr2.r.q1){
+		;	# move to self; no-op
+	}else
+		editerror("move overlaps itself");
+}
+
+m_cmd(t: ref Text, cp: ref Cmd): int
+{
+	dot, addr2: Address;
+
+	dot = mkaddr(t.file);
+	addr2 = cmdaddress(cp.mtaddr, dot, 0);
+	if(cp.cmdc == 'm')
+		move(t.file, addr2);
+	else
+		copy(t.file, addr2);
+	return TRUE;
+}
+
+# int
+# n_cmd(File *f, Cmd *cp)
+# {
+#	int i;
+#	USED(f);
+#	USED(cp);
+#	for(i = 0; i<file.nused; i++){
+#		if(file.filepptr[i] == cmd)
+#			continue;
+#		f = file.filepptr[i];
+#		Strduplstr(&genstr, &f->name);
+#		filename(f);
+#	}
+#	return TRUE;
+#}
+
+p_cmd(t: ref Text, nil: ref Cmd): int
+{
+	return pdisplay(t.file);
+}
+
+s_cmd(t: ref Text, cp: ref Cmd): int
+{
+	i, j, k, c, m, n, nrp, didsub, ok: int;
+	p1, op, delta: int;
+	buf: ref String;
+	rp: array of Rangeset;
+	err: string;
+	rbuf: ref Astring;
+
+	n = cp.num;
+	op= -1;
+	if(rxcompile(cp.re.r) == FALSE)
+		editerror("bad regexp in s command");
+	nrp = 0;
+	rp = nil;
+	delta = 0;
+	didsub = FALSE;
+	for(p1 = addr.r.q0; p1<=addr.r.q1; ){
+		(ok, sel) = rxexecute(t, nil, p1, addr.r.q1);
+		if(!ok)
+			break;
+		if(sel[0].q0 == sel[0].q1){	# empty match?
+			if(sel[0].q0 == op){
+				p1++;
+				continue;
+			}
+			p1 = sel[0].q1+1;
+		}else
+			p1 = sel[0].q1;
+		op = sel[0].q1;
+		if(--n>0)
+			continue;
+		nrp++;
+		orp := rp;
+		rp = array[nrp] of Rangeset;
+		rp[0: ] = orp[0:nrp-1];
+		rp[nrp-1] = copysel(sel);
+		orp = nil;
+	}
+	rbuf = stralloc(BUFSIZE);
+	buf = allocstring(0);
+	for(m=0; m<nrp; m++){
+		buf.n = 0;
+		buf.r = nil;
+		sel = rp[m];
+		for(i = 0; i<cp.text.n; i++)
+			if((c = cp.text.r[i])=='\\' && i<cp.text.n-1){
+				c = cp.text.r[++i];
+				if('1'<=c && c<='9') {
+					j = c-'0';
+					if(sel[j].q1-sel[j].q0>BUFSIZE){
+						err = "replacement string too long";
+						rp = nil;
+						freestring(buf);
+						strfree(rbuf);
+						editerror(err);
+						return FALSE;
+					}
+					t.file.buf.read(sel[j].q0, rbuf, 0, sel[j].q1-sel[j].q0);
+					for(k=0; k<sel[j].q1-sel[j].q0; k++)
+						Straddc(buf, rbuf.s[k]);
+				}else
+				 	Straddc(buf, c);
+			}else if(c!='&')
+				Straddc(buf, c);
+			else{
+				if(sel[0].q1-sel[0].q0>BUFSIZE){
+					err = "right hand side too long in substitution";
+					rp = nil;
+					freestring(buf);
+					strfree(rbuf);
+					editerror(err);
+					return FALSE;
+				}
+				t.file.buf.read(sel[0].q0, rbuf, 0, sel[0].q1-sel[0].q0);
+				for(k=0; k<sel[0].q1-sel[0].q0; k++)
+					Straddc(buf, rbuf.s[k]);
+			}
+		elogreplace(t.file, sel[0].q0, sel[0].q1, buf.r, buf.n);
+		delta -= sel[0].q1-sel[0].q0;
+		delta += buf.n;
+		didsub = 1;
+		if(!cp.flag)
+			break;
+	}
+	rp = nil;
+	freestring(buf);
+	strfree(rbuf);
+	if(!didsub && nest==0)
+		editerror("no substitution");
+	t.q0 = addr.r.q0;
+	t.q1 = addr.r.q1+delta;
+	return TRUE;
+}
+
+u_cmd(t: ref Text, cp: ref Cmd): int
+{
+	n, oseq, flag: int;
+
+	n = cp.num;
+	flag = TRUE;
+	if(n < 0){
+		n = -n;
+		flag = FALSE;
+	}
+	oseq = -1;
+	while(n-->0 && t.file.seq!=0 && t.file.seq!=oseq){
+		oseq = t.file.seq;
+warning(nil, sprint("seq %d\n", t.file.seq));
+		undo(t, flag);
+	}
+	return TRUE;
+}
+
+w_cmd(t: ref Text, cp: ref Cmd): int
+{
+	r: string;
+	f: ref File;
+
+	f = t.file;
+	if(f.seq == dat->seq)
+		editerror("can't write file with pending modifications");
+	r = cmdname(f, cp.text, FALSE);
+	if(r == nil)
+		editerror("no name specified for 'w' command");
+	exec->putfile(f, addr.r.q0, addr.r.q1, r);
+	# r is freed by putfile
+	return TRUE;
+}
+
+x_cmd(t: ref Text, cp: ref Cmd): int
+{
+	if(cp.re!=nil)
+		looper(t.file, cp, cp.cmdc=='x');
+	else
+		linelooper(t.file, cp);
+	return TRUE;
+}
+
+X_cmd(nil: ref Text, cp: ref Cmd): int
+{
+	filelooper(cp, cp.cmdc=='X');
+	return TRUE;
+}
+
+runpipe(t: ref Text, cmd: int, cr: string, ncr: int, state: int)
+{
+	r, s: string;
+	n: int;
+	dir: Runestr;
+	w: ref Window;
+
+	(r, n) = skipbl(cr, ncr);
+	if(n == 0)
+		editerror("no command specified for >");
+	w = nil;
+	if(state == Inserting){
+		w = t.w;
+		t.q0 = addr.r.q0;
+		t.q1 = addr.r.q1;
+		if(cmd == '<' || cmd=='|')
+			elogdelete(t.file, t.q0, t.q1);
+	}
+	tmps := "z";
+	tmps[0] = cmd;
+	s = tmps + r;
+	n++;
+	dir.r = nil;
+	dir.nr = 0;
+	if(t != nil)
+		dir = dirname(t, nil, 0);
+	if(dir.nr==1 && dir.r[0]=='.'){	# sigh 
+		dir.r = nil;
+		dir.nr = 0;
+	}
+	editing = state;
+	if(t!=nil && t.w!=nil)
+		t.w.refx.inc();	# run will decref
+	spawn run(w, s, dir.r, dir.nr, TRUE, nil, nil, TRUE);
+	s = nil;
+	if(t!=nil && t.w!=nil)
+		t.w.unlock();
+	row.qlock.unlock();
+	<- cedit;
+	row.qlock.lock();
+	editing = Inactive;
+	if(t!=nil && t.w!=nil)
+		t.w.lock('M');
+}
+
+pipe_cmd(t: ref Text, cp: ref Cmd): int
+{
+	runpipe(t, cp.cmdc, cp.text.r, cp.text.n, Inserting);
+	return TRUE;
+}
+
+nlcount(t: ref Text, q0: int, q1: int): int
+{
+	nl: int;
+	buf: ref Astring;
+	i, nbuf: int;
+
+	buf = stralloc(BUFSIZE);
+	nbuf = 0;
+	i = nl = 0;
+	while(q0 < q1){
+		if(i == nbuf){
+			nbuf = q1-q0;
+			if(nbuf > BUFSIZE)
+				nbuf = BUFSIZE;
+			t.file.buf.read(q0, buf, 0, nbuf);
+			i = 0;
+		}
+		if(buf.s[i++] == '\n')
+			nl++;
+		q0++;
+	}
+	strfree(buf);
+	return nl;
+}
+
+printposn(t: ref Text, charsonly: int)
+{
+	l1, l2: int;
+
+	if(t != nil && t.file != nil && t.file.name != nil)
+		warning(nil, t.file.name + ":");
+	if(!charsonly){
+		l1 = 1+nlcount(t, 0, addr.r.q0);
+		l2 = l1+nlcount(t, addr.r.q0, addr.r.q1);
+		# check if addr ends with '\n' 
+		if(addr.r.q1>0 && addr.r.q1>addr.r.q0 && t.readc(addr.r.q1-1)=='\n')
+			--l2;
+		warning(nil, sprint("%ud", l1));
+		if(l2 != l1)
+			warning(nil, sprint(",%ud", l2));
+		warning(nil, "\n");
+		# warning(nil, "; ");
+		return;
+	}
+	warning(nil, sprint("#%d", addr.r.q0));
+	if(addr.r.q1 != addr.r.q0)
+		warning(nil, sprint(",#%d", addr.r.q1));
+	warning(nil, "\n");
+}
+
+eq_cmd(t: ref Text, cp: ref Cmd): int
+{
+	charsonly: int;
+
+	case(cp.text.n){
+	0 =>
+		charsonly = FALSE;
+		break;
+	1 =>
+		if(cp.text.r[0] == '#'){
+			charsonly = TRUE;
+			break;
+		}
+	* =>
+		charsonly = TRUE;
+		editerror("newline expected");
+	}
+	printposn(t, charsonly);
+	return TRUE;
+}
+
+nl_cmd(t: ref Text, cp: ref Cmd): int
+{
+	a: Address;
+	f: ref File;
+
+	f = t.file;
+	if(cp.addr == nil){
+		# First put it on newline boundaries
+		a = mkaddr(f);
+		addr = lineaddr(0, a, -1);
+		a = lineaddr(0, a, 1);
+		addr.r.q1 = a.r.q1;
+		if(addr.r.q0==t.q0 && addr.r.q1==t.q1){
+			a = mkaddr(f);
+			addr = lineaddr(1, a, 1);
+		}
+	}
+	t.show(addr.r.q0, addr.r.q1, TRUE);
+	return TRUE;
+}
+
+append(f: ref File, cp: ref Cmd, p: int): int
+{
+	if(cp.text.n > 0)
+		eloginsert(f, p, cp.text.r, cp.text.n);
+	return TRUE;
+}
+
+pdisplay(f: ref File): int
+{
+	p1, p2: int;
+	np: int;
+	buf: ref Astring;
+
+	p1 = addr.r.q0;
+	p2 = addr.r.q1;
+	if(p2 > f.buf.nc)
+		p2 = f.buf.nc;
+	buf = stralloc(BUFSIZE);
+	while(p1 < p2){
+		np = p2-p1;
+		if(np>BUFSIZE-1)
+			np = BUFSIZE-1;
+		f.buf.read(p1, buf, 0, np);
+		warning(nil, sprint("%s", buf.s[0:np]));
+		p1 += np;
+	}
+	strfree(buf);
+	f.curtext.q0 = addr.r.q0;
+	f.curtext.q1 = addr.r.q1;
+	return TRUE;
+}
+
+pfilename(f: ref File)
+{
+	dirty: int;
+	w: ref Window;
+
+	w = f.curtext.w;
+	# same check for dirty as in settag, but we know ncache==0
+	dirty = !w.isdir && !w.isscratch && f.mod;
+	warning(nil, sprint("%c%c%c %s\n", " '"[dirty],
+		'+', " ."[curtext!=nil && curtext.file==f], f.name));
+}
+
+loopcmd(f: ref File, cp: ref Cmd, rp: array of Range, nrp: int)
+{
+	i: int;
+
+	for(i=0; i<nrp; i++){
+		f.curtext.q0 = rp[i].q0;
+		f.curtext.q1 = rp[i].q1;
+		cmdexec(f.curtext, cp);
+	}
+}
+
+looper(f: ref File, cp: ref Cmd, xy: int)
+{
+	p, op, nrp, ok: int;
+	r, tr: Range;
+	rp: array of  Range;
+
+	r = addr.r;
+	if(xy)
+		op = -1;
+	else
+		op = r.q0;
+	nest++;
+	if(rxcompile(cp.re.r) == FALSE)
+		editerror(sprint("bad regexp in %c command", cp.cmdc));
+	nrp = 0;
+	rp = nil;
+	for(p = r.q0; p<=r.q1; ){
+		(ok, sel) = rxexecute(f.curtext, nil, p, r.q1);
+		if(!ok){ # no match, but y should still run
+			if(xy || op>r.q1)
+				break;
+			tr.q0 = op;
+			tr.q1 = r.q1;
+			p = r.q1+1;	# exit next loop
+		}else{
+			if(sel[0].q0==sel[0].q1){	# empty match?
+				if(sel[0].q0==op){
+					p++;
+					continue;
+				}
+				p = sel[0].q1+1;
+			}else
+				p = sel[0].q1;
+			if(xy)
+				tr = sel[0];
+			else{
+				tr.q0 = op;
+				tr.q1 = sel[0].q0;
+			}
+		}
+		op = sel[0].q1;
+		nrp++;
+		orp := rp;
+		rp = array[nrp] of Range;
+		rp[0: ] = orp[0: nrp-1];
+		rp[nrp-1] = tr;
+		orp = nil;
+	}
+	loopcmd(f, cp.cmd, rp, nrp);
+	rp = nil;
+	--nest;
+}
+
+linelooper(f: ref File, cp: ref Cmd)
+{
+	nrp, p: int;
+	r, linesel: Range;
+	a, a3: Address;
+	rp: array of Range;
+
+	nest++;
+	nrp = 0;
+	rp = nil;
+	r = addr.r;
+	a3.f = f;
+	a3.r.q0 = a3.r.q1 = r.q0;
+	a = lineaddr(0, a3, 1);
+	linesel = a.r;
+	for(p = r.q0; p<r.q1; p = a3.r.q1){
+		a3.r.q0 = a3.r.q1;
+		if(p!=r.q0 || linesel.q1==p){
+			a = lineaddr(1, a3, 1);
+			linesel = a.r;
+		}
+		if(linesel.q0 >= r.q1)
+			break;
+		if(linesel.q1 >= r.q1)
+			linesel.q1 = r.q1;
+		if(linesel.q1 > linesel.q0)
+			if(linesel.q0>=a3.r.q1 && linesel.q1>a3.r.q1){
+				a3.r = linesel;
+				nrp++;
+				orp := rp;
+				rp = array[nrp] of Range;
+				rp[0: ] = orp[0: nrp-1];
+				rp[nrp-1] = linesel;
+				orp = nil;
+				continue;
+			}
+		break;
+	}
+	loopcmd(f, cp.cmd, rp, nrp);
+	rp = nil;
+	--nest;
+}
+
+loopstruct: ref Looper;
+
+alllooper(w: ref Window, lp: ref Looper)
+{
+	t: ref Text;
+	cp: ref Cmd;
+
+	cp = lp.cp;
+#	if(w.isscratch || w.isdir)
+#		return;
+	t = w.body;
+	# only use this window if it's the current window for the file
+	if(t.file.curtext != t)
+		return;
+#	if(w.nopen[QWevent] > 0)
+#		return;
+	# no auto-execute on files without names
+	if(cp.re==nil && t.file.name==nil)
+		return;
+	if(cp.re==nil || filematch(t.file, cp.re)==lp.XY){
+		olpw := lp.w;
+		lp.w = array[lp.nw+1] of ref Window;
+		lp.w[0: ] = olpw[0: lp.nw];
+		lp.w[lp.nw++] = w;
+		olpw = nil;
+	}
+}
+
+filelooper(cp: ref Cmd, XY: int)
+{
+	i: int;
+
+	if(Glooping++)
+		editerror(sprint("can't nest %c command", "YX"[XY]));
+	nest++;
+
+	if(loopstruct == nil)
+		loopstruct = ref Looper;
+	loopstruct.cp = cp;
+	loopstruct.XY = XY;
+	if(loopstruct.w != nil)	# error'ed out last time
+		loopstruct.w = nil;
+	loopstruct.w = nil;
+	loopstruct.nw = 0;
+	aw := ref Allwin.LP(loopstruct);
+	allwindows(Edit->ALLLOOPER, aw);
+	aw = nil;
+	for(i=0; i<loopstruct.nw; i++)
+		cmdexec(loopstruct.w[i].body, cp.cmd);
+	loopstruct.w = nil;
+
+	--Glooping;
+	--nest;
+}
+
+nextmatch(f: ref File, r: ref String, p: int, sign: int)
+{
+	ok: int;
+
+	if(rxcompile(r.r) == FALSE)
+		editerror("bad regexp in command address");
+	if(sign >= 0){
+		(ok, sel) = rxexecute(f.curtext, nil, p, 16r7FFFFFFF);
+		if(!ok)
+			editerror("no match for regexp");
+		if(sel[0].q0==sel[0].q1 && sel[0].q0==p){
+			if(++p>f.buf.nc)
+				p = 0;
+			(ok, sel) = rxexecute(f.curtext, nil, p, 16r7FFFFFFF);
+			if(!ok)
+				editerror("address");
+		}
+	}else{
+		(ok, sel) = rxbexecute(f.curtext, p);
+		if(!ok)
+			editerror("no match for regexp");
+		if(sel[0].q0==sel[0].q1 && sel[0].q1==p){
+			if(--p<0)
+				p = f.buf.nc;
+			(ok, sel) = rxbexecute(f.curtext, p);
+			if(!ok)
+				editerror("address");
+		}
+	}
+}
+
+cmdaddress(ap: ref Addr, a: Address, sign: int): Address
+{
+	f := a.f;
+	a1, a2: Address;
+
+	do{
+		case(ap.typex){
+		'l' or
+		'#' =>
+			if(ap.typex == '#')
+				a = charaddr(ap.num, a, sign);
+			else
+				a = lineaddr(ap.num, a, sign);
+			break;
+
+		'.' =>
+			a = mkaddr(f);
+			break;
+
+		'$' =>
+			a.r.q0 = a.r.q1 = f.buf.nc;
+			break;
+
+		'\'' =>
+editerror("can't handle '");
+#			a.r = f.mark;
+			break;
+
+		'?' =>
+			sign = -sign;
+			if(sign == 0)
+				sign = -1;
+			if(sign >= 0)
+				v := a.r.q1;
+			else
+				v = a.r.q0;
+			nextmatch(f, ap.re, v, sign);
+			a.r = sel[0];
+			break;
+
+		'/' =>
+			if(sign >= 0)
+				v := a.r.q1;
+			else
+				v = a.r.q0;
+			nextmatch(f, ap.re, v, sign);
+			a.r = sel[0];
+			break;
+
+		'"' =>
+			f = matchfile(ap.re);
+			a = mkaddr(f);
+			break;
+
+		'*' =>
+			a.r.q0 = 0;
+			a.r.q1 = f.buf.nc;
+			return a;
+
+		',' or
+		';' =>
+			if(ap.left!=nil)
+				a1 = cmdaddress(ap.left, a, 0);
+			else{
+				a1.f = a.f;
+				a1.r.q0 = a1.r.q1 = 0;
+			}
+			if(ap.typex == ';'){
+				f = a1.f;
+				a = a1;
+				f.curtext.q0 = a1.r.q0;
+				f.curtext.q1 = a1.r.q1;
+			}
+			if(ap.next!=nil)
+				a2 = cmdaddress(ap.next, a, 0);
+			else{
+				a2.f = a.f;
+				a2.r.q0 = a2.r.q1 = f.buf.nc;
+			}
+			if(a1.f != a2.f)
+				editerror("addresses in different files");
+			a.f = a1.f;
+			a.r.q0 = a1.r.q0;
+			a.r.q1 = a2.r.q1;
+			if(a.r.q1 < a.r.q0)
+				editerror("addresses out of order");
+			return a;
+
+		'+' or
+		'-' =>
+			sign = 1;
+			if(ap.typex == '-')
+				sign = -1;
+			if(ap.next==nil || ap.next.typex=='+' || ap.next.typex=='-')
+				a = lineaddr(1, a, sign);
+			break;
+		* =>
+			error("cmdaddress");
+			return a;
+		}
+	}while((ap = ap.next)!=nil);	# assign =
+	return a;
+}
+
+alltofile(w: ref Window, tp: ref Tofile)
+{
+	t: ref Text;
+
+	if(tp.f != nil)
+		return;
+	if(w.isscratch || w.isdir)
+		return;
+	t = w.body;
+	# only use this window if it's the current window for the file
+	if(t.file.curtext != t)
+		return;
+#	if(w.nopen[QWevent] > 0)
+#		return;
+	if(tp.r.r == t.file.name)
+		tp.f = t.file;
+}
+
+tofile(r: ref String): ref File
+{
+	t: ref Tofile;
+	rr: String;
+
+	(rr.r, r.n) = skipbl(r.r, r.n);
+	t = ref Tofile;
+	t.f = nil;
+	t.r = ref String;
+	*t.r = rr;
+	aw := ref Allwin.FF(t);
+	allwindows(Edit->ALLTOFILE, aw);
+	aw = nil;
+	if(t.f == nil)
+		editerror(sprint("no such file\"%s\"", rr.r));
+	return t.f;
+}
+
+allmatchfile(w: ref Window, tp: ref Tofile)
+{
+	t: ref Text;
+
+	if(w.isscratch || w.isdir)
+		return;
+	t = w.body;
+	# only use this window if it's the current window for the file
+	if(t.file.curtext != t)
+		return;
+#	if(w.nopen[QWevent] > 0)
+#		return;
+	if(filematch(w.body.file, tp.r)){
+		if(tp.f != nil)
+			editerror(sprint("too many files match \"%s\"", tp.r.r));
+		tp.f = w.body.file;
+	}
+}
+
+matchfile(r: ref String): ref File
+{
+	tf: ref Tofile;
+
+	tf = ref Tofile;
+	tf.f = nil;
+	tf.r = r;
+	aw := ref Allwin.FF(tf);
+	allwindows(Edit->ALLMATCHFILE, aw);
+	aw = nil;
+
+	if(tf.f == nil)
+		editerror(sprint("no file matches \"%s\"", r.r));
+	return tf.f;
+}
+
+filematch(f: ref File, r: ref String): int
+{
+	buf: string;
+	w: ref Window;
+	match, i, dirty: int;
+	s: Rangeset;
+
+	# compile expr first so if we get an error, we haven't allocated anything
+	if(rxcompile(r.r) == FALSE)
+		editerror("bad regexp in file match");
+	w = f.curtext.w;
+	# same check for dirty as in settag, but we know ncache==0
+	dirty = !w.isdir && !w.isscratch && f.mod;
+	buf = sprint("%c%c%c %s\n", " '"[dirty],
+		'+', " ."[curtext!=nil && curtext.file==f], f.name);
+	(match, s) = rxexecute(nil, buf, 0, i);
+	buf = nil;
+	return match;
+}
+
+charaddr(l: int, addr: Address, sign: int): Address
+{
+	if(sign == 0)
+		addr.r.q0 = addr.r.q1 = l;
+	else if(sign < 0)
+		addr.r.q1 = addr.r.q0 -= l;
+	else if(sign > 0)
+		addr.r.q0 = addr.r.q1 += l;
+	if(addr.r.q0<0 || addr.r.q1>addr.f.buf.nc)
+		editerror("address out of range");
+	return addr;
+}
+
+lineaddr(l: int, addr: Address, sign: int): Address
+{
+	n: int;
+	c: int;
+	f := addr.f;
+	a: Address;
+	p: int;
+
+	a.f = f;
+	if(sign >= 0){
+		if(l == 0){
+			if(sign==0 || addr.r.q1==0){
+				a.r.q0 = a.r.q1 = 0;
+				return a;
+			}
+			a.r.q0 = addr.r.q1;
+			p = addr.r.q1-1;
+		}else{
+			if(sign==0 || addr.r.q1==0){
+				p = 0;
+				n = 1;
+			}else{
+				p = addr.r.q1-1;
+				n = f.curtext.readc(p++)=='\n';
+			}
+			while(n < l){
+				if(p >= f.buf.nc)
+					editerror("address out of range");
+				if(f.curtext.readc(p++) == '\n')
+					n++;
+			}
+			a.r.q0 = p;
+		}
+		while(p < f.buf.nc && f.curtext.readc(p++)!='\n')
+			;
+		a.r.q1 = p;
+	}else{
+		p = addr.r.q0;
+		if(l == 0)
+			a.r.q1 = addr.r.q0;
+		else{
+			for(n = 0; n<l; ){	# always runs once
+				if(p == 0){
+					if(++n != l)
+						editerror("address out of range");
+				}else{
+					c = f.curtext.readc(p-1);
+					if(c != '\n' || ++n != l)
+						p--;
+				}
+			}
+			a.r.q1 = p;
+			if(p > 0)
+				p--;
+		}
+		while(p > 0 && f.curtext.readc(p-1)!='\n')	# lines start after a newline
+			p--;
+		a.r.q0 = p;
+	}
+	return a;
+}
+
+allfilecheck(w: ref Window, fp: ref Filecheck)
+{
+	f: ref File;
+
+	f = w.body.file;
+	if(w.body.file == fp.f)
+		return;
+	if(fp.r == f.name)
+		warning(nil, sprint("warning: duplicate file name \"%s\"\n", fp.r));
+}
+
+cmdname(f: ref File, str: ref String , set: int): string
+{
+	r, s: string;
+	n: int;
+	fc: ref Filecheck;
+	newname: Runestr;
+
+	r = nil;
+	n = str.n;
+	s = str.r;
+	if(n == 0){
+		# no name; use existing
+		if(f.name == nil)
+			return nil;
+		return f.name;
+	}
+	(s, n) = skipbl(s, n);
+	if(n == 0)
+		;
+	else{
+		if(s[0] == '/'){
+			r = s;
+		}else{
+			newname = dirname(f.curtext, s, n);
+			r = newname.r;
+			n = newname.nr;
+		}
+		fc = ref Filecheck;
+		fc.f = f;
+		fc.r = r;
+		fc.nr = n;
+		aw := ref Allwin.FC(fc);
+		allwindows(Edit->ALLFILECHECK, aw);
+		aw = nil;
+		if(f.name == nil)
+			set = TRUE;
+	}
+
+	if(set && r[0: n] != f.name){
+		f.mark();
+		f.mod = TRUE;
+		f.curtext.w.dirty = TRUE;
+		f.curtext.w.setname(r, n);
+	}
+	return r;
+}
+
+copysel(rs: Rangeset): Rangeset
+{
+	nrs := array[NRange] of Range;
+	for(i := 0; i < NRange; i++)
+		nrs[i] = rs[i];
+	return nrs;
+}
--- /dev/null
+++ b/appl/acme/ecmd.m
@@ -1,0 +1,18 @@
+Editcmd: module {
+
+	PATH: con "/dis/acme/ecmd.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	cmdexec: fn(a0: ref Textm->Text, a1: ref Edit->Cmd): int;
+	resetxec: fn();
+	cmdaddress: fn(a0: ref Edit->Addr, a1: Edit->Address, a2: int): Edit->Address;
+	edittext: fn(f: ref Filem->File, q: int, r: string, nr: int): string;
+
+	alllooper: fn(w: ref Windowm->Window, lp: ref Dat->Looper);
+	alltofile: fn(w: ref Windowm->Window, tp: ref Dat->Tofile);
+	allmatchfile: fn(w: ref Windowm->Window, tp: ref Dat->Tofile);
+	allfilecheck: fn(w: ref Windowm->Window, fp: ref Dat->Filecheck);
+
+	readloader: fn(f: ref Filem->File, q0: int, r: string, nr: int): int;
+};
--- /dev/null
+++ b/appl/acme/edit.b
@@ -1,0 +1,676 @@
+implement Edit;
+
+include "common.m";
+
+sys: Sys;
+dat: Dat;
+utils: Utils;
+textm: Textm;
+windowm: Windowm;
+rowm: Rowm;
+scroll: Scroll;
+editlog: Editlog;
+editcomd: Editcmd;
+
+sprint, print: import sys;
+FALSE, TRUE, BUFSIZE, Null, Empty, Inactive: import Dat;
+warning, error, strchr: import utils;
+Text: import textm;
+File: import Filem;
+Window: import windowm;
+allwindows: import rowm;
+scrdraw: import scroll;
+elogterm, elogapply: import editlog;
+cmdexec, resetxec: import editcomd;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	textm = mods.textm;
+	windowm = mods.windowm;
+	rowm = mods.rowm;
+	scroll = mods.scroll;
+	editlog = mods.editlog;
+	editcomd = mods.editcmd;
+	editing = Inactive;
+}
+
+linex: con "\n";
+wordx: con "\t\n";
+
+cmdtab = array[28] of {
+#		cmdc	text	regexp	addr	defcmd	defaddr	count	token	 fn
+	Cmdt ( '\n',	0,	0,	0,	0,	aDot,	0,	nil,		C_nl ),
+	Cmdt ( 'a',		1,	0,	0,	0,	aDot,	0,	nil,		C_a ),
+	Cmdt ( 'b',		0,	0,	0,	0,	aNo,		0,	linex,	C_b ),
+	Cmdt ( 'c',		1,	0,	0,	0,	aDot,	0,	nil,		C_c ),
+	Cmdt ( 'd',		0,	0,	0,	0,	aDot,	0,	nil,		C_d ),
+	Cmdt ( 'e',		0,	0,	0,	0,	aNo,		0,	wordx,	C_e ),
+	Cmdt ( 'f',		0,	0,	0,	0,	aNo,		0,	wordx,	C_f ),
+	Cmdt ( 'g',		0,	1,	0,	'p',	aDot,	0,	nil,		C_g ),
+	Cmdt ( 'i',		1,	0,	0,	0,	aDot,	0,	nil,		C_i ),
+	Cmdt ( 'm',	0,	0,	1,	0,	aDot,	0,	nil,		C_m ),
+	Cmdt ( 'p',		0,	0,	0,	0,	aDot,	0,	nil,		C_p ),
+	Cmdt ( 'r',		0,	0,	0,	0,	aDot,	0,	wordx,	C_e ),
+	Cmdt ( 's',		0,	1,	0,	0,	aDot,	1,	nil,		C_s ),
+	Cmdt ( 't',		0,	0,	1,	0,	aDot,	0,	nil,		C_m ),
+	Cmdt ( 'u',		0,	0,	0,	0,	aNo,		2,	nil,		C_u ),
+	Cmdt ( 'v',		0,	1,	0,	'p',	aDot,	0,	nil,		C_g ),
+	Cmdt ( 'w',	0,	0,	0,	0,	aAll,		0,	wordx,	C_w ),
+	Cmdt ( 'x',		0,	1,	0,	'p',	aDot,	0,	nil,		C_x ),
+	Cmdt ( 'y',		0,	1,	0,	'p',	aDot,	0,	nil,		C_x ),
+	Cmdt ( '=',		0,	0,	0,	0,	aDot,	0,	linex,	C_eq ),
+	Cmdt ( 'B',		0,	0,	0,	0,	aNo,		0,	linex,	C_B ),
+	Cmdt ( 'D',	0,	0,	0,	0,	aNo,		0,	linex,	C_D ),
+	Cmdt ( 'X',		0,	1,	0,	'f',	aNo,		0,	nil,		C_X ),
+	Cmdt ( 'Y',		0,	1,	0,	'f',	aNo,		0,	nil,		C_X ),
+	Cmdt ( '<',		0,	0,	0,	0,	aDot,	0,	linex,	C_pipe ),
+	Cmdt ( '|',		0,	0,	0,	0,	aDot,	0,	linex,	C_pipe ),
+	Cmdt ( '>',		0,	0,	0,	0,	aDot,	0,	linex,	C_pipe ),
+	# deliberately unimplemented
+	# Cmdt ( 'k',	0,	0,	0,	0,	aDot,	0,	nil,		C_k ),
+	# Cmdt ( 'n',	0,	0,	0,	0,	aNo,		0,	nil,		C_n ),
+	# Cmdt ( 'q',	0,	0,	0,	0,	aNo,		0,	nil,		C_q ),
+	# Cmdt ( '!',	0,	0,	0,	0,	aNo,		0,	linex,	C_plan9 ),
+	Cmdt (0,		0,	0,	0,	0,	0,		0,	nil,		-1 )
+};
+
+cmdstartp: string;
+cmdendp: int;
+cmdp: int;
+editerrc: chan of string;
+
+lastpat : ref String;
+patset: int;
+
+# cmdlist: ref List;
+# addrlist: ref List;
+# stringlist: ref List;
+
+editwaitproc(pid : int, sync: chan of int)
+{
+	fd : ref Sys->FD;
+	n : int;
+
+	sys->pctl(Sys->FORKFD, nil);
+	w := sprint("#p/%d/wait", pid);
+	fd = sys->open(w, Sys->OREAD);
+	if (fd == nil)
+		error("fd == nil in editwaitproc");
+	sync <-= sys->pctl(0, nil);
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;){
+		if ((n = sys->read(fd, buf, len buf))<0)
+			error("bad read in editwaitproc");
+		status = string buf[0:n];
+		dat->cwait <-= status;
+	}
+}
+
+editthread()
+{
+	cmdp: ref Cmd;
+
+	mypid := sys->pctl(0, nil);
+	sync := chan of int;
+	spawn editwaitproc(mypid, sync);
+	yourpid := <- sync;
+	while((cmdp=parsecmd(0)) != nil){
+#		ocurfile = curfile;
+#		loaded = curfile && !curfile->unread;
+		if(cmdexec(curtext, cmdp) == 0)
+			break;
+		freecmd();
+	}
+	editerrc <-= nil;
+	utils->postnote(Utils->PNPROC, mypid, yourpid, "kill");
+}
+
+allelogterm(w: ref Window)
+{
+	elogterm(w.body.file);
+}
+
+alleditinit(w: ref Window)
+{
+	w.tag.commit(TRUE);
+	w.body.commit(TRUE);
+	w.body.file.editclean = FALSE;
+}
+
+allupdate(w: ref Window)
+{
+	t: ref Text;
+	i: int;
+	f: ref File;
+
+	t = w.body;
+	f = t.file;
+	if(f.curtext != t)		# do curtext only
+		return;
+	if(f.elog.typex == Null)
+		elogterm(f);
+	else if(f.elog.typex != Empty){
+		elogapply(f);
+		if(f.editclean){
+			f.mod = FALSE;
+			for(i=0; i<f.ntext; i++)
+				f.text[i].w.dirty = FALSE;
+		}
+		t.setselect(t.q0, t.q1);
+		scrdraw(t);
+		w.settag();
+	}
+}
+
+editerror(s: string)
+{
+	# print("%s", s);
+	freecmd();
+	allwindows(ALLELOGTERM, nil);	# truncate the edit logs
+	editerrc <-= s;
+	exit;
+}
+
+editcmd(ct: ref Text, r: string, n: int)
+{
+	err: string;
+
+	if(n == 0)
+		return;
+	if(2*n > BUFSIZE){
+		warning(nil, "string too long\n");
+		return;
+	}
+
+	allwindows(ALLEDITINIT, nil);
+	cmdstartp = r[0:n];
+	if(r[n-1] != '\n')
+		cmdstartp[n++] = '\n';
+	cmdendp = n;
+	cmdp = 0;
+	if(ct.w == nil)
+		curtext = nil;
+	else
+		curtext = ct.w.body;
+	resetxec();
+	if(editerrc == nil){
+		editerrc = chan of string;
+		lastpat = allocstring(0);
+	}
+	spawn editthread();
+	err = <- editerrc;
+	editing = Inactive;
+	if(err != nil)
+		warning(nil, sprint("Edit: %s\n", err));
+
+	# update everyone whose edit log has data
+	allwindows(ALLUPDATE, nil);
+}
+
+getch(): int
+{
+	if(cmdp == cmdendp)
+		return -1;
+	return cmdstartp[cmdp++];
+}
+
+nextc(): int
+{
+	if(cmdp == cmdendp)
+		return -1;
+	return cmdstartp[cmdp];
+}
+
+ungetch()
+{
+	if(--cmdp < 0)
+		error("ungetch");
+}
+
+getnum(signok: int): int
+{
+	n: int;
+	c, sign: int;
+
+	n = 0;
+	sign = 1;
+	if(signok>1 && nextc()=='-'){
+		sign = -1;
+		getch();
+	}
+	if((c=nextc())<'0' || '9'<c)	# no number defaults to 1
+		return sign;
+	while('0'<=(c=getch()) && c<='9')
+		n = n*10 + (c-'0');
+	ungetch();
+	return sign*n;
+}
+
+cmdskipbl(): int
+{
+	c: int;
+	do
+		c = getch();
+	while(c==' ' || c=='\t');
+	if(c >= 0)
+		ungetch();
+	return c;
+}
+
+# Check that list has room for one more element.
+# growlist(l: ref List)
+# {
+#	if(l.elems == nil || l.nalloc==0){
+#		l.nalloc = INCR;
+#		l.elems = array[INCR] of Listelement;
+#		l.nused = 0;
+#	}else if(l.nused == l.nalloc){
+#		old := l.elems;
+#		l.elems = array[l.nalloc+INCR] of Listelement;
+#		l.elems[0:] = old[0:l.nalloc];
+#		l.nalloc += INCR;
+#	}
+# }
+
+# Remove the ith element from the list
+# dellist(l: ref List, i: int)
+# {
+#	l.elems[i:] = l.elems[i+1:l.nused];
+#	l.nused--;
+# }
+
+# Add a new element, whose position is i, to the list
+# inslist(l: ref List, i: int, val: int)
+# {
+#	growlist(l);
+#	l.elems[i+1:] = l.elems[i:l.nused];
+#	l.elems[i] = val;
+#	l.nused++;
+# }
+
+# listfree(l: ref List)
+# {
+#	l.elems = nil;
+# }
+
+allocstring(n: int): ref String
+{
+	s: ref String;
+
+	s = ref String;
+	s.n = n;
+	s.r = string array[s.n] of { * => byte '\0' };
+	return s;
+}
+
+freestring(s: ref String)
+{
+	s.r = nil;
+}
+
+newcmd(): ref Cmd
+{
+	p: ref Cmd;
+
+	p = ref Cmd;
+	# inslist(cmdlist, cmdlist.nused, p);
+	return p;
+}
+
+newstring(n: int): ref String
+{
+	p: ref String;
+
+	p = allocstring(n);
+	# inslist(stringlist, stringlist.nused, p);
+	return p;
+}
+
+newaddr(): ref Addr
+{
+	p: ref Addr;
+
+	p = ref Addr;
+	# inslist(addrlist, addrlist.nused, p);
+	return p;
+}
+
+freecmd()
+{
+	# i: int;
+
+	# cmdlist.elems = nil;
+	# addrlist.elems = nil;
+	# stringlist.elems = nil;
+	# cmdlist.nused = addrlist.nused = stringlist.nused = 0;
+}
+
+okdelim(c: int)
+{
+	if(c=='\\' || ('a'<=c && c<='z')
+	|| ('A'<=c && c<='Z') || ('0'<=c && c<='9'))
+		editerror(sprint("bad delimiter %c\n", c));
+}
+
+atnl()
+{
+	c: int;
+
+	cmdskipbl();
+	c = getch();
+	if(c != '\n')
+		editerror(sprint("newline expected (saw %c)", c));
+}
+
+Straddc(s: ref String, c: int)
+{
+	s.r[s.n++] = c;
+}
+
+getrhs(s: ref String, delim: int, cmd: int)
+{
+	c: int;
+
+	while((c = getch())>0 && c!=delim && c!='\n'){
+		if(c == '\\'){
+			if((c=getch()) <= 0)
+				error("bad right hand side");
+			if(c == '\n'){
+				ungetch();
+				c='\\';
+			}else if(c == 'n')
+				c='\n';
+			else if(c!=delim && (cmd=='s' || c!='\\'))	# s does its own
+				Straddc(s, '\\');
+		}
+		Straddc(s, c);
+	}
+	ungetch();	# let client read whether delimiter, '\n' or whatever
+}
+
+collecttoken(end: string): ref String
+{
+	c: int;
+
+	s := newstring(0);
+
+	while((c=nextc())==' ' || c=='\t')
+		Straddc(s, getch()); # blanks significant for getname()
+	while((c=getch())>0 && strchr(end, c)<0)
+		Straddc(s, c);
+	if(c != '\n')
+		atnl();
+	return s;
+}
+
+collecttext(): ref String
+{
+	s: ref String;
+	begline, i, c, delim: int;
+
+	s = newstring(0);
+	if(cmdskipbl()=='\n'){
+		getch();
+		i = 0;
+		do{
+			begline = i;
+			while((c = getch())>0 && c!='\n'){
+				i++;
+				Straddc(s, c);
+			}
+			i++;
+			Straddc(s, '\n');
+			if(c < 0)
+				return s;
+		}while(s.r[begline]!='.' || s.r[begline+1]!='\n');
+		s.r[s.n-2] = '\0';
+	}else{
+		okdelim(delim = getch());
+		getrhs(s, delim, 'a');
+		if(nextc()==delim)
+			getch();
+		atnl();
+	}
+	return s;
+}
+
+cmdlookup(c: int): int
+{
+	i: int;
+
+	for(i=0; cmdtab[i].cmdc; i++)
+		if(cmdtab[i].cmdc == c)
+			return i;
+	return -1;
+}
+
+parsecmd(nest: int): ref Cmd
+{
+	i, c: int;
+	cp, ncp: ref Cmd;
+	cmd: ref Cmd;
+
+	cmd = ref Cmd;
+	cmd.next = cmd.cmd = nil;
+	cmd.re = nil;
+	cmd.flag = cmd.num = 0;
+	cmd.addr = compoundaddr();
+	if(cmdskipbl() == -1)
+		return nil;
+	if((c=getch())==-1)
+		return nil;
+	cmd.cmdc = c;
+	if(cmd.cmdc=='c' && nextc()=='d'){	# sleazy two-character case
+		getch();		# the 'd'
+		cmd.cmdc='c'|16r100;
+	}
+	i = cmdlookup(cmd.cmdc);
+	if(i >= 0){
+		if(cmd.cmdc == '\n'){
+			cp = newcmd();
+			*cp = *cmd;
+			return cp;
+			# let nl_cmd work it all out
+		}
+		ct := cmdtab[i];
+		if(ct.defaddr==aNo && cmd.addr != nil)
+			editerror("command takes no address");
+		if(ct.count)
+			cmd.num = getnum(ct.count);
+		if(ct.regexp){
+			# x without pattern -> .*\n, indicated by cmd.re==0
+			# X without pattern is all files
+			if((ct.cmdc!='x' && ct.cmdc!='X') ||
+			   ((c = nextc())!=' ' && c!='\t' && c!='\n')){
+				cmdskipbl();
+				if((c = getch())=='\n' || c<0)
+					editerror("no address");
+				okdelim(c);
+				cmd.re = getregexp(c);
+				if(ct.cmdc == 's'){
+					cmd.text = newstring(0);
+					getrhs(cmd.text, c, 's');
+					if(nextc() == c){
+						getch();
+						if(nextc() == 'g')
+							cmd.flag = getch();
+					}
+			
+				}
+			}
+		}
+		if(ct.addr && (cmd.mtaddr=simpleaddr())==nil)
+			editerror("bad address");
+		if(ct.defcmd){
+			if(cmdskipbl() == '\n'){
+				getch();
+				cmd.cmd = newcmd();
+				cmd.cmd.cmdc = ct.defcmd;
+			}else if((cmd.cmd = parsecmd(nest))==nil)
+				error("defcmd");
+		}else if(ct.text)
+			cmd.text = collecttext();
+		else if(ct.token != nil)
+			cmd.text = collecttoken(ct.token);
+		else
+			atnl();
+	}else
+		case(cmd.cmdc){
+		'{' =>
+			cp = nil;
+			do{
+				if(cmdskipbl()=='\n')
+					getch();
+				ncp = parsecmd(nest+1);
+				if(cp != nil)
+					cp.next = ncp;
+				else
+					cmd.cmd = ncp;
+			}while((cp = ncp) != nil);
+			break;
+		'}' =>
+			atnl();
+			if(nest==0)
+				editerror("right brace with no left brace");
+			return nil;
+		'c'|16r100 =>
+			editerror("unimplemented command cd");
+		* =>
+			editerror(sprint("unknown command %c", cmd.cmdc));
+		}
+	cp = newcmd();
+	*cp = *cmd;
+	return cp;
+}
+
+getregexp(delim: int): ref String
+{
+	buf, r: ref String;
+	i, c: int;
+
+	buf = allocstring(0);
+	for(i=0; ; i++){
+		if((c = getch())=='\\'){
+			if(nextc()==delim)
+				c = getch();
+			else if(nextc()=='\\'){
+				Straddc(buf, c);
+				c = getch();
+			}
+		}else if(c==delim || c=='\n')
+			break;
+		if(i >= BUFSIZE)
+			editerror("regular expression too long");
+		Straddc(buf, c);
+	}
+	if(c!=delim && c)
+		ungetch();
+	if(buf.n > 0){
+		patset = TRUE;
+		freestring(lastpat);
+		lastpat = buf;
+	}else
+		freestring(buf);
+	if(lastpat.n == 0)
+		editerror("no regular expression defined");
+	r = newstring(lastpat.n);
+	k := lastpat.n;
+	for(j := 0; j < k; j++)
+		r.r[j] = lastpat.r[j];	# newstring put \0 at end
+	return r;
+}
+
+simpleaddr(): ref Addr
+{
+	addr: Addr;
+	ap, nap: ref Addr;
+
+	addr.next = nil;
+	addr.left = nil;
+	case(cmdskipbl()){
+	'#' =>
+		addr.typex = getch();
+		addr.num = getnum(1);
+		break;
+	'0' to '9' =>
+		addr.num = getnum(1);
+		addr.typex='l';
+		break;
+	'/' or '?' or '"' =>
+		addr.re = getregexp(addr.typex = getch());
+		break;
+	'.' or
+	'$' or
+	'+' or
+	'-' or
+	'\'' =>
+		addr.typex = getch();
+		break;
+	* =>
+		return nil;
+	}
+	if((addr.next = simpleaddr()) != nil)
+		case(addr.next.typex){
+		'.' or
+		'$' or
+		'\'' =>
+			if(addr.typex!='"')
+				editerror("bad address syntax");
+			break;
+		'"' =>
+			editerror("bad address syntax");
+			break;
+		'l' or
+		'#' =>
+			if(addr.typex=='"')
+				break;
+			if(addr.typex!='+' && addr.typex!='-'){
+				# insert the missing '+'
+				nap = newaddr();
+				nap.typex='+';
+				nap.next = addr.next;
+				addr.next = nap;
+			}
+			break;
+		'/' or
+		'?' =>
+			if(addr.typex!='+' && addr.typex!='-'){
+				# insert the missing '+'
+				nap = newaddr();
+				nap.typex='+';
+				nap.next = addr.next;
+				addr.next = nap;
+			}
+			break;
+		'+' or
+		'-' =>
+			break;
+		* =>
+			error("simpleaddr");
+		}
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
+
+compoundaddr(): ref Addr
+{
+	addr: Addr;
+	ap, next: ref Addr;
+
+	addr.left = simpleaddr();
+	if((addr.typex = cmdskipbl())!=',' && addr.typex!=';')
+		return addr.left;
+	getch();
+	next = addr.next = compoundaddr();
+	if(next != nil && (next.typex==',' || next.typex==';') && next.left==nil)
+		editerror("bad address syntax");
+	ap = newaddr();
+	*ap = addr;
+	return ap;
+}
+
--- /dev/null
+++ b/appl/acme/edit.m
@@ -1,0 +1,85 @@
+Edit: module {
+	#pragma	varargck	argpos	editerror	1
+
+	PATH: con "/dis/acme/edit.dis";
+
+	String: adt{
+		n: int;
+		r: string;
+	};
+
+	Addr: adt{
+		typex: int;		# # (char addr), l (line addr), / ? . $ + - , ;
+		num: int;
+		next: cyclic ref Addr;		# or right side of , and ; 
+		re: ref String;
+		left: cyclic ref Addr;		# left side of , and ; 
+	};
+
+	Address: adt{
+		r: Dat->Range;
+		f: ref Filem->File;
+	};
+
+	Cmd: adt{
+		addr: ref Addr;			# address (range of text)
+		re: ref String;			# regular expression for e.g. 'x'
+		next: cyclic ref Cmd;		# pointer to next element in {}
+		num: int;
+		flag: int;				# whatever
+		cmdc: int;				# command character; 'x' etc.
+		cmd: cyclic ref Cmd;			# target of x, g, {, etc.
+		text: ref String;			# text of a, c, i; rhs of s
+		mtaddr: ref Addr;		# address for m, t
+	};
+
+	Cmdt: adt{
+		cmdc: int;			# command character
+		text: int;			# takes a textual argument?
+		regexp: int;		# takes a regular expression?
+		addr: int;		# takes an address (m or t)?
+		defcmd: int;		# default command; 0==>none
+		defaddr: int;		# default address
+		count: int;		# takes a count e.g. s2///
+		token: string;		# takes text terminated by one of these
+		fnc: int;			# function to call with parse tree
+	};
+
+	cmdtab: array of Cmdt;
+
+	INCR: con 25;	# delta when growing list
+
+	List: adt{
+		nalloc: int;
+		nused: int;
+		pick{
+			C => cmdptr: array of ref Cmd;
+			S => stringptr: array of ref String;
+			A => addrptr: array of ref Addr;
+		}
+	};
+
+	aNo, aDot, aAll: con iota;	# default addresses
+
+	ALLLOOPER, ALLTOFILE, ALLMATCHFILE, ALLFILECHECK, ALLELOGTERM, ALLEDITINIT, ALLUPDATE: con iota;
+
+	C_nl, C_a, C_b, C_c, C_d, C_B, C_D, C_e, C_f, C_g, C_i, C_k, C_m, C_n, C_p, C_s, C_u, C_w, C_x, C_X, C_pipe, C_eq: con iota;
+
+	editing: int;
+	curtext: ref Textm->Text;
+
+	init : fn(mods : ref Dat->Mods);
+
+	allocstring: fn(a0: int): ref String;
+	freestring: fn(a0: ref String);
+	getregexp: fn(a0: int): ref String;
+	newaddr: fn(): ref Addr;
+	editcmd: fn(t: ref Textm->Text, r: string, n: int);
+	editerror: fn(a0: string);
+	cmdlookup: fn(a0: int): int;
+	Straddc: fn(a0: ref String, a1: int);
+
+	allelogterm: fn(w: ref Windowm->Window);
+	alleditinit: fn(w: ref Windowm->Window);
+	allupdate: fn(w: ref Windowm->Window);
+};
--- /dev/null
+++ b/appl/acme/elog.b
@@ -1,0 +1,353 @@
+implement Editlog;
+
+include "common.m";
+
+sys: Sys;
+utils: Utils;
+buffm: Bufferm;
+filem: Filem;
+textm: Textm;
+edit: Edit;
+
+sprint, fprint: import sys;
+FALSE, TRUE, BUFSIZE, Empty, Null, Delete, Insert, Replace, Filename, Astring: import Dat;
+File: import filem;
+Buffer: import buffm;
+Text: import textm;
+error, warning, stralloc, strfree: import utils;
+editerror: import edit;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	utils = mods.utils;
+	buffm = mods.bufferm;
+	filem = mods.filem;
+	textm = mods.textm;
+	edit = mods.edit;
+}
+
+Wsequence := "warning: changes out of sequence\n";
+warned := FALSE;
+
+#
+# Log of changes made by editing commands.  Three reasons for this:
+# 1) We want addresses in commands to apply to old file, not file-in-change.
+# 2) It's difficult to track changes correctly as things move, e.g. ,x m$
+# 3) This gives an opportunity to optimize by merging adjacent changes.
+# It's a little bit like the Undo/Redo log in Files, but Point 3) argues for a
+# separate implementation.  To do this well, we use Replace as well as
+# Insert and Delete
+#
+
+Buflog: adt{
+	typex: int;		# Replace, Filename
+	q0: int;		# location of change (unused in f)
+	nd: int;		# runes to delete
+	nr: int;		# runes in string or file name
+};
+
+Buflogsize: con 7;
+SHM : con 16rffff;
+
+pack(b: Buflog) : string
+{
+	a := "0123456";
+	a[0] = b.typex;
+	a[1] = b.q0&SHM;
+	a[2] = (b.q0>>16)&SHM;
+	a[3] = b.nd&SHM;
+	a[4] = (b.nd>>16)&SHM;
+	a[5] = b.nr&SHM;
+	a[6] = (b.nr>>16)&SHM;
+	return a;
+}
+
+scopy(s1: ref Astring, m: int, s2: string, n: int, o: int)
+{
+	p := o-n;
+	for(i := 0; i < p; i++)
+		s1.s[m++] = s2[n++];
+}
+
+#
+# Minstring shouldn't be very big or we will do lots of I/O for small changes.
+# Maxstring is BUFSIZE so we can fbufalloc() once and not realloc elog.r.
+#
+Minstring: con 16;	# distance beneath which we merge changes
+Maxstring: con BUFSIZE;	# maximum length of change we will merge into one
+
+eloginit(f: ref File)
+{
+	if(f.elog.typex != Empty)
+		return;
+	f.elog.typex = Null;
+	if(f.elogbuf == nil)
+		f.elogbuf = buffm->newbuffer();
+		# f.elogbuf = ref Buffer;
+	if(f.elog.r == nil)
+		f.elog.r = stralloc(BUFSIZE);
+	f.elogbuf.reset();
+}
+
+elogclose(f: ref File)
+{
+	if(f.elogbuf != nil){
+		f.elogbuf.close();
+		f.elogbuf = nil;
+	}
+}
+
+elogreset(f: ref File)
+{
+	f.elog.typex = Null;
+	f.elog.nd = 0;
+	f.elog.nr = 0;
+}
+
+elogterm(f: ref File)
+{
+	elogreset(f);
+	if(f.elogbuf != nil)
+		f.elogbuf.reset();
+	f.elog.typex = Empty;
+	if(f.elog.r != nil){
+		strfree(f.elog.r);
+		f.elog.r = nil;
+	}
+	warned = FALSE;
+}
+
+elogflush(f: ref File)
+{
+	b: Buflog;
+
+	b.typex = f.elog.typex;
+	b.q0 = f.elog.q0;
+	b.nd = f.elog.nd;
+	b.nr = f.elog.nr;
+	case(f.elog.typex){
+	* =>
+		warning(nil, sprint("unknown elog type 0x%ux\n", f.elog.typex));
+		break;
+	Null =>
+		break;
+	Insert or
+	Replace =>
+		if(f.elog.nr > 0)
+			f.elogbuf.insert(f.elogbuf.nc, f.elog.r.s, f.elog.nr);
+		f.elogbuf.insert(f.elogbuf.nc, pack(b), Buflogsize);
+		break;
+	Delete =>
+		f.elogbuf.insert(f.elogbuf.nc, pack(b), Buflogsize);
+		break;
+	}
+	elogreset(f);
+}
+
+elogreplace(f: ref File, q0: int, q1: int, r: string, nr: int)
+{
+	gap: int;
+
+	if(q0==q1 && nr==0)
+		return;
+	eloginit(f);
+	if(f.elog.typex!=Null && q0<f.elog.q0){
+		if(warned++ == 0)
+			warning(nil, Wsequence);
+		elogflush(f);
+	}
+	# try to merge with previous
+	gap = q0 - (f.elog.q0+f.elog.nd);	# gap between previous and this
+	if(f.elog.typex==Replace && f.elog.nr+gap+nr<Maxstring){
+		if(gap < Minstring){
+			if(gap > 0){
+				f.buf.read(f.elog.q0+f.elog.nd, f.elog.r, f.elog.nr, gap);
+				f.elog.nr += gap;
+			}
+			f.elog.nd += gap + q1-q0;
+			scopy(f.elog.r, f.elog.nr, r, 0, nr);
+			f.elog.nr += nr;
+			return;
+		}
+	}
+	elogflush(f);
+	f.elog.typex = Replace;
+	f.elog.q0 = q0;
+	f.elog.nd = q1-q0;
+	f.elog.nr = nr;
+	if(nr > BUFSIZE)
+		editerror(sprint("internal error: replacement string too large(%d)", nr));
+	scopy(f.elog.r, 0, r, 0, nr);
+}
+
+eloginsert(f: ref File, q0: int, r: string, nr: int)
+{
+	n: int;
+
+	if(nr == 0)
+		return;
+	eloginit(f);
+	if(f.elog.typex!=Null && q0<f.elog.q0){
+		if(warned++ == 0)
+			warning(nil, Wsequence);
+		elogflush(f);
+	}
+	# try to merge with previous
+	if(f.elog.typex==Insert && q0==f.elog.q0 && f.elog.nr+nr<Maxstring){
+		ofer := f.elog.r;
+		f.elog.r = stralloc(f.elog.nr+nr);
+		scopy(f.elog.r, 0, ofer.s, 0, f.elog.nr);
+		scopy(f.elog.r, f.elog.nr, r, 0, nr);
+		f.elog.nr += nr;
+		strfree(ofer);
+		return;
+	}
+	while(nr > 0){
+		elogflush(f);
+		f.elog.typex = Insert;
+		f.elog.q0 = q0;
+		n = nr;
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		f.elog.nr = n;
+		scopy(f.elog.r, 0, r, 0, n);
+		r = r[n:];
+		nr -= n;
+	}
+}
+
+elogdelete(f: ref File, q0: int, q1: int)
+{
+	if(q0 == q1)
+		return;
+	eloginit(f);
+	if(f.elog.typex!=Null && q0<f.elog.q0+f.elog.nd){
+		if(warned++ == 0)
+			warning(nil, Wsequence);
+		elogflush(f);
+	}
+	#  try to merge with previous
+	if(f.elog.typex==Delete && f.elog.q0+f.elog.nd==q0){
+		f.elog.nd += q1-q0;
+		return;
+	}
+	elogflush(f);
+	f.elog.typex = Delete;
+	f.elog.q0 = q0;
+	f.elog.nd = q1-q0;
+}
+
+elogapply(f: ref File)
+{
+	b: Buflog;
+	buf: ref Astring;
+	i, n, up, mod : int;
+	log: ref Buffer;
+
+	elogflush(f);
+	log = f.elogbuf;
+	t := f.curtext;
+
+	a := stralloc(Buflogsize);
+	buf = stralloc(BUFSIZE);
+	mod = FALSE;
+
+	#
+	# The edit commands have already updated the selection in t.q0, t.q1.
+	# The text.insert and text.delete calls below will update it again, so save the
+	# current setting and restore it at the end.
+	#
+	q0 := t.q0;
+	q1 := t.q1;
+
+	while(log.nc > 0){
+		up = log.nc-Buflogsize;
+		log.read(up, a, 0, Buflogsize);
+		b.typex = a.s[0];
+		b.q0 = a.s[1]|(a.s[2]<<16);
+		b.nd = a.s[3]|(a.s[4]<<16);
+		b.nr = a.s[5]|(a.s[6]<<16);
+		case(b.typex){
+		* =>
+			error(sprint("elogapply: 0x%ux\n", b.typex));
+			break;
+
+		Replace =>
+			if(!mod){
+				mod = TRUE;
+				f.mark();
+			}
+			# if(b.nd == b.nr && b.nr <= BUFSIZE){
+			#	up -= b.nr;
+			#	log.read(up, buf, 0, b.nr);
+			#	t.replace(b.q0, b.q0+b.nd, buf.s, b.nr, TRUE, 0);
+			#	break;
+			# }
+			t.delete(b.q0, b.q0+b.nd, TRUE);
+			up -= b.nr;
+			for(i=0; i<b.nr; i+=n){
+				n = b.nr - i;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				log.read(up+i, buf, 0, n);
+				t.insert(b.q0+i, buf.s, n, TRUE, 0);
+			}
+			# t.q0 = b.q0;
+			# t.q1 = b.q0+b.nr;
+			break;
+
+		Delete =>
+			if(!mod){
+				mod = TRUE;
+				f.mark();
+			}
+			t.delete(b.q0, b.q0+b.nd, TRUE);
+			# t.q0 = b.q0;
+			# t.q1 = b.q0;
+			break;
+
+		Insert =>
+			if(!mod){
+				mod = TRUE;
+				f.mark();
+			}
+			up -= b.nr;
+			for(i=0; i<b.nr; i+=n){
+				n = b.nr - i;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				log.read(up+i, buf, 0, n);
+				t.insert(b.q0+i, buf.s, n, TRUE, 0);
+			}
+			# t.q0 = b.q0;
+			# t.q1 = b.q0+b.nr;
+			break;
+
+#		Filename =>
+#			f.seq = u.seq;
+#			f.unsetname(epsilon);
+#			f.mod = u.mod;
+#			up -= u.n;
+#			if(u.n == 0)
+#				f.name = nil;
+#			else{
+#				fn0 := stralloc(u.n);
+#				delta.read(up, fn0, 0, u.n);
+#				f.name = fn0.s;
+#				strfree(fn0);
+#			}
+#			break;
+#
+		}
+		log.delete(up, log.nc);
+	}
+	strfree(buf);
+	strfree(a);
+	elogterm(f);
+
+	t.q0 = q0;
+	t.q1 = q1;
+	if(t.q1 > f.buf.nc)	# can't happen
+		t.q1 = f.buf.nc;
+}
--- /dev/null
+++ b/appl/acme/elog.m
@@ -1,0 +1,24 @@
+Editlog: module {
+
+	PATH: con "/dis/acme/elog.dis";
+
+	Elog: adt{
+		typex: int;		# Delete, Insert, Filename
+		q0: int;		# location of change (unused in f)
+		nd: int;		# number of deleted characters
+		nr: int;		# runes in string or file name
+		r: ref Dat->Astring;
+	};
+
+	init : fn(mods : ref Dat->Mods);
+
+	elogterm: fn(a0: ref Filem->File);
+	elogclose: fn(a0: ref Filem->File);
+	eloginsert: fn(a0: ref Filem->File, a1: int, a2: string, a3: int);
+	elogdelete: fn(a0: ref Filem->File, a1: int, a2: int);
+	elogreplace: fn(a0: ref Filem->File, a1: int, a2: int, a3: string, a4: int);
+	elogapply: fn(a0: ref Filem->File);
+
+};
+
+	
--- /dev/null
+++ b/appl/acme/exec.b
@@ -1,0 +1,1350 @@
+implement Exec;
+
+include "common.m";
+
+sys : Sys;
+dat : Dat;
+acme : Acme;
+utils : Utils;
+graph : Graph;
+gui : Gui;
+lookx : Look;
+bufferm : Bufferm;
+textm : Textm;
+scrl : Scroll;
+filem : Filem;
+windowm : Windowm;
+rowm : Rowm;
+columnm : Columnm;
+fsys : Fsys;
+editm: Edit;
+
+Dir, OREAD, OWRITE : import Sys;
+EVENTSIZE, QWaddr, QWdata, QWevent, Astring : import dat;
+Lock, Reffont, Ref, seltext, seq, row : import dat;
+warning, error, skipbl, findbl, stralloc, strfree, exec : import utils;
+dirname : import lookx;
+Body, Text : import textm;
+File : import filem;
+sprint : import sys;
+TRUE, FALSE, XXX, BUFSIZE : import Dat;
+Buffer : import bufferm;
+Row : import rowm;
+Column : import columnm;
+Window : import windowm;
+setalphabet: import textm;
+
+# snarfbuf : ref Buffer;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	acme = mods.acme;
+	utils = mods.utils;
+	graph = mods.graph;
+	gui = mods.gui;
+	lookx = mods.look;
+	bufferm = mods.bufferm;
+	textm = mods.textm;
+	scrl = mods.scroll;
+	filem = mods.filem;
+	rowm = mods.rowm;
+	windowm = mods.windowm;
+	columnm = mods.columnm;
+	fsys = mods.fsys;
+	editm = mods.edit;
+
+	snarfbuf = bufferm->newbuffer();
+}
+
+Exectab : adt {
+	name : string;
+	fun : int;
+	mark : int;
+	flag1 : int;
+	flag2 : int;
+};
+
+F_ALPHABET, F_CUT, F_DEL, F_DELCOL, F_DUMP, F_EDIT, F_EXITX, F_FONTX, F_GET, F_ID, F_INCL, F_KILL, F_LIMBO, F_LINENO, F_LOCAL, F_LOOK, F_NEW, F_NEWCOL, F_PASTE, F_PUT, F_PUTALL, F_UNDO, F_SEND, F_SORT, F_TAB, F_ZEROX : con iota;
+
+exectab := array[] of {
+	Exectab ( "Alphabet",	F_ALPHABET,	FALSE,	XXX,		XXX		),
+	Exectab ( "Cut",		F_CUT,		TRUE,	TRUE,	TRUE	),
+	Exectab ( "Del",			F_DEL,		FALSE,	FALSE,	XXX		),
+	Exectab ( "Delcol",		F_DELCOL,	FALSE,	XXX,		XXX		),
+	Exectab ( "Delete",		F_DEL,		FALSE,	TRUE,	XXX		),
+	Exectab ( "Dump",		F_DUMP,		FALSE,	TRUE,	XXX		),
+	Exectab ( "Edit",		F_EDIT,		FALSE,	XXX,		XXX		),
+	Exectab ( "Exit",		F_EXITX,		FALSE,	XXX,		XXX		),
+	Exectab ( "Font",		F_FONTX,		FALSE,	XXX,		XXX		),
+	Exectab ( "Get",			F_GET,		FALSE,	TRUE,	XXX		),
+	Exectab ( "ID",			F_ID,		FALSE,	XXX,		XXX		),
+	Exectab ( "Incl",		F_INCL,		FALSE,	XXX,		XXX		),
+	Exectab ( "Kill",			F_KILL,		FALSE,	XXX,		XXX		),
+	Exectab ( "Limbo",		F_LIMBO,		FALSE,	XXX,		XXX   	),
+	Exectab ( "Lineno",		F_LINENO,	FALSE,	XXX,		XXX		),
+	Exectab ( "Load",		F_DUMP,		FALSE,	FALSE,	XXX		),
+	Exectab ( "Local",		F_LOCAL,		FALSE,	XXX,		XXX		),
+	Exectab ( "Look",		F_LOOK,		FALSE,	XXX,		XXX		),
+	Exectab ( "New",		F_NEW,		FALSE,	XXX,		XXX		),
+	Exectab ( "Newcol",		F_NEWCOL,	FALSE,	XXX,		XXX		),
+	Exectab ( "Paste",		F_PASTE,		TRUE,	TRUE,	XXX		),
+	Exectab ( "Put",			F_PUT,		FALSE,	XXX,		XXX		),
+	Exectab ( "Putall",		F_PUTALL,	FALSE,	XXX,		XXX		),
+	Exectab ( "Redo",		F_UNDO,		FALSE,	FALSE,	XXX		),
+	Exectab ( "Send",		F_SEND,		TRUE,	XXX,		XXX		),
+	Exectab ( "Snarf",		F_CUT,		FALSE,	TRUE,	FALSE	),
+	Exectab ( "Sort",		F_SORT,		FALSE,	XXX,		XXX		),
+	Exectab ( "Tab",		F_TAB,		FALSE,	XXX,		XXX		),
+	Exectab ( "Undo",		F_UNDO,		FALSE,	TRUE,	XXX		),
+	Exectab ( "Zerox",		F_ZEROX,		FALSE,	XXX,		XXX		),
+	Exectab ( nil, 			0,			0,		0,		0		),
+};
+
+runfun(fun : int, et, t, argt : ref Text, flag1, flag2 : int, arg : string, narg : int)
+{
+	case (fun) {
+		F_ALPHABET	=> alphabet(et, argt, arg, narg);
+		F_CUT 	 	=> cut(et, t, flag1, flag2);
+		F_DEL 		=> del(et, flag1);
+		F_DELCOL	=> delcol(et);
+		F_DUMP 		=> dump(argt, flag1, arg, narg);
+		F_EDIT		=> edit(et, argt, arg, narg);
+		F_EXITX		=> exitx();
+		F_FONTX		=> fontx(et, t, argt, arg, narg);
+		F_GET 		=> get(et, t, argt, flag1, arg, narg);
+		F_ID 		=> id(et);
+		F_INCL 		=> incl(et, argt, arg, narg);
+		F_KILL 		=> kill(argt, arg, narg);
+		F_LIMBO		=> limbo(et);
+		F_LINENO		=> lineno(et);
+		F_LOCAL 		=> local(et, argt, arg);
+		F_LOOK 		=> look(et, t, argt);
+		F_NEW 		=> lookx->new(et, t, argt, flag1, flag2, arg, narg);
+		F_NEWCOL	=> newcol(et);
+		F_PASTE		=> paste(et, t, flag1, flag2);
+		F_PUT		=> put(et, argt, arg, narg);
+		F_PUTALL 	=> putall();
+		F_UNDO 		=> undo(et, flag1);
+		F_SEND		=> send(et, t);
+		F_SORT		=> sort(et);
+		F_TAB		=> tab(et, argt, arg, narg);
+		F_ZEROX		=> zerox(et, t);
+		*			=> error("bad case in runfun()");
+	}
+}	
+		
+lookup(r : string, n : int) : int
+{
+	nr : int;
+
+	(r, n) = skipbl(r, n);
+	if(n == 0)
+		return -1;
+	(nil, nr) = findbl(r, n);
+	nr = n-nr;
+	for(i := 0; exectab[i].name != nil; i++)
+		if (r[0:nr] == exectab[i].name)
+			return i;
+	return -1;
+}
+
+isexecc(c : int) : int
+{
+	if(lookx->isfilec(c))
+		return 1;
+	return c=='<' || c=='|' || c=='>';
+}
+
+execute(t : ref Text, aq0 : int, aq1 : int, external : int, argt : ref Text)
+{
+	q0, q1 : int;
+	r : ref Astring;
+	s, dir, aa, a : string;
+	e : int;
+	c, n, f : int;
+
+	q0 = aq0;
+	q1 = aq1;
+	if(q1 == q0){	# expand to find word (actually file name) 
+		# if in selection, choose selection 
+		if(t.q1>t.q0 && t.q0<=q0 && q0<=t.q1){
+			q0 = t.q0;
+			q1 = t.q1;
+		}else{
+			while(q1<t.file.buf.nc && isexecc(c=t.readc(q1)) && c!=':')
+				q1++;
+			while(q0>0 && isexecc(c=t.readc(q0-1)) && c!=':')
+				q0--;
+			if(q1 == q0)
+				return;
+		}
+	}
+	r = stralloc(q1-q0);
+	t.file.buf.read(q0, r, 0, q1-q0);
+	e = lookup(r.s, q1-q0);
+	if(!external && t.w!=nil && t.w.nopen[QWevent]>byte 0){
+		f = 0;
+		if(e >= 0)
+			f |= 1;
+		if(q0!=aq0 || q1!=aq1){
+			t.file.buf.read(aq0, r, 0, aq1-aq0);
+			f |= 2;
+		}
+		(aa, a) = getbytearg(argt, TRUE, TRUE);
+		if(a != nil){	
+			if(len a > EVENTSIZE){	# too big; too bad 
+				aa = a = nil;
+				warning(nil, "`argument string too long\n");
+				return;
+			}
+			f |= 8;
+		}
+		c = 'x';
+		if(t.what == Body)
+			c = 'X';
+		n = aq1-aq0;
+		if(n <= EVENTSIZE)
+			t.w.event(sprint("%c%d %d %d %d %s\n", c, aq0, aq1, f, n, r.s[0:n]));
+		else
+			t.w.event(sprint("%c%d %d %d 0 \n", c, aq0, aq1, f));
+		if(q0!=aq0 || q1!=aq1){
+			n = q1-q0;
+			t.file.buf.read(q0, r, 0, n);
+			if(n <= EVENTSIZE)
+				t.w.event(sprint("%c%d %d 0 %d %s\n", c, q0, q1, n, r.s[0:n]));
+			else
+				t.w.event(sprint("%c%d %d 0 0 \n", c, q0, q1));
+		}
+		if(a != nil){
+			t.w.event(sprint("%c0 0 0 %d %s\n", c, len a, a));
+			if(aa != nil)
+				t.w.event(sprint("%c0 0 0 %d %s\n", c, len aa, aa));
+			else
+				t.w.event(sprint("%c0 0 0 0 \n", c));
+		}
+		strfree(r);
+		r = nil;
+		a = aa = nil;
+		return;
+	}
+	if(e >= 0){
+		if(exectab[e].mark && seltext!=nil)
+		if(seltext.what == Body){
+			seq++;
+			seltext.w.body.file.mark();
+		}
+		(s, n) = skipbl(r.s, q1-q0);
+		(s, n) = findbl(s, n);
+		(s, n) = skipbl(s, n);
+		runfun(exectab[e].fun, t, seltext, argt, exectab[e].flag1, exectab[e].flag2, s, n);
+		strfree(r);
+		r = nil;
+		return;
+	}
+
+	(dir, n) = dirname(t, nil, 0);
+	if(n==1 && dir[0]=='.'){	# sigh 
+		dir = nil;
+		n = 0;
+	}
+	(aa, a) = getbytearg(argt, TRUE, TRUE);
+	if(t.w != nil)
+		t.w.refx.inc();
+	spawn run(t.w, r.s, dir, n, TRUE, aa, a, FALSE);
+}
+
+printarg(argt : ref Text, q0 : int, q1 : int) : string
+{
+	buf : string;
+
+	if(argt.what!=Body || argt.file.name==nil)
+		return nil;
+	if(q0 == q1)
+		buf = sprint("%s:#%d", argt.file.name, q0);
+	else
+		buf = sprint("%s:#%d,#%d", argt.file.name, q0, q1);
+	return buf;
+}
+
+getarg(argt : ref Text, doaddr : int, dofile : int) : (string, string, int)
+{
+	r : ref Astring;
+	n : int;
+	e : Dat->Expand;
+	a : string;
+	ok : int;
+
+	if(argt == nil)
+		return (nil, nil, 0);
+	a = nil;
+	argt.commit(TRUE);
+	(ok, e) = lookx->expand(argt, argt.q0, argt.q1);
+	if (ok) {
+		e.bname = nil;
+		if(len e.name && dofile){
+			if(doaddr)
+				a = printarg(argt, e.q0, e.q1);
+			return (a, e.name, len e.name);
+		}
+		e.name = nil;
+	}else{
+		e.q0 = argt.q0;
+		e.q1 = argt.q1;
+	}
+	n = e.q1 - e.q0;
+	r = stralloc(n);
+	argt.file.buf.read(e.q0, r, 0, n);
+	if(doaddr)
+		a = printarg(argt, e.q0, e.q1);
+	return(a, r.s, n);
+}
+
+getbytearg(argt : ref Text, doaddr : int, dofile : int) : (string, string)
+{
+	r : string;
+	n : int;
+	aa : string;
+
+	(aa, r, n) = getarg(argt, doaddr, dofile);
+	if(r == nil)
+		return (nil, nil);
+	return (aa, r);
+}
+
+newcol(et : ref Text)
+{
+	c : ref Column;
+
+	c = et.row.add(nil, -1);
+	if(c != nil)
+		c.add(nil, nil, -1).settag();
+}
+
+delcol(et : ref Text)
+{
+	c := et.col;
+	if(c==nil || !c.clean(FALSE))
+		return;
+	for(i:=0; i<c.nw; i++){
+		w := c.w[i];
+		if(int w.nopen[QWevent]+int w.nopen[QWaddr]+int w.nopen[QWdata] > 0){
+			warning(nil, sys->sprint("can't delete column; %s is running an external command\n", w.body.file.name));
+			return;
+		}
+	}
+	c.row.close(c, TRUE);
+}
+
+del(et : ref Text, flag1 : int)
+{
+	if(et.col==nil || et.w == nil)
+		return;
+	if(flag1 || et.w.body.file.ntext>1 || et.w.clean(FALSE, FALSE))
+		et.col.close(et.w, TRUE);
+}
+
+sort(et : ref Text)
+{
+	if(et.col != nil)
+		et.col.sort();
+}
+
+seqof(w: ref Window, isundo: int): int
+{
+	# if it's undo, see who changed with us
+	if(isundo)
+		return w.body.file.seq;
+	# if it's redo, see who we'll be sync'ed up with
+	return w.body.file.redoseq();
+}
+
+undo(et : ref Text, flag1 : int)
+{
+	i, j: int;
+	c: ref Column;
+	w: ref Window;
+	seq: int;
+
+	if(et==nil || et.w== nil)
+		return;
+	seq = seqof(et.w, flag1);
+	for(i=0; i<row.ncol; i++){
+		c = row.col[i];
+		for(j=0; j<c.nw; j++){
+			w = c.w[j];
+			if(seqof(w, flag1) == seq)
+				w.undo(flag1);
+		}
+	}
+	# et.w.undo(flag1);
+}
+
+getname(t : ref Text, argt : ref Text, arg : string, narg : int, isput : int) : string
+{
+	r, dir : string;
+	i, n, ndir, promote : int;
+
+	(nil, r, n) = getarg(argt, FALSE, TRUE);
+	promote = FALSE;
+	if(r == nil)
+		promote = TRUE;
+	else if(isput){
+		# if are doing a Put, want to synthesize name even for non-existent file 
+		# best guess is that file name doesn't contain a slash 
+		promote = TRUE;
+		for(i=0; i<n; i++)
+			if(r[i] == '/'){
+				promote = FALSE;
+				break;
+			}
+		if(promote){
+			t = argt;
+			arg = r;
+			narg = n;
+		}
+	}
+	if(promote){
+		n = narg;
+		if(n <= 0)
+			return t.file.name;
+		# prefix with directory name if necessary 
+		dir = nil;
+		ndir = 0;
+		if(n>0 && arg[0]!='/'){
+			(dir, ndir) = dirname(t, nil, 0);
+			if(n==1 && dir[0]=='.'){	# sigh 
+				dir = nil;
+				ndir = 0;
+			}
+		}
+		if(dir != nil){
+			r = dir[0:ndir] + arg[0:n];
+			dir = nil;
+			n += ndir;
+		}else
+			r = arg[0:n];
+	}
+	return r;
+}
+
+zerox(et : ref Text, t : ref Text)
+{
+	nw : ref Window;
+	c, locked : int;
+
+	locked = FALSE;
+	if(t!=nil && t.w!=nil && t.w!=et.w){
+		locked = TRUE;
+		c = 'M';
+		if(et.w != nil)
+			c = et.w.owner;
+		t.w.lock(c);
+	}
+	if(t == nil)
+		t = et;
+	if(t==nil || t.w==nil)
+		return;
+	t = t.w.body;
+	if(t.w.isdir)
+		warning(nil, sprint("%s is a directory; Zerox illegal\n", t.file.name));
+	else{
+		nw = t.w.col.add(nil, t.w, -1);
+		# ugly: fix locks so w.unlock works 
+		nw.lock1(t.w.owner);
+	}
+	if(locked)
+		t.w.unlock();
+}
+
+get(et : ref Text, t : ref Text, argt : ref Text, flag1 : int, arg : string, narg : int)
+{
+	name : string;
+	r : string;
+	i, n, dirty : int;
+	w : ref Window;
+	u : ref Text;
+	d : Dir;
+	ok : int;
+
+	if(flag1)
+		if(et==nil || et.w==nil)
+			return;
+	if(!et.w.isdir && (et.w.body.file.buf.nc>0 && !et.w.clean(TRUE, FALSE)))
+		return;
+	w = et.w;
+	t = w.body;
+	name = getname(t, argt, arg, narg, FALSE);
+	if(name == nil){
+		warning(nil, "no file name\n");
+		return;
+	}
+	if(t.file.ntext>1){
+		(ok, d) = sys->stat(name);
+		if (ok == 0 && d.qid.qtype & Sys->QTDIR) {
+			warning(nil, sprint("%s is a directory; can't read with multiple windows on it\n", name));
+			return;
+		}
+	}
+	r = name;
+	n = len name;
+	for(i=0; i<t.file.ntext; i++){
+		u = t.file.text[i];
+		# second and subsequent calls with zero an already empty buffer, but OK 
+		u.reset();
+		u.w.dirfree();
+	}
+	samename := r[0:n] == t.file.name;
+	t.loadx(0, name, samename);
+	if(samename){
+		t.file.mod = FALSE;
+		dirty = FALSE;
+	}else{
+		t.file.mod = TRUE;
+		dirty = TRUE;
+	}
+	for(i=0; i<t.file.ntext; i++)
+		t.file.text[i].w.dirty = dirty;
+	name = nil;
+	r = nil;
+	w.settag();
+	t.file.unread = FALSE;
+	for(i=0; i<t.file.ntext; i++){
+		u = t.file.text[i];
+		u.w.tag.setselect(u.w.tag.file.buf.nc, u.w.tag.file.buf.nc);
+		scrl->scrdraw(u);
+	}
+}
+
+putfile(f: ref File, q0: int, q1: int, name: string)
+{
+	n : int;
+	r, s : ref Astring;
+	w : ref Window;
+	i, q : int;
+	fd : ref Sys->FD;
+	d : Dir;
+	ok : int;
+
+	w = f.curtext.w;
+	
+	{
+		if(name == f.name){
+			(ok, d) = sys->stat(name);
+			if(ok >= 0 && (f.dev!=d.dev || f.qidpath!=d.qid.path || f.mtime<d.mtime)){
+				f.dev = d.dev;
+				f.qidpath = d.qid.path;
+				f.mtime = d.mtime;
+				if(f.unread)
+					warning(nil, sys->sprint("%s not written; file already exists\n", name));
+				else
+					warning(nil, sys->sprint("%s modified since last read\n", name));
+				raise "e";
+			}
+		}
+		fd = sys->create(name, OWRITE, 8r664);	# was 666
+		if(fd == nil){
+			warning(nil, sprint("can't create file %s: %r\n", name));
+			raise "e";
+		}
+		r = stralloc(BUFSIZE);
+		s = stralloc(BUFSIZE);
+		
+		{
+			(ok, d) = sys->fstat(fd);
+			if(ok>=0 && (d.mode&Sys->DMAPPEND) && d.length>big 0){
+				warning(nil, sprint("%s not written; file is append only\n", name));
+				raise "e";
+			}
+			for(q = q0; q < q1; q += n){
+				n = q1 - q;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				f.buf.read(q, r, 0, n);
+				ab := array of byte r.s[0:n];
+				if(sys->write(fd, ab, len ab) != len ab){
+					ab = nil;
+					warning(nil, sprint("can't write file %s: %r\n", name));
+					raise "e";
+				}
+				ab = nil;
+			}
+			if(name == f.name){
+				d0 : Dir;
+		
+				if(q0 != 0 || q1 != f.buf.nc){
+					f.mod = TRUE;
+					w.dirty = TRUE;
+					f.unread = TRUE;
+				}
+				else{
+					(ok, d0) = sys->fstat(fd);	# use old values if we failed
+					if (ok >= 0)
+						d = d0;
+					f.qidpath = d.qid.path;
+					f.dev = d.dev;
+					f.mtime = d.mtime;
+					f.mod = FALSE;
+					w.dirty = FALSE;
+					f.unread = FALSE;
+				}
+				for(i=0; i<f.ntext; i++){
+					f.text[i].w.putseq = f.seq;
+					f.text[i].w.dirty = w.dirty;
+				}
+			}
+			strfree(s);
+			strfree(r);
+			s = r = nil;
+			name = nil;
+			fd = nil;
+			w.settag();
+		}
+		exception{
+			* =>
+				strfree(s);
+				strfree(r);
+				s = r = nil;
+				fd = nil;
+				raise "e";
+		}
+	}
+	exception{
+		* =>
+			name = nil;
+			return;
+	}
+}
+
+put(et : ref Text, argt : ref Text, arg : string, narg : int)
+{
+	namer : string;
+	name : string;
+	w : ref Window;
+
+	if(et==nil || et.w==nil || et.w.isdir)
+		return;
+	w = et.w;
+	f := w.body.file;
+	
+	name = getname(w.body, argt, arg, narg, TRUE);
+	if(name == nil){
+		warning(nil, "no file name\n");
+		return;
+	}
+	namer = name;
+	putfile(f, 0, f.buf.nc, namer);
+	name = nil;
+}
+
+dump(argt : ref Text, isdump : int, arg : string, narg : int)
+{
+	name : string;
+
+	if(narg)
+		name = arg;
+	else
+		(nil, name) = getbytearg(argt, FALSE, TRUE);
+	if(isdump)
+		row.dump(name);
+	else {
+		if (!row.qlock.locked())
+			error("row not locked in dump()");
+		row.loadx(name, FALSE);
+	}
+	name = nil;
+}
+
+cut(et : ref Text, t : ref Text, dosnarf : int, docut : int)
+{
+	q0, q1, n, locked, c : int;
+	r : ref Astring;
+
+	# use current window if snarfing and its selection is non-null 
+	if(et!=t && dosnarf && et.w!=nil){
+		if(et.w.body.q1>et.w.body.q0){
+			t = et.w.body;
+			t.file.mark();	# seq has been incremented by execute
+		}
+		else if(et.w.tag.q1>et.w.tag.q0)
+			t = et.w.tag;
+	}
+	if(t == nil)
+		return;
+	locked = FALSE;
+	if(t.w!=nil && et.w!=t.w){
+		locked = TRUE;
+		c = 'M';
+		if(et.w != nil)
+			c = et.w.owner;
+		t.w.lock(c);
+	}
+	if(t.q0 == t.q1){
+		if(locked)
+			t.w.unlock();
+		return;
+	}
+	if(dosnarf){
+		q0 = t.q0;
+		q1 = t.q1;
+		snarfbuf.delete(0, snarfbuf.nc);
+		r = stralloc(BUFSIZE);
+		while(q0 < q1){
+			n = q1 - q0;
+			if(n > BUFSIZE)
+				n = BUFSIZE;
+			t.file.buf.read(q0, r, 0, n);
+			snarfbuf.insert(snarfbuf.nc, r.s, n);
+			q0 += n;
+		}
+		strfree(r);
+		r = nil;
+		acme->putsnarf();
+	}
+	if(docut){
+		t.delete(t.q0, t.q1, TRUE);
+		t.setselect(t.q0, t.q0);
+		if(t.w != nil){
+			scrl->scrdraw(t);
+			t.w.settag();
+		}
+	}else if(dosnarf)	# Snarf command 
+		dat->argtext = t;
+	if(locked)
+		t.w.unlock();
+}
+
+paste(et : ref Text, t : ref Text, selectall : int, tobody: int)
+{
+	c : int;
+	q, q0, q1, n : int;
+	r : ref Astring;
+
+	# if(tobody), use body of executing window  (Paste or Send command)
+	if(tobody && et!=nil && et.w!=nil){
+		t = et.w.body;
+		t.file.mark();	# seq has been incremented by execute
+	}
+	if(t == nil)
+		return;
+
+	acme->getsnarf();
+	if(t==nil || snarfbuf.nc==0)
+		return;
+	if(t.w!=nil && et.w!=t.w){
+		c = 'M';
+		if(et.w != nil)
+			c = et.w.owner;
+		t.w.lock(c);
+	}
+	cut(t, t, FALSE, TRUE);
+	q = 0;
+	q0 = t.q0;
+	q1 = t.q0+snarfbuf.nc;
+	r = stralloc(BUFSIZE);
+	while(q0 < q1){
+		n = q1 - q0;
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		if(r == nil)
+			r = stralloc(n);
+		snarfbuf.read(q, r, 0, n);
+		t.insert(q0, r.s, n, TRUE, 0);
+		q += n;
+		q0 += n;
+	}
+	strfree(r);
+	r = nil;
+	if(selectall)
+		t.setselect(t.q0, q1);
+	else
+		t.setselect(q1, q1);
+	if(t.w != nil){
+		scrl->scrdraw(t);
+		t.w.settag();
+	}
+	if(t.w!=nil && et.w!=t.w)
+		t.w.unlock();
+}
+
+look(et : ref Text, t : ref Text, argt : ref Text)
+{
+	r : string;
+	s : ref Astring;
+	n : int;
+
+	if(et != nil && et.w != nil){
+		t = et.w.body;
+		(nil, r, n) = getarg(argt, FALSE, FALSE);
+		if(r == nil){
+			n = t.q1-t.q0;
+			s = stralloc(n);
+			t.file.buf.read(t.q0, s, 0, n);
+			r = s.s;
+		}
+		lookx->search(t, r, n);
+		r = nil;
+	}
+}
+
+send(et : ref Text, t : ref Text)
+{
+	if(et.w==nil)
+		return;
+	t = et.w.body;
+	if(t.q0 != t.q1)
+		cut(t, t, TRUE, FALSE);
+	t.setselect(t.file.buf.nc, t.file.buf.nc);
+	paste(t, t, TRUE, TRUE);
+	if(t.readc(t.file.buf.nc-1) != '\n'){
+		t.insert(t.file.buf.nc, "\n", 1, TRUE, 0);
+		t.setselect(t.file.buf.nc, t.file.buf.nc);
+	}
+}
+
+edit(et: ref Text, argt: ref Text, arg: string, narg: int)
+{
+	r: string;
+	leng: int;
+
+	if(et == nil)
+		return;
+	(nil, r, leng) = getarg(argt, FALSE, TRUE);
+	seq++;
+	if(r != nil){
+		editm->editcmd(et, r, leng);
+		r = nil;
+	}else
+		editm->editcmd(et, arg, narg);
+}
+
+exitx()
+{
+	if(row.clean(TRUE))
+		acme->acmeexit(nil);
+}
+
+putall()
+{
+	i, j, e : int;
+	w : ref Window;
+	c : ref Column;
+	a : string;
+
+	for(i=0; i<row.ncol; i++){
+		c = row.col[i];
+		for(j=0; j<c.nw; j++){
+			w = c.w[j];
+			if(w.isscratch || w.isdir || len w.body.file.name==0)
+				continue;
+			if(w.nopen[QWevent] > byte 0)
+				continue;
+			a = w.body.file.name;
+			e = utils->access(a);
+			if(w.body.file.mod || w.body.ncache)
+				if(e < 0)
+					warning(nil, sprint("no auto-Put of %s: %r\n", a));
+				else{
+					w.commit(w.body);
+					put(w.body, nil, nil, 0);
+				}
+			a = nil;
+		}
+	}
+}
+
+id(et : ref Text)
+{
+	if(et != nil && et.w != nil)
+		warning(nil, sprint("/mnt/acme/%d/\n", et.w.id));
+}
+
+limbo(et: ref Text)
+{
+	s := getname(et.w.body, nil, nil, 0, 0);
+	if(s == nil)
+		return;
+	for(l := len s; l > 0 && s[--l] != '/'; )
+		;
+	if(s[l] == '/')
+		s = s[l+1: ];
+	s = "limbo -gw " + s;
+	(dir, n) := dirname(et, nil, 0);
+	if(n==1 && dir[0]=='.'){	# sigh 
+		dir = nil;
+		n = 0;
+	}
+	spawn run(nil, s, dir, n, TRUE, nil, nil, FALSE);
+}
+
+local(et : ref Text, argt : ref Text, arg : string)
+{
+	a, aa : string;
+	dir : string;
+	n : int;
+
+	(aa, a) = getbytearg(argt, TRUE, TRUE);
+
+	(dir, n) = dirname(et, nil, 0);
+	if(n==1 && dir[0]=='.'){	# sigh 
+		dir = nil;
+		n = 0;
+	}
+	spawn run(nil, arg, dir, n, FALSE, aa, a, FALSE);
+}
+
+kill(argt : ref Text, arg : string, narg : int)
+{
+	a, cmd, r : string;
+	na : int;
+
+	(nil, r, na) = getarg(argt, FALSE, FALSE);
+	if(r != nil)
+		kill(nil, r, na);
+	# loop condition: *arg is not a blank 
+	for(;;){
+		(a, na) = findbl(arg, narg);
+		if(a == arg)
+			break;
+		cmd = arg[0:narg-na];
+		dat->ckill <-= cmd;
+		(arg, narg) = skipbl(a, na);
+	}
+}
+
+lineno(et : ref Text)
+{
+	n : int;
+
+	if (et == nil || et.w == nil || (et = et.w.body) == nil)
+		return;
+	q0 := et.q0;
+	q1 := et.q1;
+	if (q0 < 0 || q1 < 0 || q0 > q1)
+		return;
+	ln0 := 1;
+	ln1 := 1;
+	rp := stralloc(BUFSIZE);
+	nc := et.file.buf.nc;
+	if (q0 >= nc)
+		q0 = nc-1;
+	if (q1 >= nc)
+		q1 = nc-1;
+	for (q := 0; q < q1; ) {
+		if (q+BUFSIZE > nc)
+			n = nc-q;
+		else
+			n = BUFSIZE;
+		et.file.buf.read(q, rp, 0, n);
+		for (i := 0; i < n && q < q1; i++) {
+			if (rp.s[i] == '\n') {
+				if (q < q0)
+					ln0++;
+				if (q < q1-1)
+					ln1++;
+			}
+			q++;
+		}
+	}
+	rp = nil;
+	if (et.file.name != nil)
+		file := et.file.name + ":";
+	else
+		file = nil;
+	if (ln0 == ln1)
+		warning(nil, sprint("%s%d\n", file, ln0));
+	else
+		warning(nil, sprint("%s%d,%d\n", file, ln0, ln1));
+}
+
+fontx(et : ref Text, t : ref Text, argt : ref Text, arg : string, narg : int)
+{
+	a, r, flag, file : string;
+	na, nf : int;
+	aa : string;
+	newfont : ref Reffont;
+	dp : ref Dat->Dirlist;
+	i, fix : int;
+
+	if(et==nil || et.w==nil)
+		return;
+	t = et.w.body;
+	flag = nil;
+	file = nil;
+	# loop condition: *arg is not a blank 
+	nf = 0;
+	for(;;){
+		(a, na) = findbl(arg, narg);
+		if(a == arg)
+			break;
+		r = arg[0:narg-na];
+		if(r == "fix" || r == "var"){
+			flag = nil;
+			flag = r;
+		}else{
+			file = r;
+			nf = narg-na;
+		}
+		(arg, narg) = skipbl(a, na);
+	}
+	(nil, r, na) = getarg(argt, FALSE, TRUE);
+	if(r != nil)
+		if(r == "fix" || r == "var"){
+			flag = nil;
+			flag = r;
+		}else{
+			file = r;
+			nf = na;
+		}
+	fix = 1;
+	if(flag != nil)
+		fix = flag == "fix";
+	else if(file == nil){
+		newfont = Reffont.get(FALSE, FALSE, FALSE, nil);
+		if(newfont != nil)
+			fix = newfont.f.name == t.frame.font.name;
+	}
+	if(file != nil){
+		aa = file[0:nf];
+		newfont = Reffont.get(fix, flag!=nil, FALSE, aa);
+		aa = nil;
+	}else
+		newfont = Reffont.get(fix, FALSE, FALSE, nil);
+	if(newfont != nil){
+		graph->draw(gui->mainwin, t.w.r, acme->textcols[Framem->BACK], nil, (0, 0));
+		t.reffont.close();
+		t.reffont = newfont;
+		t.frame.font = newfont.f;
+		if(t.w.isdir){
+			t.all.min.x++;	# force recolumnation; disgusting! 
+			for(i=0; i<t.w.ndl; i++){
+				dp = t.w.dlp[i];
+				aa = dp.r;
+				dp.wid = graph->strwidth(newfont.f, aa);
+				aa = nil;
+			}
+		}
+		# avoid shrinking of window due to quantization 
+		t.w.col.grow(t.w, -1, 1);
+	}
+	file = nil;
+	flag = nil;
+}
+
+incl(et : ref Text, argt : ref Text, arg : string, narg : int)
+{
+	a, r : string;
+	w : ref Window;
+	na, n, leng : int;
+
+	if(et==nil || et.w==nil)
+		return;
+	w = et.w;
+	n = 0;
+	(nil, r, leng) = getarg(argt, FALSE, TRUE);
+	if(r != nil){
+		n++;
+		w.addincl(r, leng);
+	}
+	# loop condition: *arg is not a blank 
+	for(;;){
+		(a, na) = findbl(arg, narg);
+		if(a == arg)
+			break;
+		r = arg[0:narg-na];
+		n++;
+		w.addincl(r, narg-na);
+		(arg, narg) = skipbl(a, na);
+	}
+	if(n==0 && w.nincl){
+		for(n=w.nincl; --n>=0; )
+			warning(nil, sprint("%s ", w.incl[n]));
+		warning(nil, "\n");
+	}
+}
+
+tab(et : ref Text, argt : ref Text, arg : string, narg : int)
+{
+	a, r, p : string;
+	w : ref Window;
+	na, leng, tab : int;
+
+	if(et==nil || et.w==nil)
+		return;
+	w = et.w;
+	(nil, r, leng) = getarg(argt, FALSE, TRUE);
+	tab = 0;
+	if(r!=nil && leng>0){
+		p = r[0:leng];
+		if('0'<=p[0] && p[0]<='9')
+			tab = int p;
+		p = nil;
+	}else{
+		(a, na) = findbl(arg, narg);
+		if(a != arg){
+			p = arg[0:narg-na];
+			if('0'<=p[0] && p[0]<='9')
+				tab = int p;
+			p = nil;
+		}
+	}
+	if(tab > 0){
+		if(w.body.tabstop != tab){
+			w.body.tabstop = tab;
+			w.reshape(w.r, 1);
+		}
+	}else
+		warning(nil, sys->sprint("%s: Tab %d\n", w.body.file.name, w.body.tabstop));
+}
+
+alphabet(et: ref Text, argt: ref Text, arg: string, narg: int)
+{
+	r: string;
+	leng: int;
+
+	if(et == nil)
+		return;
+	(nil, r, leng) = getarg(argt, FALSE, FALSE);
+	if(r != nil)
+		setalphabet(r[0:leng]);
+	else
+		setalphabet(arg[0:narg]);
+}
+
+runfeed(p : array of ref Sys->FD, c : chan of int)
+{
+	n : int;
+	buf : array of byte;
+	s : string;
+
+	sys->pctl(Sys->FORKFD, nil);
+	c <-= 1;
+	# p[1] = nil;
+	buf = array[256] of byte;
+	for(;;){
+		if((n = sys->read(p[0], buf, 256)) <= 0)
+			break;
+		s = string buf[0:n];
+		dat->cerr <-= s;
+		s = nil;
+	}
+	buf = nil;
+	exit;
+}
+
+run(win : ref Window, s : string, rdir : string, ndir : int, newns : int, argaddr : string, arg : string, iseditcmd: int)
+{
+	c : ref Dat->Command;
+	name, dir : string;
+	e, t : int;
+	av : list of string;
+	r : int;
+	incl : array of string;
+	inarg, i, nincl : int;
+	tfd : ref Sys->FD;
+	p : array of ref Sys->FD;
+	pc : chan of int;
+	winid : int;
+
+	c = ref Dat->Command;
+	t = 0;
+	while(t < len s && (s[t]==' ' || s[t]=='\n' || s[t]=='\t'))
+		t++;
+	for(e=t; e < len s; e++)
+		if(s[e]==' ' || s[e]=='\n' || s[e]=='\t' )
+			break;
+	name = s[t:e];
+	e = utils->strrchr(name, '/');
+	if(e >= 0)
+		name = name[e+1:];
+	name += " ";	# add blank here for ease in waittask 
+	c.name = name;
+	name = nil;
+	pipechar := 0;
+	if (t < len s && (s[t] == '<' || s[t] == '|' || s[t] == '>')){
+		pipechar = s[t++];
+		s = s[t:];
+	}
+	c.pid = sys->pctl(0, nil);
+	c.iseditcmd = iseditcmd;
+	c.text = s;
+	dat->ccommand <-= c;
+	#
+	# must pctl() after communication because rendezvous name
+	# space is part of RFNAMEG.
+	#
+	 
+	if(newns){
+		wids : string = "";
+		filename: string;
+
+		if(win != nil){
+			filename = win.body.file.name;
+			wids = string win.id;
+			nincl = win.nincl;
+			incl = array[nincl] of string;
+			for(i=0; i<nincl; i++)
+				incl[i] = win.incl[i];
+			winid = win.id;
+			win.close();
+		}else{
+			winid = 0;
+			nincl = 0;
+			incl = nil;
+			if(dat->activewin != nil)
+				winid = (dat->activewin).id;
+		}
+		# sys->pctl(Sys->FORKNS|Sys->FORKFD|Sys->NEWPGRP, nil);
+		sys->pctl(Sys->FORKNS|Sys->NEWFD|Sys->FORKENV|Sys->NEWPGRP, 0::1::2::fsys->fsyscfd()::nil);
+		if(rdir != nil){
+			dir = rdir[0:ndir];
+			sys->chdir(dir);	# ignore error: probably app. window 
+			dir = nil;
+		}
+		if(filename != nil)
+			utils->setenv("%", filename);
+		c.md = fsys->fsysmount(rdir, ndir, incl, nincl);
+		if(c.md == nil){
+			# error("child: can't mount /mnt/acme");
+			warning(nil, "can't mount /mnt/acme");
+			exit;
+		}
+		if(winid > 0 && (pipechar=='|' || pipechar=='>')){
+			buf := sys->sprint("/mnt/acme/%d/rdsel", winid);
+			tfd = sys->open(buf, OREAD);
+		}
+		else
+			tfd = sys->open("/dev/null", OREAD);
+		sys->dup(tfd.fd, 0);
+		tfd = nil;
+		if((winid > 0 || iseditcmd) && (pipechar=='|' || pipechar=='<')){
+			buf: string;
+
+			if(iseditcmd){
+				if(winid > 0)
+					buf = sprint("/mnt/acme/%d/editout", winid);
+				else
+ 					buf = sprint("/mnt/acme/editout");
+			}
+			else
+				buf = sys->sprint("/mnt/acme/%d/wrsel", winid);
+			tfd = sys->open(buf, OWRITE);
+		}
+		else
+			tfd = sys->open("/dev/cons", OWRITE);
+		sys->dup(tfd.fd, 1);
+		tfd = nil;
+		if(winid > 0 && (pipechar=='|' || pipechar=='<')){
+			tfd = sys->open("/dev/cons", OWRITE);
+			sys->dup(tfd.fd, 2);
+		}
+		else
+			sys->dup(1, 2);
+		tfd = nil;
+		utils->setenv("acmewin", wids);
+	}else{
+		if(win != nil)
+			win.close();
+		sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+		if(rdir != nil){
+			dir = rdir[0:ndir];
+			sys->chdir(dir);	# ignore error: probably app. window 
+			dir = nil;
+		}
+		p = array[2] of ref Sys->FD;
+		if(sys->pipe(p) < 0){
+			error("child: can't pipe");
+			exit;
+		}
+		pc = chan of int;
+		spawn runfeed(p, pc);
+		<-pc;
+		pc = nil;
+		fsys->fsysclose();
+		tfd = sys->open("/dev/null", OREAD);
+		sys->dup(tfd.fd, 0);
+		tfd = nil;
+		sys->dup(p[1].fd, 1);
+		sys->dup(1, 2);
+		p[0] = p[1] = nil;
+	}
+
+	if(argaddr != nil)
+		utils->setenv("acmeaddr", argaddr);
+	hard := 0;
+	if(len s > 512-10)	# may need to print into stack 
+		hard = 1;
+	else {
+		inarg = FALSE;
+		for(e=0; e < len s; e++){
+			r = s[e];
+			if(r==' ' || r=='\t')
+				continue;
+			if(r < ' ') {
+				hard = 1;
+				break;
+			}
+			if(utils->strchr("#;&|^$=`'{}()<>[]*?^~`", r) >= 0) {
+				hard = 1;
+				break;
+			}
+			inarg = TRUE;
+		}
+		if (!hard) {
+			if(!inarg)
+				exit;
+			av = nil;
+			sa := -1;
+			for(e=0; e < len s; e++){
+				r = s[e];
+				if(r==' ' || r=='\t'){
+					if (sa >= 0) {
+						av = s[sa:e] :: av;
+						sa = -1;
+					}
+					continue;
+				}
+				if (sa < 0)
+					sa = e;
+			}
+			if (sa >= 0)
+				av = s[sa:e] :: av;
+			if (arg != nil)
+				av = arg :: av;
+			av = utils->reverse(av);
+			c.av = av;
+			exec(hd av, av);
+			dat->cwait <-= string c.pid + " \"Exec\":";
+			exit;
+		}
+	}
+
+	if(arg != nil){
+		s = sprint("%s '%s'", s, arg);	# BUG: what if quote in arg? 
+		c.text = s;
+	}
+	av = nil;
+	av = s :: av;
+	av = "-c" :: av;
+	av = "/dis/sh" :: av;
+	exec(hd av, av);
+	dat->cwait <-= string c.pid + " \"Exec\":";
+	exit;
+}
+
+# Nasty bug causes
+# Edit ,|nonexistentcommand
+# (or ,> or ,<) to lock up acme.  Easy fix.  Add these two lines
+# to the failure case of runwaittask():
+#
+# /sys/src/cmd/acme/exec.c:1287 a exec.c:1288,1289
+# else{
+# if(c->iseditcmd)
+# sendul(cedit, 0);
+# free(c->name);
+# free(c->text);
+# free(c);
+# }
+
+
--- /dev/null
+++ b/appl/acme/exec.m
@@ -1,0 +1,19 @@
+Exec : module {
+	PATH : con "/dis/acme/exec.dis";
+
+	snarfbuf : ref Bufferm->Buffer;
+
+	init : fn(mods : ref Dat->Mods);
+
+	fontx : fn(et : ref Textm->Text, t : ref Textm->Text, argt : ref Textm->Text, arg : string, narg : int);
+	get : fn(et, t, argt : ref Textm->Text, flag1 : int, arg : string, narg : int);
+	put : fn(et, argt : ref Textm->Text, arg : string, narg : int);
+	cut : fn(et, t : ref Textm->Text, flag1, flag2 : int);
+	paste : fn(et, t : ref Textm->Text, flag1 : int, flag2: int);
+
+	getarg : fn(t : ref Textm->Text, m : int, n : int) : (string, string, int);
+	execute : fn(t : ref Textm->Text, aq0, aq1, external : int, argt : ref Textm->Text);
+	run : fn(w : ref Windowm->Window, s : string, rdir : string, ndir : int, newns : int, argaddr : string, arg : string, ise: int);
+	undo: fn(t: ref Textm->Text, flag: int);
+	putfile: fn(f: ref Filem->File, q0: int, q1: int, r: string);
+};
--- /dev/null
+++ b/appl/acme/file.b
@@ -1,0 +1,331 @@
+implement Filem;
+
+include "common.m";
+
+sys : Sys;
+dat : Dat;
+utils : Utils;
+buffm : Bufferm;
+textm : Textm;
+editlog: Editlog;
+
+FALSE, TRUE, XXX, Delete, Insert, Filename, BUFSIZE, Astring : import Dat;
+Buffer, newbuffer : import buffm;
+Text : import textm;
+error : import utils;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	buffm = mods.bufferm;
+	textm = mods.textm;
+	editlog = mods.editlog;
+}
+
+#
+# Structure of Undo list:
+# 	The Undo structure follows any associated data, so the list
+#	can be read backwards: read the structure, then read whatever
+#	data is associated (insert string, file name) and precedes it.
+#	The structure includes the previous value of the modify bit
+#	and a sequence number; successive Undo structures with the
+#	same sequence number represent simultaneous changes.
+#
+ 
+Undo : adt
+{
+	typex : int;	# Delete, Insert, Filename 
+	mod : int;		# modify bit 
+	seq : int;		# sequence number 
+	p0 : int;		# location of change (unused in f) 
+	n : int;		# # runes in string or file name 
+};
+
+Undosize : con 8;
+SHM : con 16rffff;
+
+undotostr(t, m, s, p, n : int) : string
+{
+	a := "01234567";
+	a[0] = t;
+	a[1] = m;
+	a[2] = s&SHM;
+	a[3] = (s>>16)&SHM;
+	a[4] = p&SHM;
+	a[5] = (p>>16)&SHM;
+	a[6] = n&SHM;
+	a[7] = (n>>16)&SHM;
+	return a;
+}
+
+strtoundo(s: string): Undo
+{
+	u: Undo;
+
+	u.typex = s[0];
+	u.mod = s[1];
+	u.seq = s[2]|(s[3]<<16);
+	u.p0 = s[4]|(s[5]<<16);
+	u.n = s[6]|(s[7]<<16);
+	return u;
+}
+
+nullfile : File;
+
+File.addtext(f : self ref File, t : ref Text) : ref File
+{
+	if(f == nil) {
+		f = ref nullfile;
+		f.buf = newbuffer();
+		f.delta = newbuffer();
+		f.epsilon = newbuffer();
+		f.ntext = 0;
+		f.unread = TRUE;
+	}
+	oft := f.text;
+	f.text = array[f.ntext+1] of ref Text;
+	f.text[0:] = oft[0:f.ntext];
+	oft = nil;
+	f.text[f.ntext++] = t;
+	f.curtext = t;
+	return f;
+}
+
+File.deltext(f : self ref File, t : ref Text)
+{
+	i : int;
+
+	for(i=0; i<f.ntext; i++)
+		if(f.text[i] == t)
+			break;
+	if (i == f.ntext)
+		error("can't find text in File.deltext");
+
+	f.ntext--;
+	if(f.ntext == 0){
+		f.close();
+		return;
+	}
+	f.text[i:] = f.text[i+1:f.ntext+1];
+	if(f.curtext == t)
+		f.curtext = f.text[0];
+}
+
+File.insert(f : self ref File, p0 : int, s : string, ns : int)
+{
+	if (p0 > f.buf.nc)
+		error("bad assert in File.insert");
+	if(f.seq > 0)
+		f.uninsert(f.delta, p0, ns);
+	f.buf.insert(p0, s, ns);
+	if(ns)
+		f.mod = TRUE;
+}
+
+File.uninsert(f : self ref File, delta : ref Buffer, p0 : int, ns : int)
+{
+	# undo an insertion by deleting 
+	a := undotostr(Delete, f.mod, f.seq, p0, ns);
+	delta.insert(delta.nc, a, Undosize);
+}
+
+File.delete(f : self ref File, p0 : int, p1 : int)
+{
+	if (p0>p1 || p0>f.buf.nc || p1>f.buf.nc)
+		error("bad assert in File.delete");
+	if(f.seq > 0)
+		f.undelete(f.delta, p0, p1);
+	f.buf.delete(p0, p1);
+	if(p1 > p0)
+		f.mod = TRUE;
+}
+
+File.undelete(f : self ref File, delta : ref Buffer, p0 : int, p1 : int)
+{
+	buf : ref Astring;
+	i, n : int;
+
+	# undo a deletion by inserting 
+	a := undotostr(Insert, f.mod, f.seq, p0, p1-p0);
+	m := p1-p0;
+	if(m > BUFSIZE)
+		m = BUFSIZE;
+	buf = utils->stralloc(m);
+	for(i=p0; i<p1; i+=n){
+		n = p1 - i;
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		f.buf.read(i, buf, 0, n);
+		delta.insert(delta.nc, buf.s, n);
+	}
+	utils->strfree(buf);
+	buf = nil;
+	delta.insert(delta.nc, a, Undosize);
+}
+
+File.setname(f : self ref File, name : string, n : int)
+{
+	if(f.seq > 0)
+		f.unsetname(f.delta);
+	f.name = name[0:n];
+	f.unread = TRUE;
+}
+
+File.unsetname(f : self ref File, delta : ref Buffer)
+{
+	# undo a file name change by restoring old name 
+	a := undotostr(Filename, f.mod, f.seq, 0, len f.name);
+	if(f.name != nil)
+		delta.insert(delta.nc, f.name, len f.name);
+	delta.insert(delta.nc, a, Undosize);
+}
+
+File.loadx(f : self ref File, p0 : int, fd : ref Sys->FD) : int
+{
+	if(f.seq > 0)
+		error("undo in file.load unimplemented");
+	return f.buf.loadx(p0, fd);
+}
+
+File.undo(f : self ref File, isundo : int, q0 : int, q1 : int) : (int, int)
+{
+	buf : ref Astring;
+	i, j, n, up : int;
+	stop : int;
+	delta, epsilon : ref Buffer;
+	u : Undo;
+
+	a := utils->stralloc(Undosize);
+	if(isundo){
+		# undo; reverse delta onto epsilon, seq decreases 
+		delta = f.delta;
+		epsilon = f.epsilon;
+		stop = f.seq;
+	}else{
+		# redo; reverse epsilon onto delta, seq increases 
+		delta = f.epsilon;
+		epsilon = f.delta;
+		stop = 0;	# don't know yet 
+	}
+
+	buf = utils->stralloc(BUFSIZE);
+	while(delta.nc > 0){
+		up = delta.nc-Undosize;
+		delta.read(up, a, 0, Undosize);
+		u = strtoundo(a.s);
+		if(isundo){
+			if(u.seq < stop){
+				f.seq = u.seq;
+				utils->strfree(buf);
+				utils->strfree(a);
+				return (q0, q1);
+			}
+		}else{
+			if(stop == 0)
+				stop = u.seq;
+			if(u.seq > stop){
+				utils->strfree(buf);
+				utils->strfree(a);
+				return (q0, q1);
+			}
+		}
+		case(u.typex){
+		Delete =>
+			f.seq = u.seq;
+			f.undelete(epsilon, u.p0, u.p0+u.n);
+			f.mod = u.mod;
+			f.buf.delete(u.p0, u.p0+u.n);
+			for(j=0; j<f.ntext; j++)
+				f.text[j].delete(u.p0, u.p0+u.n, FALSE);
+			q0 = u.p0;
+			q1 = u.p0;
+		Insert =>
+			f.seq = u.seq;
+			f.uninsert(epsilon, u.p0, u.n);
+			f.mod = u.mod;
+			up -= u.n;
+			# buf = utils->stralloc(BUFSIZE);
+			for(i=0; i<u.n; i+=n){
+				n = u.n - i;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				delta.read(up+i, buf, 0, n);
+				f.buf.insert(u.p0+i, buf.s, n);
+				for(j=0; j<f.ntext; j++)
+					f.text[j].insert(u.p0+i, buf.s, n, FALSE, 0);
+			}
+			# utils->strfree(buf);
+			# buf = nil;
+			q0 = u.p0;
+			q1 = u.p0+u.n;
+		Filename =>
+			f.seq = u.seq;
+			f.unsetname(epsilon);
+			f.mod = u.mod;
+			up -= u.n;
+			f.name = nil;
+			if(u.n == 0)
+				f.name = nil;
+			else {
+				fn0 := utils->stralloc(u.n);
+				delta.read(up, fn0, 0, u.n);
+				f.name = fn0.s;
+				utils->strfree(fn0);
+				fn0 = nil;
+			}
+		* =>
+			error(sys->sprint("undo: 0x%ux", u.typex));
+			error("");
+		}
+		delta.delete(up, delta.nc);
+	}
+	utils->strfree(buf);
+	utils->strfree(a);
+	buf = nil;
+	if(isundo)
+		f.seq = 0;
+	return (q0, q1);
+}
+
+File.reset(f : self ref File)
+{
+	f.delta.reset();
+	f.epsilon.reset();
+	f.seq = 0;
+}
+
+File.close(f : self ref File)
+{
+	f.name = nil;
+	f.ntext = 0;
+	f.text = nil;
+	f.buf.close();
+	f.delta.close();
+	f.epsilon.close();
+	editlog->elogclose(f);
+	f = nil;
+}
+
+File.mark(f : self ref File)
+{
+	if(f.epsilon.nc)
+		f.epsilon.delete(0, f.epsilon.nc);
+	f.seq = dat->seq;
+}
+
+File.redoseq(f : self ref File): int
+{
+	u: Undo;
+	delta: ref Buffer;
+
+	delta = f.epsilon;
+	if(delta.nc == 0)
+		return ~0;
+	buf := utils->stralloc(Undosize);
+	delta.read(delta.nc-Undosize, buf, 0, Undosize);
+	u = strtoundo(buf.s);
+	utils->strfree(buf);
+	return u.seq;
+}
--- /dev/null
+++ b/appl/acme/file.m
@@ -1,0 +1,40 @@
+Filem : module {
+	PATH : con "/dis/acme/file.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	File : adt {
+		buf : ref Bufferm->Buffer;		# the data
+		delta : ref Bufferm->Buffer;		# transcript of changes
+		epsilon : ref Bufferm->Buffer;	# inversion of delta for redo
+		elogbuf: ref Bufferm->Buffer;	# log of pending editor changes
+		elog: Editlog->Elog;			# current pending change
+		name : string;				# name of associated file
+		qidpath : big;				# of file when read
+		mtime : int;				# of file when read
+		dev : int;					# of file when read
+		unread : int;				# file has not been read from disk
+		editclean: int;				# mark clean after edit command
+		seq : int;					# if seq==0, File acts like Buffer
+		mod : int;
+		curtext : cyclic ref Textm->Text;	# most recently used associated text
+		text : cyclic array of ref Textm->Text;		# list of associated texts
+		ntext : int;
+		dumpid : int;				# used in dumping zeroxed windows
+
+		addtext : fn(f : self ref File, t : ref Textm->Text) : ref File;
+		deltext : fn(f : self ref File, t : ref Textm->Text);
+		insert : fn(f : self ref File, n : int, s : string, m : int);
+		delete : fn(f : self ref File, m : int, n : int);
+		loadx : fn(f : self ref File, p : int, fd : ref Sys->FD) : int;
+		setname : fn(f : self ref File, s : string, n : int);
+		undo : fn(f : self ref File, p : int, q : int, r : int) : (int, int);
+		mark : fn(f : self ref File);
+		reset : fn(f : self ref File);
+		close : fn(f : self ref File);
+		undelete : fn(f : self ref File, b : ref Bufferm->Buffer, m : int, n : int);
+		uninsert : fn(f : self ref File, b : ref Bufferm->Buffer, m : int, n : int);
+		unsetname : fn(f : self ref File, b : ref Bufferm->Buffer);
+		redoseq : fn(f: self ref File): int;
+	};
+};
--- /dev/null
+++ b/appl/acme/frame.b
@@ -1,0 +1,1189 @@
+implement Framem;
+
+include "common.m";
+
+sys : Sys;
+drawm : Draw;
+acme : Acme;
+gui : Gui;
+graph : Graph;
+utils : Utils;
+textm : Textm;
+
+sprint : import sys;
+Point, Rect, Font, Image, Pointer : import drawm;
+draw, berror, charwidth, strwidth : import graph;
+black, white : import gui;
+
+SLOP : con 25;
+
+noglyphs := array[4] of { 16rFFFD, 16r80, '?', ' ' };
+
+frame : ref Frame;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	drawm = mods.draw;
+	acme = mods.acme;
+	gui = mods.gui;
+	graph = mods.graph;
+	utils = mods.utils;
+	textm = mods.textm;
+
+	frame = newframe();
+}
+
+nullframe : Frame;
+
+newframe() : ref Frame
+{
+	f := ref nullframe;
+	f.cols = array[NCOL] of ref Draw->Image;
+	return f;
+}
+
+frdump(f : ref Frame)
+{
+	utils->debug(sprint("nchars=%d\n", f.nchars));
+	for (i := 0; i < f.nbox; i++) {
+		utils->debug(sprint("box %d : ", i));
+		fb := f.box[i];
+		if (fb.nrune >= 0)
+			utils->debug(sprint("%d %d %s\n", fb.nrune, len fb.ptr, fb.ptr));
+		else
+			utils->debug(sprint("%d\n", fb.nrune));
+	}
+}
+
+# debugcheck(f : ref Frame, n : int)
+# {
+# 	if (f.nchars != xfrstrlen(f, 0)) {
+#		utils->debug(sprint("%d : bad frame nchars\n", n));
+#		frdump(f);
+#		berror("");
+#	}
+# }
+		
+xfraddbox(f : ref Frame, bn : int, n : int)		# add n boxes after bn, shift the rest up,
+									#  * box[bn+n]==box[bn]
+{
+	i : int;
+
+	if(bn > f.nbox)
+		berror("xfraddbox");
+	# bn = f.nbox has same effect as bn = f.nbox-1
+	if(f.nbox+n > f.nalloc)
+		xfrgrowbox(f, n+SLOP);
+	for (i=f.nbox; --i > bn; ) {
+		t := f.box[i+n];
+		f.box[i+n] = f.box[i];
+		f.box[i] = t;
+	}
+	if (bn < f.nbox)
+		*f.box[bn+n] = *f.box[bn];
+	f.nbox+=n;
+}
+
+xfrclosebox(f : ref Frame, n0 : int, n1 : int)	# inclusive
+{
+	i: int;
+
+	if(n0>=f.nbox || n1>=f.nbox || n1<n0)
+		berror("xfrclosebox");
+	n1++;
+	for(i=n1; i<f.nbox; i++) {
+		t := f.box[i-(n1-n0)];
+		f.box[i-(n1-n0)] = f.box[i];
+		f.box[i] = t;
+	}
+	f.nbox -= n1-n0;
+}
+
+xfrdelbox(f : ref Frame, n0 : int, n1 : int)		# inclusive
+{
+	if(n0>=f.nbox || n1>=f.nbox || n1<n0)
+		berror("xfrdelbox");
+	xfrfreebox(f, n0, n1);
+	xfrclosebox(f, n0, n1);
+}
+
+xfrfreebox(f : ref Frame, n0 : int, n1 : int)		# inclusive
+{
+	i : int;
+
+	if(n1<n0)
+		return;
+	if(n0>=f.nbox || n1>=f.nbox)
+		berror("xfrfreebox");
+	n1++;
+	for(i=n0; i<n1; i++)
+		if(f.box[i].nrune >= 0) {
+			f.box[i].nrune = 0;
+			f.box[i].ptr = nil;
+		}
+}
+
+nilfrbox : Frbox;
+
+xfrgrowbox(f : ref Frame, delta : int)
+{
+	ofb := f.box;
+	f.box = array[f.nalloc+delta] of ref Frbox;
+	if(f.box == nil)
+		berror("xfrgrowbox");
+	f.box[0:] = ofb[0:f.nalloc];
+	for (i := 0; i < delta; i++)
+		f.box[i+f.nalloc] = ref nilfrbox;
+	f.nalloc += delta;
+	ofb = nil;
+}
+
+dupbox(f : ref Frame, bn : int)
+{
+	if(f.box[bn].nrune < 0)
+		berror("dupbox");
+	xfraddbox(f, bn, 1);
+	if(f.box[bn].nrune >= 0) {
+		f.box[bn+1].nrune = f.box[bn].nrune;
+		f.box[bn+1].ptr = f.box[bn].ptr;
+	}
+}
+
+truncatebox(f : ref Frame, b : ref Frbox, n : int)	# drop last n chars; no allocation done
+{
+	if(b.nrune<0 || b.nrune<n)
+		berror("truncatebox");
+	b.nrune -= n;
+	b.ptr = b.ptr[0:b.nrune];
+	b.wid = strwidth(f.font, b.ptr);
+}
+
+chopbox(f : ref Frame, b : ref Frbox, n : int)	# drop first n chars; no allocation done
+{
+	if(b.nrune<0 || b.nrune<n)
+		berror("chopbox");
+	b.nrune -= n;
+	b.ptr = b.ptr[n:];
+	b.wid = strwidth(f.font, b.ptr);
+}
+
+xfrsplitbox(f : ref Frame, bn : int, n : int)
+{
+	dupbox(f, bn);
+	truncatebox(f, f.box[bn], f.box[bn].nrune-n);
+	chopbox(f, f.box[bn+1], n);
+}
+
+xfrmergebox(f : ref Frame, bn : int)		# merge bn and bn+1
+{
+	b0 := f.box[bn];
+	b1 := f.box[bn+1];
+	b0.ptr += b1.ptr;
+	b0.wid += b1.wid;
+	b0.nrune += b1.nrune;
+	xfrdelbox(f, bn+1, bn+1);
+}
+
+xfrfindbox(f : ref Frame, bn : int, p : int, q : int) : int	# find box containing q and put q on a box boundary
+{
+	nrune : int;
+
+	for( ; bn < f.nbox; bn++) {
+		nrune = 1;
+		b := f.box[bn];
+# if (b.nrune >= 0 && len b.ptr != b.nrune) {
+#	frdump(f);
+#	berror(sprint("findbox %d %d %d\n", bn, p, q));
+# }
+		if(b.nrune >= 0)
+			nrune = b.nrune;
+		if(p+nrune > q)
+			break; 
+		p += nrune;
+	}
+	if(p != q)
+		xfrsplitbox(f, bn++, q-p);
+	return bn;
+}
+
+frdelete(f : ref Frame, p0 : int, p1 : int) : int
+{
+	pt0, pt1, ppt0 : Point;
+	n0, n1, n, s : int;
+	r : Rect;
+	nn0 : int;
+	col : ref Image;
+
+	if(p0 >= f.nchars || p0 == p1 || f.b == nil)
+		return 0;
+	if(p1 > f.nchars)
+		p1 = f.nchars;
+	n0 = xfrfindbox(f, 0, 0, p0);
+	if(n0 == f.nbox)
+		berror("off end in frdelete");
+	n1 = xfrfindbox(f, n0, p0, p1);
+	pt0 = xfrptofcharnb(f, p0, n0);
+	pt1 = frptofchar(f, p1);
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 0);
+	nn0 = n0;
+	ppt0 = pt0;
+	xfrfreebox(f, n0, n1-1);
+	f.modified = 1;
+
+	#
+	# Invariants:
+	#  pt0 points to beginning, pt1 points to end
+	#  n0 is box containing beginning of stuff being deleted
+	#  n1, b are box containing beginning of stuff to be kept after deletion
+	# cn1 is char position of n1
+	# f.p0 and f.p1 are not adjusted until after all deletion is done
+	#  region between pt0 and pt1 is clear
+	#
+	cn1 := p1;
+	while(pt1.x!=pt0.x && n1<f.nbox){
+		b := f.box[n1];
+		pt0 = xfrcklinewrap0(f, pt0, b);
+		pt1 = xfrcklinewrap(f, pt1, b);
+		n = xfrcanfit(f, pt0, b);
+		if(n==0)
+			berror("xfrcanfit==0");
+		r.min = pt0;
+		r.max = pt0;
+		r.max.y += f.font.height;
+		if(b.nrune > 0){
+			if(n != b.nrune){
+				xfrsplitbox(f, n1, n);
+				b = f.box[n1];
+			}
+			r.max.x += b.wid;
+			draw(f.b, r, f.b, nil, pt1);
+			cn1 += b.nrune;
+		}
+		else{
+			r.max.x += xfrnewwid0(f, pt0, b);
+			if(r.max.x > f.r.max.x)
+				r.max.x = f.r.max.x;
+			col = f.cols[BACK];
+			if(f.p0<=cn1 && cn1<f.p1)
+				col = f.cols[HIGH];
+			draw(f.b, r, col, nil, pt0);
+			cn1++;
+		}
+		pt1 = xfradvance(f, pt1, b);
+		pt0.x += xfrnewwid(f, pt0, b);
+		*f.box[n0++] = *f.box[n1++];
+	}
+	if(n1==f.nbox && pt0.x!=pt1.x)	# deleting last thing in window; must clean up
+		frselectpaint(f, pt0, pt1, f.cols[BACK]);
+	if(pt1.y != pt0.y){
+		pt2 : Point;
+
+		pt2 = xfrptofcharptb(f, 32767, pt1, n1);
+		if(pt2.y > f.r.max.y)
+			berror("frptofchar in frdelete");
+		if(n1 < f.nbox){
+			q0, q1, q2 : int;
+
+			q0 = pt0.y+f.font.height;
+			q1 = pt1.y+f.font.height;
+			q2 = pt2.y+f.font.height;
+			# rob: before was just q2 = pt1.y+f.font.height;
+			# q2 = pt2.y;
+			if(q2 > f.r.max.y)
+				q2 = f.r.max.y;
+			draw(f.b, (pt0, (pt0.x+(f.r.max.x-pt1.x), q0)),
+				f.b, nil, pt1);
+			draw(f.b, ((f.r.min.x, q0), (f.r.max.x, q0+(q2-q1))),
+				f.b, nil, (f.r.min.x, q1));
+			frselectpaint(f, (pt2.x, pt2.y-(pt1.y-pt0.y)), pt2, f.cols[BACK]);
+		}else
+			frselectpaint(f, pt0, pt2, f.cols[BACK]);
+	}
+	xfrclosebox(f, n0, n1-1);
+	if(nn0>0 && f.box[nn0-1].nrune>=0 && ppt0.x-f.box[nn0-1].wid>=f.r.min.x){
+		--nn0;
+		ppt0.x -= f.box[nn0].wid;
+	}
+	s = n0;
+	if(n0 < f.nbox-1)
+		s++;
+	xfrclean(f, ppt0, nn0, s);
+	if(f.p1 > p1)
+		f.p1 -= p1-p0;
+	else if(f.p1 > p0)
+		f.p1 = p0;
+	if(f.p0 > p1)
+		f.p0 -= p1-p0;
+	else if(f.p0 > p0)
+		f.p0 = p0;
+	f.nchars -= p1-p0;
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 1);
+	pt0 = frptofchar(f, f.nchars);
+	n = f.nlines;
+	f.nlines = (pt0.y-f.r.min.y)/f.font.height+(pt0.x>f.r.min.x);
+	return n - f.nlines;
+}
+
+xfrredraw(f : ref Frame, pt : Point)
+{
+	nb : int;
+
+	for(nb = 0; nb < f.nbox; nb++) {
+		b := f.box[nb];
+		pt = xfrcklinewrap(f, pt, b);
+		if(b.nrune >= 0)
+			graph->stringx(f.b, pt, f.font, b.ptr, f.cols[TEXT]);
+		pt.x += b.wid;
+	}
+}
+
+frdrawsel(f : ref Frame, pt : Point, p0 : int, p1 : int, issel : int)
+{
+	back, text : ref Image;
+
+	if(f.ticked)
+		frtick(f, frptofchar(f, f.p0), 0);
+	if(p0 == p1){
+		frtick(f, pt, issel);
+		return;
+	}
+	if(issel){
+		back = f.cols[HIGH];
+		text = f.cols[HTEXT];
+	}else{
+		back = f.cols[BACK];
+		text = f.cols[TEXT];
+	}
+	frdrawsel0(f, pt, p0, p1, back, text);
+}
+
+frdrawsel0(f : ref Frame, pt : Point, p0 : int, p1 : int, back : ref Image, text : ref Image)
+{
+	b : ref Frbox;
+	nb, nr, w, x, trim : int;
+	qt : Point;
+	p : int;
+	ptr : string;
+
+	p = 0;
+	trim = 0;
+	for(nb=0; nb<f.nbox && p<p1; nb++){
+		b = f.box[nb];
+		nr = b.nrune;
+		if(nr < 0)
+			nr = 1;
+		if(p+nr <= p0){
+			p += nr;
+			continue;
+		}
+		if(p >= p0){
+			qt = pt;
+			pt = xfrcklinewrap(f, pt, b);
+			if(pt.y > qt.y)
+				draw(f.b, (qt, (f.r.max.x, pt.y)), back, nil, qt);
+		}
+		ptr = b.ptr;
+		if(p < p0){		# beginning of region: advance into box
+			ptr = ptr[p0-p:];
+			nr -= (p0-p);
+			p = p0;
+		}
+		trim = 0;
+		if(p+nr > p1){	# end of region: trim box
+			nr -= (p+nr)-p1;
+			trim = 1;
+		}
+		if(b.nrune<0 || nr==b.nrune)
+			w = b.wid;
+		else
+			w = strwidth(f.font, ptr[0:nr]);
+		x = pt.x+w;
+		if(x > f.r.max.x)
+			x = f.r.max.x;
+		draw(f.b, (pt, (x, pt.y+f.font.height)), back, nil, pt);
+		if(b.nrune >= 0)
+			graph->stringx(f.b, pt, f.font, ptr[0:nr], text);
+		pt.x += w;
+		p += nr;
+	}
+	# if this is end of last plain text box on wrapped line, fill to end of line
+	if(p1>p0 &&  nb>0 && nb<f.nbox && f.box[nb-1].nrune>0 && !trim){
+		qt = pt;
+		pt = xfrcklinewrap(f, pt, f.box[nb]);
+		if(pt.y > qt.y)
+			draw(f.b, (qt, (f.r.max.x, pt.y)), back, nil, qt);
+	}
+}
+
+frtick(f : ref Frame, pt : Point, ticked : int)
+{
+	r : Rect;
+
+	if(f.ticked==ticked || f.tick==nil || !pt.in(f.r))
+		return;
+	pt.x--;	# looks best just left of where requested
+	r = (pt, (pt.x+FRTICKW, pt.y+f.font.height));
+	if(ticked){
+		draw(f.tickback, f.tickback.r, f.b, nil, pt);
+		draw(f.b, r, f.tick, nil, (0, 0));
+	}else
+		draw(f.b, r, f.tickback, nil, (0, 0));
+	f.ticked = ticked;
+}
+
+xfrdraw(f : ref Frame, pt : Point) : Point
+{
+	nb, n : int;
+
+	for(nb=0; nb < f.nbox; nb++){
+		b := f.box[nb];
+		pt = xfrcklinewrap0(f, pt, b);
+		if(pt.y == f.r.max.y){
+			f.nchars -= xfrstrlen(f, nb);
+			xfrdelbox(f, nb, f.nbox-1);
+			break;
+		}
+		if(b.nrune > 0){
+			n = xfrcanfit(f, pt, b);
+			if(n == 0)
+				berror("draw: xfrcanfit==0");
+			if(n != b.nrune){
+				xfrsplitbox(f, nb, n);
+				b = f.box[nb];
+			}
+			pt.x += b.wid;
+		}else{
+			if(b.bc == '\n') {
+				pt.x = f.r.min.x;
+				pt.y += f.font.height;
+			}
+			else
+				pt.x += xfrnewwid(f, pt, b);
+		}
+	}
+	return pt;
+}
+
+xfrstrlen(f : ref Frame, nb : int) : int
+{
+	n, nrune : int;
+
+	for(n=0; nb<f.nbox; nb++) {
+		nrune = f.box[nb].nrune;
+		if(nrune < 0)
+			nrune = 1;
+		n += nrune;
+	}
+	return n;
+}
+
+frinit(f : ref Frame, r : Rect, ft : ref Font, b : ref Image, cols : array of ref Draw->Image)
+{
+	f.font = ft;
+	f.scroll = 0;
+	f.maxtab = 8*charwidth(ft, '0');
+	f.nbox = 0;
+	f.nalloc = 0;
+	f.nchars = 0;
+	f.nlines = 0;
+	f.p0 = 0;
+	f.p1 = 0;
+	f.box = nil;
+	f.lastlinefull = 0;
+	if(cols != nil)
+		for(i := 0; i < NCOL; i++)
+			f.cols[i] = cols[i];
+	for (i = 0; i < len noglyphs; i++) {
+		if (charwidth(ft, noglyphs[i]) != 0) {
+			f.noglyph = noglyphs[i];
+			break;
+		}
+	}
+	frsetrects(f, r, b);
+	if (f.tick==nil && f.cols[BACK] != nil)
+		frinittick(f);
+}
+
+frinittick(f : ref Frame)
+{
+	ft : ref Font;
+
+	ft = f.font;
+	f.tick = nil;
+	f.tick = graph->balloc(((0, 0), (FRTICKW, ft.height)), (gui->mainwin).chans, Draw->White);
+	if(f.tick == nil)
+		return;
+	f.tickback = graph->balloc(f.tick.r, (gui->mainwin).chans, Draw->White);
+	if(f.tickback == nil){
+		f.tick = nil;
+		return;
+	}
+	# background color
+	draw(f.tick, f.tick.r, f.cols[BACK], nil, (0, 0));
+	# vertical line
+	draw(f.tick, ((FRTICKW/2, 0), (FRTICKW/2+1, ft.height)), black, nil, (0, 0));
+	# box on each end
+	# draw(f->tick, Rect(0, 0, FRTICKW, FRTICKW), f->cols[TEXT], nil, ZP);
+	# draw(f->tick, Rect(0, ft->height-FRTICKW, FRTICKW, ft->height), f->cols[TEXT], nil, ZP);
+}
+
+frsetrects(f : ref Frame, r : Rect, b : ref Image)
+{
+	f.b = b;
+	f.entire = r;
+	f.r = r;
+	f.r.max.y -= (r.max.y-r.min.y)%f.font.height;
+	f.maxlines = (r.max.y-r.min.y)/f.font.height;
+}
+
+frclear(f : ref Frame, freeall : int)
+{
+	if(f.nbox)
+		xfrdelbox(f, 0, f.nbox-1);
+	for (i := 0; i < f.nalloc; i++)
+		f.box[i] = nil;
+	if(freeall)
+		f.tick = f.tickback = nil;
+	f.box = nil;
+	f.ticked = 0;
+}
+
+DELTA : con 25;
+TMPSIZE : con 256;
+
+Plist : adt {
+	pt0 : Point;
+	pt1 : Point;
+};
+
+nalloc : int = 0;
+pts : array of Plist;
+
+bxscan(f : ref Frame, rp : string, l : int, ppt : Point) : (Point, Point)
+{
+	w, c, nb, delta, nl, nr : int;
+	sp : int = 0;
+
+	frame.r = f.r;
+	frame.b = f.b;
+	frame.font = f.font;
+	frame.maxtab = f.maxtab;
+	frame.nbox = 0;
+	frame.nchars = 0;
+	for(i := 0; i < NCOL; i++)
+		frame.cols[i] = f.cols[i];
+	frame.noglyph = f.noglyph;
+	delta = DELTA;
+	nl = 0;
+	for(nb=0; sp<l && nl <= f.maxlines; nb++){
+		if(nb == frame.nalloc){
+			xfrgrowbox(frame, delta);
+			if(delta < 10000)
+				delta *= 2;
+		}
+		b := frame.box[nb];
+		c = rp[sp];
+		if(c=='\t' || c=='\n'){
+			b.bc = c;
+			b.wid = 5000;
+			if(c == '\n')
+				b.minwid = 0;
+			else
+				b.minwid = charwidth(frame.font, ' ');
+			b.nrune = -1;
+			if(c=='\n')
+				nl++;
+			frame.nchars++;
+			sp++;
+		}else{
+			nr = 0;
+			w = 0;
+			ssp := sp;
+			nul := 0;
+			while(sp < l){
+				c = rp[sp];
+				if(c=='\t' || c=='\n')
+					break;
+				if(nr+1 >= TMPSIZE)
+					break;
+				if ((cw := charwidth(frame.font, c)) == 0) {	# used to be only for c == 0
+					c = frame.noglyph;
+					cw = charwidth(frame.font, c);
+					nul = 1;
+				}
+				w += cw;
+				sp++;
+				nr++;
+			}
+			b = frame.box[nb];
+			b.ptr = rp[ssp:sp];
+			b.wid = w;
+			b.nrune = nr;
+			frame.nchars += nr;
+			if (nul) {
+				for (i = 0; i < nr; i++)
+					if (charwidth(frame.font, b.ptr[i]) == 0)
+						b.ptr[i] = frame.noglyph;
+			}
+		}
+		frame.nbox++;
+	}
+	ppt = xfrcklinewrap0(f, ppt, frame.box[0]);
+	return (xfrdraw(frame, ppt), ppt);
+}
+
+chopframe(f : ref Frame, pt : Point, p : int, bn : int)
+{
+	nb, nrune : int;
+
+	for(nb = bn; ; nb++){
+		if(nb >= f.nbox)
+			berror("endofframe");
+		b := f.box[nb];
+		pt = xfrcklinewrap(f, pt, b);
+		if(pt.y >= f.r.max.y)
+			break;
+		nrune = b.nrune;
+		if(nrune < 0)
+			nrune = 1;
+		p += nrune;
+		pt = xfradvance(f, pt, b);
+	}
+	f.nchars = p;
+	f.nlines = f.maxlines;
+	if (nb < f.nbox)				# BUG
+		xfrdelbox(f, nb, f.nbox-1);
+}
+
+frinsert(f : ref Frame, rp : string, l : int, p0 : int)
+{
+	pt0, pt1, ppt0, ppt1, pt : Point;
+	s, n, n0, nn0, y : int;
+	r : Rect;
+	npts : int;
+	col : ref Image;
+
+	if(p0 > f.nchars || l == 0 || f.b == nil)
+		return;
+	n0 = xfrfindbox(f, 0, 0, p0);
+	cn0 := p0;
+	nn0 = n0;
+	pt0 = xfrptofcharnb(f, p0, n0);
+	ppt0 = pt0;
+	(pt1, ppt0) = bxscan(f, rp, l, ppt0);
+	ppt1 = pt1;
+	if(n0 < f.nbox){
+		b := f.box[n0];
+		pt0 = xfrcklinewrap(f, pt0, b);	# for frdrawsel()
+		ppt1 = xfrcklinewrap0(f, ppt1, b);
+	}
+	f.modified = 1;
+	#
+	# ppt0 and ppt1 are start and end of insertion as they will appear when
+	# insertion is complete. pt0 is current location of insertion position
+	# (p0); pt1 is terminal point (without line wrap) of insertion.
+	#
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 0);
+	
+	#
+	# Find point where old and new x's line up
+	# Invariants:
+	#	pt0 is where the next box (b, n0) is now
+	#	pt1 is where it will be after then insertion
+	# If pt1 goes off the rectangle, we can toss everything from there on
+	#
+
+	for(npts=0; pt1.x!= pt0.x && pt1.y!=f.r.max.y && n0<f.nbox; npts++){
+		b := f.box[n0];
+		pt0 = xfrcklinewrap(f, pt0, b);
+		pt1 = xfrcklinewrap0(f, pt1, b);
+		if(b.nrune > 0){
+			n = xfrcanfit(f, pt1, b);
+			if(n == 0)
+				berror("xfrcanfit==0");
+			if(n != b.nrune){
+				xfrsplitbox(f, n0, n);
+				b = f.box[n0];
+			}
+		}
+		if(npts == nalloc){
+			opts := pts;
+			pts = array[npts+DELTA] of Plist;
+			pts[0:] = opts[0:npts];
+			for (k := 0; k < DELTA; k++)
+				pts[k+npts].pt0 = pts[k+npts].pt1 = (0, 0);
+			opts = nil;
+			nalloc += DELTA;
+			b = f.box[n0];
+		}
+		pts[npts].pt0 = pt0;
+		pts[npts].pt1 = pt1;
+		# has a text box overflowed off the frame?
+		if(pt1.y == f.r.max.y)
+			break;
+		pt0 = xfradvance(f, pt0, b);
+		pt1.x += xfrnewwid(f, pt1, b);
+		n0++;
+		nrune := b.nrune;
+		if(nrune < 0)
+			nrune = 1;
+		cn0 += nrune;
+	}
+	if(pt1.y > f.r.max.y)
+		berror("frinsert pt1 too far");
+	if(pt1.y==f.r.max.y && n0<f.nbox){
+		f.nchars -= xfrstrlen(f, n0);
+		xfrdelbox(f, n0, f.nbox-1);
+	}
+	if(n0 == f.nbox)
+		f.nlines = (pt1.y-f.r.min.y)/f.font.height+(pt1.x>f.r.min.x);
+	else if(pt1.y!=pt0.y){
+		q0, q1 : int;
+
+		y = f.r.max.y;
+		q0 = pt0.y+f.font.height;
+		q1 = pt1.y+f.font.height;
+		f.nlines += (q1-q0)/f.font.height;
+		if(f.nlines > f.maxlines)
+			chopframe(f, ppt1, p0, nn0);
+		if(pt1.y < y){
+			r = f.r;
+			r.min.y = q1;
+			r.max.y = y;
+			if(q1 < y)
+				draw(f.b, r, f.b, nil, (f.r.min.x, q0));
+			r.min = pt1;
+			r.max.x = pt1.x+(f.r.max.x-pt0.x);
+			r.max.y = q1;
+			draw(f.b, r, f.b, nil, pt0);
+		}
+	}
+	#
+	# Move the old stuff down to make room.  The loop will move the stuff
+	# between the insertion and the point where the x's lined up.
+	# The draws above moved everything down after the point they lined up.
+	#
+	y = 0;
+	if(pt1.y == f.r.max.y)
+		y = pt1.y;
+	for(j := n0-1; --npts >= 0; --j){
+		pt = pts[npts].pt1;
+		b := f.box[j];
+		if(b.nrune > 0){
+			r.min = pt;
+			r.max = r.min;
+			r.max.x += b.wid;
+			r.max.y += f.font.height;
+			draw(f.b, r, f.b, nil, pts[npts].pt0);
+			if(pt.y < y){	# clear bit hanging off right
+				r.min = pt;
+				r.max = pt;
+				r.min.x += b.wid;
+				r.max.x = f.r.max.x;
+				r.max.y += f.font.height;
+				if(f.p0<=cn0 && cn0<f.p1)	# b+1 is inside selection
+					col = f.cols[HIGH];
+				else
+					col = f.cols[BACK];
+				draw(f.b, r, col, nil, r.min);
+			}
+			y = pt.y;
+			cn0 -= b.nrune;
+		}else{
+			r.min = pt;
+			r.max = pt;
+			r.max.x += b.wid;
+			r.max.y += f.font.height;
+			if(r.max.x >= f.r.max.x)
+				r.max.x = f.r.max.x;
+			cn0--;
+			if(f.p0<=cn0 && cn0<f.p1)	# b is inside selection
+				col = f.cols[HIGH];
+			else
+				col = f.cols[BACK];
+			draw(f.b, r, col, nil, r.min);
+			y = 0;
+			if(pt.x == f.r.min.x)
+				y = pt.y;
+		}
+	}
+	# insertion can extend the selection, so the condition here is different 
+	if(f.p0<p0 && p0<=f.p1)
+		col = f.cols[HIGH];
+	else
+		col = f.cols[BACK];
+	frselectpaint(f, ppt0, ppt1, col);
+	xfrredraw(frame, ppt0);
+	xfraddbox(f, nn0, frame.nbox);
+	for(n=0; n<frame.nbox; n++)
+		*f.box[nn0+n] = *frame.box[n];
+	if(nn0>0 && f.box[nn0-1].nrune>=0 && ppt0.x-f.box[nn0-1].wid>=f.r.min.x){
+		--nn0;
+		ppt0.x -= f.box[nn0].wid;
+	}
+	n0 += frame.nbox;
+	s = n0;
+	if(n0 < f.nbox-1)
+		s++;
+	xfrclean(f, ppt0, nn0, s);
+	f.nchars += frame.nchars;
+	if(f.p0 >= p0)
+		f.p0 += frame.nchars;
+	if(f.p0 > f.nchars)
+		f.p0 = f.nchars;
+	if(f.p1 >= p0)
+		f.p1 += frame.nchars;
+	if(f.p1 > f.nchars)
+		f.p1 = f.nchars;
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 1);
+}
+
+xfrptofcharptb(f : ref Frame, p : int, pt : Point, bn : int) : Point
+{
+	s : int;
+	l : int;
+	r : int;
+
+	for( ; bn < f.nbox; bn++){
+		b := f.box[bn];
+		pt = xfrcklinewrap(f, pt, b);
+		l = b.nrune;
+		if(l < 0)
+			l = 1;
+		if(p < l){
+			if(b.nrune > 0)
+				for(s = 0; p > 0; s++){
+					r = b.ptr[s];
+					pt.x += charwidth(f.font, r);
+					if(r==0 || pt.x>f.r.max.x)
+						berror("frptofchar");
+					p--;
+				}
+			break;
+		}
+		p -= l;
+		pt = xfradvance(f, pt, b);
+	}
+	return pt;
+}
+
+frptofchar(f : ref Frame, p : int) : Point
+{
+	return xfrptofcharptb(f, p, f.r.min, 0);
+}
+
+xfrptofcharnb(f : ref Frame, p : int, nb : int) : Point	# doesn't do final xfradvance to next line
+{
+	pt : Point;
+	nbox : int;
+
+	nbox = f.nbox;
+	f.nbox = nb;
+	pt = xfrptofcharptb(f, p, f.r.min, 0);
+	f.nbox = nbox;
+	return pt;
+}
+
+xfrgrid(f : ref Frame, p: Point) : Point
+{
+	p.y -= f.r.min.y;
+	p.y -= p.y%f.font.height;
+	p.y += f.r.min.y;
+	if(p.x > f.r.max.x)
+		p.x = f.r.max.x;
+	return p;
+}
+
+frcharofpt(f : ref Frame, pt : Point) : int
+{
+	qt : Point;
+	bn, nrune : int;
+	s : int;
+	p : int;
+	r : int;
+
+	pt = xfrgrid(f, pt);
+	qt = f.r.min;
+
+	bn=0;
+	for(p=0; bn<f.nbox && qt.y<pt.y; bn++){
+		b := f.box[bn];
+		qt = xfrcklinewrap(f, qt, b);
+		if(qt.y >= pt.y)
+			break;
+		qt = xfradvance(f, qt, b);
+		nrune = b.nrune;
+		if(nrune < 0)
+			nrune = 1;
+		p += nrune;
+	}
+
+	for(; bn<f.nbox && qt.x<=pt.x; bn++){
+		b := f.box[bn];
+		qt = xfrcklinewrap(f, qt, b);
+		if(qt.y > pt.y)
+			break;
+		if(qt.x+b.wid > pt.x){
+			if(b.nrune < 0)
+				qt = xfradvance(f, qt, b);
+			else{
+				s = 0;
+				for(;;){
+					r = b.ptr[s++];
+					qt.x += charwidth(f.font, r);
+					if(qt.x > pt.x)
+						break;
+					p++;
+				}
+			}
+		}else{
+			nrune = b.nrune;
+			if(nrune < 0)
+				nrune = 1;
+			p += nrune;
+			qt = xfradvance(f, qt, b);
+		}
+	}
+	return p;
+}
+
+region(a, b : int) : int
+{
+	if(a < b)
+		return -1;
+	if(a == b)
+		return 0;
+	return 1;
+}
+
+frselect(f : ref Frame, m : ref Pointer)	# when called, button 1 is down
+{
+	p0, p1, q : int;
+	mp, pt0, pt1, qt : Point;
+	b, scrled, reg : int;
+
+	mp = m.xy;
+	b = m.buttons;
+
+	f.modified = 0;
+	frdrawsel(f, frptofchar(f, f.p0), f.p0, f.p1, 0);
+	p0 = p1 = frcharofpt(f, mp);
+	f.p0 = p0;
+	f.p1 = p1;
+	pt0 = frptofchar(f, p0);
+	pt1 = frptofchar(f, p1);
+	frdrawsel(f, pt0, p0, p1, 1);
+	do{
+		scrled = 0;
+		if(f.scroll){
+			if(m.xy.y < f.r.min.y){
+				textm->framescroll(f, -(f.r.min.y-m.xy.y)/f.font.height-1);
+				p0 = f.p1;
+				p1 = f.p0;
+				scrled = 1;
+			}else if(m.xy.y > f.r.max.y){
+				textm->framescroll(f, (m.xy.y-f.r.max.y)/f.font.height+1);
+				p0 = f.p0;
+				p1 = f.p1;
+				scrled = 1;
+			}
+			if(scrled){
+				pt0 = frptofchar(f, p0);
+				pt1 = frptofchar(f, p1);
+				reg = region(p1, p0);
+			}
+		}
+		q = frcharofpt(f, m.xy);
+		if(p1 != q){
+			if(reg != region(q, p0)){	# crossed starting point; reset
+				if(reg > 0)
+					frdrawsel(f, pt0, p0, p1, 0);
+				else if(reg < 0)
+					frdrawsel(f, pt1, p1, p0, 0);
+				p1 = p0;
+				pt1 = pt0;
+				reg = region(q, p0);
+				if(reg == 0)
+					frdrawsel(f, pt0, p0, p1, 1);
+			}
+			qt = frptofchar(f, q);
+			if(reg > 0){
+				if(q > p1)
+					frdrawsel(f, pt1, p1, q, 1);
+				else if(q < p1)
+					frdrawsel(f, qt, q, p1, 0);
+			}else if(reg < 0){
+				if(q > p1)
+					frdrawsel(f, pt1, p1, q, 0);
+				else
+					frdrawsel(f, qt, q, p1, 1);
+			}
+			p1 = q;
+			pt1 = qt;
+		}
+		f.modified = 0;
+		if(p0 < p1) {
+			f.p0 = p0;
+			f.p1 = p1;
+		}
+		else {
+			f.p0 = p1;
+			f.p1 = p0;
+		}
+		if(scrled)
+			textm->framescroll(f, 0);
+		graph->bflush();
+		if(!scrled)
+			acme->frgetmouse();
+	}while(m.buttons == b);
+}
+
+frselectpaint(f : ref Frame, p0 : Point, p1 : Point, col : ref Image)
+{
+	n : int;
+	q0, q1 : Point;
+
+	q0 = p0;
+	q1 = p1;
+	q0.y += f.font.height;
+	q1.y += f.font.height;
+	n = (p1.y-p0.y)/f.font.height;
+	if(f.b == nil)
+		berror("frselectpaint b==0");
+	if(p0.y == f.r.max.y)
+		return;
+	if(n == 0)
+		draw(f.b, (p0, q1), col, nil, (0, 0));
+	else{
+		if(p0.x >= f.r.max.x)
+			p0.x = f.r.max.x-1;
+		draw(f.b, ((p0.x, p0.y), (f.r.max.x, q0.y)), col, nil, (0, 0));
+		if(n > 1)
+			draw(f.b, ((f.r.min.x, q0.y), (f.r.max.x, p1.y)),
+				col, nil, (0, 0));
+		draw(f.b, ((f.r.min.x, p1.y), (q1.x, q1.y)),
+			col, nil, (0, 0));
+	}
+}
+
+xfrcanfit(f : ref Frame, pt : Point, b : ref Frbox) : int
+{
+	left, nr : int;
+	p : int;
+	r : int;
+
+	left = f.r.max.x-pt.x;
+	if(b.nrune < 0)
+		return b.minwid <= left;
+	if(left >= b.wid)
+		return b.nrune;
+	nr = 0;
+	for(p = 0; p < len b.ptr; p++){
+		r = b.ptr[p];
+		left -= charwidth(f.font, r);
+		if(left < 0)
+			return nr;
+		nr++;
+	}
+	berror("xfrcanfit can't");
+	return 0;
+}
+
+xfrcklinewrap(f : ref Frame, p : Point, b : ref Frbox) : Point
+{
+	wid : int;
+
+	if(b.nrune < 0)
+		wid = b.minwid;
+	else
+		wid = b.wid;
+
+	if(wid > f.r.max.x-p.x){
+		p.x = f.r.min.x;
+		p.y += f.font.height;
+	}
+	return p;
+}
+
+xfrcklinewrap0(f : ref Frame, p : Point, b : ref Frbox) : Point
+{
+	if(xfrcanfit(f, p, b) == 0){
+		p.x = f.r.min.x;
+		p.y += f.font.height;
+	}
+	return p;
+}
+
+xfrcklinewrap1(f : ref Frame, p : Point, wid : int) : Point
+{
+	if(wid > f.r.max.x-p.x){
+		p.x = f.r.min.x;
+		p.y += f.font.height;
+	}
+	return p;
+}
+
+xfradvance(f : ref Frame, p : Point, b : ref Frbox) : Point
+{
+	if(b.nrune<0 && b.bc=='\n'){
+		p.x = f.r.min.x;
+		p.y += f.font.height;
+	}else
+		p.x += b.wid;
+	return p;
+}
+
+xfrnewwid(f : ref Frame, pt : Point, b : ref Frbox) : int
+{
+	b.wid = xfrnewwid0(f, pt, b);
+	return b.wid;
+}
+
+xfrnewwid0(f : ref Frame, pt : Point, b : ref Frbox) : int
+{
+	c, x : int;
+
+	c = f.r.max.x;
+	x = pt.x;
+	if(b.nrune >= 0 || b.bc != '\t')
+		return b.wid;
+	if(x+b.minwid > c)
+		x = pt.x = f.r.min.x;
+	x += f.maxtab;
+	x -= (x-f.r.min.x)%f.maxtab;
+	if(x-pt.x<b.minwid || x>c)
+		x = pt.x+b.minwid;
+	return x-pt.x;
+}
+
+xfrclean(f : ref Frame, pt : Point, n0 : int, n1 : int)	# look for mergeable boxes
+{
+	nb, c : int;
+
+	c = f.r.max.x;
+	for(nb=n0; nb<n1-1; nb++){
+		b0 := f.box[nb];
+		b1 := f.box[nb+1];
+		pt = xfrcklinewrap(f, pt, b0);
+		while(b0.nrune>=0 && nb<n1-1 && b1.nrune>=0 && pt.x+b0.wid+b1.wid<c){
+			xfrmergebox(f, nb);
+			n1--;
+			b0 = f.box[nb];
+			b1 = f.box[nb+1];
+		}
+		pt = xfradvance(f, pt, f.box[nb]);
+	}
+	for(; nb<f.nbox; nb++){
+		b := f.box[nb];
+		pt = xfrcklinewrap(f, pt, b);
+		pt = xfradvance(f, pt, f.box[nb]);
+	}
+	f.lastlinefull = 0;
+	if(pt.y >= f.r.max.y)
+		f.lastlinefull = 1;
+}
--- /dev/null
+++ b/appl/acme/frame.m
@@ -1,0 +1,54 @@
+Framem : module {
+	PATH : con "/dis/acme/frame.dis";
+
+	BACK, HIGH, BORD, TEXT, HTEXT, NCOL : con iota;
+
+	FRTICKW : con 3;
+
+	init : fn(mods : ref Dat->Mods);
+
+	newframe : fn() : ref Frame;
+
+	Frbox : adt {
+		wid : int;					# in pixels
+		nrune : int;				# <0 ==> negate and treat as break char
+		ptr : string;
+		bc : int;		# break char
+		minwid : int;
+	};
+
+	Frame : adt {
+		font : ref Draw->Font;		# of chars in the frame
+		b : ref Draw->Image;		# on which frame appears
+		cols : array of ref Draw->Image;	# colours
+		r : Draw->Rect;				# in which text appears
+		entire : Draw->Rect;			# of full frame
+		box : array of ref Frbox;
+		scroll : int;				# call framescroll function
+		p0 : int;
+		p1 : int;					# selection
+		nbox, nalloc : int;
+		maxtab : int;				# max size of tab, in pixels
+		nchars : int;				# runes in frame
+		nlines : int;				# lines with text
+		maxlines : int;				# total # lines in frame
+		lastlinefull : int;				# last line fills frame
+		modified : int;				# changed since frselect()
+		noglyph : int;				# char to use when a char has 0 width glyph
+		tick : ref Draw->Image;		# typing tick
+		tickback : ref Draw->Image;	# saved image under tick
+		ticked : int;				# is tick on screen ?
+	};
+
+	frcharofpt : fn(f : ref Frame, p : Draw->Point) : int;
+	frptofchar : fn(f : ref Frame, c : int) : Draw->Point;
+	frdelete : fn(f : ref Frame, c1 : int, c2 : int) : int;
+	frinsert : fn(f : ref Frame, s : string, l : int, i : int);
+	frselect : fn(f : ref Frame, m : ref Draw->Pointer);
+	frinit : fn(f : ref Frame, r : Draw->Rect, f : ref Draw->Font, b : ref Draw->Image, cols : array of ref Draw->Image);
+	frsetrects : fn(f : ref Frame, r : Draw->Rect, b : ref Draw->Image);
+	frclear : fn(f : ref Frame, x : int);
+	frdrawsel : fn(f : ref Frame, p : Draw->Point, p0 : int, p1 : int, n : int);
+	frdrawsel0 : fn(f : ref Frame, p : Draw->Point, p0 : int, p1 : int, i1 : ref Draw->Image, i2 : ref Draw->Image);
+	frtick : fn(f : ref Frame, p : Draw->Point, n : int);
+};
--- /dev/null
+++ b/appl/acme/fsys.b
@@ -1,0 +1,866 @@
+implement Fsys;
+
+include "common.m";
+
+sys : Sys;
+styx : Styx;
+styxaux : Styxaux;
+acme : Acme;
+dat : Dat;
+utils : Utils;
+look : Look;
+windowm : Windowm;
+xfidm : Xfidm;
+
+QTDIR, QTFILE, QTAPPEND : import Sys;
+DMDIR, DMAPPEND, Qid, ORCLOSE, OTRUNC, OREAD, OWRITE, ORDWR, Dir : import Sys;
+sprint : import sys;
+MAXWELEM, Rerror : import Styx;
+Qdir,Qacme,Qcons,Qconsctl,Qdraw,Qeditout,Qindex,Qlabel,Qnew,QWaddr,QWbody,QWconsctl,QWctl,QWdata,QWeditout,QWevent,QWrdsel,QWwrsel,QWtag,QMAX : import Dat;
+TRUE, FALSE : import Dat;
+cxfidalloc, cerr : import dat;
+Mntdir, Fid, Dirtab, Lock, Ref, Smsg0 : import dat;
+Tmsg, Rmsg : import styx;
+msize, version, fid, uname, aname, newfid, name, mode, offset, count, setmode : import styxaux;
+Xfid : import xfidm;
+row : import dat;
+Column : import Columnm;
+Window : import windowm;
+lookid : import look;
+warning, error : import utils;
+
+init(mods : ref Dat->Mods)
+{
+	messagesize = Styx->MAXRPC;
+
+	sys = mods.sys;
+	styx = mods.styx;
+	styxaux = mods.styxaux;
+	acme = mods.acme;
+	dat = mods.dat;
+	utils = mods.utils;
+	look = mods.look;
+	windowm = mods.windowm;
+	xfidm = mods.xfidm;
+}
+
+sfd, cfd : ref Sys->FD;
+
+Nhash : con 16;
+DEBUG : con 0;
+
+fids := array[Nhash] of ref Fid;
+
+Eperm := "permission denied";
+Eexist := "file does not exist";
+Enotdir := "not a directory";
+
+dirtab := array[10] of {
+	Dirtab ( ".",		QTDIR,		Qdir,			8r500|DMDIR ),
+	Dirtab ( "acme",	QTDIR,		Qacme,		8r500|DMDIR ),
+	Dirtab ( "cons",		QTFILE,		Qcons,		8r600 ),
+	Dirtab ( "consctl",	QTFILE,		Qconsctl,		8r000 ),
+	Dirtab ( "draw",		QTDIR,		Qdraw,		8r000|DMDIR ),
+	Dirtab ( "editout",	QTFILE,		Qeditout,		8r200 ),
+	Dirtab ( "index",	QTFILE,		Qindex,		8r400 ),
+	Dirtab ( "label",		QTFILE,		Qlabel,		8r600 ),
+	Dirtab ( "new",		QTDIR,		Qnew,		8r500|DMDIR ),
+	Dirtab ( nil,		0,			0,			0 ),
+};
+
+dirtabw := array[12] of {
+	Dirtab ( ".",		QTDIR,		Qdir,			8r500|DMDIR ),
+	Dirtab ( "addr",		QTFILE,		QWaddr,		8r600 ),
+	Dirtab ( "body",		QTAPPEND,	QWbody,		8r600|DMAPPEND ),
+	Dirtab ( "ctl",		QTFILE,		QWctl,		8r600 ),
+	Dirtab ( "consctl",	QTFILE,		QWconsctl,	8r200 ),
+	Dirtab ( "data",		QTFILE,		QWdata,		8r600 ),
+	Dirtab ( "editout",	QTFILE,		QWeditout,	8r200 ),
+	Dirtab ( "event",	QTFILE,		QWevent,		8r600 ),
+	Dirtab ( "rdsel",		QTFILE,		QWrdsel,		8r400 ),
+	Dirtab ( "wrsel",	QTFILE,		QWwrsel,		8r200 ),
+	Dirtab ( "tag",		QTAPPEND,	QWtag,		8r600|DMAPPEND ),
+	Dirtab ( nil, 		0,			0,			0 ),
+};
+
+Mnt : adt {
+	qlock : ref Lock;
+	id : int;
+	md : ref Mntdir;
+};
+
+mnt : Mnt;
+user : string;
+clockfd : ref Sys->FD;
+closing := 0;
+
+fsysinit() 
+{
+	p :  array of ref Sys->FD;
+
+	p = array[2] of ref Sys->FD;
+	if(sys->pipe(p) < 0)
+		error("can't create pipe");
+	cfd = p[0];
+	sfd = p[1];
+	clockfd = sys->open("/dev/time", Sys->OREAD);
+	user = utils->getuser();
+	if (user == nil)
+		user = "Wile. E. Coyote";
+	mnt.qlock = Lock.init();
+	mnt.id = 0;
+	spawn fsysproc();
+}
+
+fsyscfd() : int
+{
+	return cfd.fd;
+}
+
+QID(w, q : int) : int
+{
+	return (w<<8)|q;
+}
+
+FILE(q : Qid) : int
+{
+	return int q.path & 16rFF;
+}
+
+WIN(q : Qid) : int
+{
+	return (int q.path>>8) & 16rFFFFFF;
+}
+
+# nullsmsg : Smsg;
+nullsmsg0 : Smsg0;
+
+fsysproc()
+{
+	n, ok : int;
+	x : ref Xfid;
+	f : ref Fid;
+	t : Smsg0;
+
+	acme->fsyspid = sys->pctl(0, nil);
+	x = nil;
+	for(;;){
+		if(x == nil){
+			cxfidalloc <-= nil;
+			x = <-cxfidalloc;
+		}
+		n = sys->read(sfd, x.buf, messagesize);
+		if(n <= 0) {
+			if (closing)
+				break;
+			error("i/o error on server channel");
+		}
+		(ok, x.fcall) = Tmsg.unpack(x.buf[0:n]);
+		if(ok < 0)
+			error("convert error in convM2S");
+		if(DEBUG)
+			utils->debug(sprint("%d:%s\n", x.tid, x.fcall.text()));
+		pick fc := x.fcall {
+			Version =>
+				f = nil;
+			Auth =>
+				f = nil;
+			* =>
+				f = allocfid(fid(x.fcall));
+		}
+		x.f = f;
+		pick fc := x.fcall {
+			Readerror =>	x = fsyserror();
+			Flush =>		x = fsysflush(x);
+			Version =>	x = fsysversion(x);
+			Auth =>		x = fsysauth(x);
+			Attach =>		x = fsysattach(x, f);
+			Walk =>		x = fsyswalk(x, f);
+			Open =>		x = fsysopen(x, f);
+			Create =>		x = fsyscreate(x);
+			Read =>		x = fsysread(x, f);
+			Write =>		x = fsyswrite(x);
+			Clunk =>		x = fsysclunk(x, f);
+			Remove =>	x = fsysremove(x);
+			Stat =>		x = fsysstat(x, f);
+			Wstat =>		x = fsyswstat(x);
+			# Clone =>	x = fsysclone(x, f);
+			* =>
+				x = respond(x, t, "bad fcall type");
+		}
+	}
+}
+
+fsysaddid(dir : string, ndir : int, incl : array of string, nincl : int) : ref Mntdir
+{
+	m : ref Mntdir;
+	id : int;
+
+	mnt.qlock.lock();
+	id = ++mnt.id;
+	m = ref Mntdir;
+	m.id = id;
+	m.dir =  dir;
+	m.refs = 1;	# one for Command, one will be incremented in attach
+	m.ndir = ndir;
+	m.next = mnt.md;
+	m.incl = incl;
+	m.nincl = nincl;
+	mnt.md = m;
+	mnt.qlock.unlock();
+	return m;
+}
+
+fsysdelid(idm : ref Mntdir)
+{
+	m, prev : ref Mntdir;
+	i : int;
+	
+	if(idm == nil)
+		return;
+	mnt.qlock.lock();
+	if(--idm.refs > 0){
+		mnt.qlock.unlock();
+		return;
+	}
+	prev = nil;
+	for(m=mnt.md; m != nil; m=m.next){
+		if(m == idm){
+			if(prev != nil)
+				prev.next = m.next;
+			else
+				mnt.md = m.next;
+			for(i=0; i<m.nincl; i++)
+				m.incl[i] = nil;
+			m.incl = nil;
+			m.dir = nil;
+			m = nil;
+			mnt.qlock.unlock();
+			return;
+		}
+		prev = m;
+	}
+	mnt.qlock.unlock();
+	buf := sys->sprint("fsysdelid: can't find id %d\n", idm.id);
+	cerr <-= buf;
+}
+
+#
+# Called only in exec.l:run(), from a different FD group
+#
+fsysmount(dir : string, ndir : int, incl : array of string, nincl : int) : ref Mntdir
+{
+	m : ref Mntdir;
+
+	# close server side so don't hang if acme is half-exited
+	# sfd = nil;
+	m = fsysaddid(dir, ndir, incl, nincl);
+	buf := sys->sprint("%d", m.id);
+	if(sys->mount(cfd, nil, "/mnt/acme", Sys->MREPL, buf) < 0){
+		fsysdelid(m);
+		return nil;
+	}
+	# cfd = nil;
+	sys->bind("/mnt/acme", "/chan", Sys->MBEFORE);	# was MREPL
+	if(sys->bind("/mnt/acme", "/dev", Sys->MBEFORE) < 0){
+		fsysdelid(m);
+		return nil;
+	}
+	return m;
+}
+
+fsysclose()
+{
+	closing = 1;
+	# sfd = cfd = nil;
+}
+
+respond(x : ref Xfid, t0 : Smsg0, err : string) : ref Xfid
+{
+	t : ref Rmsg;
+
+	# t = nullsmsg;
+	tag := x.fcall.tag;
+	# fid := fid(x.fcall);
+	qid := t0.qid;
+	if(err != nil)
+		t = ref Rmsg.Error(tag, err);
+	else
+	pick fc := x.fcall {
+		Readerror =>	t = ref Rmsg.Error(tag, err);
+		Flush =>		t = ref Rmsg.Flush(tag);
+		Version =>	t = ref Rmsg.Version(tag, t0.msize, t0.version);
+		Auth =>		t = ref Rmsg.Auth(tag, qid);
+		# Clone =>	t = ref Rmsg.Clone(tag, fid);
+		Attach =>		t = ref Rmsg.Attach(tag, qid);
+		Walk =>		t = ref Rmsg.Walk(tag, t0.qids);
+		Open =>		t = ref Rmsg.Open(tag, qid, t0.iounit);
+		Create =>		t = ref Rmsg.Create(tag, qid, 0);
+		Read =>		if(t0.count == len t0.data)
+						t = ref Rmsg.Read(tag, t0.data);
+					else
+						t = ref Rmsg.Read(tag, t0.data[0: t0.count]);
+		Write =>		t = ref Rmsg.Write(tag, t0.count);
+		Clunk =>		t = ref Rmsg.Clunk(tag);
+		Remove =>	t = ref Rmsg.Remove(tag);
+		Stat =>		t = ref Rmsg.Stat(tag, t0.stat);
+		Wstat =>		t = ref Rmsg.Wstat(tag);
+		
+	}
+	# t.qid = t0.qid;
+	# t.count = t0.count;
+	# t.data = t0.data;
+	# t.stat = t0.stat;
+	# t.fid = x.fcall.fid;
+	# t.tag = x.fcall.tag;
+	buf := t.pack();
+	if(buf == nil)
+		error("convert error in convS2M");
+	if(sys->write(sfd, buf, len buf) != len buf)
+		error("write error in respond");
+	buf = nil;
+	if(DEBUG)
+		utils->debug(sprint("%d:r: %s\n", x.tid, t.text()));
+	return x;
+}
+
+# fsysnop(x : ref Xfid) : ref Xfid
+# {
+# 	t : Smsg0;
+# 
+# 	return respond(x, t, nil);
+# }
+
+fsyserror() : ref Xfid
+{
+	error("sys error : Terror");
+	return nil;
+}
+
+fsyssession(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	# BUG: should shut everybody down ??
+	t = nullsmsg0;
+	return respond(x, t, nil);
+}
+
+fsysversion(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	pick m := x.fcall {
+		Version =>
+			(t.msize, t.version) = styx->compatible(m, messagesize, nil);
+			messagesize = t.msize;
+			return respond(x, t, nil);
+	}
+	return respond(x, t, "acme: bad version");
+
+	# ms := msize(x.fcall);
+	# if(ms < 256)
+	# 	return respond(x, t, "version: message size too small");
+	# t.msize = messagesize = ms;
+	# v := version(x.fcall);
+	# if(len v < 6 || v[0: 6] != "9P2000")
+	# 	return respond(x, t, "unrecognized 9P version");
+	# t.version = "9P2000";
+	# return respond(x, t, nil);
+}
+
+fsysauth(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	return respond(x, t, "acme: authentication not required");
+}
+
+fsysflush(x : ref Xfid) : ref Xfid
+{
+	x.c <-= Xfidm->Xflush;
+	return nil;
+}
+
+fsysattach(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+	id : int;
+	m : ref Mntdir;
+
+	if (uname(x.fcall) != user)
+		return respond(x, t, Eperm);
+	f.busy = TRUE;
+	f.open = FALSE;
+	f.qid = (Qid)(big Qdir, 0, QTDIR);
+	f.dir = dirtab;
+	f.nrpart = 0;
+	f.w = nil;
+	t.qid = f.qid;
+	f.mntdir = nil;
+	id = int aname(x.fcall);
+	mnt.qlock.lock();
+	for(m=mnt.md; m != nil; m=m.next)
+		if(m.id == id){
+			f.mntdir = m;
+			m.refs++;
+			break;
+		}
+	if(m == nil)
+		cerr <-= "unknown id in attach";
+	mnt.qlock.unlock();
+	return respond(x, t, nil);
+}
+
+fsyswalk(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+	c, i, j, id : int;
+	path, qtype : int;
+	d, dir : array of Dirtab;
+	w : ref Window;
+	nf : ref Fid;
+
+	if(f.open)
+		return respond(x, t, "walk of open file");
+	if(fid(x.fcall) != newfid(x.fcall)){
+		nf = allocfid(newfid(x.fcall));
+		if(nf.busy)
+			return respond(x, t, "newfid already in use");
+		nf.busy = TRUE;
+		nf.open = FALSE;
+		nf.mntdir = f.mntdir;
+		if(f.mntdir != nil)
+			f.mntdir.refs++;
+		nf.dir = f.dir;
+		nf.qid = f.qid;
+		nf.w = f.w;
+		nf.nrpart = 0;	# not open, so must be zero
+		if(nf.w != nil)
+			nf.w.refx.inc();
+		f = nf;	# walk f
+	}
+
+	qtype = QTFILE;
+	wqids: list of Qid;
+	err := string nil;
+	id = WIN(f.qid);
+	q := f.qid;
+	names := styxaux->names(x.fcall);
+	nwname := len names;
+
+	if(nwname > 0){
+		for(i = 0; i < nwname; i++){
+			if((q.qtype & QTDIR) == 0){
+				err = Enotdir;
+				break;
+			}
+
+			name := names[i];
+			if(name == ".."){
+				path = Qdir;
+				qtype = QTDIR;
+				id = 0;
+				if(w != nil){
+					w.close();
+					w = nil;
+				}
+				if(i == MAXWELEM){
+					err = "name too long";
+					break;
+				}
+				q.qtype = qtype;
+				q.vers = 0;
+				q.path = big QID(id, path);
+				wqids = q :: wqids;
+				continue;
+			}
+
+			# is it a numeric name?
+			regular := 0;
+			for(j=0; j < len name; j++) {
+				c = name[j];
+				if(c<'0' || '9'<c) {
+					regular = 1;
+					break;
+				}
+			}
+
+			if (!regular) {
+				# yes: it's a directory
+				if(w != nil)	# name has form 27/23; get out before losing w
+					break;
+				id = int name;
+				row.qlock.lock();
+				w = lookid(id, FALSE);
+				if(w == nil){
+					row.qlock.unlock();
+					break;
+				}
+				w.refx.inc();
+				path = Qdir;
+				qtype = QTDIR;
+				row.qlock.unlock();
+				dir = dirtabw;
+				if(i == MAXWELEM){
+					err = "name too long";
+					break;
+				}
+				q.qtype = qtype;
+				q.vers = 0;
+				q.path = big QID(id, path);
+				wqids = q :: wqids;
+				continue;
+			}
+			else {
+				# if(FILE(f.qid) == Qacme) 	# empty directory
+				#	break;
+				if(name == "new"){
+					if(w != nil)
+						error("w set in walk to new");
+					cw := chan of ref Window;
+					spawn x.walk(cw);
+					w = <- cw;
+					w.refx.inc();
+					path = QID(w.id, Qdir);
+					qtype = QTDIR;
+					id = w.id;
+					dir = dirtabw;
+					# x.c <-= Xfidm->Xwalk;
+					if(i == MAXWELEM){
+						err = "name too long";
+						break;
+					}
+					q.qtype = qtype;
+					q.vers = 0;
+					q.path = big QID(id, path);
+					wqids = q :: wqids;
+					continue;
+				}
+
+				if(id == 0)
+					d = dirtab;
+				else
+					d = dirtabw;
+				k := 1;	# skip '.'
+				found := 0;
+				for( ; d[k].name != nil; k++){
+					if(name == d[k].name){
+						path = d[k].qid;
+						qtype = d[k].qtype;
+						dir = d[k:];
+						if(i == MAXWELEM){
+							err = "name too long";
+							break;
+						}
+						q.qtype = qtype;
+						q.vers = 0;
+						q.path = big QID(id, path);
+						wqids = q :: wqids;
+						found = 1;
+						break;
+					}
+				}
+				if(found)
+					continue;
+				break;	# file not found
+			}
+		}
+
+		if(i == 0 && err == nil)
+			err = Eexist;
+	}
+
+	nwqid := len wqids;
+	if(nwqid > 0){
+		t.qids = array[nwqid] of Qid;
+		for(i = nwqid-1; i >= 0; i--){
+			t.qids[i] = hd wqids;
+			wqids = tl wqids;
+		}
+	}
+	if(err != nil || nwqid < nwname){
+		if(nf != nil){
+			nf.busy = FALSE;
+			fsysdelid(nf.mntdir);
+		}
+	}
+	else if(nwqid == nwname){
+		if(w != nil){
+			f.w = w;
+			w = nil;
+		}
+		if(dir != nil)
+			f.dir = dir;
+		f.qid = q;
+	}
+
+	if(w != nil)
+		w.close();
+
+	return respond(x, t, err);
+}
+
+fsysopen(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+	m : int;
+
+	# can't truncate anything, so just disregard
+	setmode(x.fcall, mode(x.fcall)&~OTRUNC);
+	# can't execute or remove anything
+	if(mode(x.fcall)&ORCLOSE)
+		return respond(x, t, Eperm);
+	case(mode(x.fcall)){
+	OREAD =>
+		m = 8r400;
+	OWRITE =>
+		m = 8r200;
+	ORDWR =>
+		m = 8r600;
+	* =>
+		return respond(x, t, Eperm);
+	}
+	if(((f.dir[0].perm&~(DMDIR|DMAPPEND))&m) != m)
+		return respond(x, t, Eperm);
+	x.c <-= Xfidm->Xopen;
+	return nil;
+}
+
+fsyscreate(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	return respond(x, t, Eperm);
+}
+
+idcmp(a, b : int) : int
+{
+	return a-b;
+}
+
+qsort(a : array of int, n : int)
+{
+	i, j : int;
+	t : int;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && idcmp(a[i], a[0]) < 0);
+			do
+				j--;
+			while(j > 0 && idcmp(a[j], a[0]) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+fsysread(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+	b : array of byte;
+	i, id, n, o, e, j, k, nids : int;
+	ids : array of int;
+	d : array of Dirtab;
+	dt : Dirtab;
+	c : ref Column;
+	clock : int;
+
+	b = nil;
+	if(f.qid.qtype & QTDIR){
+		# if(int offset(x.fcall) % DIRLEN)
+		#	return respond(x, t, "illegal offset in directory");
+		if(FILE(f.qid) == Qacme){	# empty dir
+			t.data = nil;
+			t.count = 0;
+			respond(x, t, nil);
+			return x;
+		}
+		o = int offset(x.fcall);
+		e = int offset(x.fcall)+count(x.fcall);
+		clock = getclock();
+		b = array[messagesize] of byte;
+		id = WIN(f.qid);
+		n = 0;
+		if(id > 0)
+			d = dirtabw;
+		else
+			d = dirtab;
+		k = 1;	# first entry is '.' 
+		leng := 0;
+		for(i=0; d[k].name!=nil && i<e; i+=leng){
+			bb := styx->packdir(dostat(WIN(x.f.qid), d[k], clock));
+			leng = len bb;
+			for (kk := 0; kk < leng; kk++)
+				b[kk+n] = bb[kk];
+			bb = nil;
+			if(leng <= Styx->BIT16SZ)
+				break;
+			if(i >= o)
+				n += leng;
+			k++;
+		}
+		if(id == 0){
+			row.qlock.lock();
+			nids = 0;
+			ids = nil;
+			for(j=0; j<row.ncol; j++){
+				c = row.col[j];
+				for(k=0; k<c.nw; k++){
+					oids := ids;
+					ids = array[nids+1] of int;
+					ids[0:] = oids[0:nids];
+					oids = nil;
+					ids[nids++] = c.w[k].id;
+				}
+			}
+			row.qlock.unlock();
+			qsort(ids, nids);
+			j = 0;
+			for(; j<nids && i<e; i+=leng){
+				k = ids[j];
+				dt.name = sys->sprint("%d", k);
+				dt.qid = QID(k, 0);
+				dt.qtype = QTDIR;
+				dt.perm = DMDIR|8r700;
+				bb := styx->packdir(dostat(k, dt, clock));
+				leng = len bb;
+				for (kk := 0; kk < leng; kk++)
+					b[kk+n] = bb[kk];
+				bb = nil;
+				if(leng == 0)
+					break;
+				if(i >= o)
+					n += leng;
+				j++;
+			}
+			ids = nil;
+		}
+		t.data = b;
+		t.count = n;
+		respond(x, t, nil);
+		b = nil;
+		return x;
+	}
+	x.c <-= Xfidm->Xread;
+	return nil;
+}
+
+fsyswrite(x : ref Xfid) : ref Xfid
+{
+	x.c <-= Xfidm->Xwrite;
+	return nil;
+}
+
+fsysclunk(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+
+	fsysdelid(f.mntdir);
+	if(f.open){
+		f.busy = FALSE;
+		f.open = FALSE;
+		x.c <-= Xfidm->Xclose;
+		return nil;
+	}
+	if(f.w != nil)
+		f.w.close();
+	f.busy = FALSE;
+	f.open = FALSE;
+	return respond(x, t, nil);
+}
+
+fsysremove(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	return respond(x, t, Eperm);
+}
+
+fsysstat(x : ref Xfid, f : ref Fid) : ref Xfid
+{
+	t : Smsg0;
+
+	t.stat = dostat(WIN(x.f.qid), f.dir[0], getclock());
+	return respond(x, t, nil);
+}
+
+fsyswstat(x : ref Xfid) : ref Xfid
+{
+	t : Smsg0;
+
+	return respond(x, t, Eperm);
+}
+
+allocfid(fid : int) : ref Fid
+{	
+	f, ff : ref Fid;
+	fh : int;
+
+	ff = nil;
+	fh = fid&(Nhash-1);
+	for(f=fids[fh]; f != nil; f=f.next)
+		if(f.fid == fid)
+			return f;
+		else if(ff==nil && f.busy==FALSE)
+			ff = f;
+	if(ff != nil){
+		ff.fid = fid;
+		return ff;
+	}
+	f = ref Fid;
+	f.busy = FALSE;
+	f.rpart = array[Sys->UTFmax] of byte;
+	f.nrpart = 0;
+	f.fid = fid;
+	f.next = fids[fh];
+	fids[fh] = f;
+	return f;
+}
+
+cbuf := array[32] of byte;
+
+getclock() : int
+{
+	sys->seek(clockfd, big 0, 0);
+	n := sys->read(clockfd, cbuf, len cbuf);
+	return int string cbuf[0:n];
+}
+
+dostat(id : int, dir : Dirtab, clock : int) : Sys->Dir
+{
+	d : Dir;
+
+	d.qid.path = big QID(id, dir.qid);
+	d.qid.vers = 0;
+	d.qid.qtype = dir.qtype;
+	d.mode = dir.perm;
+	d.length = big 0;	# would be nice to do better
+	d.name = dir.name;
+	d.uid = user;
+	d.gid = user;
+	d.atime = clock;
+	d.mtime = clock;
+	d.dtype = d.dev = 0;
+	return d;
+	# buf := styx->convD2M(d);
+	# d = nil;
+	# return buf;
+}
--- /dev/null
+++ b/appl/acme/fsys.m
@@ -1,0 +1,18 @@
+Fsys : module {
+	PATH : con "/dis/acme/fsys.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	messagesize: int;
+
+	QID : fn(w, f : int) : int;
+	FILE : fn(q : Sys->Qid) : int;
+	WIN : fn(q : Sys->Qid) : int;
+
+	fsysinit : fn();
+	fsyscfd : fn() : int;
+	fsysmount: fn(dir : string, ndir : int, incl : array of string, nincl : int) : ref Dat->Mntdir;
+	fsysdelid : fn(idm : ref Dat->Mntdir);
+	fsysclose: fn();
+	respond : fn(x : ref Xfidm->Xfid, t : Dat->Smsg0, err : string) : ref Xfidm->Xfid;
+};
--- /dev/null
+++ b/appl/acme/graph.b
@@ -1,0 +1,82 @@
+implement Graph;
+
+include "common.m";
+
+sys : Sys;
+drawm : Draw;
+dat : Dat;
+gui : Gui;
+utils : Utils;
+
+Image, Point, Rect, Font, Display : import drawm;
+black, white, display : import gui;
+error : import utils;
+
+refp : ref Point;
+pixarr : array of byte;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	drawm = mods.draw;
+	dat = mods.dat;
+	gui = mods.gui;
+	utils = mods.utils;
+
+	refp = ref Point;
+	refp.x = refp.y = 0;
+}
+
+charwidth(f : ref Font, c : int) : int
+{
+	s : string = "z";
+
+	s[0] = c;
+	return f.width(s);
+}
+
+strwidth(f : ref Font, s : string) : int
+{
+	return f.width(s);
+}
+
+balloc(r : Rect, c : Draw->Chans, col : int) : ref Image
+{
+	im := display.newimage(r, c, 0, col);
+	if (im == nil)
+		error("failed to get new image");
+	return im;
+}
+
+draw(d : ref Image, r : Rect, s : ref Image, m : ref Image, p : Point)
+{
+	d.draw(r, s, m, p);
+}
+
+stringx(d : ref Image, p : Point, f : ref Font, s : string, c : ref Image)
+{
+	d.text(p, c, (0, 0), f, s);
+}
+
+cursorset(p : Point)
+{
+	gui->cursorset(p);
+}
+
+cursorswitch(c : ref Dat->Cursor)
+{
+	gui->cursorswitch(c);
+}
+
+binit()
+{
+}
+
+bflush()
+{
+}
+
+berror(s : string)
+{
+	error(s);
+}
--- /dev/null
+++ b/appl/acme/graph.m
@@ -1,0 +1,18 @@
+Graph : module {
+	PATH : con "/dis/acme/graph.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	balloc : fn(r : Draw->Rect, c : Draw->Chans, col : int) : ref Draw->Image;
+	draw : fn(d : ref Draw->Image, r : Draw->Rect, s : ref Draw->Image, m : ref Draw->Image, p : Draw->Point);
+	stringx : fn(d : ref Draw->Image, p : Draw->Point, f : ref Draw->Font, s : string, c : ref Draw->Image);
+	cursorset: fn(p : Draw->Point);
+	cursorswitch : fn(c : ref Dat->Cursor);
+	charwidth : fn(f : ref Draw->Font, c : int) : int;
+	strwidth : fn(f : ref Draw->Font, p : string) : int;
+	binit : fn();
+	bflush : fn();
+	berror : fn(s : string);
+
+	font : ref Draw->Font;
+};
--- /dev/null
+++ b/appl/acme/gui.b
@@ -1,0 +1,126 @@
+implement Gui;
+
+include "common.m";
+include "tk.m";
+include "wmclient.m";
+	wmclient: Wmclient;
+
+sys : Sys;
+draw : Draw;
+acme : Acme;
+dat : Dat;
+utils : Utils;
+
+Font, Point, Rect, Image, Context, Screen, Display, Pointer : import draw;
+keyboardpid, mousepid : import acme;
+ckeyboard, cmouse : import dat;
+mousefd: ref Sys->FD;
+error : import utils;
+
+win: ref Wmclient->Window;
+
+r2s(r: Rect): string
+{
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	draw = mods.draw;
+	acme = mods.acme;
+	dat = mods.dat;
+	utils = mods.utils;
+	wmclient = load Wmclient Wmclient->PATH;
+	if(wmclient == nil)
+		error(sys->sprint("cannot load %s: %r", Wmclient->PATH));
+	wmclient->init();
+
+	if(acme->acmectxt == nil)
+		acme->acmectxt = wmclient->makedrawcontext();
+	display = (acme->acmectxt).display;
+	win = wmclient->window(acme->acmectxt, "Acme", Wmclient->Appl);
+	wmclient->win.reshape(((0, 0), (win.displayr.size().div(2))));
+	cmouse = chan of ref Draw->Pointer;
+	ckeyboard = win.ctxt.kbd;
+	wmclient->win.onscreen("place");
+	wmclient->win.startinput("kbd"::"ptr"::nil);
+	mainwin = win.image;
+	
+	yellow = display.color(Draw->Yellow);
+	green = display.color(Draw->Green);
+	red = display.color(Draw->Red);
+	blue = display.color(Draw->Blue);
+	black = display.color(Draw->Black);
+	white = display.color(Draw->White);
+}
+
+spawnprocs()
+{
+	spawn mouseproc();
+	spawn eventproc();
+}
+
+zpointer: Draw->Pointer;
+
+eventproc()
+{
+	for(;;) alt{
+	e := <-win.ctl or
+	e = <-win.ctxt.ctl =>
+		p := ref zpointer;
+		if(e == "exit"){
+			p.buttons = Acme->M_QUIT;
+			cmouse <-= p;
+		}else{
+			wmclient->win.wmctl(e);
+			if(win.image != mainwin){
+				mainwin = win.image;
+				p.buttons = Acme->M_RESIZE;
+				cmouse <-= p;
+			}
+		}
+	}
+}
+
+mouseproc()
+{
+	for(;;){
+		p := <-win.ctxt.ptr;
+		if(wmclient->win.pointer(*p) == 0){
+			p.buttons &= ~Acme->M_DOUBLE;
+			cmouse <-= p;
+		}
+	}
+}
+		
+
+# consctlfd : ref Sys->FD;
+
+cursorset(p: Point)
+{
+	wmclient->win.wmctl("ptr " + string p.x + " " + string p.y);
+}
+
+cursorswitch(cur: ref Dat->Cursor)
+{
+	s: string;
+	if(cur == nil)
+		s = "cursor";
+	else{
+		Hex: con "0123456789abcdef";
+		s = sys->sprint("cursor %d %d %d %d ", cur.hot.x, cur.hot.y, cur.size.x, cur.size.y);
+		buf := cur.bits;
+		for(i := 0; i < len buf; i++){
+			c := int buf[i];
+			s[len s] = Hex[c >> 4];
+			s[len s] = Hex[c & 16rf];
+	 	}
+	}
+	wmclient->win.wmctl(s);
+}
+
+killwins()
+{
+	wmclient->win.wmctl("exit");
+}
--- /dev/null
+++ b/appl/acme/gui.m
@@ -1,0 +1,15 @@
+Gui: module {
+	PATH: con "/dis/acme/gui.dis";
+	WMPATH: con "/dis/acme/guiwm.dis";
+
+	display : ref Draw->Display;
+	mainwin : ref Draw->Image;
+	yellow, green, red, blue, black, white : ref Draw->Image;
+
+	init : fn(mods : ref Dat->Mods);
+	spawnprocs : fn();
+	cursorset : fn(p : Draw->Point);
+	cursorswitch: fn(c : ref Dat->Cursor);
+
+	killwins : fn();
+};
--- /dev/null
+++ b/appl/acme/look.b
@@ -1,0 +1,743 @@
+implement Look;
+
+include "common.m";
+
+sys : Sys;
+draw : Draw;
+utils : Utils;
+dat : Dat;
+graph : Graph;
+acme : Acme;
+framem : Framem;
+regx : Regx;
+bufferm : Bufferm;
+textm : Textm;
+windowm : Windowm;
+columnm : Columnm;
+exec : Exec;
+scrl : Scroll;
+plumbmsg : Plumbmsg;
+
+sprint : import sys;
+Point : import draw;
+warning, isalnum, stralloc, strfree, strchr, tgetc : import utils;
+Range, TRUE, FALSE, XXX, BUFSIZE, Astring : import Dat;
+Expand, seltext, row : import dat;
+cursorset : import graph;
+frptofchar : import framem;
+isaddrc, isregexc, address : import regx;
+Buffer : import bufferm;
+Text : import textm;
+Window : import windowm;
+Column : import columnm;
+Msg : import plumbmsg;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	draw = mods.draw;
+	utils = mods.utils;
+	graph = mods.graph;
+	acme = mods.acme;
+	framem = mods.framem;
+	regx = mods.regx;
+	dat = mods.dat;
+	bufferm = mods.bufferm;
+	textm = mods.textm;
+	windowm = mods.windowm;
+	columnm = mods.columnm;
+	exec = mods.exec;
+	scrl = mods.scroll;
+	plumbmsg = mods.plumbmsg;
+}
+
+nuntitled : int;
+
+look3(t : ref Text, q0 : int, q1 : int, external : int)
+{
+	n, c, f : int;
+	ct : ref Text;
+	e : Expand;
+	r : ref Astring;
+	expanded : int;
+
+	ct = seltext;
+	if(ct == nil)
+		seltext = t;
+	(expanded, e) = expand(t, q0, q1);
+	if(!external && t.w!=nil && t.w.nopen[Dat->QWevent]>byte 0){
+		if(!expanded)
+			return;
+		f = 0;
+		if((e.at!=nil && t.w!=nil) || (e.name!=nil && lookfile(e.name, len e.name)!=nil))
+			f = 1;		# acme can do it without loading a file 
+		if(q0!=e.q0 || q1!=e.q1)
+			f |= 2;	# second (post-expand) message follows 
+		if(e.name != nil)
+			f |= 4;	# it's a file name 
+		c = 'l';
+		if(t.what == Textm->Body)
+			c = 'L';
+		n = q1-q0;
+		if(n <= Dat->EVENTSIZE){
+			r = stralloc(n);
+			t.file.buf.read(q0, r, 0, n);
+			t.w.event(sprint("%c%d %d %d %d %s\n", c, q0, q1, f, n, r.s[0:n]));
+			strfree(r);
+			r = nil;
+		}else
+			t.w.event(sprint("%c%d %d %d 0 \n", c, q0, q1, f));
+		if(q0==e.q0 && q1==e.q1)
+			return;
+		if(e.name != nil){
+			n = len e.name;
+			if(e.a1 > e.a0)
+				n += 1+(e.a1-e.a0);
+			r = stralloc(n);
+			for (i := 0; i < len e.name; i++)
+				r.s[i] = e.name[i];
+			if(e.a1 > e.a0){
+				r.s[len e.name] = ':';
+				e.at.file.buf.read(e.a0, r, len e.name+1, e.a1-e.a0);
+			}
+		}else{
+			n = e.q1 - e.q0;
+			r = stralloc(n);
+			t.file.buf.read(e.q0, r, 0, n);
+		}
+		f &= ~2;
+		if(n <= Dat->EVENTSIZE)
+			t.w.event(sprint("%c%d %d %d %d %s\n", c, e.q0, e.q1, f, n, r.s[0:n]));
+		else
+			t.w.event(sprint("%c%d %d %d 0 \n", c, e.q0, e.q1, f));
+		strfree(r);
+		r = nil;
+		return;
+	}
+	if(0 && dat->plumbed){	# don't do yet : 2 acmes running => only 1 receives msg
+		m := ref Msg;
+		m.src = "acme";
+		m.dst = nil;
+		(dir, nil) := dirname(t, nil, 0);
+		if(dir == ".")	# sigh
+			dir = nil;
+		if(dir == nil)
+			dir = acme->wdir;
+		m.dir = dir;
+		m.kind = "text";
+		m.attr = nil;
+		if(q1 == q0){
+			if(t.q1>t.q0 && t.q0<=q0 && q0<=t.q1){
+				q0 = t.q0;
+				q1 = t.q1;
+			}else{
+				p := q0;
+				while(q0 > 0 && (c = tgetc(t, q0-1)) != ' ' && c != '\t' && c != '\n')
+					q0--;
+				while(q1 < t.file.buf.nc && (c = tgetc(t, q1)) != ' ' && c != '\t' && c != '\n')
+					q1++;
+				if(q1 == q0)
+					return;
+				m.attr = "click=" + string (p-q0);
+			}
+		}
+		r = stralloc(q1-q0);
+		t.file.buf.read(q0, r, 0, q1-q0);
+		m.data = array of byte r.s;
+		strfree(r);
+		if(m.send() >= 0)
+			return;
+		# plumber failed to match : fall through
+	}
+	if(!expanded)
+		return;
+	if(e.name != nil || e.at != nil)
+		(nil, e) = openfile(t, e);
+	else{
+		if(t.w == nil)
+			return;
+		ct = t.w.body;
+		if(t.w != ct.w)
+			ct.w.lock('M');
+		if(t == ct)
+			ct.setselect(e.q1, e.q1);
+		n = e.q1 - e.q0;
+		r = stralloc(n);
+		t.file.buf.read(e.q0, r, 0, n);
+		if(search(ct, r.s, n) && e.jump)
+			cursorset(frptofchar(ct.frame, ct.frame.p0).add((4, ct.frame.font.height-4)));
+		if(t.w != ct.w)
+			ct.w.unlock();
+		strfree(r);
+		r = nil;
+	}
+	e.name = nil;
+	e.bname = nil;
+}
+
+plumblook(m : ref Msg)
+{
+	e : Expand;
+
+	if (len m.data > Dat->PLUMBSIZE) {
+		warning(nil, sys->sprint("plumb message too long : %s\n", string m.data));
+		return;
+	}
+	e.q0 = e.q1 = 0;
+	if (len m.data == 0)
+		return;
+	e.ar = nil;
+	e.name = string m.data;
+	if(e.name[0] != '/' && m.dir != nil)
+		e.name = m.dir + "/" + e.name;
+	(e.name, nil) = cleanname(e.name, len e.name);
+	e.bname = e.name;
+	e.jump = TRUE;
+	e.a0 = e.a1 = 0;
+	(found, addr) := plumbmsg->lookup(plumbmsg->string2attrs(m.attr), "addr");
+	if (found && addr != nil) {
+		e.ar = addr;
+		e.a1 = len addr;
+	}
+	openfile(nil, e);
+	e.at = nil;
+}
+
+plumbshow(m : ref Msg)
+{
+	w := utils->newwindow(nil);
+	(found, name) := plumbmsg->lookup(plumbmsg->string2attrs(m.attr), "filename");
+	if (!found || name == nil) {
+		nuntitled++;
+		name = "Untitled-" + string nuntitled;
+	}
+	if (name[0] != '/' && m.dir != nil)
+		name = m.dir + "/" + name;
+	(name, nil) = cleanname(name, len name);
+	w.setname(name, len name);
+	d := string m.data;
+	w.body.insert(0, d, len d, TRUE, FALSE);
+	w.body.file.mod = FALSE;
+	w.dirty = FALSE;
+	w.settag();
+	scrl->scrdraw(w.body);
+	w.tag.setselect(w.tag.file.buf.nc, w.tag.file.buf.nc);
+}
+
+search(ct : ref Text, r : string, n : int) : int
+{
+	q, nb, maxn : int;
+	around : int;
+	s : ref Astring;
+	b, c : int;
+
+	if(n==0 || n>ct.file.buf.nc)
+		return FALSE;
+	if(2*n > BUFSIZE){
+		warning(nil, "string too long\n");
+		return FALSE;
+	}
+	maxn = utils->max(2*n, BUFSIZE);
+	s = utils->stralloc(BUFSIZE);
+	b = nb = 0;
+	around = 0;
+	q = ct.q1;
+	for(;;){
+		if(q >= ct.file.buf.nc){
+			q = 0;
+			around = 1;
+			nb = 0;
+		}
+		if(nb > 0){
+			for (c = 0; c < nb; c++)
+				if (s.s[b+c] == r[0])
+					break;
+			if(c >= nb){
+				q += nb;
+				nb = 0;
+				if(around && q>=ct.q1)
+					break;
+				continue;
+			}
+			q += c;
+			nb -= c;
+			b += c;
+		}
+		# reload if buffer covers neither string nor rest of file 
+		if(nb<n && nb!=ct.file.buf.nc-q){
+			nb = ct.file.buf.nc-q;
+			if(nb >= maxn)
+				nb = maxn-1;
+			ct.file.buf.read(q, s, 0, nb);
+			b = 0;
+		}
+		if(n <= nb && s.s[b:b+n] == r[0:n]){
+			if(ct.w != nil){
+				ct.show(q, q+n, TRUE);
+				ct.w.settag();
+			}else{
+				ct.q0 = q;
+				ct.q1 = q+n;
+			}
+			seltext = ct;
+			utils->strfree(s);
+			s = nil;
+			return TRUE;
+		}
+		if(around && q>=ct.q1)
+			break;
+		--nb;
+		b++;
+		q++;
+	}
+	utils->strfree(s);
+	s = nil;
+	return FALSE;
+}
+
+isfilec(r : int) : int
+{
+	if(isalnum(r))
+		return TRUE;
+	if(strchr(".-+/:", r) >= 0)
+		return TRUE;
+	return FALSE;
+}
+
+cleanname(b : string, n : int) : (string, int)
+{
+	i, j, found : int;
+
+	b = b[0:n];
+	# compress multiple slashes 
+	for(i=0; i<n-1; i++)
+		if(b[i]=='/' && b[i+1]=='/'){
+			b = b[0:i] + b[i+1:];
+			--n;
+			--i;
+		}
+	#  eliminate ./ 
+	for(i=0; i<n-1; i++)
+		if(b[i]=='.' && b[i+1]=='/' && (i==0 || b[i-1]=='/')){
+			b = b[0:i] + b[i+2:];
+			n -= 2;
+			--i;
+		}
+	# eliminate trailing . 
+	if(n>=2 && b[n-2]=='/' && b[n-1]=='.') {
+		--n;
+		b = b[0:n];
+	}
+	do{
+		# compress xx/.. 
+		found = FALSE;
+		for(i=1; i<=n-3; i++)
+			if(b[i:i+3] == "/.."){
+				if(i==n-3 || b[i+3]=='/'){
+					found = TRUE;
+					break;
+				}
+			}
+		if(found)
+			for(j=i-1; j>=0; --j)
+				if(j==0 || b[j-1]=='/'){
+					i += 3;		# character beyond .. 
+					if(i<n && b[i]=='/')
+						++i;
+					b = b[0:j] + b[i:];
+					n -= (i-j);
+					break;
+				}
+	}while(found);
+	if(n == 0){
+		b = ".";
+		n = 1;
+	}
+	return (b, n);
+}
+
+includefile(dir : string, file : string, nfile : int) : (string, int)
+{
+	m, n : int;
+	a : string;
+
+	if (dir == ".") {
+		m = 0;
+		a = file;
+	}
+	else {
+		m = 1 + len dir;
+		a = dir + "/" + file;
+	}
+	n = utils->access(a);
+	if(n < 0) {
+		a = nil;
+		return (nil, 0);
+	}
+	file = nil;
+	return cleanname(a, m+nfile);
+}
+
+objdir : string;
+
+includename(t : ref Text , r : string, n : int) : (string, int)
+{
+	file : string;
+	i, nfile : int;
+	w : ref Window;
+
+	{
+		w = t.w;
+		if(n==0 || r[0]=='/' || w==nil)
+			raise "e";
+		if(n>2 && r[0]=='.' && r[1]=='/')
+			raise "e";
+		file = nil;
+		nfile = 0;
+		(file, nfile) = includefile(".", r, n);
+		if (file == nil) {
+			(dr, dn) := dirname(t, r, n);
+			(file, nfile) = includefile(".", dr, dn);
+		}
+		if (file == nil) {
+			for(i=0; i<w.nincl && file==nil; i++)
+				(file, nfile) = includefile(w.incl[i], r, n);
+		}
+		if(file == nil)
+			(file, nfile) = includefile("/module", r, n);
+		if(file == nil)
+			(file, nfile) = includefile("/include", r, n);
+		if(file==nil && objdir!=nil)
+			(file, nfile) = includefile(objdir, r, n);
+		if(file == nil)
+			raise "e";
+		return (file, nfile);
+	}
+	exception{
+		* =>
+			return (r, n);
+	}
+	return (nil, 0);
+}
+
+dirname(t : ref Text, r : string, n : int) : (string, int)
+{
+	b : ref Astring;
+	c : int;
+	m, nt : int;
+	slash : int;
+
+	{
+		b = nil;
+		if(t == nil || t.w == nil)
+			raise "e";
+		nt = t.w.tag.file.buf.nc;
+		if(nt == 0)
+			raise "e";
+		if(n>=1 &&  r[0]=='/')
+			raise "e";
+		b = stralloc(nt+n+1);
+		t.w.tag.file.buf.read(0, b, 0, nt);
+		slash = -1;
+		for(m=0; m<nt; m++){
+			c = b.s[m];
+			if(c == '/')
+				slash = m;
+			if(c==' ' || c=='\t')
+				break;
+		}
+		if(slash < 0)
+			raise "e";
+		for (i := 0; i < n; i++)
+			b.s[slash+1+i] = r[i];
+		r = nil;
+		return cleanname(b.s, slash+1+n);
+	}
+	exception{
+		* =>
+			b = nil;
+			if(r != nil)
+				return cleanname(r, n);
+			return (r, n);
+	}
+	return (nil, 0);
+}
+
+expandfile(t : ref Text, q0 : int, q1 : int, e : Expand) : (int, Expand)
+{
+	i, n, nname, colon : int;
+	amin, amax : int;
+	r : ref Astring;
+	c : int;
+	w : ref Window;
+
+	amax = q1;
+	if(q1 == q0){
+		colon = -1;
+		while(q1<t.file.buf.nc && isfilec(c=t.readc(q1))){
+			if(c == ':'){
+				colon = q1;
+				break;
+			}
+			q1++;
+		}
+		while(q0>0 && (isfilec(c=t.readc(q0-1)) || isaddrc(c) || isregexc(c))){
+			q0--;
+			if(colon==-1 && c==':')
+				colon = q0;
+		}
+		#
+		# if it looks like it might begin file: , consume address chars after :
+		# otherwise terminate expansion at :
+		#
+		
+		if(colon>=0 && colon<t.file.buf.nc-1 && isaddrc(t.readc(colon+1))){
+			q1 = colon+1;
+			while(q1<t.file.buf.nc-1 && isaddrc(t.readc(q1)))
+				q1++;
+		}else if(colon >= 0)
+			q1 = colon;
+		if(q1 > q0)
+			if(colon >= 0){	# stop at white space
+				for(amax=colon+1; amax<t.file.buf.nc; amax++)
+					if((c=t.readc(amax))==' ' || c=='\t' || c=='\n')
+						break;
+			}else
+				amax = t.file.buf.nc;
+	}
+	amin = amax;
+	e.q0 = q0;
+	e.q1 = q1;
+	n = q1-q0;
+	if(n == 0)
+		return (FALSE, e);
+	# see if it's a file name 
+	r = stralloc(n);
+	t.file.buf.read(q0, r, 0, n);
+	# first, does it have bad chars? 
+	nname = -1;
+	for(i=0; i<n; i++){
+		c = r.s[i];
+		if(c==':' && nname<0){
+			if(q0+i+1<t.file.buf.nc && (i==n-1 || isaddrc(t.readc(q0+i+1))))
+				amin = q0+i;
+			else {
+				strfree(r);
+				r = nil;
+				return (FALSE, e);
+			}
+			nname = i;
+		}
+	}
+	if(nname == -1)
+		nname = n;
+	for(i=0; i<nname; i++)
+		if(!isfilec(r.s[i])) {
+			strfree(r);
+			r = nil;
+			return (FALSE, e);
+		}
+	#
+	# See if it's a file name in <>, and turn that into an include
+	# file name if so.  Should probably do it for "" too, but that's not
+	# restrictive enough syntax and checking for a #include earlier on the
+	# line would be silly.
+	#
+	 
+	isfile := 0;
+	if(q0>0 && t.readc(q0-1)=='<' && q1<t.file.buf.nc && t.readc(q1)=='>')
+		(r.s, nname) = includename(t, r.s, nname);
+	else if(q0>0 && t.readc(q0-1)=='"' && q1<t.file.buf.nc && t.readc(q1)=='"')
+		(r.s, nname) = includename(t, r.s, nname);
+	else if(amin == q0)
+		isfile = 1;
+	else
+		(r.s, nname) = dirname(t, r.s, nname);
+	if (!isfile) {
+		e.bname = r.s;
+		# if it's already a window name, it's a file 
+		w = lookfile(r.s, nname);
+		# if it's the name of a file, it's a file 
+		if(w == nil && utils->access(e.bname) < 0){
+			e.bname = nil;
+			strfree(r);
+			r = nil;
+			return (FALSE, e);
+		}
+	}
+
+	e.name = r.s[0:nname];
+	e.at = t;
+	e.a0 = amin+1;
+	(nil, e.a1, nil) = address(nil, nil, (Range)(-1,-1), (Range)(0, 0), t, nil, e.a0, amax, FALSE);
+	strfree(r);
+	r = nil;
+	return (TRUE, e);
+}
+
+expand(t : ref Text, q0 : int, q1 : int) : (int, Expand)
+{
+	e : Expand;
+	ok : int;
+
+	e.q0 = e.q1 = e.a0 = e.a1 = 0;
+	e.name = e.bname = nil;
+	e.at = nil;
+	# if in selection, choose selection 
+	e.jump = TRUE;
+	if(q1==q0 && t.q1>t.q0 && t.q0<=q0 && q0<=t.q1){
+		q0 = t.q0;
+		q1 = t.q1;
+		if(t.what == Textm->Tag)
+			e.jump = FALSE;
+	}
+
+	(ok, e) = expandfile(t, q0, q1, e);
+	if (ok)
+		return (TRUE, e);
+
+	if(q0 == q1){
+		while(q1<t.file.buf.nc && isalnum(t.readc(q1)))
+			q1++;
+		while(q0>0 && isalnum(t.readc(q0-1)))
+			q0--;
+	}
+	e.q0 = q0;
+	e.q1 = q1;
+	return (q1 > q0, e);
+}
+
+lookfile(s : string, n : int) : ref Window
+{
+	i, j, k : int;
+	w : ref Window;
+	c : ref Column;
+	t : ref Text;
+
+	# avoid terminal slash on directories 
+	if(n > 1 && s[n-1] == '/')
+		--n;
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c.nw; i++){
+			w = c.w[i];
+			t = w.body;
+			k = len t.file.name;
+			if(k>0 && t.file.name[k-1] == '/')
+				k--;
+			if(t.file.name[0:k] == s[0:n]){
+				w = w.body.file.curtext.w;
+				if(w.col != nil)	# protect against race deleting w
+					return w;
+			}
+		}
+	}
+	return nil;
+}
+
+lookid(id : int, dump : int) : ref Window
+{
+	i, j : int;
+	w : ref Window;
+	c : ref Column;
+
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c.nw; i++){
+			w = c.w[i];
+			if(dump && w.dumpid == id)
+				return w;
+			if(!dump && w.id == id)
+				return w;
+		}
+	}
+	return nil;
+}
+
+openfile(t : ref Text, e : Expand) : (ref Window, Expand)
+{
+	r : Range;
+	w, ow : ref Window;
+	eval, i, n : int;
+
+	if(e.name == nil){
+		w = t.w;
+		if(w == nil)
+			return (nil, e);
+	}else
+		w = lookfile(e.name, len e.name);
+	if(w != nil){
+		t = w.body;
+		if(!t.col.safe && t.frame.maxlines==0) # window is obscured by full-column window
+			t.col.grow(t.col.w[0], 1, 1);
+	}
+	else{
+		ow = nil;
+		if(t != nil)
+			ow = t.w;
+		w = utils->newwindow(t);
+		t = w.body;
+		w.setname(e.name, len e.name);
+		t.loadx(0, e.bname, 1);
+		t.file.mod = FALSE;
+		t.w.dirty = FALSE;
+		t.w.settag();
+		t.w.tag.setselect(t.w.tag.file.buf.nc, t.w.tag.file.buf.nc);
+		if(ow != nil)
+			for(i=ow.nincl; --i>=0; ){
+				n = len ow.incl[i];
+				w.addincl(ow.incl[i], n);	# really do want to copy here
+			}
+	}
+	if(e.a1 == e.a0)
+		eval = FALSE;
+	else
+		(eval, nil, r) = address(nil, t, (Range)(-1, -1), (Range)(t.q0, t.q1), e.at, e.ar, e.a0, e.a1, TRUE);
+		# was (eval, nil, r) = address(nil, t, (Range)(-1, -1), (Range)(t.q0, t.q1), e.at, nil, e.a0, e.a1, TRUE);
+	if(eval == FALSE){
+		r.q0 = t.q0;
+		r.q1 = t.q1;
+	}
+	t.show(r.q0, r.q1, TRUE);
+	t.w.settag();
+	seltext = t;
+	if(e.jump)
+		cursorset(frptofchar(t.frame, t.frame.p0).add((4, t.frame.font.height-4)));
+	return (w, e);
+}
+
+new(et : ref Text, t : ref Text, argt : ref Text, flag1 : int, flag2 : int, arg : string, narg : int)
+{
+	ndone : int;
+	a, f : string;
+	na, nf : int;
+	e : Expand;
+
+	(nil, a, na) = exec->getarg(argt, FALSE, TRUE);
+	if(a != nil){
+		new(et, t, nil, flag1, flag2, a, na);
+		if(narg == 0)
+			return;
+	}
+	# loop condition: *arg is not a blank 
+	for(ndone=0; ; ndone++){
+		(a, na) = utils->findbl(arg, narg);
+		if(a == arg){
+			if(ndone==0 && et.col!=nil)
+				et.col.add(nil, nil, -1).settag();
+			break;
+		}
+		nf = narg-na;
+		f = arg[0:nf];	# want a copy
+		(f, nf) = dirname(et, f, nf);
+		e.q0 = e.q1 = e.a0 = e.a1 = 0;
+		e.at = nil;
+		e.name = f;
+		e.bname = f;
+		e.jump = TRUE;
+		(nil, e) = openfile(et, e);
+		f = nil;
+		e.bname = nil;
+		(arg, narg) = utils->skipbl(a, na);
+	}
+}
--- /dev/null
+++ b/appl/acme/look.m
@@ -1,0 +1,17 @@
+Look : module {
+	PATH : con "/dis/acme/look.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	isfilec: fn(r : int) : int;
+	lookid : fn(n : int, b : int) : ref Windowm->Window;
+	lookfile : fn(s : string, n : int) : ref Windowm->Window;
+	dirname : fn(t : ref Textm->Text, r : string, n : int) : (string, int);
+	cleanname : fn(s : string, n : int) : (string, int);
+	new : fn(et, t, argt : ref Textm->Text, flag1, flag2 : int, arg : string, narg : int);
+	expand : fn(t : ref Textm->Text, q0, q1 : int) : (int, Dat->Expand);
+	search : fn(t : ref Textm->Text, r : string, n : int) : int;
+	look3 : fn(t : ref Textm->Text, q0, q1, external : int);
+	plumblook : fn(m : ref Plumbmsg->Msg);
+	plumbshow : fn(m : ref Plumbmsg->Msg);
+};
--- /dev/null
+++ b/appl/acme/mkfile
@@ -1,0 +1,90 @@
+<../../mkconfig
+
+DIRS=\
+	acme\
+
+TARG=\
+	acme.dis\
+	dat.dis\
+	buff.dis\
+	col.dis\
+	disk.dis\
+	exec.dis\
+	file.dis\
+	fsys.dis\
+	look.dis\
+	regx.dis\
+	row.dis\
+	scrl.dis\
+	text.dis\
+	time.dis\
+	util.dis\
+	wind.dis\
+	graph.dis\
+	xfid.dis\
+	gui.dis\
+	frame.dis\
+	edit.dis\
+	ecmd.dis\
+	elog.dis\
+	styxaux.dis\
+
+ICONS=\
+	abcde.bit\
+
+MODULES=\
+	acme.m\
+	buff.m\
+	col.m\
+	disk.m\
+	exec.m\
+	file.m\
+	fsys.m\
+	look.m\
+	regx.m\
+	row.m\
+	scrl.m\
+	text.m\
+	time.m\
+	util.m\
+	wind.m\
+	xfid.m\
+	common.m\
+	graph.m\
+	gui.m\
+	frame.m\
+	dat.m\
+	edit.m\
+	elog.m\
+	ecmd.m\
+	styxaux.m\
+
+SYSMODULES=\
+	bufio.m\
+	daytime.m\
+	debug.m\
+	draw.m\
+	sh.m\
+	string.m\
+	styx.m\
+	sys.m\
+	tk.m\
+	workdir.m\
+	wmclient.m\
+
+DISBIN=$ROOT/dis/acme
+
+all:V:	acme.dis
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
+
+install:V:	$ROOT/dis/acme.dis
+
+$ROOT/dis/acme.dis:	acme.dis
+	rm -f $target && cp acme.dis $target
+
+acme.dis:	$MODULES $SYS_MODULES
+
+nuke:V:
+	rm -f $ROOT/dis/acme.dis
--- /dev/null
+++ b/appl/acme/regx.b
@@ -1,0 +1,1050 @@
+implement Regx;
+
+include "common.m";
+
+sys : Sys;
+utils : Utils;
+textm : Textm;
+
+FALSE, TRUE, XXX : import Dat;
+NRange : import Dat;
+Range, Rangeset : import Dat;
+error, warning, tgetc, rgetc : import utils;
+Text : import textm;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	utils = mods.utils;
+	textm = mods.textm;
+}
+
+None : con 0;
+Fore : con '+';
+Back : con '-';
+
+Char : con 0;
+Line : con 1;
+
+isaddrc(r : int) : int
+{
+	if (utils->strchr("0123456789+-/$.#", r) >= 0)
+		return TRUE;
+	return FALSE;
+}
+
+#
+# quite hard: could be almost anything but white space, but we are a little conservative,
+# aiming for regular expressions of alphanumerics and no white space
+#
+isregexc(r : int) : int
+{
+	if(r == 0)
+		return FALSE;
+	if(utils->isalnum(r))
+		return TRUE;
+	if(utils->strchr("^+-.*?#,;[]()$", r)>=0)
+		return TRUE;
+	return FALSE;
+}
+
+number(md: ref Dat->Mntdir, t : ref Text, r : Range, line : int, dir : int, size : int) : (int, Range)
+{
+	q0, q1 : int;
+
+	{
+		if(size == Char){
+			if(dir == Fore)
+				line = r.q1+line;	# was t.file.buf.nc+line;
+			else if(dir == Back){
+				if(r.q0==0 && line > 0)
+					r.q0 = t.file.buf.nc;
+				line = r.q0-line;	# was t.file.buf.nc - line;
+			}
+			if(line<0 || line>t.file.buf.nc)
+				raise "e";
+			return (TRUE, (line, line));
+		}
+		(q0, q1) = r;
+		case(dir){
+		None =>
+			q0 = 0;
+			q1 = 0;
+			while(line>0 && q1<t.file.buf.nc)
+				if(t.readc(q1++) == '\n')
+					if(--line > 0)
+						q0 = q1;
+			if(line==1 && t.readc(q1-1)!='\n')	# no newline at end - count it
+				;
+			else if(line > 0)
+				raise "e";
+		Fore =>
+			if(q1 > 0)
+				while(t.readc(q1-1) != '\n')
+					q1++;
+			q0 = q1;
+			while(line>0 && q1<t.file.buf.nc)
+				if(t.readc(q1++) == '\n')
+					if(--line > 0)
+						q0 = q1;
+			if(line > 0)
+				raise "e";
+		Back =>
+			if(q0 < t.file.buf.nc)
+				while(q0>0 && t.readc(q0-1)!='\n')
+					q0--;
+			q1 = q0;
+			while(line>0 && q0>0){
+				if(t.readc(q0-1) == '\n'){
+					if(--line >= 0)
+						q1 = q0;
+				}
+				--q0;
+			}
+			if(line > 0)
+				raise "e";
+			while(q0>0 && t.readc(q0-1)!='\n')
+				--q0;
+		}
+		return (TRUE, (q0, q1));
+	}
+	exception{
+		* =>
+			if(md != nil)
+				warning(nil, "address out of range\n");
+			return (FALSE, r);
+	}
+	return (FALSE, r);
+}
+
+regexp(md: ref Dat->Mntdir, t : ref Text, lim : Range, r : Range, pat : string, dir : int) : (int, Range)
+{
+	found : int;
+	sel : Rangeset;
+	q : int;
+
+	if(pat == nil && rxnull()){
+		warning(md, "no previous regular expression");
+		return (FALSE, r);
+	}
+	if(pat == nil || !rxcompile(pat))
+		return (FALSE, r);
+	if(dir == Back)
+		(found, sel) = rxbexecute(t, r.q0);
+	else{
+		if(lim.q0 < 0)
+			q = Dat->Infinity;
+		else
+			q = lim.q1;
+		(found, sel) = rxexecute(t, nil, r.q1, q);
+	}
+	if(!found && md == nil)
+		warning(nil, "no match for regexp\n");
+	return (found, sel[0]);
+}
+
+xgetc(a0 : ref Text, a1 : string, n : int) : int
+{
+	if (a0 == nil)
+		return rgetc(a1, n);
+	return tgetc(a0, n);
+}
+
+address(md: ref Dat->Mntdir, t : ref Text, lim : Range, ar : Range, a0 : ref Text, a1 : string, q0 : int, q1 : int,  eval : int) : (int, int, Range)
+{
+	dir, size : int;
+	prevc, c, n : int;
+	q : int;
+	pat : string;
+	r, nr : Range;
+
+	r = ar;
+	q = q0;
+	dir = None;
+	size = Line;
+	c = 0;
+	while(q < q1){
+		prevc = c;
+		c = xgetc(a0, a1, q++);
+		case(c){
+		';' =>
+			ar = r;
+			if(prevc == 0)	# lhs defaults to 0
+				r.q0 = 0;
+			if(q>=q1 && t!=nil && t.file!=nil)	# rhs defaults to $
+				r.q1 = t.file.buf.nc;
+			else{
+				(eval, q, nr) = address(md, t, lim, ar, a0, a1, q, q1, eval);
+				r.q1 = nr.q1;
+			}
+			return (eval, q, r);
+		',' =>
+			if(prevc == 0)	# lhs defaults to 0
+				r.q0 = 0;
+			if(q>=q1 && t!=nil && t.file!=nil)	# rhs defaults to $
+				r.q1 = t.file.buf.nc;
+			else{
+				(eval, q, nr) = address(md, t, lim, ar, a0, a1, q, q1, eval);
+				r.q1 = nr.q1;
+			}
+			return (eval, q, r);
+		'+'  or '-' =>
+			if(eval && (prevc=='+' || prevc=='-')){
+				if((nc := xgetc(a0, a1, q)) != '#' && nc != '/' && nc != '?')
+					(eval, r) = number(md, t, r, 1, prevc, Line);	# do previous one
+			}
+			dir = c;
+		'.' or '$' =>
+			if(q != q0+1)
+				return (eval, q-1, r);
+			if(eval)
+				if(c == '.')
+					r = ar;
+				else
+					r = (t.file.buf.nc, t.file.buf.nc);
+			if(q < q1)
+				dir = Fore;
+			else
+				dir = None;
+		'#' =>
+			if(q==q1 || (c=xgetc(a0, a1, q++))<'0' || '9'<c)
+				return (eval, q-1, r);
+			size = Char;
+			n = c -'0';
+			while(q<q1){
+				c = xgetc(a0, a1, q++);
+				if(c<'0' || '9'<c){
+					q--;
+					break;
+				}
+				n = n*10+(c-'0');
+			}
+			if(eval)
+				(eval, r) = number(md, t, r, n, dir, size);
+			dir = None;
+			size = Line;
+		'0' to '9' =>
+			n = c -'0';
+			while(q<q1){
+				c = xgetc(a0, a1, q++);
+				if(c<'0' || '9'<c){
+					q--;
+					break;
+				}
+				n = n*10+(c-'0');
+			}
+			if(eval)
+				(eval, r) = number(md, t, r, n, dir, size);
+			dir = None;
+			size = Line;
+		'/' =>
+			pat = nil;
+			break2 := 0; # Ow !
+			while(q<q1){
+				c = xgetc(a0, a1, q++);
+				case(c){
+				'\n' =>
+					--q;
+					break2 = 1;
+				'\\' =>
+					pat[len pat] = c;
+					if(q == q1)
+						break2 = 1;
+					else
+						c = xgetc(a0, a1, q++);
+				'/' =>
+					break2 = 1;
+				}
+				if (break2)
+					break;
+				pat[len pat] = c;
+			}
+			if(eval)
+				(eval, r) = regexp(md, t, lim, r, pat, dir);
+			pat = nil;
+			dir = None;
+			size = Line;
+		* =>
+			return (eval, q-1, r);
+		}
+	}
+	if(eval && dir != None)
+		(eval, r) = number(md, t, r, 1, dir, Line);	# do previous one
+	return (eval, q, r);
+}
+
+sel : Rangeset = array[NRange] of Range;
+lastregexp : string;
+
+# Machine Information
+ 
+Inst : adt {
+	typex : int;		# < 16r10000 ==> literal, otherwise action 
+	# sid : int;
+	subid : int;
+	class : int;
+	# other : cyclic ref Inst;
+	right : cyclic ref Inst;
+	# left : cyclic ref Inst;
+	next : cyclic ref Inst;
+};
+
+NPROG : con	1024;
+program := array[NPROG] of ref Inst;
+progp : int;
+startinst : ref Inst;		# First inst. of program; might not be program[0] 
+bstartinst : ref Inst;		# same for backwards machine 
+
+Ilist : adt {
+	inst : ref Inst;			# Instruction of the thread 
+	se : Rangeset;
+	startp : int;		# first char of match 
+};
+
+NLIST : con	128;
+
+thl, nl : array of Ilist;			# This list, next list 
+listx := array[2] of array of Ilist;
+sempty : Rangeset = array[NRange] of Range;
+
+#
+# Actions and Tokens
+#
+#	0x100xx are operators, value == precedence
+#	0x200xx are tokens, i.e. operands for operators
+#
+
+OPERATOR : con		16r10000;	# Bitmask of all operators 
+START	  : con		16r10000;	# Start, used for marker on stack 
+RBRA	  : con		16r10001;	# Right bracket, ) 
+LBRA	  : con		16r10002;	# Left bracket, ( 
+OR		  : con		16r10003;	# Alternation, | 
+CAT		  : con		16r10004;	# Concatentation, implicit operator 
+STAR	  : con		16r10005;	# Closure, * 
+PLUS		  : con		16r10006;	# a+ == aa* 
+QUEST	  : con		16r10007;	# a? == a|nothing, i.e. 0 or 1 a's 
+ANY		  : con		16r20000;	# Any character but newline, . 
+NOP		  : con		16r20001;	# No operation, internal use only 
+BOL		  : con		16r20002;	# Beginning of line, ^ 
+EOL		  : con		16r20003;	# End of line, $ 
+CCLASS	  : con		16r20004;	# Character class, [] 
+NCCLASS	  : con		16r20005;	# Negated character class, [^] 
+END		  : con		16r20077;	# Terminate: match found 
+
+ISATOR	  : con		16r10000;
+ISAND	  : con		16r20000;
+
+# Parser Information
+ 
+Node : adt {
+	first : ref Inst;
+	last : ref Inst;
+};
+
+NSTACK : con	20;
+andstack := array[NSTACK] of ref Node;
+andp : int;
+atorstack := array[NSTACK] of int;
+atorp : int;
+lastwasand : int;	# Last token was operand 
+cursubid : int;
+subidstack := array[NSTACK] of int;
+subidp : int;
+backwards : int;
+nbra : int;
+exprs : string;
+exprp : int;		# pointer to next character in source expression 
+DCLASS : con	10;	# allocation increment 
+nclass : int;		# number active 
+Nclass : int = 0;		# high water mark 
+class : array of string;
+negateclass : int;
+
+nilnode : Node;
+nilinst : Inst;
+
+rxinit()
+{
+	lastregexp = nil;
+	for (k := 0; k < NPROG; k++)
+		program[k] = ref nilinst;
+	for (k = 0; k < NSTACK; k++)
+		andstack[k] = ref nilnode;
+	for (k = 0; k < 2; k++) {
+		listx[k] = array[NLIST] of Ilist;
+		for (i := 0; i < NLIST; i++) {
+			listx[k][i].inst = nil;
+			listx[k][i].startp = 0;
+			listx[k][i].se = array[NRange] of Range;
+			for (j := 0; j < NRange; j++)
+				listx[k][i].se[j].q0 = listx[k][i].se[j].q1 = 0;
+		}
+	}
+}
+
+regerror(e : string)
+{
+	lastregexp = nil;
+	buf := sys->sprint("regexp: %s\n", e);
+	warning(nil, buf);
+	raise "regerror";
+}
+
+newinst(t : int) : ref Inst
+{
+	if(progp >= NPROG)
+		regerror("expression too long");
+	program[progp].typex = t;
+	program[progp].next = nil;	# next was left
+	program[progp].right = nil;
+	return program[progp++];
+}
+
+realcompile(s : string) : ref Inst
+{
+	token : int;
+
+	{
+		startlex(s);
+		atorp = 0;
+		andp = 0;
+		subidp = 0;
+		cursubid = 0;
+		lastwasand = FALSE;
+		# Start with a low priority operator to prime parser 
+		pushator(START-1);
+		while((token=lex()) != END){
+			if((token&ISATOR) == OPERATOR)
+				operator(token);
+			else
+				operand(token);
+		}
+		# Close with a low priority operator 
+		evaluntil(START);
+		# Force END 
+		operand(END);
+		evaluntil(START);
+		if(nbra)
+			regerror("unmatched `('");
+		--andp;	# points to first and only operand 
+		return andstack[andp].first;
+	}
+	exception{
+		"regerror" =>
+			return nil;
+	}
+	return nil;
+}
+
+rxcompile(r : string) : int
+{
+	oprogp : int;
+
+	if(lastregexp == r)
+		return TRUE;
+	lastregexp = nil;
+	for (i := 0; i < nclass; i++)
+		class[i] = nil;
+	nclass = 0;
+	progp = 0;
+	backwards = FALSE;
+	bstartinst = nil;
+	startinst = realcompile(r);
+	if(startinst == nil)
+		return FALSE;
+	optimize(0);
+	oprogp = progp;
+	backwards = TRUE;
+	bstartinst = realcompile(r);
+	if(bstartinst == nil)
+		return FALSE;
+	optimize(oprogp);
+	lastregexp = r;
+	return TRUE;
+}
+
+operand(t : int)
+{
+	i : ref Inst;
+
+	if(lastwasand)
+		operator(CAT);	# catenate is implicit 
+	i = newinst(t);
+	if(t == CCLASS){
+		if(negateclass)
+			i.typex = NCCLASS;	# UGH 
+		i.class = nclass-1;		# UGH 
+	}
+	pushand(i, i);
+	lastwasand = TRUE;
+}
+
+operator(t : int)
+{
+	if(t==RBRA && --nbra<0)
+		regerror("unmatched `)'");
+	if(t==LBRA){
+		cursubid++;	# silently ignored 
+		nbra++;
+		if(lastwasand)
+			operator(CAT);
+	}else
+		evaluntil(t);
+	if(t!=RBRA)
+		pushator(t);
+	lastwasand = FALSE;
+	if(t==STAR || t==QUEST || t==PLUS || t==RBRA)
+		lastwasand = TRUE;	# these look like operands 
+}
+
+pushand(f : ref Inst, l : ref Inst)
+{
+	if(andp >= NSTACK)
+		error("operand stack overflow");
+	andstack[andp].first = f;
+	andstack[andp].last = l;
+	andp++;
+}
+
+pushator(t : int)
+{
+	if(atorp >= NSTACK)
+		error("operator stack overflow");
+	atorstack[atorp++]=t;
+	if(cursubid >= NRange)
+		subidstack[subidp++]= -1;
+	else
+		subidstack[subidp++]=cursubid;
+}
+
+popand(op : int) : ref Node
+{
+	if(andp <= 0)
+		if(op){
+			buf := sys->sprint("missing operand for %c", op);
+			regerror(buf);
+		}else
+			regerror("malformed regexp");
+	return andstack[--andp];
+}
+
+popator() : int
+{
+	if(atorp <= 0)
+		error("operator stack underflow");
+	--subidp;
+	return atorstack[--atorp];
+}
+
+evaluntil(pri : int)
+{
+	op1, op2 : ref Node;
+	inst1, inst2 : ref Inst;
+
+	while(pri==RBRA || atorstack[atorp-1]>=pri){
+		case(popator()){
+		LBRA =>
+			op1 = popand('(');
+			inst2 = newinst(RBRA);
+			inst2.subid = subidstack[subidp];
+			op1.last.next = inst2;
+			inst1 = newinst(LBRA);
+			inst1.subid = subidstack[subidp];
+			inst1.next = op1.first;
+			pushand(inst1, inst2);
+			return;		# must have been RBRA 
+		OR =>
+			op2 = popand('|');
+			op1 = popand('|');
+			inst2 = newinst(NOP);
+			op2.last.next = inst2;
+			op1.last.next = inst2;
+			inst1 = newinst(OR);
+			inst1.right = op1.first;
+			inst1.next = op2.first;	# next was left
+			pushand(inst1, inst2);
+		CAT =>
+			op2 = popand(0);
+			op1 = popand(0);
+			if(backwards && op2.first.typex!=END)
+				(op1, op2) = (op2, op1);
+			op1.last.next = op2.first;
+			pushand(op1.first, op2.last);
+		STAR =>
+			op2 = popand('*');
+			inst1 = newinst(OR);
+			op2.last.next = inst1;
+			inst1.right = op2.first;
+			pushand(inst1, inst1);
+		PLUS =>
+			op2 = popand('+');
+			inst1 = newinst(OR);
+			op2.last.next = inst1;
+			inst1.right = op2.first;
+			pushand(op2.first, inst1);
+		QUEST =>
+			op2 = popand('?');
+			inst1 = newinst(OR);
+			inst2 = newinst(NOP);
+			inst1.next = inst2;	# next was left
+			inst1.right = op2.first;
+			op2.last.next = inst2;
+			pushand(inst1, inst2);
+		* =>
+			error("unknown regexp operator");
+		}
+	}
+}
+
+optimize(start : int)
+{
+	inst : int;
+	target : ref Inst;
+
+	for(inst=start; program[inst].typex!=END; inst++){
+		target = program[inst].next;
+		while(target.typex == NOP)
+			target = target.next;
+		program[inst].next = target;
+	}
+}
+
+startlex(s : string)
+{
+	exprs = s;
+	exprp = 0;
+	nbra = 0;
+}
+
+lex() : int
+{
+	c : int;
+
+	if (exprp == len exprs)
+		return END;
+	c = exprs[exprp++];
+	case(c){
+	'\\' =>
+		if(exprp < len exprs)
+			if((c= exprs[exprp++])=='n')
+				c='\n';
+	'*' =>
+		c = STAR;
+	'?' =>
+		c = QUEST;
+	'+' =>
+		c = PLUS;
+	 '|' =>
+		c = OR;
+	'.' =>
+		c = ANY;
+	'(' =>
+		c = LBRA;
+	')' =>
+		c = RBRA;
+	'^' =>
+		c = BOL;
+	'$' =>
+		c = EOL;
+	'[' =>
+		c = CCLASS;
+		bldcclass();
+	}
+	return c;
+}
+
+nextrec() : int
+{
+	if(exprp == len exprs || (exprp == len exprs-1 && exprs[exprp]=='\\'))
+		regerror("malformed `[]'");
+	if(exprs[exprp] == '\\'){
+		exprp++;
+		if(exprs[exprp]=='n'){
+			exprp++;
+			return '\n';
+		}
+		return exprs[exprp++] | 16r10000;
+	} 
+	return exprs[exprp++];
+}
+
+bldcclass()
+{
+	c1, c2 : int;
+	classp : string;
+
+	# we have already seen the '[' 
+	if(exprp < len exprs && exprs[exprp] == '^'){
+		classp[len classp] = '\n';	# don't match newline in negate case 
+		negateclass = TRUE;
+		exprp++;
+	}else
+		negateclass = FALSE;
+	while((c1 = nextrec()) != ']'){
+		if(c1 == '-'){
+			classp = nil;
+			regerror("malformed `[]'");
+		}
+		if(exprp < len exprs && exprs[exprp] == '-'){
+			exprp++;	# eat '-' 
+			if((c2 = nextrec()) == ']') {
+				classp = nil;
+				regerror("malformed '[]'");
+			}
+			classp[len classp] = 16rFFFF;
+			classp[len classp] = c1;
+			classp[len classp] = c2;
+		}else
+			classp[len classp] = c1;
+	}
+	if(nclass == Nclass){
+		Nclass += DCLASS;
+		oc := class;
+		class = array[Nclass] of string;
+		if (oc != nil) {
+			class[0:] = oc[0:Nclass-DCLASS];
+			oc = nil;
+		}
+	}
+	class[nclass++] = classp;
+}
+
+classmatch(classno : int, c : int, negate : int) : int
+{
+	p : string;
+
+	p = class[classno];
+	for (i := 0; i < len p; ) {
+		if(p[i] == 16rFFFF){
+			if(p[i+1]<=c && c<=p[i+2])
+				return !negate;
+			i += 3;
+		}else if(p[i++] == c)
+			return !negate;
+	}
+	return negate;
+}
+
+#
+# Note optimization in addinst:
+# 	*l must be pending when addinst called; if *l has been looked
+#		at already, the optimization is a bug.
+#
+addinst(l : array of Ilist, inst : ref Inst, sep : Rangeset)
+{
+	p : int;
+
+	for(p = 0; l[p].inst != nil; p++){
+		if(l[p].inst==inst){
+			if(sep[0].q0 < l[p].se[0].q0)
+				l[p].se[0:] = sep[0:NRange]; # this would be bug 
+			return;	# It's already there 
+		}
+	}
+	l[p].inst = inst;
+	l[p].se[0:]= sep[0:NRange];
+	l[p+1].inst = nil;
+}
+
+rxnull() : int
+{
+	return startinst==nil || bstartinst==nil;
+}
+
+OVERFLOW : con "overflow";
+
+# either t!=nil or r!=nil, and we match the string in the appropriate place
+rxexecute(t : ref Text, r: string, startp : int, eof : int) : (int, Rangeset)
+{
+	flag : int;
+	inst : ref Inst;
+	tlp : int;
+	p : int;
+	nnl, ntl : int;
+	nc, c : int;
+	wrapped : int;
+	startchar : int;
+
+	flag = 0;
+	p = startp;
+	startchar = 0;
+	wrapped = 0;
+	nnl = 0;
+	if(startinst.typex<OPERATOR)
+		startchar = startinst.typex;
+	listx[0][0].inst = listx[1][0].inst = nil;
+	sel[0].q0 = -1;
+	
+	{
+		if(t != nil)
+			nc = t.file.buf.nc;
+		else
+			nc = len r;
+		# Execute machine once for each character 
+		for(;;p++){
+			if(p>=eof || p>=nc){
+				case(wrapped++){
+				0 or 2 =>		# let loop run one more click 
+					;
+				1 =>		# expired; wrap to beginning 
+					if(sel[0].q0>=0 || eof!=Dat->Infinity)
+						return (sel[0].q0>=0, sel);
+					listx[0][0].inst = listx[1][0].inst = nil;
+					p = -1;
+					continue;
+				* =>
+					return (sel[0].q0>=0, sel);
+				}
+				c = 0;
+			}else{
+				if(((wrapped && p>=startp) || sel[0].q0>0) && nnl==0)
+					break;
+				if(t != nil)
+					c = t.readc(p);
+				else
+					c = r[p];
+			}
+			# fast check for first char 
+			if(startchar && nnl==0 && c!=startchar)
+				continue;
+			thl = listx[flag];
+			nl = listx[flag^=1];
+			nl[0].inst = nil;
+			ntl = nnl;
+			nnl = 0;
+			if(sel[0].q0<0 && (!wrapped || p<startp || startp==eof)){
+				# Add first instruction to this list 
+				if(++ntl >= NLIST)
+					raise OVERFLOW;
+				sempty[0].q0 = p;
+				addinst(thl, startinst, sempty);
+			}
+			# Execute machine until this list is empty 
+			tlp = 0;
+			inst = thl[0].inst;
+			while(inst  != nil){	# assignment = 
+				case(inst.typex){
+				LBRA =>
+					if(inst.subid>=0)
+						thl[tlp].se[inst.subid].q0 = p;
+					inst = inst.next;
+					continue;
+				RBRA =>
+					if(inst.subid>=0)
+						thl[tlp].se[inst.subid].q1 = p;
+					inst = inst.next;
+					continue;
+				ANY =>
+					if(c!='\n') {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				BOL =>
+					if(p==0 || (t != nil && t.readc(p-1)=='\n') || (r != nil && r[p-1] == '\n')){
+						inst = inst.next;
+						continue;
+					}
+				EOL =>
+					if(c == '\n') {
+						inst = inst.next;
+						continue;
+					}
+				CCLASS =>
+					if(c>=0 && classmatch(inst.class, c, 0)) {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				NCCLASS =>
+					if(c>=0 && classmatch(inst.class, c, 1)) {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				OR =>
+					# evaluate right choice later 
+					if(++ntl >= NLIST)
+						raise OVERFLOW;
+					addinst(thl[tlp:], inst.right, thl[tlp].se);
+					# efficiency: advance and re-evaluate 
+					inst = inst.next;	# next was left
+					continue;
+				END =>		# Match! 
+					thl[tlp].se[0].q1 = p;
+					newmatch(thl[tlp].se);
+				* =>		# regular character 
+					if(inst.typex==c){
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				}
+				tlp++;
+				inst = thl[tlp].inst;
+			}
+		}
+		return (sel[0].q0>=0, sel);
+	}
+	exception{
+		OVERFLOW =>
+			error("regexp list overflow");
+			sel[0].q0 = -1;
+			return (0, sel);
+	}
+	return (0, sel);
+}
+
+newmatch(sp : Rangeset)
+{
+	if(sel[0].q0<0 || sp[0].q0<sel[0].q0 ||
+	   (sp[0].q0==sel[0].q0 && sp[0].q1>sel[0].q1))
+		sel[0:] = sp[0:NRange];
+}
+
+rxbexecute(t : ref Text, startp : int) : (int, Rangeset)
+{
+	flag : int;
+	inst : ref Inst;
+	tlp : int;
+	p : int;
+	nnl, ntl : int;
+	c : int;
+	wrapped : int;
+	startchar : int;
+
+	flag = 0;
+	nnl = 0;
+	wrapped = 0;
+	p = startp;
+	startchar = 0;
+	if(bstartinst.typex<OPERATOR)
+		startchar = bstartinst.typex;
+	listx[0][0].inst = listx[1][0].inst = nil;
+	sel[0].q0= -1;
+	
+	{
+		# Execute machine once for each character, including terminal NUL 
+		for(;;--p){
+			if(p <= 0){
+				case(wrapped++){
+				0 or 2 =>		# let loop run one more click 
+					;
+				1 =>			# expired; wrap to end 
+					if(sel[0].q0>=0)
+						return (sel[0].q0>=0, sel);
+					listx[0][0].inst = listx[1][0].inst = nil;
+					p = t.file.buf.nc+1;
+					continue;
+				3 or * =>
+					return (sel[0].q0>=0, sel);
+				}
+				c = 0;
+			}else{
+				if(((wrapped && p<=startp) || sel[0].q0>0) && nnl==0)
+					break;
+				c = t.readc(p-1);
+			}
+			# fast check for first char 
+			if(startchar && nnl==0 && c!=startchar)
+				continue;
+			thl = listx[flag];
+			nl = listx[flag^=1];
+			nl[0].inst = nil;
+			ntl = nnl;
+			nnl = 0;
+			if(sel[0].q0<0 && (!wrapped || p>startp)){
+				# Add first instruction to this list 
+				if(++ntl >= NLIST)
+					raise OVERFLOW;
+				# the minus is so the optimizations in addinst work 
+				sempty[0].q0 = -p;
+				addinst(thl, bstartinst, sempty);
+			}
+			# Execute machine until this list is empty 
+			tlp = 0;
+			inst = thl[0].inst;
+			while(inst != nil){	# assignment = 
+				case(inst.typex){
+				LBRA =>
+					if(inst.subid>=0)
+						thl[tlp].se[inst.subid].q0 = p;
+					inst = inst.next;
+					continue;
+				RBRA =>
+					if(inst.subid >= 0)
+						thl[tlp].se[inst.subid].q1 = p;
+					inst = inst.next;
+					continue;
+				ANY =>
+					if(c != '\n') {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				BOL =>
+					if(c=='\n' || p==0){
+						inst = inst.next;
+						continue;
+					}
+				EOL =>
+					if(p<t.file.buf.nc && t.readc(p)=='\n') {
+						inst = inst.next;
+						continue;
+					}
+				CCLASS =>
+					if(c>0 && classmatch(inst.class, c, 0)) {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				NCCLASS =>
+					if(c>0 && classmatch(inst.class, c, 1)) {
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				OR =>
+					# evaluate right choice later 
+					if(++ntl >= NLIST)
+						raise OVERFLOW;
+					addinst(thl[tlp:], inst.right, thl[tlp].se);
+					# efficiency: advance and re-evaluate 
+					inst = inst.next;	# next was left
+					continue;
+				END =>		# Match! 
+					thl[tlp].se[0].q0 = -thl[tlp].se[0].q0; # minus sign 
+					thl[tlp].se[0].q1 = p;
+					bnewmatch(thl[tlp].se);
+				* =>	# regular character 
+					if(inst.typex == c){
+						if(++nnl >= NLIST)
+							raise OVERFLOW;
+						addinst(nl, inst.next, thl[tlp].se);
+					}
+				}
+				tlp++;
+				inst = thl[tlp].inst;
+			}
+		}
+		return (sel[0].q0>=0, sel);
+	}
+	exception{
+		OVERFLOW =>
+			error("regexp list overflow");
+			sel[0].q0 = -1;
+			return (0, sel);
+	}
+	return (0, sel);
+}
+
+bnewmatch(sp : Rangeset)
+{
+        i : int;
+
+        if(sel[0].q0<0 || sp[0].q0>sel[0].q1 || (sp[0].q0==sel[0].q1 && sp[0].q1<sel[0].q0))
+                for(i = 0; i<NRange; i++){       # note the reversal; q0<=q1 
+                        sel[i].q0 = sp[i].q1;
+                        sel[i].q1 = sp[i].q0;
+                }
+}
--- /dev/null
+++ b/appl/acme/regx.m
@@ -1,0 +1,13 @@
+Regx : module {
+	PATH : con "/dis/acme/regx.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	rxinit : fn();
+	rxcompile: fn(r : string) : int;
+	rxexecute: fn(t : ref Textm->Text, r: string, startp : int, eof : int) : (int, Dat->Rangeset);
+	rxbexecute: fn(t : ref Textm->Text, startp : int) : (int, Dat->Rangeset);
+	isaddrc : fn(r : int) : int;
+	isregexc : fn(r : int) : int;
+	address : fn(md: ref Dat->Mntdir, t : ref Textm->Text, lim : Dat->Range, ar : Dat->Range, a0 : ref Textm->Text, a1 : string, q0 : int, q1 : int, eval : int) : (int, int, Dat->Range);
+};
--- /dev/null
+++ b/appl/acme/row.b
@@ -1,0 +1,767 @@
+implement Rowm;
+
+include "common.m";
+
+sys : Sys;
+bufio : Bufio;
+utils : Utils;
+drawm : Draw;
+acme : Acme;
+graph : Graph;
+gui : Gui;
+dat : Dat;
+bufferm : Bufferm;
+textm : Textm;
+filem : Filem;
+windowm : Windowm;
+columnm : Columnm;
+exec : Exec;
+look : Look;
+edit : Edit;
+ecmd : Editcmd;
+
+ALLLOOPER, ALLTOFILE, ALLMATCHFILE, ALLFILECHECK, ALLELOGTERM, ALLEDITINIT, ALLUPDATE: import Edit;
+sprint : import sys;
+FALSE, TRUE, XXX : import Dat;
+Border, BUFSIZE, Astring : import Dat;
+Reffont, reffont, Lock, Ref : import dat;
+row, home, mouse : import dat;
+fontnames : import acme;
+font, draw : import graph;
+Point, Rect, Image : import drawm;
+min, max, abs, error, warning, clearmouse, stralloc, strfree : import utils;
+black, white, mainwin : import gui;
+Buffer : import bufferm;
+Tag, Rowtag, Text : import textm;
+Window : import windowm;
+File : import filem;
+Column : import columnm;
+Iobuf : import bufio;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	bufio = mods.bufio;
+	dat = mods.dat;
+	utils = mods.utils;
+	drawm = mods.draw;
+	acme = mods.acme;
+	graph = mods.graph;
+	gui = mods.gui;
+	bufferm = mods.bufferm;
+	textm = mods.textm;
+	filem = mods.filem;
+	windowm = mods.windowm;
+	columnm = mods.columnm;
+	exec = mods.exec;
+	look = mods.look;
+	edit = mods.edit;
+	ecmd = mods.editcmd;
+}
+
+newrow() : ref Row
+{
+	r := ref Row;
+	r.qlock = Lock.init();
+	r.r = ((0, 0), (0, 0));
+	r.tag = nil;
+	r.col = nil;
+	r.ncol = 0;
+	return r;
+}
+
+Row.init(row : self ref Row, r : Rect)
+{
+	r1 : Rect;
+	t : ref Text;
+	dummy : ref File = nil;
+
+	draw(mainwin, r, white, nil, (0, 0));
+	row.r = r;
+	row.col = nil;
+	row.ncol = 0;
+	r1 = r;
+	r1.max.y = r1.min.y + font.height;
+	row.tag = textm->newtext();
+	t = row.tag;
+	t.init(dummy.addtext(t), r1, Reffont.get(FALSE, FALSE, FALSE, nil), acme->tagcols);
+	t.what = Rowtag;
+	t.row = row;
+	t.w = nil;
+	t.col = nil;
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(mainwin, r1, black, nil, (0, 0));
+	t.insert(0, "Newcol Kill Putall Dump Exit ", 29, TRUE, 0);
+	t.setselect(t.file.buf.nc, t.file.buf.nc);
+}
+
+Row.add(row : self ref Row, c : ref Column, x : int) : ref Column
+{
+	r, r1 : Rect;
+	d : ref Column;
+	i : int;
+
+	d = nil;
+	r = row.r;
+	r.min.y = row.tag.frame.r.max.y+Border;
+	if(x<r.min.x && row.ncol>0){	#steal 40% of last column by default 
+		d = row.col[row.ncol-1];
+		x = d.r.min.x + 3*d.r.dx()/5;
+	}
+	# look for column we'll land on 
+	for(i=0; i<row.ncol; i++){
+		d = row.col[i];
+		if(x < d.r.max.x)
+			break;
+	}
+	if(row.ncol > 0){
+		if(i < row.ncol)
+			i++;	# new column will go after d 
+		r = d.r;
+		if(r.dx() < 100)
+			return nil;
+		draw(mainwin, r, white, nil, (0, 0));
+		r1 = r;
+		r1.max.x = min(x, r.max.x-50);
+		if(r1.dx() < 50)
+			r1.max.x = r1.min.x+50;
+		d.reshape(r1);
+		r1.min.x = r1.max.x;
+		r1.max.x = r1.min.x+Border;
+		draw(mainwin, r1, black, nil, (0, 0));
+		r.min.x = r1.max.x;
+	}
+	if(c == nil){
+		c = ref Column;
+		c.init(r);
+		reffont.r.inc();
+	}else
+		c.reshape(r);
+	c.row = row;
+	c.tag.row = row;
+	orc := row.col;
+	row.col = array[row.ncol+1] of ref Column;
+	row.col[0:] = orc[0:i];
+	row.col[i+1:] = orc[i:row.ncol];
+	orc = nil;
+	row.col[i] = c;
+	row.ncol++;
+	clearmouse();
+	return c;
+}
+
+Row.reshape(row : self ref Row, r : Rect)
+{
+	i, dx, odx : int;
+	r1, r2 : Rect;
+	c : ref Column;
+
+	dx = r.dx();
+	odx = row.r.dx();
+	row.r = r;
+	r1 = r;
+	r1.max.y = r1.min.y + font.height;
+	row.tag.reshape(r1);
+	r1.min.y = r1.max.y;
+	r1.max.y += Border;
+	draw(mainwin, r1, black, nil, (0, 0));
+	r.min.y = r1.max.y;
+	r1 = r;
+	r1.max.x = r1.min.x;
+	for(i=0; i<row.ncol; i++){
+		c = row.col[i];
+		r1.min.x = r1.max.x;
+		if(i == row.ncol-1)
+			r1.max.x = r.max.x;
+		else
+			r1.max.x = r1.min.x+c.r.dx()*dx/odx;
+		r2 = r1;
+		r2.max.x = r2.min.x+Border;
+		draw(mainwin, r2, black, nil, (0, 0));
+		r1.min.x = r2.max.x;
+		c.reshape(r1);
+	}
+}
+
+Row.dragcol(row : self ref Row, c : ref Column)
+{
+	r : Rect;
+	i, b, x : int;
+	p, op : Point;
+	d : ref Column;
+
+	clearmouse();
+	graph->cursorswitch(dat->boxcursor);
+	b = mouse.buttons;
+	op = mouse.xy;
+	while(mouse.buttons == b)
+		acme->frgetmouse();
+	graph->cursorswitch(dat->arrowcursor);
+	if(mouse.buttons){
+		while(mouse.buttons)
+			acme->frgetmouse();
+		return;
+	}
+
+	for(i=0; i<row.ncol; i++)
+		if(row.col[i] == c)
+			break;
+	if (i == row.ncol)
+		error("can't find column");
+
+	if(i == 0)
+		return;
+	p = mouse.xy;
+	if((abs(p.x-op.x)<5 && abs(p.y-op.y)<5))
+		return;
+	if((i>0 && p.x<row.col[i-1].r.min.x) || (i<row.ncol-1 && p.x>c.r.max.x)){
+		# shuffle 
+		x = c.r.min.x;
+		row.close(c, FALSE);
+		if(row.add(c, p.x) == nil)	# whoops! 
+		if(row.add(c, x) == nil)		# WHOOPS! 
+		if(row.add(c, -1)==nil){		# shit! 
+			row.close(c, TRUE);
+			return;
+		}
+		c.mousebut();
+		return;
+	}
+	d = row.col[i-1];
+	if(p.x < d.r.min.x+80+Dat->Scrollwid)
+		p.x = d.r.min.x+80+Dat->Scrollwid;
+	if(p.x > c.r.max.x-80-Dat->Scrollwid)
+		p.x = c.r.max.x-80-Dat->Scrollwid;
+	r = d.r;
+	r.max.x = c.r.max.x;
+	draw(mainwin, r, white, nil, (0, 0));
+	r.max.x = p.x;
+	d.reshape(r);
+	r = c.r;
+	r.min.x = p.x;
+	r.max.x = r.min.x;
+	r.max.x += Border;
+	draw(mainwin, r, black, nil, (0, 0));
+	r.min.x = r.max.x;
+	r.max.x = c.r.max.x;
+	c.reshape(r);
+	c.mousebut();
+}
+
+Row.close(row : self ref Row, c : ref Column, dofree : int)
+{
+	r : Rect;
+	i : int;
+
+	for(i=0; i<row.ncol; i++)
+		if(row.col[i] == c)
+			break;
+	if (i == row.ncol)
+		error("can't find column");
+
+	r = c.r;
+	if(dofree)
+		c.closeall();
+	orc := row.col;
+	row.col = array[row.ncol-1] of ref Column;
+	row.col[0:] = orc[0:i];
+	row.col[i:] = orc[i+1:row.ncol];
+	orc = nil;
+	row.ncol--;
+	if(row.ncol == 0){
+		draw(mainwin, r, white, nil, (0, 0));
+		return;
+	}
+	if(i == row.ncol){		# extend last column right 
+		c = row.col[i-1];
+		r.min.x = c.r.min.x;
+		r.max.x = row.r.max.x;
+	}else{			# extend next window left 
+		c = row.col[i];
+		r.max.x = c.r.max.x;
+	}
+	draw(mainwin, r, white, nil, (0, 0));
+	c.reshape(r);
+}
+
+Row.whichcol(row : self ref Row, p : Point) : ref Column
+{
+	i : int;
+	c : ref Column;
+
+	for(i=0; i<row.ncol; i++){
+		c = row.col[i];
+		if(p.in(c.r))
+			return c;
+	}
+	return nil;
+}
+
+Row.which(row : self ref Row, p : Point) : ref Text
+{
+	c : ref Column;
+
+	if(p.in(row.tag.all))
+		return row.tag;
+	c = row.whichcol(p);
+	if(c != nil)
+		return c.which(p);
+	return nil;
+}
+
+Row.typex(row : self ref Row, r : int, p : Point) : ref Text
+{
+	w : ref Window;
+	t : ref Text;
+
+	clearmouse();
+	row.qlock.lock();
+	if(dat->bartflag)
+		t = dat->barttext;
+	else
+		t = row.which(p);
+	if(t!=nil && !(t.what==Tag && p.in(t.scrollr))){
+		w = t.w;
+		if(w == nil)
+			t.typex(r, 0);
+		else{
+			w.lock('K');
+			w.typex(t, r);
+			w.unlock();
+		}
+	}
+	row.qlock.unlock();
+	return t;
+}
+
+Row.clean(row : self ref Row, exiting : int) : int
+{
+	clean : int;
+	i : int;
+
+	clean = TRUE;
+	for(i=0; i<row.ncol; i++)
+		clean &= row.col[i].clean(exiting);
+	return clean;
+}
+
+Row.dump(row : self ref Row, file : string)
+{
+	i, j, m, n, dumped : int;
+	q0, q1 : int;
+	b : ref Iobuf;
+	buf, fontname, a : string;
+	r : ref Astring;
+	c : ref Column;
+	w, w1 : ref Window;
+	t : ref Text;
+
+	if(row.ncol == 0)
+		return;
+	
+	{
+		if(file == nil){
+			if(home == nil){
+				warning(nil, "can't find file for dump: $home not defined\n");
+				raise "e";
+			}
+			buf = sprint("%s/acme.dump", home);
+			file = buf;
+		}
+		b = bufio->create(file, Bufio->OWRITE, 8r600);
+		if(b == nil){
+			warning(nil, sprint("can't open %s: %r\n", file));
+			raise "e";
+		}
+		r = stralloc(BUFSIZE);
+		b.puts(acme->wdir); b.putc('\n');
+		b.puts(fontnames[0]); b.putc('\n');
+		b.puts(fontnames[1]); b.putc('\n');
+		for(i=0; i<row.ncol; i++){
+			c = row.col[i];
+			b.puts(sprint("%11d", 100*(c.r.min.x-row.r.min.x)/row.r.dx()));
+			if(i == row.ncol-1)
+				b.putc('\n');
+			else
+				b.putc(' ');
+		}
+		for(i=0; i<row.ncol; i++){
+			c = row.col[i];
+			for(j=0; j<c.nw; j++)
+				c.w[j].body.file.dumpid = 0;
+		}
+		for(i=0; i<row.ncol; i++){
+			c = row.col[i];
+			for(j=0; j<c.nw; j++){
+				w = c.w[j];
+				w.commit(w.tag);
+				t = w.body;
+				# windows owned by others get special treatment 
+				if(w.nopen[Dat->QWevent] > byte 0)
+					if(w.dumpstr == nil)
+						continue;
+				# zeroxes of external windows are tossed 
+				if(t.file.ntext > 1)
+					for(n=0; n<t.file.ntext; n++){
+						w1 = t.file.text[n].w;
+						if(w == w1)
+							continue;
+						if(w1.nopen[Dat->QWevent] != byte 0) {
+							j = c.nw;
+							continue;
+						}
+					}
+				fontname = "";
+				if(t.reffont.f != font)
+					fontname = t.reffont.f.name;
+				a = t.file.name;
+				if(t.file.dumpid){
+					dumped = FALSE;
+					b.puts(sprint("x%11d %11d %11d %11d %11d %s\n", i, t.file.dumpid,
+						w.body.q0, w.body.q1,
+						100*(w.r.min.y-c.r.min.y)/c.r.dy(),
+						fontname));
+				}else if(w.dumpstr != nil){
+					dumped = FALSE;
+					b.puts(sprint("e%11d %11d %11d %11d %11d %s\n", i, t.file.dumpid,
+						0, 0,
+						100*(w.r.min.y-c.r.min.y)/c.r.dy(),
+						fontname));
+				}else if(len a == 0){	# don't save unnamed windows 
+					continue;
+				}else if((!w.dirty && utils->access(a)==0) || w.isdir){
+					dumped = FALSE;
+					t.file.dumpid = w.id;
+					b.puts(sprint("f%11d %11d %11d %11d %11d %s\n", i, w.id,
+						w.body.q0, w.body.q1,
+						100*(w.r.min.y-c.r.min.y)/c.r.dy(),
+						fontname));
+				}else{
+					dumped = TRUE;
+					t.file.dumpid = w.id;
+					b.puts(sprint("F%11d %11d %11d %11d %11d %11d %s\n", i, j,
+						w.body.q0, w.body.q1,
+						100*(w.r.min.y-c.r.min.y)/c.r.dy(),
+						w.body.file.buf.nc, fontname));
+				}
+				a = nil;
+				buf = w.ctlprint(0);
+				b.puts(buf);
+				m = min(BUFSIZE, w.tag.file.buf.nc);
+				w.tag.file.buf.read(0, r, 0, m);
+				n = 0;
+				while(n<m && r.s[n]!='\n')
+					n++;
+				r.s[n++] = '\n';
+				b.puts(r.s[0:n]);
+				if(dumped){
+					q0 = 0;
+					q1 = t.file.buf.nc;
+					while(q0 < q1){
+						n = q1 - q0;
+						if(n > Dat->BUFSIZE)
+							n = Dat->BUFSIZE;
+						t.file.buf.read(q0, r, 0, n);
+						b.puts(r.s[0:n]);
+						q0 += n;
+					}
+				}
+				if(w.dumpstr != nil){
+					if(w.dumpdir != nil)
+						b.puts(sprint("%s\n%s\n", w.dumpdir, w.dumpstr));
+					else
+						b.puts(sprint("\n%s\n", w.dumpstr));
+				}
+			}
+		}
+		b.close();
+		b = nil;
+		strfree(r);
+		r = nil;
+	}
+	exception{
+		* =>
+			return;
+	}
+}
+
+rdline(b : ref Iobuf, line : int) : (int, string)
+{
+	l : string;
+
+	l = b.gets('\n');
+	if(l != nil)
+		line++;
+	return (line, l);
+}
+
+Row.loadx(row : self ref Row, file : string, initing : int)
+{
+	i, j, line, percent, y, nr, nfontr, n, ns, ndumped, dumpid, x : int;
+	b, bout : ref Iobuf;
+	fontname : string;
+	l, buf, t : string;
+	rune : int;
+	r, fontr : string;
+	c, c1, c2 : ref Column;
+	q0, q1 : int;
+	r1, r2 : Rect;
+	w : ref Window;
+
+	{
+		if(file == nil){
+			if(home == nil){
+				warning(nil, "can't find file for load: $home not defined\n");
+				raise "e";
+			}
+			buf = sprint("%s/acme.dump", home);
+			file = buf;
+		}
+		b = bufio->open(file, Bufio->OREAD);
+		if(b == nil){
+			warning(nil, sprint("can't open load file %s: %r\n", file));
+			raise "e";
+		}
+		
+		{
+			# current directory 
+			(line, l) = rdline(b, 0);
+			if(l == nil)
+				raise "e";
+			l = l[0:len l - 1];
+			if(sys->chdir(l) < 0){
+				warning(nil, sprint("can't chdir %s\n", l));
+				b.close();
+				return;
+			}
+			# global fonts 
+			for(i=0; i<2; i++){
+				(line, l) = rdline(b, line);
+				if(l == nil)
+					raise "e";
+				l = l[0:len l -1];
+				if(l != nil && l != fontnames[i])
+					Reffont.get(i, TRUE, i==0 && initing, l);
+			}
+			if(initing && row.ncol==0)
+				row.init(mainwin.clipr);
+			(line, l) = rdline(b, line);
+			if(l == nil)
+				raise "e";
+			j = len l/12;
+			if(j<=0 || j>10)
+				raise "e";
+			for(i=0; i<j; i++){
+				percent = int l[12*i:12*i+11];
+				if(percent<0 || percent>=100)
+					raise "e";
+				x = row.r.min.x+percent*row.r.dx()/100;
+				if(i < row.ncol){
+					if(i == 0)
+						continue;
+					c1 = row.col[i-1];
+					c2 = row.col[i];
+					r1 = c1.r;
+					r2 = c2.r;
+					r1.max.x = x;
+					r2.min.x = x+Border;
+					if(r1.dx() < 50 || r2.dx() < 50)
+						continue;
+					draw(mainwin, (r1.min, r2.max), white, nil, (0, 0));
+					c1.reshape(r1);
+					c2.reshape(r2);
+					r2.min.x = x;
+					r2.max.x = x+Border;
+					draw(mainwin, r2, black, nil, (0, 0));
+				}
+				if(i >= row.ncol)
+					row.add(nil, x);
+			}
+			for(;;){
+				(line, l) = rdline(b, line);
+				if(l == nil)
+					break;
+				dumpid = 0;
+				case(l[0]){
+				'e' =>
+					if(len l < 1+5*12+1)
+						raise "e";
+					(line, l) = rdline(b, line);	# ctl line; ignored 
+					if(l == nil)
+						raise "e";
+					(line, l) = rdline(b, line);	# directory 
+					if(l == nil)
+						raise "e";
+					l = l[0:len l -1];
+					if(len l != 0)
+						r = l;
+					else{
+						if(home == nil)
+							r = "./";
+						else
+							r = home+"/";
+					}
+					nr = len r;
+					(line, l) = rdline(b, line);	# command 
+					if(l == nil)
+						raise "e";
+					t = l[0:len l -1];
+					spawn exec->run(nil, t, r, nr, TRUE, nil, nil, FALSE);
+					# r is freed in run() 
+					continue;
+				'f' =>
+					if(len l < 1+5*12+1)
+						raise "e";
+					fontname = l[1+5*12:len l - 1];
+					ndumped = -1;
+				'F' =>
+					if(len l < 1+6*12+1)
+						raise "e";
+					fontname = l[1+6*12:len l - 1];
+					ndumped = int l[1+5*12:1+5*12+11];
+				'x' =>
+					if(len l < 1+5*12+1)
+						raise "e";
+					fontname = l[1+5*12: len l - 1];
+					ndumped = -1;
+					dumpid = int l[1+1*12:1+1*12+11];
+				* =>
+					raise "e";
+				}
+				l = l[0:len l -1];
+				if(len fontname != 0) {
+					fontr = fontname;
+					nfontr = len fontname;
+				}
+				else
+					(fontr, nfontr) = (nil, 0);
+				i = int l[1+0*12:1+0*12+11];
+				j = int l[1+1*12:1+1*12+11];
+				q0 = int l[1+2*12:1+2*12+11];
+				q1 = int l[1+3*12:1+3*12+11];
+				percent = int l[1+4*12:1+4*12+11];
+				if(i<0 || i>10)
+					raise "e";
+				if(i > row.ncol)
+					i = row.ncol;
+				c = row.col[i];
+				y = c.r.min.y+(percent*c.r.dy())/100;
+				if(y<c.r.min.y || y>=c.r.max.y)
+					y = -1;
+				if(dumpid == 0)
+					w = c.add(nil, nil, y);
+				else
+					w = c.add(nil, look->lookid(dumpid, TRUE), y);
+				if(w == nil)
+					continue;
+				w.dumpid = j;
+				(line, l) = rdline(b, line);
+				if(l == nil)
+					raise "e";
+				l = l[0:len l - 1];
+				r = l[5*12:len l];
+				nr = len r;
+				ns = -1;
+				for(n=0; n<nr; n++){
+					if(r[n] == '/')
+						ns = n;
+					if(r[n] == ' ')
+						break;
+				}
+				if(dumpid == 0)
+					w.setname(r, n);
+				for(; n<nr; n++)
+					if(r[n] == '|')
+						break;
+				w.cleartag();
+				w.tag.insert(w.tag.file.buf.nc, r[n+1:len r], nr-(n+1), TRUE, 0);
+				if(ndumped >= 0){
+					# simplest thing is to put it in a file and load that 
+					buf = sprint("/tmp/d%d.%.4sacme", sys->pctl(0, nil), utils->getuser());
+					bout = bufio->create(buf, Bufio->OWRITE, 8r600);
+					if(bout == nil){
+						warning(nil, "can't create temp file: %r\n");
+						b.close();
+						return;
+					}
+					for(n=0; n<ndumped; n++){
+						rune = b.getc();
+						if(rune == '\n')
+							line++;
+						if(rune == Bufio->EOF){
+							bout.close();
+							bout = nil;
+							raise "e";
+						}
+						bout.putc(rune);
+					}
+					bout.close();
+					bout = nil;
+					w.body.loadx(0, buf, 1);
+					w.body.file.mod = TRUE;
+					for(n=0; n<w.body.file.ntext; n++)
+						w.body.file.text[n].w.dirty = TRUE;
+					w.settag();
+					sys->remove(buf);
+					buf = nil;
+				}else if(dumpid==0 && r[ns+1]!='+' && r[ns+1]!='-')
+					exec->get(w.body, nil, nil, FALSE, nil, 0);
+				l = r = nil;
+				if(fontr != nil){
+					exec->fontx(w.body, nil, nil, fontr, nfontr);
+					fontr = nil;
+				}
+				if(q0>w.body.file.buf.nc || q1>w.body.file.buf.nc || q0>q1)
+					q0 = q1 = 0;
+				w.body.show(q0, q1, TRUE);
+				w.maxlines = min(w.body.frame.nlines, max(w.maxlines, w.body.frame.maxlines));
+			}
+			b.close();
+		}
+		exception{
+			* =>
+			 	warning(nil, sprint("bad load file %s:%d\n", file, line));
+				b.close();
+				raise "e";
+		}
+	}
+	exception{
+		* =>
+			return;
+	}
+}
+
+allwindows(o: int, aw: ref  Dat->Allwin)
+{
+	for(i:=0; i<row.ncol; i++){
+		c := row.col[i];
+		for(j:=0; j<c.nw; j++){
+			w := c.w[j];
+			case (o){
+			ALLLOOPER =>
+				pick k := aw{
+					LP => ecmd->alllooper(w, k.lp);
+				}
+			ALLTOFILE =>
+				pick k := aw{
+					FF => ecmd->alltofile(w, k.ff);
+				}
+			ALLMATCHFILE =>
+				pick k := aw{
+					FF => ecmd->allmatchfile(w, k.ff);
+				}
+			ALLFILECHECK =>
+				pick k := aw{
+					FC => ecmd->allfilecheck(w, k.fc);
+				}
+			ALLELOGTERM =>
+				edit->allelogterm(w);
+			ALLEDITINIT =>
+				edit->alleditinit(w);
+			ALLUPDATE =>
+				edit->allupdate(w);
+			}
+		}
+	}
+}
--- /dev/null
+++ b/appl/acme/row.m
@@ -1,0 +1,29 @@
+Rowm : module {
+	PATH : con "/dis/acme/row.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	newrow : fn() : ref Row;
+
+	Row : adt {
+		qlock : ref Dat->Lock;
+		r : Draw->Rect;
+		tag : cyclic ref Textm->Text;
+		col : cyclic array of ref Columnm->Column;
+		ncol : int;
+
+		init : fn(r : self ref Row, re : Draw->Rect);
+		add : fn(r : self ref Row, c : ref Columnm->Column, n : int) : ref Columnm->Column;
+		close : fn(r : self ref Row, c : ref Columnm->Column, n : int);
+		which : fn(r : self ref Row, p : Draw->Point) : ref Textm->Text;
+		whichcol : fn(r : self ref Row, p : Draw->Point) : ref Columnm->Column;
+		reshape : fn(r : self ref Row, re : Draw->Rect);
+		typex : fn(r : self ref Row, ru : int, p : Draw->Point) : ref Textm->Text;
+		dragcol : fn(r : self ref Row, c : ref Columnm->Column);
+		clean : fn(r : self ref Row, exiting : int) : int;
+		dump : fn(r : self ref Row, b : string);
+		loadx : fn(r : self ref Row, b : string, n : int);
+	};
+
+	allwindows: fn(a0: int, aw: ref Dat->Allwin);
+};
--- /dev/null
+++ b/appl/acme/scrl.b
@@ -1,0 +1,187 @@
+implement Scroll;
+
+include "common.m";
+
+sys : Sys;
+drawm : Draw;
+acme : Acme;
+graph : Graph;
+utils : Utils;
+gui : Gui;
+dat : Dat;
+framem : Framem;
+textm : Textm;
+timerm : Timerm;
+
+BORD, BACK : import Framem;
+FALSE, TRUE, XXX, Maxblock : import Dat;
+error, warning : import utils;
+Point, Rect, Image, Display : import drawm;
+draw : import graph;
+black, white, display : import gui;
+mouse, cmouse : import dat;
+Frame : import framem;
+Timer : import Dat;
+Text : import textm;
+frgetmouse : import acme;
+mainwin : import gui;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	drawm = mods.draw;
+	acme = mods.acme;
+	graph = mods.graph;
+	utils = mods.utils;
+	gui = mods.gui;
+	dat = mods.dat;
+	framem = mods.framem;
+	textm = mods.textm;
+	timerm = mods.timerm;
+}
+
+scrpos(r : Rect, p0 : int, p1 : int, tot : int) : Rect
+{
+	h : int;
+	q : Rect;
+
+	q = r;
+	# q = r.inset(1);
+	h = q.max.y-q.min.y;
+	if(tot == 0)
+		return q;
+	if(tot > 1024*1024){
+		tot >>= 10;
+		p0 >>= 10;
+		p1 >>= 10;
+	}
+	if(p0 > 0)
+		q.min.y += h*p0/tot;
+	if(p1 < tot)
+		q.max.y -= h*(tot-p1)/tot;
+	if(q.max.y < q.min.y+2){
+		if(q.min.y+2 <= r.max.y)
+			q.max.y = q.min.y+2;
+		else
+			q.min.y = q.max.y-2;
+	}
+	return q;
+}
+
+scrx : ref Image;
+
+scrresize()
+{
+	scrx = nil;
+	h := 1024;
+	if (display != nil)
+		h = display.image.r.dy();
+	rr := Rect((0, 0), (32, h));
+	scrx = graph->balloc(rr, mainwin.chans, Draw->White);
+	if(scrx == nil)
+		error("scroll balloc");
+}
+
+scrdraw(t : ref Text)
+{
+	r, r1, r2 : Rect;
+
+	if(t.w==nil || t.what!=Textm->Body || t != t.w.body)
+		return;
+	if(scrx == nil)
+		scrresize();
+	r = t.scrollr;
+	b := scrx;
+	r1 = r;
+	# r.min.x += 1;	# border between margin and bar 
+	r1.min.x = 0;
+	r1.max.x = r.dx();
+	r2 = scrpos(r1, t.org, t.org+t.frame.nchars, t.file.buf.nc);
+	if(!r2.eq(t.lastsr)){
+		t.lastsr = r2;
+		draw(b, r1, t.frame.cols[BORD], nil, (0, 0));
+		draw(b, r2, t.frame.cols[BACK], nil, (0, 0));
+		r2.min.x = r2.max.x-1;
+		draw(b, r2, t.frame.cols[BORD], nil, (0, 0));
+		draw(t.frame.b, r, b, nil, (0, r1.min.y));
+		# bflush();
+	}
+}
+
+scrsleep(dt : int)
+{
+	timer : ref Timer;
+
+	timer = timerm->timerstart(dt);
+	graph->bflush();
+	# only run from mouse task, so safe to use cmouse 
+	alt{
+	<-(timer.c) =>
+		timerm->timerstop(timer);
+	*mouse = *<-cmouse =>
+		spawn timerm->timerwaittask(timer);
+	}
+}
+
+scroll(t : ref Text, but : int)
+{
+	p0, oldp0 : int;
+	s : Rect;
+	x, y, my, h, first : int;
+
+	s = t.scrollr.inset(1);
+	h = s.max.y-s.min.y;
+	x = (s.min.x+s.max.x)/2;
+	oldp0 = ~0;
+	first = TRUE;
+	do{
+		graph->bflush();
+		my = mouse.xy.y;
+		if(my < s.min.y)
+			my = s.min.y;
+		if(my >= s.max.y)
+			my = s.max.y;
+		if(but == 2){
+			y = my;
+			if(y > s.max.y-2)
+				y = s.max.y-2;
+			if(t.file.buf.nc > 1024*1024)
+				p0 = ((t.file.buf.nc>>10)*(y-s.min.y)/h)<<10;
+			else
+				p0 = t.file.buf.nc*(y-s.min.y)/h;
+			if(oldp0 != p0)
+				t.setorigin(p0, FALSE);
+			oldp0 = p0;
+			frgetmouse();
+			continue;
+		}
+		if(but == 1) {
+			p0 = t.backnl(t.org, (my-s.min.y)/t.frame.font.height);
+			if(p0 == t.org)
+				p0 = t.backnl(t.org, 1);
+		}
+		else {
+			p0 = t.org+framem->frcharofpt(t.frame, (s.max.x, my));
+			if(p0 == t.org)
+				p0 = t.forwnl(t.org, 1);
+		}
+		if(oldp0 != p0)
+			t.setorigin(p0, TRUE);	
+		oldp0 = p0;
+		# debounce 
+		if(first){
+			graph->bflush();
+			sys->sleep(200);
+			alt {
+			*mouse = *<-cmouse =>
+				;
+			* =>
+				;
+			}
+			first = FALSE;
+		}
+		scrsleep(80);
+	}while(mouse.buttons & (1<<(but-1)));
+	while(mouse.buttons)
+		frgetmouse();
+}
--- /dev/null
+++ b/appl/acme/scrl.m
@@ -1,0 +1,9 @@
+Scroll : module {
+	PATH : con "/dis/acme/scrl.dis";
+
+	init : fn(mods : ref Dat->Mods);
+	scrsleep : fn(n : int);
+	scrdraw : fn(t : ref Textm->Text);
+	scrresize : fn();
+	scroll : fn(t : ref Textm->Text, but : int);
+};
--- /dev/null
+++ b/appl/acme/styxaux.b
@@ -1,0 +1,174 @@
+implement Styxaux;
+
+include "sys.m";
+	sys: Sys;
+include "styx.m";
+include "styxaux.m";
+
+Tmsg : import Styx;
+
+init()
+{
+}
+
+msize(m: ref Tmsg): int
+{
+	pick fc := m {
+		Version =>	return fc.msize;
+	}
+	error("bad styx msize", m);
+	return 0;
+}
+
+version(m: ref Tmsg): string
+{
+	pick fc := m {
+		Version =>	return fc.version;
+	}
+	error("bad styx version", m);
+	return nil;
+}
+
+fid(m: ref Tmsg): int
+{
+	pick fc := m {
+		Readerror =>	return 0;
+		Version =>	return 0;
+		Flush =>		return 0;
+		Walk =>		return fc.fid;
+		Open =>		return fc.fid;
+		Create =>		return fc.fid;
+		Read =>		return fc.fid;
+		Write =>		return fc.fid;
+		Clunk =>		return fc.fid;
+		Remove =>	return fc.fid;
+		Stat =>		return fc.fid;
+		Wstat =>		return fc.fid;
+		Attach =>		return fc.fid;
+	}
+	error("bad styx fid", m);
+	return 0;
+}
+
+uname(m: ref Tmsg): string
+{
+	pick fc := m {
+		Attach =>		return fc.uname;
+	}
+	error("bad styx uname", m);
+	return nil;
+}
+
+aname(m: ref Tmsg): string
+{
+	pick fc := m {
+		Attach =>		return fc.aname;
+	}
+	error("bad styx aname", m);
+	return nil;
+}
+
+newfid(m: ref Tmsg): int
+{
+	pick fc := m {
+		Walk =>		return fc.newfid;
+	}
+	error("bad styx newfd", m);
+	return 0;
+}
+
+name(m: ref Tmsg): string
+{
+	pick fc := m {
+		Create =>		return fc.name;
+	}
+	error("bad styx name", m);
+	return nil;
+}
+
+names(m: ref Tmsg): array of string
+{
+	pick fc := m {
+		Walk =>		return fc.names;
+	}
+	error("bad styx names", m);
+	return nil;
+}
+
+mode(m: ref Tmsg): int
+{
+	pick fc := m {
+		Open =>		return fc.mode;
+	}
+	error("bad styx mode", m);
+	return 0;
+}
+
+setmode(m: ref Tmsg, mode: int)
+{
+	pick fc := m {
+		Open =>		fc.mode = mode;
+		* =>			error("bad styx setmode", m);
+	}
+}
+
+offset(m: ref Tmsg): big
+{
+	pick fc := m {
+		Read =>		return fc.offset;
+		Write =>		return fc.offset;
+	}
+	error("bad styx offset", m);
+	return big 0;
+}
+
+count(m: ref Tmsg): int
+{
+	pick fc := m {
+		Read =>		return fc.count;
+		Write =>		return len fc.data;
+	}
+	error("bad styx count", m);
+	return 0;
+}
+
+setcount(m: ref Tmsg, count: int)
+{
+	pick fc := m {
+		Read =>		fc.count = count;
+		* =>			error("bad styx setcount", m);
+	}
+}
+
+oldtag(m: ref Tmsg): int
+{
+	pick fc := m {
+		Flush =>		return fc.oldtag;
+	}
+	error("bad styx oldtag", m);
+	return 0;
+}
+
+data(m: ref Tmsg): array of byte
+{
+	pick fc := m {
+		Write =>		return fc.data;
+	}
+	error("bad styx data", m);
+	return nil;
+}
+
+setdata(m: ref Tmsg, data: array of byte)
+{
+	pick fc := m {
+		Write =>		fc.data = data;
+		* =>			error("bad styx setdata", m);
+	}
+}
+
+error(s: string, m: ref Tmsg)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	sys->fprint(sys->fildes(2), "%s %d\n", s, tagof m);
+}
--- /dev/null
+++ b/appl/acme/styxaux.m
@@ -1,0 +1,24 @@
+Styxaux : module {
+	PATH : con "/dis/acme/styxaux.dis";
+
+	init : fn();
+
+	msize: fn(m: ref Styx->Tmsg): int;
+	version: fn(m: ref Styx->Tmsg): string;
+	fid: fn(m: ref Styx->Tmsg): int;
+	uname: fn(m: ref Styx->Tmsg): string;
+	aname: fn(m: ref Styx->Tmsg): string;
+	newfid: fn(m: ref Styx->Tmsg): int;
+	name: fn(m: ref Styx->Tmsg): string;
+	names: fn(m: ref Styx->Tmsg): array of string;
+	mode: fn(m: ref Styx->Tmsg): int;
+	offset: fn(m: ref Styx->Tmsg): big;
+	count: fn(m: ref Styx->Tmsg): int;
+	oldtag: fn(m: ref Styx->Tmsg): int;
+	data: fn(m: ref Styx->Tmsg): array of byte;
+
+	setmode: fn(m: ref Styx->Tmsg, mode: int);
+	setcount: fn(m: ref Styx->Tmsg, count: int);
+	setdata: fn(m: ref Styx->Tmsg, data: array of byte);
+
+};
--- /dev/null
+++ b/appl/acme/text.b
@@ -1,0 +1,1455 @@
+implement Textm;
+
+include "common.m";
+include "keyboard.m";
+
+sys : Sys;
+utils : Utils;
+framem : Framem;
+drawm : Draw;
+acme : Acme;
+graph : Graph;
+gui : Gui;
+dat : Dat;
+scrl : Scroll;
+bufferm : Bufferm;
+filem : Filem;
+columnm : Columnm;
+windowm : Windowm;
+exec : Exec;
+
+Dir, sprint : import sys;
+frgetmouse : import acme;
+min, warning, error, stralloc, strfree, isalnum : import utils;
+Frame, frinsert, frdelete, frptofchar, frcharofpt, frselect, frdrawsel, frdrawsel0, frtick : import framem;
+BUFSIZE, Astring, SZINT, TRUE, FALSE, XXX, Reffont, Dirlist,Scrollwid, Scrollgap, seq, mouse : import dat;
+EM_NORMAL, EM_RAW, EM_MASK : import dat;
+ALPHA_LATIN, ALPHA_GREEK, ALPHA_CYRILLIC: import Dat;
+BACK, TEXT, HIGH, HTEXT : import Framem;
+Flushon, Flushoff : import Draw;
+Point, Display, Rect, Image : import drawm;
+charwidth, bflush, draw : import graph;
+black, white, mainwin, display : import gui;
+Buffer : import bufferm;
+File : import filem;
+Column : import columnm;
+Window : import windowm;
+scrdraw : import scrl;
+
+cvlist: adt {
+	ld: int;
+	nm: string;
+	si: string;
+	so: string;
+};
+
+#	"@@",  "'EKSTYZekstyz   ",	"ьЕКСТЫЗекстызъЁё",
+
+latintab := array[] of {
+	cvlist(
+		ALPHA_LATIN,
+		"latin",
+		nil,
+		nil
+	),
+	cvlist(
+		ALPHA_GREEK,
+		"greek",
+		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+		"ΑΒΞΔΕΦΓΘΙΪΚΛΜΝΟΠΨΡΣΤΥΫΩΧΗΖαβξδεφγθιϊκλμνοπψρστυϋωχηζ"
+	),
+	cvlist(
+		ALPHA_CYRILLIC,
+		"cyrillic",
+		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+		"АБЧДЭФГШИЙХЛМНОПЕРЩЦУВЮХЯЖабчдэфгшийхлмноперщцувюхяж"
+	),
+	cvlist(-1, nil, nil, nil)
+};
+
+alphabet := ALPHA_LATIN;	# per window perhaps
+
+setalphabet(s: string)
+{
+	for(a := 0; latintab[a].ld != -1; a++){
+		k := latintab[a].ld;
+		for(i := 0; latintab[i].ld != -1; i++){
+			if(s == transs(latintab[i].nm, k)){
+				alphabet = latintab[i].ld;
+				return;
+			}
+		}
+	}
+}
+
+transc(c: int, k: int): int
+{
+	for(i := 0; latintab[i].ld != -1; i++){
+		if(k == latintab[i].ld){
+			si := latintab[i].si;
+			so := latintab[i].so;
+			ln := len si;
+			for(j := 0; j < ln; j++)
+				if(c == si[j])
+					return so[j];
+		}
+	}
+	return c;
+}
+
+transs(s: string, k: int): string
+{
+	ln := len s;
+	for(i := 0; i < ln; i++)
+		s[i] = transc(s[i], k);
+	return s;
+}
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	framem = mods.framem;
+	dat = mods.dat;
+	utils = mods.utils;
+	drawm = mods.draw;
+	acme = mods.acme;
+	graph = mods.graph;
+	gui = mods.gui;
+	scrl = mods.scroll;
+	bufferm = mods.bufferm;
+	filem = mods.filem;
+	columnm = mods.columnm;
+	windowm = mods.windowm;
+	exec = mods.exec;
+}
+
+TABDIR : con 3;	# width of tabs in directory windows
+
+# remove eventually
+KF : con 16rF000;
+Kup : con KF | 16r0E;
+Kleft : con KF | 16r11;
+Kright : con KF | 16r12;
+Kend : con KF | 16r18;
+Kdown : con 16r80;
+
+
+nulltext : Text;
+
+newtext() : ref Text
+{
+	t := ref nulltext;
+	t.frame = framem->newframe();
+	return t;
+}
+
+Text.init(t : self ref Text, f : ref File, r : Rect, rf : ref Dat->Reffont, cols : array of ref Image)
+{
+	t.file = f;
+	t.all = r;
+	t.scrollr = r;
+	t.scrollr.max.x = r.min.x+Scrollwid;
+	t.lastsr = dat->nullrect;
+	r.min.x += Scrollwid+Scrollgap;
+	t.eq0 = ~0;
+	t.ncache = 0;
+	t.reffont = rf;
+	t.tabstop = dat->maxtab;
+	for(i:=0; i<Framem->NCOL; i++)
+		t.frame.cols[i] = cols[i];
+	t.redraw(r, rf.f, mainwin, -1);
+}
+
+Text.redraw(t : self ref Text, r : Rect, f : ref Draw->Font, b : ref Image, odx : int)
+{
+	framem->frinit(t.frame, r, f, b, t.frame.cols);
+	rr := t.frame.r;
+	rr.min.x -= Scrollwid;	# back fill to scroll bar
+	draw(t.frame.b, rr, t.frame.cols[Framem->BACK], nil, (0, 0));
+	# use no wider than 3-space tabs in a directory
+	maxt := dat->maxtab;
+	if(t.what == Body){
+		if(t.w != nil && t.w.isdir)
+			maxt = min(TABDIR, dat->maxtab);
+		else
+			maxt = t.tabstop;
+	}
+	t.frame.maxtab = maxt*charwidth(f, '0');
+	# c = '0';
+	# if(t.what==Body && t.w!=nil && t.w.isdir)
+	#	c = ' ';
+	# t.frame.maxtab = Dat->Maxtab*charwidth(f, c);
+	if(t.what==Body && t.w.isdir && odx!=t.all.dx()){
+		if(t.frame.maxlines > 0){
+			t.reset();
+			t.columnate(t.w.dlp,  t.w.ndl);
+			t.show(0, 0, TRUE);
+		}
+	}else{
+		t.fill();
+		t.setselect(t.q0, t.q1);
+	}
+}
+
+Text.reshape(t : self ref Text, r : Rect) : int
+{
+	odx : int;
+
+	if(r.dy() > 0)
+		r.max.y -= r.dy()%t.frame.font.height;
+	else
+		r.max.y = r.min.y;
+	odx = t.all.dx();
+	t.all = r;
+	t.scrollr = r;
+	t.scrollr.max.x = r.min.x+Scrollwid;
+	t.lastsr = dat->nullrect;
+	r.min.x += Scrollwid+Scrollgap;
+	framem->frclear(t.frame, 0);
+	# t.redraw(r, t.frame.font, t.frame.b, odx);
+	t.redraw(r, t.frame.font, mainwin, odx);
+	return r.max.y;
+}
+
+Text.close(t : self ref Text)
+{
+	t.cache = nil;
+	framem->frclear(t.frame, 1);
+	t.file.deltext(t);
+	t.file = nil;
+	t.reffont.close();
+	if(dat->argtext == t)
+		dat->argtext = nil;
+	if(dat->typetext == t)
+		dat->typetext = nil;
+	if(dat->seltext == t)
+		dat->seltext = nil;
+	if(dat->mousetext == t)
+		dat->mousetext = nil;
+	if(dat->barttext == t)
+		dat->barttext = nil;
+}
+
+dircmp(da : ref Dirlist, db : ref Dirlist) : int
+{
+	if (da.r < db.r)
+		return -1;
+	if (da.r > db.r)
+		return 1;
+	return 0;
+}
+
+qsort(a : array of ref Dirlist, n : int)
+{
+	i, j : int;
+	t : ref Dirlist;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && dircmp(a[i], a[0]) < 0);
+			do
+				j--;
+			while(j > 0 && dircmp(a[j], a[0]) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+Text.columnate(t : self ref Text, dlp : array of ref Dirlist, ndl : int)
+{
+	i, j, w, colw, mint, maxt, ncol, nrow : int;
+	dl : ref Dirlist;
+	q1 : int;
+
+	if(t.file.ntext > 1)
+		return;
+	mint = charwidth(t.frame.font, '0');
+	# go for narrower tabs if set more than 3 wide
+	t.frame.maxtab = min(dat->maxtab, TABDIR)*mint;
+	maxt = t.frame.maxtab;
+	colw = 0;
+	for(i=0; i<ndl; i++){
+		dl = dlp[i];
+		w = dl.wid;
+		if(maxt-w%maxt < mint)
+			w += mint;
+		if(w % maxt)
+			w += maxt-(w%maxt);
+		if(w > colw)
+			colw = w;
+	}
+	if(colw == 0)
+		ncol = 1;
+	else
+		ncol = utils->max(1, t.frame.r.dx()/colw);
+	nrow = (ndl+ncol-1)/ncol;
+
+	q1 = 0;
+	for(i=0; i<nrow; i++){
+		for(j=i; j<ndl; j+=nrow){
+			dl = dlp[j];
+			t.file.insert(q1, dl.r, len dl.r);
+			q1 += len dl.r;
+			if(j+nrow >= ndl)
+				break;
+			w = dl.wid;
+			if(maxt-w%maxt < mint){
+				t.file.insert(q1, "\t", 1);
+				q1++;
+				w += mint;
+			}
+			do{
+				t.file.insert(q1, "\t", 1);
+				q1++;
+				w += maxt-(w%maxt);
+			}while(w < colw);
+		}
+		t.file.insert(q1, "\n", 1);
+		q1++;
+	}
+}
+
+Text.loadx(t : self ref Text, q0 : int, file : string, setqid : int) : int
+{
+	rp : ref Astring;
+	dl : ref Dirlist;
+	dlp : array of ref Dirlist;
+	i, n, ndl : int;
+	fd : ref Sys->FD;
+	q, q1 : int;
+	d : Dir;
+	u : ref Text;
+	ok : int;
+
+	if(t.ncache!=0 || t.file.buf.nc || t.w==nil || t!=t.w.body || (t.w.isdir && t.file.name==nil))
+		error("text.load");
+
+	{
+		fd = sys->open(file, Sys->OREAD);
+		if(fd == nil){
+			warning(nil, sprint("can't open %s: %r\n", file));
+			raise "e";
+		}
+		(ok, d) = sys->fstat(fd);
+		if(ok){
+			warning(nil, sprint("can't fstat %s: %r\n", file));
+			raise "e";
+		}
+		if(d.qid.qtype & Sys->QTDIR){
+			# this is checked in get() but it's possible the file changed underfoot 
+			if(t.file.ntext > 1){
+				warning(nil, sprint("%s is a directory; can't read with multiple windows on it\n", file));
+				raise "e";
+			}
+			t.w.isdir = TRUE;
+			t.w.filemenu = FALSE;
+			if(t.file.name[len t.file.name-1] != '/')
+				t.w.setname(t.file.name + "/", len t.file.name+1);
+			dlp = nil;
+			ndl = 0;
+			for(;;){
+				(nd, dbuf) := sys->dirread(fd);
+				if(nd <= 0)
+					break;
+				for(i=0; i<nd; i++){
+					dl = ref Dirlist;
+					dl.r = dbuf[i].name;
+					if(dbuf[i].mode & Sys->DMDIR)
+						dl.r = dl.r + "/";
+					dl.wid = graph->strwidth(t.frame.font, dl.r);
+					ndl++;
+					odlp := dlp;
+					dlp = array[ndl] of ref Dirlist;
+					dlp[0:] = odlp[0:ndl-1];
+					odlp = nil;
+					dlp[ndl-1] = dl;
+				}
+			}
+			qsort(dlp, ndl);
+			t.w.dlp = dlp;
+			t.w.ndl = ndl;
+			t.columnate(dlp, ndl);
+			q1 = t.file.buf.nc;
+		}else{
+			tmp : int;
+	
+			t.w.isdir = FALSE;
+			t.w.filemenu = TRUE;
+			tmp = t.file.loadx(q0, fd);
+			q1 = q0 + tmp;
+		}
+		fd = nil;
+		if(setqid){
+			t.file.dev = d.dev;
+			t.file.mtime = d.mtime;
+			t.file.qidpath = d.qid.path;
+		}
+		rp = stralloc(BUFSIZE);
+		for(q=q0; q<q1; q+=n){
+			n = q1-q;
+			if(n > Dat->BUFSIZE)
+				n = Dat->BUFSIZE;
+			t.file.buf.read(q, rp, 0, n);
+			if(q < t.org)
+				t.org += n;
+			else if(q <= t.org+t.frame.nchars)
+				frinsert(t.frame, rp.s, n, q-t.org);
+			if(t.frame.lastlinefull)
+				break;
+		}
+		strfree(rp);
+		rp = nil;
+		for(i=0; i<t.file.ntext; i++){
+			u = t.file.text[i];
+			if(u != t){
+				if(u.org > u.file.buf.nc)	# will be 0 because of reset(), but safety first 
+					u.org = 0;
+				u.reshape(u.all);
+				u.backnl(u.org, 0);	# go to beginning of line 
+			}
+			u.setselect(q0, q0);
+		}
+		return q1-q0;
+	}
+	exception{
+		* =>
+			fd = nil;
+			return 0;
+	}
+	return 0;
+}
+
+Text.bsinsert(t : self ref Text, q0 : int, r : string, n : int, tofile : int) : (int, int)
+{
+	tp : ref Astring;
+	bp, up : int;
+	i, initial : int;
+
+	{
+		if(t.what == Tag)	# can't happen but safety first: mustn't backspace over file name 
+			raise "e";
+		bp = 0;
+		for(i=0; i<n; i++)
+			if(r[bp++] == '\b'){
+				--bp;
+				initial = 0;
+				tp = utils->stralloc(n);
+				for (k := 0; k < i; k++)
+					tp.s[k] = r[k];
+				up = i;
+				for(; i<n; i++){
+					tp.s[up] = r[bp++];
+					if(tp.s[up] == '\b')
+						if(up == 0)
+							initial++;
+						else
+							--up;
+					else
+						up++;
+				}
+				if(initial){
+					if(initial > q0)
+						initial = q0;
+					q0 -= initial;
+					t.delete(q0, q0+initial, tofile);
+				}
+				n = up;
+				t.insert(q0, tp.s, n, tofile, 0);
+				strfree(tp);
+				tp = nil;
+				return (q0, n);
+			}
+		raise "e";
+		return(0, 0);
+	}
+	exception{
+		* =>
+			t.insert(q0, r, n, tofile, 0);
+			return (q0, n);
+	}
+	return (0, 0);
+}
+
+Text.insert(t : self ref Text, q0 : int, r : string, n : int, tofile : int, echomode : int)
+{
+	c, i : int;
+	u : ref Text;
+
+	if(tofile && t.ncache != 0)
+		error("text.insert");
+	if(n == 0)
+		return;
+	if(tofile){
+		t.file.insert(q0, r, n);
+		if(t.what == Body){
+			t.w.dirty = TRUE;
+			t.w.utflastqid = -1;
+		}
+		if(t.file.ntext > 1)
+			for(i=0; i<t.file.ntext; i++){
+				u = t.file.text[i];
+				if(u != t){
+					u.w.dirty = TRUE;	# always a body 
+					u.insert(q0, r, n, FALSE, echomode);
+					u.setselect(u.q0, u.q1);
+					scrdraw(u);
+				}
+			}		
+	}
+	if(q0 < t.q1)
+		t.q1 += n;
+	if(q0 < t.q0)
+		t.q0 += n;
+	if(q0 < t.org)
+		t.org += n;
+	else if(q0 <= t.org+t.frame.nchars) {
+		if (echomode == EM_MASK && len r == 1 && r[0] != '\n')
+			frinsert(t.frame, "*", n, q0-t.org);
+		else
+			frinsert(t.frame, r, n, q0-t.org);
+	}
+	if(t.w != nil){
+		c = 'i';
+		if(t.what == Body)
+			c = 'I';
+		if(n <= Dat->EVENTSIZE)
+			t.w.event(sprint("%c%d %d 0 %d %s\n", c, q0, q0+n, n, r[0:n]));
+		else
+			t.w.event(sprint("%c%d %d 0 0 \n", c, q0, q0+n));
+	}
+}
+
+Text.fill(t : self ref Text)
+{
+	rp : ref Astring;
+	i, n, m, nl : int;
+
+	if(t.frame.lastlinefull || t.nofill)
+		return;
+	if(t.ncache > 0){
+		if(t.w != nil)
+			t.w.commit(t);
+		else
+			t.commit(TRUE);
+	}
+	rp = stralloc(BUFSIZE);
+	do{
+		n = t.file.buf.nc-(t.org+t.frame.nchars);
+		if(n == 0)
+			break;
+		if(n > 2000)	# educated guess at reasonable amount 
+			n = 2000;
+		t.file.buf.read(t.org+t.frame.nchars, rp, 0, n);
+		#
+		# it's expensive to frinsert more than we need, so
+		# count newlines.
+		#
+		 
+		nl = t.frame.maxlines-t.frame.nlines;
+		m = 0;
+		for(i=0; i<n; ){
+			if(rp.s[i++] == '\n'){
+				m++;
+				if(m >= nl)
+					break;
+			}
+		}
+		frinsert(t.frame, rp.s, i, t.frame.nchars);
+	}while(t.frame.lastlinefull == FALSE);
+	strfree(rp);
+	rp = nil;
+}
+
+Text.delete(t : self ref Text, q0 : int, q1 : int, tofile : int)
+{
+	n, p0, p1 : int;
+	i, c : int;
+	u : ref Text;
+
+	if(tofile && t.ncache != 0)
+		error("text.delete");
+	n = q1-q0;
+	if(n == 0)
+		return;
+	if(tofile){
+		t.file.delete(q0, q1);
+		if(t.what == Body){
+			t.w.dirty = TRUE;
+			t.w.utflastqid = -1;
+		}
+		if(t.file.ntext > 1)
+			for(i=0; i<t.file.ntext; i++){
+				u = t.file.text[i];
+				if(u != t){
+					u.w.dirty = TRUE;	# always a body 
+					u.delete(q0, q1, FALSE);
+					u.setselect(u.q0, u.q1);
+					scrdraw(u);
+				}
+			}
+	}
+	if(q0 < t.q0)
+		t.q0 -= min(n, t.q0-q0);
+	if(q0 < t.q1)
+		t.q1 -= min(n, t.q1-q0);
+	if(q1 <= t.org)
+		t.org -= n;
+	else if(q0 < t.org+t.frame.nchars){
+		p1 = q1 - t.org;
+		if(p1 > t.frame.nchars)
+			p1 = t.frame.nchars;
+		if(q0 < t.org){
+			t.org = q0;
+			p0 = 0;
+		}else
+			p0 = q0 - t.org;
+		frdelete(t.frame, p0, p1);
+		t.fill();
+	}
+	if(t.w != nil){
+		c = 'd';
+		if(t.what == Body)
+			c = 'D';
+		t.w.event(sprint("%c%d %d 0 0 \n", c, q0, q1));
+	}
+}
+
+onechar : ref Astring;
+
+Text.readc(t : self ref Text, q : int) : int
+{
+	if(t.cq0<=q && q<t.cq0+t.ncache)
+		return t.cache[q-t.cq0];
+	if (onechar == nil)
+		onechar = stralloc(1);
+	t.file.buf.read(q, onechar, 0, 1);
+	return onechar.s[0];
+}
+
+Text.bswidth(t : self ref Text, c : int) : int
+{
+	q, eq : int;
+	r : int;
+	skipping : int;
+
+	# there is known to be at least one character to erase 
+	if(c == 16r08)	# ^H: erase character 
+		return 1;
+	q = t.q0;
+	skipping = TRUE;
+	while(q > 0){
+		r = t.readc(q-1);
+		if(r == '\n'){		# eat at most one more character 
+			if(q == t.q0)	# eat the newline 
+				--q;
+			break; 
+		}
+		if(c == 16r17){
+			eq = isalnum(r);
+			if(eq && skipping)	# found one; stop skipping 
+				skipping = FALSE;
+			else if(!eq && !skipping)
+				break;
+		}
+		--q;
+	}
+	return t.q0-q;
+}
+
+Text.typex(t : self ref Text, r : int, echomode : int)
+{
+	q0, q1 : int;
+	nnb, nb, n, i : int;
+	u : ref Text;
+
+	if(alphabet != ALPHA_LATIN)
+		r = transc(r, alphabet);
+	if (echomode == EM_RAW && t.what == Body) {
+		if (t.w != nil) {
+			s := "a";
+			s[0] = r;
+			t.w.event(sprint("R0 0 0 1 %s\n", s));
+		}
+		return;
+	}
+	if(t.what!=Body && r=='\n')
+		return;
+	case r {
+		Dat->Kscrolldown=>
+			if(t.what == Body){
+				q0 = t.org+frcharofpt(t.frame, (t.frame.r.min.x, t.frame.r.min.y+2*t.frame.font.height));
+				t.setorigin(q0, FALSE);
+			}
+			return;
+		Dat->Kscrollup=>
+			if(t.what == Body){
+				q0 = t.backnl(t.org, 4);
+				t.setorigin(q0, FALSE);
+			}
+			return;		
+		Kdown or Keyboard->Down =>
+			n = t.frame.maxlines/3;
+			q0 = t.org+frcharofpt(t.frame, (t.frame.r.min.x, t.frame.r.min.y+n*t.frame.font.height));
+			t.setorigin(q0, FALSE);
+			return;
+		Keyboard->Pgdown =>
+			n = 2*t.frame.maxlines/3;
+			q0 = t.org+frcharofpt(t.frame, (t.frame.r.min.x, t.frame.r.min.y+n*t.frame.font.height));
+			t.setorigin(q0, FALSE);
+			return;
+		Kup or Keyboard->Up =>
+			n = t.frame.maxlines/3;
+			q0 = t.backnl(t.org, n);
+			t.setorigin(q0, FALSE);
+			return;
+		Keyboard->Pgup =>
+			n = 2*t.frame.maxlines/3;
+			q0 = t.backnl(t.org, n);
+			t.setorigin(q0, FALSE);
+			return;
+		Keyboard->Home =>
+			t.commit(TRUE);
+			t.show(0, 0, FALSE);
+			return;
+		Kend or Keyboard->End =>
+			t.commit(TRUE);
+			t.show(t.file.buf.nc, t.file.buf.nc, FALSE);
+			return;
+		Kleft or Keyboard->Left =>
+			t.commit(TRUE);
+			if(t.q0 != t.q1)
+				t.show(t.q0, t.q0, TRUE);
+			else if(t.q0 != 0)
+				t.show(t.q0-1, t.q0-1, TRUE);
+			return;
+		Kright or Keyboard->Right =>
+			t.commit(TRUE);
+			if(t.q0 != t.q1)
+				t.show(t.q1, t.q1, TRUE);
+			else if(t.q1 != t.file.buf.nc)
+				t.show(t.q1+1, t.q1+1, TRUE);
+			return;
+		1 =>  	# ^A: beginning of line
+			t.commit(TRUE);
+			# go to where ^U would erase, if not already at BOL
+			nnb = 0;
+			if(t.q0>0 && t.readc(t.q0-1)!='\n')
+				nnb = t.bswidth(16r15);
+			t.show(t.q0-nnb, t.q0-nnb, TRUE);
+			return;
+		5 =>  	# ^E: end of line
+			t.commit(TRUE);
+			q0 = t.q0;
+			while(q0<t.file.buf.nc && t.readc(q0)!='\n')
+				q0++;
+			t.show(q0, q0, TRUE);
+			return;
+	}
+	if(t.what == Body){
+		seq++;
+		t.file.mark();
+	}
+	if(t.q1 > t.q0){
+		if(t.ncache != 0)
+			error("text.type");
+		exec->cut(t, t, TRUE, TRUE);
+		t.eq0 = ~0;
+		if (r == 16r08 || r == 16r7f){	# erase character : odd if a char then erased
+			t.show(t.q0, t.q0,TRUE);
+			return;
+		}
+	}
+	t.show(t.q0, t.q0, TRUE);
+	case(r){
+	16r1B =>
+		if(t.eq0 != ~0)
+			t.setselect(t.eq0, t.q0);
+		if(t.ncache > 0){
+			if(t.w != nil)
+				t.w.commit(t);
+			else
+				t.commit(TRUE);
+		}
+		return;
+	16r08 or 16r15 or 16r17 =>
+		# ^H: erase character or ^U: erase line or ^W: erase word 
+		if(t.q0 == 0)
+			return;
+if(0)	# DEBUGGING 
+	for(i=0; i<t.file.ntext; i++){
+		u = t.file.text[i];
+		if(u.cq0!=t.cq0 && (u.ncache!=t.ncache || t.ncache!=0))
+			error("text.type inconsistent caches");
+	}
+		nnb = t.bswidth(r);
+		q1 = t.q0;
+		q0 = q1-nnb;
+		for(i=0; i<t.file.ntext; i++){
+			u = t.file.text[i];
+			u.nofill = TRUE;
+			nb = nnb;
+			n = u.ncache;
+			if(n > 0){
+				if(q1 != u.cq0+n)
+					error("text.type backspace");
+				if(n > nb)
+					n = nb;
+				u.ncache -= n;
+				u.delete(q1-n, q1, FALSE);
+				nb -= n;
+			}
+			if(u.eq0==q1 || u.eq0==~0)
+				u.eq0 = q0;
+			if(nb && u==t)
+				u.delete(q0, q0+nb, TRUE);
+			if(u != t)
+				u.setselect(u.q0, u.q1);
+			else
+				t.setselect(q0, q0);
+			u.nofill = FALSE;
+		}
+		for(i=0; i<t.file.ntext; i++)
+			t.file.text[i].fill();
+		return;
+	16r7f or Keyboard->Del =>
+		# Delete character - forward delete
+		t.commit(TRUE);
+		if(t.q0 >= t.file.buf.nc)
+			return;
+		nnb = 1;
+		q0 = t.q0;
+		q1 = q0+nnb;
+		for(i=0; i<t.file.ntext; i++){
+			u = t.file.text[i];
+			if (u!=t)
+				u.commit(FALSE);
+			u.nofill = TRUE;
+			if(u.eq0==q1 || u.eq0==~0)
+				u.eq0 = q0;
+			if(u==t)
+				u.delete(q0, q1, TRUE);
+			if(u != t)
+				u.setselect(u.q0, u.q1);
+			else
+				t.setselect(q0, q0);
+			u.nofill = FALSE;
+		}
+		for(i=0; i<t.file.ntext; i++)
+			t.file.text[i].fill();
+		return;
+	}
+	# otherwise ordinary character; just insert, typically in caches of all texts 
+if(0)	# DEBUGGING 
+	for(i=0; i<t.file.ntext; i++){
+		u = t.file.text[i];
+		if(u.cq0!=t.cq0 && (u.ncache!=t.ncache || t.ncache!=0))
+			error("text.type inconsistent caches");
+	}
+	for(i=0; i<t.file.ntext; i++){
+		u = t.file.text[i];
+		if(u.eq0 == ~0)
+			u.eq0 = t.q0;
+		if(u.ncache == 0)
+			u.cq0 = t.q0;
+		else if(t.q0 != u.cq0+u.ncache)
+			error("text.type cq1");
+		str := "Z";
+		str[0] = r;
+		u.insert(t.q0, str, 1, FALSE, echomode);
+		str = nil;
+		if(u != t)
+			u.setselect(u.q0, u.q1);
+		if(u.ncache == u.ncachealloc){
+			u.ncachealloc += 10;
+			u.cache += "1234567890";
+		}
+		u.cache[u.ncache++] = r;
+	}
+	t.setselect(t.q0+1, t.q0+1);
+	if(r=='\n' && t.w!=nil)
+		t.w.commit(t);
+}
+
+Text.commit(t : self ref Text, tofile : int)
+{
+	if(t.ncache == 0)
+		return;
+	if(tofile)
+		t.file.insert(t.cq0, t.cache, t.ncache);
+	if(t.what == Body){
+		t.w.dirty = TRUE;
+		t.w.utflastqid = -1;
+	}
+	t.ncache = 0;
+}
+
+clicktext : ref Text;
+clickmsec : int = 0;
+selecttext : ref Text;
+selectq : int = 0;
+
+#
+# called from frame library
+#
+ 
+framescroll(f : ref Frame, dl : int)
+{
+	if(f != selecttext.frame)
+		error("frameselect not right frame");
+	selecttext.framescroll(dl);
+}
+
+Text.framescroll(t : self ref Text, dl : int)
+{
+	q0 : int;
+
+	if(dl == 0){
+		scrl->scrsleep(100);
+		return;
+	}
+	if(dl < 0){
+		q0 = t.backnl(t.org, -dl);
+		if(selectq > t.org+t.frame.p0)
+			t.setselect0(t.org+t.frame.p0, selectq);
+		else
+			t.setselect0(selectq, t.org+t.frame.p0);
+	}else{
+		if(t.org+t.frame.nchars == t.file.buf.nc)
+			return;
+		q0 = t.org+frcharofpt(t.frame, (t.frame.r.min.x, t.frame.r.min.y+dl*t.frame.font.height));
+		if(selectq > t.org+t.frame.p1)
+			t.setselect0(t.org+t.frame.p1, selectq);
+		else
+			t.setselect0(selectq, t.org+t.frame.p1);
+	}
+	t.setorigin(q0, TRUE);
+}
+
+
+Text.select(t : self ref Text, double : int)
+{
+	q0, q1 : int;
+	b, x, y : int;
+	state : int;
+
+	selecttext = t;
+	#
+	# To have double-clicking and chording, we double-click
+	# immediately if it might make sense.
+	#
+	 
+	b = mouse.buttons;
+	q0 = t.q0;
+	q1 = t.q1;
+	selectq = t.org+frcharofpt(t.frame, mouse.xy);
+	if(double || (clicktext==t && mouse.msec-clickmsec<500))
+	if(q0==q1 && selectq==q0){
+		(q0, q1) = t.doubleclick(q0, q1);
+		t.setselect(q0, q1);
+		bflush();
+		x = mouse.xy.x;
+		y = mouse.xy.y;
+		# stay here until something interesting happens 
+		do
+			frgetmouse();
+		while(mouse.buttons==b && utils->abs(mouse.xy.x-x)<3 && utils->abs(mouse.xy.y-y)<3);
+		mouse.xy.x = x;	# in case we're calling frselect 
+		mouse.xy.y = y;
+		q0 = t.q0;	# may have changed 
+		q1 = t.q1;
+		selectq = q0;
+	}
+	if(mouse.buttons == b){
+		t.frame.scroll = 1;
+		frselect(t.frame, mouse);
+		# horrible botch: while asleep, may have lost selection altogether 
+		if(selectq > t.file.buf.nc)
+			selectq = t.org + t.frame.p0;
+		t.frame.scroll = 0;
+		if(selectq < t.org)
+			q0 = selectq;
+		else
+			q0 = t.org + t.frame.p0;
+		if(selectq > t.org+t.frame.nchars)
+			q1 = selectq;
+		else
+			q1 = t.org+t.frame.p1;
+	}
+	if(q0 == q1){
+		if(q0==t.q0 && (double || clicktext==t && mouse.msec-clickmsec<500)){
+			(q0, q1) = t.doubleclick(q0, q1);
+			clicktext = nil;
+		}else{
+			clicktext = t;
+			clickmsec = mouse.msec;
+		}
+	}else
+		clicktext = nil;
+	t.setselect(q0, q1);
+	bflush();
+	state = 0;	# undo when possible; +1 for cut, -1 for paste 
+	while(mouse.buttons){
+		mouse.msec = 0;
+		b = mouse.buttons;
+		if(b & 6){
+			if(state==0 && t.what==Body){
+				seq++;
+				t.w.body.file.mark();
+			}
+			if(b & 2){
+				if(state==-1 && t.what==Body){
+					t.w.undo(TRUE);
+					t.setselect(q0, t.q0);
+					state = 0;
+				}else if(state != 1){
+					exec->cut(t, t, TRUE, TRUE);
+					state = 1;
+				}
+			}else{
+				if(state==1 && t.what==Body){
+					t.w.undo(TRUE);
+					t.setselect(q0, t.q1);
+					state = 0;
+				}else if(state != -1){
+					exec->paste(t, t, TRUE, FALSE);
+					state = -1;
+				}
+			}
+			scrdraw(t);
+			utils->clearmouse();
+		}
+		bflush();
+		while(mouse.buttons == b)
+			frgetmouse();
+		clicktext = nil;
+	}
+}
+
+Text.show(t : self ref Text, q0 : int, q1 : int, doselect : int)
+{
+	qe : int;
+	nl : int;
+	q : int;
+
+	if(t.what != Body){
+		if(doselect)
+			t.setselect(q0, q1);
+		return;
+	}
+	if(t.w!=nil && t.frame.maxlines==0)
+		t.col.grow(t.w, 1, 0);
+	if(doselect)
+		t.setselect(q0, q1);
+	qe = t.org+t.frame.nchars;
+	if(t.org<=q0 && (q0<qe || (q0==qe && qe==t.file.buf.nc+t.ncache)))
+		scrdraw(t);
+	else{
+		if(t.w.nopen[Dat->QWevent]>byte 0)
+			nl = 3*t.frame.maxlines/4;
+		else
+			nl = t.frame.maxlines/4;
+		q = t.backnl(q0, nl);
+		# avoid going backwards if trying to go forwards - long lines! 
+		if(!(q0>t.org && q<t.org))
+			t.setorigin(q, TRUE);
+		while(q0 > t.org+t.frame.nchars)
+			t.setorigin(t.org+1, FALSE);
+	}
+}
+
+region(a, b : int) : int
+{
+	if(a < b)
+		return -1;
+	if(a == b)
+		return 0;
+	return 1;
+}
+
+selrestore(f : ref Frame, pt0 : Point, p0 : int, p1 : int)
+{
+	if(p1<=f.p0 || p0>=f.p1){
+		# no overlap
+		frdrawsel0(f, pt0, p0, p1, f.cols[BACK], f.cols[TEXT]);
+		return;
+	}
+	if(p0>=f.p0 && p1<=f.p1){
+		# entirely inside
+		frdrawsel0(f, pt0, p0, p1, f.cols[HIGH], f.cols[HTEXT]);
+		return;
+	}
+	# they now are known to overlap
+	# before selection
+	if(p0 < f.p0){
+		frdrawsel0(f, pt0, p0, f.p0, f.cols[BACK], f.cols[TEXT]);
+		p0 = f.p0;
+		pt0 = frptofchar(f, p0);
+	}
+	# after selection
+	if(p1 > f.p1){
+		frdrawsel0(f, frptofchar(f, f.p1), f.p1, p1, f.cols[BACK], f.cols[TEXT]);
+		p1 = f.p1;
+	}
+	# inside selection
+	frdrawsel0(f, pt0, p0, p1, f.cols[HIGH], f.cols[HTEXT]);
+}
+
+Text.setselect(t : self ref Text, q0 : int, q1 : int)
+{
+	p0, p1 : int;
+
+	# t.p0 and t.p1 are always right; t.q0 and t.q1 may be off 
+	t.q0 = q0;
+	t.q1 = q1;
+	# compute desired p0,p1 from q0,q1
+	p0 = q0-t.org;
+	p1 = q1-t.org;
+	if(p0 < 0)
+		p0 = 0;
+	if(p1 < 0)
+		p1 = 0;
+	if(p0 > t.frame.nchars)
+		p0 = t.frame.nchars;
+	if(p1 > t.frame.nchars)
+		p1 = t.frame.nchars;
+	if(p0==t.frame.p0 && p1==t.frame.p1)
+		return;
+	# screen disagrees with desired selection
+	if(t.frame.p1<=p0 || p1<=t.frame.p0 || p0==p1 || t.frame.p1==t.frame.p0){
+		# no overlap or too easy to bother trying
+		frdrawsel(t.frame, frptofchar(t.frame, t.frame.p0), t.frame.p0, t.frame.p1, 0);
+		frdrawsel(t.frame, frptofchar(t.frame, p0), p0, p1, 1);
+		t.frame.p0 = p0;
+		t.frame.p1 = p1;
+		return;
+	}
+	# overlap; avoid unnecessary painting
+	if(p0 < t.frame.p0){
+		# extend selection backwards
+		frdrawsel(t.frame, frptofchar(t.frame, p0), p0, t.frame.p0, 1);
+	}else if(p0 > t.frame.p0){
+		# trim first part of selection
+		frdrawsel(t.frame, frptofchar(t.frame, t.frame.p0), t.frame.p0, p0, 0);
+	}
+	if(p1 > t.frame.p1){
+		# extend selection forwards
+		frdrawsel(t.frame, frptofchar(t.frame, t.frame.p1), t.frame.p1, p1, 1);
+	}else if(p1 < t.frame.p1){
+		# trim last part of selection
+		frdrawsel(t.frame, frptofchar(t.frame, p1), p1, t.frame.p1, 0);
+	}
+	t.frame.p0 = p0;
+	t.frame.p1 = p1;
+}
+
+Text.setselect0(t : self ref Text, q0 : int, q1 : int)
+{
+	t.q0 = q0;
+	t.q1 = q1;
+}
+
+xselect(f : ref Frame, mc : ref Draw->Pointer, col, colt : ref Image) : (int, int)
+{
+	p0, p1, q, tmp : int;
+	mp, pt0, pt1, qt : Point;
+	reg, b : int;
+
+	# when called button 1 is down
+	mp = mc.xy;
+	b = mc.buttons;
+
+	# remove tick
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 0);
+	p0 = p1 = frcharofpt(f, mp);
+	pt0 = frptofchar(f, p0);
+	pt1 = frptofchar(f, p1);
+	reg = 0;
+	frtick(f, pt0, 1);
+	do{
+		q = frcharofpt(f, mc.xy);
+		if(p1 != q){
+			if(p0 == p1)
+				frtick(f, pt0, 0);
+			if(reg != region(q, p0)){	# crossed starting point; reset
+				if(reg > 0)
+					selrestore(f, pt0, p0, p1);
+				else if(reg < 0)
+					selrestore(f, pt1, p1, p0);
+				p1 = p0;
+				pt1 = pt0;
+				reg = region(q, p0);
+				if(reg == 0)
+					frdrawsel0(f, pt0, p0, p1, col, colt);
+			}
+			qt = frptofchar(f, q);
+			if(reg > 0){
+				if(q > p1)
+					frdrawsel0(f, pt1, p1, q, col, colt);
+				else if(q < p1)
+					selrestore(f, qt, q, p1);
+			}else if(reg < 0){
+				if(q > p1)
+					selrestore(f, pt1, p1, q);
+				else
+					frdrawsel0(f, qt, q, p1, col, colt);
+			}
+			p1 = q;
+			pt1 = qt;
+		}
+		if(p0 == p1)
+			frtick(f, pt0, 1);
+		bflush();
+		frgetmouse();
+	}while(mc.buttons == b);
+	if(p1 < p0){
+		tmp = p0;
+		p0 = p1;
+		p1 = tmp;
+	}
+	pt0 = frptofchar(f, p0);
+	if(p0 == p1)
+		frtick(f, pt0, 0);
+	selrestore(f, pt0, p0, p1);
+	# restore tick
+	if(f.p0 == f.p1)
+		frtick(f, frptofchar(f, f.p0), 1);
+	bflush();
+	return (p0, p1);
+}
+
+Text.select23(t : self ref Text, q0 : int, q1 : int, high, low : ref Image, mask : int) : (int, int, int)
+{
+	p0, p1 : int;
+	buts : int;
+
+	(p0, p1) = xselect(t.frame, mouse, high, low);
+	buts = mouse.buttons;
+	if((buts & mask) == 0){
+		q0 = p0+t.org;
+		q1 = p1+t.org;
+	}
+	while(mouse.buttons)
+		frgetmouse();
+	return (buts, q0, q1);
+}
+
+Text.select2(t : self ref Text, q0 : int, q1 : int) : (int, ref Text, int, int)
+{
+	buts : int;
+	
+	(buts, q0, q1) = t.select23(q0, q1, acme->but2col, acme->but2colt, 4);
+	if(buts & 4)
+		return (0, nil, q0, q1);
+	if(buts & 1)	# pick up argument 
+		return (1, dat->argtext, q0, q1);
+	return (1, nil, q0, q1);
+}
+
+Text.select3(t : self ref Text, q0 : int, q1 : int) : (int, int, int)
+{
+	buts : int;
+	
+	(buts, q0, q1) = t.select23(q0, q1, acme->but3col, acme->but3colt, 1|2);
+	return (buts == 0, q0, q1);
+}
+
+left := array[4] of {
+	"{[(<«",
+	"\n",
+	"'\"`",
+	nil
+};
+right := array[4] of {
+	"}])>»",
+	"\n",
+	"'\"`",
+	nil
+};
+
+Text.doubleclick(t : self ref Text, q0 : int, q1 : int) : (int, int)
+{
+	c, i : int;
+	r, l : string;
+	p : int;
+	q : int;
+	res : int;
+
+	for(i=0; left[i]!=nil; i++){
+		q = q0;
+		l = left[i];
+		r = right[i];
+		# try matching character to left, looking right 
+		if(q == 0)
+			c = '\n';
+		else
+			c = t.readc(q-1);
+		p = utils->strchr(l, c);
+		if(p >= 0){
+			(res, q) = t.clickmatch(c, r[p], 1, q);
+			if (res)
+				q1 = q-(c!='\n');
+			return (q0, q1);
+		}
+		# try matching character to right, looking left 
+		if(q == t.file.buf.nc)
+			c = '\n';
+		else
+			c = t.readc(q);
+		p = utils->strchr(r, c);
+		if(p >= 0){
+			(res, q) = t.clickmatch(c, l[p], -1, q);
+			if (res){
+				q1 = q0+(q0<t.file.buf.nc && c=='\n');
+				q0 = q;
+				if(c!='\n' || q!=0 || t.readc(0)=='\n')
+					q0++;
+			}
+			return (q0, q1);
+		}
+	}
+	# try filling out word to right 
+	while(q1<t.file.buf.nc && isalnum(t.readc(q1)))
+		q1++;
+	# try filling out word to left 
+	while(q0>0 && isalnum(t.readc(q0-1)))
+		q0--;
+	return (q0, q1);
+}
+
+Text.clickmatch(t : self ref Text, cl : int, cr : int, dir : int, q : int) : (int, int)
+{
+	c : int;
+	nest : int;
+
+	nest = 1;
+	for(;;){
+		if(dir > 0){
+			if(q == t.file.buf.nc)
+				break;
+			c = t.readc(q);
+			q++;
+		}else{
+			if(q == 0)
+				break;
+			q--;
+			c = t.readc(q);
+		}
+		if(c == cr){
+			if(--nest==0)
+				return (1, q);
+		}else if(c == cl)
+			nest++;
+	}
+	return (cl=='\n' && nest==1, q);
+}
+
+Text.forwnl(t : self ref Text, p : int, n : int) : int
+{
+	i, j : int;
+
+	e := t.file.buf.nc-1;
+	i = n;
+	while(i-- > 0 && p<e){
+		++p;
+		if(p == e)
+			break;
+		for(j=128; --j>0 && p<e; p++)
+			if(t.readc(p)=='\n')
+				break;
+	}
+	return p;
+}
+
+Text.backnl(t : self ref Text, p : int, n : int) : int
+{
+	i, j : int;
+
+	# look for start of this line if n==0 
+	if(n==0 && p>0 && t.readc(p-1)!='\n')
+		n = 1;
+	i = n;
+	while(i-- > 0 && p>0){
+		--p;	# it's at a newline now; back over it 
+		if(p == 0)
+			break;
+		# at 128 chars, call it a line anyway 
+		for(j=128; --j>0 && p>0; p--)
+			if(t.readc(p-1)=='\n')
+				break;
+	}
+	return p;
+}
+
+Text.setorigin(t : self ref Text, org : int, exact : int)
+{
+	i, a : int;
+	r : ref Astring;
+	n : int;
+
+	t.frame.b.flush(Flushoff);
+	if(org>0 && !exact){
+		# org is an estimate of the char posn; find a newline 
+		# don't try harder than 256 chars 
+		for(i=0; i<256 && org<t.file.buf.nc; i++){
+			if(t.readc(org) == '\n'){
+				org++;
+				break;
+			}
+			org++;
+		}
+	}
+	a = org-t.org;
+	fixup := 0;
+	if(a>=0 && a<t.frame.nchars){
+		frdelete(t.frame, 0, a);
+		fixup = 1;		# frdelete can leave end of last line in wrong selection mode; it doesn't know what follows 
+	}
+	else if(a<0 && -a<t.frame.nchars){
+		n = t.org - org;
+		r = utils->stralloc(n);
+		t.file.buf.read(org, r, 0, n);
+		frinsert(t.frame, r.s, n, 0);
+		utils->strfree(r);
+		r = nil;
+	}else
+		frdelete(t.frame, 0, t.frame.nchars);
+	t.org = org;
+	t.fill();
+	scrdraw(t);
+	t.setselect(t.q0, t.q1);
+	if(fixup && t.frame.p1 > t.frame.p0)
+		frdrawsel(t.frame, frptofchar(t.frame, t.frame.p1-1), t.frame.p1-1, t.frame.p1, 1);
+	t.frame.b.flush(Flushon);
+}
+
+Text.reset(t : self ref Text)
+{
+	t.file.seq = 0;
+	t.eq0 = ~0;
+	# do t.delete(0, t.nc, TRUE) without building backup stuff 
+	t.setselect(t.org, t.org);
+	frdelete(t.frame, 0, t.frame.nchars);
+	t.org = 0;
+	t.q0 = 0;
+	t.q1 = 0;
+	t.file.reset();
+	t.file.buf.reset();
+}
--- /dev/null
+++ b/appl/acme/text.m
@@ -1,0 +1,65 @@
+Textm : module {
+	PATH : con "/dis/acme/text.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	# Text.what
+	Columntag, Rowtag, Tag, Body : con iota;
+
+	newtext : fn() : ref Text;
+
+	Text : adt {
+		file : cyclic ref Filem->File;
+		frame : ref Framem->Frame;
+		reffont : ref Dat->Reffont;
+		org : int;
+		q0 : int;
+		q1 : int;
+		what : int;
+		tabstop : int;
+		w : cyclic ref Windowm->Window;
+		scrollr : Draw->Rect;
+		lastsr : Draw->Rect;
+		all : Draw->Rect;
+		row : cyclic ref Rowm->Row;
+		col : cyclic ref Columnm->Column;
+		eq0 : int;		# start of typing for ESC
+		cq0 : int;		# cache position
+		ncache : int;	# storage for insert
+		ncachealloc : int;
+		cache : string;
+		nofill : int;
+
+		init : fn(t : self ref Text, f : ref Filem->File, r : Draw->Rect, rf : ref Dat->Reffont, cols : array of ref Draw->Image);
+		redraw : fn(t : self ref Text, r : Draw->Rect, f : ref Draw->Font, b : ref Draw->Image, n : int);
+		insert : fn(t : self ref Text, n : int, s : string, p : int, q : int, r : int);
+		bsinsert : fn(t : self ref Text, n : int, s : string, p : int, q : int) : (int, int);
+		delete : fn(t : self ref Text, n : int, p : int, q : int);
+		loadx : fn(t : self ref Text, n : int, b : string, q : int) : int;
+		typex : fn(t : self ref Text, r : int, echomode : int);
+		select : fn(t : self ref Text, d : int);
+		select2 : fn(t : self ref Text, p : int, q : int) : (int, ref Text, int, int);
+		select3 : fn(t : self ref Text, p: int, q : int) : (int, int, int);
+		setselect : fn(t : self ref Text, p : int, q : int);
+		setselect0 : fn(t : self ref Text, p : int, q : int);
+		show : fn(t : self ref Text, p : int, q : int, dosel : int);
+		fill : fn(t : self ref Text);
+		commit : fn(t : self ref Text, n : int);
+		setorigin : fn(t : self ref Text, p : int, q : int);
+		readc : fn(t : self ref Text, n : int) : int;
+		reset : fn(t : self ref Text);
+		reshape : fn(t : self ref Text, r : Draw->Rect) : int;
+		close : fn(t : self ref Text);
+		framescroll : fn(t : self ref Text, n : int);
+		select23 : fn(t : self ref Text, p : int, q : int, i, it : ref Draw->Image, n : int) : (int, int, int);
+		forwnl : fn(t : self ref Text, p : int, q : int) : int;
+		backnl : fn(t : self ref Text, p : int, q : int) : int;
+		bswidth : fn(t : self ref Text, r : int) : int;
+		doubleclick : fn(t : self ref Text, p : int, q : int) : (int, int);
+		clickmatch : fn(t : self ref Text, p : int, q : int, r : int, n : int) : (int, int);
+		columnate : fn(t : self ref Text, d : array of ref Dat->Dirlist, n : int);
+	};
+
+	framescroll : fn(f : ref Framem->Frame, dl : int);
+	setalphabet: fn(s: string);
+};
--- /dev/null
+++ b/appl/acme/time.b
@@ -1,0 +1,129 @@
+implement Timerm;
+
+include "common.m";
+
+sys : Sys;
+acme : Acme;
+utils : Utils;
+dat : Dat;
+
+millisec : import sys;
+Timer : import dat;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	acme = mods.acme;
+	utils = mods.utils;
+	dat = mods.dat;
+}
+
+ctimer : chan of ref Timer;
+
+timeproc()
+{
+	i, nt, na, dt : int;
+	x : ref Timer;
+	t : array of ref Timer;
+	old, new : int;
+
+	acme->timerpid = sys->pctl(0, nil);
+	sys->pctl(Sys->FORKFD, nil);
+	t = array[10] of ref Timer;
+	na = 10;
+	nt = 0;
+	old = millisec();
+	for(;;){
+		if (nt == 0) {	# don't waste cpu time
+			x = <-ctimer;
+			t[nt++] = x;
+			old = millisec();
+		}
+		sys->sleep(1);	# will sleep minimum incr 
+		new = millisec();
+		dt = new-old;
+		old = new;
+		if(dt < 0)	# timer wrapped; go around, losing a tick 
+			continue;
+		for(i=0; i<nt; i++){
+			x = t[i];
+			x.dt -= dt;
+			if(x.dt <= 0){
+				#
+				# avoid possible deadlock if client is
+				# now sending on ctimer
+				#
+				 
+				alt {
+					x.c <-= 0 =>
+						t[i:] = t[i+1:nt];
+						t[nt-1] = nil;
+						nt--;
+						i--;
+					* =>
+						;
+				}
+			}
+		}
+		gotone := 1;
+		while (gotone) {
+			alt {
+				x = <-ctimer =>
+					if (nt == na) {
+						ot := t;
+						t = array[na+10] of ref Timer;
+						t[0:] = ot[0:na];
+						ot = nil;
+						na += 10;
+					}
+					t[nt++] = x;
+					old = millisec();
+				* =>
+					gotone = 0;
+			}
+		}
+	}
+}
+
+timerinit()
+{
+	ctimer = chan of ref Timer;
+	spawn timeproc();
+}
+
+#
+# timeralloc() and timerfree() don't lock, so can only be
+# called from the main proc.
+#
+ 
+
+timer : ref Timer;
+
+timerstart(dt : int) : ref Timer
+{
+	t : ref Timer;
+
+	t = timer;
+	if(t != nil)
+		timer = timer.next;
+	else{
+		t = ref Timer;
+		t.c = chan of int;
+	}
+	t.next = nil;
+	t.dt = dt;
+	ctimer <-= t;
+	return t;
+}
+
+timerstop(t : ref Timer)
+{
+	t.next = timer;
+	timer = t;
+}
+
+timerwaittask(timer : ref Timer)
+{
+	<-(timer.c);
+	timerstop(timer);
+}
--- /dev/null
+++ b/appl/acme/time.m
@@ -1,0 +1,10 @@
+Timerm : module {
+	PATH : con "/dis/acme/time.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	timerinit: fn();
+	timerstart : fn(dt : int) : ref Dat->Timer;
+	timerstop : fn(t : ref Dat->Timer);
+	timerwaittask : fn(t : ref Dat->Timer);
+};
--- /dev/null
+++ b/appl/acme/util.b
@@ -1,0 +1,574 @@
+implement Utils;
+
+include "common.m";
+include "sh.m";
+include "env.m";
+
+sys : Sys;
+draw : Draw;
+gui : Gui;
+acme : Acme;
+dat : Dat;
+graph : Graph;
+textm : Textm;
+windowm : Windowm;
+columnm : Columnm;
+rowm : Rowm;
+scrl : Scroll;
+look : Look;
+
+RELEASECOPY : import acme;
+Point, Rect : import draw;
+Astring, TRUE, FALSE, Mntdir, Lock : import dat;
+mouse, activecol, seltext, row : import dat;
+cursorset : import graph;
+mainwin : import gui;
+Text : import textm;
+Window : import windowm;
+Column : import columnm;
+Row : import rowm;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	draw = mods.draw;
+	gui = mods.gui;
+	acme = mods.acme;
+	dat = mods.dat;
+	graph = mods.graph;
+	textm = mods.textm;
+	windowm = mods.windowm;
+	columnm = mods.columnm;
+	rowm = mods.rowm;
+	scrl = mods.scroll;
+	look = mods.look;
+
+	stderr = sys->fildes(2);
+}
+
+min(x : int, y : int) : int
+{
+	if (x < y)
+		return x;
+	return y;
+}
+
+max(x : int, y : int) : int
+{
+	if (x > y)
+		return x;
+	return y;
+}
+
+abs(x : int) : int
+{
+	if (x < 0)
+		return -x;
+	return x;
+}
+
+isalnum(c : int) : int
+{
+	#
+	# Hard to get absolutely right.  Use what we know about ASCII
+	# and assume anything above the Latin control characters is
+	# potentially an alphanumeric.
+	#
+	if(c <= ' ')
+		return FALSE;
+	if(16r7F<=c && c<=16rA0)
+		return FALSE;
+	if(strchr("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", c) >= 0)
+		return FALSE;
+	return TRUE;
+	# return ('a' <= c && c <= 'z') || 
+	#	   ('A' <= c && c <= 'Z') ||
+	#	   ('0' <= c && c <= '9');
+}
+
+strchr(s : string, c : int) : int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+} 
+
+strrchr(s : string, c : int) : int
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+strncmp(s, t : string, n : int) : int
+{
+	if (len s > n)
+		s = s[0:n];
+	if (len t > n)
+		t = t[0:n];
+	if (s < t)
+		return -1;
+	if (s > t)
+		return 1;
+	return 0;
+}
+
+env : Env;
+
+getenv(s : string) : string
+{
+	if (env == nil)
+		env = load Env Env->PATH;
+	e := env->getenv(s);
+	if(e != nil && e[len e - 1] == '\n')	# shell bug
+		return e[0: len e -1];
+	return e;
+}
+
+setenv(s, t : string)
+{
+	if (env == nil)
+		env = load Env Env->PATH;
+	env->setenv(s, t);
+}
+
+stob(s : string, n : int) : array of byte
+{
+	b := array[2*n] of byte;
+	for (i := 0; i < n; i++) {
+		b[2*i] = byte (s[i]&16rff);
+		b[2*i+1] = byte ((s[i]>>8)&16rff);
+	}
+	return b;
+}
+
+btos(b : array of byte, s : ref Astring)
+{
+	n := (len b)/2;
+	for (i := 0; i < n; i++)
+		s.s[i] = int b[2*i] | ((int b[2*i+1])<<8);
+}
+
+reverse(ol : list of string) : list of string
+{
+	nl : list of string;
+
+	nl = nil;
+	while (ol != nil) {
+		nl = hd ol :: nl;
+		ol = tl ol;
+	}
+	return nl;
+}
+
+nextarg(p : ref Arg) : int
+{
+	bp : string;
+
+	if(p.av != nil){
+		bp = hd p.av;
+		if(bp != nil && bp[0] == '-'){
+			p.p = bp[1:];
+			p.av = tl p.av;
+			return 1;
+		}
+	}
+	p.p = nil;
+	return 0;
+}
+
+arginit(av : list of string) : ref Arg
+{
+	p : ref Arg;
+
+	p = ref Arg;
+	p.arg0 = hd av;
+	p.av = tl av;
+	nextarg(p);
+	return p;
+}
+
+argopt(p : ref Arg) : int
+{
+	r : int;
+
+	if(p.p == nil && nextarg(p) == 0)
+		return 0;
+	r = p.p[0];
+	p.p = p.p[1:];
+	return r;
+}
+
+argf(p : ref Arg) : string
+{
+	bp : string;
+
+	if(p.p != nil){
+		bp = p.p;
+		p.p = nil;
+	} else if(p.av != nil){
+		bp = hd p.av;
+		p.av = tl p.av;
+	} else
+		bp = nil;
+	return bp;
+}
+
+exec(cmd : string, argl : list of string)
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			# debug(sys->sprint("file %s not found\n", file));
+			sys->fprint(stderr, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(acme->acmectxt, argl);
+}
+
+getuser() : string
+{
+  	fd := sys->open("/dev/user", sys->OREAD);
+  	if(fd == nil)
+    		return "";
+
+  	buf := array[128] of byte;
+  	n := sys->read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+
+  	return string buf[0:n];	
+}
+
+gethome(usr : string) : string
+{
+	if (usr == nil)
+		usr = "tmp";
+	return "/usr/" + usr;
+}
+
+postnote(t : int, this : int, pid : int, note : string) : int
+{
+	if (pid == this || pid == 0)
+		return 0;
+	# fd := sys->open("/prog/" + string pid + "/ctl", sys->OWRITE);
+	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if (fd == nil)
+		return -1;
+	if (t == PNGROUP)
+		note += "grp";
+	sys->fprint(fd, "%s", note);
+	fd = nil;
+	return 0;
+}
+
+error(s : string)
+{
+	sys->fprint(stderr, "acme: %s: %r\n", s);
+	debug(sys->sprint("error %s : %r\n", s));
+	# s[-1] = 0;	# create broken process for debugging
+	acme->acmeexit("error");
+}
+
+dlock : ref Lock;
+dfd : ref Sys->FD;
+
+debuginit()
+{
+	if (RELEASECOPY)
+		return;
+	dfd = sys->create("./debug", Sys->OWRITE, 8r600);
+	# fd = nil;
+	dlock = Lock.init();
+}
+
+debugpr(s : string)
+{
+	if (RELEASECOPY)
+		return;
+	# fd := sys->open("./debug", Sys->OWRITE);
+	# sys->seek(fd, big 0, Sys->SEEKEND);
+	sys->fprint(dfd, "%s", s);
+	# fd = nil;
+}
+
+debug(s : string)
+{
+	if (RELEASECOPY)
+		return;
+	if (dfd == nil)
+		return;
+	dlock.lock();
+	debugpr(s);	
+	dlock.unlock();
+}
+
+memfd : ref Sys->FD;
+memb : array of byte;
+
+memdebug(s : string)
+{
+	if (RELEASECOPY)
+		return;
+	dlock.lock();
+	if (memfd == nil) {
+		sys->bind("#c", "/usr/jrf/mnt", Sys->MBEFORE);
+		memfd = sys->open("/usr/jrf/mnt/memory", Sys->OREAD);
+		memb = array[1024] of byte;
+	}
+	sys->seek(memfd, big 0, 0);
+	n := sys->read(memfd, memb, len memb);
+	if (n <= 0) {
+		dlock.unlock();
+		debug(sys->sprint("bad read %r\n"));
+		return;
+	}
+	s = s + " : " + string memb[0:n] + "\n";
+	dlock.unlock();
+	debug(s);
+	s = nil;
+}
+
+rgetc(s : string, n : int) : int
+{
+	if (n < 0 || n >= len s)
+		return 0;
+	return s[n];
+}
+
+tgetc(t : ref Text, n : int) : int
+{
+	if(n >= t.file.buf.nc)
+		return 0;
+	return t.readc(n);
+}
+
+skipbl(r : string, n : int) : (string, int)
+{
+	i : int = 0;
+
+	while(n>0 && (r[i]==' ' || r[i]=='\t' || r[i]=='\n')){
+		--n;
+		i++;
+	}
+	return (r[i:], n);
+}
+
+findbl(r : string, n : int) : (string, int)
+{
+	i : int = 0;
+
+	while(n>0 && r[i]!=' ' && r[i]!='\t' && r[i]!='\n'){
+		--n;
+		i++;
+	}
+	return (r[i:], n);
+}
+
+prevmouse : Point;
+mousew : ref Window;
+
+savemouse(w : ref Window)
+{
+	prevmouse = mouse.xy;
+	mousew = w;
+}
+
+restoremouse(w : ref Window)
+{
+	if(mousew!=nil && mousew==w)
+		cursorset(prevmouse);
+	mousew = nil;
+}
+
+clearmouse()
+{
+	mousew = nil;
+}
+
+#
+# Heuristic city.
+#
+newwindow(t : ref Text) : ref Window
+{
+	c : ref Column;
+	w, bigw, emptyw : ref Window;
+	emptyb : ref Text;
+	i, y, el : int;
+
+	if(activecol != nil)
+		c = activecol;
+	else if(seltext != nil && seltext.col != nil)
+		c = seltext.col;
+	else if(t != nil && t.col != nil)
+		c = t.col;
+	else{
+		if(row.ncol==0 && row.add(nil, -1)==nil)
+			error("can't make column");
+		c = row.col[row.ncol-1];
+	}
+	activecol = c;
+	if(t==nil || t.w==nil || c.nw==0)
+		return c.add(nil, nil, -1);
+
+	# find biggest window and biggest blank spot
+	emptyw = c.w[0];
+	bigw = emptyw;
+	for(i=1; i<c.nw; i++){
+		w = c.w[i];
+		# use >= to choose one near bottom of screen
+		if(w.body.frame.maxlines >= bigw.body.frame.maxlines)
+			bigw = w;
+		if(w.body.frame.maxlines-w.body.frame.nlines >= emptyw.body.frame.maxlines-emptyw.body.frame.nlines)
+			emptyw = w;
+	}
+	emptyb = emptyw.body;
+	el = emptyb.frame.maxlines-emptyb.frame.nlines;
+	# if empty space is big, use it
+	if(el>15 || (el>3 && el>(bigw.body.frame.maxlines-1)/2))
+		y = emptyb.frame.r.min.y+emptyb.frame.nlines*(graph->font).height;
+	else{
+		# if this window is in column and isn't much smaller, split it
+		if(t.col==c && t.w.r.dy()>2*bigw.r.dy()/3)
+			bigw = t.w;
+		y = (bigw.r.min.y + bigw.r.max.y)/2;
+	}
+	w = c.add(nil, nil, y);
+	if(w.body.frame.maxlines < 2)
+		w.col.grow(w, 1, 1);
+	return w;
+}
+
+stralloc(n : int) : ref Astring
+{
+	r := ref Astring;
+	ab := array[n] of { * => byte 'z' };
+	r.s = string ab;
+	if (len r.s != n)
+		error("bad stralloc");
+	ab = nil;
+	return r;
+}
+
+strfree(s : ref Astring)
+{
+	s.s = nil;
+	s = nil;
+}
+
+access(s : string) : int
+{
+	fd := sys->open(s, 0);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+errorwin(dir : string, ndir : int, incl : array of string, nincl : int) : ref Window
+{
+	w : ref Window;
+	r : string;
+	i, n : int;
+
+	n = ndir;
+	r = dir + "+Errors";
+	n += 7;
+	w = look->lookfile(r, n);
+	if(w == nil){
+		w = row.col[row.ncol-1].add(nil, nil, -1);
+		w.filemenu = FALSE;
+		w.setname(r, n);
+	}
+	r = nil;
+	for(i=nincl; --i>=0; )
+		w.addincl(incl[i], n);
+	return w;
+}
+
+warning(md : ref Mntdir, s : string)
+{
+	n, q0, owner : int;
+	w : ref Window;
+	t : ref Text;
+
+	debug(sys->sprint("warning %s\n", s));
+	if (row == nil) {
+		sys->fprint(sys->fildes(2), "warning: %s\n", s);
+		debug(s); 
+		debug("\n");
+		return;
+	}	
+	if(row.ncol == 0){	# really early error
+		row.init(mainwin.clipr);
+		row.add(nil, -1);
+		row.add(nil, -1);
+		if(row.ncol == 0)
+			error("initializing columns in warning()");
+	}
+	if(md != nil){
+		for(;;){
+			w = errorwin(md.dir, md.ndir, md.incl, md.nincl);
+			w.lock('E');
+			if (w.col != nil)
+				break;
+			# window was deleted too fast
+			w.unlock();
+		}
+	}else
+		w = errorwin(nil, 0, nil, 0);
+	t = w.body;
+	owner = w.owner;
+	if(owner == 0)
+		w.owner = 'E';
+	w.commit(t);
+	(q0, n) = t.bsinsert(t.file.buf.nc, s, len s, TRUE);
+	t.show(q0, q0+n, TRUE);
+	t.w.settag();
+	scrl->scrdraw(t);
+	w.owner = owner;
+	w.dirty = FALSE;
+	if(md != nil)
+		w.unlock();
+}
+
+getexc(): string
+{
+	f := "/prog/"+string sys->pctl(0, nil)+"/exception";
+	if((fd := sys->open(f, Sys->OREAD)) == nil)
+		return nil;
+	b := array[8192] of byte;
+	if((n := sys->read(fd, b, len b)) < 0)
+		return nil;
+	return string b[0: n];
+}
+
+# returns pc, module, exception
+readexc(): (int, string, string)
+{
+	s := getexc();
+	if(s == nil)
+		return (0, nil, nil);
+	(m, l) := sys->tokenize(s, " ");
+	if(m < 3)
+		return (0, nil, nil);
+	pc := int hd l;	l = tl l;
+	mod := hd l;	l = tl l;
+	exc := hd l;	l = tl l;
+	for( ; l != nil; l = tl l)
+		exc += " " + hd l;
+	return (pc, mod, exc);
+}
--- /dev/null
+++ b/appl/acme/util.m
@@ -1,0 +1,52 @@
+Utils : module {
+	PATH : con "/dis/acme/util.dis";
+
+	stderr : ref Sys->FD;
+
+	Arg : adt {
+		arg0 : string;
+		av : list of string;
+		p : string;
+	};
+
+	PNPROC, PNGROUP : con iota;
+
+	init : fn(mods : ref Dat->Mods);
+	arginit : fn(av : list of string) : ref Arg;
+	argopt : fn(p : ref Arg) : int;
+	argf : fn(p : ref Arg) : string;
+	min : fn(a : int, b : int) : int;
+	max : fn(a : int, b : int) : int;
+	abs : fn(x : int) : int;
+	error : fn(s : string);
+	warning : fn(md : ref Dat->Mntdir, t : string);
+	debuginit : fn();
+	debug : fn(s : string);
+	memdebug : fn(s : string);
+	postnote : fn(t : int, this : int, pid : int, note : string) : int;
+	exec: fn(c: string, args : list of string);
+	getuser : fn() : string;
+	gethome : fn(user : string) : string;
+	access : fn(s : string) : int;
+	isalnum : fn(c : int) : int;
+	savemouse : fn(w : ref Windowm->Window);
+	restoremouse : fn(w : ref Windowm->Window);
+	clearmouse : fn();
+	rgetc : fn(r : string, n : int) : int;
+	tgetc : fn(t : ref Textm->Text, n : int) : int;
+	reverse : fn(l : list of string) : list of string;
+	stralloc : fn(n : int) : ref Dat->Astring;
+	strfree :fn(s : ref Dat->Astring);
+	strchr : fn(s : string, c : int) : int;
+	strrchr: fn(s : string, c : int) : int;
+	strncmp : fn(s, t : string, n : int) : int;
+	getenv : fn(s : string) : string;
+	setenv : fn(s, t : string);
+	stob : fn(s : string, n : int) : array of byte;
+	btos : fn(b : array of byte, s : ref Dat->Astring);
+	findbl : fn(s : string, n : int) : (string, int);
+	skipbl : fn(s : string, n : int) : (string, int);
+	newwindow : fn(t : ref Textm->Text) : ref Windowm->Window;
+	getexc: fn(): string;
+	readexc : fn() : (int, string, string);
+};
--- /dev/null
+++ b/appl/acme/wind.b
@@ -1,0 +1,558 @@
+implement Windowm;
+
+include "common.m";
+
+sys : Sys;
+utils : Utils;
+drawm : Draw;
+graph : Graph;
+gui : Gui;
+dat : Dat;
+bufferm : Bufferm;
+textm : Textm;
+filem : Filem;
+look : Look;
+scrl : Scroll;
+acme : Acme;
+
+sprint : import sys;
+FALSE, TRUE, XXX, Astring : import Dat;
+Reffont, reffont, Lock, Ref, button, modbutton : import dat;
+Point, Rect, Image : import drawm;
+min, max, error, warning, stralloc, strfree : import utils;
+font, draw : import graph;
+black, white, mainwin : import gui;
+Buffer : import bufferm;
+Body, Text, Tag : import textm;
+File : import filem;
+Xfid : import Xfidm;
+scrdraw : import scrl;
+tagcols, textcols : import acme;
+BORD : import Framem;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	utils = mods.utils;
+	drawm = mods.draw;
+	graph = mods.graph;
+	gui = mods.gui;
+	textm = mods.textm;
+	filem = mods.filem;
+	bufferm = mods.bufferm;
+	look = mods.look;
+	scrl = mods.scroll;
+	acme = mods.acme;
+}
+
+winid : int;
+nullwin : Window;
+
+Window.init(w : self ref Window, clone : ref Window, r : Rect)
+{
+	r1, br : Rect;
+	f : ref File;
+	rf : ref Reffont;
+	rp : ref Astring;
+	nc : int;
+	dummy : ref File = nil;
+
+	c := w.col;
+	*w = nullwin;
+	w.col = c;
+	w.nopen = array[Dat->QMAX] of byte;
+	for (i := 0; i < Dat->QMAX; i++)
+		w.nopen[i] = byte 0;
+	w.qlock = Lock.init();
+	w.ctllock = Lock.init();
+	w.refx = Ref.init();
+	w.tag = textm->newtext();
+	w.tag.w = w;
+	w.body = textm->newtext();
+	w.body.w = w;
+	w.id = ++winid;
+	w.refx.inc();
+	w.ctlfid = ~0;
+	w.utflastqid = -1;
+	r1 = r;
+	r1.max.y = r1.min.y + font.height;
+	reffont.r.inc();
+	f = dummy.addtext(w.tag);
+	w.tag.init(f, r1, reffont, tagcols);
+	w.tag.what = Tag;
+	# tag is a copy of the contents, not a tracked image 
+	if(clone != nil){
+		w.tag.delete(0, w.tag.file.buf.nc, TRUE);
+		nc = clone.tag.file.buf.nc;
+		rp = utils->stralloc(nc);
+		clone.tag.file.buf.read(0, rp, 0, nc);
+		w.tag.insert(0, rp.s, nc, TRUE, 0);
+		utils->strfree(rp);
+		rp = nil;
+		w.tag.file.reset();
+		w.tag.setselect(nc, nc);
+	}
+	r1 = r;
+	r1.min.y += font.height + 1;
+	if(r1.max.y < r1.min.y)
+		r1.max.y = r1.min.y;
+	f = nil;
+	if(clone != nil){
+		f = clone.body.file;
+		w.body.org = clone.body.org;
+		w.isscratch = clone.isscratch;
+		rf = Reffont.get(FALSE, FALSE, FALSE, clone.body.reffont.f.name);
+	}else
+		rf = Reffont.get(FALSE, FALSE, FALSE, nil);
+	f = f.addtext(w.body);
+	w.body.what = Body;
+	w.body.init(f, r1, rf, textcols);
+	r1.min.y -= 1;
+	r1.max.y = r1.min.y+1;
+	draw(mainwin, r1, tagcols[BORD], nil, (0, 0));
+	scrdraw(w.body);
+	w.r = r;
+	w.r.max.y = w.body.frame.r.max.y;
+	br.min = w.tag.scrollr.min;
+	br.max.x = br.min.x + button.r.dx();
+	br.max.y = br.min.y + button.r.dy();
+	draw(mainwin, br, button, nil, button.r.min);
+	w.filemenu = TRUE;
+	w.maxlines = w.body.frame.maxlines;
+	if(clone != nil){
+		w.dirty = clone.dirty;
+		w.body.setselect(clone.body.q0, clone.body.q1);
+		w.settag();
+	}
+}
+
+Window.reshape(w : self ref Window, r : Rect, safe : int) : int
+{
+	r1, br : Rect;
+	y : int;
+	b : ref Image;
+
+	r1 = r;
+	r1.max.y = r1.min.y + font.height;
+	y = r1.max.y;
+	if(!safe || !w.tag.frame.r.eq(r1)){
+		y = w.tag.reshape(r1);
+		b = button;
+		if(w.body.file.mod && !w.isdir && !w.isscratch)
+			b = modbutton;
+		br.min = w.tag.scrollr.min;
+		br.max.x = br.min.x + b.r.dx();
+		br.max.y = br.min.y + b.r.dy();
+		draw(mainwin, br, b, nil, b.r.min);
+	}
+	if(!safe || !w.body.frame.r.eq(r1)){
+		if(y+1+font.height > r.max.y){		# no body 
+			r1.min.y = y;
+			r1.max.y = y;
+			w.body.reshape(r1);
+			w.r = r;
+			w.r.max.y = y;
+			return y;
+		}
+		r1 = r;
+		r1.min.y = y;
+		r1.max.y = y + 1;
+		draw(mainwin, r1, tagcols[BORD], nil, (0, 0));
+		r1.min.y = y + 1;
+		r1.max.y = r.max.y;
+		y = w.body.reshape(r1);
+		w.r = r;
+		w.r.max.y = y;
+		scrdraw(w.body);
+	}
+	w.maxlines = min(w.body.frame.nlines, max(w.maxlines, w.body.frame.maxlines));
+	return w.r.max.y;
+}
+
+Window.lock1(w : self ref Window, owner : int)
+{
+	w.refx.inc();
+	w.qlock.lock();
+	w.owner = owner;
+}
+
+Window.lock(w : self ref Window, owner : int)
+{
+	i : int;
+	f : ref File;
+
+	f = w.body.file;
+	for(i=0; i<f.ntext; i++)
+		f.text[i].w.lock1(owner);
+}
+
+Window.unlock(w : self ref Window)
+{
+	f := w.body.file;
+	#
+	# subtle: loop runs backwards to avoid tripping over
+	# winclose indirectly editing f.text and freeing f
+	# on the last iteration of the loop
+	#
+	for(i:=f.ntext-1; i>=0; i--){
+		w = f.text[i].w;
+		w.owner = 0;
+		w.qlock.unlock();
+		w.close();
+	}
+}
+
+Window.mousebut(w : self ref Window)
+{
+	graph->cursorset(w.tag.scrollr.min.add(w.tag.scrollr.max).div(2));
+}
+
+Window.dirfree(w : self ref Window)
+{
+	i : int;
+	dl : ref Dat->Dirlist;
+
+	if(w.isdir){
+		for(i=0; i<w.ndl; i++){
+			dl = w.dlp[i];
+			dl.r = nil;
+			dl = nil;
+		}
+	}
+	w.dlp = nil;
+	w.ndl = 0;
+}
+
+Window.close(w : self ref Window)
+{
+	i : int;
+
+	if(w.refx.dec() == 0){
+		w.dirfree();
+		w.tag.close();
+		w.body.close();
+		if(dat->activewin == w)
+			dat->activewin = nil;
+		for(i=0; i<w.nincl; i++)
+			w.incl[i] = nil;
+		w.incl = nil;
+		w.events = nil;
+		w = nil;
+	}
+}
+
+Window.delete(w : self ref Window)
+{
+	x : ref Xfid;
+
+	x = w.eventx;
+	if(x != nil){
+		w.nevents = 0;
+		w.events = nil;
+		w.eventx = nil;
+		x.c <-= Xfidm->Xnil;
+	}
+}
+
+Window.undo(w : self ref Window, isundo : int)
+{
+	body : ref Text;
+	i : int;
+	f : ref File;
+	v : ref Window;
+
+	if(w==nil)
+		return;
+	w.utflastqid = -1;
+	body = w.body;
+	(body.q0, body.q1) = body.file.undo(isundo, body.q0, body.q1);
+	body.show(body.q0, body.q1, TRUE);
+	f = body.file;
+	for(i=0; i<f.ntext; i++){
+		v = f.text[i].w;
+		v.dirty = (f.seq != v.putseq);
+		if(v != w){
+			v.body.q0 = v.body.frame.p0+v.body.org;
+			v.body.q1 = v.body.frame.p1+v.body.org;
+		}
+	}
+	w.settag();
+}
+
+Window.setname(w : self ref Window, name : string, n : int)
+{
+	t : ref Text;
+	v : ref Window;
+	i : int;
+
+	t = w.body;
+	if(t.file.name == name)
+		return;
+	w.isscratch = FALSE;
+	if(n>=6 && name[n-6:n] == "/guide")
+		w.isscratch = TRUE;
+	else if(n>=7 && name[n-7:n] == "+Errors")
+		w.isscratch = TRUE;
+	t.file.setname(name, n);
+	for(i=0; i<t.file.ntext; i++){
+		v = t.file.text[i].w;
+		v.settag();
+		v.isscratch = w.isscratch;
+	}
+}
+
+Window.typex(w : self ref Window, t : ref Text, r : int)
+{
+	i : int;
+
+	t.typex(r, w.echomode);
+	if(t.what == Body)
+		for(i=0; i<t.file.ntext; i++)
+			scrdraw(t.file.text[i]);
+	w.settag();
+}
+
+Window.cleartag(w : self ref Window)
+{
+	i, n : int;
+	r : ref Astring;
+
+	# w must be committed 
+	n = w.tag.file.buf.nc;
+	r = utils->stralloc(n);
+	w.tag.file.buf.read(0, r, 0, n);
+	for(i=0; i<n; i++)
+		if(r.s[i]==' ' || r.s[i]=='\t')
+			break;
+	for(; i<n; i++)
+		if(r.s[i] == '|')
+			break;
+	if(i == n)
+		return;
+	i++;
+	w.tag.delete(i, n, TRUE);
+	utils->strfree(r);
+	r = nil;
+	w.tag.file.mod = FALSE;
+	if(w.tag.q0 > i)
+		w.tag.q0 = i;
+	if(w.tag.q1 > i)
+		w.tag.q1 = i;
+	w.tag.setselect(w.tag.q0, w.tag.q1);
+}
+
+Window.settag(w : self ref Window)
+{
+	i : int;
+	f : ref File;
+
+	f = w.body.file;
+	for(i=0; i<f.ntext; i++){
+		v := f.text[i].w;
+		if(v.col.safe || v.body.frame.maxlines>0)
+			v.settag1();
+	}
+}
+
+Window.settag1(w : self ref Window)
+{
+	ii, j, k, n, bar, dirty : int;
+	old : ref Astring;
+	new : string;
+	r : int;
+	b : ref Image;
+	q0, q1 : int;
+	br : Rect;
+
+	if(w.tag.ncache!=0 || w.tag.file.mod)
+		w.commit(w.tag);	# check file name; also can now modify tag
+	old = utils->stralloc(w.tag.file.buf.nc);
+	w.tag.file.buf.read(0, old, 0, w.tag.file.buf.nc);
+	for(ii=0; ii<w.tag.file.buf.nc; ii++)
+		if(old.s[ii]==' ' || old.s[ii]=='\t')
+			break;
+	if(old.s[0:ii] != w.body.file.name){
+		w.tag.delete(0, ii, TRUE);
+		w.tag.insert(0, w.body.file.name, len w.body.file.name, TRUE, 0);
+		strfree(old);
+		old = nil;
+		old = utils->stralloc(w.tag.file.buf.nc);
+		w.tag.file.buf.read(0, old, 0, w.tag.file.buf.nc);
+	}
+	new = w.body.file.name + " Del Snarf";
+	if(w.filemenu){
+		if(w.body.file.delta.nc>0 || w.body.ncache)
+			new += " Undo";
+		if(w.body.file.epsilon.nc > 0)
+			new += " Redo";
+		dirty = w.body.file.name != nil && (w.body.ncache || w.body.file.seq!=w.putseq);
+		if(!w.isdir && dirty)
+			new += " Put";
+	}
+	if(w.isdir)
+		new += " Get";
+	l := len w.body.file.name;
+	if(l >= 2 && w.body.file.name[l-2: ] == ".b")
+		new += " Limbo";
+	new += " |";
+	r = utils->strchr(old.s, '|');
+	if(r >= 0)
+		k = r+1;
+	else{
+		k = w.tag.file.buf.nc;
+		if(w.body.file.seq == 0)
+			new += " Look ";
+	}
+	if(new != old.s[0:k]){
+		n = k;
+		if(n > len new)
+			n = len new;
+		for(j=0; j<n; j++)
+			if(old.s[j] != new[j])
+				break;
+		q0 = w.tag.q0;
+		q1 = w.tag.q1;
+		w.tag.delete(j, k, TRUE);
+		w.tag.insert(j, new[j:], len new - j, TRUE, 0);
+		# try to preserve user selection 
+		r = utils->strchr(old.s, '|');
+		if(r >= 0){
+			bar = r;
+			if(q0 > bar){
+				bar = utils->strchr(new, '|')-bar;
+				w.tag.q0 = q0+bar;
+				w.tag.q1 = q1+bar;
+			}
+		}
+	}
+	strfree(old);
+	old = nil;
+	new = nil;
+	w.tag.file.mod = FALSE;
+	n = w.tag.file.buf.nc+w.tag.ncache;
+	if(w.tag.q0 > n)
+		w.tag.q0 = n;
+	if(w.tag.q1 > n)
+		w.tag.q1 = n;
+	w.tag.setselect(w.tag.q0, w.tag.q1);
+	b = button;
+	if(!w.isdir && !w.isscratch && (w.body.file.mod || w.body.ncache))
+		b = modbutton;
+	br.min = w.tag.scrollr.min;
+	br.max.x = br.min.x + b.r.dx();
+	br.max.y = br.min.y + b.r.dy();
+	draw(mainwin, br, b, nil, b.r.min);
+}
+
+Window.commit(w : self ref Window, t : ref Text)
+{
+	r : ref Astring;
+	i : int;
+	f : ref File;
+
+	t.commit(TRUE);
+	f = t.file;
+	if(f.ntext > 1)
+		for(i=0; i<f.ntext; i++)
+			f.text[i].commit(FALSE);	# no-op for t 
+	if(t.what == Body)
+		return;
+	r = utils->stralloc(w.tag.file.buf.nc);
+	w.tag.file.buf.read(0, r, 0, w.tag.file.buf.nc);
+	for(i=0; i<w.tag.file.buf.nc; i++)
+		if(r.s[i]==' ' || r.s[i]=='\t')
+			break;
+	if(r.s[0:i] != w.body.file.name){
+		dat->seq++;
+		w.body.file.mark();
+		w.body.file.mod = TRUE;
+		w.dirty = TRUE;
+		w.setname(r.s, i);
+		w.settag();
+	}
+	utils->strfree(r);
+	r = nil;
+}
+
+Window.addincl(w : self ref Window, r : string, n : int)
+{
+	{
+		(ok, d) := sys->stat(r);
+		if(ok < 0){
+			if(r[0] == '/')
+				raise "e";
+			(r, n) = look->dirname(w.body, r, n);
+			(ok, d) = sys->stat(r);
+			if(ok < 0)
+				raise "e";
+		}
+		if((d.mode&Sys->DMDIR) == 0){
+			warning(nil, sprint("%s: not a directory\n", r));
+			r = nil;
+			return;
+		}
+		w.nincl++;
+		owi := w.incl;
+		w.incl = array[w.nincl] of string;
+		w.incl[1:] = owi[0:w.nincl-1];
+		owi = nil;
+		w.incl[0] = r;
+		r = nil;
+	}
+	exception{
+		* =>
+			warning(nil, sprint("%s: %r\n", r));
+			r = nil;
+	}
+}
+
+Window.clean(w : self ref Window, conservative : int, exiting : int) : int	# as it stands, conservative is always TRUE 
+{
+	if(w.isscratch || w.isdir)	# don't whine if it's a guide file, error window, etc. 
+		return TRUE;
+	if((!conservative||exiting) && w.nopen[Dat->QWevent]>byte 0)
+		return TRUE;
+	if(w.dirty){
+		if(w.body.file.name != nil)
+			warning(nil, sprint("%s modified\n", w.body.file.name));
+		else{
+			if(w.body.file.buf.nc < 100)	# don't whine if it's too small 
+				return TRUE;
+			warning(nil, "unnamed file modified\n");
+		}
+		w.dirty = FALSE;
+		return FALSE;
+	}
+	return TRUE;
+}
+
+Window.ctlprint(w : self ref Window, fonts : int) : string
+{
+	s := sprint("%11d %11d %11d %11d %11d ", w.id, w.tag.file.buf.nc,
+			w.body.file.buf.nc, w.isdir, w.dirty);
+	if(fonts)
+		return sprint("%s%11d %q %11d ", s, w.body.frame.r.dx(),
+			w.body.reffont.f.name, w.body.frame.maxtab);
+	return s;
+}
+
+Window.event(w : self ref Window, fmt : string)
+{
+	n : int;
+	x : ref Xfid;
+
+	if(w.nopen[Dat->QWevent] == byte 0)
+		return;
+	if(w.owner == 0)
+		error("no window owner");
+	n = len fmt;
+	w.events[len w.events] = w.owner;
+	w.events += fmt;
+	w.nevents += n+1;
+	x = w.eventx;
+	if(x != nil){
+		w.eventx = nil;
+		x.c <-= Xfidm->Xnil;
+	}
+}
--- /dev/null
+++ b/appl/acme/wind.m
@@ -1,0 +1,67 @@
+Windowm : module {
+	PATH : con "/dis/acme/wind.dis";
+
+	init : fn(mods : ref Dat->Mods);
+
+	Window : adt {
+		qlock : ref Dat->Lock;
+		refx : ref Dat->Ref;
+		tag : 	cyclic ref Textm->Text;
+		body : cyclic ref Textm->Text;
+		r : Draw->Rect;
+		isdir : int;
+		isscratch : int;
+		filemenu : int;
+		dirty : int;
+		id : int;
+		addr : Dat->Range;
+		limit : Dat->Range;
+		nopen : array of byte;
+		nomark : int;
+		noscroll : int;
+		echomode : int;
+		wrselrange : Dat->Range;
+		rdselfd : ref Sys->FD;
+		col : cyclic ref Columnm->Column;
+		eventx : cyclic ref Xfidm->Xfid;
+		events : string;
+		nevents : int;
+		owner : int;
+		maxlines :	int;
+		dlp : array of ref Dat->Dirlist;
+		ndl : int;
+		putseq : int;
+		nincl : int;
+		incl : array of string;
+		reffont : ref Dat->Reffont;
+		ctllock : ref Dat->Lock;
+		ctlfid : int;
+		dumpstr : string;
+		dumpdir : string;
+		dumpid : int;
+		utflastqid : int;
+		utflastboff : int;
+		utflastq : int;
+
+		init : fn(w : self ref Window, w0 : ref Window, r : Draw->Rect);
+		lock : fn(w : self ref Window, n : int);
+		lock1 : fn(w : self ref Window, n : int);
+		unlock : fn(w : self ref Window);
+		typex : fn(w : self ref Window, t : ref Textm->Text, r : int);
+		undo : fn(w : self ref Window, n : int);
+		setname : fn(w : self ref Window, r : string, n : int);
+		settag : fn(w : self ref Window);
+		settag1 : fn(w : self ref Window);
+		commit : fn(w : self ref Window, t : ref Textm->Text);
+		reshape : fn(w : self ref Window, r : Draw->Rect, n : int) : int;
+		close : fn(w : self ref Window);
+		delete : fn(w : self ref Window);
+		clean : fn(w : self ref Window, n : int, exiting : int) : int;
+		dirfree : fn(w : self ref Window);
+		event : fn(w : self ref Window, b : string);
+		mousebut : fn(w : self ref Window);
+		addincl : fn(w : self ref Window, r : string, n : int);
+		cleartag : fn(w : self ref Window);
+		ctlprint : fn(w : self ref Window, fonts : int) : string;
+	};
+};
--- /dev/null
+++ b/appl/acme/xfid.b
@@ -1,0 +1,1087 @@
+implement Xfidm;
+
+include "common.m";
+
+sys : Sys;
+dat : Dat;
+graph : Graph;
+utils : Utils;
+regx : Regx;
+bufferm : Bufferm;
+diskm : Diskm;
+filem : Filem;
+textm : Textm;
+columnm : Columnm;
+scrl : Scroll;
+look : Look;
+exec : Exec;
+windowm : Windowm;
+fsys : Fsys;
+editm: Edit;
+ecmd: Editcmd;
+styxaux: Styxaux;
+
+UTFmax : import Sys;
+sprint : import sys;
+Smsg0 : import Dat;
+TRUE, FALSE, XXX, BUFSIZE, MAXRPC : import Dat;
+EM_NORMAL, EM_RAW, EM_MASK : import Dat;
+Qdir, Qcons, Qlabel, Qindex, Qeditout : import Dat;
+QWaddr, QWdata, QWevent, QWconsctl, QWctl, QWbody, QWeditout, QWtag, QWrdsel, QWwrsel : import Dat;
+seq, cxfidfree, Lock, Ref, Range, Mntdir, Astring : import dat;
+error, warning, max, min, stralloc, strfree, strncmp : import utils;
+address : import regx;
+Buffer : import bufferm;
+File : import filem;
+Text : import textm;
+scrdraw : import scrl;
+Window : import windowm;
+bflush : import graph;
+Column : import columnm;
+row : import dat;
+FILE, QID, respond : import fsys;
+oldtag, name, offset, count, data, setcount, setdata : import styxaux;
+
+init(mods : ref Dat->Mods)
+{
+	sys = mods.sys;
+	dat = mods.dat;
+	graph = mods.graph;
+	utils = mods.utils;
+	regx = mods.regx;
+	filem = mods.filem;
+	bufferm = mods.bufferm;
+	diskm = mods.diskm;
+	textm = mods.textm;
+	columnm = mods.columnm;
+	scrl = mods.scroll;
+	look = mods.look;
+	exec = mods.exec;
+	windowm = mods.windowm;
+	fsys = mods.fsys;
+	editm = mods.edit;
+	ecmd = mods.editcmd;
+	styxaux = mods.styxaux;
+}
+
+nullxfid : Xfid;
+
+newxfid() : ref Xfid
+{
+	x := ref Xfid;
+	*x = nullxfid;
+	x.buf = array[fsys->messagesize+UTFmax] of byte;
+	return x;
+}
+
+Ctlsize : con 5*12;
+
+Edel		:= "deleted window";
+Ebadctl	:= "ill-formed control message";
+Ebadaddr	:= "bad address syntax";
+Eaddr	:= "address out of range";
+Einuse	:= "already in use";
+Ebadevent:= "bad event syntax";
+
+clampaddr(w : ref Window)
+{
+	if(w.addr.q0 < 0)
+		w.addr.q0 = 0;
+	if(w.addr.q1 < 0)
+		w.addr.q1 = 0;
+	if(w.addr.q0 > w.body.file.buf.nc)
+		w.addr.q0 = w.body.file.buf.nc;
+	if(w.addr.q1 > w.body.file.buf.nc)
+		w.addr.q1 = w.body.file.buf.nc;
+}
+
+xfidtid : array of int;
+nxfidtid := 0;
+
+xfidkill()
+{
+	if (sys == nil)
+		return;
+	thispid := sys->pctl(0, nil);
+	for (i := 0; i < nxfidtid; i++)
+		utils->postnote(Utils->PNPROC, thispid, xfidtid[i], "kill");
+}
+
+Xfid.ctl(x : self ref Xfid)
+{
+	x.tid = sys->pctl(0, nil);
+	ox := xfidtid;
+	xfidtid = array[nxfidtid+1] of int;
+	xfidtid[0:] = ox[0:nxfidtid];
+	xfidtid[nxfidtid++] = x.tid;
+	ox = nil;
+	for (;;) {
+		f := <- x.c;
+		case (f) {
+			Xnil => ;
+			Xflush => x.flush();
+			Xwalk => x.walk(nil);
+			Xopen => x.open();
+			Xclose => x.close();
+			Xread => x.read();
+			Xwrite => x.write();
+			* =>		error("bad case in Xfid.ctl()");
+		}
+		bflush();
+		cxfidfree <-= x;
+	}
+}
+ 
+Xfid.flush(x : self ref Xfid)
+{
+	fc : Smsg0;
+	i, j : int;
+	w : ref Window;
+	c : ref Column;
+	wx : ref Xfid;
+
+	# search windows for matching tag
+	row.qlock.lock();
+loop:
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c.nw; i++){
+			w = c.w[i];
+			w.lock('E');
+			wx = w.eventx;
+			if(wx!=nil && wx.fcall.tag==oldtag(x.fcall)){
+				w.eventx = nil;
+				wx.flushed = TRUE;
+				wx.c <-= Xnil;
+				w.unlock();
+				break loop;
+			}
+			w.unlock();
+		}
+	}
+	row.qlock.unlock();
+	respond(x, fc, nil);
+}
+ 
+Xfid.walk(nil : self ref Xfid, cw: chan of ref Window)
+{
+	# fc : Smsg0;
+	w : ref Window;
+
+	# if(name(x.fcall) != "new")
+	#	error("unknown path in walk\n");
+	row.qlock.lock();	# tasks->procs now
+	w = utils->newwindow(nil);
+	w.settag();
+	# w.refx.inc();
+	# x.f.w = w;
+	# x.f.qid.path = big QID(w.id, Qdir);
+	# x.f.qid.qtype = Sys->QTDIR;
+	# fc.qid = x.f.qid;
+	row.qlock.unlock();
+	# respond(x, fc, nil);
+	cw <-= w;
+}
+ 
+Xfid.open(x : self ref Xfid)
+{
+	fc : Smsg0;
+	w : ref Window;
+	q : int;
+
+	fc.iounit = 0;
+	w = x.f.w;
+	if(w != nil){
+		t := w.body;
+		row.qlock.lock();	# tasks->procs now
+		w.lock('E');
+		q = FILE(x.f.qid);
+		case(q){
+		QWaddr or QWdata or QWevent =>
+			if(w.nopen[q]++ == byte 0){
+				if(q == QWaddr){
+					w.addr = (Range)(0,0);
+					w.limit = (Range)(-1,-1);
+				}
+				if(q==QWevent && !w.isdir && w.col!=nil){
+					w.filemenu = FALSE;
+					w.settag();
+				}
+			}
+		QWrdsel =>
+			#
+			# Use a temporary file.
+			# A pipe would be the obvious, but we can't afford the
+			# broken pipe notification.  Using the code to read QWbody
+			# is n², which should probably also be fixed.  Even then,
+			# though, we'd need to squirrel away the data in case it's
+			# modified during the operation, e.g. by |sort
+			#
+			if(w.rdselfd != nil){
+				w.unlock();
+				respond(x, fc, Einuse);
+				return;
+			}
+			w.rdselfd = diskm->tempfile();
+			if(w.rdselfd == nil){
+				w.unlock();
+				respond(x, fc, "can't create temp file");
+				return;
+			}
+			w.nopen[q]++;
+			q0 := t.q0;
+			q1 := t.q1;
+			r := utils->stralloc(BUFSIZE);
+			while(q0 < q1){
+				n := q1 - q0;
+				if(n > BUFSIZE)
+					n = BUFSIZE;
+				t.file.buf.read(q0, r, 0, n);
+				s := array of byte r.s[0:n];
+				m := len s;
+				if(sys->write(w.rdselfd, s, m) != m){
+					warning(nil, "can't write temp file for pipe command %r\n");
+					break;
+				}
+				s = nil;
+				q0 += n;
+			}
+			utils->strfree(r);
+		QWwrsel =>
+			w.nopen[q]++;
+			seq++;
+			t.file.mark();
+			exec->cut(t, t, FALSE, TRUE);
+			w.wrselrange = (Range)(t.q1, t.q1);
+			w.nomark = TRUE;
+		QWeditout =>
+			if(editm->editing == FALSE){
+				w.unlock();
+				respond(x, fc, "permission denied");
+				return;
+			}
+			w.wrselrange = (Range)(t.q1, t.q1);
+			break;
+		}
+		w.unlock();
+		row.qlock.unlock();
+	}
+	fc.qid = x.f.qid;
+	fc.iounit = fsys->messagesize-Styx->IOHDRSZ;
+	x.f.open = TRUE;
+	respond(x, fc, nil);
+}
+ 
+Xfid.close(x : self ref Xfid)
+{
+	fc : Smsg0;
+	w : ref Window;
+	q : int;
+
+	w = x.f.w;
+	# BUG in C version ? fsysclunk() has just set busy, open to FALSE
+	# x.f.busy = FALSE;
+	# if(!x.f.open){
+	#	if(w != nil)
+	#		w.close();
+	#	respond(x, fc, nil);
+	#	return;
+	# }
+	# x.f.open = FALSE;
+	if(w != nil){
+		row.qlock.lock();	# tasks->procs now 
+		w.lock('E');
+		q = FILE(x.f.qid);
+		case(q){
+		QWctl =>
+			if(w.ctlfid!=~0 && w.ctlfid==x.f.fid){
+				w.ctlfid = ~0;
+				w.ctllock.unlock();
+			}
+		QWdata or QWaddr or QWevent =>	
+			# BUG: do we need to shut down Xfid?
+			if (q == QWdata)
+				w.nomark = FALSE;
+			if(--w.nopen[q] == byte 0){
+				if(q == QWdata)
+					w.nomark = FALSE;
+				if(q==QWevent && !w.isdir && w.col!=nil){
+					w.filemenu = TRUE;
+					w.settag();
+				}
+				if(q == QWevent){
+					w.dumpstr = nil;
+					w.dumpdir = nil;
+				}
+			}
+		QWrdsel =>
+			w.rdselfd = nil;
+		QWwrsel =>
+			w.nomark = FALSE;
+			t :=w.body;
+			# before: only did this if !w->noscroll, but that didn't seem right in practice
+			t.show(min(w.wrselrange.q0, t.file.buf.nc),
+				    min(w.wrselrange.q1, t.file.buf.nc), TRUE);
+			scrdraw(t);
+		QWconsctl=>
+			w.echomode = EM_NORMAL;
+		}
+		w.close();
+		w.unlock();
+		row.qlock.unlock();
+	}
+	respond(x, fc, nil);
+}
+ 
+Xfid.read(x : self ref Xfid)
+{
+	fc : Smsg0;
+	n, q : int;
+	off : int;
+	sbuf : string;
+	buf : array of byte;
+	w : ref Window;
+
+	sbuf = nil;
+	q = FILE(x.f.qid);
+	w = x.f.w;
+	if(w == nil){
+		fc.count = 0;
+		case(q){
+		Qcons or Qlabel =>
+			;
+		Qindex =>
+			x.indexread();
+			return;
+		* =>
+			warning(nil, sprint("unknown qid %d\n", q));
+		}
+		respond(x, fc, nil);
+		return;
+	}
+	w.lock('F');
+	if(w.col == nil){
+		w.unlock();
+		respond(x, fc, Edel);
+		return;
+	}
+	off = int offset(x.fcall);	
+	case(q){
+	QWaddr =>
+		w.body.commit(TRUE);
+		clampaddr(w);
+		sbuf = sprint("%11d %11d ", w.addr.q0, w.addr.q1);
+	QWbody =>
+		x.utfread(w.body, 0, w.body.file.buf.nc, QWbody);
+	QWctl =>
+		sbuf = w.ctlprint(1);
+	QWevent =>
+		x.eventread(w);
+	QWdata =>
+		# BUG: what should happen if q1 > q0?
+		if(w.addr.q0 > w.body.file.buf.nc){
+			respond(x, fc, Eaddr);
+			break;
+		}
+		w.addr.q0 += x.runeread(w.body, w.addr.q0, w.body.file.buf.nc);
+		w.addr.q1 = w.addr.q0;
+	QWtag =>
+		x.utfread(w.tag, 0, w.tag.file.buf.nc, QWtag);
+	QWrdsel =>
+		sys->seek(w.rdselfd, big off, 0);
+		n = count(x.fcall);
+		if(n > BUFSIZE)
+			n = BUFSIZE;
+		b := array[n] of byte;
+		n = sys->read(w.rdselfd, b, n);
+		if(n < 0){
+			respond(x, fc, "I/O error in temp file");
+			break;
+		}
+		fc.count = n;
+		fc.data = b;
+		respond(x, fc, nil);
+		b = nil;
+	* =>
+		sbuf = sprint("unknown qid %d in read", q);
+		respond(x, fc, sbuf);
+		sbuf = nil;
+	}
+	if (sbuf != nil) {
+		buf = array of byte sbuf;
+		sbuf = nil;
+		n = len buf;
+		if(off > n)
+			off = n;
+		if(off+count(x.fcall) > n)
+			setcount(x.fcall, n-off);
+		fc.count = count(x.fcall);
+		fc.data = buf[off:];
+		respond(x, fc, nil);
+		buf = nil;
+	}
+	w.unlock();
+}
+ 
+Xfid.write(x : self ref Xfid)
+{
+	fc  : Smsg0;
+	c, cnt, qid, q, nb, nr, eval : int;
+	w : ref Window;
+	r : string;
+	a : Range;
+	t : ref Text;
+	q0, tq0, tq1 : int;
+	md : ref Mntdir;
+
+	qid = FILE(x.f.qid);
+	w = x.f.w;
+	row.qlock.lock();	# tasks->procs now
+	if(w != nil){
+		c = 'F';
+		if(qid==QWtag || qid==QWbody)
+			c = 'E';
+		w.lock(c);
+		if(w.col == nil){
+			w.unlock();
+			row.qlock.unlock();
+			respond(x, fc, Edel);
+			return;
+		}
+	}
+	bodytag := 0;
+	case(qid){
+	Qcons =>
+		md = x.f.mntdir;
+		warning(md, string data(x.fcall));
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	QWconsctl =>
+		if (w != nil) {
+			r = string data(x.fcall);
+			if (strncmp(r, "rawon", 5) == 0)
+				w.echomode = EM_RAW;
+			else if (strncmp(r, "rawoff", 6) == 0)
+				w.echomode = EM_NORMAL;
+		}
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	Qlabel =>
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	QWaddr =>
+		r = string data(x.fcall);
+		nr = len r;
+		t = w.body;
+		w.commit(t);
+		(eval, nb, a) = address(x.f.mntdir, t, w.limit, w.addr, nil, r, 0, nr, TRUE);
+		r = nil;
+		if(nb < nr){
+			respond(x, fc, Ebadaddr);
+			break;
+		}
+		if(!eval){
+			respond(x, fc, Eaddr);
+			break;
+		}
+		w.addr = a;
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	Qeditout or
+	QWeditout =>
+		r = string data(x.fcall);
+		nr = len r;
+		if(w!=nil)
+			err := ecmd->edittext(w.body.file, w.wrselrange.q1, r, nr);
+		else
+			err = ecmd->edittext(nil, 0, r, nr);
+		r = nil;
+		if(err != nil){
+			respond(x, fc, err);
+			break;
+		}
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+		break;
+	QWbody or QWwrsel =>
+		t = w.body;
+		bodytag = 1;
+	QWctl =>
+		x.ctlwrite(w);
+	QWdata =>
+		t = w.body;
+		w.commit(t);
+		if(w.addr.q0>t.file.buf.nc || w.addr.q1>t.file.buf.nc){
+			respond(x, fc, Eaddr);
+			break;
+		}
+		nb = sys->utfbytes(data(x.fcall), count(x.fcall));
+		r = string data(x.fcall)[0:nb];
+		nr = len r;
+		if(w.nomark == FALSE){
+			seq++;
+			t.file.mark();
+		}
+		q0 = w.addr.q0;
+		if(w.addr.q1 > q0){
+			t.delete(q0, w.addr.q1, TRUE);
+			w.addr.q1 = q0;
+		}
+		tq0 = t.q0;
+		tq1 = t.q1;
+		t.insert(q0, r, nr, TRUE, 0);
+		if(tq0 >= q0)
+			tq0 += nr;
+		if(tq1 >= q0)
+			tq1 += nr;
+		if(!t.w.noscroll)
+			t.show(tq0, tq1, TRUE);
+		scrdraw(t);
+		w.settag();
+		r = nil;
+		w.addr.q0 += nr;
+		w.addr.q1 = w.addr.q0;
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	QWevent =>
+		x.eventwrite(w);
+	QWtag =>
+		t = w.tag;
+		bodytag = 1;
+	* =>
+		r = sprint("unknown qid %d in write", qid);
+		respond(x, fc, r);
+		r = nil;
+	}
+	if (bodytag) {
+		q = x.f.nrpart;
+		cnt = count(x.fcall);
+		if(q > 0){
+			nd := array[cnt+q] of byte;
+			nd[q:] = data(x.fcall)[0:cnt];
+			nd[0:] = x.f.rpart[0:q];
+			setdata(x.fcall, nd);
+			cnt += q;
+			x.f.nrpart = 0;
+		}
+		nb = sys->utfbytes(data(x.fcall), cnt);
+		r = string data(x.fcall)[0:nb];
+		nr = len r;
+		if(nb < cnt){
+			x.f.rpart = data(x.fcall)[nb:cnt];
+			x.f.nrpart = cnt-nb;
+		}
+		if(nr > 0){
+			t.w.commit(t);
+			if(qid == QWwrsel){
+				q0 = w.wrselrange.q1;
+				if(q0 > t.file.buf.nc)
+					q0 = t.file.buf.nc;
+			}else
+				q0 = t.file.buf.nc;
+			if(qid == QWbody || qid == QWwrsel){
+				if(!w.nomark){
+					seq++;
+					t.file.mark();
+				}
+				(q0, nr) = t.bsinsert(q0, r, nr, TRUE);
+				if(qid!=QWwrsel && !t.w.noscroll)
+					t.show(q0+nr, q0+nr, TRUE);
+				scrdraw(t);
+			}else
+				t.insert(q0, r, nr, TRUE, 0);
+			w.settag();
+			if(qid == QWwrsel)
+				w.wrselrange.q1 += nr;
+			r = nil;
+		}
+		fc.count = count(x.fcall);
+		respond(x, fc, nil);
+	}
+	if(w != nil)
+		w.unlock();
+	row.qlock.unlock();
+}
+
+Xfid.ctlwrite(x : self ref Xfid, w : ref Window)
+{
+	fc : Smsg0;
+	i, m, n, nb : int;
+	r, err, p, pp : string;
+	q : int;
+	scrdrw, settag : int;
+	t : ref Text;
+
+	err = nil;
+	scrdrw = FALSE;
+	settag = FALSE;
+	w.tag.commit(TRUE);
+	nb = sys->utfbytes(data(x.fcall), count(x.fcall));
+	r = string data(x.fcall)[0:nb];
+loop :
+	for(n=0; n<len r; n+=m){
+		p = r[n:];
+		if(strncmp(p, "lock", 4) == 0){	# make window exclusive use
+			w.ctllock.lock();
+			w.ctlfid = x.f.fid;
+			m = 4;
+		}else
+		if(strncmp(p, "unlock", 6) == 0){	# release exclusive use
+			w.ctlfid = ~0;
+			w.ctllock.unlock();
+			m = 6;
+		}else
+		if(strncmp(p, "clean", 5) == 0){	# mark window 'clean', seq=0
+			t = w.body;
+			t.eq0 = ~0;
+			t.file.reset();
+			t.file.mod = FALSE;
+			w.dirty = FALSE;
+			settag = TRUE;
+			m = 5;
+		}else
+		if(strncmp(p, "show", 4) == 0){	# show dot
+			t = w.body;
+			t.show(t.q0, t.q1, TRUE);
+			m = 4;
+		}else
+		if(strncmp(p, "name ", 5) == 0){	# set file name
+			pp = p[5:];
+			m = 5;
+			q = utils->strchr(pp, '\n');
+			if(q<=0){
+				err = Ebadctl;
+				break;
+			}
+			nm := pp[0:q];
+			for(i=0; i<len nm; i++)
+				if(nm[i] <= ' '){
+					err = "bad character in file name";
+					break loop;
+				}
+			seq++;
+			w.body.file.mark();
+			w.setname(nm, len nm);
+			m += (q+1);
+		}else
+		if(strncmp(p, "dump ", 5) == 0){	# set dump string
+			pp = p[5:];
+			m = 5;
+			q = utils->strchr(pp, '\n');
+			if(q<=0){
+				err = Ebadctl;
+				break;
+			}
+			nm := pp[0:q];
+			w.dumpstr = nm;
+			m += (q+1);
+		}else
+		if(strncmp(p, "dumpdir ", 8) == 0){	# set dump directory
+			pp = p[8:];
+			m = 8;
+			q = utils->strchr(pp, '\n');
+			if(q<=0){
+				err = Ebadctl;
+				break;
+			}
+			nm := pp[0:q];
+			w.dumpdir = nm;
+			m += (q+1);
+		}else
+		if(strncmp(p, "delete", 6) == 0){	# delete for sure
+			w.col.close(w, TRUE);
+			m = 6;
+		}else
+		if(strncmp(p, "del", 3) == 0){	# delete, but check dirty
+			if(!w.clean(TRUE, FALSE)){
+				err = "file dirty";
+				break;
+			}
+			w.col.close(w, TRUE);
+			m = 3;
+		}else
+		if(strncmp(p, "get", 3) == 0){	# get file
+			exec->get(w.body, nil, nil, FALSE, nil, 0);
+			m = 3;
+		}else
+		if(strncmp(p, "put", 3) == 0){	# put file
+			exec->put(w.body, nil, nil, 0);
+			m = 3;
+		}else
+		if(strncmp(p, "dot=addr", 8) == 0){	# set dot
+			w.body.commit(TRUE);
+			clampaddr(w);
+			w.body.q0 = w.addr.q0;
+			w.body.q1 = w.addr.q1;
+			w.body.setselect(w.body.q0, w.body.q1);
+			settag = TRUE;
+			m = 8;
+		}else
+		if(strncmp(p, "addr=dot", 8) == 0){	# set addr
+			w.addr.q0 = w.body.q0;
+			w.addr.q1 = w.body.q1;
+			m = 8;
+		}else
+		if(strncmp(p, "limit=addr", 10) == 0){	# set limit
+			w.body.commit(TRUE);
+			clampaddr(w);
+			w.limit.q0 = w.addr.q0;
+			w.limit.q1 = w.addr.q1;
+			m = 10;
+		}else
+		if(strncmp(p, "nomark", 6) == 0){	# turn off automatic marking
+			w.nomark = TRUE;
+			m = 6;
+		}else
+		if(strncmp(p, "mark", 4) == 0){	# mark file
+			seq++;
+			w.body.file.mark();
+			settag = TRUE;
+			m = 4;
+		}else
+		if(strncmp(p, "noscroll", 8) == 0){	# turn off automatic scrolling
+			w.noscroll = TRUE;
+			m = 8;
+		}else
+		if(strncmp(p, "cleartag", 8) == 0){	# wipe tag right of bar
+			w.cleartag();
+			settag = TRUE;
+			m = 8;
+		}else
+		if(strncmp(p, "scroll", 6) == 0){	# turn on automatic scrolling (writes to body only)
+			w.noscroll = FALSE;
+			m = 6;
+		}else
+		if(strncmp(p, "noecho", 6) == 0){	# don't echo chars - mask them
+			w.echomode = EM_MASK;
+			m = 6;
+		}else
+		if (strncmp(p, "echo", 4) == 0){		# echo chars (normal state)
+			w.echomode = EM_NORMAL;
+			m = 4;
+		}else{
+			err = Ebadctl;
+			break;
+		}
+		while(m < len p && p[m] == '\n')
+			m++;
+	}
+	
+	ab := array of byte r[0:n];
+	n = len ab;
+	ab = nil;
+	r = nil;
+	if(err != nil)
+		n = 0;
+	fc.count = n;
+	respond(x, fc, err);
+	if(settag)
+		w.settag();
+	if(scrdrw)
+		scrdraw(w.body);
+}
+
+Xfid.eventwrite(x : self ref Xfid, w : ref Window)
+{
+	fc : Smsg0;
+	m, n, nb : int;
+	r, err : string;
+	p, q : int;
+	t : ref Text;
+	c : int;
+	q0, q1 : int;
+
+	err = nil;
+	nb = sys->utfbytes(data(x.fcall), count(x.fcall));
+	r = string data(x.fcall)[0:nb];
+loop :
+	for(n=0; n<len r; n+=m){
+		p = n;
+		w.owner = r[p++];	# disgusting
+		c = r[p++];
+		while(r[p] == ' ')
+			p++;
+		q0 = int r[p:];
+		q = p;
+		if (r[q] == '+' || r[q] == '-')
+			q++;
+		while (r[q] >= '0' && r[q] <= '9')
+			q++;
+		if(q == p) {
+			err = Ebadevent;
+			break;
+		}
+		p = q;
+		while(r[p] == ' ')
+			p++;
+		q1 = int r[p:];
+		q = p;
+		if (r[q] == '+' || r[q] == '-')
+			q++;
+		while (r[q] >= '0' && r[q] <= '9')
+			q++;
+		if(q == p) {
+			err = Ebadevent;
+			break;
+		}
+		p = q;
+		while(r[p] == ' ')
+			p++;
+		if(r[p++] != '\n') {
+			err = Ebadevent;
+			break;
+		}
+		m = p-n;
+		if('a'<=c && c<='z')
+			t = w.tag;
+		else if('A'<=c && c<='Z')
+			t = w.body;
+		else {
+			err = Ebadevent;
+			break;
+		}
+		if(q0>t.file.buf.nc || q1>t.file.buf.nc || q0>q1) {
+			err = Ebadevent;
+			break;
+		}
+		# row.qlock.lock();
+		case(c){
+		'x' or 'X' =>
+			exec->execute(t, q0, q1, TRUE, nil);
+		'l' or 'L' =>
+			look->look3(t, q0, q1, TRUE);
+		* =>
+			err = Ebadevent;
+			break loop;
+		}
+		# row.qlock.unlock();
+	}
+
+	ab := array of byte r[0:n];
+	n = len ab;
+	ab = nil;
+	r = nil;
+	if(err != nil)
+		n = 0;
+	fc.count = n;
+	respond(x, fc, err);
+}
+
+Xfid.utfread(x : self ref Xfid, t : ref Text, q0, q1 : int, qid : int)
+{
+	fc : Smsg0;
+	w : ref Window;
+	r : ref Astring;
+	b, b1 : array of byte;
+	q, off, boff : int;
+	m, n, nr, nb : int;
+
+	w = t.w;
+	w.commit(t);
+	off = int offset(x.fcall);
+	r = stralloc(BUFSIZE);
+	b1 = array[MAXRPC] of byte;
+	n = 0;
+	if(qid==w.utflastqid && off>=w.utflastboff && w.utflastq<=q1){
+		boff = w.utflastboff;
+		q = w.utflastq;
+	}else{
+		# BUG: stupid code: scan from beginning
+		boff = 0;
+		q = q0;
+	}
+	w.utflastqid = qid;
+	while(q<q1 && n<count(x.fcall)){
+		w.utflastboff = boff;
+		w.utflastq = q;
+		nr = q1-q;
+		if(nr > BUFSIZE)
+			nr = BUFSIZE;
+		t.file.buf.read(q, r, 0, nr);
+		b = array of byte r.s[0:nr];
+		nb = len b;
+		if(boff >= off){
+			m = nb;
+			if(boff+m > off+count(x.fcall))
+				m = off+count(x.fcall) - boff;
+			b1[n:] = b[0:m];
+			n += m;
+		}else if(boff+nb > off){
+			if(n != 0)
+				error("bad count in utfrune");
+			m = nb - (off-boff);
+			if(m > count(x.fcall))
+				m = count(x.fcall);
+			b1[0:] = b[off-boff:off-boff+m];
+			n += m;
+		}
+		b = nil;
+		boff += nb;
+		q += nr;
+	}
+	strfree(r);
+	r = nil;
+	fc.count = n;
+	fc.data = b1;
+	respond(x, fc, nil);
+	b1 = nil;
+}
+
+Xfid.runeread(x : self ref Xfid, t : ref Text, q0, q1 : int) : int
+{
+	fc : Smsg0;
+	w : ref Window;
+	r : ref Astring;
+	junk, ok : int;
+	b, b1 : array of byte;
+	q, boff : int;
+	i, rw, m, n, nr, nb : int;
+
+	w = t.w;
+	w.commit(t);
+	r = stralloc(BUFSIZE);
+	b1 = array[MAXRPC] of byte;
+	n = 0;
+	q = q0;
+	boff = 0;
+	while(q<q1 && n<count(x.fcall)){
+		nr = q1-q;
+		if(nr > BUFSIZE)
+			nr = BUFSIZE;
+		t.file.buf.read(q, r, 0, nr);
+		b = array of byte r.s[0:nr];
+		nb = len b;
+		m = nb;
+		if(boff+m > count(x.fcall)){
+			i = count(x.fcall) - boff;
+			# copy whole runes only
+			m = 0;
+			nr = 0;
+			while(m < i){
+				(junk, rw, ok) = sys->byte2char(b, m);
+				if(m+rw > i)
+					break;
+				m += rw;
+				nr++;
+			}
+			if(m == 0)
+				break;
+		}
+		b1[n:] = b[0:m];
+		b = nil;
+		n += m;
+		boff += nb;
+		q += nr;
+	}
+	strfree(r);
+	r = nil;
+	fc.count = n;
+	fc.data = b1;
+	respond(x, fc, nil);
+	b1 = nil;
+	return q-q0;
+}
+
+Xfid.eventread(x : self ref Xfid, w : ref Window)
+{
+	fc : Smsg0;
+	b : string;
+	i, n : int;
+
+	i = 0;
+	x.flushed = FALSE;
+	while(w.nevents == 0){
+		if(i){
+			if(!x.flushed)
+				respond(x, fc, "window shut down");
+			return;
+		}
+		w.eventx = x;
+		w.unlock();
+		<- x.c;
+		w.lock('F');
+		i++;
+	}
+	eveb := array of byte w.events;
+	ne := len eveb;
+	n = w.nevents;
+	if(ne > count(x.fcall)) {
+		ne = count(x.fcall);
+		while (sys->utfbytes(eveb, ne) != ne)
+			--ne;
+		s := string eveb[0:ne];
+		n = len s;
+		s = nil;
+	}
+	fc.count = ne;
+	fc.data = eveb;
+	respond(x, fc, nil);
+	b = w.events;
+	w.events = w.events[n:];
+	b = nil;
+	w.nevents -= n;
+	eveb = nil;
+}
+
+Xfid.indexread(x : self ref Xfid)
+{
+	fc : Smsg0;
+	i, j, m, n, nmax, cnt, off : int;
+	w : ref Window;
+	b : array of byte;
+	r : ref Astring;
+	c : ref Column;
+
+	row.qlock.lock();
+	nmax = 0;
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c.nw; i++){
+			w = c.w[i];
+			nmax += Ctlsize + w.tag.file.buf.nc*UTFmax + 1;
+		}
+	}
+	nmax++;
+	b = array[nmax] of byte;
+	r = stralloc(BUFSIZE);
+	n = 0;
+	for(j=0; j<row.ncol; j++){
+		c = row.col[j];
+		for(i=0; i<c.nw; i++){
+			w = c.w[i];
+			# only show the currently active window of a set
+			if(w.body.file.curtext != w.body)
+				continue;
+			ctls := w.ctlprint(0);
+			ctlb := array of byte ctls;
+			if (len ctls != Ctlsize || len ctlb != Ctlsize)
+				error("bad length in indexread");
+			b[n:] = ctlb[0:];
+			n += Ctlsize;
+			ctls = nil;
+			ctlb = nil;
+			m = min(BUFSIZE, w.tag.file.buf.nc);
+			w.tag.file.buf.read(0, r, 0, m);
+			rb := array of byte r.s[0:m];
+			b[n:] = rb[0:len rb];
+			m = n+len rb;
+			rb = nil;
+			while(n<m && b[n]!=byte '\n')
+				n++;
+			b[n++] = byte '\n';
+		}
+	}
+	row.qlock.unlock();
+	off = int offset(x.fcall);
+	cnt = count(x.fcall);
+	if(off > n)
+		off = n;
+	if(off+cnt > n)
+		cnt = n-off;
+	fc.count = cnt;
+	fc.data = b[off:off+cnt];
+	respond(x, fc, nil);
+	b = nil;
+	strfree(r);
+	r = nil;
+}
--- /dev/null
+++ b/appl/acme/xfid.m
@@ -1,0 +1,34 @@
+Xfidm : module {
+	PATH : con "/dis/acme/xfid.dis";
+
+	Xnil, Xflush, Xwalk, Xopen, Xclose, Xread, Xwrite : con iota;
+
+	init : fn(mods : ref Dat->Mods);
+
+	newxfid : fn() : ref Xfid;
+	xfidkill : fn();
+
+	Xfid : adt {
+		tid : int;
+		fcall : ref Styx->Tmsg;
+		next : cyclic ref Xfid;
+		c : chan of int;
+		f : cyclic ref Dat->Fid;
+		buf : array of byte;
+		flushed : int; 
+
+		ctl : fn(x : self ref Xfid);
+		flush: fn(x : self ref Xfid);
+		walk: fn(x : self ref Xfid, c: chan of ref Windowm->Window);
+		open: fn(x : self ref Xfid);
+		close: fn(x : self ref Xfid);
+		read: fn(x : self ref Xfid);
+		write: fn(x : self ref Xfid);
+		ctlwrite: fn(x : self ref Xfid, w : ref Windowm->Window);
+		eventread: fn(x : self ref Xfid, w : ref Windowm->Window);
+		eventwrite: fn(x : self ref Xfid, w : ref Windowm->Window);
+		indexread: fn(x : self ref Xfid);
+		utfread: fn(x : self ref Xfid, t : ref Textm->Text, m : int, n : int, qid : int);
+		runeread: fn(x : self ref Xfid, t : ref Textm->Text, m : int, n : int) : int;
+	};
+};
--- /dev/null
+++ b/appl/alphabet/abc/abc.b
@@ -1,0 +1,53 @@
+implement Mkabc, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Mkabc: module {};
+types(): string
+{
+	return "A";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		nil: list of ref Value
+	): ref Value
+{
+	alphabet := load Alphabet Alphabet->PATH;
+	if(alphabet == nil){
+		report(errorc, sys->sprint("abc: cannot load %q: %r", Alphabet->PATH));
+		return nil;
+	}
+	alphabet->init();
+	c := chan[1] of int;
+	c <-= 1;
+	return ref Value.VA((c, alphabet));
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/autoconvert.b
@@ -1,0 +1,80 @@
+implement Autoconvert, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	Cmd,
+	n_BLOCK, n_WORD, n_SEQ, n_LIST, n_ADJ, n_VAR: import Sh;
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Autoconvert: module {};
+types(): string
+{
+	return "AAssc";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	a := (hd args).A().i.alphabet;
+	src := (hd tl args).s().i;
+	dst := (hd tl tl args).s().i;
+	c := (hd tl tl tl args).c().i;
+
+	# {word} -> {(src); word $1}
+	if(c.ntype == n_BLOCK && c.left.ntype == n_WORD){
+		c = mk(n_BLOCK,
+			mk(n_SEQ,
+				mk(n_LIST, mkw(src), nil),
+				mk(n_ADJ,
+					c.left,
+					mk(n_VAR, mkw("1"), nil)
+				)
+			),
+			nil
+		);
+	}
+			
+	err := a->autoconvert(src, dst, c, errorc);
+	if(err != nil){
+		report(errorc, "abcautoconvert: "+err);
+		return nil;
+	}
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
+
+mk(ntype: int, left, right: ref Cmd): ref Cmd
+{
+	return ref Cmd(ntype, left, right, nil, nil);
+}
+mkw(w: string): ref Cmd
+{
+	return ref Cmd(n_WORD, nil, nil, w, nil);
+}
--- /dev/null
+++ b/appl/alphabet/abc/autodeclare.b
@@ -1,0 +1,42 @@
+implement Autoconvert, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Autoconvert: module {};
+types(): string
+{
+	return "AAs";
+}
+
+init()
+{
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(nil: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	(hd args).A().i.alphabet->setautodeclare(int (hd tl args).s().i);
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/declare.b
@@ -1,0 +1,70 @@
+implement Declare, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Declare: module {};
+types(): string
+{
+	return "AAss*-q-c";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		opts: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	flags := 0;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).t0 {
+		'q' =>
+			flags |= Alphabet->ONDEMAND;
+		'c' =>
+			flags |= Alphabet->CHECK;
+		}
+	}
+
+	n := len args;
+	if(n > 3){
+		report(errorc, "declare: maximum of two arguments allowed");
+		return nil;
+	}
+	a := (hd args).A().i.alphabet;
+	m := (hd tl args).s().i;
+	sig := "";
+	if(n > 2)
+		sig = (hd tl tl args).s().i;
+	e := a->declare(m, sig, flags);
+	if(e != nil){
+		report(errorc, "declare: "+e);
+		return nil;
+	}
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/declares.b
@@ -1,0 +1,124 @@
+implement Declares, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	n_BLOCK, n_ADJ, n_VAR, n_WORD: import Sh;
+include "alphabet/reports.m";
+	reports: Reports;
+	report, Report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+include "alphabet/abctypes.m";
+	abctypes: Abctypes;
+	Abccvt: import abctypes;
+
+cvt: ref Abccvt;
+
+types(): string
+{
+	return "AAc";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+	alphabet = checkload(load Alphabet Alphabet->PATH, Alphabet->PATH);
+	alphabet->init();
+	abctypes = checkload(load Abctypes Abctypes->PATH, Abctypes->PATH);
+	(c, nil, abccvt) := abctypes->proxy0();
+	cvt = abccvt;
+	alphabet->loadtypeset("/abc", c, nil);
+	alphabet->importtype("/abc/abc");
+	alphabet->importtype("/string");
+	alphabet->importtype("/cmd");
+	c = nil;
+	# note: it's faster if we provide the signatures, as we don't
+	# have to load the module to find out its signature just to throw
+	# it away again. pity about the maintenance.
+
+	# Edit x s:(/abc/[a-z]+) (.*):declimport("\1", "\2");
+	declimport("/abc/autoconvert", "abc string string cmd -> abc");
+	declimport("/abc/autodeclare", "abc string -> abc");
+	declimport("/abc/declare", "[-qc] abc string [string...] -> abc");
+	declimport("/abc/define", "abc string cmd -> abc");
+	declimport("/abc/import", "abc string [string...] -> abc");
+	declimport("/abc/type", "abc string [string...] -> abc");
+	declimport("/abc/typeset", "abc string -> abc");
+	declimport("/abc/undeclare", "abc string [string...] -> abc");
+}
+
+quit()
+{
+	alphabet->quit();
+}
+
+run(errorc: chan of string, r: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	(av, err) := alphabet->importvalue(cvt.int2ext((hd args).dup()), "/abc/abc");
+	if(av == nil){
+		report(errorc, sys->sprint("declares: cannot import abc value: %s", err));
+		return nil;
+	}
+	vc := chan of ref Alphabet->Value;
+	spawn alphabet->eval0((hd tl args).c().i, "/abc/abc", nil, r, r.start("evaldecl"), av :: nil, vc);
+	av = <-vc;
+	if(av == nil)
+		return nil;
+	v := cvt.ext2int(av).dup();
+	alphabet->av.free(1);
+	return v;
+}
+
+declimport(m: string, sig: string)
+{
+	if((e := alphabet->declare(m, sig, Alphabet->ONDEMAND)) != nil)
+		raise sys->sprint("fail:cannot declare %s: %s", m, e);
+	alphabet->importmodule(m);
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
+
+declares(a: Alphabet, decls: ref Sh->Cmd, errorc: chan of string, stopc: chan of int): string
+{
+	spawn reports->reportproc(reportc := chan of string, stopc, reply := chan of ref Report);
+	r := <-reply;
+	reply = nil;
+	spawn declaresproc(a, decls, r.start("declares"), r, vc := chan of ref Value);
+	r.enable();
+
+	v: ref Value;
+wait:
+	for(;;)alt{
+	v = <-vc =>
+		;
+	msg := <-reportc =>
+		if(msg == nil)
+			break wait;
+		errorc <-= sys->sprint("declares: %s", msg);
+	}
+	if(v == nil)
+		return "declarations failed";
+	return nil;
+}
+
+declaresproc(a: Alphabet, decls: ref Sh->Cmd, errorc: chan of string, r: ref Report, vc: chan of ref Value)
+{
+	novals: list of ref Value;
+	vc <-= run(errorc, r, nil, abc->mkabc(a).dup() :: ref Value.Vc(decls) :: novals);
+	errorc <-= nil;
+}
--- /dev/null
+++ b/appl/alphabet/abc/define.b
@@ -1,0 +1,52 @@
+implement Define, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Define: module {};
+types(): string
+{
+	return "AAsc";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	a := (hd args).A().i.alphabet;
+	m := (hd tl args).s().i;
+	c := (hd tl tl args).c().i;
+	if((e := a->define(m, c, errorc)) != nil){
+		report(errorc, "define: error: "+e);
+		return nil;
+	}
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/eval.b
@@ -1,0 +1,66 @@
+implement Evalabc, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report, Report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Evalabc: module {};
+types(): string
+{
+	return "rAcs*";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(nil: chan of string, r: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	a := (hd args).A().i.alphabet;
+	c := (hd tl args).c().i;
+	vl, rvl: list of ref Alphabet->Value;
+	for(args = tl tl args; args != nil; args = tl args)
+		vl = ref (Alphabet->Value).Vs((hd args).s().i) :: vl;
+	for(; vl != nil; vl = tl vl)
+		rvl = hd vl :: rvl;
+	vc := chan of ref Alphabet->Value;
+	spawn a->eval0(c, "/status", nil, r, r.start("abceval"), rvl, vc);
+	v := <-vc;
+	if(v == nil)
+		return nil;
+	return ref Value.Vr(vr(v).i);
+}
+
+vr(v: ref Alphabet->Value): ref (Alphabet->Value).Vr
+{
+	pick xv := v {
+	Vr =>
+		return xv;
+	}
+	return nil;
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/import.b
@@ -1,0 +1,53 @@
+implement Import, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Import: module {};
+types(): string
+{
+	return "AAss*";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	av := (hd args);
+	a := av.A().i.alphabet;
+	for(args = tl args; args != nil; args = tl args){
+		if((e := a->importmodule((hd args).s().i)) != nil){
+			report(errorc, "import: "+(hd args).s().i+": "+e);
+			return nil;
+		}
+	}
+	return av.dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/mkfile
@@ -1,0 +1,29 @@
+<../../../mkconfig
+
+TARG=\
+	abc.dis\
+	autoconvert.dis\
+	autodeclare.dis\
+	declare.dis\
+	declares.dis\
+	define.dis\
+	eval.dis\
+	import.dis\
+	rewrite.dis\
+	type.dis\
+	typeset.dis\
+	undeclare.dis\
+
+SYSMODULES=\
+	alphabet.m\
+	draw.m\
+	alphabet/abc.m\
+	alphabet/reports.m\
+	sh.m\
+	string.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/alphabet/abc
+
+<$ROOT/mkfiles/mkdis
+LIMBOFLAGS=-F $LIMBOFLAGS
--- /dev/null
+++ b/appl/alphabet/abc/newtypeset.b
@@ -1,0 +1,147 @@
+implement Newtypeset, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value, Vtype: import abc;
+
+# types abc -> types
+#	returns a set of types defined in terms of the types and modules in $1
+# stdtypes types -> types
+#	adds the standard root types to $1
+# newtype [-u] types string string cmd -> types
+#	adds a new type named $2 to $1; the underlying type will be $3, and the destructor $4.
+#	-u flag implies values of this type cannot be duplicated.
+# modules types -> modules
+#	returns a value suitable for defining modules in terms of types defined in $1,
+#	containing no module definitions.
+# module modules string string cmd -> modules
+# newtypeset abc string modules -> abc
+
+# declares adds some autoconversions:
+# 
+# 	autoconvert abc types "{| types}
+# 	autoconvert types modules "{| modules}
+# 
+# declares "{(abc)
+# 	autodeclare 1 |
+# 	newtypeset $1 /images {
+#		abc |
+#		autoconvert 1 |
+# 		newtype image /fd "{} |
+# 		newmodule read '/fd -> image' "{
+#			| /filter "{canonimage}
+#		} |
+# 		newmodule rotate 'image -> image' "{
+#			| /filter "{rotate}
+#		} |
+# 		newmodule display 'image -> /status' "{
+#			| /filter "{showimage} | /create /dev/null
+#		}
+# 	} |
+# 	type /images/image |
+# 	import /images/rotate |
+# 	autoconvert /string /fd "{|/read} |
+# 	autoconvert /fd image "{|/images/read} |
+# 	autoconvert image /status "{|/images/display}
+# }
+# 
+# -{rotate x.bit}
+
+Newtypeset: module {};
+types(): string
+{
+	return "AAsm";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	a := (hd args).A().i.alphabet;
+	d := (hd tl args).s().i;
+	path := "/dis/alphabet/" + d + "/alphabet"
+	iob := bufio->open(, Sys->OREAD);
+	if(iob == nil){
+		report(errorc, sys->sprint("scripttypeset: cannot open %q: %r", path));
+		return nil;
+	}
+	{
+		(types, decls) := parse(iob);
+		alphabet := load Alphabet Alphabet->PATH;
+		if(alphabet == nil){
+			report(errorc, sys->sprint("scripttypeset: cannot load %q: %r", Alphabet->PATH));
+			return nil;
+		}
+		declares := load Declares Declares->PATH;
+		if(declares == nil){
+			report(errorc, sys->sprint("scripttypeset: cannot load %q: %r", Alphabet->PATH));
+			return nil;
+		}
+		if((err := declares->declares(alphabet, decls, errorc)) != nil){
+			report(errorc, "scripttypeset: error on declarations: "+err);
+			return nil;
+		}
+		declares->quit();
+		declares = nil;
+		if(checktypes(alphabet, types, errorc) == -1)
+			return nil;
+		spawn scripttypesetproc(alphabet, types, c := chan of ref Proxy->Typescmd[ref Alphabet->Value]);
+		if((err := a->loadtypeset(d, c, errorc)) != nil){
+			c <-= nil;
+			return nil;
+		}
+		return (hd args).dup();
+	} exception e {
+	"parse:*" =>
+		report(errorc, sys->sprint("scripttypeset: error parsing %q: %s", path, e[6:]));
+		return nil;
+	}
+}
+
+checktypes(alphabet: Alphabet, types: list of ref Type, errorc: chan of string): int
+{
+	for(; types != nil; types = tl types){
+		t := hd types;
+		if(t.destructor != nil){
+			report(errorc, "destructors not supported yet");
+		}
+	}
+}
+
+scripttypesetproc(alphabet: Alphabet, types: list of ref Type, c: chan of Proxy->Typescmd[ref Alphabet->Value])
+{
+	while((gr := <-c) != nil){
+		pick r := gr {
+		Alphabet =>
+		Load =>
+		
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/rewrite.b
@@ -1,0 +1,71 @@
+implement Rewrite, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Rewrite: module {};
+types(): string
+{
+	return "cAc-ss-rs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		opts: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	rtype, sig: string;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).t0 {
+		's' =>
+			sig = (hd (hd opts).t1).s().i;
+		'r' =>
+			rtype = (hd (hd opts).t1).s().i;
+		}
+	}
+	a := (hd args).A().i.alphabet;
+	c := (hd tl args).c().i;
+	actsig: string;
+	(c, actsig) = a->rewrite(c, rtype, errorc);
+	if(c == nil)
+		return nil;
+	if(sig != nil){
+		(ok, err) := a->typecompat(sig, actsig);
+		if(err != nil){
+			report(errorc, "rewrite: "+err);
+			return nil;
+		}
+		if(ok == 0){
+			report(errorc, sys->sprint("rewrite: %q is not compatible with %q", sig, actsig));
+			return nil;
+		}
+	}
+	return ref Value.Vc(c);
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/type.b
@@ -1,0 +1,53 @@
+implement Type, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Type: module {};
+types(): string
+{
+	return "AAss*";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	av := (hd args);
+	a := av.A().i.alphabet;
+	for(args = tl args; args != nil; args = tl args){
+		if((e := a->importtype((hd args).s().i)) != nil){
+			report(errorc, "type: "+(hd args).s().i+": "+e);
+			return nil;
+		}
+	}
+	return av.dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/typeset.b
@@ -1,0 +1,51 @@
+implement Typeset, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Typeset: module {};
+types(): string
+{
+	return "AAs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(errorc: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	a := (hd args).A().i.alphabet;
+	e := a->loadtypeset((hd tl args).s().i, nil, errorc);
+	if(e != nil){
+		report(errorc, "typeset: "+(hd tl args).s().i+": "+e);
+		return nil;
+	}
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/abc/undeclare.b
@@ -1,0 +1,48 @@
+implement Undeclare, Abcmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report: import reports;
+include "alphabet.m";
+include "alphabet/abc.m";
+	abc: Abc;
+	Value: import abc;
+
+Undeclare: module {};
+types(): string
+{
+	return "AAss*";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+}
+
+quit()
+{
+}
+
+run(nil: chan of string, nil: ref Reports->Report,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value
+	): ref Value
+{
+	a := (hd args).A().i.alphabet;
+	for(al := tl args; al != nil; al = tl al)
+		a->undeclare((hd al).s().i);
+	return (hd args).dup();
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/alphabet.b
@@ -1,0 +1,1677 @@
+implement Alphabet, Copy;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "readdir.m";
+include "sh.m";
+	sh: Sh;
+	n_BLOCK, n_SEQ, n_LIST, n_ADJ, n_WORD, n_VAR, n_BQ2, n_PIPE: import Sh;
+include "sets.m";
+	sets: Sets;
+	Set: import sets;
+include "alphabet/reports.m";
+	reports: Reports;
+	Report: import reports;
+	Modulecmd, Typescmd: import Proxy;
+include "alphabet.m";
+	evalmod: Eval;
+	Context: import evalmod;
+
+Mainsubtypes: module {
+	proxy: fn(): chan of ref Proxy->Typescmd[ref Alphabet->Value];
+};
+
+# to do:
+# - sort out concurrent access to alphabet.
+# - if multiple options are given where only one is expected,
+#	most modules ignore some values, where they should
+#	discard them correctly. this could cause a malicious user
+#	to hang up an alphabet expression (waiting for report to end)
+# - proper implementation of endpointsrv:
+#	- resilience to failures
+#	- security of endpoints
+#	- no need for write(0)... (or maybe there is)
+# - proper implementation of rexecsrv:
+#	- should be aware of user
+
+Debug: con 0;
+autodeclare := 0;
+
+Module: adt {
+	modname:	string;		# used when loading on demand.
+	typeset:		ref Typeset;
+	sig:			string;
+	c:			chan of ref Modulecmd[ref Value];
+	m:			Mainmodule;
+	def:			ref Sh->Cmd;
+	defmods:		ref Strhash[cyclic ref Module];
+	refcount:		int;
+
+	find:		fn(ctxt: ref Evalctxt, s: string): (ref Module, string);
+	typesig:	fn(m: self ref Module): string;
+	run:		fn(m: self ref Module, ctxt: ref Evalctxt,
+					errorc: chan of string,
+					opts: list of (int, list of ref Value),
+					args: list of ref Value): ref Value;
+	typename2c:	fn(s: string): int;
+	mks:		fn(ctxt: ref Evalctxt, s: string): ref Value;
+	mkc:		fn(ctxt: ref Evalctxt, c: ref Sh->Cmd): ref Value;
+	ensureloaded:	fn(m: self ref Module): string;
+	cvt:		fn(ctxt: ref Evalctxt, v: ref Value, tc: int, errorc: chan of string): ref Value;
+};
+
+Evalctxt: adt {
+	modules:	ref Strhash[ref Module];
+	drawctxt: ref Draw->Context;
+	report: ref Report;
+#	stopc: chan of int;
+};
+
+# used for rewriting expressions.
+Rvalue: adt {
+	i: ref Sh->Cmd;
+	tc: int;
+	refcount: int;
+	opts: list of (int, list of ref Rvalue);
+	args: list of ref Rvalue;
+
+	dup:		fn(t: self ref Rvalue): ref Rvalue;
+	free:		fn(v: self ref Rvalue, used: int);
+	isstring:	fn(v: self ref Rvalue): int;
+	gets:		fn(t: self ref Rvalue): string;
+	type2s:	fn(tc: int): string;
+	typec:	fn(t: self ref Rvalue): int;
+};
+
+Rmodule: adt {
+	m: ref Module;
+
+	cvt:		fn(ctxt: ref Revalctxt, v: ref Rvalue, tc: int, errorc: chan of string): ref Rvalue;
+	find:		fn(nil: ref Revalctxt, s: string): (ref Rmodule, string);
+	typesig:	fn(m: self ref Rmodule): string;
+	run:		fn(m: self ref Rmodule, ctxt: ref Revalctxt, errorc: chan of string,
+				opts: list of (int, list of ref Rvalue), args: list of ref Rvalue): ref Rvalue;
+	mks:		fn(ctxt: ref Revalctxt, s: string): ref Rvalue;
+	mkc:		fn(ctxt: ref Revalctxt, c: ref Sh->Cmd): ref Rvalue;
+	typename2c:	fn(s: string): int;
+};
+
+Revalctxt: adt {
+	modules: ref Strhash[ref Module];
+	used: ref Strhash[ref Module];
+	defs:	int;
+	vals: list of ref Rvalue;
+};
+
+Renv: adt {
+	items: list of ref Rvalue;
+	n: int;
+};
+
+Typeset: adt {
+	name: string;
+	c: chan of ref Typescmd[ref Value];
+	types: ref Table[cyclic ref Type];		# indexed by external type character
+	parent: ref Typeset;
+
+	gettype:	fn(ts: self ref Typeset, tc: int): ref Type;
+};
+
+Type: adt {
+	id:	int;
+	tc:	int;
+	transform: list of ref Transform;
+	typeset: ref Typeset;
+	qname:	string;
+	name:	string;
+};
+
+Transform: adt {
+	dst: int;				# which type we're transforming into.
+	all: Set;				# set of all types this transformation can lead to.
+	expr: ref Sh->Cmd;		# transformation operation.
+};
+
+Table: adt[T] {
+	items:	array of list of (int, T);
+	nilval:	T;
+
+	new: fn(nslots: int, nilval: T): ref Table[T];
+	add:	fn(t: self ref Table, id: int, x: T): int;
+	del:	fn(t: self ref Table, id: int): int;
+	find:	fn(t: self ref Table, id: int): T;
+};
+
+Strhash: adt[T] {
+	items:	array of list of (string, T);
+	nilval:	T;
+
+	new: fn(nslots: int, nilval: T): ref Strhash[T];
+	add:	fn(t: self ref Strhash, id: string, x: T);
+	del:	fn(t: self ref Strhash, id: string);
+	find:	fn(t: self ref Strhash, id: string): T;
+};
+
+Copy: module {
+	initcopy: fn(
+		typesets: list of ref Typeset,
+		roottypeset: ref Typeset,
+		modules: ref Strhash[ref Module],
+		typebyname: ref Strhash[ref Type],
+		typebyc: ref Table[ref Type],
+		types: array of ref Type,
+		currtypec: int
+	): Alphabet;
+};
+
+typesets: list of ref Typeset;
+roottypeset: ref Typeset;
+modules: ref Strhash[ref Module];
+typebyname: ref Strhash[ref Type];
+typebyc: ref Table[ref Type];	# indexed by internal type character.
+types: array of ref Type;		# indexed by id.
+currtypec := 16r25a0;		# pretty graphics.
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	sys->fprint(sys->fildes(2), "alphabet: cannot load %s: %r\n", path);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	sh = load Sh Sh->PATH;
+	sets = checkload(load Sets Sets->PATH, Sets->PATH);
+	evalmod = checkload(load Eval Eval->PATH, Eval->PATH);
+	evalmod->init();
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+
+	roottypeset = ref Typeset("/", nil, Table[ref Type].new(5, nil), nil);
+	typesets = roottypeset :: typesets;
+	types = array[] of {
+		ref Type(-1, 'c', nil, roottypeset, "/cmd", "cmd"),
+		ref Type(-1, 's', nil, roottypeset, "/string", "string"),
+		ref Type(-1, 'r', nil, roottypeset, "/status", "status"),
+		ref Type(-1, 'f', nil, roottypeset, "/fd", "fd"),
+		ref Type(-1, 'w', nil, roottypeset, "/wfd", "wfd"),
+		ref Type(-1, 'd', nil, roottypeset, "/data", "data"),
+	};
+	typebyname = typebyname.new(11, nil);
+	typebyc = typebyc.new(11, nil);
+	for(i := 0; i < len types; i++){
+		types[i].id = i;
+		typebyc.add(types[i].tc, types[i]);
+		typebyname.add(types[i].qname, types[i]);
+		roottypeset.types.add(types[i].tc, types[i]);
+	}
+#	typebyc.add('a', ref Type(-1, 'a', nil, nil, "/any", "any"));		# not sure about this anymore
+	modules = modules.new(3, nil);
+}
+
+initcopy(
+		xtypesets: list of ref Typeset,
+		xroottypeset: ref Typeset,
+		xmodules: ref Strhash[ref Module],
+		xtypebyname: ref Strhash[ref Type],
+		xtypebyc: ref Table[ref Type],
+		xtypes: array of ref Type,
+		xcurrtypec: int): Alphabet
+{
+	# XXX must do copy-on-write, and refcounting on typesets.
+	typesets = xtypesets;
+	roottypeset = xroottypeset;
+	modules = xmodules;
+	typebyname = xtypebyname;
+	typebyc = xtypebyc;
+	types = xtypes;
+	currtypec = xcurrtypec;
+	return load Alphabet "$self";
+}
+
+copy(): Alphabet
+{
+	a := load Copy Alphabet->PATH;
+	if(a == nil)
+		return nil;
+	return a->initcopy(typesets, roottypeset, modules, typebyname, typebyc, types, currtypec);
+}
+
+setautodeclare(x: int)
+{
+	autodeclare = x;
+}
+
+quit()
+{
+	for(ts := typesets; ts != nil; ts = tl ts)
+		if((hd ts).c != nil)
+			(hd ts).c <-= nil;
+	delmods(modules);
+}
+
+delmods(mods: ref Strhash[ref Module])
+{
+	for(i := 0; i < len mods.items; i++){
+		for(l := mods.items[i]; l != nil; l = tl l){
+			m := (hd l).t1;
+			if(--m.refcount == 0){
+				if(m.c != nil){
+					m.c <-= nil;
+					m.c = nil;
+				}else if(m.defmods != nil)
+					delmods(m.defmods);
+				else if(m.m != nil){
+					m.m->quit();
+					m.m = nil;
+				}
+			}
+		}
+	}
+}
+
+# XXX could do some more checking to see whether it looks vaguely like
+# a valid alphabet expression.
+parse(expr: string): (ref Sh->Cmd, string)
+{
+	return sh->parse(expr);
+}
+
+eval(expr: ref Sh->Cmd,
+	drawctxt: ref Draw->Context,
+	args: list of ref Value): string
+{
+	spawn reports->reportproc(reportc := chan of string, nil, reply := chan of ref Report);
+	r := <-reply;
+	reply = nil;
+	stderr := sys->fildes(2);
+	spawn eval0(expr, "/status", drawctxt, r, reports->r.start("eval"), args, vc := chan of ref Value);
+	reports->r.enable();
+	v: ref Value;
+wait:
+	for(;;)alt{
+	v = <-vc =>
+		if(v != nil)
+			v.r().i <-= nil;
+	msg := <-reportc =>
+		if(msg == nil)
+			break wait;
+		sys->fprint(stderr, "alphabet: %s\n", msg);
+	}
+	# we'll always get the value before the report ends.
+	if(v == nil)
+		return "no value";
+	return <-v.r().i;
+}
+
+eval0(expr: ref Sh->Cmd,
+	dsttype: string,
+	drawctxt: ref Draw->Context,
+	r: ref Report,
+	errorc: chan of string,
+	args: list of ref Value,
+	vc: chan of ref Value)
+{
+	c: Eval->Context[ref Value, ref Module, ref Evalctxt];
+	ctxt := ref Evalctxt(modules, drawctxt, r);
+	tc := -1;
+	if(dsttype != nil && (tc = Module.typename2c(dsttype)) == -1){
+		report(errorc, "error: unknown type "+dsttype);
+		vc <-= nil;
+		reports->quit(errorc);
+	}
+
+	v := c.eval(expr, ctxt, errorc, args);
+	if(tc != -1)
+		v = Module.cvt(ctxt, v, tc, errorc);
+	vc <-= v;
+	reports->quit(errorc);
+}
+
+define(name: string, expr: ref Sh->Cmd, errorc: chan of string): string
+{
+	if(name == nil || name[0] == '/')
+		return "bad module name";
+	m := modules.find(name);
+	if(m != nil)
+		return "module already declared";
+	sig: string;
+	used: ref Strhash[ref Module];
+	used = used.new(11, nil);
+	(expr, sig) = rewrite0(expr, -1, errorc, used);
+	if(sig == nil)
+		return "cannot rewrite";
+	modules.add(name, ref Module(name, roottypeset, sig, nil, nil, expr, used, 1));
+	return nil;
+}
+
+typecompat(t0, t1: string): (int, string)
+{
+	m: ref Module;
+	(sig0, err) := evalmod->usage2sig(m, t0);
+	if(err != nil)
+		return (0, sys->sprint("bad usage %q: %s", t0, err));
+	sig1: string;
+	(sig1, err) = evalmod->usage2sig(m, t1);
+	if(err != nil)
+		return (0, sys->sprint("bad usage %q: %s", t1, err));
+	return (evalmod->typecompat(sig0, sig1), nil);
+}
+
+rewrite(expr: ref Sh->Cmd, dsttype: string, errorc: chan of string): (ref Sh->Cmd, string)
+{
+	v: ref Value;
+	tc := -1;
+	if(dsttype != nil){
+		tc = Module.typename2c(dsttype);
+		if(tc == -1){
+			report(errorc, "error: unknown type "+dsttype);
+			return (nil, nil);
+		}
+	}
+	sig: string;
+	(expr, sig) = rewrite0(expr, tc, errorc, nil);
+	if(sig == nil)
+		return (nil, nil);
+
+	return (expr, evalmod->cmdusage(v, sig));
+}
+
+# XXX different kinds of rewrite:
+# could rewrite forcing all names to qualified
+# or just leave names as they are.
+
+# return (expr, sig).
+# add all modules used by the expression to mods if non-nil.
+rewrite0(expr: ref Sh->Cmd, tc: int, errorc: chan of string, used: ref Strhash[ref Module]): (ref Sh->Cmd, string)
+{
+	m: ref Rmodule;
+	ctxt := ref Revalctxt(modules, used, 1, nil);
+	(sig, err) := evalmod->blocksig(m, ctxt, expr);
+	if(sig == nil){
+		report(errorc, "error: cannot get expr type: "+err);
+		return (nil, nil);
+	}
+	args: list of ref Rvalue;
+	for(i := len sig - 1; i >= 1; i--)
+		args = ref Rvalue(mk(-1, nil, nil), sig[i], 1, nil, nil) :: args;	# N.Vb. cmd node is never used.
+
+	c: Eval->Context[ref Rvalue, ref Rmodule, ref Revalctxt];
+	v := c.eval(expr, ctxt, errorc, args);
+	if(v != nil && tc != -1)
+		v = Rmodule.cvt(ctxt, v, tc, errorc);
+	if(v == nil)
+		return (nil, nil);
+	sig[0] = v.tc;
+	v.refcount++;
+	expr = gen(v, ref Renv(nil, 0));
+	if(len sig > 1){
+		t := mkw(Value.type2s(sig[1]));
+		for(i = 2; i < len sig; i++)
+			t = mk(n_ADJ, t, mkw(Value.type2s(sig[i])));
+		expr = mk(n_BLOCK, mk(n_SEQ, mk(n_LIST, t, nil), expr.left), nil);
+	}
+	return (expr, sig);
+}
+
+# generate the expression that gave rise to v.
+# it puts in parentenv any values referred to externally.
+gen(v: ref Rvalue, parentenv: ref Renv): ref Sh->Cmd
+{
+	v.refcount--;
+	if(v.refcount > 0)
+		return mk(n_VAR, mkw(string addenv(parentenv, v)), nil);
+	c := v.i;
+	(opts, args) := (v.opts, v.args);
+	if(opts == nil && args == nil)
+		return c;
+	env := parentenv;
+	if(genblock := needblock(v))
+		env = ref Renv(nil, 0);
+	for(; opts != nil; opts = tl opts){
+		c = mk(n_ADJ, c, mkw(sys->sprint("-%c", (hd opts).t0)));
+		for(a := (hd opts).t1; a != nil; a = tl a)
+			c = mk(n_ADJ, c, gen(hd a, env));
+	}
+	if(args != nil && len (hd args).i.word > 1 && (hd args).i.word[0] == '-')
+		c = mk(n_ADJ, c, mkw("--"));		# XXX potentially dodgy; some sigs don't interpret "--"?
+
+	# use pipe notation when possible
+	arg0: ref Sh->Cmd;
+	if(args != nil){
+		if((arg0 = gen(hd args, env)).ntype != n_BLOCK){
+			c = mk(n_ADJ, c, arg0);
+			arg0 = nil;
+		}
+		args = tl args;
+	}
+	for(; args != nil; args = tl args)
+		c = mk(n_ADJ, c, gen(hd args, env));
+	if(arg0 != nil)
+		c = mk(n_PIPE, arg0.left, c);
+	if(genblock){
+		args = rev(env.items);
+		m := mkw(Value.type2s((hd args).tc));
+		for(a := tl args; a != nil; a = tl a)
+			m = mk(n_ADJ, m, mkw(Value.type2s((hd a).tc)));
+		c = mk(n_BLOCK, mk(n_SEQ, mk(n_LIST, m, nil), c), nil);
+		return gen(ref Rvalue(c, v.tc, 1, nil, args), parentenv);
+	}
+	return mk(n_BLOCK, c, nil);
+}
+
+addenv(env: ref Renv, v: ref Rvalue): int
+{
+	for(i := env.items; i != nil; i = tl i)
+		if(hd i == v)
+			return len i;
+	env.items = v :: env.items;
+	v.refcount++;
+	return ++env.n;
+}
+
+# need a new block if we have any duplicated values we can resolve locally.
+# i.e. for a particular value, if we're the only thing pointing to that value
+# and its refcount is > 1 to start with.
+needblock(v: ref Rvalue): int
+{
+	dups := getdups(v, nil);
+	for(d := dups; d != nil; d = tl d)
+		--(hd d).refcount;
+	r := 0;
+	for(d = dups; d != nil; d = tl d)
+		if((hd d).refcount++ == 0)
+			r = 1;
+	return r;
+}
+
+# find all values which need $ referencing (but don't go any deeper)
+getdups(v: ref Rvalue, onto: list of ref Rvalue): list of ref Rvalue
+{
+	if(v.refcount > 1)
+		return v :: onto;
+	for(o := v.opts; o != nil; o = tl o)
+		for(a := (hd o).t1; a != nil; a = tl a)
+			onto = getdups(hd a, onto);
+	for(a = v.args; a != nil; a = tl a)
+		onto = getdups(hd a, onto);
+	return onto;
+}
+
+loadtypeset(qname: string, c: chan of ref Typescmd[ref Value], errorc: chan of string): string
+{
+	tsname := canon(qname);
+	if(gettypeset(tsname) != nil)
+		return nil;
+	(parent, name) := splitqname(tsname);
+	if((pts := gettypeset(parent)) == nil)
+		return "parent typeset not found";
+
+	if(pts.c != nil){
+		if(c != nil)
+			return "typecmd channel may only be provided for top-level typesets";
+		reply := chan of (chan of ref Typescmd[ref Value], string);
+		pts.c <-= ref Typescmd[ref Value].Loadtypes(name, reply);
+		err: string;
+		(c, err) = <-reply;
+		if(c == nil)
+			return err;
+	}else if(c == nil){
+		tsmod := load Mainsubtypes "/dis/alphabet/"+name+"types.dis";
+		if(tsmod == nil)
+			return sys->sprint("cannot load %q: %r", name+"types.dis");
+		c = tsmod->proxy();
+	}
+
+	reply := chan of string;
+	c <-= ref Typescmd[ref Value].Alphabet(reply);
+	a := <-reply;
+	ts := ref Typeset(tsname, c, Table[ref Type].new(7, nil), pts);
+	typesets = ts :: typesets;
+	newtypes: list of ref Type;
+	for(i := 0; i < len a; i++){
+		tc := a[i];
+		if((t := ts.parent.gettype(tc)) == nil){
+			t = ref Type(-1, -1, nil, ts, nil, nil);
+			sreply := chan of string;
+			c <-= ref Typescmd[ref Value].Type2s(tc, sreply);
+			t.name = <-sreply;
+			# XXX check that type name is syntactically valid.
+			t.qname = mkqname(tsname, t.name);
+			if(typebyname.find(t.qname) != nil)
+				report(errorc, sys->sprint("warning: oops: typename clash on %q", t.qname));
+			else
+				typebyname.add(t.qname, t);
+			newtypes = t :: newtypes;
+		}
+		ts.types.add(tc, t);
+	}
+	id := len types;
+	types = (array[len types + len newtypes] of ref Type)[0:] = types;
+	for(; newtypes != nil; newtypes = tl newtypes){
+		types[id] = hd newtypes;
+		typebyc.add(currtypec, hd newtypes);
+		types[id].tc = currtypec++;
+		types[id].id = id;
+		id++;
+	}
+	return nil;
+}
+
+autoconvert(src, dst: string, expr: ref Sh->Cmd, errorc: chan of string): string
+{
+	tdst := typebyname.find(dst);
+	if(tdst == nil)
+		return "unknown type " + dst;
+	tsrc := typebyname.find(src);
+	if(tsrc == nil)
+		return "unknown type " + src;
+	if(tdst.typeset != tsrc.typeset && tdst.typeset != roottypeset && tsrc.typeset != roottypeset)
+		return "conversion between incompatible typesets";
+	if(expr != nil && expr.ntype == n_WORD){
+		# mod -> {(srctype); mod $1}
+		expr = mk(n_BLOCK,
+			mk(n_SEQ,
+				mk(n_LIST, mkw(src), nil),
+				mk(n_ADJ,
+					mkw(expr.word),
+					mk(n_VAR, mkw("1"), nil)
+				)
+			),
+			nil
+		);
+	}
+				
+	(e, sig) := rewrite0(expr, tdst.tc, errorc, nil);
+	if(sig == nil)
+		return "cannot rewrite transformation "+sh->cmd2string(expr);
+	if(!evalmod->typecompat(sys->sprint("%c%c", tdst.tc, tsrc.tc), sig))
+		return "incompatible module type";
+	err := addconversion(tsrc, tdst, e);
+	if(err != nil)
+		return sys->sprint("bad auto-conversion %s->%s via %s: %s",
+					tsrc.qname, tdst.qname, sh->cmd2string(expr), err);
+	return nil;
+}
+
+mk(ntype: int, left, right: ref Sh->Cmd): ref Sh->Cmd
+{
+	return ref Sh->Cmd(ntype, left, right, nil, nil);
+}
+mkw(w: string): ref Sh->Cmd
+{
+	return ref Sh->Cmd(n_WORD, nil, nil, w, nil);
+}
+
+declare(qname: string, usig: string, flags: int): string
+{
+	return declare0(qname, usig, flags).t1;
+}
+
+# declare a module.
+# if (flags&ONDEMAND), then we don't need to actually load
+# the module (although we do if (flags&CHECK) or if sig==nil,
+# in order to check or find out the type signature)
+declare0(qname: string, usig: string, flags: int): (ref Module, string)
+{
+	sig, err: string;
+	m: ref Module;
+	if(usig != nil){
+		(sig, err) = evalmod->usage2sig(m, usig);
+		if(sig == nil)
+			return (nil, "bad type sig: " + err);
+	}
+	# if not a qualified name, declare it virtually
+	if(qname != nil && qname[0] != '/'){
+		if(sig == nil)
+			return (nil, "virtual module declaration must include signature");
+		m = ref Module(qname, nil, sig, nil, nil, nil, nil, 0);
+	}else{
+		qname = canon(qname);
+		(typeset, mod) := splitqname(qname);
+		if((ts := gettypeset(typeset)) == nil)
+			return (nil, "unknown typeset");
+		if((m = modules.find(qname)) != nil){
+			if(m.typeset == ts)
+				return (m, nil);
+			return (nil, "already imported");
+		}
+		m = ref Module(mod, ts, sig, nil, nil, nil, nil, 0);
+		if(sig == nil || (flags&CHECK) || (flags&ONDEMAND)==0){
+			if((e := m.ensureloaded()) != nil)
+				return (nil, e);
+			if(flags&ONDEMAND){
+				if(m.c != nil){
+					m.c <-= nil;
+					m.c = nil;
+				}
+				m.m = nil;
+			}
+		}
+	}
+
+	modules.add(qname, m);
+	m.refcount++;
+	return (m, nil);
+}
+
+undeclare(name: string): string
+{
+	m := modules.find(name);
+	if(m == nil)
+		return "module not declared";
+	modules.del(name);
+	if(--m.refcount == 0){
+		if(m.c != nil){
+			m.c <-= nil;
+			m.c = nil;
+		}else if(m.defmods != nil){
+			delmods(m.defmods);
+		}
+	}
+	return nil;
+}
+
+# get info on a module.
+# return (qname, usage, def)
+getmodule(name: string): (string, string, ref Sh->Cmd)
+{
+	(qname, sig, def) := getmodule0(name);
+	if(sig == nil)
+		return (qname, sig, def);
+	v: ref Value;
+	return (qname, evalmod->cmdusage(v, sig), def);
+}
+
+getmodule0(name: string): (string, string, ref Sh->Cmd)
+{
+	m: ref Module;
+	if(name != nil && name[0] != '/'){
+		if((m = modules.find(name)) == nil)
+			return (nil, nil, nil);
+		# XXX could add path searching here.
+	}else{
+		name = canon(name);
+		(typeset, mod) := splitqname(name);
+		if((m = modules.find(name)) == nil){
+			if(autodeclare == 0)
+				return (nil, nil, nil);
+			ts := gettypeset(typeset);
+			if(ts == nil)
+				return (nil, nil, nil);
+			m = ref Module(mod, ts, nil, nil, nil, nil, nil, 0);
+			if((e := m.ensureloaded()) != nil)
+				return (nil, nil, nil);
+			if(m.c != nil)
+				m.c <-= nil;
+		}
+	}
+
+	qname := m.modname;
+	if(m.def == nil && m.typeset != nil)
+		qname = mkqname(m.typeset.name, qname);
+	return (qname, m.sig, m.def);
+}
+
+getmodules(): list of string
+{
+	r: list of string;
+	for(i := 0; i < len modules.items; i++)
+		for(ml := modules.items[i]; ml != nil; ml = tl ml)
+			r = (hd ml).t0 :: r;
+	return r;
+}
+
+#Cmpdeclts: adt {
+#	gt: fn(nil: self ref Cmpdeclts, d1, d2: ref Decltypeset): int
+#};
+#Cmpdeclts.gt(nil: self ref Cmpdeclts, d1, d2: ref Decltypeset)
+#{
+#	return d1.name > d2.name;
+#}
+#Cmpstring: adt {
+#	gt: fn(nil: self ref Cmpdeclts, d1, d2: string): int
+#};
+#Cmpstring.gt(nil: self ref Cmpstring, d1, d2: string): int
+#{
+#	return d1 > d2;
+#}
+#Cmptype: adt {
+#	gt: fn(nil: self ref Cmptype, d1, d2: ref Type): int
+#};
+#Cmptype.gt(nil: self ref Cmptype, d1, d2: ref Type): int
+#{
+#	return d1.name > d2.name;
+#}
+#
+#getdecls(): ref Declarations
+#{
+#	cmptype: ref Cmptype;
+#	d := ref Declarations(array[len typesets] of ref Decltypeset);
+#	i := 0;
+#	ta := array[len types] of ref Type;
+#	for(tsl := typesets; tsl != nil; tsl = tl tsl){
+#		t := hd tsl;
+#		ts := ref Decltypeset;
+#		ts.name = t.name;
+#
+#		# all types in the typeset, in alphabetical order.
+#		j := 0;
+#		for(k := 0; k < len t.types.items; k++)
+#			for(tt := t.types.items[k]; tt != nil; tt = tl tt)
+#				ta[j++] = hd tt;
+#		sort(cmptype, ta[0:j]);
+#		ts.types = array[j] of string;
+#		for(k = 0; k < j; k++){
+#			ts.types[k] = ta[k].name;
+#			ts.alphabet[k] = ta[k].tc;
+#		}
+#
+#		# all modules in the typeset
+#		c := gettypesetmodules(ts.name);
+#		while((m := <-c) != nil){
+#			
+#
+#	d.types = array[len types] of string;
+#	for(i := 0; i < len types; i++){
+#		d.alphabet[i] = types[i].tc;
+#		d.types[i] = types[i].qname;
+#	}
+#	
+
+gettypesetmodules(tsname: string): chan of string
+{
+	ts := gettypeset(tsname);
+	if(ts == nil)
+		return nil;
+	r := chan of string;
+	if(ts.c == nil)
+		spawn mainmodules(r);
+	else
+		ts.c <-= ref Typescmd[ref Value].Modules(r);
+	return r;
+}
+
+mainmodules(r: chan of string)
+{
+	if((readdir := load Readdir Readdir->PATH) != nil){
+		(a, nil) := readdir->init("/dis/alphabet/main", Readdir->NAME|Readdir->COMPACT);
+		for(i := 0; i < len a; i++){
+			m := a[i].name;
+			if((a[i].mode & Sys->DMDIR) == 0 && len m > 4 && m[len m - 4:] == ".dis")
+				r <-= m[0:len m - 4];
+		}
+	}
+	r <-= nil;
+}
+
+gettypes(ts: string): list of string
+{
+	r: list of string;
+	for(i := 0; i < len types; i++){
+		if(ts == nil)
+			r = Value.type2s(types[i].tc) :: r;
+		else if (types[i].typeset.name == ts)
+			r = types[i].name :: r;
+	}
+	return r;
+}
+
+gettypesets(): list of string
+{
+	r: list of string;
+	for(t := typesets; t != nil; t = tl t)
+		r = (hd t).name :: r;
+	return r;
+}
+
+getautoconversions(): list of (string, string, ref Sh->Cmd)
+{
+	cl: list of (string, string, ref Sh->Cmd);
+	for(i := 0; i < len types; i++){
+		if(types[i] == nil)
+			continue;
+		srct := Value.type2s(types[i].tc);
+		for(l := types[i].transform; l != nil; l = tl l)
+			cl = (srct, Value.type2s(types[(hd l).dst].tc), (hd l).expr) :: cl;
+	}
+	return cl;
+}
+
+importmodule(qname: string): string
+{
+	qname = canon(qname);
+	(typeset, mod) := splitqname(qname);
+	if(typeset == nil)
+		return "unknown typeset";
+	if((m := modules.find(mod)) != nil){
+		if(m.typeset == nil)
+			return "already defined";
+		if(m.typeset.name == typeset)
+			return nil;
+		return "already imported from "+m.typeset.name;
+	}
+	if((m = modules.find(qname)) == nil){
+		if(autodeclare == 0)
+			return "module not declared";
+		err: string;
+		(m, err) = Module.find(nil, qname);
+		if(m == nil)
+			return "cannot import: "+ err;
+		modules.add(qname, m);
+		m.refcount++;
+	}
+	modules.add(mod, m);
+	return nil;
+}
+
+
+gettypeset(name: string): ref Typeset
+{
+	name = canon(name);
+	for(l := typesets; l != nil; l = tl l)
+		if((hd l).name == name)
+			break;
+	if(l == nil)
+		return nil;
+	return hd l;
+}
+
+importtype(qname: string): string
+{
+	qname = canon(qname);
+	(typeset, tname) := splitqname(qname);
+	if((ts := gettypeset(typeset)) == nil)
+		return "unknown typeset";
+	t := typebyname.find(tname);
+	if(t != nil){
+		if(t.typeset == ts)
+			return nil;
+		return "type already imported from " + t.typeset.name;
+	}
+	t = typebyname.find(qname);
+	if(t == nil)
+		return sys->sprint("%s does not hold type %s", typeset, tname);
+	typebyname.add(tname, t);
+	return nil;
+}
+
+importvalue(v: ref Value, tname: string): (ref Value, string)
+{
+	if(v == nil || tagof v != tagof Value.Vz)
+		return (v, nil);
+	if(tname == nil || tname[0] == '/')
+		tname = canon(tname);
+	t := typebyname.find(tname);
+	if(t == nil)
+		return (nil, "no such type");
+	pick xv := v {
+	Vz =>
+		if(t.typeset.types.find(xv.i.typec) != t)
+			return (nil, "value appears to be of different type");
+		xv.i.typec = t.tc;
+	}
+	return (v, nil);
+}
+
+gettype(tc: int): ref Type
+{
+	return typebyc.find(tc);
+}
+
+Typeset.gettype(ts: self ref Typeset, tc: int): ref Type
+{
+	return ts.types.find(tc);
+}
+
+Module.find(ctxt: ref Evalctxt, name: string): (ref Module, string)
+{
+	mods := modules;
+	if(ctxt != nil)
+		mods = ctxt.modules;
+	m := mods.find(name);
+	if(m == nil){
+		if(autodeclare == 0 || name == nil || name[0] != '/')
+			return (nil, "module not declared");
+		err: string;
+		(m, err) = declare0(name, nil, 0);
+		if(m == nil)
+			return (nil, err);
+	}else if((err := m.ensureloaded()) != nil)
+		return (nil, err);
+	return (m, nil);
+}
+
+Module.ensureloaded(m: self ref Module): string
+{
+	if(m.c != nil || m.m != nil || m.def != nil || m.typeset == nil)
+		return nil;
+
+	sig: string;
+	if(m.typeset.c == nil){
+		p := "/dis/alphabet/main/" + m.modname + ".dis";
+		mod := load Mainmodule p;
+		if(mod == nil)
+			return sys->sprint("cannot load %q: %r", p);
+		{
+			mod->init();
+		} exception e {
+		"fail:*" =>
+			return sys->sprint("init %q failed: %s", m.modname, e[5:]);
+		}
+		m.m = mod;
+		sig = mod->typesig();
+	}else{
+		reply := chan of (chan of ref Modulecmd[ref Value], string);
+		m.typeset.c <-= ref Typescmd[ref Value].Load(m.modname, reply);
+		(mc, err) := <-reply;
+		if(mc == nil)
+			return sys->sprint("cannot load: %s", err);
+		m.c = mc;
+		sig = gettypesig(m);
+	}
+	if(m.sig == nil)
+		m.sig = sig;
+	else if(!evalmod->typecompat(m.sig, sig)){
+		v: ref Value;
+		if(m.c != nil){
+			m.c <-= nil;
+			m.c = nil;
+		}
+		m.m = nil;
+		return sys->sprint("%q not compatible with %q (%q vs %q, %d)",
+			m.modname+" "+evalmod->cmdusage(v, sig),
+			evalmod->cmdusage(v, m.sig), m.sig, sig, m.sig==sig);
+	}
+	return nil;
+}
+
+Module.typesig(m: self ref Module): string
+{
+	return m.sig;
+}
+
+# get the type signature of a module in its native typeset.
+# it's not valid to call this on defined or virtually declared modules.
+gettypesig(m: ref Module): string
+{
+	reply := chan of string;
+	m.c <-= ref Modulecmd[ref Value].Typesig(reply);
+	sig := <-reply;
+	origsig := sig;
+	for(i := 0; i < len sig; i++){
+		tc := sig[i];
+		if(tc == '-'){
+			i++;
+			continue;
+		}
+		if(tc != '*'){
+			t := m.typeset.gettype(sig[i]);
+			if(t == nil){
+sys->print("no type found for '%c' in sig %q\n", sig[i], origsig);
+				return nil;		# XXX is it alright to break here?
+			}
+			sig[i] = t.tc;
+		}
+	}
+	return sig;
+}
+
+Module.run(m: self ref Module, ctxt: ref Evalctxt, errorc: chan of string, opts: list of (int, list of ref Value), args: list of ref Value): ref Value
+{
+	if(m.c != nil){
+		reply := chan of ref Value;
+		m.c <-= ref Modulecmd[ref Value].Run(ctxt.drawctxt, ctxt.report, errorc, opts, args, reply);
+		if((v := <-reply) != nil){
+			pick xv := v {
+			Vz =>
+				xv.i.typec = m.typeset.types.find(xv.i.typec).tc;
+			}
+		}
+		return v;
+	}else if(m.def != nil){
+		c: Eval->Context[ref Value, ref Module, ref Evalctxt];
+		return c.eval(m.def, ref Evalctxt(m.defmods, ctxt.drawctxt, ctxt.report), errorc, args);
+	}else if(m.typeset != nil){
+		v := m.m->run(ctxt.drawctxt, ctxt.report, errorc, opts, args);
+		free(opts, args, v != nil);
+		return v;
+	}
+	report(errorc, "error: cannot run a virtually declared module");
+	return nil;
+}
+
+free[V](opts: list of (int, list of V), args: list of V, used: int)
+	for{
+	V =>
+		free: fn(v: self V, used: int);
+	}
+{
+	for(; args != nil; args = tl args)
+		(hd args).free(used);
+	for(; opts != nil; opts = tl opts)
+		for(args = (hd opts).t1; args != nil; args = tl args)
+			(hd args).free(used);
+}
+
+Module.typename2c(s: string): int
+{
+	if((t := typebyname.find(s)) == nil)
+		return -1;
+	return t.tc;
+}
+
+Module.cvt(ctxt: ref Evalctxt, v: ref Value, tc: int, errorc: chan of string): ref Value
+{
+	if(v == nil)
+		return nil;
+	srctc := v.typec();
+	dstid := gettype(tc).id;
+	while((vtc := v.typec()) != tc){
+		# XXX assumes v always returns a valid typec: might that be dangerous?
+		for(l := gettype(vtc).transform; l != nil; l = tl l)
+			if((hd l).all.holds(dstid))
+				break;
+		if(l == nil){
+			report(errorc, sys->sprint("error: no way to get from %s to %s", gettype(v.typec()).qname,
+					types[dstid].qname));
+			v.free(0);
+			return nil;		# should only happen the first time.
+		}
+		t := hd l;
+		c: Eval->Context[ref Value, ref Module, ref Evalctxt];
+		nv := c.eval(t.expr, ctxt, errorc, v::nil);
+		if(nv == nil){
+			report(errorc, sys->sprint("error: autoconvert %q failed", sh->cmd2string(t.expr)));
+			return nil;
+		}
+		v = nv;
+	}
+	return v;
+}
+
+Module.mks(nil: ref Evalctxt, s: string): ref Value
+{
+	return ref Value.Vs(s);
+}
+
+Module.mkc(nil: ref Evalctxt, c: ref Sh->Cmd): ref Value
+{
+	return ref Value.Vc(c);
+}
+
+show()
+{
+	for(i := 0; i < len types; i++){
+		if(types[i] == nil)
+			continue;
+		sys->print("%s =>\n", types[i].qname);
+		for(l := types[i].transform; l != nil; l = tl l)
+			sys->print("\t%s -> %s {%s}\n", set2s((hd l).all), types[(hd l).dst].qname, sh->cmd2string((hd l).expr));
+	}
+}
+
+set2s(set: Set): string
+{
+	s := "{";
+	for(i := 0; i < len types; i++){
+		if(set.holds(i)){
+			if(len s > 1)
+				s[len s] = ' ';
+			s += types[i].qname;
+		}
+	}
+	return s + "}";
+}
+
+Value.dup(v: self ref Value): ref Value
+{
+	if(v == nil)
+		return nil;
+	pick xv := v {
+	Vr =>
+		return nil;
+	Vd =>
+		return nil;
+	Vf or
+	Vw =>
+		return nil;
+	Vz =>
+		rc := chan of ref Value;
+		gettype(xv.i.typec).typeset.c <-= ref Typescmd[ref Value].Dup(xv, rc);
+		nv := <-rc;
+		if(nv == nil)
+			return nil;
+		if(nv == v)
+			return v;
+		pick nxv := nv {
+		Vz =>
+			if(nxv.i.typec == xv.i.typec)
+				return nxv;
+		}
+		sys->print("oh dear, invalid duplicated value from typeset %s\n",  gettype(xv.i.typec).typeset.name);
+		return nil;
+	}
+	return v;
+}
+
+Value.typec(v: self ref Value): int
+{
+	pick xv := v {
+	Vc =>
+		return 'c';
+	Vs =>
+		return 's';
+	Vr =>
+		return 'r';
+	Vf =>
+		return 'f';
+	Vw =>
+		return 'w';
+	Vd =>
+		return 'd';
+	Vz =>
+		return xv.i.typec;
+	}
+}
+
+Value.typename(v: self ref Value): string
+{
+	return Value.type2s(v.typec());
+}
+
+Value.free(v: self ref Value, used: int)
+{
+	if(v == nil)
+		return;
+	pick xv := v {
+	Vr =>
+		if(!used)
+			xv.i <-= "stop";
+	Vf or
+	Vw=>
+		if(!used){
+			<-xv.i;
+			xv.i <-= nil;
+		}
+	Vd =>
+		if(!used){
+			alt{
+			xv.i.stop <-= 1 =>
+				;
+			* =>
+				;
+			}
+		}
+	Vz =>
+		gettype(xv.i.typec).typeset.c <-= ref Typescmd[ref Value].Free(xv, used, reply := chan of int);
+		<-reply;
+	}
+}
+
+Value.isstring(v: self ref Value): int
+{
+	return tagof v == tagof Value.Vs;
+}
+Value.gets(v: self ref Value): string
+{
+	return v.s().i;
+}
+Value.c(v: self ref Value): ref Value.Vc
+{
+	pick xv :=v {Vc => return xv;}
+	raise "type error";
+}
+Value.s(v: self ref Value): ref Value.Vs
+{
+	pick xv :=v {Vs => return xv;}
+	raise "type error";
+}
+Value.r(v: self ref Value): ref Value.Vr
+{
+	pick xv :=v {Vr => return xv;}
+	raise "type error";
+}
+Value.f(v: self ref Value): ref Value.Vf
+{
+	pick xv :=v {Vf => return xv;}
+	raise "type error";
+}
+Value.w(v: self ref Value): ref Value.Vw
+{
+	pick xv :=v {Vw => return xv;}
+	raise "type error";
+}
+Value.d(v: self ref Value): ref Value.Vd
+{
+	pick xv :=v {Vd => return xv;}
+	raise "type error";
+}
+Value.z(v: self ref Value): ref Value.Vz
+{
+	pick xv :=v {Vz => return xv;}
+	raise "type error";
+}
+
+Value.type2s(tc: int): string
+{
+	t := gettype(tc);
+	if(t == nil)
+		return "unknown";
+	if(typebyname.find(t.name) == t)
+		return t.name;
+	return t.qname;
+}
+
+Rmodule.find(ctxt: ref Revalctxt, s: string): (ref Rmodule, string)
+{
+	m := ctxt.modules.find(s);
+	if(m == nil){
+		if(autodeclare == 0 || s == nil || s[0] != '/')
+			return (nil, "module not declared");
+		if(ctxt.modules != modules)
+			return (nil, "shouldn't happen: module not found in defined block");
+		err: string;
+		(m, err) = declare0(s, nil, ONDEMAND);
+		if(m == nil)
+			return (nil, err);
+	}
+	return (ref Rmodule(m), nil);
+}
+
+Rmodule.cvt(ctxt: ref Revalctxt, v: ref Rvalue, tc: int, errorc: chan of string): ref Rvalue
+{
+	if(v == nil)
+		return nil;
+	srctc := v.typec();
+	dstid := gettype(tc).id;
+	while((vtc := v.typec()) != tc){
+		# XXX assumes v always returns a valid typec: might that be dangerous?
+		for(l := gettype(vtc).transform; l != nil; l = tl l)
+			if((hd l).all.holds(dstid))
+				break;
+		if(l == nil){
+			report(errorc, sys->sprint("error: no way to get from %s to %s", gettype(v.typec()).qname,
+					types[dstid].qname));
+			return nil;		# should only happen the first time.
+		}
+		t := hd l;
+		c: Eval->Context[ref Rvalue, ref Rmodule, ref Revalctxt];
+		v = c.eval(t.expr, ctxt, errorc, v::nil);
+	}
+	return v;
+}
+
+Rmodule.typesig(m: self ref Rmodule): string
+{
+	return m.m.sig;
+}
+
+Rmodule.typename2c(name: string): int
+{
+	return Module.typename2c(name);
+}
+
+Rmodule.mks(ctxt: ref Revalctxt, s: string): ref Rvalue
+{
+	v := ref Rvalue(mkw(s), 's', 0, nil, nil);
+	ctxt.vals = v :: ctxt.vals;
+	return v;
+}
+
+Rmodule.mkc(ctxt: ref Revalctxt, c: ref Sh->Cmd): ref Rvalue
+{
+	v := ref Rvalue(mk(n_BQ2, c, nil), 'c', 0, nil, nil);
+	ctxt.vals = v :: ctxt.vals;
+	return v;
+}
+
+Rmodule.run(m: self ref Rmodule, ctxt: ref Revalctxt, errorc: chan of string,
+		opts: list of (int, list of ref Rvalue), args: list of ref Rvalue): ref Rvalue
+{
+	if(ctxt.defs && m.m.def != nil){
+		c: Eval->Context[ref Rvalue, ref Rmodule, ref Revalctxt];
+		nctxt := ref Revalctxt(m.m.defmods, ctxt.used, ctxt.defs, ctxt.vals);
+		v := c.eval(m.m.def, nctxt, errorc, args);
+		ctxt.vals = nctxt.vals;
+		return v;
+	}
+	name := mkqname(m.m.typeset.name, m.m.modname);
+	if(ctxt.used != nil){
+		ctxt.used.add(name, m.m);
+		m.m.refcount++;
+	}
+	v := ref Rvalue(mkw(name), m.m.sig[0], 0, opts, args);
+	if(args == nil && opts == nil)
+		v.i = mk(n_BLOCK, v.i, nil);
+	for(; args != nil; args = tl args)
+		(hd args).refcount++;
+	for(; opts != nil; opts = tl opts)
+		for(args = (hd opts).t1; args != nil; args = tl args)
+			(hd args).refcount++;
+	ctxt.vals = v :: ctxt.vals;
+	return v;
+}
+
+Rvalue.dup(v: self ref Rvalue): ref Rvalue
+{
+	return v;
+}
+	
+Rvalue.free(nil: self ref Rvalue, nil: int)
+{
+	# XXX perhaps there should be some way of finding out whether a particular
+	# type will allow duplication of values or not.
+}
+
+Rvalue.isstring(v: self ref Rvalue): int
+{
+	return v.tc == 's';
+}
+
+Rvalue.gets(t: self ref Rvalue): string
+{
+	return t.i.word;
+}
+
+Rvalue.type2s(tc: int): string
+{
+	return Value.type2s(tc);
+}
+
+Rvalue.typec(t: self ref Rvalue): int
+{
+	return t.tc;
+}
+
+addconversion(src, dst: ref Type, expr: ref Sh->Cmd): string
+{
+	# allow the same transform to be added again
+	for(l := src.transform; l != nil; l = tl l)
+		if((hd l).all.holds(dst.id)){
+			if((hd l).dst == dst.id && sh->cmd2string((hd l).expr) == sh->cmd2string(expr))
+				return nil;
+		}
+
+	reached := array[len types/8+1] of {* => byte 0};
+	if((at := ambiguous(dst, reached)) != nil)
+		return sys->sprint("ambiguity: %s", at);
+
+	src.transform = ref Transform(dst.id, sets->bytes2set(reached), expr) :: src.transform;
+	# check we haven't created ambiguity in nodes that point to src.
+	for(i := 0; i < len types; i++){
+		for(l = types[i].transform; l != nil; l = tl l){
+			if((hd l).all.holds(src.id) && (at = ambiguous(types[i], array[len types/8+1] of {* => byte 0})) != nil){
+				src.transform = tl src.transform;
+				return sys->sprint("ambiguity: %s", at);
+			}
+		}
+	}
+	all := (Sets->None).add(dst.id);
+	for(l = types[dst.id].transform; l != nil; l = tl l)
+		all = all.X(Sets->A|Sets->B, (hd l).all);
+	# add everything pointed to by dst to the all sets of those types
+	# that had previously pointed (indirectly) to src
+	for(i = 0; i < len types; i++)
+		for(l = types[i].transform; l != nil; l = tl l)
+			if((hd l).all.holds(src.id))
+				(hd l).all = (hd l).all.X(Sets->A|Sets->B, all);
+	return nil;
+}
+
+ambiguous(t: ref Type, reached: array of byte): string
+{
+	if((dt := ambiguous1(t, reached)) == nil)
+		return nil;
+	(nil, at) := findambiguous(t, dt, array[len reached] of {* =>byte 0}, "self "+types[t.id].qname);
+	s := hd at;
+	for(at = tl at; at != nil; at = tl at)
+		s += ", " + hd at;
+	return s;
+}
+
+# a conversion is ambiguous if there's more than one
+# way of reaching the same type.
+# return the type at which the ambiguity is found.
+ambiguous1(t: ref Type, reached: array of byte): ref Type
+{
+	if(bsetholds(reached, t.id))
+		return t;
+	bsetadd(reached, t.id);
+	for(l := t.transform; l != nil; l = tl l)
+		if((at := ambiguous1(types[(hd l).dst], reached)) != nil)
+			return at;
+	return nil;
+}
+
+findambiguous(t: ref Type, dt: ref Type, reached: array of byte, s: string): (int, list of string)
+{
+	a: list of string;
+	if(t == dt)
+		a = s :: nil;
+	if(bsetholds(reached, t.id))
+		return (1, a);
+	bsetadd(reached, t.id);
+	for(l := t.transform; l != nil; l = tl l){
+		(found, at) := findambiguous(types[(hd l).dst], dt, reached,
+				sys->sprint("%s|%s", s, sh->cmd2string((hd l).expr)));	# XXX rewite correctly
+		for(; at != nil; at = tl at)
+			a = hd at :: a;
+		if(found)
+			return (1, a);
+	}
+	return (0, a);
+}
+
+bsetholds(x: array of byte, n: int): int
+{
+	return int x[n >> 3] & (1 << (n & 7));
+}
+
+bsetadd(x: array of byte, n: int)
+{
+	x[n >> 3] |= byte (1 << (n & 7));
+}
+
+mkqname(parent, child: string): string
+{
+	if(parent == "/")
+		return parent+child;
+	return parent+"/"+child;
+}
+
+# splits a canonical qname into typeset and name components.
+splitqname(name: string): (string, string)
+{
+	if(name == nil)
+		return (nil, nil);
+	for(i := len name - 1; i >= 0; i--)
+		if(name[i] == '/')
+			break;
+	if(i == 0)
+		return ("/", name[1:]);
+	return (name[0:i], name[i+1:]);
+}
+
+# compress multiple slashes into single; remove trailing slashes.
+canon(name: string): string
+{
+	if(name == nil || name[0] != '/')
+		return nil;
+
+	slash := nonslash := 0;
+	s := "";
+	for(i := 0; i < len name; i++){
+		c := name[i];
+		if(c == '/')
+			slash = 1;
+		else{
+			if(slash){
+				s[len s] = '/';
+				nonslash++;
+				slash = 0;
+			}
+			s[len s] = c;
+		}
+	}
+	if(slash && !nonslash)
+		s[len s] = '/';
+	return s;
+}
+
+report(errorc: chan of string, s: string)
+{
+	if(Debug || errorc == nil)
+		sys->fprint(sys->fildes(2), "%s\n", s);
+	if(errorc != nil)
+		errorc <-= s;
+}
+
+Table[T].new(nslots: int, nilval: T): ref Table[T]
+{
+	if(nslots == 0)
+		nslots = 13;
+	return ref Table[T](array[nslots] of list of (int, T), nilval);
+}
+
+Table[T].add(t: self ref Table[T], id: int, x: T): int
+{
+	slot := id % len t.items;
+	for(q := t.items[slot]; q != nil; q = tl q)
+		if((hd q).t0 == id)
+			return 0;
+	t.items[slot] = (id, x) :: t.items[slot];
+	return 1;
+}
+
+Table[T].del(t: self ref Table[T], id: int): int
+{
+	slot := id % len t.items;
+	
+	p: list of (int, T);
+	r := 0;
+	for(q := t.items[slot]; q != nil; q = tl q){
+		if((hd q).t0 == id){
+			p = joinip(p, tl q);
+			r = 1;
+			break;
+		}
+		p = hd q :: p;
+	}
+	t.items[slot] = p;
+	return r;
+}
+
+Table[T].find(t: self ref Table[T], id: int): T
+{
+	for(p := t.items[id % len t.items]; p != nil; p = tl p)
+		if((hd p).t0 == id)
+			return (hd p).t1;
+	return t.nilval;
+}
+
+hashfn(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
+
+Strhash[T].new(nslots: int, nilval: T): ref Strhash[T]
+{
+	if(nslots == 0)
+		nslots = 13;
+	return ref Strhash[T](array[nslots] of list of (string, T), nilval);
+}
+
+Strhash[T].add(t: self ref Strhash, id: string, x: T)
+{
+	slot := hashfn(id, len t.items);
+	t.items[slot] = (id, x) :: t.items[slot];
+}
+
+Strhash[T].del(t: self ref Strhash, id: string)
+{
+	slot := hashfn(id, len t.items);
+
+	p: list of (string, T);
+	for(q := t.items[slot]; q != nil; q = tl q)
+		if((hd q).t0 != id)
+			p = hd q :: p;
+	t.items[slot] = p;
+}
+
+Strhash[T].find(t: self ref Strhash, id: string): T
+{
+	for(p := t.items[hashfn(id, len t.items)]; p != nil; p = tl p)
+		if((hd p).t0 == id)
+			return (hd p).t1;
+	return t.nilval;
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
+
+# join x to y, leaving result in arbitrary order.
+join[T](x, y: list of T): list of T
+{
+	if(len x > len y)
+		(x, y) = (y, x);
+	for(; x != nil; x = tl x)
+		y = hd x :: y;
+	return y;
+}
+
+# join x to y, leaving result in arbitrary order.
+joinip[T](x, y: list of (int, T)): list of (int, T)
+{
+	if(len x > len y)
+		(x, y) = (y, x);
+	for(; x != nil; x = tl x)
+		y = hd x :: y;
+	return y;
+}
+
+sort[S, T](s: S, a: array of T)
+	for{
+	S =>
+		gt: fn(s: self S, x, y: T): int;
+	}
+{
+	mergesort(s, a, array[len a] of T);
+}
+
+mergesort[S, T](s: S, a, b: array of T)
+	for{
+	S =>
+		gt: fn(s: self S, x, y: T): int;
+	}
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(s, a[0:m], b[0:m]);
+		mergesort(s, a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if(s.gt(b[i], b[j]))
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
--- /dev/null
+++ b/appl/alphabet/alphabet.proto
@@ -1,0 +1,29 @@
+# -{/fs/proto alphabet.proto | /fs/filter {/fs/or {/fs/path /dis} {/fs/not {/fs/or *.dis *.sbl}}} | /fs/write /tmp/blah}
+# -{/fs/proto alphabet.proto | /fs/filter {/fs/not {/fs/or *.dis *.sbl}} | /fs/select {/fs/mode -d}}
+module
+	alphabet.m
+	alphabet
+		+
+dis
+	sh
+		alphabet.dis
+	alphabet
+		+
+	scheduler
+		workflowgen.dis
+appl
+	alphabet
+		+
+	cmd
+		scheduler
+			workflowgen.b
+			tgsimple.b
+			mkfile
+man
+	1
+		sh-alphabet
+		alphabet-main
+		alphabet-grid
+		alphabet-fs
+	2
+		alphabet-intro
--- /dev/null
+++ b/appl/alphabet/alphabet.shmod.b
@@ -1,0 +1,413 @@
+implement Alphabetsh, Shellbuiltin;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context, Listnode: import sh;
+	n_WORD: import sh;
+include "alphabet/reports.m";
+	reports: Reports;
+	report, Report: import reports;
+include "readdir.m";
+	readdir: Readdir;
+include "alphabet.m";
+	alphabet: Alphabet;
+	Value, CHECK, ONDEMAND: import alphabet;
+include "alphabet/abc.m";
+
+Alphabetsh: module {};
+
+myself: Shellbuiltin;
+
+initbuiltin(ctxt: ref Sh->Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	myself = load Shellbuiltin "$self";
+	sh = shmod;
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("file2chan: cannot load self: %r"));
+
+	alphabet = load Alphabet Alphabet->PATH;
+	if(alphabet == nil)
+		ctxt.fail("bad module", sys->sprint("alphabet: cannot load %q: %r", Alphabet->PATH));
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		ctxt.fail("bad module", sys->sprint("alphabet: cannot load %q: %r", Reports->PATH));
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		ctxt.fail("bad module", sys->sprint("alphabet: cannot load %q: %r", Readdir->PATH));
+
+	alphabet->init();
+	alphabet->setautodeclare(1);
+
+	if((decls := ctxt.get("autodeclares")) != nil){
+		for(; decls != nil; decls = tl decls){
+			d := hd decls;
+			if(d.cmd == nil){
+				err: string;
+				(d.cmd, err) = sh->parse(d.word);
+				if(err != nil){
+					sys->fprint(sys->fildes(2), "alphabet: warning: bad autodeclaration: %s\n", err);
+					continue;
+				}
+			}
+			{
+				declares(ctxt, nil::d::nil);
+			}exception{
+			"fail:*" =>
+				;
+			}
+		}
+	}
+
+	ctxt.addbuiltin("declare", myself);
+	ctxt.addbuiltin("declares", myself);
+	ctxt.addbuiltin("undeclare", myself);
+	ctxt.addbuiltin("define", myself);
+	ctxt.addbuiltin("import", myself);
+	ctxt.addbuiltin("autodeclare", myself);
+	ctxt.addbuiltin("type", myself);
+	ctxt.addbuiltin("typeset", myself);
+	ctxt.addbuiltin("autoconvert", myself);
+	ctxt.addbuiltin("-", myself);
+	ctxt.addbuiltin("info", myself);
+	ctxt.addbuiltin("clear", myself);
+
+#	ctxt.addsbuiltin("-", myself);
+	ctxt.addsbuiltin("rewrite", myself);
+	ctxt.addsbuiltin("modules", myself);
+	ctxt.addsbuiltin("types", myself);
+	ctxt.addsbuiltin("usage", myself);
+	return nil;
+}
+
+runbuiltin(c: ref Sh->Context, nil: Sh,
+			cmd: list of ref Listnode, nil: int): string
+{
+	case (hd cmd).word {
+	"declare" =>
+		return declare(c, cmd);
+	"declares" =>
+		return declares(c, cmd);
+	"undeclare" =>
+		return undeclare(c, cmd);
+	"define" =>
+		return define(c, cmd);
+	"import" =>
+		return importf(c, cmd);
+	"type" =>
+		return importtype(c, cmd);
+	"typeset" =>
+		return typeset(c, cmd);
+	"autoconvert" =>
+		return autoconvert(c, cmd);
+	"autodeclare" =>
+		if(len cmd != 2)
+			usage(c, "usage: autodeclare 0/1");
+		alphabet->setautodeclare(int word(hd tl cmd));
+	"info" =>
+		return info(c, cmd);
+	"clear" =>
+		a := load Alphabet Alphabet->PATH;
+		if(a == nil)
+			c.fail("bad module", sys->sprint("alphabet: cannot load %q: %r", Alphabet->PATH));
+		alphabet->quit();
+		alphabet = a;
+		alphabet->init();
+		alphabet->setautodeclare(1);
+	"-" =>
+		return eval(c, cmd);
+	}
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, mod: string, wtype: int): string
+{
+	if(wtype == OTHER){
+		(qname, sig, def) := alphabet->getmodule(mod);
+		if(qname == nil)
+			return nil;
+		s := sys->sprint("declare %q %q", qname, sig);
+		if(def != nil){
+			for(i := len sig-1; i >= 0; i--){
+				if(sig[i] == '>'){
+					sig = sig[0:i-1];
+					break;
+				}
+			}
+			s += sys->sprint("; define %q {(%s); %s}", qname, sig, sh->cmd2string(def));
+		}
+		return s;
+	}
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runsbuiltin(ctxt: ref Context, nil: Sh,
+			argv: list of ref Listnode): list of ref Listnode
+{
+	case (hd argv).word {
+	"rewrite" =>
+		return rewrite(ctxt, argv);
+	"modules" =>
+		return sh->stringlist2list(alphabet->getmodules());
+	"types" =>
+		ts := "";
+		if(tl argv != nil)
+			ts = word(hd tl argv);
+		r := sh->stringlist2list(alphabet->gettypes(ts));
+		if(r == nil)
+			ctxt.fail("error", sys->sprint("unknown typeset %q", ts));
+		return r;
+	"usage" =>
+		if(len argv != 2)
+			usage(ctxt, "usage qname");
+		(qname, u, nil) := alphabet->getmodule(word(hd tl argv));
+		if(qname == nil)
+			ctxt.fail("error", "module not declared");
+		return ref Listnode(nil, u) :: nil;
+	}
+	return nil;
+}
+
+usage(ctxt: ref Context, s: string)
+{
+	ctxt.fail("usage", "usage: " + s);
+}
+
+declares(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if(argv == nil || (hd argv).cmd == nil)
+		ctxt.fail("usage", "usage: declares decls");
+	decls := (hd argv).cmd;
+	declares := load Declares Declares->PATH;
+	if(declares == nil)
+		ctxt.fail("bad module", sys->sprint("alphabet: cannot load %q: %r", Declares->PATH));
+	{
+		declares->init();
+	} exception e {
+	"fail:*" =>
+		ctxt.fail("declares init", e[5:]);
+	}
+
+	spawn printerrors(errorc := chan of string);
+	e := declares->declares(alphabet, decls, errorc, nil);
+	declares->quit();
+	if(e != nil)
+		ctxt.fail("bad declaration", sys->sprint("alphabet: declaration failed: %s", e));
+	return nil;
+}
+
+rewrite(ctxt: ref Sh->Context, argv: list of ref Listnode): list of ref Listnode
+{
+	argv = tl argv;
+	n := len argv;
+	if(n != 1 && n != 2 || (hd argv).cmd == nil)
+		usage(ctxt, "rewrite {expr} [desttype]");
+	spawn printerrors(errorc := chan of string);
+	desttype := "";
+	if(n == 2)
+		desttype = word(hd tl argv);
+	(c, usage) := alphabet->rewrite((hd argv).cmd, desttype, errorc);
+	errorc <-= nil;
+	if(c == nil)
+		raise "fail:bad expression";
+	return (ref Listnode(c, nil) :: ref Listnode(nil, usage) :: nil);
+}
+
+# XXX add support for optional ONDEMAND and CHECK flags
+declare(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	n := len argv;
+	if(n < 1 || n > 2)
+		usage(ctxt, "declare qname [type]");
+	decltype := "";
+	if(n == 2)
+		decltype = word(hd tl argv);
+	e := alphabet->declare(word(hd argv), decltype, 0);
+	if(e != nil)
+		ctxt.fail("error", sys->sprint("cannot declare %s: %s", word(hd argv), e));
+	return nil;
+}
+
+undeclare(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if(argv == nil)
+		usage(ctxt, "undeclare name...");
+	for(; argv != nil; argv = tl argv){
+		if((e := alphabet->undeclare(word(hd argv))) != nil)
+			sys->fprint(sys->fildes(2), "alphabet: cannot undeclare %q: %s\n", word(hd argv), e);
+	}
+	return nil;
+}
+
+# usage define name expr
+define(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if(len argv != 2 || (hd tl argv).cmd == nil)
+		usage(ctxt, "define name {expr}");
+	
+	spawn printerrors(errorc := chan of string);
+
+	err := alphabet->define((hd argv).word, (hd tl argv).cmd, errorc);
+	errorc <-= nil;
+	if(err != nil)
+		raise "fail:bad define: "+err;
+	return nil;
+}
+
+importf(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if(argv == nil)
+		usage(ctxt, "import qname...");
+	errs := 0;
+	for(; argv != nil; argv = tl argv){
+		e := alphabet->importmodule(word(hd argv));
+		if(e != nil){
+			sys->fprint(sys->fildes(2), "alphabet: cannot import %s: %s\n", word(hd argv), e);
+			errs++;
+		}
+	}
+	if(errs)
+		raise "fail:import error";
+	return nil;
+}
+
+importtype(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if(argv == nil)
+		usage(ctxt, "type qname...");
+	errs := 0;
+	for(; argv != nil; argv = tl argv){
+		e := alphabet->importtype(word(hd argv));
+		if(e != nil){
+			sys->fprint(sys->fildes(2), "alphabet: cannot import type %s: %s\n", word(hd argv), e);
+			errs++;
+		}
+	}
+	if(errs)
+		raise "fail:type declare error";
+	return nil;
+}
+
+typeset(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if(len argv != 1)
+		usage(ctxt, "typeset qname");
+	spawn printerrors(errorc := chan of string);
+	e := alphabet->loadtypeset(word(hd argv), nil, errorc);	# XXX errorc?
+	errorc <-= nil;
+	if(e != nil)
+		ctxt.fail("error", sys->sprint("cannot load typeset %q: %s", word(hd argv), e));
+	return nil;
+}
+
+autoconvert(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if(len argv != 3)
+		usage(ctxt, "autoconvert src dst fn");
+	src := word(hd argv);
+	dst := word(hd tl argv);
+	expr := (hd tl tl argv).cmd;
+	if(expr == nil)
+		expr = ref Sh->Cmd(Sh->n_WORD, nil, nil, (hd tl tl argv).word, nil);
+	spawn printerrors(errorc := chan of string);
+	e := alphabet->autoconvert(src, dst, expr, errorc);
+	errorc <-= nil;
+	if(e != nil)
+		ctxt.fail("error", sys->sprint("cannot autoconvert %s to %s via %s: %s",
+				src, dst, word(hd tl tl argv), e));
+	return nil;
+}
+
+info(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	first := 1;
+	if(tl argv != nil)
+		usage(ctxt, "info");
+	for(tsl := alphabet->gettypesets(); tsl != nil; tsl = tl tsl){
+		ts := hd tsl;
+		r := alphabet->gettypesetmodules(ts);
+		if(r == nil)
+			continue;
+		if(first == 0)
+			sys->print("\n");
+		sys->print("typeset %s\n", ts);
+		while((mod := <-r) != nil){
+			(qname, u, nil) := alphabet->getmodule(ts+"/"+mod);
+			if(qname != nil)
+				sys->print("%s %s\n", qname, u);
+		}
+		first = 0;
+	}
+	acl := alphabet->getautoconversions();
+	if(acl != nil)
+		sys->print("\n");
+
+	for(; acl != nil; acl = tl acl){
+		(src, dst, via) := hd acl;
+		sys->print("autoconvert %q %q %s\n", src, dst, sh->cmd2string(via));
+	}
+	return nil;
+}
+
+eval(ctxt: ref Sh->Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if(argv == nil || (hd argv).cmd == nil)
+		usage(ctxt, "- {expr} [arg...]");
+	c := (hd argv).cmd;
+	if(c == nil)
+		c = mkw((hd argv).word);
+
+
+	args: list of ref Value;
+	for(argv = tl argv; argv != nil; argv = tl argv){
+		if((hd argv).cmd != nil)
+			args = ref Value.Vc((hd argv).cmd) :: args;
+		else
+			args = ref Value.Vs((hd argv).word) :: args;
+	}
+	return alphabet->eval(c, ctxt.drawcontext, rev(args));
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
+
+printerrors(c: chan of string)
+{
+	while((s := <-c) != nil)
+		sys->fprint(sys->fildes(2), "e: %s\n", s);
+}
+
+mkw(w: string): ref Sh->Cmd
+{
+	return ref Sh->Cmd(n_WORD, nil, nil, w, nil);
+}
--- /dev/null
+++ b/appl/alphabet/auxi/endpoints.b
@@ -1,0 +1,105 @@
+implement Endpoints;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "string.m";
+	str: String;
+include "sh.m";
+	sh: Sh;
+include "alphabet/endpoints.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	sh = load Sh Sh->PATH;
+	sh->initialise();
+	str = load String String->PATH;
+}
+
+DIR: con "/n/endpoint";
+
+new(nil, addr: string, force: int): string		# XXX don't ignore net directory
+{
+	if(!force && sys->stat(DIR+"/"+addr+"/clone").t0 != -1)
+		return nil;
+	if((e := sh->run(nil, "mount"::"{mntgen}"::DIR::nil)) != nil)
+		return "mount mntgen failed: "+e;
+	if((e = sh->run(nil, "endpointsrv"::addr::DIR+"/"+addr::nil)) != nil)
+		return "endpoint failed: "+e;
+	if((e = sh->run(nil, "listen"::addr::"export"::DIR+"/"+addr::nil)) != nil){
+		sys->unmount(nil, DIR+"/"+addr);
+		return "listen failed: "+e;
+	}
+	return nil;
+}
+
+err(e: string): Endpoint
+{
+	return (nil, nil, e);
+}
+
+create(addr: string): (ref Sys->FD, Endpoint)
+{
+	d := DIR+"/"+addr;
+	fd := sys->open(d+"/clone", Sys->OREAD);
+	if(fd == nil)
+		return (nil, err(sys->sprint("cannot open %s/clone: %r", d)));
+
+	buf := array[1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return (nil, err("read id failed"));
+	s := string buf[0:n];
+	(nt, toks) := sys->tokenize(s, " ");
+	if(nt != 2)
+		return (nil, err(sys->sprint("invalid id read %q", s)));
+	id: string;
+	(addr, id) = (hd toks, hd tl toks);
+	fd = sys->open(d+"/"+id+".in", Sys->OWRITE);
+	if(fd == nil)
+		return (nil, err(sys->sprint("cannot write to %s/%s: %r", d, id)));
+	return (fd, Endpoint(addr, id, nil));
+}
+
+open(net: string, ep: Endpoint): (ref Sys->FD, string)
+{
+	if(hasslash(ep.addr))
+		return (nil, "bad address");
+	if(hasslash(ep.id))
+		return (nil, "bad id");
+	d := DIR+"/"+ep.addr;
+	fd := sys->open(d+"/"+ep.id, Sys->OREAD);
+	if(fd != nil)
+		return (fd, nil);
+	e := sys->sprint("%r");
+	if(sys->stat(d+"/clone").t0 != -1)
+		return (nil, sys->sprint("endpoint does not exist: %s", e));
+	if((e = sh->run(nil, "mount"::"-A"::net+ep.addr::d::nil)) != nil)
+		return (nil, e);
+	fd = sys->open(d+"/"+ep.id, Sys->OREAD);
+	if(fd == nil)
+		return (nil, sys->sprint("endpoint does not exist: %r"));
+	return (fd, nil);
+}
+
+Endpoint.text(ep: self Endpoint): string
+{
+	return sys->sprint("%q %q %q", ep.addr, ep.id, ep.about);
+}
+
+Endpoint.mk(s: string): Endpoint
+{
+	t := str->unquoted(s);
+	if(len t != 3)
+		return err("invalid endpoint string");
+	# XXX could do more validation than this.
+	return (hd t, hd tl t, hd tl tl t);
+}
+
+hasslash(s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '/')
+			return 1;
+	return 0;
+}
--- /dev/null
+++ b/appl/alphabet/auxi/endpointsrv.b
@@ -1,0 +1,58 @@
+implement Endpointsrv;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+Endpointsrv: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if(len argv != 3)
+		fatal("usage: endpointsrv addr [dir]");
+	addr := hd tl argv;
+	dir := hd tl tl argv;
+	if(sys->bind("#s", dir, Sys->MREPL) == -1)
+		fatal(sys->sprint("cannot bind #s onto %q: %r", dir));
+
+	fio := sys->file2chan(dir, "clone");
+	spawn endpointproc(addr, dir, fio);
+}
+
+endpointproc(addr, dir: string, fio: ref Sys->FileIO)
+{
+	n := 0;
+	for(;;) alt {
+	(offset, nil, nil, rc) := <-fio.read =>
+		if(rc != nil){
+			if(offset > 0)
+				rc <-= (nil, nil);
+			else{
+				mkpipe(dir, string n);
+				rc <-= (array of byte (addr+" "+string n++), nil);
+			}
+		}
+	(nil, nil, nil, wc) := <-fio.write =>
+		if(wc != nil)
+			wc <-= (0, "cannot write");
+	}
+}
+
+mkpipe(dir: string, p: string)
+{
+	sys->bind("#|", "/tmp", Sys->MREPL);
+	d := Sys->nulldir;
+	d.name = p;
+	sys->wstat("/tmp/data", d);
+	d.name = p + ".in";
+	sys->wstat("/tmp/data1", d);
+	sys->bind("/tmp", dir, Sys->MBEFORE);
+}
+
+fatal(e: string)
+{
+	sys->fprint(sys->fildes(2), "endpointsrv: %s\n", e);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/alphabet/auxi/fsfilter.b
@@ -1,0 +1,62 @@
+implement Fsfilter;
+include "sys.m";
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	Fschan, Next, Quit, Skip, Down: import Fslib;
+
+filter[T](t: T, src, dst: Fschan)
+	for{
+	T =>
+		query: fn(t: self T, d: ref Sys->Dir, name: string, depth: int): int;
+	}
+{
+	names: list of string;
+	name: string;
+	indent := 0;
+	myreply := chan of int;
+loop:
+	for(;;){
+		(d, reply) := <-src;
+		if(d.dir != nil){
+			p := name;
+			if(indent > 0){
+				if(p != nil && p[len p - 1] != '/')
+					p[len p] = '/';
+			}
+			if(t.query(d.dir, p + d.dir.name, indent) == 0 && indent > 0){
+				reply <-= Next;
+				continue;
+			}
+		}
+		dst <-= (d, myreply);
+		case reply <-= <-myreply {
+		Quit =>
+			break loop;
+		Next =>
+			if(d.dir == nil && d.data == nil){
+				if(--indent == 0)
+					break loop;
+				(name, names) = (hd names, tl names);
+			}
+		Skip =>
+			if(--indent == 0)
+				break loop;
+			(name, names) = (hd names, tl names);
+		Down =>
+			if(d.dir != nil){
+				names = name :: names;
+				if(d.dir.mode & Sys->DMDIR){
+					if(indent == 0)
+						name = d.dir.name;
+					else{
+						if(name[len name - 1] != '/')
+							name[len name] = '/';
+						name += d.dir.name;
+					}
+				}
+				indent++;
+			}
+		}
+	}
+}
--- /dev/null
+++ b/appl/alphabet/auxi/mkfile
@@ -1,0 +1,21 @@
+<../../../mkconfig
+
+TARG=\
+	endpoints.dis\
+	endpointsrv.dis\
+	rexecsrv.dis\
+	fsfilter.dis\
+
+SYSMODULES=\
+	alphabet.m\
+	alphabet/endpoints.m\
+	alphabet/reports.m\
+	draw.m\
+	sh.m\
+	string.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/alphabet
+
+<$ROOT/mkfiles/mkdis
+LIMBOFLAGS=-F $LIMBOFLAGS
--- /dev/null
+++ b/appl/alphabet/auxi/rexecsrv.b
@@ -1,0 +1,301 @@
+implement Rexecsrv;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+include "alphabet/endpoints.m";
+	endpoints: Endpoints;
+	Endpoint: import endpoints;
+include "alphabet/reports.m";
+	reports: Reports;
+	Report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+	Value: import alphabet;
+include "alphabet/abc.m";
+include "alphabet/abctypes.m";
+include "string.m";
+	str: String;
+
+Rexecsrv: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+drawctxt: ref Draw->Context;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	endpoints = load Endpoints Endpoints->PATH;
+	if(endpoints == nil)
+		fatal(sys->sprint("cannot load %s: %r", Endpoints->PATH));
+	endpoints->init();
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		fatal(sys->sprint("cannot load %s: %r", Sh->PATH));
+	sh->initialise();
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		fatal(sys->sprint("cannot load %s: %r", Reports->PATH));
+	str = load String String->PATH;
+	if(str == nil)
+		fatal(sys->sprint("cannot load %s: %r", String->PATH));
+	if(len argv != 3)
+		fatal("usage: rexecsrv dir {decls}");
+	drawctxt = ctxt;
+	if(sys->stat("/n/endpoint/local/clone").t0 == -1)
+		fatal("no local endpoints available");
+	dir := hd tl argv;
+	decls := parse(hd tl tl argv);
+	if(sys->bind("#s", dir, Sys->MREPL) == -1)
+		fatal(sys->sprint("cannot bind #s onto %q: %r", dir));
+
+	alphabet = declares(decls);
+
+	fio := sys->file2chan(dir, "exec");
+	sync := chan of int;
+	spawn rexecproc(sync, fio);
+	<-sync;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+# use one alphabet module to bootstrap another
+# with the desired declarations that we can use to
+# execute external commands.
+declares(decls: ref Sh->Cmd): Alphabet
+{
+	alphabet0 := load Alphabet Alphabet->PATH;
+	if(alphabet0 == nil)
+		fatal(sys->sprint("cannot load %s: %r", Alphabet->PATH));
+	alphabet0->init();
+	abctypes := load Abctypes Abctypes->PATH;
+	if(abctypes == nil)
+		fatal(sys->sprint("cannot load %s: %r", Abctypes->PATH));
+	Abccvt: import abctypes;
+	abc := load Abc Abc->PATH;
+	if(abc == nil)
+		fatal(sys->sprint("cannot load %s: %r", Abc->PATH));
+	abc->init();
+	Value: import abc;
+
+	(c, nil, abccvt) := abctypes->proxy0();
+
+	spawn reports->reportproc(errorc := chan of string, nil, reply := chan of ref Report);
+	r := <-reply;
+	if((err := alphabet0->loadtypeset("/abc", c, nil)) != nil)
+		fatal("cannot load typeset /abc: "+err);
+	alphabet0->setautodeclare(1);
+	spawn alphabet0->eval0(
+		parse("{(/cmd);"+
+			"/abc/abc |"+
+			"/abc/declares $1"+
+			"}"
+		),
+		"/abc/abc",
+		nil,
+		r,
+		r.start("evaldecls"),
+		ref (Alphabet->Value).Vc(decls) :: nil,
+		vc := chan of ref Alphabet->Value
+	);
+	r.enable();
+	av: ref Alphabet->Value;
+wait:
+	for(;;)alt{
+	av = <-vc =>
+		;
+	msg := <-errorc =>
+		if(msg == nil)
+			break wait;
+		sys->fprint(stderr(), "rexecsrv: %s\n", msg);
+	}
+	if(av == nil)
+		fatal("declarations failed");
+	v := abccvt.ext2int(av).dup();
+	alphabet0->av.free(1);
+	pick xv := v {
+	VA =>
+		return xv.i.alphabet;
+	}
+	return nil;
+}
+
+parse(s: string): ref Sh->Cmd
+{
+	(c, err) := sh->parse(s);
+	if(c== nil)
+		fatal(sys->sprint("cannot parse %q: %s", s, err));
+	return c;
+}
+
+lc(cmd: ref Sh->Cmd): ref Sh->Listnode
+{
+	return ref Sh->Listnode(cmd, nil);
+}
+
+lw(word: string): ref Sh->Listnode
+{
+	return ref Sh->Listnode(nil, word);
+}
+
+# write endpoints, cmd
+# read endpoints
+rexecproc(sync: chan of int, fio: ref Sys->FileIO)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	pending: list of (int, string);
+	sync <-= 1;
+	for(;;) alt {
+	(nil, data, fid, wc) := <-fio.write =>
+		if(wc == nil)
+			break;
+		req := string data;
+		l := str->unquoted(req);
+		if(len l != 2 || Endpoint.mk(hd l).addr == nil){
+			wc <-= (0, "bad request");
+			break;
+		}
+		pending = (fid, req) :: pending;
+		wc <-= (0, nil);
+	(offset, nil, fid, rc) := <-fio.read =>
+		if(rc == nil){
+			(pending, nil) = removefid(fid, pending);
+			break;
+		}
+		if(offset > 0){
+			rc <-= (nil, nil);
+			break;
+		}
+		req: string;
+		(pending, req) = removefid(fid, pending);
+		if(req == nil){
+			rc <-= (nil, "no pending exec");
+			break;
+		}
+		l := str->unquoted(req);
+		spawn exec(sync1 := chan of int, Endpoint.mk(hd l), hd tl l, rc);
+		<-sync1;
+	}
+}
+
+gather(errorc: chan of string)
+{
+	s := "";
+	while((e := <-errorc) != nil)
+		s += e + "\n";
+	errorc <-= s;
+}
+
+exec(sync: chan of int, ep: Endpoint, expr: string,
+		rc: chan of (array of byte, string))
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+
+	spawn gather(errorc := chan of string);
+	(c, err) := alphabet->parse(expr);
+	if(c == nil){
+		rc <-= (nil, "parse error: "+err);
+		return;
+	}
+	usage: string;
+	(c, usage) = alphabet->rewrite(c, "/fd", errorc);
+	errorc <-= nil;
+	err = <-errorc;
+	if(c == nil){
+		rc <-= (nil, err);
+		return;
+	}
+	if(!alphabet->typecompat("/fd -> /fd", usage).t0)
+		rc <-= (nil, "incompatible type: "+usage);
+
+	fd0: ref Sys->FD;
+	(fd0, err) = endpoints->open(nil, ep);
+	if(fd0 == nil){
+		rc <-= (nil, err);
+		return;
+	}
+	(fd1, ep1) := endpoints->create("local");
+	if(fd1 == nil){
+		rc <-= (nil, "cannot make endpoints: "+ep1.about);
+		return;
+	}
+	rc <-= (array of byte ep1.text(), nil);
+
+	runcmd(c, fd0, fd1);
+}
+
+fdproc(f: chan of ref Sys->FD, fd0: ref Sys->FD)
+{
+	f <-= fd0;
+	fd1 := <-f;
+	if(fd1 == nil)
+		exit;
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd0, buf, len buf)) > 0)
+		if(sys->write(fd1, buf, n) == -1)
+			break;
+}
+
+runcmd(c: ref Sh->Cmd, fd0, fd1: ref Sys->FD)
+{
+	f := chan of ref Sys->FD;
+	spawn fdproc(f, fd0);
+
+	spawn reports->reportproc(errorc := chan of string, nil, reply := chan of ref Report);
+	r := <-reply;
+	spawn alphabet->eval0(
+		c,
+		"/fd",
+		drawctxt,
+		r,
+		r.start("evalcmd"),
+		ref (Alphabet->Value).Vf(f) :: nil,
+		vc := chan of ref Alphabet->Value
+	);
+	r.enable();
+	av: ref Alphabet->Value;
+wait:
+	for(;;)alt{
+	av = <-vc =>
+		if(av == nil){
+			sys->fprint(stderr(), "rexecsrv: no value received\n");
+			break;
+		}
+		pick v := av {
+		Vf =>
+			<-v.i;
+			v.i <-= fd1;
+		* =>
+			sys->fprint(stderr(), "rexecsrv: can't happen: expression has wrong type '%c'\n",
+					alphabet->v.typec());
+		}
+	msg := <-errorc =>
+		if(msg == nil)
+			break wait;
+		# XXX could queue diagnostics back to caller here.
+		sys->fprint(stderr(), "rexecsrv: %s\n", msg);
+	}
+	sys->write(fd1, array[0] of byte, 0);
+}
+
+removefid(fid: int, l: list of (int, string)): (list of (int, string), string)
+{
+	if(l == nil)
+		return (nil, nil);
+	if((hd l).t0 == fid)
+		return (removefid(fid, tl l).t0, (hd l).t1);
+	(rl, d) := removefid(fid, tl l);
+	return (hd l :: rl, d);
+}
+
+fatal(e: string)
+{
+	sys->fprint(sys->fildes(2), "rexecsrv: %s\n", e);
+	raise "fail:error";
+}
+
--- /dev/null
+++ b/appl/alphabet/declare.sh
@@ -1,0 +1,25 @@
+load std alphabet
+
+type /string /fd /status /cmd /wfd
+
+typeset /fs
+type /fs/fs /fs/entries /fs/gate /fs/selector
+
+typeset /grid
+type /grid/endpoint
+
+autoconvert fd status {(fd); /print $1 1}
+autoconvert string fd /read
+autoconvert cmd string /unparse
+autoconvert wfd fd /w2fd
+
+autoconvert fs entries /fs/entries
+autoconvert string gate /fs/match
+autoconvert entries fd /fs/print
+autoconvert endpoint fd {(endpoint); /grid/local -v $1}
+
+fn pretty {
+	-{
+		/echo {/pretty $1}
+	} ${rewrite $1 /status}
+}
--- /dev/null
+++ b/appl/alphabet/eval.b
@@ -1,0 +1,757 @@
+implement Eval;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	n_BLOCK,  n_VAR, n_BQ, n_BQ2, n_REDIR,
+	n_DUP, n_LIST, n_SEQ, n_CONCAT, n_PIPE, n_ADJ,
+	n_WORD, n_NOWAIT, n_SQUASH, n_COUNT,
+	n_ASSIGN, n_LOCAL: import sh;
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+
+# XXX /usr/inferno/appl/alphabet/eval.b:189: function call type mismatch
+# ... a remarkably uninformative error message!
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	sys->fprint(sys->fildes(2), "eval: cannot load %s: %r\n", path);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	sh = checkload(load Sh Sh->PATH, Sh->PATH);
+}
+
+WORD, VALUE: con iota;
+
+# to do:
+# - change value letters to more appropriate (e.g. fs->f, entries->e, gate->g).
+# - allow shell $variable expansions
+
+Evalstate: adt[V, M, C]
+	for {
+	V =>
+		dup:	fn(t: self V): V;
+		free:		fn(t: self V, used: int);
+		gets:		fn(t: self V): string;
+		isstring:	fn(t: self V): int;
+		type2s:	fn(tc: int): string;
+		typec:	fn(t: self V): int;
+	M =>
+		find: fn(c: C, s: string): (M, string);
+		typesig:	fn(m: self M): string;
+		run:		fn(m: self M, c: C,
+					errorc: chan of string,
+					opts: list of (int, list of V), args: list of V): V;
+		mks:		fn(c: C, s: string): V;
+		mkc: 	fn(c: C, cmd: ref Sh->Cmd): V;
+		typename2c: fn(s: string): int;
+		cvt:		fn(c: C, v: V, tc: int, errorc: chan of string): V;
+	}
+{
+	ctxt: C;
+	errorc: chan of string;
+
+	expr:	fn(e: self ref Evalstate, c: ref Sh->Cmd, env: Env[V]): V;
+	runcmd:	fn(e: self ref Evalstate, cmd: ref Sh->Cmd, arg0: V, args: list of V): V;
+	getargs:	fn(e: self ref Evalstate, c: ref Sh->Cmd, env: Env[V]): (ref Sh->Cmd, list of V);
+	getvar:	fn(e: self ref Evalstate, c: ref Sh->Cmd, env: Env[V]): V;
+};
+
+Env: adt[V]
+	for {
+	V =>
+		free: fn(v: self V, used: int);
+		dup:	fn(v: self V): V;
+	}
+{
+	items: array of V;
+
+	new: fn(args: list of V, nilval: V): Env[V];
+	get:	fn(t: self Env, id: int): V;
+	discard: fn(t: self Env);
+};
+
+Context[V, M, Ectxt].eval(expr: ref Sh->Cmd, ctxt: Ectxt, errorc: chan of string,
+	args: list of V): V
+{
+	if(expr == nil){
+		discardlist(nil, args);
+		return nil;
+	}
+	nilv: V;
+	e := ref Evalstate[V, M, Ectxt](ctxt, errorc);
+	{
+		return e.runcmd(expr, nilv, args);
+	} exception x {
+	"error:*" =>
+		report(e.errorc, x);
+		return nil;
+	}
+}
+
+Evalstate[V,M,C].expr(e: self ref Evalstate, c: ref Sh->Cmd, env: Env[V]): V
+{
+	op: ref Sh->Cmd;
+	args: list of V;
+	arg0: V;
+	case c.ntype {
+	n_PIPE =>
+		if(c.left == nil){
+			# N.B. side effect on env.
+			arg0 = env.items[0];
+			env.items[0] = nil;
+			env.items = env.items[1:];
+		}else
+			arg0 = e.expr(c.left, env);
+		{
+			(op, args) = e.getargs(c.right, env);
+		} exception {
+		"error:*" =>
+			arg0.free(0);
+			raise;
+		}
+	n_ADJ or
+	n_WORD or
+	n_BLOCK or
+	n_BQ2 =>
+		(op, args) = e.getargs(c, env);
+	* =>
+		raise "error: expected pipe, adj or word, got " + sh->cmd2string(c);
+	}
+
+	return e.runcmd(op, arg0, args);
+}
+
+# a b c -> adj(adj('a', 'b'), 'c')
+Evalstate[V,M,C].getargs(e: self ref Evalstate, c: ref Sh->Cmd, env: Env[V]): (ref Sh->Cmd, list of V)
+{
+	# do a quick sanity check of module/command-block type
+	for(d := c; d.ntype == n_ADJ; d = d.left)
+		;
+	if(d.ntype != n_WORD && d.ntype != n_BLOCK)
+		raise "error: expected word or block, got "+sh->cmd2string(d);
+	args: list of V;
+	for(; c.ntype == n_ADJ; c = c.left){
+		r: V;
+		case c.right.ntype {
+		n_VAR =>
+			r = e.getvar(c.right.left, env);
+		n_BLOCK =>
+			r = e.expr(c.right.left, env);
+		n_WORD =>
+			r = M.mks(e.ctxt, deglob(c.right.word));
+		n_BQ2 =>
+			r = M.mkc(e.ctxt, c.right.left);
+		* =>
+			discardlist(nil, args);
+			raise "error: syntax error: expected var, block or word. got "+sh->cmd2string(c);
+		}
+		args = r :: args;
+	}
+	return (c, args);
+}
+	
+Evalstate[V,M,C].getvar(nil: self ref Evalstate, c: ref Sh->Cmd, env: Env[V]): V
+{
+	if(c == nil || c.ntype != n_WORD)
+		raise "error: bad variable name";
+	var := deglob(c.word);
+	v := env.get(int var);
+	if(v == nil)
+		raise sys->sprint("error: $%q not defined or cannot be reused", var);
+	return v;
+}
+
+# get rid of GLOB characters left there by the shell.
+deglob(s: string): string
+{
+	j := 0;
+	for (i := 0; i < len s; i++) {
+		if (s[i] != Sh->GLOB) {
+			if (i != j)		# a worthy optimisation???
+				s[j] = s[i];
+			j++;
+		}
+	}
+	if (i == j)
+		return s;
+	return s[0:j];
+}
+
+Evalstate[V,M,C].runcmd(e: self ref Evalstate, cmd: ref Sh->Cmd, arg0: V, args: list of V): V
+{
+	m: M;
+	sig: string;
+	err: string;
+	if(cmd.ntype == n_WORD){
+		(m, err) = M.find(e.ctxt, cmd.word);
+		if(err != nil){
+			discardlist(nil, arg0::args);
+			raise sys->sprint("error: cannot load %q: %s", cmd.word, err);
+		}
+		sig = m.typesig();
+	}else{
+		(sig, cmd, err) = blocksig0(m, e.ctxt, cmd);
+		if(sig == nil){
+			discardlist(nil, arg0::args);
+			raise sys->sprint("error: invalid command: %s", err);
+		}
+	}
+	ok: int;
+	opts: list of (int, list of V);
+	x: M;
+	(ok, opts, args) = cvtargs(x, e.ctxt, sig, cmd, arg0, args, e.errorc);
+	if(ok == -1){
+		x: V;
+		discardlist(opts, args);
+		raise "error: usage: " + sh->cmd2string(cmd)+" "+cmdusage(x, sig);
+	}
+	if(m != nil){
+		r := m.run(e.ctxt, e.errorc, opts, args);
+		if(r == nil)
+			raise "error: command failed";
+		return r;
+	}else{
+		v: V;	# XXX prevent spurious (?) compiler error message: "type polymorphic type does not have a 'discard' function"
+		env := Env[V].new(args, v);
+		{
+			v = e.expr(cmd, env);
+			env.discard();
+			return v;
+		} exception ex {
+		"error:*" =>
+			env.discard();
+			raise;
+		}
+	}
+}
+
+# {(fd string); walk $2 | merge {unbundle $1}}
+blocksig[M, Ectxt](nilm: M, ctxt: Ectxt, e: ref Sh->Cmd): (string, string)
+	for{
+	M =>
+		typename2c: fn(s: string): int;
+		find:	fn(c: Ectxt, s: string): (M, string);
+		typesig: fn(m: self M): string;
+	}
+{
+	(sig, nil, err) := blocksig0(nilm, ctxt, e);
+	return (sig, err);
+}
+
+# {(fd string); walk $2 | merge {unbundle $1}}
+blocksig0[M, Ectxt](nilm: M, ctxt, e: ref Sh->Cmd): (string, ref Sh->Cmd, string)
+	for{
+	M =>
+		typename2c: fn(s: string): int;
+		find:	fn(c: Ectxt, s: string): (M, string);
+		typesig: fn(m: self M): string;
+	}
+{
+	if(e == nil || e.ntype != n_BLOCK)
+		return (nil, nil, "expected block, got "+sh->cmd2string(e));
+	e = e.left;
+
+	
+	if(e == nil || e.ntype != n_SEQ || e.left == nil || e.left.ntype != n_LIST){
+		(ptc, err) := pipesig(nilm, ctxt, e);
+		if(err != nil)
+			return (nil, nil, err);
+		sig := "a";
+		if(ptc != -1)
+			sig[len sig] = ptc;
+		return (sig, e, nil);
+	}
+
+	r := e.right;
+	e = e.left.left;
+	if(e == nil)
+		return ("a", r, nil);
+	argt: list of string;
+	while(e.ntype == n_ADJ){
+		if(e.right.ntype != n_WORD)
+			return (nil, nil, "bad declaration: expected word, got "+sh->cmd2string(e.right));
+		argt = deglob(e.right.word) :: argt;
+		e = e.left;
+	}
+	if(e.ntype != n_WORD)
+		return (nil, nil, "bad declaration: expected word, got "+sh->cmd2string(e));
+	argt = e.word :: argt;
+	i := 1;
+	sig := "a";
+	(ptc, err) := pipesig(nilm, ctxt, r);
+	if(err != nil)
+		return (nil, nil, err);
+	if(ptc != -1)
+		sig[len sig] = ptc;
+
+	for(a := argt; a != nil; a = tl a){
+		tc := M.typename2c(hd a);
+		if(tc == -1)
+			return (nil, nil, sys->sprint("unknown type %q", hd a));
+		sig[len sig] = tc;
+		i++;
+	}
+	return (sig, r, nil);
+}
+
+# if e represents an expression with an empty first pipe element,
+# return the type of its first argument (-1 if it doesn't).
+# string represents error if module doesn't have a first argument.
+pipesig[M, Ectxt](nilm: M, ctxt: Ectxt, e: ref Sh->Cmd): (int, string)
+	for{
+	M =>
+		typename2c: fn(s: string): int;
+		find:	fn(c: Ectxt, s: string): (M, string);
+		typesig: fn(m: self M): string;
+	}
+{
+	if(e == nil)
+		return (-1, nil);
+	for(; e.ntype == n_PIPE; e = e.left){
+		if(e.left == nil){
+			# find actual module that's being called.
+			for(e = e.right; e.ntype == n_ADJ; e = e.left)
+				;
+			sig: string;
+			if(e.ntype == n_WORD){
+				(m, err) := M.find(ctxt, e.word);
+				if(m == nil)
+					return (-1, err);
+				sig = m.typesig();
+			}
+			else if(e.ntype == n_BLOCK){
+				err: string;
+				(sig, nil, err) = blocksig0(nilm, ctxt, e);
+				if(sig == nil)
+					return (-1, err);
+			}else
+				return (-1, "expected word or block, got "+sh->cmd2string(e));
+			if(len sig < 2)
+				return (-1, "cannot pipe into "+sh->cmd2string(e));
+			return (sig[1], nil);
+		}
+	}
+	return (-1, nil);
+}
+
+cvtargs[M,V,C](nil: M, ctxt: C, otype: string, cmd: ref Sh->Cmd, arg0: V, args: list of V, errorc: chan of string): (int, list of (int, list of V), list of V)
+	for{
+	V =>
+		typec: fn(v: self V): int;
+		isstring: fn(v: self V): int;
+		type2s: fn(tc: int): string;
+		gets: fn(v: self V): string;
+	M =>
+		cvt: fn(c: C, v: V, tc: int, errorc: chan of string): V;
+		mks: fn(c: C, s: string): V;
+	}
+{
+	ok: int;
+	opts: list of (int, list of V);
+	(nil, at, t) := splittype(otype);
+	x: M;
+	(ok, opts, args) = cvtopts(x, ctxt, t, cmd, args, errorc);
+	if(arg0 != nil)
+		args = arg0 :: args;
+	if(ok == -1)
+		return (-1, opts, args);
+	if(len at > 0 && at[0] == '*'){
+		report(errorc, sys->sprint("error: invalid type descriptor %#q for %s", at, sh->cmd2string(cmd)));
+		return (-1, opts, args);
+	}
+	n := len args;
+	if(at != nil && at[len at - 1] == '*'){
+		tc := at[len at - 2];
+		at = at[0:len at - 2];
+		for(i := len at; i < n; i++)
+			at[i] = tc;
+	}
+	if(n != len at){
+		report(errorc, sys->sprint("error: wrong number of arguments (%d/%d) to %s", n, len at, sh->cmd2string(cmd)));
+		return (-1, opts, args);
+	}
+	d: list of V;
+	(ok, args, d) = cvtvalues(x, ctxt, at, cmd, args, errorc);
+	if(ok == -1)
+		args = join(args, d);
+	return (ok, opts, args);
+}
+
+cvtvalues[M,V,C](nil: M, ctxt: C, t: string, cmd: ref Sh->Cmd, args: list of V, errorc: chan of string): (int, list of V, list of V)
+	for{
+	V =>
+		type2s: fn(tc: int): string;
+		typec: fn(v: self V): int;
+	M =>
+		cvt: fn(c: C, v: V, tc: int, errorc: chan of string): V;
+	}
+{
+	cargs: list of V;
+	for(i := 0; i < len t; i++){
+		tc := t[i];
+		if(args == nil){
+			report(errorc, sys->sprint("error: missing argument of type %s for %s", V.type2s(tc), sh->cmd2string(cmd)));
+			return (-1, cargs, args);
+		}
+		v := M.cvt(ctxt, hd args, tc, errorc);
+		if(v == nil){
+			report(errorc, "error: conversion failed for "+sh->cmd2string(cmd));
+			return (-1, cargs, tl args);
+		}
+		cargs = v :: cargs;
+		args = tl args;
+	}
+	return (0, rev(cargs), args);
+}
+
+cvtopts[M,V,C](nil: M, ctxt: C, opttype: string, cmd: ref Sh->Cmd, args: list of V, errorc: chan of string): (int, list of (int, list of V), list of V)
+	for{
+	V =>
+		type2s: fn(tc: int): string;
+		isstring: fn(v: self V): int;
+		typec: fn(v: self V): int;
+		gets: fn(v: self V): string;
+	M =>
+		cvt: fn(c: C, v: V, tc: int, errorc: chan of string): V;
+		mks: fn(c: C, s: string): V;
+	}
+{
+	if(opttype == nil)
+		return (0, nil, args);
+	opts: list of (int, list of V);
+getopts:
+	while(args != nil){
+		s := "";
+		if((hd args).isstring()){
+			s = (hd args).gets();
+			if(s == nil || s[0] != '-' || len s == 1)
+				s = nil;
+			else if(s == "--"){
+				args = tl args;
+				s = nil;
+			}
+		}
+		if(s == nil)
+			return (0, opts, args);
+		s = s[1:];
+		while(len s > 0){
+			opt := s[0];
+			if(((ok, t) := opttypes(opt, opttype)).t0 == -1){
+				report(errorc, sys->sprint("error: unknown option -%c for %s", opt, sh->cmd2string(cmd)));
+				return (-1, opts, args);
+			}
+			if(t == nil){
+				s = s[1:];
+				opts = (opt, nil) :: opts;
+			}else{
+				if(len s > 1)
+					args = M.mks(ctxt, s[1:]) :: tl args;
+				else
+					args = tl args;
+				vl: list of V;
+				x: M;
+				(ok, vl, args) = cvtvalues(x, ctxt, t, cmd, args, errorc);
+				if(ok == -1)
+					return (-1, opts, join(vl, args));
+				opts = (opt, vl) :: opts;
+				continue getopts;
+			}
+		}
+		args = tl args;
+	}
+	return (0, opts, args);
+}
+
+discardlist[V](ol: list of (int, list of V), vl: list of V)
+	for{
+	V =>
+		free: fn(v: self V, used: int);
+	}
+{
+	for(; ol != nil; ol = tl ol)
+		for(ovl := (hd ol).t1; ovl != nil; ovl = tl ovl)
+			vl = (hd ovl) :: vl;
+	for(; vl != nil; vl = tl vl)
+		(hd vl).free(0);
+}
+
+# true if a module with type sig t1 is compatible with a caller that expects t0
+typecompat(t0, t1: string): int
+{
+	(rt0, at0, ot0) := splittype(t0);
+	(rt1, at1, ot1) := splittype(t1);
+
+	if((rt0 != rt1 && rt0 != 'a') || at0 != at1)	# XXX could do better for repeated args.
+		return 0;
+
+	for(i := 1; i < len ot0; i++){
+		for(j := i; j < len ot0; j++)
+			if(ot0[j] == '-')
+				break;
+		(ok, t) := opttypes(ot0[i], ot1);
+		if(ok == -1 || ot0[i+1:j] != t)
+			return 0;
+		i = j;
+	}
+	return 1;
+}
+
+splittype(t: string): (int, string, string)
+{
+	if(t == nil)
+		return (-1, nil, nil);
+	for(i := 1; i < len t; i++)
+		if(t[i] == '-')
+			break;
+	return (t[0], t[1:i], t[i:]);
+}
+
+opttypes(opt: int, opts: string): (int, string)
+{
+	for(i := 1; i < len opts; i++){
+		if(opts[i] == opt && opts[i-1] == '-'){
+			for(j := i+1; j < len opts; j++)
+				if(opts[j] == '-')
+					break;
+			return (0, opts[i+1:j]);
+		}
+	}
+	return (-1, nil);
+}
+
+usage2sig[V](nil: V, u: string): (string, string)
+	for{
+	V =>
+		typename2c: fn(s: string): int;
+	}
+{
+	u[len u] = '\0';
+
+	i := 0;
+	t: int;
+	tok: string;
+
+	# options
+	opts: string;
+	for(;;){
+		(t, tok, i) = optstok(u, i);
+		if(t != '[')
+			break;
+		o := i;
+		(t, tok, i) = optstok(u, i);
+		if(t != '-'){
+			i = o;
+			t = '[';
+			break;
+		}
+		for(j := 0; j < len tok; j++){
+			opts[len opts] = '-';
+			opts[len opts] = tok[j];
+		}
+		for(;;){
+			(t, tok, i) = optstok(u, i);
+			if(t == ']')
+				break;
+			if(t != 't')
+				return (nil, sys->sprint("bad option syntax, got '%c'", t));
+			tc := V.typename2c(tok);
+			if(tc == -1)
+				return (nil, "unknown type: "+tok);
+			opts[len opts] = tc;
+		}
+	}
+
+	# arguments
+	args: string;
+parseargs:
+	for(;;){
+		case t {
+		'>' =>
+			break parseargs;
+		'[' =>
+			(t, tok, i) = optstok(u, i);
+			if(t != 't')
+				return (nil, "bad argument syntax");
+			tc := V.typename2c(tok);
+			if(tc == -1)
+				return (nil, "unknown type: "+tok);
+			if(((t, nil, i) = optstok(u, i)).t0 != '*')
+				return (nil, "bad argument syntax");
+			if(((t, nil, i) = optstok(u, i)).t0 != ']')
+				return (nil, "bad argument syntax");
+			if(((t, nil, i) = optstok(u, i)).t0 != '>')
+				return (nil, "bad argument syntax");
+			args[len args] = tc;
+			args[len args] = '*';
+			break parseargs;
+		't' =>
+			tc := V.typename2c(tok);
+			if(tc == -1)
+				return (nil, "unknown type: "+tok);
+			args[len args] = tc;
+			(t, tok, i) = optstok(u, i);
+		* =>
+			return (nil, "no return type");
+		}
+	}
+
+	# return type
+	(t, tok, i) = optstok(u, i);
+	if(t != 't')
+		return (nil, "expected return type");
+	tc := V.typename2c(tok);
+	if(tc == -1)
+		return (nil, "unknown type: "+tok);
+	r: string;
+	r[0] = tc;
+	r += args;
+	r += opts;
+	return (r, nil);
+}
+
+optstok(u: string, i: int): (int, string, int)
+{
+	while(u[i] == ' ')
+		i++;
+	case u[i] {
+	'\0' =>
+		return (-1, nil, i);
+	'-' =>
+		i++;
+		if(u[i] == '>')
+			return ('>', nil, i+1);
+		start := i;
+		while((c := u[i]) != '\0'){
+			if(c == ']' || c == ' ')
+				break;
+			i++;
+		}
+		return ('-', u[start:i], i);
+	'[' =>
+		return (u[i], nil, i+1);
+	']' =>
+		return (u[i], nil, i+1);
+	'.' =>
+		start := i;
+		while(u[i] == '.')
+			i++;
+		if(i - start < 3)
+			raise "parse:error at '.'";
+		return ('*', nil, i);
+	* =>
+		start := i;
+		while((c := u[i]) != '\0'){
+			if(c == ' ' || c == ']' || c == '-' || (c == '.' && u[i+1] == '.'))
+				return ('t', u[start:i], i);
+			i++;
+		}
+		return ('t', u[start:i], i);
+	}
+}
+
+cmdusage[V](nil: V, t: string): string
+	for{
+	V =>
+		type2s: fn(c: int): string;
+	}
+{
+	if(t == nil)
+		return "-> bad";
+	for(oi := 0; oi < len t; oi++)
+		if(t[oi] == '-')
+			break;
+	s := "";
+	if(oi < len t){
+		single, multi: string;
+		for(i := oi; i < len t - 1;){
+			for(j := i + 1; j < len t; j++)
+				if(t[j] == '-')
+					break;
+
+			optargs := t[i+2:j];
+			if(optargs == nil)
+				single[len single] = t[i+1];
+			else{
+				multi += sys->sprint(" [-%c", t[i+1]);
+				for (k := 0; k < len optargs; k++)
+					multi += " " + V.type2s(optargs[k]);
+				multi += "]";
+			}
+			i = j;
+		}
+		if(single != nil)
+			s += " [-" + single + "]";
+		s += multi;
+	}
+	multi := 0;
+	if(oi > 2 && t[oi - 1] == '*'){
+		multi = 1;
+		oi -= 2;
+	}
+	for(k := 1; k < oi; k++)
+		s += " " + V.type2s(t[k]);
+	if(multi)
+		s += " [" + V.type2s(t[k]) + "...]";
+	s += " -> " + V.type2s(t[0]);
+	if(s[0] == ' ')
+		s=s[1:];
+	return s;
+}
+
+Env[V].new(args: list of V, nilval: V): Env[V]
+{
+	if(args == nil)
+		return Env(nil);
+	e := Env[V](array[len args] of {* => nilval});
+	for(i := 0; args != nil; args = tl args)
+		e.items[i++] = hd args;
+	return e;
+}
+
+Env[V].get(t: self Env, id: int): V
+{
+	id--;
+	if(id < 0 || id >= len t.items)
+		return nil;
+	x := t.items[id];
+	if((y := x.dup()) == nil){
+		t.items[id] = nil;
+		y = x;
+	}
+	return y;
+}
+
+Env[V].discard(t: self Env)
+{
+	for(i := 0; i < len t.items; i++)
+		t.items[i].free(0);
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
+
+# join x to y, leaving result in arbitrary order.
+join[T](x, y: list of T): list of T
+{
+	if(len x > len y)
+		(x, y) = (y, x);
+	for(; x != nil; x = tl x)
+		y = hd x :: y;
+	return y;
+}
--- /dev/null
+++ b/appl/alphabet/extvalues.b
@@ -1,0 +1,49 @@
+implement Extvalues;
+include "sys.m";
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet.m";
+
+Values[V].new(): ref Values[V]
+{
+	v: V;
+	return ref Values[V](chan[1] of int, array[4] of {* => (0, v)}, 0::1::2::3::nil);
+}
+
+Values[V].add(vals: self ref Values, v: V): int
+{
+	vals.lock <-= 1;
+	if(vals.freeids == nil){
+		n := len vals.v;
+		vals.v = (array[len vals.v * 3 / 2] of (int, V))[0:] = vals.v;
+		for(; n < len vals.v; n++)
+			vals.freeids = n :: vals.freeids;
+	}
+	id := hd vals.freeids;
+	vals.freeids = tl vals.freeids;
+	vals.v[id] = (1, v);
+#(load Sys Sys->PATH)->print("add %d\n", id);
+	<-vals.lock;
+	return id;
+}
+
+Values[V].inc(vals: self ref Values, id: int)
+{
+	vals.lock <-= 1;
+	vals.v[id].t0++;
+#(load Sys Sys->PATH)->print("inc %d -> %d\n", id, vals.v[id].t0);
+	<-vals.lock;
+}
+
+Values[V].del(vals: self ref Values, id: int)
+{
+	vals.lock <-= 1;
+	if(--vals.v[id].t0 == 0){
+		vals.v[id].t1 = nil;
+		vals.freeids = id :: vals.freeids;
+	}
+#(load Sys Sys->PATH)->print("del %d -> %d\n", id, vals.v[id].t0);
+	<-vals.lock;
+}
+
--- /dev/null
+++ b/appl/alphabet/fs/and.b
@@ -1,0 +1,70 @@
+implement And,Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+And: module {};
+
+types(): string
+{
+	return "pppp*";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	c := chan of Gatequery;
+	spawn andgate(c, args);
+	return ref Value.Vp(c);
+}
+
+andgate(c: Gatechan, args: list of ref Value)
+{
+	sub: list of Gatechan;
+	for(; args != nil; args = tl args)
+		sub = (hd args).p().i :: sub;
+	sub = rev(sub);
+	myreply := chan of int;
+	while(((d, reply) := <-c).t0.t0 != nil){
+		for(l := sub; l != nil; l = tl l){
+			(hd l) <-= (d, myreply);
+			if(<-myreply == 0)
+				break;
+		}
+		reply <-= l == nil;
+	}
+	for(; sub != nil; sub = tl sub)
+		hd sub <-= (Nilentry, nil);
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
--- /dev/null
+++ b/appl/alphabet/fs/bundle.b
@@ -1,0 +1,210 @@
+implement Bundle, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "readdir.m";
+	readdir: Readdir;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report, quit, report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Bundle: module {};
+
+# XXX if we can't open a directory, is it ever worth passing its metadata
+# through anyway?
+
+EOF: con "end of archive\n";
+
+types(): string
+{
+	return "fx";
+}
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: bundle: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmod(Readdir->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmod(Readdir->PATH);
+	bufio->fopen(nil, Sys->OREAD);		# XXX no bufio->init!
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Readdir->PATH);
+	fs->init();
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+}
+
+run(nil: ref Draw->Context, r: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	f := chan of ref Sys->FD;
+	spawn bundleproc((hd args).x().i, f, r.start("bundle"));
+	return ref Value.Vf(f);
+}
+
+#bundle(r: ref Report, iob: ref Iobuf, c: Fschan): chan of string
+bundle(nil: ref Report, nil: ref Iobuf, nil: Fschan): chan of string
+{
+	return nil;
+#	sync := chan[1] of string;
+#	spawn bundleproc(c, sync, iob, r.start("bundle"));
+#	return sync;
+}
+
+bundleproc(c: Fschan, f: chan of ref Sys->FD, errorc: chan of string)
+{
+	f <-= nil;
+	if((fd := <-f) == nil){
+		(<-c).t1 <-= Quit;
+		quit(errorc);
+	}
+	iob := bufio->fopen(fd, Sys->OWRITE);
+	fd = nil;
+	(d, reply) := <-c;
+	if(d.dir == nil){
+		report(errorc, "no root directory");
+		endarchive(iob, errorc);
+	}
+	if(puts(iob, dir2header(d.dir), errorc) == -1){
+		reply <-= Quit;
+		quit(errorc);
+	}
+	reply <-= Down;
+	bundledir(d.dir.name, d, c, iob, errorc);
+	endarchive(iob, errorc);
+}
+
+endarchive(iob: ref Iobuf, errorc: chan of string)
+{
+	{
+		if(puts(iob, EOF, errorc) != -1)
+			iob.flush();
+		sys->fprint(iob.fd, "");
+	} exception {
+	"write on closed pipe" =>
+		;
+	}
+	quit(errorc);
+}
+
+bundledir(path: string, d: Fsdata,
+		c: Fschan,
+		iob: ref Iobuf, errorc: chan of string)
+{
+	if(d.dir.mode & Sys->DMDIR){
+		path[len path] = '/';
+		for(;;){
+			(ent, reply) := <-c;
+			if(ent.dir == nil){
+				reply <-= Skip;
+				break;
+			}
+			if(puts(iob, dir2header(ent.dir), errorc) == -1){
+				reply <-= Quit;
+				quit(errorc);
+			}
+			reply <-= Down;
+			bundledir(path + ent.dir.name, ent, c, iob, errorc);
+		}
+		iob.putc('\n');
+	}else{
+		buf: array of byte;
+		reply: chan of int;
+		length := big d.dir.length;
+		n := big 0;
+		for(;;){
+			((nil, buf), reply) = <-c;
+			if(buf == nil){
+				reply <-= Skip;
+				break;
+			}
+			if(write(iob, buf, len buf, errorc) != len buf){
+				reply <-= Quit;
+				quit(errorc);
+			}
+			n += big len buf;
+			if(n > length){		# should never happen
+				report(errorc, sys->sprint("%q is longer than expected (fatal)", path));
+				reply <-= Quit;
+				quit(errorc);
+			}
+			if(n == length){
+				reply <-= Skip;
+				break;
+			}
+			reply <-= Next;
+		}
+		if(n < length){
+			report(errorc, sys->sprint("%q is shorter than expected (%bd/%bd); adding null bytes", path, n, length));
+			buf = array[Sys->ATOMICIO] of {* => byte 0};
+			while(n < length){
+				nb := len buf;
+				if(length - n < big len buf)
+					nb = int (length - n);
+				if(write(iob, buf, nb, errorc) != nb){
+					(<-c).t1 <-= Quit;
+					quit(errorc);
+				}
+				report(errorc, sys->sprint("added %d null bytes", nb));
+				n += big nb;
+			}
+		}
+	}
+}
+
+dir2header(d: ref Sys->Dir): string
+{
+	return sys->sprint("%q %uo %q %q %ud %bd\n", d.name, d.mode, d.uid, d.gid, d.mtime, d.length);
+}
+
+puts(iob: ref Iobuf, s: string, errorc: chan of string): int
+{
+	{
+		if(iob.puts(s) == -1)
+			report(errorc, sys->sprint("write error: %r"));
+		return 0;
+	} exception {
+	"write on closed pipe" =>
+		report(errorc, sys->sprint("write on closed pipe"));
+		return -1;
+	}
+}
+
+write(iob: ref Iobuf, buf: array of byte, n: int, errorc: chan of string): int
+{
+	{
+		nw := iob.write(buf, n);
+		if(nw < n){
+			if(nw >= 0)
+				report(errorc, "short write");
+			else{
+				report(errorc, sys->sprint("write error: %r"));
+			}
+		}
+		return nw;
+	} exception {
+	"write on closed pipe" =>
+		report(errorc, "write on closed pipe");
+		return -1;
+	}
+}
--- /dev/null
+++ b/appl/alphabet/fs/bundle.m
@@ -1,0 +1,9 @@
+Bundle: module {
+	PATH: con "/dis/fs/bundle.dis";
+
+	types: fn(): string;
+	init: fn();
+	run: fn(nil: ref Draw->Context, report: ref Reports->Report,
+			nil: list of Fs->Option, args: list of ref Fs->Value): ref Fs->Value;
+	bundle:	fn(r: ref Reports->Report, iob: ref Bufio->Iobuf, c: Fs->Fschan): chan of string;
+};
--- /dev/null
+++ b/appl/alphabet/fs/chstat.b
@@ -1,0 +1,189 @@
+implement Chstat, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet/fs.m";
+	fsfilter: Fsfilter;
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Chstat: module {};
+
+Query: adt {
+	gate: Gatechan;
+	stat: Sys->Dir;
+	mask: int;
+	cflag: int;
+	reply: chan of int;
+
+	query: fn(q: self ref Query, d: ref Sys->Dir, name: string, depth: int): int;
+};
+
+types(): string
+{
+	return "xx-pp-ms-us-gs-ts-as-c";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	
+	fsfilter = load Fsfilter Fsfilter->PATH;
+	if(fsfilter == nil)
+		badmod(Fsfilter->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	ws := Sys->nulldir;
+	mask := 0;
+	gate: ref Value;
+	cflag := 0;
+	for(; opts != nil; opts = tl opts){
+		o := (hd opts).args;
+		case (hd opts).opt {
+		'p' =>
+			gate.free(0);
+			gate = hd o;
+		'm' =>
+			ok: int;
+			m := (hd o).s().i;
+			(ok, mask, ws.mode) = parsemode(m);
+			mask &= ~Sys->DMDIR;
+			if(ok == 0){
+				sys->fprint(sys->fildes(2), "fs: chstat: bad mode %#q\n", m);
+				gate.free(0);
+				return nil;
+			}
+		'u' =>
+			ws.uid = (hd o).s().i;
+		'g' =>
+			ws.gid = (hd o).s().i;
+		't' =>
+			ws.mtime = int (hd o).s().i;
+		'a' =>
+			ws.atime = int (hd o).s().i;
+		'c' =>
+			cflag++;
+		}
+	}
+
+	dst := chan of (Fsdata, chan of int);
+	p: Gatechan;
+	if(gate != nil)
+		p = gate.p().i;
+	spawn chstatproc((hd args).x().i, dst, p, ws, mask, cflag);
+	return ref Value.Vx(dst);
+}
+
+chstatproc(src, dst: Fschan, gate: Gatechan, stat: Sys->Dir, mask: int, cflag: int)
+{
+	fsfilter->filter(ref Query(gate, stat, mask, cflag, chan of int), src, dst);
+	if(gate != nil)
+		gate <-= ((nil, nil, 0), nil);
+}
+
+Query.query(q: self ref Query, d: ref Sys->Dir, name: string, depth: int): int
+{
+	c := 1;
+	if(q.gate != nil){
+		q.gate <-= ((d, name, depth), q.reply);
+		c = <-q.reply;
+	}
+	if(c){
+		if(q.cflag){
+			m := d.mode & 8r700;
+			d.mode = (d.mode & ~8r77)|(m>>3)|(m>>6);
+		}
+		stat := q.stat;
+		d.mode = (d.mode & ~q.mask) | (stat.mode & q.mask);
+		if(stat.uid != nil)
+			d.uid = stat.uid;
+		if(stat.gid != nil)
+			d.gid = stat.gid;
+		if(stat.mtime != ~0)
+			d.mtime = stat.mtime;
+		if(stat.atime != ~0)
+			d.atime = stat.atime;
+	}
+	return 1;
+}
+
+# stolen from /appl/cmd/chmod.b
+User:	con 8r700;
+Group:	con 8r070;
+Other:	con 8r007;
+All:	con User | Group | Other;
+
+Read:	con 8r444;
+Write:	con 8r222;
+Exec:	con 8r111;
+parsemode(spec: string): (int, int, int)
+{
+	mask := Sys->DMAPPEND | Sys->DMEXCL | Sys->DMDIR | Sys->DMAUTH;
+loop:
+	for(i := 0; i < len spec; i++){
+		case spec[i] {
+		'u' =>
+			mask |= User;
+		'g' =>
+			mask |= Group;
+		'o' =>
+			mask |= Other;
+		'a' =>
+			mask |= All;
+		* =>
+			break loop;
+		}
+	}
+	if(i == len spec)
+		return (0, 0, 0);
+	if(i == 0)
+		mask |= All;
+
+	op := spec[i++];
+	if(op != '+' && op != '-' && op != '=')
+		return (0, 0, 0);
+
+	mode := 0;
+	for(; i < len spec; i++){
+		case spec[i]{
+		'r' =>
+			mode |= Read;
+		'w' =>
+			mode |= Write;
+		'x' =>
+			mode |= Exec;
+		'a' =>
+			mode |= Sys->DMAPPEND;
+		'l' =>
+			mode |= Sys->DMEXCL;
+		'd' =>
+			mode |= Sys->DMDIR;
+		'A' =>
+			mode |= Sys->DMAUTH;
+		* =>
+			return (0, 0, 0);
+		}
+	}
+	if(op == '+' || op == '-')
+		mask &= mode;
+	if(op == '-')
+		mode = ~mode;
+	return (1, mask, mode);
+}
--- /dev/null
+++ b/appl/alphabet/fs/compose.b
@@ -1,0 +1,105 @@
+implement Compose, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Cmpchan,
+	Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Compose: module {};
+
+AinB:	con 1<<3;
+BinA:	con 1<<2;
+AoutB:	con 1<<1;
+BoutA:	con 1<<0;
+
+A:		con AinB|AoutB;
+AoverB:	con AinB|AoutB|BoutA;
+AatopB:	con AinB|BoutA;
+AxorB:	con AoutB|BoutA;
+
+B:		con BinA|BoutA;
+BoverA:	con BinA|BoutA|AoutB;
+BatopA:	con BinA|AoutB;
+BxorA:	con BoutA|AoutB;
+
+ops := array[] of {
+	AinB => "AinB",
+	BinA => "BinA",
+	AoutB => "AoutB",
+	BoutA => "BoutA",
+	A => "A",
+	AoverB => "AoverB",
+	AatopB => "AatopB",
+	AxorB => "AxorB",
+	B => "B",
+	BoverA => "BoverA",
+	BatopA => "BatopA",
+};
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+types(): string
+{
+	return "ms-d";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	c := chan of (ref Sys->Dir, ref Sys->Dir, chan of int);
+	s := (hd args).s().i;
+	for(i := 0; i < len ops; i++)
+		if(ops[i] == s)
+			break;
+	if(i == len ops){
+		sys->fprint(sys->fildes(2), "fs: join: bad op %q\n", s);
+		return nil;
+	}
+	spawn compose(c, i, opts != nil);
+	return ref Value.Vm(c);
+}
+
+compose(c: Cmpchan, op: int, dflag: int)
+{
+	t := array[4] of {* => 0};
+	if(op & AinB)
+		t[2r11] = 2r01;
+	if(op & BinA)
+		t[2r11] = 2r10;
+	if(op & AoutB)
+		t[2r01] = 2r01;
+	if(op & BoutA)
+		t[2r10] = 2r10;
+	if(dflag){
+		while(((d0, d1, reply) := <-c).t2 != nil){
+			x := (d1 != nil) << 1 | d0 != nil;
+			r := t[d0 != nil | (d1 != nil) << 1];
+			if(r == 0 && x == 2r11 && (d0.mode & d1.mode & Sys->DMDIR))
+				r = 2r11;
+			reply <-= r;
+		}
+	}else{
+		while(((d0, d1, reply) := <-c).t2 != nil)
+			reply <-= t[(d1 != nil) << 1 | d0 != nil];
+	}
+}
--- /dev/null
+++ b/appl/alphabet/fs/depth.b
@@ -1,0 +1,54 @@
+implement Depth, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Depth: module {};
+
+types(): string
+{
+	return "ps";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	d := int (hd args).s().i;
+	if(d <= 0){
+		sys->fprint(sys->fildes(2), "fs: depth: invalid depth\n");
+		return nil;
+	}
+	c := chan of Gatequery;
+	spawn depthgate(c, d);
+	return ref Value.Vp(c);
+}
+
+depthgate(c: Gatechan, d: int)
+{
+	while((((dir, nil, depth), reply) := <-c).t0.t0 != nil)
+		reply <-= depth <= d;
+}
--- /dev/null
+++ b/appl/alphabet/fs/entries.b
@@ -1,0 +1,91 @@
+implement Entries, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Entries: module {};
+
+types(): string
+{
+	return "tx";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	sc := Entrychan(chan of int, chan of Entry);
+	spawn entriesproc((hd args).x().i, sc);
+	return ref Value.Vt(sc);
+}
+
+entriesproc(c: Fschan, sc: Entrychan)
+{
+	if(<-sc.sync == 0){
+		(<-c).t1 <-= Quit;
+		exit;
+	}
+	indent := 0;
+	names: list of string;
+	name: string;
+loop:
+	for(;;){
+		(d, reply) := <-c;
+		if(d.dir != nil){
+			p: string;
+			depth := indent;
+			if(d.dir.mode & Sys->DMDIR){
+				names = name :: names;
+				if(indent == 0)
+					name = d.dir.name;
+				else{
+					if(name[len name - 1] != '/')
+						name[len name] = '/';
+					name += d.dir.name;
+				}
+				indent++;
+				reply <-= Down;
+				p = name;
+			}else{
+				p = name;
+				if(p[len p - 1] != '/')
+					p[len p] = '/';
+				p += d.dir.name;
+				reply <-= Next;
+			}
+			if(p != nil)
+				sc.c <-= (d.dir, p, depth);
+		}else{
+			reply <-= Next;
+			if(d.dir == nil && d.data == nil){
+				if(--indent == 0)
+					break loop;
+				(name, names) = (hd names, tl names);
+			}
+		}
+	}
+	sc.c <-= Nilentry;
+}
--- /dev/null
+++ b/appl/alphabet/fs/exec.b
@@ -1,0 +1,172 @@
+implement Exec, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "alphabet/reports.m";
+	reports: Reports;
+	Report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Option, Value, Entrychan: import fs;
+
+Exec: module {};
+
+# usage: exec [-n nfiles] [-t endcmd] [-pP] command entries
+types(): string
+{
+	return "rtc-ns-tc-p-P";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: exec: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		badmod(Sh->PATH);
+	sh->initialise();
+}
+
+run(drawctxt: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	n := 1;
+	pflag := 0;
+	tcmd: ref Sh->Cmd;
+	for(; opts != nil; opts = tl opts){
+		o := hd opts;
+		case o.opt {
+		'n' =>
+			if((n = int (hd o.args).s().i) <= 0){
+				sys->fprint(sys->fildes(2), "fs: exec: invalid argument to -n\n");
+				return nil;
+			}
+		't' =>
+			tcmd = (hd o.args).c().i;
+		'p' =>
+			pflag = 1;
+		'P' =>
+			pflag = 2;
+		}
+	}
+	if(pflag && n > 1){
+		sys->fprint(sys->fildes(2), "fs: exec: cannot specify -p with -n %d\n", n);
+		return nil;
+	}
+	cmd := (hd tl args).c().i;
+	c := (hd args).t().i;
+	sync := chan of string;
+	spawn execproc(drawctxt, sync, n, pflag, c, cmd, tcmd, report.start("exec"));
+	sync <-= nil;
+	return ref Value.Vr(sync);
+}
+
+execproc(drawctxt: ref Draw->Context, sync: chan of string, n, pflag: int,
+		c: Entrychan, cmd, tcmd: ref Sh->Cmd, errorc: chan of string)
+{
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	ctxt := Context.new(drawctxt);
+	<-sync;
+	if(<-sync != nil){
+		c.sync <-= 0;
+		errorc <-= nil;
+		exit;
+	}
+	c.sync <-= 1;
+	argv := ref Sh->Listnode(cmd, nil) :: nil;
+
+	fl: list of ref Sh->Listnode;
+	nf := 0;
+	while(((d, p, nil) := <-c.c).t0 != nil){
+		fl = ref Sh->Listnode(nil, p) :: fl;
+		if(++nf >= n){
+			ctxt.set("file", rev(fl));
+			if(pflag)
+				setstatenv(ctxt, d, pflag);
+			fl = nil;
+			nf = 0;
+			{ctxt.run(argv, 0);} exception {"fail:*" =>;}
+		}
+	}
+	if(nf > 0){
+		ctxt.set("file", rev(fl));
+		{ctxt.run(argv, 0);} exception {"fail:*" =>;}
+	}
+	if(tcmd != nil){
+		ctxt.set("file", nil);
+		{ctxt.run(ref Sh->Listnode(tcmd, nil) :: nil, 0);} exception {"fail:*" =>;}
+	}
+	errorc <-= nil;
+	sync <-= nil;		# XXX should return result here...
+}
+
+setenv(ctxt: ref Context, var: string, val: list of string)
+{
+	ctxt.set(var, sh->stringlist2list(val));
+}
+
+setstatenv(ctxt: ref Context, dir: ref Sys->Dir, pflag: int)
+{
+	setenv(ctxt, "mode", modes(dir.mode) :: nil);
+	setenv(ctxt, "uid", dir.uid :: nil);
+	setenv(ctxt, "mtime", string dir.mtime :: nil);
+	setenv(ctxt, "length", string dir.length :: nil);
+
+	if(pflag > 1){
+		setenv(ctxt, "name", dir.name :: nil);
+		setenv(ctxt, "gid", dir.gid :: nil);
+		setenv(ctxt, "muid", dir.muid :: nil);
+		setenv(ctxt, "qid", sys->sprint("16r%ubx", dir.qid.path) :: string dir.qid.vers :: nil);
+		setenv(ctxt, "atime", string dir.atime :: nil);
+		setenv(ctxt, "dtype", sys->sprint("%c", dir.dtype) :: nil);
+		setenv(ctxt, "dev", string dir.dev :: nil);
+	}
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
--- /dev/null
+++ b/appl/alphabet/fs/filter.b
@@ -1,0 +1,66 @@
+implement Filter, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet/fs.m";
+	fsfilter: Fsfilter;
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Filter: module {};
+
+Query: adt {
+	gate: Gatechan;
+	dflag: int;
+	reply: chan of int;
+	query: fn(q: self ref Query, d: ref Sys->Dir, name: string, depth: int): int;
+};
+
+types(): string
+{
+	return "xxp-d";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fsfilter = load Fsfilter Fsfilter->PATH;
+	if(fsfilter == nil)
+		badmod(Fsfilter->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	dst := chan of (Fsdata, chan of int);
+	spawn filterproc((hd args).x().i, dst, (hd tl args).p().i, opts != nil);
+	return ref Value.Vx(dst);
+}
+
+filterproc(src, dst: Fschan, gate: Gatechan, dflag: int)
+{
+	fsfilter->filter(ref Query(gate, dflag, chan of int), src, dst);
+	gate <-= ((nil, nil, 0), nil);
+}
+
+Query.query(q: self ref Query, d: ref Sys->Dir, name: string, depth: int): int
+{
+	if(depth == 0 || (q.dflag && (d.mode & Sys->DMDIR)))
+		return 1;
+	q.gate <-= ((d, name, depth), q.reply);
+	return <-q.reply;
+}
--- /dev/null
+++ b/appl/alphabet/fs/ls.b
@@ -1,0 +1,107 @@
+implement Ls, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "daytime.m";
+	daytime: Daytime;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Option, Value, Entrychan: import fs;
+
+Ls: module {};
+
+types(): string
+{
+	return "ft-u-m";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: ls: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		badmod(Daytime->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	f := chan of ref Sys->FD;
+	spawn lsproc(f, opts, (hd args).t().i, report.start("/fs/ls"));
+	return ref Value.Vf(f);
+}
+
+lsproc(f: chan of ref Sys->FD, opts: list of Option, c: Entrychan, errorc: chan of string)
+{
+	f <-= nil;
+	if((fd := <-f) == nil){
+		c.sync <-= 0;
+		reports->quit(errorc);
+	}
+	now := daytime->now();
+	mflag := uflag := 0;
+	c.sync <-= 1;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).opt {
+		'm' =>
+			mflag = 1;
+		'u' =>
+			uflag = 1;
+		}
+	}
+	while(((dir, p, nil) := <-c.c).t0 != nil){
+		t := dir.mtime;
+		if(uflag)
+			t = dir.atime;
+		s := sys->sprint("%s %c %d %s %s %bud %s %s\n",
+			modes(dir.mode), dir.dtype, dir.dev,
+			dir.uid, dir.gid, dir.length,
+			daytime->filet(now, dir.mtime), p);
+		if(mflag)
+			s = "[" + dir.muid + "] " + s;
+		sys->fprint(fd, "%s", s);
+	}
+	reports->quit(errorc);
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
--- /dev/null
+++ b/appl/alphabet/fs/match.b
@@ -1,0 +1,84 @@
+implement Match, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "filepat.m";
+	filepat: Filepat;
+include "regex.m";
+	regex: Regex;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Match: module {};
+
+types(): string
+{
+	return "ps-a-r";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	regex = load Regex Regex->PATH;
+	if(regex == nil)
+		badmod(Regex->PATH);
+	filepat = load Filepat Filepat->PATH;
+	if(filepat == nil)
+		badmod(Filepat->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	pat := (hd args).s().i;
+	aflag := rflag := 0;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).opt {
+		'a' =>
+			aflag = 1;
+		'r' =>
+			rflag = 1;
+		}
+	}
+	v := ref Value.Vp(chan of Gatequery);
+	re: Regex->Re;
+	if(rflag){
+		err: string;
+		(re, err) = regex->compile(pat, 0);
+		if(re == nil){
+			sys->fprint(sys->fildes(2), "fs: match: regex error on %#q: %s\n", pat, err);
+			return nil;
+		}
+	}
+	spawn matchproc(v.i, aflag, pat, re);
+	return v;
+}
+
+matchproc(c: Gatechan, all: int, pat: string, re: Regex->Re)
+{
+	while((((d, name, nil), reply) := <-c).t0.t0 != nil){
+		if(all == 0)
+			name = d.name;
+		if(re != nil)
+			reply <-= regex->execute(re, name) != nil;		# XXX should anchor it?
+		else
+			reply <-= filepat->match(pat, name);
+	}
+}
--- /dev/null
+++ b/appl/alphabet/fs/merge.b
@@ -1,0 +1,192 @@
+implement Merge, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Cmpchan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Merge: module {};
+
+# e.g....
+# fs select {mode -d} {merge -c {compose -d AoutB} {filter {not {path /chan /dev /usr/rog /n/local /net}} /} {merge {proto FreeBSD} {proto Hp} {proto Irix} {proto Linux} {proto MacOSX} {proto Nt} {proto Nt.ti} {proto Nt.ti925} {proto Plan9} {proto Plan9.ti} {proto Plan9.ti925} {proto Solaris} {proto authsrv} {proto dl} {proto dlsrc} {proto ep7} {proto inferno} {proto inferno.ti} {proto ipaqfs} {proto minitel} {proto os} {proto scheduler.client} {proto scheduler.server} {proto sds} {proto src} {proto src.ti} {proto sword} {proto ti925.ti} {proto ti925bin} {proto tipaq} {proto umec} {proto utils} {proto utils.ti}}} >[2] /dev/null
+
+types(): string
+{
+	return "xxxx*-1-cm";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil){
+		sys->fprint(sys->fildes(2), "fs: cannot load %s: %r\n", Fs->PATH);
+		raise "fail:bad module";
+	}
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	recurse := 1;
+	cmp: Cmpchan;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).opt {
+		'1' =>
+			recurse = 0;
+		'c' =>
+			cmp = (hd (hd opts).args).m().i;
+		}
+	}
+	dst := chan of (Fsdata, chan of int);
+	spawn mergeproc((hd args).x().i, (hd tl args).x().i, dst, recurse, cmp, tl tl args == nil);
+	for(args = tl tl args; args != nil; args = tl args){
+		dst1 := chan of (Fsdata, chan of int);
+		spawn mergeproc(dst, (hd args).x().i, dst1, recurse, cmp, tl args == nil);
+		dst = dst1;
+	}
+	return ref Value.Vx(dst);
+}
+
+# merge two trees; assume directories are alphabetically sorted.
+mergeproc(c0, c1, dst: Fschan, recurse: int, cmp: Cmpchan, killcmp: int)
+{
+	myreply := chan of int;
+	((d0, nil), reply0) := <-c0;
+	((d1, nil), reply1) := <-c1;
+
+	if(compare(cmp, d0, d1) == 2r10)
+		dst <-= ((d1, nil), myreply);
+	else
+		dst <-= ((d0, nil), myreply);
+	r := <-myreply;
+	reply0 <-= r;
+	reply1 <-= r;
+	if(r == Down){
+		{
+			mergedir(c0, c1, dst, recurse, cmp);
+		} exception {"exit" =>;}
+	}
+	if(cmp != nil && killcmp)
+		cmp <-= (nil, nil, nil);
+}
+
+mergedir(c0, c1, dst: Fschan, recurse: int, cmp: Cmpchan)
+{
+	myreply := chan of int;
+	reply0, reply1: chan of int;
+	d0, d1: ref Sys->Dir;
+	eof0 := eof1 := 0;
+	for(;;){
+		if(!eof0 && d0 == nil){
+			((d0, nil), reply0) = <-c0;
+			if(d0 == nil){
+				reply0 <-= Next;
+				eof0 = 1;
+			}
+		}
+		if(!eof1 && d1 == nil){
+			((d1, nil), reply1) = <-c1;
+			if(d1 == nil){
+				reply1 <-= Next;
+				eof1 = 1;
+			}
+		}
+		if(eof0 && eof1)
+			break;
+
+		(wd0, wd1) := (d0, d1);
+		if(d0 != nil && d1 != nil && d0.name != d1.name){
+			if(d0.name < d1.name)
+				wd1 = nil;
+			else
+				wd0 = nil;
+		}
+
+		wc0, wc1: Fschan;
+		wreply0, wreply1: chan of int;
+		weof0, weof1: int;
+
+		c := compare(cmp, wd0, wd1);
+		if(wd0 != nil && wd1 != nil){
+			if(c != 0 && recurse && (wd0.mode & wd1.mode & Sys->DMDIR) != 0){
+				dst <-= ((wd0, nil), myreply);
+				r := <-myreply;
+				reply0 <-= r;
+				reply1 <-= r;
+				d0 = d1 = nil;
+				case r {
+				Quit =>
+					raise "exit";
+				Skip =>
+					return;
+				Down =>
+					mergedir(c0, c1, dst, 1, cmp);
+				}
+				continue;
+			}
+			# when we can't merge and there's a clash, choose c0 over c1, unless cmp says otherwise
+			if(c == 2r10){
+				reply0 <-= Next;
+				d0 = nil;
+			}else{
+				reply1 <-= Next;
+				d1 = nil;
+			}
+		}
+		if(c & 2r01){
+			(wd0, wc0, wreply0, weof0) = (d0, c0, reply0, eof0);
+			(wd1, wc1, wreply1, weof1) = (d1, c1, reply1, eof1);
+			d0 = nil;
+		}else if(c & 2r10){
+			(wd0, wc0, wreply0, weof0) = (d1, c1, reply1, eof1);
+			(wd1, wc1, wreply1, weof1) = (d0, c0, reply0, eof0);
+			d1 = nil;
+		}else{
+			if(wd0 == nil){
+				reply1 <-= Next;
+				d1 = nil;
+			}else{
+				reply0 <-= Next;
+				d0 = nil;
+			}
+			continue;
+		}
+		dst <-= ((wd0, nil), myreply);
+		r := <-myreply;
+		wreply0 <-= r;
+		if(r == Down)
+			r = fs->copy(wc0, dst);		# XXX hmm, maybe this should be a mergedir()
+		case r {
+		Quit or
+		Skip =>
+			if(wd1 == nil && !weof1)
+				(nil, wreply1) = <-wc1;
+			wreply1 <-= r;
+			if(r == Quit)
+				raise "exit";
+			return;
+		}
+	}
+	dst <-= ((nil, nil), myreply);
+	if(<-myreply == Quit)
+		raise "exit";
+}
+
+compare(cmp: Cmpchan, d0, d1: ref Sys->Dir): int
+{
+	mask := (d0 != nil) | (d1 != nil) << 1;
+	if(cmp == nil)
+		return mask;
+	reply := chan of int;
+	cmp <-= (d0, d1, reply);
+	return <-reply & mask;
+}
--- /dev/null
+++ b/appl/alphabet/fs/mergewrite.b
@@ -1,0 +1,244 @@
+implement Mergewrite, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "readdir.m";
+	readdir: Readdir;
+include "alphabet/reports.m";
+	reports: Reports;
+	Report, report, quit: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Cmpchan, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Mergewrite: module {};
+
+types(): string
+{
+	return "rxsm-v-n";
+}
+
+VERBOSE, NOWRITE, ASSUME: con 1<<iota;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil){
+		sys->fprint(sys->fildes(2), "fs: mergewrite: cannot load %s: %r\n", Readdir->PATH);
+		raise "fail:bad module";
+	}
+	readdir->init(nil, 0);
+
+	fs = load Fs Fs->PATH;
+	if(fs == nil){
+		sys->fprint(sys->fildes(2), "fs: mergewrite: cannot load %s: %r\n", Fs->PATH);
+		raise "fail:bad module";
+	}
+	reports = load Reports Reports->PATH;
+	if(reports == nil){
+		sys->fprint(sys->fildes(2), "fs: mergewrite: cannot load %s: %r\n", Reports->PATH);
+		raise "fail:bad module";
+	}
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	sync := chan of string;
+	flags := 0;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).opt {
+		'n' =>
+			flags |= NOWRITE;
+		'v' =>
+			flags |= VERBOSE;
+		}
+	}
+			
+	spawn  fswriteproc(sync, flags, (hd args).x().i, (hd tl args).s().i, (hd tl tl args).m().i, report.start("mergewrite"));
+	sync <-= nil;
+	return ref Value.Vr(sync);
+}
+
+fswriteproc(sync: chan of string, flags: int, c: Fschan, root: string, cmp: Cmpchan, errorc: chan of string)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	<-sync;
+	if(<-sync != nil){
+		(<-c).t1 <-= Quit;
+		quit(errorc);
+	}
+	
+	((d, nil), reply) := <-c;
+	if(root != nil){
+		d = ref *d;
+		d.name = root;
+	}
+	fswritedir(d.name, cmp, d, reply, c, errorc, flags);
+	errorc <-= nil;
+	sync <-= nil;		# XXX should return result here...
+}
+
+fswritedir(path: string, cmp: Cmpchan, dir: ref Sys->Dir, dreply: chan of int, c: Fschan,
+		errorc: chan of string, flags: int)
+{
+	fd: ref Sys->FD;
+	if(dir.mode & Sys->DMDIR){
+		made := 0;
+		if(flags&VERBOSE)
+			report(errorc, sys->sprint("create %q %uo", path, dir.mode));
+		if(flags&NOWRITE){
+			if(flags&ASSUME)
+				made = 1;
+			else{
+				fd = sys->open(dir.name, Sys->OREAD);
+				if(fd == nil){
+					made = 1;
+					flags |= ASSUME;
+				}else if(sys->chdir(dir.name) == -1){
+					dreply <-= Next;
+					report(errorc, sys->sprint("cannot cd to %q: %r", path));
+					return;
+				}
+			}
+		}else{
+			fd = sys->create(dir.name, Sys->OREAD, dir.mode|8r300);
+			made = fd != nil;
+			if(fd == nil && (fd = sys->open(dir.name, Sys->OREAD)) == nil){
+				dreply <-= Next;
+				report(errorc, sys->sprint("cannot create %q, mode %uo: %r", path, dir.mode|8r300));
+				return;
+			}
+			# XXX if we haven't just made it, we should chmod the old entry u+w to enable writing.
+			if(sys->chdir(dir.name) == -1){		# XXX beware of names starting with '#'
+				dreply <-= Next;
+				report(errorc, sys->sprint("cannot cd to %q: %r", path));
+				fd = nil;
+				sys->remove(dir.name);
+				return;
+			}
+		}
+		dreply <-= Down;
+		entries: array of ref Sys->Dir;
+		if(made == 0)
+			entries = readdir->readall(fd, Readdir->NAME|Readdir->COMPACT).t0;
+		i := 0;
+		eod := 0;
+		d0, d1: ref Sys->Dir;
+		reply: chan of int;
+		path[len path] = '/';
+		for(;;){
+			if(!eod && d0 == nil){
+				((d0, nil), reply) = <-c;
+				if(d0 == nil){
+					reply <-= Next;
+					eod = 1;
+				}
+			}
+			if(d1 == nil && i < len entries)
+				d1 = entries[i++];
+			if(d0 == nil && d1 == nil)
+				break;
+
+			(wd0, wd1) := (d0, d1);
+			if(d0 != nil && d1 != nil && d0.name != d1.name){
+				if(d0.name < d1.name)
+					wd1 = nil;
+				else
+					wd0 = nil;
+			}
+			r := compare(cmp, wd0, wd1);
+			if(wd1 != nil){
+				if((r & 2r10) == 0){
+					if(flags&VERBOSE)
+						report(errorc, "removing "+path+wd1.name);
+					if((flags&NOWRITE)==0){
+						if(wd1.mode & Sys->DMDIR)
+							rmdir(wd1.name);
+						else
+							remove(wd1.name);
+					}
+				}
+				d1 = nil;
+			}
+			if(wd0 != nil){
+				if((r & 2r01) == 0)
+					reply <-= Next;
+				else
+					fswritedir(path + wd0.name, cmp, d0, reply, c, errorc, flags);
+				d0 = nil;
+			}
+		}
+		if((flags&ASSUME)==0)
+			sys->chdir("..");
+		if((flags&NOWRITE)==0){
+			if((dir.mode & 8r300) != 8r300){
+				ws := Sys->nulldir;
+				ws.mode = dir.mode;
+				if(sys->fwstat(fd, ws) == -1)
+					report(errorc, sys->sprint("cannot wstat %q: %r", path));
+			}
+		}
+	}else{
+		if(flags&VERBOSE)
+			report(errorc, sys->sprint("create %q %uo", path, dir.mode));
+		if(flags&NOWRITE){
+			dreply <-= Next;
+			return;
+		}
+		fd = sys->create(dir.name, Sys->OWRITE, dir.mode);
+		if(fd == nil){
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot create %q, mode %uo: %r", path, dir.mode|8r300));
+			return;
+		}
+		dreply <-= Down;
+		while((((nil, buf), reply) := <-c).t0.data != nil){
+			nw := sys->write(fd, buf, len buf);
+			if(nw < len buf){
+				if(nw == -1)
+					errorc <-= sys->sprint("error writing %q: %r", path);
+				else
+					errorc <-= sys->sprint("short write");
+				reply <-= Skip;
+				break;
+			}
+			reply <-= Next;
+		}
+		reply <-= Next;
+	}
+}
+
+rmdir(name: string)
+{
+	(d, n) := readdir->init(name, Readdir->NONE|Readdir->COMPACT);
+	for(i := 0; i < n; i++){
+		path := name+"/"+d[i].name;
+		if(d[i].mode & Sys->DMDIR)
+			rmdir(path);
+		else
+			remove(path);
+	}
+	remove(name);
+}
+
+remove(name: string)
+{
+	if(sys->remove(name) < 0)
+		sys->fprint(sys->fildes(2), "mergewrite: cannot remove %q: %r\n", name);
+}
+
+compare(cmp: Cmpchan, d0, d1: ref Sys->Dir): int
+{
+	mask := (d0 != nil) | (d1 != nil) << 1;
+	if(cmp == nil)
+		return mask;
+	reply := chan of int;
+	cmp <-= (d0, d1, reply);
+	return <-reply & mask;
+}
--- /dev/null
+++ b/appl/alphabet/fs/mkext.b
@@ -1,0 +1,266 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "bundle.m";
+	bundle: Bundle;
+include "draw.m";
+include "sh.m";
+include "alphabet/fs.m";
+	fslib: Fs;
+	Report, Value,quit, report: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Quit, Next, Skip, Down,
+	Option: import Fs;
+
+to do...
+read file. if non-seekable, make temporary copy.
+record offsets of all files
+sort by filename
+output in proper order.
+
+
+types(): string
+{
+	return "xs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: exec: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fs Fs->PATH;
+	if(fslib == nil)
+		badmod(Fs->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmod(Bufio->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		badmod(String->PATH);
+	bundle = load Bundle Bundle->PATH;
+	if(bundle == nil)
+		badmod(Bundle->PATH);
+	bundle->init();
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	p := (hd args).s().i;
+	iob: ref Bufio->Iobuf;
+	if(p == "-")
+		iob = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	else
+		iob = bufio->open(p, Sys->OREAD);
+	if(iob == nil){
+		sys->fprint(sys->fildes(2), "fs: unbundle: cannot open %q: %r\n", p);
+		return nil;
+	}
+	seekable := p != "-";
+	if(seekable)
+		seekable = isseekable(iob.fd);
+	if(
+	return ref Value.Vx(mkext(report, iob, seekable, Sys->ATOMICIO));
+}
+
+# dodgy heuristic... avoid, or using the stat-length of pipes and net connections
+isseekable(fd: ref Sys->FD): int
+{
+	(ok, stat) := sys->fstat(fd);
+	if(ok != -1 && stat.dtype == '|' || stat.dtype == 'I')
+		return 0;
+	return 1;
+}
+
+mkext(r: ref Report, iob: ref Iobuf, seekable, blocksize: int): Fschan
+{
+	c := chan of (Fsdata, chan of int);
+	spawn unbundleproc(iob, c, seekable, blocksize, r.start("bundle"));
+	return c;
+}
+
+EOF: con "end of archive\n";
+
+unbundleproc(iob: ref Iobuf, c: Fschan, seekable, blocksize: int, errorc: chan of string)
+{
+	reply := chan of int;
+	p := iob.gets('\n');
+	# XXX overall header?
+	if(p == nil || p == EOF){
+		fslib->sendnulldir(c);
+		quit(errorc);
+	}
+	d := header2dir(p);
+	if(d == nil){
+		fslib->sendnulldir(c);
+		report(errorc, "invalid first header");
+		quit(errorc);
+	}
+	if((d.mode & Sys->DMDIR) == 0){
+		fslib->sendnulldir(c);
+		report(errorc, "first entry is not a directory");
+		quit(errorc);
+	}
+	c <-= ((d, nil), reply);
+	case r := <-reply {
+	Down =>
+		unbundledir(iob, c, 0, seekable, blocksize, errorc);
+		c <-= ((nil, nil), reply);
+		<-reply;
+	Skip or
+	Next =>
+		unbundledir(iob, c, 1, seekable, blocksize, errorc);
+	Quit =>
+		break;
+	}
+	quit(errorc);
+}
+
+unbundledir(iob: ref Iobuf, c: Fschan,
+			skipping, seekable, blocksize: int, errorc: chan of string): int
+{
+	reply := chan of int;
+	while((p := iob.gets('\n')) != nil){
+		if(p == EOF)
+			break;
+		if(p[0] == '\n')
+			break;
+		d := header2dir(p);
+		if(d == nil){
+			report(errorc, sys->sprint("invalid bundle header %q", p[0:len p - 1]));
+			return -1;
+		}
+		if(d.mode & Sys->DMDIR){
+			if(skipping)
+				continue;
+			c <-= ((d, nil), reply);
+			case <-reply {
+			Quit =>
+				quit(errorc);
+			Down =>
+				r := unbundledir(iob, c, 0, seekable, blocksize, errorc);
+				c <-= ((nil, nil), reply);
+				if(<-reply == Quit)
+					quit(errorc);
+				if(r == -1)
+					return -1;
+			Skip =>
+				if(unbundledir(iob, c, 1, seekable, blocksize, errorc) == -1)
+					return -1;
+				skipping = 1;
+			Next =>
+				if(unbundledir(iob, c, 1, seekable, blocksize, errorc) == -1)
+					return -1;
+			}
+		}else{
+			if(skipping){
+				if(skipdata(iob, d.length, seekable) == -1)
+					return -1;
+			}else{
+				case unbundlefile(iob, d, c, errorc, seekable, blocksize) {
+				-1 =>
+					return -1;
+				Skip =>
+					skipping = 1;
+				}
+			}
+		}
+	}
+	if(p == nil)
+		report(errorc, "unexpected eof");
+	return 0;
+}
+
+skipdata(iob: ref Iobuf, length: big, seekable: int): int
+{
+	if(seekable){
+		iob.seek(big length, Sys->SEEKRELA);
+		return 0;
+	}
+	buf := array[Sys->ATOMICIO] of byte;
+	for(n := big 0; n < length; ){
+		nb := Sys->ATOMICIO;
+		if(length - n < big Sys->ATOMICIO)
+			nb = int (length - n);
+		nb = iob.read(buf, nb);
+		if(nb <= 0)
+			return -1;
+		n += big nb;
+	}
+	return 0;
+}
+
+unbundlefile(iob: ref Iobuf, d: ref Sys->Dir,
+	c: Fschan, errorc: chan of string, seekable, blocksize: int): int
+{
+	reply := chan of int;
+	c <-= ((d, nil), reply);
+	case <-reply {
+	Quit =>
+		quit(errorc);
+	Skip =>
+		if(skipdata(iob, d.length, seekable) == -1)
+			return -1;
+		return Skip;
+	Next =>
+		if(skipdata(iob, d.length, seekable) == -1)
+			return -1;
+		return Next;
+	}
+	length := d.length;
+	for(n := big 0; n < length; ){
+		nr := blocksize;
+		if(n + big blocksize > length)
+			nr = int (length - n);
+		buf := array[nr] of byte;
+		nr = iob.read(buf, nr);
+		if(nr <= 0){
+			if(nr < 0)
+				report(errorc, sys->sprint("read error: %r"));
+			else
+				report(errorc, sys->sprint("premature eof"));
+			return -1;
+		}else if(nr < len buf)
+			buf = buf[0:nr];
+		c <-= ((nil, buf), reply);
+		n += big nr;
+		case <-reply {
+		Quit =>
+			quit(errorc);
+		Skip =>
+			if(skipdata(iob, length - n, seekable) == -1)
+				return -1;
+			return Next;
+		}
+	}
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+header2dir(s: string): ref Sys->Dir
+{
+	toks := str->unquoted(s);
+	nf := len toks;
+	if(nf != 6)
+		return nil;
+	d := ref Sys->nulldir;
+	(d.name, toks) = (hd toks, tl toks);
+	(d.mode, toks) = (str->toint(hd toks, 8).t0, tl toks);
+	(d.uid, toks) = (hd toks, tl toks);
+	(d.gid, toks) = (hd toks, tl toks);
+	(d.mtime, toks) = (int hd toks, tl toks);
+	(d.length, toks) = (big hd toks, tl toks);
+	return d;
+}
--- /dev/null
+++ b/appl/alphabet/fs/mkfile
@@ -1,0 +1,55 @@
+<../../../mkconfig
+
+TARG=\
+	and.dis\
+	bundle.dis\
+	chstat.dis\
+	compose.dis\
+	depth.dis\
+	entries.dis\
+	exec.dis\
+	filter.dis\
+	ls.dis\
+	match.dis\
+	merge.dis\
+	mergewrite.dis\
+	mode.dis\
+	newer.dis\
+	not.dis\
+	or.dis\
+	path.dis\
+	pipe.dis\
+	print.dis\
+	proto.dis\
+	query.dis\
+	run.dis\
+	select.dis\
+	setroot.dis\
+	size.dis\
+	unbundle.dis\
+	walk.dis\
+	write.dis\
+
+MODULES=\
+	bundle.m\
+	unbundle.m\
+
+SYSMODULES=\
+	alphabet/fs.m\
+	alphabet/reports.m\
+	bufio.m\
+	bundle.m\
+	daytime.m\
+	draw.m\
+	filepat.m\
+	readdir.m\
+	regex.m\
+	sh.m\
+	string.m\
+	sys.m\
+	unbundle.m\
+
+DISBIN=$ROOT/dis/alphabet/fs
+
+<$ROOT/mkfiles/mkdis
+LIMBOFLAGS=-F $LIMBOFLAGS
--- /dev/null
+++ b/appl/alphabet/fs/mode.b
@@ -1,0 +1,125 @@
+implement Mode, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Mode: module {};
+
+# XXX implement octal modes.
+
+User:	con 8r700;
+Group:	con 8r070;
+Other:	con 8r007;
+All:	con User | Group | Other;
+
+Read:	con 8r444;
+Write:	con 8r222;
+Exec:	con 8r111;
+
+types(): string
+{
+	return "ps";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	spec := (hd args).s().i;
+	(ok, mask, mode) := parsemode(spec);
+	if(ok == 0){
+		sys->fprint(sys->fildes(2), "fs: mode: bad mode %#q\n", spec);
+		return nil;
+	}
+	c := chan of Gatequery;
+	spawn modegate(c, mask, mode);
+	return ref Value.Vp(c);
+}
+
+modegate(c: Gatechan, mask, mode: int)
+{
+	m := mode & mask;
+	while((((d, nil, nil), reply) := <-c).t0.t0 != nil)
+		reply <-= ((d.mode & mask) ^ m) == 0;
+}
+
+# stolen from /appl/cmd/chmod.b
+parsemode(spec: string): (int, int, int)
+{
+	mask := Sys->DMAPPEND | Sys->DMEXCL | Sys->DMDIR | Sys->DMAUTH;
+loop:
+	for(i := 0; i < len spec; i++){
+		case spec[i] {
+		'u' =>
+			mask |= User;
+		'g' =>
+			mask |= Group;
+		'o' =>
+			mask |= Other;
+		'a' =>
+			mask |= All;
+		* =>
+			break loop;
+		}
+	}
+	if(i == len spec)
+		return (0, 0, 0);
+	if(i == 0)
+		mask |= All;
+
+	op := spec[i++];
+	if(op != '+' && op != '-' && op != '=')
+		return (0, 0, 0);
+
+	mode := 0;
+	for(; i < len spec; i++){
+		case spec[i]{
+		'r' =>
+			mode |= Read;
+		'w' =>
+			mode |= Write;
+		'x' =>
+			mode |= Exec;
+		'a' =>
+			mode |= Sys->DMAPPEND;
+		'l' =>
+			mode |= Sys->DMEXCL;
+		'd' =>
+			mode |= Sys->DMDIR;
+		'A' =>
+			mode |= Sys->DMAUTH;
+		* =>
+			return (0, 0, 0);
+		}
+	}
+	if(op == '+' || op == '-')
+		mask &= mode;
+	if(op == '-')
+		mode = ~mode;
+	return (1, mask, mode);
+}
+
+
--- /dev/null
+++ b/appl/alphabet/fs/newer.b
@@ -1,0 +1,64 @@
+implement Newer, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Cmpchan, Option: import Fs;
+
+Newer: module {};
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+types(): string
+{
+	return "m-d";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+# select those items in A that are newer than those in B
+# or those that exist in A that don't in B.
+# if -d flag is given, select all directories in A too.
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, nil: list of ref Value): ref Value
+{
+	c := chan of (ref Sys->Dir, ref Sys->Dir, chan of int);
+	spawn newer(c, opts != nil);
+	return ref Value.Vm(c);
+}
+
+newer(c: Cmpchan, dflag: int)
+{
+	while(((d0, d1, reply) := <-c).t2 != nil){
+		r: int;
+		if(d0 == nil)
+			r = 2r10;
+		else if(d1 == nil)
+			r = 2r01;
+		else if(dflag && (d0.mode & Sys->DMDIR))
+			r = 2r11;
+		else {
+			if(d0.mtime > d1.mtime)
+				r = 2r01;
+			else
+				r= 2r10;
+		}
+		reply <-= r;
+	}
+}
--- /dev/null
+++ b/appl/alphabet/fs/not.b
@@ -1,0 +1,53 @@
+implement Not, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Not: module {};
+
+types(): string
+{
+	return "pp";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	c := chan of Gatequery;
+	spawn notgate(c, (hd args).p().i);
+	return ref Value.Vp(c);
+}
+
+notgate(c, sub: Gatechan)
+{
+	myreply := chan of int;
+	while(((d, reply) := <-c).t0.t0 != nil){
+		sub <-= (d, myreply);
+		reply <-= !<-myreply;
+	}
+	sub <-= (Nilentry, nil);
+}
--- /dev/null
+++ b/appl/alphabet/fs/or.b
@@ -1,0 +1,70 @@
+implement Or, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Or: module {};
+
+types(): string
+{
+	return "pppp*";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	c := chan of Gatequery;
+	spawn orgate(c, args);
+	return ref Value.Vp(c);
+}
+
+orgate(c: Gatechan, args: list of ref Value)
+{
+	sub: list of Gatechan;
+	for(; args != nil; args = tl args)
+		sub = (hd args).p().i :: sub;
+	sub = rev(sub);
+	myreply := chan of int;
+	while(((d, reply) := <-c).t0.t0 != nil){
+		for(l := sub; l != nil; l = tl l){
+			(hd l) <-= (d, myreply);
+			if(<-myreply)
+				break;
+		}
+		reply <-= l != nil;
+	}
+	for(; sub != nil; sub = tl sub)
+		hd sub <-= (Nilentry, nil);
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
--- /dev/null
+++ b/appl/alphabet/fs/path.b
@@ -1,0 +1,82 @@
+implement Path, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Path: module {};
+
+types(): string
+{
+	return "pss*-x";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	# XXX cleanname all paths?
+	c := chan of Gatequery;
+	p: list of string;
+	for(; args != nil; args = tl args)
+		p = (hd args).s().i :: p;
+	spawn pathgate(c, opts != nil, p);
+	return ref Value.Vp(c);
+}
+
+pathgate(c: Gatechan, xflag: int, paths: list of string)
+{
+	if(xflag){
+		while((((d, path, nil), reply) := <-c).t0.t0 != nil){
+			for(q := paths; q != nil; q = tl q){
+				r := 1;
+				p := hd q;
+				if(len path > len p)
+					r = path[len p] != '/' || path[0:len p] != p;
+				else if(len path == len p)
+					r = path != p;
+				if(r == 0)
+					break;
+			}
+			reply <-= q == nil;
+		}
+	}else{
+		while((((d, path, nil), reply) := <-c).t0.t0 != nil){
+			for(q := paths; q != nil; q = tl q){
+				r := 0;
+				p := hd q;
+				if(len path > len p)
+					r = path[len p] == '/' && path[0:len p] == p;
+				else if(len path == len p)
+					r = path == p;
+				else
+					r = p[len path] == '/' && p[0:len path] == path;
+				if(r)
+					break;
+			}
+			reply <-= q != nil;
+		}
+	}
+}
--- /dev/null
+++ b/appl/alphabet/fs/pipe.b
@@ -1,0 +1,230 @@
+implement Pipe, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Option, Value, Fschan: import fs;
+	Skip, Next, Down, Quit: import fs;
+
+Pipe: module {};
+
+# pipe the contents of the files in a filesystem through
+# a command. -1 causes one command only to be executed.
+# -p and -P (exclusive to -1) cause stat modes to be set in the shell environment.
+types(): string
+{
+	return "rxc-1-p-P";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: exec: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		badmod(Sh->PATH);
+	sh->initialise();
+}
+
+run(drawctxt: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	n := 1;
+	oneflag := pflag := 0;
+	for(; opts != nil; opts = tl opts){
+		o := hd opts;
+		case o.opt {
+		'1' =>
+			oneflag = 1;
+		'p' =>
+			pflag = 1;
+		'P' =>
+			pflag = 2;
+		}
+	}
+	if(pflag && oneflag){
+		sys->fprint(sys->fildes(2), "fs: exec: cannot specify -p with -1\n");
+		return nil;
+	}
+	c := (hd args).x().i;
+	cmd := (hd tl args).c().i;
+	sync := chan of string;
+	spawn execproc(drawctxt, sync, oneflag, pflag, c, cmd);
+	sync <-= nil;
+	return ref Value.Vr(sync);
+}
+
+execproc(drawctxt: ref Draw->Context, sync: chan of string, oneflag, pflag: int,
+		c: Fschan, cmd: ref Sh->Cmd)
+{
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	ctxt := Context.new(drawctxt);
+	<-sync;
+	if(<-sync != nil){
+		(<-c).t1 <-= Quit;
+		exit;
+	}
+	argv := ref Sh->Listnode(cmd, nil) :: nil;
+	fd: ref Sys->FD;
+	result := chan of string;
+	if(oneflag){
+		fd = popen(ctxt, argv, result);
+		if(fd == nil){
+			(<-c).t1 <-= Quit;
+			sync <-= "cannot make pipe";
+			exit;
+		}
+	}
+
+	names: list of string;
+	name: string;
+	indent := 0;
+	for(;;){
+		(d, reply) := <-c;
+		if(d.dir == nil){
+			reply <-= Next;
+			if(--indent == 0){
+				break;
+			}
+			(name, names) = (hd names, tl names);
+			continue;
+		}
+		if((d.dir.mode & Sys->DMDIR) != 0){
+			reply <-= Down;
+			names = name :: names;
+			if(indent > 0 && name != nil && name[len name - 1] != '/')
+				name[len name] = '/';
+			name += d.dir.name;
+			indent++;
+			continue;
+		}
+		if(!oneflag){
+			p := name;
+			if(p != nil && p[len p - 1] != '/')
+				p[len p] = '/';
+			setenv(ctxt, "file", p + d.dir.name :: nil);
+			if(pflag)
+				setstatenv(ctxt, d.dir, pflag);
+			fd = popen(ctxt, argv, result);
+		}
+		if(fd == nil){
+			reply <-= Next;
+			continue;
+		}
+		reply <-= Down;
+		for(;;){
+			data: array of byte;
+			((nil, data), reply) = <-c;
+			reply <-= Next;
+			if(data == nil)
+				break;
+			n := -1;
+			{n = sys->write(fd, data, len data);}exception {"write on closed pipe" => ;}
+			if(n != len data){
+				if(oneflag){
+					(<-c).t1 <-= Quit;
+					sync <-= "truncated write";
+					exit;
+				}
+				(<-c).t1 <-= Skip;
+				break;
+			}
+		}
+		if(!oneflag){
+			fd = nil;
+			<-result;
+		}
+	}
+	fd = nil;
+	if(oneflag)
+		sync <-= <-result;
+	else
+		sync <-= nil;
+}
+
+popen(ctxt: ref Context, argv: list of ref Sh->Listnode, result: chan of string): ref Sys->FD
+{
+	sync := chan of int;
+	fds := array[2] of ref Sys->FD;
+	sys->pipe(fds);
+	spawn runcmd(ctxt, argv, fds[0], sync, result);
+	<-sync;
+	return fds[1];
+}
+
+runcmd(ctxt: ref Context, argv: list of ref Sh->Listnode, stdin: ref Sys->FD, sync: chan of int, result: chan of string)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	stdin = nil;
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	ctxt = ctxt.copy(0);
+	sync <-= 0;
+	r := ctxt.run(argv, 0);
+	ctxt = nil;
+	sys->pctl(Sys->NEWFD, nil);
+	result <-=r;
+}
+
+setenv(ctxt: ref Context, var: string, val: list of string)
+{
+	ctxt.set(var, sh->stringlist2list(val));
+}
+
+setstatenv(ctxt: ref Context, dir: ref Sys->Dir, pflag: int)
+{
+	setenv(ctxt, "mode", modes(dir.mode) :: nil);
+	setenv(ctxt, "uid", dir.uid :: nil);
+	setenv(ctxt, "mtime", string dir.mtime :: nil);
+	setenv(ctxt, "length", string dir.length :: nil);
+
+	if(pflag > 1){
+		setenv(ctxt, "name", dir.name :: nil);
+		setenv(ctxt, "gid", dir.gid :: nil);
+		setenv(ctxt, "muid", dir.muid :: nil);
+		setenv(ctxt, "qid", sys->sprint("16r%ubx", dir.qid.path) :: string dir.qid.vers :: nil);
+		setenv(ctxt, "atime", string dir.atime :: nil);
+		setenv(ctxt, "dtype", sys->sprint("%c", dir.dtype) :: nil);
+		setenv(ctxt, "dev", string dir.dev :: nil);
+	}
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
--- /dev/null
+++ b/appl/alphabet/fs/print.b
@@ -1,0 +1,61 @@
+implement Print, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Print: module {};
+
+types(): string
+{
+	return "ft";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	f := chan of ref Sys->FD;
+	spawn printproc(f, (hd args).t().i, report.start("/fs/print"));
+	return ref Value.Vf(f);
+}
+
+printproc(f: chan of ref Sys->FD, c: Entrychan, errorc: chan of string)
+{
+	f <-= nil;
+	if((fd := <-f) == nil){
+		c.sync <-= 0;
+		reports->quit(errorc);
+	}
+	c.sync <-= 1;
+	while(((d, p, nil) := <-c.c).t0 != nil)
+		sys->fprint(fd, "%s\n", p);
+	sys->fprint(fd, "");
+	reports->quit(errorc);
+}
--- /dev/null
+++ b/appl/alphabet/fs/proto.b
@@ -1,0 +1,416 @@
+implement Proto, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "readdir.m";
+	readdir: Readdir;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report, quit, report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Proto: module {};
+
+File: adt {
+	name: string;
+	mode: int;
+	owner: string;
+	group: string;
+	old: string;
+	flags: int;
+	sub: cyclic array of ref File;
+};
+
+Protof: adt {
+	indent: int;
+	lastline: string;
+	iob: ref Iobuf;
+};
+
+Star, Plus: con 1<<iota;
+
+types(): string
+{
+	return "xf-rs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: proto: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmod(Readdir->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmod(Bufio->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		badmod(String->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	f := (hd args).f().i;
+	rootpath: string;
+	if(opts != nil)
+		rootpath = (hd (hd opts).args).s().i;
+	if(rootpath == nil)
+		rootpath = "/";
+	
+	root := ref File(rootpath, ~0, nil, nil, nil, 0, nil);
+	c := chan of (Fsdata, chan of int);
+	spawn protowalk(c, f, root, report.start("proto"));
+	return ref Value.Vx(c);
+}
+
+protowalk(c: Fschan, f: chan of ref Sys->FD, root: ref File, errorc: chan of string)
+{
+	fd := <-f;
+	if(fd != nil)
+		f <-= nil;
+	else{
+		sys->pipe(p := array[2] of ref Sys->FD);
+		f <-= p[1];
+		fd = p[0];
+	}
+	proto := ref Protof(0, nil, nil);
+	proto.iob = bufio->fopen(fd, Sys->OREAD);
+	(root.flags, root.sub) = readproto(proto, -1);
+
+	d: ref Sys->Dir;
+	(ok, rd) := sys->stat(root.name);
+	if(ok != -1)
+		d = ref rd;
+
+	protowalk1(c, root.flags, root.name, file2dir(root, d), root.sub, errorc);
+	quit(errorc);
+}
+
+protowalk1(c: Fschan, flags: int, path: string, d: ref Sys->Dir,
+		sub: array of ref File, errorc: chan of string): int
+{
+	reply := chan of int;
+	c <-= ((d, nil), reply);
+	case r := <-reply {
+	Quit =>
+		quit(errorc);
+	Next or
+	Skip =>
+		return r;
+	}
+	(a, n) := readdir->init(path, Readdir->NAME|Readdir->COMPACT);
+	if(len a == 0){
+		c <-= ((nil, nil), reply);
+		if(<-reply == Quit)
+			quit(errorc);
+		return Next;
+	}
+	j := 0;
+	preventry: string;
+	useentry: int;
+	for(i := 0; i < n; i += useentry){
+		useentry = 1;
+		for(; j < len sub; j++){
+			s := sub[j].name;
+			if(s == preventry){
+				report(errorc, sys->sprint("duplicate entry %s", pathconcat(path, s)));
+				continue;			# eliminate duplicates in proto
+			}
+			if(s >= a[i].name)
+				break;
+			# entry has not been found, but we've got a substitute version,
+			# so save the rest of the entries to match the rest of sub.
+			if(sub[j].old != nil){
+				useentry = 0;
+				break;
+			}
+			report(errorc, sys->sprint("%s not found", pathconcat(path, s)));
+		}
+		foundsub := j < len sub && (sub[j].name == a[i].name || sub[j].old != nil);
+		if(foundsub || flags&Plus ||
+				(flags&Star && (a[i].mode & Sys->DMDIR)==0)){
+			f: ref File;
+			if(foundsub){
+				f = sub[j++];
+				preventry = f.name;
+			}
+			p: string;
+			d: ref Sys->Dir;
+			if(foundsub && f.old != nil){
+				p = f.old;
+				(ok, xd) := sys->stat(p);
+				if(ok == -1){
+					report(errorc, sys->sprint("cannot stat %q: %r", p));
+					continue;
+				}
+				d = ref xd;
+			}else{
+				p = pathconcat(path, a[i].name);
+				d = a[i];
+			}
+
+			d = file2dir(f, d);
+			r: int;
+			if((d.mode & Sys->DMDIR) == 0)
+				r = walkfile(c, p, d, errorc);
+			else if(flags & Plus)
+				r = protowalk1(c, Plus, p, d, nil, errorc);
+			else
+				r = protowalk1(c, f.flags, p, d, f.sub, errorc);
+			if(r == Skip)
+				return Next;
+		}
+	}
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+pathconcat(p, name: string): string
+{		
+	if(p != nil && p[len p - 1] != '/')
+		p[len p] = '/';
+	p += name;
+	return p;
+}
+
+# from(ish) walk.b
+walkfile(c: Fschan, path: string, d: ref Sys->Dir, errorc: chan of string): int
+{
+	reply := chan of int;
+	fd := sys->open(path, Sys->OREAD);
+	if(fd == nil){
+		report(errorc, sys->sprint("cannot open %q: %r", path));
+		return Next;
+	}
+	c <-= ((d, nil), reply);
+	case r := <-reply {
+	Quit =>
+		quit(errorc);
+	Next or
+	Skip =>
+		return r;
+	}
+	length := d.length;
+	for(n := big 0; n < length; ){
+		nr := Sys->ATOMICIO;
+		if(n + big Sys->ATOMICIO > length)
+			nr = int (length - n);
+		buf := array[nr] of byte;
+		nr = sys->read(fd, buf, nr);
+		if(nr <= 0){
+			if(nr < 0)
+				report(errorc, sys->sprint("error reading %q: %r", path));
+			else
+				report(errorc, sys->sprint("%q is shorter than expected (%bd/%bd)",
+						path, n, length));
+			break;
+		}else if(nr < len buf)
+			buf = buf[0:nr];
+		c <-= ((nil, buf), reply);
+		case <-reply {
+		Quit =>
+			quit(errorc);
+		Skip =>
+			return Next;
+		}
+		n += big nr;
+	}
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+readproto(proto: ref Protof, indent: int): (int, array of ref File)
+{
+	a := array[10] of ref File;
+	n := 0;
+	flags := 0;
+	while((f := readline(proto, indent)) != nil){
+		if(f.name == "*")
+			flags |= Star;
+		else if(f.name == "+")
+			flags |= Plus;
+		else{
+			(f.flags, f.sub) = readproto(proto, proto.indent);
+			if(n == len a)
+				a = (array[n * 2] of ref File)[0:] = a;
+			a[n++] = f;
+		}
+	}
+	if(n < len a)
+		a = (array[n] of ref File)[0:] = a[0:n];
+	mergesort(a, array[n] of ref File);
+	return (flags, a);
+}
+
+readline(proto: ref Protof, indent: int): ref File
+{
+	s: string;
+	if(proto.lastline != nil){
+		s = proto.lastline;
+		proto.lastline = nil;
+	}else if(proto.indent == -1)
+		return nil;
+	else if((s = proto.iob.gets('\n')) == nil){
+		proto.indent = -1;
+		return nil;
+	}
+	spc := 0;
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c == ' ')
+			spc++;
+		else if(c == '\t')
+			spc += 8;
+		else
+			break;
+	}
+	if(i == len s || s[i] == '#' || s[i] == '\n')
+		return readline(proto, indent);	# XXX sort out tail recursion!
+	if(spc <= indent){
+		proto.lastline = s;
+		return nil;
+	}
+	proto.indent = spc;
+	(n, toks) := sys->tokenize(s, " \t\n");
+	f := ref File(nil, ~0, nil, nil, nil, 0, nil);
+	(f.name, toks) = (getname(hd toks, 0), tl toks);
+	if(toks == nil)
+		return f;
+	(f.mode, toks) = (getmode(hd toks), tl toks);
+	if(toks == nil)
+		return f;
+	(f.owner, toks) = (getname(hd toks, 1), tl toks);
+	if(toks == nil)
+		return f;
+	(f.group, toks) = (getname(hd toks, 1), tl toks);
+	if(toks == nil)
+		return f;
+	(f.old, toks) = (hd toks, tl toks);
+	return f;
+}
+
+mergesort(a, b: array of ref File)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m]);
+		mergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if(b[i].name > b[j].name)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+getname(s: string, allowminus: int): string
+{
+	if(s == nil)
+		return nil;
+	if(allowminus && s == "-")
+		return nil;
+	if(s[0] == '$')
+		return getenv(s[1:]);
+	return s;
+}
+
+getenv(s: string): string
+{
+	# XXX implement env variables
+	return nil;
+}
+
+getmode(s: string): int
+{
+	s = getname(s, 1);
+	if(s == nil)
+		return ~0;
+	m := 0;
+	i := 0;
+	if(s[i] == 'd'){
+		m |= Sys->DMDIR;
+		i++;
+	}
+	if(i < len s && s[i] == 'a'){
+		m |= Sys->DMAPPEND;
+		i++;
+	}
+	if(i < len s && s[i] == 'l'){
+		m |= Sys->DMEXCL;
+		i++;
+	}
+	(xmode, t) := str->toint(s, 8);
+	if(t != nil){
+		# report(aux.errorc, "bad mode specification %q", s);
+		return ~0;
+	}
+	return xmode | m;
+}
+
+file2dir(f: ref File, old: ref Sys->Dir): ref Sys->Dir
+{
+	d := ref Sys->nulldir;
+	if(old != nil){
+		if(old.dtype != 'M'){
+			d.uid = "sys";
+			d.gid = "sys";
+			xmode := (old.mode >> 6) & 7;
+			d.mode = old.mode | xmode | (xmode << 3);
+		}else{
+			d.uid = old.uid;
+			d.gid = old.gid;
+			d.mode = old.mode;
+		}
+		d.length = old.length;
+		d.mtime = old.mtime;
+		d.atime = old.atime;
+		d.muid = old.muid;
+		d.name = old.name;
+	}
+	if(f != nil){
+		d.name = f.name;
+		if(f.owner != nil)
+			d.uid = f.owner;
+		if(f.group != nil)
+			d.gid = f.group;
+		if(f.mode != ~0)
+			d.mode = f.mode;
+	}
+	return d;
+}
--- /dev/null
+++ b/appl/alphabet/fs/query.b
@@ -1,0 +1,135 @@
+implement Query, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Option, Value, Gatechan, Gatequery, Nilentry: import fs;
+
+Query: module {};
+
+types(): string
+{
+	return "pc-p-P";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: query: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		badmod(Sh->PATH);
+}
+
+run(drawctxt: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	pflag := 0;
+	for(; opts != nil; opts = tl opts){
+		o := hd opts;
+		case o.opt {
+		'p' =>
+			pflag = 1;
+		'P' =>
+			pflag = 2;
+		}
+	}
+
+	v := ref Value.Vp(chan of Gatequery);
+	spawn querygate(drawctxt, v.i, (hd args).c().i, pflag);
+	v.i <-= (Nilentry, nil);
+	return v;
+}
+
+querygate(drawctxt: ref Draw->Context, c: Gatechan, cmd: ref Sh->Cmd, pflag: int)
+{
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	ctxt := Context.new(drawctxt);
+	<-c;
+	argv := ref Sh->Listnode(cmd, nil) :: nil;
+	while((((d, p, nil), reply) := <-c).t0.t0 != nil){
+		ctxt.set("file", ref Sh->Listnode(nil, p) :: nil);
+		if(pflag)
+			setstatenv(ctxt, d, pflag);
+		err := "";
+		{
+			err = ctxt.run(argv, 0);
+		} exception e {
+		"fail:*" =>
+			err = e;
+		}
+		reply <-= (err == nil);
+	}
+}
+
+# XXX shouldn't duplicate this...
+
+setenv(ctxt: ref Context, var: string, val: list of string)
+{
+	ctxt.set(var, sh->stringlist2list(val));
+}
+
+setstatenv(ctxt: ref Context, dir: ref Sys->Dir, pflag: int)
+{
+	setenv(ctxt, "mode", modes(dir.mode) :: nil);
+	setenv(ctxt, "uid", dir.uid :: nil);
+	setenv(ctxt, "mtime", string dir.mtime :: nil);
+	setenv(ctxt, "length", string dir.length :: nil);
+
+	if(pflag > 1){
+		setenv(ctxt, "name", dir.name :: nil);
+		setenv(ctxt, "gid", dir.gid :: nil);
+		setenv(ctxt, "muid", dir.muid :: nil);
+		setenv(ctxt, "qid", sys->sprint("16r%ubx", dir.qid.path) :: string dir.qid.vers :: nil);
+		setenv(ctxt, "atime", string dir.atime :: nil);
+		setenv(ctxt, "dtype", sys->sprint("%c", dir.dtype) :: nil);
+		setenv(ctxt, "dev", string dir.dev :: nil);
+	}
+}
+
+start(startc: chan of (string, chan of string), name: string): chan of string
+{
+	c := chan of string;
+	startc <-= (name, c);
+	return c;
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
--- /dev/null
+++ b/appl/alphabet/fs/run.b
@@ -1,0 +1,65 @@
+implement Run, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Run: module {};
+
+types(): string
+{
+	return "sc";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		badmod(Sh->PATH);
+	sh->initialise();
+}
+
+run(drawctxt: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	c := (hd args).c().i;
+	ctxt := Context.new(drawctxt);
+	ctxt.setlocal("s", nil);
+	{
+		ctxt.run(ref Sh->Listnode(c, nil)::nil, 0);
+	} exception e {
+	"fail:*" =>
+		sys->fprint(sys->fildes(2), "fs: run: exception %q raised in %s\n", e[5:], sh->cmd2string(c));
+		return nil;
+	}
+	sl := ctxt.get("s");
+	if(sl == nil || tl sl != nil){
+		sys->fprint(sys->fildes(2), "fs: run: $s has %d members; exactly one is required\n", len sl);
+		return nil;
+	}
+	s := (hd sl).word;
+	if(s == nil && (hd sl).cmd != nil)
+		s = sh->cmd2string((hd sl).cmd);
+	return ref Value.Vs(s);
+}
--- /dev/null
+++ b/appl/alphabet/fs/select.b
@@ -1,0 +1,60 @@
+implement Select, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Select: module {};
+types(): string
+{
+	return "ttp";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	dst := Entrychan(chan of int, chan of Entry);
+	spawn selectproc((hd args).t().i, dst, (hd tl args).p().i);
+	return ref Value.Vt(dst);
+}
+
+selectproc(src, dst: Entrychan, query: Gatechan)
+{
+	if(<-dst.sync == 0){
+		query <-= (Nilentry, nil);
+		src.sync <-= 0;
+		exit;
+	}
+	src.sync <-= 1;
+	reply := chan of int;
+	while((d := <-src.c).t0 != nil){
+		query <-= (d, reply);
+		if(<-reply)
+			dst.c <-= d;
+	}
+	dst.c <-= Nilentry;
+	query <-= (Nilentry, nil);
+}
--- /dev/null
+++ b/appl/alphabet/fs/setroot.b
@@ -1,0 +1,109 @@
+implement Setroot, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	Report: import Reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Setroot: module {};
+
+# set the root 
+types(): string
+{
+	return "xxs-c";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	root := (hd tl args).s().i;
+	if(root == nil && opts == nil){
+		sys->fprint(sys->fildes(2), "fs: setroot: empty path\n");
+		return nil;
+	}
+	v := ref Value.Vx(chan of (Fsdata, chan of int));
+	spawn setroot((hd args).x().i, v.i, root, opts != nil);
+	return v;
+}
+
+setroot(src, dst: Fschan, root: string, cflag: int)
+{
+	((d, nil), reply) := <-src;
+	if(cflag){
+		createroot(src, dst, root, d, reply);
+	}else{
+		myreply := chan of int;
+		rd := ref *d;
+		rd.name = root;
+		dst <-= ((rd, nil), myreply);
+		if(<-myreply == Down){
+			reply <-= Down;
+			fs->copy(src, dst);
+		}
+	}
+}
+
+createroot(src, dst: Fschan, root: string, d: ref Sys->Dir, reply: chan of int)
+{
+	if(root == nil)
+		root = d.name;
+	(n, elems) := sys->tokenize(root, "/");		# XXX should really do a cleanname first
+	if(root[0] == '/'){
+		elems = "/" :: elems;
+		n++;
+	}
+	myreply := chan of int;
+	lev := 0;
+	r := -1;
+	for(; elems != nil; elems = tl elems){
+		rd := ref *d;
+		rd.name = hd elems;
+		dst <-= ((rd, nil), myreply);
+		case r = <-myreply {
+		Quit =>
+			(<-src).t1 <-= Quit;
+			exit;
+		Skip =>
+			break;
+		Next =>
+			lev++;
+			break;
+		}
+		lev++;
+	}
+	if(r == Down){
+		reply <-= Down;
+		if(fs->copy(src, dst) == Quit)
+			exit;
+	}else
+		reply <-= Quit;
+	while(lev-- > 1){
+		dst <-= ((nil, nil), myreply);
+		if(<-myreply == Quit){
+			(<-src).t1 <-= Quit;
+			exit;
+		}
+	}
+}
--- /dev/null
+++ b/appl/alphabet/fs/size.b
@@ -1,0 +1,64 @@
+implement Size, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Size: module {};
+
+types(): string
+{
+	return "ft";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	f := chan of ref Sys->FD;
+	spawn sizeproc(f, (hd args).t().i, report.start("size"));
+	return ref Value.Vf(f);
+}
+
+sizeproc(f: chan of ref Sys->FD, c: Entrychan, errorc: chan of string)
+{
+	f <-= nil;
+	if((fd := <-f) == nil){
+		c.sync <-= 0;
+		exit;
+	}
+	c.sync <-= 1;
+
+	size := big 0;
+	while(((d, nil, nil) := <-c.c).t0 != nil)
+		size += d.length;
+	sys->fprint(fd, "%bd\n", size);
+	sys->fprint(fd, "");
+	errorc <-= nil;
+}
--- /dev/null
+++ b/appl/alphabet/fs/unbundle.b
@@ -1,0 +1,259 @@
+implement Unbundle, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report, quit, report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Quit, Next, Skip, Down,
+	Option: import Fs;
+
+Unbundle: module {};
+types(): string
+{
+	return "xf";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: exec: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmod(Bufio->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		badmod(String->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	f := (hd args).f().i;
+	c := ref Value.Vx(chan of (Fsdata, chan of int));
+	spawn unbundleproc((hd args).f().i, nil, c.i, -1, Sys->ATOMICIO, report.start("unbundle"));
+	return c;
+}
+
+# dodgy heuristic... avoid, or using the stat-length of pipes and net connections
+isseekable(fd: ref Sys->FD): int
+{
+	(ok, stat) := sys->fstat(fd);
+	if(ok != -1 && stat.dtype == '|' || stat.dtype == 'I')
+		return 0;
+	return 1;
+}
+
+EOF: con "end of archive\n";
+
+unbundleproc(f: chan of ref Sys->FD, iob: ref Iobuf, c: Fschan,
+		seekable, blocksize: int, errorc: chan of string)
+{
+	if(f != nil){
+		fd := <-f;
+		if(fd == nil){
+			sys->pipe(p := array[2] of ref Sys->FD);
+			f <-= p[1];
+			p[1] = nil;
+			fd = p[0];
+		}else
+			f <-= nil;
+		if(seekable == -1)
+			seekable = isseekable(fd);
+		iob = bufio->fopen(fd, Sys->OREAD);
+		f = nil;
+	}
+
+	reply := chan of int;
+	p := iob.gets('\n');
+	# XXX overall header?
+	if(p == nil || p == EOF){
+		fs->sendnulldir(c);
+		quit(errorc);
+	}
+	d := header2dir(p);
+	if(d == nil){
+		fs->sendnulldir(c);
+		report(errorc, sys->sprint("invalid first header %q", p[0:len p - 1]));
+		quit(errorc);
+	}
+	if((d.mode & Sys->DMDIR) == 0){
+		fs->sendnulldir(c);
+		report(errorc, "first entry is not a directory");
+		quit(errorc);
+	}
+	c <-= ((d, nil), reply);
+	case r := <-reply {
+	Down =>
+		unbundledir(iob, c, 0, seekable, blocksize, errorc);
+		c <-= ((nil, nil), reply);
+		<-reply;
+	Skip or
+	Next =>
+		unbundledir(iob, c, 1, seekable, blocksize, errorc);
+	Quit =>
+		break;
+	}
+	quit(errorc);
+}
+
+unbundledir(iob: ref Iobuf, c: Fschan,
+			skipping, seekable, blocksize: int, errorc: chan of string): int
+{
+	reply := chan of int;
+	while((p := iob.gets('\n')) != nil){
+		if(p == EOF)
+			break;
+		if(p[0] == '\n')
+			break;
+		d := header2dir(p);
+		if(d == nil){
+			report(errorc, sys->sprint("invalid bundle header %q", p[0:len p - 1]));
+			return -1;
+		}
+		if(d.mode & Sys->DMDIR){
+			if(skipping)
+				continue;
+			c <-= ((d, nil), reply);
+			case <-reply {
+			Quit =>
+				quit(errorc);
+			Down =>
+				r := unbundledir(iob, c, 0, seekable, blocksize, errorc);
+				c <-= ((nil, nil), reply);
+				if(<-reply == Quit)
+					quit(errorc);
+				if(r == -1)
+					return -1;
+			Skip =>
+				if(unbundledir(iob, c, 1, seekable, blocksize, errorc) == -1)
+					return -1;
+				skipping = 1;
+			Next =>
+				if(unbundledir(iob, c, 1, seekable, blocksize, errorc) == -1)
+					return -1;
+			}
+		}else{
+			if(skipping){
+				if(skipdata(iob, d.length, seekable) == -1)
+					return -1;
+			}else{
+				case unbundlefile(iob, d, c, errorc, seekable, blocksize) {
+				-1 =>
+					return -1;
+				Skip =>
+					skipping = 1;
+				}
+			}
+		}
+	}
+	if(p == nil)
+		report(errorc, "unexpected eof");
+	return 0;
+}
+
+skipdata(iob: ref Iobuf, length: big, seekable: int): int
+{
+	if(seekable){
+		iob.seek(big length, Sys->SEEKRELA);
+		return 0;
+	}
+	buf := array[Sys->ATOMICIO] of byte;
+	for(n := big 0; n < length; ){
+		nb := Sys->ATOMICIO;
+		if(length - n < big Sys->ATOMICIO)
+			nb = int (length - n);
+		nb = iob.read(buf, nb);
+		if(nb <= 0)
+			return -1;
+		n += big nb;
+	}
+	return 0;
+}
+
+unbundlefile(iob: ref Iobuf, d: ref Sys->Dir,
+	c: Fschan, errorc: chan of string, seekable, blocksize: int): int
+{
+	reply := chan of int;
+	c <-= ((d, nil), reply);
+	case <-reply {
+	Quit =>
+		quit(errorc);
+	Skip =>
+		if(skipdata(iob, d.length, seekable) == -1)
+			return -1;
+		return Skip;
+	Next =>
+		if(skipdata(iob, d.length, seekable) == -1)
+			return -1;
+		return Next;
+	}
+	length := d.length;
+	for(n := big 0; n < length; ){
+		nr := blocksize;
+		if(n + big blocksize > length)
+			nr = int (length - n);
+		buf := array[nr] of byte;
+		nr = iob.read(buf, nr);
+		if(nr <= 0){
+			if(nr < 0)
+				report(errorc, sys->sprint("read error: %r"));
+			else
+				report(errorc, sys->sprint("premature eof"));
+			return -1;
+		}else if(nr < len buf)
+			buf = buf[0:nr];
+		c <-= ((nil, buf), reply);
+		n += big nr;
+		case <-reply {
+		Quit =>
+			quit(errorc);
+		Skip =>
+			if(skipdata(iob, length - n, seekable) == -1)
+				return -1;
+			return Next;
+		}
+	}
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+header2dir(s: string): ref Sys->Dir
+{
+	toks := str->unquoted(s);
+	nf := len toks;
+	if(nf != 6)
+		return nil;
+	d := ref Sys->nulldir;
+	(d.name, toks) = (hd toks, tl toks);
+	(d.mode, toks) = (str->toint(hd toks, 8).t0, tl toks);
+	(d.uid, toks) = (hd toks, tl toks);
+	(d.gid, toks) = (hd toks, tl toks);
+	(d.mtime, toks) = (int hd toks, tl toks);
+	(d.length, toks) = (big hd toks, tl toks);
+	return d;
+}
--- /dev/null
+++ b/appl/alphabet/fs/unbundle.m
@@ -1,0 +1,9 @@
+Unbundle: module {
+	PATH: con "/dis/fs/bundle.dis";
+
+	types: fn(): string;
+	init: fn();
+	run: fn(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value;
+	unbundle:	fn(r: ref Reports->Report, iob: ref Bufio->Iobuf, seekable: int, blocksize: int): Fs->Fschan;
+};
--- /dev/null
+++ b/appl/alphabet/fs/walk.b
@@ -1,0 +1,242 @@
+implement Walk, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "readdir.m";
+	readdir: Readdir;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report, quit, report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Walk: module {};
+
+Loopcheck: adt {
+	a: array of list of ref Sys->Dir;
+
+	new:		fn(): ref Loopcheck;
+	enter:	fn(l: self ref Loopcheck, d: ref Sys->Dir): int;
+	leave:	fn(l: self ref Loopcheck, d: ref Sys->Dir);
+};
+
+types(): string
+{
+	return "xs-bs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: walk: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmod(Readdir->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	path := (hd args).s().i;
+	(ok, d) := sys->stat(path);
+	if(ok== -1){
+		sys->fprint(sys->fildes(2), "fs: walk: cannot stat %q: %r\n", path);
+		return nil;
+	}
+	if((d.mode & Sys->DMDIR) == 0){
+		# XXX could produce an fs containing just the single file.
+		# would have to split the path though.
+		sys->fprint(sys->fildes(2), "fs: walk: %q is not a directory\n", path);
+		return nil;
+	}
+	sync := chan of int;
+	c := chan of (Fsdata, chan of int);
+	spawn fswalkproc(sync, path, c, Sys->ATOMICIO, report.start("walk"));
+	<-sync;
+	return ref Value.Vx(c);
+}
+
+# XXX need to avoid loops in the filesystem...
+fswalkproc(sync: chan of int, path: string, c: Fschan, blocksize: int, errorc: chan of string)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+	# XXX could allow a single root file?
+	if(sys->chdir(path) == -1){
+		report(errorc, sys->sprint("cannot cd to %q: %r", path));
+		fs->sendnulldir(c);
+		quit(errorc);
+	}
+	(ok, d) := sys->stat(".");
+	if(ok == -1){
+		report(errorc, sys->sprint("cannot stat %q: %r", path));
+		fs->sendnulldir(c);
+		quit(errorc);
+	}
+	d.name = path;
+	reply := chan of int;
+	c <-= ((ref d, nil), reply);
+	if(<-reply == Down){
+		loopcheck := Loopcheck.new();
+		loopcheck.enter(ref d);
+		if(path[len path - 1] != '/')
+			path[len path] = '/';
+		fswalkdir(path, c, blocksize, loopcheck, errorc);
+		c <-= ((nil, nil), reply);
+		<-reply;
+	}
+	quit(errorc);
+}
+
+fswalkdir(path: string, c: Fschan, blocksize: int, loopcheck: ref Loopcheck, errorc: chan of string)
+{
+	reply := chan of int;
+	(a, n) := readdir->init(".", Readdir->NAME|Readdir->COMPACT);
+	if(n == -1){
+		report(errorc, sys->sprint("cannot readdir %q: %r", path));
+		return;
+	}
+	for(i := 0; i < n; i++)
+		if(a[i].mode & Sys->DMDIR)
+			if(loopcheck.enter(a[i]) == 0)
+				a[i].dtype = ~0;
+directory:
+	for(i = 0; i < n; i++){
+		if(a[i].mode & Sys->DMDIR){
+			d := a[i];
+			if(d.dtype == ~0){
+				report(errorc, sys->sprint("filesystem loop at %#q", path + d.name));
+				continue;
+			}
+			if(sys->chdir("./" + d.name) == -1){
+				report(errorc, sys->sprint("cannot cd to %#q: %r", path + a[i].name));
+				continue;
+			}
+			c <-= ((d, nil), reply);
+			case <-reply {
+			Quit =>
+				quit(errorc);
+			Down =>
+				fswalkdir(path + a[i].name + "/", c, blocksize, loopcheck, errorc);
+				c <-= ((nil, nil), reply);
+				if(<-reply == Quit)
+					quit(errorc);
+			Skip =>
+				sys->chdir("..");
+				i++;
+				break directory;
+			Next =>
+				break;
+			}
+			if(sys->chdir("..") == -1)		# XXX what should we do if this fails?
+				report(errorc, sys->sprint("failed to cd .. from %#q: %r\n", path + a[i].name));
+			
+		} else {
+			if(fswalkfile(path, a[i], c, blocksize, errorc) == Skip)
+				break directory;
+		}
+	}
+	for(i = n - 1; i >= 0; i--)
+		if(a[i].mode & Sys->DMDIR && a[i].dtype != ~0)
+			loopcheck.leave(a[i]);
+}
+
+fswalkfile(path: string, d: ref Sys->Dir, c: Fschan, blocksize: int, errorc: chan of string): int
+{
+	reply := chan of int;
+	fd := sys->open(d.name, Sys->OREAD);
+	if(fd == nil){
+		report(errorc, sys->sprint("cannot open %q: %r", path+d.name));
+		return Next;
+	}
+	c <-= ((d, nil), reply);
+	case <-reply {
+	Quit =>
+		quit(errorc);
+	Skip =>
+		return Skip;
+	Next =>
+		return Next;
+	Down =>
+		break;
+	}
+	length := d.length;
+	for(n := big 0; n < length; ){
+		nr := blocksize;
+		if(n + big blocksize > length)
+			nr = int (length - n);
+		buf := array[nr] of byte;
+		nr = sys->read(fd, buf, nr);
+		if(nr <= 0){
+			if(nr < 0)
+				report(errorc, sys->sprint("error reading %q: %r", path + d.name));
+			else
+				report(errorc, sys->sprint("%q is shorter than expected (%bd/%bd)",
+						path + d.name, n, length));
+			break;
+		}else if(nr < len buf)
+			buf = buf[0:nr];
+		c <-= ((nil, buf), reply);
+		case <-reply {
+		Quit =>
+			quit(errorc);
+		Skip =>
+			return Next;
+		}
+		n += big nr;
+	}
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+HASHSIZE: con 32;
+
+issamedir(d0, d1: ref Sys->Dir): int
+{
+	(q0, q1) := (d0.qid, d1.qid);
+	return q0.path == q1.path &&
+		q0.qtype == q1.qtype &&
+		d0.dtype == d1.dtype &&
+		d0.dev == d1.dev;
+}
+
+Loopcheck.new(): ref Loopcheck
+{
+	return ref Loopcheck(array[HASHSIZE] of list of ref Sys->Dir);
+}
+
+# XXX we're assuming no-one modifies the values in d behind our back...
+Loopcheck.enter(l: self ref Loopcheck, d: ref Sys->Dir): int
+{
+	slot := int d.qid.path & (HASHSIZE-1);
+	for(ll := l.a[slot]; ll != nil; ll = tl ll)
+		if(issamedir(d, hd ll))
+			return 0;
+	l.a[slot] = d :: l.a[slot];
+	return 1;
+}
+
+Loopcheck.leave(l: self ref Loopcheck, d: ref Sys->Dir)
+{
+	slot := int d.qid.path & (HASHSIZE-1);
+	l.a[slot] = tl l.a[slot];
+}
--- /dev/null
+++ b/appl/alphabet/fs/write.b
@@ -1,0 +1,137 @@
+implement Write, Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report, report: import reports;
+include "alphabet/fs.m";
+	fs: Fs;
+	Value: import fs;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Option,
+	Next, Down, Skip, Quit: import Fs;
+
+Write: module {};
+types(): string
+{
+	return "rxs-v";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: write: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fs = load Fs Fs->PATH;
+	if(fs == nil)
+		badmod(Fs->PATH);
+	fs->init();
+	reports = load Reports Reports->PATH;
+	if(reports == nil)
+		badmod(Reports->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	sync := chan of string;
+	spawn  fswriteproc(sync, (hd tl args).s().i, (hd args).x().i, report.start("fswrite"), opts!=nil);
+	<-sync;
+	return ref Value.Vr(sync);
+}
+
+fswriteproc(sync: chan of string, root: string, c: Fschan, errorc: chan of string, verbose: int)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= nil;
+	if(<-sync != nil){
+		(<-c).t1 <-= Quit;
+		quit(sync, errorc);
+	}
+		
+	(d, reply) := <-c;
+	if(root != nil){
+		d.dir = ref *d.dir;
+		d.dir.name = root;
+	}
+	fswritedir(d.dir.name, d, reply, c, errorc, verbose);
+	quit(sync, errorc);
+}
+
+quit(sync: chan of string, errorc: chan of string)
+{
+	errorc <-= nil;
+	sync <-= nil;
+	exit;
+}
+
+fswritedir(path: string, d: Fsdata, dreply: chan of int, c: Fschan, errorc: chan of string, verbose: int)
+{
+	fd: ref Sys->FD;
+	if(verbose)
+		report(errorc, sys->sprint("create %q %uo", path, d.dir.mode));
+	if(d.dir.mode & Sys->DMDIR){
+		created := 1;
+		fd = sys->create(d.dir.name, Sys->OREAD, d.dir.mode|8r777);
+		if(fd == nil){
+			err := sys->sprint("%r");
+			if((fd = sys->open(d.dir.name, Sys->OREAD)) == nil){
+				dreply <-= Next;
+				report(errorc, sys->sprint("cannot create %q, mode %uo: %s", path, d.dir.mode|8r300, err));
+				return;
+			}else
+				created = 0;
+		}
+		if(sys->chdir(d.dir.name) == -1){		# XXX beware of names starting with '#'
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot cd to %q: %r", path));
+			fd = nil;
+			sys->remove(d.dir.name);
+			return;
+		}
+		dreply <-= Down;
+		path[len path] = '/';
+		for(;;){
+			(ent, reply) := <-c;
+			if(ent.dir == nil){
+				reply <-= Next;
+				break;
+			}
+			fswritedir(path + ent.dir.name, ent, reply, c, errorc, verbose);
+		}
+		sys->chdir("..");
+		if(created && (d.dir.mode & 8r777) != 8r777){
+			ws := Sys->nulldir;
+			ws.mode = d.dir.mode;
+			if(sys->fwstat(fd, ws) == -1)
+				report(errorc, sys->sprint("cannot wstat %q: %r", path));
+		}
+	}else{
+		fd = sys->create(d.dir.name, Sys->OWRITE, d.dir.mode);
+		if(fd == nil){
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot create %q, mode %uo: %r", path, d.dir.mode|8r300));
+			return;
+		}
+		dreply <-= Down;
+		while((((nil, buf), reply) := <-c).t0.data != nil){
+			nw := sys->write(fd, buf, len buf);
+			if(nw < len buf){
+				if(nw == -1)
+					errorc <-= sys->sprint("error writing %q: %r", path);
+				else
+					errorc <-= sys->sprint("short write");
+				reply <-= Skip;
+				break;
+			}
+			reply <-= Next;
+		}
+		reply <-= Next;
+	}
+}
--- /dev/null
+++ b/appl/alphabet/fsdecl.sh
@@ -1,0 +1,13 @@
+load alphabet std
+
+typeset /fs
+
+declare /fs/walk
+declare /fs/entries
+declare /fs/match
+declare /fs/print
+
+autoconvert /string /fs/fs /fs/walk
+autoconvert /fs/fs /fs/entries /fs/entries
+autoconvert /string /fs/gate /fs/match
+autoconvert /fs/entries /fd /fs/print
--- /dev/null
+++ b/appl/alphabet/getendpoint.sh
@@ -1,0 +1,13 @@
+#!/dis/sh -n
+autoload=std
+load std
+if{! ~ $#* 1}{
+	echo usage: getendpoint addr >[1=2]
+	raise usage
+}
+addr:=$1
+if{! ftest -e /n/endpoint/dsgdsfgeafreqeq}{
+	mount {mntgen} /n/endpoint
+}
+mount -A $addr /n/endpoint/$addr
+bind /n/endpoint/$addr /n/endpoint/local
--- /dev/null
+++ b/appl/alphabet/grid/farm.b
@@ -1,0 +1,144 @@
+implement Farm, Gridmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+include "string.m";
+	str: String;
+include "alphabet/reports.m";
+	reports: Reports;
+	report, Report, quit: import reports;
+include "alphabet/endpoints.m";
+	endpoints: Endpoints;
+	Endpoint: import endpoints;
+include "alphabet/grid.m";
+	grid: Grid;
+	Value: import grid;
+
+Farm: module {};
+
+types(): string
+{
+	return "eesss*-A-k-a-v-bs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	endpoints = checkload(load Endpoints Endpoints->PATH, Endpoints->PATH);
+	endpoints->init();
+	grid = checkload(load Grid Grid->PATH, Grid->PATH);
+	grid->init();
+	sh = checkload(load Sh Sh->PATH, Sh->PATH);
+	sh->initialise();
+	str = checkload(load String String->PATH, String->PATH);
+}
+
+run(nil: chan of string, r: ref Reports->Report,
+		opt: list of (int, list of ref Grid->Value), args: list of ref Grid->Value): ref Grid->Value
+{
+	ec0 := (hd args).e().i;
+	addr := (hd tl args).s().i;
+	job, opts: string;
+	noauth := 0;
+	for(; opt != nil; opt = tl opt){
+		c := (hd opt).t0;
+		case (hd opt).t0 {
+		'A' => 
+			noauth = 1;
+		'b' =>
+			opts += " -b "+(hd (hd opt).t1).s().i;
+		* =>
+			opts += sys->sprint(" -%c", (hd opt).t0);
+		}
+	}
+	for(args = tl tl args; args != nil; args = tl args)
+		job += sys->sprint(" %q", (hd args).s().i);
+
+	spawn farmproc(sync := chan of int, addr, ec0, opts, job, noauth, r.start("farm"), ec := chan of Endpoint);
+	<-sync;
+	return ref Value.Ve(ec);
+}
+
+farmproc(sync: chan of int,
+		addr: string,
+		ec0: chan of Endpoint,
+		opts: string,
+		job: string,
+		noauth: int,
+		errorc: chan of string,
+		ec1: chan of Endpoint)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+	ep0 := <-ec0;
+	if(ep0.addr == nil){
+		ec1 <-= ep0;
+		quit(errorc);
+	}
+	(v, e) := farm(addr, ep0, opts, job, noauth, errorc);
+	if(e != nil){
+		endpoints->open(nil, ep0);
+		report(errorc, "error: "+e);
+	}
+	ec1 <-= v;
+	quit(errorc);
+}
+
+Nope: con Endpoint(nil, nil, nil);
+
+farm(addr: string,
+	ep0: Endpoint,
+	opts: string,
+	job: string,
+	noauth: int,
+	errorc: chan of string): (Endpoint, string)
+{
+	args := addr::"/n/remote"::nil;
+	if(noauth)
+		args = "-A"::args;
+	if((e := sh->run(nil, "mount"::args)) != nil)
+		return (Nope, sys->sprint("cannot mount scheduler at %q: %s, args %s", addr, e, str->quoted(args)));
+
+	fd := sys->open("/n/remote/admin/clone", Sys->ORDWR);
+	if(fd == nil)
+		return (Nope, sys->sprint("cannot open clone: %r"));
+	if((d := gets(fd)) == nil)
+		return (Nope, "read clone failed");
+	dir := "/n/remote/admin/"+d;
+	if(sys->fprint(fd, "load workflow%s %q %s", opts, ep0.text(), job) == -1)
+		return (Nope, sys->sprint("job load failed: %r"));
+	if(sys->fprint(fd, "start") == -1)
+		return (Nope, sys->sprint("job start failed: %r"));
+	dfd := sys->open(dir+"/data", Sys->OREAD);
+	if(dfd == nil){
+		sys->fprint(fd, "delete");
+		return (Nope, sys->sprint("cannot open job data file: %r"));
+	}
+	s := gets(dfd);
+	ep1 := Endpoint.mk(s);
+	if(ep1.addr == nil)
+		return (Nope, sys->sprint("bad remote endpoint %q", s));
+	report(errorc, sys->sprint("job %s started, id %s", d, gets(sys->open(dir+"/id", Sys->OREAD))));
+	# XXX how is the job going to be deleted eventually
+	ep1.about = sys->sprint("%s | farm%s %s%s", ep0.about, opts, addr, job);
+	return (ep1, nil);
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
+
+gets(fd: ref Sys->FD): string
+{
+	d := array[8192] of byte;
+	n := sys->read(fd, d, len d);
+	if(n <= 0)
+		return nil;
+	return string d[0:n];
+}
--- /dev/null
+++ b/appl/alphabet/grid/line2rec.b
@@ -1,0 +1,91 @@
+implement Line2rec, Gridmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet/endpoints.m";
+include "alphabet/grid.m";
+	grid: Grid;
+	Value: import grid;
+
+Line2rec: module {};
+
+types(): string
+{
+	return "bf";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	grid = load Grid Grid->PATH;
+	reports = load Reports Reports->PATH;
+	bufio = load Bufio Bufio->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: chan of string, r: ref Report,
+		nil: list of (int, list of ref Value), args: list of ref Value): ref Value
+{
+	f := chan of ref Sys->FD;
+	spawn line2recproc((hd args).f().i, f, r.start("line2rec"));
+	return ref Value.Vb(f);
+}
+
+line2recproc(
+	f0,
+	f1: chan of ref Sys->FD,
+	errorc: chan of string)
+{
+	(fd0, fd1) := startfilter(f0, f1, errorc);
+	iob0 := bufio->fopen(fd0, Sys->OREAD);
+	iob1 := bufio->fopen(fd1, Sys->OWRITE);
+	{
+		while((s := iob0.gets('\n')) != nil){
+			d := array of byte s;
+			if(iob1.puts("data "+string len d) < 0)
+				break;
+			if(iob1.write(d, len d) != len d)
+				break;
+		}
+		iob1.flush();
+		sys->fprint(fd1, "");
+	}exception{
+	"write on closed pipe" =>
+		;
+	}
+	reports->quit(errorc);
+}
+
+# read side (when it's an argument):
+# 	read proposed new fd
+# 	write actual fd for them to write to (creating pipe in necessary)
+# 
+# write side (when you're returning it):
+# 	write a proposed new fd (or nil if no suggestion)
+# 	read actual fd for writing
+startfilter(f0, f1: chan of ref Sys->FD, errorc: chan of string): (ref Sys->FD, ref Sys->FD)
+{
+	f1 <-= nil;
+	if((fd1 := <-f1) == nil){
+		<-f0;
+		f0 <-= nil;
+		reports->quit(errorc);
+	}
+	if((fd0 := <-f0) == nil){
+		sys->pipe(p := array[2] of ref Sys->FD);
+		f0 <-= p[1];
+		fd0 = p[0];
+	}else
+		f0 <-= nil;
+	return (fd0, fd1);
+}
--- /dev/null
+++ b/appl/alphabet/grid/local.b
@@ -1,0 +1,86 @@
+implement Local,Gridmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report, quit, Report: import reports;
+include "alphabet/endpoints.m";
+	endpoints: Endpoints;
+	Endpoint: import endpoints;
+include "alphabet/grid.m";
+	grid: Grid;
+	Value: import grid;
+
+Local: module {};
+types(): string
+{
+	return "fe-v";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	endpoints = checkload(load Endpoints Endpoints->PATH, Endpoints->PATH);
+	endpoints->init();
+	grid = checkload(load Grid Grid->PATH, Grid->PATH);
+	grid->init();
+}
+
+run(nil: chan of string, r: ref Reports->Report,
+		opts: list of (int, list of ref Grid->Value), args: list of ref Grid->Value): ref Grid->Value
+{
+
+	spawn localproc((hd args).e().i, f := chan of ref Sys->FD, opts!=nil, r.start("local"));
+	return ref Value.Vf(f);
+}
+
+localproc(ec: chan of Endpoint, f: chan of ref Sys->FD, verbose: int, errorc: chan of string)
+{
+	ep := <-ec;
+	if(ep.addr == nil){
+		# error should already have been printed (XXX is that the right way to do it?)
+		f <-= nil;
+		<-f;
+		quit(errorc);
+	}
+	if(verbose)
+		report(errorc, sys->sprint("endpoint %q at %q: %s", ep.id, ep.addr, ep.about));
+	(fd0, err) := endpoints->open(nil, ep);
+	if(fd0 == nil){
+		report(errorc, sys->sprint("error: local: cannot open endpoint (%q %q): %s", ep.addr, ep.id, err));
+		f <-= nil;
+		<-f;
+		quit(errorc);
+	}
+	f <-= fd0;
+	fd1 := <-f;
+	if(fd1 == nil)
+		quit(errorc);
+	
+	buf := array[Sys->ATOMICIO] of byte;
+	{
+		while((n := sys->read(fd0, buf, len buf)) > 0){
+#sys->print("local read %d bytes\n", n);
+			sys->write(fd1, buf, n);
+		}
+#sys->print("local eof %d\n", n);
+		sys->write(fd1, array[0] of byte, 0);
+		if(n < 0)
+			report(errorc, sys->sprint("read error: %r"));
+	} exception e {
+	"write on closed pipe" =>
+		report(errorc, "write on closed pipe");
+		;
+	}
+	quit(errorc);
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/grid/mkfile
@@ -1,0 +1,22 @@
+<../../../mkconfig
+
+TARG=\
+	farm.dis\
+	line2rec.dis\
+	local.dis\
+	remote.dis\
+	rexec.dis\
+
+SYSMODULES=\
+	draw.m\
+	alphabet/endpoints.m\
+	alphabet/grid.m\
+	alphabet/reports.m\
+	sh.m\
+	string.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/alphabet/grid
+
+<$ROOT/mkfiles/mkdis
+LIMBOFLAGS=-F $LIMBOFLAGS
--- /dev/null
+++ b/appl/alphabet/grid/remote.b
@@ -1,0 +1,88 @@
+implement Remote, Gridmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	report, quit, Report: import reports;
+include "alphabet/endpoints.m";
+	endpoints: Endpoints;
+	Endpoint: import endpoints;
+include "alphabet/grid.m";
+	grid: Grid;
+	Value: import grid;
+
+Remote: module {};
+
+types(): string
+{
+	return "ef-as";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	endpoints = checkload(load Endpoints Endpoints->PATH, Endpoints->PATH);
+	endpoints->init();
+	grid = checkload(load Grid Grid->PATH, Grid->PATH);
+	grid->init();
+}
+
+run(nil: chan of string, r: ref Reports->Report,
+		opts: list of (int, list of ref Grid->Value), args: list of ref Grid->Value): ref Grid->Value
+{
+	addr := "local";
+	if(opts != nil)
+		addr = (hd (hd opts).t1).s().i;
+	f := (hd args).f().i;
+	spawn remoteproc(ec := chan of Endpoint, f, addr, r.start("remote"));
+	return ref Value.Ve(ec);
+}
+
+Noendpoint: con Endpoint(nil, nil, nil);
+
+remoteproc(ec: chan of Endpoint, f: chan of ref Sys->FD, addr: string, errorc: chan of string)
+{
+	(fd1, ep) := endpoints->create(addr);
+	if(fd1 == nil){
+		report(errorc, "error: remote: cannot create endpoint at "+addr+": "+ep.about);
+		ec <-= Noendpoint;
+		<-f;
+		f <-= nil;
+		quit(errorc);
+	}
+	fd0 := <-f;
+	if(fd0 != nil)
+		ep.about = sys->sprint("local(%#q)", sys->fd2path(fd0));
+	else
+		ep.about = "local(pipe)";
+	ec <-= ep;
+	f <-= fd1;
+	quit(errorc);
+}
+
+#	sys->pipe(p := array[2] of ref Sys->FD);
+#	f <-= p[1];
+#	p[1] = nil;
+#	buf := array[Sys->ATOMICIO] of byte;
+#	while((n := sys->read(p[0], buf, len buf)) > 0){
+#		if(sys->write(fd, buf, n) == -1){
+#			report(errorc, sys->sprint("write error: %r"));
+#			break;
+#		}
+#	}exception{
+#	"write on closed pipe" =>
+#		report(errorc, "got write on closed pipe");
+#	}
+#	sys->write(fd, array[0] of byte, 0);
+#	quit(errorc);
+#}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/grid/rexec.b
@@ -1,0 +1,112 @@
+implement Rexec, Gridmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+include "string.m";
+	str: String;
+include "alphabet/reports.m";
+	reports: Reports;
+	report, Report, quit: import reports;
+include "alphabet/endpoints.m";
+	endpoints: Endpoints;
+	Endpoint: import endpoints;
+include "alphabet/grid.m";
+	grid: Grid;
+	Value: import grid;
+
+Rexec: module {};
+
+types(): string
+{
+	return "eesc-A";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	reports = checkload(load Reports Reports->PATH, Reports->PATH);
+	endpoints = checkload(load Endpoints Endpoints->PATH, Endpoints->PATH);
+	endpoints->init();
+	grid = checkload(load Grid Grid->PATH, Grid->PATH);
+	grid->init();
+	sh = checkload(load Sh Sh->PATH, Sh->PATH);
+	sh->initialise();
+	str = checkload(load String String->PATH, String->PATH);
+}
+
+run(nil: chan of string, r: ref Reports->Report,
+		opts: list of (int, list of ref Grid->Value), args: list of ref Grid->Value): ref Grid->Value
+{
+	ec0 := (hd args).e().i;
+	addr := (hd tl args).s().i;
+	cmd := (hd tl tl args).c().i;
+
+	spawn rexecproc(sync := chan of int, addr, ec0, cmd, r.start("rexec"), opts != nil, ec1 := chan of Endpoint);
+	<-sync;
+	return ref Value.Ve(ec1);
+}
+
+rexecproc(sync: chan of int,
+		addr: string,
+		ec0: chan of Endpoint,
+		cmd: ref Sh->Cmd,
+		errorc: chan of string,
+		noauth: int,
+		ec1: chan of Endpoint
+	)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+
+	ep0 := <-ec0;
+	if(ep0.addr == nil){
+		ec1 <-= ep0;
+		quit(errorc);
+	}
+
+	(ep1, err) := exec(addr, ep0, cmd, noauth);
+	if(err != nil){
+		endpoints->open(nil, ep0);	# discard 
+		report(errorc, err);
+	}
+	ec1 <-= ep1;
+	quit(errorc);
+}
+
+Nope: con Endpoint(nil, nil, nil);
+
+exec(addr: string, ep0: Endpoint, cmd: ref Sh->Cmd, noauth: int): (Endpoint, string)
+{
+	args := addr::"/n/remote"::nil;
+	if(noauth)
+		args = "-A"::args;
+	if((e := sh->run(nil, "mount"::args)) != nil)
+		return (Nope, sys->sprint("cannot mount rexec at %q: %s", addr, e));
+
+	fd := sys->open("/n/remote/exec", Sys->ORDWR);
+	if(fd == nil)
+		return (Nope, sys->sprint("cannot open exec at %q: %r", addr));
+	if(sys->fprint(fd, "%q %q", ep0.text(), sh->cmd2string(cmd)) == -1)
+		return (Nope, sys->sprint("exec write failed: %r"));
+	buf := array[1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return (Nope, sys->sprint("error reading endpoint: %r"));
+	if(n == 0)
+		return (Nope, "eof reading endpoint");
+	s := string buf[0:n];
+	ep1 := Endpoint.mk(s);
+	if(ep1.addr == nil)
+		return (Nope, sys->sprint("bad endpoint %#q: %s", s, ep1.about));
+	ep1.about = sys->sprint("%s | rexec %q %s", ep0.about, addr, sh->cmd2string(cmd));
+	return (ep1, nil);
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	raise sys->sprint("fail:cannot load %s: %r", path);
+}
--- /dev/null
+++ b/appl/alphabet/main/auth.b
@@ -1,0 +1,157 @@
+implement Authenticate, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	auth: Auth;
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Authenticate: module {};
+
+typesig(): string
+{
+	return "ww-ks-Cs-v";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+	keyring = load Keyring Keyring->PATH;
+	auth = load Auth Auth->PATH;
+	auth->init();
+}
+
+quit()
+{
+}
+
+After, Before, Create: con 1<<iota;
+
+run(nil: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+		opts: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	keyfile: string;
+	alg: string;
+	verbose: int;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).t0 {
+		'k' =>
+			keyfile = (hd (hd opts).t1).s().i;
+			if (keyfile != nil && ! (keyfile[0] == '/' || (len keyfile > 2 &&  keyfile[0:2] == "./")))
+				keyfile = "/usr/" + user() + "/keyring/" + keyfile;
+		'C' =>
+			alg = (hd (hd opts).t1).s().i;
+		'v' =>
+			verbose = 1;
+		}
+	}
+	if(keyfile == nil)
+		keyfile = "/usr/" + user() + "/keyring/default";
+	cert := keyring->readauthinfo(keyfile);
+	if (cert == nil) {
+		report(errorc, sys->sprint("auth: cannot read %q: %r", keyfile));
+		return nil;
+	}
+	w := chan of ref Sys->FD;
+	spawn authproc((hd args).w().i, w, cert, verbose, alg, r.start("auth"));
+	return ref Value.Vw(w);
+}
+
+authproc(f0, f1: chan of ref Sys->FD, cert: ref Keyring->Authinfo,
+		verbose: int, alg: string, errorc: chan of string)
+{
+	fd0 := <-f0;
+	if(fd0 == nil){
+		sys->pipe(p := array[2] of ref Sys->FD);
+		f0 <-= p[1];
+		fd0 = p[0];
+	}else
+		f0 <-= nil;
+
+	eu: string;
+	(fd0, eu) = auth->client(alg, cert, fd0);
+	if(fd0 == nil){
+		report(errorc, "authentication failed: "+eu);
+		f1 <-= nil;
+		<-f1;
+		reports->quit(errorc);
+	}
+	if(verbose)
+		report(errorc, sys->sprint("remote user %q", eu));
+	f1 <-= fd0;
+	fd1 := <-f1;
+	if(fd1 == nil)
+		reports->quit(errorc);
+	wstream(fd0, fd1, errorc);
+	reports->quit(errorc);
+}
+
+wstream(fd0, fd1: ref Sys->FD, errorc: chan of string)
+{
+	sync := chan[2] of int;
+	qc := chan of int;
+	spawn stream(fd0, fd1, sync, qc, errorc);
+	spawn stream(fd1, fd0, sync, qc, errorc);
+	<-qc;
+	kill(<-sync);
+	kill(<-sync);
+}
+
+stream(fd0, fd1: ref Sys->FD, sync, qc: chan of int, errorc: chan of string)
+{
+	sync <-= sys->pctl(0, nil);
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd0, buf, len buf)) > 0){
+		if(sys->write(fd1, buf, n) == -1){
+			report(errorc, sys->sprint("write error: %r"));
+			break;
+		}
+	}
+	qc <-= 1;
+	exit;
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open("#p/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
+
+
+exists(f: string): int
+{
+	(ok, nil) := sys->stat(f);
+	return ok != -1;
+}
+
+user(): string
+{
+	u := readfile("/dev/user");
+	if (u == nil)
+		return "nobody";
+	return u;
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
--- /dev/null
+++ b/appl/alphabet/main/cat.b
@@ -1,0 +1,78 @@
+implement Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+typesig(): string
+{
+	return "ff*";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, r: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	fds: list of chan of ref Sys->FD;
+	for(; args != nil; args = tl args)
+		fds = (hd args).f().i :: fds;
+	f := chan of ref Sys->FD;
+	spawn catproc(f, rev(fds), r.start("print"));
+	return ref Value.Vf(f);
+}
+
+catproc(f: chan of ref Sys->FD, fds: list of chan of ref Sys->FD, reportc: chan of string)
+{
+	f <-= nil;
+	if((fd1 := <-f) == nil){
+		for(; fds != nil; fds = tl fds){
+			<-hd fds;
+			hd fds <-= nil;
+		}
+		reports->quit(reportc);
+	}
+	buf := array[8192] of byte;
+	for(; fds != nil; fds = tl fds){
+		fd0 := <-hd fds;
+		if(fd0 == nil){
+			p := array[2] of ref Sys->FD;
+			sys->pipe(p);
+			fd0 = p[0];
+			hd fds <-= p[1];
+		}else
+			hd fds <-= nil;
+		while((n := sys->read(fd0, buf, len buf)) > 0){
+			sys->write(fd1, buf, n);
+		}exception{
+		"write on closed pipe" =>
+			;
+		}
+	}
+	sys->write(fd1, array[0] of byte, 0);
+	reports->quit(reportc);
+}
+
+rev[T](l: list of T): list of T
+{
+	r: list of T;
+	for(; l != nil; l = tl l)
+		r = hd l :: r;
+	return r;
+}
--- /dev/null
+++ b/appl/alphabet/main/create.b
@@ -1,0 +1,55 @@
+implement Create,Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Create: module {};
+
+typesig(): string
+{
+	return "rfs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, errorc: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	r := chan of string;
+	fd := sys->create((hd tl args).s().i, Sys->OWRITE, 8r666);
+	if(fd == nil){
+		report(errorc, sys->sprint("error: cannot create %q: %r", (hd tl args).s().i));
+		return nil;
+	}
+	spawn createproc(r, (hd args).f().i, fd);
+	return ref Value.Vr(r);
+}
+
+createproc(r: chan of string, f: chan of ref Sys->FD, fd: ref Sys->FD)
+{
+	if(<-r != nil){
+		<-f;
+		f <-= nil;
+		exit;
+	}
+	<-f;
+	f <-= fd;
+	r <-= nil;
+}
--- /dev/null
+++ b/appl/alphabet/main/dial.b
@@ -1,0 +1,85 @@
+implement Dial,Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Dial: module {};
+
+typesig(): string
+{
+	return "ws";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	w := chan of ref Sys->FD;
+	addr := (hd args).s().i;
+	(ok, c) := sys->dial(addr, nil);
+	if(ok == -1){
+		report(errorc, sys->sprint("dial: cannot dial %q: %r", addr));
+		return nil;
+	}
+	f := chan of ref Sys->FD;
+	spawn dialproc(f, c.dfd, r.start("dial"));
+	return ref Value.Vw(f);
+}
+
+dialproc(f: chan of ref Sys->FD, fd0: ref Sys->FD, errorc: chan of string)
+{
+	f <-= fd0;
+	fd1 := <-f;
+	if(fd1 == nil)
+		reports->quit(errorc);
+	wstream(fd0, fd1, errorc);
+	reports->quit(errorc);
+}
+
+wstream(fd0, fd1: ref Sys->FD, errorc: chan of string)
+{
+	sync := chan[2] of int;
+	qc := chan of int;
+	spawn stream(fd0, fd1, sync, qc, errorc);
+	spawn stream(fd1, fd0, sync, qc, errorc);
+	<-qc;
+	kill(<-sync);
+	kill(<-sync);
+}
+
+stream(fd0, fd1: ref Sys->FD, sync, qc: chan of int, errorc: chan of string)
+{
+	sync <-= sys->pctl(0, nil);
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd0, buf, len buf)) > 0){
+		if(sys->write(fd1, buf, n) == -1){
+			report(errorc, sys->sprint("write error: %r"));
+			break;
+		}
+	}
+	qc <-= 1;
+	exit;
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open("#p/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
--- /dev/null
+++ b/appl/alphabet/main/echo.b
@@ -1,0 +1,51 @@
+implement Echo, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Echo: module {};
+
+typesig(): string
+{
+	return "fs-n";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, nil: chan of string,
+		opts: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	f := chan of ref Sys->FD;
+	s := (hd args).s().i;
+	if(opts == nil)
+		s[len s] = '\n';
+	spawn echoproc(f, s);
+	return ref Value.Vf(f);
+}
+
+echoproc(f: chan of ref Sys->FD, s: string)
+{
+	f <-= nil;
+	fd := <-f;
+	if(fd == nil)
+		exit;
+	sys->fprint(fd, "%s", s);
+	sys->write(fd, array[0] of byte, 0);
+}
--- /dev/null
+++ b/appl/alphabet/main/export.b
@@ -1,0 +1,52 @@
+implement Export,Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Export: module {};
+
+typesig(): string
+{
+	return "ws";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, r: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	w := chan of ref Sys->FD;
+	addr := (hd args).s().i;
+	f := chan of ref Sys->FD;
+	spawn exportproc(f, (hd args).s().i, r.start("export"));
+	return ref Value.Vw(f);
+}
+
+exportproc(f: chan of ref Sys->FD, dir: string, errorc: chan of string)
+{
+	f <-= nil;
+	fd := <-f;
+	if(fd == nil)
+		reports->quit(errorc);
+	errorc <-= nil;
+	if(sys->export(fd, dir, Sys->EXPASYNC) == -1)
+		report(errorc, sys->sprint("cannot export: %r"));
+	reports->quit(errorc);
+}
--- /dev/null
+++ b/appl/alphabet/main/fd.b
@@ -1,0 +1,83 @@
+implement Fd, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Fd: module {};
+
+typesig(): string
+{
+	return "ws";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	fd := sys->fildes(int (hd args).s().i);
+	if(fd == nil){
+		report(errorc, sys->sprint("error: no such file descriptor %q", (hd args).s().i));
+		return nil;
+	}
+	f := chan of ref Sys->FD;
+	spawn readfdproc(f, fd, r.start("stdin"));
+	return ref Value.Vw(f);
+}
+
+readfdproc(f: chan of ref Sys->FD, fd0: ref Sys->FD, errorc: chan of string)
+{
+	f <-= fd0;
+	fd1 := <-f;
+	if(fd1 == nil)
+		reports->quit(errorc);
+	wstream(fd0, fd1, errorc);
+	reports->quit(errorc);
+}
+
+wstream(fd0, fd1: ref Sys->FD, errorc: chan of string)
+{
+	sync := chan[2] of int;
+	qc := chan of int;
+	spawn stream(fd0, fd1, sync, qc, errorc);
+	spawn stream(fd1, fd0, sync, qc, errorc);
+	<-qc;
+	kill(<-sync);
+	kill(<-sync);
+}
+
+stream(fd0, fd1: ref Sys->FD, sync, qc: chan of int, errorc: chan of string)
+{
+	sync <-= sys->pctl(0, nil);
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd0, buf, len buf)) > 0){
+		if(sys->write(fd1, buf, n) == -1){
+			report(errorc, sys->sprint("write error: %r"));
+			break;
+		}
+	}
+	qc <-= 1;
+	exit;
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open("#p/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
--- /dev/null
+++ b/appl/alphabet/main/filter.b
@@ -1,0 +1,114 @@
+implement Filter, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Filter: module {};
+
+typesig(): string
+{
+	return "ffcs*";		# XXX option to suppress stderr?
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+	bufio = load Bufio Bufio->PATH;
+	sh = load Sh Sh->PATH;
+	sh->initialise();
+}
+
+quit()
+{
+}
+
+run(drawctxt: ref Draw->Context, report: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value): ref Value
+{
+	f := chan of ref Sys->FD;
+	a: list of ref Sh->Listnode;
+	for(al := tl tl args; al != nil; al = tl al)
+		a = ref Sh->Listnode(nil, (hd al).s().i) :: a;
+	spawn filterproc(drawctxt, (hd args).f().i, f, (hd tl args).c().i, rev(a), report.start("filter"));
+	return ref Value.Vf(f);
+}
+
+filterproc(drawctxt: ref Draw->Context,
+	f0,
+	f1: chan of ref Sys->FD,
+	c: ref Sh->Cmd,
+	args: list of ref Sh->Listnode,
+	errorc: chan of string)
+{
+	(fd0, fd1) := startfilter(f0, f1, errorc);
+	sys->pipe(p := array[2] of ref Sys->FD);
+	spawn stderrproc(p[0], errorc);
+	p[0] = nil;
+
+	# i hate this stuff.
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(fd0.fd, 0);
+	sys->dup(fd1.fd, 1);
+	sys->dup(p[1].fd, 2);
+	fd0 = fd1 = nil;
+	p = nil;
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	Context.new(drawctxt).run(ref Sh->Listnode(c, nil)::args, 0);
+	sys->fprint(sys->fildes(2), "");
+}
+
+# read side (when it's an argument):
+# 	read proposed new fd
+# 	write actual fd for them to write to (creating pipe in necessary)
+# 
+# write side (when you're returning it):
+# 	write a proposed new fd (or nil if no suggestion)
+# 	read actual fd for writing
+startfilter(f0, f1: chan of ref Sys->FD, errorc: chan of string): (ref Sys->FD, ref Sys->FD)
+{
+	f1 <-= nil;
+	if((fd1 := <-f1) == nil){
+		<-f0;
+		f0 <-= nil;
+		reports->quit(errorc);
+	}
+	if((fd0 := <-f0) == nil){
+		sys->pipe(p := array[2] of ref Sys->FD);
+		f0 <-= p[1];
+		fd0 = p[0];
+	}else
+		f0 <-= nil;
+	return (fd0, fd1);
+}
+
+stderrproc(fd: ref Sys->FD, errorc: chan of string)
+{
+	iob := bufio->fopen(fd, Sys->OREAD);
+	while((s := iob.gets('\n')) != nil)
+		if(len s > 1)
+			errorc <-= s[0:len s - 1];
+	errorc <-= nil;
+}
+
+rev[T](l: list of T): list of T
+{
+	r: list of T;
+	for(; l != nil; l = tl l)
+		r = hd l :: r;
+	return r;
+}
--- /dev/null
+++ b/appl/alphabet/main/genfilter.b
@@ -1,0 +1,79 @@
+implement Myfilter, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Myfilter: module {};
+
+typesig(): string
+{
+	return "ff";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+	bufio = load Bufio Bufio->PATH;
+}
+
+quit()
+{
+}
+
+run(drawctxt: ref Draw->Context, report: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	f := chan of ref Sys->FD;
+	spawn filterproc(drawctxt, (hd args).f().i, f, report.start("myfilter"));
+	return ref Value.Vf(f);
+}
+
+filterproc(nil: ref Draw->Context, f0, f1: chan of ref Sys->FD, errorc: chan of string)
+{
+	(fd0, fd1) := startfilter(f0, f1, errorc);
+	iob0 := bufio->fopen(fd0, Sys->OREAD);
+	iob1 := bufio->fopen(fd1, Sys->OWRITE);
+
+	# XXX your filter here!
+	while((s := iob0.gets('\n')) != nil){
+		d := array of byte s;
+		iob1.puts("data "+string len d+"\n");
+		iob1.write(d, len d);
+	}exception{
+	"write on closed pipe" =>
+		;
+	}
+	iob1.flush();
+	sys->fprint(fd1, "");
+	reports->quit(errorc);
+}
+
+startfilter(f0, f1: chan of ref Sys->FD, errorc: chan of string): (ref Sys->FD, ref Sys->FD)
+{
+	f1 <-= nil;
+	if((fd1 := <-f1) == nil){
+		<-f0;
+		f0 <-= nil;
+		reports->quit(errorc);
+	}
+	if((fd0 := <-f0) == nil){
+		sys->pipe(p := array[2] of ref Sys->FD);
+		f0 <-= p[1];
+		fd0 = p[0];
+	}else
+		f0 <-= nil;
+	return (fd0, fd1);
+}
--- /dev/null
+++ b/appl/alphabet/main/mkfile
@@ -1,0 +1,36 @@
+<../../../mkconfig
+
+TARG=\
+	auth.dis\
+	cat.dis\
+	create.dis\
+	dial.dis\
+	echo.dis\
+	env.dis\
+	export.dis\
+	fd.dis\
+	filter.dis\
+	mount.dis\
+	par.dis\
+	parse.dis\
+	pretty.dis\
+	print.dis\
+	read.dis\
+	readall.dis\
+	rewrite.dis\
+	seq.dis\
+	unparse.dis\
+	w2fd.dis\
+	wait.dis\
+
+SYSMODULES=\
+	alphabet.m\
+	alphabet/reports.m\
+	draw.m\
+	sh.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/alphabet/main
+
+<$ROOT/mkfiles/mkdis
+LIMBOFLAGS=-F $LIMBOFLAGS
--- /dev/null
+++ b/appl/alphabet/main/mount.b
@@ -1,0 +1,80 @@
+implement Mount,Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Mount: module {};
+
+typesig(): string
+{
+	return "rws-a-b-c-xs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+After, Before, Create: con 1<<iota;
+
+run(nil: ref Draw->Context, report: ref Reports->Report, nil: chan of string,
+		opts: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	flag := Sys->MREPL;
+	aname := "";
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).t0 {
+		'a' =>
+			flag = After & (flag&Sys->MCREATE);
+		'b' =>
+			flag = Before & (flag&Sys->MCREATE);
+		'c' =>
+			flag |= Create;
+		'x' =>
+			aname = (hd (hd opts).t1).s().i;
+		}
+	}
+	r := chan of string;
+	spawn mountproc(r, (hd args).w().i, (hd tl args).s().i, aname, flag, report.start("mount"));
+	return ref Value.Vr(r);
+}
+
+mountproc(r: chan of string, w: chan of ref Sys->FD, dir, aname: string, flag: int, errorc: chan of string)
+{
+	if(<-r != nil){
+		errorc <-= nil;
+		<-w;
+		w <-= nil;
+		exit;
+	}
+	fd := <-w;
+	if(fd == nil){
+		sys->pipe(p := array[2] of ref Sys->FD);
+		w <-= p[0];
+		fd = p[1];
+	}else
+		w <-= nil;
+	if(sys->mount(fd, nil, dir, flag, aname) == -1){
+		e := sys->sprint("mount error on %#q: %r", dir);
+		report(errorc, e);
+		r <-= e;
+		exit;
+	}
+
+	errorc <-= nil;
+	r <-= nil;
+}
--- /dev/null
+++ b/appl/alphabet/main/par.b
@@ -1,0 +1,50 @@
+implement Seq, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Seq: module {};
+
+typesig(): string
+{
+	return "rr*";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	spawn parproc(r := chan of string, args);
+	return ref Value.Vr(r);
+}
+
+parproc(r: chan of string, args: list of ref Alphabet->Value)
+{
+	if(<-r != nil){
+		for(; args != nil; args = tl args)
+			(hd args).r().i <-= "die!";
+	}else{
+		status := "";
+		for(a := args; a != nil; a = tl a)
+			(hd a).r().i <-= nil;
+		for(; args != nil; args = tl args)
+			if((e := <-(hd args).r().i) != nil)
+				status = e;
+		r <-= status;
+	}
+}
--- /dev/null
+++ b/appl/alphabet/main/parse.b
@@ -1,0 +1,43 @@
+implement Parse, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Parse: module {};
+
+typesig(): string
+{
+	return "cs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	sh = load Sh Sh->PATH;
+	sh->initialise();
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, errorc: chan of string,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value): ref Value
+{
+	(c, err) := sh->parse((hd args).s().i);
+	if(c == nil){
+		report(errorc, sys->sprint("parse: parse %q failed: %s", (hd args).s().i, err));
+		return nil;
+	}
+	return ref Value.Vc(c);
+}
--- /dev/null
+++ b/appl/alphabet/main/pretty.b
@@ -1,0 +1,116 @@
+implement Pretty, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	n_BLOCK,  n_VAR, n_BQ, n_BQ2, n_REDIR,
+	n_DUP, n_LIST, n_SEQ, n_CONCAT, n_PIPE, n_ADJ,
+	n_WORD, n_NOWAIT, n_SQUASH, n_COUNT,
+	n_ASSIGN, n_LOCAL,
+	GLOB: import Sh;
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Pretty: module {};
+
+typesig(): string
+{
+	return "sc";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	sh = load Sh Sh->PATH;
+	sh->initialise();
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value): ref Value
+{
+	{
+		return ref Value.Vs(pretty((hd args).c().i, 0));
+	}exception{
+	"bad expr" =>
+		return nil;
+	}
+}
+
+pretty(n: ref Sh->Cmd, depth: int): string
+{
+	if (n == nil)
+		return nil;
+	s: string;
+	case n.ntype {
+	n_BLOCK =>
+		s = "{\n"+tabs(depth+1)+pretty(n.left,depth+1) + "\n"+tabs(depth)+"}";
+	n_VAR =>
+		s = "$" + pretty(n.left, depth);
+	n_LIST =>
+		s = "(" + pretty(n.left, depth) + ")";
+	n_SEQ =>
+		s = pretty(n.left, depth) + "\n"+tabs(depth)+pretty(n.right, depth);
+	n_PIPE =>
+		s = pretty(n.left, depth) + " |\n"+tabs(depth)+pretty(n.right, depth);
+	n_ADJ =>
+		s = pretty(n.left, depth) + " " + pretty(n.right, depth);
+	n_WORD =>
+		s = quote(n.word, 1);
+	n_BQ2 =>
+		# if we can't do it, revert to ugliness.
+		{
+			s = "\"" + pretty(n.left, depth);
+		} exception {
+		"bad expr" =>
+			s = sh->cmd2string(n);
+		}
+	* =>
+		raise "bad expr";
+	}
+	return s;
+}
+
+tabs(n: int): string
+{
+	s: string;
+	while(n-- > 0)
+		s[len s] = '\t';
+	return s;
+}
+
+# stolen from sh.y
+quote(s: string, glob: int): string
+{
+	needquote := 0;
+	t := "";
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'{' or '}' or '(' or ')' or '`' or '&' or ';' or '=' or '>' or '<' or '#' or
+		'|' or '*' or '[' or '?' or '$' or '^' or ' ' or '\t' or '\n' or '\r' =>
+			needquote = 1;
+		'\'' =>
+			t[len t] = '\'';
+			needquote = 1;
+		GLOB =>
+			if (glob) {
+				if (i < len s - 1)
+					i++;
+			}
+		}
+		t[len t] = s[i];
+	}
+	if (needquote || t == nil)
+		t = "'" + t + "'";
+	return t;
+}
--- /dev/null
+++ b/appl/alphabet/main/print.b
@@ -1,0 +1,55 @@
+implement Print,Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Print: module {};
+
+typesig(): string
+{
+	return "rfs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, errorc: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	r := chan of string;
+	fd := sys->fildes(int (hd tl args).s().i);
+	if(fd == nil){
+		report(errorc, sys->sprint("error: no such fd %q", (hd tl args).s().i));
+		return nil;
+	}
+	spawn printproc(r, (hd args).f().i, fd);
+	return ref Value.Vr(r);
+}
+
+printproc(r: chan of string, f: chan of ref Sys->FD, fd: ref Sys->FD)
+{
+	if(<-r != nil){
+		<-f;
+		f <-= nil;
+		exit;
+	}
+	<-f;
+	f <-= fd;
+	r <-= nil;
+}
--- /dev/null
+++ b/appl/alphabet/main/read.b
@@ -1,0 +1,56 @@
+implement Read,Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Read: module{};
+
+typesig(): string
+{
+	return "fs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	f := chan of ref Sys->FD;
+	file := (hd args).s().i;
+	if((fd0 := sys->open(file, Sys->OREAD)) == nil){
+		report(errorc, sys->sprint("cannot open %q: %r", file));
+		return nil;
+	}
+	spawn readproc(f, fd0, r.start("read"));
+	return ref Value.Vf(f);
+}
+
+readproc(f: chan of ref Sys->FD, fd0: ref Sys->FD, errorc: chan of string)
+{
+	f <-= fd0;
+	fd1 := <-f;
+	if(fd1 == nil)
+		reports->quit(errorc);
+	buf := array[8192] of byte;
+	while((n := sys->read(fd0, buf, len buf)) > 0)
+		sys->write(fd1, buf, n);
+	sys->write(fd1, array[0] of byte, 0);
+	reports->quit(errorc);
+}
--- /dev/null
+++ b/appl/alphabet/main/readall.b
@@ -1,0 +1,46 @@
+implement F2s, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+F2s: module {};
+
+typesig(): string
+{
+	return "sf";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value): ref Value
+{
+	f := (hd args).f().i;
+	fd := <-f;
+	if(fd == nil){
+		sys->pipe(p := array[2] of ref Sys->FD);
+		f <-= p[1];
+		fd = p[0];
+	}
+	s: string;
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0)
+		s += string buf[0:n];
+	return ref Value.Vs(s);
+}
--- /dev/null
+++ b/appl/alphabet/main/rewrite.b
@@ -1,0 +1,97 @@
+implement Rewrite, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "alphabet/reports.m";
+	reports: Reports;
+		report: import reports;
+include "alphabet.m";
+	Value: import Alphabet;
+
+Rewrite: module {};
+
+typesig(): string
+{
+	return "ccc-ds";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	sh = load Sh Sh->PATH;
+	sh->initialise();
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(drawctxt: ref Draw->Context, nil: ref Reports->Report, errorc: chan of string,
+		opts: list of (int, list of ref Value),
+		args: list of ref Value): ref Value
+{
+	c := chan of ref Value;
+	spawn rewriteproc(drawctxt, errorc, opts, args, c);
+	return <-c;
+}
+
+# we need a separate process so that we can create a shell context
+# without worrying about opening an already-opened wait file.
+rewriteproc(drawctxt: ref Draw->Context, errorc: chan of string,
+		opts: list of (int, list of ref Value),
+		args: list of ref Value,
+		c: chan of ref Value)
+{
+	c <-= rewrite(drawctxt, errorc, opts, args);
+}
+
+rewrite(drawctxt: ref Draw->Context, errorc: chan of string,
+		opts: list of (int, list of ref Value),
+		args: list of ref Value): ref Value
+{
+	alphabet := load Alphabet Alphabet->PATH;
+	if(alphabet == nil){
+		report(errorc, sys->sprint("rewrite: cannot load %q: %r", Alphabet->PATH));
+		return nil;
+	}
+	Value: import alphabet;
+	alphabet->init();
+	expr := (hd args).c().i;
+	decls := (hd tl args).c().i;
+	ctxt := Context.new(drawctxt);
+	{
+		ctxt.run(w("load")::w("alphabet")::nil, 0);
+		ctxt.run(c(decls) :: nil, 0);
+		dstarg: list of ref Sh->Listnode;
+		if(opts != nil)
+			dstarg = w((hd (hd opts).t1).s().i) :: nil;
+		ctxt.run(w("{x=${rewrite $1 $2}}") :: c(expr) :: dstarg, 0);
+	} exception e {
+	"fail:*" =>
+		ctxt.run(w("clear")::nil, 0);
+		report(errorc, "rewrite failed: "+e[5:]);
+		return nil;
+	}
+	r := ctxt.get("x");
+	if(len r != 2 || (hd r).cmd == nil){
+		ctxt.run(w("clear")::nil, 0);
+		report(errorc, "rewrite not available, strange... (len "+string len r+")");
+		return nil;
+	}
+	ctxt.run(w("clear")::nil, 0);
+	return ref Value.Vc((hd r).cmd);
+}
+
+c(c: ref Sh->Cmd): ref Sh->Listnode
+{
+	return ref Sh->Listnode(c, nil);
+}
+
+w(w: string): ref Sh->Listnode
+{
+	return ref Sh->Listnode(nil, w);
+}
--- /dev/null
+++ b/appl/alphabet/main/rw.b
@@ -1,0 +1,50 @@
+implement Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report, quit: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+typesig(): string
+{
+	return "fs";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+run(nil: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	f := chan of ref Sys->FD;
+	file := (hd args).s().i;
+	if((fd0 := sys->open(file, Sys->OREAD)) == nil){
+		report(errorc, sys->sprint("cannot open %q: %r", file));
+		return nil;
+	}
+	spawn readproc(f, fd0, r.start("read"));
+	return ref Value.F(f);
+}
+
+readproc(f: chan of ref Sys->FD, fd0: ref Sys->FD, errorc: chan of string)
+{
+	f <-= fd0;
+	fd1 := <-f;
+	if(fd1 == nil)
+		quit(errorc);
+	buf := array[8192] of byte;
+	while((n := sys->read(fd0, buf, len buf)) > 0)
+		sys->write(fd1, buf, n);
+	sys->write(fd1, array[0] of byte, 0);
+	quit(errorc);
+}
--- /dev/null
+++ b/appl/alphabet/main/seq.b
@@ -1,0 +1,66 @@
+implement Seq, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Seq: module {};
+
+typesig(): string
+{
+	return "rr*-a-o";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, nil: chan of string,
+		opts: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	stop := -1;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).t0 {
+		'a' =>
+			stop = 0;
+		'o' =>
+			stop = 1;
+		}
+	}
+	spawn seqproc(r := chan of string, args, stop);
+	return ref Value.Vr(r);
+}
+
+seqproc(r: chan of string, args: list of ref Alphabet->Value, stop: int)
+{
+	status := "";
+	if(<-r == nil){
+pid := sys->pctl(0, nil);
+sys->print("%d. seq %d args\n", pid, len args);
+		for(; args != nil; args = tl args){
+			sr := (hd args).r().i;
+sys->print("%d. started\n", pid);
+			sr <-= nil;
+			status = <-sr;
+sys->print("%d. got status\n", pid);
+			if((status == nil) == stop)
+				break;
+		}
+	}else
+		r = nil;
+	for(; args != nil; args = tl args)
+		(hd args).r().i <-= "die!";
+	if(r != nil)
+		r <-= status;
+}
--- /dev/null
+++ b/appl/alphabet/main/unparse.b
@@ -1,0 +1,38 @@
+implement Unparse, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+include "alphabet/reports.m";
+	reports: Reports;
+		Report, report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Unparse: module {};
+
+typesig(): string
+{
+	return "sc";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+	sh = load Sh Sh->PATH;
+	sh->initialise();
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Value),
+		args: list of ref Value): ref Value
+{
+	return ref Value.Vs(sh->cmd2string((hd args).c().i));
+}
--- /dev/null
+++ b/appl/alphabet/main/w2fd.b
@@ -1,0 +1,61 @@
+implement ToFD,Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+	reports: Reports;
+	Report: import reports;
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+ToFD: module {};
+
+typesig(): string
+{
+	return "fw";
+}
+
+init()
+{
+	alphabet = load Alphabet Alphabet->PATH;
+	reports = load Reports Reports->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, r: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	sys = load Sys Sys->PATH;
+	f := chan of ref Sys->FD;
+	spawn tofdproc(f, (hd args).w().i, r.start("2fd"));
+	return ref Value.Vf(f);
+}
+
+tofdproc(f, w: chan of ref Sys->FD, errorc: chan of string)
+{
+	fd0 := <-w;
+	f <-= fd0;
+	fd1 := <-f;
+	if(fd1 == nil)		# asked to quit? tell w to quit too.
+		w <-= nil;
+	else
+	if(fd0 == nil)		# no proposed fd? give 'em the one we've just got.
+		w <-= fd1;
+	else{				# otherwise one-way stream from w to f.
+		w <-= nil;
+		buf := array[Sys->ATOMICIO] of byte;
+		while((n := sys->read(fd0, buf, len buf)) > 0){
+			if(sys->write(fd1, buf, n) == -1){
+				reports->report(errorc, sys->sprint("write error: %r"));
+				break;
+			}
+		}
+	}
+	reports->quit(errorc);
+}
--- /dev/null
+++ b/appl/alphabet/main/wait.b
@@ -1,0 +1,35 @@
+implement Wait, Mainmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet.m";
+	alphabet: Alphabet;
+		Value: import alphabet;
+
+Wait: module {};
+
+typesig(): string
+{
+	return "sr";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	alphabet = load Alphabet Alphabet->PATH;
+}
+
+quit()
+{
+}
+
+run(nil: ref Draw->Context, nil: ref Reports->Report, nil: chan of string,
+		nil: list of (int, list of ref Alphabet->Value),
+		args: list of ref Alphabet->Value): ref Alphabet->Value
+{
+	r := (hd args).r().i;
+	r <-= nil;
+	return ref Value.Vs(<-r);
+}
--- /dev/null
+++ b/appl/alphabet/mkendpoint.sh
@@ -1,0 +1,14 @@
+#!/dis/sh -n
+autoload=std
+load std
+if{! ~ $#* 1}{
+	echo usage: mkendpoint addr >[1=2]
+	raise usage
+}
+addr:=$1
+if{! ftest -e /n/endpoint/dsgdsfgeafreqeq}{
+	mount {mntgen} /n/endpoint
+}
+mount {pctl forkns; alphabet/endpointsrv $addr /n; export /n} /n/endpoint/$addr
+bind /n/endpoint/$addr /n/endpoint/local
+styxlisten -A $addr {export /n/endpoint/local}
--- /dev/null
+++ b/appl/alphabet/mkfile
@@ -1,0 +1,49 @@
+<../../mkconfig
+DIRS=\
+	typesets\
+	auxi\
+	abc\
+	fs\
+	grid\
+	main\
+
+TARG=\
+	alphabet.dis\
+	alphabet.shmod.dis\
+	eval.dis\
+	extvalues.dis\
+	proxy.dis\
+	reports.dis\
+
+INS=${TARG:%=$ROOT/dis/alphabet/%} \
+	$ROOT/dis/sh/alphabet.dis
+
+MODULES=\
+
+SYSMODULES=\
+	alphabet.m\
+	alphabet/abc.m\
+	alphabet/reports.m\
+	draw.m\
+	readdir.m\
+	sets.m\
+	sh.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/alphabet
+
+<$ROOT/mkfiles/mkdis
+LIMBOFLAGS=-F $LIMBOFLAGS
+install:V:	$INS
+
+nuke:V: clean
+	rm -f $INS
+
+uninstall:V:
+	rm -f $INS
+
+$ROOT/dis/sh/alphabet.dis: alphabet.shmod.dis
+	rm -f $ROOT/dis/sh/alphabet.dis && cp alphabet.shmod.dis $ROOT/dis/sh/alphabet.dis
+
+<$ROOT/mkfiles/mksubdirs
+
--- /dev/null
+++ b/appl/alphabet/newtypesets
@@ -1,0 +1,229 @@
+arithmetic typeset:
+	
+		int
+		big
+		real
+	
+	i+i int int -> int
+	
+	{(int); {i+i $1 10}}
+	
+	
+	
+	{i+i 12 34} | {i*i 10} | {
+	
+	Expr: adt {
+		pick {
+		Op =>
+			op: int;
+			l: ref Expr;
+			r: ref Expr;
+		Int =>
+			i: int;
+		Big =>
+			i: big;
+		Real =>
+			i: real;
+		}
+	};
+	
+	+ {int 12} {
+	
+	when we come to run the expression, say in module
+	generate limbo code containing function
+	
+		gen(hd args);
+		gen("+");
+		gen(hd tl args);
+		compile();
+	
+	output limbo code might look like:
+	
+	implement M;
+	M: module {
+		f: fn(a, b: int): int;
+	};
+	
+	f(a, b: int): int
+	{
+		return (a + 
+
+graphics:
+
+	rect point point -> rect
+	point string string -> point
+
+	x point -> string
+	y point -> string
+
+	r string [string...] -> rect
+	r.canon rect -> rect
+	r.min rect -> point
+	r.max rect -> point
+	r.dx rect -> string
+	r.dy rect -> string
+	r.combine rect rect -> rect
+	r+p rect point -> rect
+	r-p rect point -> rect
+	r.inset rect string -> rect
+
+	image [-r] [-b string] [-c string] rect -> image
+	draw [-o string] image point image -> image
+	win [-t string] rect -> image
+	
+	tkwin [-t string] rect -> tk
+	tk tk string -> tk
+
+{(rect); r {min $1|x} {min $1|y} {max $1|x} {max $1|y}}
+
+if we wish to be at all efficient, we need to deal with chans
+not single values.
+
+	r: chan of Rect;
+
+or do we?
+if we had some way of expressing combinations
+of external modules, then perhaps an external
+typeset could do a reasonable job of interpreting stuff.
+
+if a typeset can build expressions bottom-up, incrementally
+out of its own components...
+
+when we're rewriting an expression, we could rewrite it
+in terms of module units provided by the underlying
+typeset... when we ask to find a module, the typeset
+can return some other info as well
+
+we can give the underlying typeset an opportunity
+to optimise the expression, if some of its arguments are
+modules from the same typeset, or from a parent/grandparent
+typeset.
+
+on Load, the typeset could be given expressions representing
+each of its arguments. it then has the opportunity to rewrite
+this whole expression, yielding a module customised for the
+particular arguments it's been given.
+
+perhaps a typeset could assign ids to each module it has returned,
+so that it could easily look up...
+of course, the arguments to the expression consist either
+of modules external to the typeset (no optimisation possible),
+or of modules that have already been loaded by the typeset
+(or by its parent), in which case we can retrieve info on them
+and decide what sort of optimisation might be possible.
+
+there's a moment when you should actually have
+the opportunity to compile optimised code
+(when the expression is passed to another typeset's module?)
+
+---
+
+what about expression types, and allowing access to expressions
+from within the context of a particular typeset.
+
+perhaps any typeset could be treated as the root
+typeset for the purposes of a particular expression evaluation:
+
+what about
+
+	{(/grid/data /fs/fs)
+		/grid/local $1 |
+		/fs/unbundle |
+		/fs/merge $2
+	}
+
+when we wish to pass $1 and $2 from our own program?
+
+rewritten:
+	/fs/merge {/fs/unbundle {/grid/local $1}} $2}
+
+so reduces to
+
+	fd := {grid/local $1}						# in /grid/typeset
+	result := {/fs/merge {/fs/unbundle $fd} $2}	# in /fs typeset
+
+maybe not possible. (bollocks)
+
+---
+
+typeset for the control library.
+
+
+decl {
+	declare read (string >> fd)
+	define hello (string >> fd) {(string); read $1}
+	
+
+abc typeset
+
+	declare [-t string] abc string string -> abc
+	typeset abc string -> abc
+	define abc string cmd -> abc
+	eval abc cmd -> any
+
+{
+	abc |
+	declare read (string >> fd) |
+	define wc (fd >> fd) |
+	define readfile {(>>fd); read /tmp/blah}
+} | {(abc);
+	eval $1 "{
+		read 
+
+compile string >> expr
+
+compile string >> (abc string >> expr)
+
+compile '100 + 12 * sin($1)'
+
+
+transform fd (string >> string) >> fd
+
+
+----
+
+descendant typesets problem...
+
+we can't tell which types are identical.
+
+when we load a typeset, we have to look at its parent
+typeset and use its types if the typec characters are contained there.
+
+
+---- 
+
+if we allow expression types, we have to be very careful...
+can get recursion (equivalent to Y-combinator in λ calculus):
+
+declare eval (cmd->cmd) [(cmd->cmd)...] -> (cmd->cmd)
+
+{((cmd->cmd)->(cmd->cmd))
+	{((cmd->cmd)->(cmd->cmd))
+		eval $1 $1
+	} "{((cmd->cmd)->(cmd->cmd))
+		eval $1 $1
+	}
+}
+
+note this isn't possible without an eval operator and/or
+something that admits a cyclic expression type evaluation.
+
+note also that if this was done in the current implementation,
+it would just hang, as two runs can't be outstanding at the same time (monitor channel).
+
+-----
+
+records:
+
+apply1 records (data -> status) -> records
+apply records (data -> status) -> status
+
+filter records (data -> data) -> records
+filter1 records (data -> data) -> records
+
+discard records -> status
+
+| apply1 "{
+	| data2fd | /fs/unbundle | /fs/write somewhere
+} | apply "{
+	| 
--- /dev/null
+++ b/appl/alphabet/proxy.b
@@ -1,0 +1,304 @@
+implement Proxy;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet.m";
+
+Debug: con 0;
+
+proxy[Ctxt,Cvt,M,V,EV](ctxt: Ctxt): (
+		chan of ref Typescmd[EV],
+		chan of (string, chan of ref Typescmd[V])
+	) for {
+		M =>
+			typesig: fn(m: self M): string;
+			run: fn(m: self M, ctxt: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+					opts: list of (int, list of V), args: list of V): V;
+			quit: fn(m: self M);
+		Ctxt =>
+			loadtypes: fn(ctxt: self Ctxt, name: string): (chan of ref Proxy->Typescmd[V], string);
+			type2s: fn(ctxt: self Ctxt, tc: int): string;
+			alphabet: fn(ctxt: self Ctxt): string;
+			modules: fn(ctxt: self Ctxt, r: chan of string);
+			find: fn(ctxt: self Ctxt, s: string): (M, string);
+			getcvt: fn(ctxt: self Ctxt): Cvt;
+		Cvt =>
+			int2ext: fn(cvt: self Cvt, v: V): EV;
+			ext2int: fn(cvt: self Cvt, ev: EV): V;
+			free: fn(cvt: self Cvt, v: EV, used: int);
+			dup:	fn(cvt: self Cvt, v: EV): EV;
+	}
+{
+	sys = load Sys Sys->PATH;
+	t := chan of ref Typescmd[EV];
+	newts := chan of (string, chan of ref Typescmd[V]);
+	spawn proxyproc(ctxt, t, newts);
+	return (t, newts);
+}
+
+proxyproc[Ctxt,Cvt,M,V,EV](
+		ctxt: Ctxt,
+		t: chan of ref Typescmd[EV],
+		newts: chan of (string, chan of ref Typescmd[V])
+	)
+	for{
+	M =>
+		typesig: fn(m: self M): string;
+		run: fn(m: self M, ctxt: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+				opts: list of (int, list of V), args: list of V): V;
+		quit: fn(m: self M);
+	Ctxt =>
+		loadtypes: fn(ctxt: self Ctxt, name: string): (chan of ref Proxy->Typescmd[V], string);
+		type2s: fn(ctxt: self Ctxt, tc: int): string;
+		alphabet: fn(ctxt: self Ctxt): string;
+		modules: fn(ctxt: self Ctxt, r: chan of string);
+		find: fn(ctxt: self Ctxt, s: string): (M, string);
+		getcvt: fn(ctxt: self Ctxt): Cvt;
+	Cvt =>
+		int2ext: fn(cvt: self Cvt, v: V): EV;
+		ext2int: fn(cvt: self Cvt, ev: EV): V;
+		free: fn(cvt: self Cvt, v: EV, used: int);
+		dup:	fn(cvt: self Cvt, v: EV): EV;
+	}
+{
+	typesets: list of (string, chan of ref Typescmd[V]);
+	cvt := ctxt.getcvt();
+	for(;;)alt{
+	gr := <-t =>
+		if(gr == nil){
+			for(; typesets != nil; typesets = tl typesets)
+				(hd typesets).t1 <-= nil;
+			exit;
+		}
+		pick r := gr {
+		Load =>
+			(m, err) := ctxt.find(r.cmd);
+			if(m == nil){
+				r.reply <-= (nil, err);
+			}else{
+				c := chan of ref Modulecmd[EV];
+				spawn modproxyproc(cvt, m, c);
+				r.reply <-= (c, nil);
+			}
+		Alphabet =>
+			r.reply <-= ctxt.alphabet();
+		Free =>
+			cvt.free(r.v, r.used);
+			r.reply <-= 0;
+		Dup =>
+			r.reply <-= cvt.dup(r.v);
+		Type2s =>
+			r.reply <-= ctxt.type2s(r.tc);
+		Loadtypes =>
+			ts := typesets;
+			typesets = nil;
+			c: chan of ref Typescmd[V];
+			for(; ts != nil; ts = tl ts){
+				if((hd ts).t0 == r.name)
+					c = (hd ts).t1;
+				else
+					typesets = hd ts :: typesets;
+			}
+			err: string;
+			if(c == nil)
+				(c, err) = ctxt.loadtypes(r.name);
+			if(c == nil)
+				r.reply <-= (nil, err);
+			else{
+				et := chan of ref Typescmd[EV];
+				spawn extproxyproc(ctxt, ctxt.alphabet(), c, et);
+				r.reply <-= (et, nil);
+			}
+		Modules =>
+			spawn ctxt.modules(r.reply);
+		* =>
+			sys->fprint(sys->fildes(2), "unknown type of proxy request %d\n", tagof gr);
+			raise "unknown type proxy request";
+		}
+	typesets = <-newts :: typesets =>
+		;
+	}
+}
+
+modproxyproc[Cvt,V,EV,M](cvt: Cvt, m: M, c: chan of ref Modulecmd[EV])
+	for{
+	M =>
+		typesig: fn(m: self M): string;
+		run: fn(m: self M, ctxt: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+				opts: list of (int, list of V), args: list of V): V;
+		quit: fn(m: self M);
+	Cvt =>
+		int2ext: fn(cvt: self Cvt, v: V): EV;
+		ext2int: fn(cvt: self Cvt, ev: EV): V;
+		free: fn(cvt: self Cvt, ev: EV, used: int);
+	}
+{
+	while((gr := <-c) != nil){
+		pick r := gr {
+		Typesig =>
+			r.reply <-= m.typesig();
+		Run =>
+			# XXX could start (or invoke) a new process so that we don't potentially
+			# block concurrency while we're starting the command.
+			{
+				iopts: list of (int, list of V);
+				for(o := r.opts; o != nil; o = tl o){
+					il := extlist2intlist(cvt, (hd o).t1);
+					iopts = ((hd o).t0, il) :: iopts;
+				}
+				iopts = revip(iopts);
+				v := cvt.int2ext(m.run(r.ctxt, r.report, r.errorc, iopts, extlist2intlist(cvt, r.args)));
+				free(cvt, r.opts, r.args, v != nil);
+				r.reply <-= v;
+			} exception {
+			"type error" =>
+				if(Debug)
+					sys->fprint(sys->fildes(2), "error: type conversion failed");
+				if(r.errorc != nil)
+					r.errorc <-= "error: type conversion failed";
+				r.reply <-= nil;
+			}
+		}
+	}
+	m.quit();
+}
+
+extproxyproc[Ctxt,Cvt,V,EV](ctxt: Ctxt, alphabet: string, t: chan of ref Typescmd[V], et: chan of ref Typescmd[EV])
+	for{
+	Ctxt =>
+		type2s: fn(ctxt: self Ctxt, tc: int): string;
+		getcvt: fn(ctxt: self Ctxt): Cvt;
+	Cvt =>
+		int2ext: fn(cvt: self Cvt, v: V): EV;
+		ext2int: fn(cvt: self Cvt, ev: EV): V;
+		free: fn(cvt: self Cvt, ev: EV, used: int);
+		dup: fn(cvt: self Cvt, ev: EV): EV;
+	}
+{
+	cvt := ctxt.getcvt();
+	for(;;){
+		gr := <-et;
+		if(gr == nil)
+			break;
+		pick r := gr {
+		Load =>
+			reply := chan of (chan of ref Modulecmd[V], string);
+			t <-= ref Typescmd[V].Load(r.cmd, reply);
+			(c, err) := <-reply;
+			if(c == nil){
+				r.reply <-= (nil, err);
+			}else{
+				ec := chan of ref Modulecmd[EV];
+				spawn extmodproxyproc(cvt, c, ec);
+				r.reply <-= (ec, nil);
+			}
+		Alphabet =>
+			t <-= ref Typescmd[V].Alphabet(r.reply);
+		Free =>
+			cvt.free(r.v, r.used);
+		Dup =>
+			r.reply <-= cvt.dup(r.v);
+		Type2s =>
+			for(i := 0; i < len alphabet; i++)
+				if(alphabet[i] == r.tc)
+					break;
+			if(i == len alphabet)
+				t <-= ref Typescmd[V].Type2s(r.tc, r.reply);
+			else
+				r.reply <-= ctxt.type2s(r.tc);
+		Loadtypes =>
+			reply := chan of (chan of ref Typescmd[V], string);
+			t <-= ref Typescmd[V].Loadtypes(r.name, reply);
+			(c, err) := <-reply;
+			if(c == nil)
+				r.reply <-= (nil, err);
+			else{
+				t <-= ref Typescmd[V].Alphabet(areply := chan of string);
+				ec := chan of ref Typescmd[EV];
+				spawn extproxyproc(ctxt, <-areply, c, ec);
+				r.reply <-= (ec, nil);
+			}
+		Modules =>
+			t <-= ref Typescmd[V].Modules(r.reply);
+		* =>
+			sys->fprint(sys->fildes(2), "unknown type of proxy request %d\n", tagof gr);
+			raise "unknown type proxy request";
+		}
+	}
+	et <-= nil;
+}
+	
+extmodproxyproc[Cvt,V,EV](cvt: Cvt, c: chan of ref Modulecmd[V], ec: chan of ref Modulecmd[EV])
+	for{
+	Cvt =>
+		int2ext: fn(cvt: self Cvt, v: V): EV;
+		ext2int: fn(cvt: self Cvt, ev: EV): V;
+		free: fn(cvt: self Cvt, ev: EV, used: int);
+	}
+{
+	while((gr := <-ec) != nil){
+		pick r := gr {
+		Typesig =>
+			c <-= ref Modulecmd[V].Typesig(r.reply);
+		Run =>
+			{
+				iopts: list of (int, list of V);
+				for(o := r.opts; o != nil; o = tl o){
+					il := extlist2intlist(cvt, (hd o).t1);
+					iopts = ((hd o).t0, il) :: iopts;
+				}
+				iopts = revip(iopts);
+				c <-= ref Modulecmd[V].Run(
+					r.ctxt,
+					r.report,
+					r.errorc,
+					iopts,
+					extlist2intlist(cvt, r.args),
+					reply := chan of V
+				);
+				v := cvt.int2ext(<-reply);
+				free(cvt, r.opts, r.args, v != nil);
+				r.reply <-= v;
+			}
+		}
+	}
+}
+
+
+revip[V](l: list of (int, V)): list of (int, V)
+{
+	m: list of (int, V);
+	for(; l != nil; l = tl l)
+		m = hd l :: m;
+	return m;
+}
+
+extlist2intlist[V,EV,Cvt](cvt: Cvt, vl: list of EV): list of V
+	for{
+	Cvt =>
+		int2ext: fn(cvt: self Cvt, v: V): EV;
+		ext2int: fn(cvt: self Cvt, ev: EV): V;
+	}
+{
+	l, m: list of V;
+	for(; vl != nil; vl = tl vl)
+		l = cvt.ext2int(hd vl) :: l;
+	for(; l != nil; l = tl l)
+		m = hd l :: m;
+	return m;
+}
+
+free[V,Cvt](cvt: Cvt, opts: list of (int, list of V), args: list of V, used: int)
+	for{
+	Cvt =>
+		free: fn(cvt: self Cvt, ev: V, used: int);
+	}
+{
+	for(; args != nil; args = tl args)
+		cvt.free(hd args, used);
+	for(; opts != nil; opts = tl opts)
+		for(args = (hd opts).t1; args != nil; args = tl args)
+			cvt.free(hd args, used);
+}
--- /dev/null
+++ b/appl/alphabet/reports.b
@@ -1,0 +1,189 @@
+implement Reports;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+
+Reporter: adt {
+	id: int;
+	name: string;
+	stopc: chan of int;
+};
+
+reportproc(errorc: chan of string, stopc: chan of int, reply: chan of ref Report)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	r := ref Report(chan of (string, chan of string, chan of int), chan of int);
+	if(stopc == nil)
+		stopc = chan of int;
+	else
+		sys->pctl(Sys->NEWPGRP, nil);
+	reply <-= r;
+	reportproc0(stopc, errorc, r.startc, r.enablec);
+}
+
+Report.start(r: self ref Report, name: string): chan of string
+{
+	if(r == nil)
+		return nil;
+	errorc := chan of string;
+	r.startc <-= (name, errorc, nil);
+	return errorc;
+}
+
+Report.add(r: self ref Report, name: string, errorc: chan of string, stopc: chan of int)
+{
+	r.startc <-= (name, errorc, stopc);
+}
+
+Report.enable(r: self ref Report)
+{
+	r.enablec <-= 0;
+}
+
+reportproc0(
+		stopc: chan of int,
+		reportc: chan of string,
+		startc: chan of (string, chan of string, chan of int),
+		enablec: chan of int
+	)
+{
+	realc := array[2] of chan of string;
+	p := array[len realc] of Reporter;
+	a := array[0] of chan of string;
+	id := n := 0;
+	stopped := 0;
+out:
+	for(;;) alt{
+	<-stopc =>
+		stopped = 1;
+		break out;
+	(prefix, c, stop) := <-startc =>
+		if(n == len realc){
+			if(realc == a)
+				a = nil;
+			realc = (array[n * 2] of chan of string)[0:] = realc;
+			p = (array[n * 2] of Reporter)[0:] = p;
+			if(a == nil)
+				a = realc;
+		}
+		realc[n] = c;
+		p[n] = (id++, prefix, stop);
+		n++;
+	<-enablec =>
+		if(n == 0)
+			break out;
+		a = realc;
+	(x, msg) := <-a =>
+		if(msg == nil){
+			if(--n == 0)
+				break out;
+			if(n != x){
+				a[x] = a[n];
+				a[n] = nil;
+				p[x] = p[n];
+				p[n] = (-1, nil, nil);
+			}
+		}else{
+			if(reportc != nil){
+				alt{
+				reportc <-= sys->sprint("%d. %s: %s", p[x].id, p[x].name, msg) =>
+					;
+				<-stopc =>
+					stopped = 1;
+					break out;
+				}
+			}
+		}
+	}
+	if(stopped == 0){
+		if(reportc != nil){
+			alt{
+			reportc <-= nil =>
+				;
+			<-stopc =>
+				stopped = 1;
+			}
+		}
+	}
+	if(stopped){
+		for(i := 0; i < n; i++)
+			note(p[i].stopc);
+		note(stopc);
+	}
+}
+
+quit(errorc: chan of string)
+{
+	if(errorc != nil)
+		errorc <-= nil;
+	exit;
+}
+
+report(errorc: chan of string, err: string)
+{
+	if(errorc != nil)
+		errorc <-= err;
+}
+
+newpgrp(stopc: chan of int, flags: int): chan of int
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(flags&PROPAGATE){
+		if(stopc == nil)
+			stopc = chan[1] of int;
+		sys->pipe(p := array[2] of ref Sys->FD);
+		spawn deadman(p[1]);
+		sys->pctl(Sys->NEWPGRP, nil);
+		spawn watchproc(p[0], stopc); 
+	}else
+		sys->pctl(Sys->NEWPGRP, nil);
+	spawn grpproc(stopc, newstopc := chan[1] of int, flags&KILL);
+	return newstopc;
+}
+
+grpproc(noteparent, noteself: chan of int, kill: int)
+{
+	if(noteparent == nil)
+		noteparent = chan of int;
+	alt{
+	<-noteparent =>
+		note(noteparent);
+	<-noteself =>
+		;
+	}
+	note(noteself);
+	if(kill){
+		pid := sys->pctl(0, nil);
+		fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+		if(fd == nil)
+			fd = sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+		sys->fprint(fd, "killgrp");
+	}
+}
+
+note(c: chan of int)
+{
+	if(c != nil){
+		alt {
+		c <-= 1 =>
+			;
+		* =>
+			;
+		}
+	}
+}
+
+deadman(nil: ref Sys->FD)
+{
+	<-chan of int;
+}
+
+watchproc(fd: ref Sys->FD, stopc: chan of int)
+{
+	sys->read(fd, array[1] of byte, 1);
+	note(stopc);
+}
--- /dev/null
+++ b/appl/alphabet/rexecsrv.sh
@@ -1,0 +1,9 @@
+#!/dis/sh
+if{! ~ $#* 2}{
+	echo usage rexecsrv net!addr decls >[1=2]
+	raise usage
+}
+(addr decls) := $*
+/appl/alphabet/mkendpoint.sh $addr!2222
+alphabet/rexecsrv /n/cd $decls
+listen -v $addr!2223 {export /n/cd&}
--- /dev/null
+++ b/appl/alphabet/setup
@@ -1,0 +1,63 @@
+/appl/alphabet/rexecsrv.sh tcp!rogero {typeset /fs; import /fs/unbundle /fs/entries /fs/print}
+
+####################
+addr=tcp!rogero!1234
+run /appl/alphabet/declare.sh
+/appl/alphabet/mkendpoint.sh $addr
+echo ${rewrite {
+	/echo hello |
+		/grid/remote |
+		/grid/rexec tcp!rogero!1235 "{(/fd);/filter $1 "{wc}}
+	}
+}
+- {
+	/echo hello |
+		/grid/remote |
+		/grid/rexec tcp!rogero!1235 "{(/fd);/filter $1 "{wc}} |
+		/grid/local
+	}
+# - {remote /n/local/lib/words | farm rogero!1235 "{tr -d e} } | /grid/local}
+######################
+
+/appl/alphabet/mkendpoint.sh tcp!rogero!9998
+load alphabet
+run /appl/alphabet/declare.sh
+- {
+	/fs/walk /tmp |
+	/fs/bundle |
+	/grid/remote |
+	/grid/rexec tcp!rogero!1235 "{
+		(/fd)
+		/fs/unbundle $1 |
+		/fs/entries |
+		/fs/print
+	}
+}
+
+- {
+	/fs/walk /tmp |
+	/fs/bundle |
+	/grid/remote |
+	/grid/local |
+	/fs/unbundle |
+	/fs/print
+}
+
+###############
+
+the below script generates:
+
+alphabet: 2. bundle: write error: i/o on hungup channel
+and a much truncated file.
+
+-{
+	/fs/walk /tmp |
+	/fs/bundle |
+	/grid/remote |
+	/grid/rexec tcp!127.1!1235 "{
+		(/fd)
+		/fs/unbundle $1 |
+		/fs/filter -d {/fs/match '*.b'} |
+		/fs/bundle
+	} | /create xx
+}
--- /dev/null
+++ b/appl/alphabet/typesets/abc.b
@@ -1,0 +1,180 @@
+# warning: autogenerated code; don't bother to change this, change mktypeset.b or abc.b instead
+implement Abc;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet.m";
+include "abc.m";
+mkabc(a: Alphabet): ref Value.VA
+{
+	r := chan[1] of int;
+	r <-= 1;
+	return ref Value.VA((r, a));
+}
+
+valuec := array[] of {
+	tagof(Value.Vm) => 'm',
+	tagof(Value.Vt) => 't',
+	tagof(Value.VA) => 'A',
+	tagof(Value.Vw) => 'w',
+	tagof(Value.Vc) => 'c',
+	tagof(Value.Vr) => 'r',
+	tagof(Value.Vf) => 'f',
+	tagof(Value.Vs) => 's',
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+Value.type2s(c: int): string
+{
+	case c {
+	'm' =>
+		return "vmods";
+	't' =>
+		return "vtypes";
+	'A' =>
+		return "abc";
+	'w' =>
+		return "wfd";
+	'c' =>
+		return "cmd";
+	'r' =>
+		return "status";
+	'f' =>
+		return "fd";
+	's' =>
+		return "string";
+	* =>
+		return sys->sprint("unknowntype('%c')", c);
+	}
+}
+
+typeerror(tc: int, v: ref Value): string
+{
+	sys->fprint(sys->fildes(2), "fs: bad type conversion, expected %s, was actually %s\n", Value.type2s(tc), Value.type2s(valuec[tagof v]));
+	return "type conversion error";
+}
+
+Value.m(v: self ref Value): ref Value.Vm
+{
+	pick xv := v {Vm => return xv;}
+	raise typeerror('m', v);
+}
+
+Value.t(v: self ref Value): ref Value.Vt
+{
+	pick xv := v {Vt => return xv;}
+	raise typeerror('t', v);
+}
+
+Value.A(v: self ref Value): ref Value.VA
+{
+	pick xv := v {VA => return xv;}
+	raise typeerror('A', v);
+}
+
+Value.w(v: self ref Value): ref Value.Vw
+{
+	pick xv := v {Vw => return xv;}
+	raise typeerror('w', v);
+}
+
+Value.c(v: self ref Value): ref Value.Vc
+{
+	pick xv := v {Vc => return xv;}
+	raise typeerror('c', v);
+}
+
+Value.r(v: self ref Value): ref Value.Vr
+{
+	pick xv := v {Vr => return xv;}
+	raise typeerror('r', v);
+}
+
+Value.f(v: self ref Value): ref Value.Vf
+{
+	pick xv := v {Vf => return xv;}
+	raise typeerror('f', v);
+}
+
+Value.s(v: self ref Value): ref Value.Vs
+{
+	pick xv := v {Vs => return xv;}
+	raise typeerror('s', v);
+}
+
+Value.typec(v: self ref Value): int
+{
+	return valuec[tagof v];
+}
+
+Value.dup(xv: self ref Value): ref Value
+{
+	if(xv == nil)
+		return nil;
+	pick v := xv {
+	Vm =>
+		v = nil;
+		xv = v;
+	Vt =>
+		v = nil;
+		xv = v;
+	VA =>
+		a := v.A().i;
+		a.refcount <-= <-a.refcount + 1;
+		xv = v;
+	Vw =>
+		v = nil;
+		xv = v;
+	Vr =>
+		v = nil;
+		xv = v;
+	Vf =>
+		v = nil;
+		xv = v;
+	}
+	return xv;
+}
+
+Value.free(xv: self ref Value, used: int)
+{
+	if(xv == nil)
+		return;
+	pick v := xv {
+	Vm =>
+		if(!used){
+			v.i.abc.free(0);
+		}
+	Vt =>
+		if(!used){
+			v.i.abc.free(0);
+		}
+	VA =>
+		r := v.i.refcount <-= <-v.i.refcount - 1;
+		if(r == 0){
+			v.i.alphabet->quit();
+			v.i.alphabet = nil;
+			v.i.refcount = nil;
+		}
+	Vw =>
+		if(!used){
+			<-v.i;
+			v.i <-= nil;
+		}
+	Vr =>
+		if(!used){
+			v.i <-= "stop";
+		}
+	Vf =>
+		if(!used){
+			<-v.i;
+			v.i <-= nil;
+		}
+	}
+}
+
--- /dev/null
+++ b/appl/alphabet/typesets/abctypes.b
@@ -1,0 +1,229 @@
+# warning: autogenerated code; don't bother to change this, change mktypeset.b or abc.b instead
+implement Abctypes;
+include "sys.m";
+	sys: Sys;
+include "alphabet/reports.m";
+include "draw.m";
+include "sh.m";
+include "alphabet.m";
+	extvalues: Extvalues;
+	Values: import extvalues;
+	proxymod: Proxy;
+	Typescmd, Modulecmd: import Proxy;
+include "abc.m";
+	abc: Abc;
+	Value: import abc;
+include "abctypes.m";
+
+Pcontext: adt {
+	cvt: ref Abccvt;
+	ctxt: ref Context;
+
+	loadtypes: fn(ctxt: self ref Pcontext, name: string): (chan of ref Proxy->Typescmd[ref Value], string);
+	type2s: fn(ctxt: self ref Pcontext, tc: int): string;
+	alphabet: fn(ctxt: self ref Pcontext): string;
+	modules: fn(ctxt: self ref Pcontext, r: chan of string);
+	find: fn(ctxt: self ref Pcontext, s: string): (ref Module, string);
+	getcvt: fn(ctxt: self ref Pcontext): ref Abccvt;
+};
+
+proxy(): chan of ref Typescmd[ref Alphabet->Value]
+{
+	return proxy0().t0;
+}
+
+proxy0(): (
+		chan of ref Typescmd[ref Alphabet->Value],
+		chan of (string, chan of ref Typescmd[ref Abc->Value]),
+		ref Abccvt
+	)
+{
+	sys = load Sys Sys->PATH;
+	extvalues = checkload(load Extvalues Extvalues->PATH, Extvalues->PATH);
+	proxymod = checkload(load Proxy Proxy->PATH, Proxy->PATH);
+	abc = checkload(load Abc Abc->PATH, Abc->PATH);
+	abc->init();
+	cvt := ref Abccvt(Values[ref Value].new());
+	(t, newts) := proxymod->proxy(ref Pcontext(cvt, Context.new()));
+	return (t, newts, cvt);
+}
+
+include "readdir.m";
+Context: adt {
+	modules: fn(ctxt: self ref Context, r: chan of string);
+	loadtypes: fn(ctxt: self ref Context, name: string)
+		: (chan of ref Proxy->Typescmd[ref Value], string);
+	find: fn(ctxt: self ref Context, s: string): (ref Module, string);
+	new:	fn(): ref Context;
+};
+Module: adt {
+	m: Abcmodule;
+	run: fn(m: self ref Module, ctxt: ref Draw->Context, r: ref Reports->Report,
+		errorc: chan of string, opts: list of (int, list of ref Value),
+		args: list of ref Value): ref Value;
+	typesig: fn(m: self ref Module): string;
+	quit: fn(m: self ref Module);
+};
+Context.new(): ref Context
+{
+	return nil;
+}
+Context.loadtypes(nil: self ref Context, name: string): (chan of ref Typescmd[ref Value], string)
+{
+	p := "/dis/alphabet/abc/"+name+"types.dis";
+	types := load Abcsubtypes p;
+	if(types == nil)
+		return (nil, sys->sprint("cannot load %q: %r", p));
+	return (types->proxy(), nil);
+}
+Context.modules(nil: self ref Context, r: chan of string)
+{
+	if((readdir := load Readdir Readdir->PATH) != nil){
+		(a, nil) := readdir->init("/dis/alphabet/abc", Readdir->NAME|Readdir->COMPACT);
+		for(i := 0; i < len a; i++){
+			m := a[i].name;
+			if((a[i].mode & Sys->DMDIR) == 0 && len m > 4 && m[len m - 4:] == ".dis")
+				r <-= m[0:len m - 4];
+		}
+	}
+	r <-= nil;
+}
+Context.find(nil: self ref Context, s: string): (ref Module, string)
+{
+	p := "/dis/alphabet/abc/"+s+".dis";
+	m := load Abcmodule p;
+	if(m == nil)
+		return (nil, sys->sprint("cannot load %q: %r", p));
+	{
+		m->init();
+	} exception e {
+	"fail:*" =>
+		return (nil, "init failed: " + e[5:]);
+	}
+	return (ref Module(m), nil);
+}
+Module.run(m: self ref Module, nil: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+		opts: list of (int, list of ref Value), args: list of ref Value): ref Value
+{
+	return  m.m->run(errorc, r, opts, args);
+}
+Module.typesig(m: self ref Module): string
+{
+	return m.m->types();
+}
+Module.quit(nil: self ref Module)
+{
+}
+Pcontext.type2s(nil: self ref Pcontext, tc: int): string
+{
+	return Value.type2s(tc);
+}
+
+Pcontext.alphabet(nil: self ref Pcontext): string
+{
+	return "mtAwcrfs";
+}
+
+Pcontext.getcvt(ctxt: self ref Pcontext): ref Abccvt
+{
+	return ctxt.cvt;
+}
+
+Pcontext.find(ctxt: self ref Pcontext, s: string): (ref Module, string)
+{
+	return ctxt.ctxt.find(s);
+}
+
+Pcontext.modules(ctxt: self ref Pcontext, r: chan of string)
+{
+	ctxt.ctxt.modules(r);
+}
+
+Pcontext.loadtypes(ctxt: self ref Pcontext, name: string): (chan of ref Typescmd[ref Value], string)
+{
+	return ctxt.ctxt.loadtypes(name);
+}
+
+Abccvt.int2ext(cvt: self ref Abccvt, gv: ref Value): ref Alphabet->Value
+{
+	if(gv == nil)
+		return nil;
+	pick v := gv {
+	Vw =>
+		return ref (Alphabet->Value).Vw(v.i);
+	Vf =>
+		return ref (Alphabet->Value).Vf(v.i);
+	Vr =>
+		return ref (Alphabet->Value).Vr(v.i);
+	Vs =>
+		return ref (Alphabet->Value).Vs(v.i);
+	Vc =>
+		return ref (Alphabet->Value).Vc(v.i);
+	* =>
+		id := cvt.values.add(gv);
+		return ref (Alphabet->Value).Vz((gv.typec(), id));
+	}
+}
+
+Abccvt.ext2int(cvt: self ref Abccvt, ev: ref Alphabet->Value): ref Value
+{
+	if(ev == nil)
+		return nil;
+	pick v := ev {
+	Vd =>
+		return nil;		# can't happen
+	Vw =>
+		return ref Value.Vw(v.i);
+	Vf =>
+		return ref Value.Vf(v.i);
+	Vr =>
+		return ref Value.Vr(v.i);
+	Vs =>
+		return ref Value.Vs(v.i);
+	Vc =>
+		return ref Value.Vc(v.i);
+	Vz =>
+		x := cvt.values.v[v.i.id].t1;
+		if(x == nil){
+			sys->print("abctypes: bad id %d, type %c\n", v.i.id, v.i.typec);
+			return nil;
+		}
+		return x;
+	}
+}
+
+Abccvt.free(cvt: self ref Abccvt, gv: ref Alphabet->Value, used: int)
+{
+	pick v := gv {
+	Vz =>
+		id := v.i.id;
+		cvt.values.v[id].t1.free(used);
+		cvt.values.del(id);
+	}
+}
+
+Abccvt.dup(cvt: self ref Abccvt, gv: ref Alphabet->Value): ref Alphabet->Value
+{
+	pick ev := gv {
+	Vz =>
+		id := ev.i.id;
+		v := cvt.values.v[id].t1;
+		nv := v.dup();
+		if(nv == nil)
+			return nil;
+		if(nv != v)
+			return ref (Alphabet->Value).Vz((ev.i.typec, cvt.values.add(nv)));
+		cvt.values.inc(id);
+		return ev;
+	* =>
+		return nil;
+	}
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	sys->fprint(sys->fildes(2), "abctypes: cannot load %s: %r\n", path);
+	raise "fail:bad module";
+}
--- /dev/null
+++ b/appl/alphabet/typesets/fs.b
@@ -1,0 +1,226 @@
+# warning: autogenerated code; don't bother to change this, change mktypeset.b or fs.b instead
+implement Fs;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "fs.m";
+sendnulldir(c: Fschan): int
+{
+	reply := chan of int;
+	c <-= ((ref Sys->nulldir, nil), reply);
+	if((r := <-reply) == Down){
+		c <-= ((nil, nil), reply);
+		if(<-reply != Quit)
+			return Quit;
+		return Next;
+	}
+	return r;
+}
+# copy the contents (not the entry itself) of a directory from src to dst.
+copy(src, dst: Fschan): int
+{
+	indent := 1;
+	myreply := chan of int;
+	for(;;){
+		(d, reply) := <-src;
+		dst <-= (d, myreply);
+		r := <-myreply;
+		case reply <-= r {
+		Quit =>
+			return Quit;
+		Next =>
+			if(d.dir == nil && d.data == nil)
+				if(--indent == 0)
+					return Next;
+		Skip =>
+			if(--indent == 0)
+				return Next;
+		Down =>
+			if(d.dir != nil || d.data != nil)
+				indent++;
+		}
+	}
+}
+
+valuec := array[] of {
+	tagof(Value.Vr) => 'r',
+	tagof(Value.Vd) => 'd',
+	tagof(Value.Vc) => 'c',
+	tagof(Value.Vf) => 'f',
+	tagof(Value.Vs) => 's',
+	tagof(Value.Vm) => 'm',
+	tagof(Value.Vp) => 'p',
+	tagof(Value.Vt) => 't',
+	tagof(Value.Vx) => 'x',
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+Value.type2s(c: int): string
+{
+	case c {
+	'r' =>
+		return "status";
+	'd' =>
+		return "data";
+	'c' =>
+		return "command";
+	'f' =>
+		return "fd";
+	's' =>
+		return "string";
+	'm' =>
+		return "selector";
+	'p' =>
+		return "gate";
+	't' =>
+		return "entries";
+	'x' =>
+		return "fs";
+	* =>
+		return sys->sprint("unknowntype('%c')", c);
+	}
+}
+
+typeerror(tc: int, v: ref Value): string
+{
+	sys->fprint(sys->fildes(2), "fs: bad type conversion, expected %s, was actually %s\n", Value.type2s(tc), Value.type2s(valuec[tagof v]));
+	return "type conversion error";
+}
+
+Value.r(v: self ref Value): ref Value.Vr
+{
+	pick xv := v {Vr => return xv;}
+	raise typeerror('r', v);
+}
+
+Value.d(v: self ref Value): ref Value.Vd
+{
+	pick xv := v {Vd => return xv;}
+	raise typeerror('d', v);
+}
+
+Value.c(v: self ref Value): ref Value.Vc
+{
+	pick xv := v {Vc => return xv;}
+	raise typeerror('c', v);
+}
+
+Value.f(v: self ref Value): ref Value.Vf
+{
+	pick xv := v {Vf => return xv;}
+	raise typeerror('f', v);
+}
+
+Value.s(v: self ref Value): ref Value.Vs
+{
+	pick xv := v {Vs => return xv;}
+	raise typeerror('s', v);
+}
+
+Value.m(v: self ref Value): ref Value.Vm
+{
+	pick xv := v {Vm => return xv;}
+	raise typeerror('m', v);
+}
+
+Value.p(v: self ref Value): ref Value.Vp
+{
+	pick xv := v {Vp => return xv;}
+	raise typeerror('p', v);
+}
+
+Value.t(v: self ref Value): ref Value.Vt
+{
+	pick xv := v {Vt => return xv;}
+	raise typeerror('t', v);
+}
+
+Value.x(v: self ref Value): ref Value.Vx
+{
+	pick xv := v {Vx => return xv;}
+	raise typeerror('x', v);
+}
+
+Value.typec(v: self ref Value): int
+{
+	return valuec[tagof v];
+}
+
+Value.dup(xv: self ref Value): ref Value
+{
+	if(xv == nil)
+		return nil;
+	pick v := xv {
+	Vr =>
+		v = nil;
+		xv = v;
+	Vd =>
+		v = nil;
+		xv = v;
+	Vf =>
+		v = nil;
+		xv = v;
+	Vm =>
+		v = nil;
+		xv = v;
+	Vp =>
+		v = nil;
+		xv = v;
+	Vt =>
+		v = nil;
+		xv = v;
+	Vx =>
+		v = nil;
+		xv = v;
+	}
+	return xv;
+}
+
+Value.free(xv: self ref Value, used: int)
+{
+	if(xv == nil)
+		return;
+	pick v := xv {
+	Vr =>
+		if(!used){
+			v.i <-= "stop";
+		}
+	Vd =>
+		if(!used){
+			alt{
+			v.i.stop <-= 1 =>
+				;
+			* =>
+				;
+			}
+		}
+	Vf =>
+		if(!used){
+			<-v.i;
+			v.i <-= nil;
+		}
+	Vm =>
+		if(!used){
+			v.i <-= (nil, nil, nil);
+		}
+	Vp =>
+		if(!used){
+			v.i <-= (Nilentry, nil);
+		}
+	Vt =>
+		if(!used){
+			v.i.sync <-= 0;
+		}
+	Vx =>
+		if(!used){
+			(<-v.i).t1 <-= Quit;
+		}
+	}
+}
+
--- /dev/null
+++ b/appl/alphabet/typesets/fstypes.b
@@ -1,0 +1,230 @@
+# warning: autogenerated code; don't bother to change this, change mktypeset.b or fs.b instead
+implement Fstypes;
+include "sys.m";
+	sys: Sys;
+include "alphabet/reports.m";
+include "draw.m";
+include "sh.m";
+include "alphabet.m";
+	extvalues: Extvalues;
+	Values: import extvalues;
+	proxymod: Proxy;
+	Typescmd, Modulecmd: import Proxy;
+include "fs.m";
+	fs: Fs;
+	Value: import fs;
+include "fstypes.m";
+
+Pcontext: adt {
+	cvt: ref Fscvt;
+	ctxt: ref Context;
+
+	loadtypes: fn(ctxt: self ref Pcontext, name: string): (chan of ref Proxy->Typescmd[ref Value], string);
+	type2s: fn(ctxt: self ref Pcontext, tc: int): string;
+	alphabet: fn(ctxt: self ref Pcontext): string;
+	modules: fn(ctxt: self ref Pcontext, r: chan of string);
+	find: fn(ctxt: self ref Pcontext, s: string): (ref Module, string);
+	getcvt: fn(ctxt: self ref Pcontext): ref Fscvt;
+};
+
+proxy(): chan of ref Typescmd[ref Alphabet->Value]
+{
+	return proxy0().t0;
+}
+
+proxy0(): (
+		chan of ref Typescmd[ref Alphabet->Value],
+		chan of (string, chan of ref Typescmd[ref Fs->Value]),
+		ref Fscvt
+	)
+{
+	sys = load Sys Sys->PATH;
+	extvalues = checkload(load Extvalues Extvalues->PATH, Extvalues->PATH);
+	proxymod = checkload(load Proxy Proxy->PATH, Proxy->PATH);
+	fs = checkload(load Fs Fs->PATH, Fs->PATH);
+	fs->init();
+	cvt := ref Fscvt(Values[ref Value].new());
+	(t, newts) := proxymod->proxy(ref Pcontext(cvt, Context.new()));
+	return (t, newts, cvt);
+}
+
+include "readdir.m";
+Context: adt {
+	modules: fn(ctxt: self ref Context, r: chan of string);
+	loadtypes: fn(ctxt: self ref Context, name: string)
+		: (chan of ref Proxy->Typescmd[ref Value], string);
+	find: fn(ctxt: self ref Context, s: string): (ref Module, string);
+	new:	fn(): ref Context;
+};
+Module: adt {
+	m: Fsmodule;
+	run: fn(m: self ref Module, ctxt: ref Draw->Context, r: ref Reports->Report,
+		errorc: chan of string, opts: list of (int, list of ref Value),
+		args: list of ref Value): ref Value;
+	typesig: fn(m: self ref Module): string;
+	quit: fn(m: self ref Module);
+};
+Context.new(): ref Context
+{
+	return nil;
+}
+Context.loadtypes(nil: self ref Context, name: string): (chan of ref Typescmd[ref Value], string)
+{
+	p := "/dis/alphabet/fs/"+name+"types.dis";
+	types := load Fssubtypes p;
+	if(types == nil)
+		return (nil, sys->sprint("cannot load %q: %r", p));
+	return (types->proxy(), nil);
+}
+Context.modules(nil: self ref Context, r: chan of string)
+{
+	if((readdir := load Readdir Readdir->PATH) != nil){
+		(a, nil) := readdir->init("/dis/alphabet/fs", Readdir->NAME|Readdir->COMPACT);
+		for(i := 0; i < len a; i++){
+			m := a[i].name;
+			if((a[i].mode & Sys->DMDIR) == 0 && len m > 4 && m[len m - 4:] == ".dis")
+				r <-= m[0:len m - 4];
+		}
+	}
+	r <-= nil;
+}
+Context.find(nil: self ref Context, s: string): (ref Module, string)
+{
+	p := "/dis/alphabet/fs/"+s+".dis";
+	m := load Fsmodule p;
+	if(m == nil)
+		return (nil, sys->sprint("cannot load %q: %r", p));
+	{
+		m->init();
+	} exception e {
+	"fail:*" =>
+		return (nil, "init failed: " + e[5:]);
+	}
+	return (ref Module(m), nil);
+}
+Module.run(m: self ref Module, ctxt: ref Draw->Context, r: ref Reports->Report, nil: chan of string,
+		opts: list of (int, list of ref Value), args: list of ref Value): ref Value
+{
+	# add errorc
+	return m.m->run(ctxt, r, opts, args);
+}
+Module.typesig(m: self ref Module): string
+{
+	return m.m->types();
+}
+Module.quit(nil: self ref Module)
+{
+}
+Pcontext.type2s(nil: self ref Pcontext, tc: int): string
+{
+	return Value.type2s(tc);
+}
+
+Pcontext.alphabet(nil: self ref Pcontext): string
+{
+	return "rdcfsmptx";
+}
+
+Pcontext.getcvt(ctxt: self ref Pcontext): ref Fscvt
+{
+	return ctxt.cvt;
+}
+
+Pcontext.find(ctxt: self ref Pcontext, s: string): (ref Module, string)
+{
+	return ctxt.ctxt.find(s);
+}
+
+Pcontext.modules(ctxt: self ref Pcontext, r: chan of string)
+{
+	ctxt.ctxt.modules(r);
+}
+
+Pcontext.loadtypes(ctxt: self ref Pcontext, name: string): (chan of ref Typescmd[ref Value], string)
+{
+	return ctxt.ctxt.loadtypes(name);
+}
+
+Fscvt.int2ext(cvt: self ref Fscvt, gv: ref Value): ref Alphabet->Value
+{
+	if(gv == nil)
+		return nil;
+	pick v := gv {
+	Vd =>
+		return ref (Alphabet->Value).Vd(v.i);
+	Vf =>
+		return ref (Alphabet->Value).Vf(v.i);
+	Vr =>
+		return ref (Alphabet->Value).Vr(v.i);
+	Vs =>
+		return ref (Alphabet->Value).Vs(v.i);
+	Vc =>
+		return ref (Alphabet->Value).Vc(v.i);
+	* =>
+		id := cvt.values.add(gv);
+		return ref (Alphabet->Value).Vz((gv.typec(), id));
+	}
+}
+
+Fscvt.ext2int(cvt: self ref Fscvt, ev: ref Alphabet->Value): ref Value
+{
+	if(ev == nil)
+		return nil;
+	pick v := ev {
+	Vd =>
+		return ref Value.Vd(v.i);
+	Vw =>
+		return nil;		# can't happen
+	Vf =>
+		return ref Value.Vf(v.i);
+	Vr =>
+		return ref Value.Vr(v.i);
+	Vs =>
+		return ref Value.Vs(v.i);
+	Vc =>
+		return ref Value.Vc(v.i);
+	Vz =>
+		x := cvt.values.v[v.i.id].t1;
+		if(x == nil){
+			sys->print("fstypes: bad id %d, type %c\n", v.i.id, v.i.typec);
+			return nil;
+		}
+		return x;
+	}
+}
+
+Fscvt.free(cvt: self ref Fscvt, gv: ref Alphabet->Value, used: int)
+{
+	pick v := gv {
+	Vz =>
+		id := v.i.id;
+		cvt.values.v[id].t1.free(used);
+		cvt.values.del(id);
+	}
+}
+
+Fscvt.dup(cvt: self ref Fscvt, gv: ref Alphabet->Value): ref Alphabet->Value
+{
+	pick ev := gv {
+	Vz =>
+		id := ev.i.id;
+		v := cvt.values.v[id].t1;
+		nv := v.dup();
+		if(nv == nil)
+			return nil;
+		if(nv != v)
+			return ref (Alphabet->Value).Vz((ev.i.typec, cvt.values.add(nv)));
+		cvt.values.inc(id);
+		return ev;
+	* =>
+		return nil;
+	}
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	sys->fprint(sys->fildes(2), "fstypes: cannot load %s: %r\n", path);
+	raise "fail:bad module";
+}
--- /dev/null
+++ b/appl/alphabet/typesets/grid.b
@@ -1,0 +1,160 @@
+# warning: autogenerated code; don't bother to change this, change mktypeset.b or grid.b instead
+implement Grid;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "alphabet/reports.m";
+include "alphabet/endpoints.m";
+include "grid.m";
+endpoints: Endpoints;
+
+valuec := array[] of {
+	tagof(Value.Vb) => 'b',
+	tagof(Value.Ve) => 'e',
+	tagof(Value.Vw) => 'w',
+	tagof(Value.Vc) => 'c',
+	tagof(Value.Vr) => 'r',
+	tagof(Value.Vf) => 'f',
+	tagof(Value.Vs) => 's',
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	endpoints = load Endpoints Endpoints->PATH;
+	endpoints->init();
+}
+
+Value.type2s(c: int): string
+{
+	case c {
+	'b' =>
+		return "records";
+	'e' =>
+		return "endpoint";
+	'w' =>
+		return "wfd";
+	'c' =>
+		return "cmd";
+	'r' =>
+		return "status";
+	'f' =>
+		return "fd";
+	's' =>
+		return "string";
+	* =>
+		return sys->sprint("unknowntype('%c')", c);
+	}
+}
+
+typeerror(tc: int, v: ref Value): string
+{
+	sys->fprint(sys->fildes(2), "fs: bad type conversion, expected %s, was actually %s\n", Value.type2s(tc), Value.type2s(valuec[tagof v]));
+	return "type conversion error";
+}
+
+Value.b(v: self ref Value): ref Value.Vb
+{
+	pick xv := v {Vb => return xv;}
+	raise typeerror('b', v);
+}
+
+Value.e(v: self ref Value): ref Value.Ve
+{
+	pick xv := v {Ve => return xv;}
+	raise typeerror('e', v);
+}
+
+Value.w(v: self ref Value): ref Value.Vw
+{
+	pick xv := v {Vw => return xv;}
+	raise typeerror('w', v);
+}
+
+Value.c(v: self ref Value): ref Value.Vc
+{
+	pick xv := v {Vc => return xv;}
+	raise typeerror('c', v);
+}
+
+Value.r(v: self ref Value): ref Value.Vr
+{
+	pick xv := v {Vr => return xv;}
+	raise typeerror('r', v);
+}
+
+Value.f(v: self ref Value): ref Value.Vf
+{
+	pick xv := v {Vf => return xv;}
+	raise typeerror('f', v);
+}
+
+Value.s(v: self ref Value): ref Value.Vs
+{
+	pick xv := v {Vs => return xv;}
+	raise typeerror('s', v);
+}
+
+Value.typec(v: self ref Value): int
+{
+	return valuec[tagof v];
+}
+
+Value.dup(xv: self ref Value): ref Value
+{
+	if(xv == nil)
+		return nil;
+	pick v := xv {
+	Vb =>
+		v = nil;
+		xv = v;
+	Ve =>
+		v = nil;
+		xv = v;
+	Vw =>
+		v = nil;
+		xv = v;
+	Vr =>
+		v = nil;
+		xv = v;
+	Vf =>
+		v = nil;
+		xv = v;
+	}
+	return xv;
+}
+
+Value.free(xv: self ref Value, used: int)
+{
+	if(xv == nil)
+		return;
+	pick v := xv {
+	Vb =>
+		if(!used){
+			<-v.i;
+			v.i <-= nil;
+		}
+	Ve =>
+		if(!used){
+			ep := <-v.i;
+			if(ep.addr != nil)
+				endpoints->open(nil, ep);		# open and discard
+		}
+	Vw =>
+		if(!used){
+			<-v.i;
+			v.i <-= nil;
+		}
+	Vr =>
+		if(!used){
+			v.i <-= "stop";
+		}
+	Vf =>
+		if(!used){
+			<-v.i;
+			v.i <-= nil;
+		}
+	}
+}
+
--- /dev/null
+++ b/appl/alphabet/typesets/gridtypes.b
@@ -1,0 +1,230 @@
+# warning: autogenerated code; don't bother to change this, change mktypeset.b or grid.b instead
+implement Gridtypes;
+include "sys.m";
+	sys: Sys;
+include "alphabet/reports.m";
+include "draw.m";
+include "sh.m";
+include "alphabet.m";
+	extvalues: Extvalues;
+	Values: import extvalues;
+	proxymod: Proxy;
+	Typescmd, Modulecmd: import Proxy;
+include "alphabet/endpoints.m";
+include "grid.m";
+	grid: Grid;
+	Value: import grid;
+include "gridtypes.m";
+
+Pcontext: adt {
+	cvt: ref Gridcvt;
+	ctxt: ref Context;
+
+	loadtypes: fn(ctxt: self ref Pcontext, name: string): (chan of ref Proxy->Typescmd[ref Value], string);
+	type2s: fn(ctxt: self ref Pcontext, tc: int): string;
+	alphabet: fn(ctxt: self ref Pcontext): string;
+	modules: fn(ctxt: self ref Pcontext, r: chan of string);
+	find: fn(ctxt: self ref Pcontext, s: string): (ref Module, string);
+	getcvt: fn(ctxt: self ref Pcontext): ref Gridcvt;
+};
+
+proxy(): chan of ref Typescmd[ref Alphabet->Value]
+{
+	return proxy0().t0;
+}
+
+proxy0(): (
+		chan of ref Typescmd[ref Alphabet->Value],
+		chan of (string, chan of ref Typescmd[ref Grid->Value]),
+		ref Gridcvt
+	)
+{
+	sys = load Sys Sys->PATH;
+	extvalues = checkload(load Extvalues Extvalues->PATH, Extvalues->PATH);
+	proxymod = checkload(load Proxy Proxy->PATH, Proxy->PATH);
+	grid = checkload(load Grid Grid->PATH, Grid->PATH);
+	grid->init();
+	cvt := ref Gridcvt(Values[ref Value].new());
+	(t, newts) := proxymod->proxy(ref Pcontext(cvt, Context.new()));
+	return (t, newts, cvt);
+}
+
+include "readdir.m";
+Context: adt {
+	modules: fn(ctxt: self ref Context, r: chan of string);
+	loadtypes: fn(ctxt: self ref Context, name: string)
+		: (chan of ref Proxy->Typescmd[ref Value], string);
+	find: fn(ctxt: self ref Context, s: string): (ref Module, string);
+	new:	fn(): ref Context;
+};
+Module: adt {
+	m: Gridmodule;
+	run: fn(m: self ref Module, ctxt: ref Draw->Context, r: ref Reports->Report,
+		errorc: chan of string, opts: list of (int, list of ref Value),
+		args: list of ref Value): ref Value;
+	typesig: fn(m: self ref Module): string;
+	quit: fn(m: self ref Module);
+};
+Context.new(): ref Context
+{
+	return nil;
+}
+Context.loadtypes(nil: self ref Context, name: string): (chan of ref Typescmd[ref Value], string)
+{
+	p := "/dis/alphabet/grid/"+name+"types.dis";
+	types := load Gridsubtypes p;
+	if(types == nil)
+		return (nil, sys->sprint("cannot load %q: %r", p));
+	return (types->proxy(), nil);
+}
+Context.modules(nil: self ref Context, r: chan of string)
+{
+	if((readdir := load Readdir Readdir->PATH) != nil){
+		(a, nil) := readdir->init("/dis/alphabet/grid", Readdir->NAME|Readdir->COMPACT);
+		for(i := 0; i < len a; i++){
+			m := a[i].name;
+			if((a[i].mode & Sys->DMDIR) == 0 && len m > 4 && m[len m - 4:] == ".dis")
+				r <-= m[0:len m - 4];
+		}
+	}
+	r <-= nil;
+}
+Context.find(nil: self ref Context, s: string): (ref Module, string)
+{
+	p := "/dis/alphabet/grid/"+s+".dis";
+	m := load Gridmodule p;
+	if(m == nil)
+		return (nil, sys->sprint("cannot load %q: %r", p));
+	{
+		m->init();
+	} exception e {
+	"fail:*" =>
+		return (nil, "init failed: " + e[5:]);
+	}
+	return (ref Module(m), nil);
+}
+Module.run(m: self ref Module, nil: ref Draw->Context, r: ref Reports->Report, errorc: chan of string,
+		opts: list of (int, list of ref Value), args: list of ref Value): ref Value
+{
+	return  m.m->run(errorc, r, opts, args);
+}
+Module.typesig(m: self ref Module): string
+{
+	return m.m->types();
+}
+Module.quit(nil: self ref Module)
+{
+}
+Pcontext.type2s(nil: self ref Pcontext, tc: int): string
+{
+	return Value.type2s(tc);
+}
+
+Pcontext.alphabet(nil: self ref Pcontext): string
+{
+	return "bewcrfs";
+}
+
+Pcontext.getcvt(ctxt: self ref Pcontext): ref Gridcvt
+{
+	return ctxt.cvt;
+}
+
+Pcontext.find(ctxt: self ref Pcontext, s: string): (ref Module, string)
+{
+	return ctxt.ctxt.find(s);
+}
+
+Pcontext.modules(ctxt: self ref Pcontext, r: chan of string)
+{
+	ctxt.ctxt.modules(r);
+}
+
+Pcontext.loadtypes(ctxt: self ref Pcontext, name: string): (chan of ref Typescmd[ref Value], string)
+{
+	return ctxt.ctxt.loadtypes(name);
+}
+
+Gridcvt.int2ext(cvt: self ref Gridcvt, gv: ref Value): ref Alphabet->Value
+{
+	if(gv == nil)
+		return nil;
+	pick v := gv {
+	Vw =>
+		return ref (Alphabet->Value).Vw(v.i);
+	Vf =>
+		return ref (Alphabet->Value).Vf(v.i);
+	Vr =>
+		return ref (Alphabet->Value).Vr(v.i);
+	Vs =>
+		return ref (Alphabet->Value).Vs(v.i);
+	Vc =>
+		return ref (Alphabet->Value).Vc(v.i);
+	* =>
+		id := cvt.values.add(gv);
+		return ref (Alphabet->Value).Vz((gv.typec(), id));
+	}
+}
+
+Gridcvt.ext2int(cvt: self ref Gridcvt, ev: ref Alphabet->Value): ref Value
+{
+	if(ev == nil)
+		return nil;
+	pick v := ev {
+	Vd =>
+		return nil;		# can't happen
+	Vw =>
+		return ref Value.Vw(v.i);
+	Vf =>
+		return ref Value.Vf(v.i);
+	Vr =>
+		return ref Value.Vr(v.i);
+	Vs =>
+		return ref Value.Vs(v.i);
+	Vc =>
+		return ref Value.Vc(v.i);
+	Vz =>
+		x := cvt.values.v[v.i.id].t1;
+		if(x == nil){
+			sys->print("gridtypes: bad id %d, type %c\n", v.i.id, v.i.typec);
+			return nil;
+		}
+		return x;
+	}
+}
+
+Gridcvt.free(cvt: self ref Gridcvt, gv: ref Alphabet->Value, used: int)
+{
+	pick v := gv {
+	Vz =>
+		id := v.i.id;
+		cvt.values.v[id].t1.free(used);
+		cvt.values.del(id);
+	}
+}
+
+Gridcvt.dup(cvt: self ref Gridcvt, gv: ref Alphabet->Value): ref Alphabet->Value
+{
+	pick ev := gv {
+	Vz =>
+		id := ev.i.id;
+		v := cvt.values.v[id].t1;
+		nv := v.dup();
+		if(nv == nil)
+			return nil;
+		if(nv != v)
+			return ref (Alphabet->Value).Vz((ev.i.typec, cvt.values.add(nv)));
+		cvt.values.inc(id);
+		return ev;
+	* =>
+		return nil;
+	}
+}
+
+checkload[T](m: T, path: string): T
+{
+	if(m != nil)
+		return m;
+	sys->fprint(sys->fildes(2), "gridtypes: cannot load %s: %r\n", path);
+	raise "fail:bad module";
+}
--- /dev/null
+++ b/appl/alphabet/typesets/mkfile
@@ -1,0 +1,34 @@
+<../../../mkconfig
+
+TARG=\
+	mktypeset.dis\
+	abc.dis\
+	abctypes.dis\
+	fs.dis\
+	fstypes.dis\
+	grid.dis\
+	gridtypes.dis\
+
+SYSMODULES=\
+	alphabet.m\
+	alphabet/endpoints.m\
+	alphabet/reports.m\
+	draw.m\
+	readdir.m\
+	sh.m\
+	sys.m\
+	alphabet/abc.m\
+	alphabet/abctypes.m\
+	alphabet/fs.m\
+	alphabet/fstypes.m\
+	alphabet/grid.m\
+	alphabet/gridtypes.m\
+
+DISBIN=$ROOT/dis/alphabet
+
+<$ROOT/mkfiles/mkdis
+
+LIMBOFLAGS=$LIMBOFLAGS -i -F
+
+#%.b %types.b %.m %types.m: %.typeset
+#	mktypeset $stem.typeset
--- /dev/null
+++ b/appl/charon/build.b
@@ -1,0 +1,2864 @@
+implement Build;
+
+include "common.m";
+
+# local copies from CU
+sys: Sys;
+CU: CharonUtils;
+	ByteSource, CImage, ImageCache, color, Nameval: import CU;
+
+D: Draw;
+	Point, Rect, Image: import D;
+S: String;
+T: StringIntTab;
+C: Ctype;
+LX: Lex;
+	RBRA, Token, TokenSource: import LX;
+U: Url;
+	Parsedurl: import U;
+J: Script;
+
+ctype: array of byte;
+
+whitespace :  con " \t\n\r";
+notwhitespace :  con "^ \t\n\r";
+
+# These tables must be sorted
+align_tab := array[] of { T->StringInt
+	("baseline",	int Abaseline),
+	("bottom",	int Abottom),
+	("center",	int Acenter),
+	("char",	int Achar),
+	("justify",	int Ajustify),
+	("left",	int Aleft),
+	("middle",	int Amiddle),
+	("right",	int Aright),
+	("top",	int Atop),
+};
+
+input_tab := array[] of { T->StringInt
+	("button",		Fbutton),
+	("checkbox",	Fcheckbox),
+	("file",		Ffile),
+	("hidden",		Fhidden),
+	("image",		Fimage),
+	("password",	Fpassword),
+	("radio",		Fradio),
+	("reset",		Freset),
+	("submit",		Fsubmit),
+	("text",		Ftext),
+};
+
+clear_tab := array[] of { T->StringInt
+	("all",	IFcleft|IFcright),
+	("left",	IFcleft),
+	("right",	IFcright),
+};
+
+fscroll_tab := array[] of { T->StringInt
+	("auto",	FRhscrollauto|FRvscrollauto),
+	("no",	FRnoscroll),
+	("yes",	FRhscroll|FRvscroll),
+};
+
+# blockbrk[tag] is break info for a block level element, or one
+# of a few others that get the same treatment re ending open paragraphs
+# and requiring a line break / vertical space before them.
+# If we want a line of space before the given element, SPBefore is OR'd in.
+# If we want a line of space after the given element, SPAfter is OR'd in.
+SPBefore: con byte 2;
+SPAfter: con byte 4;
+BL: con byte 1;
+BLBA: con BL|SPBefore|SPAfter;
+blockbrk := array[LX->Numtags] of {
+	LX->Taddress => BLBA, LX->Tblockquote => BLBA, LX->Tcenter => BL,
+	LX->Tdir => BLBA, LX->Tdiv => BL, LX->Tdd => BL, LX->Tdl => BLBA,
+	LX->Tdt => BL, LX->Tform => BLBA,
+	# headings and tables get breaks added manually
+	LX->Th1 => BL, LX->Th2 => BL, LX->Th3 => BL,
+	LX->Th4 => BL, LX->Th5 => BL, LX->Th6 => BL,
+	LX->Thr => BL, LX->Tisindex => BLBA, LX->Tli => BL, LX->Tmenu => BLBA,
+	LX->Tol => BLBA, LX->Tp => BLBA, LX->Tpre => BLBA,
+	LX->Tul => BLBA, LX->Txmp => BLBA,
+	* => byte 0
+};
+
+# attrinfo is information about attributes.
+# The AGEN value means that the attribute is generic (applies to almost all elements)
+AGEN: con byte 1;
+attrinfo := array[LX->Numattrs] of {
+	LX->Aid => AGEN, LX->Aclass => AGEN, LX->Astyle => AGEN, LX->Atitle => AGEN,
+	LX->Aonabort => AGEN, LX->Aonblur => AGEN, LX->Aonchange => AGEN,
+	LX->Aonclick => AGEN, LX->Aondblclick => AGEN, LX->Aonerror => AGEN,
+	LX->Aonfocus => AGEN, LX->Aonkeydown => AGEN, LX->Aonkeypress => AGEN, LX->Aonkeyup => AGEN,
+	LX->Aonload => AGEN, LX->Aonmousedown => AGEN, LX->Aonmousemove => AGEN,
+	LX->Aonmouseout => AGEN, LX->Aonmouseover => AGEN,
+	LX->Aonmouseup => AGEN, LX->Aonreset => AGEN, LX->Aonresize => AGEN, LX->Aonselect => AGEN,
+	LX->Aonsubmit => AGEN, LX->Aonunload => AGEN,
+	* => byte 0
+};
+
+# Some constants
+FRKIDMARGIN: con 6;	# default margin around kid frames
+IMGHSPACE: con 0;		# default hspace for images (0 matches IE, Netscape)
+IMGVSPACE: con 0;		# default vspace for images
+FLTIMGHSPACE: con 2;	# default hspace for float images
+TABSP: con 2;			# default cellspacing for tables
+TABPAD: con 2;		# default cell padding for tables
+LISTTAB: con 1;		# number of tabs to indent lists
+BQTAB: con 1;			# number of tabs to indent blockquotes
+HRSZ: con 2;			# thickness of horizontal rules
+SUBOFF: con 4;			# vertical offset for subscripts
+SUPOFF: con 6;			# vertical offset for superscripts
+NBSP: con ' ';			# non-breaking space character
+
+dbg := 0;
+warn := 0;
+doscripts := 0;
+
+utf8 : Btos;
+latin1 : Btos;
+
+init(cu: CharonUtils)
+{
+	CU = cu;
+	sys = load Sys Sys->PATH;
+	D = load Draw Draw->PATH; 
+	S = load String String->PATH;;
+	T = load StringIntTab StringIntTab->PATH;
+	U = load Url Url->PATH;
+	if (U != nil)
+		U->init();
+	C = cu->C;
+	J = cu->J;
+	LX = cu->LX;
+	ctype = C->ctype;
+	utf8 = CU->getconv("utf8");
+	latin1 = CU->getconv("latin1");
+	if (utf8 == nil || latin1 == nil) {
+		sys->print("cannot load utf8 or latin1 charset converter\n");
+		raise "EXinternal:build init";
+	}
+	dbg = int (CU->config).dbg['h'];
+	warn = (int (CU->config).dbg['w']) || dbg;
+	doscripts = (CU->config).doscripts && J != nil;
+}
+
+# Assume f has been reset, and then had any values from HTTP headers
+# filled in (e.g., base, chset).
+ItemSource.new(bs: ref ByteSource, f: ref Layout->Frame, mtype: int) : ref ItemSource
+{
+	di := f.doc;
+# sys->print("chset = %s\n", di.chset);
+	chset := CU->getconv(di.chset);
+	if (chset == nil)
+		chset = latin1;
+	ts := TokenSource.new(bs, chset, mtype);
+	psstk := list of { Pstate.new() };
+	if(mtype != CU->TextHtml) {
+		ps := hd psstk;
+		ps.curstate &= ~IFwrap;
+		ps.literal = 1;
+		pushfontstyle(ps, FntT);
+	}
+	return ref ItemSource(ts, mtype, di, f, psstk, 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, nil);
+}
+
+ItemSource.getitems(is: self ref ItemSource) : ref Item
+{
+	psstk := is.psstk;
+	ps := hd psstk;		# ps is always same as hd psstk
+	curtab: ref Table = nil;	# curtab is always same as hd is.tabstk
+	if(is.tabstk != nil)
+		curtab = hd is.tabstk;
+	toks := is.toks;
+	is.toks = nil;
+	tokslen := len toks;
+	toki := 0;
+	di := is.doc;
+TokLoop:
+	for(;; toki++) {
+		if(toki >= tokslen) {
+			outerps := lastps(psstk);
+			if(outerps.items.next != nil)
+				break;
+			toks = is.ts.gettoks();
+			tokslen = len toks;
+			if(dbg)
+				sys->print("build: got %d tokens from token source\n", tokslen);
+			if(tokslen == 0)
+				break;
+			toki = 0;
+		}
+		tok := toks[toki];
+		if(dbg > 1)
+			sys->print("build: curstate %ux, token %s\n", ps.curstate, tok.tostring());
+		tag := tok.tag;
+		brk := byte 0;
+		brksp := 0;
+		if(tag < LX->Numtags) {
+			brk = blockbrk[tag];
+			if((brk&SPBefore) != byte 0)
+				brksp = 1;
+		}
+		else if(tag < LX->Numtags+RBRA) {
+			brk = blockbrk[tag-RBRA];
+			if((brk&SPAfter) != byte 0)
+				brksp = 1;
+		}
+		if(brk != byte 0) {
+			addbrk(ps, brksp, 0);
+			if(ps.inpar) {
+				popjust(ps);
+				ps.inpar = 0;
+			}
+		}
+		# check common case first (Data), then case statement on tag
+		if(tag == LX->Data) {
+			# Lexing didn't pay attention to SGML record boundary rules:
+			# \n after start tag or before end tag to be discarded.
+			# (Lex has already discarded all \r's).
+			# Some pages assume this doesn't happen in <PRE> text,
+			# so we won't do it if literal is true.
+			# BUG: won't discard \n before a start tag that begins
+			# the next bufferful of tokens.
+			s := tok.text;
+			if(!ps.literal) {
+				i := 0;
+				j := len s;
+				if(toki > 0) {
+					pt := toks[toki-1].tag;
+					# IE and Netscape both ignore this rule (contrary to spec)
+					# if previous tag was img
+					if(pt < LX->Numtags && pt != LX->Timg && j>0 && s[0]=='\n')
+						i++;
+				}
+				if(toki < tokslen-1) {
+					nt := toks[toki+1].tag;
+					if(nt >= RBRA && nt < LX->Numtags+RBRA && j>i && s[j-1]=='\n')
+						j--;
+				}
+				if(i>0 || j <len s)
+					s = s[i:j];
+			}
+			if(ps.skipwhite) {
+				s = S->drop(s, whitespace);
+				if(s != "")
+					ps.skipwhite = 0;
+			}
+			if(s != "")
+				addtext(ps, s);
+		}
+		else case tag {
+		# Some abbrevs used in following DTD comments
+		# %text = #PCDATA
+		#		| TT | I | B | U | STRIKE | BIG | SMALL | SUB | SUP
+		#		| EM | STRONG | DFN | CODE | SAMP | KBD | VAR | CITE
+		#		| A | IMG | APPLET | FONT | BASEFONT | BR | SCRIPT | MAP
+		#		| INPUT | SELECT | TEXTAREA
+		# %block = P | UL | OL | DIR | MENU | DL | PRE | DL | DIV | CENTER
+		#		| BLOCKQUOTE | FORM | ISINDEX | HR | TABLE
+		# %flow = (%text | %block)*
+		# %body.content = (%heading | %text | %block | ADDRESS)*
+
+		# <!ELEMENT A - - (%text) -(A)>
+		# Anchors are not supposed to be nested, but you sometimes see
+		# href anchors inside destination anchors.
+		LX->Ta =>
+			if(ps.curanchor != 0) {
+				if(warn)
+					sys->print("warning: nested <A> or missing </A>\n");
+				endanchor(ps, di.text);
+			}
+			name := aval(tok, LX->Aname);
+			href := aurlval(tok, LX->Ahref, nil, di.base);
+			target := astrval(tok, LX->Atarget, di.target);
+			ga := getgenattr(tok);
+			evl : list of Lex->Attr = nil;
+			if(ga != nil) {
+				evl = ga.events;
+				if(evl != nil && doscripts)
+					di.hasscripts = 1;
+			}
+			# ignore rel, rev, and title attrs
+			if(href != nil) {
+				di.anchors = ref Anchor(++is.nanchors, name, href, target, evl, 0) :: di.anchors;
+				ps.curanchor = is.nanchors;
+				ps.curfg = di.link;
+				ps.fgstk = ps.curfg :: ps.fgstk;
+				# underline, too
+				ps.ulstk = ULunder :: ps.ulstk;
+				ps.curul = ULunder;
+			}
+			if(name != nil) {
+				# add a null item to be destination
+				brkstate := ps.curstate & IFbrk;
+				additem(ps, Item.newspacer(ISPnull, 0), tok);
+				ps.curstate |= brkstate;	# not quite right
+				di.dests = ref DestAnchor(++is.nanchors, name, ps.lastit) :: di.dests;
+			}
+
+		LX->Ta+RBRA =>
+			endanchor(ps, di.text);
+
+		# <!ELEMENT APPLET - - (PARAM | %text)* >
+		# We can't do applets, so ignore PARAMS, and let
+		# the %text contents appear for the alternative rep
+		LX->Tapplet or LX->Tapplet+RBRA =>
+			if(warn && tag == LX->Tapplet)
+				sys->print("warning: <APPLET> ignored\n");
+
+		# <!ELEMENT AREA - O EMPTY>
+		LX->Tarea =>
+			map := is.curmap;
+			if(map == nil) {
+				if(warn)
+					sys->print("warning: <AREA> not inside <MAP>\n");
+				continue;
+			}
+			map.areas = Area(S->tolower(astrval(tok, LX->Ashape, "rect")),
+						aurlval(tok, LX->Ahref, nil, di.base),
+						astrval(tok, LX->Atarget, di.target),
+						dimlist(tok, LX->Acoords)) :: map.areas;
+
+		# <!ELEMENT (B|STRONG) - - (%text)*>
+		LX->Tb or LX->Tstrong =>
+			pushfontstyle(ps, FntB);
+
+		LX->Tb+RBRA or LX->Tcite+RBRA
+		  or LX->Tcode+RBRA or LX->Tdfn+RBRA
+		  or LX->Tem+RBRA or LX->Tkbd+RBRA
+		  or LX->Ti+RBRA or LX->Tsamp+RBRA
+		  or LX->Tstrong+RBRA or LX->Ttt+RBRA
+		  or LX->Tvar+RBRA or LX->Taddress+RBRA =>
+			popfontstyle(ps);
+
+		# <!ELEMENT BASE - O EMPTY>
+		LX->Tbase =>
+			di.base = aurlval(tok, LX->Ahref, di.base, di.base);
+			di.target = astrval(tok, LX->Atarget, di.target);
+
+		# <!ELEMENT BASEFONT - O EMPTY>
+		LX->Tbasefont =>
+			ps.adjsize = aintval(tok, LX->Asize, 3) - 3;
+
+		# <!ELEMENT (BIG|SMALL) - - (%text)*>
+		LX->Tbig or LX->Tsmall =>
+			sz := ps.adjsize;
+			if(tag == LX->Tbig)
+				sz += Large;
+			else
+				sz += Small;
+			pushfontsize(ps, sz);
+
+		LX->Tbig+RBRA or  LX->Tsmall+RBRA =>
+			popfontsize(ps);
+
+		# <!ELEMENT BLOCKQUOTE - - %body.content>
+		LX->Tblockquote =>
+			changeindent(ps, BQTAB);
+
+		LX->Tblockquote+RBRA =>
+			changeindent(ps, -BQTAB);
+
+		# <!ELEMENT BODY O O %body.content>
+		LX->Tbody =>
+			ps.skipping = 0;
+			bg := Background(nil, color(aval(tok, LX->Abgcolor), di.background.color));
+			bgurl := aurlval(tok, LX->Abackground, nil, di.base);
+			if(bgurl != nil) {
+				pick ni := Item.newimage(di, bgurl, nil,"", Anone, 0, 0, 0, 0, 0, 0, 1, nil, nil, nil){
+				Iimage =>
+					bg.image = ni;
+				}
+				di.images = bg.image :: di.images;
+			}
+			di.background = ps.curbg = bg;
+			ps.curbg.image = nil;
+			di.text = color(aval(tok, LX->Atext), di.text);
+			di.link = color(aval(tok, LX->Alink), di.link);
+			di.vlink = color(aval(tok, LX->Avlink), di.vlink);
+			di.alink = color(aval(tok, LX->Aalink), di.alink);
+			if(doscripts) {
+				ga := getgenattr(tok);
+				if(ga != nil && ga.events != nil) {
+					di.events = ga.events;
+					di.hasscripts = 1;
+				}
+			}
+			if(di.text != ps.curfg) {
+				ps.curfg = di.text;
+				ps.fgstk = nil;
+			}
+
+		LX->Tbody+RBRA =>
+			# HTML spec says ignore things after </body>,
+			# but IE and Netscape don't
+			# ps.skipping = 1;
+			;
+
+		# <!ELEMENT BR - O EMPTY>
+		LX->Tbr =>
+			addlinebrk(ps, atabval(tok, LX->Aclear, clear_tab, 0));
+
+		# <!ELEMENT CAPTION - - (%text;)*>
+		LX->Tcaption =>
+			if(curtab == nil) {
+				if(warn)
+					sys->print("warning: <CAPTION> outside <TABLE>\n");
+				continue;
+			}
+			if(curtab.caption != nil) {
+				if(warn)
+					sys->print("warning: more than one <CAPTION> in <TABLE>\n");
+				continue;
+			}
+			ps = Pstate.new();
+			psstk = ps :: psstk;
+			curtab.caption_place =atabbval(tok, LX->Aalign, align_tab, Atop);
+
+		LX->Tcaption+RBRA =>
+			if(curtab == nil || tl psstk == nil) {
+				if(warn)
+					sys->print("warning: unexpected </CAPTION>\n");
+				continue;
+			}
+			curtab.caption = ps.items.next;
+			psstk = tl psstk;
+			ps = hd psstk;
+
+		LX->Tcenter or LX->Tdiv =>
+			if(tag == LX->Tcenter)
+				al := Acenter;
+			else
+				al = atabbval(tok, LX->Aalign, align_tab, ps.curjust);
+			pushjust(ps, al);
+
+		LX->Tcenter+RBRA or LX->Tdiv+RBRA =>
+			popjust(ps);
+
+		# <!ELEMENT DD - O  %flow >
+		LX->Tdd =>
+			if(ps.hangstk == nil) {
+				if(warn)
+					sys->print("warning: <DD> not inside <DL\n");
+				continue;
+			}
+			h := hd ps.hangstk;
+			if(h != 0)
+				changehang(ps, -10*LISTTAB);
+			else
+				addbrk(ps, 0, 0);
+			ps.hangstk = 0 :: ps.hangstk;
+
+		#<!ELEMENT (DIR|MENU) - - (LI)+ -(%block) >
+		#<!ELEMENT (OL|UL) - - (LI)+>
+		LX->Tdir or LX->Tmenu or LX->Tol or LX->Tul =>
+			changeindent(ps, LISTTAB);
+			if(tag == LX->Tol)
+				tydef := LT1;
+			else
+				tydef = LTdisc;
+			start := aintval(tok, LX->Astart, 1);
+			ps.listtypestk = listtyval(tok, tydef) :: ps.listtypestk;
+			ps.listcntstk = start :: ps.listcntstk;
+
+		LX->Tdir+RBRA or LX->Tmenu+RBRA
+		or LX->Tol+RBRA or LX->Tul+RBRA =>
+			if(ps.listtypestk == nil) {
+				if(warn)
+					sys->print("warning: %s ended no list\n", tok.tostring());
+				continue;
+			}
+			addbrk(ps, 0, 0);
+			ps.listtypestk = tl ps.listtypestk;
+			ps.listcntstk = tl ps.listcntstk;
+			changeindent(ps, -LISTTAB);
+
+		# <!ELEMENT DL - - (DT|DD)+ >
+		LX->Tdl =>
+			changeindent(ps, LISTTAB);
+			ps.hangstk = 0 :: ps.hangstk;
+
+		LX->Tdl+RBRA =>
+			if(ps.hangstk == nil) {
+				if(warn)
+					sys->print("warning: unexpected </DL>\n");
+				continue;
+			}
+			changeindent(ps, -LISTTAB);
+			if(hd ps.hangstk != 0)
+				changehang(ps, -10*LISTTAB);
+			ps.hangstk = tl ps.hangstk;
+
+		# <!ELEMENT DT - O (%text)* >
+		LX->Tdt =>
+			if(ps.hangstk == nil) {
+				if(warn)
+					sys->print("warning: <DT> not inside <DL>\n");
+				continue;
+			}
+			h := hd ps.hangstk;
+			ps.hangstk = tl ps.hangstk;
+			if(h != 0)
+				changehang(ps, -10*LISTTAB);
+			changehang(ps, 10*LISTTAB);
+			ps.hangstk = 1 :: ps.hangstk;
+
+		# <!ELEMENT FONT - - (%text)*>
+		LX->Tfont =>
+			sz := stackhd(ps.fntsizestk, Normal);
+			(szfnd, nsz) := tok.aval(LX->Asize);
+			if(szfnd) {
+				if(S->prefix("+", nsz))
+					sz = Normal + int (nsz[1:]) + ps.adjsize;
+				else if(S->prefix("-", nsz))
+					sz = Normal - int (nsz[1:]) + ps.adjsize;
+				else if(nsz != "")
+					sz = Normal + ( int nsz - 3);
+			}
+			ps.curfg = color(aval(tok, LX->Acolor), ps.curfg);
+			ps.fgstk = ps.curfg :: ps.fgstk;
+			pushfontsize(ps, sz);
+
+		LX->Tfont+RBRA =>
+			if(ps.fgstk == nil) {
+				if(warn)
+					sys->print("warning: unexpected </FONT>\n");
+				continue;
+			}
+			ps.fgstk = tl ps.fgstk;
+			if(ps.fgstk == nil)
+				ps.curfg = di.text;
+			else
+				ps.curfg = hd ps.fgstk;
+			popfontsize(ps);
+
+		# <!ELEMENT FORM - - %body.content -(FORM) >
+		LX->Tform =>
+			if(is.curform != nil) {
+				if(warn)
+					sys->print("warning: <FORM> nested inside another\n");
+				continue;
+			}
+			action := aurlval(tok, LX->Aaction, di.base, di.base);
+			name := astrval(tok, LX->Aname, aval(tok, LX->Aid));
+			target := astrval(tok, LX->Atarget, di.target);
+			smethod := S->tolower(astrval(tok, LX->Amethod, "get"));
+			method := CU->HGet;
+			if(smethod == "post")
+				method = CU->HPost;
+			else if(smethod != "get") {
+				if(warn)
+					sys->print("warning: unknown form method %s\n", smethod);
+			}
+			(ecfnd, enctype) := tok.aval(LX->Aenctype);
+			if(warn && ecfnd && enctype != "application/x-www-form-urlencoded")
+				sys->print("form enctype %s not handled\n", enctype);
+			ga := getgenattr(tok);
+			evl : list of Lex->Attr = nil;
+			if(ga != nil) {
+				evl = ga.events;
+				if(evl != nil && doscripts)
+					di.hasscripts = 1;
+			}
+			frm := Form.new(++is.nforms, name, action, target, method, evl);
+			di.forms = frm :: di.forms;
+			is.curform = frm;
+
+		LX->Tform+RBRA =>
+			if(is.curform == nil) {
+				if(warn)
+					sys->print("warning: unexpected </FORM>\n");
+				continue;
+			}
+			# put fields back in input order
+			fields : list of ref Formfield = nil;
+			for(fl := is.curform.fields; fl != nil; fl = tl fl)
+				fields = hd fl :: fields;
+			is.curform.fields = fields;
+			is.curform.state = FormDone;
+			is.curform = nil;
+
+		# HTML 4
+		# <!ELEMENT FRAME - O EMPTY>
+		LX->Tframe =>
+			if(is.kidstk == nil) {
+				if(warn)
+					sys->print("warning: <FRAME> not in <FRAMESET>\n");
+				continue;
+			}
+			ks := hd is.kidstk;
+			kd := Kidinfo.new(0);
+			kd.src = aurlval(tok, LX->Asrc, nil, di.base);
+			kd.name = aval(tok, LX->Aname);
+			if(kd.name == "")
+				kd.name = "_fr" + string (++is.nframes);
+			kd.marginw = aintval(tok, LX->Amarginwidth, 0);
+			kd.marginh = aintval(tok, LX->Amarginheight, 0);
+			kd.framebd = aintval(tok, LX->Aframeborder, ks.framebd);
+			kd.flags = atabval(tok, LX->Ascrolling, fscroll_tab, kd.flags);
+			norsz := aboolval(tok, LX->Anoresize);
+			if(norsz)
+				kd.flags |= FRnoresize;
+			ks.kidinfos = kd :: ks.kidinfos;
+
+		# HTML 4
+		# <!ELEMENT FRAMESET - - (FRAME|FRAMESET)+>
+		LX->Tframeset =>
+			ks := Kidinfo.new(1);
+			if(is.kidstk == nil)
+				di.kidinfo = ks;
+			else {
+				pks := hd is.kidstk;
+				pks.kidinfos = ks :: pks.kidinfos;
+			}
+			is.kidstk = ks :: is.kidstk;
+			ks.framebd = aintval(tok, LX->Aborder, 1);
+			ks.rows = dimlist(tok, LX->Arows);
+			if(ks.rows == nil)
+				ks.rows = array[] of {Dimen.make(Dpercent,100)};
+			ks.cols = dimlist(tok, LX->Acols);
+			if(ks.cols == nil)
+				ks.cols = array[] of {Dimen.make(Dpercent,100)};
+			if(doscripts) {
+				ga := getgenattr(tok);
+				if(ga != nil && ga.events != nil) {
+					di.events = ga.events;
+					di.hasscripts = 1;
+				}
+			}
+
+		LX->Tframeset+RBRA =>
+			if(is.kidstk == nil) {
+				if(warn)
+					sys->print("warning: unexpected </FRAMESET>\n");
+				continue;
+			}
+			ks := hd is.kidstk;
+			# put kids back in original order
+			# and add blank frames to fill out cells
+			n := (len ks.rows) * (len ks.cols);
+			nblank := n - len ks.kidinfos;
+			while(nblank-- > 0)
+				ks.kidinfos = Kidinfo.new(0) :: ks.kidinfos;
+			kids : list of ref Kidinfo = nil;
+			for(kl := ks.kidinfos; kl != nil; kl = tl kl)
+				kids = hd kl :: kids;
+			ks.kidinfos= kids;
+			is.kidstk = tl is.kidstk;
+			if(is.kidstk == nil) {
+				for(;;) {
+					toks = is.ts.gettoks();
+					if(len toks == 0)
+						break;
+				}
+				tokslen = 0;
+			}
+
+		# <!ELEMENT H1 - - (%text;)*>, etc.
+		LX->Th1 or  LX->Th2 or LX->Th3
+		or LX->Th4 or LX->Th5 or LX->Th6 =>
+			# don't want extra space if this is first addition
+			# to this item list (BUG: problem if first of bufferful)
+			bramt := 1;
+			if(ps.items == ps.lastit)
+				bramt = 0;
+			addbrk(ps, bramt, IFcleft|IFcright);
+			# assume Th2 = Th1+1, etc.
+			sz := Verylarge - (tag - LX->Th1);
+			if(sz < Tiny)
+				sz = Tiny;
+			pushfontsize(ps, sz);
+			sty := stackhd(ps.fntstylestk, FntR);
+			if(tag == LX->Th1)
+				sty = FntB;
+			pushfontstyle(ps, sty);
+			pushjust(ps, atabbval(tok, LX->Aalign, align_tab, ps.curjust));
+			ps.skipwhite = 1;
+
+		LX->Th1+RBRA or LX->Th2+RBRA
+		    or LX->Th3+RBRA or LX->Th4+RBRA
+		    or LX->Th5+RBRA or LX->Th6+RBRA =>
+			addbrk(ps, 1, IFcleft|IFcright);
+			popfontsize(ps);
+			popfontstyle(ps);
+			popjust(ps);
+
+		LX->Thead =>
+			# HTML spec says ignore regular markup in head,
+			# but Netscape and IE don't
+			# ps.skipping = 1;
+			;
+
+		LX->Thead+RBRA =>
+			ps.skipping = 0;
+
+		# <!ELEMENT HR - O EMPTY>
+		LX->Thr =>
+			al := atabbval(tok, LX->Aalign, align_tab, Acenter);
+			sz := aintval(tok, LX->Asize, HRSZ);
+			wd := makedimen(tok, LX->Awidth);
+			if(wd.kind() == Dnone)
+				wd = Dimen.make(Dpercent, 100);
+			nosh := aboolval(tok, LX->Anoshade);
+			additem(ps, Item.newrule(al, sz, nosh, wd), tok);
+			addbrk(ps, 0, 0);
+
+		# <!ELEMENT (I|CITE|DFN|EM|VAR) - - (%text)*>
+		LX->Ti  or LX->Tcite or LX->Tdfn
+		or LX->Tem or LX->Tvar or LX->Taddress =>
+			pushfontstyle(ps, FntI);
+
+		# <!ELEMENT IMG - O EMPTY>
+		LX->Timage or		# common html error supported by other browsers
+		LX->Timg =>
+			tok.tag = LX->Timg;
+			map : ref Map = nil;
+			usemap := aval(tok, LX->Ausemap);
+			oldcuranchor := ps.curanchor;
+			if(usemap != "") {
+				# can't handle non-local maps
+				if(!S->prefix("#", usemap)) {
+					if(warn)
+						sys->print("warning: can't handle non-local map %s\n", usemap);
+				}
+				else {
+					map = getmap(di, usemap[1:]);
+					if(ps.curanchor == 0) {
+						# make an anchor so charon's easy test for whether
+						# there's an action for the item works
+						di.anchors = ref Anchor(++is.nanchors, "", nil, di.target, nil, 0) :: di.anchors;
+						ps.curanchor = is.nanchors;
+					}
+				}
+			}
+			align := atabbval(tok, LX->Aalign, align_tab, Abottom);
+			dfltbd := 0;
+			if(ps.curanchor != 0)
+				dfltbd = 2;
+			src := aurlval(tok, LX->Asrc, nil, di.base);
+			if(src == nil) {
+				if(warn)
+					sys->print("warning: <img> has no src attribute\n");
+				ps.curanchor = oldcuranchor;
+				continue;
+			}
+			img := Item.newimage(di, src,
+				aurlval(tok, LX->Alowsrc, nil, di.base),
+				aval(tok, LX->Aalt),
+				align,
+				aintval(tok, LX->Awidth, 0),
+				aintval(tok, LX->Aheight, 0),
+				aintval(tok, LX->Ahspace, IMGHSPACE),
+				aintval(tok, LX->Avspace, IMGVSPACE),
+				aintval(tok, LX->Aborder, dfltbd),
+				aboolval(tok, LX->Aismap),
+				0, # not a background image
+				map,
+				aval(tok, LX->Aname),
+				getgenattr(tok));
+			if(align == Aleft || align == Aright) {
+				additem(ps, Item.newfloat(img, align), tok);
+				# if no hspace specified, use FLTIMGHSPACE
+				(fnd,nil) := tok.aval(LX->Ahspace);
+				if(!fnd) {
+					pick ii := img {
+					Iimage =>
+						ii.hspace = byte FLTIMGHSPACE;
+					}
+				}
+			} else {
+				ps.skipwhite = 0;
+				additem(ps, img, tok);
+			}
+			if(!ps.skipping)
+				di.images = img :: di.images;
+			ps.curanchor = oldcuranchor;
+
+		# <!ELEMENT INPUT - O EMPTY>
+		LX->Tinput =>
+			if (ps.skipping)
+				continue;
+			ps.skipwhite = 0;
+			if(is.curform ==nil) {
+				if(warn)
+					sys->print("<INPUT> not inside <FORM>\n");
+					continue;
+			}
+			field := Formfield.new(atabval(tok, LX->Atype, input_tab, Ftext),
+					++is.curform.nfields,	# fieldid
+					is.curform,	# form
+					aval(tok, LX->Aname),
+					aval(tok, LX->Avalue),
+					aintval(tok, LX->Asize, 0),
+					aintval(tok, LX->Amaxlength, 1000));
+			if(aboolval(tok, LX->Achecked))
+				field.flags = FFchecked;
+
+			case field.ftype {
+				Ftext or Fpassword or Ffile =>
+					if(field.size == 0)
+						field.size = 20;
+				Fcheckbox =>
+					if(field.name == "") {
+						if(warn)
+							sys->print("warning: checkbox form field missing name\n");
+#						continue;
+					}
+					if(field.value == "")
+						field.value = "on";
+				Fradio =>
+					if(field.name == "" || field.value == "") {
+						if(warn)
+							sys->print("warning: radio form field missing name or value\n");
+#						continue;
+					}
+				Fsubmit =>
+					if(field.value == "")
+						field.value = "Submit";
+					if(field.name == "")
+						field.name = "_no_name_submit_";
+				Fimage =>
+					src := aurlval(tok, LX->Asrc, nil, di.base);
+					if(src == nil) {
+						if(warn)
+							sys->print("warning: image form field missing src\n");
+#						continue;
+					} else {
+						# width and height attrs aren't specified in HTML 3.2,
+						# but some people provide them and they help avoid
+						# a relayout
+						field.image = Item.newimage(di, src,
+							aurlval(tok, LX->Alowsrc, nil, di.base),
+							astrval(tok, LX->Aalt, "Submit"),
+							atabbval(tok, LX->Aalign, align_tab, Abottom),
+							aintval(tok, LX->Awidth, 0),
+							aintval(tok, LX->Aheight, 0),
+							0, 0, 0, 0, 0, nil, field.name, nil);
+						di.images = field.image :: di.images;
+					}
+				Freset =>
+					if(field.value == "")
+						field.value = "Reset";
+				Fbutton =>
+					if(field.value == "")
+						field.value = " ";
+			}
+			is.curform.fields = field :: is.curform.fields;
+			ffit := Item.newformfield(field);
+			additem(ps, ffit, tok);
+			if(ffit.genattr != nil) {
+				field.events = ffit.genattr.events;
+				if(field.events != nil && doscripts)
+					di.hasscripts = 1;
+			}
+
+		# <!ENTITY ISINDEX - O EMPTY>
+		LX->Tisindex =>
+			ps.skipwhite = 0;
+			prompt := astrval(tok, LX->Aprompt, "Index search terms:");
+			target := astrval(tok, LX->Atarget, di.target);
+			additem(ps, textit(ps, prompt), tok);
+			frm := Form.new(++is.nforms, "", di.base, target, CU->HGet, nil);
+			ff := Formfield.new(Ftext, 1, frm, "_ISINDEX_", "", 50, 1000);
+			frm.fields =  ff :: nil;
+			frm.nfields = 1;
+			di.forms = frm :: di.forms;
+			additem(ps, Item.newformfield(ff), tok);
+			addbrk(ps, 1, 0);
+
+		# <!ELEMENT LI - O %flow>
+		LX->Tli =>
+			if(ps.listtypestk == nil) {
+				if(warn)
+					sys->print("<LI> not in list\n");
+				continue;
+			}
+			ty := hd ps.listtypestk;
+			ty2 := listtyval(tok, ty);
+			if(ty != ty2) {
+				ty = ty2;
+				ps.listtypestk = ty2 :: tl ps.listtypestk;
+			}
+			v := aintval(tok, LX->Avalue, hd ps.listcntstk);
+			if(ty == LTdisc || ty == LTsquare || ty == LTcircle)
+				hang := 10*LISTTAB - 3;
+			else
+				hang = 10*LISTTAB - 1;
+			changehang(ps, hang);
+			addtext(ps, listmark(ty, v));
+			ps.listcntstk = (v+1) :: (tl ps.listcntstk);
+			changehang(ps, -hang);
+			ps.skipwhite = 1;
+
+		# <!ELEMENT MAP - - (AREA)+>
+		LX->Tmap =>
+			is.curmap = getmap(di, aval(tok, LX->Aname));
+
+		LX->Tmap+RBRA =>
+			map := is.curmap;
+			if(map == nil) {
+				if(warn)
+					sys->print("warning: unexpected </MAP>\n");
+				continue;
+			}
+			# put areas back in input order
+			areas : list of Area = nil;
+			for(al := map.areas; al != nil; al = tl al)
+				areas = hd al :: areas;
+			map.areas = areas;
+			is.curmap = nil;
+
+		LX->Tmeta =>
+			if(ps.skipping)
+				continue;
+			(fnd, equiv) := tok.aval(LX->Ahttp_equiv);
+			if(fnd) {
+				v := aval(tok, LX->Acontent);
+				case S->tolower(equiv) {
+				"set-cookie" =>
+					if((CU->config).docookies > 0) {
+						url := di.src;
+						CU->setcookie(v, url.host, url.path);
+					}
+				"refresh" =>
+					di.refresh = v;
+				"content-script-type" =>
+					if(v == "javascript" || v == "javascript1.1" || v == "jscript")
+						di.scripttype = CU->TextJavascript;
+					# TODO: other kinds
+					else {
+						if(warn)
+							sys->print("unimplemented script type %s\n", v);
+						di.scripttype = CU->UnknownType;
+					}
+				"content-type" =>
+					(nil, parms) := S->splitl(v, ";");
+					if (parms != nil) {
+						nvs := Nameval.namevals(parms[1:], ';');
+						(got, s) := Nameval.find(nvs, "charset");
+						if (got) {
+# sys->print("HTTP-EQUIV charset: %s\n", s);
+							btos := CU->getconv(s);
+							if (btos != nil)
+								is.ts.setchset(btos);
+							else if (warn)
+								sys->print("cannot set charset %s\n", s);
+						}
+					}
+				}
+			}
+
+		# Nobr is NOT in HMTL 4.0, but it is ubiquitous on the web
+		LX->Tnobr =>
+			ps.skipwhite = 0;
+			ps.curstate &= ~IFwrap;
+
+		LX->Tnobr+RBRA =>
+			ps.curstate |= IFwrap;
+
+		# We do frames, so skip stuff in noframes
+		LX->Tnoframes =>
+			ps.skipping = 1;
+
+		LX->Tnoframes+RBRA =>
+			ps.skipping = 0;
+
+		# We do scripts (if enabled), so skip stuff in noscripts
+		LX->Tnoscript =>
+			if(doscripts)
+				ps.skipping = 1;
+
+		LX->Tnoscript+RBRA =>
+			if(doscripts)
+				ps.skipping = 0;
+
+		# <!ELEMENT OPTION - O (#PCDATA)>
+		LX->Toption =>
+			if(is.curform == nil || is.curform.fields == nil) {
+				if(warn)
+					sys->print("warning: <OPTION> not in <SELECT>\n");
+				continue;
+			}
+			field := hd is.curform.fields;
+			if(field.ftype != Fselect) {
+				if(warn)
+					sys->print("warning: <OPTION> not in <SELECT>\n");
+				continue;
+			}
+			val := aval(tok, LX->Avalue);
+			option := ref Option(aboolval(tok, LX->Aselected),
+						val, "");
+			field.options = option :: field.options;
+			(option.display, toki) = getpcdata(toks, toki);
+			option.display = optiontext(option.display);
+			if(val == "")
+				option.value = option.display;
+
+		# <!ELEMENT P - O (%text)* >
+		LX->Tp =>
+			pushjust(ps, atabbval(tok, LX->Aalign, align_tab, ps.curjust));
+			ps.inpar = 1;
+			ps.skipwhite = 1;
+			
+		LX->Tp+RBRA =>
+			;
+
+		# <!ELEMENT PARAM - O EMPTY>
+		# Do something when we do applets...
+		LX->Tparam =>
+			;
+
+		# <!ELEMENT PRE - - (%text)* -(IMG|BIG|SMALL|SUB|SUP|FONT) >
+		LX->Tpre =>
+			ps.curstate &= ~IFwrap;
+			ps.literal = 1;
+			ps.skipwhite = 0;
+			pushfontstyle(ps, FntT);
+
+		LX->Tpre+RBRA =>
+			ps.curstate |= IFwrap;
+			if(ps.literal) {
+				popfontstyle(ps);
+				ps.literal = 0;
+			}
+
+		# <!ELEMENT SCRIPT - - CDATA>
+		LX->Tscript =>
+			if(!doscripts) {
+				if(warn)
+					sys->print("warning: <SCRIPT> ignored\n");
+				ps.skipping = 1;
+				break;
+			}
+			script := "";
+			scripttoki := toki;
+			(script, toki) = getpcdata(toks, toki);
+
+			# check language version
+			lang :=  astrval(tok, LX->Alanguage, "javascript");
+			lang = S->tolower(lang);
+			lang = trim_white(lang);
+			
+			# should give preference to type
+			supported := 0;
+			for (v := 0; v < len J->versions; v++)
+				if (J->versions[v] == lang) {
+					supported = 1;
+					break;
+			}
+			if (!supported)
+				break;
+
+			di.hasscripts = 1;
+			scriptsrc := aurlval(tok, LX->Asrc, nil, di.base);
+			if(scriptsrc != nil && is.reqdurl == nil) {
+				is.reqdurl = scriptsrc;
+				toki = scripttoki;
+				# is.reqddata will contain script next time round
+				break TokLoop;
+			}
+			if (is.reqddata != nil) {
+				script = CU->stripscript(string is.reqddata);
+				is.reqddata = nil;
+				is.reqdurl = nil;
+			}
+
+			if(script == "")
+				break;
+#sys->print("SCRIPT (ver %s)\n%s\nENDSCRIPT\n", lang, script);
+			(err, replace, nil) := J->evalscript(is.frame, script);
+			if(err != "") {
+				if(warn)
+					sys->print("Javascript error: %s\n", err);
+			} else {
+				# First, worry about possible transfer back of new values
+				if(di.text != ps.curfg) {
+					# The following isn't nearly good enough
+					# (if the fgstk isn't nil, need to replace bottom of stack;
+					# and need to do similar things for all other pstates).
+					# But Netscape 4.0 doesn't do anything at all if change
+					# foreground in a script!
+					if(ps.fgstk == nil)
+						ps.curfg = di.text;
+				}
+				scripttoks := lexstring(replace);
+				ns := len scripttoks;
+				if(ns > 0) {
+					# splice scripttoks into toks, replacing <SCRIPT>...</SCRIPT>
+					if(toki+1 < tokslen && toks[toki+1].tag == LX->Tscript+RBRA)
+						toki++;
+					newtokslen := tokslen - (toki+1-scripttoki) + ns;
+					newtoks := array[newtokslen] of ref Token;
+					newtoks[0:] = toks[0:scripttoki];
+					newtoks[scripttoki:] = scripttoks;
+					if(toki+1 < tokslen)
+						newtoks[scripttoki+ns:] = toks[toki+1:tokslen];
+					toks = newtoks;
+					tokslen = newtokslen;
+					toki = scripttoki-1;
+					scripttoks = nil;
+				}
+			}
+
+		LX->Tscript+RBRA =>
+			ps.skipping = 0;
+
+		# <!ELEMENT SELECT - - (OPTION+)>
+		LX->Tselect =>
+			if(is.curform ==nil) {
+				if(warn)
+					sys->print("<SELECT> not inside <FORM>\n");
+					continue;
+			}
+			field := Formfield.new(Fselect,
+					++is.curform.nfields,	# fieldid
+					is.curform,	# form
+					aval(tok, LX->Aname),
+					"", 			# value
+					aintval(tok, LX->Asize, 1),
+					0);			# maxlength
+			if(aboolval(tok, LX->Amultiple))
+				field.flags = FFmultiple;
+			is.curform.fields = field :: is.curform.fields;
+			ffit := Item.newformfield(field);
+			additem(ps, ffit, tok);
+			if(ffit.genattr != nil) {
+				field.events = ffit.genattr.events;
+				if(field.events != nil && doscripts)
+					di.hasscripts = 1;
+			}
+			# throw away stuff until next tag (should be <OPTION>)
+			(nil, toki) = getpcdata(toks, toki);
+
+		LX->Tselect+RBRA =>
+			if(is.curform == nil || is.curform.fields == nil) {
+				if(warn)
+					sys->print("warning: unexpected </SELECT>\n");
+				continue;
+			}
+			field := hd is.curform.fields;
+			if(field.ftype != Fselect)
+				continue;
+			# put options back in input order
+			opts : list of ref Option = nil;
+			select := 0;
+			for(ol := field.options; ol != nil; ol = tl ol) {
+				o := hd ol;
+				if (o.selected)
+					select = 1;
+				opts = o :: opts;
+			}
+			# Single-choice select fields preselect the first option if none explicitly selected
+			if (!select && !int(field.flags & FFmultiple) && opts != nil)
+				(hd opts).selected = 1;
+			field.options = opts;
+
+		# <!ELEMENT (STRIKE|U) - - (%text)*>
+		LX->Tstrike or LX->Tu =>
+			if(tag == LX->Tstrike)
+				ulty := ULmid;
+			else
+				ulty = ULunder;
+			ps.ulstk = ulty :: ps.ulstk;
+			ps.curul = ulty;
+
+		LX->Tstrike+RBRA or LX->Tu+RBRA =>
+			if(ps.ulstk == nil) {
+				if(warn)
+					sys->print("warning: unexpected %s\n", tok.tostring());
+				continue;
+			}
+			ps.ulstk = tl ps.ulstk;
+			if(ps.ulstk != nil)
+				ps.curul = hd ps.ulstk;
+			else
+				ps.curul = ULnone;
+
+		# <!ELEMENT STYLE - - CDATA>
+		LX->Tstyle =>
+			if(warn)
+				sys->print("warning: unimplemented <STYLE>\n");
+			ps.skipping = 1;
+
+		LX->Tstyle+RBRA =>
+			ps.skipping = 0;
+
+		# <!ELEMENT (SUB|SUP) - - (%text)*>
+		LX->Tsub or LX->Tsup =>
+			if(tag == LX->Tsub)
+				ps.curvoff += SUBOFF;
+			else
+				ps.curvoff -= SUPOFF;
+			ps.voffstk = ps.curvoff :: ps.voffstk;
+			sz := stackhd(ps.fntsizestk, Normal);
+			pushfontsize(ps, sz-1);
+
+		LX->Tsub+RBRA or LX->Tsup+RBRA =>
+			if(ps.voffstk == nil) {
+				if(warn)
+					sys->print("warning: unexpected %s\n", tok.tostring());
+				continue;
+			}
+			ps.voffstk = tl ps.voffstk;
+			if(ps.voffstk != nil)
+				ps.curvoff = hd ps.voffstk;
+			else
+				ps.curvoff = 0;
+			popfontsize(ps);
+
+		# <!ELEMENT TABLE - - (CAPTION?, TR+)>
+		LX->Ttable =>
+			if (ps.skipping)
+				continue;
+			ps.skipwhite = 0;
+			# Handle an html error (seen on deja.com)
+			# ... sometimes see a nested <table> outside of a cell
+			# imitate observed behaviour of IE/Navigator
+			if (curtab != nil && curtab.cells == nil) {
+				curtab.align = makealign(tok);
+				curtab.width = makedimen(tok, LX->Awidth);
+				curtab.border = aflagval(tok, LX->Aborder);
+				curtab.cellspacing = aintval(tok, LX->Acellspacing, TABSP);
+				curtab.cellpadding = aintval(tok, LX->Acellpadding, TABPAD);
+				curtab.background = Background(nil, color(aval(tok, LX->Abgcolor), -1));
+				curtab.tabletok = tok;
+				continue;
+			}
+			tab := Table.new(++is.ntables,	# tableid
+					makealign(tok),	# align
+					makedimen(tok, LX->Awidth),
+					aflagval(tok, LX->Aborder),
+					aintval(tok, LX->Acellspacing, TABSP),
+					aintval(tok, LX->Acellpadding, TABPAD),
+#					Background(nil, color(aval(tok, LX->Abgcolor), ps.curbg.color)),
+					Background(nil, color(aval(tok, LX->Abgcolor), -1)),
+					tok);
+			is.tabstk = tab :: is.tabstk;
+			di.tables = tab :: di.tables;
+			curtab = tab;
+			# HTML spec says:
+			# don't add items to outer state (until </table>)
+			# but IE and Netscape don't do that
+
+		LX->Ttable+RBRA =>
+			if (ps.skipping)
+				continue;
+			if(curtab == nil) {
+				if(warn)
+					sys->print("warning: unexpected </TABLE>\n");
+				continue;
+			}
+			isempty := (curtab.cells == nil);
+			if(isempty) {
+				if(warn)
+					sys->print("warning: <TABLE> has no cells\n");
+			}
+			else {
+				(ps, psstk) = finishcell(curtab, psstk);
+				if(curtab.currows != nil)
+					(hd curtab.currows).flags = byte 0;
+				finish_table(curtab);
+			}
+			ps.skipping = 0;
+			if(!isempty) {
+				tabitem := Item.newtable(curtab);
+				al := int curtab.align.halign;
+				case al {
+				int Aleft or int Aright =>
+					additem(ps, Item.newfloat(tabitem, byte al), tok);
+				* =>
+					if(al == int Acenter)
+						pushjust(ps, Acenter);
+					addbrk(ps, 0, 0);
+					if(ps.inpar) {
+						popjust(ps);
+						ps.inpar = 0;
+					}
+					additem(ps, tabitem, curtab.tabletok);
+					if(al == int Acenter)
+						popjust(ps);
+				}
+			}
+			if(is.tabstk == nil) {
+				if(warn)
+					sys->print("warning: table stack is wrong\n");
+			}
+			else
+				is.tabstk = tl is.tabstk;
+			if(is.tabstk == nil)
+				curtab = nil;
+			else
+				curtab = hd is.tabstk;
+			if(!isempty) {
+				# the code at the beginning to add a break after table
+				# changed the nested ps, not the current one
+				addbrk(ps, 0, 0);
+			}
+
+		# <!ELEMENT (TH|TD) - O %body.content>
+		# Cells for a row are accumulated in reverse order.
+		# We push ps on a stack, and use a new one to accumulate
+		# the contents of the cell.
+		LX->Ttd or LX->Tth =>
+			if (ps.skipping)
+				continue;
+			if(curtab == nil) {
+				if(warn)
+					sys->print("%s outside <TABLE>\n", tok.tostring());
+				continue;
+			}
+			if(ps.inpar) {
+				popjust(ps);
+				ps.inpar = 0;
+			}
+			(ps, psstk) = finishcell(curtab, psstk);
+			tr : ref Tablerow = nil;
+			if(curtab.currows != nil)
+				tr = hd curtab.currows;
+			if(tr == nil || tr.flags == byte 0) {
+				if(warn)
+					sys->print("%s outside row\n", tok.tostring());
+				tr = Tablerow.new(Align(Anone,Anone), curtab.background, TFparsing);
+				curtab.currows = tr :: curtab.currows;
+			}
+			ps = cell_pstate(ps, tag == LX->Tth);
+			psstk = ps :: psstk;
+			flags := TFparsing;
+			width := makedimen(tok, LX->Awidth);
+	
+			# nowrap only applies if no width has been specified
+			if(width.kind() == Dnone && aboolval(tok, LX->Anowrap)) {
+				flags |= TFnowrap;
+				ps.curstate &= ~IFwrap;
+			}
+			if(tag == LX->Tth)
+				flags |= TFisth;
+			bg := Background(nil, color(aval(tok, LX->Abgcolor), tr.background.color));
+			c := Tablecell.new(len curtab.cells + 1, # cell id
+				aintval(tok, LX->Arowspan, 1),
+				aintval(tok, LX->Acolspan, 1),
+				makealign(tok),
+				width,
+				aintval(tok, LX->Aheight, 0),
+				bg,
+				flags);
+
+			bgurl := aurlval(tok, LX->Abackground, nil, di.base);
+			if(bgurl != nil) {
+				pick ni := Item.newimage(di, bgurl, nil,"", Anone, 0, 0, 0, 0, 0, 0, 1, nil, nil, nil){
+				Iimage =>
+					bg.image = ni;
+				}
+				di.images = bg.image :: di.images;
+			}
+			c.background = ps.curbg = bg;
+			ps.curbg.image = nil;
+			if(c.align.halign == Anone) {
+				if(tr.align.halign != Anone)
+					c.align.halign = tr.align.halign;
+				else if(tag == LX->Tth)
+					c.align.halign = Acenter;
+				else
+					c.align.halign = Aleft;
+			}
+			if(c.align.valign == Anone) {
+				if(tr.align.valign != Anone)
+					c.align.valign = tr.align.valign;
+				else
+					c.align.valign = Amiddle;
+			}
+			curtab.cells = c :: curtab.cells;
+			tr.cells = c :: tr.cells;
+
+		LX->Ttd+RBRA or LX->Tth+RBRA =>
+			if (ps.skipping)
+				continue;
+			if(curtab == nil || curtab.cells == nil) {
+				if(warn)
+					sys->print("unexpected %s\n", tok.tostring());
+				continue;
+			}
+			(ps, psstk) = finishcell(curtab, psstk);
+
+		# <!ELEMENT TEXTAREA - - (#PCDATA)>
+		LX->Ttextarea =>
+			if(is.curform ==nil) {
+				if(warn)
+					sys->print("<TEXTAREA> not inside <FORM>\n");
+					continue;
+			}
+			nrows := aintval(tok, LX->Arows, 3);
+			ncols := aintval(tok, LX->Acols, 50);
+			ft := Ftextarea;
+			if (ncols == 0 || nrows == 0)
+				ft = Fhidden;
+			field := Formfield.new(ft,
+					++is.curform.nfields,	# fieldid
+					is.curform,	# form
+					aval(tok, LX->Aname),
+					"",				# value
+					0, 0);				# size, maxlength
+			field.rows = nrows;
+			field.cols = ncols;
+			is.curform.fields = field :: is.curform.fields;
+			(field.value, toki) = getpcdata(toks, toki);
+			if(warn && toki < tokslen-1 && toks[toki+1].tag != LX->Ttextarea+RBRA)
+				sys->print("warning: <TEXTAREA> data ended by %s\n", toks[toki+1].tostring());
+			ffit :=  Item.newformfield(field);
+			additem(ps, ffit, tok);
+			if(ffit.genattr != nil) {
+				field.events = ffit.genattr.events;
+				if(field.events != nil && doscripts)
+					di.hasscripts = 1;
+			}
+
+		# <!ELEMENT TITLE - - (#PCDATA)* -(%head.misc)>
+		LX->Ttitle =>
+			(di.doctitle, toki) = getpcdata(toks, toki);
+			if(warn && toki < tokslen-1 && toks[toki+1].tag != LX->Ttitle+RBRA)
+				sys->print("warning: <TITLE> data ended by %s\n", toks[toki+1].tostring());
+
+		# <!ELEMENT TR - O (TH|TD)+>
+		# rows are accumulated in reverse order in curtab.currows
+		LX->Ttr =>
+			if (ps.skipping)
+				continue;
+			if(curtab == nil) {
+				if(warn)
+					sys->print("warning: <TR> outside <TABLE>\n");
+				continue;
+			}
+			if(ps.inpar) {
+				popjust(ps);
+				ps.inpar = 0;
+			}
+			(ps, psstk) = finishcell(curtab, psstk);
+			if(curtab.currows != nil)
+				(hd curtab.currows).flags = byte 0;
+			tr := Tablerow.new(makealign(tok),
+					Background(nil, color(aval(tok, LX->Abgcolor), curtab.background.color)),
+					TFparsing);
+			curtab.currows = tr :: curtab.currows;
+
+		LX->Ttr+RBRA =>
+			if (ps.skipping)
+				continue;
+			if(curtab == nil || curtab.currows == nil) {
+				if(warn)
+					sys->print("warning: unexpected </TR>\n");
+				continue;
+			}
+			(ps, psstk) = finishcell(curtab, psstk);
+			tr := hd curtab.currows;
+			if(tr.cells == nil) {
+				if(warn)
+					sys->print("warning: empty row\n");
+				curtab.currows = tl curtab.currows;
+			}
+			else
+				tr.flags = byte 0;		# done parsing
+
+		# <!ELEMENT (TT|CODE|KBD|SAMP) - - (%text)*>
+		LX->Ttt or LX->Tcode or LX->Tkbd	or LX->Tsamp =>
+			pushfontstyle(ps, FntT);
+
+		# <!ELEMENT (XMP|LISTING) - - %literal >
+		# additional support exists in LX to ignore character escapes etc.
+		LX->Txmp =>
+			ps.curstate &= ~IFwrap;
+			ps.literal = 1;
+			ps.skipwhite = 0;
+			pushfontstyle(ps, FntT);
+
+		LX->Txmp+RBRA =>
+			ps.curstate |= IFwrap;
+			if(ps.literal) {
+				popfontstyle(ps);
+				ps.literal = 0;
+			}
+
+		# Tags that have empty action
+
+		LX->Tabbr or LX->Tabbr+RBRA
+		or LX->Tacronym or LX->Tacronym+RBRA
+		or LX->Tarea+RBRA
+		or LX->Tbase+RBRA
+		or LX->Tbasefont+RBRA
+		or LX->Tbr+RBRA
+		or LX->Tdd+RBRA
+		or LX->Tdt+RBRA
+		or LX->Tframe+RBRA
+		or LX->Thr+RBRA
+		or LX->Thtml
+		or LX->Thtml+RBRA
+		or LX->Timg+RBRA
+		or LX->Tinput+RBRA
+		or LX->Tisindex+RBRA
+		or LX->Tli+RBRA
+		or LX->Tlink or LX->Tlink+RBRA
+		or LX->Tmeta+RBRA
+		or LX->Toption+RBRA
+		or LX->Tparam+RBRA
+		or LX->Ttextarea+RBRA
+		or LX->Ttitle+RBRA
+		=>
+			;
+
+		# Tags not implemented
+		LX->Tbdo or LX->Tbdo+RBRA
+		or LX->Tbutton or LX->Tbutton+RBRA
+		or LX->Tdel or LX->Tdel+RBRA
+		or LX->Tfieldset or LX->Tfieldset+RBRA
+		or LX->Tiframe or LX->Tiframe+RBRA
+		or LX->Tins or LX->Tins+RBRA
+		or LX->Tlabel or LX->Tlabel+RBRA
+		or LX->Tlegend or LX->Tlegend+RBRA
+		or LX->Tobject or LX->Tobject+RBRA
+		or LX->Toptgroup or LX->Toptgroup+RBRA
+		or LX->Tspan or LX->Tspan+RBRA
+		=>
+			if(warn) {
+				if(tag > RBRA)
+					tag -= RBRA;
+				sys->print("warning: unimplemented HTML tag: %s\n", LX->tagnames[tag]);
+			}
+
+		* =>
+			if(warn)
+				sys->print("warning: unknown HTML tag: %s\n", tok.text);
+		}
+	}
+	if (toki < tokslen)
+		is.toks = toks[toki:];
+	if(tokslen == 0) {
+		# we might have hit eof from lexer
+		# some pages omit trailing </table>
+		bs := is.ts.b;
+		if(bs.eof && bs.lim == bs.edata) {
+			while(curtab != nil) {
+				if(warn)
+					sys->print("warning: <TABLE> not closed\n");
+				if(curtab.cells != nil) {
+					(ps, psstk) = finishcell(curtab, psstk);
+					if(curtab.currows != nil)
+						(hd curtab.currows).flags = byte 0;
+					finish_table(curtab);
+					ps.skipping = 0;
+					additem(ps, Item.newtable(curtab), curtab.tabletok);
+					addbrk(ps, 0, 0);
+				}
+				if(is.tabstk != nil)
+					is.tabstk = tl is.tabstk;
+				if(is.tabstk == nil)
+					curtab = nil;
+				else
+					curtab = hd is.tabstk;
+			}
+		}
+	}
+	outerps := lastps(psstk);
+	ans := outerps.items.next;
+	# note: ans may be nil and di.kids not nil, if there's a frameset!
+	outerps.items = Item.newspacer(ISPnull, 0);
+	outerps.lastit = outerps.items;
+	is.psstk = psstk;
+
+	if(dbg) {
+		if(ans == nil)
+			sys->print("getitems returning nil\n");
+		else
+			ans.printlist("getitems returning:");
+	}
+	return ans;
+}
+
+endanchor(ps: ref Pstate, docfg: int)
+{
+	if(ps.curanchor != 0) {
+		if(ps.fgstk != nil) {
+			ps.fgstk = tl ps.fgstk;
+			if(ps.fgstk == nil)
+				ps.curfg = docfg;
+			else
+				ps.curfg = hd ps.fgstk;
+		}
+		ps.curanchor = 0;
+		if(ps.ulstk != nil) {
+			ps.ulstk = tl ps.ulstk;
+			if(ps.ulstk == nil)
+				ps.curul = ULnone;
+			else
+				ps.curul = hd ps.ulstk;
+		}
+	}
+}
+
+lexstring(s: string) : array of ref Token
+{
+	bs := ByteSource.stringsource(s);
+	ts := TokenSource.new(bs, utf8, CU->TextHtml);
+	ans : array of ref Token = nil;
+	# gettoks might return answer in several hunks
+	for(;;) {
+		toks := ts.gettoks();
+		if(toks == nil)
+			break;
+		if(ans != nil) {
+			newans := array[len ans + len toks] of ref Token;
+			newans[0:] = ans;
+			newans[len ans:] = toks;
+			ans = newans;
+		}
+		else
+			ans = toks;
+	}
+	return ans;
+}
+
+lastps(psl: list of ref Pstate) : ref Pstate
+{
+	if(psl == nil)
+		raise "EXInternal: empty pstate stack";
+	while(tl psl != nil)
+		psl = tl psl;
+	return hd psl;
+}
+
+# Concatenate together maximal set of Data tokens, starting at toks[toki+1].
+# Lexer has ensured that there will either be a following non-data token or
+# we will be at eof.
+# Return (trimmed concatenation, last used toki).
+getpcdata(toks: array of ref Token, toki: int) : (string, int)
+{
+	ans := "";
+	tokslen := len toks;
+	toki++;
+	for(;;) {
+		if(toki >= tokslen)
+			break;
+		tok := toks[toki];
+		if(tok.tag == LX->Data) {
+			toki++;
+			ans = ans + tok.text;
+		}
+		else
+			break;
+	}
+	return (trim_white(ans),  toki-1);
+}
+
+optiontext(str : string) : string
+{
+	ans := "";
+	lastc := 0;
+	for (i := 0; i < len str; i++) {
+		if (str[i] > 16r20)
+			ans[len ans] = str[i];
+		else if (lastc > 16r20)
+			ans[len ans] = ' ';
+		lastc = str[i];
+	}
+	return ans;
+}
+
+finishcell(curtab: ref Table, psstk: list of ref Pstate) : (ref Pstate, list of ref Pstate)
+{
+	if(curtab.cells != nil) {
+		c := hd curtab.cells;
+		if((c.flags&TFparsing) != byte 0) {
+			if(tl psstk == nil) {
+				if(warn)
+					sys->print("warning: parse state stack is wrong\n");
+			}
+			else {
+				ps := hd psstk;
+				c.content = ps.items.next;
+				c.flags &= ~TFparsing;
+				psstk = tl psstk;
+			}
+		}
+	}
+	return (hd psstk, psstk);
+}
+
+Pstate.new() : ref Pstate
+{
+	ps := ref Pstate (
+			0, 0, DefFnt,	# skipping, skipwhite, curfont
+			CU->Black,	# curfg
+			Background(nil, CU->White),
+			0,			# curvoff
+			ULnone, Aleft,	# curul, curjust
+			0, IFwrap,		# curanchor, curstate
+			0, 0, 0,		# literal, inpar, adjsize
+			nil, nil, nil,		# items, lastit, prelastit
+			nil, nil, nil, nil,	# fntstylestk, fntsizestk, fgstk, ulstk
+			nil, nil, nil, nil,	# voffstk, listtypestk, listcntstk, juststk
+			nil);			# hangstk
+	ps.items = Item.newspacer(ISPnull, 0);
+	ps.lastit = ps.items;
+	ps.prelastit = nil;
+	return ps;
+}
+
+cell_pstate(oldps: ref Pstate, ishead: int) : ref Pstate
+{
+	ps := Pstate.new();
+	ps.skipwhite = 1;
+	ps.curanchor = oldps.curanchor;
+	ps.fntstylestk = oldps.fntstylestk;
+	ps.fntsizestk = oldps.fntsizestk;
+	ps.curfont = oldps.curfont;
+	ps.curfg = oldps.curfg;
+	ps.curbg = oldps.curbg;
+	ps.fgstk = oldps.fgstk;
+	ps.adjsize = oldps.adjsize;
+	if(ishead) {
+		# make bold
+		sty := ps.curfont%NumSize;
+		ps.curfont = FntB*NumSize + sty;
+	}
+	return ps;
+}
+
+trim_white(data: string): string
+{
+	data = S->drop(data, whitespace);
+	(l,nil) := S->splitr(data, notwhitespace);
+	return l;
+}
+
+# Add it to end of ps item chain, adding in current state from ps.
+# Also, if tok is not nil, scan it for generic attributes and assign
+# the genattr field of the item accordingly.
+additem(ps: ref Pstate, it: ref Item, tok: ref LX->Token)
+{
+	if(ps.skipping) {
+		if(warn) {
+			sys->print("warning: skipping item:\n");
+			it.print();
+		}
+		return;
+	}
+	it.anchorid = ps.curanchor;
+	it.state |= ps.curstate;
+	if(tok != nil)
+		it.genattr = getgenattr(tok);
+	ps.curstate &= ~(IFbrk|IFbrksp|IFnobrk|IFcleft|IFcright);
+	ps.prelastit = ps.lastit;
+	ps.lastit.next = it;
+	ps.lastit = it;
+}
+
+getgenattr(tok: ref LX->Token) : ref Genattr
+{
+	any := 0;
+	i, c, s, t: string;
+	e: list of LX->Attr = nil;
+	for(al := tok.attr; al != nil; al = tl al) {
+		a := hd al;
+		aid := a.attid;
+		if(attrinfo[aid] == byte 0)
+			continue;
+		case aid {
+		LX->Aid =>
+			i = a.value;
+			any = 1;
+		LX->Aclass =>
+			c = a.value;
+			any = 1;
+		LX->Astyle =>
+			s = a.value;
+			any = 1;
+		LX->Atitle =>
+			t = a.value;
+			any = 1;
+		* =>
+			CU->assert(aid >= LX->Aonabort && aid <= LX->Aonunload);
+			e = a :: e;
+			any = 1;
+		}
+	}
+	if(any)
+		return ref Genattr(i, c, s, t, e, 0);
+	return nil;
+}
+
+textit(ps: ref Pstate, s: string) : ref Item
+{
+	return Item.newtext(s, ps.curfont, ps.curfg, ps.curvoff+Voffbias, ps.curul);
+}
+
+# Add text item or items for s, paying attention to
+# current font, foreground, baseline offset, underline state,
+# and literal mode.  Unless we're in literal mode, compress
+# whitespace to single blank, and, if curstate has a break,
+# trim any leading whitespace.  Whether in literal mode or not,
+# turn nonbreaking spaces into spacer items with IFnobrk set.
+#
+# In literal mode, break up s at newlines and add breaks instead.
+# Also replace tabs appropriate number of spaces.
+# In nonliteral mode, break up the items every 100 or so characters
+# just to make the layout algorithm not go quadratic.
+#
+# This code could be written much shorter using the String module
+# split and drop functions, but we want this part to go fast.
+addtext(ps: ref Pstate, s: string)
+{
+	n := len s;
+	i := 0;
+	j := 0;
+	if(ps.literal) {
+		col := 0;
+		while(i < n) {
+			if(s[i] == '\n') {
+				if(i > j) {
+					# trim trailing blanks from line
+					for(k := i; k > j; k--)
+						if(s[k-1] != ' ')
+							break;
+					if(k > j)
+						additem(ps, textit(ps, s[j:k]), nil);
+				}
+				addlinebrk(ps, 0);
+				j = i+1;
+				col = 0;
+			}
+			else {
+				if(s[i] == '\t') {
+					col += i-j;
+					nsp := 8 - (col % 8);
+					additem(ps, textit(ps, s[j:i] + "        "[0:nsp]), nil);
+					col += nsp;
+					j = i+1;
+				}
+				else if(s[i] == NBSP) {
+					if(i > j)
+						additem(ps, textit(ps, s[j:i]), nil);
+					addnbsp(ps);
+					col += (i-j) + 1;
+					j = i+1;
+				}
+			}
+			i++;
+		}
+		if(i > j)
+			additem(ps, textit(ps, s[j:i]), nil);
+	}
+	else {
+		if((ps.curstate&IFbrk) || ps.lastit == ps.items)
+			while(i < n) {
+				c := s[i];
+				if(c >= C->NCTYPE || ctype[c] != C->W)
+					break;
+				i++;
+			}
+		ss := "";
+		j = i;
+		for( ; i < n; i++) {
+			c := s[i];
+			if(c == NBSP) {
+				if(i > j)
+					ss += s[j:i];
+				if(ss != "")
+					additem(ps, textit(ps, ss), nil);
+				ss = "";
+				addnbsp(ps);
+				j = i + 1;
+				continue;
+			}
+			if(c < C->NCTYPE && ctype[c] == C->W) {
+				ss += s[j:i] + " ";
+				while(i < n-1) {
+					c = s[i+1];
+					if(c >= C->NCTYPE || ctype[c] != C->W)
+						break;
+					i++;
+				}
+				j = i + 1;
+			}
+			if(i - j >= 100) {
+				ss += s[j:i+1];
+				j = i + 1;
+			}
+			if(len ss >= 100) {
+				additem(ps, textit(ps, ss), nil);
+				ss = "";
+			}
+		}
+		if(i > j && j < n)
+			ss += s[j:i];
+		# don't add a space if previous item ended in a space
+		if(ss == " " && ps.lastit != nil) {
+			pick t := ps.lastit {
+			Itext =>
+				sp := t.s;
+				nsp := len sp;
+				if(nsp > 0 && sp[nsp-1] == ' ')
+					ss = "";
+			}
+		}
+		if(ss != "")
+			additem(ps, textit(ps, ss), nil);
+	}
+}
+
+# Add a break to ps.curstate, with extra space if sp is true.
+# If there was a previous break, combine this one's parameters
+# with that to make the amt be the max of the two and the clr
+# be the most general. (amt will be 0 or 1)
+# Also, if the immediately preceding item was a text item,
+# trim any whitespace from the end of it, if not in literal mode.
+# Finally, if this is at the very beginning of the item list
+# (the only thing there is a null spacer), then don't add the space.
+addbrk(ps: ref Pstate, sp: int, clr: int)
+{
+	state := ps.curstate;
+	clr = clr | (state&(IFcleft|IFcright));
+	if(sp && !(ps.lastit == ps.items))
+		sp = IFbrksp;
+	else
+		sp = 0;
+	ps.curstate = IFbrk | sp | (state&~(IFcleft|IFcright)) | clr;
+	if(ps.lastit != ps.items) {
+		if(!ps.literal && tagof ps.lastit == tagof Item.Itext) {
+			pick t := ps.lastit {
+			Itext =>
+				(l,nil) := S->splitr(t.s, notwhitespace);
+				# try to avoid making empty items
+				# (but not crucial if the occasional one gets through)
+				if(l == "" && ps.prelastit != nil) {
+					ps.lastit = ps.prelastit;
+					ps.lastit.next = nil;
+					ps.prelastit = nil;
+				}
+				else
+					t.s = l;
+			}
+		}
+	}
+}
+
+# Add break due to a <br> or a newline within a preformatted section.
+# We add a null item first, with current font's height and ascent, to make
+# sure that the current line takes up at least that amount of vertical space.
+# This ensures that <br>s on empty lines cause blank lines, and that
+# multiple <br>s in a row give multiple blank lines.
+# However don't add the spacer if the previous item was something that
+# takes up space itself. [[ I think this is not what we want; see
+# MR inf983435. --Ravi ]]
+addlinebrk(ps: ref Pstate, clr: int)
+{
+	# don't want break before our null item unless the previous item
+	# was also a null item for the purposes of line breaking
+	obrkstate := ps.curstate & (IFbrk|IFbrksp);
+	b := IFnobrk;
+	if(ps.lastit != nil) {
+		pick pit := ps.lastit {
+		Ispacer =>
+			if(pit.spkind == ISPvline)
+				b = IFbrk;
+		}
+	}
+	ps.curstate = (ps.curstate & ~(IFbrk|IFbrksp)) | b;
+	additem(ps, Item.newspacer(ISPvline, ps.curfont), nil);
+	ps.curstate = (ps.curstate & ~(IFbrk|IFbrksp)) | obrkstate;
+	addbrk(ps, 0, clr);
+}
+
+# Add a nonbreakable space
+addnbsp(ps: ref Pstate)
+{
+	# if nbsp comes right where a break was specified,
+	# do the break anyway (nbsp is being used to generate undiscardable
+	# space rather than to prevent a break)
+	if((ps.curstate&IFbrk) == 0)
+		ps.curstate |=  IFnobrk;
+	additem(ps, Item.newspacer(ISPhspace, ps.curfont), nil);
+	# but definitely no break on next item
+	ps.curstate |= IFnobrk;
+}
+
+# Change hang in ps.curstate by delta.
+# The amount is in 1/10ths of tabs, and is the amount that
+# the current contiguous set of items with a hang value set
+# is to be shifted left from its normal (indented) place.
+changehang(ps: ref Pstate, delta: int)
+{
+	amt := (ps.curstate&IFhangmask) + delta;
+	if(amt < 0) {
+		if(warn)
+			sys->print("warning: hang went negative\n");
+		amt = 0;
+	}
+	ps.curstate = (ps.curstate&~IFhangmask) | amt;
+}
+
+# Change indent in ps.curstate by delta.
+changeindent(ps: ref Pstate, delta: int)
+{
+	amt := ((ps.curstate&IFindentmask)>>IFindentshift) + delta;
+	if(amt < 0) {
+		if(warn)
+			sys->print("warning: indent went negative\n");
+		amt = 0;
+	}
+	ps.curstate = (ps.curstate&~IFindentmask) | (amt<<IFindentshift);
+}
+
+stackhd(stk: list of int, dflt: int) : int
+{
+	if(stk == nil)
+		return dflt;
+	return hd stk;
+}
+
+popfontstyle(ps: ref Pstate)
+{
+	if(ps.fntstylestk != nil)
+		ps.fntstylestk = tl ps.fntstylestk;
+	setcurfont(ps);
+}
+
+pushfontstyle(ps: ref Pstate, sty: int)
+{
+	ps.fntstylestk = sty :: ps.fntstylestk;
+	setcurfont(ps);
+}
+
+popfontsize(ps: ref Pstate)
+{
+	if(ps.fntsizestk != nil)
+		ps.fntsizestk = tl ps.fntsizestk;
+	setcurfont(ps);
+}
+
+pushfontsize(ps: ref Pstate, sz: int)
+{
+	ps.fntsizestk = sz :: ps.fntsizestk;
+	setcurfont(ps);
+}
+
+setcurfont(ps: ref Pstate)
+{
+	sty := FntR;
+	sz := Normal;
+	if(ps.fntstylestk != nil)
+		sty = hd ps.fntstylestk;
+	if(ps.fntsizestk != nil)
+		sz = hd ps.fntsizestk;
+	if(sz < Tiny)
+		sz = Tiny;
+	if(sz > Verylarge)
+		sz = Verylarge;
+	ps.curfont = sty*NumSize + sz;
+}
+
+popjust(ps: ref Pstate)
+{
+	if(ps.juststk != nil)
+		ps.juststk = tl ps.juststk;
+	setcurjust(ps);
+}
+
+pushjust(ps: ref Pstate, j: byte)
+{
+	ps.juststk = j :: ps.juststk;
+	setcurjust(ps);
+}
+
+setcurjust(ps: ref Pstate)
+{
+	if(ps.juststk != nil)
+		j := hd ps.juststk;
+	else
+		j = Aleft;
+	if(j != ps.curjust) {
+		ps.curjust = j;
+		state := ps.curstate;
+		state &= ~(IFrjust|IFcjust);
+		if(j == Acenter)
+			state |= IFcjust;
+		else if(j == Aright)
+			state |= IFrjust;
+		ps.curstate = state;
+	}
+}
+
+# Do final rearrangement after table parsing is finished
+# and assign cells to grid points
+finish_table(t: ref Table)
+{
+	t.nrow = len t.currows;
+	t.rows = array[t.nrow] of ref Tablerow;
+	ncol := 0;
+	r := t.nrow-1;
+	for(rl := t.currows; rl != nil; rl = tl rl) {
+		row := hd rl;
+		t.rows[r--] = row;
+		rcols := 0;
+		cl := row.cells;
+		# If rowspan is > 1 but this is the last row,
+		# reset the rowspan
+		if(cl != nil && (hd cl).rowspan > 1 && rl == t.currows)
+			(hd cl).rowspan = 1;
+		row.cells = nil;
+		while(cl != nil) {
+			c := hd cl;
+			row.cells = c :: row.cells;
+			rcols += c.colspan;
+			cl = tl cl;
+		}
+		if(rcols > ncol)
+			ncol = rcols;
+	}
+	t.currows = nil;
+	t.ncol = ncol;
+	t.cols = array[ncol] of { * => Tablecol(0, Align(Anone, Anone), (0,0)) };
+
+	# Reverse cells just so they are drawn in source order.
+	# Also, trim their contents so they don't end in whitespace.
+	cells : list of ref Tablecell = nil;
+	for(cl := t.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		trim_cell(c);
+		cells = c :: cells;
+	}
+	t.cells = cells;
+
+	t.grid = array[t.nrow] of { * => array[t.ncol] of ref Tablecell };
+	# The following arrays keep track of cells that are spanning
+	# multiple rows;  rowspancnt[i] is the number of rows left
+	# to be spanned in column i.
+	# When done, cell's (row,col) is upper left grid point.
+	rowspancnt := array[t.ncol] of { * => 0};
+	rowspancell := array[t.ncol] of ref Tablecell;
+
+	ri := 0;
+	ci := 0;
+	for(ri = 0; ri < t.nrow; ri++) {
+		row := t.rows[ri];
+		cl = row.cells;
+		for(ci = 0; ci < t.ncol || cl != nil; ) {
+			if(ci < t.ncol && rowspancnt[ci] > 0) {
+				t.grid[ri][ci] = rowspancell[ci];
+				rowspancnt[ci]--;
+				ci++;
+			}
+			else {
+				if(cl == nil) {
+					ci++;
+					continue;
+				}
+				c := hd cl;
+				cl = tl cl;
+				cspan := c.colspan;
+				rspan := c.rowspan;
+				if(ci+cspan > t.ncol) {
+					# because of row spanning, we calculated
+					# ncol incorrectly; adjust it
+					newncol := ci+cspan;
+					newcols := array[newncol] of Tablecol;
+					newrowspancnt := array[newncol] of { * => 0};
+					newrowspancell := array[newncol] of ref Tablecell;
+					newcols[0:] = t.cols;
+					newrowspancnt[0:] = rowspancnt;
+					newrowspancell[0:] = rowspancell;
+					for(k := t.ncol; k < newncol; k++)
+						newcols[k] = Tablecol(0, Align(Anone, Anone), (0,0));
+					t.cols = newcols;
+					rowspancnt = newrowspancnt;
+					rowspancell = newrowspancell;
+					for(j := 0; j < t.nrow; j++) {
+						newgrr := array[newncol] of ref Tablecell;
+						newgrr[0:] = t.grid[j];
+						for(k = t.ncol; k < newncol; k++)
+							newgrr[k] = nil;
+						t.grid[j] = newgrr;
+					}
+					t.ncol = newncol;
+				}
+				c.row = ri;
+				c.col = ci;
+				for(i := 0; i < cspan; i++) {
+					t.grid[ri][ci] = c;
+					if(rspan > 1) {
+						rowspancnt[ci] = rspan-1;
+						rowspancell[ci] = c;
+					}
+					ci++;
+				}
+			}
+		}
+	}
+	t.flags |= Layout->Lchanged;
+}
+
+# Remove tail of cell content until it isn't whitespace.
+trim_cell(c: ref Tablecell)
+{
+	dropping := 1;
+	while(c.content != nil && dropping) {
+		p := c.content;
+		pprev : ref Item = nil;
+		while(p.next != nil) {
+			pprev = p;
+			p = p.next;
+		}
+		dropping = 0;
+		if(!(p.state&IFnobrk)) {
+			pick q := p {
+			Itext =>
+				s := q.s;
+				(x,y) := S->splitr(s, notwhitespace);
+				if(x == nil)
+					dropping = 1;
+				else if(y != nil)
+					q.s = x;
+			}
+		}
+		if(dropping) {
+			if(pprev == nil)
+				c.content = nil;
+			else
+				pprev.next = nil;
+		}
+	}
+}
+
+roman := array[] of {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X",
+	"XI", "XII", "XIII", "XIV", "XV"};
+
+listmark(ty: byte, n: int) : string
+{
+	s := "";
+	case int ty {
+		int LTdisc =>
+			s = "•";
+		int LTsquare =>
+			s = "∎";
+		int LTcircle =>
+			s = "∘";
+		int LT1 =>
+			s = string n + ".";
+		int LTa or int LTA =>
+			n--;
+			i := 0;
+			if(n < 0)
+				n = 0;
+			if(n > 25) {
+				n2 := n / 26;
+				n %= 26;
+				if(n2 > 25)
+					n2 = 25;
+				s[i++] = n2 + 'A';
+			}
+			s[i++] = n + 'A';
+			s[i++] = '.';
+			if(ty == LTa)
+				s = S->tolower(s);
+		int LTi or int LTI =>
+			if(n >= len roman) {
+				if(warn)
+					sys->print("warning: unimplemented roman number > %d\n", len roman);
+				n = len roman;
+			}
+			s = roman[n-1];
+			if(ty == LTi)
+				s = S->tolower(s);
+			s += ".";
+	}
+	return s;
+}
+
+# Find map with given name in di.maps.
+# If not there, add one.
+getmap(di: ref Docinfo, name: string) : ref Map
+{
+	m : ref Map;
+	for(ml := di.maps; ml != nil; ml = tl ml) {
+		m = hd ml;
+		if(m.name == name)
+			return m;
+	}
+	m = Map.new(name);
+	di.maps = m :: di.maps;
+	return m;
+}
+
+# attrvalue, when "found" status doesn't matter
+# (because nil ans is sufficient indication)
+aval(tok: ref Token, attid: int) : string
+{
+	(nil, ans) := tok.aval(attid);
+	return ans;
+}
+
+# attrvalue, when ans is a string, but need default
+astrval(tok: ref Token, attid: int, dflt: string) : string
+{
+	(fnd, ans) := tok.aval(attid);
+	if(!fnd)
+		return dflt;
+	else
+		return ans;
+}
+
+# attrvalue, when supposed to convert to int
+# and have default for when not found
+aintval(tok: ref Token, attid: int, dflt: int) : int
+{
+	(fnd, ans) := tok.aval(attid);
+	if(!fnd || ans == "")
+		return dflt;
+	else
+		return toint(ans);
+}
+
+# Like int conversion, but with possible error check (if warning)
+toint(s: string) : int
+{
+	if(warn) {
+		ok := 0;
+		for(i := 0; i < len s; i++) {
+			c := s[i];
+			if(!(c < C->NCTYPE && ctype[c] == C->W))
+				break;
+		}
+		for(; i < len s; i++) {
+			c := s[i];
+			if(c < C->NCTYPE && ctype[c] == C->D)
+				ok = 1;
+			else {
+				ok = 0;
+				break;
+			}
+		}
+		if(!ok || i != len s)
+			sys->print("warning: expected integer, got '%s'\n", s);
+	}
+	return int s;
+}
+
+# attrvalue when need a table to convert strings to ints
+atabval(tok: ref Token, attid: int, tab: array of T->StringInt, dflt: int) : int
+{
+	(fnd, aval) := tok.aval(attid);
+	ans := dflt;
+	if(fnd) {
+		name := S->tolower(aval);
+		(fnd, ans) = T->lookup(tab, name);
+		if(!fnd) {
+			ans = dflt;
+			if(warn)
+				sys->print("warning: name not found in table lookup: %s\n", name);
+		}
+	}
+	return ans;
+}
+
+# like atabval, but when want a byte answer
+atabbval(tok: ref Token, attid: int, tab: array of T->StringInt, dflt: byte) : byte
+{
+	(fnd, aval) := tok.aval(attid);
+	ans := dflt;
+	if(fnd) {
+		name := S->tolower(aval);
+		ians : int;
+		(fnd, ians) = T->lookup(tab, name);
+		if(fnd)
+			ans = byte ians;
+		else if(warn)
+			sys->print("warning: name not found in table lookup: %s\n", name);
+	}
+	return ans;
+}
+
+# special for list types, where "i" and "I" are different,
+# but "square" and "SQUARE" are the same
+listtyval(tok: ref Token, dflt: byte) : byte
+{
+	(fnd, aval) := tok.aval(LX->Atype);
+	ans := dflt;
+	if(fnd) {
+		case aval {
+		"1" => ans = LT1;
+		"A" => ans = LTA;
+		"I" => ans = LTI;
+		"a" => ans = LTa;
+		"i" => ans = LTi;
+		* =>
+			aval = S->tolower(aval);
+			case aval {
+			"circle" => ans = LTcircle;
+			"disc" => ans = LTdisc;
+			"square" => ans = LTsquare;
+			* => if(warn)
+				sys->print("warning: unknown list element type %s\n", aval);
+			}
+		}
+	}
+	return ans;
+}
+
+# attrvalue when value is a URL
+aurlval(tok: ref Token, attid: int, dflt, base: ref Parsedurl) : ref Parsedurl
+{
+	ans := dflt;
+	(fnd, url) := tok.aval(attid);
+	if(fnd && url != nil) {
+		url = S->drop(url, whitespace);
+		ans = U->parse(url);
+		case (ans.scheme) {
+		"javascript" =>
+			;	# don't strip whitespace from the URL
+		* =>
+			# sometimes people put extraneous whitespace in
+			url = stripwhite(url);
+			ans = U->parse(url);
+			if(base != nil)
+				ans = U->mkabs(ans, base);
+		}
+	}
+	return ans;
+}
+
+# remove any whitespace characters from any part of s
+# up to a '#' (assuming s is a url and '#' begins a fragment
+# (can return s if there are no whitespace characters in it)
+stripwhite(s: string) : string
+{
+	j := 0;
+	n := len s;
+	strip := 1;
+	for(i := 0; i < n; i++) {
+		c := s[i];
+		if(c == '#')
+			strip = 0;
+		if(strip && c < C->NCTYPE && ctype[c]==C->W)
+			continue;
+		s[j++] = c;
+	}
+	if(j < n)
+		s = s[0:j];
+	return s;
+}
+
+# Presence of attribute implies true, omission implies false.
+# Boolean attributes can have a value equal to their attribute name.
+# HTML4.01 does not state whether the attribute is true or false
+# if a value is given that doesn't match the attribute name.
+aboolval(tok: ref Token, attid: int): int
+{
+	(fnd, nil) := tok.aval(attid);
+	return fnd;
+}
+
+# attrvalue when mere presence of attr implies value of 1
+aflagval(tok: ref Token, attid: int) : int
+{
+	val := 0;
+	(fnd, sval) := tok.aval(attid);
+	if(fnd) {
+		val = 1;
+		if(sval != "")
+			val = toint(sval);
+	}
+	return val;
+}
+
+# Make an Align (two alignments, horizontal and vertical)
+makealign(tok: ref Token) : Align
+{
+	h := atabbval(tok, LX->Aalign, align_tab, Anone);
+	v := atabbval(tok, LX->Avalign, align_tab, Anone);
+	return Align(h, v);
+}
+
+# Make a Dimen, based on value of attid attr
+makedimen(tok: ref Token, attid: int) : Dimen
+{
+	(fnd, wd) := tok.aval(attid);
+	if(fnd)
+		return parsedim(wd);
+	else
+		return Dimen.make(Dnone, 0);
+}
+
+# Parse s as num[.[num]][unit][%|*]
+parsedim(s: string) : Dimen
+{
+	kind := Dnone;
+	spec := 0;
+	(l,r) := S->splitl(s, "^0-9");
+	if(l != "") {
+		# accumulate 1000 * value (to work in fixed point)
+		spec = 1000 * toint(l);
+		if(S->prefix(".", r)) {
+			f : string;
+			(f,r) = S->splitl(r[1:], "^0-9");
+			if(f != "") {
+				mul := 100;
+				for(i := 0; i < len f; i++) {
+					spec = spec + mul * toint(f[i:i+1]);
+					mul = mul / 10;
+				}
+			}
+		}
+		kind = Dpixels;
+		if(r != "") {
+			if(len r >= 2) {
+				Tkdpi := 100;	# hack, but matches current tk
+				units := r[0:2];
+				r = r[2:];
+				case units {
+				"pt" => spec = (spec*Tkdpi)/72;
+				"pi" => spec = (spec*12*Tkdpi)/72;
+				"in" => spec = spec*Tkdpi;
+				"cm" => spec = (spec*100*Tkdpi)/254;
+				"mm" => spec = (spec*10*Tkdpi)/254;
+				"em" => spec = spec * 15;	# hack, lucidasans 8pt is 15 pixels high
+				* =>
+					if(warn)
+						sys->print("warning: unknown units %s\n", units);
+				}
+			}
+			if(r == "%")
+				kind = Dpercent;
+			else if(r == "*")
+				kind = Drelative;
+		}
+		spec = spec / 1000;
+	}
+	else if(r == "*") {
+		spec = 1;
+		kind = Drelative;
+	}
+	return Dimen.make(kind, spec);
+}
+
+dimlist(tok: ref Token, attid: int) : array of Dimen
+{
+	s := aval(tok, attid);
+	if(s != "") {
+		(nc, cl) := sys->tokenize(s, ", ");
+		if(nc > 0) {
+			d := array[nc] of Dimen;
+			for(k := 0; k < nc; k++) {
+				d[k] = parsedim(hd cl);
+				cl = tl cl;
+			}
+			return d;
+		}
+	}
+	return nil;
+}
+
+stringdim(d: Dimen) : string
+{
+	ans := string d.spec();
+	k := d.kind();
+	if(k == Dpercent)
+		ans += "%";
+	if(k == Drelative)
+		ans += "*";
+	return ans;
+}
+
+stringalign(a: byte) : string
+{
+	s := T->revlookup(align_tab, int a);
+	if(s == nil)
+		s = "none";
+	return s;
+}
+
+stringstate(state: int) : string
+{
+	s := "";
+	if(state&IFbrk) {
+		c := state&(IFcleft|IFcright);
+		clr := "";
+		if(int c) {
+			if(c == (IFcleft|IFcright))
+				clr = " both";
+			else if(c == IFcleft)
+				clr = " left";
+			else
+				clr = " right";
+		}
+		amt := 0;
+		if(state&IFbrksp)
+			amt = 1;
+		s = sys->sprint("brk(%d%s)", amt, clr);
+	}
+	if(state&IFnobrk)
+		s += " nobrk";
+	if(!(state&IFwrap))
+		s += " nowrap";
+	if(state&IFrjust)
+		s += " rjust";
+	if(state&IFcjust)
+		s += " cjust";
+	if(state&IFsmap)
+		s += " smap";
+	indent := (state&IFindentmask)>>IFindentshift;
+	if(indent > 0)
+		s += " indent=" + string indent;
+	hang := state&IFhangmask;
+	if(hang > 0)
+		s += " hang=" + string hang;
+	return s;
+}
+
+Item.newtext(s: string, fnt, fg, voff: int, ul: byte) : ref Item
+{
+	return ref Item.Itext(nil, 0, 0, 0, 0, 0, nil, s, fnt, fg, byte voff, ul);
+}
+
+Item.newrule(align: byte, size, noshade: int, wspec: Dimen) : ref Item
+{
+	return ref Item.Irule(nil, 0, 0, 0, 0, 0, nil, align, byte noshade, size, wspec);
+}
+
+Item.newimage(di: ref Docinfo, src: ref Parsedurl, lowsrc: ref Parsedurl, altrep: string,
+	align: byte, width, height, hspace, vspace, border, ismap, isbkg: int,
+	map: ref Map, name: string, genattr: ref Genattr) : ref Item
+{
+	ci := CImage.new(src, lowsrc, width, height);
+	state := 0;
+	if(ismap)
+		state = IFsmap;
+	if (isbkg)
+		state = IFbkg;
+	return ref Item.Iimage(nil, 0, 0, 0, 0, state, genattr, len di.images,
+			ci, width, height, altrep, map, name, -1, align, byte hspace, byte vspace, byte border);
+}
+
+Item.newformfield(ff: ref Formfield) : ref Item
+{
+	return ref Item.Iformfield(nil, 0, 0, 0, 0, 0, nil, ff);
+}
+
+Item.newtable(t: ref Table) : ref Item
+{
+	return ref Item.Itable(nil, 0, 0, 0, 0, 0, nil, t);
+}
+
+Item.newfloat(it: ref Item, side: byte) : ref Item
+{
+	return ref Item.Ifloat(nil, 0, 0, 0, 0, IFwrap, nil, it, 0, 0, side, byte 0);
+}
+
+Item.newspacer(spkind, font: int) : ref Item
+{
+	return ref Item.Ispacer(nil, 0, 0, 0, 0, 0, nil, spkind, font);
+}
+
+Item.revlist(itl: list of ref Item) : list of ref Item
+{
+	ans : list of ref Item = nil;
+	for( ;itl != nil; itl = tl itl)
+		ans = hd itl :: ans;
+	return ans;
+}
+
+Item.print(it: self ref Item)
+{
+	s := stringstate(it.state);
+	if(s != "")
+		sys->print("%s\n",s);
+	pick a := it {
+	Itext =>
+		sys->print("Text '%s', fnt=%d, fg=%x", a.s, a.fnt, a.fg);
+	Irule =>
+		sys->print("Rule wspec=%s, size=%d, al=%s",
+			stringdim(a.wspec), a.size, stringalign(a.align));
+	Iimage =>
+		src := "";
+		if(a.ci.src != nil)
+			src = a.ci.src.tostring();
+		map := "";
+		if(a.map != nil)
+			map = a.map.name;
+		sys->print("Image src=%s, alt=%s, al=%s, w=%d, h=%d hsp=%d, vsp=%d, bd=%d, map=%s, name=%s",
+			src, a.altrep, stringalign(a.align), a.imwidth, a.imheight,
+			int a.hspace, int a.vspace, int a.border, map, a.name);
+	Iformfield =>
+		ff := a.formfield;
+		if(ff.ftype == Ftextarea)
+			ty := "textarea";
+		else if(ff.ftype == Fselect)
+			ty = "select";
+		else
+			ty = T->revlookup(input_tab, int ff.ftype);
+		sys->print("Formfield %s, fieldid=%d, formid=%d, name=%s, value=%s",
+			ty, ff.fieldid, int ff.form.formid, ff.name, ff.value);
+	Itable =>
+		tab := a.table;
+		sys->print("Table tableid=%d, width=%s, nrow=%d, ncol=%d, ncell=%d, totw=%d, toth=%d\n",
+			tab.tableid, stringdim(tab.width), tab.nrow, tab.ncol, tab.ncell, tab.totw, tab.toth);
+		for(cl := tab.cells; cl != nil; cl = tl cl) {
+			c := hd cl;
+			c.content.printlist(sys->sprint("Cell %d.%d, at (%d,%d)", tab.tableid, c.cellid, c.row, c.col));
+		}
+		sys->print("End of Table %d", tab.tableid);
+	Ifloat =>
+		sys->print("Float, x=%d y=%d, side=%s, it=", a.x, a.y, stringalign(a.side));
+		a.item.print();
+		sys->print("\n\t");
+	Ispacer =>
+		s = "";
+		case a.spkind {
+		ISPnull =>
+			s = "null";
+		ISPvline =>
+			s = "vline";
+		ISPhspace =>
+			s = "hspace";
+		}
+		sys->print("Spacer %s ", s);
+	}
+	sys->print(" w=%d, h=%d, a=%d, anchor=%d\n", it.width, it.height, it.ascent, it.anchorid);
+}
+
+Item.printlist(items: self ref Item, msg: string)
+{
+	sys->print("%s\n", msg);
+	il := items;
+	while(il != nil) {
+		il.print();
+		il = il.next;
+	}
+}
+
+Formfield.new(ftype, fieldid: int, form: ref Form, name, value: string, size, maxlength: int) : ref Formfield
+{
+	return ref Formfield(ftype, fieldid, form, name, value, size,
+				maxlength, 0, 0, byte 0, nil, nil, -1, nil, 0);
+}
+
+Form.new(formid: int, name: string, action: ref Parsedurl, target: string, method: int, events: list of Lex->Attr) : ref Form
+{
+	return ref Form(formid, name, action, target, method, events, 0, 0, nil, FormBuild);
+}
+
+Table.new(tableid: int, align: Align, width: Dimen,
+		border, cellspacing, cellpadding: int, bg: Background, tok: ref Lex->Token) : ref Table
+{
+	return ref Table(tableid,
+			0, 0, 0,		# nrow, ncol, ncell
+			align, width, border, cellspacing, cellpadding, bg,
+			nil, Abottom, -1,	# caption, caption_place, caption_lay
+			nil, nil, nil,	nil,	# currows, cols, rows, cells
+			0, 0, 0, 0,		# totw, toth, caph, availw
+			nil, tok, byte 0);	# grid, tabletok, flags
+}
+
+Tablerow.new(align: Align, bg: Background, flags: byte) : ref Tablerow
+{
+	return ref Tablerow(nil,	# cells
+			0, 0,			# height, ascent
+			align,		# align
+			bg,			# background
+			Point(0,0),		# pos
+			flags);
+}
+
+Tablecell.new(cellid, rowspan, colspan: int, align: Align, wspec: Dimen,
+			hspec: int, bg: Background, flags: byte) : ref Tablecell
+{
+	if(colspan < 0)
+		colspan = 0;
+	if(rowspan < 0)
+		rowspan = 0;
+	return ref Tablecell(cellid,
+			nil, -1,		# content, layid
+			rowspan, colspan, align, flags, wspec, hspec, bg,
+			0, 0, 0,		# minw, maxw, ascent
+			0, 0,			# row, col
+			Point(0,0));	# pos
+}
+
+Dimen.kind(d: self Dimen) : int
+{
+	return (d.kindspec & Dkindmask);
+}
+
+Dimen.spec(d: self Dimen) : int
+{
+	return (d.kindspec & Dspecmask);
+}
+
+Dimen.make(kind, spec: int) : Dimen
+{
+	if(spec & Dkindmask) {
+		if(warn)
+			sys->print("warning: dimension spec too big: %d\n", spec);
+		spec = 0;
+	}
+	return Dimen(kind | spec);
+}
+
+Map.new(name: string) : ref Map
+{
+	return ref Map(name, nil);
+}
+
+Docinfo.new() : ref Docinfo
+{
+	ans := ref Docinfo;
+	ans.reset();
+	return ans;
+}
+
+Docinfo.reset(d: self ref Docinfo)
+{
+	d.src = nil;
+	d.base = nil;
+	d.referrer = nil;
+	d.doctitle = "";
+	d.backgrounditem = nil;
+	d.background = (nil, CU->White);
+	d.text = CU->Black;
+	d.link = CU->Blue;
+	d.vlink = CU->Blue;
+	d.alink = CU->Blue;
+	d.target = "_self";
+	d.refresh = "";
+	d.chset = (CU->config).charset;
+	d.lastModified = "";
+	d.scripttype = CU->TextJavascript;
+	d.hasscripts = 0;
+	d.events = nil;
+	d.evmask = 0;
+	d.kidinfo = nil;
+	d.frameid = -1;
+
+	d.anchors = nil;
+	d.dests = nil;
+	d.forms = nil;
+	d.tables = nil;
+	d.maps = nil;
+	d.images = nil;
+}
+
+Kidinfo.new(isframeset: int) : ref Kidinfo
+{
+	ki := ref Kidinfo(isframeset,
+			nil,		# src
+			"",		# name
+			0, 0, 0,	# marginw, marginh, framebd
+			0,		# flags
+			nil, nil, nil	# rows, cols, kidinfos
+			);
+	if(!isframeset) {
+		ki.flags = FRhscrollauto|FRvscrollauto;
+		ki.marginw = FRKIDMARGIN;
+		ki.marginh = FRKIDMARGIN;
+		ki.framebd = 1;
+	}
+	return ki;
+}
--- /dev/null
+++ b/appl/charon/build.m
@@ -1,0 +1,478 @@
+Build: module
+{
+PATH: con "/dis/charon/build.dis";
+
+# Item layout is dictated by desire to have all but formfield and table
+# items allocated in one piece.
+# Also aiming for the 128-byte allocation quantum, which means
+# keeping the total size at 17 32-bit words, including pick tag.
+Item: adt
+{
+	next:		cyclic ref Item;	# successor in list of items
+	width:	int;			# width in pixels (0 for floating items)
+	height:	int;			# height in pixels
+	ascent:	int;			# ascent (from top to baseline) in pixels
+	anchorid:	int;			# if nonzero, which anchor we're in
+	state:	int;			# flags and values (see below)
+	genattr:	ref Genattr;	# generic attributes and events
+
+	pick {
+		Itext =>
+			s: string;		# the characters
+			fnt: int;		# style*NumSize+size (see font stuff, below)
+			fg: int;		# Pixel (color) for text
+			voff: byte;		# Voffbias+vertical offset from baseline, in pixels (+ve == down)
+			ul: byte;		# ULnone, ULunder, or ULmid
+		Irule =>
+			align: byte;	# alignment spec
+			noshade: byte;	# if true, don't shade
+			size: int;		# size attr (rule height)
+			wspec: Dimen;	# width spec
+		Iimage =>
+			imageid: int;		# serial no. of image within its doc
+			ci: ref CharonUtils->CImage;		# charon image (has src, actual width, height)
+			imwidth: int;		# spec width (actual, if no spec)
+			imheight: int;		# spec height (actual, if no spec)
+			altrep: string;		# alternate representation, in absence of image
+			map: ref Map;		# if non-nil, client side map
+			name: string;		# name attribute
+			ctlid: int;			# if animated
+			align: byte;		# vertical alignment
+			hspace: byte;		# in pixels; buffer space on each side
+			vspace: byte;		# in pixels; buffer space on top and bottom
+			border: byte;		# in pixels: border width to draw around image
+		Iformfield =>
+			formfield: ref Formfield;
+		Itable =>
+			table: ref Table;
+		Ifloat =>
+			item: ref Item;		# content of float
+			x: int;			# x coord of top (from right, if Aright)
+			y: int;			# y coord of top
+			side: byte;			# margin it floats to: Aleft or Aright
+			infloats: byte;		# true if this has been added to a lay.floats
+		Ispacer =>
+			spkind: int;		# ISPnone, etc.
+			fnt: int;			# font number
+	}
+
+	newtext: fn(s: string, fnt, fg, voff: int, ul: byte) : ref Item;
+	newrule: fn(align: byte, size, noshade: int, wspec: Dimen) : ref Item;
+	newimage: fn(di: ref Docinfo, src: ref Url->Parsedurl, lowsrc: ref Url->Parsedurl, altrep: string,
+		align: byte, width, height, hspace, vspace, border, ismap, isbkg: int,
+		map: ref Map, name: string, genattr: ref Genattr) : ref Item;
+	newformfield: fn(ff: ref Formfield) : ref Item;
+	newtable: fn(t: ref Table) : ref Item;
+	newfloat: fn(i: ref Item, side: byte) : ref Item;
+	newspacer: fn(spkind, font: int) : ref Item;
+
+	revlist: fn(itl: list of ref Item) : list of ref Item;
+	print: fn(it: self ref Item);
+	printlist: fn(items: self ref Item, msg: string);
+};
+
+# Item state flags and value fields
+IFbrk:		con (1<<31);	# forced break before this item
+IFbrksp:		con (1<<30);	# add 1 line space to break (IFbrk set too)
+IFnobrk:		con (1<<29);	# break not allowed before this item
+IFcleft:		con (1<<28);	# clear left floats (IFbrk set too)
+IFcright:		con (1<<27);	# clear right floats (IFbrk set too)
+IFwrap:		con (1<<26);	# in a wrapping (non-pre) line
+IFhang:		con (1<<25);	# in a hanging (into left indent) item
+IFrjust:		con (1<<24);	# right justify current line
+IFcjust:		con (1<<23);	# center justify current line
+IFsmap:		con (1<<22);	# image is server-side map
+IFbkg:		con (1<<21);	# Item.image is a background image
+IFindentshift:	con 8;
+IFindentmask:	con (255<<IFindentshift);	# current indent, in tab stops
+IFhangmask:	con 255;	# current hang into left indent, in 1/10th tabstops
+
+Voffbias:	con 128;
+
+# Spacer kinds.  ISPnull has 0 height and width,
+# ISPvline has height/ascent of current font
+# ISPhspace has width of space in current font
+# ISPgeneral used for other purposes (e.g. between list markers and list).
+ISPnull, ISPvline, ISPhspace, ISPgeneral: con iota;
+
+# Generic attributes and events (not many elements will have any of these set)
+Genattr: adt
+{
+	id: string;			# document-wide unique id
+	class: string;		# space-separated list of classes
+	style: string;		# associated style info
+	title: string;		# advisory title
+	events: list of Lex->Attr;	# attid will be Aonblur, etc., value is script
+	evmask: int;		# Aonblur|Aonfocus, etc. when present
+};
+
+
+# Formfield Item: a field from a form
+
+# form field types (ints because often case on them)
+Ftext, Fpassword, Fcheckbox, Fradio, Fsubmit, Fhidden, Fimage,
+		Freset, Ffile, Fbutton, Fselect, Ftextarea: con iota;
+
+Formfield: adt
+{
+	ftype: int;		# Ftext, Fpassword, etc.
+	fieldid: int;		# serial no. of field within its form
+	form: cyclic ref Form;	# containing form
+	name: string;		# name attr
+	value: string;		# value attr
+	size: int;			# size attr
+	maxlength: int;		# maxlength attr
+	rows: int;			# rows attr
+	cols: int;			# cols attr
+	flags: byte;		# FFchecked, etc.
+	options: list of ref Option;	# for Fselect fields
+	image: cyclic ref Item;	# image item, for Fimage fields
+	ctlid: int;			# identifies control for this field in layout
+	events: list of Lex->Attr;	# same as genattr.events of containing item
+	evmask: int;
+
+	new: fn(ftype, fieldid: int, form: ref Form, name, value: string, size, maxlength: int) : ref Formfield;
+};
+
+# Form flags
+FFchecked: con byte (1<<7);
+FFmultiple: con byte (1<<6);
+
+# Option holds info about an option in a "select" form field
+Option: adt {
+	selected: int;		# true if selected initially
+	value: string;		# value attr
+	display: string;	# display string
+};
+
+# Form holds info about a form
+Form: adt
+{
+	formid: int;		# serial no. of form within its doc
+	name: string;		# name or id attr (netscape uses name, HTML 4.0 uses id)
+	action: ref Url->Parsedurl;		# action attr
+	target: string;		# target attribute
+	method: int;		# HGet or HPost
+	events: list of Lex->Attr;	# attid will be Aonreset or Aonsubmit
+	evmask: int;
+	nfields: int;		# number of fields
+	fields: cyclic list of ref Formfield;	# field's forms, in input order
+	state: int;			# see Form states enum
+
+	new: fn(formid: int, name: string, action: ref Url->Parsedurl, target: string, method: int, events: list of Lex->Attr) : ref Form;
+};
+
+# Form states
+FormBuild,					# seen <FORM>
+FormDone,					# seen </FORM>
+FormTransferred : con iota;		# tx'd to javascript
+
+# Flags used in various table structures
+TFparsing:	con byte (1<<7);
+TFnowrap:	con byte (1<<6);
+TFisth:		con byte (1<<5);
+
+# A Table Item is for a table.
+Table: adt
+{
+	tableid: int;			# serial no. of table within its doc
+	nrow: int;			# total number of rows
+	ncol: int;			# total number of columns
+	ncell: int;			# total number of cells
+	align: Align;			# alignment spec for whole table
+	width: Dimen;			# width spec for whole table
+	border: int;			# border attr
+	cellspacing: int;		# cellspacing attr
+	cellpadding: int;		# cellpadding attr
+	background: Background;	# table background
+	caption: cyclic ref Item;	# linked list of Items, giving caption
+	caption_place: byte;		# Atop or Abottom
+	caption_lay: int;		# identifies layout of caption
+	currows: cyclic list of ref Tablerow;	# during parsing
+	cols: array of Tablecol;		# column specs
+	rows: cyclic array of ref Tablerow;	# row specs
+	cells: cyclic list of ref Tablecell;		# the unique cells
+	totw: int;					# total width
+	toth: int;					# total height
+	caph: int;					# caption height
+	availw: int;				# used for previous 3 sizes
+	grid: cyclic array of array of ref Tablecell;
+	tabletok: ref Lex->Token;		# token that started the table
+	flags: byte;				# Lchanged
+
+	new: fn(tableid: int, align: Align, width: Dimen,
+		border, cellspacing, cellpadding: int, bg: Background, tok: ref Lex->Token) : ref Table;
+};
+
+# A table column info
+Tablecol: adt
+{
+	width: int;
+	align: Align;
+	pos: Draw->Point;
+};
+
+# A table row spec
+Tablerow: adt
+{
+	cells: cyclic list of ref Tablecell;
+	height: int;
+	ascent: int;
+	align: Align;
+	background: Background;
+	pos: Draw->Point;
+	flags: byte;		# 0 or TFparsing
+
+	new: fn(align: Align, bg: Background, flags: byte) : ref Tablerow;
+};
+
+# A Tablecell is one cell of a table.
+# It may span multiple rows and multiple columns.
+# The (row,col) given indexes upper left corner of cell.
+# Try to keep this under 17 words long.
+Tablecell: adt
+{
+	cellid: int;			# serial no. of cell within table
+	content: cyclic ref Item;	# contents before layout
+	layid: int;			# identifies layout of cell
+	rowspan: int;		# number of rows spanned by this cell
+	colspan: int;		# number of cols spanned by this cell
+	align: Align;		# alignment spec
+	flags: byte;		# TFparsing, TFnowrap, TFisth
+	wspec: Dimen;		# suggested width
+	hspec: int;			# suggested height
+	background: Background;	# cell background
+	minw: int;			# minimum possible width
+	maxw: int;		# maximum width
+	ascent: int;
+	row: int;
+	col: int;
+	pos: Draw->Point;		# nw corner of cell contents, in cell
+
+	new: fn(cellid, rowspan, colspan: int, align: Align, wspec: Dimen,
+			hspec: int, bg: Background, flags: byte) : ref Tablecell;
+};
+
+# Align holds both a vertical and a horizontal alignment.
+# Usually not all are possible in a given context.
+# Anone means no dimension was specified
+
+# alignment types
+Anone, Aleft, Acenter, Aright, Ajustify, Achar, Atop, Amiddle, Abottom, Abaseline: con byte iota;
+
+Align: adt
+{
+	halign: byte;		# one of Anone, Aleft, etc.
+	valign: byte;		# one of Anone, Atop, etc.
+};
+
+# A Dimen holds a dimension specification, especially for those
+# cases when a number can be followed by a % or a * to indicate
+# percentage of total or relative weight.
+# Dnone means no dimension was specified
+
+# Dimen
+# To fit in a word, use top bits to identify kind, rest for value
+Dnone:	con 0;
+Dpixels:	con 1<<29;
+Dpercent:	con 2<<29;
+Drelative:	con 3<<29;
+Dkindmask:	con 3<<29;
+Dspecmask:	con ~Dkindmask;
+
+Dimen: adt
+{
+	kindspec: int;	# kind | spec
+
+	kind: fn(d: self Dimen) : int;
+	spec: fn(d: self Dimen) : int;
+
+	make: fn(kind, spec: int) : Dimen;
+};
+
+
+# Anchor is for info about hyperlinks that go somewhere
+Anchor: adt
+{
+	index: int;			# serial no. of anchor within its doc
+	name: string;		# name attr
+	href: ref Url->Parsedurl;	# href attr
+	target: string;		# target attr
+	events: list of Lex->Attr;	# same as genattr.events of containing items
+	evmask: int;
+};
+
+# DestAnchor is for info about hyperlinks that are destinations
+DestAnchor: adt
+{
+	index: int;		# serial no. of anchor within its doc
+	name: string;		# name attr
+	item: ref Item;		# the destination
+};
+
+# Maps (client side)
+Map: adt
+{
+	name: string;		# map name
+	areas: list of Area;	# hotzones
+
+	new: fn(name: string) : ref Map;
+};
+
+Area: adt
+{
+	shape: string;		# rect, circle, or poly
+	href: ref Url->Parsedurl;		# associated hypertext link
+	target: string;			# associated target frame
+	coords: array of Dimen;	# coords for shape
+};
+
+# Background is either an image or a color.
+# If both are set, the image has precedence.
+Background: adt
+{
+	image: ref Item.Iimage;	# with state |= IFbkg
+	color: int;			# RGB in lower 3 bytes
+};
+
+# Font styles
+FntR, FntI, FntB, FntT, NumStyle: con iota;
+
+# Font sizes
+Tiny, Small, Normal, Large, Verylarge, NumSize: con iota;
+
+NumFnt: con (NumStyle*NumSize);
+DefFnt: con (FntR*NumSize+Normal);
+
+# Lines are needed through some text items, for underlining or strikethrough
+ULnone, ULunder, ULmid: con byte iota;
+
+# List number types
+LTdisc, LTsquare, LTcircle, LT1, LTa, LTA, LTi, LTI: con byte iota;
+
+# Kidinfo flags
+FRnoresize, FRnoscroll, FRhscroll, FRvscroll, FRhscrollauto, FRvscrollauto: con (1<<iota);
+
+# Information about child frame or frameset
+Kidinfo: adt {
+	isframeset: int;
+
+	# fields for "frame"
+	src: ref Url->Parsedurl;		# only nil if a "dummy" frame or this is frameset
+	name: string;			# always non-empty if this isn't frameset
+	marginw: int;
+	marginh: int;
+	framebd: int;
+	flags: int;
+	
+	# fields for "frameset"
+	rows: array of Dimen;
+	cols: array of Dimen;
+	kidinfos: cyclic list of ref Kidinfo;
+
+	new: fn(isframeset: int) : ref Kidinfo;
+};
+
+# Document info (global information about HTML page)
+Docinfo: adt {
+	# stuff from HTTP headers, doc head, and body tag
+	src: ref Url->Parsedurl;			# original source of doc
+	base: ref Url->Parsedurl;			# base URL of doc
+	referrer: ref Url->Parsedurl;			# JavaScript document.referrer
+	doctitle: string;				# from <title> element
+	background: Background;	# background specification
+	backgrounditem: ref Item;	# Image Item for doc background image, or nil
+	text: int;					# doc foreground (text) color
+	link: int;					# unvisited hyperlink color
+	vlink: int;					# visited hyperlink color
+	alink: int;					# highlighting hyperlink color
+	target: string;				# target frame default
+	refresh: string;				# content of <http-equiv=Refresh ...>
+	chset: string;				# charset encoding
+	lastModified: string;				# last-modified time
+	scripttype: int;				# CU->TextJavascript, etc.
+	hasscripts: int;				# true if scripts used
+	events: list of Lex->Attr;			# event handlers
+	evmask: int;
+	kidinfo: ref Kidinfo;			# if a frameset
+	frameid: int;				# id of document frame
+
+	# info needed to respond to user actions
+	anchors: list of ref Anchor;	# info about all href anchors
+	dests: list of ref DestAnchor;	# info about all destination anchors
+	forms: list of ref Form;		# info about all forms
+	tables: list of ref Table;		# info about all tables
+	maps: list of ref Map;		# info about all maps
+	images: list of ref Item;		# all image items in doc
+
+	new: fn() : ref Docinfo;
+	reset: fn(f: self ref Docinfo);
+};
+
+# Parsing stuff
+
+# Parsing state
+Pstate: adt {
+	skipping: int;			# true when we shouldn't add items
+	skipwhite: int;			# true when we should strip leading space
+	curfont: int;			# font index for current font
+	curfg: int;				# current foreground color
+	curbg: Background;		# current background
+	curvoff: int;			# current baseline offset
+	curul: byte;			# current underline/strike state
+	curjust: byte;			# current justify state
+	curanchor: int;			# current (href) anchor id (if in one), or 0
+	curstate: int;			# current value of item state
+	literal: int;				# current literal state
+	inpar: int;				# true when in a paragraph-like construct
+	adjsize: int;			# current font size adjustment
+	items: ref Item;			# dummy head of item list we're building
+	lastit: ref Item;			# tail of item list we're building
+	prelastit: ref Item;		# item before lastit
+	fntstylestk: list of int;		# style stack
+	fntsizestk: list of int;		# size stack
+	fgstk: list of int;			# text color stack
+	ulstk: list of byte;		# underline stack
+	voffstk: list of int;		# vertical offset stack
+	listtypestk: list of byte;	# list type stack
+	listcntstk: list of int;		# list counter stack
+	juststk: list of byte;		# justification stack
+	hangstk: list of int;		# hanging stack
+
+	new: fn() : ref Pstate;
+};
+
+
+# A source of Items (resulting of HTML parsing).
+# After calling new with a ByteSource (which is past 'gethdr' stage),
+# call getitems repeatedly until get nil.  Errors are signalled by exceptions.
+# Possible exceptions raised:
+#	EXInternal		(start, getitems)
+#	exGeterror	(getitems)
+#	exAbort		(getitems)
+ItemSource: adt
+{
+	ts: ref Lex->TokenSource;	# source of tokens
+	mtype: int;			# media type (TextHtml or TextPlain)
+	doc: ref Docinfo;		# global information about page
+	frame: ref Layout->Frame;	# containing frame
+	psstk: list of ref Pstate;	# local parsing state stack
+	nforms: int;			# used to make formids
+	ntables: int;			# used to make tableids
+	nanchors: int;			# used to make anchor ids
+	nframes: int;			# used to make names for frames
+	curform: ref Form;		# current form (if in one)
+	curmap: ref Map;		# current map (if in one)
+	tabstk: list of ref Table;	# table stack
+	kidstk: list of ref Kidinfo;	# kidinfo stack
+	reqdurl: ref Url->Parsedurl;
+	reqddata: array of byte;
+	toks: array of ref Lex->Token;
+
+	new: fn(bs: ref CharonUtils->ByteSource, f: ref Layout->Frame, mtype: int) : ref ItemSource;
+	getitems: fn(is: self ref ItemSource) : ref Item;
+};
+
+init: fn(cu: CharonUtils);
+trim_white: fn(data: string): string;
+};
--- /dev/null
+++ b/appl/charon/charon.b
@@ -1,0 +1,2171 @@
+implement Charon;
+
+include "common.m";
+include "debug.m";
+
+sys: Sys;
+CU: CharonUtils;
+	ByteSource, MaskedImage, CImage, ImageCache, ReqInfo, Header, 
+	ResourceState, config, max, min, X: import CU;
+
+D: Draw;
+	Point, Rect, Font, Image, Display, Screen: import D;
+
+S: String;
+U: Url;
+	Parsedurl: import U;
+L: Layout;
+	Frame, Loc, Control: import L;
+I: Img;
+	ImageSource: import I;
+
+B: Build;
+	Item, Dimen: import B;
+
+E: Events;
+	Event: import E;
+
+J: Script;
+
+G: Gui;
+
+C : Ctype;
+
+include "sh.m"; 
+
+# package up info related to a navigation command
+GoSpec: adt {
+	kind: int;				# GoNormal, etc.
+	url: ref Parsedurl;		# destination (absolute)
+	meth: int;				# HGet or HPost
+	body: string;			# used if HPost
+	target: string;			# name of target frame
+	auth: string;			# optional auth info
+	histnode: ref HistNode;	# if kind is GoHistnode
+
+	newget: fn(kind: int, url: ref Parsedurl, target: string) : ref GoSpec;
+	newpost: fn(url: ref Parsedurl, body, target: string) : ref GoSpec;
+	newspecial: fn(kind: int, histnode: ref HistNode) : ref GoSpec;
+	equal: fn(a: self ref GoSpec, b: ref GoSpec) : int;
+};
+
+GoNormal, GoReplace, GoLink, GoHistnode, GoSettext: con iota;
+
+# Information about a set of frames making up the screen
+DocConfig: adt {
+	framename: string;		# nonempty, except possibly for topconfig
+	title: string;
+	initconfig: int;			# true unless this is a frameset and some subframe changed
+	gospec: cyclic ref GoSpec;
+	# TODO: add current y pos and form field values
+
+	equal: fn(a: self ref DocConfig, b: ref DocConfig) : int;
+	equalarray: fn(a1: array of ref DocConfig, a2: array of ref DocConfig) : int;
+};
+
+# Information about a particular screen configuration
+HistNode: adt {
+	topconfig: cyclic ref DocConfig;			# config of top (whole doc, or frameset root)
+	kidconfigs: cyclic array of ref DocConfig;	# configs for kid frames (if a frameset)
+	preds: cyclic list of ref HistNode;	# edges in (via normal navigation)
+	succs: cyclic list of ref HistNode;	# edges out (via normal navigation)
+	findid : int;
+	findchain : cyclic list of ref HistNode;
+
+	addedge: fn(a: self ref HistNode, b: ref HistNode, atob: int);
+	copy: fn(a: self ref HistNode) : ref HistNode;
+};
+
+History: adt {
+	h: array of ref HistNode;	# all visited HistNodes, in LRU order
+	n: int;				# h[0:n] is valid part of h
+	findid : int;
+
+	add: fn(h: self ref History, f: ref Frame, g: ref GoSpec, navkind: int);
+	update: fn(h: self ref History, f: ref Frame);
+	find: fn(h: self ref History, k: int) : ref HistNode;
+	print: fn(h: self ref History);
+	histinfo: fn(h: self ref History) : (int, string, string, string);
+	findurl: fn(h: self ref History, s: string) : ref HistNode;
+};
+
+# Authentication strings
+AuthInfo: adt {
+	realm: string;
+	credentials: string;
+};
+
+auths: list of ref AuthInfo = nil;
+
+history : ref History;
+keyfocus: ref Control;
+mouseover: ref B->Anchor;
+mouseoverfr: ref Frame;
+grabctl: ref Control;
+popupctl: ref Control;
+
+SP : con 8;			# a spacer for between controls
+SP2 : con 4;			# half of SP
+SP3 : con 2;
+pgrp := 0;
+gopgrp := 0;
+dbg := 0;
+warn := 0;
+dbgres := 0;
+doscripts := 0;
+
+top, curframe: ref Frame;
+mainwin: ref Image;
+p0 := Point(0,0);
+
+context: ref Draw->Context;
+opener: chan of string;
+
+sendopener(s: string)
+{
+	if(opener != nil){
+		alt{
+			opener <- = s =>
+				;
+			* =>
+				;
+		}
+	}
+}
+
+hasopener(): int
+{
+	return opener != nil;
+}
+
+init(ctxt: ref Draw->Context, argl: list of string)
+{
+	chctxt := ref Context(ctxt, argl, nil, nil, nil);
+	initc(chctxt);
+}
+
+initc(ctxt: ref Context)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil)
+		fatalerror("bad args\n");
+	opener = ctxt.c;
+	argl := ctxt.args;
+	context = ctxt.ctxt;
+
+	(retval, nil) := sys->stat("/net/tcp");
+	if(retval < 0)
+		sys->bind("#I", "/net", sys->MREPL);
+	(retval, nil) = sys->stat("/net/cs");
+	if(retval < 0)
+		startcs();
+
+	pgrp = sys->pctl(sys->NEWPGRP, nil);
+	CU = load CharonUtils CharonUtils->PATH;
+	if(CU == nil)
+		fatalerror(sys->sprint("Couldn't load %s\n", CharonUtils->PATH));
+
+	ech := chan of ref Event;
+	errpath := CU->init(load Charon SELF, CU, argl, ech, ctxt.cksrv, ctxt.ckclient);
+	if(errpath != "")
+		fatalerror(sys->sprint("Couldn't load %s\n", errpath));
+	ctxt = nil;
+
+	sys = load Sys Sys->PATH;
+	D = load Draw Draw->PATH;
+	S = load String String->PATH;
+	U = load Url Url->PATH;
+	if (U != nil)
+		U->init();
+	E = CU->E;
+	L = CU->L;
+	I = CU->I;
+	B = CU->B;
+	J = CU->J;
+	G = CU->G;
+	C = CU->C;
+
+	dbg = int (CU->config).dbg['d'];
+	warn = dbg ||  int (CU->config).dbg['w'];
+	dbgres = int (CU->config).dbg['r'];
+	doscripts = (CU->config).doscripts && J != nil;
+	if(dbg && (CU->config).dbgfile != "") {
+		dfile := sys->create((CU->config).dbgfile, sys->OWRITE, 8r666);
+		if(dfile != nil) {
+			sys->dup(dfile.fd, 1);
+		}
+	}
+	curres := ResourceState.cur();
+	newres: ResourceState;
+	if(dbgres) {
+		(CU->startres).print("starting resources");
+		curres = ResourceState.cur();
+	}
+
+	context = G->init(context, CU);
+	if(dbgres) {
+		newres = ResourceState.cur();
+		newres.since(curres).print("difference after G->init (made screen windows)");
+		curres = newres;
+	}
+	mainwin = G->mainwin;
+
+	# L->init() was deferred until after G was inited
+	L->init(CU);
+	if(dbgres) {
+		newres = ResourceState.cur();
+		newres.since(curres).print("difference after L->init (loaded Build, Lex)");
+		curres = newres;
+	}
+	(CU->imcache).init();
+	if(dbgres) {
+		newres = ResourceState.cur();
+		newres.since(curres).print("difference after (CU->imcache).init");
+		curres = newres;
+	}
+	start();
+	if(J != nil)
+		J->frametreechanged(top);
+	startpage := config.starturl;
+	g := GoSpec.newget(GoNormal, CU->makeabsurl(startpage), "_top");
+	if(dbgres) {
+		newres = ResourceState.cur();
+		newres.since(curres).print("difference after initial configure");
+		curres = newres;
+	}
+	spawn plumbwatch();
+	spawn go(g);
+
+	sendopener("B");
+
+Forloop:
+	for(;;) {
+		ev := <- ech;
+
+		if(dbg > 1) {
+			pick de := ev {
+			Emouse =>
+				if(dbg > 2 || de.mtype != E->Mmove)
+					sys->print("%s\n", ev.tostring());
+			* =>
+				sys->print("%s\n", ev.tostring());
+			}
+		}
+		pick  e := ev {
+		Ekey =>
+			g = nil;
+			case e.keychar {
+			E->Kdown =>
+				curframe.yscroll(L->CAscrollpage, -1);
+			E->Kup =>
+				curframe.yscroll(L->CAscrollpage, 1);
+			E->Khome =>
+				curframe.yscroll(L->CAscrollpage, -10000);
+			E->Kend => 
+				curframe.yscroll(L->CAscrollpage, 10000);	
+			E->Kaup =>
+				curframe.yscroll(L->CAscrollline, -1);
+			E->Kadown => 
+				curframe.yscroll(L->CAscrollline, 1);	
+			* =>
+				handlekey(e);
+			}
+		Emouse =>
+			g = handlemouse(e);
+		Ereshape =>
+			mainwin = G->mainwin;
+			redraw(1);
+			curframe = top;
+			g = GoSpec.newspecial(GoHistnode, history.find(0));
+		Equit =>
+			break Forloop;
+		Estop =>
+			if(gopgrp != 0)
+				stop();
+			g = nil;
+		Eback =>
+			g = GoSpec.newspecial(GoHistnode, history.find(-1));
+		Efwd =>
+			g = GoSpec.newspecial(GoHistnode, history.find(1));
+		Eform =>
+			formaction(e.frameid, e.formid, e.ftype, 0);
+			g = nil;
+		Eformfield =>
+			formfieldaction(e.frameid, e.formid, e.fieldid, e.fftype);
+			g = nil;
+		Ego =>
+			case e.gtype {
+			E->EGnormal =>
+				url := CU->makeabsurl(e.url);
+				if (url != nil)
+					g = GoSpec.newget(GoNormal,url, e.target);
+				else
+					g = nil;
+			E->EGreplace =>
+				g = GoSpec.newget(GoReplace, U->parse(e.url), e.target);
+			E->EGreload =>
+				g = GoSpec.newspecial(GoHistnode, history.find(0));
+			E->EGforward =>
+				g = GoSpec.newspecial(GoHistnode, history.find(1));
+			E->EGback =>
+				g = GoSpec.newspecial(GoHistnode, history.find(-1));
+			E->EGdelta =>
+				g = GoSpec.newspecial(GoHistnode, history.find(e.delta));
+			E->EGlocation =>
+				g = GoSpec.newspecial(GoHistnode, history.findurl(e.url));
+			}
+		Esubmit =>
+			if(e.subkind == CU->HGet)
+				g = GoSpec.newget(GoNormal, e.action, e.target);
+			else {
+				g = GoSpec.newpost(e.action, e.data, e.target);
+			}
+		Escroll =>
+			f := findframe(top, e.frameid);
+			if (f != nil)
+				f.scrollabs(e.pt);
+			g = nil;
+		Escrollr =>
+			f := findframe(top, e.frameid);
+			if (f != nil)
+				f.scrollrel(e.pt);
+			g = nil;
+		Esettext =>
+			f := findframe(top, e.frameid);
+			if (f != nil)
+				g = ref GoSpec (GoSettext, e.url, 0, e.text, f.name, "", nil);
+		Elostfocus =>
+			setfocus(nil);
+			g = nil;
+		Edismisspopup =>
+			if (popupctl != nil)
+				setfocus(popupctl.donepopup());
+			popupctl = nil;
+			grabctl = nil;
+		}
+
+		if (g == nil)
+			continue;
+
+		if (g.kind != GoSettext) {
+			if (g.url != nil) {
+				scheme := g.url.scheme;
+				if (scheme == "javascript") {
+					if (doscripts)
+						spawn dojsurl(g);
+					continue;
+				}
+				if (!CU->schemeok(scheme)) {
+					url := g.url.tostring();
+					if (plumbsend(url, "url") == -1)
+						G->setstatus(X("bad URL", "gui")+": "+url);
+					continue;
+				}
+			}
+		}
+
+		if(gopgrp != 0)
+			stop();
+		spawn go(g);
+	}
+	finish();
+}
+
+mkprog(c: Command, ctxt: ref Draw->Context, args: list of string)
+{
+	sys->pctl(Sys->NEWPGRP|Sys->NEWFD, list of {0, 1, 2});
+	c->init(ctxt, args);
+}
+
+start()
+{
+	top = Frame.new();
+	curframe = top;
+	history = ref History(nil, 0, 0);
+	
+	keyfocus = nil;
+	mouseover = nil;
+	redraw(1);
+}
+
+redraw(resized: int)
+{
+	im := mainwin;
+	if(resized) {
+#		top.r = im.r.inset(2*L->ReliefBd);
+		top.r = im.r;
+		top.cim = mainwin;
+		top.reset();
+		(CU->imcache).resetlimits();
+	}
+	im.clipr = im.r;
+#	L->drawrelief(im, top.r.inset(-L->ReliefBd), L->ReliefRaised);
+#	L->drawrelief(im, top.r, L->ReliefSunk);
+	L->drawfill(im, top.r, CU->White);
+	G->flush(im.r);
+#	im.clipr = top.r;
+}
+
+# Return a Loc representing a control in the frame f
+frameloc(c: ref Control, f: ref Frame) : ref Loc
+{
+	loc := Loc.new();
+	loc.add(L->LEframe, f.r.min);
+	loc.le[loc.n-1].frame = f;
+	if (c != nil) {
+		loc.add(L->LEcontrol, c.r.min);
+		loc.le[loc.n-1].control = c;
+	}
+	return loc;
+}
+
+resetkeyfocus(f: ref Frame)
+{
+	# determine if focus is in frame f or one of its sub-frames
+	if (keyfocus == nil)
+		return;
+
+	for (focusf := keyfocus.f; focusf != nil; focusf = focusf.parent) {
+		if (focusf == f) {
+			keyfocus = nil;
+			break;
+		}
+	}
+	# current focus not in frameset being modified - leave as is
+}
+
+ctlmouse(e: ref Event.Emouse, ctl, grab: ref Control): ref Control
+{
+	ev := E->SEnone;
+	(action, newgrab) := ctl.domouse(e.p, e.mtype, grab);
+	case (action) {
+	L->CAbuttonpush =>
+		if(doscripts && ctl.ff != nil && ctl.ff.evmask)
+			ev = E->SEonclick;
+		else
+			pushaction(ctl, e.p.sub(ctl.r.min));
+	L->CAkeyfocus =>
+		setfocus(ctl);
+	L->CAchanged =>
+		# Select Formfield - selection has changed
+		ev = E->SEonchange;
+	L->CAselected =>
+		# text input Formfield - text selection has changed
+		ev = E->SEonselect;
+	L->CAdopopup =>
+		popupctl = ctl.dopopup();
+		if (popupctl != nil)
+			setfocus(popupctl);
+	L->CAdonepopup =>
+		setfocus(ctl.donepopup());
+		ev = E->SEonchange;
+		popupctl = nil;
+	}
+	if (doscripts && ctl.ff != nil && (ctl.ff.evmask & ev)) {
+		se := ref E->ScriptEvent(ev, ctl.f.id, ctl.ff.form.formid, ctl.ff.fieldid,
+				-1, -1, e.p.x, e.p.y, 1, nil, nil, 0);
+		J->jevchan <-= se;
+	}
+	return newgrab;
+}
+
+mainwinmouse(e: ref Event.Emouse) : (ref GoSpec, ref Control)
+{
+	p := e.p;
+	g : ref GoSpec;
+	ctl : ref Control;
+	newgrab : ref Control;
+	domouseout := 0;
+	loc : ref Loc;
+	if(mouseover != nil)
+		domouseout = 1;
+
+	loc = top.find(p, nil);
+	if(loc != nil) {
+		if(dbg > 1)
+			loc.print("mouse loc");
+		f := loc.lastframe();
+		hasscripts := f.doc.hasscripts;
+		if(e.mtype != E->Mmove)
+			curframe = f;
+		n1 := loc.n-1;
+		case loc.le[n1].kind {
+		L->LEitem =>
+			it := loc.le[n1].item;
+			if (it.anchorid < 0)
+				break;
+
+			a : ref Build->Anchor = nil;
+			for(al := f.doc.anchors; al != nil; al = tl al) {
+				a = hd al;
+				if(a.index == it.anchorid)
+					break;
+			}
+			if (al == nil)
+				break;
+
+			if(dbg > 1)
+				sys->print("in anchor %d, href=%s\n", a.index, a.href.tostring());
+			if(doscripts && a.evmask) {
+				if(a == mouseover) {
+					domouseout = 0;	# still over same anchor
+				} else if(e.mtype == E->Mmove) {
+					if(domouseout) {
+						if(mouseover.evmask & E->SEonmouseout) {
+							se := ref E->ScriptEvent(E->SEonmouseout, mouseoverfr.id, -1, -1, mouseover.index, -1, 0, 0, 0, nil, nil, 0);
+							J->jevchan <-= se;
+						}
+						domouseout = 0;
+					}
+					mouseover = a;
+					mouseoverfr = f;
+					if(a.evmask & E->SEonmouseover) {
+						se := ref E->ScriptEvent(E->SEonmouseover, f.id, -1, -1, a.index, -1, e.p.x, e.p.y, 0, nil, nil, 0);
+						J->jevchan <-= se;
+					}
+				}
+				if (e.mtype == E->Mlbuttonup || e.mtype == E->Mldrop) {
+					if(a.evmask & E->SEonclick) {
+						se := ref E->ScriptEvent(E->SEonclick, f.id, -1, -1, a.index, -1, 0, 0, 0, nil, nil, 0);
+						J->jevchan <-= se;
+						break;
+					}
+					ctl = nil;
+				}
+			}
+			if(e.mtype == E->Mlbuttonup || e.mtype == E->Mldrop) {
+				g = anchorgospec(it, a, loc.pos);
+				if (g == nil)
+					break;
+			} else if(e.mtype == E->Mmbuttonup) {
+				g = anchorgospec(it, a, loc.pos);
+				if (g == nil)
+					break;
+				url := g.url.tostring();
+				G->setstatus(url);
+				G->snarfput(url);
+				g = nil;
+			}
+		L->LEcontrol =>
+			ctl = loc.le[n1].control;
+		}
+	}
+	if (ctl != nil)
+		newgrab = ctlmouse(e, ctl, nil);
+	if(newgrab == nil && domouseout && doscripts) {
+		if(mouseover.evmask & E->SEonmouseout) {
+			se := ref E->ScriptEvent(E->SEonmouseout,
+				mouseoverfr.id, -1, -1, mouseover.index, -1, 0, 0, 0, nil, nil, 0);
+			J->jevchan <-= se;
+		}
+		mouseoverfr = nil;
+		mouseover = nil;
+	}
+	return (g, newgrab);
+}
+
+dojsurl(g : ref GoSpec)
+{
+	f := curframe;
+	case g.target {
+	"_top" =>
+		f = top;
+	"_self" =>
+		; # curframe is already OK
+	"_parent" =>
+		if(f.parent != nil)
+			f = f.parent;
+	"_blank" =>
+		f = top; # we don't create new browsers...
+	* =>
+		# this is recommended "current practice"
+		f = findnamedframe(f, g.target);
+		if(f == nil) {
+			f = findnamedframe(top, g.target);
+			if(f == nil)
+				f = top;
+		}
+	}
+
+	jev := ref E->ScriptEvent (E->SEscript, f.id, -1, -1, -1, -1, 0, 0, 0, g.url.path, chan of string, 0);
+	J->jevchan <-= jev;
+	v := <- jev.reply;
+	if (v != nil) {
+		ev := ref Event.Esettext(f.id, g.url, v);
+		E->evchan <-= ev;
+	}
+}
+
+# If mouse event results in command to navigate somewhere else,
+# return a GoSpec ref, else nil.
+handlemouse(e: ref Event.Emouse): ref GoSpec
+{
+	g: ref GoSpec;
+	ctl := grabctl;
+	if (popupctl != nil)
+		ctl = popupctl;
+	if (ctl != nil)
+		grabctl = ctlmouse(e, ctl, grabctl);
+	else if (e.p.in(mainwin.r))
+		(g, grabctl) = mainwinmouse(e);
+	return g;
+}
+
+setfocus(newc : ref Control)
+{
+	newf, oldf: ref Frame;
+	if (newc != nil)
+		newf = newc.f;
+
+	oldc := keyfocus;
+	if (oldc != nil)
+		oldf = oldc.f;
+	
+	if (oldc != nil && oldc != newc)
+		oldc.losefocus(1);
+	if (oldf != nil && oldf != newf)
+		oldf.focus(0, 1);
+	if (newf != nil && newf != oldf)
+		newf.focus(1,1);
+	if (newc != nil && newc != oldc)
+		newc.gainfocus(1);
+	keyfocus = newc;
+}
+
+handlekey(e: ref Event.Ekey)
+{
+	c := keyfocus;
+	if (c == nil)
+		return;
+
+	pick ce := c {
+	Centry =>
+		case c.dokey(e.keychar) {
+		L->CAreturnkey =>
+			if(c.ff != nil) {
+				spawn form_submit(c.f, c.ff.form, p0, c, 1);
+				return;
+			}
+		L->CAtabkey =>
+			# if control in a form - move focus to next focus-able control
+			if (c.ff != nil) {
+				found := 0;
+				form := c.ff.form;
+				nextff : ref B->Formfield;
+				for (ffl := form.fields; ffl != nil; ffl = tl ffl) {
+					ff := hd ffl;
+					if (ff == c.ff) {
+						found = 1;
+						continue;
+					}
+					if (ff.ftype == B->Ftext || ff.ftype == B->Fpassword) {
+						if (nextff == nil || found)
+							nextff = ff;
+						if (found)
+							break;
+					}
+				}
+				if (nextff != nil)
+					formfield_focus(c.f, nextff);
+			}
+		}
+	}
+	return;
+}
+
+fileexist(file: string) :int
+{
+		fd := sys->open(file, sys->OREAD);
+		if (fd == nil)
+			return 0;
+		else
+			return 1;
+}
+
+go(g: ref GoSpec)
+{
+	gopgrp = sys->pctl(sys->NEWPGRP, nil);
+	spawn goproc(g);
+
+	# got to make netget the thread with the gopgrp thread,
+	# since it runs until killed, and killing a pgrp needs an active
+	# thread
+	CU->netget();
+}
+
+goproc(g: ref GoSpec)
+{
+	origkind := g.kind;
+	hn : ref HistNode = nil;
+	doctext := "";
+	case origkind {
+	GoNormal or
+	GoReplace or
+	GoSettext =>
+		;
+	GoHistnode =>
+		hn = g.histnode;
+		if(hn == nil)
+			return;
+		g = hn.topconfig.gospec;
+	}
+	case g.target {
+	"_top" =>
+		curframe = top;
+	"_self" =>
+		; # curframe is already OK
+	"_parent" =>
+		if(curframe.parent != nil)
+			curframe = curframe.parent;
+	"_blank" =>
+		curframe = top; # we don't create new browsers...
+	* =>
+		# this is recommended "current practice"
+		curframe = findnamedframe(curframe, g.target);
+		if(curframe == nil) {
+			curframe = findnamedframe(top, g.target);
+			if(curframe == nil)
+				curframe = top;
+		}
+	}
+
+	f := curframe;
+	if(dbg) {
+		sys->print("\n\nGO TO %s\n", g.url.tostring());
+		if(g.target != "_top")
+			sys->print("target frame name=%s\n", f.name);
+	}
+	G->progress <-= (-1, G->Pstart, 0, "");
+	err := "";
+	status := "Done";
+
+	if((origkind == GoNormal || origkind == GoReplace || origkind == GoLink) && g.url.frag != "" 
+			&& f.doc != nil && f.doc.src != nil && CU->urlequal(g.url, f.doc.src))
+		go_local(f, g.url.frag);
+	else {
+		if (g.kind == GoSettext)
+			settext(g, f, g.body);
+		else
+			err = get(g, f, origkind, hn);
+
+		if(doscripts && J->defaultStatus != "")
+			status = J->defaultStatus;
+	}
+	if(err != nil) {
+		status = err;
+		G->progress <-= (-1, G->Perr, 100, err);
+	} else 
+		G->progress <-= (-1, G->Pdone, 0, nil);
+		
+	G->setstatus(status);
+	checkrefresh(f);
+}
+
+settext(g : ref GoSpec, f : ref Frame, text : string) : string
+{
+	sdest := g.url.tostring();
+	G->setstatus(X("Fetching", "gui") + " " + sdest);
+	bs := CU->stringreq(text);
+	G->seturl(sdest);
+	history.add(f, g, GoNormal);
+	resetkeyfocus(f);
+	L->layout(f, bs, 0);
+	if (J != nil)
+		J->framedone(f, f.doc.hasscripts);
+	history.update(f);
+	error := "";
+	if(f.kids != nil) {
+		if(J != nil)
+			J->frametreechanged(f);
+		nkids := len f.kids;
+		kdone := chan of (ref Frame, string);
+		for(kl := f.kids; kl != nil; kl = tl kl) {
+			k := hd kl;
+			if(k.src != nil) {
+				gs := GoSpec.newget(GoNormal, k.src, "_self");
+				if(dbg)
+					sys->print("get child frame %s\n", gs.url.tostring());
+				spawn getproc(gs, k, GoNormal, nil, kdone);
+			}
+		}
+		while (nkids--) {
+			(k, e) := <- kdone;
+			if (error != nil)
+				error = e;
+			checkrefresh(k);
+		}
+	}
+
+	if (J != nil) {
+#this code should be split off as it is duplicated from get()
+		# at this point all sub-frames and images have been loaded
+		# Optimise this! so as only do it if a doc in the frameset
+		# has script/event code
+		J->jevchan <-= ref E->ScriptEvent(E->SEonload, f.id, -1, -1, -1, -1, -1, -1, -1, nil, nil, 0);
+		if (doscripts && f.doc.hasscripts) {
+			for(itl := f.doc.images; itl != nil; itl = tl itl) {
+				it := hd itl;
+				if(it.genattr == nil || !it.genattr.evmask)
+					continue;
+				ev := E->SEnone;
+				pick im := it {
+				Iimage =>
+					case im.ci.complete {
+					# correct to equate these two ?
+					Img->Mimnone or
+					Img->Mimerror =>
+						ev = E->SEonerror;
+					Img->Mimdone =>
+						ev = E->SEonload;
+					}
+					if(im.genattr.evmask & ev)
+						J->jevchan <-= ref E->ScriptEvent(ev, f.id, -1, -1, -1, im.imageid, -1, -1, -1, nil, nil, 0);
+				}
+			}
+		}
+	}
+	return error;
+}
+
+getproc(g: ref GoSpec, f: ref Frame, origkind: int, hn: ref HistNode, done : chan of (ref Frame, string))
+{
+	done <-= (f, get(g, f, origkind, hn));
+}
+
+get(g: ref GoSpec, f: ref Frame, origkind: int, hn: ref HistNode) : string
+{
+	curres, newres: ResourceState;
+	if(dbgres) {
+		(CU->imcache).clear();
+		curres = ResourceState.cur();
+	}
+	sdest := g.url.tostring();
+        G->setstatus(X("Fetching", "gui") + " " + sdest);
+	bsmain : ref ByteSource;
+	hdr : ref Header;
+	ri := ref ReqInfo(g.url, g.meth, array of byte g.body, g.auth, g.target);
+	authtried := 0;
+	realm := "";
+	auth := "";
+	error := "";
+	for(nredirs := 0; ; nredirs++) {
+		bsmain = CU->startreq(ri);
+		error = bsmain.err;
+		if(error != "") {
+			CU->freebs(bsmain);
+			return error;
+		}
+		CU->waitreq(bsmain::nil);
+		error = bsmain.err;
+		if(error != "") {
+			CU->freebs(bsmain);
+			return error;
+		}
+		hdr = bsmain.hdr;
+		(use, e, challenge, newurl) := CU->hdraction(bsmain, 1, nredirs);
+		error = e;
+		if(challenge != nil) {
+			if(authtried) {
+				# we already tried once; give up
+				error = "Need authorization";
+				use = 1;
+			}
+			else {
+				(realm, auth) = getauth(challenge);
+				if(auth != "") {
+					ri.auth = auth;
+					authtried = 1;
+					CU->freebs(bsmain);
+					continue;
+				}
+				else {
+					error = "Need authorization";
+					use = 1;
+				}
+			}
+		}
+		if (error == nil) {
+			if (hdr.code != CU->HCOk)
+				error = CU->hcphrase(hdr.code);
+			if(authtried) {
+				# it succeeded; add to auths list so don't have to ask again
+				auths = ref AuthInfo(realm, auth) :: auths;
+			}
+		}
+		if(newurl != nil) {
+			ri.url = newurl;
+			# some sites (e.g., amazon.com) assume that POST turns into
+			# GET on redirect (maybe this is just http 1.0?)
+			ri.method = CU->HGet;
+			CU->freebs(bsmain);
+			continue;
+		}
+		if(use == 0) {
+			CU->freebs(bsmain);
+			return error;
+		}
+		break;
+	}
+	if(dbgres > 1) {
+		newres = ResourceState.cur();
+		newres.since(curres).print("resources to get header");
+		curres = newres;
+	}
+	if(hdr.mtype == CU->TextHtml || hdr.mtype == CU->TextPlain ||
+					I->supported(hdr.mtype)) {
+		G->seturl(sdest);
+		history.add(f, g, origkind);
+		resetkeyfocus(f);
+		srcdata := L->layout(f, bsmain, origkind == GoLink);
+		if (J != nil)
+			J->framedone(f, f.doc.hasscripts);
+		history.update(f);
+		if(dbgres > 1) {
+			newres = ResourceState.cur();
+			newres.since(curres).print("resources to get page and do layout");
+			curres = newres;
+		}
+		if(f.kids != nil) {
+			if(J != nil)
+				J->frametreechanged(f);
+			i := 0;
+			nkids := len f.kids;
+			kdone := chan of (ref Frame, string);
+			for(kl := f.kids; kl != nil; kl = tl kl) {
+				k := hd kl;
+				if(k.src != nil) {
+					if(hn != nil)
+						gs := hn.kidconfigs[i].gospec;
+					else
+						gs = GoSpec.newget(GoNormal, k.src, "_self");
+					if(dbg)
+						sys->print("get child frame %s\n", gs.url.tostring());
+					gokind := GoLink;
+					if (origkind != GoLink)
+						gokind = GoNormal;
+					spawn getproc(gs, k, gokind, nil, kdone);
+				}
+				i++;
+			}
+			while (nkids--) {
+				(k, err) := <- kdone;
+				if (error == nil)
+					# we currently only capture the first error
+					# as we only have one palce to report it
+					error = err;
+				checkrefresh(k);
+			}
+		}
+
+		if (J != nil) {
+			# at this point all sub-frames and images have been loaded
+			J->jevchan <-= ref E->ScriptEvent(E->SEonload, f.id, -1, -1, -1, -1, -1, -1, -1, nil, nil, 0);
+			if (doscripts && f.doc.hasscripts) {
+				for(itl := f.doc.images; itl != nil; itl = tl itl) {
+					it := hd itl;
+					if(it.genattr == nil || !it.genattr.evmask)
+						continue;
+					ev := E->SEnone;
+					pick im := it {
+					Iimage =>
+						case im.ci.complete {
+						# correct to equate these two ?
+						Img->Mimnone or
+						Img->Mimerror =>
+							ev = E->SEonerror;
+						Img->Mimdone =>
+							ev = E->SEonload;
+						}
+						if(im.genattr.evmask & ev)
+							J->jevchan <-= ref E->ScriptEvent(ev, f.id, -1, -1, -1, im.imageid, -1, -1, -1, nil, nil, 0);
+					}
+				}
+			}
+		}
+
+		if(g.url.frag != "")
+			go_local(f, g.url.frag);
+	}
+	else {
+		error = X("Unsupported media type", "gui")+ " "+CU->mnames[hdr.mtype];
+		# Optionally put a save-as dialog up here.
+		if((CU->config).offersave)
+			dosaveas(bsmain);
+		CU->freebs(bsmain);
+	}
+	if(dbgres == 1) {
+		newres = ResourceState.cur();
+		newres.since(curres).print("resources to do page");
+		curres = newres;
+	}
+	return error;
+}
+
+# Scroll frame f so that destination hyperlink loc is at top of view
+go_local(f: ref Frame, loc: string)
+{
+	if(dbg)
+		sys->print("go to local destination %s\n", loc);
+	for(ld := f.doc.dests; ld != nil; ld = tl ld) {
+		d := hd ld;
+		if(d.name == loc) {
+			dloc := f.find(p0, d.item);
+			if(dloc == nil) {
+				if(warn)
+					sys->print("couldn't find item for destination anchor %s\n", loc);
+				return;
+			}
+			p := f.sptolp(dloc.le[dloc.n-1].pos);
+			f.yscroll(L->CAscrollabs, p.y);
+			return;
+		}
+	}
+	# special location names...
+	l := S->tolower(loc);
+	if(l == "top" || l == "home"){
+		f.yscroll(L->CAscrollabs, 0);
+		return;
+	}
+	if(l == "end" || l=="bottom"){
+		f.yscroll(L->CAscrollabs, f.totalr.max.y);
+		return;
+	}
+	if(warn)
+		sys->print("couldn't find destination anchor %s\n", loc);
+}
+
+stripwhite(s: string) : string
+{
+	j := 0;
+	n := len s;
+	for(i := 0; i < n; i++) {
+		c := s[i];
+		if(c < C->NCTYPE && C->ctype[c]==C->W)
+			continue;
+		s[j++] = c;
+	}
+	if(j < n)
+		s = s[0:j];
+	return s;
+}
+
+# If refresh has been set in f (i.e., client pull),
+# pause the appropriate amount of time and then go to new place
+checkrefresh(f: ref Frame)
+{
+	if(f.doc != nil && f.doc.refresh != "") {
+		seconds := 0;
+		url : ref Parsedurl = nil;
+		refresh := stripwhite(f.doc.refresh);
+		(n, l) := sys->tokenize(refresh, ";");
+		if(n > 0) {
+			seconds = int hd l;
+			if(n > 1) {
+				s := hd tl l;
+				if(len s > 4 && S->tolower(s[0:4]) == "url=") {
+					url = U->mkabs(U->parse(s[4:]), f.doc.base);
+				}
+			}
+		}
+		spawn dorefresh(f, seconds, url);
+	}
+}
+
+dorefresh(f: ref Frame, seconds: int, url: ref Parsedurl)
+{
+	sys->sleep(seconds * 1000);
+	e : ref Event;
+	if(url == nil)
+		e = ref Event.Ego(nil, f.name, 0, E->EGreload);
+	else
+		e = ref Event.Ego(url.tostring(), f.name, 0, E->EGnormal);
+	E->evchan <-= e;
+}
+
+# Do depth first search from f, looking for frame with given name.
+findnamedframe(f: ref Frame, name: string) : ref Frame
+{
+	if(f.name == name)
+		return f;
+	for(l := f.kids; l != nil; l = tl l) {
+		k := hd l;
+		a := findnamedframe(k, name);
+		if(a != nil)
+			return a;
+	}
+	return nil;
+}
+
+# Similar, but look for frame id, starting from f
+findframe(f: ref Frame, id: int) : ref Frame
+{
+	if(f.id == id)
+		return f;
+	for(l := f.kids; l != nil; l = tl l) {
+		k := hd l;
+		a := findframe(k, id);
+		if(a != nil)
+			return a;
+	}
+	return nil;
+}
+
+# Return Gospec resulting from button up in anchor a, at offset pos inside item it.
+anchorgospec(it: ref Item, a: ref B->Anchor, p: Point) : ref GoSpec
+{
+	g : ref GoSpec;
+	u := a.href;
+	target := a.target;
+	pick i := it {
+	Iimage =>
+		ci := i.ci;
+		if(ci.mims != nil) {
+			if(i.map != nil) {
+				(u, target) = findhit(i.map, p, ci.width, ci.height);
+			}
+			else if(u != nil && u.scheme != "javascript" && (it.state&B->IFsmap)) {
+				# copy u, add ?x,y
+				x := min(max(p.x-(int i.hspace + int i.border),0),ci.width-1);
+				y := min(max(p.y-(int i.vspace + int i.border),0),ci.height-1);
+				u = ref *a.href;
+				u.query = string x + "," + string y;
+			}
+		}
+	Ifloat =>
+		return anchorgospec(i.item, a, p);
+	}
+
+	if(u != nil)
+		g = GoSpec.newget(GoLink, u, target);
+	return g;
+}
+
+# Control c has been pushed.
+# Find the form it is in and perform required action (reset, or submit).
+pushaction(c: ref Control, pt: Point)
+{
+	pick b := c {
+	Cbutton =>
+		ff := b.ff;
+		f := b.f;
+		if(ff != nil) {
+			case ff.ftype {
+			B->Fsubmit or B->Fimage =>
+				spawn form_submit(c.f, ff.form, pt, c, 1);
+			B->Freset =>
+				spawn form_reset(f, ff.form);
+			}
+		}
+	}
+}
+
+# if onsubmit==1, then raise onsubmit event (if handler present)
+form_submit(fr: ref Frame, frm: ref B->Form, p: Point, submitctl: ref Control, onsubmit: int)
+{
+	submitfield : ref B->Formfield;
+	if (submitctl != nil)
+		submitfield = submitctl.ff;
+
+	if(submitctl != nil && tagof(submitctl) == tagof(Control.Centry)) {
+		# Via CR, so only submit if there is a submit button (first one is the default)
+		firstsubmit : ref B->Formfield;
+		for(l := frm.fields; l != nil; l = tl l) {
+			f := hd l;
+			if (f.ftype == B->Fsubmit) {
+				firstsubmit = f;
+				break;
+			}
+		}
+		if (firstsubmit == nil)
+			return;
+		submitfield = firstsubmit;
+	}
+	if(doscripts && fr.doc.hasscripts && onsubmit && (frm.evmask & E->SEonsubmit)) {
+		c := chan of string;
+		J->jevchan <-= ref E->ScriptEvent(E->SEonsubmit, fr.id, frm.formid, -1, -1, -1, -1, -1, -1, nil, c, 0);
+		if(<-c == nil)
+			return;
+	}
+	v := "";
+	sep := "";
+	radiodone : list of string = nil;
+floop:
+	for(l := frm.fields; l != nil; l = tl l) {
+		f := hd l;
+		if(f.name == "")
+			continue;
+		val := "";
+		c: ref Control;
+		if(f.ctlid >= 0)
+			c = fr.controls[f.ctlid];
+		case f.ftype {
+			B->Ftext or B->Fpassword or B->Ftextarea =>
+				if(c != nil)
+					pick e := c {
+					Centry =>
+						val = e.s;
+					}
+				if(val != "" && f.name == "_ISINDEX_") {
+					# just the index terms after the "?"
+					if(sep != "")
+						v = v + sep;
+					sep = "&";
+					v = v + ucvt(val);
+					break floop;
+				}
+			B->Fcheckbox or B->Fradio =>
+				if(f.ftype == B->Fradio) {
+					# Need the following to catch case where there
+					# is more than one radiobutton with the same name
+					# and value.
+					for(rl := radiodone; rl != nil; rl = tl rl)
+						if(hd rl == f.name)
+							continue floop;
+				}
+				checked := 0;
+				if(c != nil)
+					pick cb := c {
+					Ccheckbox =>
+						checked = cb.flags & L->CFactive;
+					}
+				if(checked) {
+					val = f.value;
+					if(f.ftype == B->Fradio)
+						radiodone = f.name :: radiodone;
+				}
+				else
+					continue;
+			B->Fhidden =>
+				val = f.value;
+			B->Fsubmit =>
+				if(submitctl != nil && f == submitctl.ff && f.name != "_no_name_submit_")
+					val = f.value;
+				else
+					continue;
+			B->Fselect =>
+				if(c != nil)
+					pick s := c {
+					Cselect =>
+						for(i := 0; i < len s.options; i++) {
+							if(s.options[i].selected) {
+								if(sep != "")
+									v = v + sep;
+								sep = "&";
+								v = v + ucvt(f.name) + "=" + ucvt(s.options[i].value);
+							}
+						}
+						continue;
+					}
+			B->Fimage =>
+				if(submitctl != nil && f == submitctl.ff) {
+					if(sep != "")
+						v = v + sep;
+					sep = "&";
+					v = v + ucvt(f.name + ".x") + "=" + ucvt(string max(p.x,0))
+						+ sep + ucvt(f.name + ".y") + "=" + ucvt(string max(p.y,0));
+					continue;
+				}
+		}
+#		if(val != "") {
+			if(sep != "")
+				v = v + sep;
+			sep = "&";
+			v = v + ucvt(f.name) + "=" + ucvt(val);
+#		}
+	}
+	action := ref *frm.action;
+	if (frm.method == CU->HGet) {
+		if (action.query != "" && v != "")
+			action.query += "&";
+		action.query += v;
+		v = "";
+	}
+#	action.query = v;
+	E->evchan <-= ref Event.Esubmit(frm.method, action, v, frm.target);
+}
+
+hexdigit := "0123456789ABCDEF";
+urlchars := array [128] of {
+	'a' to 'z' => byte 1,
+	'A' to 'Z' => byte 1,
+	'0' to '9' => byte 1,
+	'-' or '/' or '$' or '_' or '@' or '.' or '!' or '*' or '\'' or '(' or ')' => byte 1,
+	* => byte 0
+};
+
+ucvt(s: string): string
+{
+	b := array of byte s;
+	u := "";
+	for(i := 0; i < len b; i++) {
+		c := int b[i];
+		if (c < len urlchars && int urlchars[c])
+			u[len u] = c;
+		else if(c == ' ')
+			u[len u] = '+';
+		else {
+			u[len u] = '%';
+			u[len u] = hexdigit[(c>>4)&15];
+			u[len u] = hexdigit[c&15];
+		}
+	}
+	return u;
+}
+
+form_reset(fr: ref Frame, frm: ref B->Form)
+{
+	if(doscripts && fr.doc.hasscripts && (frm.evmask & E->SEonreset)) {
+		c := chan of string;
+		J->jevchan <-= ref E->ScriptEvent(E->SEonreset, fr.id, frm.formid, -1, -1, -1, -1, -1, -1, nil, c, 0);
+		if(<-c == nil)
+			return;
+	}
+	for(fl := frm.fields; fl != nil; fl = tl fl) {
+		a := hd fl;
+		if(a.ctlid >= 0)
+			fr.controls[a.ctlid].reset();
+	}
+#	fr.cim.flush(D->Flushnow);
+}
+
+formaction(frameid, formid, ftype, onsubmit: int)
+{
+	if(dbg > 1)
+		sys->print("formaction %d %d %d %d\n", frameid, formid, ftype, onsubmit);
+	f := findframe(top, frameid);
+	if(f != nil) {
+		d := f.doc;
+		if(d != nil) {
+			for(fl := d.forms; fl != nil; fl = tl fl) {
+				frm := hd fl;
+				if(frm.formid == formid) {
+					if(ftype == E->EFsubmit)
+						spawn form_submit(f, frm, Point(0,0), nil, onsubmit);
+					else
+						spawn form_reset(f, frm);
+				}
+			}
+		}
+	}
+}
+
+formfield_blur(f: ref Frame, ff: ref B->Formfield)
+{
+	if(ff.ftype != B->Fhidden) {
+		c := f.controls[ff.ctlid];
+		if(!(c.flags & L->CFhasfocus))
+			return;
+		# lose focus quietly - don't raise "onblur" event for the given control
+		c.losefocus(0);
+		setfocus(nil);
+	}
+}
+
+formfield_focus(f: ref Frame, ff: ref B->Formfield)
+{
+	if(ff.ftype != B->Fhidden) {
+		c := f.controls[ff.ctlid];
+		if(c.flags & L->CFhasfocus)
+			return;
+		# gain focus quietly - don't raise "onfocus" event for the given control
+		c.gainfocus(0);
+		setfocus(c);
+	}
+}
+
+# simulate a mouse click, but don't trigger onclick event
+formfield_click(f: ref Frame, frm: ref B->Form, ff: ref B->Formfield)
+{
+	c := f.controls[ff.ctlid];
+	case ff.ftype {
+	B->Fcheckbox or
+	B->Fradio or
+	B->Fbutton =>
+		c.domouse(p0, E->Mlbuttonup, nil);
+	B->Fsubmit =>
+		spawn form_submit(f, frm, p0, c, 1);
+	B->Freset =>
+		spawn form_reset(f, frm);
+	}
+}
+
+formfield_select(f: ref Frame, ff: ref B->Formfield)
+{
+	case ff.ftype {
+	B->Ftext or
+	B->Fselect or
+	B->Ftextarea =>
+		ctl := f.controls[ff.ctlid];
+		pick c := ctl {
+		Centry =>
+			c.sel = (0, len c.s);
+			ctl.draw(1);
+		}
+	}
+}
+
+formfieldaction(frameid, formid, fieldid, fftype: int)
+{
+	if(dbg > 1)
+		sys->print("formfieldaction %d %d %d %d\n", frameid, formid, fieldid, fftype);
+	f := findframe(top, frameid);
+	if(f == nil || f.doc == nil)
+		return;
+
+	# find form in frame
+	frm : ref B->Form;
+	for(fl := f.doc.forms; fl != nil; fl = tl fl) {
+		if((hd fl).formid == formid) {
+			frm = hd fl;
+			break;
+		}
+	}
+	if(frm == nil)
+		return;
+
+	# find formfield in form
+	ff : ref B->Formfield;
+	for(ffl := frm.fields; ffl != nil; ffl = tl ffl) {
+		if((hd ffl).fieldid == fieldid) {
+			ff = hd ffl;
+			break;
+		}
+	}
+	if(ff == nil || ff.ctlid < 0)
+		return;
+
+	# perform action
+	case fftype {
+	E->EFFblur =>
+		formfield_blur(f, ff);
+	E->EFFfocus =>
+		formfield_focus(f, ff);
+	E->EFFclick =>
+		formfield_click(f, frm, ff);
+	E->EFFselect =>
+		formfield_select(f, ff);
+	E->EFFredraw =>
+		c := f.controls[ff.ctlid];
+		pick ctl := c {
+		Cselect =>
+			sel := 0;
+			for (i := 0; i < len ctl.options; i++) {
+				if (ctl.options[i].selected) {
+					sel = i;
+					break;
+				}
+			}
+			if (sel > len ctl.options - ctl.nvis)
+				sel = len ctl.options - ctl.nvis;
+			ctl.first = sel;
+		}
+		c.draw(1);
+	}
+}
+
+# Find hit in a local map
+findhit(map: ref B->Map, p: Point, w, h: int) : (ref Parsedurl, string)
+{
+	x := p.x;
+	y := p.y;
+	dflt : ref Parsedurl = nil;
+	dflttarg := "";
+	for(al := map.areas; al != nil; al = tl al) {
+		a := hd al;
+		c := a.coords;
+		nc := len c;
+		x1 := 0;
+		y1 := 0;
+		x2 := 0;
+		y2 := 0;
+		if(nc >= 2) {
+			x1 = d2pix(c[0], w);
+			y1= d2pix(c[1], h);
+			if(nc > 2) {
+				x2 = d2pix(c[2], w);
+				if(nc > 3)
+					y2 = d2pix(c[3], h);
+			}
+		}
+		hit := 0;
+		case a.shape {
+		"rect" or "rectangle" =>
+			if(nc == 4)
+				hit = x1 <= x && x <= x2 &&
+					y1 <= y && y <= y2;
+		"circ" or "circle" =>
+			if(nc == 3) {
+				xd := x - x1;
+				yd := y - y1;
+				hit = xd*xd + yd*yd <= x2*x2;
+			}
+		"poly" or "polygon" =>
+			np := nc / 2;
+			hit = 0;
+			xr := real x;
+			yr := real y;
+			j := np - 1;
+			for(i := 0; i < np; j = i++) {
+				xi := real d2pix(c[2*i], w);
+				yi := real d2pix(c[2*i+1], h);
+				xj := real d2pix(c[2*j], w);
+				yj := real d2pix(c[2*j+1], h);
+				if ((((yi<=yr) && (yr<yj)) ||
+				     ((yj<=yr) && (yr<yi))) &&
+				    (xr < (xj - xi) * (yr - yi) / (yj - yi) + xi))
+					hit = !hit;
+			}
+		"def" or "default" =>
+			dflt = a.href;
+			dflttarg = a.target;
+		}
+		if(hit)
+			return (a.href, a.target);
+	}
+	return (dflt, dflttarg);
+}
+
+d2pix(d: B->Dimen, tot: int) : int
+{
+	ans := d.spec();
+	if(d.kind() == B->Dpercent)
+		ans = (ans * tot) / 100;
+	return ans;
+}
+GoSpec.newget(kind: int, url: ref Parsedurl, target: string) : ref GoSpec
+{
+	return ref GoSpec(kind, url, CU->HGet, "", target, "", nil);
+}
+
+GoSpec.newpost(url: ref Parsedurl, body, target: string) : ref GoSpec
+{
+	return ref GoSpec(GoNormal, url, CU->HPost, body, target, "", nil);
+}
+
+GoSpec.newspecial(kind: int, hn: ref HistNode) : ref GoSpec
+{
+	return ref GoSpec(kind, nil, 0, "", "", "", hn);
+}
+
+GoSpec.equal(a: self ref GoSpec, b: ref GoSpec) : int
+{
+	if(a.url == nil || b.url == nil)
+		return 0;
+	return CU->urlequal(a.url, b.url) && a.meth == b.meth && a.body == b.body;
+}
+
+DocConfig.equal(a: self ref DocConfig, b: ref DocConfig) : int
+{
+	return a.framename == b.framename && a.gospec.equal(b.gospec);
+}
+
+DocConfig.equalarray(a1: array of ref DocConfig, a2: array of ref DocConfig) : int
+{
+	n := len a1;
+	if(n != len a2)
+		return 0;
+	for(i := 0; i < n; i++) {
+		if(a1[i] == nil || a2[i] == nil)
+			continue;
+		if(!(a1[i]).equal(a2[i]))
+			return 0;
+	}
+	return 1;
+}
+
+# Put b in a.succs (if atob is true) or a.preds (if atob is false)
+# at front of list.
+# If it is already in the list, move it to the front.
+HistNode.addedge(a: self ref HistNode, b: ref HistNode, atob: int)
+{
+	if(atob)
+		oldl := a.succs;
+	else
+		oldl = a.preds;
+	there := 0;
+	for(l := oldl; l != nil; l = tl l)
+		if(hd l == b) {
+			there = 1;
+			break;
+		}
+	if(there)
+		newl := b :: remhnode(oldl, b);
+	else
+		newl = b :: oldl;
+	if(atob)
+		a.succs = newl;
+	else
+		a.preds = newl;
+}
+
+# return copy of l with hn removed (known that hn
+# occurs at most once)
+remhnode(l: list of ref HistNode, hn: ref HistNode) : list of ref HistNode
+{
+	if(l == nil)
+		return nil;
+	hdl := hd l;
+	if(hdl == hn)
+		return tl l;
+	return hdl :: remhnode(tl l, hn);
+}
+
+# Copy of a, with new kidconfigs array (so that it can be changed independent
+# of a), and clear the preds and succs.
+HistNode.copy(a: self ref HistNode) : ref HistNode
+{
+	n := len a.kidconfigs;
+	kc : array of ref DocConfig = nil;
+	if(n > 0) {
+		kc = array[n] of ref DocConfig;
+		for(i := 0; i < n; i++)
+			kc[i] = a.kidconfigs[i];
+	}
+	return ref HistNode(a.topconfig, kc, nil, nil, -1, nil);
+}
+
+# This is called just before layout of f with result of getting g.
+# (we don't yet know doctitle and whether this is a frameset).
+# If navkind is not GoHistnode, update the history graph; but if
+# navkind is GoReplace, replace oldcur with the new HistNode.
+# In any case reorder the history array to put latest last in array.
+History.add(h: self ref History, f: ref Frame, g: ref GoSpec, navkind: int)
+{
+	if(len h.h <= h.n) {
+		newh := array[len h.h + 20] of ref HistNode;
+		newh[0:] = h.h;
+		h.h = newh;
+	}
+	oldcur : ref HistNode;
+	if(h.n > 0)
+		oldcur = h.h[h.n-1];
+	dc := ref DocConfig(f.name, g.url.tostring(), navkind != GoHistnode, g);
+	hnode := ref HistNode(dc, nil, nil, nil, -1, nil);
+	if(f == top) {
+		g.target = "_top";
+	}
+	else if(oldcur != nil) {
+		# oldcur should be a frameset and f should be a kid in it
+		kidpos := -1;
+		for(i := 0; i < len oldcur.kidconfigs; i++) {
+			kc := oldcur.kidconfigs[i];
+			if(kc != nil && kc.framename == f.name) {
+				kidpos = i;
+				break;
+			}
+		}
+		if(kidpos == -1) {
+			if(dbg)
+				sys->print("history botch\n");
+		}
+		else {
+			hnode = oldcur.copy();
+			hnode.kidconfigs[kidpos] = dc;
+		}
+	}
+	# see if equivalent node to hnode is already in history
+	hnodepos := -1;
+	for(i := 0; i < h.n; i++) {
+		if(hnode.topconfig.equal(h.h[i].topconfig)) {
+			if((hnode.kidconfigs==nil && h.h[i].topconfig.initconfig) ||
+			   DocConfig.equalarray(hnode.kidconfigs, h.h[i].kidconfigs)) {
+				hnodepos = i;
+				hnode = h.h[i];
+				break;
+			}
+		}
+	}
+	if(hnodepos == -1) {
+		if(navkind == GoReplace && h.n > 0)
+			h.n--;
+		hnodepos = h.n;
+		h.h[h.n++] = hnode;
+	}
+	if(oldcur != nil && hnode != oldcur && navkind != GoHistnode) {
+		oldcur.addedge(hnode, 1);
+		if(navkind != GoReplace)
+			hnode.addedge(oldcur, 0);
+		else if(oldcur.preds != nil)
+			hnode.addedge(hd oldcur.preds, 0);
+	}
+	if(hnodepos != h.n-1) {
+		# move hnode to h.n-1, and shift rest back
+		for(k := hnodepos; k < h.n-1; k++)
+			h.h[k] = h.h[k+1];
+		h.h[h.n-1] = hnode;
+	}
+	G->backbutton(hnode.preds != nil);
+	G->fwdbutton(hnode.succs != nil);
+}
+
+# This is called just after layout of f.
+# Now we can put in correct doctitle, and make kids array if necessary.
+History.update(h: self ref History, f: ref Frame)
+{
+	hnode := h.h[h.n-1];
+	if(f == top) {
+		hnode.topconfig.title = f.doc.doctitle;
+		if(f.kids != nil && hnode.kidconfigs == nil) {
+			kc := array[len f.kids] of ref DocConfig;
+			i := 0;
+			for(l := f.kids; l != nil; l = tl l) {
+				kf := hd l;
+				if(kf.src != nil)
+					kc[i] = ref DocConfig(kf.name, kf.src.tostring(), 1,  GoSpec.newget(GoNormal, kf.src, "_self"));
+				i++;
+			}
+			hnode.kidconfigs = kc;
+		}
+	}
+	else {
+		# hnode should be a frameset and f should be a kid in it
+		for(i := 0; i < len hnode.kidconfigs; i++) {
+			kc := hnode.kidconfigs[i];
+			if(kc != nil && kc.framename == f.name) {
+				hnode.kidconfigs[i].title = f.doc.doctitle;
+				return;
+			}
+		}
+		if(dbg)
+			sys->print("history update botch\n");
+	}
+}
+
+# Find the gokind node (-1==Back, 0==Same, +1==Forward)
+# other gokind values come from JavaScript's History.go(delta)
+History.find(h: self ref History, gokind: int) : ref HistNode
+{
+	if(h.n > 0) {
+		cur := h.h[h.n-1];
+		case gokind {
+		1 =>
+			if(cur.succs != nil)
+				return hd cur.succs;
+		-1 =>
+			if(cur.preds != nil)
+				return hd cur.preds;
+		0 =>
+			return cur;
+		* =>
+# BUG: follows circularities: gives rise to different behaviour to other
+# browsers but maintains the property of find(n) being equivalent to
+# the user pressing the (forward/back) button n times
+
+			h.findid++;
+			while (gokind != 0 && cur != nil) {
+				hn : list of ref HistNode;
+				if (gokind > 0) {
+					gokind--;
+					hn = cur.succs;
+				} else {
+					gokind++;
+					hn = cur.preds;
+				}
+				if (cur.findid == h.findid)
+					hn = cur.findchain;
+				else
+					cur.findid = h.findid;
+				if (hn != nil) {
+					cur.findchain = tl hn;
+					cur = hd hn;
+				} else
+					cur = nil;
+			}
+			return cur;
+		}
+	}
+	return nil;
+}
+
+# for debugging
+History.print(h: self ref History)
+{
+	sys->print("History\n");
+	for(i := 0; i < h.n; i++) {
+		hn := history.h[i];
+		sys->print("Node %d:\n", i);
+		dc := hn.topconfig;
+		sys->print("\tframe=%s, target=%s, url=%s\n", dc.framename, dc.gospec.target, dc.gospec.url.tostring());
+		if(hn.kidconfigs != nil) {
+			for(j := 0; j < len hn.kidconfigs; j++) {
+				dc = hn.kidconfigs[j];
+				if(dc != nil)
+					sys->print("\t\t%d: frame=%s, target=%s, url=%s\n",
+							j, dc.framename, dc.gospec.target, dc.gospec.url.tostring());
+			}
+		}
+		if(hn.preds != nil)
+			printhnodeindices(h, "Preds", hn.preds);
+		if(hn.succs != nil)
+			printhnodeindices(h, "Succs", hn.succs);
+	}
+	sys->print("\n");
+}
+
+# helpers for JavaScript's History object
+History.histinfo(h: self ref History) : (int, string, string, string)
+{
+	length := 0;
+	current, next, previous : string;
+
+	if(h.n > 0) {
+		hn := h.h[h.n-1];
+		length = len hn.succs + len hn.preds + 1;
+		current = hn.topconfig.gospec.url.tostring();
+		if(hn.succs != nil) {
+			fwd := hd hn.succs;
+			next = fwd.topconfig.gospec.url.tostring();
+		}
+		if(hn.preds != nil) {
+			back := hd hn.preds;
+			previous = back.topconfig.gospec.url.tostring();
+		}
+	}
+	return (length, current, next, previous);
+}
+
+histinfo() : (int, string, string, string)
+{
+	return history.histinfo();
+}
+
+# does URL in hn contain s as a substring?
+isurlsubstring(hn: ref HistNode, s: string) : int
+{
+	url := hn.topconfig.gospec.url.tostring();
+	(l, r) := S->splitstrl(url, s);
+	if(r != nil)
+		return 1;
+	return 0;
+}
+
+# for JavaScript's History.go(location)
+# find nearest history entry whose URL contains s as a substring
+# (search forward and backward from current "in parallel"?)
+History.findurl(h: self ref History, s: string) : ref HistNode
+{
+	if(h.n > 0) {
+		hn := h.h[h.n-1];
+		if(isurlsubstring(hn, s))
+			return hn;
+		fwd := hn.succs;
+		back := hn.preds;
+		while(fwd != nil && back != nil) {
+			if(fwd != nil) {
+				if(isurlsubstring(hd fwd, s))
+					return hd fwd;
+				fwd = tl fwd;
+			}
+			if(back != nil) {
+				if(isurlsubstring(hd back, s))
+					return hd back;
+				back = tl back;
+			}
+		}
+	}
+	return nil;
+}
+
+printhnodeindices(h: ref History, label: string, l: list of ref HistNode)
+{
+	sys->print("\t%s:", label);
+	for( ; l != nil; l = tl l) {
+		hn := hd l;
+		for(i := 0; i < h.n; i++) {
+			if(hn == h.h[i]) {
+				sys->print(" %d", i);
+				break;
+			}
+		}
+		if(i == h.n)
+			sys->print(" ?");
+	}
+	sys->print("\n");
+}
+
+dumphistory()
+{
+	fname := config.userdir + "/history.html";
+	fd := sys->create(fname, sys->OWRITE, 8r600);
+	if(fd == nil) {
+		if(warn)
+			sys->print("can't create history file\n");
+		return;
+	}
+	line := "<HEAD><TITLE>History</TITLE>\n<META HTTP-EQUIV=\"content-type\" CONTENT=\"text/html; charset=utf8\">\n</HEAD>\n<BODY>\n";
+	buf := array[Sys->ATOMICIO] of byte;
+	aline := array of byte line;
+	buf[0:] = aline;
+	bufpos := len aline;
+	for(i := history.n-1; i >= 0; i--) {
+		hn := history.h[i];
+		dc := hn.topconfig;
+		line = "<A HREF=" + dc.gospec.url.tostring() + " TARGET=\"_top\">" + dc.title + "</A><BR>\n";
+		if(hn.kidconfigs != nil) {
+			line += "<UL>";
+			for(j := 0; j < len hn.kidconfigs; j++) {
+				dc = hn.kidconfigs[j];
+				if(dc != nil) {
+					line += "<LI><A HREF=" + dc.gospec.url.tostring() +
+						" TARGET=\"" + dc.framename + "\">" +
+						dc.title + "</A>\n";
+				}
+			}
+			line += "</UL>";
+		}
+		aline = array of byte line;
+		if(bufpos + len aline > Sys->ATOMICIO) {
+			sys->write(fd, buf, bufpos);
+			bufpos = 0;
+		}
+		buf[bufpos:] = aline;
+		bufpos += len aline;
+	}
+	if(bufpos > 0)
+		sys->write(fd, buf, bufpos);
+}
+
+# getauth returns the (realm, credentials), with "" for the credentials
+# if we fail in getting authorization for some reason
+getauth(chal: string) : (string, string)
+{
+	if(len chal < 12 || S->tolower(chal[0:12]) != "basic realm=") {
+		if(dbg || warn)
+			sys->print("unrecognized authorization challenge: %s\n", chal);
+		return ("", "");
+	}
+	realm := chal[12:];
+	if(realm[0] == '"')
+		realm = realm[1:len realm - 1];
+	for(al := auths; al != nil; al = tl al) {
+		a := hd al;
+		if(realm == a.realm)
+			return (realm, a.credentials);
+	}
+	
+	c := chan of (int, string);
+	(code, uname, pword) := G->auth(realm);
+	if(code != 1)
+		return (nil, nil);
+	cred := uname + ":" + pword;
+	cred = tobase64(cred);
+	return (realm, cred);
+}
+
+# Convert string to the base64 encoding
+tobase64(a: string) : string
+{
+	n := len a;
+	if(n == 0)
+		return "";
+	out := "";
+	j := 0;
+	i := 0;
+	while(i < n) {
+		x := a[i++] << 16;
+		if(i < n)
+			x |= (a[i++]&255) << 8;
+		if(i < n)
+			x |= (a[i++]&255);
+		out[j++] = c64(x>>18);
+		out[j++] = c64(x>>12);
+		out[j++] = c64(x>> 6);
+		out[j++] = c64(x);
+	}
+	nmod3 := n % 3;
+	if(nmod3 != 0) {
+		out[j-1] = '=';
+		if(nmod3 == 1)
+			out[j-2] = '=';
+	}
+	return out;
+}
+
+c64(c: int) : int
+{
+	v : con "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+	return v[c&63];
+}
+
+dosaveas(bsmain: ref ByteSource)
+{
+	(code, ans) := G->prompt("Save as", nil);
+	if (code == -1)
+		return;
+	if(code == 1 && ans != "") {
+		if(ans[0] != '/')
+			ans = config.userdir + "/" + ans;
+		fd := sys->create(ans, sys->OWRITE, 8r644);
+		if(fd == nil) {
+			G->alert(X("Couldn't create", "gui") + " " + ans);
+			return;
+		}
+		G->setstatus(X("Saving", "gui") + " " + bsmain.hdr.actual.tostring());
+		# TODO: should really use a different protocol that
+		# doesn't require getting whole file before proceeding
+		s := "";
+		while(!bsmain.eof) {
+			CU->waitreq(bsmain::nil);
+			if(bsmain.err != "") {
+				s = bsmain.err;
+				break;
+			}
+		}
+		if(s == "") {
+			flen := bsmain.edata;
+			for(i := 0; i < bsmain.edata; ) {
+				n := sys->write(fd, bsmain.data[i:flen], flen-i);
+				if(n <= 0)
+					break;
+				i += n;
+			}
+			if(i != flen)
+				s = "whole file not written";
+		}
+		if(s == "")
+			s = X("Created", "gui") + " " + ans;
+		G->setstatus(X("Created", "gui") + " " + ans);
+		# G->alert(s);
+	}
+	CU->freebs(bsmain);
+}
+
+fatalerror(msg: string)
+{
+	sys->print("Fatal error: %s\n", msg);
+	finish();
+}
+
+pctoloc(mod: string, pc: int) : string
+{
+	ans := sys->sprint("pc=%d", pc);
+	db := load Debug Debug->PATH;
+	if(db == nil)
+		return ans;
+	Sym : import db;
+	db->init();
+	modname := mod;
+	for(i := 0; i < len mod; i++)
+		if(mod[i] == '[') {
+			modname = mod[0:i];
+			break;
+		}
+	sblname := "";
+	case modname {
+	"Build" =>
+		sblname = "build.sbl";
+	"CharonUtils" =>
+		sblname = "chutils.sbl";
+	"Gui" =>
+		sblname = "gui.sbl";
+	"Img" =>
+		sblname = "img.sbl";
+	"Layout" =>
+		sblname = "layout.sbl";
+	"Lex" =>
+		sblname = "lex.sbl";
+	"Test" =>
+		sblname = "test.sbl";
+	}
+	if(sblname == "")
+		return ans;
+	(sym, nil) := db->sym(sblname);
+	if(sym == nil)
+		return ans;
+	src := sym.pctosrc(pc);
+	if(src == nil)
+		return ans;
+	return sys->sprint("%s:%d", src.start.file, src.start.line);
+}
+
+startcs()
+{
+	cs := load Command "/dis/ndb/cs.dis";
+	if (cs == nil) {
+		sys->print("failed to start cs\n");
+		return;
+	}
+	spawn cs->init(nil, nil);
+	sys->sleep(1000);
+}
+
+startcharon(url: string, c: chan of string)
+{
+	ctxt := ref Context;
+	ctxt.ctxt = context;
+	ctxt.args = "charon" :: url :: nil;
+	ctxt.c = c;
+	ctxt.cksrv = CU->CK;
+	ctxt.ckclient = CU->ckclient;
+	ch := load Charon "/dis/charon.dis";
+	fdl := list of {0, 1, 2};
+	if (CU->ckclient != nil)
+		fdl = (CU->ckclient).fd.fd :: fdl;
+	if(ch != nil){
+		sys->pctl(Sys->NEWPGRP|Sys->NEWFD, fdl);
+		ch->initc(ctxt);
+	}
+}
+
+# Kill all processes spawned by us, and exit
+finish()
+{
+	if (CU != nil) {
+		CU->kill(pgrp, 1);
+		if(gopgrp != 0)
+			CU->kill(gopgrp, 1);
+	}
+	if(plumb != nil)
+		plumb->shutdown();
+	sendopener("E");
+	exit;
+}
+
+include "plumbmsg.m";
+	plumb: Plumbmsg;
+	Msg: import plumb;
+
+plumbwatch()
+{
+	plumb = load Plumbmsg Plumbmsg->PATH;
+	if (plumb == nil)
+		return;
+	if (plumb->init(1, (CU->config).plumbport, 0) == -1) {
+		# try to set up plumbing for sending only
+		if (plumb->init(1, nil, 0) == -1)
+			plumb = nil;
+		return;
+	}
+	while ((m := Msg.recv()) != nil) {
+		if (m.kind == "text") {
+			u := CU->makeabsurl(string m.data);
+			if (u != nil)
+				E->evchan <-= ref Event.Ego(u.tostring(), "_top", 0, E->EGnormal);
+		}
+	}
+}
+
+plumbsend(s, dest: string): int
+{
+	if (plumb == nil)
+		return -1;
+	if (dest != nil)
+		dest = "type="+dest;
+	msg := ref Msg((CU->config).plumbport, nil, "", "text", dest, array of byte s);
+	if (msg.send() < 0)
+		return -1;
+	return 0;
+}
+
+stop()
+{
+	stopped := X("Stopped", "gui");
+	G->progress <-= (-1, G->Paborted, 0, stopped);
+	G->setstatus(stopped);
+	CU->abortgo(gopgrp);
+}
+
+gettop(): ref Layout->Frame
+{
+	return top;
+}
--- /dev/null
+++ b/appl/charon/charon.m
@@ -1,0 +1,20 @@
+Charon : module
+{
+	PATH: con "/dis/charon.dis";
+
+	Context: adt {
+		ctxt: ref Draw->Context;
+		args: list of string;
+		c: chan of string;
+		cksrv: Cookiesrv;
+		ckclient: ref Cookiesrv->Client;
+	};
+
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+	initc: fn(ctxt: ref Context);
+	histinfo: fn(): (int, string, string, string);
+	startcharon: fn(url: string, c: chan of string);
+	hasopener: fn(): int;
+	sendopener: fn(s: string);
+	gettop: fn(): ref Layout->Frame;
+};
--- /dev/null
+++ b/appl/charon/chutils.b
@@ -1,0 +1,2047 @@
+implement CharonUtils;
+
+include "common.m";
+include "transport.m";
+include "date.m";
+include "translate.m";
+
+	date: Date;
+	me: CharonUtils;
+	sys: Sys;
+	D: Draw;
+	S: String;
+	U: Url;
+	T: StringIntTab;
+
+Font : import D;
+Parsedurl: import U;
+convcs : Convcs;
+trans : Translate;
+	Dict : import trans;
+dict : ref Dict;
+
+NCTimeout : con 100000;		# free NC slot after 100 seconds
+UBufsize : con 40*1024;		# initial buffer size for unknown lengths
+UEBufsize : con 1024;		# initial buffer size for unknown lengths, error responses
+
+botchexception := "EXInternal: ByteSource protocol botch";
+bytesourceid := 0;
+crlf : con "\r\n";
+ctype : array of byte;	# local ref to C->ctype[]
+dbgproto : int;
+dbg: int;
+netconnid := 0;
+netconns := array[10] of ref Netconn;
+sptab : con " \t";
+
+THTTP, TFTP, TFILE, TMAX: con iota;
+transports := array[TMAX] of Transport;
+tpaths := array [TMAX] of {
+	THTTP =>	Transport->HTTPPATH,
+	TFTP =>	Transport->FTPPATH,
+	TFILE =>	Transport->FILEPATH,
+};
+
+schemes := array [] of {
+	("http", 	THTTP),
+	("https",	THTTP),
+	("ftp",	TFTP),
+	("file",	TFILE),
+};
+
+ngchan : chan of (int, list of ref ByteSource, ref Netconn, chan of ref ByteSource);
+
+# must track HTTP methods in chutils.m
+# (upper-case, since that's required in HTTP requests)
+hmeth = array[] of { "GET", "POST" };
+
+# following array must track media type def in chutils.m
+# keep in alphabetical order
+mnames = array[] of {
+	"application/msword",
+	"application/octet-stream",
+	"application/pdf",
+	"application/postscript",
+	"application/rtf",
+	"application/vnd.framemaker",
+	"application/vnd.ms-excel",
+	"application/vnd.ms-powerpoint",
+	"application/x-unknown",
+	"audio/32kadpcm",
+	"audio/basic",
+	"image/cgm",
+	"image/g3fax",
+	"image/gif",
+	"image/ief",
+	"image/jpeg",
+	"image/png",
+	"image/tiff",
+	"image/x-bit",
+	"image/x-bit2",
+	"image/x-bitmulti",
+	"image/x-inferno-bit",
+	"image/x-xbitmap",
+	"model/vrml",
+	"multipart/digest",
+	"multipart/mixed",
+	"text/css",
+	"text/enriched",
+	"text/html",
+	"text/javascript",
+	"text/plain",
+	"text/richtext",
+	"text/sgml",
+	"text/tab-separated-values",
+	"text/xml",
+	"video/mpeg",
+	"video/quicktime"
+};
+
+ncstatenames = array[] of {
+	"free", "idle", "connect", "gethdr", "getdata",
+	"done", "err"
+};
+
+hsnames = array[] of {
+	"none", "information", "ok", "redirect", "request error", "server error"
+};
+
+hcphrase(code: int) : string
+{
+	ans : string;
+	case code {
+	HCContinue =>				ans = X("Continue", "http");
+	HCSwitchProto =>			ans = X("Switching Protocols", "http");
+	HCOk =>					ans = X("Ok", "http");
+	HCCreated =>				ans = X("Created", "http");
+	HCAccepted =>				ans = X("Accepted", "http");
+	HCOkNonAuthoritative =>		ans = X("Non-Authoratative Information", "http");
+	HCNoContent =>			ans = X("No content", "http");
+	HCResetContent =>			ans = X("Reset content", "http");
+	HCPartialContent =>			ans = X("Partial content", "http");
+	HCMultipleChoices =>		ans = X("Multiple choices", "http");
+	HCMovedPerm =>			ans = X("Moved permanently", "http");
+	HCMovedTemp =>			ans = X("Moved temporarily", "http");
+	HCSeeOther =>				ans = X("See other", "http");
+	HCNotModified =>			ans = X("Not modified", "http");
+	HCUseProxy =>				ans = X("Use proxy", "http");
+	HCBadRequest =>			ans = X("Bad request", "http");
+	HCUnauthorized =>			ans = X("Unauthorized", "http");
+	HCPaymentRequired =>		ans = X("Payment required", "http");
+	HCForbidden =>			ans = X("Forbidden", "http");
+	HCNotFound =>			ans = X("Not found", "http");
+	HCMethodNotAllowed =>		ans = X("Method not allowed", "http");
+	HCNotAcceptable =>			ans = X("Not Acceptable", "http");
+	HCProxyAuthRequired =>		ans = X("Proxy authentication required", "http");
+	HCRequestTimeout =>		ans = X("Request timed-out", "http");
+	HCConflict =>				ans = X("Conflict", "http");
+	HCGone =>				ans = X("Gone", "http");
+	HCLengthRequired =>		ans = X("Length required", "http");
+	HCPreconditionFailed =>		ans = X("Precondition failed", "http");
+	HCRequestTooLarge =>		ans = X("Request entity too large", "http");
+	HCRequestURITooLarge =>	ans = X("Request-URI too large", "http");
+	HCUnsupportedMedia =>		ans = X("Unsupported media type", "http");
+	HCRangeInvalid =>			ans = X("Requested range not valid", "http");
+	HCExpectFailed =>			ans = X("Expectation failed", "http");
+	HCServerError =>			ans = X("Internal server error", "http");
+	HCNotImplemented =>		ans = X("Not implemented", "http");
+	HCBadGateway =>			ans = X("Bad gateway", "http");
+	HCServiceUnavailable =>		ans = X("Service unavailable", "http");
+	HCGatewayTimeout =>		ans = X("Gateway time-out", "http");
+	HCVersionUnsupported =>	ans = X("HTTP version not supported", "http");
+	HCRedirectionFailed =>		ans = X("Redirection failed", "http");
+	* =>						ans = X("Unknown code", "http");
+	}
+	return ans;
+}
+
+# This array should be kept sorted
+fileexttable := array[] of { T->StringInt
+	("ai", ApplPostscript),
+	("au", AudioBasic),
+# ("bit", ImageXBit),
+	("bit", ImageXInfernoBit),
+	("bit2", ImageXBit2),
+	("bitm", ImageXBitmulti),
+	("eps", ApplPostscript),
+	("gif", ImageGif),
+	("gz",	ApplOctets),
+	("htm", TextHtml),
+	("html", TextHtml),
+	("jpe", ImageJpeg),
+	("jpeg", ImageJpeg),
+	("jpg", ImageJpeg),
+	("pdf", ApplPdf),
+	("png", ImagePng),
+	("ps", ApplPostscript),
+	("shtml", TextHtml),
+	("text", TextPlain),
+	("tif", ImageTiff),
+	("tiff", ImageTiff),
+	("txt", TextPlain),
+	("zip", ApplOctets)
+};
+
+# argl is command line
+# Return string that is empty if all ok, else path of module
+# that failed to load.
+init(ch: Charon, c: CharonUtils, argl: list of string, evc: chan of ref E->Event, cksrv: Cookiesrv, ckc: ref Cookiesrv->Client) : string
+{
+	me = c;
+	sys = load Sys Sys->PATH;
+	startres = ResourceState.cur();
+	D = load Draw Draw->PATH;
+	CH = ch;
+	S = load String String->PATH;
+	if(S == nil)
+		return String->PATH;
+
+	U = load Url Url->PATH;
+	if(U == nil)
+		return Url->PATH;
+	U->init();
+
+	DI = load Dial Dial->PATH;
+	if(DI == nil)
+		return Dial->PATH;
+
+	T = load StringIntTab StringIntTab->PATH;
+	if(T == nil)
+		return StringIntTab->PATH;
+
+	trans = load Translate Translate->PATH;
+	if (trans != nil) {
+		trans->init();
+		(dict, nil) = trans->opendict(trans->mkdictname(nil, "charon"));
+	}
+
+	# Now have all the modules needed to process command line
+	# (hereafter can use our loadpath() function to substitute the
+	# build directory version if dbg['u'] is set)
+
+	setconfig(argl);
+	dbg = int config.dbg['d'];
+
+	G = load Gui loadpath(Gui->PATH);
+	if(G == nil)
+		return loadpath(Gui->PATH);
+
+	C = load Ctype loadpath(Ctype->PATH);
+	if(C == nil)
+		return loadpath(Ctype->PATH);
+
+	E = load Events Events->PATH;
+	if(E == nil)
+		return loadpath(Events->PATH);
+
+	J = load Script loadpath(Script->JSCRIPTPATH);
+	# don't report an error loading JavaScript, handled elsewhere
+
+	LX = load Lex loadpath(Lex->PATH);
+	if(LX == nil)
+		return loadpath(Lex->PATH);
+
+	B = load Build loadpath(Build->PATH);
+	if(B == nil)
+		return loadpath(Build->PATH);
+
+	I = load Img loadpath(Img->PATH);
+	if(I == nil)
+		return loadpath(Img->PATH);
+
+	L = load Layout loadpath(Layout->PATH);
+	if(L == nil)
+		return loadpath(Layout->PATH);
+	date = load Date loadpath(Date->PATH);
+	if (date == nil)
+		return loadpath(Date->PATH);
+
+	convcs = load Convcs Convcs->PATH;
+	if (convcs == nil)
+		return loadpath(Convcs->PATH);
+
+
+	# Intialize all modules after loading all, so that each
+	# may cache pointers to the other modules
+	# (G will be initialized in main charon routine, and L has to
+	# be inited after that, because it needs G's display to allocate fonts)
+
+	E->init(evc);
+	I->init(me);
+	err := convcs->init(nil);
+	if (err != nil)
+		return err;
+	if(J != nil) {
+		err = J->init(me);
+		if (err != nil) {
+			# non-fatal: just don't handle javascript
+			J = nil;
+			if (dbg)
+				sys->print("%s\n", err);
+		}
+	}
+	B->init(me);
+	LX->init(me);
+	date->init(me);
+
+	if (config.docookies) {
+		CK = cksrv;
+		ckclient = ckc;
+		if (CK == nil) {
+			path := loadpath(Cookiesrv->PATH);
+			CK = load Cookiesrv path;
+			if (CK == nil)
+				sys->print("cookies: cannot load server %s: %r\n", path);
+			else
+				ckclient = CK->start(config.userdir + "/cookies", 0);
+		}
+	}
+
+	# preload some transports
+	gettransport("http");
+	gettransport("file");
+
+	progresschan = chan of (int, int, int, string);
+	imcache = ref ImageCache;
+	ctype = C->ctype;
+	dbgproto = int config.dbg['p'];
+	ngchan = chan of (int, list of ref ByteSource, ref Netconn, chan of ref ByteSource);
+	return "";
+}
+
+# like startreq() but special case for a string ByteSource
+# which doesn't need an associated netconn
+stringreq(s : string) : ref ByteSource
+{
+	bs := ByteSource.stringsource(s);
+
+	G->progress <-= (bs.id, G->Pstart, 0, "text");
+	anschan := chan of ref ByteSource;
+	ngchan <-= (NGstartreq, bs :: nil, nil, anschan);
+	<-anschan;
+	return bs;
+}
+
+# Make a ByteSource for given request, and make sure
+# that it is on the queue of some Netconn.
+# If don't have a transport for the request's scheme,
+# the returned bs will have err set.
+startreq(req: ref ReqInfo) : ref ByteSource
+{
+	bs := ref ByteSource(
+			bytesourceid++,
+			req,		# req
+			nil,		# hdr
+			nil,		# data
+			0,		# edata
+			"",		# err
+			nil,		# net
+			1,		# refgo
+			1,		# refnc
+			0,		# eof
+			0,		# lim
+			0		# seenhdr
+		);
+
+	G->progress <-= (bs.id, G->Pstart, 0, req.url.tostring());
+	anschan := chan of ref ByteSource;
+	ngchan <-= (NGstartreq, bs::nil, nil, anschan);
+	<-anschan;
+	return bs;
+}
+
+# Wait for some ByteSource in current go generation to
+# have a state change that goproc hasn't seen yet.
+waitreq(bsl: list of ref ByteSource) : ref ByteSource
+{
+	anschan := chan of ref ByteSource;
+	ngchan <-= (NGwaitreq, bsl, nil, anschan);
+	return <-anschan;
+}
+
+# Notify netget that goproc is finished with bs.
+freebs(bs: ref ByteSource)
+{
+	anschan := chan of ref ByteSource;
+	ngchan <-= (NGfreebs, bs::nil, nil, anschan);
+	<-anschan;
+}
+
+abortgo(gopgrp: int)
+{
+	if(int config.dbg['d'])
+		sys->print("abort go\n");
+	kill(gopgrp, 1);
+	freegoresources();
+	# renew the channels so that receives/sends by killed threads don't
+	# muck things up
+	ngchan = chan of (int, list of ref ByteSource, ref Netconn, chan of ref ByteSource);
+}
+
+freegoresources()
+{
+	for(i := 0; i < len netconns; i++) {
+		nc := netconns[i];
+		nc.makefree();
+	}
+}
+
+# This runs as a separate thread.
+# It acts as a monitor to synchronize access to the Netconn data
+# structures, as a dispatcher to start runnetconn's as needed to
+# process work on Netconn queues, and as a notifier to let goproc
+# know when any ByteSources have advanced their state.
+netget()
+{
+	msg, n, i: int;
+	bsl : list of ref ByteSource;
+	nc: ref Netconn;
+	waitix := 0;
+	c : chan of ref ByteSource;
+	waitpending : list of (list of ref ByteSource, chan of ref ByteSource);
+	maxconn := config.nthreads;
+	gncs := array[maxconn] of int;
+
+	for(n = 0; n < len netconns; n++)
+		netconns[n] = Netconn.new(n);
+
+	# capture netget chan to prevent abortgo() reset of
+	# ngchan from breaking us (channel hungup) before kill() does its job
+	ngc := ngchan;
+mainloop:
+	for(;;) {
+		(msg,bsl,nc,c) = <- ngc;
+		case msg {
+		NGstartreq =>
+			bs := hd bsl;
+			# bs has req filled in, and is otherwise in its initial state.
+			# Find a suitable Netconn and add bs to its queue of work,
+			# then send nil along c to let goproc continue.
+
+			# if ReqInfo is nil then this is a string ByteSource
+			# in which case we don't need a netconn to service it as we have all
+			# data already
+			if (bs.req == nil) {
+				c <- = nil;
+				continue;
+			}
+
+			if(dbgproto)
+				sys->print("Startreq BS=%d for %s\n", bs.id, bs.req.url.tostring());
+			scheme := bs.req.url.scheme;
+			host := bs.req.url.host;
+			(transport, err) := gettransport(scheme);
+			if(err != "")
+				bs.err = err;
+			else {
+				sport :=bs.req.url.port;
+				if(sport == "")
+					port := transport->defaultport(scheme);
+				else
+					port = int sport;
+				i = 0;
+				freen := -1;
+				for(n = 0; n < len netconns && (i < maxconn || freen == -1); n++) {
+					nc = netconns[n];
+					if(nc.state == NCfree) {
+						if(freen == -1)
+							freen = n;
+					}
+					else if(nc.host == host
+					   && nc.port == port && nc.scheme == scheme && i < maxconn) {
+						gncs[i++] = n;
+					}
+				}
+				if(i < maxconn) {
+					# use a new netconn for this bs
+					if(freen == -1) {
+						freen = len netconns;
+						newncs := array[freen+10] of ref Netconn;
+						newncs[0:] = netconns;
+						for(n = freen; n < freen+10; n++)
+							newncs[n] = Netconn.new(n);
+						netconns = newncs;
+					}
+					nc = netconns[freen];
+					nc.host = host;
+					nc.port = port;
+					nc.scheme = scheme;
+					nc.qlen = 0;
+					nc.ngcur = 0;
+					nc.gocur = 0;
+					nc.reqsent = 0;
+					nc.pipeline = 0;
+					nc.connected = 0;
+				}
+				else {
+					# use existing netconn with fewest outstanding requests
+					nc = netconns[gncs[0]];
+					if(maxconn > 1) {
+						minqlen := nc.qlen - nc.gocur;
+						for(i = 1; i < maxconn; i++) {
+							x := netconns[gncs[i]];
+							if(x.qlen-x.gocur < minqlen) {
+								nc = x;
+								minqlen = x.qlen-x.gocur;
+							}
+						}
+					}
+				}
+				if(nc.qlen == len nc.queue) {
+					nq := array[nc.qlen+10] of ref ByteSource;
+					nq[0:] = nc.queue;
+					nc.queue = nq;
+				}
+				nc.queue[nc.qlen++] = bs;
+				bs.net = nc;
+				if(dbgproto)
+					sys->print("Chose NC=%d for BS %d, qlen=%d\n", nc.id, bs.id, nc.qlen);
+				if(nc.state == NCfree || nc.state == NCidle) {
+					if(nc.connected) {
+						nc.state = NCgethdr;
+						if(dbgproto)
+							sys->print("NC %d: starting runnetconn in gethdr state\n", nc.id);
+					}
+					else {
+						nc.state = NCconnect;
+						if(dbgproto)
+							sys->print("NC %d: starting runnetconn in connect state\n", nc.id);
+					}
+					spawn runnetconn(nc, transport);
+				}
+			}
+			c <-= nil;
+
+		NGwaitreq =>
+			# goproc wants to be notified when some ByteSource
+			# changes to a state that the goproc hasn't seen yet.
+			# Send such a ByteSource along return channel c.
+
+			if(dbgproto)
+				sys->print("Waitreq\n");
+
+			for (scanlist := bsl; scanlist != nil; scanlist = tl scanlist) {
+				bs := hd scanlist;
+				if (bs.refnc == 0) {
+					# string ByteSource or completed or error
+					if (bs.err != nil || bs.edata >= bs.lim) {
+						c <-= bs;
+						continue mainloop;
+					}
+					continue;
+				}
+				# netcon based bytesource
+				if ((bs.hdr != nil && !bs.seenhdr && bs.hdr.mtype != UnknownType) || bs.edata > bs.lim) {
+					c <-= bs;
+					continue mainloop;
+				}
+			}
+
+			if(dbgproto)
+				sys->print("Waitpending\n");
+			waitpending = (bsl, c) :: waitpending;
+			
+		NGfreebs =>
+			# goproc is finished with bs.
+			bs := hd bsl;
+
+			if(dbgproto)
+				sys->print("Freebs BS=%d\n", bs.id);
+			nc = bs.net;
+			bs.refgo = 0;
+			if(bs.refnc == 0) {
+				bs.free();
+				if(nc != nil)
+					nc.queue[nc.gocur] = nil;
+			}
+			if(nc != nil) {
+				# can be nil if no transport was found
+				nc.gocur++;
+				if(dbgproto)
+					sys->print("NC %d: gocur=%d, ngcur=%d, qlen=%d\n", nc.id, nc.gocur, nc.ngcur, nc.qlen);
+				if(nc.gocur == nc.qlen && nc.ngcur == nc.qlen) {
+					if(!nc.connected)
+						nc.makefree();
+				}
+			}
+			# don't need to check waitpending fro NGwait requests involving bs
+			# the only thread doing a freebs() should be the only thread that
+			# can do a waitreq() on the same bs.  Same thread cannot be in both states.
+	
+			c <-= nil;
+
+		NGstatechg =>
+			# Some runnetconn is telling us tht it changed the
+			# state of nc.  Send a nil along c to let it continue.
+			bs : ref ByteSource;
+			if(dbgproto)
+				sys->print("Statechg NC=%d, state=%s\n",
+					nc.id, ncstatenames[nc.state]);
+			sendtopending : ref ByteSource = nil;
+			pendingchan : chan of ref ByteSource;
+			if(waitpending != nil && nc.gocur < nc.qlen) {
+				bs = nc.queue[nc.gocur];
+				if(dbgproto) {
+					totlen := 0;
+					if(bs.hdr != nil)
+						totlen = bs.hdr.length;
+					sys->print("BS %d: havehdr=%d seenhdr=%d edata=%d lim=%d, length=%d\n",
+						bs.id, bs.hdr != nil, bs.seenhdr, bs.edata, bs.lim, totlen);
+					if(bs.err != "")
+						sys->print ("   err=%s\n", bs.err);
+				}
+				if(bs.refgo &&
+				   (bs.err != "" ||
+				   (bs.hdr != nil && !bs.seenhdr) ||
+				   (nc.gocur == nc.ngcur && nc.state == NCdone) ||
+				   (bs.edata > bs.lim))) {
+					nwp: list of (list of ref ByteSource, chan of ref ByteSource) = nil;
+					for (waitlist := waitpending; waitlist != nil; waitlist = tl waitlist) {
+						(bslist, anschan) := hd waitlist;
+						if (sendtopending != nil) {
+							nwp = (bslist, anschan) :: nwp;
+							continue;
+						}
+						for (look := bslist; look != nil; look = tl look) {
+							if (bs == hd look) {
+								sendtopending = bs;
+								pendingchan = anschan;
+								break;
+							}
+						}
+						if (sendtopending == nil)
+							nwp = (bslist, anschan) :: nwp;
+					}
+					waitpending = nwp;
+				}
+			}
+			if(nc.state == NCdone || nc.state == NCerr) {
+				if(dbgproto)
+					sys->print("NC %d: runnetconn finishing\n", nc.id);
+				assert(nc.ngcur < nc.qlen);
+				bs = nc.queue[nc.ngcur];
+				bs.refnc = 0;
+				if(bs.refgo == 0) {
+					bs.free();
+					nc.queue[nc.ngcur] = nil;
+				}
+				nc.ngcur++;
+				if(dbgproto)
+					sys->print("NC %d: ngcur=%d\n", nc.id, nc.ngcur);
+				nc.state = NCidle;
+				if(dbgproto)
+					sys->print("NC %d: idle\n", nc.id);
+				if(nc.ngcur < nc.qlen) {
+					if(nc.connected) {
+						nc.state = NCgethdr;
+						if(dbgproto)
+							sys->print("NC %d: starting runnetconn in gethdr state\n", nc.id);
+					}
+					else {
+						nc.state = NCconnect;
+						if(dbgproto)
+							sys->print("NC %d: starting runnetconn in connect state\n", nc.id);
+					}
+					(t, nil) := gettransport(nc.scheme);
+					spawn runnetconn(nc, t);
+				}
+				else if(nc.gocur == nc.qlen && !nc.connected)
+					nc.makefree();
+			}
+			c <-= nil;
+			if(sendtopending != nil) {
+				if(dbgproto)
+					sys->print("Send BS %d to pending waitreq\n", bs.id);
+				pendingchan <-= sendtopending;
+				sendtopending = nil;
+			}
+		}
+	}
+}
+
+# A separate thread, to handle ngcur request of transport.
+# If nc.gen ever goes < gen, we have aborted this go.
+runnetconn(nc: ref Netconn, t: Transport)
+{
+	ach := chan of ref ByteSource;
+	retry := 4;
+#	retry := 0;
+	err := "";
+
+	assert(nc.ngcur < nc.qlen);
+	bs := nc.queue[nc.ngcur];
+
+	# dummy loop, just for breaking out of in error cases
+eloop:
+	for(;;) {
+		# Make the connection, if necessary
+		if(nc.state == NCconnect) {
+			t->connect(nc, bs);
+			if(bs.err != "") {
+				if (retry) {
+					retry--;
+					bs.err = "";
+					sys->sleep(100);
+					continue eloop;
+				}
+				break eloop;
+			}
+			nc.state = NCgethdr;
+		}
+		assert(nc.state == NCgethdr && nc.connected);
+		if(nc.scheme == "https")
+			G->progress <-= (bs.id, G->Psslconnected, 0, "");
+		else
+			G->progress <-= (bs.id, G->Pconnected, 0, "");
+
+		t->writereq(nc, bs);
+		nc.reqsent++;
+		if (bs.err != "") {
+			if (retry) {
+				retry--;
+				bs.err = "";
+				nc.state = NCconnect;
+				sys->sleep(100);
+				continue eloop;
+			}
+			break eloop;
+		}
+		# managed to write the request
+		# do not retry if we are doing form POSTs	
+		# See RFC1945 section 12.2 "Safe Methods"
+		if (bs.req.method == HPost)
+			retry = 0;
+
+		# Get the header
+		t->gethdr(nc, bs);
+		if(bs.err != "") {
+			if (retry) {
+				retry--;
+				bs.err = "";
+				nc.state = NCconnect;
+				sys->sleep(100);
+				continue eloop;
+			}
+			break eloop;
+		}
+		assert(bs.hdr != nil);
+		G->progress <-= (bs.id, G->Phavehdr, 0, "");
+
+		nc.state = NCgetdata;
+
+		# read enough data to guess media type
+		while (bs.hdr.mtype == UnknownType && ncgetdata(t, nc, bs))
+			bs.hdr.setmediatype(bs.hdr.actual.path, bs.data[:bs.edata]);
+		if (bs.hdr.mtype == UnknownType) {
+			bs.hdr.mtype = TextPlain;
+			bs.hdr.chset = "utf8";
+		}
+		ngchan <-= (NGstatechg,nil,nc,ach);
+		<- ach;
+		while (ncgetdata(t, nc, bs)) {
+			ngchan <-= (NGstatechg,nil,nc,ach);
+			<- ach;
+		}
+		nc.state = NCdone;
+		G->progress <-= (bs.id, G->Phavedata, 100, "");
+		break;
+	}
+	if(bs.err != "") {
+		nc.state = NCerr;
+		nc.connected = 0;
+		G->progress <-= (bs.id, G->Perr, 0, bs.err);
+	}
+	bs.eof = 1;
+	ngchan <-= (NGstatechg, nil, nc, ach);
+	<- ach;
+}
+
+ncgetdata(t: Transport, nc: ref Netconn, bs: ref ByteSource): int
+{
+	hdr := bs.hdr;
+	if (bs.data == nil) {
+		blen := hdr.length;
+		if (blen <= 0) {
+			if(hdr.code == HCOk || hdr.code == HCOkNonAuthoritative)
+				blen = UBufsize;
+			else
+				blen = UEBufsize;
+		}
+		bs.data = array[blen] of byte;
+	}
+	nr := 0;
+	if (hdr.length > 0) {
+		if (bs.edata == hdr.length)
+			return 0;
+		nr = t->getdata(nc, bs);
+		if (nr <= 0)
+			return 0;
+	} else {
+		# don't know data length - keep growing input buffer as needed
+		if (bs.edata == len bs.data) {
+			nd := array [2*len bs.data] of byte;
+			nd[:] = bs.data;
+			bs.data = nd;
+		}
+		nr = t->getdata(nc, bs);
+		if (nr <= 0) {
+			# assume EOF
+			bs.data = bs.data[0:bs.edata];
+			bs.err = "";
+			hdr.length = bs.edata;
+			nc.connected = 0;
+			return 0;
+		}
+	}
+	bs.edata += nr;
+	G->progress <-= (bs.id, G->Phavedata, 100*bs.edata/len bs.data, "");
+	return 1;
+}
+
+Netconn.new(id: int) : ref Netconn
+{
+	return ref Netconn(
+			id,		# id
+			"",		# host
+			0,		# port
+			"",		# scheme
+			ref Dial->Connection(nil, nil, ""),	# conn
+			nil,		# ssl context
+			0,		# undetermined ssl version
+			NCfree,	# state
+			array[10] of ref ByteSource,	# queue
+			0,		# qlen
+			0,0,0,	# gocur, ngcur, reqsent
+			0,		# pipeline
+			0,		# connected
+			0,		# tstate
+			nil,		# tbuf
+			0		# idlestart
+			);
+}
+
+Netconn.makefree(nc: self ref Netconn)
+{
+	if(dbgproto)
+		sys->print("NC %d: free\n", nc.id);
+	nc.state = NCfree;
+	nc.host = "";
+	nc.conn = nil;
+	nc.qlen = 0;
+	nc.gocur = 0;
+	nc.ngcur = 0;
+	nc.reqsent = 0;
+	nc.pipeline = 0;
+	nc.connected = 0;
+	nc.tstate = 0;
+	nc.tbuf = nil;
+	for(i := 0; i < len nc.queue; i++)
+		nc.queue[i] = nil;
+}
+
+ByteSource.free(bs: self ref ByteSource)
+{
+	if(dbgproto)
+		sys->print("BS %d freed\n", bs.id);
+	if(bs.err == "")
+		G->progress <-= (bs.id, G->Pdone, 100, "");
+	else
+		G->progress <-= (bs.id, G->Perr, 0, bs.err);
+	bs.req = nil;
+	bs.hdr = nil;
+	bs.data = nil;
+	bs.err = "";
+	bs.net = nil;
+}
+
+# Return an ByteSource that is completely filled, from string s
+ByteSource.stringsource(s: string) : ref ByteSource
+{
+	a := array of byte s;
+	n := len a;
+	hdr := ref Header(
+			HCOk,		# code
+			nil,			# actual
+			nil,			# base
+			nil,			# location
+			n,			# length
+			TextHtml, 	# mtype
+			"utf8",		# chset
+			"",			# msg
+			"",			# refresh
+			"",			# chal
+			"",			# warn
+			""			# last-modified
+		);
+	bs := ref ByteSource(
+			bytesourceid++,
+			nil,		# req
+			hdr,		# hdr
+			a,		# data
+			n,		# edata
+			"",		# err
+			nil,		# net
+			1,		# refgo
+			0,		# refnc
+			1,		# eof	- edata is final
+			0,		# lim
+			1		# seenhdr
+		);
+	return bs;
+}
+
+MaskedImage.free(mim: self ref MaskedImage)
+{
+	mim.im = nil;
+	mim.mask = nil;
+}
+
+CImage.new(src: ref U->Parsedurl, lowsrc: ref U->Parsedurl, width, height: int) : ref CImage
+{
+	return ref CImage(src, lowsrc, nil, strhash(src.host + "/" + src.path), width, height, nil, nil, 0);
+}
+
+# Return true if Cimages a and b represent the same image.
+# As well as matching the src urls, the specified widths and heights must match too.
+# (Widths and heights are specified if at least one of those is not zero.)
+#
+# BUG: the width/height matching code isn't right.  If one has width and height
+# specified, and the other doesn't, should say "don't match", because the unspecified
+# one should come in at its natural size.  But we overwrite the width and height fields
+# when the actual size comes in, so we can't tell whether width and height are nonzero
+# because they were specified or because they're their natural size.
+CImage.match(a: self ref CImage, b: ref CImage) : int
+{
+	if(a.imhash == b.imhash) {
+		if(urlequal(a.src, b.src)) {
+			return (a.width == 0 || b.width == 0 || a.width == b.width) &&
+				(a.height == 0 || b.height == 0 || a.height == b.height);
+			# (above is not quite enough: should also check that don't have
+			# situation where one has width set, not height, and the other has reverse,
+			# but it is unusual for an image to have a spec in only one dimension anyway)
+		}
+	}
+	return 0;
+}
+
+# Return approximate number of bytes in image memory used
+# by ci.
+CImage.bytes(ci: self ref CImage) : int
+{
+	tot := 0;
+	for(i := 0; i < len ci.mims; i++) {
+		mim := ci.mims[i];
+		dim := mim.im;
+		if(dim != nil)
+			tot += ((dim.r.max.x-dim.r.min.x)*dim.depth/8) *
+					(dim.r.max.y-dim.r.min.y);
+		dim = mim.mask;
+		if(dim != nil)
+			tot += ((dim.r.max.x-dim.r.min.x)*dim.depth/8) *
+					(dim.r.max.y-dim.r.min.y);
+	}
+	return tot;
+}
+
+# Call this after initial windows have been made,
+# so that resetlimits() will exclude the images for those
+# windows from the available memory.
+ImageCache.init(ic: self ref ImageCache)
+{
+	ic.imhd = nil;
+	ic.imtl = nil;
+	ic.n = 0;
+	ic.memused = 0;
+	ic.resetlimits();
+}
+
+# Call resetlimits when amount of non-image-cache image
+# memory might have changed significantly (e.g., on main window resize).
+ImageCache.resetlimits(ic: self ref ImageCache)
+{
+	res := ResourceState.cur();
+	avail := res.imagelim - (res.image-ic.memused);
+		# (res.image-ic.memused) is used memory not in image cache
+	avail = 8*avail/10;	# allow 20% slop for other applications, etc.
+	ic.memlimit = config.imagecachemem;
+	if(ic.memlimit > avail)
+		ic.memlimit = avail;
+#	ic.nlimit = config.imagecachenum;
+	ic.nlimit = 10000;	# let's try this
+	ic.need(0);	# if resized, perhaps need to shed some images
+}
+
+# Look for a CImage matching ci, and if found, move it
+# to the tail position (i.e., MRU)
+ImageCache.look(ic: self ref ImageCache, ci: ref CImage) : ref CImage
+{
+	ans : ref CImage = nil;
+	prev : ref CImage = nil;
+	for(i := ic.imhd; i != nil; i = i.next) {
+		if(i.match(ci)) {
+			if(ic.imtl != i) {
+				# remove from current place in cache chain
+				# and put at tail
+				if(prev != nil)
+					prev.next = i.next;
+				else
+					ic.imhd = i.next;
+				i.next = nil;
+				ic.imtl.next = i;
+				ic.imtl = i;
+			}
+			ans = i;
+			break;
+		}
+		prev = i;
+	}
+	return ans;
+}
+
+# Call this to add ci as MRU of cache chain (should only call if
+# it is known that a ci with same image isn't already there).
+# Update ic.memused.
+# Assume ic.need has been called to ensure that neither
+# memlimit nor nlimit will be exceeded.
+ImageCache.add(ic: self ref ImageCache, ci: ref CImage)
+{
+	ci.next = nil;
+	if(ic.imhd == nil)
+		ic.imhd = ci;
+	else
+		ic.imtl.next = ci;
+	ic.imtl = ci;
+	ic.memused += ci.bytes();
+	ic.n++;
+}
+
+# Delete least-recently-used image in image cache
+# and update memused and n.
+ImageCache.deletelru(ic: self ref ImageCache)
+{
+	ci := ic.imhd;
+	if(ci != nil) {
+		ic.imhd = ci.next;
+		if(ic.imhd == nil) {
+			ic.imtl = nil;
+			ic.memused = 0;
+		}
+		else
+			ic.memused -= ci.bytes();
+		for(i := 0; i < len ci.mims; i++)
+			ci.mims[i].free();
+		ci.mims = nil;
+		ic.n--;
+	}
+}
+
+ImageCache.clear(ic: self ref ImageCache)
+{
+	while(ic.imhd != nil)
+		ic.deletelru();
+}
+
+# Call this just before allocating an Image that will used nbytes
+# of image memory, to ensure that if the image were to be
+# added to the image cache then memlimit and nlimit will be ok.
+# LRU images will be shed if necessary.
+# Return 0 if it will be impossible to make enough memory.
+ImageCache.need(ic: self ref ImageCache, nbytes: int) : int
+{
+	while(ic.n >= ic.nlimit || ic.memused+nbytes > ic.memlimit) {
+		if(ic.imhd == nil)
+			return 0;
+		ic.deletelru();
+	}
+	return 1;
+}
+
+strhash(s: string) : int
+{
+	prime: con 8388617;
+	hash := 0;
+	n := len s;
+	for(i := 0; i < n; i++) {
+		hash = hash % prime;
+		hash = (hash << 7) + s[i];
+	}
+	return hash;
+}
+
+schemeid(s: string): int
+{
+	for (i := 0; i < len schemes; i++) {
+		(n, id) := schemes[i];
+		if (n == s)
+			return id;
+	}
+	return -1;
+}
+
+schemeok(s: string): int
+{
+	return schemeid(s) != -1;
+}
+
+gettransport(scheme: string) : (Transport, string)
+{
+	err := "";
+	transport: Transport = nil;
+	tindex := schemeid(scheme);
+	if (tindex == -1)
+		return (nil, "Unknown scheme");
+	transport = transports[tindex];
+	if (transport == nil) {
+		transport = load Transport loadpath(tpaths[tindex]);
+		if(transport == nil)
+			return (nil, sys->sprint("Can't load transport %s: %r", tpaths[tindex]));
+		transport->init(me);
+		transports[tindex] = transport;
+	}
+	return (transport, err);
+}
+
+# Return new Header with default values for fields
+Header.new() : ref Header
+{
+	return ref Header(
+		HCOk,		# code
+		nil,		# actual
+		nil,		# base
+		nil,		# location
+		-1,		# length
+		UnknownType,	# mtype
+		nil,		# chset
+		"",		# msg
+		"",		# refresh
+		"",		# chal
+		"",		# warn
+		""		# last-modified
+	);
+}
+
+jpmagic := array[] of {byte 16rFF, byte 16rD8, byte 16rFF, byte 16rE0,
+		byte 0, byte 0, byte 'J', byte 'F', byte 'I', byte 'F', byte 0};
+pngsig := array[] of { byte 137, byte 80, byte 78, byte 71, byte 13, byte 10, byte 26, byte 10 };
+
+# Set the mtype (and possibly chset) fields of h based on (in order):
+#	first bytes of file, if unambigous
+#	file name extension
+#	first bytes of file, even if unambigous (guess)
+#	if all else fails, then leave as UnknownType.
+# If it's a text type, also set the chset.
+# (HTTP Transport will try to use Content-Type first, and call this if that
+# doesn't work; other Transports will have to rely on this "guessing" function.)
+Header.setmediatype(h: self ref Header, name: string, first: array of byte)
+{
+	# Look for key signatures at beginning of file (perhaps after whitespace)
+	n := len first;
+	mt := UnknownType;
+	for(i := 0; i < n; i++)
+		if(ctype[int first[i]] != C->W)
+			break;
+	if(n - i >= 6) {
+		s := string first[i:i+6];
+		case S->tolower(s) {
+		"<html " or "<html\t" or "<html>" or "<head>" or "<title" =>
+			mt = TextHtml;
+		"<!doct" =>
+			if(n - i >= 14 && string first[i+6:i+14] == "ype html")
+				mt = TextHtml;
+		"gif87a" or "gif89a" =>
+			if(i == 0)
+				mt = ImageGif;
+		"#defin" =>
+			# perhaps should check more definitively...
+			mt = ImageXXBitmap;
+		}
+
+		if (mt == UnknownType && n > 0) {
+			if (first[0] == jpmagic[0] && n >= len jpmagic) {
+				for(i++; i<len jpmagic; i++)
+					if(jpmagic[i]>byte 0 && first[i]!=jpmagic[i])
+						break;
+				if (i == len jpmagic)
+					mt = ImageJpeg;
+			} else if (first[0] == pngsig[0] && n >= len pngsig) {
+				for(i++; i<len pngsig; i++)
+					if (first[i] != pngsig[i])
+						break;
+				if (i == len pngsig)
+					mt = ImagePng;
+			}
+		}
+	}
+
+	if(mt == UnknownType) {
+		# Try file name extension
+		(nil, file) := S->splitr(name, "/");
+		if(file != "") {
+			(f, ext) := S->splitr(file, ".");
+			if(f != "" && ext != "") {
+				(fnd, val) := T->lookup(fileexttable, S->tolower(ext));
+				if(fnd)
+					mt = val;
+			}
+		}
+	}
+
+#	if(mt == UnknownType) {
+#		mt = TextPlain;
+#		h.chset = "utf8";
+#	}
+	h.mtype = mt;
+}
+
+Header.print(h: self ref Header)
+{
+	mtype := "?";
+	if(h.mtype >= 0 && h.mtype < len mnames)
+		mtype = mnames[h.mtype];
+	chset := "?";
+	if(h.chset != nil)
+		chset = h.chset;
+	# sys->print("code=%d (%s) length=%d mtype=%s chset=%s\n",
+	#	h.code, hcphrase(h.code), h.length, mtype, chset);
+	if(h.base != nil)
+		sys->print("  base=%s\n", h.base.tostring());
+	if(h.location != nil)
+		sys->print("  location=%s\n", h.location.tostring());
+	if(h.refresh != "")
+		sys->print("  refresh=%s\n", h.refresh);
+	if(h.chal != "")
+		sys->print("  chal=%s\n", h.chal);
+	if(h.warn != "")
+		sys->print("  warn=%s\n", h.warn);
+}
+
+
+mfd : ref sys->FD = nil;
+ResourceState.cur() : ResourceState
+{
+	ms := sys->millisec();
+	main := 0;
+	mainlim := 0;
+	heap := 0;
+	heaplim := 0;
+	image := 0;
+	imagelim := 0;
+	if(mfd == nil)
+		mfd = sys->open("/dev/memory", sys->OREAD);
+	if (mfd == nil)
+		raise sys->sprint("can't open /dev/memory: %r");
+
+	sys->seek(mfd, big 0, Sys->SEEKSTART);
+
+	buf := array[400] of byte;
+	n := sys->read(mfd, buf, len buf);
+	if (n <= 0)
+		raise sys->sprint("can't read /dev/memory: %r");
+
+	(nil, l) := sys->tokenize(string buf[0:n], "\n");
+	# p->cursize, p->maxsize, p->hw, p->nalloc, p->nfree, p->nbrk, poolmax(p), p->name)
+	while(l != nil) {
+		s := hd l;
+		cur_size := int s[0:12];				
+		max_size := int s[12:24];
+		case s[7*12:] {
+		"main" =>
+			main = cur_size;
+			mainlim = max_size;
+		"heap" =>
+			heap = cur_size;
+			heaplim = max_size;
+		"image" =>
+			image = cur_size;
+			imagelim = max_size;
+		}
+		l = tl l;
+	}
+
+	return ResourceState(ms, main, mainlim, heap, heaplim, image, imagelim);
+}
+
+ResourceState.since(rnew: self ResourceState, rold: ResourceState) : ResourceState
+{
+	return (rnew.ms - rold.ms, 
+		rnew.main - rold.main, 
+		rnew.heaplim,
+		rnew.heap - rold.heap,
+		rnew.heaplim, 
+		rnew.image - rold.image, 
+		rnew.imagelim);
+}
+
+ResourceState.print(r: self ResourceState, msg: string)
+{
+	sys->print("%s:\n\ttime: %d.%#.3ds; memory: main %dk, mainlim %dk, heap %dk, heaplim %dk, image %dk, imagelim %dk\n",
+				msg, r.ms/1000, r.ms % 1000, r.main / 1024, r.mainlim / 1024,
+				r.heap / 1024, r.heaplim / 1024, r.image / 1024, r.imagelim / 1024);
+}
+
+# Decide what to do based on Header and whether this is
+# for the main entity or not, and the number of redirections-so-far.
+# Return tuple contains:
+#	(use, error, challenge, redir)
+# and action to do is:
+#	If use==1, use the entity else drain its byte source.
+#	If error != nil, mesg was put in progress bar
+#	If challenge != nil, get auth info and make new request with auth
+#	Else if redir != nil, make a new request with redir for url
+#
+# (if challenge or redir is non-nil, use will be 0)
+hdraction(bs: ref ByteSource, ismain: int, nredirs: int) : (int, string, string, ref U->Parsedurl)
+{
+	use := 1;
+	error := "";
+	challenge := "";
+	redir : ref U->Parsedurl = nil;
+
+	h := bs.hdr;
+	assert(h != nil);
+	bs.seenhdr = 1;
+	code := h.code;
+	case code/100 {
+	HSOk =>
+		if(code != HCOk)
+			error = "unexpected code: " + hcphrase(code);
+	HSRedirect =>
+		if(h.location != nil) {
+			redir = h.location;
+			# spec says url should be absolute, but some
+			# sites give relative ones
+			if(redir.scheme == nil)
+				redir = U->mkabs(redir, h.base);
+			if(dbg)
+				sys->print("redirect %s to %s\n", h.actual.tostring(), redir.tostring());
+			if(nredirs >= Maxredir) {
+				redir = nil;
+				error = "probable redirect loop";
+			}
+			else
+				use = 0;
+		}
+	HSError =>
+		if(code == HCUnauthorized && h.chal != "") {
+			challenge = h.chal;
+			use = 0;
+		}
+		else {
+			error = hcphrase(code);
+			use = ismain;
+		}
+	HSServererr =>
+		error = hcphrase(code);
+		use = ismain;
+	* =>
+		error = "unexpected code: " + string code;
+		use = 0;
+
+	}
+	if(error != "")
+		G->progress <-= (bs.id, G->Perr, 0, error);
+	return (use, error, challenge, redir);
+}
+
+# Use event when only care about time stamps on events
+event(s: string, data: int)
+{
+	sys->print("%s: %d %d\n", s, sys->millisec()-startres.ms, data);
+}
+
+kill(pid: int, dogroup: int)
+{
+	msg : array of byte;
+	if(dogroup)
+		msg = array of byte "killgrp";
+	else
+		msg = array of byte "kill";
+	ctl := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if(ctl != nil)
+		if (sys->write(ctl, msg, len msg) < 0)
+			sys->print("charon: kill write failed (pid %d, grp %d): %r\n", pid, dogroup);
+}
+
+# Read a line up to and including cr/lf (be tolerant and allow missing cr).
+# Look first in buf[bstart:bend], and if that isn't sufficient to get whole line,
+# refill buf from fd as needed.
+# Return values:
+#	array of byte: the line, not including cr/lf
+#	eof, true if there was no line to get or a read error
+#	bstart', bend': new valid portion of buf (after cr/lf).
+getline(fd: ref sys->FD, buf: array of byte, bstart, bend: int) :
+		(array of byte, int, int, int)
+{
+	ans : array of byte = nil;
+	last : array of byte = nil;
+	eof := 0;
+mainloop:
+	for(;;) {
+		for(i := bstart; i < bend; i++) {
+			if(buf[i] == byte '\n') {
+				k := i;
+				if(k > bstart && buf[k-1] == byte '\r')
+					k--;
+				last = buf[bstart:k];
+				bstart = i+1;
+				break mainloop;
+			}
+		}
+		if(bend > bstart)
+			ans = append(ans, buf[bstart:bend]);
+		last = nil;
+		bstart = 0;
+		bend = sys->read(fd, buf, len buf);
+		if(bend <= 0) {
+			eof = 1;
+			bend = 0;
+			break mainloop;
+		}
+	}
+	return (append(ans, last), eof, bstart, bend);
+}
+
+# Append copy of second array to first, return (possibly new)
+# address of the concatenation.
+append(a: array of byte, b: array of byte) : array of byte
+{
+	if(b == nil)
+		return a;
+	na := len a;
+	nb := len b;
+	ans := realloc(a, nb);
+	ans[na:] = b;
+	return ans;
+}
+
+# Return copy of a, but incr bytes bigger
+realloc(a: array of byte, incr: int) : array of byte
+{
+	n := len a;
+	newa := array[n + incr] of byte;
+	if(a != nil)
+		newa[0:] = a;
+	return newa;
+}
+
+# Look (linearly) through a for s; return its index if found, else -1.
+strlookup(a: array of string, s: string) : int
+{
+	n := len a;
+	for(i := 0; i < n; i++)
+		if(s == a[i])
+			return i;
+	return -1;
+}
+
+# Set up config global to defaults, then try to read user-specifiic
+# config data from /usr/<username>/charon/config, then try to
+# override from command line arguments.
+setconfig(argl: list of string)
+{
+	# Defaults, in absence of any other information
+	config.userdir = "";
+	config.srcdir = "/appl/cmd/charon";
+	config.starturl = "file:/services/webget/start.html";
+	config.homeurl = config.starturl;
+	config.change_homeurl = 1;
+	config.helpurl = "file:/services/webget/help.html";
+	config.usessl = SSLV3;	# was NOSSL
+	config.devssl = 0;
+	config.custbkurl = "/services/config/bookmarks.html";
+	config.dualbkurl = "/services/config/dualdisplay.html";
+	config.httpproxy = nil;
+	config.noproxydoms = nil;
+	config.buttons = "help,resize,hide,exit";
+	config.framework = "all";
+	config.defaultwidth = 640;
+	config.defaultheight = 480;
+	config.x = -1;
+	config.y = -1;
+	config.nocache = 0;
+	config.maxstale = 0;
+	config.imagelvl = ImgFull;
+	config.imagecachenum = 120;
+	config.imagecachemem = 100000000;	# 100Meg, will get lowered later
+	config.docookies = 1;
+	config.doscripts = 1;
+	config.httpminor = 0;
+	config.agentname = "Mozilla/4.08 (Charon; Inferno)";
+	config.nthreads = 4;
+	config.offersave = 1;
+	config.charset = "windows-1252";
+	config.plumbport = "web";
+	config.wintitle = "Charon";	# tkclient->titlebar() title, used by GUI
+	config.dbgfile = "";
+	config.dbg = array[128] of { * => byte 0 };
+	
+	# Reading default config file
+	readconf("/services/config/charon.cfg");
+
+	# Try reading user config file
+	user := "";
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd != nil) {
+		b := array[40] of byte;
+		n := sys->read(fd, b, len b);
+		if(n > 0)
+			user = string b[0:n];
+	}
+	if(user != "") {
+		config.userdir = "/usr/" + user + "/charon";
+		readconf(config.userdir + "/config");
+	}
+
+	if(argl == nil)
+		return;
+	# Try command line arguments
+	# All should be 'key=val' or '-key' or '-key val', except last which can be url to start
+	for(l := tl argl; l != nil; l = tl l) {
+		s := hd l;
+		if(s == "")
+			continue;
+		if (s[0] != '-')
+			break;
+		a := s[1:];
+		b := "";
+		if(tl l != nil) {
+			b = hd tl l;
+			if(S->prefix("-", b))
+				b = "";
+			else
+				l = tl l;
+		}
+		if(!setopt(a, b)) {
+			if (b != nil)
+				s += " "+b;
+			sys->print("couldn't set option from arg '%s'\n", s);
+		}
+	}
+	if(l != nil) {
+		if (tl l != nil)
+			# usage error
+			sys->print("too many URL's\n");
+		else
+			if(!setopt("starturl", hd l))
+				sys->print("couldn't set starturl from arg '%s'\n", hd l);
+	}
+}
+
+readconf(fname: string)
+{
+	cfgio := sys->open(fname, sys->OREAD);
+	if(cfgio != nil) {
+		buf := array[sys->ATOMICIO] of byte;
+		i := 0;
+		j := 0;
+		aline : array of byte;
+		eof := 0;
+		for(;;) {
+			(aline, eof, i, j) = getline(cfgio, buf, i, j);
+			if(eof)
+				break;
+			line := string aline;
+			if(len line == 0 || line[0]=='#')
+				continue;
+			(key, val) := S->splitl(line, " \t=");
+			if(key != "") {
+				val = S->take(S->drop(val, " \t="), "^#\r\n");
+				if(!setopt(key, val))
+					sys->print("couldn't set option from line '%s'\n", line);
+			}
+		}
+	}
+}
+
+# Set config option named 'key' to val, returning 1 if OK
+setopt(key: string, val: string) : int
+{
+	ok := 1;
+	if(val == "none")
+		val = "";
+	v := int val;
+	case key {
+	"userdir" =>
+		config.userdir = val;
+	"srcdir" =>
+		config.srcdir = val;
+	"starturl" =>
+		if(val != "")
+			config.starturl = val;
+		else
+			ok = 0;
+	"change_homeurl" =>
+		config.change_homeurl = v;
+	"homeurl" =>
+		if(val != "")
+			if(config.change_homeurl) {
+				config.homeurl = val;
+				# order dependent
+				config.starturl = config.homeurl;
+			}
+		else
+			ok = 0;
+	"helpurl" =>
+		if(val != "")
+			config.helpurl = val;
+		else
+			ok = 0;
+ 	"usessl" =>
+ 		if(val == "v2")
+ 			config.usessl |= SSLV2;
+ 		if(val == "v3")
+ 			config.usessl |= SSLV3;
+ 	"devssl" =>
+ 		if(v == 0)
+ 			config.devssl = 0;
+ 		else
+ 			config.devssl = 1;
+#	"custbkurl" =>
+#	"dualbkurl" =>
+	"httpproxy" =>
+		if(val != "")
+			config.httpproxy = makeabsurl(val);
+		else
+			config.httpproxy = nil;
+	"noproxy" or "noproxydoms" =>
+		(nil, config.noproxydoms) = sys->tokenize(val, ";, \t");
+	"buttons" =>
+		config.buttons = S->tolower(val);
+	"framework" =>
+		config.framework = S->tolower(val);
+	"defaultwidth" or "width" =>
+		if(v > 200)
+			config.defaultwidth = v;
+		else
+			ok = 0;
+	"defaultheight" or "height" =>
+		if(v > 100)
+			config.defaultheight = v;
+		else
+			ok = 0;
+	"x" =>
+		config.x = v;
+	"y" =>
+		config.y = v;
+	"nocache" =>
+		config.nocache = v;
+	"maxstale" =>
+		config.maxstale = v;
+	"imagelvl" =>
+		config.imagelvl = v;
+	"imagecachenum" =>
+		config.imagecachenum = v;
+	"imagecachemem" =>
+		config.imagecachemem = v;
+	"docookies" =>
+		config.docookies = v;
+	"doscripts" =>
+		config.doscripts = v;
+	"http" =>
+		if(val == "1.1")
+			config.httpminor = 1;
+		else
+			config.httpminor = 0;
+	"agentname" =>
+		config.agentname = val;
+	"nthreads" =>
+		if (v < 1)
+			ok = 0;
+		else
+			config.nthreads = v;
+	"offersave" =>
+		if (v < 1)
+			config.offersave = 0;
+		else
+			config.offersave = 1;
+	"charset" =>
+		config.charset = val;
+	"plumbport" =>
+		config.plumbport = val;
+	"wintitle" =>
+		config.wintitle = val;
+	"dbgfile" =>
+		config.dbgfile = val;
+	"dbg" =>
+		for(i := 0; i < len val; i++) {
+			c := val[i];
+			if(c < len config.dbg)
+				config.dbg[c]++;
+			else {
+				ok = 0;
+				break;
+			}
+		}
+	* =>
+		ok = 0;
+	}
+	return ok;
+}
+
+saveconfig(): int
+{
+	fname := config.userdir + "/config";
+	buf := array [Sys->ATOMICIO] of byte;
+	fd := sys->create(fname, Sys->OWRITE, 8r600);
+	if(fd == nil)
+		return -1;
+
+	nbyte := savealine(fd, buf, "# Charon user configuration\n", 0);
+	nbyte = savealine(fd, buf, "userdir=" + config.userdir + "\n", nbyte);
+	nbyte = savealine(fd, buf, "srcdir=" + config.srcdir +"\n", nbyte);
+	if(config.change_homeurl){ 
+		nbyte = savealine(fd, buf, "starturl=" + config.starturl + "\n", nbyte);
+ 		nbyte = savealine(fd, buf, "homeurl=" + config.homeurl + "\n", nbyte); 	
+	}
+	if(config.httpproxy != nil)
+		nbyte = savealine(fd, buf, "httpproxy=" + config.httpproxy.tostring() + "\n", nbyte); 	
+ 	if(config.usessl & SSLV23) {
+ 		nbyte = savealine(fd, buf, "usessl=v2\n", nbyte);
+ 		nbyte = savealine(fd, buf, "usessl=v3\n", nbyte);
+	}
+	else {
+ 		if(config.usessl & SSLV2)
+ 			nbyte = savealine(fd, buf, "usessl=v2\n", nbyte);
+ 		if(config.usessl & SSLV3)
+ 			nbyte = savealine(fd, buf, "usessl=v3\n", nbyte);
+ 	}
+	if(config.devssl == 0)
+		nbyte = savealine(fd, buf, "devssl=0\n", nbyte);
+	else
+		nbyte = savealine(fd, buf, "devssl=1\n", nbyte);
+	if(config.noproxydoms != nil) {
+		doms := "";
+		doml := config.noproxydoms;
+		while(doml != nil) {
+			doms += hd doml + ",";
+			doml = tl doml;
+		}
+		nbyte = savealine(fd, buf, "noproxy=" + doms + "\n", nbyte);
+	}
+	nbyte = savealine(fd, buf, "defaultwidth=" + string config.defaultwidth + "\n", nbyte); 	 	
+	nbyte = savealine(fd, buf, "defaultheight=" + string config.defaultheight + "\n", nbyte); 	
+	if(config.x >= 0)
+		nbyte = savealine(fd, buf, "x=" + string config.x + "\n", nbyte);
+	if(config.y >= 0)
+		nbyte = savealine(fd, buf, "y=" + string config.y + "\n", nbyte);
+	nbyte = savealine(fd, buf, "nocache=" + string config.nocache + "\n", nbyte);
+	nbyte = savealine(fd, buf, "maxstale=" + string config.maxstale + "\n", nbyte);
+	nbyte = savealine(fd, buf, "imagelvl=" + string config.imagelvl + "\n", nbyte);
+	nbyte = savealine(fd, buf, "imagecachenum=" + string config.imagecachenum + "\n", nbyte);
+	nbyte = savealine(fd, buf, "imagecachemem=" + string config.imagecachemem + "\n", nbyte);
+	nbyte = savealine(fd, buf, "docookies=" + string config.docookies + "\n", nbyte);
+	nbyte = savealine(fd, buf, "doscripts=" + string config.doscripts + "\n", nbyte);
+	nbyte = savealine(fd, buf, "http=" + "1." + string config.httpminor + "\n", nbyte);
+	nbyte = savealine(fd, buf, "agentname=" + string config.agentname + "\n", nbyte);
+	nbyte = savealine(fd, buf, "nthreads=" + string config.nthreads + "\n", nbyte);
+	nbyte = savealine(fd, buf, "charset=" + config.charset + "\n", nbyte);
+	#for(i := 0; i < len config.dbg; i++)
+		#nbyte = savealine(fd, buf, "dbg=" + string config.dbg[i] + "\n", nbyte);
+
+	if(nbyte > 0)
+		sys->write(fd, buf, nbyte);
+
+	return 0; 
+}
+
+savealine(fd: ref Sys->FD, buf: array of byte, s: string, n: int): int
+{
+	if(Sys->ATOMICIO < n + len s) {
+		sys->write(fd, buf, n);
+		buf[0:] = array of byte s;
+		return len s;
+	}
+	buf[n:] = array of byte s;
+	return n + len s;
+}
+
+# Make a StringInt table out of a, mapping each string
+# to its index.  Check that entries are in alphabetical order.
+makestrinttab(a: array of string) : array of T->StringInt
+{
+	n := len a;
+	ans := array[n] of T->StringInt;
+	for(i := 0; i < n; i++) {
+		ans[i].key = a[i];
+		ans[i].val = i;
+		if(i > 0 && a[i] < a[i-1])
+			raise "EXInternal: table out of alphabetical order";
+	}
+	return ans;
+}
+
+# Should really move into Url module.
+# Don't include fragment in test, since we are testing if the
+# pointed to docs are the same, not places within docs.
+urlequal(a, b: ref U->Parsedurl) : int
+{
+	return a.scheme == b.scheme
+		&& a.host == b.host
+		&& a.port == b.port
+		&& a.user == b.user
+		&& a.passwd == b.passwd
+		&& a.path == b.path
+		&& a.query == b.query;
+}
+
+# U->makeurl, but add http:// if not an absolute path already
+makeabsurl(s: string) : ref Parsedurl
+{
+	if (s == "")
+		return nil;
+	u := U->parse(s);
+	if (u.scheme != nil)
+		return u;
+	if (s[0] == '/')
+		# try file:
+		s = "file://localhost" + s;
+	else
+		# try http
+		s = "http://" + s;
+	u = U->parse(s);
+	return u;
+}
+
+# Return place to load from, given installed-path name.
+# (If config.dbg['u'] is set, change directory to config.srcdir.)
+loadpath(s: string) : string
+{
+	if(config.dbg['u'] == byte 0)
+		return s;
+	(nil, f) := S->splitr(s, "/");
+	return config.srcdir + "/" + f;
+}
+
+color_tab := array[] of { T->StringInt
+	("aqua",	16r00FFFF),
+	("black",	Black),
+	("blue",	Blue),
+	("fuchsia",	16rFF00FF),
+	("gray",	16r808080),
+	("green",	16r008000),
+	("lime",	16r00FF00),
+	("maroon",	16r800000),
+	("navy",	Navy),
+	("olive",	16r808000),
+	("purple",	16r800080),
+	("red",	Red),
+	("silver",	16rC0C0C0),
+	("teal",	16r008080),
+	("white",	White),
+	("yellow",	16rFFFF00)
+};
+# Convert HTML color spec to RGB value, returning dflt if can't.
+# Argument is supposed to be a valid HTML color, or "".
+# Return the RGB value of the color, using dflt if s
+# is "" or an invalid color.
+color(s: string, dflt: int) : int
+{
+	if(s == "")
+		return dflt;
+	s = S->tolower(s);
+	c := s[0];
+	if(c < C->NCTYPE && ctype[c] == C->L) {
+		(fnd, v) := T->lookup(color_tab, s);
+		if(fnd)
+			return v;
+	}
+	if(s[0] == '#')
+		s = s[1:];
+	(v, rest) := S->toint(s, 16);
+	if(rest == "")
+		return v;
+	# s was invalid, so choose a valid one
+	return dflt;
+}
+
+max(a,b: int) : int
+{
+	if(a > b)
+		return a;
+	return b;
+}
+
+min(a,b: int) : int
+{
+	if(a < b)
+		return a;
+	return b;
+}
+
+assert(i: int)
+{
+	if(!i) {
+		raise "EXInternal: assertion failed";
+#		sys->print("assertion failed\n");
+#		s := hmeth[-1];
+	}
+}
+
+getcookies(host, path: string, secure: int): string
+{
+	if (CK == nil || ckclient == nil)
+		return nil;
+	Client: import CK;
+	return ckclient.getcookies(host, path, secure);
+}
+
+setcookie(host, path, cookie: string)
+{
+	if (CK == nil || ckclient == nil)
+		return;
+	Client: import CK;
+	ckclient.set(host, path, cookie);
+}
+
+ex_mkdir(dirname: string): int
+{
+	(ok, nil) := sys->stat(dirname);
+	if(ok < 0) {
+		f := sys->create(dirname, sys->OREAD, sys->DMDIR + 8r777);
+		if(f == nil) {
+			sys->print("mkdir: can't create %s: %r\n", dirname);
+			return 0;
+		}
+		f = nil;
+	}
+	return 1;
+}
+
+stripscript(s: string): string
+{
+	# strip leading whitespace and SGML comment start symbol '<!--'
+	if (s == nil)
+		return nil;
+	cs := "<!--";
+	ci := 0;
+	for (si := 0; si < len s; si++) {
+		c := s[si];
+		if (c == cs[ci]) {
+			if (++ci >= len cs)
+				ci = 0;
+		} else {
+			ci = 0;
+			if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
+				continue;
+			break;
+		}
+	}
+	# strip trailing whitespace and SGML comment terminator '-->'
+	cs = "-->";
+	ci = len cs -1;
+	for (se := len s - 1; se > si; se--) {
+		c := s[se];
+		if (c == cs[ci]) {
+			if (ci-- == 0)
+				ci = len cs -1;
+		} else {
+			ci = len cs - 1;
+			if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
+				continue;
+			break;
+		}
+	}
+	if (se < si)
+		return nil;
+	return s[si:se+1];
+}
+
+# Split a value (guaranteed trimmed) into sep-separated list of one of
+# 	token
+#	token = token
+#	token = "quoted string"
+# and put into list of Namevals (lowercase the first token)
+Nameval.namevals(s: string, sep: int) : list of Nameval
+{
+	ans : list of Nameval = nil;
+	n := len s;
+	i := 0;
+	while(i < n) {
+		tok : string;
+		(tok, i) = gettok(s, i, n);
+		if(tok == "")
+			break;
+		tok = S->tolower(tok);
+		val := "";
+		while(i < n && ctype[s[i]] == C->W)
+			i++;
+		if(i == n || s[i] == sep)
+			i++;
+		else if(s[i] == '=') {
+			i++;
+			while(i < n && ctype[s[i]] == C->W)
+				i++;
+			if (i == n)
+				break;
+			if(s[i] == '"')
+				(val, i) = getqstring(s, i, n);
+			else
+				(val, i) = gettok(s, i, n);
+		}
+		else
+			break;
+		ans = Nameval(tok, val) :: ans;
+	}
+	return ans;
+}
+
+gettok(s: string, i,n: int) : (string, int)
+{
+	while(i < n && ctype[s[i]] == C->W)
+		i++;
+	if(i == n)
+		return ("", i);
+	is := i;
+	for(; i < n; i++) {
+		c := s[i];
+		ct := ctype[c];
+		if(!(int (ct&(C->D|C->L|C->U|C->N|C->S))))
+			if(int (ct&(C->W|C->C)) || S->in(c, "()<>@,;:\\\"/[]?={}"))
+				break;
+	}
+	return (s[is:i], i);
+}
+
+# get quoted string; return it without quotes, and index after it
+getqstring(s: string, i,n: int) : (string, int)
+{
+	while(i < n && ctype[s[i]] == C->W)
+		i++;
+	if(i == n || s[i] != '"')
+		return ("", i);
+	is := ++i;
+	for(; i < n; i++) {
+		c := s[i];
+		if(c == '\\')
+			i++;
+		else if(c == '"')
+			return (s[is:i], i+1);
+	}
+	return (s[is:i], i);
+}
+
+# Find value corresponding to key (should be lowercase)
+# and return (1, value) if found or (0, "")
+Nameval.find(l: list of Nameval, key: string) : (int, string)
+{
+	for(; l != nil; l = tl l)
+		if((hd l).key == key)
+			return (1, (hd l).val);
+	return (0, "");
+}
+
+# this should be a converter cache
+getconv(chset : string) : Btos
+{
+	(btos, err) := convcs->getbtos(chset);
+	if (err != nil)
+		sys->print("Converter error: %s\n", err);
+	return btos;
+}
+
+X(s, note : string) : string
+{
+	if (dict == nil)
+		return s;
+	return dict.xlaten(s, note);
+}
--- /dev/null
+++ b/appl/charon/chutils.m
@@ -1,0 +1,371 @@
+CharonUtils: module
+{
+	PATH: con "/dis/charon/chutils.dis";
+
+	# Modules for everyone to share
+	C: Ctype;
+	E: Events;
+	G: Gui;
+	L: Layout;
+	I: Img;
+	B: Build;
+	LX: Lex;
+	J: Script;
+	CH: Charon;
+	CK: Cookiesrv;
+	DI: Dial;
+
+	# HTTP methods
+	HGet, HPost : con iota;
+	hmeth: array of string;
+
+	# Media types (must track mnames in chutils.b)
+	ApplMsword, ApplOctets, ApplPdf, ApplPostscript, ApplRtf,
+	ApplFramemaker, ApplMsexcel, ApplMspowerpoint, UnknownType,
+
+	Audio32kadpcm, AudioBasic,
+
+	ImageCgm, ImageG3fax, ImageGif, ImageIef, ImageJpeg, ImagePng, ImageTiff,
+	ImageXBit, ImageXBit2, ImageXBitmulti, ImageXInfernoBit, ImageXXBitmap,
+
+	ModelVrml,
+
+	MultiDigest, MultiMixed,
+
+	TextCss, TextEnriched, TextHtml, TextJavascript, TextPlain, TextRichtext,
+	TextSgml, TextTabSeparatedValues, TextXml,
+
+	VideoMpeg, VideoQuicktime : con iota;
+
+	mnames: array of string;
+
+	# Netconn states
+	NCfree, NCidle, NCconnect, NCgethdr, NCgetdata,
+	NCdone, NCerr : con iota;
+
+	ncstatenames: array of string;
+
+	# Netcomm synch protocol values
+	NGstartreq, NGwaitreq, NGstatechg, NGfreebs : con iota;
+
+	# Colors
+	White: con 16rFFFFFF;
+	Black: con 16r000000;
+	Grey: con 16rdddddd;
+	DarkGrey: con 16r9d9d9d;
+	LightGrey: con 16rededed;
+	Blue: con 16r0000CC;
+	Navy: con 16r000080;
+	Red: con 16rFF0000;
+	Green: con 16r00FF00;
+	DarkRed: con 16r9d0000;
+
+	# Header major status values (code/100)
+	HSNone, HSInformation, HSOk, HSRedirect, HSError, HSServererr : con iota;
+	hsnames: array of string;
+
+	# Individual status code values (HTTP, but use for other transports too)
+	HCContinue:		con 100;
+	HCSwitchProto:		con 101;
+	HCOk:			con 200;
+	HCCreated:		con 201;
+	HCAccepted:		con 202;
+	HCOkNonAuthoritative:	con 203;
+	HCNoContent:		con 204;
+	HCResetContent:		con 205;
+	HCPartialContent:	con 206;
+	HCMultipleChoices:	con 300;
+	HCMovedPerm:		con 301;
+	HCMovedTemp:		con 302;
+	HCSeeOther:		con 303;
+	HCNotModified:		con 304;
+	HCUseProxy:		con 305;
+	HCBadRequest:		con 400;
+	HCUnauthorized:		con 401;
+	HCPaymentRequired:	con 402;
+	HCForbidden:		con 403;
+	HCNotFound:		con 404;
+	HCMethodNotAllowed:	con 405;
+	HCNotAcceptable:	con 406;
+	HCProxyAuthRequired:	con 407;
+	HCRequestTimeout:	con 408;
+	HCConflict:		con 409;
+	HCGone:			con 410;
+	HCLengthRequired:	con 411;
+	HCPreconditionFailed:	con 412;
+	HCRequestTooLarge:	con 413;
+	HCRequestURITooLarge:	con 414;
+	HCUnsupportedMedia:	con 415;
+	HCRangeInvalid:		con 416;
+	HCExpectFailed:		con 419;
+	HCServerError:		con 500;
+	HCNotImplemented:	con 501;
+	HCBadGateway:		con 502;
+	HCServiceUnavailable:	con 503;
+	HCGatewayTimeout:	con 504;
+	HCVersionUnsupported:	con 505;
+	HCRedirectionFailed:	con 506;
+
+	# Max number of redirections tolerated
+	Maxredir : con 10;
+
+	# Image Level config options
+	ImgNone, ImgNoAnim, ImgProgressive, ImgFull: con iota;
+
+ 	# SSL connection version
+ 	NOSSL, SSLV2, SSLV3, SSLV23: con iota;
+
+	# User Configuration Information (Options)
+	# Debug option letters:
+	# 'd' -> Basic operation info (navigation, etc.)
+	# 'e' -> Events (timing of progress through get/layout/image conversion)
+	# 'h' -> Build layout items from lex tokens
+	# 'i' -> Image conversion
+	# 'l' -> Layout
+	# 'n' -> transport (Network access)
+	# 'o' -> always use old http (http/1.0)
+	# 'p' -> synch Protocol between ByteSource/Netconn
+	# 'r' -> Resource usage
+	# 's' -> Scripts
+	# 't' -> Table layout
+	# 'u' -> use Uninstalled dis modules
+	# 'w' -> Warn about recoverable problems in retrieved pages
+	# 'x -> lex Html tokens
+	Config: adt
+	{
+		userdir:	string;		# where to find bookmarks, cache, etc.
+		srcdir:		string;		# where to find charon src (for debugging)
+		starturl:	string;# never nil (could be last of command args)
+		change_homeurl:	int;
+		homeurl:	string;# never nil
+		helpurl:	string;
+ 		usessl:		int; # use ssl version 2, 3 or both
+ 		devssl:		int; # use devssl
+		custbkurl:	string; # where are customized bookmarks-never nil
+		dualbkurl:	string; # where is the dual bookmark page-never nil
+		httpproxy:	ref Url->Parsedurl;# nil, if no proxy
+		noproxydoms:	list of string; # domains that don't require proxy
+		buttons:	string;		# customized buttons
+		framework: string;		# customized gui framework
+		defaultwidth:	int;		# of entire browser
+		defaultheight:	int;		# of entire browser
+		x:			int;		# initial x position for browser
+		y:			int;		# initial y position for browser
+		nocache:	int;		# true if shouldn't retrieve from or store to
+		maxstale:	int;		# allow cache hit even if exceed expiration by maxstale
+		imagelvl:	int;		# ImgNone, etc.
+		imagecachenum: int;	# imcache.nlimit
+		imagecachemem: int;	# imcache.memlimit
+		docookies:	int;		# allow cookie storage/sending?
+		doscripts:		int;		# allow scripts to execute?
+		httpminor:	int;		# use HTTP 1.httpminor
+		agentname:	string;	# what to send in HTTP header
+		nthreads:		int;		# number of simultaneous gets allowed
+		offersave:	int;		# offer to save a file of a type that can't be handled
+		charset: string;			# default character set
+		plumbport: string;		# from/to plumbing port name (default = "web")
+		wintitle: string;
+		dbgfile:		string;	# file to write debug messages to
+		dbg:		array of byte;	# ascii letters for different debugging kinds
+	};
+
+	# Information for fulfilling HTTP request
+	ReqInfo : adt
+	{
+		url:	ref Url->Parsedurl;	# should be absolute
+		method:	int;			# HGet or HPost
+		body:	array of byte;		# used for HPost
+		auth:	string;			# optional auth info
+		target:	string;			# target frame name
+	};
+
+	MaskedImage: adt {
+		im:		ref Draw->Image;		# the image
+		mask:	ref Draw->Image;		# if non-nil, a mask for the image
+		delay:	int;			# if animated, delay in millisec before next frame
+		more:	int;			# true if more frames follow
+		bgcolor:	int;			# if not -1, restore to this (RGB) color before next frame
+		origin:	Draw->Point;		# origin of im relative to first frame of an animation
+
+		free: fn(mim: self ref MaskedImage);
+	};
+
+	# Charon Image info.
+	# If this is an animated image then len mims > 1
+	CImage: adt
+	{
+		src:	ref Url->Parsedurl;	# source of image
+		lowsrc:	ref Url->Parsedurl;	# for low-resolution devices
+		actual: ref Url->Parsedurl;	# what came back as actual source of image
+		imhash:	int;			# hash of src, for fast comparison
+		width:	int;
+		height:	int;
+		next:	cyclic ref CImage;	# next (newer) image in cache
+		mims: array of ref MaskedImage;
+		complete: int;			# JavaScript Image.complete
+
+		new: fn(src: ref Url->Parsedurl, lowsrc: ref Url->Parsedurl, width, height: int) : ref CImage;
+		match: fn(a: self ref CImage, b: ref CImage) : int;
+		bytes: fn(ci: self ref CImage) : int;
+	};
+
+	# In-memory cache of CImages
+	ImageCache: adt
+	{
+		imhd:	ref CImage;	# head (LRU) of cache chain (linked through CImage.next)
+		imtl:		ref CImage;	# tail MRU) of cache chain
+		n:	int;			# size of chain
+		memused: int;		# current total of image mem used by cached images
+		memlimit: int;		# keep memused less than this
+		nlimit: int;			# keep n less than this
+
+		init: fn(ic: self ref ImageCache);
+		resetlimits: fn(ic: self ref ImageCache);
+		look: fn(ic: self ref ImageCache, ci: ref CImage) : ref CImage;
+		add: fn(ic: self ref ImageCache, ci: ref CImage);
+		deletelru: fn(ic: self ref ImageCache);
+		clear: fn(ic: self ref ImageCache);
+		need: fn(ic: self ref ImageCache, nbytes: int) : int;
+	};
+
+	# An connection to some host
+	Netconn: adt
+	{
+		id:		 int;			# for debugging
+		host:	string;			# host name
+		port:	int;			# port number
+		scheme: string;		# Url scheme ("http", "file", etc.)
+		conn:	ref Dial->Connection;	# fds, etc.
+ 		sslx:	ref SSL3->Context;	# ssl connection
+ 		vers:	int;			# ssl version
+		state:	int;			# NCfree, etc.
+		queue:	cyclic array of ref ByteSource;
+						# following are indexes into queue
+		qlen:		int;		# queue[0:qlen] is queue of requests
+		gocur:	int;		# go thread currently processing
+		ngcur:	int;		# ng threads currently processing
+		reqsent:	int;		# next to send request for
+		pipeline:	int;		# are requests being pipelined?
+		connected:	int;	# are we connected to host?
+		tstate:	int;		# for use by transport
+		tbuf: 	array of byte;	# for use by transport
+		idlestart:	int;		# timestamp when went Idle
+
+		new: fn(id: int) : ref Netconn;
+		makefree: fn(nc: self ref Netconn);
+	};
+
+	# Info from an HTTP response header
+	Header: adt
+	{
+		code:	int;			# HC... (detailed response code)
+		actual:	ref Url->Parsedurl;	# actual request url (may be result of redir)
+		base:	ref Url->Parsedurl;	# Content-Base or request url
+		location:	ref Url->Parsedurl;	# Content-Location
+		length:	int;			# -1 if unknown
+		mtype:	int;			# TextHtml, etc.
+		chset:	string;		# charset encoding
+		msg:	string;			# possible message explaining status
+		refresh:string;			# used for server push
+		chal:	string;			# used if code is HSneedauth
+		warn:	string;			# should show this to user
+		lastModified:	string;		# last-modified field
+
+		new: fn() : ref Header;
+		setmediatype: fn(h: self ref Header, name: string, first: array of byte);
+		print: fn(h: self ref Header);
+	};
+
+	# A source of raw bytes (with HTTP info)
+	ByteSource: adt
+	{
+		id: int;				# for debugging
+		req:	ref ReqInfo;
+		hdr:	ref Header;		# filled in from headers
+		data:	array of byte;		# all the data, maybe partially filled
+		edata: int;				# data[0:edata] is valid
+		err: string;			# there was an error
+		net:	cyclic ref Netconn;	# servicing fd, etc.
+		refgo: int;				# go proc is still using
+		refnc: int;				# netconn proc is still using
+
+		# producer sets eof upon finalising data & edata
+		eof: int;
+
+		# consumer changes only these fields:
+		lim: int;				# consumer has seen data[0:lim]
+		seenhdr: int;			# consumer has seen hdr
+
+		free: fn(bs: self ref ByteSource);
+		stringsource: fn(s: string) : ref ByteSource;
+	};
+
+	# Snapshot of current system resources
+	ResourceState: adt
+	{
+		ms: int;		# a millisecond time stamp
+		main: int;		# main memory
+		mainlim: int;		# max main memory
+		heap: int;		# heap memory
+		heaplim: int;		# max heap memory
+		image: int;		# used image memory
+		imagelim: int;		# max image memory
+
+		cur: fn() : ResourceState;
+		since: fn(rnew: self ResourceState, rold: ResourceState) : ResourceState;
+		print: fn(r: self ResourceState, msg: string);
+	};
+
+
+	Nameval: adt {
+		key: string;
+		val: string;
+
+		namevals: fn(s: string, sep: int) : list of Nameval;
+		find: fn(l: list of Nameval, key: string) : (int, string);
+	};
+
+
+	# Globals
+	config: Config;
+	startres: ResourceState;
+	imcache: ref ImageCache;
+	progresschan: chan of (int, int, int, string);
+	gen: int;	# go generation number
+	ckclient: ref Cookiesrv->Client;
+
+	init: fn(ch: Charon, me: CharonUtils, argl: list of string, evch: chan of ref E->Event, cksrv: Cookiesrv, ckclient: ref Cookiesrv->Client) : string;
+
+	# Dispatcher functions
+	stringreq: fn(s : string) : ref ByteSource;
+	startreq: fn(req: ref ReqInfo) : ref ByteSource;
+	waitreq: fn(bsl : list of ref ByteSource) : ref ByteSource;
+	freebs: fn(bs: ref ByteSource);
+	abortgo: fn(gopgrp: int);
+	netget: fn();
+
+	# Miscellaneous utility functions
+	kill: fn(pid: int, dogroup: int);
+	getline: fn(fd: ref Sys->FD, buf: array of byte, bstart, bend: int) :
+		(array of byte, int, int, int);
+	saveconfig: fn() : int;
+	strlookup: fn(a: array of string, s: string) : int;
+	realloc: fn(a: array of byte, incr: int) : array of byte;
+	hcphrase: fn(code: int) : string;
+	hdraction: fn(bs: ref ByteSource, ismain: int, nredirs: int) : (int, string, string, ref Url->Parsedurl);
+	makestrinttab: fn(a: array of string) : array of StringIntTab->StringInt;
+	urlequal: fn(a, b: ref Url->Parsedurl) : int;
+	makeabsurl: fn(s: string) : ref Url->Parsedurl;
+	loadpath: fn(s: string) : string;
+	event: fn(s: string, data: int);
+	color: fn(s: string, dflt: int) : int;
+	max: fn(a, b : int) : int;
+	min: fn(a, b : int) : int;
+	assert: fn(i: int);
+	stripscript: fn(s: string) : string;	# strip HTML comments from Script
+	getconv: fn(chset : string) : Btos;
+	setcookie: fn(host, path, cookie: string);
+	getcookies: fn(host, path: string, secure: int): string;
+	schemeok: fn(scheme: string): int;	# is URL scheme supported?
+	X: fn(s, note : string) : string;
+};
--- /dev/null
+++ b/appl/charon/common.m
@@ -1,0 +1,24 @@
+include "sys.m";
+include "draw.m";
+include "string.m";
+include "url.m";
+include "strinttab.m";
+include "ctype.m";
+include "keyring.m";
+include "asn1.m";
+include "pkcs.m";
+include "x509.m";
+include "dial.m";
+include "sslsession.m";
+include "ssl3.m";
+include "convcs.m";
+include "cookiesrv.m";
+include "chutils.m";
+include "lex.m";
+include "script.m";
+include "build.m";
+include "layout.m";
+include "img.m";
+include "event.m";
+include "gui.m";
+include "charon.m";
--- /dev/null
+++ b/appl/charon/cookiesrv.b
@@ -1,0 +1,595 @@
+implement Cookiesrv;
+include "sys.m";
+include "bufio.m";
+include "string.m";
+include "daytime.m";
+include "cookiesrv.m";
+
+sys: Sys;
+bufio: Bufio;
+S: String;
+daytime: Daytime;
+
+Iobuf: import bufio;
+
+Cookielist: adt {
+	prev: cyclic ref Cookielist;
+	next: cyclic ref Cookie;
+};
+
+Cookie: adt {
+	name: string;
+	value: string;
+	dom: string;
+	path: string;
+	expire: int;		# seconds from epoch, -1 => not set, 0 => expire now
+	secure: int;
+	touched: int;
+	link: cyclic ref Cookielist;	# linkage for list of cookies in the same domain
+};
+
+Domain: adt {
+	name: string;
+	doms: cyclic list of ref Domain;
+	cookies: ref Cookielist;
+};
+
+MAXCOOKIES: con 300;		# total number of cookies allowed
+LISTMAX: con 20;			# max number of cookies per Domain
+PURGENUM: con 30;			# number of cookies to delete when freeing up space
+MAXCKLEN: con 4*1024;		# max cookie length
+
+ncookies := 0;
+doms: list of ref Domain;
+now: int;	# seconds since epoch
+cookiepath: string;
+touch := 0;
+
+start(path: string, saveinterval: int): ref Client
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->print("cookiesrv: cannot load %s: %r\n", Bufio->PATH);
+		return nil;
+	}
+	S = load String String->PATH;
+	if (S == nil) {
+		sys->print("cookiesrv: cannot load %s: %r\n", String->PATH);
+		return nil;
+	}
+	daytime = load Daytime Daytime->PATH;
+	if (daytime == nil) {
+		sys->print("cookiesrv: cannot load %s: %r\n", Daytime->PATH);
+		return nil;
+	}
+
+	cookiepath = path;
+	now = daytime->now();
+
+	# load the cookie file
+	# order is most recently touched first 
+	iob := bufio->open(cookiepath, Sys->OREAD);
+	if (iob != nil) {
+		line: string;
+		while ((line = iob.gets('\n')) != nil) {
+			if (line[len line -1] == '\n')
+				line = line[:len line -1];
+			loadcookie(line);
+		}
+		iob.close();
+		iob = nil;
+		expire();
+	}
+	fdc := chan of ref Sys->FD;
+	spawn server(fdc, saveinterval);
+	fd := <- fdc;
+	if (fd == nil)
+		return nil;
+	return ref Client(fd);
+}
+
+addcookie(ck: ref Cookie, domlist: ref Cookielist)
+{
+	(last, n) := lastlink(domlist);
+	if (n == LISTMAX)
+		rmcookie(last.prev.next);
+	if (ncookies == MAXCOOKIES)
+		rmlru();
+	ck.link = ref Cookielist(domlist, domlist.next);
+	if (domlist.next != nil)
+		domlist.next.link.prev = ck.link;
+	domlist.next = ck;
+	ncookies++;
+}
+
+rmcookie(ck: ref Cookie)
+{
+	nextck := ck.link.next;
+	ck.link.prev.next = nextck;
+	if (nextck != nil) 
+		nextck.link.prev = ck.link.prev;
+	ncookies--;
+}
+
+lastlink(ckl: ref Cookielist): (ref Cookielist, int)
+{
+	n := 0;
+	for (nckl := ckl.prev; nckl != nil; nckl = nckl.prev)
+		n++;
+	for (; ckl.next != nil; ckl = ckl.next.link)
+		n++;
+	return (ckl, n);
+}
+
+rmlru()
+{
+	cka := array [ncookies] of ref Cookie;
+	ix := getallcookies(doms, cka, 0);
+	if (ix < PURGENUM)
+		return;
+	mergesort(cka, nil, SORT_TOUCHED);
+	for (n := 0; n < PURGENUM; n++)
+		rmcookie(cka[n]);
+}
+
+getallcookies(dl: list of ref Domain, cka: array of ref Cookie, ix: int): int
+{
+	for (; dl != nil; dl = tl dl) {
+		dom := hd dl;
+		for (ck := dom.cookies.next; ck != nil; ck = ck.link.next)
+			cka[ix++] = ck;
+		ix = getallcookies(dom.doms, cka, ix);
+	}
+	return ix;
+}
+
+isipaddr(s: string): int
+{
+	# assume ipaddr if only numbers and '.'s
+	# should maybe count the dots too (what about IPV6?)
+	return S->drop(s, ".0123456789") == nil;
+}
+
+setcookie(ck: ref Cookie)
+{
+	parent, dom: ref Domain;
+	domain := ck.dom;
+	if (isipaddr(domain))
+		(parent, dom, domain) = getdom(doms, nil, domain);
+	else
+		(parent, dom, domain) = getdom(doms, domain, nil);
+
+	if (dom == nil)
+		dom = newdom(parent, domain);
+
+	for (oldck := dom.cookies.next; oldck != nil; oldck = oldck.link.next) {
+		if (ck.name == oldck.name && ck.path == oldck.path) {
+			rmcookie(oldck);
+			break;
+		}
+	}
+	if (ck.expire > 0 && ck.expire <= now)
+		return;
+	addcookie(ck, dom.cookies);
+}
+
+expire()
+{
+	cka := array [ncookies] of ref Cookie;
+	ix := getallcookies(doms, cka, 0);
+	for (i := 0; i < ix; i++) {
+		ck := cka[i];
+		if (ck.expire > 0 && ck.expire < now)
+			rmcookie(ck);
+	}
+}
+
+newdom(parent: ref Domain, domain: string): ref Domain
+{
+	while (domain != "") {
+		(lhs, rhs) := splitdom(domain);
+		d := ref Domain(rhs, nil, ref Cookielist(nil, nil));
+		if (parent == nil)
+			doms = d :: doms;
+		else
+			parent.doms = d :: parent.doms;
+		parent = d;
+		domain = lhs;
+	}
+	return parent;
+}
+
+getdom(dl: list of ref Domain, lhs, rhs: string): (ref Domain, ref Domain, string)
+{
+	if (rhs == "")
+		(lhs, rhs) = splitdom(lhs);
+	parent: ref Domain;
+	while (dl != nil) {
+		d := hd dl;
+		if (d.name != rhs) {
+			dl = tl dl;
+			continue;
+		}
+		# name matches
+		if (lhs == nil)
+			return (parent, d, rhs);
+		parent = d;
+		(lhs, rhs) = splitdom(lhs);
+		dl = d.doms;
+	}
+	return (parent, nil, lhs+rhs);
+}
+
+# returned list is in shortest to longest domain match order
+getdoms(dl: list of ref Domain, lhs, rhs: string): list of ref Domain
+{
+	if (rhs == "")
+		(lhs, rhs) = splitdom(lhs);
+	for (; dl != nil; dl = tl dl) {
+		d := hd dl;
+		if (d.name == rhs) {
+			if (lhs == nil)
+				return d :: nil;
+			(lhs, rhs) = splitdom(lhs);
+			return d :: getdoms(d.doms, lhs, rhs);
+		}
+	}
+	return nil;
+}
+
+server(fdc: chan of ref Sys->FD, saveinterval: int)
+{
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+	sys->bind("#s", "/chan", Sys->MBEFORE);
+	fio := sys->file2chan("/chan", "ctl");
+	if (fio == nil) {
+		fdc <-= nil;
+		return;
+	}
+	fd := sys->open("/chan/ctl", Sys->OWRITE);
+	fdc <-= fd;
+	if (fd == nil)
+		return;
+	fd = nil;
+		
+	tick := chan of int;
+	spawn ticker(tick, 1*60*1000);	# clock tick once a minute
+	tickerpid := <- tick;
+
+	modified := 0;
+	savetime := now + saveinterval;
+
+	for (;;) alt {
+	now = <- tick =>
+		expire();
+		if (saveinterval != 0 && now > savetime) {
+			if (modified) {
+				save();
+				modified = 0;
+			}
+			savetime = now + saveinterval;
+		}
+	(nil, line, nil, rc) := <- fio.write =>
+		now = daytime->now();
+		if (rc == nil) {
+			kill(tickerpid);
+			expire();
+			save();
+			return;
+		}
+		loadcookie(string line);
+		alt {
+		rc <-= (len line, nil) =>
+			;
+		* =>
+			;
+		};
+		modified = 1;
+	}
+}
+
+ticker(tick: chan of int, ms: int)
+{
+	tick <-= sys->pctl(0, nil);
+	for (;;) {
+		sys->sleep(ms);
+		tick <-= daytime->now();
+	}
+}
+
+# sort orders
+SORT_TOUCHED, SORT_PATHLEN: con iota;
+
+mergesort(a, b: array of ref Cookie, order: int)
+{
+	if (b == nil)
+		b = array [len a] of ref Cookie;
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m], order);
+		mergesort(a[m:], b[m:], order);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (greater(b[i], b[j], order))
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+greater(x, y: ref Cookie, order: int): int
+{
+	if (y == nil)
+		return 0;
+	case order {
+	SORT_TOUCHED =>
+		if (x.touched > y.touched)
+			return 1;
+	SORT_PATHLEN =>
+		if (len x.path < len y.path)
+			return 1;
+	}
+	return 0;
+}
+
+cookie2str(ck: ref Cookie): string
+{
+	if (len ck.name +1 > MAXCKLEN)
+		return "";
+	namval := sys->sprint("%s=%s", ck.name, ck.value);
+	if (len namval > MAXCKLEN)
+		namval = namval[:MAXCKLEN];
+	return sys->sprint("%s\t%s\t%d\t%d\t%s", ck.dom, ck.path, ck.expire, ck.secure, namval);
+}
+
+loadcookie(ckstr: string)
+{
+	(n, toks) := sys->tokenize(ckstr, "\t");
+	if (n < 5)
+		return;
+	dom, path, exp, sec, namval: string;
+	(dom, toks) = (hd toks, tl toks);
+	(path, toks) = (hd toks, tl toks);
+	(exp, toks) = (hd toks, tl toks);
+	(sec, toks) = (hd toks, tl toks);
+	(namval, toks) = (hd toks, tl toks);
+
+	# some sanity checks
+	if (dom == "" || path == "" || path[0] != '/')
+		return;
+
+	(name, value) := S->splitl(namval, "=");
+	if (value == nil)
+		return;
+	value = value[1:];
+	ck := ref Cookie(name, value, dom, path, int exp, int sec, touch++, nil);
+	setcookie(ck);
+}
+
+Client.set(c: self ref Client, host, path, cookie: string)
+{
+	ck := parsecookie(host, path, cookie);
+	if (ck == nil)
+		return;
+	b := array of byte cookie2str(ck);
+	sys->write(c.fd, b, len b);
+}
+
+Client.getcookies(nil: self ref Client, host, path: string, secure: int): string
+{
+	dl: list of ref Domain;
+	if (isipaddr(host))
+		dl = getdoms(doms, nil, host);
+	else {
+		# note some domains match hosts
+		# e.g. site X.com has to set a cookie for '.X.com'
+		# to get around the netscape '.' count check
+		# this messes up our domain checking
+		# putting a '.' on the front of host is a safe way of handling this
+#		host = "." + host;
+		dl = getdoms(doms, host, nil);
+	}
+	cookies: list of ref Cookie;
+	for (; dl != nil; dl = tl dl) {
+		ckl := (hd dl).cookies;
+		for (ck := ckl.next; ck != nil; ck = ck.link.next) {
+			if (ck.secure && !secure)
+				continue;
+			if (!S->prefix(ck.path, path))
+				continue;
+			ck.touched = touch++;
+			cookies = ck :: cookies;
+		}
+	}
+	if (cookies == nil)
+		return "";
+
+	# sort w.r.t path len and creation order
+	cka := array [len cookies] of ref Cookie;
+	for (i := 0; cookies != nil; cookies = tl cookies)
+		cka[i++] = hd cookies;
+
+	mergesort(cka, nil, SORT_PATHLEN);
+
+	s := sys->sprint("%s=%s", cka[0].name, cka[0].value);
+	for (i = 1; i < len cka; i++)
+		s += sys->sprint("; %s=%s", cka[i].name, cka[i].value);
+	return s;
+}
+
+save()
+{
+	fd := sys->create(cookiepath, Sys->OWRITE, 8r600);
+	if (fd == nil)
+		return;
+	cka := array [ncookies] of ref Cookie;
+	ix := getallcookies(doms, cka, 0);
+	mergesort(cka, nil, SORT_TOUCHED);
+
+	for (i := 0; i < ix; i++) {
+		ck := cka[i];
+		if (ck.expire > now)
+			sys->fprint(fd, "%s\n", cookie2str(cka[i]));
+	}
+}
+
+parsecookie(dom, path, cookie: string): ref Cookie
+{
+	defpath := "/";
+	if (path != nil)
+		(defpath, nil) = S->splitr(path, "/");
+
+	(nil, toks) := sys->tokenize(cookie, ";");
+	namval := hd toks;
+	toks = tl toks;
+
+	(name, value) := S->splitl(namval, "=");
+	name = trim(name);
+	if (value != nil && value[0] == '=')
+		value = value[1:];
+	value = trim(value);
+
+	ck := ref Cookie(name, value, dom, defpath, -1, 0, 0, nil);
+	for (; toks != nil; toks = tl toks) {
+		(name, value) = S->splitl(hd toks, "=");
+		if (value != nil && value[0] == '=')
+			value = value[1:];
+		name = trim(name);
+		value = trim(value);
+		case S->tolower(name) {
+		"domain" =>
+			ck.dom = value;
+		"expires" =>
+			ck.expire = date2sec(value);
+		"path" =>
+			ck.path = value;
+		"secure" =>
+			ck.secure = 1;
+		}
+	}
+	if (ckcookie(ck, dom, path))
+		return ck;
+	return nil;
+}
+
+# Top Level Domains as defined in Netscape cookie spec
+tld := array [] of {
+	".com", ".edu", ".net", ".org", ".gov", ".mil", ".int"
+};
+
+ckcookie(ck: ref Cookie, host, path: string): int
+{
+#dumpcookie(ck, "CKCOOKIE");
+	if (ck == nil)
+		return 0;
+	if (ck.path == "" || ck.dom == "")
+		return 0;
+	if (host == "" || path == "")
+		return 1;
+
+# netscape does no path check on accpeting a cookie
+# any page can set a cookie on any path within its domain.
+# the filtering is done when sending cookies back to the server
+#	if (!S->prefix(ck.path, path))
+#		return 0;
+
+	if (host == ck.dom)
+		return 1;
+	if (ck.dom[0] != '.' || len host < len ck.dom)
+		return 0;
+
+	ipaddr := S->drop(host, ".0123456789") == nil;
+	if (ipaddr)
+		# ip addresses have to match exactly
+		return 0;
+
+	D := host[len host - len ck.dom:];
+	if (D != ck.dom)
+		return 0;
+
+	# netscape specific policy
+	ndots := 0;
+	for (i := 0; i < len D; i++)
+		if (D[i] == '.')
+			ndots++;
+	for (i = 0; i < len tld; i++) {
+		if (len D >= len tld[i] && D[len D - len tld[i]:] == tld[i]) {
+			if (ndots < 2)
+				return 0;
+			return 1;
+		}
+	}
+	if (ndots < 3)
+		return 0;
+	return 1;
+}
+
+trim(s: string): string
+{
+	is := 0;
+	ie := len s;
+	while(is < ie) {
+		c := s[is];
+		if(!(c == ' ' || c == '\t'))
+			break;
+		is++;
+	}
+	if(is == ie)
+		return "";
+	while(ie > is) {
+		c := s[ie-1];
+		if(!(c == ' ' || c == '\t'))
+			break;
+		ie--;
+	}
+	if(is >= ie)
+		return "";
+	if(is == 0 && ie == len s)
+		return s;
+	return s[is:ie];
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
+
+date2sec(date: string): int
+{
+	Tm: import daytime;
+	tm := daytime->string2tm(date);
+	if(tm == nil || tm.year < 70 || tm.zone != "GMT")
+		t := -1;
+	else
+		t = daytime->tm2epoch(tm);
+	return t;
+}
+
+dumpcookie(ck: ref Cookie, msg: string)
+{
+	if (msg != nil)
+		sys->print("%s: ", msg);
+	if (ck == nil)
+		sys->print("NIL\n");
+	else {
+		dbgval := ck.value;
+		if (len dbgval > 10)
+			dbgval = dbgval[:10];
+		sys->print("dom[%s], path[%s], name[%s], value[%s], secure=%d\n", ck.dom, ck.path, ck.name, dbgval, ck.secure);
+	}
+}
+
+splitdom(s: string): (string, string)
+{
+	for (ie := len s -1; ie > 0; ie--)
+		if (s[ie] == '.')
+			break;
+	return (s[:ie], s[ie:]);
+}
--- /dev/null
+++ b/appl/charon/cookiesrv.m
@@ -1,0 +1,12 @@
+Cookiesrv: module {
+	PATH: con "/dis/charon/cookiesrv.dis";
+
+	Client: adt {
+		fd: ref Sys->FD;
+		set: fn(c: self ref Client,host, path, cookie: string);
+		getcookies: fn(c: self ref Client, host, path: string, secure: int): string;
+	};
+
+	# save interval is in minutes
+	start: fn(path: string, saveinterval: int): ref Client;
+};
--- /dev/null
+++ b/appl/charon/ctype.b
@@ -1,0 +1,70 @@
+implement Ctype;
+
+include "ctype.m";
+
+ctype = array[NCTYPE] of {
+#0000 	0001  	0002  	0003  	0004  	0005  	0006  	0007  
+C,	C,	C,	C,	C,	C,	C,	C,
+#0008\b	0009 \t	000a \n	000b \v	000c \f	000d \r	000e  	000f  
+C,	W,	W,	C,	C,	W,	C,	C,
+#0010  	0011  	0012  	0013  	0014  	0015  	0016  	0017  
+C,	C,	C,	C,	C,	C,	C,	C,
+#0018  	0019  	001a  	001b  	001c  	001d  	001e  	001f  
+C,	C,	C,	C,	C,	C,	C,	C,
+#0020  	0021 !	0022 "	0023 #	0024 $	0025 %	0026 &	0027 '
+W,	P,	P,	P,	P,	P,	P,	P,
+#0028 (	0029 )	002a *	002b +	002c ,	002d -	002e .	002f /
+P,	P,	P,	P,	P,	N,	N,	P,
+#0030 0	0031 1	0032 2	0033 3	0034 4	0035 5	0036 6	0037 7
+D,	D,	D,	D,	D,	D,	D,	D,
+#0038 8	0039 9	003a :	003b ;	003c <	003d =	003e >	003f ?
+D,	D,	P,	P,	P,	P,	P,	P,
+#0040 @	0041 A	0042 B	0043 C	0044 D	0045 E	0046 F	0047 G
+P,	U,	U,	U,	U,	U,	U,	U,
+#0048 H	0049 I	004a J	004b K	004c L	004d M	004e N	004f O
+U,	U,	U,	U,	U,	U,	U,	U,
+#0050 P	0051 Q	0052 R	0053 S	0054 T	0055 U	0056 V	0057 W
+U,	U,	U,	U,	U,	U,	U,	U,
+#0058 X	0059 Y	005a Z	005b [	005c \	005d ]	005e ^	005f _
+U,	U,	U,	P,	P,	P,	P,	S,
+#0060 `	0061 a	0062 b	0063 c	0064 d	0065 e	0066 f	0067 g
+P,	L,	L,	L,	L,	L,	L,	L,
+#0068 h	0069 i	006a j	006b k	006c l	006d m	006e n	006f o
+L,	L,	L,	L,	L,	L,	L,	L,
+#0070 p	0071 q	0072 r	0073 s	0074 t	0075 u	0076 v	0077 w
+L,	L,	L,	L,	L,	L,	L,	L,
+#0078 x	0079 y	007a z	007b {	007c |	007d }	007e ~	007f 
+L,	L,	L,	P,	P,	P,	P,	C,
+#0080 €	0081 	0082 ‚	0083 ƒ	0084 „	0085 …	0086 †	0087 ‡
+C,	C,	C,	C,	C,	C,	C,	C,
+#0088 ˆ	0089 ‰	008a Š	008b ‹	008c Œ	008d 	008e Ž	008f 
+C,	C,	C,	C,	C,	C,	C,	C,
+#0090 	0091 ‘	0092 ’	0093 “	0094 ”	0095 •	0096 –	0097 —
+C,	C,	C,	C,	C,	C,	C,	C,
+#0098 ˜	0099 ™	009a š	009b ›	009c œ	009d 	009e ž	009f Ÿ
+C,	C,	C,	C,	C,	C,	C,	C,
+#00a0  	00a1 ¡	00a2 ¢	00a3 £	00a4 ¤	00a5 ¥	00a6 ¦	00a7 §
+P,	P,	P,	P,	P,	P,	P,	P,
+#00a8 ¨	00a9 ©	00aa ª	00ab «	00ac ¬	00ad ­	00ae ®	00af ¯
+P,	P,	P,	P,	P,	P,	P,	P,
+#00b0 °	00b1 ±	00b2 ²	00b3 ³	00b4 ´	00b5 µ	00b6 ¶	00b7 ·
+P,	P,	P,	P,	P,	P,	P,	P,
+#00b8 ¸	00b9 ¹	00ba º	00bb »	00bc ¼	00bd ½	00be ¾	00bf ¿
+P,	P,	P,	P,	P,	P,	P,	P,
+#00c0 À	00c1 Á	00c2 Â	00c3 Ã	00c4 Ä	00c5 Å	00c6 Æ	00c7 Ç
+U,	U,	U,	U,	U,	U,	U,	U,
+#00c8 È	00c9 É	00ca Ê	00cb Ë	00cc Ì	00cd Í	00ce Î	00cf Ï
+U,	U,	U,	U,	U,	U,	U,	U,
+#00d0 Ð	00d1 Ñ	00d2 Ò	00d3 Ó	00d4 Ô	00d5 Õ	00d6 Ö	00d7 ×
+U,	U,	U,	U,	U,	U,	U,	P,
+#00d8 Ø	00d9 Ù	00da Ú	00db Û	00dc Ü	00dd Ý	00de Þ	00df ß
+U,	U,	U,	U,	U,	U,	U,	L,
+#00e0 à	00e1 á	00e2 â	00e3 ã	00e4 ä	00e5 å	00e6 æ	00e7 ç
+L,	L,	L,	L,	L,	L,	L,	L,
+#00e8 è	00e9 é	00ea ê	00eb ë	00ec ì	00ed í	00ee î	00ef ï
+L,	L,	L,	L,	L,	L,	L,	L,
+#00f0 ð	00f1 ñ	00f2 ò	00f3 ó	00f4 ô	00f5 õ	00f6 ö	00f7 ÷
+L,	L,	L,	L,	L,	L,	L,	P,
+#00f8 ø	00f9 ù	00fa ú	00fb û	00fc ü	00fd ý	00fe þ	00ff ÿ
+L,	L,	L,	L,	L,	L,	L,	L
+};
--- /dev/null
+++ b/appl/charon/ctype.m
@@ -1,0 +1,24 @@
+Ctype: module
+{
+	PATH: con "/dis/charon/ctype.dis";
+
+	# Classify first NCTYPE chars of Unicode into one of
+	#
+	#   W: whitespace
+	#   D: decimal digit
+	#   L: lowercase letter
+	#   U: uppercase letter
+	#   N: '.' or '-' (parts of certain kinds of names)
+	#   S: '_' (parts of other kinds of names)
+	#   P: printable other than all of above
+	#   C: control other than whitespace
+	#
+	# These are separate bits, so can test for, e.g., ctype[c]&(U|L),
+	# but only one is set for any particular character,
+	# so can use faster ctype[c]==W too.
+
+	W, D, L, U, N, S, P, C: con byte (1<<iota);
+	NCTYPE: con 256;
+
+	ctype: array of byte;
+};
--- /dev/null
+++ b/appl/charon/date.b
@@ -1,0 +1,62 @@
+implement Date;
+
+include "common.m";
+include "date.m";
+include "daytime.m";
+	daytime: Daytime;
+	Tm: import daytime;
+
+sys: Sys;
+CU: CharonUtils;
+
+wdayname := array[] of {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+monname := array[] of {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+init(cu: CharonUtils)
+{
+	sys = load Sys Sys->PATH;
+	CU = cu;
+	daytime = load Daytime Daytime->PATH;
+	if (daytime==nil)
+		raise sys->sprint("EXInternal: can't load Daytime: %r");
+}
+
+# print dates in the format
+# Wkd, DD Mon YYYY HH:MM:SS GMT
+
+dateconv(t: int): string
+{
+	tm : ref Tm;
+	tm = daytime->gmt(t);
+	return sys->sprint("%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT",
+		wdayname[tm.wday], tm.mday, monname[tm.mon], tm.year+1900,
+		tm.hour, tm.min, tm.sec);
+}
+
+# parse a date and return the seconds since the epoch
+# return 0 for a failure
+#
+# need to handle three formats (we'll be a bit more tolerant)
+#  Sun, 06 Nov 1994 08:49:37 GMT  (rfc822+rfc1123; preferred)
+#  Sunday, 06-Nov-94 08:49:37 GMT (rfc850, obsoleted by rfc1036)
+#  Sun Nov  6 08:49:37 1994	  (ANSI C's asctime() format; GMT assumed)
+
+date2sec(date : string): int
+{
+	tm := daytime->string2tm(date);
+	if(tm == nil || tm.year < 70 || tm.zone != "GMT")
+		t := 0;
+	else
+		t = daytime->tm2epoch(tm);
+	return t;
+}
+
+now(): int
+{
+	return daytime->now();
+}
--- /dev/null
+++ b/appl/charon/date.m
@@ -1,0 +1,12 @@
+
+Date: module{
+	PATH : con "/dis/charon/date.dis";
+
+	dateconv: fn(secs :int): string; # returns an http formatted
+					 # date representing secs.
+	date2sec: fn(foo:string): int;   # parses a date and returns
+					 # number of secs since the 
+					 # epoch that it represents. 
+	now: fn(): int;		# so don't have to load daytime too
+	init: fn(cu: CharonUtils);
+};
--- /dev/null
+++ b/appl/charon/event.b
@@ -1,0 +1,273 @@
+implement Events;
+
+include "common.m";
+
+sys: Sys;
+url: Url;
+	Parsedurl: import url;
+
+archan : chan of (ref Event, int, int);
+
+init(ev : chan of ref Event)
+{
+	sys = load Sys Sys->PATH;
+	url = load Url Url->PATH;
+	if (url != nil)
+		url->init();
+	evchan = chan of ref Event;
+	archan = chan of (ref Event, int ,int);
+	spawn eventfilter(evchan, ev);
+}
+
+timer(go, tick : chan of int)
+{
+	go <-= sys->pctl(0, nil);
+	for(;;) {
+		ms := <- go;
+		sys->sleep(ms);
+		tick <-= 1;
+	}
+}
+
+# Handle mouse filtering and auto-repeating.
+# If we are waiting to send to Charon then accept events whilst they are
+# compatible with the pending event (eg. only keep most recent mouse move).
+# Once we have a recv event that isn't compatible with the pending send event
+# stop accepting events until the pending one has been sent.
+# Auto-repeat events are discarded if they cannot be sent or combined with the pending one.
+#
+eventfilter(fromc, toc : chan of ref Event)
+{
+	timergo := chan of int;
+	timertick := chan of int;
+	timeractive := 0;
+	spawn timer(timergo, timertick);
+	timerpid := <-timergo;
+
+	pendingev : ref Event;
+	bufferedev : ref Event;
+	dummyin := chan of ref Event;
+	dummyout := chan of ref Event;
+	inchan := fromc;
+	outchan := dummyout;
+
+	arev : ref Event;
+	aridlems, arms : int;
+
+	for (;;) alt {
+	ev := <- inchan =>
+		outchan = toc;
+		if (pendingev == nil) 
+			pendingev = ev;
+		else {
+			# an event is pending - see if we can combine/replace it
+			replace := evreplace(pendingev, ev);
+			if (replace != nil)
+				pendingev = replace;
+			else
+				bufferedev = ev;
+		}
+		if (bufferedev != nil)
+			inchan = dummyin;
+
+	outchan <- = pendingev =>
+		pendingev = bufferedev;
+		bufferedev = nil;
+		inchan = fromc;
+		if (pendingev == nil)
+			outchan = dummyout;
+
+	(arev, aridlems, arms) = <- archan =>
+		if (arev == nil) {
+			if(timeractive) {
+				# kill off old timer action so we don't get nasty
+				# holdovers from past autorepeats.
+				kill(timerpid);
+				spawn timer(timergo, timertick);
+				timerpid = <-timergo;
+				timeractive = 0;
+			}
+		} else if (!timeractive) {
+			timeractive = 1;
+			timergo <-= aridlems;
+		}
+
+	<- timertick =>
+		timeractive = 0;
+		if (arev != nil) {
+			if (pendingev == nil) {
+				pendingev = arev;
+			} else if (bufferedev == nil) {
+				replace := evreplace(pendingev, arev);
+				if (replace != nil)
+					pendingev = replace;
+				else
+					bufferedev = arev;
+			} else {
+				# try and combine with the buffered event
+				replace := evreplace(bufferedev, arev);
+				if (replace != nil)
+					bufferedev = replace;
+			} # else: discard auto-repeat event
+
+			if (bufferedev != nil)
+				inchan = dummyin;
+
+			# kick-start sends (we always have something to send)
+			outchan = toc;
+			timergo <- = arms;
+			timeractive = 1;
+		}
+	}
+}
+
+evreplace(oldev, newev : ref Event) : ref Event
+{
+	pick n := newev {
+	Emouse =>
+		pick o := oldev {
+		Emouse =>
+			if (n.mtype == o.mtype && (n.mtype == Mmove || n.mtype == Mldrag || n.mtype == Mmdrag || n.mtype == Mrdrag))
+				return newev;
+		}
+	Equit =>
+		# always takes precedence
+		return newev;
+	Ego =>
+		pick o := oldev {
+		Ego =>
+			if (n.target == o.target)
+				return newev;
+		}
+	Escroll =>
+		pick o := oldev {
+		Escroll =>
+			if (n.frameid == o.frameid)
+				return newev;
+		}
+	Escrollr =>
+		pick o := oldev {
+		Escrollr =>
+			if (n.frameid == o.frameid)
+				return newev;
+		}
+	Esettext =>
+		pick o := oldev {
+		Esettext =>
+			if (n.frameid == o.frameid)
+				return newev;
+		}
+	Edismisspopup =>
+		if (tagof oldev == tagof Event.Edismisspopup)
+			return newev;
+	* =>
+		return nil;
+	}
+	return nil;
+}
+
+autorepeat(ev : ref Event, idlems, ms : int)
+{
+	archan <- = (ev, idlems, ms);
+}
+
+Event.tostring(ev: self ref Event) : string
+{
+	s := "?";
+	pick e := ev {
+		Ekey =>
+			t : string;
+			case e.keychar {
+			' ' =>	 t = "<SP>";
+			'\t' => t = "<TAB>";
+			'\n' => t = "<NL>";
+			'\r' => t = "<CR>";
+			'\b' => t = "<BS>";
+			16r7F => t = "<DEL>";
+			Kup => t = "<UP>";
+			Kdown => t = "<DOWN>";
+			Khome => t = "<HOME>";
+			Kleft => t = "<LEFT>";
+			Kright => t = "<RIGHT>";
+			Kend => t = "<END>";
+			* => t = sys->sprint("%c", e.keychar);
+			}
+			s = sys->sprint("key %d = %s", e.keychar, t);
+		Emouse =>
+			t := "?";
+			case e.mtype {
+			Mmove => t = "move";
+			Mlbuttondown => t = "lbuttondown";
+			Mlbuttonup => t = "lbuttonup";
+			Mldrag => t = "ldrag";
+			Mmbuttondown => t = "mbuttondown";
+			Mmbuttonup => t = "mbuttonup";
+			Mmdrag => t = "mdrag";
+			Mrbuttondown => t = "rbuttondown";
+			Mrbuttonup => t = "rbuttonup";
+			Mrdrag => t = "rdrag";
+			}
+			s = sys->sprint("mouse (%d,%d) %s", e.p.x, e.p.y, t);
+		Emove =>
+			s = sys->sprint("move (%d,%d)", e.p.x, e.p.y);
+		Ereshape =>
+			s = sys->sprint("reshape (%d,%d) (%d,%d)", e.r.min.x, e.r.min.y, e.r.max.x, e.r.max.y);
+		Equit =>
+			s = "quit";
+		Estop =>
+			s = "stop";
+		Eback =>
+			s = "back";
+		Efwd =>
+			s = "fwd";
+		Eform =>
+			case e.ftype {
+			EFsubmit => s = "form submit";
+			EFreset => s = "form reset";
+			}
+		Eformfield =>
+			case e.fftype {
+			EFFblur => s = "formfield blur";
+			EFFfocus => s = "formfield focus";
+			EFFclick => s = "formfield click";
+			EFFselect => s = "formfield select";
+			EFFredraw => s = "formfield redraw";
+			}
+		Ego =>
+			s = "go(";
+			case e.gtype {
+			EGlocation or
+			EGnormal or
+			EGreplace => s += e.url;
+			EGreload => s += "RELOAD";
+			EGforward => s += "FORWARD";
+			EGback => s += "BACK";
+			EGdelta => s += "HISTORY[" + string e.delta + "]";
+			}
+			s += ", " + e.target + ")";
+		Esubmit =>
+			if(e.subkind == CharonUtils->HGet)
+				s = "GET";
+			else
+				s = "POST";
+			s = "submit(" + s;
+			s += ", " + e.action.tostring();
+			s += ", " + e.target + ")";
+		Escroll =>
+			s = "scroll(" + string e.frameid + ", (" + string e.pt.x + ", " + string e.pt.y + "))";
+		Escrollr =>
+			s = "scrollr(" + string e.frameid + ", (" + string e.pt.x + ", " + string e.pt.y + "))";
+		Esettext =>
+			s = "settext(frameid=" + string e.frameid + ", text=" + e.text + ")";
+		Elostfocus =>
+			s = "lostfocus";
+		Edismisspopup =>
+			s = "dismisspopup";
+	}
+	return s;
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
--- /dev/null
+++ b/appl/charon/event.m
@@ -1,0 +1,100 @@
+Events: module {
+	PATH: con "/dis/charon/event.dis";
+	Event: adt {
+		pick {
+			Ekey =>
+				keychar: int;		# Unicode char for pressed key
+			Emouse =>
+				p: Draw->Point;	# coords of pointer
+				mtype: int;		# Mmove, etc.
+			Emove =>
+				p: Draw->Point;	# new top-left of moved window
+			Ereshape =>
+				r: Draw->Rect;		# new window place and size
+			Equit =>
+				dummy: int;
+			Estop =>
+				dummy: int;
+			Eback =>
+				dummy: int;
+			Efwd =>
+				dummy: int;
+			Eform =>
+				frameid: int;		# which frame is form in
+				formid: int;		# which form in the frame
+				ftype: int;			# EFsubmit or EFreset
+			Eformfield =>
+				frameid: int;		# which frame is form in
+				formid: int;		# which form in the frame
+				fieldid: int;		# which formfield in the form
+				fftype: int;		# EFFblur, EFFfocus, etc.
+			Ego =>
+				url: string;			# where to go
+				target: string;		# frame to replace
+				delta: int;		# History.go(delta)
+				gtype: int;
+			Esubmit =>
+				subkind: int;		# CU->HGet or CU->HPost
+				action: ref Url->Parsedurl;
+				data: string;
+				target: string;
+			Escroll or Escrollr =>
+				frameid: int;
+				pt: Draw->Point;
+			Esettext =>
+				frameid: int;
+				url: ref Url->Parsedurl;
+				text: string;
+			Elostfocus =>			# main window has lost keyboard focus
+				dummy: int;
+			Edismisspopup =>		# popup window has been dismissed by gui
+				dummy: int;
+		}
+
+		tostring: fn(e: self ref Event) : string;
+	};
+
+	# Events sent to scripting engines
+	ScriptEvent: adt {
+		kind: int;
+		frameid: int;
+		formid: int;
+		fieldid: int;
+		anchorid: int;
+		imageid: int;
+		x: int;
+		y: int;
+		which: int;
+		script: string;
+		reply: chan of string;	# onreset/onsubmit reply channel
+		ms: int;
+	};
+
+	# ScriptEvent kinds
+	SEonclick, SEondblclick, SEonkeydown, SEonkeypress, SEonkeyup,
+		SEonmousedown, SEonmouseover, SEonmouseout, SEonmouseup, SEonblur, SEonfocus,
+		SEonchange, SEonload, SEtimeout, SEonabort, SEonerror,
+		SEonreset, SEonresize, SEonselect, SEonsubmit, SEonunload, SEscript, SEinterval, SEnone : con 1 << iota;
+
+	# some special keychars (use Unicode Private Area)
+	Kup, Kdown, Khome, Kleft, Kright, Kend, Kaup, Kadown : con (iota + 16rF000);
+
+	# Mouse event subtypes
+	Mmove, Mlbuttondown, Mlbuttonup, Mldrag, Mldrop,
+		Mmbuttondown, Mmbuttonup, Mmdrag,
+		Mrbuttondown, Mrbuttonup, Mrdrag,
+		Mhold : con iota;
+
+	# Form event subtypes
+	EFsubmit, EFreset : con iota;
+
+	# FormField event subtypes
+	EFFblur, EFFfocus, EFFclick, EFFselect, EFFredraw, EFFnone : con iota;
+
+	# Go event subtypes
+	EGnormal, EGreplace, EGreload, EGforward, EGback, EGdelta, EGlocation: con iota;
+
+	init: fn(evchan : chan of ref Event);
+	autorepeat: fn(ev : ref Event, idlems, ms : int);
+	evchan: chan of ref Event;
+};
--- /dev/null
+++ b/appl/charon/file.b
@@ -1,0 +1,133 @@
+implement Transport;
+
+include "common.m";
+include "transport.m";
+
+# local copies from CU
+sys: Sys;
+U: Url;
+	Parsedurl: import U;
+CU: CharonUtils;
+	Netconn, ByteSource, Header, config : import CU;
+
+dbg := 0;
+
+init(c: CharonUtils)
+{
+	CU = c;
+	sys = load Sys Sys->PATH;
+	U = load Url Url->PATH;
+	if (U != nil)
+		U->init();
+	dbg = int (CU->config).dbg['n'];
+}
+
+connect(nc: ref Netconn, nil: ref ByteSource)
+{
+	nc.connected = 1;
+	nc.state = CU->NCgethdr;
+	return;
+}
+
+writereq(nil: ref Netconn, nil: ref ByteSource)
+{
+	return;
+}
+
+gethdr(nc: ref Netconn, bs: ref ByteSource)
+{
+	u := bs.req.url;
+	f := u.path;
+	hdr := Header.new();
+	nc.conn = ref Dial->Connection;
+	nc.conn.dfd = sys->open(f, sys->OREAD);
+	if(nc.conn.dfd == nil) {
+		if(dbg)
+			sys->print("file %d: can't open %s: %r\n", nc.id, f);
+		# Could examine %r to distinguish between NotFound
+		# and Forbidden and other, but string is OS-dependent.
+		hdr.code = CU->HCNotFound;
+		bs.hdr = hdr;
+		nc.connected = 0;
+		return;
+	}
+
+	(ok, statbuf) := sys->fstat(nc.conn.dfd);
+	if(ok < 0) {
+		bs.err = "stat error";
+		return;
+	}
+
+	if (statbuf.mode & Sys->DMDIR) {
+		bs.err = "Directories not implemented";
+		return;
+	}
+
+	# assuming file (not directory)
+	n := int statbuf.length;
+	hdr.length = n;
+	if(n > sys->ATOMICIO)
+		n = sys->ATOMICIO;
+	a := array[n] of byte;
+	n = sys->read(nc.conn.dfd, a, n);
+	if(dbg)
+		sys->print("file %d: initial read %d bytes\n", nc.id, n);
+	if(n < 0) {
+		bs.err = "read error";
+		return;
+	}
+	hdr.setmediatype(f, a[0:n]);
+	hdr.base = hdr.actual = bs.req.url;
+	if(dbg)
+		sys->print("file %d: hdr has mediatype=%s, length=%d\n",
+			nc.id, CU->mnames[hdr.mtype], hdr.length);
+	bs.hdr = hdr;
+	if(n == len a)
+		nc.tbuf = a;
+	else
+		nc.tbuf = a[0:n];
+}
+
+getdata(nc: ref Netconn, bs: ref ByteSource): int
+{
+	dfd := nc.conn.dfd;
+	if (dfd == nil)
+		return -1;
+	if (bs.data == nil || bs.edata >= len bs.data) {
+		closeconn(nc);
+		return 0;
+	}
+	buf := bs.data[bs.edata:];
+	n := len buf;
+	if (nc.tbuf != nil) {
+		# initial overread of header
+		if (n >= len nc.tbuf) {
+			n = len nc.tbuf;
+			buf[:] = nc.tbuf;
+			nc.tbuf = nil;
+			return n;
+		}
+		buf[:] = nc.tbuf[:n];
+		nc.tbuf = nc.tbuf[n:];
+		return n;
+	}
+	n = sys->read(dfd, buf, n);
+	if(dbg > 1)
+		sys->print("ftp %d: read %d bytes\n", nc.id, n);
+	if(n <= 0) {
+		bs.err = sys->sprint("%r");
+		closeconn(nc);
+	}
+	return n;
+}
+
+defaultport(nil: string) : int
+{
+	return 0;
+}
+
+closeconn(nc: ref Netconn)
+{
+	nc.conn = nil;
+	nc.connected = 0;
+}
--- /dev/null
+++ b/appl/charon/ftp.b
@@ -1,0 +1,313 @@
+implement Transport;
+
+include "common.m";
+include "transport.m";
+
+# local copies from CU
+sys: Sys;
+U: Url;
+	Parsedurl: import U;
+S: String;
+DI: Dial;
+CU: CharonUtils;
+	Netconn, ByteSource, Header, config: import CU;
+
+FTPPORT: con 21;
+
+# Return codes
+Extra, Success, Incomplete, TempFail, PermFail : con (1+iota);
+
+cmdbuf := array[200] of byte;
+dbg := 0;
+
+init(c: CharonUtils)
+{
+	CU = c;
+	sys = load Sys Sys->PATH;
+	S = load String String->PATH;
+	U = load Url Url->PATH;
+	if (U != nil)
+		U->init();
+	DI = CU->DI;
+	dbg = int (CU->config).dbg['n'];
+}
+
+connect(nc: ref Netconn, bs: ref ByteSource)
+{
+	port := nc.port;
+	if(port == 0)
+		port = FTPPORT;
+	addr := DI->netmkaddr(nc.host, "net", string port);
+	if(dbg)
+		sys->print("ftp %d: dialing %s\n", nc.id, addr);
+	err := "";
+	ctlfd : ref sys->FD = nil;
+	nc.conn = DI->dial(addr, nil);
+	if(nc.conn == nil) {
+		syserr := sys->sprint("%r");
+		if(S->prefix("cs: dialup", syserr))
+			err = syserr[4:];
+		else if(S->prefix("cs: dns: no translation found", syserr))
+			err = "unknown host";
+		else
+			err = sys->sprint("couldn't connect: %s", syserr);
+	}
+	else {
+		if(dbg)
+			sys->print("ftp %d: connected\n", nc.id);
+		ctlfd = nc.conn.dfd;
+		# use cfd to hold control connection so can use dfd to hold data connection
+		nc.conn.cfd = ctlfd;
+		nc.conn.dfd = nil;
+
+		# look for Hello
+		(code, msg) := getreply(nc, ctlfd);
+		if(code != Success)
+			err = "instead of hello: " + msg;
+		else {
+			# logon
+			err = sendrequest(nc, ctlfd, "USER anonymous");
+			if(err == "") {
+				(code, msg) = getreply(nc, ctlfd);
+				if(code == Incomplete) {
+					# need password
+					err = sendrequest(nc, ctlfd, "PASS webget@webget.com");
+					if(err == "")
+						(code, msg) = getreply(nc, ctlfd);
+				}
+				if(err == "") {
+					if(code != Success)
+						err =  "login failed: " + msg;
+
+					# image type
+					err = sendrequest(nc, ctlfd, "TYPE I");
+					if(err == "") {
+						(code, msg) = getreply(nc, ctlfd);
+						if(code != Success)
+							err =  "can't set type I: " + msg;
+					}
+				}
+			}
+		}
+	}
+	if(err == "") {
+		nc.connected = 1;
+		nc.state = CU->NCgethdr;
+	}
+	else {
+		if(dbg)
+			sys->print("ftp %d: connection failed: %s\n", nc.id, err);
+		bs.err = err;
+		closeconn(nc);
+	}
+}
+
+# Ask ftp server on ctlfd for passive port and dial it
+dialdata(nc: ref Netconn, ctlfd: ref sys->FD) : string
+{
+	# put in passive mode
+	sendrequest(nc, ctlfd, "PASV");
+	(code, msg) := getreply(nc, ctlfd);
+	if(code != Success)
+		return "can't use passive mode: " + msg;
+	(paddr, pport) := passvap(msg);
+	if(paddr == "")
+		return "passive mode protocol botch: " + msg;
+	# dial data port
+	daddr := DI->netmkaddr(paddr, "net", pport);
+	if(dbg)
+		sys->print("ftp %d: dialing data %s", nc.id, daddr);
+	dnet := DI->dial(daddr, nil);
+	if(dnet == nil)
+		return "data dial error";
+	nc.conn.dfd = dnet.dfd;
+	return "";
+}
+
+writereq(nc: ref Netconn, bs: ref ByteSource)
+{
+	ctlfd := nc.conn.cfd;
+	CU->assert(ctlfd != nil);
+	err := dialdata(nc, ctlfd);
+	if(err == "") {
+		# tell remote to send file
+		err = sendrequest(nc, ctlfd, "RETR " + bs.req.url.path);
+	}
+	if(err != "") {
+		if(dbg)
+			sys->print("ftp %d: error: %s\n", nc.id, err);
+		bs.err = err;
+		closeconn(nc);
+	}
+}
+
+gethdr(nc: ref Netconn, bs: ref ByteSource)
+{
+	hdr := Header.new();
+	bs.hdr = hdr;
+	err := "";
+	ctlfd := nc.conn.cfd;
+	dfd := nc.conn.dfd;
+	CU->assert(ctlfd != nil && dfd != nil);
+	(code, msg) := getreply(nc, ctlfd);
+	if(code != Extra) {
+		if(dbg)
+			sys->print("ftp %d: retrieve failed: %s\n",
+				nc.id, msg);
+		hdr.code = CU->HCNotFound;
+		hdr.msg = "Not found";
+	}
+	else {
+		hdr.code = CU->HCOk;
+
+		# try to guess media type before returning header
+		buf := array[sys->ATOMICIO] of byte;
+		n := sys->read(dfd, buf, len buf);
+		if(dbg)
+			sys->print("ftp %d: read %d bytes\n", nc.id, n);
+		if(n < 0)
+			err = "error reading data";
+		else {
+			if(n > 0)
+				nc.tbuf = buf[0:n];
+			else
+				nc.tbuf = nil;
+			hdr.setmediatype(bs.req.url.path, nc.tbuf);
+			hdr.actual = bs.req.url;
+			hdr.base = hdr.actual;
+			hdr.length = -1;
+			hdr.msg = "Ok";
+		}
+	}
+	if(err != "") {
+		if(dbg)
+			sys->print("ftp %d: error %s\n", nc.id, err);
+		bs.err = err;
+		closeconn(nc);
+	}
+}
+
+getdata(nc: ref Netconn, bs: ref ByteSource): int
+{
+	dfd := nc.conn.dfd;
+	CU->assert(dfd != nil);
+	if (bs.data == nil || bs.edata >= len bs.data) {
+		closeconn(nc);
+		return 0;
+	}
+	buf := bs.data[bs.edata:];
+	n := len buf;
+	if (nc.tbuf != nil) {
+		# initial overread of header
+		if (n >= len nc.tbuf) {
+			n = len nc.tbuf;
+			buf[:] = nc.tbuf;
+			nc.tbuf = nil;
+			return n;
+		}
+		buf[:] = nc.tbuf[:n];
+		nc.tbuf = nc.tbuf[n:];
+		return n;
+	}
+	n = sys->read(dfd, buf, n);
+	if(dbg > 1)
+		sys->print("ftp %d: read %d bytes\n", nc.id, n);
+	if(n <= 0) {
+		bs.err = "eof";
+		closeconn(nc);
+	}
+	return n;
+}
+
+# Send ftp request cmd along fd; return "" if OK else error string.
+sendrequest(nc: ref Netconn, fd: ref sys->FD, cmd: string) : string
+{
+	if(dbg > 1)
+		sys->print("ftp %d: send request: %s\n", nc.id, cmd);
+	cmd = cmd + "\r\n";
+	buf := array of byte cmd;
+	n := len buf;
+	if(sys->write(fd, buf, n) != n)
+		return sys->sprint("write error: %r");
+	return "";
+}
+
+# Get reply to ftp request along fd.
+# Reply may be more than one line ("commentary")
+# but ends with a line that has a status code in the first
+# three characters (a number between 100 and 600)
+# followed by a blank and a possible message.
+# If OK, return the hundreds digit of the status (which will
+# mean one of Extra, Success, etc.), and the whole
+# last line; else return (-1, "").
+getreply(nc: ref Netconn, fd: ref sys->FD) : (int, string)
+{
+	# Reply might contain more than one line,
+	# because there might be "commentary" lines.
+	i := 0;
+	j := 0;
+	aline: array of byte;
+	eof := 0;
+	for(;;) {
+		(aline, eof, i, j) = CU->getline(fd, cmdbuf, i, j);
+		if(eof)
+			break;
+		line := string aline;
+		n := len line;
+		if(n == 0)
+			break;
+		if(dbg > 1)
+			sys->print("ftp %d: got reply: %s\n", nc.id, line);
+		rv := int line;
+		if(rv >= 100 && rv < 600) {
+			# if line is like '123-stuff'
+			# then there will be more lines until
+			# '123 stuff'
+			if(len line<4 || line[3]==' ')
+				return (rv/100, line);
+		}
+	}
+	return (-1, "");
+}
+
+# Parse reply to PASSV to find address and port numbers.
+# This is AI because extant agents aren't good at following
+# the standard.
+passvap(s: string) : (string, string)
+{
+	addr := "";
+	port := "";
+	(nil, v) := S->splitl(s, "(");
+	if(v != "")
+		s = v[1:];
+	else
+		(nil, s) = S->splitl(s, "0123456789");
+	if(s != "") {
+		(n, l) := sys->tokenize(s, ",");
+		if(n >= 6) {
+			addr = hd l + ".";
+			l = tl l;
+			addr += hd l + ".";
+			l = tl l;
+			addr += hd l + ".";
+			l = tl l;
+			addr += hd l;
+			l = tl l;
+			p1 := int hd l;
+			p2 := int hd tl l;
+			port = string (((p1&255)<<8)|(p2&255));
+		}
+	}
+	return (addr, port);
+}
+
+defaultport(nil: string) : int
+{
+	return FTPPORT;
+}
+
+closeconn(nc: ref Netconn)
+{
+	nc.conn = nil;
+	nc.connected = 0;
+}
--- /dev/null
+++ b/appl/charon/gui.b
@@ -1,0 +1,560 @@
+# Gui implementation for running under wm (tk window manager)
+implement Gui;
+
+include "common.m";
+include "tk.m";
+include "tkclient.m";
+
+include "dialog.m";
+	dialog: Dialog;
+
+sys: Sys;
+
+D: Draw;
+	Font,Point, Rect, Image, Screen, Display: import D;
+
+CU: CharonUtils;
+
+E: Events;
+	Event: import E;
+
+tk: Tk;
+
+tkclient: Tkclient;
+
+WINDOW, CTLS, PROG, STATUS, BORDER, EXIT: con 1 << iota;
+REQD: con ~0;
+
+cfg := array[] of {
+	(REQD,	"entry .ctlf.url -bg white -font /fonts/lucidasans/unicode.7.font -height 16"),
+	(REQD,	"button .ctlf.back -bd 1 -command {send gctl back} -state disabled -text {back} -font /fonts/lucidasans/unicode.7.font"),
+	(REQD,	"button .ctlf.stop -bd 1 -command {send gctl stop} -state disabled -text {stop} -font /fonts/lucidasans/unicode.7.font"),
+	(REQD,	"button .ctlf.fwd -bd 1 -command {send gctl fwd} -state disabled -text {next} -font /fonts/lucidasans/unicode.7.font"),
+	(REQD,	"label .status.status -bd 1 -font /fonts/lucidasans/unicode.6.font -height 14 -anchor w"),
+	(REQD,	"button .ctlf.exit -bd 1 -bitmap exit.bit -command {send wm_title exit}"),
+	(REQD,	"frame .f -bd 0"),
+	(BORDER,	".f configure -bd 2 -relief sunken"),
+	(CTLS|EXIT,	"frame .ctlf"),
+	(STATUS,	"frame .status -bd 0"),
+	(STATUS,	"frame .statussep -bg black -height 1"),
+	(STATUS,	"button .status.snarf -text snarf -command {send gctl snarfstatus} -font /fonts/charon/plain.small.font"),
+
+	(CTLS,	"bind .ctlf.url <Key-\n> {send gctl go}"),
+	(CTLS,	"bind .ctlf.url <Key-\u0003> {send gctl copyurl}"),
+	(CTLS,	"bind .ctlf.url <Key-\u0016> {send gctl pasteurl}"),
+
+#	(PROG,	"canvas .prog -bd 0 -height 20"),
+#	(PROG,	"bind .prog <ButtonPress-1> {send gctl b1p %X %Y}"),
+	(CTLS,	"pack .ctlf.back .ctlf.stop .ctlf.fwd -side left -anchor w -fill y"),
+	(CTLS,	"pack .ctlf.url -side left -padx 2 -fill x -expand 1"),
+	(EXIT,	"pack .ctlf.exit -side right -anchor e"),
+	(CTLS|EXIT,	"pack .ctlf -side top -fill x"),
+	(REQD,	"pack .f -side top -fill both -expand 1"),
+#	(PROG,	"pack .prog -side bottom -fill x"),
+	(STATUS,	"pack .status.snarf -side right"),
+	(STATUS,	"pack .status.status -side right -fill x -expand 1"),
+	(STATUS,	"pack .statussep -side top -fill x"),
+	(STATUS,	"pack .status -side bottom -fill x"),
+	(CTLS|EXIT,	"pack propagate .ctlf 0"),
+	(STATUS,		"pack propagate .status 0"),
+};
+
+framebinds := array[] of {
+	"bind .f <Key> {send gctl k %s}",
+	"bind .f <FocusOut> {send gctl focusout}",
+	"bind .f <ButtonPress-1> {grab set .f;send gctl b1p %X %Y}",
+	"bind .f <Double-ButtonPress-1> {send gctl b1p %X %Y}",
+	"bind .f <ButtonRelease-1> {grab release .f;send gctl b1r %X %Y}",
+	"bind .f <Motion-Button-1> {send gctl b1d %X %Y}",
+	"bind .f <ButtonPress-2> {send gctl b2p %X %Y}",
+	"bind .f <Double-ButtonPress-2> {send gctl b2p %X %Y}",
+	"bind .f <ButtonRelease-2> {send gctl b2r %X %Y}",
+	"bind .f <Motion-Button-2> {send gctl b2d %X %Y}",
+	"bind .f <ButtonPress-3> {send gctl b3p %X %Y}",
+	"bind .f <Double-ButtonPress-3> {send gctl b3p %X %Y}",
+	"bind .f <ButtonRelease-3> {send gctl b3r %X %Y}",
+	"bind .f <Motion-Button-3> {send gctl b3d %X %Y}",
+	"bind .f <Motion> {send gctl m %X %Y}",
+};
+
+tktop: ref Tk->Toplevel;
+mousegrabbed := 0;
+offset: Point;
+ZP: con Point(0,0);
+popup: ref Popup;
+popuptk: ref Tk->Toplevel;
+gctl: chan of string;
+drawctxt: ref Draw->Context;
+
+realwin: ref Draw->Image;
+mask: ref Draw->Image;
+
+init(ctxt: ref Draw->Context, cu: CharonUtils): ref Draw->Context
+{
+	sys = load Sys Sys->PATH;
+	D = load Draw Draw->PATH;
+	CU = cu;
+	E = cu->E;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if(tkclient == nil)
+		raise sys->sprint("EXInternal: can't load module Tkclient: %r");
+	tkclient->init();
+
+	wmctl: chan of string;
+	buttons := parsebuttons((CU->config).buttons);
+	winopts := parsewinopts((CU->config).framework);
+
+	(tktop, wmctl) = tkclient->toplevel(ctxt, "", (CU->config).wintitle, buttons);
+
+	ctxt = tktop.ctxt.ctxt;
+	drawctxt = ctxt;
+	display = ctxt.display;
+
+	gctl = chan of string;
+	tk->namechan(tktop, gctl, "gctl");
+	tk->cmd(tktop, "pack propagate . 0");
+	filtertkcmds(tktop, winopts, cfg);
+	tkcmds(tktop, framebinds);
+	w := (CU->config).defaultwidth;
+	h := (CU->config).defaultheight;
+	tk->cmd(tktop, ". configure -width " + string w + " -height " + string h);
+	tk->cmd(tktop, "update");
+	tkclient->onscreen(tktop, nil);
+	tkclient->startinput(tktop, "kbd"::"ptr"::nil);
+	makewins();
+	mask = display.opaque;
+	progress = chan of Progressmsg;
+	pidc := chan of int;
+	spawn progmon(pidc);
+	<- pidc;
+	spawn evhandle(tktop, wmctl, E->evchan);
+	return ctxt;
+}
+
+parsebuttons(s: string): int
+{
+	b := 0;
+	(nil, toks) := sys->tokenize(s, ",");
+	for (;toks != nil; toks = tl toks) {
+		case hd toks {
+		"help" =>
+			b |= Tkclient->Help;
+		"resize" =>
+			b |= Tkclient->Resize;
+		"hide" =>
+			b |= Tkclient->Hide;
+		"plain" =>
+			b = Tkclient->Plain;
+		}
+	}
+	return b | Tkclient->Help;
+}
+
+parsewinopts(s: string): int
+{
+	b := WINDOW;
+	(nil, toks) := sys->tokenize(s, ",");
+	for (;toks != nil; toks = tl toks) {
+		case hd toks {
+		"status" =>
+			b |= STATUS;
+		"controls" or "ctls" =>
+			b |= CTLS;
+		"progress" or "prog" =>
+			b |= PROG;
+		"border" =>
+			b |= BORDER;
+		"exit" =>
+			b |= EXIT;
+		"all" =>
+			# note: "all" doesn't include 'EXIT' !
+			b |= WINDOW | STATUS | CTLS | PROG | BORDER;
+		}
+	}
+	return b;
+}
+
+filtertkcmds(top: ref Tk->Toplevel, filter: int, cmds: array of (int, string))
+{
+	for (i := 0; i < len cmds; i++) {
+		(val, cmd) := cmds[i];
+		if (val & filter) {
+			if ((e := tk->cmd(top, cmd)) != nil && e[0] == '!')
+				sys->print("tk error on '%s': %s\n", cmd, e);
+		}
+	}
+}
+
+tkcmds(top: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++)
+		if ((e := tk->cmd(top, cmds[i])) != nil && e[0] == '!')
+			sys->print("tk error on '%s': %s\n", cmds[i], e);
+}
+
+clientr(t: ref Tk->Toplevel, wname: string): Rect
+{
+	bd := int tk->cmd(t, wname + " cget -borderwidth");
+	x := bd + int tk->cmd(t, wname + " cget -actx");
+	y := bd + int tk->cmd(t, wname + " cget -acty");
+	w := int tk->cmd(t, wname + " cget -actwidth");
+	h := int tk->cmd(t, wname + " cget -actheight");
+	return Rect((x,y),(x+w,y+h));
+}
+
+progmon(pidc: chan of int)
+{
+	pidc <-= sys->pctl(0, nil);
+	for (;;) {
+		msg := <- progress;
+#prprog(msg);
+		# just handle stop button for now
+		if (msg.bsid == -1) {
+			case (msg.state) {
+			Pstart =>	stopbutton(1);
+			* =>		stopbutton(0);
+			}
+		}
+	}
+}
+
+st2s := array [] of {
+	Punused => "unused",
+	Pstart => "start",
+	Pconnected => "connected",
+	Psslconnected => "sslconnected",
+	Phavehdr => "havehdr",
+	Phavedata => "havedata",
+	Pdone => "done",
+	Perr => "error",
+	Paborted => "aborted",
+};
+
+prprog(m:Progressmsg)
+{
+	sys->print("%d %s %d%% %s\n", m.bsid, st2s[m.state], m.pcnt, m.s);
+}
+
+
+r2s(r: Rect): string
+{
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+winpos(t: ref Tk->Toplevel): Point
+{
+	return (int tk->cmd(t, ". cget -actx"), int tk->cmd(t, ". cget -acty"));
+}
+
+evhandle(t: ref Tk->Toplevel, wmctl: chan of string, evchan: chan of ref Event)
+{
+	for(;;) {
+		ev: ref Event = nil;
+		dismisspopup := 1;
+		alt {
+		s := <-gctl =>
+			(nil, l) := sys->tokenize(s, " ");
+			case hd l {
+			"focusout" =>
+				ev = ref Event.Elostfocus;
+			"b1p" or "b1r" or "b1d" or 
+			"b2p" or "b2r" or "b2d" or 
+			"b3p" or "b3r" or "b3d" or 
+			"m" =>
+				l = tl l;
+				pt := Point(int hd l, int hd tl l);
+				pt = pt.sub(offset);
+				mtype := s2mtype(s);
+				dismisspopup = 0;
+				if(mtype == E->Mlbuttondown) {
+					tk->cmd(t, "focus .f");
+					pu := popup;
+					if (pu != nil && !pu.r.contains(pt))
+						dismisspopup = 1;
+					pu = nil;
+				}
+				ev = ref Event.Emouse(pt, mtype);
+			"k" =>
+				dismisspopup = 0;
+				k := int hd tl l;
+				if(k != 0)
+					ev = ref Event.Ekey(k);
+			"back" =>
+				ev = ref Event.Eback;
+			"stop" =>
+				ev = ref Event.Estop;
+			"fwd" =>
+				ev = ref Event.Efwd;
+			"go" =>
+				url := tk->cmd(tktop, ".ctlf.url get");
+				if (url != nil)
+					ev = ref Event.Ego(url, nil, 0, E->EGnormal);
+			"copyurl" =>
+				url := tk->cmd(tktop, ".ctlf.url get");
+				snarfput(url);
+			"pasteurl" =>
+				url := tk->quote(tkclient->snarfget());
+				tk->cmd(tktop, ".ctlf.url delete 0 end");
+				tk->cmd(tktop, ".ctlf.url insert end " + url);
+				tk->cmd(tktop, "update");
+			"snarfstatus" =>
+				url := tk->cmd(tktop, ".status.status cget -text");
+				tkclient->snarfput(url);
+			}
+		s := <-t.ctxt.ctl or
+		s = <-t.wreq or
+		s = <-wmctl =>
+			case s {
+			"exit" =>
+				hidewins();
+				ev = ref Event.Equit(0);
+			"task" =>
+				if (cancelpopup())
+					evchan <-= ref Event.Edismisspopup;
+				tkclient->wmctl(t, s);
+				if(tktop.image == nil)
+					realwin = nil;
+			"help" =>
+				ev = ref Event.Ego((CU->config).helpurl, nil, 0, E->EGnormal);
+			* =>
+				if (s[0] == '!' && cancelpopup())
+					evchan <-= ref Event.Edismisspopup;
+				oldimg := t.image;
+				e := tkclient->wmctl(t, s);
+				if(s[0] == '!' && e == nil){
+					if(t.image != oldimg){
+						oldimg = nil;
+						makewins();
+						ev = ref Event.Ereshape(mainwin.r);
+					}
+					offset = tk->rect(tktop, ".f", 0).min;
+				}
+			}
+		s := <-t.ctxt.kbd =>
+			tk->keyboard(t, s);
+		s := <-t.ctxt.ptr =>
+			tk->pointer(t, *s);
+		}
+		if (dismisspopup) {
+			if (cancelpopup()) {
+				evchan <-= ref Event.Edismisspopup;
+			}
+		}
+		if (ev != nil)
+			evchan <-= ev;
+	}
+}
+
+s2mtype(s: string): int
+{
+	mtype := E->Mmove;
+	if(s[0] == 'm')
+		mtype = E->Mmove;
+	else {
+		case s[1] {
+		'1' =>
+			case s[2] {
+			'p' => mtype = E->Mlbuttondown;
+			'r' => mtype = E->Mlbuttonup;
+			'd' => mtype = E->Mldrag;
+			}
+		'2' =>
+			case s[2] {
+			'p' => mtype = E->Mmbuttondown;
+			'r' => mtype = E->Mmbuttonup;
+			'd' => mtype = E->Mmdrag;
+			}
+		'3' =>
+			case s[2] {
+			'p' => mtype = E->Mrbuttondown;
+			'r' => mtype = E->Mrbuttonup;
+			'd' => mtype = E->Mrdrag;
+			}
+		}
+	}
+	return mtype;
+}
+
+makewins()
+{
+	if(tktop.image == nil)
+		return;
+	screen := Screen.allocate(tktop.image, display.transparent, 0);
+	offset = tk->rect(tktop, ".f", 0).min;
+	r := tk->rect(tktop, ".f", Tk->Local);
+	realwin = screen.newwindow(r, D->Refnone, D->White);
+	realwin.origin(ZP, r.min);
+	if(realwin == nil)
+		raise sys->sprint("EXFatal: can't initialize windows: %r");
+
+	mainwin = display.newimage(realwin.r, realwin.chans, 0, D->White);
+	if(mainwin == nil)
+		raise sys->sprint("EXFatal: can't initialize windows: %r");
+}
+
+hidewins()
+{
+	tk->cmd(tktop, ". unmap");
+}
+
+snarfput(s: string)
+{
+	tkclient->snarfput(s);
+}
+
+setstatus(s: string)
+{
+	tk->cmd(tktop, ".status.status configure -text " + tk->quote(s));
+	tk->cmd(tktop, "update");
+}
+
+seturl(s: string)
+{
+	tk->cmd(tktop, ".ctlf.url delete 0 end");
+	tk->cmd(tktop, ".ctlf.url insert 0 " + tk->quote(s));
+	tk->cmd(tktop, "update");
+}
+
+auth(realm: string): (int, string, string)
+{
+	user := prompt(realm + " username?", nil).t1;
+	passwd := prompt("password?", nil).t1;
+	if(user == nil)
+		return (0, nil, nil);
+	return (1, user, passwd);
+}
+
+alert(msg: string)
+{
+sys->print("ALERT:%s\n", msg);
+	return;
+}
+
+confirm(msg: string): int
+{
+sys->print("CONFIRM:%s\n", msg);
+	return -1;
+}
+
+prompt(msg, dflt: string): (int, string)
+{
+	if(dialog == nil){
+		dialog = load Dialog Dialog->PATH;
+		dialog->init();
+	}
+	return (1, dialog->getstring(drawctxt, mainwin, msg));
+	# return (-1, "");
+}
+
+stopbutton(enable: int)
+{
+	state: string;
+	if (enable) {
+		tk->cmd(tktop, ".ctlf.stop configure -bg red -activebackground red -activeforeground white");
+		state = "normal";
+	} else {
+		tk->cmd(tktop, ".ctlf.stop configure -bg #dddddd");
+		state = "disabled";
+	}
+	tk->cmd(tktop, ".ctlf.stop configure -state " + state + ";update");
+}
+
+backbutton(enable: int)
+{
+	state: string;
+	if (enable) {
+		tk->cmd(tktop, ".ctlf.back configure -bg lime -activebackground lime -activeforeground red");
+		state = "normal";
+	} else {
+		tk->cmd(tktop, ".ctlf.back configure -bg #dddddd");
+		state = "disabled";
+	}
+	tk->cmd(tktop, ".ctlf.back configure -state " + state + ";update");
+}
+
+fwdbutton(enable: int)
+{
+	state: string;
+	if (enable) {
+		tk->cmd(tktop, ".ctlf.fwd  configure -bg lime -activebackground lime -activeforeground red");
+		state = "normal";
+	} else {
+		tk->cmd(tktop, ".ctlf.fwd configure -bg #dddddd");
+		state = "disabled";
+	}
+	tk->cmd(tktop, ".ctlf.fwd configure -state " + state + ";update");
+}
+
+flush(r: Rect)
+{
+	if(realwin != nil) {
+		oclipr := mainwin.clipr;
+		mainwin.clipr = r;
+		realwin.draw(r, mainwin, nil, r.min);
+		mainwin.clipr = oclipr;
+	}
+}
+
+clientfocus()
+{
+	tk->cmd(tktop, "focus .f");
+	tk->cmd(tktop, "update");
+}
+
+exitcharon()
+{
+	hidewins();
+	E->evchan <-= ref Event.Equit(0);
+}
+
+getpopup(r: Rect): ref Popup
+{
+	return nil;
+#	cancelpopup();
+##	img := screen.newwindow(r, D->White);
+#	img := display.newimage(r, screen.image.chans, 0, D->White);
+#	if (img == nil)
+#		return nil;
+#	winr := r.addpt(offset);	# race for offset
+#
+#	pos := "-x " + string winr.min.x + " -y " + string winr.min.y;
+#	(top, nil) := tkclient->toplevel(drawctxt, pos, nil, Tkclient->Plain);
+#	tk->namechan(top, gctl, "gctl");
+#	tk->cmd(top, "frame .f -bd 0 -bg white -width " + string r.dx() + " -height " + string r.dy());
+#	tkcmds(top, framebinds);
+#	tk->cmd(top, "pack .f; update");
+#	tkclient->onscreen(tktop, "onscreen");
+#	tkclient->startinput(tktop, "kbd"::"ptr"::nil);
+#	win := screen.newwindow(winr, D->Refbackup, D->White);
+#	if (win == nil)
+#		return nil;
+#	win.origin(r.min, winr.min);
+#
+#	popuptk = top;
+#	popup = ref Popup(r, img, win);
+## XXXX need to start a thread to feed mouse/kbd events from popup,
+## but we need to know when to tear it down.
+#	return popup;
+}
+
+cancelpopup(): int
+{
+	popuptk = nil;
+	pu := popup;
+	if (pu == nil)
+		return 0;
+	pu.image = nil;
+	pu.window = nil;
+	pu = nil;
+	popup = nil;
+	return 1;
+}
+
+Popup.flush(p: self ref Popup, r: Rect)
+{
+	win := p.window;
+	img := p.image;
+	if (win != nil && img != nil)
+		win.draw(r, img, nil, r.min);
+}
--- /dev/null
+++ b/appl/charon/gui.m
@@ -1,0 +1,48 @@
+Gui: module {
+	PATH: con "/dis/charon/gui.dis";
+
+	Progressmsg : adt {
+		bsid : int;
+		state : int;
+		pcnt : int;
+		s : string;
+	};
+
+	# clients should never capture Popup.image
+	# other than during drawing operations
+	Popup: adt {
+		r: Draw->Rect;
+		image: ref Draw->Image;
+		window: ref Draw->Image;
+
+		flush: fn(p: self ref Popup, r: Draw->Rect);
+	};
+
+	# Progress states
+	Punused, Pstart, Pconnected, Psslconnected, Phavehdr,
+	Phavedata, Pdone, Perr, Paborted : con iota;
+
+	display: ref Draw->Display;
+	mainwin: ref Draw->Image;
+	progress: chan of Progressmsg;
+
+	init: fn(ctxt: ref Draw->Context, cu: CharonUtils): ref Draw->Context;
+
+	snarfput: fn(s: string);
+	setstatus: fn(s: string);
+	seturl: fn(s: string);
+	auth: fn(realm: string) : (int, string, string);
+	alert: fn(msg: string);
+	confirm: fn(msg: string) : int;
+	prompt: fn(msg, dflt: string) : (int, string);
+	backbutton: fn(enable : int);
+	fwdbutton: fn (enable : int);
+
+	flush: fn (r : Draw->Rect);
+	clientfocus: fn();
+
+	getpopup: fn(r: Draw->Rect): ref Popup;
+	cancelpopup: fn(): int;
+
+	exitcharon: fn();
+};
--- /dev/null
+++ b/appl/charon/http.b
@@ -1,0 +1,1039 @@
+implement Transport;
+
+include "common.m";
+include "transport.m";
+include "date.m";
+
+#D: Date;
+# sslhs: SSLHS;
+ssl3: SSL3;
+Context: import ssl3;
+# Inferno supported cipher suites:
+ssl_suites := array [] of {
+	byte 0, byte 16r03,	# RSA_EXPORT_WITH_RC4_40_MD5
+	byte 0, byte 16r04,	# RSA_WITH_RC4_128_MD5
+	byte 0, byte 16r05,	# RSA_WITH_RC4_128_SHA
+	byte 0, byte 16r06,	# RSA_EXPORT_WITH_RC2_CBC_40_MD5
+	byte 0, byte 16r07,	# RSA_WITH_IDEA_CBC_SHA
+	byte 0, byte 16r08,	# RSA_EXPORT_WITH_DES40_CBC_SHA
+	byte 0, byte 16r09,	# RSA_WITH_DES_CBC_SHA
+	byte 0, byte 16r0A,	# RSA_WITH_3DES_EDE_CBC_SHA
+	
+	byte 0, byte 16r0B,	# DH_DSS_EXPORT_WITH_DES40_CBC_SHA
+	byte 0, byte 16r0C,	# DH_DSS_WITH_DES_CBC_SHA
+	byte 0, byte 16r0D,	# DH_DSS_WITH_3DES_EDE_CBC_SHA
+	byte 0, byte 16r0E,	# DH_RSA_EXPORT_WITH_DES40_CBC_SHA
+	byte 0, byte 16r0F,	# DH_RSA_WITH_DES_CBC_SHA
+	byte 0, byte 16r10,	# DH_RSA_WITH_3DES_EDE_CBC_SHA
+	byte 0, byte 16r11,	# DHE_DSS_EXPORT_WITH_DES40_CBC_SHA
+	byte 0, byte 16r12,	# DHE_DSS_WITH_DES_CBC_SHA
+	byte 0, byte 16r13,	# DHE_DSS_WITH_3DES_EDE_CBC_SHA
+	byte 0, byte 16r14,	# DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
+	byte 0, byte 16r15,	# DHE_RSA_WITH_DES_CBC_SHA
+	byte 0, byte 16r16,	# DHE_RSA_WITH_3DES_EDE_CBC_SHA
+	
+	byte 0, byte 16r17,	# DH_anon_EXPORT_WITH_RC4_40_MD5
+	byte 0, byte 16r18,	# DH_anon_WITH_RC4_128_MD5
+	byte 0, byte 16r19,	# DH_anon_EXPORT_WITH_DES40_CBC_SHA
+	byte 0, byte 16r1A,	# DH_anon_WITH_DES_CBC_SHA
+	byte 0, byte 16r1B,	# DH_anon_WITH_3DES_EDE_CBC_SHA
+	
+	byte 0, byte 16r1C,	# FORTEZZA_KEA_WITH_NULL_SHA
+	byte 0, byte 16r1D,	# FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA
+	byte 0, byte 16r1E,	# FORTEZZA_KEA_WITH_RC4_128_SHA
+};
+
+ssl_comprs := array [] of {byte 0};
+
+# local copies from CU
+sys: Sys;
+U: Url;
+	Parsedurl: import U;
+S: String;
+C: Ctype;
+T: StringIntTab;
+DI: Dial;
+CU: CharonUtils;
+	Netconn, ByteSource, Header, config, Nameval : import CU;
+
+ctype: array of byte;	# local copy of C->ctype
+
+HTTPD:		con 80;		# Default IP port
+HTTPSD:		con 443;	# Default IP port for HTTPS
+
+# For Inferno, won't be able to read more than this at one go anyway
+BLOCKSIZE:	con 1460;
+
+# HTTP/1.1 Spec says 5, but we've seen more than that in non-looping redirs
+# MAXREDIR:	con 10;
+
+# tstate bits
+THTTP_1_0, TPersist, TProxy, TSSL: con (1<<iota);
+
+# Header fields (in order: general, request, response, entity)
+HCacheControl, HConnection, HDate, HPragma, HTransferEncoding,
+	HUpgrade, HVia,
+	HKeepAlive, # extension
+HAccept, HAcceptCharset, HAcceptEncoding, HAcceptLanguage,
+	HAuthorization, HExpect, HFrom, HHost, HIfModifiedSince,
+	HIfMatch, HIfNoneMatch, HIfRange, HIfUnmodifiedSince,
+	HMaxForwards, HProxyAuthorization, HRange, HReferer,
+	HUserAgent,
+	HCookie, # extension
+HAcceptRanges, HAge, HLocation, HProxyAuthenticate, HPublic,
+	HRetryAfter, HServer, HSetProxy, HVary, HWarning,
+	HWWWAuthenticate,
+	HContentDisposition, HSetCookie, HRefresh, # extensions
+	HWindowTarget, HPICSLabel, # more extensions
+HAllow, HContentBase, HContentEncoding, HContentLanguage,
+	HContentLength, HContentLocation, HContentMD5,
+	HContentRange, HContentType, HETag, HExpires,
+	HLastModified,
+	HXReqTime, HXRespTime, HXUrl, # our extensions, for cached entities
+	NumHfields: con iota;
+
+# (track above enumeration)
+hdrnames := array[] of {
+	"Cache-Control",
+	"Connection",
+	"Date",
+	"Pragma",
+	"Transfer-Encoding",
+	"Upgrade",
+	"Via",
+	"Keep-Alive",
+	"Accept",
+	"Accept-Charset",
+	"Accept-Encoding",
+	"Accept-Language",
+	"Authorization",
+	"Expect",
+	"From",
+	"Host", 
+	"If-Modified-Since",
+	"If-Match",
+	"If-None-Match",
+	"If-Range",
+	"If-Unmodified-Since",
+	"Max-Forwards",
+	"Proxy-Authorization",
+	"Range",
+	"Refererer",
+	"User-Agent",
+	"Cookie",
+	"Accept-Ranges",
+	"Age", 
+	"Location",
+	"Proxy-Authenticate",
+	"Public",
+	"Retry-After",
+	"Server",
+	"Set-Proxy",
+	"Vary",
+	"Warning",
+	"WWW-Authenticate",
+	"Content-Disposition",
+	"Set-Cookie",
+	"Refresh",
+	"Window-Target",
+	"PICS-Label",
+	"Allow", 
+	"Content-Base", 
+	"Content-Encoding",
+	"Content-Language",
+	"Content-Length",
+	"Content-Location",
+	"Content-MD5",
+	"Content-Range",
+	"Content-Type",
+	"ETag",
+	"Expires",
+	"Last-Modified",
+	"X-Req-Time",
+	"X-Resp-Time",
+	"X-Url"
+};
+
+# For fast lookup; track above, and keep sorted and lowercase
+hdrtable := array[] of { T->StringInt
+	("accept", HAccept),
+	("accept-charset", HAcceptCharset),
+	("accept-encoding", HAcceptEncoding),
+	("accept-language", HAcceptLanguage),
+	("accept-ranges", HAcceptRanges),
+	("age", HAge),
+	("allow", HAllow),
+	("authorization", HAuthorization),
+	("cache-control", HCacheControl),
+	("connection", HConnection),
+	("content-base", HContentBase),
+	("content-disposition", HContentDisposition),
+	("content-encoding", HContentEncoding),
+	("content-language", HContentLanguage),
+	("content-length", HContentLength),
+	("content-location", HContentLocation),
+	("content-md5", HContentMD5),
+	("content-range", HContentRange),
+	("content-type", HContentType),
+	("cookie", HCookie),
+	("date", HDate),
+	("etag", HETag),
+	("expect", HExpect),
+	("expires", HExpires),
+	("from", HFrom),
+	("host", HHost),
+	("if-modified-since", HIfModifiedSince),
+	("if-match", HIfMatch),
+	("if-none-match", HIfNoneMatch),
+	("if-range", HIfRange),
+	("if-unmodified-since", HIfUnmodifiedSince),
+	("keep-alive", HKeepAlive),
+	("last-modified", HLastModified),
+	("location", HLocation),
+	("max-forwards", HMaxForwards),
+	("pics-label", HPICSLabel),
+	("pragma", HPragma),
+	("proxy-authenticate", HProxyAuthenticate),
+	("proxy-authorization", HProxyAuthorization),
+	("public", HPublic),
+	("range", HRange),
+	("referer", HReferer),
+	("refresh", HRefresh),
+	("retry-after", HRetryAfter),
+	("server", HServer),
+	("set-cookie", HSetCookie),
+	("set-proxy", HSetProxy),
+	("transfer-encoding", HTransferEncoding),
+	("upgrade", HUpgrade),
+	("user-agent", HUserAgent),
+	("vary", HVary),
+	("via", HVia),
+	("warning", HWarning),
+	("window-target", HWindowTarget),
+	("www-authenticate", HWWWAuthenticate),
+	("x-req-time", HXReqTime),
+	("x-resp-time", HXRespTime),
+	("x-url", HXUrl)
+};
+
+HTTP_Header: adt {
+	startline: string;
+
+	# following four fields only filled in if this is a response header
+	protomajor: int;
+	protominor: int;
+	code: int;
+	reason: string;
+	iossl: int; # true for ssl 
+
+	vals: array of string;
+	cookies: list of string;
+
+	new: fn() : ref HTTP_Header;
+ 	read: fn(h: self ref HTTP_Header, fd: ref sys->FD, sslx: ref SSL3->Context, buf: array of byte) : (string, int, int);
+ 	write: fn(h: self ref HTTP_Header, fd: ref sys->FD, sslx: ref SSL3->Context) : int;
+	usessl: fn(h: self ref HTTP_Header);
+	addval: fn(h: self ref HTTP_Header, key: int, val: string);
+	getval: fn(h: self ref HTTP_Header, key: int) : string;
+};
+
+mediatable: array of T->StringInt;
+
+agent : string;
+dbg := 0;
+warn := 0;
+sptab : con " \t";
+
+init(cu: CharonUtils)
+{
+	CU = cu;
+	sys = load Sys Sys->PATH;
+	S = load String String->PATH;
+	U = load Url Url->PATH; 
+	if (U != nil)
+		U->init();
+	DI = cu->DI;
+	C = cu->C;
+	T = load StringIntTab StringIntTab->PATH;
+#	D = load Date CU->loadpath(Date->PATH);
+#	if (D == nil)
+#		CU->raise(sys->sprint("EXInternal: can't load Date: %r"));
+#	D->init(cu);
+	ctype = C->ctype;
+	# sslhs = nil;	# load on demand
+	ssl3 = nil; # load on demand
+	mediatable = CU->makestrinttab(CU->mnames);
+	agent = (CU->config).agentname;
+	dbg = int (CU->config).dbg['n'];
+	warn = dbg || int (CU->config).dbg['w'];
+}
+
+connect(nc: ref Netconn, bs: ref ByteSource)
+{
+	if(nc.scheme == "https")
+		nc.tstate |= TSSL;
+	if(config.httpminor == 0)
+		nc.tstate |= THTTP_1_0;
+	dialhost := nc.host;
+	dialport := string nc.port;
+	if(nc.scheme != "https" && config.httpproxy != nil && need_proxy(nc.host)) {
+		nc.tstate |= TProxy;
+		dialhost = config.httpproxy.host;
+		if(config.httpproxy.port != "")
+			dialport = config.httpproxy.port;
+	}
+	addr := DI->netmkaddr(dialhost, "net", dialport);
+	err := "";
+	if(dbg)
+		sys->print("http %d: dialing %s\n", nc.id, addr);
+	nc.conn = DI->dial(addr, nil);
+	if(nc.conn == nil) {
+		syserr := sys->sprint("%r");
+		if(S->prefix("cs: dialup", syserr))
+			err = syserr[4:];
+		else if(S->prefix("cs: dns: no translation found", syserr))
+			err = "unknown host";
+		else
+			err = sys->sprint("couldn't connect: %s", syserr);
+	}
+	else {
+		if(dbg)
+			sys->print("http %d: connected\n", nc.id);
+		if(nc.tstate&TSSL) {
+			#if(sslhs == nil) {
+			#	sslhs = load SSLHS SSLHS->PATH;
+			#	if(sslhs == nil)
+			#		err = sys->sprint("can't load SSLHS: %r");
+			#	else
+			#		sslhs->init(2);
+			#}
+			#if(err == "")
+			#	(err, nc.conn) = sslhs->client(nc.conn.dfd, addr);
+			if(nc.tstate&TProxy) # tunelling SSL through proxy
+				err = tunnel_ssl(nc);
+	 		vers := 0;
+ 			if(err == "") {
+				if(ssl3 == nil) {
+	 				m := load SSL3 SSL3->PATH;
+ 					if(m == nil)
+ 						err = "can't load SSL3 module";
+					else if((err = m->init()) == nil)
+						ssl3 = m;
+				}
+				if(config.usessl == CU->NOSSL)
+					err = "ssl is configured off";
+				else if((config.usessl & CU->SSLV23) == CU->SSLV23)
+					vers = 23;
+	 			else if(config.usessl & CU->SSLV2)
+					vers = 2;
+	 			else if(config.usessl & CU->SSLV3)
+					vers = 3;
+			}
+ 			if(err == "") {
+ 				nc.sslx = ssl3->Context.new();
+ 				if(config.devssl)
+ 					nc.sslx.use_devssl();
+ 				info := ref SSL3->Authinfo(ssl_suites, ssl_comprs, nil, 
+ 						0, nil, nil, nil);
+vers = 3;
+ 				(err, nc.vers) =  nc.sslx.client(nc.conn.dfd, addr, vers, info);
+ 			}
+		}
+	}
+	if(err == "") {
+		nc.connected = 1;
+		nc.state = CU->NCgethdr;
+	}
+	else {
+		if(dbg)
+			sys->print("http %d: connection failed: %s\n", nc.id, err);
+		bs.err = err;
+#constate("connect", nc.conn);
+		closeconn(nc);
+	}
+}
+
+constate(msg: string, conn: ref Dial->Connection)
+{
+	fd := conn.dfd;
+	fdfd := -1;
+	if (fd != nil)
+		fdfd = fd.fd;
+	sys->print("connstate(%s, %d) ", msg, fdfd);
+	sfd := sys->open(conn.dir + "/status", Sys->OREAD);
+	if (sfd == nil) {
+		sys->print("cannot open %s/status: %r\n", conn.dir);
+		return;
+	}
+	buf := array [1024] of byte;
+	n := sys->read(sfd, buf, len buf);
+	s := sys->sprint("error: %r");
+	if (n > 0)
+		s = string buf[:n];
+	sys->print("%s status: %s\n", conn.dir, s);
+}
+
+tunnel_ssl(nc: ref Netconn) : string
+{
+	httpvers: string;
+	if(nc.state&THTTP_1_0)
+		httpvers = "1.0";
+	else
+		httpvers = "1.1";
+	req := "CONNECT " + nc.host + ":" + string nc.port + " HTTP/" + httpvers;
+ 	n := sys->fprint(nc.conn.dfd, "%s\r\n\r\n", req);
+	if(n < 0)
+		return sys->sprint("proxy: %r");
+	buf := array [Sys->ATOMICIO] of byte;
+	n = sys->read(nc.conn.dfd, buf, Sys->ATOMICIO);
+	if(n < 0)
+		return sys->sprint("proxy: %r");;
+	resp := string buf[0:n];
+	(m, s) := sys->tokenize(resp, " ");
+
+	if(m < 2)
+		return "proxy: " + resp;
+	if(hd tl s != "200"){
+		(nil, e) := sys->tokenize(resp, "\n\r");
+		return hd e;
+	}
+	return "";
+}
+
+need_proxy(h: string) : int
+{
+	doms := config.noproxydoms;
+	lh := len h;
+	for(; doms != nil; doms = tl doms) {
+		dom := hd doms;
+		ld := len dom;
+		if(lh >= ld && h[lh-ld:] == dom)
+			return 0; # domain is on the no proxy list
+	}
+	return 1;
+}
+
+writereq(nc: ref Netconn, bs: ref ByteSource)
+{
+	#
+	# Prepare the request
+	#
+	req := bs.req;
+	u := ref *req.url;
+	requ, httpvers: string;
+	#if(nc.tstate&TProxy)
+	if((nc.tstate&TProxy) && !(nc.tstate&TSSL)) {
+		u.frag = nil;
+		requ = u.tostring();
+	} else {
+		requ = u.path;
+		if(u.query != "")
+			requ += "?" + u.query;
+	}
+	if(nc.tstate&THTTP_1_0)
+		httpvers = "1.0";
+	else
+		httpvers = "1.1";
+	reqhdr := HTTP_Header.new();
+ 	if(nc.tstate&TSSL)
+ 		reqhdr.usessl();
+	reqhdr.startline = CU->hmeth[req.method] + " " +  requ + " HTTP/" + httpvers;
+	if(u.port != "")
+		reqhdr.addval(HHost, u.host+ ":" + u.port);
+	else
+		reqhdr.addval(HHost, u.host);
+	reqhdr.addval(HUserAgent, agent);
+	reqhdr.addval(HAccept, "*/*; *");
+#	if(cr != nil && (cr.status == CRRevalidate || cr.status == CRMustRevalidate)) {
+#		if(cr.etag != "")
+#			reqhdr.addval(HIfNoneMatch, cr.etag);
+#		else
+#			reqhdr.addval(HIfModifiedSince, D->dateconv(cr.notafter));
+#	}
+	if(req.auth != "")
+		reqhdr.addval(HAuthorization, "Basic " + req.auth);
+	if(req.method == CU->HPost) {
+		reqhdr.addval(HContentLength, string (len req.body));
+		reqhdr.addval(HContentType, "application/x-www-form-urlencoded");
+	}
+        if((CU->config).docookies > 0) {
+                cookies := CU->getcookies(u.host, u.path, nc.tstate&TSSL);
+		if (cookies != nil)
+                       reqhdr.addval(HCookie, cookies);
+        }
+	#
+	# Issue the request
+	#
+	err := "";
+	if(dbg > 1) {
+		sys->print("http %d: writing request:\n", nc.id);
+		reqhdr.write(sys->fildes(1), nil);
+	}
+	rv := reqhdr.write(nc.conn.dfd, nc.sslx);
+	if(rv >= 0 && req.method == CU->HPost) {
+		if(dbg > 1)
+			sys->print("http %d: writing body:\n%s\n", nc.id, string req.body);
+ 		if((nc.tstate&TSSL) && nc.sslx != nil)
+ 			rv = nc.sslx.write(req.body, len req.body);
+ 		else
+ 			rv = sys->write(nc.conn.dfd, req.body, len req.body);
+	}
+	if(rv < 0) {
+		err = sys->sprint("error writing to host: %r");
+#constate("writereq", nc.conn);
+	}
+	if(err != "") {
+		if(dbg)
+			sys->print("http %d: error: %s", nc.id, err);
+		bs.err = err;
+		closeconn(nc);
+	}
+}
+
+
+gethdr(nc: ref Netconn, bs: ref ByteSource)
+{
+	resph := HTTP_Header.new();
+ 	if(nc.tstate&TSSL)
+ 		resph.usessl();
+	hbuf := array[8000] of byte;
+ 	(err, i, j) := resph.read(nc.conn.dfd, nc.sslx, hbuf);
+	if(err != "") {
+#constate("gethdr", nc.conn);
+		if(!(nc.tstate&THTTP_1_0)) {
+			# try switching to http 1.0
+			if(dbg)
+				sys->print("http %d: switching to HTTP/1.0\n", nc.id);
+			nc.tstate |= THTTP_1_0;
+		}
+	}
+	else {
+		if(dbg) {
+			sys->print("http %d: got response header:\n", nc.id);
+			resph.write(sys->fildes(1), nil);
+			sys->print("http %d: %d bytes remaining from read\n", nc.id, j-i);
+		}
+		if(resph.protomajor == 1) {
+			if(!(nc.tstate&THTTP_1_0) && resph.protominor == 0) {
+				nc.tstate |= THTTP_1_0;
+				if(dbg)
+					sys->print("http %d: switching to HTTP/1.0\n", nc.id);
+			}
+		}
+		else if(warn)
+			sys->print("warning: unimplemented major protocol %d.%d\n",
+				resph.protomajor, resph.protominor);
+		if(j > i)
+			nc.tbuf = hbuf[i:j];
+		else
+			nc.tbuf = nil;
+		bs.hdr = hdrconv(resph, bs.req.url);
+		if(bs.hdr.length == 0 && (nc.tstate&THTTP_1_0))
+			closeconn(nc);
+	}
+	if(err != "") {
+		if(dbg)
+			sys->print("http %d: error %s\n", nc.id, err);
+		bs.err = err;
+		closeconn(nc);
+	}
+}
+
+# returns number of bytes transferred to bs.data
+# 0 => EOF
+# -1 => error
+getdata(nc: ref Netconn, bs: ref ByteSource): int
+{
+	if (bs.data == nil || bs.edata >= len bs.data) {
+		if(nc.tstate&THTTP_1_0) {
+			# hmm - when do non-eof'd HTTP1.1 connections close?
+			closeconn(nc);
+		}
+		return 0;
+	}
+	buf := bs.data[bs.edata:];
+	n := len buf;
+	if (nc.tbuf != nil) {
+		# initial overread of header
+		if (n >= len nc.tbuf) {
+			n = len nc.tbuf;
+			buf[:] = nc.tbuf;
+			nc.tbuf = nil;
+			return n;
+		}
+		buf[:] = nc.tbuf[:n];
+		nc.tbuf = nc.tbuf[n:];
+		return n;
+	}
+	if ((nc.tstate&TSSL) && nc.sslx != nil) 
+		n = nc.sslx.read(buf, n);
+	else
+		n = sys->read(nc.conn.dfd, buf, n);
+	if(dbg > 1)
+		sys->print("http %d: read %d bytes\n", nc.id, n);
+	if (n <= 0) {
+#constate("getdata", nc.conn);
+		closeconn(nc);
+		if(n < 0)
+			bs.err = sys->sprint("%r");
+	}
+#else
+#sys->write(sys->fildes(1), buf[:n], n);
+	return n;
+ }
+
+#getdata(nc: ref Netconn, bs: ref ByteSource)
+#{
+#	buf := bs.data;
+#	n := 0;
+#	if(nc.tbuf != nil) {
+#		# initial data from overread of header
+#		# Note: can have more data in nc.tbuf than was
+#		# reported by the HTTP header
+#		n = len nc.tbuf;
+#		if (n > bs.hdr.length) {
+#			n = bs.hdr.length;
+#			nc.tbuf = nc.tbuf[0:n];
+#		}
+#		if(len buf <= n) {
+#			if(warn && len buf < n)
+#				sys->print("more initial data than specified length\n");
+#			bs.data = nc.tbuf;
+#		}
+#		else
+#			buf[0:] = nc.tbuf[:n];
+#		nc.tbuf = nil;
+#	}
+#	if(n == 0) {
+# 		if((nc.tstate&TSSL) && nc.sslx != nil) 
+# 			n = nc.sslx.read(buf[bs.edata:], len buf - bs.edata);
+# 		else
+# 			n = sys->read(nc.conn.dfd, buf[bs.edata:], len buf - bs.edata);
+#	}
+#	if(dbg > 1)
+#		sys->print("http %d: read %d bytes\n", nc.id, n);
+#	if(n <= 0) {
+#		closeconn(nc);
+#		if(n < 0)
+#			bs.err = sys->sprint("%r");
+#	}
+#	else {
+#		bs.edata += n;
+#		if(bs.edata == len buf && bs.hdr.length != 100000000) {
+#			if(nc.tstate&THTTP_1_0) {
+#				closeconn(nc);
+#			}
+#		}
+#	}
+#	if(bs.err != "") {
+#		if(dbg)
+#			sys->print("http %d: error %s\n", nc.id, bs.err);
+#		closeconn(nc);
+#	}
+#}
+
+hdrconv(hh: ref HTTP_Header, u: ref Parsedurl) : ref Header
+{
+	hdr := Header.new();
+	hdr.code = hh.code;
+	hdr.actual = u;
+	s := hh.getval(HContentBase);
+	if(s != "")
+		hdr.base = U->parse(s);
+	else
+		hdr.base = hdr.actual;
+	s = hh.getval(HLocation);
+	if(s != "")
+		hdr.location = U->parse(s);
+	s = hh.getval(HContentLength);
+	if(s != "")
+		hdr.length = int s;
+	else
+		hdr.length = -1;
+	s = hh.getval(HContentType);
+	if(s != "")
+		setmtype(hdr, s);
+	hdr.msg = hh.reason;
+	hdr.refresh = hh.getval(HRefresh);
+	hdr.chal = hh.getval(HWWWAuthenticate);
+	s = hh.getval(HContentEncoding);
+	if(s != "") {
+		if(warn)
+			sys->print("warning: unhandled content encoding: %s\n", s);
+		# force "save as" dialog
+		hdr.mtype = CU->UnknownType;
+	}
+	hdr.warn = hh.getval(HWarning);
+	hdr.lastModified = hh.getval(HLastModified);
+        if((CU->config).docookies > 0) {
+		for (ckl := hh.cookies; ckl != nil; ckl = tl ckl)
+			CU->setcookie(u.host, u.path, hd ckl);
+	}
+	return hdr;
+}
+
+# Set hdr's media type and chset (if a text type).
+# If can't set media type, leave it alone (caller will guess).
+setmtype(hdr: ref CU->Header, s: string)
+{
+	(ty, parms) := S->splitl(S->tolower(s), ";");
+	(fnd, val) := T->lookup(mediatable, trim(ty));
+	if(fnd) {
+		hdr.mtype = val;
+		if(len parms > 0 && val >= CU->TextCss && val <= CU->TextXml) {
+			nvs := Nameval.namevals(parms[1:], ';');
+			s: string;
+			(fnd, s) = Nameval.find(nvs, "charset");
+			if(fnd)
+				hdr.chset = s;
+		}
+	}
+	else {
+		if(warn)
+			sys->print("warning: unknown media type in %s\n", s);
+	}
+}
+
+# Remove leading and trailing whitespace from s.
+trim(s: string) : string
+{
+	is := 0;
+	ie := len s;
+	while(is < ie) {
+		if(ctype[s[is]] != C->W)
+			break;
+		is++;
+	}
+	if(is == ie)
+		return "";
+	while(ie > is) {
+		if(ctype[s[ie-1]] != C->W)
+			break;
+		ie--;
+	}
+	if(is >= ie)
+		return "";
+	if(is == 0 && ie == len s)
+		return s;
+	return s[is:ie];
+}
+
+# If s is in double quotes, remove them
+remquotes(s: string) : string
+{
+	n := len s;
+	if(n >= 2 && s[0] == '"' && s[n-1] == '"')
+		return s[1:n-1];
+	return s;
+}
+
+HTTP_Header.new() : ref HTTP_Header
+{
+	return ref HTTP_Header("", 0, 0, 0, "", 0, array[NumHfields] of { * => "" }, nil);
+}
+
+HTTP_Header.usessl(h: self ref HTTP_Header)
+{
+ 	h.iossl = 1;
+}
+
+HTTP_Header.addval(h: self ref HTTP_Header, key: int, val: string)
+{
+	if (key == HSetCookie) {
+		h.cookies = val :: h.cookies;
+		return;
+	}
+	oldv := h.vals[key];
+	if(oldv != "") {
+		# check that hdr type allows list of things
+		case key {
+		HAccept or HAcceptCharset or HAcceptEncoding
+		or HAcceptLanguage or HAcceptRanges
+		or HCacheControl or HConnection or HContentEncoding
+		or HContentLanguage or HIfMatch or HIfNoneMatch
+		or HPragma or HPublic or HUpgrade or HVia
+		or HWarning or HWWWAuthenticate or HExpect =>
+			val = oldv + ", " + val;
+		HCookie =>
+			val = oldv + "; " + val;
+		* =>
+			if(warn)
+				sys->print("warning: multiple %s headers not allowed\n", hdrnames[key]);
+		}
+	}
+	h.vals[key] = val;
+}
+
+HTTP_Header.getval(h: self ref HTTP_Header, key: int) : string
+{
+	return h.vals[key];
+}
+
+# Read into supplied buf.
+# Returns (ok, start of non-header bytes, end of non-header bytes)
+# If bytes > 127 appear, assume Latin-1
+#
+# Header values added will always be trimmed (see trim() above).
+HTTP_Header.read(h: self ref HTTP_Header, fd: ref sys->FD, sslx: ref SSL3->Context, buf: array of byte) : (string, int, int)
+{
+	i := 0;
+	j := 0;
+	aline : array of byte = nil;
+	eof := 0;
+ 	if(h.iossl && sslx != nil) {
+ 		(aline, eof, i, j) = ssl_getline(sslx, buf, i, j);
+ 	}
+ 	else {
+ 		(aline, eof, i, j) = CU->getline(fd, buf, i, j);
+ 	}
+	if(aline == nil) {
+		return ("header read got immediate eof", 0, 0);
+	}
+	h.startline = latin1tostring(aline);
+	if(dbg > 1)
+		sys->print("header read, startline=%s\n", h.startline);
+	(vers, srest) := S->splitl(h.startline, " ");
+	if(len srest > 0)
+		srest = srest[1:];
+	(scode, reason) := S->splitl(srest, " ");
+	ok := 1;
+	if(len vers >= 8 && vers[0:5] == "HTTP/") {
+		(smaj, vrest) := S->splitl(vers[5:], ".");
+		if(smaj == "" || len vrest <= 1)
+			ok = 0;
+		else {
+			h.protomajor = int smaj;
+			if(h.protomajor < 1)
+				ok = 0;
+			else
+				h.protominor = int vrest[1:];
+		}
+		if(len scode != 3)
+			ok = 0;
+		else {
+			h.code = int scode;
+			if(h.code < 100)
+				ok = 0;
+		}
+		if(len reason > 0)
+			reason = reason[1:];
+		h.reason = reason;
+	}
+	else
+		ok = 0;
+	if(!ok)
+		return (sys->sprint("header read failed to parse start line '%s'\n", string aline), 0, 0);
+	
+	prevkey := -1;
+	while(len aline > 0) {
+ 		if(h.iossl && sslx != nil) {
+ 			(aline, eof, i, j) = ssl_getline(sslx, buf, i, j);
+ 		}
+ 		else {
+ 			(aline, eof, i, j) = CU->getline(fd, buf, i, j);
+ 		}
+		if(eof)
+			return ("header doesn't end with blank line", 0, 0);
+		if(len aline == 0)
+			break;
+		line := latin1tostring(aline);
+		if(dbg > 1)
+			sys->print("%s\n", line);
+		if(ctype[line[0]] == C->W) {
+			if(prevkey < 0) {
+				if(warn)
+					sys->print("warning: header continuation line at beginning: %s\n", line);
+			}
+			else
+				h.vals[prevkey] = h.vals[prevkey] + " " + trim(line);
+		}
+		else {
+			(nam, val) := S->splitl(line, ":");
+			if(val == nil) {
+				if(warn)
+					sys->print("warning: header line has no colon: %s\n", line);
+			}
+			else {
+				(fnd, key) := T->lookup(hdrtable, S->tolower(nam));
+				if(!fnd) {
+					if(warn)
+						sys->print("warning: unknown header field: %s\n", line);
+				}
+				else {
+					h.addval(key, trim(val[1:]));
+					prevkey = key;
+				}
+			}
+		}
+	}
+	return ("", i, j);
+}
+
+# Write in big hunks.  Convert to Latin1.
+# Return last sys->write return value.
+HTTP_Header.write(h: self ref HTTP_Header, fd: ref sys->FD, sslx: ref SSL3->Context) : int
+{
+	# Expect almost all responses will fit in this sized buf
+	buf := array[sys->ATOMICIO] of byte;
+	i := 0;
+	buflen := len buf;
+	need := len h.startline + 2 + 2;
+	if(need > buflen) {
+		buf = CU->realloc(buf, need-buflen);
+		buflen = len buf;
+	}
+	i = copyaslatin1(buf, h.startline, i, 1);
+	for(key := 0; key < NumHfields; key++) {
+		val := h.vals[key];
+		if(val != "") {
+			# 4 extra for this line, 2 for final cr/lf
+			need = len val + len hdrnames[key] + 4 + 2;
+			if(i + need > buflen) {
+				buf = CU->realloc(buf, i+need-buflen);
+				buflen = len buf;
+			}
+			i = copyaslatin1(buf, hdrnames[key], i, 0);
+			buf[i++] = byte ':';
+			buf[i++] = byte ' ';
+			# perhaps should break up really long lines,
+			# but we aren't expecting any
+			i = copyaslatin1(buf, val, i, 1);
+		}
+	}
+	buf[i++] = byte '\r';
+	buf[i++] = byte '\n';
+	n := 0;
+	for(k := 0; k < i; ) {
+ 		if(h.iossl && sslx != nil) {
+ 			n = sslx.write(buf[k:], i-k);
+ 		}
+ 		else {
+ 			n = sys->write(fd, buf[k:], i-k);
+ 		}
+		if(n <= 0)
+			break;
+		k += n;
+	}
+	return n;
+}
+
+# For latin1tostring, so don't have to keep allocating it
+lbuf := array[300] of byte;
+
+# Assume we call this on 'lines', so they won't be too long
+latin1tostring(a: array of byte) : string
+{
+	imax := len lbuf - 1;
+	i := 0;
+	n := len a;
+	for(k := 0; k < n; k++) {
+		b := a[k];
+		if(b < byte 128)
+			lbuf[i++] = b;
+		else
+			i += sys->char2byte(int b, lbuf, i);
+		if(i >= imax) {
+			if(imax > 1000) {
+				if(warn)
+					sys->print("warning: header line too long\n");
+				break;
+			}
+			lbuf = CU->realloc(lbuf, 100);
+			imax = len lbuf - 1;
+		}
+	}
+	ans := string lbuf[0:i];
+	return ans;
+}
+
+# Copy s into a[i:], converting to Latin1.
+# Add cr/lf if addcrlf is true.
+# Assume caller has checked that a has enough room.
+copyaslatin1(a: array of byte, s: string, i: int, addcrlf: int) : int
+{
+	ns := len s;
+	for(k := 0; k < ns; k++) {
+		c := s[k];
+		if(c < 256)
+			a[i++] = byte c;
+		else {
+			if(warn)
+				sys->print("warning: non-latin1 char in header ignored: '%c'\n", c);
+		}
+	}
+	if(addcrlf) {
+		a[i++] = byte '\r';
+		a[i++] = byte '\n';
+	}
+	return i;
+}
+
+defaultport(scheme: string) : int
+{
+	if(scheme == "https")
+		return HTTPSD;
+	return HTTPD;
+}
+
+closeconn(nc: ref Netconn)
+{
+	nc.conn = nil;
+	nc.connected = 0;
+	nc.sslx = nil;
+}
+
+ssl_getline(sslx: ref SSL3->Context, buf: array of byte, bstart, bend: int)
+	:(array of byte, int, int, int)
+{
+ 	ans : array of byte = nil;
+ 	last : array of byte = nil;
+ 	eof := 0;
+mainloop:
+ 	for(;;) {
+ 		for(i := bstart; i < bend; i++) {
+ 			if(buf[i] == byte '\n') {
+ 				k := i;
+ 				if(k > bstart && buf[k-1] == byte '\r')
+ 					k--;
+ 				last = buf[bstart:k];
+ 				bstart = i+1;
+ 				break mainloop;
+ 			}
+ 		}
+ 		if(bend > bstart)
+ 			ans = append(ans, buf[bstart:bend]);
+ 		last = nil;
+ 		bstart = 0;
+ 		bend = sslx.read(buf, len buf);
+ 		if(bend <= 0) {
+ 			eof = 1;
+ 			bend = 0;
+ 			break mainloop;
+ 		}
+ 	}
+ 	return (append(ans, last), eof, bstart, bend);
+}
+ 
+# Append copy of second array to first, return (possibly new)
+# address of the concatenation.
+append(a: array of byte, b: array of byte) : array of byte
+{
+ 	if(b == nil)
+ 		return a;
+ 	na := len a;
+ 	nb := len b;
+ 	ans := realloc(a, nb);
+ 	ans[na:] = b;
+ 	return ans;
+}
+ 
+# Return copy of a, but incr bytes bigger
+realloc(a: array of byte, incr: int) : array of byte
+{
+ 	n := len a;
+ 	newa := array[n + incr] of byte;
+ 	if(a != nil)
+ 		newa[0:] = a;
+ 	return newa;
+}
+
--- /dev/null
+++ b/appl/charon/img.b
@@ -1,0 +1,3607 @@
+implement Img;
+
+include "common.m";
+
+# headers for png support
+include "filter.m";
+include "crc.m";
+
+# big tables in separate files
+include "rgb.inc";
+include "ycbcr.inc";
+
+include "xxx.inc";
+
+# local copies from CU
+sys: Sys;
+CU: CharonUtils;
+	Header, ByteSource, MaskedImage, ImageCache, ResourceState: import CU;
+D: Draw;
+	Chans, Point, Rect, Image, Display: import D;
+E: Events;
+	Event: import E;
+G: Gui;
+
+# channel descriptions
+CRGB:   con 0;  # three channels, R, G, B, no map
+CY:     con 1;  # one channel, luminance
+CRGB1:  con 2;  # one channel, map present
+CYCbCr:  con 3;  # three channels, Y, Cb, Cr, no map
+
+dbg := 0;
+dbgev := 0;
+warn := 0;
+progressive := 0;
+display: ref D->Display;
+
+inflate: Filter;
+crc: Crc;
+CRCstate: import crc;
+
+init(cu: CharonUtils)
+{
+	sys = load Sys Sys->PATH;
+	CU = cu;
+	D = load Draw Draw->PATH;
+	G = cu->G;
+	crc = load Crc Crc->PATH;
+	inflate = load Filter "/dis/lib/inflate.dis";
+	inflate->init();
+	init_tabs();
+}
+
+# Return true if mtype is an image type we can handle
+supported(mtype: int) : int
+{
+	case mtype {
+	CU->ImageJpeg or
+	CU->ImageGif or
+	CU->ImageXXBitmap or
+	CU->ImageXInfernoBit or
+	CU->ImagePng =>
+		return 1;
+	}
+	return 0;
+}
+
+# w,h passed in are specified width and height.
+# Result will be resampled if they don't match the dimensions
+# in the decoded picture (if only one of w,h is specified, the other
+# dimension is scaled by the same factor).
+ImageSource.new(bs: ref ByteSource, w, h: int) : ref ImageSource
+{
+	dbg = int (CU->config).dbg['i'];
+	warn = (int (CU->config).dbg['w']) || dbg;
+	dbgev = int (CU->config).dbg['e'];
+	display = G->display;
+	mtype := CU->UnknownType;
+	if(bs.hdr != nil)
+		mtype = bs.hdr.mtype;
+	is := ref ImageSource(
+		w,h,		# width, height
+		0,0,		# origw, origh
+		mtype,	# mtype
+		0,		# i
+		0,		# curframe
+		bs,		# bs
+		nil,		# ghdr
+		nil,		# jhdr
+		""		# err
+		);
+	return is;
+}
+
+ImageSource.free(is: self ref ImageSource)
+{
+	is.bs = nil;
+	is.gstate = nil;
+	is.jstate = nil;
+}
+
+ImageSource.getmim(is: self ref ImageSource) : (int, ref MaskedImage)
+{
+	if(dbg)
+		sys->print("img: getmim\n");
+	if(dbgev)
+		CU->event("IMAGE_GETMIM", is.width*is.height);
+	ans : ref MaskedImage = nil;
+	ret := Mimnone;
+prtype := 0;
+	{
+		if(is.bs.hdr == nil)
+			return (Mimnone, nil);
+		# temporary hack: wait until whole file is here first
+		if(is.bs.eof) {
+			if(is.mtype == CU->UnknownType) {
+				u := is.bs.req.url;
+				h := is.bs.hdr;
+				h.setmediatype(u.path, is.bs.data);
+				is.mtype = h.mtype;
+			}
+			case is.mtype {
+			CU->ImageJpeg =>
+				ans = getjpegmim(is);
+			CU->ImageGif =>
+				ans = getgifmim(is);
+			CU->ImageXXBitmap =>
+				ans = getxbitmapmim(is);
+			CU->ImageXInfernoBit =>
+				ans = getbitmim(is);
+			CU->ImagePng =>
+				ans = getpngmim(is);
+			* =>
+				is.err = sys->sprint("unsupported image type %s", (CU->mnames)[is.mtype]);
+				ret = Mimerror;
+				ans = nil;
+			}
+			if(ans != nil)
+				ret = Mimdone;
+		}
+		else {
+			# slow down the spin-waiting for this image
+			sys->sleep(100);
+		}
+	}exception ex{
+	"exImageerror*" =>
+		ret = Mimerror;
+		if(dbg)
+			sys->print("getmim got err: %s\n", is.err);
+	}
+	if(dbg)
+		sys->print("img: getmim returns (%d,%x)\n", ret, ans);
+	if(dbgev)
+		CU->event("IMAGE_GETMIM_END", 0);
+	is.bs.lim = is.i;
+	return (ret, ans);
+}
+
+# Raise exImagerror exception
+imgerror(is: ref ImageSource, msg: string)
+{
+	is.err = msg;
+	if(dbg)
+		sys->print("Image error: %s\n", msg);
+	raise "exImageerror:";
+}
+
+# Get next char or raise exception if cannot
+getc(is: ref ImageSource) : int
+{
+	if(is.i >= len is.bs.data) {
+		imgerror(is, "premature eof");
+	}
+	return int is.bs.data[is.i++];
+}
+
+# Unget the last character.
+# When called before any other getting routines, we
+# know the buffer still has that character in it.
+ungetc(is: ref ImageSource)
+{
+	if(is.i == 0)
+		raise "EXInternal: ungetc past beginning of buffer";
+	is.i--;
+}
+
+# Like ungetc, but ungets two bytes (gotten in order b1, another char).
+# Now the bytes could have spanned a boundary, if we were unlucky,
+# so we have to be prepared to put b1 in front of current buffer.
+ungetc2(is: ref ImageSource, nil: byte)
+{
+	if(is.i < 2) {
+		if(is.i != 1)
+			raise "EXInternal: ungetc2 past beginning of buffer";
+		is.i = 0;
+	}
+	else
+		is.i -= 2;
+}
+
+# Get 2 bytes and return the 16-bit value, little-endian order.
+getlew(is: ref ImageSource) : int
+{
+	c0 := getc(is);
+	c1 := getc(is);
+	return c0 + (c1<<8);
+}
+
+# Get 2 bytes and return the 16-bit value, big-endian order.
+getbew(is: ref ImageSource) : int
+{
+	c0 := getc(is);
+	c1 := getc(is);
+	return (c0<<8) + c1;
+}
+
+# Copy next n bytes of input into buf
+# or raise exception if cannot.
+read(is: ref ImageSource, buf: array of byte, n: int)
+{
+	ok := 0;
+	if(is.i +n < len is.bs.data) {
+		buf[0:] = is.bs.data[is.i:is.i+n];
+		is.i += n;
+	}
+	else
+		imgerror(is, "premature eof");
+}
+
+# Caller needs n bytes.
+# Return an (array, index into array) where at least
+# the next n bytes can be found.
+# There might be a "premature eof" exception.
+getn(is: ref ImageSource, n: int) : (array of byte, int)
+{
+	a := is.bs.data;
+	i := is.i;
+	if(i + n <= len a)
+		is.i += n;
+	else
+		imgerror(is, "premature eof");
+	return (a, i);
+}
+
+# display.newimage with some defaults; throw exception if fails
+newimage(is: ref ImageSource, w, h: int) : ref Image
+{
+	if(!(CU->imcache).need(w*h))
+		imgerror(is, "out of memory");
+	im := display.newimage(((0,0),(w,h)), D->CMAP8, 0, D->White);
+	if(im == nil)
+		imgerror(is, "out of memory");
+	return im;
+}
+
+newimage24(is: ref ImageSource, w, h: int) : ref Image
+{
+	if(!(CU->imcache).need(w*h*3))
+		imgerror(is, "out of memory");
+	im := display.newimage(((0,0),(w,h)), D->RGB24, 0, D->White);
+	if(im == nil)
+		imgerror(is, "out of memory");
+	return im;
+}
+
+newimagegrey(is: ref ImageSource, w, h: int) : ref Image
+{
+	if(!(CU->imcache).need(w*h))
+		imgerror(is, "out of memory");
+	im := display.newimage(((0,0),(w,h)), D->GREY8, 0, D->White);
+	if(im == nil)
+		imgerror(is, "out of memory");
+	return im;
+}
+
+
+newmi(im: ref Image) : ref MaskedImage
+{
+	return ref MaskedImage(im, nil, 0, 0, -1, Point(0,0));
+}
+
+# Call this after origw and origh are set to set the width and height
+# to our desired (rescaled) answer dimensions.
+# If only one of the dimensions is specified, the other is scaled by
+# the same factor.
+setdims(is: ref ImageSource)
+{
+	sw := is.origw;
+	sh := is.origh;
+	dw := is.width;
+	dh := is.height;
+	if(dw == 0 && dh == 0) {
+		dw = sw;
+		dh = sh;
+	}
+	else if(dw == 0 || dh == 0) {
+		if(dw == 0) {
+			dw = int ((real sw) * (real dh/real sh));
+			if(dw == 0)
+				dw = 1;
+		}
+		else {
+			dh = int ((real sh) * (real dw/real sw));
+			if(dh == 0)
+				dh = 1;
+		}
+	}
+	is.width = dw;
+	is.height = dh;
+}
+
+# for debugging
+printarray(a: array of int, name: string)
+{
+	sys->print("%s:", name);
+	for(i := 0; i < len a; i++) {
+		if((i%10)==0)
+			sys->print("\n%5d: ", i);
+		sys->print("%6d", a[i]);
+	}
+	sys->print("\n");
+}
+
+################# XBitmap ###################
+
+getxbitmaphdr(is: ref ImageSource)
+{
+	fnd: int;
+	(fnd, is.origw) = getxbitmapdefine(is);
+	if(fnd)
+		(fnd, is.origh) = getxbitmapdefine(is);
+	if(!fnd)
+		imgerror(is, "xbitmap starts badly");
+	# now, optional x_hot, y_hot
+	(fnd, nil) = getxbitmapdefine(is);
+	if(fnd)
+		(fnd, nil) = getxbitmapdefine(is);
+	# now expect 'static char x...x_bits[] = {'
+	get_to_char(is, '{');
+}
+
+getxbitmapmim(is: ref ImageSource) : ref MaskedImage
+{
+	getxbitmaphdr(is);
+	setdims(is);
+	bytesperline := (is.origw+7) / 8;
+	pixels := array[is.origw*is.origh] of byte;
+	pixi := 0;
+	for(i := 0; i < is.origh; i++) {
+		for(j := 0; j < bytesperline; j++) {
+			v := get_hexbyte(is);
+			kend := 7;
+			if(j == bytesperline-1)
+				kend = (is.origw-1)%8;
+			for(k := 0; k <= kend; k++) {
+				if(v & (1<<k))
+					pixels[pixi] = byte D->Black;
+				else
+					pixels[pixi] = byte D->White;
+				pixi++;
+			}
+		}
+	}
+	if(is.width != is.origw || is.height != is.origh)
+		pixels = resample(pixels, is.origw, is.origh, is.width, is.height);
+	im := newimage(is, is.width, is.height);
+	im.writepixels(im.r, pixels);
+	return newmi(im);
+}
+
+# get a line, which should be of form
+#	'#define fieldname val'
+# and return (found, integer rep of val)
+getxbitmapdefine(is: ref ImageSource) : (int, int)
+{
+	fnd := 0;
+	n := 0;
+	c := getc(is);
+	if(c == '#') {
+		get_to_char(is, ' ');
+		get_to_char(is, ' ');
+		c = getc(is);
+		while(c >= '0' && c <= '9') {
+			fnd = 1;
+			n = n*10 + c - '0';
+			c = getc(is);
+		}
+	}
+	else
+		ungetc(is);
+	get_to_char(is, '\n');
+	return (fnd, n);
+}
+
+# read fd until get char cterm
+# (raise exception if eof hit first)
+get_to_char(is: ref ImageSource, cterm: int)
+{
+	for(;;) {
+		if(getc(is) == cterm)
+			return;
+	}
+}
+
+# read fd until get xDD, were DD are hex digits.
+# (raise exception if not hex digits or if eof hit first)
+get_hexbyte(is: ref ImageSource) : int
+{
+	get_to_char(is, 'x');
+	n1 := hexdig(getc(is));
+	n2 := hexdig(getc(is));
+	if(n1 < 0 || n2 < 0)
+		imgerror(is, "X Bitmap expected hex digits");
+	return (n1<<4) + n2;
+}
+
+hexdig(c: int) : int
+{
+	if('0' <= c && c <= '9')
+		c -= '0';
+	else if('a' <= c && c <= 'f')
+		c += 10 - 'a';
+	else if('A' <= c && c <= 'F')
+		c += 10 - 'A';
+	else
+		c = -1;
+	return c;
+}
+
+################# GIF ###################
+
+# GIF flags
+TRANSP:		con 1;
+INPUT:		con 2;
+DISPMASK:	con 7<<2;
+HASCMAP:	con 16r80;
+INTERLACED:	con 16r40;
+
+Entry: adt
+{
+	prefix: int;
+	exten: int;
+};
+
+getgifhdr(is: ref ImageSource)
+{
+	if(dbg)
+		sys->print("img: getgifhdr\n");
+	h := ref Gifstate;
+	(buf, i) := getn(is, 6);
+	vers := string buf[i:i+6];
+	if(vers!="GIF87a" && vers!="GIF89a")
+		imgerror(is, "unknown GIF version " + vers);
+	is.origw = getlew(is);
+	is.origh = getlew(is);
+	h.fields = getc(is);
+	h.bgrnd = getc(is);
+	h.aspect = getc(is);
+	setdims(is);
+	if(dbg)
+		sys->print("img: getgifhdr has vers=%s, origw=%d, origh=%d, w=%d, h=%d, fields=16r%x, bgrnd=%d, aspect=%d\n",
+			vers, is.origw, is.origh, is.width, is.height, h.fields, h.bgrnd, h.aspect);
+	h.flags = 0;
+	h.delay = 0;
+	h.trindex = byte 0;
+	h.tbl = array[4096] of GifEntry;
+	for(i = 0; i < 258; i++) {
+		h.tbl[i].prefix = -1;
+		h.tbl[i].exten = i;
+	}
+	h.globalcmap = nil;
+	h.cmap = nil;
+	if(h.fields & HASCMAP)
+		h.globalcmap = gifreadcmap(is, (h.fields&7)+1);
+	is.gstate = h;
+	if(warn && h.aspect != 0)
+		sys->print("warning: non-standard aspect ratio in GIF image ignored\n");
+	if(!gifgettoimage(is))
+		imgerror(is, "GIF file has no image");
+}
+
+gifgettoimage(is: ref ImageSource) : int
+{
+	if(dbg)
+		sys->print("img: gifgettoimage\n");
+	h := is.gstate;
+loop:
+	for(;;) {
+		# some GIFs omit Trailer
+		if(is.i >= len is.bs.data)
+			break;
+		case c := getc(is) {
+		16r2C =>	# Image Descriptor
+			return 1;
+
+		16r21 =>	# Extension
+			hsize := 0;
+			hasdata := 0;
+		
+			case getc(is){
+			16r01 =>	# Plain Text Extension
+				hsize = 14;
+				hasdata = 1;
+				if(dbg)
+					sys->print("gifgettoimage: text extension\n");
+			16rF9 =>	# Graphic Control Extension
+				getc(is);	# blocksize (should be 4)
+				h.flags = getc(is);
+				h.delay = getlew(is);
+				h.trindex = byte getc(is);
+				getc(is);	# block terminator (should be 0)
+				# set minimum delay
+				if (h.delay < 20)
+					h.delay = 20;
+				if(dbg)
+					sys->print("gifgettoimage: graphic control flags=16r%x, delay=%d, trindex=%d\n",
+						h.flags, h.delay, int h.trindex);
+			16rFE =>	# Comment Extension
+				if(dbg)
+					sys->print("gifgettoimage: comment extension\n");
+				hasdata = 1;
+			16rFF =>	# Application Extension
+				if(dbg)
+					sys->print("gifgettoimage: application extension\n");
+				hsize = getc(is);
+				# standard says this must be 11, but Adobe likes to put out 10-byte ones,
+				# so we pay attention to the field.
+				hasdata = 1;
+			* =>
+				imgerror(is, "GIF unknown extension");
+			}
+			if(hsize > 0)
+				getn(is, hsize);
+			if(hasdata) {
+				for(;;) {
+					if((nbytes := getc(is)) == 0)
+						break;
+					(a, i) := getn(is, nbytes);
+					if(dbg)
+						sys->print("extension data: '%s'\n", string a[i:i+nbytes]);
+				}
+			}
+
+		16r3B =>	# Trailer
+			# read to end of data
+			getn(is, len is.bs.data - is.i);
+			break loop;
+
+		* =>
+			if(c == 0)
+				continue;		# FIX for some buggy gifs
+			imgerror(is, "GIF unknown block type " + string c);
+		}
+	}
+	return 0;
+}
+
+getgifmim(is: ref ImageSource) : ref MaskedImage
+{
+	if(is.gstate == nil)
+		getgifhdr(is);
+
+	# At this point, should just have read Image Descriptor marker byte
+	h := is.gstate;
+	left :=getlew(is);
+	top := getlew(is);
+	width := getlew(is);
+	height := getlew(is);
+	h.fields = getc(is);
+	totpix := width*height;
+	h.cmap = h.globalcmap;
+	if(dbg)
+		sys->print("getgifmim, left=%d, top=%d, width=%d, height=%d, fields=16r%x\n",
+			left, top, width, height, h.fields);
+	if(dbgev)
+		CU->event("IMAGE_GETGIFMIM", 0);
+	if(h.fields & HASCMAP)
+		h.cmap = gifreadcmap(is, (h.fields&7)+1);
+	if(h.cmap == nil)
+		imgerror(is, "GIF needs colormap");
+
+	# now decode the image
+	c, incode: int;
+
+	codesize := getc(is);
+	if(codesize > 8)
+		imgerror(is, "GIF bad codesize");
+	if(len h.cmap!=3*(1<<codesize) 
+	  && len h.cmap != 3*(1<<(codesize-1))	# peculiar GIF bitmap files
+	  && (codesize!=2 || len h.cmap!=3*2)){ # peculiar GIF bitmap files II
+		if (warn)
+			sys->print("warning: GIF codesize = %d doesn't match cmap len = %d\n", codesize, len h.cmap);
+		#imgerror(is, "GIF codesize doesn't match color map");
+	}
+
+	CTM :=1<<codesize;
+	EOD := CTM+1;
+
+	pic := array[totpix] of byte;
+	pici := 0;
+	data : array of byte = nil;
+	datai := 0;
+	dataend := 0;
+
+	nbits := 0;
+	sreg := 0;
+	stack := array[4096] of byte;
+	stacki: int;
+	fc := 0;
+	tbl := h.tbl;
+
+Decode:
+	for(;;) {
+		csize := codesize+1;
+		csmask := ((1<<csize) - 1);
+		nentry := EOD+1;
+		maxentry := csmask;
+		first := 1;
+		ocode := -1;
+
+		for(;; ocode = incode) {
+			while(nbits < csize) {
+				if(datai == dataend) {
+					nbytes := getc(is);
+					if (nbytes == 0)
+						# Block Terminator
+						break Decode;
+					(data, datai) = getn(is, nbytes);
+					dataend = datai+nbytes;
+				}
+				c = int data[datai++];
+				sreg |= c<<nbits;
+				nbits += 8;
+			}
+			code := sreg & csmask;
+			sreg >>= csize;
+			nbits -= csize;
+
+			if(code == EOD) {
+				nbytes := getc(is);
+				if(nbytes != 0 && warn)
+					sys->print("warning: unexpected data past EOD\n");
+				break Decode;
+			}
+
+			if(code == CTM)
+				continue Decode;
+
+			stacki = len stack-1;
+
+			incode = code;
+
+			# special case for KwKwK 
+			if(code == nentry) {
+				stack[stacki--] = byte fc;
+				code = ocode;
+			}
+
+			if(code > nentry)
+				imgerror(is, "GIF bad code");
+		
+			for(c=code; c>=0; c=tbl[c].prefix)
+				stack[stacki--] = byte tbl[c].exten;
+
+			nb := len stack-(stacki+1);
+			if(pici+nb > len pic) {
+				# this common error is harmless
+				# we have to keep reading to keep the blocks in sync
+				;
+			}
+			else {
+				pic[pici:] = stack[stacki+1:];
+				pici += nb;
+			}
+
+			fc = int stack[stacki+1];
+
+			if(first) {
+				first = 0;
+				continue;
+			}
+			early:=0; # peculiar tiff feature here for reference
+			if(nentry == maxentry-early) {
+				if(csize >= 12)
+					continue;
+				csize++;
+				maxentry = (1<<csize);
+				csmask = maxentry - 1;
+				if(csize < 12)
+					maxentry--;
+			}
+			tbl[nentry].prefix = ocode;
+			tbl[nentry].exten = fc;
+			nentry++;
+		}
+	}
+	while(pici < len pic) {
+		# shouldn't happen, but sometimes get buggy gifs
+		pic[pici++] = byte 0;
+	}
+
+	if(h.fields & INTERLACED) {
+		if(dbg)
+			sys->print("getgifmim uninterlacing\n");
+		if(dbgev)
+			CU->event("IMAGE_GETGIFMIM_INTERLACE_START", 0);
+		# (TODO: Could un-interlace in place.
+		# Decompose permutation into cycles,
+		# then need one double-copy of a line
+		# per cycle).
+		ipic := array[totpix] of byte;
+		# Group 1: every 8th row, starting with row 0
+		pici = 0;
+		ipici := 0;
+		ipiclim := totpix-width;
+		w2 := width+width;
+		w4 := w2+w2;
+		w8 := w4+w4;
+		startandby := array[4] of {(0,w8), (w4,w8), (w2,w4), (width,w2)};
+		for(k := 0; k < 4; k++) {
+			(start, by) := startandby[k];
+			for(ipici=start; ipici <= ipiclim; ipici += by) {
+				ipic[ipici:] = pic[pici:pici+width];
+				pici += width;
+			}
+		}
+		pic = ipic;
+		if(dbgev)
+			CU->event("IMAGE_GETGIFMIM_INTERLACE_END", 0);
+	}
+	if(is.width != is.origw || is.height != is.origh) {
+		if (is.width < 0)
+			is.width = 0;
+		if (is.height < 0)
+			is.height = 0;
+		# need to resample, using same factors as original image
+		wscale := real is.width / real is.origw;
+		hscale := real is.height / real is.origh;
+		owidth := width;
+		oheight := height;
+		width = int (wscale * real width);
+		if(width == 0)
+			width = 1;
+		height = int (hscale * real height);
+		if(height == 0)
+			height = 1;
+		left = int (wscale * real left);
+		top = int (hscale * real top);
+		pic = resample(pic, owidth, oheight, width, height);
+	}
+	mask : ref Image;
+	if(h.flags & TRANSP) {
+		if(dbg)
+			sys->print("getgifmim making mask, trindex=%d\n", int h.trindex);
+		if(dbgev)
+			CU->event("IMAGE_GETGIFMIM_MASK_START", 0);
+		# make a 1-bit deep bitmap for mask
+		# expect most mask bits will be 1
+		bytesperrow := (width+7)/8;
+		trpix := h.trindex;
+		mpic := array[bytesperrow*height] of byte;
+		mpici := 0;
+		pici = 0;
+		for(y := 0; y < height; y++) {
+			v := byte 16rFF;
+			k := 0;
+			for(x := 0; x < width; x++) {
+				if(pic[pici++] == trpix)
+					v &= ~(byte 16r80>>k);
+				if(++k == 8) {
+					k = 0;
+					mpic[mpici++] = v;
+					v = byte 16rFF;
+				}
+			}
+			if(k != 0)
+				mpic[mpici++] = v;
+		}
+		if(!(CU->imcache).need(bytesperrow*height))
+			imgerror(is, "out of memory");
+		mask = display.newimage(((0,0),(width,height)), D->GREY1, 0, D->Opaque);
+		if(mask == nil)
+			imgerror(is, "out of memory");
+		mask.writepixels(mask.r, mpic);
+		mpic = nil;
+		if(dbgev)
+			CU->event("IMAGE_GETGIFMIM_MASK_END", 0);
+	}
+	if(dbgev)
+		CU->event("IMAGE_GETGIFMIM_REMAP_START", 0);
+	pic24 := remap24(pic, h.cmap);
+#	remap1(pic, width, height, h.cmap);
+	if(dbgev)
+		CU->event("IMAGE_GETGIFMIM_REMAP_END", 0);
+	bgcolor := -1;
+	i := h.bgrnd;
+	if(i >= 0 && 3*i+2 < len h.cmap) {
+		bgcolor = ((int h.cmap[3*i])<<16)
+			| ((int h.cmap[3*i+1])<<8)
+			| (int h.cmap[3*i+2]);
+	}
+	im := newimage24(is, width, height);
+	im.writepixels(im.r, pic24);
+	if(is.curframe == 0) {
+		# make sure first frame fills up whole rectangle
+		if(is.width != width || is.height != height || left != 0 || top != 0) {
+			r := Rect((left,top),(left+width,top+height));
+			pix := D->White;
+			if(bgcolor != -1)
+				pix = (bgcolor<<8) | 16rFF;
+			newim := display.newimage(((0,0),(is.width,is.height)), D->RGB24, 0, pix);
+			if(newim == nil)
+				imgerror(is, "out of memory");
+			newim.draw(r, im, mask, (0,0));
+			im = newim;
+			if(mask != nil) {
+				newmask := display.newimage(((0,0),(is.width,is.height)), D->GREY1, 0, D->Opaque);
+				if(newmask == nil)
+					imgerror(is, "out of memory");
+				newmask.draw(r, mask, nil, (0,0));
+				mask = newmask;
+			}
+			left = 0;
+			top = 0;
+		}
+	}
+	pic = nil;
+	mi := newmi(im);
+	mi.mask = mask;
+	mi.delay = h.delay*10;	# convert centiseconds to milliseconds
+	mi.origin = Point(left, top);
+	dispmeth := (h.flags>>2)&7;
+	if(dispmeth == 2) {
+		# reset to background color after displaying this frame
+		mi.bgcolor = bgcolor;
+	}
+	else if(dispmeth == 3) {
+		# Supposed to "reset to previous", which appears to
+		# mean the previous frame that didn't have a "reset to previous".
+		# Signal this special case to layout by setting bgcolor to -2.
+		mi.bgcolor = -2;
+	}
+	if(gifgettoimage(is)) {
+		mi.more = 1;
+		is.curframe++;
+		# have to reinitialize table for next time
+		for(i = 0; i < 258; i++) {
+			h.tbl[i].prefix = -1;
+			h.tbl[i].exten = i;
+		}
+	}
+	if(dbgev)
+		CU->event("IMAGE_GETGIFMIM_END", 0);
+	return mi;	
+}
+
+# Read a GIF colormap, where bpe is number of bits in an entry.
+# Raises a 'premature eof' exception if can't get the whole map.
+gifreadcmap(is: ref ImageSource, bpe: int) : array of byte
+{
+	size := 3*(1<<bpe);
+	map := array[size] of byte;
+	if(dbg > 1)
+		sys->print("gifreadcmap wants %d bytes\n", size);
+	read(is, map, size);
+	return map;
+}
+
+################# JPG ###################
+
+# Constants, all preceded by byte 16rFF
+SOF:	con 16rC0;	# Start of Frame
+SOF2:	con 16rC2;	# Start of Frame; progressive Huffman
+JPG:	con 16rC8;	# Reserved for JPEG extensions
+DHT:	con 16rC4;	# Define Huffman Tables
+DAC:	con 16rCC;	# Arithmetic coding conditioning
+RST:	con 16rD0;	# Restart interval termination
+RST7:	con 16rD7;	# Restart interval termination (highest value)
+SOI:	con 16rD8;	# Start of Image
+EOI:	con 16rD9;	# End of Image
+SOS:	con 16rDA;	# Start of Scan
+DQT:	con 16rDB;	# Define quantization tables
+DNL:	con 16rDC;	# Define number of lines
+DRI:	con 16rDD;	# Define restart interval
+DHP:	con 16rDE;	# Define hierarchical progression
+EXP:	con 16rDF;	# Expand reference components
+APPn:	con 16rE0;	# Reserved for application segments
+JPGn:	con 16rF0;	# Reserved for JPEG extensions
+COM:	con 16rFE;	# Comment
+
+NBUF:	con 16*1024;
+
+
+jpegcolorspace: con CYCbCr;
+
+zerobytes := array[64] of { * => byte 0 };
+zeroints := array[64] of { * => 0 };
+
+getjpeghdr(is: ref ImageSource)
+{
+	if(dbg)
+		sys->print("getjpeghdr\n");
+	h := ref Jpegstate(
+		0, 0,		# sr, cnt
+		0,		# Nf
+		nil,		# comp
+		byte 0,	# mode,
+		0, 0,		# X, Y
+		nil,		# qt
+		nil, nil,	# dcht, acht
+		0,		# Ns
+		nil,		# scomp
+		0, 0,		# Ss, Se
+		0, 0,		# Ah, Al
+		0, 0,		# ri, nseg
+		nil,		# nblock
+		nil, nil,	# dccoeff, accoeff
+		0, 0, 0, 0	# nacross, ndown, Hmax, Vmax
+		);
+	is.jstate = h;
+	if(jpegmarker(is) != SOI)
+		imgerror(is, "Jpeg expected SOI marker");
+	(m, n) := jpegtabmisc(is);
+	if(!(m == SOF || m == SOF2))
+		imgerror(is, "Jpeg expected Frame marker");
+	nil = getc(is);		# sample precision
+	h.Y = getbew(is);
+	h.X = getbew(is);
+	h.Nf = getc(is);
+	if(dbg)
+		sys->print("start of frame, Y=%d, X=%d, Nf=%d\n", h.Y, h.X, h.Nf);
+	h.comp = array[h.Nf] of Framecomp;
+	h.nblock = array[h.Nf] of int;
+	for(i:=0; i<h.Nf; i++) {
+		h.comp[i].C = getc(is);
+		(H, V) := nibbles(getc(is));
+		h.comp[i].H = H;
+		h.comp[i].V = V;
+		h.comp[i].Tq = getc(is);
+		h.nblock[i] =H*V;
+		if(dbg)
+			sys->print("comp[%d]: C=%d, H=%d, V=%d, Tq=%d\n",
+				i, h.comp[i].C, H, V, h.comp[i].Tq);
+	}
+	h.mode = byte m;
+	is.origw = h.X;
+	is.origh = h.Y;
+	setdims(is);
+	if(n != 6+3*h.Nf)
+		imgerror(is, "Jpeg bad SOF length");
+}
+
+jpegmarker(is: ref ImageSource) : int
+{
+	if(getc(is) != 16rFF)
+		imgerror(is, "Jpeg expected marker");
+	return getc(is);
+}
+
+# Consume tables and miscellaneous marker segments,
+# returning the marker id and length of the first non-such-segment
+# (after having consumed the marker).
+# May raise "premature eof" or other exception.
+jpegtabmisc(is: ref ImageSource) : (int, int)
+{
+	h := is.jstate;
+	m, n : int;
+Loop:
+	for(;;) {
+		h.nseg++;
+		m = jpegmarker(is);
+		n = 0;
+		if(m != EOI)
+			n = getbew(is) - 2;
+		if(dbg > 1)
+			sys->print("jpegtabmisc reading segment, got m=%x, n=%d\n", m, n);
+		case m {
+		SOF or SOF2 or SOS or EOI =>
+			break Loop;
+
+		APPn+0 =>
+			if(h.nseg==1 && n >= 6) {
+				(buf, i) := getn(is, 6);
+				n -= 6;
+				if(string buf[i:i+4]=="JFIF") {
+					vers0 := int buf[i+5];
+					vers1 := int buf[i+6];
+					if(vers0>1 || vers1>2)
+						imgerror(is, "Jpeg unimplemented version");
+				}
+			}
+
+		APPn+1 to APPn+15 =>
+			;
+
+		DQT =>
+			jpegquanttables(is, n);
+			n = 0;
+
+		DHT =>
+			jpeghuffmantables(is, n);
+			n = 0;
+
+		DRI =>
+			h.ri =getbew(is);
+			n -= 2;
+
+		COM =>
+			;
+
+		* =>
+			imgerror(is, "Jpeg unexpected marker");
+		}
+		if(n > 0)
+			getn(is, n);
+	}
+	return (m, n);
+}
+
+# Consume huffman tables, raising exception on error.
+jpeghuffmantables(is: ref ImageSource, n: int)
+{
+	if(dbg)
+		sys->print("jpeghuffmantables\n");
+	h := is.jstate;
+	if(h.dcht == nil) {
+		h.dcht = array[4] of ref Huffman;
+		h.acht = array[4] of ref Huffman;
+	}
+	for(l:= 0; l < n; )
+		l += jpeghuffmantable(is);
+	if(l != n)
+		imgerror(is, "Jpeg huffman table bad length");
+}
+
+jpeghuffmantable(is: ref ImageSource) : int
+{
+	t := ref Huffman;
+	h := is.jstate;
+	(Tc, th) := nibbles(getc(is));
+	if(dbg > 1)
+		sys->print("jpeghuffmantable, Tc=%d, th=%d\n", Tc, th);
+	if(Tc > 1)
+		imgerror(is, "Jpeg unknown Huffman table class");
+	if(th>3 || (h.mode==byte SOF && th>1))
+		imgerror(is, "Jpeg unknown Huffman table index");
+	if(Tc == 0)
+		h.dcht[th] = t;
+	else
+		h.acht[th] = t;
+
+	# flow chart C-2
+	(b, bi) := getn(is, 16);
+	numcodes := array[16] of int;
+	nsize := 0;
+	for(i:=0; i<16; i++)
+		nsize += (numcodes[i] = int b[bi+i]);
+	t.size = array[nsize+1] of int;
+	k := 0;
+	for(i=1; i<=16; i++) {
+		n :=numcodes[i-1];
+		for(j:=0; j<n; j++)
+			t.size[k++] = i;
+	}
+	t.size[k] = 0;
+
+	# initialize HUFFVAL
+	t.val = array[nsize] of int;
+	(b, bi) = getn(is, nsize);
+	for(i=0; i<nsize; i++)
+		t.val[i] = int b[bi++];
+
+	# flow chart C-3
+	t.code = array[nsize+1] of int;
+	k = 0;
+	code := 0;
+	si := t.size[0];
+	for(;;) {
+		do
+			t.code[k++] = code++;
+		while(t.size[k] == si);
+		if(t.size[k] == 0)
+			break;
+		do {
+			code <<= 1;
+			si++;
+		} while(t.size[k] != si);
+	}
+
+	# flow chart F-25
+	t.mincode = array[17] of int;
+	t.maxcode = array[17] of int;
+	t.valptr = array[17] of int;
+	i = 0;
+	j := 0;
+    F25:
+	for(;;) {
+		for(;;) {
+			i++;
+			if(i > 16)
+				break F25;
+			if(numcodes[i-1] != 0)
+				break;
+			t.maxcode[i] = -1;
+		}
+		t.valptr[i] = j;
+		t.mincode[i] = t.code[j];
+		j += int numcodes[i-1]-1;
+		t.maxcode[i] = t.code[j];
+		j++;
+	}
+
+	# create byte-indexed fast path tables
+	t.value = array[256] of int;
+	t.shift = array[256] of int;
+	maxcode := t.maxcode;
+	# stupid startup algorithm: just run machine for each byte value
+  Bytes:
+	for(v:=0; v<256; v++){
+		cnt := 7;
+		m := 1<<7;
+		code = 0;
+		sr := v;
+		i = 1;
+		for(;;i++){
+			if(sr & m)
+				code |= 1;
+			if(code <= maxcode[i])
+				break;
+			code <<= 1;
+			m >>= 1;
+			if(m == 0){
+				t.shift[v] = 0;
+				t.value[v] = -1;
+				continue Bytes;
+			}
+			cnt--;
+		}
+		t.shift[v] = 8-cnt;
+		t.value[v] = t.val[t.valptr[i]+(code-t.mincode[i])];
+	}
+	if(dbg > 2) {
+		sys->print("Huffman table %d:\n", th);
+		printarray(t.size, "size");
+		printarray(t.code, "code");
+		printarray(t.val, "val");
+		printarray(t.mincode, "mincode");
+		printarray(t.maxcode, "maxcode");
+		printarray(t.value, "value");
+		printarray(t.shift, "shift");
+	}
+
+	return nsize+17;
+}
+
+jpegquanttables(is: ref ImageSource, n: int)
+{
+	if(dbg)
+		sys->print("jpegquanttables\n");
+	h := is.jstate;
+	if(h.qt == nil)
+		h.qt = array[4] of array of int;
+	for(l:=0; l<n; )
+		l += jpegquanttable(is);
+	if(l != n)
+		imgerror(is, "Jpeg quant table bad length");
+}
+
+jpegquanttable(is: ref ImageSource): int
+{
+	(pq, tq) := nibbles(getc(is));
+	if(dbg)
+		sys->print("jpegquanttable pq=%d tq=%d\n", pq, tq);
+	if(pq > 1)
+		imgerror(is, "Jpeg unknown quantization table class");
+	if(tq > 3)
+		imgerror(is, "Jpeg bad quantization table index");
+	q := array[64] of int;
+	is.jstate.qt[tq] = q;
+	for(i:=0; i<64; i++) {
+		if(pq == 0)
+			q[i] =getc(is);
+		else
+			q[i] = getbew(is);
+	}
+	if(dbg > 2)
+		printarray(q, "quant table");
+	return 1+(64*(1+pq));;
+}
+
+# Have just read Frame header.
+# Now expect:
+#	((tabl/misc segment(s))* (scan header) (entropy coded segment)+)+ EOI
+getjpegmim(is: ref ImageSource) : ref MaskedImage
+{
+	if(dbg)
+		sys->print("getjpegmim\n");
+	if(dbgev)
+		CU->event("IMAGE_GETJPGMIM", is.width*is.height);
+	getjpeghdr(is);
+	h := is.jstate;
+	chans: array of array of byte = nil;
+	for(;;) {
+		(m, n) := jpegtabmisc(is);
+		if(m == EOI)
+			break;
+		if(m != SOS)
+			imgerror(is, "Jpeg expected start of scan");
+		h.Ns = getc(is);
+		if(dbg)
+			sys->print("start of scan, Ns=%d\n", h.Ns);
+		scomp := array[h.Ns] of Scancomp;
+		for(i := 0; i < h.Ns; i++) {
+			scomp[i].C = getc(is);
+			(scomp[i].tdc, scomp[i].tac) = nibbles(getc(is));
+		}
+		h.scomp = scomp;
+		h.Ss = getc(is);
+		h.Se = getc(is);
+		(h.Ah, h.Al) = nibbles(getc(is));
+		if(n != 4+h.Ns*2)
+			imgerror(is, "Jpeg SOS header wrong length");
+
+		if(h.mode == byte SOF) {
+			if(chans != nil)
+				imgerror(is, "Jpeg baseline has > 1 scan");
+			chans = jpegbaselinescan(is);
+		}
+		else
+			jpegprogressivescan(is);
+	}
+	if(h.mode == byte SOF2)
+		chans = jprogressiveIDCT(is);
+	if(chans == nil)
+		imgerror(is, "jpeg has no image");
+	width := is.width;
+	height := is.height;
+	if(width != h.X || height != h.Y) {
+		for(k := 0; k < len chans; k++)
+			chans[k] = resample(chans[k], h.X, h.Y, width, height);
+	}
+	if(dbgev)
+		CU->event("IMAGE_JPG_REMAP", 0);
+	if(len chans == 1) {
+		im := newimagegrey(is, width, height);
+		im.writepixels(im.r, chans[0]);
+		return newmi(im);
+#		remapgrey(chans[0], width, height);
+	} else {
+		if (len chans == 3) {
+			r := remapYCbCr(chans);
+			im := newimage24(is, width, height);
+			im.writepixels(im.r, r);
+			return newmi(im);
+		}
+		remaprgb(chans, width, height, jpegcolorspace);
+	}
+	if(dbgev)
+		CU->event("IMAGE_JPG_REMAP_END", 0);
+	im := newimage(is, width, height);
+	im.writepixels(im.r, chans[0]);
+	if(dbgev)
+		CU->event("IMAGE_GETJPGMIM_END", 0);
+	return newmi(im);
+}
+
+remapYCbCr(chans: array of array of byte): array of byte
+{
+	Y := chans[0];
+	Cb := chans[1];
+	Cr := chans[2];
+
+	rgb := array [3*len Y] of byte;
+	bix := 0;
+	for (i := 0; i < len Y; i++) {
+		y := int Y[i];
+		cb := int Cb[i];
+		cr := int Cr[i];
+		r := y + Cr2r[cr];
+		g := y - Cr2g[cr] - Cb2g[cb];
+		b := y + Cb2b[cb];
+
+		rgb[bix++] = clampb[b+CLAMPBOFF];
+		rgb[bix++] = clampb[g+CLAMPBOFF];
+		rgb[bix++] = clampb[r+CLAMPBOFF];
+	}
+	return rgb;
+}
+
+zig := array[64] of {
+	0, 1, 8, 16, 9, 2, 3, 10, 17, # 0-7
+	24, 32, 25, 18, 11, 4, 5, # 8-15
+	12, 19, 26, 33, 40, 48, 41, 34, # 16-23
+	27, 20, 13, 6, 7, 14, 21, 28, # 24-31
+	35, 42, 49, 56, 57, 50, 43, 36, # 32-39
+	29, 22, 15, 23, 30, 37, 44, 51, # 40-47
+	58, 59, 52, 45, 38, 31, 39, 46, # 48-55
+	53, 60, 61, 54, 47, 55, 62, 63 # 56-63
+};
+
+jpegbaselinescan(is: ref ImageSource) : array of array of byte
+{
+	if(dbg)
+		sys->print("jpegbaselinescan\n");
+	if(dbgev)
+		CU->event("IMAGE_JPGBASELINESCAN", 0);
+	h := is.jstate;
+	Ns := h.Ns;
+	if(Ns != h.Nf)
+		imgerror(is, "Jpeg baseline needs Ns==Nf");
+	if(!(Ns==3 || Ns==1))
+		imgerror(is, "Jpeg baseline needs Ns==1 or 3");
+
+	res := ResourceState.cur();
+	heapavail := res.heaplim - res.heap;
+
+	# check heap availability for
+	#   chans: (3+Ns)*4 + (Ns*(3*4+h.X*h.Y)) bytes
+	#   Td, Ta, data, H, V, DC: 6 arrays of (3+Ns)*4 bytes
+	#
+	heapavail -= (3+Ns)*28 + (Ns*(12 + h.X * h.Y));
+	if(heapavail <= 0) {
+		if(dbg)
+			sys->print("jpegbaselinescan: no memory for chans et al.\n");
+		imgerror(is, "not enough memory");
+	}
+	
+	chans := array[h.Nf] of array of byte;
+	for(k:=0; k<h.Nf; k++)
+		chans[k] = array[h.X*h.Y] of byte;
+
+	# build per-component arrays
+	Td := array[Ns] of int;
+	Ta := array[Ns] of int;
+	data := array[Ns] of array of array of int;
+	H := array[Ns] of int;
+	V := array[Ns] of int;
+	DC := array[Ns] of int;
+
+	# compute maximum H and V
+	Hmax := 0;
+	Vmax := 0;
+	for(comp:=0; comp<Ns; comp++) {
+		if(h.comp[comp].H > Hmax)
+			Hmax = h.comp[comp].H;
+		if(h.comp[comp].V > Vmax)
+			Vmax = h.comp[comp].V;
+	}
+	if(dbg > 1)
+		sys->print("Hmax=%d, Vmax=%d\n", Hmax, Vmax);
+
+	# initialize data structures
+	allHV1 := 1;
+	for(comp=0; comp<Ns; comp++) {
+		# JPEG requires scan components to be in same order as in frame,
+		# so if both have 3 we know scan is Y Cb Cr and there's no need to
+		# reorder
+		Td[comp] = h.scomp[comp].tdc;
+		Ta[comp] = h.scomp[comp].tac;
+		H[comp] = h.comp[comp].H;
+		V[comp] = h.comp[comp].V;
+		nblock := H[comp]*V[comp];
+		if(nblock != 1)
+			allHV1 = 0;
+
+		# data[comp]: needs (3+nblock)*4 + nblock*(3+8*8)*4 bytes
+		heapavail -= 272*nblock + 12;
+		if(heapavail <= 0){
+			if(dbg)
+				sys->print("jpegbaselinescan: no memory for data\n");
+			imgerror(is, "not enough memory");
+		}
+
+		data[comp] = array[nblock] of array of int;
+		DC[comp] = 0;
+		for(m:=0; m<nblock; m++)
+			data[comp][m] = array[8*8] of int;
+		if(dbg > 2)
+			sys->print("scan comp %d: H=%d, V=%d, nblock=%d, Td=%d, Ta=%d\n",
+				comp, H[comp], V[comp], nblock, Td[comp], Ta[comp]);
+	}
+
+	ri := h.ri;
+
+	h.cnt = 0;
+	h.sr = 0;
+	nacross := ((h.X+(8*Hmax-1))/(8*Hmax));
+	nmcu := ((h.Y+(8*Vmax-1))/(8*Vmax))*nacross;
+	if(dbg)
+		sys->print("nacross=%d, nmcu=%d\n", nacross, nmcu);
+	for(mcu:=0; mcu<nmcu; ) {
+		if(dbg > 2)
+			sys->print("mcu %d\n", mcu);
+		for(comp=0; comp<Ns; comp++) {
+			if(dbg > 2)
+				sys->print("comp %d\n", comp);
+			dcht := h.dcht[Td[comp]];
+			acht := h.acht[Ta[comp]];
+			qt := h.qt[h.comp[comp].Tq];
+
+			for(block:=0; block<H[comp]*V[comp]; block++) {
+				if(dbg > 2)
+					sys->print("block %d\n", block);
+				# F-22
+				t := jdecode(is, dcht);
+				diff := jreceive(is, t);
+				DC[comp] += diff;
+				if(dbg > 2)
+					sys->print("t=%d, diff=%d, DC=%d\n", t, diff, DC[comp]);
+
+				# F-23
+				zz := data[comp][block];
+				zz[0:] = zeroints;
+				zz[0] = qt[0]*DC[comp];
+				k = 1;
+
+				for(;;) {
+					rs := jdecode(is, acht);
+					(rrrr, ssss) := nibbles(rs);
+					if(ssss == 0){
+						if(rrrr != 15)
+							break;
+						k += 16;
+					}else{
+						k += rrrr;
+						z := jreceive(is, ssss);
+						zz[zig[k]] = z*qt[k];
+						if(k == 63)
+							break;
+						k++;
+					}
+				}
+
+				idct(zz);
+			}
+		}
+
+		# rotate colors to RGB and assign to bytes
+		if(Ns == 1) # very easy
+			colormap1(h, chans[0], data[0][0], mcu, nacross);
+		else if(allHV1) # fairly easy
+			colormapall1(h, chans, data[0][0], data[1][0], data[2][0], mcu, nacross);
+		else # miserable general case
+			colormap(h, chans, data[0], data[1], data[2], mcu, nacross, Hmax, Vmax, H, V);
+
+		# process restart marker, if present
+		mcu++;
+		if(ri>0 && mcu<nmcu && mcu%ri==0){
+			jrestart(is, mcu);
+			for(comp=0; comp<Ns; comp++)
+				DC[comp] = 0;
+		}
+	}
+	if(dbgev)
+		CU->event("IMAGE_JPGBASELINESCAN_END", 0);
+	return chans;
+}
+
+jrestart(is: ref ImageSource, mcu: int)
+{
+	h := is.jstate;
+	ri := h.ri;
+	restart := mcu/ri-1;
+	rst, nskip: int;
+	nskip = 0;
+	do {
+		do{
+			rst = jnextborm(is);
+			nskip++;
+		}while(rst>=0 && rst!=16rFF);
+		if(rst == 16rFF){
+			rst = jnextborm(is);
+			nskip++;
+		}
+	} while(rst>=0 && (rst&~7)!= RST);
+	if(nskip != 2 || rst < 0 || ((rst&7) != (restart&7)))
+		imgerror(is, "Jpeg restart problem");
+	h.cnt = 0;
+	h.sr = 0;
+}
+
+jpegprogressivescan(is: ref ImageSource)
+{
+	if(dbgev)
+		CU->event("IMAGE_JPGPROGSCAN", 0);
+	h := is.jstate;
+	if(h.dccoeff == nil)
+		jprogressiveinit(is, h);
+
+	c := h.scomp[0].C;
+	comp := -1;
+	for(i:=0; i<h.Nf; i++)
+		if(h.comp[i].C == c)
+			comp = i;
+	if(comp == -1)
+		imgerror(is, "Jpeg bad component index in scan header");
+
+	if(h.Ss == 0)
+		jprogressivedc(is, comp);
+	else if(h.Ah == 0)
+		jprogressiveac(is, comp);
+	else
+		jprogressiveacinc(is, comp);
+	if(dbgev)
+		CU->event("IMAGE_JPGPROGSCAN_END", 0);
+}
+
+jprogressiveIDCT(is: ref ImageSource): array of array of byte
+{
+	if(dbgev)
+		CU->event("IMAGE_JPGPROGIDCT", 0);
+	h := is.jstate;
+	Nf := h.Nf;
+
+	res := ResourceState.cur();
+	heapavail := res.heaplim - res.heap;
+
+	# check heap availability for
+	#   H, V, data, blockno: 4 arrays of (3+Nf)*4 bytes
+	#   chans: (3+Nf)*4 + (Nf*(3*4+h.X*h.Y)) bytes
+	#
+	heapavail -= (3+Nf)*20 + (Nf*(12 + h.X * h.Y));
+	if(heapavail <= 0) {
+		if(dbg)
+			sys->print("jprogressiveIDCT: no memory for chans et al.\n");
+		imgerror(is, "not enough memory");
+	}
+	H := array[Nf] of int;
+	V := array[Nf] of int;
+
+	allHV1 := 1;
+
+	data := array[Nf] of array of array of int;
+	for(comp:=0; comp<Nf; comp++){
+		H[comp] = h.comp[comp].H;
+		V[comp] = h.comp[comp].V;
+		nblock := h.nblock[comp];
+		if(nblock != 1)
+			allHV1 = 0;
+
+		# data[comp]: needs (3+nblock)*4 + nblock*(3+8*8)*4 bytes
+		heapavail -= 272*nblock + 12;
+		if(heapavail <= 0){
+			if(dbg)
+				sys->print("jprogressiveIDCT: no memory for data\n");
+			imgerror(is, "not enough memory");
+		}
+
+		data[comp] = array[nblock] of array of int;
+		for(m:=0; m<nblock; m++)
+			data[comp][m] = array[8*8] of int;
+	}
+
+	chans := array[h.Nf] of array of byte;
+	for(k:=0; k<h.Nf; k++)
+		chans[k] = array[h.X*h.Y] of byte;
+
+	blockno := array[Nf] of {* => 0};
+	nmcu := h.nacross*h.ndown;
+	for(mcu:=0; mcu<nmcu; mcu++){
+		for(comp=0; comp<Nf; comp++){
+			dccoeff := h.dccoeff[comp];
+			accoeff := h.accoeff[comp];
+			bn := blockno[comp];
+			for(block:=0; block<h.nblock[comp]; block++){
+				zz := data[comp][block];
+				zz[0:] = zeroints;
+				zz[0] = dccoeff[bn];
+
+				for(k=1; k<64; k++)
+					zz[zig[k]] = accoeff[bn][k];
+
+				idct(zz);
+				bn++;
+			}
+			blockno[comp] = bn;
+		}
+
+		# rotate colors to RGB and assign to bytes
+		if(Nf == 1) # very easy
+			colormap1(h, chans[0], data[0][0], mcu, h.nacross);
+		else if(allHV1) # fairly easy
+			colormapall1(h, chans, data[0][0], data[1][0], data[2][0], mcu, h.nacross);
+		else # miserable general case
+			colormap(h, chans, data[0], data[1], data[2], mcu, h.nacross, h.Hmax, h.Vmax, H, V);
+	}
+	return chans;
+}
+
+jprogressiveinit(is: ref ImageSource, h: ref Jpegstate)
+{
+	Ns := h.Ns;
+	Nf := h.Nf;
+	if((Ns!=3 && Ns!=1) || Ns!=Nf)
+		imgerror(is, "Jpeg image must have 1 or 3 components");
+
+	# compute maximum H and V
+	h.Hmax = 0;
+	h.Vmax = 0;
+	for(comp:=0; comp<Nf; comp++){
+		if(h.comp[comp].H > h.Hmax)
+			h.Hmax = h.comp[comp].H;
+		if(h.comp[comp].V > h.Vmax)
+			h.Vmax = h.comp[comp].V;
+	}
+	h.nacross = ((h.X+(8*h.Hmax-1))/(8*h.Hmax));
+	h.ndown = ((h.Y+(8*h.Vmax-1))/(8*h.Vmax));
+	nmcu := h.nacross*h.ndown;
+
+	res := ResourceState.cur();
+	heapavail := res.heaplim - res.heap;
+
+	# check heap availability for
+	#   h.dccoeff: (3+Nf)*4 bytes
+	#   h.accoeff: (3+Nf)*4 bytes
+	heapavail -= (3+Nf)*8;
+	if(heapavail <= 0) {
+		if(dbg)
+			sys->print("jprogressiveinit: no memory for coeffs\n");
+		imgerror(is, "not enough memory");
+	}
+
+	h.dccoeff = array[Nf] of array of int;
+	h.accoeff = array[Nf] of array of array of int;
+	for(k:=0; k<Nf; k++){
+		n := h.nblock[k]*nmcu;
+
+		# check heap availability for
+		#   h.dccoeff[k]: (3+n)*4 bytes
+		#   h.accoeff[k]: (3+n)*4 + n*(3+64)*4 bytes
+		heapavail -= 276*n + 24;
+		if(heapavail <= 0){
+			if(dbg)
+				sys->print("jprogressiveinit: no memory for coeff arrays\n");
+			imgerror(is, "not enough memory");
+		}
+
+		h.dccoeff[k] = array[n] of {* => 0};
+		h.accoeff[k] = array[n] of array of int;
+		for(j:=0; j<n; j++)
+			h.accoeff[k][j] = array[64] of {* => 0};
+	}
+}
+
+jprogressivedc(is: ref ImageSource, comp: int)
+{
+	h := is.jstate;
+	Ns := h.Ns;
+	Ah := h.Ah;
+	Al := h.Al;
+	if(Ns!=h.Nf)
+		imgerror(is, "Jpeg progressive with Nf!=Ns in DC scan");
+
+	# build per-component arrays
+	Td := array[Ns] of int;
+	DC := array[Ns] of int;
+
+	# initialize data structures
+	h.cnt = 0;
+	h.sr = 0;
+	for(comp=0; comp<Ns; comp++) {
+		# JPEG requires scan components to be in same order as in frame,
+		# so if both have 3 we know scan is Y Cb Cr and there's no need to
+		# reorder
+		Td[comp] = h.scomp[comp].tdc;
+		DC[comp] = 0;
+	}
+
+	ri := h.ri;
+
+	nmcu := h.nacross*h.ndown;
+	blockno := array[Ns] of {* => 0};
+	for(mcu:=0; mcu<nmcu; ){
+		for(comp=0; comp<Ns; comp++){
+			dcht := h.dcht[Td[comp]];
+			qt := h.qt[h.comp[comp].Tq][0];
+			dc := h.dccoeff[comp];
+			bn := blockno[comp];
+
+			for(block:=0; block<h.nblock[comp]; block++) {
+				if(Ah == 0) {
+					t := jdecode(is, dcht);
+					diff := jreceive(is, t);
+					DC[comp] += diff;
+					dc[bn] = qt*DC[comp]<<Al;
+				} else
+					dc[bn] |= qt*jreceivebit(is)<<Al;
+				bn++;
+			}
+			blockno[comp] = bn;
+		}
+
+		# process restart marker, if present
+		mcu++;
+		if(ri>0 && mcu<nmcu && mcu%ri==0){
+			jrestart(is, mcu);
+			for(comp=0; comp<Ns; comp++)
+				DC[comp] = 0;
+		}
+	}
+}
+
+jprogressiveac(is: ref ImageSource, comp: int)
+{
+	h := is.jstate;
+	Ns := h.Ns;
+	Al := h.Al;
+	if(Ns != 1)
+		imgerror(is, "Jpeg illegal Ns>1 in progressive AC scan");
+	Ss := h.Ss;
+	Se := h.Se;
+	H := h.comp[comp].H;
+	V := h.comp[comp].V;
+
+	nacross := h.nacross*H;
+	ndown := h.ndown*V;
+	q := 8*h.Hmax/H;
+	nhor := (h.X+q-1)/q;
+	q = 8*h.Vmax/V;
+	nver := (h.Y+q-1)/q;
+
+	# initialize data structures
+	h.cnt = 0;
+	h.sr = 0;
+	Ta := h.scomp[0].tac;
+
+	ri := h.ri;
+
+	eobrun := 0;
+	acht := h.acht[Ta];
+	qt := h.qt[h.comp[comp].Tq];
+	nmcu := nacross*ndown;
+	mcu := 0;
+	for(y:=0; y<nver; y++) {
+		for(x:=0; x<nhor; x++) {
+			# Figure G-3
+			if(eobrun > 0){
+				--eobrun;
+				continue;
+			}
+
+			# arrange blockno to be in same sequence as
+			# original scan calculation.
+			tmcu := x/H + (nacross/H)*(y/V);
+			blockno := tmcu*H*V + H*(y%V) + x%H;
+			acc := h.accoeff[comp][blockno];
+			k := Ss;
+			for(;;) {
+				rs := jdecode(is, acht);
+				(rrrr, ssss) := nibbles(rs);
+				if(ssss == 0) {
+					if(rrrr < 15) {
+						eobrun = 0;
+						if(rrrr > 0)
+							eobrun = jreceiveEOB(is, rrrr)-1;
+						break;
+					}
+					k += 16;
+				}
+				else {
+					k += rrrr;
+					z := jreceive(is, ssss);
+					acc[k] = z*qt[k]<<Al;
+					if(k == Se)
+						break;
+					k++;
+				}
+			}
+		}
+
+		# process restart marker, if present
+		mcu++;
+		if(ri>0 && mcu<nmcu && mcu%ri==0) {
+			jrestart(is, mcu);
+			eobrun = 0;
+		}
+	}
+}
+
+jprogressiveacinc(is: ref ImageSource, comp: int)
+{
+	h := is.jstate;
+	Ns := h.Ns;
+	if(Ns != 1)
+		imgerror(is, "Jpeg  illegal Ns>1 in progressive AC scan");
+	Ss := h.Ss;
+	Se := h.Se;
+	H := h.comp[comp].H;
+	V := h.comp[comp].V;
+	Al := h.Al;
+
+	nacross := h.nacross*H;
+	ndown := h.ndown*V;
+	q := 8*h.Hmax/H;
+	nhor := (h.X+q-1)/q;
+	q = 8*h.Vmax/V;
+	nver := (h.Y+q-1)/q;
+
+	# initialize data structures
+	h.cnt = 0;
+	h.sr = 0;
+	Ta := h.scomp[0].tac;
+	ri := h.ri;
+
+	eobrun := 0;
+	ac := h.accoeff[comp];
+	acht := h.acht[Ta];
+	qt := h.qt[h.comp[comp].Tq];
+	nmcu := nacross*ndown;
+	mcu := 0;
+	pending := 0;
+	nzeros := -1;
+	for(y:=0; y<nver; y++){
+		for(x:=0; x<nhor; x++){
+			# Figure G-7
+
+			# arrange blockno to be in same sequence as
+			# original scan calculation.
+			tmcu := x/H + (nacross/H)*(y/V);
+			blockno := tmcu*H*V + H*(y%V) + x%H;
+			acc := ac[blockno];
+			if(eobrun > 0){
+				if(nzeros > 0)
+					imgerror(is, "Jpeg zeros pending at block start");
+				for(k:=Ss; k<=Se; k++)
+					jincrement(is, acc, k, qt[k]<<Al);
+				--eobrun;
+				continue;
+			}
+
+			for(k:=Ss; k<=Se; ){
+				if(nzeros >= 0){
+					if(acc[k] != 0)
+						jincrement(is, acc, k, qt[k]<<Al);
+					else if(nzeros-- == 0)
+						acc[k] = pending;
+					k++;
+					continue;
+				}
+				rs := jdecode(is, acht);
+				(rrrr, ssss) := nibbles(rs);
+				if(ssss == 0){
+					if(rrrr < 15){
+						eobrun = 0;
+						if(rrrr > 0)
+							eobrun = jreceiveEOB(is, rrrr)-1;
+						while(k <= Se){
+							jincrement(is, acc, k, qt[k]<<Al);
+							k++;
+						}
+						break;
+					}
+					for(i:=0; i<16; k++){
+						jincrement(is, acc, k, qt[k]<<Al);
+						if(acc[k] == 0)
+							i++;
+					}
+					continue;
+				}else if(ssss != 1)
+					imgerror(is, "Jpeg ssss!=1 in progressive increment");
+				nzeros = rrrr;
+				pending = jreceivebit(is);
+				if(pending == 0)
+					pending = -1;
+				pending *= qt[k]<<Al;
+			}
+		}
+
+		# process restart marker, if present
+		mcu++;
+		if(ri>0 && mcu<nmcu && mcu%ri==0){
+			jrestart(is, mcu);
+			eobrun = 0;
+			nzeros = -1;
+		}
+	}
+}
+
+jincrement(is: ref ImageSource, acc: array of int, k, Pt: int)
+{
+	if(acc[k] == 0)
+		return;
+	b := jreceivebit(is);
+	if(b != 0)
+		if(acc[k] < 0)
+			acc[k] -= Pt;
+		else
+			acc[k] += Pt;
+}
+
+jc1: con 2871;		# 1.402 * 2048
+jc2: con 705;		# 0.34414 * 2048
+jc3: con 1463;		# 0.71414 * 2048
+jc4: con 3629;		# 1.772 * 2048
+
+# Fills in pixels (x,y) for x = minx=8*(mcu%nacross), minx+1, ..., minx+7 (or h.X-1, if less)
+# and for y = miny=8*(mcu/nacross), miny+1, ..., miny+7 (or h.Y-1, if less)
+colormap1(h: ref Jpegstate, pic: array of byte, data: array of int, mcu, nacross: int)
+{
+	minx := 8*(mcu%nacross);
+	dx := 8;
+	if(minx+dx > h.X)
+		dx = h.X-minx;
+	miny := 8*(mcu/nacross);
+	dy := 8;
+	if(miny+dy > h.Y)
+		dy = h.Y-miny;
+	pici := miny*h.X+minx;
+	k := 0;
+	for(y:=0; y<dy; y++) {
+		for(x:=0; x<dx; x++)
+			pic[pici+x] = clampb[(data[k+x]+128)+CLAMPBOFF];
+		pici += h.X;
+		k += 8;
+	}
+}
+
+# Fills in same pixels as colormap1
+colormapall1(h: ref Jpegstate, chans: array of array of byte, data0, data1, data2: array of int, mcu, nacross: int)
+{
+	rpic := chans[0];
+	gpic := chans[1];
+	bpic := chans[2];
+	minx := 8*(mcu%nacross);
+	dx := 8;
+	if(minx+dx > h.X)
+		dx = h.X-minx;
+	miny := 8*(mcu/nacross);
+	dy := 8;
+	if(miny+dy > h.Y)
+		dy = h.Y-miny;
+	pici := miny*h.X+minx;
+	k := 0;
+	for(y:=0; y<dy; y++) {
+		for(x:=0; x<dx; x++){
+			if(jpegcolorspace == CYCbCr) {
+				rpic[pici+x] = clampb[data0[k+x]+128+CLAMPBOFF];
+				gpic[pici+x] = clampb[data1[k+x]+128+CLAMPBOFF];
+				bpic[pici+x] = clampb[data2[k+x]+128+CLAMPBOFF];
+			}
+			else { # RGB
+				Y := (data0[k+x]+128) << 11;
+				Cb := data1[k+x];
+				Cr := data2[k+x];
+				r := Y+jc1*Cr;
+				g := Y-jc2*Cb-jc3*Cr;
+				b := Y+jc4*Cb;
+				rpic[pici+x] = clampb[(r>>11)+CLAMPBOFF];
+				gpic[pici+x] = clampb[(g>>11)+CLAMPBOFF];
+				bpic[pici+x] = clampb[(b>>11)+CLAMPBOFF];
+			}
+		}
+		pici += h.X;
+		k += 8;
+	}
+}
+
+# Fills in pixels (x,y) for x = minx=8*Hmax*(mcu%nacross), minx+1, ..., minx+8*Hmax-1 (or h.X-1, if less)
+# and for y = miny=8*Vmax*(mcu/nacross), miny+1, ..., miny+8*Vmax-1 (or h.Y-1, if less)
+colormap(h: ref Jpegstate, chans: array of array of byte, data0, data1, data2: array of array of int, mcu, nacross, Hmax, Vmax: int,  H, V: array of int)
+{
+	rpic := chans[0];
+	gpic := chans[1];
+	bpic := chans[2];
+	minx := 8*Hmax*(mcu%nacross);
+	dx := 8*Hmax;
+	if(minx+dx > h.X)
+		dx = h.X-minx;
+	miny := 8*Vmax*(mcu/nacross);
+	dy := 8*Vmax;
+	if(miny+dy > h.Y)
+		dy = h.Y-miny;
+	pici := miny*h.X+minx;
+	H0 := H[0];
+	H1 := H[1];
+	H2 := H[2];
+	if(dbg > 2)
+		sys->print("colormap, minx=%d, miny=%d, dx=%d, dy=%d, pici=%d, H0=%d, H1=%d, H2=%d\n",
+			minx, miny, dx, dy, pici, H0, H1, H2);
+	for(y:=0; y<dy; y++) {
+		t := y*V[0];
+		b0 := H0*(t/(8*Vmax));
+		y0 := 8*((t/Vmax)&7);
+		t = y*V[1];
+		b1 := H1*(t/(8*Vmax));
+		y1 := 8*((t/Vmax)&7);
+		t = y*V[2];
+		b2 := H2*(t/(8*Vmax));
+		y2 := 8*((t/Vmax)&7);
+		x0 := 0;
+		x1 := 0;
+		x2 := 0;
+		for(x:=0; x<dx; x++) {
+			if(jpegcolorspace == CYCbCr) {
+				rpic[pici+x] = clampb[data0[b0][y0+x0++*H0/Hmax] + 128 + CLAMPBOFF];
+				gpic[pici+x] = clampb[data1[b1][y1+x1++*H1/Hmax] + 128 + CLAMPBOFF];
+				bpic[pici+x] = clampb[data2[b2][y2+x2++*H2/Hmax] + 128 + CLAMPBOFF];
+			}
+			else { # RGB
+				Y := (data0[b0][y0+x0++*H0/Hmax]+128) << 11;
+				Cb := data1[b1][y1+x1++*H1/Hmax];
+				Cr := data2[b2][y2+x2++*H2/Hmax];
+				r := Y+jc1*Cr;
+				g := Y-jc2*Cb-jc3*Cr;
+				b := Y+jc4*Cb;
+				rpic[pici+x] = clampb[(r>>11)+CLAMPBOFF];
+				gpic[pici+x] = clampb[(g>>11)+CLAMPBOFF];
+				bpic[pici+x] = clampb[(b>>11)+CLAMPBOFF];
+			}
+			if(x0*H0/Hmax >= 8){
+				x0 = 0;
+				b0++;
+			}
+			if(x1*H1/Hmax >= 8){
+				x1 = 0;
+				b1++;
+			}
+			if(x2*H2/Hmax >= 8){
+				x2 = 0;
+				b2++;
+			}
+		}
+		pici += h.X;
+	}
+}
+
+# decode next 8-bit value from entropy-coded input.  chart F-26
+jdecode(is: ref ImageSource, t: ref Huffman): int
+{
+	h := is.jstate;
+	maxcode := t.maxcode;
+	if(h.cnt < 8)
+		jnextbyte(is);
+	# fast lookup
+	code := (h.sr>>(h.cnt-8))&16rFF;
+	v := t.value[code];
+	if(v >= 0){
+		h.cnt -= t.shift[code];
+		return v;
+	}
+
+	h.cnt -= 8;
+	if(h.cnt == 0)
+		jnextbyte(is);
+	h.cnt--;
+	cnt := h.cnt;
+	m := 1<<cnt;
+	sr := h.sr;
+	code <<= 1;
+	i := 9;
+	for(;;i++){
+		if(sr & m)
+			code |= 1;
+		if(code <= maxcode[i])
+			break;
+		code <<= 1;
+		m >>= 1;
+		if(m == 0){
+			sr = jnextbyte(is);
+			m = 16r80;
+			cnt = 8;
+		}
+		cnt--;
+	}
+	h.cnt = cnt;
+	return t.val[t.valptr[i]+(code-t.mincode[i])];
+}
+
+# load next byte of input
+jnextbyte(is: ref ImageSource): int
+{
+	b :=getc(is);
+
+	if(b == 16rFF) {
+		b2 :=getc(is);
+		if(b2 != 0) {
+			if(b2 == int DNL)
+				imgerror(is, "Jpeg  DNL marker unimplemented");
+			# decoder is reading into marker; satisfy it and restore state
+			ungetc2(is, byte b);
+		}
+	}
+	h := is.jstate;
+	h.cnt += 8;
+	h.sr = (h.sr<<8)| b;
+	return b;
+}
+
+# like jnextbyte, but look for marker too
+jnextborm(is: ref ImageSource): int
+{
+	b :=getc(is);
+
+	if(b == 16rFF)
+		return b;
+	h := is.jstate;
+	h.cnt += 8;
+	h.sr = (h.sr<<8)| b;
+	return b;
+}
+
+# return next s bits of input, MSB first, and level shift it
+jreceive(is: ref ImageSource, s: int): int
+{
+	h := is.jstate;
+	while(h.cnt < s)
+		jnextbyte(is);
+	h.cnt -= s;
+	v := h.sr >> h.cnt;
+	m := (1<<s);
+	v &= m-1;
+	# level shift
+	if(v < (m>>1))
+		v += ~(m-1)+1;
+	return v;
+}
+
+# return next s bits of input, decode as EOB
+jreceiveEOB(is: ref ImageSource, s: int): int
+{
+	h := is.jstate;
+	while(h.cnt < s)
+		jnextbyte(is);
+	h.cnt -= s;
+	v := h.sr >> h.cnt;
+	m := (1<<s);
+	v &= m-1;
+	# level shift
+	v += m;
+	return v;
+}
+
+# return next bit of input
+jreceivebit(is: ref ImageSource): int
+{
+	h := is.jstate;
+	if(h.cnt < 1)
+		jnextbyte(is);
+	h.cnt--;
+	return (h.sr >> h.cnt) & 1;
+}
+
+
+nibbles(c: int) : (int, int)
+{
+	return (c>>4, c&15);
+
+}
+
+# Scaled integer implementation.
+# inverse two dimensional DCT, Chen-Wang algorithm
+# (IEEE ASSP-32, pp. 803-816, Aug. 1984)
+# 32-bit integer arithmetic (8 bit coefficients)
+# 11 mults, 29 adds per DCT
+#
+# coefficients extended to 12 bit for IEEE1180-1990
+# compliance
+
+W1:	con 2841;	# 2048*sqrt(2)*cos(1*pi/16)
+W2:	con 2676;	# 2048*sqrt(2)*cos(2*pi/16)
+W3:	con 2408;	# 2048*sqrt(2)*cos(3*pi/16)
+W5:	con 1609;	# 2048*sqrt(2)*cos(5*pi/16)
+W6:	con 1108;	# 2048*sqrt(2)*cos(6*pi/16)
+W7:	con 565;	# 2048*sqrt(2)*cos(7*pi/16)
+
+W1pW7:	con 3406;	# W1+W7
+W1mW7:	con 2276;	# W1-W7
+W3pW5:	con 4017;	# W3+W5
+W3mW5:	con 799;	# W3-W5
+W2pW6:	con 3784;	# W2+W6
+W2mW6:	con 1567;	# W2-W6
+
+R2:	con 181;	# 256/sqrt(2)
+
+idct(b: array of int)
+{
+	# transform horizontally
+	for(y:=0; y<8; y++){
+		eighty := y<<3;
+		# if all non-DC components are zero, just propagate the DC term
+		if(b[eighty+1]==0)
+		if(b[eighty+2]==0 && b[eighty+3]==0)
+		if(b[eighty+4]==0 && b[eighty+5]==0)
+		if(b[eighty+6]==0 && b[eighty+7]==0){
+			v := b[eighty]<<3;
+			b[eighty+0] = v;
+			b[eighty+1] = v;
+			b[eighty+2] = v;
+			b[eighty+3] = v;
+			b[eighty+4] = v;
+			b[eighty+5] = v;
+			b[eighty+6] = v;
+			b[eighty+7] = v;
+			continue;
+		}
+		# prescale
+		x0 := (b[eighty+0]<<11)+128;
+		x1 := b[eighty+4]<<11;
+		x2 := b[eighty+6];
+		x3 := b[eighty+2];
+		x4 := b[eighty+1];
+		x5 := b[eighty+7];
+		x6 := b[eighty+5];
+		x7 := b[eighty+3];
+		# first stage
+		x8 := W7*(x4+x5);
+		x4 = x8 + W1mW7*x4;
+		x5 = x8 - W1pW7*x5;
+		x8 = W3*(x6+x7);
+		x6 = x8 - W3mW5*x6;
+		x7 = x8 - W3pW5*x7;
+		# second stage
+		x8 = x0 + x1;
+		x0 -= x1;
+		x1 = W6*(x3+x2);
+		x2 = x1 - W2pW6*x2;
+		x3 = x1 + W2mW6*x3;
+		x1 = x4 + x6;
+		x4 -= x6;
+		x6 = x5 + x7;
+		x5 -= x7;
+		# third stage
+		x7 = x8 + x3;
+		x8 -= x3;
+		x3 = x0 + x2;
+		x0 -= x2;
+		x2 = (R2*(x4+x5)+128)>>8;
+		x4 = (R2*(x4-x5)+128)>>8;
+		# fourth stage
+		b[eighty+0] = (x7+x1)>>8;
+		b[eighty+1] = (x3+x2)>>8;
+		b[eighty+2] = (x0+x4)>>8;
+		b[eighty+3] = (x8+x6)>>8;
+		b[eighty+4] = (x8-x6)>>8;
+		b[eighty+5] = (x0-x4)>>8;
+		b[eighty+6] = (x3-x2)>>8;
+		b[eighty+7] = (x7-x1)>>8;
+	}
+	# transform vertically
+	for(x:=0; x<8; x++){
+		# if all non-DC components are zero, just propagate the DC term
+		if(b[x+8*1]==0)
+		if(b[x+8*2]==0 && b[x+8*3]==0)
+		if(b[x+8*4]==0 && b[x+8*5]==0)
+		if(b[x+8*6]==0 && b[x+8*7]==0){
+			v := (b[x+8*0]+32)>>6;
+			b[x+8*0] = v;
+			b[x+8*1] = v;
+			b[x+8*2] = v;
+			b[x+8*3] = v;
+			b[x+8*4] = v;
+			b[x+8*5] = v;
+			b[x+8*6] = v;
+			b[x+8*7] = v;
+			continue;
+		}
+		# prescale
+		x0 := (b[x+8*0]<<8)+8192;
+		x1 := b[x+8*4]<<8;
+		x2 := b[x+8*6];
+		x3 := b[x+8*2];
+		x4 := b[x+8*1];
+		x5 := b[x+8*7];
+		x6 := b[x+8*5];
+		x7 := b[x+8*3];
+		# first stage
+		x8 := W7*(x4+x5) + 4;
+		x4 = (x8+W1mW7*x4)>>3;
+		x5 = (x8-W1pW7*x5)>>3;
+		x8 = W3*(x6+x7) + 4;
+		x6 = (x8-W3mW5*x6)>>3;
+		x7 = (x8-W3pW5*x7)>>3;
+		# second stage
+		x8 = x0 + x1;
+		x0 -= x1;
+		x1 = W6*(x3+x2) + 4;
+		x2 = (x1-W2pW6*x2)>>3;
+		x3 = (x1+W2mW6*x3)>>3;
+		x1 = x4 + x6;
+		x4 -= x6;
+		x6 = x5 + x7;
+		x5 -= x7;
+		# third stage
+		x7 = x8 + x3;
+		x8 -= x3;
+		x3 = x0 + x2;
+		x0 -= x2;
+		x2 = (R2*(x4+x5)+128)>>8;
+		x4 = (R2*(x4-x5)+128)>>8;
+		# fourth stage
+		b[x+8*0] = (x7+x1)>>14;
+		b[x+8*1] = (x3+x2)>>14;
+		b[x+8*2] = (x0+x4)>>14;
+		b[x+8*3] = (x8+x6)>>14;
+		b[x+8*4] = (x8-x6)>>14;
+		b[x+8*5] = (x0-x4)>>14;
+		b[x+8*6] = (x3-x2)>>14;
+		b[x+8*7] = (x7-x1)>>14;
+	}
+}
+
+################# Remap colors and Dither ##############
+
+closest_rgbpix(r, g, b: int) : int
+{
+	pix := int closestrgb[((r>>4)<<8)+((g>>4)<<4)+(b>>4)];
+	# If white is the closest but original r,g,b wasn't white,
+	# look for another color, because web page designer probably
+	# cares more about contrast than actual color
+	if(pix == 0 && !(r == 255 && g ==255 && b == 255)) {
+		bestdist := 1000000;
+		for(i := 1; i < 256; i++) {
+			dr := r-rgbvmap_r[i];
+			dg := g-rgbvmap_g[i];
+			db := b-rgbvmap_b[i];
+			d := dr*dr + dg*dg + db*db;
+			if(d < bestdist) {
+				bestdist = d;
+				pix = i;
+			}
+		}
+	}
+	return pix;
+}
+
+CLAMPBOFF: con 300;
+NCLAMPB: con CLAMPBOFF+256+CLAMPBOFF;
+CLAMPNOFF: con 64;
+NCLAMPN: con CLAMPNOFF+256+CLAMPNOFF;
+
+clampb: array of byte;		# clamps byte values
+clampn_b: array of int;		# clamps byte values, then shifts >> 4
+clampn_g: array of int;		# clamps byte values, then masks off lower 4 bits
+clampn_r: array of int;		# clamps byte values, masks off lower 4 bits, then shifts <<4
+
+init_tabs()
+{
+	clampn_b = array[NCLAMPN] of int;
+	clampn_g = array[NCLAMPN] of int;
+	clampn_r = array[NCLAMPN] of int;
+	for(j:=0; j<CLAMPNOFF; j++) {
+		clampn_b[j] = 0;
+		clampn_g[j] = 0;
+		clampn_r[j] = 0;
+	}
+	for(j=0; j<256; j++) {
+		t := j>>4;
+		clampn_b[CLAMPNOFF+j] = t;
+		clampn_g[CLAMPNOFF+j] = t<<4;
+		clampn_r[CLAMPNOFF+j] = t<<8;
+	}
+	for(j=0; j<CLAMPNOFF; j++) {
+		clampn_b[CLAMPNOFF+256+j] = 16r0F;
+		clampn_g[CLAMPNOFF+256+j] = 16rF0;
+		clampn_r[CLAMPNOFF+256+j] = 16rF00;
+	}
+	clampb = array[NCLAMPB] of byte;
+	for(j=0; j<CLAMPBOFF; j++)
+		clampb[j] = byte 0;
+	for(j=0; j<256; j++)
+		clampb[CLAMPBOFF+j] = byte j;
+	for(j=0; j<CLAMPBOFF; j++)
+		clampb[CLAMPBOFF+256+j] = byte 16rFF;
+}
+
+# could account for mask in alpha rather than having separate mask
+remap24(pic: array of byte, cmap: array of byte): array of byte
+{
+	cmap_r := array[256] of byte;
+	cmap_g := array[256] of byte;
+	cmap_b := array[256] of byte;
+	i := 0;
+	for(j := 0; j < 256 && i < len cmap; j++) {
+		cmap_r[j] = cmap[i++];
+		cmap_g[j] = cmap[i++];
+		cmap_b[j] = cmap[i++];
+	}
+	# in case input has bad indices
+	for( ; j < 256; j++) {
+		cmap_r[j] = byte 0;
+		cmap_g[j] = byte 0;
+		cmap_b[j] = byte 0;
+	}
+	pic24 := array [3 * len pic] of byte;
+	ix24 := 0;
+	for (i = 0; i < len pic; i++) {
+		c := int pic[i];
+		pic24[ix24++] = cmap_b[c];
+		pic24[ix24++] = cmap_g[c];
+		pic24[ix24++] = cmap_r[c];
+	}
+	return pic24;
+}
+
+# Remap pixels of pic[] into the closest colors in the rgbv map,
+# and do error diffusion of the result.
+# pic is a one-channel image whose rgb values are given by looking
+# up values in cmap.
+remap1(pic: array of byte, dx, dy: int, cmap: array of byte)
+{
+	if(dbg)
+		sys->print("remap1, pic len %d, dx=%d, dy=%d\n", len pic, dx, dy);
+	cmap_r := array[256] of int;
+	cmap_g := array[256] of int;
+	cmap_b := array[256] of int;
+	i := 0;
+	for(j := 0; j < 256 && i < len cmap; j++) {
+		cmap_r[j] = int cmap[i++];
+		cmap_g[j] = int cmap[i++];
+		cmap_b[j] = int cmap[i++];
+	}
+	# in case input has bad indices
+	for( ; j < 256; j++) {
+		cmap_r[j] = 0;
+		cmap_g[j] = 0;
+		cmap_b[j] = 0;
+	}
+	# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+	ered := array[dx+1] of { * => 0 };
+	egrn := array[dx+1] of int;
+	eblu := array[dx+1] of int;
+	egrn[0:] = ered;
+	eblu[0:] = ered;
+	p := 0;
+	for(y:=0; y<dy; y++) {
+		er := 0;
+		eg := 0;
+		eb := 0;
+		for(x:=0; x<dx; ) {
+			x1 := x+1;
+			in := int pic[p];
+			r := cmap_r[in]+ered[x];
+			g := cmap_g[in]+egrn[x];
+			b := cmap_b[in]+eblu[x];
+			col := int (closestrgb[clampn_r[r+CLAMPNOFF]
+					+clampn_g[g+CLAMPNOFF]
+					+clampn_b[b+CLAMPNOFF]]);
+			pic[p++] = byte 255 - byte col;
+
+			r -= rgbvmap_r[col];
+			t := (3*r)>>4;
+			ered[x] = t+er;
+			ered[x1] += t;
+			er = r-3*t;
+
+			g -= rgbvmap_g[col];
+			t = (3*g)>>4;
+			egrn[x] = t+eg;
+			egrn[x1] += t;
+			eg = g-3*t;
+
+			b -= rgbvmap_b[col];
+			t = (3*b)>>4;
+			eblu[x] = t+eb;
+			eblu[x1] += t;
+			eb = b-3*t;
+
+			x = x1;
+		}
+	}
+}
+
+# Remap pixels of pic[] into the closest greyscale colors in the rgbv map,
+# and do error diffusion of the result.
+# pic is a one-channel greyscale image.
+remapgrey(pic: array of byte, dx, dy: int)
+{
+	if(dbg)
+		sys->print("remapgrey, pic len %d, dx=%d, dy=%d\n", len pic, dx, dy);
+	# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+	e := array[dx+1] of {* => 0 };
+	p := 0;
+	for(y:=0; y<dy; y++){
+		eb := 0;
+		for(x:=0; x<dx; ) {
+			x1 := x+1;
+			b := int pic[p]+e[x];
+			b1 := clampn_b[b+CLAMPNOFF];
+			col := 255-17*b1;
+			pic[p++] = byte col;
+
+			b -= rgbvmap_b[col];
+			t := (3*b)>>4;
+			e[x] = t+eb;
+			e[x1] += t;
+			eb = b-3*t;
+			x = x1;
+		}
+	}
+}
+
+# Remap pixels of chans into the closest colors in the rgbv map,
+# and do error diffusion of the result.
+# chans is a 3-channel image whose channels are either (y,cb,cr) or
+# (r,g,b), depending on whether colorspace is CYCbCr or CRGB.
+# Variable names use r,g,b (historical).
+remaprgb(chans: array of array of byte, dx, dy, colorspace: int)
+{
+	if(dbg)
+		sys->print("remaprgb, pic len %d, dx=%d, dy=%d\n", len chans[0], dx, dy);
+	rpic := chans[0];
+	gpic := chans[1];
+	bpic := chans[2];
+	pic := chans[0];
+	# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+	ered := array[dx+1] of { * => 0 };
+	egrn := array[dx+1] of int;
+	eblu := array[dx+1] of int;
+	egrn[0:] = ered;
+	eblu[0:] = ered;
+	closest: array of byte;
+	map0, map1, map2: array of int;
+	if(colorspace == CRGB) {
+		closest = closestrgb;
+		map0 = rgbvmap_r;
+		map1 = rgbvmap_g;
+		map2 = rgbvmap_b;
+	}
+	else {
+		closest = closestycbcr;
+		map0 = rgbvmap_y;
+		map1 = rgbvmap_cb;
+		map2 = rgbvmap_cr;
+	}
+	p := 0;
+	for(y:=0; y<dy; y++ ) {
+		er := 0;
+		eg := 0;
+		eb := 0;
+		for(x:=0; x<dx; ) {
+			x1 := x + 1;
+			r := int rpic[p]+ered[x];
+			g := int gpic[p]+egrn[x];
+			b := int bpic[p]+eblu[x];
+			# Errors can be uncorrectable if converting from YCbCr,
+			# since we can't guarantee that an extremal value of one of
+			# the components selects a color with an extremal value.
+			# If we don't, the errors accumulate without bound.  This
+			# doesn't happen in RGB because the closest table can guarantee
+			# a color on the edge of the gamut, producing a zero error in
+			# that component.  For the rotation YCbCr space, there may be
+			# no color that can guarantee zero error at the edge.
+			# Therefore we must clamp explicitly rather than by assuming
+			# an upper error bound of CLAMPOFF.  The performance difference
+			# is miniscule anyway.
+			if(r < 0)
+				r = 0;
+			else if(r > 255)
+				r = 255;
+			if(g < 0)
+				g = 0;
+			else if(g > 255)
+				g = 255;
+			if(b < 0)
+				b = 0;
+			else if(b > 255)
+				b = 255;
+			col := int (closest[(b>>4)+16*((g>>4)+(r&16rF0))]);
+			pic[p++] = byte (255-col);
+#			col := int (pic[p++] = closest[(b>>4)+16*((g>>4)+16*(r>>4))]);
+
+			r -= map0[col];
+			t := (3*r)>>4;
+			ered[x] = t+er;
+			ered[x1] += t;
+			er = r-3*t;
+
+			g -= map1[col];
+			t = (3*g)>>4;
+			egrn[x] = t+eg;
+			egrn[x1] += t;
+			eg = g-3*t;
+
+			b -= map2[col];
+			t = (3*b)>>4;
+			eblu[x] = t+eb;
+			eblu[x1] += t;
+			eb = b-3*t;
+
+			x = x1;
+		}
+	}
+}
+
+# Given src array, representing sw*sh pixel values, resample them into
+# the returned array, with dimensions dw*dh.
+#
+# Quick and dirty resampling: just interpolate.
+# This lets us resample arrays of pixels indices (e.g., result of gif decoding).
+# The filter-based resampling methods need conversion to rgb or grayscale.
+# Also, although the results won't look good, people really shouldn't be
+# asking the browser to resample except for special purposes (like the common
+# case of resizing a 1x1 image to make a spacer).
+resample(src: array of byte, sw, sh: int, dw, dh: int) : array of byte
+{
+	if(dbgev)
+		CU->event("IMAGE_RESAMPLE_START", 0);
+	if(src == nil || sw == 0 || sh == 0 || dw == 0 || dh == 0)
+		return src;
+	xfac := real sw / real dw;
+	yfac := real sh / real dh;
+	totpix := dw*dh;
+	dst := array[totpix] of byte;
+	dindex := 0;
+
+	# precompute index in src row corresponding to each index in dst row
+	sindices := array[dw] of int;
+	dx := 0.0;
+	for(x := 0; x < dw; x++) {
+		sx := int dx;
+		dx += xfac;
+		if(sx >= sw)
+			sx = sw-1;
+		sindices[x] = sx;
+	}
+	dy := 0.0;
+	for(y := 0; y < dh; y++) {
+		sy := int dy;
+		dy += yfac;
+		if(sy >= sh)
+			sy = sh-1;
+		soffset := sy * sw;
+		for(x = 0; x < dw; x++)
+			dst[dindex++] = src[soffset + sindices[x]];
+	}
+	if(dbgev)
+		CU->event("IMAGE_RESAMPLE_END", 0);
+	return dst;
+}
+
+################# BIT ###################
+
+getbitmim(is: ref ImageSource) : ref MaskedImage
+{
+	if(dbg)
+		sys->print("img getbitmim: w=%d h=%d len=%d\n",
+			is.width, is.height, len is.bs.data);
+
+	im := getbitimage(is, display, is.bs.data);
+	if(im == nil)
+		imgerror(is, "out of memory");
+	is.i = is.bs.edata;		# getbitimage should do this too!
+	is.width = im.r.max.x;
+	is.height = im.r.max.y;
+	return newmi(im);
+}
+
+
+NMATCH: con 3;			# shortest match possible
+NCBLOCK: con 6000;		# size of compressed blocks
+drawld2chan := array[] of {
+0 =>	Draw->GREY1,
+1 =>	Draw->GREY2,
+2 =>	Draw->GREY4,
+3 =>	Draw->CMAP8
+};
+
+getbitimage(is: ref ImageSource, disp: ref Display, d: array of byte): ref Image
+{
+	compressed := 0;
+
+	if(len d < 5*12)
+		imgerror(is, "bad bit format");
+
+	if(string d[:11] == "compressed\n"){
+		if(dbg)
+			sys->print("img: bit compressed\n");
+		compressed = 1;
+		d = d[11:];
+	}
+
+	#
+	# distinguish new channel descriptor from old ldepth.
+	# channel descriptors have letters as well as numbers,
+	# while ldepths are a single digit formatted as %-11d
+	#
+	new := 0;
+	for(m := 0; m < 10; m++){
+		if(d[m] != byte ' '){
+			new = 1;
+			break;
+		}
+	}
+	if(d[11] != byte ' ')
+		imgerror(is, "bad bit format");
+	chans: Chans;
+	if(new){
+		s := string d[0:11];
+		chans = Chans.mk(s);
+		if(chans.desc == 0)
+			imgerror(is, sys->sprint("bad channel string %s", s));
+	}else{
+		ld := int( d[10] - byte '0' );
+		if(ld < 0 || ld > 3)
+			imgerror(is, "bad bit ldepth");
+		chans = drawld2chan[ld];
+	}
+
+	xmin := int string d[ 1*12 : 2*12 ];
+	ymin := int string d[ 2*12 : 3*12 ];
+	xmax := int string d[ 3*12 : 4*12 ];
+	ymax := int string d[ 4*12 : 5*12 ];
+	if( (xmin > xmax) || (ymin > ymax) )
+		imgerror(is, "bad bit rectangle");
+
+	if(dbg)
+		sys->print("img: bit: chans=%s, xmin=%d, ymin=%d, xmax=%d, ymax=%d\n",
+			chans.text(), xmin, ymin, xmax, ymax);
+
+	r := Rect( (xmin, ymin), (xmax, ymax) );
+	im := disp.newimage(r, chans, 0, D->Black);
+	if(im == nil)
+		return nil;
+
+	if (!compressed){
+		if(!new)
+			for(j:=5*12; j<len d; j++)
+				d[j] ^= byte 16rFF;
+		im.writepixels(im.r, d[5*12:]);
+		return im;
+	}
+
+	# see /libdraw/readimage.c, /libdraw/creadimage.c, and
+	# /libmemdraw/cload.c for reference implementation
+	# of bit compression
+
+	bpl := D->bytesperline(r, im.depth);
+	a := array[(ymax-ymin)*bpl] of byte;
+	ai := 0;		#index into uncompressed data array a
+	di := 5*12;		#index into compressed data
+	while(ymin < ymax){
+		y := int string d[ di        : di + 1*12 ];
+		n := int string d[ di + 1*12 : di + 2*12 ];
+		di += 2*12;
+
+		if (y <= ymin || ymax < y)
+			imgerror(is, "bad compressed bit y-max");
+		if (n <= 0 || NCBLOCK < n)
+			imgerror(is, "bad compressed bit count");
+
+		# no input-stream error checking :-(
+		u := di;
+		while(di < u+n){
+			c := int d[di++];
+			if (c >= 128){
+				# copy as is
+				cnt := c-128 + 1;
+
+				# check for overrun of index di within d?
+
+				a[ai:] = d[di:di+cnt];
+				if(!new)
+					for(j:=0; j<cnt; j++)
+						a[ai+j] ^= byte 16rFF;
+				di += cnt;
+				ai += cnt;
+			}
+			else {
+				# copy a run/match
+				offs := int(d[di++]) + ((c&3)<<8) + 1;
+				cnt := (c>>2) + NMATCH;
+
+				# simply: a[ai:ai+cnt] = a[ai-offs:ai-offs+cnt];
+				for(i:=0; i<cnt; i++)
+					a[ai+i] = a[ai-offs+i];
+				ai += cnt;
+			}
+		}
+		ymin = y;
+	}
+	im.writepixels(im.r, a);
+	return im;
+}
+
+################# PNG ###################
+
+Rawimage: adt {
+	r:	Draw->Rect;
+	cmap:    array of byte;
+	transp:  int;	# transparency flag (only for nchans=1)
+	trindex: byte;	# transparency index
+	nchans:  int;
+	chans:   array of array of byte;
+	chandesc:int;
+
+	fields:	int;    # defined by format
+};
+
+Chunk: adt {
+	size : int;
+	typ: string;
+	crc_state: ref CRCstate;
+};
+
+Png: adt {
+	depth: int;
+	filterbpp: int;
+	colortype: int;
+	compressionmethod: int;
+	filtermethod: int;
+	interlacemethod: int;
+	# tRNS
+	PLTEsize: int;
+	tRNS: array of byte;
+	# state for managing unpacking
+	alpha: int;
+	done: int;
+	error: string;
+	row, rowstep, colstart, colstep: int;
+	phase: int;
+	phasecols: int;
+	phaserows: int;
+	rowsize: int;
+	rowbytessofar: int;
+	thisrow: array of byte;
+	lastrow: array of byte;
+};
+
+# currently do not support transparency
+# hence no mask is set
+#
+# need to re-jig this code
+# for example there is no point in mapping up a 2 or 4 bit greyscale image
+# to 8 bit luminance to then remap it to the inferno palette when
+# the draw device will do that for us anyway!
+
+getpngmim(is: ref ImageSource) : ref MaskedImage
+{
+	chunk := ref Chunk;
+	png := ref Png;
+	raw := ref Rawimage;
+
+	chunk.crc_state = crc->init(0, int 16rffffffff);
+# Check it's a PNG
+	if (!png_signature(is))
+		imgerror(is, "PNG not a PNG");
+# Get the IHDR
+	if (!png_chunk_header(is, chunk))
+		imgerror(is, "PNG duff header");
+	if (chunk.typ != "IHDR")
+		imgerror(is, "PNG IHDR must come first");
+	if (chunk.size != 13)
+		imgerror(is, "PNG IHDR wrong size");
+	raw.r.max.x = png_int(is, chunk.crc_state);
+	if (raw.r.max.x <= 0)
+		imgerror(is, "PNG invalid width");
+	raw.r.max.y = png_int(is, chunk.crc_state);
+	if (raw.r.max.y <= 0)
+		imgerror(is, "PNG invalid height");
+	png.depth = png_byte(is, chunk.crc_state);
+	case png.depth {
+	1 or 2 or 4 or 8 or 16 =>
+		;
+	* =>
+		imgerror(is, "PNG invalid depth");
+	}
+	png.colortype = png_byte(is, chunk.crc_state);
+
+	okcombo : int;
+
+	case png.colortype {
+	0 =>
+		okcombo = 1;
+		raw.nchans = 1;
+		raw.chandesc = CY;
+		png.alpha = 0;
+	2  =>
+		okcombo = (png.depth == 8 || png.depth == 16);
+		raw.nchans = 3;
+		raw.chandesc = CRGB;
+		png.alpha = 0;
+	3 =>
+		okcombo = (png.depth != 16);
+		raw.nchans = 1;
+		raw.chandesc = CRGB1;
+		png.alpha = 0;
+	4 =>
+		okcombo = (png.depth == 8 || png.depth == 16);
+		raw.nchans = 1;
+		raw.chandesc = CY;
+		png.alpha = 1;
+	6 =>
+		okcombo = (png.depth == 8 || png.depth == 16);
+		raw.nchans = 3;
+		raw.chandesc = CRGB;
+		png.alpha = 1;
+	* =>
+		imgerror(is, "PNG invalid colortype");
+	}
+	if (!okcombo)
+		imgerror(is, "PNG invalid depth/colortype combination");
+	png.compressionmethod = png_byte(is, chunk.crc_state);
+	if (png.compressionmethod != 0)
+		imgerror(is, "PNG invalid compression method " + string png.compressionmethod);
+	png.filtermethod = png_byte(is, chunk.crc_state);
+	if (png.filtermethod != 0)
+		imgerror(is, "PNG invalid filter method");
+	png.interlacemethod = png_byte(is, chunk.crc_state);
+	if (png.interlacemethod != 0 && png.interlacemethod != 1)
+		imgerror(is, "PNG invalid interlace method");
+#	sys->print("width %d height %d depth %d colortype %d interlace %d\n",
+#		raw.r.max.x, raw.r.max.y, png.depth, png.colortype, png.interlacemethod);
+	if (!png_crc_and_check(is, chunk))
+		imgerror(is, "PNG invalid CRC");
+# Stash some detail in raw
+	raw.r.min = Point(0, 0);
+	raw.transp = 0;
+	raw.chans = array[raw.nchans] of array of byte;
+	{
+		for (r:= 0; r < raw.nchans; r++)
+			raw.chans[r] = array[raw.r.max.x * raw.r.max.y] of byte;
+	}
+# Get the next chunk
+	seenPLTE := 0;
+	seenIDAT := 0;
+	seenLastIDAT := 0;
+	inflateFinished := 0;
+	seenIEND := 0;
+	seentRNS := 0;
+	rq: chan of ref Filter->Rq;
+
+	png.error = nil;
+	rq = nil;
+	while (png.error == nil) {
+		if (!png_chunk_header(is, chunk)) {
+			if (!seenIEND)
+				png.error = "duff header";
+			break;
+		}
+		if (seenIEND) {
+			png.error = "rubbish at eof";
+			break;
+		}
+		case (chunk.typ) {
+		"IEND" =>
+			seenIEND = 1;
+		"PLTE" =>
+			if (seenPLTE) {
+				png.error = "too many PLTEs";
+				break;
+			}
+			if (seentRNS) {
+				png.error = "tRNS before PLTE";
+				break;
+			}
+			if (seenIDAT) {
+				png.error = "PLTE too late";
+				break;
+			}
+			if (chunk.size % 3 || chunk.size < 1 * 3 || chunk.size > 256 * 3) {
+				png.error = "PLTE strange size";
+				break;
+			}
+			if (png.colortype == 0 || png.colortype == 4) {
+				png.error = "superfluous PLTE";
+				break;
+			}
+			raw.cmap = array[256 * 3] of byte;
+			png.PLTEsize = chunk.size / 3;
+			if (!png_bytes(is, chunk.crc_state, raw.cmap, chunk.size)) {
+				png.error = "eof in PLTE";
+				break;
+			}
+#			{
+#				x: int;
+#				sys->print("Palette:\n");
+#				for (x = 0; x < chunk.size; x += 3)
+#					sys->print("%3d: (%3d, %3d, %3d)\n",
+#						x / 3, int raw.cmap[x], int raw.cmap[x + 1], int raw.cmap[x + 2]);
+#			}
+			seenPLTE = 1;
+		"tRNS" =>
+			if (seenIDAT) {
+				png.error = "tRNS too late";
+				break;
+			}
+			case png.colortype {
+			0 =>
+				if (chunk.size != 2) {
+					png.error = "tRNS wrong size";
+					break;
+				}
+				level := png_ushort(is, chunk.crc_state);
+				if (level < 0) {
+					png.error = "eof in tRNS";
+					break;
+				}
+				if (png.depth != 16) {
+					raw.transp = 1;
+					raw.trindex = byte level;
+				}
+			2 =>
+				# a legitimate coding, but we can't use the information
+				if (!png_skip_bytes(is, chunk.crc_state, chunk.size))
+					png.error = "eof in skipped tRNS chunk";
+				break;
+			3 =>
+				if (!seenPLTE) {
+					png.error = "tRNS too early";
+					break;
+				}
+				if (chunk.size > png.PLTEsize) {
+					png.error = "tRNS too big";
+					break;
+				}
+				png.tRNS = array[png.PLTEsize] of byte;
+				for (x := chunk.size; x < png.PLTEsize; x++)
+					png.tRNS[x] = byte 255;
+				if (!png_bytes(is, chunk.crc_state, png.tRNS, chunk.size)) {
+					png.error = "eof in tRNS";
+					break;
+				}
+#				{
+#					sys->print("tRNS:\n");
+#					for (x = 0; x < chunk.size; x++)
+#						sys->print("%3d: (%3d)\n", x, int png.tRNS[x]);
+#				}
+				if (png.error == nil) {
+					# analyse the tRNS chunk to see if it contains a single transparent index
+					# translucent entries are treated as opaque
+					for (x = 0; x < chunk.size; x++)
+						if (png.tRNS[x] == byte 0) {
+							raw.trindex = byte x;
+							if (raw.transp) {
+								raw.transp = 0;
+								break;
+							}
+							raw.transp = 1;
+						}
+#					if (raw.transp)
+#						sys->print("selected index %d\n", int raw.trindex);
+				}
+			4 or 6 =>
+				png.error = "tRNS invalid when alpha present";
+			}
+			seentRNS = 1;
+		"IDAT" =>
+			if (seenLastIDAT) {
+				png.error = "non contiguous IDATs";
+				break;
+			}
+			if (inflateFinished) {
+				png.error = "too many IDATs";
+				break;
+			}
+			remaining := 0;
+			if (!seenIDAT) {
+				# open channel to inflate filter
+				if (!processdatainit(png, raw))
+					break;
+				rq = inflate->start(nil);
+				png_skip_bytes(is, chunk.crc_state, 2);
+				remaining = chunk.size - 2;
+			}
+			else
+				remaining = chunk.size;
+			while (remaining && png.error == nil) {
+				pick m := <- rq {
+				Fill =>
+#					sys->print("Fill(%d) remaining %d\n", len m.buf, remaining);
+					toget := len m.buf;
+					if (toget > remaining)
+						toget = remaining;
+					if (!png_bytes(is, chunk.crc_state, m.buf, toget)) {
+						m.reply <-= -1;
+						png.error = "eof during IDAT";
+						break;
+					}
+					m.reply <-= toget;
+					remaining -= toget;
+				Result =>
+#					sys->print("Result(%d)\n", len m.buf);
+					m.reply <-= 0;
+					processdata(png, raw, m.buf);
+				Info =>
+#					sys->print("Info(%s)\n", m.msg);
+				Finished =>
+					inflateFinished = 1;
+#					sys->print("Finished\n");
+				Error =>
+					imgerror(is, "PNG inflate error\n");
+				}
+			}
+			seenIDAT = 1;
+		* =>
+			# skip the blighter
+			if (!png_skip_bytes(is, chunk.crc_state, chunk.size))
+				png.error = "eof in skipped chunk";
+		}
+		if (png.error != nil)
+			break;
+		if (!png_crc_and_check(is, chunk))
+			imgerror(is, "PNG invalid CRC");
+		if (chunk.typ != "IDAT" && seenIDAT)
+			seenLastIDAT = 1;
+	}
+	# can only get here if IEND was last chunk, or png.error set
+	
+	if (png.error == nil && !seenIDAT) {
+		png.error = "no IDAT!";
+		inflateFinished = 1;
+	}
+	while (rq != nil && !inflateFinished) {
+		pick m := <-rq {
+		Fill =>
+#			sys->print("Fill(%d)\n", len m.buf);
+			png.error = "eof in zlib stream";
+			m.reply <-= -1;
+			inflateFinished = 1;
+		Result =>
+#			sys->print("Result(%d)\n", len m.buf);
+			if (png.error != nil) {
+				m.reply <-= -1;
+				inflateFinished = 1;
+			}
+			else {
+				m.reply <-= 0;
+				processdata(png, raw, m.buf);
+			}
+		Info =>
+#			sys->print("Info(%s)\n", m.msg);
+		Finished =>
+#			sys->print("Finished\n");
+			inflateFinished = 1;
+			break;
+		Error =>
+			png.error = "inflate error\n";
+			inflateFinished = 1;
+		}
+		
+	}
+	if (png.error == nil && !png.done)
+		png.error = "insufficient data";
+	if (png.error != nil)
+		imgerror(is, "PNG " + png.error);
+
+	width := raw.r.dx();
+	height := raw.r.dy();
+	case raw.chandesc {
+	CY =>
+		remapgrey(raw.chans[0], width, height);
+	CRGB =>
+		remaprgb(raw.chans, width, height, CRGB);
+	CRGB1 =>
+		remap1(raw.chans[0], width, height, raw.cmap);
+	}
+	pixels := raw.chans[0];
+	is.origw = width;
+	is.origh = height;
+	setdims(is);
+	if(is.width != is.origw || is.height != is.origh)
+		pixels = resample(pixels, is.origw, is.origh, is.width, is.height);
+	im := newimage(is, is.width, is.height);
+	im.writepixels(im.r, pixels);
+	mi := newmi(im);
+#	mi.mask = display.newimage(im.r, D->GREY1, 0, D->Black);
+	return mi;	
+}
+
+phase2stepping(phase: int): (int, int, int, int)
+{
+	case phase {
+	0 =>
+		return (0, 1, 0, 1);
+	1 =>
+		return (0, 8, 0, 8);
+	2 =>
+		return (0, 8, 4, 8);
+	3 =>
+		return (4, 8, 0, 4);
+	4 =>
+		return (0, 4, 2, 4);
+	5 =>
+		return (2, 4, 0, 2);
+	6 =>
+		return (0, 2, 1, 2);
+	7 =>
+		return (1, 2, 0, 1);
+	* =>
+		return (-1, -1, -1, -1);
+	}
+}
+
+processdatainitphase(png: ref Png, raw: ref Rawimage)
+{
+	(png.row, png.rowstep, png.colstart, png.colstep) = phase2stepping(png.phase);
+	if (raw.r.max.x > png.colstart)
+		png.phasecols = (raw.r.max.x - png.colstart + png.colstep - 1) / png.colstep;
+	else
+		png.phasecols = 0;
+	if (raw.r.max.y > png.row)
+		png.phaserows = (raw.r.max.y - png.row + png.rowstep - 1) / png.rowstep;
+	else
+		png.phaserows = 0;
+	png.rowsize = png.phasecols * (raw.nchans + png.alpha) * png.depth;
+	png.rowsize = (png.rowsize + 7) / 8;
+	png.rowsize++;		# for the filter byte
+	png.rowbytessofar = 0;
+	png.thisrow = array[png.rowsize] of byte;
+	png.lastrow = array[png.rowsize] of byte;
+#	sys->print("init phase %d: r (%d, %d, %d) c (%d, %d, %d) (%d)\n",
+#		png.phase, png.row, png.rowstep, png.phaserows,
+#		png.colstart, png.colstep, png.phasecols, png.rowsize);
+}
+
+processdatainit(png: ref Png, raw: ref Rawimage): int
+{
+	if (raw.nchans != 1&& raw.nchans != 3) {
+		png.error = "only 1 or 3 channels supported";
+		return 0;
+	}
+#	if (png.interlacemethod != 0) {
+#		png.error = "only progressive supported";
+#		return 0;
+#	}
+	if (png.colortype == 3 && raw.cmap == nil) {
+		png.error = "PLTE chunk missing";
+		return 0;
+	}
+	png.done = 0;
+	png.filterbpp = (png.depth * (raw.nchans + png.alpha) + 7) / 8;
+	png.phase = png.interlacemethod;
+
+	processdatainitphase(png, raw);
+
+	return 1;
+}
+
+upconvert(out: array of byte, outstride: int, in: array of byte, pixels: int, bpp: int)
+{
+	b: byte;
+	bits := pixels * bpp;
+	lim := bits / 8;
+	mask := byte ((1 << bpp) - 1);
+	outx := 0;
+	inx := 0;
+	for (x := 0; x < lim; x++) {
+		b = in[inx];
+		for (s := 8 - bpp; s >= 0; s -= bpp) {
+			pixel := (b >> s) & mask;
+			ucp := pixel;
+			for (y := bpp; y < 8; y += bpp)
+				ucp |= pixel << y;
+			out[outx] = ucp; 
+			outx += outstride;
+		}
+		inx++;
+	}
+	residue := (bits % 8) / bpp;
+	if (residue) {
+		b = in[inx];
+		for (s := 8 - bpp; s >= 0; s -= bpp) {
+			pixel := (b >> s) & mask;
+			ucp := pixel;
+			for (y := bpp; y < 8; y += bpp)
+				ucp |= pixel << y;
+			out[outx] = ucp; 
+			outx += outstride;
+			if (--residue <= 0)
+				break;
+		}
+	}
+}
+
+# expand (1 or 2 or 4) bit to 8 bit without scaling (for palletized stuff)
+
+expand(out: array of byte, outstride: int, in: array of byte, pixels: int, bpp: int)
+{
+	b: byte;
+	bits := pixels * bpp;
+	lim := bits / 8;
+	mask := byte ((1 << bpp) - 1);
+	outx := 0;
+	inx := 0;
+	for (x := 0; x < lim; x++) {
+		b = in[inx];
+		for (s := 8 - bpp; s >= 0; s -= bpp) {
+			out[outx] = (b >> s) & mask;
+			outx += outstride;
+		}
+		inx++;
+	}
+	residue := (bits % 8) / bpp;
+	if (residue) {
+		b = in[inx];
+		for (s := 8 - bpp; s >= 0; s -= bpp) {
+			out[outx] = (b >> s) & mask;
+			outx += outstride;
+			if (--residue <= 0)
+				break;
+		}
+	}
+}
+
+copybytes(out: array of byte, outstride: int, in: array of byte, instride: int, pixels: int)
+{
+	inx := 0;
+	outx := 0;
+	for (x := 0; x < pixels; x++) {
+		out[outx] = in[inx];
+		inx += instride;
+		outx += outstride;
+	}
+}
+
+outputrow(png: ref Png, raw: ref Rawimage, row: array of byte)
+{
+	offset := png.row * raw.r.max.x;
+	case raw.nchans {
+	1 =>
+		case (png.depth) {
+		* =>
+			png.error = "depth not supported";
+			return;
+		1 or 2 or 4 =>
+			if (raw.chandesc == CRGB1)
+				expand(raw.chans[0][offset + png.colstart:], png.colstep, row, png.phasecols, png.depth);
+			else
+				upconvert(raw.chans[0][offset + png.colstart:], png.colstep, row, png.phasecols, png.depth);
+		8 or 16 =>
+			# might have an Alpha channel to ignore!
+			stride := (png.alpha + 1) * png.depth / 8;
+			copybytes(raw.chans[0][offset + png.colstart:], png.colstep, row, stride, png.phasecols);
+		}
+	3 =>
+		case (png.depth) {
+		* =>
+			png.error = "depth not supported (2)";
+			return;
+		8 or 16 =>
+			# split rgb into three channels
+			bytespc := png.depth / 8;
+			stride := (3  + png.alpha) * bytespc;
+			copybytes(raw.chans[0][offset + png.colstart:], png.colstep, row, stride, png.phasecols);
+			copybytes(raw.chans[1][offset + png.colstart:], png.colstep, row[bytespc:], stride, png.phasecols);
+			copybytes(raw.chans[2][offset + png.colstart:], png.colstep, row[bytespc * 2:], stride, png.phasecols);
+		}
+	}
+}
+
+filtersub(png: ref Png)
+{
+	subx := 1;
+	for (x := int png.filterbpp + 1; x < png.rowsize; x++) {
+		png.thisrow[x] += png.thisrow[subx];
+		subx++;
+	}
+}
+
+filterup(png: ref Png)
+{
+	if (png.row == 0)
+		return;
+	for (x := 1; x < png.rowsize; x++)
+		png.thisrow[x] += png.lastrow[x];
+}
+
+filteraverage(png: ref Png)
+{
+	for (x := 1; x < png.rowsize; x++) {
+		a: int;
+		if (x > png.filterbpp)
+			a = int png.thisrow[x - png.filterbpp];
+		else
+			a = 0;
+		if (png.row != 0)
+			a += int png.lastrow[x];
+		png.thisrow[x] += byte (a / 2);
+	}
+}
+
+filterpaeth(png: ref Png)
+{
+	a, b, c: byte;
+	p, pa, pb, pc: int;
+	for (x := 1; x < png.rowsize; x++) {
+		if (x > png.filterbpp)
+			a = png.thisrow[x - png.filterbpp];
+		else
+			a = byte 0;
+		if (png.row == 0) {
+			b = byte 0;
+			c = byte 0;
+		} else {
+			b = png.lastrow[x];
+			if (x > png.filterbpp)
+				c = png.lastrow[x - png.filterbpp];
+			else
+				c = byte 0;
+		}
+		p = int a + int b - int c;
+		pa = p - int a;
+		if (pa < 0)
+			pa = -pa;
+		pb  = p - int b;
+		if (pb < 0)
+			pb = -pb;
+		pc = p - int c;
+		if (pc < 0)
+			pc = -pc;
+		if (pa <= pb && pa <= pc)
+			png.thisrow[x] += a;
+		else if (pb <= pc)
+			png.thisrow[x] += b;
+		else
+			png.thisrow[x] += c;
+	}		
+}
+
+phaseendcheck(png: ref Png, raw: ref Rawimage): int
+{
+	if (png.row >= raw.r.max.y || png.rowsize <= 1) {
+		# this phase is over
+		if (png.phase == 0) {
+			png.done = 1;
+		}
+		else {
+			png.phase++;
+			if (png.phase > 7)
+				png.done = 1;
+			else
+				processdatainitphase(png, raw);
+		}
+		return 1;
+	}
+	return 0;
+}
+
+processdata(png: ref Png, raw: ref Rawimage, buf: array of byte)
+{
+#sys->print("processdata(%d)\n", len buf);
+	if (png.error != nil)
+		return;
+	i := 0;
+	while (i < len buf) {
+		if (png.done) {
+			png.error = "too much data";
+			return;
+		}
+		if (phaseendcheck(png, raw))
+			continue;
+		tocopy := (png.rowsize - png.rowbytessofar);
+		if (tocopy > (len buf - i))
+			tocopy = len buf - i;
+		png.thisrow[png.rowbytessofar :] = buf[i : i + tocopy];
+		i += tocopy;
+		png.rowbytessofar += tocopy;
+		if (png.rowbytessofar >= png.rowsize) {
+			# a new row has arrived
+			# apply filter here
+#sys->print("phase %d row %d\n", png.phase, png.row);
+			case int png.thisrow[0] {
+			0 =>
+				;
+			1 =>
+				filtersub(png);
+			2 =>
+				filterup(png);
+			3 =>
+				filteraverage(png);
+			4 =>
+				filterpaeth(png);
+			* =>
+#				sys->print("implement filter method %d\n", int png.thisrow[0]);
+				png.error = "filter method unsupported";
+				return;
+			}
+			# output row
+			if (png.row >= raw.r.max.y) {
+				png.error = "too much data";
+				return;
+			}
+			outputrow(png, raw, png.thisrow[1 :]);
+			png.row += png.rowstep;
+			save := png.lastrow;
+			png.lastrow = png.thisrow;
+			png.thisrow = save;
+			png.rowbytessofar = 0;
+		}
+	}
+	phaseendcheck(png, raw);
+}
+
+png_signature(is: ref ImageSource): int
+{
+	sig := array[8] of { byte 137, byte 80, byte 78, byte 71, byte 13, byte 10, byte 26, byte 10 };
+	x: int;
+	for (x = 0; x < 8; x++)
+		if (png_getb(is) != int sig[x])
+			return 0;
+	return 1;
+}
+
+png_getb(is: ref ImageSource) : int
+{
+	if(is.i >= len is.bs.data)
+		return -1;
+	return int is.bs.data[is.i++];
+}
+
+png_bytes(is: ref ImageSource, crc_state: ref CRCstate, buf: array of byte, n: int): int
+{
+	if (is.i +n > len is.bs.data) {
+		is.i = len is.bs.data;
+		return 0;
+	}
+	if (buf == nil) {
+		is.i += n;
+		return 1;
+	}
+	buf[0:] = is.bs.data[is.i:is.i+n];
+	is.i += n;
+	if (crc_state != nil)
+		crc->crc(crc_state, buf, n);
+	return 1;
+}
+
+png_skip_bytes(is: ref ImageSource, crc_state: ref CRCstate, n: int): int
+{
+	buf := array[1024] of byte;
+	while (n) {
+		thistime: int = 1024;
+		if (thistime > n)
+			thistime = n;
+		if (!png_bytes(is, crc_state, buf, thistime))
+			return 0;
+		n -= thistime;
+	}
+	return 1;
+}
+
+png_get_4(is: ref ImageSource, crc_state: ref CRCstate, signed: int): (int, int)
+{
+	buf := array[4] of byte;
+	if (!png_bytes(is, crc_state, buf, 4))
+		return (0, 0);
+	if (signed && int buf[0] & 16r80)
+		return (0, 0);
+	r:int  = (int buf[0] << 24) | (int buf[1] << 16) | (int buf[2] << 8) | (int buf[3]);
+#	sys->print("got int %d\n", r);
+	return (1, r);
+}
+
+png_int(is: ref ImageSource, crc_state: ref CRCstate): int
+{
+	ok, r: int;
+	(ok, r) = png_get_4(is, crc_state, 1);
+	if (ok)
+		return r;
+	return -1;
+}
+
+png_ushort(is: ref ImageSource, crc_state: ref CRCstate): int
+{
+	buf := array[2] of byte;
+	if (!png_bytes(is, crc_state, buf, 2))
+		return -1;
+	return (int buf[0] << 8) | int buf[1];
+}
+
+png_crc_and_check(is: ref ImageSource, chunk: ref Chunk): int
+{
+	crc, ok: int;
+	(ok, crc) = png_get_4(is, nil, 0);
+	if (!ok)
+		return 0;
+#	sys->print("crc: computed %.8ux expected %.8ux\n", chunk.crc_state.crc, crc);
+	if (chunk.crc_state.crc != crc)
+		return 1;
+	return 1;
+}
+
+png_byte(is: ref ImageSource, crc_state: ref CRCstate): int
+{
+	buf := array[1] of byte;
+	if (!png_bytes(is, crc_state, buf, 1))
+		return -1;
+#	sys->print("got byte %d\n", int buf[0]);
+	return int buf[0];
+}
+
+png_type(is: ref ImageSource, crc_state: ref CRCstate): string
+{
+	x: int;
+	buf := array[4] of byte;
+	if (!png_bytes(is, crc_state, buf, 4))
+		return nil;
+	for (x = 0; x < 4; x++) {
+		c: int;
+		c = int buf[x];
+		if ((c < 65 || c > 90 && c < 97) || c > 122)
+			return nil;
+	}
+	return string buf;
+}
+
+png_chunk_header(is: ref ImageSource, chunk: ref Chunk): int
+{
+	chunk.size = png_int(is, nil);
+	if (chunk.size < 0)
+		return 0;
+	crc->reset(chunk.crc_state);
+	chunk.typ = png_type(is, chunk.crc_state);
+	if (chunk.typ == nil)
+		return 0;
+#	sys->print("%s(%d)\n", chunk.typ, chunk.size);
+	return 1;
+}
--- /dev/null
+++ b/appl/charon/img.m
@@ -1,0 +1,115 @@
+Img: module {
+	PATH : con "/dis/charon/img.dis";
+
+	Mimerror, Mimnone, Mimpartial, Mimdone: con iota + 1;
+
+	# Getmim returns image and possible mask;
+	# the int returned is either Mimnone, Mimpartial or Mimdone, depending on
+	# how much of returned mim is filled in.
+	# if the image is animated, successive calls to getmim return subsequent frames.
+	# Errors are indicated by returning Mimerror, with the err field non empty.
+	# Should call free() when don't intend to call getmim any more
+	ImageSource: adt
+	{
+		width:	int;
+		height:	int;
+		origw:	int;
+		origh:	int;
+		mtype:	int;
+		i:		int;
+		curframe:	int;
+		bs:		ref CharonUtils->ByteSource;
+		gstate:	ref Gifstate;
+		jstate:	ref Jpegstate;
+		err:		string;
+
+		new: fn(bs: ref CharonUtils->ByteSource, w, h: int) : ref ImageSource;
+		getmim: fn(is: self ref ImageSource) : (int, ref CharonUtils->MaskedImage);
+		free: fn(is: self ref ImageSource);
+	};
+
+	# Following are private to implementation
+	Jpegstate: adt
+	{
+		# variables in i/o routines
+		sr:	int;	# shift register, right aligned
+		cnt:	int;	# # bits in right part of sr
+	
+		Nf:		int;
+		comp:	array of Framecomp;
+		mode:	byte;
+		X,Y:		int;
+		qt:		array of array of int;	# quantization tables
+		dcht:		array of ref Huffman;
+		acht:		array of ref Huffman;
+		Ns:		int;
+		scomp:	array of Scancomp;
+		Ss:		int;
+		Se:		int;
+		Ah:		int;
+		Al:		int;
+		ri:		int;
+		nseg:	int;
+		nblock:	array of int;
+	
+		# progressive scan
+		dccoeff:	array of array of int;
+		accoeff:	array of array of array of int;	# only need 8 bits plus quantization
+		nacross:	int;
+		ndown:	int;
+		Hmax:	int;
+		Vmax:	int;
+	};
+
+	Huffman: adt
+	{
+		bits:	array of int;
+		size:	array of int;
+		code:	array of int;
+		val:	array of int;
+		mincode:	array of int;
+		maxcode:	array of int;
+		valptr:	array of int;
+		# fast lookup
+		value:	array of int;
+		shift:	array of int;
+	};
+	
+	Framecomp: adt	# Frame component specifier from SOF marker
+	{
+		C:	int;
+		H:	int;
+		V:	int;
+		Tq:	int;
+	};
+
+	Scancomp: adt	# Frame component specifier from SOF marker
+	{
+		C:	int;
+		tdc:	int;
+		tac:	int;
+	};
+
+	Gifstate: adt
+	{
+		fields: int;
+		bgrnd: int;
+		aspect: int;
+		flags: int;
+		delay: int;
+		trindex: byte;
+		tbl: array of GifEntry;
+		globalcmap: array of byte;
+		cmap: array of byte;
+	};
+
+	GifEntry: adt
+	{
+		prefix: int;
+		exten: int;
+	};
+
+	init: fn(cu: CharonUtils);
+	supported: fn(mtype: int) : int;
+	closest_rgbpix: fn(r, g, b: int) : int;
+};
--- /dev/null
+++ b/appl/charon/jscript.b
@@ -1,0 +1,3025 @@
+implement JScript;
+
+include "common.m";
+include "ecmascript.m";
+
+ES: Ecmascript;
+	Exec, Obj, Call, Prop, Val, Ref, RefVal, Builtin, ReadOnly: import ES;
+me: ESHostobj;
+
+# local copies from CU
+sys: Sys;
+CU: CharonUtils;
+
+D: Draw;
+S: String;
+T: StringIntTab;
+C: Ctype;
+B: Build;
+	Item: import B;
+CH: Charon;
+L: Layout;
+	Frame, Control: import L;
+U: Url;
+	Parsedurl: import U;
+E: Events;
+	Event, ScriptEvent: import E;
+
+G : Gui;
+
+JScript: module
+{
+	# First, conform to Script interface
+	defaultStatus: string;
+	jevchan: chan of ref ScriptEvent;
+	versions: array of string;
+
+	init: fn(cu: CharonUtils): string;
+	frametreechanged: fn(top: ref Layout->Frame);
+	havenewdoc: fn(f: ref Layout->Frame);
+	evalscript: fn(f: ref Layout->Frame, s: string) : (string, string, string);
+	framedone: fn(f : ref Layout->Frame, hasscripts : int);
+
+	#
+	# implement the host object interface, too
+	#
+	get:		fn(ex: ref Exec, o: ref Obj, property: string): ref Val;
+	put:		fn(ex: ref Exec, o: ref Obj, property: string, val: ref Val);
+	canput:	fn(ex: ref Exec, o: ref Obj, property: string): ref Val;
+	hasproperty:	fn(ex: ref Exec, o: ref Obj, property: string): ref Val;
+	delete:		fn(ex: ref Exec, o: ref Obj, property: string);
+	defaultval:	fn(ex: ref Exec, o: ref Obj, tyhint: int): ref Val;
+	call:		fn(ex: ref Exec, func, this: ref Obj, args: array of ref Val, eval: int): ref Ref;
+	construct:	fn(ex: ref Exec, func: ref Obj, args: array of ref Val): ref Obj;
+};
+
+versions = array [] of {
+	"javascript",
+	"javascript1.0",
+	"javascript1.1",
+	"javascript1.2",
+	"javascript1.3",
+};
+
+# Call init() before calling anything else.
+# It makes a global object (a Window) for the browser's top level frame,
+# and also puts a navaigator object in it.  The document can't be filled
+# in until the first document gets loaded.
+#
+# This module keeps track of the correspondence between the Script Window
+# objects and the corresponding Layout Frames, using the ScriptWin adt to
+# build a tree mirroring the structure.  The root of the tree never changes
+# after first being set (but changing its document essentially resets all of the
+# other data structures).  After charon has built its top-level window, it
+# should call frametreechanged(top).
+#
+# When a frame gets reset or gets some frame children added, call frametreechanged(f),
+# where f is the changed frame.  This module will update its ScriptWin tree as needed.
+#
+# Whenever the document in a (Layout) Frame f changes, call havenewdoc(f)
+# after the frame's doc field is set. This causes this module to initialize the document
+# object in the corresponding window object.
+#
+# From within the build process, call evalscript(containing frame, script) to evaluate
+# global code fragments as needed.  The return value is two strings: a possible error
+# description, and HTML that is the result of a document.write (so it should be spliced
+# in at the point where the <SCRIPT> element occurred).  evalscript() also handles
+# the job of synching up the Docinfo data (on the Build side) with the document object
+# (on the Script side).
+#
+# For use by other external routines, the xfertoscriptobjs() and xferfromscriptobjs()
+# functions that do the just-described synch-up are available for external callers.
+
+# Adt for keeping track of correspondence between Image objects
+# and their corresponding Build items.
+ScriptImg: adt
+{
+	item: ref Build->Item.Iimage;
+	obj: ref Obj;
+};
+
+ScriptForm : adt {
+	form : ref Build->Form;
+	obj : ref Obj;
+	ix : int;		# index in document.forms array
+	fields : list of (ref Build->Formfield, ref Obj);
+};
+
+# Adt for keeping track of correspondence between Window
+# objects and their corresponding Frames.
+
+ScriptWin: adt
+{
+	frame: ref Layout->Frame;
+	ex: ref Exec;		# ex.global is frame's Window obj
+	locobj: ref Obj;		# Location object for window
+	val : ref Val;		# val of ex.global - used to side-effect entry in parent.frames[]
+
+	parent: ref ScriptWin;
+	forms: list of ref ScriptForm;	# no guaranteed order
+	kids: cyclic list of ref ScriptWin;
+	imgs: list of ref ScriptImg;
+	newloc: string;		# url to go to after script finishes executing
+	newloctarg: string;	# target frame for newloc
+	docwriteout: string;
+	inbuild: int;
+	active: int;		# frame or sub-frame has scripts
+	error: int;
+	imgrelocs: list of ref Obj;
+
+	new: fn(f: ref Layout->Frame, ex: ref Exec, loc: ref Obj, par: ref ScriptWin) : ref ScriptWin;
+	addkid: fn(sw: self ref ScriptWin, f: ref Layout->Frame);
+	dummy: fn(): ref ScriptWin;
+#	findbyframe: fn(sw: self ref ScriptWin, f: ref Layout->Frame) : ref ScriptWin;
+	findbyframeid: fn(sw: self ref ScriptWin, fid: int) : ref ScriptWin;
+	findbydoc: fn(sw: self ref ScriptWin, d: ref Build->Docinfo) : ref ScriptWin;
+	findbyobj: fn(sw : self ref ScriptWin, obj : ref Obj) : ref ScriptWin;
+	findbyname: fn(sw : self ref ScriptWin, name : string) : ref ScriptWin;
+};
+
+opener: ref ScriptWin;
+winclose: int;
+
+# Helper adts for initializing objects.
+# Methods go in prototype, properties go in objects
+
+MethSpec: adt
+{
+	name: string;
+	args: array of string;
+};
+
+
+IVundef, IVnull, IVtrue, IVfalse, IVnullstr, IVzero, IVzerostr, IVarray: con iota;
+
+PropSpec: adt
+{
+	name: string;
+	attr: int;
+	initval: int;	# one of IVnull, etc.
+};
+
+ObjSpec: adt
+{
+	name: string;
+	methods: array of MethSpec;
+	props: array of PropSpec;
+};
+
+MimeSpec: adt
+{
+	description: string;
+	suffixes: string;
+	ty: string;
+};
+
+# Javascript 1.1 (Netscape 3) client objects
+
+objspecs := array[] of {
+    ObjSpec("Anchor", 
+	nil,
+	array[] of {PropSpec
+		("name", ReadOnly, IVnullstr) }
+	),
+    ObjSpec("Applet",
+	nil,
+	nil
+	),
+    ObjSpec("document",
+	array[] of {MethSpec
+		("close", nil),
+		("open", array[] of { "mimetype", "replace" }),
+		("write", array[] of { "string" }),
+		("writeln", array[] of { "string" }) },
+	array[] of {PropSpec
+		("alinkColor", 0, IVnullstr),
+		("anchors", ReadOnly, IVarray),
+		("applets", ReadOnly, IVarray),
+		("bgColor", 0, IVnullstr),
+		("cookie", 0, IVnullstr),
+		("domain", 0, IVnullstr),
+		("embeds", ReadOnly, IVarray),
+		("fgColor", 0, IVnullstr),
+		("forms", ReadOnly, IVarray),
+		("images", ReadOnly, IVarray),
+		("lastModified", ReadOnly, IVnullstr),
+		("linkColor", 0, IVnullstr),
+		("links", ReadOnly, IVarray),
+		("location", 0, IVnullstr),
+		("plugins", ReadOnly, IVarray),
+		("referrer", ReadOnly, IVnullstr),
+		("title", ReadOnly, IVnullstr),
+		("URL", ReadOnly, IVnullstr),
+		("vlinkColor", 0, IVnullstr) }
+	),
+    ObjSpec("Form",
+	array[] of {MethSpec
+		("reset", nil),
+		("submit", nil) },
+	array[] of {PropSpec
+		("action", 0, IVnullstr),
+		("elements", ReadOnly, IVarray),
+		("encoding", 0, IVnullstr),
+		("length", ReadOnly, IVzero),
+		("method", 0, IVnullstr),
+		("name", 0, IVnullstr),
+		("target", 0, IVnullstr) }
+	),
+   # This is merge of Netscape objects (to save code & data space):
+   # Button, Checkbox, Hidden, Radio, Reset, Select, Text, and Textarea
+    ObjSpec("FormField",
+	array[] of {MethSpec
+		("blur", nil),
+		("click", nil),
+		("focus", nil),
+		("select", nil) },
+	array[] of {PropSpec
+		("checked", 0, IVundef),
+		("defaultChecked", 0, IVundef),
+		("defaultValue", 0, IVundef),
+		("form", ReadOnly, IVundef),
+		("length", 0, IVundef),
+		("name", 0, IVnullstr),
+		("options", 0, IVundef),
+		("type", ReadOnly, IVundef),
+		("selectedIndex", 0, IVundef),
+		("value", 0, IVnullstr) }
+	),
+    ObjSpec("History",
+	array[] of {MethSpec
+		("back", nil),
+		("forward", nil),
+		("go", array[] of { "location-or-delta" }) },
+	array[] of {PropSpec
+		("current", ReadOnly, IVnullstr),
+		("length", ReadOnly, IVzero),
+		("next", ReadOnly, IVnullstr),
+		("previous", ReadOnly, IVnullstr) }
+	),
+    ObjSpec("Image",
+	nil,
+	array[] of {PropSpec
+		("border", ReadOnly, IVzerostr),
+		("complete", ReadOnly, IVfalse),
+		("height", ReadOnly, IVzerostr),
+		("hspace", ReadOnly, IVzerostr),
+		("lowsrc", 0, IVnullstr),
+		("name", ReadOnly, IVnullstr),
+		("src", 0, IVnullstr),
+		("vspace", ReadOnly, IVzerostr),
+		("width", ReadOnly, IVzerostr) }
+	),
+    ObjSpec("Link",
+	nil,
+	array[] of {PropSpec
+		("hash", 0, IVnullstr),
+		("host", 0, IVnullstr),
+		("hostname", 0, IVnullstr),
+		("href", 0, IVnullstr),
+		("pathname", 0, IVnullstr),
+		("port", 0, IVnullstr),
+		("protocol", 0, IVnullstr),
+		("search", 0, IVnullstr),
+		("target", 0, IVnullstr) }
+	),
+    ObjSpec("Location",
+	array[] of {MethSpec
+		("reload", array[] of { "forceGet" }),
+		("replace", array[] of { "URL" }) },
+	array[] of {PropSpec
+		("hash", 0, IVnullstr),
+		("host", 0, IVnullstr),
+		("hostname", 0, IVnullstr),
+		("href", 0, IVnullstr),
+		("pathname", 0, IVnullstr),
+		("port", 0, IVnullstr),
+		("protocol", 0, IVnullstr),
+		("search", 0, IVnullstr) }
+	),
+    ObjSpec("MimeType",
+	nil,
+	array[] of {PropSpec
+		("description", ReadOnly, IVnullstr),
+		("enabledPlugin", ReadOnly, IVnull),
+		("suffixes", ReadOnly, IVnullstr),
+		("type", ReadOnly, IVnullstr) }
+	),
+    ObjSpec("Option",
+	nil,
+	array[] of {PropSpec
+		("defaultSelected", 0, IVfalse),
+		("index", 0, IVundef),
+		("selected", 0, IVfalse),
+		("text", 0, IVnullstr),
+		("value", 0, IVnullstr) }
+	),
+    ObjSpec("navigator",
+	array[] of {MethSpec
+		("javaEnabled", nil),
+		("plugins.refresh", nil),
+		("taintEnabled", nil) },
+	array[] of {PropSpec
+		("appCodeName", ReadOnly, IVnullstr),
+		("appName", ReadOnly, IVnullstr),
+		("appVersion", ReadOnly, IVnullstr),
+		("mimeTypes", ReadOnly, IVarray),
+		("platform", ReadOnly, IVnullstr),
+		("plugins", ReadOnly, IVarray),
+		("userAgent", ReadOnly, IVnullstr) }
+	),
+    ObjSpec("Plugin",
+	nil,
+	array[] of {PropSpec
+		("description", 0, IVnullstr),
+		("filename", 0, IVnullstr),
+		("length", 0, IVzero),
+		("name", 0, IVnullstr) }
+	),
+     ObjSpec("Screen",
+	nil,
+	array[] of {PropSpec
+		("availHeight", ReadOnly, IVzero),
+		("availWidth", ReadOnly, IVzero),
+		("availLeft", ReadOnly, IVzero),
+		("availTop", ReadOnly, IVzero),
+		("colorDepth", ReadOnly, IVzero),
+		("pixelDepth", ReadOnly, IVzero),
+		("height", ReadOnly, IVzero),
+		("width", ReadOnly, IVzero) }
+	),
+     ObjSpec("Window",
+	array[] of {MethSpec
+		("alert", array[] of { "msg" }),
+		("blur", nil),
+		("clearInterval", array[] of { "intervalid" }),
+		("clearTimeout", array[] of { "timeoutid" }),
+		("close", nil),
+		("confirm", array[] of  { "msg" }),
+		("focus", nil),
+		("moveBy", array[] of { "dx", "dy" }),
+		("moveTo", array[] of { "x", "y" }),
+		("open", array[] of { "url", "winname", "winfeatures" }),
+		("prompt", array[] of { "msg", "inputdflt" }),
+		("resizeBy", array[] of { "dh", "dw" }),
+		("resizeTo", array[] of { "width", "height" }),
+		("scroll", array[] of { "x", "y"  }),
+		("scrollBy", array[] of { "dx", "dy" }),
+		("scrollTo", array[] of { "x", "y" }),
+		("setInterval", array[] of { "code", "msec" }),
+		("setTimeout", array[] of { "expr", "msec" }) },
+	array[] of {PropSpec
+		("closed", ReadOnly, IVfalse),
+		("defaultStatus", 0, IVnullstr),
+		("document", 0, IVnull),
+		("frames", ReadOnly, IVnull),	# array, really
+		("history", 0, IVnull),	# array, really
+		("length", ReadOnly, IVzero),
+		("location", 0, IVnullstr),
+#		("Math", ReadOnly, IVnull),
+		("name", 0, IVnullstr),
+		("navigator", ReadOnly, IVnull),
+		("offscreenBuffering", 0, IVnullstr),
+		("opener", 0, IVnull),
+		("parent", ReadOnly, IVnull),
+		("screen", 0, IVnull),
+		("self", ReadOnly, IVnull),
+		("status", 0, IVnullstr),
+		("top", ReadOnly, IVnull),
+		("window", ReadOnly, IVnull) }
+	)
+};
+
+# Currently supported charon mime types
+mimespecs := array[] of {
+    MimeSpec("HTML", 
+	"htm,html",
+	"text/html"
+	),
+    MimeSpec("Plain text", 
+	"txt,text",
+	"text/plain"
+	),
+   MimeSpec("Gif Image", 
+	"gif",
+	"image/gif"
+	),
+    MimeSpec("Jpeg Image", 
+	"jpeg,jpg,jpe",
+	"image/jpeg"
+	),
+    MimeSpec("X Bitmap Image", 
+	"",
+	"image/x-xbitmap"
+	)
+};
+
+# charon's 's' debug flag:
+#	1:	basic syntax and runtime errors
+#	2:	'event' logging and DOM actions
+#	3:	print parsed code and ops as executed
+#	4:	print value of expression statements and abort on runtime errors
+dbg := 0;
+dbgdom := 0;
+
+top: ref ScriptWin;
+createdimages : list of ref Obj;
+nullstrval: ref Val;
+zeroval: ref Val;
+zerostrval: ref Val;
+
+# Call this after charon's main (top) frame has been built
+init(cu: CharonUtils) : string
+{
+	CU = cu;
+	sys = load Sys Sys->PATH;
+	D = load Draw Draw->PATH;
+	S = load String String->PATH;
+	T = load StringIntTab StringIntTab->PATH;
+	U = load Url Url->PATH;
+	if (U != nil)
+		U->init();
+	C = cu->C;
+	B = cu->B;
+	L = cu->L;
+	E = cu->E;
+	CH = cu->CH;
+	G = cu->G;
+	dbg = int (CU->config).dbg['s'];
+	if (dbg > 1)
+		dbgdom = 1;
+	ES = load Ecmascript Ecmascript->PATH;
+	if(ES == nil)
+		return sys->sprint("could not load module %s: %r", Ecmascript->PATH);
+	err := ES->init();
+	if (err != nil) 
+		return sys->sprint("ecmascript error: %s", err);
+
+	me = load ESHostobj SELF;
+	if(me == nil)
+		return sys->sprint("jscript: could not load  self as a ESHostobj: %r");
+	if(dbg >= 3) {
+		ES->debug['p'] = 1;	# print parsed code
+		ES->debug['e'] = 1;	# prinv ops as they are executed
+		if(dbg >= 4) {
+			ES->debug['e'] = 2;
+			ES->debug['v'] = 1;	# print value of expression statements
+			ES->debug['r'] = 1;	# print and abort if runtime errors
+		}
+	}
+	
+	# some constant values, for initialization
+	nullstrval = ES->strval("");
+	zeroval = ES->numval(0.);
+	zerostrval = ES->strval("0");
+	jevchan = chan of ref ScriptEvent;
+	spawn jevhandler();
+	return nil;
+}
+
+doneevent := ScriptEvent(-1,0,0,0,0,0,0,0,0,nil,nil,0);
+
+# Used to receive and act upon ScriptEvents from main charon thread.
+# Want to queue the events up, so that the main thread doesn't have
+# to wait, and spawn off a do_on, one at a time, so that they don't
+# interfere with each other.
+# When do_on is finished, it must send a copy of doneevent
+# so that jevhandler knows it can spawn another do_on.
+jevhandler()
+{
+	q := array[10] of ref ScriptEvent;
+	qhead := 0;
+	qtail := 0;
+	spawnok := 1;
+	for(;;) {
+		jev := <- jevchan;
+		if(jev.kind == -1)
+			spawnok = 1;
+		else
+			q[qtail++] = jev;
+		jev = nil;
+
+		# remove next event to process, if ok to and there is one
+		if(spawnok && qhead < qtail)
+			jev = q[qhead++];
+
+		# adjust queue to make sure there is room for next event
+		if(qhead == qtail) {
+			qhead = 0;
+			qtail = 0;
+		}
+		if(qtail == len q) {
+			if(qhead > 0) {
+				q[0:] = q[qhead:qtail];
+				qtail -= qhead;
+				qhead = 0;
+			}
+			else {
+				newq := array[len q + 10] of ref ScriptEvent;
+				newq[0:] = q;
+				q = newq;
+			}
+		}
+
+		# process next event, if any
+		if(jev != nil) {
+			spawnok = 0;
+			spawn do_on(jev);
+		}
+	}
+}
+
+# Create an execution context for the frame.
+# The global object of the frame is the frame's Window object.
+# Return the execution context and the Location object for the window.
+makeframeex(f : ref Layout->Frame) : (ref Exec, ref Obj)
+{
+	winobj := mkhostobj(nil, "Window");
+	ex := ES->mkexec(winobj);
+	winobj.prototype = ex.objproto;
+	winobj.prototype = mkprototype(ex, specindex("Window"));
+
+	navobj := mknavobj(ex);
+	reinitprop(winobj, "navigator", ES->objval(navobj));
+
+	histobj := mkhostobj(ex, "History");
+	(length, current, next, previous) := CH->histinfo();
+	reinitprop(histobj, "current", ES->strval(current));
+	reinitprop(histobj, "length", ES->numval(real length));
+	reinitprop(histobj, "next", ES->strval(next));
+	reinitprop(histobj, "previous", ES->strval(previous));
+	ES->put(ex, winobj, "history", ES->objval(histobj));
+
+	locobj := mkhostobj(ex, "Location");
+	src : ref U->Parsedurl;
+	di := f.doc;
+	if (di != nil && di.src != nil) {
+		src = di.src;
+		reinitprop(locobj, "hash", ES->strval("#" + src.frag));
+		reinitprop(locobj, "host", ES->strval(src.host + ":" + src.port));
+		reinitprop(locobj, "hostname", ES->strval(src.host));
+		reinitprop(locobj, "href", ES->strval(src.tostring()));
+		reinitprop(locobj, "pathname", ES->strval(src.path));
+		reinitprop(locobj, "port", ES->strval(src.port));
+		reinitprop(locobj, "protocol", ES->strval(src.scheme + ":"));
+		reinitprop(locobj, "search", ES->strval("?" + src.query));
+	}
+	ES->put(ex, winobj, "location", ES->objval(locobj));
+
+	scrobj := mkhostobj(ex, "Screen");
+	scr := (CU->G->display).image;
+	scrw := D->(scr.r.dx)();
+	scrh := D->(scr.r.dy)();
+	reinitprop(scrobj, "availHeight", ES->numval(real scrh));
+	reinitprop(scrobj, "availWidth", ES->numval(real scrw));
+	reinitprop(scrobj, "availLeft", ES->numval(real scr.r.min.x));
+	reinitprop(scrobj, "availTop", ES->numval(real scr.r.min.y));
+	reinitprop(scrobj, "colorDepth", ES->numval(real scr.depth));
+	reinitprop(scrobj, "pixelDepth", ES->numval(real scr.depth));
+	reinitprop(scrobj, "height", ES->numval(real scrh));
+	reinitprop(scrobj, "width", ES->numval(real scrw));
+	ES->put(ex, winobj, "screen", ES->objval(scrobj));
+
+	# make the non-core constructor objects
+#	improto := mkprototype(ex, specindex("Image"));
+	o := ES->biinst(winobj, Builtin("Image", "Image", array[] of {"width", "height"}, 2),
+			ex.funcproto, me);
+	o.construct = o.call;
+
+	o = ES->biinst(winobj, Builtin("Option", "Option", array[] of {"text", "value", "defaultSelected", "selected"}, 4),
+			ex.funcproto, me);
+	o.construct = o.call;
+	defaultStatus = "";
+	return (ex, locobj);
+}
+
+mknavobj(ex: ref Exec) : ref Obj
+{
+	navobj := mkhostobj(ex, "navigator");
+	reinitprop(navobj, "appCodeName", ES->strval("Mozilla"));
+	reinitprop(navobj, "appName", ES->strval("Netscape"));
+#	reinitprop(navobj, "appVersion", ES->strval("3.0 (Inferno, U)"));
+#	reinitprop(navobj, "userAgent", ES->strval("Mozilla/3.0 (Inferno; U)"));
+	reinitprop(navobj, "appVersion", ES->strval("4.08 (Charon; Inferno)"));
+	reinitprop(navobj, "userAgent", ES->strval("Mozilla/4.08 (Charon; Inferno)"));
+
+	omty := getobj(ex, navobj, "mimeTypes");
+	for(i := 0; i < len mimespecs; i++) {
+		sp := mimespecs[i];
+		v := mkhostobj(ex, "MimeType");
+		reinitprop(v, "description", ES->strval(sp.description));
+		reinitprop(v, "suffixes", ES->strval(sp.suffixes));
+		reinitprop(v, "type", ES->strval(sp.ty));
+		arrayput(ex, omty, i, sp.ty, ES->objval(v));
+	}
+	return navobj;
+}
+
+# Something changed in charon's frame tree
+frametreechanged(t: ref Layout->Frame)
+{
+	rebuild : ref ScriptWin;
+	if (top == nil) {
+		(ex, loc) := makeframeex(t);
+		top = ScriptWin.new(t, ex, loc, nil);
+		rebuild = top;
+	} else {
+		rebuild = top.findbyframeid(t.id);
+		# t could be new frame - need to look for parent
+		while (rebuild == nil && t.parent != nil) {
+			t = t.parent;
+			rebuild = top.findbyframeid(t.id);
+		}
+		# if we haven't found it by now, it's not in the official Frame
+		# hierarchy, so ignore it
+	}
+	if (rebuild != nil)
+		wininstant(rebuild);
+}
+
+# Frame f has just been reset, then given a new doc field
+# (with initial values for src, base, refresh, chset).
+# We'll defer doing any actual building of the script objects
+# until an evalscript; that way, pages that don't use scripts
+# incur minimum penalties).
+havenewdoc(f: ref Layout->Frame)
+{
+	sw := top.findbyframeid(f.id);
+	if(sw != nil) {
+		sw.inbuild = 1;
+		sw.forms = nil;
+		(sw.ex, sw.locobj) = makeframeex(f);
+		if (sw.val != nil)
+			# global object is referenced via parent.frames array
+			sw.val.obj = sw.ex.global;
+		wininstant(sw);
+	}
+}
+
+# returns (error, output, value)
+# error: error message
+# output: result of any document.writes
+# value: value of last statement executed (used for handling "javascript:" URL scheme)
+#
+evalscript(f: ref Layout->Frame, s: string) : (string, string, string)
+{
+	if (top.error)
+		return("scripts disabled for this document", "", "");
+	sw := top.findbyframeid(f.id);
+	if (sw == nil)
+		return("cannot find script window", "", "");
+	if (sw.ex == nil)
+		return("script window has no execution context", "", "");
+	if(sw == nil || sw.ex == nil)
+		return ("", "", "");
+
+	ex := sw.ex;
+	sw.docwriteout = "";
+	expval := "";
+	createdimages = nil;
+	{
+		xfertoscriptobjs(f, 1);
+		if(s != "") {
+			ex.error = nil;
+			c := ES->eval(ex, s);
+			if (c.kind == ES->CThrow && dbg) {
+				sys->print("unhandled error:\n\tvalue:%s\n\treason:%s\n",
+					ES->toString(ex, c.val), ex.error);
+				sys->print("%s\n", s);
+			}
+			if (c.kind == ES->CNormal && c.val != nil) {
+				if (ES->isstr(c.val))
+					expval = c.val.str;
+			}
+			xferfromscriptobjs(f, 1);
+			checknewlocs(top);
+			checkopener();
+		}
+		w := sw.docwriteout;
+		sw.docwriteout = nil;
+		return("", w, expval);
+	}exception exc{
+	"*" =>
+		if(dbg) {
+			sys->print("fatal error %q executing evalscript: %s\nscript=", exc, ex.error);
+			sa := array of byte s;
+			sys->write(sys->fildes(1), sa, len sa);
+			sys->print("\n");
+		}
+		top.error = 1;
+		emsg := "Fatal error processing script\n\nScript processing suspended for this page";
+		G->alert(emsg);
+		w := sw.docwriteout;
+		sw.docwriteout = nil;
+		return (ex.error, w, "");
+	}
+}
+
+xfertoscriptobjs(f: ref Layout->Frame, inbuild: int)
+{
+	sw := top.findbyframeid(f.id);
+	if(sw == nil)
+		return;
+	ex := sw.ex;
+	ow := ex.global;
+	di := f.doc;
+
+	for(el := di.events; el != nil; el = tl el) {
+		e := hd el;
+		hname := "";
+		dhname := "";
+		case e.attid {
+		Lex->Aonblur =>
+			hname = "onblur";
+			di.evmask |= E->SEonblur;
+		Lex->Aonerror =>
+			hname = "onerror";
+			di.evmask |= E->SEonerror;
+		Lex->Aonfocus =>
+			hname = "onfocus";
+			di.evmask |= E->SEonfocus;
+		Lex->Aonload =>
+			hname = "onload";
+			di.evmask |= E->SEonload;
+		Lex->Aonresize =>
+			hname = "onresize";
+			di.evmask |= E->SEonresize;
+		Lex->Aonunload =>
+			hname = "onunload";
+			di.evmask |= E->SEonunload;
+		Lex->Aondblclick =>
+			dhname = "ondblclick";
+			di.evmask |= E->SEondblclick;
+		Lex->Aonkeydown =>
+			dhname = "onkeydown";
+			di.evmask |= E->SEonkeydown;
+		Lex->Aonkeypress =>
+			dhname = "onkeypress";
+			di.evmask |= E->SEonkeypress;
+		Lex->Aonkeyup =>
+			dhname = "onkeyup";
+			di.evmask |= E->SEonkeyup;
+		Lex->Aonmousedown =>
+			dhname = "onmousedown";
+			di.evmask |= E->SEonmousedown;
+		Lex->Aonmouseup =>
+			dhname = "onmouseup";
+			di.evmask |= E->SEonmouseup;
+		}
+		if(hname != "")
+			puthandler(ex, ow, hname, e.value);
+		if(dhname != ""){
+			od := getobj(ex, ow, "document");
+			if(od == nil) {
+				reinitprop(ow, "document", docinstant(ex, f));
+				od = getobj(ex, ow, "document");
+			}
+			puthandler(ex, od, dhname, e.value);
+		}
+	}
+	di.events = nil;
+
+	od := getobj(ex, ow, "document");
+	if(od == nil) {
+		reinitprop(ow, "document", docinstant(ex, f));
+		od = getobj(ex, ow, "document");
+		CU->assert(od != nil);
+	}
+	else if(inbuild) {
+		docfill(ex, od, f);
+		ES->put(ex, od, "location", ES->objval(sw.locobj));
+	}
+	for(frml := sw.forms; frml != nil; frml = tl frml) {
+		frm := hd frml;
+		for (fldl := frm.fields; fldl != nil; fldl = tl fldl) {
+			(fld, ofield) := hd fldl;
+			if (ofield == nil)
+				continue;
+			if(fld.ctlid >= 0 && fld.ctlid < len f.controls) {
+				pick c := f.controls[fld.ctlid] {
+				Centry =>
+					reinitprop(ofield, "value", ES->strval(c.s));
+				Ccheckbox =>
+					cv := ES->false;
+					if(c.flags&Layout->CFactive)
+						cv = ES->true;
+					reinitprop(ofield, "checked", cv);
+				Cselect =>
+					for(i := 0; i < len c.options; i++) {
+						if(c.options[i].selected) {
+							reinitprop(ofield, "selectedIndex", ES->numval(real i));
+							# hack for common mistake in scripts
+							# (implemented by other browsers)
+							opts := getobj(ex, ofield, "options");
+							if (opts != nil)
+								reinitprop(opts, "selectedIndex", ES->numval(real i));
+						}
+					}
+				}
+			}
+		}
+	}
+	for(sil := sw.imgs; sil != nil; sil = tl sil) {
+		si := hd sil;
+		if(si.item.ci.complete != 0)
+			reinitprop(si.obj, "complete", ES->true);
+	}
+}
+
+xferfromscriptobjs(f: ref Layout->Frame, inbuild: int)
+{
+	sw := top.findbyframeid(f.id);
+	if(sw == nil)
+		return;
+	ex := sw.ex;
+	ow := ex.global;
+	od := getobj(ex, ow, "document");
+	if(od != nil) {
+		if(inbuild) {
+			di := f.doc;
+			di.doctitle = strxfer(ex, od, "title", di.doctitle);
+			di.background.color = colorxfer(ex, od, "bgColor", di.background.color);
+			di.text = colorxfer(ex, od, "fgColor", di.text);
+			di.alink = colorxfer(ex, od, "alinkColor", di.alink);
+			di.link = colorxfer(ex, od, "linkColor", di.link);
+			di.vlink = colorxfer(ex, od, "vlinkColor", di.vlink);
+			if(createdimages != nil) {
+				for(oil := createdimages; oil != nil; oil = tl oil) {
+					oi := hd oil;
+					vsrc := ES->get(ex, oi, "src");
+					if(ES->isstr(vsrc)) {
+						u := U->parse(vsrc.str);
+						if(u.path != "") {
+							u = U->mkabs(u, di.base);
+							it := Item.newimage(di, u, nil, "", B->Anone,
+								0, 0, 0, 0, 0, 0, 0, nil, nil, nil);
+							di.images = it :: di.images;
+						}
+					}
+				}
+			}
+		}
+		else {
+			for (ol := sw.imgrelocs; ol != nil; ol = tl ol) {
+				oi := hd ol;
+				vnewsrc := ES->get(ex, oi, "src");
+				if(ES->isstr(vnewsrc) && vnewsrc.str != nil) {
+					for(sil := sw.imgs; sil != nil; sil = tl sil) {
+						si := hd sil;
+						if(si.obj == oi) {
+							f.swapimage(si.item, vnewsrc.str);
+							break;
+						}
+					}
+				}
+			}
+			sw.imgrelocs = nil;
+		}
+	}
+}
+
+# Check ScriptWin tree for non-empty newlocs.
+# When found, generate a go event to the new place.
+# If found, don't recurse into children, because those
+# child frames are about to go away anyway.
+# Otherwise, recurse into all kids -- this might generate
+# multiple go events.
+# BUG: if multiple events are generated, later ones will
+# interrupt (STOP!) loading of pages specified by preceding
+# events.  To fix, need to queue them up, probably in
+# main charon module.
+checknewlocs(sw: ref ScriptWin)
+{
+	if(sw.newloc != "") {
+		E->evchan <-= ref Event.Ego(sw.newloc, sw.newloctarg, 0, E->EGnormal);
+		sw.newloc = "";
+	}
+	else {
+		for(l := sw.kids; l != nil; l = tl l)
+			checknewlocs(hd l);
+	}
+}
+
+checkopener()
+{
+	if(opener != nil && opener.newloc != "") {
+		CH->sendopener(sys->sprint("L %s", opener.newloc));	# just location for now
+		opener.newloc = "";
+	}
+	if(winclose)
+		G->exitcharon();
+}
+
+# if e.anchorid >= 0	=> target is Link
+# if e.fieldid > 0	=> target is FormField (and e.formid > 0)
+# if e.formid > 0	=> target is Form (e.fieldid == -1)
+# if e.imageid >= 0	=> target is Image
+# otherwise		=> target is window
+do_on(e: ref ScriptEvent)
+{
+	if(dbgdom)
+		sys->print("do_on %d, frameid=%d, formid=%d, fieldid=%d, anchorid=%d, imageid=%d, x=%d, y=%d, which=%d\n",
+			e.kind, e.frameid, e.formid, e.fieldid, e.anchorid, e.imageid, e.x, e.y, e.which);
+	if (top.error) {
+		if (dbgdom)
+			sys->print("do_on() previous error prevents processing\n");
+		if (e.reply != nil)
+			e.reply <-= nil;
+		jevchan <-= ref doneevent;
+		return;
+	}
+	sw := top.findbyframeid(e.frameid);
+	# BUG FIX: Frame can be reset by Charon main thread
+	# between us getting its ref and using it
+	# WARNING - xferfromscriptobjs() will not update non-ref-type members of frame adt
+	# (currently not a problem) as updates will go to our copy
+	f : ref Frame;
+	if (sw != nil && !sw.inbuild) {
+		f = ref *sw.frame;
+		if (f.doc == nil)
+			f = nil;
+	}
+	if (f == nil) {
+		if(e.reply != nil)
+			e.reply <-= nil;
+		jevchan <-= ref doneevent;
+		if (dbgdom)
+			sys->print("do_on() failed to find frame %d\n", e.frameid);
+		return;
+	}
+	ex := sw.ex;
+	ow := ex.global;
+	od := getobj(ex, ow, "document");
+	sw.docwriteout = nil;
+	
+{
+	# event target types
+	TAnchor, TForm, TFormField, TImage, TDocument, TWindow, Tnone: con iota;
+	ttype := Tnone;
+	target, oform: ref Obj;
+	if(e.anchorid >= 0) {
+		ttype = TAnchor;
+		target = getanchorobj(ex, e.frameid, e.anchorid);
+	} else if(e.formid > 0) {
+		oform = getformobj(ex, e.frameid, e.formid);
+		if(e.fieldid > 0) {
+			ttype = TFormField;
+			target = getformfieldobj(e.frameid, e.formid, e.fieldid);
+		} else {
+			ttype = TForm;
+			target = oform;
+		}
+	} else if(e.imageid >= 0) {
+		ttype = TImage;
+		target = getimageobj(ex, e.frameid, e.imageid);
+	} else if(e.kind == E->SEondblclick || e.kind == E->SEonkeydown ||
+		    e.kind == E->SEonkeypress || e.kind == E->SEonkeyup ||
+		    e.kind == E->SEonmousedown || e.kind == E->SEonmouseup){
+		ttype = TDocument;
+		target = od;
+	} else {
+		ttype = TWindow;
+		target = ow;
+	}
+	if(target != nil) {
+		oscript: ref Obj;
+		scrname := "";
+		case e.kind {
+		E->SEonabort =>
+			scrname = "onabort";
+		E->SEonblur =>
+			scrname = "onblur";
+		E->SEonchange =>
+			scrname = "onchange";
+		E->SEonclick =>
+			scrname = "onclick";
+		E->SEondblclick =>
+			scrname = "ondblclick";
+		E->SEonerror =>
+			scrname = "onerror";
+		E->SEonfocus =>
+			scrname = "onfocus";
+		E->SEonkeydown =>
+			scrname = "onkeydown";
+		E->SEonkeypress =>
+			scrname = "onkeypress";
+		E->SEonkeyup =>
+			scrname = "onkeyup";
+		E->SEonload =>
+			scrname = "onload";
+		E->SEonmousedown =>
+			scrname = "onmousedown";
+		E->SEonmouseout =>
+			scrname = "onmouseout";
+		E->SEonmouseover =>
+			scrname = "onmouseover";
+		E->SEonmouseup =>
+			scrname = "onmouseup";
+		E->SEonreset =>
+			scrname = "onreset";
+		E->SEonresize =>
+			scrname = "onresize";
+		E->SEonselect =>
+			scrname = "onselect";
+		E->SEonsubmit =>
+			scrname = "onsubmit";
+		E->SEonunload =>
+			scrname = "onunload";
+		E->SEtimeout or
+		E->SEinterval =>
+			oscript = dotimeout(ex, target, e);
+		E->SEscript =>
+			# TODO - handle document text from evalscript
+			# need to determine if document is 'open' or not.
+			(nil, nil, val) := evalscript(f, e.script);
+			if (e.reply != nil)
+				e.reply <- = val;
+			e.reply = nil;
+		}
+		if(scrname != "")
+			oscript = getobj(ex, target, scrname);
+		if(oscript != nil) {
+			xfertoscriptobjs(f, 0);
+			if(dbgdom)
+				sys->print("calling script\n");
+			# establish scope chain per Rhino p. 287 (3rd edition)
+			oldsc := ex.scopechain;
+			sc := ow :: nil;
+			if(ttype != TWindow) {
+				sc = od :: sc;
+				if(ttype == TFormField)
+					sc = oform :: sc;
+				if(ttype != TDocument)
+					sc = target :: sc;
+			}
+			ex.scopechain = sc;
+			v := ES->call(ex, oscript, target, nil, 1).val;
+			# 'fix' for onsubmit
+			# JS references state that if the handler returns false
+			# then the action is cancelled.
+			# other browsers interpret this as "if and only if the handler
+			# returns false."
+			# When a function completes normally without returning a value
+			# its value is 'undefined', toBoolean(undefined) = false
+			if (v == ES->undefined)
+				v = ES->true;
+			else
+				v = ES->toBoolean(ex, v);
+			ex.scopechain = oldsc;
+			# onreset/onsubmit reply channel
+			if(e.reply != nil) {
+				ans : string;
+				if(v == ES->true)
+					ans = "true";
+				e.reply <-= ans;
+				e.reply = nil;
+			}
+			xferfromscriptobjs(f, 0);
+			checknewlocs(top);
+			checkopener();
+
+			if (ttype == TFormField && e.kind == E->SEonclick && v == ES->true)
+				E->evchan <-= ref Event.Eformfield(e.frameid, e.formid, e.fieldid, E->EFFclick);
+			if (ttype == TAnchor && e.kind == E->SEonclick && v == ES->true) {
+				gohref := getstr(ex, target, "href");
+				gotarget := getstr(ex, target, "target");
+				if (gotarget == "")
+					gotarget = "_self";
+				E->evchan <-= ref Event.Ego(gohref, gotarget, 0, E->EGnormal);
+			}
+		}
+	}
+	if(e.reply != nil)
+		e.reply <-= nil;
+	checkdocwrite(top);
+	jevchan <-= ref doneevent;
+}
+exception exc{
+	"*" =>
+		if (exc == "throw") {
+			# ignore ecmascript runtime errors
+			if(dbgdom)
+				sys->print("error executing 'on' handler: %s\n", ex.error);
+		} else {
+			# fatal error
+			top.error = 1;
+			emsg := "Fatal error in script ("+exc+"):\n" + ex.error + "\n";
+			G->alert(emsg);
+		}
+		if(e.reply != nil)
+			e.reply <-= nil;
+		jevchan <-= ref doneevent;
+		return;
+}
+}
+
+xferframeset(sw : ref ScriptWin)
+{
+	if (!sw.inbuild)
+		xfertoscriptobjs(sw.frame, 1);
+	for (k := sw.kids; k != nil; k = tl k)
+		xferframeset(hd k);
+}
+
+framedone(f : ref Frame, hasscripts : int)
+{
+	sw := top.findbyframeid(f.id);
+	if (sw != nil) {
+		if (!top.active && hasscripts) {
+			# need to transfer entire frame tree
+			# as one frame can reference objects in another
+			xferframeset(top);
+		}
+		sw.active |= hasscripts;
+		top.active |= hasscripts;
+		if (top.active)
+			xfertoscriptobjs(f, 1);
+		sw.inbuild = 0;
+	}
+}
+
+checkdocwrite(sw : ref ScriptWin) : int
+{
+	if (sw.inbuild)
+		return 0;
+
+	if (sw.docwriteout != nil) {
+		# The URL is bogus - not sure what the correct value should be
+		ev := ref Event.Esettext(sw.frame.id, sw.frame.src, sw.docwriteout);
+		sw.docwriteout = "";
+		E->evchan <- = ev;
+		return 1;
+	}
+	for (k := sw.kids; k != nil; k = tl k)
+		if (checkdocwrite(hd k))
+			break;
+	return 0;
+}
+
+#
+# interface for host objects
+#
+get(ex: ref Exec, o: ref Obj, property: string): ref Val
+{
+	if(o.class == "document" && property == "cookie") {
+		ans := "";
+		target := top.findbyobj(o);
+		if(target != nil) {
+			url := target.frame.doc.src;
+			ans = CU->getcookies(url.host, url.path, url.scheme == "https");
+		}
+		return ES->strval(ans);
+	}
+	if(o.class == "Window" && property == "opener"){
+		if(!CH->hasopener() || top.ex.global != o)
+			v := ES->undefined;
+		else{
+			if(opener == nil)
+				opener = ScriptWin.dummy();
+			v = ES->objval(opener.ex.global);
+		}
+		reinitprop(o, "opener", v);
+	}
+	return ES->get(ex, o, property);
+}
+
+
+put(ex: ref Exec, o: ref Obj, property: string, val: ref Val)
+{
+	if(dbgdom)
+		sys->print("put property %s in cobj of class %s\n", property, o.class);
+
+	url : ref Parsedurl;
+	target : ref ScriptWin;
+	str := ES->toString(ex, val);
+	ev := E->SEnone;
+
+	case o.class {
+	"Array" =>
+		# we 'host' the Formfield.options array so as we can
+		# track changes to the options list
+		vformfield := ES->get(ex, o, "@PRIVformfield");
+		if (!ES->isobj(vformfield))
+			# not one of our 'options' arrays
+			break;
+		ix := prop2index(property);
+		if (property != "length" && ix == -1)
+			# not a property that affects us
+			break;
+		oformfield := vformfield.obj;
+		oform := getobj(ex, oformfield, "form");
+		if (oform == nil)
+			break;
+		ES->put(ex, o, property, val);
+		if (ES->isobj(val) && val.obj.class == "Option") {
+			ES->put(ex, val.obj, "@PRIVformfield", vformfield);
+			ES->put(ex, val.obj, "form", ES->objval(oform));
+			reinitprop(val.obj, "index", ES->numval(real ix));
+		}
+		updateffopts(ex, oform, oformfield, ix);
+		return;
+	"Window" =>
+		case property {
+		"defaultStatus" or
+		"status" =>
+			if(ES->isstr(val)) {
+				if(property == "defaultStatus")
+					defaultStatus = val.str;
+				G->setstatus(val.str);
+			}
+		"location" =>
+			target = top.findbyobj(o);
+			if (target == nil)
+				break;
+			url = U->parse(str);
+			# TODO: be more defensive
+			url = U->mkabs(url, target.frame.doc.base);
+		"name" =>
+			sw := top.findbyobj(o);
+			if (sw == nil)
+				break;
+			name := ES->toString(ex, val);
+			if (sw.parent != nil) {
+				w := sw.parent.ex.global;
+				v := sw.val;
+				if (sw.frame.name != nil)
+					ES->delete(ex, w, sw.frame.name);
+				ES->varinstant(w, 0, name, ref RefVal(v));
+			}
+			# Window.name is used for determining TARGET of <A> etc.
+			# update Charon's Frame info so as new name gets used properly
+			sw.frame.name = name;
+		"offscreenBuffering" =>
+			if(ES->isstr(val) || val == ES->true || val == ES->false){
+			}	
+		"onblur" =>
+			ev = E->SEonblur;
+		"onerror" =>
+			ev = E->SEonerror;
+		"onfocus" =>
+			ev = E->SEonfocus;
+		"onload" =>
+			ev = E->SEonload;
+		"onresize" =>
+			ev = E->SEonresize;
+		"onunload" =>
+			ev = E->SEonunload;
+		"opener" =>
+			;
+		}
+		if(ev != E->SEnone) {
+			target = top.findbyobj(o);
+			if(target == nil)
+				break;
+			di := target.frame.doc;
+			if(!ES->isobj(val) || val.obj.call == nil)
+				di.evmask &= ~ev;
+			else
+				di.evmask |= ev;
+		}
+	"Link" =>
+		case property {
+		"onclick" =>
+			ev = E->SEonclick;
+		"ondblclick" =>
+			ev = E->SEondblclick;
+		"onkeydown" =>
+			ev = E->SEonkeydown;
+		"onkeypress" =>
+			ev = E->SEonkeypress;
+		"onkeyup" =>
+			ev = E->SEonkeyup;
+		"onmousedown" =>
+			ev = E->SEonmousedown;
+		"onmouseout" =>
+			ev = E->SEonmouseout;
+		"onmouseover" =>
+			ev = E->SEonmouseover;
+		"onmouseup" =>
+			ev = E->SEonmouseup;
+		}
+		if(ev != E->SEnone) {
+			vframeid := ES->get(ex, o, "@PRIVframeid");
+			vanchorid := ES->get(ex, o, "@PRIVanchorid");
+			if(!ES->isnum(vframeid) || !ES->isnum(vanchorid))
+				break;
+			frameid := ES->toInt32(ex, vframeid);
+			anchorid := ES->toInt32(ex, vanchorid);
+			target = top.findbyframeid(frameid);
+			if(target == nil)
+				break;
+			anchor: ref B->Anchor;
+			for(al := target.frame.doc.anchors; al != nil; al = tl al) {
+				a := hd al;
+				if(a.index == anchorid) {
+					anchor = a;
+					break;
+				}
+			}
+			if(anchor == nil)
+				break;
+			if(!ES->isobj(val) || val.obj.call == nil)
+				anchor.evmask &= ~ev;
+			else
+				anchor.evmask |= ev;
+		}
+	"Location" =>
+		target = top.findbyobj(o);
+		if (target == nil) {
+			break;
+		}
+		url = ref *target.frame.doc.src;
+		case property {
+		"hash" =>
+			if (str != nil && str[0] == '#')
+				str = str[1:];
+			url.frag = str;
+		"host" =>
+			# host:port
+			(h, p) := S->splitl(str, ":");
+			if (p != nil)
+				p = p[1:];
+			if (h != nil)
+				url.host = h;
+			if (p != nil)
+				url.port = p;
+		"hostname" =>
+			url.host = str;
+		"href" or
+		"pathname" =>
+			url = U->mkabs(U->parse(str), target.frame.doc.base);
+		"port" =>
+			url.port = str;
+		"protocol" =>
+			url.scheme = S->tolower(str);
+		"search" =>
+			url.query = str;
+		* =>
+			url = nil;
+		}
+	"Image" =>
+		case property {
+		"src" or
+		"lowsrc" =>
+			# making URLs absolute matches Netscape
+			target = top.findbyobj(o);
+			if(target == nil)
+				break;
+			url = U->mkabs(U->parse(str), target.frame.doc.base);
+			val = ES->strval(url.tostring());
+			target.imgrelocs = o :: target.imgrelocs;
+			url = nil;
+		"onabort" =>
+			ev = E->SEonabort;
+		"ondblclick" =>
+			ev = E->SEondblclick;
+		"onkeydown" =>
+			ev = E->SEonkeydown;
+		"onkeypress" =>
+			ev = E->SEonkeypress;
+		"onkeyup" =>
+			ev = E->SEonkeyup;
+		"onerror" =>
+			ev = E->SEonerror;
+		"onload" =>
+			ev = E->SEonload;
+		"onmousedown" =>
+			ev = E->SEonmousedown;
+		"onmouseout" =>
+			ev = E->SEonmouseout;
+		"onmouseover" =>
+			ev = E->SEonmouseover;
+		"onmouseup" =>
+			ev = E->SEonmouseup;
+		}
+		if(ev != E->SEnone) {
+			target = top.findbyobj(o);
+			if(target == nil)
+				break;
+			vimageid := ES->get(ex, o, "@PRIVimageid");
+			if(!ES->isnum(vimageid))
+				break;
+			imageid := ES->toInt32(ex, vimageid);
+			image: ref (Build->Item).Iimage;
+		forloop:
+			for(il := target.frame.doc.images; il != nil; il = tl il) {
+				pick im := hd il {
+				Iimage =>
+					if(im.imageid == imageid) {
+						image = im;
+						break forloop;
+					}
+				}
+			}
+			# BUG: if image has no genattr then the event handler update
+			# will not be done - can never set a handler for an image that
+			# didn't have a handler
+			if(image == nil || image.genattr == nil)
+				break;
+			if(!ES->isobj(val) || val.obj.call == nil)
+				image.genattr.evmask &= ~ev;
+			else
+				image.genattr.evmask |= ev;
+		}
+	"Form" =>
+		action := "";
+		case property {
+		"onreset" =>
+			ev = E->SEonreset;
+		"onsubmit" =>
+			ev = E->SEonsubmit;
+		"action" =>
+			action = str;
+		* =>
+			break;
+		}
+		vframeid := ES->get(ex, o, "@PRIVframeid");
+		vformid := ES->get(ex, o, "@PRIVformid");
+		if(!ES->isnum(vframeid) || !ES->isnum(vformid))
+			break;
+		frameid := ES->toInt32(ex, vframeid);
+		formid := ES->toInt32(ex, vformid);
+		target = top.findbyframeid(frameid);
+		if(target == nil)
+			break;
+		form: ref B->Form;
+		for(fl := target.frame.doc.forms; fl != nil; fl = tl fl) {
+			f := hd fl;
+			if(f.formid == formid) {
+				form = f;
+				break;
+			}
+		}
+		if(form == nil)
+			break;
+		if (ev != E->SEnone) {
+			if(!ES->isobj(val) || val.obj.call == nil)
+				form.evmask &= ~ev;
+			else
+				form.evmask |= ev;
+			break;
+		}
+		if (action != "")
+			form.action = U->mkabs(U->parse(action), target.frame.doc.base);
+	"FormField" =>
+		oform := getobj(ex, o, "form");
+		vframeid := ES->get(ex, oform, "@PRIVframeid");
+		vformid := ES->get(ex, oform, "@PRIVformid");
+		vfieldid := ES->get(ex, o, "@PRIVfieldid");
+		if(!ES->isnum(vframeid) || !ES->isnum(vformid) || !ES->isnum(vfieldid))
+			break;
+		frameid := ES->toInt32(ex, vframeid);
+		formid := ES->toInt32(ex, vformid);
+		fieldid := ES->toInt32(ex, vfieldid);
+		target = top.findbyframeid(frameid);
+		if(target == nil)
+			break;
+		form: ref B->Form;
+		for(fl := target.frame.doc.forms; fl != nil; fl = tl fl) {
+			f := hd fl;
+			if(f.formid == formid) {
+				form = f;
+				break;
+			}
+		}
+		if(form == nil)
+			break;
+		field: ref B->Formfield;
+		for(ffl := form.fields; ffl != nil; ffl = tl ffl) {
+			ff := hd ffl;
+			if(ff.fieldid == fieldid) {
+				field = ff;
+			break;
+			}
+		}
+		if(field == nil)
+			break;
+		case property {
+		"onblur" =>
+			ev = E->SEonblur;
+		"onchange" =>
+			ev = E->SEonchange;
+		"onclick" =>
+			ev = E->SEonclick;
+		"ondblclick" =>
+			ev = E->SEondblclick;
+		"onfocus" =>
+			ev = E->SEonfocus;
+		"onkeydown" =>
+			ev = E->SEonkeydown;
+		"onkeypress" =>
+			ev = E->SEonkeypress;
+		"onkeyup" =>
+			ev = E->SEonkeyup;
+		"onmousedown" =>
+			ev = E->SEonmousedown;
+		"onmouseup" =>
+			ev = E->SEonmouseup;
+		"onselect" =>
+			ev = E->SEonselect;
+		"value" =>
+			field.value = str;
+			if(target.frame.controls == nil ||
+			   field.ctlid < 0 ||
+			   field.ctlid > len target.frame.controls){
+				break;
+			}
+			c := target.frame.controls[field.ctlid];
+			pick ctl := c {
+			Centry =>
+				ctl.s = str;
+				ctl.sel = (0, 0);
+				E->evchan <-= ref Event.Eformfield(frameid, formid, fieldid, E->EFFredraw);
+			}
+		}
+
+		if(ev != E->SEnone) {
+			if(!ES->isobj(val) || val.obj.call == nil)
+				field.evmask &= ~ev;
+			else
+				field.evmask |= ev;
+		}
+	"document" =>
+		case property {
+		"location" =>
+			target = top.findbyobj(o);
+			if (target == nil)
+				break;
+			# TODO: be more defensive
+			url = U->mkabs(U->parse(str), target.frame.doc.base);
+		"cookie" =>
+			target = top.findbyobj(o);
+			if(target != nil && (CU->config).docookies > 0) {
+				url = target.frame.doc.src;
+				CU->setcookie(url.host, url.path, str);
+			}
+			return;
+		"ondblclick" =>
+			ev = E->SEondblclick;
+		"onkeydown" =>
+			ev = E->SEonkeydown;
+		"onkeypress" =>
+			ev = E->SEonkeypress;
+		"onkeyup" =>
+			ev = E->SEonkeyup;
+		"onmousedown" =>
+			ev = E->SEonmousedown;
+		"onmouseup" =>
+			ev = E->SEonmouseup;
+		}
+		if(ev != E->SEnone){
+			target = top.findbyobj(o);
+			if(target == nil)
+				break;
+			di := target.frame.doc;
+			if(!ES->isobj(val) || val.obj.call == nil)
+				di.evmask &= ~ev;
+			else
+				di.evmask |= ev;
+		}
+	"Option" =>
+		vformfield := ES->get(ex, o, "@PRIVformfield");
+		vindex := ES->get(ex, o, "index");
+		if (!ES->isobj(vformfield) || !ES->isnum(vindex))
+			# not one of our 'options' objects
+			break;
+		oformfield := vformfield.obj;
+		oform := getobj(ex, oformfield, "form");
+		if (oform == nil)
+			break;
+		ES->put(ex, o, property, val);
+		index := ES->toInt32(ex, vindex);
+		updateffopts(ex, oform, oformfield, index);
+	}
+	ES->put(ex, o, property, val);
+
+	if (url != nil && target != nil) {
+		if (!CU->urlequal(url, target.frame.doc.src)) {
+			target.newloc = url.tostring();
+			target.newloctarg = "_top";
+			if(target.frame != nil)
+				target.newloctarg = target.frame.name;
+		}
+	}
+}
+
+canput(ex: ref Exec, o: ref Obj, property: string): ref Val
+{
+	return ES->canput(ex, o, property);
+}
+
+hasproperty(ex: ref Exec, o: ref Obj, property: string): ref Val
+{
+	return ES->hasproperty(ex, o, property);
+}
+
+delete(ex: ref Exec, o: ref Obj, property: string)
+{
+	ES->delete(ex, o, property);
+}
+
+defaultval(ex: ref Exec, o: ref Obj, tyhint: int): ref Val
+{
+	return ES->defaultval(ex, o, tyhint);
+}
+
+call(ex: ref Exec, func, this: ref Obj, args: array of ref Val, nil: int): ref Ref
+{
+	if(dbgdom)
+		sys->print("call %x (class %s), val %s\n", func, func.class, func.val.str);
+	ans := ES->valref(ES->true);
+	case func.val.str{
+	"document.prototype.open" =>
+		sw := top.findbyobj(this);
+		if (sw != nil)
+			sw.docwriteout = "";
+	"document.prototype.close" =>
+		# ignore for now
+		;
+	"document.prototype.write" =>
+		sw := top.findbyobj(this);
+		if (sw != nil) {
+			for (ai := 0; ai < len args; ai++)
+				sw.docwriteout += ES->toString(ex, ES->biarg(args, ai));
+		}
+	"document.prototype.writeln" =>
+		sw := top.findbyobj(this);
+		if (sw != nil) {
+			for (ai := 0; ai < len args; ai++)
+				sw.docwriteout += ES->toString(ex, ES->biarg(args, ai));
+			sw.docwriteout += "\n";
+		}
+	"navigator.prototype.javaEnabled" or
+	"navigator.prototype.taintEnabled" =>
+		ans = ES->valref(ES->false);
+	"Form.prototype.reset" or "Form.prototype.submit"=>
+		vframeid := ES->get(ex, this, "@PRIVframeid");
+		vformid := ES->get(ex, this, "@PRIVformid");
+		if(ES->isnum(vframeid) && ES->isnum(vformid)) {
+			frameid := ES->toInt32(ex, vframeid);
+			formid := ES->toInt32(ex, vformid);
+			ftype : int;
+			if(func.val.str == "Form.prototype.reset")
+				ftype = E->EFreset;
+			else
+				ftype = E->EFsubmit;
+			E->evchan <-= ref Event.Eform(frameid, formid, ftype);
+		}
+	"FormField.prototype.blur" or
+	"FormField.prototype.click" or
+	"FormField.prototype.focus" or
+	"FormField.prototype.select" =>
+		oform := getobj(ex, this, "form");
+		vformid := ES->get(ex, oform, "@PRIVformid");
+		vframeid := ES->get(ex, oform, "@PRIVframeid");
+		vfieldid := ES->get(ex, this, "@PRIVfieldid");
+		if(ES->isnum(vframeid) && ES->isnum(vformid) && ES->isnum(vfieldid)) {
+			frameid := ES->toInt32(ex, vframeid);
+			formid := ES->toInt32(ex, vformid);
+			fieldid := ES->toInt32(ex, vfieldid);
+			fftype : int;
+			case func.val.str{
+			"FormField.prototype.blur" =>
+				fftype = E->EFFblur;
+			"FormField.prototype.click" =>
+				fftype = E->EFFclick;
+			"FormField.prototype.focus" =>
+				fftype = E->EFFfocus;
+			"FormField.prototype.select" =>
+				fftype = E->EFFselect;
+			* =>
+				fftype = E->EFFnone;
+			}
+			E->evchan <-= ref Event.Eformfield(frameid, formid, fieldid, fftype);
+		}
+	"History.prototype.back" =>
+		E->evchan <-= ref Event.Ego("", "", 0, E->EGback);
+	"History.prototype.forward" =>
+		E->evchan <-= ref Event.Ego("", "", 0, E->EGforward);
+	"History.prototype.go" =>
+		ego : ref Event.Ego;
+		v := ES->biarg(args, 0);
+		if(ES->isstr(v))
+			ego = ref Event.Ego(v.str, "", 0, E->EGlocation);
+		else if(ES->isnum(v)) {
+			delta := ES->toInt32(ex, v);
+			case delta {
+			-1 =>
+				ego = ref Event.Ego("", "", 0, E->EGback);
+			0 =>
+				ego = ref Event.Ego("", "", 0, E->EGreload);
+			1 =>
+				ego = ref Event.Ego("", "", 0, E->EGforward);
+			* =>
+				ego = ref Event.Ego("", "", delta,  E->EGdelta);
+			}
+		}
+		if(ego != nil)
+			E->evchan <-= ego;
+	"Location.prototype.reload" =>
+		# ignore 'force' argument for now
+		E->evchan <-= ref Event.Ego("", "", 0, E->EGreload);
+	"Location.prototype.replace" =>
+		v := ES->biarg(args, 0);
+		if(ES->isstr(v)) {
+			sw := top.findbyobj(this);
+			if(sw == nil)
+				fname := "_top";
+			else
+				fname = sw.frame.name;
+			if (v.str != nil) {
+				url := U->mkabs(U->parse(v.str), sw.frame.doc.base);
+				E->evchan <-= ref Event.Ego(url.tostring(), fname, 0, E->EGreplace);
+			}
+		}
+	"Window.prototype.alert" =>
+		G->alert(ES->toString(ex, ES->biarg(args, 0)));
+	"Window.prototype.blur" =>
+		;
+#		sw := top.findbyobj(this);
+#		if (sw != nil)
+#			E->evchan <-= ref Event.Eframefocus(sw.frame.id, 0);
+	
+	"Window.prototype.clearTimeout" or
+	"Window.prototype.clearInterval" =>
+		v := ES->biarg(args, 0);
+		id := ES->toInt32(ex, v);
+		clrtimeout(ex, this, id);
+	"Window.prototype.close" =>
+		if(this == top.ex.global)
+			winclose = 1;
+		# no-op
+		;
+	"Window.prototype.confirm" =>
+		code := G->confirm(ES->toString(ex, ES->biarg(args, 0)));
+		if(code != 1)
+			ans = ES->valref(ES->false);
+	"Window.prototype.focus" =>
+		;
+#		sw := top.findbyobj(this);
+#		if (sw != nil)
+#			E->evchan <-= ref Event.Eframefocus(sw.frame.id, 1);
+	"Window.prototype.moveBy" or
+	"Window.prototype.moveTo" =>
+		# no-op
+		;
+	"Window.prototype.open" =>
+		if (dbgdom)
+			sys->print("window.open called\n");
+		u := ES->toString(ex, ES->biarg(args, 0));
+		n := ES->toString(ex, ES->biarg(args, 1));
+		sw : ref ScriptWin;
+		if (n != "")
+			sw = top.findbyname(n);
+		newch := 0;
+		if (sw == nil){
+			sw = top;
+			newch = 1;
+		}
+		if(u != "") {
+			# Want to replace window by navigation to u
+			sw.newloc = u;
+			if (sw.frame.name != "")
+				sw.newloctarg = sw.frame.name;
+			else
+				sw.newloctarg = "_top";
+			url : ref U->Parsedurl;
+			if (sw.frame.doc != nil && sw.frame.doc.base != nil)
+				url = U->mkabs(U->parse(u), sw.frame.doc.base);
+			else 
+				url = CU->makeabsurl(u);
+			sw.newloc = url.tostring();
+		}
+		if(newch){
+			# create dummy window
+			dw := ScriptWin.dummy();
+			spawn newcharon(sw.newloc, sw.newloctarg, sw);
+			sw.newloc = "";
+			sw.newloctarg = "";
+			ans = ES->valref(dw.val);
+		}
+		else
+			ans = ES->valref(sw.val);
+	"Window.prototype.prompt" =>
+		msg := ES->toString(ex, ES->biarg(args, 0));
+		dflt := ES->toString(ex, ES->biarg(args, 1));
+		(code, input) := G->prompt(msg, dflt);
+		v := ES->null;
+		if(code == 1)
+			v = ES->strval(input);
+		ans = ES->valref(v);
+	"Window.prototype.resizeBy" or
+	"Window.prototype.resizeTo" =>
+		# no-op
+		;
+	"Window.prototype.scroll" or
+	"Window.prototype.scrollTo" =>
+		# scroll is done via an event to avoid race in calls to
+		# Layout->fixframegeom() [made by scroll code]
+		sw := top.findbyobj(this);
+		if (sw != nil) {
+			(xv, yv) := (ES->biarg(args, 0), ES->biarg(args, 1));
+			pt := Draw->Point(ES->toInt32(ex, xv), ES->toInt32(ex, yv));
+			E->evchan <-= ref Event.Escroll(sw.frame.id, pt);
+		}
+	"Window.prototype.scrollBy" =>
+		sw := top.findbyobj(this);
+		if (sw != nil) {
+			(dxv, dyv) := (ES->biarg(args, 0), ES->biarg(args, 1));
+			pt := Draw->Point(ES->toInt32(ex, dxv), ES->toInt32(ex, dyv));
+			E->evchan <-= ref Event.Escrollr(sw.frame.id, pt);
+		}
+	"Window.prototype.setTimeout" =>
+		(v1, v2) := (ES->biarg(args, 0), ES->biarg(args, 1));
+		cmd := ES->toString(ex, v1);
+		ms := ES->toInt32(ex, v2);
+		id := addtimeout(ex, this, cmd, ms, E->SEtimeout);
+		ans = ES->valref(ES->numval(real id));
+	"Window.prototype.setInterval" =>
+		(v1, v2) := (ES->biarg(args, 0), ES->biarg(args, 1));
+		cmd := ES->toString(ex, v1);
+		ms := ES->toInt32(ex, v2);
+		id := addtimeout(ex, this, cmd, ms, E->SEinterval);
+		ans = ES->valref(ES->numval(real id));
+	* =>
+		ES->runtime(ex, nil, "unknown or unimplemented func "+func.val.str+" in host call");
+		return nil;
+	}
+	return ans;
+}
+
+construct(ex: ref Exec, func: ref Obj, args: array of ref Val): ref Obj
+{
+	if(dbgdom)
+		sys->print("construct %x (class %s), val %s\n", func, func.class, func.val.str);
+	params: array of string;
+	o: ref Obj;
+#sys->print("Construct(%s)\n", func.val.str);
+	case func.val.str {
+	"Image" =>
+		o = mkhostobj(ex, "Image");
+		params = array [] of {"width", "height"};
+		createdimages = o :: createdimages;
+	"Option" =>
+		o = mkhostobj(ex, "Option");
+		params = array [] of {"text", "value", "defaultSelected", "selected"};
+	* =>
+		return nil;
+	}
+	for (i := 0; i < len args && i < len params; i++)
+		reinitprop(o, params[i], args[i]);
+	return o;
+}
+
+updateffopts(ex: ref Exec, oform, oformfield: ref Obj, ix: int)
+{
+	vframeid := ES->get(ex, oform, "@PRIVframeid");
+	vformid := ES->get(ex, oform, "@PRIVformid");
+	vfieldid := ES->get(ex, oformfield, "@PRIVfieldid");
+	if(!ES->isnum(vframeid) || !ES->isnum(vformid) || !ES->isnum(vfieldid))
+		return;
+	frameid := ES->toInt32(ex, vframeid);
+	formid := ES->toInt32(ex, vformid);
+	fieldid := ES->toInt32(ex, vfieldid);
+
+	target := top.findbyframeid(frameid);
+	if(target == nil)
+		return;
+	form: ref B->Form;
+	for(fl := target.frame.doc.forms; fl != nil; fl = tl fl) {
+		f := hd fl;
+		if(f.formid == formid) {
+			form = f;
+			break;
+		}
+	}
+	if(form == nil)
+		return;
+	field: ref B->Formfield;
+	for(ffl := form.fields; ffl != nil; ffl = tl ffl) {
+		ff := hd ffl;
+		if(ff.fieldid == fieldid) {
+			field = ff;
+			break;
+		}
+	}
+	if(field == nil)
+		return;
+
+	selctl : ref Control.Cselect;
+	pick ctl := target.frame.controls[field.ctlid] {
+	Cselect =>
+		selctl = ctl;
+	* =>
+		return;
+	}
+
+	(opts, nopts) := getarraywithlen(ex, oformfield, "options");
+	if (opts == nil)
+		return;
+	optl: list of ref B->Option;
+	selobj, firstobj: ref Obj;
+	selopt, firstopt: ref B->Option;
+	noptl := 0;
+	for (i := 0; i < nopts; i++) {
+		vopt := ES->get(ex, opts, string i);
+		if (!ES->isobj(vopt) || vopt.obj.class != "Option")
+			continue;
+		oopt := vopt.obj;
+		sel := ES->get(ex, oopt, "selected") == ES->true;
+		val := ES->toString(ex, ES->get(ex, oopt, "value"));
+		text := ES->toString(ex, ES->get(ex, oopt, "text"));
+		option := ref B->Option(sel, val, text);
+		optl = option :: optl;
+		if (noptl++ == 0) {
+			firstobj = oopt;
+			firstopt = option;
+		}
+		if (sel && (selobj == nil || ix == i)) {
+			selobj = oopt;
+			selopt = option;
+		}
+		if (! int(field.flags & B->FFmultiple)) {
+			ES->put(ex, oopt, "selected", ES->false);
+			option.selected = 0;
+		}
+	}
+	if (selobj != nil)
+		ES->put(ex, selobj, "selected", ES->true);
+	else if (firstobj != nil)
+		ES->put(ex, firstobj, "selected", ES->true);
+	if (selopt != nil)
+		selopt.selected = 1;
+	else if (firstopt != nil)
+		firstopt.selected = 1;
+	opta := array [noptl] of B->Option;
+	for (i = noptl - 1; i >= 0; i--)
+		(opta[i], optl) = (*hd optl, tl optl);
+	# race here with charon.b:form_submit() and layout code
+	selctl.options = opta;
+	E->evchan <-= ref Event.Eformfield(frameid, formid, fieldid, E->EFFredraw);
+}
+
+timeout(e : ref ScriptEvent, ms : int)
+{
+	sys->sleep(ms);
+	jevchan <- = e;
+}
+
+# BUGS
+#	cannot set a timeout for a window just created by window.open()
+#	because it will not have an entry in the ScriptWin tree
+#	(This is really a problem with the ScriptEvent adt only taking a frame id)
+#
+addtimeout(ex : ref Exec, win : ref Obj, cmd : string, ms : int, evk: int) : int
+{
+	sw := top.findbyobj(win);
+	if (sw == nil || cmd == nil || ms <= 0)
+		return -1;
+
+	# check for timeout handler array, create if doesn't exist
+	(toa, n) := getarraywithlen(ex, win, "@PRIVtoa");
+	if (toa == nil) {
+		toa = ES->mkobj(ex.arrayproto, "Array");
+		ES->varinstant(toa, ES->DontEnum|ES->DontDelete, "length", ref RefVal(ES->numval(0.)));
+		ES->varinstant(win, ES->DontEnum|ES->DontDelete, "@PRIVtoa", ref RefVal(ES->objval(toa)));
+	}
+	# find first free handler
+	for (ix := 0; ix < n; ix++) {
+		hv := ES->get(ex, toa, string ix);
+
+		if (hv == nil)
+			break;
+		# val == null		Timeout has been cancelled, but timer still running
+		# val == undefined	Timeout has expired
+		if (hv == ES->undefined)
+			break;
+	}
+
+	# construct a private handler for the timeout
+	# The code is always executed in the scope of the window object
+	# for which the timeout is being set.
+	oldsc := ex.scopechain;
+	ex.scopechain = win :: nil;
+	ES->eval(ex, "function PRIVhandler() {" + cmd + "}");
+	hobj := getobj(ex, win, "PRIVhandler");
+	ex.scopechain = oldsc;
+	if(hobj == nil)
+		return -1;
+	ES->put(ex, toa, string ix, ES->objval(hobj));
+	ev := ref ScriptEvent(evk, sw.frame.id, -1, -1, -1, -1, 0, 0, ix, nil, nil, ms);
+	spawn timeout(ev, ms);
+	return ix;
+}
+
+dotimeout(ex : ref Exec, win : ref Obj, e: ref ScriptEvent) : ref Ecmascript->Obj
+{
+	id := e.which;
+	if (id < 0)
+		return nil;
+
+	(toa, n) := getarraywithlen(ex, win, "@PRIVtoa");
+	if (toa == nil || id >= n)
+		return nil;
+
+	handler := getobj(ex, toa, string id);
+	if (handler == nil)
+		return nil;
+	if(e.kind == E->SEinterval){
+		ev := ref ScriptEvent;
+		*ev = *e;
+		spawn timeout(ev, e.ms);
+		return handler;
+	}
+	if (id == n-1)
+		ES->put(ex, toa, "length", ES->numval(real (n-1)));
+	else
+		ES->put(ex, toa, string id, ES->undefined);
+	return handler;
+}
+
+clrtimeout(ex : ref Exec, win : ref Obj, id : int)
+{
+	if (id < 0)
+		return;
+	(toa, n) := getarraywithlen(ex, win, "@PRIVtoa");
+	if (toa == nil || id >= n)
+		return;
+
+	ES->put(ex, toa, string id, ES->null);
+}
+
+# Make a host object with given class.
+# Get the prototype from the objspecs array
+# (if none yet, make one up and install the methods).
+# Put in required properties, with undefined values initially.
+# If mainex is nil (it will be for bootstrapping the initial object),
+# the prototype has to be filled in later.
+mkhostobj(ex : ref Exec, class: string) : ref Obj
+{
+	ci := specindex(class);
+	proto : ref Obj;
+	if(ex != nil)
+		proto = mkprototype(ex, ci);
+	ans := ES->mkobj(proto, class);
+	initprops(ex, ans, objspecs[ci].props);
+	ans.host = me;
+	return ans;
+}
+
+initprops(ex : ref Exec, o: ref Obj, props: array of PropSpec)
+{
+	if(props == nil)
+		return;
+	for(i := 0; i < len props; i++) {
+		v := ES->undefined;
+		case props[i].initval {
+		IVundef =>
+			v = ES->undefined;
+		IVnull =>
+			v = ES->null;
+		IVtrue =>
+			v = ES->true;
+		IVfalse =>
+			v = ES->false;
+		IVnullstr =>
+			v = nullstrval;
+		IVzero =>
+			v = zeroval;
+		IVzerostr =>
+			v = zerostrval;
+		IVarray =>
+			# need a separate one for each array,
+			# since we'll update these rather than replacing
+			ao := ES->mkobj(ex.arrayproto, "Array");
+			ES->varinstant(ao, ES->DontEnum|ES->DontDelete, "length", ref RefVal(ES->numval(0.)));
+			v = ES->objval(ao);
+		* =>
+			CU->assert(0);
+		}
+		ES->varinstant(o, props[i].attr | ES->DontDelete, props[i].name, ref RefVal(v));
+	}
+}
+
+# Return index into objspecs where class is specified
+specindex(class: string) : int
+{
+	for(i := 0; i < len objspecs; i++)
+		if(objspecs[i].name == class)
+			break;
+	if(i == len objspecs)
+		raise "EXInternal: couldn't find host object class " + class;
+	return i;
+}
+
+# Make a prototype for host object specified by objspecs[ci]
+mkprototype(ex : ref Exec, ci : int) : ref Obj
+{
+	CU->assert(ex != nil);
+	class := objspecs[ci].name;
+	prototype := ES->mkobj(ex.objproto, class);
+	meths := objspecs[ci].methods;
+	for(k := 0; k < len meths; k++) {
+		name := meths[k].name;
+		fullname := class + ".prototype." + name;
+		args := meths[k].args;
+		ES->biinst(prototype, Builtin(name, fullname, args, len args),
+			ex.funcproto, me);
+	}
+	return prototype;
+}
+
+
+getframeobj(frameid: int) : ref Obj
+{
+	sw := top.findbyframeid(frameid);
+	if(sw != nil)
+		return sw.ex.global;
+	return nil;
+}
+
+getdocobj(ex : ref Exec, frameid: int) : ref Obj
+{
+	return getobj(ex, getframeobj(frameid), "document");
+}
+
+getformobj(ex : ref Exec, frameid, formid: int) : ref Obj
+{
+	# frameids are 1-origin, document.forms is 0-origin
+	return getarrayelem(ex, getdocobj(ex, frameid), "forms", formid-1);
+}
+
+getformfieldobj(frameid, formid, fieldid: int) : ref Obj
+{
+	sw := top.findbyframeid(frameid);
+	if (sw == nil)
+		return nil;
+	flds : list of (ref Build->Formfield, ref Obj);
+	for (fl := sw.forms; fl != nil; fl = tl fl) {
+		sf := hd fl;
+		if (sf.form.formid == formid) {
+			flds = sf.fields;
+			break;
+		}
+	}
+	for (; flds != nil; flds = tl flds) {
+		(fld, obj) := hd flds;
+		if (fld.fieldid == fieldid)
+			return obj;
+	}
+	return nil;
+}
+
+getanchorobj(ex: ref Exec, frameid, anchorid: int) : ref Obj
+{
+	od := getdocobj(ex, frameid);
+	if(od != nil) {
+		(olinks, olinkslen) := getarraywithlen(ex, od, "links");
+		if(olinks != nil) {
+			for(i := 0; i < olinkslen; i++) {
+				ol := getobj(ex, olinks, string i);
+				if(ol != nil) {
+					v := ES->get(ex, ol, "@PRIVanchorid");
+					if(ES->isnum(v) && ES->toInt32(ex, v) == anchorid)
+						return ol;
+				}
+			}
+		}
+	}
+	return nil;
+}
+
+getimageobj(ex: ref Exec, frameid, imageid: int) : ref Obj
+{
+	od := getdocobj(ex, frameid);
+	if(od != nil) {
+		(oimages, oimageslen) := getarraywithlen(ex, od, "images");
+		if(oimages != nil) {
+			for(i := 0; i < oimageslen; i++) {
+				oi := getobj(ex, oimages, string i);
+				if(oi != nil) {
+					v := ES->get(ex, oi, "@PRIVimageid");
+					if(ES->isnum(v) && ES->toInt32(ex, v) == imageid)
+						return oi;
+				}
+			}
+		}
+	}
+	return nil;
+}
+
+# return nil if none such, or not an object
+getobj(ex : ref Exec, o: ref Obj, prop: string) : ref Obj
+{
+	if(o != nil) {
+		v := ES->get(ex, o, prop);
+		if(ES->isobj(v))
+			return ES->toObject(ex, v);
+	}
+	return nil;
+}
+
+# return nil if none such, or not an object
+getarrayelem(ex : ref Exec, o: ref Obj, arrayname: string, index: int) : ref Obj
+{
+	oarr := getobj(ex, o, arrayname);
+	if(oarr != nil) {
+		v := ES->get(ex, oarr, string index);
+		if(ES->isobj(v))
+			return ES->toObject(ex, v);
+	}
+	return nil;
+}
+
+# return "" if none such, or not a string
+getstr(ex : ref Exec, o: ref Obj, prop: string) : string
+{
+
+	if(o != nil) {
+		v := ES->get(ex, o, prop);
+		if(ES->isstr(v))
+			return ES->toString(ex, v);
+	}
+	return "";
+}
+
+# Property index, -1 if doesn't exist
+pind(o: ref Obj, prop: string) : int
+{
+	props := o.props;
+	for(i := 0; i < len props; i++){
+		if(props[i] != nil && props[i].name == prop)
+			return i;
+	}
+	return -1;
+}
+
+# Reinitialize property prop of object o to value v
+# (pay no attention to ReadOnly status, so can't use ES->put).
+# Assume the property exists already.
+reinitprop(o: ref Obj, prop: string, v: ref Val)
+{
+	i := pind(o, prop);
+	if(i < 0) {
+		# set up dummy ex for now - needs sorting out
+		ex := ref Exec;
+		ES->runtime(ex, nil, "missing property " + prop); # shouldn't happen
+	}
+	CU->assert(i >= 0);
+	o.props[i].val.val = v;
+}
+
+# Get the array object named aname from o, and also find its current
+# length value.  If there is any problem, return (nil, 0).
+getarraywithlen(ex : ref Exec, o: ref Obj, aname: string) : (ref Obj, int)
+{
+	varray := ES->get(ex, o, aname);
+	if(ES->isobj(varray)) {
+		oarray := ES->toObject(ex, varray);
+		vlen := ES->get(ex, oarray, "length");
+		if(vlen != ES->undefined)
+			return (oarray, ES->toInt32(ex, vlen));
+	}
+	return (nil, 0);	
+}
+
+# Put val v as property "index" of object oarray.
+# Also, if the name doesn't conflict with array properties, add the val as
+# a "name" property too
+arrayput(ex : ref Exec, oarray: ref Obj, index: int, name: string, v: ref Val)
+{
+	ES->put(ex, oarray, string index, v);
+	if (name != "length" && prop2index(name) == -1)
+		ES->put(ex, oarray, name, v);
+}
+
+prop2index(p: string): int
+{
+	if (p == nil)
+		return -1;
+	v := 0;
+	for (i := 0; i < len p; i++) {
+		c := p[i];
+		if (c < '0' || c > '9')
+			return -1;
+		v = 10 * v + c - '0';
+	}
+	return v;
+}
+
+# Instantiate window object.
+# mkhostobj has already put the property names and default initial values in;
+# we have to fill in the proper values.
+wininstant(sw: ref ScriptWin)
+{
+	ex := sw.ex;
+	w := ex.global;
+	f := sw.frame;
+
+	sw.error = 0;
+	prevkids := sw.kids;
+	sw.kids = nil;
+	sw.forms = nil;
+	sw.imgs = nil;
+	sw.active = 0;
+
+	# document to be init'd by xfertoscriptobjs - WRONG,
+	# has to be init'd up-front as one frame may refer
+	# to another's document object (esp. for document.write calls)
+	od := getobj(ex, w, "document");
+	if(od == nil) {
+		docv := ES->objval(mkhostobj(ex, "document"));
+		reinitprop(w, "document", docv);
+		od = getobj(ex, w, "document");
+		CU->assert(od != nil);
+	}
+
+	# frames[ ]
+	ao := ES->mkobj(ex.arrayproto, "Array");
+	ES->varinstant(ao, ES->DontEnum|ES->DontDelete, "length", ref RefVal(ES->numval(0.)));
+	reinitprop(w, "frames", ES->objval(ao));
+	for (kl := f.kids; kl != nil; kl = tl kl) {
+		klf := hd kl;
+		# look for original ScriptWin
+		for (oldkl := prevkids; oldkl != nil; oldkl = tl oldkl) {
+			oldksw := hd oldkl;
+			if (oldksw.frame == klf) {
+				wininstant(oldksw);
+				sw.kids = oldksw :: sw.kids;
+				break;
+			}
+		}
+		if (oldkl == nil)
+			sw.addkid(klf);
+	}
+	kn := 0;
+	for (swkl := sw.kids; swkl != nil; swkl = tl swkl) {
+		k := hd swkl;
+		# Yes, frame name should be defined as property of parent
+		arrayput(ex, ao, kn++, "", k.val);
+		if (k.frame != nil && k.frame.name != nil) {
+			ES->put(ex, ao, k.frame.name, k.val);
+			ES->varinstant(w, 0, k.frame.name, ref RefVal(k.val));
+		}
+	}
+
+	reinitprop(w, "length", ES->numval(real len f.kids));
+
+	v := ref Val;
+	if (sw.parent == nil)
+		v = ES->objval(w);
+	else
+		v = ES->objval(sw.parent.ex.global);
+	reinitprop(w, "parent", v);
+
+	if (f.name != nil)
+		reinitprop(w, "name", ES->strval(f.name));
+	reinitprop(w, "self", ES->objval(w));
+	reinitprop(w, "window", ES->objval(w));
+	reinitprop(w, "top", ES->objval(top.ex.global));
+	reinitprop(w, "Math", ES->get(ex, top.ex.global, "Math"));
+	reinitprop(w, "navigator", ES->get(ex, top.ex.global, "navigator"));
+}
+
+# Return initial document object value, based on d
+docinstant(ex: ref Exec, f: ref Layout->Frame) : ref Val
+{
+	od := mkhostobj(ex, "document");
+	docfill(ex, od, f);
+	return ES->objval(od);
+}
+
+# Fill in properties of doc object, based on d.
+# Can be called at various points during build.
+docfill(ex: ref Exec, od: ref Obj, f: ref Layout->Frame)
+{
+	sw := top.findbyframeid(f.id);
+	if(sw == nil)
+		return;
+	di := f.doc;
+	if(di.src != nil) {
+		reinitprop(od, "URL", ES->strval(di.src.tostring()));
+		reinitprop(od, "domain", ES->strval(di.src.host));
+	}
+	if(di.referrer != nil)
+		reinitprop(od, "referrer", ES->strval(di.referrer.tostring()));
+	if(di.doctitle != "")
+		reinitprop(od, "title", ES->strval(di.doctitle));
+	reinitprop(od, "lastModified", ES->strval(di.lastModified));
+	reinitprop(od, "bgColor", colorval(di.background.color));
+	reinitprop(od, "fgColor", colorval(di.text));
+	reinitprop(od, "alinkColor", colorval(di.alink));
+	reinitprop(od, "linkColor", colorval(di.link));
+	reinitprop(od, "vlinkColor", colorval(di.vlink));
+
+	# Forms in d.forms are in reverse order of appearance.
+	# Add any that aren't already in the document.forms object,
+	# assuming that the relative lengths will tell us what needs
+	# to be done.
+	if(di.forms != nil) {
+		newformslen := len di.forms;
+		oldformslen := len sw.forms;
+		oforms := getobj(ex, od, "forms");
+
+		# oforms should be non-nil, because the object is initialized
+		# to an empty array and is readonly.  The following test
+		# is just defensive.
+		if(oforms != nil) {
+			# run through our existing list of forms, looking
+			# for any not marked as Transferred (happens as a result
+			# of a script being called in the body of a form, while it is
+			# still being parsed)
+			for (sfl := sw.forms; sfl != nil; sfl = tl sfl) {
+				sf := hd sfl;
+				form := sf.form;
+				if (form.state != B->FormTransferred) {
+#					(sf.obj, sf.fields) = forminstant(ex, form, di.frameid);
+					(newobj, newfields) := forminstant(ex, form, di.frameid);
+					*sf.obj = *newobj;
+					sf.fields = newfields;
+				}
+				if (form.state == B->FormDone)
+					form.state = B->FormTransferred;
+			}
+
+			# process additional forms
+			fl := di.forms;
+			for(i := newformslen-1; i >= oldformslen; i--) {
+				form := hd fl;
+				fl = tl fl;
+				if (form.state != B->FormTransferred) {
+					sf := ref ScriptForm (form, nil, i, nil);
+					(sf.obj, sf.fields) = forminstant(ex, form, di.frameid);
+					arrayput(ex, oforms, i, form.name, ES->objval(sf.obj));
+					if(form.name != "")
+						ES->put(ex, od, form.name, ES->objval(sf.obj));
+					sw.forms = sf :: sw.forms;
+				}
+				if (form.state == B->FormDone)
+					form.state = B->FormTransferred;
+			}
+		}
+	}
+
+	# Charon calls "DestAnchor" what Netscape calls "Anchor".
+	# Use same method as for forms to discover new ones.
+	if(di.dests != nil) {
+		newdestslen := len di.dests;
+		(oanchors, oldanchorslen) := getarraywithlen(ex, od, "anchors");
+		if(oanchors != nil) {
+			dl := di.dests;
+			for(i := newdestslen-1; i >= oldanchorslen; i--) {
+				dest := hd dl;
+				dl = tl dl;
+				arrayput(ex, oanchors, i, dest.name, anchorinstant(ex, dest.name));
+			}
+		}
+	}
+
+	# Charon calls "Anchor" what Netscape calls "Link" (how confusing for us!).
+	# Use same method as for forms to discover new ones.
+	# BUG: Areas are supposed to be in this list too.
+	if(di.anchors != nil) {
+		newanchorslen := len di.anchors;
+		(olinks, oldlinkslen) := getarraywithlen(ex, od, "links");
+		if(olinks != nil) {
+			al := di.anchors;
+			for(i := newanchorslen-1; i >= oldlinkslen; i--) {
+				a := hd al;
+				al = tl al;
+				arrayput(ex, olinks, i, a.name,  linkinstant(ex, a, f.id));
+			}
+		}
+	}
+
+	if(di.images != nil) {
+		newimageslen := len di.images;
+		(oimages, oldimageslen) := getarraywithlen(ex, od, "images");
+		if(oimages != nil) {
+			il := di.images;
+			for(i := newimageslen-1; i >= oldimageslen; i--) {
+				imit := hd il;
+				il = tl il;
+				pick ii := imit {
+				Iimage =>
+					vim := imageinstant(ex, ii);
+					arrayput(ex, oimages, i, ii.name, vim);
+					ES->put(ex, od, ii.name, vim);
+					if(ES->isobj(vim)) {
+						sw.imgs = ref ScriptImg(ii, vim.obj) :: sw.imgs;
+					}
+				}
+			}
+		}
+	}
+
+	# private variables
+	ES->varinstant(od, ES->DontEnum|ES->DontDelete, "@PRIVframeid",
+			ref RefVal(ES->numval(real di.frameid)));
+}
+
+forminstant(ex : ref Exec, form: ref Build->Form, frameid: int) : (ref Obj, list of (ref Build->Formfield, ref Obj))
+{
+	fields : list of (ref Build->Formfield, ref ES->Obj);
+	oform := mkhostobj(ex, "Form");
+	reinitprop(oform, "action", ES->strval(form.action.tostring()));
+	reinitprop(oform, "encoding", ES->strval("application/x-www-form-urlencoded"));
+	reinitprop(oform, "length", ES->numval(real form.nfields));
+	reinitprop(oform, "method", ES->strval(CU->hmeth[form.method]));
+	reinitprop(oform, "name", ES->strval(form.name));
+	reinitprop(oform, "target", ES->strval(form.target));
+	ffl := form.fields;
+	if(ffl != nil) {
+		velements := ES->get(ex, oform, "elements");
+		if(ES->isobj(velements)) {
+			oelements := ES->toObject(ex, velements);
+			for(i := 0; i < form.nfields; i++) {
+				field := hd ffl;
+				ffl = tl ffl;
+				vfield := fieldinstant(ex, field, oform);
+
+				# convert multiple fields of same name to an array
+				prev := ES->get(ex, oform, field.name);
+				if (prev != nil && ES->isobj(prev)) {
+					newix := 0;
+					ar : ref Obj;
+					if (ES->isarray(prev.obj)) {
+						ar = prev.obj;
+						vlen := ES->get(ex, ar, "length");
+						newix = ES->toInt32(ex, vlen);
+					} else {
+						# create a new array
+						ar = ES->mkobj(ex.arrayproto, "Array");
+						ES->varinstant(ar, ES->DontEnum|ES->DontDelete, "length", ref RefVal(ES->numval(real 2)));
+						ES->put(ex, oform, field.name, ES->objval(ar));
+						arrayput(ex, ar, 0, "", prev);
+						newix = 1;
+					}
+					arrayput(ex, ar, newix, "", vfield);
+				} else {
+					# first time we have seen a field of this name
+					ES->put(ex, oform, field.name, vfield);
+				}
+				# although it is incorrect to add field name to
+				# elements array (as well as being indexed)
+				# - gives rise to name clashes, e.g radio buttons
+				# do it because other browsers do and some fools use it!
+				arrayput(ex, oelements, i, field.name, vfield);
+				fields = (field, ES->toObject(ex, vfield)) :: fields;
+			}
+		}
+	}
+	for(el := form.events; el != nil; el = tl el) {
+		e := hd el;
+		hname := "";
+		case e.attid {
+		Lex->Aonreset =>
+			hname = "onreset";
+			form.evmask |= E->SEonreset;
+		Lex->Aonsubmit =>
+			hname = "onsubmit";
+			form.evmask |= E->SEonsubmit;
+		}
+		if(hname != "")
+			puthandler(ex, oform, hname, e.value);
+	}
+#	form.events = nil;
+	# private variables
+	ES->varinstant(oform, ES->DontEnum|ES->DontDelete, "@PRIVformid",
+			ref RefVal(ES->numval(real form.formid)));
+	ES->varinstant(oform, ES->DontEnum|ES->DontDelete, "@PRIVframeid",
+			ref RefVal(ES->numval(real frameid)));
+	return (oform, fields);
+}
+
+fieldinstant(ex : ref Exec, field: ref Build->Formfield, oform: ref Obj) : ref Val
+{
+	ofield := mkhostobj(ex, "FormField");
+	reinitprop(ofield, "form", ES->objval(oform));
+	reinitprop(ofield, "name", ES->strval(field.name));
+	reinitprop(ofield, "value", ES->strval(field.value));
+	reinitprop(ofield, "defaultValue", ES->strval(field.value));
+	chkd := ES->false;
+	if((field.flags & Build->FFchecked) != byte 0)
+		chkd = ES->true;
+	reinitprop(ofield, "checked", chkd);
+	reinitprop(ofield, "defaultChecked", chkd);
+	nopts := len field.options;
+	reinitprop(ofield, "length", ES->numval(real nopts));
+	reinitprop(ofield, "selectedIndex", ES->numval(-1.0)); # BUG: search for selected option
+	ty : string;
+	case field.ftype {
+	Build->Ftext =>
+		ty = "text";
+		reinitprop(ofield, "value", ES->strval(field.value));
+	Build->Fpassword =>
+		ty = "password";
+	Build->Fcheckbox =>
+		ty = "checkbox";
+	Build->Fradio =>
+		ty = "radio";
+	Build->Fsubmit =>
+		ty = "submit";
+	Build->Fhidden =>
+		ty = "hidden";
+	Build->Fimage =>
+		ty = "image";
+	Build->Freset =>
+		ty = "reset";
+	Build->Ffile =>
+		ty = "fileupload";
+	Build->Fbutton =>
+		ty = "button";
+	Build->Fselect =>
+		ty = "select";
+		si := -1;
+		options := ES->mkobj(ex.arrayproto, "Array");
+		ES->varinstant(options, ES->DontEnum|ES->DontDelete, "length",
+					ref RefVal(ES->numval(real nopts)));
+		reinitprop(ofield, "options", ES->objval(options));
+		optl := field.options;
+		vfield := ES->objval(ofield);
+		for(i := 0; i < nopts; i++) {
+			opt := hd optl;
+			optl = tl optl;
+			oopt := mkhostobj(ex, "Option");
+			reinitprop(oopt, "index", ES->numval(real i));
+			reinitprop(oopt, "value", ES->strval(opt.value));
+			reinitprop(oopt, "text", ES->strval(opt.display));
+			# private variables
+			ES->put(ex, oopt, "@PRIVformfield", vfield);
+			if(opt.selected) {
+				si = i;
+				reinitprop(oopt, "selected", ES->true);
+				reinitprop(oopt, "defaultSelected", ES->true);
+				reinitprop(ofield, "selectedIndex", ES->numval(real i));
+			}
+			ES->put(ex, options, string i, ES->objval(oopt));
+		}
+		ES->put(ex, options, "selectedIndex", ES->numval(real si));
+		ES->put(ex, options, "@PRIVformfield", vfield);
+		options.host = me;
+	Build->Ftextarea =>
+		ty = "textarea";
+	}
+	reinitprop(ofield, "type", ES->strval(ty));
+	for(el := field.events; el != nil; el = tl el) {
+		e := hd el;
+		hname := "";
+		case e.attid {
+		Lex->Aonblur =>
+			hname = "onblur";
+			field.evmask |= E->SEonblur;
+		Lex->Aonchange =>
+			hname = "onchange";
+			field.evmask |= E->SEonchange;
+		Lex->Aonclick =>
+			hname = "onclick";
+			field.evmask |= E->SEonclick;
+		Lex->Aondblclick =>
+			hname = "ondblclick";
+			field.evmask |= E->SEondblclick;
+		Lex->Aonfocus =>
+			hname = "onfocus";
+			field.evmask |= E->SEonfocus;
+		Lex->Aonkeydown =>
+			hname = "onkeydown";
+			field.evmask |= E->SEonkeydown;
+		Lex->Aonkeypress =>
+			hname = "onkeypress";
+			field.evmask |= E->SEonkeypress;
+		Lex->Aonkeyup =>
+			hname = "onkeyup";
+			field.evmask |= E->SEonkeyup;
+		Lex->Aonmousedown =>
+			hname = "onmousedown";
+			field.evmask |= E->SEonmousedown;
+		Lex->Aonmouseup =>
+			hname = "onmouseup";
+			field.evmask |= E->SEonmouseup;
+		Lex->Aonselect =>
+			hname = "onselect";
+			field.evmask |= E->SEonselect;
+		}
+		if(hname != "")
+			puthandler(ex, ofield, hname, e.value);
+	}
+#	field.events = nil;
+	# private variables
+	ES->varinstant(ofield, ES->DontEnum|ES->DontDelete, "@PRIVfieldid",
+			ref RefVal(ES->numval(real field.fieldid)));
+	return ES->objval(ofield);
+}
+
+# Make an event handler named hname in o, with given body.
+puthandler(ex: ref Exec, o: ref Obj, hname: string, hbody: string)
+{
+	ES->eval(ex, "function PRIVhandler() {" + hbody + "}");
+	hobj := getobj(ex, ex.global, "PRIVhandler");
+	if(hobj != nil) {
+		ES->put(ex, o, hname, ES->objval(hobj));
+	}
+}
+
+anchorinstant(ex : ref Exec, nm: string) : ref Val
+{
+	oanchor := mkhostobj(ex, "Anchor");
+	reinitprop(oanchor, "name", ES->strval(nm));
+	return ES->objval(oanchor);
+}
+
+# Build ensures that the anchor href has been made absolute
+linkinstant(ex: ref Exec, anchor: ref Build->Anchor, frameid: int) : ref Val
+{
+	olink := mkhostobj(ex, "Link");
+	u := anchor.href;
+	if(u != nil) {
+		if(u.frag != "")
+			reinitprop(olink, "hash", ES->strval("#" + u.frag));
+		host := u.host;
+		if(u.user != "" || u.passwd != "") {
+			host = u.user;
+			if(u.passwd != "")
+				host += ":" + u.passwd;
+			host += "@" + u.host;
+		}
+		reinitprop(olink, "host",  ES->strval(host));
+		hostname := host;
+		if(u.port != "")
+			hostname += ":" + u.port;
+		reinitprop(olink, "hostname", ES->strval(hostname));
+		reinitprop(olink, "href", ES->strval(u.tostring()));
+		reinitprop(olink, "pathname", ES->strval(u.path));
+		if(u.port != "")
+			reinitprop(olink, "port", ES->strval(u.port));
+		reinitprop(olink, "protocol", ES->strval(u.scheme + ":"));
+		if(u.query != "")
+			reinitprop(olink, "search", ES->strval("?" + u.query));
+	}
+	if(anchor.target != "")
+		reinitprop(olink, "target", ES->strval(anchor.target));
+
+	for(el := anchor.events; el != nil; el = tl el) {
+		e := hd el;
+		hname := "";
+		case e.attid {
+		Lex->Aonclick =>
+			hname = "onclick";
+			anchor.evmask |= E->SEonclick;
+		Lex->Aondblclick =>
+			hname = "ondblclick";
+			anchor.evmask |= E->SEondblclick;
+		Lex->Aonkeydown =>
+			hname = "onkeydown";
+			anchor.evmask |= E->SEonkeydown;
+		Lex->Aonkeypress =>
+			hname = "onkeypress";
+			anchor.evmask |= E->SEonkeypress;
+		Lex->Aonkeyup =>
+			hname = "onkeyup";
+			anchor.evmask |= E->SEonkeyup;
+		Lex->Aonmousedown =>
+			hname = "onmousedown";
+			anchor.evmask |= E->SEonmousedown;
+		Lex->Aonmouseout =>
+			hname = "onmouseout";
+			anchor.evmask |= E->SEonmouseout;
+		Lex->Aonmouseover =>
+			hname = "onmouseover";
+			anchor.evmask |= E->SEonmouseover;
+		Lex->Aonmouseup =>
+			hname = "onmouseup";
+			anchor.evmask |= E->SEonmouseup;
+		}
+		if(hname != "")
+			puthandler(ex, olink, hname, e.value);
+	}
+	anchor.events = nil;
+	# private variable
+	ES->varinstant(olink, ES->DontEnum|ES->DontDelete, "@PRIVanchorid",
+			ref RefVal(ES->numval(real anchor.index)));
+	ES->varinstant(olink, ES->DontEnum|ES->DontDelete, "@PRIVframeid",
+			ref RefVal(ES->numval(real frameid)));
+
+	return ES->objval(olink);
+}
+
+imageinstant(ex: ref Exec, im: ref Build->Item.Iimage) : ref Val
+{
+	oim := mkhostobj(ex, "Image");
+	src := im.ci.src.tostring();
+	reinitprop(oim, "border", ES->numval(real im.border));
+	reinitprop(oim, "height", ES->numval(real im.imheight));
+	reinitprop(oim, "hspace", ES->numval(real im.hspace));
+	reinitprop(oim, "name", ES->strval(im.name));
+	reinitprop(oim, "src", ES->strval(src));
+	if(im.ci.lowsrc != nil)
+		reinitprop(oim, "lowsrc", ES->strval(im.ci.lowsrc.tostring()));
+	reinitprop(oim, "vspace", ES->numval(real im.vspace));
+	reinitprop(oim, "width", ES->numval(real im.imwidth));
+	if(im.ci.complete == 0)
+		done := ES->false;
+	else
+		done = ES->true;
+	reinitprop(oim, "complete", done);
+
+	el : list of Lex->Attr = nil;
+	if(im.genattr != nil)
+		el = im.genattr.events;
+	for(; el != nil; el = tl el) {
+		e := hd el;
+		hname := "";
+		case e.attid {
+		Lex->Aonabort =>
+			hname = "onabort";
+			im.genattr.evmask |= E->SEonabort;
+		Lex->Aondblclick =>
+			hname = "ondblclick";
+			im.genattr.evmask |= E->SEondblclick;
+		Lex->Aonerror =>
+			hname = "onerror";
+			im.genattr.evmask |= E->SEonerror;
+		Lex->Aonkeydown =>
+			hname = "onkeydown";
+			im.genattr.evmask |= E->SEonkeydown;
+		Lex->Aonkeypress =>
+			hname = "onkeypress";
+			im.genattr.evmask |= E->SEonkeypress;
+		Lex->Aonkeyup =>
+			hname = "onkeyup";
+			im.genattr.evmask |= E->SEonkeyup;
+		Lex->Aonload =>
+			hname = "onload";
+			im.genattr.evmask |= E->SEonload;
+		Lex->Aonmousedown =>
+			hname = "onmousedown";
+			im.genattr.evmask |= E->SEonmousedown;
+		Lex->Aonmouseout =>
+			hname = "onmouseout";
+			im.genattr.evmask |= E->SEonmouseout;
+		Lex->Aonmouseover =>
+			hname = "onmouseover";
+			im.genattr.evmask |= E->SEonmouseover;
+		Lex->Aonmouseup =>
+			hname = "onmouseup";
+			im.genattr.evmask |= E->SEonmouseup;
+		}
+		if(hname != "")
+			puthandler(ex, oim, hname, e.value);
+	}
+	if(im.genattr != nil)
+		im.genattr.events = nil;
+
+	# private variables
+	ES->varinstant(oim, ES->DontEnum|ES->DontDelete, "@PRIVimageid",
+			ref RefVal(ES->numval(real im.imageid)));
+	# to keep track of src as currently known in item
+#	ES->varinstant(oim, ES->DontEnum|ES->DontDelete, "@PRIVsrc",
+#			ref RefVal(ES->strval(src)));
+	return ES->objval(oim);
+}
+
+colorval(v: int) : ref Val
+{
+	return ES->strval(sys->sprint("%.6x", v));
+}
+
+# If the o.name is a recognizable color, return it, else dflt
+colorxfer(ex: ref Exec, o: ref Obj, name: string, dflt: int) : int
+{
+	v := ES->get(ex, o, name);
+	if(v == ES->undefined)
+		return dflt;
+	return CU->color(ES->toString(ex, v), dflt);
+}
+
+strxfer(ex : ref Exec, o: ref Obj, name: string, dflt: string) : string
+{
+	v := ES->get(ex, o, name);
+	if(v == ES->undefined)
+		return dflt;
+	return ES->toString(ex, v);
+}
+
+ScriptWin.new(f: ref Layout->Frame, ex: ref Exec, loc: ref Obj, par: ref ScriptWin) : ref ScriptWin
+{
+	return ref ScriptWin(f, ex, loc, ES->objval(ex.global), par, nil, nil, nil, "", "", "", 1, 0, 0, nil);
+}
+
+# Make a new ScriptWin with f as frame and new, empty
+# Window object as obj, to be a child window of sw's window.
+ScriptWin.addkid(sw: self ref ScriptWin, f: ref Layout->Frame)
+{
+	(cex, clocobj) := makeframeex(f);
+	csw := ScriptWin.new(f, cex, clocobj, sw);
+	wininstant(csw);
+	sw.kids = csw :: sw.kids;
+}
+
+ScriptWin.dummy(): ref ScriptWin
+{
+	f := ref Layout->Frame;
+	f.doc = ref Build->Docinfo;
+	f.doc.base = U->parse("");
+	f.doc.src = U->parse("");
+	(cex, clocobj) := makeframeex(f);
+	csw := ScriptWin.new(f, cex, clocobj, nil);
+	wininstant(csw);
+	return csw;
+}
+
+# Find the ScriptWin in the tree with sw as root that has
+# f as frame, returning nil if none.
+#ScriptWin.findbyframe(sw: self ref ScriptWin, f: ref Layout->Frame) : ref ScriptWin
+#{
+#	if(sw.frame.id == f.id)
+#		return sw;
+#	for(l := sw.kids; l != nil; l = tl l) {
+#		x := (hd l).findbyframe(f);
+#		if(x != nil)
+#			return x;
+#	}
+#	return nil;
+#}
+
+# Find the ScriptWin in the tree with sw as root that has
+# fid as frame id, returning nil if none.
+ScriptWin.findbyframeid(sw: self ref ScriptWin, fid: int) : ref ScriptWin
+{
+	if(sw.frame.id == fid)
+		return sw;
+	for(l := sw.kids; l != nil; l = tl l) {
+		x := (hd l).findbyframeid(fid);
+		if(x != nil)
+			return x;
+	}
+	return nil;
+}
+
+# Find the ScriptWin in the tree with sw as root that has
+# d as doc for the frame, returning nil if none.
+ScriptWin.findbydoc(sw: self ref ScriptWin, d: ref Build->Docinfo) : ref ScriptWin
+{
+	if(sw.frame.doc == d)
+		return sw;
+	for(l := sw.kids; l != nil; l = tl l) {
+		x := (hd l).findbydoc(d);
+		if(x != nil)
+			return x;
+	}
+	return nil;
+}
+
+# obj can either be the frame's Window obj, Location obj, or document obj,
+# or an Image object within the frame
+ScriptWin.findbyobj(sw : self ref ScriptWin, obj : ref Obj) : ref ScriptWin
+{
+	if (sw.locobj == obj || sw.ex.global == obj || obj == getdocobj(sw.ex, sw.frame.id))
+		return sw;
+	if(opener != nil && (opener.locobj == obj || opener.ex.global == obj))
+		return opener;
+	for(sil := sw.imgs; sil != nil; sil = tl sil) {
+		if((hd sil).obj == obj)
+			return sw;
+	}
+	for (l := sw.kids; l != nil; l = tl l) {
+		x := (hd l).findbyobj(obj);
+		if (x != nil)
+			return x;
+	}
+	return nil;
+}
+
+ScriptWin.findbyname(sw : self ref ScriptWin, name : string) : ref ScriptWin
+{
+	if (sw.frame != nil && sw.frame.name == name)
+		return sw;
+	for (l := sw.kids; l != nil; l = tl l) {
+		x := (hd l).findbyname(name);
+		if (x != nil)
+			return x;
+	}
+	return nil;
+}
+
+newcharon(url: string, nm: string, sw: ref ScriptWin)
+{
+	cs := chan of string;
+
+	spawn CH->startcharon(url, cs);
+	for(;;){
+		alt{
+			s := <- cs =>
+				if(s == "B")
+					continue;
+				if(s == "E")
+					exit;
+				(nil, l) := sys->tokenize(s, " ");
+				case hd l{
+					"L" =>
+						sw.newloc = hd tl l;
+						sw.newloctarg = nm;
+						checknewlocs(sw);
+				}
+		}
+	}
+}
--- /dev/null
+++ b/appl/charon/layout.b
@@ -1,0 +1,4832 @@
+implement Layout;
+
+include "common.m";
+include "keyboard.m";
+
+sys: Sys;
+CU: CharonUtils;
+	ByteSource, MaskedImage, CImage, ImageCache, max, min,
+	White, Black, Grey, DarkGrey, LightGrey, Blue, Navy, Red, Green, DarkRed: import CU;
+
+D: Draw;
+	Point, Rect, Font, Image, Display: import D;
+S: String;
+T: StringIntTab;
+U: Url;
+	Parsedurl: import U;
+I: Img;
+	ImageSource: import I;
+J: Script;
+E: Events;
+	Event: import E;
+G: Gui;
+	Popup: import G;
+B: Build;
+
+# B : Build, declared in layout.m so main program can use it
+	Item, ItemSource,
+	IFbrk, IFbrksp, IFnobrk, IFcleft, IFcright, IFwrap, IFhang,
+	IFrjust, IFcjust, IFsmap, IFindentshift, IFindentmask,
+	IFhangmask,
+	Voffbias,
+	ISPnull, ISPvline, ISPhspace, ISPgeneral,
+	Align, Dimen, Formfield, Option, Form,
+	Table, Tablecol, Tablerow, Tablecell,
+	Anchor, DestAnchor, Map, Area, Kidinfo, Docinfo,
+	Anone, Aleft, Acenter, Aright, Ajustify, Achar, Atop, Amiddle,
+	Abottom, Abaseline,
+	Dnone, Dpixels, Dpercent, Drelative,
+	Ftext, Fpassword, Fcheckbox, Fradio, Fsubmit, Fhidden, Fimage,
+	Freset, Ffile, Fbutton, Fselect, Ftextarea,
+	Background,
+	FntR, FntI, FntB, FntT, NumStyle,
+	Tiny, Small, Normal, Large, Verylarge, NumSize, NumFnt, DefFnt,
+	ULnone, ULunder, ULmid,
+	FRnoresize, FRnoscroll, FRhscroll, FRvscroll,
+	FRhscrollauto, FRvscrollauto
+    : import B;
+
+# font stuff
+Fontinfo : adt {
+	name:	string;
+	f:	ref Font;
+	spw:	int;			# width of a space in this font
+};
+
+fonts := array[NumFnt] of {
+	FntR*NumSize+Tiny => Fontinfo("/fonts/charon/plain.tiny.font", nil, 0),
+	FntR*NumSize+Small => ("/fonts/charon/plain.small.font", nil, 0),
+	FntR*NumSize+Normal => ("/fonts/charon/plain.normal.font", nil, 0),
+	FntR*NumSize+Large => ("/fonts/charon/plain.large.font", nil, 0),
+	FntR*NumSize+Verylarge => ("/fonts/charon/plain.vlarge.font", nil, 0),
+	
+	FntI*NumSize+Tiny => ("/fonts/charon/italic.tiny.font", nil, 0),
+	FntI*NumSize+Small => ("/fonts/charon/italic.small.font", nil, 0),
+	FntI*NumSize+Normal => ("/fonts/charon/italic.normal.font", nil, 0),
+	FntI*NumSize+Large => ("/fonts/charon/italic.large.font", nil, 0),
+	FntI*NumSize+Verylarge => ("/fonts/charon/italic.vlarge.font", nil, 0),
+	
+	FntB*NumSize+Tiny => ("/fonts/charon/bold.tiny.font", nil, 0),
+	FntB*NumSize+Small => ("/fonts/charon/bold.small.font", nil, 0),
+	FntB*NumSize+Normal => ("/fonts/charon/bold.normal.font", nil, 0),
+	FntB*NumSize+Large => ("/fonts/charon/bold.large.font", nil, 0),
+	FntB*NumSize+Verylarge => ("/fonts/charon/bold.vlarge.font", nil, 0),
+	
+	FntT*NumSize+Tiny => ("/fonts/charon/cw.tiny.font", nil, 0),
+	FntT*NumSize+Small => ("/fonts/charon/cw.small.font", nil, 0),
+	FntT*NumSize+Normal => ("/fonts/charon/cw.normal.font", nil, 0),
+	FntT*NumSize+Large => ("/fonts/charon/cw.large.font", nil, 0),
+	FntT*NumSize+Verylarge => ("/fonts/charon/cw.vlarge.font", nil, 0)
+};
+
+# Seems better to use a slightly smaller font in Controls, to match other browsers
+CtlFnt: con (FntR*NumSize+Small);
+
+# color stuff.  have hash table mapping RGB values to D->Image for that color
+Colornode : adt {
+	rgb:	int;
+	im:	ref Image;
+	next:	ref Colornode;
+};
+
+# Source of info for page (html, image, etc.)
+Source: adt {
+	bs:	ref ByteSource;
+	redirects:	int;
+	pick {
+		Srequired or
+		Shtml =>
+			itsrc: ref ItemSource;
+		Simage =>
+			ci: ref CImage;
+			itl: list of ref Item;
+			imsrc: ref ImageSource;
+	}
+};
+
+Sources: adt {
+	main: ref Source;
+	reqd: ref Source;
+	srcs: list of ref Source;
+
+	new: fn(m : ref Source) : ref Sources;
+	add: fn(srcs: self ref Sources, s: ref Source, required: int);
+	done: fn(srcs: self ref Sources, s: ref Source);
+	waitsrc: fn(srcs : self ref Sources) : ref Source;
+};
+
+NCOLHASH : con 19;	# 19 checked for standard colors: only 1 collision
+colorhashtab := array[NCOLHASH] of ref Colornode;
+
+# No line break should happen between adjacent characters if
+# they are 'wordchars' : set in this array, or outside the array range.
+# We include certain punctuation characters that are not traditionally
+# regarded as 'word' characters.
+wordchar := array[16rA0] of {
+	'!' => byte 1, 
+	'0'=>byte 1, '1'=>byte 1, '2'=>byte 1, '3'=>byte 1, '4'=>byte 1,
+	'5'=>byte 1, '6'=>byte 1, '7'=>byte 1, '8'=>byte 1, '9'=>byte 1,
+	':'=>byte 1, ';' => byte 1,
+	'?' => byte 1,
+	'A'=>byte 1, 'B'=>byte 1, 'C'=>byte 1, 'D'=>byte 1, 'E'=>byte 1, 'F'=>byte 1,
+	'G'=>byte 1, 'H'=>byte 1, 'I'=>byte 1, 'J'=>byte 1, 'K'=>byte 1, 'L'=>byte 1,
+	'M'=>byte 1, 'N'=>byte 1, 'O'=>byte 1, 'P'=>byte 1, 'Q'=>byte 1, 'R'=>byte 1,
+	'S'=>byte 1, 'T'=>byte 1, 'U'=>byte 1, 'V'=>byte 1, 'W'=>byte 1, 'X'=>byte 1,
+	'Y'=>byte 1, 'Z'=>byte 1,
+	'a'=>byte 1, 'b'=>byte 1, 'c'=>byte 1, 'd'=>byte 1, 'e'=>byte 1, 'f'=>byte 1,
+	'g'=>byte 1, 'h'=>byte 1, 'i'=>byte 1, 'j'=>byte 1, 'k'=>byte 1, 'l'=>byte 1,
+	'm'=>byte 1, 'n'=>byte 1, 'o'=>byte 1, 'p'=>byte 1, 'q'=>byte 1, 'r'=>byte 1,
+	's'=>byte 1, 't'=>byte 1, 'u'=>byte 1, 'v'=>byte 1, 'w'=>byte 1, 'x'=>byte 1,
+	'y'=>byte 1, 'z'=>byte 1,
+	'_'=>byte 1,
+	'\''=>byte 1, '"'=>byte 1, '.'=>byte 1, ','=>byte 1, '('=>byte 1, ')'=>byte 1,
+	* => byte 0
+};
+
+TABPIX: con 30;		# number of pixels in a tab
+CAPSEP: con 5;			# number of pixels separating tab from caption
+SCRBREADTH: con 14;	# scrollbar breadth (normal)
+SCRFBREADTH: con 14;	# scrollbar breadth (inside child frame or select control)
+FRMARGIN: con 0;		# default margin around frames
+RULESP: con 7;			# extra space before and after rules
+POPUPLINES: con 12;	# number of lines in popup select list
+MINSCR: con 6;			# min size in pixels of scrollbar drag widget
+SCRDELTASF: con 10000;	# fixed-point scale factor for scrollbar per-pixel step
+
+# all of the following include room for relief
+CBOXWID: con 14;		# check box width
+CBOXHT: con 12;		# check box height
+ENTVMARGIN : con 4;	# vertical margin inside entry box
+ENTHMARGIN : con 6;	# horizontal margin inside entry box
+SELMARGIN : con 4;		# margin inside select control
+BUTMARGIN: con 4;		# margin inside button control
+PBOXWID: con 10;		# progress box width
+PBOXHT: con 16;		# progress box height
+PBOXBD: con 2;		# progress box border width
+
+TABLEMAXTARGET: con 2000;	# targetwidth to get max width of table cell
+TABLEFLOATTARGET: con 1;	# targetwidth for floating tables
+
+SELBG: con 16r00FFFF;	# aqua
+
+ARPAUSE : con 500;			# autorepeat initial delay (ms)
+ARTICK : con 100;			# autorepeat tick delay (ms)
+
+display: ref D->Display;
+
+dbg := 0;
+dbgtab := 0;
+dbgev := 0;
+linespace := 0;
+lineascent := 0;
+charspace := 0;
+spspace := 0;
+ctllinespace := 0;
+ctllineascent := 0;
+ctlcharspace := 0;
+ctlspspace := 0;
+frameid := 0;
+zp := Point(0,0);
+
+init(cu: CharonUtils)
+{
+	CU = cu;
+	sys = load Sys Sys->PATH;
+	D = load Draw Draw->PATH;
+	S = load String String->PATH;
+	T = load StringIntTab StringIntTab->PATH;
+	U = load Url Url->PATH;
+	if (U != nil)
+		U->init();
+	E = cu->E;
+	G = cu->G;
+	I = cu->I;
+	J = cu->J;
+	B = cu->B;
+	display = G->display;
+
+	# make sure default and control fonts are loaded
+	getfont(DefFnt);
+	fnt := fonts[DefFnt].f;
+	linespace = fnt.height;
+	lineascent = fnt.ascent;
+	charspace = fnt.width("a");	# a kind of average char width
+	spspace = fonts[DefFnt].spw;
+	getfont(CtlFnt);
+	fnt = fonts[CtlFnt].f;
+	ctllinespace = fnt.height;
+	ctllineascent = fnt.ascent;
+	ctlcharspace = fnt.width("a");
+	ctlspspace = fonts[CtlFnt].spw;
+}
+
+stringwidth(s: string): int
+{
+	return fonts[DefFnt].f.width(s)/charspace;
+}
+
+# Use bsmain to fill frame f.
+# Return buffer containing source when done.
+layout(f: ref Frame, bsmain: ref ByteSource, linkclick: int) : array of byte
+{
+	dbg = int (CU->config).dbg['l'];
+	dbgtab = int (CU->config).dbg['t'];
+	dbgev = int (CU->config).dbg['e'];
+	if(dbgev)
+		CU->event("LAYOUT", 0);
+	sources : ref Sources;
+	hdr := bsmain.hdr;
+	auth := "";
+	url : ref Parsedurl;
+	if (bsmain.req != nil) {
+		auth = bsmain.req.auth;
+		url = bsmain.req.url;
+	}
+#	auth := bsmain.req.auth;
+	ans : array of byte = nil;
+	di := Docinfo.new();
+	if(linkclick && f.doc != nil)
+		di.referrer = f.doc.src;
+	f.reset();
+	f.doc = di;
+	di.frameid = f.id;
+	di.src = hdr.actual;
+	di.base = hdr.base;
+	di.refresh = hdr.refresh;
+	if (hdr.chset != nil)
+		di.chset = hdr.chset;
+	di.lastModified = hdr.lastModified;
+	if(J != nil)
+		J->havenewdoc(f);
+	oclipr := f.cim.clipr;
+	f.cim.clipr = f.cr;
+	if(f.framebd != 0) {
+		f.cr = f.r.inset(2);
+		drawborder(f.cim, f.cr, 2, DarkGrey);
+	}
+	fillbg(f, f.cr);
+	G->flush(f.cr);
+	f.cim.clipr = oclipr;
+	if(f.flags&FRvscroll)
+		createvscroll(f);
+	if(f.flags&FRhscroll)
+		createhscroll(f);
+	l := Lay.new(f.cr.dx(), Aleft, f.marginw, di.background);
+	f.layout = l;
+	anyanim := 0;
+	if(hdr.mtype == CU->TextHtml || hdr.mtype == CU->TextPlain) {
+		itsrc := ItemSource.new(bsmain, f, hdr.mtype);
+		sources = Sources.new(ref Source.Shtml(bsmain, 0, itsrc));
+	}
+	else {
+		# for now, must be supported image type
+		if(!I->supported(hdr.mtype)) {
+			sys->print("Need to implement something: source isn't supported image type\n");
+			return nil;
+		}
+		imsrc := I->ImageSource.new(bsmain, 0, 0);
+		ci := CImage.new(url, nil, 0, 0);
+		simage := ref Source.Simage(bsmain, 0, ci, nil, imsrc);
+		sources = Sources.new(simage);
+		it := ref Item.Iimage(nil, 0, 0, 0, 0, 0, nil, len di.images, ci, 0, 0, "", nil, nil, -1, Abottom, byte 0, byte 0, byte 0);
+		di.images = it :: nil;
+		appenditems(f, l, it);
+		simage.itl = it :: nil;
+	}
+	while ((src := sources.waitsrc()) != nil) {
+		if(dbgev)
+			CU->event("LAYOUT GETSOMETHING", 0);
+		bs := src.bs;
+		freeit := 0;
+		if(bs.err != "") {
+			if(dbg)
+				sys->print("error getting %s: %s\n", bs.req.url.tostring(), bs.err);
+			pick s := src {
+			Srequired =>
+				s.itsrc.reqddata = array [0] of byte;
+				sources.done(src);
+				CU->freebs(bs);
+				src.bs = nil;
+				continue;
+			}
+			freeit = 1;
+		}
+		else {
+			if(bs.hdr != nil && !bs.seenhdr) {
+				(use, error, challenge, newurl) := CU->hdraction(bs, 0, src.redirects);
+				if(challenge != nil) {
+					sys->print("Need to implement authorization credential dialog\n");
+					error = "Need authorization";
+					use = 0;
+				}
+				if(error != "" && dbg)
+					sys->print("subordinate error: %s\n", error);
+				if(newurl != nil) {
+					s := ref *src;
+					freeit = 1;
+					pick ps := src {
+					Shtml or Srequired =>
+						sys->print("unexpected redirect of subord\n");
+					Simage =>
+						newci := CImage.new(newurl, nil, ps.ci.width, ps.ci.height);
+						for(itl := ps.itl; itl != nil ; itl = tl itl) {
+							pick imi := hd itl {
+							Iimage =>
+								imi.ci = newci;
+							}
+						}
+						news := ref Source.Simage(nil, 0, newci, ps.itl, nil);
+						sources.add(news, 0);
+						startimreq(news, auth);
+					}
+				}
+				if(!use)
+					freeit = 1;
+			}
+			if(!freeit) {
+				pick s := src {
+				Srequired or
+				Shtml =>
+					if (tagof src == tagof Source.Srequired) {
+						s.itsrc.reqddata = bs.data;
+						sources.done(src);
+						CU->freebs(bs);
+						src.bs = nil;
+						continue;
+#						src = sources.main;
+#						CU->assert(src != nil);
+					}
+					itl := s.itsrc.getitems();
+					if(di.kidinfo != nil) {
+						if(s.itsrc.kidstk == nil) {
+							layframeset(f, di.kidinfo);
+							G->flush(f.r);
+							freeit = 1;
+						}
+					}
+					else {
+						l.background = di.background;
+						anyanim |= addsubords(sources, di, auth);
+						if(itl != nil) {
+							appenditems(f, l, itl);
+							fixframegeom(f);
+							if(dbgev)
+								CU->event("LAYOUT_DRAWALL", 0);
+							f.dirty(f.totalr);
+							drawall(f);
+						}
+					}
+					if (s.itsrc.reqdurl != nil) {
+						news := ref Source.Srequired(nil, 0, s.itsrc);
+						sources.add(news, 1);
+						rbs := CU->startreq(ref CU->ReqInfo(s.itsrc.reqdurl, CU->HGet, nil, "", ""));
+						news.bs = rbs;
+					} else {
+						if (bs.eof && bs.lim == bs.edata && s.itsrc.toks == nil)
+							freeit = 1;
+					}
+				Simage =>
+					(ret, mim) := s.imsrc.getmim();
+					# mark it done even if error
+					s.ci.complete = ret;
+					if(ret == I->Mimerror) {
+						bs.err = s.imsrc.err;
+						freeit = 1;
+					}
+					else if(ret != I->Mimnone) {
+						if(s.ci.mims == nil) {
+							s.ci.mims = array[1] of { mim };
+							s.ci.width = s.imsrc.width;
+							s.ci.height = s.imsrc.height;
+							if(ret == I->Mimdone && (CU->config).imagelvl <= CU->ImgNoAnim)
+								freeit = 1;
+						}
+						else {
+							n := len s.ci.mims;
+							if(mim != s.ci.mims[n-1]) {
+								newmims := array[n + 1] of ref MaskedImage;
+								newmims[0:] = s.ci.mims;
+								newmims[n] = mim;
+								s.ci.mims = newmims;
+								anyanim = 1;
+							}
+						}
+						if(s.ci.mims[0] == mim)
+							haveimage(f, s.ci, s.itl);
+						if(bs.eof && bs.lim == bs.edata)
+							(CU->imcache).add(s.ci);
+					}
+					if(!freeit && bs.eof && bs.lim == bs.edata)
+						freeit = 1;
+				}
+			}
+		}
+		if(freeit) {
+			if(bs == bsmain)
+				ans = bs.data[0:bs.edata];
+			CU->freebs(bs);
+			src.bs = nil;
+			sources.done(src);
+		}
+	}
+	if(anyanim && (CU->config).imagelvl > CU->ImgNoAnim)
+		spawn animproc(f);
+	if(dbgev)
+		CU->event("LAYOUT_END", 0);
+	return ans;
+}
+
+# return value is 1 if found any existing images needed animation
+addsubords(sources: ref Sources, di: ref Docinfo, auth: string) : int
+{
+	anyanim := 0;
+	if((CU->config).imagelvl == CU->ImgNone)
+		return anyanim;
+	newsims: list of ref Source.Simage = nil;
+	for(il := di.images; il != nil; il = tl il) {
+		it := hd il;
+		pick i := it {
+		Iimage =>
+			if(i.ci.mims == nil) {
+				cachedci := (CU->imcache).look(i.ci);
+				if(cachedci != nil) {
+					i.ci = cachedci;
+					if(i.imwidth == 0)
+						i.imwidth = i.ci.width;
+					if(i.imheight == 0)
+						i.imheight = i.ci.height;
+					anyanim |= (len cachedci.mims > 1);
+				}
+				else {
+				    sloop:
+					for(sl := sources.srcs; sl != nil; sl = tl sl) {
+						pick s := hd sl {
+						Simage =>
+							if(s.ci.match(i.ci)) {
+								s.itl = it :: s.itl;
+								# want all items on list to share same ci;
+								# want most-specific dimension specs
+								iciw := i.ci.width;
+								icih := i.ci.height;
+								i.ci = s.ci;
+								if(s.ci.width == 0 && s.ci.height == 0) {
+									s.ci.width = iciw;
+									s.ci.height = icih;
+								}
+								break sloop;
+							}
+						}
+					}
+					if(sl == nil) {
+						# didn't find existing Source for this image
+						s := ref Source.Simage(nil, 0, i.ci, it:: nil, nil);
+						newsims = s :: newsims;
+						sources.add(s, 0);
+					}
+				}
+			}
+		}
+	}
+	# Start requests for new newsources.
+	# di.images are in last-in-document-first order,
+	# so newsources is in first-in-document-first order (good order to load in).
+	for(sl := newsims; sl != nil; sl = tl sl)
+		startimreq(hd sl, auth);
+	return anyanim;
+}
+
+startimreq(s: ref Source.Simage, auth: string)
+{
+	if(dbgev)
+		CU->event(sys->sprint("LAYOUT STARTREQ %s", s.ci.src.tostring()), 0);
+	bs := CU->startreq(ref CU->ReqInfo(s.ci.src, CU->HGet, nil, auth, ""));
+	s.bs = bs;
+	s.imsrc = I->ImageSource.new(bs, s.ci.width, s.ci.height);
+}
+
+createvscroll(f: ref Frame)
+{
+	breadth := SCRBREADTH;
+	if(f.parent != nil)
+		breadth = SCRFBREADTH;
+	length := f.cr.dy();
+	if(f.flags&FRhscroll)
+		length -= breadth;
+	f.vscr = Control.newscroll(f, 1, length, breadth);
+	f.vscr.r = f.vscr.r.addpt(Point(f.cr.max.x-breadth, f.cr.min.y));
+	f.cr.max.x -= breadth;
+	if(f.cr.dx() <= 2*f.marginw)
+		raise "EXInternal: frame too small for layout";
+	f.vscr.draw(1);
+}
+
+createhscroll(f: ref Frame)
+{
+	breadth := SCRBREADTH;
+	if(f.parent != nil)
+		breadth = SCRFBREADTH;
+	length := f.cr.dx();
+	x := f.cr.min.x;
+	f.hscr = Control.newscroll(f, 0, length, breadth);
+	f.hscr.r = f.hscr.r.addpt(Point(x,f.cr.max.y-breadth));
+	f.cr.max.y -= breadth;
+	if(f.cr.dy() <= 2*f.marginh)
+		raise "EXInternal: frame too small for layout";
+	f.hscr.draw(1);
+}
+
+# Call after a change to f.layout or f.viewr.min to fix totalr and viewr
+# (We are to leave viewr.min unchanged, if possible, as
+# user might be scrolling).
+fixframegeom(f: ref Frame)
+{
+	l := f.layout;
+	if(dbg)
+		sys->print("fixframegeom, layout width=%d, height=%d\n", l.width, l.height);
+	crwidth := f.cr.dx();
+	crheight := f.cr.dy();
+	layw := max(l.width, crwidth);
+	layh := max(l.height, crheight);
+	f.totalr.max = Point(layw, layh);
+	crchanged := 0;
+	n := l.height+l.margin-crheight;
+	if(n > 0 && f.vscr == nil && (f.flags&FRvscrollauto)) {
+		createvscroll(f);
+		crchanged = 1;
+		crwidth = f.cr.dx();
+	}
+	if(f.viewr.min.y > n)
+		f.viewr.min.y = max(0, n);
+	n = l.width+l.margin-crwidth;
+	if(!crchanged && n > 0 && f.hscr == nil && (f.flags&FRhscrollauto)) {
+		createhscroll(f);
+		crchanged = 1;
+		crheight = f.cr.dy();
+	}
+	if(crchanged) {
+		relayout(f, l, crwidth, l.just);
+		fixframegeom(f);
+		return;
+	}
+	if(f.viewr.min.x > n)
+		f.viewr.min.x = max(0, n);
+	f.viewr.max.x = min(f.viewr.min.x+crwidth, layw);
+	f.viewr.max.y = min(f.viewr.min.y+crheight, layh);
+	if(f.vscr != nil)
+		f.vscr.scrollset(f.viewr.min.y, f.viewr.max.y, f.totalr.max.y, 0, 1);
+	if(f.hscr != nil)
+		f.hscr.scrollset(f.viewr.min.x, f.viewr.max.x, f.totalr.max.x, f.viewr.dx()/5, 1);
+}
+
+# The items its within f are Iimage items,
+# and its image, ci, now has at least a ci.mims[0], which may be partially
+# or fully filled.
+haveimage(f: ref Frame, ci: ref CImage, itl: list of ref Item)
+{
+	if(dbgev)
+		CU->event("HAVEIMAGE", 0);
+	if(dbg)
+		sys->print("\nHAVEIMAGE src=%s w=%d h=%d\n", ci.src.tostring(), ci.width, ci.height);
+	# make all base images repl'd - makes handling backgrounds much easier
+	im := ci.mims[0].im;
+	im.repl = 1;
+	im.clipr = Rect((-16rFFFFFFF, -16r3FFFFFFF), (16r3FFFFFFF, 16r3FFFFFFF));
+	dorelayout := 0;
+	for( ; itl != nil; itl = tl itl) {
+		it := hd itl;
+		pick i := it {
+		Iimage =>
+			if (!(it.state & B->IFbkg)) {
+				# If i.imwidth and i.imheight are not both 0, the HTML specified the dimens.
+				# If one of them is 0, the other is to be scaled by the same factor;
+				# we have to relay the line in that case too.
+				if(i.imwidth == 0 || i.imheight == 0) {
+					i.imwidth = ci.width;
+					i.imheight = ci.height;
+					setimagedims(i);
+					loc := f.find(zp, it);
+					# sometimes the image was added to doc image list, but
+					# never made it to layout (e.g., because html bug prevented
+					# a table from being added).
+					# also, script-created images won't have items
+					if(loc != nil) {
+						f.layout.flags |= Lchanged;
+						markchanges(loc);
+						dorelayout = 1;
+						# Floats are assumed to be premeasured, so if there
+						# are any floats in the loc list, remeasure them
+						for(k := loc.n-1; k > 0; k--) {
+							if(loc.le[k].kind == LEitem) {
+								locit := loc.le[k].item;
+								pick fit := locit {
+								Ifloat =>
+									pick xi := fit.item {
+									Iimage =>
+										fit.height = fit.item.height;
+									Itable =>
+										checktabsize(f, xi, TABLEFLOATTARGET);
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+			if(dbg > 1) {
+				sys->print("\nhaveimage item: ");
+				it.print();
+			}
+		}
+	}
+	if(dorelayout) {
+		relayout(f, f.layout, f.layout.targetwidth, f.layout.just);
+		fixframegeom(f);
+	}
+	f.dirty(f.totalr);
+	drawall(f);
+	if(dbgev)
+		CU->event("HAVEIMAGE_END", 0);
+}
+# For first layout of subelements, such as table cells.
+# After this, content items will be dispersed throughout resulting lay.
+# Return index into f.sublays.
+# (This roundabout way of storing sublayouts avoids pointers to Lay
+# in Build, so that all of the layout-related stuff can be in Layout
+# where it belongs.)
+sublayout(f: ref Frame, targetwidth: int, just: byte, bg: Background, content: ref Item) : int
+{
+	if(dbg)
+		sys->print("sublayout, targetwidth=%d\n", targetwidth);
+	l := Lay.new(targetwidth, just, 0, bg);
+	if(f.sublayid >= len f.sublays) {
+		newsublays := array[len f.sublays + 30] of ref Lay;
+		newsublays[0:] = f.sublays;
+		f.sublays = newsublays;
+	}
+	id := f.sublayid;
+	f.sublays[id] = l;
+	f.sublayid++;
+	appenditems(f, l, content);
+	l.flags &= ~Lchanged;
+	if(dbg)
+		sys->print("after sublayout, width=%d\n", l.width);
+	return id;
+}
+
+# Relayout of lay, given a new target width or if something changed inside
+# or if the global justification for the layout changed.
+# Floats are hard: for now, just relay everything with floats temporarily
+# moved way down, if there are any floats.
+relayout(f: ref Frame, lay: ref Lay, targetwidth: int, just: byte)
+{
+	if(dbg)
+		sys->print("relayout, targetwidth=%d, old target=%d, changed=%d\n",
+			targetwidth, lay.targetwidth, (lay.flags&Lchanged) != byte 0);
+	changeall := (lay.targetwidth != targetwidth || lay.just != just);
+	if(!changeall && !int(lay.flags&Lchanged))
+		return;
+	if(lay.floats != nil) {
+		# move the current y positions of floats to a big value,
+		# so they don't contribute to floatw until after they've
+		# been encountered in current fixgeom
+		for(flist := lay.floats; flist != nil; flist = tl flist) {
+			ff := hd flist;
+			ff.y = 16r6fffffff;
+		}
+		changeall = 1;
+	}
+	lay.targetwidth = targetwidth;
+	lay.just = just;
+	lay.height = 0;
+	lay.width = 0;
+	if(changeall)
+		changelines(lay.start.next, lay.end);
+	fixgeom(f, lay, lay.start.next);
+	lay.flags &= ~Lchanged;
+	if(dbg)
+		sys->print("after relayout, width=%d\n", lay.width);
+}
+
+# Measure and append the items to the end of layout lay,
+# and fix the geometry.
+appenditems(f: ref Frame, lay: ref Lay, items: ref Item)
+{
+	measure(f, items);
+	if(dbg)
+		items.printlist("appenditems, after measure");
+	it := items;
+	if(it == nil)
+		return;
+	lprev := lay.end.prev;
+	l : ref Line;
+	lit := lastitem(lprev.items);
+	if(lit == nil || (it.state&IFbrk)) {
+		# start a new line after existing last line
+		l = Line.new();
+		appendline(lprev, l);
+		l.items = it;
+	}
+	else {
+		# start appending items to existing last line
+		l = lprev;
+		lit.next = it;
+	}
+	l.flags |= Lchanged;
+	while(it != nil) {
+		nexti := it.next;
+		if(nexti == nil || (nexti.state&IFbrk)) {
+			it.next = nil;
+			fixgeom(f, lay, l);
+			if(nexti == nil)
+				break;
+			# now there may be multiple lines containing the
+			# items from l, but the one after the last is lay.end
+			l = Line.new();
+			appendline(lay.end.prev, l);
+			l.flags |= Lchanged;
+			it = nexti;
+			l.items = it;
+		}
+		else
+			it = nexti;
+	}
+}
+
+# Fix up the geometry of line l and successors.
+# Assume geometry of previous line is correct.
+fixgeom(f: ref Frame, lay: ref Lay, l: ref Line)
+{
+	while(l != nil) {
+		fixlinegeom(f, lay, l);
+		mergetext(l);
+		l = l.next;
+	}
+	lay.height = max(lay.height, lay.end.pos.y);
+}
+
+mergetext(l: ref Line)
+{
+	lastit : ref Item;
+	for (it := l.items; it != nil; it = it.next) {
+		pick i := it {
+		Itext =>
+			if (lastit == nil)
+				break; #pick
+			pick pi := lastit {
+			Itext =>
+				# ignore item state flags as fixlinegeom() 
+				# will have taken account of them.
+				if (pi.anchorid == i.anchorid &&
+				pi.fnt == i.fnt && pi.fg == i.fg && pi.voff == i.voff && pi.ul == i.ul) {
+					# compatible - merge
+					pi.s += i.s;
+					pi.width += i.width;
+					pi.next = i.next;
+					continue;
+				}
+			}
+		}
+		lastit = it;
+	}
+}
+
+# Fix geom for one line.
+# This may change the overall lay.width, if there is no way
+# to fit the line into the target width. 
+fixlinegeom(f: ref Frame, lay: ref Lay, l: ref Line)
+{
+	lprev := l.prev;
+	y := lprev.pos.y + lprev.height;
+	it := l.items;
+	state := it.state;
+	if(dbg > 1) {
+		sys->print("\nfixlinegeom start, y=prev.y+prev.height=%d+%d=%d, changed=%d\n",
+				l.prev.pos.y, lprev.height, y, int (l.flags&Lchanged));
+		if(dbg > 2)
+			it.printlist("items");
+		else {
+			sys->print("first item: ");
+			it.print();
+		}
+	}
+	if(state&IFbrk) {
+		y = pastbrk(lay, y, state);
+		if(dbg > 1 && y != lprev.pos.y + lprev.height)
+			sys->print("after pastbrk, line y is now %d\n", y);
+	}
+	l.pos.y = y;
+	lineh := max(l.height, linespace);
+	lfloatw := floatw(y, y+lineh, lay.floats, Aleft);
+	rfloatw := floatw(y, y+lineh, lay.floats, Aright);
+	if((l.flags&Lchanged) == byte 0) {
+		# possibly adjust lay.width
+		n := (lay.width-rfloatw)-(l.pos.x-lay.margin+l.width);
+		if(n < 0)
+			lay.width += -n;
+		return;
+	}
+	hang := (state&IFhangmask)*TABPIX/10;
+	linehang := hang;
+	hangtogo := hang;
+	indent := ((state&IFindentmask)>>IFindentshift)*TABPIX;
+	just := (state&(IFcjust|IFrjust));
+	if(just == 0 && lay.just != Aleft) {
+		if(lay.just == byte Acenter)
+			just = IFcjust;
+		else if(lay.just == Aright)
+			just = IFrjust;
+	}
+	right := lay.targetwidth - lay.margin;
+	lwid := right - (lfloatw+rfloatw+indent+lay.margin);
+	if(lwid < 0) {
+		if (right - lwid > lay.width)
+			lay.width = right - lwid;
+		right += -lwid;
+		lwid = 0;
+	}
+	lwid += hang;
+	if(dbg > 1) {
+		sys->print("fixlinegeom, now y=%d, lfloatw=%d, rfloatw=%d, indent=%d, hang=%d, lwid=%d\n",
+				y, lfloatw, rfloatw, indent, hang, lwid);
+	}
+	w := 0;
+	lineh = 0;
+	linea := 0;
+	lastit: ref Item = nil;
+	nextfloats: list of ref Item.Ifloat = nil;
+	anystuff := 0;
+	eol := 0;
+	while(it != nil && !eol) {
+		if(dbg > 2) {
+			sys->print("fixlinegeom loop head, w=%d, loop item:\n", w);
+			it.print();
+		}
+		state = it.state;
+		wrapping := int (state&IFwrap);
+		if(anystuff && (state&IFbrk))
+			break;
+		checkw := 1;
+		if(hang && !(state&IFhangmask)) {
+			lwid -= hang;
+			hang = 0;
+			if(hangtogo > 0) {
+				# insert a null spacer item
+				spaceit := Item.newspacer(ISPgeneral, 0);
+				spaceit.width = hangtogo;
+				if(lastit != nil) {
+					spaceit.state = lastit.state & ~(IFbrk|IFbrksp|IFnobrk|IFcleft|IFcright);
+					lastit.next = spaceit;
+				}
+				else
+					lastit = spaceit;
+				spaceit.next = it;
+			}
+		}
+		pick i := it {
+		Ifloat =>
+			if(anystuff) {
+				# float will go after this line
+				nextfloats = i :: nextfloats;
+			}
+			else {
+				# add float beside current line, adjust widths
+				fixfloatxy(lay, y, i);
+				# TODO: only do following if y and/or height changed
+				changelines(l.next, lay.end);
+				newlfloatw := floatw(y, y+lineh, lay.floats, Aleft);
+				newrfloatw := floatw(y, y+lineh, lay.floats, Aright);
+				lwid -= (newlfloatw-lfloatw) + (newrfloatw-rfloatw);
+				if (lwid < 0) {
+					right += -lwid;
+					lwid = 0;
+				}
+				lfloatw = newlfloatw;
+				rfloatw = newrfloatw;
+			}
+			checkw = 0;
+		Itable =>
+			# When just doing layout for cell dimensions, don't
+			# want a "100%" spec to make the table really wide
+			kindspec := 0;
+			if(lay.targetwidth == TABLEMAXTARGET && i.table.width.kind() == Dpercent) {
+				kindspec = i.table.width.kindspec;
+				i.table.width = Dimen.make(Dnone, 0);
+			}
+			checktabsize(f, i, lwid-w);
+			if(kindspec != 0)
+				i.table.width.kindspec = kindspec;
+		Irule =>
+			avail := lwid-w;
+			# When just doing layout for cell dimensions, don't
+			# want a "100%" spec to make the rule really wide
+			if(lay.targetwidth == TABLEMAXTARGET)
+				avail = min(10, avail);
+			i.width = widthfromspec(i.wspec, avail);
+		Iformfield =>
+			checkffsize(f, i, i.formfield);
+		}
+		if(checkw) {
+			iw := it.width;
+			if(wrapping && w + iw > lwid) {
+				# it doesn't fit; see if it can be broken
+				takeit: int;
+				noneok := (anystuff || lfloatw != 0 || rfloatw != 0) && !(state&IFnobrk);
+				(takeit, iw) = trybreak(it, lwid-w, iw, noneok);
+				eol = 1;
+				if(!takeit) {
+					if(lastit == nil) {
+						# Nothing added because one of the float widths
+						# is nonzero, and not enough room for anything else.
+						# Move y down until there's more room and try again.
+						CU->assert(lfloatw != 0 || rfloatw != 0);
+						oldy := y;
+						y = pastbrk(lay, y, IFcleft|IFcright);
+						if(dbg > 1)
+							sys->print("moved y past %d, now y=%d\n", oldy, y);
+						CU->assert(y > oldy);	# else infinite recurse
+						# Do the move down by artificially increasing the
+						# height of the previous line
+						lprev.height += y-oldy;
+						fixlinegeom(f, lay, l);
+						return;
+					} else
+						break;
+				}
+			}
+			w += iw;
+			if(hang)
+				hangtogo -= w;
+			(lineh, linea) = lgeom(lineh, linea, it);
+			if(!anystuff) {
+				anystuff = 1;
+				# don't count an ordinary space as 'stuff' if wrapping
+				pick t := it {
+				Itext =>
+					if(wrapping && t.s == " ")
+						anystuff = 0;
+				}
+			}
+		}
+		lastit = it;
+		it = it.next;
+		if(it == nil && !eol) {
+			# perhaps next lines items can now fit on this line
+			nextl := l.next;
+			nit := nextl.items;
+			if(nextl != lay.end && !(nit.state&IFbrk)) {
+				lastit.next = nit;
+				# remove nextl
+				l.next = nextl.next;
+				l.next.prev = l;
+				it = nit;
+			}
+		}
+	}
+	# line is complete, next line will start with it (or it is nil)
+	rest := it;
+	if(lastit == nil)
+		raise "EXInternal: no items on line";
+	lastit.next = nil;
+
+	l.width = w;
+	x := lfloatw + lay.margin + indent - linehang;
+	# shift line if it begins with a space or a rule
+	pick pi := l.items {
+	Itext =>
+		if(pi.s != nil && pi.s[0] == ' ')
+			x -= fonts[pi.fnt].spw;
+	Irule =>
+		# note: build ensures that rules appear on lines
+		# by themselves
+		if(pi.align == Acenter)
+			just = IFcjust;
+		else if(pi.align == Aright)
+			just = IFrjust;
+	Ifloat =>
+		if(pi.next != nil) {
+			pick qi := pi.next {
+			Itext =>
+				if(qi.s != nil && qi.s[0] == ' ')
+					x -= fonts[qi.fnt].spw;
+			}
+		}
+	}
+	xright := x+w;
+	if (xright + rfloatw > lay.width)
+		lay.width = xright+rfloatw;
+	n := lay.targetwidth-(lay.margin+rfloatw+xright);
+	if(n > 0 && just) {
+		if(just&IFcjust)
+			x += n/2;
+		else
+			x += n;
+	}
+	if(dbg > 1) {
+		sys->print("line geometry fixed, (x,y)=(%d,%d), w=%d, h=%d, a=%d, lfloatw=%d, rfloatw=%d, lay.width=%d\n",
+			x, l.pos.y, w, lineh, linea, lfloatw, rfloatw, lay.width);
+		if(dbg > 2)
+			l.items.printlist("final line items");
+	}
+	l.pos.x = x;
+	l.height = lineh;
+	l.ascent = linea;
+	l.flags &= ~Lchanged;
+
+	if(nextfloats != nil)
+		fixfloatsafter(lay, l, nextfloats);
+
+	if(rest != nil) {
+		nextl := l.next;
+		if(nextl == lay.end || (nextl.items.state&IFbrk)) {
+			nextl = Line.new();
+			appendline(l, nextl);
+		}
+		li := lastitem(rest);
+		li.next = nextl.items;
+		nextl.items = rest;
+		nextl.flags |= Lchanged;
+	}
+}
+
+# Return y coord after y due to a break.
+pastbrk(lay: ref Lay, y, state: int) : int
+{
+	nextralines := 0;
+	if(state&IFbrksp)
+		nextralines = 1;
+	ynext := y;
+	if(state&IFcleft)
+		ynext = floatclry(lay.floats, Aleft, ynext);
+	if(state&IFcright)
+		ynext = max(ynext, floatclry(lay.floats, Aright, ynext));
+	ynext += nextralines*linespace;
+	return ynext;
+}
+
+# Add line l after lprev (and before lprev's current successor)
+appendline(lprev, l: ref Line)
+{
+	l.next = lprev.next;
+	l.prev = lprev;
+	l.next.prev = l;
+	lprev.next = l;
+}
+
+# Mark lines l up to but not including lend as changed
+changelines(l, lend: ref Line)
+{
+	for( ; l != lend; l = l.next)
+		l.flags |= Lchanged;
+}
+
+# Return a ref Font for font number num = (style*NumSize + size)
+getfont(num: int) : ref Font
+{
+	f := fonts[num].f;
+	if(f == nil) {
+		f = Font.open(display, fonts[num].name);
+		if(f == nil) {
+			if(num == DefFnt)
+				raise sys->sprint("exLayout: can't open default font %s: %r", fonts[num].name);
+			else {
+				if(int (CU->config).dbg['w'])
+					sys->print("warning: substituting default for font %s\n",
+						fonts[num].name);
+				f = fonts[DefFnt].f;
+			}
+		}
+		fonts[num].f = f;
+		fonts[num].spw = f.width(" ");
+	}
+	return f;
+}
+
+# Set the width, height and ascent fields of all items, getting any necessary fonts.
+# Some widths and heights depend on the available width on the line, and may be
+# wrong until checked during fixlinegeom.
+# Don't do tables here at all (except floating tables).
+# Configure Controls for form fields.
+measure(fr: ref Frame, items: ref Item)
+{
+	for(it := items; it != nil; it = it.next) {
+		pick t := it {
+		Itext =>
+			f := getfont(t.fnt);
+			it.width = f.width(t.s);
+			a := f.ascent;
+			h := f.height;
+			if(t.voff != byte Voffbias) {
+				a -= (int t.voff) - Voffbias;
+				if(a > h)
+					h = a;
+			}
+			it.height = h;
+			it.ascent = a;
+		Irule =>
+			it.height =  t.size + 2*RULESP;
+			it.ascent = t.size + RULESP;
+		Iimage =>
+			setimagedims(t);
+		Iformfield =>
+			c := Control.newff(fr, t.formfield);
+			if(c != nil) {
+				t.formfield.ctlid = fr.addcontrol(c);
+				it.width = c.r.dx();
+				it.height = c.r.dy();
+				it.ascent = it.height;
+				pick pc := c {
+				Centry =>
+					it.ascent = lineascent + ENTVMARGIN;
+				Cselect =>
+					it.ascent = lineascent + SELMARGIN;
+				Cbutton =>
+					if(pc.dorelief)
+						it.ascent -= BUTMARGIN;
+				}
+			}
+		Ifloat =>
+			# Leave w at zero, so it doesn't contribute to line width in normal way
+			# (Can find its width in t.item.width).
+			pick i := t.item {
+			Iimage =>
+				setimagedims(i);
+				it.height = t.item.height;
+			Itable =>
+				checktabsize(fr, i, TABLEFLOATTARGET);
+			* =>
+				CU->assert(0);
+			}
+			it.ascent = it.height;
+		Ispacer =>
+			case t.spkind {
+			ISPvline =>
+				f := getfont(t.fnt);
+				it.height = f.height;
+				it.ascent = f.ascent;
+			ISPhspace =>
+				getfont(t.fnt);
+				it.width = fonts[t.fnt].spw;
+			}
+		}
+	}
+}
+
+# Set the dimensions of an image item
+setimagedims(i: ref Item.Iimage)
+{
+	i.width = i.imwidth + 2*(int i.hspace + int i.border);
+	i.height = i.imheight + 2*(int i.vspace + int i.border);
+	i.ascent = i.height - (int i.vspace + int i.border);
+	if((CU->config).imagelvl == CU->ImgNone && i.altrep != "") {
+		f := fonts[DefFnt].f;
+		i.width = max(i.width, f.width(i.altrep));
+		i.height = max(i.height, f.height);
+		i.ascent = f.ascent;
+	}
+}
+
+# Line geometry function:
+# Given current line height (H) and ascent (distance from top to baseline) (A),
+# and an item, see if that item changes height and ascent.
+# Return (H', A'), the updated line height and ascent.
+lgeom(H, A: int, it: ref Item) : (int, int)
+{
+	h := it.height;
+	a := it.ascent;
+	atype := Abaseline;
+	pick i := it {
+	Iimage =>
+		atype = i.align;
+	Itable =>
+		atype = Atop;
+	Ifloat =>
+		return (H, A);
+	}
+	d := h-a;
+	Hnew := H;
+	Anew := A;
+	case int atype {
+	int Abaseline or int Abottom =>
+		if(a > A) {
+			Anew = a;
+			Hnew += (Anew - A);
+		}
+		if(d > Hnew - Anew)
+			Hnew = Anew + d;
+	int Atop =>
+		# OK to ignore what comes after in the line
+		if(h > H)
+			Hnew = h;
+	int Amiddle or int Acenter =>
+		# supposed to align middle with baseline
+		hhalf := h/2;
+		if(hhalf > A)
+			Anew = hhalf;
+		if(hhalf > H-Anew)
+			Hnew = Anew + hhalf;
+	}
+	return (Hnew, Anew);
+}
+
+# Try breaking item bit to make it fit in availw.
+# If that is possible, change bit to be the part that fits
+# and insert the rest between bit and bit.next.
+# iw is the current width of bit.
+# If noneok is 0, break off the minimum size word
+# even if it exceeds availw.
+# Return (1 if supposed to take bit, iw' = new width of bit)
+trybreak(bit: ref Item, availw, iw, noneok: int) : (int, int)
+{
+	if(iw <= 0)
+		return (1, iw);
+	if(availw < 0) {
+		if(noneok)
+			return (0, iw);
+		else
+			availw = 0;
+	}
+	pick t := bit {
+	Itext =>
+		if(len t.s < 2)
+			return (!noneok, iw);
+		(s1, w1, s2, w2) := breakstring(t.s, iw, fonts[t.fnt].f, availw, noneok);
+		if(w1 == 0)
+			return (0, iw);
+		itn := Item.newtext(s2, t.fnt, t.fg, int t.voff, t.ul);
+		itn.width = w2;
+		itn.height = t.height;
+		itn.ascent = t.ascent;
+		itn.anchorid = t.anchorid;
+		itn.state = t.state & ~(IFbrk|IFbrksp|IFnobrk|IFcleft|IFcright);
+		itn.next = t.next;
+		t.next = itn;
+		t.s = s1;
+		t.width = w1;
+		return (1, w1);
+	}
+	return (!noneok, iw);
+}
+
+# s has width sw when drawn in fnt.
+# Break s into s1 and s2 so that s1 fits in availw.
+# If noneok is true, it is ok for s1 to be nil, otherwise might
+# have to return an s1 that overflows availw somewhat.
+# Return (s1, w1, s2, w2) where w1 and w2 are widths of s1 and s2.
+# Assume caller has already checked that sw > availw.
+breakstring(s: string, sw: int, fnt: ref Font, availw, noneok: int) : (string, int, string, int)
+{
+	slen := len s;
+	if(slen < 2) {
+		if(noneok)
+			return (nil, 0, s, sw);
+		else
+			return (s, sw, nil, 0);
+	}
+
+	# Use linear interpolation to guess break point.
+	# We know avail < iw by conditions of trybreak call.
+	i := slen*availw / sw - 1;
+	if(i < 0)
+		i = 0;
+	i = breakpoint(s, i, -1);
+	(ss, ww) := tryw(fnt, s, i);
+	if(ww > availw) {
+		while(ww > availw) {
+			i = breakpoint(s, i-1, -1);
+			if(i <= 0)
+				break;
+			(ss, ww) = tryw(fnt, s, i);
+		}
+	}
+	else {
+		oldi := i;
+		oldss := ss;
+		oldww := ww;
+		while(ww < availw) {
+			oldi = i;
+			oldss = ss;
+			oldww = ww;
+			i = breakpoint(s, i+1, 1);
+			if(i >= slen)
+				break;
+			(ss, ww) = tryw(fnt, s, i);
+		}
+		i = oldi;
+		ss = oldss;
+		ww = oldww;
+	}
+	if(i <= 0 || i >= slen) {
+		if(noneok)
+			return (nil, 0, s, sw);
+		i = breakpoint(s, 1, 1);
+		(ss,ww) = tryw(fnt, s, i);
+	}
+	return (ss, ww, s[i:slen], sw-ww);
+}
+
+# If can break between s[i-1] and s[i], return i.
+# Else move i in direction incr until this is true.
+# (Might end up returning 0 or len s).
+breakpoint(s: string, i, incr: int) : int
+{
+	slen := len s;
+	ans := 0;
+	while(i > 0 && i < slen) {
+		ci := s[i];
+		di := s[i-1];
+		
+		# ASCII rules
+		if ((ci < 16rA0 && !int wordchar[ci]) || (di < 16rA0 && !int wordchar[di])) {
+			ans = i;
+			break;
+		}
+
+		# Treat all ideographs as breakable.
+		# The following range includes unassigned unicode code points.
+		# All assigned code points in the range are class ID (ideograph) as defined
+		# by the Unicode consortium's LineBreak data.
+		# There are many other class ID code points outside of this range.
+		# For details on how to do unicode line breaking properly see:
+		# Unicode Standard Annex #14 (http://www.unicode.org/unicode/reports/tr14/)
+
+		if ((ci >= 16r30E && ci <= 16r9FA5) || (di >= 16r30E && di <= 16r9FA5)) {
+			ans = i;
+			break;
+		}
+
+		# consider all other characters as unbreakable
+		i += incr;
+	}
+	if(i == slen)
+		ans = slen;
+	return ans;
+}
+
+# Return (s[0:i], width of that slice in font fnt)
+tryw(fnt: ref Font, s: string, i: int) : (string, int)
+{
+	if(i == 0)
+		return ("", 0);
+	ss := s[0:i];
+	return (ss, fnt.width(ss));
+}
+
+# Return max width of a float that overlaps [ymin, ymax) on given side.
+# Floats are in reverse order of addition, so each float's y is <= that of
+# preceding floats in list.  Floats from both sides are intermixed.
+floatw(ymin, ymax: int, flist: list of ref Item.Ifloat, side: byte) : int
+{
+	ans := 0;
+	for( ; flist != nil; flist = tl flist) {
+		fl := hd flist;
+		if(fl.side != side)
+			continue;
+		fymin := fl.y;
+		fymax := fymin + fl.item.height;
+		if((fymin <= ymin && ymin < fymax) ||
+		   (ymin <= fymin && fymin < ymax)) {
+			w := fl.x;
+			if(side == Aleft)
+				w += fl.item.width;
+			if(ans < w)
+				ans = w;
+		}
+	}
+	return ans;
+}
+
+# Float f is to be at vertical position >= y.
+# Fix its (x,y) pos and add it to lay.floats, if not already there.
+fixfloatxy(lay: ref Lay, y: int, f: ref Item.Ifloat)
+{
+	height := f.item.height;
+	width := f.item.width;
+	f.y = y;
+	flist := lay.floats;
+	if(f.infloats != byte 0) {
+		# only take previous floats into account for width
+		while(flist != nil) {
+			x := hd flist;
+			flist = tl flist;
+			if(x == f)
+				break;
+		}
+	}
+	f.x = floatw(y, y+height, flist, f.side);
+	endx := f.x + width + lay.margin;
+	if (endx > lay.width)
+		lay.width = endx;
+	if (f.side == Aright)
+		f.x += width;
+	endy := f.y + height + lay.margin;
+	if (endy > lay.height)
+		lay.height = endy;
+	if(f.infloats == byte 0) {
+		lay.floats = f :: lay.floats;
+		f.infloats = byte 1;
+	}
+}
+
+# Floats in flist are to go after line l.
+fixfloatsafter(lay: ref Lay, l: ref Line, flist: list of ref Item.Ifloat)
+{
+	change := 0;
+	y := l.pos.y + l.height;
+	for(itl := Item.revlist(flist); itl != nil; itl = tl itl) {
+		pick fl := hd itl {
+		Ifloat =>
+			oldy := fl.y;
+			fixfloatxy(lay, y, fl);
+			if(fl.y != oldy)
+				change = 1;
+			y += fl.item.height;
+		}
+	}
+#	if(change)
+# TODO only change if y and/or height changed
+		changelines(l.next, lay.end);
+}
+
+# If there's a float on given side that starts on or before y and
+# ends after y, return ending y of that float, else return original y.
+# Assume float list is bottom up.
+floatclry(flist: list of ref Item.Ifloat, side: byte, y: int) : int
+{
+	ymax := y;
+	for( ; flist != nil; flist = tl flist) {
+		fl := hd flist;
+		if(fl.side == side) {
+			if(fl.y <= y) {
+				flymax := fl.y + fl.item.height;
+				if (fl.item.height == 0)
+					# assume it will have some height later
+					flymax++;
+				if(flymax > ymax)
+					ymax = flymax;
+			}
+		}
+	}
+	return ymax;
+}
+
+# Do preliminaries to laying out table tab in target width linewidth,
+# setting total height and width.
+sizetable(f: ref Frame, tab: ref Table, availwidth: int)
+{
+	if(dbgtab)
+		sys->print("sizetable %d, availwidth=%d, nrow=%d, ncol=%d, changed=%x, tab.availw=%d\n",
+			tab.tableid, availwidth, tab.nrow, tab.ncol, int (tab.flags&Lchanged), tab.availw);
+	if(tab.ncol == 0 || tab.nrow == 0)
+		return;
+	if(tab.availw == availwidth && (tab.flags&Lchanged) == byte 0)
+		return;
+	(hsp, vsp, pad, bd, cbd, hsep, vsep) := tableparams(tab);
+	totw := widthfromspec(tab.width, availwidth);
+	# reduce totw by spacing, padding, and rule widths
+	# to leave amount left for contents
+	totw -= (tab.ncol-1)*hsep+ 2*(hsp+bd+pad+cbd);
+	if(totw <= 0)
+		totw = 1;
+	if(dbgtab)
+		sys->print("\nsizetable %d, totw=%d, hsp=%d, vsp=%d, pad=%d, bd=%d, cbd=%d, hsep=%d, vsep=%d\n",
+			tab.tableid, totw, hsp, vsp, pad, bd, cbd, hsep, vsep);
+	for(cl := tab.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		clay : ref Lay = nil;
+		if(c.layid >= 0)
+			clay = f.sublays[c.layid];
+		if(clay == nil || (clay.flags&Lchanged) != byte 0) {
+			c.minw = -1;
+			tw := TABLEMAXTARGET;
+			if(c.wspec.kind() != Dnone)
+				tw = widthfromspec(c.wspec, totw);
+
+			# When finding max widths, want to lay out using Aleft alignment,
+			# because we don't yet know final width for proper justification.
+			# If the max widths are accepted, we'll redo those needing other justification.
+			if(clay == nil) {
+				if(dbg)
+					sys->print("Initial layout for cell %d.%d\n", tab.tableid, c.cellid);
+				c.layid = sublayout(f, tw, Aleft, c.background, c.content);
+				clay = f.sublays[c.layid];
+				c.content = nil;
+			}
+			else {
+				if(dbg)
+					sys->print("Relayout (for max) for cell %d.%d\n", tab.tableid, c.cellid);
+				relayout(f, clay, tw, Aleft);
+			}
+			clay.flags |= Lchanged;	# for min test, below
+			c.maxw = clay.width;
+			if(dbgtab)
+				sys->print("sizetable %d for cell %d max layout done, targw=%d, c.maxw=%d\n",
+						tab.tableid, c.cellid, tw, c.maxw);
+			if(c.wspec.kind() == Dpixels) {
+				# Other browsers don't make the following adjustment for
+				# percentage and relative widths
+				if(c.maxw <= tw)
+					c.maxw = tw;
+				if(dbgtab)
+					sys->print("after spec adjustment, c.maxw=%d\n", c.maxw);
+			}
+		}
+	}
+
+	# calc max column widths
+	colmaxw := array[tab.ncol] of { * => 0};
+	maxw := widthcalc(tab, colmaxw, hsep, 1);
+
+	if(dbgtab)
+		sys->print("sizetable %d maxw=%d, totw=%d\n", tab.tableid, maxw, totw);
+	ci: int;
+	if(maxw <= totw) {
+		# trial layouts are fine,
+		# but if table width was specified, add more space
+		d := 0;
+		adjust := (totw > maxw && tab.width.kind() != Dnone);
+		for(ci = 0; ci < tab.ncol; ci++) {
+			if (adjust) {
+				delta := (totw-maxw);
+				d = delta / (tab.ncol - ci);
+				if (d <= 0) {
+					d = delta;
+					adjust = 0;
+				}
+				maxw += d;
+			}
+			tab.cols[ci].width = colmaxw[ci] + d;
+		}
+	}
+	else {
+		# calc min column widths and  apportion out
+		# differences
+		if(dbgtab)
+			sys->print("sizetable %d, availwidth %d, need min widths too\n", tab.tableid, availwidth);
+		for(cl = tab.cells; cl != nil; cl = tl cl) {
+			c := hd cl;
+			clay := f.sublays[c.layid];
+			if(c.minw == -1 || (clay.flags&Lchanged) != byte 0) {
+				if(dbg)
+					sys->print("Relayout (for min) for cell %d.%d\n", tab.tableid, c.cellid);
+				relayout(f, clay, 1, Aleft);
+				c.minw = clay.width;
+				if(dbgtab)
+					sys->print("sizetable %d for cell %d min layout done, c.min=%d\n",
+						tab.tableid, c.cellid, clay.width);
+			}
+		}
+		colminw := array[tab.ncol] of { * => 0};
+		minw := widthcalc(tab, colminw, hsep, 0);
+		w := totw - minw;
+		d := maxw - minw;
+		if(dbgtab)
+			sys->print("sizetable %d minw=%d, w=%d, d=%d\n", tab.tableid, minw, w, d);
+		for(ci = 0; ci < tab.ncol; ci++) {
+			wd : int;
+			if(w < 0 || d < 0)
+				wd = colminw[ci];
+			else
+				wd = colminw[ci] + (colmaxw[ci] - colminw[ci])*w/d;
+			if(dbgtab)
+				sys->print("sizetable %d col[%d].width = %d\n", tab.tableid, ci, wd);
+			tab.cols[ci].width = wd;
+		}
+
+		if(dbgtab)
+			sys->print("sizetable %d, availwidth %d, doing final layouts\n", tab.tableid, availwidth);
+	}
+
+	# now have col widths; set actual cell dimensions
+	# and relayout (note: relayout will do no work if the target width
+	# and just haven't changed from last layout)
+	for(cl = tab.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		clay := f.sublays[c.layid];
+		wd := cellwidth(tab, c, hsep);
+		if(dbgtab)
+			sys->print("sizetable %d for cell %d, clay.width=%d, cellwidth=%d\n",
+					tab.tableid, c.cellid, clay.width, wd);
+		if(dbg)
+			sys->print("Relayout (final) for cell %d.%d\n", tab.tableid, c.cellid);
+		relayout(f, clay, wd, c.align.halign);
+		if(dbgtab)
+			sys->print("sizetable %d for cell %d, final width %d, got width %d, height %d\n",
+					tab.tableid, c.cellid, wd, clay.width, clay.height);
+	}
+
+	# set row heights and ascents
+	# first pass: ignore cells with rowspan > 1
+	for(ri := 0; ri < tab.nrow; ri++) {
+		row := tab.rows[ri];
+		h := 0;
+		a := 0;
+		n : int;
+		for(rcl := row.cells; rcl != nil; rcl = tl rcl) {
+			c := hd rcl;
+			if(c.rowspan > 1 || c.layid < 0)
+				continue;
+			al := c.align.valign;
+			if(al == Anone)
+				al = tab.rows[c.row].align.valign;
+			clay := f.sublays[c.layid];
+			if(al == Abaseline) {
+				n = c.ascent;
+				if(n > a) {
+					h += (n - a);
+					a = n;
+				}
+				n = clay.height - c.ascent;
+				if(n > h-a)
+					h = a + n;
+			}
+			else {
+				n = clay.height;
+				if(n > h)
+					h = n;
+			}
+		}
+		row.height = h;
+		row.ascent = a;
+	}
+	# second pass: take care of rowspan > 1
+	# (this algorithm isn't quite right -- it might add more space
+	# than is needed in the presence of multiple overlapping rowspans)
+	for(cl = tab.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		if(c.rowspan > 1) {
+			spanht := 0;
+			for(i := 0; i < c.rowspan && c.row+i < tab.nrow; i++)
+				spanht += tab.rows[c.row+i].height;
+			if(c.layid < 0)
+				continue;
+			clay := f.sublays[c.layid];
+			ht := clay.height - (c.rowspan-1)*vsep;
+			if(ht > spanht) {
+				# add extra space to last spanned row
+				i = c.row+c.rowspan-1;
+				if(i >= tab.nrow)
+					i = tab.nrow - 1;
+				tab.rows[i].height += ht - spanht;
+				if(dbgtab)
+					sys->print("sizetable %d, row %d height %d\n", tab.tableid, i, tab.rows[i].height);
+			}
+		}
+	}
+	# get total width, heights, and col x / row y positions
+	totw = bd + hsp + cbd + pad;
+	for(ci = 0; ci < tab.ncol; ci++) {
+		tab.cols[ci].pos.x = totw;
+		if(dbgtab)
+			sys->print("sizetable %d, col %d at x=%d\n", tab.tableid, ci, totw);
+		totw += tab.cols[ci].width + hsep;
+	}
+	totw = totw - (cbd+pad) + bd;
+	toth := bd + vsp + cbd + pad;
+	# first time: move tab.caption items into layout
+	if(tab.caption != nil) {
+		# lay caption with Aleft; drawing will center it over the table width
+		tab.caption_lay = sublayout(f, availwidth, Aleft, f.layout.background, tab.caption);
+		caplay := f.sublays[tab.caption_lay];
+		tab.caph = caplay.height + CAPSEP;
+		tab.caption = nil;
+	}
+	else if(tab.caption_lay >= 0) {
+		caplay := f.sublays[tab.caption_lay];
+		if(tab.availw != availwidth || (caplay.flags&Lchanged) != byte 0) {
+			relayout(f, caplay, availwidth, Aleft);
+			tab.caph = caplay.height + CAPSEP;
+		}
+	}
+	if(tab.caption_place == Atop)
+		toth += tab.caph;
+	for(ri = 0; ri < tab.nrow; ri++) {
+		tab.rows[ri].pos.y = toth;
+		if(dbgtab)
+			sys->print("sizetable %d, row %d at y=%d\n", tab.tableid, ri, toth);
+		toth += tab.rows[ri].height + vsep;
+	}
+	toth = toth - (cbd+pad) + bd;
+	if(tab.caption_place == Abottom)
+		toth += tab.caph;
+	tab.totw = totw;
+	tab.toth = toth;
+	tab.availw = availwidth;
+	tab.flags &= ~Lchanged;
+	if(dbgtab)
+		sys->print("\ndone sizetable %d, availwidth %d, totw=%d, toth=%d\n\n",
+			tab.tableid, availwidth, totw, toth);
+}
+
+# Calculate various table spacing parameters
+tableparams(tab: ref Table) : (int, int, int, int, int, int, int)
+{
+	bd := tab.border;
+	hsp := tab.cellspacing;
+	vsp := hsp;
+	pad := tab.cellpadding;
+	if(bd != 0)
+		cbd := 1;
+	else
+		cbd = 0;
+	hsep := 2*(cbd+pad)+hsp;
+	vsep := 2*(cbd+pad)+vsp;
+	return (hsp, vsp, pad, bd, cbd, hsep, vsep);
+}
+
+# return cell width, taking multicol spanning into account
+cellwidth(tab: ref Table, c: ref Tablecell, hsep: int) : int
+{
+	if(c.colspan == 1)
+		return tab.cols[c.col].width;
+	wd := (c.colspan-1)*hsep;
+	for(i := 0; i < c.colspan && c.col + i < tab.ncol; i++)
+		wd += tab.cols[c.col + i].width;
+	return wd;
+}
+
+# return cell height, taking multirow spanning into account
+cellheight(tab: ref Table, c: ref Tablecell, vsep: int) : int
+{
+	if(c.rowspan == 1)
+		return tab.rows[c.row].height;
+	ht := (c.rowspan-1)*vsep;
+	for(i := 0; i < c.rowspan && c.row + i < tab.nrow; i++)
+		ht += tab.rows[c.row + i].height;
+	return ht;
+}
+
+# Calculate the column widths w as the max of the cells
+# maxw or minw (as domax is 1 or 0).
+# Return the total of all w.
+# (hseps were accounted for by the adjustment that got
+# totw from availwidth).
+# hsep is amount of free space available between columns
+# where there is multicolumn spanning.
+# This is a two-pass algorithm.  The first pass ignores
+# cells that span multiple columns.  The second pass
+# sees if those multispanners need still more space, and
+# if so, apportions the space out.
+widthcalc(tab: ref Table, w: array of int, hsep, domax: int) : int
+{
+	anyspan := 0;
+	totw := 0;
+	for(pass := 1; pass <= 2; pass++) {
+		if(pass==2 && !anyspan)
+			break;
+		totw = 0;
+		for(ci := 0; ci < tab.ncol; ci++) {
+			for(ri := 0; ri < tab.nrow; ri++) {
+				c := tab.grid[ri][ci];
+				if(c == nil)
+					continue;
+				if(domax)
+					cwd := c.maxw;
+				else
+					cwd = c.minw;
+				if(pass == 1) {
+					if(c.colspan > 1) {
+						anyspan = 1;
+						continue;
+					}
+					if(cwd > w[ci])
+						w[ci] = cwd;
+				}
+				else {
+					if(c.colspan == 1 || !(ci==c.col && ri==c.row))
+						continue;
+					curw := 0;
+					iend := ci+c.colspan;
+					if(iend > tab.ncol)
+						iend = tab.ncol;
+					for(i:=ci; i < iend; i++)
+						curw += w[i];
+				
+					# padding between spanned cols is free
+					cwd -= hsep*(c.colspan-1);
+					diff := cwd-curw;
+					if(diff <= 0)
+						continue;
+					# doesn't fit: apportion diff among cols
+					# in proportion to their current w
+					for(i = ci; i < iend; i++) {
+						if(curw == 0)
+							w[i] = diff/c.colspan;
+						else
+							w[i] += diff*w[i]/curw;
+					}
+				}
+			}
+			totw += w[ci];
+		}
+	}
+	return totw;
+}
+
+layframeset(f: ref Frame, ki: ref Kidinfo)
+{
+	fwid := f.cr.dx();
+	fht := f.cr.dy();
+	if(dbg)
+		sys->print("layframeset, configuring frame %d wide by %d high\n", fwid, fht);
+	(nrow, rowh) := frdimens(ki.rows, fht);
+	(ncol, colw) := frdimens(ki.cols, fwid);
+	l := ki.kidinfos;
+	y := f.cr.min.y;
+	for(i := 0; i < nrow; i++) {
+		x := f.cr.min.x;
+		for(j := 0; j < ncol; j++) {
+			if(l == nil)
+				return;
+			r := Rect(Point(x,y), Point(x+colw[j],y+rowh[i]));
+			if(dbg)
+				sys->print("kid gets rect (%d,%d)(%d,%d)\n", r.min.x, r.min.y, r.max.x, r.max.y);
+			kidki := hd l;
+			l = tl l;
+			kidf := Frame.newkid(f, kidki, r);
+			if(!kidki.isframeset)
+				f.kids = kidf :: f.kids;
+			if(kidf.framebd != 0) {
+				kidf.cr = kidf.r.inset(2);
+				drawborder(kidf.cim, kidf.cr, 2, DarkGrey);
+			}
+			if(kidki.isframeset) {
+				layframeset(kidf, kidki);
+				for(al := kidf.kids; al != nil; al = tl al)
+					f.kids = (hd al) :: f.kids;
+			}
+			x += colw[j];
+		}
+		y += rowh[i];
+	}
+}
+
+# Use the dimension specs in dims to allocate total space t.
+# Return (number of dimens, array of allocated space)
+frdimens(dims: array of B->Dimen, t: int): (int, array of int)
+{
+	n := len dims;
+	if(n == 1)
+		return (1, array[] of {t});
+	totpix := 0;
+	totpcnt := 0;
+	totrel := 0;
+	for(i := 0; i < n; i++) {
+		v := dims[i].spec();
+		kind := dims[i].kind();
+		if(v < 0) {
+			v = 0;
+			dims[i] = Dimen.make(kind, v);
+		}
+		case kind {
+			B->Dpixels => totpix += v;
+			B->Dpercent => totpcnt += v;
+			B->Drelative => totrel += v;
+			B->Dnone => totrel++;
+		}
+	}
+	spix := 1.0;
+	spcnt := 1.0;
+	min_relu := 0;
+	if(totrel > 0)
+		min_relu = 30;	# allow for scrollbar (14) and a bit
+	relu := real min_relu;
+	tt := totpix + (t*totpcnt/100) + totrel*min_relu;
+	# want
+	#  t ==  totpix*spix + (totpcnt/100)*spcnt*t + totrel*relu
+	if(tt < t) {
+		# need to expand one of spix, spcnt, relu
+		if(totrel == 0) {
+			if(totpcnt != 0)
+				# spix==1.0, relu==0, solve for spcnt
+				spcnt = real ((t-totpix) * 100)/ real (t*totpcnt);
+			else
+				# relu==0, totpcnt==0, solve for spix
+				spix = real t/ real totpix;
+		}
+		else
+			# spix=1.0, spcnt=1.0, solve for relu
+			relu += real (t-tt)/ real totrel;
+	}
+	else {
+		# need to contract one or more of spix, spcnt, and have relu==min_relu
+		totpixrel := totpix+totrel*min_relu;
+		if(totpixrel < t) {
+			# spix==1.0, solve for spcnt
+			spcnt = real ((t-totpixrel) * 100)/ real (t*totpcnt);
+		}
+		else {
+			# let spix==spcnt, solve
+			trest := t - totrel*min_relu;
+			if(trest > 0) {
+				spcnt = real trest/real (totpix+(t*totpcnt/100));
+			}
+			else {
+				spcnt = real t / real tt;
+				relu = 0.0;
+			}
+			spix = spcnt;
+		}
+	}
+	x := array[n] of int;
+	tt = 0;
+	for(i = 0; i < n-1; i++) {
+		vr := real dims[i].spec();
+		case dims[i].kind() {
+			B->Dpixels => vr = vr * spix;
+			B->Dpercent => vr = vr * real t * spcnt / 100.0;
+			B->Drelative => vr = vr * relu;
+			B->Dnone => vr = relu;
+		}
+		x[i] = int vr;
+		tt += x[i];
+	}
+	x[n-1] = t - tt;
+	return (n, x);
+}
+
+# Return last item of list of items, or nil if no items
+lastitem(it: ref Item) : ref Item
+{
+	ans : ref Item = it;
+	for( ; it != nil; it = it.next)
+		ans = it;
+	return ans;
+}
+
+# Lay out table if availw changed or tab changed
+checktabsize(f: ref Frame, t: ref Item.Itable, availw: int)
+{
+	tab := t.table;
+	if (dbgtab)
+		sys->print("checktabsize %d, availw %d, tab.availw %d, changed %d\n", tab.tableid, availw, tab.availw, (tab.flags&Lchanged)>byte 0);
+	if(availw != tab.availw || int (tab.flags&Lchanged)) {
+		sizetable(f, tab, availw);
+		t.width = tab.totw + 2*tab.border;
+		t.height = tab.toth + 2*tab.border;
+		t.ascent = t.height;
+	}
+}
+
+widthfromspec(wspec: Dimen, availw: int) : int
+{
+	w := availw;
+	spec := wspec.spec();
+	case wspec.kind() {
+		Dpixels => w = spec;
+		Dpercent => w = spec*w/100;
+	}
+	return w;
+}
+
+# An image may have arrived for an image input field
+checkffsize(f: ref Frame, i: ref Item, ff: ref Formfield)
+{
+	if(ff.ftype == Fimage && ff.image != nil) {
+		pick imi := ff.image {
+		Iimage =>
+			if(imi.ci.mims != nil && ff.ctlid >= 0) {
+				pick b := f.controls[ff.ctlid] {
+				Cbutton =>
+					if(b.pic == nil) {
+						b.pic = imi.ci.mims[0].im;
+						b.picmask = imi.ci.mims[0].mask;
+						w := b.pic.r.dx();
+						h := b.pic.r.dy();
+						b.r.max.x = b.r.min.x + w;
+						b.r.max.y = b.r.min.y + h;
+						i.width = w;
+						i.height = h;
+						i.ascent = h;
+					}
+				}
+			}
+		}
+	}
+	else if(ff.ftype == Fselect) {
+		opts := ff.options;
+		if(ff.ctlid >=0) {
+			pick c := f.controls[ff.ctlid] {
+			Cselect =>
+				if(len opts != len c.options) {
+					nc := Control.newff(f, ff);
+					f.controls[ff.ctlid] = nc;
+					i.width = nc.r.dx();
+					i.height = nc.r.dy();
+					i.ascent = lineascent + SELMARGIN;
+				}
+			}
+		}
+	}
+}
+
+drawall(f: ref Frame)
+{
+	oclipr := f.cim.clipr;
+	origin := f.lptosp(zp);
+	clipr := f.dirtyr.addpt(origin);
+	f.cim.clipr = clipr;
+	fillbg(f, clipr);
+	if(dbg > 1)
+		sys->print("drawall, cr=(%d,%d,%d,%d), viewr=(%d,%d,%d,%d), origin=(%d,%d)\n",
+			f.cr.min.x, f.cr.min.y, f.cr.max.x, f.cr.max.y,
+			f.viewr.min.x, f.viewr.min.y, f.viewr.max.x, f.viewr.max.y,
+			origin.x, origin.y);
+	if(f.layout != nil)
+		drawlay(f, f.layout, origin);
+	f.cim.clipr = oclipr;
+	G->flush(f.cr);
+	f.isdirty = 0;
+}
+
+drawlay(f: ref Frame, lay: ref Lay, origin: Point)
+{
+	for(l := lay.start.next; l != lay.end; l = l.next)
+		drawline(f, origin, l, lay);
+}
+
+# Draw line l in frame f, assuming that content's (0,0)
+# aligns with layorigin in f.cim.
+drawline(f : ref Frame, layorigin : Point, l: ref Line, lay: ref Lay)
+{
+	im := f.cim;
+	o := layorigin.add(l.pos);
+	x := o.x;
+	y := o.y;
+	lr := Rect(zp, Point(l.width, l.height)).addpt(o);
+	isdirty := f.isdirty && lr.Xrect(f.dirtyr.addpt(f.lptosp(zp)));
+	inview := lr.Xrect(f.cr) && isdirty;
+
+	# note: drawimg must always be called to update
+	# draw point of animated images
+	for(it := l.items; it != nil; it = it.next) {
+		pick i := it {
+		Itext =>
+			if (!inview || i.s == nil)
+				break;
+			fnt := fonts[i.fnt];
+			width := i.width;
+			yy := y+l.ascent - fnt.f.ascent + (int i.voff) - Voffbias;
+			if (f.prctxt != nil) {
+				if (yy < f.cr.min.y)
+					continue;
+				endy := yy + fnt.f.height;
+				if (endy > f.cr.max.y) {
+					# do not draw
+					if (yy < f.prctxt.endy)
+						f.prctxt.endy = yy;
+					continue;
+				}
+			}
+			fgi := colorimage(i.fg);
+			im.text(Point(x, yy), fgi, zp, fnt.f, i.s);
+			if(i.ul != ULnone) {
+				if(i.ul == ULmid)
+					yy += 2*i.ascent/3;
+				else
+					yy += i.height - 1;
+				# don't underline leading space
+				# have already adjusted x pos in fixlinegeom()
+				ulx := x;
+				ulw := width;
+				if (i.s[0] == ' ') {
+					ulx += fnt.spw;
+					ulw -= fnt.spw;
+				}
+				if (i.s[len i.s - 1] == ' ')
+					ulw -= fnt.spw;
+				if (ulw < 1)
+					continue;
+				im.drawop(Rect(Point(ulx,yy),Point(ulx+ulw,yy+1)), fgi, nil, zp, Draw->S);
+			}
+		Irule =>
+			if (!inview)
+				break;
+			yy := y + RULESP;
+			im.draw(Rect(Point(x,yy),Point(x+i.width,yy+i.size)),
+					display.black, nil, zp);
+		Iimage =>
+			yy := y;
+			if(i.align == Abottom)
+				# bottom aligns with baseline
+				yy += l.ascent - i.imheight;
+			else if(i.align == Amiddle)
+				yy += l.ascent - (i.imheight/2);
+			drawimg(f, Point(x,yy), i);
+		Iformfield =>
+			ff := i.formfield;
+			if(ff.ctlid >= 0 && ff.ctlid < len f.controls) {
+				ctl := f.controls[ff.ctlid];
+				dims := ctl.r.max.sub(ctl.r.min);
+				# align as text
+				yy := y + l.ascent - i.ascent;
+				p := Point(x,yy);
+				ctl.r = Rect(p, p.add(dims));
+				if (!inview)
+					break;
+				if (f.prctxt != nil) {
+					if (yy < f.cr.min.y)
+						continue;
+					if (ctl.r.max.y > f.cr.max.y) {
+						# do not draw
+						if (yy < f.prctxt.endy)
+							f.prctxt.endy = yy;
+						continue;
+					}
+				}
+				ctl.draw(0);
+			}
+		Itable =>
+			# don't check inview - table can contain images
+			drawtable(f, lay, Point(x,y), i.table);
+			t := i.table;
+		Ifloat =>
+			xx := layorigin.x + lay.margin;
+			if(i.side == Aright) {
+				xx -= i.x;
+#				# for main layout of frame, floats hug
+#				# right edge of frame, not layout
+#				# (other browsers do that)
+#				if(f.layout == lay)
+					xx += lay.targetwidth;
+#				else
+#					xx += lay.width;
+			}
+			else
+				xx += i.x;
+			pick fi := i.item {
+			Iimage =>
+				drawimg(f, Point(xx, layorigin.y + i.y + (int fi.border + int fi.vspace)), fi);
+			Itable =>
+				drawtable(f, lay, Point(xx, layorigin.y + i.y), fi.table);
+			}
+		}
+		x += it.width;
+	}
+}
+
+drawimg(f: ref Frame, iorigin: Point, i: ref Item.Iimage)
+{
+	ci := i.ci;
+	im := f.cim;
+	iorigin.x += int i.hspace + int i.border;
+	# y coord is already adjusted for border and vspace
+	if(ci.mims != nil) {
+		r := Rect(iorigin, iorigin.add(Point(i.imwidth,i.imheight)));
+		inview := r.Xrect(f.cr);
+		if(i.ctlid >= 0) {
+			# animated
+			c := f.controls[i.ctlid];
+			dims := c.r.max.sub(c.r.min);
+			c.r = Rect(iorigin, iorigin.add(dims));
+			if (inview) {
+				pick ac := c {
+				Canimimage =>
+					ac.redraw = 1;
+					ac.bg = f.layout.background;
+				}
+				c.draw(0);
+			}
+		}
+		else if (inview) {
+			mim := ci.mims[0];
+			iorigin = iorigin.add(mim.origin);
+			im.draw(r, mim.im, mim.mask, zp);
+		}
+		if(inview && i.border != byte 0) {
+			if(i.anchorid != 0)
+				bdcol := f.doc.link;
+			else
+				bdcol = Black;
+			drawborder(im, r, int i.border, bdcol);
+		}
+	}
+	else if((CU->config).imagelvl == CU->ImgNone && i.altrep != "") {
+		fnt := fonts[DefFnt].f;
+		yy := iorigin.y+(i.imheight-fnt.height)/2;
+		xx := iorigin.x + (i.width-fnt.width(i.altrep))/2;
+		if(i.anchorid != 0)
+			col := f.doc.link;
+		else
+			col = DarkGrey;
+		fgi := colorimage(col);
+		im.text(Point(xx, yy), fgi, zp, fnt, i.altrep);
+	}
+}
+
+drawtable(f : ref Frame, parentlay: ref Lay, torigin: Point, tab: ref Table)
+{
+	if (dbgtab)
+		sys->print("drawtable %d\n", tab.tableid);
+	if(tab.ncol == 0 || tab.nrow == 0)
+		return;
+	im := f.cim;
+	(hsp, vsp, pad, bd, cbd, hsep, vsep) := tableparams(tab);
+	x := torigin.x;
+	y := torigin.y;
+	capy := y;
+	boxy := y;
+	if(tab.caption_place == Abottom)
+		capy = y+tab.toth-tab.caph+vsp;
+	else
+		boxy = y+tab.caph;
+	if (tab.background.color != -1 && tab.background.color != parentlay.background.color) {
+#	if(tab.background.image != parentlay.background.image ||
+#	   tab.background.color != parentlay.background.color) {
+		bgi := colorimage(tab.background.color);
+		im.draw(((x,boxy),(x+tab.totw,boxy+tab.toth-tab.caph)),
+			bgi, nil, zp);
+	}
+	if(bd != 0)
+		drawborder(im, ((x+bd,boxy+bd),(x+tab.totw-bd,boxy+tab.toth-tab.caph-bd)),
+			1, Black);
+	for(cl := tab.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		if (c.layid == -1 || c.layid >= len f.sublays) {
+			# for some reason (usually scrolling)
+			# we are drawing this cell before it has been layed out
+			continue;
+		}
+		clay := f.sublays[c.layid];
+		if(clay == nil)
+			continue;
+		if(c.col >= len tab.cols)
+			continue;
+		cx := x + tab.cols[c.col].pos.x;
+		cy := y + tab.rows[c.row].pos.y;
+		wd := cellwidth(tab, c, hsep);
+		ht := cellheight(tab, c, vsep);
+		if(c.background.image != nil && c.background.image.ci != nil && c.background.image.ci.mims != nil) {
+			cellr := Rect((cx-pad,cy-pad),(cx+wd+pad,cy+ht+pad));
+			ci := c.background.image.ci;
+			bgi := ci.mims[0].im;
+			bgmask := ci.mims[0].mask;
+			im.draw(cellr, bgi, bgmask, bgi.r.min);
+		} else if(c.background.color != -1 && c.background.color != tab.background.color) {
+			bgi := colorimage(c.background.color);
+			im.draw(((cx-pad,cy-pad),(cx+wd+pad,cy+ht+pad)),
+				bgi, nil, zp);
+		}
+		if(bd != 0)
+			drawborder(im, ((cx-pad+1,cy-pad+1),(cx+wd+pad-1,cy+ht+pad-1)),
+				1, Black);
+		if(c.align.valign != Atop && c.align.valign != Abaseline) {
+			n := ht - clay.height;
+			if(c.align.valign == Amiddle)
+				cy += n/2;
+			else if(c.align.valign == Abottom)
+				cy += n;
+		}
+		if(dbgtab)
+			sys->print("drawtable %d cell %d at (%d,%d)\n",
+				tab.tableid, c.cellid, cx, cy);
+		drawlay(f, clay, Point(cx,cy));
+	}
+	if(tab.caption_lay >= 0) {
+		caplay := f.sublays[tab.caption_lay];
+		capx := x;
+		if(caplay.width < tab.totw)
+			capx += (tab.totw-caplay.width) / 2;
+		drawlay(f, caplay, Point(capx,capy));
+	}
+}
+
+# Draw border of width n just outside r, using src color
+drawborder(im: ref Image, r: Rect, n, color: int)
+{
+	x := r.min.x-n;
+	y := r.min.y - n;
+	xr := r.max.x+n;
+	ybi := r.max.y;
+	src := colorimage(color);
+	im.draw((Point(x,y),Point(xr,y+n)), src, nil, zp);				# top
+	im.draw((Point(x,ybi),Point(xr,ybi+n)), src, nil, zp);			# bottom
+	im.draw((Point(x,y+n),Point(x+n,ybi)), src, nil, zp);			# left
+	im.draw((Point(xr-n,y+n),Point(xr,ybi)), src, nil, zp);			# right
+}
+
+# Draw relief border just outside r, width 2 border,
+# colors white/lightgrey/darkgrey/black
+# to give raised relief (if raised != 0) or sunken.
+drawrelief(im: ref Image, r: Rect, raised: int)
+{
+	# ((x,y),(xr,yb)) == r
+	x := r.min.x;
+	x1 := x-1;
+	x2 := x-2;
+	xr := r.max.x;
+	xr1 := xr+1;
+	xr2 := xr+2;
+	y := r.min.y;
+	y1 := y-1;
+	y2 := y-2;
+	yb := r.max.y;
+	yb1 := yb+1;
+	yb2 := yb+2;
+
+	# colors for top/left outside, top/left inside, bottom/right outside, bottom/right inside
+	tlo, tli, bro, bri: ref Image;
+	if(raised) {
+		tlo = colorimage(Grey);
+		tli = colorimage(White);
+		bro = colorimage(Black);
+		bri = colorimage(DarkGrey);
+	}
+	else {
+		tlo = colorimage(DarkGrey);
+		tli = colorimage(Black);
+		bro = colorimage(White);
+		bri = colorimage(Grey);
+	}
+
+	im.draw((Point(x2,y2), Point(xr1,y1)), tlo, nil, zp);		# top outside
+	im.draw((Point(x1,y1), Point(xr,y)), tli, nil, zp);			# top inside
+	im.draw((Point(x2,y1), Point(x1,yb1)), tlo, nil, zp);		# left outside
+	im.draw((Point(x1,y), Point(x,yb)), tli, nil, zp);			# left inside
+	im.draw((Point(xr,y1),Point(xr1,yb)), bri, nil, zp);		# right inside
+	im.draw((Point(xr1,y),Point(xr2,yb1)), bro, nil, zp);		# right outside
+	im.draw((Point(x1,yb),Point(xr1,yb1)), bri, nil, zp);		# bottom inside
+	im.draw((Point(x,yb1),Point(xr2,yb2)), bro, nil, zp);		# bottom outside
+}
+
+# Fill r with color
+drawfill(im: ref Image, r: Rect, color: int)
+{
+	im.draw(r, colorimage(color), nil, zp);
+}
+
+# Draw string in default font at p
+drawstring(im: ref Image, p: Point, s: string)
+{
+	im.text(p, colorimage(Black), zp, fonts[DefFnt].f, s);
+}
+
+# Return (width, height) of string in default font
+measurestring(s: string) : Point
+{
+	f := fonts[DefFnt].f;
+	return (f.width(s), f.height);
+}
+
+# Mark as "changed" everything with change flags on the loc path
+markchanges(loc: ref Loc)
+{
+	lastf : ref Frame = nil;
+	for(i := 0; i < loc.n; i++) {
+		case loc.le[i].kind {
+		LEframe =>
+			lastf = loc.le[i].frame;
+			lastf.layout.flags |= Lchanged;
+		LEline =>
+			loc.le[i].line.flags |= Lchanged;
+		LEitem =>
+			pick it := loc.le[i].item {
+			Itable =>
+				it.table.flags |= Lchanged;
+			Ifloat =>
+				# whole layout will be redone if layout changes
+				# and there are any floats
+				;
+			}
+		LEtablecell =>
+			if(lastf == nil)
+				raise "EXInternal: markchanges no lastf";
+			c := loc.le[i].tcell;
+			clay := lastf.sublays[c.layid];
+			if(clay != nil)
+				clay.flags |= Lchanged;
+		}
+	}
+}
+
+# one-item cache for colorimage
+prevrgb := -1;
+prevrgbimage : ref Image = nil;
+
+colorimage(rgb: int) : ref Image
+{
+	if(rgb == prevrgb)
+		return prevrgbimage;
+	prevrgb = rgb;
+	if(rgb == Black)
+		prevrgbimage = display.black;
+	else if(rgb == White)
+		prevrgbimage = display.white;
+	else {
+		hv := rgb % NCOLHASH;
+		if (hv < 0)
+			hv = -hv;
+		xhd := colorhashtab[hv];
+		x := xhd;
+		while(x != nil && x.rgb  != rgb)
+			x = x.next;
+		if(x == nil) {
+#			pix := I->closest_rgbpix((rgb>>16)&255, (rgb>>8)&255, rgb&255);
+#			im := display.color(pix);
+			im := display.rgb((rgb>>16)&255, (rgb>>8)&255, rgb&255);
+			if(im == nil)
+				raise sys->sprint("exLayout: can't allocate color #%8.8ux: %r", rgb);
+			x = ref Colornode(rgb, im, xhd);
+			colorhashtab[hv] = x;
+		}
+		prevrgbimage = x.im;
+	}
+	return prevrgbimage;
+}
+
+# Use f.background.image (if not nil) or f.background.color to fill r (in cim coord system)
+# with background color.
+fillbg(f: ref Frame, r: Rect)
+{
+	bgi: ref Image;
+	ii := f.doc.background.image;
+	if (ii != nil && ii.ci != nil && ii.ci.mims != nil)
+		bgi = ii.ci.mims[0].im;
+	if(bgi == nil)
+		bgi = colorimage(f.doc.background.color);
+	f.cim.drawop(r, bgi, nil, f.viewr.min, Draw->S);
+}
+
+TRIup, TRIdown, TRIleft, TRIright: con iota;
+# Assume r is a square
+drawtriangle(im: ref Image, r: Rect, kind, style: int)
+{
+	drawfill(im, r, Grey);
+	b := r.max.x - r.min.x;
+	if(b < 4)
+		return;
+	b2 := b/2;
+	bm2 := b-ReliefBd;
+	p := array[3] of Point;
+	col012, col20 : ref Image;
+	d := colorimage(DarkGrey);
+	l := colorimage(White);
+	case kind {
+	TRIup =>
+		p[0] = Point(b2, ReliefBd);
+		p[1] = Point(bm2,bm2);
+		p[2] = Point(ReliefBd,bm2);
+		col012 = d;
+		col20 = l;
+	TRIdown =>
+		p[0] = Point(b2,bm2);
+		p[1] = Point(ReliefBd,ReliefBd);
+		p[2] = Point(bm2,ReliefBd);
+		col012 = l;
+		col20 = d;
+	TRIleft =>
+		p[0] = Point(bm2, ReliefBd);
+		p[1] = Point(bm2, bm2);
+		p[2] = Point(ReliefBd,b2);
+		col012 = d;
+		col20 = l;
+	TRIright =>
+		p[0] = Point(ReliefBd,bm2);
+		p[1] = Point(ReliefBd,ReliefBd);
+		p[2] = Point(bm2,b2);
+		col012 = l;
+		col20 = d;
+	}
+	if(style == ReliefSunk) {
+		t := col012;
+		col012 = col20;
+		col20 = t;
+	}
+	for(i := 0; i < 3; i++)
+		p[i] = p[i].add(r.min);
+	im.fillpoly(p, ~0, colorimage(Grey), zp);
+	im.line(p[0], p[1], 0, 0, ReliefBd/2, col012, zp);
+	im.line(p[1], p[2], 0, 0, ReliefBd/2, col012, zp);
+	im.line(p[2], p[0], 0, 0, ReliefBd/2, col20, zp);
+}
+
+abs(a: int) : int
+{
+	if(a < 0)
+		return -a;
+	return a;
+}
+
+Frame.new() : ref Frame
+{
+	f := ref Frame;
+	f.parent = nil;
+	f.cim = nil;
+	f.r = Rect(zp, zp);
+	f.animpid = 0;
+	f.reset();
+	return f;
+}
+
+Frame.newkid(parent: ref Frame, ki: ref Kidinfo, r: Rect) : ref Frame
+{
+	f := ref Frame;
+	f.parent = parent;
+	f.cim = parent.cim;
+	f.r = r;
+	f.animpid = 0;
+	f.reset();
+	f.src = ki.src;
+	f.name = ki.name;
+	f.marginw = ki.marginw;
+	f.marginh = ki.marginh;
+	f.framebd = ki.framebd;
+	f.flags = ki.flags;
+	return f;
+}
+
+# Note: f.parent, f.cim and f.r should not be reset
+# And if f.parent is true, don't reset params set in frameset.
+Frame.reset(f: self ref Frame)
+{
+	f.id = ++frameid;
+	f.doc = nil;
+	if(f.parent == nil) {
+		f.src = nil;
+		f.name = "";
+		f.marginw = FRMARGIN;
+		f.marginh = FRMARGIN;
+		f.framebd = 0;
+		f.flags = FRvscrollauto | FRhscrollauto;
+	}
+	f.layout = nil;
+	f.sublays = nil;
+	f.sublayid = 0;
+	f.controls = nil;
+	f.controlid = 0;
+	f.cr = f.r;
+	f.isdirty = 1;
+	f.dirtyr = f.cr;
+	f.viewr = Rect(zp, zp);
+	f.totalr = f.viewr;
+	f.vscr = nil;
+	f.hscr = nil;
+	hadkids := (f.kids != nil);
+	f.kids = nil;
+	if(f.animpid != 0)
+		CU->kill(f.animpid, 0);
+	if(J != nil && hadkids)
+		J->frametreechanged(f);
+	f.animpid = 0;
+}
+
+Frame.dirty(f: self ref Frame, r: Draw->Rect)
+{
+	if (f.isdirty)
+		f.dirtyr= f.dirtyr.combine(r);
+	else {
+		f.dirtyr = r;
+		f.isdirty = 1;
+	}
+}
+
+Frame.addcontrol(f: self ref Frame, c: ref Control) : int
+{
+	if(len f.controls <= f.controlid) {
+		newcontrols := array[len f.controls + 30] of ref Control;
+		newcontrols[0:] = f.controls;
+		f.controls = newcontrols;
+	}
+	f.controls[f.controlid] = c;
+	ans := f.controlid++;
+	return ans;
+}
+
+Frame.xscroll(f: self ref Frame, kind, val: int)
+{
+	newx := f.viewr.min.x;
+	case kind {
+	CAscrollpage =>
+		newx += val*(f.cr.dx()*8/10);
+	CAscrollline =>
+		newx += val*f.cr.dx()/10;
+	CAscrolldelta =>
+		newx += val;
+	CAscrollabs =>
+		newx = val;
+	}
+	f.scrollabs(Point(newx, f.viewr.min.y));
+}
+
+# Don't actually scroll by "page" and "line",
+# But rather, 80% and 10%, which give more
+# context in the first case, and more motion
+# in the second.
+Frame.yscroll(f: self ref Frame, kind, val: int)
+{
+	newy := f.viewr.min.y;
+	case kind {
+	CAscrollpage =>
+		newy += val*(f.cr.dy()*8/10);
+	CAscrollline =>
+		newy += val*f.cr.dy()/20;
+	CAscrolldelta =>
+		newy += val;
+	CAscrollabs =>
+		newy = val;
+	}
+	f.scrollabs(Point(f.viewr.min.x, newy));
+}
+
+Frame.scrollrel(f : self ref Frame, p : Point)
+{
+	(x, y) := p;
+	x += f.viewr.min.x;
+	y += f.viewr.min.y;
+	f.scrollabs(f.viewr.min.add(p));
+}
+
+Frame.scrollabs(f : self ref Frame, p : Point)
+{
+	(x, y) := p;
+	lay := f.layout;
+	margin := 0;
+	if (lay != nil)
+		margin = lay.margin;
+	x = max(0, min(x, f.totalr.max.x));
+	y = max(0, min(y, f.totalr.max.y + margin - f.cr.dy()));
+	(oldx, oldy) := f.viewr.min;
+	if (oldx != x || oldy != y) {
+		f.viewr.min = (x, y);
+		fixframegeom(f);
+		# blit scroll
+		dx := f.viewr.min.x - oldx;
+		dy := f.viewr.min.y - oldy;
+		origin := f.lptosp(zp);
+		destr := f.viewr.addpt(origin);
+		srcpt := destr.min.add((dx, dy));
+		oclipr := f.cim.clipr;
+		f.cim.clipr = f.cr;
+		f.cim.drawop(destr, f.cim, nil, srcpt, Draw->S);
+		if (dx > 0)
+			f.dirty(Rect((f.viewr.max.x - dx, f.viewr.min.y), f.viewr.max));
+		else if (dx < 0)
+			f.dirty(Rect(f.viewr.min, (f.viewr.min.x - dx, f.viewr.max.y)));
+
+		if (dy > 0)
+			f.dirty(Rect((f.viewr.min.x, f.viewr.max.y-dy), f.viewr.max));
+		else if (dy < 0)
+			f.dirty(Rect(f.viewr.min, (f.viewr.max.x, f.viewr.min.y-dy)));
+#f.cim.draw(destr, display.white, nil, zp);
+		drawall(f);
+		f.cim.clipr = oclipr;
+	}
+}
+
+# Convert layout coords (where (0,0) is top left of layout)
+# to screen coords (i.e., coord system of mouse, f.cr, etc.)
+Frame.sptolp(f: self ref Frame, sp: Point) : Point
+{
+	return f.viewr.min.add(sp.sub(f.cr.min));
+}
+
+# Reverse translation of sptolp
+Frame.lptosp(f: self ref Frame, lp: Point) : Point
+{
+	return lp.add(f.cr.min.sub(f.viewr.min));
+}
+
+# Return Loc of Item or Scrollbar containing p (p in screen coords)
+# or item it, if that is not nil.
+Frame.find(f: self ref Frame, p: Point, it: ref Item) : ref Loc
+{
+	return framefind(Loc.new(), f, p, it);
+}
+
+# Find it (if non-nil) or place where p is (known to be inside f's layout).
+framefind(loc: ref Loc, f: ref Frame, p: Point, it: ref Item) : ref Loc
+{
+	loc.add(LEframe, f.r.min);
+	loc.le[loc.n-1].frame = f;
+	if(it == nil) {
+		if(f.vscr != nil && p.in(f.vscr.r)) {
+			loc.add(LEcontrol, f.vscr.r.min);
+			loc.le[loc.n-1].control = f.vscr;
+			loc.pos = p.sub(f.vscr.r.min);
+			return loc;
+		}
+		if(f.hscr != nil && p.in(f.hscr.r)) {
+			loc.add(LEcontrol, f.hscr.r.min);
+			loc.le[loc.n-1].control = f.hscr;
+			loc.pos = p.sub(f.hscr.r.min);
+			return loc;
+		}
+	}
+	if(it != nil || p.in(f.cr)) {
+		lay := f.layout;
+		if(f.kids != nil) {
+			for(fl := f.kids; fl != nil; fl = tl fl) {
+				kf := hd fl;
+				try := framefind(loc, kf, p, it);
+				if(try != nil)
+					return try;
+			}
+		}
+		else if(lay != nil)
+			return layfind(loc, f, lay, f.lptosp(zp), p, it);
+	}
+	return nil;
+}
+
+# Find it (if non-nil) or place where p is (known to be inside f's layout).
+# p (in screen coords), lay offset by origin also in screen coords
+layfind(loc: ref Loc, f: ref Frame, lay: ref Lay, origin, p: Point, it: ref Item) : ref Loc
+{
+	for(flist := lay.floats; flist != nil; flist = tl flist) {
+		fl := hd flist;
+		fymin := fl.y+origin.y;
+		fymax := fymin + fl.item.height;
+		inside := 0;
+		xx : int;
+		if(it != nil || (fymin <= p.y && p.y < fymax)) {
+			xx = origin.x + lay.margin;
+			if(fl.side == Aright) {
+				xx -= fl.x;
+				xx += lay.targetwidth;
+#				if(lay == f.layout)
+#					xx = origin.x + (f.cr.dx() - lay.margin) - fl.x;
+##					xx += f.cr.dx() - fl.x;
+#				else
+#					xx += lay.width - fl.x;
+			}
+			else
+				xx += fl.x;
+			if(p.x >= xx && p.x < xx+fl.item.width)
+					inside = 1;
+		}
+		fp := Point(xx,fymin);
+		match := 0;
+		if(it != nil) {
+			pick fi := fl.item {
+			Itable =>
+				loc.add(LEitem, fp);
+				loc.le[loc.n-1].item = fl;
+				loc.pos = p.sub(fp);
+				lloc := tablefind(loc, f, fi, fp, p, it);
+				if(lloc != nil)
+					return lloc;
+			Iimage =>
+				match = (it == fl || it == fl.item);
+			}
+		}
+		if(match || inside) {
+			loc.add(LEitem, fp);
+			loc.le[loc.n-1].item = fl;
+			loc.pos = p.sub(fp);
+			if(it == fl.item) {
+				loc.add(LEitem, fp);
+				loc.le[loc.n-1].item = fl.item;
+			}
+			if(inside) {
+				pick fi := fl.item {
+				Itable =>
+					loc = tablefind(loc, f, fi, fp, p, it);
+				}
+			}
+			return loc;
+		}
+	}
+	for(l :=lay.start; l != nil; l = l.next) {
+		o := origin.add(l.pos);
+		if(it != nil || (o.y <= p.y && p.y < o.y+l.height)) {
+			lloc := linefind(loc, f, l, o, p, it);
+			if(lloc != nil)
+				return lloc;
+			if(it == nil && o.y + l.height >= p.y)
+				break;
+		}
+	}
+	return nil;
+}
+
+# p (in screen coords), line at o, also in screen coords
+linefind(loc: ref Loc, f: ref Frame, l: ref Line, o, p: Point, it: ref Item) : ref Loc
+{
+	loc.add(LEline, o);
+	loc.le[loc.n-1].line = l;
+	x := o.x;
+	y := o.y;
+	inside := 0;
+	for(i := l.items; i != nil; i = i.next) {
+		if(it != nil || (x <= p.x && p.x < x+i.width)) {
+			yy := y;
+			h := 0;
+			pick pi := i {
+			Itext =>
+				fnt := fonts[pi.fnt].f;
+				yy += l.ascent - fnt.ascent + (int pi.voff) - Voffbias;
+				h = fnt.height;
+			Irule =>
+				h = pi.size;
+			Iimage =>
+				yy = y;
+				if(pi.align == Abottom)
+					yy += l.ascent - pi.imheight;
+				else if(pi.align == Amiddle)
+					yy += l.ascent - (pi.imheight/2);
+				h = pi.imheight;
+			Iformfield =>
+				h = pi.height;
+				yy += l.ascent - pi.ascent;
+				if(it != nil) {
+					if(it == pi.formfield.image) {
+						loc.add(LEitem, Point(x,yy));
+						loc.le[loc.n-1].item = i;
+						loc.add(LEitem, Point(x,yy));
+						loc.le[loc.n-1].item = it;
+						loc.pos = zp;	# doesn't matter, its an 'it' test
+						return loc;
+					}
+				}
+				else if(yy < p.y && p.y < yy+h && pi.formfield.ctlid >= 0) {
+					loc.add(LEcontrol, Point(x,yy));
+					loc.le[loc.n-1].control = f.controls[pi.formfield.ctlid];
+					loc.pos = p.sub(Point(x,yy));
+					return loc;
+				}
+			Itable =>
+				lloc := tablefind(loc, f, pi, Point(x,y), p, it);
+				if(lloc != nil)
+					return lloc;
+				# else leave h==0 so p test will fail
+
+			# floats were handled separately. nulls can be picked by 'it' test
+			# leave h==0, so p test will fail
+			}
+			if(it == i || (it == nil && yy <= p.y && p.y < yy+h)) {
+				loc.add(LEitem, Point(x,yy));
+				loc.le[loc.n-1].item = i;
+				loc.pos = p.sub(Point(x,yy));
+				return loc;
+			}
+			if(it == nil)
+				return nil;
+		}
+		x += i.width;
+		if(it == nil && x >= p.x)
+			break;
+	}
+	loc.n--;
+	return nil;
+}
+
+tablefind(loc: ref Loc, f: ref Frame, ti: ref Item.Itable, torigin: Point, p: Point, it: ref Item) : ref Loc
+{
+	loc.add(LEitem, torigin);
+	loc.le[loc.n-1].item = ti;
+	t := ti.table;
+	(hsp, vsp, pad, bd, cbd, hsep, vsep) := tableparams(t);
+	if(t.caption_lay >= 0) {
+		caplay := f.sublays[t.caption_lay];
+		capy := torigin.y;
+		if(t.caption_place == Abottom)
+			capy += t.toth-t.caph+vsp;
+		lloc := layfind(loc, f, caplay, Point(torigin.x,capy), p, it);
+		if(lloc != nil)
+			return lloc;
+	}
+	for(cl := t.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		if(c.layid == -1 || c.layid >= len f.sublays)
+			continue;
+		clay := f.sublays[c.layid];
+		if(clay == nil)
+			continue;
+		cx := torigin.x + t.cols[c.col].pos.x;
+		cy := torigin.y + t.rows[c.row].pos.y;
+		wd := cellwidth(t, c, hsep);
+		ht := cellheight(t, c, vsep);
+		if(it == nil && !p.in(Rect(Point(cx,cy),Point(cx+wd,cy+ht))))
+			continue;
+		if(c.align.valign != Atop && c.align.valign != Abaseline) {
+			n := ht - clay.height;
+			if(c.align.valign == Amiddle)
+				cy += n/2;
+			else if(c.align.valign == Abottom)
+				cy += n;
+		}
+		loc.add(LEtablecell, Point(cx,cy));
+		loc.le[loc.n-1].tcell = c;
+		lloc := layfind(loc, f, clay, Point(cx,cy), p, it);
+		if(lloc != nil)
+			return lloc;
+		loc.n--;
+		if(it == nil)
+			return nil;
+	}
+	loc.n--;
+	return nil;
+}
+
+# (called from jscript)
+# 'it' is an Iimage item in frame f whose image is to be switched
+# to come from the src URL.
+#
+# For now, assume this is called only after the entire build process
+# has finished.  Also, only handle the case where the image has
+# been preloaded and is in the cache now.  This isn't right (BUG), but will
+# cover most of the cases of extant image swapping, and besides,
+# image swapping is mostly cosmetic anyway.
+# 
+# For now, pay no attention to scaling issues or animation issues.
+Frame.swapimage(f: self ref Frame, im: ref Item.Iimage, src: string)
+{
+	u := U->parse(src);
+	if(u.scheme == "")
+		return;
+	u = U->mkabs(u, f.doc.base);
+	# width=height=0 finds u if in cache
+	newci := CImage.new(u, nil, 0, 0);
+	cachedci := (CU->imcache).look(newci);
+	if(cachedci == nil || cachedci.mims == nil)
+		return;
+	im.ci = cachedci;
+
+	# we're assuming image will have same dimensions
+	# as one that is replaced, so no relayout is needed;
+	# otherwise need to call haveimage() instead of drawall()
+	# Netscape scales replacement image to size of replaced image
+
+	f.dirty(f.totalr);
+	drawall(f);
+}
+
+Frame.focus(f : self ref Frame, focus, raisex : int)
+{
+	di := f.doc;
+	if (di == nil || (CU->config).doscripts == 0)
+		return;
+	if (di.evmask && raisex) {
+		kind := E->SEonfocus;
+		if (!focus)
+			kind = E->SEonblur;
+		if(di.evmask & kind)
+			se := ref E->ScriptEvent(kind, f.id, -1, -1, -1, -1, -1, -1, 0, nil, nil, 0);
+	}
+}
+
+Control.newff(f: ref Frame, ff: ref B->Formfield) : ref Control
+{
+	ans : ref Control = nil;
+	case ff.ftype {
+	Ftext or Fpassword or Ftextarea =>
+		nh := ff.size;
+		nv := 1;
+		linewrap := 0;
+		if(ff.ftype == Ftextarea) {
+			nh = ff.cols;
+			nv = ff.rows;
+			linewrap = 1;
+		}
+		ans = Control.newentry(f, nh, nv, linewrap);
+		if(ff.ftype == Fpassword)
+			ans.flags |= CFsecure;
+		ans.entryset(ff.value);
+	Fcheckbox or Fradio =>
+		ans = Control.newcheckbox(f, ff.ftype==Fradio);
+		if((ff.flags&B->FFchecked) != byte 0)
+			ans.flags |= CFactive;
+	Fsubmit or Fimage or Freset or Fbutton =>
+		if(ff.image == nil)
+			ans = Control.newbutton(f, nil, nil, ff.value, nil, 0, 1);
+		else {
+			pick i := ff.image {
+			Iimage =>
+				pic, picmask : ref Image;
+				if(i.ci.mims != nil) {
+					pic = i.ci.mims[0].im;
+					picmask = i.ci.mims[0].mask;
+				}
+				lab := "";
+				if((CU->config).imagelvl == CU->ImgNone) {
+					lab = i.altrep;
+					i = nil;
+				}
+				ans = Control.newbutton(f, pic, picmask, lab, i, 0, 0);
+			}
+		}
+	Fselect =>
+		n := len ff.options;
+		if(n > 0) {
+			ao := array[n] of Option;
+			l := ff.options;
+			for(i := 0; i < n; i++) {
+				o := hd l;
+				# these are copied, so selected can be used for current state
+				ao[i] = *o;
+				l = tl l;
+			}
+			nvis := ff.size;
+			ans = Control.newselect(f, nvis, ao);
+		}
+	Ffile =>
+		if(dbg)
+			sys->print("warning: unimplemented file form field\n");
+	}
+	if(ans != nil)
+		ans.ff = ff;
+	return ans;
+}
+
+Control.newscroll(f: ref Frame, isvert, length, breadth: int) : ref Control
+{
+	# need room for at least two squares and 2 borders of size 2
+	if(length < 12) {
+		breadth = 0;
+		length = 0;
+	}
+	else if(breadth*2 + 4 > length)
+		breadth = (length - 4) / 2;
+	maxpt : Point;
+	flags := CFenabled;
+	if(isvert) {
+		maxpt = Point(breadth, length);
+		flags |= CFscrvert;
+	}
+	else
+		maxpt = Point(length, breadth);
+	return ref Control.Cscrollbar(f, nil, Rect(zp,maxpt), flags, nil, 0, 0, 1, 0, nil, (0, 0));
+}
+
+Control.newentry(f: ref Frame, nh, nv, linewrap: int) : ref Control
+{
+	w := ctlcharspace*nh + 2*ENTHMARGIN;
+	h := ctllinespace*nv + 2*ENTVMARGIN;
+	scr : ref Control;
+	if (linewrap) {
+		scr = Control.newscroll(f, 1, h-4, SCRFBREADTH);
+		scr.r.addpt(Point(w,0));
+		w += SCRFBREADTH;
+	}
+	ans := ref Control.Centry(f, nil, Rect(zp,Point(w,h)), CFenabled, nil, scr, "", (0, 0), 0, linewrap, 0);
+	if (scr != nil) {
+		pick pscr := scr {
+		Cscrollbar =>
+			pscr.ctl = ans;
+		}
+		scr.scrollset(0, 1, 1, 0, 0);
+	}
+	return ans;
+}
+
+Control.newbutton(f: ref Frame, pic, picmask: ref Image, lab: string, it: ref Item.Iimage, candisable, dorelief: int) : ref Control
+{
+	dpic, dpicmask: ref Image;
+	w := 0;
+	h := 0;
+	if(pic != nil) {
+		w = pic.r.dx();
+		h = pic.r.dy();
+	}
+	else if(it != nil) {
+		w = it.imwidth;
+		h = it.imheight;
+	}
+	else {
+		w = fonts[CtlFnt].f.width(lab);
+		h = ctllinespace;
+	}
+	if(dorelief) {
+		# form image buttons are shown without margins in other browsers
+		w += 2*BUTMARGIN;
+		h += 2*BUTMARGIN;
+	}
+	r := Rect(zp, Point(w,h));
+	if(candisable && pic != nil) {
+		# make "greyed out" image:
+		#	- convert pic to monochrome (ones where pic is non-white)
+		#	- draw pic in White, then DarkGrey shifted (-1,-1) and use
+		#	    union of those two areas as mask
+		dpicmask = display.newimage(pic.r, Draw->GREY1, 0, D->White);
+		dpic = display.newimage(pic.r, pic.chans, 0, D->White);
+		dpic.draw(dpic.r, colorimage(White), pic, zp);
+		dpicmask.draw(dpicmask.r, display.black, pic, zp);
+		dpic.draw(dpic.r.addpt(Point(-1,-1)), colorimage(DarkGrey), pic, zp);
+		dpicmask.draw(dpicmask.r.addpt(Point(-1,-1)), display.black, pic, zp);
+	}
+	b := ref Control.Cbutton(f, nil, r, CFenabled, nil, pic, picmask, dpic, dpicmask, lab, dorelief);
+	return b;
+}
+
+Control.newcheckbox(f: ref Frame, isradio: int) : ref Control
+{
+	return ref Control.Ccheckbox(f, nil, Rect((0,0),(CBOXWID,CBOXHT)), CFenabled, nil, isradio);
+}
+
+Control.newselect(f: ref Frame, nvis: int, options: array of B->Option) : ref Control
+{
+	nvis = min(5, len options);
+	if (nvis < 1)
+		nvis = 1;
+	fnt := fonts[CtlFnt].f;
+	w := 0;
+	first := -1;
+	for(i := 0; i < len options; i++) {
+		if (first == -1 && options[i].selected)
+			first = i;
+		w = max(w, fnt.width(options[i].display));
+	}
+	if (first == -1)
+		first = 0;
+	if (len options -nvis > 0 && len options - nvis < first)
+		first = len options - nvis;
+	w += 2*SELMARGIN;
+	h := ctllinespace*nvis + 2*SELMARGIN;
+	scr: ref Control;
+	if (nvis > 1 && nvis < len options) {
+		scr = Control.newscroll(f, 1, h, SCRFBREADTH);
+		scr.r.addpt(Point(w,0));
+	}
+	if (nvis < len options)
+		w += SCRFBREADTH;
+	ans := ref Control.Cselect(f, nil, Rect(zp, Point(w,h)), CFenabled, nil, nil, scr, nvis, first, options);
+	if(scr != nil) {
+		pick pscr := scr {
+		Cscrollbar =>
+			pscr.ctl = ans;
+		}
+		scr.scrollset(first, first+nvis, len options, len options, 0);
+	}
+	return ans;
+}
+
+Control.newlistbox(f: ref Frame, nrow, ncol: int, options: array of B->Option) : ref Control
+{
+	fnt := fonts[CtlFnt].f;
+	w := charspace*ncol + 2*SELMARGIN;
+	h := fnt.height*nrow + 2*SELMARGIN;
+
+	vscr: ref Control = nil;
+	#if(nrow < len options) {
+		vscr = Control.newscroll(f, 1, (h-4)+SCRFBREADTH, SCRFBREADTH);
+		vscr.r.addpt(Point(w-SCRFBREADTH,0));
+		w += SCRFBREADTH;
+	#}
+
+	maxw := 0;
+	for(i := 0; i < len options; i++)
+		maxw = max(maxw, fnt.width(options[i].display));
+
+	hscr: ref Control = nil;
+	#if(w < maxw) {
+		# allow for border (inset(2))
+		hscr = Control.newscroll(f, 0, (w-4)-SCRFBREADTH, SCRFBREADTH);
+		hscr.r.addpt(Point(0, h-SCRBREADTH));
+		h += SCRFBREADTH;
+	#}
+
+	ans := ref Control.Clistbox(f, nil, Rect(zp, Point(w,h)), CFenabled, nil, hscr, vscr, nrow, 0, 0, maxw/charspace, options, nil);
+	if(vscr != nil) {
+		pick pscr := vscr {
+		Cscrollbar =>
+			pscr.ctl = ans;
+		}
+		vscr.scrollset(0, nrow, len options, len options, 0);
+	}
+	if(hscr != nil) {
+		pick pscr := hscr {
+		Cscrollbar =>
+			pscr.ctl = ans;
+		}
+		hscr.scrollset(0, w-SCRFBREADTH, maxw, 0, 0);
+	}
+	return ans;	
+}
+
+Control.newanimimage(f: ref Frame, cim: ref CU->CImage, bg: Background) : ref Control
+{
+	return ref Control.Canimimage(f, nil, Rect((0,0),(cim.width,cim.height)), 0, nil, cim, 0, 0, big 0, bg);
+}
+
+Control.newlabel(f: ref Frame, s: string) : ref Control
+{
+	w := fonts[DefFnt].f.width(s);
+	h := ctllinespace + 2*ENTVMARGIN;	# give it same height as an entry box
+	return ref Control.Clabel(f, nil, Rect(zp,Point(w,h)), 0, nil, s);
+}
+
+Control.disable(c: self ref Control)
+{
+	if(c.flags & CFenabled) {
+		win := c.f.cim;
+		c.flags &= ~CFenabled;
+		if(c.f.cim != nil)
+			c.draw(1);
+	}
+}
+
+Control.enable(c: self ref Control)
+{
+	if(!(c.flags & CFenabled)) {
+		c.flags |= CFenabled;
+		if(c.f.cim != nil)
+			c.draw(1);
+	}
+}
+
+changeevent(c: ref Control)
+{
+	onchange := 0;
+	pick pc := c {
+	Centry =>
+		onchange = pc.onchange;
+		pc.onchange = 0;
+# this code reproduced Navigator 2 bug
+# changes to Select Formfield selection only resulted in onchange event upon
+# loss of focus.  Now handled by domouse() code so event can be raised
+# immediately
+#	Cselect =>
+#		onchange = pc.onchange;
+#		pc.onchange = 0;
+	}
+	if(onchange && (c.ff.evmask & E->SEonchange)) {
+		se := ref E->ScriptEvent(E->SEonchange, c.f.id, c.ff.form.formid, c.ff.fieldid, -1, -1, -1, -1, 1, nil, nil, 0);
+		J->jevchan <-= se;
+	}
+}
+
+blurfocusevent(c: ref Control, kind, raisex: int)
+{
+	if((CU->config).doscripts && c.ff != nil && c.ff.evmask) {
+		if(kind == E->SEonblur)
+			changeevent(c);
+		if (!raisex || !(c.ff.evmask & kind))
+			return;
+		se := ref E->ScriptEvent(kind, c.f.id, c.ff.form.formid, c.ff.fieldid, -1, -1, -1, -1, 0, nil, nil, 0);
+		J->jevchan <-= se;
+	}
+}
+
+Control.losefocus(c: self ref Control, raisex: int)
+{
+	if(c.flags & CFhasfocus) {
+		c.flags &= ~CFhasfocus;
+		if(c.f.cim != nil) {
+			blurfocusevent(c, E->SEonblur, raisex);
+			c.draw(1);
+		}
+	}
+}
+
+Control.gainfocus(c: self ref Control, raisex: int)
+{
+	if(!(c.flags & CFhasfocus)) {
+		c.flags |= CFhasfocus;
+		if(c.f.cim != nil) {
+			blurfocusevent(c, E->SEonfocus, raisex);
+			c.draw(1);
+		}
+		G->clientfocus();
+	}
+}
+
+Control.scrollset(c: self ref Control, v1, v2, vmax, nsteps, draw: int)
+{
+	pick sc := c {
+	Cscrollbar =>
+		if(v1 < 0)
+			v1 = 0;
+		if(v2 > vmax)
+			v2 = vmax;
+		if(v1 > v2)
+			v1 = v2;
+		if(v1 == 0 && v2 == vmax) {
+			sc.mindelta = 1;
+			sc.deltaval = 0;
+			sc.top = 0;
+			sc.bot = 0;
+		}
+		else {
+			length, breadth: int;
+			if(sc.flags&CFscrvert) {
+				length = sc.r.max.y - sc.r.min.y;
+				breadth = sc.r.max.x - sc.r.min.x;
+			}
+			else {
+				length = sc.r.max.x - sc.r.min.x;
+				breadth = sc.r.max.y - sc.r.min.y;
+			}
+			l := length - (2*breadth + MINSCR);
+			if(l <= 0)
+				l = 1;
+			if(l < 0)
+				raise "EXInternal: negative scrollbar trough";
+			sc.top = l*v1/vmax;
+			sc.bot = l*(vmax-v2)/vmax;
+			if (nsteps == 0)
+				sc.mindelta = 1;
+			else
+				sc.mindelta = max(1, length/nsteps);
+			sc.deltaval = max(1, vmax/(l/sc.mindelta))*SCRDELTASF;
+		}
+		if(sc.f.cim != nil && draw)
+			sc.draw(1);
+	}
+}
+
+SPECMASK : con 16rf000;
+CTRLMASK : con 16r1f;
+DEL : con 16r7f;
+TAB : con '\t';
+CR: con '\n';
+
+Control.dokey(ctl: self ref Control, keychar: int) : int
+{
+	if(!(ctl.flags&CFenabled))
+		return CAnone;
+	ans := CAnone;
+	pick c := ctl {
+	Centry =>
+		olds := c.s;
+		slen := len c.s;
+		(sels, sele) := normalsel(c.sel);
+		modified := 0;
+		(osels, osele) := (sels, sele);
+		case keychar {
+			('a' & CTRLMASK) or Keyboard->Home =>
+				(sels, sele) = (0, 0);
+			('e' & CTRLMASK) or Keyboard->End =>
+				(sels, sele) = (slen, slen);
+			'f' & CTRLMASK or Keyboard->Right =>
+				if(sele < slen)
+					(sels, sele) = (sele+1, sele+1);
+			'b' & CTRLMASK or Keyboard->Left =>
+				if(sels > 0)
+					(sels, sele) = (sels-1, sels-1);
+			Keyboard->Up =>
+				if (c.linewrap)
+					sels = sele = entryupdown(c, sels, -1);
+			Keyboard->Down =>
+				if (c.linewrap)
+					sels = sele = entryupdown(c, sele, 1);
+			'u' & CTRLMASK =>
+				entrydelrange(c, 0, slen);
+				modified = 1;
+				(sels, sele) = c.sel;
+			'c' & CTRLMASK =>
+				entrysetsnarf(c);
+			'v' & CTRLMASK =>
+				entryinsertsnarf(c);
+				modified = 1;
+				(sels, sele) = c.sel;
+			'h' & CTRLMASK or DEL=>
+				if (sels != sele) {
+					entrydelrange(c, sels, sele);
+					modified = 1;
+				} else if(sels > 0) {
+					entrydelrange(c, sels-1, sels);
+					modified = 1;
+				}
+				(sels, sele) = c.sel;
+			Keyboard->Del =>
+				if (sels != sele) {
+					entrydelrange(c, sels, sele);
+					modified = 1;
+				} else if(sels < len c.s) {
+					entrydelrange(c, sels, sels+1);
+					modified = 1;
+				}
+				(sels, sele) = c.sel;
+			TAB =>
+				ans = CAtabkey;
+			* =>
+				if ((keychar & SPECMASK) == Keyboard->Spec)
+					# ignore all other special keys
+					break;
+				if(keychar == CR) {
+					if(c.linewrap)
+						keychar = '\n';
+					else
+						ans = CAreturnkey;
+				}
+				if(keychar > CTRLMASK || (keychar == '\n' && c.linewrap)) {
+					if (sels != sele) {
+						entrydelrange(c, sels, sele);
+						(sels, sele) = c.sel;
+					}
+					slen = len c.s;
+					c.s[slen] = 0;	# expand string by 1 char
+					for(k := slen; k > sels; k--)
+						c.s[k] = c.s[k-1];
+					c.s[sels] = keychar;
+					(sels, sele) = (sels+1, sels+1);
+					modified = 1;
+				}
+		}
+		c.sel = (sels, sele);
+		if(osels != sels || osele != sele || modified) {
+			entryscroll(c);
+			c.draw(1);
+		}
+		if (c.s != olds)
+			c.onchange = 1;
+	}
+	return ans;
+}
+
+Control.domouse(ctl: self ref Control, p: Point, mtype: int, oldgrab : ref Control) : (int, ref Control)
+{
+	up := (mtype == E->Mlbuttonup || mtype == E->Mldrop);
+	down := (mtype == E->Mlbuttondown);
+	drag := (mtype == E->Mldrag);
+	hold := (mtype == E->Mhold);
+	move := (mtype == E->Mmove);
+
+	# any button actions stop auto-repeat
+	# it's up to the individual controls to re-instate it
+	if (!move)
+		E->autorepeat(nil, 0, 0);
+
+	if(!(ctl.flags&CFenabled))
+		return (CAnone, nil);
+	ans := CAnone;
+	changed := 0;
+	newgrab : ref Control;
+	grabbed := oldgrab != nil;
+	pick c := ctl {
+	Cbutton =>
+		if(down) {
+			c.flags |= CFactive;
+			newgrab = c;
+			changed = 1;
+		}
+		else if(move && c.ff == nil) {
+			ans = CAflyover;
+		}
+		else if (drag && grabbed) {
+			newgrab = c;
+			active := 0;
+			if (p.in(c.r))
+				active = CFactive;
+			if ((c.flags & CFactive) != active)
+				changed = 1;
+			c.flags = (c.flags & ~CFactive) | active;
+		}
+		else if(up) {
+			if (c.flags & CFactive)
+				ans = CAbuttonpush;
+			c.flags &= ~CFactive;
+			changed = 1;
+		}
+	Centry =>
+		if(c.scr != nil && !grabbed && p.x >= c.r.max.x-SCRFBREADTH) {
+			pick scr := c.scr {
+			Cscrollbar =>
+				return scr.domouse(p, mtype, oldgrab);
+			}
+		}
+		(sels, sele) := c.sel;
+		if(mtype == E->Mlbuttonup && grabbed) {
+			if (sels != sele)
+				ans = CAselected;
+		}
+		if(down || (drag && grabbed)) {
+			newgrab = c;
+			x := c.r.min.x+ENTHMARGIN;
+			fnt := fonts[CtlFnt].f;
+			s := c.s;
+			if(c.flags&CFsecure) {
+				for(i := 0; i < len s; i++)
+					s[i] = '*';
+			}
+			(osels, osele) := c.sel;
+			s1 := " ";
+			i := 0;
+			iend := len s - 1;
+			if(c.linewrap) {
+				(lines, linestarts, topline, cursline) := entrywrapcalc(c);
+				if(len lines > 1) {
+					lineno := topline + (p.y - (c.r.min.y+ENTVMARGIN)) / ctllinespace;
+					lineno = min(lineno, len lines -1);
+					lineno = max(lineno, 0);
+
+					i = linestarts[lineno];
+					iend = i + len lines[lineno] -1;
+				}
+			} else
+				x -= fnt.width(s[:c.left]);
+			for(; i <= iend; i++) {
+				s1[0] = s[i];
+				cx := fnt.width(s1);
+				if(p.x < x + cx)
+					break;
+				x += cx;
+			}
+			sele = i;
+
+			if (down)
+				sels = sele;
+			c.sel = (sels, sele);
+
+			if (sels != osels || sele != osele) {
+				changed = 1;
+				entryscroll(c);
+				if (p.x < c.r.min.x + ENTHMARGIN || p.x > c.r.max.x - ENTHMARGIN
+				|| p.y < c.r.min.y + ENTVMARGIN || p.y > c.r.max.y - ENTVMARGIN) {
+					E->autorepeat(ref (Event.Emouse)(p, mtype), ARTICK, ARTICK);
+				}
+			}
+
+			if(!(c.flags&CFhasfocus))
+				ans = CAkeyfocus;
+		}
+	Ccheckbox=>
+		if(up) {
+			if(c.isradio) {
+				if(!(c.flags&CFactive)) {
+					c.flags |= CFactive;
+					changed = 1;
+					ans = CAbuttonpush;
+					# turn off other radio button
+					frm := c.ff.form;
+					for(lf := frm.fields; lf != nil; lf = tl lf) {
+						ff := hd lf;
+						if(ff == c.ff)
+							continue;
+						if(ff.ftype == Fradio && ff.name==c.ff.name && ff.ctlid >= 0) {
+							d := c.f.controls[ff.ctlid];
+							if(d.flags&CFactive) {
+								d.flags &= ~CFactive;
+								d.draw(1);
+								break;		# at most one other should be on
+							}
+						}
+					}
+				}
+			}
+			else {
+				c.flags ^= CFactive;
+				changed = 1;
+			}
+		}
+	Cselect =>
+		if (c.nvis == 1 && up && c.popup == nil && c.r.contains(p))
+			return (CAdopopup, nil);
+		if(c.scr != nil && (grabbed || p.x >= c.r.max.x-SCRFBREADTH)) {
+			pick scr := c.scr {
+			Cscrollbar =>
+				(a, grab) := scr.domouse(p, mtype, oldgrab);
+				if (grab != nil)
+					grab = c;
+				return (a, grab);
+			}
+			return (ans, nil);
+		}
+		n := (p.y - (c.r.min.y+SELMARGIN))/ctllinespace + c.first;
+		if (n >= c.first && n < c.first+c.nvis) {
+			if ((c.ff.flags&B->FFmultiple) != byte 0) {
+				if (down) {
+					c.options[n].selected ^= 1;
+					changed = 1;
+				}
+			} else if (up || drag) {
+				changed = c.options[n].selected == 0;
+				c.options[n].selected = 1;
+				for(i := 0; i < len c.options; i++) {
+					if(i != n)
+						c.options[i].selected = 0;
+				}
+			}
+		}
+		if (up) {
+			if (c.popup != nil)
+				ans = CAdonepopup;
+			else
+				ans = CAchanged;
+		}
+	Clistbox =>
+		if(c.vscr != nil && (c.grab == c.vscr || (!grabbed && p.x >= c.r.max.x-SCRFBREADTH))) {
+			c.grab = nil;
+			pick vscr := c.vscr {
+			Cscrollbar =>
+				(a, grab) := vscr.domouse(p, mtype, oldgrab);
+				if (grab != nil) {
+					c.grab = c.vscr;
+					grab = c;
+				}
+				return (a, grab);
+			}
+		}
+		else if(c.hscr != nil && (c.grab == c.hscr || (!grabbed && p.y >= c.r.max.y-SCRFBREADTH))) {
+			c.grab = nil;
+			pick hscr := c.hscr {
+			Cscrollbar =>
+				(a, grab) := hscr.domouse(p, mtype, oldgrab);
+				if (grab != nil) {
+					c.grab = c.hscr;
+					grab = c;
+				}
+				return (a, grab);
+			}
+		}
+		else if(up) {
+			fnt := fonts[CtlFnt].f;
+			n := (p.y - (c.r.min.y+SELMARGIN))/fnt.height + c.first;
+			if(n >= 0 && n < len c.options) {
+				c.options[n].selected ^= 1;
+				# turn off other selections
+				for(i := 0; i < len c.options; i++) {
+					if(i != n)
+						c.options[i].selected = 0;
+				}
+				ans = CAchanged;
+				changed = 1;
+			}
+		}
+	Cscrollbar =>
+		val := 0;
+		v, vmin, vmax, b: int;
+		if(c.flags&CFscrvert) {
+			v = p.y;
+			vmin = c.r.min.y;
+			vmax = c.r.max.y;
+			b = c.r.dx();
+		}
+		else {
+			v = p.x;
+			vmin = c.r.min.x;
+			vmax = c.r.max.x;
+			b = c.r.dy();
+		}
+		vsltop := vmin+b+c.top;
+		vslbot := vmax-b-c.bot;
+		actflags := 0;
+		oldactflags := c.flags&CFscrallact;
+
+		if ((down || drag) && !up && !hold)
+			newgrab = c;
+
+		if (down) {
+			newgrab = c;
+			holdval := 0;
+			repeat := 1;
+			if (v >= vsltop && v < vslbot) {
+				holdval = v - vsltop;
+				actflags = CFactive;
+				repeat = 0;
+			}
+			if(v < vmin+b) {
+				holdval = -1;
+				actflags = CFscracta1;
+			}
+			else if(v < vsltop) {
+				holdval = -1;
+				actflags = CFscracttr1;
+			}
+			else if(v >= vmax-b) {
+				holdval = 1;
+				actflags = CFscracta2;
+			}
+			else if(v >= vslbot) {
+				holdval = 1;
+				actflags = CFscracttr2;
+			}
+			c.holdstate = (actflags, holdval);
+			if (repeat) {
+				E->autorepeat(ref (Event.Emouse)(p, E->Mhold), ARPAUSE, ARTICK);
+			}
+		}
+		if (drag) {
+			(actflags, val) = c.holdstate;
+			if (actflags == CFactive) {
+				# dragging main scroll widget (relative to top of drag block)
+				val = (v - vsltop) - val;
+				if(abs(val) >= c.mindelta) {
+					ans = CAscrolldelta;
+					val = (c.deltaval * (val / c.mindelta))/SCRDELTASF;
+				}
+			} else {
+				E->autorepeat(ref (Event.Emouse)(p, E->Mhold), ARTICK, ARTICK);
+			}
+		}
+		if (up || hold) {
+			# set the action according to the hold state
+			# Note: main widget (case CFactive) handled by drag
+			act := 0;
+			(act, val) = c.holdstate;
+			case act {
+			CFscracta1 or
+			CFscracta2 =>
+				ans = CAscrollline;
+			CFscracttr1 or
+			CFscracttr2 =>
+				ans = CAscrollpage;
+			}
+			if (up) {
+				c.holdstate = (0, 0);
+			} else { # hold
+				(actflags, nil) = c.holdstate;
+				if (ans != CAnone) {
+					E->autorepeat(ref (Event.Emouse)(p, E->Mhold), ARTICK, ARTICK);
+					newgrab = c;
+				}
+			}
+		}
+		c.flags = (c.flags & ~CFscrallact) | actflags;
+		if(ans != CAnone) {
+			ftoscroll := c.f;
+			if(c.ctl != nil) {
+				pick cff := c.ctl {
+				Centry =>
+					ny := (cff.r.dy() - 2 * ENTVMARGIN) / ctllinespace;
+					(nil, linestarts, topline, nil) := entrywrapcalc(cff);
+					nlines := len linestarts;
+					case ans {
+					CAscrollpage =>
+						topline += val*ny;
+					CAscrollline =>
+						topline += val;
+					CAscrolldelta =>
+#						# insufficient for large number of lines
+						topline += val;
+#						if(val > 0)
+#							topline++;
+#						else
+#							topline--;
+					}
+					if (topline+ny >= nlines)
+						topline = (nlines-1) - ny;
+					if (topline < 0)
+						topline = 0;
+					cff.left = linestarts[topline];
+					c.scrollset(topline, topline+ny, nlines - 1, nlines, 1);
+					cff.draw(1);
+					return (ans, newgrab);
+				Cselect =>
+					newfirst := cff.first;
+					case ans {
+					CAscrollpage =>
+						newfirst += val*cff.nvis;
+					CAscrollline =>
+						newfirst += val;
+					CAscrolldelta =>
+#						# insufficient for very long select lists
+						newfirst += val;
+#						if(val > 0)
+#							newfirst++;
+#						else
+#							newfirst--;
+					}
+					newfirst = max(0, min(newfirst, len cff.options - cff.nvis));
+					cff.first = newfirst;
+					nopt := len cff.options;
+					c.scrollset(newfirst, newfirst+cff.nvis, nopt, nopt, 0);
+					cff.draw(1);
+					return (ans, newgrab);
+				Clistbox =>
+					if(c.flags&CFscrvert) {
+						newfirst := cff.first;
+						case ans {
+						CAscrollpage =>
+							newfirst += val*cff.nvis;
+						CAscrollline =>
+							newfirst += val;
+						CAscrolldelta =>
+							newfirst += val;
+#							if(val > 0)
+#								newfirst++;
+#							else
+#								newfirst--;
+						}
+						newfirst = max(0, min(newfirst, len cff.options - cff.nvis));
+						cff.first = newfirst;
+						c.scrollset(newfirst, newfirst+cff.nvis, len cff.options, 0, 1);
+						# TODO: need redraw only vscr and content
+					}
+					else {
+						hw := cff.maxcol;
+						w := (c.r.max.x - c.r.min.x - SCRFBREADTH)/charspace;
+						newstart := cff.start;
+						case ans {
+						CAscrollpage =>
+								newstart += val*hw;
+						CAscrollline =>
+								newstart += val;
+						CAscrolldelta =>
+							if(val > 0)
+								newstart++;
+							else
+								newstart--;
+						}
+						if(hw < w)
+							newstart = 0;
+						else
+							newstart = max(0, min(newstart, hw - w));
+						cff.start = newstart;
+						c.scrollset(newstart, w+newstart, hw, 0, 1);
+						# TODO: need redraw only hscr and content
+					}
+					cff.draw(1);
+					return (ans, newgrab);
+				}
+			}
+			else {
+				if(c.flags&CFscrvert)
+					c.f.yscroll(ans, val);
+				else
+					c.f.xscroll(ans, val);
+			}
+			changed = 1;
+		}
+		else if(actflags != oldactflags) {
+			changed = 1;
+		}
+	}
+	if(changed)
+		ctl.draw(1);
+	return (ans, newgrab);
+}
+
+# returns a new popup control
+Control.dopopup(ctl: self ref Control): ref Control
+{
+	sel : ref Control.Cselect;
+	pick c := ctl {
+	Cselect =>
+		if (c.nvis > 1)
+			return nil;
+		sel = c;
+	* =>
+		return nil;
+	}
+
+	w := sel.r.dx();
+	nopt := len sel.options;
+	nvis := min(nopt, POPUPLINES);
+	first := sel.first;
+	if (first + nvis > nopt)
+		first = nopt - nvis;
+	h := ctllinespace*nvis + 2*SELMARGIN;
+	r := Rect(sel.r.min, sel.r.min.add(Point(w, h)));
+	popup := G->getpopup(r);
+	if (popup == nil)
+		return nil;
+	scr : ref Control;
+	if (nvis < nopt) {
+		scr = Control.newscroll(sel.f, 1, h, SCRFBREADTH);
+		scr.r.addpt(Point(w,0));
+	}
+	newsel := ref Control.Cselect(sel.f, sel.ff, r, sel.flags, popup, sel, scr, nvis, first, sel.options);
+	if(scr != nil) {
+		pick pscr := scr {
+		Cscrollbar =>
+			pscr.ctl = newsel;
+		}
+		scr.popup = popup;
+		scr.scrollset(first, first+nvis, nopt, nopt, 0);
+	}
+	newsel.draw(1);
+	return newsel;
+}
+
+# returns original control for which this was a popup
+Control.donepopup(ctl: self ref Control): ref Control
+{
+	owner: ref Control;
+	pick c := ctl {
+	Cselect =>
+		if (c.owner == nil)
+			return nil;
+		owner = c.owner;
+	* =>
+		return nil;
+	}
+	G->cancelpopup();
+	pick c := owner {
+	Cselect =>
+		for (first := 0; first < len c.options; first++)
+			if (c.options[first].selected)
+				break;
+		if (first == len c.options)
+			first = 0;
+		c.first = first;
+	}
+	owner.draw(1);
+	return owner;
+}
+
+
+Control.reset(ctl: self ref Control)
+{
+	pick c := ctl {
+	Cbutton =>
+		c.flags &= ~CFactive;
+	Centry =>
+		c.s = "";
+		c.sel = (0, 0);
+		c.left = 0;
+		if(c.ff != nil && c.ff.value != "")
+			c.s = c.ff.value;
+		if (c.scr != nil)
+			c.scr.scrollset(0, 1, 1, 0, 0);
+	Ccheckbox=>
+		c.flags &= ~CFactive;
+		if(c.ff != nil && (c.ff.flags&B->FFchecked) != byte 0)
+			c.flags |= CFactive;
+	Cselect =>
+		nopt := len c.options;
+		if(c.ff != nil) {
+			l := c.ff.options;
+			for(i := 0; i < nopt; i++) {
+				o := hd l;
+				c.options[i].selected = o.selected;
+				l = tl l;
+			}
+		}
+		c.first = 0;
+		if(c.scr != nil) {
+			c.scr.scrollset(0, c.nvis, nopt, nopt, 0);
+		}
+	Clistbox =>
+		c.first = 0;
+		nopt := len c.options;
+		if(c.vscr != nil) {
+			c.vscr.scrollset(0, c.nvis, nopt, nopt, 0);
+		}
+		hw := 0;
+		for(i := 0; i < len c.options; i++)
+			hw = max(hw, fonts[DefFnt].f.width(c.options[i].display)); 
+		if(c.hscr != nil) {
+			c.hscr.scrollset(0, c.r.max.x, hw, 0, 0); 
+		}
+	Canimimage =>
+		c.cur = 0;
+	}
+	ctl.draw(0);
+}
+
+Control.draw(ctl: self ref Control, flush: int)
+{
+	win := ctl.f.cim;
+	if (ctl.popup != nil)
+		win = ctl.popup.image;
+	if (win == nil)
+		return;
+	oclipr := win.clipr;
+	clipr := oclipr;
+	any: int;
+	if (ctl.popup == nil) {
+		(clipr, any) = ctl.r.clip(ctl.f.cr);
+		if(!any && ctl != ctl.f.vscr && ctl != ctl.f.hscr)
+			return;
+		win.clipr = clipr;
+	}
+	pick c := ctl {
+	Cbutton =>
+		if(c.ff != nil && c.ff.image != nil && c.pic == nil) {
+			# check to see if image arrived
+			# (dimensions will have been set by checkffsize, if needed;
+			# this code is only for when the HTML specified the dimensions)
+			pick imi := c.ff.image {
+			Iimage =>
+				if(imi.ci.mims != nil) {
+					c.pic = imi.ci.mims[0].im;
+					c.picmask = imi.ci.mims[0].mask;
+				}
+			}
+		}
+		if(c.dorelief || c.pic == nil)
+			win.draw(c.r, colorimage(Grey), nil, zp);
+		if(c.pic != nil) {
+			p, m: ref Image;
+			if(c.flags & CFenabled) {
+				p = c.pic;
+				m = c.picmask;
+			}
+			else {
+				p = c.dpic;
+				m = c.dpicmask;
+			}
+			w := p.r.dx();
+			h := p.r.dy();
+			x := c.r.min.x + (c.r.dx() - w) / 2;
+			y := c.r.min.y + (c.r.dy() - h) / 2;
+			if((c.flags & CFactive) && c.dorelief) {
+				x++;
+				y++;
+			}
+			win.draw(Rect((x,y),(x+w,y+h)), p, m, zp);
+		}
+		else if(c.label != "") {
+			p := c.r.min.add(Point(BUTMARGIN, BUTMARGIN));
+			if(c.flags & CFactive)
+				p = p.add(Point(1,1));
+			win.text(p, colorimage(Black), zp, fonts[CtlFnt].f, c.label);
+		}
+		if(c.dorelief) {
+			relief := ReliefRaised;
+			if(c.flags & CFactive)
+				relief = ReliefSunk;
+			drawrelief(win, c.r.inset(2), relief);
+		}
+	Centry =>
+		win.draw(c.r, colorimage(White), nil, zp);
+		insetr := c.r.inset(2);
+		drawrelief(win,insetr, ReliefSunk);
+		eclipr := c.r;
+		eclipr.min.x += ENTHMARGIN;
+		eclipr.max.x -= ENTHMARGIN;
+		eclipr.min.y += ENTVMARGIN;
+		eclipr.max.y -= ENTVMARGIN;
+#		if (c.scr != nil)
+#			eclipr.max.x -= SCRFBREADTH;
+		(eclipr, any) = clipr.clip(eclipr);
+		win.clipr = eclipr;
+		p := c.r.min.add(Point(ENTHMARGIN,ENTVMARGIN));
+		s := c.s;
+		fnt := fonts[CtlFnt].f;
+		if(c.left > 0)
+			s = s[c.left:];
+		if(c.flags&CFsecure) {
+			for(i := 0; i < len s; i++)
+				s[i] = '*';
+		}
+
+		(sels, sele) := normalsel(c.sel);
+		(sels, sele) = (sels-c.left, sele-c.left);
+
+		lines : array of string;
+		linestarts : array of int;
+		textw := c.r.dx()-2*ENTHMARGIN;
+		if (c.scr != nil) {
+			textw -= SCRFBREADTH;
+			c.scr.r = c.scr.r.subpt(c.scr.r.min);
+			c.scr.r = c.scr.r.addpt(Point(insetr.max.x-SCRFBREADTH,insetr.min.y));
+			c.scr.draw(0);
+		}
+		if (c.linewrap)
+			(lines, linestarts) = wrapstring(fnt, s, textw);
+		else
+			(lines, linestarts) = (array [] of {s}, array [] of {0});
+
+		q := p;
+		black := colorimage(Black);
+		white := colorimage(White);
+		navy := colorimage(Navy);
+		nlines := len lines;
+		for (n := 0; n < nlines; n++) {
+			segs : list of (int, int, int);
+			# only show cursor or selection if we have focus
+			if (c.flags & CFhasfocus)
+				segs = selsegs(len lines[n], sels-linestarts[n], sele-linestarts[n]);
+			else
+				segs = (0, len lines[n], 0) :: nil;
+			for (; segs != nil; segs = tl segs) {
+				(ss, se, sel) := hd segs;
+				txt := lines[n][ss:se];
+				w := fnt.width(txt);
+				txtcol : ref Image;
+				if (!sel)
+					txtcol = black;
+				else {
+					txtcol = white;
+					bgcol := navy;
+					if (n < nlines-1 && sele >= linestarts[n+1])
+						w = (p.x-q.x) + textw;
+					selr := Rect((q.x, q.y-1), (q.x+w, q.y+ctllinespace+1));
+					if (selr.dx() == 0) {
+						# empty selection - assume cursor
+						bgcol = black;
+						selr.max.x = selr.min.x + 2;
+					}
+					win.draw(selr, bgcol, nil, zp);
+				}
+				if (se > ss)
+					win.text(q, txtcol, zp, fnt, txt);
+				q.x += w;
+			}
+			q = (p.x, q.y + ctllinespace);
+		}
+	Ccheckbox=>
+		win.draw(c.r, colorimage(White), nil, zp);
+		if(c.isradio) {
+			a := CBOXHT/2;
+			a1 := a-1;
+			cen := Point(c.r.min.x+a,c.r.min.y+a);
+			win.ellipse(cen, a1, a1, 1, colorimage(DarkGrey), zp);
+			win.arc(cen, a, a, 0, colorimage(Black), zp, 45, 180);
+			win.arc(cen, a, a, 0, colorimage(Grey), zp, 225, 180);
+			if(c.flags&CFactive)
+				win.fillellipse(cen, 2, 2, colorimage(Black), zp);
+		}
+		else {
+			ir := c.r.inset(2);
+			ir.min.x += CBOXWID-CBOXHT;
+			ir.max.x -= CBOXWID-CBOXHT;
+			drawrelief(win, ir, ReliefSunk);
+			if(c.flags&CFactive) {
+				p1 := Point(ir.min.x, ir.min.y);
+				p2 := Point(ir.max.x, ir.max.y);
+				p3 := Point(ir.max.x, ir.min.y);
+				p4 := Point(ir.min.x, ir.max.y);
+				win.line(p1, p2, D->Endsquare, D->Endsquare, 0, colorimage(Black), zp);
+				win.line(p3, p4, D->Endsquare, D->Endsquare, 0, colorimage(Black), zp);
+			}
+		}
+	Cselect =>
+		black := colorimage(Black);
+		white := colorimage(White);
+		navy := colorimage(Navy);
+		win.draw(c.r, white, nil, zp);
+		drawrelief(win, c.r.inset(2), ReliefSunk);
+		ir := c.r.inset(SELMARGIN);
+		p := ir.min;
+		fnt := fonts[CtlFnt].f;
+		drawsel := c.nvis > 1;
+		for(i := c.first; i < len c.options && i < c.first+c.nvis; i++) {
+			if(drawsel && c.options[i].selected) {
+				maxx := ir.max.x;
+				if (c.scr != nil)
+					maxx -= SCRFBREADTH;
+				r := Rect((p.x-SELMARGIN,p.y),(maxx,p.y+ctllinespace));
+				win.draw(r, navy, nil, zp);
+				win.text(p, white, zp, fnt, c.options[i].display);
+			}
+			else {
+				win.text(p, black, zp, fnt, c.options[i].display);
+			}
+			p.y += ctllinespace;
+		}
+		if (c.nvis == 1 && len c.options > 1) {
+			# drop down select list - draw marker (must be same width as scroll bar)
+			r := Rect((ir.max.x - SCRFBREADTH, ir.min.y), ir.max);
+			drawtriangle(win, r, TRIdown, ReliefRaised);
+		} 
+		if(c.scr != nil) {
+			c.scr.r = Rect((ir.max.x - SCRFBREADTH, ir.min.y), ir.max);
+			c.scr.draw(0);
+		}
+	Clistbox =>
+		black := colorimage(Black);
+		white := colorimage(White);
+		navy := colorimage(Navy);
+		win.draw(c.r, white, nil, zp);
+		insetr := c.r.inset(2);
+		#drawrelief(win, c.r.inset(2), ReliefSunk);
+		ir := c.r.inset(SELMARGIN);
+		p := ir.min;
+		fnt := fonts[CtlFnt].f;
+		for(i := c.first; i < len c.options && i < c.first+c.nvis; i++) {
+			txt := "";
+			if (c.start < len c.options[i].display)
+				txt = c.options[i].display[c.start:];
+			if(c.options[i].selected) {
+				r := Rect((p.x-SELMARGIN,p.y),(c.r.max.x-SCRFBREADTH,p.y+fnt.height));
+				win.draw(r, navy, nil, zp);
+				win.text(p, white, zp, fnt, txt);
+			}
+			else {
+ 				win.text(p, black, zp, fnt, txt);
+			}
+			p.y +=fnt.height;
+		}
+		if(c.vscr != nil) {
+			c.vscr.r = c.vscr.r.subpt(c.vscr.r.min);
+			c.vscr.r = c.vscr.r.addpt(Point(insetr.max.x-SCRFBREADTH,insetr.min.y));
+			c.vscr.draw(0);
+ 		}
+ 		if(c.hscr != nil) {
+			c.hscr.r = c.hscr.r.subpt(c.hscr.r.min);
+			c.hscr.r = c.hscr.r.addpt(Point(insetr.min.x, insetr.max.y-SCRFBREADTH));
+ 			c.hscr.draw(0);
+		}
+		drawrelief(win, insetr, ReliefSunk);
+
+	Cscrollbar =>
+		# Scrollbar components: arrow 1 (a1), trough 1 (t1), slider (s), trough 2 (t2), arrow 2 (a2)
+		x := c.r.min.x;
+		y := c.r.min.y;
+		ra1, rt1, rs, rt2, ra2: Rect;
+		b, l, a1kind, a2kind: int;
+		if(c.flags&CFscrvert) {
+			l = c.r.max.y - c.r.min.y;
+			b = c.r.max.x - c.r.min.x;
+			xr := x+b;
+			yt1 := y+b;
+			ys := yt1+c.top;
+			yb := y+l;
+			ya2 := yb-b;
+			yt2 := ya2-c.bot;
+			ra1 = Rect(Point(x,y),Point(xr,yt1));
+			rt1 = Rect(Point(x,yt1),Point(xr,ys));
+			rs = Rect(Point(x,ys),Point(xr,yt2));
+			rt2 = Rect(Point(x,yt2),Point(xr,ya2));
+			ra2 = Rect(Point(x,ya2),Point(xr,yb));
+			a1kind = TRIup;
+			a2kind = TRIdown;
+		}
+		else {
+			l = c.r.max.x - c.r.min.x;
+			b = c.r.max.y - c.r.min.y;
+			yb := y+b;
+			xt1 := x+b;
+			xs := xt1+c.top;
+			xr := x+l;
+			xa2 := xr-b;
+			xt2 := xa2-c.bot;
+			ra1 = Rect(Point(x,y),Point(xt1,yb));
+			rt1 = Rect(Point(xt1,y),Point(xs,yb));
+			rs = Rect(Point(xs,y),Point(xt2,yb));
+			rt2 = Rect(Point(xt2,y),Point(xa2,yb));
+			ra2 = Rect(Point(xa2,y),Point(xr,yb));
+			a1kind = TRIleft;
+			a2kind = TRIright;
+		}
+		a1relief := ReliefRaised;
+		if(c.flags&CFscracta1)
+			a1relief = ReliefSunk;
+		a2relief := ReliefRaised;
+		if(c.flags&CFscracta2)
+			a2relief = ReliefSunk;
+		drawtriangle(win, ra1, a1kind, a1relief);
+		drawtriangle(win, ra2, a2kind, a2relief);
+		drawfill(win, rt1, Grey);
+		rs = rs.inset(2);
+		drawfill(win, rs, Grey);
+		rsrelief := ReliefRaised;
+		if(c.flags&CFactive)
+			rsrelief = ReliefSunk;
+		drawrelief(win, rs, rsrelief);
+		drawfill(win, rt2, Grey);
+	Canimimage =>
+		i := c.cur;
+		if(c.redraw)
+			i = 0;
+		else if(i > 0) {
+			iprev := i-1;
+			if(c.cim.mims[iprev].bgcolor != -1) {
+				i = iprev;
+				# get i back to before all "reset to previous"
+				# images (which will be skipped in following
+				# image drawing loop)
+				while(i > 0 && c.cim.mims[i].bgcolor == -2)
+					i--;
+			}
+		}
+		bgi := colorimage(c.bg.color);
+		if(c.bg.image != nil && c.bg.image.ci != nil && len c.bg.image.ci.mims > 0)
+			bgi = c.bg.image.ci.mims[0].im;
+		for( ; i <= c.cur; i++) {
+			mim := c.cim.mims[i];
+			if(i > 0 && i < c.cur && mim.bgcolor == -2)
+				continue;
+			p := c.r.min.add(mim.origin);
+			r := mim.im.r;
+			r = Rect(p, p.add(Point(r.dx(), r.dy())));
+
+			# IE takes "clear-to-background" disposal method to mean
+			# clear to background of HTML page, ignoring any background
+			# color specified in the GIF.
+			# IE clears to background before frame 0
+			if(i == 0)
+				win.draw(c.r, bgi, nil, zp);
+
+			if(i != c.cur && mim.bgcolor >= 0)
+				win.draw(r, bgi, nil, zp);
+			else
+				win.draw(r, mim.im, mim.mask, zp);
+		}
+	Clabel =>
+		p := c.r.min.add(Point(0,ENTVMARGIN));
+		win.text(p, colorimage(Black), zp, fonts[DefFnt].f, c.s);
+	}
+	if(flush) {
+		if (ctl.popup != nil)
+			ctl.popup.flush(ctl.r);
+		else
+			G->flush(ctl.r);
+	}
+	win.clipr = oclipr;
+}
+
+# Break s up into substrings that fit in width availw
+# when printing with font fnt.
+# The second returned array contains the indexes into the original
+# string where the corresponding line starts (which might not be simply
+# the sum of the preceding lines because of cr/lf's in the original string
+# which are omitted from the lines array.
+# Empty lines (ending in cr) get put into the array as empty strings.
+# The start indices array has an entry for the phantom next line, to avoid
+# the need for special cases in the rest of the code.
+wrapstring(fnt: ref Font, s: string, availw: int) : (array of string, array of int)
+{
+	sl : list of (string, int) = nil;
+	sw := fnt.width(s);
+	n := 0;
+	k := 0;	# index into original s where current s starts
+	origlen := len s;
+	done := 0;
+	while(!done) {
+		kincr := 0;
+		s1, s2: string;
+		if(s == "") {
+			s1 = s;
+			done = 1;
+		}
+		else {
+			# if any newlines in s1, it's a forced break
+			# (and newlines aren't to appear in result)
+			(s1, s2) = S->splitl(s, "\n");
+			if(s2 != nil && fnt.width(s1) <= availw) {
+				s = s2[1:];
+				sw = fnt.width(s);
+				kincr = (len s1) + 1;
+			}
+			else if(sw <= availw) {
+				s1 = s;
+				done = 1;
+			}
+			else {
+				(s1, nil, s, sw) = breakstring(s, sw, fnt, availw, 0);
+				kincr = len s1;
+				if(s == "")
+					done = 1;
+			}
+		}
+		sl = (s1, k) :: sl;
+		k += kincr;
+		n++;
+	}
+	# reverse sl back to original order
+	lines := array[n] of string;
+	linestarts := array[n+1] of int;
+	linestarts[n] = origlen;
+	while(sl != nil) {
+		(ss, nn) := hd sl;
+		lines[--n] = ss;
+		linestarts[n] = nn;
+		sl = tl sl;
+	}
+	return (lines, linestarts);
+}
+
+normalsel(sel : (int, int)) : (int, int)
+{
+	(s, e) := sel;
+	if (s > e)
+		(e, s) = sel;
+	return (s, e);
+}
+
+selsegs(n, s, e : int) : list of (int, int, int)
+{
+	if (e < 0 || s > n)
+		# selection is not in 0..n
+		return (0, n, 0) :: nil;
+
+	if (e > n) {
+		# second half of string is selected
+		if (s <= 0)
+			return (0, n, 1) :: nil;
+		return (0, s, 0) :: (s, n, 1) :: nil;
+	}
+
+	if (s < 0) {
+		# first half of string is selected
+		if (e >= n)
+			return (0, n, 1) :: nil;
+		return (0, e, 1) :: (e, n, 0) :: nil;
+	}
+	# middle section of string is selected
+	return (0, s, 0) :: (s, e, 1) :: (e, n, 0) :: nil;
+}
+
+# Figure out in which area of scrollbar, if any, p lies.
+# Then use p and mtype from mouse event to return desired action.
+Control.entryset(c: self ref Control, s: string)
+{
+	pick e := c {
+	Centry =>
+		e.s = s;
+		e.sel = (0, 0);
+		e.left = 0;
+		# calculate scroll bar settings
+		if (e.linewrap && e.scr != nil) {
+			(lines, nil, nil, nil) := entrywrapcalc(e);
+			nlines := len lines;
+			ny := (e.r.dy() - 2 * ENTVMARGIN)/ctllinespace;
+			e.scr.scrollset(0, ny, (nlines - 1), nlines, 0);
+		}
+	}
+}
+
+entryupdown(e: ref Control.Centry, cur : int, delta : int) : int
+{
+	e.sel = (cur, cur);
+	(lines, linestarts, topline, cursline) := entrywrapcalc(e);
+	newl := cursline + delta;
+	if (newl < 0 || newl >= len lines)
+		return cur;
+
+	fnt := fonts[CtlFnt].f;
+	x := cur - linestarts[cursline];
+	w := fnt.width(lines[cursline][0:x]);
+	l := lines[newl];
+	if (len l == 0)
+		return linestarts[newl];
+	prevw := fnt.width(l);
+	curw := prevw;
+	for (ix := len l - 1; ix > 0 ; ix--) {
+		prevw = curw;
+		curw = fnt.width(l[:ix]);
+		if (curw < w)
+			break;
+	}
+	# decide on closest (curw <= w <= prevw)
+	if (prevw-w <= w - curw)
+		# closer to rhs
+		ix++;
+	return linestarts[newl]+ix;
+}
+
+# delete given range of characters, and redraw
+entrydelrange(e: ref Control.Centry, istart, iend: int)
+{
+	n := iend - istart;
+	(sels, sele) := normalsel(e.sel);
+	if(n > 0) {
+		e.s = e.s[0:istart] + e.s[iend:];
+
+		if(sels > istart) {
+			if(sels < iend)
+				sels = istart;
+			else
+				sels -= n;
+		}
+		if (sele > istart) {
+			if (sele < iend)
+				sele = istart;
+			else
+				sele -= n;
+		}
+
+		if(e.left > istart)
+			e.left = max(istart-1, 0);
+		e.sel = (sels, sele);
+		entryscroll(e);
+	}
+}
+
+snarf : string;
+entrysetsnarf(e: ref Control.Centry)
+{
+	if (e.s == nil)
+		return;
+	s := e.s;
+	(sels, sele) := normalsel(e.sel);
+	if (sels != sele)
+		s = e.s[sels:sele];
+		
+	f := sys->open("/chan/snarf", sys->OWRITE);
+	if (f == nil)
+		snarf = s;
+	else {
+		data := array of byte s;
+		sys->write(f, data, len data);
+	}
+}
+
+entryinsertsnarf(e: ref Control.Centry)
+{
+	f := sys->open("/chan/snarf", sys->OREAD);
+	if(f != nil) {
+		buf := array[sys->ATOMICIO] of byte;
+		n := sys->read(f, buf, len buf);
+		if(n > 0) {
+			# trim a trailing newline, as a service...
+			if(buf[n-1] == byte '\n')
+				n--;
+		}
+		snarf = "";
+		if (n > 0)
+			snarf = string buf[:n];
+	}
+
+	if (snarf != nil) {
+		(sels, sele) := normalsel(e.sel);
+		if (sels != sele) {
+			entrydelrange(e, sels, sele);
+			(sels, sele) = e.sel;
+		}
+		lhs, rhs : string;
+		if (sels > 0)
+			lhs = e.s[:sels];
+		if (sels < len e.s)
+			rhs  = e.s[sels:];
+		e.entryset(lhs + snarf + rhs);
+		e.sel = (len lhs, len lhs + len snarf);
+	}
+}
+
+# make sure can see cursor and following char or two
+entryscroll(e: ref Control.Centry)
+{
+	s := e.s;
+	slen := len s;
+	if(e.flags&CFsecure) {
+		for(i := 0; i < slen; i++)
+			s[i] = '*';
+	}
+	if(e.linewrap) {
+		# For multiple line entries, c.left is the char
+		# at the beginning of the topmost visible line,
+		# and we just want to scroll to make sure that
+		# the line with the cursor is visible
+		(lines, linestarts, topline, cursline) := entrywrapcalc(e);
+		vislines := (e.r.dy()-2*ENTVMARGIN) / ctllinespace;
+		nlines := len linestarts;
+		if(cursline < topline)
+			topline = cursline;
+		else {
+			if(cursline >= topline+vislines)
+				topline = cursline-vislines+1;
+			if (topline + vislines >= nlines)
+				topline = max(0, (nlines-1) - vislines);
+		}
+		e.left = linestarts[topline];
+		if (e.scr != nil)
+			e.scr.scrollset(topline, topline+vislines, nlines-1, nlines, 1);
+	}
+	else {
+		(nil, sele) := e.sel;
+		# sele is always the drag point
+		if(sele < e.left)
+			e.left = sele;
+		else if(sele > e.left) {
+			fnt := fonts[CtlFnt].f;
+			wantw := e.r.dx() -2*ENTHMARGIN; # - 2*ctlspspace;
+			while(e.left < sele-1) {
+				w := fnt.width(e.s[e.left:sele]);
+				if(w < wantw)
+					break;
+				e.left++;
+			}
+		}
+	}
+}
+
+# Given e, a Centry with line wrapping,
+# return (wrapped lines, line start indices, line# of top displayed line, line# containing cursor).
+entrywrapcalc(e: ref Control.Centry) : (array of string, array of int, int, int)
+{
+	s := e.s;
+	if(e.flags&CFsecure) {
+		for(i := 0; i < len s; i++)
+			s[i] = '*';
+	}
+	(nil, sele) := e.sel;
+	textw := e.r.dx()-2*ENTHMARGIN;
+	if (e.scr != nil)
+		textw -= SCRFBREADTH;
+	(lines, linestarts) := wrapstring(fonts[CtlFnt].f, s, textw);
+	topline := 0;
+	cursline := 0;
+	for(i := 0; i < len lines; i++) {
+		s = lines[i];
+		i1 := linestarts[i];
+		i2 := linestarts[i+1];
+		if(e.left >= i1 && e.left < i2)
+			topline = i;
+		if(sele >= i1 && sele < i2)
+			cursline = i;
+	}
+	if(sele == linestarts[len lines])
+		cursline = len lines - 1;
+	return (lines, linestarts, topline, cursline);
+}
+
+Lay.new(targwidth: int, just: byte, margin: int, bg: Background) : ref Lay
+{
+	ans := ref Lay(Line.new(), Line.new(),
+			targwidth, 0, 0, margin, nil, bg, just, byte 0);
+	if(ans.targetwidth < 0)
+		ans.targetwidth = 0;
+	ans.start.pos = Point(margin, margin);
+	ans.start.next = ans.end;
+	ans.end.prev = ans.start;
+	# dummy item at end so ans.end will have correct y coord
+	it := Item.newspacer(ISPnull, 0);
+	it.state = IFbrk|IFcleft|IFcright;
+	ans.end.items = it;
+	return ans;
+}
+
+Line.new() : ref Line
+{
+	return ref Line(
+			nil, nil, nil,	# items, next, prev
+			zp,		# pos
+			0, 0, 0,	# width, height, ascent
+			byte 0);	# flags
+}
+
+Loc.new() : ref Loc
+{
+	return ref Loc(array[10] of Locelem, 0, zp);	# le, n, pos
+}
+
+Loc.add(loc: self ref Loc, kind: int, pos: Point)
+{
+	if(loc.n == len loc.le) {
+		newa := array[len loc.le + 10] of Locelem;
+		newa[0:] = loc.le;
+		loc.le = newa;
+	}
+	loc.le[loc.n].kind = kind;
+	loc.le[loc.n].pos = pos;
+	loc.n++;
+}
+
+# return last frame in loc's path
+Loc.lastframe(loc: self ref Loc) : ref Frame
+{
+	if (loc == nil)
+		return nil;
+	for(i := loc.n-1; i >=0; i--)
+		if(loc.le[i].kind == LEframe)
+			return loc.le[i].frame;
+	return nil;
+}
+
+Loc.print(loc: self ref Loc, msg: string)
+{
+	sys->print("%s: Loc with %d components, pos=(%d,%d)\n", msg, loc.n, loc.pos.x, loc.pos.y);
+	for(i := 0; i < loc.n; i++) {
+		case loc.le[i].kind {
+		LEframe =>
+			sys->print("frame %x\n",  loc.le[i].frame);
+		LEline =>
+			sys->print("line %x\n", loc.le[i].line);
+		LEitem =>
+			sys->print("item: %x", loc.le[i].item);
+			loc.le[i].item.print();
+		LEtablecell =>
+			sys->print("tablecell: %x, cellid=%d\n", loc.le[i].tcell, loc.le[i].tcell.cellid);
+		LEcontrol =>
+			sys->print("control %x\n", loc.le[i].control);
+		}
+	}
+}
+
+Sources.new(m : ref Source) : ref Sources
+{
+	srcs := ref Sources;
+	srcs.main = m;
+	return srcs;
+}
+
+Sources.add(srcs: self ref Sources, s: ref Source, required: int)
+{
+	if (required) {
+		CU->assert(srcs.reqd == nil);
+		srcs.reqd = s;
+	} else
+		srcs.srcs = s :: srcs.srcs;
+}
+
+Sources.done(srcs: self ref Sources, s: ref Source)
+{
+	if (s == srcs.main) {
+		if (srcs.reqd != nil) {
+			sys->print("FREEING MAIN WHEN REQD != nil\n");
+			if (s.bs == nil)
+				sys->print("s.bs == nil\n");
+			else
+				sys->print("main.eof = %d main.lim = %d, main.edata = %d\n", s.bs.eof, s.bs.lim, s.bs.edata);
+		}
+		srcs.main = nil;
+	}
+	else if (s == srcs.reqd)
+		srcs.reqd = nil;
+	else {
+		new : list of ref Source;
+		for (old := srcs.srcs; old != nil; old = tl old) {
+			src := hd old;
+			if (src == s)
+				continue;
+			new = src :: new;
+		}
+		srcs.srcs = new;
+	}
+}
+
+Sources.waitsrc(srcs: self ref Sources) : ref Source
+{
+	if (srcs == nil)
+		return nil;
+
+	bsl : list of ref ByteSource;
+
+	if (srcs.reqd == nil && srcs.main != nil) {
+		pick s := srcs.main {
+		Shtml =>
+			if (s.itsrc.toks != nil || s.itsrc.reqddata != nil)
+				return s;
+		}
+	}
+
+	# always check for subordinates
+	for (sl := srcs.srcs; sl != nil; sl = tl sl)
+		bsl = (hd sl).bs :: bsl;
+	# reqd is taken in preference to main source as main
+	# cannot be processed until we have the whole of reqd
+	if (srcs.reqd != nil)
+		bsl = srcs.reqd.bs :: bsl;
+	else if (srcs.main != nil)
+		bsl = srcs.main.bs :: bsl;
+	if (bsl == nil)
+		return nil;
+	bs : ref ByteSource;
+	for (;;) {
+		bs = CU->waitreq(bsl);
+		if (srcs.reqd == nil || srcs.reqd.bs != bs)
+			break;
+		# only interested in reqd if we have got it all
+		if (bs.err != "" || bs.eof)
+			return srcs.reqd;
+	}
+	if (srcs.main != nil && srcs.main.bs == bs)
+		return srcs.main;
+	found : ref Source;
+	for(sl = srcs.srcs; sl != nil; sl = tl sl) {
+		s := hd sl;
+		if(s.bs == bs) {
+			found = s;
+			break;
+		}
+	}
+	CU->assert(found != nil);
+	return found;
+}
+
+# spawned to animate images in frame f
+animproc(f: ref Frame)
+{
+	f.animpid = sys->pctl(0, nil);
+	aits : list of ref Item = nil;
+	# let del be millisecs to sleep before next frame change
+	del := 10000000;
+	d : int;
+	for(il := f.doc.images; il != nil; il = tl il) {
+		it := hd il;
+		pick i := it {
+		Iimage =>
+			ms := i.ci.mims;
+			if(len ms > 1) {
+				loc := f.find(zp, it);
+				if(loc == nil) {
+					# could be background, I suppose -- don't animate it
+					if(dbg)
+						sys->print("couldn't find item for animated image\n");
+					continue;
+				}
+				p := loc.le[loc.n-1].pos;
+				p.x += int i.hspace + int i.border;
+				# BUG: should get background from least enclosing layout
+				ctl := Control.newanimimage(f, i.ci, f.layout.background);
+				ctl.r = ctl.r.addpt(p);
+				i.ctlid = f.addcontrol(ctl);
+				d = ms[0].delay;
+				if(dbg)
+					sys->print("added anim ctl %d for image %s, initial delay %d\n",
+						i.ctlid, i.ci.src.tostring(), d);
+				aits = it :: aits;
+				if(d < del)
+					del = d;
+			}
+		}
+	}
+	if(aits == nil)
+		return;
+	tot := big 0;
+	for(;;) {
+		sys->sleep(del);
+		tot = tot + big del;
+		newdel := 10000000;
+		for(al := aits; al != nil; al = tl al) {
+			it := hd al;
+			pick i := hd al {
+			Iimage =>
+				ms := i.ci.mims;
+				pick c := f.controls[i.ctlid] {
+				Canimimage =>
+					m := ms[c.cur];
+					d = m.delay;
+					if(d > 0)
+						d -= int (tot - c.ts);
+					if(d == 0) {
+						# advance to next frame and show it
+						c.cur++;
+						if(c.cur == len ms)
+							c.cur = 0;
+						d = ms[c.cur].delay;
+						c.ts = tot;
+						c.draw(1);
+					}
+					if(d < newdel)
+						newdel = d;
+				}
+			}
+		}
+		del = newdel;
+	}
+}
--- /dev/null
+++ b/appl/charon/layout.m
@@ -1,0 +1,235 @@
+Layout: module
+{
+PATH: con "/dis/charon/layout.dis";
+
+ReliefBd: con 2;
+ReliefSunk, ReliefRaised : con iota;
+
+# Frames
+
+Frame: adt
+{
+	id: int;					# unique id
+	doc: ref Build->Docinfo;		# various global attributes from HTML and headers
+	src: ref Url->Parsedurl;
+	name: string;				# current name (assigned by parent frame, or by default)
+	marginw: int;				# margin on sides
+	marginh: int;				# margin on top and bottom
+	framebd: int;				# frame border desired
+	flags: int;					# Build->FRnoresize, etc.
+	layout: ref Lay;				# representation of layout
+	sublays: array of ref Lay;		# table cells, captions
+	sublayid: int;				# next sublayid to use
+	controls: cyclic array of ref Control;	# controls
+	controlid: int;				# next control id to use
+	cim: ref Draw->Image;		# image where we draw contents
+	r: Draw->Rect;				# part of cimage.r for this frame (including scrollbars)
+	cr: Draw->Rect;			# part of r for contents (excluding scrollbars, including margins)
+	totalr: Draw->Rect;			# total rectangle for page -- (0,0) is top left
+	viewr: Draw->Rect;			# view: subrect of totalr currently on screen
+	vscr: cyclic ref Control;		# vertical scrollbar
+	hscr: cyclic ref Control;		# horizontal scrollbar
+	parent: cyclic ref Frame;		# if this frame is in a frameset
+	kids: cyclic list of ref Frame;	# if this frame is a frameset
+	animpid: int;				# image animating thread
+	prctxt: ref Printcontext;		# nil if not printing
+
+# TEMP
+dirtyr: Draw->Rect;
+dirty: fn (f: self ref Frame, r: Draw->Rect);
+isdirty: int;
+
+	# reset() clears everything but parent, cim and r
+	# new() and newkid() call reset
+	# newkid() fills in name, etc., from ki, and copies cim from parent
+	new: fn() : ref Frame;
+	newkid: fn(parent: ref Frame, ki: ref Build->Kidinfo, r: Draw->Rect) : ref Frame;
+	reset: fn(f: self ref Frame);
+	addcontrol: fn(f: self ref Frame, c: ref Control) : int;
+	lptosp: fn(f: self ref Frame, lp: Draw->Point) : Draw->Point;
+	sptolp: fn(f: self ref Frame, sp: Draw->Point) : Draw->Point;
+	xscroll: fn(f: self ref Frame, kind, val: int); # kind is CAscrollpage, etc
+	yscroll: fn(f: self ref Frame, kind, val: int);
+	scrollabs: fn(f : self ref Frame, p : Draw->Point);
+	scrollrel: fn(f : self ref Frame, p : Draw->Point);
+	find: fn(f: self ref Frame, p: Draw->Point, it: ref Build->Item) : ref Loc;
+	swapimage: fn(f: self ref Frame, it: ref Build->Item.Iimage, src: string);
+	focus: fn(f : self ref Frame, focus, raisex : int);
+};
+
+Printcontext: adt {
+	mask: ref Draw->Image;
+	endy: int;
+};
+
+# Line flags
+Ldrawn, Lmoved, Lchanged: con byte (1<<iota);
+
+# Layout engine organizes Items into Lines
+Line: adt
+{
+	items: ref Build->Item;
+	next: cyclic ref Line;
+	prev: cyclic ref Line;
+	pos: Draw->Point;
+	width: int;
+	height: int;
+	ascent: int;
+	flags: byte;
+
+	new: fn() : ref Line;
+};
+
+# A place where an item, or a where mouse or keyboard focus could be.
+Loc: adt
+{
+	le:		array of Locelem;
+	n:		int;					# locs[0:n] form access path
+	pos:		Draw->Point;				# offset in final item
+
+	new:		fn() : ref Loc;
+	add:		fn(loc: self ref Loc, kind: int, pos: Draw->Point);
+	lastframe:	fn(loc: self ref Loc) : ref Frame;
+	print:	fn(loc: self ref Loc, msg: string);
+};
+
+# Don't use pick so that can make array of Locelems (rather than ref Locelems),
+# which saves a lot of alloc/frees in search functions.
+# (Also, saves memory overall, in Limbo).
+Locelem: adt
+{
+	kind:	int;				# LEframe, etc.
+	pos: Draw->Point;			# position in screen coords of this element
+	frame: ref Frame;		# root, or kid of previous (a frame)
+	line: ref Line;			# a line in lay of previous
+	item: ref Build->Item;		# an item in previous (a line or item)
+	tcell: ref Build->Tablecell;	# a cell in previous (a table item)
+	control: ref Control;		# a control in previous item, or scrollbar in previous frame
+};
+
+# Locelem kinds
+LEframe, LEline, LEitem, LEtablecell, LEcontrol : con iota;
+
+# One of the possible controls, and possible associated form field
+Control: adt {
+	f: cyclic ref Frame;
+	ff: ref Build->Formfield;
+	r: Draw->Rect;			# coords in f.cim coord system
+	flags:	int;
+	popup:	ref Gui->Popup;
+	pick {
+		Cbutton =>
+			pic:		ref Draw->Image;		# picture on button (if no label)
+			picmask:	ref Draw->Image;		# mask for pic
+			dpic:	ref Draw->Image;		# disabled ("greyed out") pic
+			dpicmask:	ref Draw->Image;	# mask for dpic
+			label:	string;		# label on button (if no pic), or else flyover hint
+			dorelief:	int;			# draw background & relief?
+		Centry =>
+			scr:		ref Control;
+			s:		string;		# current contents
+			sel:		(int,int);	# range of characters in s that are selected
+			left:		int;			# index of character in s that is at left of window
+			linewrap:	int;			# true if supposed to line-wrap
+			onchange:	int;		# true if want onchange event
+		Ccheckbox=>
+			isradio: 	int;			# true if for radio button
+		Cselect =>
+			#
+			owner:	ref Control;	# if this is a popup
+			scr:		ref Control;	# if needed
+			nvis:		int;			# number of visible options
+			first:		int;			# index of current top visible option
+			options:	array of Build->Option;
+#			onchange:	int;		# true if want onchange event
+		Clistbox =>
+			hscr:		ref Control;
+			vscr:		ref Control;
+			nvis:		int;
+			first:		int;			# index of current top visible option
+			start:		int;			# index of current start column
+			maxcol:		int;			# max column
+			options:	array of Build->Option;
+			grab:		cyclic ref Control;
+		Cscrollbar =>
+			top:		int;			# pixels in trough above/left of slider
+			bot:		int;			# pixels in trough below/right of slider
+			mindelta:	int;			# need delta of at least this (pixels)
+			deltaval: int;
+			ctl:		cyclic ref Control;	# if non-nil, scrolls this control
+			holdstate: (int, int);
+		Canimimage =>
+			cim:		ref CharonUtils->CImage;
+			cur:		int;				# current frame
+			redraw:	int;				# need to redraw all?
+			ts:		big;				# timestamp of current frame
+			bg:		Build->Background;	# if need restore-to-background
+		Clabel =>
+			s:		string;
+	}
+
+	newff: fn(f: ref Frame, ff: ref Build->Formfield) : ref Control;
+	newscroll: fn(f: ref Frame, isvert, length, breadth: int) : ref Control;
+	newentry: fn(f: ref Frame, nh, nv, linewrap: int) : ref Control;
+	newbutton: fn(f: ref Frame, pic, picmask: ref Draw->Image, lab: string, it: ref Build->Item.Iimage, candisable, dorelief: int) : ref Control;
+	newcheckbox: fn(f: ref Frame, isradio: int) : ref Control;
+	newselect: fn(f: ref Frame, nvis: int, options: array of Build->Option) : ref Control;
+	newlistbox: fn(f: ref Frame, nvis, w: int, options: array of Build->Option) : ref Control;
+	newanimimage: fn(f: ref Frame, cim: ref CharonUtils->CImage, bg: Build->Background) : ref Control;
+	newlabel: fn(f: ref Frame, s: string) : ref Control;
+	disable: fn(b: self ref Control);
+	enable: fn(b: self ref Control);
+	losefocus: fn(b: self ref Control, raisex: int);
+	gainfocus: fn(b: self ref Control, raisex: int);
+	scrollset: fn(sc: self ref Control, v1, v2, vmax, nsteps, draw: int);
+	entryset: fn(e: self ref Control, s: string);
+	# returns CAnone, etc.
+	dokey: fn(c: self ref Control, keychar: int) : int;
+	# domouse returns (action, grab) action = CAnone etc, grab = control that has grabbed mouse
+	domouse: fn(c: self ref Control, p: Draw->Point, mtype: int, oldgrab : ref Control) : (int, ref Control);
+	dopopup: fn(c: self ref Control): ref Control;
+	donepopup: fn(c: self ref Control): ref Control;
+	reset: fn(c: self ref Control);
+	draw: fn(c: self ref Control, flush: int);
+};
+
+# Control flags
+CFactive, CFenabled, CFsecure, CFhasfocus, CFscrvert, CFscracta1, CFscracta2, CFscracttr1, CFscracttr2: con (1<<iota);
+CFscrallact : con (CFactive|CFscracta1|CFscracta2|CFscracttr1|CFscracttr2);
+
+# Control Actions
+CAnone, CAscrollpage, CAscrollline, CAscrolldelta, CAscrollabs,
+CAbuttonpush, CAflyover, CAreturnkey, CAtabkey, CAkeyfocus, CAselected, CAchanged, CAdopopup, CAdonepopup: con iota;
+
+# Result of layout
+Lay: adt
+{
+	start: ref Line;			# fake before-the-first-line
+	end: ref Line;			# fake after-the-last-line
+	targetwidth: int;		# target width
+	width: int;				# actual width
+	height: int;			# actual height
+	margin: int;			# extra space on all four sides
+	floats:list of ref Build->Item.Ifloat;	# floats, from bottom up
+	background: Build->Background;	# background for layout
+	just: byte;				# default line justification
+	flags: byte;			# Lchanged
+
+	new: fn(targetwidth: int, just: byte, margin: int, bg: Build->Background) : ref Lay;
+};
+
+#B: Build;
+
+init: fn(cu: CharonUtils);
+layout: fn(f: ref Frame, bs: ref CharonUtils->ByteSource, linkclick: int) : array of byte;
+
+drawrelief: fn(im: ref Draw->Image, r: Draw->Rect, style: int);
+drawborder: fn(im: ref Draw->Image, r: Draw->Rect, n, color: int);
+drawfill: fn(im: ref Draw->Image, r: Draw->Rect, color: int);
+drawstring: fn(im: ref Draw->Image, p: Draw->Point, s: string);
+measurestring: fn(s: string) : Draw->Point;
+drawall: fn(f: ref Frame);
+relayout: fn(f: ref Frame, l: ref Lay, targetw: int, just: byte);
+
+stringwidth: fn(s: string): int;
+};
--- /dev/null
+++ b/appl/charon/lex.b
@@ -1,0 +1,1340 @@
+implement Lex;
+
+include "common.m";
+
+# local copies from CU
+sys: Sys;
+CU: CharonUtils;
+S: String;
+T: StringIntTab;
+C: Ctype;
+J: Script;
+ctype: array of byte;
+
+EOF : con -2;
+EOB : con -1;
+
+tagnames = array[] of {
+	" ",
+	"!",
+	"a", 
+	"abbr",
+	"acronym",
+	"address",
+	"applet", 
+	"area",
+	"b",
+	"base",
+	"basefont",
+	"bdo",
+	"big",
+	"blink",
+	"blockquote",
+	"body",
+	"bq",
+	"br",
+	"button",
+	"caption",
+	"center",
+	"cite",
+	"code",
+	"col",
+	"colgroup",
+	"dd",
+	"del",
+	"dfn",
+	"dir",
+	"div",
+	"dl",
+	"dt",
+	"em",
+	"fieldset",
+	"font",
+	"form",
+	"frame",
+	"frameset",
+	"h1",
+	"h2",
+	"h3",
+	"h4",
+	"h5",
+	"h6",
+	"head",
+	"hr",
+	"html",
+	"i",
+	"iframe",
+	"image",
+	"img",
+	"input",
+	"ins",
+	"isindex",
+	"kbd",
+	"label",
+	"legend",
+	"li",
+	"link",
+	"map",
+	"menu",
+	"meta",
+	"nobr",
+	"noframes",
+	"noscript",
+	"object",
+	"ol",
+	"optgroup",
+	"option",
+	"p",
+	"param",
+	"pre",
+	"q",
+	"s",
+	"samp",
+	"script",
+	"select",
+	"small",
+	"span",
+	"strike",
+	"strong",
+	"style",
+	"sub",
+	"sup",
+	"table",
+	"tbody",
+	"td",
+	"textarea",
+	"tfoot",
+	"th",
+	"thead",
+	"title",
+	"tr",
+	"tt",
+	"u",
+	"ul",
+	"var",
+	"xmp"
+};
+
+tagtable : array of T->StringInt;	# initialized from tagnames
+
+attrnames = array[] of {
+	"abbr",
+	"accept",
+	"accept-charset",
+	"accesskey",
+	"action",
+	"align",
+	"alink",
+	"alt",
+	"archive",
+	"axis",
+	"background",
+	"bgcolor",
+	"border",
+	"cellpadding",
+	"cellspacing",
+	"char",
+	"charoff",
+	"charset",
+	"checked",
+	"cite",
+	"class",
+	"classid",
+	"clear",
+	"code",
+	"codebase",
+	"codetype",
+	"color",
+	"cols",
+	"colspan",
+	"compact",
+	"content",
+	"coords",
+	"data",
+	"datafld",
+	"dataformatas",
+	"datapagesize",
+	"datasrc",
+	"datetime",
+	"declare",
+	"defer",
+	"dir",
+	"disabled",
+	"enctype",
+	"event",
+	"face",
+	"for",
+	"frame",
+	"frameborder",
+	"headers",
+	"height",
+	"href",
+	"hreflang",
+	"hspace",
+	"http-equiv",
+	"id",
+	"ismap",
+	"label",
+	"lang",
+	"language",
+	"link",
+	"longdesc",
+	"lowsrc",
+	"marginheight",
+	"marginwidth",
+	"maxlength",
+	"media",
+	"method",
+	"multiple",
+	"name",
+	"nohref",
+	"noresize",
+	"noshade",
+	"nowrap",
+	"object",
+	"onabort",
+	"onblur",
+	"onchange",
+	"onclick",
+	"ondblclick",
+	"onerror",
+	"onfocus",
+	"onkeydown",
+	"onkeypress",
+	"onkeyup",
+	"onload",
+	"onmousedown",
+	"onmousemove",
+	"onmouseout",
+	"onmouseover",
+	"onmouseup",
+	"onreset",
+	"onresize",
+	"onselect",
+	"onsubmit",
+	"onunload",
+	"profile",
+	"prompt",
+	"readonly",
+	"rel",
+	"rev",
+	"rows",
+	"rowspan",
+	"rules",
+	"scheme",
+	"scope",
+	"scrolling",
+	"selected",
+	"shape",
+	"size",
+	"span",
+	"src",
+	"standby",
+	"start",
+	"style",
+	"summary",
+	"tabindex",
+	"target",
+	"text",
+	"title",
+	"type",
+	"usemap",
+	"valign",
+	"value",
+	"valuetype",
+	"version",
+	"vlink",
+	"vspace",
+	"width"
+};
+
+attrtable : array of T->StringInt;	# initialized from attrnames
+
+chartab:= array[] of { T->StringInt
+	("AElig",	'Æ'),
+	("Aacute",	'Á'),
+	("Acirc",	'Â'),
+	("Agrave",	'À'),
+	("Alpha",	'Α'),
+	("Aring",	'Å'),
+	("Atilde",	'Ã'),
+	("Auml",	'Ä'),
+	("Beta",	'Β'),
+	("Ccedil",	'Ç'),
+	("Chi",	'Χ'),
+	("Dagger",	'‡'),
+	("Delta",	'Δ'),
+	("ETH",	'Ð'),
+	("Eacute",	'É'),
+	("Ecirc",	'Ê'),
+	("Egrave",	'È'),
+	("Epsilon",	'Ε'),
+	("Eta",	'Η'),
+	("Euml",	'Ë'),
+	("Gamma",	'Γ'),
+	("Iacute",	'Í'),
+	("Icirc",	'Î'),
+	("Igrave",	'Ì'),
+	("Iota",	'Ι'),
+	("Iuml",	'Ï'),
+	("Kappa",	'Κ'),
+	("Lambda",	'Λ'),
+	("Mu",	'Μ'),
+	("Ntilde",	'Ñ'),
+	("Nu",	'Ν'),
+	("OElig",	'Œ'),
+	("Oacute",	'Ó'),
+	("Ocirc",	'Ô'),
+	("Ograve",	'Ò'),
+	("Omega",	'Ω'),
+	("Omicron",	'Ο'),
+	("Oslash",	'Ø'),
+	("Otilde",	'Õ'),
+	("Ouml",	'Ö'),
+	("Phi",	'Φ'),
+	("Pi",	'Π'),
+	("Prime",	'″'),
+	("Psi",	'Ψ'),
+	("Rho",	'Ρ'),
+	("Scaron",	'Š'),
+	("Sigma",	'Σ'),
+	("THORN",	'Þ'),
+	("Tau",	'Τ'),
+	("Theta",	'Θ'),
+	("Uacute",	'Ú'),
+	("Ucirc",	'Û'),
+	("Ugrave",	'Ù'),
+	("Upsilon",	'Υ'),
+	("Uuml",	'Ü'),
+	("Xi",	'Ξ'),
+	("Yacute",	'Ý'),
+	("Yuml",	'Ÿ'),
+	("Zeta",	'Ζ'),
+	("aacute",	'á'),
+	("acirc",	'â'),
+	("acute",	'´'),
+	("aelig",	'æ'),
+	("agrave",	'à'),
+	("alefsym",	'ℵ'),
+	("alpha",	'α'),
+	("amp",	'&'),
+	("and",	'∧'),
+	("ang",	'∠'),
+	("aring",	'å'),
+	("asymp",	'≈'),
+	("atilde",	'ã'),
+	("auml",	'ä'),
+	("bdquo",	'„'),
+	("beta",	'β'),
+	("brvbar",	'¦'),
+	("bull",	'•'),
+	("cap",	'∩'),
+	("ccedil",	'ç'),
+	("cdots", '⋯'),
+	("cedil",	'¸'),
+	("cent",	'¢'),
+	("chi",	'χ'),
+	("circ",	'ˆ'),
+	("clubs",	'♣'),
+	("cong",	'≅'),
+	("copy",	'©'),
+	("crarr",	'↵'),
+	("cup",	'∪'),
+	("curren",	'¤'),
+	("dArr",	'⇓'),
+	("dagger",	'†'),
+	("darr",	'↓'),
+	("ddots", '⋱'),
+	("deg",	'°'),
+	("delta",	'δ'),
+	("diams",	'♦'),
+	("divide",	'÷'),
+	("eacute",	'é'),
+	("ecirc",	'ê'),
+	("egrave",	'è'),
+	("emdash", '—'),
+	("empty",	'∅'),
+	("emsp",	' '),
+	("endash", '–'),
+	("ensp",	' '),
+	("epsilon",	'ε'),
+	("equiv",	'≡'),
+	("eta",	'η'),
+	("eth",	'ð'),
+	("euml",	'ë'),
+	("euro",	'€'),
+	("exist",	'∃'),
+	("fnof",	'ƒ'),
+	("forall",	'∀'),
+	("frac12",	'½'),
+	("frac14",	'¼'),
+	("frac34",	'¾'),
+	("frasl",	'⁄'),
+	("gamma",	'γ'),
+	("ge",	'≥'),
+	("gt",	'>'),
+	("hArr",	'⇔'),
+	("harr",	'↔'),
+	("hearts",	'♥'),
+	("hellip",	'…'),
+	("iacute",	'í'),
+	("icirc",	'î'),
+	("iexcl",	'¡'),
+	("igrave",	'ì'),
+	("image",	'ℑ'),
+	("infin",	'∞'),
+	("int",	'∫'),
+	("iota",	'ι'),
+	("iquest",	'¿'),
+	("isin",	'∈'),
+	("iuml",	'ï'),
+	("kappa",	'κ'),
+	("lArr",	'⇐'),
+	("lambda",	'λ'),
+	("lang",	'〈'),
+	("laquo",	'«'),
+	("larr",	'←'),
+	("lceil",	'⌈'),
+	("ldots", '…'),
+	("ldquo",	'“'),
+	("le",	'≤'),
+	("lfloor",	'⌊'),
+	("lowast",	'∗'),
+	("loz",	'◊'),
+	("lrm",	'‎'),
+	("lsaquo",	'‹'),
+	("lsquo",	'‘'),
+	("lt",	'<'),
+	("macr",	'¯'),
+	("mdash",	'—'),
+	("micro",	'µ'),
+	("middot",	'·'),
+	("minus",	'−'),
+	("mu",	'μ'),
+	("nabla",	'∇'),
+	("nbsp",	' '),
+	("ndash",	'–'),
+	("ne",	'≠'),
+	("ni",	'∋'),
+	("not",	'¬'),
+	("notin",	'∉'),
+	("nsub",	'⊄'),
+	("ntilde",	'ñ'),
+	("nu",	'ν'),
+	("oacute",	'ó'),
+	("ocirc",	'ô'),
+	("oelig",	'œ'),
+	("ograve",	'ò'),
+	("oline",	'‾'),
+	("omega",	'ω'),
+	("omicron",	'ο'),
+	("oplus",	'⊕'),
+	("or",	'∨'),
+	("ordf",	'ª'),
+	("ordm",	'º'),
+	("oslash",	'ø'),
+	("otilde",	'õ'),
+	("otimes",	'⊗'),
+	("ouml",	'ö'),
+	("para",	'¶'),
+	("part",	'∂'),
+	("permil",	'‰'),
+	("perp",	'⊥'),
+	("phi",	'φ'),
+	("pi",	'π'),
+	("piv",	'ϖ'),
+	("plusmn",	'±'),
+	("pound",	'£'),
+	("prime",	'′'),
+	("prod",	'∏'),
+	("prop",	'∝'),
+	("psi",	'ψ'),
+	("quad", ' '),
+	("quot",	'"'),
+	("quot", '"'),
+	("rArr",	'⇒'),
+	("radic",	'√'),
+	("rang",	'〉'),
+	("raquo",	'»'),
+	("rarr",	'→'),
+	("rceil",	'⌉'),
+	("rdquo",	'”'),
+	("real",	'ℜ'),
+	("reg",	'®'),
+	("rfloor",	'⌋'),
+	("rho",	'ρ'),
+	("rlm",	'‏'),
+	("rsaquo",	'›'),
+	("rsquo",	'’'),
+	("sbquo",	'‚'),
+	("scaron",	'š'),
+	("sdot",	'⋅'),
+	("sect",	'§'),
+	("shy",	'­'),
+	("sigma",	'σ'),
+	("sigmaf",	'ς'),
+	("sim",	'∼'),
+	("sp", ' '),
+	("spades",	'♠'),
+	("sub",	'⊂'),
+	("sube",	'⊆'),
+	("sum",	'∑'),
+	("sup",	'⊃'),
+	("sup1",	'¹'),
+	("sup2",	'²'),
+	("sup3",	'³'),
+	("supe",	'⊇'),
+	("szlig",	'ß'),
+	("tau",	'τ'),
+	("there4",	'∴'),
+	("theta",	'θ'),
+	("thetasym",	'ϑ'),
+	("thinsp",	' '),
+	("thorn",	'þ'),
+	("tilde",	'˜'),
+	("times",	'×'),
+	("trade",	'™'),
+	("uArr",	'⇑'),
+	("uacute",	'ú'),
+	("uarr",	'↑'),
+	("ucirc",	'û'),
+	("ugrave",	'ù'),
+	("uml",	'¨'),
+	("upsih",	'ϒ'),
+	("upsilon",	'υ'),
+	("uuml",	'ü'),
+	("varepsilon", '∈'),
+	("varphi", 'ϕ'),
+	("varpi", 'ϖ'),
+	("varrho", 'ϱ'),
+	("vdots", '⋮'),
+	("vsigma", 'ς'),
+	("vtheta", 'ϑ'), 
+	("weierp",	'℘'),
+	("xi",	'ξ'),
+	("yacute",	'ý'),
+	("yen",	'¥'),
+	("yuml",	'ÿ'),
+	("zeta",	'ζ'),
+	("zwj",	'‍'),
+	("zwnj",	'‌'),
+};
+
+# Characters Winstart..Winend are those that Windows
+# uses interpolated into the Latin1 set.
+# They aren't supposed to appear in HTML, but they do....
+Winstart : con 16r7f;
+Winend: con 16r9f;
+winchars := array[] of { '•',
+	'•', '•', '‚', 'ƒ', '„', '…', '†', '‡',
+	'ˆ', '‰', 'Š', '‹', 'Œ', '•', '•', '•',
+	'•', '‘', '’', '“', '”', '•', '–', '—',
+	'˜', '™', 'š', '›', 'œ', '•', '•', 'Ÿ' 
+};
+
+NAMCHAR : con (C->L|C->U|C->D|C->N);
+LETTER : con (C->L|C->U);
+
+dbg := 0;
+warn := 0;
+
+init(cu: CharonUtils)
+{
+	CU = cu;
+	sys = load Sys Sys->PATH;
+	S = load String String->PATH;
+	C = cu->C;
+	J = cu->J;
+	T = load StringIntTab StringIntTab->PATH;
+	tagtable = CU->makestrinttab(tagnames);
+	attrtable = CU->makestrinttab(attrnames);
+	ctype = C->ctype;
+}
+
+TokenSource.new(b: ref CU->ByteSource, chset : Btos, mtype: int) : ref TokenSource
+{
+	ts := ref TSstate (
+		0,				# bi
+		0, 				# prevbi
+		"",				# s
+		0,				# si
+		Convcs->Startstate,	# state
+		Convcs->Startstate	# prevstate
+	);
+	ans := ref TokenSource(
+		b,			# b
+		chset,		# chset
+		ts,			# state
+		mtype,		# mtype
+		0			# inxmp
+	);
+	dbg = int (CU->config).dbg['x'];
+	warn = (int (CU->config).dbg['w']) || dbg;
+	return ans;
+}
+
+TokenSource.gettoks(ts: self ref TokenSource): array of ref Token
+{
+	ToksMax : con 500;		# max chunk of tokens returned
+	a := array[ToksMax] of ref Token;
+	ai := 0;
+	pcdai := 0;
+	lim := 0;
+	# put some dbg output in here
+	if(ts.mtype == CU->TextHtml) {
+		pcdstate : ref TSstate;
+gather:
+		while(ai < ToksMax-1) {	# always allow space for a Data token
+			state := getstate(ts);
+			c := getchar(ts);
+			if (c < ' ') {
+				c = eatctls(c, ts);
+				if (c < 0)
+					break;
+			}
+			tok : ref Token;
+			if(c == '<') {
+				tok = gettag(ts);
+				if (tok != nil && ts.inxmp && tok.tag != Txmp+RBRA) {
+					rewind(ts, state);
+					getchar(ts);	# consume the '<'
+					tok = ref Token(Data, "<", nil);
+				}
+				if(tok != nil && tok.tag != Comment) {
+					a[ai++] = tok;
+					case (tok.tag) {
+					Tselect or Ttitle or Toption=>
+						# Several tags expect PCDATA after them.
+						# Capture state so we can rewind if necessary
+						pcdstate = state;
+						pcdai = ai-1;
+					Ttextarea =>
+						pcdstate = state;
+						pcdai = ai-1;
+						# not sure if we should parse entity references
+						tok = gettagdata(ts, tok.tag, 1);
+						if(tok != nil) {
+							pcdstate = nil;
+							a[ai++] = tok;
+						}
+					Tscript =>
+						pcdstate = state;
+						pcdai = ai-1;
+						# special rules for getting Data
+						tok = getscriptdata(ts);
+						if(tok != nil) {
+							pcdstate = nil;
+							a[ai++] = tok;
+						}
+					Txmp =>
+						pcdstate = nil;
+						ts.inxmp = 1;
+					Txmp+RBRA =>
+						pcdstate = nil;
+						ts.inxmp = 0;
+					Data =>
+						;
+					Tmeta =>
+						pcdstate = nil;
+						break gather;
+					* =>
+						pcdstate = nil;
+					}
+				}
+			} else {
+				tok = getdata(ts, c);
+				if(tok != nil)
+					a[ai++] = tok;
+			}
+			if(tok == nil && !eof(ts)) {
+				# we need more input to complete the token
+				lim = ts.state.bi;
+				rewind(ts, state);
+				break gather;
+			} else
+				if(dbg > 1)
+					sys->print("lex: got token %s\n", tok.tostring());
+		}
+		# Several tags expect PCDATA after them.
+		# which means that build needs to see another tag or eof
+		# after any data in order to know that PCDATA is ended.
+		# Rewind if we haven't got to the following tag yet.
+		if (pcdstate != nil && !eof(ts)) {
+			rewind(ts, pcdstate);
+			ai = pcdai;
+		}
+	}
+	else {
+		# plain text (non-html) tokens
+		while(ai < ToksMax) {
+			tok := getplaindata(ts);
+			if(tok == nil)
+				break;
+			else
+				a[ai++] = tok;
+			if(dbg > 1)
+				sys->print("lex: got token %s\n", tok.tostring());
+		}
+	}
+	if(dbg)
+		sys->print("lex: returning %d tokens\n", ai);
+	if (lim > ts.b.lim)
+		ts.b.lim = lim;
+	else
+		ts.b.lim = ts.state.prevbi;
+	if(ai == 0)
+		return nil;
+	return a[0:ai];
+}
+
+# must not be called from within TokenSource.gettoks()
+# as it will not work with rewind() and ungetchar()
+#
+TokenSource.setchset(ts: self ref TokenSource, chset: Btos)
+{
+	st := ts.state;
+	nchars := st.si;
+	if (nchars > 0 && nchars < len st.s) {
+		# align bi to the current input char
+		bs := ts.b;
+		(nil, nil, n) := ts.chset->btos(st.prevcsstate, bs.data[st.prevbi:st.bi], nchars);
+		st.bi = st.prevbi + n;
+		st.prevbi = st.bi;
+	}
+	ts.chset = chset;
+	st.csstate = st.prevcsstate = Convcs->Startstate;
+	st.s = nil;
+	st.si = 0;
+}
+
+
+eof(ts : ref TokenSource) : int
+{
+	st := ts.state;
+	bs := ts.b;
+	return (st.s == nil && bs.eof && st.prevbi == bs.edata);
+}
+
+# For case where source isn't HTML.
+# Just make data tokens, one per line (or partial line,
+# at end of buffer), ignoring non-whitespace control
+# characters and dumping \r's
+getplaindata(ts: ref TokenSource): ref Token
+{
+	s := "";
+	j := 0;
+
+	for(c := getchar(ts); c >= 0; c = getchar(ts)) {
+		if(c < ' ') {
+			if(ctype[c] == C->W) {
+				if(c == '\r') {
+					# ignore it unless no following '\n',
+					# in which case treat it like '\n'
+					c = getchar(ts);
+					if(c != '\n') {
+						if(c >= 0)
+							ungetchar(ts);
+						c = '\n';
+					}
+				}
+			}
+			else
+				c = 0;	# ignore
+		}
+		if(c != 0)
+			s[j++] = c;
+		if(c == '\n')
+			break;
+	}
+	if(s == "")
+		return nil;
+	return ref Token(Data, s, nil);
+}
+
+eatctls(c: int, ts: ref TokenSource): int
+{
+	while (c >= 0) {
+		if (c >= ' ')
+			return c;
+		if(ctype[c] == C->W) {
+			if(c == '\r') {
+				c = getchar(ts);
+				if (c != '\n' && c >= 0) {
+					ungetchar(ts);
+					c = '\n';
+				}
+			}
+			return c;
+		}
+		c = getchar(ts);
+	}
+	return -1;
+}
+
+# Gather data up to next start-of-tag or end-of-buffer.
+# Translate entity references (&amp;) if not in <XMP> section.
+# Ignore non-whitespace control characters and get rid of \r's.
+getdata(ts: ref TokenSource, firstc : int): ref Token
+{
+	s := "";
+	j := 0;
+	c := firstc;
+
+	while(c >= 0) {
+		if (c < ' ')
+			c = eatctls(c, ts);
+		if (c < 0)
+			break;
+		if(c == '&' && !ts.inxmp) {
+			ok : int;
+			(c, ok) = ampersand(ts);
+			if(!ok) {
+				ungetchar(ts);
+				break;	# incomplete entity reference (ts backed up by ampersand)
+			}
+		}
+		else if(c == '<') {
+			ungetchar(ts);
+			break;
+		}
+		if(c != 0)
+			s[j++] = c;
+		c = getchar(ts);
+	}
+	if(s == "")
+		return nil;
+	return ref Token(Data, s, nil);
+}
+
+# The rules for lexing scripts are different (ugh).
+# Gather up everything until see a </SCRIPT>.
+getscriptdata(ts: ref TokenSource): ref Token
+{
+	tok := gettagdata(ts, Tscript, 0);
+	if (tok != nil)
+		tok.text = CU->stripscript(tok.text);
+	return tok;
+}
+
+gettagdata(ts: ref TokenSource, tag, doentities: int): ref Token
+{
+	s := "";
+	j := 0;
+	c := getchar(ts);
+
+	while(c >= 0) {
+		if (c == '<') {
+			tstate := getstate(ts);
+			tok := gettag(ts);
+			rewind(ts, tstate);
+			if (tok != nil && tok.tag == tag+RBRA) {
+				ungetchar(ts);
+				return ref Token(Data, s, nil);
+			}
+			# tag was not </tag>, take as regular data
+		}
+		if (doentities && c == '&')
+			(c, nil) = ampersand(ts);
+
+		if(c < 0)
+			break;
+		if(c != 0)
+			s[j++] = c;
+		c = getchar(ts);
+	}
+	if(eof(ts))
+		return ref Token(Data, s, nil);
+
+	return nil;
+}
+
+# We've just seen a '<'.  Gather up stuff to closing '>' (if buffer
+# ends before then, return nil).
+# If it's a tag, look up the name, gather the attributes, and return
+# the appropriate token.
+# Else it's either just plain data or some kind of ignorable stuff:
+# return a Data or Comment token as appropriate.
+gettag(ts: ref TokenSource): ref Token
+{
+	rbra := 0;
+	ans : ref Token = nil;
+	al: list of Attr;
+	start := getstate(ts);
+	c := getchar(ts);
+
+	# dummy loop: break out of this when hit end of buffer
+ eob:
+	for(;;) {
+		if(c == '/') {
+			rbra = RBRA;
+			c = getchar(ts);
+		}
+		if(c < 0)
+			break eob;
+		if(c>=C->NCTYPE || !int (ctype[c]&LETTER)) {
+			# not a tag
+			if(c == '!') {
+				ans = comment(ts);
+				if(ans != nil)
+					return ans;
+				break eob;
+			}
+			else {
+				rewind(ts, start);
+				return ref Token(Data, "<", nil);
+			}
+		}
+		# c starts a tagname
+		ans = ref Token(Notfound, nil, nil);
+		name := "";
+		name[0] = lowerc(c);
+		i := 1;
+		for(;;) {
+			c = getchar(ts);
+			if(c < 0)
+				break eob;
+			if(c>=C->NCTYPE || !int (ctype[c]&NAMCHAR))
+				break;
+			name[i++] = lowerc(c);
+		}
+		(fnd, tag) := T->lookup(tagtable, name);
+		if(fnd)
+			ans.tag = tag+rbra;
+		else
+			ans.text = name;	# for warning print, in build
+attrloop:
+		for(;;) {
+			# look for "ws name" or "ws name ws = ws val"  (ws=whitespace)
+			# skip whitespace
+			while(c < C->NCTYPE && ctype[c] == C->W) {
+				c = getchar(ts);
+				if(c < 0)
+					break eob;
+			}
+			if(c == '>')
+				break attrloop;
+			if(c == '<') {
+				if(warn)
+					sys->print("warning: unclosed tag; last name=%s\n", name);
+				ungetchar(ts);
+				break attrloop;
+			}
+			if(c >= C->NCTYPE || !int (ctype[c]&LETTER)) {
+				if(warn)
+					sys->print("warning: expected attribute name; last name=%s\n", name);
+				# skip to next attribute name
+				for(;;) {
+					c = getchar(ts);
+					if(c < 0)
+						break eob;
+					if(c < C->NCTYPE && int (ctype[c]&LETTER))
+						continue attrloop;
+					if(c == '<') {
+						if(warn)
+							sys->print("warning: unclosed tag; last name=%s\n", name);
+						ungetchar(ts);
+						break attrloop;
+					}
+					if(c == '>')
+						break attrloop;
+				}
+			}
+			# gather attribute name
+			name = "";
+			name[0] = lowerc(c);
+			i = 1;
+			for(;;) {
+				c = getchar(ts);
+				if(c < 0)
+					break eob;
+				if(c >= C->NCTYPE || !int (ctype[c]&NAMCHAR))
+					break;
+				name[i++] = lowerc(c);
+			}
+			(afnd, attid) := T->lookup(attrtable, name);
+			if(warn && !afnd)
+				sys->print("warning: unknown attribute name %s\n", name);
+			# skip whitespace
+			while(c < C->NCTYPE && ctype[c] == C->W) {
+				c = getchar(ts);
+				if(c < 0)
+					break eob;
+			}
+			if(c != '=') {
+				# no value for this attr
+				if(afnd)
+					al = (attid, "") :: al;
+				continue attrloop;
+			}
+			# c is '=' here;  skip whitespace
+			for(;;) {
+				c = getchar(ts);
+				if(c < 0)
+					break eob;
+				if(c >= C->NCTYPE || ctype[c] != C->W)
+					break;
+			}
+			# gather value
+			quote := 0;
+			if(c == '\'' || c == '"') {
+				quote = c;
+				c = getchar(ts);
+				if(c < 0)
+					break eob;
+			}
+			val := "";
+			nv := 0;
+		valloop:
+			for(;;) {
+				if(c < 0)
+					break eob;
+# other browsers allow value strings to be broken across lines
+# especially the case for Javascript event handlers / URLs
+				if (c == '>' && !quote)
+					break valloop;
+# old code otherwise ok - keep for now for reference
+#				if(c == '>') {
+#					if(quote) {
+#						# c might be part of string (though not good style)
+#						# but if line ends before close quote, assume
+#						# there was an unmatched quote
+#						ti := ts.i;
+#						for(;;) {
+#							c = getchar(ts);
+#							if(c < 0)
+#								break eob;
+#							if(c == quote) {
+#								backup(ts, ti);
+#								val[nv++] = '>';
+#								c = getchar(ts);
+#								continue valloop;
+#							}
+#							if(c == '\n') {
+#								if(warn)
+#									sys->print("warning: apparent unmatched quote\n");
+#								backup(ts, ti);
+#								quote = 0;
+#								c = '>';
+#								break valloop;
+#							}
+#						}
+#					}
+#					else
+#						break valloop;
+#				}
+				if(quote) {
+					if(c == quote) {
+						c = getchar(ts);
+						if(c < 0)
+							break eob;
+						break valloop;
+					}
+					if(c == '\r') {
+						c = getchar(ts);
+						continue valloop;
+					}
+					if(c == '\t' || c == '\n')
+						c = ' ';
+				}
+				else {
+					if(c < C->NCTYPE && ctype[c]==C->W)
+						break valloop;
+				}
+				if(c == '&') {
+					ok : int;
+					(c, ok) = ampersand(ts);
+					if(!ok)
+						break eob;
+				}
+				val[nv++] = c;
+				c = getchar(ts);
+			}
+			if(afnd)
+				al = (attid, val) :: al;
+		}
+		ans.attr = al;
+		return ans;
+	}
+	if(eof(ts)) {
+		if(warn)
+			sys->print("warning: incomplete tag at end of page\n");
+		rewind(ts, start);
+		return ref Token(Data, "<", nil);
+	}
+	return nil;
+}
+
+
+# We've just read a '<!',
+# so this may be a comment or other ignored section, or it may
+# be just a literal string if there is no close before end of file
+# (other browsers do that).
+# The accepted practice seems to be (note: contrary to SGML spec!):
+# If see <!--, look for --> to close, or if none, > to close.
+# If see <!(not --), look for > to close.
+# If no close before end of file, leave original characters in as literal data.
+#
+# If we see ignorable stuff, return Comment token.
+# Else return nil (caller should back up and try again when more data arrives,
+# unless at end of file, in which case caller should just make '<' a data token).
+comment(ts: ref TokenSource) : ref Token
+{
+	havecomment := 0;
+	commentstart := 0;
+	c := getchar(ts);
+	if(c == '-') {
+		state := getstate(ts);
+		c = getchar(ts);
+		if(c == '-') {
+			commentstart = 1;
+			if(findstr(ts, "-->"))
+				havecomment = 1;
+			else
+				rewind(ts, state);
+		}
+	}
+	if(!havecomment) {
+		if(c == '>')
+			havecomment = 1;
+		else if(c >= 0) {
+			if(findstr(ts, ">"))
+				havecomment = 1;
+		}
+	}
+	if(havecomment)
+		return ref Token(Comment, nil, nil);
+	return nil;
+}
+
+# Look for string s in token source.
+# If found, return 1, with buffer at next char after s,
+# else return 0 (caller should back up).
+findstr(ts: ref TokenSource, s: string) : int
+{
+	n := len s;
+	eix := n-1;
+	buf := "";
+	c : int;
+
+	if (n == 1) {
+		while ((c = getchar(ts)) >= 0)
+			if (c == s[0])
+				return 1;
+		return 0;
+	}
+
+	for (i := 0; i < n; i++) {
+		c = getchar(ts);
+		if (c < 0)
+			return 0;
+		buf[i] = c;
+	}
+
+	for (;;) {
+		# this could be much more efficient by tracking
+		# the start char through buf
+		if (buf == s)
+			return 1;
+		c = getchar(ts);
+		if (c < 0)
+			return 0;
+		buf = buf[1:];
+		buf[eix] = c;
+	}
+	return 0;	# keep the compiler quiet
+}
+
+# We've just read an '&'; look for an entity reference
+# name, and if found, return (translated char, 1).
+# Otherwise the input stream is rewound to just after
+# the '&'
+# if there is a complete entity name but it isn't known,
+# ('&', 1) is returned, if an incomplete name is encountered
+# (0, 0) is returned
+ampersand(ts: ref TokenSource): (int, int)
+{
+	state := getstate(ts);
+	c := getchar(ts);
+	fnd := 0;
+	ans := 0;
+	if(c == '#') {
+		v := 0;
+		c = getchar(ts);
+		if (c == 'x' || c == 'X') {
+			for (c = getchar(ts); c >= 0; c = getchar(ts)) {
+				if (int (ctype[c] & C->D)) {
+					v = v*16 + c-'0';
+					continue;
+				}
+				c = lowerc(c);
+				if (c >= 'a' && c <= 'f') {
+					v = v*16 + 10 + c-'a';
+					continue;
+				}
+				break;
+			}
+		} else {
+			while(c >= 0) {
+				if(ctype[c] != C->D)
+					break;
+				v = v*10 + c-'0';
+				c = getchar(ts);
+			}
+		}
+		if(c >= 0) {
+			if(!(c == ';' || c == '\n' || c == '\r' || c == '<'))
+				ungetchar(ts);
+			c = v;
+			if(c==160)
+				c = ' ';   # non-breaking space
+			if(c >= Winstart && c <= Winend)
+				c = winchars[c-Winstart];
+			ans = c;
+			fnd = (v != 0);
+		}
+	}
+	# only US-ASCII chars can make up &charnames;
+	else if(c >= 0 && c < 16r80 && int (ctype[c] & LETTER)) {
+		s := "";
+		s[0] = c;
+		k := 1;
+		for(;;) {
+			c = getchar(ts);
+			if(c < 0)
+				break;
+			if(c < 16r80 && int (ctype[c]&NAMCHAR))
+				s[k++] = c;
+			else {
+				if(!(c == ';' || c == '\n' || c == '\r'))
+					ungetchar(ts);
+				break;
+			}
+		}
+		if (c < 0 || c == ' ' || c == ';' || c == '\n' || c == '\r' || c == '<')
+			(fnd, ans) = T->lookup(chartab, s);
+	}
+	if(!fnd) {
+		if(c < 0 && !eof(ts)) {
+			# was incomplete
+			rewind(ts, state);
+			return (0, 0);
+		}
+		else {
+			rewind(ts, state);
+			return ('&', 1);
+		}
+	}
+	# elide soft hyphens (&shy; / &xAD;)
+# not suficient - need to do it for all input in getdata() which is too heavy handed
+#	if (ans == '­')
+#		ans = 0;
+	return (ans, 1);
+}
+
+# If c is an uppercase letter, return its lowercase version,
+# otherwise return c.
+# Assume c is a NAMCHAR, so don't need range check on ctype[]
+lowerc(c: int) : int
+{
+	if(ctype[c] == C->U) {
+		# this works for accented characters in Latin1, too
+		return c + 16r20;
+	}
+	return c;
+}
+
+Token.aval(t: self ref Token, attid: int): (int, string)
+{
+	attr := t.attr;
+	while(attr != nil) {
+		a := hd attr;
+		if(a.attid == attid)
+			return (1, a.value); 
+		attr = tl attr;
+	}
+	return (0, "");
+}
+
+
+# for debugging
+Token.tostring(t: self ref Token) : string
+{
+	ans := "";
+	tag := t.tag;
+	if(tag == Data)
+		ans = ans + "'" + t.text + "'";
+	else {
+		ans = ans + "<";
+		if(tag >= RBRA) {
+			tag -= RBRA;
+			ans = ans + "/";
+		}
+		tname := tagnames[tag];
+		if(tag == Notfound)
+			tname = "?";
+		ans = ans + S->toupper(tname);
+		for(al := t.attr; al != nil; al = tl al) {
+			a := hd al;
+			aname := attrnames[a.attid];
+			ans = ans + " " + aname;
+			if(a.value != "")
+				ans = ans + "='" + a.value + "'";
+		}
+		ans = ans + ">";
+	}
+	return ans;
+}
+
+
+CONVBLK : con 1024;		# number of characters to convert at a time
+
+# Returns -1 if no complete character left before current end of data.
+getchar(ts: ref TokenSource): int
+{
+	st := ts.state;
+	if (st.s == nil || st.si >= len st.s) {
+		bs := ts.b;
+		st.si = 0;
+		st.s = "";
+		st.prevcsstate = st.csstate;
+		st.prevbi = st.bi;
+		edata := bs.edata;
+		if (st.bi >= edata)
+			return -1;
+		(state, s, n ) := ts.chset->btos(st.csstate, bs.data[st.bi:edata], CONVBLK);
+		if (s == nil) {
+			if (bs.eof && edata == bs.edata) {
+				# must have been an encoding error at eof
+				st.prevbi = st.bi = edata;
+			}
+			return -1;
+		}
+		st.csstate = state;
+		st.s = s;
+		st.bi += n;
+	}
+	return st.s[st.si++];
+}
+
+# back up by one input character
+# NOTE: can only call this function post a successful getchar() call
+ungetchar(ts : ref TokenSource)
+{
+	st := ts.state;
+	# assert(len st.s >= 1 && st.si > 0)
+	if (st.si <= 0)
+		raise "EXInternal:too many backups";
+	st.si--;
+}
+
+rewind(ts : ref TokenSource, state : ref TSstate)
+{
+	ts.state = state;
+}
+
+# return a copy of the TokenSource state
+getstate(ts : ref TokenSource) : ref TSstate
+{
+	return ref *ts.state;
+}
+
--- /dev/null
+++ b/appl/charon/lex.m
@@ -1,0 +1,105 @@
+Lex: module
+{
+	PATH: con "/dis/charon/lex.dis";
+
+	# HTML 4.0 tags (blink, nobr)
+	# sorted in lexical order; used as array indices
+	Notfound, Comment,
+	Ta, Tabbr, Tacronym, Taddress, Tapplet, Tarea, Tb,
+		Tbase, Tbasefont, Tbdo, Tbig, Tblink, Tblockquote, Tbody,
+		Tbq, Tbr, Tbutton, Tcaption, Tcenter, Tcite, Tcode, Tcol, Tcolgroup,
+		Tdd, Tdel, Tdfn, Tdir, Tdiv, Tdl, Tdt, Tem,
+		Tfieldset, Tfont, Tform, Tframe, Tframeset,
+		Th1, Th2, Th3, Th4, Th5, Th6, Thead, Thr, Thtml, Ti, Tiframe, Timage,
+		Timg, Tinput, Tins, Tisindex, Tkbd, Tlabel, Tlegend, Tli, Tlink, Tmap,
+		Tmenu, Tmeta, Tnobr, Tnoframes, Tnoscript,
+		Tobject, Tol, Toptgroup, Toption, Tp, Tparam, Tpre,
+		Tq, Ts, Tsamp, Tscript, Tselect, Tsmall, Tspan, Tstrike, Tstrong,
+		Tstyle, Tsub, Tsup, Ttable, Ttbody, Ttd, Ttextarea, Ttfoot, Tth,
+		Tthead, Ttitle, Ttr, Ttt, Tu, Tul, Tvar, Txmp,
+		Numtags
+			: con iota;
+	RBRA : con Numtags;
+	Data: con Numtags+RBRA;
+
+	tagnames: array of string;
+
+	# HTML 4.0 tag attributes
+	# Keep sorted in lexical order
+	Aabbr, Aaccept, Aaccept_charset, Aaccesskey, Aaction,
+		Aalign, Aalink, Aalt, Aarchive, Aaxis,
+		Abackground, Abgcolor, Aborder,
+		Acellpadding, Acellspacing, Achar, Acharoff,
+		Acharset, Achecked, Acite, Aclass, Aclassid, Aclear,
+		Acode, Acodebase, Acodetype,
+		Acolor, Acols, Acolspan, Acompact, Acontent, Acoords,
+		Adata, Adatafld, Adataformatas, Adatapagesize, Adatasrc,
+		Adatetime, Adeclare, Adefer, Adir, Adisabled,
+		Aenctype, Aevent,
+		Aface, Afor, Aframe, Aframeborder,
+		Aheaders, Aheight, Ahref, Ahreflang, Ahspace, Ahttp_equiv,
+		Aid, Aismap, Alabel, Alang, Alanguage, Alink, Alongdesc, Alowsrc,
+		Amarginheight, Amarginwidth, Amaxlength, Amedia, Amethod, Amultiple,
+		Aname, Anohref, Anoresize, Anoshade, Anowrap, Aobject,
+		Aonabort, Aonblur, Aonchange, Aonclick, Aondblclick,
+		Aonerror, Aonfocus, Aonkeydown, Aonkeypress, Aonkeyup, Aonload,
+		Aonmousedown, Aonmousemove, Aonmouseout, Aonmouseover,
+		Aonmouseup, Aonreset, Aonresize, Aonselect, Aonsubmit, Aonunload,
+		Aprofile, Aprompt, Areadonly, Arel, Arev, Arows, Arowspan, Arules,
+		Ascheme, Ascope, Ascrolling, Aselected, Ashape, Asize,
+		Aspan, Asrc, Astandby, Astart, Astyle, Asummary,
+		Atabindex, Atarget, Atext, Atitle, Atype, Ausemap,
+		Avalign, Avalue, Avaluetype, Aversion, Avlink, Avspace, Awidth,
+		Numattrs
+			: con iota;
+
+	attrnames: array of string;
+
+	Token: adt
+	{
+		tag:		int;
+		text:		string;	# text in Data, attribute text in tag
+		attr:		list of Attr;
+
+		aval: fn(t: self ref Token, attid: int) : (int, string);
+		tostring: fn(t: self ref Token) : string;
+	};
+
+	Attr: adt
+	{
+		attid:		int;
+		value:	string;
+	};
+
+	# A source of HTML tokens.
+	# After calling new with a ByteSource (which is past 'gethdr' stage),
+	# call gettoks repeatedly until get nil.  Errors are signalled by exceptions.
+	# Possible exceptions raised:
+	#	EXInternal		(start, gettoks)
+	#	exGeterror	(gettoks)
+	#	exAbort		(gettoks)
+	TokenSource: adt
+	{
+		b: ref CharonUtils->ByteSource;
+		chset: Btos;				# charset converter
+		state : ref TSstate;
+		mtype: int;				# CU->TextHtml or CU->TextPlain
+		inxmp: int;
+
+		new: fn(b: ref CharonUtils->ByteSource, chset : Btos, mtype: int) : ref TokenSource;
+		gettoks: fn(ts: self ref TokenSource) : array of ref Token;
+		setchset: fn(ts: self ref TokenSource, conv : Btos);
+	};
+
+	TSstate : adt {
+		bi : int;
+		prevbi : int;
+		s : string;
+		si : int;
+		csstate : Convcs->State;
+		prevcsstate : Convcs->State;
+	};
+	
+
+	init: fn(cu: CharonUtils);
+};
--- /dev/null
+++ b/appl/charon/mkfile
@@ -1,0 +1,92 @@
+<../../mkconfig
+
+TARG=\
+	build.dis\
+	cookiesrv.dis\
+	chutils.dis\
+	ctype.dis\
+	date.dis\
+	event.dis\
+	file.dis\
+	ftp.dis\
+	gui.dis\
+	http.dis\
+	img.dis\
+	jscript.dis\
+	layout.dis\
+	lex.dis\
+	url.dis\
+
+ICONS=\
+	bookmark.bit\
+	charon.bit\
+	circarrow.bit\
+	conf.bit\
+	down.bit\
+	edit.bit\
+	exit.bit\
+	help.bit\
+	history.bit\
+	home.bit\
+	maxf.bit\
+	minus.bit\
+	plus.bit\
+	redleft.bit\
+	redright.bit\
+	ssloff.bit\
+	sslon.bit\
+	stop.bit\
+	task.bit\
+	up.bit\
+
+MODULES=\
+	build.m\
+	charon.m\
+	chutils.m\
+	common.m\
+	cookiesrv.m\
+	ctype.m\
+	date.m\
+	event.m\
+	gui.m\
+	img.m\
+	layout.m\
+	lex.m\
+	script.m\
+	transport.m\
+	rgb.inc\
+	ycbcr.inc\
+	url.m\
+
+SYSMODULES=\
+	bufio.m\
+	daytime.m\
+	debug.m\
+	draw.m\
+	ecmascript.m\
+	sh.m\
+	ssl3.m\
+	string.m\
+	strinttab.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+
+DISBIN=$ROOT/dis/charon
+
+all:V:	charon.dis
+
+<$ROOT/mkfiles/mkdis
+
+install:V:	$ROOT/dis/charon.dis
+
+$ROOT/dis/charon.dis:	charon.dis
+	rm -f $target && cp charon.dis $target
+
+charon.dis:	$MODULES $SYS_MODULES
+
+img.dis:	img.b $MODULE $SYS_MODULE
+	limbo $LIMBOFLAGS -c -gw img.b
+
+nuke:V:
+	rm -f $ROOT/dis/charon.dis
--- /dev/null
+++ b/appl/charon/paginate.b
@@ -1,0 +1,511 @@
+implement Paginate;
+
+include "common.m";
+include "print.m";
+include "paginate.m";
+
+sys: Sys;
+print: Print;
+L: Layout;
+D: Draw;
+
+Frame, Lay, Line, Control: import Layout;
+Docinfo: import Build;
+Image, Display, Rect, Point: import D;
+Item: import Build;
+MaskedImage: import CharonUtils;
+
+disp: ref Display;
+p0: Point;
+nulimg: ref Image;
+
+DPI: con 110;
+
+init(layout: Layout, draw: Draw, display: ref Draw->Display): string
+{
+	sys = load Sys Sys->PATH;
+	L = layout;
+	D = draw;
+	disp = display;
+	if (L == nil || D == nil || disp == nil)
+		return "bad args";
+	print = load Print Print->PATH;
+	if (print == nil)
+		return sys->sprint("cannot load module %s: %r", Print->PATH);
+	print->init();
+#nullfd := sys->open("/dev/null", Sys->OWRITE);
+#print->set_printfd(nullfd);
+	p0 = Point(0, 0);
+	nulimg = disp.newimage(((0, 0), (1, 1)), 3, 0, 0);
+	return nil;
+}
+
+paginate(frame: ref Layout->Frame, orient: int, pnums, cancel: chan of int, result: chan of (string, ref Pageset))
+{
+	pidc := chan of int;
+	spawn watchdog(pidc, cancel, nil, sys->pctl(0, nil));
+	watchpid := <- pidc;
+
+	if (frame.kids != nil) {
+		result <-= ("cannot print frameset", nil);
+		kill(watchpid);
+		return;
+	}
+
+	defp := print->get_defprinter();
+	if (defp == nil) {
+		result <-= ("no default printer", nil);
+		kill(watchpid);
+		return;
+	}
+
+	# assuming printer's X & Y resolution are the same
+	if (orient == PORTRAIT)
+		defp.popt.orientation = Print->PORTRAIT;
+	else
+		defp.popt.orientation = Print->LANDSCAPE;
+	(dpi, pagew, pageh) := print->get_size(defp);
+	pagew = (DPI * pagew)/dpi;
+	pageh = (DPI * pageh)/dpi;
+
+	pfr := copyframe(frame);
+	pr := Rect(p0, (pagew, pageh));
+	pfr.r = pr;
+	pfr.cr = pr;
+	pfr.viewr = pr;
+	l := pfr.layout;
+	L->relayout(pfr, l, pagew, l.just);
+	maxy := l.height + l.margin; # don't include bottom margin
+	prctxt := ref Layout->Printcontext;
+	pfr.prctxt = prctxt;
+	pfr.cim = nulimg;
+	pnum := 1;
+	startys : list of int;
+
+	for (y := 0; y < maxy;) {
+		startys = y :: startys;
+		pnums <-= pnum++;
+		endy := y + pageh;
+		prctxt.endy = pageh;
+		pfr.viewr.min.y = y;
+		pfr.viewr.max.y = endy;
+		L->drawall(pfr);
+		y += prctxt.endy;
+	}
+
+	# startys are in reverse order
+	ys : list of int;
+	for (; startys != nil; startys = tl startys)
+		ys = hd startys :: ys;
+
+	pageset := ref Pageset(defp, pfr, ys);
+	result <-= (nil, pageset);
+	kill(watchpid);
+}
+
+printpageset(pset: ref Pageset, pnums, cancel: chan of int)
+{
+	pidc := chan of int;
+	stopdog := chan of int;
+	spawn watchdog(pidc, cancel, stopdog, sys->pctl(0, nil));
+	watchpid := <- pidc;
+
+	frame := pset.frame;
+	pageh := frame.cr.dy();
+	white := disp.rgb2cmap(255, 255, 255);
+	prctxt := frame.prctxt;
+	l := frame.layout;
+	maxy := l.height + l.margin; # don't include bottom margin
+	maxy = max(maxy, pageh);
+	pnum := 1;
+
+	for (pages := pset.pages; pages != nil; pages = tl pages) {
+		y := hd pages;
+		if (y + pageh > maxy)
+			pageh = maxy - y;
+		frame.cr.max.y = pageh;
+		frame.cim = disp.newimage(frame.cr, 3, 0, white);
+		if (frame.cim == nil) {
+			pnums <-= -1;
+			kill(watchpid);
+			return;
+		}
+		pnums <-= pnum++;
+		endy := y + pageh;
+		prctxt.endy = pageh;
+		frame.viewr.min.y = y;
+		frame.viewr.max.y = endy;
+		L->drawall(frame);
+		stopdog <-= 1;
+#start := sys->millisec();
+		if (print->print_image(pset.printer, disp, frame.cim, 100, cancel) == -1) {
+			# cancelled
+			kill(watchpid);
+			return;
+		}
+		stopdog <-= 1;
+#sys->print("PAGE %d: %dms\n", pnum -1, sys->millisec()-start);
+	}
+	pnums <-= -1;
+	kill(watchpid);
+}
+
+watchdog(pidc, cancel, pause: chan of int, pid: int)
+{
+	pidc <-= sys->pctl(0, nil);
+	if (pause == nil)
+		pause = chan of int;
+	for (;;) alt {
+	<- cancel =>
+		kill(pid);
+		return;
+	<- pause =>
+		<- pause;
+	}
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open("/prog/" + string pid +"/ctl", Sys->OWRITE), "kill");
+}
+
+killgrp(pid: int)
+{
+	sys->fprint(sys->open("/prog/" + string pid +"/ctl", Sys->OWRITE), "killgrp");
+}
+
+max(a, b: int): int
+{
+	if (a > b)
+		return a;
+	return b;
+}
+
+copyframe(f: ref Frame): ref Frame
+{
+
+	zr := Draw->Rect(p0, p0);
+	newf := ref Frame(
+		-1,			# id
+		nil,			# doc
+		nil,			# src
+		" PRINT FRAME ",	# name
+		f.marginw,	# marginw
+		f.marginh,	# marginh
+		0,			# framebd
+		Build->FRnoscroll,	# flags
+		nil,			# layout - filled in below, needs this frame ref
+		nil,			# sublays - filled in by geometry code
+		0,			# sublayid
+		nil,			# controls - filled in below, needs this frame ref
+		0,			# controlid - filled in below
+		nil,			# cim
+		zr,			# r
+		zr,			# cr
+		zr,			# totalr
+		zr,			# viewr
+		nil,			# vscr
+		nil,			# hscr
+		nil,			# parent
+		nil,			# kids
+		0,			# animpid
+		nil			# prctxt
+	);
+
+	newf.doc = copydoc(f, newf, f.doc);
+	controls := array [len f.controls] of ref Control;
+	for (i := 0; i < len controls; i++)
+		controls[i] = copycontrol(f, newf, f.controls[i]);
+	newf.layout = copylay(f, newf, f.layout);
+	newf.controls = controls;
+	newf.controlid = len controls;
+
+	return newf;
+}
+
+copysublay(oldf, f: ref Frame, oldid:int): int
+{
+	if (oldid < 0)
+		return -1;
+	if (f.sublayid >= len f.sublays)
+		f.sublays = (array [len f.sublays + 30] of ref Lay)[:] = f.sublays;
+	id := f.sublayid++;
+	lay := copylay(oldf, f, oldf.sublays[oldid]);
+	f.sublays[id] = lay;
+	return id;
+}
+
+copydoc(oldf, f : ref Frame, doc: ref Build->Docinfo): ref Docinfo
+{
+	background := copybackground(oldf, f, doc.background);
+	newdoc := ref Build->Docinfo(
+		nil,		#src
+		nil,		#base
+		nil,		#referrer
+		nil,		#doctitle
+		background,
+		nil,		#backgrounditem
+		doc.text, doc.link, doc.vlink, doc.alink,
+		nil,		#target
+		nil,		#refresh
+		nil,		#chset
+		nil,		#lastModified
+		0,		#scripttype
+		0,		#hasscripts
+		nil,		#events
+		0,		#evmask
+		nil,		#kidinfo
+		0,		#frameid
+		nil,		#anchors
+		nil,		#dests
+		nil,		#forms
+		nil,		#tables
+		nil,		#maps
+		nil		#images
+	);
+	return newdoc;
+}
+
+copylay(oldf, f: ref Frame, l: ref Lay): ref Lay
+{
+	start := copyline(oldf, f, nil, l.start);
+	end := start;
+	for (line := l.start.next; line != nil; line = line.next)
+		end = copyline(oldf, f, end, line);
+
+	newl := ref Lay(
+		start,
+		end,
+		l.targetwidth,		# targetwidth
+		l.width,		# width
+		l.height,		# height
+		l.margin,	# margin
+		nil,		# floats - filled in by geometry code
+		copybackground(oldf, f, l.background),
+		l.just,
+		Layout->Lchanged
+	);
+	start.flags = end.flags = byte 0;
+	return newl;
+}
+
+copycontrol(oldf, f: ref Frame, ctl: ref Control): ref Control
+{
+	if (ctl == nil)
+		return nil;
+
+	pick c := ctl {
+	Cbutton =>
+		return ref Control.Cbutton(f, nil, c.r, c.flags, nil, c.pic, c.picmask, c.dpic, c.dpicmask, c.label, c.dorelief);
+	Centry =>
+		scr := copycontrol(oldf, f, c.scr);
+		return ref Control.Centry(f, nil, c.r, c.flags, nil, scr, c.s, c.sel, c.left, c.linewrap, 0);
+	Ccheckbox=>
+		return ref Control.Ccheckbox(f, nil, c.r, c.flags, nil, c.isradio);
+	Cselect =>
+		scr := copycontrol(oldf, f, c.scr);
+		options := (array [len c.options] of Build->Option)[:] = c.options;
+		return ref Control.Cselect(f, nil, c.r, c.flags, nil, nil, scr, c.nvis, c.first, options);
+	Clistbox =>
+		hscr := copycontrol(oldf, f, c.hscr);
+		vscr := copycontrol(oldf, f, c.vscr);
+		options := (array [len c.options] of Build->Option)[:] = c.options;
+		return ref Control.Clistbox(f, nil, c.r, c.flags, nil, hscr, vscr, c.nvis, c.first, c.start, c.maxcol, options, nil);
+	Cscrollbar =>
+		# do not copy ctl as this is set by those associated controls
+		return ref Control.Cscrollbar(f, nil, c.r, c.flags, nil, c.top, c.bot, c.mindelta, c.deltaval, nil, c.holdstate);
+	Canimimage =>
+		bg := copybackground(oldf, f, c.bg);
+		return ref Control.Canimimage(f, nil, c.r, c.flags, nil, c.cim, 0, 0, big 0, bg);
+	Clabel =>
+		return ref Control.Clabel(f, nil, c.r, c.flags, nil, c.s);
+	* =>
+		return nil;
+	}
+}
+
+copyline(oldf, f: ref Frame, prev, l: ref Line): ref Line
+{
+	if (l == nil)
+		return nil;
+	cp := ref *l;
+	items := copyitems(oldf, f, l.items);
+	newl := ref Line (items, nil, prev, l.pos, l.width, l.height, l.ascent, Layout->Lchanged);
+	if (prev != nil)
+		prev.next = newl;
+	return newl;
+}
+
+copyitems(oldf, f: ref Frame, items: ref Item): ref Item
+{
+	if (items == nil)
+		return nil;
+	item := copyitem(oldf, f, items);
+	end := item;
+	for (items = items.next; items != nil; items = items.next) {
+		end.next = copyitem(oldf, f, items);
+		end = end.next;
+	}
+	return item;
+}
+
+copyitem(oldf, f : ref Frame, item: ref Item): ref Item
+{
+	if (item == nil)
+		return nil;
+	pick it := item {
+	Itext =>
+		return ref Item.Itext(
+			nil, it.width, it.height, it.ascent, 0, it.state, nil,
+			it.s, it.fnt, it.fg, it.voff, it.ul);
+	Irule =>
+		return ref Item.Irule(
+			nil, it.width, it.height, it.ascent, 0, it.state, nil,
+			it.align, it.noshade, it.size, it.wspec);
+	Iimage =>
+		# need to copy the image to prevent
+		# ongoing image fetches from messing up our layout
+		ci := copycimage(it.ci);
+		return ref Item.Iimage(
+			nil, it.width, it.height, it.ascent, 0, it.state, nil,
+			it.imageid, ci, it.imwidth, it.imheight, it.altrep,
+			nil, it.name, -1, it.align, it.hspace, it.vspace, it.border);
+	Iformfield =>
+		return ref Item.Iformfield(
+			nil, it.width, it.height, it.ascent, 0, it.state, nil,
+			copyformfield(oldf, f, it.formfield)
+		);
+	Itable =>
+		return ref Item.Itable(
+			nil, it.width, it.height, it.ascent, 0, it.state, nil,
+			copytable(oldf, f, it.table));
+	Ifloat =>
+		items := copyitem(oldf, f, it.item);
+		return ref Item.Ifloat(
+			nil, it.width, it.height, it.ascent, 0, it.state, nil,
+			items, it.x, it.y, it.side, byte 0);
+	Ispacer =>
+		return ref Item.Ispacer(
+			nil, it.width, it.height, it.ascent, 0, it.state, nil,
+			it.spkind, it.fnt);
+	* =>
+		return nil;
+	}
+}
+
+copycimage(ci: ref CharonUtils->CImage): ref CharonUtils->CImage
+{
+	if (ci == nil)
+		return nil;
+	mims : array of ref MaskedImage;
+	if (len ci.mims > 0)
+		# if len> 1 then animated, but we only want first frame
+		mims = array [1] of {0 => ci.mims[0]};
+	return ref CharonUtils->CImage(nil, nil, nil, 0, ci.width, ci.height, nil, mims, 0);
+}
+
+copyformfield(oldf, f: ref Frame, ff: ref Build->Formfield): ref Build->Formfield
+{
+	image := copyitem(oldf, f, ff.image);
+	# should be safe to reference Option list
+	newff := ref Build->Formfield(
+		ff.ftype, 0, nil, ff.name, ff.value, ff.size, ff.maxlength, ff.rows,
+		ff.cols, ff.flags, ff.options, image, ff.ctlid, nil, 0
+	);
+	return newff;
+}
+
+copytable(oldf, f: ref Frame, tbl: ref Build->Table): ref Build->Table
+{
+	nrow := tbl.nrow;
+	ncol := tbl.ncol;
+	caption_lay := copysublay(oldf, f, tbl.caption_lay);
+	cols := (array [ncol] of Build->Tablecol)[:] = tbl.cols;
+	rows := array [nrow] of ref Build->Tablerow;
+	for (i := 0; i < nrow; i++) {
+		r := tbl.rows[i];
+		rows[i] = ref Build->Tablerow(nil, r.height, r.ascent, r.align, r.background, r.pos, r.flags);
+	}
+
+	cells : list of ref Build->Tablecell;
+	grid := array [nrow] of {* => array [ncol] of ref Build->Tablecell};
+	for (rix := 0; rix < nrow; rix++) {
+		rowcells: list of ref Build->Tablecell = nil;
+		for (colix := 0; colix < ncol; colix++) {
+			cell := copytablecell(oldf, f, tbl.grid[rix][colix]);
+			if (cell == nil)
+				continue;
+			grid[rix][colix] = cell;
+			cells = cell :: cells;
+			rowcells = cell :: rowcells;
+		}
+		# reverse the row cells;
+		rcells : list of ref Build->Tablecell = nil;
+		for (; rowcells != nil; rowcells = tl rowcells)
+			rcells = hd rowcells :: rcells;
+		rows[rix].cells = rcells;
+	}
+
+	# reverse the cells
+	sllec: list of ref Build->Tablecell;
+	for (; cells != nil; cells = tl cells)
+		sllec = hd cells :: sllec;
+	cells = sllec;
+
+	return ref Build->Table(
+		tbl.tableid,	# tableid
+		nrow,		# nrow
+		ncol,			# ncol
+		len cells,		# ncell
+		tbl.align,		# align
+		tbl.width,		# width
+		tbl.border,	# border
+		tbl.cellspacing,	# cellspacing
+		tbl.cellpadding,	# cellpadding
+		tbl.background,	# background
+		nil,			# caption
+		tbl.caption_place,	# caption_place
+		caption_lay,	# caption_lay
+		nil,			# currows
+		cols,			# cols
+		rows,		# rows
+		cells,		# cells
+		tbl.totw,		# totw
+		tbl.toth,		# toth
+		tbl.caph,		# caph
+		tbl.availw,	# availw
+		grid,			# grid
+		nil,			# tabletok
+		Layout->Lchanged		# flags
+	);
+}
+
+copytablecell(oldf, f: ref Frame, cell: ref Build->Tablecell): ref Build->Tablecell
+{
+	if (cell == nil)
+		return nil;
+
+	layid := copysublay(oldf, f, cell.layid);
+	background := copybackground(oldf, f, cell.background);
+	newcell := ref Build->Tablecell(
+		cell.cellid,
+		nil,	# content
+		layid,
+		cell.rowspan, cell.colspan, cell.align,
+		cell.flags, cell.wspec, cell.hspec,
+		background, cell.minw, cell.maxw,
+		cell.ascent, cell.row, cell.col, cell.pos);
+	return newcell;
+}
+
+copybackground(oldf, f: ref Frame, bg: Build->Background): Build->Background
+{
+	img := copyitem(oldf, f, bg.image);
+	if (img != nil) {
+		pick i := img {
+		Iimage =>
+			bg.image = i;
+		}
+	}
+	return bg;
+}
--- /dev/null
+++ b/appl/charon/paginate.m
@@ -1,0 +1,16 @@
+Paginate: module {
+	PATH: con "/dis/charon/paginate.dis";
+
+	init: fn(layout: Layout, draw: Draw, display: ref Draw->Display): string;
+
+	Pageset: adt {
+		printer: ref Print->Printer;
+		frame: ref Layout->Frame;
+		pages: list of int;
+	};
+
+	PORTRAIT, LANDSCAPE: con iota;
+
+	paginate: fn(frame: ref Layout->Frame, orient: int, pagenums, cancel: chan of int, result: chan of (string, ref Pageset));
+	printpageset: fn(pages: ref Pageset, pagenums, cancel: chan of int);
+};
--- /dev/null
+++ b/appl/charon/rgb.inc
@@ -1,0 +1,620 @@
+# closest color in the rgbvmap, indexed by B+16*(G+16*B)
+closestrgb:= array[16*16*16] of {
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 255,byte 255,byte 250,byte 250,byte 250,byte 220,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 250,byte 250,byte 250,byte 250,byte 249,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 250,byte 250,byte 250,byte 233,byte 233,byte 249,
+	byte 249,byte 232,byte 215,byte 215,byte 248,byte 231,byte 214,byte 197,
+	byte 234,byte 234,byte 250,byte 250,byte 233,byte 233,byte 216,byte 216,
+	byte 249,byte 232,byte 215,byte 198,byte 198,byte 231,byte 214,byte 197,
+	byte 217,byte 217,byte 217,byte 246,byte 233,byte 216,byte 216,byte 199,
+	byte 199,byte 215,byte 215,byte 198,byte 198,byte 198,byte 214,byte 197,
+	byte 200,byte 200,byte 246,byte 246,byte 246,byte 216,byte 199,byte 199,
+	byte 245,byte 245,byte 198,byte 244,byte 244,byte 244,byte 227,byte 197,
+	byte 247,byte 247,byte 246,byte 246,byte 246,byte 246,byte 199,byte 245,
+	byte 245,byte 245,byte 228,byte 244,byte 244,byte 244,byte 227,byte 193,
+	byte 230,byte 230,byte 246,byte 246,byte 229,byte 229,byte 212,byte 245,
+	byte 245,byte 228,byte 228,byte 211,byte 244,byte 227,byte 210,byte 193,
+	byte 213,byte 213,byte 229,byte 229,byte 212,byte 212,byte 212,byte 195,
+	byte 228,byte 228,byte 211,byte 211,byte 194,byte 227,byte 210,byte 193,
+	byte 196,byte 196,byte 242,byte 242,byte 212,byte 195,byte 195,byte 241,
+	byte 241,byte 211,byte 211,byte 194,byte 194,byte 240,byte 210,byte 193,
+	byte 243,byte 243,byte 242,byte 242,byte 242,byte 195,byte 195,byte 241,
+	byte 241,byte 241,byte 194,byte 194,byte 240,byte 240,byte 239,byte 205,
+	byte 226,byte 226,byte 242,byte 242,byte 225,byte 225,byte 195,byte 241,
+	byte 241,byte 224,byte 224,byte 240,byte 240,byte 239,byte 239,byte 205,
+	byte 209,byte 209,byte 225,byte 225,byte 208,byte 208,byte 208,byte 224,
+	byte 224,byte 223,byte 223,byte 223,byte 239,byte 239,byte 222,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 238,byte 221,byte 221,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 221,byte 221,byte 221,byte 204,byte 250,byte 220,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 221,byte 221,byte 204,byte 250,byte 250,byte 249,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 204,byte 250,byte 250,byte 233,byte 233,byte 249,
+	byte 249,byte 232,byte 215,byte 215,byte 248,byte 231,byte 214,byte 197,
+	byte 234,byte 234,byte 250,byte 250,byte 233,byte 233,byte 216,byte 216,
+	byte 249,byte 232,byte 215,byte 198,byte 198,byte 231,byte 214,byte 197,
+	byte 217,byte 217,byte 217,byte 246,byte 233,byte 216,byte 216,byte 199,
+	byte 199,byte 215,byte 215,byte 198,byte 198,byte 198,byte 214,byte 197,
+	byte 200,byte 200,byte 246,byte 246,byte 246,byte 216,byte 199,byte 199,
+	byte 245,byte 245,byte 198,byte 244,byte 244,byte 244,byte 227,byte 197,
+	byte 247,byte 247,byte 246,byte 246,byte 246,byte 246,byte 199,byte 245,
+	byte 245,byte 245,byte 228,byte 244,byte 244,byte 244,byte 227,byte 193,
+	byte 230,byte 230,byte 246,byte 246,byte 229,byte 229,byte 212,byte 245,
+	byte 245,byte 228,byte 228,byte 211,byte 244,byte 227,byte 210,byte 193,
+	byte 213,byte 213,byte 229,byte 229,byte 212,byte 212,byte 212,byte 195,
+	byte 228,byte 228,byte 211,byte 211,byte 194,byte 227,byte 210,byte 193,
+	byte 196,byte 196,byte 242,byte 242,byte 212,byte 195,byte 195,byte 241,
+	byte 241,byte 211,byte 211,byte 194,byte 194,byte 240,byte 210,byte 193,
+	byte 243,byte 243,byte 242,byte 242,byte 242,byte 195,byte 195,byte 241,
+	byte 241,byte 241,byte 194,byte 194,byte 240,byte 240,byte 239,byte 205,
+	byte 226,byte 226,byte 242,byte 242,byte 225,byte 225,byte 195,byte 241,
+	byte 241,byte 224,byte 224,byte 240,byte 240,byte 239,byte 239,byte 205,
+	byte 209,byte 209,byte 225,byte 225,byte 208,byte 208,byte 208,byte 224,
+	byte 224,byte 223,byte 223,byte 223,byte 239,byte 239,byte 222,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 255,byte 255,byte 255,byte 191,byte 191,byte 191,byte 220,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 255,byte 221,byte 221,byte 221,byte 204,byte 191,byte 220,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 255,byte 221,byte 221,byte 204,byte 204,byte 204,byte 186,byte 186,
+	byte 186,byte 186,byte 186,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 221,byte 204,byte 204,byte 204,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 232,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 233,byte 216,byte 186,
+	byte 186,byte 186,byte 215,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 217,byte 217,byte 183,byte 183,byte 183,byte 216,byte 216,byte 199,
+	byte 182,byte 182,byte 215,byte 198,byte 198,byte 181,byte 214,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 199,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 181,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 193,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 228,byte 165,byte 181,byte 181,byte 164,byte 164,byte 193,
+	byte 167,byte 167,byte 183,byte 229,byte 166,byte 212,byte 212,byte 182,
+	byte 182,byte 165,byte 211,byte 211,byte 181,byte 164,byte 210,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 195,byte 178,
+	byte 178,byte 178,byte 211,byte 194,byte 177,byte 177,byte 177,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 195,byte 178,
+	byte 178,byte 178,byte 178,byte 177,byte 177,byte 177,byte 177,byte 205,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 178,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 177,byte 160,byte 205,
+	byte 163,byte 163,byte 162,byte 162,byte 162,byte 162,byte 208,byte 178,
+	byte 161,byte 161,byte 223,byte 177,byte 177,byte 160,byte 160,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 176,byte 176,byte 191,byte 191,byte 191,byte 191,byte 190,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 176,byte 221,byte 221,byte 204,byte 191,byte 191,byte 190,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 188,byte 221,byte 204,byte 204,byte 204,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 173,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 168,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 170,byte 170,byte 182,
+	byte 182,byte 169,byte 152,byte 152,byte 181,byte 168,byte 151,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 165,byte 181,byte 181,byte 181,byte 164,byte 193,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 164,byte 193,
+	byte 167,byte 167,byte 167,byte 166,byte 166,byte 166,byte 149,byte 182,
+	byte 165,byte 165,byte 165,byte 148,byte 181,byte 164,byte 147,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 149,byte 178,
+	byte 178,byte 178,byte 148,byte 177,byte 177,byte 177,byte 147,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 178,byte 178,
+	byte 178,byte 178,byte 178,byte 177,byte 177,byte 177,byte 160,byte 205,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 162,byte 162,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 160,byte 160,byte 205,
+	byte 163,byte 163,byte 162,byte 162,byte 162,byte 162,byte 145,byte 161,
+	byte 161,byte 161,byte 144,byte 144,byte 160,byte 160,byte 160,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 176,byte 176,byte 191,byte 191,byte 191,byte 174,byte 174,byte 190,
+	byte 190,byte 173,byte 156,byte 156,byte 189,byte 172,byte 155,byte 138,
+	byte 176,byte 176,byte 204,byte 191,byte 191,byte 174,byte 174,byte 190,
+	byte 190,byte 173,byte 156,byte 156,byte 189,byte 172,byte 155,byte 138,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 187,byte 187,byte 170,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 152,byte 152,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 183,byte 183,byte 170,byte 170,byte 170,byte 153,
+	byte 182,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 153,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 165,byte 181,byte 181,byte 181,byte 164,byte 130,
+	byte 167,byte 167,byte 183,byte 183,byte 166,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 166,byte 166,byte 166,byte 149,byte 149,byte 182,
+	byte 165,byte 165,byte 148,byte 148,byte 164,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 179,byte 179,byte 179,byte 149,byte 132,byte 178,
+	byte 178,byte 178,byte 148,byte 131,byte 177,byte 177,byte 147,byte 130,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 132,byte 178,
+	byte 178,byte 178,byte 161,byte 177,byte 177,byte 177,byte 160,byte 142,
+	byte 163,byte 163,byte 179,byte 179,byte 162,byte 162,byte 162,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 162,byte 162,byte 145,byte 145,byte 145,byte 161,
+	byte 161,byte 144,byte 144,byte 144,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 175,byte 175,byte 191,byte 191,byte 174,byte 174,byte 157,byte 157,
+	byte 190,byte 173,byte 156,byte 139,byte 139,byte 172,byte 155,byte 138,
+	byte 175,byte 175,byte 191,byte 191,byte 174,byte 174,byte 157,byte 157,
+	byte 190,byte 173,byte 156,byte 139,byte 139,byte 172,byte 155,byte 138,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 157,byte 186,
+	byte 186,byte 186,byte 156,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 168,byte 138,
+	byte 171,byte 171,byte 187,byte 187,byte 170,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 152,byte 152,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 187,byte 170,byte 170,byte 170,byte 170,byte 153,
+	byte 169,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 170,byte 153,byte 153,
+	byte 169,byte 152,byte 152,byte 135,byte 135,byte 135,byte 151,byte 134,
+	byte 154,byte 154,byte 183,byte 183,byte 183,byte 153,byte 153,byte 153,
+	byte 182,byte 182,byte 135,byte 135,byte 181,byte 181,byte 164,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 164,byte 130,
+	byte 167,byte 167,byte 183,byte 166,byte 166,byte 166,byte 149,byte 182,
+	byte 165,byte 165,byte 165,byte 148,byte 181,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 150,byte 166,byte 149,byte 149,byte 149,byte 132,
+	byte 165,byte 165,byte 148,byte 148,byte 131,byte 147,byte 147,byte 130,
+	byte 133,byte 133,byte 179,byte 179,byte 149,byte 132,byte 132,byte 132,
+	byte 178,byte 148,byte 148,byte 131,byte 131,byte 131,byte 130,byte 130,
+	byte 133,byte 133,byte 179,byte 179,byte 179,byte 132,byte 132,byte 178,
+	byte 178,byte 178,byte 131,byte 131,byte 131,byte 177,byte 160,byte 142,
+	byte 163,byte 163,byte 179,byte 162,byte 162,byte 162,byte 132,byte 178,
+	byte 161,byte 161,byte 144,byte 131,byte 177,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 162,byte 162,byte 145,byte 145,byte 145,byte 161,
+	byte 161,byte 144,byte 144,byte 143,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 158,byte 158,byte 158,byte 112,byte 174,byte 157,byte 157,byte 140,
+	byte 140,byte 156,byte 156,byte 139,byte 139,byte 139,byte 155,byte 138,
+	byte 158,byte 158,byte 158,byte 112,byte 174,byte 157,byte 157,byte 140,
+	byte 140,byte 156,byte 156,byte 139,byte 139,byte 139,byte 155,byte 138,
+	byte 158,byte 158,byte 124,byte 124,byte 124,byte 157,byte 157,byte 140,
+	byte 123,byte 123,byte 156,byte 139,byte 139,byte 122,byte 155,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 170,byte 170,byte 123,
+	byte 123,byte 169,byte 152,byte 152,byte 122,byte 168,byte 151,byte 138,
+	byte 171,byte 171,byte 124,byte 124,byte 170,byte 170,byte 170,byte 153,
+	byte 123,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 170,byte 153,byte 153,
+	byte 169,byte 152,byte 152,byte 135,byte 135,byte 135,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 153,byte 153,byte 153,
+	byte 136,byte 152,byte 135,byte 135,byte 135,byte 135,byte 134,byte 134,
+	byte 137,byte 137,byte 137,byte 120,byte 153,byte 153,byte 153,byte 136,
+	byte 136,byte 136,byte 135,byte 135,byte 135,byte 118,byte 164,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 166,byte 136,byte 136,
+	byte 136,byte 165,byte 165,byte 118,byte 118,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 120,byte 166,byte 166,byte 149,byte 149,byte 136,
+	byte 165,byte 165,byte 148,byte 148,byte 118,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 150,byte 149,byte 149,byte 149,byte 132,byte 132,
+	byte 165,byte 148,byte 148,byte 131,byte 131,byte 147,byte 147,byte 130,
+	byte 133,byte 133,byte 133,byte 149,byte 132,byte 132,byte 132,byte 132,
+	byte 115,byte 148,byte 131,byte 131,byte 131,byte 131,byte 130,byte 130,
+	byte 133,byte 133,byte 133,byte 116,byte 132,byte 132,byte 132,byte 132,
+	byte 115,byte 115,byte 131,byte 131,byte 131,byte 131,byte 160,byte 142,
+	byte 133,byte 133,byte 116,byte 162,byte 162,byte 132,byte 132,byte 115,
+	byte 161,byte 161,byte 144,byte 131,byte 131,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 146,byte 145,byte 145,byte 145,byte 128,byte 161,
+	byte 144,byte 144,byte 144,byte 143,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 141,byte 141,byte 112,byte 112,byte 112,byte 157,byte 140,byte 140,
+	byte 140,byte 127,byte 139,byte 126,byte 126,byte 126,byte 109,byte 138,
+	byte 141,byte 141,byte 112,byte 112,byte 112,byte 157,byte 140,byte 140,
+	byte 140,byte 127,byte 139,byte 126,byte 126,byte 126,byte 109,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 140,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 122,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 105,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 153,byte 123,
+	byte 123,byte 123,byte 152,byte 122,byte 122,byte 122,byte 105,byte 134,
+	byte 154,byte 154,byte 124,byte 124,byte 124,byte 153,byte 153,byte 153,
+	byte 123,byte 123,byte 135,byte 135,byte 122,byte 122,byte 105,byte 134,
+	byte 137,byte 137,byte 137,byte 120,byte 153,byte 153,byte 153,byte 136,
+	byte 136,byte 136,byte 135,byte 135,byte 135,byte 118,byte 105,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 153,byte 136,byte 136,
+	byte 136,byte 119,byte 119,byte 118,byte 118,byte 118,byte 118,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 120,byte 136,byte 136,
+	byte 119,byte 119,byte 119,byte 118,byte 118,byte 118,byte 101,byte 130,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte 130,
+	byte 133,byte 133,byte 120,byte 120,byte 149,byte 132,byte 132,byte 119,
+	byte 119,byte 102,byte 148,byte 131,byte 131,byte 101,byte 101,byte 130,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 132,byte 132,byte 115,
+	byte 115,byte 115,byte 131,byte 131,byte 114,byte 114,byte 114,byte 130,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 132,byte 115,
+	byte 115,byte 115,byte 131,byte 114,byte 114,byte 114,byte 114,byte 142,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte  98,byte 114,byte 114,byte 114,byte  97,byte 142,
+	byte 100,byte 100,byte 116,byte  99,byte  99,byte  99,byte  99,byte 115,
+	byte  98,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 113,byte 113,byte 112,byte 112,byte 112,byte 112,byte 140,byte 140,
+	byte 127,byte 127,byte 110,byte 126,byte 126,byte 126,byte 109,byte  75,
+	byte 113,byte 113,byte 112,byte 112,byte 112,byte 112,byte 140,byte 140,
+	byte 127,byte 127,byte 110,byte 126,byte 126,byte 126,byte 109,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 122,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 122,byte 105,byte  71,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte 105,byte  71,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 107,byte 136,byte 136,
+	byte 136,byte 106,byte 106,byte 118,byte 118,byte 105,byte  88,byte  71,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 120,byte 136,byte 136,
+	byte 119,byte 119,byte 119,byte 118,byte 118,byte 118,byte 101,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte  67,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 103,byte 103,byte 119,
+	byte 119,byte 102,byte 102,byte 118,byte 118,byte 101,byte 101,byte  67,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte 115,byte 114,byte 114,byte 114,byte 114,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte 115,byte 114,byte 114,byte 114,byte  97,byte  79,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte  99,byte  99,byte 115,
+	byte 115,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  99,byte  82,byte  98,
+	byte  98,byte  98,byte  81,byte 114,byte  97,byte  97,byte  97,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  96,byte  96,byte 112,byte 112,byte 111,byte 111,byte  94,byte 127,
+	byte 127,byte 110,byte 110,byte  93,byte 126,byte 109,byte  92,byte  75,
+	byte  96,byte  96,byte 112,byte 112,byte 111,byte 111,byte  94,byte 127,
+	byte 127,byte 110,byte 110,byte  93,byte 126,byte 109,byte  92,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 105,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte 105,byte  75,
+	byte 108,byte 108,byte 124,byte 124,byte 107,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte  88,byte  71,
+	byte 108,byte 108,byte 124,byte 107,byte 107,byte 107,byte  90,byte 123,
+	byte 106,byte 106,byte 106,byte  89,byte 122,byte 105,byte  88,byte  71,
+	byte  91,byte  91,byte 120,byte 107,byte 107,byte  90,byte  90,byte 136,
+	byte 106,byte 106,byte  89,byte  89,byte 118,byte 105,byte  88,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 103,byte 103,byte 119,
+	byte 119,byte 102,byte 102,byte 118,byte 118,byte 101,byte 101,byte  67,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 104,byte 104,byte 103,byte 103,byte 103,byte 103,byte  86,byte 102,
+	byte 102,byte 102,byte  85,byte  85,byte 101,byte 101,byte  84,byte  67,
+	byte  87,byte  87,byte 116,byte 116,byte 116,byte  86,byte  86,byte 115,
+	byte 115,byte 115,byte  85,byte  85,byte 114,byte 114,byte  84,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte  98,byte 114,byte 114,byte 114,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  99,byte  99,byte 115,
+	byte  98,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte  79,
+	byte  83,byte  83,byte  99,byte  99,byte  82,byte  82,byte  82,byte  98,
+	byte  98,byte  81,byte  81,byte  81,byte  97,byte  97,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  95,byte  95,byte 111,byte 111,byte  94,byte  94,byte  94,byte  77,
+	byte 110,byte 110,byte  93,byte  93,byte  76,byte 109,byte  92,byte  75,
+	byte  95,byte  95,byte 111,byte 111,byte  94,byte  94,byte  94,byte  77,
+	byte 110,byte 110,byte  93,byte  93,byte  76,byte 109,byte  92,byte  75,
+	byte 108,byte 108,byte 124,byte 111,byte 107,byte  94,byte  94,byte 123,
+	byte 123,byte 106,byte  93,byte  93,byte 122,byte 105,byte  92,byte  75,
+	byte 108,byte 108,byte 108,byte 107,byte 107,byte 107,byte  90,byte 123,
+	byte 106,byte 106,byte 106,byte  89,byte 122,byte 105,byte  88,byte  75,
+	byte  91,byte  91,byte 107,byte 107,byte 107,byte  90,byte  90,byte 123,
+	byte 106,byte 106,byte  89,byte  89,byte 105,byte 105,byte  88,byte  71,
+	byte  91,byte  91,byte  91,byte 107,byte  90,byte  90,byte  90,byte  73,
+	byte 106,byte 106,byte  89,byte  89,byte  72,byte  88,byte  88,byte  71,
+	byte  91,byte  91,byte  91,byte  90,byte  90,byte  90,byte  73,byte  73,
+	byte 106,byte  89,byte  89,byte  72,byte  72,byte  88,byte  88,byte  71,
+	byte  74,byte  74,byte 120,byte 120,byte 120,byte  73,byte  73,byte 119,
+	byte 119,byte 102,byte  89,byte  72,byte  72,byte 101,byte 101,byte  71,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 104,byte 104,byte 103,byte 103,byte 103,byte 103,byte  86,byte 102,
+	byte 102,byte 102,byte  85,byte  85,byte 101,byte 101,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte 103,byte  86,byte  86,byte  86,byte  86,
+	byte 102,byte  85,byte  85,byte  85,byte  85,byte  84,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte  86,byte  86,byte  86,byte  69,byte  69,
+	byte 115,byte  85,byte  85,byte  85,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte 116,byte 116,byte  99,byte  69,byte  69,byte  69,
+	byte 115,byte  98,byte  85,byte  68,byte  68,byte  97,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  82,byte  82,byte  98,
+	byte  98,byte  98,byte  81,byte  68,byte  97,byte  97,byte  97,byte  79,
+	byte  83,byte  83,byte  83,byte  82,byte  82,byte  82,byte  82,byte  98,
+	byte  81,byte  81,byte  81,byte  64,byte  97,byte  97,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  78,byte  78,byte  49,byte  49,byte  94,byte  77,byte  77,byte  48,
+	byte  48,byte  93,byte  93,byte  76,byte  76,byte  63,byte  92,byte  75,
+	byte  78,byte  78,byte  49,byte  49,byte  94,byte  77,byte  77,byte  48,
+	byte  48,byte  93,byte  93,byte  76,byte  76,byte  63,byte  92,byte  75,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  77,byte  60,
+	byte  60,byte  60,byte  93,byte  76,byte  59,byte  59,byte  59,byte  75,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  90,byte  60,
+	byte  60,byte  60,byte  89,byte  59,byte  59,byte  59,byte  88,byte  75,
+	byte  91,byte  91,byte  61,byte  61,byte  61,byte  90,byte  73,byte  60,
+	byte  60,byte  60,byte  89,byte  72,byte  59,byte  59,byte  88,byte  71,
+	byte  74,byte  74,byte  61,byte  61,byte  90,byte  73,byte  73,byte  73,
+	byte  60,byte  89,byte  89,byte  72,byte  72,byte  72,byte  71,byte  71,
+	byte  74,byte  74,byte  74,byte  90,byte  73,byte  73,byte  73,byte  73,
+	byte  56,byte  89,byte  72,byte  72,byte  72,byte  72,byte  71,byte  71,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  73,byte  73,byte  56,
+	byte  56,byte  56,byte  72,byte  72,byte  55,byte  55,byte  55,byte  71,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  56,byte  55,byte  55,byte  55,byte  55,byte  67,
+	byte  87,byte  87,byte  57,byte  57,byte  57,byte  86,byte  86,byte  56,
+	byte  56,byte  56,byte  85,byte  85,byte  55,byte  55,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte  86,byte  86,byte  86,byte  69,byte  69,
+	byte  56,byte  85,byte  85,byte  85,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte  70,byte  53,byte  69,byte  69,byte  69,byte  69,
+	byte  52,byte  85,byte  85,byte  68,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  68,byte  51,byte  51,byte  79,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  80,byte  79,
+	byte  83,byte  83,byte  53,byte  82,byte  82,byte  65,byte  65,byte  52,
+	byte  52,byte  81,byte  64,byte  64,byte  51,byte  80,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  50,byte  50,byte  49,byte  49,byte  49,byte  77,byte  77,byte  48,
+	byte  48,byte  48,byte  76,byte  76,byte  63,byte  63,byte  46,byte  12,
+	byte  50,byte  50,byte  49,byte  49,byte  49,byte  77,byte  77,byte  48,
+	byte  48,byte  48,byte  76,byte  76,byte  63,byte  63,byte  46,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  77,byte  60,
+	byte  60,byte  60,byte  60,byte  59,byte  59,byte  59,byte  59,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  60,byte  60,
+	byte  60,byte  60,byte  60,byte  59,byte  59,byte  59,byte  42,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  73,byte  60,
+	byte  60,byte  60,byte  43,byte  59,byte  59,byte  59,byte  42,byte   8,
+	byte  74,byte  74,byte  61,byte  61,byte  61,byte  73,byte  73,byte  60,
+	byte  60,byte  60,byte  72,byte  72,byte  72,byte  59,byte  42,byte   8,
+	byte  74,byte  74,byte  74,byte  57,byte  73,byte  73,byte  73,byte  73,
+	byte  56,byte  56,byte  72,byte  72,byte  72,byte  72,byte  42,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  73,byte  56,
+	byte  56,byte  56,byte  72,byte  55,byte  55,byte  55,byte  55,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  56,byte  55,byte  55,byte  55,byte  38,byte   4,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  39,byte  55,byte  55,byte  55,byte  38,byte   4,
+	byte  70,byte  70,byte  57,byte  57,byte  40,byte  69,byte  69,byte  69,
+	byte  56,byte  39,byte  85,byte  68,byte  68,byte  38,byte  38,byte   4,
+	byte  70,byte  70,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  68,byte  51,byte  51,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  51,byte   0,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  53,byte  69,byte  52,
+	byte  52,byte  52,byte  35,byte  51,byte  51,byte  51,byte  34,byte   0,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  33,byte  33,byte  49,byte  49,byte  32,byte  32,byte  77,byte  48,
+	byte  48,byte  47,byte  47,byte  63,byte  63,byte  46,byte  46,byte  12,
+	byte  33,byte  33,byte  49,byte  49,byte  32,byte  32,byte  77,byte  48,
+	byte  48,byte  47,byte  47,byte  63,byte  63,byte  46,byte  46,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  60,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  59,byte  42,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  44,byte  44,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  42,byte  42,byte  12,
+	byte  45,byte  45,byte  61,byte  61,byte  44,byte  44,byte  44,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  42,byte  42,byte   8,
+	byte  45,byte  45,byte  61,byte  44,byte  44,byte  44,byte  73,byte  60,
+	byte  43,byte  43,byte  26,byte  72,byte  59,byte  42,byte  42,byte   8,
+	byte  74,byte  74,byte  57,byte  44,byte  44,byte  73,byte  73,byte  56,
+	byte  43,byte  43,byte  26,byte  72,byte  72,byte  42,byte  42,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  39,byte  55,byte  55,byte  55,byte  38,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  40,byte  40,byte  56,
+	byte  56,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   4,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  40,byte  40,byte  56,
+	byte  39,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   4,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  23,byte  23,byte  39,
+	byte  39,byte  39,byte  22,byte  68,byte  38,byte  38,byte  38,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  21,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  53,byte  69,byte  52,
+	byte  52,byte  52,byte  35,byte  51,byte  51,byte  51,byte  34,byte   0,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte  37,byte  37,byte  36,byte  36,byte  36,byte  36,byte  36,byte  35,
+	byte  35,byte  35,byte  35,byte  18,byte  34,byte  34,byte  34,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  16,byte  16,byte  32,byte  32,byte  31,byte  31,byte  31,byte  47,
+	byte  47,byte  30,byte  30,byte  30,byte  46,byte  46,byte  29,byte  12,
+	byte  16,byte  16,byte  32,byte  32,byte  31,byte  31,byte  31,byte  47,
+	byte  47,byte  30,byte  30,byte  30,byte  46,byte  46,byte  29,byte  12,
+	byte  45,byte  45,byte  44,byte  44,byte  44,byte  44,byte  31,byte  60,
+	byte  43,byte  43,byte  30,byte  59,byte  59,byte  42,byte  42,byte  12,
+	byte  45,byte  45,byte  44,byte  44,byte  44,byte  44,byte  27,byte  43,
+	byte  43,byte  43,byte  26,byte  26,byte  42,byte  42,byte  42,byte  12,
+	byte  28,byte  28,byte  44,byte  44,byte  27,byte  27,byte  27,byte  43,
+	byte  43,byte  26,byte  26,byte  26,byte  42,byte  42,byte  25,byte   8,
+	byte  28,byte  28,byte  44,byte  44,byte  27,byte  27,byte  27,byte  43,
+	byte  43,byte  26,byte  26,byte   9,byte  42,byte  42,byte  25,byte   8,
+	byte  28,byte  28,byte  28,byte  27,byte  27,byte  27,byte  10,byte  43,
+	byte  26,byte  26,byte  26,byte   9,byte  42,byte  42,byte  25,byte   8,
+	byte  41,byte  41,byte  57,byte  40,byte  40,byte  40,byte  40,byte  56,
+	byte  39,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   8,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  40,byte  23,byte  39,
+	byte  39,byte  39,byte  22,byte  55,byte  38,byte  38,byte  38,byte   4,
+	byte  24,byte  24,byte  40,byte  40,byte  23,byte  23,byte  23,byte  39,
+	byte  39,byte  22,byte  22,byte  22,byte  38,byte  38,byte  21,byte   4,
+	byte  24,byte  24,byte  24,byte  23,byte  23,byte  23,byte  23,byte  39,
+	byte  22,byte  22,byte  22,byte   5,byte  38,byte  38,byte  21,byte   4,
+	byte  24,byte  24,byte  53,byte  23,byte  23,byte   6,byte   6,byte  52,
+	byte  52,byte  22,byte   5,byte   5,byte  51,byte  21,byte  21,byte   4,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte  37,byte  37,byte  36,byte  36,byte  36,byte  36,byte  36,byte  35,
+	byte  35,byte  35,byte  35,byte  18,byte  34,byte  34,byte  34,byte   0,
+	byte  20,byte  20,byte  36,byte  36,byte  19,byte  19,byte  19,byte  35,
+	byte  35,byte  18,byte  18,byte  18,byte  34,byte  34,byte  17,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0
+};
+
+rgbvmap_r := array[256] of {
+	16rff, 16rff, 16rff, 16rff, 16rff, 16rff, 16rff, 16rff, 
+	16rff, 16rff, 16rff, 16rff, 16rff, 16rff, 16rff, 16rff, 
+	16ree, 16ree, 16ree, 16ree, 16ree, 16ree, 16ree, 16ree, 
+	16ree, 16ree, 16ree, 16ree, 16ree, 16ree, 16ree, 16ree, 
+	16rdd, 16rdd, 16rdd, 16rdd, 16rdd, 16rdd, 16rdd, 16rdd, 
+	16rdd, 16rdd, 16rdd, 16rdd, 16rdd, 16rdd, 16rdd, 16rdd, 
+	16rcc, 16rcc, 16rcc, 16rcc, 16rcc, 16rcc, 16rcc, 16rcc, 
+	16rcc, 16rcc, 16rcc, 16rcc, 16rcc, 16rcc, 16rcc, 16rcc, 
+	16raa, 16raa, 16raa, 16raa, 16rbb, 16rbb, 16rbb, 16raa, 
+	16rbb, 16rbb, 16rbb, 16raa, 16rbb, 16rbb, 16rbb, 16raa, 
+	16r9e, 16r9e, 16r9e, 16r9e, 16r9e, 16raa, 16raa, 16raa, 
+	16r9e, 16raa, 16raa, 16raa, 16r9e, 16raa, 16raa, 16raa, 
+	16r99, 16r93, 16r93, 16r93, 16r93, 16r93, 16r99, 16r99, 
+	16r99, 16r93, 16r99, 16r99, 16r99, 16r93, 16r99, 16r99, 
+	16r88, 16r88, 16r88, 16r88, 16r88, 16r88, 16r88, 16r88, 
+	16r88, 16r88, 16r88, 16r88, 16r88, 16r88, 16r88, 16r88, 
+	16r55, 16r55, 16r55, 16r5d, 16r5d, 16r5d, 16r55, 16r5d, 
+	16r77, 16r77, 16r55, 16r5d, 16r77, 16r77, 16r55, 16r55, 
+	16r4f, 16r4f, 16r4f, 16r4f, 16r55, 16r55, 16r55, 16r4f, 
+	16r55, 16r66, 16r66, 16r4f, 16r55, 16r66, 16r66, 16r4f, 
+	16r49, 16r49, 16r49, 16r49, 16r49, 16r4c, 16r4c, 16r4c, 
+	16r49, 16r4c, 16r55, 16r55, 16r49, 16r4c, 16r55, 16r55, 
+	16r44, 16r44, 16r44, 16r44, 16r44, 16r44, 16r44, 16r44, 
+	16r44, 16r44, 16r44, 16r44, 16r44, 16r44, 16r44, 16r44, 
+	16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 
+	16r00, 16r00, 16r00, 16r00, 16r33, 16r00, 16r00, 16r00, 
+	16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 
+	16r00, 16r00, 16r00, 16r00, 16r00, 16r22, 16r00, 16r00, 
+	16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 
+	16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r11, 16r00, 
+	16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 
+	16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00, 16r00
+};
+
+rgbvmap_g := array[256] of {
+	16rff, 16rff, 16rff, 16rff, 16raa, 16raa, 16raa, 16raa, 
+	16r55, 16r55, 16r55, 16r55, 16r00, 16r00, 16r00, 16r00, 
+	16r00, 16ree, 16ree, 16ree, 16ree, 16r9e, 16r9e, 16r9e, 
+	16r9e, 16r4f, 16r4f, 16r4f, 16r4f, 16r00, 16r00, 16r00, 
+	16r00, 16r00, 16rdd, 16rdd, 16rdd, 16rdd, 16r93, 16r93, 
+	16r93, 16r93, 16r49, 16r49, 16r49, 16r49, 16r00, 16r00, 
+	16r00, 16r00, 16r00, 16rcc, 16rcc, 16rcc, 16rcc, 16r88, 
+	16r88, 16r88, 16r88, 16r44, 16r44, 16r44, 16r44, 16r00, 
+	16rff, 16rff, 16rff, 16raa, 16rbb, 16rbb, 16rbb, 16r55, 
+	16r5d, 16r5d, 16r5d, 16r00, 16r00, 16r00, 16r00, 16rff, 
+	16ree, 16ree, 16ree, 16ree, 16r9e, 16raa, 16raa, 16raa, 
+	16r4f, 16r55, 16r55, 16r55, 16r00, 16r00, 16r00, 16r00, 
+	16r00, 16rdd, 16rdd, 16rdd, 16rdd, 16r93, 16r99, 16r99, 
+	16r99, 16r49, 16r4c, 16r4c, 16r4c, 16r00, 16r00, 16r00, 
+	16r00, 16r00, 16rcc, 16rcc, 16rcc, 16rcc, 16r88, 16r88, 
+	16r88, 16r88, 16r44, 16r44, 16r44, 16r44, 16r00, 16r00, 
+	16rff, 16rff, 16raa, 16rbb, 16rbb, 16rbb, 16r55, 16r5d, 
+	16r77, 16r77, 16r00, 16r00, 16r00, 16r00, 16rff, 16rff, 
+	16ree, 16ree, 16ree, 16r9e, 16raa, 16raa, 16raa, 16r4f, 
+	16r55, 16r66, 16r66, 16r00, 16r00, 16r00, 16r00, 16ree, 
+	16rdd, 16rdd, 16rdd, 16rdd, 16r93, 16r99, 16r99, 16r99, 
+	16r49, 16r4c, 16r55, 16r55, 16r00, 16r00, 16r00, 16r00, 
+	16r00, 16rcc, 16rcc, 16rcc, 16rcc, 16r88, 16r88, 16r88, 
+	16r88, 16r44, 16r44, 16r44, 16r44, 16r00, 16r00, 16r00, 
+	16rff, 16raa, 16rbb, 16rbb, 16rbb, 16r55, 16r5d, 16r77, 
+	16r77, 16r00, 16r00, 16r00, 16r33, 16rff, 16rff, 16rff, 
+	16ree, 16ree, 16r9e, 16raa, 16raa, 16raa, 16r4f, 16r55, 
+	16r66, 16r66, 16r00, 16r00, 16r00, 16r22, 16ree, 16ree, 
+	16rdd, 16rdd, 16rdd, 16r93, 16r99, 16r99, 16r99, 16r49, 
+	16r4c, 16r55, 16r55, 16r00, 16r00, 16r00, 16r11, 16rdd, 
+	16rcc, 16rcc, 16rcc, 16rcc, 16r88, 16r88, 16r88, 16r88, 
+	16r44, 16r44, 16r44, 16r44, 16r00, 16r00, 16r00, 16r00
+};
+
+rgbvmap_b := array[256] of {
+	16rff, 16raa, 16r55, 16r00, 16rff, 16raa, 16r55, 16r00, 
+	16rff, 16raa, 16r55, 16r00, 16rff, 16raa, 16r55, 16r00, 
+	16r00, 16ree, 16r9e, 16r4f, 16r00, 16ree, 16r9e, 16r4f, 
+	16r00, 16ree, 16r9e, 16r4f, 16r00, 16ree, 16r9e, 16r4f, 
+	16r49, 16r00, 16rdd, 16r93, 16r49, 16r00, 16rdd, 16r93, 
+	16r49, 16r00, 16rdd, 16r93, 16r49, 16r00, 16rdd, 16r93, 
+	16r88, 16r44, 16r00, 16rcc, 16r88, 16r44, 16r00, 16rcc, 
+	16r88, 16r44, 16r00, 16rcc, 16r88, 16r44, 16r00, 16rcc, 
+	16raa, 16r55, 16r00, 16rff, 16rbb, 16r5d, 16r00, 16rff, 
+	16rbb, 16r5d, 16r00, 16rff, 16rbb, 16r5d, 16r00, 16rff, 
+	16ree, 16r9e, 16r4f, 16r00, 16ree, 16raa, 16r55, 16r00, 
+	16ree, 16raa, 16r55, 16r00, 16ree, 16raa, 16r55, 16r00, 
+	16r00, 16rdd, 16r93, 16r49, 16r00, 16rdd, 16r99, 16r4c, 
+	16r00, 16rdd, 16r99, 16r4c, 16r00, 16rdd, 16r99, 16r4c, 
+	16r44, 16r00, 16rcc, 16r88, 16r44, 16r00, 16rcc, 16r88, 
+	16r44, 16r00, 16rcc, 16r88, 16r44, 16r00, 16rcc, 16r88, 
+	16r55, 16r00, 16rff, 16rbb, 16r5d, 16r00, 16rff, 16rbb, 
+	16r77, 16r00, 16rff, 16rbb, 16r77, 16r00, 16rff, 16raa, 
+	16r9e, 16r4f, 16r00, 16ree, 16raa, 16r55, 16r00, 16ree, 
+	16raa, 16r66, 16r00, 16ree, 16raa, 16r66, 16r00, 16ree, 
+	16rdd, 16r93, 16r49, 16r00, 16rdd, 16r99, 16r4c, 16r00, 
+	16rdd, 16r99, 16r55, 16r00, 16rdd, 16r99, 16r55, 16r00, 
+	16r00, 16rcc, 16r88, 16r44, 16r00, 16rcc, 16r88, 16r44, 
+	16r00, 16rcc, 16r88, 16r44, 16r00, 16rcc, 16r88, 16r44, 
+	16r00, 16rff, 16rbb, 16r5d, 16r00, 16rff, 16rbb, 16r77, 
+	16r00, 16rff, 16rbb, 16r77, 16r33, 16rff, 16raa, 16r55, 
+	16r4f, 16r00, 16ree, 16raa, 16r55, 16r00, 16ree, 16raa, 
+	16r66, 16r00, 16ree, 16raa, 16r66, 16r22, 16ree, 16r9e, 
+	16r93, 16r49, 16r00, 16rdd, 16r99, 16r4c, 16r00, 16rdd, 
+	16r99, 16r55, 16r00, 16rdd, 16r99, 16r55, 16r11, 16rdd, 
+	16rcc, 16r88, 16r44, 16r00, 16rcc, 16r88, 16r44, 16r00, 
+	16rcc, 16r88, 16r44, 16r00, 16rcc, 16r88, 16r44, 16r00
+};
--- /dev/null
+++ b/appl/charon/script.m
@@ -1,0 +1,14 @@
+Script: module
+{
+	JSCRIPTPATH: con "/dis/charon/jscript.dis";
+
+	defaultStatus: string;
+	jevchan: chan of ref Events->ScriptEvent;
+	versions : array of string;
+
+	init: fn(cu: CharonUtils): string;
+	frametreechanged: fn(top: ref Layout->Frame);
+	havenewdoc: fn(f: ref Layout->Frame);
+	evalscript: fn(f: ref Layout->Frame, s: string) : (string, string, string);
+	framedone: fn(f : ref Layout->Frame, hasscripts : int);
+};
--- /dev/null
+++ b/appl/charon/transport.m
@@ -1,0 +1,13 @@
+Transport: module
+{
+	HTTPPATH: con "/dis/charon/http.dis";
+	FTPPATH: con "/dis/charon/ftp.dis";
+	FILEPATH: con "/dis/charon/file.dis";
+
+	init:		fn(cu: CharonUtils);
+	connect:	fn(nc: ref CharonUtils->Netconn, bs: ref CharonUtils->ByteSource);
+	writereq:	fn(nc: ref CharonUtils->Netconn, bs: ref CharonUtils->ByteSource);
+	gethdr:	fn(nc: ref CharonUtils->Netconn, bs: ref CharonUtils->ByteSource);
+	getdata:	fn(nc: ref CharonUtils->Netconn, bs: ref CharonUtils->ByteSource): int;
+	defaultport:	fn(scheme: string) : int;
+};
--- /dev/null
+++ b/appl/charon/url.b
@@ -1,0 +1,225 @@
+implement Url;
+
+include "sys.m";
+include "string.m";
+include "url.m";
+
+dbg: con 0;
+
+sys: Sys;
+S: String;
+schemechars : array of byte;
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	S = load String String->PATH;
+	if (S == nil)
+		return sys->sprint("cannot load %s: %r", String->PATH);
+
+	schemechars = array [128] of { * => byte 0 };
+	alphabet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-.";
+	for (i := 0; i < len alphabet; i++)
+		schemechars[alphabet[i]] = byte 1;
+	return nil;
+}
+
+# To allow relative urls, only fill in specified pieces (don't apply defaults)
+#  general syntax: <scheme>:<scheme-specific>
+#  for IP schemes, <scheme-specific> is
+#      //<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
+#
+parse(url: string): ref Parsedurl
+{
+	if (dbg)
+		sys->print("URL parse: [%s]\n", url);
+	scheme, user, passwd, host, port, path, params, query, frag : string;
+	gotscheme := 0;
+	for (i := 0; i < len url; i++) {
+		c := url[i];
+		if (c == ':') {
+			gotscheme = 1;
+			break;
+		}
+		if (c < 0 || c > len schemechars || schemechars[c] == byte 0)
+			break;
+	}
+	if (gotscheme) {
+		if (i > 0)
+			scheme = S->tolower(url[0:i]);
+		if (i+1 < len url)
+			url = url[i+1:];
+		else
+			url = nil;
+	}
+
+	if (scheme != nil && !relscheme(scheme))
+		path = url;
+	else {
+		if(!S->prefix("//", url))
+			path = url;
+		else {
+			netloc: string;
+			(netloc, path) = S->splitl(url[2:], "/");
+			if(scheme == "file")
+				host = netloc;
+			else {
+				(up,hp) := split(netloc, "@");
+				if(hp == "")
+					hp = up;
+				else
+					(user, passwd) = split(up, ":");
+				(host, port) = split(hp, ":");
+			}
+		}
+		if(scheme == "file") {
+			if(host == "")
+				host = "localhost";
+		} else {
+			if (path == nil)
+				path = "/";
+			else {
+				(path, frag) = split(path, "#");
+				(path, query) = split(path, "?");
+				(path, params) = split(path, ";");
+			}
+		}
+	}
+	return ref Parsedurl(scheme, user, passwd, host, port, path, params, query, frag);
+}
+
+relscheme(s: string): int
+{
+	# schemes we know to be suitable as "Relative Uniform Resource Locators"
+	# as defined in RFC1808 (+ others)
+	return (s=="http" || s=="https" || s=="file" || s=="ftp" || s=="nntp");
+}
+
+Parsedurl.tostring(u: self ref Parsedurl): string
+{
+	return tostring(u);
+}
+
+tostring(u: ref Parsedurl) : string
+{
+	if (u == nil)
+		return "";
+
+	ans := "";
+	if (u.scheme != nil)
+		ans = u.scheme + ":";
+	if(u.host != "") {
+		ans = ans + "//";
+		if(u.user != "") {
+			ans = ans + u.user;
+			if(u.passwd != "")
+				ans = ans + ":" + u.passwd;
+			ans = ans + "@";
+		}
+		ans = ans + u.host;
+		if(u.port != "")
+			ans = ans + ":" + u.port;
+	}
+	ans = ans + u.path;
+	if(u.params != "")
+		ans = ans + ";" + u.params;
+	if(u.query != "")
+		ans = ans + "?" + u.query;
+	if(u.frag != "")
+		ans = ans + "#" + u.frag;
+	return ans;
+}
+
+mkabs(u, b: ref Parsedurl): ref Parsedurl
+{
+	if (dbg)
+		sys->print("URL mkabs [%s] [%s]\n", tostring(u), tostring(b));
+	if (tostring(b) == "")
+		return u;
+	if (tostring(u) == "")
+		return b;
+
+	if (u.scheme != nil && !relscheme(u.scheme))
+		return u;
+
+	if (u.scheme == nil) {
+		if (b.scheme == nil)
+			# try http
+			u.scheme = "http";
+		else {
+			if (!relscheme(b.scheme))
+				return nil;
+			u.scheme = b.scheme;
+		}
+	}
+
+	r := ref *u;
+	if (r.host == nil) {
+		r.user = b.user;
+		r.passwd = b.passwd;
+		r.host = b.host;
+		r.port = b.port;
+		if (r.path == nil || r.path[0] != '/') {
+			if (r.path == nil) {
+				r.path = b.path;
+				if (r.params == nil) {
+					r.params = b.params;
+					if (r.query == nil)
+						r.query = b.query;
+				}
+			} else {
+				(p1,nil) := S->splitr(b.path, "/");
+				r.path = canonize(p1 + r.path);
+			}
+		}
+	}
+	r.path = canonize(r.path);
+	if (dbg)
+		sys->print("URL mkabs returns [%s]\n", tostring(r));
+	return r;
+}
+
+# Like splitl, but assume one char match, and omit that from second part.
+# If c doesn't appear in s, the return is (s, "").
+split(s, c: string) : (string, string)
+{
+	(a,b) := S->splitl(s, c);
+	if(b != "")
+		b = b[1:];
+	return (a,b);
+}
+
+# remove ./ and ../ from s
+canonize(s: string): string
+{
+	ans := "";
+	(nil, file) := S->splitr(s, "/");
+	if (file == nil || file == "." | file == "..")
+		ans = "/";
+
+	(nil,path) := sys->tokenize(s, "/");
+	revpath : list of string = nil;
+	for(p := path; p != nil; p = tl p) {
+		seg := hd p;
+		if(seg == "..") {
+			if (revpath != nil)
+				revpath = tl revpath;
+		} else if(seg != ".")
+			revpath = seg :: revpath;
+	}
+	while(revpath != nil && hd revpath == "..")
+		revpath = tl revpath;
+	if(revpath != nil) {
+		ans ="/" +  (hd revpath) + ans;
+		revpath = tl revpath;
+		while(revpath != nil) {
+			ans = "/" + (hd revpath) + ans;
+			revpath = tl revpath;
+		}
+	}
+	return ans;
+}
+
+
+
+
--- /dev/null
+++ b/appl/charon/url.m
@@ -1,0 +1,30 @@
+Url: module
+{
+	PATH : con "/dis/charon/url.dis";
+
+	# "Common Internet Scheme" url syntax (rfc 1808)
+	#
+	#    <scheme>://<user>:<passwd>@<host>:<port>/<path>;<params>?<query>#<fragment>
+	#
+	# relative urls might omit some prefix of the above
+	# the path of absolute urls include the leading '/'
+	Parsedurl: adt
+	{
+		scheme:	string;
+		user:		string;
+		passwd:	string;
+		host:		string;
+		port:		string;
+		path:	string;
+		params:	string;
+		query:	string;
+		frag:		string;
+
+		tostring: fn(u: self ref Parsedurl): string;
+	};
+
+	init: fn(): string;	# call before anything else
+	parse: fn(url: string): ref Parsedurl;
+	mkabs: fn(u, base: ref Parsedurl): ref Parsedurl;
+};
+
--- /dev/null
+++ b/appl/charon/xxx.inc
@@ -1,0 +1,75 @@
+Cr2r := array [256] of {
+	-179, -178, -177, -175, -174, -172, -171, -170, -168, -167, -165, -164, -163, -161, -160, -158,
+	-157, -156, -154, -153, -151, -150, -149, -147, -146, -144, -143, -142, -140, -139, -137, -136,
+	-135, -133, -132, -130, -129, -128, -126, -125, -123, -122, -121, -119, -118, -116, -115, -114,
+	-112, -111, -109, -108, -107, -105, -104, -102, -101, -100, -98, -97, -95, -94, -93, -91,
+	-90, -88, -87, -86, -84, -83, -81, -80, -79, -77, -76, -74, -73, -72, -70, -69,
+	-67, -66, -64, -63, -62, -60, -59, -57, -56, -55, -53, -52, -50, -49, -48, -46,
+	-45, -43, -42, -41, -39, -38, -36, -35, -34, -32, -31, -29, -28, -27, -25, -24,
+	-22, -21, -20, -18, -17, -15, -14, -13, -11, -10, -8, -7, -6, -4, -3, -1,
+	0, 1, 3, 4, 6, 7, 8, 10, 11, 13, 14, 15, 17, 18, 20, 21,
+	22, 24, 25, 27, 28, 29, 31, 32, 34, 35, 36, 38, 39, 41, 42, 43,
+	45, 46, 48, 49, 50, 52, 53, 55, 56, 57, 59, 60, 62, 63, 64, 66,
+	67, 69, 70, 72, 73, 74, 76, 77, 79, 80, 81, 83, 84, 86, 87, 88,
+	90, 91, 93, 94, 95, 97, 98, 100, 101, 102, 104, 105, 107, 108, 109, 111,
+	112, 114, 115, 116, 118, 119, 121, 122, 123, 125, 126, 128, 129, 130, 132, 133,
+	135, 136, 137, 139, 140, 142, 143, 144, 146, 147, 149, 150, 151, 153, 154, 156,
+	157, 158, 160, 161, 163, 164, 165, 167, 168, 170, 171, 172, 174, 175, 177, 178,
+};
+
+Cr2g := array [256] of {
+	-91, -91, -90, -89, -89, -88, -87, -86, -86, -85, -84, -84, -83, -82, -81, -81,
+	-80, -79, -79, -78, -77, -76, -76, -75, -74, -74, -73, -72, -71, -71, -70, -69,
+	-69, -68, -67, -66, -66, -65, -64, -64, -63, -62, -61, -61, -60, -59, -59, -58,
+	-57, -56, -56, -55, -54, -54, -53, -52, -51, -51, -50, -49, -49, -48, -47, -46,
+	-46, -45, -44, -44, -43, -42, -41, -41, -40, -39, -39, -38, -37, -36, -36, -35,
+	-34, -34, -33, -32, -31, -31, -30, -29, -29, -28, -27, -26, -26, -25, -24, -24,
+	-23, -22, -21, -21, -20, -19, -19, -18, -17, -16, -16, -15, -14, -14, -13, -12,
+	-11, -11, -10, -9, -9, -8, -7, -6, -6, -5, -4, -4, -3, -2, -1, -1,
+	0, 1, 1, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9, 9, 10, 11,
+	11, 12, 13, 14, 14, 15, 16, 16, 17, 18, 19, 19, 20, 21, 21, 22,
+	23, 24, 24, 25, 26, 26, 27, 28, 29, 29, 30, 31, 31, 32, 33, 34,
+	34, 35, 36, 36, 37, 38, 39, 39, 40, 41, 41, 42, 43, 44, 44, 45,
+	46, 46, 47, 48, 49, 49, 50, 51, 51, 52, 53, 54, 54, 55, 56, 56,
+	57, 58, 59, 59, 60, 61, 61, 62, 63, 64, 64, 65, 66, 66, 67, 68,
+	69, 69, 70, 71, 71, 72, 73, 74, 74, 75, 76, 76, 77, 78, 79, 79,
+	80, 81, 81, 82, 83, 84, 84, 85, 86, 86, 87, 88, 89, 89, 90, 91,
+};
+
+Cb2g := array [256] of {
+	-44, -44, -43, -43, -43, -42, -42, -42, -41, -41, -41, -40, -40, -40, -39, -39,
+	-39, -38, -38, -38, -37, -37, -36, -36, -36, -35, -35, -35, -34, -34, -34, -33,
+	-33, -33, -32, -32, -32, -31, -31, -31, -30, -30, -30, -29, -29, -29, -28, -28,
+	-28, -27, -27, -26, -26, -26, -25, -25, -25, -24, -24, -24, -23, -23, -23, -22,
+	-22, -22, -21, -21, -21, -20, -20, -20, -19, -19, -19, -18, -18, -18, -17, -17,
+	-17, -16, -16, -15, -15, -15, -14, -14, -14, -13, -13, -13, -12, -12, -12, -11,
+	-11, -11, -10, -10, -10, -9, -9, -9, -8, -8, -8, -7, -7, -7, -6, -6,
+	-6, -5, -5, -4, -4, -4, -3, -3, -3, -2, -2, -2, -1, -1, -1, 0,
+	0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5,
+	6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11,
+	11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16,
+	17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22,
+	22, 22, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27,
+	28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 32, 32, 32, 33,
+	33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 38,
+	39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 43, 44,
+};
+
+Cb2b := array [256] of {
+	-227, -225, -223, -222, -220, -218, -216, -214, -213, -211, -209, -207, -206, -204, -202, -200,
+	-198, -197, -195, -193, -191, -190, -188, -186, -184, -183, -181, -179, -177, -175, -174, -172,
+	-170, -168, -167, -165, -163, -161, -159, -158, -156, -154, -152, -151, -149, -147, -145, -144,
+	-142, -140, -138, -136, -135, -133, -131, -129, -128, -126, -124, -122, -120, -119, -117, -115,
+	-113, -112, -110, -108, -106, -105, -103, -101, -99, -97, -96, -94, -92, -90, -89, -87,
+	-85, -83, -82, -80, -78, -76, -74, -73, -71, -69, -67, -66, -64, -62, -60, -58,
+	-57, -55, -53, -51, -50, -48, -46, -44, -43, -41, -39, -37, -35, -34, -32, -30,
+	-28, -27, -25, -23, -21, -19, -18, -16, -14, -12, -11, -9, -7, -5, -4, -2,
+	0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 18, 19, 21, 23, 25, 27,
+	28, 30, 32, 34, 35, 37, 39, 41, 43, 44, 46, 48, 50, 51, 53, 55,
+	57, 58, 60, 62, 64, 66, 67, 69, 71, 73, 74, 76, 78, 80, 82, 83,
+	85, 87, 89, 90, 92, 94, 96, 97, 99, 101, 103, 105, 106, 108, 110, 112,
+	113, 115, 117, 119, 120, 122, 124, 126, 128, 129, 131, 133, 135, 136, 138, 140,
+	142, 144, 145, 147, 149, 151, 152, 154, 156, 158, 159, 161, 163, 165, 167, 168,
+	170, 172, 174, 175, 177, 179, 181, 183, 184, 186, 188, 190, 191, 193, 195, 197,
+	198, 200, 202, 204, 206, 207, 209, 211, 213, 214, 216, 218, 220, 222, 223, 225,
+};
--- /dev/null
+++ b/appl/charon/ycbcr.inc
@@ -1,0 +1,621 @@
+# closest color in the rgbvmap, indexed by Cr+16*(Cb+16*Y)
+closestycbcr:= array[16*16*16] of {
+	byte 247,byte 200,byte 200,byte 217,byte 234,byte 234,byte 251,byte 251,
+	byte 251,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 247,byte 200,byte 217,byte 217,byte 234,byte 251,byte 251,byte 251,
+	byte 251,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 200,byte 200,byte 217,byte 234,byte 234,byte 251,byte 251,byte 251,
+	byte 255,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 200,byte 217,byte 217,byte 234,byte 251,byte 251,byte 251,byte 251,
+	byte 255,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 200,byte 217,byte 234,byte 234,byte 251,byte 251,byte 251,byte 255,
+	byte 255,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 217,byte 217,byte 234,byte 251,byte 251,byte 251,byte 251,byte 255,
+	byte 255,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 217,byte 234,byte 234,byte 251,byte 251,byte 251,byte 255,byte 255,
+	byte 255,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 217,byte 234,byte 251,byte 251,byte 251,byte 251,byte 255,byte 255,
+	byte 255,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 234,byte 234,byte 251,byte 251,byte 251,byte 251,byte 255,byte 255,
+	byte 255,byte 255,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 234,byte 251,byte 251,byte 251,byte 251,byte 255,byte 255,byte 255,
+	byte 255,byte 238,byte 176,byte 176,byte 175,byte 141,byte 113,byte  96,
+	byte 250,byte 250,byte 250,byte 250,byte 250,byte 254,byte 254,byte 254,
+	byte 254,byte 254,byte 191,byte 191,byte 191,byte 112,byte 112,byte 111,
+	byte 233,byte 250,byte 250,byte 250,byte 237,byte 237,byte 237,byte 237,
+	byte 237,byte 237,byte 191,byte 191,byte 174,byte 157,byte 112,byte 111,
+	byte 249,byte 249,byte 249,byte 203,byte 203,byte 203,byte 203,byte 203,
+	byte 203,byte 203,byte 190,byte 190,byte 157,byte 140,byte 140,byte 127,
+	byte 249,byte 249,byte 249,byte 253,byte 253,byte 253,byte 253,byte 253,
+	byte 253,byte 253,byte 190,byte 190,byte 173,byte 140,byte 127,byte 110,
+	byte 232,byte 232,byte 219,byte 219,byte 219,byte 219,byte 219,byte 219,
+	byte 219,byte 219,byte 173,byte 156,byte 156,byte 139,byte 110,byte  93,
+	byte 248,byte 248,byte 252,byte 252,byte 252,byte 252,byte 252,byte 252,
+	byte 252,byte 252,byte 189,byte 189,byte 139,byte 139,byte 126,byte 126,
+	byte 230,byte 247,byte 247,byte 200,byte 217,byte 217,byte 234,byte 251,
+	byte 251,byte 188,byte 188,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 230,byte 247,byte 200,byte 200,byte 217,byte 234,byte 234,byte 251,
+	byte 251,byte 188,byte 176,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 247,byte 247,byte 200,byte 217,byte 217,byte 234,byte 251,byte 251,
+	byte 251,byte 188,byte 176,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 247,byte 200,byte 200,byte 217,byte 234,byte 234,byte 251,byte 251,
+	byte 251,byte 176,byte 176,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 247,byte 200,byte 217,byte 217,byte 234,byte 251,byte 251,byte 251,
+	byte 251,byte 176,byte 176,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 200,byte 200,byte 217,byte 234,byte 234,byte 251,byte 251,byte 251,
+	byte 255,byte 176,byte 176,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 200,byte 217,byte 217,byte 234,byte 251,byte 251,byte 251,byte 251,
+	byte 255,byte 176,byte 176,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 200,byte 217,byte 234,byte 234,byte 251,byte 251,byte 251,byte 255,
+	byte 255,byte 176,byte 176,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 217,byte 217,byte 234,byte 251,byte 251,byte 251,byte 251,byte 238,
+	byte 238,byte 238,byte 176,byte 175,byte 158,byte 113,byte  96,byte  95,
+	byte 250,byte 250,byte 250,byte 250,byte 250,byte 250,byte 254,byte 254,
+	byte 238,byte 221,byte 191,byte 191,byte 112,byte 112,byte 112,byte 111,
+	byte 233,byte 233,byte 250,byte 250,byte 250,byte 250,byte 254,byte 254,
+	byte 254,byte 191,byte 191,byte 174,byte 174,byte 112,byte 111,byte  94,
+	byte 216,byte 233,byte 233,byte 233,byte 250,byte 220,byte 220,byte 220,
+	byte 220,byte 220,byte 174,byte 174,byte 157,byte 140,byte 111,byte  77,
+	byte 249,byte 249,byte 249,byte 249,byte 249,byte 253,byte 253,byte 253,
+	byte 253,byte 190,byte 190,byte 190,byte 140,byte 127,byte 127,byte 110,
+	byte 232,byte 232,byte 232,byte 232,byte 236,byte 236,byte 236,byte 236,
+	byte 236,byte 190,byte 173,byte 173,byte 156,byte 127,byte 110,byte  93,
+	byte 198,byte 248,byte 248,byte 248,byte 202,byte 202,byte 202,byte 202,
+	byte 202,byte 189,byte 189,byte 139,byte 139,byte 126,byte 126,byte  76,
+	byte 231,byte 248,byte 248,byte 248,byte 235,byte 235,byte 235,byte 235,
+	byte 235,byte 189,byte 189,byte 172,byte 139,byte 126,byte 109,byte 109,
+	byte 213,byte 230,byte 230,byte 247,byte 200,byte 217,byte 217,byte 234,
+	byte 251,byte 188,byte 188,byte 188,byte 141,byte 113,byte  95,byte  78,
+	byte 213,byte 230,byte 247,byte 247,byte 200,byte 217,byte 234,byte 234,
+	byte 251,byte 188,byte 188,byte 158,byte 141,byte 113,byte  95,byte  78,
+	byte 230,byte 230,byte 247,byte 200,byte 200,byte 217,byte 234,byte 251,
+	byte 251,byte 188,byte 188,byte 158,byte 141,byte 113,byte  95,byte  78,
+	byte 230,byte 247,byte 247,byte 200,byte 217,byte 217,byte 234,byte 251,
+	byte 251,byte 188,byte 188,byte 158,byte 141,byte 113,byte  95,byte  78,
+	byte 230,byte 247,byte 200,byte 200,byte 217,byte 234,byte 234,byte 251,
+	byte 251,byte 188,byte 175,byte 158,byte 141,byte 113,byte  95,byte  78,
+	byte 247,byte 247,byte 200,byte 217,byte 217,byte 234,byte 251,byte 251,
+	byte 251,byte 188,byte 175,byte 158,byte 141,byte 113,byte  95,byte  78,
+	byte 247,byte 200,byte 200,byte 217,byte 234,byte 234,byte 251,byte 251,
+	byte 251,byte 176,byte 175,byte 158,byte 141,byte 113,byte  95,byte  78,
+	byte 247,byte 200,byte 217,byte 217,byte 234,byte 251,byte 251,byte 251,
+	byte 238,byte 176,byte 175,byte 158,byte 141,byte 113,byte  95,byte  78,
+	byte 200,byte 200,byte 217,byte 234,byte 234,byte 251,byte 251,byte 221,
+	byte 221,byte 221,byte 175,byte 158,byte 141,byte 113,byte  95,byte  78,
+	byte 246,byte 246,byte 233,byte 250,byte 250,byte 250,byte 250,byte 250,
+	byte 221,byte 191,byte 191,byte 174,byte 112,byte 112,byte 111,byte  49,
+	byte 216,byte 216,byte 233,byte 233,byte 233,byte 250,byte 250,byte 237,
+	byte 237,byte 191,byte 174,byte 157,byte 157,byte 111,byte  94,byte  77,
+	byte 199,byte 216,byte 216,byte 249,byte 249,byte 249,byte 249,byte 203,
+	byte 203,byte 190,byte 190,byte 157,byte 140,byte 127,byte  77,byte  77,
+	byte 232,byte 232,byte 232,byte 249,byte 249,byte 249,byte 236,byte 236,
+	byte 236,byte 190,byte 173,byte 173,byte 127,byte 110,byte 110,byte  48,
+	byte 215,byte 215,byte 215,byte 215,byte 232,byte 219,byte 219,byte 219,
+	byte 219,byte 173,byte 156,byte 139,byte 139,byte 110,byte  93,byte  76,
+	byte 198,byte 248,byte 248,byte 248,byte 248,byte 252,byte 252,byte 252,
+	byte 252,byte 189,byte 189,byte 139,byte 126,byte 126,byte  76,byte  63,
+	byte 214,byte 214,byte 231,byte 231,byte 231,byte 218,byte 218,byte 218,
+	byte 218,byte 172,byte 155,byte 155,byte 109,byte 109,byte  92,byte  63,
+	byte 196,byte 213,byte 230,byte 230,byte 247,byte 200,byte 200,byte 217,
+	byte 188,byte 188,byte 171,byte 125,byte 125,byte  96,byte  78,byte  50,
+	byte 213,byte 213,byte 230,byte 247,byte 247,byte 200,byte 217,byte 217,
+	byte 188,byte 188,byte 171,byte 125,byte 125,byte  96,byte  78,byte  50,
+	byte 213,byte 230,byte 230,byte 247,byte 200,byte 200,byte 217,byte 234,
+	byte 188,byte 188,byte 188,byte 125,byte 125,byte  96,byte  78,byte  50,
+	byte 213,byte 230,byte 247,byte 247,byte 200,byte 217,byte 217,byte 234,
+	byte 188,byte 188,byte 188,byte 125,byte 113,byte  96,byte  78,byte  50,
+	byte 230,byte 230,byte 247,byte 200,byte 200,byte 217,byte 234,byte 234,
+	byte 188,byte 188,byte 188,byte 141,byte 113,byte  96,byte  78,byte  50,
+	byte 230,byte 230,byte 247,byte 200,byte 217,byte 217,byte 234,byte 251,
+	byte 188,byte 188,byte 188,byte 141,byte 113,byte  96,byte  78,byte  50,
+	byte 230,byte 247,byte 247,byte 200,byte 217,byte 234,byte 234,byte 251,
+	byte 188,byte 188,byte 175,byte 141,byte 113,byte  96,byte  78,byte  50,
+	byte 230,byte 247,byte 200,byte 200,byte 217,byte 234,byte 251,byte 251,
+	byte 221,byte 188,byte 175,byte 141,byte 113,byte  96,byte  78,byte  50,
+	byte 246,byte 246,byte 246,byte 246,byte 250,byte 250,byte 250,byte 204,
+	byte 204,byte 204,byte 191,byte 112,byte 112,byte 111,byte  49,byte  49,
+	byte 246,byte 246,byte 246,byte 233,byte 233,byte 233,byte 250,byte 250,
+	byte 204,byte 191,byte 174,byte 112,byte 112,byte 111,byte  94,byte  49,
+	byte 199,byte 199,byte 216,byte 216,byte 216,byte 233,byte 233,byte 249,
+	byte 186,byte 174,byte 157,byte 157,byte 140,byte  94,byte  77,byte  77,
+	byte 245,byte 199,byte 199,byte 249,byte 249,byte 249,byte 249,byte 249,
+	byte 190,byte 190,byte 190,byte 140,byte 127,byte 110,byte  48,byte  48,
+	byte 245,byte 215,byte 215,byte 215,byte 232,byte 232,byte 232,byte 236,
+	byte 173,byte 173,byte 156,byte 156,byte 110,byte 110,byte  93,byte  47,
+	byte 198,byte 198,byte 198,byte 198,byte 248,byte 248,byte 248,byte 202,
+	byte 189,byte 189,byte 139,byte 139,byte 126,byte  93,byte  76,byte  63,
+	byte 244,byte 231,byte 231,byte 231,byte 231,byte 248,byte 248,byte 235,
+	byte 189,byte 172,byte 172,byte 126,byte 109,byte 109,byte  63,byte  63,
+	byte 197,byte 197,byte 214,byte 214,byte 214,byte 214,byte 218,byte 218,
+	byte 155,byte 155,byte 138,byte 138,byte  92,byte  92,byte  75,byte  46,
+	byte 243,byte 196,byte 213,byte 213,byte 230,byte 247,byte 247,byte 184,
+	byte 184,byte 171,byte 154,byte 125,byte 108,byte  62,byte  62,byte  33,
+	byte 196,byte 196,byte 213,byte 230,byte 230,byte 247,byte 200,byte 184,
+	byte 171,byte 171,byte 154,byte 125,byte 108,byte  62,byte  50,byte  33,
+	byte 196,byte 213,byte 213,byte 230,byte 247,byte 247,byte 200,byte 184,
+	byte 171,byte 171,byte 171,byte 125,byte 125,byte  62,byte  50,byte  33,
+	byte 196,byte 213,byte 230,byte 230,byte 247,byte 200,byte 200,byte 217,
+	byte 171,byte 171,byte 125,byte 125,byte 125,byte  95,byte  50,byte  33,
+	byte 213,byte 213,byte 230,byte 247,byte 247,byte 200,byte 217,byte 188,
+	byte 188,byte 171,byte 125,byte 125,byte 125,byte  95,byte  50,byte  33,
+	byte 213,byte 230,byte 230,byte 247,byte 200,byte 200,byte 217,byte 188,
+	byte 188,byte 171,byte 125,byte 125,byte  96,byte  95,byte  50,byte  33,
+	byte 213,byte 230,byte 247,byte 247,byte 200,byte 217,byte 217,byte 188,
+	byte 188,byte 188,byte 125,byte 125,byte  96,byte  95,byte  50,byte  33,
+	byte 230,byte 246,byte 246,byte 246,byte 200,byte 217,byte 234,byte 204,
+	byte 204,byte 187,byte 124,byte 124,byte  96,byte  95,byte  49,byte  33,
+	byte 229,byte 246,byte 246,byte 246,byte 246,byte 233,byte 250,byte 187,
+	byte 187,byte 187,byte 124,byte 112,byte 111,byte  94,byte  49,byte  32,
+	byte 229,byte 229,byte 246,byte 216,byte 216,byte 233,byte 233,byte 187,
+	byte 187,byte 187,byte 157,byte 112,byte 111,byte  94,byte  77,byte  32,
+	byte 245,byte 199,byte 199,byte 199,byte 216,byte 216,byte 249,byte 186,
+	byte 186,byte 186,byte 140,byte 140,byte 127,byte  77,byte  48,byte  48,
+	byte 245,byte 245,byte 245,byte 215,byte 232,byte 232,byte 232,byte 186,
+	byte 186,byte 173,byte 127,byte 127,byte 110,byte  93,byte  48,byte  47,
+	byte 244,byte 244,byte 198,byte 198,byte 215,byte 215,byte 215,byte 185,
+	byte 185,byte 156,byte 139,byte 126,byte  93,byte  93,byte  76,byte  30,
+	byte 244,byte 244,byte 198,byte 198,byte 248,byte 248,byte 248,byte 185,
+	byte 185,byte 189,byte 139,byte 126,byte 109,byte  76,byte  63,byte  46,
+	byte 227,byte 214,byte 214,byte 214,byte 214,byte 214,byte 231,byte 168,
+	byte 155,byte 155,byte 155,byte 109,byte  92,byte  92,byte  46,byte  46,
+	byte 197,byte 197,byte 197,byte 197,byte 197,byte 197,byte 197,byte 201,
+	byte 138,byte 138,byte 138,byte  92,byte  75,byte  75,byte  75,byte  29,
+	byte 226,byte 243,byte 196,byte 196,byte 213,byte 230,byte 184,byte 184,
+	byte 184,byte 154,byte 137,byte 108,byte  91,byte  62,byte  62,byte  45,
+	byte 243,byte 243,byte 196,byte 213,byte 213,byte 230,byte 184,byte 184,
+	byte 184,byte 154,byte 154,byte 108,byte  91,byte  62,byte  62,byte  45,
+	byte 243,byte 196,byte 196,byte 213,byte 230,byte 230,byte 184,byte 184,
+	byte 154,byte 154,byte 125,byte 108,byte  91,byte  62,byte  62,byte  16,
+	byte 243,byte 196,byte 213,byte 213,byte 230,byte 247,byte 184,byte 184,
+	byte 154,byte 154,byte 125,byte 108,byte 108,byte  62,byte  62,byte  16,
+	byte 196,byte 196,byte 213,byte 230,byte 230,byte 247,byte 184,byte 184,
+	byte 171,byte 154,byte 125,byte 108,byte 108,byte  62,byte  33,byte  16,
+	byte 196,byte 213,byte 213,byte 230,byte 247,byte 247,byte 200,byte 184,
+	byte 171,byte 154,byte 125,byte 125,byte 108,byte  62,byte  33,byte  16,
+	byte 196,byte 213,byte 230,byte 230,byte 247,byte 200,byte 200,byte 171,
+	byte 171,byte 171,byte 125,byte 125,byte 108,byte  78,byte  33,byte  16,
+	byte 242,byte 229,byte 246,byte 246,byte 246,byte 246,byte 183,byte 187,
+	byte 187,byte 124,byte 124,byte 124,byte 107,byte  49,byte  49,byte  32,
+	byte 212,byte 229,byte 229,byte 246,byte 246,byte 246,byte 233,byte 187,
+	byte 170,byte 170,byte 124,byte 124,byte  94,byte  77,byte  32,byte  31,
+	byte 212,byte 212,byte 199,byte 199,byte 199,byte 216,byte 216,byte 186,
+	byte 170,byte 170,byte 123,byte 123,byte  77,byte  77,byte  48,byte  31,
+	byte 228,byte 245,byte 245,byte 245,byte 199,byte 199,byte 186,byte 186,
+	byte 186,byte 123,byte 123,byte 123,byte 110,byte  48,byte  48,byte  47,
+	byte 228,byte 228,byte 228,byte 245,byte 215,byte 215,byte 215,byte 169,
+	byte 169,byte 152,byte 123,byte 110,byte  93,byte  76,byte  47,byte  30,
+	byte 244,byte 244,byte 244,byte 198,byte 198,byte 198,byte 185,byte 185,
+	byte 185,byte 122,byte 122,byte 126,byte  76,byte  76,byte  63,byte  13,
+	byte 227,byte 227,byte 244,byte 244,byte 214,byte 231,byte 231,byte 168,
+	byte 168,byte 168,byte 126,byte 109,byte  92,byte  63,byte  46,byte  46,
+	byte 210,byte 210,byte 197,byte 197,byte 197,byte 197,byte 214,byte 151,
+	byte 151,byte 138,byte  92,byte  92,byte  75,byte  75,byte  29,byte  29,
+	byte 193,byte 197,byte 197,byte 197,byte 197,byte 197,byte 197,byte 134,
+	byte 138,byte 138,byte 138,byte  75,byte  75,byte  75,byte  12,byte  12,
+	byte 209,byte 226,byte 243,byte 243,byte 196,byte 213,byte 167,byte 167,
+	byte 167,byte 137,byte 121,byte  91,byte  74,byte  62,byte  45,byte  28,
+	byte 226,byte 226,byte 243,byte 196,byte 196,byte 213,byte 167,byte 167,
+	byte 167,byte 137,byte 137,byte  91,byte  74,byte  62,byte  45,byte  28,
+	byte 226,byte 243,byte 243,byte 196,byte 213,byte 213,byte 184,byte 184,
+	byte 137,byte 137,byte 137,byte  91,byte  74,byte  62,byte  45,byte  28,
+	byte 226,byte 243,byte 196,byte 196,byte 213,byte 230,byte 184,byte 184,
+	byte 154,byte 137,byte 108,byte  91,byte  74,byte  62,byte  45,byte  28,
+	byte 243,byte 243,byte 196,byte 213,byte 213,byte 230,byte 184,byte 184,
+	byte 154,byte 137,byte 108,byte  91,byte  62,byte  62,byte  45,byte  15,
+	byte 243,byte 196,byte 196,byte 213,byte 230,byte 230,byte 184,byte 184,
+	byte 154,byte 154,byte 108,byte  91,byte  62,byte  62,byte  45,byte  15,
+	byte 242,byte 242,byte 213,byte 229,byte 246,byte 246,byte 183,byte 183,
+	byte 154,byte 154,byte 124,byte 107,byte  61,byte  61,byte  61,byte  15,
+	byte 242,byte 212,byte 212,byte 229,byte 229,byte 246,byte 183,byte 183,
+	byte 170,byte 124,byte 124,byte 107,byte  61,byte  61,byte  32,byte  14,
+	byte 195,byte 195,byte 212,byte 229,byte 229,byte 199,byte 183,byte 170,
+	byte 153,byte 153,byte 107,byte  90,byte  61,byte  61,byte  31,byte  14,
+	byte 241,byte 228,byte 245,byte 245,byte 245,byte 199,byte 182,byte 153,
+	byte 153,byte 123,byte 123,byte 123,byte  60,byte  48,byte  47,byte  30,
+	byte 211,byte 228,byte 228,byte 228,byte 245,byte 245,byte 182,byte 169,
+	byte 152,byte 123,byte 123,byte 106,byte  60,byte  47,byte  30,byte  30,
+	byte 211,byte 211,byte 211,byte 244,byte 244,byte 198,byte 152,byte 152,
+	byte 135,byte 122,byte 122,byte  89,byte  76,byte  76,byte  30,byte  13,
+	byte 227,byte 227,byte 244,byte 244,byte 244,byte 198,byte 185,byte 168,
+	byte 168,byte 122,byte 122,byte 105,byte  63,byte  63,byte  46,byte  29,
+	byte 210,byte 210,byte 227,byte 227,byte 197,byte 214,byte 151,byte 151,
+	byte 151,byte 105,byte 105,byte  92,byte  75,byte  46,byte  29,byte  29,
+	byte 193,byte 193,byte 210,byte 197,byte 197,byte 197,byte 134,byte 134,
+	byte 134,byte 134,byte  75,byte  75,byte  75,byte  75,byte  12,byte  12,
+	byte 193,byte 193,byte 197,byte 197,byte 197,byte 197,byte 134,byte 134,
+	byte 134,byte 134,byte  75,byte  75,byte  75,byte  75,byte  12,byte  12,
+	byte 192,byte 209,byte 226,byte 226,byte 243,byte 180,byte 150,byte 150,
+	byte 150,byte 121,byte 104,byte  58,byte  74,byte  28,byte  11,byte  11,
+	byte 209,byte 209,byte 226,byte 243,byte 243,byte 180,byte 150,byte 150,
+	byte 121,byte 121,byte 121,byte  74,byte  74,byte  28,byte  11,byte  11,
+	byte 209,byte 226,byte 226,byte 243,byte 196,byte 180,byte 167,byte 167,
+	byte 121,byte 121,byte 121,byte  74,byte  74,byte  45,byte  28,byte  11,
+	byte 209,byte 226,byte 243,byte 243,byte 196,byte 167,byte 167,byte 167,
+	byte 137,byte 121,byte 121,byte  74,byte  74,byte  45,byte  28,byte  11,
+	byte 226,byte 226,byte 243,byte 196,byte 196,byte 167,byte 167,byte 167,
+	byte 137,byte 121,byte  91,byte  74,byte  74,byte  45,byte  28,byte  11,
+	byte 226,byte 243,byte 243,byte 196,byte 213,byte 167,byte 167,byte 167,
+	byte 137,byte 137,byte  91,byte  74,byte  62,byte  45,byte  28,byte  11,
+	byte 242,byte 242,byte 242,byte 242,byte 229,byte 183,byte 183,byte 183,
+	byte 120,byte 120,byte 107,byte  90,byte  61,byte  44,byte  27,byte  27,
+	byte 225,byte 195,byte 195,byte 212,byte 212,byte 183,byte 183,byte 183,
+	byte 153,byte 120,byte  90,byte  90,byte  61,byte  44,byte  27,byte  14,
+	byte 241,byte 195,byte 195,byte 195,byte 212,byte 182,byte 182,byte 153,
+	byte 136,byte 136,byte  90,byte  73,byte  60,byte  60,byte  27,byte  14,
+	byte 241,byte 241,byte 228,byte 228,byte 228,byte 182,byte 182,byte 182,
+	byte 136,byte 123,byte 106,byte  60,byte  60,byte  43,byte  30,byte  13,
+	byte 194,byte 211,byte 211,byte 211,byte 228,byte 165,byte 165,byte 135,
+	byte 135,byte 106,byte  89,byte  89,byte  43,byte  43,byte  13,byte  13,
+	byte 194,byte 194,byte 194,byte 244,byte 244,byte 181,byte 181,byte 135,
+	byte 135,byte 122,byte 122,byte  59,byte  59,byte  59,byte  13,byte  13,
+	byte 210,byte 210,byte 227,byte 227,byte 227,byte 181,byte 181,byte 151,
+	byte 151,byte 105,byte 105,byte  88,byte  59,byte  46,byte  29,byte  12,
+	byte 193,byte 193,byte 210,byte 210,byte 210,byte 164,byte 134,byte 134,
+	byte 134,byte  88,byte  88,byte  71,byte  75,byte  29,byte  12,byte  12,
+	byte 193,byte 193,byte 193,byte 193,byte 197,byte 134,byte 134,byte 134,
+	byte 134,byte  71,byte  71,byte  71,byte  75,byte  12,byte  12,byte  12,
+	byte 193,byte 193,byte 193,byte 193,byte 197,byte 134,byte 134,byte 134,
+	byte 134,byte  71,byte  71,byte  75,byte  75,byte  12,byte  12,byte  12,
+	byte 192,byte 192,byte 209,byte 226,byte 180,byte 180,byte 133,byte 133,
+	byte 104,byte 104,byte 104,byte  58,byte  58,byte  11,byte  11,byte  11,
+	byte 192,byte 192,byte 209,byte 226,byte 180,byte 180,byte 133,byte 133,
+	byte 104,byte 104,byte 104,byte  58,byte  58,byte  11,byte  11,byte  11,
+	byte 192,byte 209,byte 209,byte 226,byte 180,byte 180,byte 133,byte 133,
+	byte 104,byte 104,byte 104,byte  58,byte  58,byte  11,byte  11,byte  11,
+	byte 192,byte 209,byte 226,byte 226,byte 180,byte 180,byte 150,byte 150,
+	byte 121,byte 104,byte 104,byte  58,byte  58,byte  28,byte  11,byte  11,
+	byte 209,byte 209,byte 226,byte 243,byte 180,byte 180,byte 150,byte 150,
+	byte 121,byte 104,byte  58,byte  58,byte  45,byte  28,byte  11,byte  11,
+	byte 225,byte 225,byte 242,byte 242,byte 179,byte 179,byte 166,byte 120,
+	byte 120,byte 120,byte  57,byte  57,byte  44,byte  27,byte  10,byte  10,
+	byte 225,byte 225,byte 242,byte 242,byte 179,byte 166,byte 166,byte 166,
+	byte 120,byte 120,byte  90,byte  73,byte  44,byte  27,byte  10,byte  10,
+	byte 208,byte 195,byte 195,byte 195,byte 195,byte 149,byte 149,byte 136,
+	byte 136,byte 136,byte  73,byte  73,byte  27,byte  27,byte  10,byte  10,
+	byte 241,byte 241,byte 241,byte 241,byte 178,byte 182,byte 182,byte 136,
+	byte 119,byte 119,byte  73,byte  60,byte  60,byte  43,byte  26,byte   9,
+	byte 224,byte 241,byte 241,byte 211,byte 211,byte 165,byte 165,byte 165,
+	byte 119,byte 119,byte  89,byte  89,byte  43,byte  26,byte   9,byte   9,
+	byte 240,byte 194,byte 194,byte 194,byte 211,byte 181,byte 181,byte 118,
+	byte 118,byte  89,byte  72,byte  72,byte  59,byte   9,byte   9,byte  13,
+	byte 240,byte 240,byte 240,byte 227,byte 164,byte 164,byte 181,byte 118,
+	byte 118,byte 105,byte  72,byte  59,byte  42,byte  42,byte  25,byte  12,
+	byte 193,byte 193,byte 193,byte 210,byte 210,byte 164,byte 164,byte 134,
+	byte  88,byte  88,byte  71,byte  71,byte  42,byte  25,byte  12,byte  12,
+	byte 193,byte 193,byte 193,byte 193,byte 193,byte 147,byte 134,byte 134,
+	byte  71,byte  71,byte  71,byte  71,byte  25,byte   8,byte  12,byte  12,
+	byte 193,byte 193,byte 193,byte 193,byte 193,byte 130,byte 134,byte 134,
+	byte  71,byte  71,byte  71,byte  71,byte   8,byte  12,byte  12,byte  12,
+	byte 193,byte 193,byte 193,byte 193,byte 193,byte 134,byte 134,byte 134,
+	byte  71,byte  71,byte  71,byte  71,byte   8,byte  12,byte  12,byte  12,
+	byte 192,byte 192,byte 192,byte 209,byte 163,byte 163,byte 133,byte 117,
+	byte 117,byte  87,byte  87,byte  41,byte  41,byte  24,byte  11,byte  11,
+	byte 192,byte 192,byte 192,byte 209,byte 163,byte 163,byte 133,byte 117,
+	byte 117,byte  87,byte  87,byte  41,byte  41,byte  24,byte  11,byte  11,
+	byte 192,byte 192,byte 209,byte 209,byte 163,byte 163,byte 133,byte 117,
+	byte 104,byte  87,byte  58,byte  58,byte  41,byte  11,byte  11,byte  11,
+	byte 192,byte 192,byte 209,byte 226,byte 180,byte 180,byte 133,byte 117,
+	byte 104,byte  87,byte  58,byte  58,byte  41,byte  11,byte  11,byte  11,
+	byte 192,byte 209,byte 209,byte 226,byte 180,byte 180,byte 133,byte 133,
+	byte 104,byte 104,byte  58,byte  58,byte  41,byte  11,byte  11,byte  11,
+	byte 208,byte 208,byte 225,byte 225,byte 179,byte 179,byte 149,byte 116,
+	byte 103,byte 103,byte  57,byte  57,byte  40,byte  10,byte  10,byte  10,
+	byte 207,byte 208,byte 225,byte 225,byte 179,byte 132,byte 132,byte 132,
+	byte 103,byte 103,byte  57,byte  57,byte  27,byte  10,byte  10,byte  10,
+	byte 207,byte 224,byte 241,byte 241,byte 178,byte 132,byte 132,byte 119,
+	byte 119,byte 119,byte  56,byte  56,byte  10,byte  10,byte  10,byte  10,
+	byte 223,byte 224,byte 224,byte 241,byte 178,byte 165,byte 165,byte 119,
+	byte 119,byte 102,byte  56,byte  56,byte  26,byte  26,byte   9,byte   9,
+	byte 223,byte 223,byte 194,byte 194,byte 148,byte 148,byte 148,byte 102,
+	byte 102,byte 102,byte  72,byte  72,byte  26,byte   9,byte   9,byte   9,
+	byte 239,byte 240,byte 240,byte 194,byte 177,byte 164,byte 164,byte 118,
+	byte 118,byte 118,byte  72,byte  72,byte  42,byte   9,byte   9,byte   9,
+	byte 239,byte 239,byte 239,byte 210,byte 147,byte 147,byte 147,byte 101,
+	byte 101,byte 101,byte  71,byte  42,byte  25,byte  25,byte   8,byte   8,
+	byte 222,byte 193,byte 193,byte 193,byte 130,byte 130,byte 130,byte 101,
+	byte  84,byte  71,byte  71,byte  25,byte  25,byte   8,byte   8,byte  12,
+	byte 193,byte 193,byte 193,byte 193,byte 130,byte 130,byte 130,byte 130,
+	byte  71,byte  71,byte  71,byte  71,byte   8,byte   8,byte   8,byte  12,
+	byte 193,byte 193,byte 193,byte 193,byte 130,byte 130,byte 130,byte 134,
+	byte  71,byte  71,byte  71,byte  71,byte   8,byte   8,byte  12,byte  12,
+	byte 193,byte 193,byte 193,byte 193,byte 130,byte 130,byte 130,byte 134,
+	byte  71,byte  71,byte  71,byte  71,byte   8,byte   8,byte  12,byte  12,
+	byte 192,byte 192,byte 192,byte 129,byte 146,byte 146,byte 117,byte 100,
+	byte 100,byte  70,byte  70,byte  24,byte  24,byte   7,byte   7,byte  11,
+	byte 192,byte 192,byte 192,byte 146,byte 146,byte 146,byte 117,byte 117,
+	byte 117,byte  70,byte  70,byte  24,byte  24,byte   7,byte   7,byte  11,
+	byte 192,byte 192,byte 192,byte 146,byte 146,byte 146,byte 117,byte 117,
+	byte  87,byte  70,byte  70,byte  24,byte  24,byte   7,byte  11,byte  11,
+	byte 192,byte 192,byte 192,byte 146,byte 146,byte 163,byte 117,byte 117,
+	byte  87,byte  70,byte  41,byte  41,byte  24,byte   7,byte  11,byte  11,
+	byte 207,byte 207,byte 208,byte 162,byte 162,byte 162,byte 116,byte 116,
+	byte 116,byte  86,byte  57,byte  40,byte  40,byte  23,byte  10,byte  10,
+	byte 207,byte 207,byte 208,byte 162,byte 162,byte 162,byte 116,byte 116,
+	byte  86,byte  86,byte  40,byte  40,byte  23,byte  10,byte  10,byte  10,
+	byte 207,byte 207,byte 207,byte 162,byte 162,byte 132,byte 132,byte 115,
+	byte  86,byte  86,byte  40,byte  40,byte  23,byte  10,byte  10,byte  10,
+	byte 223,byte 223,byte 224,byte 178,byte 178,byte 178,byte 115,byte 115,
+	byte 102,byte  56,byte  56,byte  39,byte  39,byte   9,byte   9,byte   9,
+	byte 206,byte 223,byte 223,byte 161,byte 161,byte 148,byte 148,byte 102,
+	byte 102,byte  85,byte  39,byte  39,byte   9,byte   9,byte   9,byte   9,
+	byte 206,byte 206,byte 240,byte 177,byte 177,byte 131,byte 131,byte 118,
+	byte  85,byte  55,byte  55,byte  55,byte   9,byte   9,byte   9,byte   9,
+	byte 239,byte 239,byte 239,byte 177,byte 177,byte 131,byte 131,byte 101,
+	byte 101,byte  55,byte  55,byte  38,byte  25,byte  25,byte   8,byte   8,
+	byte 222,byte 222,byte 239,byte 160,byte 130,byte 130,byte 130,byte  84,
+	byte  84,byte  84,byte  38,byte  25,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 222,byte 193,byte 130,byte 130,byte 130,byte 130,byte  84,
+	byte  67,byte  71,byte  71,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 193,byte 193,byte 130,byte 130,byte 130,byte 130,byte  67,
+	byte  67,byte  71,byte  71,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 193,byte 193,byte 130,byte 130,byte 130,byte 130,byte  67,
+	byte  71,byte  71,byte  71,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 193,byte 193,byte 130,byte 130,byte 130,byte 130,byte  67,
+	byte  71,byte  71,byte  71,byte   8,byte   8,byte   8,byte   8,byte  12,
+	byte 192,byte 192,byte 192,byte 129,byte 129,byte 129,byte 100,byte  83,
+	byte  83,byte  54,byte  54,byte   7,byte   7,byte   7,byte   7,byte   7,
+	byte 192,byte 192,byte 192,byte 129,byte 129,byte 129,byte 100,byte 100,
+	byte  83,byte  54,byte  54,byte   7,byte   7,byte   7,byte   7,byte   7,
+	byte 192,byte 192,byte 192,byte 129,byte 129,byte 129,byte 100,byte 100,
+	byte  70,byte  54,byte  54,byte   7,byte   7,byte   7,byte   7,byte   7,
+	byte 192,byte 192,byte 192,byte 129,byte 129,byte 129,byte 100,byte 100,
+	byte  70,byte  53,byte  53,byte   7,byte   7,byte   7,byte   7,byte  11,
+	byte 207,byte 207,byte 207,byte 145,byte 145,byte 145,byte 116,byte  99,
+	byte  53,byte  53,byte  23,byte  23,byte   6,byte   6,byte   6,byte  10,
+	byte 207,byte 207,byte 207,byte 145,byte 145,byte 145,byte  99,byte  99,
+	byte  69,byte  69,byte  23,byte  23,byte   6,byte   6,byte  10,byte  10,
+	byte 207,byte 207,byte 207,byte 161,byte 161,byte 115,byte 115,byte 115,
+	byte  69,byte  69,byte  39,byte  39,byte   6,byte   6,byte  10,byte  10,
+	byte 206,byte 206,byte 144,byte 144,byte 161,byte 115,byte 115,byte 115,
+	byte  85,byte  39,byte  39,byte  22,byte  22,byte   9,byte   9,byte   9,
+	byte 206,byte 206,byte 144,byte 144,byte 144,byte 131,byte 114,byte  85,
+	byte  85,byte  68,byte  22,byte  22,byte   5,byte   9,byte   9,byte   9,
+	byte 206,byte 239,byte 160,byte 160,byte 160,byte 114,byte 114,byte 114,
+	byte  68,byte  55,byte  38,byte  38,byte  38,byte   9,byte   9,byte   9,
+	byte 222,byte 222,byte 160,byte 160,byte 160,byte 114,byte 114,byte  84,
+	byte  84,byte  38,byte  38,byte  21,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 159,byte 159,byte 130,byte 130,byte  67,byte  67,
+	byte  67,byte  67,byte  21,byte  21,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 205,byte 130,byte 130,byte 130,byte  67,byte  67,
+	byte  67,byte  67,byte  21,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 205,byte 130,byte 130,byte 130,byte  67,byte  67,
+	byte  67,byte  67,byte   8,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 193,byte 130,byte 130,byte 130,byte  67,byte  67,
+	byte  67,byte  67,byte   8,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 193,byte 130,byte 130,byte 130,byte  67,byte  67,
+	byte  67,byte  71,byte   8,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 192,byte 192,byte 129,byte 129,byte 129,byte 129,byte  66,byte  66,
+	byte  66,byte  37,byte  37,byte   7,byte   7,byte   7,byte   7,byte   7,
+	byte 192,byte 192,byte 129,byte 129,byte 129,byte 129,byte  83,byte  83,
+	byte  54,byte  37,byte  37,byte   7,byte   7,byte   7,byte   7,byte   7,
+	byte 192,byte 192,byte 129,byte 129,byte 129,byte  83,byte  83,byte  83,
+	byte  54,byte  37,byte  37,byte   7,byte   7,byte   7,byte   7,byte   7,
+	byte 207,byte 207,byte 128,byte 128,byte 128,byte  99,byte  82,byte  82,
+	byte  53,byte  53,byte  36,byte   6,byte   6,byte   6,byte   6,byte   6,
+	byte 207,byte 207,byte 128,byte 128,byte 128,byte  82,byte  82,byte  82,
+	byte  53,byte  53,byte   6,byte   6,byte   6,byte   6,byte   6,byte   6,
+	byte 207,byte 207,byte 128,byte 128,byte 128,byte  82,byte  82,byte  82,
+	byte  69,byte  52,byte   6,byte   6,byte   6,byte   6,byte   6,byte   6,
+	byte 206,byte 206,byte 144,byte 144,byte 144,byte  98,byte  98,byte  98,
+	byte  52,byte  52,byte  22,byte  22,byte   5,byte   5,byte   5,byte   9,
+	byte 206,byte 206,byte 143,byte 143,byte 143,byte  98,byte  98,byte  98,
+	byte  68,byte  52,byte  22,byte   5,byte   5,byte   5,byte   5,byte   9,
+	byte 206,byte 206,byte 143,byte 143,byte 143,byte 114,byte 114,byte  68,
+	byte  68,byte  51,byte   5,byte   5,byte   5,byte   5,byte   9,byte   9,
+	byte 205,byte 222,byte 159,byte 159,byte 159,byte  97,byte  97,byte  97,
+	byte  51,byte  38,byte  21,byte  21,byte  21,byte   4,byte   8,byte   8,
+	byte 205,byte 205,byte 159,byte 159,byte 159,byte  97,byte  97,byte  67,
+	byte  67,byte  21,byte  21,byte   4,byte   4,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 142,byte 142,byte 142,byte  80,byte  67,byte  67,
+	byte  67,byte  21,byte   4,byte   4,byte   4,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 142,byte 142,byte 142,byte 130,byte  67,byte  67,
+	byte  67,byte   4,byte   4,byte   4,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 142,byte 142,byte 130,byte 130,byte  67,byte  67,
+	byte  67,byte   4,byte   4,byte   4,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 142,byte 142,byte 130,byte 130,byte  67,byte  67,
+	byte  67,byte   4,byte   4,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 205,byte 205,byte 142,byte 130,byte 130,byte 130,byte  67,byte  67,
+	byte  67,byte   4,byte   4,byte   8,byte   8,byte   8,byte   8,byte   8,
+	byte 192,byte 129,byte 129,byte 129,byte 129,byte  66,byte  66,byte  66,
+	byte  20,byte  20,byte  20,byte   3,byte   7,byte   7,byte   7,byte   7,
+	byte 192,byte 129,byte 129,byte 129,byte 129,byte  66,byte  66,byte  66,
+	byte  37,byte  20,byte  20,byte  20,byte   7,byte   7,byte   7,byte   7,
+	byte 192,byte 129,byte 129,byte 129,byte 129,byte  66,byte  66,byte  66,
+	byte  36,byte  36,byte  20,byte   7,byte   7,byte   7,byte   7,byte   7,
+	byte 207,byte 128,byte 128,byte 128,byte 128,byte  65,byte  65,byte  65,
+	byte  36,byte  36,byte  19,byte   6,byte   6,byte   6,byte   6,byte   6,
+	byte 207,byte 128,byte 128,byte 128,byte 128,byte  65,byte  65,byte  65,
+	byte  36,byte  36,byte  19,byte   6,byte   6,byte   6,byte   6,byte   6,
+	byte 207,byte 128,byte 128,byte 128,byte 128,byte  81,byte  81,byte  52,
+	byte  52,byte  35,byte  35,byte   6,byte   6,byte   6,byte   6,byte   6,
+	byte 206,byte 143,byte 143,byte 143,byte 143,byte  81,byte  81,byte  81,
+	byte  35,byte  35,byte   5,byte   5,byte   5,byte   5,byte   5,byte   5,
+	byte 206,byte 143,byte 143,byte 143,byte 143,byte  81,byte  64,byte  64,
+	byte  51,byte  51,byte   5,byte   5,byte   5,byte   5,byte   5,byte   5,
+	byte 206,byte 143,byte 143,byte 143,byte  97,byte  97,byte  97,byte  51,
+	byte  51,byte  51,byte   5,byte   5,byte   5,byte   5,byte   5,byte   5,
+	byte 205,byte 142,byte 142,byte 142,byte 142,byte  80,byte  80,byte  80,
+	byte  34,byte  21,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,
+	byte 205,byte 142,byte 142,byte 142,byte 142,byte  80,byte  80,byte  67,
+	byte  67,byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,
+	byte 205,byte 142,byte 142,byte 142,byte 142,byte  79,byte  79,byte  67,
+	byte  67,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,byte   8,
+	byte 205,byte 142,byte 142,byte 142,byte 142,byte  79,byte  67,byte  67,
+	byte  67,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,byte   8,
+	byte 205,byte 142,byte 142,byte 142,byte 142,byte  79,byte  67,byte  67,
+	byte  67,byte   4,byte   4,byte   4,byte   4,byte   8,byte   8,byte   8,
+	byte 205,byte 142,byte 142,byte 142,byte 142,byte  67,byte  67,byte  67,
+	byte  67,byte   4,byte   4,byte   4,byte   4,byte   8,byte   8,byte   8,
+	byte 205,byte 142,byte 142,byte 142,byte 142,byte  67,byte  67,byte  67,
+	byte  67,byte   4,byte   4,byte   4,byte   8,byte   8,byte   8,byte   8,
+	byte 129,byte 129,byte 129,byte 129,byte  66,byte  66,byte  66,byte  66,
+	byte  20,byte   3,byte   3,byte   3,byte   3,byte   7,byte   7,byte   7,
+	byte 129,byte 129,byte 129,byte 129,byte  66,byte  66,byte  66,byte  66,
+	byte  20,byte   3,byte   3,byte   3,byte   3,byte   7,byte   7,byte   7,
+	byte 128,byte 128,byte 128,byte 128,byte  65,byte  65,byte  65,byte  65,
+	byte  19,byte  19,byte   2,byte   2,byte   6,byte   6,byte   6,byte   6,
+	byte 128,byte 128,byte 128,byte 128,byte  65,byte  65,byte  65,byte  65,
+	byte  19,byte  19,byte   2,byte   2,byte   6,byte   6,byte   6,byte   6,
+	byte 128,byte 128,byte 128,byte 128,byte  65,byte  65,byte  65,byte  65,
+	byte  19,byte  19,byte   2,byte   6,byte   6,byte   6,byte   6,byte   6,
+	byte 143,byte 143,byte 143,byte 143,byte  64,byte  64,byte  64,byte  35,
+	byte  35,byte  18,byte  18,byte   5,byte   5,byte   5,byte   5,byte   5,
+	byte 143,byte 143,byte 143,byte 143,byte  64,byte  64,byte  64,byte  64,
+	byte  18,byte  18,byte  18,byte   5,byte   5,byte   5,byte   5,byte   5,
+	byte 143,byte 143,byte 143,byte 143,byte  64,byte  64,byte  64,byte  34,
+	byte  34,byte  34,byte   5,byte   5,byte   5,byte   5,byte   5,byte   5,
+	byte 142,byte 142,byte 142,byte 142,byte  80,byte  80,byte  80,byte  34,
+	byte  34,byte  34,byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,
+	byte 142,byte 142,byte 142,byte 142,byte  79,byte  79,byte  79,byte  34,
+	byte  17,byte  17,byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,
+	byte 142,byte 142,byte 142,byte 142,byte  79,byte  79,byte  79,byte  79,
+	byte  17,byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,
+	byte 142,byte 142,byte 142,byte 142,byte  79,byte  79,byte  79,byte  79,
+	byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,
+	byte 142,byte 142,byte 142,byte 142,byte  79,byte  79,byte  79,byte  79,
+	byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,
+	byte 142,byte 142,byte 142,byte 142,byte  79,byte  79,byte  79,byte  67,
+	byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,
+	byte 142,byte 142,byte 142,byte 142,byte  79,byte  79,byte  79,byte  67,
+	byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,byte   8,
+	byte 142,byte 142,byte 142,byte 142,byte  79,byte  79,byte  67,byte  67,
+	byte   4,byte   4,byte   4,byte   4,byte   4,byte   4,byte   8,byte   8,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,byte   1,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,
+	byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0,byte   0
+	
+};
+
+rgbvmap_y := array[256] of {
+	16rff, 16rf5, 16reb, 16re1, 16rcd, 16rc3, 16rb9, 16rb0, 
+	16r9b, 16r91, 16r87, 16r7e, 16r69, 16r5f, 16r55, 16r4c, 
+	16r47, 16ree, 16re4, 16rdb, 16rd2, 16rbf, 16rb5, 16rac, 
+	16ra3, 16r90, 16r87, 16r7e, 16r75, 16r62, 16r59, 16r50, 
+	16r4a, 16r42, 16rdd, 16rd4, 16rcc, 16rc3, 16rb1, 16ra9, 
+	16ra0, 16r98, 16r86, 16r7d, 16r75, 16r6c, 16r5b, 16r52, 
+	16r4c, 16r44, 16r3c, 16rcc, 16rc4, 16rbc, 16rb4, 16ra4, 
+	16r9c, 16r94, 16r8c, 16r7c, 16r74, 16r6c, 16r64, 16r54, 
+	16rdb, 16rd2, 16rc8, 16rb3, 16rbb, 16rb0, 16ra5, 16r81, 
+	16r83, 16r79, 16r6e, 16r4f, 16r4d, 16r42, 16r37, 16re5, 
+	16rd6, 16rcc, 16rc3, 16rba, 16ra7, 16raa, 16ra0, 16r96, 
+	16r78, 16r78, 16r6e, 16r64, 16r4a, 16r46, 16r3c, 16r32, 
+	16r2d, 16rc6, 16rbe, 16rb6, 16rad, 16r9b, 16r99, 16r90, 
+	16r87, 16r6f, 16r6b, 16r63, 16r5a, 16r45, 16r3f, 16r36, 
+	16r30, 16r28, 16rb7, 16raf, 16ra8, 16ra0, 16r8f, 16r88, 
+	16r80, 16r78, 16r67, 16r60, 16r58, 16r50, 16r3f, 16r38, 
+	16rb8, 16raf, 16r9a, 16r9e, 16r94, 16r89, 16r68, 16r67, 
+	16r77, 16r69, 16r36, 16r31, 16r31, 16r23, 16rcc, 16rc2, 
+	16rb5, 16rac, 16ra3, 16r8f, 16r90, 16r86, 16r7d, 16r61, 
+	16r5e, 16r66, 16r5a, 16r32, 16r2c, 16r2a, 16r1e, 16rbe, 
+	16rb0, 16ra8, 16r9f, 16r97, 16r85, 16r81, 16r79, 16r70, 
+	16r59, 16r54, 16r55, 16r4b, 16r2f, 16r28, 16r23, 16r19, 
+	16r14, 16ra3, 16r9b, 16r93, 16r8c, 16r7b, 16r73, 16r6b, 
+	16r64, 16r53, 16r4b, 16r44, 16r3c, 16r2b, 16r23, 16r1c, 
+	16r95, 16r80, 16r83, 16r78, 16r6d, 16r4e, 16r4b, 16r53, 
+	16r45, 16r1d, 16r15, 16r0d, 16r33, 16rb2, 16ra9, 16r9f, 
+	16r94, 16r8b, 16r77, 16r77, 16r6d, 16r63, 16r49, 16r45, 
+	16r47, 16r3b, 16r1b, 16r13, 16r0b, 16r22, 16ra6, 16r9d, 
+	16r92, 16r8a, 16r81, 16r6f, 16r6b, 16r62, 16r59, 16r44, 
+	16r3e, 16r3b, 16r31, 16r19, 16r11, 16r09, 16r11, 16r9a, 
+	16r8f, 16r87, 16r7f, 16r77, 16r67, 16r5f, 16r57, 16r4f, 
+	16r3f, 16r37, 16r2f, 16r27, 16r17, 16r0f, 16r07, 16r00 
+};
+
+rgbvmap_cb := array[256] of {
+	16r80, 16r55, 16r2b, 16r00, 16r9c, 16r71, 16r47, 16r1c, 
+	16rb8, 16r8d, 16r63, 16r38, 16rd4, 16ra9, 16r7f, 16r54, 
+	16r57, 16r80, 16r58, 16r30, 16r09, 16r9a, 16r72, 16r4b, 
+	16r23, 16rb4, 16r8c, 16r65, 16r3d, 16rce, 16ra6, 16r7f, 
+	16r7f, 16r5a, 16r80, 16r5b, 16r36, 16r11, 16r98, 16r73, 
+	16r4e, 16r2a, 16rb1, 16r8c, 16r67, 16r42, 16rc9, 16ra4, 
+	16ra1, 16r7f, 16r5d, 16r80, 16r5e, 16r3c, 16r1a, 16r96, 
+	16r74, 16r52, 16r30, 16rad, 16r8b, 16r69, 16r47, 16rc3, 
+	16r63, 16r39, 16r0e, 16raa, 16r80, 16r51, 16r22, 16rc6, 
+	16r9f, 16r70, 16r41, 16re2, 16rbd, 16r8e, 16r60, 16r8e, 
+	16r8d, 16r65, 16r3d, 16r16, 16ra8, 16r80, 16r55, 16r2b, 
+	16rc2, 16r9c, 16r71, 16r47, 16rdc, 16rb8, 16r8d, 16r63, 
+	16r66, 16r8c, 16r67, 16r42, 16r1d, 16ra5, 16r80, 16r59, 
+	16r33, 16rbd, 16r99, 16r73, 16r4d, 16rd5, 16rb2, 16r8c, 
+	16r8b, 16r69, 16r8b, 16r69, 16r47, 16r25, 16ra2, 16r80, 
+	16r5e, 16r3c, 16rb8, 16r96, 16r74, 16r52, 16rcf, 16rad, 
+	16r47, 16r1d, 16rb8, 16r8f, 16r60, 16r32, 16rd5, 16raf, 
+	16r80, 16r44, 16rf1, 16rcd, 16ra7, 16r6b, 16r9c, 16r72, 
+	16r72, 16r4b, 16r23, 16rb5, 16r8e, 16r63, 16r39, 16rcf, 
+	16raa, 16r80, 16r4d, 16re9, 16rc6, 16ra1, 16r6e, 16r9a, 
+	16r98, 16r73, 16r4e, 16r2a, 16rb1, 16r8c, 16r66, 16r40, 
+	16rca, 16ra6, 16r80, 16r55, 16re2, 16rbf, 16r9c, 16r71, 
+	16r74, 16r96, 16r74, 16r52, 16r30, 16rad, 16r8b, 16r69, 
+	16r47, 16rc4, 16ra2, 16r80, 16r5e, 16rda, 16rb8, 16r96, 
+	16r2b, 16rc7, 16r9f, 16r70, 16r42, 16re3, 16rbe, 16r94, 
+	16r58, 16rff, 16rdd, 16rbb, 16r80, 16rab, 16r80, 16r56, 
+	16r58, 16r31, 16rc2, 16r9c, 16r72, 16r47, 16rdc, 16rb8, 
+	16r91, 16r5e, 16rf7, 16rd5, 16rb3, 16r80, 16ra8, 16r80, 
+	16r80, 16r5b, 16r36, 16rbd, 16r99, 16r73, 16r4d, 16rd6, 
+	16rb3, 16r8e, 16r63, 16ree, 16rcc, 16raa, 16r80, 16ra5, 
+	16ra2, 16r80, 16r5e, 16r3c, 16rb8, 16r96, 16r74, 16r52, 
+	16rcf, 16rad, 16r8b, 16r69, 16re6, 16rc4, 16ra2, 16r80
+};
+
+rgbvmap_cr := array[256] of {
+	16r80, 16r86, 16r8d, 16r94, 16ra3, 16raa, 16rb1, 16rb8, 
+	16rc7, 16rce, 16rd5, 16rdb, 16rea, 16rf1, 16rf8, 16rff, 
+	16rf7, 16r80, 16r86, 16r8c, 16r93, 16ra1, 16ra8, 16rae, 
+	16rb4, 16rc2, 16rc9, 16rcf, 16rd5, 16re3, 16rea, 16rf0, 
+	16re8, 16ree, 16r80, 16r86, 16r8c, 16r91, 16r9e, 16ra5, 
+	16rab, 16rb0, 16rbd, 16rc3, 16rca, 16rcf, 16rdc, 16re2, 
+	16rda, 16re0, 16re6, 16r80, 16r85, 16r8b, 16r90, 16r9c, 
+	16ra2, 16ra7, 16rad, 16rb8, 16rbe, 16rc4, 16rc9, 16rd5, 
+	16r5c, 16r63, 16r6a, 16r79, 16r80, 16r87, 16r8f, 16r9c, 
+	16ra7, 16raf, 16rb6, 16rc0, 16rce, 16rd5, 16rdd, 16r55, 
+	16r58, 16r5e, 16r64, 16r6b, 16r79, 16r80, 16r86, 16r8d, 
+	16r9a, 16ra3, 16raa, 16rb1, 16rbb, 16rc7, 16rce, 16rd5, 
+	16rcc, 16r5b, 16r61, 16r67, 16r6c, 16r79, 16r80, 16r86, 
+	16r8c, 16r98, 16ra0, 16ra6, 16rac, 16rb7, 16rc0, 16rc6, 
+	16rbe, 16rc4, 16r5e, 16r63, 16r69, 16r6e, 16r7a, 16r80, 
+	16r85, 16r8b, 16r96, 16r9c, 16ra2, 16ra7, 16rb3, 16rb8, 
+	16r38, 16r3f, 16r4e, 16r51, 16r58, 16r60, 16r72, 16r78, 
+	16r80, 16r89, 16r95, 16r9f, 16rb1, 16rbb, 16r2b, 16r31, 
+	16r37, 16r3d, 16r43, 16r51, 16r55, 16r5c, 16r63, 16r73, 
+	16r79, 16r80, 16r88, 16r94, 16r9c, 16raa, 16rb3, 16r30, 
+	16r36, 16r3c, 16r42, 16r47, 16r54, 16r59, 16r5f, 16r65, 
+	16r73, 16r79, 16r80, 16r86, 16r92, 16r99, 16ra3, 16raa, 
+	16ra2, 16r3c, 16r41, 16r47, 16r4c, 16r58, 16r5e, 16r63, 
+	16r69, 16r74, 16r7a, 16r80, 16r85, 16r91, 16r96, 16r9c, 
+	16r15, 16r24, 16r22, 16r2a, 16r31, 16r47, 16r49, 16r44, 
+	16r4e, 16r6b, 16r70, 16r76, 16r80, 16r00, 16r07, 16r0e, 
+	16r15, 16r1c, 16r2a, 16r2a, 16r31, 16r38, 16r4b, 16r4e, 
+	16r4d, 16r55, 16r6c, 16r72, 16r77, 16r80, 16r08, 16r0f, 
+	16r17, 16r1d, 16r23, 16r30, 16r33, 16r39, 16r3f, 16r4f, 
+	16r53, 16r55, 16r5c, 16r6e, 16r73, 16r79, 16r80, 16r11, 
+	16r1a, 16r1f, 16r25, 16r2a, 16r36, 16r3c, 16r41, 16r47, 
+	16r52, 16r58, 16r5e, 16r63, 16r6f, 16r74, 16r7a, 16r80
+};
--- /dev/null
+++ b/appl/cmd/9660srv.b
@@ -1,0 +1,1504 @@
+implement ISO9660;
+
+include "sys.m";
+	sys: Sys;
+	Dir, Qid, QTDIR, QTFILE, DMDIR: import sys;
+
+include "draw.m";
+
+include "daytime.m";
+	daytime:	Daytime;
+
+include "string.m";
+	str: String;
+
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+
+include "arg.m";
+
+ISO9660: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Sectorsize: con 2048;
+Maxname: con 256;
+
+Enonexist:	con "file does not exist";
+Eperm:	con "permission denied";
+Enofile:	con "no file system specified";
+Eauth:	con "authentication failed";
+Ebadfid:	con	"invalid fid";
+Efidinuse:	con	"fid already in use";
+Enotdir:	con	"not a directory";
+Esyntax:	con	"file name syntax";
+
+devname: string;
+
+chatty := 0;
+showstyx := 0;
+progname := "9660srv";
+stderr: ref Sys->FD;
+noplan9 := 0;
+nojoliet := 0;
+norock := 0;
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: %s [-rabc] [-9JR] [-s] cd_device dir\n", progname);
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+
+	if(args != nil)
+		progname = hd args;
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		noload(Styx->PATH);
+	styx->init();
+
+	if(args != nil)
+		progname = hd args;
+	mountopt := Sys->MREPL;
+	copt := 0;
+	stdio := 0;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		noload(Arg->PATH);
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		'v' or 'D' => chatty = 1; showstyx = 1;
+		'r' => mountopt = Sys->MREPL;
+		'a' => mountopt = Sys->MAFTER;
+		'b' => mountopt = Sys->MBEFORE;
+		'c' => copt = Sys->MCREATE;
+		's' => stdio = 1;
+		'9' => noplan9 = 1;
+		'J' => nojoliet = 1;
+		'R' => norock = 1;
+		* => usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(args == nil || tl args == nil)
+		usage();
+	what := hd args;
+	mountpt := hd tl args;
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		noload(Daytime->PATH);
+
+	iobufinit(Sectorsize);
+
+	pip := array[2] of ref Sys->FD;
+	if(stdio){
+		pip[0] = sys->fildes(0);
+		pip[1] = sys->fildes(1);
+	}else
+		if(sys->pipe(pip) < 0)
+			error(sys->sprint("can't create pipe: %r"));
+
+	devname = what;
+
+	sync := chan of int;
+	spawn fileserve(pip[1], sync);
+	<-sync;
+
+	if(sys->mount(pip[0], nil, mountpt, mountopt|copt, nil) < 0) {
+		sys->fprint(sys->fildes(2), "%s: mount %s %s failed: %r\n", progname, what, mountpt);
+		exit;
+	}
+}
+
+noload(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s: can't load %s: %r\n", progname, s);
+	raise "fail:load";
+}
+
+error(p: string)
+{
+	sys->fprint(sys->fildes(2), "9660srv: %s\n", p);
+	raise "fail:error";
+}
+
+fileserve(rfd: ref Sys->FD, sync: chan of int)
+{
+	sys->pctl(Sys->NEWFD|Sys->FORKNS, list of {2, rfd.fd});
+	rfd = sys->fildes(rfd.fd);
+	stderr = sys->fildes(2);
+	sync <-= 1;
+	while((m := Tmsg.read(rfd, 0)) != nil){
+		if(showstyx)
+			chat(sys->sprint("%s...", m.text()));
+		r: ref Rmsg;
+		pick t := m {
+		Readerror =>
+			error(sys->sprint("mount read error: %s", t.error));
+		Version =>
+ 			r = rversion(t);
+		Auth =>
+			r = rauth(t);
+		Flush =>
+ 			r = rflush(t);
+		Attach =>
+ 			r = rattach(t);
+		Walk =>
+ 			r = rwalk(t);
+		Open =>
+ 			r = ropen(t);
+		Create =>
+ 			r = rcreate(t);
+		Read =>
+ 			r = rread(t);
+		Write =>
+ 			r = rwrite(t);
+		Clunk =>
+ 			r = rclunk(t);
+		Remove =>
+ 			r = rremove(t);
+		Stat =>
+ 			r = rstat(t);
+		Wstat =>
+ 			r = rwstat(t);
+		* =>
+			error(sys->sprint("invalid T-message tag: %d", tagof m));
+		}
+		pick e := r {
+		Error =>
+			r.tag = m.tag;
+		}
+		rbuf := r.pack();
+		if(rbuf == nil)
+			error("bad R-message conversion");
+		if(showstyx)
+			chat(r.text()+"\n");
+		if(sys->write(rfd, rbuf, len rbuf) != len rbuf)
+			error(sys->sprint("connection write error: %r"));
+	}
+
+	if(chatty)
+		chat("server end of file\n");
+}
+
+E(s: string): ref Rmsg.Error
+{
+	return ref Rmsg.Error(0, s);
+}
+
+rversion(t: ref Tmsg.Version): ref Rmsg
+{
+	(msize, version) := styx->compatible(t, Styx->MAXRPC, Styx->VERSION);
+	return ref Rmsg.Version(t.tag, msize, version);
+}
+
+rauth(t: ref Tmsg.Auth): ref Rmsg
+{
+	return ref Rmsg.Error(t.tag, "authentication not required");
+}
+
+rflush(t: ref Tmsg.Flush): ref Rmsg
+{
+	return ref Rmsg.Flush(t.tag);
+}
+
+rattach(t: ref Tmsg.Attach): ref Rmsg
+{
+	dname := devname;
+	if(t.aname != "")
+		dname = t.aname;
+	(dev, err) := devattach(dname, Sys->OREAD, Sectorsize);
+	if(dev == nil)
+		return E(err);
+
+	xf := Xfs.new(dev);
+	root := cleanfid(t.fid);
+	root.qid = Sys->Qid(big 0, 0, Sys->QTDIR);
+	root.xf = xf;
+	err = root.attach();
+	if(err != nil){
+		clunkfid(t.fid);
+		return E(err);
+	}
+	xf.rootqid = root.qid;
+	return ref Rmsg.Attach(t.tag, root.qid);
+}
+
+walk1(f: ref Xfile, name: string): string
+{
+	if(!(f.qid.qtype & Sys->QTDIR))
+		return Enotdir;
+	case name {
+	"." =>
+		return nil;	# nop, but shouldn't happen
+	".." =>
+		if(f.qid.path==f.xf.rootqid.path)
+			return nil;
+		return f.walkup();
+	* =>
+		return f.walk(name);
+	}
+}
+
+rwalk(t: ref Tmsg.Walk): ref Rmsg
+{
+	f:=findfid(t.fid);
+	if(f == nil)
+		return E(Ebadfid);
+	nf, sf: ref Xfile;
+	if(t.newfid != t.fid){
+		nf = cleanfid(t.newfid);
+		if(nf == nil)
+			return E(Efidinuse);
+		f.clone(nf);
+		f = nf;
+	}else
+		sf = f.save();
+
+	qids: array of Sys->Qid;
+	if(len t.names > 0){
+		qids = array[len t.names] of Sys->Qid;
+		for(i := 0; i < len t.names; i++){
+			e := walk1(f, t.names[i]);
+			if(e != nil){
+				if(nf != nil){
+					nf.clunk();
+					clunkfid(t.newfid);
+				}else
+					f.restore(sf);
+				if(i == 0)
+					return E(e);
+				return ref Rmsg.Walk(t.tag, qids[0:i]);
+			}
+			qids[i] = f.qid;
+		}
+	}
+	return ref Rmsg.Walk(t.tag, qids);
+}
+
+ropen(t: ref Tmsg.Open): ref Rmsg
+{
+	f := findfid(t.fid);
+	if(f == nil)
+		return E(Ebadfid);
+	if(f.flags&Omodes)
+		return E("open on open file");
+	e := f.open(t.mode);
+	if(e != nil)
+		return E(e);
+	f.flags = openflags(t.mode);
+	return ref Rmsg.Open(t.tag, f.qid, Styx->MAXFDATA);
+}
+
+rcreate(t: ref Tmsg.Create): ref Rmsg
+{
+	name := t.name;
+	if(name == "." || name == "..")
+		return E(Esyntax);
+	f := findfid(t.fid);
+	if(f == nil)
+		return E(Ebadfid);
+	if(f.flags&Omodes)
+		return E("create on open file");
+	if(!(f.qid.qtype&Sys->QTDIR))
+		return E("create in non-directory");
+	e := f.create(name, t.perm, t.mode);
+	if(e != nil)
+		return E(e);
+	f.flags = openflags(t.mode);
+	return ref Rmsg.Create(t.tag, f.qid, Styx->MAXFDATA);
+}
+
+rread(t: ref Tmsg.Read): ref Rmsg
+{
+	err: string;
+
+	f := findfid(t.fid);
+	if(f == nil)
+		return E(Ebadfid);
+	if (!(f.flags&Oread))
+		return E("file not opened for reading");
+	if(t.count < 0 || t.offset < big 0)
+		return E("negative offset or count");
+	b := array[Styx->MAXFDATA] of byte;
+	count: int;
+	if(f.qid.qtype & Sys->QTDIR)
+		(count, err) = f.readdir(b, int t.offset, t.count);
+	else
+		(count, err) = f.read(b, int t.offset, t.count);
+	if(err != nil)
+		return E(err);
+	if(count != len b)
+		b = b[0:count];
+	return ref Rmsg.Read(t.tag, b);
+}
+
+rwrite(nil: ref Tmsg.Write): ref Rmsg
+{
+	return E(Eperm);
+}
+
+rclunk(t: ref Tmsg.Clunk): ref Rmsg
+{
+	f := findfid(t.fid);
+	if(f == nil)
+		return E(Ebadfid);
+	f.clunk();
+	clunkfid(t.fid);
+	return ref Rmsg.Clunk(t.tag);
+}
+
+rremove(t: ref Tmsg.Remove): ref Rmsg
+{
+	f := findfid(t.fid);
+	if(f == nil)
+		return E(Ebadfid);
+	f.clunk();
+	clunkfid(t.fid);
+	return E(Eperm);
+}
+
+rstat(t: ref Tmsg.Stat): ref Rmsg
+{
+	f := findfid(t.fid);
+	if(f == nil)
+		return E(Ebadfid);
+	(dir, nil) := f.stat();
+	return ref Rmsg.Stat(t.tag, *dir);
+}
+
+rwstat(nil: ref Tmsg.Wstat): ref Rmsg
+{
+	return E(Eperm);
+}
+
+openflags(mode: int): int
+{
+	flags := 0;
+	case mode & ~(Sys->OTRUNC|Sys->ORCLOSE) {
+	Sys->OREAD =>
+		flags = Oread;
+	Sys->OWRITE =>
+		flags = Owrite;
+	Sys->ORDWR =>
+		flags = Oread|Owrite;
+	}
+	if(mode & Sys->ORCLOSE)
+		flags |= Orclose;
+	return flags;
+}
+
+chat(s: string)
+{
+	if(chatty)
+		sys->fprint(stderr, "%s", s);
+}
+
+Fid: adt {
+	fid:	int;
+	file:	ref Xfile;
+};
+
+FIDMOD: con 127;	# prime
+fids := array[FIDMOD] of list of ref Fid;
+
+hashfid(fid: int): (ref Fid, array of list of ref Fid)
+{
+	nl: list of ref Fid;
+
+	hp := fids[fid%FIDMOD:];
+	nl = nil;
+	for(l := hp[0]; l != nil; l = tl l){
+		f := hd l;
+		if(f.fid == fid){
+			l = tl l;	# excluding f
+			for(; nl != nil; nl = tl nl)
+				l = (hd nl) :: l;	# put examined ones back, in order
+			hp[0] = l;
+			return (f, hp);
+		} else
+			nl = f :: nl;
+	}
+	return (nil, hp);
+}
+
+findfid(fid: int): ref Xfile
+{
+	(f, hp) := hashfid(fid);
+	if(f == nil){
+		chat("unassigned fid");
+		return nil;
+	}
+	hp[0] = f :: hp[0];
+	return f.file;
+}
+
+cleanfid(fid: int): ref Xfile
+{
+	(f, hp) := hashfid(fid);
+	if(f != nil){
+		chat("fid in use");
+		return nil;
+	}
+	f = ref Fid;
+	f.fid = fid;
+	f.file = Xfile.new();
+	hp[0] = f :: hp[0];
+	return f.file.clean();
+}
+
+clunkfid(fid: int)
+{
+	(f, nil) := hashfid(fid);
+	if(f != nil)
+		f.file.clean();
+}
+
+#
+#
+#
+
+Xfs: adt {
+	d:	ref Device;
+	inuse:	int;
+	issusp:	int;	# system use sharing protocol in use?
+	suspoff:	int;	# LEN_SKP, if so
+	isplan9:	int;	# has Plan 9-specific directory info
+	isrock:	int;	# is rock ridge
+	rootqid:	Sys->Qid;
+	ptr:	int;	# tag for private data
+
+	new:	fn(nil: ref Device): ref Xfs;
+	incref:	fn(nil: self ref Xfs);
+	decref:	fn(nil: self ref Xfs);
+};
+
+Xfile:	adt {
+	xf:	ref Xfs;
+	flags:	int;
+	qid:	Sys->Qid;
+	ptr:	ref Isofile;	# tag for private data
+
+	new:		fn(): ref Xfile;
+	clean:	fn(nil: self ref Xfile): ref Xfile;
+
+	save:		fn(nil: self ref Xfile): ref Xfile;
+	restore:	fn(nil: self ref Xfile, s: ref Xfile);
+
+	attach:	fn(nil: self ref Xfile): string;
+	clone:	fn(nil: self ref Xfile, nil: ref Xfile);
+	walkup:	fn(nil: self ref Xfile): string;
+	walk:	fn(nil: self ref Xfile, nil: string): string;
+	open:	fn(nil: self ref Xfile, nil: int): string;
+	create:	fn(nil: self ref Xfile, nil: string, nil: int, nil: int): string;
+	readdir:	fn(nil: self ref Xfile, nil: array of byte, nil: int, nil: int): (int, string);
+	read:		fn(nil: self ref Xfile, nil: array of byte, nil: int, nil: int): (int, string);
+	write:	fn(nil: self ref Xfile, nil: array of byte, nil: int, nil: int): (int, string);
+	clunk:	fn(nil: self ref Xfile);
+	remove:	fn(nil: self ref Xfile): string;
+	stat:		fn(nil: self ref Xfile): (ref Sys->Dir, string);
+	wstat:	fn(nil: self ref Xfile, nil: ref Sys->Dir): string;
+};
+
+Oread, Owrite, Orclose: con 1<<iota;
+Omodes: con 3;	# mask
+
+VOLDESC: con 16;	# sector number
+
+Drec: adt {
+	reclen:	int;
+	attrlen:	int;
+	addr:	int;	# should be big?
+	size:	int;	# should be big?
+	date:	array of byte;
+	time:	int;
+	tzone:	int;	# not in high sierra
+	flags:	int;
+	unitsize:	int;
+	gapsize:	int;
+	vseqno:	int;
+	name:	array of byte;
+	data:	array of byte;	# system extensions
+};
+
+Isofile: adt {
+	fmt:	int;	# 'z' if iso, 'r' if high sierra
+	blksize:	int;
+	offset:	int;	# true offset when reading directory
+	doffset:	int;	# styx offset when reading directory
+	d:	ref Drec;
+};
+
+Xfile.new(): ref Xfile
+{
+	f := ref Xfile;
+	return f.clean();
+}
+
+Xfile.clean(f: self ref Xfile): ref Xfile
+{
+	if(f.xf != nil){
+		f.xf.decref();
+		f.xf = nil;
+	}
+	f.ptr = nil;
+	f.flags = 0;
+	f.qid = Qid(big 0, 0, 0);
+	return f;
+}
+
+Xfile.save(f: self ref Xfile): ref Xfile
+{
+	s := ref Xfile;
+	*s = *f;
+	s.ptr = ref *f.ptr;
+	s.ptr.d = ref *f.ptr.d;
+	return s;
+}
+
+Xfile.restore(f: self ref Xfile, s: ref Xfile)
+{
+	f.flags = s.flags;
+	f.qid = s.qid;
+	*f.ptr = *s.ptr;
+}
+
+Xfile.attach(root: self ref Xfile): string
+{
+	fmt := 0;
+	blksize := 0;
+	haveplan9 := 0;
+	dirp: ref Block;
+	dp := ref Drec;
+	for(a:=VOLDESC;a<VOLDESC+100;a++){
+		p := Block.get(root.xf.d, a);
+		if(p == nil){
+			if(dirp != nil)
+				dirp.put();
+			return "can't read volume descriptor";
+		}
+		v := p.data;	# Voldesc
+		if(eqs(v[0:7], "\u0001CD001\u0001")){		# ISO
+			if(dirp != nil)
+				dirp.put();
+			dirp = p;
+			fmt = 'z';
+			convM2Drec(v[156:], dp, 0);	# v.z.desc.rootdir
+			blksize = l16(v[128:]);	# v.z.desc.blksize
+			if(chatty)
+				chat(sys->sprint("iso, blksize=%d...", blksize));
+			haveplan9 = eqs(v[8:8+6], "PLAN 9");	# v.z.boot.sysid
+			if(haveplan9){
+				if(noplan9) {
+					chat("ignoring plan9");
+					haveplan9 = 0;
+				}else{
+					fmt = '9';
+					chat("plan9 iso...");
+				}
+			}
+			continue;
+		}
+		if(eqs(v[8:8+7], "\u0001CDROM\u0001")){	# high sierra
+			if(dirp != nil)
+				dirp.put();
+			dirp = p;
+			fmt = 'r';
+			convM2Drec(v[180:], dp, 1);	# v.r.desc.rootdir
+			blksize = l16(v[136:]);	# v.r.desc.blksize
+			if(chatty)
+				chat(sys->sprint("high sierra, blksize=%d...", blksize));
+			continue;
+		}
+		if(haveplan9==0 && !nojoliet && eqs(v[0:7], "\u0002CD001\u0001")){
+			q := v[88:];	# v.z.desc.escapes
+			if(q[0] == byte 16r25 && q[1] == byte 16r2F &&
+			   (q[2] == byte 16r40 || q[2] == byte 16r43 || q[2] == byte 16r45)){	# joliet, it appears
+				if(dirp != nil)
+					dirp.put();
+				dirp = p;
+				fmt = 'J';
+				convM2Drec(v[156:], dp, 0);	# v.z.desc.rootdir
+				if(blksize != l16(v[128:]))	# v.z.desc.blksize
+					sys->fprint(stderr, "9660srv: warning: suspicious Joliet block size: %d\n", l16(v[128:]));
+				chat("joliet...");
+				continue;
+			}
+		}else{
+			p.put();
+			if(v[0] == byte 16rFF)
+				break;
+		}
+	}
+
+	if(fmt ==  0){
+		if(dirp != nil)
+			dirp.put();
+		return "CD format not recognised";
+	}
+
+	if(chatty)
+		showdrec(stderr, fmt, dp);
+	if(blksize > Sectorsize){
+		dirp.put();
+		return "blocksize too big";
+	}
+	fp := iso(root);
+	root.xf.isplan9 = haveplan9;
+	fp.fmt = fmt;
+	fp.blksize = blksize;
+	fp.offset = 0;
+	fp.doffset = 0;
+	fp.d = dp;
+	root.qid.path = big dp.addr;
+	root.qid.qtype = QTDIR;
+	root.qid.vers = 0;
+	dirp.put();
+	dp = ref Drec;
+	if(getdrec(root, dp) >= 0){
+		s := dp.data;
+		n := len s;
+		if(n >= 7 && s[0] == byte 'S' && s[1] == byte 'P' && s[2] == byte 7 &&
+		   s[3] == byte 1 && s[4] == byte 16rBE && s[5] == byte 16rEF){
+			root.xf.issusp = 1;
+			root.xf.suspoff = int s[6];
+			n -= root.xf.suspoff;
+			s = s[root.xf.suspoff:];
+			while(n >= 4){
+				l := int s[2];
+				if(s[0] == byte 'E' && s[1] == byte 'R'){
+					if(int s[4] == 10 && eqs(s[8:18], "RRIP_1991A"))
+						root.xf.isrock = 1;
+					break;
+				} else if(s[0] == byte 'C' && s[1] == byte 'E' && int s[2] >= 28){
+					(s, n) = getcontin(root.xf.d, s);
+					continue;
+				} else if(s[0] == byte 'R' && s[1] == byte 'R'){
+					if(!norock)
+						root.xf.isrock = 1;
+					break;	# can skip search for ER
+				} else if(s[0] == byte 'S' && s[1] == byte 'T')
+					break;
+				s = s[l:];
+				n -= l;
+			}
+		}
+	}
+	if(root.xf.isrock)
+		chat("Rock Ridge...");
+	fp.offset = 0;
+	fp.doffset = 0;
+	return nil;
+}
+
+Xfile.clone(oldf: self ref Xfile, newf: ref Xfile)
+{
+	*newf = *oldf;
+	newf.ptr = nil;
+	newf.xf.incref();
+	ip := iso(oldf);
+	np := iso(newf);
+	*np = *ip;	# might not be right; shares ip.d
+}
+
+Xfile.walkup(f: self ref Xfile): string
+{
+	pf := Xfile.new();
+	ppf := Xfile.new();
+	e := walkup(f, pf, ppf);
+	pf.clunk();
+	ppf.clunk();
+	return e;
+}
+
+walkup(f, pf, ppf: ref Xfile): string
+{
+	e := opendotdot(f, pf);
+	if(e != nil)
+		return sys->sprint("can't open pf: %s", e);
+	paddr := iso(pf).d.addr;
+	if(iso(f).d.addr == paddr)
+		return nil;
+	e = opendotdot(pf, ppf);
+	if(e != nil)
+		return sys->sprint("can't open ppf: %s", e);
+	d := ref Drec;
+	while(getdrec(ppf, d) >= 0){
+		if(d.addr == paddr){
+			newdrec(f, d);
+			f.qid.path = big paddr;
+			f.qid.qtype = QTDIR;
+			f.qid.vers = 0;
+			return nil;
+		}
+	}
+	return "can't find addr of ..";
+}
+
+Xfile.walk(f: self ref Xfile, name: string): string
+{
+	ip := iso(f);
+	if(!f.xf.isplan9){
+		for(i := 0; i < len name; i++)
+			if(name[i] == ';')
+				break;
+		if(i >= Maxname)
+			i = Maxname-1;
+		name = name[0:i];
+	}
+	if(chatty)
+		chat(sys->sprint("%d \"%s\"...", len name, name));
+	ip.offset = 0;
+	dir := ref Dir;
+	d := ref Drec;
+	while(getdrec(f, d) >= 0) {
+		dvers := rzdir(f.xf, dir, ip.fmt, d);
+		if(name != dir.name)
+			continue;
+		newdrec(f, d);
+		f.qid.path = dir.qid.path;
+		f.qid.qtype = dir.qid.qtype;
+		f.qid.vers = dir.qid.vers;
+		if(dvers){
+			# versions ignored
+		}
+		return nil;
+	}
+	return Enonexist;
+}
+
+Xfile.open(f: self ref Xfile, mode: int): string
+{
+	if(mode != Sys->OREAD)
+		return Eperm;
+	ip := iso(f);
+	ip.offset = 0;
+	ip.doffset = 0;
+	return nil;
+}
+
+Xfile.create(nil: self ref Xfile, nil: string, nil: int, nil: int): string
+{
+	return Eperm;
+}
+
+Xfile.readdir(f: self ref Xfile, buf: array of byte, offset: int, count: int): (int, string)
+{
+	ip := iso(f);
+	d := ref Dir;
+	drec := ref Drec;
+	if(offset < ip.doffset){
+		ip.offset = 0;
+		ip.doffset = 0;
+	}
+	rcnt := 0;
+	while(rcnt < count && getdrec(f, drec) >= 0){
+		if(len drec.name == 1){
+			if(drec.name[0] == byte 0)
+				continue;
+			if(drec.name[0] == byte 1)
+				continue;
+		}
+		rzdir(f.xf, d, ip.fmt, drec);
+		d.qid.vers = f.qid.vers;
+		a := styx->packdir(*d);
+		if(ip.doffset < offset){
+			ip.doffset += len a;
+			continue;
+		}
+		if(rcnt+len a > count)
+			break;
+		buf[rcnt:] = a;		# BOTCH: copy
+		rcnt += len a;
+	}
+	ip.doffset += rcnt;
+	return (rcnt, nil);
+}
+
+Xfile.read(f: self ref Xfile, buf: array of byte, offset: int, count: int): (int, string)
+{
+	ip := iso(f);
+	if(offset >= ip.d.size)
+		return (0, nil);
+	if(offset+count > ip.d.size)
+		count = ip.d.size - offset;
+	addr := (ip.d.addr+ip.d.attrlen)*ip.blksize + offset;
+	o := addr % Sectorsize;
+	addr /= Sectorsize;
+	if(chatty)
+		chat(sys->sprint("d.addr=0x%x, addr=0x%x, o=0x%x...", ip.d.addr, addr, o));
+	n := Sectorsize - o;
+	rcnt := 0;
+	while(count > 0){
+		if(n > count)
+			n = count;
+		p := Block.get(f.xf.d, addr);
+		if(p == nil)
+			return (-1, "i/o error");
+		buf[rcnt:] = p.data[o:o+n];
+		p.put();
+		count -= n;
+		rcnt += n;
+		addr++;
+		o = 0;
+		n = Sectorsize;
+	}
+	return (rcnt, nil);
+}
+
+Xfile.write(nil: self ref Xfile, nil: array of byte, nil: int, nil: int): (int, string)
+{
+	return (-1, Eperm);
+}
+
+Xfile.clunk(f: self ref Xfile)
+{
+	f.ptr = nil;
+}
+
+Xfile.remove(nil: self ref Xfile): string
+{
+	return Eperm;
+}
+
+Xfile.stat(f: self ref Xfile): (ref Dir, string)
+{
+	ip := iso(f);
+	d := ref Dir;
+	rzdir(f.xf, d, ip.fmt, ip.d);
+	d.qid.vers = f.qid.vers;
+	if(d.qid.path==f.xf.rootqid.path){
+		d.qid.path = big 0;
+		d.qid.qtype = QTDIR;
+	}
+	return (d, nil);
+}
+
+Xfile.wstat(nil: self ref Xfile, nil: ref Dir): string
+{
+	return Eperm;
+}
+
+Xfs.new(d: ref Device): ref Xfs
+{
+	xf := ref Xfs;
+	xf.inuse = 1;
+	xf.d = d;
+	xf.isplan9 = 0;
+	xf.issusp = 0;
+	xf.isrock = 0;
+	xf.suspoff = 0;
+	xf.ptr = 0;
+	xf.rootqid = Qid(big 0, 0, QTDIR);
+	return xf;
+}
+
+Xfs.incref(xf: self ref Xfs)
+{
+	xf.inuse++;
+}
+
+Xfs.decref(xf: self ref Xfs)
+{
+	xf.inuse--;
+	if(xf.inuse == 0){
+		if(xf.d != nil)
+			xf.d.detach();
+	}
+}
+
+showdrec(fd: ref Sys->FD, fmt: int, d: ref Drec)
+{
+	if(d.reclen == 0)
+		return;
+	sys->fprint(fd, "%d %d %d %d ",
+		d.reclen, d.attrlen, d.addr, d.size);
+	sys->fprint(fd, "%s 0x%2.2x %d %d %d ",
+		rdate(d.date, fmt), d.flags,
+		d.unitsize, d.gapsize, d.vseqno);
+	sys->fprint(fd, "%d %s", len d.name, nstr(d.name));
+	syslen := len d.data;
+	if(syslen != 0)
+		sys->fprint(fd, " %s", nstr(d.data));
+	sys->fprint(fd, "\n");
+}
+
+newdrec(f: ref Xfile, dp: ref Drec)
+{
+	x := iso(f);
+	n := ref Isofile;
+	n.fmt = x.fmt;
+	n.blksize = x.blksize;
+	n.offset = 0;
+	n.doffset = 0;
+	n.d = dp;
+	f.ptr = n;
+}
+
+getdrec(f: ref Xfile, d: ref Drec): int
+{
+	if(f.ptr == nil)
+		return -1;
+	boff := 0;
+	ip := iso(f);
+	size := ip.d.size;
+	while(ip.offset<size){
+		addr := (ip.d.addr+ip.d.attrlen)*ip.blksize + ip.offset;
+		boff = addr % Sectorsize;
+		if(boff > Sectorsize-34){
+			ip.offset += Sectorsize-boff;
+			continue;
+		}
+		p := Block.get(f.xf.d, addr/Sectorsize);
+		if(p == nil)
+			return -1;
+		nb := int p.data[boff];
+		if(nb >= 34) {
+			convM2Drec(p.data[boff:], d, ip.fmt=='r');
+			#chat(sys->sprint("off %d", ip.offset));
+			#showdrec(stderr, ip.fmt, d);
+			p.put();
+			ip.offset += nb + (nb&1);
+			return 0;
+		}
+		p.put();
+		p = nil;
+		ip.offset += Sectorsize-boff;
+	}
+	return -1;
+}
+
+# getcontin returns a slice of the Iobuf, valid until next i/o call
+getcontin(d: ref Device, a: array of byte): (array of byte, int)
+{
+	bn := l32(a[4:]);
+	off := l32(a[12:]);
+	n := l32(a[20:]);
+	p := Block.get(d, bn);
+	if(p == nil)
+		return (nil, 0);
+	return (p.data[off:off+n], n);
+}
+
+iso(f: ref Xfile): ref Isofile
+{
+	if(f.ptr == nil){
+		f.ptr = ref Isofile;
+		f.ptr.d = ref Drec;
+	}
+	return f.ptr;
+}
+
+opendotdot(f: ref Xfile, pf: ref Xfile): string
+{
+	d := ref Drec;
+	ip := iso(f);
+	ip.offset = 0;
+	if(getdrec(f, d) < 0)
+		return "opendotdot: getdrec(.) failed";
+	if(len d.name != 1 || d.name[0] != byte 0)
+		return "opendotdot: no . entry";
+	if(d.addr != ip.d.addr)
+		return "opendotdot: bad . address";
+	if(getdrec(f, d) < 0)
+		return "opendotdot: getdrec(..) failed";
+	if(len d.name != 1 || d.name[0] != byte 1)
+		return "opendotdot: no .. entry";
+
+	pf.xf = f.xf;
+	pip := iso(pf);
+	pip.fmt = ip.fmt;
+	pip.blksize = ip.blksize;
+	pip.offset = 0;
+	pip.doffset = 0;
+	pip.d = d;
+	return nil;
+}
+
+rzdir(fs: ref Xfs, d: ref Dir, fmt: int, dp: ref Drec): int
+{
+	Hmode, Hname: con 1<<iota;
+	vers := -1;
+	have := 0;
+	d.qid.path = big dp.addr;
+	d.qid.vers = 0;
+	d.qid.qtype = QTFILE;
+	n := len dp.name;
+	if(n == 1) {
+		case int dp.name[0] {
+		0 => d.name = "."; have |= Hname;
+		1 =>	d.name = ".."; have |= Hname;
+		* =>	d.name = ""; d.name[0] = tolower(int dp.name[0]);
+		}
+	} else {
+		if(fmt == 'J'){	# Joliet, 16-bit Unicode
+			d.name = "";
+			for(i:=0; i<n; i+=2){
+				r := (int dp.name[i]<<8) | int dp.name[i+1];
+				d.name[len d.name] = r;
+			}
+		}else{
+			if(n >= Maxname)
+				n = Maxname-1;
+			d.name = "";
+			for(i:=0; i<n && int dp.name[i] != '\r'; i++)
+				d.name[i] = tolower(int dp.name[i]);
+		}
+	}
+
+	if(fs.isplan9 && dp.reclen>34+len dp.name) {
+		#
+		# get gid, uid, mode and possibly name
+		# from plan9 directory extension
+		#
+		s := dp.data;
+		n = int s[0];
+		if(n)
+			d.name = string s[1:1+n];
+		l := 1+n;
+		n = int s[l++];
+		d.uid = string s[l:l+n];
+		l += n;
+		n = int s[l++];
+		d.gid = string s[l:l+n];
+		l += n;
+		if(l & 1)
+			l++;
+		d.mode = l32(s[l:]);
+		if(d.mode & DMDIR)
+			d.qid.qtype = QTDIR;
+	} else {
+		d.mode = 8r444;
+		case fmt {
+		'z' =>
+			if(fs.isrock)
+				d.gid = "ridge";
+			else
+				d.gid = "iso";
+		'r' =>
+			d.gid = "sierra";
+		'J' =>
+			d.gid = "joliet";
+		* =>
+			d.gid = "???";
+		}
+		flags := dp.flags;
+		if(flags & 2){
+			d.qid.qtype = QTDIR;
+			d.mode |= DMDIR|8r111;
+		}
+		d.uid = "cdrom";
+		for(i := 0; i < len d.name; i++)
+			if(d.name[i] == ';') {
+				vers = int string d.name[i+1:];	# inefficient
+				d.name = d.name[0:i];	# inefficient
+				break;
+			}
+		n = len dp.data - fs.suspoff;
+		if(fs.isrock && n >= 4){
+			s := dp.data[fs.suspoff:];
+			nm := 0;
+			while(n >= 4 && have != (Hname|Hmode)){
+				l := int s[2];
+				if(s[0] == byte 'P' && s[1] == byte 'X' && s[3] == byte 1){
+					# posix file attributes
+					mode := l32(s[4:12]);
+					d.mode = mode & 8r777;
+					if((mode & 8r170000) == 8r0040000){
+						d.mode |= DMDIR;
+						d.qid.qtype = QTDIR;
+					}
+					have |= Hmode;
+				} else if(s[0] == byte 'N' && s[1] == byte 'M' && s[3] == byte 1){
+					# alternative name
+					flags = int s[4];
+					if((flags & ~1) == 0){
+						if(nm == 0){
+							d.name = string s[5:l];
+							nm = 1;
+						} else
+							d.name += string s[5:l];
+						if(flags == 0)
+							have |= Hname;	# no more
+					}
+				} else if(s[0] == byte 'C' && s[1] == byte 'E' && int s[2] >= 28){
+					(s, n) = getcontin(fs.d, s);
+					continue;
+				} else if(s[0] == byte 'S' && s[1] == byte 'T')
+					break;
+				n -= l;
+				s = s[l:];
+			}
+		}
+	}
+	d.length = big 0;
+	if((d.mode & DMDIR) == 0)
+		d.length = big dp.size;
+	d.dtype = 0;
+	d.dev = 0;
+	d.atime = dp.time;
+	d.mtime = d.atime;
+	return vers;
+}
+
+convM2Drec(a: array of byte, d: ref Drec, highsierra: int)
+{
+	d.reclen = int a[0];
+	d.attrlen = int a[1];
+	d.addr = int l32(a[2:10]);
+	d.size = int l32(a[10:18]);
+	d.time = gtime(a[18:24]);
+	d.date = array[7] of byte;
+	d.date[0:] = a[18:25];
+	if(highsierra){
+		d.tzone = 0;
+		d.flags = int a[24];
+		d.unitsize = 0;
+		d.gapsize = 0;
+		d.vseqno = 0;
+	} else {
+		d.tzone = int a[24];
+		d.flags = int a[25];
+		d.unitsize = int a[26];
+		d.gapsize = int a[27];
+		d.vseqno = l32(a[28:32]);
+	}
+	n := int a[32];
+	d.name = array[n] of byte;
+	d.name[0:] = a[33:33+n];
+	n += 33;
+	if(n & 1)
+		n++;	# check this
+	syslen := d.reclen - n;
+	if(syslen > 0){
+		d.data = array[syslen] of byte;
+		d.data[0:] = a[n:n+syslen];
+	} else
+		d.data = nil;
+}
+
+nstr(p: array of byte): string
+{
+	q := "";
+	n := len p;
+	for(i := 0; i < n; i++){
+		if(int p[i] == '\\')
+			q[len q] = '\\';
+		if(' ' <= int p[i] && int p[i] <= '~')
+			q[len q] = int p[i];
+		else
+			q += sys->sprint("\\%2.2ux", int p[i]);
+	}
+	return q;
+}
+
+rdate(p: array of byte, fmt: int): string
+{
+	c: int;
+
+	s := sys->sprint("%2.2d.%2.2d.%2.2d %2.2d:%2.2d:%2.2d",
+		int p[0], int p[1], int p[2], int p[3], int p[4], int p[5]);
+	if(fmt == 'z'){
+		htz := int p[6];
+		if(htz >= 128){
+			htz = 256-htz;
+			c = '-';
+		}else
+			c = '+';
+		s += sys->sprint(" (%c%.1f)", c, real htz/2.0);
+	}
+	return s;
+}
+
+dmsize := array[] of {
+	31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
+};
+
+dysize(y: int): int
+{
+	if((y%4) == 0)
+		return 366;
+	return 365;
+}
+
+gtime(p: array of byte): int	# yMdhms
+{
+	y:=int p[0]; M:=int p[1]; d:=int p[2];
+	h:=int p[3]; m:=int p[4]; s:=int p[5];;
+	if(y < 70)
+		return 0;
+	if(M < 1 || M > 12)
+		return 0;
+	if(d < 1 || d > dmsize[M-1])
+		return 0;
+	if(h > 23)
+		return 0;
+	if(m > 59)
+		return 0;
+	if(s > 59)
+		return 0;
+	y += 1900;
+	t := 0;
+	for(i:=1970; i<y; i++)
+		t += dysize(i);
+	if(dysize(y)==366 && M >= 3)
+		t++;
+	M--;
+	while(M-- > 0)
+		t += dmsize[M];
+	t += d-1;
+	t = 24*t + h;
+	t = 60*t + m;
+	t = 60*t + s;
+	return t;
+}
+
+l16(p: array of byte): int
+{
+	v := (int p[1]<<8)| int p[0];
+	if (v >= 16r8000)
+		v -= 16r10000;
+	return v;
+}
+
+l32(p: array of byte): int
+{
+	return (((((int p[3]<<8)| int p[2])<<8)| int p[1])<<8)| int p[0];
+}
+
+eqs(a: array of byte, b: string): int
+{
+	if(len a != len b)
+		return 0;
+	for(i := 0; i < len a; i++)
+		if(int a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+tolower(c: int): int
+{
+	if(c >= 'A' && c <= 'Z')
+		return c-'A' + 'a';
+	return c;
+}
+
+#
+# I/O buffers
+#
+
+Device: adt {
+	inuse:	int;	# attach count
+	name:	string;	# of underlying file
+	fd:	ref Sys->FD;
+	sectorsize:	int;
+	qid:	Sys->Qid;	# (qid,dtype,dev) identify uniquely
+	dtype:	int;
+	dev:	int;
+
+	detach:	fn(nil: self ref Device);
+};
+
+Block: adt {
+	dev:	ref Device;
+	addr:	int;
+	data:	array of byte;
+
+	# internal
+	next:	cyclic ref Block;
+	prev:	cyclic ref Block;
+	busy:	int;
+
+	get:	fn(nil: ref Device, addr: int): ref Block;
+	put:	fn(nil: self ref Block);
+};
+
+devices:	list of ref Device;
+
+NIOB:	con 100;	# for starters
+HIOB:	con 127;	# prime
+
+hiob := array[HIOB] of list of ref Block;	# hash buckets
+iohead:	ref Block;
+iotail:	ref Block;
+bufsize := 0;
+
+iobufinit(bsize: int)
+{
+	bufsize = bsize;
+	for(i:=0; i<NIOB; i++)
+		newblock();
+}
+
+newblock(): ref Block
+{
+	p := ref Block;
+	p.busy = 0;
+	p.addr = -1;
+	p.dev = nil;
+	p.data = array[bufsize] of byte;
+	p.next = iohead;
+	if(iohead != nil)
+		iohead.prev = p;
+	iohead = p;
+	if(iotail == nil)
+		iotail = p;
+	return p;
+}
+
+Block.get(dev: ref Device, addr: int): ref Block
+{
+	p: ref Block;
+
+	dh := hiob[addr%HIOB:];
+	for(l := dh[0]; l != nil; l = tl l) {
+		p = hd l;
+		if(p.addr == addr && p.dev == dev) {
+			p.busy++;
+			return p;
+		}
+	}
+	# Find a non-busy buffer from the tail
+	for(p = iotail; p != nil && p.busy; p = p.prev)
+		;
+	if(p == nil)
+		p = newblock();
+
+	# Delete from hash chain
+	if(p.addr >= 0) {
+		hp := hiob[p.addr%HIOB:];
+		l = nil;
+		for(f := hp[0]; f != nil; f = tl f)
+			if(hd f != p)
+				l = (hd f) :: l;
+		hp[0] = l;
+	}
+
+	# Hash and fill
+	p.addr = addr;
+	p.dev = dev;
+	p.busy++;
+	sys->seek(dev.fd, big addr*big dev.sectorsize, 0);
+	if(sys->read(dev.fd, p.data, dev.sectorsize) != dev.sectorsize){
+		p.addr = -1;	# stop caching
+		p.put();
+		purge(dev);
+		return nil;
+	}
+	dh[0] = p :: dh[0];
+	return p;
+}
+
+Block.put(p: self ref Block)
+{
+	p.busy--;
+	if(p.busy < 0)
+		panic("Block.put");
+
+	if(p == iohead)
+		return;
+
+	# Link onto head for lru
+	if(p.prev != nil) 
+		p.prev.next = p.next;
+	else
+		iohead = p.next;
+
+	if(p.next != nil)
+		p.next.prev = p.prev;
+	else
+		iotail = p.prev;
+
+	p.prev = nil;
+	p.next = iohead;
+	iohead.prev = p;
+	iohead = p;
+}
+
+purge(dev: ref Device)
+{
+	for(i := 0; i < HIOB; i++){
+		l := hiob[i];
+		hiob[i] = nil;
+		for(; l != nil; l = tl l){	# reverses bucket's list, but never mind
+			p := hd l;
+			if(p.dev == dev)
+				p.busy = 0;
+			else
+				hiob[i] = p :: hiob[i];
+		}
+	}
+}
+
+devattach(name: string, mode: int, sectorsize: int): (ref Device, string)
+{
+	if(sectorsize > bufsize)
+		return (nil, "sector size too big");
+	fd := sys->open(name, mode);
+	if(fd == nil)
+		return(nil, sys->sprint("%s: can't open: %r", name));
+	(rc, dir) := sys->fstat(fd);
+	if(rc < 0)
+		return (nil, sys->sprint("%r"));
+	for(dl := devices; dl != nil; dl = tl dl){
+		d := hd dl;
+		if(d.qid.path != dir.qid.path || d.qid.vers != dir.qid.vers)
+			continue;
+		if(d.dtype != dir.dtype || d.dev != dir.dev)
+			continue;
+		d.inuse++;
+		if(chatty)
+			sys->print("inuse=%d, \"%s\", dev=%H...\n", d.inuse, d.name, d.fd);
+		return (d, nil);
+	}
+	if(chatty)
+		sys->print("alloc \"%s\", dev=%H...\n", name, fd);
+	d := ref Device;
+	d.inuse = 1;
+	d.name = name;
+	d.qid = dir.qid;
+	d.dtype = dir.dtype;
+	d.dev = dir.dev;
+	d.fd = fd;
+	d.sectorsize = sectorsize;
+	devices = d :: devices;
+	return (d, nil);
+}
+
+Device.detach(d: self ref Device)
+{
+	d.inuse--;
+	if(d.inuse < 0)
+		panic("putxdata");
+	if(chatty)
+		sys->print("decref=%d, \"%s\", dev=%H...\n", d.inuse, d.name, d.fd);
+	if(d.inuse == 0){
+		if(chatty)
+			sys->print("purge...\n");
+		purge(d);
+		dl := devices;
+		devices = nil;
+		for(; dl != nil; dl = tl dl)
+			if((hd dl) != d)
+				devices = (hd dl) :: devices;
+	}
+}
+
+panic(s: string)
+{
+	sys->print("panic: %s\n", s);
+	a: array of byte;
+	a[5] = byte 0; # trap
+}
--- /dev/null
+++ b/appl/cmd/9export.b
@@ -1,0 +1,165 @@
+implement P9export;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+include "keyring.m";
+include "security.m";
+include "factotum.m";
+include "encoding.m";
+include "arg.m";
+
+P9export: module
+{
+	init:	 fn(nil: ref Draw->Context, nil: list of string);
+};
+
+factotumfile := "/mnt/factotum/rpc";
+
+fail(status, msg: string)
+{
+	sys->fprint(sys->fildes(2), "9export: %s\n", msg);
+	raise "fail:"+status;
+}
+
+nomod(mod: string)
+{
+	fail("load", sys->sprint("can't load %s: %r", mod));
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+
+	arg->init(args);
+	arg->setusage("9export [-aA9] [-k keyspec] [-e enc digest]");
+	cryptalg := "";	# will be rc4_256 sha1
+	keyspec := "";
+	noauth := 0;
+	xflag := Sys->EXPWAIT;
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>
+			xflag = Sys->EXPASYNC;
+		'A' =>
+			noauth = 1;
+		'e' =>
+			cryptalg = arg->earg();
+			if(cryptalg == "clear")
+				cryptalg = nil;
+		'k' =>
+			keyspec = arg->earg();
+		'9' =>
+			;
+		*   =>
+			arg->usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	sys->pctl(Sys->FORKFD|Sys->FORKNS, nil);
+
+	fd := sys->fildes(0);
+
+	secret: array of byte;
+	if(noauth == 0){
+		factotum := load Factotum Factotum->PATH;
+		if(factotum == nil)
+			nomod(Factotum->PATH);
+		factotum->init();
+		facfd := sys->open(factotumfile, Sys->ORDWR);
+		if(facfd == nil)
+			fail("factotum", sys->sprint("can't open %s: %r", factotumfile));
+		ai := factotum->proxy(fd, facfd, "proto=p9any role=server "+keyspec);
+		if(ai == nil)
+			fail("auth", sys->sprint("can't authenticate 9export: %r"));
+		secret = ai.secret;
+	}
+
+	# read tree; it's a Plan 9 bug that there's no reliable delimiter
+	btree := array[2048] of byte;
+	n := sys->read(fd, btree, len btree);
+	if(n <= 0)
+		fail("tree", sys->sprint("can't read tree: %r"));
+	tree := string btree[0:n];
+	if(sys->chdir(tree) < 0){
+		sys->fprint(fd, "chdir(%d:\"%s\"): %r", n, tree);
+		fail("tree", sys->sprint("bad tree: %s", tree));
+	}
+	if(sys->write(fd, array of byte "OK", 2) != 2)
+		fail("tree", sys->sprint("can't OK tree: %r"));
+	impo := array[2048] of byte;
+	for(n = 0; n < len impo; n++)
+		if(sys->read(fd, impo[n:], 1) != 1)
+			fail("impo", sys->sprint("can't read impo: %r"));
+		else if(impo[n] == byte 0 || impo[n] == byte '\n')
+			break;
+	if(n < 4 || string impo[0:4] != "impo")
+		fail("impo", "wasn't impo: possibly old import/cpu");
+	if(noauth == 0 && cryptalg != nil){
+		if(secret == nil)
+			fail("import", "didn't establish shared secret");
+		random := load Random Random->PATH;
+		if(random == nil)
+			nomod(Random->PATH);
+		kr := load Keyring Keyring->PATH;
+		if(kr == nil)
+			nomod(Keyring->PATH);
+		ssl := load SSL SSL->PATH;
+		if(ssl == nil)
+			nomod(SSL->PATH);
+		base64 := load Encoding Encoding->BASE64PATH;
+		if(base64 == nil)
+			nomod(Encoding->BASE64PATH);
+		key := array[16] of byte;	# myrand[4] secret[8] hisrand[4]
+		key[0:] = random->randombuf(Random->ReallyRandom, 4);
+		ns := len secret;
+		if(ns > 8)
+			ns = 8;
+		key[12:] = secret[0:ns];
+		if(sys->write(fd, key[12:], 4) != 4)
+			fail("import", sys->sprint("can't write key to remote: %r"));
+		if(sys->readn(fd, key, 4) != 4)
+			fail("import", sys->sprint("can't read remote key: %r"));
+		digest := array[Keyring->SHA1dlen] of byte;
+		kr->sha1(key, len key, digest, nil);
+		err: string;
+		(fd, err) = pushssl(fd, base64->dec(S(digest[10:20])), base64->dec(S(digest[0:10])), cryptalg);
+		if(err != nil)
+			fail("import", sys->sprint("can't push security layer: %s", err));
+	}
+	if(sys->export(fd, ".", xflag) < 0)
+		fail("export", sys->sprint("can't export %s: %r", tree));
+}
+
+S(a: array of byte): string
+{
+	s := "";
+	for(i:=0; i<len a; i++)
+		s += sys->sprint("%.2ux", int a[i]);
+	return s;
+}
+
+pushssl(fd: ref Sys->FD, secretin, secretout: array of byte, alg: string): (ref Sys->FD, string)
+{
+	ssl := load SSL SSL->PATH;
+	if(ssl == nil)
+		nomod(SSL->PATH);
+
+	(err, c) := ssl->connect(fd);
+	if(err != nil)
+		return (nil, "can't connect ssl: " + err);
+
+	err = ssl->secret(c, secretin, secretout);
+	if(err != nil)
+		return (nil, "can't write secret: " + err);
+	if(sys->fprint(c.cfd, "alg %s", alg) < 0)
+		return (nil, sys->sprint("can't push algorithm %s: %r", alg));
+
+	return (c.dfd, nil);
+}
--- /dev/null
+++ b/appl/cmd/9srvfs.b
@@ -1,0 +1,99 @@
+implement P9srvfs;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "sh.m";
+	sh: Sh;
+
+include "arg.m";
+
+P9srvfs: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	if(str == nil)
+		nomod(String->PATH);
+
+	perm := 8r600;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	arg->init(args);
+	arg->setusage("9srvfs [-p perm] name path|{command}");
+	while((o := arg->opt()) != 0)
+		case o {
+		'p' =>
+			s := arg->earg();
+			if(s == nil)
+				arg->usage();
+			(perm, s) = str->toint(s, 8);
+			if(s != nil)
+				arg->usage();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 2)
+		arg->usage();
+	arg = nil;
+
+	srvname := hd args;
+	args = tl args;
+	dest := hd args;
+	if(dest == nil)
+		dest = ".";
+	iscmd := dest[0] == '{' && dest[len dest-1] == '}';
+	if(!iscmd){		# quick check before creating service file
+		(ok, d) := sys->stat(dest);
+		if(ok < 0)
+			error(sys->sprint("can't stat %s: %r", dest));
+		if((d.mode & Sys->DMDIR) == 0)
+			error(sys->sprint("%s: not a directory", dest));
+	}else{
+		sh = load Sh Sh->PATH;
+		if(sh == nil)
+			nomod(Sh->PATH);
+	}
+	srvfd := sys->create("/srv/"+srvname, Sys->ORDWR, perm);
+	if(srvfd == nil)
+		error(sys->sprint("can't create /srv/%s: %r", srvname));
+	if(iscmd){
+		sync := chan of int;
+		spawn runcmd(sh, ctxt, dest :: nil, srvfd, sync);
+		<-sync;
+	}else{
+		if(sys->export(srvfd, dest, Sys->EXPWAIT) < 0)
+			error(sys->sprint("export failed: %r"));
+	}
+}
+
+error(msg: string)
+{
+	sys->fprint(sys->fildes(2), "9srvfs: %s\n", msg);
+	raise "fail:error";
+}
+
+nomod(mod: string)
+{
+	error(sys->sprint("can't load %s: %r", mod));
+}
+
+runcmd(sh: Sh, ctxt: ref Draw->Context, argv: list of string, stdin: ref Sys->FD, sync: chan of int)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	stdin = nil;
+	sync <-= 0;
+	sh->run(ctxt, argv);
+}
--- /dev/null
+++ b/appl/cmd/9win.b
@@ -1,0 +1,453 @@
+implement Ninewin;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Image, Display, Pointer: import draw;
+include "arg.m";
+include "keyboard.m";
+include "tk.m";
+include "wmclient.m";
+	wmclient: Wmclient;
+	Window: import wmclient;
+include "sh.m";
+	sh: Sh;
+
+# run a p9 graphics program (default rio) under inferno wm,
+# making available to it:
+# /dev/winname - naming the current inferno window (changing on resize)
+# /dev/mouse - pointer file + resize events; write to change position
+# /dev/cursor - change appearance of cursor.
+# /dev/draw - inferno draw device
+# /dev/cons - read keyboard events, write to 9win stdout.
+
+Ninewin: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+winname: string;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	size := Draw->Point(500, 500);
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	wmclient = load Wmclient Wmclient->PATH;
+	wmclient->init();
+	sh = load Sh Sh->PATH;
+
+	buts := Wmclient->Resize;
+	if(ctxt == nil){
+		ctxt = wmclient->makedrawcontext();
+		buts = Wmclient->Plain;
+	}
+	arg := load Arg Arg->PATH;
+	arg->init(argv);
+	arg->setusage("9win [-s] [-x width] [-y height]");
+	exportonly := 0;
+	while(((opt := arg->opt())) != 0){
+		case opt {
+		's' =>
+			exportonly = 1;
+		'x' =>
+			size.x = int arg->earg();
+		'y' =>
+			size.y = int arg->earg();
+		* =>
+			arg->usage();
+		}
+	}
+	if(size.x < 1 || size.y < 1)
+		arg->usage();
+	argv = arg->argv();
+	if(argv != nil && hd argv == "-s"){
+		exportonly = 1;
+		argv = tl argv;
+	}
+	if(argv == nil && !exportonly)
+		argv = "rio" :: nil;
+	if(argv != nil && exportonly){
+		sys->fprint(sys->fildes(2), "9win: no command allowed with -s flag\n");
+		raise "fail:usage";
+	}
+	title := "9win";
+	if(!exportonly)
+		title += " " + hd argv;
+	w := wmclient->window(ctxt, title, buts);
+	w.reshape(((0, 0), size));
+	w.onscreen(nil);
+	if(w.image == nil){
+		sys->fprint(sys->fildes(2), "9win: cannot get image to draw on\n");
+		raise "fail:no window";
+	}
+
+	sys->pctl(Sys->FORKNS|Sys->NEWPGRP, nil);
+	ld := "/n/9win";
+	if(sys->bind("#s", ld, Sys->MREPL) == -1 &&
+			sys->bind("#s", ld = "/n/local", Sys->MREPL) == -1){
+		sys->fprint(sys->fildes(2), "9win: cannot bind files: %r\n");
+		raise "fail:error";
+	}
+	w.startinput("kbd" :: "ptr" :: nil);
+	spawn ptrproc(rq := chan of Sys->Rread, ptr := chan[10] of ref Pointer, reshape := chan[1] of int);
+
+		
+	fwinname := sys->file2chan(ld, "winname");
+	fconsctl := sys->file2chan(ld, "consctl");
+	fcons := sys->file2chan(ld, "cons");
+	fmouse := sys->file2chan(ld, "mouse");
+	fcursor := sys->file2chan(ld, "cursor");
+	if(!exportonly){
+		spawn run(sync := chan of string, w.ctl, ld, argv);
+		if((e := <-sync) != nil){
+			sys->fprint(sys->fildes(2), "9win: %s", e);
+			raise "fail:error";
+		}
+	}
+	spawn serveproc(w, rq, fwinname, fconsctl, fcons, fmouse, fcursor);
+	if(!exportonly){
+		# handle events synchronously so that we don't get a "killed" message
+		# from the shell.
+		handleevents(w, ptr, reshape);
+	}else{
+		spawn handleevents(w, ptr, reshape);
+		sys->bind(ld, "/dev", Sys->MBEFORE);
+		export(sys->fildes(0), w.ctl);
+	}
+}
+
+handleevents(w: ref Window, ptr: chan of ref Pointer, reshape: chan of int)
+{
+	for(;;)alt{
+	c := <-w.ctxt.ctl or
+	c = <-w.ctl =>
+		e := w.wmctl(c);
+		if(e != nil)
+			sys->fprint(sys->fildes(2), "9win: ctl error: %s\n", e);
+		if(e == nil && c != nil && c[0] == '!'){
+			alt{
+			reshape <-= 1 =>
+				;
+			* =>
+				;
+			}
+			winname = nil;
+		}
+	p := <-w.ctxt.ptr =>
+		if(w.pointer(*p) == 0){
+			# XXX would block here if client isn't reading mouse... but we do want to
+			# extert back-pressure, which conflicts.
+			alt{
+			ptr <-= p =>
+				;
+			* =>
+				; # sys->fprint(sys->fildes(2), "9win: discarding mouse event\n");
+			}
+		}
+	}
+}
+
+serveproc(w: ref Window, mouserq: chan of Sys->Rread, fwinname, fconsctl, fcons, fmouse, fcursor: ref Sys->FileIO)
+{
+	winid := 0;
+	krc: list of Sys->Rread;
+	ks: string;
+
+	for(;;)alt {
+	c := <-w.ctxt.kbd =>
+		ks[len ks] = inf2p9key(c);
+		if(krc != nil){
+			hd krc <-= (array of byte ks, nil);
+			ks = nil;
+			krc = tl krc;
+		}
+	(nil, d, nil, wc) := <-fcons.write =>
+		if(wc != nil){
+			sys->write(sys->fildes(1), d, len d);
+			wc <-= (len d, nil);
+		}
+	(nil, nil, nil, rc) := <-fcons.read =>
+		if(rc != nil){
+			if(ks != nil){
+				rc <-= (array of byte ks, nil);
+				ks = nil;
+			}else
+				krc = rc :: krc;
+		}
+	(offset, nil, nil, rc) := <-fwinname.read =>
+		if(rc != nil){
+			if(winname == nil){
+				winname = sys->sprint("noborder.9win.%d", winid++);
+				if(w.image.name(winname, 1) == -1){
+					sys->fprint(sys->fildes(2), "9win: namewin %q failed: %r", winname);
+					rc <-= (nil, "namewin failure");
+					break;
+				}
+			}
+			d := array of byte winname;
+			if(offset < len d)
+				d = d[offset:];
+			else
+				d = nil;
+			rc <-= (d, nil);
+		}
+	(nil, nil, nil, wc) := <-fwinname.write =>
+		if(wc != nil)
+			wc <-= (-1, "permission denied");
+	(nil, nil, nil, rc) := <-fconsctl.read =>
+		if(rc != nil)
+			rc <-= (nil, "permission denied");
+	(nil, d, nil, wc) := <-fconsctl.write =>
+		if(wc != nil){
+			if(string d != "rawon")
+				wc <-= (-1, "cannot change console mode");
+			else
+				wc <-= (len d, nil);
+		}
+	(nil, nil, nil, rc) := <-fmouse.read =>
+		if(rc != nil)
+			mouserq <-= rc;
+	(nil, d, nil, wc) := <-fmouse.write =>
+		if(wc != nil){
+			e := cursorset(w, string d);
+			if(e == nil)
+				wc <-= (len d, nil);
+			else
+				wc <-= (-1, e);
+		}
+	(nil, nil, nil, rc) := <-fcursor.read =>
+		if(rc != nil)
+			rc <-= (nil, "permission denied");
+	(nil, d, nil, wc) := <-fcursor.write =>
+		if(wc != nil){
+			e := cursorswitch(w, d);
+			if(e == nil)
+				wc <-= (len d, nil);
+			else
+				wc <-= (-1, e);
+		}
+	}
+}
+
+ptrproc(rq: chan of Sys->Rread, ptr: chan of ref Pointer, reshape: chan of int)
+{
+	rl: list of Sys->Rread;
+	c := ref Pointer(0, (0, 0), 0);
+	for(;;){
+		ch: int;
+		alt{
+		p := <-ptr =>
+			ch = 'm';
+			c = p;
+		<-reshape =>
+			ch = 'r';
+		rc := <-rq =>
+			rl  = rc :: rl;
+			continue;
+		}
+		if(rl == nil)
+			rl = <-rq :: rl;
+		hd rl <-= (sys->aprint("%c%11d %11d %11d %11d ", ch, c.xy.x, c.xy.y, c.buttons, c.msec), nil);
+		rl = tl rl;
+	}
+}
+
+cursorset(w: ref Window, m: string): string
+{
+	if(m == nil || m[0] != 'm')
+		return "invalid mouse message";
+	x := int m[1:];
+	for(i := 1; i < len m; i++)
+		if(m[i] == ' '){
+			while(m[i] == ' ')
+				i++;
+			break;
+		}
+	if(i == len m)
+		return "invalid mouse message";
+	y := int m[i:];
+	return w.wmctl(sys->sprint("ptr %d %d", x, y));
+}
+
+cursorswitch(w: ref Window, d: array of byte): string
+{
+	Hex: con "0123456789abcdef";
+	if(len d != 2*4+64)
+		return w.wmctl("cursor");
+	hot := Draw->Point(bglong(d, 0*4), bglong(d, 1*4));
+	s := sys->sprint("cursor %d %d 16 32 ", hot.x, hot.y);
+	for(i := 2*4; i < len d; i++){
+		c := int d[i];
+		s[len s] = Hex[c >> 4];
+		s[len s] = Hex[c & 16rf];
+	}
+	return w.wmctl(s);
+}
+
+run(sync, ctl: chan of string, ld: string, argv: list of string)
+{
+	Rcmeta: con "|<>&^*[]?();";
+	sys->pctl(Sys->FORKNS, nil);
+	if(sys->bind("#₪", "/srv", Sys->MCREATE) == -1){
+		sync <-= sys->sprint("cannot bind srv device: %r");
+		exit;
+	}
+	srvname := "/srv/9win."+string sys->pctl(0, nil);	# XXX do better.
+	fd := sys->create(srvname, Sys->ORDWR, 8r600);
+	if(fd == nil){
+		sync <-= sys->sprint("cannot create %s: %r", srvname);
+		exit;
+	}
+	sync <-= nil;
+	spawn export(fd, ctl);
+	sh->run(nil, "os" ::
+		"rc" :: "-c" ::
+			"mount "+srvname+" /mnt/term;"+
+			"rm "+srvname+";"+
+			"bind -b /mnt/term"+ld+" /dev;"+
+			"bind /mnt/term/dev/draw /dev/draw ||"+
+				"bind -a /mnt/term/dev /dev;"+
+			quotedc("cd"::"/mnt/term"+cwd()::nil, Rcmeta)+";"+
+			quotedc(argv, Rcmeta)+";"::
+			nil
+		);
+}
+
+export(fd: ref Sys->FD, ctl: chan of string)
+{
+	sys->export(fd, "/", Sys->EXPWAIT);
+	ctl <-= "exit";
+}
+
+inf2p9key(c: int): int
+{
+	KF: import Keyboard;
+
+	P9KF: con	16rF000;
+	Spec: con	16rF800;
+	Khome: con	P9KF|16r0D;
+	Kup: con	P9KF|16r0E;
+	Kpgup: con	P9KF|16r0F;
+	Kprint: con	P9KF|16r10;
+	Kleft: con	P9KF|16r11;
+	Kright: con	P9KF|16r12;
+	Kdown: con	Spec|16r00;
+	Kview: con	Spec|16r00;
+	Kpgdown: con	P9KF|16r13;
+	Kins: con	P9KF|16r14;
+	Kend: con	P9KF|16r18;
+	Kalt: con		P9KF|16r15;
+	Kshift: con	P9KF|16r16;
+	Kctl: con		P9KF|16r17;
+
+	case c {
+	Keyboard->LShift =>
+		return Kshift;
+	Keyboard->LCtrl =>
+		return Kctl;
+	Keyboard->LAlt =>
+		return Kalt;
+	Keyboard->Home =>
+		return Khome;
+	Keyboard->End =>
+		return Kend;
+	Keyboard->Up =>
+		return Kup;
+	Keyboard->Down =>
+		return Kdown;
+	Keyboard->Left =>
+		return Kleft;
+	Keyboard->Right =>
+		return Kright;
+	Keyboard->Pgup =>
+		return Kpgup;
+	Keyboard->Pgdown =>
+		return Kpgdown;
+	Keyboard->Ins =>
+		return Kins;
+
+	# function keys
+	KF|1 or
+	KF|2 or
+	KF|3 or
+	KF|4 or
+	KF|5 or
+	KF|6 or
+	KF|7 or
+	KF|8 or
+	KF|9 or
+	KF|10 or
+	KF|11 or
+	KF|12 =>
+		return (c - KF) + P9KF;
+	}
+	return c;
+}
+
+cwd(): string
+{
+	return sys->fd2path(sys->open(".", Sys->OREAD));
+}
+
+# from string.b, waiting for declaration to be uncommented.
+quotedc(argv: list of string, cl: string): string
+{
+	s := "";
+	while (argv != nil) {
+		arg := hd argv;
+		for (i := 0; i < len arg; i++) {
+			c := arg[i];
+			if (c == ' ' || c == '\t' || c == '\n' || c == '\'' || in(c, cl))
+				break;
+		}
+		if (i < len arg || arg == nil) {
+			s += "'" + arg[0:i];
+			for (; i < len arg; i++) {
+				if (arg[i] == '\'')
+					s[len s] = '\'';
+				s[len s] = arg[i];
+			}
+			s[len s] = '\'';
+		} else
+			s += arg;
+		if (tl argv != nil)
+			s[len s] = ' ';
+		argv = tl argv;
+	}
+	return s;
+}
+
+in(c: int, s: string): int
+{
+	n := len s;
+	if(n == 0)
+		return 0;
+	ans := 0;
+	negate := 0;
+	if(s[0] == '^') {
+		negate = 1;
+		s = s[1:];
+		n--;
+	}
+	for(i := 0; i < n; i++) {
+		if(s[i] == '-' && i > 0 && i < n-1)  {
+			if(c >= s[i-1] && c <= s[i+1]) {
+				ans = 1;
+				break;
+			}
+			i++;
+		}
+		else
+		if(c == s[i]) {
+			ans = 1;
+			break;
+		}
+	}
+	if(negate)
+		ans = !ans;
+	return ans;
+}
+
+bglong(d: array of byte, i: int): int
+{
+	return int d[i] | (int d[i+1]<<8) | (int d[i+2]<<16) | (int d[i+3]<<24);
+}
--- /dev/null
+++ b/appl/cmd/B.b
@@ -1,0 +1,107 @@
+implement B;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+
+FD: import Sys;
+Context: import Draw;
+
+B: module
+{
+	init:	fn(nil: ref Context, argv: list of string);
+};
+
+sys: Sys;
+stderr: ref FD;
+wkdir: string;
+
+init(nil: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	if(len argv < 2) {
+		sys->fprint(stderr, "Usage: B file ...\n");
+		return;
+	}
+	argv = tl argv;
+
+	cmd := "exec B ";
+	while(argv != nil) {
+		f := hd argv;
+		if(len f > 0 && f[0] != '/' && f[0] != '-')
+			f = wd() + f;
+		cmd += "/usr/inferno"+f;
+		argv = tl argv;
+		if(argv != nil)
+			cmd += " ";
+	}			
+	cfd := sys->open("/cmd/clone", sys->ORDWR);
+	if(cfd == nil) {
+		sys->fprint(stderr, "B: open /cmd/clone: %r\n");
+		return;
+	}
+	
+	buf := array[32] of byte;
+	n := sys->read(cfd, buf, len buf);
+	if(n <= 0) {
+		sys->fprint(stderr, "B: read /cmd/#/ctl: %r\n");
+		return;
+	}
+	dir := "/cmd/"+string buf[0:n];
+
+	# Start the Command
+	n = sys->fprint(cfd, "%s", cmd);
+	if(n <= 0) {
+		sys->fprint(stderr, "B: exec: %r\n");
+		return;
+	}
+
+	io := sys->open(dir+"/data", sys->ORDWR);
+	if(io == nil) {
+		sys->fprint(stderr, "B: open /cmd/#/data: %r\n");
+		return;
+	}
+
+	sys->pctl(sys->NEWPGRP, nil);
+	copy(io, sys->fildes(1), nil);
+}
+
+wd(): string
+{
+	if(wkdir != nil)
+		return wkdir;
+
+	gwd := load Workdir Workdir->PATH;
+
+	wkdir = gwd->init();
+	if(wkdir == nil) {
+		sys->fprint(stderr, "B: can't get working dir: %r");
+		exit;
+	}
+	wkdir = wkdir+"/";
+	return wkdir;
+}
+
+copy(f, t: ref FD, c: chan of int)
+{
+	if(c != nil)
+		c <-= sys->pctl(0, nil);
+
+	buf := array[8192] of byte;
+	for(;;) {
+		r := sys->read(f, buf, len buf);
+		if(r <= 0)
+			break;
+		w := sys->write(t, buf, r);
+		if(w != r)
+			break;
+	}
+}
+
+kill(pid: int)
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", sys->OWRITE);
+	sys->fprint(fd, "kill");
+}
--- /dev/null
+++ b/appl/cmd/ar.b
@@ -1,0 +1,857 @@
+implement Ar;
+
+#
+# ar - portable (ascii) format version
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "string.m";
+	str: String;
+
+Ar: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+ARMAG: con "!<arch>\n";
+SARMAG: con len ARMAG;
+ARFMAG0: con byte '`';
+ARFMAG1: con byte '\n';
+SARNAME: con 16;	# ancient limit
+
+#
+# printable archive header
+#	name[SARNAME] date[12] uid[6] gid[6] mode[8] size[10] fmag[2]
+#
+Oname:	con 0;
+Lname:	con SARNAME;
+Odate:	con Oname+Lname;
+Ldate:	con 12;
+Ouid:	con Odate+Ldate;
+Luid:		con 6;
+Ogid:	con Ouid+Luid;
+Lgid:		con 6;
+Omode:	con Ogid+Lgid;
+Lmode:	con 8;
+Osize:	con Omode+Lmode;
+Lsize:	con 10;
+Ofmag:	con Osize+Lsize;
+Lfmag:	con 2;
+SAR_HDR:	con Ofmag+Lfmag;	# 60
+
+#
+# 	The algorithm uses up to 3 temp files.  The "pivot contents" is the
+# 	archive contents specified by an a, b, or i option.  The temp files are
+# 	astart - contains existing contentss up to and including the pivot contents.
+# 	amiddle - contains new files moved or inserted behind the pivot.
+# 	aend - contains the existing contentss that follow the pivot contents.
+# 	When all contentss have been processed, function 'install' streams the
+#  	temp files, in order, back into the archive.
+#
+
+Armember: adt {	# one per archive contents
+	name:	string;	# trimmed
+	length:	int;
+	date:	int;
+	uid:	int;
+	gid:	int;
+	mode:	int;
+	size:	int;
+	contents:	array of byte;
+	fd:	ref Sys->FD;	# if contents is nil and fd is not nil, fd has contents
+	next:	cyclic ref Armember;
+
+	new:		fn(name: string, fd: ref Sys->FD): ref Armember;
+	rdhdr:	fn(b: ref Iobuf): ref Armember;
+	read:		fn(m: self ref Armember, b:  ref Iobuf): int;
+	wrhdr:	fn(m: self ref Armember, fd: ref Sys->FD);
+	write:	fn(m: self ref Armember, fd: ref Sys->FD);
+	skip:		fn(m: self ref Armember, b: ref Iobuf);
+	replace:	fn(m: self ref Armember, name: string, fd: ref Sys->FD);
+	copyout:	fn(m: self ref Armember, b: ref Iobuf, destfd: ref Sys->FD);
+};
+
+Arfile: adt {	# one per tempfile
+	fd:	ref Sys->FD;	# paging file descriptor, nil if none allocated
+
+	head:	ref Armember;
+	tail:	ref Armember;
+
+	new:		fn(): ref Arfile;
+	copy:	fn(ar: self ref Arfile, b: ref Iobuf, mem: ref Armember);
+	insert:	fn(ar: self ref Arfile, mem: ref Armember);
+	stream:	fn(ar: self ref Arfile, fd: ref Sys->FD);
+	page:	fn(ar: self ref Arfile): int;
+};
+
+File: adt {
+	name:	string;
+	trimmed:	string;
+	found:	int;
+};
+
+man :=	"mrxtdpq";
+opt :=	"uvnbailo";
+
+aflag := 0;
+bflag := 0;
+cflag := 0;
+oflag := 0;
+uflag := 0;
+vflag := 0;
+
+pivotname: string;
+bout: ref Iobuf;
+stderr: ref Sys->FD;
+parts: array of ref Arfile;
+
+comfun: ref fn(a: string, f: array of ref File);
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	str = load String String->PATH;
+
+	stderr = sys->fildes(2);
+	bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+	if(len args < 3)
+		usage();
+	args = tl args;
+	s := hd args; args = tl args;
+	for(i := 0; i < len s; i++){
+		case s[i] {
+		'a' =>	aflag = 1;
+		'b' =>	bflag = 1;
+		'c' =>	cflag = 1;
+		'd' =>	setcom(dcmd);
+		'i' =>		bflag = 1;
+		'l' =>		;	# ignored
+		'm' =>	setcom(mcmd);
+		'o' =>	oflag = 1;
+		'p' =>	setcom(pcmd);
+		'q' =>	setcom(qcmd);
+		'r' =>		setcom(rcmd);
+		't' =>		setcom(tcmd);
+		'u' =>	uflag = 1;
+		'v' =>	vflag = 1;
+		'x' =>	setcom(xcmd);
+		* =>
+			sys->fprint(stderr, "ar: bad option `%c'\n", s[i]);
+			usage();
+		}
+	}
+	if(aflag && bflag){
+		sys->fprint(stderr, "ar: only one of 'a' and 'b' can be specified\n");
+		usage();
+	}
+	if(aflag || bflag){
+		pivotname = trim(hd args); args = tl args;
+		if(len args < 2)
+			usage();
+	}
+	if(comfun == nil){
+		if(uflag == 0){
+			sys->fprint(stderr, "ar: one of [%s] must be specified\n", man);
+			usage();
+		}
+		setcom(rcmd);
+	}
+	cp := hd args; args = tl args;
+	files := array[len args] of ref File;
+	for(i = 0; args != nil; args = tl args)
+		files[i++] = ref File(hd args, trim(hd args), 0);
+	comfun(cp, files);	# do the command
+	allfound := 1;
+	for(i = 0; i < len files; i++)
+		if(!files[i].found){
+			sys->fprint(stderr, "ar: %s not found\n", files[i].name);
+			allfound = 0;
+		}
+	bout.flush();
+	if(!allfound)
+		raise "fail: file not found";
+}
+
+#
+# 	select a command
+#
+setcom(fun: ref fn(s: string, f: array of ref File))
+{
+	if(comfun != nil){
+		sys->fprint(stderr, "ar: only one of [%s] allowed\n", man);
+		usage();
+	}
+	comfun = fun;
+}
+
+#
+# 	perform the 'r' and 'u' commands
+#
+rcmd(arname: string, files: array of ref File)
+{
+	bar := openar(arname, Sys->ORDWR, 1);
+	parts = array[2] of {Arfile.new(), nil};
+	ap := parts[0];
+	if(bar != nil){
+		while((mem := Armember.rdhdr(bar)) != nil){
+			if(bamatch(mem.name, pivotname))	# check for pivot
+				ap = parts[1] = Arfile.new();
+			f := match(files, mem.name);
+			if(f == nil){
+				ap.copy(bar, mem);
+				continue;
+			}
+			f.found = 1;
+			dfd := sys->open(f.name, Sys->OREAD);
+			if(dfd == nil){
+				if(len files > 0)
+					sys->fprint(stderr, "ar: cannot open %s: %r\n", f.name);
+				ap.copy(bar, mem);
+				continue;
+			}
+			if(uflag){
+				(ok, d) := sys->fstat(dfd);
+				if(ok < 0 || d.mtime <= mem.date){
+					if(ok < 0)
+						sys->fprint(stderr, "ar: cannot stat %s: %r\n", f.name);
+					ap.copy(bar, mem);
+					continue;
+				}
+			}
+			mem.skip(bar);
+			mesg('r', f.name);
+			mem.replace(f.name, dfd);
+			ap.insert(mem);
+			dfd = nil;
+		}
+	}
+	# copy in remaining files named on command line
+	for(i := 0; i < len files; i++){
+		f := files[i];
+		if(f.found)
+			continue;
+		f.found = 1;
+		dfd := sys->open(f.name, Sys->OREAD);
+		if(dfd != nil){
+			mesg('a', f.name);
+			parts[0].insert(Armember.new(f.trimmed, dfd));
+		}else
+			sys->fprint(stderr, "ar: cannot open %s: %r\n", f.name);
+	}
+	if(bar == nil && !cflag)
+		install(arname, parts, 1);	# issue 'creating' msg
+	else
+		install(arname, parts, 0);
+}
+
+dcmd(arname: string, files: array of ref File)
+{
+	if(len files == 0)
+		return;
+	changed := 0;
+	parts = array[] of {Arfile.new()};
+	bar := openar(arname, Sys->ORDWR, 0);
+	while((mem := Armember.rdhdr(bar)) != nil){
+		if(match(files, mem.name) != nil){
+			mesg('d', mem.name);
+			mem.skip(bar);
+			changed = 1;
+		}else
+			parts[0].copy(bar, mem);
+		mem =  nil;	# conserves memory
+	}
+	if(changed)
+		install(arname, parts, 0);
+}
+
+xcmd(arname: string, files: array of ref File)
+{
+	bar := openar(arname, Sys->OREAD, 0);
+	i := 0;
+	while((mem := Armember.rdhdr(bar)) != nil){
+		if((f := match(files, mem.name)) != nil){
+			f.found = 1;
+			fd := sys->create(f.name, Sys->OWRITE, mem.mode & 8r777);
+			if(fd == nil){
+				sys->fprint(stderr, "ar: cannot create %s: %r\n", f.name);
+				mem.skip(bar);
+			}else{
+				mesg('x', f.name);
+				mem.copyout(bar, fd);
+				if(oflag){
+					dx := sys->nulldir;
+					dx.atime = mem.date;
+					dx.mtime = mem.date;
+					if(sys->fwstat(fd, dx) < 0)
+						sys->fprint(stderr, "ar: can't set times on %s: %r", f.name);
+				}
+				fd = nil;
+				mem = nil;
+			}
+			if(len files > 0 && ++i >= len files)
+				break;
+		}else
+			mem.skip(bar);
+	}
+}
+
+pcmd(arname: string, files: array of ref File)
+{
+	bar := openar(arname, Sys->OREAD, 0);
+	i := 0;
+	while((mem := Armember.rdhdr(bar)) != nil){
+		if((f := match(files, mem.name)) != nil){
+			if(vflag)
+				sys->print("\n<%s>\n\n", f.name);
+			mem.copyout(bar, sys->fildes(1));
+			if(len files > 0 && ++i >= len files)
+				break;
+		}else
+			mem.skip(bar);
+		mem = nil;	# we no longer need the contents
+	}
+}
+
+mcmd(arname: string, files: array of ref File)
+{
+	if(len files == 0)
+		return;
+	parts = array[3] of {Arfile.new(), Arfile.new(), nil};
+	bar := openar(arname, Sys->ORDWR, 0);
+	ap := parts[0];
+	while((mem := Armember.rdhdr(bar)) != nil){
+		if(bamatch(mem.name, pivotname))
+			ap = parts[2] = Arfile.new();
+		if((f := match(files, mem.name)) != nil){
+			mesg('m', f.name);
+			parts[1].copy(bar, mem);
+		}else
+			ap.copy(bar, mem);
+	}
+	if(pivotname != nil && parts[2] == nil)
+		sys->fprint(stderr, "ar: %s not found - files moved to end\n", pivotname);
+	install(arname, parts, 0);
+}
+
+tcmd(arname: string, files: array of ref File)
+{
+	bar := openar(arname, Sys->OREAD, 0);
+	while((mem := Armember.rdhdr(bar)) != nil){
+		if((f := match(files, mem.name)) != nil){
+			longls := "";
+			if(vflag)
+				longls = longtext(mem)+" ";
+			bout.puts(longls+f.trimmed+"\n");
+		}
+		mem.skip(bar);
+		mem = nil;
+	}
+}
+
+qcmd(arname: string, files: array of ref File)
+{
+	if(aflag || bflag){
+		sys->fprint(stderr, "ar: abi not allowed with q\n");
+		raise "fail:usage";
+	}
+	fd := openrawar(arname, Sys->ORDWR, 1);
+	if(fd == nil){
+		if(!cflag)
+			sys->fprint(stderr, "ar: creating %s\n", arname);
+		fd = arcreate(arname);
+	}
+	# leave note group behind when writing archive; i.e. sidestep interrupts
+	sys->seek(fd, big 0, 2);	# append
+	for(i := 0; i < len files; i++){
+		f := files[i];
+		f.found = 1;
+		dfd := sys->open(f.name, Sys->OREAD);
+		if(dfd != nil){
+			mesg('q', f.name);
+			mem := Armember.new(f.trimmed, dfd);
+			if(mem != nil){
+				mem.write(fd);
+				mem = nil;
+			}
+		}else
+			sys->fprint(stderr, "ar: cannot open %s: %r\n", f.name);
+	}
+}
+
+#
+# 	open an archive and validate its header
+#
+openrawar(arname: string, mode: int, errok: int): ref Sys->FD
+{
+	fd := sys->open(arname, mode);
+	if(fd == nil){
+		if(!errok){
+			sys->fprint(stderr, "ar: cannot open %s: %r\n", arname);
+			raise "fail:error";
+		}
+		return nil;
+	}
+	mbuf := array[SARMAG] of byte;
+	if(sys->read(fd, mbuf, SARMAG) != SARMAG || string mbuf != ARMAG){
+		sys->fprint(stderr, "ar: %s not in archive format\n", arname);
+		raise "fail:error";
+	}
+	return fd;
+}
+
+openar(arname: string, mode: int, errok: int): ref Iobuf
+{
+	fd := openrawar(arname, mode, errok);
+	if(fd == nil)
+		return nil;
+	bfd := bufio->fopen(fd, mode);
+	bfd.seek(big SARMAG, 0);
+	return bfd;
+}
+
+#
+# 	create an archive and set its header
+#
+arcreate(arname: string): ref Sys->FD
+{
+	fd := sys->create(arname, Sys->OWRITE, 8r666);
+	if(fd == nil){
+		sys->fprint(stderr, "ar: cannot create %s: %r\n", arname);
+		raise "fail:create";
+	}
+	a := array of byte ARMAG;
+	mustwrite(fd, a, len a);
+	return fd;
+}
+
+#
+# 		error handling
+#
+wrerr()
+{
+	sys->fprint(stderr, "ar: write error: %r\n");
+	raise "fail:write error";
+}
+
+rderr()
+{
+	sys->fprint(stderr, "ar: read error: %r\n");
+	raise "fail:read error";
+}
+
+phaseerr(offset: big)
+{
+	sys->fprint(stderr, "ar: phase error at offset %bd\n", offset);
+	raise "fail:phase error";
+}
+
+usage()
+{
+	sys->fprint(stderr, "usage: ar [%s][%s] archive files ...\n", opt, man);
+	raise "fail:usage";
+}
+
+#
+# concatenate the several sequences of members into one archive
+#
+install(arname: string, seqs: array of ref Arfile, createflag: int)
+{
+	# leave process group behind when copying back; i.e. sidestep interrupts
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	if(createflag)
+		sys->fprint(stderr, "ar: creating %s\n", arname);
+	fd := arcreate(arname);
+	for(i := 0; i < len seqs; i++)
+		if((ap := seqs[i]) != nil)
+			ap.stream(fd);
+}
+
+#
+# return the command line File matching a given name
+#
+match(files: array of ref File, file: string): ref File
+{
+	if(len files == 0)
+		return ref File(file, file, 0);	# empty list always matches
+	for(i := 0; i < len files; i++)
+		if(!files[i].found && files[i].trimmed == file){
+			files[i].found = 1;
+			return files[i];
+		}
+	return nil;
+}
+
+#
+# is `file' the pivot member's name and is the archive positioned
+# at the correct point wrt after or before options?  return true if so.
+#
+state := 0;
+
+bamatch(file: string, pivot: string): int
+{
+	case state {
+	0 =>			# looking for position file
+		if(aflag){
+			if(file == pivot)
+				state = 1;
+		}else if(bflag){
+			if(file == pivot){
+				state = 2;	# found
+				return 1;
+			}
+		}
+	1 =>			# found - after previous file
+		state = 2;
+		return 1;
+	2 =>			# already found position file
+		;
+	}
+	return 0;
+}
+
+#
+# output a message, if 'v' option was specified
+#
+mesg(c: int, file: string)
+{
+	if(vflag)
+		bout.puts(sys->sprint("%c - %s\n", c, file));
+}
+
+#
+# return just the file name
+#
+trim(s: string): string
+{
+	for(j := len s; j > 0 && s[j-1] == '/';)
+		j--;
+	k := 0;
+	for(i := 0; i < j; i++)
+		if(s[i] == '/')
+			k = i+1;
+	return s[k: j];
+}
+
+longtext(mem: ref Armember): string
+{
+	s := modes(mem.mode);
+	s += sys->sprint(" %3d/%1d", mem.uid, mem.gid);
+	s += sys->sprint(" %7ud", mem.size);
+	t := daytime->text(daytime->local(mem.date));
+	return s+sys->sprint(" %-12.12s %-4.4s ", t[4:], t[24:]);
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	return mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+}
+
+#
+# read the header for the next archive contents
+#
+Armember.rdhdr(b: ref Iobuf): ref Armember
+{
+	buf := array[SAR_HDR] of byte;
+	if((n := b.read(buf, len buf)) != len buf){
+		if(n == 0)
+			return nil;
+		if(n > 0)
+			sys->werrstr("unexpected end-of-file");
+		rderr();
+	}
+	mem := ref Armember;
+	for(i := Oname+Lname; i > Oname; i--)
+		if(buf[i-1] != byte '/' && buf[i-1] != byte ' ')
+			break;
+	mem.name = string buf[Oname:i];
+	mem.date = intof(buf[Odate: Odate+Ldate], 10);
+	mem.uid = intof(buf[Ouid: Ouid+Luid], 10);
+	mem.gid = intof(buf[Ogid: Ogid+Lgid], 10);
+	mem.mode = intof(buf[Omode: Omode+Lmode], 8);
+	mem.size = intof(buf[Osize: Osize+Lsize], 10);
+	if(buf[Ofmag] != ARFMAG0 || buf[Ofmag+1] != ARFMAG1)
+		phaseerr(b.offset()-big SAR_HDR);
+	return mem;
+}
+
+intof(a: array of byte, base: int): int
+{
+	for(i := len a; i > 0; i--)
+		if(a[i-1] != byte ' '){
+			a = a[0:i];
+			break;
+		}
+	(n, s) := str->toint(string a, base);
+	if(s != nil){
+		sys->fprint(stderr, "ar: invalid integer in archive member's header: %q\n", string a);
+		raise "fail:error";
+	}
+	return n;
+}
+
+Armember.wrhdr(mem: self ref Armember, fd: ref Sys->FD)
+{
+	b := array[SAR_HDR] of {* => byte ' '};
+	nm := array of byte mem.name;
+	if(len nm > Lname)
+		nm = nm[0:Lname];
+	b[Oname:] = nm;
+	b[Odate:] = sys->aprint("%-12ud", mem.date);
+	b[Ouid:] = sys->aprint("%-6d", 0);
+	b[Ogid:] = sys->aprint("%-6d", 0);
+	b[Omode:] = sys->aprint("%-8uo", mem.mode);
+	b[Osize:] = sys->aprint("%-10ud", mem.size);
+	b[Ofmag] = ARFMAG0;
+	b[Ofmag+1] = ARFMAG1;
+	mustwrite(fd, b, len b);
+}
+
+#
+# make a new member from the given file, with the file's contents
+#
+Armember.new(name: string, fd: ref Sys->FD): ref Armember
+{
+	mem := ref Armember;
+	mem.replace(name, fd);
+	return mem;
+}
+
+#
+# replace the contents  of an existing member
+#
+Armember.replace(mem: self ref Armember, name: string, fd: ref Sys->FD)
+{
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0){
+		sys->fprint(stderr, "ar: cannot stat %s: %r\n", name);
+		raise "fail:no stat";
+	}
+	mem.name = trim(name);
+	mem.date = d.mtime;
+	mem.uid = 0;
+	mem.gid = 0;
+	mem.mode = d.mode & 8r777;
+	mem.size = int d.length;
+	if(big mem.size != d.length){
+		sys->fprint(stderr, "ar: file %s too big\n", name);
+		raise "fail:error";
+	}
+	mem.fd = fd;
+	mem.contents = nil;	# will be copied across from fd when needed
+}
+
+#
+# read the contents of an archive member
+#
+Armember.read(mem: self ref Armember, b: ref Iobuf): int
+{
+	if(mem.contents != nil)
+		return len mem.contents;
+	mem.contents = buffer(mem.size + (mem.size&1));
+	n := b.read(mem.contents, len mem.contents);
+	if(n != len mem.contents){
+		if(n >= 0)
+			sys->werrstr("unexpected end-of-file");
+		rderr();
+	}
+	return n;
+}
+
+mustwrite(fd: ref Sys->FD, buf: array of byte, n: int)
+{
+	if(sys->write(fd, buf, n) != n)
+		wrerr();
+}
+
+#
+# write an archive member to ofd, including header
+#
+Armember.write(mem: self ref Armember, ofd: ref Sys->FD)
+{
+	mem.wrhdr(ofd);
+	if(mem.contents != nil){
+		mustwrite(ofd, mem.contents, len mem.contents);
+		return;
+	}
+	if(mem.fd == nil)
+		raise "ar: write nil fd";
+	buf := array[Sys->ATOMICIO] of byte;	# could be bigger
+	for(nr := mem.size; nr > 0;){
+		n := nr;
+		if(n > len buf)
+			n = len buf;
+		n = sys->read(mem.fd, buf, n);
+		if(n <= 0){
+			if(n == 0)
+				sys->werrstr("unexpected end-of-file");
+			rderr();
+		}
+		mustwrite(ofd, buf, n);
+		nr -= n;
+	}
+	if(mem.size & 1)
+		mustwrite(ofd, array[] of {byte '\n'}, 1);
+}
+
+#
+# seek past the current member's contents in b
+#
+Armember.skip(mem: self ref Armember, b: ref Iobuf)
+{
+	b.seek(big(mem.size + (mem.size&1)), 1);
+}
+
+#
+# copy a member's contents from memory or directly from an archive to another file
+#
+Armember.copyout(mem: self ref Armember, b: ref Iobuf, ofd: ref Sys->FD)
+{
+	if(mem.contents != nil){
+		mustwrite(ofd, mem.contents, len mem.contents);
+		return;
+	}
+	buf := array[Sys->ATOMICIO] of byte;	# could be bigger
+	for(nr := mem.size; nr > 0;){
+		n := nr;
+		if(n > len buf)
+			n = len buf;
+		n = b.read(buf, n);
+		if(n <= 0){
+			if(n == 0)
+				sys->werrstr("unexpected end-of-file");
+			rderr();
+		}
+		mustwrite(ofd, buf, n);
+		nr -= n;
+	}
+	if(mem.size & 1)
+		b.getc();
+}
+
+#
+# 	Temp file I/O subsystem.  We attempt to cache all three temp files in
+# 	core.  When we run out of memory we spill to disk.
+# 	The I/O model assumes that temp files:
+# 		1) are only written on the end
+# 		2) are only read from the beginning
+# 		3) are only read after all writing is complete.
+# 	The architecture uses one control block per temp file.  Each control
+# 	block anchors a chain of buffers, each containing an archive contents.
+#
+Arfile.new(): ref Arfile
+{
+	return ref Arfile;
+}
+
+#
+# copy the contents of mem at b into the temporary
+#
+Arfile.copy(ap: self ref Arfile, b: ref Iobuf, mem: ref Armember)
+{
+	mem.read(b);
+	ap.insert(mem);
+}
+
+#
+#  insert a contents buffer into the contents chain
+#
+Arfile.insert(ap: self ref Arfile, mem: ref Armember)
+{
+	mem.next = nil;
+	if(ap.head == nil)
+		ap.head = mem;
+	else
+		ap.tail.next = mem;
+	ap.tail = mem;
+}
+
+#
+# stream the contents in a temp file to the file referenced by 'fd'.
+#
+Arfile.stream(ap: self ref Arfile, fd: ref Sys->FD)
+{
+	if(ap.fd != nil){		# copy prefix from disk
+		buf := array[Sys->ATOMICIO] of byte;
+		sys->seek(ap.fd, big 0, 0);
+		while((n := sys->read(ap.fd, buf, len buf)) > 0)
+			mustwrite(fd, buf, n);
+		if(n < 0)
+			rderr();
+		ap.fd = nil;
+	}
+	# dump the in-core buffers, which always follow the contents in the temp file
+	for(mem := ap.head; mem != nil; mem = mem.next)
+		mem.write(fd);
+}
+
+#
+# spill a member's contents to disk
+#
+
+totalmem := 0;
+warned := 0;
+tn := 0;
+
+Arfile.page(ap: self ref Arfile): int
+{
+	mem := ap.head;
+	if(ap.fd == nil && !warned){
+		pid := sys->pctl(0, nil);
+		for(i := 0;; i++){
+			name := sys->sprint("/tmp/art%d.%d.%d", pid, tn, i);
+			ap.fd = sys->create(name, Sys->OEXCL | Sys->ORDWR | Sys->ORCLOSE, 8r600);
+			if(ap.fd != nil)
+				break;
+			if(i >= 20){
+				warned =1;
+				sys->fprint(stderr,"ar: warning: can't create temp file %s: %r\n", name);
+				return 0;	# we'll simply use the memory
+			}
+		}
+		tn++;
+	}
+	mem.write(ap.fd);
+	ap.head = mem.next;
+	if(ap.tail == mem)
+		ap.tail = mem.next;
+	totalmem -= len mem.contents;
+	return 1;
+}
+
+#
+# account for the space taken by a contents's contents,
+# pushing earlier contentss to disk to keep the space below a
+# reasonable level
+#
+
+buffer(n: int): array of byte
+{
+Flush:
+	while(totalmem + n > 1024*1024){
+		for(i := 0; i < len parts; i++)
+			if(parts[i] != nil && parts[i].page())
+				continue Flush;
+		break;
+	}
+	totalmem += n;
+	return array[n] of byte;
+}
--- /dev/null
+++ b/appl/cmd/archfs.b
@@ -1,0 +1,630 @@
+implement Archfs;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+
+include "string.m";
+	str: String;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "styx.m";
+	styx: Styx;
+	NOFID: import Styx;
+
+include "arg.m";
+
+Archfs: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Ahdr: adt {
+	name: string;
+	modestr: string;
+	d: ref Sys->Dir;
+};
+
+Archive: adt {
+	b: ref Bufio->Iobuf;
+	nexthdr: big;
+	canseek: int;
+	hdr: ref Ahdr;
+	err: string;
+};
+
+Iobuf: import bufio;
+Tmsg, Rmsg: import styx;
+
+Einuse		: con "fid already in use";
+Ebadfid		: con "bad fid";
+Eopen		: con "fid already opened";
+Enotfound	: con "file does not exist";
+Enotdir		: con "not a directory";
+Eperm		: con "permission denied";
+
+UID: con "inferno";
+GID: con "inferno";
+
+debug := 0;
+
+Dir: adt {
+	dir: Sys->Dir;
+	offset: big;
+	parent: cyclic ref Dir;
+	child: cyclic ref Dir;
+	sibling: cyclic ref Dir;
+};
+
+Fid: adt {
+	fid:	int;
+	open:	int;
+	dir:	ref Dir;
+};
+
+HTSZ: con 32;
+fidtab := array[HTSZ] of list of ref Fid;
+
+root: ref Dir;
+qid: int;
+mtpt := "/mnt/arch";
+bio: ref Iobuf;
+buf: array of byte;
+skip := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	daytime = load Daytime Daytime->PATH;
+	styx = load Styx Styx->PATH;
+	if(bufio == nil || styx == nil || daytime == nil || str == nil)
+		fatal("failed to load modules");
+	styx->init();
+
+	flags := Sys->MREPL;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		fatal("failed to load "+Arg->PATH);
+	arg->init(args);
+	arg->setusage("archfs [-ab] [-m mntpt] archive [prefix ...]");
+	while((c := arg->opt()) != 0){
+		case c {
+		'D' =>
+			debug = 1;
+		'a' =>
+			flags = Sys->MAFTER;
+		'b' =>
+			flags = Sys->MBEFORE;
+		'm' =>
+			mtpt = arg->earg();
+		's' =>
+			skip = 1;
+		* =>
+			arg->usage();
+		}
+	}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	buf = array[Sys->ATOMICIO] of byte;
+	# root = newdir("/", UID, GID, 8r755|Sys->DMDIR, daytime->now());
+	root = newdir(basename(mtpt), UID, GID, 8r555|Sys->DMDIR, daytime->now());
+	root.parent = root;
+	readarch(hd args, tl args);
+	p := array[2] of ref Sys->FD;
+	if(sys->pipe(p) < 0)
+		fatal("can't create pipe");
+	pidch := chan of int;
+	spawn serve(p[1], pidch);
+	<- pidch;
+	if(sys->mount(p[0], nil, mtpt, flags, nil) < 0)
+		fatal(sys->sprint("cannot mount archive on %s: %r", mtpt));
+}
+
+reply(fd: ref Sys->FD, m: ref Rmsg): int
+{
+	if(debug)
+		sys->fprint(sys->fildes(2), "-> %s\n", m.text());
+	s := m.pack();
+	if(s == nil)
+		return -1;
+	return sys->write(fd, s, len s);
+}
+
+error(fd: ref Sys->FD, m: ref Tmsg, e: string)
+{
+	reply(fd, ref Rmsg.Error(m.tag, e));
+}
+
+serve(fd: ref Sys->FD, pidch: chan of int)
+{
+	e: string;
+	f: ref Fid;
+
+	pidch <-= sys->pctl(Sys->NEWNS|Sys->NEWFD, 1 :: 2 :: fd.fd :: bio.fd.fd :: nil);
+	bio.fd = sys->fildes(bio.fd.fd);
+	fd = sys->fildes(fd.fd);
+Work:
+	while((m0 := Tmsg.read(fd, Styx->MAXRPC)) != nil){
+		if(debug)
+			sys->fprint(sys->fildes(2), "<- %s\n", m0.text());
+		pick m := m0 {
+		Readerror =>
+			fatal("read error on styx server");
+		Version =>
+			(s, v) := styx->compatible(m, Styx->MAXRPC, Styx->VERSION);
+			reply(fd, ref Rmsg.Version(m.tag, s, v));
+		Auth =>
+			error(fd, m, "authentication not required");
+		Flush =>
+			reply(fd, ref Rmsg.Flush(m.tag));
+		Walk =>
+			(f, e) = mapfid(m.fid);
+			if(e != nil){
+				error(fd, m, e);
+				continue;
+			}
+			if(f.open){
+				error(fd, m, Eopen);
+				continue;
+			}
+			dir := f.dir;
+			nq := 0;
+			nn := len m.names;
+			qids := array[nn] of Sys->Qid;
+			if(nn > 0){
+				for(k := 0; k < nn; k++){
+					if((dir.dir.mode & Sys->DMDIR) == 0){
+						if(k == 0){
+							error(fd, m, Enotdir);
+							continue Work;
+						}
+						break;
+					}
+					dir  = lookup(dir, m.names[k]);
+					if(dir == nil){
+						if(k == 0){
+							error(fd, m, Enotfound);
+							continue Work;
+						}
+						break;
+					}
+					qids[nq++] = dir.dir.qid;
+				}
+			}
+			if(nq < nn)
+				qids = qids[0: nq];
+			if(nq == nn){
+				if(m.newfid != m.fid){
+					f = newfid(m.newfid);
+					if(f == nil){
+						error(fd, m, Einuse);
+						continue Work;
+					}
+				}
+				f.dir = dir;
+			}
+			reply(fd, ref Rmsg.Walk(m.tag, qids));
+		Open =>
+			(f, e) = mapfid(m.fid);
+			if(e != nil){
+				error(fd, m, e);
+				continue;
+			}
+			if(m.mode != Sys->OREAD){
+				error(fd, m, Eperm);
+				continue;
+			}
+			f.open = 1;
+			reply(fd, ref Rmsg.Open(m.tag, f.dir.dir.qid, Styx->MAXFDATA));
+		Create =>
+			error(fd, m, Eperm);
+		Read =>
+			(f, e) = mapfid(m.fid);
+			if(e != nil){
+				error(fd, m, e);
+				continue;
+			}
+			data := read(f.dir, m.offset, m.count);
+			reply(fd, ref Rmsg.Read(m.tag, data));
+		Write =>
+			error(fd, m, Eperm);				
+		Clunk =>
+			(f, e) = mapfid(m.fid);
+			if(e != nil){
+				error(fd, m, e);
+				continue;
+			}
+			freefid(f);
+			reply(fd, ref Rmsg.Clunk(m.tag));
+		Stat =>
+			(f, e) = mapfid(m.fid);
+			if(e != nil){
+				error(fd, m, e);
+				continue;
+			}
+			reply(fd, ref Rmsg.Stat(m.tag, f.dir.dir));
+		Remove =>
+			error(fd, m, Eperm);
+		Wstat =>
+			error(fd, m, Eperm);
+		Attach =>
+			f = newfid(m.fid);
+			if(f == nil){
+				error(fd, m, Einuse);
+				continue;
+			}
+			f.dir = root;
+			reply(fd, ref Rmsg.Attach(m.tag, f.dir.dir.qid));
+		* =>
+			fatal("unknown styx message");
+		}
+	}
+}
+
+newfid(fid: int): ref Fid
+{
+	if(fid == NOFID)
+		return nil;
+	hv := hashval(fid);
+	ff: ref Fid;
+	for(l := fidtab[hv]; l != nil; l = tl l){
+		f := hd l;
+		if(f.fid == fid)
+			return nil;
+		if(ff == nil && f.fid == NOFID)
+			ff = f;
+	}
+	if((f := ff) == nil){
+		f = ref Fid;
+		fidtab[hv] = f :: fidtab[hv];
+	}
+	f.fid = fid;
+	f.open = 0;
+	return f;
+}
+
+freefid(f: ref Fid)
+{
+	hv := hashval(f.fid);
+	for(l := fidtab[hv]; l != nil; l = tl l)
+		if(hd l == f){
+			f.fid = NOFID;
+			f.dir = nil;
+			f.open = 0;
+			return;
+		}
+	fatal("cannot find fid");
+}
+	
+mapfid(fid: int): (ref Fid, string)
+{
+	if(fid == NOFID)
+		return (nil, Ebadfid);
+	hv := hashval(fid);
+	for(l := fidtab[hv]; l != nil; l = tl l){
+		f := hd l;
+		if(f.fid == fid){
+			if(f.dir == nil)
+				return (nil, Enotfound);
+			return (f, nil);
+		}
+	}
+	return (nil, Ebadfid);
+}
+
+hashval(n: int): int
+{
+	n %= HTSZ;
+	if(n < 0)
+		n += HTSZ;
+	return n;
+}
+
+readarch(f: string, args: list of string)
+{
+	ar := openarch(f);
+	if(ar == nil || ar.b == nil)
+		fatal(sys->sprint("cannot open %s: %r", f));
+	bio = ar.b;
+	while((a := gethdr(ar)) != nil){
+		if(args != nil){
+			if(!selected(a.name, args)){
+				if(skip)
+					return;
+				#drain(ar, int a.d.length);
+				continue;
+			}
+			mkdirs("/", a.name);
+		}
+		d := mkdir(a.name, a.d.mode, a.d.mtime, a.d.uid, a.d.gid, 0);
+		if((a.d.mode & Sys->DMDIR) == 0){
+			d.dir.length = a.d.length;
+			d.offset = bio.offset();
+		}
+		#drain(ar, int a.d.length);
+	}
+	if(ar.err != nil)
+		fatal(ar.err);
+}
+
+selected(s: string, args: list of string): int
+{
+	for(; args != nil; args = tl args)
+		if(fileprefix(hd args, s))
+			return 1;
+	return 0;
+}
+
+fileprefix(prefix, s: string): int
+{
+	n := len prefix;
+	m := len s;
+	if(n > m || !str->prefix(prefix, s))
+		return 0;
+	if(m > n && s[n] != '/')
+		return 0;
+	return 1;
+}
+
+basename(f: string): string
+{
+	for(i := len f; i > 0; ) 
+		if(f[--i] == '/')
+			return f[i+1:];
+	return f;
+}
+
+split(p: string): (string, string)
+{
+	if(p == nil)
+		fatal("nil string in split");
+	if(p[0] != '/')
+		fatal("p0 not / in split");
+	while(p[0] == '/')
+		p = p[1:];
+	i := 0;
+	while(i < len p && p[i] != '/')
+		i++;
+	if(i == len p)
+		return (p, nil);
+	else
+		return (p[0:i], p[i:]);
+}
+
+mkdirs(basedir, name: string)
+{
+	(nil, names) := sys->tokenize(name, "/");
+	while(names != nil){
+		# sys->print("mkdir %s\n", basedir);
+		mkdir(basedir, 8r775|Sys->DMDIR, daytime->now(), UID, GID, 1);
+		if(tl names == nil)
+			break;
+		basedir = basedir + "/" + hd names;
+		names = tl names;
+	}
+}
+
+read(d: ref Dir, offset: big, n: int): array of byte
+{
+	if(d.dir.mode & Sys->DMDIR)
+		return readdir(d, int offset, n);
+	return readfile(d, offset, n);
+}
+	
+readdir(d: ref Dir, o: int, n: int): array of byte
+{
+	k := 0;
+	m := 0;
+	b := array[n] of byte;
+	for(s := d.child; s != nil; s = s.sibling){
+		l := styx->packdirsize(s.dir);
+		if(k < o){
+			k += l;
+			continue;
+		}
+		if(m+l > n)
+			break;
+		b[m: ] = styx->packdir(s.dir);
+		m += l;
+	}
+	return b[0: m];
+}
+
+readfile(d: ref Dir, offset: big, n: int): array of byte
+{
+	if(offset+big n > d.dir.length)
+		n = int(d.dir.length-offset);
+	if(n <= 0 || offset < big 0)
+		return nil;
+	bio.seek(d.offset+offset, Bufio->SEEKSTART);
+	a := array[n] of byte;
+	p := 0;
+	m := 0;
+	for( ; n != 0; n -= m){
+		l := len buf;
+		if(n < l)
+			l = n;
+		m = bio.read(buf, l);
+		if(m <= 0 || m != l)
+			fatal("premature eof");
+		a[p:] = buf[0:m];
+		p += m;
+	}
+	return a;
+}
+
+mkdir(f: string, mode: int, mtime: int, uid: string, gid: string, existsok: int): ref Dir
+{
+	if(f == "/")
+		return nil;
+	d := newdir(basename(f), uid, gid, mode, mtime);
+	addfile(d, f, existsok);
+	return d;
+}
+
+addfile(d: ref Dir, path: string, existsok: int)
+{
+	elem: string;
+
+	opath := path;
+	p := prev := root;
+	basedir := "";
+# sys->print("addfile %s: %s\n", d.dir.name, path);
+	while(path != nil){
+		(elem, path) = split(path);
+		basedir += "/" + elem;
+		op := p;
+		p = lookup(p, elem);
+		if(path == nil){
+			if(p != nil){
+				if(!existsok && (p.dir.mode&Sys->DMDIR) == 0)
+					sys->fprint(sys->fildes(2), "addfile: %s already there", opath);
+					# fatal(sys->sprint("addfile: %s already there", opath));
+				return;
+			}
+			if(prev.child == nil)
+				prev.child = d;
+			else {
+				for(s := prev.child; s.sibling != nil; s = s.sibling)
+					;
+				s.sibling = d;
+			}
+			d.parent = prev;
+		}
+		else {
+			if(p == nil){
+				mkdir(basedir, 8r775|Sys->DMDIR, daytime->now(), UID, GID, 1);
+				p = lookup(op, elem);
+				if(p == nil)
+					fatal("bad file system");
+			}
+		}
+		prev = p;
+	}
+}
+
+lookup(p: ref Dir, f: string): ref Dir
+{
+	if((p.dir.mode&Sys->DMDIR) == 0) 
+		fatal("not a directory in lookup");
+	if(f == ".")
+		return p;
+	if(f == "..")
+		return p.parent;
+	for(d := p.child; d != nil; d = d.sibling)
+		if(d.dir.name == f)
+			return d;
+	return nil;
+}
+
+newdir(name, uid, gid: string, mode, mtime: int): ref Dir
+{
+	dir := sys->zerodir;
+	dir.name = name;
+	dir.uid = uid;
+	dir.gid = gid;
+	dir.mode = mode;
+	dir.qid.path = big (qid++);
+	dir.qid.qtype = mode>>24;
+	dir.qid.vers = 0;
+	dir.atime = dir.mtime = mtime;
+	dir.length = big 0;
+
+	d := ref Dir;
+	d.dir = dir;
+	d.offset = big 0;
+	return d;
+}
+
+prd(d: ref Dir)
+{
+	dir := d.dir;
+	sys->print("%q %q %q %bx %x %x %d %d %bd %d %d %bd\n",
+		dir.name, dir.uid, dir.gid, dir.qid.path, dir.qid.vers, dir.mode, dir.atime, dir.mtime, dir.length, dir.dtype, dir.dev, d.offset);
+}
+
+fatal(e: string)
+{
+	sys->fprint(sys->fildes(2), "archfs: %s\n", e);
+	raise "fail:error";
+}
+
+openarch(file: string): ref Archive
+{
+	b := bufio->open(file, Bufio->OREAD);
+	if(b == nil)
+		return nil;
+	ar := ref Archive;
+	ar.b = b;
+	ar.nexthdr = big 0;
+	ar.canseek = 1;
+	ar.hdr = ref Ahdr;
+	ar.hdr.d = ref Sys->Dir;
+	return ar;
+}
+
+NFLDS: con 6;
+
+gethdr(ar: ref Archive): ref Ahdr
+{
+	a := ar.hdr;
+	b := ar.b;
+	m := b.offset();
+	n := ar.nexthdr;
+	if(m != n){
+		if(ar.canseek)
+			b.seek(n, Bufio->SEEKSTART);
+		else {
+			if(m > n)
+				fatal(sys->sprint("bad offset in gethdr: m=%bd n=%bd", m, n));
+			if(drain(ar, int(n-m)) < 0)
+				return nil;
+		}
+	}
+	if((s := b.gets('\n')) == nil){
+		ar.err = "premature end of archive";
+		return nil;
+	}
+	if(s == "end of archive\n")
+		return nil;
+	(nf, fs) := sys->tokenize(s, " \t\n");
+	if(nf != NFLDS){
+		ar.err = "too few fields in file header";
+		return nil;
+	}
+	a.name = hd fs;						fs = tl fs;
+	(a.d.mode, nil) = str->toint(hd fs, 8);		fs = tl fs;
+	a.d.uid = hd fs;						fs = tl fs;
+	a.d.gid = hd fs;						fs = tl fs;
+	(a.d.mtime, nil) = str->toint(hd fs, 10);	fs = tl fs;
+	(tmp, nil) := str->toint(hd fs, 10);		fs = tl fs;
+	a.d.length = big tmp;
+	ar.nexthdr = b.offset()+a.d.length;
+	return a;
+}
+
+drain(ar: ref Archive, n: int): int
+{
+	while(n > 0){
+		m := n;
+		if(m > len buf)
+			m = len buf;
+		p := ar.b.read(buf, m);
+		if(p != m){
+			ar.err = "unexpectedly short read";
+			return -1;
+		}
+		n -= m;
+	}
+	return 0;	
+}
--- /dev/null
+++ b/appl/cmd/asm/asm.b
@@ -1,0 +1,2369 @@
+implement Asm;
+
+#line	2	"asm.y"
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "math.m";
+	math: Math;
+	export_real: import math;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+
+include "../limbo/isa.m";
+
+YYSTYPE: adt {
+	inst:	ref Inst;
+	addr:	ref Addr;
+	op:	int;
+	ival:	big;
+	fval:	real;
+	str:	string;
+	sym:	ref Sym;
+	listv:	ref List;
+};
+
+YYLEX: adt {
+	lval:	YYSTYPE;
+	EOF:	con -1;
+	lex:	fn(l: self ref YYLEX): int;
+	error:	fn(l: self ref YYLEX, msg: string);
+
+	numsym:	fn(l: self ref YYLEX, first: int): int;
+	eatstring:	fn(l: self ref YYLEX);
+};
+
+Eof: con -1;
+False: con 0;
+True: con 1;
+Strsize: con 1024;
+Hashsize: con 128;
+
+Addr: adt
+{
+	mode:	int;
+	off:	int;
+	val:	int;
+	sym:	ref Sym;
+
+	text:	fn(a: self ref Addr): string;
+};
+
+List: adt
+{
+	link:	cyclic ref List;
+	addr:	int;
+	typ:	int;
+	pick{
+	Int =>	ival: big;	# DEFB, DEFW, DEFL
+	Bytes =>	b: array of byte;	# DEFF, DEFS
+	Array =>	a: ref Array;	# DEFA
+	}
+};
+
+Inst: adt
+{
+	op:	int;
+	typ:	int;
+	size:	int;
+	reg:	ref Addr;
+	src:	ref Addr;
+	dst:	ref Addr;
+	pc:	int;
+	sym:	ref Sym;
+	link:	cyclic ref Inst;
+
+	text:	fn(i: self ref Inst): string;
+};
+
+Sym: adt
+{
+	name:	string;
+	lexval:	int;
+	value:	int;
+	ds:	int;
+};
+
+Desc: adt
+{
+	id:	int;
+	size:	int;
+	np:	int;
+	map:	array of byte;
+	link:	cyclic ref Desc;
+};
+
+Array: adt
+{
+	i:	int;
+	size:	int;
+};
+
+Link: adt
+{
+	desc:	int;
+	addr:	int;
+	typ:	int;
+	name:	string;
+	link:	cyclic ref Link;
+};
+
+Keywd: adt
+{
+	name:	string;
+	op:	int;
+	terminal:	int;
+};
+
+Ldts: adt
+{
+	n:	int;
+	ldt:	list of ref Ldt;
+};
+
+Ldt: adt
+{
+	sign:	int;
+	name:	string;
+};
+
+Exc: adt
+{
+	n1, n2, n3, n4, n5, n6: int;
+	etab: list of ref Etab;
+};
+
+Etab: adt
+{
+	n: int;
+	name:	string;
+};
+
+Asm: module {
+
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+TOKI0: con	57346;
+TOKI1: con	57347;
+TOKI2: con	57348;
+TOKI3: con	57349;
+TCONST: con	57350;
+TOKSB: con	57351;
+TOKFP: con	57352;
+TOKHEAP: con	57353;
+TOKDB: con	57354;
+TOKDW: con	57355;
+TOKDL: con	57356;
+TOKDF: con	57357;
+TOKDS: con	57358;
+TOKVAR: con	57359;
+TOKEXT: con	57360;
+TOKMOD: con	57361;
+TOKLINK: con	57362;
+TOKENTRY: con	57363;
+TOKARRAY: con	57364;
+TOKINDIR: con	57365;
+TOKAPOP: con	57366;
+TOKLDTS: con	57367;
+TOKEXCS: con	57368;
+TOKEXC: con	57369;
+TOKETAB: con	57370;
+TOKSRC: con	57371;
+TID: con	57372;
+TFCONST: con	57373;
+TSTRING: con	57374;
+
+};
+YYEOFCODE: con 1;
+YYERRCODE: con 2;
+YYMAXDEPTH: con 200;
+
+#line	527	"asm.y"
+
+
+kinit()
+{
+	for(i := 0; keywds[i].name != nil; i++) {
+		s := enter(keywds[i].name, keywds[i].terminal);
+		s.value = keywds[i].op;
+	}
+
+	enter("desc", TOKHEAP);
+	enter("mp", TOKSB);
+	enter("fp", TOKFP);
+
+	enter("byte", TOKDB);
+	enter("word", TOKDW);
+	enter("long", TOKDL);
+	enter("real", TOKDF);
+	enter("string", TOKDS);
+	enter("var", TOKVAR);
+	enter("ext", TOKEXT);
+	enter("module", TOKMOD);
+	enter("link", TOKLINK);
+	enter("entry", TOKENTRY);
+	enter("array", TOKARRAY);
+	enter("indir", TOKINDIR);
+	enter("apop", TOKAPOP);
+	enter("ldts", TOKLDTS);
+	enter("exceptions", TOKEXCS);
+	enter("exception", TOKEXC);
+	enter("exctab", TOKETAB);
+	enter("source", TOKSRC);
+
+	cmap['0'] = '\0'+1;
+	cmap['z'] = '\0'+1;
+	cmap['n'] = '\n'+1;
+	cmap['r'] = '\r'+1;
+	cmap['t'] = '\t'+1;
+	cmap['b'] = '\b'+1;
+	cmap['f'] = '\f'+1;
+	cmap['a'] = '\a'+1;
+	cmap['v'] = '\v'+1;
+	cmap['\\'] = '\\'+1;
+	cmap['"'] = '"'+1;
+}
+
+Bgetc(b: ref Iobuf): int
+{
+	return b.getb();
+}
+
+Bungetc(b: ref Iobuf)
+{
+	b.ungetb();
+}
+
+Bgetrune(b: ref Iobuf): int
+{
+	return b.getc();
+}
+
+Bputc(b: ref Iobuf, c: int)
+{
+	b.putb(byte c);
+}
+
+strchr(s: string, c: int): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return s[i:];
+	return nil;
+}
+
+escchar(c: int): int
+{
+	buf := array[32] of byte;
+	if(c >= '0' && c <= '9') {
+		n := 1;
+		buf[0] = byte c;
+		for(;;) {
+			c = Bgetc(bin);
+			if(c == Eof)
+				fatal(sys->sprint("%d: <eof> in escape sequence", line));
+			if(strchr("0123456789xX", c) == nil) {
+				Bungetc(bin);
+				break;
+			}
+			buf[n++] = byte c;
+		}
+		return int string buf[0:n];
+	}
+
+	n := cmap[c];
+	if(n == 0)
+		return c;
+	return n-1;
+}
+
+strbuf := array[Strsize] of byte;
+
+resizebuf()
+{
+	t := array[len strbuf+Strsize] of byte;
+	t[0:] = strbuf;
+	strbuf = t;
+}
+
+YYLEX.eatstring(l: self ref YYLEX)
+{
+	esc := 0;
+Scan:
+	for(cnt := 0;;) {
+		c := Bgetc(bin);
+		case c {
+		Eof =>
+			fatal(sys->sprint("%d: <eof> in string constant", line));
+
+		'\n' =>
+			line++;
+			diag("newline in string constant");
+			break Scan;
+
+		'\\' =>
+			if(esc) {
+				if(cnt >= len strbuf)
+					resizebuf();
+				strbuf[cnt++] = byte c;
+				esc = 0;
+				break;
+			}
+			esc = 1;
+
+		'"' =>
+			if(esc == 0)
+				break Scan;
+			c = escchar(c);
+			esc = 0;
+			if(cnt >= len strbuf)
+				resizebuf();
+			strbuf[cnt++] = byte c;
+
+		* =>
+			if(esc) {
+				c = escchar(c);
+				esc = 0;
+			}
+			if(cnt >= len strbuf)
+				resizebuf();
+			strbuf[cnt++] = byte c;
+		}
+	}
+	l.lval.str = string strbuf[0: cnt];
+}
+
+eatnl()
+{
+	line++;
+	for(;;) {
+		c := Bgetc(bin);
+		if(c == Eof)
+			diag("eof in comment");
+		if(c == '\n')
+			return;
+	}
+}
+
+YYLEX.lex(l: self ref YYLEX): int
+{
+	for(;;){
+		c := Bgetc(bin);
+		case c {
+		Eof =>
+			return Eof;
+		'"' =>
+			l.eatstring();
+			return TSTRING;
+		' ' or
+		'\t' or
+		'\r' =>
+			continue;
+		'\n' =>
+			line++;
+		'.' =>
+			c = Bgetc(bin);
+			Bungetc(bin);
+			if(isdigit(c))
+				return l.numsym('.');
+			return '.';
+		'#' =>
+			eatnl();
+		'(' or
+		')' or
+		';' or
+		',' or
+		'~' or
+		'$' or
+		'+' or
+		'/' or
+		'%' or
+		'^' or
+		'*' or
+		'&' or
+		'=' or
+		'|' or
+		'<' or
+		'>' or
+		'-' or
+		':' =>
+			return c;
+		'\'' =>
+			c = Bgetrune(bin);
+			if(c == '\\')
+				l.lval.ival = big escchar(Bgetc(bin));
+			else
+				l.lval.ival = big c;
+			c = Bgetc(bin);
+			if(c != '\'') {
+				diag("missing '");
+				Bungetc(bin);
+			}
+			return TCONST;
+
+		* =>
+			return l.numsym(c);
+		}
+	}
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isxdigit(c: int): int
+{
+	return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F';
+}
+
+isalnum(c: int): int
+{
+	return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || isdigit(c);
+}
+
+YYLEX.numsym(l: self ref YYLEX, first: int): int
+{
+	Int, Hex, Frac, Expsign, Exp: con iota;
+	state: int;
+
+	symbol[0] = byte first;
+	p := 0;
+
+	if(first == '.')
+		state = Frac;
+	else
+		state = Int;
+
+	c: int;
+	if(isdigit(int symbol[p++]) || state == Frac) {
+	Collect:
+		for(;;) {
+			c = Bgetc(bin);
+			if(c < 0)
+				fatal(sys->sprint("%d: <eof> eating numeric", line));
+
+			case state {
+			Int =>
+				if(isdigit(c))
+					break;
+				case c {
+				'x' or
+				'X' =>
+					c = 'x';
+					state = Hex;
+				'.' =>
+					state = Frac;
+				'e' or
+				'E' =>
+					c = 'e';
+					state = Expsign;
+				* =>
+					break Collect;
+				}
+			Hex =>
+				if(!isxdigit(c))
+					break Collect;
+			Frac =>
+				if(isdigit(c))
+					break;
+				if(c != 'e' && c != 'E')
+					break Collect;
+				c = 'e';
+				state = Expsign;
+			Expsign =>
+				state = Exp;
+				if(c == '-' || c == '+')
+					break;
+				if(!isdigit(c))
+					break Collect;
+			Exp =>
+				if(!isdigit(c))
+					break Collect;
+			}
+			symbol[p++] = byte c;
+		}
+
+		# break Collect
+		lastsym = string symbol[0:p];
+		Bungetc(bin);
+		case state {
+		Frac or
+		Expsign or
+		Exp =>
+			l.lval.fval = real lastsym;
+			return TFCONST;
+		* =>
+			if(len lastsym >= 3 && lastsym[0:2] == "0x")
+				(l.lval.ival, nil) = str->tobig(lastsym[2:], 16);
+			else
+				(l.lval.ival, nil) = str->tobig(lastsym, 10);
+			return TCONST;
+		}
+	}
+
+	for(;;) {
+		c = Bgetc(bin);
+		if(c < 0)
+			fatal(sys->sprint("%d <eof> eating symbols", line));
+		# '$' and '/' can occur in fully-qualified Java class names
+		if(c != '_' && c != '.' && c != '/' && c != '$' && !isalnum(c)) {
+			Bungetc(bin);
+			break;
+		}
+		symbol[p++] = byte c;
+	}
+
+	lastsym = string symbol[0:p];
+	s := enter(lastsym,TID);
+	case s.lexval {
+	TOKI0 or
+	TOKI1 or
+	TOKI2 or
+	TOKI3 =>
+		l.lval.op = s.value;
+	* =>
+		l.lval.sym = s;
+	}
+	return s.lexval;
+}
+
+hash := array[Hashsize] of list of ref Sym;
+
+enter(name: string, stype: int): ref Sym
+{
+	s := lookup(name);
+	if(s != nil)
+		return s;
+
+	h := 0;
+	for(p := 0; p < len name; p++)
+		h = h*3 + name[p];
+	if(h < 0)
+		h = ~h;
+	h %= Hashsize;
+
+	s = ref Sym(name, stype, 0, 0);
+	hash[h] = s :: hash[h];
+	return s;
+}
+
+lookup(name: string): ref Sym
+{
+	h := 0;
+	for(p := 0; p < len name; p++)
+		h = h*3 + name[p];
+	if(h < 0)
+		h = ~h;
+	h %= Hashsize;
+
+	for(l := hash[h]; l != nil; l = tl l)
+		if((s := hd l).name == name)
+			return s;
+	return nil;
+}
+
+YYLEX.error(l: self ref YYLEX, s: string)
+{
+	if(s == "syntax error") {
+		l.error(sys->sprint("syntax error, near symbol '%s'", lastsym));
+		return;
+	}
+	sys->print("%s %d: %s\n", file, line, s);
+	if(nerr++ > 10) {
+		sys->fprint(sys->fildes(2), "%s:%d: too many errors, giving up\n", file, line);
+		sys->remove(ofile);
+		raise "fail: yyerror";
+	}
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "asm: %d (fatal compiler problem) %s\n", line, s);
+	raise "fail:"+s;
+}
+
+diag(s: string)
+{
+	srcline := line;
+	sys->fprint(sys->fildes(2), "%s:%d: %s\n", file, srcline, s);
+	if(nerr++ > 10) {
+		sys->fprint(sys->fildes(2), "%s:%d: too many errors, giving up\n", file, line);
+		sys->remove(ofile);
+		raise "fail: error";
+	}
+}
+
+zinst: Inst;
+
+ai(op: int): ref Inst
+{
+	i := ref zinst;
+	i.op = op;
+
+	return i;
+}
+
+aa(val: big): ref Addr
+{
+	if(val <= big -1073741824 && val > big 1073741823)
+		diag("offset out of range");
+	return ref Addr(0, 0, int val, nil);
+}
+
+isoff2big(o: int): int
+{
+	return o < 0 || o > 16rFFFF;
+}
+
+inldt := 0;
+nldts := 0;
+aldts: list of ref Ldts;
+curl: ref Ldts;
+nexcs := 0;
+aexcs: list of ref Exc;
+cure: ref Exc;
+srcpath: string;
+
+bin: ref Iobuf;
+bout: ref Iobuf;
+
+line := 0;
+heapid := 0;
+symbol := array[1024] of byte;
+lastsym: string;
+nerr := 0;
+cmap := array[256] of int;
+file: string;
+
+dlist: ref Desc;
+dcout := 0;
+dseg := 0;
+dcount := 0;
+
+mdata: ref List;
+amodule: ref Sym;
+links: ref Link;
+linkt: ref Link;
+nlink := 0;
+listing := 0;
+mustcompile := 0;
+dontcompile := 0;
+ofile: string;
+dentry := 0;
+pcentry := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	math = load Math Math->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->setusage("asm [-l] file.s");
+	arg->init(args);
+	while((c := arg->opt()) != 0){
+		case c {
+		'C' =>	dontcompile++;
+		'c' =>	mustcompile++;
+		'l' =>		listing++;
+		* =>		arg->usage();
+		}
+	}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+	arg = nil;
+
+	kinit();
+	pcentry = -1;
+	dentry = -1;
+
+	file = hd args;
+	bin = bufio->open(file, Bufio->OREAD);
+	if(bin == nil) {
+		sys->fprint(sys->fildes(2), "asm: can't open %s: %r\n", file);
+		raise "fail: errors";
+	}
+	p := strrchr(file, '/');
+	if(p == nil)
+		p = file;
+	else
+		p = p[1:];
+	ofile = mkfile(p, ".s", ".dis");
+	bout = bufio->create(ofile, Bufio->OWRITE, 8r666);
+	if(bout == nil){
+		sys->fprint(sys->fildes(2), "asm: can't create: %s: %r\n", ofile);
+		raise "fail: errors";
+	}
+	line = 1;
+	yyparse(ref YYLEX);
+	bout.close();
+
+	if(nerr != 0){
+		sys->remove(ofile);
+		raise "fail: errors";
+	}
+}
+
+strrchr(s: string, c: int): string
+{
+	for(i := len s; --i >= 0;)
+		if(s[i] == c)
+			return s[i:];
+	return nil;
+}
+
+mkfile(file: string, oldext: string, ext: string): string
+{
+	n := len file;
+	n2 := len oldext;
+	if(n >= n2 && file[n-n2:] == oldext)
+		n -= n2;
+	return file[0:n] + ext;
+}
+
+opcode(i: ref Inst): int
+{
+	if(i.op < 0 || i.op >= len keywds)
+		fatal(sys->sprint("internal error: invalid op %d (%#x)", i.op, i.op));
+	return keywds[i.op].op;
+}
+
+Inst.text(i: self ref Inst): string
+{
+	if(i == nil)
+		return "IZ";
+
+	case keywds[i.op].terminal {
+	TOKI0 =>
+		return sys->sprint("%s", keywds[i.op].name);
+	TOKI1 =>
+		return sys->sprint("%s\t%s", keywds[i.op].name, i.dst.text());
+	TOKI3 =>
+		if(i.reg != nil) {
+			pre := "";
+			post := "";
+			case i.reg.mode {
+			AXIMM =>
+				pre = "$";
+				break;
+			AXINF =>
+				post = "(fp)";
+				break;
+			AXINM =>
+				post = "(mp)";
+			 	break;
+			}
+			return sys->sprint("%s\t%s, %s%d%s, %s", keywds[i.op].name, i.src.text(), pre, i.reg.val, post, i.dst.text());
+		}
+		return sys->sprint("%s\t%s, %s", keywds[i.op].name, i.src.text(), i.dst.text());
+	TOKI2 =>
+		return sys->sprint("%s\t%s, %s", keywds[i.op].name, i.src.text(), i.dst.text());
+	* =>
+		return "IGOK";
+	}
+}
+
+Addr.text(a: self ref Addr): string
+{
+	if(a == nil)
+		return "AZ";
+
+	if(a.mode & AIND) {		
+		case a.mode & ~AIND {
+		AFP =>
+			return sys->sprint("%d(%d(fp))", a.val, a.off);
+		AMP =>
+			return sys->sprint("%d(%d(mp))", a.val, a.off);
+		}
+	}
+	else {
+		case a.mode {
+		AFP =>
+			return sys->sprint("%d(fp)", a.val);
+		AMP =>
+			return sys->sprint("%d(mp)", a.val);
+		AIMM =>
+			return sys->sprint("$%d", a.val);
+		}
+	}
+
+	return "AGOK";
+}
+
+append[T](l: list of T, v: T): list of T
+{
+	if(l == nil)
+		return v :: nil;
+	return hd l :: append(tl l, v);
+}
+
+newa(i: int, size: int): ref List
+{
+	a := ref Array(i, size);
+	l := ref List.Array(nil, -1, 0, a);
+	return l;
+}
+
+# does order matter?
+newi(v: big, l: ref List): ref List
+{
+	n := ref List.Int(nil, -1, 0, v);
+	if(l == nil)
+		return n;
+
+	for(t := l; t.link != nil; t = t.link)
+		;
+	t.link = n;
+
+	return l;
+}
+
+news(s: string, l: ref List): ref List
+{
+	return ref List.Bytes(l, -1, 0, array of byte s);
+}
+
+newb(a: array of byte, l: ref List): ref List
+{
+	return ref List.Bytes(l, -1, 0, a);
+}
+
+digit(x: int): int
+{
+	if(x >= 'A' && x <= 'F')
+		return x - 'A' + 10;
+	if(x >= 'a' && x <= 'f')
+		return x - 'a' + 10;
+	if(x >= '0' && x <= '9')
+		return x - '0';
+	diag("bad hex value in pointers");
+	return 0;
+}
+
+heap(id: int, size: int, ptr: string)
+{
+	d := ref Desc;
+	d.id = id;
+	d.size = size;
+	size /= IBY2WD;
+	d.map = array[size] of {* => byte 0};
+	d.np = 0;
+	if(dlist == nil)
+		dlist = d;
+	else {
+		f: ref Desc;
+		for(f = dlist; f.link != nil; f = f.link)
+			;
+		f.link = d;
+	}
+	d.link = nil;
+	dcount++;
+
+	if(ptr == nil)
+		return;
+	if(len ptr & 1) {
+		diag("pointer descriptor has odd length");
+		return;	
+	}
+
+	k := 0;
+	l := len ptr;
+	for(i := 0; i < l; i += 2) {
+		d.map[k++] = byte ((digit(ptr[i])<<4)|digit(ptr[i+1]));
+		if(k > size) {
+			diag("pointer descriptor too long");
+			break;
+		}
+	}
+	d.np = k;
+}
+
+conout(val: int)
+{
+	if(val >= -64 && val <= 63) {
+		Bputc(bout, val & ~16r80);
+		return;
+	}
+	if(val >= -8192 && val <= 8191) {
+		Bputc(bout, ((val>>8) & ~16rC0) | 16r80);
+		Bputc(bout, val);
+		return;
+	}
+	if(val < 0 && ((val >> 29) & 7) != 7
+	|| val > 0 && (val >> 29) != 0)
+		diag(sys->sprint("overflow in constant 0x%ux\n", val));
+	Bputc(bout, (val>>24) | 16rC0);
+	Bputc(bout, val>>16);
+	Bputc(bout, val>>8);
+	Bputc(bout, val);
+}
+
+aout(a: ref Addr)
+{
+	if(a == nil)
+		return;
+	if(a.mode & AIND)
+		conout(a.off);
+	conout(a.val);
+}
+
+Bputs(b: ref Iobuf, s: string)
+{
+	for(i := 0; i < len s; i++)
+		Bputc(b, s[i]);
+	Bputc(b, '\0');
+}
+
+lout()
+{
+	if(amodule == nil)
+		amodule = enter("main", 0);
+
+	Bputs(bout, amodule.name);
+
+	for(l := links; l != nil; l = l.link) {
+		conout(l.addr);
+		conout(l.desc);
+		Bputc(bout, l.typ>>24);
+		Bputc(bout, l.typ>>16);
+		Bputc(bout, l.typ>>8);
+		Bputc(bout, l.typ);
+		Bputs(bout, l.name);
+	}
+}
+
+ldtout()
+{
+	conout(nldts);
+	for(la := aldts; la != nil; la = tl la){
+		ls := hd la;
+		conout(ls.n);
+		for(l := ls.ldt; l != nil; l = tl l){
+			t := hd l;
+			Bputc(bout, t.sign>>24);
+			Bputc(bout, t.sign>>16);
+			Bputc(bout, t.sign>>8);
+			Bputc(bout, t.sign);
+			Bputs(bout, t.name);
+		}
+	}
+	conout(0);
+}
+
+excout()
+{
+	if(nexcs == 0)
+		return;
+	conout(nexcs);
+	for(es := aexcs; es != nil; es = tl es){
+		e := hd es;
+		conout(e.n3);
+		conout(e.n1);
+		conout(e.n2);
+		conout(e.n4);
+		conout(e.n5|(e.n6<<16));
+		for(ets := e.etab; ets != nil; ets = tl ets){
+			et := hd ets;
+			if(et.name != nil)
+				Bputs(bout, et.name);
+			conout(et.n);
+		}
+	}
+	conout(0);
+}
+
+srcout()
+{
+	if(srcpath == nil)
+		return;
+	Bputs(bout, srcpath);
+}
+
+assem(i: ref Inst)
+{
+	f: ref Inst;
+	while(i != nil){
+		link := i.link;
+		i.link = f;
+		f = i;
+		i = link;
+	}
+	i = f;
+
+	pc := 0;
+	for(f = i; f != nil; f = f.link) {
+		f.pc = pc++;
+		if(f.sym != nil)
+			f.sym.value = f.pc;
+	}
+
+	if(pcentry >= pc)
+		diag("entry pc out of range");
+	if(dentry >= dcount)
+		diag("entry descriptor out of range");
+
+	conout(XMAGIC);
+	hints := 0;
+	if(mustcompile)
+		hints |= MUSTCOMPILE;
+	if(dontcompile)
+		hints |= DONTCOMPILE;
+	hints |= HASLDT;
+	if(nexcs > 0)
+		hints |= HASEXCEPT;
+	conout(hints);		# Runtime flags
+	conout(1024);		# default stack size
+	conout(pc);
+	conout(dseg);
+	conout(dcount);
+	conout(nlink);
+	conout(pcentry);
+	conout(dentry);
+
+	for(f = i; f != nil; f = f.link) {
+		if(f.dst != nil && f.dst.sym != nil) {
+			f.dst.mode = AIMM;
+			f.dst.val = f.dst.sym.value;
+		}
+		o := opcode(f);
+		if(o == IRAISE){
+			f.src = f.dst;
+			f.dst = nil;
+		}
+		Bputc(bout, o);
+		n := 0;
+		if(f.src != nil)
+			n |= src(f.src.mode);
+		else
+			n |= src(AXXX);
+		if(f.dst != nil)
+			n |= dst(f.dst.mode);
+		else
+			n |= dst(AXXX);
+		if(f.reg != nil)
+			n |= f.reg.mode;
+		else
+			n |= AXNON;
+		Bputc(bout, n);
+		aout(f.reg);
+		aout(f.src);
+		aout(f.dst);
+
+		if(listing)
+			sys->print("%4d %s\n", f.pc, f.text());
+	}
+
+	for(d := dlist; d != nil; d = d.link) {
+		conout(d.id);
+		conout(d.size);
+		conout(d.np);
+		for(n := 0; n < d.np; n++)
+			Bputc(bout, int d.map[n]);
+	}
+
+	dout();
+	lout();
+	ldtout();
+	excout();
+	srcout();
+}
+
+data(typ: int, addr: big, l: ref List)
+{
+	if(inldt){
+		ldtw(int intof(l));
+		return;
+	}
+
+	l.typ = typ;
+	l.addr = int addr;
+
+	if(mdata == nil)
+		mdata = l;
+	else {
+		for(f := mdata; f.link != nil; f = f.link)
+			;
+		f.link = l;
+	}
+}
+
+ext(addr: int, typ: int, s: string)
+{
+	if(inldt){
+		ldte(typ, s);
+		return;
+	}
+
+	data(DEFW, big addr, newi(big typ, nil));
+
+	n: ref List;
+	for(i := 0; i < len s; i++)
+		n = newi(big s[i], n);
+	data(DEFB, big(addr+IBY2WD), n);
+
+	if(addr+len s > dseg)
+		diag("ext beyond mp");
+}
+
+mklink(desc: int, addr: int, typ: int, s: string)
+{
+	for(ls := links; ls != nil; ls = ls.link)
+		if(ls.name == s)
+			diag(sys->sprint("%s already defined", s));
+
+	nlink++;
+	l := ref Link;
+	l.desc = desc;
+	l.addr = addr;
+	l.typ = typ;
+	l.name = s;
+	l.link = nil;
+
+	if(links == nil)
+		links = l;
+	else
+		linkt.link = l;
+	linkt = l;
+}
+
+intof(l: ref List): big
+{
+	pick rl := l {
+	Int =>
+		return rl.ival;
+	* =>
+		raise "list botch";
+	}
+}
+
+arrayof(l: ref List): ref Array
+{
+	pick rl := l {
+	Array =>
+		return rl.a;
+	* =>
+		raise "list botch";
+	}
+}
+
+bytesof(l: ref List): array of byte
+{
+	pick rl := l {
+	Bytes =>
+		return rl.b;
+	* =>
+		raise "list botch";
+	}
+}
+
+nel(l: ref List): (int, ref List)
+{
+	n := 1;
+	for(e := l.link; e != nil && e.addr == -1; e = e.link)
+		n++;
+	return (n, e);
+}
+
+dout()
+{
+	e: ref List;
+	n: int;
+	for(l := mdata; l != nil; l = e) {
+		case l.typ {
+		DEFB =>
+			(n, e) = nel(l);
+			if(n < DMAX)
+				Bputc(bout, dbyte(DEFB, n));
+			else {
+				Bputc(bout, dbyte(DEFB, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			while(l != e) {
+				Bputc(bout, int intof(l));
+				l = l.link;
+			}
+			break;
+		DEFW =>
+			(n, e) = nel(l);
+			if(n < DMAX)
+				Bputc(bout, dbyte(DEFW, n));
+			else {
+				Bputc(bout, dbyte(DEFW, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			while(l != e) {
+				n = int intof(l);
+				Bputc(bout, n>>24);
+				Bputc(bout, n>>16);
+				Bputc(bout, n>>8);
+				Bputc(bout, n);
+				l = l.link;
+			}
+			break;
+		DEFL =>
+			(n, e) = nel(l);
+			if(n < DMAX)
+				Bputc(bout, dbyte(DEFL, n));
+			else {
+				Bputc(bout, dbyte(DEFL, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			while(l != e) {
+				b := intof(l);
+				Bputc(bout, int (b>>56));
+				Bputc(bout, int (b>>48));
+				Bputc(bout, int (b>>40));
+				Bputc(bout, int (b>>32));
+				Bputc(bout, int (b>>24));
+				Bputc(bout, int (b>>16));
+				Bputc(bout, int (b>>8));
+				Bputc(bout, int b);
+				l = l.link;
+			}
+			break;
+		DEFF =>
+			(n, e) = nel(l);
+			if(n < DMAX)
+				Bputc(bout, dbyte(DEFF, n));
+			else {
+				Bputc(bout, dbyte(DEFF, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			while(l != e) {
+				b := bytesof(l);
+				Bputc(bout, int b[0]);
+				Bputc(bout, int b[1]);
+				Bputc(bout, int b[2]);
+				Bputc(bout, int b[3]);
+				Bputc(bout, int b[4]);
+				Bputc(bout, int b[5]);
+				Bputc(bout, int b[6]);
+				Bputc(bout, int b[7]);
+				l = l.link;
+			}
+			break;
+		DEFS =>
+			a := bytesof(l);
+			n = len a;
+			if(n < DMAX && n != 0)
+				Bputc(bout, dbyte(DEFS, n));
+			else {
+				Bputc(bout, dbyte(DEFS, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			for(i := 0; i < n; i++)
+				Bputc(bout, int a[i]);
+
+			e = l.link;
+			break;
+		DEFA =>
+			Bputc(bout, dbyte(DEFA, 1));
+			conout(l.addr);
+			ar := arrayof(l);
+			Bputc(bout, ar.i>>24);
+			Bputc(bout, ar.i>>16);
+			Bputc(bout, ar.i>>8);
+			Bputc(bout, ar.i);
+			Bputc(bout, ar.size>>24);
+			Bputc(bout, ar.size>>16);
+			Bputc(bout, ar.size>>8);
+			Bputc(bout, ar.size);
+			e = l.link;
+			break;
+		DIND =>
+			Bputc(bout, dbyte(DIND, 1));
+			conout(l.addr);
+			Bputc(bout, 0);
+			Bputc(bout, 0);
+			Bputc(bout, 0);
+			Bputc(bout, 0);
+			e = l.link;
+			break;
+		DAPOP =>
+			Bputc(bout, dbyte(DAPOP, 1));
+			conout(0);
+			e = l.link;
+			break;
+		}
+	}
+
+	Bputc(bout, dbyte(DEFZ, 0));
+}
+
+ldts(n: int)
+{
+	nldts = n;
+	inldt = 1;
+}
+
+ldtw(n: int)
+{
+	ls := ref Ldts(n, nil);
+	aldts = append(aldts, ls);
+	curl = ls;
+}
+
+ldte(n: int, s: string)
+{
+	l := ref Ldt(n, s);
+	curl.ldt = append(curl.ldt, l);
+}
+
+excs(n: int)
+{
+	nexcs = n;
+}
+
+exc(n1: int, n2: int, n3: int, n4: int, n5: int, n6: int)
+{
+	e := ref Exc;
+	e.n1 = n1;
+	e.n2 = n2;
+	e.n3 = n3;
+	e.n4 = n4;
+	e.n5 = n5;
+	e.n6 = n6;
+	e.etab = nil;
+	aexcs = append(aexcs, e);
+	cure = e;
+}
+
+etab(s: string, n: int)
+{
+	et := ref Etab;
+	et.n = n;
+	et.name = s;
+	cure.etab = append(cure.etab, et);
+}
+
+source(s: string)
+{
+	srcpath = s;
+}
+
+dtype(x: int): int
+{
+	return (x>>4)&16rF;
+}
+
+dbyte(x: int, l: int): int
+{
+	return (x<<4) | l;
+}
+
+dlen(x: int): int
+{
+	return x & (DMAX-1);
+}
+
+src(x: int): int
+{
+	return x<<3;
+}
+
+dst(x: int): int
+{
+	return x<<0;
+}
+
+dtocanon(d: real): array of byte
+{
+	b := array[8] of byte;
+	export_real(b, array[] of {d});
+	return b;
+}
+
+keywds: array of Keywd = array[] of
+{
+	("nop",		INOP,		TOKI0),
+	("alt",		IALT,		TOKI3),
+	("nbalt",	INBALT,		TOKI3),
+	("goto",		IGOTO,		TOKI2),
+	("call",		ICALL,		TOKI2),
+	("frame",	IFRAME,		TOKI2),
+	("spawn",	ISPAWN,		TOKI2),
+	("runt",		IRUNT,		TOKI2),
+	("load",		ILOAD,		TOKI3),
+	("mcall",	IMCALL,		TOKI3),
+	("mspawn",	IMSPAWN,	TOKI3),
+	("mframe",	IMFRAME,	TOKI3),
+	("ret",		IRET,		TOKI0),
+	("jmp",		IJMP,		TOKI1),
+	("case",		ICASE,		TOKI2),
+	("exit",		IEXIT,		TOKI0),
+	("new",		INEW,		TOKI2),
+	("newa",		INEWA,		TOKI3),
+	("newcb",	INEWCB,		TOKI1),
+	("newcw",	INEWCW,		TOKI1),
+	("newcf",	INEWCF,		TOKI1),
+	("newcp",	INEWCP,		TOKI1),
+	("newcm",	INEWCM,		TOKI2),
+	("newcmp",	INEWCMP,	TOKI2),
+	("send",		ISEND,		TOKI2),
+	("recv",		IRECV,		TOKI2),
+	("consb",	ICONSB,		TOKI2),
+	("consw",	ICONSW,		TOKI2),
+	("consp",	ICONSP,		TOKI2),
+	("consf",	ICONSF,		TOKI2),
+	("consm",	ICONSM,		TOKI3),
+	("consmp",	ICONSMP,	TOKI3),
+	("headb",	IHEADB,		TOKI2),
+	("headw",	IHEADW,		TOKI2),
+	("headp",	IHEADP,		TOKI2),
+	("headf",	IHEADF,		TOKI2),
+	("headm",	IHEADM,		TOKI3),
+	("headmp",	IHEADMP,	TOKI3),
+	("tail",		ITAIL,		TOKI2),
+	("lea",		ILEA,		TOKI2),
+	("indx",		IINDX,		TOKI3),
+	("movp",		IMOVP,		TOKI2),
+	("movm",		IMOVM,		TOKI3),
+	("movmp",	IMOVMP,		TOKI3),
+	("movb",		IMOVB,		TOKI2),
+	("movw",		IMOVW,		TOKI2),
+	("movf",		IMOVF,		TOKI2),
+	("cvtbw",	ICVTBW,		TOKI2),
+	("cvtwb",	ICVTWB,		TOKI2),
+	("cvtfw",	ICVTFW,		TOKI2),
+	("cvtwf",	ICVTWF,		TOKI2),
+	("cvtca",	ICVTCA,		TOKI2),
+	("cvtac",	ICVTAC,		TOKI2),
+	("cvtwc",	ICVTWC,		TOKI2),
+	("cvtcw",	ICVTCW,		TOKI2),
+	("cvtfc",	ICVTFC,		TOKI2),
+	("cvtcf",	ICVTCF,		TOKI2),
+	("addb",		IADDB,		TOKI3),
+	("addw",		IADDW,		TOKI3),
+	("addf",		IADDF,		TOKI3),
+	("subb",		ISUBB,		TOKI3),
+	("subw",		ISUBW,		TOKI3),
+	("subf",		ISUBF,		TOKI3),
+	("mulb",		IMULB,		TOKI3),
+	("mulw",		IMULW,		TOKI3),
+	("mulf",		IMULF,		TOKI3),
+	("divb",		IDIVB,		TOKI3),
+	("divw",		IDIVW,		TOKI3),
+	("divf",		IDIVF,		TOKI3),
+	("modw",		IMODW,		TOKI3),
+	("modb",		IMODB,		TOKI3),
+	("andb",		IANDB,		TOKI3),
+	("andw",		IANDW,		TOKI3),
+	("orb",		IORB,		TOKI3),
+	("orw",		IORW,		TOKI3),
+	("xorb",		IXORB,		TOKI3),
+	("xorw",		IXORW,		TOKI3),
+	("shlb",		ISHLB,		TOKI3),
+	("shlw",		ISHLW,		TOKI3),
+	("shrb",		ISHRB,		TOKI3),
+	("shrw",		ISHRW,		TOKI3),
+	("insc",		IINSC,		TOKI3),
+	("indc",		IINDC,		TOKI3),
+	("addc",		IADDC,		TOKI3),
+	("lenc",		ILENC,		TOKI2),
+	("lena",		ILENA,		TOKI2),
+	("lenl",		ILENL,		TOKI2),
+	("beqb",		IBEQB,		TOKI3),
+	("bneb",		IBNEB,		TOKI3),
+	("bltb",		IBLTB,		TOKI3),
+	("bleb",		IBLEB,		TOKI3),
+	("bgtb",		IBGTB,		TOKI3),
+	("bgeb",		IBGEB,		TOKI3),
+	("beqw",		IBEQW,		TOKI3),
+	("bnew",		IBNEW,		TOKI3),
+	("bltw",		IBLTW,		TOKI3),
+	("blew",		IBLEW,		TOKI3),
+	("bgtw",		IBGTW,		TOKI3),
+	("bgew",		IBGEW,		TOKI3),
+	("beqf",		IBEQF,		TOKI3),
+	("bnef",		IBNEF,		TOKI3),
+	("bltf",		IBLTF,		TOKI3),
+	("blef",		IBLEF,		TOKI3),
+	("bgtf",		IBGTF,		TOKI3),
+	("bgef",		IBGEF,		TOKI3),
+	("beqc",		IBEQC,		TOKI3),
+	("bnec",		IBNEC,		TOKI3),
+	("bltc",		IBLTC,		TOKI3),
+	("blec",		IBLEC,		TOKI3),
+	("bgtc",		IBGTC,		TOKI3),
+	("bgec",		IBGEC,		TOKI3),
+	("slicea",	ISLICEA,	TOKI3),
+	("slicela",	ISLICELA,	TOKI3),
+	("slicec",	ISLICEC,	TOKI3),
+	("indw",		IINDW,		TOKI3),
+	("indf",		IINDF,		TOKI3),
+	("indb",		IINDB,		TOKI3),
+	("negf",		INEGF,		TOKI2),
+	("movl",		IMOVL,		TOKI2),
+	("addl",		IADDL,		TOKI3),
+	("subl",		ISUBL,		TOKI3),
+	("divl",		IDIVL,		TOKI3),
+	("modl",		IMODL,		TOKI3),
+	("mull",		IMULL,		TOKI3),
+	("andl",		IANDL,		TOKI3),
+	("orl",		IORL,		TOKI3),
+	("xorl",		IXORL,		TOKI3),
+	("shll",		ISHLL,		TOKI3),
+	("shrl",		ISHRL,		TOKI3),
+	("bnel",		IBNEL,		TOKI3),
+	("bltl",		IBLTL,		TOKI3),
+	("blel",		IBLEL,		TOKI3),
+	("bgtl",		IBGTL,		TOKI3),
+	("bgel",		IBGEL,		TOKI3),
+	("beql",		IBEQL,		TOKI3),
+	("cvtlf",	ICVTLF,		TOKI2),
+	("cvtfl",	ICVTFL,		TOKI2),
+	("cvtlw",	ICVTLW,		TOKI2),
+	("cvtwl",	ICVTWL,		TOKI2),
+	("cvtlc",	ICVTLC,		TOKI2),
+	("cvtcl",	ICVTCL,		TOKI2),
+	("headl",	IHEADL,		TOKI2),
+	("consl",	ICONSL,		TOKI2),
+	("newcl",	INEWCL,		TOKI1),
+	("casec",	ICASEC,		TOKI2),
+	("indl",		IINDL,		TOKI3),
+	("movpc",	IMOVPC,		TOKI2),
+	("tcmp",		ITCMP,		TOKI2),
+	("mnewz",	IMNEWZ,		TOKI3),
+	("cvtrf",	ICVTRF,		TOKI2),
+	("cvtfr",	ICVTFR,		TOKI2),
+	("cvtws",	ICVTWS,		TOKI2),
+	("cvtsw",	ICVTSW,		TOKI2),
+	("lsrw",		ILSRW,		TOKI3),
+	("lsrl",		ILSRL,		TOKI3),
+	("eclr",		IECLR,		TOKI0),
+	("newz",		INEWZ,		TOKI2),
+	("newaz",	INEWAZ,		TOKI3),
+	("raise",	IRAISE,	TOKI1),
+	("casel",	ICASEL,	TOKI2),
+	("mulx",	IMULX,	TOKI3),
+	("divx",	IDIVX,	TOKI3),
+	("cvtxx",	ICVTXX,	TOKI3),
+	("mulx0",	IMULX0,	TOKI3),
+	("divx0",	IDIVX0,	TOKI3),
+	("cvtxx0",	ICVTXX0,	TOKI3),
+	("mulx1",	IMULX1,	TOKI3),
+	("divx1",	IDIVX1,	TOKI3),
+	("cvtxx1",	ICVTXX1,	TOKI3),
+	("cvtfx",	ICVTFX,	TOKI3),
+	("cvtxf",	ICVTXF,	TOKI3),
+	("expw",	IEXPW,	TOKI3),
+	("expl",	IEXPL,	TOKI3),
+	("expf",	IEXPF,	TOKI3),
+	("self",	ISELF,	TOKI1),
+	(nil,	0, 0),
+};
+yyexca := array[] of {-1, 1,
+	1, -1,
+	-2, 0,
+-1, 61,
+	4, 54,
+	5, 54,
+	6, 54,
+	7, 54,
+	8, 54,
+	9, 54,
+	10, 54,
+	11, 54,
+	12, 54,
+	13, 54,
+	46, 54,
+	-2, 46,
+-1, 140,
+	44, 44,
+	-2, 50,
+-1, 159,
+	44, 43,
+	-2, 45,
+};
+YYNPROD: con 70;
+YYPRIVATE: con 57344;
+yytoknames: array of string;
+yystates: array of string;
+include "y.debug";
+yydebug: con 1;
+YYLAST:	con 561;
+yyact := array[] of {
+  64,  59, 107,  65, 162, 161,  31, 160, 158,  34,
+  42,  43,  44,  45, 156,  47,  48,  33,  50,  51,
+  52,  30,  32,  54,  55, 148,  39,  38,  63,  66,
+  67, 105, 100,  70,  99,  36,  98,  96,  90,  69,
+  57, 172,  85,  81,  80,  79,  77,  78,  72,  73,
+  74,  75,  76, 165, 163, 126, 151,  61,  58,  53,
+  49, 101,  60,  41, 103,  40,  46, 102, 143, 144,
+ 106,  56, 108, 109, 110, 111, 112, 113, 153, 152,
+ 116, 117, 118,   7, 115, 114, 119, 108, 108, 120,
+ 121, 127, 128, 129, 130,   6, 132, 133, 134, 135,
+ 136, 131, 137,   1, 140, 103,  35, 145, 142, 146,
+  29,  28,  27,  26,  68, 149, 150,   5,   8,   9,
+  10,  11,  12,  13,  14,  16,  15,  17,  18,  19,
+  20,  21,  22,  23,  24,  25,   4,  74,  75,  76,
+ 159,  62, 138, 125,   2,  82,  83,  84,   3, 164,
+   0, 122,  29,  28,  27,  26, 166, 167, 168,   0,
+ 169,  81,  80,  79,  77,  78,  72,  73,  74,  75,
+  76,   0, 173, 124, 123, 175,   0, 177,  81,  80,
+  79,  77,  78,  72,  73,  74,  75,  76,  39,  38,
+   0,   0,  39,  38,  63,   0,   0,  36, 143, 144,
+   0,  36,   0, 141,  81,  80,  79,  77,  78,  72,
+  73,  74,  75,  76,  72,  73,  74,  75,  76,  37,
+ 104,   0,   0,  61,   0,  41,   0,  40, 139,  41,
+   0,  40,  81,  80,  79,  77,  78,  72,  73,  74,
+  75,  76,   0,   0, 176,  81,  80,  79,  77,  78,
+  72,  73,  74,  75,  76,  81,  80,  79,  77,  78,
+  72,  73,  74,  75,  76,  77,  78,  72,  73,  74,
+  75,  76, 174,  81,  80,  79,  77,  78,  72,  73,
+  74,  75,  76,   0,   0, 171,  80,  79,  77,  78,
+  72,  73,  74,  75,  76, 170,  81,  80,  79,  77,
+  78,  72,  73,  74,  75,  76,   0,   0,   0,   0,
+   0,   0,   0, 157,  81,  80,  79,  77,  78,  72,
+  73,  74,  75,  76,  81,  80,  79,  77,  78,  72,
+  73,  74,  75,  76,   0,   0, 155,  81,  80,  79,
+  77,  78,  72,  73,  74,  75,  76,   0,   0,   0,
+   0,   0,   0,   0, 154,  79,  77,  78,  72,  73,
+  74,  75,  76,   0, 147,  81,  80,  79,  77,  78,
+  72,  73,  74,  75,  76,   0,   0,  97,  81,  80,
+  79,  77,  78,  72,  73,  74,  75,  76,  81,  80,
+  79,  77,  78,  72,  73,  74,  75,  76,   0,   0,
+   0,   0,   0,   0,   0,  95,  81,  80,  79,  77,
+  78,  72,  73,  74,  75,  76,   0,   0,  94,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,  93,  81,
+  80,  79,  77,  78,  72,  73,  74,  75,  76,   0,
+   0,   0,   0,   0,   0,   0,  92,  81,  80,  79,
+  77,  78,  72,  73,  74,  75,  76,  81,  80,  79,
+  77,  78,  72,  73,  74,  75,  76,   0,   0,  91,
+  81,  80,  79,  77,  78,  72,  73,  74,  75,  76,
+   0,   0,   0,   0,   0,   0,   0,  89,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,  88,  81,  80,
+  79,  77,  78,  72,  73,  74,  75,  76,   0,   0,
+  87,  81,  80,  79,  77,  78,  72,  73,  74,  75,
+  76,  39,  38,   0,   0,   0,   0,   0,   0,   0,
+  36,   0,   0,   0,   0,   0,   0,   0,  86,  81,
+  80,  79,  77,  78,  72,  73,  74,  75,  76,   0,
+   0,  71,  37,   0,   0,   0,   0,   0,  41,   0,
+  40,
+};
+yypact := array[] of {
+-1000,-1000,  96,-1000, -22, -23,-1000,-1000, 512, 512,
+ 512, 512, 512,  26, 512, 512,  20, 512, 512, 512,
+-1000,  19, 512, 512,  29,  16,  17,  17,  17,-1000,
+ 138,  -5, 512,-1000, 507,-1000,-1000,-1000, 512, 512,
+ 512, 512, 494, 466, 453, 443,  -6, 425, 402,-1000,
+ 384, 374, 361,  -7, 535, 333,  -8, -10,-1000, -12,
+ 512,-1000,-1000, 512, 174,-1000, -13,-1000,-1000, 512,
+ 535, 512, 512, 512, 512, 512, 512,  78,  76, 512,
+ 512, 512,-1000,-1000,-1000,  39, 512, 512, 133,  13,
+ 512, 512, 512, 512, -23, 512, 512, 512, 512, 512,
+ 183, 535,-1000, 157, 179,  17, 320, -19, 535, 126,
+ 126,-1000,-1000,-1000, 512, 512, 258, 349, 281,-1000,
+ -19, -19,-1000,-1000,-1000,  38,-1000, 535, 310, 292,
+ 535, -30, 535, 535, 269, 535, 535,-1000, -36, 512,
+-1000,  49, -40, -42, -43,-1000,-1000,  12, 512, 205,
+ 205,-1000,-1000,-1000,  11, 512, 512, 512,  17, 535,
+-1000,-1000,-1000,-1000, 535,-1000, 251, 535, 241,-1000,
+  -1, 512,-1000, 228, 512, 200, 512, 535,
+};
+yypgo := array[] of {
+   0, 148, 144,  83, 106,   0,   6,   1, 142, 141,
+   3,   2, 109, 103,  95,
+};
+yyr1 := array[] of {
+   0,  13,   2,   2,   1,   1,   1,   1,   6,   6,
+  12,  12,  11,  11,   3,   3,   3,   3,   3,  14,
+  14,  14,  14,  14,  14,  14,  14,  14,  14,  14,
+  14,  14,  14,  14,  14,  14,  14,  14,  14,  14,
+  14,  14,  14,   8,   8,   7,   7,   7,   9,   9,
+   9,  10,  10,   4,   4,   4,   4,   4,   4,   5,
+   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,
+};
+yyr2 := array[] of {
+   0,   1,   0,   2,   3,   5,   1,   1,   2,   1,
+   0,   2,   1,   3,   4,   6,   4,   2,   1,   4,
+   4,   4,   4,   4,   4,   5,   5,   5,   4,   4,
+   6,   8,   2,   4,   6,   4,   1,   4,   2,  12,
+   4,   4,   2,   2,   1,   2,   1,   1,   2,   4,
+   1,   4,   4,   1,   1,   2,   2,   2,   3,   1,
+   3,   3,   3,   3,   3,   4,   4,   3,   3,   3,
+};
+yychk := array[] of {
+-1000, -13,  -2,  -1,  40,  21, -14,  -3,  22,  23,
+  24,  25,  26,  27,  28,  30,  29,  31,  32,  33,
+  34,  35,  36,  37,  38,  39,  17,  16,  15,  14,
+  43,  -6,  45,  40,  -5,  -4,  18,  40,  10,   9,
+  48,  46,  -5,  -5,  -5,  -5,  40,  -5,  -5,  40,
+  -5,  -5,  -5,  40,  -5,  -5,  42,  11,  42,  -7,
+  45,  40,  -9,  11,  -5, -10,  -7,  -7,  -3,  44,
+  -5,  44,   9,  10,  11,  12,  13,   7,   8,   6,
+   5,   4,  -4,  -4,  -4,  -5,  44,  44,  44,  44,
+  44,  44,  44,  44,  44,  44,  44,  44,  44,  44,
+  44,  -5, -10,  -5,  46,  44,  -5, -11,  -5,  -5,
+  -5,  -5,  -5,  -5,   7,   8,  -5,  -5,  -5,  47,
+ -11, -11,  18,  41,  40,  10,  42,  -5,  -5,  -5,
+  -5,  -6,  -5,  -5,  -5,  -5,  -5,  -7,  -8,  45,
+ -10,  46, -10,  19,  20,  -7, -12,  44,  44,  -5,
+  -5,  18,  41,  40,  44,  44,  44,  44,  44,  -5,
+  47,  47,  47,  42,  -5,  42,  -5,  -5,  -5,  -7,
+  44,  44,  42,  -5,  44,  -5,  44,  -5,
+};
+yydef := array[] of {
+   2,  -2,   1,   3,   0,   0,   6,   7,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+  36,   0,   0,   0,   0,   0,   0,   0,   0,  18,
+   0,   0,   0,   9,   0,  59,  53,  54,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,  32,
+   0,   0,   0,   0,  38,   0,   0,   0,  42,   0,
+   0,  -2,  47,   0,   0,  50,   0,  17,   4,   0,
+   8,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,  55,  56,  57,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,  45,  48,   0,   0,   0,  10,  19,  12,  60,
+  61,  62,  63,  64,   0,   0,  67,  68,  69,  58,
+  20,  21,  22,  23,  24,   0,  28,  29,   0,   0,
+  33,   0,  35,  37,   0,  40,  41,  14,   0,   0,
+  -2,   0,   0,   0,   0,  16,   5,   0,   0,  65,
+  66,  25,  26,  27,   0,   0,   0,   0,   0,  -2,
+  49,  51,  52,  11,  13,  30,   0,  34,   0,  15,
+   0,   0,  31,   0,   0,   0,   0,  39,
+};
+yytok1 := array[] of {
+   1,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,  45,  13,   6,   3,
+  46,  47,  11,   9,  44,  10,   3,  12,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,  43,   3,
+   7,   3,   8,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   5,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   4,   3,  48,
+};
+yytok2 := array[] of {
+   2,   3,  14,  15,  16,  17,  18,  19,  20,  21,
+  22,  23,  24,  25,  26,  27,  28,  29,  30,  31,
+  32,  33,  34,  35,  36,  37,  38,  39,  40,  41,
+  42,
+};
+yytok3 := array[] of {
+   0
+};
+
+YYSys: module
+{
+	FD: adt
+	{
+		fd:	int;
+	};
+	fildes:		fn(fd: int): ref FD;
+	fprint:		fn(fd: ref FD, s: string, *): int;
+};
+
+yysys: YYSys;
+yystderr: ref YYSys->FD;
+
+YYFLAG: con -1000;
+
+# parser for yacc output
+
+yytokname(yyc: int): string
+{
+	if(yyc > 0 && yyc <= len yytoknames && yytoknames[yyc-1] != nil)
+		return yytoknames[yyc-1];
+	return "<"+string yyc+">";
+}
+
+yystatname(yys: int): string
+{
+	if(yys >= 0 && yys < len yystates && yystates[yys] != nil)
+		return yystates[yys];
+	return "<"+string yys+">\n";
+}
+
+yylex1(yylex: ref YYLEX): int
+{
+	c : int;
+	yychar := yylex.lex();
+	if(yychar <= 0)
+		c = yytok1[0];
+	else if(yychar < len yytok1)
+		c = yytok1[yychar];
+	else if(yychar >= YYPRIVATE && yychar < YYPRIVATE+len yytok2)
+		c = yytok2[yychar-YYPRIVATE];
+	else{
+		n := len yytok3;
+		c = 0;
+		for(i := 0; i < n; i+=2) {
+			if(yytok3[i+0] == yychar) {
+				c = yytok3[i+1];
+				break;
+			}
+		}
+		if(c == 0)
+			c = yytok2[1];	# unknown char
+	}
+	if(yydebug >= 3)
+		yysys->fprint(yystderr, "lex %.4ux %s\n", yychar, yytokname(c));
+	return c;
+}
+
+YYS: adt
+{
+	yyv: YYSTYPE;
+	yys: int;
+};
+
+yyparse(yylex: ref YYLEX): int
+{
+	if(yydebug >= 1 && yysys == nil) {
+		yysys = load YYSys "$Sys";
+		yystderr = yysys->fildes(2);
+	}
+
+	yys := array[YYMAXDEPTH] of YYS;
+
+	yyval: YYSTYPE;
+	yystate := 0;
+	yychar := -1;
+	yynerrs := 0;		# number of errors
+	yyerrflag := 0;		# error recovery flag
+	yyp := -1;
+	yyn := 0;
+
+yystack:
+	for(;;){
+		# put a state and value onto the stack
+		if(yydebug >= 4)
+			yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+
+		yyp++;
+		if(yyp >= len yys)
+			yys = (array[len yys * 2] of YYS)[0:] = yys;
+		yys[yyp].yys = yystate;
+		yys[yyp].yyv = yyval;
+
+		for(;;){
+			yyn = yypact[yystate];
+			if(yyn > YYFLAG) {	# simple state
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+				yyn += yychar;
+				if(yyn >= 0 && yyn < YYLAST) {
+					yyn = yyact[yyn];
+					if(yychk[yyn] == yychar) { # valid shift
+						yychar = -1;
+						yyp++;
+						if(yyp >= len yys)
+							yys = (array[len yys * 2] of YYS)[0:] = yys;
+						yystate = yyn;
+						yys[yyp].yys = yystate;
+						yys[yyp].yyv = yylex.lval;
+						if(yyerrflag > 0)
+							yyerrflag--;
+						if(yydebug >= 4)
+							yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+						continue;
+					}
+				}
+			}
+		
+			# default state action
+			yyn = yydef[yystate];
+			if(yyn == -2) {
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+		
+				# look through exception table
+				for(yyxi:=0;; yyxi+=2)
+					if(yyexca[yyxi] == -1 && yyexca[yyxi+1] == yystate)
+						break;
+				for(yyxi += 2;; yyxi += 2) {
+					yyn = yyexca[yyxi];
+					if(yyn < 0 || yyn == yychar)
+						break;
+				}
+				yyn = yyexca[yyxi+1];
+				if(yyn < 0){
+					yyn = 0;
+					break yystack;
+				}
+			}
+
+			if(yyn != 0)
+				break;
+
+			# error ... attempt to resume parsing
+			if(yyerrflag == 0) { # brand new error
+				yylex.error("syntax error");
+				yynerrs++;
+				if(yydebug >= 1) {
+					yysys->fprint(yystderr, "%s", yystatname(yystate));
+					yysys->fprint(yystderr, "saw %s\n", yytokname(yychar));
+				}
+			}
+
+			if(yyerrflag != 3) { # incompletely recovered error ... try again
+				yyerrflag = 3;
+	
+				# find a state where "error" is a legal shift action
+				while(yyp >= 0) {
+					yyn = yypact[yys[yyp].yys] + YYERRCODE;
+					if(yyn >= 0 && yyn < YYLAST) {
+						yystate = yyact[yyn];  # simulate a shift of "error"
+						if(yychk[yystate] == YYERRCODE)
+							continue yystack;
+					}
+	
+					# the current yyp has no shift onn "error", pop stack
+					if(yydebug >= 2)
+						yysys->fprint(yystderr, "error recovery pops state %d, uncovers %d\n",
+							yys[yyp].yys, yys[yyp-1].yys );
+					yyp--;
+				}
+				# there is no state on the stack with an error shift ... abort
+				yyn = 1;
+				break yystack;
+			}
+
+			# no shift yet; clobber input char
+			if(yydebug >= 2)
+				yysys->fprint(yystderr, "error recovery discards %s\n", yytokname(yychar));
+			if(yychar == YYEOFCODE) {
+				yyn = 1;
+				break yystack;
+			}
+			yychar = -1;
+			# try again in the same state
+		}
+	
+		# reduction by production yyn
+		if(yydebug >= 2)
+			yysys->fprint(yystderr, "reduce %d in:\n\t%s", yyn, yystatname(yystate));
+	
+		yypt := yyp;
+		yyp -= yyr2[yyn];
+#		yyval = yys[yyp+1].yyv;
+		yym := yyn;
+	
+		# consult goto table to find next state
+		yyn = yyr1[yyn];
+		yyg := yypgo[yyn];
+		yyj := yyg + yys[yyp].yys + 1;
+	
+		if(yyj >= YYLAST || yychk[yystate=yyact[yyj]] != -yyn)
+			yystate = yyact[yyg];
+		case yym {
+			
+1=>
+#line	178	"asm.y"
+{
+		assem(yys[yypt-0].yyv.inst);
+	}
+2=>
+#line	184	"asm.y"
+{ yyval.inst = nil; }
+3=>
+#line	186	"asm.y"
+{
+		if(yys[yypt-0].yyv.inst != nil) {
+			yys[yypt-0].yyv.inst.link = yys[yypt-1].yyv.inst;
+			yyval.inst = yys[yypt-0].yyv.inst;
+		}
+		else
+			yyval.inst = yys[yypt-1].yyv.inst;
+	}
+4=>
+#line	197	"asm.y"
+{
+		yys[yypt-0].yyv.inst.sym = yys[yypt-2].yyv.sym;
+		yyval.inst = yys[yypt-0].yyv.inst;
+	}
+5=>
+#line	202	"asm.y"
+{
+		heap(int yys[yypt-3].yyv.ival, int yys[yypt-1].yyv.ival, yys[yypt-0].yyv.str);
+		yyval.inst = nil;
+	}
+6=>
+#line	207	"asm.y"
+{
+		yyval.inst = nil;
+	}
+7=>
+yyval.inst = yys[yyp+1].yyv.inst;
+8=>
+#line	214	"asm.y"
+{
+		yyval.ival = yys[yypt-0].yyv.ival;
+	}
+9=>
+#line	218	"asm.y"
+{
+		yys[yypt-0].yyv.sym.value = heapid++;
+		yyval.ival = big yys[yypt-0].yyv.sym.value;
+	}
+10=>
+#line	225	"asm.y"
+{ yyval.str = nil; }
+11=>
+#line	227	"asm.y"
+{
+		yyval.str = yys[yypt-0].yyv.str;
+	}
+12=>
+#line	233	"asm.y"
+{
+		yyval.listv = newi(yys[yypt-0].yyv.ival, nil);
+	}
+13=>
+#line	237	"asm.y"
+{
+		yyval.listv = newi(yys[yypt-0].yyv.ival, yys[yypt-2].yyv.listv);
+	}
+14=>
+#line	243	"asm.y"
+{
+		yyval.inst = ai(yys[yypt-3].yyv.op);
+		yyval.inst.src = yys[yypt-2].yyv.addr;
+		yyval.inst.dst = yys[yypt-0].yyv.addr;
+	}
+15=>
+#line	249	"asm.y"
+{
+		yyval.inst = ai(yys[yypt-5].yyv.op);
+		yyval.inst.src = yys[yypt-4].yyv.addr;
+		yyval.inst.reg = yys[yypt-2].yyv.addr;
+		yyval.inst.dst = yys[yypt-0].yyv.addr;
+	}
+16=>
+#line	256	"asm.y"
+{
+		yyval.inst = ai(yys[yypt-3].yyv.op);
+		yyval.inst.src = yys[yypt-2].yyv.addr;
+		yyval.inst.dst = yys[yypt-0].yyv.addr;
+	}
+17=>
+#line	262	"asm.y"
+{
+		yyval.inst = ai(yys[yypt-1].yyv.op);
+		yyval.inst.dst = yys[yypt-0].yyv.addr;
+	}
+18=>
+#line	267	"asm.y"
+{
+		yyval.inst = ai(yys[yypt-0].yyv.op);
+	}
+19=>
+#line	273	"asm.y"
+{
+		data(DEFB, yys[yypt-2].yyv.ival, yys[yypt-0].yyv.listv);
+	}
+20=>
+#line	277	"asm.y"
+{
+		data(DEFW, yys[yypt-2].yyv.ival, yys[yypt-0].yyv.listv);
+	}
+21=>
+#line	281	"asm.y"
+{
+		data(DEFL, yys[yypt-2].yyv.ival, yys[yypt-0].yyv.listv);
+	}
+22=>
+#line	285	"asm.y"
+{
+		data(DEFF, yys[yypt-2].yyv.ival, newb(dtocanon(real yys[yypt-0].yyv.ival), nil));
+	}
+23=>
+#line	289	"asm.y"
+{
+		data(DEFF, yys[yypt-2].yyv.ival, newb(dtocanon(yys[yypt-0].yyv.fval), nil));
+	}
+24=>
+#line	293	"asm.y"
+{
+		case yys[yypt-0].yyv.sym.name {
+		"Inf" or "Infinity" =>
+			b := array[] of {byte 16r7F, byte 16rF0, byte 0, byte 0, byte 0, byte 0, byte 0, byte 0};
+			data(DEFF, yys[yypt-2].yyv.ival, newb(b, nil));
+		"NaN" =>
+			b := array[] of {byte 16r7F, byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF};
+			data(DEFF, yys[yypt-2].yyv.ival, newb(b, nil));
+		* =>
+			diag(sys->sprint("bad value for real: %s", yys[yypt-0].yyv.sym.name));
+		}
+	}
+25=>
+#line	306	"asm.y"
+{
+		data(DEFF, yys[yypt-3].yyv.ival, newb(dtocanon(-real yys[yypt-0].yyv.ival), nil));
+	}
+26=>
+#line	310	"asm.y"
+{
+		data(DEFF, yys[yypt-3].yyv.ival, newb(dtocanon(-yys[yypt-0].yyv.fval), nil));
+	}
+27=>
+#line	314	"asm.y"
+{
+		case yys[yypt-0].yyv.sym.name {
+		"Inf" or "Infinity" =>
+			b := array[] of {byte 16rFF, byte 16rF0, byte 0, byte 0, byte 0, byte 0, byte 0, byte 0};
+			data(DEFF, yys[yypt-3].yyv.ival, newb(b, nil));
+		* =>
+			diag(sys->sprint("bad value for real: %s", yys[yypt-0].yyv.sym.name));
+		}
+	}
+28=>
+#line	324	"asm.y"
+{
+		data(DEFS, yys[yypt-2].yyv.ival, news(yys[yypt-0].yyv.str, nil));
+	}
+29=>
+#line	328	"asm.y"
+{
+		if(yys[yypt-2].yyv.sym.ds != 0)
+			diag(sys->sprint("%s declared twice", yys[yypt-2].yyv.sym.name));
+		yys[yypt-2].yyv.sym.ds = int yys[yypt-0].yyv.ival;
+		yys[yypt-2].yyv.sym.value = dseg;
+		dseg += int yys[yypt-0].yyv.ival;
+	}
+30=>
+#line	336	"asm.y"
+{
+		ext(int yys[yypt-4].yyv.ival, int yys[yypt-2].yyv.ival, yys[yypt-0].yyv.str);
+	}
+31=>
+#line	340	"asm.y"
+{
+		mklink(int yys[yypt-6].yyv.ival, int yys[yypt-4].yyv.ival, int yys[yypt-2].yyv.ival, yys[yypt-0].yyv.str);
+	}
+32=>
+#line	344	"asm.y"
+{
+		if(amodule != nil)
+			diag(sys->sprint("this module already defined as %s", yys[yypt-0].yyv.sym.name));
+		else
+			amodule = yys[yypt-0].yyv.sym;
+	}
+33=>
+#line	351	"asm.y"
+{
+		if(pcentry >= 0)
+			diag(sys->sprint("this module already has entry point %d, %d" , pcentry, dentry));
+		pcentry = int yys[yypt-2].yyv.ival;
+		dentry = int yys[yypt-0].yyv.ival;
+	}
+34=>
+#line	358	"asm.y"
+{
+		data(DEFA, yys[yypt-4].yyv.ival, newa(int yys[yypt-2].yyv.ival, int yys[yypt-0].yyv.ival));
+	}
+35=>
+#line	362	"asm.y"
+{
+		data(DIND, yys[yypt-2].yyv.ival, newa(int yys[yypt-0].yyv.ival, 0));
+	}
+36=>
+#line	366	"asm.y"
+{
+		data(DAPOP, big 0, newa(0, 0));
+	}
+37=>
+#line	370	"asm.y"
+{
+		ldts(int yys[yypt-0].yyv.ival);
+	}
+38=>
+#line	374	"asm.y"
+{
+		excs(int yys[yypt-0].yyv.ival);
+	}
+39=>
+#line	378	"asm.y"
+{
+		exc(int yys[yypt-10].yyv.ival, int yys[yypt-8].yyv.ival, int yys[yypt-6].yyv.ival, int yys[yypt-4].yyv.ival, int yys[yypt-2].yyv.ival, int yys[yypt-0].yyv.ival);
+	}
+40=>
+#line	382	"asm.y"
+{
+		etab(yys[yypt-2].yyv.str, int yys[yypt-0].yyv.ival);
+	}
+41=>
+#line	386	"asm.y"
+{
+		etab(nil, int yys[yypt-0].yyv.ival);
+	}
+42=>
+#line	390	"asm.y"
+{
+		source(yys[yypt-0].yyv.str);
+	}
+43=>
+#line	396	"asm.y"
+{
+		yyval.addr = aa(yys[yypt-0].yyv.ival);
+		yyval.addr.mode = AXIMM;
+		if(yyval.addr.val > 16r7FFF || yyval.addr.val < -16r8000)
+			diag(sys->sprint("immediate %d too large for middle operand", yyval.addr.val));
+	}
+44=>
+#line	403	"asm.y"
+{
+		if(yys[yypt-0].yyv.addr.mode == AMP)
+			yys[yypt-0].yyv.addr.mode = AXINM;
+		else
+			yys[yypt-0].yyv.addr.mode = AXINF;
+		if(yys[yypt-0].yyv.addr.mode == AXINM && isoff2big(yys[yypt-0].yyv.addr.val))
+			diag(sys->sprint("register offset %d(mp) too large", yys[yypt-0].yyv.addr.val));
+		if(yys[yypt-0].yyv.addr.mode == AXINF && isoff2big(yys[yypt-0].yyv.addr.val))
+			diag(sys->sprint("register offset %d(fp) too large", yys[yypt-0].yyv.addr.val));
+		yyval.addr = yys[yypt-0].yyv.addr;
+	}
+45=>
+#line	417	"asm.y"
+{
+		yyval.addr = aa(yys[yypt-0].yyv.ival);
+		yyval.addr.mode = AIMM;
+	}
+46=>
+#line	422	"asm.y"
+{
+		yyval.addr = aa(big 0);
+		yyval.addr.sym = yys[yypt-0].yyv.sym;
+	}
+47=>
+yyval.addr = yys[yyp+1].yyv.addr;
+48=>
+#line	430	"asm.y"
+{
+		yys[yypt-0].yyv.addr.mode |= AIND;
+		yyval.addr = yys[yypt-0].yyv.addr;
+	}
+49=>
+#line	435	"asm.y"
+{
+		yys[yypt-1].yyv.addr.mode |= AIND;
+		if(yys[yypt-1].yyv.addr.val & 3)
+			diag("indirect offset must be word size");
+		if(yys[yypt-1].yyv.addr.mode == (AMP|AIND) && (isoff2big(yys[yypt-1].yyv.addr.val) || isoff2big(int yys[yypt-3].yyv.ival)))
+			diag(sys->sprint("indirect offset %bd(%d(mp)) too large", yys[yypt-3].yyv.ival, yys[yypt-1].yyv.addr.val));
+		if(yys[yypt-1].yyv.addr.mode == (AFP|AIND) && (isoff2big(yys[yypt-1].yyv.addr.val) || isoff2big(int yys[yypt-3].yyv.ival)))
+			diag(sys->sprint("indirect offset %bd(%d(fp)) too large", yys[yypt-3].yyv.ival, yys[yypt-1].yyv.addr.val));
+		yys[yypt-1].yyv.addr.off = yys[yypt-1].yyv.addr.val;
+		yys[yypt-1].yyv.addr.val = int yys[yypt-3].yyv.ival;
+		yyval.addr = yys[yypt-1].yyv.addr;
+	}
+50=>
+yyval.addr = yys[yyp+1].yyv.addr;
+51=>
+#line	451	"asm.y"
+{
+		yyval.addr = aa(yys[yypt-3].yyv.ival);
+		yyval.addr.mode = AMP;
+	}
+52=>
+#line	456	"asm.y"
+{
+		yyval.addr = aa(yys[yypt-3].yyv.ival);
+		yyval.addr.mode = AFP;
+	}
+53=>
+yyval.ival = yys[yyp+1].yyv.ival;
+54=>
+#line	464	"asm.y"
+{
+		yyval.ival = big yys[yypt-0].yyv.sym.value;
+	}
+55=>
+#line	468	"asm.y"
+{
+		yyval.ival = -yys[yypt-0].yyv.ival;
+	}
+56=>
+#line	472	"asm.y"
+{
+		yyval.ival = yys[yypt-0].yyv.ival;
+	}
+57=>
+#line	476	"asm.y"
+{
+		yyval.ival = ~yys[yypt-0].yyv.ival;
+	}
+58=>
+#line	480	"asm.y"
+{
+		yyval.ival = yys[yypt-1].yyv.ival;
+	}
+59=>
+yyval.ival = yys[yyp+1].yyv.ival;
+60=>
+#line	487	"asm.y"
+{
+		yyval.ival = yys[yypt-2].yyv.ival + yys[yypt-0].yyv.ival;
+	}
+61=>
+#line	491	"asm.y"
+{
+		yyval.ival = yys[yypt-2].yyv.ival - yys[yypt-0].yyv.ival;
+	}
+62=>
+#line	495	"asm.y"
+{
+		yyval.ival = yys[yypt-2].yyv.ival * yys[yypt-0].yyv.ival;
+	}
+63=>
+#line	499	"asm.y"
+{
+		yyval.ival = yys[yypt-2].yyv.ival / yys[yypt-0].yyv.ival;
+	}
+64=>
+#line	503	"asm.y"
+{
+		yyval.ival = yys[yypt-2].yyv.ival % yys[yypt-0].yyv.ival;
+	}
+65=>
+#line	507	"asm.y"
+{
+		yyval.ival = yys[yypt-3].yyv.ival << int yys[yypt-0].yyv.ival;
+	}
+66=>
+#line	511	"asm.y"
+{
+		yyval.ival = yys[yypt-3].yyv.ival >> int yys[yypt-0].yyv.ival;
+	}
+67=>
+#line	515	"asm.y"
+{
+		yyval.ival = yys[yypt-2].yyv.ival & yys[yypt-0].yyv.ival;
+	}
+68=>
+#line	519	"asm.y"
+{
+		yyval.ival = yys[yypt-2].yyv.ival ^ yys[yypt-0].yyv.ival;
+	}
+69=>
+#line	523	"asm.y"
+{
+		yyval.ival = yys[yypt-2].yyv.ival | yys[yypt-0].yyv.ival;
+	}
+		}
+	}
+
+	return yyn;
+}
--- /dev/null
+++ b/appl/cmd/asm/asm.y
@@ -1,0 +1,1907 @@
+%{
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "math.m";
+	math: Math;
+	export_real: import math;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+
+include "../limbo/isa.m";
+
+YYSTYPE: adt {
+	inst:	ref Inst;
+	addr:	ref Addr;
+	op:	int;
+	ival:	big;
+	fval:	real;
+	str:	string;
+	sym:	ref Sym;
+	listv:	ref List;
+};
+
+YYLEX: adt {
+	lval:	YYSTYPE;
+	EOF:	con -1;
+	lex:	fn(l: self ref YYLEX): int;
+	error:	fn(l: self ref YYLEX, msg: string);
+
+	numsym:	fn(l: self ref YYLEX, first: int): int;
+	eatstring:	fn(l: self ref YYLEX);
+};
+
+Eof: con -1;
+False: con 0;
+True: con 1;
+Strsize: con 1024;
+Hashsize: con 128;
+
+Addr: adt
+{
+	mode:	int;
+	off:	int;
+	val:	int;
+	sym:	ref Sym;
+
+	text:	fn(a: self ref Addr): string;
+};
+
+List: adt
+{
+	link:	cyclic ref List;
+	addr:	int;
+	typ:	int;
+	pick{
+	Int =>	ival: big;	# DEFB, DEFW, DEFL
+	Bytes =>	b: array of byte;	# DEFF, DEFS
+	Array =>	a: ref Array;	# DEFA
+	}
+};
+
+Inst: adt
+{
+	op:	int;
+	typ:	int;
+	size:	int;
+	reg:	ref Addr;
+	src:	ref Addr;
+	dst:	ref Addr;
+	pc:	int;
+	sym:	ref Sym;
+	link:	cyclic ref Inst;
+
+	text:	fn(i: self ref Inst): string;
+};
+
+Sym: adt
+{
+	name:	string;
+	lexval:	int;
+	value:	int;
+	ds:	int;
+};
+
+Desc: adt
+{
+	id:	int;
+	size:	int;
+	np:	int;
+	map:	array of byte;
+	link:	cyclic ref Desc;
+};
+
+Array: adt
+{
+	i:	int;
+	size:	int;
+};
+
+Link: adt
+{
+	desc:	int;
+	addr:	int;
+	typ:	int;
+	name:	string;
+	link:	cyclic ref Link;
+};
+
+Keywd: adt
+{
+	name:	string;
+	op:	int;
+	terminal:	int;
+};
+
+Ldts: adt
+{
+	n:	int;
+	ldt:	list of ref Ldt;
+};
+
+Ldt: adt
+{
+	sign:	int;
+	name:	string;
+};
+
+Exc: adt
+{
+	n1, n2, n3, n4, n5, n6: int;
+	etab: list of ref Etab;
+};
+
+Etab: adt
+{
+	n: int;
+	name:	string;
+};
+
+%}
+
+%module Asm {
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+}
+
+%left	'|'
+%left	'^'
+%left	'&'
+%left	'<' '>'
+%left	'+' '-'
+%left	'*' '/' '%'
+
+%type<inst>	label ilist inst
+%type<ival>	con expr heapid
+%type<addr>	addr raddr mem roff
+%type<listv>	elist
+%type<str>	ptrs
+%token<op>	TOKI0 TOKI1 TOKI2 TOKI3
+%token <ival>	TCONST
+%token		TOKSB TOKFP TOKHEAP TOKDB TOKDW TOKDL TOKDF TOKDS TOKVAR
+%token		TOKEXT TOKMOD TOKLINK TOKENTRY TOKARRAY TOKINDIR TOKAPOP TOKLDTS TOKEXCS TOKEXC TOKETAB TOKSRC
+%token<sym>	TID
+%token<fval>	TFCONST
+%token<str>	TSTRING
+
+%%
+prog	: ilist
+	{
+		assem($1);
+	}
+	;
+
+ilist	:
+	{ $$ = nil; }
+	| ilist label
+	{
+		if($2 != nil) {
+			$2.link = $1;
+			$$ = $2;
+		}
+		else
+			$$ = $1;
+	}
+	;
+
+label	: TID ':' inst
+	{
+		$3.sym = $1;
+		$$ = $3;
+	}
+	| TOKHEAP heapid ',' expr ptrs
+	{
+		heap(int $2, int $4, $5);
+		$$ = nil;
+	}
+	| data
+	{
+		$$ = nil;
+	}
+	| inst
+	;
+
+heapid	: '$' expr
+	{
+		$$ = $2;
+	}
+	| TID
+	{
+		$1.value = heapid++;
+		$$ = big $1.value;
+	}
+	;
+
+ptrs	:
+	{ $$ = nil; }
+	| ',' TSTRING
+	{
+		$$ = $2;
+	}
+	;
+
+elist	: expr
+	{
+		$$ = newi($1, nil);
+	}
+	| elist ',' expr
+	{
+		$$ = newi($3, $1);
+	}
+	;
+
+inst	: TOKI3 addr ',' addr
+	{
+		$$ = ai($1);
+		$$.src = $2;
+		$$.dst = $4;
+	}
+	| TOKI3 addr ',' raddr ',' addr
+	{
+		$$ = ai($1);
+		$$.src = $2;
+		$$.reg = $4;
+		$$.dst = $6;
+	}
+	| TOKI2 addr ',' addr
+	{
+		$$ = ai($1);
+		$$.src = $2;
+		$$.dst = $4;
+	}
+	| TOKI1 addr
+	{
+		$$ = ai($1);
+		$$.dst = $2;
+	}
+	| TOKI0
+	{
+		$$ = ai($1);
+	}
+	;
+
+data	: TOKDB expr ',' elist
+	{
+		data(DEFB, $2, $4);
+	}
+	| TOKDW expr ',' elist
+	{
+		data(DEFW, $2, $4);
+	}
+	| TOKDL expr ',' elist
+	{
+		data(DEFL, $2, $4);
+	}
+	| TOKDF expr ',' TCONST
+	{
+		data(DEFF, $2, newb(dtocanon(real $4), nil));
+	}
+	| TOKDF expr ',' TFCONST
+	{
+		data(DEFF, $2, newb(dtocanon($4), nil));
+	}
+	| TOKDF expr ',' TID
+	{
+		case $4.name {
+		"Inf" or "Infinity" =>
+			b := array[] of {byte 16r7F, byte 16rF0, byte 0, byte 0, byte 0, byte 0, byte 0, byte 0};
+			data(DEFF, $2, newb(b, nil));
+		"NaN" =>
+			b := array[] of {byte 16r7F, byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF};
+			data(DEFF, $2, newb(b, nil));
+		* =>
+			diag(sys->sprint("bad value for real: %s", $4.name));
+		}
+	}
+	| TOKDF expr ',' '-' TCONST
+	{
+		data(DEFF, $2, newb(dtocanon(-real $5), nil));
+	}
+	| TOKDF expr ',' '-' TFCONST
+	{
+		data(DEFF, $2, newb(dtocanon(-$5), nil));
+	}
+	| TOKDF expr ',' '-' TID
+	{
+		case $5.name {
+		"Inf" or "Infinity" =>
+			b := array[] of {byte 16rFF, byte 16rF0, byte 0, byte 0, byte 0, byte 0, byte 0, byte 0};
+			data(DEFF, $2, newb(b, nil));
+		* =>
+			diag(sys->sprint("bad value for real: %s", $5.name));
+		}
+	}
+	| TOKDS expr ',' TSTRING
+	{
+		data(DEFS, $2, news($4, nil));
+	}
+	| TOKVAR TID ',' expr
+	{
+		if($2.ds != 0)
+			diag(sys->sprint("%s declared twice", $2.name));
+		$2.ds = int $4;
+		$2.value = dseg;
+		dseg += int $4;
+	}
+	| TOKEXT expr ',' expr ',' TSTRING
+	{
+		ext(int $2, int $4, $6);
+	}
+	| TOKLINK expr ',' expr ',' expr ',' TSTRING
+	{
+		mklink(int $2, int $4, int $6, $8);
+	}
+	| TOKMOD TID
+	{
+		if(amodule != nil)
+			diag(sys->sprint("this module already defined as %s", $2.name));
+		else
+			amodule = $2;
+	}
+	| TOKENTRY expr ',' expr
+	{
+		if(pcentry >= 0)
+			diag(sys->sprint("this module already has entry point %d, %d" , pcentry, dentry));
+		pcentry = int $2;
+		dentry = int $4;
+	}
+	| TOKARRAY expr ',' heapid ',' expr
+	{
+		data(DEFA, $2, newa(int $4, int $6));
+	}
+	| TOKINDIR expr ',' expr
+	{
+		data(DIND, $2, newa(int $4, 0));
+	}
+	| TOKAPOP
+	{
+		data(DAPOP, big 0, newa(0, 0));
+	}
+	| TOKLDTS TID ',' expr
+	{
+		ldts(int $4);
+	}
+	| TOKEXCS expr
+	{
+		excs(int $2);
+	}
+	| TOKEXC expr ',' expr ',' expr ',' expr ',' expr ',' expr
+	{
+		exc(int $2, int $4, int $6, int $8, int $10, int $12);
+	}
+	| TOKETAB TSTRING ',' expr
+	{
+		etab($2, int $4);
+	}
+	| TOKETAB '*' ',' expr
+	{
+		etab(nil, int $4);
+	}
+	| TOKSRC TSTRING
+	{
+		source($2);
+	}
+	;
+
+raddr	: '$' expr
+	{
+		$$ = aa($2);
+		$$.mode = AXIMM;
+		if($$.val > 16r7FFF || $$.val < -16r8000)
+			diag(sys->sprint("immediate %d too large for middle operand", $$.val));
+	}
+	| roff
+	{
+		if($1.mode == AMP)
+			$1.mode = AXINM;
+		else
+			$1.mode = AXINF;
+		if($1.mode == AXINM && isoff2big($1.val))
+			diag(sys->sprint("register offset %d(mp) too large", $1.val));
+		if($1.mode == AXINF && isoff2big($1.val))
+			diag(sys->sprint("register offset %d(fp) too large", $1.val));
+		$$ = $1;
+	}
+	;
+
+addr	: '$' expr
+	{
+		$$ = aa($2);
+		$$.mode = AIMM;
+	}
+	| TID
+	{
+		$$ = aa(big 0);
+		$$.sym = $1;
+	}
+	| mem
+	;
+
+mem	: '*' roff
+	{
+		$2.mode |= AIND;
+		$$ = $2;
+	}
+	| expr '(' roff ')'
+	{
+		$3.mode |= AIND;
+		if($3.val & 3)
+			diag("indirect offset must be word size");
+		if($3.mode == (AMP|AIND) && (isoff2big($3.val) || isoff2big(int $1)))
+			diag(sys->sprint("indirect offset %bd(%d(mp)) too large", $1, $3.val));
+		if($3.mode == (AFP|AIND) && (isoff2big($3.val) || isoff2big(int $1)))
+			diag(sys->sprint("indirect offset %bd(%d(fp)) too large", $1, $3.val));
+		$3.off = $3.val;
+		$3.val = int $1;
+		$$ = $3;
+	}
+	| roff
+	;
+
+roff	: expr '(' TOKSB ')'
+	{
+		$$ = aa($1);
+		$$.mode = AMP;
+	}
+	| expr '(' TOKFP ')'
+	{
+		$$ = aa($1);
+		$$.mode = AFP;
+	}
+	;
+
+con	: TCONST
+	| TID
+	{
+		$$ = big $1.value;
+	}
+	| '-' con
+	{
+		$$ = -$2;
+	}
+	| '+' con
+	{
+		$$ = $2;
+	}
+	| '~' con
+	{
+		$$ = ~$2;
+	}
+	| '(' expr ')'
+	{
+		$$ = $2;
+	}
+	;
+
+expr:	con
+	| expr '+' expr
+	{
+		$$ = $1 + $3;
+	}
+	| expr '-' expr
+	{
+		$$ = $1 - $3;
+	}
+	| expr '*' expr
+	{
+		$$ = $1 * $3;
+	}
+	| expr '/' expr
+	{
+		$$ = $1 / $3;
+	}
+	| expr '%' expr
+	{
+		$$ = $1 % $3;
+	}
+	| expr '<' '<' expr
+	{
+		$$ = $1 << int $4;
+	}
+	| expr '>' '>' expr
+	{
+		$$ = $1 >> int $4;
+	}
+	| expr '&' expr
+	{
+		$$ = $1 & $3;
+	}
+	| expr '^' expr
+	{
+		$$ = $1 ^ $3;
+	}
+	| expr '|' expr
+	{
+		$$ = $1 | $3;
+	}
+	;
+%%
+
+kinit()
+{
+	for(i := 0; keywds[i].name != nil; i++) {
+		s := enter(keywds[i].name, keywds[i].terminal);
+		s.value = keywds[i].op;
+	}
+
+	enter("desc", TOKHEAP);
+	enter("mp", TOKSB);
+	enter("fp", TOKFP);
+
+	enter("byte", TOKDB);
+	enter("word", TOKDW);
+	enter("long", TOKDL);
+	enter("real", TOKDF);
+	enter("string", TOKDS);
+	enter("var", TOKVAR);
+	enter("ext", TOKEXT);
+	enter("module", TOKMOD);
+	enter("link", TOKLINK);
+	enter("entry", TOKENTRY);
+	enter("array", TOKARRAY);
+	enter("indir", TOKINDIR);
+	enter("apop", TOKAPOP);
+	enter("ldts", TOKLDTS);
+	enter("exceptions", TOKEXCS);
+	enter("exception", TOKEXC);
+	enter("exctab", TOKETAB);
+	enter("source", TOKSRC);
+
+	cmap['0'] = '\0'+1;
+	cmap['z'] = '\0'+1;
+	cmap['n'] = '\n'+1;
+	cmap['r'] = '\r'+1;
+	cmap['t'] = '\t'+1;
+	cmap['b'] = '\b'+1;
+	cmap['f'] = '\f'+1;
+	cmap['a'] = '\a'+1;
+	cmap['v'] = '\v'+1;
+	cmap['\\'] = '\\'+1;
+	cmap['"'] = '"'+1;
+}
+
+Bgetc(b: ref Iobuf): int
+{
+	return b.getb();
+}
+
+Bungetc(b: ref Iobuf)
+{
+	b.ungetb();
+}
+
+Bgetrune(b: ref Iobuf): int
+{
+	return b.getc();
+}
+
+Bputc(b: ref Iobuf, c: int)
+{
+	b.putb(byte c);
+}
+
+strchr(s: string, c: int): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return s[i:];
+	return nil;
+}
+
+escchar(c: int): int
+{
+	buf := array[32] of byte;
+	if(c >= '0' && c <= '9') {
+		n := 1;
+		buf[0] = byte c;
+		for(;;) {
+			c = Bgetc(bin);
+			if(c == Eof)
+				fatal(sys->sprint("%d: <eof> in escape sequence", line));
+			if(strchr("0123456789xX", c) == nil) {
+				Bungetc(bin);
+				break;
+			}
+			buf[n++] = byte c;
+		}
+		return int string buf[0:n];
+	}
+
+	n := cmap[c];
+	if(n == 0)
+		return c;
+	return n-1;
+}
+
+strbuf := array[Strsize] of byte;
+
+resizebuf()
+{
+	t := array[len strbuf+Strsize] of byte;
+	t[0:] = strbuf;
+	strbuf = t;
+}
+
+YYLEX.eatstring(l: self ref YYLEX)
+{
+	esc := 0;
+Scan:
+	for(cnt := 0;;) {
+		c := Bgetc(bin);
+		case c {
+		Eof =>
+			fatal(sys->sprint("%d: <eof> in string constant", line));
+
+		'\n' =>
+			line++;
+			diag("newline in string constant");
+			break Scan;
+
+		'\\' =>
+			if(esc) {
+				if(cnt >= len strbuf)
+					resizebuf();
+				strbuf[cnt++] = byte c;
+				esc = 0;
+				break;
+			}
+			esc = 1;
+
+		'"' =>
+			if(esc == 0)
+				break Scan;
+			c = escchar(c);
+			esc = 0;
+			if(cnt >= len strbuf)
+				resizebuf();
+			strbuf[cnt++] = byte c;
+
+		* =>
+			if(esc) {
+				c = escchar(c);
+				esc = 0;
+			}
+			if(cnt >= len strbuf)
+				resizebuf();
+			strbuf[cnt++] = byte c;
+		}
+	}
+	l.lval.str = string strbuf[0: cnt];
+}
+
+eatnl()
+{
+	line++;
+	for(;;) {
+		c := Bgetc(bin);
+		if(c == Eof)
+			diag("eof in comment");
+		if(c == '\n')
+			return;
+	}
+}
+
+YYLEX.lex(l: self ref YYLEX): int
+{
+	for(;;){
+		c := Bgetc(bin);
+		case c {
+		Eof =>
+			return Eof;
+		'"' =>
+			l.eatstring();
+			return TSTRING;
+		' ' or
+		'\t' or
+		'\r' =>
+			continue;
+		'\n' =>
+			line++;
+		'.' =>
+			c = Bgetc(bin);
+			Bungetc(bin);
+			if(isdigit(c))
+				return l.numsym('.');
+			return '.';
+		'#' =>
+			eatnl();
+		'(' or
+		')' or
+		';' or
+		',' or
+		'~' or
+		'$' or
+		'+' or
+		'/' or
+		'%' or
+		'^' or
+		'*' or
+		'&' or
+		'=' or
+		'|' or
+		'<' or
+		'>' or
+		'-' or
+		':' =>
+			return c;
+		'\'' =>
+			c = Bgetrune(bin);
+			if(c == '\\')
+				l.lval.ival = big escchar(Bgetc(bin));
+			else
+				l.lval.ival = big c;
+			c = Bgetc(bin);
+			if(c != '\'') {
+				diag("missing '");
+				Bungetc(bin);
+			}
+			return TCONST;
+
+		* =>
+			return l.numsym(c);
+		}
+	}
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isxdigit(c: int): int
+{
+	return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F';
+}
+
+isalnum(c: int): int
+{
+	return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || isdigit(c);
+}
+
+YYLEX.numsym(l: self ref YYLEX, first: int): int
+{
+	Int, Hex, Frac, Expsign, Exp: con iota;
+	state: int;
+
+	symbol[0] = byte first;
+	p := 0;
+
+	if(first == '.')
+		state = Frac;
+	else
+		state = Int;
+
+	c: int;
+	if(isdigit(int symbol[p++]) || state == Frac) {
+	Collect:
+		for(;;) {
+			c = Bgetc(bin);
+			if(c < 0)
+				fatal(sys->sprint("%d: <eof> eating numeric", line));
+
+			case state {
+			Int =>
+				if(isdigit(c))
+					break;
+				case c {
+				'x' or
+				'X' =>
+					c = 'x';
+					state = Hex;
+				'.' =>
+					state = Frac;
+				'e' or
+				'E' =>
+					c = 'e';
+					state = Expsign;
+				* =>
+					break Collect;
+				}
+			Hex =>
+				if(!isxdigit(c))
+					break Collect;
+			Frac =>
+				if(isdigit(c))
+					break;
+				if(c != 'e' && c != 'E')
+					break Collect;
+				c = 'e';
+				state = Expsign;
+			Expsign =>
+				state = Exp;
+				if(c == '-' || c == '+')
+					break;
+				if(!isdigit(c))
+					break Collect;
+			Exp =>
+				if(!isdigit(c))
+					break Collect;
+			}
+			symbol[p++] = byte c;
+		}
+
+		# break Collect
+		lastsym = string symbol[0:p];
+		Bungetc(bin);
+		case state {
+		Frac or
+		Expsign or
+		Exp =>
+			l.lval.fval = real lastsym;
+			return TFCONST;
+		* =>
+			if(len lastsym >= 3 && lastsym[0:2] == "0x")
+				(l.lval.ival, nil) = str->tobig(lastsym[2:], 16);
+			else
+				(l.lval.ival, nil) = str->tobig(lastsym, 10);
+			return TCONST;
+		}
+	}
+
+	for(;;) {
+		c = Bgetc(bin);
+		if(c < 0)
+			fatal(sys->sprint("%d <eof> eating symbols", line));
+		# '$' and '/' can occur in fully-qualified Java class names
+		if(c != '_' && c != '.' && c != '/' && c != '$' && !isalnum(c)) {
+			Bungetc(bin);
+			break;
+		}
+		symbol[p++] = byte c;
+	}
+
+	lastsym = string symbol[0:p];
+	s := enter(lastsym,TID);
+	case s.lexval {
+	TOKI0 or
+	TOKI1 or
+	TOKI2 or
+	TOKI3 =>
+		l.lval.op = s.value;
+	* =>
+		l.lval.sym = s;
+	}
+	return s.lexval;
+}
+
+hash := array[Hashsize] of list of ref Sym;
+
+enter(name: string, stype: int): ref Sym
+{
+	s := lookup(name);
+	if(s != nil)
+		return s;
+
+	h := 0;
+	for(p := 0; p < len name; p++)
+		h = h*3 + name[p];
+	if(h < 0)
+		h = ~h;
+	h %= Hashsize;
+
+	s = ref Sym(name, stype, 0, 0);
+	hash[h] = s :: hash[h];
+	return s;
+}
+
+lookup(name: string): ref Sym
+{
+	h := 0;
+	for(p := 0; p < len name; p++)
+		h = h*3 + name[p];
+	if(h < 0)
+		h = ~h;
+	h %= Hashsize;
+
+	for(l := hash[h]; l != nil; l = tl l)
+		if((s := hd l).name == name)
+			return s;
+	return nil;
+}
+
+YYLEX.error(l: self ref YYLEX, s: string)
+{
+	if(s == "syntax error") {
+		l.error(sys->sprint("syntax error, near symbol '%s'", lastsym));
+		return;
+	}
+	sys->print("%s %d: %s\n", file, line, s);
+	if(nerr++ > 10) {
+		sys->fprint(sys->fildes(2), "%s:%d: too many errors, giving up\n", file, line);
+		sys->remove(ofile);
+		raise "fail: yyerror";
+	}
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "asm: %d (fatal compiler problem) %s\n", line, s);
+	raise "fail:"+s;
+}
+
+diag(s: string)
+{
+	srcline := line;
+	sys->fprint(sys->fildes(2), "%s:%d: %s\n", file, srcline, s);
+	if(nerr++ > 10) {
+		sys->fprint(sys->fildes(2), "%s:%d: too many errors, giving up\n", file, line);
+		sys->remove(ofile);
+		raise "fail: error";
+	}
+}
+
+zinst: Inst;
+
+ai(op: int): ref Inst
+{
+	i := ref zinst;
+	i.op = op;
+
+	return i;
+}
+
+aa(val: big): ref Addr
+{
+	if(val <= big -1073741824 && val > big 1073741823)
+		diag("offset out of range");
+	return ref Addr(0, 0, int val, nil);
+}
+
+isoff2big(o: int): int
+{
+	return o < 0 || o > 16rFFFF;
+}
+
+inldt := 0;
+nldts := 0;
+aldts: list of ref Ldts;
+curl: ref Ldts;
+nexcs := 0;
+aexcs: list of ref Exc;
+cure: ref Exc;
+srcpath: string;
+
+bin: ref Iobuf;
+bout: ref Iobuf;
+
+line := 0;
+heapid := 0;
+symbol := array[1024] of byte;
+lastsym: string;
+nerr := 0;
+cmap := array[256] of int;
+file: string;
+
+dlist: ref Desc;
+dcout := 0;
+dseg := 0;
+dcount := 0;
+
+mdata: ref List;
+amodule: ref Sym;
+links: ref Link;
+linkt: ref Link;
+nlink := 0;
+listing := 0;
+mustcompile := 0;
+dontcompile := 0;
+ofile: string;
+dentry := 0;
+pcentry := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	math = load Math Math->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->setusage("asm [-l] file.s");
+	arg->init(args);
+	while((c := arg->opt()) != 0){
+		case c {
+		'C' =>	dontcompile++;
+		'c' =>	mustcompile++;
+		'l' =>		listing++;
+		* =>		arg->usage();
+		}
+	}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+	arg = nil;
+
+	kinit();
+	pcentry = -1;
+	dentry = -1;
+
+	file = hd args;
+	bin = bufio->open(file, Bufio->OREAD);
+	if(bin == nil) {
+		sys->fprint(sys->fildes(2), "asm: can't open %s: %r\n", file);
+		raise "fail: errors";
+	}
+	p := strrchr(file, '/');
+	if(p == nil)
+		p = file;
+	else
+		p = p[1:];
+	ofile = mkfile(p, ".s", ".dis");
+	bout = bufio->create(ofile, Bufio->OWRITE, 8r666);
+	if(bout == nil){
+		sys->fprint(sys->fildes(2), "asm: can't create: %s: %r\n", ofile);
+		raise "fail: errors";
+	}
+	line = 1;
+	yyparse(ref YYLEX);
+	bout.close();
+
+	if(nerr != 0){
+		sys->remove(ofile);
+		raise "fail: errors";
+	}
+}
+
+strrchr(s: string, c: int): string
+{
+	for(i := len s; --i >= 0;)
+		if(s[i] == c)
+			return s[i:];
+	return nil;
+}
+
+mkfile(file: string, oldext: string, ext: string): string
+{
+	n := len file;
+	n2 := len oldext;
+	if(n >= n2 && file[n-n2:] == oldext)
+		n -= n2;
+	return file[0:n] + ext;
+}
+
+opcode(i: ref Inst): int
+{
+	if(i.op < 0 || i.op >= len keywds)
+		fatal(sys->sprint("internal error: invalid op %d (%#x)", i.op, i.op));
+	return keywds[i.op].op;
+}
+
+Inst.text(i: self ref Inst): string
+{
+	if(i == nil)
+		return "IZ";
+
+	case keywds[i.op].terminal {
+	TOKI0 =>
+		return sys->sprint("%s", keywds[i.op].name);
+	TOKI1 =>
+		return sys->sprint("%s\t%s", keywds[i.op].name, i.dst.text());
+	TOKI3 =>
+		if(i.reg != nil) {
+			pre := "";
+			post := "";
+			case i.reg.mode {
+			AXIMM =>
+				pre = "$";
+				break;
+			AXINF =>
+				post = "(fp)";
+				break;
+			AXINM =>
+				post = "(mp)";
+			 	break;
+			}
+			return sys->sprint("%s\t%s, %s%d%s, %s", keywds[i.op].name, i.src.text(), pre, i.reg.val, post, i.dst.text());
+		}
+		return sys->sprint("%s\t%s, %s", keywds[i.op].name, i.src.text(), i.dst.text());
+	TOKI2 =>
+		return sys->sprint("%s\t%s, %s", keywds[i.op].name, i.src.text(), i.dst.text());
+	* =>
+		return "IGOK";
+	}
+}
+
+Addr.text(a: self ref Addr): string
+{
+	if(a == nil)
+		return "AZ";
+
+	if(a.mode & AIND) {		
+		case a.mode & ~AIND {
+		AFP =>
+			return sys->sprint("%d(%d(fp))", a.val, a.off);
+		AMP =>
+			return sys->sprint("%d(%d(mp))", a.val, a.off);
+		}
+	}
+	else {
+		case a.mode {
+		AFP =>
+			return sys->sprint("%d(fp)", a.val);
+		AMP =>
+			return sys->sprint("%d(mp)", a.val);
+		AIMM =>
+			return sys->sprint("$%d", a.val);
+		}
+	}
+
+	return "AGOK";
+}
+
+append[T](l: list of T, v: T): list of T
+{
+	if(l == nil)
+		return v :: nil;
+	return hd l :: append(tl l, v);
+}
+
+newa(i: int, size: int): ref List
+{
+	a := ref Array(i, size);
+	l := ref List.Array(nil, -1, 0, a);
+	return l;
+}
+
+# does order matter?
+newi(v: big, l: ref List): ref List
+{
+	n := ref List.Int(nil, -1, 0, v);
+	if(l == nil)
+		return n;
+
+	for(t := l; t.link != nil; t = t.link)
+		;
+	t.link = n;
+
+	return l;
+}
+
+news(s: string, l: ref List): ref List
+{
+	return ref List.Bytes(l, -1, 0, array of byte s);
+}
+
+newb(a: array of byte, l: ref List): ref List
+{
+	return ref List.Bytes(l, -1, 0, a);
+}
+
+digit(x: int): int
+{
+	if(x >= 'A' && x <= 'F')
+		return x - 'A' + 10;
+	if(x >= 'a' && x <= 'f')
+		return x - 'a' + 10;
+	if(x >= '0' && x <= '9')
+		return x - '0';
+	diag("bad hex value in pointers");
+	return 0;
+}
+
+heap(id: int, size: int, ptr: string)
+{
+	d := ref Desc;
+	d.id = id;
+	d.size = size;
+	size /= IBY2WD;
+	d.map = array[size] of {* => byte 0};
+	d.np = 0;
+	if(dlist == nil)
+		dlist = d;
+	else {
+		f: ref Desc;
+		for(f = dlist; f.link != nil; f = f.link)
+			;
+		f.link = d;
+	}
+	d.link = nil;
+	dcount++;
+
+	if(ptr == nil)
+		return;
+	if(len ptr & 1) {
+		diag("pointer descriptor has odd length");
+		return;	
+	}
+
+	k := 0;
+	l := len ptr;
+	for(i := 0; i < l; i += 2) {
+		d.map[k++] = byte ((digit(ptr[i])<<4)|digit(ptr[i+1]));
+		if(k > size) {
+			diag("pointer descriptor too long");
+			break;
+		}
+	}
+	d.np = k;
+}
+
+conout(val: int)
+{
+	if(val >= -64 && val <= 63) {
+		Bputc(bout, val & ~16r80);
+		return;
+	}
+	if(val >= -8192 && val <= 8191) {
+		Bputc(bout, ((val>>8) & ~16rC0) | 16r80);
+		Bputc(bout, val);
+		return;
+	}
+	if(val < 0 && ((val >> 29) & 7) != 7
+	|| val > 0 && (val >> 29) != 0)
+		diag(sys->sprint("overflow in constant 0x%ux\n", val));
+	Bputc(bout, (val>>24) | 16rC0);
+	Bputc(bout, val>>16);
+	Bputc(bout, val>>8);
+	Bputc(bout, val);
+}
+
+aout(a: ref Addr)
+{
+	if(a == nil)
+		return;
+	if(a.mode & AIND)
+		conout(a.off);
+	conout(a.val);
+}
+
+Bputs(b: ref Iobuf, s: string)
+{
+	for(i := 0; i < len s; i++)
+		Bputc(b, s[i]);
+	Bputc(b, '\0');
+}
+
+lout()
+{
+	if(amodule == nil)
+		amodule = enter("main", 0);
+
+	Bputs(bout, amodule.name);
+
+	for(l := links; l != nil; l = l.link) {
+		conout(l.addr);
+		conout(l.desc);
+		Bputc(bout, l.typ>>24);
+		Bputc(bout, l.typ>>16);
+		Bputc(bout, l.typ>>8);
+		Bputc(bout, l.typ);
+		Bputs(bout, l.name);
+	}
+}
+
+ldtout()
+{
+	conout(nldts);
+	for(la := aldts; la != nil; la = tl la){
+		ls := hd la;
+		conout(ls.n);
+		for(l := ls.ldt; l != nil; l = tl l){
+			t := hd l;
+			Bputc(bout, t.sign>>24);
+			Bputc(bout, t.sign>>16);
+			Bputc(bout, t.sign>>8);
+			Bputc(bout, t.sign);
+			Bputs(bout, t.name);
+		}
+	}
+	conout(0);
+}
+
+excout()
+{
+	if(nexcs == 0)
+		return;
+	conout(nexcs);
+	for(es := aexcs; es != nil; es = tl es){
+		e := hd es;
+		conout(e.n3);
+		conout(e.n1);
+		conout(e.n2);
+		conout(e.n4);
+		conout(e.n5|(e.n6<<16));
+		for(ets := e.etab; ets != nil; ets = tl ets){
+			et := hd ets;
+			if(et.name != nil)
+				Bputs(bout, et.name);
+			conout(et.n);
+		}
+	}
+	conout(0);
+}
+
+srcout()
+{
+	if(srcpath == nil)
+		return;
+	Bputs(bout, srcpath);
+}
+
+assem(i: ref Inst)
+{
+	f: ref Inst;
+	while(i != nil){
+		link := i.link;
+		i.link = f;
+		f = i;
+		i = link;
+	}
+	i = f;
+
+	pc := 0;
+	for(f = i; f != nil; f = f.link) {
+		f.pc = pc++;
+		if(f.sym != nil)
+			f.sym.value = f.pc;
+	}
+
+	if(pcentry >= pc)
+		diag("entry pc out of range");
+	if(dentry >= dcount)
+		diag("entry descriptor out of range");
+
+	conout(XMAGIC);
+	hints := 0;
+	if(mustcompile)
+		hints |= MUSTCOMPILE;
+	if(dontcompile)
+		hints |= DONTCOMPILE;
+	hints |= HASLDT;
+	if(nexcs > 0)
+		hints |= HASEXCEPT;
+	conout(hints);		# Runtime flags
+	conout(1024);		# default stack size
+	conout(pc);
+	conout(dseg);
+	conout(dcount);
+	conout(nlink);
+	conout(pcentry);
+	conout(dentry);
+
+	for(f = i; f != nil; f = f.link) {
+		if(f.dst != nil && f.dst.sym != nil) {
+			f.dst.mode = AIMM;
+			f.dst.val = f.dst.sym.value;
+		}
+		o := opcode(f);
+		if(o == IRAISE){
+			f.src = f.dst;
+			f.dst = nil;
+		}
+		Bputc(bout, o);
+		n := 0;
+		if(f.src != nil)
+			n |= src(f.src.mode);
+		else
+			n |= src(AXXX);
+		if(f.dst != nil)
+			n |= dst(f.dst.mode);
+		else
+			n |= dst(AXXX);
+		if(f.reg != nil)
+			n |= f.reg.mode;
+		else
+			n |= AXNON;
+		Bputc(bout, n);
+		aout(f.reg);
+		aout(f.src);
+		aout(f.dst);
+
+		if(listing)
+			sys->print("%4d %s\n", f.pc, f.text());
+	}
+
+	for(d := dlist; d != nil; d = d.link) {
+		conout(d.id);
+		conout(d.size);
+		conout(d.np);
+		for(n := 0; n < d.np; n++)
+			Bputc(bout, int d.map[n]);
+	}
+
+	dout();
+	lout();
+	ldtout();
+	excout();
+	srcout();
+}
+
+data(typ: int, addr: big, l: ref List)
+{
+	if(inldt){
+		ldtw(int intof(l));
+		return;
+	}
+
+	l.typ = typ;
+	l.addr = int addr;
+
+	if(mdata == nil)
+		mdata = l;
+	else {
+		for(f := mdata; f.link != nil; f = f.link)
+			;
+		f.link = l;
+	}
+}
+
+ext(addr: int, typ: int, s: string)
+{
+	if(inldt){
+		ldte(typ, s);
+		return;
+	}
+
+	data(DEFW, big addr, newi(big typ, nil));
+
+	n: ref List;
+	for(i := 0; i < len s; i++)
+		n = newi(big s[i], n);
+	data(DEFB, big(addr+IBY2WD), n);
+
+	if(addr+len s > dseg)
+		diag("ext beyond mp");
+}
+
+mklink(desc: int, addr: int, typ: int, s: string)
+{
+	for(ls := links; ls != nil; ls = ls.link)
+		if(ls.name == s)
+			diag(sys->sprint("%s already defined", s));
+
+	nlink++;
+	l := ref Link;
+	l.desc = desc;
+	l.addr = addr;
+	l.typ = typ;
+	l.name = s;
+	l.link = nil;
+
+	if(links == nil)
+		links = l;
+	else
+		linkt.link = l;
+	linkt = l;
+}
+
+intof(l: ref List): big
+{
+	pick rl := l {
+	Int =>
+		return rl.ival;
+	* =>
+		raise "list botch";
+	}
+}
+
+arrayof(l: ref List): ref Array
+{
+	pick rl := l {
+	Array =>
+		return rl.a;
+	* =>
+		raise "list botch";
+	}
+}
+
+bytesof(l: ref List): array of byte
+{
+	pick rl := l {
+	Bytes =>
+		return rl.b;
+	* =>
+		raise "list botch";
+	}
+}
+
+nel(l: ref List): (int, ref List)
+{
+	n := 1;
+	for(e := l.link; e != nil && e.addr == -1; e = e.link)
+		n++;
+	return (n, e);
+}
+
+dout()
+{
+	e: ref List;
+	n: int;
+	for(l := mdata; l != nil; l = e) {
+		case l.typ {
+		DEFB =>
+			(n, e) = nel(l);
+			if(n < DMAX)
+				Bputc(bout, dbyte(DEFB, n));
+			else {
+				Bputc(bout, dbyte(DEFB, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			while(l != e) {
+				Bputc(bout, int intof(l));
+				l = l.link;
+			}
+			break;
+		DEFW =>
+			(n, e) = nel(l);
+			if(n < DMAX)
+				Bputc(bout, dbyte(DEFW, n));
+			else {
+				Bputc(bout, dbyte(DEFW, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			while(l != e) {
+				n = int intof(l);
+				Bputc(bout, n>>24);
+				Bputc(bout, n>>16);
+				Bputc(bout, n>>8);
+				Bputc(bout, n);
+				l = l.link;
+			}
+			break;
+		DEFL =>
+			(n, e) = nel(l);
+			if(n < DMAX)
+				Bputc(bout, dbyte(DEFL, n));
+			else {
+				Bputc(bout, dbyte(DEFL, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			while(l != e) {
+				b := intof(l);
+				Bputc(bout, int (b>>56));
+				Bputc(bout, int (b>>48));
+				Bputc(bout, int (b>>40));
+				Bputc(bout, int (b>>32));
+				Bputc(bout, int (b>>24));
+				Bputc(bout, int (b>>16));
+				Bputc(bout, int (b>>8));
+				Bputc(bout, int b);
+				l = l.link;
+			}
+			break;
+		DEFF =>
+			(n, e) = nel(l);
+			if(n < DMAX)
+				Bputc(bout, dbyte(DEFF, n));
+			else {
+				Bputc(bout, dbyte(DEFF, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			while(l != e) {
+				b := bytesof(l);
+				Bputc(bout, int b[0]);
+				Bputc(bout, int b[1]);
+				Bputc(bout, int b[2]);
+				Bputc(bout, int b[3]);
+				Bputc(bout, int b[4]);
+				Bputc(bout, int b[5]);
+				Bputc(bout, int b[6]);
+				Bputc(bout, int b[7]);
+				l = l.link;
+			}
+			break;
+		DEFS =>
+			a := bytesof(l);
+			n = len a;
+			if(n < DMAX && n != 0)
+				Bputc(bout, dbyte(DEFS, n));
+			else {
+				Bputc(bout, dbyte(DEFS, 0));
+				conout(n);
+			}
+			conout(l.addr);
+			for(i := 0; i < n; i++)
+				Bputc(bout, int a[i]);
+
+			e = l.link;
+			break;
+		DEFA =>
+			Bputc(bout, dbyte(DEFA, 1));
+			conout(l.addr);
+			ar := arrayof(l);
+			Bputc(bout, ar.i>>24);
+			Bputc(bout, ar.i>>16);
+			Bputc(bout, ar.i>>8);
+			Bputc(bout, ar.i);
+			Bputc(bout, ar.size>>24);
+			Bputc(bout, ar.size>>16);
+			Bputc(bout, ar.size>>8);
+			Bputc(bout, ar.size);
+			e = l.link;
+			break;
+		DIND =>
+			Bputc(bout, dbyte(DIND, 1));
+			conout(l.addr);
+			Bputc(bout, 0);
+			Bputc(bout, 0);
+			Bputc(bout, 0);
+			Bputc(bout, 0);
+			e = l.link;
+			break;
+		DAPOP =>
+			Bputc(bout, dbyte(DAPOP, 1));
+			conout(0);
+			e = l.link;
+			break;
+		}
+	}
+
+	Bputc(bout, dbyte(DEFZ, 0));
+}
+
+ldts(n: int)
+{
+	nldts = n;
+	inldt = 1;
+}
+
+ldtw(n: int)
+{
+	ls := ref Ldts(n, nil);
+	aldts = append(aldts, ls);
+	curl = ls;
+}
+
+ldte(n: int, s: string)
+{
+	l := ref Ldt(n, s);
+	curl.ldt = append(curl.ldt, l);
+}
+
+excs(n: int)
+{
+	nexcs = n;
+}
+
+exc(n1: int, n2: int, n3: int, n4: int, n5: int, n6: int)
+{
+	e := ref Exc;
+	e.n1 = n1;
+	e.n2 = n2;
+	e.n3 = n3;
+	e.n4 = n4;
+	e.n5 = n5;
+	e.n6 = n6;
+	e.etab = nil;
+	aexcs = append(aexcs, e);
+	cure = e;
+}
+
+etab(s: string, n: int)
+{
+	et := ref Etab;
+	et.n = n;
+	et.name = s;
+	cure.etab = append(cure.etab, et);
+}
+
+source(s: string)
+{
+	srcpath = s;
+}
+
+dtype(x: int): int
+{
+	return (x>>4)&16rF;
+}
+
+dbyte(x: int, l: int): int
+{
+	return (x<<4) | l;
+}
+
+dlen(x: int): int
+{
+	return x & (DMAX-1);
+}
+
+src(x: int): int
+{
+	return x<<3;
+}
+
+dst(x: int): int
+{
+	return x<<0;
+}
+
+dtocanon(d: real): array of byte
+{
+	b := array[8] of byte;
+	export_real(b, array[] of {d});
+	return b;
+}
+
+keywds: array of Keywd = array[] of
+{
+	("nop",		INOP,		TOKI0),
+	("alt",		IALT,		TOKI3),
+	("nbalt",	INBALT,		TOKI3),
+	("goto",		IGOTO,		TOKI2),
+	("call",		ICALL,		TOKI2),
+	("frame",	IFRAME,		TOKI2),
+	("spawn",	ISPAWN,		TOKI2),
+	("runt",		IRUNT,		TOKI2),
+	("load",		ILOAD,		TOKI3),
+	("mcall",	IMCALL,		TOKI3),
+	("mspawn",	IMSPAWN,	TOKI3),
+	("mframe",	IMFRAME,	TOKI3),
+	("ret",		IRET,		TOKI0),
+	("jmp",		IJMP,		TOKI1),
+	("case",		ICASE,		TOKI2),
+	("exit",		IEXIT,		TOKI0),
+	("new",		INEW,		TOKI2),
+	("newa",		INEWA,		TOKI3),
+	("newcb",	INEWCB,		TOKI1),
+	("newcw",	INEWCW,		TOKI1),
+	("newcf",	INEWCF,		TOKI1),
+	("newcp",	INEWCP,		TOKI1),
+	("newcm",	INEWCM,		TOKI2),
+	("newcmp",	INEWCMP,	TOKI2),
+	("send",		ISEND,		TOKI2),
+	("recv",		IRECV,		TOKI2),
+	("consb",	ICONSB,		TOKI2),
+	("consw",	ICONSW,		TOKI2),
+	("consp",	ICONSP,		TOKI2),
+	("consf",	ICONSF,		TOKI2),
+	("consm",	ICONSM,		TOKI3),
+	("consmp",	ICONSMP,	TOKI3),
+	("headb",	IHEADB,		TOKI2),
+	("headw",	IHEADW,		TOKI2),
+	("headp",	IHEADP,		TOKI2),
+	("headf",	IHEADF,		TOKI2),
+	("headm",	IHEADM,		TOKI3),
+	("headmp",	IHEADMP,	TOKI3),
+	("tail",		ITAIL,		TOKI2),
+	("lea",		ILEA,		TOKI2),
+	("indx",		IINDX,		TOKI3),
+	("movp",		IMOVP,		TOKI2),
+	("movm",		IMOVM,		TOKI3),
+	("movmp",	IMOVMP,		TOKI3),
+	("movb",		IMOVB,		TOKI2),
+	("movw",		IMOVW,		TOKI2),
+	("movf",		IMOVF,		TOKI2),
+	("cvtbw",	ICVTBW,		TOKI2),
+	("cvtwb",	ICVTWB,		TOKI2),
+	("cvtfw",	ICVTFW,		TOKI2),
+	("cvtwf",	ICVTWF,		TOKI2),
+	("cvtca",	ICVTCA,		TOKI2),
+	("cvtac",	ICVTAC,		TOKI2),
+	("cvtwc",	ICVTWC,		TOKI2),
+	("cvtcw",	ICVTCW,		TOKI2),
+	("cvtfc",	ICVTFC,		TOKI2),
+	("cvtcf",	ICVTCF,		TOKI2),
+	("addb",		IADDB,		TOKI3),
+	("addw",		IADDW,		TOKI3),
+	("addf",		IADDF,		TOKI3),
+	("subb",		ISUBB,		TOKI3),
+	("subw",		ISUBW,		TOKI3),
+	("subf",		ISUBF,		TOKI3),
+	("mulb",		IMULB,		TOKI3),
+	("mulw",		IMULW,		TOKI3),
+	("mulf",		IMULF,		TOKI3),
+	("divb",		IDIVB,		TOKI3),
+	("divw",		IDIVW,		TOKI3),
+	("divf",		IDIVF,		TOKI3),
+	("modw",		IMODW,		TOKI3),
+	("modb",		IMODB,		TOKI3),
+	("andb",		IANDB,		TOKI3),
+	("andw",		IANDW,		TOKI3),
+	("orb",		IORB,		TOKI3),
+	("orw",		IORW,		TOKI3),
+	("xorb",		IXORB,		TOKI3),
+	("xorw",		IXORW,		TOKI3),
+	("shlb",		ISHLB,		TOKI3),
+	("shlw",		ISHLW,		TOKI3),
+	("shrb",		ISHRB,		TOKI3),
+	("shrw",		ISHRW,		TOKI3),
+	("insc",		IINSC,		TOKI3),
+	("indc",		IINDC,		TOKI3),
+	("addc",		IADDC,		TOKI3),
+	("lenc",		ILENC,		TOKI2),
+	("lena",		ILENA,		TOKI2),
+	("lenl",		ILENL,		TOKI2),
+	("beqb",		IBEQB,		TOKI3),
+	("bneb",		IBNEB,		TOKI3),
+	("bltb",		IBLTB,		TOKI3),
+	("bleb",		IBLEB,		TOKI3),
+	("bgtb",		IBGTB,		TOKI3),
+	("bgeb",		IBGEB,		TOKI3),
+	("beqw",		IBEQW,		TOKI3),
+	("bnew",		IBNEW,		TOKI3),
+	("bltw",		IBLTW,		TOKI3),
+	("blew",		IBLEW,		TOKI3),
+	("bgtw",		IBGTW,		TOKI3),
+	("bgew",		IBGEW,		TOKI3),
+	("beqf",		IBEQF,		TOKI3),
+	("bnef",		IBNEF,		TOKI3),
+	("bltf",		IBLTF,		TOKI3),
+	("blef",		IBLEF,		TOKI3),
+	("bgtf",		IBGTF,		TOKI3),
+	("bgef",		IBGEF,		TOKI3),
+	("beqc",		IBEQC,		TOKI3),
+	("bnec",		IBNEC,		TOKI3),
+	("bltc",		IBLTC,		TOKI3),
+	("blec",		IBLEC,		TOKI3),
+	("bgtc",		IBGTC,		TOKI3),
+	("bgec",		IBGEC,		TOKI3),
+	("slicea",	ISLICEA,	TOKI3),
+	("slicela",	ISLICELA,	TOKI3),
+	("slicec",	ISLICEC,	TOKI3),
+	("indw",		IINDW,		TOKI3),
+	("indf",		IINDF,		TOKI3),
+	("indb",		IINDB,		TOKI3),
+	("negf",		INEGF,		TOKI2),
+	("movl",		IMOVL,		TOKI2),
+	("addl",		IADDL,		TOKI3),
+	("subl",		ISUBL,		TOKI3),
+	("divl",		IDIVL,		TOKI3),
+	("modl",		IMODL,		TOKI3),
+	("mull",		IMULL,		TOKI3),
+	("andl",		IANDL,		TOKI3),
+	("orl",		IORL,		TOKI3),
+	("xorl",		IXORL,		TOKI3),
+	("shll",		ISHLL,		TOKI3),
+	("shrl",		ISHRL,		TOKI3),
+	("bnel",		IBNEL,		TOKI3),
+	("bltl",		IBLTL,		TOKI3),
+	("blel",		IBLEL,		TOKI3),
+	("bgtl",		IBGTL,		TOKI3),
+	("bgel",		IBGEL,		TOKI3),
+	("beql",		IBEQL,		TOKI3),
+	("cvtlf",	ICVTLF,		TOKI2),
+	("cvtfl",	ICVTFL,		TOKI2),
+	("cvtlw",	ICVTLW,		TOKI2),
+	("cvtwl",	ICVTWL,		TOKI2),
+	("cvtlc",	ICVTLC,		TOKI2),
+	("cvtcl",	ICVTCL,		TOKI2),
+	("headl",	IHEADL,		TOKI2),
+	("consl",	ICONSL,		TOKI2),
+	("newcl",	INEWCL,		TOKI1),
+	("casec",	ICASEC,		TOKI2),
+	("indl",		IINDL,		TOKI3),
+	("movpc",	IMOVPC,		TOKI2),
+	("tcmp",		ITCMP,		TOKI2),
+	("mnewz",	IMNEWZ,		TOKI3),
+	("cvtrf",	ICVTRF,		TOKI2),
+	("cvtfr",	ICVTFR,		TOKI2),
+	("cvtws",	ICVTWS,		TOKI2),
+	("cvtsw",	ICVTSW,		TOKI2),
+	("lsrw",		ILSRW,		TOKI3),
+	("lsrl",		ILSRL,		TOKI3),
+	("eclr",		IECLR,		TOKI0),
+	("newz",		INEWZ,		TOKI2),
+	("newaz",	INEWAZ,		TOKI3),
+	("raise",	IRAISE,	TOKI1),
+	("casel",	ICASEL,	TOKI2),
+	("mulx",	IMULX,	TOKI3),
+	("divx",	IDIVX,	TOKI3),
+	("cvtxx",	ICVTXX,	TOKI3),
+	("mulx0",	IMULX0,	TOKI3),
+	("divx0",	IDIVX0,	TOKI3),
+	("cvtxx0",	ICVTXX0,	TOKI3),
+	("mulx1",	IMULX1,	TOKI3),
+	("divx1",	IDIVX1,	TOKI3),
+	("cvtxx1",	ICVTXX1,	TOKI3),
+	("cvtfx",	ICVTFX,	TOKI3),
+	("cvtxf",	ICVTXF,	TOKI3),
+	("expw",	IEXPW,	TOKI3),
+	("expl",	IEXPL,	TOKI3),
+	("expf",	IEXPF,	TOKI3),
+	("self",	ISELF,	TOKI1),
+	(nil,	0, 0),
+};
--- /dev/null
+++ b/appl/cmd/asm/mkfile
@@ -1,0 +1,27 @@
+<../../../mkconfig
+
+TARG=asm.dis
+
+YFILES=	asm.y
+
+MODULES=\
+	../limbo/disoptab.m\
+	../limbo/isa.m\
+	../limbo/opname.m\
+	asm.b\
+
+SYSMODULES=\
+	arg.m\
+	bufio.m\
+	draw.m\
+	keyring.m\
+	math.m\
+	string.m\
+	sys.m\
+
+DISBIN=$ROOT/dis
+
+<$ROOT/mkfiles/mkdis
+
+#asm.b:	asm.y
+#	yacc -D1 -o asm.b asm.y
--- /dev/null
+++ b/appl/cmd/auplay.b
@@ -1,0 +1,114 @@
+implement AuPlay;
+
+include "sys.m";
+include "draw.m";
+
+sys:	Sys;
+FD:	import sys;
+stderr:	ref FD;
+
+include "string.m";
+
+str:	String;
+
+prog:	string;
+play:	int;
+Magic:	con "rate";
+data:	con "/dev/audio";
+ctl:	con "/dev/audioctl";
+buffz:	con Sys->ATOMICIO;
+
+AuPlay: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+process(f: string)
+{
+	buff := array[buffz] of byte;
+	inf := sys->open(f, Sys->OREAD);
+	if (inf == nil) {
+		sys->fprint(stderr, "%s: could not open %s: %r\n", prog, f);
+		return;
+	}
+	n := sys->read(inf, buff, buffz);
+	if (n < 0) {
+		sys->fprint(stderr, "%s: could not read %s: %r\n", prog, f);
+		return;
+	}
+	if (n < 10 || string buff[0:4] != Magic) {
+		sys->fprint(stderr, "%s: %s: not an audio file\n", prog, f);
+		return;
+	}
+	i := 0;
+	for (;;) {
+		if (i == n) {
+			sys->fprint(stderr, "%s: %s: bad header\n", prog, f);
+			return;
+		}
+		if (buff[i] == byte '\n') {
+			i++;
+			if (i == n) {
+				sys->fprint(stderr, "%s: %s: bad header\n", prog, f);
+				return;
+			}
+			if (buff[i] == byte '\n') {
+				i++;
+				if ((i % 4) != 0) {
+					sys->fprint(stderr, "%s: %s: unpadded header\n", prog, f);
+					return;
+				}
+				break;
+			}
+		}
+		else
+			i++;
+	}
+	if (!play) {
+		sys->write(stderr, buff, i - 1);
+		return;
+	}
+	df := sys->open(data, Sys->OWRITE);
+	if (df == nil) {
+		sys->fprint(stderr, "%s: could not open %s: %r\n", prog, data);
+		return;
+	}
+	cf := sys->open(ctl, Sys->OWRITE);
+	if (cf == nil) {
+		sys->fprint(stderr, "%s: could not open %s: %r\n", prog, ctl);
+		return;
+	}
+	if (sys->write(cf, buff, i - 1) < 0) {
+		sys->fprint(stderr, "%s: could not write %s: %r\n", prog, ctl);
+		return;
+	}
+	if (n > i && sys->write(df, buff[i:n], n - i) < 0) {
+		sys->fprint(stderr, "%s: could not write %s: %r\n", prog, data);
+		return;
+	}
+	if (sys->stream(inf, df, Sys->ATOMICIO) < 0) {
+		sys->fprint(stderr, "%s: could not stream %s: %r\n", prog, data);
+		return;
+	}
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	stderr = sys->fildes(2);
+	p := hd argv;
+	v := tl argv;
+	(nil, b) := str->splitr(p, "/");
+	if (b != nil)
+		p = b;
+	(b, nil) = str->splitr(p, ".");
+	if (b != nil)
+		p = b[0:len b - 1];
+	prog = p;
+	play = prog == "auplay";
+	while (v != nil) {
+		process(hd v);
+		v = tl v;
+	}
+}
--- /dev/null
+++ b/appl/cmd/auth/aescbc.b
@@ -1,0 +1,240 @@
+implement Aescbc;
+
+#
+# broadly transliterated from the Plan 9 command
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "keyring.m";
+	kr: Keyring;
+	AESbsize, MD5dlen, SHA1dlen: import Keyring;
+
+include "arg.m";
+
+Aescbc: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+#
+# encrypted file: v2hdr, 16 byte IV, AES-CBC(key, random || file), HMAC_SHA1(md5(key), AES-CBC(random || file))
+#
+
+Checkpat: con "XXXXXXXXXXXXXXXX";
+Checklen: con len Checkpat;
+Bufsize: con 4096;
+AESmaxkey: con 32;
+
+V2hdr: con "AES CBC SHA1  2\n";
+
+bin: ref Iobuf;
+bout: ref Iobuf;
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	sys->pctl(Sys->FORKFD, nil);
+	stderr = sys->fildes(2);
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("auth/aescbc -d [-k key] [-f keyfile] <file.aes >clear.txt\n  or: auth/aescbc -e [-k key] [-f keyfile] <clear.txt >file.aes");
+	encrypt := -1;
+	keyfile: string;
+	pass: string;
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' or 'e' =>
+			if(encrypt >= 0)
+				arg->usage();
+			encrypt = o == 'e';
+		'f' =>
+			keyfile = arg->earg();
+		'k' =>
+			pass = arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args != nil || encrypt < 0)
+		arg->usage();
+	arg = nil;
+
+	bin = bufio->fopen(sys->fildes(0), Bufio->OREAD);
+	bout = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+
+	buf := array[Bufsize+SHA1dlen] of byte;	# Checklen <= SHA1dlen
+
+	pwd: array of byte;
+	if(keyfile != nil){
+		fd := sys->open(keyfile, Sys->OREAD);
+		if(fd == nil)
+			error(sys->sprint("can't open %q: %r", keyfile), "keyfile");
+		n := sys->readn(fd, buf, len buf);
+		while(n > 0 && buf[n-1] == byte '\n')
+			n--;
+		if(n <= 0)
+			error("no key", "no key");
+		pwd = buf[0:n];
+	}else{
+		if(pass == nil)
+			pass = readpassword("password");
+		if(pass == nil)
+			error("no key", "no key");
+		pwd = array of byte pass;
+		for(i := 0;  i < len pass; i++)
+			pass[i] = 0;
+	}
+	key := array[AESmaxkey] of byte;
+	key2 := array[SHA1dlen] of byte;
+	dstate := kr->sha1(array of byte "aescbc file", 11, nil, nil);
+	kr->sha1(pwd, len pwd, key2, dstate);
+	for(i := 0; i < len pwd; i++)
+		pwd[i] = byte 0;
+	key[0:] = key2[0:MD5dlen];
+	nkey := MD5dlen;
+	kr->md5(key, nkey, key2, nil);	# protect key even if HMAC_SHA1 is broken
+	key2 = key2[0:MD5dlen];
+
+	if(encrypt){
+		Write(array of byte V2hdr, AESbsize);
+		genrandom(buf, 2*AESbsize); # CBC is semantically secure if IV is unpredictable.
+		aes := kr->aessetup(key[0:nkey], buf);  # use first AESbsize bytes as IV
+		kr->aescbc(aes, buf[AESbsize:], AESbsize, Keyring->Encrypt);  # use second AESbsize bytes as initial plaintext
+		Write(buf, 2*AESbsize);
+		dstate = kr->hmac_sha1(buf[AESbsize:], AESbsize, key2, nil, nil);
+		while((n := bin.read(buf, Bufsize)) > 0){
+			kr->aescbc(aes, buf, n, Keyring->Encrypt);
+			Write(buf, n);
+			dstate = kr->hmac_sha1(buf, n, key2, nil, dstate);
+			if(n < Bufsize)
+				break;
+		}
+		if(n < 0)
+			error(sys->sprint("read error: %r"), "read error");
+		kr->hmac_sha1(nil, 0, key2, buf, dstate);
+		Write(buf, SHA1dlen);
+	}else{	# decrypt
+		Read(buf, AESbsize);
+		if(string buf[0:AESbsize] == V2hdr){
+			Read(buf, 2*AESbsize);	# read IV and random initial plaintext
+			aes := kr->aessetup(key[0:nkey], buf);
+			dstate = kr->hmac_sha1(buf[AESbsize:], AESbsize, key2, nil, nil);
+			kr->aescbc(aes, buf[AESbsize:], AESbsize, Keyring->Decrypt);
+			Read(buf, SHA1dlen);
+			while((n := bin.read(buf[SHA1dlen:], Bufsize)) > 0){
+				dstate = kr->hmac_sha1(buf, n, key2, nil, dstate);
+				kr->aescbc(aes, buf, n, Keyring->Decrypt);
+				Write(buf, n);
+				buf[0:] = buf[n:n+SHA1dlen];	# these bytes are not yet decrypted
+			}
+			kr->hmac_sha1(nil, 0, key2, buf[SHA1dlen:], dstate);
+			if(!eqbytes(buf, buf[SHA1dlen:], SHA1dlen))
+				error("decrypted file failed to authenticate", "failed to authenticate");
+		}else{	# compatibility with past mistake; assume we're decrypting secstore files
+			aes := kr->aessetup(key[0:AESbsize], buf);
+			Read(buf, Checklen);
+			kr->aescbc(aes, buf, Checklen, Keyring->Decrypt);
+			while((n := bin.read(buf[Checklen:], Bufsize)) > 0){
+				kr->aescbc(aes, buf[Checklen:], n, Keyring->Decrypt);
+				Write(buf, n);
+				buf[0:] = buf[n:n+Checklen];
+			}
+			if(string buf[0:Checklen] != Checkpat)
+				error("decrypted file failed to authenticate", "failed to authenticate");
+		}
+	}
+	bout.flush();
+}
+
+error(s: string, why: string)
+{
+	bout.flush();
+	sys->fprint(stderr, "aescbc: %s\n", s);
+	raise "fail:"+why;
+}
+
+eqbytes(a: array of byte, b: array of byte, n: int): int
+{
+	if(len a < n || len b < n)
+		return 0;
+	for(i := 0; i < n; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+Read(buf: array of byte, n: int)
+{
+	if(bin.read(buf, n) != n){
+		sys->fprint(sys->fildes(2), "aescbc: unexpectedly short read\n");
+		raise "fail:read error";
+	}
+}
+
+Write(buf: array of byte, n: int)
+{
+	if(bout.write(buf,  n) != n){
+		sys->fprint(sys->fildes(2), "aescbc: write error: %r\n");
+		raise "fail:write error";
+	}
+}
+
+readpassword(prompt: string): string
+{
+	cons := sys->open("/dev/cons", Sys->ORDWR);
+	if(cons == nil)
+		return nil;
+	stdin := bufio->fopen(cons, Sys->OREAD);
+	if(stdin == nil)
+		return nil;
+	cfd := sys->open("/dev/consctl", Sys->OWRITE);
+	if (cfd == nil || sys->fprint(cfd, "rawon") <= 0)
+		sys->fprint(stderr, "aescbc: warning: cannot hide typed password\n");
+	s: string;
+L:
+	for(;;){
+		sys->fprint(cons, "%s: ", prompt);
+		s = "";
+		while ((c := stdin.getc()) >= 0){
+			case c {
+			'\n' =>
+				break L;
+			'\b' or 8r177 =>
+				if(len s > 0)
+					s = s[0:len s - 1];
+			'u' & 8r037 =>
+				sys->fprint(cons, "\n");
+				continue L;
+			* =>
+				s[len s] = c;
+			}
+		}
+	}
+	sys->fprint(cons, "\n");
+	return s;
+}
+
+genrandom(b: array of byte, n: int)
+{
+	fd := sys->open("/dev/notquiterandom", Sys->OREAD);
+	if(fd == nil){
+		sys->fprint(stderr, "aescbc: can't open /dev/notquiterandom: %r\n");
+		raise "fail:random";
+	}
+	if(sys->read(fd, b, n) != n){
+		sys->fprint(stderr, "aescbc: can't read random numbers: %r\n");
+		raise "fail:read random";
+	}
+}
--- /dev/null
+++ b/appl/cmd/auth/ai2key.b
@@ -1,0 +1,144 @@
+implement Ai2fact;
+
+# authinfo to factotum key set
+#	intermediate version, for use until revised Inferno authentication is ready
+
+
+# converts an old authinfo entry in keyring directory to a key for factotum
+#
+# keys are in proto=infauth, and include the data for the signed certificate, and the diffie-helman parameters
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	keyring: Keyring;
+	Certificate, IPint, PK, SK: import keyring;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "arg.m";
+
+Ai2fact: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("ai2key [-t 'attr=value attr=value ...'] keyfile ...");
+	tag: string;
+	while((o := arg->opt()) != 0)
+		case o {
+		't' =>
+			tag = arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	now := daytime->now();
+	for(; args != nil; args = tl args){
+		keyfile := hd args;
+		ai := keyring->readauthinfo(keyfile);
+		if(ai == nil)
+			error(sys->sprint("cannot read %s: %r", keyfile));
+		if(ai.cert.exp != 0 && ai.cert.exp <= now){
+			sys->fprint(sys->fildes(2), "ai2key: %s: certificate expired -- key ignored\n", keyfile);
+			continue;
+		}
+
+		if(ai.cert.exp != 0)
+			expires := sys->sprint(" expires=%ud", ai.cert.exp);
+		ha := ai.cert.ha;
+		if(ha == "sha")
+			ha = "sha1";
+
+		if(tag != nil)
+			tag = " "+tag;
+
+		sys->print("key proto=infauth%s %s sigalg=%s-%s user=%q signer=%q pk=%s !sk=%s spk=%s cert=%s dh-alpha=%s dh-p=%s%s\n",
+			tag, locations(filename(keyfile)), ai.cert.sa.name, ha, ai.mypk.owner, ai.spk.owner, pktostr(ai.mypk), sktostr(ai.mysk),
+			pktostr(ai.spk), certtostr(ai.cert), ai.alpha.iptostr(16), ai.p.iptostr(16), expires);
+	}
+}
+
+error(e: string)
+{
+	sys->fprint(sys->fildes(2), "ai2key: %s\n", e);
+	raise "fail:error";
+}
+
+filename(s: string): string
+{
+	(nil, fld) := sys->tokenize(s, "/");
+	for(; fld != nil && tl fld != nil; fld = tl fld){
+		# skip
+	}
+	return hd fld;
+}
+
+# guess plausible domain, server and service attributes from the file name
+locations(file: string): string
+{
+	if(file == "default")
+		return "dom=* server=*";
+	(nf, flds) := sys->tokenize(file, "!");
+	case nf {
+	* =>
+		return sys->sprint("%s", server(file));
+	2 =>
+		return sys->sprint("%s", server(hd tl flds));
+	3 =>
+		# ignore network component
+		return sys->sprint("%s service=%q", server(hd tl flds), hd tl tl flds);
+	}
+}
+
+server(name: string): string
+{
+	# if the name contains dot(s), we'll treat it as a domain name
+	if(sys->tokenize(name, ".").t0 > 1)
+		return sys->sprint("dom=%q server=%q", name, name);
+	return sys->sprint("server=%q", name);
+}
+
+certtostr(c: ref Certificate): string
+{
+	return dnl(keyring->certtostr(c));
+}
+
+pktostr(pk: ref PK): string
+{
+	return dnl(keyring->pktostr(pk));
+}
+
+sktostr(sk: ref SK): string
+{
+	return dnl(keyring->sktostr(sk));
+}
+
+dnl(s: string): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '\n')
+			s[i] = '^';
+	while(--i > 0 && s[i] == '^'){
+		# skip
+	}
+	if(i != len s)
+		return s[0: i+1];
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/auth/changelogin.b
@@ -1,0 +1,304 @@
+implement Changelogin;
+
+include "sys.m";
+	sys: Sys;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+
+Changelogin: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr, stdin, stdout: ref Sys->FD;
+keydb := "/mnt/keys";
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	ok: int;
+	word: string;
+
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+
+	stdin = sys->fildes(0);
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+
+	argv0 := hd args;
+	args = tl args;
+
+	if(args == nil){
+		sys->fprint(stderr, "usage: %s userid\n", argv0);
+		raise "fail:usage";
+	}
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil) {
+		sys->fprint(stderr, "%s: can't load Daytime: %r\n", argv0);
+		raise "fail:load";
+	}
+
+	# get password
+	id := hd args;
+	(dbdir, secret, expiry, err) := getuser(id);
+	if(dbdir == nil){
+		if(err != nil){
+			sys->fprint(stderr, "%s: can't get auth info for %s in %s: %s\n", argv0, id, keydb, err);
+			raise "fail:no key";
+		}
+		sys->print("new account\n");
+	}
+	for(;;){
+		if(secret != nil)
+			sys->print("secret [default = don't change]: ");
+		else
+			sys->print("secret: ");
+		(ok, word) = readline(stdin, "rawon");
+		if(!ok)
+			exit;
+		if(word == "" && secret != nil)
+			break;
+		if(len word >= 8)
+			break;
+		sys->print("!secret must be at least 8 characters\n");
+	}
+	newsecret: array of byte;
+	if(word != ""){
+		# confirm password change
+		word1 := word;
+		sys->print("confirm: ");
+		(ok, word) = readline(stdin, "rawon");
+		if(!ok || word != word1) {
+			sys->print("Entries do not match. Authinfo record unchanged.\n"); 
+			raise "fail:mismatch";
+		}
+
+		pwbuf := array of byte word;
+		newsecret = array[Keyring->SHA1dlen] of byte;
+		kr->sha1(pwbuf, len pwbuf, newsecret, nil);
+	}
+
+	# get expiration time (midnight of date specified)
+	maxdate := "17012038";			# largest date possible without incurring integer overflow
+	now := daytime->now();
+	tm := daytime->local(now);
+	tm.sec = 59;
+	tm.min = 59;
+	tm.hour = 23;
+	tm.year += 1;
+	if(dbdir == nil)
+		expsecs := daytime->tm2epoch(tm);	# set expiration date to 23:59:59 one year from today
+	else
+		expsecs = expiry;
+	for(;;){
+		defexpdate := "permanent";
+		if(expsecs != 0) {
+			otm := daytime->local(expsecs);
+			defexpdate = sys->sprint("%2.2d%2.2d%4.4d", otm.mday, otm.mon+1, otm.year+1900);
+		}
+		sys->print("expires [DDMMYYYY/permanent, return = %s]: ", defexpdate);
+		(ok, word) = readline(stdin, "rawoff");
+		if(!ok)
+			exit;
+		if(word == "")
+			word = defexpdate;
+		if(word == "permanent"){
+			expsecs = 0;
+			break;
+		}
+		if(len word != 8){
+			sys->print("!bad date format %s\n", word);
+			continue;
+		}
+		tm.mday = int word[0:2];
+		if(tm.mday > 31 || tm.mday < 1){
+			sys->print("!bad day of month %d\n", tm.mday);
+			continue;
+		}
+		tm.mon = int word[2:4] - 1;
+		if(tm.mon > 11 || tm.mday < 0){
+			sys->print("!bad month %d\n", tm.mon + 1);
+			continue;
+		}
+		tm.year = int word[4:8] - 1900;
+		if(tm.year < 70){
+			sys->print("!bad year %d (year may be no earlier than 1970)\n", tm.year + 1900);
+			continue;
+		}
+		expsecs = daytime->tm2epoch(tm);
+		if(expsecs > now)
+			break;
+		else {
+			newexpdate := sys->sprint("%2.2d%2.2d%4.4d", tm.mday, tm.mon+1, tm.year+1900);
+			tm          = daytime->local(daytime->now());
+			today      := sys->sprint("%2.2d%2.2d%4.4d", tm.mday, tm.mon+1, tm.year+1900);
+			sys->print("!bad expiration date %s (must be between %s and %s)\n", newexpdate, today, maxdate);
+			expsecs = now;
+		}
+	}
+	newexpiry := expsecs;
+
+#	# get the free form field
+#	if(pw != nil)
+#		npw.other = pw.other;
+#	else
+#		npw.other = "";
+#	sys->print("free form info [return = %s]: ", npw.other);
+#	(ok, word) = readline(stdin,"rawoff");
+#	if(!ok)
+#		exit;
+#	if(word != "")
+#		npw.other = word;
+
+	if(dbdir == nil){
+		dbdir = keydb+"/"+id;
+		fd := sys->create(dbdir, Sys->OREAD, Sys->DMDIR|8r700);
+		if(fd == nil){
+			sys->fprint(stderr, "%s: can't create account %s: %r\n", argv0, id);
+			raise "fail:create user";
+		}
+	}
+	changed := 0;
+	if(!eq(newsecret, secret)){
+		if(putsecret(dbdir, newsecret) < 0){
+			sys->fprint(stderr, "%s: can't update secret for %s: %r\n", argv0, id);
+			raise "fail:update";
+		}
+		changed = 1;
+	}
+	if(newexpiry != expiry){
+		if(putexpiry(dbdir, newexpiry) < 0){
+			sys->fprint(stderr, "%s: can't update expiry time for %s: %r\n", argv0, id);
+			raise "fail:update";
+		}
+		changed = 1;
+	}
+	sys->print("change written\n");
+}
+
+getuser(id: string): (string, array of byte, int, string)
+{
+	(ok, nil) := sys->stat(keydb);
+	if(ok < 0)
+		return (nil, nil, 0, sys->sprint("can't stat %s: %r", id));
+	dbdir := keydb+"/"+id;
+	(ok, nil) = sys->stat(dbdir);
+	if(ok < 0)
+		return (nil, nil, 0, nil);
+	fd := sys->open(dbdir+"/secret", Sys->OREAD);
+	if(fd == nil)
+		return (nil, nil, 0, sys->sprint("can't open %s/secret: %r", id));
+	d: Sys->Dir;
+	(ok, d) = sys->fstat(fd);
+	if(ok < 0)
+		return (nil, nil, 0, sys->sprint("can't stat %s/secret: %r", id));
+	l := int d.length;
+	secret: array of byte;
+	if(l > 0){
+		secret = array[l] of byte;
+		if(sys->read(fd, secret, len secret) != len secret)
+			return (nil, nil, 0, sys->sprint("error reading %s/secret: %r", id));
+	}
+	fd = sys->open(dbdir+"/expire", Sys->OREAD);
+	if(fd == nil)
+		return (nil, nil, 0, sys->sprint("can't open %s/expiry: %r", id));
+	b := array[32] of byte;
+	n := sys->read(fd, b, len b);
+	if(n <= 0)
+		return (nil, nil, 0, sys->sprint("error reading %s/expiry: %r", id));
+	return (dbdir, secret, int string b[0:n], nil);
+}
+
+eq(a, b: array of byte): int
+{
+	if(len a != len b)
+		return 0;
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+putsecret(dir: string, secret: array of byte): int
+{
+	fd := sys->create(dir+"/secret", Sys->OWRITE, 8r600);
+	if(fd == nil)
+		return -1;
+	return sys->write(fd, secret, len secret);
+}
+
+putexpiry(dir: string, expiry: int): int
+{
+	fd := sys->open(dir+"/expire", Sys->OWRITE);
+	if(fd == nil)
+		return -1;
+	return sys->fprint(fd, "%d", expiry);
+}
+
+readline(io: ref Sys->FD, mode: string): (int, string)
+{
+	r : int;
+	line : string;
+	buf := array[8192] of byte;
+	fdctl : ref Sys->FD;
+	rawoff := array of byte "rawoff";
+
+	#
+	# Change console mode to rawon
+	#
+	if(mode == "rawon"){
+		fdctl = sys->open("/dev/consctl", sys->OWRITE);
+		if(fdctl == nil || sys->write(fdctl,array of byte mode,len mode) != len mode){
+			sys->fprint(stderr, "unable to change console mode");
+			return (0,nil);
+		}
+	}
+
+	#
+	# Read up to the CRLF
+	#
+	line = "";
+	for(;;) {
+		r = sys->read(io, buf, len buf);
+		if(r <= 0){
+			sys->fprint(stderr, "error read from console mode");
+			if(mode == "rawon")
+				sys->write(fdctl,rawoff,6);
+			return (0, nil);
+		}
+
+		line += string buf[0:r];
+		if ((len line >= 1) && (line[(len line)-1] == '\n')){
+			if(mode == "rawon"){
+				r = sys->write(stdout,array of byte "\n",1);
+				if(r <= 0) {
+					sys->write(fdctl,rawoff,6);
+					return (0, nil);
+				}
+			}
+			break;
+		}
+		else {
+			if(mode == "rawon"){
+				#r = sys->write(stdout, array of byte "*",1);
+				if(r <= 0) {
+					sys->write(fdctl,rawoff,6);
+					return (0, nil);
+				}
+			}
+		}
+	}
+
+	if(mode == "rawon")
+		sys->write(fdctl,rawoff,6);
+
+	# Total success!
+	return (1, line[0:len line - 1]);
+}
--- /dev/null
+++ b/appl/cmd/auth/convpasswd.b
@@ -1,0 +1,120 @@
+implement Convpasswd;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "keyring.m";
+	keyring: Keyring;
+	IPint: import keyring;
+
+include "security.m";
+
+include "arg.m";
+
+Convpasswd: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+PW: adt {
+	id:	string;			# user id
+	pw:	array of byte;	# password hashed by SHA
+	expire:	int;		# expiration time (epoch seconds)
+	other:	string;		# about the account	
+};
+
+mntpt := "/mnt/keys";
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		noload(Arg->PATH);
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		noload(Arg->PATH);
+	force := 0;
+	verbose := 0;
+	arg->init(args);
+	arg->setusage("convpasswd [-f] [-v] [-m /mnt/keys] [passwordfile]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'f' =>		force = 1;
+		'm' =>	mntpt = arg->earg();
+		'v' =>	verbose = 1;
+		* =>		arg->usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	f := "/keydb/password";
+	if(args != nil)
+		f = hd args;
+	iob := bufio->open(f, Bufio->OREAD);
+	if(iob == nil)
+		error(sys->sprint("%s: %r", f));
+	for(line := 1; (s := iob.gets('\n')) != nil; line++) {
+		(n, tokl) := sys->tokenize(s, ":\n");
+		if (n < 3){
+			sys->fprint(sys->fildes(2), "convpasswd: %s:%d: invalid format\n", f, line);
+			continue;
+		}
+		pw := ref PW;
+		pw.id = hd tokl;
+		pw.pw = IPint.b64toip(hd tl tokl).iptobytes();
+		pw.expire = int hd tl tl tokl;
+		if (n==3)
+			pw.other = nil;
+		else
+			pw.other = hd tl tl tl tokl;
+		err := writekey(pw, force);
+		if(err != nil)
+			error(sys->sprint("error writing /mnt/keys entry for %s: %s", pw.id, err));
+		if(verbose)
+			sys->print("%s\n", pw.id);
+	}
+}
+
+noload(p: string)
+{
+	error(sys->sprint("can't load %s: %r", p));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "convpasswd: %s\n", s);
+	raise "fail:error";
+}
+
+writekey(pw: ref PW, force: int): string
+{
+	dir := mntpt+"/"+pw.id;
+	if(sys->open(dir, Sys->OREAD) == nil){
+		# make it
+		d := sys->create(dir, Sys->OREAD, Sys->DMDIR|8r600);
+		if(d == nil)
+			return sys->sprint("can't create %s: %r", dir);
+	}else if(!force)
+		return nil;		# leave existing entry alone
+	secret := dir+"/secret";
+	fd := sys->open(secret, Sys->OWRITE);
+	if(fd == nil)
+		return sys->sprint("can't open %s: %r", secret);
+	if(sys->write(fd, pw.pw, len pw.pw) != len pw.pw)
+		return sys->sprint("error writing %s: %r", secret);
+	expire := dir+"/expire";
+	fd = sys->open(expire, Sys->OWRITE);
+	if(fd == nil)
+		return sys->sprint("can't open %s: %r", expire);
+	if(sys->fprint(fd, "%d", pw.expire) < 0)
+		return sys->sprint("error writing %s: %r", expire);
+	# no equivalent of `other'
+	return nil;
+}
--- /dev/null
+++ b/appl/cmd/auth/countersigner.b
@@ -1,0 +1,64 @@
+implement Countersigner;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "keyring.m";
+	kr: Keyring;
+
+include "msgio.m";
+	msgio: Msgio;
+
+include "security.m";
+
+Countersigner: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr, stdin, stdout: ref Sys->FD;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	msgio = load Msgio Msgio->PATH;
+	msgio->init();
+
+	stdin = sys->fildes(0);
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+
+	sys->pctl(Sys->FORKNS, nil);
+	if(sys->chdir("/keydb") < 0){
+		sys->fprint(stderr, "countersigner: no key database\n");
+		raise "fail:no keydb";
+	}
+
+	# get boxid
+	buf := msgio->getmsg(stdin);
+	if(buf == nil){
+		sys->fprint(stderr, "countersigner: client hung up\n");
+		raise "fail:hungup";
+	}
+	boxid := string buf;
+
+	# read file
+	file := "countersigned/"+boxid;
+	fd := sys->open(file, Sys->OREAD);
+	if(fd == nil){
+		sys->fprint(stderr, "countersigner: can't open %s: %r\n", file);
+		raise "fail:bad boxid";
+	}
+	blind := msgio->getmsg(fd);
+	if(blind == nil){
+		sys->fprint(stderr, "countersigner: can't read %s\n", file);
+		raise "fail:no blind";
+	}
+
+	# answer client
+	msgio->sendmsg(stdout, blind, len blind);
+}
--- /dev/null
+++ b/appl/cmd/auth/createsignerkey.b
@@ -1,0 +1,140 @@
+implement Createsignerkey;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "daytime.m";
+
+include "ipints.m";
+include "crypt.m";
+	crypt: Crypt;
+
+include "oldauth.m";
+	oldauth: Oldauth;
+
+include "arg.m";
+
+# signer key never expires
+SKexpire:       con 0;
+
+# size in bits of modulus for public keys
+PKmodlen:		con 1024;
+
+# size in bits of modulus for diffie hellman
+DHmodlen:		con 1024;
+
+algs := array[] of {"rsa", "elgamal"};	# first entry is default
+
+Createsignerkey: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	err: string;
+
+	sys = load Sys Sys->PATH;
+	crypt = load Crypt Crypt->PATH;
+	oldauth = load Oldauth Oldauth->PATH;
+	oldauth->init();
+	arg := load Arg Arg->PATH;
+
+	arg->init(args);
+	arg->setusage("createsignerkey [-a algorithm] [-f keyfile] [-e ddmmyyyy] [-b size-in-bits] name-of-owner");
+	alg := algs[0];
+	filename := "/keydb/signerkey";
+	expire := SKexpire;
+	bits := PKmodlen;
+	while((c := arg->opt()) != 0){
+		case c {
+		'a' =>
+			alg = arg->arg();
+			if(alg == nil)
+				arg->usage();
+			for(i:=0;; i++){
+				if(i >= len algs)
+					error(sys->sprint("unknown algorithm: %s", alg));
+				else if(alg == algs[i])
+					break;
+			}
+		'f' or 'k' =>
+			filename = arg->earg();
+		'e' =>
+			s := arg->earg();
+			(err, expire) = checkdate(s);
+			if(err != nil)
+				error(err);
+		'b' =>
+			s := arg->earg();
+			bits = int s;
+			if(bits < 32 || bits > 4096)
+				error("modulus must be in the range of 32 to 4096 bits");
+		* =>
+			arg->usage();
+		}
+	}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	owner := hd args;
+
+	# generate a local key, self-signed
+	info := ref Oldauth->Authinfo;
+	info.mysk = crypt->genSK(alg, bits);
+	if(info.mysk == nil)
+		error(sys->sprint("algorithm %s not configured in system", alg));
+	info.owner = owner;
+	info.mypk = crypt->sktopk(info.mysk);
+	info.spk = crypt->sktopk(info.mysk);
+	myPKbuf := array of byte oldauth->pktostr(info.mypk, owner);
+	state := crypt->sha1(myPKbuf, len myPKbuf, nil, nil);
+	info.cert = oldauth->sign(info.mysk, owner, expire, state, "sha1");
+	(info.alpha, info.p) = crypt->dhparams(DHmodlen);
+
+	if(oldauth->writeauthinfo(filename, info) < 0)
+		error(sys->sprint("can't write signerkey file %s: %r", filename));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "createsignerkey: %s\n", s);
+	raise "fail:error";
+}
+
+checkdate(word: string): (string, int)
+{
+	if(len word != 8)
+		return ("!date must be in form ddmmyyyy", 0);
+
+	daytime := load Daytime Daytime->PATH;
+
+	now := daytime->now();
+
+	tm := daytime->local(now);
+	tm.sec = 59;
+	tm.min = 59;
+	tm.hour = 24;
+
+	tm.mday = int word[0:2];
+	if(tm.mday > 31 || tm.mday < 1)
+		return ("!bad day of month", 0);
+
+	tm.mon = int word[2:4] - 1;
+	if(tm.mon > 11 || tm.mday < 0)
+		return ("!bad month", 0);
+
+	tm.year = int word[4:8] - 1900;
+	if(tm.year < 70)
+		return ("!bad year", 0);
+
+	newdate := daytime->tm2epoch(tm);
+	if(newdate < now)
+		return ("!expiration date must be in the future", 0);
+
+	return (nil, newdate);
+}
--- /dev/null
+++ b/appl/cmd/auth/dsagen.b
@@ -1,0 +1,66 @@
+implement Dsagen;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "ipints.m";
+	ipints: IPints;
+	IPint: import ipints;
+
+include "crypt.m";
+	crypt: Crypt;
+
+include "arg.m";
+
+Dsagen: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	ipints = load IPints IPints->PATH;
+	crypt = load Crypt Crypt->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("auth/dsagen [-t 'attr=value attr=value ...']");
+	tag: string;
+	while((o := arg->opt()) != 0)
+		case o {
+		't' =>
+			tag = arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args != nil)
+		arg->usage();
+	arg = nil;
+
+	sk := crypt->dsagen(nil);
+	if(tag != nil)
+		tag = " "+tag;
+	s := add("p", sk.pk.p);
+	s += add("q", sk.pk.q);
+	s += add("alpha", sk.pk.alpha);
+	s += add("key", sk.pk.key);
+	s += add("!secret", sk.secret);
+	a := sys->aprint("key proto=dsa%s%s\n", tag, s);
+	if(sys->write(sys->fildes(1), a, len a) != len a)
+		error(sys->sprint("error writing key: %r"));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "dsagen: %s\n", s);
+	raise "fail:error";
+}
+
+add(name: string, b: ref IPint): string
+{
+	return " "+name+"="+b.iptostr(16);
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/authio.m
@@ -1,0 +1,85 @@
+Authio: module
+{
+
+	Aattr, Aval, Aquery: con iota;
+
+	Attr: adt {
+		tag:	int;
+		name:	string;
+		val:	string;
+
+		text:	fn(a: self ref Attr): string;
+	};
+
+	Key: adt {
+		attrs:	list of ref Attr;
+		secrets:	list of ref Attr;
+	#	proto:	Authproto;
+
+		mk:	fn(attrs: list of ref Attr): ref Key;
+		text:	fn(k: self ref Key): string;
+		safetext:	fn(k: self ref Key): string;
+	};
+
+	Fid: adt
+	{
+		fid:	int;
+		pid:	int;
+		err:	string;
+		attrs:	list of ref Attr;
+		write:	chan of (array of byte, Sys->Rwrite);
+		read:	chan of (int, Sys->Rread);
+	#	proto:	Authproto;
+		done:	int;
+		ai:	ref Authinfo;
+	};
+
+	Rpc: adt {
+		r:	ref Fid;
+		cmd:	int;
+		arg:	array of byte;
+		nbytes:	int;
+		rc:	chan of (array of byte, string);
+	};
+
+	IO: adt {
+		f:	ref Fid;
+		rpc:	ref Rpc;
+
+		findkey:	fn(io: self ref IO, attrs: list of ref Attr, extra: string): (ref Key, string);
+		findkeys:	fn(io: self ref IO, attrs: list of ref Attr, extra: string): (list of ref Key, string);
+		needkey:	fn(io: self ref IO, attrs: list of ref Attr, extra: string): (ref Key, string);
+		read:	fn(io: self ref IO): array of byte;
+		readn:	fn(io: self ref IO, n: int): array of byte;
+		write:	fn(io: self ref IO, buf: array of byte, n: int): int;
+		toosmall:	fn(io: self ref IO, n: int);
+		error:	fn(io: self ref IO, s: string);
+		rdwr:	fn(io: self ref IO): array of byte;
+		reply2read:	fn(io: self ref IO, a: array of byte, n: int): int;
+		ok:	fn(io: self ref IO);
+		done:	fn(io: self ref IO, ai: ref Authinfo);
+	};
+
+	# need more ... ?
+	Authinfo: adt {
+		cuid:	string;	# caller id
+		suid:	string;	# server id
+		cap:	string;	# capability (only valid on server side)
+		secret:	array of byte;
+	};
+
+	memrandom:	fn(a: array of byte, n: int);
+	eqbytes:	fn(a, b: array of byte): int;
+	netmkaddr:	fn(addr, net, svc: string): string;
+	user:	fn(): string;
+	lookattrval:	fn(a: list of ref Attr, n: string): string;
+	parseline:	fn(s: string): list of ref Attr;
+	attrtext:	fn(a: list of ref Attr): string;
+};
+
+Authproto: module
+{
+	init:	fn(f: Authio): string;
+	interaction:	fn(attrs: list of ref Authio->Attr, io: ref Authio->IO): string;
+	keycheck:	fn(k: ref Authio->Key): string;
+};
--- /dev/null
+++ b/appl/cmd/auth/factotum/factotum.b
@@ -1,0 +1,1066 @@
+implement Factotum, Authio;
+
+#
+# Copyright © 2003-2004 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+	Rread, Rwrite: import Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "keyring.m";
+
+include "authio.m";
+
+include "arg.m";
+
+include "readdir.m";
+
+Factotum: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+#confirm, log
+
+Files: adt {
+	ctl:	ref Sys->FileIO;
+	rpc:	ref Sys->FileIO;
+	proto:	ref Sys->FileIO;
+	needkey:	ref Sys->FileIO;
+};
+
+Debug: con 0;
+debug := Debug;
+
+files: Files;
+authio: Authio;
+
+keymanc: chan of (list of ref Attr, int, chan of (list of ref Key, string));
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	authio = load Authio "$self";
+
+	svcname := "#sfactotum";
+	mntpt := "/mnt/factotum";
+	arg := load Arg Arg->PATH;
+	if(arg != nil){
+		arg->init(args);
+		arg->setusage("auth/factotum [-d] [-m /mnt/factotum] [-s factotum]");
+		while((o := arg->opt()) != 0)
+			case o {
+			'd' =>	debug++;
+			'm' =>	mntpt = arg->earg();
+			's' =>		svcname = "#s"+arg->earg();
+			* =>	arg->usage();
+			}
+		args = arg->argv();
+		if(args != nil)
+			arg->usage();
+		arg = nil;
+	}
+	sys->unmount(nil, mntpt);
+	if(sys->bind(svcname, mntpt, Sys->MREPL) < 0)
+		err(sys->sprint("can't bind %s on %s: %r", svcname, mntpt));
+	files.ctl = sys->file2chan(mntpt, "ctl");
+	files.rpc = sys->file2chan(mntpt, "rpc");
+	files.proto = sys->file2chan(mntpt, "proto");
+	files.needkey = sys->file2chan(mntpt, "needkey");
+	if(files.ctl == nil || files.rpc == nil || files.proto == nil || files.needkey == nil)
+		err(sys->sprint("can't create %s/*: %r", mntpt));
+	keymanc = chan of (list of ref Attr, int, chan of (list of ref Key, string));
+	spawn factotumsrv();
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	b := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, b, len b);
+	if(n <= 0)
+		return nil;
+	return string b[0:n];
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "factotum: %s\n", s);
+	raise "fail:error";
+}
+
+rlist: list of ref Fid;
+
+factotumsrv()
+{
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD|Sys->FORKENV, nil);
+	if(debug == 0)
+		privacy();
+	allkeys := array[0] of ref Key;
+	pidc := chan of int;
+	donec := chan of ref Fid;
+#	keyc := chan of (list of ref Attr, chan of (ref Key, string));
+	needfid := -1;
+	needed, needy: list of (int, list of ref Attr, chan of (list of ref Key, string));
+	needread: Sys->Rread;
+	needtag := 0;
+	for(;;) X: alt{
+	r := <-donec =>
+		r.pid = 0;
+		cleanfid(r.fid);
+
+	(off, nbytes, nil, rc) := <-files.ctl.read =>
+		if(rc == nil)
+			break;
+		s := "";
+		for(i := 0; i < len allkeys; i++)
+			if((k := allkeys[i]) != nil)
+				s += k.safetext()+"\n";
+		rc <-= reads(s, off, nbytes);
+	(nil, data, nil, wc) := <-files.ctl.write =>
+		if(wc == nil)
+			break;
+		(nf, flds) := sys->tokenize(string data, "\n\r");
+		if(nf > 1){
+			# compatibility with plan 9; has the advantage you can tell which key is wrong
+			wc <-= (0, "multiline write not allowed");
+			break;
+		}
+		if(flds == nil || (hd flds)[0] == '#'){
+			wc <-= (len data, nil);
+			break;
+		}
+		s := hd flds;
+		for(i := 0; i < len s && s[i] != ' '; i++){
+			# skip
+		}
+		verb := s[0:i];
+		if(i < len s)
+			i++;
+		s = s[i:];
+		case verb {
+		"key" =>
+			k := Key.mk(parseline(s));
+			if(k == nil){
+				wc <-= (len data, nil);	# ignore it
+				break;
+			}
+			if(lookattrval(k.attrs, "proto") == nil){
+				wc <-= (0, "key without proto");
+				break;
+			}
+			allkeys = addkey(allkeys, k);
+			wc <-= (len data, nil);
+		"delkey" =>
+			attrs := parseline(s);
+			for(al := attrs; al != nil; al = tl al){
+				a := hd al;
+				if(a.name[0] == '!' && (a.val != nil || a.tag != Aquery)){
+					wc <-= (0, "cannot specify values for private fields");
+					break X;
+				}
+			}
+			if(delkey(allkeys, attrs) == 0)
+				wc <-= (0, "no matching keys");
+			else
+				wc <-= (len data, nil);
+		"debug" =>
+			wc <-= (len data, nil);
+		* =>
+			wc <-= (0, "unknown verb");
+		}
+
+	(nil, nbytes, fid, rc) := <-files.rpc.read =>
+		if(rc == nil)
+			break;
+		r := findfid(fid);
+		if(r == nil){
+			rc <-= (nil, "no rpc pending");
+			break;
+		}
+		alt{
+		r.read <-= (nbytes, rc) =>
+			;
+		* =>
+			rc <-= (nil, "concurrent rpc read not allowed");
+		}
+	(nil, data, fid, wc) := <-files.rpc.write =>
+		if(wc == nil){
+			cleanfid(fid);
+			break;
+		}
+		r := findfid(fid);
+		if(r == nil){
+			r = ref Fid(fid, 0, nil, nil, chan[1] of (array of byte, Rwrite), chan[1] of (int, Rread), 0, nil);
+			spawn request(r, pidc, donec);
+			r.pid = <-pidc;
+			rlist = r :: rlist;
+		}
+		# this non-blocking write avoids a potential deadlock situation that
+		# can happen when a proto module calls findkey at the same time
+		# a client tries to write to the rpc file. this might not be the correct fix!
+		alt{
+		r.write <-= (data, wc) =>
+			;
+		* =>
+			wc <-= (-1, "concurrent rpc write not allowed");
+		}
+
+	(off, nbytes, nil, rc) := <-files.proto.read =>
+		if(rc == nil)
+			break;
+		rc <-= reads(readprotos(), off, nbytes);
+	(nil, nil, nil, wc) := <-files.proto.write =>
+		if(wc != nil)
+			wc <-= (0, "illegal operation");
+
+	(nil, nil, fid, rc) := <-files.needkey.read =>
+		if(rc == nil)
+			break;
+		if(needfid >= 0 && fid != needfid){
+			rc <-= (nil, "file in use");
+			break;
+		}
+		needfid = fid;
+		if(needy != nil){
+			(tag, attr, kc) := hd needy;
+			needy = tl needy;
+			needed = (tag, attr, kc) :: needed;
+			rc <-= (sys->aprint("needkey tag=%ud %s", tag, attrtext(attr)), nil);
+			break;
+		}
+		if(needread != nil){
+			rc <-= (nil, "already reading");
+			break;
+		}
+		needread = rc;
+	(nil, data, fid, wc) := <-files.needkey.write =>
+		if(wc == nil){
+			if(needfid == fid){
+				needfid = -1;	# TO DO? give needkey errors back to request
+				needread = nil;
+			}
+			break;
+		}
+		if(needfid >= 0 && fid != needfid){
+			wc <-= (0, "file in use");
+			break;
+		}
+		needfid = fid;
+		tagline := parseline(string data);
+		if(len tagline != 1 || (t := lookattrval(tagline, "tag")) == nil){
+			wc <-= (0, "no tag");
+			break;
+		}
+		tag := int t;
+		nl: list of (int, list of ref Attr, chan of (list of ref Key, string));
+		found := 0;
+		for(l := needed; l != nil; l = tl l){
+			(ntag, attrs, kc) := hd l;
+			if(tag == ntag){
+				found = 1;
+				k := findkey(allkeys, attrs);
+				if(k != nil)
+					kc <-= (k :: nil, nil);
+				else
+					kc <-= (nil, "needkey "+attrtext(attrs));
+				while((l = tl l) != nil)
+					nl = hd l :: nl;
+				break;
+			}
+			nl = hd l :: nl;
+		}
+		if(found)
+			wc <-= (len data, nil);
+		else
+			wc <-= (0, "tag not found");
+
+	(attrs, required, kc) := <-keymanc =>
+		# look for key and reply
+		kl := findkeys(allkeys, attrs);
+		if(kl != nil){
+			kc <-= (kl, nil);
+			break;
+		}else if(!required || needfid == -1){
+			kc <-= (nil, "needkey "+attrtext(attrs));
+			break;
+		}
+		# query surrounding environment using needkey
+		if(needread != nil){
+			needed = (needtag, attrs, kc) :: needed;
+			needread <-= (sys->aprint("needkey tag=%ud %s", needtag, attrtext(attrs)), nil);
+			needread = nil;
+			needtag++;
+		}else
+			needy = (needtag++, attrs, kc) :: needy;
+	}
+}
+
+findfid(fid: int): ref Fid
+{
+	for(rl := rlist; rl != nil; rl = tl rl){
+		r := hd rl;
+		if(r.fid == fid)
+			return r;
+	}
+	return nil;
+}
+
+cleanfid(fid: int)
+{
+	rl := rlist;
+	rlist = nil;
+	for(; rl != nil; rl = tl rl){
+		r := hd rl;
+		if(r.fid != fid)
+			rlist = r :: rlist;
+		else if(r.pid)
+			kill(r.pid);
+	}
+}
+
+kill(pid: int)
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+privacy()
+{
+	fd := sys->open("#p/"+string sys->pctl(0, nil)+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "private") < 0)
+		sys->fprint(sys->fildes(2), "factotum: warning: unable to make memory private: %r\n");
+}
+
+reads(str: string, off, nbytes: int): (array of byte, string)
+{
+	bstr := array of byte str;
+	slen := len bstr;
+	if(off < 0 || off >= slen)
+		return (nil, nil);
+	if(off + nbytes > slen)
+		nbytes = slen - off;
+	if(nbytes <= 0)
+		return (nil, nil);
+	return (bstr[off:off+nbytes], nil);
+}
+
+readprotos(): string
+{
+	readdir := load Readdir Readdir->PATH;
+	if(readdir == nil)
+		return "unknown\n";
+	(dirs, nil) := readdir->init("/dis/auth/proto", Readdir->NAME|Readdir->COMPACT);
+	s := "";
+	for(i := 0; i < len dirs; i++){
+		n := dirs[i].name;
+		if(len n > 4 && n[len n-4:] == ".dis")
+			s += n[0: len n-4]+"\n";
+	}
+	return s;
+}
+
+Ogok, Ostart, Oread, Owrite, Oauthinfo, Oattr: con iota;
+
+ops := array[] of {
+	(Ostart, "start"),
+	(Oread, "read"),
+	(Owrite, "write"),
+	(Oauthinfo, "authinfo"),
+	(Oattr, "attr"),
+};
+
+request(r: ref Fid, pidc: chan of int, donec: chan of ref Fid)
+{
+	pidc <-= sys->pctl(0, nil);
+	rpc := rio(r);
+	while(rpc != nil){
+		if(rpc.cmd == Ostart){
+			(proto, attrs, e) := startproto(string rpc.arg);
+			if(e != nil){
+				reply(rpc, "error "+e);
+				rpc = rio(r);
+				continue;
+			}
+			r.attrs = attrs;	# saved for attr request
+			ok(rpc);
+			io := ref IO(r, nil);
+			{
+				err := proto->interaction(attrs, io);
+				if(debug && err != nil)
+					sys->fprint(sys->fildes(2), "factotum: failure: %s\n", err);
+				if(r.err == nil)
+					r.err = err;
+				r.done = 1;
+			}exception ex{
+			"*" =>
+				r.done = 0;
+				r.err = "exception "+ex;
+			}
+			if(r.err != nil)
+				io.error(r.err);
+			rpc = finish(r);
+			r.attrs = nil;
+			r.err = nil;
+			r.done = 0;
+			r.ai = nil;
+		}else
+			reply(rpc, "no current protocol");
+	}
+	flushreq(r, donec);
+}
+
+startproto(request: string): (Authproto, list of ref Attr, string)
+{
+	attrs := parseline(request);
+	if(debug > 1)
+		sys->print("-> %s <-\n", attrtext(attrs));
+	p := lookattrval(attrs, "proto");
+	if(p == nil)
+		return (nil, nil, "did not specify protocol");
+	if(debug > 1)
+		sys->print("proto=%s\n", p);
+	if(any(p, "./"))	# avoid unpleasantness
+		return (nil, nil, "illegal protocol: "+p);
+	proto := load Authproto "/dis/auth/proto/"+p+".dis";
+	if(proto == nil)
+		return (nil, nil, sys->sprint("protocol %s: %r", p));
+	if(debug)
+		sys->print("start %s\n", p);
+	e: string;
+	{
+		e = proto->init(authio);
+	}exception ex{
+	"*" =>
+		e = "exception "+ex;
+	}
+	if(e != nil)
+		return (nil, nil, e);
+	return (proto, attrs, nil);
+}
+
+finish(r: ref Fid): ref Rpc
+{
+	while((rpc := rio(r)) != nil)
+		case rpc.cmd {
+		Owrite =>
+			phase(rpc, "protocol phase error");
+		Oread =>
+			if(r.err != nil)
+				reply(rpc, "error "+r.err);
+			else
+				done(rpc, r.ai);
+		Oauthinfo =>
+			if(r.done){
+				if(r.ai == nil)
+					reply(rpc, "error no authinfo available");
+				else{
+					a := packai(r.ai);
+					if(rpc.nbytes-3 < len a)
+						reply(rpc, sys->sprint("toosmall %d", len a + 3));
+					else
+						okdata(rpc, a);
+				}
+			}else
+				reply(rpc, "error authentication unfinished");
+		Ostart =>
+			return rpc;
+		* =>
+			reply(rpc, "error unexpected request");
+		}
+	return nil;
+}
+
+flushreq(r: ref Fid, donec: chan of ref Fid)
+{
+	for(;;) alt{
+	donec <-= r =>
+		exit;
+	(nil, wc) := <-r.write =>
+		wc <-= (0, "write rpc protocol error");
+	(nil, rc) := <-r.read =>
+		rc <-= (nil, "read rpc protocol error");
+	}
+}
+
+rio(r: ref Fid): ref Rpc
+{
+	req: array of byte;
+	for(;;) alt{
+	(data, wc) := <-r.write =>
+		if(req != nil){
+			wc <-= (0, "rpc pending; read to clear");
+			break;
+		}
+		req = data;
+		wc <-= (len data, nil);
+
+	(nbytes, rc) := <-r.read =>
+		if(req == nil){
+			rc <-= (nil, "no rpc pending");
+			break;
+		}
+		(cmd, arg) := op(req, ops);
+		req = nil;
+		rpc := ref Rpc(r, cmd, arg, nbytes, rc);
+		case cmd {
+		Ogok =>
+			reply(rpc, "error unknown rpc");
+			break;
+		Oattr =>
+			if(r.attrs == nil)
+				reply(rpc, "error no attributes");
+			else
+				reply(rpc, "ok "+attrtext(r.attrs));
+			break;
+		* =>
+			return rpc;
+		}
+	}
+}
+
+ok(rpc: ref Rpc)
+{
+	reply(rpc, "ok");
+}
+
+okdata(rpc: ref Rpc, a: array of byte)
+{
+	b := array[len a + 3] of byte;
+	b[0] = byte 'o';
+	b[1] = byte 'k';
+	b[2] = byte ' ';
+	b[3:] = a;
+	rpc.rc <-= (b, nil);
+}
+
+done(rpc: ref Rpc, ai: ref Authinfo)
+{
+	rpc.r.ai = ai;
+	rpc.r.done = 1;
+	if(ai != nil)
+		reply(rpc, "done haveai");
+	else
+		reply(rpc, "done");
+}
+
+phase(rpc: ref Rpc, s: string)
+{
+	reply(rpc, "phase "+s);
+}
+
+needkey(rpc: ref Rpc, attrs: list of ref Attr)
+{
+	reply(rpc, "needkey "+attrtext(attrs));
+}
+
+reply(rpc: ref Rpc, s: string)
+{
+	rpc.rc <-= reads(s, 0, rpc.nbytes);
+}
+
+puta(a: array of byte, n: int, v: array of byte): int
+{
+	if(n < 0)
+		return -1;
+	c := len v;
+	if(n+2+c > len a)
+		return -1;
+	a[n++] = byte c;
+	a[n++] = byte (c>>8);
+	a[n:] = v;
+	return n + len v;
+}
+
+packai(ai: ref Authinfo): array of byte
+{
+	a := array[1024] of byte;
+	i := puta(a, 0, array of byte ai.cuid);
+	i = puta(a, i, array of byte ai.suid);
+	i = puta(a, i, array of byte ai.cap);
+	i = puta(a, i, ai.secret);
+	if(i < 0)
+		return nil;
+	return a[0:i];
+}
+
+op(a: array of byte, ops: array of (int, string)): (int, array of byte)
+{
+	arg: array of byte;
+	for(i := 0; i < len a; i++)
+		if(a[i] == byte ' '){
+			if(i+1 < len a)
+				arg = a[i+1:];
+			break;
+		}
+	s := string a[0:i];
+	for(i = 0; i < len ops; i++){
+		(cmd, name) := ops[i];
+		if(s == name)
+			return (cmd, arg);
+	}
+	return (Ogok, arg);
+}
+
+parseline(s: string): list of ref Attr
+{
+	fld := str->unquoted(s);
+	rfld := fld;
+	for(fld = nil; rfld != nil; rfld = tl rfld)
+		fld = (hd rfld) :: fld;
+	attrs: list of ref Attr;
+	for(; fld != nil; fld = tl fld){
+		n := hd fld;
+		a := "";
+		tag := Aattr;
+		for(i:=0; i<len n; i++)
+			if(n[i] == '='){
+				a = n[i+1:];
+				n = n[0:i];
+				tag = Aval;
+			}
+		if(len n == 0)
+			continue;
+		if(tag == Aattr && len n > 1 && n[len n-1] == '?'){
+			tag = Aquery;
+			n = n[0:len n-1];
+		}
+		attrs = ref Attr(tag, n, a) :: attrs;
+	}
+	return attrs;
+}
+
+Attr.text(a: self ref Attr): string
+{
+	case a.tag {
+	Aattr =>
+		return a.name;
+	Aval =>
+		return a.name+"="+a.val;
+	Aquery =>
+		return a.name+"?";
+	* =>
+		return "??";
+	}
+}
+
+attrtext(attrs: list of ref Attr): string
+{
+	s := "";
+	sp := 0;
+	for(; attrs != nil; attrs = tl attrs){
+		if(sp)
+			s[len s] = ' ';
+		sp = 1;
+		s += (hd attrs).text();
+	}
+	return s;
+}
+
+lookattr(attrs: list of ref Attr, n: string): ref Attr
+{
+	for(; attrs != nil; attrs = tl attrs)
+		if((a := hd attrs).tag != Aquery && a.name == n)
+			return a;
+	return nil;
+}
+
+lookattrval(attrs: list of ref Attr, n: string): string
+{
+	if((a := lookattr(attrs, n)) != nil)
+		return a.val;
+	return nil;
+}
+
+anyattr(attrs: list of ref Attr, n: string): ref Attr
+{
+	for(; attrs != nil; attrs = tl attrs)
+		if((a := hd attrs).name == n)
+			return a;
+	return nil;
+}
+
+reverse[T](l: list of T): list of T
+{
+	r: list of T;
+	for(; l != nil; l = tl l)
+		r = hd l :: r;
+	return r;
+}
+
+setattrs(lv: list of ref Attr, rv: list of ref Attr): list of ref Attr
+{
+	# new attributes
+	nl: list of ref Attr;
+	for(rl := rv; rl != nil; rl = tl rl)
+		if(anyattr(lv, (hd rl).name) == nil)
+			nl = ref(*hd rl) :: nl;
+
+	# new values
+	for(; lv != nil; lv = tl lv){
+		a := lookattr(rv, (hd lv).name);	# won't take queries
+		if(a != nil)
+			nl = ref *a :: nl;
+	}
+
+	return reverse(nl);
+}
+
+delattrs(lv: list of ref Attr, rv: list of ref Attr): list of ref Attr
+{
+	nl: list of ref Attr;
+	for(; lv != nil; lv = tl lv)
+		if(anyattr(rv, (hd lv).name) == nil)
+			nl = hd lv :: nl;
+	return reverse(nl);
+}
+
+ignored(s: string): int
+{
+	return s == "role" || s == "disabled";
+}
+
+matchattr(attrs: list of ref Attr, pat: ref Attr): int
+{
+	return (b := lookattr(attrs, pat.name)) != nil && (pat.tag == Aquery || b.val == pat.val) ||
+			ignored(pat.name);
+}
+
+matchattrs(pub: list of ref Attr, secret: list of ref Attr, pats: list of ref Attr): int
+{
+	for(pl := pats; pl != nil; pl = tl pl)
+		if(!matchattr(pub, hd pl) && !matchattr(secret, hd pl))
+			return 0;
+	return 1;
+}
+
+sortattrs(attrs: list of ref Attr): list of ref Attr
+{
+	a := array[len attrs] of ref Attr;
+	i := 0;
+	for(l := attrs; l != nil; l = tl l)
+		a[i++] = hd l;
+	shellsort(a);
+	for(i = 0; i < len a; i++)
+		l = a[i] :: l;
+	return l;
+}
+
+# sort into decreasing order (we'll reverse the list)
+shellsort(a: array of ref Attr)
+{
+	n := len a;
+	for(gap := n; gap > 0; ) {
+		gap /= 2;
+		max := n-gap;
+		ex: int;
+		do{
+			ex = 0;
+			for(i := 0; i < max; i++) {
+				j := i+gap;
+				if(a[i].name > a[j].name || a[i].name == nil) {
+					t := a[i]; a[i] = a[j]; a[j] = t;
+					ex = 1;
+				}
+			}
+		}while(ex);
+	}
+}
+
+findkey(keys: array of ref Key, attrs: list of ref Attr): ref Key
+{
+	if(debug)
+		sys->print("findkey %q\n", attrtext(attrs));
+	for(i := 0; i < len keys; i++)
+		if((k := keys[i]) != nil && matchattrs(k.attrs, k.secrets, attrs))
+			return k;
+	return nil;
+}
+
+findkeys(keys: array of ref Key, attrs: list of ref Attr): list of ref Key
+{
+	if(debug)
+		sys->print("findkey %q\n", attrtext(attrs));
+	kl: list of ref Key;
+	for(i := 0; i < len keys; i++)
+		if((k := keys[i]) != nil && matchattrs(k.attrs, k.secrets, attrs))
+			kl = k :: kl;
+	return reverse(kl);
+}
+
+delkey(keys: array of ref Key, attrs: list of ref Attr): int
+{
+	nk := 0;
+	for(i := 0; i < len keys; i++)
+		if((k := keys[i]) != nil)
+			if(matchattrs(k.attrs, k.secrets, attrs)){
+				nk++;
+				keys[i] = nil;
+			}
+	return nk;
+}
+
+Key.mk(attrs: list of ref Attr): ref Key
+{
+	k := ref Key;
+	for(; attrs != nil; attrs = tl attrs){
+		a := hd attrs;
+		if(a.name != nil){
+			if(a.name[0] == '!')
+				k.secrets = a :: k.secrets;
+			else
+				k.attrs = a :: k.attrs;
+		}
+	}
+	if(k.attrs != nil || k.secrets != nil)
+		return k;
+	return nil;
+}
+
+addkey(keys: array of ref Key, k: ref Key): array of ref Key
+{
+	for(i := 0; i < len keys; i++)
+		if(keys[i] == nil){
+			keys[i] = k;
+			return keys;
+		}
+	n := array[len keys+1] of ref Key;
+	n[0:] = keys;
+	n[len keys] = k;
+	return n;
+}
+
+Key.text(k: self ref Key): string
+{
+	s := attrtext(k.attrs);
+	if(s != nil && k.secrets != nil)
+		s[len s] = ' ';
+	return s + attrtext(k.secrets);
+}
+
+Key.safetext(k: self ref Key): string
+{
+	s := attrtext(sortattrs(k.attrs));
+	sp := s != nil;
+	for(sl := k.secrets; sl != nil; sl = tl sl){
+		if(sp)
+			s[len s] = ' ';
+		s += sys->sprint("%s?", (hd sl).name);
+	}
+	return s;
+}
+
+any(s: string, t: string): int
+{
+	for(i := 0; i < len s; i++)
+		for(j := 0; j < len t; j++)
+			if(s[i] == t[j])
+				return 1;
+	return 0;
+}
+
+IO.findkey(io: self ref IO, attrs: list of ref Attr, extra: string): (ref Key, string)
+{
+	(kl, err) := io.findkeys(attrs, extra);
+	if(kl != nil)
+		return (hd kl, err);
+	return (nil, err);
+}
+
+IO.findkeys(nil: self ref IO, attrs: list of ref Attr, extra: string): (list of ref Key, string)
+{
+	ea := parseline(extra);
+	for(; ea != nil; ea = tl ea)
+		attrs = hd ea :: attrs;
+	kc := chan of (list of ref Key, string);
+	keymanc <-= (attrs, 1, kc);	# TO DO: 1 => 0 for not needed
+	return <-kc;
+}
+
+IO.needkey(nil: self ref IO, attrs: list of ref Attr, extra: string): (ref Key, string)
+{
+	ea := parseline(extra);
+	for(; ea != nil; ea = tl ea)
+		attrs = hd ea :: attrs;
+	kc := chan of (list of ref Key, string);
+	keymanc <-= (attrs, 1, kc);
+	(kl, err) := <-kc;
+	if(kl != nil)
+		return (hd kl, err);
+	return (nil, err);
+}
+
+IO.read(io: self ref IO): array of byte
+{
+	io.ok();
+	while((rpc := rio(io.f)) != nil)
+		case rpc.cmd {
+		* =>
+			phase(rpc, "protocol phase error");
+		Oauthinfo =>
+			reply(rpc, "error authentication unfinished");
+		Owrite =>
+			io.rpc = rpc;
+			if(rpc.arg == nil)
+				rpc.arg = array[0] of byte;
+			return rpc.arg;
+		}
+	exit;
+}
+
+IO.readn(io: self ref IO, n: int): array of byte
+{
+	while((buf := io.read()) != nil && len buf < n)
+		io.toosmall(n);
+	return buf;
+}
+
+IO.write(io: self ref IO, buf: array of byte, n: int): int
+{
+	io.ok();
+	while((rpc := rio(io.f)) != nil)
+		case rpc.cmd {
+		Oread =>
+			if(rpc.nbytes-3 >= n){
+				okdata(rpc, buf[0:n]);
+				return n;
+			}
+			io.rpc = rpc;
+			io.toosmall(n+3);
+		Oauthinfo =>
+			reply(rpc, "error authentication unfinished");
+		* =>
+			phase(rpc, "protocol phase error");
+		}
+	exit;
+}
+
+IO.rdwr(io: self ref IO): array of byte
+{
+	io.ok();
+	while((rpc := rio(io.f)) != nil)
+		case rpc.cmd {
+		Oread =>
+			io.rpc = rpc;
+			if(rpc.nbytes >= 3)
+				return nil;
+			io.toosmall(128+3);		# make them read something
+		Owrite =>
+			io.rpc = rpc;
+			if(rpc.arg == nil)
+				rpc.arg = array[0] of byte;
+			return rpc.arg;
+		Oauthinfo =>
+			reply(rpc, "error authentication unfinished");
+		* =>
+			phase(rpc, "protocol phase error");
+		}
+	exit;
+}
+
+IO.reply2read(io: self ref IO, buf: array of byte, n: int): int
+{
+	if(io.rpc == nil)
+		return 0;
+	rpc := io.rpc;
+	if(rpc.cmd != Oread){
+		io.rpc = nil;
+		phase(rpc, "internal phase error");
+		return 0;
+	}
+	if(rpc.nbytes-3 < n){
+		io.toosmall(n+3);
+		return 0;
+	}
+	io.rpc = nil;
+	okdata(rpc, buf[0:n]);
+	return 1;
+}
+
+IO.ok(io: self ref IO)
+{
+	if(io.rpc != nil){
+		reply(io.rpc, "ok");
+		io.rpc = nil;
+	}
+}
+
+IO.toosmall(io: self ref IO, n: int)
+{
+	if(io.rpc != nil){
+		reply(io.rpc, sys->sprint("toosmall %d", n));
+		io.rpc = nil;
+	}
+}
+
+IO.error(io: self ref IO, s: string)
+{
+	if(io.rpc != nil){
+		io.rpc.rc <-= (nil, "error "+s);
+		io.rpc = nil;
+	}
+}
+
+IO.done(io: self ref IO, ai: ref Authinfo)
+{
+	io.f.ai = ai;
+	io.ok();
+	while((rpc := rio(io.f)) != nil)
+		case rpc.cmd {
+		Oread or Owrite =>
+			done(rpc, ai);
+			return;
+		* =>
+			phase(rpc, "protocol phase error");
+		}
+}
+
+memrandom(a: array of byte, n: int)
+{
+	if(0){
+		# speed up testing
+		for(i := 0; i < len a; i++)
+			a[i] = byte i;
+		return;
+	}
+	fd := sys->open("/dev/notquiterandom", Sys->OREAD);
+	if(fd == nil)
+		err("can't open /dev/notquiterandom");
+	if(sys->read(fd, a, n) != n)
+		err("can't read /dev/notquiterandom");
+}
+
+eqbytes(a, b: array of byte): int
+{
+	if(len a != len b)
+		return 0;
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+netmkaddr(addr, net, svc: string): string
+{
+	if(net == nil)
+		net = "net";
+	(n, nil) := sys->tokenize(addr, "!");
+	if(n <= 1){
+		if(svc== nil)
+			return sys->sprint("%s!%s", net, addr);
+		return sys->sprint("%s!%s!%s", net, addr, svc);
+	}
+	if(svc == nil || n > 2)
+		return addr;
+	return sys->sprint("%s!%s", addr, svc);
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/feedkey.b
@@ -1,0 +1,321 @@
+implement Feedkey;
+
+#
+# Copyright © 2004 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "string.m";
+	str: String;
+
+Feedkey: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+config := array[] of {
+	"frame .f",
+	"button .f.done -command {send cmd done} -text {Done}",
+	"frame .f.key -bg white",
+	"pack .f.key .f.done .f",
+	"update"
+};
+
+Debug: con 0;
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	str = load String String->PATH;
+
+	needfile := "/mnt/factotum/needkey";
+	if(Debug)
+		needfile = "/dev/null";
+
+	needs := chan of list of ref Attr;
+	acks := chan of int;
+
+	sys->pctl(Sys->NEWPGRP|Sys->NEWFD, list of {0, 1, 2});
+
+	fd := sys->open(needfile, Sys->ORDWR);
+	if(fd == nil)
+		err(sys->sprint("can't open %s: %r", needfile));
+	spawn needy(fd, needs, acks);
+	fd = nil;
+
+	ctlfile := "/mnt/factotum/ctl";
+	keyfd := sys->open(ctlfile, Sys->ORDWR);
+	if(keyfd == nil)
+		err(sys->sprint("can't open %s: %r", ctlfile));
+
+	tkclient->init();
+
+	spawn feedkey(ctxt, keyfd, needs, acks);
+}
+
+feedkey(ctxt: ref Draw->Context, keyfd: ref Sys->FD, needs: chan of list of ref Attr, acks: chan of int)
+{
+	(top, tkctl) := tkclient->toplevel(ctxt, nil, "Need key", Tkclient->Appl);
+
+	cmd := chan of string;
+	tk->namechan(top, cmd, "cmd");
+
+	for(i := 0; i < len config; i++)
+		tkcmd(top, config[i]);
+	tkclient->startinput(top, "ptr" :: nil);
+	tkclient->onscreen(top, nil);
+	if(!Debug)
+		tkclient->wmctl(top, "task");
+
+	attrs: list of ref Attr;
+	for(;;) alt{
+	s :=<-tkctl or
+	s = <-top.ctxt.ctl or
+	s = <-top.wreq =>
+		tkclient->wmctl(top, s);
+	p := <-top.ctxt.ptr =>
+		tk->pointer(top, *p);
+	c := <-top.ctxt.kbd =>
+		tk->keyboard(top, c);
+
+	s := <-cmd =>
+		case s {
+		"done" =>
+			result := extract(top, ".f.key", attrs);
+			if(Debug)
+				sys->print("result: %s\n", attrtext(result));
+			if(sys->fprint(keyfd, "key %s", attrtext(result)) < 0)
+				sys->fprint(sys->fildes(2), "feedkey: can't install key %q: %r\n", attrtext(result));
+			acks <-= 0;
+			tkclient->wmctl(top, "task");
+			tk->cmd(top, "pack forget .f.key");
+		* =>
+			sys->fprint(sys->fildes(2), "feedkey: odd command: %q\n", s);
+		}
+
+	attrs = <-needs =>
+		if(attrs == nil)
+			exit;
+		tkclient->startinput(top, "kbd" :: nil);
+		tkcmd(top, "destroy .f.key");
+		tkcmd(top, "frame .f.key -bg white");
+		populate(top, ".f.key", attrs);
+		tkcmd(top, "pack forget .f.done");
+		tkcmd(top, "pack .f.key .f.done .f");
+		tkcmd(top, "update");
+		tkclient->wmctl(top, "unhide");
+	}
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "feedkey: %s\n", s);
+	raise "fail:error";
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	b := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, b, len b);
+	if(n <= 0)
+		return nil;
+	return string b[0:n];
+}
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	if(0)
+		sys->print("tk: %q\n", cmd);
+	r := tk->cmd(top, cmd);
+	if(r != nil && r[0] == '!')
+		sys->fprint(sys->fildes(2), "feedkey: tk: %q on %q\n", r, cmd);
+	return r;
+}
+
+populate(top: ref Tk->Toplevel, tag: string, attrs: list of ref Attr)
+{
+	c := 0;
+	for(al := attrs; al != nil; al = tl al){
+		a := hd al;
+		if(a.name == nil)
+			tkcmd(top, sys->sprint("entry %s.n%d -bg yellow", tag, c));
+		else
+			tkcmd(top, sys->sprint("label %s.n%d -bg white -text '%s", tag, c, a.name));
+		tkcmd(top, sys->sprint("label %s.e%d -bg white -text '  =  ", tag, c));
+		case a.tag {
+		Aquery =>
+			show := "";
+			if(a.name != nil && a.name[0] == '!')
+				show = " -show {•}";
+			tkcmd(top, sys->sprint("entry %s.v%d%s -bg yellow", tag, c, show));
+			if(a.val == nil && a.name == "user")
+				a.val = user();
+			tkcmd(top, sys->sprint("%s.v%d insert 0 '%s", tag, c, a.val));
+			tkcmd(top, sys->sprint("grid %s.n%d %s.e%d %s.v%d -in %s -sticky w -pady 1", tag, c, tag, c, tag, c, tag));
+		Aval =>
+			if(a.name != nil){
+				val := a.val;
+				if(a.name[0] == '!')
+					val = "...";	# just in case
+				tkcmd(top, sys->sprint("label %s.v%d -bg white -text %s", tag, c, val));
+			}else
+				tkcmd(top, sys->sprint("entry %s.v%d -bg yellow", tag, c));
+			tkcmd(top, sys->sprint("grid %s.n%d %s.e%d %s.v%d -in %s -sticky w -pady 1", tag, c, tag, c, tag, c, tag));
+		Aattr =>
+			tkcmd(top, sys->sprint("grid %s.n%d x x -in %s -sticky w -pady 1", tag, c, tag));
+		}
+		c++;
+	}
+}
+
+extract(top: ref Tk->Toplevel, tag: string, attrs: list of ref Attr): list of ref Attr
+{
+	c := 0;
+	nl: list of ref Attr;
+	for(al := attrs; al != nil; al = tl al){
+		a := ref *hd al;
+		if(a.tag == Aquery){
+			a.val = tkcmd(top, sys->sprint("%s.v%d get", tag, c));
+			if(a.name == nil)
+				a.name = tk->cmd(top, sys->sprint("%s.n%d get", tag, c));	# name might start with `!'
+			if(a.name != nil){
+				a.tag = Aval;
+				nl = a :: nl;
+			}
+		}else
+			nl = a :: nl;
+		c++;
+	}
+	return nl;
+}
+
+reverse[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+needy(fd: ref Sys->FD, needs: chan of list of ref Attr, acks: chan of int)
+{
+	if(Debug){
+		for(;;){
+			needs <-= parseline("proto=pass user? server=fred.com service=ftp confirm !password?");
+			<-acks;
+		}
+	}
+
+	buf := array[512] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0){
+		s := string buf[0:n];
+		for(i := 0; i < len s; i++)
+			if(s[i] == ' ')
+				break;
+		if(i >= len s)
+			continue;
+		attrs := parseline(s[i+1:]);
+		nl: list of ref Attr;
+		tag: ref Attr;
+		for(; attrs != nil; attrs = tl attrs){
+			a := hd attrs;
+			if(a.name == "tag")
+				tag = a;
+			else
+				nl = a :: nl;
+		}
+		if(nl == nil)
+			continue;
+		attrs = reverse(ref Attr(Aquery, nil, nil) :: ref Attr(Aquery, nil, nil) :: nl);	# add a few blank
+		if(attrs != nil && tag != nil && tag.val != nil){
+			needs <-= attrs;
+			<-acks;
+			sys->fprint(fd, "tag=%d", int tag.val);
+		}
+	}
+	if(n < 0)
+		sys->fprint(sys->fildes(2), "feedkey: error reading needkey: %r\n");
+	needs <-= nil;
+}
+
+# need a library module
+
+Aattr, Aval, Aquery: con iota;
+
+Attr: adt {
+	tag:	int;
+	name:	string;
+	val:	string;
+
+	text:	fn(a: self ref Attr): string;
+};
+
+parseline(s: string): list of ref Attr
+{
+	fld := str->unquoted(s);
+	rfld := fld;
+	for(fld = nil; rfld != nil; rfld = tl rfld)
+		fld = (hd rfld) :: fld;
+	attrs: list of ref Attr;
+	for(; fld != nil; fld = tl fld){
+		n := hd fld;
+		a := "";
+		tag := Aattr;
+		for(i:=0; i<len n; i++)
+			if(n[i] == '='){
+				a = n[i+1:];
+				n = n[0:i];
+				tag = Aval;
+			}
+		if(len n == 0)
+			continue;
+		if(tag == Aattr && len n > 1 && n[len n-1] == '?'){
+			tag = Aquery;
+			n = n[0:len n-1];
+		}
+		attrs = ref Attr(tag, n, a) :: attrs;
+	}
+	return attrs;
+}
+
+Attr.text(a: self ref Attr): string
+{
+	case a.tag {
+	Aattr =>
+		return a.name;
+	Aval =>
+		return sys->sprint("%q=%q", a.name, a.val);
+	Aquery =>
+		return a.name+"?";
+	* =>
+		return "??";
+	}
+}
+
+attrtext(attrs: list of ref Attr): string
+{
+	s := "";
+	sp := 0;
+	for(; attrs != nil; attrs = tl attrs){
+		if(sp)
+			s[len s] = ' ';
+		sp = 1;
+		s += (hd attrs).text();
+	}
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/mkfile
@@ -1,0 +1,27 @@
+<../../../../mkconfig
+
+DIRS=\
+	proto\
+
+TARG=\
+	factotum.dis\
+	feedkey.dis\
+	rpc.dis\
+
+SYSMODULES=\
+	arg.m\
+	keyring.m\
+	security.m\
+	rand.m\
+	sys.m\
+	draw.m\
+	bufio.m\
+	string.m\
+
+MODULES=\
+	authio.m\
+
+DISBIN=$ROOT/dis/auth
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/cmd/auth/factotum/proto/authquery.b
@@ -1,0 +1,204 @@
+implement Authproto;
+include "sys.m";
+	sys: Sys;
+	Rread, Rwrite: import Sys;
+include "draw.m";
+include "keyring.m";
+include "bufio.m";
+include "sexprs.m";
+	sexprs: Sexprs;
+	Sexp: import sexprs;
+include "spki.m";
+	spki: SPKI;
+include "../authio.m";
+	authio: Authio;
+	Aattr, Aval, Aquery: import Authio;
+	Attr, IO, Key, Authinfo: import authio;
+
+# queries to handle:
+# are you a member of group X?
+# are you group leader of group X?
+
+Debug: con 0;
+
+# init, addkey, closekey, write, read, close, keyprompt
+
+Query: adt {
+	e: ref Sexp;
+	certs: list of ref Sexp;
+	gotcerts: list of ref Sexp;
+
+	parse: fn(se: ref Sexp): (ref Query, string);
+	neededcert: fn(q: self ref Query): ref Sexp;
+	addcert: fn(q: self ref Query, cert: ref Sexp): string;
+	result: fn(q: self ref Query): ref Sexp;
+};
+
+Read: adt {
+	buf: array of byte;
+	ptr: int;
+	off: int;
+	io: ref IO;
+
+	new: fn(io: ref IO): ref Read;
+	getb: fn(r: self ref Read): int;
+	ungetb: fn(r: self ref Read): int;
+	offset: fn(r: self ref Read): big;
+};
+
+
+Maxmsg: con 4000;
+
+init(f: Authio): string
+{
+	authio = f;
+	sys = load Sys Sys->PATH;
+	spki = load SPKI SPKI->PATH;
+	spki->init();
+	sexprs = load Sexprs Sexprs->PATH;
+	sexprs->init();
+	return nil;
+}
+
+interaction(attrs: list of ref Attr, io: ref IO): string
+{
+	case authio->lookattrval(attrs, "role") {
+	"client" =>
+		return client(attrs, io);
+	"server" =>
+		return server(attrs, io);
+	* =>
+		return "unknown role";
+	}
+}
+
+client(attrs: list of ref Attr, io: ref IO): string
+{
+	(sexp, nil, err) := Sexp.parse(authio->lookattrval(attrs, "query"));
+	if(sexp == nil || err != nil)
+		raise sys->sprint("bad or empty query %q: %s", authio->lookattrval(attrs, "query"), err);
+	for(;;){
+		write(io, sexp.pack());
+		(sexp, err) = Sexp.read(Read.new(io));
+		if(err != nil)
+			return "authquery: bad query: "+err;
+		if(sexp == nil)
+			return "authquery: no result";
+		if(sexp.op() != "needcert"){
+			io.done(ref Authinfo(nil, nil, nil, sexp.pack()));	# XXX use something other than secret
+			return nil;
+		}
+		(sexp, err) = needcert(io, sexp);
+		if(sexp == nil)
+			return "authquery: no cert: "+err;
+	}
+}
+
+server(nil: list of ref Attr, io: ref IO): string
+{
+	(sexp, err) := Sexp.read(Read.new(io));
+	if(err != nil)
+		return "authquery: bad query sexp: "+err;
+	q: ref Query;
+	(q, err) = Query.parse(sexp);
+	if(q == nil)
+		return "authquery: bad query: "+err;
+	while((sexp = q.neededcert()) != nil){
+		write(io, sexp.pack());
+		(sexp, err) = Sexp.read(Read.new(io));
+		if(err != nil)
+			return "authquery: bad cert sexp: "+err;
+		if((err = q.addcert(sexp)) != nil)
+			return "authquery: bad certificate received: "+err;
+	}
+	write(io,  q.result().pack());
+	io.done(ref Authinfo);
+	return nil;
+}
+
+mkop(op: string, els: list of ref Sexp): ref Sexp
+{
+	return ref Sexp.List(ref Sexp.String(op, nil) :: els);
+}
+
+needcert(nil: ref IO, se: ref Sexp): (ref Sexp, string)
+{
+	return (mkop("cert", se :: nil), nil);
+#	(key, err) := io.findkey(
+}
+
+write(io: ref IO, buf: array of byte)
+{
+	while(len buf > Maxmsg){
+		io.write(buf[0:Maxmsg], Maxmsg);
+		buf = buf[Maxmsg:];
+	}
+	io.write(buf, len buf);
+}
+
+Query.parse(sexp: ref Sexp): (ref Query, string)
+{
+	if(!sexp.islist())
+		return (nil, "query must be a list");
+	return (ref Query(sexp, sexp.els(), nil), nil);
+}
+
+Query.neededcert(q: self ref Query): ref Sexp
+{
+	if(q.certs == nil)
+		return nil;
+	c := hd q.certs;
+	q.certs = tl q.certs;
+	if(c.op() != "cert")
+		return nil;
+	for(a := c.args(); a != nil; a = tl a)
+		if((hd a).op() == "delay" && (hd a).args() != nil)
+			sys->sleep(int (hd (hd a).args()).astext());
+	return mkop("needcert", c :: nil);
+}
+
+Query.addcert(q: self ref Query, cert: ref Sexp): string
+{
+	q.gotcerts = cert :: q.gotcerts;
+	return nil;
+}
+
+Query.result(q: self ref Query): ref Sexp
+{
+	return mkop("result", q.gotcerts);
+}
+
+Read.new(io: ref IO): ref Read
+{
+	return ref Read(nil, 0, 0, io);
+}
+
+Read.getb(r: self ref Read): int
+{
+	if(r.ptr >= len r.buf){
+		while((buf := r.io.read()) == nil || len buf == 0)
+			r.io.toosmall(Maxmsg);
+		r.buf = buf;
+		r.ptr = 0;
+	}
+	r.off++;
+	return int r.buf[r.ptr++];
+}
+
+Read.ungetb(r: self ref Read): int
+{
+	if(r.buf == nil || r.ptr == 0)
+		return -1;
+	r.off--;
+	return int r.buf[--r.ptr];
+}
+
+Read.offset(r: self ref Read): big
+{
+	return big r.off;
+}
+
+keycheck(nil: ref Authio->Key): string
+{
+	return nil;
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/proto/infauth.b
@@ -1,0 +1,431 @@
+implement Authproto;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "keyring.m";
+	keyring: Keyring;
+	IPint: import keyring;
+	SK, PK, Certificate, DigestState: import Keyring;
+include "security.m";
+include "bufio.m";
+include "sexprs.m";
+	sexprs: Sexprs;
+	Sexp: import sexprs;
+include "spki.m";
+	spki: SPKI;
+include "daytime.m";
+	daytime: Daytime;
+include "keyreps.m";
+	keyreps: Keyreps;
+	Keyrep: import keyreps;
+include "../authio.m";
+	authio: Authio;
+	Aattr, Aval, Aquery: import Authio;
+	Attr, IO, Key, Authinfo: import authio;
+
+# at end of authentication, sign a hash of the authenticated username and
+# a secret known only to factotum. that certificate can act as
+# a later proof that this factotum has authenticated that user,
+# and hence factotum will disclose certificates that allow disclosure
+# only to that username.
+
+Debug: con 0;
+
+Maxmsg: con 4000;
+
+Error0, Error1: exception(string);
+
+init(f: Authio): string
+{
+	authio = f;
+	sys = load Sys Sys->PATH;
+	spki = load SPKI SPKI->PATH;
+	spki->init();
+	sexprs = load Sexprs Sexprs->PATH;
+	sexprs->init();
+	keyring = load Keyring Keyring->PATH;
+	daytime = load Daytime Daytime->PATH;
+	keyreps = load Keyreps Keyreps->PATH;
+	keyreps->init();
+	return nil;
+}
+
+interaction(attrs: list of ref Attr, io: ref IO): string
+{
+	ai: ref Authinfo;
+	(key, err) := io.findkey(attrs, "proto=infauth");
+	if(key == nil)
+		return err;
+	info: ref Keyring->Authinfo;
+	(info, err) = keytoauthinfo(key);
+	if(info == nil)
+		return err;
+	anysigner := int authio->lookattrval(key.attrs, "anysigner");
+	rattrs: list of ref Sexp;
+	{
+		# send auth protocol version number
+		sendmsg(io, array of byte "1");
+
+		# get auth protocol version number
+		if(int string getmsg(io) != 1)
+			raise Error0("incompatible authentication protocol");
+
+		# generate alpha**r0
+		p := info.p;
+		low := p.shr(p.bits()/4);
+		r0 := rand(low, p, Random->NotQuiteRandom);
+		αr0 := info.alpha.expmod(r0, p);
+		# trim(αr0);	the IPint library should do this for us, i think.
+
+		# send alpha**r0 mod p, mycert, and mypk
+		sendmsg(io, array of byte αr0.iptob64());
+		sendmsg(io, array of byte keyring->certtostr(info.cert));
+		sendmsg(io, array of byte keyring->pktostr(info.mypk));
+
+		# get alpha**r1 mod p, hiscert, hispk
+		αr1 := IPint.b64toip(string getmsg(io));
+
+		# trying a fast one
+		if(p.cmp(αr1) <= 0)
+			raise Error0("implausible parameter value");
+
+		# if alpha**r1 == alpha**r0, someone may be trying a replay
+		if(αr0.eq(αr1))
+			raise Error0("possible replay attack");
+
+		hiscert := keyring->strtocert(string getmsg(io));
+		if(hiscert == nil && !anysigner)
+			raise Error0(sys->sprint("bad certificate: %r"));
+
+		buf := getmsg(io);
+		hispk := keyring->strtopk(string buf);
+		if(!anysigner){
+			# verify their public key
+			if(verify(info.spk, hiscert, buf) == 0)
+				raise Error0("pk doesn't match certificate");	# likely the signers don't match.
+
+			# check expiration date - in seconds of epoch
+			if(hiscert.exp != 0 && hiscert.exp <= now())
+				raise Error0("certificate expired");
+		}
+		buf = nil;
+
+		# sign alpha**r0 and alpha**r1 and send
+		αcert := sign(info.mysk, "sha", 0, array of byte (αr0.iptob64() + αr1.iptob64()));
+		sendmsg(io, array of byte keyring->certtostr(αcert));
+
+		# get signature of alpha**r1 and alpha**r0 and verify
+		αcert = keyring->strtocert(string getmsg(io));
+		if(αcert == nil)
+			raise Error0("alpha**r1 doesn't match certificate");
+
+		if(verify(hispk, αcert, array of byte (αr1.iptob64() + αr0.iptob64())) == 0)
+			raise Error0(sys->sprint("bad certificate: %r"));
+
+		ai = ref Authinfo;
+		# we are now authenticated and have a common secret, alpha**(r0*r1)
+		if(!anysigner)
+			rattrs = sl(ss("signer") :: principal(info.spk) :: nil) :: rattrs;
+		rattrs = sl(ss("remote-pk") :: principal(hispk) :: nil) :: rattrs;
+		rattrs = sl(ss("local-pk") :: principal(info.mypk) :: nil) :: rattrs;
+		rattrs = sl(ss("secret") :: sb(αr1.expmod(r0, p).iptobytes()) :: nil) :: rattrs;
+		ai.suid = hispk.owner;
+		ai.cuid = info.mypk.owner;
+		sendmsg(io, array of byte "OK");
+	}exception e{
+	Error0 =>
+		err = e;
+		senderr(io, e);
+		break;
+	Error1 =>
+		senderr(io, "failed");	# acknowledge error
+		return remote(e);
+	}
+
+	{	
+		while(string getmsg(io) != "OK")
+			;
+	}exception e{
+	Error0 =>
+		return e;
+	Error1 =>
+		return remote(e);
+	}
+	if(err != nil)
+		return err;
+
+	return negotiatecrypto(io, key, ai, rattrs);
+}
+
+remote(s: string): string
+{
+	# account for strange earlier interface
+	if(len s < 6 || s[0: 6] != "remote")
+		return "remote: "+s;
+	return s;
+}
+
+# TO DO: exchange attr/value pairs, covered by hmac (use part of secret up to hmac block size of 64 bytes)
+# the old scheme can be distinguished either by a prefix "attrs " or simply because the string contains "=",
+# and the server side can then reply.  the hmac is to prevent tampering.
+negotiatecrypto(io: ref IO, key: ref Key, ai: ref Authinfo, attrs: list of ref Sexp): string
+{
+	role := authio->lookattrval(key.attrs, "role");
+	alg: string;
+	{
+		if(role == "client"){
+			alg = authio->lookattrval(key.attrs, ":alg");
+			if(alg == nil)
+				alg = authio->lookattrval(key.attrs, "alg");	# old way
+			if(alg == nil)
+				alg = "md5/rc4_256";
+			sendmsg(io, array of byte alg);
+		}else if(role == "server"){
+			alg = string getmsg(io);
+			if(!algcompatible(alg, sys->tokenize(authio->lookattrval(key.attrs, "algs"), " ").t1))
+				raise Error0("unsupported client algorithm");
+		}
+	}exception e{
+	Error0 or
+	Error1 =>
+		return e;
+	}
+
+	if(alg != nil)
+		attrs = sl(ss("alg") :: ss(alg) :: nil) :: attrs;
+	ai.secret = sl(attrs).pack();
+	if(role == "server")
+		ai.cap = capability(nil, ai.suid);
+
+	io.done(ai);
+	return nil;
+}
+
+capability(ufrom, uto: string): string
+{
+	capfd := sys->open("#¤/caphash", Sys->OWRITE);
+	if(capfd == nil)
+		return nil;
+	key := IPint.random(0, 160).iptob64();
+	if(key == nil)
+		return nil;
+
+	users := uto;
+	if(ufrom != nil)
+		users = ufrom+"@"+uto;
+	digest := array[Keyring->SHA1dlen] of byte;
+	ausers := array of byte users;
+	keyring->hmac_sha1(ausers, len ausers, array of byte key, digest, nil);
+	if(sys->write(capfd, digest, len digest) < 0)
+		return nil;
+	return users+"@"+key;
+}
+
+algcompatible(nil: string, nil: list of string): int
+{
+	return 1;	# XXX
+}
+
+principal(pk: ref Keyring->PK): ref Sexp
+{
+	return spki->(Keyrep.pk(pk).mkkey()).sexp();
+}
+
+ipint(i: int): ref IPint
+{
+	return IPint.inttoip(i);
+}
+
+rand(p, q: ref IPint, nil: int): ref IPint
+{
+	if(p.cmp(q) > 0)
+		(p, q) = (q, p);
+	diff := q.sub(p);
+	q = nil;
+	if(diff.cmp(ipint(2)) < 0){
+		sys->print("rand range must be at least 2");
+		return IPint.inttoip(0);
+	}
+	l := diff.bits();
+	T := ipint(1).shl(l);
+	l = ((l + 7) / 8) * 8;
+	slop := T.div(diff).t1;
+	r: ref IPint;
+	do{
+		r = IPint.random(0, l);
+	}while(r.cmp(slop) < 0);
+	r = r.div(diff).t1.add(p);
+	return r;
+}
+
+now(): int
+{
+	return daytime->now();
+}
+
+Hashfn: type ref fn(a: array of byte, alen: int, digest: array of byte, state: ref DigestState): ref DigestState;
+
+hashalg(ha: string): Hashfn
+{
+	case ha {
+	"sha" or
+	"sha1" =>
+		return keyring->sha1;
+	"md4" =>
+		return keyring->md4;
+	"md5" =>
+		return keyring->md5;
+	}
+	return nil;
+}
+
+sign(sk: ref SK, ha: string, exp: int, buf: array of byte): ref Certificate
+{
+	state := hashalg(ha)(buf, len buf, nil, nil);
+	return keyring->sign(sk, exp, state, ha);
+}
+
+verify(pk: ref PK, cert: ref Certificate, buf: array of byte): int
+{
+	state := hashalg(cert.ha)(buf, len buf, nil, nil);
+	return keyring->verify(pk, cert, state);
+}
+
+getmsg(io: ref IO): array of byte raises (Error0, Error1)
+{
+	while((buf := io.read()) == nil || (n := len buf) < 5)
+		io.toosmall(5);
+	if(len buf != 5)
+		raise Error0("io error: (impossible?) msg length " + string n);
+	h := string buf;
+	if(h[0] == '!')
+		m := int h[1:];
+	else
+		m = int h;
+	while((buf = io.read()) == nil || (n = len buf) < m)
+		io.toosmall(m);
+	if(len buf != m)
+		raise Error0("io error: (impossible?) msg length " + string m);
+	if(h[0] == '!'){
+		if(0)
+			sys->print("got remote error: %q, len %d\n", string buf, len string buf);
+		raise Error1(string buf);
+	}
+	return buf;
+}
+
+sendmsg(io: ref IO, buf: array of byte)
+{
+	h := sys->aprint("%4.4d\n", len buf);
+	io.write(h, len h);
+	io.write(buf, len buf);
+}
+
+senderr(io: ref IO, e: string)
+{
+	buf := array of byte e;
+	h := sys->aprint("!%3.3d\n", len buf);
+	io.write(h, len h);
+	io.write(buf, len buf);
+}
+
+# both the s-expression and k=v form are interim, until all
+# the factotum implementations can manage public keys
+# the s-expression form was the original one used by Inferno factotum
+# the form in which Authinfo components are separate attributes is the
+# one now used by Plan 9 and Plan 9 Ports factotum implementations
+keytoauthinfo(key:ref Key): (ref Keyring->Authinfo, string)
+{
+	if((s := authio->lookattrval(key.secrets, "!authinfo")) != nil)
+		return strtoauthinfo(s);
+	# TO DO: could look up authinfo by hash
+	ai := ref Keyring->Authinfo;
+	if((s = kv(key.secrets, "!sk")) == nil || (ai.mysk = keyring->strtosk(s)) == nil)
+		return (nil, "bad secret key");
+	if((s = kv(key.attrs, "pk")) == nil || (ai.mypk = keyring->strtopk(s)) == nil)
+		return (nil, "bad public key");
+	if((s = kv(key.attrs, "cert")) == nil || (ai.cert = keyring->strtocert(s)) == nil)
+		return (nil, "bad certificate");
+	if((s = kv(key.attrs, "spk")) == nil || (ai.spk = keyring->strtopk(s)) == nil)
+		return (nil, "bad signer public key");
+	if((s = kv(key.attrs, "dh-alpha")) == nil || (ai.alpha = IPint.strtoip(s, 16)) == nil)
+		return (nil, "bad value for alpha");
+	if((s = kv(key.attrs, "dh-p")) == nil || (ai.p = IPint.strtoip(s, 16)) == nil)
+		return (nil, "bad value for p");
+	return (ai, nil);
+}
+
+kv(a: list of ref Attr, name: string): string
+{
+	return rnl(authio->lookattrval(a, name));
+}
+
+rnl(s: string): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '^')
+			s[i] = '\n';
+	return s;
+}
+
+# s-expression form
+strtoauthinfo(s: string): (ref Keyring->Authinfo, string)
+{
+	(se, err, nil) := Sexp.parse(s);
+	if(se == nil)
+		return (nil, err);
+	els := se.els();
+	if(len els != 5)
+		return (nil, "bad authinfo contents");
+	ai := ref Keyring->Authinfo;
+	if((ai.spk = keyring->strtopk((hd els).astext())) == nil)
+		return (nil, "bad signer public key");
+	els = tl els;
+	if((ai.cert = keyring->strtocert((hd els).astext())) == nil)
+		return (nil, "bad certificate");
+	els = tl els;
+	if((ai.mysk = keyring->strtosk((hd els).astext())) == nil)
+		return (nil, "bad secret/public key");
+	if((ai.mypk = keyring->sktopk(ai.mysk)) == nil)
+		return (nil, "cannot make pk from sk");
+	els = tl els;
+	if((ai.alpha = IPint.bytestoip((hd els).asdata())) == nil)
+		return (nil, "bad value for alpha");
+	els = tl els;
+	if((ai.p = IPint.bytestoip((hd els).asdata())) == nil)
+		return (nil, "bad value for p");
+	return (ai, nil);
+}
+	
+authinfotostr(ai: ref Keyring->Authinfo): string
+{
+	return (ref Sexp.List(
+		ss(keyring->pktostr(ai.spk)) ::
+		ss(keyring->certtostr(ai.cert)) ::
+		ss(keyring->sktostr(ai.mysk)) ::
+		sb(ai.alpha.iptobytes()) ::
+		sb(ai.p.iptobytes()) ::
+		nil
+	)).b64text();
+}
+
+ss(s: string): ref Sexp.String
+{
+	return ref Sexp.String(s, nil);
+}
+
+sb(d: array of byte): ref Sexp.Binary
+{
+	return ref Sexp.Binary(d, nil);
+}
+
+sl(l: list of ref Sexp): ref Sexp
+{
+	return ref Sexp.List(l);
+}
+
+keycheck(nil: ref Authio->Key): string
+{
+	return nil;
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/proto/keyreps.b
@@ -1,0 +1,173 @@
+implement Keyreps;
+include "sys.m";
+	sys: Sys;
+include "keyring.m";
+	kr: Keyring;
+	IPint: import kr;
+include "sexprs.m";
+include "spki.m";
+include "encoding.m";
+	base64: Encoding;
+include "keyreps.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	base64 = load Encoding Encoding->BASE64PATH;
+}
+
+keyextract(flds: list of string, names: list of (string, int)): list of (string, ref IPint)
+{
+	a := array[len flds] of ref IPint;
+	for(i := 0; i < len a; i++){
+		a[i] = IPint.b64toip(hd flds);
+		flds = tl flds;
+	}
+	rl: list of (string, ref IPint);
+	for(; names != nil; names = tl names){
+		(n, p) := hd names;
+		if(p < len a)
+			rl = (n, a[p]) :: rl;
+	}
+	return revt(rl);
+}
+
+Keyrep.pk(pk: ref Keyring->PK): ref Keyrep.PK
+{
+	s := kr->pktostr(pk);
+	(nf, flds) := sys->tokenize(s, "\n");
+	if((nf -= 2) < 0)
+		return nil;
+	case hd flds {
+	"rsa" =>
+		return ref Keyrep.PK(hd flds, hd tl flds,
+			keyextract(tl tl flds, list of {("e",1), ("n",0)}));
+	"elgamal" or "dsa" =>
+		return ref Keyrep.PK(hd flds, hd tl flds,
+			keyextract(tl tl flds, list of {("p",0), ("alpha",1), ("key",2)}));
+	* =>
+		return nil;
+	}
+}
+
+Keyrep.sk(pk: ref Keyring->SK): ref Keyrep.SK
+{
+	s := kr->pktostr(pk);
+	(nf, flds) := sys->tokenize(s, "\n");
+	if((nf -= 2) < 0)
+		return nil;
+	case hd flds {
+	"rsa" =>
+		return ref Keyrep.SK(hd flds, hd tl flds,
+			keyextract(tl tl flds,list of {("e",1), ("n",0), ("!dk",2), ("!p",3), ("!q",4), ("!kp",5), ("!kq",6), ("!c2",7)}));
+	"elgamal" or "dsa" =>
+		return ref Keyrep.SK(hd flds, hd tl flds,
+			keyextract(tl tl flds, list of {("p",0), ("alpha",1), ("key",2), ("!secret",3)}));
+	* =>
+		return nil;
+	}
+}
+
+Keyrep.get(k: self ref Keyrep, n: string): ref IPint
+{
+	for(el := k.els; el != nil; el = tl el)
+		if((hd el).t0 == n)
+			return (hd el).t1;
+	return nil;
+}
+
+Keyrep.getb(k: self ref Keyrep, n: string): array of byte
+{
+	v := k.get(n);
+	if(v == nil)
+		return nil;
+	return pre0(v.iptobebytes());
+}
+
+pre0(a: array of byte): array of byte
+{
+	for(i:=0; i<len a-1; i++)
+		if(a[i] != a[i+1] && (a[i] != byte 0 || (int a[i+1] & 16r80) != 0))
+			break;
+	if(i > 0)
+		a = a[i:];
+	if(len a < 1 || (int a[0] & 16r80) == 0)
+		return a;
+	b := array[len a + 1] of byte;
+	b[0] = byte 0;
+	b[1:] = a;
+	return b;
+}
+
+Keyrep.mkpk(k: self ref Keyrep): (ref Keyring->PK, int)
+{
+	case k.alg {
+	"rsa" =>
+		e := k.get("e");
+		n := k.get("n");
+		return (kr->strtopk(sys->sprint("rsa\n%s\n%s\n%s\n", k.owner, n.iptob64(), e.iptob64())), n.bits());
+	* =>
+		raise "Keyrep: unknown algorithm" + k.alg;
+	}
+}
+
+Keyrep.mksk(k: self ref Keyrep): ref Keyring->SK
+{
+	case k.alg {
+	"rsa" =>
+		e := k.get("e");
+		n := k.get("n");
+		dk := k.get("!dk");
+		p := k.get("!p");
+		q := k.get("!q");
+		kp := k.get("!kp");
+		kq := k.get("!kq");
+		c12 := k.get("!c2");
+		return kr->strtosk(sys->sprint("rsa\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n",
+			k.owner, n.iptob64(), e.iptob64(), dk.iptob64(), p.iptob64(), q.iptob64(),
+			kp.iptob64(), kq.iptob64(), c12.iptob64()));
+	* =>
+		raise "Keyrep: unknown algorithm";
+	}
+}
+
+Keyrep.eq(k1: self ref Keyrep, k2: ref Keyrep): int
+{
+	# n⁲ but n is small
+	for(l1 := k1.els; l1 != nil; l1 = tl l1){
+		(n, v1) := hd l1;
+		v2 := k2.get(n);
+		if(v2 == nil || !v1.eq(v2))
+			return 0;
+	}
+	for(l2 := k2.els; l2 != nil; l2 = tl l2)
+		if(k1.get((hd l2).t0) == nil)
+			return 0;
+	return 1;
+}
+
+Keyrep.mkkey(kr: self ref Keyrep): ref SPKI->Key
+{
+	k := ref SPKI->Key;
+	(k.pk, k.nbits) = kr.mkpk();
+	k.sk = kr.mksk();
+	return k;
+}
+
+sig2icert(sig: ref SPKI->Signature, signer: string, exp: int): ref Keyring->Certificate
+{
+	if(sig.sig == nil)
+		return nil;
+	s := sys->sprint("%s\n%s\n%s\n%d\n%s\n", "rsa", sig.hash.alg, signer, exp, base64->enc((hd sig.sig).t1));
+#sys->print("alg %s *** %s\n", sig.sa, base64->enc((hd sig.sig).t1));
+	return kr->strtocert(s);
+}
+
+revt[S,T](l: list of (S,T)): list of (S,T)
+{
+	rl: list of (S,T);
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/proto/keyreps.m
@@ -1,0 +1,23 @@
+Keyreps: module
+{
+	PATH: con "/dis/lib/spki/keyreps.dis";
+	init: fn();
+	Keyrep: adt {
+		alg:	string;
+		owner:	string;
+		els:	list of (string, ref Keyring->IPint);
+		pick{	# keeps a type distance between public and private keys
+		PK =>
+		SK =>
+		}
+
+		pk:	fn(pk: ref Keyring->PK): ref Keyrep.PK;
+		sk:	fn(sk: ref Keyring->SK): ref Keyrep.SK;
+		mkpk:	fn(k: self ref Keyrep): (ref Keyring->PK, int);
+		mksk:	fn(k: self ref Keyrep): ref Keyring->SK;
+		get:	fn(k: self ref Keyrep, n: string): ref Keyring->IPint;
+		getb:	fn(k: self ref Keyrep, n: string): array of byte;
+		eq:	fn(k1: self ref Keyrep, k2: ref Keyrep): int;
+		mkkey: fn(k: self ref Keyrep): ref SPKI->Key;
+	};
+};
--- /dev/null
+++ b/appl/cmd/auth/factotum/proto/mkfile
@@ -1,0 +1,24 @@
+<../../../../../mkconfig
+
+TARG=\
+	authquery.dis\
+	p9any.dis\
+	pass.dis\
+	rsa.dis\
+
+SYSMODULES=\
+	factotum.m\
+	keyring.m\
+	security.m\
+	rand.m\
+	sys.m\
+	draw.m\
+	bufio.m\
+	string.m\
+
+MODULES=\
+	../authio.m\
+
+DISBIN=$ROOT/dis/auth/proto
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/auth/factotum/proto/p9any.b
@@ -1,0 +1,237 @@
+implement Authproto;
+
+# currently includes p9sk1
+
+include "sys.m";
+	sys: Sys;
+	Rread, Rwrite: import Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+
+include "auth9.m";
+	auth9: Auth9;
+	ANAMELEN, AERRLEN, DOMLEN, DESKEYLEN, CHALLEN, SECRETLEN: import Auth9;
+	TICKREQLEN, TICKETLEN, AUTHENTLEN: import Auth9;
+	Ticketreq, Ticket, Authenticator: import auth9;
+
+include "../authio.m";
+	authio: Authio;
+	Aattr, Aval, Aquery: import Authio;
+	Attr, IO, Key, Authinfo: import authio;
+	netmkaddr, eqbytes, memrandom: import authio;
+
+include "encoding.m";
+	base16: Encoding;
+
+Debug: con 0;
+
+# init, addkey, closekey, write, read, close, keyprompt
+
+init(f: Authio): string
+{
+	authio = f;
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	auth9 = load Auth9 Auth9->PATH;
+	auth9->init();
+	base16 = load Encoding Encoding->BASE16PATH;
+	return nil;
+}
+
+version := 1;
+
+interaction(attrs: list of ref Attr, io: ref IO): string
+{
+	return p9any(io);
+}
+
+p9any(io: ref IO): string
+{
+	while((buf := io.read()) == nil || (n := len buf) == 0 || buf[n-1] != byte 0)
+		io.toosmall(2048);
+	s := string buf[0:n-1];
+	if(Debug)
+		sys->print("s: %q\n", s);
+	(nil, flds) := sys->tokenize(s, " \t");
+	if(flds != nil && len hd flds >= 2 && (hd flds)[0:2] == "v."){
+		if(hd flds == "v.2"){
+			version = 2;
+			flds = tl flds;
+			if(Debug)
+				sys->print("version 2\n");
+		}else
+			return "p9any: unknown version";
+	}
+	doms: list of string;
+	for(; flds != nil; flds = tl flds){
+		(nf, subf) := sys->tokenize(hd flds, "@");
+		if(nf == 2 && hd subf == "p9sk1")
+			doms = hd tl subf :: doms;
+	}
+	if(doms == nil)
+		return "p9any: unsupported protocol";
+	if(Debug){
+		for(l := doms; l != nil; l = tl l)
+			sys->print("dom: %q\n", hd l);
+	}
+	r := array of byte ("p9sk1 "+hd doms);
+	buf[0:] = r;
+	buf[len r] = byte 0;
+	io.write(buf, len r + 1);
+	if(version == 2){
+		b := io.readn(3);
+		if(b == nil || b[0] != byte 'O' || b[1] != byte 'K' || b[2] != byte 0)
+			return "p9any: AS protocol botch: not OK";
+		if(Debug)
+			sys->print("OK\n");
+	}
+	return p9sk1client(io, hd doms);
+}
+
+#p9sk1:
+#	C->S:	nonce-C
+#	S->C:	nonce-S, uid-S, domain-S
+#	C->A:	nonce-S, uid-S, domain-S, uid-C, factotum-C
+#	A->C:	Kc{nonce-S, uid-C, uid-S, Kn}, Ks{nonce-S, uid-C, uid-S, K-n}
+#	C->S:	Ks{nonce-S, uid-C, uid-S, K-n}, Kn{nonce-S, counter}
+#	S->C:	Kn{nonce-C, counter}
+
+#asserts that uid-S and uid-C share new secret Kn
+#increment the counter to reuse the ticket.
+
+p9sk1client(io: ref IO, udom: string): string
+{
+
+	#	C->S:	nonce-C
+	cchal := array[CHALLEN] of byte;
+	memrandom(cchal, CHALLEN);
+	if(io.write(cchal, len cchal) != len cchal)
+		return sys->sprint("p9sk1: can't write cchal: %r");
+
+	#	S->C:	nonce-S, uid-S, domain-S
+	trbuf := io.readn(TICKREQLEN);
+	if(trbuf == nil)
+		return sys->sprint("p9sk1: can't read ticketreq: %r");
+
+	(nil, tr) := Ticketreq.unpack(trbuf);
+	if(tr == nil)
+		return "p9sk1: can't unpack ticket request";
+	if(Debug)
+		sys->print("ticketreq: type=%d authid=%q authdom=%q chal= hostid=%q uid=%q\n",
+			tr.rtype, tr.authid, tr.authdom, tr.hostid, tr.uid);
+
+	(mykey, diag) := io.findkey(nil, sys->sprint("dom=%q proto=p9sk1 user? !password?", udom));
+	if(mykey == nil)
+		return "can't find key: "+diag;
+	ukey: array of byte;
+	if((a := authio->lookattrval(mykey.secrets, "!hex")) != nil){
+		ukey = base16->dec(a);
+		if(len ukey != DESKEYLEN)
+			return "p9sk1: invalid !hex key";
+	}else	if((a = authio->lookattrval(mykey.secrets, "!password")) != nil)
+		ukey = auth9->passtokey(a);
+	else
+		return "no !password (or !hex) in key";
+
+	#	A->C:	Kc{nonce-S, uid-C, uid-S, Kn}, Ks{nonce-S, uid-C, uid-S, K-n}
+	user := authio->lookattrval(mykey.attrs, "user");
+	if(user == nil)
+		user = authio->user();	# shouldn't happen
+	tr.rtype = Auth9->AuthTreq;
+	tr.hostid = user;
+	tr.uid = tr.hostid;	# not speaking for anyone else
+	(tick, serverbits) := getastickets(tr, ukey);
+	if(tick == nil)
+		return sys->sprint("p9sk1: getasticket failed: %r");
+	if(tick.num != Auth9->AuthTc)
+		return "p9sk1: getasticket: failed: wrong key?";
+	if(Debug)
+		sys->print("ticket: num=%d chal= cuid=%q suid=%q key=\n", tick.num, tick.cuid, tick.suid);
+
+	#	C->S:	Ks{nonce-S, uid-C, uid-S, K-n}, Kn{nonce-S, counter}
+	ar := ref Authenticator;
+	ar.num = Auth9->AuthAc;
+	ar.chal = tick.chal;
+	ar.id = 0;
+	obuf := array[TICKETLEN+AUTHENTLEN] of byte;
+	obuf[0:] = serverbits;
+	obuf[TICKETLEN:] = ar.pack(tick.key);
+	if(io.write(obuf, len obuf) != len obuf)
+		return "p9sk1: error writing authenticator: %r";
+
+	#	S->C:	Kn{nonce-C, counter}
+	sbuf := io.readn(AUTHENTLEN);
+	if(sbuf == nil)
+		return sys->sprint("p9sk1: can't read server's authenticator: %r");
+	(nil, ar) = Authenticator.unpack(sbuf, tick.key);
+	if(ar.num != Auth9->AuthAs || !eqbytes(ar.chal, cchal) || ar.id != 0)
+		return "invalid authenticator from server";
+
+	ai := ref Authinfo(tick.cuid, tick.suid, nil, auth9->des56to64(tick.key));
+	io.done(ai);
+
+	return nil;
+}
+
+getastickets(tr: ref Ticketreq, key: array of byte): (ref Ticket, array of byte)
+{
+	afd := authdial(nil, tr.authdom);
+	if(afd == nil)
+		return (nil, nil);
+	return auth9->_asgetticket(afd, tr, key);
+}
+
+#
+# where to put the following functions?
+#
+
+csgetvalue(netroot: string, keytag: string, keyval: string, needtag: string): string
+{
+	cs := "/net/cs";
+	if(netroot != nil)
+		cs = netroot+"/cs";
+	fd := sys->open(cs, Sys->ORDWR);	# TO DO: choice of root
+	if(fd == nil)
+		return nil;
+	if(sys->fprint(fd, "!%s=%s %s=*", keytag, keyval, needtag) < 0)
+		return nil;
+	sys->seek(fd, big 0, 0);
+	buf := array[1024] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0){
+		al := authio->parseline(string buf[0:n]);	# assume the conventions match factotum's
+		for(; al != nil; al = tl al)
+			if((hd al).name == needtag)
+				return (hd al).val;
+	}
+	return nil;
+}
+
+authdial(netroot: string, dom: string): ref Sys->FD
+{
+	p: string;
+	if(dom != nil){
+		# look up an auth server in an authentication domain
+		p = csgetvalue(netroot, "authdom", dom, "auth");
+
+		# if that didn't work, just try the IP domain
+		if(p == nil)
+			p = csgetvalue(netroot, "dom", dom, "auth");
+		if(p == nil)
+			p = "$auth";	# temporary ...
+		if(p == nil){
+			sys->werrstr("no auth server found for "+dom);
+			return nil;
+		}
+	}else
+		p = "$auth";	# look for one relative to my machine
+	(nil, conn) := sys->dial(netmkaddr(p, netroot, "ticket"), nil);
+	return conn.dfd;
+}
+
+keycheck(nil: ref Authio->Key): string
+{
+	return nil;
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/proto/pass.b
@@ -1,0 +1,34 @@
+implement Authproto;
+
+include "sys.m";
+	sys: Sys;
+
+include "../authio.m";
+	authio:	Authio;
+	Attr, IO: import authio;
+
+init(f: Authio): string
+{
+	sys = load Sys Sys->PATH;
+	authio = f;
+	return nil;
+}
+
+interaction(attrs: list of ref Attr, io: ref Authio->IO): string
+{
+	(key, err) := io.findkey(attrs, "user? !password?");
+	if(key == nil)
+		return err;
+	user := authio->lookattrval(key.attrs, "user");
+	if(user == nil)
+		return "unknown user";
+	pass := authio->lookattrval(key.secrets, "!password");
+	a := sys->aprint("%q %q", user, pass);
+	io.write(a, len a);
+	return nil;
+}
+
+keycheck(nil: ref Authio->Key): string
+{
+	return nil;
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/proto/rsa.b
@@ -1,0 +1,126 @@
+implement Authproto;
+
+# SSH RSA authentication.
+#
+# Client protocol:
+#	read public key
+#		if you don't like it, read another, repeat
+#	write challenge
+#	read response
+# all numbers are hexadecimal biginits parsable with strtomp.
+#
+
+include "sys.m";
+	sys: Sys;
+	Rread, Rwrite: import Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+	IPint, RSAsk, RSApk: import kr;
+
+include "../authio.m";
+	authio: Authio;
+	Aattr, Aval, Aquery: import Authio;
+	Attr, IO, Key, Authinfo: import authio;
+	eqbytes, memrandom: import authio;
+	lookattrval: import authio;
+
+
+init(f: Authio): string
+{
+	authio = f;
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+#	base16 = load Encoding Encoding->BASE16PATH;
+	return nil;
+}
+
+interaction(attrs: list of ref Attr, io: ref IO): string
+{
+	role := lookattrval(attrs, "role");
+	if(role == nil)
+		return "role not specified";
+	if(role != "client")
+		return "only client role supported";
+	sk: ref RSAsk;
+	keys: list of ref Key;
+	err: string;
+	for(;;){
+		waitread(io);
+		(keys, err) = io.findkeys(attrs, "");
+		if(keys != nil)
+			break;
+		io.error(err);
+	}
+	for(; keys != nil; keys = tl keys){
+		(sk, err) = keytorsa(hd keys);
+		if(sk != nil){
+			r := array of byte sk.pk.n.iptostr(16);
+			while(!io.reply2read(r, len r))
+				waitread(io);
+			data := io.rdwr();
+			if(data != nil){
+				chal := IPint.strtoip(string data, 16);
+				if(chal == nil){
+					io.error("invalid challenge value");
+					continue;
+				}
+				m := sk.decrypt(chal);
+				b := array of byte m.iptostr(16);
+				io.write(b, len b);
+				io.done(nil);
+				return nil;
+			}
+		}
+	}
+	for(;;){
+		io.error("no key matches "+authio->attrtext(attrs));
+		waitread(io);
+	}
+}
+
+waitread(io: ref IO)
+{
+	while(io.rdwr() != nil)
+		io.error("no current key");
+}
+
+Badkey: exception(string);
+
+ipint(attrs: list of ref Attr, name: string): ref IPint raises Badkey
+{
+	s := lookattrval(attrs, name);
+	if(s == nil)
+		raise Badkey("missing attribute "+name);
+	m := IPint.strtoip(s, 16);
+	if(m == nil)
+		raise Badkey("invalid value for "+name);
+	return m;
+}
+
+keytorsa(k: ref Key): (ref RSAsk, string)
+{
+	sk := ref RSAsk;
+	sk.pk = ref RSApk;
+	{
+		sk.pk.ek = ipint(k.attrs, "ek");
+		sk.pk.n = ipint(k.attrs, "n");
+		sk.dk = ipint(k.secrets, "!dk");
+		sk.p = ipint(k.secrets, "!p");
+		sk.q = ipint(k.secrets, "!q");
+		sk.kp = ipint(k.secrets, "!kp");
+		sk.kq = ipint(k.secrets, "!kq");
+		sk.c2 = ipint(k.secrets, "!c2");
+	}exception e{
+	Badkey =>
+		return (nil, "rsa key "+e);
+	}
+	return (sk, nil);
+}
+
+keycheck(k: ref Authio->Key): string
+{
+	return keytorsa(k).t1;
+}
--- /dev/null
+++ b/appl/cmd/auth/factotum/rpc.b
@@ -1,0 +1,68 @@
+implement Rpcio;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+
+Rpcio: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: rpc\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		cantload(Bufio->PATH);
+
+	file := "/mnt/factotum/rpc";
+	if(len args > 1)
+		file = hd tl args;
+	rfd := sys->open(file, Sys->ORDWR);
+	if(rfd == nil){
+		sys->fprint(sys->fildes(2), "rpc: can't open %s: %r\n", file);
+		raise "fail:load";
+	}
+	f := bufio->fopen(sys->fildes(0), Sys->OREAD);
+	for(;;){
+		sys->print("> ");
+		s := f.gets('\n');
+		if(s == nil)
+			break;
+		rpc(rfd, s[0:len s-1]);
+	}
+}
+
+cantload(s: string)
+{
+	sys->fprint(sys->fildes(2), "csquery: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+rpc(f: ref Sys->FD, addr: string)
+{
+	b := array of byte addr;
+	if(sys->write(f, b, len b) > 0){
+		sys->seek(f, big 0, Sys->SEEKSTART);
+		buf := array[4096+3] of byte;
+		if((n := sys->read(f, buf, len buf)) > 0)
+			sys->print("%s\n", string buf[0:n]);
+		if(n >= 0)
+			return;
+	}
+	sys->print("!%r\n");
+}
--- /dev/null
+++ b/appl/cmd/auth/getpk.b
@@ -1,0 +1,83 @@
+implement Getpk;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+include "keyring.m";
+	keyring: Keyring;
+
+Getpk: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+badmodule(p: string)
+{
+	sys->fprint(sys->fildes(2), "getpk: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	if(keyring == nil)
+		badmodule(Keyring->PATH);
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		badmodule(Arg->PATH);
+	arg->init(argv);
+	arg->setusage("usage: getpk [-asu] file...");
+	aflag := 0;
+	sflag := 0;
+	uflag := 0;
+	while((opt := arg->opt()) != 0){
+		case opt {
+		's' =>
+			sflag++;
+		'a' =>
+			aflag++;
+		'u' =>
+			uflag++;
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	if(argv == nil)
+		arg->usage();
+	multi := len argv > 1;
+	for(; argv != nil; argv = tl argv){
+		info := keyring->readauthinfo(hd argv);
+		if(info == nil){
+			sys->fprint(sys->fildes(2), "getpk: cannot read %s: %r\n", hd argv);
+			continue;
+		}
+		pk := info.mypk;
+		if(sflag)
+			pk = info.spk;
+		s := keyring->pktostr(pk);
+		if(!aflag)
+			s = hex(hash(s));
+		if(multi)
+			s = hd argv + ": " + s;
+		if(uflag)
+			s += " " + pk.owner;
+		sys->print("%s\n", s);
+	}
+}
+
+hash(s: string): array of byte
+{
+	d := array of byte s;
+	digest := array[Keyring->SHA1dlen] of byte;
+	keyring->sha1(d, len d, digest, nil);
+	return digest;
+}
+
+hex(a: array of byte): string
+{
+	s := "";
+	for(i := 0; i < len a; i++)
+		s += sys->sprint("%2.2ux", int a[i]);
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/auth/keyfs.b
@@ -1,0 +1,806 @@
+implement Keyfs;
+
+#
+# Copyright © 2002,2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+	Qid: import Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+	AESbsize, AESstate: import kr;
+
+include "rand.m";
+	rand: Rand;
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+include "styxservers.m";
+	styxservers: Styxservers;
+	Fid, Styxserver, Navigator, Navop: import styxservers;
+	Enotfound, Eperm, Ebadarg, Edot: import styxservers;
+
+include "arg.m";
+
+Keyfs: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+User: adt
+{
+	x:	int;		# table index
+	name:	string;
+	secret:	array of byte;	# eg, password hashed by SHA1
+	expire:	int;	# expiration time (epoch seconds)
+	status:	int;
+	failed:	int;	# count of failed attempts
+	path:		big;
+};
+
+Qroot, Quser, Qsecret, Qlog, Qstatus, Qexpire: con iota;
+files := array[] of {
+	(Qsecret, "secret"),
+	(Qlog, "log"),
+	(Qstatus, "status"),
+	(Qexpire, "expire")
+};
+
+Maxsecret: con 255;
+Maxname: con 255;
+Maxfail: con 50;
+users: array of ref User;
+Sok, Sdisabled: con iota;
+status := array[] of {Sok => "ok", Sdisabled => "disabled" };
+Never: con 0;	# expiry time
+
+Eremoved: con "user has been removed";
+
+pathgen := 0;
+keyversion := 0;
+user: string;
+now: int;
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "Usage: keyfs [-D] [-m mountpoint] [keyfile]\n");
+	raise "fail:usage";
+}
+
+nomod(s: string)
+{
+	sys->fprint(sys->fildes(2), "keyfs: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+	kr = load Keyring Keyring->PATH;
+	if(kr == nil)
+		nomod(Keyring->PATH);
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		nomod(Styx->PATH);
+	styxservers = load Styxservers Styxservers->PATH;
+	if(styxservers == nil)
+		nomod(Styxservers->PATH);
+	rand = load Rand Rand->PATH;
+	if(rand == nil)
+		nomod(Rand->PATH);
+
+	styx->init();
+	styxservers->init(styx);
+	rand->init(sys->millisec());
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	arg->init(args);
+	arg->setusage("keyfs [-m mntpt] [-D] [-n nvramfile] [keyfile]");
+	mountpt := "/mnt/keys";
+	keyfile := "/keydb/keys";
+	nvram: string;
+	while((o := arg->opt()) != 0)
+		case o {
+		'm' =>
+			mountpt = arg->earg();
+		'D' =>
+			styxservers->traceset(1);
+		'n' =>
+			nvram = arg->earg();
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(args != nil)
+		keyfile = hd args;
+
+	pwd, err: string;
+	if(nvram != nil){
+		pwd = rf(nvram);
+		if(pwd == nil)
+			error(sys->sprint("can't read %s: %r", nvram));
+	}
+	if(pwd == nil){
+		(pwd, err) = readconsline("Key: ", 1);
+		if(pwd == nil || err == "exit")
+			exit;
+		if(err != nil)
+			error(sys->sprint("couldn't get key: %s", err));
+		(rc, d) := sys->stat(keyfile);
+		if(rc == -1 || d.length == big 0){
+			pwd0 := pwd;
+			(pwd, err) = readconsline("Confirm key: ", 1);
+			if(pwd == nil || err == "exit")
+				exit;
+			if(pwd != pwd0)
+				error("key mismatch");
+			for(i := 0; i < len pwd0; i++)
+				pwd0[i] = ' ';	# clear it out
+		}
+	}
+
+	thekey = hashkey(pwd);
+	for(i:=0; i<len pwd; i++)
+		pwd[i] = ' ';	# clear it out
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD, nil);	# immediately avoid sharing keyfd
+
+	readkeys(keyfile);
+
+	user = rf("/dev/user");
+	if(user == nil)
+		user = "keyfs";
+
+	fds := array[2] of ref Sys->FD;
+	if(sys->pipe(fds) < 0)
+		error(sys->sprint("can't create pipe: %r"));
+
+	navops := chan of ref Navop;
+	spawn navigator(navops);
+
+	(tchan, srv) := Styxserver.new(fds[0], Navigator.new(navops), big Qroot);
+	fds[0] = nil;
+
+	pidc := chan of int;
+	spawn serveloop(tchan, srv, pidc, navops, keyfile);
+	<-pidc;
+
+	if(sys->mount(fds[1], nil, mountpt, Sys->MREPL|Sys->MCREATE, nil) < 0)
+		error(sys->sprint("mount on %s failed: %r", mountpt));
+}
+
+rf(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	b := array[256] of byte;
+	n := sys->read(fd, b, len b);
+	if(n < 0)
+		return nil;
+	return string b[0:n];
+}
+
+quit(err: string)
+{
+	fd := sys->open("/prog/"+string sys->pctl(0, nil)+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+	if(err != nil)
+		raise "fail:"+err;
+	exit;
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "keyfs: %s\n", s);
+	quit("error");
+}
+
+thekey: array of byte;
+
+hashkey(s: string): array of byte
+{
+	key := array of byte s;
+	skey := array[Keyring->SHA1dlen] of byte;
+	sha := kr->sha1(array of byte "aescbc file", 11, nil, nil);
+	kr->sha1(key, len key, skey, sha);
+	for(i:=0; i<len key; i++)
+		key[i] = byte 0;	# clear it out
+#{sys->print("HEX="); for(i:=0;i<len skey&&i<AESbsize; i++)sys->print("%.2ux", int skey[i]);sys->print("\n");}
+	return skey[0:AESbsize];
+}
+
+readconsline(prompt: string, raw: int): (string, string)
+{
+	fd := sys->open("/dev/cons", Sys->ORDWR);
+	if(fd == nil)
+		return (nil, sys->sprint("can't open cons: %r"));
+	sys->fprint(fd, "%s", prompt);
+	fdctl: ref Sys->FD;
+	if(raw){
+		fdctl = sys->open("/dev/consctl", sys->OWRITE);
+		if(fdctl == nil || sys->fprint(fdctl, "rawon") < 0)
+			return (nil, sys->sprint("can't open consctl: %r"));
+	}
+	line := array[256] of byte;
+	o := 0;
+	err: string;
+	buf := array[1] of byte;
+  Read:
+	while((r := sys->read(fd, buf, len buf)) > 0){
+		c := int buf[0];
+		case c {
+		16r7F =>
+			err = "interrupt";
+			break Read;
+		'\b' =>
+			if(o > 0)
+				o--;
+		'\n' or '\r' or 16r4 =>
+			break Read;
+		* =>
+			if(o > len line){
+				err = "line too long";
+				break Read;
+			}
+			line[o++] = byte c;
+		}
+	}
+	sys->fprint(fd, "\n");
+	if(r < 0)
+		err = sys->sprint("can't read cons: %r");
+	if(raw)
+		sys->fprint(fdctl, "rawoff");
+	if(err != nil)
+		return (nil, err);
+	return (string line[0:o], err);
+}
+
+serveloop(tchan: chan of ref Tmsg, srv: ref Styxserver, pidc: chan of int, navops: chan of ref Navop, keyfile: string)
+{
+	pidc <-= sys->pctl(Sys->FORKNS|Sys->NEWFD, 1::2::srv.fd.fd::nil);
+	while((gm := <-tchan) != nil){
+		now = time();
+		pick m := gm {
+		Readerror =>
+			error(sys->sprint("mount read error: %s", m.error));
+		Create =>
+			(c, mode, nil, err) := srv.cancreate(m);
+			if(c == nil){
+				srv.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			case TYPE(c.path) {	# parent
+			Qroot =>
+				if((m.perm & Sys->DMDIR) == 0){
+					srv.reply(ref Rmsg.Error(m.tag, Eperm));
+					break;
+				}
+				u := findusername(m.name);
+				if(u != nil){
+					srv.reply(ref Rmsg.Error(m.tag, "user already exists"));
+					continue;
+				}
+				if(len m.name > Maxname){
+					srv.reply(ref Rmsg.Error(m.tag, "user name too long"));
+					continue;
+				}
+				u = newuser(m.name, nil);
+				qid := Qid((u.path | big Quser), 0, Sys->QTDIR);
+				c.open(mode, qid);
+				writekeys(keyfile);
+				srv.reply(ref Rmsg.Create(m.tag, qid, srv.iounit()));
+			* =>
+				srv.reply(ref Rmsg.Error(m.tag, Eperm));
+				break;
+			}
+		Read =>
+			(c, err) := srv.canread(m);
+			if(c == nil){
+				srv.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			if(c.qtype & Sys->QTDIR){
+				srv.read(m);	# does readdir
+				break;
+			}
+			u := finduserpath(c.path);
+			if(u == nil){
+				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
+				break;
+			}
+			case TYPE(c.path) {
+			Qsecret =>
+				if(u.status != Sok){
+					srv.reply(ref Rmsg.Error(m.tag, "user disabled"));
+					break;
+				}
+				if(u.expire < now && u.expire != Never){
+					srv.reply(ref Rmsg.Error(m.tag, "user expired"));
+					break;
+				}
+				srv.reply(styxservers->readbytes(m, u.secret));
+			Qlog =>
+				srv.reply(styxservers->readstr(m, sys->sprint("%d", u.failed)));
+			Qstatus =>
+				s := status[u.status];
+				if(u.status == Sok && u.expire != Never && u.expire < now)
+					s = "expired";
+				srv.reply(styxservers->readstr(m, s));
+			Qexpire =>
+				s: string;
+				if(u.expire != Never)
+					s = sys->sprint("%ud", u.expire);
+				else
+					s = "never";
+				srv.reply(styxservers->readstr(m, s));
+			* =>
+				srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			}
+		Write =>
+			(c, merr) := srv.canwrite(m);
+			if(c == nil){
+				srv.reply(ref Rmsg.Error(m.tag, merr));
+				break;
+			}
+			u := finduserpath(c.path);
+			if(u == nil){
+				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
+				break;
+			}
+		    Case:
+			case TYPE(c.path) {
+			Qsecret =>
+				if(m.offset != big 0 || len m.data > Maxsecret){
+					srv.reply(ref Rmsg.Error(m.tag, "illegal write"));
+					break;
+				}
+				u.secret = m.data;
+				writekeys(keyfile);
+				srv.reply(ref Rmsg.Write(m.tag, len m.data));
+			Qexpire =>
+				s := trim(string m.data);
+				if(s != "never"){
+					if(!isnumeric(s)){
+						srv.reply(ref Rmsg.Error(m.tag, "illegal expiry time"));
+						break;
+					}
+					u.expire = int s;
+				}else
+					u.expire = Never;
+				u.failed = 0;
+				writekeys(keyfile);
+				srv.reply(ref Rmsg.Write(m.tag, len m.data));
+			Qstatus =>
+				s := trim(string m.data);
+				for(i := 0; i < len status; i++)
+					if(s == status[i]){
+						u.status = i;
+						if(i == Sok)
+							u.failed = 0;
+						writekeys(keyfile);
+						srv.reply(ref Rmsg.Write(m.tag, len m.data));
+						break Case;
+					}
+				srv.reply(ref Rmsg.Error(m.tag, "unknown status"));
+			Qlog =>
+				s := trim(string m.data);
+				if(s != "good" && s != "ok"){
+					if(++u.failed >= Maxfail)
+						u.status = Sdisabled;
+				}else
+					u.failed = 0;
+				writekeys(keyfile);
+				srv.reply(ref Rmsg.Write(m.tag, len m.data));
+			* =>
+				srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			}
+		Remove =>
+			c := srv.getfid(m.fid);
+			if(c == nil){
+				srv.remove(m);	# let it diagnose the errors
+				break;
+			}
+			case TYPE(c.path) {
+			Quser =>
+				srv.delfid(c);
+				u := finduserpath(c.path);
+				if(u == nil){
+					srv.reply(ref Rmsg.Error(m.tag, Eremoved));
+					break;
+				}
+				removeuser(u);
+				writekeys(keyfile);
+				srv.reply(ref Rmsg.Remove(m.tag));
+			Qsecret =>
+				srv.delfid(c);
+				u := finduserpath(c.path);
+				if(u == nil){
+					srv.reply(ref Rmsg.Error(m.tag, Eremoved));
+					break;
+				}
+				u.secret = nil;
+				writekeys(keyfile);
+				srv.reply(ref Rmsg.Remove(m.tag));
+			* =>
+				srv.remove(m);	# let it reject it
+			}
+		Wstat =>
+			# rename user
+			c := srv.getfid(m.fid);
+			if(c == nil || TYPE(c.path) != Quser){
+				srv.default(gm);	# let it reject it
+				break;
+			}
+			u := finduserpath(c.path);
+			if(u == nil){
+				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
+				break;
+			}
+			if((new := m.stat.name) == nil){
+				srv.default(gm);
+				break;
+			}
+			if(new == "." || new == ".."){
+				srv.reply(ref Rmsg.Error(m.tag, Edot));
+				break;
+			}
+			if(findusername(new) != nil){
+				srv.reply(ref Rmsg.Error(m.tag, "user already exists"));
+				break;
+			}
+			# unhashuser(u);
+			u.name = new;
+			# hashuser(u);
+			writekeys(keyfile);
+			srv.reply(ref Rmsg.Wstat(m.tag));
+		* =>
+			srv.default(gm);
+		}
+	}
+	navops <-= nil;		# shut down navigator
+}
+
+trim(s: string): string
+{
+	(nf, flds) := sys->tokenize(s, " \t\n");
+	if(nf == 0)
+		return nil;
+	return hd flds;
+}
+
+isnumeric(s: string): int
+{
+	for(i:=0; i<len s; i++)
+		if(!(s[i]>='0' && s[i]<='9'))
+			return 0;
+	return i>0;
+}
+
+TYPE(path: big): int
+{
+	return int path & 16rF;
+}
+
+INDEX(path: big): int
+{
+	return (int path & 16rFFFF) >> 4;
+}
+
+finduserpath(path: big): ref User
+{
+	i := INDEX(path);
+	if(i >= len users || (u := users[i]) == nil || u.path != (path & ~big 16rF))
+		return nil;
+	return u;
+}
+
+findusername(name: string): ref User
+{
+	for(i := 0; i < len users; i++)
+		if((u := users[i]) != nil && u.name == name)
+			return u;
+	return nil;
+}
+
+newuser(name: string, u: ref User): ref User
+{
+	for(i := 0; i < len users; i++)
+		if(users[i] == nil)
+			break;
+	if(i >= len users)
+		users = (array[i+16] of ref User)[0:] = users;
+	path := big ((pathgen++ << 16) | (i<<4));
+	if(u == nil)
+		u = ref User(i, name, nil, Never, Sok, 0, path);
+	else{
+		u.x = i;
+		u.path = path;
+	}
+	users[i] = u;
+	return u;
+}
+
+removeuser(u: ref User)
+{
+	if(u != nil)
+		users[u.x] = nil;
+}
+
+dirslot(n: int): int
+{
+	for(i := 0; i < len users; i++){
+		u := users[i];
+		if(u != nil){
+			if(n == 0)
+				break;
+			n--;
+		}
+	}
+	return i;
+}
+
+dir(qid: Sys->Qid, name: string, length: big, perm: int): ref Sys->Dir
+{
+	d := ref sys->zerodir;
+	d.qid = qid;
+	if(qid.qtype & Sys->QTDIR)
+		perm |= Sys->DMDIR;
+	d.mode = perm;
+	d.name = name;
+	d.uid = user;
+	d.gid = user;
+	d.length = length;
+	d.atime = now;
+	d.mtime = now;
+	return d;
+}
+
+dirgen(p: big, name: string, u: ref User): (ref Sys->Dir, string)
+{
+	case t := TYPE(p) {
+	Qroot =>
+		return (dir(Qid(big Qroot, keyversion,Sys->QTDIR), "/", big 0, 8r755), nil);
+	Quser =>
+		if(name == nil){
+			if(u == nil){
+				u = finduserpath(p);
+				if(u == nil)
+					return (nil, Enotfound);
+			}
+			name = u.name;
+		}
+		return (dir(Qid(p,0,Sys->QTDIR), name, big 0, 8r500), nil);	# note: unwritable
+	* =>
+		l := 0;
+		if(t == Qsecret){
+			if(u == nil)
+				u = finduserpath(p);
+			if(u != nil)
+				l = len u.secret;
+		}
+		return (dir(Qid(p,0,Sys->QTFILE), name, big l, 8r600), nil);
+	}
+}
+
+navigator(navops: chan of ref Navop)
+{
+	while((m := <-navops) != nil){
+	   Pick:
+		pick n := m {
+		Stat =>
+			n.reply <-= dirgen(n.path, nil, nil);
+		Walk =>
+			case TYPE(n.path) {
+			Qroot =>
+				if(n.name == ".."){
+					n.reply <-= dirgen(n.path, nil, nil);
+					break;
+				}
+				u := findusername(n.name);
+				if(u == nil){
+					n.reply <-= (nil, Enotfound);
+					break;
+				}
+				n.reply <-= dirgen(u.path | big Quser, u.name, u);
+			Quser =>
+				if(n.name == ".."){
+					n.reply <-= dirgen(big Qroot, nil, nil);
+					break;
+				}
+				for(j := 0; j < len files; j++){
+					(ftype, name) := files[j];
+					if(n.name == name){
+						n.reply <-= dirgen((n.path & ~big 16rF) | big ftype, name, nil);
+						break Pick;
+					}
+				}
+				n.reply <-= (nil, Enotfound);
+			* =>
+				if(n.name != ".."){
+					n.reply <-= (nil, Enotfound);
+					break;
+				}
+				n.reply <-= dirgen((n.path & ~big 16rF) | big Quser, nil, nil);	# parent directory
+			}
+		Readdir =>
+			case TYPE(n.path) {
+			Qroot =>
+				for(j := dirslot(n.offset); --n.count >= 0 && j < len users; j++)
+					if((u := users[j]) != nil)
+						n.reply <-= dirgen(u.path | big Quser, u.name, u);
+				n.reply <-= (nil, nil);
+			Quser =>
+				u := finduserpath(n.path);
+				if(u == nil){
+					n.reply <-= (nil, Eremoved);
+					break;
+				}
+				for(j := n.offset; --n.count >= 0 && j < len files; j++){
+					(ftype, name) := files[j];
+					n.reply <-= dirgen((n.path & ~big 16rF)|big ftype, name, u);
+				}
+				n.reply <-= (nil, nil);
+			}
+		}
+	}
+}
+
+timefd: ref Sys->FD;
+
+time(): int
+{
+	if(timefd == nil){
+		timefd = sys->open("/dev/time", Sys->OREAD);
+		if(timefd == nil)
+			return 0;
+	}
+	buf := array[128] of byte;
+	sys->seek(timefd, big 0, 0);
+	n := sys->read(timefd, buf, len buf);
+	if(n < 0)
+		return 0;
+	t := (big string buf[0:n]) / big 1000000;
+	return int t;
+}
+
+Checkpat: con "XXXXXXXXXXXXXXXX";	# it's what Plan 9's aescbc uses
+Checklen: con len Checkpat;
+
+Hdrlen: con 1+1+4;
+
+packedsize(u: ref User): int
+{
+	return Hdrlen+(1+len array of byte u.name)+(1+len u.secret);
+}
+
+pack(u: ref User): array of byte
+{
+	a := array[packedsize(u)] of byte;
+	a[0] = byte u.status;
+	a[1] = byte u.failed;
+	a[2] = byte u.expire;
+	a[3] = byte (u.expire>>8);
+	a[4] = byte (u.expire>>16);
+	a[5] = byte (u.expire>>24);
+	bn := array of byte u.name;
+	n := len bn;
+	if(n > 255)
+		error(sys->sprint("overlong user name: %s", u.name));	# shouldn't happen
+	a[6] = byte n;
+	a[7:] = bn;
+	n += 7;
+	a[n] = byte len u.secret;
+	a[n+1:] = u.secret;
+	return a;
+}
+
+unpack(a: array of byte): (ref User, int)
+{
+	if(len a < Hdrlen+2)
+		return (nil, 0);
+	u := ref User;
+	u.status = int a[0];
+	u.failed = int a[1];
+	u.expire = (int a[5] << 24) | (int a[4] << 16) | (int a[3] << 8) | int a[2];
+	n := int a[6];
+	j := 7+n;
+	if(j > len a)
+		return (nil, 0);
+	u.name = string a[7:j];
+	if(j >= len a)
+		return (nil, 0);
+	n = int a[j++];
+	if(j+n > len a)
+		return (nil, 0);
+	if(n > 0){
+		u.secret = array[n] of byte;
+		u.secret[0:] = a[j:j+n];
+	}
+	return (u, j+n);
+}
+
+corrupt(keyfile: string)
+{
+	error(sys->sprint("%s: incorrect key or corrupt/damaged keyfile", keyfile));
+}
+
+readkeys(keyfile: string)
+{
+	fd := sys->open(keyfile, Sys->OREAD);
+	if(fd == nil)
+		error(sys->sprint("can't open %s: %r", keyfile));
+	(rc, d) := sys->fstat(fd);
+	if(rc < 0)
+		error(sys->sprint("can't get status of %s: %r", keyfile));
+	length := int d.length;
+	if(length == 0)
+		return;
+	if(length < AESbsize+Checklen)
+		corrupt(keyfile);
+	buf := array[length] of byte;
+	if(sys->read(fd, buf, len buf) != len buf)
+		error(sys->sprint("can't read %s: %r", keyfile));
+	state := kr->aessetup(thekey, buf[0:AESbsize]);
+	if(state == nil)
+		error("can't initialise AES");
+	kr->aescbc(state, buf[AESbsize:], length-AESbsize, Keyring->Decrypt);
+	if(string buf[length-Checklen:] != Checkpat)
+		corrupt(keyfile);
+	length -= Checklen;
+	for(i := AESbsize; i < length;){
+		(u, n) := unpack(buf[i:]);
+		if(u == nil)
+			corrupt(keyfile);
+		newuser(u.name, u);
+		i += n;
+	}
+}
+
+writekeys(keyfile: string)
+{
+	length := 0;
+	for(i := 0; i < len users; i++)
+		if((u := users[i]) != nil)
+			length += packedsize(u);
+	if(length == 0){
+		# leave it empty for clarity
+		fd := sys->create(keyfile, Sys->OWRITE, 8r600);
+		if(fd == nil)
+			error(sys->sprint("can't create %s: %r", keyfile));
+		return;
+	}
+	length += AESbsize+Checklen;
+	buf := array[length] of byte;
+	for(i=0; i<AESbsize; i++)
+		buf[i] = byte rand->rand(256);
+	j := AESbsize;
+	for(i = 0; i < len users; i++)
+		if((u = users[i]) != nil){
+			a := pack(u);
+			buf[j:] = a;
+			j += len a;
+		}
+	buf[length-Checklen:] = array of byte Checkpat;
+	state := kr->aessetup(thekey, buf[0:AESbsize]);
+	if(state == nil)
+		error("can't initialise AES");
+	kr->aescbc(state, buf[AESbsize:], length-AESbsize, Keyring->Encrypt);
+	fd := sys->create(keyfile, Sys->OWRITE, 8r600);
+	if(fd == nil)
+		error(sys->sprint("can't create %s: %r", keyfile));
+	if(sys->write(fd, buf, len buf) != len buf)
+		error(sys->sprint("error writing to %s: %r", keyfile));
+}
--- /dev/null
+++ b/appl/cmd/auth/keysrv.b
@@ -1,0 +1,199 @@
+implement Keysrv;
+
+#
+# remote access to keys (currently only to change secret)
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+
+include "security.m";
+	auth: Auth;
+
+include "arg.m";
+
+keydb := "/mnt/keys";
+
+Keysrv: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: keysrv\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if(sys->pctl(Sys->FORKNS|Sys->NEWPGRP, nil) < 0)
+		err(sys->sprint("can't fork name space: %r"));
+
+	keyfile := "/usr/"+user()+"/keyring/default";
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		err("can't load Arg");
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		'k' =>
+			keyfile = arg->arg();
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	kr = load Keyring Keyring->PATH;
+	if(kr == nil)
+		err("can't load Keyring");
+
+	auth = load Auth Auth->PATH;
+	if(auth == nil)
+		err("can't load Auth");
+	auth->init();
+
+	ai := kr->readauthinfo(keyfile);
+	if(ai == nil)
+		err(sys->sprint("can't read server key file %s: %r", keyfile));
+
+	(fd, id_or_err) := auth->server("sha1" :: "rc4_256" :: nil, ai, sys->fildes(0), 0);
+	if(fd == nil)
+		err(sys->sprint("can't authenticate: %s", id_or_err));
+
+	if(sys->bind("#s", "/mnt/keysrv", Sys->MREPL) < 0)
+		err(sys->sprint("can't bind #s on /mnt/keysrv: %r"));
+	srv := sys->file2chan("/mnt/keysrv", "secret");
+	if(srv == nil)
+		err(sys->sprint("can't create file2chan on /mnt/keysrv: %r"));
+	exitc := chan of int;
+	spawn worker(srv, id_or_err, exitc);
+	if(sys->export(fd, "/mnt/keysrv", Sys->EXPWAIT) < 0){
+		exitc <-= 1;
+		err(sys->sprint("can't export %s: %r", "/mnt/keysrv"));
+	}
+	exitc <-= 1;
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "keysrv: %s\n", s);
+	raise "fail:error";
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd == nil)
+		err(sys->sprint("can't open /dev/user: %r"));
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		err(sys->sprint("error reading /dev/user: %r"));
+
+	return string buf[0:n];	
+}
+
+worker(file: ref Sys->FileIO, user: string, exitc: chan of int)
+{
+	(keydir, secret, err) := getuser(user);
+	if(keydir == nil || secret == nil){
+		if(err == nil)
+			err = "no existing secret";		# can't change it remotely until set
+	}
+	(nil, hash) := hashkey(secret);
+	for(;;)alt{
+	<-exitc =>
+		exit;
+	(nil, nil, nil, rc) := <-file.read =>
+		if(rc == nil)
+			break;
+		if(err != nil){
+			rc <-= (nil, err);
+			break;
+		}
+		rc <-= (nil, nil);
+	(nil, data, nil, wc) := <-file.write =>
+		if(wc == nil)
+			break;
+		if(err != nil){
+			wc <-= (0, err);
+			break;
+		}
+		for(i := 0; i < len data; i++)
+			if(data[i] == byte ' ')
+				break;
+		if(string data[0:i] != hash){
+			wc <-= (0, "wrong secret");
+			break;
+		}
+		if(++i >= len data){
+			wc <-= (0, nil);
+			break;
+		}
+		if(len data - i < 8){
+			wc <-= (0, "unacceptable secret");
+			break;
+		}
+		if(putsecret(keydir, data[i:]) < 0){
+			wc <-= (0, sys->sprint("can't update secret: %r"));
+			break;
+		}
+		wc <-= (len data, nil);
+	}
+}
+
+hashkey(a: array of byte): (array of byte, string)
+{
+	hash := array[Keyring->SHA1dlen] of byte;
+	kr->sha1(a, len a, hash, nil);
+	s := "";
+	for(i := 0; i < len hash; i++)
+		s += sys->sprint("%2.2ux", int hash[i]);
+	return (hash, s);
+}
+
+getuser(id: string): (string, array of byte, string)
+{
+	(ok, nil) := sys->stat(keydb);
+	if(ok < 0)
+		return (nil, nil, sys->sprint("can't stat %s: %r", id));
+	dbdir := keydb+"/"+id;
+	(ok, nil) = sys->stat(dbdir);
+	if(ok < 0)
+		return (nil, nil, sys->sprint("user not registered: %s", id));
+	fd := sys->open(dbdir+"/secret", Sys->OREAD);
+	if(fd == nil)
+		return (nil, nil, sys->sprint("can't open %s/secret: %r", id));
+	d: Sys->Dir;
+	(ok, d) = sys->fstat(fd);
+	if(ok < 0)
+		return (nil, nil, sys->sprint("can't stat %s/secret: %r", id));
+	l := int d.length;
+	secret: array of byte;
+	if(l > 0){
+		secret = array[l] of byte;
+		if(sys->read(fd, secret, len secret) != len secret)
+			return (nil, nil, sys->sprint("error reading %s/secret: %r", id));
+	}
+	return (dbdir, secret, nil);
+}
+
+putsecret(dir: string, secret: array of byte): int
+{
+	fd := sys->create(dir+"/secret", Sys->OWRITE, 8r600);
+	if(fd == nil)
+		return -1;
+	return sys->write(fd, secret, len secret);
+}
--- /dev/null
+++ b/appl/cmd/auth/logind.b
@@ -1,0 +1,246 @@
+implement Logind;
+
+#
+# certification service (signer)
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+	IPint: import kr;
+
+include "dial.m";
+
+include "security.m";
+	ssl: SSL;
+
+include "daytime.m";
+	daytime: Daytime;
+
+Logind: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+TimeLimit: con 5*60*1000;	# five minutes
+keydb := "/mnt/keys";
+
+stderr: ref Sys->FD;
+ 
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->open("/dev/cons", sys->OWRITE);
+
+	kr = load Keyring Keyring->PATH;
+
+	ssl = load SSL SSL->PATH;
+	if(ssl == nil)
+		nomod(SSL->PATH);
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil) 
+		nomod(Daytime->PATH);
+
+	(err, c) := ssl->connect(sys->fildes(0));     
+	if(c == nil)
+		fatal("pushing ssl: " + err);
+
+	# impose time out to ensure dead network connections recovered well before TCP/IP's long time out
+
+	grpid := sys->pctl(Sys->NEWPGRP,nil);
+	pidc := chan of int;
+	spawn stalker(pidc, grpid);
+	tpid := <-pidc;
+	err = dologin(c);
+	if(err != nil){
+		sys->fprint(stderr, "logind: %s\n", err);
+		kr->puterror(c.dfd, err);
+	}
+	kill(tpid, "kill");
+}
+
+dologin(c: ref Dial->Connection): string
+{
+	ivec: array of byte;
+
+	(info, err) := signerkey("/keydb/signerkey");
+	if(info == nil)
+		return "can't read signer's own key: "+err;
+
+	# get user name; ack
+	s: string;
+	(s, err) = kr->getstring(c.dfd);
+	if(err != nil)
+		return err;
+	name := s;
+	kr->putstring(c.dfd, name);
+
+	# get initialization vector
+	(ivec, err) = kr->getbytearray(c.dfd);
+	if(err != nil)
+		return "can't get initialization vector: "+err;
+
+	# lookup password
+	pw := getsecret(s);
+	if(pw == nil)
+		return sys->sprint("no password entry for %s: %r", s);
+	if(len pw < Keyring->SHA1dlen)
+		return "bad password for "+s+": not SHA1 hashed?";
+	userexp := getexpiry(s);
+	if(userexp < 0)
+		return sys->sprint("expiry time for %s: %r", s);
+
+	# generate our random diffie hellman part
+	bits := info.p.bits();
+	r0 := kr->IPint.random(bits/4, bits);
+
+	# generate alpha0 = alpha**r0 mod p
+	alphar0 := info.alpha.expmod(r0, info.p);
+
+	# start encrypting
+	pwbuf := array[8] of byte;
+	for(i := 0; i < 8; i++)
+		pwbuf[i] = pw[i] ^ pw[8+i];
+	for(i = 0; i < 4; i++)
+		pwbuf[i] ^= pw[16+i];
+	for(i = 0; i < 8; i++)
+		pwbuf[i] ^= ivec[i];
+	err = ssl->secret(c, pwbuf, pwbuf);
+	if(err != nil)
+		return "can't set ssl secret: "+err;
+
+	if(sys->fprint(c.cfd, "alg rc4") < 0)
+		return sys->sprint("can't push alg rc4: %r");
+
+	# send P(alpha**r0 mod p)
+	if(kr->putstring(c.dfd, alphar0.iptob64()) < 0)
+		return sys->sprint("can't send (alpha**r0 mod p): %r");
+
+	# stop encrypting
+	if(sys->fprint(c.cfd, "alg clear") < 0)
+		return sys->sprint("can't clear alg: %r");
+
+	# send alpha, p
+	if(kr->putstring(c.dfd, info.alpha.iptob64()) < 0 ||
+	   kr->putstring(c.dfd, info.p.iptob64()) < 0)
+		return sys->sprint("can't send alpha, p: %r");
+
+	# get alpha**r1 mod p
+	(s, err) = kr->getstring(c.dfd);
+	if(err != nil)
+		return "can't get alpha**r1 mod p:"+err;
+	alphar1 := kr->IPint.b64toip(s);
+
+	# compute alpha**(r0*r1) mod p
+	alphar0r1 := alphar1.expmod(r0, info.p);
+
+	# turn on digesting
+	secret := alphar0r1.iptobytes();
+	err = ssl->secret(c, secret, secret);
+	if(err != nil)
+		return "can't set digest secret: "+err;
+	if(sys->fprint(c.cfd, "alg sha1") < 0)
+		return sys->sprint("can't push alg sha1: %r");
+
+	# send our public key
+	if(kr->putstring(c.dfd, kr->pktostr(kr->sktopk(info.mysk))) < 0)
+		return sys->sprint("can't send signer's public key: %r");
+
+	# get his public key
+	(s, err) = kr->getstring(c.dfd);
+	if(err != nil)
+		return "client public key: "+err;
+	hisPKbuf := array of byte s;
+	hisPK := kr->strtopk(s);
+	if(hisPK.owner != name)
+		return "pk name doesn't match user name";
+
+	# sign and return
+	state := kr->sha1(hisPKbuf, len hisPKbuf, nil, nil);
+	cert := kr->sign(info.mysk, userexp, state, "sha1");
+
+	if(kr->putstring(c.dfd, kr->certtostr(cert)) < 0)
+		return sys->sprint("can't send certificate: %r");
+
+	return nil;
+}
+
+nomod(mod: string)
+{
+	fatal(sys->sprint("can't load %s: %r",mod));
+}
+
+fatal(msg: string)
+{
+	sys->fprint(stderr, "logind: %s\n", msg);
+	exit;
+}
+
+signerkey(filename: string): (ref Keyring->Authinfo, string)
+{
+
+	info := kr->readauthinfo(filename);
+	if(info == nil)
+		return (nil, sys->sprint("readauthinfo %r"));
+
+	# validate signer key
+	now := daytime->now();
+	if(info.cert.exp != 0 && info.cert.exp < now)
+		return (nil, sys->sprint("signer key expired"));
+
+	return (info, nil);
+}
+
+getsecret(id: string): array of byte
+{
+	fd := sys->open(sys->sprint("%s/%s/secret", keydb, id), Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0)
+		return nil;
+	a := array[int d.length] of byte;
+	n := sys->read(fd, a, len a);
+	if(n < 0)
+		return nil;
+	return a[0:n];
+}
+
+getexpiry(id: string): int
+{
+	fd := sys->open(sys->sprint("%s/%s/expire", keydb, id), Sys->OREAD);
+	if(fd == nil)
+		return -1;
+	a := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, a, len a);
+	if(n < 0)
+		return -1;
+	s := string a[0:n];
+	if(s == "never")
+		return 0;
+	if(s == "expired"){
+		sys->werrstr(sys->sprint("entry for %s expired", id));
+		return -1;
+	}
+	return int s;
+}
+
+stalker(pidc: chan of int, killpid: int)
+{
+	pidc <-= sys->pctl(0, nil);
+	sys->sleep(TimeLimit);
+	sys->fprint(stderr, "logind: login timed out\n");
+	kill(killpid, "killgrp");
+}
+
+kill(pid: int, how: string)
+{
+	fd := sys->open("#p/" + string pid + "/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", how) < 0)
+		sys->fprint(stderr, "logind: can't %s %d: %r\n", how, pid);
+}
--- /dev/null
+++ b/appl/cmd/auth/mkauthinfo.b
@@ -1,0 +1,125 @@
+implement Mkauthinfo;
+
+#
+#  sign a new key to produce a certificate
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+	IPint: import kr;
+
+include "security.m";
+	auth: Auth;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "arg.m";
+
+Mkauthinfo: module{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->open("/dev/cons", sys->OWRITE);
+
+	kr = load Keyring Keyring->PATH;
+
+	auth = load Auth Auth->PATH;
+	if(auth == nil)
+		nomod(Auth->PATH);
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil) 
+		nomod(Daytime->PATH);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	arg->init(args);
+	arg->setusage("auth/mkauthinfo [-k keyspec] [-e ddmmyyyy] user [keyfile]");
+	keyspec := "key=default";
+	expiry := 0;
+	while((o := arg->opt()) != 0)
+		case o {
+		'k' =>
+			keyspec = arg->earg();
+		'e' =>
+			expiry = parsedate(arg->earg());
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	user := hd args;
+	args = tl args;
+	dstfile := "/fd/1";
+	if(args != nil)
+		dstfile = hd args;
+	arg = nil;
+
+	sai := auth->key(keyspec);
+	if(sai == nil){
+		sys->fprint(stderr, "sign: can't find key matching %q: %r\n", keyspec);
+		raise "fail:no key";
+	}
+
+	info := ref Keyring->Authinfo;
+	info.alpha = sai.alpha;
+	info.p = sai.p;
+	info.mysk = kr->genSKfromPK(sai.spk, user);
+	info.mypk = kr->sktopk(info.mysk);
+	info.spk = sai.mypk;
+	pkbuf := array of byte kr->pktostr(info.mypk);
+	state := kr->sha1(pkbuf, len pkbuf, nil, nil);
+	info.cert = kr->sign(sai.mysk, expiry, state, "sha1");
+	if(kr->writeauthinfo("/fd/1", info) < 0){
+		sys->fprint(stderr, "sign: error writing certificate: %r\n");
+		raise "fail:write error";
+	}
+}
+
+parsedate(s: string): int
+{
+	now := daytime->now();
+	tm := daytime->local(now);
+	if(s == "permanent")
+		return 0;
+	if(len s != 8)
+		fatal("bad date format "+s+" (expected DDMMYYYY)");
+	tm.mday = int s[0:2];
+	if(tm.mday > 31 || tm.mday < 1)
+		fatal(sys->sprint("bad day of month %d", tm.mday));
+	tm.mon = int s[2:4] - 1;
+	if(tm.mon > 11 || tm.mday < 0)
+		fatal(sys->sprint("bad month %d\n", tm.mon + 1));
+	tm.year = int s[4:8] - 1900;
+	if(tm.year < 70)
+		fatal(sys->sprint("bad year %d (year may be no earlier than 1970)", tm.year + 1900));
+	expiry := daytime->tm2epoch(tm);
+	expiry += 60;
+	if(expiry <= now)
+		fatal("expiry date has already passed");
+	return expiry;
+}
+
+nomod(mod: string)
+{
+	fatal(sys->sprint("can't load %s: %r",mod));
+}
+
+fatal(msg: string)
+{
+	sys->fprint(stderr, "mkauthinfo: %s\n", msg);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/auth/mkfile
@@ -1,0 +1,40 @@
+<../../../mkconfig
+
+DIRS=\
+	factotum\
+
+TARG=\
+	aescbc.dis\
+	ai2key.dis\
+	changelogin.dis\
+	countersigner.dis\
+	convpasswd.dis\
+	createsignerkey.dis\
+	dsagen.dis\
+	keyfs.dis\
+	keysrv.dis\
+	getpk.dis\
+	logind.dis\
+	mkauthinfo.dis\
+	passwd.dis\
+	rsagen.dis\
+	secstore.dis\
+	signer.dis\
+	verify.dis\
+
+SYSMODULES=\
+	arg.m\
+	keyring.m\
+	security.m\
+	rand.m\
+	sys.m\
+	draw.m\
+	bufio.m\
+	string.m\
+	styx.m\
+	styxservers.m\
+
+DISBIN=$ROOT/dis/auth
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/cmd/auth/passwd.b
@@ -1,0 +1,281 @@
+implement Passwd;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+
+include "dial.m";
+	dial: Dial;
+
+include "security.m";
+	auth: Auth;
+
+include "arg.m";
+
+Passwd: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr, stdin, stdout: ref Sys->FD;
+keysrv := "/mnt/keysrv";
+signer := "$SIGNER";
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: passwd [-u user] [-s signer] [keyfile]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	stdin = sys->fildes(0);
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+
+	kr = load Keyring Keyring->PATH;
+	if(kr == nil)
+		noload(Keyring->PATH);
+	dial = load Dial Dial->PATH;
+	if(dial == nil)
+		noload(Dial->PATH);
+	auth = load Auth Auth->PATH;
+	if(auth == nil)
+		noload(Auth->PATH);
+	auth->init();
+
+	keyfile, id: string;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		noload(Arg->PATH);
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		's' =>
+			signer = arg->arg();
+		'u' =>
+			id = arg->arg();
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(args == nil)
+		args = "default" :: nil;
+
+	if(id == nil)
+		id= user();
+
+	if(args != nil)
+		keyfile = hd args;
+	else
+		keyfile = "default";
+	if(len keyfile > 0 && keyfile[0] != '/')
+		keyfile = "/usr/" + id + "/keyring/" + keyfile;
+
+	ai := kr->readauthinfo(keyfile);
+	if(ai == nil)
+		err(sys->sprint("can't read certificate from %s: %r", keyfile));
+sys->print("key owner: %s\n", ai.mypk.owner);
+
+	sys->pctl(Sys->FORKNS|Sys->FORKFD, nil);
+	mountsrv(ai);
+
+	# get password
+	ok: int;
+	secret: array of byte;
+	oldhash: array of byte;
+	word: string;
+	for(;;){
+		sys->print("Inferno secret: ");
+		(ok, word) = readline(stdin, "rawon");
+		if(!ok || word == nil)
+			exit;
+		secret = array of byte word;
+		(nil, s) := hashkey(secret);
+		for(i := 0; i < len word; i++)
+			word[i] = ' ';
+		oldhash = array of byte s;
+		e := putsecret(oldhash, nil);
+		if(e != "wrong secret"){
+			if(e == nil)
+				break;
+			err(e);
+		}
+		sys->fprint(stderr, "!wrong secret\n");
+	}
+	newsecret: array of byte;
+	for(;;){
+		for(;;){
+			sys->print("new secret [default = don't change]: ");
+			(ok, word) = readline(stdin, "rawon");
+			if(!ok)
+				exit;
+			if(word == "" && secret != nil)
+				break;
+			if(len word >= 8)
+				break;
+			sys->print("!secret must be at least 8 characters\n");
+		}
+		if(word != ""){
+			# confirm password change
+			word1 := word;
+			sys->print("confirm: ");
+			(ok, word) = readline(stdin, "rawon");
+			if(!ok || word != word1){
+				sys->fprint(stderr, "!entries didn't match\n");
+				continue;
+			}
+			# TO DO...
+			#pwbuf := array of byte word;
+			#newsecret = array[Keyring->SHA1dlen] of byte;
+			#kr->sha1(pwbuf, len pwbuf, newsecret, nil);
+			newsecret = array of byte word;
+		}
+		if(!eq(newsecret, secret)){
+			if((e := putsecret(oldhash, newsecret)) != nil){
+				sys->fprint(stderr, "passwd: can't update secret for %s: %s\n", id, e);
+				continue;
+			}
+		}
+		break;
+	}
+}
+
+noload(s: string)
+{
+	err(sys->sprint("can't load %s: %r", s));
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "passwd: %s\n", s);
+	raise "fail:error";
+}
+
+mountsrv(ai: ref Keyring->Authinfo): string
+{
+	c := dial->dial(dial->netmkaddr(signer, "net", "infkey"), nil);
+	if(c == nil)
+		err(sys->sprint("can't dial %s: %r", signer));
+	(fd, id_or_err) := auth->client("sha1/rc4_256", ai, c.dfd);
+	if(fd == nil)
+		err(sys->sprint("can't authenticate with %s: %r", signer));
+	if(sys->mount(fd, nil, keysrv, Sys->MREPL, nil) < 0)
+		err(sys->sprint("can't mount %s on %s: %r", signer, keysrv));
+	return id_or_err;
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd == nil)
+		err(sys->sprint("can't open /dev/user: %r"));
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		err(sys->sprint("error reading /dev/user: %r"));
+
+	return string buf[0:n];	
+}
+
+eq(a, b: array of byte): int
+{
+	if(len a != len b)
+		return 0;
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+hashkey(a: array of byte): (array of byte, string)
+{
+	hash := array[Keyring->SHA1dlen] of byte;
+	kr->sha1(a, len a, hash, nil);
+	s := "";
+	for(i := 0; i < len hash; i++)
+		s += sys->sprint("%2.2ux", int hash[i]);
+	return (hash, s);
+}
+
+putsecret(oldhash: array of byte, secret: array of byte): string
+{
+	fd := sys->create(keysrv+"/secret", Sys->OWRITE, 8r600);
+	if(fd == nil)
+		return sys->sprint("%r");
+	n := len oldhash;
+	if(secret != nil)
+		n += 1 + len secret;
+	buf := array[n] of byte;
+	buf[0:] = oldhash;
+	if(secret != nil){
+		buf[len oldhash] = byte ' ';
+		buf[len oldhash+1:] = secret;
+	}
+	if(sys->write(fd, buf, len buf) < 0)
+		return sys->sprint("%r");
+	return nil;
+}
+
+readline(io: ref Sys->FD, mode: string): (int, string)
+{
+	r : int;
+	line : string;
+	buf := array[8192] of byte;
+	fdctl : ref Sys->FD;
+	rawoff := array of byte "rawoff";
+
+	if(mode == "rawon"){
+		fdctl = sys->open("/dev/consctl", sys->OWRITE);
+		if(fdctl == nil || sys->write(fdctl,array of byte mode,len mode) != len mode){
+			sys->fprint(stderr, "unable to change console mode");
+			return (0,nil);
+		}
+	}
+
+	line = "";
+	for(;;) {
+		r = sys->read(io, buf, len buf);
+		if(r <= 0){
+			sys->fprint(stderr, "error read from console mode");
+			if(mode == "rawon")
+				sys->write(fdctl,rawoff,6);
+			return (0, nil);
+		}
+
+		line += string buf[0:r];
+		if ((len line >= 1) && (line[(len line)-1] == '\n')){
+			if(mode == "rawon"){
+				r = sys->write(stdout,array of byte "\n",1);
+				if(r <= 0) {
+					sys->write(fdctl,rawoff,6);
+					return (0, nil);
+				}
+			}
+			break;
+		}
+		else {
+			if(mode == "rawon"){
+				#r = sys->write(stdout, array of byte "*",1);
+				if(r <= 0) {
+					sys->write(fdctl,rawoff,6);
+					return (0, nil);
+				}
+			}
+		}
+	}
+
+	if(mode == "rawon")
+		sys->write(fdctl,rawoff,6);
+
+	return (1, line[0:len line - 1]);
+}
--- /dev/null
+++ b/appl/cmd/auth/rsagen.b
@@ -1,0 +1,78 @@
+implement Rsagen;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "ipints.m";
+	ipints: IPints;
+	IPint: import ipints;
+
+include "crypt.m";
+	crypt: Crypt;
+
+include "arg.m";
+
+Rsagen: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	ipints = load IPints IPints->PATH;
+	crypt = load Crypt Crypt->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("auth/rsagen [-b bits] [-t 'attr=value attr=value ...']");
+	tag: string;
+	nbits := 1024;
+	while((o := arg->opt()) != 0)
+		case o {
+		'b' =>
+			nbits = int arg->earg();
+			if(nbits <= 0)
+				arg->usage();
+			if(nbits > 4096)
+				error("bits must be no greater than 4096");
+		't' =>
+			tag = arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args != nil)
+		arg->usage();
+	arg = nil;
+
+	sk := crypt->rsagen(nbits, 6, 0);
+	if(sk == nil)
+		error("unable to generate key");
+	if(tag != nil)
+		tag = " "+tag;
+	s := add("ek", sk.pk.ek);
+	s += add("n", sk.pk.n);
+	s += add("!dk", sk.dk);
+	s += add("!p", sk.p);
+	s += add("!q", sk.q);
+	s += add("!kp", sk.kp);
+	s += add("!kq", sk.kq);
+	s += add("!c2", sk.c2);
+	a := sys->aprint("key proto=rsa%s size=%d%s\n", tag, sk.pk.n.bits(), s);
+	if(sys->write(sys->fildes(1), a, len a) != len a)
+		error(sys->sprint("error writing key: %r"));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "rsagen: %s\n", s);
+	raise "fail:error";
+}
+
+add(name: string, b: ref IPint): string
+{
+	return " "+name+"="+b.iptostr(16);
+}
--- /dev/null
+++ b/appl/cmd/auth/secstore.b
@@ -1,0 +1,332 @@
+implement Secstorec;
+
+#
+# interact with the Plan 9 secstore
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "dial.m";
+	dial: Dial;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "secstore.m";
+	secstore: Secstore;
+
+include "arg.m";
+
+Secstorec: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Maxfilesize: con 128*1024;
+
+stderr: ref Sys->FD;
+conn: ref Dial->Connection;
+seckey: array of byte;
+filekey: array of byte;
+file: array of byte;
+verbose := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	secstore = load Secstore Secstore->PATH;
+	dial = load Dial Dial->PATH;
+
+	sys->pctl(Sys->FORKFD, nil);
+	stderr = sys->fildes(2);
+	secstore->init();
+	secstore->privacy();
+
+	addr := "net!$auth!secstore";
+	user := readfile("/dev/user");
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("auth/secstore [-iv] [-k key] [-p pin] [-s net!server!secstore] [-u user] [{drptx} file ...]");
+	iflag := 0;
+	pass, pin: string;
+	while((o := arg->opt()) != 0)
+		case o {
+		'i' => iflag = 1;
+		'k' => pass = arg->earg();
+		'v' => verbose = 1;
+		's' =>	addr = arg->earg();
+		'u' => user = arg->earg();
+		'p' => pin = arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	op := -1;
+	if(args != nil){
+		if(len hd args != 1)
+			arg->usage();
+		op = (hd args)[0];
+		args = tl args;
+		case op {
+		'd' or 'r' or 'p' or 'x' =>
+			if(args == nil)
+				arg->usage();
+		't' =>
+			;
+		* =>
+			arg->usage();
+		}
+	}
+	arg = nil;
+
+	if(iflag){
+		buf := array[Secstore->Maxmsg] of byte;
+		stdin := sys->fildes(0);
+		for(nr := 0; nr < len buf && (n := sys->read(stdin, buf, len buf-nr)) > 0;)
+			nr += n;
+		s := string buf[0:nr];
+		secstore->erasekey(buf[0:nr]);
+		(nf, flds) := sys->tokenize(s, "\n");
+		for(i := 0; i < len s; i++)
+			s[i] = 0;
+		if(nf < 1)
+			error("no password on standard input");
+		pass = hd flds;
+		if(nf > 1)
+			pin = hd tl flds;
+	}
+	conn: ref Dial->Connection;
+Auth:
+	for(;;){
+		if(!iflag)
+			pass = readpassword("secstore password");
+		if(pass == nil)
+			exit;
+		erase();
+		seckey = secstore->mkseckey(pass);
+		filekey = secstore->mkfilekey(pass);
+		for(i := 0; i < len pass; i++)
+			pass[i] = 0;	# clear it
+		conn = secstore->dial(dial->netmkaddr(addr, "net", "secstore"));
+		if(conn == nil)
+			error(sys->sprint("can't connect to secstore: %r"));
+		(srvname, diag) := secstore->auth(conn, user, seckey);
+		if(srvname == nil){
+			secstore->bye(conn);
+			sys->fprint(stderr, "secstore: authentication failed: %s\n",  diag);
+			if(iflag)
+				raise "fail:auth";
+			continue;
+		}
+		case diag {
+		"" =>
+			if(verbose)
+				sys->fprint(stderr, "server: %s\n", srvname);
+			secstore->erasekey(seckey);
+			seckey = nil;
+			break Auth;
+		"need pin" =>
+			if(!iflag){
+				pin = readpassword("STA PIN+SecureID");
+				if(len pin == 0){
+					sys->fprint(stderr, "cancelled");
+					exit;
+				}
+			}else if(pin == nil)
+				raise "fail:no pin";
+			if(secstore->sendpin(conn, pin) < 0){
+				sys->fprint(stderr, "secstore: pin rejected: %r\n");
+				if(iflag)
+					raise "fail:bad pin";
+				continue;
+			}
+		}
+	}
+	if(op == 't'){
+		erase();	# no longer need the keys
+		entries := secstore->files(conn);
+		for(; entries != nil; entries = tl entries){
+			(name, size, date, hash, nil) := hd entries;
+			if(args != nil){
+				for(l := args; l != nil; l = tl l)
+					if((hd args) == name)
+						break;
+				if(args == nil)
+					continue;
+			}
+			if(verbose)
+				sys->print("%-14q %10d %s %s\n", name, size, date, hash);
+			else
+				sys->print("%q\n", name);
+		}
+		exit;
+	}
+	for(; args != nil; args = tl args){
+		fname := hd args;
+		case op {
+		'd' =>
+			checkname(fname, 1);
+			if(secstore->remove(conn, fname) < 0)
+				error(sys->sprint("can't remove %q: %r", fname));
+			verb('d', fname);
+		'p' =>
+			checkname(fname, 1);
+			file = getfile(conn, fname, filekey);
+			lines := secstore->lines(file);
+			lno := 1;
+			for(; lines != nil; lines = tl lines){
+				l := hd lines;
+				if(sys->write(sys->fildes(1), l, len l) != len l)
+					sys->fprint(sys->fildes(2), "secstore (%s:%d): %r\n", fname, lno);
+				lno++;
+			}
+			secstore->erasekey(file);
+			file = nil;
+			verb('p', fname);
+		'x' =>
+			checkname(fname, 1);
+			file = getfile(conn, fname, filekey);
+			ofd := sys->create(fname, Sys->OWRITE, 8r600);
+			if(ofd == nil)
+				error(sys->sprint("can't create %q: %r", fname));
+			if(sys->write(ofd, file, len file) != len file)
+				error(sys->sprint("error writing to %q: %r", fname));
+			secstore->erasekey(file);
+			file = nil;
+			verb('x', fname);
+		'r' =>
+			checkname(fname, 1);
+			fd := sys->open(fname, sys->OREAD);
+			if(fd == nil)
+				error(sys->sprint("open %q: %r", fname));
+			(ok, dir) := sys->fstat(fd);
+			if(ok != 0)
+				error(sys->sprint("stat %q: %r", fname));
+			if(int dir.length > Maxfilesize)
+				error(sys->sprint("length %bd > Maxfilesize %d", dir.length, Maxfilesize));
+			file = array[int dir.length] of byte;
+			if(sys->readn(fd, file, len file) != len file)
+				error(sys->sprint("short read: %r"));
+			if(putfile(conn, fname, file, filekey) < 0)
+				error(sys->sprint("putfile: %r"));
+			secstore->erasekey(file);
+			file = nil;
+			verb('r', fname);
+		* =>
+			error(sys->sprint("op %c not implemented", op));
+		}
+	}
+	erase();
+}
+
+checkname(s: string, noslash: int): string
+{
+	tail := s;
+	for(i := 0; i < len s; i++){
+		if(s[i] == '/'){
+			if(noslash)
+				break;
+			tail = s[i+1:];
+		}
+		if(s[i] == '\n' || s[i] <= ' ')
+			break;
+	}
+	if(s == nil || tail == nil || i < len s || s == "..")
+		error(sys->sprint("can't use %q as a secstore file name", s));	# server checks as well, of course
+	return tail;
+}
+
+verb(op: int, n: string)
+{
+	if(verbose)
+		sys->fprint(stderr, "%c %q\n", op, n);
+}
+
+getfile(conn: ref Dial->Connection, fname: string, key: array of byte): array of byte
+{
+	f := secstore->getfile(conn, fname, 0);
+	if(f == nil)
+		error(sys->sprint("can't fetch %q: %r", fname));
+	if(fname != "."){
+		f = secstore->decrypt(f, key);
+		if(f == nil)
+			error(sys->sprint("can't decrypt %q: %r", fname));
+	}
+	return f;
+}
+
+putfile(conn: ref Dial->Connection, fname: string, data, key: array of byte): int
+{
+	data = secstore->encrypt(data, key);
+	if(data == nil)
+		return -1;
+	return secstore->putfile(conn, fname, data);
+}
+
+erase()
+{
+	if(secstore != nil){
+		secstore->erasekey(seckey);
+		secstore->erasekey(filekey);
+		secstore->erasekey(file);
+	}
+}
+
+error(s: string)
+{
+	erase();
+	sys->fprint(stderr, "secstore: %s\n", s);
+	raise "fail:error";
+}
+
+readpassword(prompt: string): string
+{
+	cons := sys->open("/dev/cons", Sys->ORDWR);
+	if(cons == nil)
+		return nil;
+	stdin := bufio->fopen(cons, Sys->OREAD);
+	if(stdin == nil)
+		return nil;
+	cfd := sys->open("/dev/consctl", Sys->OWRITE);
+	if (cfd == nil || sys->fprint(cfd, "rawon") <= 0)
+		sys->fprint(stderr, "secstore: warning: cannot hide typed password\n");
+L:
+	for(;;){
+		sys->fprint(cons, "%s: ", prompt);
+		s := "";
+		while ((c := stdin.getc()) >= 0){
+			case c {
+			'\n' or ('d'&8r037) =>
+				sys->fprint(cons, "\n");
+				return s;
+			'\b' or 8r177 =>
+				if(len s > 0)
+					s = s[0:len s - 1];
+			'u' & 8r037 =>
+				sys->fprint(cons, "\n");
+				continue L;
+			* =>
+				s[len s] = c;
+			}
+		}
+		break;
+	}
+	return nil;
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil)
+		return "";
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+	return string buf[0:n]; 
+}
--- /dev/null
+++ b/appl/cmd/auth/signer.b
@@ -1,0 +1,132 @@
+implement Signer;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+	IPint: import kr;
+
+include "security.m";
+	random: Random;
+
+Signer: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+# size in bits of modulus for public keys
+PKmodlen:		con 512;
+
+# size in bits of modulus for diffie hellman
+DHmodlen:		con 512;
+
+stderr, stdin, stdout: ref Sys->FD;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	random = load Random Random->PATH;
+	kr = load Keyring Keyring->PATH;
+
+	stdin = sys->fildes(0);
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+
+	sys->pctl(Sys->FORKNS, nil);
+	if(sys->chdir("/keydb") < 0){
+		sys->fprint(stderr, "signer: no key database\n");
+		raise "fail:no keydb";
+	}
+
+	err := sign();
+	if(err != nil){
+		sys->fprint(stderr, "signer: %s\n", err);
+		raise "fail:error";
+	}
+}
+
+sign(): string
+{
+	info := signerkey("signerkey");
+	if(info == nil)
+		return "can't read key";
+
+	# send public part to client
+	mypkbuf := array of byte kr->pktostr(kr->sktopk(info.mysk));
+	kr->sendmsg(stdout, mypkbuf, len mypkbuf);
+	alphabuf := array of byte info.alpha.iptob64();
+	kr->sendmsg(stdout, alphabuf, len alphabuf);
+	pbuf := array of byte info.p.iptob64();
+	kr->sendmsg(stdout, pbuf, len pbuf);
+
+	# get client's public key
+	hisPKbuf := kr->getmsg(stdin);
+	if(hisPKbuf == nil)
+		return "caller hung up";
+	hisPK := kr->strtopk(string hisPKbuf);
+	if(hisPK == nil)
+		return "illegal caller PK";
+
+	# hash, sign, and blind
+	state := kr->sha1(hisPKbuf, len hisPKbuf, nil, nil);
+	cert := kr->sign(info.mysk, 0, state, "sha1");
+
+	# sanity clause
+	state = kr->sha1(hisPKbuf, len hisPKbuf, nil, nil);
+	if(kr->verify(info.mypk, cert, state) == 0)
+		return "bad signer certificate";
+
+	certbuf := array of byte kr->certtostr(cert);
+	blind := random->randombuf(random->ReallyRandom, len certbuf);
+	for(i := 0; i < len blind; i++)
+		certbuf[i] = certbuf[i] ^ blind[i];
+
+	# sum PKs and blinded certificate
+	state = kr->md5(mypkbuf, len mypkbuf, nil, nil);
+	kr->md5(hisPKbuf, len hisPKbuf, nil, state);
+	digest := array[Keyring->MD5dlen] of byte;
+	kr->md5(certbuf, len certbuf, digest, state);
+
+	# save sum and blinded cert in a file
+	file := "signed/"+hisPK.owner;
+	fd := sys->create(file, Sys->OWRITE, 8r600);
+	if(fd == nil)
+		return "can't create "+file+sys->sprint(": %r");
+	if(kr->sendmsg(fd, blind, len blind) < 0 ||
+	   kr->sendmsg(fd, digest, len digest) < 0){
+		sys->remove(file);
+		return "can't write "+file+sys->sprint(": %r");
+	}
+
+	# send blinded cert to client
+	kr->sendmsg(stdout, certbuf, len certbuf);
+
+	return nil;
+}
+
+signerkey(filename: string): ref Keyring->Authinfo
+{
+	info := kr->readauthinfo(filename);
+	if(info != nil)
+		return info;
+
+	# generate a local key
+	info = ref Keyring->Authinfo;
+	info.mysk = kr->genSK("elgamal", "*", PKmodlen);
+	info.mypk = kr->sktopk(info.mysk);
+	info.spk = kr->sktopk(info.mysk);
+	myPKbuf := array of byte kr->pktostr(info.mypk);
+	state := kr->sha1(myPKbuf, len myPKbuf, nil, nil);
+	info.cert = kr->sign(info.mysk, 0, state, "sha1");
+	(info.alpha, info.p) = kr->dhparams(DHmodlen);
+
+	if(kr->writeauthinfo(filename, info) < 0){
+		sys->fprint(stderr, "can't write signerkey file: %r\n");
+		return nil;
+	}
+
+	return info;
+}
--- /dev/null
+++ b/appl/cmd/auth/verify.b
@@ -1,0 +1,85 @@
+implement Verify;
+
+include "sys.m";
+	sys: Sys;
+
+include "keyring.m";
+	kr: Keyring;
+
+include "draw.m";
+
+Verify: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr, stdin: ref Sys->FD;
+
+pro := array[] of {
+	"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf",
+	"hotel", "india", "juliet", "kilo", "lima", "mike", "nancy", "oscar",
+	"papa", "quebec", "romeo", "sierra", "tango", "uniform",
+	"victor", "whisky", "xray", "yankee", "zulu"
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+
+	stdin = sys->fildes(0);
+	stderr = sys->fildes(2);
+
+	if(args != nil)
+		args = tl args;
+	if(args == nil){
+		sys->fprint(stderr, "usage: verify boxid\n");
+		raise "fail:usage";
+	}
+
+	sys->pctl(Sys->FORKNS, nil);
+	if(sys->chdir("/keydb") < 0){
+		sys->fprint(stderr, "signer: no key database\n");
+		raise "fail:no keydb";
+	}
+
+	boxid := hd args;
+	file := "signed/"+boxid;
+	fd := sys->open(file, Sys->OREAD);
+	if(fd == nil){
+		sys->fprint(stderr, "signer: can't open %s: %r\n", file);
+		raise "fail:no certificate";
+	}
+	certbuf := kr->getmsg(fd);
+	digest := kr->getmsg(fd);
+	if(digest == nil || certbuf == nil){
+		sys->fprint(stderr, "signer: can't read %s: %r\n", file);
+		raise "fail:bad certificate";
+	}
+
+	s: string;
+	for(i := 0; i < len digest; i++){
+		s = s + (string (2*i)) + ": " + pro[((int digest[i])>>4)%len pro] + "\t";
+		s = s + (string (2*i+1)) + ": " + pro[(int digest[i])%len pro] + "\n";
+	}
+
+	sys->print("%s\naccept (y or n)? ", s);
+	buf := array[5] of byte;
+	n := sys->read(stdin, buf, len buf);
+	if(n < 1 || buf[0] != byte 'y'){
+		sys->print("\nrejected\n");
+		raise "fail:rejected";
+	}
+	sys->print("\naccepted\n");
+
+	nfile := "countersigned/"+boxid;
+	fd = sys->create(nfile, Sys->OWRITE, 8r600);
+	if(fd == nil){
+		sys->fprint(stderr, "signer: can't create %s: %r\n", nfile);
+		raise "fail:create";
+	}
+	if(kr->sendmsg(fd, certbuf, len certbuf) < 0){
+		sys->fprint(stderr, "signer: can't write %s: %r\n", nfile);
+		raise "fail:write";
+	}
+}
--- /dev/null
+++ b/appl/cmd/auxi/cpuslave.b
@@ -1,0 +1,79 @@
+implement CPUslave;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Context, Display, Screen: import draw;
+include "arg.m";
+
+include "sh.m";
+
+stderr: ref Sys->FD;
+
+CPUslave: module
+{
+	init: fn(ctxt: ref Context, args: list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: cpuslave [-s screenid] command args\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil) {
+		sys->fprint(stderr, "cpuslave: cannot load %s: %r\n", Arg->PATH);
+		raise "fail:bad module";
+	}
+	screenid := -1;
+	arg->init(args);
+	while ((opt := arg->opt()) != 0) {
+		if (opt != 's' || (a := arg->arg()) == nil)
+			usage();
+		screenid = int a;
+	}
+	args = arg->argv();
+	if(args == nil)
+		usage();
+
+	file := hd args + ".dis";
+	cmd := load Command file;
+	if(cmd == nil)
+		cmd = load Command "/dis/"+file;
+	if(cmd == nil){
+		sys->fprint(stderr, "cpuslave: can't load %s: %r\n", hd args);
+		raise "fail:bad command";
+	}
+
+	ctxt: ref Context;
+	if (screenid >= 0) {
+		display := Display.allocate(nil);
+		if(display == nil){
+			sys->fprint(stderr, "cpuslave: can't initialize display: %r\n");
+			raise "fail:no display";
+		}
+	
+		screen: ref Screen;
+		if(screenid >= 0){
+			screen = display.publicscreen(screenid);
+			if(screen == nil){
+				sys->fprint(stderr, "cpuslave: cannot access screen %d: %r\n", screenid);
+				raise "fail:bad screen";
+			}
+		}
+
+		ctxt = ref Context;
+		ctxt.screen = screen;
+		ctxt.display = display;
+	}
+	
+	spawn cmd->init(ctxt, args);
+}
--- /dev/null
+++ b/appl/cmd/auxi/digest.b
@@ -1,0 +1,91 @@
+implement Digest;
+
+#
+# read a classifier example file and write its digest
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "strokes.m";
+	strokes: Strokes;
+	Classifier, Penpoint, Stroke: import strokes;
+	readstrokes: Readstrokes;
+	writestrokes: Writestrokes;
+
+include "arg.m";
+
+Digest: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "Usage: digest [file.cl ...]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	strokes = load Strokes Strokes->PATH;
+	if(strokes == nil)
+		nomod(Strokes->PATH);
+	strokes->init();
+	readstrokes = load Readstrokes Readstrokes->PATH;
+	if(readstrokes == nil)
+		nomod(Readstrokes->PATH);
+	readstrokes->init(strokes);
+	writestrokes = load Writestrokes Writestrokes->PATH;
+	if(writestrokes == nil)
+		nomod(Writestrokes->PATH);
+	writestrokes->init(strokes);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	arg->init(args);
+	while((opt := arg->opt()) != 0)
+		case opt {
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	for(; args != nil; args = tl args){
+		ofile := file := hd args;
+		n := len file;
+		if(n >= 3 && ofile[n-3:] == ".cl")
+			ofile = ofile[0:n-3];
+		ofile += ".clx";
+		(err, rec) := readstrokes->read_classifier(hd args, 1, 0);
+		if(err != nil)
+			error(sys->sprint("error reading classifier from %s: %s", file, err));
+		fd := sys->create(ofile, Sys->OWRITE, 8r666);
+		if(fd == nil)
+			error(sys->sprint("can't create %s: %r", file));
+		err = writestrokes->write_digest(fd, rec.cnames, rec.dompts);
+		if(err != nil)
+			error(sys->sprint("error writing digest to %s: %s", file, err));
+	}
+}
+
+nomod(s: string)
+{
+	error(sys->sprint("can't load %s: %r", s));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "digest: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/auxi/fpgaload.b
@@ -1,0 +1,67 @@
+implement Fpgaload;
+
+include"sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+Fpgaload: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		error(sys->sprint("can't load %s: %r", Arg->PATH));
+	arg->init(args);
+	arg->setusage("fpgaload [-c clock] file.rbf");
+	clock := -1;
+	while((c := arg->opt()) != 0)
+		case c {
+		'c' =>
+			clock = int arg->earg();
+			if(clock <= 0)
+				error("invalid clock value");
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	fd := sys->open(hd args, Sys->OREAD);
+	if(fd == nil)
+		error(sys->sprint("can't open %s: %r", hd args));
+	ofd := sys->open("#G/fpgaprog", Sys->OWRITE);
+	if(ofd == nil)
+		error(sys->sprint("can't open %s: %r", "#G/fpgaprog"));
+	a := array[128*1024] of byte;
+	while((n := sys->read(fd, a, len a)) > 0)
+		if(sys->write(ofd, a, n) != n)
+			error(sys->sprint("write error: %r"));
+	if(n < 0)
+		error(sys->sprint("read error: %r"));
+	if(clock >= 0)
+		setclock(clock);
+}
+
+setclock(n: int)
+{
+	fd := sys->open("#G/fpgactl", Sys->OWRITE);
+	if(fd == nil)
+		error(sys->sprint("can't open %s: %r", "#G/fpgactl"));
+	if(sys->fprint(fd, "bclk %d", n) < 0)
+		error(sys->sprint("can't set clock to %d: %r", n));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "fpgaload: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/auxi/mangaload.b
@@ -1,0 +1,350 @@
+implement Mangaload;
+
+# to do:
+#	- set arp entry based on /lib/ndb if necessary
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "ip.m";
+	ip: IP;
+	IPaddr: import ip;
+
+include "timers.m";
+	timers: Timers;
+	Timer: import timers;
+
+include "dial.m";
+	dial: Dial;
+
+include "arg.m";
+
+Mangaload: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+# manga parameters
+FlashBlocksize: con 16r10000;
+FlashSize: con 16r400000;	# 4meg for now
+FlashUserArea: con 16r3C0000;
+
+# magic values
+FooterOffset: con 16rFFEC;
+FooterSig: con 16rA0FFFF9F;	# ARM flash library
+FileInfosize: con 64;
+FileNamesize: con FileInfosize - 3*4;	# x, y, z
+Packetdatasize: con 1500-28;	# ether data less IP + ICMP header
+RequestTimeout: con 500;
+Probecount: con 10;	# query unit every so many packets
+
+# manga uses extended TFTP ops in ICMP InfoRequest packets
+Tftp_Req: con 0;
+Tftp_Read: con 1;
+Tftp_Write: con 2;
+Tftp_Data: con 3;
+Tftp_Ack: con 4;
+Tftp_Error: con 5;
+Tftp_Last: con 6;
+
+Icmp: adt
+{
+	ttl:	int;	# time to live
+	src:	IPaddr;
+	dst:	IPaddr;
+	ptype:	int;
+	code:	int;
+	id:	int;
+	seq:	int;
+	data:	array of byte;
+	munged:	int;	# packet received but corrupt
+
+	unpack:	fn(b: array of byte): ref Icmp;
+};
+
+# ICMP packet types
+EchoReply: con 0;
+Unreachable: con 3;
+SrcQuench: con 4;
+EchoRequest: con 8;
+TimeExceed: con 11;
+Timestamp: con 13;
+TimestampReply: con 14;
+InfoRequest: con 15;
+InfoReply: con 16;
+
+Nmsg: con 32;
+Interval: con 1000;	# ms
+
+debug := 0;
+flashblock := 1;	# never 0, that's the boot firmware
+maxfilesize := 8*FlashBlocksize;
+flashlim := FlashSize/FlashBlocksize;
+loadinitrd := 0;
+maxlen := 512*1024;
+mypid := 0;
+Datablocksize: con 4096;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	timers = load Timers Timers->PATH;
+	dial = load Dial Dial->PATH;
+	ip = load IP IP->PATH;
+	ip->init();
+
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("mangaload [-48dr] destination file");
+	while((o := arg->opt()) != 0)
+		case o {
+		'4' =>
+			flashlim = 4*1024*1024/FlashBlocksize;
+		'8' =>
+			flashlim = 8*1024*1024/FlashBlocksize;
+		'r' =>
+			loadinitrd = 1;
+			flashblock = 9;
+			if(flashlim > 4*1024*1024/FlashBlocksize)
+				maxfilesize = 113*FlashBlocksize;
+			else
+				maxfilesize = 50*FlashBlocksize;
+		'd' =>
+			debug++;
+		}
+	args = arg->argv();
+	if(len args != 2)
+		arg->usage();
+	arg = nil;
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD, nil);
+
+	filename := hd tl args;
+	fd := sys->open(filename, Sys->OREAD);
+	if(fd == nil){
+		sys->fprint(sys->fildes(2), "mangaload: can't open %s: %r\n", filename);
+		raise "fail:open";
+	}
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0){
+		sys->fprint(sys->fildes(2), "mangaload: can't stat %s: %r\n", filename);
+		raise "fail:stat";
+	}
+	if(d.length > big maxfilesize){
+		sys->fprint(sys->fildes(2), "mangaload: file %s too long (must not exceed %d bytes)\n",
+			filename, maxfilesize);
+		raise "fail:size";
+	}
+	filesize := int d.length;
+
+	port := sys->sprint("%d", 16r8695);
+	addr := dial->netmkaddr(hd args, "icmp", port);
+	c := dial->dial(addr, port);
+	if(c == nil){
+		sys->fprint(sys->fildes(2), "mangaload: can't dial %s: %r\n", addr);
+		raise "fail:dial";
+	}
+	
+	tpid := timers->init(20);
+
+	pids := chan of int;
+	replies := chan [2] of ref Icmp;
+	spawn reader(c.dfd, replies, pids);
+	rpid := <-pids;
+
+	flashoffset := flashblock * FlashBlocksize;
+
+	# file name first
+	bname := array of byte filename;
+	l := len bname;
+	buf := array[Packetdatasize] of byte;
+	ip->put4(buf, 0, filesize);
+	ip->put4(buf, 4, l);
+	buf[8:] = bname;
+	l += 2*4;
+	buf[l++] = byte 0;
+	ip->put4(buf, l, flashoffset);
+	l += 4;
+	{
+		if(send(c.dfd, buf[0:l], Tftp_Write, 0) < 0)
+			senderr();
+		(op, iseq, data) := recv(replies, 400);
+		sys->print("initial reply: %d %d\n", op, iseq);
+		if(op != Tftp_Ack){
+			why := "no response";
+			if(op == Tftp_Error)
+				why = "manga cannot receive file";
+			sys->fprint(sys->fildes(2), "mangaload: %s\n", why);
+			raise "fail:error";
+		}
+		sys->print("sending %s size %d at address %d (0x%x)\n", filename, filesize, flashoffset, flashoffset);
+		seq := 1;
+		nsent := 0;
+		last := 0;
+		while((n := sys->read(fd, buf, len buf)) >= 0 && !last){
+			last = n != len buf;
+		  Retry:
+			for(;;){
+				if(++nsent%10 == 0){	# probe
+					o = Tftp_Req;
+					send(c.dfd, array[0] of byte, Tftp_Req, seq);
+					(op, iseq, data) = recv(replies, 500);
+					if(debug || op != Tftp_Ack)
+						sys->print("ack reply: %d %d\n", op, iseq);
+					if(op == Tftp_Last || op == Tftp_Error){
+						if(op == Tftp_Last)
+							sys->print("timed out\n");
+						else
+							sys->print("error reply\n");
+						raise "disaster";
+					}
+					if(debug)
+						sys->print("ok\n");
+					continue Retry;
+				}
+				send(c.dfd, buf[0:n], Tftp_Data, seq);
+				(op, iseq, data) = recv(replies, 40);
+				case op {
+				Tftp_Error =>
+					sys->fprint(sys->fildes(2), "mangaload: manga refused data\n");
+					raise "disaster";
+				Tftp_Ack =>
+					if(seq == iseq){
+						seq++;
+						break Retry;
+					}
+					sys->print("sequence error: rcvd %d expected %d\n", iseq, seq);
+					if(iseq > seq){
+						sys->print("unrecoverable sequence error\n");
+						send(c.dfd, array[0] of byte, Tftp_Data, ++seq);	# stop manga
+						raise "disaster";
+					}
+					# resend
+					sys->seek(fd, -big ((seq-iseq)*len buf), 1);
+					seq = iseq;
+				Tftp_Last =>
+					seq++;
+					break Retry;	# timeout ok: manga doesn't usually reply unless packet lost
+				}
+			}
+		}
+	}exception{
+	* =>
+		;
+	}
+	kill(rpid);
+	kill(tpid);
+	sys->print("ok?\n");
+}
+
+kill(pid: int)
+{
+	if(pid)
+		sys->fprint(sys->open("#p/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
+
+senderr()
+{
+	sys->fprint(sys->fildes(2), "mangaload: icmp write failed: %r\n");
+	raise "disaster";
+}
+
+send(fd: ref Sys->FD, data: array of byte, op: int, seq: int): int
+{
+	buf := array[64*1024+512] of {* => byte 0};
+	buf[Odata:] = data;
+	ip->put2(buf, Oseq, seq);
+	buf[Otype] = byte InfoRequest;
+	buf[Ocode] = byte op;
+	if(sys->write(fd, buf, Odata+len data) < Odata+len data)
+		return -1;
+	if(debug)
+		sys->print("sent op=%d seq=%d ld=%d\n", op, seq, len data);
+	return 0;
+}
+
+flush(input: chan of ref Icmp)
+{
+	for(;;)alt{
+	<-input =>
+		;
+	* =>
+		return;
+	}
+}
+
+recv(input: chan of ref Icmp, msec: int): (int, int, array of byte)
+{
+	t := Timer.start(msec);
+	alt{
+	<-t.timeout =>
+		return (Tftp_Last, 0, nil);
+	ic := <-input =>
+		t.stop();
+		if(ic.ptype == InfoReply)
+			return (ic.code, ic.seq, ic.data);
+		return (Tftp_Last, 0, nil);
+	}
+}
+
+reader(fd: ref Sys->FD, out: chan of ref Icmp, pid: chan of int)
+{
+	pid <-= sys->pctl(0, nil);
+	for(;;){
+		buf := array[64*1024+512] of byte;
+		n := sys->read(fd, buf, len buf);
+		if(n <= 0){
+			if(n == 0)
+				sys->werrstr("unexpected eof");
+			break;
+		}
+		ic := Icmp.unpack(buf[0:n]);
+		if(ic != nil){
+			if(debug)
+				sys->print("recv type=%d op=%d seq=%d id=%d\n", ic.ptype, ic.code, ic.seq, ic.id);
+			out <-= ic;
+		}else
+			sys->fprint(sys->fildes(2), "mangaload: corrupt icmp packet rcvd\n");
+	}
+	sys->print("read: %r\n");
+	out <-= nil;
+}
+
+# IP and ICMP packet header
+Ovihl: con 0;
+Otos: con 1;
+Olength: con 2;
+Oid: con Olength+2;
+Ofrag: con Oid+2;
+Ottl: con Ofrag+2;
+Oproto: con Ottl+1;
+Oipcksum: con Oproto+1;
+Osrc: con Oipcksum+2;
+Odst: con Osrc+4;
+Otype: con Odst+4;
+Ocode: con Otype+1;
+Ocksum: con Ocode+1;
+Oicmpid: con Ocksum+2;
+Oseq: con Oicmpid+2;
+Odata: con Oseq+2;
+
+Icmp.unpack(b: array of byte): ref Icmp
+{
+	if(len b < Odata)
+		return nil;
+	ic := ref Icmp;
+	ic.ttl = int b[Ottl];
+	ic.src = IPaddr.newv4(b[Osrc:]);
+	ic.dst = IPaddr.newv4(b[Odst:]);
+	ic.ptype = int b[Otype];
+	ic.code = int b[Ocode];
+	ic.seq = ip->get2(b, Oseq);
+	ic.id = ip->get2(b, Oicmpid);
+	ic.munged = 0;
+	if(len b > Odata)
+		ic.data = b[Odata:];
+	return ic;
+}
--- /dev/null
+++ b/appl/cmd/auxi/mkfile
@@ -1,0 +1,24 @@
+<../../../mkconfig
+
+TARG=\
+	cpuslave.dis\
+	digest.dis\
+	fpgaload.dis\
+	mangaload.dis\
+	pcmcia.dis\
+	rdbgsrv.dis\
+	rstyxd.dis\
+
+SYSMODULES=\
+	arg.m\
+	bufio.m\
+	draw.m\
+	sh.m\
+	string.m\
+	strokes.m\
+	styx.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/auxi
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/auxi/pcmcia.b
@@ -1,0 +1,491 @@
+implement Pcmcia;
+
+#
+# Copyright © 1995-2001 Lucent Technologies Inc.  All rights reserved.
+# Revisions Copyright © 2001-2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+	print, fprint: import sys;
+
+include "draw.m";
+
+Pcmcia: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+End:	con 16rFF;
+
+fd: ref Sys->FD;
+stderr: ref Sys->FD;
+pos := 0;
+
+hex := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	if(args != nil)
+		args = tl args;
+	if(args != nil && hd args == "-x"){
+		hex = 1;
+		args = tl args;
+	}
+
+	file := "#y/pcm0attr";
+	if(args != nil)
+		file = hd args;
+
+	fd = sys->open(file, Sys->OREAD);
+	if(fd == nil)
+		fatal(sys->sprint("can't open %s: %r", file));
+
+	for(next := 0; next >= 0;)
+		next = dtuple(next);
+}
+
+fatal(s: string)
+{
+	fprint(stderr, "pcmcia: %s\n", s);
+	raise "fail:error";
+}
+
+readc(): int
+{
+	x := array[1] of byte;
+	sys->seek(fd, big(2*pos), 0);
+	pos++;
+	rv := sys->read(fd, x, 1);
+	if(rv != 1){
+		if(rv < 0)
+			sys->print("readc err: %r\n");
+		return -1;
+	}
+	v := int x[0];
+	if(hex)
+		print("%2.2ux ", v);
+	return v;
+}
+
+dtuple(next: int): int
+{
+	pos = next;
+	if((ttype := readc()) < 0)
+		return -1;
+	if(ttype == End)
+		return -1;
+	if((link := readc()) < 0)
+		return -1;
+	case ttype {
+	* =>	print("unknown tuple type #%2.2ux\n", ttype);
+	16r01 =>	tdevice(ttype, link);
+	16r15 =>	tvers1(ttype, link);
+	16r17 =>	tdevice(ttype, link);
+	16r1A =>	tcfig(ttype, link);
+	16r1B =>	tentry(ttype, link);
+	}
+	if(link == End)
+		next = -1;
+	else
+		next = next+2+link;
+	return next;
+}
+
+speedtab := array[16] of {
+0 => 0,
+1 =>	250,
+2 =>	200,
+3 =>	150,
+4 =>	100,
+};
+
+mantissa := array[16] of {
+1 =>	10,
+2 =>	12,
+3 =>	13,
+4 =>	15,
+5 =>	20,
+6 =>	25,
+7 =>	30,
+8 =>	35,
+9 =>	40,
+10=>	45,
+11=>	50,
+12=>	55,
+13=>	60,
+14=>	70,
+15=>	80,
+};
+
+exponent := array[] of {
+	1,
+	10,
+	100,
+	1000,
+	10000,
+	100000,
+	1000000,
+	10000000,
+};
+
+typetab := array [256] of {
+1=>	"Masked ROM",
+2=>	"PROM",
+3=>	"EPROM",
+4=>	"EEPROM",
+5=>	"FLASH",
+6=>	"SRAM",
+7=>	"DRAM",
+16rD=>	"IO+MEM",
+* => "Unknown",
+};
+
+getlong(size: int): int
+{
+	x := 0;
+	for(i := 0; i < size; i++){
+		if((c := readc()) < 0)
+			break;
+		x |= c<<(i*8);
+	}
+	return x;
+}
+
+tdevice(dtype: int, tlen: int)
+{
+	while(tlen > 0){
+		if((id := readc()) < 0)
+			return;
+		tlen--;
+		if(id == End)
+			return;
+
+		speed := id & 16r7;
+		ns := 0;
+		if(speed == 16r7){
+			if((speed = readc()) < 0)
+				return;
+			tlen--;
+			if(speed & 16r80){
+				if((aespeed := readc()) < 0)
+					return;
+				ns = 0;
+			} else
+				ns = (mantissa[(speed>>3)&16rF]*exponent[speed&7])/10;
+		} else
+			ns = speedtab[speed];
+
+		ttype := id>>4;
+		if(ttype == 16rE){
+			if((ttype = readc()) < 0)
+				return;
+			tlen--;
+		}
+		tname := typetab[ttype];
+		if(tname == nil)
+			tname = "unknown";
+
+		if((size := readc()) < 0)
+			return;
+		tlen--;
+		bytes := ((size>>3)+1) * 512 * (1<<(2*(size&16r7)));
+
+		ttname := "attr device";
+		if(dtype == 1)
+			ttname = "device";
+		print("%s %d bytes of %dns %s\n", ttname, bytes, ns, tname);
+	}
+}
+
+tvers1(nil: int, tlen: int)
+{
+	if((major := readc()) < 0)
+		return;
+	tlen--;
+	if((minor := readc()) < 0)
+		return;
+	tlen--;
+	print("version %d.%d\n", major, minor);
+	while(tlen > 0){
+		s := "";
+		while(tlen > 0){
+			if((c := readc()) < 0)
+				return;
+			tlen--;
+			if(c == 0)
+				break;
+			if(c == End){
+				if(s != "")
+					print("\t%s<missing null>\n", s);
+				return;
+			}
+			s[len s] = c;
+		}
+		print("\t%s\n", s);
+	}
+}
+
+tcfig(nil: int, nil: int)
+{
+	if((size := readc()) < 0)
+		return;
+	rasize := (size&16r3) + 1;
+	rmsize := ((size>>2)&16rf) + 1;
+	if((last := readc()) < 0)
+		return;
+	caddr := getlong(rasize);
+	cregs := getlong(rmsize);
+
+	print("configuration registers at");
+	for(i := 0; i < 16; i++)
+		if((1<<i) & cregs)
+			print(" (%d) #%ux", i, caddr + i*2);
+	print("\n");
+}
+
+intrname := array[16] of {
+0 =>	"memory",
+1 =>	"I/O",
+4 =>	"Custom 0",
+5 =>	"Custom 1",
+6 =>	"Custom 2",
+7 =>	"Custom 3",
+* =>	"unknown"
+};
+
+vexp := array[8] of {
+	1, 10, 100, 1000, 10000, 100000, 1000000, 10000000
+};
+vmant := array[16] of {
+	10, 12, 13, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 70, 80, 90,
+};
+
+volt(name: string)
+{
+	if((c := readc()) < 0)
+		return;
+	exp := vexp[c&16r7];
+	microv := vmant[(c>>3)&16rf]*exp;
+	while(c & 16r80){
+		if((c = readc()) < 0)
+			return;
+		case c {
+		16r7d =>
+			break;		# high impedence when sleeping
+		16r7e or 16r7f =>
+			microv = 0;	# no connection
+		* =>
+			exp /= 10;
+			microv += exp*(c&16r7f);
+		}
+	}
+	print(" V%s %duV", name, microv);
+}
+
+amps(name: string)
+{
+	if((c := readc()) < 0)
+		return;
+	amps := vexp[c&16r7]*vmant[(c>>3)&16rf];
+	while(c & 16r80){
+		if((c = readc()) < 0)
+			return;
+		if(c == 16r7d || c == 16r7e || c == 16r7f)
+			amps = 0;
+	}
+	if(amps >= 1000000)
+		print(" I%s %dmA", name, amps/100000);
+	else if(amps >= 1000)
+		print(" I%s %duA", name, amps/100);
+	else
+		print(" I%s %dnA", name, amps*10);
+}
+
+power(name: string)
+{
+	print("\t%s: ", name);
+	if((feature := readc()) < 0)
+		return;
+	if(feature & 1)
+		volt("nominal");
+	if(feature & 2)
+		volt("min");
+	if(feature & 4)
+		volt("max");
+	if(feature & 8)
+		amps("static");
+	if(feature & 16r10)
+		amps("avg");
+	if(feature & 16r20)
+		amps("peak");
+	if(feature & 16r40)
+		amps("powerdown");
+	print("\n");
+}
+
+ttiming(name: string, scale: int)
+{
+	if((unscaled := readc()) < 0)
+		return;
+	scaled := (mantissa[(unscaled>>3)&16rf]*exponent[unscaled&7])/10;
+	scaled = scaled * vexp[scale];
+	print("\t%s %dns\n", name, scaled);
+}
+
+timing()
+{
+	if((c := readc()) < 0)
+		return;
+	i := c&16r3;
+	if(i != 3)
+		ttiming("max wait", i);
+	i = (c>>2)&16r7;
+	if(i != 7)
+		ttiming("max ready/busy wait", i);
+	i = (c>>5)&16r7;
+	if(i != 7)
+		ttiming("reserved wait", i);
+}
+
+range(asize: int, lsize: int)
+{
+	address := getlong(asize);
+	alen := getlong(lsize);
+	print("\t\t%ux - %ux\n", address, address+alen);
+}
+
+ioaccess := array[] of {
+	0 => " no access",
+	1 => " 8bit access only",
+	2 => " 8bit or 16bit access",
+	3 => " selectable 8bit or 8&16bit access",
+};
+
+iospace(c: int): int
+{
+	print("\tIO space %d address lines%s\n", c&16r1f, ioaccess[(c>>5)&3]);
+	if((c & 16r80) == 0)
+		return -1;
+
+	if((c = readc()) < 0)
+		return -1;
+
+	for(i := (c&16rf)+1; i; i--)
+		range((c>>4)&16r3, (c>>6)&16r3);
+	return 0;
+}
+
+iospaces()
+{
+	if((c := readc()) < 0)
+		return;
+	iospace(c);
+}
+
+irq()
+{
+	if((c := readc()) < 0)
+		return;
+	irqs: int;
+	if(c & 16r10){
+		if((irq1 := readc()) < 0)
+			return;
+		if((irq2 := readc()) < 0)
+			return;
+		irqs = irq1|(irq2<<8);
+	} else
+		irqs = 1<<(c&16rf);
+	level := "";
+	if(c & 16r20)
+		level = " level";
+	pulse := "";
+	if(c & 16r40)
+		pulse = " pulse";
+	shared := "";
+	if(c & 16r80)
+		shared = " shared";
+	print("\tinterrupts%s%s%s", level, pulse, shared);
+	for(i := 0; i < 16; i++)
+		if(irqs & (1<<i))
+			print(", %d", i);
+	print("\n");
+}
+
+memspace(asize: int, lsize: int, host: int)
+{
+	alen := getlong(lsize)*256;
+	address := getlong(asize)*256;
+	if(host){
+		haddress := getlong(asize)*256;
+		print("\tmemory address range #%ux - #%ux hostaddr #%ux\n",
+			address, address+alen, haddress);
+	} else
+		print("\tmemory address range #%ux - #%ux\n", address, address+alen);
+}
+
+misc()
+{
+}
+
+tentry(nil: int, nil: int)
+{
+	if((c := readc()) < 0)
+		return;
+	def := "";
+	if(c & 16r40)
+		def = " (default)";
+	print("configuration %d%s\n", c&16r3f, def);
+	if(c & 16r80){
+		if((i := readc()) < 0)
+			return;
+		tname := intrname[i & 16rf];
+		if(tname == "")
+			tname = sys->sprint("type %d", i & 16rf);
+		attrib := "";
+		if(i & 16r10)
+			attrib += " Battery status active";
+		if(i & 16r20)
+			attrib += " Write Protect active";
+		if(i & 16r40)
+			attrib += " Ready/Busy active";
+		if(i & 16r80)
+			attrib += " Memory Wait required";
+		print("\t%s device, %s\n", tname, attrib);
+	}
+	if((feature := readc()) < 0)
+		return;
+	case feature&16r3 {
+	1 =>
+		power("Vcc");
+	2 =>
+		power("Vcc");
+		power("Vpp");
+	3 =>
+		power("Vcc");
+		power("Vpp1");
+		power("Vpp2");
+	}
+	if(feature&16r4)
+		timing();
+	if(feature&16r8)
+		iospaces();
+	if(feature&16r10)
+		irq();
+	case (feature>>5)&16r3 {
+	1 =>
+		memspace(0, 2, 0);
+	2 =>
+		memspace(2, 2, 0);
+	3 =>
+		if((c = readc()) < 0)
+			return;
+		for(i := 0; i <= (c&16r7); i++)
+			memspace((c>>5)&16r3, (c>>3)&16r3, c&16r80);
+		break;
+	}
+	if(feature&16r80)
+		misc();
+}
--- /dev/null
+++ b/appl/cmd/auxi/rdbgsrv.b
@@ -1,0 +1,222 @@
+implement RDbgSrv;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+
+include "arg.m";
+	arg: Arg;
+
+RDbgSrv: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+debug:=	0;
+dev:=	"/dev/eia0";
+speed:=	38400;
+progname: string;
+rpid := 0;
+wpid := 0;
+
+usage()
+{
+	sys->fprint(stderr(), "Usage: rdbgsrv [-d n] [-s speed] [-f dev] mountpoint\n");
+	raise "fail: usage";
+}
+
+init(nil: ref Draw->Context, av: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil)
+		return;
+	styx = load Styx Styx->PATH;
+	if(styx == nil){
+		sys->fprint(stderr(), "rdbgsrv: can't load %s; %r\n", Styx->PATH);
+		raise "fail:load";
+	}
+	arg = load Arg Arg->PATH;
+	if(arg == nil){
+		sys->fprint(stderr(), "rdbgsrv: can't load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+
+	arg->init(av);
+	progname = arg->progname();
+	while(o := arg->opt())
+		case o {
+		'd' =>
+			d := arg->arg();
+			if(d == nil)
+				usage();
+			debug = int d;
+		's' =>
+			s := arg->arg();
+			if(s == nil)
+				usage();
+			speed = int s;
+		'f' =>
+			s := arg->arg();
+			if(s == nil)
+				usage();
+			dev = s;
+		'h' =>
+			usage();
+		}
+
+	mtpt := arg->arg();
+	if(mtpt == nil)
+		usage();
+
+	ctl := dev + "ctl";
+	cfd := sys->open(ctl, Sys->OWRITE);
+	if(cfd == nil){
+		sys->fprint(stderr(), "%s: can't open %s: %r\n", progname, ctl);
+		raise "fail: open eia\n";
+	}
+
+	sys->fprint(cfd, "b%d", speed);
+	sys->fprint(cfd, "l8");
+	sys->fprint(cfd, "pn");
+	sys->fprint(cfd, "s1");
+
+	(rfd, wfd) := start(dev);
+	if(rfd == nil){
+		sys->fprint(stderr(), "%s: failed to start protocol\n", progname);
+		raise "fail:proto start";
+	}
+
+	fds := array[2] of ref Sys->FD;
+
+	if(sys->pipe(fds) == -1){
+		sys->fprint(stderr(), "%s: pipe: %r\n", progname);
+		raise "fail:no pipe";
+	}
+
+	if(debug)
+		sys->fprint(stderr(), "%s: starting server\n", progname);
+
+	rc := chan of int;
+	spawn copymsg(fds[1], wfd, "->", rc);
+	rpid = <-rc;
+	spawn copymsg(rfd, fds[1], "<-", rc);
+	wpid = <-rc;
+
+	if(sys->mount(fds[0], nil, mtpt, Sys->MREPL, nil) == -1) {
+		fds[1] = nil;
+		sys->fprint(stderr(), "%s: can't mount on %s: %r\n", progname, mtpt);
+		quit("mount");
+	}
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+killpid(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+quit(err: string)
+{
+	killpid(rpid);
+	killpid(wpid);
+	if(err != nil)
+		raise "fail:"+err;
+	exit;
+}
+
+start(name:string): (ref Sys->FD, ref Sys->FD)
+{
+	rfd := sys->open(name, Sys->OREAD);
+	wfd := sys->open(name, Sys->OWRITE);
+	if(rfd == nil || wfd == nil)
+			return (nil, nil);
+	if(sys->fprint(wfd, "go") < 0)
+		return (nil, nil);
+	c := array[1] of byte;
+	state := 0;
+	for(;;) {
+		if(sys->read(rfd, c, 1) != 1)
+			return (nil, nil);
+		if(state == 0 && c[0] == byte 'o')
+			state = 1;
+		else if(state == 1 && c[0] == byte 'k')
+			break;
+		else
+			state = 0;
+	}
+	return (rfd, wfd);
+}
+
+copymsg(f: ref Sys->FD, t: ref Sys->FD, dir: string, pidc: chan of int)
+{
+	pidc <-= sys->pctl(0, nil);
+	
+	{
+		for(;;) {
+			(msg, err) := styx->readmsg(f, 0);
+			if(msg == nil){
+				sys->fprint(stderr(), "%s: %s: read error: %s\n", progname, dir, err);
+				quit("error");
+			}
+			if(debug &1)
+				trace(dir, msg);
+			if(debug & 2)
+				dump(dir, msg, len msg);
+			if(sys->write(t, msg, len msg) != len msg){
+				sys->fprint(stderr(), "%s: %s: write error: %r\n", progname, dir);
+				quit("error");
+			}
+		}
+	}exception e{
+	"*" =>
+		sys->print("%s: %s: %s: exiting\n", progname, dir, e);
+		quit("exception");
+	}
+}
+
+trace(sourcept: string,  op: array of byte ) 
+{
+	if(styx->istmsg(op)){
+		(nil, m) := Tmsg.unpack(op);
+		if(m != nil)
+			sys->print("%s: %s\n", sourcept, m.text());
+		else
+			sys->print("%s: unknown\n", sourcept);
+	}else{
+		(nil, m) := Rmsg.unpack(op);
+		if(m != nil)
+			sys->print("%s: %s\n", sourcept, m.text());
+		else
+			sys->print("%s: unknown\n", sourcept);
+	}
+}
+
+dump(msg: string, buf: array of byte, n: int)
+{
+	sys->print("%s: [%d bytes]: ", msg, n);
+	s := "";
+	for(i:=0;i<n;i++) {
+		if((i % 20) == 0) {
+			sys->print(" %s\n", s);
+			s = "";
+		}
+		sys->print("%2.2x ", int buf[i]);
+		if(int buf[i] >= 32 && int buf[i] < 127)
+			s[len s] = int buf[i];
+		else
+			s += ".";
+	}
+	for(i %= 20; i < 20; i++)
+		sys->print("   ");
+	sys->print(" %s\n\n", s);
+}
--- /dev/null
+++ b/appl/cmd/auxi/rstyxd.b
@@ -1,0 +1,110 @@
+implement Rstyxd;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+include "string.m";
+
+sys: Sys;
+str: String;
+stderr: ref Sys->FD;
+
+Rstyxd: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+#
+# argv is a list of Inferno supported algorithms from Security->Auth
+#
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	if (str == nil)
+		badmod(String->PATH);
+
+	fd := sys->fildes(0);
+	stderr = sys->fildes(2);
+	sys->pctl(sys->FORKFD, fd.fd :: nil);
+
+	args := readargs(fd);
+	if(args == nil)
+		err(sys->sprint("error reading arguments: %r"));
+
+	cmd := hd args;
+	s := "";
+	for (a := args; a != nil; a = tl a)
+		s += hd a + " ";
+	sys->fprint(stderr, "rstyxd: cmd: %s\n", s);
+	s = nil;
+	file: string;
+	if(cmd == "sh")
+		file = "/dis/sh.dis";
+	else
+		file = cmd + ".dis";
+	mod := load Command file;
+	if(mod == nil){
+		mod = load Command "/dis/"+file;
+		if(mod == nil)
+			badmod("/dis/"+file);
+	}
+
+	sys->pctl(Sys->FORKNS|Sys->FORKENV, nil);
+
+	if(sys->mount(fd, nil, "/n/client", Sys->MREPL, "") < 0)
+		err(sys->sprint("cannot mount connection on /n/client: %r"));
+
+	if(sys->bind("/n/client/dev", "/dev", Sys->MBEFORE) < 0)
+		err(sys->sprint("cannot bind /n/client/dev to /dev: %r"));
+
+	fd = sys->open("/dev/cons", sys->OREAD);
+	sys->dup(fd.fd, 0);
+	fd = sys->open("/dev/cons", sys->OWRITE);
+	sys->dup(fd.fd, 1);
+	sys->dup(fd.fd, 2);
+	fd = nil;
+
+	mod->init(nil, args);
+}
+
+readargs(fd: ref Sys->FD): list of string
+{
+	buf := array[1024] of byte;
+	c := array[1] of byte;
+	for(i:=0; ; i++){
+		if(i>=len buf || sys->read(fd, c, 1)!=1)
+			return nil;
+		buf[i] = c[0];
+		if(c[0] == byte '\n')
+			break;
+	}
+	nb := int string buf[0:i];
+	if(nb <= 0)
+		return nil;
+	args := readn(fd, nb);
+	if (args == nil)
+		return nil;
+	return str->unquoted(string args[0:nb]);
+}
+
+readn(fd: ref Sys->FD, nb: int): array of byte
+{
+	buf:= array[nb] of byte;
+	if(sys->readn(fd, buf, nb) != nb)
+		return nil;
+	return buf;
+}
+
+
+err(s: string)
+{
+	sys->fprint(stderr, "rstyxd: %s\n", s);
+	raise "fail:error";
+}
+
+badmod(s: string)
+{
+	sys->fprint(stderr, "rstyxd: can't load %s: %r\n", s);
+	raise "fail:load";
+}
--- /dev/null
+++ b/appl/cmd/avr/burn.b
@@ -1,0 +1,859 @@
+implement Burn;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "timers.m";
+	timers: Timers;
+	Timer: import timers;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+
+Burn: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Avr: adt {
+	id:	int;
+	rev:	int;
+	flashsize:	int;
+	eepromsize:	int;
+	fusebytes:	int;
+	lockbytes:	int;
+	serfprog:	int;	# serial fuse programming support
+	serlprog:	int;	# serial lockbit programming support
+	serflread:	int;	# serial fuse/lockbit reading support
+	commonlfr:	int;	# lockbits and fuses are combined
+	sermemprog:	int;	# serial memory programming support
+	pagesize:	int;
+	eeprompagesize:	int;
+	selftimed:	int;	# all instructions are self-timed
+	fullpar:	int;	# part has full parallel interface
+	polling:	int;	# polling can be used during SPI access
+	fpoll:	int;	# flash poll value
+	epoll1:	int;	# eeprom poll value 1
+	epoll2:	int;	# eeprom poll value 2
+	name:	string;
+	signalpagel:	int;	# posn of PAGEL signal (16rD7 by default)
+	signalbs2:	int;	# posn of BS2 signal (16rA0 by default)
+};
+
+F, T: con iota;
+ATMEGA128: con 16rB2;	# 128k devices
+
+avrs: array of Avr = array[] of {
+	(ATMEGA128,  1, 131072, 4096, 3, 1, T,  T,  T,  F, T,  256, 8,  T,  T,  T,  16rFF, 16rFF, 16rFF, "ATmega128",   16rD7, 16rA0),
+};
+
+sfd: ref Sys->FD;
+cfd: ref Sys->FD;
+rd: ref Rd;
+mib510 := 1;
+
+Rd: adt {
+	c:	chan of array of byte;
+	pid:	int;
+	fd:	ref Sys->FD;
+	buf:	array of byte;
+	new:	fn(fd: ref Sys->FD): ref Rd;
+	read:	fn(r: self ref Rd, ms: int): array of byte;
+	readn:	fn(r: self ref Rd, n: int, ms: int): array of byte;
+	flush:	fn(r: self ref Rd);
+	stop:	fn(r: self ref Rd);
+	reader:	fn(r: self ref Rd, c: chan of int);
+};
+
+debug := 0;
+verify := 0;
+erase := 1;
+ignore := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = ckl(load Bufio Bufio->PATH, Bufio->PATH);
+	str = ckl(load String String->PATH, String->PATH);
+	timers = ckl(load Timers Timers->PATH, Timers->PATH);
+
+	serial := "/dev/eia0";
+	fuseext := -1;
+	fuselow := -1;
+	fusehigh := -1;
+	arg := ckl(load Arg Arg->PATH, Arg->PATH);
+	arg->init(args);
+	arg->setusage("burn [-rD] [-d serialdev] file.out");
+	while((o := arg->opt()) != 0)
+		case o {
+		'D' =>	debug++;
+		'e' =>	erase = 0;
+		'r' =>	verify = 1;
+		'd' =>	serial = arg->earg();
+		'i' =>	ignore = 1;
+		'E' =>	fuseext = fuseval(arg->earg());
+		'L' =>	fuselow = fuseval(arg->earg());
+		'H' =>	fusehigh = fuseval(arg->earg());
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+	arg = nil;
+
+	sfile := hd args;
+	fd := bufio->open(sfile, Sys->OREAD);
+	if(fd == nil)
+		err(sys->sprint("can't open %s: %r", sfile));
+			
+	timers->init(2);
+	sfd = sys->open(serial, Sys->ORDWR);
+	if(sfd == nil)
+		err(sys->sprint("can't open %s: %r", "/dev/eia0"));
+	cfd = sys->open(serial+"ctl", Sys->ORDWR);
+	sys->fprint(cfd, "f");
+	sys->fprint(cfd, "b115200");
+	sys->fprint(cfd, "i8");
+#	sys->fprint(cfd, "f\nb115200\ni8");
+	rd = Rd.new(sfd);
+
+	initialise();
+	if(fuseext >= 0 || fuselow >= 0 || fusehigh >= 0){
+		if(fuselow >= 0 && (fuselow & 16rF) == 0)
+			err("don't program external clock");
+		if(fuseext >= 0 && (fuseext & (1<<0)) == 0)
+			err("don't program ATmega103 compatibility");
+		if(fusehigh >= 0 && (fusehigh & (1<<7)) == 0)
+			err("don't program OCDEN=0");
+		if(fusehigh >= 0 && writefusehigh(fusehigh) >= 0)
+			sys->print("set fuse high=%.2ux\n", fusehigh);
+		if(fuselow >= 0 && writefuselow(fuselow) >= 0)
+			sys->print("set fuse low=%.2ux\n", fuselow);
+		if(fuseext >= 0 && writefuseext(fuseext) >= 0)
+			sys->print("set fuse ext=%.2ux\n", fuseext);
+		shutdown();
+		exit;
+	}
+
+	if(!verify && erase){
+		chiperase();
+		sys->print("Erased flash\n");
+	}
+
+	totbytes := 0;
+	while((l := fd.gets('\n')) != nil){
+		(c, addr, data) := sdecode(l);
+		if(c >= '1' && c <= '3'){
+			if(verify){
+				fdata := readflashdata(addr, len data);
+				if(!eq(fdata, data))
+					sys->print("mismatch: %d::%d at %4.4ux\n", len data, len fdata, addr);
+			}else if(writeflashdata(addr, data) != len data)
+				err("failed to program device");
+			totbytes += len data;
+		} else if(c == '0')
+			sys->print("title: %q\n", string data);
+	}
+	if(!verify){
+		flushpage();
+		sys->print("Programmed %ud (0x%4.4ux) bytes\n", totbytes, totbytes);
+	}
+
+	shutdown();
+}
+
+ckl[T](m: T, s: string): T
+{
+	if(m == nil)
+		err(sys->sprint("can't load %s: %r", s));
+	return m;
+}
+
+fuseval(s: string): int
+{
+	(n, t) := str->toint(s, 16);
+	if(t != nil || n < 0 || n > 255)
+		err("illegal fuse value");
+	return n;
+}
+
+cache: (int, array of byte);
+
+readflashdata(addr: int, nbytes: int): array of byte
+{
+	data := array[nbytes] of byte;
+	ia := addr;
+	ea := addr+nbytes;
+	while(addr < ea){
+		(ca, cd) := cache;
+		if(addr >= ca && addr < ca+len cd){
+			n := nbytes;
+			o := addr-ca;
+			if(o+n > len cd)
+				n = len cd - o;
+			if(addr-ia+n > len data)
+				n = len data - (addr-ia);
+			data[addr-ia:] = cd[o:o+n];
+			addr += n;
+		}else{
+			ca = addr & ~16rFF;
+			cd = readflashpage(ca, 16r100);
+			cache = (ca, cd);
+		}
+	}
+	return data;
+}
+
+writeflashdata(addr: int, data: array of byte): int
+{
+	pagesize := avrs[0].pagesize;
+	ia := addr;
+	ea := addr+len data;
+	while(addr < ea){
+		(ca, cd) := cache;
+		if(addr >= ca && addr < ca+len cd){
+			n := len data;
+			o := addr-ca;
+			if(o+n > len cd)
+				n = len cd - o;
+			cd[o:] = data[0:n];
+			addr += n;
+			data = data[n:];
+		}else{
+			if(flushpage() < 0)
+				break;
+			cache = (addr & ~16rFF, array[pagesize] of {* => byte 16rFF});
+		}
+	}
+	return addr-ia;
+}
+
+flushpage(): int
+{
+	(ca, cd) := cache;
+	if(len cd == 0)
+		return 0;
+	cache = (0, nil);
+	if(writeflashpage(ca, cd) != len cd)
+		return -1;
+	return len cd;
+}
+			
+shutdown()
+{
+#	setisp(0);
+	if(rd != nil){
+		rd.stop();
+		rd = nil;
+	}
+	if(timers != nil)
+		timers->shutdown();
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "burn: %s\n", s);
+	shutdown();
+	raise "fail:error";
+}
+
+dump(a: array of byte): string
+{
+	s := sys->sprint("[%d]", len a);
+	for(i := 0; i < len a; i++)
+		s += sys->sprint(" %.2ux", int a[i]);
+	return s;
+}
+
+initialise()
+{
+	if(mib510){
+		# MIB510-specific: switch rs232 to STK500
+		for(i:=0; i<8; i++){
+			setisp0(1);
+			sys->sleep(10);
+			rd.flush();
+			if(setisp(1))
+				break;
+		}
+		if(!setisp(1))
+			err("no response from programmer");
+	}
+	resync();
+	resync();
+	if(!mib510){
+		r := rpc(array[] of {Cmd_STK_GET_SIGN_ON}, 7);
+		if(r != nil)
+			sys->print("got: %q\n", string r);
+	}
+	r := readsig();
+	if(len r > 0 && r[0] != byte 16rFF)
+		sys->print("sig: %s\n", dump(r));
+	(min, maj) := version();
+	sys->print("Firmware version: %s.%s\n", min, maj);
+	setdevice(avrs[0]);
+	pgmon();
+	r = readsig();
+	sys->print("sig: %s\n", dump(r));
+	pgmoff();
+	if(len r < 3 || r[0] != byte 16r1e || r[1] != byte 16r97 || r[2] != byte 16r02)
+		if(!ignore)
+		err("unlikely response: check connections");
+
+	# could set voltages here...
+	sys->print("fuses: h=%.2ux l=%.2ux e=%.2ux\n", readfusehigh(), readfuselow(), readfuseext());
+}
+
+resync()
+{
+	for(i := 0; i < 8; i++){
+		rd.flush();
+		r := rpc(array[] of {Cmd_STK_GET_SYNC}, 0);
+		if(r != nil)
+			return;
+	}
+	err("lost sync with programmer");
+}
+
+getparam(p: byte): int
+{
+	r := rpc(array[] of {Cmd_STK_GET_PARAMETER, p}, 1);
+	if(len r > 0)
+		return int r[0];
+	return -1;
+}
+
+version(): (string, string)
+{
+	maj := getparam(Parm_STK_SW_MAJOR);
+	min := getparam(Parm_STK_SW_MINOR);
+	if(mib510)
+		return (sys->sprint("%c", maj), sys->sprint("%c", min));
+	return (sys->sprint("%d", maj), sys->sprint("%d", min));
+}
+
+eq(a, b: array of byte): int
+{
+	if(len a != len b)
+		return 0;
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+#
+# Motorola S records
+#
+
+badsrec(s: string)
+{
+	err("bad S record: "+s);
+}
+
+hexc(c: int): int
+{
+	if(c >= '0' && c <= '9')
+		return c-'0';
+	if(c >= 'a' && c <= 'f')
+		return c-'a'+10;
+	if(c >= 'A' && c <= 'F')
+		return c-'A'+10;
+	return -1;
+}
+
+g8(s: string): int
+{
+	if(len s >= 2){
+		c0 := hexc(s[0]);
+		c1 := hexc(s[1]);
+		if(c0 >= 0 && c1 >= 0)
+			return (c0<<4) | c1;
+	}
+	return -1;
+}
+
+# S d len 
+sdecode(s: string): (int, int, array of byte)
+{
+	while(len s > 0 && (s[len s-1] == '\r' || s[len s-1] == '\n'))
+		s = s[0:len s-1];
+	if(len s < 4 || s[0] != 'S')
+		badsrec(s);
+	l := g8(s[2:4]);
+	if(l < 0)
+		badsrec("length: "+s);
+	if(2*l != len s - 4)
+		badsrec("length: "+s);
+	csum := l;
+	na := 2;
+	if(s[1] >= '1' && s[1] <= '3')
+		na = s[1]-'1'+2;
+	addr := 0;
+	for(i:=0; i<na; i++){
+		b := g8(s[4+i*2:]);
+		if(b < 0)
+			badsrec(s);
+		csum += b;
+		addr = (addr << 8) | b;
+	}
+	case s[1] {
+	'0' or		# used as segment name (seems to be srec file name with TinyOS)
+	'1' to '3' or	# data
+	'5' or		# plot so far
+	'7' to '9' =>	# end/start address
+		;
+	* =>
+		badsrec("type: "+s);
+	}
+	data := array[l-na-1] of byte;
+	for(i = 0; i < len data; i++){
+		c := g8(s[4+(na+i)*2:]);
+		csum += c;
+		data[i] = byte c;
+	}
+	v := g8(s[4+l*2-2:]);
+	csum += v;
+	if((csum & 16rFF) != 16rFF)
+		badsrec("checksum: "+s);
+	return (s[1], addr, data);
+}
+
+#
+# serial port
+#
+
+Rd.new(fd: ref Sys->FD): ref Rd
+{
+	r := ref Rd(chan[4] of array of byte, 0, fd, nil);
+	c := chan of int;
+	spawn r.reader(c);
+	<-c;
+	return r;
+}
+
+Rd.reader(r: self ref Rd, c: chan of int)
+{
+	r.pid = sys->pctl(0, nil);
+	c <-= 1;
+	for(;;){
+		buf := array[258] of byte;
+		n := sys->read(r.fd, buf, len buf);
+		if(n <= 0){
+			r.pid = 0;
+			err(sys->sprint("read error: %r"));
+		}
+		if(debug)
+			sys->print("<- %s\n", dump(buf[0:n]));
+		r.c <-= buf[0:n];
+	}
+}
+
+Rd.read(r: self ref Rd, ms: int): array of byte
+{
+	if((a := r.buf) != nil){
+		r.buf = nil;
+		return a;
+	}
+	t := Timer.start(ms);
+	alt{
+	a = <-r.c =>
+		t.stop();
+	    Acc:
+		for(;;){
+			sys->sleep(5);
+			alt{
+			b := <-r.c =>
+				if(b == nil)
+					break Acc;
+				a = cat(a, b);
+			* =>
+				break Acc;
+			}
+		}
+		return a;
+	<-t.timeout =>
+		return nil;
+	}
+}
+
+Rd.readn(r: self ref Rd, n: int, ms: int): array of byte
+{
+	a: array of byte;
+
+	while((need := n - len a) > 0){
+		b := r.read(ms);
+		if(b == nil)
+			break;
+		if(len b > need){
+			r.buf = b[need:];
+			b = b[0:need];
+		}
+		a = cat(a, b);
+	}
+	return a;
+}
+
+Rd.flush(r: self ref Rd)
+{
+	r.buf = nil;
+	sys->sleep(5);
+	for(;;){
+		alt{
+		<-r.c =>
+			;
+		* =>
+			return;
+		}
+	}
+}
+
+Rd.stop(r: self ref Rd)
+{
+	pid := r.pid;
+	if(pid){
+		fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+		if(fd != nil)
+			sys->fprint(fd, "kill");
+	}
+}
+
+cat(a, b: array of byte): array of byte
+{
+	if(len b == 0)
+		return a;
+	if(len a == 0)
+		return b;
+	c := array[len a + len b] of byte;
+	c[0:] = a;
+	c[len a:] = b;
+	return c;
+}
+
+#
+# STK500 communication protocol
+#
+
+STK_SIGN_ON_MESSAGE: con "AVR STK";   # Sign on string for Cmd_STK_GET_SIGN_ON
+
+# Responses
+
+Resp_STK_OK: con byte 16r10;
+Resp_STK_FAILED: con byte 16r11;
+Resp_STK_UNKNOWN: con byte 16r12;
+Resp_STK_NODEVICE: con byte 16r13;
+Resp_STK_INSYNC: con byte 16r14;
+Resp_STK_NOSYNC: con byte 16r15;
+
+Resp_ADC_CHANNEL_ERROR: con byte 16r16;
+Resp_ADC_MEASURE_OK: con byte 16r17;
+Resp_PWM_CHANNEL_ERROR: con byte 16r18;
+Resp_PWM_ADJUST_OK: con byte 16r19;
+
+# Special constants
+
+Sync_CRC_EOP: con byte 16r20;
+
+# Commands
+
+Cmd_STK_GET_SYNC: con byte 16r30;
+Cmd_STK_GET_SIGN_ON: con byte 16r31;
+
+Cmd_STK_SET_PARAMETER: con byte 16r40;
+Cmd_STK_GET_PARAMETER: con byte 16r41;
+Cmd_STK_SET_DEVICE: con byte 16r42;
+Cmd_STK_SET_DEVICE_EXT: con byte 16r45;
+
+Cmd_STK_ENTER_PROGMODE: con byte 16r50;
+Cmd_STK_LEAVE_PROGMODE: con byte 16r51;
+Cmd_STK_CHIP_ERASE: con byte 16r52;
+Cmd_STK_CHECK_AUTOINC: con byte 16r53;
+Cmd_STK_LOAD_ADDRESS: con byte 16r55;
+Cmd_STK_UNIVERSAL: con byte 16r56;
+Cmd_STK_UNIVERSAL_MULTI: con byte 16r57;
+
+Cmd_STK_PROG_FLASH: con byte 16r60;
+Cmd_STK_PROG_DATA: con byte 16r61;
+Cmd_STK_PROG_FUSE: con byte 16r62;
+Cmd_STK_PROG_LOCK: con byte 16r63;
+Cmd_STK_PROG_PAGE: con byte 16r64;
+Cmd_STK_PROG_FUSE_EXT: con byte 16r65;
+
+Cmd_STK_READ_FLASH: con byte 16r70;
+Cmd_STK_READ_DATA: con byte 16r71;
+Cmd_STK_READ_FUSE: con byte 16r72;
+Cmd_STK_READ_LOCK: con byte 16r73;
+Cmd_STK_READ_PAGE: con byte 16r74;
+Cmd_STK_READ_SIGN: con byte 16r75;
+Cmd_STK_READ_OSCCAL: con byte 16r76;
+Cmd_STK_READ_FUSE_EXT: con byte 16r77;
+Cmd_STK_READ_OSCCAL_EXT: con byte 16r78;
+
+# Parameter constants
+
+Parm_STK_HW_VER: con byte 16r80; # ' ' - R
+Parm_STK_SW_MAJOR: con byte 16r81; # ' ' - R
+Parm_STK_SW_MINOR: con byte 16r82; # ' ' - R
+Parm_STK_LEDS: con byte 16r83; # ' ' - R/W
+Parm_STK_VTARGET: con byte 16r84; # ' ' - R/W
+Parm_STK_VADJUST: con byte 16r85; # ' ' - R/W
+Parm_STK_OSC_PSCALE: con byte 16r86; # ' ' - R/W
+Parm_STK_OSC_CMATCH: con byte 16r87; # ' ' - R/W
+Parm_STK_RESET_DURATION: con byte 16r88; # ' ' - R/W
+Parm_STK_SCK_DURATION: con byte 16r89; # ' ' - R/W
+
+Parm_STK_BUFSIZEL: con byte 16r90; # ' ' - R/W, Range {0..255}
+Parm_STK_BUFSIZEH: con byte 16r91; # ' ' - R/W, Range {0..255}
+Parm_STK_DEVICE: con byte 16r92; # ' ' - R/W, Range {0..255}
+Parm_STK_PROGMODE: con byte 16r93; # ' ' - 'P' or 'S'
+Parm_STK_PARAMODE: con byte 16r94; # ' ' - TRUE or FALSE
+Parm_STK_POLLING: con byte 16r95; # ' ' - TRUE or FALSE
+Parm_STK_SELFTIMED: con byte 16r96; # ' ' - TRUE or FALSE
+
+# status bits
+
+Stat_STK_INSYNC: con byte 16r01; # INSYNC status bit, '1' - INSYNC
+Stat_STK_PROGMODE: con byte 16r02; # Programming mode,  '1' - PROGMODE
+Stat_STK_STANDALONE: con byte 16r04; # Standalone mode,   '1' - SM mode
+Stat_STK_RESET: con byte 16r08; # RESET button,      '1' - Pushed
+Stat_STK_PROGRAM: con byte 16r10; # Program button, '   1' - Pushed
+Stat_STK_LEDG: con byte 16r20; # Green LED status,  '1' - Lit
+Stat_STK_LEDR: con byte 16r40; # Red LED status,    '1' - Lit
+Stat_STK_LEDBLINK: con byte 16r80; # LED blink ON/OFF,  '1' - Blink
+
+ispmode := array[] of {byte 16rAA, byte 16r55, byte 16r55, byte 16rAA, byte 16r17, byte 16r51, byte 16r31, byte 16r13,  byte 0};	# last byte is 1 to switch isp on 0 to switch off
+
+ck(r: array of byte)
+{
+	if(r == nil)
+		err("programming failed");
+}
+
+pgmon()
+{
+	ck(rpc(array[] of {Cmd_STK_ENTER_PROGMODE}, 0));
+}
+
+pgmoff()
+{
+	ck(rpc(array[] of {Cmd_STK_LEAVE_PROGMODE}, 0));
+}
+
+setisp0(on: int)
+{
+	rd.flush();
+	buf := array[len ispmode] of byte;
+	buf[0:] = ispmode;
+	buf[8] = byte on;
+	sys->write(sfd, buf, len buf);
+}
+
+setisp(on: int): int
+{
+	rd.flush();
+	buf := array[len ispmode] of byte;
+	buf[0:] = ispmode;
+	buf[8] = byte on;
+	r := send(buf, 2);
+	return len r == 2 && ok(r);
+}
+
+readsig(): array of byte
+{
+	r := send(array[] of {Cmd_STK_READ_SIGN, Sync_CRC_EOP}, 5);
+	# doesn't behave as documented in AVR061: it repeats the command bytes instead
+	if(len r != 5 || r[0] != Cmd_STK_READ_SIGN || r[4] != Sync_CRC_EOP){
+		sys->fprint(sys->fildes(2), "bad reply %s\n", dump(r));
+		return nil;
+	}
+	return r[1:len r-1];	# trim proto bytes
+}
+
+pgrpc(a: array of byte, repn: int): array of byte
+{
+	pgmon();
+	r := rpc(a, repn);
+	pgmoff();
+	return r;
+}
+
+eop := array[] of {Sync_CRC_EOP};
+
+rpc(a: array of byte, repn: int): array of byte
+{
+	r := send(cat(a, eop), repn+2);
+	if(!ok(r)){
+		if(len r >= 2 && r[0] == Resp_STK_INSYNC && r[len r-1] == Resp_STK_NODEVICE)
+			err("internal error: programming parameters not correctly set");
+		if(len r >= 1 && r[0] == Resp_STK_NOSYNC)
+			err("lost synchronisation");
+		sys->fprint(sys->fildes(2), "bad reply %s\n", dump(r));
+		return nil;
+	}
+	return r[1:len r-1];	# trim sync bytes
+}
+
+send(a: array of byte, repn: int): array of byte
+{
+	if(debug)
+		sys->print("-> %s\n", dump(a));
+	if(sys->write(sfd, a, len a) != len a)
+		err(sys->sprint("write error: %r"));
+	return rd.readn(repn, 2000);
+}
+
+ok(r: array of byte): int
+{
+	return len r >= 2 && r[0] == Resp_STK_INSYNC && r[len r -1] == Resp_STK_OK;
+}
+
+universal(req: array of byte): int
+{
+	r := pgrpc(cat(array[] of {Cmd_STK_UNIVERSAL}, req), 1);
+	if(r == nil)
+		return -1;
+	return int r[0];
+}
+
+setdevice(d: Avr)
+{
+	b := array[] of {
+		Cmd_STK_SET_DEVICE,
+		byte d.id,
+		byte d.rev,
+		byte 0,	# prog type (CHECK)
+		byte d.fullpar,
+		byte d.polling,
+		byte d.selftimed,
+		byte d.lockbytes,
+		byte d.fusebytes,
+		byte d.fpoll,
+		byte d.fpoll,
+		byte d.epoll1,
+		byte d.epoll2,
+		byte (d.pagesize >> 8), byte d.pagesize,
+		byte (d.eepromsize>>8), byte d.eepromsize,
+		byte (d.flashsize>>24), byte (d.flashsize>>16), byte (d.flashsize>>8), byte d.flashsize
+	};
+	ck(rpc(b, 0));
+	if(mib510)
+		return;
+	b = array[] of {
+		Cmd_STK_SET_DEVICE_EXT,
+		byte 4,
+		byte d.eeprompagesize,
+		byte d.signalpagel,
+		byte d.signalbs2,
+		byte 0	# ResetDisable
+	};
+	ck(rpc(b, 0));
+}
+
+chiperase()
+{
+	ck(pgrpc(array[] of {Cmd_STK_CHIP_ERASE}, 0));
+}
+
+readfuselow(): int
+{
+	return universal(array[] of {byte 16r50, byte 0, byte 0, byte 0});
+}
+
+readfusehigh(): int
+{
+	return universal(array[] of {byte 16r58, byte 8, byte 0, byte 0});
+}
+
+readfuseext(): int
+{
+	return universal(array[] of {byte 16r50, byte 8, byte 0, byte 0});
+}
+
+readlockfuse(): int
+{
+	return universal(array[] of {byte 16r58, byte 0, byte 0, byte 0});
+}
+
+readflashpage(addr: int, nb: int): array of byte
+{
+	return readmem('F', addr/2, nb);
+}
+
+readeeprompage(addr: int, nb: int): array of byte
+{
+	return readmem('E', addr, nb);
+}
+
+readmem(memtype: int, addr: int, nb: int): array of byte
+{
+	if(nb > 256)
+		nb = 256;
+	pgmon();
+	r := rpc(array[] of {Cmd_STK_LOAD_ADDRESS, byte addr, byte (addr>>8)}, 0);
+	if(r != nil){
+		r = send(array[] of {Cmd_STK_READ_PAGE, byte (nb>>8), byte nb, byte memtype, Sync_CRC_EOP}, nb+2);
+		l := len r;
+		# AVR601 says last byte should be Resp_STK_OK but it's not, at least on MIB; check for both
+		if(l >= 2 && r[0] == Resp_STK_INSYNC && (r[l-1] == Resp_STK_INSYNC || r[l-1] == Resp_STK_OK))
+			r = r[1:l-1];	# trim framing bytes
+		else{
+			sys->print("bad reply: %s\n", dump(r));
+			r = nil;
+		}
+		if(len r < nb)
+			sys->print("short [%d@%4.4ux]\n", nb, addr);
+	}
+	pgmoff();
+	return r;
+}
+
+writeflashpage(addr: int, data: array of byte): int
+{
+	return writemem('F', addr/2, data);
+}
+
+writeeeprompage(addr: int, data: array of byte): int
+{
+	return writemem('E', addr, data);
+}
+
+writemem(memtype: int, addr: int, data: array of byte): int
+{
+	nb := len data;
+	if(nb > 256){
+		nb = 256;
+		data = data[0:nb];
+	}
+	pgmon();
+	r := rpc(array[] of {Cmd_STK_LOAD_ADDRESS, byte addr, byte (addr>>8)}, 0);
+	if(r != nil){
+		r = rpc(cat(array[] of {Cmd_STK_PROG_PAGE, byte (nb>>8), byte nb, byte memtype},data), 0);
+		if(r == nil)
+			nb = -1;
+	}
+	pgmoff();
+	return nb;
+}
+
+writefuseext(v: int): int
+{
+	return universal(array[] of {byte 16rAC, byte 16rA4, byte 16rFF, byte v});
+}
+
+writefuselow(v: int): int
+{
+	return universal(array[] of {byte 16rAC, byte 16rA0, byte 16rFF, byte v});
+}
+
+writefusehigh(v: int): int
+{
+	return universal(array[] of {byte 16rAC, byte 16rA8, byte 16rFF, byte v});
+}
--- /dev/null
+++ b/appl/cmd/avr/mkfile
@@ -1,0 +1,10 @@
+<../../../mkconfig
+
+TARG=\
+	burn.dis\
+
+SYSMODULES=\
+
+DISBIN=$ROOT/dis/avr
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/basename.b
@@ -1,0 +1,50 @@
+implement Basename;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "names.m";
+	names: Names;
+
+include "arg.m";
+
+Basename: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	names = load Names Names->PATH;
+	arg := load Arg Arg->PATH;
+
+	dirname := 0;
+	arg->init(args);
+	arg->setusage("basename [-d] string [suffix]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' =>
+			dirname = 1;
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil || tl args != nil && (dirname || tl tl args != nil))
+		arg->usage();
+	arg = nil;
+
+	if(dirname){
+		s := names->dirname(hd args);
+		if(s == nil)
+			s = ".";
+		sys->print("%s\n", s);
+		exit;
+	}
+	suffix: string;
+	if(tl args != nil)
+		suffix = hd tl args;
+	sys->print("%s\n", names->basename(hd args, suffix));
+}
--- /dev/null
+++ b/appl/cmd/bind.b
@@ -1,0 +1,66 @@
+implement Bind;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+Bind: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+stderr: ref Sys->FD;
+
+usage()
+{
+	sys->fprint(stderr, "usage: bind [-a|-b|-c|-ac|-bc] [-q] source target\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	stderr = sys->fildes(2);
+	flags := 0;
+	qflag := 0;
+	if(args != nil)
+		args = tl args;
+	while(args != nil && (a := hd args) != "" && a[0] == '-'){
+		args = tl args;
+		if(a == "--")
+			break;
+		for(o := 1; o < len a; o++)
+			case a[o] {
+			'a' =>
+				flags |= Sys->MAFTER;
+			'b' =>
+				flags |= Sys->MBEFORE;
+			'c' =>
+				flags |= Sys->MCREATE;
+			'q' =>
+				qflag = 1;
+			* =>
+				usage();
+			}
+	}
+	if(len args != 2 || flags&Sys->MAFTER && flags&Sys->MBEFORE)
+		usage();
+
+	f1 := hd args;
+	f2 := hd tl args;
+	if(sys->bind(f1, f2, flags) < 0){
+		if(qflag)
+			exit;
+		#  try to improve the error message
+		err := sys->sprint("%r");
+		if(sys->stat(f1).t0 < 0)
+			sys->fprint(stderr, "bind: %s: %r\n", f1);
+		else if(sys->stat(f2).t0 < 0)
+			sys->fprint(stderr, "bind: %s: %r\n", f2);
+		else
+			sys->fprint(stderr, "bind: cannot bind %s onto %s: %s\n", f1, f2, err);
+		raise "fail:bind";
+	}
+}
--- /dev/null
+++ b/appl/cmd/bit2gif.b
@@ -1,0 +1,86 @@
+#
+# bit2gif -
+#
+# A simple command line utility for converting inferno bitmaps
+# to gif images.
+#
+# Craig Newell, Jan. 1999	CraigN@cheque.uq.edu.au
+#
+implement bit2gif;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display: import draw;
+include "string.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "imagefile.m";
+
+bit2gif : module
+{
+	init: fn(ctx: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->print("usage: bit2gif <inferno bitmap>\n");
+	exit;
+}	
+
+init(ctx: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	# check arguments
+	if (argv == nil) 
+		usage();
+	argv = tl argv;
+	if (argv == nil)
+		usage();
+	s := hd argv;
+	if (len s && s[0] == '-')
+		usage();
+
+	# load the modules	
+	str := load String String->PATH;
+	draw = load Draw Draw->PATH;
+	bufio = load Bufio Bufio->PATH;
+	imgfile := load WImagefile WImagefile->WRITEGIFPATH;
+	imgfile->init(bufio);
+
+	# open the display
+	display: ref Draw->Display;
+	if (ctx == nil) {
+		display = Display.allocate(nil);
+	} else {
+		display = ctx.display;
+	}
+
+	# process all the files 
+	while (argv != nil) {
+	
+		# get the filenames		
+		bit_name := hd argv;
+		(gif_name, nil) := str->splitstrl(bit_name, ".bit");
+		gif_name = gif_name + ".gif";
+
+		# load inferno bitmap
+		img := display.open(bit_name);
+		if (img == nil) {
+			sys->print("bit2gif: unable to read <%s>\n", bit_name);
+		} else {
+			# save as gif
+			o := bufio->create(gif_name, Bufio->OWRITE, 8r644);
+			if (o != nil) {
+				imgfile->writeimage(o, img);
+				o.close();
+			}
+		}
+
+		# next argument
+		argv = tl argv;
+	}
+}
--- /dev/null
+++ b/appl/cmd/bytes.b
@@ -1,0 +1,212 @@
+implement Bytes;
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+stdin, stdout: ref Iobuf;
+
+Bytes: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: bytes start end [bytes]\n");
+	raise "fail:usage";
+}
+
+END: con 16r7fffffff;
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->fprint(stderr, "bytes: cannot load %s: %r\n", Bufio->PATH);
+		raise "fail:bad module";
+	}
+	stdin = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	stdout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+	start := end := END;
+	if (len argv < 3)
+		usage();
+	argv = tl argv;
+	if (hd argv != "end")
+		start = int hd argv;
+	argv = tl argv;
+	if (hd argv != "end")
+		end = int hd argv;
+	if (end < start) {
+		sys->fprint(stderr, "bytes: out of order range\n");
+		raise "fail:bad range";
+	}
+	argv = tl argv;
+	if (argv == nil)
+		showbytes(start, end);
+	else {
+		if (tl argv != nil)
+			usage();
+		b := s2bytes(hd argv);
+		setbytes(start, end, b);
+	}
+	stdout.close();
+}
+
+showbytes(start, end: int)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	hold := array[Sys->UTFmax] of byte;
+	tot := 0;
+	nhold := 0;
+	while (tot < end && (n := stdin.read(buf[nhold:], len buf - nhold)) > 0) {
+		sys->fprint(stderr, "bytes: read %d bytes\n", n);
+		if (tot + n < start)
+			continue;
+		sb := 0;
+		eb := n;
+		if (start > tot)
+			sb = start - tot;
+		if (tot + n > end)
+			eb = end - tot;
+		nhold = putbytes(buf[sb:eb], hold);
+		buf[0:] = hold[0:nhold];
+		tot += n - nhold;
+	}
+	sys->fprint(stderr, "out of loop\n");
+	flushbytes(hold[0:nhold]);
+}
+
+setbytes(start, end: int, d: array of byte)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	tot := 0;
+	while ((n := stdin.read(buf, len buf)) > 0) {
+		if (tot + n < start || tot >= end) {
+			stdout.write(buf, n);
+			continue;
+		}
+		if (tot <= start) {
+			stdout.write(buf[0:start-tot], start-tot);
+			stdout.write(d, len d);
+			if (end == END)
+				return;
+		}
+		if (tot + n >= end)
+			stdout.write(buf[end - tot:], n - (end - tot));
+		tot += n;
+	}
+	if (tot == start || start == END)
+		stdout.write(d, len d);
+}
+
+putbytes(d: array of byte, hold: array of byte): int
+{
+	i := 0;
+	while (i < len d) {
+		(c, n, ok) := sys->byte2char(d, i);
+		if (ok && n > 0) {
+			if (c == '\\')
+				stdout.putc('\\');
+			stdout.putc(c);
+		} else {
+			if (n == 0) {
+				hold[0:] = d[i:];
+				return len d - i;
+			} else {
+				putbyte(d[i]);
+				n = 1;
+			}
+		}
+		i += n;
+	}
+	return 0;
+}
+
+flushbytes(hold: array of byte)
+{
+	for (i := 0; i < len hold; i++)
+		putbyte(hold[i]);
+}
+
+putbyte(b: byte)
+{
+	stdout.puts(sys->sprint("\\%2.2X", int b));
+}
+
+isbschar(c: int): int
+{
+	case c {
+	'n' or 'r' or 't' or 'v' =>
+		return 1;
+	}
+	return 0;
+}
+
+s2bytes(s: string): array of byte
+{
+	d := array[len s + 2] of byte;
+	j := 0;
+	for (i := 0; i < len s; i++) {
+		if (s[i] == '\\') {
+			if (i >= len s - 1 || (!isbschar(s[i+1]) && i >= len s - 2)) {
+				sys->fprint(stderr, "bytes: invalid backslash sequence\n");
+				raise "fail:bad args";
+			}
+			d = assure(d, j + 1);
+			if (isbschar(s[i+1])) {
+				case s[i+1] {
+				'n' =>	d[j++] = byte '\n';
+				'r' =>		d[j++] = byte '\r';
+				't' =>		d[j++] = byte '\t';
+				'v' =>	d[j++] = byte '\v';
+				'\\' =>	d[j++] = byte '\\';
+				* =>
+					sys->fprint(stderr, "bytes: invalid backslash sequence\n");
+					raise "fail:bad args";
+				}
+				i++;
+			} else if (!ishex(s[i+1]) || !ishex(s[i+2])) {
+				sys->fprint(stderr, "bytes: invalid backslash sequence\n");
+				raise "fail:bad args";
+			} else {
+				d[j++] = byte ((hex(s[i+1]) << 4) + hex(s[i+2]));
+				i += 2;
+			}
+		} else {
+			d = assure(d, j + 3);
+			j += sys->char2byte(s[i], d, j);
+		}
+	}
+	return d[0:j];
+}
+
+assure(d: array of byte, n: int): array of byte
+{
+	if (len d >= n)
+		return d;
+	nd := array[n] of byte;
+	nd[0:] = d;
+	return nd;
+}
+
+ishex(c: int): int
+{
+	return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
+}
+
+hex(c: int): int
+{
+	case c {
+	'0' to '9' =>
+		return c - '0';
+	'a' to 'f' =>
+		return c - 'a' + 10;
+	'A' to 'F' =>
+		return c-  'A' + 10;
+	}
+	return 0;
+}
--- /dev/null
+++ b/appl/cmd/cal.b
@@ -1,0 +1,295 @@
+implement Cal;
+
+#
+# Copyright © 1995-2002 Lucent Technologies Inc.  All rights reserved.
+# Limbo transliteration 2003 by Vita Nuova
+# This software is subject to the Plan 9 Open Source Licence.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "daytime.m";
+	daytime: Daytime;
+	Tm: import daytime;
+
+Cal: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+dayw :=	" S  M Tu  W Th  F  S";
+smon := array[] of {
+	"January", "February", "March", "April",
+	"May", "June", "July", "August",
+	"September", "October", "November", "December",
+};
+
+mon := array[] of {
+	0,
+	31, 29, 31, 30,
+	31, 30, 31, 31,
+	30, 31, 30, 31,
+};
+
+bout: ref Iobuf;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	y, m: int;
+
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	argc := len args;
+	if(argc > 3){
+		sys->fprint(sys->fildes(2), "usage: cal [month] [year]\n");
+		raise "fail:usage";
+	}
+	bout = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+
+#
+# no arg, print current month
+#
+	if(argc <= 1) {
+		m = curmo();
+		y = curyr();
+		return xshort(m, y);
+	}
+	args = tl args;
+
+#
+# one arg
+#	if looks like a month, print month
+#	else print year
+#
+	if(argc == 2) {
+		y = number(hd args);
+		if(y < 0)
+			y = -y;
+		if(y >= 1 && y <= 12)
+			return xshort(y, curyr());
+		return xlong(y);
+	}
+
+#
+# two arg, month and year
+#
+	m = number(hd args);
+	if(m < 0)
+		m = -m;
+	y = number(hd tl args);
+	return xshort(m, y);
+}
+
+#
+#	print out just month
+#
+xshort(m: int, y: int)
+{
+	if(m < 1 || m > 12)
+		badarg();
+	if(y < 1 || y > 9999)
+		badarg();
+	bout.puts(sys->sprint("   %s %ud\n", smon[m-1], y));
+	bout.puts(sys->sprint("%s\n", dayw));
+	lines := cal(m, y);
+	for(i := 0; i < len lines; i++){
+		bout.puts(lines[i]);
+		bout.putc('\n');
+	}
+	bout.flush();
+}
+
+#
+#	print out complete year
+#
+xlong(y: int)
+{
+	if(y<1 || y>9999)
+		badarg();
+	bout.puts("\n\n\n");
+	bout.puts(sys->sprint("                                %ud\n", y));
+	bout.putc('\n');
+	months := array[3] of array of string;
+	for(i:=0; i<12; i+=3) {
+		bout.puts(sys->sprint("         %.3s", smon[i]));
+		bout.puts(sys->sprint("                    %.3s", smon[i+1]));
+		bout.puts(sys->sprint("                    %.3s\n", smon[i+2]));
+		bout.puts(sys->sprint("%s   %s   %s\n", dayw, dayw, dayw));
+		for(j := 0; j < 3; j++)
+			months[j] = cal(i+j+1, y);
+		for(l := 0; l < 6; l++){
+			s := "";
+			for(j = 0; j < 3; j++)
+				s += sys->sprint("%-20.20s   ", months[j][l]);
+			for(j = len s; j > 0 && s[j-1] == ' ';)
+				j--;
+			bout.puts(s[0:j]);
+			bout.putc('\n');
+		}
+	}
+	bout.flush();
+}
+
+badarg()
+{
+	sys->fprint(sys->fildes(2), "cal: bad argument\n");
+	raise "fail:bad argument";
+}
+
+dict := array[] of {
+	("january",	1),
+	("february",	2),
+	("march",	3),
+	("april",	4),
+	("may",		5),
+	("june",		6),
+	("july",		7),
+	("august",	8),
+	("sept",		9),
+	("september",	9),
+	("october",	10),
+	("november",	11),
+	("december",	12),
+};
+
+#
+# convert to a number.
+# if its a dictionary word,
+# return negative  number
+#
+number(s: string): int
+{
+	if(len s >= 3){
+		for(n:=0; n < len dict; n++){
+			(word, val) := dict[n];
+			if(s == word || s == word[0:3])
+				return -val;
+		}
+	}
+	n := 0;
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c<'0' || c>'9')
+			badarg();
+		n = n*10 + c-'0';
+	}
+	return n;
+}
+
+pstr(str: string, n: int)
+{
+	bout.puts(sys->sprint("%-*.*s\n", n, n, str));
+}
+
+cal(m: int, y: int): array of string
+{
+	d := jan1(y);
+	mon[9] = 30;
+
+	case (jan1(y+1)+7-d)%7 {
+
+	#
+	#	non-leap year
+	#
+	1 =>
+		mon[2] = 28;
+
+	#
+	#	leap year
+	#
+	2 =>
+		mon[2] = 29;
+
+	#
+	#	1752
+	#
+	* =>
+		mon[2] = 29;
+		mon[9] = 19;
+	}
+	for(i:=1; i<m; i++)
+		d += mon[i];
+	d %= 7;
+	lines := array[6] of string;
+	l := 0;
+	s := "";
+	for(i = 0; i < d; i++)
+		s += "   ";
+	for(i=1; i<=mon[m]; i++) {
+		if(i==3 && mon[m]==19) {
+			i += 11;
+			mon[m] += 11;
+		}
+		s += sys->sprint("%2d", i);
+		if(++d == 7) {
+			d = 0;
+			lines[l++] = s;
+			s = "";
+		}else
+			s[len s] = ' ';
+	}
+	if(s != nil){
+		while(s[len s-1] == ' ')
+			s = s[:len s-1];
+		lines[l] = s;
+	}
+	return lines;
+}
+
+#
+#	return day of the week
+#	of jan 1 of given year
+#
+jan1(y: int): int
+{
+#
+#	normal gregorian calendar
+#	one extra day per four years
+#
+
+	d := 4+y+(y+3)/4;
+
+#
+#	julian calendar
+#	regular gregorian
+#	less three days per 400
+#
+
+	if(y > 1800) {
+		d -= (y-1701)/100;
+		d += (y-1601)/400;
+	}
+
+#
+#	great calendar changeover instant
+#
+
+	if(y > 1752)
+		d += 3;
+
+	return d%7;
+}
+
+#
+# get current month and year
+#
+curmo(): int
+{
+	tm := daytime->local(daytime->now());
+	return tm.mon+1;
+}
+
+curyr(): int
+{
+	tm := daytime->local(daytime->now());
+	return tm.year+1900;
+}
--- /dev/null
+++ b/appl/cmd/calc.b
@@ -1,0 +1,2547 @@
+implement Calc;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+	arg: Arg;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "math.m";
+	maths: Math;
+include "rand.m";
+	rand: Rand;
+include "daytime.m";
+	daytime: Daytime;
+
+Calc: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg = load Arg Arg->PATH;
+	bufio = load Bufio Bufio->PATH;
+	maths = load Math Math->PATH;
+	rand = load Rand Rand->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	maths->FPcontrol(0, Math->INVAL|Math->ZDIV|Math->OVFL|Math->UNFL|Math->INEX);
+
+	rand->init(daytime->now());
+	rand->init(rand->rand(Big)^rand->rand(Big));
+	daytime = nil;
+
+	arg->init(args);
+	while((c := arg->opt()) != 0){
+		case(c){
+		'b' =>
+			bits = 1;
+		'd' =>
+			debug = 1;
+		's' =>
+			strict = 1;
+		}
+	}
+	gargs = args = arg->argv();
+	if(args == nil){
+		stdin = 1;
+		bin = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	}
+	else if(tl args == nil)
+		bin = bufio->open(hd args, Sys->OREAD);
+
+	syms = array[Hash] of ref Sym;
+
+	pushscope();
+	for(i := 0; keyw[i].t0 != nil; i++)
+		enter(keyw[i].t0, keyw[i].t1);
+	for(i = 0; conw[i].t0 != nil; i++)
+		adddec(conw[i].t0, Ocon, conw[i].t1, 0);
+	for(i = 0; varw[i].t0 != nil; i++)
+		adddec(varw[i].t0, Ovar, varw[i].t1, 0);
+	for(i = 0; funw[i].t0 != nil; i++)
+		adddec(funw[i].t0, Olfun, real funw[i].t1, funw[i].t2);
+	
+	deg = lookup(Deg).dec;
+	pbase = lookup(Base).dec;
+	errdec = ref Dec;
+
+	pushscope();
+	for(;;){
+		e: ref Node;
+
+		{
+			t := lex();
+			if(t == Oeof)
+				break;
+			unlex(t);
+			ls := lexes;
+			e = stat(1);
+			ckstat(e, Onothing, 0);
+			if(ls == lexes){
+				t = lex();
+				error(nil, sys->sprint("syntax error near %s", opstring(t)));
+				unlex(t);
+			}
+			consume(Onl);
+		}
+		exception ex{
+			Eeof =>
+				e = nil;
+				err("premature eof");
+				skip();
+			"*" =>
+				e = nil;
+				err(ex);
+				skip();
+		}
+		if(0 && debug)
+			prtree(e, 0);
+		if(e != nil && e.op != Ofn){
+			(k, v) := (Onothing, 0.0);
+			{
+				(k, v) = estat(e);
+			}
+			exception ex{
+			"*" =>
+				e = nil;
+				err(ex);
+			}
+			if(pexp(e))
+				printnum(v, "\n");
+			if(k == Oexit)
+				exit;
+		}
+	}
+	popscope();
+	popscope();
+}
+
+bits: int;
+debug: int;
+strict: int;
+
+None: con -2;
+Eof: con -1;
+Eeof: con "eof";
+
+Hash: con 16;
+Big: con 1<<30;
+Maxint: con 16r7FFFFFFF;
+Nan: con Math->NaN;
+Infinity: con Math->Infinity;
+Pi: con Math->Pi;
+Eps: con 1E-10;
+Bigeps: con 1E-2;
+Ln2: con 0.6931471805599453;
+Ln10: con 2.302585092994046;
+Euler: con 2.71828182845904523536;
+Gamma: con 0.57721566490153286060;
+Phi: con 1.61803398874989484820;
+
+Oeof,
+Ostring, Onum, Oident, Ocon, Ovar, Ofun, Olfun,
+Oadd, Osub, Omul, Odiv, Omod, Oidiv, Oexp, Oand, Oor, Oxor, Olsh, Orsh,
+Oadde, Osube, Omule, Odive, Omode, Oidive, Oexpe, Oande, Oore, Oxore, Olshe, Orshe,
+Oeq, One, Ogt, Olt, Oge, Ole,
+Oinc, Opreinc, Opostinc, Odec, Opredec, Opostdec,
+Oandand, Ooror,
+Oexc, Onot, Ofact, Ocom,
+Oas, Odas,
+Oplus, Ominus, Oinv,
+Ocomma, Oscomma, Oquest, Ocolon,
+Onand, Onor, Oimp, Oimpby, Oiff,
+Olbr, Orbr, Olcbr, Orcbr,  Oscolon, Onl,
+Onothing,
+Oprint, Oread,
+Oif, Oelse, Ofor, Owhile, Odo, Obreak, Ocont, Oexit, Oret, Ofn, Oinclude,
+Osigma, Opi, Ocfrac, Oderiv, Ointeg, Osolve,
+Olog, Olog10, Olog2, Ologb, Oexpf, Opow, Osqrt, Ocbrt, Ofloor, Oceil, Omin, Omax, Oabs, Ogamma, Osign, Oint, Ofrac, Oround, Oerf, Oatan2, Osin, Ocos, Otan, Oasin, Oacos, Oatan, Osinh, Ocosh, Otanh, Oasinh, Oacosh, Oatanh, Orand,
+Olast: con iota;
+
+Binary: con (1<<8);
+Preunary:	con (1<<9);
+Postunary: con (1<<10);
+Assoc: con (1<<11);
+Rassoc: con (1<<12);
+Prec: con Binary-1;
+
+opss := array[Olast] of
+{
+	"eof",
+	"string",
+	"number",
+	"identifier",
+	"constant",
+	"variable",
+	"function",
+	"library function",
+	"+",
+	"-",
+	"*",
+	"/",
+	"%",
+	"//",
+	"&",
+	"|",
+	"^",
+	"<<",
+	">>",
+	"+=",
+	"-=",
+	"*=",
+	"/=",
+	"%=",
+	"//=",
+	"&=",
+	"|=",
+	"^=",
+	"<<=",
+	">>=",
+	"==",
+	"!=",
+	">",
+	"<",
+	">=",
+	"<=",
+	"++",
+	"++",
+	"++",
+	"--",
+	"--",
+	"--",
+	"**",
+	"&&",
+	"||",
+	"!",
+	"!",
+	"!",
+	"~",
+	"=",
+	":=",
+	"+",
+	"-",
+	"1/",
+	",",
+	",",
+	"?",
+	":",
+	"↑",
+	"↓",
+	"->",
+	"<-",
+	"<->",
+	"(",
+	")",
+	"{",
+	"}",
+	";",
+	"\n",
+	"",
+};
+
+ops := array[Olast] of
+{
+	Oeof =>	0,
+	Ostring =>	17,
+	Onum =>	17,
+	Oident =>	17,
+	Ocon =>	17,
+	Ovar =>	17,
+	Ofun =>	17,
+	Olfun =>	17,
+	Oadd =>	12|Binary|Assoc|Preunary,
+	Osub =>	12|Binary|Preunary,
+	Omul =>	13|Binary|Assoc,
+	Odiv =>	13|Binary,
+	Omod =>	13|Binary,
+	Oidiv =>	13|Binary,
+	Oexp =>	14|Binary|Rassoc,
+	Oand =>	8|Binary|Assoc,
+	Oor =>	6|Binary|Assoc,
+	Oxor =>	7|Binary|Assoc,
+	Olsh =>	11|Binary,
+	Orsh =>	11|Binary,
+	Oadde =>	2|Binary|Rassoc,
+	Osube =>	2|Binary|Rassoc,
+	Omule =>	2|Binary|Rassoc,
+	Odive =>	2|Binary|Rassoc,
+	Omode =>	2|Binary|Rassoc,
+	Oidive =>	2|Binary|Rassoc,
+	Oexpe =>	2|Binary|Rassoc,
+	Oande =>	2|Binary|Rassoc,
+	Oore =>	2|Binary|Rassoc,
+	Oxore =>	2|Binary|Rassoc,
+	Olshe =>	2|Binary|Rassoc,
+	Orshe =>	2|Binary|Rassoc,
+	Oeq =>	9|Binary,
+	One =>	9|Binary,
+	Ogt =>	10|Binary,
+	Olt =>	10|Binary,
+	Oge =>	10|Binary,
+	Ole =>	10|Binary,
+	Oinc =>	15|Rassoc|Preunary|Postunary,
+	Opreinc =>	15|Rassoc|Preunary,
+	Opostinc =>	15|Rassoc|Postunary,
+	Odec =>	15|Rassoc|Preunary|Postunary,
+	Opredec =>	15|Rassoc|Preunary,
+	Opostdec =>	15|Rassoc|Postunary,
+	Oandand =>	5|Binary|Assoc,
+	Ooror =>	4|Binary|Assoc,
+	Oexc =>	15|Rassoc|Preunary|Postunary,
+	Onot =>	15|Rassoc|Preunary,
+	Ofact =>	15|Rassoc|Postunary,
+	Ocom =>	15|Rassoc|Preunary,
+	Oas =>	2|Binary|Rassoc,
+	Odas =>	2|Binary|Rassoc,
+	Oplus =>	15|Rassoc|Preunary,
+	Ominus =>	15|Rassoc|Preunary,
+	Oinv =>	15|Rassoc|Postunary,
+	Ocomma =>	1|Binary|Assoc,
+	Oscomma =>	1|Binary|Assoc,
+	Oquest =>	3|Binary|Rassoc,
+	Ocolon =>	3|Binary|Rassoc,
+	Onand =>	8|Binary,
+	Onor =>	6|Binary,
+	Oimp =>	9|Binary,
+	Oimpby =>	9|Binary,
+	Oiff =>	10|Binary|Assoc,
+	Olbr =>	16,
+	Orbr =>	16,
+	Onothing =>	0,
+};
+
+Deg: con "degrees";
+Base: con "printbase";
+Limit: con "solvelimit";
+Step: con "solvestep";
+
+keyw := array[] of
+{
+	("include",	Oinclude),
+	("if",	Oif),
+	("else",	Oelse),
+	("for",	Ofor),
+	("while",	Owhile),
+	("do",	Odo),
+	("break",	Obreak),
+	("continue",	Ocont),
+	("exit",	Oexit),
+	("return",	Oret),
+	("print",	Oprint),
+	("read",	Oread),
+	("fn",	Ofn),
+	("",	0),
+};
+
+conw := array[] of
+{
+	("π",	Pi),
+	("Pi", Pi),
+	("e",	Euler),
+	("γ",	Gamma),
+	("Gamma",	Gamma),
+	("φ",	Phi),
+	("Phi",	Phi),
+	("∞",	Infinity),
+	("Infinity",	Infinity),
+	("NaN",	Nan),
+	("Nan",	Nan),
+	("nan",	Nan),
+	("",	0.0),
+};
+
+varw := array[] of
+{
+	(Deg, 0.0),
+	(Base, 10.0),
+	(Limit, 100.0),
+	(Step, 1.0),
+	("", 0.0),
+};
+
+funw := array[] of
+{
+	("log",	Olog,	1),
+	("ln",		Olog,	1),
+	("log10",	Olog10,	1),
+	("log2",	Olog2,	1),
+	("logb",	Ologb,	2),
+	("exp",	Oexpf,	1),
+	("pow",	Opow,	2),
+	("sqrt",	Osqrt,	1),
+	("cbrt",	Ocbrt,	1),
+	("floor",	Ofloor,	1),
+	("ceiling",	Oceil,	1),
+	("min",	Omin,	2),
+	("max",	Omax,	2),
+	("abs",	Oabs,	1),
+	("Γ",	Ogamma,	1),
+	("gamma",	Ogamma,	1),
+	("sign",	Osign,	1),
+	("int",	Oint,	1),
+	("frac",	Ofrac,	1),
+	("round",	Oround,	1),
+	("erf",	Oerf,	1),
+	("atan2",	Oatan2,	2),
+	("sin",	Osin,	1),
+	("cos",	Ocos,	1),
+	("tan",	Otan,	1),
+	("asin",	Oasin,	1),
+	("acos",	Oacos,	1),
+	("atan",	Oatan,	1),
+	("sinh",	Osinh,	1),
+	("cosh",	Ocosh,	1),
+	("tanh",	Otanh,	1),
+	("asinh",	Oasinh,	1),
+	("acosh",	Oacosh,	1),
+	("atanh",	Oatanh,	1),
+	("rand",	Orand,	0),
+	("Σ",	Osigma,	3),
+	("sigma",	Osigma,	3),
+	("Π",	Opi,	3),
+	("pi",	Opi,	3),
+	("cfrac", Ocfrac,	3),
+	("Δ",	Oderiv,	2),
+	("differential",	Oderiv,	2),
+	("∫",	Ointeg,	3),
+	("integral",	Ointeg,	3),
+	("solve",	Osolve,	1),
+	("",	0,	0),
+};
+
+stdin: int;
+bin: ref Iobuf;
+lineno: int = 1;
+file: string;
+iostack: list of (int, int, int, string, ref Iobuf);
+geof: int;
+garg: string;
+gargs: list of string;
+bufc: int = None;
+buft: int = Olast;
+lexes: int;
+lexval: real;
+lexstr: string;
+lexsym: ref Sym;
+syms: array of ref Sym;
+deg: ref Dec;
+pbase: ref Dec;
+errdec: ref Dec;
+inloop: int;
+infn: int;
+
+Node: adt
+{
+	op: int;
+	left: cyclic ref Node;
+	right: cyclic ref Node;
+	val: real;
+	str: string;
+	dec: cyclic ref Dec;
+	src: int;
+};
+
+Dec: adt
+{
+	kind: int;
+	scope: int;
+	sym: cyclic ref Sym;
+	val: real;
+	na: int;
+	code: cyclic ref Node;
+	old: cyclic ref Dec;
+	next: cyclic ref Dec;
+};
+
+Sym: adt
+{
+	name: string;
+	kind: int;
+	dec: cyclic ref Dec;
+	next: cyclic ref Sym;
+};
+
+opstring(t: int): string
+{
+	s := opss[t];
+	if(s != nil)
+		return s;
+	for(i := 0; keyw[i].t0 != nil; i++)
+		if(t == keyw[i].t1)
+			return keyw[i].t0;
+	for(i = 0; funw[i].t0 != nil; i++)
+		if(t == funw[i].t1)
+			return funw[i].t0;
+	return s;
+}
+
+err(s: string)
+{
+	sys->print("error: %s\n", s);
+}
+
+error(n: ref Node, s: string)
+{
+	if(n != nil)
+		lno := n.src;
+	else
+		lno = lineno;
+	s = sys->sprint("line %d: %s", lno, s);
+	if(file != nil)
+		s = sys->sprint("file %s: %s", file, s);
+	raise s;
+}
+
+fatal(s: string)
+{
+	sys->print("fatal: %s\n", s);
+	exit;
+}
+
+stack(s: string, f: ref Iobuf)
+{
+	iostack = (bufc, buft, lineno, file, bin) :: iostack;
+	bufc = None;
+	buft = Olast;
+	lineno = 1;
+	file = s;
+	bin = f;
+}
+
+unstack()
+{
+	(bufc, buft, lineno, file, bin) = hd iostack;
+	iostack = tl iostack;
+}
+
+doinclude(s: string)
+{
+	f := bufio->open(s, Sys->OREAD);
+	if(f == nil)
+		error(nil, sys->sprint("cannot open %s", s));
+	stack(s, f);
+}
+
+getc(): int
+{
+	if((c := bufc) != None)
+		bufc = None;
+	else if(bin != nil)
+		c = bin.getc();
+	else{
+		if(garg == nil){
+			if(gargs == nil){
+				if(geof == 0){
+					geof = 1;
+					c = '\n';
+				}
+				else
+					c = Eof;
+			}
+			else{
+				garg = hd gargs;
+				gargs = tl gargs;
+				c = ' ';
+			}
+		}
+		else{
+			c = garg[0];
+			garg = garg[1: ];
+		}
+	}
+	if(c == Eof && iostack != nil){
+		unstack();
+		return getc();
+	}
+	return c;
+}
+
+ungetc(c: int)
+{
+	bufc = c;
+}
+
+slash(c: int): int
+{
+	if(c != '\\')
+		return c;
+	nc := getc();
+	case(nc){
+	'b' => return '\b';
+	'f' => return '\f';
+	'n' => return '\n';
+	'r' => return '\r';
+	't' => return '\t';
+	}
+	return nc;
+}
+
+lexstring(): int
+{
+	sp := "";
+	while((c := getc()) != '"'){
+		if(c == Eof)
+			raise Eeof;
+		sp[len sp] = slash(c);
+	}
+	lexstr = sp;
+	return Ostring;
+}
+
+lexchar(): int
+{
+	while((c := getc()) != '\''){
+		if(c == Eof)
+			raise Eeof;
+		lexval = real slash(c);
+	}
+	return Onum;
+}
+
+basev(c: int, base: int): int
+{
+	if(c >= 'a' && c <= 'z')
+		c += 10-'a';
+	else if(c >= 'A' && c <= 'Z')
+		c += 10-'A';
+	else if(c >= '0' && c <= '9')
+		c -= '0';
+	else
+		return -1;
+	if(c >= base)
+		error(nil, "bad digit");
+	return c;
+}
+
+lexe(base: int): int
+{
+	neg := 0;
+	v := big 0;
+	c := getc();
+	if(c == '-')
+		neg = 1;
+	else
+		ungetc(c);
+	for(;;){
+		c = getc();
+		cc := basev(c, base);
+		if(cc < 0){
+			ungetc(c);
+			break;
+		}
+		v = big base*v+big cc;
+	}
+	if(neg)
+		v = -v;
+	return int v;
+}
+
+lexnum(): int
+{
+	base := 10;
+	exp := 0;
+	r := f := e := 0;
+	v := big 0;
+	c := getc();
+	if(c == '0'){
+		base = 8;
+		c = getc();
+		if(c == '.'){
+			base = 10;
+			ungetc(c);
+		}
+		else if(c == 'x' || c == 'X')
+			base = 16;
+		else
+			ungetc(c);
+	}
+	else
+		ungetc(c);
+	for(;;){
+		c = getc();
+		if(!r && (c == 'r' || c == 'R')){
+			if(f || e)
+				error(nil, "bad base");
+			r = 1;
+			base = int v;
+			if(base < 2 || base > 36)
+				error(nil, "bad base");
+			v = big 0;
+			continue;
+		}
+		if(c == '.'){
+			if(f || e)
+				error(nil, "bad real");
+			f = 1;
+			continue;
+		}
+		if(base == 10 && (c == 'e' || c == 'E')){
+			if(e)
+				error(nil, "bad E part");
+			e = 1;
+			exp = lexe(base);
+			continue;
+		}
+		cc := basev(c, base);
+		if(cc < 0){
+			ungetc(c);
+			break;
+		}
+		v = big base*v+big cc;
+		if(f)
+			f++;
+	}
+	lexval = real v;
+	if(f)
+		lexval /= real base**(f-1);
+	if(exp){
+		if(exp > 0)
+			lexval *= real base**exp;
+		else
+			lexval *= maths->pow(real base, real exp);
+	}
+	return Onum;
+}
+
+lexid(): int
+{
+	sp := "";
+	for(;;){
+		c := getc();
+		if(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c >= 'α' && c <=  'ω' || c >= 'Α' && c <= 'Ω' || c == '_')
+			sp[len sp] = c;
+		else{
+			ungetc(c);
+			break;
+		}
+	}
+	lexsym = enter(sp, Oident);
+	return lexsym.kind;
+}
+
+follow(c: int, c1: int, c2: int): int
+{
+	nc := getc();
+	if(nc == c)
+		return c1;
+	ungetc(nc);
+	return c2;
+}
+
+skip()
+{
+	if((t := buft) != Olast){
+		lex();
+		if(t == Onl)
+			return;
+	}
+	for(;;){
+		c := getc();
+		if(c == Eof){
+			ungetc(c);
+			return;
+		}
+		if(c == '\n'){
+			lineno++;
+			return;
+		}
+	}
+}
+
+lex(): int
+{
+	lexes++;
+	if((t := buft) != Olast){
+		buft = Olast;
+		if(t == Onl)
+			lineno++;
+		return t;
+	}
+	for(;;){
+		case(c := getc()){
+		Eof =>
+			return Oeof;
+		'#' =>
+			while((c = getc()) != '\n'){
+				if(c == Eof)
+					raise Eeof;
+			}
+			lineno++;
+		'\n' =>
+			lineno++;
+			return Onl;
+		' ' or
+		'\t' or
+		'\r' or
+		'\v' =>
+			;
+		'"' =>
+			return lexstring();
+		'\'' =>
+			return lexchar();
+		'0' to '9' =>
+			ungetc(c);
+			return lexnum();
+		'a' to 'z' or
+		'A' to 'Z' or
+		'α' to 'ω' or
+		'Α' to 'Ω' or
+		'_' =>
+			ungetc(c);
+			return lexid();
+		'+' =>
+			c = getc();
+			if(c == '=')
+				return Oadde;
+			ungetc(c);
+			return follow('+', Oinc, Oadd);
+		'-' =>
+			c = getc();
+			if(c == '=')
+				return Osube;
+			if(c == '>')
+				return Oimp;
+			ungetc(c);
+			return follow('-', Odec, Osub);
+		'*' =>
+			c = getc();
+			if(c == '=')
+				return Omule;
+			if(c == '*')
+				return follow('=', Oexpe, Oexp);
+			ungetc(c);
+			return Omul;
+		'/' =>
+			c = getc();
+			if(c == '=')
+				return Odive;
+			if(c == '/')
+				return follow('=', Oidive, Oidiv);
+			ungetc(c);
+			return Odiv;
+		'%' =>
+			return follow('=', Omode, Omod);
+		'&' =>
+			c = getc();
+			if(c == '=')
+				return Oande;
+			ungetc(c);
+			return follow('&', Oandand, Oand);
+		'|' =>
+			c = getc();
+			if(c == '=')
+				return Oore;
+			ungetc(c);
+			return follow('|', Ooror, Oor);
+		'^' =>
+			return follow('=', Oxore, Oxor);
+		'=' =>
+			return follow('=', Oeq, Oas);
+		'!' =>
+			return follow('=', One, Oexc);
+		'>' =>
+			c = getc();
+			if(c == '=')
+				return Oge;
+			if(c == '>')
+				return follow('=', Orshe, Orsh);
+			ungetc(c);
+			return Ogt;
+		'<' =>
+			c = getc();
+			if(c == '=')
+				return Ole;
+			if(c == '<')
+				return follow('=', Olshe, Olsh);
+			if(c == '-')
+				return follow('>', Oiff, Oimpby);
+			ungetc(c);
+			return Olt;
+		'(' =>
+			return Olbr;
+		')' =>
+			return Orbr;
+		'{' =>
+			return Olcbr;
+		'}' =>
+			return Orcbr;
+		'~' =>
+			return Ocom;
+		'.' =>
+			ungetc(c);
+			return lexnum();
+		',' =>
+			return Ocomma;
+		'?' =>
+			return Oquest;
+		':' =>
+			return follow('=', Odas, Ocolon);
+		';' =>
+			return Oscolon;
+		'↑' =>
+			return Onand;
+		'↓' =>
+			return Onor;
+		'∞' =>
+			lexval = Infinity;
+			return Onum;
+		* =>
+			error(nil, sys->sprint("bad character %c", c));
+		}
+	}
+}
+
+unlex(t: int)
+{
+	lexes--;
+	buft = t;
+	if(t == Onl)
+		lineno--;
+}
+
+mustbe(t: int)
+{
+	nt := lex();
+	if(nt != t)
+		error(nil, sys->sprint("expected %s not %s", opstring(t), opstring(nt)));
+}
+
+consume(t: int)
+{
+	nt := lex();
+	if(nt != t)
+		unlex(nt);
+}
+
+elex(): int
+{
+	t := lex();
+	if(binary(t))
+		return t;
+	if(hexp(t)){
+		unlex(t);
+		return Oscomma;
+	}
+	return t;
+}
+
+hexp(o: int): int
+{
+	return preunary(o) || o == Olbr || atom(o);
+}
+
+atom(o: int): int
+{
+	return o >= Ostring && o <= Olfun;
+}
+
+asop(o: int): int
+{
+	return o == Oas || o == Odas || o >= Oadde && o <= Orshe || o >= Oinc && o <= Opostdec;
+}
+
+preunary(o: int): int
+{
+	return ops[o]&Preunary;
+}
+
+postunary(o: int): int
+{
+	return ops[o]&Postunary;
+}
+
+binary(o: int): int
+{
+	return ops[o]&Binary;
+}
+
+prec(o: int): int
+{
+	return ops[o]&Prec;
+}
+
+assoc(o: int): int
+{
+	return ops[o]&Assoc;
+}
+
+rassoc(o: int): int
+{
+	return ops[o]&Rassoc;
+}
+
+preop(o: int): int
+{
+	case(o){
+	Oadd => return Oplus;
+	Osub => return Ominus;
+	Oinc => return Opreinc;
+	Odec => return Opredec;
+	Oexc => return Onot;
+	}
+	return o;
+}
+
+postop(o: int): int
+{
+	case(o){
+	Oinc => return Opostinc;
+	Odec => return Opostdec;
+	Oexc => return Ofact;
+	}
+	return o;
+}
+
+prtree(p: ref Node, in: int)
+{
+	if(p == nil)
+		return;
+	for(i := 0; i < in; i++)
+		sys->print("    ");
+	sys->print("%s ", opstring(p.op));
+	case(p.op){
+	Ostring =>
+		sys->print("%s", p.str);
+	Onum =>
+		sys->print("%g", p.val);
+	Ocon or
+	Ovar =>
+		sys->print("%s(%g)", p.dec.sym.name, p.dec.val);
+	Ofun or
+	Olfun =>
+		sys->print("%s", p.dec.sym.name);
+	}
+	sys->print("\n");
+	# sys->print(" - %d\n", p.src);
+	prtree(p.left, in+1);
+	prtree(p.right, in+1);
+}
+
+tree(o: int, l: ref Node, r: ref Node): ref Node
+{
+	p := ref Node;
+	p.op = o;
+	p.left = l;
+	p.right = r;
+	p.src = lineno;
+	if(asop(o)){
+		if(o >= Oadde && o <= Orshe){
+			p = tree(Oas, l, p);
+			p.right.op += Oadd-Oadde;
+		}
+	}
+	return p;
+}
+
+itree(n: int): ref Node
+{
+	return vtree(real n);
+}
+
+vtree(v: real): ref Node
+{
+	n := tree(Onum, nil, nil);
+	n.val = v;
+	return n;
+}
+
+ltree(s: string, a: ref Node): ref Node
+{
+	n := tree(Olfun, a, nil);
+	n.dec = lookup(s).dec;
+	return n;
+}
+
+ptree(n: ref Node, p: real): ref Node
+{
+	if(isinteger(p)){
+		i := int p;
+		if(i == 0)
+			return itree(1);
+		if(i == 1)
+			return n;
+		if(i == -1)
+			return tree(Oinv, n, nil);
+		if(i < 0)
+			return tree(Oinv, tree(Oexp, n, itree(-i)), nil);
+	}
+	return tree(Oexp, n, vtree(p));
+}
+
+iscon(n: ref Node): int
+{
+	return n.op == Onum || n.op == Ocon;
+}
+
+iszero(n: ref Node): int
+{
+	return iscon(n) && eval(n) == 0.0;
+}
+
+isone(n: ref Node): int
+{
+	return iscon(n) && eval(n) == 1.0;
+}
+
+isnan(n: ref Node): int
+{
+	return iscon(n) && maths->isnan(eval(n));
+}
+
+isinf(n: ref Node): int
+{
+	return iscon(n) && (v := eval(n)) == Infinity || v == -Infinity;
+}
+
+stat(scope: int): ref Node
+{
+	e1, e2, e3, e4: ref Node;
+
+	consume(Onl);
+	t := lex();
+	case(t){
+	Olcbr =>
+		if(scope)
+			pushscope();
+		for(;;){
+			e2 = stat(1);
+			if(e1 == nil)
+				e1 = e2;
+			else
+				e1 = tree(Ocomma, e1, e2);
+			consume(Onl);
+			t = lex();
+			if(t == Oeof)
+				raise Eeof;
+			if(t == Orcbr)
+				break;
+			unlex(t);
+		}
+		if(scope)
+			popscope();
+		return e1;
+	Oprint or
+	Oread or
+	Oret =>
+		if(t == Oret && !infn)
+			error(nil, "return not in fn");
+		e1= tree(t, expr(0, 1), nil);
+		consume(Oscolon);
+		if(t == Oread)
+			allvar(e1.left);
+		return e1;
+	Oif =>
+		# mustbe(Olbr);
+		e1 = expr(0, 1);
+		# mustbe(Orbr);
+		e2 = stat(1);
+		e3 = nil;
+		consume(Onl);
+		t = lex();
+		if(t == Oelse)
+			e3 = stat(1);
+		else
+			unlex(t);
+		return tree(Oif, e1, tree(Ocomma, e2, e3));
+	Ofor =>
+		inloop++;
+		mustbe(Olbr);
+		e1 = expr(0, 1);
+		mustbe(Oscolon);
+		e2 = expr(0, 1);
+		mustbe(Oscolon);
+		e3 = expr(0, 1);
+		mustbe(Orbr);
+		e4 = stat(1);
+		inloop--;
+		return tree(Ocomma, e1, tree(Ofor, e2, tree(Ocomma, e4, e3)));
+	Owhile =>
+		inloop++;
+		# mustbe(Olbr);
+		e1 = expr(0, 1);
+		# mustbe(Orbr);
+		e2 = stat(1);
+		inloop--;
+		return tree(Ofor, e1, tree(Ocomma, e2, nil));
+	Odo =>
+		inloop++;
+		e1 = stat(1);
+		consume(Onl);
+		mustbe(Owhile);
+		# mustbe(Olbr);
+		e2 = expr(0, 1);
+		# mustbe(Orbr);
+		consume(Oscolon);
+		inloop--;
+		return tree(Odo, e1, e2);
+	Obreak or
+	Ocont or
+	Oexit =>
+		if((t == Obreak || t == Ocont) && !inloop)
+			error(nil, "break/continue not in loop");
+		consume(Oscolon);
+		return tree(t, nil, nil);
+	Ofn =>
+		if(infn)
+			error(nil, "nested functions not allowed");
+		infn++;
+		mustbe(Oident);
+		s := lexsym;
+		d := mkdec(s, Ofun, 1);
+		d.code = tree(Ofn, nil, nil);
+		pushscope();
+		(d.na, d.code.left) = args(0);
+		allvar(d.code.left);
+		pushparams(d.code.left);
+		d.code.right = stat(0);
+		popscope();
+		infn--;
+		return d.code;
+	Oinclude =>
+		e1 = expr(0, 0);
+		if(e1.op != Ostring)
+			error(nil, "bad include file");
+		consume(Oscolon);
+		doinclude(e1.str);
+		return nil;
+	* =>
+		unlex(t);
+		e1 = expr(0, 1);
+		consume(Oscolon);
+		if(debug)
+			prnode(e1);
+		return e1;
+	}
+	return nil;
+}
+
+ckstat(n: ref Node, parop: int, pr: int)
+{
+	if(n == nil)
+		return;
+	pr |= n.op == Oprint;
+	ckstat(n.left, n.op, pr);
+	ckstat(n.right, n.op, pr);
+	case(n.op){
+	Ostring =>
+		if(!pr || parop != Oprint && parop != Ocomma)
+			error(n, "illegal string operation");
+	}
+}
+	
+pexp(e: ref Node): int
+{
+	if(e == nil)
+		return 0;
+	if(e.op == Ocomma)
+		return pexp(e.right);
+	return e.op >= Ostring && e.op <= Oiff && !asop(e.op);
+}
+
+expr(p: int, zok: int): ref Node
+{
+	n := exp(p, zok);
+	ckexp(n, Onothing);
+	return n;
+}
+
+exp(p: int, zok: int): ref Node
+{
+	l := prim(zok);
+	if(l == nil)
+		return nil;
+	while(binary(t := elex()) && (o := prec(t)) >= p){
+		if(rassoc(t))
+			r := exp(o, 0);
+		else
+			r = exp(o+1, 0);
+		if(t == Oscomma)
+			t = Ocomma;
+		l = tree(t, l, r);
+	}
+	if(t != Oscomma)
+		unlex(t);
+	return l;
+}
+
+prim(zok: int): ref Node
+{
+	p: ref Node;
+	na: int;
+
+	t := lex();
+	if(preunary(t)){
+		t = preop(t);
+		return tree(t, exp(prec(t), 0), nil);
+	}
+	case(t){
+	Olbr =>
+		p = exp(0, zok);
+		mustbe(Orbr);
+	Ostring =>
+		p = tree(t, nil, nil);
+		p.str = lexstr;
+	Onum =>
+		p = tree(t, nil ,nil);
+		p.val = lexval;
+	Oident =>
+		s := lexsym;
+		d := s.dec;
+		if(d == nil)
+			d = mkdec(s, Ovar, 0);
+		case(t = d.kind){
+		Ocon or
+		Ovar =>
+			p = tree(t, nil, nil);
+			p.dec = d;
+		Ofun or
+		Olfun =>
+			p = tree(t, nil, nil);
+			p.dec = d;
+			(na, p.left) = args(prec(t));
+			if(!(t == Olfun && d.val == real Osolve && na == 2))
+			if(na != d.na)
+				error(p, "wrong number of arguments");
+			if(t == Olfun){
+				case(int d.val){
+				Osigma or
+				Opi or
+				Ocfrac or
+				Ointeg =>
+					if((op := p.left.left.left.op) != Oas && op != Odas)
+						error(p.left, "expression not an assignment");
+				Oderiv =>
+					if((op := p.left.left.op) != Oas && op != Odas)
+						error(p.left, "expression not an assignment");
+				}
+			}
+		}
+	* =>
+		unlex(t);
+		if(!zok)
+			error(nil, "missing expression");
+		return nil;
+	}
+	while(postunary(t = lex())){
+		t = postop(t);
+		p = tree(t, p, nil);
+	}
+	unlex(t);
+	return p;	
+}
+
+ckexp(n: ref Node, parop: int)
+{
+	if(n == nil)
+		return;
+	o := n.op;
+	l := n.left;
+	r := n.right;
+	if(asop(o))
+		var(l);
+	case(o){
+	Ovar =>
+		s := n.dec.sym;
+		d := s.dec;
+		if(d == nil){
+			if(strict)
+				error(n, sys->sprint("%s undefined", s.name));
+			d = mkdec(s, Ovar, 1);
+		}
+		n.dec = d;
+	Odas =>
+		ckexp(r, o);
+		l.dec = mkdec(l.dec.sym, Ovar, 1);
+	* =>
+		ckexp(l, o);
+		ckexp(r, o);
+		if(o == Oquest && r.op != Ocolon)
+			error(n, "bad '?' operator");
+		if(o == Ocolon && parop != Oquest)
+			error(n, "bad ':' operator");
+	}
+}
+
+commas(n: ref Node): int
+{
+	if(n == nil || n.op == Ofun || n.op == Olfun)
+		return 0;
+	c := commas(n.left)+commas(n.right);
+	if(n.op == Ocomma)
+		c++;
+	return c;
+}
+
+allvar(n: ref Node)
+{
+	if(n == nil)
+		return;
+	if(n.op == Ocomma){
+		allvar(n.left);
+		allvar(n.right);
+		return;
+	}
+	var(n);
+}
+
+args(p: int): (int, ref Node)
+{
+	if(!p)
+		mustbe(Olbr);
+	a := exp(p, 1);
+	if(!p)
+		mustbe(Orbr);
+	na := 0;
+	if(a != nil)
+		na = commas(a)+1;
+	return (na, a);
+}
+
+hash(s: string): int
+{
+	l := len s;
+	h := 4104;
+	for(i := 0; i < l; i++)
+		h = 1729*h ^ s[i];
+	if(h < 0)
+		h = -h;
+	return h&(Hash-1);
+}
+
+enter(sp: string, k: int): ref Sym
+{
+	for(s := syms[hash(sp)]; s != nil; s = s.next){
+		if(sp == s.name)
+			return s;
+	}
+	s = ref Sym;
+	s.name = sp;
+	s.kind = k;
+	h := hash(sp);
+	s.next = syms[h];
+	syms[h] = s;
+	return s;
+}
+
+lookup(sp: string): ref Sym
+{
+	return enter(sp, Oident);
+}
+
+mkdec(s: ref Sym, k: int, dec: int): ref Dec
+{
+	d := ref Dec;
+	d.kind = k;
+	d.val = 0.0;
+	d.na = 0;
+	d.sym = s;
+	d.scope = 0;
+	if(dec)
+		pushdec(d);
+	return d;
+}
+
+adddec(sp: string, k: int, v: real, n: int): ref Dec
+{
+	d := mkdec(enter(sp, Oident), k, 1);
+	d.val = v;
+	d.na = n;
+	return d;
+}
+
+scope: int;
+curscope: ref Dec;
+scopes: list of ref Dec;
+
+pushscope()
+{
+	scope++;
+	scopes = curscope :: scopes;
+	curscope = nil;
+}
+
+popscope()
+{
+	popdecs();
+	curscope = hd scopes;
+	scopes = tl scopes;
+	scope--;
+}
+
+pushparams(n: ref Node)
+{
+	if(n == nil)
+		return;
+	if(n.op == Ocomma){
+		pushparams(n.left);
+		pushparams(n.right);
+		return;
+	}
+	n.dec = mkdec(n.dec.sym, Ovar, 1);
+}
+
+pushdec(d: ref Dec)
+{
+	if(0 && debug)
+		sys->print("dec %s scope %d\n", d.sym.name, scope);
+	d.scope = scope;
+	s := d.sym;
+	if(s.dec != nil && s.dec.scope == scope)
+		error(nil, sys->sprint("redeclaration of %s", s.name));
+	d.old = s.dec;
+	s.dec = d;
+	d.next = curscope;
+	curscope = d;
+}
+
+popdecs()
+{
+	nd: ref Dec;
+	for(d := curscope; d != nil; d = nd){
+		d.sym.dec = d.old;
+		d.old = nil;
+		nd = d.next;
+		d.next = nil;
+	}
+	curscope = nil;
+}
+
+estat(n: ref Node): (int, real)
+{
+	k: int;
+	v: real;
+
+	if(n == nil)
+		return (Onothing, 0.0);
+	l := n.left;
+	r := n.right;
+	case(n.op){
+	Ocomma =>
+		(k, v) = estat(l);
+		if(k == Oexit || k == Oret || k == Obreak || k == Ocont)
+			return (k, v);
+		return estat(r);
+	Oprint =>
+		v = print(l);
+		return (Onothing, v);
+	Oread =>
+		v = read(l);
+		return (Onothing, v);
+	Obreak or
+	Ocont or
+	Oexit =>
+		return (n.op, 0.0);
+	Oret =>
+		return (Oret, eval(l));
+	Oif =>
+		v = eval(l);
+		if(int v)
+			return estat(r.left);
+		else if(r.right != nil)
+			return estat(r.right);
+		else
+			return (Onothing, v);
+	Ofor =>
+		for(;;){
+			v = eval(l);
+			if(!int v)
+				break;
+			(k, v) = estat(r.left);
+			if(k == Oexit || k == Oret)
+				return (k, v);
+			if(k == Obreak)
+				break;
+			if(r.right != nil)
+				v = eval(r.right);
+		}
+		return (Onothing, v);
+	Odo =>
+		for(;;){
+			(k, v) = estat(l);
+			if(k == Oexit || k == Oret)
+				return (k, v);
+			if(k == Obreak)
+				break;
+			v = eval(r);
+			if(!int v)
+				break;
+		}
+		return (Onothing, v);
+	* =>
+		return (Onothing, eval(n));
+	}
+	return (Onothing, 0.0);
+}
+
+eval(e: ref Node): real
+{
+	lv, rv: real;
+
+	if(e == nil)
+		return 1.0;
+	o := e.op;
+	l := e.left;
+	r := e.right;
+	if(o != Ofun && o != Olfun)
+		lv = eval(l);
+	if(o != Oandand && o != Ooror && o != Oquest)
+		rv = eval(r);
+	case(o){
+	Ostring =>
+		return 0.0;
+	Onum =>
+		return e.val;
+	Ocon or
+	Ovar =>
+		return e.dec.val;
+	Ofun =>
+		return call(e.dec, l);
+	Olfun =>
+		return libfun(int e.dec.val, l);
+	Oadd =>
+		return lv+rv;
+	Osub =>
+		return lv-rv;
+	Omul =>
+		return lv*rv;
+	Odiv =>
+		return lv/rv;
+	Omod =>
+		return real (big lv%big rv);
+	Oidiv =>
+		return real (big lv/big rv);
+	Oand =>
+		return real (big lv&big rv);
+	Oor =>
+		return real (big lv|big rv);
+	Oxor =>
+		return real (big lv^big rv);
+	Olsh =>
+		return real (big lv<<int rv);
+	Orsh =>
+		return real (big lv>>int rv);
+	Oeq =>
+		return real (lv == rv);
+	One =>
+		return real (lv != rv);
+	Ogt =>
+		return real (lv > rv);
+	Olt =>
+		return real (lv < rv);
+	Oge =>
+		return real (lv >= rv);
+	Ole =>
+		return real (lv <= rv);
+	Opreinc =>
+		l.dec.val += 1.0;
+		return l.dec.val;
+	Opostinc =>
+		l.dec.val += 1.0;
+		return l.dec.val-1.0;
+	Opredec =>
+		l.dec.val -= 1.0;
+		return l.dec.val;
+	Opostdec =>
+		l.dec.val -= 1.0;
+		return l.dec.val+1.0;
+	Oexp =>
+		if(isinteger(rv) && rv >= 0.0)
+			return lv**int rv;
+		return maths->pow(lv, rv);
+	Oandand =>
+		if(!int lv)
+			return lv;
+		return eval(r);
+	Ooror =>
+		if(int lv)
+			return lv;
+		return eval(r);
+	Onot =>
+		return real !int lv;
+	Ofact =>
+		if(isinteger(lv) && lv >= 0.0){
+			n := int lv;
+			lv = 1.0;
+			for(i := 2; i <= n; i++)
+				lv *= real i;
+			return lv;
+		}
+		return gamma(lv+1.0);
+	Ocom =>
+		return real ~big lv;
+	Oas or
+	Odas =>
+		l.dec.val = rv;
+		return rv;
+	Oplus =>
+		return lv;
+	Ominus =>
+		return -lv;
+	Oinv =>
+		return 1.0/lv;
+	Ocomma =>
+		return rv;
+	Oquest =>
+		if(int lv)
+			return eval(r.left);
+		else
+			return eval(r.right);
+	Onand =>
+		return real !(int lv&int rv);
+	Onor =>
+		return real !(int lv|int rv);
+	Oimp =>
+		return real (!int lv|int rv);
+	Oimpby =>
+		return real (int lv|!int rv);
+	Oiff =>
+		return real !(int lv^int rv);
+	* =>
+		fatal(sys->sprint("case %s in eval", opstring(o)));
+	}
+	return 0.0;
+}
+
+var(e: ref Node)
+{
+	if(e == nil || e.op != Ovar || e.dec.kind != Ovar)
+		error(e, "expected a variable");
+}
+
+libfun(o: int, a: ref Node): real
+{
+	a1, a2: real;
+
+	case(o){
+	Osolve =>
+		return solve(a);
+	Osigma or
+	Opi or
+	Ocfrac =>
+		return series(o, a);
+	Oderiv =>
+		return differential(a);
+	Ointeg =>
+		return integral(a);
+	}
+	v := 0.0;
+	if(a != nil && a.op == Ocomma){
+		a1 = eval(a.left);
+		a2 = eval(a.right);
+	}
+	else
+		a1 = eval(a);
+	case(o){
+	Olog =>
+		v = maths->log(a1);
+	Olog10 =>
+		v = maths->log10(a1);
+	Olog2 =>
+		v = maths->log(a1)/maths->log(2.0);
+	Ologb =>
+		v = maths->log(a1)/maths->log(a2);
+	Oexpf =>
+		v = maths->exp(a1);
+	Opow =>
+		v = maths->pow(a1, a2);
+	Osqrt =>
+		v = maths->sqrt(a1);
+	Ocbrt =>
+		v = maths->cbrt(a1);
+	Ofloor =>
+		v = maths->floor(a1);
+	Oceil =>
+		v = maths->ceil(a1);
+	Omin =>
+		v = maths->fmin(a1, a2);
+	Omax =>
+		v = maths->fmax(a1, a2);
+	Oabs =>
+		v = maths->fabs(a1);
+	Ogamma =>
+		v = gamma(a1);
+	Osign =>
+		if(a1 > 0.0)
+			v = 1.0;
+		else if(a1 < 0.0)
+			v = -1.0;
+		else
+			v = 0.0;
+	Oint =>
+		(vi, nil) := maths->modf(a1);
+		v = real vi;
+	Ofrac =>
+		(nil, v) = maths->modf(a1);
+	Oround =>
+		v = maths->rint(a1);
+	Oerf =>
+		v = maths->erf(a1);
+	Osin =>
+		v = maths->sin(D2R(a1));
+	Ocos =>
+		v = maths->cos(D2R(a1));
+	Otan =>
+		v = maths->tan(D2R(a1));
+	Oasin =>
+		v = R2D(maths->asin(a1));
+	Oacos =>
+		v = R2D(maths->acos(a1));
+	Oatan =>
+		v = R2D(maths->atan(a1));
+	Oatan2 =>
+		v = R2D(maths->atan2(a1, a2));
+	Osinh =>
+		v = maths->sinh(a1);
+	Ocosh =>
+		v = maths->cosh(a1);
+	Otanh =>
+		v = maths->tanh(a1);
+	Oasinh =>
+		v = maths->asinh(a1);
+	Oacosh =>
+		v = maths->acosh(a1);
+	Oatanh =>
+		v = maths->atanh(a1);
+	Orand =>
+		v = real rand->rand(Big)/real Big;
+	* =>
+		fatal(sys->sprint("case %s in libfun", opstring(o)));
+	}
+	return v;
+}
+
+series(o: int, a: ref Node): real
+{
+	p0, p1, q0, q1: real;
+
+	l := a.left;
+	r := a.right;
+	if(o == Osigma)
+		v := 0.0;
+	else if(o == Opi)
+		v = 1.0;
+	else{
+		p0 = q1 = 0.0;
+		p1 = q0 = 1.0;
+		v = Infinity;
+	}
+	i := l.left.left.dec;
+	ov := i.val;
+	i.val = eval(l.left.right);
+	eq := 0;
+	for(;;){
+		rv := eval(l.right);
+		if(i.val > rv)
+			break;
+		lv := v;
+		ev := eval(r);
+		if(o == Osigma)
+			v += ev;
+		else if(o == Opi)
+			v *= ev;
+		else{
+			t := ev*p1+p0;
+			p0 = p1;
+			p1 = t;
+			t = ev*q1+q0;
+			q0 = q1;
+			q1 = t;
+			v = p1/q1;
+		}
+		if(v == lv && rv == Infinity){
+			eq++;
+			if(eq > 100)
+				break;
+		}
+		else
+			eq = 0;
+		i.val += 1.0;
+	}
+	i.val = ov;
+	return v;
+}
+
+pushe(a: ref Node, l: list of real): list of real
+{
+	if(a == nil)
+		return l;
+	if(a.op == Ocomma){
+		l = pushe(a.left, l);
+		return pushe(a.right, l);
+	}
+	l = eval(a) :: l;
+	return l;
+}
+
+pusha(f: ref Node, l: list of real, nl: list of real): (list of real, list of real)
+{
+	if(f == nil)
+		return (l, nl);
+	if(f.op == Ocomma){
+		(l, nl) = pusha(f.left, l, nl);
+		return pusha(f.right, l, nl);
+	}
+	l = f.dec.val :: l;
+	f.dec.val = hd nl;
+	return (l, tl nl);
+}
+
+pop(f: ref Node, l: list of real): list of real
+{
+	if(f == nil)
+		return l;
+	if(f.op == Ocomma){
+		l = pop(f.left, l);
+		return pop(f.right, l);
+	}
+	f.dec.val = hd l;
+	return tl l;
+}
+
+rev(l: list of real): list of real
+{
+	nl: list of real;
+	
+	for( ; l != nil; l = tl l)
+		nl = hd l :: nl;
+	return nl;
+}
+
+call(d: ref Dec, a: ref Node): real
+{
+	l: list of real;
+
+	nl := rev(pushe(a, nil));
+	(l, nil) = pusha(d.code.left, nil, nl);
+	l = rev(l);
+	(k, v) := estat(d.code.right);
+	l = pop(d.code.left, l);
+	if(k == Oexit)
+		exit;
+	return v;
+}
+
+print(n: ref Node): real
+{
+	if(n == nil)
+		return 0.0;
+	if(n.op == Ocomma){
+		print(n.left);
+		return print(n.right);
+	}
+	if(n.op == Ostring){
+		sys->print("%s", n.str);
+		return 0.0;
+	}
+	v := eval(n);
+	printnum(v, "");
+	return v;
+}
+
+read(n: ref Node): real
+{
+	bio: ref Iobuf;
+
+	if(n == nil)
+		return 0.0;
+	if(n.op == Ocomma){
+		read(n.left);
+		return read(n.right);
+	}
+	sys->print("%s ? ", n.dec.sym.name);
+	if(!stdin){
+		bio = bufio->fopen(sys->fildes(0), Sys->OREAD);
+		stack(nil, bio);
+	}
+	lexnum();
+	consume(Onl);
+	n.dec.val = lexval;
+	if(!stdin && bin == bio)
+		unstack();
+	return n.dec.val;
+}
+
+isint(v: real): int
+{
+	return v >= -real Maxint && v <= real Maxint;
+}
+
+isinteger(v: real): int
+{
+	return v == real int v && isint(v);
+}
+
+split(v: real): (int, real)
+{
+	# v >= 0.0
+	n := int v;
+	if(real n > v)
+		n--;
+	return (n, v-real n);
+}
+
+n2c(n: int): int
+{
+	if(n < 10)
+		return n+'0';
+	return n-10+'a';
+}
+
+gamma(v: real): real
+{
+	(s, lg) := maths->lgamma(v);
+	return real s*maths->exp(lg);
+}
+
+D2R(a: real): real
+{
+	if(deg.val != 0.0)
+		a *= Pi/180.0;
+	return a;
+}
+
+R2D(a: real): real
+{
+	if(deg.val != 0.0)
+		a /= Pi/180.0;
+	return a;
+}
+
+side(n: ref Node): int
+{
+	if(n == nil)
+		return 0;
+	if(asop(n.op) || n.op == Ofun)
+		return 1;
+	return side(n.left) || side(n.right);
+}
+
+sametree(n1: ref Node, n2: ref Node): int
+{
+	if(n1 == n2)
+		return 1;
+	if(n1 == nil || n2 == nil)
+		return 0;
+	if(n1.op != n2.op)
+		return 0;
+	case(n1.op){
+	Ostring =>
+		return n1.str == n2.str;
+	Onum =>
+		return n1.val == n2.val;
+	Ocon or
+	Ovar =>
+		return n1.dec == n2.dec;
+	Ofun or
+	Olfun =>
+		return n1.dec == n2.dec && sametree(n1.left, n2.left);
+	* =>
+		return sametree(n1.left, n2.left) && sametree(n1.right, n2.right);
+	}
+	return 0;
+}
+
+simplify(n: ref Node): ref Node
+{
+	if(n == nil)
+		return nil;
+	op := n.op;
+	l := n.left = simplify(n.left);
+	r := n.right = simplify(n.right);
+	if(l != nil && iscon(l) && (r == nil || iscon(r))){
+		if(isnan(l))
+			return l;
+		if(r != nil && isnan(r))
+			return r;
+		return vtree(eval(n));
+	}
+	case(op){
+		Onum or
+		Ocon or
+		Ovar or
+		Olfun or
+		Ocomma =>
+			return n;
+		Oplus =>
+			return l;
+		Ominus =>
+			if(l.op == Ominus)
+				return l.left;
+		Oinv =>
+			if(l.op == Oinv)
+				return l.left;
+		Oadd =>
+			if(iszero(l))
+				return r;
+			if(iszero(r))
+				return l;
+			if(sametree(l, r))
+				return tree(Omul, itree(2), l);
+		Osub =>
+			if(iszero(l))
+				return simplify(tree(Ominus, r, nil));
+			if(iszero(r))
+				return l;
+			if(sametree(l, r))
+				return itree(0);
+		Omul =>
+			if(iszero(l))
+				return l;
+			if(iszero(r))
+				return r;
+			if(isone(l))
+				return r;
+			if(isone(r))
+				return l;
+			if(sametree(l, r))
+				return tree(Oexp, l, itree(2));
+		Odiv =>
+			if(iszero(l))
+				return l;
+			if(iszero(r))
+				return vtree(Infinity);
+			if(isone(l))
+				return ptree(r, -1.0);
+			if(isone(r))
+				return l;
+			if(sametree(l, r))
+				return itree(1);
+		Oexp =>
+			if(iszero(l))
+				return l;
+			if(iszero(r))
+				return itree(1);
+			if(isone(l))
+				return l;
+			if(isone(r))
+				return l;
+		* =>
+			fatal(sys->sprint("case %s in simplify", opstring(op)));
+	}
+	return n;
+}
+
+deriv(n: ref Node, d: ref Dec): ref Node
+{
+	if(n == nil)
+		return nil;
+	op := n.op;
+	l := n.left;
+	r := n.right;
+	case(op){
+		Onum or
+		Ocon =>
+			n = itree(0);
+		Ovar =>
+			if(d == n.dec)
+				n = itree(1);
+			else
+				n = itree(0);
+		Olfun =>
+			case(int n.dec.val){
+				Olog =>
+					n = ptree(l, -1.0);
+				Olog10 =>
+					n = ptree(tree(Omul, l, vtree(Ln10)), -1.0);
+				Olog2 =>
+					n = ptree(tree(Omul, l, vtree(Ln2)), -1.0);
+				Oexpf =>
+					n = n;
+				Opow =>
+					return deriv(tree(Oexp, l.left, l.right), d);
+				Osqrt =>
+					return deriv(tree(Oexp, l, vtree(0.5)), d);
+				Ocbrt =>
+					return deriv(tree(Oexp, l, vtree(1.0/3.0)), d);
+				Osin =>
+					n = ltree("cos", l);
+				Ocos =>
+					n = tree(Ominus, ltree("sin", l), nil);
+				Otan =>
+					n = ptree(ltree("cos", l), -2.0);
+				Oasin =>
+					n = ptree(tree(Osub, itree(1), ptree(l, 2.0)), -0.5);
+				Oacos =>
+					n = tree(Ominus, ptree(tree(Osub, itree(1), ptree(l, 2.0)), -0.5), nil);
+				Oatan =>
+					n = ptree(tree(Oadd, itree(1), ptree(l, 2.0)), -1.0);
+				Osinh =>
+					n = ltree("cosh", l);
+				Ocosh =>
+					n = ltree("sinh", l);
+				Otanh =>
+					n = ptree(ltree("cosh", l), -2.0);
+				Oasinh =>
+					n = ptree(tree(Oadd, itree(1), ptree(l, 2.0)), -0.5);
+				Oacosh =>
+					n = ptree(tree(Osub, ptree(l, 2.0), itree(1)), -0.5);
+				Oatanh =>
+					n = ptree(tree(Osub, itree(1), ptree(l, 2.0)), -1.0);
+				* =>
+					return vtree(Nan);
+			}
+			return tree(Omul, n, deriv(l, d));
+		Oplus or
+		Ominus =>
+			n = tree(op, deriv(l, d), nil);
+		Oinv =>
+			n = tree(Omul, tree(Ominus, ptree(l, -2.0), nil), deriv(l, d));
+		Oadd or
+		Osub or
+		Ocomma =>
+			n = tree(op, deriv(l, d), deriv(r, d));
+		Omul =>
+			n = tree(Oadd, tree(Omul, deriv(l, d), r), tree(Omul, l, deriv(r, d)));
+		Odiv =>
+			n = tree(Osub, tree(Omul, deriv(l, d), r), tree(Omul, l, deriv(r, d)));
+			n = tree(Odiv, n, ptree(r, 2.0));
+		Oexp =>
+			nn := tree(Oadd, tree(Omul, deriv(l, d), tree(Odiv, r, l)), tree(Omul, ltree("log", l), deriv(r, d)));
+			n = tree(Omul, n, nn);
+		* =>
+			n = vtree(Nan);
+	}
+	return n;
+}
+
+derivative(n: ref Node, d: ref Dec): ref Node
+{
+	n = simplify(deriv(n, d));
+	if(isnan(n))
+		error(n, "no derivative");
+	if(debug)
+		prnode(n);
+	return n;
+}
+
+newton(f: ref Node, e: ref Node, d: ref Dec, v1: real, v2: real): (int, real)
+{
+	v := (v1+v2)/2.0;
+	lv := 0.0;
+	its := 0;
+	for(;;){
+		lv = v;
+		d.val = v;
+		v = eval(e);
+		# if(v < v1 || v > v2)
+		#	return (0, 0.0);
+		if(maths->isnan(v))
+			return (0, 0.0);
+		if(its > 100 || fabs(v-lv) < Eps)
+			break;
+		its++;
+	}
+	if(fabs(v-lv) > Bigeps || fabs(eval(f)) > Bigeps)
+		return (0, 0.0);
+	return (1, v);
+}
+
+solve(n: ref Node): real
+{
+	d: ref Dec;
+
+	if(n == nil)
+		return Nan;
+	if(n.op == Ocomma){	# solve(..., var)
+		var(n.right);
+		d = n.right.dec;
+		n = n.left;
+		if(!varmem(n, d))
+			error(n, "variable not in equation");
+	}
+	else{
+		d = findvar(n, nil);
+		if(d == nil)
+			error(n, "variable missing");
+		if(d == errdec)
+			error(n, "one variable only required");
+	}
+	if(n.op == Oeq)
+		n.op = Osub;
+	dn := derivative(n, d);
+	var := tree(Ovar, nil, nil);
+	var.dec = d;
+	nr := tree(Osub, var, tree(Odiv, n, dn));
+	ov := d.val;
+	lim := lookup(Limit).dec.val;
+	step := lookup(Step).dec.val;
+	rval := Infinity;
+	d.val = -lim-step;
+	v1 := 0.0;
+	v2 := eval(n);
+	for(v := -lim; v <= lim; v += step){
+		d.val = v;
+		v1 = v2;
+		v2 = eval(n);
+		if(maths->isnan(v2))	# v == nan, v <= nan, v >= nan all give 1
+			continue;
+		if(fabs(v2) < Eps){
+			if(v >= -lim && v <= lim && v != rval){
+				printnum(v, " ");
+				rval = v;
+			}
+		}
+		else if(v1*v2 <= 0.0){
+			(f, rv) := newton(n, nr, var.dec, v-step, v);
+			if(f && rv >= -lim && rv <= lim && rv != rval){
+				printnum(rv, " ");
+				rval = rv;
+			}
+		}
+	}
+	d.val = ov;
+	if(rval == Infinity)
+		error(n, "no roots found");
+	else
+		sys->print("\n");
+	return rval;
+}
+
+differential(n: ref Node): real
+{
+	x := n.left.left.dec;
+	ov := x.val;
+	v := evalx(derivative(n.right, x), x, eval(n.left.right));
+	x.val = ov;
+	return v;
+}
+
+integral(n: ref Node): real
+{
+	l := n.left;
+	r := n.right;
+	x := l.left.left.dec;
+	ov := x.val;
+	a := eval(l.left.right);
+	b := eval(l.right);
+	h := b-a;
+	end := evalx(r, x, a) + evalx(r, x, b);
+	odd := even := 0.0;
+	oldarea := 0.0;
+	area := h*end/2.0;
+	for(i := 1; i < 1<<16; i <<= 1){
+		even += odd;
+		odd = 0.0;
+		xv := a+h/2.0;
+		for(j := 0; j < i; j++){
+			odd += evalx(r, x, xv);
+			xv += h;
+		}
+		h /= 2.0;
+		oldarea = area;
+		area = h*(end+4.0*odd+2.0*even)/3.0;
+		if(maths->isnan(area))
+			error(n, "integral not found");
+		if(fabs(area-oldarea) < Eps)
+			break;
+	}
+	if(fabs(area-oldarea) > Bigeps)
+		error(n, "integral not found");
+	x.val = ov;
+	return area;
+}
+
+evalx(n: ref Node, d: ref Dec, v: real): real
+{
+	d.val = v;
+	return eval(n);
+}
+
+findvar(n: ref Node, d: ref Dec): ref Dec
+{
+	if(n == nil)
+		return d;
+	d = findvar(n.left, d);
+	d = findvar(n.right, d);
+	if(n.op == Ovar){
+		if(d == nil)
+			d = n.dec;
+		if(n.dec != d)
+			d = errdec;
+	}
+	return d;
+}
+
+varmem(n: ref Node, d: ref Dec): int
+{
+	if(n == nil)
+		return 0;
+	if(n.op == Ovar)
+		return d == n.dec;
+	return varmem(n.left, d) || varmem(n.right, d);
+}
+
+fabs(r: real): real
+{
+	if(r < 0.0)
+		return -r;
+	return r;
+}
+
+cvt(v: real, base: int): string
+{
+	if(base == 10)
+		return sys->sprint("%g", v);
+	neg := 0;
+	if(v < 0.0){
+		neg = 1;
+		v = -v;
+	}
+	if(!isint(v)){
+		n := 0;
+		lg := maths->log(v)/maths->log(real base);
+		if(lg < 0.0){
+			(n, nil) = split(-lg);
+			v *= real base**n;
+			n = -n;
+		}
+		else{
+			(n, nil) = split(lg);
+			v /= real base**n;
+		}
+		s := cvt(v, base) + "E" + string n;
+		if(neg)
+			s = "-" + s;
+		return s;
+	}
+	(n, f) := split(v);
+	s := "";
+	do{
+		r := n%base;
+		n /= base;
+		s[len s] = n2c(r);
+	}while(n != 0);
+	ls := len s;
+	for(i := 0; i < ls/2; i++){
+		t := s[i];
+		s[i] = s[ls-1-i];
+		s[ls-1-i] = t;
+	}
+	if(f != 0.0){
+		s[len s] = '.';
+		for(i = 0; i < 16 && f != 0.0; i++){
+			f *= real base;
+			(n, f) = split(f);
+			s[len s] = n2c(n);
+		}
+	}
+	s = string base + "r" + s;
+	if(neg)
+		s = "-" + s;
+	return s;
+}
+
+printnum(v: real, s: string)
+{
+	base := int pbase.val;
+	if(!isinteger(pbase.val) || base < 2 || base > 36)
+		base = 10;
+	sys->print("%s%s", cvt(v, base), s);
+	if(bits){
+		r := array[1] of real;
+		b := array[8] of byte;
+		r[0] = v;
+		maths->export_real(b, r);
+		for(i := 0; i < 8; i++)
+			sys->print("%2.2x ", int b[i]);
+		sys->print("\n");
+	}
+}
+
+Left, Right, Pre, Post: con 1<<iota;
+
+lspace := array[] of { 0, 0, 2, 3, 4, 5, 0, 0, 0, 9, 10, 0, 0, 0, 0, 0, 0, 0 };
+rspace := array[] of { 0, 1, 2, 3, 4, 5, 0, 0, 0, 9, 10, 0, 0, 0, 0, 0, 0, 0 };
+
+preced(op1: int, op2: int, s: int): int
+{
+	br := 0;
+	p1 := prec(op1);
+	p2 := prec(op2);
+	if(p1 > p2)
+		br = 1;
+	else if(p1 == p2){
+		if(op1 == op2){
+			if(rassoc(op1))
+				br = s == Left;
+			else
+				br = s == Right && !assoc(op1);
+		}
+		else{
+			if(rassoc(op1))
+				br = s == Left;
+			else
+				br = s == Right && op1 != Oadd;
+			if(postunary(op1) && preunary(op2))
+				br = 1;
+		}
+	}
+	return br;
+}
+
+prnode(n: ref Node)
+{
+	pnode(n, Onothing, Pre);
+	sys->print("\n");
+}
+
+pnode(n: ref Node, opp: int, s: int)
+{
+	if(n == nil)
+		return;
+	op := n.op;
+	if(br := preced(opp, op, s))
+		sys->print("(");
+	if(op == Oas && n.right.op >= Oadd && n.right.op <= Orsh && n.left == n.right.left){
+		pnode(n.left, op, Left);
+		sys->print(" %s ", opstring(n.right.op+Oadde-Oadd));
+		pnode(n.right.right, op, Right);
+	}
+	else if(binary(op)){
+		p := prec(op);
+		pnode(n.left, op, Left);
+		if(lspace[p])
+			sys->print(" ");
+		sys->print("%s", opstring(op));
+		if(rspace[p])
+			sys->print(" ");
+		pnode(n.right, op, Right);
+	}
+	else if(op == Oinv){	# cannot print postunary -1
+		sys->print("%s", opstring(op));
+		pnode(n.left, Odiv, Right);
+	}
+	else if(preunary(op)){
+		sys->print("%s", opstring(op));
+		pnode(n.left, op, Pre);
+	}
+	else if(postunary(op)){
+		pnode(n.left, op, Post);
+		sys->print("%s", opstring(op));
+	}
+	else{
+		case(op){
+		Ostring =>
+			sys->print("%s", n.str);
+		Onum =>
+			sys->print("%g", n.val);
+		Ocon or
+		Ovar =>
+			sys->print("%s", n.dec.sym.name);
+		Ofun or
+		Olfun =>
+			sys->print("%s(", n.dec.sym.name);
+			pnode(n.left, Onothing, Pre);
+			sys->print(")");
+		* =>
+			fatal(sys->sprint("bad op %s in pnode()", opstring(op)));
+		}
+	}
+	if(br)
+		sys->print(")");
+}
--- /dev/null
+++ b/appl/cmd/cat.b
@@ -1,0 +1,48 @@
+implement Cat;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+Cat: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stdout: ref Sys->FD;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdout = sys->fildes(1);
+	args = tl args;
+	if(args == nil)
+		args = "-" :: nil;
+	for(; args != nil; args = tl args){
+		file := hd args;
+		if(file != "-"){
+			fd := sys->open(file, Sys->OREAD);
+			if(fd == nil){
+				sys->fprint(sys->fildes(2), "cat: cannot open %s: %r\n", file);
+				raise "fail:bad open";
+			}
+			cat(fd, file);
+		}else
+			cat(sys->fildes(0), "<stdin>");
+	}
+}
+
+cat(fd: ref Sys->FD, file: string)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0)
+		if(sys->write(stdout, buf, n) < n) {
+			sys->fprint(sys->fildes(2), "cat: write error: %r\n");
+			raise "fail:write error";
+		}
+	if(n < 0) {
+		sys->fprint(sys->fildes(2), "cat: error reading %s: %r\n", file);
+		raise "fail:read error";
+	}
+}
--- /dev/null
+++ b/appl/cmd/cd.b
@@ -1,0 +1,48 @@
+implement Cd;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+Cd: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	stderr = sys->fildes(2);
+
+	argv = tl argv;
+	if(argv == nil)
+		argv = "/usr/"+user() :: nil;
+
+	if(tl argv != nil) {
+		sys->fprint(stderr, "Usage: cd [directory]\n");
+		raise "fail:usage";
+	}
+
+	if(sys->chdir(hd argv) < 0) {
+		sys->fprint(stderr, "cd: %s: %r\n", hd argv);
+		raise "fail:failed";
+	}
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "inferno";
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return "inferno";
+
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/cmd/cddb.b
@@ -1,0 +1,247 @@
+implement Cddb;
+
+# this is a near transliteration of Plan 9 source, and subject to the Lucent Public License 1.02
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "dial.m";
+	dial: Dial;
+
+include "arg.m";
+
+Cddb: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+server := "freedb.freedb.org";
+debug := 0;
+tflag := 0;
+Tflag := 0;
+
+Track: adt {
+	n:	int;
+	title:	string;
+};
+
+Toc: adt {
+	diskid:	int;
+	ntrack:	int;
+	title:	string;
+	track:	array of Track;
+};
+
+DPRINT(fd: int, s: string)
+{
+	if(debug)
+		sys->fprint(sys->fildes(fd), "%s", s);
+}
+
+dumpcddb(t: ref Toc)
+{
+	sys->print("title	%s\n", t.title);
+	for(i:=0; i<t.ntrack; i++){
+		if(tflag){
+			n := t.track[i+1].n;
+			if(i == t.ntrack-1)
+				n *= 75;
+			s := (n - t.track[i].n)/75;
+			sys->print("%d\t%s\t%d:%2.2d\n", i+1, t.track[i].title, s/60, s%60);
+		}
+		else
+			sys->print("%d\t%s\n", i+1, t.track[i].title);
+	}
+	if(Tflag){
+		s := t.track[i].n;
+		sys->print("Total time: %d:%2.2d\n", s/60, s%60);
+	}
+}
+
+cddbfilltoc(t: ref Toc): int
+{
+	conn := dial->dial(dial->netmkaddr(server, "tcp", "888"), nil);
+	if(conn == nil){
+		sys->fprint(sys->fildes(2), "cddb: cannot dial %s: %r\n", server);
+		return -1;
+	}
+	bin := bufio->fopen(conn.dfd, Bufio->OREAD);
+
+	if((p:=getline(bin)) == nil || atoi(p)/100 != 2)
+		return died(p);
+
+	sys->fprint(conn.dfd, "cddb hello gre plan9 9cd 1.0\r\n");
+	if((p = getline(bin)) == nil || atoi(p)/100 != 2)
+		return died(p);
+
+	#
+	#	Protocol level 6 is the same as level 5 except that
+	#	the character set is now UTF-8 instead of ISO-8859-1. 
+	#
+	sys->fprint(conn.dfd, "proto 6\r\n");
+	if((p = getline(bin)) == nil || atoi(p)/100 != 2)
+		return died(p);
+	DPRINT(2, sys->sprint("%s\n", p));
+
+	sys->fprint(conn.dfd, "cddb query %8.8ux %d", t.diskid, t.ntrack);
+	DPRINT(2, sys->sprint("cddb query %8.8ux %d", t.diskid, t.ntrack));
+	for(i:=0; i<t.ntrack; i++) {
+		sys->fprint(conn.dfd, " %d", t.track[i].n);
+		DPRINT(2, sys->sprint(" %d", t.track[i].n));
+	}
+	sys->fprint(conn.dfd, " %d\r\n", t.track[t.ntrack].n);
+	DPRINT(2, sys->sprint(" %d\r\n", t.track[t.ntrack].n));
+
+	if((p = getline(bin)) == nil || atoi(p)/100 != 2)
+		return died(p);
+	DPRINT(2, sys->sprint("cddb: %s\n", p));
+	(nf, fl) := sys->tokenize(p, " \t\n\r");
+	if(nf < 1)
+		return died(p);
+
+	categ, id: string;
+	case atoi(hd fl) {
+	200 =>	# exact match
+		if(nf < 3)
+			return died(p);
+		categ = hd tl fl;
+		id = hd tl tl fl;
+	210 or	# exact matches
+	211 =>	# close matches
+		if((p = getline(bin)) == nil)
+			return died(nil);
+		if(p[0] == '.')	# no close matches?
+			return died(nil);
+
+		# accept first match
+		(nsf, f) := sys->tokenize(p, " \t\n\r");
+		if(nsf < 2)
+			return died(p);
+		categ = hd f;
+		id = hd tl f;
+
+		# snarf rest of buffer
+		while(p[0] != '.') {
+			if((p = getline(bin)) == nil)
+				return died(p);
+			DPRINT(2, sys->sprint("cddb: %s\n", p));
+		}
+	202 or	# no match
+	* =>
+		return died(p);
+	}
+
+	t.title = "";
+	for(i=0; i<t.ntrack; i++)
+		t.track[i].title = "";
+
+	# fetch results for this cd
+	sys->fprint(conn.dfd, "cddb read %s %s\r\n", categ, id);
+	do {
+		if((p = getline(bin)) == nil)
+			return died(nil);
+DPRINT(2, sys->sprint("cddb %s\n", p));
+		if(len p >= 7 && p[0:7] == "DTITLE=")
+			t.title += p[7:];
+		else if(len p >= 7 && p[0:6] == "TTITLE"&& isdigit(p[6])) {
+			i = atoi(p[6:]);
+			if(i < t.ntrack) {
+				p = p[6:];
+				while(p != nil && isdigit(p[0]))
+					p = p[1:];
+				if(p != nil && p[0] == '=')
+					p = p[1:];
+				t.track[i].title += p;
+			}
+		} 
+	} while(p[0] != '.');
+
+	sys->fprint(conn.dfd, "quit\r\n");
+
+	return 0;
+}
+
+getline(f: ref Iobuf): string
+{
+	p := f.gets('\n');
+	while(p != nil && isspace(p[len p-1]))
+		p = p[0: len p-1];
+	return p;
+}
+
+isdigit(c: int): int
+{
+	return c>='0' && c <= '9';
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+}
+
+died(p: string): int
+{
+	sys->fprint(sys->fildes(2), "cddb: error talking to server\n");
+	if(p != nil){
+		p = p[0:len p-1];
+		sys->fprint(sys->fildes(2), "cddb: server says: %s\n", p);
+	}
+	return -1;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	dial = load Dial Dial->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("cddb [-DTt] [-s server] query diskid n ...");
+	while((o := arg->opt()) != 0)
+	case o {
+	'D' =>	debug = 1;
+	's' =>	server = arg->earg();
+	'T' =>	Tflag = 1; tflag = 1;
+	't' =>	tflag = 1;
+	* =>	arg->usage();
+	}
+	args = arg->argv();
+	argc := len args;
+	if(argc < 3 || hd args != "query")
+		arg->usage();
+	arg = nil;
+
+	ntrack := atoi(hd tl tl args);
+	toc := ref Toc(str->toint(hd tl args, 16).t0, ntrack, nil, array[ntrack+1] of Track);
+	if(argc != 3+toc.ntrack+1){
+		sys->fprint(sys->fildes(2), "cddb: argument count does not match given ntrack");
+		raise "fail:error";
+	}
+	args = tl tl tl args;
+
+	for(i:=0; i<=toc.ntrack; i++){	# <=?
+		toc.track[i].n = atoi(hd args);
+		args = tl args;
+	}
+
+	if(cddbfilltoc(toc) < 0)
+		raise "fail:whoops";
+
+	dumpcddb(toc);
+}
+
+atoi(s: string): int
+{
+	return int s;
+}
--- /dev/null
+++ b/appl/cmd/chgrp.b
@@ -1,0 +1,58 @@
+implement Chgrp;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+Chgrp: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: chgrp [-uo] group file ...\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil){
+		sys->fprint(sys->fildes(2), "chgrp: can't load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+	setuser := 0;
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		'o' or 'u' =>
+			setuser = 1;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	if(args == nil)
+		usage();
+	id := hd args;
+	err := 0;
+	while((args = tl args) != nil){
+		d := sys->nulldir;
+		if(setuser)
+			d.uid = id;
+		else
+			d.gid = id;
+		if(sys->wstat(hd args, d) < 0){
+			sys->fprint(sys->fildes(2), "chgrp: can't change %s: %r\n", hd args);
+			err = 1;
+		}
+	}
+	if(err)
+		raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/chmod.b
@@ -1,0 +1,125 @@
+implement Chmod;
+
+include "sys.m";
+include "draw.m";
+include "string.m";
+
+Chmod: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+sys:	Sys;
+stderr: ref Sys->FD;
+
+str:	String;
+
+User:	con 8r700;
+Group:	con 8r070;
+Other:	con 8r007;
+All:	con User | Group | Other;
+
+Read:	con 8r444;
+Write:	con 8r222;
+Exec:	con 8r111;
+
+usage()
+{
+	sys->fprint(stderr, "usage: chmod [8r]777 file ... or chmod [augo][+-=][rwxal] file ...\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	str = load String String->PATH;
+	if(str == nil){
+		sys->fprint(stderr, "chmod: cannot load %s: %r\n", String->PATH);
+		raise "fail:bad module";
+	}
+
+	if(len argv < 3)
+		usage();
+	argv = tl argv;
+	m := hd argv;
+	argv = tl argv;
+
+	mask := All;
+	if (str->prefix("8r", m))
+		m = m[2:];
+	(mode, s) := str->toint(m, 8);
+	if(s != "" || m == ""){
+		ok := 0;
+		(ok, mask, mode) = parsemode(m);
+		if(!ok){
+			sys->fprint(stderr, "chmod: bad mode '%s'\n", m);
+			usage();
+		}
+	}
+	ndir := sys->nulldir;
+	for(; argv != nil; argv = tl argv){
+		f := hd argv;
+		(ok, dir) := sys->stat(f);
+		if(ok < 0){
+			sys->fprint(stderr, "chmod: cannot stat %s: %r\n", f);
+			continue;
+		}
+		ndir.mode = (dir.mode & ~mask) | (mode & mask);
+		if(sys->wstat(f, ndir) < 0)
+			sys->fprint(stderr, "chmod: cannot wstat %s: %r\n", f);
+	}
+}
+
+parsemode(spec: string): (int, int, int)
+{
+	mask := Sys->DMAPPEND | Sys->DMEXCL | Sys->DMTMP;
+loop:	for(i := 0; i < len spec; i++){
+		case spec[i] {
+		'u' =>
+			mask |= User;
+		'g' =>
+			mask |= Group;
+		'o' =>
+			mask |= Other;
+		'a' =>
+			mask |= All;
+		* =>
+			break loop;
+		}
+	}
+	if(i == len spec)
+		return (0, 0, 0);
+	if(i == 0)
+		mask |= All;
+
+	op := spec[i++];
+	if(op != '+' && op != '-' && op != '=')
+		return (0, 0, 0);
+
+	mode := 0;
+	for(; i < len spec; i++){
+		case spec[i]{
+		'r' =>
+			mode |= Read;
+		'w' =>
+			mode |= Write;
+		'x' =>
+			mode |= Exec;
+		'a' =>
+			mode |= Sys->DMAPPEND;
+		'l' =>
+			mode |= Sys->DMEXCL;
+		't' =>
+			mode |= Sys->DMTMP;
+		* =>
+			return (0, 0, 0);
+		}
+	}
+	if(op == '+' || op == '-')
+		mask &= mode;
+	if(op == '-')
+		mode = ~mode;
+	return (1, mask, mode);
+}
--- /dev/null
+++ b/appl/cmd/cleanname.b
@@ -1,0 +1,45 @@
+implement Cleanname;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "names.m";
+	names: Names;
+
+include "arg.m";
+
+Cleanname: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	names = load Names Names->PATH;
+	arg := load Arg Arg->PATH;
+
+	dir: string;
+	arg->init(args);
+	arg->setusage("cleanname [-d pwd] name ...");
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' =>
+			dir = arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	for(; args != nil; args = tl args){
+		n := hd args;
+		if(dir != nil && n != nil && n[0] != '/' && n[0] != '#')
+			n = dir+"/"+n;
+		sys->print("%s\n", names->cleanname(n));	# %q?
+	}
+}
--- /dev/null
+++ b/appl/cmd/cmp.b
@@ -1,0 +1,151 @@
+implement Cmp;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "arg.m";
+
+BUF: con 65536;
+stderr: ref Sys->FD;
+
+Cmp: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	lflag := Lflag := sflag := 0;
+	buf1 := array[BUF] of byte;
+	buf2 := array[BUF] of byte;
+
+	stderr = sys->fildes(2);
+
+	arg := load Arg Arg->PATH;	
+	if(arg == nil){
+		sys->fprint(stderr, "cmp: cannot load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+	arg->init(args);
+	while((op := arg->opt()) != 0)
+		case op {
+		'l' =>		lflag = 1;
+		'L' =>	Lflag = 1;
+		's' =>		sflag = 1;
+		* =>		usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	if(args == nil)
+		usage();
+
+	if(len args < 2)
+		usage();
+	name1 := hd args;
+	args = tl args;
+
+	if((f1 := sys->open(name1, Sys->OREAD)) == nil){
+		sys->fprint(stderr, "cmp: can't open %s: %r\n",name1);
+		raise "fail:open";
+	}
+	name2 := hd args;
+	args = tl args;
+
+	if((f2 := sys->open(name2, Sys->OREAD)) == nil){
+		sys->fprint(stderr, "cmp: can't open %s: %r\n",name2);
+		raise "fail:open";
+	}
+
+	if(args != nil){
+		o := big hd args;
+		if(sys->seek(f1, o, 0) < big 0){
+			sys->fprint(stderr, "cmp: seek by offset1 failed: %r\n");
+			raise "fail:seek 1";
+		}
+		args = tl args;
+	}
+
+	if(args != nil){
+		o := big hd args;
+		if(sys->seek(f2, o, 0) < big 0){
+			sys->fprint(stderr, "cmp: seek by offset2 failed: %r");
+			raise "fail:seek 2";
+		}
+		args = tl args;
+	}
+	if(args != nil)
+		usage();
+	nc := big 1;
+	l := big 1;
+	diff := 0;
+	b1, b2: array of byte;
+	for(;;){
+		if(len b1 == 0){
+			nr := sys->read(f1, buf1, BUF);
+			if(nr < 0){
+				if(!sflag)
+					sys->print("error on %s after %bd bytes\n", name1, nc-big 1);
+				raise "fail:read error";
+			}
+			b1 = buf1[0: nr];
+		}
+		if(len b2 == 0){
+			nr := sys->read(f2, buf2, BUF);
+			if(nr < 0){
+				if(!sflag)
+					sys->print("error on %s after %bd bytes\n", name2, nc-big 1);
+				raise "fail:read error";
+			}
+			b2 = buf2[0: nr];
+		}
+		n := len b2;
+		if(n > len b1)
+			n = len b1;
+		if(n == 0)
+			break;
+		for(i:=0; i<n; i++){
+			if(Lflag && b1[i]== byte '\n')
+				l++;
+			if(b1[i] != b2[i]){
+				if(!lflag){
+					if(!sflag){
+						sys->print("%s %s differ: char %bd", name1, name2, nc+big i);
+						if(Lflag)
+							sys->print(" line %bd\n", l);
+						else
+							sys->print("\n");
+					}
+					raise "fail:differ";
+				}
+				sys->print("%6bd 0x%.2x 0x%.2x\n", nc+big i, int b1[i], int b2[i]);
+				diff = 1;
+			}
+		}
+		nc += big n;
+		b1 = b1[n:];
+		b2 = b2[n:];
+	}
+	if(len b1 != len b2) {
+		nc--;
+		if(len b1 > len b2)
+			sys->print("EOF on %s after %bd bytes\n", name2, nc);
+		else 
+			sys->print("EOF on %s after %bd bytes\n", name1, nc);
+		raise "fail:EOF";
+	}
+	if(diff)
+		raise "fail:differ";
+	exit;
+}
+
+
+usage() 
+{
+	sys->fprint(stderr, "Usage: cmp [-lsL] file1 file2 [offset1 [offset2] ]\n");
+	raise "fail:usage";
+}
--- /dev/null
+++ b/appl/cmd/comm.b
@@ -1,0 +1,124 @@
+implement Comm;
+
+# Copyright © 2002 Lucent Technologies Inc.
+# Subject to the Lucent Public Licence 1.02
+# Limbo translation by Vita Nuova 2004; bug fixed.
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+
+Comm: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+One, Two, Three: con 1<<iota;
+cols := One|Two|Three;
+ldr := array[3] of {"", "\t", "\t\t"};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("comm [-123] file1 file2");
+	while((c := arg->opt()) != 0){
+		case c {
+		'1' to '3' =>
+			cols &= ~(1 << (c-'1'));
+		* =>
+			arg->usage();
+		}
+	}
+	args = arg->argv();
+	if(len args != 2)
+		arg->usage();
+	arg = nil;
+
+	if((cols & One) == 0){
+		ldr[1] = "";
+		ldr[2] = ldr[2][1:];
+	}
+	if((cols & Two) == 0)
+		ldr[2] = ldr[2][1:];
+
+	ib1 := openfil(hd args);
+	ib2 := openfil(hd tl args);
+	if((lb1 := ib1.gets('\n')) == nil){
+		if((lb2 := ib2.gets('\n')) == nil)
+			exit;
+		copy(ib2, lb2, 2);
+	}
+	if((lb2 := ib2.gets('\n')) == nil)
+		copy(ib1, lb1, 1);
+	for(;;)
+		case compare(lb1, lb2) {
+		0 =>
+			wr(lb1, 3);
+			if((lb1 = ib1.gets('\n')) == nil){
+				if((lb2 = ib2.gets('\n')) == nil)
+					exit;
+				copy(ib2, lb2, 2);
+			}
+			if((lb2 = ib2.gets('\n')) == nil)
+				copy(ib1, lb1, 1);
+		1 =>
+			wr(lb1, 1);
+			if((lb1 = ib1.gets('\n')) == nil)
+				copy(ib2, lb2, 2);
+		2 =>
+			wr(lb2, 2);
+			if((lb2 = ib2.gets('\n')) == nil)
+				copy(ib1, lb1, 1);
+		}
+}
+
+wr(str: string, n: int)
+{
+	if(cols & (1<<(n-1)))
+		sys->print("%s%s", ldr[n-1], str);
+}
+
+copy(ibuf: ref Iobuf, lbuf: string, n: int)
+{
+	do
+		wr(lbuf, n);
+	while((lbuf = ibuf.gets('\n')) != nil);
+	exit;
+}
+
+compare(a: string, b: string): int
+{
+	for(i := 0; i < len a; i++){
+		if(i >= len b || a[i] < b[i])
+			return 1;
+		if(a[i] != b[i])
+			return 2;
+	}
+	if(i == len b)
+		return 0;
+	return 2;
+}
+
+openfil(s: string): ref Iobuf
+{
+	if(s == "-")
+		b := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+	else
+		b = bufio->open(s, Bufio->OREAD);
+	if(b != nil)
+		return b;
+	sys->fprint(sys->fildes(2), "comm: cannot open %s: %r\n", s);
+	raise "fail:open";
+}
+
--- /dev/null
+++ b/appl/cmd/cook.b
@@ -1,0 +1,1924 @@
+implement Cook;
+
+include "sys.m";
+	sys: Sys;
+	FD: import Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "bufio.m";
+	B: Bufio;
+	Iobuf: import B;
+
+include "string.m";
+	S: String;
+	splitl, splitr, splitstrl, drop, take, in, prefix, tolower : import S;
+
+include "brutus.m";
+	Size6, Size8, Size10, Size12, Size16, NSIZE,
+	Roman, Italic, Bold, Type, NFONT, NFONTTAG,
+	Example, Caption, List, Listelem, Label, Labelref,
+	Exercise, Heading, Nofill, Author, Title,
+	Index, Indextopic,
+	DefFont, DefSize, TitleFont, TitleSize, HeadingFont, HeadingSize: import Brutus;
+
+# following are needed for types in brutusext.m
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+
+include "brutusext.m";
+	SGML, Text, Par, Extension, Float, Special, Celem,
+	FLatex, FLatexProc, FLatexBook, FLatexPart, FLatexSlides, FHtml: import Brutusext;
+
+include "strinttab.m";
+	T: StringIntTab;
+
+Cook: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+# keep this sorted by name
+tagstringtab := array[] of { T->StringInt
+	("Author", Author),
+	("Bold.10", Bold*NSIZE + Size10),
+	("Bold.12", Bold*NSIZE + Size12),
+	("Bold.16", Bold*NSIZE + Size16),
+	("Bold.6", Bold*NSIZE + Size6),
+	("Bold.8", Bold*NSIZE + Size8),
+	("Caption", Caption),
+	("Example", Example),
+	("Exercise", Exercise),
+	("Extension", Extension),
+	("Float", Float),
+	("Heading", Heading),
+	("Index", Index),
+	("Index-topic", Indextopic),
+	("Italic.10", Italic*NSIZE + Size10),
+	("Italic.12", Italic*NSIZE + Size12),
+	("Italic.16", Italic*NSIZE + Size16),
+	("Italic.6", Italic*NSIZE + Size6),
+	("Italic.8", Italic*NSIZE + Size8),
+	("Label", Label),
+	("Label-ref", Labelref),
+	("List", List),
+	("List-elem", Listelem),
+	("No-fill", Nofill),
+	("Par", Par),
+	("Roman.10", Roman*NSIZE + Size10),
+	("Roman.12", Roman*NSIZE + Size12),
+	("Roman.16", Roman*NSIZE + Size16),
+	("Roman.6", Roman*NSIZE + Size6),
+	("Roman.8", Roman*NSIZE + Size8),
+	("SGML", SGML),
+	("Title", Title),
+	("Type.10", Type*NSIZE + Size10),
+	("Type.12", Type*NSIZE + Size12),
+	("Type.16", Type*NSIZE + Size16),
+	("Type.6", Type*NSIZE + Size6),
+	("Type.8", Type*NSIZE + Size8),
+};
+
+# This table must be sorted
+fmtstringtab := array[] of { T->StringInt
+	("html", FHtml),
+	("latex", FLatex),
+	("latexbook", FLatexBook),
+	("latexpart", FLatexPart),
+	("latexproc", FLatexProc),
+	("latexslides", FLatexSlides),
+};
+
+Transtab: adt
+{
+	ch:		int;
+	trans:	string;
+};
+
+# Order doesn't matter for these table
+
+ltranstab := array[] of { Transtab
+	('$', "\\textdollar{}"),
+	('&', "\\&"),
+	('%', "\\%"),
+	('#', "\\#"),
+	('_', "\\textunderscore{}"),
+	('{', "\\{"),
+	('}', "\\}"),
+	('~', "\\textasciitilde{}"),
+	('^', "\\textasciicircum{}"),
+	('\\', "\\textbackslash{}"),
+	('+', "\\textplus{}"),
+	('=', "\\textequals{}"),
+	('|', "\\textbar{}"),
+	('<', "\\textless{}"),
+	('>', "\\textgreater{}"),
+	(' ', "~"),
+	('-', "-"),  # needs special case ligature treatment
+	('\t', " "),   # needs special case treatment
+};
+
+htranstab := array[] of { Transtab
+	('α', "&alpha;"),
+	('Æ', "&AElig;"),
+	('Á', "&Aacute;"),
+	('Â', "&Acirc;"),
+	('À', "&Agrave;"),
+	('Å', "&Aring;"),
+	('Ã', "&Atilde;"),
+	('Ä', "&Auml;"),
+	('Ç', "&Ccedil;"),
+	('Ð', "&ETH;"),
+	('É', "&Eacute;"),
+	('Ê', "&Ecirc;"),
+	('È', "&Egrave;"),
+	('Ë', "&Euml;"),
+	('Í', "&Iacute;"),
+	('Î', "&Icirc;"),
+	('Ì', "&Igrave;"),
+	('Ï', "&Iuml;"),
+	('Ñ', "&Ntilde;"),
+	('Ó', "&Oacute;"),
+	('Ô', "&Ocirc;"),
+	('Ò', "&Ograve;"),
+	('Ø', "&Oslash;"),
+	('Õ', "&Otilde;"),
+	('Ö', "&Ouml;"),
+	('Þ', "&THORN;"),
+	('Ú', "&Uacute;"),
+	('Û', "&Ucirc;"),
+	('Ù', "&Ugrave;"),
+	('Ü', "&Uuml;"),
+	('Ý', "&Yacute;"),
+	('æ', "&aElig;"),
+	('á', "&aacute;"),
+	('â', "&acirc;"),
+	('à', "&agrave;"),
+	('α', "&alpha;"),
+	('&', "&amp;"),
+	('å', "&aring;"),
+	('ã', "&atilde;"),
+	('ä', "&auml;"),
+	('β', "&beta;"),
+	('ç', "&ccedil;"),
+	('⋯', "&cdots;"),
+	('χ', "&chi;"),
+	('©', "&copy;"),
+	('⋱', "&ddots;"),
+	('δ', "&delta;"),
+	('é', "&eacute;"),
+	('ê', "&ecirc;"),
+	('è', "&egrave;"),
+	('—', "&emdash;"),
+	(' ', "&emsp;"),
+	('–', "&endash;"),
+	('ε', "&epsilon;"),
+	('η', "&eta;"),
+	('ð', "&eth;"),
+	('ë', "&euml;"),
+	('γ', "&gamma;"),
+	('>', "&gt;"),
+	('í', "&iacute;"),
+	('î', "&icirc;"),
+	('ì', "&igrave;"),
+	('ι', "&iota;"),
+	('ï', "&iuml;"),
+	('κ', "&kappa;"),
+	('λ', "&lambda;"),
+	('…', "&ldots;"),
+	('<', "&lt;"),
+	('μ', "&mu;"),
+	(' ', "&nbsp;"),
+	('ñ', "&ntilde;"),
+	('ν', "&nu;"),
+	('ó', "&oacute;"),
+	('ô', "&ocirc;"),
+	('ò', "&ograve;"),
+	('ω', "&omega;"),
+	('ο', "&omicron;"),
+	('ø', "&oslash;"),
+	('õ', "&otilde;"),
+	('ö', "&ouml;"),
+	('φ', "&phi;"),
+	('π', "&pi;"),
+	('ψ', "&psi;"),
+	(' ', "&quad;"),
+	('"', "&quot;"),
+	('®', "&reg;"),
+	('ρ', "&rho;"),
+	('­', "&shy;"),
+	('σ', "&sigma;"),
+	('ß', "&szlig;"),
+	('τ', "&tau;"),
+	('θ', "&theta;"),
+	(' ', "&thinsp;"),
+	('þ', "&thorn;"),
+	('™', "&trade;"),
+	('ú', "&uacute;"),
+	('û', "&ucirc;"),
+	('ù', "&ugrave;"),
+	('υ', "&upsilon;"),
+	('ü', "&uuml;"),
+	('∈', "&varepsilon;"),
+	('ϕ', "&varphi;"),
+	('ϖ', "&varpi;"),
+	('ϱ', "&varrho;"),
+	('⋮', "&vdots;"),
+	('ς', "&vsigma;"),
+	('ϑ', "&vtheta;"),
+	('ξ', "&xi;"),
+	('ý', "&yacute;"),
+	('ÿ', "&yuml;"),
+	('ζ', "&zeta;"),
+	('−', "-"),
+};
+
+# For speedy lookups of ascii char translation, use asciitrans.
+# It should be initialized by ascii elements from one of above tables
+asciitrans := array[128] of string;
+
+stderr: ref FD;
+infilename := "";
+outfilename := "";
+linenum := 0;
+fin : ref Iobuf = nil;
+fout : ref Iobuf = nil;
+debug := 0;
+fmt := FLatex;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	S = load String String->PATH;
+	B = load Bufio Bufio->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	T = load StringIntTab StringIntTab->PATH;
+	stderr = sys->fildes(2);
+
+	for(argv = tl argv; argv != nil; ) {
+		s := hd argv;
+		tlargv := tl argv;
+		case s {
+		"-f" =>
+			if(tlargv == nil)
+				usage();
+			fnd: int;
+			(fnd, fmt) = T->lookup(fmtstringtab, hd(tlargv));
+			if(!fnd) {
+				sys->fprint(stderr, "unknown format: %s\n", hd(tlargv));
+				exit;
+			}
+			argv = tlargv;
+		"-o" =>
+			if(tlargv == nil)
+				usage();
+			outfilename = hd(tlargv);
+			argv = tlargv;
+		"-d" =>
+			debug = 1;
+		"-dd" =>
+			debug = 2;
+		* =>
+			if(tlargv == nil)
+				infilename = s;
+			else
+				usage();
+		}
+		argv = tl argv;
+	}
+	if(infilename == "") {
+		fin = B->fopen(sys->fildes(0), sys->OREAD);
+		infilename = "<stdin>";
+	}
+	else
+		fin = B->open(infilename, sys->OREAD);
+	if(fin == nil) {
+		sys->fprint(stderr, "cook: error opening %s: %r\n", infilename);
+		exit;
+	}
+	if(outfilename == "") {
+		fout = B->fopen(sys->fildes(1), sys->OWRITE);
+		outfilename = "<stdout>";
+	}
+	else
+		fout = B->create(outfilename, sys->OWRITE, 8r664);
+	if(fout == nil) {
+		sys->fprint(stderr, "cook: error creating %s: %r\n", outfilename);
+		exit;
+	}
+	line0 := fin.gets('\n');
+	if(line0 != "<SGML>\n") {
+		parse_err("not an SGML file\n");
+		exit;
+	}
+	linenum = 1;
+	e := parse(SGML);
+	findpars(e, 1, nil);
+	e = delemptystrs(e);
+	(e, nil) = canonfonts(e, DefFont*NSIZE+DefSize, DefFont*NSIZE+DefSize);
+	mergeadjs(e);
+	findfloats(e);
+	cleanexts(e);
+	cleanpars(e);
+	if(debug) {
+		fout.puts("After Initial transformations:\n");
+		printelem(e, "", 1);
+		fout.flush();
+	}
+	case fmt {
+	FLatex or FLatexProc or FLatexBook or FLatexPart or FLatexSlides =>
+		latexconv(e);
+	FHtml =>
+		htmlconv(e);
+	}
+	fin.close();
+	fout.close();
+}
+
+usage()
+{
+	sys->fprint(stderr, "Usage: cook [-f (latex|html)] [-o outfile] [infile]\n");
+	exit;
+}
+
+parse_err(msg: string)
+{
+	sys->fprint(stderr, "%s:%d: %s\n", infilename, linenum, msg);
+}
+
+# Parse into elements.
+# Assumes tags are balanced.
+# String elements are split so that there is never an internal newline.
+parse(id: int) : ref Celem
+{
+	els : ref Celem = nil;
+	elstail : ref Celem = nil;
+	for(;;) {
+		c := fin.getc();
+		if(c == Bufio->EOF) {
+			if(id == SGML)
+				break;
+			else {
+				parse_err(sys->sprint("EOF while parsing %s", tagname(id)));
+				return nil;
+			}
+		}
+		if(c == '<') {
+			tag := "";
+			start := 1;
+			i := 0;
+			for(;;) {
+				c = fin.getc();
+				if(c == Bufio->EOF) {
+					parse_err("EOF in middle of tag");
+					return nil;
+				}
+				if(c == '\n') {
+					linenum++;
+					parse_err("newline in middle of tag");
+					break;
+				}
+				if(c == '>')
+					break;
+				if(i == 0 && c == '/')
+					start = 0;
+				else
+					tag[i++] = c;
+			}
+			(fnd, tid) := T->lookup(tagstringtab, tag);
+			if(!fnd) {
+				if(prefix("Extension ", tag)) {
+					el := ref Celem(Extension, tag[10:], nil, nil, nil, nil);
+					if(els == nil) {
+						els = el;
+						elstail = el;
+					}
+					else {
+						el.prev = elstail;
+						elstail.next = el;
+						elstail = el;
+					}
+				}
+				else
+					parse_err(sys->sprint("unknown tag <%s>\n", tag));
+				continue;
+			}
+			if(start) {
+				el := parse(tid);
+				if(el == nil)
+					return nil;
+				if(els == nil) {
+					els = el;
+					elstail = el;
+				}
+				else {
+					el.prev = elstail;
+					elstail.next = el;
+					elstail = el;
+				}
+			}
+			else {
+				if(tid != id) {
+					parse_err(sys->sprint("<%s> ended by </%s>",
+						tagname(id), tag));
+					continue;
+				}
+				break;
+			}
+		}
+		else {
+			s := "";
+			i := 0;
+			for(;;) {
+				if(c == Bufio->EOF)
+					break;
+				if(c == '<') {
+					fin.ungetc();
+					break;
+				}
+				if(c == ';' && i >=3 && s[i-1] == 't' && s[i-2] == 'l' && s[i-3] == '&') {
+					i -= 2;
+					s[i-1] = '<';
+					s = s[0:i];
+				}
+				else
+					s[i++] = c;
+				if(c == '\n') {
+					linenum++;
+					break;
+				}
+				else
+					c = fin.getc();
+			}
+			if(s != "") {
+				el := ref Celem(Text, s, nil, nil, nil, nil);
+				if(els == nil) {
+					els = el;
+					elstail = el;
+				}
+				else {
+					el.prev = elstail;
+					elstail.next = el;
+					elstail = el;
+				}
+			}
+		}
+	}
+	ans := ref Celem(id, "", els, nil, nil, nil);
+	if(els != nil)
+		els.parent = ans;
+	return ans;
+}
+
+# Modify tree e so that blank lines become Par elements.
+# Only do it if parize is set, and unset parize when descending into TExample's.
+# Pass in most recent TString or TPar element, and return updated most-recent-TString/TPar.
+# This function may set some TString strings to ""
+findpars(e: ref Celem, parize: int, prevspe: ref Celem) : ref Celem
+{
+	while(e != nil) {
+		prevnl := 0;
+		prevpar := 0;
+		if(prevspe != nil) {
+			if(prevspe.tag == Text && len prevspe.s != 0
+			   && prevspe.s[(len prevspe.s)-1] == '\n')
+				prevnl = 1;
+			else if(prevspe.tag == Par)
+				prevpar = 1;
+		}
+		if(e.tag == Text) {
+			if(parize && (prevnl || prevpar) && e.s[0] == '\n') {
+				if(prevnl)
+					prevspe.s = prevspe.s[0 : (len prevspe.s)-1];
+				e.tag = Par;
+				e.s = nil;
+			}
+			prevspe = e;
+		}
+		else {
+			nparize := parize;
+			if(e.tag == Example)
+				nparize = 0;
+			prevspe = findpars(e.contents, nparize, prevspe);
+		}
+		e = e.next;
+	}
+	return prevspe;
+}
+
+# Delete any empty strings from e's tree and return modified e.
+# Also, delete any entity that has empty contents, except the
+# Par ones
+delemptystrs(e: ref Celem) : ref Celem
+{
+	if(e.tag == Text) {
+		if(e.s == "")
+			return nil;
+		else
+			return e;
+	}
+	if(e.tag == Par || e.tag == Extension || e.tag == Special)
+		return e;
+	h := e.contents;
+	while(h != nil) {
+		hnext := h.next;
+		hh := delemptystrs(h);
+		if(hh == nil)
+			delete(h);
+		h = hnext;
+	}
+	if(e.contents == nil)
+		return nil;
+	return e;
+}
+
+# Change tree under e so that any font elems contain only strings
+# (by pushing the font changes down).
+# Answer an be a list, so return beginning and end of list.
+# Leave strings bare if font change would be to deffont,
+# and adjust deffont appropriately when entering Title and
+# Heading environments.
+canonfonts(e: ref Celem, curfont, deffont: int) : (ref Celem, ref Celem)
+{
+	f := curfont;
+	head : ref Celem = nil;
+	tail : ref Celem = nil;
+	tocombine : ref Celem = nil;
+	if(e.tag == Text) {
+		if(f == deffont) {
+			head = e;
+			tail = e;
+		}
+		else {
+			head = ref Celem(f, nil, e, nil, nil, nil);
+			e.parent = head;
+			tail = head;
+		}
+	}
+	else if(e.contents == nil) {
+		head = e;
+		tail = e;
+	}
+	else if(e.tag < NFONTTAG) {
+		f = e.tag;
+		allstrings := 1;
+		for(g := e.contents; g != nil; g = g.next) {
+			if(g.tag != Text)
+				allstrings = 0;
+			tail = g;
+		}
+		if(allstrings) {
+			if(f == deffont)
+				head = e.contents;
+			else {
+				head = e;
+				tail = e;
+			}
+		}
+	}
+	if(head == nil) {
+		if(e.tag == Title)
+			deffont = TitleFont*NSIZE+TitleSize;
+		else if(e.tag == Heading)
+			deffont = HeadingFont*NSIZE+HeadingSize;
+		for(h := e.contents; h != nil; ) {
+			prev := h.prev;
+			next := h.next;
+			excise(h);
+			(e1, en) := canonfonts(h, f, deffont);
+			splicebetween(e1, en, prev, next);
+			if(prev == nil)
+				head = e1;
+			tail = en;
+			h = next;
+		}
+		tocombine = head;
+		if(e.tag >= NFONTTAG) {
+			e.contents = head;
+			head.parent = e;
+			head = e;
+			tail = e;
+		}
+	}
+	if(tocombine != nil) {
+		# combine adjacent font changes to same font
+		r := tocombine;
+		while(r != nil) {
+			if(r.tag < NFONTTAG && r.next != nil && r.next.tag == r.tag) {
+				for(v := r.next; v != nil; v = v.next) {
+					if(v.tag != r.tag)
+						break;
+					if(v == tail)
+						tail = r;
+				}
+				# now r up to, not including v, all change to same font
+				for(p := r.next; p != v; p = p.next) {
+					append(r.contents, p.contents);
+				}
+				r.next = v;
+				if(v != nil)
+					v.prev = r;
+				r = v;
+			}
+			else
+				r = r.next;
+		}
+	}
+	head.parent = nil;
+	return (head, tail);
+}
+
+# Remove Pars that appear just before or just after Heading, Title, Examples, Extensions
+# Really should worry about this happening at different nesting levels, but in
+# practice this happens all at the same nesting level
+cleanpars(e: ref Celem)
+{
+	for(h := e.contents; h != nil; h = h.next) {
+		cleanpars(h);
+		if(h.tag == Title || h.tag == Heading || h.tag == Example || h.tag == Extension) {
+			hp := h.prev;
+			hn := h.next;
+			if(hp !=nil && hp.tag == Par)
+				delete(hp);
+			if(hn != nil && hn.tag == Par)
+				delete(hn);
+		}
+	}
+}
+
+# Remove a single tab if it appears before an Extension
+cleanexts(e: ref Celem)
+{
+	for(h := e.contents; h != nil; h = h.next) {
+		cleanexts(h);
+		if(h.tag == Extension) {
+			hp := h.prev;
+			if(hp != nil && stringof(hp) == "\t")
+				delete(hp);
+		}
+	}
+}
+
+mergeable := array[] of { List, Exercise, Caption,Index, Indextopic };
+
+# Merge some adjacent elements (which were probably created separate
+# because of font changes)
+mergeadjs(e: ref Celem)
+{
+	for(h := e.contents; h != nil; h = h.next) {
+		hn := h.next;
+		domerge := 0;
+		if(hn != nil) {
+			for(i := 0; i < len mergeable; i++) {
+				mi := mergeable[i];
+				if(h.tag == mi && hn.tag == mi)
+					domerge = 1;
+			}
+		}
+		if(domerge) {
+			append(h.contents, hn.contents);
+			delete(hn);
+		}
+		else
+			mergeadjs(h);
+	}
+}
+
+# Find floats: they are paragraphs with Captions at the end.
+findfloats(e: ref Celem)
+{
+	lastpar : ref Celem = nil;
+	for(h := e.contents; h != nil; h = h.next) {
+		if(h.tag == Par)
+			lastpar = h;
+		else if(h.tag == Caption) {
+			ne := ref Celem(Float, "", nil, nil, nil, nil);
+			if(lastpar == nil)
+				flhead := e.contents;
+			else
+				flhead = lastpar.next;
+			insertbefore(ne, flhead);
+			# now move flhead ... h into contents of ne
+			ne.contents = flhead;
+			flhead.parent = ne;
+			flhead.prev = nil;
+			ne.next = h.next;
+			if(ne.next != nil)
+				ne.next.prev = ne;
+			h.next = nil;
+			h = ne;
+		}
+		else
+			findfloats(h);
+	}
+}
+
+insertbefore(e, ebefore: ref Celem)
+{
+	e.prev = ebefore.prev;
+	if(e.prev == nil) {
+		e.parent = ebefore.parent;
+		ebefore.parent = nil;
+		e.parent.contents = e;
+	}
+	else
+		e.prev.next = e;
+	e.next = ebefore;
+	ebefore.prev = e;
+}
+
+insertafter(e, eafter: ref Celem)
+{
+	e.next = eafter.next;
+	if(e.next != nil)
+		e.next.prev = e;
+	e.prev = eafter;
+	eafter.next = e;
+}
+
+# remove e from its list, leaving siblings disconnected
+excise(e: ref Celem)
+{
+	next := e. next;
+	prev := e.prev;
+	e.next = nil;
+	e.prev = nil;
+	if(prev != nil)
+		prev.next = nil;
+	if(next != nil)
+		next.prev = nil;
+	e.parent = nil;
+}
+
+splicebetween(e1, en, prev, next: ref Celem)
+{
+	if(prev != nil)
+		prev.next = e1;
+	e1.prev = prev;
+	en.next = next;
+	if(next != nil)
+		next.prev = en;
+}
+
+append(e1, e2: ref Celem)
+{
+	e1last := last(e1);
+	e1last.next = e2;
+	e2.prev = e1last;
+	e2.parent = nil;
+}
+
+last(e: ref Celem) : ref Celem
+{
+	if(e != nil)
+		while(e.next != nil)
+			e = e.next;
+	return e;
+}
+
+succ(e: ref Celem) : ref Celem
+{
+	if(e == nil)
+		return nil;
+	if(e.next != nil)
+		return e.next;
+	return succ(e.parent);
+}
+
+delete(e: ref Celem)
+{
+	ep := e.prev;
+	en := e.next;
+	eu := e.parent;
+	if(ep == nil) {
+		if(eu != nil)
+			eu.contents = en;
+		if(en != nil)
+			en.parent = eu;
+	}
+	else
+		ep.next = en;
+	if(en != nil)
+		en.prev = ep;
+}
+
+# return string represented by e, peering through font changes
+stringof(e: ref Celem) : string
+{
+	if(e != nil) {
+		if(e.tag == Text)
+			return e.s;
+		if(e.tag < NFONTTAG)
+			return stringof(e.contents);
+	}
+	return "";
+}
+
+# remove any initial whitespace from e and its sucessors,
+dropwhite(e: ref Celem)
+{
+	if(e == nil)
+		return;
+	del := 0;
+	if(e.tag == Text) {
+		e.s = drop(e.s, " \t\n");
+		if(e.s == "")
+			del = 1;;
+	}
+	else if(e.tag < NFONTTAG) {
+		dropwhite(e.contents);
+		if(e.contents == nil)
+			del = 1;
+	}
+	if(del) {
+		enext := e.next;
+		delete(e);
+		dropwhite(enext);
+	}
+
+}
+
+firstchar(e: ref Celem) : int
+{
+	s := stringof(e);
+	if(len s >= 1)
+		return s[0];
+	return -1;
+}
+
+lastchar(e: ref Celem) : int
+{
+	if(e == nil)
+		return -1;
+	while(e.next != nil)
+		e = e.next;
+	s := stringof(e);
+	if(len s >= 1)
+		return s[len s -1];
+	return -1;
+}
+
+tlookup(t: array of Transtab, v: int) : string
+{
+	n := len t;
+	for(i := 0; i < n; i++)
+		if(t[i].ch == v)
+			return t[i].trans;
+	return "";
+}
+
+initasciitrans(t: array of Transtab)
+{
+	n := len t;
+	for(i := 0; i < n; i++) {
+		c := t[i].ch;
+		if(c < 128)
+			asciitrans[c] = t[i].trans;
+	}
+}
+
+tagname(id: int) : string
+{
+	name := T->revlookup(tagstringtab, id);
+	if(name == nil)
+		name = "_unknown_";
+	return name;
+}
+
+printelem(e: ref Celem, indent: string, recurse: int)
+{
+	fout.puts(indent);
+	if(debug > 1) {
+		fout.puts(sys->sprint("%x: ", e));
+		if(e != nil && e.parent != nil)
+			fout.puts(sys->sprint("(parent %x): ", e.parent));
+	}
+	if(e == nil)
+		fout.puts("NIL\n");
+	else if(e.tag == Text || e.tag == Special || e.tag == Extension) {
+		if(e.tag == Special)
+			fout.puts("S");
+		else if(e.tag == Extension)
+			fout.puts("E");
+		fout.puts("«");
+		fout.puts(e.s);
+		fout.puts("»\n");
+	}
+	else {
+		name := tagname(e.tag);
+		fout.puts("<" + name + ">\n");
+		if(recurse && e.contents != nil)
+			printelems(e.contents, indent + "    ", recurse);
+	}
+}
+
+printelems(els: ref Celem, indent: string, recurse: int)
+{
+	for(; els != nil; els = els.next)
+		printelem(els, indent, recurse);
+}
+
+check(e: ref Celem, msg: string)
+{
+	err := checke(e);
+	if(err != "") {
+		fout.puts(msg + ": tree is inconsistent:\n" + err);
+		printelem(e, "", 1);
+		fout.flush();
+		exit;
+	}
+}
+
+checke(e: ref Celem) : string
+{
+	err := "";
+	if(e.tag == SGML && e.next != nil)
+		err = sys->sprint("root %x has a next field\n", e);
+	ec := e.contents;
+	if(ec != nil) {
+		if(ec.parent != e)
+			err += sys->sprint("node %x contents %x has bad parent %x\n", e, ec, e.parent);
+		if(ec.prev != nil)
+			err += sys->sprint("node %x contents %x has non-nil prev %x\n", e, ec, e.prev);
+		p := ec;
+		for(h := ec.next; h != nil; h = h.next) {
+			if(h.prev != p)
+				err += sys->sprint("node %x comes after %x, but prev is %x\n", h, p, h.prev);
+			if(h.parent != nil)
+				err += sys->sprint("node %x, not first in siblings, has parent %x\n", h, h.parent);
+			p = h;
+		}
+		for(h = ec; h != nil; h = h.next) {
+			err2 := checke(h);
+			if(err2 != nil)
+				err += err2;
+		}
+	}
+	return err;
+}
+
+# Translation to Latex
+
+# state bits
+SLT, SLB, SLI, SLS6, SLS8, SLS12, SLS16, SLE, SLO, SLF : con (1<<iota);
+
+SLFONTMASK : con SLT|SLB|SLI|SLS6|SLS8|SLS12|SLS16;
+SLSIZEMASK : con SLS6|SLS8|SLS12|SLS16;
+
+# fonttag-to-state-bit table
+lftagtostate := array[NFONTTAG] of {
+	Roman*NSIZE+Size6 => SLS6,
+	Roman*NSIZE+Size8 => SLS8,
+	Roman*NSIZE+Size10 => 0,
+	Roman*NSIZE+Size12 => SLS12,
+	Roman*NSIZE+Size16 => SLS16,
+	Italic*NSIZE+Size6 => SLI | SLS6,
+	Italic*NSIZE+Size8 => SLI | SLS8,
+	Italic*NSIZE+Size10 => SLI,
+	Italic*NSIZE+Size12 => SLI | SLS12,
+	Italic*NSIZE+Size16 => SLI | SLS16,
+	Bold*NSIZE+Size6 => SLB | SLS6,
+	Bold*NSIZE+Size8 => SLB | SLS8,
+	Bold*NSIZE+Size10 => SLB,
+	Bold*NSIZE+Size12 => SLB | SLS12,
+	Bold*NSIZE+Size16 => SLB | SLS16,
+	Type*NSIZE+Size6 => SLT | SLS6,
+	Type*NSIZE+Size8 => SLT | SLS8,
+	Type*NSIZE+Size10 => SLT,
+	Type*NSIZE+Size12 => SLT | SLS12,
+	Type*NSIZE+Size16 => SLT | SLS16
+};
+
+lsizecmd := array[] of { "\\footnotesize", "\\small", "\\normalsize", "\\large", "\\Large"};
+llinepos : int;
+lslidenum : int;
+LTABSIZE : con 4;
+
+latexconv(e: ref Celem)
+{
+	initasciitrans(ltranstab);
+
+	case fmt {
+	FLatex or FLatexProc =>
+		if(fmt == FLatex) {
+			fout.puts("\\documentclass{article}\n");
+			fout.puts("\\def\\encodingdefault{T1}\n");
+		}
+		else {
+			fout.puts("\\documentclass[10pt,twocolumn]{article}\n");
+			fout.puts("\\def\\encodingdefault{T1}\n");
+			fout.puts("\\usepackage{latex8}\n");
+			fout.puts("\\bibliographystyle{latex8}\n");
+		}
+		fout.puts("\\usepackage{times}\n");
+		fout.puts("\\usepackage{brutus}\n");
+		fout.puts("\\usepackage{unicode}\n");
+		fout.puts("\\usepackage{epsf}\n");
+		title := lfindtitle(e);
+		authors := lfindauthors(e);
+		abstract := lfindabstract(e);
+		fout.puts("\\begin{document}\n");
+		if(title != nil) {
+			fout.puts("\\title{");
+			llinepos = 0;
+			lconvl(title, 0);
+			fout.puts("}\n");
+			if(authors != nil) {
+				fout.puts("\\author{");
+				for(l := authors; l != nil; l = tl l) {
+					llinepos = 0;
+					lconvl(hd l, SLO|SLI);
+					if(tl l != nil)
+						fout.puts("\n\\and\n");
+				}
+				fout.puts("}\n");
+			}
+			fout.puts("\\maketitle\n");
+		}
+		fout.puts("\\pagestyle{empty}\\thispagestyle{empty}\n");
+		if(abstract != nil) {
+			if(fmt == FLatexProc) {
+				fout.puts("\\begin{abstract}\n");
+				llinepos = 0;
+				lconvl(abstract, 0);
+				fout.puts("\\end{abstract}\n");
+			}
+			else {
+				fout.puts("\\section*{Abstract}\n");
+				llinepos = 0;
+				lconvl(abstract, 0);
+			}
+		}
+	FLatexBook =>
+		fout.puts("\\documentclass{ibook}\n");
+		fout.puts("\\usepackage{brutus}\n");
+		fout.puts("\\usepackage{epsf}\n");
+		fout.puts("\\begin{document}\n");
+	FLatexSlides =>
+		fout.puts("\\documentclass[portrait]{seminar}\n");
+		fout.puts("\\def\\encodingdefault{T1}\n");
+		fout.puts("\\usepackage{times}\n");
+		fout.puts("\\usepackage{brutus}\n");
+		fout.puts("\\usepackage{unicode}\n");
+		fout.puts("\\usepackage{epsf}\n");
+		fout.puts("\\centerslidesfalse\n");
+		fout.puts("\\slideframe{none}\n");
+		fout.puts("\\slidestyle{empty}\n");
+		fout.puts("\\pagestyle{empty}\n");
+		fout.puts("\\begin{document}\n");
+		lslidenum = 0;
+	}
+
+	llinepos = 0;
+	if(e.tag == SGML)
+		lconvl(e.contents, 0);
+
+	if(fmt == FLatexSlides && lslidenum > 0)
+		fout.puts("\\vfill\\end{slide*}\n");
+	if(fmt != FLatexPart)
+		fout.puts("\\end{document}\n");
+}
+
+lconvl(el: ref Celem, state: int)
+{
+	for(e := el; e != nil; e = e.next) {
+		tag := e.tag;
+		op := "";
+		cl := "";
+		parlike := 1;
+		nstate := state;
+		if(tag < NFONTTAG) {
+			parlike = 0;
+			ss := lftagtostate[tag];
+			if((state & SLFONTMASK) != ss) {
+				t := state & SLT;
+				b := state & SLB;
+				i := state & SLI;
+				newt := ss & SLT;
+				newb := ss & SLB;
+				newi := ss & SLI;
+				op = "{";
+				cl = "}";
+				if(t && !newt)
+					op += "\\rmfamily";
+				else if(!t && newt)
+					op += "\\ttfamily";
+				if(b && !newb)
+					op += "\\mdseries";
+				else if(!b && newb)
+					op += "\\bfseries";
+				if(i && !newi)
+					op += "\\upshape";
+				else if(!i && newi) {
+					op += "\\itshape";
+					bc := lastchar(e.contents);
+					ac := firstchar(e.next);
+					if(bc != -1 && bc != ' ' && bc != '\n' && ac != -1 && ac != '.' && ac != ',')
+						cl = "\\/}";
+				}
+				if((state & SLSIZEMASK) != (ss & SLSIZEMASK)) {
+					nsize := 2;
+					if(ss & SLS6)
+						nsize = 0;
+					else if(ss & SLS8)
+						nsize = 1;
+					else if(ss & SLS12)
+						nsize = 3;
+					else if(ss & SLS16)
+						nsize = 4;
+					# examples shrunk one size
+					if((state & SLE) && nsize > 0)
+							nsize--;
+					op += lsizecmd[nsize];
+				}
+				fc := firstchar(e.contents);
+				if(fc == ' ')
+					op += "{}";
+				else
+					op += " ";
+				nstate = (state & ~SLFONTMASK) | ss;
+			}
+		}
+		else
+			case tag {
+			Text =>
+				parlike = 0;
+				if(state & SLO) {
+					asciitrans[' '] = "\\ ";
+					asciitrans['\n'] = "\\\\\n";
+				}
+				s := e.s;
+				n := len s;
+				for(k := 0; k < n; k++) {
+					c := s[k];
+					x := "";
+					if(c < 128)
+						x = asciitrans[c];
+					else
+						x = tlookup(ltranstab, c);
+					if(x == "") {
+						fout.putc(c);
+						if(c == '\n')
+							llinepos = 0;
+						else
+							llinepos++;
+					}
+					else {
+						# split up ligatures
+						if(c == '-' && k < n-1 && s[k+1] == '-')
+								x = "-{}";
+						# Avoid the 'no line to end here' latex error
+						if((state&SLO) && c == '\n' && llinepos == 0)
+								fout.puts("\\ ");
+						else if((state&SLO) && c == '\t') {
+							nspace := LTABSIZE - llinepos%LTABSIZE;
+							llinepos += nspace;
+							while(nspace-- > 0)
+								fout.puts("\\ ");
+								
+						}
+						else {
+							fout.puts(x);
+							if(x[len x - 1] == '\n')
+								llinepos = 0;
+							else
+								llinepos++;
+						}
+					}
+				}
+				if(state & SLO) {
+					asciitrans[' '] = nil;
+					asciitrans['\n'] = nil;
+				}
+			Example =>
+				if(!(state&SLE)) {
+					op = "\\begin{example}";
+					cl = "\\end{example}\\noindent ";
+					nstate |= SLE | SLO;
+				}
+			List =>
+				(n, bigle) := lfindbigle(e.contents);
+				if(n <= 2) {
+					op = "\\begin{itemize}\n";
+					cl = "\\end{itemize}";
+				}
+				else {
+					fout.puts("\\begin{itemizew}{");
+					lconvl(bigle.contents, nstate);
+					op = "}\n";
+					cl = "\\end{itemizew}";
+				}
+			Listelem =>
+				op = "\\item[{";
+				cl = "}]";
+			Heading =>
+				if(fmt == FLatexProc)
+					op = "\n\\Section{";
+				else
+					op = "\n\\section{";
+				cl = "}\n";
+				nstate = (state & ~SLFONTMASK) | (SLB | SLS12);
+			Nofill =>
+				op = "\\begin{nofill}";
+				cl = "\\end{nofill}\\noindent ";
+				nstate |= SLO;
+			Title =>
+				if(fmt == FLatexSlides) {
+					op = "\\begin{slide*}\n" +
+						"\\begin{center}\\Large\\bfseries ";
+					if(lslidenum > 0)
+						op = "\\vfill\\end{slide*}\n" + op;
+					cl = "\\end{center}\n";
+					lslidenum++;
+				}
+				else {
+					if(stringof(e.contents) == "Index") {
+						op = "\\printindex\n";
+						e.contents = nil;
+					}
+					else {
+						op = "\\chapter{";
+						cl = "}\n";
+					}
+				}
+				nstate = (state & ~SLFONTMASK) | (SLB | SLS16);
+			Par =>
+				op = "\n\\par\n";
+				while(e.next != nil && e.next.tag == Par)
+					e = e.next;
+			Extension =>
+				e.contents = convextension(e.s);
+				if(e.contents != nil)
+					e.contents.parent = e;
+			Special =>
+				fout.puts(e.s);
+			Float =>
+				if(!(state&SLF)) {
+					isfig := lfixfloat(e);
+					if(isfig) {
+						op = "\\begin{figure}\\begin{center}\\leavevmode ";
+						cl = "\\end{center}\\end{figure}";
+					}
+					else {
+						op = "\\begin{table}\\begin{center}\\leavevmode ";
+						cl = "\\end{center}\\end{table}";
+					}
+					nstate |= SLF;
+				}
+			Caption=>
+				if(state&SLF) {
+					op = "\\caption{";
+					cl = "}";
+					nstate = (state & ~SLFONTMASK) | SLS8;
+				}
+				else {
+					op = "\\begin{center}";
+					cl = "\\end{center}";
+				}
+			Label or Labelref =>
+				parlike = 0;
+				if(tag == Label)
+					op = "\\label";
+				else
+					op = "\\ref";
+				cl = "{" + stringof(e.contents) + "}";
+				e.contents = nil;
+			Exercise =>
+				lfixexercise(e);
+				op = "\\begin{exercise}";
+				cl = "\\end{exercise}";
+			Index or Indextopic =>
+				parlike = 0;
+				if(tag == Index)
+					lconvl(e.contents, nstate);
+				fout.puts("\\showidx{");
+				lconvl(e.contents, nstate);
+				fout.puts("}");
+				lconvindex(e.contents, nstate);
+				e.contents = nil;
+			}
+		if(op != "")
+			fout.puts(op);
+		if(e.contents != nil) {
+			if(parlike)
+				llinepos = 0;
+			lconvl(e.contents, nstate);
+			if(parlike)
+				llinepos = 0;
+		}
+		if(cl != "")
+			fout.puts(cl);
+	}
+}
+
+lfixfloat(e: ref Celem) : int
+{
+	dropwhite(e.contents);
+	fstart := e.contents;
+	fend := last(fstart);
+	hasfig := 0;
+	hastab := 0;
+	if(fend.tag == Caption) {
+		dropwhite(fend.prev);
+		if(fend.prev != nil && stringof(fstart) == "\t")
+			delete(fend.prev);
+		# If fend.contents is "YYY " <Label> "." rest
+		# where YYY is Figure or Table,
+		# then replace it with just rest, and move <Label>
+		# after the caption.
+		# Probably should be more robust about what to accept.
+		ec := fend.contents;
+		s := stringof(ec);
+		if(s == "Figure ")
+			hasfig = 1;
+		else if(s == "Table ")
+			hastab = 1;
+		if(hasfig || hastab) {
+			ec2 := ec.next;
+			ec3 : ref Celem = nil;
+			ec4 : ref Celem = nil;
+			if(ec2 != nil && ec2.tag == Label) {
+				ec3 = ec2.next;
+				if(ec3 != nil && stringof(ec3) == ".")
+					ec4 = ec3.next;
+			}
+			if(ec4 != nil) {
+				dropwhite(ec4);
+				ec4 = ec3.next;
+				if(ec4 != nil) {
+					excise(ec);
+					excise(ec2);
+					excise(ec3);
+					fend.contents = ec4;
+					ec4.parent = fend;
+					insertafter(ec2, fend);
+				}
+			}
+		}
+	}
+	return !hastab;
+}
+
+lfixexercise(e: ref Celem)
+{
+	dropwhite(e.contents);
+	ec := e.contents;
+	# Expect:
+	#     "Exercise " <Label> ":" rest
+	#  If so, drop the first and third.
+	# Or
+	#	"Exercise:" rest
+	# If so, drop the first.
+	s := stringof(ec);
+	if(s == "Exercise ") {
+		ec2 := ec.next;
+		ec3 : ref Celem = nil;
+		ec4 : ref Celem = nil;
+			if(ec2 != nil && ec2.tag == Label) {
+				ec3 = ec2.next;
+				if(ec3 != nil && stringof(ec3) == ":")
+					ec4 = ec3.next;
+			}
+			if(ec4 != nil) {
+				dropwhite(ec4);
+				ec4 = ec3.next;
+				if(ec4 != nil) {
+					excise(ec);
+					excise(ec3);
+					e.contents = ec2;
+					ec2.parent = e;
+					ec2.next = ec4;
+					ec4.prev = ec2;
+				}
+			}
+	}
+	else if(s == "Exercise:") {
+		dropwhite(ec.next);
+		e.contents = ec.next;
+		excise(ec);
+		if(e.contents != nil)
+			e.contents.parent = e;
+	}
+}
+
+# convert content list headed by e to \\index{...}
+lconvindex(e: ref Celem, state: int)
+{
+	fout.puts("\\index{");
+	g := lsplitind(e);
+	gp := g;
+	needat := 0;
+	while(g != nil) {
+		gnext := g.next;
+		s := stringof(g);
+		if(s == "!" || s == "|") {
+			if(gp != g) {
+				g.next = nil;
+				g.s = "";
+				lprintindsort(gp);
+				if(needat) {
+					fout.puts("@");
+					lconvl(gp, state);
+				}
+			}
+			fout.puts(s);
+			gp = gnext;
+			needat = 0;
+			if(s == "|") {
+				if(g == nil)
+					break;
+				g = gnext;
+				# don't lconvl the Text items, so
+				# that "see{" and "}" come out untranslated.
+				# (code is wrong if stuff inside see is plain
+				# text but with special tex characters)
+				while(g != nil) {
+					gnext = g.next;
+					g.next = nil;
+					if(g.tag != Text)
+						lconvl(g, state);
+					else
+						fout.puts(g.s);
+					g = gnext;
+				}
+				gp = nil;
+				break;
+			}
+		}
+		else {
+			if(g.tag != Text)
+				needat = 1;
+		}
+		g = gnext;
+	}
+	if(gp != nil) {
+		lprintindsort(gp);
+		if(needat) {
+			fout.puts("@");
+			lconvl(gp, state);
+		}
+	}
+	fout.puts("}");
+}
+
+lprintindsort(e: ref Celem)
+{
+	while(e != nil) {
+		fout.puts(stringof(e));
+		e = e.next;
+	}
+}
+
+# return copy of e
+lsplitind(e: ref Celem) : ref Celem
+{
+	dummy := ref Celem;
+	for( ; e != nil; e = e.next) {
+		te := e;
+		if(e.tag < NFONTTAG)
+			te = te.contents;
+		if(te.tag != Text)
+			continue;
+		s := te.s;
+		i := 0;
+		for(j := 0; j < len s; j++) {
+			if(s[j] == '!' || s[j] == '|') {
+				if(j > i) {
+					nte := ref Celem(Text, s[i:j], nil, nil, nil, nil);
+					if(e == te)
+						ne := nte;
+					else
+						ne = ref Celem(e.tag, nil, nte, nil, nil, nil);
+					append(dummy, ne);
+				}
+				append(dummy, ref Celem(Text, s[j:j+1], nil, nil, nil, nil));
+				i = j+1;
+			}
+		}
+		if(j > i) {
+			nte := ref Celem(Text, s[i:j], nil, nil, nil, nil);
+			if(e == te)
+				ne := nte;
+			else
+				ne = ref Celem(e.tag, nil, nte, nil, nil, nil);
+			append(dummy, ne);
+		}
+	}
+	return dummy.next;
+}
+
+# return key part of an index entry corresponding to e list
+indexkey(e: ref Celem) : string
+{
+	s := "";
+	while(e != nil) {
+		s += stringof(e);
+		e = e.next;
+	}
+	return s;
+}
+
+# find title, excise it from e, and return contents as list
+lfindtitle(e: ref Celem) : ref Celem
+{
+	if(e.tag == Title) {
+		ans := e.contents;
+		delete(e);
+		return ans;
+	}
+	else if (e.contents != nil) {
+		for(h := e.contents; h != nil; h = h.next) {
+			a := lfindtitle(h);
+			if(a != nil)
+				return a;
+		}
+	}
+	return nil;
+}
+
+# find authors, excise them from e, and return as list of lists
+lfindauthors(e: ref Celem) : list of ref Celem
+{
+	if(e.tag == Author) {
+		a := e.contents;
+		en := e.next;
+		delete(e);
+		rans : list of ref Celem = a :: nil;
+		if(en != nil) {
+			e = en;
+			while(e != nil) {
+				if(e.tag == Par) {
+					en = e.next;
+					if(en.tag == Author) {
+						delete(e);
+						a = en.contents;
+						for(y := a; y != nil; ) {
+							yn := y.next;
+							if(y.tag == Par)
+								delete(y);
+							y = yn;
+						}
+						e = en.next;
+						delete(en);
+						rans = a :: rans;
+					}
+					else
+						break;
+				}
+				else
+					break;
+			}
+		}
+		ans : list of ref Celem = nil;
+		while(rans != nil) {
+			ans = hd rans :: ans;
+			rans = tl rans;
+		}
+		return ans;
+	}
+	else if (e.contents != nil) {
+		for(h := e.contents; h != nil; h = h.next) {
+			a := lfindauthors(h);
+			if(a != nil)
+				return a;
+		}
+	}
+	return nil;
+}
+
+# find section called abstract, excise it from e, and return as list
+lfindabstract(e: ref Celem) : ref Celem
+{
+	if(e.tag == Heading) {
+		c := e.contents;
+		if(c.tag == Text && c.s == "Abstract") {
+			for(h2 := e.next; h2 != nil; h2 = h2.next) {
+				if(h2.tag == Heading)
+					break;
+			}
+			ans := e.next;
+			ans.prev = nil;
+			ep := e.prev;
+			eu := e.parent;
+			if(ep == nil) {
+				if(eu != nil)
+					eu.contents = h2;
+				if(h2 != nil)
+					h2.parent = eu;
+			}
+			else
+				ep.next = h2;
+			if(h2 != nil) {
+				ansend := h2.prev;
+				ansend.next = nil;
+				h2.prev = ep;
+			}
+			return ans;
+		}
+	}
+	else if (e.contents != nil) {
+		for(h := e.contents; h != nil; h = h.next) {
+			a := lfindabstract(h);
+			if(a != nil)
+				return a;
+		}
+	}
+	return nil;
+}
+
+# find biggest list element with longest contents in e list
+lfindbigle(e: ref Celem) : (int, ref Celem)
+{
+	ans : ref Celem = nil;
+	maxlen := 0;
+	for(h := e; h != nil; h = h.next) {
+		if(h.tag == Listelem) {
+			n := 0;
+			for(p := h.contents; p != nil; p = p.next) {
+				if(p.tag == Text)
+					n += len p.s;
+				else if(p.tag < NFONTTAG) {
+					q := p.contents;
+					if(q.tag == Text)
+						n += len q.s;
+				}
+			}
+			if(n > maxlen) {
+				maxlen = n;
+				ans = h;
+			}
+		}
+	}
+	return (maxlen, ans);
+}
+
+# Translation to HTML
+
+# state bits
+SHA, SHO, SHFL, SHDT: con (1<<iota);
+
+htmlconv(e: ref Celem)
+{
+	initasciitrans(htranstab);
+
+	fout.puts("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n");
+	fout.puts("<HTML>\n");
+
+	if(e.tag == SGML) {
+		# Conforming 3.2 documents require a Title.
+		# Use the Title tag both for the document title and
+		# for an H1-level heading.
+		# (SHDT state bit enforces: Font change markup, etc., not allowed in Title)
+		fout.puts("<TITLE>\n");
+		title := hfindtitle(e);
+		if(title != nil)
+			hconvl(title.contents, SHDT);
+		else if(infilename != "")
+			fout.puts(infilename);
+		else
+			fout.puts("An HTML document");
+		fout.puts("</TITLE>\n");
+		fout.puts("<BODY>\n");
+		hconvl(e.contents, 0);
+		fout.puts("</BODY>\n");
+	}
+
+	fout.puts("</HTML>\n");
+}
+
+hconvl(el: ref Celem, state: int)
+{
+	for(e := el; e != nil; e = e.next) {
+		tag := e.tag;
+		op := "";
+		cl := "";
+		nstate := state;
+		if(tag == Text) {
+			s := e.s;
+			n := len s;
+			for(k := 0; k < n; k++) {
+				c := s[k];
+				x := "";
+				if(c < 128) {
+					if(c == '\n' && (state&SHO))
+						x = "\n\t";
+					else
+						x = asciitrans[c];
+				}
+				else
+					x = tlookup(htranstab, c);
+				if(x == "")
+					fout.putc(c);
+				else
+					fout.puts(x);
+			}
+		}
+		else if(!(state&SHDT))
+			case tag {
+			Roman*NSIZE+Size6 =>
+				op = "<FONT SIZE=1>";
+				cl = "</FONT>";
+				nstate |= SHA;
+			Roman*NSIZE+Size8 =>
+				op = "<FONT SIZE=2>";
+				cl = "</FONT>";
+				nstate |= SHA;
+			Roman*NSIZE+Size10 =>
+				if(state & SHA) {
+					op = "<FONT SIZE=3>";
+					cl = "</FONT>";
+					nstate &= ~SHA;
+				}
+			Roman*NSIZE+Size12 =>
+				op = "<FONT SIZE=4>";
+				cl = "</FONT>";
+				nstate |= SHA;
+			Roman*NSIZE+Size16 =>
+				op = "<FONT SIZE=5>";
+				cl = "</FONT>";
+				nstate |= SHA;
+			Italic*NSIZE+Size6 =>
+				op = "<I><FONT SIZE=1>";
+				cl = "</FONT></I>";
+				nstate |= SHA;
+			Italic*NSIZE+Size8 =>
+				op = "<I><FONT SIZE=2>";
+				cl = "</FONT></I>";
+				nstate |= SHA;
+			Italic*NSIZE+Size10 =>
+				if(state & SHA) {
+					op =  "<I><FONT SIZE=3>";
+					cl = "</FONT></I>";
+					nstate &= ~SHA;
+				}
+				else {
+					op = "<I>";
+					cl = "</I>";
+				}
+			Italic*NSIZE+Size12 =>
+				op = "<I><FONT SIZE=4>";
+				cl = "</FONT></I>";
+				nstate |= SHA;
+			Italic*NSIZE+Size16 =>
+				op = "<I><FONT SIZE=5>";
+				cl = "</FONT></I>";
+				nstate |= SHA;
+			Bold*NSIZE+Size6 =>
+				op = "<B><FONT SIZE=1>";
+				cl = "</FONT></B>";
+				nstate |= SHA;
+			Bold*NSIZE+Size8 =>
+				op = "<B><FONT SIZE=2>";
+				cl = "</FONT></B>";
+				nstate |= SHA;
+			Bold*NSIZE+Size10 =>
+				if(state & SHA) {
+					op =  "<B><FONT SIZE=3>";
+					cl = "</FONT></B>";
+					nstate &= ~SHA;
+				}
+				else {
+					op = "<B>";
+					cl = "</B>";
+				}
+			Bold*NSIZE+Size12 =>
+				op = "<B><FONT SIZE=4>";
+				cl = "</FONT></B>";
+				nstate |= SHA;
+			Bold*NSIZE+Size16 =>
+				op = "<B><FONT SIZE=5>";
+				cl = "</FONT></B>";
+				nstate |= SHA;
+			Type*NSIZE+Size6 =>
+				op = "<TT><FONT SIZE=1>";
+				cl = "</FONT></TT>";
+				nstate |= SHA;
+			Type*NSIZE+Size8 =>
+				op = "<TT><FONT SIZE=2>";
+				cl = "</FONT></TT>";
+				nstate |= SHA;
+			Type*NSIZE+Size10 =>
+				if(state & SHA) {
+					op =  "<TT><FONT SIZE=3>";
+					cl = "</FONT></TT>";
+					nstate &= ~SHA;
+				}
+				else {
+					op = "<TT>";
+					cl = "</TT>";
+				}
+			Type*NSIZE+Size12 =>
+				op = "<TT><FONT SIZE=4>";
+				cl = "</FONT></TT>";
+				nstate |= SHA;
+			Type*NSIZE+Size16 =>
+				op = "<TT><FONT SIZE=5>";
+				cl = "</FONT></TT>";
+				nstate |= SHA;
+			Example =>
+				op = "<P><PRE>\t";
+				cl = "</PRE><P>\n";
+				nstate |= SHO;
+			List =>
+				op = "<DL>";
+				cl = "</DD></DL>";
+				nstate |= SHFL;
+			Listelem =>
+				if(state & SHFL)
+					op = "<DT>";
+				else
+					op = "</DD><DT>";
+				cl = "</DT><DD>";
+				# change first-list-elem state for this level
+				state &= ~SHFL;
+			Heading =>
+				op = "<H2>";
+				cl = "</H2>\n";
+			Nofill =>
+				op = "<P><PRE>";
+				cl = "</PRE>";
+			Title =>
+				op = "<H1>";
+				cl = "</H1>\n";
+			Par =>
+				op = "<P>\n";
+			Extension =>
+				e.contents = convextension(e.s);
+			Special =>
+				fout.puts(e.s);
+			}
+		if(op != "")
+			fout.puts(op);
+		hconvl(e.contents, nstate);
+		if(cl != "")
+			fout.puts(cl);
+	}
+}
+
+# find title, if there is one, and return it (but leave it in contents too)
+hfindtitle(e: ref Celem) : ref Celem
+{
+	if(e.tag == Title)
+		return e;
+	else if (e.contents != nil) {
+		for(h := e.contents; h != nil; h = h.next) {
+			a := hfindtitle(h);
+			if(a != nil)
+				return a;
+		}
+	}
+	return nil;
+}
+
+Exten: adt
+{
+	name: string;
+	mod: Brutusext;
+};
+
+extens: list of Exten = nil;
+
+convextension(s: string) : ref Celem
+{
+	for(i:=0; i<len s; i++)
+		if(s[i] == ' ')
+			break;
+	if(i == len s) {
+		sys->fprint(stderr, "badly formed extension %s\n", s);
+		return nil;
+	}
+	modname := s[0:i];
+	s = s[i+1:];
+	mod: Brutusext = nil;
+	for(le := extens; le != nil; le = tl le) {
+		el := hd le;
+		if(el.name == modname)
+			mod = el.mod;
+	}
+	if(mod == nil) {
+		file := modname;
+		if(i < 4 || file[i-4:i] != ".dis")
+			file += ".dis";
+		if(file[0] != '/')
+			file = "/dis/wm/brutus/" + file;
+		mod = load Brutusext file;
+		if(mod == nil) {
+			sys->fprint(stderr, "can't load extension module %s: %r\n", file);
+			return nil;
+		}
+		mod->init(sys, draw, B, tk, nil);
+		extens = Exten(modname, mod) :: extens;
+	}
+	f := infilename;
+	if(f == "<stdin>")
+		f = "";
+	(ans, err) := mod->cook(f, fmt, s);
+	if(err != "") {
+		sys->fprint(stderr, "extension module %s cook error: %s\n", modname, err);
+		return nil;
+	}
+	return ans;
+}
--- /dev/null
+++ b/appl/cmd/cp.b
@@ -1,0 +1,237 @@
+implement Cp;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+include "arg.m";
+
+include "readdir.m";
+	readdir: Readdir;
+
+Cp: module
+{
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+stderr: ref Sys->FD;
+errors := 0;
+gflag := 0;
+uflag := 0;
+xflag := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	arg := load Arg Arg->PATH;
+	recursive := 0;
+	arg->init(args);
+	arg->setusage("\tcp [-gux] src target\n\tcp [-r] [-gux] src ... directory");
+	while((opt := arg->opt()) != 0)
+		case opt {
+		'r' =>	recursive = 1;
+		'g' => gflag = 1;
+		'u' => uflag = gflag = 1;
+		'x' => xflag = 1;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	argc := len args;
+	if(argc < 2)
+		arg->usage();
+	arg = nil;
+
+	dst: string;
+	for(t := args; t != nil; t = tl t)
+		dst = hd t;
+
+	(ok, dir) := sys->stat(dst);
+	todir := (ok != -1 && (dir.mode & Sys->DMDIR));
+	if(argc > 2 && !todir){
+		sys->fprint(stderr, "cp: %s  not a directory\n", dst);
+		raise "fail:error";
+	}
+	if(recursive)
+		cpdir(args, dst);
+	else{
+		for(; tl args != nil; args = tl args){
+			if(todir)
+				cp(hd args, dst, basename(hd args));
+			else
+				cp(hd args, dst, nil);
+		}
+	}
+	if(errors)
+		raise "fail:error";
+}
+
+basename(s: string): string
+{
+	for((nil, ls) := sys->tokenize(s, "/"); ls != nil; ls = tl ls)
+		s = hd ls;
+	return s;
+}
+
+cp(src, dst: string, newname: string)
+{
+	dd: Sys->Dir;
+
+	if(newname != nil)
+		dst += "/" + newname;
+	(ok, ds) := sys->stat(src);
+	if(ok < 0){
+		warning(sys->sprint("%s: %r", src));
+		return;
+	}
+	if(ds.mode & Sys->DMDIR){
+		warning(src + " is a directory");
+		return;
+	}
+	(ok, dd) = sys->stat(dst);
+	if(ok != -1 && samefile(ds, dd)){
+		warning(src + " and " + dst + " are the same file");
+		return;
+	}
+	sfd := sys->open(src, Sys->OREAD);
+	if(sfd == nil){
+		warning(sys->sprint("cannot open %s: %r", src));
+		return;
+	}
+	dfd := sys->create(dst, Sys->OWRITE, ds.mode & 8r777);
+	if(dfd == nil){
+		warning(sys->sprint("cannot create %s: %r", dst));
+		return;
+	}
+	if(copy(sfd, dfd, src, dst)!=0)
+		return;
+	if(wstat(dfd, ds, 0) < 0)
+		warning(sys->sprint("can't wstat %s: %r", src));
+}
+
+copy(sfd, dfd: ref Sys->FD, src, dst: string): int
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while((r := sys->read(sfd, buf, len buf)) > 0){
+		if(sys->write(dfd, buf, r) != r){
+			warning(sys->sprint("error writing %s: %r", dst));
+			return -1;
+		}
+	}
+	if(r < 0){
+		warning(sys->sprint("error reading %s: %r", src));
+		return -1;
+	}
+	return 0;
+}
+
+cpdir(args: list of string, dst: string)
+{
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil){
+		sys->fprint(stderr, "cp: cannot load %s: %r\n", Readdir->PATH);
+		raise "fail:bad module";
+	}
+	cache = array[NCACHE] of list of ref Sys->Dir;
+	dexists := 0;
+	(ok, dd) := sys->stat(dst);
+	# destination file exists
+	if(ok != -1){
+		if((dd.mode & Sys->DMDIR) == 0){
+			warning(dst + ": destination not a directory");
+			return;
+		}
+		dexists = 1;
+	}
+	for(; tl args != nil; args = tl args){
+		ds: Sys->Dir;
+		src := hd args;
+		(ok, ds) = sys->stat(src);
+		if(ok < 0){
+			warning(sys->sprint("can't stat %s: %r", src));
+			continue;
+		}
+		if((ds.mode & Sys->DMDIR) == 0){
+			cp(hd args, dst, basename(hd args));
+		} else if(dexists){
+			if(samefile(ds, dd)){
+				warning("cannot copy " + src + " into itself");
+				continue;
+			}
+			copydir(src, dst + "/" + basename(src), ds);
+		} else
+			copydir(src, dst, ds);
+	}
+}
+
+copydir(src, dst: string, srcd: Sys->Dir)
+{
+	(ok, nil) := sys->stat(dst);
+	if(ok != -1){
+		warning("cannot copy " + src + " onto another directory");
+		return;
+	}
+	tmode := srcd.mode | 8r777;	# Fix for Nt
+	dfd := sys->create(dst, Sys->OREAD, Sys->DMDIR | tmode);
+	if(dfd == nil){
+		warning(sys->sprint("cannot make directory %s: %r", dst));
+		return;
+	}
+	(entries, n) := readdir->init(src, Readdir->COMPACT);
+	for(i := 0; i < n; i++){
+		e := entries[i];
+		path := src + "/" + e.name;
+		if((e.mode & Sys->DMDIR) == 0)
+			cp(path, dst, e.name);
+		else if(seen(e))
+			warning(path + ": directory loop found");
+		else
+			copydir(path, dst + "/" + e.name, *e);
+	}
+	if(wstat(dfd, srcd, 1) < 0)
+		warning(sys->sprint("can't wstat %s: %r", dst));
+}
+
+wstat(dfd: ref Sys->FD, ds: Sys->Dir, mflag: int): int
+{
+	if(!xflag && !gflag && !uflag && !mflag)
+		return 0;
+	d := sys->nulldir;
+	if(xflag)
+		d.mtime = ds.mtime;
+	if(xflag || mflag)
+		d.mode = ds.mode;
+	if(uflag)
+		d.uid = ds.uid;
+	if(gflag)
+		d.gid = ds.gid;
+	return sys->fwstat(dfd, d);
+}
+
+samefile(d1: Sys->Dir, d2: Sys->Dir): int
+{
+	return d1.dtype == d2.dtype && d1.dev == d2.dev &&
+		d1.qid.qtype == d2.qid.qtype && d1.qid.path == d2.qid.path &&
+		d1.qid.vers == d2.qid.vers;
+}
+
+# Avoid loops in tangled namespaces. (from du.b)
+NCACHE: con 64; # must be power of two
+cache: array of list of ref sys->Dir;
+
+seen(dir: ref sys->Dir): int
+{
+	savlist := cache[int dir.qid.path&(NCACHE-1)];
+	for(c := savlist; c!=nil; c = tl c)
+		if(samefile(*dir, *hd c))
+			return 1;
+	cache[int dir.qid.path&(NCACHE-1)] = dir :: savlist;
+	return 0;
+}
+
+warning(e: string)
+{
+	sys->fprint(stderr, "cp: %s\n", e);
+	errors++;
+}
--- /dev/null
+++ b/appl/cmd/cprof.b
@@ -1,0 +1,190 @@
+implement Prof;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+	arg: Arg;
+include "profile.m";
+	profile: Profile;
+include "sh.m";
+
+stderr: ref Sys->FD;
+
+Prof: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+	init0: fn(nil: ref Draw->Context, argv: list of string): Profile->Coverage;
+};
+
+exits(e: string)
+{
+	if(profile != nil)
+		profile->end();
+	raise "fail:" + e;
+}
+
+pfatal(s: string)
+{
+	sys->fprint(stderr, "cprof: %s: %s\n", s, profile->lasterror());
+	exits("error");
+}
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "cprof: cannot load %s: %r\n", p);
+	exits("bad module");
+}
+
+usage(s: string)
+{
+	sys->fprint(stderr, "cprof: %s\n", s);
+	sys->fprint(stderr, "usage: cprof [-fner] [-m modname]... cmd [arg ... ]\n");
+	exits("usage");
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	init0(ctxt, argv);
+}
+
+init0(ctxt: ref Draw->Context, argv: list of string): Profile->Coverage
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	arg = load Arg Arg->PATH;
+	if(arg == nil)
+		badmodule(Arg->PATH);
+	arg->init(argv);
+	profile = load Profile Profile->PATH;
+	if(profile == nil)
+		badmodule(Profile->PATH);
+	if(profile->init() < 0)
+		pfatal("cannot initialize profile device");
+
+	v := 0;
+	ep := 0;
+	rec := 0;
+	wm := 0;
+	exec, mods: list of string;
+	while((c := arg->opt()) != 0){
+		case c {
+			'n' => v |= profile->FULLHDR;
+			'f' => v |= profile->FREQUENCY;
+			'm' =>
+				if((s := arg->arg()) == nil)
+					usage("missing module/file");
+				mods = s :: mods;
+			'e' =>
+				ep = 1;
+			'r' =>
+				rec = 1;
+			'g' =>
+				wm = 1;
+			* => 
+				usage(sys->sprint("unknown option -%c", c));
+		}
+	}
+	exec = arg->argv();
+	# if(exec == nil)
+	#	usage("nothing to execute");
+	for( ; mods != nil; mods = tl mods)
+		profile->profile(hd mods);
+	if(ep && exec != nil)
+		profile->profile(disname(hd exec));
+	if(exec != nil){
+		wfd := openwait(sys->pctl(0, nil));
+		ci := chan of int;
+		spawn execute(ctxt, hd exec, exec, ci);
+		epid := <- ci;
+		if(profile->cpstart(epid) < 0){
+			ci <-= 0;
+			pfatal("cannot start profiling");
+		}
+		ci <-= 1;
+		wait(wfd, epid);
+		if(profile->stop() < 0)
+			pfatal("cannot stop profiling");
+	}
+	if(exec == nil)
+		modl := profile->cpfstats(v);
+	else
+		modl = profile->cpstats(rec, v);
+	if(modl.mods == nil)
+		pfatal("no profile information");
+	if(wm){
+		cvr := profile->coverage(modl, v);
+		profile->end();
+		return cvr;
+	}
+	if(!rec && profile->cpshow(modl, v) < 0)
+		pfatal("cannot show profile");
+	profile->end();
+	return nil;
+}
+
+disname(cmd: string): string
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	if(exists(file))
+		return file;
+	if(file[0]!='/' && file[0:2]!="./")
+		file = "/dis/"+file;
+	# if(exists(file))
+	#	return file;
+	return file;
+}
+
+execute(ctxt: ref Draw->Context, cmd : string, argl : list of string, ci: chan of int)
+{
+	ci <-= sys->pctl(Sys->FORKNS|Sys->NEWFD|Sys->NEWPGRP, 0 :: 1 :: 2 :: stderr.fd :: nil);
+	file := cmd;
+	err := "";
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err = sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+	}
+	if(<- ci){
+		if(c == nil)
+			sys->fprint(stderr, "cprof: %s: %s\n", cmd, err);
+		else
+			c->init(ctxt, argl);
+	}
+}
+
+openwait(pid : int) : ref Sys->FD
+{
+	w := sys->sprint("#p/%d/wait", pid);
+	fd := sys->open(w, Sys->OREAD);
+	if (fd == nil)
+		pfatal("fd == nil in wait");
+	return fd;
+}
+
+wait(wfd : ref Sys->FD, wpid : int)
+{
+	n : int;
+
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;) {
+		if ((n = sys->read(wfd, buf, len buf)) < 0)
+			pfatal("bad read in wait");
+		status = string buf[0:n];
+		if (int status == wpid)
+			break;
+	}
+}
+
+exists(f: string): int
+{
+	return sys->open(f, Sys->OREAD) != nil;
+}
--- /dev/null
+++ b/appl/cmd/cpu.b
@@ -1,0 +1,151 @@
+implement CPU;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+include "string.m";
+	str: String;
+include "arg.m";
+include "keyring.m";
+include "security.m";
+include "dial.m";
+
+DEFCMD:	con "/dis/sh";
+
+CPU: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "cpu: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+usage()
+{
+	sys->fprint(stderr, "Usage: cpu [-C cryptoalg] mach command args...\n");
+	raise "fail:usage";
+}
+
+# The default level of security is NOSSL, unless
+# the keyring directory doesn't exist, in which case
+# it's disallowed.
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil) badmodule(Arg->PATH);
+
+	str = load String String->PATH;
+	if (str == nil) badmodule(String->PATH);
+
+	au := load Auth Auth->PATH;
+	if (au == nil) badmodule(Auth->PATH);
+
+	kr := load Keyring Keyring->PATH;
+	if (kr == nil) badmodule(Keyring->PATH);
+
+	dial := load Dial Dial->PATH;
+
+	arg->init(argv);
+	alg := "";
+	while ((opt := arg->opt()) != 0) {
+		if (opt == 'C') {
+			alg = arg->arg();
+		} else
+			usage();
+	}
+	argv = arg->argv();
+	args := "auxi/cpuslave";
+#	if(ctxt != nil && ctxt.screen != nil)
+#		args += " -s" + string ctxt.screen.id;
+#	else
+		args += " --";
+
+	mach: string;
+	case len argv {
+	0 =>
+		usage();
+	1 =>
+		mach = hd argv;
+		args += " " + DEFCMD;
+	* =>
+		mach = hd argv;
+		args += " " + str->quoted(tl argv);
+	}
+
+	user := getuser();
+	kd := "/usr/" + user + "/keyring/";
+	cert := kd + dial->netmkaddr(mach, "tcp", "");
+	if (!exists(cert)) {
+		cert = kd + "default";
+		if (!exists(cert)) {
+			sys->fprint(stderr, "cpu: cannot find certificate in %s; use getauthinfo\n", kd);
+			raise "fail:no certificate";
+		}
+	}
+
+	c := dial->dial(dial->netmkaddr(mach, "net", "rstyx"), nil);
+	if(c == nil){
+		sys->fprint(stderr, "cpu: can't dial %s: %r\n", mach);
+		raise "fail:dial";
+	}
+
+	ai := kr->readauthinfo(cert);
+
+	if (alg == nil)
+		alg = "none";
+	err := au->init();
+	if(err != nil) {
+		sys->fprint(stderr, "cpu: cannot initialise auth module: %s\n", err);
+		raise "fail:auth init failed";
+	}
+
+	fd := ref Sys->FD;
+	#sys->fprint(stderr, "cpu: authenticating using alg '%s'\n", alg);		
+	(fd, err) = au->client(alg, ai, c.dfd);
+	if(fd == nil) {
+		sys->fprint(stderr, "cpu: authentication failed: %s\n", err);
+		raise "fail:authentication failure";
+	}
+
+	t := array of byte sys->sprint("%d\n%s\n", len (array of byte args)+1, args);
+	if(sys->write(fd, t, len t) != len t){
+		sys->fprint(stderr, "cpu: export args write error: %r\n");
+		raise "fail:write error";
+	}
+
+	if(sys->export(fd, "/", sys->EXPWAIT) < 0){
+		sys->fprint(stderr, "cpu: export failed: %r\n");
+		raise "fail:export error";
+	}
+}
+
+exists(file: string): int
+{
+	(ok, nil) := sys->stat(file);
+	return ok != -1;
+}
+
+getuser(): string
+{
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil){
+		sys->fprint(stderr, "cpu: cannot open /dev/user: %r\n");
+		raise "fail:no user id";
+	}
+
+	buf := array[50] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0){
+		sys->fprint(stderr, "cpu: cannot read /dev/user: %r\n");
+		raise "fail:no user id";
+	}
+
+	return string buf[0:n];	
+}
--- /dev/null
+++ b/appl/cmd/crypt.b
@@ -1,0 +1,263 @@
+implement Crypt;
+
+# encrypt/decrypt from stdin to stdout
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	ssl: SSL;
+include "bufio.m";
+include "msgio.m";
+	msgio: Msgio;
+include "arg.m";
+
+Crypt: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+Ehungup: con "i/o on hungup channel";
+
+ALGSTR: con "alg ";
+DEFAULTALG: con "md5/ideacbc";
+usage()
+{
+	sys->fprint(stderr, "usage: crypt [-?] [-d] [-k secret] [-f secretfile] [-a alg[/alg]]\n");
+	sys->fprint(stderr, "available algorithms:\n");
+	showalgs(stderr);
+	fail("bad usage");
+}
+
+badmodule(m: string)
+{
+	sys->fprint(stderr, "crypt: cannot load %s: %r\n", m);
+	fail("bad module");
+}
+
+headers: con 1;
+verbose := 0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	ssl = load SSL SSL->PATH;
+	if (ssl == nil)
+		badmodule(SSL->PATH);
+	keyring = load Keyring Keyring->PATH;
+	if (keyring == nil)
+		badmodule(SSL->PATH);
+	msgio = load Msgio Msgio->PATH;
+	msgio->init();
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(SSL->PATH);
+
+	decrypt := 0;
+	secret: array of byte;
+	alg := DEFAULTALG;
+
+	arg->init(argv);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'd' =>
+			decrypt = 1;
+		'k' =>
+			if ((s := arg->arg()) == nil)
+				usage();
+			secret = array of byte s;
+		'f' =>
+			if ((f := arg->arg()) == nil)
+				usage();
+			secret = readfile(f);
+		'a' =>
+			if ((alg = arg->arg()) == nil)
+				usage();
+		'?' =>
+			showalgs(sys->fildes(1));
+			return;
+		'v' =>
+			verbose = 1;
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	if (argv != nil)
+		usage();
+	if(secret == nil)
+		secret = array of byte readpassword();
+	sk := array[Keyring->SHA1dlen] of byte;
+	keyring->sha1(secret, len secret, sk, nil);
+	if (headers) {
+		# deal with header - the header encodes the algorithm along with the data.
+		if (decrypt) {
+			msg := msgio->getmsg(sys->fildes(0));
+			if (msg != nil)
+				alg = string msg;
+			if (msg == nil || len alg < len ALGSTR || alg[0:len ALGSTR] != ALGSTR)
+				error("couldn't get decrypt algorithm");
+			alg = alg[len ALGSTR:];
+		} else {
+			msg := array of byte ("alg " + alg);
+			e := msgio->sendmsg(sys->fildes(1),  msg, len msg);
+			if (e == -1)
+				error("couldn't write algorithm string");
+		}
+	}
+	fd := docrypt(decrypt, alg, sk);
+	if (decrypt) {
+		# if decrypting, don't use stream, as we want to catch
+		# decryption or checksum errors when they happen.
+		buf := array[Sys->ATOMICIO] of byte;
+		stdout := sys->fildes(1);
+		while ((n := sys->read(fd, buf, len buf)) > 0)
+			sys->write(stdout, buf, n);
+
+		if (n == -1) {
+			err := sys->sprint("%r");
+			if (err != Ehungup) 
+				error("decryption failed: " + err);
+		}
+	} else {
+		stream(fd, sys->fildes(1), Sys->ATOMICIO);
+	}
+}
+
+docrypt(decrypt: int, alg: string, sk: array of byte): ref Sys->FD
+{
+	if (verbose)
+		sys->fprint(stderr, "%scrypting with alg %s\n", (array[] of {"en", "de"})[decrypt!=0], alg);
+	(err, fds, nil, nil) := cryptpipe(decrypt, alg, sk);
+	if (err != nil)
+		error(err);
+
+	spawn stream(sys->fildes(0), fds[1], Sys->ATOMICIO);
+	return fds[0];
+}
+
+# set up an encrypt/decrypt session; if decrypt is non-zero, then
+# decrypt, else encrypt. alg is the algorithm to use; sk is the
+# used as the secret key. 
+# returns tuple (err, fds, cfd, dir)
+# where err is non-nil on failure;
+# otherwise fds is an array of two fds; writing to fds[1] will make
+# crypted/decrypted data available to be read on fds[0].
+# dir is the ssl directory in question.
+cryptpipe(decrypt: int, alg: string, sk: array of byte): (string, array of ref Sys->FD, ref Sys->FD, string)
+{
+	pfd := array[2] of ref Sys->FD;
+	if (sys->pipe(pfd) == -1)
+		return ("pipe failed", nil, nil, nil);
+
+	(err, c) := ssl->connect(pfd[1]);
+	if (err != nil)
+		return ("could not connect ssl: "+err, nil, nil, nil);
+	pfd[1] = nil;
+	err = ssl->secret(c, sk, sk);
+	if (err != nil) 
+		return ("could not write secret: "+err, nil, nil, nil);
+
+	if (alg != nil)
+		if (sys->fprint(c.cfd, "alg %s", alg) == -1)
+			return (sys->sprint("bad algorithm %s: %r", alg), nil, nil, nil);
+
+	fds := array[2] of ref Sys->FD;
+	if (decrypt) {
+		fds[1] = pfd[0];
+		fds[0] = c.dfd;
+	} else {
+		fds[1] = c.dfd;
+		fds[0] = pfd[0];
+	}
+	return (nil, fds, c.cfd, c.dir);
+}
+
+algnames := array[] of {("crypt", "encalgs"), ("hash", "hashalgs")};
+
+# find available algorithms and return as tuple of two lists:
+# (err, hashalgs, cryptalgs)
+algs(): (string, array of list of string)
+{
+	(err, nil, nil, dir) := cryptpipe(0, nil, array[100] of byte);
+	if (err != nil)
+		return (err, nil);
+	alglists := array[len algnames] of list of string;
+	for (i := 0; i < len algnames; i++) {
+		(nil, f) := algnames[i];
+		(nil, alglists[i]) = sys->tokenize(string readfile(dir + "/"  + f), " ");
+	}
+	return (nil, alglists);
+}
+
+showalgs(fd: ref Sys->FD)
+{
+	(err, alglists) := algs();
+	if (err != nil)
+		error("cannot get algorithms: " + err);
+	for (j := 0; j < len alglists; j++) {
+		(name, nil) := algnames[j];
+		sys->fprint(fd, "%s:", name);
+		for (l := alglists[j]; l != nil; l = tl l)
+			sys->fprint(fd, " %s", hd l);
+		sys->fprint(fd, "\n");
+	}
+}
+
+readpassword(): string
+{
+	bufio := load Bufio Bufio->PATH;
+	Iobuf: import bufio;
+	stdin := bufio->open("/dev/cons", Sys->OREAD);
+
+	cfd := sys->open("/dev/consctl", Sys->OWRITE);
+	if (cfd == nil || sys->fprint(cfd, "rawon") <= 0)
+		sys->fprint(stderr, "crypt: warning: cannot hide typed password\n");
+	sys->fprint(stderr, "password: ");
+	s := "";
+	while ((c := stdin.getc()) >= 0 && c != '\n'){
+		case c {
+		'\b' =>
+			if (len s > 0)
+				s = s[0:len s - 1];
+		8r25 =>		# ^U
+			s = nil;
+		* =>
+			s[len s] = c;
+		}
+	}
+	sys->fprint(stderr, "\n");
+	return s;
+}
+
+stream(src, dst: ref Sys->FD, bufsize: int)
+{
+	sys->stream(src, dst, bufsize);
+}
+
+readfile(f: string): array of byte
+{
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil)
+		error(sys->sprint("cannot read %s: %r", f));
+	buf := array[8192] of byte;	# >8K key? get real!
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return nil;
+	return buf[0:n];
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "crypt: %s\n", s);
+	fail("error");
+}
+
+fail(e: string)
+{
+	raise "fail: "+e;
+}
--- /dev/null
+++ b/appl/cmd/date.b
@@ -1,0 +1,71 @@
+implement Date;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+include "daytime.m";
+include "arg.m";
+
+Date: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: date [-un] [seconds]\n");
+	raise "fail:usage";
+}
+
+nomod(m: string)
+{
+	sys->fprint(sys->fildes(2), "date: cannot load %s: %r", m);
+	raise "fail:load";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	daytime := load Daytime Daytime->PATH;
+	if (daytime == nil)
+		nomod(Daytime->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		nomod(Arg->PATH);
+	nflag := uflag := 0;
+	arg->init(argv);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'n' =>
+			nflag = 1;
+		'u' =>
+			uflag = 1;
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	arg = nil;
+	if (argv != nil && (tl argv != nil || !isnumeric(hd argv)))
+		usage();
+	now: int;
+	if (argv != nil)
+		now = int hd argv;
+	else
+		now = daytime->now();
+	if (nflag)
+		sys->print("%d\n", now);
+	else if (uflag)
+		sys->print("%s\n", daytime->text(daytime->gmt(now)));
+	else
+		sys->print("%s\n", daytime->text(daytime->local(now)));
+}
+
+isnumeric(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] < '0' || s[i] > '9')
+			return 0;
+	return 1;
+}
--- /dev/null
+++ b/appl/cmd/dbfs.b
@@ -1,0 +1,518 @@
+implement Dbfs;
+
+#
+# Copyright © 1999 Vita Nuova Limited.  All rights reserved.
+# Revisions copyright © 2002 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+	Qid: import Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+include "styxservers.m";
+	styxservers: Styxservers;
+	Fid, Styxserver, Navigator, Navop: import styxservers;
+	Enotfound, Eperm, Ebadarg: import styxservers;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+Record: adt {
+	id:		int;		# file number in directory
+	x:		int;		# index in file
+	dirty:	int;		# modified but not written
+	vers:		int;		# version
+	data:		array of byte;
+
+	new:		fn(x: array of byte): ref Record;
+	print:	fn(r: self ref Record, fd: ref Sys->FD);
+	qid:		fn(r: self ref Record): Sys->Qid;
+};
+
+Database: adt {
+	name:	string;
+	file:	ref Iobuf;
+	records:	array of ref Record;
+	dirty:	int;
+	vers:		int;
+	nextid:	int;
+
+	findrec:	fn(db: self ref Database, id: int): ref Record;
+};
+
+Dbfs: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Qdir, Qnew, Qdata: con iota;
+
+clockfd: ref Sys->FD;
+stderr: ref Sys->FD;
+database: ref Database;
+user: string;
+Eremoved: con "file removed";
+
+usage()
+{
+	sys->fprint(stderr, "Usage: dbfs [-a|-b|-ac|-bc] [-D] file mountpoint\n");
+	raise "fail:usage";
+}
+
+nomod(s: string)
+{
+	sys->fprint(stderr, "dbfs: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		nomod(Styx->PATH);
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	if(styxservers == nil)
+		nomod(Styxservers->PATH);
+	styxservers->init(styx);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		nomod(Bufio->PATH);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	arg->init(args);
+	flags := Sys->MREPL;
+	copt := 0;
+	empty := 0;
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>	flags = Sys->MAFTER;
+		'b' =>	flags = Sys->MBEFORE;
+		'c' =>	copt = 1;
+		'e' =>	empty = 1;
+		'D' =>	styxservers->traceset(1);
+		* =>		usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(len args != 2)
+		usage();
+	if(copt)
+		flags |= Sys->MCREATE;
+	file := hd args;
+	args = tl args;
+	mountpt := hd args;
+
+	df := bufio->open(file, Sys->OREAD);
+	if(df == nil && empty){
+		(rc, nil) := sys->stat(file);
+		if(rc < 0)
+			df = bufio->create(file, Sys->OREAD, 8r600);
+	}
+	if(df == nil){
+		sys->fprint(stderr, "dbfs: can't open %s: %r\n", file);
+		raise "fail:open";
+	}
+	(db, err) := dbread(ref Database(file, df, nil, 0, 0, 0));
+	if(db == nil){
+		sys->fprint(stderr, "dbfs: can't read %s: %s\n", file, err);
+		raise "fail:dbread";
+	}
+	db.file = nil;
+#	dbprint(db);
+	database = db;
+
+	sys->pctl(Sys->FORKFD, nil);
+
+	user = rf("/dev/user");
+	if(user == nil)
+		user = "inferno";
+
+	fds := array[2] of ref Sys->FD;
+	if(sys->pipe(fds) < 0){
+		sys->fprint(stderr, "dbfs: can't create pipe: %r\n");
+		raise "fail:pipe";
+	}
+
+	navops := chan of ref Navop;
+	spawn navigator(navops);
+
+	(tchan, srv) := Styxserver.new(fds[0], Navigator.new(navops), big Qdir);
+	fds[0] = nil;
+
+	pidc := chan of int;
+	spawn serveloop(tchan, srv, pidc, navops);
+	<-pidc;
+
+	if(sys->mount(fds[1], nil, mountpt, flags, nil) < 0) {
+		sys->fprint(stderr, "dbfs: mount failed: %r\n");
+		raise "fail:mount";
+	}
+}
+
+rf(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	b := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, b, len b);
+	if(n < 0)
+		return nil;
+	return string b[0:n];
+}
+
+dbread(db: ref Database): (ref Database, string)
+{
+	db.file.seek(big 0, Sys->SEEKSTART);
+	rl: list of ref Record;
+	n := 0;
+	for(;;){
+		(r, err) := getrec(db);
+		if(err != nil)
+			return (nil, err);		# could press on without it, or make it the `file' contents
+		if(r == nil)
+			break;
+		rl = r :: rl;
+		n++;
+	}
+	db.nextid = n;
+	db.records = array[n] of ref Record;
+	for(; rl != nil; rl = tl rl){
+		r := hd rl;
+		n--;
+		r.id = n;
+		r.x = n;
+		db.records[n] = r;
+	}
+	return (db, nil);
+}
+
+#
+# a record is (.+\n)*\n
+#
+getrec(db: ref Database): (ref Record, string)
+{
+	r := ref Record(-1, -1, 0, 0, nil);
+	data := "";
+	for(;;){
+		s := db.file.gets('\n');
+		if(s == nil){
+			if(data == nil)
+				return (nil, nil);		# BUG: distinguish i/o error from EOF?
+			break;
+		}
+		if(s[len s - 1] != '\n')
+#			return (nil, "file missing newline");	# possibly truncated
+			s += "\n";
+		if(s == "\n")
+			break;
+		data += s;
+	}
+	r.data = array of byte data;
+	return (r, nil);
+}
+
+dbsync(db: ref Database): int
+{
+	if(db.dirty){
+		db.file = bufio->create(db.name, Sys->OWRITE, 8r666);
+		if(db.file == nil)
+			return -1;
+		for(i := 0; i < len db.records; i++){
+			r := db.records[i];
+			if(r != nil && r.data != nil){
+				if(db.file.write(r.data, len r.data) != len r.data)
+					return -1;
+				db.file.putc('\n');
+			}
+		}
+		if(db.file.flush())
+			return -1;
+		db.file = nil;
+		db.dirty = 0;
+	}
+	return 0;
+}
+
+dbprint(db: ref Database)
+{
+	stdout := sys->fildes(1);
+	for(i := 0; i < len db.records; i++){
+		db.records[i].print(stdout);
+		sys->print("\n");
+	}
+}
+
+Database.findrec(db: self ref Database, id: int): ref Record
+{
+	for(i:=0; i<len db.records; i++)
+		if((r := db.records[i]) != nil && r.id == id)
+			return r;
+	return nil;
+}
+
+Record.new(fields: array of byte): ref Record
+{
+	n := len database.records;
+	r := ref Record(n, n, 0, 0, fields);
+	a := array[n+1] of ref Record;
+	if(n)
+		a[0:] = database.records[0:];
+	a[n] = r;
+	database.records = a;
+	database.vers++;
+	return r;
+}
+
+Record.print(r: self ref Record, fd: ref Sys->FD)
+{
+	if(r.data != nil)
+		sys->write(fd, r.data, len r.data);
+}
+
+Record.qid(r: self ref Record): Sys->Qid
+{
+	return Sys->Qid(QPATH(r.x, Qdata), r.vers, Sys->QTFILE);
+}
+
+serveloop(tchan: chan of ref Tmsg, srv: ref Styxserver, pidc: chan of int, navops: chan of ref Navop)
+{
+	pidc <-= sys->pctl(Sys->FORKNS|Sys->NEWFD, 1::2::srv.fd.fd::nil);
+Serve:
+	while((gm := <-tchan) != nil){
+		pick m := gm {
+		Readerror =>
+			sys->fprint(stderr, "dbfs: fatal read error: %s\n", m.error);
+			break Serve;
+		Open =>
+			c := srv.getfid(m.fid);
+			if(c == nil || TYPE(c.path) != Qnew){
+				srv.open(m);		# default action
+				break;
+			}
+			if(c.uname != user) {
+				srv.reply(ref Rmsg.Error(m.tag, Eperm));
+				break;
+			}
+			mode := styxservers->openmode(m.mode);
+			if(mode < 0) {
+				srv.reply(ref Rmsg.Error(m.tag, Ebadarg));
+				break;
+			}
+			# generate new file, change Fid's qid to match
+			r := Record.new(array[0] of byte);
+			qid := r.qid();
+			c.open(mode, qid);
+			srv.reply(ref Rmsg.Open(m.tag, qid, srv.iounit()));
+		Read =>
+			(c, err) := srv.canread(m);
+			if(c == nil){
+				srv.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			if(c.qtype & Sys->QTDIR){
+				srv.read(m);	# does readdir
+				break;
+			}
+			r := database.records[FILENO(c.path)];
+			if(r == nil)
+				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
+			else
+				srv.reply(styxservers->readbytes(m, r.data));
+		Write =>
+			(c, merr) := srv.canwrite(m);
+			if(c == nil){
+				srv.reply(ref Rmsg.Error(m.tag, merr));
+				break;
+			}
+			(value, err) := data2rec(m.data);
+			if(err != nil){
+				srv.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			fno := FILENO(c.path);
+			r := database.records[fno];
+			if(r == nil){
+				srv.reply(ref Rmsg.Error(m.tag, Eremoved));
+				break;
+			}
+			r.data = value;
+			r.vers++;
+			database.dirty++;
+			if(dbsync(database) == 0)
+				srv.reply(ref Rmsg.Write(m.tag, len m.data));
+			else
+				srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+		Clunk =>
+			# a transaction-oriented dbfs could delay updating the record until clunk
+			srv.clunk(m);
+		Remove =>
+			c := srv.getfid(m.fid);
+			if(c == nil || c.qtype & Sys->QTDIR || TYPE(c.path) != Qdata){
+				# let it diagnose all the errors
+				srv.remove(m);
+				break;
+			}
+			r := database.records[FILENO(c.path)];
+			if(r != nil)
+				r.data = nil;
+			database.dirty++;
+			srv.delfid(c);
+			if(dbsync(database) == 0)
+				srv.reply(ref Rmsg.Remove(m.tag));
+			else
+				srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+		Wstat =>
+			srv.default(gm);	# TO DO?
+		* =>
+			srv.default(gm);
+		}
+	}
+	navops <-= nil;		# shut down navigator
+}
+
+dirslot(n: int): int
+{
+	for(i := 0; i < len database.records; i++){
+		r := database.records[i];
+		if(r != nil && r.data != nil){
+			if(n == 0)
+				return i;
+			n--;
+		}
+	}
+	return -1;
+}
+
+dir(qid: Sys->Qid, name: string, length: big, uid: string, perm: int): ref Sys->Dir
+{
+	d := ref sys->zerodir;
+	d.qid = qid;
+	if(qid.qtype & Sys->QTDIR)
+		perm |= Sys->DMDIR;
+	d.mode = perm;
+	d.name = name;
+	d.uid = uid;
+	d.gid = uid;
+	d.length = length;
+	return d;
+}
+
+dirgen(p: big): (ref Sys->Dir, string)
+{
+	case TYPE(p) {
+	Qdir =>
+		return (dir(Qid(QPATH(0, Qdir),database.vers,Sys->QTDIR), "/", big 0, user, 8r700), nil);
+	Qnew =>
+		return (dir(Qid(QPATH(0, Qnew),0,Sys->QTFILE), "new", big 0, user, 8r600), nil);
+	* =>
+		n := FILENO(p);
+		if(n < 0 || n >= len database.records)
+			return (nil, nil);
+		r := database.records[n];
+		if(r == nil || r.data == nil)
+			return (nil, Enotfound);
+		return (dir(r.qid(), sys->sprint("%d", r.id), big len r.data, user, 8r600), nil);
+	}
+}
+
+navigator(navops: chan of ref Navop)
+{
+	while((m := <-navops) != nil){
+		pick n := m {
+		Stat =>
+			n.reply <-= dirgen(n.path);
+		Walk =>
+			if(int n.path != Qdir){
+				n.reply <-= (nil, "not a directory");
+				break;
+			}
+			case n.name {
+			".." =>
+				;	# nop
+			"new" =>
+				n.path = QPATH(0, Qnew);
+			* =>
+				if(len n.name < 1 || !(n.name[0]>='0' && n.name[0]<='9')){	# weak test for now
+					n.reply <-= (nil, Enotfound);
+					continue;
+				}
+				r := database.findrec(int n.name);
+				if(r == nil){
+					n.reply <-= (nil, Enotfound);
+					continue;
+				}
+				n.path = QPATH(r.x, Qdata);
+			}
+			n.reply <-= dirgen(n.path);
+		Readdir =>
+			if(int m.path != Qdir){
+				n.reply <-= (nil, "not a directory");
+				break;
+			}
+			i := n.offset;
+			if(i == 0)
+				n.reply <-= dirgen(QPATH(0,Qnew));
+			for(; --n.count >= 0 && (j := dirslot(i)) >= 0; i++)
+				n.reply <-= dirgen(QPATH(j,Qdata));	# n² but the file will be small
+			n.reply <-= (nil, nil);
+		}
+	}
+}
+
+QPATH(w, q: int): big
+{
+	return big ((w<<8)|q);
+}
+
+TYPE(path: big): int
+{
+	return int path & 16rFF;
+}
+
+FILENO(path: big) : int
+{
+	return (int path >> 8) & 16rFFFFFF;
+}
+
+#
+# a record is (.+\n)*, without final empty line
+#
+data2rec(data: array of byte): (array of byte, string)
+{
+	s: string;
+	for(b := data; len b > 0;){
+		(b, s) = getline(b);
+		if(s == nil || s[len s - 1] != '\n' || s == "\n")
+			return (nil, "partial or malformed record");	# possibly truncated
+	}
+	return (data, nil);
+}
+
+getline(b: array of byte): (array of byte, string)
+{
+	n := len b;
+	for(i := 0; i < n; i++){
+		(ch, l, nil) := sys->byte2char(b, i);
+		i += l;
+		if(l == 0 || ch == '\n')
+			break;
+	}
+	return (b[i:], string b[0:i]);
+}
--- /dev/null
+++ b/appl/cmd/dbm/delete.b
@@ -1,0 +1,34 @@
+implement Dbmdelete;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "dbm.m";
+	dbm: Dbm;
+	Datum, Dbf: import dbm;
+
+Dbmdelete: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dbm = load Dbm Dbm->PATH;
+
+	dbm->init();
+
+	args = tl args;
+	db := Dbf.open(hd args, Sys->ORDWR);
+	if(db == nil){
+		sys->fprint(sys->fildes(2), "dbm/delete: %s: %r\n", hd args);
+		raise "fail:open";
+	}
+	args = tl args;
+	key := hd args;
+	if(db.delete(array of byte key) < 0)
+		sys->fprint(sys->fildes(2), "not found\n");
+}
--- /dev/null
+++ b/appl/cmd/dbm/fetch.b
@@ -1,0 +1,37 @@
+implement Dbmfetch;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "dbm.m";
+	dbm: Dbm;
+	Datum, Dbf: import dbm;
+
+Dbmfetch: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dbm = load Dbm Dbm->PATH;
+
+	dbm->init();
+
+	args = tl args;
+	db := Dbf.open(hd args, Sys->OREAD);
+	if(db == nil){
+		sys->fprint(sys->fildes(2), "dbm/fetch: %s: %r\n", hd args);
+		raise "fail:open";
+	}
+	args = tl args;
+	key := hd args;
+	data := db.fetch(array of byte key);
+	if(data == nil)
+		sys->fprint(sys->fildes(2), "not found\n");
+	else
+		sys->write(sys->fildes(1), data, len data);
+}
--- /dev/null
+++ b/appl/cmd/dbm/keys.b
@@ -1,0 +1,32 @@
+implement Dbmkeys;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "dbm.m";
+	dbm: Dbm;
+	Datum, Dbf: import dbm;
+
+Dbmkeys: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dbm = load Dbm Dbm->PATH;
+
+	dbm->init();
+
+	args = tl args;
+	db := Dbf.open(hd args, Sys->OREAD);
+	if(db == nil){
+		sys->fprint(sys->fildes(2), "dbm/keys: %s: %r\n", hd args);
+		raise "fail:open";
+	}
+	for(key := db.firstkey(); key != nil; key = db.nextkey(key))
+		sys->print("%s\n", string key);
+}
--- /dev/null
+++ b/appl/cmd/dbm/list.b
@@ -1,0 +1,34 @@
+implement Dbmlist;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "dbm.m";
+	dbm: Dbm;
+	Datum, Dbf: import dbm;
+
+Dbmlist: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dbm = load Dbm Dbm->PATH;
+
+	dbm->init();
+
+	args = tl args;
+	db := Dbf.open(hd args, Sys->OREAD);
+	if(db == nil){
+		sys->fprint(sys->fildes(2), "dbm/list: %s: %r\n", hd args);
+		raise "fail:open";
+	}
+	for(key := db.firstkey(); key != nil; key = db.nextkey(key)){
+		d := db.fetch(key);
+		sys->print("%s	%s\n", string key, string d);
+	}
+}
--- /dev/null
+++ b/appl/cmd/dbm/mkfile
@@ -1,0 +1,19 @@
+<../../../mkconfig
+
+TARG=\
+	fetch.dis\
+	delete.dis\
+	keys.dis\
+	list.dis\
+	store.dis\
+
+SYSMODULES=\
+	arg.m\
+	sys.m\
+	draw.m\
+	bufio.m\
+	dbm.m\
+
+DISBIN=$ROOT/dis/dbm
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/dbm/store.b
@@ -1,0 +1,69 @@
+implement Dbmstore;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "dbm.m";
+	dbm: Dbm;
+	Datum, Dbf: import dbm;
+
+Dbmstore: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dbm = load Dbm Dbm->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	dbm->init();
+
+	args = tl args;
+	db := Dbf.open(hd args, Sys->ORDWR);
+	if(db == nil){
+		sys->fprint(sys->fildes(2), "dbm/store: %s: %r\n", hd args);
+		raise "fail:open";
+	}
+	args = tl args;
+	if(args == nil){
+		err := 0;
+		f := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+		while((s := f.gets('\n')) != nil){
+			s = s[0:len s-1];
+			key: string;
+			for(i :=0; i < len s; i++)
+				if(s[i] == ' ' || s[i] == '\t'){
+					key = s[0:i];
+					s = s[i+1:];
+					break;
+				}
+			if(key == nil){
+				sys->fprint(sys->fildes(2), "dbm/store: bad input\n");
+				raise "fail:error";
+			}
+			if(store(db, key, s))
+				err = 1;
+		}
+		if(err)
+			raise "fail:store";
+	}else if(store(db, hd args, hd tl args))
+		raise "fail:store";
+}
+
+store(db: ref Dbf, key: string, dat: string): int
+{
+	r := db.store(array of byte key, array of byte dat, 0);
+	if(r < 0)
+		sys->fprint(sys->fildes(2), "bad store\n");
+	else if(r)
+		sys->fprint(sys->fildes(2), "%q exists\n", key);
+	return r;
+}
--- /dev/null
+++ b/appl/cmd/dd.b
@@ -1,0 +1,625 @@
+implement dd;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+
+include "draw.m";
+
+dd: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+BIG:	con 2147483647;
+LCASE,
+UCASE,
+SWAB,
+NERR	,
+SYNC	:	con (1<<iota);
+
+NULL,
+CNULL,
+EBCDIC,
+IBM,
+ASCII,
+BLOCK,
+UNBLOCK:	con iota;
+
+cflag:		int;
+ctype:	int;
+
+fflag:		int;
+arg:		string;
+ifile:		string;
+ofile:		string;
+ibuf:		array of byte;
+obuf:		array of byte;
+op:		int;
+skip:		int;
+oseekn:	int;
+iseekn:	int;
+count:	int;
+files:=	1;
+ibs:=		512;
+obs:=		512;
+bs:		int;
+cbs:		int;
+ibc:		int;
+obc:		int;
+cbc:		int;
+nifr:		int;
+nipr:		int;
+nofr:		int;
+nopr:		int;
+ntrunc:	int;
+ibf:		ref Sys->FD;
+obf:		ref Sys->FD;
+nspace:	int;
+
+iskey(key:string, s: string): int
+{
+	return key[0] == '-' && key[1:] ==  s;
+}
+
+exits(msg: string)
+{
+	if(msg == nil)
+		exit;
+
+	raise "fail:"+msg;
+}
+
+perror(msg: string)
+{
+	sys->fprint(stderr, "%s: %r\n", msg);
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil)
+		return;
+	stderr = sys->fildes(2);
+
+	ctype = NULL;
+	argv = tl argv;
+	while(argv != nil) {
+		key := hd argv;
+		argv = tl argv;
+		if(argv == nil){
+			sys->fprint(stderr, "dd: arg %s needs a value\n", key);
+			exits("arg");
+		}
+		arg = hd argv;
+		argv = tl argv;
+		if(iskey(key, "ibs")) {
+			ibs = number(BIG);
+			continue;
+		}
+		if(iskey(key, "obs")) {
+			obs = number(BIG);
+			continue;
+		}
+		if(iskey(key, "cbs")) {
+			cbs = number(BIG);
+			continue;
+		}
+		if(iskey(key, "bs")) {
+			bs = number(BIG);
+			continue;
+		}
+		if(iskey(key, "if")) {
+			ifile = arg[0:];
+			continue;
+		}
+		if(iskey(key, "of")) {
+			ofile = arg[0:];
+			continue;
+		}
+		if(iskey(key, "skip")) {
+			skip = number(BIG);
+			continue;
+		}
+		if(iskey(key, "seek") || iskey(key, "oseek")) {
+			oseekn = number(BIG);
+			continue;
+		}
+		if(iskey(key, "iseek")) {
+			iseekn = number(BIG);
+			continue;
+		}
+		if(iskey(key, "count")) {
+			count = number(BIG);
+			continue;
+		}
+		if(iskey(key, "files")) {
+			files = number(BIG);
+			continue;
+		}
+		if(iskey(key, "conv")) {
+			do {
+				if(arg == nil)
+					break;
+				if(match(","))
+					continue;
+				if(match("ebcdic")) {
+					ctype = EBCDIC;
+					continue;
+				}
+				if(match("ibm")) {
+					ctype = IBM;
+					continue;
+				}
+				if(match("ascii")) {
+					ctype = ASCII;
+					continue;
+				}
+				if(match("block")) {
+					ctype = BLOCK;
+					continue;
+				}
+				if(match("unblock")) {
+					ctype = UNBLOCK;
+					continue;
+				}
+				if(match("lcase")) {
+					cflag |= LCASE;
+					continue;
+				}
+				if(match("ucase")) {
+					cflag |= UCASE;
+					continue;
+				}
+				if(match("swab")) {
+					cflag |= SWAB;
+					continue;
+				}
+				if(match("noerror")) {
+					cflag |= NERR;
+					continue;
+				}
+				if(match("sync")) {
+					cflag |= SYNC;
+					continue;
+				}
+			} while(1);
+			continue;
+		}
+		sys->fprint(stderr, "dd: bad arg: %s\n", key);
+		exits("arg");
+	}
+	if(ctype == NULL && cflag&(LCASE|UCASE))
+		ctype = CNULL;
+	if(ifile != nil)
+		ibf = sys->open(ifile, Sys->OREAD);
+	else
+		ibf = sys->fildes(sys->dup(0, -1));
+
+	if(ibf == nil) {
+		sys->fprint(stderr, "dd: open %s: %r\n", ifile);
+		exits("open");
+	}
+
+	if(ofile != nil){
+		obf = sys->create(ofile, Sys->OWRITE, 8r664);
+		if(obf == nil) {
+			sys->fprint(stderr, "dd: create %s: %r\n", ofile);
+			exits("create");
+		}
+	}else{
+		obf = sys->fildes(sys->dup(1, -1));
+		if(obf == nil) {
+			sys->fprint(stderr, "dd: can't dup file descriptor: %r\n");
+			exits("dup");
+		}
+	}
+	if(bs)
+		ibs = obs = bs;
+	if(ibs == obs && ctype == NULL)
+		fflag++;
+	if(ibs == 0 || obs == 0) {
+		sys->fprint(stderr, "dd: counts: cannot be zero\n");
+		exits("counts");
+	}
+	ibuf = array[ibs] of byte;
+	obuf = array[obs] of byte;
+
+	if(fflag)
+		obuf = ibuf;
+
+	sys->seek(obf, big obs*big oseekn, Sys->SEEKRELA);
+	sys->seek(ibf, big ibs*big iseekn,  Sys->SEEKRELA);
+	while(skip) {
+		sys->read(ibf, ibuf, ibs);
+		skip--;
+	}
+
+	ibc = 0;
+	obc = 0;
+	cbc = 0;
+	op = 0;
+	ip := 0;
+	do {
+		if(ibc-- == 0) {
+			ibc = 0;
+			if(count==0 || nifr+nipr!=count) {
+				if(cflag&(NERR|SYNC))
+					for(ip=0; ip < len ibuf; ip++)
+						ibuf[ip] = byte 0;
+				ibc = sys->read(ibf, ibuf, ibs);
+			}
+			if(ibc == -1) {
+				perror("read");
+				if((cflag&NERR) == 0) {
+					flsh();
+					term();
+				}
+				ibc = 0;
+				for(c:=0; c<ibs; c++)
+					if(ibuf[c] != byte 0)
+						ibc = c;
+				stats();
+			}
+			if(ibc == 0 && --files<=0) {
+				flsh();
+				term();
+			}
+			if(ibc != ibs) {
+				nipr++;
+				if(cflag&SYNC)
+					ibc = ibs;
+			} else
+				nifr++;
+			ip = 0;
+			c := (ibc>>1) & ~1;
+			if(cflag&SWAB && c) do {
+				a := ibuf[ip++];
+				ibuf[ip-1] = ibuf[ip];
+				ibuf[ip++] = a;
+			} while(--c);
+			if(fflag) {
+				obc = ibc;
+				flsh();
+				ibc = 0;
+			}
+			continue;
+		}
+		c := 0;
+		c |= int ibuf[ip++];
+		c &= 8r377;
+		conv(c);
+	} while(1);
+}
+
+conv(c: int)
+{
+	case ctype {
+	NULL => null(c);
+	CNULL => cnull(c);
+	EBCDIC => ebcdic(c);
+	IBM => ibm(c);
+	ASCII => ascii(c);
+	BLOCK => block(c);
+	UNBLOCK => unblock(c);
+	}
+}
+
+flsh()
+{
+	if(obc) {
+		if(obc == obs)
+			nofr++;
+		else
+			nopr++;
+		c := sys->write(obf, obuf, obc);
+		if(c != obc) {
+			perror("write");
+			term();
+		}
+		obc = 0;
+	}
+}
+
+match(s: string): int
+{
+	if(len s > len arg)
+		return 0;
+	if(arg[:len s] == s) {
+		arg = arg[len s:];
+		return 1;
+	}
+	return 0;
+}
+
+
+number(bignum: int): int
+{
+	n := 0;
+	i := 0;
+	while(i < len arg && arg[i] >= '0' && arg[i] <= '9')
+		n = n*10 + arg[i++] - '0';
+	for(;i<len arg; i++) case(arg[i]) {
+		'k' =>
+			n *= 1024;
+		'b' =>
+			n *= 512;
+		'x' =>
+			arg = arg[i:];
+			n *= number(BIG);
+	}
+	if(n>=bignum || n<0) {
+		sys->fprint(stderr, "dd: argument out of range\n");
+		exits("range");
+	}
+	return n;
+}
+
+cnull(cc: int)
+{
+	c := cc;
+	if((cflag&UCASE) && c>='a' && c<='z')
+		c += 'A'-'a';
+	if((cflag&LCASE) && c>='A' && c<='Z')
+		c += 'a'-'A';
+	null(c);
+}
+
+null(c: int)
+{
+	obuf[op++] = byte c;
+	if(++obc >= obs) {
+		flsh();
+		op = 0;
+	}
+}
+
+ascii(cc: int)
+{
+	c := etoa[cc];
+	if(cbs == 0) {
+		cnull(int c);
+		return;
+	}
+	if(c == byte ' ')
+		nspace++;
+	else {
+		while(nspace > 0) {
+			null(' ');
+			nspace--;
+		}
+		cnull(int c);
+	}
+
+	if(++cbc >= cbs) {
+		null('\n');
+		cbc = 0;
+		nspace = 0;
+	}
+}
+
+unblock(cc: int)
+{
+	c := cc & 8r377;
+	if(cbs == 0) {
+		cnull(c);
+		return;
+	}
+	if(c == ' ')
+		nspace++;
+	else {
+		while(nspace > 0) {
+			null(' ');
+			nspace--;
+		}
+		cnull(c);
+	}
+
+	if(++cbc >= cbs) {
+		null('\n');
+		cbc = 0;
+		nspace = 0;
+	}
+}
+
+ebcdic(cc: int)
+{
+
+	c := cc;
+	if(cflag&UCASE && c>='a' && c<='z')
+		c += 'A'-'a';
+	if(cflag&LCASE && c>='A' && c<='Z')
+		c += 'a'-'A';
+	c = int atoe[c];
+	if(cbs == 0) {
+		null(c);
+		return;
+	}
+	if(cc == '\n') {
+		while(cbc < cbs) {
+			null(int atoe[' ']);
+			cbc++;
+		}
+		cbc = 0;
+		return;
+	}
+	if(cbc == cbs)
+		ntrunc++;
+	cbc++;
+	if(cbc <= cbs)
+		null(c);
+}
+
+ibm(cc: int)
+{
+	c := cc;
+	if(cflag&UCASE && c>='a' && c<='z')
+		c += 'A'-'a';
+	if(cflag&LCASE && c>='A' && c<='Z')
+		c += 'a'-'A';
+	c = int atoibm[c] & 8r377;
+	if(cbs == 0) {
+		null(c);
+		return;
+	}
+	if(cc == '\n') {
+		while(cbc < cbs) {
+			null(int atoibm[' ']);
+			cbc++;
+		}
+		cbc = 0;
+		return;
+	}
+	if(cbc == cbs)
+		ntrunc++;
+	cbc++;
+	if(cbc <= cbs)
+		null(c);
+}
+
+block(cc: int)
+{
+	c := cc;
+	if(cflag&UCASE && c>='a' && c<='z')
+		c += 'A'-'a';
+	if(cflag&LCASE && c>='A' && c<='Z')
+		c += 'a'-'A';
+	c &= 8r377;
+	if(cbs == 0) {
+		null(c);
+		return;
+	}
+	if(cc == '\n') {
+		while(cbc < cbs) {
+			null(' ');
+			cbc++;
+		}
+		cbc = 0;
+		return;
+	}
+	if(cbc == cbs)
+		ntrunc++;
+	cbc++;
+	if(cbc <= cbs)
+		null(c);
+}
+
+term()
+{
+	stats();
+	exits(nil);
+}
+
+stats()
+{
+	sys->fprint(stderr, "%ud+%ud records in\n", nifr, nipr);
+	sys->fprint(stderr, "%ud+%ud records out\n", nofr, nopr);
+	if(ntrunc)
+		sys->fprint(stderr, "%ud truncated records\n", ntrunc);
+}
+
+etoa := array[] of
+{
+	byte 8r000,byte 8r001,byte 8r002,byte 8r003,byte 8r234,byte 8r011,byte 8r206,byte 8r177,
+	byte 8r227,byte 8r215,byte 8r216,byte 8r013,byte 8r014,byte 8r015,byte 8r016,byte 8r017,
+	byte 8r020,byte 8r021,byte 8r022,byte 8r023,byte 8r235,byte 8r205,byte 8r010,byte 8r207,
+	byte 8r030,byte 8r031,byte 8r222,byte 8r217,byte 8r034,byte 8r035,byte 8r036,byte 8r037,
+	byte 8r200,byte 8r201,byte 8r202,byte 8r203,byte 8r204,byte 8r012,byte 8r027,byte 8r033,
+	byte 8r210,byte 8r211,byte 8r212,byte 8r213,byte 8r214,byte 8r005,byte 8r006,byte 8r007,
+	byte 8r220,byte 8r221,byte 8r026,byte 8r223,byte 8r224,byte 8r225,byte 8r226,byte 8r004,
+	byte 8r230,byte 8r231,byte 8r232,byte 8r233,byte 8r024,byte 8r025,byte 8r236,byte 8r032,
+	byte 8r040,byte 8r240,byte 8r241,byte 8r242,byte 8r243,byte 8r244,byte 8r245,byte 8r246,
+	byte 8r247,byte 8r250,byte 8r133,byte 8r056,byte 8r074,byte 8r050,byte 8r053,byte 8r041,
+	byte 8r046,byte 8r251,byte 8r252,byte 8r253,byte 8r254,byte 8r255,byte 8r256,byte 8r257,
+	byte 8r260,byte 8r261,byte 8r135,byte 8r044,byte 8r052,byte 8r051,byte 8r073,byte 8r136,
+	byte 8r055,byte 8r057,byte 8r262,byte 8r263,byte 8r264,byte 8r265,byte 8r266,byte 8r267,
+	byte 8r270,byte 8r271,byte 8r174,byte 8r054,byte 8r045,byte 8r137,byte 8r076,byte 8r077,
+	byte 8r272,byte 8r273,byte 8r274,byte 8r275,byte 8r276,byte 8r277,byte 8r300,byte 8r301,
+	byte 8r302,byte 8r140,byte 8r072,byte 8r043,byte 8r100,byte 8r047,byte 8r075,byte 8r042,
+	byte 8r303,byte 8r141,byte 8r142,byte 8r143,byte 8r144,byte 8r145,byte 8r146,byte 8r147,
+	byte 8r150,byte 8r151,byte 8r304,byte 8r305,byte 8r306,byte 8r307,byte 8r310,byte 8r311,
+	byte 8r312,byte 8r152,byte 8r153,byte 8r154,byte 8r155,byte 8r156,byte 8r157,byte 8r160,
+	byte 8r161,byte 8r162,byte 8r313,byte 8r314,byte 8r315,byte 8r316,byte 8r317,byte 8r320,
+	byte 8r321,byte 8r176,byte 8r163,byte 8r164,byte 8r165,byte 8r166,byte 8r167,byte 8r170,
+	byte 8r171,byte 8r172,byte 8r322,byte 8r323,byte 8r324,byte 8r325,byte 8r326,byte 8r327,
+	byte 8r330,byte 8r331,byte 8r332,byte 8r333,byte 8r334,byte 8r335,byte 8r336,byte 8r337,
+	byte 8r340,byte 8r341,byte 8r342,byte 8r343,byte 8r344,byte 8r345,byte 8r346,byte 8r347,
+	byte 8r173,byte 8r101,byte 8r102,byte 8r103,byte 8r104,byte 8r105,byte 8r106,byte 8r107,
+	byte 8r110,byte 8r111,byte 8r350,byte 8r351,byte 8r352,byte 8r353,byte 8r354,byte 8r355,
+	byte 8r175,byte 8r112,byte 8r113,byte 8r114,byte 8r115,byte 8r116,byte 8r117,byte 8r120,
+	byte 8r121,byte 8r122,byte 8r356,byte 8r357,byte 8r360,byte 8r361,byte 8r362,byte 8r363,
+	byte 8r134,byte 8r237,byte 8r123,byte 8r124,byte 8r125,byte 8r126,byte 8r127,byte 8r130,
+	byte 8r131,byte 8r132,byte 8r364,byte 8r365,byte 8r366,byte 8r367,byte 8r370,byte 8r371,
+	byte 8r060,byte 8r061,byte 8r062,byte 8r063,byte 8r064,byte 8r065,byte 8r066,byte 8r067,
+	byte 8r070,byte 8r071,byte 8r372,byte 8r373,byte 8r374,byte 8r375,byte 8r376,byte 8r377,
+};
+atoe := array[] of
+{
+	byte 8r000,byte 8r001,byte 8r002,byte 8r003,byte 8r067,byte 8r055,byte 8r056,byte 8r057,
+	byte 8r026,byte 8r005,byte 8r045,byte 8r013,byte 8r014,byte 8r015,byte 8r016,byte 8r017,
+	byte 8r020,byte 8r021,byte 8r022,byte 8r023,byte 8r074,byte 8r075,byte 8r062,byte 8r046,
+	byte 8r030,byte 8r031,byte 8r077,byte 8r047,byte 8r034,byte 8r035,byte 8r036,byte 8r037,
+	byte 8r100,byte 8r117,byte 8r177,byte 8r173,byte 8r133,byte 8r154,byte 8r120,byte 8r175,
+	byte 8r115,byte 8r135,byte 8r134,byte 8r116,byte 8r153,byte 8r140,byte 8r113,byte 8r141,
+	byte 8r360,byte 8r361,byte 8r362,byte 8r363,byte 8r364,byte 8r365,byte 8r366,byte 8r367,
+	byte 8r370,byte 8r371,byte 8r172,byte 8r136,byte 8r114,byte 8r176,byte 8r156,byte 8r157,
+	byte 8r174,byte 8r301,byte 8r302,byte 8r303,byte 8r304,byte 8r305,byte 8r306,byte 8r307,
+	byte 8r310,byte 8r311,byte 8r321,byte 8r322,byte 8r323,byte 8r324,byte 8r325,byte 8r326,
+	byte 8r327,byte 8r330,byte 8r331,byte 8r342,byte 8r343,byte 8r344,byte 8r345,byte 8r346,
+	byte 8r347,byte 8r350,byte 8r351,byte 8r112,byte 8r340,byte 8r132,byte 8r137,byte 8r155,
+	byte 8r171,byte 8r201,byte 8r202,byte 8r203,byte 8r204,byte 8r205,byte 8r206,byte 8r207,
+	byte 8r210,byte 8r211,byte 8r221,byte 8r222,byte 8r223,byte 8r224,byte 8r225,byte 8r226,
+	byte 8r227,byte 8r230,byte 8r231,byte 8r242,byte 8r243,byte 8r244,byte 8r245,byte 8r246,
+	byte 8r247,byte 8r250,byte 8r251,byte 8r300,byte 8r152,byte 8r320,byte 8r241,byte 8r007,
+	byte 8r040,byte 8r041,byte 8r042,byte 8r043,byte 8r044,byte 8r025,byte 8r006,byte 8r027,
+	byte 8r050,byte 8r051,byte 8r052,byte 8r053,byte 8r054,byte 8r011,byte 8r012,byte 8r033,
+	byte 8r060,byte 8r061,byte 8r032,byte 8r063,byte 8r064,byte 8r065,byte 8r066,byte 8r010,
+	byte 8r070,byte 8r071,byte 8r072,byte 8r073,byte 8r004,byte 8r024,byte 8r076,byte 8r341,
+	byte 8r101,byte 8r102,byte 8r103,byte 8r104,byte 8r105,byte 8r106,byte 8r107,byte 8r110,
+	byte 8r111,byte 8r121,byte 8r122,byte 8r123,byte 8r124,byte 8r125,byte 8r126,byte 8r127,
+	byte 8r130,byte 8r131,byte 8r142,byte 8r143,byte 8r144,byte 8r145,byte 8r146,byte 8r147,
+	byte 8r150,byte 8r151,byte 8r160,byte 8r161,byte 8r162,byte 8r163,byte 8r164,byte 8r165,
+	byte 8r166,byte 8r167,byte 8r170,byte 8r200,byte 8r212,byte 8r213,byte 8r214,byte 8r215,
+	byte 8r216,byte 8r217,byte 8r220,byte 8r232,byte 8r233,byte 8r234,byte 8r235,byte 8r236,
+	byte 8r237,byte 8r240,byte 8r252,byte 8r253,byte 8r254,byte 8r255,byte 8r256,byte 8r257,
+	byte 8r260,byte 8r261,byte 8r262,byte 8r263,byte 8r264,byte 8r265,byte 8r266,byte 8r267,
+	byte 8r270,byte 8r271,byte 8r272,byte 8r273,byte 8r274,byte 8r275,byte 8r276,byte 8r277,
+	byte 8r312,byte 8r313,byte 8r314,byte 8r315,byte 8r316,byte 8r317,byte 8r332,byte 8r333,
+	byte 8r334,byte 8r335,byte 8r336,byte 8r337,byte 8r352,byte 8r353,byte 8r354,byte 8r355,
+	byte 8r356,byte 8r357,byte 8r372,byte 8r373,byte 8r374,byte 8r375,byte 8r376,byte 8r377,
+};
+atoibm := array[] of
+{
+	byte 8r000,byte 8r001,byte 8r002,byte 8r003,byte 8r067,byte 8r055,byte 8r056,byte 8r057,
+	byte 8r026,byte 8r005,byte 8r045,byte 8r013,byte 8r014,byte 8r015,byte 8r016,byte 8r017,
+	byte 8r020,byte 8r021,byte 8r022,byte 8r023,byte 8r074,byte 8r075,byte 8r062,byte 8r046,
+	byte 8r030,byte 8r031,byte 8r077,byte 8r047,byte 8r034,byte 8r035,byte 8r036,byte 8r037,
+	byte 8r100,byte 8r132,byte 8r177,byte 8r173,byte 8r133,byte 8r154,byte 8r120,byte 8r175,
+	byte 8r115,byte 8r135,byte 8r134,byte 8r116,byte 8r153,byte 8r140,byte 8r113,byte 8r141,
+	byte 8r360,byte 8r361,byte 8r362,byte 8r363,byte 8r364,byte 8r365,byte 8r366,byte 8r367,
+	byte 8r370,byte 8r371,byte 8r172,byte 8r136,byte 8r114,byte 8r176,byte 8r156,byte 8r157,
+	byte 8r174,byte 8r301,byte 8r302,byte 8r303,byte 8r304,byte 8r305,byte 8r306,byte 8r307,
+	byte 8r310,byte 8r311,byte 8r321,byte 8r322,byte 8r323,byte 8r324,byte 8r325,byte 8r326,
+	byte 8r327,byte 8r330,byte 8r331,byte 8r342,byte 8r343,byte 8r344,byte 8r345,byte 8r346,
+	byte 8r347,byte 8r350,byte 8r351,byte 8r255,byte 8r340,byte 8r275,byte 8r137,byte 8r155,
+	byte 8r171,byte 8r201,byte 8r202,byte 8r203,byte 8r204,byte 8r205,byte 8r206,byte 8r207,
+	byte 8r210,byte 8r211,byte 8r221,byte 8r222,byte 8r223,byte 8r224,byte 8r225,byte 8r226,
+	byte 8r227,byte 8r230,byte 8r231,byte 8r242,byte 8r243,byte 8r244,byte 8r245,byte 8r246,
+	byte 8r247,byte 8r250,byte 8r251,byte 8r300,byte 8r117,byte 8r320,byte 8r241,byte 8r007,
+	byte 8r040,byte 8r041,byte 8r042,byte 8r043,byte 8r044,byte 8r025,byte 8r006,byte 8r027,
+	byte 8r050,byte 8r051,byte 8r052,byte 8r053,byte 8r054,byte 8r011,byte 8r012,byte 8r033,
+	byte 8r060,byte 8r061,byte 8r032,byte 8r063,byte 8r064,byte 8r065,byte 8r066,byte 8r010,
+	byte 8r070,byte 8r071,byte 8r072,byte 8r073,byte 8r004,byte 8r024,byte 8r076,byte 8r341,
+	byte 8r101,byte 8r102,byte 8r103,byte 8r104,byte 8r105,byte 8r106,byte 8r107,byte 8r110,
+	byte 8r111,byte 8r121,byte 8r122,byte 8r123,byte 8r124,byte 8r125,byte 8r126,byte 8r127,
+	byte 8r130,byte 8r131,byte 8r142,byte 8r143,byte 8r144,byte 8r145,byte 8r146,byte 8r147,
+	byte 8r150,byte 8r151,byte 8r160,byte 8r161,byte 8r162,byte 8r163,byte 8r164,byte 8r165,
+	byte 8r166,byte 8r167,byte 8r170,byte 8r200,byte 8r212,byte 8r213,byte 8r214,byte 8r215,
+	byte 8r216,byte 8r217,byte 8r220,byte 8r232,byte 8r233,byte 8r234,byte 8r235,byte 8r236,
+	byte 8r237,byte 8r240,byte 8r252,byte 8r253,byte 8r254,byte 8r255,byte 8r256,byte 8r257,
+	byte 8r260,byte 8r261,byte 8r262,byte 8r263,byte 8r264,byte 8r265,byte 8r266,byte 8r267,
+	byte 8r270,byte 8r271,byte 8r272,byte 8r273,byte 8r274,byte 8r275,byte 8r276,byte 8r277,
+	byte 8r312,byte 8r313,byte 8r314,byte 8r315,byte 8r316,byte 8r317,byte 8r332,byte 8r333,
+	byte 8r334,byte 8r335,byte 8r336,byte 8r337,byte 8r352,byte 8r353,byte 8r354,byte 8r355,
+	byte 8r356,byte 8r357,byte 8r372,byte 8r373,byte 8r374,byte 8r375,byte 8r376,byte 8r377,
+};
--- /dev/null
+++ b/appl/cmd/dial.b
@@ -1,0 +1,153 @@
+implement Dialc;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	auth: Auth;
+include "dial.m";
+	dial: Dial;
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+
+Dialc: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+badmodule(p: string)
+{
+	sys->fprint(stderr(), "dial: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+DEFAULTALG := "none";
+
+verbose := 0;
+
+init(drawctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	auth = load Auth Auth->PATH;
+	if (auth == nil)
+		badmodule(Auth->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+	sh = load Sh Sh->PATH;
+	if (sh == nil)
+		badmodule(Sh->PATH);
+	dial = load Dial Dial->PATH;
+	if (dial == nil)
+		badmodule(Dial->PATH);
+
+	auth->init();
+	alg: string;
+	keyfile: string;
+	doauth := 1;
+	arg->init(argv);
+	arg->setusage("dial [-A] [-k keyfile] [-a alg] addr command [arg...]");
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'A' =>
+			doauth = 0;
+		'a' =>
+			alg = arg->earg();
+		'f' or
+		'k' =>
+			keyfile = arg->earg();
+			if (! (keyfile[0] == '/' || (len keyfile > 2 &&  keyfile[0:2] == "./")))
+				keyfile = "/usr/" + user() + "/keyring/" + keyfile;
+		'v' =>
+			verbose = 1;
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	if (len argv < 2)
+		arg->usage();
+	arg = nil;
+	(addr, shcmd) := (hd argv, tl argv);
+
+	if (doauth && alg == nil)
+		alg = DEFAULTALG;
+
+	if (alg != nil && keyfile == nil) {
+		kd := "/usr/" + user() + "/keyring/";
+		if (exists(kd + addr))
+			keyfile = kd + addr;
+		else
+			keyfile = kd + "default";
+	}
+	cert: ref Keyring->Authinfo;
+	if (alg != nil) {
+		cert = keyring->readauthinfo(keyfile);
+		if (cert == nil) {
+			sys->fprint(stderr(), "dial: cannot read %s: %r\n", keyfile);
+			raise "fail:bad keyfile";
+		}
+	}
+
+	c := dial->dial(addr, nil);
+	if (c == nil) {
+		sys->fprint(stderr(), "dial: cannot dial %s: %r\n", addr);
+		raise "fail:errors";
+	}
+	user: string;
+	if (alg != nil) {
+		err: string;
+		(c.dfd, err) = auth->client(alg, cert, c.dfd);
+		if (c.dfd == nil) {
+			sys->fprint(stderr(), "dial: authentication failed: %s\n", err);
+			raise "fail:errors";
+		}
+		user = err;
+	}
+	sys->dup(c.dfd.fd, 0);
+	sys->dup(c.dfd.fd, 1);
+	c.dfd = c.cfd = nil;
+	ctxt := Context.new(drawctxt);
+	if (user != nil)
+		ctxt.set("user", sh->stringlist2list(user :: nil));
+	else
+		ctxt.set("user", nil);
+	ctxt.set("net", ref Sh->Listnode(nil, c.dir) :: nil);
+	ctxt.run(sh->stringlist2list(shcmd), 1);
+}
+
+exists(f: string): int
+{
+	(ok, nil) := sys->stat(f);
+	return ok != -1;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+user(): string
+{
+	u := readfile("/dev/user");
+	if (u == nil)
+		return "nobody";
+	return u;
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
--- /dev/null
+++ b/appl/cmd/diff.b
@@ -1,0 +1,854 @@
+implement Diff;
+
+#	diff - differential file comparison
+#
+#	Uses an algorithm due to Harold Stone, which finds
+#	a pair of longest identical subsequences in the two
+#	files.
+#
+#	The major goal is to generate the match vector J.
+#	J[i] is the index of the line in file1 corresponding
+#	to line i file0. J[i] = 0 if there is no
+#	such line in file1.
+#
+#	Lines are hashed so as to work in core. All potential
+#	matches are located by sorting the lines of each file
+#	on the hash (called value). In particular, this
+#	collects the equivalence classes in file1 together.
+#	Subroutine equiv replaces the value of each line in
+#	file0 by the index of the first element of its 
+#	matching equivalence in (the reordered) file1.
+#	To save space equiv squeezes file1 into a single
+#	array member in which the equivalence classes
+#	are simply concatenated, except that their first
+#	members are flagged by changing sign.
+#
+#	Next the indices that point into member are unsorted into
+#	array class according to the original order of file0.
+#
+#	The cleverness lies in routine stone. This marches
+#	through the lines of file0, developing a vector klist
+#	of "k-candidates". At step i a k-candidate is a matched
+#	pair of lines x,y (x in file0 y in file1) such that
+#	there is a common subsequence of lenght k
+#	between the first i lines of file0 and the first y 
+#	lines of file1, but there is no such subsequence for
+#	any smaller y. x is the earliest possible mate to y
+#	that occurs in such a subsequence.
+#
+#	Whenever any of the members of the equivalence class of
+#	lines in file1 matable to a line in file0 has serial number 
+#	less than the y of some k-candidate, that k-candidate 
+#	with the smallest such y is replaced. The new 
+#	k-candidate is chained (via pred) to the current
+#	k-1 candidate so that the actual subsequence can
+#	be recovered. When a member has serial number greater
+#	that the y of all k-candidates, the klist is extended.
+#	At the end, the longest subsequence is pulled out
+#	and placed in the array J by unravel.
+#
+#	With J in hand, the matches there recorded are
+#	check'ed against reality to assure that no spurious
+#	matches have crept in due to hashing. If they have,
+#	they are broken, and "jackpot " is recorded--a harmless
+#	matter except that a true match for a spuriously
+#	mated line may now be unnecessarily reported as a change.
+#
+#	Much of the complexity of the program comes simply
+#	from trying to minimize core utilization and
+#	maximize the range of doable problems by dynamically
+#	allocating what is needed and reusing what is not.
+#	The core requirements for problems larger than somewhat
+#	are (in words) 2*length(file0) + length(file1) +
+#	3*(number of k-candidates installed),  typically about
+#	6n words for files of length n. 
+#
+# 
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio : Bufio;
+Iobuf : import bufio;
+
+include "draw.m";
+	draw: Draw;
+include "readdir.m";
+	readdir : Readdir;
+include "string.m";
+	str : String;
+include "arg.m";
+
+Diff : module  
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+mode : int;			# '\0', 'e', 'f', 'h' 
+bflag : int;			# ignore multiple and trailing blanks 
+rflag : int;			# recurse down directory trees 
+mflag : int;			# pseudo flag: doing multiple files, one dir
+
+REG,
+BIN: con iota;
+
+HALFINT : con 16;
+Usage : con  "usage: diff [ -efbwr ] file1 ... file2";
+
+cand : adt {
+	x : int;
+	y : int;
+	pred : int;
+};
+
+line : adt {
+	serial : int;
+	value : int;
+};
+
+out : ref Iobuf;
+file := array[2] of array of line;
+sfile := array[2] of array of line;	# shortened by pruning common prefix and suffix
+slen := array[2] of int;
+ilen := array[2] of int;
+pref, suff, clen : int;			# length of prefix and suffix
+firstchange : int;
+clist : array of cand;			# merely a free storage pot for candidates
+J : array of int;			# will be overlaid on class
+ixold, ixnew : array of int;
+input := array[2] of ref Iobuf ;
+file1, file2 : string;
+tmpname := array[] of {"/tmp/diff1", "/tmp/diff2"};
+whichtmp : int;
+anychange := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	bufio = load Bufio Bufio->PATH;
+	readdir = load Readdir Readdir->PATH;	
+	str = load String String->PATH;
+	if (bufio==nil)
+		fatal(sys->sprint("cannot load %s: %r", Bufio->PATH));
+	if (readdir==nil)
+		fatal(sys->sprint("cannot load %s: %r", Readdir->PATH));
+	if (str==nil)
+		fatal(sys->sprint("cannot load %s: %r", String->PATH));
+	arg := load Arg Arg->PATH;
+	if (arg==nil)
+		fatal(sys->sprint("cannot load %s: %r", Arg->PATH));
+	fsb, tsb : Sys->Dir;
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		'e' or 'f' =>
+			mode = o;
+		'w' =>
+			bflag = 2;
+		'b' =>
+			bflag = 1;
+		'r' =>
+			rflag = 1;
+		'm' =>
+			mflag = 1;
+		* =>
+			fatal(Usage);
+		}
+	tmp := arg->argv();
+	arg = nil;
+	j := len tmp;
+	if (j < 2)
+		fatal(Usage);
+	arr := array[j] of string;
+	for(i:=0;i<j;i++){
+		arr[i]= hd tmp;
+		tmp = tl tmp;
+	}
+
+	(i,tsb)=sys->stat(arr[j-1]);
+	if (i == -1)
+		fatal(sys->sprint("can't stat %s: %r", arr[j-1]));
+	if (j > 2) {
+		if (!(tsb.qid.qtype&Sys->QTDIR))
+			fatal(Usage);
+		mflag = 1;
+	}
+	else {
+		(i,fsb)=sys->stat(arr[0]);
+		if (i == -1)
+			fatal(sys->sprint("can't stat %s: %r", arr[0]));
+		if ((fsb.qid.qtype&Sys->QTDIR) && (tsb.qid.qtype&Sys->QTDIR))
+			mflag = 1;
+	}
+	out=bufio->fopen(sys->fildes(1),Bufio->OWRITE);
+	for (i = 0; i < j-1; i++) {
+		diff(arr[i], arr[j-1], 0);
+		rmtmpfiles();
+	}
+	rmtmpfiles();
+	out.flush();
+	if (anychange)
+		raise "fail:some";
+}
+
+############################# diffreg from here ....
+
+# shellsort CACM #201
+
+sort(a : array of line, n : int)
+{
+	w : line;
+	m := 0;
+	for (i := 1; i <= n; i *= 2)
+		m = 2*i - 1;
+	for (m /= 2; m != 0; m /= 2) {
+		for (j := 1; j <= n-m ; j++) {
+			ai:=j;
+			aim:=j+m;
+			do {
+				if (a[aim].value > a[ai].value ||
+				   a[aim].value == a[ai].value &&
+				   a[aim].serial > a[ai].serial)
+					break;
+				w = a[ai];
+				a[ai] = a[aim];
+				a[aim] = w;
+				aim=ai;
+				ai-=m;
+			} while (ai > 0 && aim >= ai);
+		}
+	}
+}
+
+unsort(f : array of line, l : int) : array of int 
+{
+	i : int;
+	a := array[l+1] of int;
+	for(i=1;i<=l;i++)
+		a[f[i].serial] = f[i].value;
+	return a;
+}
+
+prune() 
+{
+	for(pref=0;pref< ilen[0]&&pref< ilen[1]&&
+		file[0][pref+1].value==file[1][pref+1].value;
+		pref++ ) ;
+	for(suff=0;suff< ilen[0]-pref&&suff< ilen[1]-pref&&
+		file[0][ilen[0]-suff].value==file[1][ilen[1]-suff].value;
+		suff++) ;
+	for(j:=0;j<2;j++) {
+		sfile[j] = file[j][pref:];
+		slen[j]= ilen[j]-pref-suff;
+		for(i:=0;i<=slen[j];i++)
+			sfile[j][i].serial = i;
+	}
+}
+
+equiv(a: array of line, n:int , b: array of line, m: int, c : array of int) 
+{
+	i := 1;
+	j := 1;
+	while(i<=n && j<=m) {
+		if(a[i].value < b[j].value)
+			a[i++].value = 0;
+		else if(a[i].value == b[j].value)
+			a[i++].value = j;
+		else
+			j++;
+	}
+	while(i <= n)
+		a[i++].value = 0;
+	b[m+1].value = 0; # huh ?
+	j = 1;
+	while(j <= m) {
+		c[j] = -b[j].serial;
+		while(b[j+1].value == b[j].value) {
+			j++;
+			c[j] = b[j].serial;
+		}
+		j++;
+	}
+	c[j] = -1;
+}
+
+newcand(x, y, pred : int) : int 
+{
+	if (clen==len clist){
+		q := array[clen*2] of cand;
+		q[0:]=clist;
+		clist= array[clen*2] of cand;
+		clist[0:]=q;
+		q=nil;
+	}
+	clist[clen].x=x;
+	clist[clen].y=y;
+	clist[clen].pred=pred;
+	return clen++;
+}
+
+search(c : array of int, k,y : int) : int 
+{
+	if(clist[c[k]].y < y)	# quick look for typical case
+		return k+1;
+	i := 0;
+	j := k+1;
+	while((l:=(i+j)/2) > i) {
+		t := clist[c[l]].y;
+		if(t > y)
+			j = l;
+		else if(t < y)
+			i = l;
+		else
+			return l;
+	}
+	return l+1;
+}
+
+stone(a : array of int ,n : int, b: array of int , c : array of int) : int 
+{
+	oldc, oldl, tc, l ,y : int;
+	k := 0;
+	c[0] = newcand(0,0,0);
+	for(i:=1; i<=n; i++) {
+		j := a[i];
+		if(j==0)
+			continue;
+		y = -b[j];
+		oldl = 0;
+		oldc = c[0];
+		do {
+			if(y <= clist[oldc].y)
+				continue;
+			l = search(c, k, y);
+			if(l!=oldl+1)
+				oldc = c[l-1];
+			if(l<=k) {
+				if(clist[c[l]].y <= y)
+					continue;
+				tc = c[l];
+				c[l] = newcand(i,y,oldc);
+				oldc = tc;
+				oldl = l;
+			} else {
+				c[l] = newcand(i,y,oldc);
+				k++;
+				break;
+			}
+		} while((y=b[j+=1]) > 0);
+	}
+	return k;
+}
+
+unravel(p : int) 
+{
+	for(i:=0; i<=ilen[0]; i++) {
+		if (i <= pref)
+			J[i] = i;
+		else if (i > ilen[0]-suff)
+			J[i] = i+ ilen[1]-ilen[0];
+		else
+			J[i] = 0;
+	}
+	for(q:=clist[p];q.y!=0;q=clist[q.pred])
+		J[q.x+pref] = q.y+pref;
+}
+
+output() 
+{
+	i1: int;
+	m := ilen[0];
+	J[0] = 0;
+	J[m+1] = ilen[1]+1;
+	if (mode != 'e') {
+		for (i0 := 1; i0 <= m; i0 = i1+1) {
+			while (i0 <= m && J[i0] == J[i0-1]+1)
+				i0++;
+			j0 := J[i0-1]+1;
+			i1 = i0-1;
+			while (i1 < m && J[i1+1] == 0)
+				i1++;
+			j1 := J[i1+1]-1;
+			J[i1] = j1;
+			change(i0, i1, j0, j1);
+		}
+	}
+	else {
+		for (i0 := m; i0 >= 1; i0 = i1-1) {
+			while (i0 >= 1 && J[i0] == J[i0+1]-1 && J[i0])
+				i0--;
+			j0 := J[i0+1]-1;
+			i1 = i0+1;
+			while (i1 > 1 && J[i1-1] == 0)
+				i1--;
+			j1 := J[i1-1]+1;
+			J[i1] = j1;
+			change(i1 , i0, j1, j0);
+		}
+	}
+	if (m == 0)
+		change(1, 0, 1, ilen[1]);
+	out.flush();
+}
+
+diffreg(f,t : string) 
+{
+	k : int;
+
+	(b0, b0type) := prepare(0, f);
+	if (b0==nil)
+		return;
+	(b1, b1type) := prepare(1, t);
+	if (b1==nil) {
+		b0=nil;
+		return;
+	}
+	if (b0type == BIN || b1type == BIN) {
+		if (cmp(b0, b1)) {
+			out.puts(sys->sprint("Binary files %s %s differ\n", f, t));
+			anychange = 1;
+		}
+		b0 = nil;
+		b1 = nil;
+		return;
+	}
+	clen=0;
+	prune();
+	file[0]=nil;
+	file[1]=nil;
+	sort(sfile[0],slen[0]);
+	sort(sfile[1],slen[1]);
+	member := array[slen[1]+2] of int;
+	equiv(sfile[0], slen[0],sfile[1],slen[1], member);
+	class:=unsort(sfile[0],slen[0]);
+	sfile[0]=nil;
+	sfile[1]=nil;
+	klist := array[slen[0]+2] of int;
+	clist = array[1] of cand;
+	k = stone(class, slen[0], member, klist);
+	J = array[ilen[0]+2] of int;
+	unravel(klist[k]);
+	clist=nil;
+	klist=nil;
+	class=nil;
+	member=nil;
+	ixold = array[ilen[0]+2] of int;
+	ixnew = array[ilen[1]+2] of int;
+
+	b0.seek(big 0, 0); 
+	b1.seek(big 0, 0);
+	check(b0, b1);
+	output();
+	ixold=nil;
+	ixnew=nil;
+	b0=nil; 
+	b1=nil;			
+}
+
+######################## diffio starts here...
+
+
+# hashing has the effect of
+# arranging line in 7-bit bytes and then
+# summing 1-s complement in 16-bit hunks 
+
+readhash(bp : ref Iobuf) : int 
+{
+	sum := 1;
+	shift := 0;
+	buf := bp.gets('\n');
+	if (buf == nil)
+		return 0;
+	buf = buf[0:len buf -1];
+	p := 0;
+	case bflag {
+		# various types of white space handling 
+		0 =>
+			while (p< len buf) {
+				sum += (buf[p] << (shift &= (HALFINT-1)));
+				p++;
+				shift += 7;
+			}
+		1 =>
+			
+			 # coalesce multiple white-space
+			 
+			for (space := 0; p< len buf; p++) {
+				if (buf[p]==' ' || buf[p]=='\t') {
+					space++;
+					continue;
+				}
+				if (space) {
+					shift += 7;
+					space = 0;
+				}
+				sum +=  (buf[p] << (shift &= (HALFINT-1)));
+				p++;
+				shift += 7;
+			}
+		* =>
+			
+			 # strip all white-space
+			 
+			while (p< len buf) {
+				if (buf[p]==' ' || buf[p]=='\t') {
+					p++;
+					continue;
+				}
+				sum +=  (buf[p] << (shift &= (HALFINT-1)));
+				p++;
+				shift += 7;
+			}
+	}
+	return sum;
+}
+
+prepare(i : int, arg : string) : (ref Iobuf, int)
+{
+	h : int;
+	bp := bufio->open(arg,Bufio->OREAD);
+	if (bp==nil) {
+		error(sys->sprint("cannot open %s: %r", arg));
+		return (nil, 0);
+	}
+	buf := array[1024] of byte;
+	n :=bp.read(buf, len buf);
+	str1 := string buf[0:n];
+	for (j:=0;j<len str1 -2;j++)
+		if (str1[j] == Sys->UTFerror)
+			return (bp, BIN);
+	bp.seek(big 0, Sys->SEEKSTART);
+	p := array[4] of line;
+	for (j = 0; h = readhash(bp); p[j].value = h){
+		j++;
+		if (j+3>=len p){
+			newp:=array[len p*2] of line;
+			newp[0:]=p[0:];
+			p=array[len p*2] of line;
+			p=newp;
+			newp=nil;
+		}
+	}
+	ilen[i]=j;
+	file[i] = p;
+	input[i] = bp;			
+	if (i == 0) {			
+		file1 = arg;
+		firstchange = 0;
+	}
+	else
+		file2 = arg;
+	return (bp, REG);
+}
+
+squishspace(buf : string) : string 
+{
+	q:=0;
+	p:=0;
+	for (space := 0; q<len buf; q++) {
+		if (buf[q]==' ' || buf[q]=='\t') {
+			space++;
+			continue;
+		}
+		if (space && bflag == 1) {
+			buf[p] = ' ';
+			p++;
+			space = 0;
+		}
+		buf[p]=buf[q];
+		p++;
+	}
+	buf=buf[0:p];
+	return buf;
+}
+
+
+# need to fix up for unexpected EOF's
+
+ftell(b: ref Iobuf): int
+{
+	return int b.offset();
+}
+
+check(bf, bt : ref Iobuf) 
+{
+	fbuf, tbuf : string;
+	f:=1;
+	t:=1;
+	ixold[0] = ixnew[0] = 0;
+	for (; f < ilen[0]; f++) {
+		fbuf = bf.gets('\n');
+		if (fbuf!=nil)
+			fbuf=fbuf[0:len fbuf -1];
+		ixold[f] = ftell(bf);
+		if (J[f] == 0)
+			continue;
+		tbuflen: int;
+		do {
+			tbuf = bt.gets('\n');
+			if (tbuf!=nil)
+				tbuf=tbuf[0:len tbuf -1];
+			tbuflen = len array of byte tbuf;
+			ixnew[t] = ftell(bt);
+		} while (t++ < J[f]);
+		if (bflag) {
+			fbuf = squishspace(fbuf);
+			tbuf = squishspace(tbuf);
+		}
+		if (len fbuf != len tbuf || fbuf!=tbuf)
+			J[f] = 0;
+	}
+	while (t < ilen[1]) {
+		tbuf = bt.gets('\n');
+		if (tbuf!=nil)
+			tbuf=tbuf[0:len tbuf -1];
+		ixnew[t] = ftell(bt);
+		t++;
+	}
+}
+
+range(a, b : int, separator : string) 
+{
+	if (a>b)
+		out.puts(sys->sprint("%d", b));
+	else
+		out.puts(sys->sprint("%d", a));
+	if (a < b)
+		out.puts(sys->sprint("%s%d", separator, b));
+}
+
+fetch(f : array of int, a,b : int , bp : ref Iobuf, s : string) 
+{
+	buf : string;
+	bp.seek(big f[a-1], 0);
+	while (a++ <= b) {
+		buf=bp.gets('\n');
+		out.puts(s);
+		out.puts(buf);
+	}
+}
+
+change(a, b, c, d : int) 
+{
+	if (a > b && c > d)
+		return;
+	anychange = 1;
+	if (mflag && firstchange == 0) {
+		out.puts(sys->sprint( "diff %s %s\n", file1, file2));
+		firstchange = 1;
+	}
+	if (mode != 'f') {
+		range(a, b, ",");
+		if (a>b)
+			out.putc('a');
+		else if (c>d)
+			out.putc('d');
+		else
+			out.putc('c');
+		if (mode != 'e')
+			range(c, d, ",");
+	}
+	else {
+		if (a>b)
+			out.putc('a');
+		else if (c>d)
+			out.putc('d');
+		else
+			out.putc('c');
+		range(a, b, " ");
+	}
+	out.putc('\n');
+	if (mode == 0) {
+		fetch(ixold, a, b, input[0], "< ");
+		if (a <= b && c <= d)
+			out.puts("---\n");
+	}
+	if (mode==0)
+		fetch(ixnew, c, d, input[1], "> ");
+	else
+		fetch(ixnew, c, d, input[1], "");
+
+	if (mode != 0 && c <= d)
+		out.puts(".\n");
+}
+
+
+######################### diffdir starts here ......
+
+scandir(name : string) : array of string 
+{
+	(db,nitems):= readdir->init(name,Readdir->NAME);
+	cp := array[nitems] of string;
+	for(i:=0;i<nitems;i++)
+		cp[i]=db[i].name;
+	return cp;
+}
+
+
+diffdir(f, t : string, level : int) 
+{
+	df, dt : array of string;
+	fb, tb : string;
+	i:=0;
+	j:=0;
+	df = scandir(f);
+	dt = scandir(t);
+	while ((i<len df) || (j<len dt)) {
+		if ((j==len dt) || (i<len df && df[i] < dt[j])) {
+			if (mode == 0)
+				out.puts(sys->sprint("Only in %s: %s\n", f, df[i]));
+			i++;
+			continue;
+		}
+		if ((i==len df) || (j<len dt && df[i] > dt[j])) {
+			if (mode == 0)
+				out.puts(sys->sprint("Only in %s: %s\n", t, dt[j]));
+			j++;
+			continue;
+		}
+		fb=sys->sprint("%s/%s", f, df[i]);
+		tb=sys->sprint("%s/%s", t, dt[j]);		
+		diff(fb, tb, level+1);
+		i++; j++;
+	}
+}
+
+cmp(b0, b1: ref Iobuf): int
+{
+	b0.seek(big 0, Sys->SEEKSTART);
+	b1.seek(big 0, Sys->SEEKSTART);
+	buf0 := array[1024] of byte;
+	buf1 := array[1024] of byte;
+	for (;;) {
+		n0 := b0.read(buf0, len buf0);
+		n1 := b1.read(buf1, len buf1);
+
+		if (n0 != n1)
+			return 1;
+
+		if (n0 == 0)
+			return 0;
+
+		for (i := 0; i < n0; i++) 
+			if (buf0[i] != buf1[i])
+				return 1;
+	}
+}
+
+################## main from here.....
+
+REGULAR_FILE(s : Sys->Dir) : int 
+{
+	# both pipes and networks contain non-zero-length files
+	# which are not seekable.
+	return (s.qid.qtype&Sys->QTDIR) == 0 &&
+		s.dtype != '|' &&
+		s.dtype != 'I';
+#		&& s.length > 0;	device files have zero length. 
+}
+
+rmtmpfiles() 
+{
+	while (whichtmp > 0) {
+		whichtmp--;
+		sys->remove(tmpname[whichtmp]);
+	}
+}
+
+mktmpfile(inputf : ref Sys->FD) : (string, Sys->Dir) 
+{
+	i, j : int;
+	sb : Sys->Dir;
+	p : string;
+	buf := array[8192] of byte;
+
+	p = tmpname[whichtmp++];
+	fd := sys->create(p, Sys->OWRITE, 8r600);
+	if (fd == nil) {
+		error(sys->sprint("cannot create %s: %r", p));
+		return (nil, sb);
+	}
+	while ((i = sys->read(inputf, buf, len buf)) > 0) {
+		if ((i = sys->write(fd, buf, i)) < 0)
+			break;
+	}
+	(j,sb)=sys->fstat(fd);
+	if (i < 0 || j < 0) {
+		error(sys->sprint("cannot read/write %s: %r", p));
+		return (nil, sb);
+	}
+	return (p, sb);
+}
+
+
+statfile(file : string) : (string,Sys->Dir) 
+{
+	(ret,sb):=sys->stat(file);
+	if (ret==-1) {
+		if (file != "-" || sys->fstat(sys->fildes(0)).t0 == -1){
+			error(sys->sprint("cannot stat %s: %r", file));
+			return (nil,sb);
+		}
+		(file, sb) = mktmpfile(sys->fildes(0));
+	}
+	else if (!REGULAR_FILE(sb) && !(sb.qid.qtype&Sys->QTDIR)) {
+		if ((i := sys->open(file, Sys->OREAD)) == nil) {
+			error(sys->sprint("cannot open %s: %r", file));
+			return (nil, sb);
+		}
+		(file, sb) = mktmpfile(i);
+	}
+	return (file,sb);
+}
+
+diff(f, t : string, level : int) 
+{
+	fp,tp,p,rest,fb,tb : string;
+	fsb, tsb : Sys->Dir;
+	(fp,fsb) = statfile(f);
+	if (fp == nil)
+		return;
+	(tp,tsb) = statfile(t);
+	if (tp == nil)
+		return;
+	if ((fsb.qid.qtype&Sys->QTDIR) && (tsb.qid.qtype&Sys->QTDIR)) {
+		if (rflag || level == 0)
+			diffdir(fp, tp, level);
+		else
+			out.puts(sys->sprint("Common subdirectories: %s and %s\n", fp, tp));
+	}
+	else if (REGULAR_FILE(fsb) && REGULAR_FILE(tsb)){
+		diffreg(fp, tp);
+	} else {
+		if (!(fsb.qid.qtype&Sys->QTDIR)) {
+			(p,rest)=str->splitr(f,"/");
+			if (rest!=nil)
+				p = rest;
+			tb=sys->sprint("%s/%s", tp, p);			
+			diffreg(fp, tb);
+		}
+		else {
+			(p,rest)=str->splitr(t,"/");
+			if (rest!=nil)
+				p = rest;
+			fb=sys->sprint("%s/%s", fp, p);			
+			diffreg(fb, tp);
+		}
+	}
+}
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "diff: %s\n", s);
+	raise "fail:error";
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "diff: %s\n", s);
+}
--- /dev/null
+++ b/appl/cmd/dossrv.b
@@ -1,0 +1,3431 @@
+implement Dossrv;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+Dossrv: module
+{
+        init:   fn(ctxt: ref Draw->Context, args: list of string);
+        system:   fn(ctxt: ref Draw->Context, args: list of string): string;
+};
+
+arg0 := "dossrv";
+
+deffile: string;
+pflag := 0;
+debug := 0;
+
+usage(iscmd: int): string
+{
+	sys->fprint(sys->fildes(2), "usage: %s [-v] [-s] [-F] [-c] [-S secpertrack] [-f devicefile] [-m mountpoint]\n", arg0);
+	if(iscmd)
+		raise "fail:usage";
+	return "usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	e := init2(nil, args, 1);
+	if(e != nil){
+		sys->fprint(sys->fildes(2), "%s: %s\n", arg0, e);
+		raise "fail:error";
+	}
+}
+
+system(nil: ref Draw->Context, args: list of string): string
+{
+	e := init2(nil, args, 0);
+	if(e != nil)
+		sys->fprint(sys->fildes(2), "%s: %s\n", arg0, e);
+	return e;
+}
+
+nomod(s: string): string
+{
+	return sys->sprint("can't load %s: %r", s);
+}
+
+init2(nil: ref Draw->Context, args: list of string, iscmd: int): string
+{
+	sys = load Sys Sys->PATH;
+
+	pipefd := array[2] of ref Sys->FD;
+
+	srvfile := "/n/dos"; 
+	deffile = "";	# no default, for safety
+	sectors := 0;
+	stdin := 0;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		return nomod(Arg->PATH);
+	arg->init(args);
+	arg0 = arg->progname();
+	while((o := arg->opt()) != 0) {
+		case o {
+		'v' =>
+			if(debug & STYX_MESS)
+				debug |= VERBOSE;
+			debug |= STYX_MESS;
+		'F' =>
+			debug |= FAT_INFO;
+		'c' =>
+			debug |= CLUSTER_INFO;
+			iodebug = 1;
+		'S' =>
+			s := arg->arg();
+			if(s != nil && s[0]>='0' && s[0]<='9')
+				sectors = int s;
+			else
+				return usage(iscmd);
+		's' =>
+			stdin = 1;
+		'f' =>
+			deffile = arg->arg();
+			if(deffile == nil)
+				return usage(iscmd);
+		'm' =>
+			srvfile = arg->arg();
+			if(srvfile == nil)
+				return usage(iscmd);
+		'p' =>
+			pflag++;
+		* =>
+			return usage(iscmd);
+		}
+	}
+	args = arg->argv();
+	arg = nil;
+
+	if(deffile == "" || !stdin && srvfile == "")
+		return usage(iscmd);
+
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		return nomod(Styx->PATH);
+	styx->init();
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return nomod(Daytime->PATH);
+
+	iotrackinit(sectors);
+
+	if(!stdin) {
+		if(sys->pipe(pipefd) < 0)
+			return sys->sprint("can't create pipe: %r");
+	}else{
+		pipefd[0] = nil;
+		pipefd[1] = sys->fildes(1);
+	}
+
+	dossetup();
+
+	spawn dossrv(pipefd[1]);
+
+	if(!stdin) {
+		if(sys->mount(pipefd[0], nil, srvfile, sys->MREPL|sys->MCREATE, deffile) < 0)
+			return sys->sprint("mount %s: %r", srvfile);
+	}
+
+	return nil;
+}
+
+#
+# Styx server
+#
+
+	Enevermind,
+	Eformat,
+	Eio,
+	Enomem,
+	Enonexist,
+	Enotdir,
+	Enofid,
+	Efidopen,
+	Efidinuse,
+	Eexist,
+	Eperm,
+	Enofilsys,
+	Eauth,
+	Econtig,
+	Efull,
+	Eopen,
+	Ephase: con iota;
+
+errmsg := array[] of {
+	Enevermind	=> "never mind",
+	Eformat		=> "unknown format",
+	Eio		=> "I/O error",
+	Enomem		=> "server out of memory",
+	Enonexist	=> "file does not exist",
+	Enotdir => "not a directory",
+	Enofid => "no such fid",
+	Efidopen => "fid already open",
+	Efidinuse => "fid in use",
+	Eexist		=> "file exists",
+	Eperm		=> "permission denied",
+	Enofilsys	=> "no file system device specified",
+	Eauth		=> "authentication failed",
+	Econtig =>	"out of contiguous disk space",
+	Efull =>	"file system full",
+	Eopen =>	"invalid open mode",
+	Ephase => "phase error -- directory entry not found",
+};
+
+e(n: int): ref Rmsg.Error
+{
+	if(n < 0 || n >= len errmsg)
+		return ref Rmsg.Error(0, "it's thermal problems");
+	return ref Rmsg.Error(0, errmsg[n]);
+}
+
+dossrv(rfd: ref Sys->FD)
+{
+	sys->pctl(Sys->NEWFD, rfd.fd :: 2 :: nil);
+	rfd = sys->fildes(rfd.fd);
+	while((t := Tmsg.read(rfd, 0)) != nil){
+		if(debug & STYX_MESS)
+			chat(sys->sprint("%s...", t.text()));
+
+		r: ref Rmsg;
+		pick m := t {
+		Readerror =>
+			panic(sys->sprint("mount read error: %s", m.error));
+		Version =>
+ 			r = rversion(m);
+		Auth =>
+			r = rauth(m);
+		Flush =>
+ 			r = rflush(m);
+		Attach =>
+ 			r = rattach(m);
+		Walk =>
+ 			r = rwalk(m);
+		Open =>
+ 			r = ropen(m);
+		Create =>
+ 			r = rcreate(m);
+		Read =>
+ 			r = rread(m);
+		Write =>
+ 			r = rwrite(m);
+		Clunk =>
+ 			r = rclunk(m);
+		Remove =>
+ 			r = rremove(m);
+		Stat =>
+ 			r = rstat(m);
+		Wstat =>
+ 			r = rwstat(m);
+		* =>
+			panic("Styx mtype");
+		}
+		pick m := r {
+		Error =>
+			r.tag = t.tag;
+		}
+		rbuf := r.pack();
+		if(rbuf == nil)
+			panic("Rmsg.pack");
+		if(debug & STYX_MESS)
+			chat(sys->sprint("%s\n", r.text()));
+		if(sys->write(rfd, rbuf, len rbuf) != len rbuf)
+			panic("mount write");
+	}
+
+	if(debug & STYX_MESS)
+		chat("server EOF\n");
+}
+
+rversion(t: ref Tmsg.Version): ref Rmsg
+{
+	(msize, version) := styx->compatible(t, Styx->MAXRPC, Styx->VERSION);
+	return ref Rmsg.Version(t.tag, msize, version);
+}
+
+rauth(t: ref Tmsg.Auth): ref Rmsg
+{
+	return ref Rmsg.Error(t.tag, "authentication not required");
+}
+
+rflush(t: ref Tmsg.Flush): ref Rmsg
+{
+	return ref Rmsg.Flush(t.tag);
+}
+
+rattach(t: ref Tmsg.Attach): ref Rmsg
+{
+	root := xfile(t.fid, Clean);
+	if(root == nil)
+		return e(Eio);
+	if(t.aname == nil)
+		t.aname = deffile;
+	(xf, ec) := getxfs(t.aname);
+	root.xf = xf;
+	if(xf == nil) {
+		if(root!=nil)
+			xfile(t.fid, Clunk);
+		return ref Rmsg.Error(t.tag, ec);
+	}
+	if(xf.fmt == 0 && dosfs(xf) < 0){
+		if(root!=nil)
+			xfile(t.fid, Clunk);
+		return e(Eformat);
+	}
+
+	root.qid = Sys->Qid(big 0, 0, Sys->QTDIR);
+	root.xf.rootqid = root.qid;
+	return ref Rmsg.Attach(t.tag, root.qid);
+}
+
+clone(ofl: ref Xfile, newfid: int): ref Xfile
+{
+	nfl := xfile(newfid, Clean);
+	next := nfl.next;
+	*nfl = *ofl;
+	nfl.ptr = nil;
+	nfl.next = next;
+	nfl.fid = newfid;
+	refxfs(nfl.xf, 1);
+	if(ofl.ptr != nil){
+		dp := ref *ofl.ptr;
+		dp.p = nil;
+		dp.d = nil;
+		nfl.ptr = dp;
+	}
+	return nfl;
+}
+
+walk1(f: ref Xfile, name: string): ref Rmsg.Error
+{
+	if((f.qid.qtype & Sys->QTDIR) == 0){
+		if(debug)
+			chat(sys->sprint("qid.path=0x%bx...", f.qid.path));
+		return e(Enotdir);
+	}
+
+	if(name == ".")	# can't happen
+		return nil;
+
+	if(name== "..") {
+		if(f.qid.path == f.xf.rootqid.path) {
+			if (debug)
+				chat("walkup from root...");
+			return nil;
+		}
+		(r,dp) := walkup(f);
+		if(r < 0)
+			return e(Enonexist);
+
+		f.ptr = dp;
+		if(dp.addr == 0) {
+			f.qid.path = f.xf.rootqid.path;
+			f.qid.qtype = Sys->QTFILE;
+		} else {
+			f.qid.path = QIDPATH(dp);
+			f.qid.qtype = Sys->QTDIR;
+		}
+	} else {
+		if(getfile(f) < 0)
+			return e(Enonexist);
+		(r,dp) := searchdir(f, name, 0,1);
+		putfile(f);
+		if(r < 0)
+			return e(Enonexist);
+
+		f.ptr = dp;
+		f.qid.path = QIDPATH(dp);
+		f.qid.qtype = Sys->QTFILE;
+		if(dp.addr == 0)
+			f.qid.path = f.xf.rootqid.path;
+		else {
+			d := Dosdir.arr2Dd(dp.p.iobuf[dp.offset:dp.offset+DOSDIRSIZE]);
+			if((int d.attr & DDIR) !=  0)
+				f.qid.qtype = Sys->QTDIR;
+		}
+		putfile(f);
+	}
+	return nil;
+}
+
+rwalk(t: ref Tmsg.Walk): ref Rmsg
+{
+	f := xfile(t.fid, Asis);
+	if(f==nil) {
+		if(debug)
+			chat("no xfile...");
+		return e(Enofid);
+	}
+	nf: ref Xfile;
+	if(t.newfid != t.fid)
+		f = nf = clone(f, t.newfid);
+	qids: array of Sys->Qid;
+	if(len t.names > 0){
+		savedqid := f.qid;
+		savedptr := f.ptr;
+		qids = array[len t.names] of Sys->Qid;
+		for(i := 0; i < len t.names; i++){
+			e := walk1(f, t.names[i]);
+			if(e != nil){
+				f.qid = savedqid;
+				f.ptr = savedptr;
+				if(nf != nil)
+					xfile(t.newfid, Clunk);
+				if(i == 0)
+					return e;
+				return ref Rmsg.Walk(t.tag, qids[0:i]);
+			}
+			qids[i] = f.qid;
+		}
+	}
+	return ref Rmsg.Walk(t.tag, qids);
+}
+
+ropen(t: ref Tmsg.Open): ref Rmsg
+{
+	attr: int;
+
+	omode := 0;
+	f := xfile(t.fid, Asis);
+	if(f == nil)
+		return e(Enofid);
+	if((f.flags&Omodes) != 0)
+		return e(Efidopen);
+
+	dp := f.ptr;
+	if(dp.paddr && (t.mode & Styx->ORCLOSE) != 0) {
+		# check on parent directory of file to be deleted
+		p := getsect(f.xf, dp.paddr);
+		if(p == nil)
+			return e(Eio);
+		# 11 is the attr byte offset in a FAT directory entry
+		attr = int p.iobuf[dp.poffset+11];
+		putsect(p);
+		if((attr & int DRONLY) != 0)
+			return e(Eperm);
+		omode |= Orclose;
+	} else if(t.mode & Styx->ORCLOSE)
+		omode |= Orclose;
+
+	if(getfile(f) < 0)
+		return e(Enonexist);
+
+	if(dp.addr != 0) {
+		d := Dosdir.arr2Dd(dp.p.iobuf[dp.offset:dp.offset+DOSDIRSIZE]);
+		attr = int d.attr;
+	} else
+		attr = int DDIR;
+
+	case t.mode & 7 {
+	Styx->OREAD or
+	Styx->OEXEC =>
+		omode |= Oread;
+	Styx->ORDWR =>
+		omode |= Oread;
+		omode |= Owrite;
+		if(attr & int (DRONLY|DDIR)) {
+			putfile(f);
+			return e(Eperm);
+		}
+	Styx->OWRITE =>
+		omode |= Owrite;
+		if(attr & int (DRONLY|DDIR)) {
+			putfile(f);
+			return e(Eperm);
+		}
+	* =>
+		putfile(f);
+		return e(Eopen);
+	}
+
+	if(t.mode & Styx->OTRUNC) {
+		if((attr & int DDIR)!=0 || (attr & int DRONLY) != 0) {
+			putfile(f);
+			return e(Eperm);
+		}
+
+		if(truncfile(f) < 0) {
+			putfile(f);
+			return e(Eio);
+		}
+	}
+
+	f.flags |= omode;
+	putfile(f);
+	return ref Rmsg.Open(t.tag, f.qid, Styx->MAXFDATA);
+}
+
+mkdentry(xf: ref Xfs, ndp: ref Dosptr, name: string, sname: string, islong: int, nattr: byte, start: array of byte, length: array of byte): int
+{
+	ndp.p = getsect(xf, ndp.addr);
+	if(ndp.p == nil)
+		return Eio;
+	if(islong && (r := putlongname(xf, ndp, name, sname)) < 0){
+		putsect(ndp.p);
+		if(r == -2)
+			return Efull;
+		return Eio;
+	}
+
+	nd := ref Dosdir(".       ","   ",byte 0,array[10] of { * => byte 0},
+			array[2] of { * => byte 0}, array[2] of { * => byte 0},
+			array[2] of { * => byte 0},array[4] of { * => byte 0});
+
+	nd.attr = nattr;
+	puttime(nd);
+	nd.start[0: ] = start[0: 2];
+	nd.length[0: ] = length[0: 4];
+
+	if(islong)
+		putname(sname[0:8]+"."+sname[8:11], nd);
+	else
+		putname(name, nd);
+	ndp.p.iobuf[ndp.offset: ] = Dosdir.Dd2arr(nd);
+	ndp.p.flags |= BMOD;
+	return 0;
+}
+
+rcreate(t: ref Tmsg.Create): ref Rmsg
+{
+	bp: ref Dosbpb;
+	omode:=0;
+	start:=0;
+	sname := "";
+	islong :=0;
+
+	f := xfile(t.fid, Asis);
+	if(f == nil)
+		return e(Enofid);
+	if((f.flags&Omodes) != 0)
+		return e(Efidopen);
+	if(getfile(f)<0)
+		return e(Eio);
+
+	pdp := f.ptr;
+	if(pdp.addr != 0)
+		pd := Dosdir.arr2Dd(pdp.p.iobuf[pdp.offset:pdp.offset+DOSDIRSIZE]);
+	else
+		pd = nil;
+
+	if(pd != nil)
+		attr := int pd.attr;
+	else
+		attr = DDIR;
+
+	if(!(attr & DDIR) || (attr & DRONLY)) {
+		putfile(f);
+		return e(Eperm);
+	}
+
+	if(t.mode & Styx->ORCLOSE)
+		omode |= Orclose;
+
+	case (t.mode & 7) {
+	Styx->OREAD or
+	Styx->OEXEC =>
+		omode |= Oread;
+	Styx->OWRITE or
+	Styx->ORDWR =>
+		if ((t.mode & 7) == Styx->ORDWR)
+			omode |= Oread;
+		omode |= Owrite;
+		if(t.perm & Sys->DMDIR){
+			putfile(f);
+			return e(Eperm);
+		}
+	* =>
+		putfile(f);
+		return e(Eopen);
+	}
+
+	if(t.name=="." || t.name=="..") {
+		putfile(f);
+		return e(Eperm);
+	}
+
+	(r,ndp) := searchdir(f, t.name, 1, 1);
+	if(r < 0) {
+		putfile(f);
+		if(r == -2)
+			return e(Efull);
+		return e(Eexist);
+	}
+
+	nds := name2de(t.name);
+	if(nds > 0) {
+		# long file name, find "new" short name
+		i := 1;
+		for(;;) {
+			sname = long2short(t.name, i);
+			(r1, tmpdp) := searchdir(f, sname, 0, 0);
+			if(r1 < 0)
+				break;
+			putsect(tmpdp.p);
+			i++;
+		}
+		islong = 1;
+	}
+
+	# allocate first cluster, if making directory
+	if(t.perm & Sys->DMDIR) {
+		bp = f.xf.ptr;
+		start = falloc(f.xf);
+		if(start <= 0) {
+			putfile(f);
+			return e(Efull);
+		}
+	}
+
+	 # now we're committed
+	if(pd != nil) {
+		puttime(pd);
+		pdp.p.flags |= BMOD;
+	}
+
+	f.ptr = ndp;
+	ndp.p = getsect(f.xf, ndp.addr);
+	if(ndp.p == nil ||
+	   islong && putlongname(f.xf, ndp, t.name, sname) < 0){
+		putsect(pdp.p);
+		if(ndp.p != nil)
+			putsect(ndp.p);
+		return e(Eio);
+	}
+
+	nd := ref Dosdir(".       ","   ",byte 0,array[10] of { * => byte 0},
+			array[2] of { * => byte 0}, array[2] of { * => byte 0},
+			array[2] of { * => byte 0},array[4] of { * => byte 0});
+
+	if((t.perm & 8r222) == 0)
+		nd.attr |= byte DRONLY;
+
+	puttime(nd);
+	nd.start[0] = byte start;
+	nd.start[1] = byte (start>>8);
+
+	if(islong)
+		putname(sname[0:8]+"."+sname[8:11], nd);
+	else
+		putname(t.name, nd);
+
+	f.qid.path = QIDPATH(ndp);
+	if(t.perm & Sys->DMDIR) {
+		nd.attr |= byte DDIR;
+		f.qid.qtype |= Sys->QTDIR;
+		xp := getsect(f.xf, bp.dataaddr+(start-2)*bp.clustsize);
+		if(xp == nil) {
+			if(ndp.p!=nil)
+				putfile(f);
+			putsect(pdp.p);
+			return e(Eio);
+		}
+		xd := ref *nd;
+		xd.name = ".       ";
+		xd.ext = "   ";
+		xp.iobuf[0:] = Dosdir.Dd2arr(xd);
+		if(pd!=nil)
+			xd = ref *pd;
+		else{
+			xd = ref Dosdir("..      ","   ",byte 0,
+				array[10] of { * => byte 0},
+				array[2] of { * => byte 0},
+				array[2] of { * => byte 0},
+				array[2] of { * => byte 0},
+				array[4] of { * => byte 0});
+
+			puttime(xd);
+			xd.attr = byte DDIR;
+		}
+		xd.name="..      ";
+		xd.ext="   ";
+		xp.iobuf[DOSDIRSIZE:] = Dosdir.Dd2arr(xd);
+		xp.flags |= BMOD;
+		putsect(xp);
+	}else
+		f.qid.qtype = Sys->QTFILE;
+
+	ndp.p.flags |= BMOD;
+	tmp := Dosdir.Dd2arr(nd);
+	ndp.p.iobuf[ndp.offset:]= tmp;
+	putfile(f);
+	putsect(pdp.p);
+
+	f.flags |= omode;
+	return ref Rmsg.Create(t.tag, f.qid, Styx->MAXFDATA);
+}
+
+rread(t: ref Tmsg.Read): ref Rmsg
+{
+	r: int;
+	data: array of byte;
+
+	if(((f:=xfile(t.fid, Asis))==nil) ||
+	    (f.flags&Oread == 0))
+		return e(Eio);
+
+	if((f.qid.qtype & Sys->QTDIR) != 0) {
+		if(getfile(f) < 0)
+			return e(Eio);
+		(r, data) = readdir(f, int t.offset, t.count);
+	} else {
+		if(getfile(f) < 0)
+			return e(Eio);
+		(r,data) = readfile(f, int t.offset, t.count);
+	}
+	putfile(f);
+
+	if(r < 0)
+		return e(Eio);
+	return ref Rmsg.Read(t.tag, data[0:r]);
+}
+
+rwrite(t: ref Tmsg.Write): ref Rmsg
+{
+	if(((f:=xfile(t.fid, Asis))==nil) ||
+	   !(f.flags&Owrite))
+		return e(Eio);
+	if(getfile(f) < 0)
+		return e(Eio);
+	r := writefile(f, t.data, int t.offset, len t.data);
+	putfile(f);
+	if(r < 0){
+		if(r == -2)
+			return e(Efull);
+		return e(Eio);
+	}
+	return ref Rmsg.Write(t.tag, r);
+}
+
+rclunk(t: ref Tmsg.Clunk): ref Rmsg
+{
+	xfile(t.fid, Clunk);
+	sync();
+	return ref Rmsg.Clunk(t.tag);
+}
+
+doremove(f: ref Xfs, dp: ref Dosptr)
+{
+	dp.p.iobuf[dp.offset] = byte DOSEMPTY;
+	dp.p.flags |= BMOD;
+	for(prevdo := dp.offset-DOSDIRSIZE; prevdo >= 0; prevdo-=DOSDIRSIZE){
+		if (dp.p.iobuf[prevdo+11] != byte DLONG)
+			break;
+		dp.p.iobuf[prevdo] = byte DOSEMPTY;
+	}
+
+	if (prevdo <= 0 && dp.prevaddr != -1){
+		p := getsect(f,dp.prevaddr);
+		for(prevdo = f.ptr.sectsize-DOSDIRSIZE; prevdo >= 0; prevdo-=DOSDIRSIZE) {
+			if(p.iobuf[prevdo+11] != byte DLONG)
+				break;
+			p.iobuf[prevdo] = byte DOSEMPTY;
+			p.flags |= BMOD;
+		}
+		putsect(p);
+	}
+}
+
+rremove(t: ref Tmsg.Remove): ref Rmsg
+{
+	f := xfile(t.fid, Asis);
+	if(f == nil)
+		return e(Enofid);
+
+	if(!f.ptr.addr) {
+		if(debug)
+			chat("root...");
+		xfile(t.fid, Clunk);
+		sync();
+		return e(Eperm);
+	}
+
+	# check on parent directory of file to be deleted
+	parp := getsect(f.xf, f.ptr.paddr);
+	if(parp == nil) {
+		xfile(t.fid, Clunk);
+		sync();
+		return e(Eio);
+	}
+
+	pard := Dosdir.arr2Dd(parp.iobuf[f.ptr.poffset:f.ptr.poffset+DOSDIRSIZE]);
+	if(f.ptr.paddr && (int pard.attr & DRONLY)) {
+		if(debug)
+			chat("parent read-only...");
+		putsect(parp);
+		xfile(t.fid, Clunk);
+		sync();
+		return e(Eperm);
+	}
+
+	if(getfile(f) < 0){
+		if(debug)
+			chat("getfile failed...");
+		putsect(parp);
+		xfile(t.fid, Clunk);
+		sync();
+		return e(Eio);
+	}
+
+	dattr := int f.ptr.p.iobuf[f.ptr.offset+11];
+	if(dattr & DDIR && emptydir(f) < 0){
+		if(debug)
+			chat("non-empty dir...");
+		putfile(f);
+		putsect(parp);
+		xfile(t.fid, Clunk);
+		sync();
+		return e(Eperm);
+	}
+	if(f.ptr.paddr == 0 && dattr&DRONLY) {
+		if(debug)
+			chat("read-only file in root directory...");
+		putfile(f);
+		putsect(parp);
+		xfile(t.fid, Clunk);
+		sync();
+		return e(Eperm);
+	}
+
+	doremove(f.xf, f.ptr);
+
+	if(f.ptr.paddr) {
+		puttime(pard);
+		parp.flags |= BMOD;
+	}
+
+	parp.iobuf[f.ptr.poffset:] = Dosdir.Dd2arr(pard);
+	putsect(parp);
+	err := 0;
+	if(truncfile(f) < 0)
+		err = Eio;
+
+	putfile(f);
+	xfile(t.fid, Clunk);
+	sync();
+	if(err)
+		return e(err);
+	return ref Rmsg.Remove(t.tag);
+}
+
+rstat(t: ref Tmsg.Stat): ref Rmsg
+{
+	f := xfile(t.fid, Asis);
+	if(f == nil)
+		return e(Enofid);
+	if(getfile(f) < 0)
+		return e(Eio);
+	dir := dostat(f);
+	putfile(f);
+	return ref Rmsg.Stat(t.tag, *dir);
+}
+
+dostat(f: ref Xfile): ref Sys->Dir
+{
+	islong :=0;
+	prevdo: int;
+	longnamebuf:="";
+
+	# get file info.
+	dir := getdir(f.ptr.p.iobuf[f.ptr.offset:f.ptr.offset+DOSDIRSIZE],
+					f.ptr.addr, f.ptr.offset);
+	# get previous entry
+	if(f.ptr.prevaddr == -1) {
+		# maybe extended, but will never cross sector boundary...
+		# short filename at beginning of sector..
+		if(f.ptr.offset!=0) {
+			for(prevdo = f.ptr.offset-DOSDIRSIZE; prevdo >=0; prevdo-=DOSDIRSIZE) {
+				prevdattr := f.ptr.p.iobuf[prevdo+11];
+				if(prevdattr != byte DLONG)
+					break;
+				islong = 1;
+				longnamebuf += getnamesect(f.ptr.p.iobuf[prevdo:prevdo+DOSDIRSIZE]);
+			}
+		}
+	} else {
+		# extended and will cross sector boundary.
+		for(prevdo = f.ptr.offset-DOSDIRSIZE; prevdo >=0; prevdo-=DOSDIRSIZE) {
+			prevdattr := f.ptr.p.iobuf[prevdo+11];
+			if(prevdattr != byte DLONG)
+				break;
+			islong = 1;
+			longnamebuf += getnamesect(f.ptr.p.iobuf[prevdo:prevdo+DOSDIRSIZE]);
+		}
+		if (prevdo < 0) {
+			p := getsect(f.xf,f.ptr.prevaddr);
+			for(prevdo = f.xf.ptr.sectsize-DOSDIRSIZE; prevdo >=0; prevdo-=DOSDIRSIZE){
+				prevdattr := p.iobuf[prevdo+11];
+				if(prevdattr != byte DLONG)
+					break;
+				islong = 1;
+				longnamebuf += getnamesect(p.iobuf[prevdo:prevdo+DOSDIRSIZE]);
+			}
+			putsect(p);
+		}
+	}
+	if(islong)
+		dir.name = longnamebuf;
+	return dir;
+}
+
+nameok(elem: string): int
+{
+	isfrog := array[256] of {
+	# NUL
+	1, 1, 1, 1, 1, 1, 1, 1,
+	# BKS
+	1, 1, 1, 1, 1, 1, 1, 1,
+	# DLE
+	1, 1, 1, 1, 1, 1, 1, 1,
+	# CAN
+	1, 1, 1, 1, 1, 1, 1, 1,
+#	' ' =>	1,
+	'/' =>	1, 16r7f =>	1, * => 0
+	};
+
+	for(i:=0; i < len elem; i++) {
+		if(isfrog[elem[i]])
+			return -1;
+	}
+	return 0;
+}
+
+rwstat(t: ref Tmsg.Wstat): ref Rmsg
+{
+	f := xfile(t.fid, Asis);
+	if(f == nil)
+		return e(Enofid);
+
+	if(getfile(f) < 0)
+		return e(Eio);
+
+	dp := f.ptr;
+
+	if(dp.addr == 0){	# root
+		putfile(f);
+		return e(Eperm);
+	}
+
+	changes := 0;
+	dir := dostat(f);
+	wdir := ref t.stat;
+
+	if(dir.uid != wdir.uid || dir.gid != wdir.gid){
+		putfile(f);
+		return e(Eperm);
+	}
+
+	if(dir.mtime != wdir.mtime || ((dir.mode^wdir.mode) & 8r777))
+		changes = 1;
+
+	if((wdir.mode & 7) != ((wdir.mode >> 3) & 7)
+	|| (wdir.mode & 7) != ((wdir.mode >> 6) & 7)){
+		putfile(f);
+		return e(Eperm);
+	}
+
+	if(dir.name != wdir.name){
+		# temporarily disable this
+		# g.errno = Eperm;
+		# putfile(f);
+		# return;
+
+		#
+		# grab parent directory of file to be changed and check for write perm
+		# rename also disallowed for read-only files in root directory
+		#
+		parp := getsect(f.xf, dp.paddr);
+		if(parp == nil){
+			putfile(f);
+			return e(Eio);
+		}
+		# pard := Dosdir.arr2Dd(parp.iobuf[dp.poffset: dp.poffset+DOSDIRSIZE]);
+		pardattr := int parp.iobuf[dp.poffset+11];
+		dpd := Dosdir.arr2Dd(dp.p.iobuf[dp.offset: dp.offset+DOSDIRSIZE]);
+		if(dp.paddr != 0 && int pardattr & DRONLY
+		|| dp.paddr == 0 && int dpd.attr & DRONLY){
+			putsect(parp);
+			putfile(f);
+			return e(Eperm);
+		}
+
+		#
+		# retrieve info from old entry
+		#
+		oaddr := dp.addr;
+		ooffset := dp.offset;
+		d := dpd;
+#		od := *d;
+		# start := getstart(f.xf, d);
+		start := d.start;
+		length := d.length;
+		attr := d.attr;
+
+		#
+		# temporarily release file to allow other directory ops:
+		# walk to parent, validate new name
+		# then remove old entry
+		#
+		putfile(f);
+		pf := ref *f;
+		pdp := ref Dosptr(dp.paddr, dp.poffset, 0, 0, 0, 0, -1, -1, parp, nil);
+		# if(pdp.addr != 0)
+		# 	pdpd := Dosdir.arr2Dd(parp.iobuf[pdp.offset: pdp.offset+DOSDIRSIZE]);
+		# else
+		# 	pdpd = nil;
+		pf.ptr = pdp;
+		if(wdir.name == "." || wdir.name == ".."){
+			putsect(parp);
+			return e(Eperm);
+		}
+		islong := 0;
+		sname := "";
+		nds := name2de(wdir.name);
+		if(nds > 0) {
+			# long file name, find "new" short name
+			i := 1;
+			for(;;) {
+				sname = long2short(wdir.name, i);
+				(r1, tmpdp) := searchdir(f, sname, 0, 0);
+				if(r1 < 0)
+					break;
+				putsect(tmpdp.p);
+				i++;
+			}
+			islong = 1;
+		}else{
+			(b, e) := dosname(wdir.name);
+			sname = b+e;
+		}
+		# (r, ndp) := searchdir(pf, wdir.name, 1, 1);
+		# if(r < 0){
+		#	putsect(parp);
+		#	g.errno = Eperm;
+		#	return;
+		# }
+		if(getfile(f) < 0){
+			putsect(parp);
+			return e(Eio);
+		}
+		doremove(f.xf, dp);
+		putfile(f);
+
+		#
+		# search for dir entry again, since we may be able to use the old slot,
+		# and we need to set up the naddr field if a long name spans the block.
+		# create new entry.
+		#
+		r := 0;
+		(r, dp) = searchdir(pf, sname, 1, islong);
+		if(r < 0){
+			putsect(parp);
+			return e(Ephase);
+		}
+		if((r = mkdentry(pf.xf, dp, wdir.name, sname, islong, attr, start, length)) != 0){
+			putsect(parp);
+			return e(r);
+		}
+		putsect(parp);
+
+		#
+		# relocate up other fids to the same file, if it moved
+		#
+		f.ptr = dp;
+		f.qid.path = QIDPATH(dp);
+		if(oaddr != dp.addr || ooffset != dp.offset)
+			dosptrreloc(f, dp, oaddr, ooffset);
+		changes = 1;
+		# f = nil;
+	}
+
+	if(changes){
+		d := Dosdir.arr2Dd(dp.p.iobuf[dp.offset:dp.offset+DOSDIRSIZE]);
+		putdir(d, wdir);
+		dp.p.iobuf[dp.offset: ] = Dosdir.Dd2arr(d);
+		dp.p.flags |= BMOD;
+	}
+	if(f != nil)
+		putfile(f);
+	sync();
+	return ref Rmsg.Wstat(t.tag);
+}
+
+#
+# FAT file system format
+#
+
+Dospart: adt {
+	active: byte;
+	hstart: byte;
+	cylstart: array of byte;
+	typ: byte;
+	hend: byte;
+	cylend: array of byte;
+	start: array of byte;
+	length: array of byte;
+};
+
+Dosboot: adt {
+	arr2Db:	fn(arr: array of byte): ref Dosboot;
+	magic:	array of byte;
+	version:	array of byte;
+	sectsize:	array of byte;
+	clustsize:	byte;
+	nresrv:	array of byte;
+	nfats:	byte;
+	rootsize:	array of byte;
+	volsize:	array of byte;
+	mediadesc:	byte;
+	fatsize:	array of byte;
+	trksize:	array of byte;
+	nheads:	array of byte;
+	nhidden:	array of byte;
+	bigvolsize:	array of byte;
+	driveno:	byte;
+	bootsig:	byte;
+	volid:	array of byte;
+	label:	array of byte;
+};
+
+Dosbpb: adt {
+	sectsize: int;	# in bytes 
+	clustsize: int;	# in sectors 
+	nresrv: int;	# sectors 
+	nfats: int;	# usually 2 
+	rootsize: int;	# number of entries 
+	volsize: int;	# in sectors 
+	mediadesc: int;
+	fatsize: int;	# in sectors 
+	fatclusters: int;
+	fatbits: int;	# 12 or 16 
+	fataddr: int; #big;	# sector number 
+	rootaddr: int; #big;
+	dataaddr: int; #big;
+	freeptr: int; #big;	# next free cluster candidate 
+};
+
+Dosdir: adt {
+	Dd2arr:	fn(d: ref Dosdir): array of byte;
+	arr2Dd:	fn(arr: array of byte): ref Dosdir;
+	name:	string;
+	ext:		string;
+	attr:		byte;
+	reserved:	array of byte;
+	time:		array of byte;
+	date:		array of byte;
+	start:		array of byte;
+	length:	array of byte;
+};
+
+Dosptr: adt {
+	addr:	int;	# of file's directory entry 
+	offset:	int;
+	paddr:	int;	# of parent's directory entry 
+	poffset:	int;
+	iclust:	int;	# ordinal within file 
+	clust:	int;
+	prevaddr:	int;
+	naddr:	int;
+	p:	ref Iosect;
+	d:	ref Dosdir;
+};
+
+Asis, Clean, Clunk: con iota;
+
+FAT12: con byte 16r01;
+FAT16: con byte 16r04;
+FATHUGE: con byte 16r06;
+DMDDO: con 16r54;
+DRONLY: con 16r01;
+DHIDDEN: con 16r02;
+DSYSTEM: con 16r04;
+DVLABEL: con 16r08;
+DDIR: con 16r10;
+DARCH: con 16r20;
+DLONG: con DRONLY | DHIDDEN | DSYSTEM | DVLABEL;
+DMLONG: con DLONG | DDIR | DARCH;
+
+DOSDIRSIZE: con 32;
+DOSEMPTY: con 16rE5;
+DOSRUNES: con 13;
+
+FATRESRV: con 2;
+
+Oread: con  1;
+Owrite: con  2;
+Orclose: con  4;
+Omodes: con  3;
+
+VERBOSE, STYX_MESS, FAT_INFO, CLUSTER_INFO: con (1 << iota);
+
+nowt, nowt1: int;
+tzoff: int;
+
+#
+# because we map all incoming short names from all upper to all lower case,
+# and FAT cannot store mixed case names in short name form,
+# we'll declare upper case as unacceptable to decide whether a long name
+# is needed on output.  thus, long names are always written in the case
+# in the system call, and are always read back as written; short names
+# are produced by the common case of writing all lower case letters
+#
+isdos := array[256] of {
+	'a' to 'z' => 1, 'A' to 'Z' => 0, '0' to '9' => 1,
+	' ' => 1, '$' => 1, '%' => 1, '"' => 1, '-' => 1, '_' => 1, '@' => 1,
+	'~' => 1, '`' => 1, '!' => 1, '(' => 1, ')' => 1, '{' => 1, '}' => 1, '^' => 1,
+	'#' => 1, '&' => 1,
+	* => 0
+};
+
+dossetup()
+{
+	nowt = daytime->now();
+	nowt1 = sys->millisec();
+	tzoff = daytime->local(0).tzoff;
+}
+
+# make xf into a Dos file system... or die trying to.
+dosfs(xf: ref Xfs): int
+{
+	mbroffset := 0;
+	i: int;
+	p: ref Iosect;
+
+Dmddo:
+	for(;;) {
+		for(i=2; i>0; i--) {
+			p = getsect(xf, 0);
+			if(p == nil)
+				return -1;
+
+			if((mbroffset == 0) && (p.iobuf[0] == byte 16re9))
+				break;
+			
+			# Check if the jump displacement (magic[1]) is too 
+			# short for a FAT. DOS 4.0 MBR has a displacement of 8.
+			if(p.iobuf[0] == byte 16reb &&
+			   p.iobuf[2] == byte 16r90 &&
+			   p.iobuf[1] != byte 16r08)
+				break;
+
+			if(i < 2 ||
+			   p.iobuf[16r1fe] != byte 16r55 ||
+			   p.iobuf[16r1ff] != byte 16raa) {
+				i = 0;
+				break;
+			}
+
+			dp := 16r1be;
+			for(j:=4; j>0; j--) {
+				if(debug) {
+					chat(sys->sprint("16r%2.2ux (%d,%d) 16r%2.2ux (%d,%d) %d %d...",
+					int p.iobuf[dp], int p.iobuf[dp+1], 
+					bytes2short(p.iobuf[dp+2: dp+4]),
+					int p.iobuf[dp+4], int p.iobuf[dp+5], 
+					bytes2short(p.iobuf[dp+6: dp+8]),
+					bytes2int(p.iobuf[dp+8: dp+12]), 
+					bytes2int(p.iobuf[dp+12:dp+16])));
+				}
+
+				# Check for a disc-manager partition in the MBR.
+				# Real MBR is at lba 63. Unfortunately it starts
+				# with 16rE9, hence the check above against magic.
+				if(int p.iobuf[dp+4] == DMDDO) {
+					mbroffset = 63*Sectorsize;
+					putsect(p);
+					purgebuf(xf);
+					xf.offset += mbroffset;
+					break Dmddo;
+				}
+				
+				# Make sure it really is the right type, other
+				# filesystems can look like a FAT
+				# (e.g. OS/2 BOOT MANAGER).
+				if(p.iobuf[dp+4] == FAT12 ||
+				   p.iobuf[dp+4] == FAT16 ||
+				   p.iobuf[dp+4] == FATHUGE)
+					break;
+				dp+=16;
+			}
+
+			if(j <= 0) {
+				if(debug)
+					chat("no active partition...");
+				putsect(p);
+				return -1;
+			}
+
+			offset := bytes2int(p.iobuf[dp+8:dp+12])* Sectorsize;
+			putsect(p);
+			purgebuf(xf);
+			xf.offset = mbroffset+offset;
+		}
+		break;
+	}
+	if(i <= 0) {
+		if(debug)
+			chat("bad magic...");
+		putsect(p);
+		return -1;
+	}
+
+	b := Dosboot.arr2Db(p.iobuf);
+	if(debug & FAT_INFO)
+		bootdump(b);
+
+	bp := ref Dosbpb;
+	xf.ptr = bp;
+	xf.fmt = 1;
+
+	bp.sectsize = bytes2short(b.sectsize);
+	bp.clustsize = int b.clustsize;
+	bp.nresrv = bytes2short(b.nresrv);
+	bp.nfats = int b.nfats;
+	bp.rootsize = bytes2short(b.rootsize);
+	bp.volsize = bytes2short(b.volsize);
+	if(bp.volsize == 0)
+		bp.volsize = bytes2int(b.bigvolsize);
+	bp.mediadesc = int b.mediadesc;
+	bp.fatsize = bytes2short(b.fatsize);
+
+	bp.fataddr = int bp.nresrv;
+	bp.rootaddr = bp.fataddr + bp.nfats*bp.fatsize;
+	i = bp.rootsize*DOSDIRSIZE + bp.sectsize-1;
+	i /= bp.sectsize;
+	bp.dataaddr = bp.rootaddr + i;
+	bp.fatclusters = FATRESRV+(bp.volsize - bp.dataaddr)/bp.clustsize;
+	if(bp.fatclusters < 4087)
+		bp.fatbits = 12;
+	else
+		bp.fatbits = 16;
+	bp.freeptr = 2;
+	if(debug & FAT_INFO){
+		chat(sys->sprint("fatbits=%d (%d clusters)...",
+			bp.fatbits, bp.fatclusters));
+		for(i=0; i< int b.nfats; i++)
+			chat(sys->sprint("fat %d: %d...",
+				i, bp.fataddr+i*bp.fatsize));
+		chat(sys->sprint("root: %d...", bp.rootaddr));
+		chat(sys->sprint("data: %d...", bp.dataaddr));
+	}
+	putsect(p);
+	return 0;
+}
+
+QIDPATH(dp: ref Dosptr): big
+{
+	return big (dp.addr*(Sectorsize/DOSDIRSIZE) + dp.offset/DOSDIRSIZE);
+}
+
+isroot(addr: int): int
+{
+	return addr == 0;
+}
+
+getfile(f: ref Xfile): int
+{
+	dp := f.ptr;
+	if(dp.p!=nil)
+		panic("getfile");
+	if(dp.addr < 0)
+		panic("getfile address");
+	p := getsect(f.xf, dp.addr);
+	if(p == nil)
+		return -1;
+
+	dp.d = nil;
+	if(!isroot(dp.addr)) {
+		if(f.qid.path != QIDPATH(dp)){
+			if(debug) {
+				chat(sys->sprint("qid mismatch f=0x%x d=0x%x...",
+					int f.qid.path, int QIDPATH(dp)));
+			}
+			putsect(p);
+			return -1;
+		}
+	#	dp.d = Dosdir.arr2Dd(p.iobuf[dp.offset:dp.offset+DOSDIRSIZE]);
+	}
+	dp.p = p;
+	return 0;
+}
+
+putfile(f: ref Xfile)
+{
+	dp := f.ptr;
+	if(dp.p==nil)
+		panic("putfile");
+	putsect(dp.p);
+	dp.p = nil;
+	dp.d = nil;
+}
+
+getstart(nil: ref Xfs, d: ref Dosdir): int
+{
+	start := bytes2short(d.start);
+#	if(xf.isfat32)
+#		start |= bytes2short(d.hstart)<<16;
+	return start;
+}
+
+putstart(nil: ref Xfs, d: ref Dosdir, start: int)
+{
+	d.start[0] = byte start;
+	d.start[1] = byte (start>>8);
+#	if(xf.isfat32){
+#		d.hstart[0] = start>>16;
+#		d.hstart[1] = start>>24;
+#	}
+}
+
+#
+# return the disk cluster for the iclust cluster in f
+#
+fileclust(f: ref Xfile, iclust: int, cflag: int): int
+{
+
+#	bp := f.xf.ptr;
+	dp := f.ptr;
+	if(isroot(dp.addr))
+		return -1;		# root directory for old FAT format does not start on a cluster boundary
+	d := dp.d;
+	if(d == nil){
+		if(dp.p == nil)
+			panic("fileclust");
+		d = Dosdir.arr2Dd(dp.p.iobuf[dp.offset:dp.offset+DOSDIRSIZE]);
+	}
+	next := 0;
+	start := getstart(f.xf, d);
+	if(start == 0) {
+		if(!cflag)
+			return -1;
+		start = falloc(f.xf);
+		if(start <= 0)
+			return -1;
+		puttime(d);
+		putstart(f.xf, d, start);
+		dp.p.iobuf[dp.offset:] = Dosdir.Dd2arr(d);
+		dp.p.flags |= BMOD;
+		dp.clust = 0;
+	}
+
+	clust, nskip: int;
+	if(dp.clust == 0 || iclust < dp.iclust) {
+		clust = start;
+		nskip = iclust;
+	} else {
+		clust = dp.clust;
+		nskip = iclust - dp.iclust;
+	}
+
+	if(debug & CLUSTER_INFO  && nskip > 0)
+		chat(sys->sprint("clust %d, skip %d...", clust, nskip));
+
+	if(clust <= 0)
+		return -1;
+
+	if(nskip > 0) {
+		while(--nskip >= 0) {
+			next = getfat(f.xf, clust);
+			if(debug & CLUSTER_INFO)
+				chat(sys->sprint(".%d", next));
+			if(next <= 0){
+				if(!cflag)
+					break;
+				next = falloc(f.xf);
+				if(next <= 0)
+					return -1;
+				putfat(f.xf, clust, next);
+			}
+			clust = next;
+		}
+		if(next <= 0)
+			return -1;
+		dp.clust = clust;
+		dp.iclust = iclust;
+	}
+	if(debug & CLUSTER_INFO)
+		chat(sys->sprint(" clust(%d)=0x%x...", iclust, clust));
+	return clust;
+}
+
+#
+# return the disk sector for the isect disk sector in f,
+# allocating space if necessary and cflag is set
+#
+fileaddr(f: ref Xfile, isect: int, cflag: int): int
+{
+	bp := f.xf.ptr;
+	dp := f.ptr;
+	if(isroot(dp.addr)) {
+		if(isect*bp.sectsize >= bp.rootsize*DOSDIRSIZE)
+			return -1;
+		return bp.rootaddr + isect;
+	}
+	clust := fileclust(f, isect/bp.clustsize, cflag);
+	if(clust < 0)
+		return -1;
+	return clust2sect(bp, clust) + isect%bp.clustsize;
+}
+
+#
+# look for a directory entry matching name
+# always searches for long names which match a short name
+#
+# if creating (cflag is set), set address of available slot and allocate next cluster if necessary
+#
+searchdir(f: ref Xfile, name: string, cflag: int, lflag: int): (int, ref Dosptr)
+{
+	xf := f.xf;
+	bp := xf.ptr;
+	addr1 := -1;
+	addr2 := -1;
+	prevaddr1 := -1;
+	o1 := 0;
+	dp :=  ref Dosptr(0,0,0,0,0,0,-1,-1,nil,nil);	# prevaddr and naddr are -1
+	dp.paddr = f.ptr.addr;
+	dp.poffset = f.ptr.offset;
+	islong :=0;
+	buf := "";
+
+	need := 1;
+	if(lflag && cflag)
+		need += name2de(name);
+	if(!lflag) {
+		name = name[0:8]+"."+name[8:11];
+		i := len name -1;
+		while(i >= 0 && (name[i]==' ' || name[i] == '.'))
+			i--;
+		name = name[0:i+1];
+	}
+
+	addr := -1;
+	prevaddr: int;
+	have := 0;
+	for(isect:=0;; isect++) {
+		prevaddr = addr;
+		addr = fileaddr(f, isect, cflag);
+		if(addr < 0)
+			break;
+		p := getsect(xf, addr);
+		if(p == nil)
+			break;
+		for(o:=0; o<bp.sectsize; o+=DOSDIRSIZE) {
+			dattr := int p.iobuf[o+11];
+			dname0 := p.iobuf[o];
+			if(dname0 == byte 16r00) {
+				if(debug)
+					chat("end dir(0)...");
+				putsect(p);
+				if(!cflag)
+					return (-1, nil);
+
+				#
+				# addr1 and o1 are the start of the dirs
+				# addr2 is the optional second cluster used if the long name
+				# entry does not fit within the addr1 cluster
+				# have tells us the number of contiguous free dirs
+				# starting at addr1.o1; need is the number needed to hold the long name
+				#
+				if(addr1 < 0){
+					addr1 = addr;
+					prevaddr1 = prevaddr;
+					o1 = o;
+				}
+				nleft := (bp.sectsize-o)/DOSDIRSIZE;
+				if(addr2 < 0 && nleft+have < need){
+					addr2 = fileaddr(f, isect+1, cflag);
+					if(addr2 < 0){
+						if(debug)
+							chat("end dir(2)...");
+						return (-2, nil);
+					}
+				}else if(addr2 < 0)
+					addr2 = addr;
+				if(addr2 == addr1)
+					addr2 = -1;
+				if(debug)
+					chat(sys->sprint("allocate addr1=%d,%d addr2=%d for %s nleft=%d have=%d need=%d", addr1, o1, addr2, name, nleft, have, need));
+				dp.addr = addr1;
+				dp.offset = o1;
+				dp.prevaddr = prevaddr1;
+				dp.naddr = addr2;
+				return (0, dp);
+			}
+
+			if(dname0 == byte DOSEMPTY) {
+				if(debug)
+					chat("empty...");
+				have++;
+				if(addr1 == -1){
+					addr1 = addr;
+					o1 = o;
+					prevaddr1 = prevaddr;
+				}
+				if(addr2 == -1 && have >= need)
+					addr2 = addr;
+				continue;
+			}
+			have = 0;
+			if(addr2 == -1)
+				addr1 = -1;
+
+			if(0 && lflag && debug)
+				dirdump(p.iobuf[o:o+DOSDIRSIZE],addr,o);
+
+			if((dattr & DMLONG) == DLONG) {
+				if(!islong)
+					buf = "";
+				islong = 1;
+				buf = getnamesect(p.iobuf[o:o+DOSDIRSIZE]) + buf;	# getnamesect should return sum
+				continue;
+			}
+			if(dattr & DVLABEL) {
+				islong = 0;
+				continue;
+			}
+
+			if(!islong || !lflag) 
+				buf = getname(p.iobuf[o:o+DOSDIRSIZE]);
+			islong = 0;
+
+			if(debug)
+				chat(sys->sprint("cmp: [%s] [%s]", buf, name));
+			if(mystrcmp(buf, name) != 0) {
+				buf="";
+				continue;
+			}
+			if(debug)
+				chat("found\n");
+
+			if(cflag) {
+				putsect(p);
+				return (-1,nil);
+			}
+
+			dp.addr = addr;
+			dp.prevaddr = prevaddr;
+			dp.offset = o;
+			dp.p = p;
+			#dp.d = Dosdir.arr2Dd(p.iobuf[o:o+DOSDIRSIZE]);
+			return (0, dp);
+		}
+		putsect(p);
+	}
+	if(debug)
+		chat("end dir(1)...");
+	if(!cflag)
+		return (-1, nil);
+	#
+	# end of root directory or end of non-root directory on cluster boundary
+	#
+	if(addr1 < 0){
+		addr1 = fileaddr(f, isect, 1);
+		if(addr1 < 0)
+			return (-2, nil);
+		prevaddr1 = prevaddr;
+		o1 = 0;
+	}else{
+		if(addr2 < 0 && have < need){
+			addr2 = fileaddr(f, isect, 1);
+			if(addr2 < 0)
+				return (-2, nil);
+		}
+	}
+	if(addr2 == addr1)
+		addr2 = -1;
+	dp.addr = addr1;
+	dp.offset = o1;
+	dp.prevaddr = prevaddr1;
+	dp.naddr = addr2;
+	return (0, dp);
+}
+
+emptydir(f: ref Xfile): int
+{
+	for(isect:=0;; isect++) {
+		addr := fileaddr(f, isect, 0);
+		if(addr < 0)
+			break;
+
+		p := getsect(f.xf, addr);
+		if(p == nil)
+			return -1;
+
+		for(o:=0; o<f.xf.ptr.sectsize; o+=DOSDIRSIZE) {
+			dname0 := p.iobuf[o];
+			dattr := int p.iobuf[o+11];
+
+			if(dname0 == byte 16r00) {
+				putsect(p);
+				return 0;
+			}
+
+			if(dname0 == byte DOSEMPTY || dname0 == byte '.')
+				continue;
+
+			if(dattr & DVLABEL)
+				continue;		# ignore any long name entries: it's empty if there are no short ones
+
+			putsect(p);
+			return -1;
+		}
+		putsect(p);
+	}
+	return 0;
+}
+
+readdir(f:ref Xfile, offset: int, count: int): (int, array of byte)
+{
+	xf := f.xf;
+	bp := xf.ptr;
+	rcnt := 0;
+	buf := array[Styx->MAXFDATA] of byte;
+	islong :=0;
+	longnamebuf:="";
+
+	if(count <= 0)
+		return (0, nil);
+
+Read:
+	for(isect:=0;; isect++) {
+		addr := fileaddr(f, isect, 0);
+		if(addr < 0)
+			break;
+		p := getsect(xf, addr);
+		if(p == nil)
+			return (-1,nil);
+
+		for(o:=0; o<bp.sectsize; o+=DOSDIRSIZE) {
+			dname0 := int p.iobuf[o];
+			dattr := int p.iobuf[o+11];
+
+			if(dname0 == 16r00) {
+				putsect(p);
+				break Read;
+			}
+
+			if(dname0 == DOSEMPTY)
+				continue;
+
+			if(dname0 == '.') {
+				dname1 := int p.iobuf[o+1];
+				if(dname1 == ' ' || dname1 == 0)
+					continue;
+				dname2 := int p.iobuf[o+2];
+				if(dname1 == '.' &&
+				  (dname2 == ' ' || dname2 == 0))
+					continue;
+			}
+
+			if((dattr & DMLONG) == DLONG) {
+				if(!islong)
+					longnamebuf = "";
+				longnamebuf = getnamesect(p.iobuf[o:o+DOSDIRSIZE]) + longnamebuf;
+				islong = 1;
+				continue;
+			}
+			if(dattr & DVLABEL) {
+				islong = 0;
+				continue;
+			}
+
+			dir := getdir(p.iobuf[o:o+DOSDIRSIZE], addr, o);
+			if(islong) {
+				dir.name = longnamebuf;
+				longnamebuf = "";
+				islong = 0;
+			}
+			d := styx->packdir(*dir);
+			if(offset > 0) {
+				offset -= len d;
+				islong = 0;
+				continue;
+			}
+			if(rcnt+len d > count){
+				putsect(p);
+				break Read;
+			}
+			buf[rcnt:] = d;
+			rcnt += len d;
+			if(rcnt >= count) {
+				putsect(p);
+				break Read;
+			}
+		}
+		putsect(p);
+	}
+
+	return (rcnt, buf[0:rcnt]);
+}
+
+walkup(f: ref Xfile): (int, ref Dosptr)
+{
+	bp := f.xf.ptr;
+	dp := f.ptr;
+	o: int;
+	ndp:= ref Dosptr(0,0,0,0,0,0,-1,-1,nil,nil);
+	ndp.addr = dp.paddr;
+	ndp.offset = dp.poffset;
+
+	if(debug)
+		chat(sys->sprint("walkup: paddr=0x%x...", dp.paddr));
+
+	if(dp.paddr == 0)
+		return (0,ndp);
+
+	p := getsect(f.xf, dp.paddr);
+	if(p == nil)  
+		return (-1,nil);
+
+	if(debug)
+		dirdump(p.iobuf[dp.poffset:dp.poffset+DOSDIRSIZE],dp.paddr,dp.poffset);
+
+	xd := Dosdir.arr2Dd(p.iobuf[dp.poffset:dp.poffset+DOSDIRSIZE]);
+	start := getstart(f.xf, xd);
+	if(debug & CLUSTER_INFO)
+		if(debug)
+			chat(sys->sprint("start=0x%x...", start));
+	putsect(p);
+	if(start == 0)
+		return (-1,nil);
+
+	#
+	# check that parent's . points to itself
+	#
+	p = getsect(f.xf, bp.dataaddr + (start-2)*bp.clustsize);
+	if(p == nil)
+		return (-1,nil);
+
+	if(debug)
+		dirdump(p.iobuf,0,0);
+
+	xd = Dosdir.arr2Dd(p.iobuf);
+	if(p.iobuf[0]!= byte '.' ||
+	   p.iobuf[1]!= byte ' ' ||
+	   start != getstart(f.xf, xd)) { 
+ 		if(p!=nil) 
+			putsect(p);
+		return (-1,nil);
+	}
+
+	if(debug)
+		dirdump(p.iobuf[DOSDIRSIZE:],0,0);
+
+	#
+	# parent's .. is the next entry, and has start of parent's parent
+	#
+	xd = Dosdir.arr2Dd(p.iobuf[DOSDIRSIZE:]);
+	if(p.iobuf[32] != byte '.' || p.iobuf[33] != byte '.') { 
+ 		if(p != nil) 
+			putsect(p);
+		return (-1,nil);
+	}
+
+	#
+	# we're done if parent is root
+	#
+	pstart := getstart(f.xf, xd);
+	putsect(p);
+	if(pstart == 0)
+		return (0, ndp);
+
+	#
+	# check that parent's . points to itself
+	#
+	p = getsect(f.xf, clust2sect(bp, pstart));
+	if(p == nil) {
+		if(debug)
+			chat(sys->sprint("getsect %d failed\n", pstart));
+		return (-1,nil);
+	}
+	if(debug)
+		dirdump(p.iobuf,0,0);
+	xd = Dosdir.arr2Dd(p.iobuf);
+	if(p.iobuf[0]!= byte '.' ||
+	   p.iobuf[1]!=byte ' ' || 
+	   pstart!=getstart(f.xf, xd)) { 
+ 		if(p != nil) 
+			putsect(p);
+		return (-1,nil);
+	}
+
+	#
+	# parent's parent's .. is the next entry, and has start of parent's parent's parent
+	#
+	if(debug)
+		dirdump(p.iobuf[DOSDIRSIZE:],0,0);
+
+	xd = Dosdir.arr2Dd(p.iobuf[DOSDIRSIZE:]);
+	if(xd.name[0] != '.' || xd.name[1] !=  '.') { 
+ 		if(p != nil) 
+			putsect(p);
+		return (-1,nil);
+	}
+	ppstart :=getstart(f.xf, xd);
+	putsect(p);
+
+	#
+	# open parent's parent's parent, and walk through it until parent's paretn is found
+	# need this to find parent's parent's addr and offset
+	#
+	ppclust := ppstart;
+	# TO DO: FAT32
+	if(ppclust != 0)
+		k := clust2sect(bp, ppclust);
+	else
+		k = bp.rootaddr;
+	p = getsect(f.xf, k);
+	if(p == nil) {
+		if(debug)
+			chat(sys->sprint("getsect %d failed\n", k));
+		return (-1,nil);
+	}
+
+	if(debug)
+		dirdump(p.iobuf,0,0);
+
+	if(ppstart) {
+		xd = Dosdir.arr2Dd(p.iobuf);
+		if(p.iobuf[0]!= byte '.' ||
+		   p.iobuf[1]!= byte ' ' || 
+		   ppstart!=getstart(f.xf, xd)) { 
+ 			if(p!=nil) 
+				putsect(p);
+			return (-1,nil);
+		}
+	}
+
+	for(so:=1; ;so++) {
+		for(o=0; o<bp.sectsize; o+=DOSDIRSIZE) {
+			xdname0 := p.iobuf[o];
+			if(xdname0 == byte 16r00) {
+				if(debug)
+					chat("end dir\n");
+ 				if(p != nil) 
+					putsect(p);
+				return (-1,nil);
+			}
+
+			if(xdname0 == byte DOSEMPTY)
+				continue;
+
+			#xd = Dosdir.arr2Dd(p.iobuf[o:o+DOSDIRSIZE]);
+			xdstart:= p.iobuf[o+26:o+28];	# TO DO: getstart
+			if(bytes2short(xdstart) == pstart) {
+				putsect(p);
+				ndp.paddr = k;
+				ndp.poffset = o;
+				return (0,ndp);
+			}
+		}
+		if(ppclust) {
+			if(so%bp.clustsize == 0) {
+				ppstart = getfat(f.xf, ppstart);
+				if(ppstart < 0){
+					if(debug)
+						chat(sys->sprint("getfat %d fail\n", 
+							ppstart));
+ 					if(p != nil) 
+						putsect(p);
+					return (-1,nil);
+				}
+			}
+			k = clust2sect(bp, ppclust) + 
+				so%bp.clustsize;
+		}
+		else {
+			if(so*bp.sectsize >= bp.rootsize*DOSDIRSIZE) { 
+ 				if(p != nil) 
+					putsect(p);
+				return (-1,nil);
+			}
+			k = bp.rootaddr + so;
+		}
+		putsect(p);
+		p = getsect(f.xf, k);
+		if(p == nil) {
+			if(debug)
+				chat(sys->sprint("getsect %d failed\n", k));
+			return (-1,nil);
+		}
+	}
+	putsect(p);
+	ndp.paddr = k;
+	ndp.poffset = o;
+	return (0,ndp);
+}
+
+readfile(f: ref Xfile, offset: int, count: int): (int, array of byte)
+{
+	xf := f.xf;
+	bp := xf.ptr;
+	dp := f.ptr;
+
+	length := bytes2int(dp.p.iobuf[dp.offset+28:dp.offset+32]);
+	rcnt := 0;
+	if(offset >= length)
+		return (0,nil);
+ 	buf := array[Styx->MAXFDATA] of byte;
+	if(offset+count >= length)
+		count = length - offset;
+	isect := offset/bp.sectsize;
+	o := offset%bp.sectsize;
+	while(count > 0) {
+		addr := fileaddr(f, isect++, 0);
+		if(addr < 0)
+			break;
+		c := bp.sectsize - o;
+		if(c > count)
+			c = count;
+		p := getsect(xf, addr);
+		if(p == nil)
+			return (-1, nil);
+		buf[rcnt:] = p.iobuf[o:o+c];
+		putsect(p);
+		count -= c;
+		rcnt += c;
+		o = 0;
+	}
+	return (rcnt, buf[0:rcnt]);
+}
+
+writefile(f: ref Xfile, buf: array of byte, offset,count: int): int
+{
+	xf := f.xf;
+	bp := xf.ptr;
+	dp := f.ptr;
+	addr := 0;
+	c: int;
+	rcnt := 0;
+	p: ref Iosect;
+
+	d := dp.d;
+	if(d == nil)
+		d = Dosdir.arr2Dd(dp.p.iobuf[dp.offset:dp.offset+DOSDIRSIZE]);
+	isect := offset/bp.sectsize;
+
+	o := offset%bp.sectsize;
+	while(count > 0) {
+		addr = fileaddr(f, isect++, 1);
+		if(addr < 0)
+			break;
+		c = bp.sectsize - o;
+		if(c > count)
+			c = count;
+		if(c == bp.sectsize){
+			p = getosect(xf, addr);
+			if(p == nil)
+				return -1;
+			p.flags = 0;
+		}else{
+			p = getsect(xf, addr);
+			if(p == nil)
+				return -1;
+		}
+		p.iobuf[o:] = buf[rcnt:rcnt+c];
+		p.flags |= BMOD;
+		putsect(p);
+		count -= c;
+		rcnt += c;
+		o = 0;
+	}
+	if(rcnt <= 0 && addr < 0)
+		return -2;
+	length := 0;
+	dlen := bytes2int(d.length);
+	if(rcnt > 0)
+		length = offset+rcnt;
+	else if(dp.addr && dp.clust) {
+		c = bp.clustsize*bp.sectsize;
+		if(dp.iclust > (dlen+c-1)/c)
+			length = c*dp.iclust;
+	}
+	if(length > dlen) {
+		d.length[0] = byte length;
+		d.length[1] = byte (length>>8);
+		d.length[2] = byte (length>>16);
+		d.length[3] = byte (length>>24);
+	}
+	puttime(d);
+	dp.p.flags |= BMOD;
+	dp.p.iobuf[dp.offset:] = Dosdir.Dd2arr(d);
+	return rcnt;
+}
+
+truncfile(f: ref Xfile): int
+{
+	xf := f.xf;
+#	bp := xf.ptr;
+	dp := f.ptr;
+	d := Dosdir.arr2Dd(dp.p.iobuf[dp.offset:dp.offset+DOSDIRSIZE]);
+
+	clust := getstart(f.xf, d);
+	putstart(f.xf, d, 0);
+	while(clust > 0) {
+		next := getfat(xf, clust);
+		putfat(xf, clust, 0);
+		clust = next;
+	}
+
+	d.length[0] = byte 0;
+	d.length[1] = byte 0;
+	d.length[2] = byte 0;
+	d.length[3] = byte 0;
+
+	dp.p.iobuf[dp.offset:] = Dosdir.Dd2arr(d);
+	dp.iclust = 0;
+	dp.clust = 0;
+	dp.p.flags |= BMOD;
+
+	return 0;
+}
+
+getdir(arr: array of byte, addr,offset: int) :ref Sys->Dir 
+{
+	dp := ref Sys->Dir;
+
+	if(arr == nil || addr == 0) {
+		dp.name = "";
+		dp.qid.path = big 0;
+		dp.qid.qtype = Sys->QTDIR;
+		dp.length = big 0;
+		dp.mode = Sys->DMDIR|8r777;
+	}
+	else {
+		dp.name = getname(arr);
+		for(i:=0; i < len dp.name; i++)
+			if(dp.name[i]>='A' && dp.name[i]<='Z')
+				dp.name[i] = dp.name[i]-'A'+'a';
+
+		# dp.qid.path = bytes2short(d.start); 
+		dp.qid.path = big (addr*(Sectorsize/DOSDIRSIZE) + offset/DOSDIRSIZE);
+		dattr := int arr[11];
+
+		if(dattr & DRONLY)
+			dp.mode = 8r444;
+		else
+			dp.mode = 8r666;
+
+		dp.atime = gtime(arr);
+		dp.mtime = dp.atime;
+		if(dattr & DDIR) {
+			dp.length = big 0;
+			dp.qid.qtype |= Styx->QTDIR;
+			dp.mode |= Sys->DMDIR|8r111;
+		}
+		else 
+			dp.length = big bytes2int(arr[28:32]);
+
+		if(dattr & DSYSTEM){
+			dp.mode |= Styx->DMEXCL;
+			dp.qid.qtype |= Styx->QTEXCL;
+		}
+	}
+
+	dp.qid.vers = 0;
+	dp.dtype = 0;
+	dp.dev = 0;
+	dp.uid = "dos";
+	dp.gid = "srv";
+
+	return dp;
+}
+
+putdir(d: ref Dosdir, dp: ref Sys->Dir)
+{
+	if(dp.mode & 2)
+		d.attr &= byte ~DRONLY;
+	else
+		d.attr |= byte DRONLY;
+
+	if(dp.mode & Styx->DMEXCL)
+		d.attr |= byte DSYSTEM;
+	else
+		d.attr &= byte ~DSYSTEM;
+	xputtime(d, dp.mtime);
+}
+
+getname(arr: array of byte): string
+{
+	p: string;
+	for(i:=0; i<8; i++) {
+		c := int arr[i];
+		if(c == 0 || c == ' ')
+			break;
+		if(i == 0 && c == 16r05)
+			c = 16re5;
+		p[len p] = c;
+	}
+	for(i=8; i<11; i++) {
+		c := int arr[i];
+		if(c == 0 || c == ' ')
+			break;
+		if(i == 8)
+			p[len p] = '.';
+		p[len p] = c;
+	}
+
+	return p;
+}
+
+dosname(p: string): (string, string)
+{
+	name := "        ";
+	for(i := 0; i < len p && i < 8; i++) {
+		c := p[i];
+		if(c >= 'a' && c <= 'z')
+			c += 'A'-'a';
+		else if(c == '.')
+			break;
+		name[i] = c;
+	}
+	ext := "   ";
+	for(j := len p - 1; j >= i; j--) {
+		if(p[j] == '.') {
+			q := 0;
+			for(j++; j < len p && q < 3; j++) {
+				c := p[j];
+				if(c >= 'a' && c <= 'z')
+					c += 'A'-'a';
+				ext[q++] = c;
+			}
+			break;
+		}
+	}
+	return (name, ext);
+}
+
+putname(p: string, d: ref Dosdir)
+{
+	if ((int d.attr & DLONG) == DLONG)
+		panic("putname of long name");
+	(d.name, d.ext) = dosname(p);
+}
+
+mystrcmp(s1, s2: string): int
+{
+	n := len s1;
+	if(n != len s2)
+		return 1;
+
+	for(i := 0; i < n; i++) {
+		c := s1[i];
+		if(c >= 'A' && c <= 'Z')
+			c -= 'A'-'a';
+		d := s2[i];
+		if(d >= 'A' && d <= 'Z')
+			d -= 'A'-'a';
+		if(c != d)
+			return 1;
+	}
+	return 0;
+}
+
+#
+# return the length of a long name in directory
+# entries or zero if it's normal dos
+#
+name2de(p: string): int
+{
+	ext := 0;
+	name := 0;
+
+	for(end := len p; --end >= 0 && p[end] != '.';)
+		ext++;
+
+	if(end > 0) {
+		name = end;
+		for(i := 0; i < end; i++) {
+			if(p[i] == '.')
+				return (len p+DOSRUNES-1)/DOSRUNES;
+		}
+	}
+	else {
+		name = ext;
+		ext = 0;
+	}
+
+	if(name <= 8 && ext <= 3 && isvalidname(p))
+		return 0;
+
+	return (len p+DOSRUNES-1)/DOSRUNES;
+}
+
+isvalidname(s: string): int
+{
+	dot := 0;
+	for(i := 0; i < len s; i++)
+		if(s[i] == '.') {
+			if(++dot > 1 || i == len s-1)
+				return 0;
+		} else if(s[i] > len isdos || isdos[s[i]] == 0)
+			return 0;
+	return 1;
+}
+
+getnamesect(arr: array of byte): string
+{
+	s: string;
+	c: int;
+
+	for(i := 1; i < 11; i += 2) {
+		c = int arr[i] | (int arr[i+1] << 8);
+		if(c == 0)
+			return s;
+		s[len s] = c;
+	}
+	for(i = 14; i < 26; i += 2) {
+		c = int arr[i] | (int arr[i+1] << 8);
+		if(c == 0)
+			return s;
+		s[len s] = c;
+	}
+	for(i = 28; i < 32; i += 2) {
+		c = int arr[i] | (int arr[i+1] << 8);
+		if(c == 0)
+			return s;
+		s[len s] = c;
+	}
+	return s;
+}
+
+# takes a long filename and converts to a short dos name, with a tag number.
+long2short(src: string,val: int): string
+{
+	dst :="           ";
+	skip:=0;
+	xskip:=0;
+	ext:=len src-1;
+	while(ext>=0 && src[ext]!='.')
+		ext--;
+
+	if (ext < 0)
+		ext=len src -1;
+
+	# convert name eliding periods 
+	j:=0;
+	for(name := 0; name < ext && j<8; name++){
+		c := src[name];
+		if(c!='.' && c!=' ' && c!='\t') {
+			if(c>='a' && c<='z')
+				dst[j++] = c-'a'+'A';
+			else
+				dst[j++] = c;
+		}	
+		else
+			skip++;
+	}
+
+	# convert extension 
+	j=8;
+	for(xname := ext+1; xname < len src && j<11; xname++) {
+		c := src[xname];
+		if(c!=' ' && c!='\t'){
+			if (c>='a' && c<='z')
+				dst[j++] = c-'a'+'A';
+			else
+				dst[j++] = c;
+		}else
+			xskip++;
+	}
+	
+	# add tag number
+	j =1; 
+	for(i:=val; i > 0; i/=10)
+		j++;
+
+	if (8-j<name) 
+		name = 8-j;
+	else
+		name -= skip;
+
+	dst[name]='~';
+	for(; val > 0; val /= 10)
+		dst[name+ --j] = (val%10)+'0';
+
+	if(debug)
+		chat(sys->sprint("returning dst [%s] src [%s]\n",dst,src));
+
+	return dst;			
+}
+
+getfat(xf: ref Xfs, n: int): int
+{
+	bp := xf.ptr;
+	k := 0; 
+
+	if(n < 2 || n >= bp.fatclusters)
+		return -1;
+	fb := bp.fatbits;
+	k = (fb*n) >> 3;
+	if(k < 0 || k >= bp.fatsize*bp.sectsize)
+		panic("getfat");
+
+	sect := k/bp.sectsize + bp.fataddr;
+	o := k%bp.sectsize;
+	p := getsect(xf, sect);
+	if(p == nil)
+		return -1;
+	k = int p.iobuf[o++];
+	if(o >= bp.sectsize) {
+		putsect(p);
+		p = getsect(xf, sect+1);
+		if(p == nil)
+			return -1;
+		o = 0;
+	}
+	k |= int p.iobuf[o++]<<8;
+	if(fb == 32){
+		# fat32 is really fat28
+		k |= int p.iobuf[o++] << 16;
+		k |= (int p.iobuf[o] & 16r0F) << 24;
+		fb = 28;
+	}
+	putsect(p);
+	if(fb == 12) {
+		if(n&1)
+			k >>= 4;
+		else
+			k &= 16rfff;
+	}
+
+	if(debug & FAT_INFO)
+		chat(sys->sprint("fat(0x%x)=0x%x...", n, k));
+
+	#
+	# check for out of range
+	#
+	if(k >= (1<<fb) - 8)
+		return -1;
+	return k;
+}
+
+putfat(xf: ref Xfs, n, val: int)
+{
+	bp := xf.ptr;
+	if(n < 2 || n >= bp.fatclusters)
+		panic(sys->sprint("putfat n=%d", n));
+	k := (bp.fatbits*n) >> 3;
+	if(k >= bp.fatsize*bp.sectsize)
+		panic("putfat");
+	sect := k/bp.sectsize + bp.fataddr;
+	for(; sect<bp.rootaddr; sect+=bp.fatsize) {
+		o := k%bp.sectsize;
+		p := getsect(xf, sect);
+		if(p == nil)
+			continue;
+		case bp.fatbits {
+		12 =>
+			if(n&1) {
+				p.iobuf[o] &= byte 16r0f;
+				p.iobuf[o++] |= byte (val<<4);
+				if(o >= bp.sectsize) {
+					p.flags |= BMOD;
+					putsect(p);
+					p = getsect(xf, sect+1);
+					if(p == nil)
+						continue;
+					o = 0;
+				}
+				p.iobuf[o] = byte (val>>4);
+			}
+			else {
+				p.iobuf[o++] = byte val;
+				if(o >= bp.sectsize) {
+					p.flags |= BMOD;
+					putsect(p);
+					p = getsect(xf, sect+1);
+					if(p == nil)
+						continue;
+					o = 0;
+				}
+				p.iobuf[o] &= byte 16rf0;
+				p.iobuf[o] |= byte ((val>>8)&16r0f);
+			}
+		16 =>
+			p.iobuf[o++] = byte val;
+			p.iobuf[o] = byte (val>>8);
+		32 =>	# fat32 is really fat28
+			p.iobuf[o++] = byte val;
+			p.iobuf[o++] = byte (val>>8);
+			p.iobuf[o++] = byte (val>>16);
+			p.iobuf[o] = byte ((int p.iobuf[o] & 16rF0) | ((val>>24) & 16r0F));
+		* =>
+			panic("putfat fatbits");
+		}
+
+		p.flags |= BMOD;
+		putsect(p);
+	}
+}
+
+falloc(xf: ref Xfs): int
+{
+	bp := xf.ptr;
+	n := bp.freeptr;
+	for(;;) {
+		if(getfat(xf, n) == 0)
+			break;
+		if(++n >= bp.fatclusters)
+			n = FATRESRV;
+		if(n == bp.freeptr)
+			return 0;
+	}
+	bp.freeptr = n+1;
+	if(bp.freeptr >= bp.fatclusters)
+		bp.freeptr = FATRESRV;
+	putfat(xf, n, int 16rffffffff);
+	k := clust2sect(bp, n);
+	for(i:=0; i<bp.clustsize; i++) {
+		p := getosect(xf, k+i);
+		if(p == nil)
+			return -1;
+		for(j:=0; j<len p.iobuf; j++)
+			p.iobuf[j] = byte 0;
+		p.flags = BMOD;
+		putsect(p);
+	}
+	return n;
+}
+
+clust2sect(bp: ref Dosbpb, clust: int): int
+{
+	return bp.dataaddr + (clust - FATRESRV)*bp.clustsize;
+}
+
+sect2clust(bp: ref Dosbpb, sect: int): int
+{
+	c := (sect - bp.dataaddr) / bp.clustsize + FATRESRV;
+	# assert(sect == clust2sect(bp, c));
+	return c;
+}
+
+bootdump(b: ref Dosboot)
+{
+	chat(sys->sprint("magic: 0x%2.2x 0x%2.2x 0x%2.2x\n",
+		int b.magic[0], int b.magic[1], int b.magic[2]));
+	chat(sys->sprint("version: \"%8.8s\"\n", string b.version));
+	chat(sys->sprint("sectsize: %d\n", bytes2short(b.sectsize)));
+	chat(sys->sprint("allocsize: %d\n", int b.clustsize));
+	chat(sys->sprint("nresrv: %d\n", bytes2short(b.nresrv)));
+	chat(sys->sprint("nfats: %d\n", int b.nfats));
+	chat(sys->sprint("rootsize: %d\n", bytes2short(b.rootsize)));
+	chat(sys->sprint("volsize: %d\n", bytes2short(b.volsize)));
+	chat(sys->sprint("mediadesc: 0x%2.2x\n", int b.mediadesc));
+	chat(sys->sprint("fatsize: %d\n", bytes2short(b.fatsize)));
+	chat(sys->sprint("trksize: %d\n", bytes2short(b.trksize)));
+	chat(sys->sprint("nheads: %d\n", bytes2short(b.nheads)));
+	chat(sys->sprint("nhidden: %d\n", bytes2int(b.nhidden)));
+	chat(sys->sprint("bigvolsize: %d\n", bytes2int(b.bigvolsize)));
+	chat(sys->sprint("driveno: %d\n", int b.driveno));
+	chat(sys->sprint("bootsig: 0x%2.2x\n", int b.bootsig));
+	chat(sys->sprint("volid: 0x%8.8x\n", bytes2int(b.volid)));
+	chat(sys->sprint("label: \"%11.11s\"\n", string b.label));
+}
+
+xputtime(d: ref Dosdir, s: int)
+{
+	if(s == 0)
+		t := daytime->local((sys->millisec() - nowt1)/1000 + nowt);
+	else
+		t = daytime->local(s);
+	x := (t.hour<<11) | (t.min<<5) | (t.sec>>1);
+	d.time[0] = byte x;
+	d.time[1] = byte (x>>8);
+	x = ((t.year-80)<<9) | ((t.mon+1)<<5) | t.mday;
+	d.date[0] = byte x;
+	d.date[1] = byte (x>>8);
+}
+
+puttime(d: ref Dosdir)
+{
+	xputtime(d, 0);
+}
+
+gtime(a: array of byte): int
+{
+	tm := ref Daytime->Tm;
+	i := bytes2short(a[22:24]);	# dos time
+	tm.hour = i >> 11;
+	tm.min = (i>>5) & 63;
+	tm.sec = (i & 31) << 1;
+	i = bytes2short(a[24:26]);	# dos date
+	tm.year = 80 + (i>>9);
+	tm.mon = ((i>>5) & 15) - 1;
+	tm.mday = i & 31;
+	tm.tzoff = tzoff;	# DOS time is local time
+	return daytime->tm2epoch(tm);
+}
+
+dirdump(arr: array of byte, addr, offset: int)
+{
+	if(!debug)
+		return;
+	attrchar:= "rhsvda67";
+	d := Dosdir.arr2Dd(arr);
+	buf := sys->sprint("\"%.8s.%.3s\" ", d.name, d.ext);
+	p_i:=7;
+
+	for(i := 16r80; i != 0; i >>= 1) {
+		if((d.attr & byte i) ==  byte i)
+			ch := attrchar[p_i];
+		else 
+			ch = '-'; 
+		buf += sys->sprint("%c", ch);
+		p_i--;
+	}
+
+	i = bytes2short(d.time);
+	buf += sys->sprint(" %2.2d:%2.2d:%2.2d", i>>11, (i>>5)&63, (i&31)<<1);
+	i = bytes2short(d.date);
+	buf += sys->sprint(" %2.2d.%2.2d.%2.2d", 80+(i>>9), (i>>5)&15, i&31);
+	buf += sys->sprint(" %d %d", bytes2short(d.start), bytes2short(d.length));
+	buf += sys->sprint(" %d %d\n",addr,offset);
+	chat(buf);
+}
+
+putnamesect(longname: string, curslot: int, first: int, sum: int, a: array of byte)
+{
+	for(i := 0; i < DOSDIRSIZE; i++)
+		a[i] = byte 16rFF;
+	if(first)
+		a[0] = byte (16r40 | curslot);
+	else 
+		a[0] = byte curslot;
+	a[11] = byte DLONG;
+	a[12] = byte 0;
+	a[13] = byte sum;
+	a[26] = byte 0;
+	a[27] = byte 0;
+	# a[1:1+10] = characters 1 to 5
+	n := len longname;
+	j := (curslot-1)*DOSRUNES;
+	for(i = 1; i < 1+10; i += 2){
+		c := 0;
+		if(j < n)
+			c = longname[j++];
+		a[i] = byte c;
+		a[i+1] = byte (c >> 8);
+		if(c == 0)
+			return;
+	}
+	# a[14:14+12] = characters 6 to 11
+	for(i = 14; i < 14+12; i += 2){
+		c := 0;
+		if(j < n)
+			c = longname[j++];
+		a[i] = byte c;
+		a[i+1] = byte (c >> 8);
+		if(c == 0)
+			return;
+	}
+	# a[28:28+4] characters 12 to 13
+	for(i = 28; i < 28+4; i += 2){
+		c := 0;
+		if(j < n)
+			c = longname[j++];
+		a[i] = byte c;
+		a[i+1] = byte (c>>8);
+		if(c == 0)
+			return;
+	}
+}
+
+putlongname(xf: ref Xfs, ndp: ref Dosptr, name: string, sname: string): int
+{
+	bp := xf.ptr;
+	first := 1;
+	sum := aliassum(sname);
+	for(nds := (len name+DOSRUNES-1)/DOSRUNES; nds > 0; nds--) {
+		putnamesect(name, nds, first, sum, ndp.p.iobuf[ndp.offset:]);
+		first = 0;
+		ndp.offset += DOSDIRSIZE;
+		if(ndp.offset == bp.sectsize) {
+			if(debug)
+				chat(sys->sprint("long name %s entry %d/%d crossing sector, addr=%d, naddr=%d", name, nds, (len name+DOSRUNES-1)/DOSRUNES, ndp.addr, ndp.naddr));
+			ndp.p.flags |= BMOD;
+			putsect(ndp.p);
+			ndp.p = nil;
+			ndp.d = nil;
+
+			# switch to the next cluster for the next long entry or the subsequent normal dir. entry
+			# naddr must be set up correctly by searchdir because we'll need one or the other
+
+			ndp.prevaddr = ndp.addr;
+			ndp.addr = ndp.naddr;
+			ndp.naddr = -1;
+			if(ndp.addr < 0)
+				return -1;
+			ndp.p = getsect(xf, ndp.addr);
+			if(ndp.p == nil)
+				return -1;
+			ndp.offset = 0;
+		}
+	}
+	return 0;
+}
+
+bytes2int(a: array of byte): int 
+{
+	return (((((int a[3] << 8) | int a[2]) << 8) | int a[1]) << 8) | int a[0];
+}
+
+bytes2short(a: array of byte): int 
+{
+	return (int a[1] << 8) | int a[0];
+}
+
+chat(s: string)
+{
+	if(debug)
+		sys->fprint(sys->fildes(2), "%s", s);
+}
+
+panic(s: string)
+{
+	sys->fprint(sys->fildes(2), "dosfs: panic: %s\n", s);
+	if(pflag)
+		<-chan of int;	# hang here
+	raise "fail:panic";
+}
+
+Dosboot.arr2Db(arr: array of byte): ref Dosboot
+{
+	db := ref Dosboot;
+	db.magic = arr[0:3];
+	db.version = arr[3:11];
+	db.sectsize = arr[11:13];
+	db.clustsize = arr[13];
+	db.nresrv = arr[14:16];
+	db.nfats = arr[16];
+	db.rootsize = arr[17:19];
+	db.volsize = arr[19:21];
+	db.mediadesc = arr[21];
+	db.fatsize = arr[22:24];
+	db.trksize = arr[24:26];
+	db.nheads = arr[26:28];
+	db.nhidden = arr[28:32];
+	db.bigvolsize = arr[32:36];
+	db.driveno = arr[36];
+	db.bootsig = arr[38];
+	db.volid = arr[39:43];
+	db.label = arr[43:54];
+	return db;
+}
+
+Dosdir.arr2Dd(arr: array of byte): ref Dosdir
+{
+	dir := ref Dosdir;
+	for(i := 0; i < 8; i++)
+		dir.name[len dir.name] = int arr[i];
+	for(; i < 11; i++)
+		dir.ext[len dir.ext] = int arr[i];
+	dir.attr = arr[11];
+	dir.reserved = arr[12:22];
+	dir.time = arr[22:24];
+	dir.date = arr[24:26];
+	dir.start = arr[26:28];
+	dir.length = arr[28:32];
+	return dir;
+}
+
+Dosdir.Dd2arr(d: ref Dosdir): array of byte
+{
+	a := array[32] of byte;
+	i:=0;
+	for(j := 0; j < len d.name; j++)
+		a[i++] = byte d.name[j];
+	for(; j<8; j++)
+		a[i++]= byte 0;
+	for(j=0; j<len d.ext; j++)
+		a[i++] = byte d.ext[j];
+	for(; j<3; j++)
+		a[i++]= byte 0;
+	a[i++] = d.attr;
+	for(j=0; j<10; j++)
+		a[i++] = d.reserved[j];
+	for(j=0; j<2; j++)
+		a[i++] = d.time[j];
+	for(j=0; j<2; j++)
+		a[i++] = d.date[j];
+	for(j=0; j<2; j++)
+		a[i++] = d.start[j];
+	for(j=0; j<4; j++)
+		a[i++] = d.length[j];
+	return a;
+}
+
+#
+# checksum of short name for use in long name directory entries
+# assumes sname is already padded correctly to 8+3
+#
+aliassum(sname: string): int
+{
+	i := 0;
+	for(sum:=0; i<11; i++)
+		sum = (((sum&1)<<7)|((sum&16rfe)>>1))+sname[i];
+	return sum;
+}
+
+#
+# track i/o
+#
+
+# An Xfs represents the root of an external file system, anchored
+# to the server and the client
+Xfs: adt {
+	next:cyclic ref Xfs;
+	name: string;	# of file containing external f.s. 
+	qid: Sys->Qid;	# of file containing external f.s. 
+	refn: int;		# attach count 
+	rootqid: Sys->Qid;	# of inferno constructed root directory 
+	dev: ref Sys->FD;  # FD of the file containing external f.s.
+	fmt: int;		# successfully read format
+	offset: int;		# offset in sectors to file system
+	ptr: ref Dosbpb;
+};
+
+# An Xfile represents the mapping of fid's & qid's to the server.
+Xfile: adt {
+	next: cyclic ref Xfile;		# in hash bucket 
+	client: int;
+	fid: int;
+	flags: int;
+	qid: Sys->Qid;
+	xf: ref Xfs;
+	ptr: ref Dosptr;
+};
+
+Iosect: adt
+{
+	next: cyclic ref Iosect;
+	flags: int;
+	t: cyclic ref Iotrack;
+	iobuf: array of byte;
+};
+
+Iotrack: adt 
+{
+	flags: int;
+	xf: ref Xfs;
+	addr: int;
+	next: cyclic ref Iotrack;		# in lru list 
+	prev: cyclic ref Iotrack;
+	hnext: cyclic ref Iotrack;		# in hash list 
+	hprev: cyclic ref Iotrack;
+	refn: int;
+	tp: cyclic ref Track;
+};
+
+Track: adt
+{
+	create: fn(): ref Track;
+	p: cyclic array of ref Iosect;
+	buf: array of byte;
+};
+
+BMOD: con	1<<0;
+BIMM: con	1<<1;
+BSTALE: con	1<<2;
+
+HIOB: con 31;	# a prime 
+NIOBUF: con 20;
+
+Sectorsize: con 512;
+Sect2trk: con 9;	# default
+
+hiob := array[HIOB+1] of ref Iotrack;		# hash buckets + lru list
+iobuf := array[NIOBUF] of ref Iotrack;		# the real ones
+freelist: ref Iosect;
+sect2trk := Sect2trk;
+trksize := Sect2trk*Sectorsize;
+
+FIDMOD: con 127;	# prime
+xhead:		ref Xfs;
+client:		int;
+
+xfiles := array[FIDMOD] of ref Xfile;
+iodebug := 0;
+
+iotrackinit(sectors: int)
+{
+	if(sectors <= 0)
+		sectors = 9;
+	sect2trk = sectors;
+	trksize = sect2trk*Sectorsize;
+
+	freelist = nil;
+
+	for(i := 0;i < FIDMOD; i++)
+		xfiles[i] = ref Xfile(nil,0,0,0,Sys->Qid(big 0,0,0),nil,nil);
+
+	for(i = 0; i <= HIOB; i++)
+		hiob[i] = ref Iotrack;
+
+	for(i = 0; i < HIOB; i++) {
+		hiob[i].hprev = hiob[i];
+		hiob[i].hnext = hiob[i];
+		hiob[i].refn = 0;
+		hiob[i].addr = 0;
+	}
+	hiob[i].prev = hiob[i];
+	hiob[i].next = hiob[i];
+	hiob[i].refn = 0;
+	hiob[i].addr = 0;
+
+	for(i=0;i<NIOBUF;i++)
+		iobuf[i] = ref Iotrack;
+
+	for(i=0; i<NIOBUF; i++) {
+		iobuf[i].hprev = iobuf[i].hnext = iobuf[i];
+		iobuf[i].prev = iobuf[i].next = iobuf[i];
+		iobuf[i].refn=iobuf[i].addr=0;
+		iobuf[i].flags = 0;
+		if(hiob[HIOB].next != iobuf[i]) {
+			iobuf[i].prev.next = iobuf[i].next;
+			iobuf[i].next.prev = iobuf[i].prev;
+			iobuf[i].next = hiob[HIOB].next;
+			iobuf[i].prev = hiob[HIOB];
+			hiob[HIOB].next.prev = iobuf[i];
+			hiob[HIOB].next = iobuf[i];
+		}
+		iobuf[i].tp =  Track.create();
+	}
+}
+
+Track.create(): ref Track
+{
+	t := ref Track;
+	t.p = array[sect2trk] of ref Iosect;
+	t.buf = array[trksize] of byte;
+	return t;
+}
+
+getsect(xf: ref Xfs, addr: int): ref Iosect
+{
+	return getiosect(xf, addr, 1);
+}
+
+getosect(xf: ref Xfs, addr: int): ref Iosect
+{
+	return getiosect(xf, addr, 0);
+}
+
+# get the sector corresponding to the address addr.
+getiosect(xf: ref Xfs, addr , rflag: int): ref Iosect
+{
+	# offset from beginning of track.
+	toff := addr %  sect2trk;
+
+	# address of beginning of track.
+	taddr := addr -  toff;
+	t := getiotrack(xf, taddr);
+
+	if(rflag && t.flags&BSTALE) {
+		if(tread(t) < 0)
+			return nil;
+
+		t.flags &= ~BSTALE;
+	}
+
+	t.refn++;
+	if(t.tp.p[toff] == nil) {
+		p := newsect();
+		t.tp.p[toff] = p;
+		p.flags = t.flags&BSTALE;
+		p.t = t;
+		p.iobuf = t.tp.buf[toff*Sectorsize:(toff+1)*Sectorsize];
+	}
+	return t.tp.p[toff];
+}
+
+putsect(p: ref Iosect)
+{
+	t: ref Iotrack;
+
+	t = p.t;
+	t.flags |= p.flags;
+	p.flags = 0;
+	t.refn--;
+	if(t.refn < 0)
+		panic("putsect: refcount");
+
+	if(t.flags & BIMM) {
+		if(t.flags & BMOD)
+			twrite(t);
+		t.flags &= ~(BMOD|BIMM);
+	}
+}
+
+# get the track corresponding to addr
+# (which is the address of the beginning of a track
+getiotrack(xf: ref Xfs, addr: int): ref Iotrack
+{
+	p: ref Iotrack;
+	mp := hiob[HIOB];
+	
+	if(iodebug)
+		chat(sys->sprint("iotrack %d,%d...", xf.dev.fd, addr));
+
+	# find bucket in hash table.
+	h := (xf.dev.fd<<24) ^ addr;
+	if(h < 0)
+		h = ~h;
+	h %= HIOB;
+	hp := hiob[h];
+
+	out: for(;;){
+		loop: for(;;) {
+		 	# look for it in the active list
+			for(p = hp.hnext; p != hp; p=p.hnext) {
+				if(p.addr != addr || p.xf != xf)
+					continue;
+				if(p.addr == addr && p.xf == xf) {
+					break out;
+				}
+				continue loop;
+			}
+		
+		 	# not found
+		 	# take oldest unref'd entry
+			for(p = mp.prev; p != mp; p=p.prev)
+				if(p.refn == 0 )
+					break;
+			if(p == mp) {
+				if(iodebug)
+					chat("iotrack all ref'd\n");
+				continue loop;
+			}
+
+			if((p.flags & BMOD)!= 0) {
+				twrite(p);
+				p.flags &= ~(BMOD|BIMM);
+				continue loop;
+			}
+			purgetrack(p);
+			p.addr = addr;
+			p.xf = xf;
+			p.flags = BSTALE;
+			break out;
+		}
+	}
+
+	if(hp.hnext != p) {
+		p.hprev.hnext = p.hnext;
+		p.hnext.hprev = p.hprev;			
+		p.hnext = hp.hnext;
+		p.hprev = hp;
+		hp.hnext.hprev = p;
+		hp.hnext = p;
+	}
+	if(mp.next != p) {
+		p.prev.next = p.next;
+		p.next.prev = p.prev;
+		p.next = mp.next;
+		p.prev = mp;
+		mp.next.prev = p;
+		mp.next = p;			
+	}
+	return p;
+}
+
+purgetrack(t: ref Iotrack)
+{
+	refn := sect2trk;
+	for(i := 0; i < sect2trk; i++) {
+		if(t.tp.p[i] == nil) {
+			--refn;
+			continue;
+		}
+		freesect(t.tp.p[i]);
+		--refn;
+		t.tp.p[i]=nil;
+	}
+	if(t.refn != refn)
+		panic("purgetrack");
+	if(refn!=0)
+		panic("refn not 0");
+}
+
+twrite(t: ref Iotrack): int
+{
+	if(iodebug)
+		chat(sys->sprint("[twrite %d...", t.addr));
+
+	if((t.flags & BSTALE)!= 0) {
+		refn:=0;
+		for(i:=0; i<sect2trk; i++)
+			if(t.tp.p[i]!=nil)
+				++refn;
+
+		if(refn < sect2trk) {
+			if(tread(t) < 0) {
+				if (iodebug)
+					chat("error]");
+				return -1;
+			}
+		}
+		else
+			t.flags &= ~BSTALE;
+	}
+
+	if(devwrite(t.xf, t.addr, t.tp.buf) < 0) {
+		if(iodebug)
+			chat("error]");
+		return -1;
+	}
+
+	if(iodebug)
+		chat(" done]");
+
+	return 0;
+}
+
+tread(t: ref Iotrack): int
+{
+	refn := 0;
+	rval: int;
+
+	for(i := 0; i < sect2trk; i++)
+		if(t.tp.p[i] != nil)
+			++refn;
+
+	if(iodebug)
+		chat(sys->sprint("[tread %d...", t.addr));
+
+	tbuf := t.tp.buf;
+	if(refn != 0)
+		tbuf = array[trksize] of byte;
+
+	rval = devread(t.xf, t.addr, tbuf);
+	if(rval < 0) {
+		if(iodebug)
+			chat("error]");
+		return -1;
+	}
+
+	if(refn != 0) {
+		for(i=0; i < sect2trk; i++) {
+			if(t.tp.p[i] == nil) {
+				t.tp.buf[i*Sectorsize:]=tbuf[i*Sectorsize:(i+1)*Sectorsize];
+				if(iodebug)
+					chat(sys->sprint("%d ", i));
+			}
+		}
+	}
+
+	if(iodebug)
+		chat("done]");
+
+	t.flags &= ~BSTALE;
+	return 0;
+}
+
+purgebuf(xf: ref Xfs)
+{
+	for(p := 0; p < NIOBUF; p++) {
+		if(iobuf[p].xf != xf)
+			continue;
+		if(iobuf[p].xf == xf) {
+			if((iobuf[p].flags & BMOD) != 0)
+				twrite(iobuf[p]);
+
+			iobuf[p].flags = BSTALE;
+			purgetrack(iobuf[p]);
+		}
+	}
+}
+
+sync()
+{
+	for(p := 0; p < NIOBUF; p++) {
+		if(!(iobuf[p].flags & BMOD))
+			continue;
+
+		if(iobuf[p].flags & BMOD){
+			twrite(iobuf[p]);
+			iobuf[p].flags &= ~(BMOD|BIMM);
+		}
+	}
+}
+
+
+newsect(): ref Iosect
+{
+	if((p := freelist)!=nil)	{
+		freelist = p.next;
+		p.next = nil;
+	} else
+		p = ref Iosect(nil, 0, nil,nil);
+
+	return p;
+}
+
+freesect(p: ref Iosect)
+{
+	p.next = freelist;
+	freelist = p;
+}
+
+
+# devio from here
+deverror(name: string, xf: ref Xfs, addr,n,nret: int): int
+{
+	if(nret < 0) {
+		if(iodebug)
+			chat(sys->sprint("%s errstr=\"%r\"...", name));
+		xf.dev = nil;
+		return -1;
+	}
+	if(iodebug)
+		chat(sys->sprint("dev %d sector %d, %s: %d, should be %d\n",
+			xf.dev.fd, addr, name, nret, n));
+
+	panic(name);
+	return -1;
+}
+
+devread(xf: ref Xfs, addr: int, buf: array of byte): int
+{
+	if(xf.dev==nil)
+		return -1;
+
+	sys->seek(xf.dev, big (xf.offset+addr*Sectorsize), sys->SEEKSTART);
+	nread := sys->read(xf.dev, buf, trksize);
+	if(nread != trksize)
+		return deverror("read", xf, addr, trksize, nread);
+
+	return 0;
+}
+
+devwrite(xf: ref Xfs, addr: int, buf: array of byte): int
+{
+	if(xf.dev == nil)
+		return -1;
+
+	sys->seek(xf.dev, big (xf.offset+addr*Sectorsize), 0);
+	nwrite := sys->write(xf.dev, buf, trksize);
+	if(nwrite != trksize)
+		return deverror("write", xf, addr, trksize , nwrite);
+
+	return 0;
+}
+
+devcheck(xf: ref Xfs): int
+{
+	buf := array[Sectorsize] of byte;
+
+	if(xf.dev == nil)
+		return -1;
+
+	sys->seek(xf.dev, big 0, sys->SEEKSTART);
+	if(sys->read(xf.dev, buf, Sectorsize) != Sectorsize){
+		xf.dev = nil;
+		return -1;
+	}
+
+	return 0;
+}
+
+# setup and return the Xfs associated with "name"
+
+getxfs(name: string): (ref Xfs, string)
+{
+	if(name == nil)
+		return (nil, "no file system device specified");
+
+	
+	 # If the name passed is of the form 'name:offset' then
+	 # offset is used to prime xf->offset. This allows accessing
+	 # a FAT-based filesystem anywhere within a partition.
+	 # Typical use would be to mount a filesystem in the presence
+	 # of a boot manager programm at the beginning of the disc.
+	
+	offset := 0;
+	for(i := 0;i < len name; i++)
+		if(name[i]==':')
+			break;
+
+	if(i < len name) {
+		offset = int name[i+1:];
+		if(offset < 0)
+			return (nil, "invalid device offset to file system");
+		offset *= Sectorsize;
+		name = name[0:i];
+	}
+
+	fd := sys->open(name, Sys->ORDWR);
+	if(fd == nil) {
+		if(iodebug)
+			chat(sys->sprint("getxfs: open(%s) failed: %r\n", name));
+		return (nil, sys->sprint("can't open %s: %r", name));
+	}
+
+	(rval,dir) := sys->fstat(fd);
+	if(rval < 0)
+		return (nil, sys->sprint("can't stat %s: %r", name));
+	
+	# lock down the list of xf's.
+	fxf: ref Xfs;
+	for(xf := xhead; xf != nil; xf = xf.next) {
+		if(xf.refn == 0) {
+			if(fxf == nil)
+				fxf = xf;
+			continue;
+		}
+		if(xf.qid.path != dir.qid.path || xf.qid.vers != dir.qid.vers)
+			continue;
+
+		if(xf.name!= name || xf.dev == nil)
+			continue;
+
+		if(devcheck(xf) < 0) # look for media change
+			continue;
+
+		if(offset && xf.offset != offset)
+			continue;
+
+		if(iodebug)
+			chat(sys->sprint("incref \"%s\", dev=%d...",
+				xf.name, xf.dev.fd));
+
+		++xf.refn;
+		return (xf, nil);
+	}
+	
+	# this xf doesn't exist, make a new one and stick it on the list.
+	if(fxf == nil){
+		fxf = ref Xfs;
+		fxf.next = xhead;
+		xhead = fxf;
+	}
+
+	if(iodebug)
+		chat(sys->sprint("alloc \"%s\", dev=%d...", name, fd.fd));
+
+	fxf.name = name;
+	fxf.refn = 1;
+	fxf.qid = dir.qid;
+	fxf.dev = fd;
+	fxf.fmt = 0;
+	fxf.offset = offset;
+	return (fxf, nil);
+}
+
+refxfs(xf: ref Xfs, delta: int)
+{
+	xf.refn += delta;
+	if(xf.refn == 0) {
+		if (iodebug)
+			chat(sys->sprint("free \"%s\", dev=%d...",
+				xf.name, xf.dev.fd));
+
+		purgebuf(xf);
+		if(xf.dev !=nil)
+			xf.dev = nil;
+	}
+}
+
+xfile(fid, flag: int): ref Xfile
+{
+	pf: ref Xfile;
+
+	# find hashed file list in LRU? table.
+	k := (fid^client)%FIDMOD;
+
+	# find if this fid is in the hashed file list.
+	f:=xfiles[k];
+	for(pf = nil; f != nil; f = f.next) {
+		if(f.fid == fid && f.client == client)
+			break;
+		pf=f;
+	}
+	
+	# move this fid to the front of the list if it was further down.
+	if(f != nil && pf != nil){
+		pf.next = f.next;
+		f.next = xfiles[k];
+		xfiles[k] = f;
+	}
+
+	case flag {
+	* =>
+		panic("xfile");
+	Asis =>
+		if(f != nil && f.xf != nil && f.xf.dev == nil)
+			return nil;
+		return f;
+	Clean =>
+		break;
+	Clunk =>
+		if(f != nil) {
+			xfiles[k] = f.next;
+			clean(f);
+		}
+		return nil;
+	}
+
+	# clean it up ..
+	if(f != nil)
+		return clean(f);
+
+	# f wasn't found in the hashtable, make a new one and add it
+	f = ref Xfile;
+	f.next = xfiles[k];
+	xfiles[k] = f;
+	# sort out the fid, etc.
+	f.fid = fid;
+	f.client = client;
+	f.flags = 0;
+	f.qid = Sys->Qid(big 0, 0, Styx->QTFILE);
+	f.xf = nil;
+	f.ptr = ref Dosptr(0,0,0,0,0,0,-1,-1,nil,nil);
+	return f;
+}
+
+clean(f: ref Xfile): ref Xfile
+{
+	f.ptr = nil;
+	if(f.xf != nil) {
+		refxfs(f.xf, -1);
+		f.xf = nil;
+	}
+	f.flags = 0;
+	f.qid = Sys->Qid(big 0, 0, 0);
+	return f;
+}
+
+#
+# the file at <addr, offset> has moved
+# relocate the dos entries of all fids in the same file
+#
+dosptrreloc(f: ref Xfile, dp: ref Dosptr, addr: int, offset: int)
+{
+	i: int;
+	p: ref Xfile;
+	xdp: ref Dosptr;
+
+	for(i=0; i < FIDMOD; i++){
+		for(p = xfiles[i]; p != nil; p = p.next){
+			xdp = p.ptr;
+			if(p != f && p.xf == f.xf
+			&& xdp != nil && xdp.addr == addr && xdp.offset == offset){
+				*xdp = *dp;
+				xdp.p = nil;
+				# xdp.d = nil;
+				p.qid.path = big QIDPATH(xdp);
+			}
+		}
+	}
+}
--- /dev/null
+++ b/appl/cmd/du.b
@@ -1,0 +1,163 @@
+implement Du;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "string.m";
+	strmod: String;
+include "readdir.m";
+	readdir: Readdir;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "arg.m";
+
+aflag := 0;	# all files, not just directories
+nflag := 0;	# names only (but see -t); implies -a
+sflag := 0;	# summary of top level names
+tflag := 0;	# use modification time, not size; netlib format if -n also given
+uflag := 0;	# use last use (access) time, not size
+blocksize := big 1024;	# quantise length to this block size (still displayed in kb)
+bout: ref Iobuf;
+
+Du: module
+{
+	init:	fn(nil: ref Draw->Context, arg: list of string);
+};
+
+kb(b: big): big
+{
+	return (((b + blocksize - big 1)/blocksize)*blocksize)/big 1024;
+}
+
+report(name: string, mtime: int, atime: int, l: big, chksum: int)
+{
+	t := mtime;
+	if(uflag)
+		t = atime;
+	if(nflag){
+		if(tflag)
+			bout.puts(sprint("%q %ud %bd %d\n", name, t, l, chksum));
+		else
+			bout.puts(sprint("%q\n", name));
+	}else{
+		if(tflag)
+			bout.puts(sprint("%ud %q\n", t, name));
+		else
+			bout.puts(sprint("%-4bd %q\n", kb(l), name));
+	}
+}
+
+# Avoid loops in tangled namespaces.
+NCACHE: con 1024; # must be power of two
+cache := array[NCACHE] of list of ref sys->Dir;
+
+seen(dir: ref sys->Dir): int
+{
+	h := int dir.qid.path & (NCACHE-1);
+	for(c := cache[h]; c!=nil; c = tl c){
+		t := hd c;
+		if(dir.qid.path==t.qid.path && dir.dtype==t.dtype && dir.dev==t.dev)
+			return 1;
+	}
+	cache[h] = dir :: cache[h];
+	return 0;
+}
+
+dir(dirname: string): big
+{
+	prefix := dirname+"/";
+	if(dirname==".")
+		prefix = nil;
+	sum := big 0;
+	(de, nde) := readdir->init(dirname, readdir->NAME);
+	if(nde < 0)
+		warn("can't read", dirname);
+	for(i := 0; i < nde; i++) {
+		s := prefix+de[i].name;
+		if(de[i].mode & Sys->DMDIR){
+			if(!seen(de[i])){	# arguably should apply to files as well
+				size := dir(s);
+				sum += size;
+				if(!sflag && !nflag)
+					report(s, de[i].mtime, de[i].atime, size, 0);
+			}
+		}else{
+			l := de[i].length;
+			sum += l;
+			if(aflag)
+				report(s, de[i].mtime, de[i].atime, l, 0);
+		}
+	}
+	return sum;
+}
+
+du(name: string)
+{
+	(rc, d) := sys->stat(name);
+	if(rc < 0){
+		warn("can't stat", name);
+		return;
+	}
+	if(d.mode & Sys->DMDIR){
+		d.length = dir(name);
+		if(nflag && !sflag)
+			return;
+	}
+	report(name, d.mtime, d.atime, d.length, 0);
+}
+
+warn(why: string, f: string)
+{
+	sys->fprint(sys->fildes(2), "du: %s %q: %r\n", why, f);
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	strmod = load String String->PATH;
+	readdir = load Readdir Readdir->PATH;
+	arg := load Arg Arg->PATH;
+	if(arg == nil || bufio==nil || arg==nil || readdir==nil || readdir==nil){
+		sys->fprint(sys->fildes(2), "du: load Error: %r\n");
+		raise "fail:can't load";
+	}
+	sys->pctl(Sys->FORKFD, nil);
+	bout = bufio->fopen(sys->fildes(1), bufio->OWRITE);
+	arg->init(args);
+	arg->setusage("du [-anstu] [-b bsize] [file ...]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>
+			aflag = 1;
+		'b' =>
+			s := arg->earg();
+			blocksize = big s;
+			if(len s > 0 && s[len s-1] == 'k')
+				blocksize *= big 1024;
+			if(blocksize <= big 0)
+				blocksize = big 1;
+		'n' =>
+			nflag = 1;
+			aflag = 1;
+		's' =>
+			sflag = 1;
+		't' =>
+			tflag = 1;
+		'u' =>
+			uflag = 1;
+			tflag = 1;
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(args==nil)
+		args = "." :: nil;
+	for(; args!=nil; args = tl args)
+		du(hd args);
+	bout.close();
+}
--- /dev/null
+++ b/appl/cmd/echo.b
@@ -1,0 +1,36 @@
+implement Echo;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+Echo: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if(args != nil)
+		args = tl args;
+	addnl := 1;
+	if(args != nil && (hd args == "-n" || hd args == "--")) {
+		if(hd args == "-n")
+			addnl = 0;
+		args = tl args;
+	}
+	s := "";
+	if(args != nil) {
+		s = hd args;
+		while((args = tl args) != nil)
+			s += " " + hd args;
+	}
+	if(addnl)
+		s[len s] = '\n';
+	a := array of byte s;
+	if(sys->write(sys->fildes(1), a, len a) < 0){
+		sys->fprint(sys->fildes(2), "echo: write error: %r\n");
+		raise "fail:write error";
+	}
+}
--- /dev/null
+++ b/appl/cmd/ed.b
@@ -1,0 +1,1588 @@
+#
+# Editor
+#
+
+implement Editor;
+
+include "sys.m";
+   sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "regex.m";
+	regex: Regex;
+	Re: import regex;
+include "sh.m";
+	sh: Sh;
+
+Editor: module {
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+FNSIZE: con 128;		# file name 
+LBSIZE: con 4096;		# max line size 
+BLKSIZE: con 4096;		# block size in temp file 
+NBLK: con 8191;		# max size of temp file 
+ESIZE: con 256;			# max size of reg exp 
+GBSIZE: con 256;		# max size of global command 
+MAXSUB: con 9;		# max number of sub reg exp 
+ESCFLG: con 16rFFFF;	# escape Rune - user defined code 
+EOF: con -1;
+BytesPerRune: con 2;
+RunesPerBlock: con BLKSIZE / BytesPerRune;
+
+APPEND_GETTTY, APPEND_GETSUB, APPEND_GETCOPY, APPEND_GETFILE: con iota;
+
+Subexp: adt {
+	rsp, rep: int;
+};
+
+Globp: adt {
+	s: string;
+	isnil: int;
+};
+
+addr1: int;
+addr2: int;
+anymarks: int;
+col: int;
+count: int;
+dol: int;
+dot: int;
+fchange: int;
+file: string;
+genbuf := array[LBSIZE] of int;
+given: int;
+globp: Globp;
+iblock: int;
+ichanged: int;
+io: ref Sys->FD;
+iobuf: ref Iobuf;
+lastc: int;
+line := array [70] of byte;
+linebp := -1;
+linebuf := array [LBSIZE] of int;
+listf: int;
+listn: int;
+loc1: int;
+loc2: int;
+names := array [26] of int;
+oblock: int;
+oflag: int;
+pattern: Re;
+peekc: int;
+pflag: int;
+rescuing: int;
+rhsbuf := array [LBSIZE/2] of int;
+savedfile: string;
+subnewa: int;
+subolda: int;
+subexp: array of Subexp;
+tfname: string;
+tline: int;
+waiting: int;
+wrapp: int;
+zero: array of int;
+drawctxt: ref Draw->Context;
+
+Q: con "";
+T: con "TMP";
+WRERR: con "WRITE ERROR";
+bpagesize := 20;
+hex: con "0123456789abcdef";
+linp: int;
+nlall := 128;
+tfile: ref Sys->FD;
+vflag := 1;
+
+debug(s: string)
+{
+	sys->print("%s", s);
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	drawctxt = ctxt;
+
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->fprint(sys->fildes(2), "can't load %s\n", Bufio->PATH);
+		return;
+	}
+	regex = load Regex Regex->PATH;
+	if (regex == nil) {
+		sys->fprint(sys->fildes(2), "can't load %s\n", Regex->PATH);
+		return;
+	}
+
+#	notify(notifyf);
+
+	if (args != nil)
+		args = tl args;
+
+	if (args != nil && hd args == "-o") {
+		oflag = 1;
+		vflag = 0;
+		args = tl args;
+	}
+
+	if (args != nil && hd args == "-") {
+		vflag = 0;
+		args = tl args;
+	}
+
+	if (oflag) {
+		savedfile = "/fd/1";
+		globp = ("a", 0);
+	} else if (args != nil) {
+		savedfile = hd args;
+		globp = ("r", 0);
+	}
+	else
+		globp = (nil, 1);
+	zero = array [nlall + 5] of int;
+	tfname = mktemp("/tmp/eXXXXX");
+#	debug(sys->sprint("tfname %s\n", tfname));
+	_init();
+	for(;;){
+		{
+			commands();
+			quit();
+		}exception{
+		"savej" =>
+			;
+		}
+	}
+}
+
+casee(c: int)
+{
+	setnoaddr();
+	if(vflag && fchange) {
+		fchange = 0;
+		error(Q);
+	}
+	filename(c);
+	_init();
+	addr2 = 0;
+	caseread();
+}
+
+casep()
+{
+	newline();
+	printcom();
+}
+
+caseq()
+{
+	setnoaddr();
+	newline();
+	quit();
+}
+
+caseread()
+{
+#debug("caseread " + file);
+	if((io=sys->open(file, Sys->OREAD)) == nil) {
+		lastc = '\n';
+		error(file);
+	}
+	iobuf = bufio->fopen(io, Sys->OREAD);
+	setwide();
+	squeeze(0);
+	c := 0 != dol;
+	append(APPEND_GETFILE, addr2);
+	exfile(Sys->OREAD);
+
+	fchange = c;
+}
+
+commands()
+{
+	a1: int;
+	c, temp: int;
+	lastsep: int;
+
+	for(;;) {
+		if(pflag) {
+			pflag = 0;
+			addr1 = addr2 = dot;
+			printcom();
+		}
+		c = '\n';
+		for(addr1 = -1;;) {
+			lastsep = c;
+			a1 = address();
+			c = getchr();
+			if(c != ',' && c != ';')
+				break;
+			if(lastsep == ',')
+				error(Q);
+			if(a1 < 0) {
+				a1 = 1;
+				if(a1 > dol)
+					a1--;
+			}
+			addr1 = a1;
+			if(c == ';')
+				dot = a1;
+		}
+		if(lastsep != '\n' && a1 < 0)
+			a1 = dol;
+		if((addr2=a1) < 0) {
+			given = 0;
+			addr2 = dot;	
+		} else
+			given = 1;
+		if(addr1 < 0)
+			addr1 = addr2;
+#debug(sys->sprint("%d,%d %c\n", addr1, addr2, c));
+		case c {
+		'a' =>
+			add(0);
+			continue;
+
+		'b' =>
+			nonzero();
+			browse();
+			continue;
+
+		'c' =>
+			nonzero();
+			newline();
+			rdelete(addr1, addr2);
+			append(APPEND_GETTTY, addr1-1);
+			continue;
+
+		'd' =>
+			nonzero();
+			newline();
+			rdelete(addr1, addr2);
+			continue;
+
+		'E' =>
+			fchange = 0;
+			c = 'e';
+			casee(c);
+			continue;
+
+		'e' =>
+			casee(c);
+			continue;
+
+		'f' =>
+			setnoaddr();
+			filename(c);
+			putst(savedfile);
+			continue;
+
+		'g' =>
+			global(1);
+			continue;
+
+		'i' =>
+			add(-1);
+			continue;
+
+		'j' =>
+			if(!given)
+				addr2++;
+			newline();
+			join();
+			continue;
+
+		'k' =>
+			nonzero();
+			c = getchr();
+			if(c < 'a' || c > 'z')
+				error(Q);
+			newline();
+			names[c-'a'] = zero[addr2] & ~16r1;
+			anymarks |= 16r1;
+			continue;
+
+		'm' =>
+			move(0);
+			continue;
+
+		'n' =>
+			listn++;
+			newline();
+			printcom();
+			continue;
+
+		'\n' =>
+			if(a1 < 0) {
+				a1 = dot+1;
+				addr2 = a1;
+				addr1 = a1;
+			}
+			if(lastsep==';')
+				addr1 = a1;
+			printcom();
+			continue;
+
+		'l' =>
+			listf++;
+			casep();
+			continue;
+
+		'p' or 'P' =>
+			casep();
+			continue;
+
+		'Q' =>
+			fchange = 0;
+			caseq();
+			continue;
+
+		'q' =>
+			caseq();
+			continue;
+
+		'r' =>
+			filename(c);
+			caseread();
+			continue;
+
+		's' =>
+			nonzero();
+			substitute(!globp.isnil);
+			continue;
+
+		't' =>
+			move(1);
+			continue;
+
+		'u' =>
+			nonzero();
+			newline();
+			if((zero[addr2]&~8r01) != subnewa)
+				error(Q);
+			zero[addr2] = subolda;
+			dot = addr2;
+			continue;
+
+		'v' =>
+			global(0);
+			continue;
+
+		'W' or 'w' =>
+			if (c == 'W')
+				wrapp++;
+			setwide();
+			squeeze(dol>0);
+			temp = getchr();
+			if(temp != 'q' && temp != 'Q') {
+				peekc = temp;
+				temp = 0;
+			}
+			filename(c);
+			if(!wrapp ||
+			  ((io = sys->open(file, Sys->OWRITE)) == nil) ||
+			  ((sys->seek(io, big 0, Sys->SEEKEND)) < big 0))
+				if((io = sys->create(file, Sys->OWRITE, 8r0666)) == nil)
+					error(file);
+			iobuf = bufio->fopen(io, Sys->OWRITE);
+			wrapp = 0;
+			if(dol > 0)
+				putfile();
+			exfile(Sys->OWRITE);
+			if(addr1<=1 && addr2==dol)
+				fchange = 0;
+			if(temp == 'Q')
+				fchange = 0;
+			if(temp)
+				quit();
+			continue;
+
+		'=' =>
+			setwide();
+			squeeze(0);
+			newline();
+			count = addr2 - 0;
+			putd();
+			putchr('\n');
+			continue;
+
+		'!' =>
+			callunix();
+			continue;
+
+		EOF =>
+			return;
+
+		}
+		error(Q);
+	}
+}
+
+printcom()
+{
+	a1: int;
+
+	nonzero();
+	a1 = addr1;
+	do {
+		if(listn) {
+			count = a1-0;
+			putd();
+			putchr('\t');
+		}
+		putshst(getline(zero[a1++]));
+	} while(a1 <= addr2);
+	dot = addr2;
+	listf = 0;
+	listn = 0;
+	pflag = 0;
+}
+
+
+address(): int
+{
+	sign, a, opcnt, nextopand, b, c: int;
+
+	nextopand = -1;
+	sign = 1;
+	opcnt = 0;
+	a = dot;
+	do {
+		do {
+			c = getchr();
+		} while(c == ' ' || c == '\t');
+		if(c >= '0' && c <= '9') {
+			peekc = c;
+			if(!opcnt)
+				a = 0;
+			a += sign*getnum();
+		} else
+		case c {
+		'$' or '.' =>
+			if (c == '$')
+				a = dol;
+			if(opcnt)
+				error(Q);
+
+		'\'' =>
+			c = getchr();
+			if(opcnt || c < 'a' || c > 'z')
+				error(Q);
+			a = 0;
+			do {
+				a++;
+			} while(a <= dol && names[c-'a'] != (zero[a] & ~8r01));
+
+		'?' or '/' =>
+			if (c == '?')
+				sign = -sign;
+			compile(c);
+			b = a;
+			for(;;) {
+				a += sign;
+				if(a <= 0)
+					a = dol;
+				if(a > dol)
+					a = 0;
+				if(match(a))
+					break;
+				if(a == b)
+					error(Q);
+			}
+			break;
+
+		* =>
+			if(nextopand == opcnt) {
+				a += sign;
+				if(a < 0 || dol < a)
+					continue;       # error(Q); 
+			}
+			if(c != '+' && c != '-' && c != '^') {
+				peekc = c;
+				if(opcnt == 0)
+					a = -1;
+				return a;
+			}
+			sign = 1;
+			if(c != '+')
+				sign = -sign;
+			nextopand = ++opcnt;
+			continue;
+		}
+		sign = 1;
+		opcnt++;
+	} while(0 <= a && a <= dol);
+	error(Q);
+	return -1;
+}
+
+getnum(): int
+{
+	r, c: int;
+
+	r = 0;
+	for(;;) {
+		c = getchr();
+		if(c < '0' || c > '9')
+			break;
+		r = r*10 + (c-'0');
+	}
+	peekc = c;
+	return r;
+}
+
+setwide()
+{
+	if(!given) {
+		addr1 = 0 + (dol>0);
+		addr2 = dol;
+	}
+}
+
+setnoaddr()
+{
+	if(given)
+		error(Q);
+}
+
+nonzero()
+{
+	squeeze(1);
+}
+
+squeeze(i: int)
+{
+	if(addr1 < 0+i || addr2 > dol || addr1 > addr2)
+		error(Q);
+}
+
+newline()
+{
+	c: int;
+
+	c = getchr();
+	if(c == '\n' || c == EOF)
+		return;
+	if(c == 'p' || c == 'l' || c == 'n') {
+		pflag++;
+		if(c == 'l')
+			listf++;
+		else
+		if(c == 'n')
+			listn++;
+		c = getchr();
+		if(c == '\n')
+			return;
+	}
+	error(Q);
+}
+
+filename(comm: int)
+{
+	rune: int;
+	c: int;
+
+	count = 0;
+	c = getchr();
+	if(c == '\n' || c == EOF) {
+		if(savedfile == nil && comm != 'f')
+			error(Q);
+		file = savedfile;
+		return;
+	}
+	if(c != ' ')
+		error(Q);
+	while((c=getchr()) == ' ')
+		;
+	if(c == '\n')
+		error(Q);
+	file = nil;
+	do {
+		if(c == ' ' || c == EOF)
+			error(Q);
+		rune = c;
+		file[len file] = c;
+	} while((c=getchr()) != '\n');
+	if(savedfile == nil || comm == 'e' || comm == 'f')
+		savedfile = file;
+}
+
+exfile(om: int)
+{
+
+	if(om == Sys->OWRITE)
+		if(iobuf.flush() < 0)
+			error(Q);
+	iobuf.close();
+	iobuf = nil;
+	io = nil;
+	if(vflag) {
+		putd();
+		putchr('\n');
+	}
+}
+
+error1(s: string)
+{
+	c: int;
+
+	wrapp = 0;
+	listf = 0;
+	listn = 0;
+	count = 0;
+	sys->seek(sys->fildes(0), big 0, Sys->SEEKEND);	# what does this do?
+	pflag = 0;
+	if(!globp.isnil)
+		lastc = '\n';
+	globp = (nil, 1);
+	peekc = lastc;
+	if(lastc)
+		for(;;) {
+			c = getchr();
+			if(c == '\n' || c == EOF)
+				break;
+		}
+	if(io != nil)
+		io = nil;
+	putchr('?');
+	putst(s);
+}
+
+error(s: string)
+{
+	error1(s);
+	raise "savej";
+}
+
+rescue()
+{
+	rescuing = 1;
+	if(dol > 0) {
+		addr1 = 0+1;
+		addr2 = dol;
+		io = sys->create("ed.hup", Sys->OWRITE, 8r0666);
+		if(io != nil){
+			iobuf = bufio->fopen(io, Sys->OWRITE);
+			putfile();
+		}
+	}
+	fchange = 0;
+	quit();
+}
+
+# void
+# notifyf(void *a, char *s)
+# {
+# 	if(strcmp(s, "interrupt") == 0){
+# 		if(rescuing || waiting)
+# 			noted(NCONT);
+# 		putchr(L'\n');
+# 		lastc = '\n';
+# 		error1(Q);
+# 		notejmp(a, savej, 0);
+# 	}
+# 	if(strcmp(s, "hangup") == 0){
+# 		if(rescuing)
+# 			noted(NDFLT);
+# 		rescue();
+# 	}
+# 	fprint(2, "ed: note: %s\n", s);
+# 	abort();
+# }
+
+getchr(): int
+{
+	s := array [Sys->UTFmax] of byte;
+	i: int;
+	r: int;
+	status: int;
+	if(lastc = peekc) {
+		peekc = 0;
+#debug(sys->sprint("getchr: peekc %c\n", lastc));
+		return lastc;
+	}
+	if(!globp.isnil) {
+		if (globp.s != nil) {
+			lastc = globp.s[0];
+			globp.s = globp.s[1:];
+#debug(sys->sprint("getchr: globp %c remaining %d\n", lastc, len globp.s));
+			return lastc;
+		}
+		globp = (nil, 1);
+#debug(sys->sprint("getchr: globp end\n"));
+		return EOF;
+	}
+#debug("globp nil\n");
+	for(i=0;;) {
+		if(sys->read(sys->fildes(0), s[i:], 1) <= 0)
+			return lastc = EOF;
+		i++;
+		(r, nil, status) = sys->byte2char(s, 0);
+		if (status > 0)
+			break;
+		
+	}
+	lastc = r;
+	return lastc;
+}
+
+gety(): int
+{
+	c: int;
+	gf: int;
+	p: int;
+
+	p = 0;
+	gf = !globp.isnil;
+	for(;;) {
+		c = getchr();
+		if(c == '\n') {
+			linebuf[p] = 0;
+			return 0;
+		}
+		if(c == EOF) {
+			if(gf)
+				peekc = c;
+			return c;
+		}
+		if(c == 0)
+			continue;
+		linebuf[p++] = c;
+		if(p >= len linebuf)
+			error(Q);
+	}
+	return 0;
+}
+
+gettty(): int
+{
+	rc: int;
+
+	rc = gety();
+	if(rc)
+		return rc;
+	if(linebuf[0] == '.' && linebuf[1] == 0)
+		return EOF;
+	return 0;
+}
+
+getfile(): int
+{
+	c: int;
+	lp: int;
+
+	lp = 0;
+	do {
+		c = iobuf.getc();
+		if(c < 0) {
+			if(lp > 0) {
+				putst("'\\n' appended");
+				c = '\n';
+			} else
+				return EOF;
+		}
+		if(lp >= len linebuf) {
+			lastc = '\n';
+			error(Q);
+		}
+		linebuf[lp++] = c;
+		count++;
+	} while(c != '\n');
+	linebuf[lp - 1] = 0;
+#debug(sys->sprint("getline read %d\n", lp));
+	return 0;
+}
+
+putfile()
+{
+	a1: int;
+	lp: int;
+	c: int;
+
+	a1 = addr1;
+	do {
+		lp = getline(zero[a1++]);
+		for(;;) {
+			count++;
+			c = linebuf[lp++];
+			if(c == 0) {
+				if (iobuf.putc('\n') < 0)
+					error(Q);
+				break;
+			}
+			if (iobuf.putc(c) < 0)
+				error(Q);
+		}
+	} while(a1 <= addr2);
+	if(iobuf.flush() < 0)
+		error(Q);
+}
+
+append(f: int, a: int): int
+{
+	a1, a2, rdot, nline, _tl: int;
+	rv: int;
+
+	nline = 0;
+	dot = a;
+	for (;;) {
+		case f {
+		APPEND_GETTTY => rv = gettty();
+		APPEND_GETSUB => rv = getsub();
+		APPEND_GETCOPY => rv = getcopy();
+		APPEND_GETFILE => rv = getfile();
+		}
+		if (rv != 0)
+			break;
+		if(dol >= nlall) {
+			nlall += 512;
+			newzero := array [nlall + 5] of int;
+			if(newzero == nil) {
+				error("MEM?");
+				rescue();
+			}
+			newzero[0:] = zero;
+			zero = newzero;
+		}
+		_tl = putline();
+		nline++;
+		a1 = ++dol;
+		a2 = a1+1;
+		rdot = ++dot;
+		zero[rdot:] = zero[rdot - 1: a1];
+		zero[rdot] = _tl;
+	}
+#debug(sys->sprint("end of append - dot %d\n", dot));
+	return nline;
+}
+
+add(i: int)
+{
+	if(i && (given || dol > 0)) {
+		addr1--;
+		addr2--;
+	}
+	squeeze(0);
+	newline();
+	append(APPEND_GETTTY, addr2);
+}
+
+bformat, bnum: int;
+
+browse()
+{
+	forward, n: int;
+
+	forward = 1;
+	peekc = getchr();
+	if(peekc != '\n'){
+		if(peekc == '-' || peekc == '+') {
+			if(peekc == '-')
+				forward = 0;
+			getchr();
+		}
+		n = getnum();
+		if(n > 0)
+			bpagesize = n;
+	}
+	newline();
+	if(pflag) {
+		bformat = listf;
+		bnum = listn;
+	} else {
+		listf = bformat;
+		listn = bnum;
+	}
+	if(forward) {
+		addr1 = addr2;
+		addr2 += bpagesize;
+		if(addr2 > dol)
+			addr2 = dol;
+	} else {
+		addr1 = addr2-bpagesize;
+		if(addr1 <= 0)
+			addr1 = 0+1;
+	}
+	printcom();
+}
+
+callunix()
+{
+	buf: string;
+	c: int;
+
+	if (sh == nil)
+		sh = load Sh Sh->PATH;
+	if (sh == nil) {
+		putst("can't load shell");
+		return;
+	}
+	setnoaddr();
+	while((c=getchr()) != EOF && c != '\n')
+		buf[len buf] = c;
+	sh->system(drawctxt, buf);
+ 	if(vflag)
+ 		putst("!");
+}
+
+quit()
+{
+	if(vflag && fchange && dol!=0) {
+		fchange = 0;
+		error(Q);
+	}
+	sys->remove(tfname);
+	exit;
+}
+
+onquit(nil: int)
+{
+	quit();
+}
+
+rdelete(ad1, ad2: int)
+{
+	a1, a2, a3: int;
+
+	a1 = ad1;
+	a2 = ad2+1;
+	a3 = dol;
+	dol -= a2 - a1;
+	do {
+		zero[a1++] = zero[a2++];
+	} while (a2 <= a3);
+	a1 = ad1;
+	if(a1 > dol)
+		a1 = dol;
+	dot = a1;
+	fchange = 1;
+}
+
+gdelete()
+{
+	a1, a2, a3: int;
+
+	a3 = dol;
+	for(a1=0; (zero[a1]&8r01)==0; a1++)
+		if(a1>=a3)
+			return;
+	for(a2=a1+1; a2<=a3;) {
+		if(zero[a2] & 8r01) {
+			a2++;
+			dot = a1;
+		} else
+			zero[a1++] = zero[a2++];
+	}
+	dol = a1-1;
+	if(dot > dol)
+		dot = dol;
+	fchange = 1;
+}
+
+getline(_tl: int): int
+{
+	lp, bp: int;
+	nl: int;
+	block: array of int;
+#debug(sys->sprint("getline %d\n", _tl));
+	lp = 0;
+	(block, bp) = getblock(_tl, Sys->OREAD);
+	nl = len block - bp;
+	_tl &= ~(RunesPerBlock - 1);
+	while(linebuf[lp++] = block[bp++]) {
+		nl--;
+		if(nl == 0) {
+			(block, bp) = getblock(_tl += RunesPerBlock, Sys->OREAD);
+			nl = len block;
+		}
+	}
+	return 0;
+}
+
+putline(): int
+{
+	lp, bp: int;
+	nl, _tl: int;
+	block: array of int;
+	fchange = 1;
+	lp = 0;
+	_tl = tline;
+	(block, bp) = getblock(_tl, Sys->OWRITE);
+	nl = len block - bp;
+	_tl &= ~(RunesPerBlock-1);		# _tl is now at the beginning of the block
+	while(block[bp] = linebuf[lp++]) {
+		if(block[bp++] == '\n') {
+			block[bp-1] = 0;
+			linebp = lp;
+			break;
+		}
+		nl--;
+		if(nl == 0) {
+			_tl += RunesPerBlock;
+			(block, bp) = getblock(_tl, Sys->OWRITE);
+			nl = len block;
+		}
+	}
+	nl = tline;
+	tline += ((lp) + 8r03) & 8r077776;
+	return nl;
+}
+
+tbuf := array [BLKSIZE] of byte;
+
+getrune(buf: array of byte): int
+{
+	return int buf[0] + (int buf[1] << 8);
+}
+
+putrune(buf: array of byte, v: int)
+{
+	buf[0] = byte (v);
+	buf[1] = byte (v >> 8);
+}
+
+blkio(b: int, buf: array of int, writefunc: int)
+{
+	sys->seek(tfile, big b * big BLKSIZE, Sys->SEEKSTART);
+	if (writefunc) {
+		# flatten buf into tbuf
+		for (x := 0; x < RunesPerBlock; x++)
+			putrune(tbuf[x * BytesPerRune:], buf[x]);
+		if (sys->write(tfile, tbuf, BLKSIZE) != len tbuf) {
+			error(T);
+		}
+	}
+	else {
+		if (sys->read(tfile, tbuf, len tbuf) != len tbuf) {
+			error(T);
+		}
+		for (x := 0; x < RunesPerBlock; x++)
+			buf[x] = getrune(tbuf[x * BytesPerRune:]);
+	}
+}
+
+ibuff := array [RunesPerBlock] of int;
+obuff := array [RunesPerBlock] of int;
+
+getblock(atl, iof: int): (array of int, int)
+{
+	bno, off: int;
+	
+	bno = atl / RunesPerBlock;
+	off = (atl * BytesPerRune) & (BLKSIZE-1) & ~8r03;
+	if(bno >= NBLK) {
+		lastc = '\n';
+		error(T);
+	}
+	off /= BytesPerRune;
+	if(bno == iblock) {
+		ichanged |= iof;
+#debug(sys->sprint("getblock(%d, %d): returns ibuff offset %d\n", atl, iof, off));
+		return (ibuff, off);
+	}
+	if(bno == oblock) {
+#debug(sys->sprint("getblock(%d, %d): returns obuff offset %d\n", atl, iof, off));
+		return (obuff, off);
+	}
+	if(iof == Sys->OREAD) {
+		if(ichanged)
+			blkio(iblock, ibuff, 1);
+		ichanged = 0;
+		iblock = bno;
+		blkio(bno, ibuff, 0);
+#debug(sys->sprint("getblock(%d, %d): returns ibuff offset %d\n", atl, iof, off));
+		return (ibuff, off);
+	}
+	if(oblock >= 0)
+		blkio(oblock, obuff, 1);
+	oblock = bno;
+#debug(sys->sprint("getblock(%d, %d): returns offset %d\n", atl, iof, off));
+	return (obuff, off);
+}
+
+_init()
+{
+	markp: int;
+
+	tfile = nil;
+	tline = RunesPerBlock;
+	for(markp = 0; markp < len names; markp++)
+		names[markp] = 0;
+	subnewa = 0;
+	anymarks = 0;
+	iblock = -1;
+	oblock = -1;
+	ichanged = 0;
+	if((tfile = sys->create(tfname, Sys->ORDWR, 8r0600)) == nil){
+		error1(T);
+		exit;
+	}
+	dot = dol = 0;
+}
+
+global(k: int)
+{
+	globuf: string;
+	c, a1: int;
+
+	if(!globp.isnil)
+		error(Q);
+	setwide();
+	squeeze(dol > 0);
+	c = getchr();
+	if(c == '\n')
+		error(Q);
+	compile(c);
+	globuf = nil;
+	while((c=getchr()) != '\n') {
+		if(c == EOF)
+			error(Q);
+		if(c == '\\') {
+			c = getchr();
+			if(c != '\n')
+				globuf[len globuf] = '\\';
+		}
+		globuf[len globuf] = c;
+	}
+	if(globuf == nil)
+		globuf = "p";
+	globuf[len globuf] = '\n';
+	for(a1=0; a1<=dol; a1++) {
+		zero[a1] &= ~8r01;
+		if(a1 >= addr1 && a1 <= addr2 && match(a1) == k)
+			zero[a1] |= 8r01;
+	}
+
+	#
+	# Special case: g/.../d (avoid n^2 algorithm)
+	 
+	if(globuf == "d\n") {
+		gdelete();
+		return;
+	}
+	for(a1=0; a1<=dol; a1++) {
+		if(zero[a1] & 8r01) {
+			zero[a1] &= ~8r01;
+			dot = a1;
+			globp = (globuf, 0);
+			commands();
+			a1 = 0;
+		}
+	}
+}
+
+join()
+{
+	gp, lp: int;
+	a1: int;
+
+	nonzero();
+	gp = 0;
+	for(a1=addr1; a1<=addr2; a1++) {
+		lp = getline(zero[a1]);
+		while(genbuf[gp] = linebuf[lp++])
+			if(gp++ >= LBSIZE-2)
+				error(Q);
+	}
+	lp = 0;
+	gp = 0;
+	while(linebuf[lp++] = genbuf[gp++])
+		;
+	zero[addr1] = putline();
+	if(addr1 < addr2)
+		rdelete(addr1+1, addr2);
+	dot = addr1;
+}
+
+substitute(inglob: int)
+{
+	mp, a1, nl, gsubf, n: int;
+
+	n = getnum();	# OK even if n==0 
+	gsubf = compsub();
+	for(a1 = addr1; a1 <= addr2; a1++) {
+		if(match(a1)){
+			m := n;
+
+			do {
+				span := loc2-loc1;
+
+				if(--m <= 0) {
+					dosub();
+					if(!gsubf)
+						break;
+					if(span == 0) {	# null RE match 
+						if(zero[loc2] == 0)
+							break;
+						loc2++;
+					}
+				}
+			} while(match(-1));
+			if(m <= 0) {
+				inglob |= 8r01;
+				subnewa = putline();
+				zero[a1] &= ~8r01;
+				if(anymarks) {
+					for(mp=0; mp<len names; mp++)
+						if(names[mp] == zero[a1])
+							names[mp] = subnewa;
+				}
+				subolda = zero[a1];
+				zero[a1] = subnewa;
+#debug(sys->sprint("append-getsub linebp = %d\n", linebp));
+				nl = append(APPEND_GETSUB, a1);
+				addr2 += nl;
+			}
+		}
+	}
+	if(inglob == 0)
+		error(Q);
+}
+
+compsub(): int
+{
+	seof, c: int;
+	p: int;
+
+	seof = getchr();
+	if(seof == '\n' || seof == ' ')
+		error(Q);
+	compile(seof);
+	p = 0;
+	for(;;) {
+		c = getchr();
+		if(c == '\\') {
+			c = getchr();
+			rhsbuf[p++] = ESCFLG;
+			if(p >= LBSIZE / 2)
+				error(Q);
+		} else
+		if(c == '\n' && (globp.isnil || globp.s == nil)) {
+			peekc = c;
+			pflag++;
+			break;
+		} else
+		if(c == seof)
+			break;
+		rhsbuf[p++] = c;
+		if(p >= LBSIZE / 2)
+			error(Q);
+	}
+	rhsbuf[p] = 0;
+	peekc = getchr();
+	if(peekc == 'g') {
+		peekc = 0;
+		newline();
+		return 1;
+	}
+	newline();
+	return 0;
+}
+
+getsub(): int
+{
+	p1, p2: int;
+
+	p1 = 0;
+	if((p2 = linebp) == -1)
+		return EOF;
+	while(linebuf[p1++] = linebuf[p2++])
+		;
+	linebp = -1;
+	return 0;
+}
+
+dosub()
+{
+	lp, sp, rp: int;
+	c, n: int;
+
+#	lp = linebuf;
+#	sp = genbuf;
+#	rp = rhsbuf;
+	lp = 0;	
+	sp = 0;
+	rp = 0;
+	while(lp < loc1)
+		genbuf[sp++] = linebuf[lp++];
+	while(c = rhsbuf[rp++]) {
+		if(c == '&'){
+			sp = place(sp, loc1, loc2);
+			continue;
+		}
+		if(c == ESCFLG && (c = rhsbuf[rp++]) >= '1' && c < MAXSUB+'0') {
+			n = c-'0';
+			if(n < len subexp && subexp[n].rsp >= 0 && subexp[n].rep >= 0) {
+				sp = place(sp, subexp[n].rsp, subexp[n].rep);
+				continue;
+			}
+			error(Q);
+		}
+		genbuf[sp++] = c;
+		if(sp >= LBSIZE)
+			error(Q);
+	}
+	lp = loc2;
+	loc2 = sp;
+	while(genbuf[sp++] = linebuf[lp++])
+		if(sp >= LBSIZE)
+			error(Q);
+	linebuf[0:] = genbuf[0: sp];
+}
+
+place(sp: int, l1: int, l2: int): int
+{
+
+	while(l1 < l2) {
+		genbuf[sp++] = linebuf[l1++];
+		if(sp >= LBSIZE)
+			error(Q);
+	}
+	return sp;
+}
+
+move(cflag: int)
+{
+	_adt, ad1, ad2: int;
+
+	nonzero();
+	if((_adt = address()) < 0)	# address() guarantees addr is in range 
+		error(Q);
+	newline();
+	if(cflag) {
+		ad1 = dol;
+		append(APPEND_GETCOPY, ad1++);
+		ad2 = dol;
+	} else {
+		ad2 = addr2;
+		for(ad1 = addr1; ad1 <= ad2;)
+			zero[ad1++] &= ~8r01;
+		ad1 = addr1;
+	}
+	ad2++;
+	if(_adt<ad1) {
+		dot = _adt + (ad2-ad1);
+		if((++_adt)==ad1)
+			return;
+		reverse(_adt, ad1);
+		reverse(ad1, ad2);
+		reverse(_adt, ad2);
+	} else
+	if(_adt >= ad2) {
+		dot = _adt++;
+		reverse(ad1, ad2);
+		reverse(ad2, _adt);
+		reverse(ad1, _adt);
+	} else
+		error(Q);
+	fchange = 1;
+}
+
+reverse(a1, a2: int)
+{
+	t: int;
+
+	for(;;) {
+		t = zero[--a2];
+		if(a2 <= a1)
+			return;
+		zero[a2] = zero[a1];
+		zero[a1++] = t;
+	}
+}
+
+getcopy(): int
+{
+	if(addr1 > addr2)
+		return EOF;
+	getline(zero[addr1++]);
+	return 0;
+}
+
+compile(eof: int)
+{
+	c: int;
+
+	if((c = getchr()) == '\n') {
+		peekc = c;
+		c = eof;
+	}
+	if(c == eof) {
+		if(pattern == nil)
+			error(Q);
+		return;
+	}
+	pattern = nil;
+	program := "";
+	do {
+		
+		if(c == '\\') {
+			program[len program] = '\\';
+			if((c = getchr()) == '\n') {
+				error(Q);
+				return;
+			}
+		}
+		program[len program] = c;
+	} while((c = getchr()) != eof && c != '\n');
+	if(c == '\n')
+		peekc = c;
+	diag: string;
+#debug("program " + program + "\n");
+	(pattern, diag) = regex->compile(program, 1);
+#if (diag != nil)
+#	debug("diag " + diag + "\n");
+	if (diag != nil)
+		pattern = nil;
+}
+
+mkstring(a: array of int): string
+{
+	s: string;
+	for (x := 0; x < len a; x++) {
+		if (a[x] == 0)
+			break;
+		s[x] = a[x];
+	}
+	return s;
+}
+
+match(addr: int): int
+{
+	rsp: int;
+	if(pattern == nil)
+		return 0;
+	if(addr >= 0){
+		if(addr == 0)
+			return 0;
+		rsp = getline(zero[addr]);
+	} else
+		rsp = loc2;
+	s := mkstring(linebuf);
+	subexp = regex->executese(pattern, s, (rsp, len s), rsp == 0, 1);
+	if(subexp != nil) {
+		(loc1, loc2) = subexp[0];
+		return 1;
+	}
+	loc1 = loc2 = -1;
+	return 0;
+}
+
+putd()
+{
+	r: int;
+
+	r = count%10;
+	count /= 10;
+	if(count)
+		putd();
+	putchr(r + '0');
+}
+
+putst(s: string)
+{
+	col = 0;
+	for(x := 0; x < len s; x++)
+		putchr(s[x]);
+	putchr('\n');
+}
+
+putshst(sp: int)
+{
+	col = 0;
+	while(linebuf[sp]) {
+		putchr(linebuf[sp++]);
+	}
+	putchr('\n');
+}
+
+putchr(ac: int)
+{
+	lp: int;
+	c: int;
+	rune: int;
+	lp = linp;
+	c = ac;
+	if(listf) {
+		if(c == '\n') {
+			if(linp != 0 && line[linp - 1] == byte ' ') {
+				line[lp++] = byte '\\';
+				line[lp++] = byte 'n';
+			}
+		} else {
+			if(col > (72-6-2)) {
+				col = 8;
+				line[lp++] = byte '\\';
+				line[lp++] = byte '\n';
+				line[lp++] = byte '\t';
+			}
+			col++;
+			if(c=='\b' || c=='\t' || c=='\\') {
+				line[lp++] = byte '\\';
+				if(c == '\b')
+					c = 'b';
+				else
+				if(c == '\t')
+					c = 't';
+				col++;
+			} else
+			if(c<' ' || c>=8r0177) {
+				line[lp++] = byte '\\';
+				line[lp++] = byte 'x';
+				line[lp++] = byte hex[c>>12];
+				line[lp++] = byte hex[c>>8&16rF];
+				line[lp++] = byte hex[c>>4&16rF];
+				c     =  hex[c&16rF];
+				col += 5;
+			}
+		}
+	}
+
+	rune = c;
+	lp += sys->char2byte(rune, line, lp);
+
+	if(c == '\n' || lp >= len line - 5) {
+		linp = 0;
+		if (oflag)
+			sys->write(sys->fildes(2), line, lp);
+		else
+			sys->write(sys->fildes(1), line, lp);
+		return;
+	}
+	linp = lp;
+}
+
+stringfromint(i: int): string
+{
+	s: string;
+	s[0] = i;
+	return s;
+}
+
+mktemp(as: string): string
+{
+	pid: int;
+	s: string;
+
+	s = nil;
+	pid = sys->pctl(0, nil);
+	for (x := len as - 1; x >= 0; x--)
+		if (as[x] == 'X') {
+			s = stringfromint('0' + pid % 10) + s;
+			pid /= 10;
+		}
+		else
+			s = stringfromint(as[x]) + s;
+	s[len s] = 'a';
+	for (;;) {
+		(rv, nil) := sys->stat(s);
+		if (rv < 0)
+			break;
+		if (s[len s - 1] == 'z')
+			return "/";
+		s[len s - 1]++;
+	}
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/emuinit.b
@@ -1,0 +1,110 @@
+implement Emuinit;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "arg.m";
+	arg: Arg;
+
+Emuinit: module
+{
+	init: fn();
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	sys->bind("#e", "/env", sys->MREPL|sys->MCREATE);	# if #e not configured, that's fine
+	args := getenv("emuargs");
+	arg = load Arg Arg->PATH;
+	if (arg == nil)
+		sys->fprint(sys->fildes(2), "emuinit: cannot load %s: %r\n", Arg->PATH);
+	else{
+		arg->init(args);
+		while((c := arg->opt()) != 0)
+			case c {
+			'g' or 'c' or 'C' or 'm' or 'p' or 'f' or 'r' or 'd' =>
+				arg->arg();
+	                  }
+		args = arg->argv();
+	}
+	mod: Command;
+	(mod, args) = loadmod(args);
+	mod->init(nil, args);
+}
+
+loadmod(args: list of string): (Command, list of string)
+{
+	path := Command->PATH;
+	if(args != nil)
+		path = hd args;
+	else
+		args = "-l" :: nil;	# add startup option
+
+	# try loading the module directly.
+	mod: Command;
+	if (path != nil && path[0] == '/')
+		mod = load Command path;
+	else {
+		mod = load Command "/dis/"+path;
+		if (mod == nil)
+			mod = load Command "/"+path;
+	}
+	if(mod != nil)
+		return (mod, args);
+
+	# if we can't load the module directly, try getting the shell to run it.
+	err := sys->sprint("%r");
+	mod = load Command Command->PATH;
+	if(mod == nil){
+		sys->fprint(sys->fildes(2), "emuinit: unable to load %s: %s\n", path, err);
+		raise "fail:error";
+	}
+	return (mod, "sh" :: "-c" :: "$*" :: args);
+}
+
+getenv(v: string): list of string
+{
+	fd := sys->open("#e/"+v, Sys->OREAD);
+	if (fd == nil)
+		return nil;
+	(ok, d) := sys->fstat(fd);
+	if(ok == -1)
+		return nil;
+	buf := array[int d.length] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return nil;
+	return unquoted(string buf[0:n]);
+}
+
+unquoted(s: string): list of string
+{
+	args: list of string;
+	word: string;
+	inquote := 0;
+	for(j := len s; j > 0;){
+		c := s[j-1];
+		if(c == ' ' || c == '\t' || c == '\n'){
+			j--;
+			continue;
+		}
+		for(i := j-1; i >= 0 && ((c = s[i]) != ' ' && c != '\t' && c != '\n' || inquote); i--){	# collect word
+			if(c == '\''){
+				word = s[i+1:j] + word;
+				j = i;
+				if(!inquote || i == 0 || s[i-1] != '\'')
+					inquote = !inquote;
+				else
+					i--;
+			}
+		}
+		args = (s[i+1:j]+word) :: args;
+		word = nil;
+		j = i;
+	}
+	# if quotes were unbalanced, balance them and try again.
+	if(inquote)
+		return unquoted(s + "'");
+	return args;
+}
--- /dev/null
+++ b/appl/cmd/env.b
@@ -1,0 +1,52 @@
+implement Envcmd;
+
+#
+# Copyright © 2000 Vita Nuova Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "env.m";
+
+include "readdir.m";
+
+Envcmd: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdout := sys->fildes(1);
+	if (tl argv != nil) {
+		sys->fprint(stderr(), "Usage: env\n");
+		raise "fail:usage";
+	}
+	env := load Env Env->PATH;
+	if(env == nil)
+		error(sys->sprint("can't load %s: %r", Env->PATH));
+	readdir := load Readdir Readdir->PATH;
+	if(readdir == nil)
+		error(sys->sprint("can't load %s: %r", Readdir->PATH));
+	(a, nil) := readdir->init("/env", Readdir->NONE | Readdir->COMPACT | Readdir->DESCENDING);
+	for(i := 0; i < len a; i++){
+		s := a[i].name+"="+env->getenv(a[i].name)+"\n";
+		b := array of byte s;
+		sys->write(stdout, b, len b);
+	}
+}
+
+error(s: string)
+{
+	sys->fprint(stderr(), "env: %s\n", s);
+	raise "fail:error";
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/cmd/export.b
@@ -1,0 +1,57 @@
+#
+# export current name space on a connection
+#
+
+implement Export;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+Export: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr(), "Usage: export [-a] dir [connection]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	# usage: export dir [connection]
+	sys = load Sys Sys->PATH;
+	if(argv != nil)
+		argv = tl argv;
+	flag := Sys->EXPWAIT;
+	for(; argv != nil && len hd argv && (hd argv)[0] == '-'; argv = tl argv)
+		for(i := 1; i < len hd argv; i++)
+			case (hd argv)[i] {
+			'a' =>
+				flag = Sys->EXPASYNC;
+			* =>
+				usage();
+			}
+	n := len argv;
+	if (n < 1 || n > 2)
+		usage();
+	fd: ref Sys->FD;
+	if (n == 2) {
+		if ((fd = sys->open(hd tl argv, Sys->ORDWR)) == nil) {
+			sys->fprint(stderr(), "export: can't open %s: %r\n", hd tl argv);
+			raise "fail:open";
+		}
+	} else
+		fd = sys->fildes(0);
+	if (sys->export(fd, hd argv, flag) < 0) {
+		sys->fprint(stderr(), "export: can't export: %r\n");
+		raise "fail:export";
+	}
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/cmd/fc.b
@@ -1,0 +1,612 @@
+implement Fc;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "math.m";
+	math: Math;
+include "string.m";
+	str: String;
+include "regex.m";
+	regex: Regex;
+
+Fc: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+
+UNARY, BINARY, SPECIAL: con iota;
+
+oSWAP, oDUP, oREP, oSUM, oPRNUM, oMULT,
+oPLUS, oMINUS, oDIV, oDIVIDE, oMOD, oSHIFTL, oSHIFTR,
+oAND, oOR, oXOR, oNOT, oUMINUS, oFACTORIAL,
+oPOW, oHYPOT, oATAN2, oJN, oYN, oSCALBN, oCOPYSIGN,
+oFDIM, oFMIN, oFMAX, oNEXTAFTER, oREMAINDER, oFMOD,
+oPOW10, oSQRT, oEXP, oEXPM1, oLOG, oLOG10, oLOG1P,
+oCOS, oCOSH, oSIN, oSINH, oTAN, oTANH, oACOS, oASIN, oACOSH,
+oASINH, oATAN, oATANH, oERF, oERFC,
+oJ0, oJ1, oY0, oY1, oILOGB, oFABS, oCEIL,
+oFLOOR, oFINITE, oISNAN, oRINT, oLGAMMA, oMODF,
+oDEG, oRAD: con iota;
+Op: adt {
+	name: string;
+	kind:	int;
+	op: int;
+};
+
+ops := array[] of {
+Op
+("swap",	SPECIAL, oSWAP),
+("dup",		SPECIAL, oDUP),
+("rep",		SPECIAL, oREP),
+("sum",		SPECIAL, oSUM),
+("p",			SPECIAL, oPRNUM),
+("x",			BINARY, oMULT),
+("×",			BINARY, oMULT),
+("pow",		BINARY, oPOW),
+("xx",		BINARY, oPOW),
+("+",			BINARY, oPLUS),
+("-",			BINARY, oMINUS),
+("/",			BINARY, oDIVIDE),
+("div",		BINARY, oDIV),
+("%",			BINARY, oMOD),
+("shl",		BINARY, oSHIFTL),
+("shr",		BINARY, oSHIFTR),
+("and",		BINARY, oAND),
+("or",		BINARY, oOR),
+("⋀",			BINARY, oAND),
+("⋁",			BINARY, oOR),
+("xor",		BINARY, oXOR),
+("not",		UNARY, oNOT),
+("_",			UNARY, oUMINUS),
+("factorial",	UNARY, oFACTORIAL),
+("!",			UNARY, oFACTORIAL),
+("pow",		BINARY, oPOW),
+("hypot",		BINARY, oHYPOT),
+("atan2",		BINARY, oATAN2),
+("jn",			BINARY, oJN),
+("yn",		BINARY, oYN),
+("scalbn",		BINARY, oSCALBN),
+("copysign",	BINARY, oCOPYSIGN),
+("fdim",		BINARY, oFDIM),
+("fmin",		BINARY, oFMIN),
+("fmax",		BINARY, oFMAX),
+("nextafter",	BINARY, oNEXTAFTER),
+("remainder",	BINARY, oREMAINDER),
+("fmod",		BINARY, oFMOD),
+("pow10",		UNARY, oPOW10),
+("sqrt",		UNARY, oSQRT),
+("exp",		UNARY, oEXP),
+("expm1",		UNARY, oEXPM1),
+("log",		UNARY, oLOG),
+("log10",		UNARY, oLOG10),
+("log1p",		UNARY, oLOG1P),
+("cos",		UNARY, oCOS),
+("cosh",		UNARY, oCOSH),
+("sin",		UNARY, oSIN),
+("sinh",		UNARY, oSINH),
+("tan",		UNARY, oTAN),
+("tanh",		UNARY, oTANH),
+("acos",		UNARY, oACOS),
+("asin",		UNARY, oASIN),
+("acosh",		UNARY, oACOSH),
+("asinh",		UNARY, oASINH),
+("atan",		UNARY, oATAN),
+("atanh",		UNARY, oATANH),
+("erf",		UNARY, oERF),
+("erfc",		UNARY, oERFC),
+("j0",			UNARY, oJ0),
+("j1",			UNARY, oJ1),
+("y0",		UNARY, oY0),
+("y1",		UNARY, oY1),
+("ilogb",		UNARY, oILOGB),
+("fabs",		UNARY, oFABS),
+("ceil",		UNARY, oCEIL),
+("floor",		UNARY, oFLOOR),
+("finite",		UNARY, oFINITE),
+("isnan",		UNARY, oISNAN),
+("rint",		UNARY, oRINT),
+("rad",		UNARY, oRAD),
+("deg",		UNARY, oDEG),
+("lgamma",	SPECIAL, oLGAMMA),
+("modf",		SPECIAL, oMODF),
+};
+
+nHEX, nBINARY, nOCTAL, nRADIX1, nRADIX2, nREAL, nCHAR: con iota;
+pats0 := array[] of {
+nHEX => "-?0[xX][0-9a-fA-F]+",
+nBINARY => "-?0[bB][01]+",
+nOCTAL => "-?0[0-7]+",
+nRADIX1 => "-?[0-9][rR][0-8]+",
+nRADIX2 => "-?[0-3][0-9][rR][0-9a-zA-Z]+",
+nREAL => "-?(([0-9]+(\\.[0-9]+)?)|([0-9]*(\\.[0-9]+)))([eE]-?[0-9]+)?",
+nCHAR => "@.",
+};
+RADIX, ANNOTATE, CHAR: con 1 << (iota + 10);
+
+outbase := 10;
+pats: array of Regex->Re;
+stack: list of real;
+last_op: Op;
+stderr: ref Sys->FD;
+
+usage()
+{
+	sys->fprint(stderr,
+		"usage: fc [-xdbB] [-r radix] <postfix expression>\n" +
+		"option specifies output format:\n" +
+		"\t-d decimal (default)\n" +
+		"\t-x hex\n" +
+		"\t-o octal\n" +
+		"\t-b binary\n" +
+		"\t-B annotated binary\n" +
+		"\t-c character\n" +
+		"\t-r <radix> specified base in Limbo 99r9999 format\n" +
+		"operands are decimal(default), hex(0x), octal(0), binary(0b), radix(99r)\n");
+	sys->fprint(stderr, "operators are:\n");
+	for (i := 0; i < len ops; i++)
+		sys->fprint(stderr, "%s ", ops[i].name);
+	sys->fprint(stderr, "\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	math = load Math Math->PATH;
+	regex = load Regex Regex->PATH;
+	if (regex == nil) {
+		sys->fprint(stderr, "fc: cannot load %s: %r\n", Regex->PATH);
+		raise "fail:error";
+	}
+
+	initpats();
+
+	if (argv == nil || tl argv == nil)
+		return;
+	argv = tl argv;
+	a := hd argv;
+	if (len a > 1 && a[0] == '-' && number(a).t0 == 0) {
+		case a[1] {
+		'd' =>
+			outbase = 10;
+		'x' =>
+			outbase = 16;
+		'o' =>
+			outbase = 8;
+		'b' =>
+			outbase = 2;
+		'c' =>
+			outbase = CHAR;
+		'r' =>
+			r := 0;
+			if (len a > 2)
+				r = int a[2:];
+			else if (tl argv == nil)
+				usage();
+			else {
+				argv = tl argv;
+				r = int hd argv;
+			}
+			if (r < 2 || r > 36)
+				usage();
+			outbase = r | RADIX;
+		'B' =>
+			outbase = 2 | ANNOTATE;
+		* =>
+			sys->fprint(stderr, "fc: unknown option -%c\n", a[1]);
+			usage();
+		}
+		argv = tl argv;
+	}
+
+	math->FPcontrol(0, Math->INVAL|Math->ZDIV|Math->OVFL|Math->UNFL|Math->INEX);
+
+	for (; argv != nil; argv = tl argv) {
+		(ok, x) := number(hd argv);
+		if (ok)
+			stack = x :: stack;
+		else {
+			op := find(hd argv);
+			exec(op);
+			last_op = op;
+		}
+	}
+
+	sp: list of real;
+	for (; stack != nil; stack = tl stack)
+		sp = hd stack :: sp;
+
+	# print stack bottom first
+	for (; sp != nil; sp = tl sp)
+		printnum(hd sp);
+}
+
+printnum(n: real)
+{
+	case outbase {
+	CHAR =>
+		sys->print("@%c\n", int n);
+	2 =>
+		sys->print("%s\n", binary(big n));
+	2 | ANNOTATE =>
+		sys->print("%s\n", annotatebinary(big n));
+	8 =>
+		sys->print("%#bo\n", big n);
+	10 =>
+		sys->print("%g\n", n);
+	16 =>
+		sys->print("%#bx\n", big n);
+	* =>
+		if ((outbase & RADIX) == 0)
+			error("unknown output base " + string outbase);
+		sys->print("%s\n", big2string(big n, outbase & ~RADIX));
+	}
+}
+
+# convert to binary string, keeping multiples of 8 digits.
+binary(n: big): string
+{
+	s := "0b";
+	for (j := 7; j > 0; j--)
+		if ((n & (big 16rff << (j * 8))) != big 0)
+			break;
+	for (i := 63; i >= 0; i--)
+		if (i / 8 <= j)
+			s[len s] = (int (n >> i) & 1) + '0';
+	return s;
+}
+
+annotatebinary(n: big): string
+{
+	s := binary(n);
+	a := s + "\n  ";
+	ndig := len s - 2;
+	for (i := ndig - 1; i >= 0; i--)
+		a[len a] = (i % 10) + '0';
+	if (ndig < 10)
+		return a;
+	a += "\n  ";
+	for (i = ndig - 1; i >= 10; i--) {
+		if (i % 10 == 0)
+			a[len a] = (i / 10) + '0';
+		else
+			a[len a] = ' ';
+	}
+	return a;
+}
+
+find(name: string): Op
+{
+	# XXX could do binary search here if we weren't a lousy performer anyway
+	for (i := 0; i < len ops; i++)
+		if (name == ops[i].name)
+			break;
+	if (i == len ops)
+		error("invalid operator '" + name + "'");
+	return ops[i];
+}
+
+exec(op: Op)
+{
+	case op.kind {
+	UNARY =>
+		unaryop(op.name, op.op);
+	BINARY =>
+		binaryop(op.name, op.op);
+	SPECIAL =>
+		specialop(op.name, op.op);
+	}
+}
+
+unaryop(name: string, op: int)
+{
+	assure(1, name);
+	v := hd stack;
+	case op {
+	oNOT =>
+		v = real !(int v);
+	oUMINUS =>
+		v = -v;
+	oFACTORIAL =>
+		n := int v;
+		v = 1.0;
+		while (n > 0)
+			v *= real n--;
+	oPOW10 =>
+		v = math->pow10(int v);
+	oSQRT =>
+		v = math->sqrt(v);
+	oEXP =>
+		v = math->exp(v);
+	oEXPM1 =>
+		v = math->expm1(v);
+	oLOG =>
+		v = math->log(v);
+	oLOG10 =>
+		v = math->log10(v);
+	oLOG1P =>
+		v = math->log1p(v);
+	oCOS =>
+		v = math->cos(v);
+	oCOSH =>
+		v = math->cosh(v);
+	oSIN =>
+		v = math->sin(v);
+	oSINH =>
+		v = math->sinh(v);
+	oTAN =>
+		v = math->tan(v);
+	oTANH =>
+		v = math->tanh(v);
+	oACOS =>
+		v = math->acos(v);
+	oASIN =>
+		v = math->asin(v);
+	oACOSH =>
+		v = math->acosh(v);
+	oASINH =>
+		v = math->asinh(v);
+	oATAN =>
+		v = math->atan(v);
+	oATANH =>
+		v = math->atanh(v);
+	oERF =>
+		v = math->erf(v);
+	oERFC =>
+		v = math->erfc(v);
+	oJ0 =>
+		v = math->j0(v);
+	oJ1 =>
+		v = math->j1(v);
+	oY0 =>
+		v = math->y0(v);
+	oY1 =>
+		v = math->y1(v);
+	oILOGB =>
+		v = real math->ilogb(v);
+	oFABS =>
+		v = math->fabs(v);
+	oCEIL =>
+		v = math->ceil(v);
+	oFLOOR =>
+		v = math->floor(v);
+	oFINITE =>
+		v = real math->finite(v);
+	oISNAN =>
+		v = real math->isnan(v);
+	oRINT =>
+		v = math->rint(v);
+	oRAD =>
+		v = (v / 360.0) * 2.0 * Math->Pi;
+	oDEG =>
+		v = v / (2.0 * Math->Pi) * 360.0;
+	* =>
+		error("unknown unary operator '" + name + "'");
+	}
+	stack = v :: tl stack;
+}
+
+binaryop(name: string, op: int)
+{
+	assure(2, name);
+	v1 := hd stack;
+	v0 := hd tl stack;
+	case op {
+	oMULT =>
+		v0 = v0 * v1;
+	oPLUS =>
+		v0 = v0 + v1;
+	oMINUS =>
+		v0 = v0 - v1;
+	oDIVIDE =>
+		v0 = v0 / v1;
+	oDIV =>
+		v0 = real (big v0 / big v1);
+	oMOD =>
+		v0 = real (big v0 % big v1);
+	oSHIFTL =>
+		v0 = real (big v0 << int v1);
+	oSHIFTR =>
+		v0 = real (big v0 >> int v1);
+	oAND =>
+		v0 = real (big v0 & big v1);
+	oOR =>
+		v0 = real (big v0 | big v1);
+	oXOR =>
+		v0 = real (big v0 ^ big v1);
+	oPOW =>
+		v0 = math->pow(v0, v1);
+	oHYPOT =>
+		v0 = math->hypot(v0, v1);
+	oATAN2 =>
+		v0 = math->atan2(v0, v1);
+	oJN =>
+		v0 = math->jn(int v0, v1);
+	oYN =>
+		v0 = math->yn(int v0, v1);
+	oSCALBN =>
+		v0 = math->scalbn(v0, int v1);
+	oCOPYSIGN =>
+		v0 = math->copysign(v0, v1);
+	oFDIM =>
+		v0 = math->fdim(v0, v1);
+	oFMIN =>
+		v0 = math->fmin(v0, v1);
+	oFMAX =>
+		v0 = math->fmax(v0, v1);
+	oNEXTAFTER =>
+		v0 = math->nextafter(v0, v1);
+	oREMAINDER =>
+		v0 = math->remainder(v0, v1);
+	oFMOD =>
+		v0 = math->fmod(v0, v1);
+	* =>
+		error("unknown binary operator '" + name + "'");
+	}
+	stack = v0 :: tl tl stack;
+}
+
+specialop(name: string, op: int)
+{
+	case op {
+	oSWAP =>
+		assure(2, name);
+		stack = hd tl stack :: hd stack :: tl tl stack;
+	oDUP =>
+		assure(1, name);
+		stack = hd stack :: stack;
+	oREP =>
+		if (last_op.kind != BINARY)
+			error("invalid operator '" + last_op.name + "' for rep");
+		while (stack != nil && tl stack != nil)
+			exec(last_op);
+	oSUM =>
+		for (sum := 0.0; stack != nil; stack = tl stack)
+			sum += hd stack;
+		stack = sum :: nil;
+	oPRNUM =>
+		assure(1, name);
+		printnum(hd stack);
+		stack = tl stack;
+	oLGAMMA =>
+		assure(1, name);
+		(s, lg) := math->lgamma(hd stack);
+		stack = lg :: real s :: tl stack;
+	oMODF =>
+		assure(1, name);
+		(i, r) := math->modf(hd stack);
+		stack = r :: real i :: tl stack;
+	* =>
+		error("unknown operator '" + name + "'");
+	}
+}
+
+initpats()
+{
+	pats = array[len pats0] of Regex->Re;
+	for (i := 0; i < len pats0; i++) {
+		(re, e) := regex->compile("^" + pats0[i] + "$", 0);
+		if (re == nil) {
+			sys->fprint(stderr, "fc: bad number pattern '^%s$': %s\n", pats0[i], e);
+			raise "fail:error";
+		}
+		pats[i] = re;
+	}
+}
+
+number(s: string): (int, real)
+{
+	case s {
+	"pi" or
+	"π" =>
+		return (1, Math->Pi);
+	"e" =>
+		return (1, 2.71828182845904509);
+	"nan" or
+	"NaN" =>
+		return (1, Math->NaN);
+	"-nan" or
+	"-NaN" =>
+		return (1, -Math->NaN);
+	"infinity" or
+	"Infinity" or
+	"∞" =>
+		return (1, Math->Infinity);
+	"-infinity" or
+	"-Infinity" or
+	"-∞" =>
+		return (1, -Math->Infinity);
+	"eps" or
+	"macheps" =>
+		return (1, Math->MachEps);
+	}
+	for (i := 0; i < len pats; i++) {
+		if (regex->execute(pats[i], s) != nil)
+			break;
+	}
+	case i {
+	nHEX =>
+		return base(s, 2, 16);
+	nBINARY =>
+		return base(s, 2, 2);
+	nOCTAL =>
+		return base(s, 1, 8);
+	nRADIX1 =>
+		return base(s, 2, int s);
+	nRADIX2 =>
+		return base(s, 3, int s);
+	nREAL =>
+		return (1, real s);
+	nCHAR =>
+		return (1, real s[1]);
+	}
+	return (0, Math->NaN);
+}
+
+base(s: string, i: int, radix: int): (int, real)
+{
+	neg := s[0] == '-';
+	if (neg)
+		i++;
+	n := big 0;
+	if (radix == 10)
+		n = big s[i:];
+	else if (radix == 0 || radix > 36)
+		return (0, Math->NaN);
+	else {
+		for (; i < len s; i++) {
+			c := s[i];
+			if ('0' <= c && c <= '9')
+				n = (n * big radix) + big(c - '0');
+			else if ('a' <= c && c < 'a' + radix - 10)
+				n = (n * big radix) + big(c - 'a' + 10);
+			else if ('A' <= c && c  < 'A' + radix - 10)
+				n = (n * big radix) + big(c - 'A' + 10);
+			else
+				return (0, Math->NaN);
+		}
+	}
+	if (neg)
+		n = -n;
+	return (1, real n);
+}
+
+# stolen from /appl/cmd/sh/expr.b
+big2string(n: big, radix: int): string
+{
+	if (neg := n < big 0) {
+		n = -n;
+	}
+	s := "";
+	do {
+		c: int;
+		d := int (n % big radix);
+		if (d < 10)
+			c = '0' + d;
+		else
+			c = 'a' + d - 10;
+		s[len s] = c;
+		n /= big radix;
+	} while (n > big 0);
+	t := s;
+	for (i := len s - 1; i >= 0; i--)
+		t[len s - 1 - i] = s[i];
+	if (radix != 10)
+		t = string radix + "r" + t;
+	if (neg)
+		return "-" + t;
+	return t;
+}
+
+error(e: string)
+{
+	sys->fprint(stderr, "fc: %s\n", e);
+	raise "fail:error";
+}
+
+assure(n: int, opname: string)
+{
+	if (len stack < n)
+		error("stack too small for op '" + opname + "'");
+}
--- /dev/null
+++ b/appl/cmd/fcp.b
@@ -1,0 +1,312 @@
+implement Fcp;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+include "readdir.m";
+	readdir: Readdir;
+
+Fcp: module
+{
+	init:	fn(nil: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+errors := 0;
+
+fdc: chan of (ref Sys->FD, ref Sys->FD);
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil) {
+		sys->fprint(stderr, "fcp: cannot load %s: %r\n", Arg->PATH);
+		raise "fail:bad module";
+	}
+	recursive := 0;
+	nreaders := nwriters := 8;
+	arg->init(argv);
+	arg->setusage("\tfcp [-r] [-R nproc] [-W nproc] src target\n\tfcp [-r] [-R nproc] [-W nproc] src ... directory");
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'R' =>
+			nreaders = int arg->earg();
+		'W' =>
+			nwriters = int arg->earg();
+		'r' =>
+			recursive = 1;
+		* =>
+			arg->usage();
+		}
+	}
+	if(nreaders < 1 || nwriters < 1)
+		arg->usage();
+	if(nreaders > 1 || nwriters > 1){
+		fdc = chan of (ref Sys->FD, ref Sys->FD);
+		spawn mstream(fdc, Sys->ATOMICIO, nreaders, nwriters);
+	}
+	argv = arg->argv();
+	argc := len argv;
+	if (argc < 2)
+		arg->usage();
+	arg = nil;
+
+	dst: string;
+	for (t := argv; t != nil; t = tl t)
+		dst = hd t;
+
+	(ok, dir) := sys->stat(dst);
+	todir := (ok != -1 && (dir.mode & Sys->DMDIR));
+	if (argc > 2 && !todir) {
+		sys->fprint(stderr, "fcp: %s  not a directory\n", dst);
+		raise "fail:error";
+	}
+	if (recursive)
+		cpdir(argv, dst);
+	else {
+		for (; tl argv != nil; argv = tl argv) {
+			if (todir)
+				cp(hd argv, dst, basename(hd argv));
+			else
+				cp(hd argv, dst, nil);
+		}
+	}
+	if(fdc != nil)
+		fdc <-= (nil, nil);
+	if (errors)
+		raise "fail:error";
+}
+
+basename(s: string): string
+{
+	for ((nil, ls) := sys->tokenize(s, "/"); ls != nil; ls = tl ls)
+		s = hd ls;
+	return s;
+}
+
+cp(src, dst: string, newname: string)
+{
+	ok: int;
+	ds, dd: Sys->Dir;
+
+	if (newname != nil)
+		dst += "/" + newname;
+	(ok, ds) = sys->stat(src);
+	if (ok < 0) {
+		warning(sys->sprint("%s: %r", src));
+		return;
+	}
+	if (ds.mode & Sys->DMDIR) {
+		warning(src + " is a directory");
+		return;
+	}
+	(ok, dd) = sys->stat(dst);
+	if (ok != -1 &&
+			ds.qid.path == dd.qid.path &&
+			ds.dev == dd.dev &&
+			ds.dtype == dd.dtype) {
+		warning(src + " and " + dst + " are the same file");
+		return;
+	}
+	sfd := sys->open(src, sys->OREAD);
+	if (sfd == nil) {
+		warning(sys->sprint("cannot open %s: %r", src));
+		return;
+	}
+	dfd := sys->create(dst, sys->OWRITE, ds.mode);
+	if (dfd == nil) {
+		warning(sys->sprint("cannot create %s: %r", dst));
+		return;
+	}
+	copy(sfd, dfd, src, dst);
+}
+
+mkdir(d: string, mode: int): int
+{
+	dfd := sys->create(d, sys->OREAD, sys->DMDIR | mode);
+	if (dfd == nil) {
+		warning(sys->sprint("cannot make directory %s: %r", d));
+		return -1;
+	}
+	return 0;
+}
+
+copy(sfd, dfd: ref Sys->FD, src, dst: string): int
+{
+	if(fdc != nil){
+		fdc <-= (sfd, dfd);
+		return 0;
+	}
+	buf := array[Sys->ATOMICIO] of byte;
+	for (;;) {
+		r := sys->read(sfd, buf, Sys->ATOMICIO);
+		if (r < 0) {
+			warning(sys->sprint("error reading %s: %r", src));
+			return -1;
+		}
+		if (r == 0)
+			return 0;
+		if (sys->write(dfd, buf, r) != r) {
+			warning(sys->sprint("error writing %s: %r", dst));
+			return -1;
+		}
+	}
+}
+
+cpdir(argv: list of string, dst: string)
+{
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil) {
+		sys->fprint(stderr, "fcp: cannot load %s: %r\n", Readdir->PATH);
+		raise "fail:bad module";
+	}
+	cache = array[NCACHE] of list of ref Sys->Dir;
+	dexists := 0;
+	(ok, dd) := sys->stat(dst);
+	 # destination file exists
+	if (ok != -1) {
+		if ((dd.mode & Sys->DMDIR) == 0) {
+			warning(dst + ": destination not a directory");
+			return;
+		}
+		dexists = 1;
+	}
+	for (; tl argv != nil; argv = tl argv) {
+		ds: Sys->Dir;
+		src := hd argv;
+		(ok, ds) = sys->stat(src);
+		if (ok < 0) {
+			warning(sys->sprint("can't stat %s: %r", src));
+			continue;
+		}
+		if ((ds.mode & Sys->DMDIR) == 0) {
+			cp(hd argv, dst, basename(hd argv));
+		} else if (dexists) {
+			if (ds.qid.path==dd.qid.path &&
+					ds.dev==dd.dev &&
+					ds.dtype==dd.dtype) {
+				warning("cannot copy " + src + " into itself");
+				continue;
+			}
+			copydir(src, dst + "/" + basename(src), ds.mode);
+		} else {
+			copydir(src, dst, ds.mode);
+		}
+	}
+}
+
+copydir(src, dst: string, srcmode: int)
+{
+	(ok, nil) := sys->stat(dst);
+	if (ok != -1) {
+		warning("cannot copy " + src + " onto another directory");
+		return;
+	}
+	tmode := srcmode | 8r777;	# Fix for Nt
+	if (mkdir(dst, tmode) == -1)
+		return;
+	(entries, n) := readdir->init(src, Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		e := entries[i];
+		path := src + "/" + e.name;
+		if ((e.mode & Sys->DMDIR) == 0)
+			cp(path, dst, e.name);
+		else if (seen(e))
+			warning(path + ": directory loop found");
+		else
+			copydir(path, dst + "/" + e.name, e.mode);
+	}
+	chmod(dst, srcmode);
+}
+
+# Avoid loops in tangled namespaces. (from du.b)
+NCACHE: con 64; # must be power of two
+cache: array of list of ref sys->Dir;
+
+seen(dir: ref sys->Dir): int
+{
+	savlist := cache[int dir.qid.path&(NCACHE-1)];
+	for(c := savlist; c!=nil; c = tl c){
+		sav := hd c;
+		if(dir.qid.path==sav.qid.path &&
+			dir.dtype==sav.dtype && dir.dev==sav.dev)
+			return 1;
+	}
+	cache[int dir.qid.path&(NCACHE-1)] = dir :: savlist;
+	return 0;
+}
+
+warning(e: string)
+{
+	sys->fprint(stderr, "fcp: %s\n", e);
+	errors++;
+}
+
+chmod(s: string, mode: int): int
+{
+	(ok, d) := sys->stat(s);
+	if (ok < 0)
+		return -1;
+
+	if(d.mode == mode)
+		return 0;
+	d = sys->nulldir;
+	d.mode = mode;
+	if (sys->wstat(s, d) < 0) {
+		warning(sys->sprint("cannot wstat %s: %r", s));
+		return -1;
+	}
+	return 0;
+}
+
+mstream(fdc: chan of (ref Sys->FD, ref Sys->FD), bufsize: int, nin, nout: int)
+{
+	inc := chan of (ref Sys->FD, big, int, ref Sys->FD);
+	outc := chan of (ref Sys->FD, big, array of byte);
+	for(i := 0; i < nin; i++)
+		spawn readproc(inc, outc);
+	for(i = 0; i < nout; i++)
+		spawn writeproc(outc);
+	while(((src, dst) := <-fdc).t0 != nil){
+		(ok, stat) := sys->fstat(src);
+		if(ok == -1)
+			continue;
+		tot := stat.length;
+		o := big 0;
+		while((n := tot - o) > big 0){
+			if(n < big bufsize)
+				inc <-= (src, o, int n, dst);
+			else
+				inc <-= (src, o, bufsize, dst);
+			o += big bufsize;
+		}
+	}
+	for(i = 0; i < nin; i++)
+		inc <-= (nil, big 0, 0, nil);
+	for(i = 0; i < nout; i++)
+		outc <-= (nil, big 0, nil);
+}
+
+readproc(inc: chan of (ref Sys->FD, big, int, ref Sys->FD), outc: chan of (ref Sys->FD, big, array of byte))
+{
+	buf: array of byte;
+	while(((src, o, nb, dst) := <-inc).t0 != nil){
+		if(len buf < nb)
+			buf = array[nb*2] of byte;
+		n := sys->pread(src, buf, nb, o);
+		if(n > 0){
+			outc <-= (dst, o, buf[0:n]);
+			buf = buf[n:];
+		}
+	}
+}
+
+writeproc(outc: chan of (ref Sys->FD, big, array of byte))
+{
+	while(((dst, o, buf) := <-outc).t0 != nil)
+		sys->pwrite(dst, buf, len buf, o);
+}
--- /dev/null
+++ b/appl/cmd/fmt.b
@@ -1,0 +1,204 @@
+implement Fmt;
+
+#
+#	Copyright © 2002 Lucent Technologies Inc.
+#	based on the Plan 9 command; subject to the Lucent Public License 1.02
+#	this Vita Nuova variant uses Limbo channels and processes to avoid accumulating words
+#
+
+#
+#  block up paragraphs, possibly with indentation
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+
+Fmt: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+extraindent := 0;	# how many spaces to indent all lines
+indent := 0;	# current value of indent, before extra indent
+length := 70;	# how many columns per output line
+join := 1;	# can lines be joined?
+maxtab := 8;
+bout: ref Iobuf;
+
+Word: adt {
+	text:	string;
+	indent:	int;
+	bol:	int;
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg := load Arg Arg->PATH;
+
+	arg->init(args);
+	arg->setusage("fmt [-j] [-i indent] [-l length] [file...]");
+	while((c := arg->opt()) != 0) 
+		case(c){
+		'i' =>
+			extraindent = int arg->earg();
+		'j' =>
+			join = 0;
+		'w' or 'l' =>
+			length = int arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(length <= extraindent){
+		sys->fprint(sys->fildes(2), "fmt: line length<=indentation\n");
+		raise "fail:length";
+	}
+	arg = nil;
+
+	err := "";
+	bout = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+	if(args == nil){
+		bin := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+		fmt(bin);
+	}else
+		for(; args != nil; args = tl args){
+			bin := bufio->open(hd args, Bufio->OREAD);
+			if(bin == nil){
+				sys->fprint(sys->fildes(2), "fmt: can't open %s: %r\n", hd args);
+				err = "open";
+			}else{
+				fmt(bin);
+				if(tl args != nil)
+					bout.putc('\n');
+			}
+		}
+	bout.flush();
+	if(err != nil)
+		raise "fail:"+err;
+}
+
+fmt(f: ref Iobuf)
+{
+	words := chan of ref Word;
+	spawn parser(f, words);
+	printwords(words);
+}
+
+parser(f: ref Iobuf, words: chan of ref Word)
+{
+	while((s := f.gets('\n')) != nil){
+		if(s[len s-1] == '\n')
+			s = s[0:len s-1];
+		parseline(s, words);
+	}
+	words <-= nil;
+}
+
+parseline(line: string, words: chan of ref Word)
+{
+	ind: int;
+	(line, ind) = indentof(line);
+	indent = ind;
+	bol := 1;
+	for(i:=0; i < len line;){
+		# find next word
+		if(line[i] == ' ' || line[i] == '\t'){
+			i++;
+			continue;
+		}
+		# where does this word end?
+		for(l:=i; l < len line; l++)
+			if(line[l]==' ' || line[l]=='\t')
+				break;
+		words <-= ref Word(line[i:l], indent, bol);
+		bol = 0;
+		i = l;
+	}
+	if(bol)
+		words <-= ref Word("", -1, bol);
+}
+
+indentof(line: string): (string, int)
+{
+	ind := 0;
+	for(i:=0; i < len line; i++)
+		case line[i] {
+		' ' =>
+			ind++;
+		'\t' =>
+			ind += maxtab;
+			ind -= ind%maxtab;
+		* =>
+			return (line, ind);
+		}
+	# plain white space doesn't change the indent
+	return (line, indent);
+}
+	
+printwords(words: chan of ref Word)
+{
+	# one output line per loop
+	nw := <-words;
+	while((w := nw) != nil){
+		# if it's a blank line, print it
+		if(w.indent == -1){
+			bout.putc('\n');
+			nw = <-words;
+			continue;
+		}
+		# emit leading indent
+		col := extraindent+w.indent;
+		printindent(col);
+		# emit words until overflow; always emit at least one word
+		for(n:=0;; n++){
+			bout.puts(w.text);
+			col += len w.text;
+			if((nw = <-words) == nil)
+				break;	# out of words
+			if(nw.indent != w.indent)
+				break;	# indent change
+			nsp := nspaceafter(w.text);
+			if(col+nsp+len nw.text > extraindent+length)
+				break;	# fold line
+			if(!join && nw.bol)
+				break;
+			for(j:=0; j<nsp; j++)
+				bout.putc(' ');	# emit space; another word will follow
+			col += nsp;
+			w = nw;
+		}
+		bout.putc('\n');
+	}
+}
+
+printindent(w: int)
+{
+	while(w >= maxtab){
+		bout.putc('\t');
+		w -= maxtab;
+	}
+	while(--w >= 0)
+		bout.putc(' ');
+}
+
+# give extra space if word ends with punctuation
+nspaceafter(s: string): int
+{
+	if(len s < 2)
+		return 1;
+	if(len s < 4 && s[0] >= 'A' && s[0] <= 'Z')
+		return 1;	# assume it's a title, not full stop
+	if((c := s[len s-1]) == '.' || c == '!' || c == '?')
+		return 2;
+	return 1;
+}
--- /dev/null
+++ b/appl/cmd/fortune.b
@@ -1,0 +1,100 @@
+#
+#	initially generated by c2l
+#
+
+implement Fortune;
+
+Fortune: module
+{
+	init: fn(nil: ref Draw->Context, argl: list of string);
+};
+
+include "sys.m";
+	sys: Sys;
+	Dir: import sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "rand.m";
+	rand: Rand;
+
+include "keyring.m";
+include "security.m";
+
+choice: string;
+findex := "/lib/games/fortunes.index";
+fortunes := "/lib/games/fortunes";
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	rand = load Rand Rand->PATH;
+
+	if(args != nil)
+		args = tl args;
+	if(args != nil)
+		filename := hd args;
+	else
+		filename = fortunes;
+	if((f := bufio->open(filename,  Bufio->OREAD)) == nil){
+		sys->fprint(sys->fildes(2), "fortune: can't open %s: %r\n", filename);
+		raise "fail:open";
+	}
+	ix, nix: ref Sys->FD;
+	length := big 0;
+	if(args == nil){
+		ix = sys->open(findex, Sys->OREAD);
+		if(ix != nil){
+			(nil, ixbuf) := sys->fstat(ix);
+			(nil, fbuf) := sys->fstat(f.fd);
+			if(fbuf.mtime >= ixbuf.mtime){
+				ix = nil;
+				nix = sys->create(findex, Sys->OWRITE, 8r666);
+			}else
+				length = ixbuf.length;
+		}else
+			nix = sys->create(findex, Sys->OWRITE, 8r666);
+	}
+	off := array[4] of byte;
+	if(ix != nil && length != big 0){
+		sys->seek(ix, ((big truerand() & ((big 1<<32)-big 1))%length) & ~big 3, 0);
+		sys->read(ix, off, 4);
+		f.seek(big (int off[0]|int off[1]<<8|int off[2]<<16|int off[3]<<24), 0);
+		choice = f.gets('\n');
+		if(choice == nil)
+			choice = "Misfortune!\n";
+	}else{
+		rand->init(truerand());
+		offs := 0;
+		g := bufio->fopen(nix, Bufio->ORDWR);
+		for(i := 1;; i++){
+			if(nix != nil)
+				offs = int f.offset();
+			p := f.gets('\n');
+			if(p == nil)
+				break;
+			if(nix != nil){
+				off[0] = byte offs;
+				off[1] = byte (offs>>8);
+				off[2] = byte (offs>>16);
+				off[3] = byte (offs>>24);
+				g.write(off, 4);
+			}
+			if(rand->rand(i) == 0)
+				choice = p;
+		}
+		g.flush();
+	}
+	sys->print("%s", choice);
+}
+
+truerand(): int
+{
+	random := load Random Random->PATH;
+	return random->randomint(Random->ReallyRandom);
+}
--- /dev/null
+++ b/appl/cmd/freq.b
@@ -1,0 +1,112 @@
+implement Freq;
+
+#
+#	Copyright © 2002 Lucent Technologies Inc.
+# 	transliteration of the Plan 9 command; subject to the Lucent Public License 1.02
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+
+Freq: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+count := array[1<<16] of big;
+flag := 0;
+
+Fdec, Fhex, Foct, Fchar, Frune: con 1<<iota;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg := load Arg Arg->PATH;
+
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		* =>
+			sys->fprint(sys->fildes(2), "freq: unknown option %c\n", c);
+			raise "fail:usage";
+		'd' =>
+			flag |= Fdec;
+		'x' =>
+			flag |= Fhex;
+		'o' =>
+			flag |= Foct;
+		'c' =>
+			flag |= Fchar;
+		'r' =>
+			flag |= Frune;
+		}
+	args = arg->argv();
+	arg = nil;
+
+	bout := bufio->fopen(sys->fildes(1), Sys->OWRITE);
+	if((flag&(Fdec|Fhex|Foct|Fchar)) == 0)
+		flag |= Fdec|Fhex|Foct|Fchar;
+	if(args == nil){
+		freq(sys->fildes(0), "-", bout);
+		exit;
+	}
+	for(; args != nil; args = tl args){
+		f := sys->open(hd args, Sys->OREAD);
+		if(f == nil){
+			sys->fprint(sys->fildes(2), "cannot open %s\n", hd args);
+			continue;
+		}
+		freq(f, hd args, bout);
+		f = nil;
+	}
+}
+
+freq(f: ref Sys->FD, s: string, bout: ref Iobuf)
+{
+	c: int;
+
+	bin := bufio->fopen(f, Sys->OREAD);
+	if(flag&Frune)
+		for(;;){
+			c = bin.getc();
+			if(c < 0)
+				break;
+			count[c]++;
+		}
+	else
+		for(;;){
+			c = bin.getb();
+			if(c < 0)
+				break;
+			count[c]++;
+		}
+	if(c != Bufio->EOF)
+		sys->fprint(sys->fildes(2), "freq: read error on %s: %r\n", s);
+	for(i := 0; i < (len count)/4; i++){
+		if(count[i] == big 0)
+			continue;
+		if(flag&Fdec)
+			bout.puts(sys->sprint("%3d ", i));
+		if(flag&Foct)
+			bout.puts(sys->sprint("%.3o ", i));
+		if(flag&Fhex)
+			bout.puts(sys->sprint("%.2x ", i));
+		if(flag&Fchar)
+			if(i <= 16r20 || i >= 16r7f && i < 16ra0 || i > 16rff && !(flag&Frune))
+				bout.puts("- ");
+			else
+				bout.puts(sys->sprint("%c ", i));
+		bout.puts(sys->sprint("%8bd\n", count[i]));
+	}
+	bout.flush();
+}
+
--- /dev/null
+++ b/appl/cmd/fs.b
@@ -1,0 +1,109 @@
+implement Fs;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "readdir.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Quit: import Fslib;
+
+# fs distribution:
+
+# {filter -d {not {match -r '\.(dis|sbl)$'}} {filter {path /module/fslib.m /module/bundle.m /module/unbundle.m /appl/cmd/fs.b /appl/cmd/fs /appl/lib/fslib.b} /}}
+
+Fs: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+badmod(path: string)
+{
+	sys->fprint(stderr(), "fs: cannot load %s: %r\n", path);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	fslib->init();
+	argv = tl argv;
+
+	if(argv == nil)
+		usage();
+	report := Report.new();
+	s := hd argv;
+	if(tl argv == nil && s != nil && s[0] == '{' && s[len s - 1] == '}')
+		s = "void " + hd argv;
+	else {
+		s = "void {" + hd argv;
+		for(argv = tl argv; argv != nil; argv = tl argv){
+			a := hd argv;
+			if(a == nil || a[0] != '{')		# }
+				s += sys->sprint(" %q", a);
+			else
+				s += " " + hd argv;
+		}
+		s += "}";
+	}
+	m := load Fsmodule "/dis/fs/eval.dis";
+	if(m == nil)
+		badmod("/dis/fs/eval.dis");
+	if(!fslib->typecompat("as", m->types())){
+		sys->fprint(stderr(), "fs: eval module implements incompatible type (usage: %s)\n",
+				fslib->cmdusage("eval", m->types()));
+		raise "fail:bad eval module";
+	}
+	m->init();
+	v := m->run(ctxt, report, nil, ref Value.S(s) :: nil);
+	fail: string;
+	if(v == nil)
+		fail = "error";
+	else{
+		sync := v.v().i;
+		sync <-= 1;
+	}
+	report.enable();
+	while((e := <-report.reportc) != nil)
+		sys->fprint(stderr(), "fs: %s\n", e);
+	if(fail != nil)
+		raise "fail:" +fail;
+}
+
+usage()
+{
+	fd := stderr();
+	sys->fprint(fd, "usage: fs expression\n");
+	sys->fprint(fd, "verbs are:\n");
+	if((readdir := load Readdir Readdir->PATH) == nil){
+		sys->fprint(fd, "fs: cannot load %s: %r\n", Readdir->PATH);
+	}else{
+		(a, nil) := readdir->init("/dis/fs", Readdir->NAME|Readdir->COMPACT);
+		for(i := 0; i < len a; i++){
+			f := a[i].name;
+			if(len f < 4 || f[len f - 4:] != ".dis")
+				continue;
+			m := load Fsmodule "/dis/fs/" + f;
+			if(m == nil)
+				sys->fprint(fd, "\t(%s: cannot load: %r)\n", f[0:len f - 4]);
+			else
+				sys->fprint(fd, "\t%s\n", fslib->cmdusage(f[0:len f - 4], m->types()));
+		}
+	}
+	sys->fprint(fd, "automatic conversions:\n");
+	sys->fprint(fd, "\tstring -> fs {walk string}\n");
+	sys->fprint(fd, "\tfs -> entries {entries fs}\n");
+	sys->fprint(fd, "\tstring -> gate {match string}\n");
+	sys->fprint(fd, "\tentries -> void {print entries}\n");
+	sys->fprint(fd, "\tcommand -> string {run command}\n");
+	raise "fail:usage";
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/cmd/fs/and.b
@@ -1,0 +1,65 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "pppp*";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	c := chan of Gatequery;
+	spawn andgate(c, args);
+	return ref Value.P(c);
+}
+
+andgate(c: Gatechan, args: list of ref Value)
+{
+	sub: list of Gatechan;
+	for(; args != nil; args = tl args)
+		sub = (hd args).p().i :: sub;
+	sub = rev(sub);
+	myreply := chan of int;
+	while(((d, reply) := <-c).t0.t0 != nil){
+		for(l := sub; l != nil; l = tl l){
+			(hd l) <-= (d, myreply);
+			if(<-myreply == 0)
+				break;
+		}
+		reply <-= l == nil;
+	}
+	for(; sub != nil; sub = tl sub)
+		hd sub <-= (Nilentry, nil);
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
--- /dev/null
+++ b/appl/cmd/fs/bundle.b
@@ -1,0 +1,195 @@
+implement Bundle;
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "readdir.m";
+	readdir: Readdir;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, report, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+include "bundle.m";
+
+# XXX if we can't open a directory, is it ever worth passing its metadata
+# through anyway?
+
+EOF: con "end of archive\n";
+
+types(): string
+{
+	return "vx";
+}
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: bundle: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmod(Readdir->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmod(Readdir->PATH);
+	bufio->fopen(nil, Sys->OREAD);		# XXX no bufio->init!
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Readdir->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	return ref Value.V(
+		bundle(
+			report,
+			bufio->fopen(sys->fildes(1), Sys->OWRITE),
+			(hd args).x().i
+		)
+	);
+}
+
+bundle(r: ref Report, iob: ref Iobuf, c: Fschan): chan of int
+{
+	sync := chan of int;
+	spawn bundleproc(c, sync, iob, r.start("bundle"));
+	return sync;
+}
+
+bundleproc(c: Fschan, sync: chan of int, iob: ref Iobuf, errorc: chan of string)
+{
+	if(sync != nil && <-sync == 0){
+		(<-c).t1 <-= Quit;
+		quit(errorc);
+	}
+	(d, reply) := <-c;
+	if(d.dir == nil){
+		report(errorc, "no root directory");
+		endarchive(iob, errorc);
+	}
+	if(puts(iob, dir2header(d.dir), errorc) == -1){
+		reply <-= Quit;
+		quit(errorc);
+	}
+	reply <-= Down;
+	bundledir(d.dir.name, d, c, iob, errorc);
+	endarchive(iob, errorc);
+}
+
+endarchive(iob: ref Iobuf, errorc: chan of string)
+{
+	if(puts(iob, EOF, errorc) != -1)
+		iob.flush();
+	quit(errorc);
+	exit;
+}
+
+bundledir(path: string, d: Fsdata,
+		c: Fschan,
+		iob: ref Iobuf, errorc: chan of string)
+{
+	if(d.dir.mode & Sys->DMDIR){
+		path[len path] = '/';
+		for(;;){
+			(ent, reply) := <-c;
+			if(ent.dir == nil){
+				reply <-= Skip;
+				break;
+			}
+			if(puts(iob, dir2header(ent.dir), errorc) == -1){
+				reply <-= Quit;
+				quit(errorc);
+			}
+			reply <-= Down;
+			bundledir(path + ent.dir.name, ent, c, iob, errorc);
+		}
+		iob.putc('\n');
+	}else{
+		buf: array of byte;
+		reply: chan of int;
+		length := big d.dir.length;
+		n := big 0;
+		for(;;){
+			((nil, buf), reply) = <-c;
+			if(buf == nil){
+				reply <-= Skip;
+				break;
+			}
+			if(write(iob, buf, len buf, errorc) != len buf){
+				reply <-= Quit;
+				quit(errorc);
+			}
+			n += big len buf;
+			if(n > length){		# should never happen
+				report(errorc, sys->sprint("%q is longer than expected (fatal)", path));
+				reply <-= Quit;
+				quit(errorc);
+			}
+			if(n == length){
+				reply <-= Skip;
+				break;
+			}
+			reply <-= Next;
+		}
+		if(n < length){
+			report(errorc, sys->sprint("%q is shorter than expected (%bd/%bd); adding null bytes", path, n, length));
+			buf = array[Sys->ATOMICIO] of {* => byte 0};
+			while(n < length){
+				nb := len buf;
+				if(length - n < big len buf)
+					nb = int (length - n);
+				if(write(iob, buf, nb, errorc) != nb){
+					(<-c).t1 <-= Quit;
+					quit(errorc);
+				}
+				report(errorc, sys->sprint("added %d null bytes", nb));
+				n += big nb;
+			}
+		}
+	}
+}
+
+dir2header(d: ref Sys->Dir): string
+{
+	return sys->sprint("%q %uo %q %q %ud %bd\n", d.name, d.mode, d.uid, d.gid, d.mtime, d.length);
+}
+
+puts(iob: ref Iobuf, s: string, errorc: chan of string): int
+{
+	{
+		if(iob.puts(s) == -1)
+			report(errorc, sys->sprint("write error: %r"));
+		return 0;
+	} exception {
+	"write on closed pipe" =>
+		return -1;
+	}
+}
+
+write(iob: ref Iobuf, buf: array of byte, n: int, errorc: chan of string): int
+{
+	{
+		nw := iob.write(buf, n);
+		if(nw < n){
+			if(nw >= 0)
+				report(errorc, "short write");
+			else{
+				report(errorc, sys->sprint("write error: %r"));
+			}
+		}
+		return nw;
+	} exception {
+	"write on closed pipe" =>
+		report(errorc, "write on closed pipe");
+		return -1;
+	}
+}
--- /dev/null
+++ b/appl/cmd/fs/chstat.b
@@ -1,0 +1,185 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fsfilter: Fsfilter;
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+Query: adt {
+	gate: Gatechan;
+	stat: Sys->Dir;
+	mask: int;
+	cflag: int;
+	reply: chan of int;
+
+	query: fn(q: self ref Query, d: ref Sys->Dir, name: string, depth: int): int;
+};
+
+types(): string
+{
+	return "xx-pp-ms-us-gs-ts-as-c";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	fsfilter = load Fsfilter Fsfilter->PATH;
+	if(fsfilter == nil)
+		badmod(Fsfilter->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	ws := Sys->nulldir;
+	mask := 0;
+	gate: ref Value;
+	cflag := 0;
+	for(; opts != nil; opts = tl opts){
+		o := (hd opts).args;
+		case (hd opts).opt {
+		'p' =>
+			gate.discard();
+			gate = hd o;
+		'm' =>
+			ok: int;
+			m := (hd o).s().i;
+			(ok, mask, ws.mode) = parsemode(m);
+			mask &= ~Sys->DMDIR;
+			if(ok == 0){
+				sys->fprint(sys->fildes(2), "fs: chstat: bad mode %#q\n", m);
+				gate.discard();
+				return nil;
+			}
+		'u' =>
+			ws.uid = (hd o).s().i;
+		'g' =>
+			ws.gid = (hd o).s().i;
+		't' =>
+			ws.mtime = int (hd o).s().i;
+		'a' =>
+			ws.atime = int (hd o).s().i;
+		'c' =>
+			cflag++;
+		}
+	}
+
+	dst := chan of (Fsdata, chan of int);
+	p: Gatechan;
+	if(gate != nil)
+		p = gate.p().i;
+	spawn chstatproc((hd args).x().i, dst, p, ws, mask, cflag);
+	return ref Value.X(dst);
+}
+
+chstatproc(src, dst: Fschan, gate: Gatechan, stat: Sys->Dir, mask: int, cflag: int)
+{
+	fsfilter->filter(ref Query(gate, stat, mask, cflag, chan of int), src, dst);
+	if(gate != nil)
+		gate <-= ((nil, nil, 0), nil);
+}
+
+Query.query(q: self ref Query, d: ref Sys->Dir, name: string, depth: int): int
+{
+	c := 1;
+	if(q.gate != nil){
+		q.gate <-= ((d, name, depth), q.reply);
+		c = <-q.reply;
+	}
+	if(c){
+		if(q.cflag){
+			m := d.mode & 8r700;
+			d.mode = (d.mode & ~8r77)|(m>>3)|(m>>6);
+		}
+		stat := q.stat;
+		d.mode = (d.mode & ~q.mask) | (stat.mode & q.mask);
+		if(stat.uid != nil)
+			d.uid = stat.uid;
+		if(stat.gid != nil)
+			d.gid = stat.gid;
+		if(stat.mtime != ~0)
+			d.mtime = stat.mtime;
+		if(stat.atime != ~0)
+			d.atime = stat.atime;
+	}
+	return 1;
+}
+
+# stolen from /appl/cmd/chmod.b
+User:	con 8r700;
+Group:	con 8r070;
+Other:	con 8r007;
+All:	con User | Group | Other;
+
+Read:	con 8r444;
+Write:	con 8r222;
+Exec:	con 8r111;
+parsemode(spec: string): (int, int, int)
+{
+	mask := Sys->DMAPPEND | Sys->DMEXCL | Sys->DMDIR | Sys->DMAUTH;
+loop:
+	for(i := 0; i < len spec; i++){
+		case spec[i] {
+		'u' =>
+			mask |= User;
+		'g' =>
+			mask |= Group;
+		'o' =>
+			mask |= Other;
+		'a' =>
+			mask |= All;
+		* =>
+			break loop;
+		}
+	}
+	if(i == len spec)
+		return (0, 0, 0);
+	if(i == 0)
+		mask |= All;
+
+	op := spec[i++];
+	if(op != '+' && op != '-' && op != '=')
+		return (0, 0, 0);
+
+	mode := 0;
+	for(; i < len spec; i++){
+		case spec[i]{
+		'r' =>
+			mode |= Read;
+		'w' =>
+			mode |= Write;
+		'x' =>
+			mode |= Exec;
+		'a' =>
+			mode |= Sys->DMAPPEND;
+		'l' =>
+			mode |= Sys->DMEXCL;
+		'd' =>
+			mode |= Sys->DMDIR;
+		'A' =>
+			mode |= Sys->DMAUTH;
+		* =>
+			return (0, 0, 0);
+		}
+	}
+	if(op == '+' || op == '-')
+		mask &= mode;
+	if(op == '-')
+		mode = ~mode;
+	return (1, mask, mode);
+}
--- /dev/null
+++ b/appl/cmd/fs/compose.b
@@ -1,0 +1,100 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Cmpchan,
+	Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+AinB:	con 1<<3;
+BinA:	con 1<<2;
+AoutB:	con 1<<1;
+BoutA:	con 1<<0;
+
+A:		con AinB|AoutB;
+AoverB:	con AinB|AoutB|BoutA;
+AatopB:	con AinB|BoutA;
+AxorB:	con AoutB|BoutA;
+
+B:		con BinA|BoutA;
+BoverA:	con BinA|BoutA|AoutB;
+BatopA:	con BinA|AoutB;
+BxorA:	con BoutA|AoutB;
+
+ops := array[] of {
+	AinB => "AinB",
+	BinA => "BinA",
+	AoutB => "AoutB",
+	BoutA => "BoutA",
+	A => "A",
+	AoverB => "AoverB",
+	AatopB => "AatopB",
+	AxorB => "AxorB",
+	B => "B",
+	BoverA => "BoverA",
+	BatopA => "BatopA",
+};
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+types(): string
+{
+	return "ms-d";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	c := chan of (ref Sys->Dir, ref Sys->Dir, chan of int);
+	s := (hd args).s().i;
+	for(i := 0; i < len ops; i++)
+		if(ops[i] == s)
+			break;
+	if(i == len ops){
+		sys->fprint(sys->fildes(2), "fs: join: bad op %q\n", s);
+		return nil;
+	}
+	spawn compose(c, i, opts != nil);
+	return ref Value.M(c);
+}
+
+compose(c: Cmpchan, op: int, dflag: int)
+{
+	t := array[4] of {* => 0};
+	if(op & AinB)
+		t[2r11] = 2r01;
+	if(op & BinA)
+		t[2r11] = 2r10;
+	if(op & AoutB)
+		t[2r01] = 2r01;
+	if(op & BoutA)
+		t[2r10] = 2r10;
+	if(dflag){
+		while(((d0, d1, reply) := <-c).t2 != nil){
+			x := (d1 != nil) << 1 | d0 != nil;
+			r := t[d0 != nil | (d1 != nil) << 1];
+			if(r == 0 && x == 2r11 && (d0.mode & d1.mode & Sys->DMDIR))
+				r = 2r11;
+			reply <-= r;
+		}
+	}else{
+		while(((d0, d1, reply) := <-c).t2 != nil)
+			reply <-= t[(d1 != nil) << 1 | d0 != nil];
+	}
+}
--- /dev/null
+++ b/appl/cmd/fs/depth.b
@@ -1,0 +1,49 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "ps";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	d := int (hd args).s().i;
+	if(d <= 0){
+		sys->fprint(sys->fildes(2), "fs: depth: invalid depth\n");
+		return nil;
+	}
+	c := chan of Gatequery;
+	spawn depthgate(c, d);
+	return ref Value.P(c);
+}
+
+depthgate(c: Gatechan, d: int)
+{
+	while((((dir, nil, depth), reply) := <-c).t0.t0 != nil)
+		reply <-= depth <= d;
+}
--- /dev/null
+++ b/appl/cmd/fs/entries.b
@@ -1,0 +1,86 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "tx";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	sc := Entrychan(chan of int, chan of Entry);
+	spawn entriesproc((hd args).x().i, sc);
+	return ref Value.T(sc);
+}
+
+entriesproc(c: Fschan, sc: Entrychan)
+{
+	if(<-sc.sync == 0){
+		(<-c).t1 <-= Quit;
+		exit;
+	}
+	indent := 0;
+	names: list of string;
+	name: string;
+loop:
+	for(;;){
+		(d, reply) := <-c;
+		if(d.dir != nil){
+			p: string;
+			depth := indent;
+			if(d.dir.mode & Sys->DMDIR){
+				names = name :: names;
+				if(indent == 0)
+					name = d.dir.name;
+				else{
+					if(name[len name - 1] != '/')
+						name[len name] = '/';
+					name += d.dir.name;
+				}
+				indent++;
+				reply <-= Down;
+				p = name;
+			}else{
+				p = name;
+				if(p[len p - 1] != '/')
+					p[len p] = '/';
+				p += d.dir.name;
+				reply <-= Next;
+			}
+			if(p != nil)
+				sc.c <-= (d.dir, p, depth);
+		}else{
+			reply <-= Next;
+			if(d.dir == nil && d.data == nil){
+				if(--indent == 0)
+					break loop;
+				(name, names) = (hd names, tl names);
+			}
+		}
+	}
+	sc.c <-= Nilentry;
+}
--- /dev/null
+++ b/appl/cmd/fs/eval.b
@@ -1,0 +1,648 @@
+implement Eval;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "readdir.m";
+#include "env.m";
+#	env: Env;
+#include "string.m";
+#	str: String;
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Quit: import Fslib;
+
+# more general:
+#	eval: fn[V, M](ctxt: ref Context, r: ref Report, expr: string, args:...) with {
+#		V =>
+#			typec:	fn(t: self V): int;
+#			cvt:		fn(t: self V, tc: int): V;
+#			cvt2s:	fn(t: self V): (int, string);
+#			cvt2v:	fn(t: self V): chan of int;
+#			mkstring:	fn(s: string): V;
+#			mkcmd:	fn(c: ref Sh->Cmd): V;
+#			discard:	fn(t: self V);
+#			type2s:	fn(c: int): string;
+#			loadmod:	fn(cmd: string): M;
+#		M =>
+#			types:	fn(): string;
+#			init:		fn();
+#			run:		fn(ctxt: ref Draw->Context, r: ref Report, cmd: string,
+#						opts: list of (int, list of V), args: list of V): V;
+#		}
+# how to call eval?
+# (eval with [V=>ref Value, M=>Fsmodule])(
+#
+# sort out error reporting; stderr is not good.
+
+
+# possible things to do:
+#	pipe [-1pP] [-t command] command fs -> void
+#		pipe all files in fs through command.
+#	extract [-r root] gate fs -> fs
+#		extract the first entry within fs which
+#		passes through the gate.
+#		if -r is specified, the entry is placed
+#		within the given root, and may be a file,
+#		otherwise files are not allowed.
+#	apply string fs
+#		for each file in fs, evaluates string as an fs expression
+#		(which should yield fs), and replace the file in the
+#		original hierarchy with the result.
+#		e.g.
+#		fs apply '{unbundle $file}' {filter {or {mode +d} *.bundle} .}
+#		a bit fanciful this...
+#	merge could take an optional boolean operator
+#
+#	venti?
+#
+#	Cmpgate: chan of Cmpgatequery;
+#	Cmpgatequery: type (Entry, Entry, chan of int);
+#		returns 00, 01, 10 or 11
+#	used by merge to decide what to do when merging
+#	used by write to decide what to do when writing
+#
+#	cmpdate [-u] '>'
+#	cmpquery command
+
+Eval: module {
+	types: fn(): string;
+	init:	fn();
+	run: fn(ctxt: ref Draw->Context, r: ref Fslib->Report,
+		opts: list of Fslib->Option, args: list of ref Fslib->Value): ref Fslib->Value;
+	eval: fn(ctxt: ref Draw->Context, r: ref Fslib->Report,
+		expr: string, args: list of ref Fslib->Value, ret: int): ref Fslib->Value;
+};
+
+WORD, SHCMD, VAR: con iota;
+
+Evalstate: adt {
+	s:	string;
+	spos: int;
+	drawctxt: ref Draw->Context;
+	report: ref Report;
+	args: array of ref Value;
+	verbose: int;
+
+	expr: fn(p: self ref Evalstate): ref Value;
+	getc: fn(p: self ref Evalstate): int;
+	ungetc: fn(p: self ref Evalstate);
+	gettok: fn(p: self ref Evalstate): (int, string);
+};
+
+ops: list of (string, Fsmodule);
+lock: chan of int;
+
+# to do:
+# - change value letters to more appropriate (e.g. fs->f, entries->e, gate->g).
+# - allow shell $variable expansions
+
+types(): string
+{
+	return "as-v";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: eval: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	fslib->init();
+#	env = load Env Env->PATH;
+#	if(env == nil)
+#		badmod(Env->PATH);
+#	str = load String String->PATH;
+#	if(str == nil)
+#		badmod(String->PATH);
+	lock = chan[1] of int;
+}
+
+run(ctxt: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	return (ref Evalstate((hd args).s().i, 0, ctxt, report, nil, opts != nil)).expr();
+}
+
+eval(ctxt: ref Draw->Context, report: ref Report,
+	expr: string, args: list of ref Value, rtype: int): ref Value
+{
+	a := array[len args] of ref Value;
+	for(i := 0; args != nil; args = tl args)
+		a[i++] = hd args;
+	e := ref Evalstate(expr, 0, ctxt, report, a, 0);
+	v := e.expr();
+	vl: list of ref Value;
+	for(i = 0; i < len a; i++)
+		if(a[i] != nil)
+			vl = a[i] :: vl;
+	nv := cvt(e, v, rtype);
+	if(nv == nil){
+		vl = v :: vl;
+		sys->fprint(stderr(), "fs: eval fn: %s cannot be converted to %s\n",
+			type2s(v.typec()), type2s(rtype));
+	}
+	if(vl != nil)
+		spawn discard(nil, vl);
+	return nv;
+}
+
+tok2s(t: int, s: string): string
+{
+	case t {
+	WORD =>
+		return s;
+	SHCMD =>
+		return "@";
+	VAR =>
+		return "$" + s;
+	}
+	return sys->sprint("%c", t);
+}
+
+# expr: WORD exprs
+# exprs:
+#	| exprs '{' expr '}'
+#	| exprs WORD
+#	| exprs SHCMD
+#	| exprs VAR
+Evalstate.expr(p: self ref Evalstate): ref Value
+{
+	args: list of ref Value;
+	t: int;
+	s: string;
+	{
+		(t, s) = p.gettok();
+	} exception e {
+	"parse error" =>
+		return nil;
+	}
+	if(t != WORD){
+		sys->fprint(stderr(), "fs: eval: syntax error (char %d), expected word, found %#q\n",
+				p.spos, tok2s(t, s));
+		return nil;
+	}
+	cmd := s;
+loop:
+	for(;;){
+		{
+			(t, s) = p.gettok();
+		} exception e {
+		"parse error" =>
+			spawn discard(nil, args);
+			return nil;
+		}
+		case t {
+		'{' =>
+			v := p.expr();
+			if(v == nil){
+				spawn discard(nil, args);
+				return nil;
+			}
+			args = v :: args;
+		'}' =>
+			break loop;
+		WORD =>
+			args = ref Value.S(s) :: args;
+		VAR =>
+			n := int s;
+			if(n < 0 || n >= len p.args){
+				sys->fprint(stderr(), "fs: eval: invalid arg reference $%s\n", s);
+				spawn discard(nil, args);
+				return nil;
+			}
+			if(p.args[n] == nil){
+				sys->fprint(stderr(), "fs: eval: cannot use $%d twice\n", n);
+				spawn discard(nil, args);
+				return nil;
+			}
+			args = p.args[n] :: args;
+			p.args[n] = nil;
+		SHCMD =>
+			if(sh == nil && (sh = load Sh Sh->PATH) == nil){
+				sys->fprint(stderr(), "fs: eval: cannot load %s: %r\n", Sh->PATH);
+				spawn discard(nil, args);
+				return nil;
+			}
+			(c, err) := sh->parse(s);
+			if(c == nil){
+				sys->fprint(stderr(), "fs: eval: cannot parse shell command @%s: %s\n", s, err);
+				spawn discard(nil, args);
+				return nil;
+			}
+			args = ref Value.C(c) :: args;
+		-1 =>
+			break loop;
+		* =>
+			spawn discard(nil, args);
+			sys->fprint(stderr(), "fs: eval: syntax error; unexpected token %d before char %d\n", t, p.spos);
+			return nil;
+		}
+	}
+	return runcmd(p, cmd, rev(args));
+}
+
+runcmd(p: ref Evalstate, cmd: string, args: list of ref Value): ref Value
+{
+	m := loadmodule(cmd);
+	if(m == nil){
+		spawn discard(nil, args);
+		return nil;
+	}
+	otype := m->types();
+	ok: int;
+	opts: list of Option;
+	(ok, opts, args) = cvtargs(p, args, cmd, otype);
+	if(ok == -1){
+		sys->fprint(stderr(), "fs: eval: usage: %s\n", fslib->cmdusage(cmd, otype));
+		spawn discard(opts, args);
+		return nil;
+	}
+	r := m->run(p.drawctxt, p.report, opts, args);
+	if(r == nil)
+		spawn discard(opts, args);
+	return r;
+}
+
+cvtargs(e: ref Evalstate, args: list of ref Value, cmd, otype: string): (int, list of Option, list of ref Value)
+{
+	ok: int;
+	opts: list of Option;
+	(nil, at, t) := fslib->splittype(otype);
+	(ok, opts, args) = cvtopts(e, t, cmd, args);
+	if(ok == -1)
+		return (-1, opts, args);
+	if(len at < 1 || at[0] == '*'){
+		sys->fprint(stderr(), "fs: eval: invalid type descriptor %#q for %#q\n", at, cmd);
+		return (-1, opts, args);
+	}
+	n := len args;
+	if(at[len at - 1] == '*'){
+		tc := at[len at - 2];
+		at = at[0:len at - 2];
+		for(i := len at; i < n; i++)
+			at[i] = tc;
+	}
+	if(n != len at){
+		sys->fprint(stderr(), "fs: eval: wrong number of arguments to %#q\n", cmd);
+		return (-1, opts, args);
+	}
+	d: list of ref Value;
+	(ok, args, d) = cvtvalues(e, at, cmd, args);
+	if(ok == -1)
+		args = join(args, d);
+	return (ok, opts, args);
+}
+
+cvtvalues(e: ref Evalstate, t: string, cmd: string, args: list of ref Value): (int, list of ref Value, list of ref Value)
+{
+	cargs: list of ref Value;
+	for(i := 0; i < len t; i++){
+		tc := t[i];
+		if(args == nil){
+			sys->fprint(stderr(), "fs: eval: %q missing argument of type %s\n", cmd, type2s(tc));
+			return (-1, cargs, args);
+		}
+		v := cvt(e, hd args, tc);
+		if(v == nil){
+			sys->fprint(stderr(), "fs: eval: %q: %s cannot be converted to %s\n",
+				cmd, type2s((hd args).typec()), type2s(tc));
+			return (-1, cargs, args);
+		}
+		cargs = v :: cargs;
+		args = tl args;
+	}
+	return (0, rev(cargs), args);
+}
+
+cvtopts(e: ref Evalstate, opttype: string, cmd: string, args: list of ref Value): (int, list of Option, list of ref Value)
+{
+	if(opttype == nil)
+		return (0, nil, args);
+	opts: list of Option;
+getopts:
+	while(args != nil){
+		s := "";
+		pick v := hd args {
+		S =>
+			s = v.i;
+			if(s == nil || s[0] != '-' || len s == 1)
+				s = nil;
+			else if(s == "--"){
+				args = tl args;
+				s = nil;
+			}
+		}
+		if(s == nil)
+			return (0, opts, args);
+		s = s[1:];
+		while(len s > 0){
+			opt := s[0];
+			if(((ok, t) := fslib->opttypes(opt, opttype)).t0 == -1){
+				sys->fprint(stderr(), "fs: eval: %s: unknown option -%c\n", cmd, opt);
+				return (-1, opts, args);
+			}
+			if(t == nil){
+				s = s[1:];
+				opts = (opt, nil) :: opts;
+			}else{
+				if(len s > 1)
+					args = ref Value.S(s[1:]) :: tl args;
+				else
+					args = tl args;
+				vl: list of ref Value;
+				(ok, vl, args) = cvtvalues(e, t, cmd, args);
+				if(ok == -1)
+					return (-1, opts, join(vl, args));
+				opts = (opt, vl) :: opts;
+				continue getopts;
+			}
+		}
+		args = tl args;
+	}
+	return (0, opts, args);
+}
+
+discard(ol: list of (int, list of ref Value), vl: list of ref Value)
+{
+	for(; ol != nil; ol = tl ol)
+		for(ovl := (hd ol).t1; ovl != nil; ovl = tl ovl)
+			vl = (hd ovl) :: vl;
+	for(; vl != nil; vl = tl vl)
+		(hd vl).discard();
+}
+
+loadmodule(cmd: string): Fsmodule
+{
+	lock <-= 0;
+	for(ol := ops; ol != nil; ol = tl ol)
+		if((hd ol).t0 == cmd)
+			break;
+	if(ol != nil){
+		<-lock;
+		return (hd ol).t1;
+	}
+	p := cmd + ".dis";
+	if(p[0] != '/' && !(p[0] == '.' && p[1] == '/'))
+		p = "/dis/fs/" + p;
+	m := load Fsmodule p;
+	if(m == nil){
+		sys->fprint(stderr(), "fs: eval: cannot load %s: %r\n", p);
+		sys->fprint(stderr(), "fs: eval: unknown verb %#q\n", cmd);
+		sys->werrstr(sys->sprint("cannot load module %q", cmd));
+		<-lock;
+		return nil;
+	}
+	{
+		m->init();
+	} exception e {
+	"fail:*" =>
+		<-lock;
+		sys->werrstr(sys->sprint("module init failed: %s", e[5:]));
+		return nil;
+	}
+	ops = (cmd, m) :: ops;
+	<-lock;
+	return m;
+}
+
+runexternal(p: ref Evalstate, cmd: string, t: string, opts: list of Option, args: list of ref Value): ref Value
+{
+	m := loadmodule(cmd);
+	if(m == nil)
+		return nil;
+	if(!fslib->typecompat(t, m->types())){
+		sys->fprint(stderr(), "fs: eval: %s has incompatible type\n", cmd);
+		sys->fprint(stderr(), "fs: eval: expected usage: %s\n", fslib->cmdusage(cmd, t));
+		sys->fprint(stderr(), "fs: eval: actually usage: %s\n", fslib->cmdusage(cmd, m->types()));
+		return nil;
+	}
+	return m->run(p.drawctxt, p.report, opts, args);
+}
+
+cvt(e: ref Evalstate, v: ref Value, t: int): ref Value
+{
+	{
+		return cvt1(e, v, t);
+	} exception {
+	"type conversion" =>
+		return nil;
+	}
+}
+
+cvt1(e: ref Evalstate, v: ref Value, t: int): ref Value
+{
+	if(v.typec() == t)
+		return v;
+	r: ref Value;
+	case t {
+	't' =>
+		r = runexternal(e, "entries", "tx", nil, cvt1(e, v, 'x') :: nil);
+	'x' =>
+		r = runexternal(e, "walk", "xs", nil, cvt1(e, v, 's') :: nil);
+	'p' =>
+		r = runexternal(e, "match", "ps", nil, cvt1(e, v, 's') :: nil);
+	's' =>
+		r = runexternal(e, "run", "sc", nil, cvt1(e, v, 'c') :: nil);
+	'v' =>
+		r = runexternal(e, "print", "vt", nil, cvt1(e, v, 't') :: nil);
+	}
+	if(r == nil)
+		raise "type conversion";
+	return r;
+}
+
+Evalstate.getc(p: self ref Evalstate): int
+{
+	c := -1;
+	if(p.spos < len p.s)
+		c = p.s[p.spos];
+	p.spos++;
+	return c;
+}
+
+Evalstate.ungetc(p: self ref Evalstate)
+{
+	p.spos--;
+}
+
+# XXX backslash escapes newline?
+Evalstate.gettok(p: self ref Evalstate): (int, string)
+{
+	while ((c := p.getc()) == ' ' || c == '\t')
+		;
+	t: int;
+	s: string;
+
+	case c {
+	-1 =>
+		t = -1;
+	'\n' =>
+		t = '\n';
+	'{' =>
+		t = '{';
+	'}' =>
+		t = '}';
+	'@' =>		# embedded shell command
+		while((nc := p.getc()) == ' ' || nc == '\t')
+			;
+		if(nc != '{'){
+			sys->fprint(stderr(), "fs: eval: expected '{' after '@'\n");
+			raise "parse error";
+		}
+		s = "{";
+		d := 1;
+	getcmd:
+		while((nc = p.getc()) != -1){
+			s[len s] = nc;
+			case nc {
+			'{' =>
+				d++;
+			'}' =>
+				if(--d == 0)
+					break getcmd;
+			'\'' =>
+				s += getqword(p, 1);
+			}
+		}
+		if(nc == -1){
+			sys->fprint(stderr(), "fs: eval: unbalanced '{' in shell command\n");
+			raise "parse error";
+		}
+		t = SHCMD;
+	'$' =>
+		t = VAR;
+		s = getvar(p);
+	'\'' =>
+		s = getqword(p, 0);
+		t = WORD;
+	* =>
+		do {
+			s[len s] = c;
+			c = p.getc();
+			if (in(c, " \t{}\n")){
+				p.ungetc();
+				break;
+			}
+		} while (c >= 0);
+		t = WORD;
+	}
+	return (t, s);
+}
+
+getvar(p: ref Evalstate): string
+{
+	c := p.getc();
+	if(c == -1){
+		sys->fprint(stderr(), "fs: eval: unexpected eof after '$'\n");
+		raise "parse error";
+	}
+	v: string;
+	while(in(c, " \t\n@{}'") == 0){
+		v[len v] = c;
+		c = p.getc();
+	}
+	p.ungetc();
+	for(i := 0; i < len v; i++)
+		if(v[i] < '0' || v[i] > '9')
+			break;
+	if(i < len v || v == nil){
+		sys->fprint(stderr(), "fs: eval: invalid $ reference $%q\n", v);
+		raise "parse error";
+	}
+	return v;
+}
+#	v: string;
+#	if(c == '\''){
+#		v = getqword(p, 0);
+#		c = p.getc();
+#	} else{
+#		v[0] = c;
+#		while((c = p.getc()) != -1){
+#			if(in(c, "a-zA-Z0-9*_") == 0)		# heuristic stolen from rc
+#				break;
+#			v[len v] = c;
+#		}
+#	}
+#	vl := str->unquoted(env->getenv(v));
+#	if(vl == nil){
+#		sys->fprint(stderr(), "fs: eval: shell variable $%q has %d elements\n", v, len vl);
+#		raise "parse error";
+#	}
+#	val := hd vl;
+#	if(c == -1	|| in(c, " \t@{}\n")){
+#		p.ungetc();
+#		return (WORD, val);
+#	}
+#	(t, s) = p.gettok();
+#	if(t != WORD){
+#		sys->fprint(stderr(), "fs: eval: expected word after $%q\n", v);
+#		raise "parse error";
+#	}
+#	s = val + s;
+#}
+
+in(c: int, s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return 1;
+	return 0;
+}
+
+# get a quoted word; the starting quote has already been seen
+getqword(p: ref Evalstate, keepq: int): string
+{
+	s := "";
+	for(;;) {
+		while ((nc := p.getc()) != '\'' && nc >= 0)
+			s[len s] = nc;
+		if (nc == -1){
+			sys->fprint(stderr(), "fs: eval: unterminated quote\n");
+			raise "parse error";
+		}
+		if (p.getc() != '\'') {
+			p.ungetc();
+			if(keepq)
+				s[len s] = '\'';
+			return s;
+		}
+		s[len s] = '\'';	# 'xxx''yyy' becomes WORD(xxx'yyy)
+		if(keepq)
+			s[len s] = '\'';
+	}
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
+
+# join x to y, leaving result in arbitrary order.
+join[T](x, y: list of T): list of T
+{
+	if(len x > len y)
+		(x, y) = (y, x);
+	for(; x != nil; x = tl x)
+		y = hd x :: y;
+	return y;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/cmd/fs/exec.b
@@ -1,0 +1,162 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "fslib.m";
+	fslib: Fslib;
+	Option, Value, Entrychan, Report: import fslib;
+
+# usage: exec [-n nfiles] [-t endcmd] [-pP] command entries
+types(): string
+{
+	return "vct-ns-tc-p-P";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: exec: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		badmod(Sh->PATH);
+	sh->initialise();
+}
+
+run(drawctxt: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	n := 1;
+	pflag := 0;
+	tcmd: ref Sh->Cmd;
+	for(; opts != nil; opts = tl opts){
+		o := hd opts;
+		case o.opt {
+		'n' =>
+			if((n = int (hd o.args).s().i) <= 0){
+				sys->fprint(sys->fildes(2), "fs: exec: invalid argument to -n\n");
+				return nil;
+			}
+		't' =>
+			tcmd = (hd o.args).c().i;
+		'p' =>
+			pflag = 1;
+		'P' =>
+			pflag = 2;
+		}
+	}
+	if(pflag && n > 1){
+		sys->fprint(sys->fildes(2), "fs: exec: cannot specify -p with -n %d\n", n);
+		return nil;
+	}
+	cmd := (hd args).c().i;
+	c := (hd tl args).t().i;
+	sync := chan of int;
+	spawn execproc(drawctxt, sync, n, pflag, c, cmd, tcmd, report.start("exec"));
+	sync <-= 1;
+	return ref Value.V(sync);
+}
+
+execproc(drawctxt: ref Draw->Context, sync: chan of int, n, pflag: int,
+		c: Entrychan, cmd, tcmd: ref Sh->Cmd, errorc: chan of string)
+{
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	ctxt := Context.new(drawctxt);
+	<-sync;
+	if(<-sync == 0){
+		c.sync <-= 0;
+		errorc <-= nil;
+		exit;
+	}
+	c.sync <-= 1;
+	argv := ref Sh->Listnode(cmd, nil) :: nil;
+
+	fl: list of ref Sh->Listnode;
+	nf := 0;
+	while(((d, p, nil) := <-c.c).t0 != nil){
+		fl = ref Sh->Listnode(nil, p) :: fl;
+		if(++nf >= n){
+			ctxt.set("file", rev(fl));
+			if(pflag)
+				setstatenv(ctxt, d, pflag);
+			fl = nil;
+			nf = 0;
+			{ctxt.run(argv, 0);} exception {"fail:*" =>;}
+		}
+	}
+	if(nf > 0){
+		ctxt.set("file", rev(fl));
+		{ctxt.run(argv, 0);} exception {"fail:*" =>;}
+	}
+	if(tcmd != nil){
+		ctxt.set("file", nil);
+		{ctxt.run(ref Sh->Listnode(tcmd, nil) :: nil, 0);} exception {"fail:*" =>;}
+	}
+	errorc <-= nil;
+}
+
+setenv(ctxt: ref Context, var: string, val: list of string)
+{
+	ctxt.set(var, sh->stringlist2list(val));
+}
+
+setstatenv(ctxt: ref Context, dir: ref Sys->Dir, pflag: int)
+{
+	setenv(ctxt, "mode", modes(dir.mode) :: nil);
+	setenv(ctxt, "uid", dir.uid :: nil);
+	setenv(ctxt, "mtime", string dir.mtime :: nil);
+	setenv(ctxt, "length", string dir.length :: nil);
+
+	if(pflag > 1){
+		setenv(ctxt, "name", dir.name :: nil);
+		setenv(ctxt, "gid", dir.gid :: nil);
+		setenv(ctxt, "muid", dir.muid :: nil);
+		setenv(ctxt, "qid", sys->sprint("16r%ubx", dir.qid.path) :: string dir.qid.vers :: nil);
+		setenv(ctxt, "atime", string dir.atime :: nil);
+		setenv(ctxt, "dtype", sys->sprint("%c", dir.dtype) :: nil);
+		setenv(ctxt, "dev", string dir.dev :: nil);
+	}
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
--- /dev/null
+++ b/appl/cmd/fs/filter.b
@@ -1,0 +1,64 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fsfilter: Fsfilter;
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+
+Query: adt {
+	gate: Gatechan;
+	dflag: int;
+	reply: chan of int;
+	query: fn(q: self ref Query, d: ref Sys->Dir, name: string, depth: int): int;
+};
+
+types(): string
+{
+	return "xpx-d";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	fsfilter = load Fsfilter Fsfilter->PATH;
+	if(fsfilter == nil)
+		badmod(Fsfilter->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	dst := chan of (Fsdata, chan of int);
+	spawn filterproc((hd tl args).x().i, dst, (hd args).p().i, opts != nil);
+	return ref Value.X(dst);
+}
+
+filterproc(src, dst: Fschan, gate: Gatechan, dflag: int)
+{
+	fsfilter->filter(ref Query(gate, dflag, chan of int), src, dst);
+	gate <-= ((nil, nil, 0), nil);
+}
+
+Query.query(q: self ref Query, d: ref Sys->Dir, name: string, depth: int): int
+{
+	if(depth == 0 || (q.dflag && (d.mode & Sys->DMDIR)))
+		return 1;
+	q.gate <-= ((d, name, depth), q.reply);
+	return <-q.reply;
+}
--- /dev/null
+++ b/appl/cmd/fs/ls.b
@@ -1,0 +1,97 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "daytime.m";
+	daytime: Daytime;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Option, Value, Entrychan, Report: import fslib;
+
+types(): string
+{
+	return "vt-u-m";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: ls: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		badmod(Daytime->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	sync := chan of int;
+	spawn lsproc(sync, opts, (hd args).t().i, daytime, report.start("ls"));
+	return ref Value.V(sync);
+}
+
+lsproc(sync: chan of int, opts: list of Option, c: Entrychan, daytime: Daytime, errorc: chan of string)
+{
+	now := daytime->now();
+	mflag := uflag := 0;
+	if(<-sync == 0){
+		c.sync <-= 0;
+		errorc <-= nil;
+	}
+	c.sync <-= 1;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).opt {
+		'm' =>
+			mflag = 1;
+		'u' =>
+			uflag = 1;
+		}
+	}
+	while(((dir, p, nil) := <-c.c).t0 != nil){
+		t := dir.mtime;
+		if(uflag)
+			t = dir.atime;
+		s := sys->sprint("%s %c %d %s %s %bud %s %s\n",
+			modes(dir.mode), dir.dtype, dir.dev,
+			dir.uid, dir.gid, dir.length,
+			daytime->filet(now, dir.mtime), p);
+		if(mflag)
+			s = "[" + dir.muid + "] " + s;
+		sys->print("%s", s);
+	}
+	errorc <-= nil;
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/fs/match.b
@@ -1,0 +1,79 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "filepat.m";
+	filepat: Filepat;
+include "regex.m";
+	regex: Regex;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "ps-a-r";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	regex = load Regex Regex->PATH;
+	if(regex == nil)
+		badmod(Regex->PATH);
+	filepat = load Filepat Filepat->PATH;
+	if(filepat == nil)
+		badmod(Filepat->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	pat := (hd args).s().i;
+	aflag := rflag := 0;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).opt {
+		'a' =>
+			aflag = 1;
+		'r' =>
+			rflag = 1;
+		}
+	}
+	v := ref Value.P(chan of Gatequery);
+	re: Regex->Re;
+	if(rflag){
+		err: string;
+		(re, err) = regex->compile(pat, 0);
+		if(re == nil){
+			sys->fprint(sys->fildes(2), "fs: match: regex error on %#q: %s\n", pat, err);
+			return nil;
+		}
+	}
+	spawn matchproc(v.i, aflag, pat, re);
+	return v;
+}
+
+matchproc(c: Gatechan, all: int, pat: string, re: Regex->Re)
+{
+	while((((d, name, nil), reply) := <-c).t0.t0 != nil){
+		if(all == 0)
+			name = d.name;
+		if(re != nil)
+			reply <-= regex->execute(re, name) != nil;		# XXX should anchor it?
+		else
+			reply <-= filepat->match(pat, name);
+	}
+}
--- /dev/null
+++ b/appl/cmd/fs/merge.b
@@ -1,0 +1,187 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s: import fslib;
+	Fschan, Fsdata, Entrychan, Cmpchan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+# e.g....
+# fs select {mode -d} {merge -c {compose -d AoutB} {filter {not {path /chan /dev /usr/rog /n/local /net}} /} {merge {proto FreeBSD} {proto Hp} {proto Irix} {proto Linux} {proto MacOSX} {proto Nt} {proto Nt.ti} {proto Nt.ti925} {proto Plan9} {proto Plan9.ti} {proto Plan9.ti925} {proto Solaris} {proto authsrv} {proto dl} {proto dlsrc} {proto ep7} {proto inferno} {proto inferno.ti} {proto ipaqfs} {proto minitel} {proto os} {proto scheduler.client} {proto scheduler.server} {proto sds} {proto src} {proto src.ti} {proto sword} {proto ti925.ti} {proto ti925bin} {proto tipaq} {proto umec} {proto utils} {proto utils.ti}}} >[2] /dev/null
+
+types(): string
+{
+	return "xxxx*-1-cm";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil){
+		sys->fprint(sys->fildes(2), "fs: cannot load %s: %r\n", Fslib->PATH);
+		raise "fail:bad module";
+	}
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	recurse := 1;
+	cmp: Cmpchan;
+	for(; opts != nil; opts = tl opts){
+		case (hd opts).opt {
+		'1' =>
+			recurse = 0;
+		'c' =>
+			cmp = (hd (hd opts).args).m().i;
+		}
+	}
+	dst := chan of (Fsdata, chan of int);
+	spawn mergeproc((hd args).x().i, (hd tl args).x().i, dst, recurse, cmp, tl tl args == nil);
+	for(args = tl tl args; args != nil; args = tl args){
+		dst1 := chan of (Fsdata, chan of int);
+		spawn mergeproc(dst, (hd args).x().i, dst1, recurse, cmp, tl args == nil);
+		dst = dst1;
+	}
+	return ref Value.X(dst);
+}
+
+# merge two trees; assume directories are alphabetically sorted.
+mergeproc(c0, c1, dst: Fschan, recurse: int, cmp: Cmpchan, killcmp: int)
+{
+	myreply := chan of int;
+	((d0, nil), reply0) := <-c0;
+	((d1, nil), reply1) := <-c1;
+
+	if(compare(cmp, d0, d1) == 2r10)
+		dst <-= ((d1, nil), myreply);
+	else
+		dst <-= ((d0, nil), myreply);
+	r := <-myreply;
+	reply0 <-= r;
+	reply1 <-= r;
+	if(r == Down){
+		{
+			mergedir(c0, c1, dst, recurse, cmp);
+		} exception {"exit" =>;}
+	}
+	if(cmp != nil && killcmp)
+		cmp <-= (nil, nil, nil);
+}
+
+mergedir(c0, c1, dst: Fschan, recurse: int, cmp: Cmpchan)
+{
+	myreply := chan of int;
+	reply0, reply1: chan of int;
+	d0, d1: ref Sys->Dir;
+	eof0 := eof1 := 0;
+	for(;;){
+		if(!eof0 && d0 == nil){
+			((d0, nil), reply0) = <-c0;
+			if(d0 == nil){
+				reply0 <-= Next;
+				eof0 = 1;
+			}
+		}
+		if(!eof1 && d1 == nil){
+			((d1, nil), reply1) = <-c1;
+			if(d1 == nil){
+				reply1 <-= Next;
+				eof1 = 1;
+			}
+		}
+		if(eof0 && eof1)
+			break;
+
+		(wd0, wd1) := (d0, d1);
+		if(d0 != nil && d1 != nil && d0.name != d1.name){
+			if(d0.name < d1.name)
+				wd1 = nil;
+			else
+				wd0 = nil;
+		}
+
+		wc0, wc1: Fschan;
+		wreply0, wreply1: chan of int;
+		weof0, weof1: int;
+
+		c := compare(cmp, wd0, wd1);
+		if(wd0 != nil && wd1 != nil){
+			if(c != 0 && recurse && (wd0.mode & wd1.mode & Sys->DMDIR) != 0){
+				dst <-= ((wd0, nil), myreply);
+				r := <-myreply;
+				reply0 <-= r;
+				reply1 <-= r;
+				d0 = d1 = nil;
+				case r {
+				Quit =>
+					raise "exit";
+				Skip =>
+					return;
+				Down =>
+					mergedir(c0, c1, dst, 1, cmp);
+				}
+				continue;
+			}
+			# when we can't merge and there's a clash, choose c0 over c1, unless cmp says otherwise
+			if(c == 2r10){
+				reply0 <-= Next;
+				d0 = nil;
+			}else{
+				reply1 <-= Next;
+				d1 = nil;
+			}
+		}
+		if(c & 2r01){
+			(wd0, wc0, wreply0, weof0) = (d0, c0, reply0, eof0);
+			(wd1, wc1, wreply1, weof1) = (d1, c1, reply1, eof1);
+			d0 = nil;
+		}else if(c & 2r10){
+			(wd0, wc0, wreply0, weof0) = (d1, c1, reply1, eof1);
+			(wd1, wc1, wreply1, weof1) = (d0, c0, reply0, eof0);
+			d1 = nil;
+		}else{
+			if(wd0 == nil){
+				reply1 <-= Next;
+				d1 = nil;
+			}else{
+				reply0 <-= Next;
+				d0 = nil;
+			}
+			continue;
+		}
+		dst <-= ((wd0, nil), myreply);
+		r := <-myreply;
+		wreply0 <-= r;
+		if(r == Down)
+			r = fslib->copy(wc0, dst);		# XXX hmm, maybe this should be a mergedir()
+		case r {
+		Quit or
+		Skip =>
+			if(wd1 == nil && !weof1)
+				(nil, wreply1) = <-wc1;
+			wreply1 <-= r;
+			if(r == Quit)
+				raise "exit";
+			return;
+		}
+	}
+	dst <-= ((nil, nil), myreply);
+	if(<-myreply == Quit)
+		raise "exit";
+}
+
+compare(cmp: Cmpchan, d0, d1: ref Sys->Dir): int
+{
+	mask := (d0 != nil) | (d1 != nil) << 1;
+	if(cmp == nil)
+		return mask;
+	reply := chan of int;
+	cmp <-= (d0, d1, reply);
+	return <-reply & mask;
+}
--- /dev/null
+++ b/appl/cmd/fs/mergewrite.b
@@ -1,0 +1,186 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "readdir.m";
+	readdir: Readdir;
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, quit, report: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Cmpchan, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "vmsx";			# XXX bad argument ordering...
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil){
+		sys->fprint(sys->fildes(2), "fs: mergewrite: cannot load %s: %r\n", Readdir->PATH);
+		raise "fail:bad module";
+	}
+	readdir->init(nil, 0);
+
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil){
+		sys->fprint(sys->fildes(2), "fs: mergewrite: cannot load %s: %r\n", Fslib->PATH);
+		raise "fail:bad module";
+	}
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	sync := chan of int;
+	spawn  fswriteproc(sync, (hd args).m().i, (hd tl args).s().i, (hd tl tl args).x().i, report.start("mergewrite"));
+	<-sync;
+	return ref Value.V(sync);
+}
+
+fswriteproc(sync: chan of int, cmp: Cmpchan, root: string, c: Fschan, errorc: chan of string)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+	if(<-sync == 0){
+		(<-c).t1 <-= Quit;
+		quit(errorc);
+	}
+		
+	((d, nil), reply) := <-c;
+	if(root != nil){
+		d = ref *d;
+		d.name = root;
+	}
+	fswritedir(d.name, cmp, d, reply, c, errorc);
+	quit(errorc);
+}
+
+fswritedir(path: string, cmp: Cmpchan, dir: ref Sys->Dir, dreply: chan of int, c: Fschan, errorc: chan of string)
+{
+	fd: ref Sys->FD;
+	if(dir.mode & Sys->DMDIR){
+		fd = sys->create(dir.name, Sys->OREAD, dir.mode|8r300);
+		made := fd != nil;
+		if(fd == nil && (fd = sys->open(dir.name, Sys->OREAD)) == nil){
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot create %q, mode %uo: %r", path, dir.mode|8r300));
+			return;
+		}
+		# XXX if we haven't just made it, we should chmod the old entry u+w to enable writing.
+		if(sys->chdir(dir.name) == -1){		# XXX beware of names starting with '#'
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot cd to %q: %r", path));
+			fd = nil;
+			sys->remove(dir.name);
+			return;
+		}
+		dreply <-= Down;
+		entries: array of ref Sys->Dir;
+		if(made == 0)
+			entries = readdir->readall(fd, Readdir->NAME|Readdir->COMPACT).t0;
+		i := 0;
+		eod := 0;
+		d0, d1: ref Sys->Dir;
+		reply: chan of int;
+		path[len path] = '/';
+		for(;;){
+			if(!eod && d0 == nil){
+				((d0, nil), reply) = <-c;
+				if(d0 == nil){
+					reply <-= Next;
+					eod = 1;
+				}
+			}
+			if(d1 == nil && i < len entries)
+				d1 = entries[i++];
+			if(d0 == nil && d1 == nil)
+				break;
+
+			(wd0, wd1) := (d0, d1);
+			if(d0 != nil && d1 != nil && d0.name != d1.name){
+				if(d0.name < d1.name)
+					wd1 = nil;
+				else
+					wd0 = nil;
+			}
+			r := compare(cmp, wd0, wd1);
+			if(wd1 != nil && (r & 2r10) == 0){
+				if(wd1.mode & Sys->DMDIR)
+					rmdir(wd1.name);
+				else
+					remove(wd1.name);
+				d1 = nil;
+			}
+			if(wd0 != nil){
+				if((r & 2r01) == 0)
+					reply <-= Next;
+				else
+					fswritedir(path + wd0.name, cmp, d0, reply, c, errorc);
+				d0 = nil;
+			}
+		}
+		sys->chdir("..");
+		if((dir.mode & 8r300) != 8r300){
+			ws := Sys->nulldir;
+			ws.mode = dir.mode;
+			if(sys->fwstat(fd, ws) == -1)
+				report(errorc, sys->sprint("cannot wstat %q: %r", path));
+		}
+	}else{
+		fd = sys->create(dir.name, Sys->OWRITE, dir.mode);
+		if(fd == nil){
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot create %q, mode %uo: %r", path, dir.mode|8r300));
+			return;
+		}
+		dreply <-= Down;
+		while((((nil, buf), reply) := <-c).t0.data != nil){
+			nw := sys->write(fd, buf, len buf);
+			if(nw < len buf){
+				if(nw == -1)
+					errorc <-= sys->sprint("error writing %q: %r", path);
+				else
+					errorc <-= sys->sprint("short write");
+				reply <-= Skip;
+				break;
+			}
+			reply <-= Next;
+		}
+		reply <-= Next;
+	}
+}
+
+rmdir(name: string)
+{
+	(d, n) := readdir->init(name, Readdir->NONE|Readdir->COMPACT);
+	for(i := 0; i < n; i++){
+		path := name+"/"+d[i].name;
+		if(d[i].mode & Sys->DMDIR)
+			rmdir(path);
+		else
+			remove(path);
+	}
+	remove(name);
+}
+
+remove(name: string)
+{
+	if(sys->remove(name) < 0)
+		sys->fprint(sys->fildes(2), "mergewrite: cannot remove %q: %r\n", name);
+}
+
+compare(cmp: Cmpchan, d0, d1: ref Sys->Dir): int
+{
+	mask := (d0 != nil) | (d1 != nil) << 1;
+	if(cmp == nil)
+		return mask;
+	reply := chan of int;
+	cmp <-= (d0, d1, reply);
+	return <-reply & mask;
+}
--- /dev/null
+++ b/appl/cmd/fs/mkfile
@@ -1,0 +1,60 @@
+<../../../mkconfig
+# fs write /n/local/n/fossil/usr/inferno {filter {and {not {or *.dis *.sbl}} {path /appl/cmd/fs /module/fslib.m /appl/lib/fslib.b /appl/cmd/fs.b /man/1/fs}} /}
+TARG=\
+	and.dis\
+	bundle.dis\
+	chstat.dis\
+	compose.dis\
+	depth.dis\
+	entries.dis\
+	eval.dis\
+	exec.dis\
+	filter.dis\
+	ls.dis\
+	match.dis\
+	merge.dis\
+	mergewrite.dis\
+	mode.dis\
+	not.dis\
+	or.dis\
+	path.dis\
+	pipe.dis\
+	print.dis\
+	proto.dis\
+	query.dis\
+	run.dis\
+	select.dis\
+	setroot.dis\
+	size.dis\
+	unbundle.dis\
+	walk.dis\
+	write.dis\
+	void.dis\
+
+
+INS=	${TARG:%=$ROOT/dis/fs/%}
+
+SYSMODULES=\
+	bufio.m\
+	draw.m\
+	sh.m\
+	sys.m\
+	bundle.m\
+	fslib.m\
+
+DISBIN=$ROOT/dis/fs
+
+<$ROOT/mkfiles/mkdis
+
+all:V:		$TARG
+
+install:V:	$INS
+
+nuke:V: clean
+	rm -f $INS
+
+clean:V:
+	rm -f *.dis *.sbl
+
+uninstall:V:
+	rm -f $INS
--- /dev/null
+++ b/appl/cmd/fs/mode.b
@@ -1,0 +1,120 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+# XXX implement octal modes.
+
+User:	con 8r700;
+Group:	con 8r070;
+Other:	con 8r007;
+All:	con User | Group | Other;
+
+Read:	con 8r444;
+Write:	con 8r222;
+Exec:	con 8r111;
+
+types(): string
+{
+	return "ps";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	spec := (hd args).s().i;
+	(ok, mask, mode) := parsemode(spec);
+	if(ok == 0){
+		sys->fprint(sys->fildes(2), "fs: mode: bad mode %#q\n", spec);
+		return nil;
+	}
+	c := chan of Gatequery;
+	spawn modegate(c, mask, mode);
+	return ref Value.P(c);
+}
+
+modegate(c: Gatechan, mask, mode: int)
+{
+	m := mode & mask;
+	while((((d, nil, nil), reply) := <-c).t0.t0 != nil)
+		reply <-= ((d.mode & mask) ^ m) == 0;
+}
+
+# stolen from /appl/cmd/chmod.b
+parsemode(spec: string): (int, int, int)
+{
+	mask := Sys->DMAPPEND | Sys->DMEXCL | Sys->DMDIR | Sys->DMAUTH;
+loop:
+	for(i := 0; i < len spec; i++){
+		case spec[i] {
+		'u' =>
+			mask |= User;
+		'g' =>
+			mask |= Group;
+		'o' =>
+			mask |= Other;
+		'a' =>
+			mask |= All;
+		* =>
+			break loop;
+		}
+	}
+	if(i == len spec)
+		return (0, 0, 0);
+	if(i == 0)
+		mask |= All;
+
+	op := spec[i++];
+	if(op != '+' && op != '-' && op != '=')
+		return (0, 0, 0);
+
+	mode := 0;
+	for(; i < len spec; i++){
+		case spec[i]{
+		'r' =>
+			mode |= Read;
+		'w' =>
+			mode |= Write;
+		'x' =>
+			mode |= Exec;
+		'a' =>
+			mode |= Sys->DMAPPEND;
+		'l' =>
+			mode |= Sys->DMEXCL;
+		'd' =>
+			mode |= Sys->DMDIR;
+		'A' =>
+			mode |= Sys->DMAUTH;
+		* =>
+			return (0, 0, 0);
+		}
+	}
+	if(op == '+' || op == '-')
+		mask &= mode;
+	if(op == '-')
+		mode = ~mode;
+	return (1, mask, mode);
+}
+
+
--- /dev/null
+++ b/appl/cmd/fs/not.b
@@ -1,0 +1,48 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "pp";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	c := chan of Gatequery;
+	spawn notgate(c, (hd args).p().i);
+	return ref Value.P(c);
+}
+
+notgate(c, sub: Gatechan)
+{
+	myreply := chan of int;
+	while(((d, reply) := <-c).t0.t0 != nil){
+		sub <-= (d, myreply);
+		reply <-= !<-myreply;
+	}
+	sub <-= (Nilentry, nil);
+}
--- /dev/null
+++ b/appl/cmd/fs/or.b
@@ -1,0 +1,65 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "pppp*";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	c := chan of Gatequery;
+	spawn orgate(c, args);
+	return ref Value.P(c);
+}
+
+orgate(c: Gatechan, args: list of ref Value)
+{
+	sub: list of Gatechan;
+	for(; args != nil; args = tl args)
+		sub = (hd args).p().i :: sub;
+	sub = rev(sub);
+	myreply := chan of int;
+	while(((d, reply) := <-c).t0.t0 != nil){
+		for(l := sub; l != nil; l = tl l){
+			(hd l) <-= (d, myreply);
+			if(<-myreply)
+				break;
+		}
+		reply <-= l != nil;
+	}
+	for(; sub != nil; sub = tl sub)
+		hd sub <-= (Nilentry, nil);
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
--- /dev/null
+++ b/appl/cmd/fs/path.b
@@ -1,0 +1,77 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "pss*-x";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	# XXX cleanname all paths?
+	c := chan of Gatequery;
+	p: list of string;
+	for(; args != nil; args = tl args)
+		p = (hd args).s().i :: p;
+	spawn pathgate(c, opts != nil, p);
+	return ref Value.P(c);
+}
+
+pathgate(c: Gatechan, xflag: int, paths: list of string)
+{
+	if(xflag){
+		while((((d, path, nil), reply) := <-c).t0.t0 != nil){
+			for(q := paths; q != nil; q = tl q){
+				r := 1;
+				p := hd q;
+				if(len path > len p)
+					r = path[len p] != '/' || path[0:len p] != p;
+				else if(len path == len p)
+					r = path != p;
+				if(r == 0)
+					break;
+			}
+			reply <-= q == nil;
+		}
+	}else{
+		while((((d, path, nil), reply) := <-c).t0.t0 != nil){
+			for(q := paths; q != nil; q = tl q){
+				r := 0;
+				p := hd q;
+				if(len path > len p)
+					r = path[len p] == '/' && path[0:len p] == p;
+				else if(len path == len p)
+					r = path == p;
+				else
+					r = p[len path] == '/' && p[0:len path] == path;
+				if(r)
+					break;
+			}
+			reply <-= q != nil;
+		}
+	}
+}
--- /dev/null
+++ b/appl/cmd/fs/pipe.b
@@ -1,0 +1,223 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "fslib.m";
+	fslib: Fslib;
+	Option, Value, Fschan, Report, quit: import fslib;
+	Skip, Next, Down, Quit: import fslib;
+
+
+# pipe the contents of the files in a filesystem through
+# a command. -1 causes one command only to be executed.
+# -p and -P (exclusive to -1) cause stat modes to be set in the shell environment.
+types(): string
+{
+	return "vcx-1-p-P";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: exec: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		badmod(Sh->PATH);
+	sh->initialise();
+}
+
+run(drawctxt: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	n := 1;
+	oneflag := pflag := 0;
+	for(; opts != nil; opts = tl opts){
+		o := hd opts;
+		case o.opt {
+		'1' =>
+			oneflag = 1;
+		'p' =>
+			pflag = 1;
+		'P' =>
+			pflag = 2;
+		}
+	}
+	if(pflag && oneflag){
+		sys->fprint(sys->fildes(2), "fs: exec: cannot specify -p with -1\n");
+		return nil;
+	}
+	cmd := (hd args).c().i;
+	c := (hd tl args).x().i;
+	sync := chan of int;
+	spawn execproc(drawctxt, sync, oneflag, pflag, c, cmd, report.start("exec"));
+	sync <-= 1;
+	return ref Value.V(sync);
+}
+
+execproc(drawctxt: ref Draw->Context, sync: chan of int, oneflag, pflag: int,
+		c: Fschan, cmd: ref Sh->Cmd, errorc: chan of string)
+{
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	ctxt := Context.new(drawctxt);
+	<-sync;
+	if(<-sync == 0){
+		(<-c).t1 <-= Quit;
+		quit(errorc);
+	}
+	argv := ref Sh->Listnode(cmd, nil) :: nil;
+	fd: ref Sys->FD;
+	result := chan of string;
+	if(oneflag){
+		fd = popen(ctxt, argv, result);
+		if(fd == nil){
+			(<-c).t1 <-= Quit;
+			quit(errorc);
+		}
+	}
+
+	names: list of string;
+	name: string;
+	indent := 0;
+	for(;;){
+		(d, reply) := <-c;
+		if(d.dir == nil){
+			reply <-= Next;
+			if(--indent == 0){
+				break;
+			}
+			(name, names) = (hd names, tl names);
+			continue;
+		}
+		if((d.dir.mode & Sys->DMDIR) != 0){
+			reply <-= Down;
+			names = name :: names;
+			if(indent > 0 && name != nil && name[len name - 1] != '/')
+				name[len name] = '/';
+			name += d.dir.name;
+			indent++;
+			continue;
+		}
+		if(!oneflag){
+			p := name;
+			if(p != nil && p[len p - 1] != '/')
+				p[len p] = '/';
+			setenv(ctxt, "file", p + d.dir.name :: nil);
+			if(pflag)
+				setstatenv(ctxt, d.dir, pflag);
+			fd = popen(ctxt, argv, result);
+		}
+		if(fd == nil){
+			reply <-= Next;
+			continue;
+		}
+		reply <-= Down;
+		for(;;){
+			data: array of byte;
+			((nil, data), reply) = <-c;
+			reply <-= Next;
+			if(data == nil)
+				break;
+			n := -1;
+			{n = sys->write(fd, data, len data);}exception {"write on closed pipe" => ;}
+			if(n != len data){
+				if(oneflag){
+					(<-c).t1 <-= Quit;
+					quit(errorc);
+				}
+				(<-c).t1 <-= Skip;
+				break;
+			}
+		}
+		if(!oneflag){
+			fd = nil;
+			<-result;
+		}
+	}
+	fd = nil;
+	if(oneflag)
+		<-result;
+	quit(errorc);
+}
+
+popen(ctxt: ref Context, argv: list of ref Sh->Listnode, result: chan of string): ref Sys->FD
+{
+	sync := chan of int;
+	fds := array[2] of ref Sys->FD;
+	sys->pipe(fds);
+	spawn runcmd(ctxt, argv, fds[0], sync, result);
+	<-sync;
+	return fds[1];
+}
+
+runcmd(ctxt: ref Context, argv: list of ref Sh->Listnode, stdin: ref Sys->FD, sync: chan of int, result: chan of string)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	stdin = nil;
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	ctxt = ctxt.copy(0);
+	sync <-= 0;
+	r := ctxt.run(argv, 0);
+	ctxt = nil;
+	sys->pctl(Sys->NEWFD, nil);
+	result <-=r;
+}
+
+setenv(ctxt: ref Context, var: string, val: list of string)
+{
+	ctxt.set(var, sh->stringlist2list(val));
+}
+
+setstatenv(ctxt: ref Context, dir: ref Sys->Dir, pflag: int)
+{
+	setenv(ctxt, "mode", modes(dir.mode) :: nil);
+	setenv(ctxt, "uid", dir.uid :: nil);
+	setenv(ctxt, "mtime", string dir.mtime :: nil);
+	setenv(ctxt, "length", string dir.length :: nil);
+
+	if(pflag > 1){
+		setenv(ctxt, "name", dir.name :: nil);
+		setenv(ctxt, "gid", dir.gid :: nil);
+		setenv(ctxt, "muid", dir.muid :: nil);
+		setenv(ctxt, "qid", sys->sprint("16r%ubx", dir.qid.path) :: string dir.qid.vers :: nil);
+		setenv(ctxt, "atime", string dir.atime :: nil);
+		setenv(ctxt, "dtype", sys->sprint("%c", dir.dtype) :: nil);
+		setenv(ctxt, "dev", string dir.dev :: nil);
+	}
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/fs/print.b
@@ -1,0 +1,51 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "vt";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	sync := chan of int;
+	spawn printproc(sync, (hd args).t().i, report.start("print"));
+	return ref Value.V(sync);
+}
+
+printproc(sync: chan of int, c: Entrychan, errorc: chan of string)
+{
+	if(<-sync == 0){
+		c.sync <-= 0;
+		quit(errorc);
+		exit;
+	}
+	c.sync <-= 1;
+	while(((d, p, nil) := <-c.c).t0 != nil)
+		sys->print("%s\n", p);
+	quit(errorc);
+}
--- /dev/null
+++ b/appl/cmd/fs/proto.b
@@ -1,0 +1,393 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "readdir.m";
+	readdir: Readdir;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, report, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+File: adt {
+	name: string;
+	mode: int;
+	owner: string;
+	group: string;
+	old: string;
+	flags: int;
+	sub: cyclic array of ref File;
+};
+
+Proto: adt {
+	indent: int;
+	lastline: string;
+	iob: ref Iobuf;
+};
+
+Star, Plus, Empty: con 1<<iota;
+
+types(): string
+{
+	return "xs-rs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: proto: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmod(Readdir->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmod(Bufio->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		badmod(String->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	protofile := (hd args).s().i;
+	rootpath: string;
+	if(opts != nil)
+		rootpath = (hd (hd opts).args).s().i;
+	if(rootpath == nil)
+		rootpath = "/";
+
+	proto := ref Proto(0, nil, nil);
+	if((proto.iob = bufio->open(protofile, Sys->OREAD)) == nil){
+		sys->fprint(sys->fildes(2), "fs: proto: cannot open %q: %r\n", protofile);
+		return nil;
+	}
+	root := ref File(rootpath, ~0, nil, nil, nil, 0, nil);
+	(root.flags, root.sub) = readproto(proto, -1);
+	c := chan of (Fsdata, chan of int);
+	spawn protowalk(c, root, report.start("proto"));
+	return ref Value.X(c);
+}
+
+protowalk(c: Fschan, root: ref File, errorc: chan of string)
+{
+	protowalk1(c, root.flags, root.name, file2dir(root, nil), root.sub, errorc);
+	quit(errorc);
+}
+
+protowalk1(c: Fschan, flags: int, path: string, d: ref Sys->Dir,
+		sub: array of ref File, errorc: chan of string): int
+{
+	reply := chan of int;
+	c <-= ((d, nil), reply);
+	case r := <-reply {
+	Quit =>
+		quit(errorc);
+	Next or
+	Skip =>
+		return r;
+	}
+	a: array of ref Sys->Dir;
+	n := 0;
+	if((flags&Empty)==0)
+		(a, n) = readdir->init(path, Readdir->NAME|Readdir->COMPACT);
+	i := j := 0;
+	prevsub: string;
+	while(i < n || j < len sub){
+		for(; j < len sub; j++){
+			s := sub[j].name;
+			if(s == prevsub){
+				report(errorc, sys->sprint("duplicate entry %s", pathconcat(path, s)));
+				continue;			# eliminate duplicates in proto
+			}
+			# if we're copying from an old file, and there's a matching
+			# entry in the directory, then skip it.
+			if(sub[j].old != nil && i < n && s == a[i].name)
+				i++;
+			if(sub[j].old != nil || i < n && s >= a[i].name)
+				break;
+			report(errorc, sys->sprint("%s not found", pathconcat(path, s)));
+		}
+		foundsub := j < len sub && (sub[j].old != nil || sub[j].name == a[i].name);
+
+		if(foundsub || flags&(Plus|Star)){
+			f: ref File;
+			if(foundsub){
+				f = sub[j++];
+				prevsub = f.name;
+			}
+			p: string;
+			d: ref Sys->Dir;
+			if(foundsub && f.old != nil){
+				p = f.old;
+				(ok, xd) := sys->stat(p);
+				if(ok == -1){
+					report(errorc, sys->sprint("cannot stat %q: %r", p));
+					continue;
+				}
+				d = ref xd;
+			}else{
+				p = pathconcat(path, a[i].name);
+				d = a[i++];
+			}
+
+			d = file2dir(f, d);
+			r: int;
+			if((d.mode & Sys->DMDIR) == 0)
+				r = walkfile(c, p, d, errorc);
+			else if(flags & Plus)
+				r = protowalk1(c, Plus, p, d, nil, errorc);
+			else if((flags&Star) && !foundsub)
+				r = protowalk1(c, Empty, p, d, nil, errorc);
+			else
+				r = protowalk1(c, f.flags, p, d, f.sub, errorc);
+			if(r == Skip)
+				return Next;
+		}else
+			i++;
+	}
+
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+pathconcat(p, name: string): string
+{		
+	if(p != nil && p[len p - 1] != '/')
+		p[len p] = '/';
+	p += name;
+	return p;
+}
+
+# from(ish) walk.b
+walkfile(c: Fschan, path: string, d: ref Sys->Dir, errorc: chan of string): int
+{
+	reply := chan of int;
+	fd := sys->open(path, Sys->OREAD);
+	if(fd == nil){
+		report(errorc, sys->sprint("cannot open %q: %r", path));
+		return Next;
+	}
+	c <-= ((d, nil), reply);
+	case r := <-reply {
+	Quit =>
+		quit(errorc);
+	Next or
+	Skip =>
+		return r;
+	}
+	length := d.length;
+	for(n := big 0; n < length; ){
+		nr := Sys->ATOMICIO;
+		if(n + big Sys->ATOMICIO > length)
+			nr = int (length - n);
+		buf := array[nr] of byte;
+		nr = sys->read(fd, buf, nr);
+		if(nr <= 0){
+			if(nr < 0)
+				report(errorc, sys->sprint("error reading %q: %r", path));
+			else
+				report(errorc, sys->sprint("%q is shorter than expected (%bd/%bd)",
+						path, n, length));
+			break;
+		}else if(nr < len buf)
+			buf = buf[0:nr];
+		c <-= ((nil, buf), reply);
+		case <-reply {
+		Quit =>
+			quit(errorc);
+		Skip =>
+			return Next;
+		}
+		n += big nr;
+	}
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+readproto(proto: ref Proto, indent: int): (int, array of ref File)
+{
+	a := array[10] of ref File;
+	n := 0;
+	flags := 0;
+	while((f := readline(proto, indent)) != nil){
+		if(f.name == "*")
+			flags |= Star;
+		else if(f.name == "+")
+			flags |= Plus;
+		else{
+			(f.flags, f.sub) = readproto(proto, proto.indent);
+			if(n == len a)
+				a = (array[n * 2] of ref File)[0:] = a;
+			a[n++] = f;
+		}
+	}
+	if(n < len a)
+		a = (array[n] of ref File)[0:] = a[0:n];
+	mergesort(a, array[n] of ref File);
+	return (flags, a);
+}
+
+readline(proto: ref Proto, indent: int): ref File
+{
+	s: string;
+	if(proto.lastline != nil){
+		s = proto.lastline;
+		proto.lastline = nil;
+	}else if(proto.indent == -1)
+		return nil;
+	else if((s = proto.iob.gets('\n')) == nil){
+		proto.indent = -1;
+		return nil;
+	}
+	spc := 0;
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c == ' ')
+			spc++;
+		else if(c == '\t')
+			spc += 8;
+		else
+			break;
+	}
+	if(i == len s || s[i] == '#' || s[i] == '\n')
+		return readline(proto, indent);	# XXX sort out tail recursion!
+	if(spc <= indent){
+		proto.lastline = s;
+		return nil;
+	}
+	proto.indent = spc;
+	(nil, toks) := sys->tokenize(s, " \t\n");
+	f := ref File(nil, ~0, nil, nil, nil, 0, nil);
+	(f.name, toks) = (getname(hd toks, 0), tl toks);
+	if(toks == nil)
+		return f;
+	(f.mode, toks) = (getmode(hd toks), tl toks);
+	if(toks == nil)
+		return f;
+	(f.owner, toks) = (getname(hd toks, 1), tl toks);
+	if(toks == nil)
+		return f;
+	(f.group, toks) = (getname(hd toks, 1), tl toks);
+	if(toks == nil)
+		return f;
+	(f.old, toks) = (hd toks, tl toks);
+	return f;
+}
+
+mergesort(a, b: array of ref File)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m]);
+		mergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if(b[i].name > b[j].name)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+getname(s: string, allowminus: int): string
+{
+	if(s == nil)
+		return nil;
+	if(allowminus && s == "-")
+		return nil;
+	if(s[0] == '$')
+		return getenv(s[1:]);
+	return s;
+}
+
+getenv(s: string): string
+{
+	# XXX implement env variables
+	return nil;
+}
+
+getmode(s: string): int
+{
+	s = getname(s, 1);
+	if(s == nil)
+		return ~0;
+	m := 0;
+	i := 0;
+	if(s[i] == 'd'){
+		m |= Sys->DMDIR;
+		i++;
+	}
+	if(i < len s && s[i] == 'a'){
+		m |= Sys->DMAPPEND;
+		i++;
+	}
+	if(i < len s && s[i] == 'l'){
+		m |= Sys->DMEXCL;
+		i++;
+	}
+	(xmode, t) := str->toint(s, 8);
+	if(t != nil){
+		# report(aux.errorc, "bad mode specification %q", s);
+		return ~0;
+	}
+	return xmode | m;
+}
+
+file2dir(f: ref File, old: ref Sys->Dir): ref Sys->Dir
+{
+	d := ref Sys->nulldir;
+	if(old != nil){
+		if(old.dtype != 'M'){
+			d.uid = "sys";
+			d.gid = "sys";
+			xmode := (old.mode >> 6) & 7;
+			d.mode = old.mode | xmode | (xmode << 3);
+		}else{
+			d.uid = old.uid;
+			d.gid = old.gid;
+			d.mode = old.mode;
+		}
+		d.length = old.length;
+		d.mtime = old.mtime;
+		d.atime = old.atime;
+		d.muid = old.muid;
+		d.name = old.name;
+	}
+	if(f != nil){
+		d.name = f.name;
+		if(f.owner != nil)
+			d.uid = f.owner;
+		if(f.group != nil)
+			d.gid = f.group;
+		if(f.mode != ~0)
+			d.mode = f.mode;
+	}
+	return d;
+}
--- /dev/null
+++ b/appl/cmd/fs/query.b
@@ -1,0 +1,130 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "fslib.m";
+	fslib: Fslib;
+	Option, Value, Gatechan, Gatequery, Report, Nilentry: import fslib;
+
+types(): string
+{
+	return "pc-p-P";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: query: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		badmod(Sh->PATH);
+}
+
+run(drawctxt: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	pflag := 0;
+	for(; opts != nil; opts = tl opts){
+		o := hd opts;
+		case o.opt {
+		'p' =>
+			pflag = 1;
+		'P' =>
+			pflag = 2;
+		}
+	}
+
+	v := ref Value.P(chan of Gatequery);
+	spawn querygate(drawctxt, v.i, (hd args).c().i, pflag);
+	v.i <-= (Nilentry, nil);
+	return v;
+}
+
+querygate(drawctxt: ref Draw->Context, c: Gatechan, cmd: ref Sh->Cmd, pflag: int)
+{
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	ctxt := Context.new(drawctxt);
+	<-c;
+	argv := ref Sh->Listnode(cmd, nil) :: nil;
+	while((((d, p, nil), reply) := <-c).t0.t0 != nil){
+		ctxt.set("file", ref Sh->Listnode(nil, p) :: nil);
+		if(pflag)
+			setstatenv(ctxt, d, pflag);
+		err := "";
+		{
+			err = ctxt.run(argv, 0);
+		} exception e {
+		"fail:*" =>
+			err = e;
+		}
+		reply <-= (err == nil);
+	}
+}
+
+# XXX shouldn't duplicate this...
+
+setenv(ctxt: ref Context, var: string, val: list of string)
+{
+	ctxt.set(var, sh->stringlist2list(val));
+}
+
+setstatenv(ctxt: ref Context, dir: ref Sys->Dir, pflag: int)
+{
+	setenv(ctxt, "mode", modes(dir.mode) :: nil);
+	setenv(ctxt, "uid", dir.uid :: nil);
+	setenv(ctxt, "mtime", string dir.mtime :: nil);
+	setenv(ctxt, "length", string dir.length :: nil);
+
+	if(pflag > 1){
+		setenv(ctxt, "name", dir.name :: nil);
+		setenv(ctxt, "gid", dir.gid :: nil);
+		setenv(ctxt, "muid", dir.muid :: nil);
+		setenv(ctxt, "qid", sys->sprint("16r%ubx", dir.qid.path) :: string dir.qid.vers :: nil);
+		setenv(ctxt, "atime", string dir.atime :: nil);
+		setenv(ctxt, "dtype", sys->sprint("%c", dir.dtype) :: nil);
+		setenv(ctxt, "dev", string dir.dev :: nil);
+	}
+}
+
+start(startc: chan of (string, chan of string), name: string): chan of string
+{
+	c := chan of string;
+	startc <-= (name, c);
+	return c;
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/fs/readfile.b
@@ -1,0 +1,144 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, report, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+this is a bad idea, i think
+i think walk + filter + setroot is good enough.
+
+types(): string
+{
+	# usage: readfile [-f file] name
+	return "xs-fs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: readfile: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	path: string;
+	f := (hd args).s().i;
+	fd: ref Sys->FD;
+	seekable: int;
+	if(f == "-"){
+		if(opts == nil){
+			sys->fprint(sys->fildes(2), "fs: readfile: must specify a path when reading stdin\n");
+			return nil;
+		}
+		fd = sys->fildes(0);
+		seekable = 0;
+	}else{
+		fd = sys->open(f, Sys->OREAD);
+		seekable = isseekable(fd);
+	}
+	if(fd == nil){
+		sys->fprint(sys->fildes(2), "fs: readfile: cannot open %s: %r\n", f);
+		return nil;
+	}
+	if(opts != nil)
+		path = (hd (hd opts).args).s().i;
+	else
+		path = f;
+
+	(root, file) := pathsplit(path);
+	if(file == nil || file == "." || file == ".."){
+		sys->fprint(sys->fildes(2), "fs: readfile: invalid filename %q\n", fname);
+		return nil;
+	}
+	d.name = file;
+	v := ref Value.X(chan of (Fsdata, chan of int));
+	spawn readproc(v.i, fd, root, ref d, seekable, report.start("read"));
+	return v;
+}
+
+readproc(c: Fschan, fd: ref Sys->FD, root: string, d: ref Sys->Dir, seekable: int, errorc: chan of string)
+{
+	reply := chan of int;
+	rd := ref Sys->nulldir;
+	rd.name = root;
+	c <-= ((rd, nil), reply);
+	if(<-reply != Down)
+		quit(errorc);
+
+	c <-= ((d, nil), reply);
+	case <-reply {
+	Down =>
+		sendfile(c, fd, errorc);
+	Skip or
+	Quit =>
+		quit(errorc);
+	}
+	c <-= ((nil, nil), reply);
+	<-reply;
+	quit(errorc);
+}
+
+sendfile(c: Fschan, data: list of array of byte, length: big, errorc: chan of string)
+{
+	reply := chan of int;
+	for(;;){
+		buf: array of byte;
+		if(fd != nil){
+			buf := array[Sys->ATOMICIO] of byte;
+			if((n := sys->read(fd, buf, len buf)) <= 0){
+			if(n < 0)
+				report(errorc, sys->sprint("read error: %r"));
+			c <-= ((nil, nil), reply);
+			if(<-reply == Quit)
+				quit(errorc);
+			return;
+		}
+		c <-= ((nil, buf), reply);
+		case <-reply {
+		Quit =>
+			quit(errorc);
+		Skip =>
+			return;
+		}
+	}
+}
+
+pathsplit(p: string): (string, string)
+{
+	for (i := len p - 1; i >= 0; i--)
+		if (p[i] != '/')
+			break;
+	if (i < 0)
+		return (p, nil);
+	p = p[0:i+1];
+	for (i = len p - 1; i >=0; i--)
+		if (p[i] == '/')
+			break;
+	if (i < 0)
+		return (".", p);
+	return (p[0:i+1], p[i+1:]);
+}
+
+# dodgy heuristic... avoid, or using the stat-length of pipes and net connections
+isseekable(fd: ref Sys->FD): int
+{
+	(ok, stat) := sys->stat(iob.fd);
+	if(ok != -1 && stat.dtype == '|' || stat.dtype == 'I')
+		return 0;
+	return 1;
+}
--- /dev/null
+++ b/appl/cmd/fs/run.b
@@ -1,0 +1,60 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "sc";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	sh = load Sh Sh->PATH;
+	if(sh == nil)
+		badmod(Sh->PATH);
+	sh->initialise();
+}
+
+run(drawctxt: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	c := (hd args).c().i;
+	ctxt := Context.new(drawctxt);
+	ctxt.setlocal("s", nil);
+	{
+		ctxt.run(ref Sh->Listnode(c, nil)::nil, 0);
+	} exception e {
+	"fail:*" =>
+		sys->fprint(sys->fildes(2), "fs: run: exception %q raised in %s\n", e[5:], sh->cmd2string(c));
+		return nil;
+	}
+	sl := ctxt.get("s");
+	if(sl == nil || tl sl != nil){
+		sys->fprint(sys->fildes(2), "fs: run: $s has %d members; exactly one is required\n", len sl);
+		return nil;
+	}
+	s := (hd sl).word;
+	if(s == nil && (hd sl).cmd != nil)
+		s = sh->cmd2string((hd sl).cmd);
+	return ref Value.S(s);
+}
--- /dev/null
+++ b/appl/cmd/fs/select.b
@@ -1,0 +1,56 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "tpt";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	dst := Entrychan(chan of int, chan of Entry);
+	spawn selectproc((hd tl args).t().i, dst, (hd args).p().i);
+	return ref Value.T(dst);
+}
+
+selectproc(src, dst: Entrychan, query: Gatechan)
+{
+	if(<-dst.sync == 0){
+		query <-= (Nilentry, nil);
+		src.sync <-= 0;
+		exit;
+	}
+	src.sync <-= 1;
+	reply := chan of int;
+	while((d := <-src.c).t0 != nil){
+		query <-= (d, reply);
+		if(<-reply)
+			dst.c <-= d;
+	}
+	dst.c <-= Nilentry;
+	query <-= (Nilentry, nil);
+}
--- /dev/null
+++ b/appl/cmd/fs/setroot.b
@@ -1,0 +1,104 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+# set the root 
+types(): string
+{
+	return "xsx-c";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	root := (hd args).s().i;
+	if(root == nil && opts == nil){
+		sys->fprint(sys->fildes(2), "fs: setroot: empty path\n");
+		return nil;
+	}
+	v := ref Value.X(chan of (Fsdata, chan of int));
+	spawn setroot((hd tl args).x().i, v.i, root, opts != nil);
+	return v;
+}
+
+setroot(src, dst: Fschan, root: string, cflag: int)
+{
+	((d, nil), reply) := <-src;
+	if(cflag){
+		createroot(src, dst, root, d, reply);
+	}else{
+		myreply := chan of int;
+		rd := ref *d;
+		rd.name = root;
+		dst <-= ((rd, nil), myreply);
+		if(<-myreply == Down){
+			reply <-= Down;
+			fslib->copy(src, dst);
+		}
+	}
+}
+
+createroot(src, dst: Fschan, root: string, d: ref Sys->Dir, reply: chan of int)
+{
+	if(root == nil)
+		root = d.name;
+	(n, elems) := sys->tokenize(root, "/");		# XXX should really do a cleanname first
+	if(root[0] == '/'){
+		elems = "/" :: elems;
+		n++;
+	}
+	myreply := chan of int;
+	lev := 0;
+	r := -1;
+	for(; elems != nil; elems = tl elems){
+		rd := ref *d;
+		rd.name = hd elems;
+		dst <-= ((rd, nil), myreply);
+		case r = <-myreply {
+		Quit =>
+			(<-src).t1 <-= Quit;
+			exit;
+		Skip =>
+			break;
+		Next =>
+			lev++;
+			break;
+		}
+		lev++;
+	}
+	if(r == Down){
+		reply <-= Down;
+		if(fslib->copy(src, dst) == Quit)
+			exit;
+	}else
+		reply <-= Quit;
+	while(lev-- > 1){
+		dst <-= ((nil, nil), myreply);
+		if(<-myreply == Quit){
+			(<-src).t1 <-= Quit;
+			exit;
+		}
+	}
+}
--- /dev/null
+++ b/appl/cmd/fs/size.b
@@ -1,0 +1,54 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "vt";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	sync := chan of int;
+	spawn sizeproc(sync, (hd args).t().i, report.start("size"));
+	return ref Value.V(sync);
+}
+
+sizeproc(sync: chan of int, c: Entrychan, errorc: chan of string)
+{
+	if(<-sync == 0){
+		c.sync <-= 0;
+		quit(errorc);
+		exit;
+	}
+	c.sync <-= 1;
+
+	size := big 0;
+	while(((d, nil, nil) := <-c.c).t0 != nil)
+		size += d.length;
+	sys->print("%bd\n", size);
+	quit(errorc);
+}
--- /dev/null
+++ b/appl/cmd/fs/template.b
@@ -1,0 +1,35 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "nil";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: size: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+}
--- /dev/null
+++ b/appl/cmd/fs/unbundle.b
@@ -1,0 +1,259 @@
+implement Unbundle;
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "bundle.m";
+	bundle: Bundle;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value,quit, report: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Quit, Next, Skip, Down,
+	Option: import Fslib;
+include "unbundle.m";
+
+types(): string
+{
+	return "xs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: exec: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmod(Bufio->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		badmod(String->PATH);
+	bundle = load Bundle Bundle->PATH;
+	if(bundle == nil)
+		badmod(Bundle->PATH);
+	bundle->init();
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	p := (hd args).s().i;
+	iob: ref Bufio->Iobuf;
+	if(p == "-")
+		iob = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	else
+		iob = bufio->open(p, Sys->OREAD);
+	if(iob == nil){
+		sys->fprint(sys->fildes(2), "fs: unbundle: cannot open %q: %r\n", p);
+		return nil;
+	}
+	seekable := p != "-";
+	if(seekable)
+		seekable = isseekable(iob.fd);
+	return ref Value.X(unbundle(report, iob, seekable, Sys->ATOMICIO));
+}
+
+# dodgy heuristic... avoid, or using the stat-length of pipes and net connections
+isseekable(fd: ref Sys->FD): int
+{
+	(ok, stat) := sys->fstat(fd);
+	if(ok != -1 && stat.dtype == '|' || stat.dtype == 'I')
+		return 0;
+	return 1;
+}
+
+unbundle(r: ref Report, iob: ref Iobuf, seekable, blocksize: int): Fschan
+{
+	c := chan of (Fsdata, chan of int);
+	spawn unbundleproc(iob, c, seekable, blocksize, r.start("bundle"));
+	return c;
+}
+
+EOF: con "end of archive\n";
+
+unbundleproc(iob: ref Iobuf, c: Fschan, seekable, blocksize: int, errorc: chan of string)
+{
+	reply := chan of int;
+	p := iob.gets('\n');
+	# XXX overall header?
+	if(p == nil || p == EOF){
+		fslib->sendnulldir(c);
+		quit(errorc);
+	}
+	d := header2dir(p);
+	if(d == nil){
+		fslib->sendnulldir(c);
+		report(errorc, "invalid first header");
+		quit(errorc);
+	}
+	if((d.mode & Sys->DMDIR) == 0){
+		fslib->sendnulldir(c);
+		report(errorc, "first entry is not a directory");
+		quit(errorc);
+	}
+	c <-= ((d, nil), reply);
+	case r := <-reply {
+	Down =>
+		unbundledir(iob, c, 0, seekable, blocksize, errorc);
+		c <-= ((nil, nil), reply);
+		<-reply;
+	Skip or
+	Next =>
+		unbundledir(iob, c, 1, seekable, blocksize, errorc);
+	Quit =>
+		break;
+	}
+	quit(errorc);
+}
+
+unbundledir(iob: ref Iobuf, c: Fschan,
+			skipping, seekable, blocksize: int, errorc: chan of string): int
+{
+	reply := chan of int;
+	while((p := iob.gets('\n')) != nil){
+		if(p == EOF)
+			break;
+		if(p[0] == '\n')
+			break;
+		d := header2dir(p);
+		if(d == nil){
+			report(errorc, sys->sprint("invalid bundle header %q", p[0:len p - 1]));
+			return -1;
+		}
+		if(d.mode & Sys->DMDIR){
+			if(skipping)
+				continue;
+			c <-= ((d, nil), reply);
+			case <-reply {
+			Quit =>
+				quit(errorc);
+			Down =>
+				r := unbundledir(iob, c, 0, seekable, blocksize, errorc);
+				c <-= ((nil, nil), reply);
+				if(<-reply == Quit)
+					quit(errorc);
+				if(r == -1)
+					return -1;
+			Skip =>
+				if(unbundledir(iob, c, 1, seekable, blocksize, errorc) == -1)
+					return -1;
+				skipping = 1;
+			Next =>
+				if(unbundledir(iob, c, 1, seekable, blocksize, errorc) == -1)
+					return -1;
+			}
+		}else{
+			if(skipping){
+				if(skipdata(iob, d.length, seekable) == -1)
+					return -1;
+			}else{
+				case unbundlefile(iob, d, c, errorc, seekable, blocksize) {
+				-1 =>
+					return -1;
+				Skip =>
+					skipping = 1;
+				}
+			}
+		}
+	}
+	if(p == nil)
+		report(errorc, "unexpected eof");
+	return 0;
+}
+
+skipdata(iob: ref Iobuf, length: big, seekable: int): int
+{
+	if(seekable){
+		iob.seek(big length, Sys->SEEKRELA);
+		return 0;
+	}
+	buf := array[Sys->ATOMICIO] of byte;
+	for(n := big 0; n < length; ){
+		nb := Sys->ATOMICIO;
+		if(length - n < big Sys->ATOMICIO)
+			nb = int (length - n);
+		nb = iob.read(buf, nb);
+		if(nb <= 0)
+			return -1;
+		n += big nb;
+	}
+	return 0;
+}
+
+unbundlefile(iob: ref Iobuf, d: ref Sys->Dir,
+	c: Fschan, errorc: chan of string, seekable, blocksize: int): int
+{
+	reply := chan of int;
+	c <-= ((d, nil), reply);
+	case <-reply {
+	Quit =>
+		quit(errorc);
+	Skip =>
+		if(skipdata(iob, d.length, seekable) == -1)
+			return -1;
+		return Skip;
+	Next =>
+		if(skipdata(iob, d.length, seekable) == -1)
+			return -1;
+		return Next;
+	}
+	length := d.length;
+	for(n := big 0; n < length; ){
+		nr := blocksize;
+		if(n + big blocksize > length)
+			nr = int (length - n);
+		buf := array[nr] of byte;
+		nr = iob.read(buf, nr);
+		if(nr <= 0){
+			if(nr < 0)
+				report(errorc, sys->sprint("read error: %r"));
+			else
+				report(errorc, sys->sprint("premature eof"));
+			return -1;
+		}else if(nr < len buf)
+			buf = buf[0:nr];
+		c <-= ((nil, buf), reply);
+		n += big nr;
+		case <-reply {
+		Quit =>
+			quit(errorc);
+		Skip =>
+			if(skipdata(iob, length - n, seekable) == -1)
+				return -1;
+			return Next;
+		}
+	}
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+header2dir(s: string): ref Sys->Dir
+{
+	toks := str->unquoted(s);
+	nf := len toks;
+	if(nf != 6)
+		return nil;
+	d := ref Sys->nulldir;
+	(d.name, toks) = (hd toks, tl toks);
+	(d.mode, toks) = (str->toint(hd toks, 8).t0, tl toks);
+	(d.uid, toks) = (hd toks, tl toks);
+	(d.gid, toks) = (hd toks, tl toks);
+	(d.mtime, toks) = (int hd toks, tl toks);
+	(d.length, toks) = (big hd toks, tl toks);
+	return d;
+}
--- /dev/null
+++ b/appl/cmd/fs/void.b
@@ -1,0 +1,33 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, Option: import fslib;
+
+types(): string
+{
+	return "vv";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: void: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+}
+
+run(nil: ref Draw->Context, nil: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	return (hd args).v();
+}
--- /dev/null
+++ b/appl/cmd/fs/walk.b
@@ -1,0 +1,233 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "readdir.m";
+	readdir: Readdir;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, report, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+Loopcheck: adt {
+	a: array of list of ref Sys->Dir;
+
+	new:		fn(): ref Loopcheck;
+	enter:	fn(l: self ref Loopcheck, d: ref Sys->Dir): int;
+	leave:	fn(l: self ref Loopcheck, d: ref Sys->Dir);
+};
+
+types(): string
+{
+	return "xs-bs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: walk: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmod(Readdir->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	path := (hd args).s().i;
+	(ok, d) := sys->stat(path);
+	if(ok== -1){
+		sys->fprint(sys->fildes(2), "fs: walk: cannot stat %q: %r\n", path);
+		return nil;
+	}
+	if((d.mode & Sys->DMDIR) == 0){
+		# XXX could produce an fs containing just the single file.
+		# would have to split the path though.
+		sys->fprint(sys->fildes(2), "fs: walk: %q is not a directory\n", path);
+		return nil;
+	}
+	sync := chan of int;
+	c := chan of (Fsdata, chan of int);
+	spawn fswalkproc(sync, path, c, Sys->ATOMICIO, report.start("walk"));
+	<-sync;
+	return ref Value.X(c);
+}
+
+# XXX need to avoid loops in the filesystem...
+fswalkproc(sync: chan of int, path: string, c: Fschan, blocksize: int, errorc: chan of string)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+	# XXX could allow a single root file?
+	if(sys->chdir(path) == -1){
+		report(errorc, sys->sprint("cannot cd to %q: %r", path));
+		fslib->sendnulldir(c);
+		quit(errorc);
+	}
+	(ok, d) := sys->stat(".");
+	if(ok == -1){
+		report(errorc, sys->sprint("cannot stat %q: %r", path));
+		fslib->sendnulldir(c);
+		quit(errorc);
+	}
+	d.name = path;
+	reply := chan of int;
+	c <-= ((ref d, nil), reply);
+	if(<-reply == Down){
+		loopcheck := Loopcheck.new();
+		loopcheck.enter(ref d);
+		if(path[len path - 1] != '/')
+			path[len path] = '/';
+		fswalkdir(path, c, blocksize, loopcheck, errorc);
+		c <-= ((nil, nil), reply);
+		<-reply;
+	}
+	quit(errorc);
+}
+
+fswalkdir(path: string, c: Fschan, blocksize: int, loopcheck: ref Loopcheck, errorc: chan of string)
+{
+	reply := chan of int;
+	(a, n) := readdir->init(".", Readdir->NAME|Readdir->COMPACT);
+	if(n == -1){
+		report(errorc, sys->sprint("cannot readdir %q: %r", path));
+		return;
+	}
+	for(i := 0; i < n; i++)
+		if(a[i].mode & Sys->DMDIR)
+			if(loopcheck.enter(a[i]) == 0)
+				a[i].dtype = ~0;
+directory:
+	for(i = 0; i < n; i++){
+		if(a[i].mode & Sys->DMDIR){
+			d := a[i];
+			if(d.dtype == ~0){
+				report(errorc, sys->sprint("filesystem loop at %#q", path + d.name));
+				continue;
+			}
+			if(sys->chdir("./" + d.name) == -1){
+				report(errorc, sys->sprint("cannot cd to %#q: %r", path + a[i].name));
+				continue;
+			}
+			c <-= ((d, nil), reply);
+			case <-reply {
+			Quit =>
+				quit(errorc);
+			Down =>
+				fswalkdir(path + a[i].name + "/", c, blocksize, loopcheck, errorc);
+				c <-= ((nil, nil), reply);
+				if(<-reply == Quit)
+					quit(errorc);
+			Skip =>
+				sys->chdir("..");
+				i++;
+				break directory;
+			Next =>
+				break;
+			}
+			if(sys->chdir("..") == -1)		# XXX what should we do if this fails?
+				report(errorc, sys->sprint("failed to cd .. from %#q: %r\n", path + a[i].name));
+			
+		} else {
+			if(fswalkfile(path, a[i], c, blocksize, errorc) == Skip)
+				break directory;
+		}
+	}
+	for(i = n - 1; i >= 0; i--)
+		if(a[i].mode & Sys->DMDIR && a[i].dtype != ~0)
+			loopcheck.leave(a[i]);
+}
+
+fswalkfile(path: string, d: ref Sys->Dir, c: Fschan, blocksize: int, errorc: chan of string): int
+{
+	reply := chan of int;
+	fd := sys->open(d.name, Sys->OREAD);
+	if(fd == nil){
+		report(errorc, sys->sprint("cannot open %q: %r", path+d.name));
+		return Next;
+	}
+	c <-= ((d, nil), reply);
+	case <-reply {
+	Quit =>
+		quit(errorc);
+	Skip =>
+		return Skip;
+	Next =>
+		return Next;
+	Down =>
+		break;
+	}
+	length := d.length;
+	for(n := big 0; n < length; ){
+		nr := blocksize;
+		if(n + big blocksize > length)
+			nr = int (length - n);
+		buf := array[nr] of byte;
+		nr = sys->read(fd, buf, nr);
+		if(nr <= 0){
+			if(nr < 0)
+				report(errorc, sys->sprint("error reading %q: %r", path + d.name));
+			else
+				report(errorc, sys->sprint("%q is shorter than expected (%bd/%bd)",
+						path + d.name, n, length));
+			break;
+		}else if(nr < len buf)
+			buf = buf[0:nr];
+		c <-= ((nil, buf), reply);
+		case <-reply {
+		Quit =>
+			quit(errorc);
+		Skip =>
+			return Next;
+		}
+		n += big nr;
+	}
+	c <-= ((nil, nil), reply);
+	if(<-reply == Quit)
+		quit(errorc);
+	return Next;
+}
+
+HASHSIZE: con 32;
+
+issamedir(d0, d1: ref Sys->Dir): int
+{
+	(q0, q1) := (d0.qid, d1.qid);
+	return q0.path == q1.path &&
+		q0.qtype == q1.qtype &&
+		d0.dtype == d1.dtype &&
+		d0.dev == d1.dev;
+}
+
+Loopcheck.new(): ref Loopcheck
+{
+	return ref Loopcheck(array[HASHSIZE] of list of ref Sys->Dir);
+}
+
+# XXX we're assuming no-one modifies the values in d behind our back...
+Loopcheck.enter(l: self ref Loopcheck, d: ref Sys->Dir): int
+{
+	slot := int d.qid.path & (HASHSIZE-1);
+	for(ll := l.a[slot]; ll != nil; ll = tl ll)
+		if(issamedir(d, hd ll))
+			return 0;
+	l.a[slot] = d :: l.a[slot];
+	return 1;
+}
+
+Loopcheck.leave(l: self ref Loopcheck, d: ref Sys->Dir)
+{
+	slot := int d.qid.path & (HASHSIZE-1);
+	l.a[slot] = tl l.a[slot];
+}
--- /dev/null
+++ b/appl/cmd/fs/write.b
@@ -1,0 +1,111 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, quit, report: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "vsx";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil){
+		sys->fprint(sys->fildes(2), "fs: write: cannot load %s: %r\n", Fslib->PATH);
+		raise "fail:bad module";
+	}
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			nil: list of Option, args: list of ref Value): ref Value
+{
+	sync := chan of int;
+	spawn  fswriteproc(sync, (hd args).s().i, (hd tl args).x().i, report.start("fswrite"));
+	<-sync;
+	return ref Value.V(sync);
+}
+
+fswriteproc(sync: chan of int, root: string, c: Fschan, errorc: chan of string)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+	if(<-sync == 0){
+		(<-c).t1 <-= Quit;
+		quit(errorc);
+	}
+		
+	(d, reply) := <-c;
+	if(root != nil){
+		d.dir = ref *d.dir;
+		d.dir.name = root;
+	}
+	fswritedir(d.dir.name, d, reply, c, errorc);
+	quit(errorc);
+}
+
+fswritedir(path: string, d: Fsdata, dreply: chan of int, c: Fschan, errorc: chan of string)
+{
+	fd: ref Sys->FD;
+	if(d.dir.mode & Sys->DMDIR){
+		fd = sys->create(d.dir.name, Sys->OREAD, d.dir.mode|8r300);
+		if(fd == nil && (fd = sys->open(d.dir.name, Sys->OREAD)) == nil){
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot create %q, mode %uo: %r", path, d.dir.mode|8r300));
+			return;
+		}
+		if(sys->chdir(d.dir.name) == -1){		# XXX beware of names starting with '#'
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot cd to %q: %r", path));
+			fd = nil;
+			sys->remove(d.dir.name);
+			return;
+		}
+		dreply <-= Down;
+		path[len path] = '/';
+		for(;;){
+			(ent, reply) := <-c;
+			if(ent.dir == nil){
+				reply <-= Next;
+				break;
+			}
+			fswritedir(path + ent.dir.name, ent, reply, c, errorc);
+		}
+		sys->chdir("..");
+		if((d.dir.mode & 8r300) != 8r300){
+			ws := Sys->nulldir;
+			ws.mode = d.dir.mode;
+			if(sys->fwstat(fd, ws) == -1)
+				report(errorc, sys->sprint("cannot wstat %q: %r", path));
+		}
+	}else{
+		fd = sys->create(d.dir.name, Sys->OWRITE, d.dir.mode);
+		if(fd == nil){
+			dreply <-= Next;
+			report(errorc, sys->sprint("cannot create %q, mode %uo: %r", path, d.dir.mode|8r300));
+			return;
+		}
+		dreply <-= Down;
+		while((((nil, buf), reply) := <-c).t0.data != nil){
+			nw := sys->write(fd, buf, len buf);
+			if(nw < len buf){
+				if(nw == -1)
+					errorc <-= sys->sprint("error writing %q: %r", path);
+				else
+					errorc <-= sys->sprint("short write");
+				reply <-= Skip;
+				break;
+			}
+			reply <-= Next;
+		}
+		reply <-= Next;
+	}
+}
--- /dev/null
+++ b/appl/cmd/ftest.b
@@ -1,0 +1,153 @@
+implement Ftest;
+#
+# test file permissions or attributes
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+stderr: ref Sys->FD;
+
+Ftest: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Topr, Topw, Topx, Tope, Topf, Topd, Tops: con iota;
+
+init(nil: ref Draw->Context, argl: list of string)
+{
+	if(argl == nil)
+		return;
+
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	if (tl argl == nil)
+		usage();
+
+	a := hd tl argl;
+	argl = tl tl argl;
+	ok := 0;
+	case a {
+	"-f" =>
+		ok = filck(nxtarg(argl), Topf);
+	"-d" =>
+		ok = filck(nxtarg(argl), Topd);
+	"-r" =>
+		ok = filck(nxtarg(argl), Topr);
+	"-w" =>
+		ok = filck(nxtarg(argl), Topw);
+	"-x" =>
+		ok = filck(nxtarg(argl), Topx);
+	"-e" =>
+		ok = filck(nxtarg(argl), Tope);
+	"-s" =>
+		ok = filck(nxtarg(argl), Tops);
+	"-t" =>
+		fd := 1;
+		if (argl != nil) {
+			if (!isint(hd argl)) {
+				sys->fprint(stderr, "ftest: bad argument to -t\n");
+				usage();
+			}
+			fd = int hd argl;
+		}
+		ok = isatty(fd);
+	* =>
+		sys->fprint(stderr, "test: unknown option %s\n", a);
+		usage();
+	}
+	if (!ok)
+		raise "fail:false";
+}
+
+nxtarg(argl: list of string): string
+{
+	if(argl == nil) {
+		sys->fprint(stderr, "test: argument expected\n");
+		usage();
+	}
+	return hd argl;
+}
+
+usage()
+{
+	sys->fprint(stderr, "usage: (ftest -fdrwxes file)|(ftest -t fdno)\n");
+	raise "fail:usage";
+}
+
+isint(s: string): int
+{
+	if(s == nil)
+		return 0;
+	for(i := 0; i < len s; i++)
+		if(s[i] < '0' || s[i] > '9')
+			return 0;
+	return 1;
+}
+
+
+filck(fname: string, Top: int): int
+{
+	(ok, dir) := sys->stat(fname);
+
+	if(ok >= 0) {
+		ok = 0;
+		case Top {
+		Topr =>	# readable
+			ok = permck(dir, 8r004);
+		Topw =>	# writable
+			ok = permck(dir, 8r002);
+		Topx =>	# executable
+			ok = permck(dir, 8r001);
+		Tope =>	# exists
+			ok = 1;
+		Topf =>	# is a regular file
+			ok = (dir.mode & Sys->DMDIR) == 0;
+		Topd =>	# is a directory
+			ok = (dir.mode & Sys->DMDIR) != 0;
+		Tops =>	# has length > 0
+			ok = dir.length > big 0;
+		}
+	}
+
+	return ok > 0;
+}
+
+permck(dir: Sys->Dir, mask: int): int
+{
+	uid, gid: string;
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd != nil) {
+		buf := array [28] of byte;
+		n := sys->read(fd, buf, len buf);
+		if(n > 0)
+			uid = string buf[0:n];
+	}
+	# how do I find out what my group is?
+	
+	ok := dir.mode & mask<<0;
+	if(!ok && dir.gid == gid)
+		ok = dir.mode & mask<<3;
+	if(!ok && dir.uid == uid)
+		ok = dir.mode & mask<<6;
+
+	return ok > 0;
+}
+
+isatty(fd: int): int
+{
+	d1, d2: Sys->Dir;
+
+	ok: int;
+	(ok, d1) = sys->fstat(sys->fildes(fd));
+	if(ok < 0)
+		return 0;
+	(ok, d2) = sys->stat("/dev/cons");
+	if(ok < 0)
+		return 0;
+
+	return d1.dtype==d2.dtype && d1.dev==d2.dev && d1.qid.path==d2.qid.path;
+}
--- /dev/null
+++ b/appl/cmd/ftpfs.b
@@ -1,0 +1,1955 @@
+implement Ftpfs;
+
+include "sys.m";
+	sys: Sys;
+	FD, Dir: import Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "daytime.m";
+	time: Daytime;
+	Tm: import time;
+
+include "string.m";
+	str: String;
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+include "dial.m";
+	dial: Dial;
+	Connection: import dial;
+
+include "factotum.m";
+
+Ftpfs: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+#
+#	File system node.  Refers to parent and file structure.
+#	Siblings are linked.  The head is parent.children.
+#
+
+Node: adt
+{
+	dir:		Dir;
+	uniq:		int;
+	parent:		cyclic ref Node;
+	sibs:		cyclic ref Node;
+	children:	cyclic ref Node;
+	file:		cyclic ref File;
+	depth:		int;
+	remname:	string;
+	cached:		int;
+	valid:		int;
+
+	extendpath:	fn(parent: self ref Node, elem: string): ref Node;
+	fixsymbolic:	fn(n: self ref Node);
+	invalidate:	fn(n: self ref Node);
+	markcached:	fn(n: self ref Node);
+	uncache:	fn(n: self ref Node);
+	uncachedir:	fn(parent: self ref Node, child: ref Node);
+
+	stat:		fn(n: self ref Node): array of byte;
+	qid:		fn(n: self ref Node): Sys->Qid;
+
+	fileget:	fn(n: self ref Node): ref File;
+	filefree:	fn(n: self ref Node);
+	fileclean:	fn(n: self ref Node);
+	fileisdirty:	fn(n: self ref Node): int;
+	filedirty:	fn(n: self ref Node);
+	fileread:	fn(n: self ref Node, b: array of byte, off, c: int): int;
+	filewrite:	fn(n: self ref Node, b: array of byte, off, c: int): int;
+
+	action:		fn(n: self ref Node, cmd: string): int;
+	createdir:	fn(n: self ref Node): int;
+	createfile:	fn(n: self ref Node): int;
+	changedir:	fn(n: self ref Node): int;
+	docreate:	fn(n: self ref Node): int;
+	pathname:	fn(n: self ref Node): string;
+	readdir:	fn(n: self ref Node): int;
+	readfile:	fn(n: self ref Node): int;
+	removedir:	fn(n: self ref Node): int;
+	removefile:	fn(n: self ref Node): int;
+};
+
+#
+#	Styx protocol file identifier.
+#
+
+Fid: adt
+{
+	fid:	int;
+	node:	ref Node;
+	busy:	int;
+};
+
+#
+#	Foreign file with cache.
+#
+
+File: adt
+{
+	cache:		array of byte;
+	length:		int;
+	offset:		int;
+	fd:		ref FD;
+	inuse, dirty:	int;
+	atime:		int;
+	node:		cyclic ref Node;
+	tempname:	string;
+
+	createtmp:	fn(f: self ref File): ref FD;
+};
+
+ftp:		ref Connection;
+dfid:			ref FD;
+dfidiob:		ref Iobuf;
+buffresidue:	int = 0;
+tbuff:		array of byte;
+rbuff:		array of byte;
+ccfd:			ref FD;
+stdin, stderr:	ref FD;
+
+fids:		list of ref Fid;
+
+BSZ:		con 8192;
+Chunk:		con 1024;
+Nfiles:		con 128;
+
+CHSYML:		con 16r40000000;
+
+mountpoint:	string = "/n/ftp";
+user:			string = nil;
+password:		string;
+hostname:	string = "kremvax";
+anon:		string = "anon";
+
+firewall:		string = "tcp!$proxy!402";
+myname:		string = "anon";
+myhost:		string = "lucent.com";
+proxyid:		string;
+proxyhost:	string;
+
+errstr:		string;
+net:			string;
+port:			int;
+
+Enosuchfile:	con "file does not exist";
+Eftpproto:	con "ftp protocol error";
+Eshutdown:	con "remote shutdown";
+Eioerror:	con "io error";
+Enotadirectory:	con "not a directory";
+Eisadirectory:	con "is a directory";
+Epermission:	con "permission denied";
+Ebadoffset:	con "bad offset";
+Ebadlength:	con "bad length";
+Enowstat:	con "wstat not implemented";
+Emesgmismatch:	con "message size mismatch";
+
+remdir:		ref Node;
+remroot:	ref Node;
+remrootpath:	string;
+
+heartbeatpid: int;
+
+#
+#	FTP protocol codes are 3 digits >= 100.
+#	The code type is obtained by dividing by 100.
+#
+
+Syserr:		con -2;
+Syntax:		con -1;
+Shutdown:	con 0;
+Extra:		con 1;
+Success:	con 2;
+Incomplete:	con 3;
+TempFail:	con 4;
+PermFail:	con 5;
+Impossible:	con 6;
+Err:		con 7;
+
+debug:		int = 0;
+quiet:		int = 0;
+active:		int = 0;
+cdtoroot:		int = 0;
+
+proxy:		int = 0;
+
+mountfd:	ref FD;
+styxfd: ref FD;
+
+#
+#	Set up FDs for service.
+#
+
+connect(): string
+{
+	pip := array[2] of ref Sys->FD;
+	if(sys->pipe(pip) < 0)
+		return sys->sprint("can't create pipe: %r");
+	mountfd = pip[0];
+	styxfd = pip[1];
+	return nil;
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "ftpfs: %s\n", s);
+	raise "fail:"+s;
+}
+
+#
+#	Mount server.  Must be spawned because it does
+#	an attach transaction.
+#
+
+mount(mountpoint: string)
+{
+	if (sys->mount(mountfd, nil, mountpoint, Sys->MREPL | Sys->MCREATE, nil) < 0) {
+		sys->fprint(sys->fildes(2), "ftpfs: mount %s failed: %r\n", mountpoint);
+		shutdown();
+	}
+	mountfd = nil;
+}
+
+#
+#	Keep the link alive.
+#
+
+beatquanta:	con 10;
+beatlimit:	con 10;
+beatcount:	int;
+activity:	int;
+transfer:	int;
+
+heartbeat(pidc: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	pidc <-= pid;
+	for (;;) {
+		sys->sleep(beatquanta * 1000);
+		if (activity || transfer) {
+			beatcount = 0;
+			activity = 0;
+			continue;
+		}
+		beatcount++;
+		if (beatcount == beatlimit) {
+			acquire();
+			if (sendrequest("NOOP", 0) == Success)
+				getreply(0);
+			release();
+			beatcount = 0;
+			activity = 0;
+		}
+	}
+}
+
+#
+#	Control lock.
+#
+
+ctllock: chan of int;
+
+acquire()
+{
+	ctllock <-= 1;
+}
+
+release()
+{
+	<-ctllock;
+}
+
+#
+#	Data formatting routines.
+#
+
+sendreply(r: ref Rmsg)
+{
+	if (debug)
+		sys->print("> %s\n", r.text());
+	a := r.pack();
+	if(sys->write(styxfd, a, len a) != len a)
+		sys->print("ftpfs: error replying: %r\n");
+}
+
+rerror(tag: int, s: string)
+{
+	if (debug)
+		sys->print("error: %s\n", s);
+	sendreply(ref Rmsg.Error(tag, s));
+}
+
+seterr(e: int, s: string): int
+{
+	case e {
+	Syserr =>
+		errstr = Eioerror;
+	Syntax =>
+		errstr = Eftpproto;
+	Shutdown =>
+		errstr = Eshutdown;
+	* =>
+		errstr = s;
+	}
+	return -1;
+}
+
+#
+#	Node routines.
+#
+
+anode:	Node;
+npath:	int	= 1;
+
+newnode(parent: ref Node, name: string): ref Node
+{
+	n := ref anode;
+	n.dir.name = name;
+	n.dir.atime = time->now();
+	n.children = nil;
+	n.remname = name;
+	if (parent != nil) {
+		n.parent = parent;
+		n.sibs = parent.children;
+		parent.children = n;
+		n.depth = parent.depth + 1;
+		n.valid = 0;
+	} else {
+		n.parent = n;
+		n.sibs = nil;
+		n.depth = 0;
+		n.valid = 1;
+		n.dir.uid = anon;
+		n.dir.gid = anon;
+		n.dir.mtime = n.dir.atime;
+	}
+	n.file = nil;
+	n.uniq = npath++;
+	n.cached = 0;
+	return n;
+}
+
+Node.extendpath(parent: self ref Node, elem: string): ref Node
+{
+	n: ref Node;
+
+	for (n = parent.children; n != nil; n = n.sibs)
+		if (n.dir.name == elem)
+			return n;
+	return newnode(parent, elem);
+}
+
+Node.markcached(n: self ref Node)
+{
+	n.cached = 1;
+	n.dir.atime = time->now();
+}
+
+Node.uncache(n: self ref Node)
+{
+	if (n.fileisdirty())
+		n.createfile();
+	n.filefree();
+	n.cached = 0;
+}
+
+Node.uncachedir(parent: self ref Node, child: ref Node)
+{
+	sp: ref Node;
+
+	if (parent == nil || parent == child)
+		return;
+	for (sp = parent.children; sp != nil; sp = sp.sibs)
+		if (sp != child && sp.file != nil && !sp.file.dirty && sp.file.fd != nil) {
+			sp.filefree();
+			sp.cached = 0;
+		}
+}
+
+Node.invalidate(node: self ref Node)
+{
+	n: ref Node;
+
+	node.uncachedir(nil);
+	for (n = node.children; n != nil; n = n.sibs) {
+		n.cached = 0;
+		n.invalidate();
+		n.valid = 0;
+	}
+}
+
+Node.fixsymbolic(n: self ref Node)
+{
+	if (n.changedir() == 0) {
+		n.dir.mode |= Sys->DMDIR; 
+		n.dir.qid.qtype = Sys->QTDIR;
+	} else
+		n.dir.qid.qtype = Sys->QTFILE;
+	n.dir.mode &= ~CHSYML; 
+}
+
+Node.stat(n: self ref Node): array of byte
+{
+	return styx->packdir(n.dir);
+}
+
+Node.qid(n: self ref Node): Sys->Qid
+{
+	if(n.dir.mode & Sys->DMDIR)
+		return Sys->Qid(big n.uniq, 0, Sys->QTDIR);
+	return Sys->Qid(big n.uniq, 0, Sys->QTFILE);
+}
+
+#
+#	File routines.
+#
+
+ntmp:	int;
+files:	list of ref File;
+nfiles:	int;
+afile:	File;
+atime:	int;
+
+#
+#	Allocate a file structure for a node.  If too many
+#	are already allocated discard the oldest.
+#
+
+Node.fileget(n: self ref Node): ref File
+{
+	f, o: ref File;
+	l: list of ref File;
+
+	if (n.file != nil)
+		return n.file;
+	o = nil;
+	for (l = files; l != nil; l = tl l) {
+		f = hd l;
+		if (f.inuse == 0)
+			break;
+		if (!f.dirty && (o == nil || o.atime > f.atime))
+			o = f;
+	}
+	if (l == nil) {
+		if (nfiles == Nfiles && o != nil) {
+			o.node.uncache();
+			f = o;
+		}
+		else {
+			f = ref afile;
+			files = f :: files;
+			nfiles++;
+		}
+	}
+	n.file = f;
+	f.node = n;
+	f.atime = atime++;
+	f.inuse = 1;
+	f.dirty = 0;
+	f.length = 0;
+	f.fd = nil;
+	return f;
+}
+
+#
+#	Create a temporary file for a local copy of a file.
+#	If too many are open uncache parent.
+#
+
+File.createtmp(f: self ref File): ref FD
+{
+	t := "/tmp/ftp." + string time->now() + "." + string ntmp;
+	if (ntmp >= 16)
+		f.node.parent.uncachedir(f.node);
+	f.fd = sys->create(t, Sys->ORDWR | Sys->ORCLOSE, 8r600);
+	f.tempname = t;
+	f.offset = 0;
+	ntmp++;
+	return f.fd;
+}
+
+#
+#	Read 'c' bytes at offset 'off' from a file into buffer 'b'.
+#
+
+Node.fileread(n: self ref Node, b: array of byte, off, c: int): int
+{
+	f: ref File;
+	t, i: int;
+
+	f = n.file;
+	if (off + c > f.length)
+		c = f.length - off;
+	for (t = 0; t < c; t += i) {
+		if (off >= f.length)
+			return t;
+		if (off < Chunk) {
+			i = c;
+			if (off + i > Chunk)
+				i = Chunk - off;
+			b[t:] = f.cache[off: off + i];
+		}
+		else {
+			if (f.offset != off) {
+				if (sys->seek(f.fd, big off, Sys->SEEKSTART) < big 0) {
+					f.offset = -1;
+					return seterr(Err, sys->sprint("seek temp failed: %r"));
+				}
+			}
+			if (t == 0)
+				i = sys->read(f.fd, b, c - t);
+			else
+				i = sys->read(f.fd, rbuff, c - t);
+			if (i < 0) {
+				f.offset = -1;
+				return seterr(Err, sys->sprint("read temp failed: %r"));
+			}
+			if (i == 0)
+				break;
+			if (t > 0)
+				b[t:] = rbuff[0: i];
+			f.offset = off + i;
+		}
+		off += i;
+	}
+	return t;
+}
+
+#
+#	Write 'c' bytes at offset 'off' to a file from buffer 'b'.
+#
+
+Node.filewrite(n: self ref Node, b: array of byte, off, c: int): int
+{
+	f: ref File;
+	t, i: int;
+
+	f = n.fileget();
+	if (f.cache == nil)
+		f.cache = array[Chunk] of byte;
+	for (t = 0; t < c; t += i) {
+		if (off < Chunk) {
+			i = c;
+			if (off + i > Chunk)
+				i = Chunk - off;
+			f.cache[off:] = b[t: t + i];
+		}
+		else {
+			if (f.fd == nil) {
+				if (f.createtmp() == nil)
+					return seterr(Err, sys->sprint("temp file: %r"));
+				if (sys->write(f.fd, f.cache, Chunk) != Chunk) {
+					f.offset = -1;
+					return seterr(Err, sys->sprint("write temp failed: %r"));
+				}
+				f.offset = Chunk;
+				f.length = Chunk;
+			}
+			if (f.offset != off) {
+				if (off > f.length) {
+					# extend the file with zeroes
+					# sparse files may not be supported
+				}
+				if (sys->seek(f.fd, big off, Sys->SEEKSTART) < big 0) {
+					f.offset = -1;
+					return seterr(Err, sys->sprint("seek temp failed: %r"));
+				}
+			}
+			i = sys->write(f.fd, b[t:len b], c - t);
+			if (i != c - t) {
+				f.offset = -1;
+				return seterr(Err, sys->sprint("write temp failed: %r"));
+			}
+		}
+		off += i;
+		f.offset = off;
+	}
+	if (off > f.length)
+		f.length = off;
+	return t;
+}
+
+Node.filefree(n: self ref Node)
+{
+	f: ref File;
+
+	f = n.file;
+	if (f == nil)
+		return;
+	if (f.fd != nil) {
+		ntmp--;
+		f.fd = nil;
+		f.tempname = nil;
+	}
+	f.cache = nil;
+	f.length = 0;
+	f.inuse = 0;
+	f.dirty = 0;
+	n.file = nil;
+}
+
+Node.fileclean(n: self ref Node)
+{
+	if (n.file != nil)
+		n.file.dirty = 0;
+}
+
+Node.fileisdirty(n: self ref Node): int
+{
+	return n.file != nil && n.file.dirty;
+}
+
+Node.filedirty(n: self ref Node)
+{
+	f: ref File;
+
+	f = n.fileget();
+	f.dirty = 1;
+}
+
+#
+#	Fid management.
+#
+
+afid:	Fid;
+
+getfid(fid: int): ref Fid
+{
+	l: list of ref Fid;
+	f, ff: ref Fid;
+
+	ff = nil;
+	for (l = fids; l != nil; l = tl l) {
+		f = hd l;
+		if (f.fid == fid) {
+			if (f.busy)
+				return f;
+			else {
+				ff = f;
+				break;
+			}
+		} else if (ff == nil && !f.busy)
+			ff = f;
+	}
+	if (ff == nil) {
+		ff = ref afid;
+		fids = ff :: fids;
+	}
+	ff.node = nil;
+	ff.fid = fid;
+	return ff;
+}
+
+#
+#	FTP protocol.
+#
+
+fail(s: int, l: string)
+{
+	case s {
+	Syserr =>
+		sys->print("read fail: %r\n");
+	Syntax =>
+		sys->print("%s\n", Eftpproto);
+	Shutdown =>
+		sys->print("%s\n", Eshutdown);
+	* =>
+		sys->print("unexpected response: %s\n", l);
+	}
+	exit;
+}
+
+getfullreply(echo: int): (int, int, string)
+{
+	reply := "";
+	s: string;
+	code := -1;
+	do{
+		s = dfidiob.gets('\n');
+		if(s == nil)
+			return (Shutdown, 0, nil);
+		if(len s >= 2 && s[len s-1] == '\n'){
+			if (s[len s - 2] == '\r')
+				s = s[0: len s - 2];
+			else
+				s = s[0: len s - 1];
+		}
+		if (debug || echo)
+			sys->print("%s\n", s);
+		reply = reply+s;
+		if(code < 0){
+			if(len s < 3)
+				return (Syntax, 0, nil);
+			code = int s[0:3];
+			if(s[3] != '-')
+				break;
+		}
+	}while(len s < 4 || int s[0:3] != code || s[3] != ' ');
+
+	if(code < 100)
+		return (Syntax, 0, nil);
+	return (code / 100, code, reply);
+}
+
+getreply(echo: int): (int, string)
+{
+	(c, nil, s) := getfullreply(echo);
+	return (c, s);
+}
+
+sendrequest2(req: string, echo: int, figleaf: string): int
+{
+	activity = 1;
+	if (debug || echo) {
+		if (figleaf == nil)
+			figleaf = req;
+		sys->print("%s\n", figleaf);
+	}
+	b := array of byte (req + "\r\n");
+	n := sys->write(dfid, b, len b);
+	if (n < 0)
+		return Syserr;
+	if (n != len b)
+		return Shutdown;
+	return Success;
+}
+
+sendrequest(req: string, echo: int): int
+{
+	return sendrequest2(req, echo, req);
+}
+
+sendfail(s: int)
+{
+	case s {
+	Syserr =>
+		sys->print("write fail: %r\n");
+	Shutdown =>
+		sys->print("%s\n", Eshutdown);
+	* =>
+		sys->print("internal error\n");
+	}
+	exit;
+}
+
+dataport(l: list of string): string
+{
+	s := "tcp!" + hd l;
+	l = tl l;
+	s = s + "." + hd l;
+	l = tl l;
+	s = s + "." + hd l;
+	l = tl l;
+	s = s + "." + hd l;
+	l = tl l;
+	return s + "!" + string ((int hd l * 256) + (int hd tl l));
+}
+
+commas(l: list of string): string
+{
+	s := hd l;
+	l = tl l;
+	while (l != nil) {
+		s = s + "," + hd l;
+		l = tl l;
+	}
+	return s;
+}
+
+third(cmd: string): ref FD
+{
+	acquire();
+	for (;;) {
+		data := dial->dial(firewall, nil);
+		if(data == nil) {
+			if (debug)
+				sys->print("dial %s failed: %r\n", firewall);
+			break;
+		}
+		t := sys->sprint("\n%s!*\n\n%s\n%s\n1\n-1\n-1\n", proxyhost, myhost, myname);
+		b := array of byte t;
+		n := sys->write(data.dfd, b, len b);
+		if (n < 0) {
+			if (debug)
+				sys->print("firewall write failed: %r\n");
+			break;
+		}
+		b = array[256] of byte;
+		n = sys->read(data.dfd, b, len b);
+		if (n < 0) {
+			if (debug)
+				sys->print("firewall read failed: %r\n");
+			break;
+		}
+		(c, k) := sys->tokenize(string b[:n], "\n");
+		if (c < 2) {
+			if (debug)
+				sys->print("bad response from firewall\n");
+			break;
+		}
+		if (hd k != "0") {
+			if (debug)
+				sys->print("firewall connect: %s\n", hd tl k);
+			break;
+		}
+		p := hd tl k;
+		if (debug)
+			sys->print("portid %s\n", p);
+		(c, k) = sys->tokenize(p, "!");
+		if (c < 3) {
+			if (debug)
+				sys->print("bad portid from firewall\n");
+			break;
+		}
+		n = int hd tl tl k;
+		(c, k) = sys->tokenize(hd tl k, ".");
+		if (c != 4) {
+			if (debug)
+				sys->print("bad portid ip address\n");
+			break;
+		}
+		t = sys->sprint("PORT %s,%d,%d", commas(k), n / 256, n & 255);
+		r := sendrequest(t, 0);
+		if (r != Success)
+			break;
+		(r, nil) = getreply(0);
+		if (r != Success)
+			break;
+		r = sendrequest(cmd, 0);
+		if (r != Success)
+			break;
+		(r, nil) = getreply(0);
+		if (r != Extra)
+			break;
+		n = sys->read(data.dfd, b, len b);
+		if (n < 0) {
+			if (debug)
+				sys->print("firewall read failed: %r\n");
+			break;
+		}
+		b = array of byte "0\n?\n";
+		n = sys->write(data.dfd, b, len b);
+		if (n < 0) {
+			if (debug)
+				sys->print("firewall write failed: %r\n");
+			break;
+		}
+		release();
+		return data.dfd;
+	}
+	release();
+	return nil;
+}
+
+passive(cmd: string): ref FD
+{
+	acquire();
+	if (sendrequest("PASV", 0) != Success) {
+		release();
+		return nil;
+	}
+	(r, m) := getreply(0);
+	release();
+	if (r != Success)
+		return nil;
+	(nil, p) := str->splitl(m, "(");
+	if (p == nil)
+		str->splitl(m, "0-9");
+	else
+		p = p[1:len p];
+	(c, l) := sys->tokenize(p, ",");
+	if (c < 6) {
+		sys->print("data: %s\n", m);
+		return nil;
+	}
+	a := dataport(l);
+	if (debug)
+		sys->print("data dial %s\n", a);
+	d := dial->dial(a, nil);
+	if(d == nil)
+		return nil;
+	acquire();
+	r = sendrequest(cmd, 0);
+	if (r != Success) {
+		release();
+		return nil;
+	}
+	(r, m) = getreply(0);
+	release();
+	if (r != Extra)
+		return nil;
+	return d.dfd;
+}
+
+getnet(dir: string): (string, int)
+{
+	buf := array[50] of byte;
+	n := dir + "/local";
+	lfd := sys->open(n, Sys->OREAD);
+	if (lfd == nil) {
+		if (debug)
+			sys->fprint(stderr, "open %s: %r\n", n);
+		return (nil, 0);
+	}
+	length := sys->read(lfd, buf, len buf);
+	if (length < 0) {
+		if (debug)
+			sys->fprint(stderr, "read%s: %r\n", n);
+		return (nil, 0);
+	}
+	(r, l) := sys->tokenize(string buf[0:length], "!");
+	if (r != 2) {
+		if (debug)
+			sys->fprint(stderr, "tokenize(%s) returned (%d)\n", string buf[0:length], r);
+		return (nil, 0);
+	}
+	if (debug)
+		sys->print("net is %s!%d\n", hd l, int hd tl l);
+	return (hd l, int hd tl l);
+}
+	
+activate(cmd: string): ref FD
+{
+	r: int;
+
+	listenport, dataport: ref Connection;
+	m: string;
+
+	listenport = dial->announce("tcp!" + net + "!0");
+	if(listenport == nil)
+		return nil;
+	(x1, x2)  := getnet(listenport.dir);
+	(nil, x4) := sys->tokenize(x1, ".");
+	t := sys->sprint("PORT %s,%d,%d", commas(x4), int x2 / 256, int x2&255);
+	acquire();
+	r = sendrequest(t, 0);
+	if (r != Success) {
+		release();
+		return nil;
+	}
+	(r, m) = getreply(0);
+	if (r != Success) {
+		release();
+		return nil;
+	}
+	r = sendrequest(cmd, 0);
+	if (r != Success) {
+		release();
+		return nil;
+	}
+	(r, m) = getreply(0);
+	release();
+	if (r != Extra)
+		return nil;
+	dataport = dial->listen(listenport);
+	if(dataport == nil) {
+		sys->fprint(stderr, "activate: listen failed: %r\n");
+		return nil;
+	}
+	fd := sys->open(dataport.dir + "/data", sys->ORDWR);
+	if (debug)
+		sys->print("activate: data connection on %s\n", dataport.dir);
+	if (fd == nil) {
+		sys->fprint(stderr, "activate: open of %s failed: %r\n", dataport.dir);
+		return nil;
+	}
+	return fd;
+}
+
+data(cmd: string): ref FD
+{
+	if (proxy)
+		return third(cmd);
+	else if (active)
+		return activate(cmd);
+	else
+		return passive(cmd);
+}
+
+#
+#	File list cracking routines.
+#
+
+fields(l: list of string, n: int): array of string
+{
+	a := array[n] of string;
+	for (i := 0; i < n; i++) {
+		a[i] = hd l;
+		l = tl l;
+	}
+	return a;
+}
+
+now:	ref Tm;
+months:	con "janfebmaraprmayjunjulaugsepoctnovdec";
+
+cracktime(month, day, year, hms: string): int
+{
+	tm: Tm;
+
+	if (now == nil)
+		now = time->local(time->now());
+	tm = *now;
+	if (month[0] >= '0' && month[0] <= '9') {
+		tm.mon = int month - 1;
+		if (tm.mon < 0 || tm.mon > 11)
+			tm.mon = 5;
+	}
+	else if (len month >= 3) {
+		month = str->tolower(month[0:3]);
+		for (i := 0; i < 36; i += 3)
+			if (month == months[i:i+3]) {
+				tm.mon = i / 3;
+				break;
+			}
+	}
+	tm.mday = int day;
+	if (hms != nil) {
+		(h, z) := str->splitl(hms, "apAP");
+		(a, b) := str->splitl(h, ":");
+		tm.hour = int a;
+		if (b != nil) {
+			(c, d) := str->splitl(b[1:len b], ":");
+			tm.min = int c;
+			if (d != nil)
+				tm.sec = int d[1:len d];
+		}
+		if (z != nil && str->tolower(z)[0] == 'p')
+			tm.hour += 12;
+	}
+	if (year != nil) {
+		tm.year = int year;
+		if (tm.year >= 1900)
+			tm.year -= 1900;
+	}
+	else {
+		if (tm.mon > now.mon || (tm.mon == now.mon && tm.mday > now.mday+1))
+			tm.year--;
+	}
+	return time->tm2epoch(ref tm);
+}
+
+crackmode(p: string): int
+{
+	flags := 0;
+	case len p {
+	10 =>	# unix and new style plan 9
+		case p[0] {
+		'l' =>
+			return CHSYML | 0777;
+		'd' =>
+			flags = Sys->DMDIR;
+		}
+		p = p[1:10];
+	11 =>	# old style plan 9
+		if (p[0] == 'l')
+			flags = Sys->DMDIR;
+		p = p[2:11];
+	* =>
+		return Sys->DMDIR | 0777;
+	}
+	mode := 0;
+	n := 0;
+	for (i := 0; i < 3; i++) {
+		mode <<= 3;
+		if (p[n] == 'r')
+			mode |= 4;
+		if (p[n+1] == 'w')
+			mode |= 2;
+		case p[n+2] {
+		'x' or 's' or 'S' =>
+			mode |= 1;
+		}
+		n += 3;
+	}
+	return mode | flags;
+}
+
+crackdir(p: string): (string, Dir)
+{
+	d: Dir;
+	ln, a: string;
+
+	(n, l) := sys->tokenize(p, " \t\r\n");
+	f := fields(l, n);
+	if (n > 2 && f[n - 2] == "->")
+		n -= 2;
+	case n {
+	8 =>	# ls -l
+		ln = f[7];
+		d.uid = f[2];
+		d.gid = f[2];
+		d.mode = crackmode(f[0]);
+		d.length = big f[3];
+		(a, nil) = str->splitl(f[6], ":");
+		if (len a != len f[6])
+			d.atime = cracktime(f[4], f[5], nil, f[6]);
+		else
+			d.atime = cracktime(f[4], f[5], f[6], nil);
+	9 =>	# ls -lg
+		ln = f[8];
+		d.uid = f[2];
+		d.gid = f[3];
+		d.mode = crackmode(f[0]);
+		d.length = big f[4];
+		(a, nil) = str->splitl(f[7], ":");
+		if (len a != len f[7])
+			d.atime = cracktime(f[5], f[6], nil, f[7]);
+		else
+			d.atime = cracktime(f[5], f[6], f[7], nil);
+	10 =>	# plan 9
+		ln = f[9];
+		d.uid = f[3];
+		d.gid = f[4];
+		d.mode = crackmode(f[0]);
+		d.length = big f[5];
+		(a, nil) = str->splitl(f[8], ":");
+		if (len a != len f[8])
+			d.atime = cracktime(f[6], f[7], nil, f[8]);
+		else
+			d.atime = cracktime(f[6], f[7], f[8], nil);
+	4 =>	# NT
+		ln = f[3];
+		d.uid = anon;
+		d.gid = anon;
+		if (f[2] == "<DIR>") {
+			d.length = big 0;
+			d.mode = Sys->DMDIR | 8r777;
+		}
+		else {
+			d.mode = 8r666;
+			d.length = big f[2];
+		}
+		(n, l) = sys->tokenize(f[0], "/-");
+		if (n == 3)
+			d.atime = cracktime(hd l, hd tl l, f[2], f[1]);
+	1 =>	# ls
+		ln = f[0];
+		d.uid = anon;
+		d.gid = anon;
+		d.mode = 0777;
+		d.atime = 0;
+	* =>
+		return (nil, d);
+	}
+	if (ln == "." || ln == "..")
+		return (nil, d);
+	d.mtime = d.atime;
+	d.name = ln;
+	return (ln, d);
+}
+
+longls := 1;
+
+Node.readdir(n: self ref Node): int
+{
+	f: ref FD;
+	p: ref Node;
+
+	if (n.changedir() < 0)
+		return -1;
+	transfer = 1;
+	for (;;) {
+		if (longls) {
+			f = data("LIST -la");
+			if (f == nil) {
+				longls = 0;
+				continue;
+			}
+		}
+		else {
+			f = data("LIST");
+			if (f == nil) {
+				transfer = 0;
+				return seterr(Err, Enosuchfile);
+			}
+		}
+		break;
+	}
+	b := bufio->fopen(f, sys->OREAD);
+	if (b == nil) {
+		transfer = 0;
+		return seterr(Err, Eioerror);
+	}
+	while ((s := b.gets('\n')) != nil) {
+		if (debug)
+			sys->print("%s", s);
+		(l, d) := crackdir(s);
+		if (l == nil)
+			continue;
+		p = n.extendpath(l);
+		p.dir = d;
+		p.valid = 1;
+	}
+	b = nil;
+	f = nil;
+	(r, nil) := getreply(0);
+	transfer = 0;
+	if (r != Success)
+		return seterr(Err, Enosuchfile);
+	return 0;
+}
+
+Node.readfile(n: self ref Node): int
+{
+	c: int;
+
+	if (n.parent.changedir() < 0)
+		return -1;
+	transfer = 1;
+	f := data("RETR " + n.remname);
+	if (f == nil) {
+		transfer = 0;
+		return seterr(Err, Enosuchfile);
+	}
+	off := 0;
+	while ((c = sys->read(f, tbuff, BSZ)) > 0) {
+		if (n.filewrite(tbuff, off, c) != c) {
+			off = -1;
+			break;
+		}
+		off += c;
+	}
+	if (c < 0) {
+		transfer = 0;
+		return seterr(Err, Eioerror);
+	}
+	f = nil;
+	if(off == 0)
+		n.filewrite(tbuff, off, 0);
+	(s, nil) := getreply(0);
+	transfer = 0;
+	if (s != Success)
+		return seterr(s, Enosuchfile);
+	return off;
+}
+
+path(a, b: string): string
+{
+	if (a == nil)
+		return b;
+	if (b == nil)
+		return a;
+	if (a[len a - 1] == '/')
+		return a + b;
+	else
+		return a + "/" + b;
+}
+
+Node.pathname(n: self ref Node): string
+{
+	s: string;
+
+	while (n != n.parent) {
+		s = path(n.remname, s);
+		n = n.parent;
+	}
+	return path(remrootpath, s);
+}
+
+Node.changedir(n: self ref Node): int
+{
+	t: ref Node;
+	d: string;
+
+	t = n;
+	if (t == remdir)
+		return 0;
+	if (n.depth == 0)
+		d = remrootpath;
+	else
+		d = n.pathname();
+	remdir.uncachedir(nil);
+	acquire();
+	r := sendrequest("CWD " + d, 0);
+	if (r == Success)
+		(r, nil) = getreply(0);
+	release();
+	case r {
+	Success
+#	or Incomplete
+		=>
+		remdir = n;
+		return 0;
+	* =>
+		return seterr(r, Enosuchfile);
+	}
+}
+
+Node.docreate(n: self ref Node): int
+{
+	f: ref FD;
+
+	transfer = 1;
+	f = data("STOR " + n.remname);
+	if (f == nil) {
+		transfer = 0;
+		return -1;
+	}
+	off := 0;
+	for (;;) {
+		r := n.fileread(tbuff, off, BSZ);
+		if (r <= 0)
+			break;
+		if (sys->write(f, tbuff, r) < 0) {
+			off = -1;
+			break;
+		}
+		off += r;
+	}
+	transfer = 0;
+	return off;
+}
+
+Node.createfile(n: self ref Node): int
+{
+	if (n.parent.changedir() < 0)
+		return -1;
+	off := n.docreate();
+	if (off < 0)
+		return -1;
+	(r, nil) := getreply(0);
+	if (r != Success)
+		return -1;
+	return off;
+}
+
+Node.action(n: self ref Node, cmd: string): int
+{
+	if (n.parent.changedir() < 0)
+		return -1;
+	acquire();
+	r := sendrequest(cmd + " " + n.dir.name, 0);
+	if (r == Success)
+		(r, nil) = getreply(0);
+	release();
+	if (r != Success)
+		return -1;
+	return 0;
+}
+
+Node.createdir(n: self ref Node): int
+{
+	return n.action("MKD");
+}
+
+Node.removefile(n: self ref Node): int
+{
+	return n.action("DELE");
+}
+
+Node.removedir(n: self ref Node): int
+{
+	return n.action("RMD");
+}
+
+pwd(s: string): string
+{
+	(nil, s) = str->splitl(s, "\"");
+	if (s == nil || len s < 2)
+		return "/";
+	(s, nil) = str->splitl(s[1:len s], "\"");
+	return s;
+}
+
+#
+#	User info for firewall.
+#
+getuser()
+{
+	b := array[Sys->NAMEMAX] of byte;
+	f := sys->open("/dev/user", Sys->OREAD);
+	if (f != nil) {
+		n := sys->read(f, b, len b);
+		if (n > 0)
+			myname = string b[:n];
+		else if (n == 0)
+			sys->print("warning: empty /dev/user\n");
+		else
+			sys->print("warning: could not read /dev/user: %r\n");
+	} else
+		sys->print("warning: could not open /dev/user: %r\n");
+	f = sys->open("/dev/sysname", Sys->OREAD);
+	if (f != nil) {
+		n := sys->read(f, b, len b);
+		if (n > 0)
+			myhost = string b[:n];
+		else if (n == 0)
+			sys->print("warning: empty /dev/sysname\n");
+		else
+			sys->print("warning: could not read /dev/sysname: %r\n");
+	} else
+		sys->print("warning: could not open /dev/sysname: %r\n");
+	if (debug)
+		sys->print("proxy %s for %s@%s\n", firewall, myname, myhost);
+}
+
+server()
+{
+	while((t := Tmsg.read(styxfd, 0)) != nil){
+		if (debug)
+			sys->print("< %s\n", t.text());
+		pick x := t {
+		Readerror =>
+			sys->print("ftpfs: read error on mount point: %s\n", x.error);
+			kill(heartbeatpid);
+			exit;
+		Version =>
+			versionT(x);
+		Auth =>
+			authT(x);
+		Attach =>
+			attachT(x);
+		Clunk =>
+			clunkT(x);
+		Create =>
+			createT(x);
+		Flush =>
+			flushT(x);
+		Open =>
+			openT(x);
+		Read =>
+			readT(x);
+		Remove =>
+			removeT(x);
+		Stat =>
+			statT(x);
+		Walk =>
+			walkT(x);
+		Write =>
+			writeT(x);
+		Wstat =>
+			wstatT(x);
+		* =>
+			rerror(t.tag, "unimp");
+		}
+	}
+	if (debug)
+		sys->print("ftpfs: server: exiting\n");
+	kill(heartbeatpid);
+}
+
+raw(on: int)
+{
+	if(ccfd == nil) {
+		ccfd = sys->open("/dev/consctl", Sys->OWRITE);
+		if(ccfd == nil) {
+			sys->fprint(stderr, "ftpfs: cannot open /dev/consctl: %r\n");
+			return;
+		}
+	}
+	if(on)
+		sys->fprint(ccfd, "rawon");
+	else
+		sys->fprint(ccfd, "rawoff");
+}
+
+prompt(p: string, def: string, echo: int): string
+{
+	if (def == nil)
+		sys->print("%s: ", p);
+	else
+		sys->print("%s[%s]: ", p, def);
+	if (!echo)
+		raw(1);
+	b := bufio->fopen(stdin, Sys->OREAD);
+	s := b.gets(int '\n');
+	if (!echo) {
+		raw(0);
+		sys->print("\n");
+	}
+	if(s != nil)
+		s = s[0:len s - 1];
+	if (s == "")
+		return def;
+	return s;
+}
+
+#
+#	Entry point.  Load modules and initiate protocol.
+#
+
+nomod(s: string)
+{
+	sys->fprint(sys->fildes(2), "ftpfs: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	l: string;
+	rv: int;
+	code: int;
+
+	sys = load Sys Sys->PATH;
+	dial = load Dial Dial->PATH;
+	stdin = sys->fildes(0);
+	stderr = sys->fildes(2);
+
+	time = load Daytime Daytime->PATH;
+	if (time == nil)
+		nomod(Daytime->PATH);
+	str = load String String->PATH;
+	if (str == nil)
+		nomod(String->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		nomod(Bufio->PATH);
+	styx = load Styx Styx->PATH;
+	if (styx == nil)
+		nomod(Styx->PATH);
+	styx->init();
+	arg := load Arg Arg->PATH;	
+	if(arg == nil)
+		nomod(Arg->PATH);
+
+	# parse arguments
+	# [-/dpq] [-m mountpoint] [-a password] host
+	arg->init(args);
+	arg->setusage("ftpfs [-/dpq] [-m mountpoint] [-a password] ftphost");
+	keyspec := "";
+	while((op := arg->opt()) != 0)
+		case op {
+		'd' =>
+			debug++;
+		'/' =>
+			cdtoroot = 1;
+		'p' =>
+			active = 1;
+		'q' =>
+			quiet = 1;
+		'm' =>
+			mountpoint = arg->earg();
+		'a' =>
+			password = arg->earg();
+			user = "anonymous";
+		'k' =>
+			keyspec = arg->earg();
+		* =>
+			arg->usage();
+		}
+	argv := arg->argv();
+	if (len argv != 1)
+		arg->usage();
+	arg = nil;
+	hostname = hd argv;
+
+	if (len hostname > 6 && hostname[:6] == "proxy!") {
+		hostname = hostname[6:];
+		proxy = 1;
+	}
+		
+	if (proxy) {
+		if (!quiet)
+			sys->print("dial firewall service %s\n", firewall);
+		ftp = dial->dial(firewall, nil);
+		if(ftp == nil) {
+			sys->print("dial %s failed: %r\n", firewall);
+			exit;
+		}
+		dfid = ftp.dfd;
+		getuser();
+		t := sys->sprint("\ntcp!%s!tcp.21\n\n%s\n%s\n0\n-1\n-1\n", hostname, myhost, myname);
+		if (debug)
+			sys->print("request%s\n", t);
+		b := array of byte t;
+		rv = sys->write(dfid, b, len b);
+		if (rv < 0) {
+			sys->print("firewall write failed: %r\n");
+			exit;
+		}
+		b = array[256] of byte;
+		rv = sys->read(dfid, b, len b);
+		if (rv < 0) {
+			sys->print("firewall read failed: %r\n");
+			return;
+		}
+		(c, k) := sys->tokenize(string b[:rv], "\n");
+		if (c < 2) {
+			sys->print("bad response from firewall\n");
+			exit;
+		}
+		if (hd k != "0") {
+			sys->print("firewall connect: %s\n", hd tl k);
+			exit;
+		}
+		proxyid = hd tl k;
+		if (debug)
+			sys->print("proxyid %s\n", proxyid);
+		(c, k) = sys->tokenize(proxyid, "!");
+		if (c < 3) {
+			sys->print("bad proxyid from firewall\n");
+			exit;
+		}
+		proxyhost = (hd k) + "!" + (hd tl k);
+		if (debug)
+			sys->print("proxyhost %s\n", proxyhost);
+	} else {
+		d := dial->netmkaddr(hostname, "tcp", "ftp");
+		ftp = dial->dial(d, nil);
+		if(ftp == nil)
+			error(sys->sprint("dial %s failed: %r", d));
+		if(debug)
+			sys->print("localdir %s\n", ftp.dir);
+		dfid = ftp.dfd;
+	}
+	dfidiob = bufio->fopen(dfid, sys->OREAD);
+	(net, port) = getnet(ftp.dir);		
+	tbuff = array[BSZ] of byte;
+	rbuff = array[BSZ] of byte;
+	(rv, l) = getreply(!quiet);
+	if (rv != Success)
+		fail(rv, l);
+	if (user == nil) {
+		getuser();
+		user = myname;
+		user = prompt("User", user, 1);
+	}
+	rv = sendrequest("USER " + user, 0);
+	if (rv != Success)
+		sendfail(rv);
+	(rv, code, l) = getfullreply(!quiet);
+	if (rv != Success) {
+		if (rv != Incomplete)
+			fail(rv, l);
+		if (code == 331) {
+			if(password == nil){
+				factotum := load Factotum Factotum->PATH;
+				if(factotum != nil){
+					factotum->init();
+					if(user != nil && keyspec == nil)
+						keyspec = sys->sprint("user=%q", user);
+					(nil, password) = factotum->getuserpasswd(sys->sprint("proto=pass server=%s service=ftp %s", hostname, keyspec));
+				}
+				if(password == nil)
+					password = prompt("Password", nil, 0);
+			}
+			rv = sendrequest2("PASS " + password, 0, "PASS XXXX");
+			if (rv != Success)
+				sendfail(rv);
+			(rv, l) = getreply(0);
+			if (rv != Success)
+				fail(rv, l);
+		}
+	}
+	if (cdtoroot) {
+		rv = sendrequest("CWD /", 0);
+		if (rv != Success)
+			sendfail(rv);
+		(rv, l) = getreply(0);
+		if (rv != Success)
+			fail(rv, l);
+	}
+	rv = sendrequest("TYPE I", 0);
+	if (rv != Success)
+		sendfail(rv);
+	(rv, l) = getreply(0);
+	if (rv != Success)
+		fail(rv, l);
+	rv = sendrequest("PWD", 0);
+	if (rv != Success)
+		sendfail(rv);
+	(rv, l) = getreply(0);
+	if (rv != Success)
+		fail(rv, l);
+	remrootpath = pwd(l);
+	remroot = newnode(nil, "/");
+	remroot.dir.mode = Sys->DMDIR | 8r777;
+	remroot.dir.qid.qtype = Sys->QTDIR;
+	remdir = remroot;
+	l = connect();
+	if (l != nil) {
+		sys->print("%s\n", l);
+		exit;
+	}
+	ctllock = chan[1] of int;
+	spawn mount(mountpoint);
+	pidc := chan of int;
+	spawn heartbeat(pidc);
+	heartbeatpid = <-pidc;
+	if (debug)
+		sys->print("heartbeatpid %d\n", heartbeatpid);			
+	spawn server();				# dies when receive on chan fails
+}
+
+kill(pid: int)
+{
+	if (debug)
+		sys->print("killing %d\n", pid);
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+shutdown()
+{
+	mountfd = nil;
+}
+
+#
+#	Styx transactions.
+#
+
+versionT(t: ref Tmsg.Version)
+{
+	(msize, version) := styx->compatible(t, Styx->MAXRPC, Styx->VERSION);
+	sendreply(ref Rmsg.Version(t.tag, msize, version));
+}
+
+authT(t: ref Tmsg.Auth)
+{
+	sendreply(ref Rmsg.Error(t.tag, "authentication not required"));
+}
+
+flushT(t: ref Tmsg.Flush)
+{
+	sendreply(ref Rmsg.Flush(t.tag));
+}
+
+attachT(t: ref Tmsg.Attach)
+{
+	f := getfid(t.fid);
+	f.busy = 1;
+	f.node = remroot;
+	sendreply(ref Rmsg.Attach(t.tag, remroot.qid()));
+}
+
+walkT(t: ref Tmsg.Walk)
+{
+	f := getfid(t.fid);
+	qids: array of Sys->Qid;
+	node := f.node;
+	if(len t.names > 0){
+		qids = array[len t.names] of Sys->Qid;
+		for(i := 0; i < len t.names; i++) {
+			if ((node.dir.mode & Sys->DMDIR) == 0){
+				if(i == 0)
+					return rerror(t.tag, Enotadirectory);
+				break;
+			}
+			if (t.names[i] == "..")
+				node = node.parent;
+			else if (t.names[i] != ".") {
+				if (t.names[i] == ".flush.ftpfs") {
+					node.invalidate();
+					node.readdir();
+					qids[i] = node.qid();
+					continue;
+				}
+				node = node.extendpath(t.names[i]);
+				if (node.parent.cached) {
+					if (!node.valid) {
+						if(i == 0)
+							return rerror(t.tag, Enosuchfile);
+						break;
+					}
+					if ((node.dir.mode & CHSYML) != 0)
+						node.fixsymbolic();
+				} else if (!node.valid) {
+					if (node.changedir() == 0){
+						node.dir.qid.qtype = Sys->QTDIR;
+						node.dir.mode |= Sys->DMDIR;
+					}else{
+						node.dir.qid.qtype = Sys->QTFILE;
+						node.dir.mode &= ~Sys->DMDIR;
+					}
+				}
+				qids[i] = node.qid();
+			}
+		}
+		if(i < len t.names){
+			sendreply(ref Rmsg.Walk(t.tag, qids[0:i]));
+			return;
+		}
+	}
+	if(t.newfid != t.fid){
+		n := getfid(t.newfid);
+		if(n.busy)
+			return rerror(t.tag, "fid in use");
+		n.busy = 1;
+		n.node = node;
+	}else
+		f.node = node;
+	sendreply(ref Rmsg.Walk(t.tag, qids));
+}
+
+openT(t: ref Tmsg.Open)
+{
+	f := getfid(t.fid);
+	if ((f.node.dir.mode & Sys->DMDIR) != 0 && t.mode != Sys->OREAD) {
+		rerror(t.tag, Epermission);
+		return;
+	}
+	if ((t.mode & Sys->OTRUNC) != 0) {
+		f.node.uncache();
+		f.node.parent.uncache();
+		f.node.filedirty();
+	} else if (!f.node.cached) {
+		f.node.filefree();
+		if ((f.node.dir.mode & Sys->DMDIR) != 0) {
+			f.node.invalidate();
+			if (f.node.readdir() < 0) {
+				rerror(t.tag, Enosuchfile);
+				return;
+			}
+		}
+		else {
+			if (f.node.readfile() < 0) {
+				rerror(t.tag, errstr);
+				return;
+			}
+		}
+		f.node.markcached();
+	}
+	sendreply(ref Rmsg.Open(t.tag, f.node.qid(), Styx->MAXFDATA));
+}
+
+createT(t: ref Tmsg.Create)
+{
+	f := getfid(t.fid);
+	if ((f.node.dir.mode & Sys->DMDIR) == 0) {
+		rerror(t.tag, Enotadirectory);
+		return;
+	}
+	f.node = f.node.extendpath(t.name);
+	f.node.uncache();
+	if ((t.perm & Sys->DMDIR) != 0) {
+		if (f.node.createdir() < 0) {
+			rerror(t.tag, Epermission);
+			return;
+		}
+	}
+	else
+		f.node.filedirty();
+	f.node.parent.invalidate();
+	f.node.parent.uncache();
+	sendreply(ref Rmsg.Create(t.tag, f.node.qid(), Styx->MAXFDATA));
+}
+
+readT(t: ref Tmsg.Read)
+{
+	f := getfid(t.fid);
+	count := t.count;
+
+	if (count < 0)
+		return rerror(t.tag, Ebadlength);
+	if (count > Styx->MAXFDATA)
+		count = Styx->MAXFDATA;
+	if (t.offset < big 0)
+		return rerror(t.tag, Ebadoffset);
+	rv := 0;
+	if ((f.node.dir.mode & Sys->DMDIR) != 0) {
+		offset := int t.offset;
+		for (p := f.node.children; offset > 0 && p != nil; p = p.sibs)
+			if (p.valid)
+				offset -= len p.stat();
+		for (; rv < count && p != nil; p = p.sibs) {
+			if (p.valid) {
+				if ((p.dir.mode & CHSYML) != 0)
+					p.fixsymbolic();
+				a := p.stat();
+				size := len a;
+				if(rv+size > count)
+					break;
+				tbuff[rv:] = a;
+				rv += size;
+			}
+		}
+	} else {
+		if (!f.node.cached && f.node.readfile() < 0) {
+			rerror(t.tag, errstr);
+			return;
+		}
+		f.node.markcached();
+		rv = f.node.fileread(tbuff, int t.offset, count);
+		if (rv < 0) {
+			rerror(t.tag, errstr);
+			return;
+		}
+	}
+	sendreply(ref Rmsg.Read(t.tag, tbuff[0:rv]));
+}
+
+writeT(t: ref Tmsg.Write)
+{
+	f := getfid(t.fid);
+	if ((f.node.dir.mode & Sys->DMDIR) != 0) {
+		rerror(t.tag, Eisadirectory);
+		return;
+	}
+	count := f.node.filewrite(t.data, int t.offset, len t.data);
+	if (count < 0) {
+		rerror(t.tag, errstr);
+		return;
+	}
+	f.node.filedirty();
+	sendreply(ref Rmsg.Write(t.tag, count));
+}
+
+clunkT(t: ref Tmsg.Clunk)
+{
+	f := getfid(t.fid);
+	if (f.node.fileisdirty()) {
+		if (f.node.createfile() < 0)
+			sys->print("ftpfs: could not create %s: %r\n", f.node.pathname());
+		f.node.fileclean();
+		f.node.uncache();
+	}
+	f.busy = 0;
+	sendreply(ref Rmsg.Clunk(t.tag));
+}
+
+removeT(t: ref Tmsg.Remove)
+{
+	f := getfid(t.fid);
+	if ((f.node.dir.mode & Sys->DMDIR) != 0) {
+		if (f.node.removedir() < 0) {
+			rerror(t.tag, errstr);
+			return;
+		}
+	}
+	else {
+		if (f.node.removefile() < 0) {
+			rerror(t.tag, errstr);
+			return;
+		}
+	}
+	f.node.parent.uncache();
+	f.node.uncache();
+	f.node.valid = 0;
+	f.busy = 0;
+	sendreply(ref Rmsg.Remove(t.tag));
+}
+
+statT(t: ref Tmsg.Stat)
+{
+	f := getfid(t.fid);
+	n := f.node.parent;
+	if (!n.cached) {
+		n.invalidate();
+		n.readdir();
+		n.markcached();
+	}
+	if (!f.node.valid) {
+		rerror(t.tag, Enosuchfile);
+		return;
+	}
+	sendreply(ref Rmsg.Stat(t.tag, f.node.dir));
+}
+
+wstatT(t: ref Tmsg.Wstat)
+{
+	rerror(t.tag, Enowstat);
+}
--- /dev/null
+++ b/appl/cmd/getauthinfo.b
@@ -1,0 +1,184 @@
+implement Getauthinfo;
+
+#
+# get and save a certificate from a signer in exchange for a valid secret
+#
+
+include "sys.m";
+	sys: Sys;
+	stdin, stdout, stderr: ref Sys->FD;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+
+include "security.m";
+	login: Login;
+
+include "string.m";
+	str: String;
+
+include "promptstring.b";
+
+Getauthinfo: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: getauthinfo {net!hostname | default | /file}\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdin = sys->fildes(0);
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+
+	# Disable echoing in RAWON mode
+	RAWON_STR = nil;
+
+	argv = tl argv;
+	if(argv == nil)
+		usage();
+	keyname := hd argv;
+	if(keyname == nil)
+		usage();
+
+	kr = load Keyring Keyring->PATH;
+	if(kr == nil)
+		nomod(Keyring->PATH);
+
+	str = load String String->PATH;
+	if(str == nil)
+		nomod(String->PATH);
+
+	login = load Login Login->PATH;
+	if(login == nil)
+		nomod(Login->PATH);
+
+	user := user();
+	path := keyname;
+	if(path[0] != '/' && (len path < 2 || path[0:2] != "./"))
+		path = "/usr/" + user + "/keyring/" + keyname;
+
+	signer := defaultsigner();
+	if(signer == nil){
+		sys->fprint(stderr, "getauthinfo: warning: can't get default signer server name\n");
+		signer = "$SIGNER";
+	}
+
+	passwd := "";
+	save := "yes";
+	for(;;) {
+		signer = promptstring("use signer", signer, RAWOFF);
+		user = promptstring("remote user name", user, RAWOFF);
+		passwd = promptstring("password", passwd, RAWON);
+
+		info := logon(user, passwd, signer, path, save);
+		if(info != nil)
+			break;
+	}
+}
+
+logon(user, passwd, server, path, save: string): ref Keyring->Authinfo
+{
+	(err, info) := login->login(user, passwd, "net!"+server+"!inflogin");
+	if(err != nil){
+		sys->fprint(stderr, "getauthinfo: failed to authenticate: %s\n", err);
+		return nil;
+	}
+
+	# save the info somewhere for later access
+	save = promptstring("save in file", save, RAWOFF);
+	if(save[0] != 'y'){
+		(dir, file) := str->splitr(path, "/");
+		if(sys->bind("#s", dir, Sys->MBEFORE) < 0){
+			sys->fprint(stderr, "getauthinfo: can't bind file channel on %s: %r\n", dir);
+			return nil;
+		}
+		filio := sys->file2chan(dir, file);
+		if(filio == nil) {
+			sys->fprint(stderr, "getauthinfo: can't make file2chan %s: %r\n", path);
+			return nil;
+		}
+		sync := chan of int;
+		spawn infofile(filio, sync);
+		<-sync;
+	}
+
+	if(kr->writeauthinfo(path, info) < 0) {
+		sys->fprint(stderr, "getauthinfo: can't write certificate to %s: %r\n", path);
+		return nil;
+	}
+
+	return info;
+}
+
+user(): string
+{
+	sys = load Sys Sys->PATH;
+
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n];	
+}
+
+infofile(fileio: ref Sys->FileIO, sync: chan of int)
+{
+	infodata := array[0] of byte;
+
+	sys->pctl(Sys->NEWPGRP|Sys->NEWFD, nil);
+	sync <-= 1;
+
+	for(;;) alt {
+	(off, nbytes, nil, rc) := <-fileio.read =>
+		if(rc == nil)
+			break;
+		if(off > len infodata){
+			rc <-= (nil, nil);
+		} else {
+			if(off + nbytes > len infodata)
+				nbytes = len infodata - off;
+			rc <-= (infodata[off:off+nbytes], nil);
+		}
+
+	(off, data, nil, wc) := <-fileio.write =>
+		if(wc == nil)
+			break;
+
+		if(off != len infodata){
+			wc <-= (0, "cannot be rewritten");
+		} else {
+			nid := array[len infodata+len data] of byte;
+			nid[0:] = infodata;
+			nid[len infodata:] = data;
+			infodata = nid;
+			wc <-= (len data, nil);
+		}
+		data = nil;
+	}
+}
+
+# get default signer server name
+defaultsigner(): string
+{
+	return "$SIGNER";
+}
+
+nomod(s: string)
+{
+	sys->fprint(stderr, "getauthinfo: can't load %s: %r\n", s);
+	raise "fail:load";
+}
--- /dev/null
+++ b/appl/cmd/getfile.b
@@ -1,0 +1,74 @@
+implement Getfile;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+	draw: Draw;
+	Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "wmlib.m";
+	wmlib: Wmlib;
+include "arg.m";
+
+Getfile: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: getfile [-g geom] [-d startdir] [pattern...]\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	wmlib = load Wmlib Wmlib->PATH;
+	if (wmlib == nil) {
+		sys->fprint(stderr, "getfile: cannot load %s: %r\n", Wmlib->PATH);
+		raise "fail:bad module";
+	}
+	arg := load Arg Arg->PATH;
+	if (arg == nil) {
+		sys->fprint(stderr, "getfile: cannot load %s: %r\n", Arg->PATH);
+		raise "fail:bad module";
+	}
+
+	if (ctxt == nil) {
+		sys->fprint(stderr, "getfile: no window context\n");
+		raise "fail:bad context";
+	}
+
+	wmlib->init();
+
+	startdir := ".";
+	geom := "-x " + string (ctxt.screen.image.r.dx() / 5) +
+			" -y " + string (ctxt.screen.image.r.dy() / 5);
+	title := "Select a file";
+	arg->init(argv);
+	while (opt := arg->opt()) {
+		case opt {
+		'g' =>
+			geom = arg->arg();
+		'd' =>
+			startdir = arg->arg();
+		't' =>
+			title = arg->arg();
+		* =>
+			sys->fprint(stderr, "getfile: unknown option -%c\n", opt);
+			usage();
+		}
+	}
+	if (geom == nil || startdir == nil || title == nil)
+		usage();
+	top := tk->toplevel(ctxt.screen, geom);
+	argv = arg->argv();
+	arg = nil;
+	sys->print("%s\n", wmlib->filename(ctxt.screen, top, title, argv, startdir));
+}
--- /dev/null
+++ b/appl/cmd/gettar.b
@@ -1,0 +1,248 @@
+implement Gettar;
+
+include "sys.m";
+	sys: Sys;
+	print, sprint, fprint: import sys;
+	stdin, stderr: ref sys->FD;
+
+include "draw.m";
+
+include "arg.m";
+
+TBLOCK: con 512;	# tar logical blocksize
+
+Header: adt{
+	name: string;
+	size: int;
+	mode: int;
+	mtime: int;
+	skip: int;
+};
+
+Gettar: module
+{
+	init:   fn(nil: ref Draw->Context, nil: list of string);
+};
+
+error(mess: string)
+{
+	fprint(stderr,"gettar: %s\n",mess);
+	raise "fail:error";
+}
+
+verbose := 0;
+NBLOCK: con 20;		# traditional blocking factor for efficient read
+tarbuf := array[NBLOCK*TBLOCK] of byte;	# static buffer
+nblock := NBLOCK;			# how many blocks of data are in tarbuf
+recno := NBLOCK;			# how many blocks in tarbuf have been consumed
+
+getblock(): array of byte
+{
+	if(recno>=nblock){
+		i := sys->read(stdin,tarbuf,TBLOCK*NBLOCK);
+		if(i==0)
+			return nil;
+		if(i<0)
+			error(sys->sprint("read error: %r"));
+		if(i%TBLOCK!=0)
+			error("blocksize error");
+		nblock = i/TBLOCK;
+		recno = 0;
+	}
+	recno++;
+	return tarbuf[(recno-1)*TBLOCK:recno*TBLOCK];
+}
+
+
+octal(b:array of byte): int
+{
+	sum := 0;
+	for(i:=0; i<len b; i++){
+		bi := int b[i];
+		if(bi==' ') continue;
+		if(bi==0) break;
+		sum = 8*sum + bi-'0';
+	}
+	return sum;
+}
+
+nullterm(b:array of byte): string
+{
+	for(i:=0; i<len b; i++)
+		if(b[i]==byte 0) break;
+	return string b[0:i];
+}
+
+getdir(): ref Header
+{
+	dblock := getblock();
+	if(len dblock==0)
+		return nil;
+	if(dblock[0]==byte 0)
+		return nil;
+
+	name := nullterm(dblock[0:100]);
+	if(int dblock[345]!=0)
+		name = nullterm(dblock[345:500])+"/"+name;
+	if(!absolute){
+		if(name[0] == '#')
+			name = "./"+name;
+		else if(name[0] == '/')
+			name = "."+name;
+	}
+
+	magic := string(dblock[257:262]);
+	if(magic[0]!=0 && magic!="ustar")
+		error("bad magic "+name);
+	chksum := octal(dblock[148:156]);
+	for(ci:=148; ci<156; ci++)
+		dblock[ci] = byte ' ';
+	for(i:=0; i<TBLOCK; i++)
+		chksum -= int dblock[i];
+	if(chksum!=0)
+		error("directory checksum error "+name);
+
+	skip := 1;
+	size := 0;
+	mode := 0;
+	mtime := 0;
+	case int dblock[156]{
+	'0' or '7' or 0 =>
+		skip = 0;
+		size = octal(dblock[124:136]);
+		mode = 8r777 & octal(dblock[100: 108]);
+		mtime = octal(dblock[136:148]);
+	'1' =>
+		fprint(stderr,"gettar: skipping link %s -> %s\n",name,string(dblock[157:257]));
+	'2' or 's' =>
+		fprint(stderr,"gettar: skipping symlink %s\n",name);
+	'3' or '4' or '6' =>
+		fprint(stderr,"gettar: skipping special file %s\n",name);
+	'5' =>
+		if(name[(len name)-1]=='/')
+			checkdir(name+".");
+		else
+			checkdir(name+"/.");
+	* =>
+		error(sprint("unrecognized typeflag %d for %s",int dblock[156],name));
+	}
+	return ref Header(name, size, mode, mtime, skip);
+}
+
+keep := 0;
+absolute := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdin = sys->fildes(0);
+	stderr = sys->fildes(2);
+	ofile: ref sys->FD;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("gettar [-kTRv] [file ...]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'k' =>	keep = 1;
+		'v' =>	verbose = 1;
+		'R' =>	absolute = 1;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	while((file := getdir())!=nil){
+		if(!file.skip){
+			if((args == nil || matched(file.name, args)) && !(keep && exists(file.name))){
+				if(verbose)
+					sys->fprint(stderr, "%s\n", file.name);
+				checkdir(file.name);
+				ofile = sys->create(file.name, Sys->OWRITE, 8r666);
+				if(ofile==nil){
+					fprint(stderr, "gettar: cannot create %s: %r\n",file.name);
+					file.skip = 1;
+				}
+			}else
+				file.skip = 1;
+		}
+		bytes := file.size;
+		blocks := (bytes+TBLOCK-1)/TBLOCK;
+		if(file.skip){
+			for(; blocks>0; blocks--)
+				getblock();
+			continue;
+		}
+
+		for(; blocks>0; blocks--){
+			buf := getblock();
+			nwrite := bytes;
+			if(nwrite>TBLOCK)
+				nwrite = TBLOCK;
+			if(sys->write(ofile,buf,nwrite)!=nwrite)
+				error(sprint("write error for %s: %r",file.name));
+			bytes -= nwrite;
+		}
+		ofile = nil;
+		stat := sys->nulldir;
+		stat.mode = file.mode;
+		stat.mtime = file.mtime;
+		rc := sys->wstat(file.name,stat);
+		if(rc<0){
+			# try just the mode
+			stat.mtime = ~0;
+			rc = sys->wstat(file.name, stat);
+			if(rc < 0)
+				fprint(stderr,"gettar: cannot set mode/mtime %s %#o %ud: %r\n",file.name, file.mode, file.mtime);
+		}
+	}
+}
+
+checkdir(name: string)
+{
+	(nc,compl) := sys->tokenize(name,"/");
+	path := "";
+	while(compl!=nil){
+		comp := hd compl;
+		if(comp=="..")
+			error(".. pathnames forbidden");
+		if(nc>1){
+			if(path=="")
+				path = comp;
+			else
+				path += "/"+comp;
+			(rc,stat) := sys->stat(path);
+			if(rc<0){
+				fd := sys->create(path,Sys->OREAD,Sys->DMDIR+8r777);
+				if(fd==nil)
+					error(sprint("cannot mkdir %s: %r",path));
+				fd = nil;
+			}else if(stat.mode&Sys->DMDIR==0)
+				error(sprint("found non-directory at %s",path));
+		}
+		nc--; compl = tl compl;
+	}
+}
+
+exists(path: string): int
+{
+	return sys->stat(path).t0 >= 0;
+}
+
+matched(n: string, names: list of string): int
+{
+	for(; names != nil; names = tl names){
+		p := hd names;
+		if(prefix(p, n))
+			return 1;
+	}
+	return 0;
+}
+
+prefix(p: string, s: string): int
+{
+	l := len p;
+	if(l > len s)
+		return 0;
+	return p == s[0:l] && (l == len s || s[l] == '/');
+}
--- /dev/null
+++ b/appl/cmd/gif2bit.b
@@ -1,0 +1,101 @@
+#
+# gif2bit -
+#
+# A simple command line utility for converting GIF images to
+# inferno bitmaps.
+#
+# Craig Newell, Jan. 1999	CraigN@cheque.uq.edu.au
+#
+implement gif2bit;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display: import draw;
+include "string.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "imagefile.m";
+
+mod_name := "gif2bit";
+
+gif2bit : module
+{
+	init: fn(ctx: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->print("usage: %s <GIF file>\n", mod_name);
+	exit;
+}	
+
+init(ctx: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	# check arguments
+	if (argv == nil) 
+		usage();
+	mod_name = hd argv;
+	argv = tl argv;
+	if (argv == nil)
+		usage();
+	s := hd argv;
+	if (len s && s[0] == '-')
+		usage();
+
+	# load the modules	
+	str := load String String->PATH;
+	draw = load Draw Draw->PATH;
+	bufio = load Bufio Bufio->PATH;
+	remap := load Imageremap Imageremap->PATH;
+	imgfile := load RImagefile RImagefile->READGIFPATH;
+	imgfile->init(bufio);
+
+	# open the display
+	display: ref Draw->Display;
+	if (ctx == nil) {
+		display = Display.allocate(nil);
+	} else {
+		display = ctx.display;
+	}
+
+	# process all the files 
+	while (argv != nil) {
+	
+		# get the filenames		
+		gif_name := hd argv;
+		argv = tl argv;
+		(base_name, nil) := str->splitstrl(gif_name, ".gif");
+		bit_name := base_name + ".bit";
+
+		i := bufio->open(gif_name, Bufio->OREAD);
+		if (i == nil) {
+			sys->print("%s: unable to open <%s>\n", mod_name, gif_name);
+			continue;
+		}
+		(raw_img, errstr) := imgfile->read(i);
+		if (errstr != nil) {
+			sys->print("%s: %s\n", mod_name, errstr);
+			continue;
+		}
+		i.close();
+
+		(img, errstr1) := remap->remap(raw_img, display, 0);
+		if (errstr1 != nil) {
+			sys->print("%s: %s\n", mod_name, errstr1);
+			continue;
+		}
+	
+		ofd := sys->create(bit_name, Sys->OWRITE, 8r644);
+		if (ofd == nil) {
+			sys->print("%s: unable to create <%s>\n", mod_name, bit_name);
+			continue;
+		}
+		display.writeimage(ofd, img);
+		ofd = nil;	
+	}
+}
--- /dev/null
+++ b/appl/cmd/grep.b
@@ -1,0 +1,155 @@
+implement Grep;
+
+include "sys.m";
+	sys: Sys;
+	FD: import Sys;
+	stdin, stderr, stdout: ref FD;
+
+include "draw.m";
+	Context: import Draw;
+
+include "regex.m";
+	regex: Regex;
+	Re: import regex;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+
+
+Grep: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+multi: int;
+lflag, nflag, vflag, iflag, Lflag, sflag: int = 0;
+
+badmodule(path: string)
+{
+	sys->fprint(stderr, "grep: cannot load %s: %r\n", path);
+	raise "fail:bad module";
+}
+
+init(nil: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdin = sys->fildes(0);
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+
+	regex = load Regex Regex->PATH;
+	if(regex == nil)
+		badmodule(Regex->PATH);
+
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmodule(Bufio->PATH);
+
+	arg->init(argv);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'l' =>
+			lflag = 1;
+		'n' =>
+			nflag = 1;
+		'v' =>
+			vflag = 1;
+		'i' =>
+			iflag = 1;
+		'L' =>
+			Lflag = 1;
+		's' =>
+			sflag = 1;
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	arg = nil;
+
+	if(argv == nil)
+		usage();
+	pattern := hd argv;
+	argv = tl argv;
+	if (iflag)
+		pattern = tolower(pattern);
+	(re, err) := regex->compile(pattern,0);
+	if(re == nil) {
+		sys->fprint(stderr, "grep: %s\n", err);
+		raise "fail:bad regex";
+	}
+
+	matched := 0;
+	if(argv == nil)
+		matched = grep(re, bufio->fopen(stdin, Bufio->OREAD), "stdin");
+	else {
+		multi = (tl argv != nil);
+		for (; argv != nil; argv = tl argv) {
+			f := bufio->open(hd argv, Bufio->OREAD);
+			if(f == nil)
+				sys->fprint(stderr, "grep: cannot open %s: %r\n", hd argv);
+			else
+				matched += grep(re, f, hd argv);
+		}
+	}
+	if (!matched)
+		raise "fail:no matches";
+}
+
+usage()
+{
+	sys->fprint(stderr, "usage: grep [-lnviLs] pattern [file...]\n");
+	raise "fail:usage";
+}
+
+grep(re: Re, f: ref Iobuf, file: string): int
+{
+	matched := 0;
+	for(line := 1; ; line++) {
+		s := t := f.gets('\n');
+		if(s == nil)
+			break;
+		if (iflag)
+			s = tolower(s);
+		if((regex->executese(re, s, (0, len s-1), 1, 1) != nil) ^ vflag) {
+			matched = 1;
+			if(lflag || sflag) {
+				if (!sflag)
+					sys->print("%s\n", file);
+				return matched;
+			}
+			if (!Lflag) {
+				if(nflag)
+					if(multi)
+						sys->print("%s:%d: %s", file, line, t);
+					else
+						sys->print("%d:%s", line, t);
+				else
+					if(multi)
+						sys->print("%s: %s", file, t);
+					else
+						sys->print("%s", t);
+			}
+		}
+	}
+	if (Lflag && matched == 0 && !sflag)
+		sys->print("%s\n", file);
+	return matched;
+}
+
+tolower(s: string): string
+{
+	for (i := 0; i < len s; i++) {
+		c := s[i];
+		if (c >= 'A' && c <= 'Z')
+			s[i] = c - 'A' + 'a';
+	}
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/gunzip.b
@@ -1,0 +1,139 @@
+implement Gunzip;
+
+include "sys.m";
+	sys:	Sys;
+	fprint, sprint: import sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "bufio.m";
+	bufio:	Bufio;
+	Iobuf:	import bufio;
+
+include "filter.m";
+	inflate: Filter;
+
+Gunzip: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+argv0:	con "gunzip";
+stderr:	ref Sys->FD;
+
+INFLATEPATH: con "/dis/lib/inflate.dis";
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		fatal(sys->sprint("cannot load %s: %r", Bufio->PATH));
+	str = load String String->PATH;
+	if (bufio == nil)
+		fatal(sys->sprint("cannot load %s: %r", String->PATH));
+	inflate = load Filter INFLATEPATH;
+	if (inflate == nil)
+		fatal(sys->sprint("cannot load %s: %r", INFLATEPATH));
+
+	inflate->init();
+
+	if(argv != nil)
+		argv = tl argv;
+
+	ok := 1;
+	if(len argv == 0){
+		bin := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+		bout := bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+		ok = gunzip(bin, bout, "stdin", "stdout");
+		bout.close();
+	} else {
+		for(; argv != nil; argv = tl argv)
+			ok &= gunzipf(hd argv);
+	}
+	if(ok == 0)
+		raise "fail:errors";
+}
+
+gunzipf(file: string): int
+{
+	bin := bufio->open(file, Bufio->OREAD);
+	if(bin == nil){
+		fprint(stderr, "%s: can't open %s: %r\n", argv0, file);
+		return 0;
+	}
+
+	(nil, ofile) := str->splitr(file, "/");
+	n := len ofile;
+	if(n < 4 || ofile[n-3:] != ".gz"){
+		fprint(stderr, "%s: .gz extension required: %s\n", argv0, file);
+		bin.close();
+		return 0;
+	} else
+		ofile = ofile[:n-3];
+	bout := bufio->create(ofile, Bufio->OWRITE, 8r666);
+	if(bout == nil){
+		fprint(stderr, "%s: can't open %s: %r\n", argv0, ofile);
+		bin.close();
+		return 0;
+	}
+
+	ok := gunzip(bin, bout, file, ofile);
+	bin.close();
+	bout.close();
+	if(ok) {
+		# did possibly rename file and update modification time here.
+		if (sys->remove(file) == -1)
+			sys->fprint(stderr, "%s: cannot remove %s: %r\n", argv0, file);
+	}
+
+	return ok;
+}
+
+gunzip(bin, bout: ref Iobuf, fin, fout: string): int
+{
+	rq := inflate->start("h");
+	for(;;) {
+		pick m := <-rq {
+		Fill =>
+			n := bin.read(m.buf, len m.buf);
+			m.reply <-= n;
+			if (n == -1) {
+				sys->fprint(stderr, "%s: %s: read error: %r\n", argv0, fin);
+				return 0;
+			}
+		Result =>
+			if (len m.buf > 0) {
+				n := bout.write(m.buf, len m.buf);
+				if (n != len m.buf) {
+					m.reply <-= -1;
+					sys->fprint(stderr, "%s: %s: write error: %r\n", argv0, fout);
+					return 0;
+				}
+				m.reply <-= 0;
+			}
+		#Info =>
+		#	if m.msg begins with "file", it's the original filename of the compressed file.
+		#	if m.msg begins with "mtime", it's the original modification time.
+		Finished =>
+			if (bout.flush() != 0) {
+				sys->fprint(stderr, "%s: %s: flush error: %r\n", argv0, fout);
+				return 0;
+			}
+			return 1;
+		Error =>
+			sys->fprint(stderr, "%s: %s: inflate error: %s\n", argv0, fin, m.e);
+			return 0;
+		}
+	}
+}
+
+fatal(msg: string)
+{
+	fprint(stderr, "%s: %s\n", argv0, msg);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/gzip.b
@@ -1,0 +1,228 @@
+implement Gzip;
+
+include "sys.m";
+	sys:	Sys;
+	print, fprint: import sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "bufio.m";
+	bufio:	Bufio;
+	Iobuf: import bufio;
+
+include "filter.m";
+	deflate: Filter;
+
+DEFLATEPATH: con "/dis/lib/deflate.dis";
+
+Gzip: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Arg: adt
+{
+	argv:	list of string;
+	c:	int;
+	opts:	string;
+
+	init:	fn(argv: list of string): ref Arg;
+	opt:	fn(arg: self ref Arg): int;
+	arg:	fn(arg: self ref Arg): string;
+};
+
+argv0:	con "gzip";
+stderr:	ref Sys->FD;
+debug	:= 0;
+verbose	:= 0;
+level	:= 0;
+
+usage()
+{
+	fprint(stderr, "usage: %s [-vD1-9] [file ...]\n", argv0);
+	raise "fail:usage";
+}
+
+nomod(path: string)
+{
+	sys->fprint(stderr, "%s: cannot load %s: %r\n", argv0, path);
+	raise "fail:bad module";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		nomod(Bufio->PATH);
+	str = load String String->PATH;
+	if (str == nil)
+		nomod(String->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if (daytime == nil)
+		nomod(Daytime->PATH);
+	deflate = load Filter DEFLATEPATH;
+	if(deflate == nil)
+		nomod(DEFLATEPATH);
+
+	arg := Arg.init(argv);
+	level = 6;
+	while(c := arg.opt()){
+		case c{
+		'D' =>
+			debug++;
+		'v' =>
+			verbose++;
+		'1' to  '9' =>
+			level = c - '0';
+		* =>
+			usage();
+		}
+	}
+
+	deflate->init();
+
+	argv = arg.argv;
+
+	ok := 1;
+	if(len argv == 0){
+		bin := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+		bout := bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+		ok = gzip(nil, daytime->now(), bin, bout, "stdin", "stdout");
+		bout.close();
+		bin.close();
+	}else{
+		for(; argv != nil; argv = tl argv)
+			ok &= gzipf(hd argv);
+	}
+	exit;
+}
+
+gzipf(file: string): int
+{
+	bin := bufio->open(file, Bufio->OREAD);
+	if(bin == nil){
+		fprint(stderr, "%s: can't open %s: %r\n", argv0, file);
+		return 0;
+	}
+	(ok, dir) := sys->fstat(bin.fd);
+	if(ok >= 0)
+		mtime := dir.mtime;
+	else
+		mtime = daytime->now();
+
+	(nil, ofile) := str->splitr(file, "/");
+	ofile += ".gz";
+	bout := bufio->create(ofile, Bufio->OWRITE, 8r666);
+	if(bout == nil){
+		fprint(stderr, "%s: can't open %s: %r\n", argv0, ofile);
+		bin.close();
+		return 0;
+	}
+
+	ok = gzip(file, mtime, bin, bout, file, ofile);
+	bout.close();
+	bin.close();
+	if (ok)
+		sys->remove(file);
+	else
+		sys->remove(ofile);
+		
+	return ok;
+}
+
+gzip(nil: string, nil: int, bin, bout: ref Iobuf, fin, fout: string): int
+{
+	param := "h" + string level;
+	incount := outcount := 0;
+	if (debug)
+		param += "dv";
+	rq := deflate->start(param);
+
+	for (;;) {
+		pick m := <-rq {
+		Fill =>
+			n := bin.read(m.buf, len m.buf);
+			m.reply <-= n;
+			if (n == -1) {
+				sys->fprint(stderr, "%s: error reading %s: %r\n", argv0, fin);
+				return 0;
+			}
+			incount += n;
+		Result =>
+			n := len m.buf;
+			if (bout.write(m.buf, n) != n) {
+				sys->fprint(stderr, "%s: error writing %s: %r\n", argv0, fout);
+				m.reply <-= -1;
+				return 0;
+			}
+			m.reply <-= 0;
+			outcount += n;
+		Info =>
+			sys->fprint(stderr, "%s\n", m.msg);
+		Finished =>
+			comp := 0.0;
+			if (incount > 0)
+				comp = 1.0 - real outcount / real incount;
+			if (verbose)
+				sys->fprint(stderr, "%s: %5.2f%%\n", fin, comp * 100.0);
+			return 1;
+		Error =>
+			sys->fprint(stderr, "%s: error compressing %s: %s\n", argv0, fin, m.e);
+			return 0;
+		}
+	}
+}
+
+fatal(msg: string)
+{
+	fprint(stderr, "%s: %s\n", argv0, msg);
+	exit;
+}
+
+Arg.init(argv: list of string): ref Arg
+{
+	if(argv != nil)
+		argv = tl argv;
+	return ref Arg(argv, 0, nil);
+}
+
+Arg.opt(arg: self ref Arg): int
+{
+	if(arg.opts != ""){
+		arg.c = arg.opts[0];
+		arg.opts = arg.opts[1:];
+		return arg.c;
+	}
+	if(arg.argv == nil)
+		return arg.c = 0;
+	arg.opts = hd arg.argv;
+	if(len arg.opts < 2 || arg.opts[0] != '-')
+		return arg.c = 0;
+	arg.argv = tl arg.argv;
+	if(arg.opts == "--")
+		return arg.c = 0;
+	arg.c = arg.opts[1];
+	arg.opts = arg.opts[2:];
+	return arg.c;
+}
+
+Arg.arg(arg: self ref Arg): string
+{
+	s := arg.opts;
+	arg.opts = "";
+	if(s != "")
+		return s;
+	if(arg.argv == nil)
+		return "";
+	s = hd arg.argv;
+	arg.argv = tl arg.argv;
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/idea.b
@@ -1,0 +1,116 @@
+implement Idea;
+
+#
+# Copyright © 2002 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "keyring.m";
+	keyring: Keyring;
+
+Idea: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+decerr(s: string)
+{
+	sys->fprint(sys->fildes(2), "decrypt error: %s (wrong password ?)\n", s);
+	exit;
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdin := sys->fildes(0);
+	stdout := sys->fildes(1);
+
+	bufio = load Bufio Bufio->PATH;
+	keyring = load Keyring Keyring->PATH;
+
+	obuf := array[8] of byte;
+	buf := array[8] of byte;
+	key := array[16] of byte;
+
+	argc := len argv;
+	if((argc != 3 && argc != 4) || (hd tl argv != "-e" && hd tl argv != "-d") || len hd tl tl argv != 16){
+		sys->fprint(sys->fildes(2), "usage: idea -[e | d] <16 char key> [inputfile]\n");
+		exit;
+	}
+	dec := hd tl argv == "-d";
+	if(argc == 4){
+		s := hd tl tl tl argv;
+		stdin = sys->open(s, Sys->OREAD);
+		if(stdin == nil){
+			sys->fprint(sys->fildes(2), "cannot open %s\n", s);
+			exit;
+		}
+		if(dec){
+			l := len s;
+			if(s[l-3: l] != ".id"){
+				sys->fprint(sys->fildes(2), "input file not a .id file\n");
+				exit;
+			}
+			s = s[0: l-3];
+		}
+		else
+			s += ".id";
+		stdout = sys->create(s, Sys->OWRITE, 8r666);
+		if(stdout == nil){
+			sys->fprint(sys->fildes(2), "cannot create %s\n", s);
+			exit;
+		}
+	}
+	for(i := 0; i < 16; i++)
+		key[i] = byte (hd tl tl argv)[i];
+	is := keyring->ideasetup(key, nil);
+	m := om := 0;
+	bin := bufio->fopen(stdin, Bufio->OREAD);
+	bout := bufio->fopen(stdout, Bufio->OWRITE);
+	for(;;){
+		n := bin.read(buf[m: ], 8-m);
+		if(n <= 0)
+			break;
+		m += n;
+		if(m == 8){
+			keyring->ideaecb(is, buf, 8, dec);
+			if(dec){	# leave last block around
+				if(om > 0)
+					bout.write(obuf, 8);
+				obuf[0: ] = buf[0: 8];
+				om = 8;
+			}
+			else
+				bout.write(buf, 8);
+			m = 0;
+		}
+	}
+	if(dec){
+		if(om != 8)
+			decerr("no last block");
+		if(m != 0)
+			decerr("last block not 8 bytes long");
+		m = int obuf[7];
+		if(m < 0 || m > 7)
+			decerr("bad modulus");
+		for(i = m; i < 8-1; i++)
+			if(obuf[i] != byte 0)
+				decerr("byte not 0");
+		bout.write(obuf, m);
+	}
+	else{
+		for(i = m; i < 8; i++)
+			buf[i] = byte 0;
+		buf[7] = byte m;
+		keyring->ideaecb(is, buf, 8, dec);
+		bout.write(buf, 8);
+	}
+	bout.flush();
+	bin.close();
+	bout.close();
+}	
--- /dev/null
+++ b/appl/cmd/import.b
@@ -1,0 +1,165 @@
+implement Import;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+include "dial.m";
+include "keyring.m";
+include "security.m";
+include "factotum.m";
+include "encoding.m";
+include "arg.m";
+
+Import: module
+{
+	init:	 fn(nil: ref Draw->Context, nil: list of string);
+};
+
+factotumfile := "/mnt/factotum/rpc";
+
+fail(status, msg: string)
+{
+	sys->fprint(sys->fildes(2), "import: %s\n", msg);
+	raise "fail:"+status;
+}
+
+nomod(mod: string)
+{
+	fail("load", sys->sprint("can't load %s: %r", mod));
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	factotum := load Factotum Factotum->PATH;
+	if(factotum == nil)
+		nomod(Factotum->PATH);
+	factotum->init();
+	dial := load Dial Dial->PATH;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+
+	arg->init(args);
+	arg->setusage("import [-a|-b] [-c] [-e enc digest] host file [localfile]");
+	flags := 0;
+	cryptalg := "";	# will be rc4_256 sha1
+	keyspec := "";
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>
+			flags |= Sys->MAFTER;
+		'b' =>
+			flags |= Sys->MBEFORE;
+		'c' =>
+			flags |= Sys->MCREATE;
+		'e' =>
+			cryptalg = arg->earg();
+			if(cryptalg == "clear")
+				cryptalg = nil;
+		'k' =>
+			keyspec = arg->earg();
+		'9' =>
+			;
+		*   =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 2 && len args != 3)
+		arg->usage();
+	arg = nil;
+	addr := hd args;
+	file := hd tl args;
+	mountpt := file;
+	if(len args > 2)
+		mountpt = hd tl tl args;
+
+	sys->pctl(Sys->FORKFD, nil);
+
+	facfd := sys->open(factotumfile, Sys->ORDWR);
+	if(facfd == nil)
+		fail("factotum", sys->sprint("can't open %s: %r", factotumfile));
+
+	dest := dial->netmkaddr(addr, "net", "exportfs");
+	c := dial->dial(dest, nil);
+	if(c == nil)
+		fail("dial failed",  sys->sprint("can't dial %s: %r", dest));
+	ai := factotum->proxy(c.dfd, facfd, "proto=p9any role=client "+keyspec);
+	if(ai == nil)
+		fail("auth", sys->sprint("can't authenticate import: %r"));
+	if(sys->fprint(c.dfd, "%s", file) < 0)
+		fail("import", sys->sprint("can't write to remote: %r"));
+	buf := array[256] of byte;
+	if((n := sys->read(c.dfd, buf, len buf)) != 2 || buf[0] != byte 'O' || buf[1] != byte 'K'){
+		if(n >= 4)
+			sys->werrstr("bad remote tree: "+string buf[0:n]);
+		fail("import", sys->sprint("import %s %s: %r", addr, file));
+	}
+	if(cryptalg != nil){
+		if(ai.secret == nil)
+			fail("import", "factotum didn't establish shared secret");
+		random := load Random Random->PATH;
+		if(random == nil)
+			nomod(Random->PATH);
+		kr := load Keyring Keyring->PATH;
+		if(kr == nil)
+			nomod(Keyring->PATH);
+		base64 := load Encoding Encoding->BASE64PATH;
+		if(base64 == nil)
+			nomod(Encoding->BASE64PATH);
+		if(sys->fprint(c.dfd, "impo nofilter ssl\n") < 0)
+			fail("import", sys->sprint("can't write to remote: %r"));
+		key := array[16] of byte;	# myrand[4] secret[8] hisrand[4]
+		key[0:] = random->randombuf(Random->ReallyRandom, 4);
+		ns := len ai.secret;
+		if(ns > 8)
+			ns = 8;
+		key[4:] = ai.secret[0:ns];
+		if(sys->write(c.dfd, key, 4) != 4)
+			fail("import", sys->sprint("can't write key to remote: %r"));
+		if(sys->readn(c.dfd, key[12:], 4) != 4)
+			fail("import", sys->sprint("can't read remote key: %r"));
+		digest := array[Keyring->SHA1dlen] of byte;
+		kr->sha1(key, len key, digest, nil);
+		err: string;
+		(c.dfd, err) = pushssl(c.dfd, base64->dec(S(digest[0:10])), base64->dec(S(digest[10:20])), cryptalg);
+		if(err != nil)
+			fail("import", sys->sprint("can't push security layer: %s", err));
+	}else
+		if(sys->fprint(c.dfd, "impo nofilter clear\n") < 0)
+			fail("import", sys->sprint("can't write to remote: %r"));
+	afd := sys->fauth(c.dfd, "");
+	if(afd != nil)
+		factotum->proxy(afd, facfd, "proto=p9any role=client");
+	if(sys->mount(c.dfd, afd, mountpt, flags, "") < 0)
+		fail("mount failed", sys->sprint("import %s %s: mount failed: %r", addr, file));
+}
+
+S(a: array of byte): string
+{
+	s := "";
+	for(i:=0; i<len a; i++)
+		s += sys->sprint("%.2ux", int a[i]);
+	return s;
+}
+
+pushssl(fd: ref Sys->FD, secretin, secretout: array of byte, alg: string): (ref Sys->FD, string)
+{
+	ssl := load SSL SSL->PATH;
+	if(ssl == nil)
+		nomod(SSL->PATH);
+
+	(err, c) := ssl->connect(fd);
+	if(err != nil)
+		return (nil, "can't connect ssl: " + err);
+
+	err = ssl->secret(c, secretin, secretout);
+	if(err != nil)
+		return (nil, "can't write secret: " + err);
+	if(sys->fprint(c.cfd, "alg %s", alg) < 0)
+		return (nil, sys->sprint("can't push algorithm %s: %r", alg));
+
+	return (c.dfd, nil);
+}
--- /dev/null
+++ b/appl/cmd/install/NOTICE
@@ -1,0 +1,6 @@
+Most of the code in this directory is a limbo version of Russ Cox's wrap, the
+software package manager that was written for Plan9 distributions. His original
+C code may have  been modularized and partly rewritten to use limbo features,
+but the credit and thanks must go to Russ for developing the original system.
+
+
--- /dev/null
+++ b/appl/cmd/install/applylog.b
@@ -1,0 +1,699 @@
+implement Applylog;
+
+#
+# apply a plan 9-style replica log
+# this version applies everything and doesn't use the database
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "keyring.m";
+	kr: Keyring;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "logs.m";
+	logs: Logs;
+	Db, Entry, Byname, Byseq: import logs;
+	S: import logs;
+
+include "arg.m";
+
+Applylog: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Apply, Applydb, Install, Asis, Skip: con iota;
+
+client:	ref Db;	# client current state from client log
+updates:	ref Db;	# state delta from new section of server log
+
+nerror := 0;
+nconflict := 0;
+debug := 0;
+verbose := 0;
+resolve := 0;
+setuid := 0;
+setgid := 0;
+nflag := 0;
+timefile: string;
+clientroot: string;
+srvroot: string;
+logfd: ref Sys->FD;
+now := 0;
+gen := 0;
+noerr := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	bufio = load Bufio Bufio->PATH;
+	ensure(bufio, Bufio->PATH);
+	str = load String String->PATH;
+	ensure(str, String->PATH);
+	kr = load Keyring Keyring->PATH;
+	ensure(kr, Keyring->PATH);
+	daytime = load Daytime Daytime->PATH;
+	ensure(daytime, Daytime->PATH);
+	logs = load Logs Logs->PATH;
+	ensure(logs, Logs->PATH);
+	logs->init(bufio);
+
+	arg := load Arg Arg->PATH;
+	ensure(arg, Arg->PATH);
+	arg->init(args);
+	arg->setusage("applylog [-vuged] [-sc] [-T timefile] clientlog clientroot serverroot [path ... ] <serverlog");
+	dump := 0;
+	while((o := arg->opt()) != 0)
+		case o {
+		'T' =>	timefile = arg->earg();
+		'd' =>	dump = 1; debug = 1;
+		'e' =>	noerr = 1;
+		'g' =>	setgid = 1;
+		'n' =>	nflag = 1; verbose = 1;
+		's' or 'c' =>	resolve = o;
+		'u' =>	setuid = 1;
+		'v' =>	verbose = 1;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args < 3)
+		arg->usage();
+	arg = nil;
+
+	now = daytime->now();
+	client = Db.new("client log");
+	updates = Db.new("update log");
+	clientlog := hd args; args = tl args;
+	clientroot = hd args; args = tl args;
+	srvroot = hd args; args = tl args;
+	if(args != nil)
+		error("restriction by path not yet done");
+
+	checkroot(clientroot, "client root");
+	checkroot(srvroot, "server root");
+
+	# replay the client log to build last installation state of files taken from server
+	if(nflag)
+		logfd = sys->open(clientlog, Sys->OREAD);
+	else
+		logfd = sys->open(clientlog, Sys->ORDWR);
+	if(logfd == nil)
+		error(sys->sprint("can't open %s: %r", clientlog));
+	f := bufio->fopen(logfd, Sys->OREAD);
+	if(f == nil)
+		error(sys->sprint("can't open %s: %r", clientlog));
+	while((log := readlog(f)) != nil)
+		replaylog(client, log);
+	f = nil;
+	sys->seek(logfd, big 0, 2);
+	if(dump)
+		dumpstate();
+	if(debug){
+		sys->print("	CLIENT STATE\n");
+		client.sort(Byname);
+		dumpdb(client, 0);
+	}
+
+	# read server's log and use the new section to build a sequence of update actions
+	minseq := big 0;
+	if(timefile != nil)
+		minseq = readseq(timefile);
+	f = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	while((log = readlog(f)) != nil)
+		if(log.seq > minseq)
+			update(updates, updates.look(log.path), log);
+	updates.sort(Byseq);
+	if(debug){
+		sys->print("	SEQUENCED UPDATES\n");
+		dumpdb(updates, 1);
+	}
+
+	# apply those actions
+	maxseq := minseq;
+	skip := 0;
+	for(i := 0; i < updates.nstate; i++){
+		e := updates.state[i];
+		ce := client.look(e.path);
+		if(ce != nil && ce.seq >= e.seq){	# replay
+			if(debug)
+				sys->print("replay %c %q\n", e.action, e.path);
+			if(!nflag && !skip)
+				maxseq = e.seq;
+			continue;
+		}
+		if(verbose)
+			sys->print("%s\n", e.sumtext());
+		case chooseaction(e) {
+		Install =>
+			if(debug)
+				sys->print("resolve %q to install\n", e.path);
+			c := e;
+			c.action = 'a';	# force (re)creation/installation
+			if(!enact(c)){
+				skip = 1;
+				continue;	# don't update db
+			}
+		Apply =>
+			if(!enact(e)){
+				skip = 1;
+				continue;	# don't update db
+			}
+		Applydb =>
+			if(debug)
+				sys->print("resolve %q to update db\n", e.path);
+			# carry on to update the log
+		Asis =>
+			if(debug)
+				sys->print("resolve %q to client\n", e.path);
+			#continue;	# ?
+		Skip =>
+			if(debug)
+				sys->print("conflict %q\n", e.path);
+			skip = 1;
+			continue;
+		* =>
+			error("internal error: unexpected result from chooseaction");
+		}
+		# action complete: add to client log
+		if(ce == nil)
+			ce = client.entry(e.seq, e.path, e.d);
+		ce.update(e);
+		if(!nflag){
+			if(!skip)
+				maxseq = e.seq;
+			if(logfd != nil){
+				# append action, now accepted, to client's own log
+				if(sys->fprint(logfd, "%s\n", e.logtext()) < 0)
+					error(sys->sprint("error writing to %q: %r", clientlog));
+			}
+		}
+	}
+	sys->fprint(sys->fildes(2), "maxseq: %bud %bud\n", maxseq>>32, maxseq & 16rFFFFFFFF);
+	if(!nflag && !skip && timefile != nil)
+		writeseq(timefile, maxseq);
+	if(nconflict)
+		raise sys->sprint("fail:%d conflicts", nconflict);
+	if(nerror)
+		raise sys->sprint("fail:%d errors", nerror);
+}
+
+checkroot(dir: string, what: string)
+{
+	(ok, d) := sys->stat(dir);
+	if(ok < 0)
+		error(sys->sprint("can't stat %s %q: %r", what, dir));
+	if((d.mode & Sys->DMDIR) == 0)
+		error(sys->sprint("%s %q: not a directory", what, dir));
+}
+
+readlog(in: ref Iobuf): ref Entry
+{
+	(e, err) := Entry.read(in);
+	if(err != nil)
+		error(err);
+	return e;
+}
+
+readseq(file: string): big
+{
+	fd := sys->open(file, Sys->OREAD);
+	if(fd == nil)
+		error(sys->sprint("can't open %q: %r", file));
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		error(sys->sprint("can't read valid seq from %q", file));
+	(nf, flds) := sys->tokenize(string buf[0:n], " \t\n");
+	if(nf != 2)
+		error(sys->sprint("illegal sequence number in %q", file));
+	n0 := bigof(hd flds, 10);
+	n1 := bigof(hd tl flds, 10);
+	return (n0 << 32) | n1;
+}
+
+writeseq(file: string, n: big)
+{
+	fd := sys->create(file, Sys->OWRITE, 8r666);
+	if(fd == nil)
+		error(sys->sprint("can't create %q: %r", file));
+	if(sys->fprint(fd, "%11bud %11bud", n>>32, n&16rFFFFFFFF) < 0)
+		error(sys->sprint("error writing seq to %q: %r", file));
+}
+
+#
+# replay a log to reach the state wrt files previously taken from the server
+#
+replaylog(db: ref Db, log: ref Entry)
+{
+	e := db.look(log.path);
+	indb := e != nil && !e.removed();
+	case log.action {
+	'a' =>	# add new file
+		if(indb){
+			note(sys->sprint("%q duplicate create", log.path));
+			return;
+		}
+	'c' =>	# contents
+		if(!indb){
+			note(sys->sprint("%q contents but no entry", log.path));
+			return;
+		}
+	'd' =>	# delete
+		if(!indb){
+			note(sys->sprint("%q deleted but no entry", log.path));
+			return;
+		}
+		if(e.d.mtime > log.d.mtime){
+			note(sys->sprint("%q deleted but it's newer", log.path));
+			return;
+		}
+	'm' =>	# metadata
+		if(!indb){
+			note(sys->sprint("%q metadata but no entry", log.path));
+			return;
+		}
+	* =>
+		error(sys->sprint("bad log entry: %bd %bd", log.seq>>32, log.seq & big 16rFFFFFFFF));
+	}
+	update(db, e, log);
+}
+
+#
+# update file state e to reflect the effect of the log,
+# creating a new entry if necessary
+#
+update(db: ref Db, e: ref Entry, log: ref Entry)
+{
+	if(e == nil)
+		e = db.entry(log.seq, log.path, log.d);
+	e.update(log);
+}
+
+chooseaction(e: ref Entry): int
+{
+	cf := logs->mkpath(clientroot, e.path);
+	sf := logs->mkpath(srvroot, e.serverpath);
+	(ishere, cd) := sys->stat(logs->mkpath(clientroot, e.path));
+	ishere = ishere >= 0;				# in local file system
+	db := client.look(e.path);
+	indb := db != nil && !db.removed();	# previously arrived from server
+
+	unchanged := indb && ishere && (samestat(db.d, cd) || samecontents(sf, cf)) || !indb && !ishere;
+	if(unchanged && (e.action != 'm' || samemeta(db.d, cd)))
+		return Apply;
+	if(!ishere && e.action == 'd'){
+		if(indb)
+			return Applydb;
+		return Asis;
+	}
+	case resolve {
+	'c' =>
+		return Asis;
+	's' =>
+		if(!ishere || e.action == 'm' && !unchanged)
+			return Install;
+		return Apply;
+	* =>
+		# describe source of conflict
+		if(indb){
+			if(ishere){
+				if(e.action == 'm' && unchanged && !samemeta(db.d, cd))
+					conflict(e.path, "locally modified metadata", action(e.action));
+				else
+					conflict(e.path, "locally modified", action(e.action));
+			}else
+				conflict(e.path, "locally removed", action(e.action));
+		}else{
+			if(db != nil)
+				conflict(e.path, "locally retained or recreated", action(e.action));	# server installed it but later removed it
+			else
+				conflict(e.path, "locally created", action(e.action));
+		}
+		return Skip;
+	}
+}
+
+enact(e: ref Entry): int
+{
+	if(nflag)
+		return 0;
+	srcfile := logs->mkpath(srvroot, e.serverpath);
+	dstfile := logs->mkpath(clientroot, e.path);
+	case e.action {
+	'a' =>	# create and copy in
+		if(debug)
+			sys->print("create %q\n", dstfile);
+		if(e.d.mode & Sys->DMDIR)
+			err := mkdir(dstfile, e);
+		else
+			err = copyin(srcfile, dstfile, 1, e);
+		if(err != nil){
+			if(noerr)
+				error(err);
+			warn(err);
+			return 0;
+		}
+	'c' =>	# contents
+		err := copyin(srcfile, dstfile, 0, e);
+		if(err != nil){
+			if(noerr)
+				error(err);
+			warn(err);
+			return 0;
+		}
+	'd' =>	# delete
+		if(debug)
+			sys->print("remove %q\n", dstfile);
+		if(remove(dstfile) < 0){
+			warn(sys->sprint("can't remove %q: %r", dstfile));
+			return 0;
+		}
+	'm' =>	# metadata
+		if(debug)
+			sys->print("wstat %q\n", dstfile);
+		d := sys->nulldir;
+		d.mode = e.d.mode;
+		if(sys->wstat(dstfile, d) < 0)
+			warn(sys->sprint("%q: can't change mode to %uo", dstfile, d.mode));
+		if(setgid){
+			d = sys->nulldir;
+			d.gid = e.d.gid;
+			if(sys->wstat(dstfile, d) < 0)
+				warn(sys->sprint("%q: can't change gid to %q", dstfile, d.gid));
+		}
+		if(setuid){
+			d = sys->nulldir;
+			d.uid = e.d.uid;
+			if(sys->wstat(dstfile, d) < 0)
+				warn(sys->sprint("%q: can't change uid to %q", dstfile, d.uid));
+		}
+	* =>
+		error(sys->sprint("unexpected log operation: %c %q", e.action, e.path));
+		return 0;
+	}
+	return 1;
+}
+
+rev[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+ensure[T](m: T, path: string)
+{
+	if(m == nil)
+		error(sys->sprint("can't load %s: %r", path));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "applylog: %s\n", s);
+	raise "fail:error";
+}
+
+note(s: string)
+{
+	sys->fprint(sys->fildes(2), "applylog: note: %s\n", s);
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "applylog: warning: %s\n", s);
+	nerror++;
+}
+
+conflict(name: string, why: string, wont: string)
+{
+	sys->fprint(sys->fildes(2), "%q: %s; will not %s\n", name, why, wont);
+	nconflict++;
+}
+
+action(a: int): string
+{
+	case a {
+	'a' =>	return "create";
+	'c' =>	return "update";
+	'd' =>	return "delete";
+	'm' =>	return "update metadata";
+	* =>	return sys->sprint("unknown action %c", a);
+	}
+}
+
+samecontents(path1, path2: string): int
+{
+	f1 := sys->open(path1, Sys->OREAD);
+	if(f1 == nil)
+		return 0;
+	f2 := sys->open(path2, Sys->OREAD);
+	if(f2 == nil)
+		return 0;
+	b1 := array[Sys->ATOMICIO] of byte;
+	b2 := array[Sys->ATOMICIO] of byte;
+	n := 256;	# start with something small; dis files and big executables should fail more quickly
+	n1, n2: int;
+	do{
+		n1 = sys->read(f1, b1, n);
+		n2 = sys->read(f2, b2, n);
+		if(n1 != n2)
+			return 0;
+		for(i := 0; i < n1; i++)
+			if(b1[i] != b2[i])
+				return 0;
+		n += len b1 - n;
+	}while(n1 > 0);
+	return 1;
+}
+
+samestat(a: Sys->Dir, b: Sys->Dir): int
+{
+	# doesn't check permission/ownership, does check QTDIR/QTFILE
+	if(a.mode & Sys->DMDIR)
+		return (b.mode & Sys->DMDIR) != 0;
+	return a.length == b.length && a.mtime == b.mtime && a.qid.qtype == b.qid.qtype;	# TO DO: a.name==b.name?
+}
+
+samemeta(a: Sys->Dir, b: Sys->Dir): int
+{
+	return a.mode == b.mode && (!setuid || a.uid == b.uid) && (!setgid || a.gid == b.gid) && samestat(a, b);
+}
+
+bigof(s: string, base: int): big
+{
+	(b, r) := str->tobig(s, base);
+	if(r != nil)
+		error("cruft in integer field in log entry: "+s);
+	return b;
+}
+
+intof(s: string, base: int): int
+{
+	return int bigof(s, base);
+}
+
+mkdir(dstpath: string, e: ref Entry): string
+{
+	fd := create(dstpath, Sys->OREAD, e.d.mode);
+	if(fd == nil)
+		return sys->sprint("can't mkdir %q: %r", dstpath);
+	fchmod(fd, e.d.mode);
+	if(setgid)
+		fchgrp(fd, e.d.gid);
+	if(setuid)
+		fchown(fd, e.d.uid);
+#	e.d.mtime = now;
+	return nil;
+}
+
+fchmod(fd: ref Sys->FD, mode: int)
+{
+	d := sys->nulldir;
+	d.mode = mode;
+	if(sys->fwstat(fd, d) < 0)
+		warn(sys->sprint("%q: can't set mode %o: %r", sys->fd2path(fd), mode));
+}
+
+fchgrp(fd: ref Sys->FD, gid: string)
+{
+	d := sys->nulldir;
+	d.gid = gid;
+	if(sys->fwstat(fd, d) < 0)
+		warn(sys->sprint("%q: can't set group id %s: %r", sys->fd2path(fd), gid));
+}
+
+fchown(fd: ref Sys->FD, uid: string)
+{
+	d := sys->nulldir;
+	d.uid = uid;
+	if(sys->fwstat(fd, d) < 0)
+		warn(sys->sprint("%q: can't set user id %s: %r", sys->fd2path(fd), uid));
+}
+
+copyin(srcpath: string, dstpath: string, dowstat: int, e: ref Entry): string
+{
+	if(debug)
+		sys->print("copyin %q -> %q\n", srcpath, dstpath);
+	f := sys->open(srcpath, Sys->OREAD);
+	if(f == nil)
+		return sys->sprint("can't open %q: %r", srcpath);
+	t: ref Sys->FD;
+	(ok, nil) := sys->stat(dstpath);
+	if(ok < 0){
+		t = create(dstpath, Sys->OWRITE, e.d.mode | 8r222);
+		if(t == nil)
+			return sys->sprint("can't create %q: %r", dstpath);
+		# TO DO: force access to parent directory
+		dowstat = 1;
+	}else{
+		t = sys->open(dstpath, Sys->OWRITE|Sys->OTRUNC);
+		if(t == nil){
+			err := sys->sprint("%r");
+			if(!contains(err, "permission"))
+				return sys->sprint("can't overwrite %q: %s", dstpath, err);
+		}
+	}
+	(nw, err) := copy(f, t);
+	if(err != nil)
+		return err;
+	if(nw != e.d.length)
+		warn(sys->sprint("%q: log said %bud bytes, copied %bud bytes", dstpath, e.d.length, nw));
+	f = nil;
+	if(dowstat){
+		fchmod(t, e.d.mode);
+		if(setgid)
+			fchgrp(t, e.d.gid);
+		if(setuid)
+			fchown(t, e.d.uid);
+	}
+	nd := sys->nulldir;
+	nd.mtime = e.d.mtime;
+	if(sys->fwstat(t, nd) < 0)
+		warn(sys->sprint("%q: can't set mtime: %r", dstpath));
+	return nil;
+}
+
+copy(f: ref Sys->FD, t: ref Sys->FD): (big, string)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	nw := big 0;
+	while((n := sys->read(f, buf, len buf)) > 0){
+		if(sys->write(t, buf, n) != n)
+			return (nw, sys->sprint("error writing %q: %r", sys->fd2path(t)));
+		nw += big n;
+	}
+	if(n < 0)
+		return (nw, sys->sprint("error reading %q: %r", sys->fd2path(f)));
+	return (nw, nil);
+}
+
+contents(e: ref Entry): string
+{
+	s := "";
+	for(cl := e.contents; cl != nil; cl = tl cl)
+		s += " " + hd cl;
+	return s;
+}
+
+dumpstate()
+{
+	for(i := 0; i < client.nstate; i++)
+		sys->print("%d\t%s\n", i, client.state[i].text());
+}
+
+dumpdb(db: ref Db, tag: int)
+{
+	for(i := 0; i < db.nstate; i++){
+		if(!tag)
+			s := db.state[i].dbtext();
+		else
+			s = db.state[i].text();
+		if(s != nil)
+			sys->print("%s\n", s);
+	}
+}
+
+#
+# perhaps these should be in a utility module
+#
+parent(name: string): string
+{
+	slash := -1;
+	for(i := 0; i < len name; i++)
+		if(name[i] == '/')
+			slash = i;
+	if(slash > 0)
+		return name[0:slash];
+	return "/";
+}
+
+writableparent(name: string): (int, string)
+{
+	p := parent(name);
+	(ok, d) := sys->stat(p);
+	if(ok < 0)
+		return (-1, nil);
+	nd := sys->nulldir;
+	nd.mode |= 8r222;
+	sys->wstat(p, nd);
+	return (d.mode, p);
+}
+
+create(name: string, rw: int, mode: int): ref Sys->FD
+{
+	fd := sys->create(name, rw, mode);
+	if(fd == nil){
+		err := sys->sprint("%r");
+		if(!contains(err, "permission")){
+			sys->werrstr(err);
+			return nil;
+		}
+		(pm, p) := writableparent(name);
+		if(pm >= 0){
+			fd = sys->create(name, rw, mode);
+			d := sys->nulldir;
+			d.mode = pm;
+			sys->wstat(p, d);
+		}
+		sys->werrstr(err);
+	}
+	return fd;
+}
+
+remove(name: string): int
+{
+	if(sys->remove(name) >= 0)
+		return 0;
+	err := sys->sprint("%r");
+	if(contains(err, "entry not found") || contains(err, "not exist"))
+		return 0;
+	(pm, p) := writableparent(name);
+	rc := sys->remove(name);
+	d := sys->nulldir;
+	if(pm >= 0){
+		d.mode = pm;
+		sys->wstat(p, d);
+	}
+	sys->werrstr(err);
+	return rc;
+}
+
+contains(s: string, sub: string): int
+{
+	return str->splitstrl(s, sub).t1 != nil;
+}
--- /dev/null
+++ b/appl/cmd/install/arch.b
@@ -1,0 +1,288 @@
+implement Arch;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "daytime.m";
+	daytime : Daytime;
+include "string.m";
+	str : String;
+include "bufio.m";
+	bufio : Bufio;
+	Iobuf : import bufio;
+include "sh.m";
+include "arch.m";
+
+addp := 1;
+
+buf := array[Sys->ATOMICIO] of byte;
+
+init(bio: Bufio)
+{
+	sys = load Sys Sys->PATH;
+	if(bio == nil)
+		bufio = load Bufio Bufio->PATH;
+	else
+		bufio = bio;
+	daytime = load Daytime Daytime->PATH;
+	str = load String String->PATH;
+}
+
+addperms(p: int)
+{
+	addp = p;
+}
+
+openarch(file : string) : ref Archive
+{
+	return openarch0(file, 1);
+}
+
+openarchfs(file : string) : ref Archive
+{
+	return openarch0(file, 0);
+}
+
+openarch0(file : string, newpgrp : int) : ref Archive
+{
+	pid := 0;
+	canseek := 1;
+	b := bufio->open(file, Bufio->OREAD);
+	if (b == nil)
+		return nil;
+	if (b.getb() == 16r1f && ((c := b.getb()) == 16r8b || c == 16r9d)) {
+		# spawn gunzip
+		canseek = 0;
+		(b, pid) = gunzipstream(file, newpgrp);
+		if (b == nil)
+			return nil;
+	}
+	else
+		b.seek(big 0, Bufio->SEEKSTART);
+	ar := ref Archive;
+	ar.b = b;
+	ar.nexthdr = 0;
+	ar.canseek = canseek;
+	ar.pid = pid;
+	ar.hdr = ref Ahdr;
+	ar.hdr.d = ref Sys->Dir;
+	return ar;
+}
+
+EOARCH : con "end of archive\n";
+PREMEOARCH : con "premature end of archive";
+NFLDS : con 6;
+
+openarchgz(file : string) : (string, ref Sys->FD)
+{
+	ar := openarch(file);
+	if (ar == nil || ar.canseek)
+		return (nil, nil);
+	(newfile, fd) := opentemp("wrap.gz");
+	if (fd == nil)
+		return (nil, nil);
+	bout := bufio->fopen(fd, Bufio->OWRITE);
+	if (bout == nil)
+		return (nil, nil);
+	while ((a := gethdr(ar)) != nil) {
+		if (len a.name >= 5 && a.name[0:5] == "/wrap") {
+			puthdr(bout, a.name, a.d);
+			getfile(ar, bout, int a.d.length);
+		}
+		else
+			break;
+	}
+	closearch(ar);
+	bout.puts(EOARCH);
+	bout.flush();
+	sys->seek(fd, big 0, Sys->SEEKSTART);
+	return (newfile, fd);
+}
+
+gunzipstream(file : string, newpgrp : int) : (ref Iobuf, int)
+{
+	p := array[2] of ref Sys->FD;
+	if (sys->pipe(p) < 0)
+		return (nil, 0);
+	fd := sys->open(file, Sys->OREAD);
+	if (fd == nil)
+		return (nil, 0);
+	b := bufio->fopen(p[0], Bufio->OREAD);
+	if (b == nil)
+		return (nil, 0);
+	c := chan of int;
+	spawn gunzip(fd, p[1], c, newpgrp);
+	pid := <- c;
+	p[0] = p[1] = nil;
+	if (pid < 0)
+		return (nil, 0);
+	return (b, pid);
+}
+
+GUNZIP : con "/dis/gunzip.dis";
+
+gunzip(stdin : ref Sys->FD, stdout : ref Sys->FD, c : chan of int, newpgrp : int)
+{
+	if (newpgrp)
+		pid := sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	else
+		pid = sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	sys->dup(stdout.fd, 1);
+	sys->dup(1, 2);
+	stdin = stdout = nil;
+	cmd := load Command GUNZIP;
+	if (cmd == nil) {
+		c <-= -1;
+		return;
+	}
+	c <-= pid;
+	cmd->init(nil, GUNZIP :: nil);
+}
+
+closearch(ar : ref Archive)
+{
+	if (ar.pid != 0) {
+		fd := sys->open("#p/" + string ar.pid + "/ctl", sys->OWRITE);
+		if (fd != nil)
+			sys->fprint(fd, "killgrp");
+	}
+	ar.b.close();
+	ar.b = nil;
+}
+
+gethdr(ar : ref Archive) : ref Ahdr
+{
+	a := ar.hdr;
+	b := ar.b;
+	m := int b.offset();
+	n := ar.nexthdr;
+	if (m != n) {
+		if (ar.canseek)
+			b.seek(big n, Bufio->SEEKSTART);
+		else {
+			if (m > n)
+				fatal(sys->sprint("bad offset in gethdr: m=%d n=%d", m, n));
+			if(drain(ar, n-m) < 0)
+				return nil;
+		}
+	}
+	if ((s := b.gets('\n')) == nil) {
+		ar.err = PREMEOARCH;
+		return nil;
+	}
+# fd := sys->open("./debug", Sys->OWRITE);
+# sys->seek(fd, 0, Sys->SEEKEND);
+# sys->fprint(fd, "gethdr: %d %d %d %d %s\n", ar.canseek, m, n, b.offset(), s);
+# fd = nil;
+	if (s == EOARCH)
+		return nil;
+	(nf, fs) := sys->tokenize(s, " \t\n");
+	if(nf != NFLDS) {
+		ar.err = "too few fields in file header";
+		return nil;
+	}
+	a.name = hd fs;						fs = tl fs;
+	(a.d.mode, nil) = str->toint(hd fs, 8);		fs = tl fs;
+	a.d.uid = hd fs;						fs = tl fs;
+	a.d.gid = hd fs;						fs = tl fs;
+	(a.d.mtime, nil) = str->toint(hd fs, 10);	fs = tl fs;
+	(tmp, nil) := str->toint(hd fs, 10);		fs = tl fs;
+	a.d.length = big tmp;
+	ar.nexthdr = int (b.offset()+a.d.length);
+	return a;
+}
+
+getfile(ar : ref Archive, bout : ref Bufio->Iobuf, n : int) : string
+{
+	err: string;
+	bin := ar.b;
+	while (n > 0) {
+		m := len buf;
+		if (n < m)
+			m = n;
+		p := bin.read(buf, m);
+		if (p != m)
+			return PREMEOARCH;
+		p = bout.write(buf, m);
+		if (p != m)
+			err = sys->sprint("cannot write: %r");
+		n -= m;
+	}
+	return err;	
+}
+
+puthdr(b : ref Iobuf, name : string, d : ref Sys->Dir)
+{
+	mode := d.mode;
+	if(addp){
+		mode |= 8r664;
+		if(mode & Sys->DMDIR || mode & 8r111)
+			mode |= 8r111;
+	}
+	b.puts(sys->sprint("%s %uo %s %s %ud %d\n", name, mode, d.uid, d.gid, d.mtime, int d.length));
+}
+
+putstring(b : ref Iobuf, s : string)
+{
+	b.puts(s);
+}
+
+putfile(b : ref Iobuf, f : string, n : int) : string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil)
+		return sys->sprint("cannot open %s: %r", f);
+	i := 0;
+	for (;;) {
+		m := sys->read(fd, buf, len buf);
+		if (m < 0)
+			return sys->sprint("cannot read %s: %r", f);
+		if (m == 0)
+			break;
+		if (b.write(buf, m) != m)
+			return sys->sprint("%s: cannot write: %r", f);
+		i += m;
+	}
+	if (i != n) {
+		b.seek(big (n-i), Sys->SEEKRELA);
+		return sys->sprint("%s: %d bytes written: should be %d", f, i, n);
+	}
+	return nil;
+}
+
+putend(b : ref Iobuf)
+{
+	b.puts(EOARCH);
+	b.flush();
+}
+
+drain(ar : ref Archive, n : int) : int
+{
+	while (n > 0) {
+		m := n;
+		if (m > len buf)
+			m = len buf;
+		p := ar.b.read(buf, m);
+		if (p != m){
+			ar.err = "unexpectedly short read";
+			return -1;
+		}
+		n -= m;
+	}
+	return 0;	
+}
+
+opentemp(prefix: string): (string, ref Sys->FD)
+{
+	name := sys->sprint("/tmp/%s.%ud.%d", prefix, daytime->now(), sys->pctl(0, nil));
+	# would use ORCLOSE here but it messes up under Nt
+	fd := sys->create(name, Sys->ORDWR, 8r600);
+	return (name, fd);
+}
+
+fatal(s : string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/install/arch.m
@@ -1,0 +1,36 @@
+Arch : module
+{
+	PATH : con "/dis/install/arch.dis";
+
+	Ahdr : adt {
+		name : string;
+		modestr : string;
+		d : ref Sys->Dir;
+	};
+
+	Archive : adt {
+		b : ref Bufio->Iobuf;
+		nexthdr : int;
+		canseek : int;
+		pid : int;
+		hdr : ref Ahdr;
+		err : string;
+	};
+
+	init: fn(bio: Bufio);
+
+	openarch: fn(name : string) : ref Archive;
+	openarchfs: fn(name : string) : ref Archive;
+	openarchgz: fn(name : string) : (string, ref Sys->FD);
+	gethdr: fn(ar : ref Archive) : ref Ahdr;
+	getfile: fn(ar : ref Archive, bout : ref Bufio->Iobuf, n : int) : string;
+	drain: fn(ar : ref Archive, n : int) : int;
+	closearch: fn(ar : ref Archive);
+
+	puthdr: fn(b : ref Bufio->Iobuf, name : string, d : ref Sys->Dir);
+	putstring: fn(b : ref Bufio->Iobuf, s : string);
+	putfile: fn(b : ref Bufio->Iobuf, f : string, n : int) : string;
+	putend: fn(b : ref Bufio->Iobuf);
+
+	addperms: fn(p: int);
+};
--- /dev/null
+++ b/appl/cmd/install/archfs.b
@@ -1,0 +1,579 @@
+implement Archfs;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "bufio.m";
+	bufio : Bufio;
+include "arg.m";
+	arg : Arg;
+include "string.m";
+	str : String;
+include "daytime.m";
+	daytime : Daytime;
+include "styx.m";
+	styx: Styx;
+include "archfs.m";
+include "arch.m";
+	arch : Arch;
+
+# add write some day
+
+Iobuf : import bufio;
+Tmsg, Rmsg: import styx;
+
+Einuse		: con "fid already in use";
+Ebadfid		: con "bad fid";
+Eopen		: con "fid already opened";
+Enotfound	: con "file does not exist";
+Enotdir		: con "not a directory";
+Eperm		: con "permission denied";
+Ebadarg		: con "bad argument";
+Eexists		: con "file already exists";
+
+UID : con "inferno";
+GID : con "inferno";
+
+DEBUG: con 0;
+
+Dir : adt {
+	dir : Sys->Dir;
+	offset : int;
+	parent : cyclic ref Dir;
+	child : cyclic ref Dir;
+	sibling : cyclic ref Dir;
+};
+
+Fid : adt {
+	fid : int;
+	open: int;
+	dir : ref Dir;
+	next : cyclic ref Fid;
+};
+
+HTSZ : con 32;
+fidtab := array[HTSZ] of ref Fid;
+
+root : ref Dir;
+qid : int;
+mtpt := "/mnt";
+bio : ref Iobuf;
+buf : array of byte;
+skip := 0;
+
+# Archfs : module
+# {
+# 	init : fn(ctxt : ref Draw->Context, args : list of string);
+# };
+
+init(nil : ref Draw->Context, args : list of string)
+{
+	init0(nil, args, nil);
+}
+
+initc(args : list of string, c : chan of int)
+{
+	init0(nil, args, c);
+}
+
+chanint : chan of int;
+
+init0(nil : ref Draw->Context, args : list of string, chi : chan of int)
+{
+	chanint = chi;
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg = load Arg Arg->PATH;
+	str = load String String->PATH;
+	daytime = load Daytime Daytime->PATH;
+	styx = load Styx Styx->PATH;
+	arch = load Arch Arch->PATH;
+	if (bufio == nil || arg == nil || styx == nil || arch == nil)
+		fatal("failed to load modules", 1);
+	styx->init();
+	arch->init(bufio);
+	arg->init(args);
+	while ((c := arg->opt()) != 0) {
+		case c {
+			'm' =>
+				mtpt = arg->arg();
+				if (mtpt == nil)
+					fatal("mount point missing", 1);
+			's' =>
+				skip = 1;
+		}
+	}
+	args = arg->argv();
+	if (args == nil)
+		fatal("missing archive file", 1);
+	buf = array[Sys->ATOMICIO] of byte;
+	# root = newdir("/", UID, GID, 8r755|Sys->DMDIR, daytime->now());
+	root = newdir(basename(mtpt), UID, GID, 8r755|Sys->DMDIR, daytime->now());
+	root.parent = root;
+	readarch(hd args, tl args);
+	p := array[2] of ref Sys->FD;
+	if(sys->pipe(p) < 0)
+		fatal("can't create pipe", 1);
+	ch := chan of ref Tmsg;
+	sync := chan of int;
+	spawn reader(p[1], ch, sync);
+	<- sync;
+	pidch := chan of int;
+	spawn serve(p[1], ch, pidch);
+	pid := <- pidch;
+	if(sys->mount(p[0], nil, mtpt, Sys->MREPL, nil) < 0)
+		fatal(sys->sprint("cannot mount archive on %s: %r", mtpt), 1);
+	p[0] = p[1] = nil;
+	if (chi != nil) {
+		chi <-= pid;
+		chanint = nil;
+	}
+}
+
+reply(fd: ref Sys->FD, m: ref Rmsg): int
+{
+	if(DEBUG)
+		sys->fprint(sys->fildes(2), "R: %s\n", m.text());
+	s := m.pack();
+	if(s == nil)
+		return -1;
+	return sys->write(fd, s, len s);
+}
+
+error(fd: ref Sys->FD, m: ref Tmsg, e : string)
+{
+	reply(fd, ref Rmsg.Error(m.tag, e));
+}
+
+reader(fd: ref Sys->FD, ch: chan of ref Tmsg, sync: chan of int)
+{
+	sys->pctl(Sys->NEWFD|Sys->NEWNS, fd.fd :: nil);
+	sync <-= 1;
+	while((m := Tmsg.read(fd, Styx->MAXRPC)) != nil && tagof m != tagof Tmsg.Readerror)
+		ch <-= m;
+	ch <-= m;
+}
+
+serve(fd: ref Sys->FD, ch : chan of ref Tmsg, pidch : chan of int)
+{
+	e : string;
+	f : ref Fid;
+
+	pidch <-= sys->pctl(0, nil);
+	for (;;) {
+		m0 := <- ch;
+		if (m0 == nil)
+			return;
+		if(DEBUG)
+			sys->fprint(sys->fildes(2), "T: %s\n", m0.text());
+		pick m := m0 {
+			Readerror =>
+				fatal("read error on styx server", 1);
+			Version =>
+				(s, v) := styx->compatible(m, Styx->MAXRPC, Styx->VERSION);
+				reply(fd, ref Rmsg.Version(m.tag, s, v));
+			Auth =>
+				error(fd, m, "no authentication required");
+			Flush =>
+				reply(fd, ref Rmsg.Flush(m.tag));
+			Walk =>
+				(f, e) = mapfid(m.fid);
+				if (e != nil) {
+					error(fd, m, e);
+					continue;
+				}
+				if (f.open) {
+					error(fd, m, Eopen);
+					continue;
+				}
+				err := 0;
+				dir := f.dir;
+				nq := 0;
+				nn := len m.names;
+				qids := array[nn] of Sys->Qid;
+				if(nn > 0){
+					for(k := 0; k < nn; k++){
+						if ((dir.dir.mode & Sys->DMDIR) == 0) {
+							if(k == 0){
+								error(fd, m, Enotdir);
+								err = 1;
+							}
+							break;
+						}
+						dir  = lookup(dir, m.names[k]);
+						if (dir == nil) {
+							if(k == 0){
+								error(fd, m, Enotfound);
+								err = 1;
+							}
+							break;
+						}
+						qids[nq++] = dir.dir.qid;
+					}
+				}
+				if(err)
+					continue;
+				if(nq < nn)
+					qids = qids[0: nq];
+				if(nq == nn){
+					if(m.newfid != m.fid){
+						f = newfid(m.newfid);
+						if (f == nil) {
+							error(fd, m, Einuse);
+							continue;
+						}
+					}
+					f.dir = dir;
+				}
+				reply(fd, ref Rmsg.Walk(m.tag, qids));
+			Open =>
+				(f, e) = mapfid(m.fid);
+				if (e != nil) {
+					error(fd, m, e);
+					continue;
+				}
+				if (m.mode & (Sys->OWRITE|Sys->ORDWR|Sys->OTRUNC|Sys->ORCLOSE)) {
+					error(fd, m, Eperm);
+					continue;
+				}
+				f.open = 1;
+				reply(fd, ref Rmsg.Open(m.tag, f.dir.dir.qid, Styx->MAXFDATA));
+			Create =>
+				error(fd, m, Eperm);
+			Read =>
+				(f, e) = mapfid(m.fid);
+				if (e != nil) {
+					error(fd, m, e);
+					continue;
+				}
+				data := readdir(f.dir, int m.offset, m.count);
+				reply(fd, ref Rmsg.Read(m.tag, data));
+			Write =>
+				error(fd, m, Eperm);				
+			Clunk =>
+				(f, e) = mapfid(m.fid);
+				if (e != nil) {
+					error(fd, m, e);
+					continue;
+				}
+				freefid(f);
+				reply(fd, ref Rmsg.Clunk(m.tag));
+			Stat =>
+				(f, e) = mapfid(m.fid);
+				if (e != nil) {
+					error(fd, m, e);
+					continue;
+				}
+				reply(fd, ref Rmsg.Stat(m.tag, f.dir.dir));
+			Remove =>
+				error(fd, m, Eperm);
+			Wstat =>
+				error(fd, m, Eperm);
+			Attach =>
+				f = newfid(m.fid);
+				if (f == nil) {
+					error(fd, m, Einuse);
+					continue;
+				}
+				f.dir = root;
+				reply(fd, ref Rmsg.Attach(m.tag, f.dir.dir.qid));
+			* =>
+				fatal("unknown styx message", 1);
+		}
+	}
+}
+
+newfid(fid : int) : ref Fid
+{
+	(f, nil) := mapfid(fid);
+	if(f != nil)
+		return nil;
+	f = ref Fid;
+	f.fid = fid;
+	f.open = 0;
+	hv := hashval(fid);
+	f.next = fidtab[hv];
+	fidtab[hv] = f;
+	return f;
+}
+
+freefid(f: ref Fid)
+{
+	hv := hashval(f.fid);
+	lf : ref Fid;
+	for(ff := fidtab[hv]; ff != nil; ff = ff.next){
+		if(f == ff){
+			if(lf == nil)
+				fidtab[hv] = ff.next;
+			else
+				lf.next = ff.next;
+			return;
+		}
+		lf = ff;
+	}
+	fatal("cannot find fid", 1);
+}
+	
+mapfid(fid : int) : (ref Fid, string)
+{
+	hv := hashval(fid);
+	for (f := fidtab[hv]; f != nil; f = f.next)
+		if (int f.fid == fid)
+			break;
+	if (f == nil)
+		return (nil, Ebadfid);
+	if (f.dir == nil)
+		return (nil, Enotfound);
+	return (f, nil);
+}
+
+hashval(n : int) : int
+{
+	return (n & ~Sys->DMDIR)%HTSZ;
+}
+
+readarch(f : string, args : list of string)
+{
+	ar := arch->openarchfs(f);
+	if(ar == nil || ar.b == nil)
+		fatal(sys->sprint("cannot open %s(%r)\n", f), 1);
+	bio = ar.b;
+	while ((a := arch->gethdr(ar)) != nil) {
+		if (args != nil) {
+			if (!selected(a.name, args)) {
+				if (skip)
+					return;
+				arch->drain(ar, int a.d.length);
+				continue;
+			}
+			mkdirs("/", a.name);
+		}
+		d := mkdir(a.name, a.d.mode, a.d.mtime, a.d.uid, a.d.gid, 0);
+		if((a.d.mode & Sys->DMDIR) == 0) {
+			d.dir.length = a.d.length;
+			d.offset = int bio.offset();
+		}
+		arch->drain(ar, int a.d.length);
+	}
+	if (ar.err != nil)
+		fatal(ar.err, 0);
+}
+
+selected(s: string, args: list of string): int
+{
+	for(; args != nil; args = tl args)
+		if(fileprefix(hd args, s))
+			return 1;
+	return 0;
+}
+
+fileprefix(prefix, s: string): int
+{
+	n := len prefix;
+	m := len s;
+	if(n > m || !str->prefix(prefix, s))
+		return 0;
+	if(m > n && s[n] != '/')
+		return 0;
+	return 1;
+}
+
+basename(f : string) : string
+{
+	for (i := len f; i > 0; ) 
+		if (f[--i] == '/')
+			return f[i+1:];
+	return f;
+}
+
+split(p : string) : (string, string)
+{
+	if (p == nil)
+		fatal("nil string in split", 1);
+	if (p[0] != '/')
+		fatal("p0 not / in split", 1);
+	while (p[0] == '/')
+		p = p[1:];
+	i := 0;
+	while (i < len p && p[i] != '/')
+		i++;
+	if (i == len p)
+		return (p, nil);
+	else
+		return (p[0:i], p[i:]);
+}
+
+mkdirs(basedir, name: string)
+{
+	(nil, names) := sys->tokenize(name, "/");
+	while(names != nil) {
+		# sys->print("mkdir %s\n", basedir);
+		mkdir(basedir, 8r775|Sys->DMDIR, daytime->now(), UID, GID, 1);
+		if(tl names == nil)
+			break;
+		basedir = basedir + "/" + hd names;
+		names = tl names;
+	}
+}
+
+readdir(d : ref Dir, offset : int, n : int) : array of byte
+{
+	if (d.dir.mode & Sys->DMDIR)
+		return readd(d, offset, n);
+	else
+		return readf(d, offset, n);
+}
+	
+readd(d : ref Dir, o : int, n : int) : array of byte
+{
+	k := 0;
+	m := 0;
+	b := array[n] of byte;
+	for (s := d.child; s != nil; s = s.sibling) {
+		l := styx->packdirsize(s.dir);
+		if(k < o){
+			k += l;
+			continue;
+		}
+		if(m+l > n)
+			break;
+		b[m: ] = styx->packdir(s.dir);
+		m += l;
+	}
+	return b[0: m];
+}
+
+readf(d : ref Dir, offset : int, n : int) : array of byte
+{
+	leng := int d.dir.length;
+	if (offset+n > leng)
+		n = leng-offset;
+	if (n <= 0 || offset < 0)
+		return nil;
+	bio.seek(big (d.offset+offset), Bufio->SEEKSTART);
+	a := array[n] of byte;
+	p := 0;
+	m := 0;
+	for ( ; n != 0; n -= m) {
+		l := len buf;
+		if (n < l)
+			l = n;
+		m = bio.read(buf, l);
+		if (m <= 0 || m != l)
+			fatal("premature eof", 1);
+		a[p:] = buf[0:m];
+		p += m;
+	}
+	return a;
+}
+
+mkdir(f : string, mode : int, mtime : int, uid : string, gid : string, existsok : int) : ref Dir
+{
+	if (f == "/")
+		return nil;
+	d := newdir(basename(f), uid, gid, mode, mtime);
+	addfile(d, f, existsok);
+	return d;
+}
+
+addfile(d : ref Dir, path : string, existsok : int)
+{
+	elem : string;
+
+	opath := path;
+	p := prev := root;
+	basedir := "";
+# sys->print("addfile %s : %s\n", d.dir.name, path);
+	while (path != nil) {
+		(elem, path) = split(path);
+		basedir += "/" + elem;
+		op := p;
+		p = lookup(p, elem);
+		if (path == nil) {
+			if (p != nil) {
+				if (!existsok && (p.dir.mode&Sys->DMDIR) == 0)
+					sys->fprint(sys->fildes(2), "addfile: %s already there", opath);
+					# fatal(sys->sprint("addfile: %s already there", opath), 1);
+				return;
+			}
+			if (prev.child == nil)
+				prev.child = d;
+			else {
+				for (s := prev.child; s.sibling != nil; s = s.sibling)
+					;
+				s.sibling = d;
+			}
+			d.parent = prev;
+		}
+		else {
+			if (p == nil) {
+				mkdir(basedir, 8r775|Sys->DMDIR, daytime->now(), UID, GID, 1);
+				p = lookup(op, elem);
+				if (p == nil)
+					fatal("bad file system", 1);
+			}
+		}
+		prev = p;
+	}
+}
+
+lookup(p : ref Dir, f : string) : ref Dir
+{
+	if ((p.dir.mode&Sys->DMDIR) == 0) 
+		fatal("not a directory in lookup", 1);
+	if (f == ".")
+		return p;
+	if (f == "..")
+		return p.parent;
+	for (d := p.child; d != nil; d = d.sibling)
+		if (d.dir.name == f)
+			return d;
+	return nil;
+}
+
+newdir(name, uid, gid : string, mode, mtime : int) : ref Dir
+{
+	dir : Sys->Dir;
+
+	dir.name = name;
+	dir.uid = uid;
+	dir.gid = gid;
+	dir.qid.path = big (qid++);
+	if(mode&Sys->DMDIR)
+		dir.qid.qtype = Sys->QTDIR;
+	else
+		dir.qid.qtype = Sys->QTFILE;
+	dir.qid.vers = 0;
+	dir.mode = mode;
+	dir.atime = dir.mtime = mtime;
+	dir.length = big 0;
+	dir.dtype = 'X';
+	dir.dev = 0;
+
+	d := ref Dir;
+	d.dir = dir;
+	d.offset = 0;
+	return d;
+}
+
+# pr(d : ref Dir)
+# {
+#	dir := d.dir;
+#	sys->print("%s %s %s %x %x %x %d %d %d %d %d %d\n",
+#		dir.name, dir.uid, dir.gid, dir.qid.path, dir.qid.vers, dir.mode, dir.atime, dir.mtime, dir.length, dir.dtype, dir.dev, d.offset);
+# }
+
+fatal(e : string, pr: int)
+{
+	if(pr){
+		sys->fprint(sys->fildes(2), "fatal: %s\n", e);
+		if (chanint != nil)
+			chanint <-= -1;
+	}
+	else{
+		# probably not an archive file
+		if (chanint != nil)
+			chanint <-= -2;
+	}
+	exit;
+}
--- /dev/null
+++ b/appl/cmd/install/archfs.m
@@ -1,0 +1,7 @@
+Archfs : module
+{
+	PATH : con "/dis/install/archfs.dis";
+
+	init : fn(ctxt : ref Draw->Context, args : list of string);
+	initc : fn(args : list of string, c : chan of int);
+};
--- /dev/null
+++ b/appl/cmd/install/ckproto.b
@@ -1,0 +1,267 @@
+implement Ckproto;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "arg.m";
+	arg: Arg;
+include "readdir.m";
+	readdir : Readdir;
+include "proto.m";
+	proto : Proto;
+include "protocaller.m";
+	protocaller : Protocaller;
+
+WARN, ERROR, FATAL : import Protocaller;
+
+Ckproto: module{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+	protofile: fn(new : string, old : string, d : ref Sys->Dir);
+	protoerr: fn(lev : int, line : int, err : string);
+};
+
+Dir : adt {
+	name : string;
+	proto : string;
+	parent : cyclic ref Dir;
+	child : cyclic ref Dir;
+	sibling : cyclic ref Dir;
+};
+
+root := "/";
+droot : ref Dir;
+protof : string;
+stderr : ref Sys->FD;
+omitgen := 0;			# forget generated files
+verbose : int;
+ckmode: int;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg = load Arg Arg->PATH;
+	readdir = load Readdir Readdir->PATH;
+	proto = load Proto Proto->PATH;
+	protocaller = load Protocaller "$self";
+
+	stderr = sys->fildes(2);
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS|Sys->FORKFD, nil);
+	arg->init(args);
+	while ((c := arg->opt()) != 0) {
+		case c {
+			'r' =>
+				root = arg->arg();
+				if (root == nil)
+					fatal("missing argument to -r");
+			'o' =>
+				omitgen = 1;
+			'v' =>
+				verbose = 1;
+			'm' =>
+				ckmode = 1;
+			* =>
+				fatal("usage: install/ckproto [-o] [-v] [-m] [-r root] protofile ....");
+		}
+	}
+	droot = ref Dir("/", nil, nil, nil, nil);
+	droot.parent = droot;
+	args = arg->argv();
+	while (args != nil) {
+		protof = hd args;
+		proto->rdproto(hd args, root, protocaller);
+		args = tl args;
+	}
+	if (verbose)
+		prtree(droot, -1);
+	ckdir(root, droot);
+}
+
+protofile(new : string, old : string, nil : ref Sys->Dir)
+{
+	if (verbose) {
+		if (old == new)
+			sys->print("%s\n", new);
+		else
+	 		sys->print("%s %s\n", new, old);
+	}
+	addfile(droot, old);
+	if (new != old)
+		addfile(droot, new);
+}
+
+protoerr(lev : int, line : int, err : string)
+{
+	s := "line " + string line + " : " + err;
+	case lev {
+		WARN => warn(s);
+		ERROR => error(s);
+		FATAL => fatal(s);
+	}
+}
+
+ckdir(d : string, dird : ref Dir)
+{
+	(dir, n) := readdir->init(d, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		dire := lookup(dird, dir[i].name);
+		if(omitgen && generated(dir[i].name))
+			continue;
+		if (dire == nil){
+			sys->print("%s missing\n", mkpath(d, dir[i].name));
+			continue;
+		}
+		if(ckmode){
+			if(dir[i].mode & Sys->DMDIR){
+				if((dir[i].mode & 8r775) != 8r775)
+					sys->print("directory %s not 775 at least\n", mkpath(d, dir[i].name));
+			}
+			else{
+				if((dir[i].mode & 8r664) != 8r664)
+					sys->print("file %s not 664 at least\n", mkpath(d, dir[i].name));
+			}
+		}
+		if (dir[i].mode & Sys->DMDIR)
+			ckdir(mkpath(d, dir[i].name), dire);
+	}
+}
+
+addfile(root : ref Dir, path : string)
+{
+	elem : string;
+
+	# ckexists(path);
+	
+	curd := root;
+	opath := path;
+	while (path != nil) {
+		(elem, path) = split(path);
+		d := lookup(curd, elem);
+		if (d == nil) {
+			d = ref Dir(elem, protof, curd, nil, nil);
+			if (curd.child == nil)
+				curd.child = d;
+			else {
+				prev, this : ref Dir;
+
+				for (this = curd.child; this != nil; this = this.sibling) {
+					if (elem < this.name) {
+						d.sibling = this;
+						if (prev == nil)
+							curd.child = d;
+						else
+							prev.sibling = d;
+						break;
+					}
+					prev = this;
+				}
+				if (this == nil)
+					prev.sibling = d;
+			}
+		}
+		else if (path == nil && d.proto == protof)
+			sys->print("%s repeated in proto %s\n", opath, protof);
+		curd = d;
+	}
+}
+
+lookup(p : ref Dir, f : string) : ref Dir
+{
+	if (f == ".")
+		return p;
+	if (f == "..")
+		return p.parent;
+	for (d := p.child; d != nil; d = d.sibling) {
+		if (d.name == f)
+			return d;
+		if (d.name > f)
+			return nil;
+	}
+	return nil;
+}
+
+prtree(root : ref Dir, indent : int)
+{
+	if (indent >= 0)
+		sys->print("%s%s\n", string array[indent] of { * => byte '\t' }, root.name);
+	for (s := root.child; s != nil; s = s.sibling)
+		prtree(s, indent+1);
+}
+
+mkpath(prefix, elem: string): string
+{
+	slash1 := slash2 := 0;
+	if (len prefix > 0)
+		slash1 = prefix[len prefix - 1] == '/';
+	if (len elem > 0)
+		slash2 = elem[0] == '/';
+	if (slash1 && slash2)
+		return prefix+elem[1:];
+	if (!slash1 && !slash2)
+		return prefix+"/"+elem;
+	return prefix+elem;
+}
+
+split(p : string) : (string, string)
+{
+	if (p == nil)
+		fatal("nil string in split");
+	if (p[0] != '/')
+		fatal("p0 notg / in split");
+	while (p[0] == '/')
+		p = p[1:];
+	i := 0;
+	while (i < len p && p[i] != '/')
+		i++;
+	if (i == len p)
+		return (p, nil);
+	else
+		return (p[0:i], p[i:]);
+}
+
+
+gens := array[] of {
+	"dis", "sbl", "out", "0", "1", "2", "5", "8", "k", "q", "v", "t"
+};
+
+generated(f : string) : int
+{
+	for (i := len f -1; i >= 0; i--)
+		if (f[i] == '.')
+			break;
+	if (i < 0)
+		return 0;
+	suff := f[i+1:];
+	for (i = 0; i < len gens; i++)
+		if (suff == gens[i])
+			return 1;
+	return 0;
+}
+
+warn(s: string)
+{
+	sys->print("%s: %s\n", protof, s);
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "%s: %s\n", protof, s);
+	exit;;
+}
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "fatal: %s\n", s);
+	exit;
+}
+
+ckexists(path: string)
+{
+	s := mkpath(root, path);
+	(ok, nil) := sys->stat(s);
+	if(ok < 0)
+		sys->print("%s does not exist\n", s);
+}
--- /dev/null
+++ b/appl/cmd/install/create.b
@@ -1,0 +1,445 @@
+implement Create;
+
+include "sys.m";
+	sys: Sys;
+	Dir, sprint, fprint: import sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "arg.m";
+	arg: Arg;
+include "daytime.m";
+include "keyring.m";
+	keyring : Keyring;
+include "sh.m";
+include "wrap.m";
+	wrap : Wrap;
+include "arch.m";
+	arch : Arch;
+include "proto.m";
+	proto : Proto;
+include "protocaller.m";
+	protocaller : Protocaller;
+
+WARN, ERROR, FATAL : import Protocaller;
+
+Create: module{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+	protofile: fn(new : string, old : string, d : ref Sys->Dir);
+	protoerr: fn(lev : int, line : int, err : string);
+};
+
+bout: ref Iobuf;			# stdout when writing archive
+protof: string;
+notesf: string;
+oldroot: string;
+buf: array of byte;
+buflen := 1024-8;
+verb: int;
+xflag: int;
+stderr: ref Sys->FD;
+uid, gid : string;
+desc : string;
+pass : int;
+update : int;
+md5s : ref Keyring->DigestState;
+w : ref Wrap->Wrapped;
+root := "/";
+prefix, notprefix: list of string;
+onlist: list of (string, string);	# NEW
+remfile: string;
+
+n2o(n: string): string
+{
+	for(onl := onlist; onl != nil; onl = tl onl)
+		if((hd onl).t1 == n)
+			return (hd onl).t0;
+	return n;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	arg = load Arg Arg->PATH;
+	wrap = load Wrap Wrap->PATH;
+	wrap->init(bufio);
+	arch = load Arch Arch->PATH;
+	arch->init(bufio);
+	daytime := load Daytime Daytime->PATH;
+	now := daytime->now();
+	# {
+	#	for(i := 0; i < 21; i++){
+	#		n := now+(i-9)*100000000;
+	#		sys->print("%d	->	%s\n", n, wrap->now2string(n));
+	#		if(wrap->string2now(wrap->now2string(n)) != n)
+	#			sys->print("%d wrong\n", n);
+	#	}
+	# }
+	daytime = nil;
+	proto = load Proto Proto->PATH;
+	protocaller = load Protocaller "$self";
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS|Sys->FORKFD, nil);
+	stderr = sys->fildes(2);
+	if(arg == nil)
+		error(sys->sprint("can't load %s: %r", Arg->PATH));
+	name := "";
+	desc = "inferno";
+	tostdout := 0;
+	not := 0;
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		'n' =>
+			not = 1;
+		'o' =>
+			tostdout = 1;
+		'p' =>
+			protof = reqarg("proto file (-p)");
+		'r' => 
+			root = reqarg("root directory (-r)");
+		's' =>
+			oldroot = reqarg("source directory (-d)");
+		'u' =>
+			update = 1;
+		'v' =>
+			verb = 1;
+		'x' =>
+			xflag = 1;
+		'N' =>
+			uid = reqarg("user name (-U)");
+		'G' =>
+			gid = reqarg("group name (-G)");
+		'd' or 'D' =>
+			desc = reqarg("product description (-D)");
+		't' =>
+			rt := reqarg("package time (-t)");
+			now = int rt;
+		'i' =>
+			notesf = reqarg("file (-i)");
+		'R' =>
+			remfile = reqarg("remove file (-R)");
+		'P' =>
+			arch->addperms(0);
+		* =>
+			usage();
+		}
+
+	args = arg->argv();
+	if(args == nil)
+		usage();
+	if (tostdout || xflag) {
+		bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+		if(bout == nil)
+			error(sys->sprint("can't open standard output for archive: %r"));
+	}
+	else {
+		# ar := sys->sprint("%ud", now);
+		ar := wrap->now2string(now, 0);
+		bout = bufio->create(ar, Sys->OWRITE, 8r664);
+		if(bout == nil)
+			error(sys->sprint("can't create %s for archive: %r", ar));
+		sys->print("archiving package %s to %s\n", hd args, ar);
+	}
+	buf = array [buflen] of byte;
+	name = hd args;
+	if(update){
+		if(not)
+			notprefix = tl args;
+		else
+			prefix = tl args;
+	}
+	else if (tl args != nil)
+		fatal("only one name allowed");
+	if (!xflag)
+		digest := wrapinit(name, now);
+	fprint(stderr, "processing %s\n", protof);
+	proto->rdproto(protof, oldroot, protocaller);
+	if (!xflag)
+		wrapend(digest);
+	if (!xflag)
+		fprint(stderr, "file system made\n");
+	arch->putend(bout);
+	exits();
+}
+
+protofile(new : string, old : string, d : ref Sys->Dir)
+{
+	if(xflag && bout != nil){
+		bout.puts(sys->sprint("%s\t%d\t%bd\n", new, d.mtime, d.length));
+		return;
+	}
+	d.uid = uid;
+	d.gid = gid;
+	if (!(d.mode & Sys->DMDIR)) {
+		# if(verb)
+		#	fprint(stderr, "%s\n", new);
+		f := sys->open(old, Sys->OREAD);
+		if(f == nil){
+			warn(sys->sprint("can't open %s: %r", old));
+			return;
+		}
+	}
+	mkarch(new, old, d);
+}
+
+protoerr(lev : int, line : int, err : string)
+{
+	s := "line " + string line + " : " + err;
+	case lev {
+		WARN => warn(s);
+		ERROR => error(s);
+		FATAL => fatal(s);
+	}
+}
+
+quit()
+{
+	if(bout != nil)
+		bout.flush();
+	exits();
+}
+
+reqarg(what: string): string
+{
+	if((o := arg->arg()) == nil){
+		sys->fprint(stderr, "missing %s\n", what);
+		exits();
+	}
+	return o;
+}
+
+puthdr(f : string, d: ref Dir)
+{
+	if (d.mode & Sys->DMDIR)
+		d.length = big 0;
+	arch->puthdr(bout, f, d);
+}
+
+error(s: string)
+{
+	fprint(stderr, "%s: %s\n", protof, s);
+	quit();
+}
+
+fatal(s: string)
+{
+	fprint(stderr, "fatal: %s\n", s);
+	exits();
+}
+ 
+warn(s: string)
+{
+	fprint(stderr, "%s: %s\n", protof, s);
+}
+	
+usage()
+{
+	fprint(stderr, "usage: install/create [-ovx] [-N uid] [-G gid] [-r root] [-d desc] [-s src-fs] [-p proto] name\n");
+	fprint(stderr, "or install/create -u [-ovx] [-N uid] [-G gid] [-r root] [-d desc] [-s src-fs] [-p proto] old-package [prefix ...]\n");
+	exits();
+}
+
+wrapinit(name : string, t : int) : array of byte
+{
+	rmfile : string;
+	rmfd: ref Sys->FD;
+
+	if (uid == nil)
+		uid = "inferno";
+	if (gid == nil)
+		gid = "inferno";
+	if (update) {
+		w = wrap->openwraphdr(name, root, nil, 0);
+		if (w == nil)
+			fatal("no such package found");
+		# ignore any updates - NEW commented out
+		# while (w.nu > 0 && w.u[w.nu-1].typ == wrap->UPD)
+		#	w.nu--;
+
+		# w.nu = 1;	NEW commented out
+		if (protof == nil)
+			protof = w.u[0].dir + "/proto";
+		name = w.name;
+	}
+	else {
+		if (protof == nil)
+			fatal("proto file missing");
+	}
+	(md5file, md5fd) := opentemp("wrap.md5", t);
+	if (md5fd == nil)
+		fatal(sys->sprint("cannot create %s", md5file));
+	keyring = load Keyring Keyring->PATH;
+	md5s = keyring->md5(nil, 0, nil, nil);
+	md5b := bufio->fopen(md5fd, Bufio->OWRITE);
+	if (md5b == nil)
+		fatal(sys->sprint("cannot open %s", md5file));
+	fprint(stderr, "wrap pass %s\n", protof);
+	obout := bout;
+	bout = md5b;
+	pass = 0;
+	proto->rdproto(protof, oldroot, protocaller);
+	bout.flush();
+	bout = md5b = nil;
+	digest := array[keyring->MD5dlen] of { * => byte 0 };
+	keyring->md5(nil, 0, digest, md5s);
+	md5s = nil;
+	(md5sort, md5sfd) := opentemp("wrap.md5s", t);
+	if (md5sfd == nil)
+		fatal(sys->sprint("cannot create %s", md5sort));
+	endc := chan of int;
+	md5fd = nil;	# close md5file
+	spawn fsort(md5sfd, md5file, endc);
+	md5sfd = nil;
+	res := <- endc;
+	if (res < 0)
+		fatal("sort failed");
+	if (update) {
+		(rmfile, rmfd) = opentemp("wrap.rm", t);
+		if (rmfd == nil)
+			fatal(sys->sprint("cannot create %s", rmfile));
+		rmed: list of string;
+		for(i := w.nu-1; i >= 0; i--){	# NEW does loop
+			w.u[i].bmd5.seek(big 0, Bufio->SEEKSTART);
+			while ((p := w.u[i].bmd5.gets('\n')) != nil) {
+				if(prefix != nil && !wrap->match(p, prefix))
+					continue;
+				if(notprefix != nil && !wrap->notmatch(p, notprefix))
+					continue;
+				(q, nil) := str->splitl(p, " ");
+				q = pathcat(root, q);
+				(ok, nil) := sys->stat(q);
+				if(ok < 0)
+					(ok, nil) = sys->stat(n2o(q));
+				if (len q >= 7 && q[len q - 7:] == "emu.new")	# quick hack for now
+					continue;
+				if (ok < 0){
+					for(r := rmed; r != nil; r = tl r)	# NEW to avoid duplication
+						if(hd r == q)
+							break;
+					if(r == nil){
+						# sys->fprint(rmfd, "%s\n", q);
+						rmed = q :: rmed;
+					}
+				}
+			}
+		}
+		for(r := rmed; r != nil; r = tl r)
+			sys->fprint(rmfd, "%s\n", hd r);
+		if(remfile != nil){
+			rfd := sys->open(remfile, Sys->OREAD);
+			rbuf := array[128] of byte;
+			for(;;){
+				n := sys->read(rfd, rbuf, 128);
+				if(n <= 0)
+					break;
+				sys->write(rmfd, rbuf, n);
+			}
+		}
+		rmfd = nil;
+		rmed = nil;
+	}
+	bout = obout;
+	if (update)
+		wrap->putwrap(bout, name, t, desc, w.tfull, prefix == nil && notprefix == nil, uid, gid);
+	else
+		wrap->putwrap(bout, name, t, desc, 0, 1, uid, gid);
+	wrap->putwrapfile(bout, name, t, "proto", protof, uid, gid);
+	wrap->putwrapfile(bout, name, t, "md5sum", md5sort, uid, gid);
+	if (update)
+		wrap->putwrapfile(bout, name, t, "remove", rmfile, uid, gid);
+	if(notesf != nil)
+		wrap->putwrapfile(bout, name, t, "notes", notesf, uid, gid);
+	md5s = keyring->md5(nil, 0, nil, nil);
+	pass = 1;
+	return digest;
+}
+
+wrapend(digest : array of byte)
+{
+	digest0 := array[keyring->MD5dlen] of { * => byte 0 };
+	keyring->md5(nil, 0, digest0, md5s);
+	md5s = nil;
+	if (wrap->memcmp(digest, digest0, keyring->MD5dlen) != 0)
+		warn(sys->sprint("files changed underfoot %s %s", wrap->md5conv(digest), wrap->md5conv(digest0)));
+}
+
+mkarch(new : string, old : string, d : ref Dir)
+{
+	if(pass == 0 && old != new)
+		onlist = (old, new) :: onlist;
+	if(prefix != nil && !wrap->match(new, prefix))
+		return;
+	if(notprefix != nil && !wrap->notmatch(new, notprefix))
+		return;
+	digest := array[keyring->MD5dlen] of { * => byte 0 };
+	wrap->md5file(old, digest);
+	(ok, nil) := wrap->getfileinfo(w, new, digest, nil, nil);
+	if (ok >= 0)
+		return;
+	n := array of byte new;
+	keyring->md5(n, len n, nil, md5s);
+	if (pass == 0) {
+		bout.puts(sys->sprint("%s %s\n", new, wrap->md5conv(digest)));
+		return;
+	}
+	if(verb)
+		fprint(stderr, "%s\n", new);
+	puthdr(new, d);
+	if(!(d.mode & Sys->DMDIR)) {
+		err := arch->putfile(bout, old, int d.length);
+		if (err != nil)
+			warn(err);
+	}
+}
+
+fsort(fd : ref Sys->FD, file : string, c : chan of int)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(fd.fd, 1);
+	cmd := "/dis/sort.dis";
+	m := load Command cmd;
+	if(m == nil) {
+		c <-= -1;
+		return;
+	}
+	m->init(nil, cmd :: file :: nil);
+	c <-= 0;
+}
+
+tmpfiles: list of string;
+
+opentemp(prefix: string, t: int): (string, ref Sys->FD)
+{
+	name := sys->sprint("/tmp/%s.%ud.%d", prefix, t, sys->pctl(0, nil));
+	fd := sys->create(name, Sys->ORDWR, 8r666);
+	# fd := sys->create(name, Sys->ORDWR | Sys->ORCLOSE, 8r666); not on Nt
+	tmpfiles = name :: tmpfiles;
+	return (name, fd);
+}
+
+exits()
+{
+	wrap->end();
+	for( ; tmpfiles != nil; tmpfiles = tl tmpfiles)
+		sys->remove(hd tmpfiles);
+	exit;
+}
+
+pathcat(s : string, t : string) : string
+{
+	if (s == nil) return t;
+	if (t == nil) return s;
+	slashs := s[len s - 1] == '/';
+	slasht := t[0] == '/';
+	if (slashs && slasht)
+		return s + t[1:];
+	if (!slashs && !slasht)
+		return s + "/" + t;
+	return s + t;
+}
--- /dev/null
+++ b/appl/cmd/install/eproto.b
@@ -1,0 +1,357 @@
+implement Fsmodule;
+include "sys.m";
+	sys: Sys;
+include "readdir.m";
+	readdir: Readdir;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, report, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+File: adt {
+	name: string;
+	mode: int;
+	owner: string;
+	group: string;
+	old: string;
+	flags: int;
+	sub: cyclic array of ref File;
+};
+
+Proto: adt {
+	indent: int;
+	lastline: string;
+	iob: ref Iobuf;
+};
+
+Star, Plus: con 1<<iota;
+
+types(): string
+{
+	return "ts-rs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: eproto: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmod(Readdir->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmod(Bufio->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		badmod(String->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	protofile := (hd args).s().i;
+	rootpath: string;
+	if(opts != nil)
+		rootpath = (hd (hd opts).args).s().i;
+	if(rootpath == nil)
+		rootpath = "/";
+
+	proto := ref Proto(0, nil, nil);
+	if((proto.iob = bufio->open(protofile, Sys->OREAD)) == nil){
+		sys->fprint(sys->fildes(2), "fs: eproto: cannot open %q: %r\n", protofile);
+		return nil;
+	}
+	root := ref File(rootpath, ~0, nil, nil, nil, 0, nil);
+	(root.flags, root.sub) = readproto(proto, -1);
+	c := Entrychan(chan of int, chan of Entry);
+	spawn protowalk(c, root, report.start("proto"));
+	return ref Value.T(c);
+}
+
+protowalk(c: Entrychan, root: ref File, errorc: chan of string)
+{
+	if(<-c.sync == 0){
+		quit(errorc);
+		exit;
+	}
+	protowalk1(c, root.flags, root.name, file2dir(root, nil), root.sub, -1, errorc);
+	c.c <-= (nil, nil, 0);
+	quit(errorc);
+}
+
+protowalk1(c: Entrychan, flags: int, path: string, d: ref Sys->Dir,
+		sub: array of ref File, depth: int, errorc: chan of string): int
+{
+	if(depth >= 0)
+		c.c <-= (d, path, depth);
+	depth++;
+	(a, n) := readdir->init(path, Readdir->NAME|Readdir->COMPACT);
+	j := 0;
+	prevsub: string;
+	for(i := 0; i < n; i++){
+		for(; j < len sub; j++){
+			s := sub[j].name;
+			if(s == prevsub){
+				report(errorc, sys->sprint("duplicate entry %s", pathconcat(path, s)));
+				continue;			# eliminate duplicates in proto
+			}
+			if(s >= a[i].name || sub[j].old != nil)
+				break;
+			report(errorc, sys->sprint("%s not found", pathconcat(path, s)));
+		}
+		foundsub := j < len sub && (sub[j].name == a[i].name || sub[j].old != nil);
+		if(foundsub || flags&Plus ||
+				(flags&Star && (a[i].mode & Sys->DMDIR)==0)){
+			f: ref File;
+			if(foundsub){
+				f = sub[j++];
+				prevsub = f.name;
+			}
+			p: string;
+			d: ref Sys->Dir;
+			if(foundsub && f.old != nil){
+				p = f.old;
+				(ok, xd) := sys->stat(p);
+				if(ok == -1){
+					report(errorc, sys->sprint("cannot stat %q: %r", p));
+					continue;
+				}
+				d = ref xd;
+			}else{
+				p = pathconcat(path, a[i].name);
+				d = a[i];
+			}
+
+			d = file2dir(f, d);
+			r: int;
+			if((d.mode & Sys->DMDIR) == 0)
+				r = walkfile(c, p, d, depth, errorc);
+			else if(flags & Plus)
+				r = protowalk1(c, Plus, p, d, nil, depth, errorc);
+			else
+				r = protowalk1(c, f.flags, p, d, f.sub, depth, errorc);
+			if(r == Skip)
+				return Next;
+		}
+	}
+	return Next;
+}
+
+pathconcat(p, name: string): string
+{		
+	if(p != nil && p[len p - 1] != '/')
+		p[len p] = '/';
+	return p+name;
+}
+
+# from(ish) walk.b
+walkfile(c: Entrychan, path: string, d: ref Sys->Dir, depth: int, errorc: chan of string): int
+{
+	fd := sys->open(path, Sys->OREAD);
+	if(fd == nil)
+		report(errorc, sys->sprint("cannot open %q: %r", path));
+	else
+		c.c <-= (d, path, depth);
+	return Next;
+}
+
+readproto(proto: ref Proto, indent: int): (int, array of ref File)
+{
+	a := array[10] of ref File;
+	n := 0;
+	flags := 0;
+	while((f := readline(proto, indent)) != nil){
+		if(f.name == "*")
+			flags |= Star;
+		else if(f.name == "+")
+			flags |= Plus;
+		else{
+			(f.flags, f.sub) = readproto(proto, proto.indent);
+			if(n == len a)
+				a = (array[n * 2] of ref File)[0:] = a;
+			a[n++] = f;
+		}
+	}
+	if(n < len a)
+		a = (array[n] of ref File)[0:] = a[0:n];
+	mergesort(a, array[n] of ref File);
+	return (flags, a);
+}
+
+readline(proto: ref Proto, indent: int): ref File
+{
+	s: string;
+	if(proto.lastline != nil){
+		s = proto.lastline;
+		proto.lastline = nil;
+	}else if(proto.indent == -1)
+		return nil;
+	else if((s = proto.iob.gets('\n')) == nil){
+		proto.indent = -1;
+		return nil;
+	}
+	spc := 0;
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c == ' ')
+			spc++;
+		else if(c == '\t')
+			spc += 8;
+		else
+			break;
+	}
+	if(i == len s || s[i] == '#' || s[i] == '\n')
+		return readline(proto, indent);	# XXX sort out tail recursion!
+	if(spc <= indent){
+		proto.lastline = s;
+		return nil;
+	}
+	proto.indent = spc;
+	(nil, toks) := sys->tokenize(s, " \t\n");
+	f := ref File(nil, ~0, nil, nil, nil, 0, nil);
+	(f.name, toks) = (getname(hd toks, 0), tl toks);
+	if(toks == nil)
+		return f;
+	(f.mode, toks) = (getmode(hd toks), tl toks);
+	if(toks == nil)
+		return f;
+	(f.owner, toks) = (getname(hd toks, 1), tl toks);
+	if(toks == nil)
+		return f;
+	(f.group, toks) = (getname(hd toks, 1), tl toks);
+	if(toks == nil)
+		return f;
+	(f.old, toks) = (hd toks, tl toks);
+	return f;
+}
+
+mergesort(a, b: array of ref File)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m]);
+		mergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if(b[i].name > b[j].name)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+getname(s: string, allowminus: int): string
+{
+	if(s == nil)
+		return nil;
+	if(allowminus && s == "-")
+		return nil;
+	if(s[0] == '$'){
+		s = getenv(s[1:]);
+		if(s == nil)
+			;	# TO DO: w.warn(sys->sprint("can't read environment variable %s", s));
+		return s;
+	}
+	return s;
+}
+
+getenv(s: string): string
+{
+	if(s == "user")
+		return readfile("/dev/user");	# more accurate?
+	return readfile("/env/"+s);
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd != nil){
+		a := array[256] of byte;
+		n := sys->read(fd, a, len a);
+		if(n > 0)
+			return string a[0:n];
+	}
+	return nil;
+}
+
+getmode(s: string): int
+{
+	s = getname(s, 1);
+	if(s == nil)
+		return ~0;
+	m := 0;
+	i := 0;
+	if(s[i] == 'd'){
+		m |= Sys->DMDIR;
+		i++;
+	}
+	if(i < len s && s[i] == 'a'){
+		m |= Sys->DMAPPEND;
+		i++;
+	}
+	if(i < len s && s[i] == 'l'){
+		m |= Sys->DMEXCL;
+		i++;
+	}
+	(xmode, t) := str->toint(s, 8);
+	if(t != nil){
+		# report(aux.errorc, "bad mode specification %q", s);
+		return ~0;
+	}
+	return xmode | m;
+}
+
+file2dir(f: ref File, old: ref Sys->Dir): ref Sys->Dir
+{
+	d := ref Sys->nulldir;
+	if(old != nil){
+		if(old.dtype != 'M'){
+			d.uid = "sys";
+			d.gid = "sys";
+			xmode := (old.mode >> 6) & 7;
+			d.mode = old.mode | xmode | (xmode << 3);
+		}else{
+			d.uid = old.uid;
+			d.gid = old.gid;
+			d.mode = old.mode;
+		}
+		d.length = old.length;
+		d.mtime = old.mtime;
+		d.atime = old.atime;
+		d.muid = old.muid;
+		d.name = old.name;
+	}
+	if(f != nil){
+		d.name = f.name;
+		if(f.owner != nil)
+			d.uid = f.owner;
+		if(f.group != nil)
+			d.gid = f.group;
+		if(f.mode != ~0)
+			d.mode = f.mode;
+	}
+	return d;
+}
--- /dev/null
+++ b/appl/cmd/install/info.b
@@ -1,0 +1,73 @@
+implement Info;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "daytime.m";
+	daytime: Daytime;
+include "arg.m";
+	arg: Arg;
+include "wrap.m";
+	wrap : Wrap;
+
+Info: module{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+root : string;
+
+TYPLEN : con 4;
+typestr := array[TYPLEN] of { "???", "package", "update", "full update" };
+
+fatal(err : string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", err);
+	raise "fail:error";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	arg = load Arg Arg->PATH;
+	wrap = load Wrap Wrap->PATH;
+	wrap->init(bufio);
+
+	arg->init(args);
+	while ((c := arg->opt()) != 0) {
+		case c {
+			'r' =>
+				root = arg->arg();
+				if (root == nil)
+					fatal("missing root name");
+			* =>
+				fatal(sys->sprint("bad argument -%c", c));
+		}
+	}
+	args = arg->argv();
+	if (args == nil || tl args != nil)
+		fatal("usage: install/info [-r root] package");
+	w := wrap->openwraphdr(hd args, root, nil, 0);
+	if (w == nil)
+		fatal("no such package found");
+	tm := daytime->text(daytime->local(w.tfull));
+	sys->print("%s (complete as of %s)\n", w.name, tm[0:28]);
+	for (i := w.nu; --i >= 0;) {
+		typ := w.u[i].typ;
+		if (typ < 0 || typ >= TYPLEN)
+			sys->print("%s", typestr[0]);
+		else
+			sys->print("%s", typestr[typ]);
+		sys->print(" %s", wrap->now2string(w.u[i].time, 0));
+		if (typ & wrap->UPD)
+			sys->print(" updating %s", wrap->now2string(w.u[i].utime, 0));
+		if (w.u[i].desc != nil)
+			sys->print(": %s", w.u[i].desc);
+		sys->print("\n");
+	}
+	wrap->end();
+}
--- /dev/null
+++ b/appl/cmd/install/inst.b
@@ -1,0 +1,500 @@
+implement Inst;
+
+include "sys.m";
+	sys: Sys;
+	Dir, sprint, fprint: import sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "arg.m";
+	arg: Arg;
+include "keyring.m";
+	keyring : Keyring;
+include "arch.m";
+	arch : Arch;
+include "wrap.m";
+	wrap : Wrap;
+
+Inst: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+LEN: con Sys->ATOMICIO;
+
+tflag := 0;
+uflag := 0;
+hflag := 0;
+vflag := 0;
+fflag := 1;
+stderr: ref Sys->FD;
+bout: ref Iobuf;
+argv0 := "inst";
+oldw, w : ref Wrap->Wrapped;
+root := "/";
+force := 0;
+stoponerr := 1;
+
+# membogus(argv: list of string)
+# {
+#
+# }
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		error(sys->sprint("cannot load %s: %r\n", Bufio->PATH));
+
+	str = load String String->PATH;
+	if(str == nil)
+		error(sys->sprint("cannot load %s: %r\n", String->PATH));
+
+	arg = load Arg Arg->PATH;
+	if(arg == nil)
+		error(sys->sprint("cannot load %s: %r\n", Arg->PATH));
+	keyring = load Keyring Keyring->PATH;
+	if(keyring == nil)
+		error(sys->sprint("cannot load %s: %r\n", Keyring->PATH));
+	arch = load Arch Arch->PATH;
+	if(arch == nil)
+		error(sys->sprint("cannot load %s: %r\n", Arch->PATH));
+	arch->init(bufio);
+	wrap = load Wrap Wrap->PATH;
+	if(wrap == nil)
+		error(sys->sprint("cannot load %s: %r\n", Wrap->PATH));
+	wrap->init(bufio);
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		'f' =>
+			fflag = 0;
+		'h' =>
+			hflag = 1;
+			bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+			if(bout == nil)
+				error(sys->sprint("can't access standard output: %r"));
+		't' =>
+			tflag = 1;
+		'u' =>
+			uflag = 1;
+		'v' =>
+			vflag = 1;
+		'r' =>
+			root = arg->arg();
+			if (root == nil)
+				fatal("root missing");
+		'F' =>
+			force = 1;
+		'c' =>
+			stoponerr = 0;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	if (args == nil)
+		usage();
+	ar := arch->openarch(hd args);
+	if(ar == nil || ar.b == nil)
+		error(sys->sprint("can't access %s: %r", hd args));
+	w = wrap->openwraphdr(hd args, root, nil, 0);
+	if (w == nil)
+		fatal("no such package found");
+	if(w.nu != 1)
+		fatal("strange package: more than one piece");
+	if (force == 0)
+		oldw = wrap->openwrap(w.name, root, 0);
+	if (force == 0 && w.u[0].utime && (oldw == nil || oldw.tfull < w.u[0].utime)){
+		tfull: int;
+		if(oldw == nil)
+			tfull = -1;
+		else
+			tfull = oldw.tfull;
+		fatal(sys->sprint("need %s version of %s already installed (pkg %d)", wrap->now2string(w.u[0].utime, 0), w.name, tfull));
+	}
+	args = tl args;
+	digest := array[Keyring->MD5dlen] of byte;
+	digest0 := array[Keyring->MD5dlen] of byte;
+	digest1 := array[Keyring->MD5dlen] of byte;
+
+	while ((a := arch->gethdr(ar)) != nil) {
+		why := "";
+		docopy := 0;
+		if(force)
+			docopy = 1;
+		else if(a.d.mode & Sys->DMDIR)
+			docopy = 1;
+		else if(wrap->md5file(root+a.name, digest) < 0)
+			docopy = 1;
+		else{
+			wrap->md5filea(root+a.name, digest1);
+			(ok, t) := wrap->getfileinfo(oldw, a.name, digest, nil, digest1);
+			if (ok >= 0) {
+				if(t > w.u[0].time){
+					docopy = 0;
+					why = "version from newer package exists";
+				}
+				else
+					docopy = 1;
+			}
+			else {
+				(ok, t) = wrap->getfileinfo(oldw, a.name, nil, nil, nil);
+				if(ok >= 0){
+					docopy = 0;
+					why = "locally modified";
+				}
+				else{
+					docopy = 0;
+					why = "locally created";
+				}
+			}
+		}
+		if(!docopy){
+			wrap->md5sum(ar.b, digest0, int a.d.length);
+			if(wrap->memcmp(digest, digest0, Keyring->MD5dlen))
+				skipfile(a.name, why);
+			continue;
+		}
+		if(args != nil){
+			if(!selected(a.name, args)){
+				arch->drain(ar, int a.d.length);
+				continue;
+			}
+			if (!hflag)
+				mkdirs(root, a.name);
+		}
+		name := pathcat(root, a.name);
+		if(hflag){
+			bout.puts(sys->sprint("%s %uo %s %s %ud %d\n",
+				name, a.d.mode, a.d.uid, a.d.gid, a.d.mtime, int a.d.length));
+			arch->drain(ar, int a.d.length);
+			continue;
+		}
+		if(a.d.mode & Sys->DMDIR)
+			mkdir(name, a.d);
+		else
+			extract(ar, name, a.d);
+	}
+	arch->closearch(ar);
+	if(ar.err == nil){
+		# fprint(stderr, "done\n");
+		quit(nil);
+	}
+	else {
+		fprint(stderr, "%s\n", ar.err);
+		quit("eof");
+	}
+}
+
+skipfile(f : string, why : string)
+{
+	sys->fprint(stderr, "skipping %s: %s\n", f, why);
+}
+
+skiprmfile(f: string, why: string)
+{
+	sys->fprint(stderr, "not removing %s: %s\n", f, why);
+}
+
+doremove(s : string)
+{
+	p := pathcat(root, s);
+	digest := array[Keyring->MD5dlen] of { * => byte 0 };
+	digest1 := array[Keyring->MD5dlen] of { * => byte 0 };
+	if(wrap->md5file(p, digest) < 0)
+		;
+	else{
+		wrap->md5filea(p, digest1);
+		(ok, nil) := wrap->getfileinfo(oldw, s, digest, nil, digest1);
+		if(force == 0 && ok < 0)
+			skiprmfile(p, "locally modified");
+		else{
+			if (vflag)
+				sys->print("rm %s\n", p);
+			remove(p);
+		}
+	}
+}
+
+quit(s: string)
+{
+	if (s == nil) {
+		p := w.u[0].dir + "/remove";
+		if ((b := bufio->open(p, Bufio->OREAD)) != nil) {
+			while ((t := b.gets('\n')) != nil) {
+				lt := len t;
+				if (t[lt-1] == '\n')
+					t = t[0:lt-1];
+				doremove(t);
+			}
+		}
+	}
+	if(bout != nil)
+		bout.flush();
+	if(wrap != nil)
+		wrap->end();
+	if(s != nil)
+		raise "fail: "+s;
+	else
+		fprint(stderr, "done\n");
+	exit;
+}
+
+fileprefix(prefix, s: string): int
+{
+	n := len prefix;
+	m := len s;
+	if(n > m || !str->prefix(prefix, s))
+		return 0;
+	if(m > n && s[n] != '/')
+		return 0;
+	return 1;
+}
+
+selected(s: string, args: list of string): int
+{
+	for(; args != nil; args = tl args)
+		if(fileprefix(hd args, s))
+			return 1;
+	return 0;
+}
+
+mkdirs(basedir, name: string)
+{
+	(nil, names) := sys->tokenize(name, "/");
+	while(names != nil) {
+		create(basedir, Sys->OREAD, 8r775|Sys->DMDIR);
+		if(tl names == nil)
+			break;
+		basedir = basedir + "/" + hd names;
+		names = tl names;
+	}
+}
+
+mkdir(name: string, dir : ref Sys->Dir)
+{
+	d: Dir;
+	i: int;
+
+	if(vflag) {
+		MTPT : con "/n/remote";
+		s := name;
+		if (len name >= len MTPT && name[0:len MTPT] == MTPT)
+			s = name[len MTPT:];
+		sys->print("installing directory %s\n", s);
+	}
+	fd := create(name, Sys->OREAD, dir.mode);
+	if(fd == nil) {
+		err := sys->sprint("%r");
+		(i, d) = sys->stat(name);
+		if(i < 0 || !(d.mode & Sys->DMDIR)){
+			werr(sys->sprint("can't make directory %s: %s", name, err));
+			return;
+		}
+	}
+	else {
+		(i, d) = sys->fstat(fd);
+		if(i < 0)
+			warn(sys->sprint("can't stat %s: %r", name));
+		fd = nil;
+	}
+	d = sys->nulldir;
+	(nil, p) := str->splitr(name, "/");
+	if(p == nil)
+		p = name;
+	d.name = p;
+	d.mode = dir.mode;
+	if(tflag || uflag)
+		d.mtime = dir.mtime;
+	if(uflag){
+		d.uid = dir.uid;
+		d.gid = dir.gid;
+	}
+	fd = nil;
+	if(sys->wstat(name, d) < 0){
+		e := sys->sprint("%r");
+		if(wstat(name, d) < 0)
+			warn(sys->sprint("can't set modes for %s: %s", name, e));
+	}
+	if(uflag){
+		(i, d) = sys->stat(name);
+		if(i < 0)
+			warn(sys->sprint("can't reread modes for %s: %r", name));
+		if(dir.uid != d.uid)
+			warn(sys->sprint("%s: uid mismatch %s %s", name, dir.uid, d.uid));
+		if(dir.gid != d.gid)
+			warn(sys->sprint("%s: gid mismatch %s %s", name, dir.gid, d.gid));
+	}
+}
+
+extract(ar : ref Arch->Archive, name: string, dir : ref Sys->Dir)
+{
+	sfd := create(name, Sys->OWRITE, dir.mode);
+	if(sfd == nil) {
+		if(!fflag || remove(name) == -1 ||
+		    (sfd = create(name, Sys->OWRITE, dir.mode)) == nil) {
+			werr(sys->sprint("can't make file %s: %r", name));
+			arch->drain(ar, int dir.length);
+			return;
+		}
+	}
+	b := bufio->fopen(sfd, Bufio->OWRITE);
+	if (b == nil) {
+		warn(sys->sprint("can't open file %s for bufio : %r", name));
+		arch->drain(ar, int dir.length);
+		return;
+	}
+	err := arch->getfile(ar, b, int dir.length);
+	if (err != nil) {
+		if (len err >= 9 && err[0:9] == "premature")
+			fatal(err);
+		else
+			warn(err);
+	}
+	(i, d) := sys->fstat(b.fd);
+	if(i < 0)
+		warn(sys->sprint("can't stat %s: %r", name));
+	d = sys->nulldir;
+	(nil, p) := str->splitr(name, "/");
+	if(p == nil)
+		p = name;
+	d.name = p;
+	d.mode = dir.mode;
+	if(tflag || uflag)
+		d.mtime = dir.mtime;
+	if(uflag){
+		d.uid = dir.uid;
+		d.gid = dir.gid;
+	}
+	if(b.flush() == Bufio->ERROR)
+		werr(sys->sprint("error writing %s: %r", name));
+	b.close();
+	sfd = nil;
+	if(sys->wstat(name, d) < 0){
+		e := sys->sprint("%r");
+		if(wstat(name, d) < 0)
+			warn(sys->sprint("can't set modes for %s: %s", name, e));
+	}
+	if(uflag){
+		(i, d) = sys->stat(name);
+		if(i < 0)
+			warn(sys->sprint("can't reread modes for %s: %r", name));
+		if(d.uid != dir.uid)
+			warn(sys->sprint("%s: uid mismatch %s %s", name, dir.uid, d.uid));
+		if(d.gid != dir.gid)
+			warn(sys->sprint("%s: gid mismatch %s %s", name, dir.gid, d.gid));
+	}
+}
+
+error(s: string)
+{
+	fprint(stderr, "%s: %s\n", argv0, s);
+	quit("error");
+}
+
+werr(s: string)
+{
+	fprint(stderr, "%s: %s\n", argv0, s);
+	if(stoponerr)
+		quit("werr");
+}
+	
+warn(s: string)
+{
+	fprint(stderr, "%s: %s\n", argv0, s);
+}
+
+usage()
+{
+	fprint(stderr, "Usage: inst [-h] [-u] [-v] [-f] [-c] [-F] [-r dest-root] [file ...]\n");
+	raise "fail: usage";
+}
+
+fatal(s : string)
+{
+	sys->fprint(stderr, "inst: %s\n", s);
+	if(wrap != nil)
+		wrap->end();
+	exit;
+}
+
+parent(name : string) : string
+{
+	slash := -1;
+	for (i := 0; i < len name; i++)
+		if (name[i] == '/')
+			slash = i;
+	if (slash > 0)
+		return name[0:slash];
+	return "/";
+}
+
+create(name : string, rw : int, mode : int) : ref Sys->FD
+{
+	fd := sys->create(name, rw, mode);
+	if (fd == nil) {
+		p := parent(name);
+		(ok, d) := sys->stat(p);
+		if (ok < 0)
+			return nil;
+		omode := d.mode;
+		d = sys->nulldir;
+		d.mode = omode | 8r222;	# ensure parent is writable
+		sys->wstat(p, d);
+		fd = sys->create(name, rw, mode);
+		d.mode = omode;
+		sys->wstat(p, d);
+	}
+	return fd;
+}
+
+remove(name : string) : int
+{
+	if (sys->remove(name) < 0) {
+		(ok, d) := sys->stat(name);
+		if (ok < 0)
+			return -1;
+		omode := d.mode;
+		d.mode |= 8r222;
+		sys->wstat(name, d);
+		if (sys->remove(name) >= 0)
+			return 0;
+		d.mode = omode;
+		sys->wstat(name, d);
+		return -1;
+	}
+	return 0;
+}
+
+wstat(name : string, d : Dir) : int
+{
+	(ok, dir) := sys->stat(name);
+	if (ok < 0)
+		return -1;
+	omode := dir.mode;
+	dir.mode |= 8r222;
+	sys->wstat(name, dir);
+	if (sys->wstat(name, d) >= 0)
+		return 0;
+	dir.mode = omode;
+	sys->wstat(name, dir);
+	return -1;
+}
+
+pathcat(s : string, t : string) : string
+{
+	if (s == nil) return t;
+	if (t == nil) return s;
+	slashs := s[len s - 1] == '/';
+	slasht := t[0] == '/';
+	if (slashs && slasht)
+		return s + t[1:];
+	if (!slashs && !slasht)
+		return s + "/" + t;
+	return s + t;
+}
--- /dev/null
+++ b/appl/cmd/install/install.b
@@ -1,0 +1,430 @@
+implement Install;
+
+#
+# Determine which packages need installing and calls install/inst 
+# to actually install each one
+#
+
+# usage: install/install -d -F -g -s -u -i installdir -p platform -r root -P package
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "arg.m";
+	arg: Arg;
+include "readdir.m";
+	readdir : Readdir;
+include "sh.m";
+
+Install: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+# required dirs, usually in the standard inferno root.
+# The network download doesn't include them because of
+# problems with versions of tar that won't create empty dirs
+# so we'll make sure they exist.
+
+reqdirs := array [] of {
+	"/mnt",
+	"/mnt/wrap",
+	"/n",
+	"/n/remote",
+	"/tmp",
+};
+
+YES, NO, QUIT, ERR : con iota;
+INST : con "install/inst";	# actual install program
+MTPT : con "/n/remote";	# mount point for user's inferno root
+
+debug := 0;
+force := 0;
+exitemu := 0;
+uflag := 0;
+stderr : ref Sys->FD;
+installdir := "/install";
+platform := "Plan9";
+lcplatform : string;
+root := "/usr/inferno";
+local: int;
+global: int = 1;
+waitfd : ref Sys->FD;
+
+Product : adt {
+	name : string;
+	pkgs : ref Package;
+	nxt : ref Product;
+};
+
+Package : adt {
+	name : string;
+	nxt : ref Package;
+};
+
+instprods : ref Product;	# products/packages already installed
+
+# platform independent packages
+xpkgs := array[] of { "inferno", "utils", "src", "ipaq", "minitel", "sds" };
+ypkgs: list of string;
+		
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	# Hack for network download...
+	# make sure the dirs we need exist
+	for (dirix := 0; dirix < len reqdirs; dirix++) {
+		dir := reqdirs[dirix];
+		(exists, nil) := sys->stat(dir);
+		if (exists == -1) {
+			fd := sys->create(dir, Sys->OREAD, Sys->DMDIR + 8r7775);
+			if (fd == nil)
+				fatal(sys->sprint("cannot create directory %s: %r\n", dir));
+			fd = nil;
+		}
+	}
+
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		fatal(sys->sprint("cannot load %s: %r\n", Bufio->PATH));
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		fatal(sys->sprint("cannot load %s: %r\n", Readdir->PATH));
+	str = load String String->PATH;
+	if(str == nil)
+		fatal(sys->sprint("cannot load %s: %r\n", String->PATH));
+	arg = load Arg Arg->PATH;
+	if(arg == nil)
+		fatal(sys->sprint("cannot load %s: %r\n", Arg->PATH));
+	arg->init(args);
+	while((c := arg->opt()) != 0) {
+		case c {
+			'd' =>
+				debug = 1;
+			'F' =>
+				force = 1;
+			's' =>
+				exitemu = 1;
+			'i' => 
+				installdir = arg->arg();
+				if (installdir == nil)
+					fatal("install directory missing");
+			'p' =>
+				platform = arg->arg();
+				if (platform == nil)
+					fatal("platform missing");
+			'P' =>
+				pkg := arg->arg();
+				if (pkg == nil)
+					fatal("package missing");
+				ypkgs = pkg :: ypkgs;
+			'r' =>
+				root = arg->arg();
+				if (root == nil)
+					fatal("inferno root missing");
+			'u' =>
+				uflag = 1;
+			'g' =>
+				global = 0;
+			'*' =>
+				usage();
+		}
+	}
+	if (arg->argv() != nil)
+		usage();
+	lcplatform = str->tolower(platform);
+	(ok, dir) := sys->stat(installdir);
+	if (ok < 0)
+		fatal(sys->sprint("cannot open install directory %s", installdir));
+	nt := lcplatform == "nt";
+	if (nt) {
+		# root os of the form ?:/.........
+		if (len root < 3 || root[1] != ':' || root[2] != '/')
+			fatal(sys->sprint("root %s not of the form ?:/.......", root));
+		spec := root[0:2];
+		root = root[2:];
+		if (sys->bind("#U"+spec, MTPT, Sys->MREPL|Sys->MCREATE) < 0)
+			fatal(sys->sprint("cannot bind to drive %s", spec));
+	}
+	else {
+		if (root[0] != '/')
+			fatal(sys->sprint("root %s must be an absolute path name", root));
+		if (sys->bind("#U*", MTPT, Sys->MREPL|Sys->MCREATE) < 0)
+			fatal("cannot bind to system root");
+	}
+	(ok, dir) = sys->stat(MTPT+root);
+	if (ok >= 0) {
+		if ((dir.mode & Sys->DMDIR) == 0)
+			fatal(sys->sprint("inferno root %s is not a directory", root));
+	}
+	else if (sys->create(MTPT+root, Sys->OREAD, 8r775 | Sys->DMDIR) == nil)
+		fatal(sys->sprint("cannot create inferno root %s: %r", root));
+	# need a writable tmp directory /tmp in case installing from CD
+	(ok, dir) = sys->stat(MTPT+root+"/tmp");
+	if (ok >= 0) {
+		if ((dir.mode & Sys->DMDIR) == 0)
+			fatal(sys->sprint("inferno root tmp %s is not a directory", root+"/tmp"));
+	}
+	else if (sys->create(MTPT+root+"/tmp", Sys->OREAD, 8r775 | Sys->DMDIR) == nil)
+		fatal(sys->sprint("cannot create inferno root tmp %s: %r", root+"/tmp"));
+	if (sys->bind(MTPT+root, MTPT, Sys->MREPL | Sys->MCREATE) < 0)
+		fatal("cannot bind inferno root");
+	if (sys->bind(MTPT+"/tmp", "/tmp", Sys->MREPL | Sys->MCREATE) < 0)
+		fatal("cannot bind inferno root tmp");
+	root = MTPT;
+	
+	if (nt || 1)
+		local = 1;
+	else {
+		sys->print("You can either install software specific to %s only or\n", platform);
+		sys->print(" install software for all platforms that we support.\n");
+		sys->print("If you are unsure what to do, answer yes to the question following.\n");
+		sys->print(" You can install the remainder of the software at a later date if desired.\n");
+		sys->print("\n");
+		b := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+		if (b == nil)
+			fatal("cannot open stdin");
+		for (;;) {
+			sys->print("Install software specific to %s only ? (yes/no/quit) ", platform);
+			resp := getresponse(b);
+			ans := answer(resp);
+			if (ans == QUIT)
+				exit;
+			else if (ans == ERR)
+				sys->print("bad response %s\n\n", resp);
+			else {
+				local = ans == YES;
+				break;
+			}
+		}
+	}
+	instprods = dowraps(root+"/wrap");
+	doprods(installdir);
+	if (!nt)
+		sys->print("installation complete\n");
+	if (exitemu)
+		shutdown();
+}
+
+getresponse(b : ref Iobuf) : string
+{
+	s := b.gets('\n');
+	while (s != nil && (s[0] == ' ' || s[0] == '\t'))
+		s = s[1:];
+	while (s != nil && ((c := s[len s - 1]) == ' ' || c == '\t' || c == '\n'))
+		s = s[0: len s - 1];
+	return s;
+}
+
+answer(s : string) : int
+{
+	s = str->tolower(s);
+	if (s == "y" || s == "yes")
+		return YES;
+	if (s == "n" || s == "no")
+		return NO;
+	if (s == "q" || s == "quit")
+		return QUIT;
+	return ERR;
+}
+
+usage()
+{
+	fatal("Usage: install [-d] [-F] [-s] [-u] [-i installdir ] [-p platform ] [-r root]");
+}
+
+fatal(s : string)
+{
+	sys->fprint(stderr, "install: %s\n", s);
+	exit;
+}
+
+dowraps(d : string) : ref Product
+{
+	p : ref Product;
+
+	# make an inventory of what is already apparently installed
+	(dir, n) := readdir->init(d, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		if (dir[i].mode & Sys->DMDIR) {
+			p = ref Product(str->tolower(dir[i].name), nil, p);
+			p.pkgs = dowrap(d + "/" + dir[i].name);
+		}
+	}
+	return p;
+}
+
+dowrap(d : string) : ref Package
+{
+	p : ref Package;
+
+	(dir, n) := readdir->init(d, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++)
+		p = ref Package(dir[i].name, p);
+	return p;
+}
+	
+doprods(d : string)
+{
+	(dir, n) := readdir->init(d, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		if (dir[i].mode & Sys->DMDIR)
+			doprod(str->tolower(dir[i].name), d + "/" + dir[i].name);
+	}
+}
+
+doprod(pr : string, d : string)
+{
+	# base package, updates and update packages have the name
+	# <timestamp> or <timestamp.gz>
+	if (!wanted(pr))
+		return;
+	(dir, n) := readdir->init(d, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		pk := dir[i].name;
+		l := len pk;
+		if (l >= 4 && pk[l-3:l] == ".gz")
+			pk = pk[0:l-3];
+		else if (l >= 5 && (pk[l-4:] == ".tgz" || pk[l-4:] == ".9gz"))
+			pk = pk[0:l-4];
+		dopkg(pk, pr, d+"/"+dir[i].name);
+		
+	}
+}
+
+dopkg(pk : string, pr : string, d : string)
+{
+	if (!installed(pk, pr))
+		install(d);
+}
+
+installed(pkg : string, prd : string) : int
+{
+	for (pr := instprods; pr != nil; pr = pr.nxt) {
+		if (pr.name == prd) {
+			for (pk := pr.pkgs; pk != nil; pk = pk.nxt) {
+				if (pk.name == pkg)
+					return 1;
+			}
+			return 0;
+		}
+	}
+	return 0;
+}
+
+lookup(pr : string) : int
+{
+	for (i := 0; i < len xpkgs; i++) {
+		if (xpkgs[i] == pr)
+			return i;
+	}
+	return -1;
+}
+
+plookup(pr: string): int
+{
+	for(ps := ypkgs; ps != nil; ps = tl ps)
+		if(pr == hd ps)
+			return 1;
+	return 0;
+}
+
+wanted(pr : string) : int
+{
+	if (!local || global)
+		return 1;
+	if(ypkgs != nil)	# overrides everything else
+		return plookup(pr);
+	found := lookup(pr);
+	if (found >= 0)
+		return 1;
+	return pr == lcplatform || prefix(lcplatform, pr);
+}
+
+install(d : string)
+{
+	if (waitfd == nil)
+		waitfd = openwait(sys->pctl(0, nil));
+	sys->fprint(stderr, "installing package %s\n", d);
+	if (debug)
+		return;
+	c := chan of int;
+	args := "-t" :: "-v" :: "-r" :: root :: d :: nil;
+	if (uflag)
+		args = "-u" :: args;
+	if (force)
+		args = "-F" :: args;
+	spawn exec(INST, INST :: args, c);
+	execpid := <- c;
+	wait(waitfd, execpid);
+}
+
+exec(cmd : string, argl : list of string, ci : chan of int)
+{
+	ci <-= sys->pctl(Sys->FORKNS|Sys->NEWFD|Sys->NEWPGRP, 0 :: 1 :: 2 :: stderr.fd :: nil);
+	file := cmd;
+	if(len file<4 || file[len file-4:] !=".dis")
+		file += ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0] !='/' && file[0:2] !="./") {
+			c = load Command "/dis/"+file; 
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil)
+			fatal(sys->sprint("%s: %s\n", cmd, err));
+	}
+	c->init(nil, argl);
+}
+
+openwait(pid : int) : ref Sys->FD
+{
+	w := sys->sprint("#p/%d/wait", pid);
+	fd := sys->open(w, Sys->OREAD);
+	if (fd == nil)
+		fatal("fd == nil in wait");
+	return fd;
+}
+	
+wait(wfd : ref Sys->FD, wpid : int)
+{
+	n : int;
+
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;) {
+		if ((n = sys->read(wfd, buf, len buf)) < 0)
+			fatal("bad read in wait");
+		status = string buf[0:n];
+		break;
+	}
+	if (int status != wpid)
+		fatal("bad status in wait");
+	if(status[len status - 1] != ':')
+		fatal(sys->sprint("%s\n", status));
+}
+
+shutdown()
+{
+	fd := sys->open("/dev/sysctl", sys->OWRITE);
+	if(fd == nil)
+		fatal("cannot shutdown emu");
+	if (sys->write(fd, array of byte "halt", 4) < 0)
+		fatal(sys->sprint("shutdown: write failed: %r\n"));
+}
+
+prefix(s, t : string) : int
+{
+	if (len s <= len t)
+		return t[0:len s] == s;
+	return 0;
+}
--- /dev/null
+++ b/appl/cmd/install/log.b
@@ -1,0 +1,76 @@
+implement Fsmodule;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "sh.m";
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "fslib.m";
+	fslib: Fslib;
+	Report, Value, type2s, quit: import fslib;
+	Fschan, Fsdata, Entrychan, Entry,
+	Gatechan, Gatequery, Nilentry, Option,
+	Next, Down, Skip, Quit: import Fslib;
+
+types(): string
+{
+	return "vt-us-gs";
+}
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "fs: log: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	fslib = load Fslib Fslib->PATH;
+	if(fslib == nil)
+		badmod(Fslib->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		badmod(Daytime->PATH);
+}
+
+run(nil: ref Draw->Context, report: ref Report,
+			opts: list of Option, args: list of ref Value): ref Value
+{
+	uid, gid: string;
+	for(; opts != nil; opts = tl opts){
+		o := hd (hd opts).args;
+		case (hd opts).opt {
+		'u' =>	uid = o.s().i;
+		'g' =>	gid = o.s().i;
+		}
+	}
+	sync := chan of int;
+	spawn logproc(sync, (hd args).t().i, report.start("log"), uid, gid);
+	return ref Value.V(sync);
+}
+
+logproc(sync: chan of int, c: Entrychan, errorc: chan of string, uid: string, gid: string)
+{
+	if(<-sync == 0){
+		c.sync <-= 0;
+		quit(errorc);
+		exit;
+	}
+	c.sync <-= 1;
+
+	now := daytime->now();
+	for(seq := 0; ((d, p, nil) := <-c.c).t0 != nil; seq++){
+		if(uid != nil)
+			d.uid = uid;
+		if(gid != nil)
+			d.gid = gid;
+		sys->print("%ud %ud %c %q - - %uo %q %q %ud %bd%s\n", now, seq, 'a', p, d.mode, d.uid, d.gid, d.mtime, d.length, "");
+	}
+	quit(errorc);
+}
--- /dev/null
+++ b/appl/cmd/install/logs.b
@@ -1,0 +1,287 @@
+implement Logs;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "logs.m";
+
+Hashsize: con 1024;
+Incr: con 500;
+
+init(bio: Bufio): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = bio;
+	str = load String String->PATH;
+	if(str == nil)
+		return sys->sprint("can't load %s: %r", String->PATH);
+	return nil;
+}
+
+Entry.read(in: ref Iobuf): (ref Entry, string)
+{
+	if((s := in.gets('\n')) == nil)
+		return (nil, nil);
+	if(s[len s-1] == '\n')
+		s = s[0:len s-1];
+
+	e := ref Entry;
+	e.x = -1;
+
+	l := str->unquoted(s);
+	fields := array[11] of string;
+	for(i := 0; l != nil; l = tl l)
+		fields[i++] = S(hd l);
+
+	#  time gen verb path serverpath mode uid gid mtime length
+	# 1064889121 4 a sys/src/cmd/ip/httpd/webls.denied - 664 sys sys 1064887847 3
+	# time[0] gen[1] op[2] path[3] (serverpath|"-")[4] mode[5] uid[6] gid[7] mtime[8] length[9]
+
+	if(i < 10 || len fields[2] != 1)
+		return (nil, sys->sprint("bad log entry: %q", s));
+	e.action = fields[2][0];
+	case e.action {
+	'a' or 'c' or 'd' or 'm' =>
+		;
+	* =>
+		return (nil, sys->sprint("bad log entry: %q", s));
+	}
+
+	time := bigof(fields[0], 10);
+	sgen := bigof(fields[1], 10);
+	e.seq = (time << 32) | sgen;	# for easier comparison
+
+	# time/gen check
+	# name check
+
+	if(fields[4] == "-")	# undocumented
+		fields[4] = fields[3];
+	e.path = fields[3];
+	e.serverpath = fields[4];
+	e.d = sys->nulldir;
+	{
+		e.d.mode = intof(fields[5], 8);
+		e.d.qid.qtype = e.d.mode>>24;
+		e.d.uid = fields[6];
+		if(e.d.uid == "-")
+			e.d.uid = "";
+		e.d.gid = fields[7];
+		if(e.d.gid == "-")
+			e.d.gid = "";
+		e.d.mtime = intof(fields[8], 10);
+		e.d.length = bigof(fields[9], 10);
+	}exception ex {
+	"log format:*" =>
+		return (nil, sys->sprint("%s in log entry %q", ex, s));
+	}
+	e.contents = fields[10] :: nil;	# optional
+	return (e, nil);
+}
+
+rev[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+bigof(s: string, base: int): big
+{
+	(b, r) := str->tobig(s, base);
+	if(r != nil)
+		raise "invalid integer field";
+	return b;
+}
+
+intof(s: string, base: int): int
+{
+	return int bigof(s, base);
+}
+
+mkpath(root: string, name: string): string
+{
+	if(len root > 0 && root[len root-1] != '/' && (len name == 0 || name[0] != '/'))
+		return root+"/"+name;
+	return root+name;
+}
+
+contents(e: ref Entry): string
+{
+	if(e.contents == nil)
+		return "";
+	s := "";
+	for(cl := e.contents; cl != nil; cl = tl cl)
+		s += " " + hd cl;
+	return s;	# includes initial space
+}
+
+Entry.text(e: self ref Entry): string
+{
+	a := e.action;
+	if(a == 0)
+		a = '?';
+	return sys->sprint("%bd %bd %q [%d] %c m=%uo l=%bd t=%ud c=%q", e.seq>>32, e.seq & 16rFFFFFFFF, e.path, e.x, a, e.d.mode, e.d.length, e.d.mtime, contents(e));
+}
+
+Entry.sumtext(e: self ref Entry): string
+{
+	case e.action {
+	'a' or 'm' =>
+		return sys->sprint("%c %q %uo %q %q %ud", e.action, e.path, e.d.mode, e.d.uid, e.d.gid, e.d.mtime);
+	'd' or 'c' =>
+		return sys->sprint("%c %q", e.action, e.path);
+	* =>
+		return sys->sprint("? %q", e.path);
+	}
+}
+
+Entry.dbtext(e: self ref Entry): string
+{
+	#   path dpath|"-" mode uid gid mtime length
+	return sys->sprint("%bd %bd %q - %uo %q %q %ud %bd%s", e.seq>>32, e.seq & 16rFFFFFFFF, e.path, e.d.mode, e.d.uid, e.d.gid, e.d.mtime, e.d.length, contents(e));
+}
+
+Entry.logtext(e: self ref Entry): string
+{
+	#   gen n act path spath|"-" dpath|"-" mode uid gid mtime length
+	a := e.action;
+	if(a == 0)
+		a = '?';
+	sf := e.serverpath;
+	if(sf == nil || sf == e.path)
+		sf = "-";
+	return sys->sprint("%bd %bd %c %q %q %uo %q %q %ud %bd%s", e.seq>>32, e.seq & 16rFFFFFFFF, a, e.path, sf, e.d.mode, e.d.uid, e.d.gid, e.d.mtime, e.d.length, contents(e));
+}
+
+Entry.remove(e: self ref Entry)
+{
+	e.action = 'd';
+}
+
+Entry.removed(e: self ref Entry): int
+{
+	return e.action == 'd';
+}
+
+Entry.update(e: self ref Entry, n: ref Entry)
+{
+	if(n == nil)
+		return;
+	if(n.action == 'd')
+		e.contents = nil;
+	else
+		e.d = n.d;
+	if(n.action != 'm' || e.action == 'd')
+		e.action = n.action;
+	e.serverpath = S(n.serverpath);
+	for(nl := rev(n.contents); nl != nil; nl = tl nl)
+		e.contents = hd nl :: e.contents;
+	if(n.seq > e.seq)
+		e.seq = n.seq;
+}
+
+Db.new(name: string): ref Db
+{
+	db := ref Db;
+	db.name = name;
+	db.stateht = array[Hashsize] of list of ref Entry;
+	db.nstate = 0;
+	db.state = array[50] of ref Entry;
+	return db;
+}
+
+Db.look(db: self ref Db, name: string): ref Entry
+{
+	(b, nil) := hash(name, len db.stateht);
+	for(l := db.stateht[b]; l != nil; l = tl l)
+		if((hd l).path == name)
+			return hd l;
+	return nil;
+}
+
+Db.entry(db: self ref Db, seq: big, name: string, d: Sys->Dir): ref Entry
+{
+	e := ref Entry;
+	e.action = 'a';
+	e.seq = seq;
+	e.path = name;
+	e.d = d;
+	e.x = db.nstate++;
+	if(e.x >= len db.state){
+		a := array[len db.state + Incr] of ref Entry;
+		a[0:]  = db.state;
+		db.state = a;
+	}
+	db.state[e.x] = e;
+	(b, nil) := hash(name, len db.stateht);
+	db.stateht[b] = e :: db.stateht[b];
+	return e;
+}
+
+Db.sort(db: self ref Db, key: int)
+{
+	sortentries(db.state[0:db.nstate], key);
+}
+
+sortentries(a: array of ref Entry, key: int): (array of ref Entry, int)
+{
+	mergesort(a, array[len a] of ref Entry, key);
+	return (a, len a);
+}
+	
+mergesort(a, b: array of ref Entry, key: int)
+{
+	r := len a;
+	if(r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m], key);
+		mergesort(a[m:], b[m:], key);
+		b[0:] = a;
+		for((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if(key==Byname && b[i].path > b[j].path || key==Byseq && b[i].seq > b[j].seq)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if(i < m)
+			a[k:] = b[i:m];
+		else if(j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+strings:	array of list of string;
+
+S(s: string): string
+{
+	if(strings == nil)
+		strings = array[257] of list of string;
+	h := hash(s, len strings).t0;
+	for(sl := strings[h]; sl != nil; sl = tl sl)
+		if(hd sl == s)
+			return hd sl;
+	strings[h] = s :: strings[h];
+	return s;
+}
+
+hash(s: string, n: int): (int, int)
+{
+	# hashpjw
+	h := 0;
+	for(i:=0; i<len s; i++){
+		h = (h<<4) + s[i];
+		if((g := h & int 16rF0000000) != 0)
+			h ^= ((g>>24) & 16rFF) | g;
+	}
+	return ((h&~(1<<31))%n, h);
+}
--- /dev/null
+++ b/appl/cmd/install/logs.m
@@ -1,0 +1,44 @@
+Logs: module
+{
+	PATH:	con "/dis/install/logs.dis";
+
+	Entry: adt
+	{
+		seq:	big;	# time<<32 | gen
+		action:	int;
+		path:	string;
+		serverpath:	string;
+		x:	int;
+		d:	Sys->Dir;
+		contents:	list of string;	# MD5 hash of content, most recent first
+
+		read:	fn(in: ref Bufio->Iobuf): (ref Entry, string);
+		remove:	fn(e: self ref Entry);
+		removed:	fn(e: self ref Entry): int;
+		update:	fn(e: self ref Entry, n: ref Entry);
+		text:	fn(e: self ref Entry): string;
+		dbtext:	fn(e: self ref Entry): string;
+		sumtext:	fn(e: self ref Entry): string;
+		logtext:	fn(e: self ref Entry): string;
+	};
+
+	Db: adt
+	{
+		name:	string;
+		state:	array of ref Entry;
+		nstate:	int;
+		stateht:	array of list of ref Entry;
+
+		new:	fn(name: string): ref Db;
+		entry:	fn(db: self ref Db, seq: big, name: string, d: Sys->Dir): ref Entry;
+		look:	fn(db: self ref Db, name: string): ref Entry;
+		sort:	fn(db: self ref Db, byname: int);
+	};
+
+	Byseq, Byname: con iota;
+
+	init:	fn(bio: Bufio): string;
+
+	S:	fn(s: string): string;
+	mkpath:	fn(root: string, name: string): string;
+};
--- /dev/null
+++ b/appl/cmd/install/mergelog.b
@@ -1,0 +1,239 @@
+implement Mergelog;
+
+#
+# combine old and new log sections into one with the most recent data
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "keyring.m";
+	kr: Keyring;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "logs.m";
+	logs: Logs;
+	Db, Entry, Byname, Byseq: import logs;
+	S: import logs;
+
+include "arg.m";
+
+Mergelog: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Apply, Applydb, Install, Asis, Skip: con iota;
+
+client:	ref Db;	# client current state from client log
+updates:	ref Db;	# state delta from new section of server log
+
+nerror := 0;
+nconflict := 0;
+debug := 0;
+verbose := 0;
+resolve := 0;
+setuid := 0;
+setgid := 0;
+nflag := 0;
+timefile: string;
+clientroot: string;
+srvroot: string;
+logfd: ref Sys->FD;
+now := 0;
+gen := 0;
+noerr := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	bufio = load Bufio Bufio->PATH;
+	ensure(bufio, Bufio->PATH);
+	str = load String String->PATH;
+	ensure(str, String->PATH);
+	kr = load Keyring Keyring->PATH;
+	ensure(kr, Keyring->PATH);
+	daytime = load Daytime Daytime->PATH;
+	ensure(daytime, Daytime->PATH);
+	logs = load Logs Logs->PATH;
+	ensure(logs, Logs->PATH);
+	logs->init(bufio);
+
+	arg := load Arg Arg->PATH;
+	ensure(arg, Arg->PATH);
+	arg->init(args);
+	arg->setusage("mergelog [-vd] oldlog [path ... ] <newlog");
+	dump := 0;
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' =>	dump = 1; debug = 1;
+		'v' =>	verbose = 1;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args < 3)
+		arg->usage();
+	arg = nil;
+
+	now = daytime->now();
+	client = Db.new("existing log");
+	updates = Db.new("update log");
+	clientlog := hd args; args = tl args;
+	if(args != nil)
+		error("restriction by path not yet done");
+
+	# replay the client log to build last installation state of files taken from server
+	logfd = sys->open(clientlog, Sys->OREAD);
+	if(logfd == nil)
+		error(sys->sprint("can't open %s: %r", clientlog));
+	f := bufio->fopen(logfd, Sys->OREAD);
+	if(f == nil)
+		error(sys->sprint("can't open %s: %r", clientlog));
+	while((log := readlog(f)) != nil)
+		replaylog(client, log);
+	f = nil;
+
+	# read new log entries and use the new section to build a sequence of update actions
+	f = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	while((log = readlog(f)) != nil)
+		replaylog(client, log);
+	client.sort(Byseq);
+	dumpdb(client);
+	if(nerror)
+		raise sys->sprint("fail:%d errors", nerror);
+}
+
+readlog(in: ref Iobuf): ref Entry
+{
+	(e, err) := Entry.read(in);
+	if(err != nil)
+		error(err);
+	return e;
+}
+
+#
+# replay a log to reach the state wrt files previously taken from the server
+#
+replaylog(db: ref Db, log: ref Entry)
+{
+	e := db.look(log.path);
+	indb := e != nil && !e.removed();
+	case log.action {
+	'a' =>	# add new file
+		if(indb){
+			note(sys->sprint("%q duplicate create", log.path));
+			return;
+		}
+	'c' =>	# contents
+		if(!indb){
+			note(sys->sprint("%q contents but no entry", log.path));
+			return;
+		}
+	'd' =>	# delete
+		if(!indb){
+			note(sys->sprint("%q deleted but no entry", log.path));
+			return;
+		}
+		if(e.d.mtime > log.d.mtime){
+			note(sys->sprint("%q deleted but it's newer", log.path));
+			return;
+		}
+	'm' =>	# metadata
+		if(!indb){
+			note(sys->sprint("%q metadata but no entry", log.path));
+			return;
+		}
+	* =>
+		error(sys->sprint("bad log entry: %bd %bd", log.seq>>32, log.seq & big 16rFFFFFFFF));
+	}
+	update(db, e, log);
+}
+
+#
+# update file state e to reflect the effect of the log,
+# creating a new entry if necessary
+#
+update(db: ref Db, e: ref Entry, log: ref Entry)
+{
+	if(e == nil)
+		e = db.entry(log.seq, log.path, log.d);
+	e.update(log);
+}
+
+rev[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+ensure[T](m: T, path: string)
+{
+	if(m == nil)
+		error(sys->sprint("can't load %s: %r", path));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "applylog: %s\n", s);
+	raise "fail:error";
+}
+
+note(s: string)
+{
+	sys->fprint(sys->fildes(2), "applylog: note: %s\n", s);
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "applylog: warning: %s\n", s);
+	nerror++;
+}
+
+samestat(a: Sys->Dir, b: Sys->Dir): int
+{
+	# doesn't check permission/ownership, does check QTDIR/QTFILE
+	if(a.mode & Sys->DMDIR)
+		return (b.mode & Sys->DMDIR) != 0;
+	return a.length == b.length && a.mtime == b.mtime && a.qid.qtype == b.qid.qtype;	# TO DO: a.name==b.name?
+}
+
+samemeta(a: Sys->Dir, b: Sys->Dir): int
+{
+	return a.mode == b.mode && (!setuid || a.uid == b.uid) && (!setgid || a.gid == b.gid) && samestat(a, b);
+}
+
+bigof(s: string, base: int): big
+{
+	(b, r) := str->tobig(s, base);
+	if(r != nil)
+		error("cruft in integer field in log entry: "+s);
+	return b;
+}
+
+intof(s: string, base: int): int
+{
+	return int bigof(s, base);
+}
+
+dumpdb(db: ref Db)
+{
+	for(i := 0; i < db.nstate; i++){
+		s := db.state[i].text();
+		if(s != nil)
+			sys->print("%s\n", s);
+	}
+}
--- /dev/null
+++ b/appl/cmd/install/mkfile
@@ -1,0 +1,43 @@
+<../../../mkconfig
+
+TARG=\
+	create.dis\
+	info.dis\
+	wdiff.dis\
+	inst.dis\
+	wrap.dis\
+	archfs.dis\
+	install.dis\
+	arch.dis\
+	proto.dis\
+	ckproto.dis\
+	proto2list.dis\
+	wrap2list.dis\
+	wfind.dis\
+	mkproto.dis\
+	applylog.dis\
+	logs.dis\
+	log.dis\
+	mergelog.dis\
+	updatelog.dis\
+	eproto.dis\
+
+MODULES=\
+	wrap.m\
+	arch.m\
+	archfs.m\
+	logs.m\
+	proto.m\
+	protocaller.m\
+
+SYSMODULES=\
+	arg.m\
+	bufio.m\
+	sys.m\
+	draw.m\
+	bufio.m\
+	string.m\
+
+DISBIN=$ROOT/dis/install
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/install/mkproto.b
@@ -1,0 +1,99 @@
+#	
+# Copyright © 2000 Vita Nuova (Holdings) Limited.  All rights reserved.	
+#
+
+implement Mkproto;
+
+# make a proto description of the directory or file
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "readdir.m";
+	readdir: Readdir;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+Mkproto: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "Usage: mkproto [ file|directory ... ]\n");
+	raise "fail:usage";
+}
+
+not: list of string;
+bout: ref Iobuf;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	readdir = load Readdir Readdir->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	bout = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+	argv = tl argv;
+	while (argv != nil && hd argv != nil && (hd argv)[0] == '-') {
+		not = (hd argv)[1:] :: not;
+		argv = tl argv;
+	}
+	if (argv == nil)
+		visit(".", nil, -1);
+	else if (tl argv == nil)
+		visit(hd argv, nil, -1);
+	else {
+		for ( ; argv != nil; argv = tl argv)
+			visit(hd argv, hd argv, 0);
+	}
+	bout.flush();
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "mkproto: %s\n", s);
+}
+
+visit(fulln: string, reln: string, depth: int)
+{
+	if (depth == 0) {
+		for (n := not; n != nil; n = tl n) {
+			if (hd n == reln) {
+				# sys->fprint(stderr, "skipping %s\n", reln);
+				return;
+			}
+		}
+		# sys->fprint(stderr, "doing %s\n", reln);
+	}
+	(ok, d) := sys->stat(fulln);
+	if(ok < 0){
+		warn(sys->sprint("cannot stat %s: %r", fulln));
+		return;
+	}
+	if (depth >= 0)
+		visitf(fulln, reln, d, depth);
+	if (d.mode & Sys->DMDIR)
+		visitd(fulln, reln, d, depth);
+}
+
+visitd(fulln: string, nil: string, nil: Sys->Dir, depth: int)
+{
+	(dir, n) := readdir->init(fulln, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		path := "/"+dir[i].name;
+		visit(fulln+path, dir[i].name, depth+1);
+	}
+}
+
+visitf(nil: string, reln: string, nil: Sys->Dir, depth: int)
+{
+	for (i := 0; i < depth; i++)
+		bout.putc('\t');
+	bout.puts(sys->sprint("%q\n", reln));
+}
--- /dev/null
+++ b/appl/cmd/install/proto.b
@@ -1,0 +1,320 @@
+implement Proto;
+
+include "sys.m";
+	sys: Sys;
+	Dir : import Sys;
+include "draw.m";
+include "bufio.m";
+	bufio : Bufio;
+	Iobuf : import bufio;
+include "string.m";
+	str: String;
+include "readdir.m";
+	readdir : Readdir;
+include "proto.m";
+include "protocaller.m";
+
+NAMELEN: con	8192;
+
+WARN, ERROR, FATAL : import Protocaller;
+
+File: adt {
+	new:	string;
+	elem:	string;
+	old:	string;
+	uid:	string;
+	gid:	string;
+	mode:	int;
+};
+
+indent: int;
+lineno := 0;
+newfile: string;
+oldfile: string;
+oldroot : string;
+b: ref Iobuf;
+cmod : Protocaller;
+
+rdproto(proto : string, root : string, pcmod : Protocaller) : int
+{
+	if (sys == nil) {
+		sys = load Sys Sys->PATH;
+		bufio = load Bufio Bufio->PATH;
+		str = load String String->PATH;
+		readdir = load Readdir Readdir->PATH;
+	}
+	cmod = pcmod;
+	oldroot = root;
+	b = bufio->open(proto, Sys->OREAD);
+	if(b == nil){
+		cmod->protoerr(FATAL, lineno, sys->sprint("can't open %s: %r: skipping\n", proto));
+		b.close();
+		return -1;
+	}
+	lineno = 0;
+	indent = 0;
+	file := ref File;
+	file.mode = 0;
+	mkfs(file, -1);
+	b.close();
+	return 0;
+}
+
+mkfs(me: ref File, level: int)
+{
+	(child, fp) := getfile(me);
+	if(child == nil)
+		return;
+	if(child.elem == "+" || child.elem == "*" || child.elem == "%"){
+		rec := child.elem[0] == '+';
+		filesonly := child.elem[0] == '%';
+		child.new = me.new;
+		setnames(child);
+		mktree(child, rec, filesonly);
+		(child, fp) = getfile(me);
+	}
+	while(child != nil && indent > level){
+		if(mkfile(child))
+			mkfs(child, indent);
+		(child, fp) = getfile(me);
+	}
+	if(child != nil){
+		b.seek(big fp, 0);
+		lineno--;
+	}
+}
+
+mktree(me: ref File, rec: int, filesonly: int)
+{
+	fd := sys->open(oldfile, Sys->OREAD);
+	if(fd == nil){
+		cmod->protoerr(WARN, lineno, sys->sprint("can't open %s: %r", oldfile));
+		return;
+	}
+	child := ref *me;
+	(d, n) := readdir->init(oldfile, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		if (filesonly && (d[i].mode & Sys->DMDIR))
+			continue;
+		child.new = mkpath(me.new, d[i].name);
+		if(me.old != nil)
+			child.old = mkpath(me.old, d[i].name);
+		child.elem = d[i].name;
+		setnames(child);
+		if(copyfile(child, d[i]) && rec)
+			mktree(child, rec, filesonly);
+	}
+}
+
+mkfile(f: ref File): int
+{
+	(i, dir) := sys->stat(oldfile);
+	if(i < 0){
+		cmod->protoerr(WARN, lineno, sys->sprint("can't stat file %s: %r", oldfile));
+		skipdir();
+		return 0;
+	}
+	return copyfile(f, ref dir);
+}
+
+copyfile(f: ref File, d: ref Dir): int
+{
+	d.name = f.elem;
+	if(f.mode != ~0){
+		if((d.mode&Sys->DMDIR) != (f.mode&Sys->DMDIR))
+			cmod->protoerr(WARN, lineno, sys->sprint("inconsistent mode for %s", f.new));
+		else
+			d.mode = f.mode;
+	}
+	cmod->protofile(newfile, oldfile, d);
+	return (d.mode & Sys->DMDIR) != 0;
+}
+
+setnames(f: ref File)
+{
+	newfile = f.new;
+	if(f.old != nil){
+		if(f.old[0] == '/')
+			oldfile = mkpath(oldroot, f.old);
+		else
+			oldfile = f.old;
+	}else
+		oldfile = mkpath(oldroot, f.new);
+}
+
+#
+# skip all files in the proto that
+# could be in the current dir
+#
+skipdir()
+{
+	if(indent < 0)
+		return;
+	level := indent;
+	for(;;){
+		indent = 0;
+		fp := b.offset();
+		p := b.gets('\n');
+		if (p != nil && p[len p - 1] != '\n')
+			p += "\n";
+		lineno++;
+		if(p == nil){
+			indent = -1;
+			return;
+		}
+		for(j := 0; (c := p[j++]) != '\n';)
+			if(c == ' ')
+				indent++;
+			else if(c == '\t')
+				indent += 8;
+			else
+				break;
+		if(indent <= level){
+			b.seek(fp, 0);
+			lineno--;
+			return;
+		}
+	}
+}
+
+getfile(old: ref File): (ref File, int)
+{
+	f: ref File;
+	p, elem: string;
+	c: int;
+
+	if(indent < 0)
+		return (nil, 0);
+	fp := int b.offset();
+	do {
+		indent = 0;
+		p = b.gets('\n');
+		if (p != nil && p[len p - 1] != '\n')
+			p += "\n";
+		lineno++;
+		if(p == nil){
+			indent = -1;
+			return (nil, 0);
+		}
+		for(; (c = p[0]) != '\n'; p = p[1:])
+			if(c == ' ')
+				indent++;
+			else if(c == '\t')
+				indent += 8;
+			else
+				break;
+	} while(c == '\n' || c == '#');
+	f = ref File;
+	(elem, p) = getname(p, NAMELEN);
+	f.new = mkpath(old.new, elem);
+	(nil, f.elem) = str->splitr(f.new, "/");
+	if(f.elem == nil)
+		cmod->protoerr(ERROR, lineno, sys->sprint("can't find file name component of %s", f.new));
+	(f.mode, p) = getmode(p);
+	(f.uid, p) = getname(p, NAMELEN);
+	if(f.uid == nil)
+		f.uid = "-";
+	(f.gid, p) = getname(p, NAMELEN);
+	if(f.gid == nil)
+		f.gid = "-";
+	f.old = getpath(p);
+	if(f.old == "-")
+		f.old = nil;
+	if(f.old == nil && old.old != nil)
+		f.old = mkpath(old.old, elem);
+	setnames(f);
+	return (f, fp);
+}
+
+getpath(p: string): string
+{
+	for(; (c := p[0]) == ' ' || c == '\t'; p = p[1:])
+		;
+	for(n := 0; (c = p[n]) != '\n' && c != ' ' && c != '\t'; n++)
+		;
+	return p[0:n];
+}
+
+getname(p: string, lim: int): (string, string)
+{
+	for(; (c := p[0]) == ' ' || c == '\t'; p = p[1:])
+		;
+	i := 0;
+	s := "";
+	for(; (c = p[0]) != '\n' && c != ' ' && c != '\t'; p = p[1:])
+		s[i++] = c;
+	if(len s >= lim){
+		cmod->protoerr(WARN, lineno, sys->sprint("name %s too long; truncated", s));
+		s = s[0:lim-1];
+	}
+	if(len s > 0 && s[0] == '$'){
+		s = getenv(s[1:]);
+		if(s == nil)
+			cmod->protoerr(ERROR, lineno, sys->sprint("can't read environment variable %s", s));
+		if(len s >= NAMELEN)
+			s = s[0:NAMELEN-1];
+	}
+	return (s, p);
+}
+
+getenv(s: string): string
+{
+	if(s == "user")
+		return getuser();
+	return nil;
+}
+
+getuser(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd != nil){
+		u := array [100] of byte;
+		n := sys->read(fd, u, len u);
+		if(n > 0)
+			return string u[0:n];
+	}
+	return nil;
+}
+
+getmode(p: string): (int, string)
+{
+	s: string;
+
+	(s, p) = getname(p, 7);
+	if(s == nil || s == "-")
+		return (~0, p);
+	m := 0;
+	if(s[0] == 'd'){
+		m |= Sys->DMDIR;
+		s = s[1:];
+	}
+	if(s[0] == 'a'){
+		#m |= CHAPPEND;
+		s = s[1:];
+	}
+	if(s[0] == 'l'){
+		#m |= CHEXCL;
+		s = s[1:];
+	}
+	for(i:=0; i<len s || i < 3; i++)
+		if(i >= len s || !(s[i]>='0' && s[i]<='7')){
+		cmod->protoerr(WARN, lineno, sys->sprint("bad mode specification %s", s));
+		return (~0, p);
+	}
+	(v, nil) := str->toint(s, 8);
+	return (m|v, p);
+}
+
+mkpath(prefix, elem: string): string
+{
+	slash1 := slash2 := 0;
+	if (len prefix > 0)
+		slash1 = prefix[len prefix - 1] == '/';
+	if (len elem > 0)
+		slash2 = elem[0] == '/';
+	if (slash1 && slash2)
+		return prefix+elem[1:];
+	if (!slash1 && !slash2)
+		return prefix+"/"+elem;
+	return prefix+elem;
+}
--- /dev/null
+++ b/appl/cmd/install/proto.m
@@ -1,0 +1,6 @@
+Proto : module
+{
+	PATH : con "/dis/install/proto.dis";
+
+	rdproto: fn(proto : string, root : string, pcmod : Protocaller) : int;
+};
--- /dev/null
+++ b/appl/cmd/install/proto2list.b
@@ -1,0 +1,209 @@
+#	
+# Copyright © 2001 Vita Nuova (Holdings) Limited.  All rights reserved.	
+#
+
+implement Proto2list;
+
+# make a version list suitable for SDS from a series of proto files
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "bufio.m";
+	bufio : Bufio;
+	Iobuf : import bufio;
+include "crc.m";
+	crcm : Crc;
+include "proto.m";
+	proto : Proto;
+include "protocaller.m";
+	protocaller : Protocaller;
+
+WARN, ERROR, FATAL : import Protocaller;
+
+Proto2list: module
+{
+	init : fn(ctxt: ref Draw->Context, argv: list of string);
+	protofile: fn(new : string, old : string, d : ref Sys->Dir);
+	protoerr: fn(lev : int, line : int, err : string);
+};
+
+stderr: ref Sys->FD;
+protof: string;
+
+Element: type (string, string);
+
+List: adt{
+	as: array of Element;
+	n: int;
+	init: fn(l: self ref List);
+	add: fn(l: self ref List, e: Element);
+	end: fn(l: self ref List): array of Element;
+};
+
+flist: ref List;
+
+List.init(l: self ref List)
+{
+	l.as = array[1024] of Element;
+	l.n = 0;
+}
+
+List.add(l: self ref List, e: Element)
+{
+	if(l.n == len l.as)
+		l.as = (array[2*l.n] of Element)[0:] = l.as;
+	l.as[l.n++] = e;
+}
+
+List.end(l: self ref List): array of Element
+{
+	return l.as[0: l.n];
+}
+
+usage()
+{
+	sys->fprint(stderr, "Usage: proto2list protofile ...\n");
+	exit;
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	crcm = load Crc Crc->PATH;
+	proto = load Proto Proto->PATH;
+	protocaller = load Protocaller "$self";
+	stderr = sys->fildes(2);
+	root := "/";
+	flist = ref List;
+	flist.init();
+	for(argv = tl argv; argv != nil; argv = tl argv){
+		protof = hd argv;
+		proto->rdproto(hd argv, root, protocaller);
+	}
+	fs := flist.end();
+	sort(fs);
+	fs = uniq(fs);
+	out(fs);
+}
+
+protofile(new : string, old : string, nil : ref Sys->Dir)
+{
+	if(new == old)
+		new = "-";
+	flist.add((old, new));
+}
+
+out(fs: array of Element)
+{
+	nf := len fs;
+	for(i := 0; i < nf; i++){
+		(f, g) := fs[i];
+		(ok, d) := sys->stat(f);
+		if (ok < 0) {
+			sys->fprint(stderr, "cannot open %s\n", f);
+			continue;
+		}
+		if (d.mode & Sys->DMDIR)
+			d.length = big 0;
+		sys->print("%s	%s	%d	%d	%d	%d	%d\n", f, g, int d.length, d.mode, d.mtime, crc(f, d), 0);
+	}
+}
+
+protoerr(lev : int, line : int, err : string)
+{
+	s := "line " + string line + " : " + err;
+	case lev {
+		WARN => warn(s);
+		ERROR => error(s);
+		FATAL => fatal(s);
+	}
+}
+
+crc(f : string, d: Sys->Dir) : int
+{
+	crcs := crcm->init(0, int 16rffffffff);
+	if (d.mode & Sys->DMDIR)
+		return 0;
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil) {
+		sys->fprint(stderr, "cannot open %s\n", f);
+		return 0;
+	}
+	crc := 0;
+	buf := array[Sys->ATOMICIO] of byte;
+	for (;;) {
+		nr := sys->read(fd, buf, len buf);
+		if (nr < 0) {
+			sys->fprint(stderr, "bad read on %s : %r\n", f);
+			return 0;
+		}
+		if (nr <= 0)
+			break;
+		crc = crcm->crc(crcs, buf, nr);
+	}
+	crcm->reset(crcs);
+	return crc;
+}
+
+sort(a: array of Element)
+{
+	mergesort(a, array[len a] of Element);
+}
+	
+mergesort(a, b: array of Element)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m]);
+		mergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (b[i].t0 > b[j].t0)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+
+uniq(a: array of Element): array of Element
+{
+	m := n := len a;
+	for(i := 0; i < n-1; ){
+		if(a[i].t0 == a[i+1].t0){
+			if(a[i].t1 != a[i+1].t1)
+				warn(sys->sprint("duplicate %s(%s %s)", a[i].t0, a[i].t1, a[i+1].t1));
+			a[i+1:] = a[i+2: n--];
+		}
+		else
+			i++;
+	}
+	if(n == m)
+		return a;
+	return a[0: n];
+}
+		
+error(s: string)
+{
+	sys->fprint(stderr, "%s: %s\n", protof, s);
+	exit;
+}
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "fatal: %s\n", s);
+	exit;
+}
+ 
+warn(s: string)
+{
+	sys->fprint(stderr, "%s: %s\n", protof, s);
+}
--- /dev/null
+++ b/appl/cmd/install/protocaller.m
@@ -1,0 +1,8 @@
+Protocaller : module{
+	init: fn(ctxt : ref Draw->Context, args : list of string);
+	protofile: fn(new : string, old : string, d : ref Sys->Dir);
+
+	WARN, ERROR, FATAL : con iota;
+
+	protoerr: fn(lev : int, line : int, err : string);
+};
--- /dev/null
+++ b/appl/cmd/install/updatelog.b
@@ -1,0 +1,386 @@
+implement Updatelog;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "string.m";
+	str: String;
+
+include "keyring.m";
+	kr: Keyring;
+
+include "logs.m";
+	logs: Logs;
+	Db, Entry, Byname, Byseq: import logs;
+	S, mkpath: import logs;
+	Log: type Entry;
+
+include "fsproto.m";
+	fsproto: FSproto;
+	Direntry: import fsproto;
+
+include "arg.m";
+
+Updatelog: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+now: int;
+gen := 0;
+changesonly := 0;
+uid: string;
+gid: string;
+debug := 0;
+state: ref Db;
+rootdir := ".";
+scanonly: list of string;
+exclude: list of string;
+sums := 0;
+stderr: ref Sys->FD;
+Seen: con 1<<31;
+bout: ref Iobuf;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	ensure(bufio, Bufio->PATH);
+	fsproto = load FSproto FSproto->PATH;
+	ensure(fsproto, FSproto->PATH);
+	daytime = load Daytime Daytime->PATH;
+	ensure(daytime, Daytime->PATH);
+	str = load String String->PATH;
+	ensure(str, String->PATH);
+	logs = load Logs Logs->PATH;
+	ensure(logs, Logs->PATH);
+	kr = load Keyring Keyring->PATH;
+	ensure(kr, Keyring->PATH);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		error(sys->sprint("can't load %s: %r", Arg->PATH));
+
+	protofile := "/lib/proto/all";
+	arg->init(args);
+	arg->setusage("updatelog [-p proto] [-r root] [-t now gen] [-c] [-x path] x.log [path ...]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'D' =>
+			debug = 1;
+		'p' =>
+			protofile = arg->earg();
+		'r' =>
+			rootdir = arg->earg();
+		'c' =>
+			changesonly = 1;
+		'u' =>
+			uid = arg->earg();
+		'g' =>
+			gid = arg->earg();
+		's' =>
+			sums = 1;
+		't' =>
+			now = int arg->earg();
+			gen = int arg->earg();
+		'x' =>
+			s := arg->earg();
+			exclude = trimpath(s) :: exclude;
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	stderr = sys->fildes(2);
+	bout = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+
+	fsproto->init();
+	logs->init(bufio);
+
+	logfile := hd args;
+	while((args = tl args) != nil)
+		scanonly = trimpath(hd args) :: scanonly;
+	checkroot(rootdir, "replica root");
+
+	state = Db.new("server state");
+
+	#
+	# replay log to rebuild server state
+	#
+	logfd := sys->open(logfile, Sys->OREAD);
+	if(logfd == nil)
+		error(sys->sprint("can't open %s: %r", logfile));
+	f := bufio->fopen(logfd, Sys->OREAD);
+	if(f == nil)
+		error(sys->sprint("can't open %s: %r", logfile));
+	while((log := readlog(f)) != nil)
+		replaylog(state, log);
+
+	#
+	# walk the set of names produced by the proto file, comparing against the server state
+	#
+	now = daytime->now();
+	doproto(rootdir, protofile);
+
+	if(changesonly){
+		bout.flush();
+		exit;
+	}
+
+	#
+	# names in the original state that we didn't see in the walk must have been removed:
+	# print 'd' log entries for them, in reverse lexicographic order (children before parents)
+	#
+	state.sort(Logs->Byname);
+	for(i := state.nstate; --i >= 0;){
+		e := state.state[i];
+		if((e.x & Seen) == 0 && considered(e.path)){
+			change('d', e, e.seq, e.d, e.path, e.serverpath, e.contents);	# TO DO: content
+			if(debug)
+				sys->fprint(sys->fildes(2), "remove %q\n", e.path);
+		}
+	}
+	bout.flush();
+}
+
+ensure[T](m: T, path: string)
+{
+	if(m == nil)
+		error(sys->sprint("can't load %s: %r", path));
+}
+
+checkroot(dir: string, what: string)
+{
+	(ok, d) := sys->stat(dir);
+	if(ok < 0)
+		error(sys->sprint("can't stat %s %q: %r", what, dir));
+	if((d.mode & Sys->DMDIR) == 0)
+		error(sys->sprint("%s %q: not a directory", what, dir));
+}
+
+considered(s: string): int
+{
+	if(scanonly != nil && !islisted(s, scanonly))
+		return 0;
+	return exclude == nil || !islisted(s, exclude);
+}
+
+readlog(in: ref Iobuf): ref Log
+{
+	(e, err) := Entry.read(in);
+	if(err != nil)
+		error(err);
+	return e;
+}
+
+#
+# replay a log to reach the state wrt files previously taken from the server
+#
+replaylog(db: ref Db, log: ref Log)
+{
+	e := db.look(log.path);
+	indb := e != nil && !e.removed();
+	case log.action {
+	'a' =>	# add new file
+		if(indb){
+			note(sys->sprint("%q duplicate create", log.path));
+			return;
+		}
+	'c' =>	# contents
+		if(!indb){
+			note(sys->sprint("%q contents but no entry", log.path));
+			return;
+		}
+	'd' =>	# delete
+		if(!indb){
+			note(sys->sprint("%q deleted but no entry", log.path));
+			return;
+		}
+		if(e.d.mtime > log.d.mtime){
+			note(sys->sprint("%q deleted but it's newer", log.path));
+			return;
+		}
+	'm' =>	# metadata
+		if(!indb){
+			note(sys->sprint("%q metadata but no entry", log.path));
+			return;
+		}
+	* =>
+		error(sys->sprint("bad log entry: %bd %bd", log.seq>>32, log.seq & big 16rFFFFFFFF));
+	}
+	update(db, e, log);
+}
+
+#
+# update file state e to reflect the effect of the log,
+# creating a new entry if necessary
+#
+update(db: ref Db, e: ref Entry, log: ref Entry)
+{
+	if(e == nil)
+		e = db.entry(log.seq, log.path, log.d);
+	e.update(log);
+}
+
+doproto(tree: string, protofile: string)
+{
+	entries := chan of Direntry;
+	warnings := chan of (string, string);
+	err := fsproto->readprotofile(protofile, tree, entries, warnings);
+	if(err != nil)
+		error(sys->sprint("can't read %s: %s", protofile, err));
+	for(;;)alt{
+	(old, new, d) := <-entries =>
+		if(d == nil)
+			return;
+		if(debug)
+			sys->fprint(stderr, "old=%q new=%q length=%bd\n", old, new, d.length);
+		while(new != nil && new[0] == '/')
+			new = new[1:];
+		if(!considered(new))
+			continue;
+		if(sums && (d.mode & Sys->DMDIR) == 0)
+			digests := md5sum(old) :: nil;
+		if(uid != nil)
+			d.uid = uid;
+		if(gid != nil)
+			d.gid = gid;
+		old = relative(old, rootdir);
+		db := state.look(new);
+		if(db == nil){
+			if(!changesonly){
+				db = state.entry(nextseq(), new, *d);
+				change('a', db, db.seq, db.d, db.path, old, digests);
+			}
+		}else{
+			if(!samestat(db.d, *d))
+				change('c', db, nextseq(), *d, new, old, digests);
+			if(!samemeta(db.d, *d))
+				change('m', db, nextseq(), *d, new, old, nil);	# need digest?
+		}
+		if(db != nil)
+			db.x |= Seen;
+	(old, msg) := <-warnings =>
+		#if(contains(msg, "entry not found") || contains(msg, "not exist"))
+		#	break;
+		sys->fprint(sys->fildes(2), "updatelog: warning[old=%s]: %s\n", old, msg);
+	}
+}
+
+change(action: int, e: ref Entry, seq: big, d: Sys->Dir, path: string, serverpath: string, digests: list of string)
+{
+	log := ref Entry;
+	log.seq = seq;
+	log.action = action;
+	log.d = d;
+	log.path = path;
+	log.serverpath = serverpath;
+	log.contents = digests;
+	e.update(log);
+	bout.puts(log.logtext()+"\n");
+}
+
+samestat(a: Sys->Dir, b: Sys->Dir): int
+{
+	# doesn't check permission/ownership, does check QTDIR/QTFILE
+	if(a.mode & Sys->DMDIR)
+		return (b.mode & Sys->DMDIR) != 0;
+	return a.length == b.length && a.mtime == b.mtime && a.qid.qtype == b.qid.qtype;	# TO DO: a.name==b.name?
+}
+
+samemeta(a: Sys->Dir, b: Sys->Dir): int
+{
+	return a.mode == b.mode && (uid == nil || a.uid == b.uid) && (gid == nil || a.gid == b.gid) && samestat(a, b);
+}
+
+nextseq(): big
+{
+	return (big now << 32) | big gen++;
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "updatelog: %s\n", s);
+	raise "fail:error";
+}
+
+note(s: string)
+{
+	sys->fprint(sys->fildes(2), "updatelog: note: %s\n", s);
+}
+
+contains(s: string, sub: string): int
+{
+	return str->splitstrl(s, sub).t1 != nil;
+}
+
+isprefix(a, b: string): int
+{
+	la := len a;
+	lb := len b;
+	if(la > lb)
+		return 0;
+	if(la == lb)
+		return a == b;
+	return a == b[0:la] && b[la] == '/';
+}
+
+trimpath(s: string): string
+{
+	while(len s > 1 && s[len s-1] == '/')
+		s = s[0:len s-1];
+	while(s != nil && s[0] == '/')
+		s = s[1:];
+	return s;
+}
+
+relative(name: string, root: string): string
+{
+	if(root == nil || name == nil)
+		return name;
+	if(isprefix(root, name)){
+		name = name[len root:];
+		while(name != nil && name[0] == '/')
+			name = name[1:];
+	}
+	return name;
+}
+
+islisted(s: string, l: list of string): int
+{
+	for(; l != nil; l = tl l)
+		if(isprefix(hd l, s))
+			return 1;
+	return 0;
+}
+
+md5sum(file: string): string
+{
+	fd := sys->open(file, Sys->OREAD);
+	if(fd == nil)
+		error(sys->sprint("can't open %s: %r", file));
+	ds: ref Keyring->DigestState;
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0)
+		ds = kr->md5(buf, n, nil, ds);
+	if(n < 0)
+		error(sys->sprint("error reading %s: %r", file));
+	digest := array[Keyring->MD5dlen] of byte;
+	kr->md5(nil, 0, digest, ds);
+	s: string;
+	for(i := 0; i < len digest; i++)
+		s += sys->sprint("%.2ux", int digest[i]);
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/install/wdiff.b
@@ -1,0 +1,148 @@
+implement Wdiff;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "arg.m";
+	arg: Arg;
+include "wrap.m";
+	wrap : Wrap;
+include "sh.m";
+include "keyring.m";
+	keyring : Keyring;
+
+
+Wdiff: module{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+root := "/";
+bflag : int;
+listing : int;
+package: int;
+
+diff(w : ref Wrap->Wrapped, name : string, c : chan of int)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	wrapped := w.root+"/"+name;
+	local := root+"/"+name;
+	(ok, dir) := sys->stat(local);
+	if (ok < 0) {
+		sys->print("cannot stat %s\n", local);
+		c <-= -1;
+		return;
+	}
+	(ok, dir) = sys->stat(wrapped);
+	if (ok < 0) {
+		sys->print("cannot stat %s\n", wrapped);
+		c <-= -1;
+		return;
+	}
+	cmd := "/dis/diff.dis";
+	m := load Command cmd;
+	if(m == nil) {
+		c <-= -1;
+		return;
+	}
+	if (bflag)
+		m->init(nil, cmd :: "-b" :: wrapped :: local :: nil);
+	else
+		m->init(nil, cmd :: wrapped :: local :: nil);
+	c <-= 0;
+}
+	
+fatal(err : string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", err);
+	exit;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg = load Arg Arg->PATH;
+	keyring = load Keyring Keyring->PATH;
+	wrap = load Wrap Wrap->PATH;
+	wrap->init(bufio);
+
+	arg->init(args);
+	while ((c := arg->opt()) != 0) {
+		case c {
+			'b' =>
+				bflag = 1;
+			'l' =>
+				listing = 1;
+			'p' =>
+				package = 1;
+			'r' =>
+				root = arg->arg();
+				if (root == nil)
+					fatal("missing root name");
+			* =>
+				fatal(sys->sprint("bad argument -%c", c));
+		}
+	}
+	args = arg->argv();
+	if (args == nil || tl args != nil)
+		fatal("usage: install/wdiff [-blp] [-r root] package");
+	(ok, dir) := sys->stat(hd args);
+	if (ok < 0)
+		fatal(sys->sprint("no such file %s", hd args));
+	w := wrap->openwraphdr(hd args, root, nil, !listing);
+	if (w == nil)
+		fatal("no such package found");
+
+	if(package){
+		while(w.nu > 0 && w.u[w.nu-1].typ == wrap->UPD)
+			w.nu--;
+	}
+
+	digest := array[keyring->MD5dlen] of { * => byte 0 };
+	digest0 := array[keyring->MD5dlen] of { * => byte 0 };
+
+	# loop through each md5sum file of each package in increasing time order
+	for(i := 0; i < w.nu; i++){
+		b := bufio->open(w.u[i].dir+"/md5sum", Sys->OREAD);
+		if (b == nil)
+			fatal("md5sum file not found");
+		while ((p := b.gets('\n')) != nil) {
+			(n, lst) := sys->tokenize(p, " \t\n");
+			if (n != 2)
+				fatal("error in md5sum file");
+			p = hd lst;
+			q := root+"/"+p;
+			(ok, dir) = sys->stat(q);
+			if (ok >= 0 && (dir.mode & Sys->DMDIR))
+				continue;
+			t: int;
+			(ok, t) = wrap->getfileinfo(w, p, nil, digest0, nil);
+			if(ok < 0){
+				sys->print("cannot happen\n");
+				continue;
+			}
+			if(t != w.u[i].time)	# covered by later update
+				continue;
+			if (wrap->md5file(q, digest) < 0) {
+				sys->print("%s removed\n", p);
+				continue;
+			}
+			str := wrap->md5conv(digest);
+			str0 := wrap->md5conv(digest0);
+			# if (str == hd tl lst)
+			if(str == str0)
+				continue;
+			if (listing)
+				sys->print("%s modified\n", p);
+			else {
+				endc := chan of int;
+				spawn diff(w, p, endc);
+				<- endc;
+			}
+		}
+	}
+	wrap->end();
+}
--- /dev/null
+++ b/appl/cmd/install/wfind.b
@@ -1,0 +1,204 @@
+implement Wfind;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "arg.m";
+	arg: Arg;
+include "wrap.m";
+	wrap : Wrap;
+include "sh.m";
+include "keyring.m";
+	keyring : Keyring;
+include "readdir.m";
+	readdir : Readdir;
+
+Wfind: module{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+fatal(err : string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", err);
+	exit;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg = load Arg Arg->PATH;
+	keyring = load Keyring Keyring->PATH;
+	readdir = load Readdir Readdir->PATH;
+	wrap = load Wrap Wrap->PATH;
+	wrap->init(bufio);
+
+	pkgs: list of string;
+	indir := "/install";
+	arg->init(args);
+	while ((c := arg->opt()) != 0) {
+		case c {
+			'p' =>
+				pkg := arg->arg();
+				if (pkg == nil)
+					fatal("missing package name");
+				pkgs = pkg :: pkgs;
+			* =>
+				fatal(sys->sprint("bad argument -%c", c));
+		}
+	}
+	args = arg->argv();
+	if (args == nil)
+		fatal("usage: install/wfind [-p package ... ] file ...");
+	# (ok, dir) := sys->stat(indir);
+	# if (ok < 0)
+	#	fatal(sys->sprint("cannot open install directory %s", indir));
+	if(pkgs != nil){
+		npkgs: list of string;
+		for(pkg := pkgs; pkg != nil; pkg = tl pkg)
+			npkgs = hd pkg :: npkgs;
+		pkgs = npkgs;
+		for(pkg = pkgs; pkg != nil; pkg = tl pkg)
+			scanpkg(hd pkg, indir+"/"+hd pkg, args);
+	}
+	else
+		scanpkgs(indir, args);
+	prfiles();
+}
+
+scanpkgs(d : string, files: list of string)
+{
+	(dir, n) := readdir->init(d, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		if (dir[i].mode & Sys->DMDIR)
+			scanpkg(dir[i].name, d + "/" + dir[i].name, files);
+	}
+}
+
+scanpkg(pkg : string, d : string, files: list of string)
+{
+	# base package, updates and update packages have the name
+	# <timestamp> or <timestamp.gz>
+	(dir, n) := readdir->init(d, Readdir->NAME|Readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		f := dir[i].name;
+		l := len f;
+		if (l >= 4 && f[l-3:l] == ".gz")
+			f = f[0:l-3];
+		scanfile(f, pkg, d+"/"+dir[i].name, files);
+	}
+	w := wrap->openwrap(pkg, "/", 0);
+	if(w == nil)
+		return;
+	for(i = 0; i < w.nu; i++)
+		scanw(w, i, files, WRAP, pkg);
+}
+
+scanfile(f: string, pkg: string, d: string, files: list of string)
+{
+	f = nil;
+	# sys->print("%s	%s	%s\n", f, pkg, d);
+	w := wrap->openwraphdr(d, "/", nil, 0);
+	if(w == nil)
+		return;
+	if(w.nu != 1)
+		fatal("strange package: more than one piece");
+	# sys->print("	%s %d %s %d %d %d\n", w.name, w.tfull, w.u[0].desc, w.u[0].time, w.u[0].utime, w.u[0].typ);
+	scanw(w, 0, files, INSTALL, pkg);
+}
+
+scanw(w: ref Wrap->Wrapped, i: int, files: list of string, where: int, pkg: string)
+{
+	w.u[i].bmd5.seek(big 0, Bufio->SEEKSTART);
+	while ((p := w.u[i].bmd5.gets('\n')) != nil){
+		# sys->print("%s", p);
+		(n, l) := sys->tokenize(p, " \n");
+		if(n != 2)
+			fatal(sys->sprint("bad md5 file in %s\n", wtype(where)+"/"+w.name+"/"+wrap->now2string(w.u[i].time, 0)));
+		file := hd l;
+		md5 := hd tl l;
+		for(fs := files; fs != nil; fs = tl fs){
+			if(strsuffix(file, hd fs)){
+				# sys->print("%s %s %s %d\n", pkg, file, md5, where);
+				addfile(file, w, i, md5, where, pkg);
+			}
+		}
+	}
+}
+
+Stat: adt{
+	name: string;
+	occs: list of (ref Wrap->Wrapped, int, string, int, string);
+	md5: string;
+};
+
+stats: list of ref Stat;
+	
+addfile(file: string, w: ref Wrap->Wrapped, i: int, md5: string, where: int, pkg: string)
+{
+	for(sts := stats; sts != nil; sts = tl sts){
+		st := hd sts;
+		if(st.name == file){
+			st.occs = (w, i, md5, where, pkg) :: st.occs;
+			return;
+		}
+	}
+	digest := array[keyring->MD5dlen] of { * => byte 0 };
+	if (wrap->md5file(file, digest) < 0)
+		str := "non-existent"+blanks(32-12);
+	else
+		str = wrap->md5conv(digest);
+	st := ref Stat;
+	st.name = file;
+	st.occs = (w, i, md5, where, pkg) :: nil;
+	st.md5 = str;
+	stats = st :: stats;
+}
+
+prfiles()
+{
+	for(sts := stats; sts != nil; sts = tl sts){
+		st := hd sts;
+		sys->print("%s\n", st.name);
+		proccs(st.occs);
+		sys->print("\t%s %s\n", st.md5, st.name);
+	}
+}
+
+proccs(ocs: list of (ref Wrap->Wrapped, int, string, int, string))
+{
+	if(ocs != nil){
+		proccs(tl ocs);
+		(w, i, md5, where, pkg) := hd ocs;
+		sys->print("\t%s %s/%s(%s)\t%s\n", md5, w.name, wrap->now2string(w.u[i].time, 0), ptype(w.u[i].typ), wtype(where)+"/"+pkg);
+	}
+}
+		
+ptype(p: int): string
+{
+	return (array[] of { "???", "package ", "update  ", "full upd" })[p];
+}
+
+INSTALL: con 0;
+WRAP: con 1;
+
+wtype(w: int): string
+{
+	return (array[] of { "/install", "/wrap" })[w];
+}
+
+strsuffix(s: string, suf: string): int
+{
+	return (l1 := len s) >= (l2 := len suf) && s[l1-l2: l1] == suf;
+}
+
+blanks(n: int): string
+{
+	s := "";
+	for(i := 0; i < n; i++)
+		s += " ";
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/install/wrap.b
@@ -1,0 +1,684 @@
+implement Wrap;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "bufio.m";
+	bufio : Bufio;
+	Iobuf : import bufio;
+include "keyring.m";
+	keyring : Keyring;
+include "sh.m";
+include "arch.m";
+	arch : Arch;
+include "wrap.m";
+include "archfs.m";
+
+archpid := -1;
+gzfd: ref Sys->FD;
+gzfile: string;
+
+init(bio: Bufio)
+{
+	sys = load Sys Sys->PATH;
+	if(bio == nil)
+		bufio = load Bufio Bufio->PATH;
+	else
+		bufio = bio;
+	keyring = load Keyring Keyring->PATH;
+	arch = load Arch Arch->PATH;
+	arch->init(bufio);
+}
+
+end()
+{
+	if(gzfile != nil)
+		sys->remove(gzfile);
+	if (archpid > 0){
+		fd := sys->open("#p/" + string archpid + "/ctl", sys->OWRITE);
+		if (fd != nil)
+			sys->fprint(fd, "killgrp");
+	}
+}
+ 
+archfs(f : string, mtpt : string, all : int, c : chan of int)
+{
+	sys->pctl(Sys->NEWPGRP, nil);
+	cmd := "/dis/install/archfs.dis";
+	m := load Archfs Archfs->PATH;
+	if(m == nil) {
+		c <-= -1;
+		return;
+	}
+	ch := chan of int;
+	if (all)
+		spawn m->initc(cmd :: "-m" :: mtpt :: f :: nil, ch);
+	else
+		spawn m->initc(cmd :: "-s" :: "-m" :: mtpt :: f :: "/wrap" :: nil, ch);
+	pid := <- ch;
+	c <-= pid;
+}
+
+mountarch(f : string, mtpt : string, all : int) : int
+{
+	c := chan of int;
+	spawn archfs(f, mtpt, all, c);
+	pid := <- c;
+	if (pid < 0) {
+		if(pid == -1)
+			sys->fprint(sys->fildes(2), "fatal: cannot run archfs\n");
+		# else probably not an archive file
+		return -1;
+	}
+	archpid = pid;
+	return 0;
+}
+	
+openmount(f : string, d : string) : ref Wrapped
+{
+	if (f == nil) {
+		p := d+"/wrap";
+		f = getfirstdir(p);
+		if (f == nil)
+			return nil;
+	}
+	w := ref Wrapped;
+	w.name = f;
+	w.root = d;
+	# p := d + "/wrap/" + f;
+	p := pathcat(d, pathcat("wrap", f));
+	(w.u, w.nu, w.tfull) = openupdate(p);
+	if (w.nu < 0) {
+		closewrap(w);
+		return nil;
+	}
+	return w;
+}
+
+closewrap(w : ref Wrapped)
+{
+	w = nil;
+}
+
+openwraphdr(f : string, d : string, argl : list of string, all : int) : ref Wrapped
+{
+	argl = nil;
+	(ok, dir) := sys->stat(f);
+	if (ok < 0 || dir.mode & Sys->DMDIR)
+		return openwrap(f, d, all);
+	(nf, fd) := arch->openarchgz(f);
+	if (nf != nil) {
+		gzfile = nf;
+		f = nf;
+		gzfd = fd;
+	}
+	return openwrap(f, "/mnt/wrap", all);
+}
+
+openwrap(f : string, d : string, all : int) : ref Wrapped
+{
+	if (d == nil)
+		d = "/";
+	if((w := openmount(f, d)) != nil)
+		return w;		# don't mess about if /wrap/ structure exists
+	(ok, dir) := sys->stat(f);
+	if (ok < 0)
+		return nil;
+	# accept root/ or root/wrap/pkgname
+	if (dir.mode & Sys->DMDIR) {
+		d = f;
+		if ((i := strstr(f, "/wrap/")) >= 0) {
+			f = f[i+6:];
+			d = d[0:i+6];
+		}
+		else
+			f = nil;
+		return openmount(f, d);
+	}
+	(ok, dir) = sys->stat(f);
+	if (ok < 0 || dir.mode & Sys->DMDIR)
+		return openmount(f, d);		# ?
+	if (mountarch(f, d, all) < 0)
+		return nil;
+	return openmount(nil, d);
+}
+
+getfirstdir(d : string) : string
+{
+	if ((fd := sys->open(d, Sys->OREAD)) == nil)
+		return nil;
+	for(;;){
+		(n, dir) := sys->dirread(fd);
+		if(n <= 0)
+			break;
+		for(i:=0; i<n; i++)
+			if(dir[i].mode & Sys->DMDIR)
+				return dir[i].name;
+	}
+	return nil;
+}
+
+NONE : con 0;
+
+sniffdir(base : string, elem : string) : (int, int)
+{
+	# t := int elem;
+	t := string2now(elem, 0);
+	if (t == 0)
+		return (NONE, 0);
+	# buf := sys->sprint("%ud", t);
+	# if (buf != elem)
+	#	return (NONE, 0);
+	rv := NONE;
+	p := base + "/" + elem + "/package";
+	(ok, nil) := sys->stat(p);
+	if (ok >= 0)
+		rv |= FULL;
+	p = base + "/" + elem + "/update";
+	(ok, nil) = sys->stat(p);
+	if (ok >= 0)
+		rv |= UPD;
+	return (rv, t);
+}
+
+openupdate(d : string) : (array of Update, int, int)
+{
+	u : array of Update;
+
+	if ((fd := sys->open(d, Sys->OREAD)) == nil)
+		return (nil, -1, 0);
+	#
+	# We are looking to find the most recent full
+	# package; anything before that is irrelevant.
+	# Also figure out the most recent package update.
+	# Non-package updates before that are irrelevant.
+	# If there are no packages installed, 
+	# grab all the updates we can find.
+	#
+	tbase := -1;
+	tfull := -1;
+	nu := 0;
+	for(;;){
+		(n, dir) := sys->dirread(fd);
+		if(n <= 0)
+			break;
+		for(i := 0; i < n; i++){
+			(k, t) := sniffdir(d, dir[i].name);
+			case (k) {
+				FULL =>
+					nu++;
+					if (t > tfull)
+						tfull = t;
+					if (t > tbase)
+						tbase = t;
+				FULL|UPD =>
+					nu++;
+					if (t > tfull)
+						tfull = t;
+				UPD =>
+					nu++;
+			}
+		}
+	}
+	if (nu == 0)
+		return (nil, -1, 0);
+	u = nil;
+	nu = 0;
+	if ((fd = sys->open(d, Sys->OREAD)) == nil)
+		return (nil, -1, 0);
+	for(;;){
+		(n, dir) := sys->dirread(fd);
+		if(n <= 0)
+			break;
+		for(i := 0; i < n; i++){
+			(k, t) := sniffdir(d, dir[i].name);
+			if (k == 0)
+				continue;
+			if (t < tbase)
+				continue;
+			if (t < tfull && k == UPD)
+				continue;
+			if (nu%8 == 0) {
+				newu := array[nu+8] of Update;
+				newu[0:] = u[0:nu];
+				u = newu;
+			}
+			u[nu].typ = k;
+			if (readupdate(u, nu, d, dir[i].name) != nil)
+				nu++;
+		}
+	}
+	if (nu == 0)
+		return (nil, -1, 0);
+	qsort(u, nu);
+	return (u, nu, tfull);
+}
+
+readupdate(u : array of Update, ui : int, base : string, elem : string) : array of Update
+{
+	# u[ui].dir = base + "/" + elem;
+	u[ui].dir = pathcat(base, elem);
+	p := u[ui].dir + "/desc";
+	u[ui].desc = readfile(p);
+	# u[ui].time = int elem;
+	u[ui].time = string2now(elem, 0);
+	p = u[ui].dir + "/md5sum";
+	u[ui].bmd5 = bufio->open(p, Bufio->OREAD);
+	p = u[ui].dir + "/update";
+	q := readfile(p);
+	if (q != nil)
+		u[ui].utime = int q;
+	else
+		u[ui].utime = 0;
+	if (u[ui].bmd5 == nil)
+		return nil;
+	return u;
+}
+
+readfile(s : string) : string
+{
+	(ok, d) := sys->stat(s);
+	if (ok < 0)
+		return nil;
+	buf := array[int d.length] of byte;
+	if ((fd := sys->open(s, Sys->OREAD)) == nil || sys->read(fd, buf, int d.length) != int d.length)
+		return nil;
+	s = string buf;
+	ls := len s;
+	if (s[ls-1] == '\n')
+		s = s[0:ls-1];
+	return s;
+}
+	
+hex(c : int) : int
+{
+	if (c >= '0' && c <= '9')
+		return c-'0';
+	if (c >= 'a' && c <= 'f')
+		return c-'a'+10;
+	if (c >= 'A' && c <= 'F')
+		return c-'A'+10;
+	return -1;
+}
+
+getfileinfo(w : ref Wrapped, f : string, rdigest : array of byte, wdigest : array of byte, ardigest: array of byte) : (int, int)
+{
+	p : string;
+
+	if (w == nil)
+		return (-1, 0);
+	digest := array[keyring->MD5dlen] of { * => byte 0 };
+	for (i := w.nu-1; i >= 0; i--){
+		if ((p = bsearch(w.u[i].bmd5, f)) == nil)
+			continue;
+		if (p == nil)
+			continue;
+		k := 0;
+		while (k < len p && p[k] != ' ')
+			k++;
+		if (k == len p)
+			continue;
+		q := p[k+1:];
+		if (q == nil)
+			continue;
+		if (len q != 2*Keyring->MD5dlen+1)
+			continue;
+		for (j := 0; j < Keyring->MD5dlen; j++) {
+			a := hex(q[2*j]);
+			b := hex(q[2*j+1]);
+			if (a < 0 || b < 0)
+				break;
+			digest[j] = byte ((a<<4)|b);
+		}
+		if(j != Keyring->MD5dlen)
+			continue;
+		if(rdigest == nil || memcmp(rdigest, digest, keyring->MD5dlen) == 0 || (ardigest != nil && memcmp(ardigest, digest, keyring->MD5dlen) == 0))
+			break;
+		else
+			return (-1, 0);	# NEW
+	}
+	if(i < 0)
+		return (-1, 0);
+	if(wdigest != nil)
+		wdigest[0:] = rdigest;
+	return (0, w.u[i].time);
+		
+	
+}
+
+bsearch(b : ref Bufio->Iobuf, p : string) : string
+{
+	if (b == nil)
+		return nil;
+	lo := 0;
+	b.seek(big 0, Bufio->SEEKEND);
+	hi := int b.offset();
+	l := len p;
+	while (lo < hi) {
+		m := (lo+hi)/2;
+		b.seek(big m, Bufio->SEEKSTART);
+		b.gets('\n');
+		if (int b.offset() == hi) {
+			bgetbackc(b);
+			m = int b.offset();
+			while (m-- > lo) {
+				if (bgetbackc(b) == '\n') {
+					b.getc();
+					break;
+				}
+			}
+		}
+		s := b.gets('\n');
+		if (len s >= l+1 && s[0:l] == p && (s[l] == ' ' || s[l] == '\n'))
+			return s;
+		if (s < p)
+			lo = int b.offset();
+		else
+			hi = int b.offset()-len s;
+	}
+	return nil;
+}
+
+bgetbackc(b : ref Bufio->Iobuf) : int
+{
+	m := int b.offset();
+	b.seek(big (m-1), Bufio->SEEKSTART);
+	c := b.getc();
+	b.ungetc();
+	return c;
+}
+
+strstr(s : string, p : string) : int
+{
+	lp := len p;
+	ls := len s;
+	for (i := 0; i < ls-lp; i++)
+		if (s[i:i+lp] == p)
+			return i;
+	return -1;
+}
+
+qsort(a : array of Update, n : int)
+{
+	i, j : int;
+	t : Update;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && a[i].time < a[0].time);
+			do
+				j--;
+			while(j > 0 && a[j].time > a[0].time);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsort(a, j);
+			a = a[j+1:];
+		} else {
+			qsort(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+md5file(file : string, digest : array of byte) : int
+{
+	(ok, d) := sys->stat(file);
+	if (ok < 0)
+		return -1;
+	if (d.mode & Sys->DMDIR)
+		return 0;
+	bio := bufio->open(file, Bufio->OREAD);
+	if (bio == nil)
+		return -1;
+	# return md5sum(bio, digest, d.length);
+	buff := array[Sys->ATOMICIO] of byte;
+	ds := keyring->md5(nil, 0, nil, nil);
+	while ((n := bio.read(buff, len buff)) > 0)
+		keyring->md5(buff, n, nil, ds);
+	keyring->md5(nil, 0, digest, ds);
+	bio = nil;
+	return 0;
+}
+
+md5sum(b : ref Iobuf, digest : array of byte, leng : int) : int
+{
+	ds := keyring->md5(nil, 0, nil, nil);
+	buff := array[Sys->ATOMICIO] of byte;
+	while (leng > 0) {
+		if (leng > len buff)
+			n := len buff;
+		else
+			n = leng;
+		if ((n = b.read(buff, n)) <= 0)
+			return -1;
+		keyring->md5(buff, n, nil, ds);
+		leng -= n;
+	}
+	keyring->md5(nil, 0, digest, ds);
+	return 0;
+}
+		
+md5conv(d : array of byte) : string
+{
+	s : string = nil;
+
+	for (i := 0; i < keyring->MD5dlen; i++)
+		s += sys->sprint("%.2ux", int d[i]);
+	return s;
+}	
+
+zd : Sys->Dir;
+
+newd(time : int, uid : string, gid : string) : ref Sys->Dir
+{
+	d := ref Sys->Dir;
+	*d = zd;
+	d.uid = uid;
+	d.gid = gid;
+	d.mtime = time;
+	return d;
+}
+
+putwrapfile(b : ref Iobuf, name : string, time : int, elem : string, file : string, uid : string, gid : string)
+{
+	d := newd(time, uid, gid);
+	d.mode = 8r444;
+	(ok, dir) := sys->stat(file);
+	if (ok < 0)
+		sys->fprint(sys->fildes(2), "cannot stat %s: %r", file);
+	d.length = dir.length;
+	# s := "/wrap/"+name+"/"+sys->sprint("%ud", time)+"/"+elem;
+	s := "/wrap/"+name+"/"+now2string(time, 0)+"/"+elem;
+	arch->puthdr(b, s, d);
+	arch->putfile(b, file, int d.length);
+}
+
+putwrap(b : ref Iobuf, name : string, time : int, desc : string, utime : int, pkg : int, uid : string, gid : string)
+{
+	if (!(utime || pkg))
+		sys->fprint(sys->fildes(2), "bad precondition in putwrap()");
+	d := newd(time, uid, gid);
+	d.mode = Sys->DMDIR|8r775;
+	s := "/wrap";
+	arch->puthdr(b, s, d);
+	s += "/"+name;
+	arch->puthdr(b, s, d);
+	# s += "/"+sys->sprint("%ud", time);
+	s += "/"+now2string(time, 0);
+	arch->puthdr(b, s, d);
+	d.mode = 8r444;
+	s += "/";
+	dir := s;
+	if (utime) {
+		s = dir+"update";
+		d.length = big 23;
+		arch->puthdr(b, s, d);
+		arch->putstring(b, sys->sprint("%22ud\n", utime));
+	}
+	if (pkg) {
+		s = dir+"package";
+		d.length = big 0;
+		arch->puthdr(b, s, d);
+	}
+	if (desc != nil) {
+		s = dir+"desc";
+		d.length = big (len desc+1);
+		d.mode = 8r444;
+		arch->puthdr(b, s, d);
+		arch->putstring(b, desc+"\n");
+	}
+}
+
+memcmp(b1, b2 : array of byte, n : int) : int
+{
+	for (i := 0; i < n; i++)
+		if (b1[i] < b2[i])
+			return -1;
+		else if (b1[i] > b2[i])
+			return 1;
+	return 0;
+}
+
+strprefix(s: string, pre: string): int
+{
+	return len s >= (l := len pre) && s[0:l] == pre;
+}
+
+match(s: string, pre: list of string): int
+{
+	if(pre == nil || s == "/wrap" || strprefix(s, "/wrap/"))
+		return 1;
+	for( ; pre != nil; pre = tl pre)
+		if(strprefix(s, hd pre))
+			return 1;
+	return 0;
+}
+
+notmatch(s: string, pre: list of string): int
+{
+	if(pre == nil || s == "/wrap" || strprefix(s, "/wrap/"))
+		return 1;
+	for( ; pre != nil; pre = tl pre)
+		if(strprefix(s, hd pre))
+			return 0;
+	return 1;
+}
+
+pathcat(s : string, t : string) : string
+{
+	if (s == nil) return t;
+	if (t == nil) return s;
+	slashs := s[len s - 1] == '/';
+	slasht := t[0] == '/';
+	if (slashs && slasht)
+		return s + t[1:];
+	if (!slashs && !slasht)
+		return s + "/" + t;
+	return s + t;
+}
+
+md5filea(file : string, digest : array of byte) : int
+{
+	n, n0: int;
+
+	(ok, d) := sys->stat(file);
+	if (ok < 0)
+		return -1;
+	if (d.mode & Sys->DMDIR)
+		return 0;
+	bio := bufio->open(file, Bufio->OREAD);
+	if (bio == nil)
+		return -1;
+	buff := array[Sys->ATOMICIO] of byte;
+	m := len buff;
+	ds := keyring->md5(nil, 0, nil, nil);
+	r := 0;
+	while(1){
+		if(r){
+			if((n = bio.read(buff[1:], m-1)) <= 0)
+				break;
+			n++;
+		}
+		else{
+			if ((n = bio.read(buff, m)) <= 0)
+				break;
+		}
+		(n0, r) = remcr(buff, n);
+		if(r){
+			keyring->md5(buff, n0-1, nil, ds);
+			buff[0] = byte '\r';
+		}
+		else
+			keyring->md5(buff, n0, nil, ds);
+	}
+	if(r)
+		keyring->md5(buff, 1, nil, ds);
+	keyring->md5(nil, 0, digest, ds);
+	bio = nil;
+	return 0;
+}
+
+remcr(b: array of byte, n: int): (int, int)
+{
+	if(n == 0)
+		return (0, 0);
+	for(i := 0; i < n; ){
+		if(b[i] == byte '\r' && i+1 < n && b[i+1] == byte '\n')
+			b[i:] = b[i+1:n--];
+		else
+			i++;
+	}
+	return (n, b[n-1] == byte '\r');
+}
+
+TEN2EIGHT: con 100000000;
+
+now2string(n: int, flag: int): string
+{
+	if(flag == 0)
+		return sys->sprint("%ud", n);
+	if(n < 0)
+		return nil;
+	q := n/TEN2EIGHT;
+	s := "0" +  string (n-TEN2EIGHT*q);
+	while(len s < 9)
+		s = "0" + s;
+	if(q <= 9)
+		s[0] = '0' + q - 0;
+	else if(q <= 21)
+		s[0] = 'A' + q - 10;
+	else
+		return nil;
+	return s;
+}
+
+string2now(s: string, flag: int): int
+{
+	if(flag == 0 && s[0] != 'A')
+		return int s;
+	if(len s != 9)
+		return 0;
+	r := int s[1: ];
+	c := s[0];
+	if(c >= '0' && c <= '9')
+		q := c - '0' + 0;
+	else if(c >= 'A' && c <= 'L')
+		q = c - 'A' + 10;
+	else
+		return 0;
+	n := TEN2EIGHT*q + r;
+	if(n < 0)
+		return 0;
+	return n;
+}
--- /dev/null
+++ b/appl/cmd/install/wrap.m
@@ -1,0 +1,41 @@
+Wrap : module
+{
+	PATH : con "/dis/install/wrap.dis";
+
+	FULL, UPD : con iota+1;
+
+	Update : adt {
+		desc : string;
+		dir : string;
+		time : int;
+		utime : int;
+		bmd5 : ref Bufio->Iobuf;
+		typ : int;
+	};
+
+	Wrapped : adt {
+		name : string;
+		root : string;
+		tfull : int;
+		u : array of Update;
+		nu : int;
+	};
+
+	init: fn(bio: Bufio);
+	openwrap: fn(f : string, d : string, all : int) : ref Wrapped;
+	openwraphdr: fn(f : string, d : string, argl : list of string, all : int) : ref Wrapped;
+	getfileinfo: fn(w : ref Wrapped, f : string, rdigest : array of byte, wdigest: array of byte, ardigest: array of byte) : (int, int);
+	putwrapfile: fn(b : ref Bufio->Iobuf, name : string, time : int, elem : string, file : string, uid : string, gid : string);
+	putwrap: fn(b : ref Bufio->Iobuf, name : string, time : int, desc : string, utime : int, pkg : int, uid : string, gid : string);
+	md5file: fn(file : string, digest : array of byte) : int;
+	md5filea: fn(file : string, digest : array of byte) : int;
+	md5sum: fn(b : ref Bufio->Iobuf, digest : array of byte, leng : int) : int;
+	md5conv: fn(d : array of byte) : string;
+	# utilities
+	match: fn(s: string, pre: list of string): int;
+	notmatch: fn(s: string, pre: list of string): int;
+	memcmp: fn(b1, b2: array of byte, n: int): int;
+	end: fn();
+	now2string: fn(n: int, flag: int): string;
+	string2now: fn(s: string, flag: int): int;
+};
--- /dev/null
+++ b/appl/cmd/install/wrap2list.b
@@ -1,0 +1,305 @@
+#	
+# Copyright © 2001 Vita Nuova (Holdings) Limited.  All rights reserved.	
+#
+
+implement Wrap2list;
+
+# make a version list suitable for SDS from /wrap
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "bufio.m";
+	bufio : Bufio;
+	Iobuf : import bufio;
+include "crc.m";
+	crcm : Crc;
+include "wrap.m";
+	wrap: Wrap;
+
+Wrap2list: module
+{
+	init : fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+HASHSZ: con 64;
+
+Element: type string;
+
+Hash: adt{
+	elems: array of Element;
+	nelems: int;
+};
+
+List: adt{
+	tabs: array of ref Hash;
+	init: fn(l: self ref List);
+	add: fn(l: self ref List, e: Element);
+	subtract: fn(l: self ref List, e: Element);
+	end: fn(l: self ref List): array of Element;
+};
+
+flist: ref List;
+
+hash(s: string): int
+{
+	h := 0;
+	n := len s;
+	for(i := 0; i < n; i++)
+		h += s[i];
+	if(h < 0)
+		h = -h;
+	return h%HASHSZ;
+}
+
+List.init(l: self ref List)
+{
+	ts := l.tabs = array[HASHSZ] of ref Hash;
+	for(i := 0; i < HASHSZ; i++){
+		t := ts[i] = ref Hash;
+		t.elems = array[HASHSZ] of Element;
+		t.nelems = 0;
+	}
+}
+
+List.add(l: self ref List, e: Element)
+{
+	h := hash(e);
+	t := l.tabs[h];
+	n := t.nelems;
+	es := t.elems;
+	for(i := 0; i < n; i++){
+		if(e == es[i])
+			return;
+	}
+	if(n == len es)
+		es = t.elems = (array[2*n] of Element)[0:] = es;
+	es[t.nelems++] = e;
+# sys->print("+ %s\n", e);
+}
+
+List.subtract(l: self ref List, e: Element)
+{
+	h := hash(e);
+	t := l.tabs[h];
+	n := t.nelems;
+	es := t.elems;
+	for(i := 0; i < n; i++){
+		if(e == es[i]){
+			es[i] = nil;
+			break;
+		}
+	}
+# sys->print("- %s\n", e);
+}
+
+List.end(l: self ref List): array of Element
+{
+	tot := 0;
+	ts := l.tabs;
+	for(i := 0; i < HASHSZ; i++)
+		tot += ts[i].nelems;
+	a := array[tot] of Element;
+	m := 0;
+	for(i = 0; i < HASHSZ; i++){
+		t := ts[i];
+		n := t.nelems;
+		es := t.elems;
+		a[m:] = es[0: n];
+		m += n;
+	}
+	return a;
+}
+
+usage()
+{
+	sys->fprint(stderr, "Usage: wrap2list [ file ... ]\n");
+	exit;
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	crcm = load Crc Crc->PATH;
+	wrap = load Wrap Wrap->PATH;
+	wrap->init(bufio);
+	if(argv != nil)
+		argv = tl argv;
+	init := 0;
+	if(argv != nil && hd argv == "-i"){
+		init = 1;
+		argv = tl argv;
+	}
+	stderr = sys->fildes(2);
+	# root := "/";
+	flist = ref List;
+	flist.init();
+	fd := sys->open("/wrap", Sys->OREAD);
+	for(;;){
+		(nd, d) := sys->dirread(fd);
+		if(nd <= 0)
+			break;
+		for(i:=0; i<nd; i++){
+			if((d[0].mode & Sys->DMDIR) && (w := wrap->openwrap(d[i].name, "/", 1)) != nil){
+				# sys->fprint(stderr, "%s %s %d %d\n", w.name, w.root, w.tfull, w.nu);
+				for(j := 0; j < w.nu; j++){
+					addfiles(w.u[j].bmd5);
+					if((b := bufio->open(w.u[j].dir+"/remove", Bufio->OREAD)) != nil)
+						subtractfiles(b);
+					# sys->fprint(stderr, "%d: %s %s %d %d %d\n", i, w.u[j].desc, w.u[j].dir, w.u[j].time, w.u[j].utime, w.u[j].typ);
+				}
+			}
+		}
+	}
+	for( ; argv != nil; argv = tl argv){
+		if((b := bufio->open(hd argv, Bufio->OREAD)) != nil)
+			addfiles(b);
+	}
+	out(uniq(rmnil(sort(flist.end()))), init);
+}
+
+addfiles(b: ref Bufio->Iobuf)
+{
+	b.seek(big 0, Bufio->SEEKSTART);
+	while((s := b.gets('\n')) != nil){
+		(n, l) := sys->tokenize(s, " \n");
+		if(n > 0)
+			flist.add(hd l);
+	}
+}
+
+subtractfiles(b: ref Bufio->Iobuf)
+{
+	b.seek(big 0, Bufio->SEEKSTART);
+	while((s := b.gets('\n')) != nil){
+		(n, l) := sys->tokenize(s, " \n");
+		if(n > 0)
+			flist.subtract(hd l);
+	}
+}
+
+out(fs: array of Element, init: int)
+{
+	nf := len fs;
+	for(i := 0; i < nf; i++){
+		f := fs[i];
+		outl(f, nil, init);
+		l := len f;
+		if(l >= 7 && f[l-7:] == "emu.new"){
+			g := f;
+			f[l-3] = 'e';
+			f[l-2] = 'x';
+			f[l-1] = 'e';
+			outl(f, g, init);		# try emu.exe
+			outl(f[0: l-4], g, init);	# try emu
+# sys->fprint(sys->fildes(2), "%s %s\n", f, g);
+		}
+	}
+}
+
+outl(f: string, g: string, init: int)
+{
+	(ok, d) := sys->stat(f);
+	if(ok < 0){
+		# sys->fprint(stderr, "cannot open %s\n", f);
+		return;
+	}
+	if(g == nil)
+		g = "-";
+	if(d.mode & Sys->DMDIR)
+		d.length = big 0;
+	if(init)
+		mtime := 0;
+	else
+		mtime = d.mtime;
+	sys->print("%s	%s	%d	%d	%d	%d	%d\n", f, g, int d.length, d.mode, mtime, crc(f, d), 0);
+}
+
+crc(f: string, d: Sys->Dir): int
+{
+	crcs := crcm->init(0, int 16rffffffff);
+	if(d.mode & Sys->DMDIR)
+		return 0;
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil){
+		sys->fprint(stderr, "cannot open %s\n", f);
+		return 0;
+	}
+	crc := 0;
+	buf := array[Sys->ATOMICIO] of byte;
+	for(;;){
+		nr := sys->read(fd, buf, len buf);
+		if(nr < 0){
+			sys->fprint(stderr, "bad read on %s : %r\n", f);
+			return 0;
+		}
+		if(nr <= 0)
+			break;
+		crc = crcm->crc(crcs, buf, nr);
+	}
+	crcm->reset(crcs);
+	return crc;
+}
+
+sort(a: array of Element): array of Element
+{
+	qsort(a, len a);
+	return a;
+}
+
+rmnil(a: array of Element): array of Element
+{
+	n := len a;
+	for(i := 0; i < n; i++)
+		if(a[i] != nil)
+			break;
+	return a[i: n];
+}
+
+uniq(a: array of Element): array of Element
+{
+	n := len a;
+	for(i := 0; i < n-1; ){
+		if(a[i] == a[i+1])
+			a[i+1:] = a[i+2: n--];
+		else
+			i++;
+	}
+	return a[0: n];
+}
+
+qsort(a: array of Element, n: int)
+{
+	i, j: int;
+	t: Element;
+
+	while(n > 1){
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;){
+			do
+				i++;
+			while(i < n && a[i] < a[0]);
+			do
+				j--;
+			while(j > 0 && a[j] > a[0]);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n){
+			qsort(a, j);
+			a = a[j+1:];
+		}else{
+			qsort(a[j+1:], n);
+			n = j;
+		}
+	}
+}
--- /dev/null
+++ b/appl/cmd/iostats.b
@@ -1,0 +1,635 @@
+implement Iostats;
+
+#
+# iostats - gather file system access statistics
+#
+
+include "sys.m";
+	sys: Sys;
+	Qid: import sys;
+
+include "draw.m";
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg, NOFID, NOTAG: import styx;
+
+include "workdir.m";
+	workdir: Workdir;
+
+include "sh.m";
+
+include "arg.m";
+
+Iostats: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Maxmsg: con 128*1024+Styx->IOHDRSZ;
+Ns2ms: con big 1000000;
+
+Rpc: adt
+{
+	name:	string;
+	count:	big;
+	time:		big;
+	lo:	big;
+	hi:	big;
+	bin:		big;
+	bout:	big;
+};
+
+Stats: adt
+{
+	totread:	big;
+	totwrite:	big;
+	nrpc:	int;
+	nproto:	int;
+	rpc:		array of ref Rpc;	# Maxrpc
+};
+
+Fid: adt {
+	nr:	int;	# fid number
+	path:		ref Path;	# path used to open Fid
+	qid:		Qid;
+	mode:	int;
+	nread:	big;
+	nwrite:	big;
+	bread:	big;
+	bwrite:	big;
+	offset:	big;	# for directories
+};
+
+Path: adt {
+	parent:	cyclic ref Path;
+	name:	string;
+};
+
+Frec: adt
+{
+	op:	ref Path;	# first name?
+	qid:	Qid;
+	nread:	big;
+	nwrite:	big;
+	bread:	big;
+	bwrite:	big;
+	opens:	int;
+};
+
+Tag: adt {
+	m: 		ref Tmsg;
+	fid:		ref Fid;
+	stime:	big;
+	next: 	cyclic ref Tag;
+};
+
+NTAGHASH: con 1<<4;	# power of 2
+NFIDHASH: con 1<<4;	# power of 2
+
+tags := array[NTAGHASH] of ref Tag;
+fids := array[NFIDHASH] of list of ref Fid;
+dbg := 0;
+
+stats: Stats;
+frecs:	list of ref Frec;
+
+replymap := array[tagof Rmsg.Stat+1] of {
+	tagof Rmsg.Version => tagof Tmsg.Version,
+	tagof Rmsg.Auth => tagof Tmsg.Auth,
+	tagof Rmsg.Attach => tagof Tmsg.Attach,
+	tagof Rmsg.Flush => tagof Tmsg.Flush,
+	tagof Rmsg.Clunk => tagof Tmsg.Clunk,
+	tagof Rmsg.Remove => tagof Tmsg.Remove,
+	tagof Rmsg.Wstat => tagof Tmsg.Wstat,
+	tagof Rmsg.Walk => tagof Tmsg.Walk,
+	tagof Rmsg.Create => tagof Tmsg.Create,
+	tagof Rmsg.Open => tagof Tmsg.Open,
+	tagof Rmsg.Read => tagof Tmsg.Read,
+	tagof Rmsg.Write => tagof Tmsg.Write,
+	tagof Rmsg.Stat => tagof Tmsg.Stat,
+	* => -1,
+};
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	workdir = load Workdir Workdir->PATH;
+	sh := load Sh Sh->PATH;
+	styx = load Styx Styx->PATH;
+	styx->init();
+
+	wd := workdir->init();
+
+	dbfile := "iostats.out";
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("iostats [-d] [-f debugfile] cmds [args ...]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' =>	dbg++;
+		'f' =>		dbfile = arg->earg();
+		* =>		arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	sys->pctl(Sys->FORKFD|Sys->FORKNS|Sys->NEWPGRP|Sys->FORKENV, nil);
+
+	if(dbg){
+		fd := sys->create(dbfile, Sys->OWRITE, 8r666);
+		if(fd == nil)
+			fatal(sys->sprint("can't create %q: %r", dbfile));
+		sys->dup(fd.fd, 2);
+	}
+
+	if(sys->chdir("/") < 0)
+		fatal(sys->sprint("chdir /: %r"));
+
+	stats.totread = big 0;
+	stats.totwrite = big 0;
+	stats.nrpc = 0;
+	stats.nproto = 0;
+	stats.rpc = array[tagof Tmsg.Wstat + 1] of ref Rpc;
+	stats.rpc[tagof Tmsg.Version] = mkrpc("version");
+	stats.rpc[tagof Tmsg.Auth] = mkrpc("auth");
+	stats.rpc[tagof Tmsg.Flush] = mkrpc("flush");
+	stats.rpc[tagof Tmsg.Attach] = mkrpc("attach");
+	stats.rpc[tagof Tmsg.Walk] = mkrpc("walk");
+	stats.rpc[tagof Tmsg.Open] = mkrpc("open");
+	stats.rpc[tagof Tmsg.Create] = mkrpc("create");
+	stats.rpc[tagof Tmsg.Clunk] = mkrpc("clunk");
+	stats.rpc[tagof Tmsg.Read] = mkrpc("read");
+	stats.rpc[tagof Tmsg.Write] = mkrpc("write");
+	stats.rpc[tagof Tmsg.Remove] = mkrpc("remove");
+	stats.rpc[tagof Tmsg.Stat] = mkrpc("stat");
+	stats.rpc[tagof Tmsg.Wstat] = mkrpc("wstat");
+
+	mpipe := array[2] of ref Sys->FD;
+	if(sys->pipe(mpipe) < 0)
+		fatal(sys->sprint("can't create pipe: %r"));
+	pids := chan of int;
+	cmddone := chan of int;
+	spawn cmd(sh, ctxt, args, wd, mpipe[0], pids, cmddone);
+	<-pids;
+	mpipe[0] = nil;
+	epipe := array[2] of ref Sys->FD;
+	if(sys->pipe(epipe) < 0)
+		fatal(sys->sprint("can't create pipe: %r"));
+	spawn export(epipe[1], pids);
+	<-pids;
+	epipe[1] = nil;
+	iodone := chan of int;
+	spawn iostats(epipe[0], mpipe[1], pids, iodone);
+	<-pids;
+	epipe[0] = mpipe[1] = nil;
+	<-cmddone;
+	<-iodone;
+	results();
+}
+
+cmd(sh: Sh, ctxt: ref Draw->Context, args: list of string, wdir: string, fsfd: ref Sys->FD, pids: chan of int, done: chan of int)
+{
+	{
+		pids <-= sys->pctl(Sys->FORKNS|Sys->FORKFD, nil);
+		if(sys->mount(fsfd, nil, "/", Sys->MREPL, "") < 0)
+			fatal(sys->sprint("can't mount /: %r"));
+		fsfd = nil;
+		sys->bind("#e", "/env", Sys->MREPL | Sys->MCREATE);
+		sys->bind("#d", "/fd", Sys->MREPL);	# better than nothing
+		if(sys->chdir(wdir) < 0)
+			fatal(sys->sprint("can't chdir to %s: %r", wdir));
+		sh->run(ctxt, args);
+	}exception{
+	"fail:*" =>
+		;	# don't mention it
+	* =>
+		raise;	# cause the fault
+	}
+	done <-= 1;
+}
+
+iostats(expfd: ref Sys->FD, mountfd: ref Sys->FD, pids: chan of int, done: chan of int)
+{
+	pids <-= sys->pctl(Sys->NEWFD|Sys->NEWPGRP, 1 :: 2 :: expfd.fd :: mountfd.fd :: nil);
+	timefd := sys->open("/dev/time", Sys->OREAD);
+	if(timefd == nil)
+		fatal(sys->sprint("can't open /dev/time: %r"));
+	tmsgs := chan of (int, ref Tmsg);
+	spawn Treader(mountfd, expfd, tmsgs);
+	(tpid, nil) := <-tmsgs;
+	rmsgs := chan of (int, ref Rmsg);
+	spawn Rreader(expfd, mountfd, rmsgs);
+	(rpid, nil) := <-rmsgs;
+	expfd = mountfd = nil;
+	stderr := sys->fildes(2);
+Run:
+	for(;;)alt{
+	(n, t) := <-tmsgs =>	# n.b.: received on tmsgs before it goes to server
+		if(t == nil || tagof t == tagof Tmsg.Readerror)
+			break Run;	# TO DO?
+		if(dbg)
+			sys->fprint(stderr, "->%s\n", t.text());
+		tag := newtag(t, nsec(timefd));
+		stats.nrpc++;
+		stats.nproto += n;
+		rpc := stats.rpc[tagof t];
+		if(rpc == nil){
+			sys->fprint(stderr, "iostats: unexpected T-msg %d\n", tagof t);
+			continue;
+		}
+		rpc.count++;
+		rpc.bin += big n;
+		pick pt := t {
+		Auth =>
+			tag.fid = newfid(pt.afid);
+		Attach =>
+			tag.fid = newfid(pt.fid);
+		Walk =>
+			tag.fid = findfid(pt.fid);
+		Open =>
+			tag.fid = findfid(pt.fid);
+		Create =>
+			tag.fid = findfid(pt.fid);
+		Read =>
+			tag.fid = findfid(pt.fid);
+		Write =>
+			tag.fid = findfid(pt.fid);
+			pt.data = nil;	# don't need to keep data
+		Clunk or
+		Stat or
+		Remove =>
+			tag.fid = findfid(pt.fid);
+		Wstat =>
+			tag.fid = findfid(pt.fid);
+		}
+	(n, r) := <-rmsgs =>
+		if(r == nil || tagof r == tagof Rmsg.Readerror){
+			break Run;	# TO DO
+		}
+		if(dbg)
+			sys->fprint(stderr, "<-%s\n", r.text());
+		stats.nproto += n;
+		tag := findtag(r.tag, 1);
+		if(tag == nil)
+			continue;	# client or server error TO DO: account for flush
+		if(tagof r < len replymap && (tt := replymap[tagof r]) >= 0 && (rpc := stats.rpc[tt]) != nil){
+			update(rpc, nsec(timefd)-tag.stime);
+			rpc.bout += big n;
+		}
+		fid := tag.fid;
+		pick pr := r {
+		Error =>
+			pick m := tag.m {
+			Auth =>
+				if(fid != nil){
+					if(fid.nread != big 0 || fid.nwrite != big 0)
+						fidreport(fid);
+					freefid(fid);
+				}
+			}
+		Version =>
+			# could pick up message size
+			# flush fids/tags
+			tags = array[len tags] of ref Tag;
+			fids = array[len fids] of list of ref Fid;
+		Auth =>
+			# afid from fid.t, qaid from auth
+			if(fid != nil){
+				fid.qid = pr.aqid;
+				fid.path = ref Path(nil, "#auth");
+			}
+		Attach =>
+			if(fid != nil){
+				fid.qid = pr.qid;
+				fid.path = ref Path(nil, "/");
+			}
+		Walk =>
+			pick m := tag.m {
+			Walk =>
+				if(len pr.qids != len m.names)
+					break;	# walk failed, no change
+				if(fid == nil)
+					break;
+				if(m.newfid != m.fid){
+					nf := newfid(m.newfid);
+					nf.path = fid.path;
+					fid = nf;	# walk new fid
+				}
+				for(i := 0; i < len m.names; i++){
+					fid.qid = pr.qids[i];
+					if(m.names[i] == ".."){
+						if(fid.path.parent != nil)
+							fid.path = fid.path.parent;
+					}else
+						fid.path = ref Path(fid.path, m.names[i]);
+				}
+			}
+		Open or
+		Create =>
+			if(fid != nil)
+				fid.qid = pr.qid;
+		Read =>
+			fid.nread++;
+			nr := big len pr.data;
+			fid.bread += nr;
+			stats.totread += nr;
+		Write =>
+			# count
+			fid.nwrite++;
+			fid.bwrite += big pr.count;
+			stats.totwrite += big pr.count;
+		Flush =>
+			pick m := tag.m {
+			Flush =>
+				findtag(m.oldtag, 1);	# discard if there
+			}
+		Clunk or
+		Remove =>
+			if(fid != nil){
+				if(fid.nread != big 0 || fid.nwrite != big 0)
+					fidreport(fid);
+				freefid(fid);
+			}
+		}
+	}
+	kill(rpid, "kill");
+	kill(tpid, "kill");
+	done <-= 1;
+}
+
+results()
+{
+	stderr := sys->fildes(2);
+	rpc := stats.rpc[tagof Tmsg.Read];
+	brpsec := real stats.totread / ((real rpc.time/1.0e9)+.000001);
+
+	rpc = stats.rpc[tagof Tmsg.Write];
+	bwpsec := real stats.totwrite / ((real rpc.time/1.0e9)+.000001);
+
+	ttime := big 0;
+	for(n := 0; n < len stats.rpc; n++){
+		rpc = stats.rpc[n];
+		if(rpc == nil || rpc.count == big 0)
+			continue;
+		ttime += rpc.time;
+	}
+
+	bppsec := real stats.nproto / ((real ttime/1.0e9)+.000001);
+
+	sys->fprint(stderr, "\nread      %bud bytes, %g Kb/sec\n", stats.totread, brpsec/1024.0);
+	sys->fprint(stderr, "write     %bud bytes, %g Kb/sec\n", stats.totwrite, bwpsec/1024.0);
+	sys->fprint(stderr, "protocol  %ud bytes, %g Kb/sec\n", stats.nproto, bppsec/1024.0);
+	sys->fprint(stderr, "rpc       %ud count\n\n", stats.nrpc);
+
+	sys->fprint(stderr, "%-10s %5s %5s %5s %5s %5s           T        R\n", 
+	      "Message", "Count", "Low", "High", "Time", "  Avg");
+
+	for(n = 0; n < len stats.rpc; n++){
+		rpc = stats.rpc[n];
+		if(rpc == nil || rpc.count == big 0)
+			continue;
+		sys->fprint(stderr, "%-10s %5bud %5bud %5bud %5bud %5bud ms %8bud %8bud bytes\n", 
+			rpc.name, 
+			rpc.count,
+			rpc.lo/Ns2ms,
+			rpc.hi/Ns2ms,
+			rpc.time/Ns2ms,
+			rpc.time/Ns2ms/rpc.count,
+			rpc.bin,
+			rpc.bout);
+	}
+
+	# unclunked fids
+	for(n = 0; n < NFIDHASH; n++)
+		for(fl := fids[n]; fl != nil; fl = tl fl){
+			fid := hd fl;
+			if(fid.nread != big 0 || fid.nwrite != big 0)
+				fidreport(fid);
+		}
+	if(frecs == nil)
+		exit;
+
+	sys->fprint(stderr, "\nOpens    Reads  (bytes)   Writes  (bytes) File\n");
+	for(frl := frecs; frl != nil; frl = tl frl){
+		fr := hd frl;
+		case s := makepath(fr.op) {
+		"/fd/0" =>	s = "(stdin)";
+		"/fd/1" =>	s = "(stdout)";
+		"/fd/2" =>	s = "(stderr)";
+		"" =>		s = "/.";
+		}
+		sys->fprint(stderr, "%5ud %8bud %8bud %8bud %8bud %s\n", fr.opens, fr.nread, fr.bread,
+							fr.nwrite, fr.bwrite, s);
+	}
+}
+
+Treader(fd: ref Sys->FD, ofd: ref Sys->FD, out: chan of (int, ref Tmsg))
+{
+	out <-= (sys->pctl(0, nil), nil);
+	fd = sys->fildes(fd.fd);
+	ofd = sys->fildes(ofd.fd);
+	for(;;){
+		(a, err) := styx->readmsg(fd, Maxmsg);
+		if(err != nil){
+			out <-= (0, ref Tmsg.Readerror(0, err));
+			break;
+		}
+		if(a == nil){
+			out <-= (0, nil);
+			break;
+		}
+		(nil, m) := Tmsg.unpack(a);
+		if(m == nil){
+			out <-= (0, ref Tmsg.Readerror(0, "bad Styx T-message format"));
+			break;
+		}
+		out <-= (len a, m);
+		sys->write(ofd, a, len a);	# TO DO: errors
+	}
+}
+
+Rreader(fd: ref Sys->FD, ofd: ref Sys->FD, out: chan of (int, ref Rmsg))
+{
+	out <-= (sys->pctl(0, nil), nil);
+	fd = sys->fildes(fd.fd);
+	ofd = sys->fildes(ofd.fd);
+	for(;;){
+		(a, err) := styx->readmsg(fd, Maxmsg);
+		if(err != nil){
+			out <-= (0, ref Rmsg.Readerror(0, err));
+			break;
+		}
+		if(a == nil){
+			out <-= (0, nil);
+			break;
+		}
+		(nil, m) := Rmsg.unpack(a);
+		if(m == nil){
+			out <-= (0, ref Rmsg.Readerror(0, "bad Styx R-message format"));
+			break;
+		}
+		out <-= (len a, m);
+		sys->write(ofd, a, len a);	# TO DO: errors
+	}
+}
+
+reply(fd: ref Sys->FD, m: ref Rmsg)
+{
+	d := m.pack();
+	sys->write(fd, d, len d);
+}
+
+mkrpc(s: string): ref Rpc
+{
+	return ref Rpc(s, big 0, big 0, big 1 << 40, big 0, big 0, big 0);
+}
+
+newfid(nr: int): ref Fid
+{
+	h := nr%NFIDHASH;
+	for(fl := fids[h]; fl != nil; fl = tl fl)
+		if((hd fl).nr == nr)
+			return hd fl;	# shouldn't happen: faulty client
+	fid := ref Fid;
+	fid.nr = nr;
+	fid.nread = big 0;
+	fid.nwrite = big 0;
+	fid.bread = big 0;
+	fid.bwrite = big 0;
+	fid.qid = Qid(big 0, 0, -1);
+	fids[h] = fid :: fids[h];
+	return fid;
+}
+
+findfid(nr: int): ref Fid
+{
+	for(fl := fids[nr%NFIDHASH]; fl != nil; fl = tl fl)
+		if((hd fl).nr == nr)
+			return hd fl;
+	return nil;
+}
+
+freefid(fid: ref Fid)
+{
+	h := fid.nr%NFIDHASH;
+	nl: list of ref Fid;
+	for(fl := fids[h]; fl != nil; fl = tl fl)
+		if((hd fl).nr != fid.nr)
+			nl = hd fl :: nl;
+	fids[h] = nl;
+}
+
+makepath(p: ref Path): string
+{
+	nl: list of string;
+	for(; p != nil; p = p.parent)
+		if(p.name != "/")
+			nl = p.name :: nl;
+	s := "";
+	for(; nl != nil; nl = tl nl)
+		if(s != nil)
+			s += "/" + hd nl;
+		else
+			s = hd nl;
+	return "/"+s;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "iostats: %s: %r\n", s);
+	raise "fatal:error";
+}
+
+nsec(fd: ref Sys->FD): big
+{
+	buf := array[100] of byte;
+	n := sys->pread(fd, buf, len buf, big 0);
+	if(n <= 0)
+		return big 0;
+	return big string buf[0:n];
+}
+
+fidreport(f: ref Fid)
+{
+	for(fl := frecs; fl != nil; fl = tl fl){
+		fr := hd fl;
+		if(eqqid(f.qid, fr.qid)){
+			# could put f.path in list of paths if aliases were interesting
+			fr.nread += f.nread;
+			fr.nwrite += f.nwrite;
+			fr.bread += f.bread;
+			fr.bwrite += f.bwrite;
+			fr.opens++;
+			return;
+		}
+	}
+
+	fr := ref Frec;
+	fr.op = f.path;
+	fr.qid = f.qid;
+	fr.nread = f.nread;
+	fr.nwrite = f.nwrite;
+	fr.bread = f.bread;
+	fr.bwrite = f.bwrite;
+	fr.opens = 1;
+	frecs = fr :: frecs;
+}
+
+update(rpc: ref Rpc, t: big)
+{
+	if(t < big 0)
+		t = big 0;
+
+	rpc.time += t;
+	if(t < rpc.lo)
+		rpc.lo = t;
+	if(t > rpc.hi)
+		rpc.hi = t;
+}
+
+newtag(m: ref Tmsg, t: big): ref Tag
+{
+	slot := m.tag & (NTAGHASH - 1);
+	tag := ref Tag(m, nil, t, tags[slot]);
+	tags[slot] = tag;
+	return tag;
+}
+
+findtag(tag: int, destroy: int): ref Tag
+{
+	slot := tag & (NTAGHASH - 1);
+	prev: ref Tag;
+	for(t := tags[slot]; t != nil; t = t.next){
+		if(t.m.tag == tag)
+			break;
+		prev = t;
+	}
+	if(t == nil || !destroy)
+		return t;
+	if(prev == nil)
+		tags[slot] = t.next;
+	else
+		prev.next = t.next;
+	return t;
+}
+
+eqqid(a, b: Qid): int
+{
+	return a.path == b.path && a.qtype == b.qtype;
+}
+
+export(fd: ref Sys->FD, pid: chan of int)
+{
+	pid <-= sys->pctl(Sys->NEWFD|Sys->FORKNS, fd.fd::0::1::2::nil);
+	sys->export(fd, "/", Sys->EXPWAIT);
+}
+
+kill(pid: int, what: string)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "%s", what);
+}
--- /dev/null
+++ b/appl/cmd/ip/bootpd.b
@@ -1,0 +1,667 @@
+implement Bootpd;
+
+#
+# to do:
+#	DHCP
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "attrdb.m";
+	attrdb: Attrdb;
+	Attr, Db, Dbentry, Tuples: import attrdb;
+
+include "dial.m";
+	dial: Dial;
+
+include "ip.m";
+	ip: IP;
+	IPaddr, Udphdr: import ip;
+
+include "ipattr.m";
+	ipattr: IPattr;
+
+include "ether.m";
+	ether: Ether;
+
+include "arg.m";
+
+Bootpd: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+debug: int;
+sniff: int;
+verbose: int;
+
+siaddr: IPaddr;
+netmask: IPaddr;
+myname: string;
+progname := "bootpd";
+net := "/net";
+ndb: ref Db;
+ndbfile := "/lib/ndb/local";
+mtime := 0;
+testing := 0;
+
+Udphdrsize: con IP->Udphdrlen;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		loadfail(Bufio->PATH);
+	attrdb = load Attrdb Attrdb->PATH;
+	if(attrdb == nil)
+		loadfail(Attrdb->PATH);
+	attrdb->init();
+	dial = load Dial Dial->PATH;
+	if(dial == nil)
+		loadfail(Dial->PATH);
+	ip = load IP IP->PATH;
+	if(ip == nil)
+		loadfail(IP->PATH);
+	ip->init();
+	ipattr = load IPattr IPattr->PATH;
+	if(ipattr == nil)
+		loadfail(IPattr->PATH);
+	ipattr->init(attrdb, ip);
+	ether = load Ether Ether->PATH;
+	if(ether == nil)
+		loadfail(Ether->PATH);
+	ether->init();
+
+	verbose = 1;
+	sniff = 0;
+	debug = 0;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		raise "fail: load Arg";
+	arg->init(args);
+	arg->setusage("bootpd [-dsqv] [-f file] [-x network]");
+	progname = arg->progname();
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' =>	debug++;
+		's' =>		sniff = 1; debug = 255;
+		'q' =>	verbose = 0;
+		'v' =>	verbose = 1;
+		'x' =>	net = arg->earg();
+		'f' =>		ndbfile = arg->earg();
+		't' =>		testing = 1; debug = 1; verbose = 1;
+		* =>		arg->usage();
+		}
+	args = arg->argv();
+	if(args != nil)
+		arg->usage();
+	arg = nil;
+
+	sys->pctl(Sys->FORKFD|Sys->FORKNS, nil);
+
+	if(!sniff && (err := dbread()) != nil)
+		error(err);
+
+	myname = sysname();
+	if(myname == nil)
+		error("system name not set");
+	(siaddr, err) = csquery(myname);
+	if(err != nil)
+		error(sys->sprint("can't find IP address for %s: %s", myname, err));
+	if(debug)
+		sys->fprint(stderr, "bootpd: local IP address is %s\n", siaddr.text());
+
+	addr := net+"/udp!*!67";
+	if(testing)
+		addr = net+"/udp!*!499";
+	if(debug)
+		sys->fprint(stderr, "bootpd: announcing %s\n", addr);
+	c := dial->announce(addr);
+	if(c == nil)
+		error(sys->sprint("can't announce %s: %r", addr));
+	if(sys->fprint(c.cfd, "headers") < 0)
+		error(sys->sprint("can't set headers mode: %r"));
+
+	if(debug)
+		sys->fprint(stderr, "bootpd: opening %s/data\n", c.dir);
+	c.dfd = sys->open(c.dir+"/data", sys->ORDWR);
+	if(c.dfd == nil)
+		error(sys->sprint("can't open %s/data: %r", c.dir));
+
+	spawn server(c);
+}
+
+loadfail(s: string)
+{
+	error(sys->sprint("can't load %s: %r", s));
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "bootpd: %s\n", s);
+	raise "fail:error";
+}
+
+server(c: ref Sys->Connection)
+{
+	buf := array[2048] of byte;
+	badread := 0;
+	for(;;) {
+		n := sys->read(c.dfd, buf, len buf);
+		if(n <0) {
+			if (badread++ > 10)
+				break;
+			continue;
+		}
+		badread = 0;
+		if(n < Udphdrsize) {
+			if(debug)
+				sys->fprint(stderr, "bootpd: short Udphdr: %d bytes\n", n);
+			continue;
+		}
+		hdr := Udphdr.unpack(buf, Udphdrsize);
+		if(debug)
+			sys->fprint(stderr, "bootpd: received request from udp!%s!%d\n", hdr.raddr.text(), hdr.rport);
+		if(n < Udphdrsize+300) {
+			if(debug)
+				sys->fprint(stderr, "bootpd: short request of %d bytes\n", n - Udphdrsize);
+			continue;
+		}
+
+		(bootp, err) := Bootp.unpack(buf[Udphdrsize:]);
+		if(err != nil) {
+			if(debug)
+				sys->fprint(stderr, "bootpd: can't unpack packet: %s\n", err);
+			continue;
+		}
+		if(debug >= 2)
+			sys->fprint(stderr, "bootpd: recvd {%s}\n", bootp.text());
+		if(sniff)
+			continue;
+		if(bootp.htype != 1 || bootp.hlen != 6) {
+			# if it isn't ether, we don't do it
+			if(debug)
+				sys->fprint(stderr, "bootpd: hardware type not ether; ignoring.\n");
+			continue;
+		}
+		if((err = dbread()) != nil) {
+			sys->fprint(stderr,  "bootpd: getreply: dbread failed: %s\n", err);
+			continue;
+		}
+		rec := lookup(bootp);
+		if(rec == nil) {
+			# we can't answer this request
+			if(debug)
+				sys->fprint(stderr, "bootpd: cannot answer request.\n");
+			continue;
+		}
+		if(debug)
+			sys->fprint(stderr, "bootpd: found a matching entry: {%s}\n", rec.text());
+		mkreply(bootp, rec);
+		if(verbose)
+			sys->print("bootpd: %s -> %s %s\n", ether->text(rec.ha), rec.hostname, rec.ip.text());
+		if(debug)
+			sys->fprint(stderr, "bootpd: reply {%s}\n", bootp.text());
+		repl := bootp.pack();
+		if(!testing)
+			arpenter(rec.ip.text(), ether->text(rec.ha));
+		send(hdr, repl);
+	}
+	sys->fprint(stderr, "bootpd: %d read errors: %r\n", badread);
+}
+
+arpenter(ip, ha: string)
+{
+	if(debug)
+		sys->fprint(stderr, "bootpd: arp: %s -> %s\n", ip, ha);
+	fd := sys->open(net+"/arp", Sys->OWRITE);
+	if(fd == nil) {
+		if(debug)
+			sys->fprint(stderr, "bootpd: arp open failed: %r\n");
+		return;
+	}
+	if(sys->fprint(fd, "add %s %s", ip, ha) < 0){
+		if(debug)
+			sys->fprint(stderr, "bootpd: error writing arp: %r\n");
+	}
+}
+
+sysname(): string
+{
+	t := rf("/dev/sysname");
+	if(t != nil)
+		return t;
+	return rf("#e/sysname");
+}
+
+rf(name: string): string
+{
+	fd := sys->open(name, Sys->OREAD);
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+	return string buf[0:n];
+}
+
+csquery(name: string): (IPaddr, string)
+{
+	siaddr = ip->noaddr;
+	# get a local IP address by translating our sysname with cs(8)
+	csfile := net+"/cs";
+	fd := sys->open(net+"/cs", Sys->ORDWR);
+	if(fd == nil)
+		return (ip->noaddr, sys->sprint("can't open %s/cs: %r", csfile));
+	if(sys->fprint(fd, "net!%s!0", name) < 0)
+		return (ip->noaddr, sys->sprint("can't translate net!%s!0: %r", name));
+	sys->seek(fd, big 0, 0);
+	a := array[1024] of byte;
+	n := sys->read(fd, a, len a);
+	if(n <= 0)
+		return (ip->noaddr, "no result from "+csfile);
+	reply := string a[0:n];
+	(l, addr):= sys->tokenize(reply, " ");
+	if(l != 2)
+		return (ip->noaddr, "bad cs reply format");
+	(l, addr) = sys->tokenize(hd tl addr, "!");
+	if(l < 2)
+		return (ip->noaddr, "bad cs reply format");
+	(ok, ipa) := IPaddr.parse(hd addr);
+	if(ok < 0 || !ipok(siaddr))
+		return (ip->noaddr, "can't parse address: "+hd addr);
+	return (ipa, nil);
+}
+
+Hostinfo: adt {
+	hostname: string;
+
+	ha: array of byte;	# hardware addr
+	ip: IPaddr;		# client IP addr
+	bootf: string;		# boot file path
+	netmask: IPaddr;	# subnet mask
+	ipgw: IPaddr;	# gateway IP addr
+	fs: IPaddr;		# file server IP addr
+	auth: IPaddr;	# authentication server IP addr
+
+	text:	fn(inf: self ref Hostinfo): string;
+};
+
+send(hdr: ref Udphdr, msg: array of byte)
+{
+	replyaddr := net+"/udp!255.255.255.255!68";	# TO DO: gateway
+	if(testing)
+		replyaddr = sys->sprint("udp!%s!%d", hdr.raddr.text(), hdr.rport);
+	lport := "67";
+	if(testing)
+		lport = "499";
+	c := dial->dial(replyaddr, lport);
+	if(c == nil) {
+		sys->fprint(stderr, "bootpd: can't dial %s for reply: %r\n", replyaddr);
+		return;
+	}
+	n := sys->write(c.dfd, msg, len msg);
+	if(n != len msg)
+		sys->fprint(stderr, "bootpd: udp write error: %r\n");
+}
+
+mkreply(bootp: ref Bootp, rec: ref Hostinfo)
+{
+	bootp.op = 2; # boot reply
+	bootp.yiaddr = rec.ip;
+	bootp.siaddr = siaddr;
+	bootp.giaddr = ip->noaddr;
+	bootp.sname = myname;
+	bootp.file = string rec.bootf;
+	bootp.vend = array of byte sys->sprint("p9  %s %s %s %s", rec.netmask.text(), rec.fs.text(), rec.auth.text(), rec.ipgw.text());
+}
+
+dbread(): string
+{
+	if(ndb == nil){
+		ndb = Db.open(ndbfile);
+		if(ndb == nil)
+			return sys->sprint("cannot open %s: %r", ndbfile);
+	}else if(ndb.changed())
+		ndb.reopen();
+	return nil;
+}
+
+ipok(a: IPaddr): int
+{
+	return a.isv4() && !(a.eq(ip->v4noaddr) || a.eq(ip->noaddr) || a.ismulticast());
+}
+
+lookup(bootp: ref Bootp): ref Hostinfo
+{
+	if(ndb == nil)
+		return nil;
+	inf: ref Hostinfo;
+	hwaddr := ether->text(bootp.chaddr);
+	if(ipok(bootp.ciaddr)){
+		# client thinks it knows address; check match with MAC address
+		ipaddr := bootp.ciaddr.text();
+		ptr: ref Attrdb->Dbptr;
+		for(;;){
+			e: ref Dbentry;
+			(e, ptr) = ndb.findbyattr(ptr, "ip", ipaddr, "ether");
+			if(e == nil)
+				break;
+			# TO DO: check result
+			inf = matchandfill(e, "ip", ipaddr, "ether", hwaddr);
+			if(inf != nil)
+				return inf;
+		}
+	}
+	# look up an ip address associated with given MAC address
+	ptr: ref Attrdb->Dbptr;
+	for(;;){
+		e: ref Dbentry;
+		(e, ptr) = ndb.findbyattr(ptr, "ether", hwaddr, "ip");
+		if(e == nil)
+			break;
+		# TO DO: check right net etc.
+		inf = matchandfill(e, "ether", hwaddr, "ip", nil);
+		if(inf != nil)
+			return inf;
+	}
+	return nil;
+}
+
+matchandfill(e: ref Dbentry, attr: string, val: string, rattr: string, rval: string): ref Hostinfo
+{
+	matches := e.findbyattr(attr, val, rattr);
+	for(; matches != nil; matches = tl matches){
+		(line, attrs) := hd matches;
+		for(; attrs != nil; attrs = tl attrs){
+			if(rval == nil || (hd attrs).val == rval){
+				inf := fillup(line, e);
+				if(inf != nil)
+					return inf;
+				break;
+			}
+		}
+	}
+	return nil;
+}
+
+fillup(line: ref Tuples, e: ref Dbentry): ref Hostinfo
+{
+	ok: int;
+	inf := ref Hostinfo;
+	inf.netmask = ip->noaddr;
+	inf.ipgw = ip->noaddr;
+	inf.fs = ip->v4noaddr;
+	inf.auth = ip->v4noaddr;
+	inf.hostname = find(line, e, "sys");
+	s := find(line, e, "ether");
+	if(s != nil)
+		inf.ha = ether->parse(s);
+	s = find(line, e, "ip");
+	if(s == nil)
+		return nil;
+	(ok, inf.ip) = IPaddr.parse(s);
+	if(ok < 0)
+		return nil;
+	(results, err) := ipattr->findnetattrs(ndb, "ip", s, list of{"ipmask", "ipgw", "fs", "FILESERVER", "SIGNER", "auth", "bootf"});
+	if(err != nil)
+		return nil;
+	for(; results != nil; results = tl results){
+		(a, nattrs) := hd results;
+		if(!a.eq(inf.ip))
+			continue;	# different network
+		for(; nattrs != nil; nattrs = tl nattrs){
+			na := hd nattrs;
+			case na.name {
+			"ipmask" =>
+				inf.netmask = takeipmask(na.pairs, inf.netmask);
+			"ipgw" =>
+				inf.ipgw = takeipattr(na.pairs, inf.ipgw);
+			"fs" or "FILESERVER" =>
+				inf.fs = takeipattr(na.pairs, inf.fs);
+			"auth" or "SIGNER" =>
+				inf.auth = takeipattr(na.pairs, inf.auth);
+			"bootf" =>
+				inf.bootf = takeattr(na.pairs, inf.bootf);
+			}
+		}
+	}
+	return inf;
+}
+
+takeattr(pairs: list of ref Attr, s: string): string
+{
+	if(s != nil || pairs == nil)
+		return s;
+	return (hd pairs).val;
+}
+
+takeipattr(pairs: list of ref Attr, a: IPaddr): IPaddr
+{
+	if(pairs == nil || !(a.eq(ip->noaddr) || a.eq(ip->v4noaddr)))
+		return a;
+	(ok, na) := parseip((hd pairs).val);
+	if(ok < 0)
+		return a;
+	return na;
+}
+
+takeipmask(pairs: list of ref Attr, a: IPaddr): IPaddr
+{
+	if(pairs == nil || !(a.eq(ip->noaddr) || a.eq(ip->v4noaddr)))
+		return a;
+	(ok, na) := IPaddr.parsemask((hd pairs).val);
+	if(ok < 0)
+		return a;
+	return na;
+}
+
+findip(line: ref Tuples, e: ref Dbentry, attr: string): (int, IPaddr)
+{
+	s := find(line, e, attr);
+	if(s == nil)
+		return (-1, ip->noaddr);
+	return parseip(s);
+}
+
+parseip(s: string): (int, IPaddr)
+{
+	(ok, a) := IPaddr.parse(s);
+	if(ok < 0){
+		# look it up if it's a system name
+		s = findbyattr("sys", s, "ip");
+		(ok, a) = IPaddr.parse(s);
+	}
+	return (ok, a);
+}
+
+find(line: ref Tuples, e: ref Dbentry, attr: string): string
+{
+	if(line != nil){
+		a := line.find(attr);
+		if(a != nil)
+			return (hd a).val;
+	}
+	if(e != nil){
+		for(matches := e.find(attr); matches != nil; matches = tl matches){
+			(nil, a) := hd matches;
+			if(a != nil)
+				return (hd a).val;
+		}
+	}
+	return nil;
+}
+
+findbyattr(attr: string, val: string, rattr: string): string
+{
+	ptr: ref Attrdb->Dbptr;
+	for(;;){
+		e: ref Dbentry;
+		(e, ptr) = ndb.findbyattr(ptr, attr, val, rattr);
+		if(e == nil)
+			break;
+		rvl := e.find(rattr);
+		if(rvl != nil){
+			(nil, al) := hd rvl;
+			return (hd al).val;
+		}
+	}
+	return nil;
+}
+
+missing(rec: ref Hostinfo): string
+{
+	s := "";
+	if(rec.ha == nil)
+		s += " hardware address";
+	if(rec.ip.eq(ip->noaddr))
+		s += " IP address";
+	if(rec.bootf == nil)
+		s += " bootfile";
+	if(rec.netmask.eq(ip->noaddr))
+		s += " subnet mask";
+	if(rec.ipgw.eq(ip->noaddr))
+		s += " gateway";
+	if(rec.fs.eq(ip->noaddr))
+		s += " file server";
+	if(rec.auth.eq(ip->noaddr))
+		s += " authentication server";
+	if(s != "")
+		return s[1:];
+	return nil;
+}
+
+dtoa(data: array of byte): string
+{
+	if(data == nil)
+		return nil;
+	result: string;
+	for(i:=0; i < len data; i++)
+		result += sys->sprint(".%d", int data[i]);
+	return result[1:];
+}
+
+magic(cookie: array of byte): string
+{
+	if(eqa(cookie, array[] of { byte 'p', byte '9', byte ' ', byte ' ' }))
+		return "plan9";
+	if(eqa(cookie, array[] of { byte 99, byte 130, byte 83, byte 99 }))
+		return "rfc1048";
+	if(eqa(cookie, array[] of { byte 'C', byte 'M', byte 'U', byte 0 }))
+		return "cmu";
+	return dtoa(cookie);
+}
+
+eqa(a1: array of byte, a2: array of byte): int
+{
+	if(len a1 != len a2)
+		return 0;
+	for(i := 0; i < len a1; i++)
+		if(a1[i] != a2[i])
+			return 0;
+	return 1;
+}
+
+Hostinfo.text(rec: self ref Hostinfo): string
+{
+	return sys->sprint("ha=%s ip=%s bf=%s sm=%s gw=%s fs=%s au=%s",
+		ether->text(rec.ha), rec.ip.text(), rec.bootf, rec.netmask.masktext(), rec.ipgw.text(), rec.fs.text(), rec.auth.text());
+}
+
+Bootp: adt
+{
+	op:	int;		# opcode [1]
+	htype:	int;	# hardware type[1]
+	hlen:	int;		# hardware address length [1]
+	hops:	int;	# gateway hops [1]
+	xid:	int;		# random number [4]
+	secs:	int;		# seconds elapsed since client started booting [2]
+	flags:	int;	# flags[2]
+	ciaddr:	IPaddr;	# client ip address (client->server)[4]
+	yiaddr:	IPaddr;	# your ip address (server->client)[4]
+	siaddr:	IPaddr;	# server's ip address [4]
+	giaddr:	IPaddr;	# gateway ip address [4]
+	chaddr:	array of byte;	# client hardware (mac) address [16]
+	sname:	string;	# server host name [64]
+	file:	string;		# boot file name [128]
+	vend:	array of byte;	# vendor-specific [128]
+
+	unpack:	fn(a: array of byte): (ref Bootp, string);
+	pack:	fn(bp: self ref Bootp): array of byte;
+	text:	fn(bp: self ref Bootp): string;
+};
+
+Bootp.unpack(data: array of byte): (ref Bootp, string)
+{
+	if(len data < 300)
+		return (nil, "too short");
+
+	bp := ref Bootp;
+	bp.op = int data[0];
+	bp.htype = int data[1];
+	bp.hlen = int data[2];
+	if(bp.hlen > 16)
+		return (nil, "length error");
+	bp.hops = int data[3];
+	bp.xid = ip->get4(data, 4);
+	bp.secs = ip->get2(data, 8);
+	bp.flags = ip->get2(data, 10);
+	bp.ciaddr = IPaddr.newv4(data[12:16]);
+	bp.yiaddr = IPaddr.newv4(data[16:20]);
+	bp.siaddr = IPaddr.newv4(data[20:24]);
+	bp.giaddr = IPaddr.newv4(data[24:28]);
+	bp.chaddr = data[28:28+bp.hlen];
+	bp.sname = ctostr(data[44:108]);
+	bp.file = ctostr(data[108:236]);
+	bp.vend = data[236:300];
+	return (bp, nil);
+}
+
+Bootp.pack(bp: self ref Bootp): array of byte
+{
+	data := array[364] of { * => byte 0 };
+	data[0] = byte bp.op;
+	data[1] = byte bp.htype;
+	data[2] = byte bp.hlen;
+	data[3] = byte bp.hops;
+	ip->put4(data, 4, bp.xid);
+	ip->put2(data, 8, bp.secs);
+	ip->put2(data, 10, bp.flags);
+	data[12:] = bp.ciaddr.v4();
+	data[16:] = bp.yiaddr.v4();
+	data[20:] = bp.siaddr.v4();
+	data[24:] = bp.giaddr.v4();
+	data[28:] = bp.chaddr;
+	data[44:] = array of byte bp.sname;
+	data[108:] = array of byte bp.file;
+	data[236:] = bp.vend;
+	return data;
+}
+
+ctostr(cstr: array of byte): string
+{
+	for(i:=0; i<len cstr; i++)
+		if(cstr[i] == byte 0)
+			break;
+	return string cstr[0:i];
+}
+
+Bootp.text(bp: self ref Bootp): string
+{
+	s := sys->sprint("op=%d htype=%d hlen=%d hops=%d xid=%ud secs=%ud ciaddr=%s yiaddr=%s",
+		int bp.op, bp.htype, bp.hlen, bp.hops, bp.xid, bp.secs, bp.ciaddr.text(), bp.yiaddr.text());
+	s += sys->sprint(" server=%s gateway=%s hwaddr=%q host=%q file=%q magic=%q",
+		bp.siaddr.text(), bp.giaddr.text(), ether->text(bp.chaddr), bp.sname, bp.file, magic(bp.vend[0:4]));
+	if(magic(bp.vend[0:4]) == "plan9")
+		s += "("+ctostr(bp.vend)+")";
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/ip/dhcp.b
@@ -1,0 +1,162 @@
+implement Dhcp;
+
+#
+# configure an interface using DHCP
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "ip.m";
+	ip: IP;
+	IPv4off, IPaddrlen: import IP;
+	IPaddr: import ip;
+	get2, get4, put2, put4: import ip;
+
+include "dhcp.m";
+	dhcpclient: Dhcpclient;
+	Bootconf, Lease: import dhcpclient;
+
+include "arg.m";
+
+Dhcp: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+RetryTime: con 10*1000;	# msec
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	ip = load IP IP->PATH;
+	dhcpclient = load Dhcpclient Dhcpclient->PATH;
+
+	sys->pctl(Sys->NEWFD|Sys->NEWPGRP, 0 :: 1 :: 2 :: nil);
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("dhcp [-bdmnpr] [-g ipgw] [-h hostname] [-x /net] ifcdir [ip [ipmask]]");
+	trace := 0;
+	pcfg := 0;
+	bootp := 0;
+	monitor := 0;
+	retry := 0;
+	noctl := 0;
+	netdir := "/net";
+	cfg := Bootconf.new();
+	while((o := arg->opt()) != 0)
+		case o {
+		'b' =>	bootp = 1;
+		'd' =>	trace++;
+		'g' =>	cfg.ipgw = arg->earg();
+		'h' =>	cfg.puts(Dhcpclient->Ohostname, arg->earg());
+		'm' =>	monitor = 1;
+		'n' =>	noctl = 1;
+		'p' =>	pcfg = 1;
+		'r' =>		retry = 1;
+		'x' =>	netdir = arg->earg();
+		* =>		arg->usage();
+		}
+	args = arg->argv();
+	if(len args == 0)
+		arg->usage();
+
+	ifcdir := hd args;
+	args = tl args;
+	if(args != nil){
+		cfg.ip = hd args;
+		args = tl args;
+		if(args != nil){
+			cfg.ipmask = hd args;
+			args = tl args;
+			if(args != nil)
+				arg->usage();
+		}
+	}
+	arg = nil;
+
+	ifcctl: ref Sys->FD;
+	if(noctl == 0){
+		ifcctl = sys->open(ifcdir+"/ctl", Sys->OWRITE);
+		if(ifcctl == nil)
+			err(sys->sprint("cannot open %s/ctl: %r", ifcdir));
+	}
+	etherdir := finddev(ifcdir);
+	if(etherdir == nil)
+		err(sys->sprint("cannot find network device in %s/status: %r", ifcdir));
+	if(etherdir[0] != '/' && etherdir[0] != '#')
+		etherdir = netdir+"/"+etherdir;
+
+	ip->init();
+	dhcpclient->init();
+	dhcpclient->tracing(trace);
+	e: string;
+	lease: ref Lease;
+	for(;;){
+		if(bootp){
+			(cfg, e) = dhcpclient->bootp(netdir, ifcctl, etherdir+"/addr", cfg);
+			if(e == nil){
+				if(cfg != nil)
+					dhcpclient->applycfg(netdir, ifcctl, cfg);
+				if(pcfg)
+					printcfg(cfg);
+				break;
+			}
+		}else{
+			(cfg, lease, e) = dhcpclient->dhcp(netdir, ifcctl, etherdir+"/addr", cfg, nil);	# last is array of int options
+			if(e == nil){
+				if(pcfg)
+					printcfg(cfg);
+				if(cfg.lease > 0 && monitor)
+					leasemon(lease.configs, pcfg);
+				break;
+			}
+		}
+		if(!retry)
+			err("failed to configure network: "+e);
+		sys->fprint(sys->fildes(2), "dhcp: failed to configure network: %s; retrying", e);
+		sys->sleep(RetryTime);
+	}
+}
+
+leasemon(configs: chan of (ref Bootconf, string), pcfg: int)
+{
+	for(;;){
+		(cfg, e) := <-configs;
+		if(e != nil)
+			sys->fprint(sys->fildes(2), "dhcp: %s", e);
+		if(pcfg)
+			printcfg(cfg);
+	}
+}
+
+printcfg(cfg: ref Bootconf)
+{
+	sys->print("ip=%s ipmask=%s ipgw=%s iplease=%d\n", cfg.ip, cfg.ipmask, cfg.ipgw, cfg.lease);
+}
+
+finddev(ifcdir: string): string
+{
+	fd := sys->open(ifcdir+"/status", Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+	(nf, l) := sys->tokenize(string buf[0:n], " \n");
+	if(nf < 2){
+		sys->werrstr("unexpected format for status file");
+		return nil;
+	}
+	return hd tl l;
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "dhcp: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/ip/mkfile
@@ -1,0 +1,29 @@
+<../../../mkconfig
+
+DIRS=\
+	ppp\
+#	nppp\
+
+TARG=\
+	bootpd.dis\
+	dhcp.dis\
+	ping.dis\
+	rip.dis\
+	tftpd.dis\
+	virgild.dis\
+	sntp.dis\
+
+SYSMODULES=\
+	attrdb.m\
+	bufio.m\
+	dhcp.m\
+	draw.m\
+	ether.m\
+	ip.m\
+	ipattr.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/ip
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/cmd/ip/nppp/mkfile
@@ -1,0 +1,24 @@
+<../../../../mkconfig
+
+TARG=\
+	ppplink.dis\
+	pppchat.dis\
+	modem.dis\
+	script.dis\
+#	ppptest.dis\
+
+MODULES=\
+	modem.m\
+	script.m\
+
+SYSMODULES=\
+	sys.m\
+	draw.m\
+	tk.m\
+	dict.m\
+	string.m\
+	lock.m\
+
+DISBIN=$ROOT/dis/ip/nppp
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/ip/nppp/modem.b
@@ -1,0 +1,469 @@
+implement Modem;
+
+#
+# Copyright © 1998-2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "lock.m";
+	lock: Lock;
+	Semaphore: import lock;
+
+include "draw.m";
+
+include "modem.m";
+
+hangupcmd := "ATH0";		# was ATZH0 but some modem versions on Umec hung on ATZ
+
+# modem return codes
+Ok, Success, Failure, Abort, Noise, Found: con iota;
+
+maxspeed: con 115200;
+
+#
+#  modem return messages
+#
+Msg: adt {
+	text: 		string;
+	code: 		int;
+};
+
+msgs: array of Msg = array [] of {
+	("OK", 			Ok),
+	("NO CARRIER", 	Failure),
+	("ERROR", 		Failure),
+	("NO DIALTONE", Failure),
+	("BUSY", 		Failure),
+	("NO ANSWER", 	Failure),
+	("CONNECT", 	Success),
+};
+
+kill(pid: int)
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "kill") < 0)
+		sys->print("modem: can't kill %d: %r\n", pid);
+}
+
+#
+# prepare a modem port
+#
+openserial(d: ref Device): string
+{
+	d.data = nil;
+	d.ctl = nil;
+
+	d.data = sys->open(d.local, Sys->ORDWR);
+	if(d.data == nil)
+		return sys->sprint("can't open %s: %r", d.local);
+
+	d.ctl = sys->open(d.local+"ctl", Sys->ORDWR);
+	if(d.ctl == nil)
+		return sys->sprint("can't open %s: %r", d.local+"ctl");
+
+	d.speed = maxspeed;
+	d.avail = nil;
+	return nil;
+}
+
+#
+# shut down the monitor (if any) and return the connection
+#
+
+Device.close(m: self ref Device): ref Sys->Connection
+{
+	if(m.pid != 0){
+		kill(m.pid);
+		m.pid = 0;
+	}
+	if(m.data == nil)
+		return nil;
+	mc := ref sys->Connection(m.data, m.ctl, nil);
+	m.ctl = nil;
+	m.data = nil;
+	return mc;
+}
+
+#
+# Send a string to the modem
+#
+
+Device.send(d: self ref Device, x: string): string
+{
+	a := array of byte x;
+	f := sys->write(d.data, a, len a);
+	if(f != len a) {
+		# let's attempt to close & reopen the modem
+		d.close();
+		err := openserial(d);
+		if(err != nil)
+			return err;
+		f = sys->write(d.data,a, len a);
+		if(f < 0)
+			return sys->sprint("%r");
+		if(f != len a)
+			return "short write";
+	}
+	if(d.trace)
+		sys->print("->%s\n",x);
+	return nil;
+}
+
+#
+#  apply a string of commands to modem & look for a response
+#
+
+apply(d: ref Device, s: string, substr: string, secs: int): int
+{
+	m := Ok;
+	buf := "";
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		buf[len buf] = c;		# assume no Unicode
+		if(c == '\r' || i == (len s -1)){
+			if(c != '\r')
+				buf[len buf] = '\r';
+			if(d.send(buf) != nil)
+				return Abort;
+			(m, nil) = readmsg(d, secs, substr);
+			buf = "";
+		}
+	}
+	return m;
+}
+
+#
+#  get modem into command mode if it isn't already
+#
+GUARDTIME: con 1100;	# usual default for S12=50 in units of 1/50 sec; allow 100ms fuzz
+
+attention(d: ref Device): int
+{
+	for(i := 0; i < 3; i++){
+		if(apply(d, hangupcmd, nil, 2) == Ok)
+			return Ok;
+		sys->sleep(GUARDTIME);
+		if(d.send("+++") != nil)
+			return Abort;
+		sys->sleep(GUARDTIME);
+		(nil, msg) := readmsg(d, 0, nil);
+		if(msg != nil && d.trace)
+			sys->print("status: %s\n", msg);
+	}
+	return Failure;
+}
+
+#
+#  apply a command type
+#
+
+applyspecial(d: ref Device, cmd: string): int
+{
+	if(cmd == nil)
+		return Failure;
+	return apply(d, cmd, nil, 2);
+}
+
+#
+#  hang up any connections in progress and close the device
+#
+Device.onhook(d: self ref Device)
+{
+	# hang up the modem
+	monitoring(d);
+	if(attention(d) != Ok)
+		sys->print("modem: no attention\n");
+
+	# hangup the stream (eg, for ppp) and toggle the lines to the modem
+	if(d.ctl != nil) {
+		sys->fprint(d.ctl,"d0\n");
+		sys->fprint(d.ctl,"r0\n");
+		sys->fprint(d.ctl, "h\n");	# hangup on native serial 
+		sys->sleep(250);
+		sys->fprint(d.ctl,"r1\n");
+		sys->fprint(d.ctl,"d1\n");
+	}
+
+	d.close();
+}
+
+#
+# does string s contain t anywhere?
+#
+
+contains(s, t: string): int
+{
+	if(t == nil)
+		return 1;
+	if(s == nil)
+		return 0;
+	n := len t;
+	for(i := 0; i+n <= len s; i++)
+		if(s[i:i+n] == t)
+			return 1;
+	return 0;
+}
+
+#
+#  read till we see a message or we time out
+#
+readmsg(d: ref Device, secs: int, substr: string): (int, string)
+{
+	found := 0;
+	msecs := secs*1000;
+	limit := 1000;		# pretty arbitrary
+	s := "";
+
+	for(start := sys->millisec(); sys->millisec() <= start+msecs;){
+		a := d.getinput(1);
+		if(len a == 0){
+			if(limit){
+				sys->sleep(1);
+				continue;
+			}
+			break;
+		}
+		if(a[0] == byte '\n' || a[0] == byte '\r' || limit == 0){
+			if (len s) {
+				if (s[(len s)-1] == '\r')
+					s[(len s)-1] = '\n';
+				sys->print("<-%s\n",s);
+			}
+			if(substr != nil && contains(s, substr))
+				found = 1;
+			for(k := 0; k < len msgs; k++)
+				if(len s >= len msgs[k].text &&
+				   s[0:len msgs[k].text] == msgs[k].text){
+					if(found)
+						return (Found, s);
+					return (msgs[k].code, s);
+				}
+			start = sys->millisec();
+			s = "";
+			continue;
+		}
+		s[len s] = int a[0];
+		limit--;
+	}
+	s = "no response from modem";
+	if(found)
+		return (Found, s);
+
+	return (Noise, s);
+}
+
+#
+#  get baud rate from a connect message
+#
+
+getspeed(msg: string, speed: int): int
+{
+	p := msg[7:];	# skip "CONNECT"
+	while(p[0] == ' ' || p[0] == '\t')
+		p = p[1:];
+	s := int p;
+	if(s <= 0)
+		return speed;
+	else
+		return s;
+}
+
+#
+#  set speed and RTS/CTS modem flow control
+#
+
+setspeed(d: ref Device, baud: int)
+{
+	if(d != nil && d.ctl != nil){
+		sys->fprint(d.ctl, "b%d", baud);
+		sys->fprint(d.ctl, "m1");
+	}
+}
+
+monitoring(d: ref Device)
+{
+	# if no monitor then spawn one
+	if(d.pid == 0) {
+		pidc := chan of int;
+		spawn monitor(d, pidc, nil);
+		d.pid = <-pidc;
+	}
+}
+
+#
+#  a process to read input from a modem.
+#
+monitor(d: ref Device, pidc: chan of int, errc: chan of string)
+{
+	err := openserial(d);
+	pidc <-= sys->pctl(0, nil);
+	if(err != nil && errc != nil)
+		errc <-= err;
+	a := array[Sys->ATOMICIO] of byte;
+	for(;;) {
+		d.lock.obtain();
+		d.status = "Idle";
+		d.remote = "";
+		setspeed(d, d.speed);
+		d.lock.release();
+		# shuttle bytes
+		while((n := sys->read(d.data, a, len a)) > 0){
+			d.lock.obtain();
+			if (len d.avail < Sys->ATOMICIO) {
+				na := array[len d.avail + n] of byte;
+				na[0:] = d.avail[0:];
+				na[len d.avail:] = a[0:n];
+				d.avail = na;
+			}				
+			d.lock.release();
+		}
+		# on an error, try reopening the device
+		d.data = nil;
+		d.ctl = nil;
+		err = openserial(d);
+		if(err != nil && errc != nil)
+			errc <-= err;
+	}
+}
+
+#
+#  return up to n bytes read from the modem by monitor()
+#
+Device.getinput(d: self ref Device, n: int): array of byte
+{
+	if(d==nil || n <= 0)
+		return nil;
+	a: array of byte;
+	d.lock.obtain();
+	if(len d.avail != 0){
+		if(n > len d.avail)
+			n = len d.avail;
+		a = d.avail[0:n];
+		d.avail = d.avail[n:];
+	}
+	d.lock.release();
+	return a;
+}
+
+Device.getc(d: self ref Device, msec: int): int
+{
+	start := sys->millisec();
+	while((b  := d.getinput(1)) == nil) {
+		if (msec && sys->millisec() > start+msec)
+			return 0;
+		sys->sleep(1);
+	}
+	return int b[0];
+}
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	lock = load Lock Lock->PATH;
+	if(lock == nil)
+		return sys->sprint("can't load %s: %r", Lock->PATH);
+	lock->init();
+	return nil;
+}
+
+Device.new(modeminfo: ref ModemInfo, trace: int): ref Device
+{
+	d := ref Device;
+	d.lock = Semaphore.new();
+	d.local = modeminfo.path;
+	d.pid = 0;
+	d.speed = 0;
+	d.t = *modeminfo;
+	if(d.t.hangup == nil)
+		d.t.hangup = hangupcmd;
+	d.trace = trace | 1;	# always trace for now
+	return d;
+}
+
+#
+#  dial a number
+#
+Device.dial(d: self ref Device, number: string): string
+{
+	monitoring(d);
+
+	# modem type should already be established, but just in case
+	if(d.trace)
+		sys->print("modem: attention\n");
+	x := attention(d);
+	if (x != Ok && d.trace)
+		return "bad response from modem";
+	#
+	#  extended Hayes commands, meaning depends on modem
+	#
+	sys->print("modem: init\n");
+	if(d.t.country != nil)
+		applyspecial(d, d.t.country);
+
+	if(d.t.init != nil)
+		applyspecial(d, d.t.init);
+
+	if(d.t.other != nil)
+		applyspecial(d, d.t.other);
+
+	applyspecial(d, d.t.errorcorrection);
+
+	compress := Abort;
+	if(d.t.mnponly != nil)
+			compress = applyspecial(d, d.t.mnponly);
+	if(d.t.compression != nil)
+			compress = applyspecial(d, d.t.compression);
+
+	rateadjust := Abort;
+	if(compress != Ok)
+		rateadjust = applyspecial(d, d.t.rateadjust);
+	applyspecial(d, d.t.flowctl);
+
+	# finally, dialout
+	if(d.trace)
+		sys->print("modem: dial\n");
+	if((dt := d.t.dialtype) == nil)
+		dt = "ATDT";
+	err := d.send(sys->sprint("%s%s\r", dt, number));
+	if(err != nil){
+		if(d.trace)
+			sys->print("modem: can't dial %s: %s\n", number, err);
+		return err;
+	}
+
+	(i, msg) := readmsg(d, 120, nil);
+	if(i != Success){
+		if(d.trace)
+			sys->print("modem: modem error reply: %s\n", msg);
+		return msg;
+	}
+
+	connectspeed := getspeed(msg, d.speed);
+
+	# change line rate if not compressing
+	if(rateadjust == Ok)
+		setspeed(d, connectspeed);
+
+	if(d.ctl != nil){
+		if(d != nil)
+			sys->fprint(d.ctl, "s%d", connectspeed);	# set DCE speed (if device implements it)
+		sys->fprint(d.ctl, "c1");	# enable CD monitoring
+	}
+
+	return nil;
+}
+
+dumpa(a: array of byte): string
+{
+	s := "";
+	for(i:=0; i<len a; i++){
+		b := int a[i];
+		if(b >= ' ' && b < 16r7f)
+			s[len s] = b;
+		else
+			s += sys->sprint("\\%.2x", b);
+	}
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/ip/nppp/modem.m
@@ -1,0 +1,47 @@
+Modem: module
+{
+	PATH:	con "/dis/ip/nppp/modem.dis";
+
+	ModemInfo: adt {
+		path:			string;
+		init:			string;
+		country:		string;
+		other:		string;
+		errorcorrection:string;
+		compression:	string;
+		flowctl:		string;
+		rateadjust:	string;
+		mnponly:		string;
+		dialtype:		string;
+		hangup:		string;
+	};
+
+	Device: adt {
+		lock:	ref Lock->Semaphore;
+		# modem stuff
+		ctl:	ref Sys->FD;
+		data:	ref Sys->FD;
+
+		local:	string;
+		remote:	string;
+		status:	string;
+		speed:	int;
+		t:		ModemInfo;
+		trace:	int;
+
+		# input reader
+		avail:	array of byte;
+		pid:		int;
+
+		new:		fn(i: ref ModemInfo, trace: int): ref Device;
+		dial:		fn(m: self ref Device, number: string): string;
+		getc:		fn(m: self ref Device, msec: int): int;
+		getinput:	fn(m: self ref Device, n: int): array of byte;
+		send:	fn(m: self ref Device, x: string): string;
+		close:	fn(m: self ref Device): ref Sys->Connection;
+		onhook:	fn(m: self ref Device);
+	};
+
+	init:	fn(): string;
+
+};
--- /dev/null
+++ b/appl/cmd/ip/nppp/pppchat.b
@@ -1,0 +1,322 @@
+implement Dialupchat;
+
+#
+# Copyright © 2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+        sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+
+include "tk.m";
+        tk: Tk;
+
+include "wmlib.m";
+	wmlib: Wmlib;
+
+include "translate.m";
+	translate: Translate;
+	Dict: import translate;
+	dict: ref Dict;
+
+Dialupchat: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+# Dimension constant for ISP Connect window
+WIDTH: con 300;
+HEIGHT: con 58;
+
+LightGreen: con "#00FF80";           # colour for successful blob
+Blobx: con 8;
+Gapx: con 4;
+BARW: con (Blobx+Gapx)*10;			# Progress bar width
+BARH: con 18;			# Progress bar height
+DIALQUANTA : con 1000;
+ICONQUANTA : con 5000;
+
+pppquanta := DIALQUANTA;
+
+Maxstep: con 9;
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	wmlib = load Wmlib Wmlib->PATH;
+	wmlib->init();
+
+	translate = load Translate Translate->PATH;
+	if(translate != nil) {
+		translate->init();
+		dictname := translate->mkdictname("", "pppchat");
+		dicterr: string;
+		(dict, dicterr) = translate->opendict(dictname);
+		if(dicterr != nil)
+			sys->fprint(sys->fildes(2), "pppchat: can't open %s: %s\n", dictname, dicterr);
+	}else
+		sys->fprint(sys->fildes(2), "pppchat: can't load %s: %r\n", Translate->PATH);
+
+	tkargs: string;
+	if(args != nil) {
+		tkargs = hd args;
+		args = tl args;
+	}
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	pppfd := sys->open("/chan/pppctl", Sys->ORDWR);
+	if(pppfd == nil)
+		error(sys->sprint("can't open /chan/pppctl: %r"));
+
+	(t, wmctl) := wmlib->titlebar(ctxt.screen, tkargs, X("Dialup Connection"), Wmlib->Hide);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	pb := Progressbar.mk(t, ".f.prog.c", (BARW, BARH));
+
+	config_win := array[] of {
+		"frame .f",
+		"frame .f.prog",
+		"frame .f.b",
+
+		pb.tkcreate(),
+		"pack .f.prog.c -pady 6 -side top",
+
+		"label .f.stat -fg blue -text {"+X("Initialising connection...")+"}",
+		"pack .f.stat -side top -fill x -expand 1 -anchor n",
+
+		"pack .f -side top -expand 1 -padx 5 -pady 3 -fill both -anchor w",
+		"pack .f.prog -side top -expand 1 -fill x",
+		"button .f.b.done -text {"+X("Cancel")+"} -command {send cmd cancel}",
+		"pack .f.b.done -side right -padx 1 -pady 1 -anchor s",
+		"button .f.b.retry -text {"+X("Retry")+"} -command {send cmd retry} -state disabled",
+		"pack .f.b.retry -side left -padx 1 -pady 1 -anchor s",
+		"pack .f.b -side top -expand 1 -fill x",
+
+		"pack propagate . 0",
+		"update",
+	};
+
+	for(i := 0; i < len config_win; i++)
+		tkcmd(t, config_win[i]);
+
+	connected := 0;
+	winmapped := 1;
+	timecount := 0;
+	xmin := 0;
+	x := 0;
+	turn := 0;
+
+	pppquanta = DIALQUANTA;
+	ticks := chan of int;
+	spawn ppptimer(ticks);
+
+	statuslines := chan of (string, string);
+	pids := chan of int;
+	spawn ctlreader(pppfd, pids, statuslines);
+	ctlpid := <-pids;
+
+Work:
+	for(;;) alt {
+
+	s := <-wmctl =>
+		if(s == "exit")
+			s = "task";
+		if(s == "task"){
+			spawn wmlib->titlectl(t, s);
+			continue;
+		}
+		wmlib->titlectl(t, s);
+
+	press := <-cmd =>
+		case press {
+		"cancel" or "disconnect" =>
+			tkcmd(t, sys->sprint(".f.stat configure -text '%s", X("Disconnecting")));
+			tkcmd(t, "update");
+			if(sys->fprint(pppfd, "hangup") < 0){
+				err := sys->sprint("%r");
+				tkcmd(t, sys->sprint(".f.stat configure -text '%s: %s", X("Error disconnecting"), X(err)));
+				sys->fprint(sys->fildes(2), "pppchat: can't disconnect: %s\n", err);
+			}
+			break Work;
+		"retry" =>
+			if(sys->fprint(pppfd, "connect") < 0){
+				err := sys->sprint("%r");
+			}
+		}
+
+	<-ticks =>
+		ticks <-= 1;
+		if(!connected){
+			if(pb != nil){
+				if((turn ^= 1) == 0)
+					pb.setcolour("white");
+				else
+					pb.setcolour(LightGreen);
+			}
+			tkcmd(t, "raise .; update");
+		}
+
+	(status, err) := <-statuslines =>
+		if(status == nil){
+			status = "0 1 empty status";
+			if(err != nil)
+				sys->print("pppchat: !%s\n", err);
+		} else
+			sys->print("pppchat: %s\n", status);
+		(nf, flds) := sys->tokenize(status, " \t\n");
+#		for(i = 0; i < len status; i++)
+#			if(status[i] == ' ' || status[i] == '\t') {
+#				status = status[i+1:];
+#				break;
+#			}
+		if(nf < 3)
+			break;
+		step := int hd flds; flds = tl flds;
+		nstep := int hd flds; flds = tl flds;
+		if(step < 0)
+			raise "pppchat: bad step";
+		case hd flds {
+		"error:" =>
+			tkcmd(t, ".f.stat configure -fg red -text '"+X(status));
+			tkcmd(t, ".f.b.retry configure -state normal");
+			tkcmd(t, "update");
+			wmlib->unhide();
+			winmapped = 1;
+			pb.stepto(step, "red");
+			#break Work;
+		* =>
+			pb.setcolour(LightGreen);
+			pb.stepto(step, LightGreen);
+		}
+		turn = 0;
+		statusmsg := X(status);
+		tkcmd(t, ".f.stat configure -text '"+statusmsg);
+		tkcmd(t, "raise .; update");
+
+		case hd flds {
+		"up" or "done" =>
+			if(!connected){
+				connected = 1;
+			}
+			pppquanta = ICONQUANTA;
+
+			# display connection speed
+			if(tl flds != nil)
+				tkcmd(t, ".f.stat configure -text {"+statusmsg+" "+"SPEED"+" hd tl flds}");
+			else
+				tkcmd(t, ".f.stat configure -text {"+statusmsg+"}");
+			tkcmd(t, ".f.b.done configure -text Disconnect -command 'send cmd disconnect");
+			tkcmd(t, "update");
+			sys->sleep(2000);
+			tkcmd(t, "pack forget .f.prog; update");
+			spawn wmlib->titlectl(t, "task");
+			winmapped = 0;
+		}
+		tkcmd(t, "update");
+	}
+	<-ticks;
+	ticks <-= 0;	# stop ppptimer
+	kill(ctlpid);
+}
+
+ppptimer(ticks: chan of int)
+{
+	do{
+		sys->sleep(pppquanta);
+		ticks <-= 1;
+	}while(<-ticks);
+}
+
+ctlreader(fd: ref Sys->FD, pidc: chan of int, lines: chan of (string, string))
+{
+	pidc <-= sys->pctl(0, nil);
+	buf := array[128] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0)
+		lines <-= (string buf[0:n], nil);
+	if(n < 0)
+		lines <-= (nil, sys->sprint("%r"));
+	else
+		lines <-= (nil, nil);
+}
+
+Progressbar: adt {
+	t:	ref Tk->Toplevel;
+	canvas:	string;
+	csize:	Point;
+	blobs:	list of string;
+
+	mk:		fn(t: ref Tk->Toplevel, canvas: string, csize: Point): ref Progressbar;
+	tkcreate:	fn(pb: self ref Progressbar): string;
+	setcolour:	fn(pb: self ref Progressbar, c: string);
+	stepto:	fn(pb: self ref Progressbar, step: int, col: string);
+	destroy:	fn(pb: self ref Progressbar);
+};
+
+Progressbar.mk(t: ref Tk->Toplevel, canvas: string, csize: Point): ref Progressbar
+{
+	return ref Progressbar(t, canvas, csize, nil);
+}
+
+Progressbar.tkcreate(pb: self ref Progressbar): string
+{
+	return sys->sprint("canvas %s -width %d -height %d", pb.canvas, pb.csize.x, pb.csize.y);
+}
+
+Progressbar.setcolour(pb: self ref Progressbar, colour: string)
+{
+	if(pb.blobs != nil)
+		tkcmd(pb.t, sys->sprint("%s itemconfigure %s -fill %s; update", pb.canvas, hd pb.blobs, colour));
+}
+
+Progressbar.stepto(pb: self ref Progressbar, step: int, col: string)
+{
+	for(nblob := len pb.blobs; nblob > step+1; nblob--){
+		tkcmd(pb.t, sys->sprint("%s delete %s", pb.canvas, hd pb.blobs));
+		pb.blobs = tl pb.blobs;
+	}
+	if(nblob == step+1)
+		return;
+	p := Point(step*(Blobx+Gapx), 0);
+	r := Rect(p, p.add((Blobx, pb.csize.y-2)));
+	pb.blobs =  tkcmd(pb.t, sys->sprint("%s create rectangle %d %d %d %d -fill %s", pb.canvas, r.min.x,r.min.y, r.max.x,r.max.y, col)) :: pb.blobs;
+}
+
+Progressbar.destroy(pb: self ref Progressbar)
+{
+	tk->cmd(pb.t, "destroy "+pb.canvas);	# ignore errors
+}
+
+tkcmd(t: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(t, s);
+	if(e != nil && e[0] == '!')
+		sys->print("pppchat: tk error: %s [%s]\n", e, s);
+	return e;
+}
+
+kill(pid: int)
+{
+	if(pid > 0 && (fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "kill");
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "pppchat: %s\n", s);
+	raise "fail:error";
+}
+
+X(s: string): string
+{
+	if(dict != nil)
+		return dict.xlate(s);
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/ip/nppp/ppplink.b
@@ -1,0 +1,782 @@
+implement PPPlink;
+
+#
+# Copyright © 2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+include "cfgfile.m";
+	cfg: CfgFile;
+	ConfigFile: import cfg;
+
+include "lock.m";
+include "modem.m";
+include "script.m";
+
+include "sh.m";
+
+include "translate.m";
+	translate: Translate;
+	Dict: import translate;
+	dict: ref Dict;
+
+PPPlink: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+PPPInfo: adt {
+	ipaddr:		string;
+	ipmask:		string;
+	peeraddr:		string;
+	maxmtu:		string;
+	username:	string;
+	password:		string;
+};
+
+modeminfo: ref Modem->ModemInfo;
+context: ref Draw->Context;
+pppinfo: ref PPPInfo;
+scriptinfo: ref Script->ScriptInfo;
+isp_number: string;
+lastCdir:		ref Sys->Dir;	# state of file when last read
+netdir := "/net";
+
+Packet: adt {
+	src:	array of byte;
+	dst:	array of byte;
+	data:	array of byte;
+};
+
+DEFAULT_ISP_DB_PATH:	con "/services/ppp/isp.cfg";	# contains pppinfo & scriptinfo
+DEFAULT_MODEM_DB_PATH:	con	"/services/ppp/modem.cfg";			# contains modeminfo
+MODEM_DB_PATH:	con	"modem.cfg";			# contains modeminfo
+ISP_DB_PATH:	con "isp.cfg";		# contains pppinfo & scriptinfo
+
+primary := 0;
+framing := 1;
+
+Disconnected, Modeminit, Dialling, Modemup, Scriptstart, Scriptdone, Startingppp, Startedppp, Login, Linkup: con iota;
+Error: con -1;
+
+Ignorems: con 10*1000;	# time to ignore outgoing packets between dial attempts
+
+statustext := array[] of {
+Disconnected => "Disconnected",
+Modeminit =>	"Initializing Modem",
+Dialling =>	"Dialling Service Provider",
+Modemup =>	"Logging Into Network",
+Scriptstart =>	"Executing Login Script",
+Scriptdone =>	"Script Execution Complete",
+Startingppp =>	"Logging Into Network",
+Startedppp => "Logging Into Network",
+Login =>	"Verifying Password",
+Linkup =>	"Connected",
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: ppplink [-P] [-f] [-m mtu] [local [remote]]\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	translate = load Translate Translate->PATH;
+	if(translate != nil) {
+		translate->init();
+		dictname := translate->mkdictname("", "pppclient");
+		(dict, nil) = translate->opendict(dictname);
+	}
+	mtu := 1450;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		error(0, sys->sprint("can't load %s: %r", Arg->PATH));
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		'm' =>
+			if((s := arg->arg()) == nil || !(s[0]>='0' && s[0]<='9'))
+				usage();
+			mtu = int s;
+		'P' =>
+			primary = 1;
+		'f' =>
+			framing = 0;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	localip := "10.9.8.7";	# should be something locally unique
+	fake := 1;
+	if(args != nil){
+		fake = 0;
+		localip = hd args;
+		args = tl args;
+	}
+
+	cerr := configinit();
+	if(cerr != nil)
+		error(0, sys->sprint("can't configure: %s", cerr));
+	context = ctxt;
+
+	# make default (for now)
+	# if packet appears, start ppp and reset routing until it stops
+
+	(cfd, dir, err) := getifc();
+	if(err != nil)
+		error(0, err);
+
+	if(sys->fprint(cfd, "bind pkt") < 0)
+		error(0, sys->sprint("can't bind pkt: %r"));
+	if(sys->fprint(cfd, "add %s 255.255.255.0 10.9.8.0 %d", localip, mtu) < 0)
+		error(0, sys->sprint("can't add ppp addresses: %r"));
+	if(primary && addroute("0", "0", localip) < 0)
+		error(0, sys->sprint("can't add default route: %r"));
+	dfd := sys->open(dir+"/data", Sys->ORDWR);
+	if(dfd == nil)
+		error(0, sys->sprint("can't open %s: %r", dir));
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	packets := chan of ref Packet;
+	spawn netreader(dfd, dir, localip, fake, packets);
+
+	logger := chan of (int, string);
+	iocmd := sys->file2chan("/chan", "pppctl");
+	if(iocmd == nil)
+		error(0, sys->sprint("can't create /chan/pppctl: %r"));
+	spawn servestatus(iocmd.read, logger);
+
+	starteduser := 0;
+	lasttime := 0;
+
+	for(;;) alt{
+	(nil, data, nil, wc) := <-iocmd.write =>	# remote io control
+		if(wc == nil)
+			break;
+		(nil, flds) := sys->tokenize(string data, " \t");
+		if(len flds > 1){
+			case hd flds {
+			"cancel" or "disconnect" or "hangup" =>
+				;	# ignore it
+			"connect" =>
+				# start connection ...
+				;
+			* =>
+				wreply(wc, (0, "illegal request"));
+				continue;
+			}
+		}
+		wreply(wc, (len data, nil));
+
+	pkt := <-packets =>
+		sys->print("ppplink: received packet %s->%s: %d bytes\n", ipa(pkt.src), ipa(pkt.dst), len pkt.data);
+		if(abs(sys->millisec()-lasttime) < Ignorems){
+			sys->print("ppplink: ignored, not enough time elapsed yet between dial attempts\n");
+			break;
+		}
+		(ok, stat) := sys->stat(ISP_DB_PATH);
+		if(ok < 0 || lastCdir == nil || !samefile(*lastCdir, stat)){
+			cerr = configinit();
+			if(cerr != nil){
+				sys->print("ppplink: can't reconfigure: %s\n", cerr);
+				# use existing configuration
+			}
+		}
+		if(!starteduser){
+			sync := chan of int;
+			spawn userinterface(sync);
+			starteduser = <-sync;
+		}
+		(ppperr, pppdir) := makeconnection(packets, logger, iocmd.write);
+		lasttime = sys->millisec();
+		if(ppperr == nil){
+			sys->print("ppplink: connected on %s\n", pppdir);
+			# converse ...
+sys->sleep(120*1000);
+		}else{
+			sys->print("ppplink: ppp connect error: %s\n", ppperr);
+			hangup(pppdir);
+		}
+	}
+}
+
+servestatus(reader: chan of (int, int, int, Sys->Rread), updates: chan of (int, string))
+{
+	statuspending := 0;
+	statusreq: (int, int, Sys->Rread);
+	step := Disconnected;
+	statuslist := statusline(step, step, nil) :: nil;
+
+	for(;;) alt{
+	(off, nbytes, fid, rc) := <-reader=>
+		if(rc == nil){
+			statuspending = 0;
+			if(step == Disconnected)
+				statuslist = nil;
+			break;
+		}
+		if(statuslist == nil){
+			if(statuspending){
+				alt{
+				rc <-= (nil, "pppctl file already in use") => ;
+				* => ;
+				}
+				break;
+			}
+			statusreq = (nbytes, fid, rc);
+			statuspending = 1;
+			break;
+		}
+		alt{
+		rc <-= reads(hd statuslist, 0, nbytes) =>
+			statuslist = tl statuslist;
+		* => ;
+		}
+
+	(code, arg) := <-updates =>
+		# convert to string
+		if(code != Error)
+			step = code;
+		status := statusline(step, code, arg);
+		if(code == Error)
+			step = Disconnected;
+		statuslist = appends(statuslist, status);
+		sys->print("status: %d %d %s\n", step, code, status);
+		if(statuspending){
+			(nbytes, nil, rc) := statusreq;
+			statuspending = 0;
+			alt{
+			rc <-= reads(hd statuslist, 0, nbytes) =>
+				statuslist = tl statuslist;
+			* =>
+				;
+			}
+		}
+	}
+}
+
+makeconnection(packets: chan of ref Packet, logger: chan of (int, string), writer: chan of (int, array of byte, int, Sys->Rwrite)): (string, string)
+{
+	result := chan of (string, string);
+	sync := chan of int;
+	spawn pppconnect(result, sync, logger);
+	pid := <-sync;
+	for(;;) alt{
+	(err, pppdir) := <-result =>
+		# pppconnect finished
+		return (err, pppdir);
+
+	pkt := <-packets =>
+		# ignore packets whilst connecting
+		sys->print("ppplink: ignored packet %s->%s: %d byten", ipa(pkt.src), ipa(pkt.dst), len pkt.data);
+
+	(nil, data, nil, wc) := <-writer =>	# user control
+		if(wc == nil)
+			break;
+		(nil, flds) := sys->tokenize(string data, " \t");
+		if(len flds > 1){
+			case hd flds {
+			"connect" =>
+				;	# ignore it
+			"cancel" or "disconnect" or "hangup"=>
+				kill(pid, "killgrp");
+				wreply(wc, (len data, nil));
+				return ("cancelled", nil);
+			* =>
+				wreply(wc, (0, "illegal request"));
+				continue;
+			}
+		}
+		wreply(wc, (len data, nil));
+	}
+}
+
+wreply(wc: chan of (int, string), v: (int, string))
+{
+	alt{
+	wc <-= v => ;
+	* => ;
+	}
+}
+
+appends(l: list of string, s: string): list of string
+{
+	if(l == nil)
+		return s :: nil;
+	return hd l :: appends(tl l, s);
+}
+
+statusline(step: int, code: int, arg: string): string
+{
+	s: string;
+	if(code >= 0 && code < len statustext){
+		n := "step";
+		if(code == Linkup)
+			n = "connect";
+		s = sys->sprint("%d %d %s %s", step, len statustext, n, X(statustext[code]));
+	}else
+		s = sys->sprint("%d %d error", step, len statustext);
+	if(arg != nil)
+		s += sys->sprint(": %s", arg);
+	return s;
+}
+
+getifc(): (ref Sys->FD, string, string)
+{
+	clonefile := netdir+"/ipifc/clone";
+	cfd := sys->open(clonefile, Sys->ORDWR);
+	if(cfd == nil)
+		return (nil, nil, sys->sprint("can't open %s: %r", clonefile));
+	buf := array[32] of byte;
+	n := sys->read(cfd, buf, len buf);
+	if(n <= 0)
+		return (nil, nil, sys->sprint("can't read %s: %r", clonefile));
+	return (cfd, netdir+"/ipifc/" + string buf[0:n], nil);
+}
+
+addroute(addr, mask, gate: string): int
+{
+	fd := sys->open(netdir+"/iproute", Sys->OWRITE);
+	if(fd == nil)
+		return -1;
+	return sys->fprint(fd, "add %s %s %s", addr, mask, gate);
+}
+
+#	uchar	vihl;		/* Version and header length */
+#	uchar	tos;		/* Type of service */
+#	uchar	length[2];	/* packet length */
+#	uchar	id[2];		/* ip->identification */
+#	uchar	frag[2];	/* Fragment information */
+#	uchar	ttl;		/* Time to live */
+#	uchar	proto;		/* Protocol */
+#	uchar	cksum[2];	/* Header checksum */
+#	uchar	src[4];		/* IP source */
+#	uchar	dst[4];		/* IP destination */
+IPhdrlen: con 20;
+
+netreader(dfd: ref Sys->FD, dir: string, localip: string, fake: int, outc: chan of ref Packet)
+{
+	buf := array [32*1024] of byte;
+	while((n := sys->read(dfd, buf, len buf)) > 0){
+		if(n < IPhdrlen){
+			sys->print("ppplink: received short packet: %d bytes\n", n);
+			continue;
+		}
+		pkt := ref Packet;
+		if(n < 9*1024){
+			pkt.data = array[n] of byte;
+			pkt.data[0:] = buf[0:n];
+		}else{
+			pkt.data = buf[0:n];
+			buf = array[32*1024] of byte;
+		}
+		pkt.src = pkt.data[12:];
+		pkt.dst = pkt.data[16:];
+		outc <-= pkt;
+	}
+	if(n < 0)
+		error(1, sys->sprint("packet interface read error: %r"));
+	else if(n == 0)
+		error(1, "packet interface: end of file");
+}
+
+ipa(a: array of byte): string
+{
+	if(len a < 4)
+		return "???";
+	return sys->sprint("%d.%d.%d.%d", int a[0], int a[1], int a[2], int a[3]);
+}
+
+reads(str: string, off, nbytes: int): (array of byte, string)
+{
+	bstr := array of byte str;
+	slen := len bstr;
+	if(off < 0 || off >= slen)
+		return (nil, nil);
+	if(off + nbytes > slen)
+		nbytes = slen - off;
+	if(nbytes <= 0)
+		return (nil, nil);
+	return (bstr[off:off+nbytes], nil);
+}
+
+readppplog(log: chan of (int, string), errfile: string, pidc: chan of int) 
+{
+	pidc <-= sys->pctl(0, nil);
+	src := sys->open(errfile, Sys->OREAD);
+	if(src == nil)
+		log <-= (Error, sys->sprint("can't open %s: %r", errfile));
+
+	buf := array[1024] of byte;
+	connected := 0;
+	lasterror := "";
+
+    	while((count := sys->read(src, buf, len buf)) > 0) {
+	    	(nil, tokens) := sys->tokenize(string buf[:count],"\n");
+	    	for(; tokens != nil; tokens = tl tokens) {
+			case hd tokens {
+			"no error" =>
+				log <-= (Linkup, nil);
+				lasterror = nil;
+				connected = 1;
+			"permission denied" =>
+				lasterror = X("Username or Password Incorrect");
+				log <-= (Error, lasterror);
+			"write to hungup channel" =>
+				lasterror = X("Remote Host Hung Up");
+				log <-= (Error, lasterror);
+			* =>
+				lasterror = X(hd tokens);
+				log <-= (Error, lasterror);
+			}
+		}
+	}
+	if(count == 0 && connected && lasterror == nil){	# should change ip/pppmedium.c instead?
+		#hangup(nil);
+		log <-= (Error, X("Lost Connection"));
+	}
+}
+
+dialup(mi: ref Modem->ModemInfo, number: string, scriptinfo: ref Script->ScriptInfo, logchan: chan of (int, string)): (string, ref Sys->Connection)
+{
+	logchan <-= (Modeminit, nil);
+
+	# open & init the modem
+
+	modeminfo = mi;
+	modem := load Modem Modem->PATH;
+	if(modem == nil)
+		return (sys->sprint("can't load %s: %r", Modem->PATH), nil);
+	err := modem->init();
+	if(err != nil)
+		return (sys->sprint("couldn't init modem: %s", err), nil);
+	Device: import modem;
+	d := Device.new(modeminfo, 1);
+	logchan <-= (Dialling, number);
+	err = d.dial(number);
+	if(err != nil){
+		d.close();
+		return (err, nil);
+	}
+	logchan <-= (Modemup, nil);
+
+	# login script
+
+	if(scriptinfo != nil) {
+		logchan <-= (Scriptstart, nil);
+		err = runscript(modem, d, scriptinfo);
+		if(err != nil){
+			d.close();
+			return (err, nil);
+		}
+		logchan <-= (Scriptdone, nil);
+	}
+
+	mc := d.close();
+	return (nil, mc);
+
+}
+
+startppp(logchan: chan of (int, string), pppinfo: ref PPPInfo): (string, string)
+{
+	(ifd, dir, err) := getifc();
+	if(ifd == nil)
+		return (err, nil);
+
+	sync := chan of int;
+	spawn readppplog(logchan, dir + "/err", sync);		# unbind gives eof on err
+	<-sync;
+
+	if(pppinfo.ipaddr == nil)
+		pppinfo.ipaddr = "-";
+#	if(pppinfo.ipmask == nil)
+#		pppinfo.ipmask = "255.255.255.255";
+	if(pppinfo.peeraddr == nil)
+		pppinfo.peeraddr = "-";
+	if(pppinfo.maxmtu == nil)
+		pppinfo.maxmtu = "-";
+#	if(pppinfo.maxmtu <= 0)
+#		pppinfo.maxmtu = mtu;
+#	if(pppinfo.maxmtu < 576)
+#		pppinfo.maxmtu = 576;
+	if(pppinfo.username == nil)
+		pppinfo.username = "-";
+	if(pppinfo.password == nil)
+		pppinfo.password = "-";
+
+	ifc := "bind ppp "+modeminfo.path+" "+ pppinfo.ipaddr+" "+pppinfo.peeraddr+" "+pppinfo.maxmtu
+			+" "+string framing+" "+pppinfo.username+" "+pppinfo.password;
+
+	if(sys->fprint(ifd, "%s", ifc) < 0)
+		return (sys->sprint("can't bind ppp to %s: %r", dir), nil);
+
+	sys->print("ppplink: %s\n", ifc);
+
+	return (nil, dir);
+}
+
+runscript(modem: Modem, dev: ref Modem->Device, scriptinfo: ref Script->ScriptInfo): string
+{
+	script := load Script Script->PATH;
+	if(script == nil)
+		return sys->sprint("can't load %s: %r", Script->PATH);
+	err := script->init(modem);
+	if(err != nil)
+		return err;
+	return script->execute(dev, scriptinfo);
+}
+
+hangup(pppdir: string)
+{
+	sys->print("ppplink: hangup...\n");
+	if(pppdir != nil){	# shut down the PPP link
+		fd := sys->open(pppdir + "/ctl", Sys->OWRITE);
+		if(fd == nil || sys->fprint(fd, "unbind") < 0)
+			sys->print("ppplink: hangup: can't unbind ppp on %s: %r\n", pppdir);
+		fd = nil;
+	}
+	modem := load Modem Modem->PATH;
+	if(modem == nil) {
+		sys->print("ppplink: hangup: can't load %s: %r", Modem->PATH);
+		return;
+	}
+	err := modem->init();
+	if(err != nil){
+		sys->print("ppplink: hangup: couldn't init modem: %s", err);
+		return;
+	}
+	Device: import modem;
+	d := Device.new(modeminfo, 1);
+	if(d != nil){
+		d.onhook();
+		d.close();
+	}
+}
+
+kill(pid: int, msg: string)
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", msg) < 0)
+		sys->print("pppclient: can't %s %d: %r\n", msg, pid);
+}
+
+error(dokill: int, s: string)
+{
+	sys->fprint(sys->fildes(2), "ppplink: %s\n", s);
+	if(dokill)
+		kill(sys->pctl(0, nil), "killgrp");
+	raise "fail:error";
+}
+
+X(s : string) : string
+{
+	if(dict != nil)
+		return dict.xlate(s);
+	return s;
+}
+
+cfile(file: string): string
+{
+	if(len file > 0 && file[0] == '/')
+		return file;
+	return "/usr/"+user()+"/config/"+file;
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	buf := array[64] of byte;
+	if(fd != nil && (n := sys->read(fd, buf, len buf)) > 0)
+		return string buf[0:n];
+	return "inferno";	# hmmm.
+}
+
+cfvalue(c: ref ConfigFile, key: string) :string
+{
+	s := "";
+	for(values := c.getcfg(key); values != nil; values = tl values){
+		if(s != "")
+			s[len s] = ' ';
+		s += hd values;
+	}
+	return s;
+}
+
+configinit(): string
+{
+	cfg = load CfgFile CfgFile->PATH;
+	if(cfg == nil)
+		return sys->sprint("can't load %s: %r", CfgFile->PATH);
+
+	# Modem Configuration
+
+	modemdb := cfile(MODEM_DB_PATH);
+	cfg->verify(DEFAULT_MODEM_DB_PATH, modemdb);
+	modemcfg := cfg->init(modemdb);
+	if(modemcfg == nil)
+		return sys->sprint("can't open %s: %r", modemdb);
+	modeminfo = ref Modem->ModemInfo;
+	modeminfo.path = cfvalue(modemcfg, "PATH");
+	modeminfo.init = cfvalue(modemcfg, "INIT");
+	modeminfo.country = cfvalue(modemcfg, "COUNTRY");
+	modeminfo.other = cfvalue(modemcfg, "OTHER");
+	modeminfo.errorcorrection = cfvalue(modemcfg,"CORRECT");
+	modeminfo.compression = cfvalue(modemcfg,"COMPRESS");
+	modeminfo.flowctl = cfvalue(modemcfg,"FLOWCTL");
+	modeminfo.rateadjust = cfvalue(modemcfg,"RATEADJ");
+	modeminfo.mnponly = cfvalue(modemcfg,"MNPONLY");
+	modeminfo.dialtype = cfvalue(modemcfg,"DIALING");
+	if(modeminfo.dialtype!="ATDP")
+		modeminfo.dialtype="ATDT";
+
+	ispdb := cfile(ISP_DB_PATH);
+	cfg->verify(DEFAULT_ISP_DB_PATH, ispdb);
+	sys->print("cfg->init(%s)\n", ispdb);
+
+	# ISP Configuration
+	pppcfg := cfg->init(ispdb);
+	if(pppcfg == nil)
+		return sys->sprint("can't read or create ISP configuration file %s: %r", ispdb);
+	(ok, stat) := sys->stat(ispdb);
+	if(ok >= 0)
+		lastCdir = ref stat;
+
+	pppinfo = ref PPPInfo;
+	isp_number = cfvalue(pppcfg, "NUMBER");
+	pppinfo.ipaddr = cfvalue(pppcfg,"IPADDR");
+	pppinfo.ipmask = cfvalue(pppcfg,"IPMASK");
+	pppinfo.peeraddr = cfvalue(pppcfg,"PEERADDR");
+	pppinfo.maxmtu = cfvalue(pppcfg,"MAXMTU");
+	pppinfo.username = cfvalue(pppcfg,"USERNAME");
+	pppinfo.password = cfvalue(pppcfg,"PASSWORD");
+
+	info := pppcfg.getcfg("SCRIPT");
+	if(info != nil) {
+		scriptinfo = ref Script->ScriptInfo;
+		scriptinfo.path = hd info;
+		scriptinfo.username = pppinfo.username;
+		scriptinfo.password = pppinfo.password;
+	} else
+		scriptinfo = nil;
+
+	info = pppcfg.getcfg("TIMEOUT");
+	if(info != nil)
+		scriptinfo.timeout = int (hd info);
+	cfg = nil;	# unload it
+
+	if(modeminfo.path == nil)
+		return "no modem device configured";
+	if(isp_number == nil)
+		return "no telephone number configured for ISP";
+
+	return nil;
+}
+
+isipaddr(a: string): int
+{
+	i, c, ac, np : int = 0;
+ 
+	for(i = 0; i < len a; i++) {
+		c = a[i];
+		if(c >= '0' && c <= '9') {
+			np = 10*np + c - '0';
+			continue;
+		}
+		if(c == '.' && np) {
+			ac++;
+	 		if(np > 255)
+				return 0;
+			np = 0;
+			continue;
+		}
+		return 0;
+	}
+	return np && np < 256 && ac == 3;
+}
+
+userinterface(sync: chan of int)
+{
+	pppgui := load Command "pppchat.dis";
+	if(pppgui == nil){
+		sys->fprint(sys->fildes(2), "ppplink: can't load %s: %r\n", "/dis/svc/nppp/pppchat.dis");
+		# TO DO: should be optional
+		sync <-= 0;
+	}
+
+	sys->pctl(Sys->NEWPGRP|Sys->NEWFD, list of {0, 1, 2});
+	sync <-= sys->pctl(0, nil);
+	pppgui->init(context, "pppchat" :: nil);
+}
+
+pppconnect(result: chan of (string, string), sync: chan of int, status: chan of (int, string))
+{
+	sys->pctl(Sys->NEWPGRP|Sys->NEWFD, list of {0, 1, 2});
+	sync <-= sys->pctl(0, nil);
+	pppdir: string;
+	(err, mc) := dialup(modeminfo, isp_number, scriptinfo, status);	# mc keeps connection open until startppp binds it to ppp
+	if(err == nil){
+		if(0 && (cfd := mc.cfd) != nil){
+			sys->fprint(cfd, "m1");	# cts/rts flow control/fifo's on
+			sys->fprint(cfd, "q64000"); # increase queue size to 64k
+			sys->fprint(cfd, "n1");	# nonblocking writes on
+			sys->fprint(cfd, "r1");	# rts on
+			sys->fprint(cfd, "d1");	# dtr on
+		}
+		status <-= (Startingppp, nil);
+		(err, pppdir) = startppp(status, pppinfo);
+		if(err == nil){
+			status <-= (Startedppp, nil);
+			result <-= (nil, pppdir);
+			return;
+		}
+	}
+	status <-= (Error, err);
+	result <-= (err, nil);
+}
+
+getspeed(file: string): string
+{
+	return findrate("/dev/modemstat", "rcvrate" :: "baud" :: nil);
+}
+
+findrate(file: string, opt: list of string): string
+{
+	fd := sys->open(file, sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array [1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 1)
+		return nil;
+	(nil, flds) := sys->tokenize(string buf[0:n], " \t\r\n");
+	for(; flds != nil; flds = tl flds)
+		for(l := opt; l != nil; l = tl l)
+			if(hd flds == hd l)
+				return hd tl flds;
+	return nil;
+}
+
+samefile(d1, d2: Sys->Dir): int
+{
+	return d1.dev==d2.dev && d1.dtype==d2.dtype &&
+			d1.qid.path==d2.qid.path && d1.qid.vers==d2.qid.vers &&
+			d1.mtime==d2.mtime;
+}
+
+abs(n: int): int
+{
+	if(n < 0)
+		return -n;
+	return n;
+}
--- /dev/null
+++ b/appl/cmd/ip/nppp/ppptest.b
@@ -1,0 +1,90 @@
+#    Last change:  R    24 May 2001   11:05 am
+implement PPPTest;
+
+include "sys.m";
+	sys:	Sys;
+include "draw.m";
+
+include "lock.m";
+include "modem.m";
+include "script.m";
+include "pppclient.m";
+include "pppgui.m";
+
+PPPTest: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+usage()
+{
+    sys->print("ppptest device modem_init tel user password \n");
+	sys->print("Example: ppptest /dev/modem atw2 4125678 rome xxxxxxxx\n");
+	exit;
+	
+}
+init( ctxt: ref Draw->Context, argv: list of string )
+{
+	sys = load Sys Sys->PATH;
+
+	mi:	Modem->ModemInfo;
+	pi:	PPPClient->PPPInfo;
+	tel : string;
+#	si:	Script->ScriptInfo;
+	argv = tl argv;
+    if(argv == nil)
+	    usage();
+	else
+		mi.path = hd argv;
+
+	argv = tl argv;
+	if(argv == nil)
+	    usage();
+	else
+		mi.init = hd argv;
+	argv = tl argv;
+	if(argv == nil)
+		usage();
+	else
+		tel = hd argv;
+	argv = tl argv;
+	if(argv == nil)
+		usage();
+	else
+		pi.username = hd argv;
+	argv = tl argv;
+	if(argv==nil)
+	    usage();
+	else
+	    pi.password = hd argv;
+
+
+	#si.path = "rdid.script";
+	#si.username = "ericvh";
+	#si.password = "foobar";
+	#si.timeout = 60;
+
+
+	ppp := load PPPClient PPPClient->PATH;
+
+	logger := chan of int;
+
+	spawn ppp->connect( ref mi, tel, nil, ref pi, logger );
+	
+	pppgui := load PPPGUI PPPGUI->PATH;
+	(respchan, err) := pppgui->init(ctxt, logger, ppp, nil);
+	if(err != nil){
+		sys->print("ppptest: can't %s: %s\n", PPPGUI->PATH, err);
+		exit;
+	}
+
+	event := 0;
+	while(1) {
+		event =<- respchan;
+		sys->print("GUI event received: %d\n",event);
+		if(event) {
+			sys->print("success");
+			exit;
+		} else {
+			raise "fail: Couldn't connect to ISP";
+		}
+	}	
+}
--- /dev/null
+++ b/appl/cmd/ip/nppp/script.b
@@ -1,0 +1,171 @@
+implement Script;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "lock.m";
+include "modem.m";
+	modem: Modem;
+	Device: import modem;
+
+include "script.m";
+
+Scriptlim: con 32*1024;		# should be enough for all
+
+init(mm: Modem): string
+{
+	sys = load Sys Sys->PATH;
+	modem = mm;
+	str = load String String->PATH;
+	if(str == nil)
+		return sys->sprint("can't load %s: %r", String->PATH);
+	return nil;
+}
+
+execute(m: ref Modem->Device, scriptinfo: ref ScriptInfo): string
+{
+	if(scriptinfo.path != nil) {
+		if(m.trace)
+			sys->print("script: using %s\n",scriptinfo.path);
+		# load the script
+		err: string;
+		(scriptinfo.content, err) = scriptload(scriptinfo.path);
+		if(err != nil)
+			return err;
+	}else{
+		if(m.trace)
+			sys->print("script: using inline script\n");
+	}
+	
+	if(scriptinfo.timeout == 0)
+		scriptinfo.timeout = 20;
+
+	tend := sys->millisec() + 1000*scriptinfo.timeout;
+
+	for(conv := scriptinfo.content; conv != nil; conv = tl conv){
+		e, s:	string = nil;
+		p := hd conv;
+		if(len p == 0)
+			continue;
+		if(m.trace)
+			sys->print("script: %s\n",p);
+		if(p[0] == '-') {	# just send
+			if(len p == 1)
+				continue;
+			s = p[1:];
+		} else {
+			(n, esl) := sys->tokenize(p, "-");
+			if(n > 0) {
+				e = hd esl;
+				esl = tl esl;
+				if(n > 1)
+					s = hd esl;
+			}
+		}
+		if(e  != nil) {
+			if(match(m, special(e,scriptinfo), tend-sys->millisec()) == 0) {
+				if(m.trace)
+					sys->print("script: match failed\n");
+				return "script failed";
+			}
+		}
+		if(s != nil)
+			m.send(special(s, scriptinfo));
+	}
+	if(m.trace)
+		sys->print("script: done\n");
+	return nil;
+}
+
+match(m: ref Modem->Device, s: string, msec: int): int
+{
+	for(;;) {
+		c := m.getc(msec);
+		if(c ==  '\r')
+			c = '\n';
+		if(m.trace)
+			sys->print("%c",c);
+		if(c == 0)
+			return 0;
+	head:
+		while(c == s[0]) {
+			i := 1;
+			while(i < len s) {
+				c = m.getc(msec);
+				if(c == '\r')
+					c = '\n';
+				if(m.trace)
+					sys->print("%c",c);
+				if(c == 0)
+					return 0;
+				if(c != s[i])
+					continue head;
+				i++;
+			}
+			return 1;
+		}
+		if(c == '~')
+			return 1;	# assume PPP for now
+	}
+}
+
+#
+# Expand special script sequences
+#
+special(s: string, scriptinfo: ref ScriptInfo): string
+{
+	if(s == "$username") 					# special variable
+		s = scriptinfo.username;
+	else if(s == "$password") 
+		s = scriptinfo.password;
+	return deparse(s);
+}
+
+deparse(s: string): string
+{
+	r: string = "";
+	for(i:=0; i < len s; i++) {
+		c := s[i];
+		if(c == '\\'  && i+1 < len s) {
+			c = s[++i];
+			case c {
+			't'	=> c = '\t';
+			'n'	=> c = '\n';
+			'r'	=> c = '\r';
+			'b'	=> c = '\b';
+			'a'	=> c = '\a';
+			'v'	=> c = '\v';
+			'0'	=> c = '\0';
+			'$'	=> c = '$';
+			'u'	=> 
+				if(i+4 < len s) {
+					i++;
+					(c, nil) = str->toint(s[i:i+4], 16);
+					i+=3;
+				}
+			}
+		}
+		r[len r] = c;
+	}
+	return r;
+}
+
+scriptload(path: string): (list of string, string)
+{
+	dfd := sys->open(path, Sys->OREAD);
+	if(dfd == nil)
+		return (nil, sys->sprint("can't open script %s: %r", path));
+
+	b := array[Scriptlim] of byte;
+	n := sys->read(dfd, b, len b);
+	if(n < 0)
+		return (nil, sys->sprint("can't read script %s: %r", path));
+    
+	(nil, script) := sys->tokenize(string b[0:n], "\n");
+	return (script, nil);
+}
--- /dev/null
+++ b/appl/cmd/ip/nppp/script.m
@@ -1,0 +1,15 @@
+Script: module
+{
+	PATH:	con "/dis/ip/nppp/script.dis";
+
+	ScriptInfo: adt {
+		path:			string;
+		content:		list of string;
+		timeout:		int;
+		username:	string;
+		password:		string;
+	};
+
+	init:	fn(m: Modem): string;
+	execute:	fn(m: ref Modem->Device, scriptinfo: ref ScriptInfo): string;
+};
--- /dev/null
+++ b/appl/cmd/ip/ping.b
@@ -1,0 +1,358 @@
+implement Ping;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "ip.m";
+	ip: IP;
+	IPaddr: import ip;
+
+include "timers.m";
+	timers: Timers;
+	Timer: import timers;
+
+include "rand.m";
+	rand: Rand;
+
+include "dial.m";
+	dial: Dial;
+
+include "arg.m";
+
+Ping: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Icmp: adt
+{
+	ttl:	int;	# time to live
+	src:	IPaddr;
+	dst:	IPaddr;
+	ptype:	int;
+	code:	int;
+	seq:	int;
+	munged:	int;
+	time:	big;
+
+	unpack:	fn(b: array of byte): ref Icmp;
+};
+
+# packet types
+EchoReply: con 0;
+Unreachable: con 3;
+SrcQuench: con 4;
+EchoRequest: con 8;
+TimeExceed: con 11;
+Timestamp: con 13;
+TimestampReply: con 14;
+InfoRequest: con 15;
+InfoReply: con 16;
+
+Nmsg: con 32;
+Interval: con 1000;	# ms
+
+Req: adt
+{
+	seq:	int;	# sequence number
+	time:	big;	# time sent
+	rtt:	big;
+	ttl:	int;
+	replied:	int;
+};
+
+debug := 0;
+quiet := 0;
+lostonly := 0;
+lostmsgs := 0;
+rcvdmsgs := 0;
+sum := big 0;
+firstseq := 0;
+addresses := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	rand = load Rand Rand->PATH;
+	timers = load Timers Timers->PATH;
+	dial = load Dial Dial->PATH;
+	ip = load IP IP->PATH;
+	ip->init();
+
+
+	msglen := interval := 0;
+	nmsg := Nmsg;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("ip/ping [-alq] [-s msgsize] [-i millisecs] [-n #pings] destination");
+	while((o := arg->opt()) != 0)
+		case o {
+		'l' =>
+			lostonly++;
+		'd' =>
+			debug++;
+		's' =>
+			msglen = int arg->earg();
+		'i' =>
+			interval = int arg->earg();
+		'n' =>
+			nmsg = int arg->earg();
+		'a' =>
+			addresses = 1;
+		'q' =>
+			quiet = 1;
+		}
+	if(msglen < 32)
+		msglen = 64;
+	if(msglen >= 65*1024)
+		msglen = 65*1024-1;
+	if(interval <= 0)
+		interval = Interval;
+
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD, nil);
+	opentime();
+	rand->init(int(nsec()/big 1000));
+
+	addr := dial->netmkaddr(hd args, "icmp", "1");
+	c := dial->dial(addr, nil);
+	if(c == nil){
+		sys->fprint(sys->fildes(2), "ip/ping: can't dial %s: %r\n", addr);
+		raise "fail:dial";
+	}
+
+	sys->print("sending %d %d byte messages %d ms apart\n", nmsg, msglen, interval);
+
+	done := chan of int;
+	reqs := chan of ref Req;
+
+	spawn sender(c.dfd, msglen, interval, nmsg, done, reqs);
+	spid := <-done;
+
+	pids := chan of int;
+	replies := chan [8] of ref Icmp;
+	spawn reader(c.dfd, msglen, replies, pids);
+	rpid := <-pids;
+
+	tpid := 0;
+	timeout := chan of int;
+	requests: list of ref Req;
+Work:
+	for(;;) alt{
+	r := <-reqs =>
+		requests = r :: requests;
+	ic := <-replies =>
+		if(ic == nil){
+			rpid = 0;
+			break Work;
+		}
+		if(ic.munged)
+			sys->print("corrupted reply\n");
+		if(ic.ptype != EchoReply || ic.code != 0){
+			sys->print("bad type/code %d/%d seq %d\n",
+				ic.ptype, ic.code, ic.seq);
+			continue;
+		}
+		requests = clean(requests, ic);
+		if(lostmsgs+rcvdmsgs == nmsg)
+			break Work;
+	<-done =>
+		spid = 0;
+		# must be at least one message outstanding; wait for it
+		tpid = timers->init(Timers->Sec);
+		timeout = Timer.start((nmsg-lostmsgs-rcvdmsgs)*interval+5*Timers->Sec).timeout;
+	<-timeout =>
+		break Work;
+	}
+	kill(rpid);
+	kill(spid);
+	kill(tpid);
+	
+	for(; requests != nil; requests = tl requests)
+		if((hd requests).replied == 0)
+			lostmsgs++;
+
+	if(lostmsgs){
+		sys->print("%d out of %d message(s) lost\n", lostmsgs, lostmsgs+rcvdmsgs);
+		raise "fail:lost messages";
+	}
+}
+
+kill(pid: int)
+{
+	if(pid)
+		sys->fprint(sys->open("#p/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
+
+SECOND: con big 1000000000;	# nanoseconds
+MINUTE: con big 60*SECOND;
+
+clean(l: list of ref Req, ip: ref Icmp): list of ref Req
+{
+	left: list of ref Req;
+	for(; l != nil; l = tl l){
+		r := hd l;
+		if(ip.seq == r.seq){
+			r.rtt = ip.time-r.time;
+			r.ttl = ip.ttl;
+			reply(r, ip);
+		}
+		if(ip.time-r.time > MINUTE){
+			r.rtt = ip.time-r.time;
+			r.ttl = ip.ttl;
+			if(!r.replied)
+				lost(r, ip);
+		}else
+			left = r :: left;
+	}
+	return left;
+}
+
+sender(fd: ref Sys->FD, msglen: int, interval: int, n: int, done: chan of int, reqs: chan of ref Req)
+{
+
+	done <-= sys->pctl(0, nil);
+
+	firstseq = rand->rand(65536) - n;	# -n to ensure we don't exceed 16 bits
+	if(firstseq < 0)
+		firstseq = 0;
+
+	buf := array[64*1024+512] of {* => byte 0};
+	for(i := Odata; i < msglen; i++)
+		buf[i] = byte i;
+	buf[Otype] = byte EchoRequest;
+	buf[Ocode] = byte 0;
+
+	seq := firstseq;
+	for(i = 0; i < n; i++){
+		if(i != 0)
+			sys->sleep(interval);
+		ip->put2(buf, Oseq, seq);	# order?
+		r := ref Req;
+		r.seq = seq;
+		r.replied = 0;
+		r.time = nsec();
+		reqs <-= r;
+		if(sys->write(fd, buf, msglen) < msglen){
+			sys->fprint(sys->fildes(2), "ping: write failed: %r\n");
+			break;
+		}
+		seq++;
+	}
+	done <-= 1;
+}
+
+reader(fd: ref Sys->FD, msglen: int, out: chan of ref Icmp, pid: chan of int)
+{
+	pid <-= sys->pctl(0, nil);
+	buf := array[64*1024+512] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0){
+		now := nsec();
+		if(n < msglen){
+			sys->print("bad len %d/%d\n", n, msglen);
+			continue;
+		}
+		ic := Icmp.unpack(buf[0:n]);
+		ic.munged = 0;
+		for(i := Odata; i < msglen; i++)
+			if(buf[i] != byte i)
+				ic.munged++;
+		ic.time = now;
+		out <-= ic;
+	}
+	sys->print("read: %r\n");
+	out <-= nil;
+}
+
+reply(r: ref Req, ic: ref Icmp)
+{
+	rcvdmsgs++;
+	r.rtt /= big 1000;
+	sum += r.rtt;
+	if(!quiet && !lostonly){
+		if(addresses)
+			sys->print("%ud: %s->%s rtt %bd µs, avg rtt %bd µs, ttl = %d\n",
+				r.seq-firstseq,
+				ic.src.text(), ic.dst.text(),
+				r.rtt, sum/big rcvdmsgs, r.ttl);
+		else
+			sys->print("%ud: rtt %bd µs, avg rtt %bd µs, ttl = %d\n",
+				r.seq-firstseq,
+				r.rtt, sum/big rcvdmsgs, r.ttl);
+	}
+	r.replied = 1;	# TO DO: duplicates might be interesting
+}
+
+lost(r: ref Req, ic: ref Icmp)
+{
+	if(!quiet){
+		if(addresses)
+			sys->print("lost %ud: %s->%s avg rtt %bd µs\n",
+				r.seq-firstseq,
+				ic.src.text(), ic.dst.text(),
+				sum/big rcvdmsgs);
+		else
+			sys->print("lost %ud: avg rtt %bd µs\n",
+				r.seq-firstseq,
+				sum/big rcvdmsgs);
+	}
+	lostmsgs++;
+}
+
+Ovihl: con 0;
+Otos: con 1;
+Olength: con 2;
+Oid: con Olength+2;
+Ofrag: con Oid+2;
+Ottl: con Ofrag+2;
+Oproto: con Ottl+1;
+Oipcksum: con Oproto+1;
+Osrc: con Oipcksum+2;
+Odst: con Osrc+4;
+Otype: con Odst+4;
+Ocode: con Otype+1;
+Ocksum: con Ocode+1;
+Oicmpid: con Ocksum+2;
+Oseq: con Oicmpid+2;
+Odata: con Oseq+2;
+
+Icmp.unpack(b: array of byte): ref Icmp
+{
+	ic := ref Icmp;
+	ic.ttl = int b[Ottl];
+	ic.src = IPaddr.newv4(b[Osrc:]);
+	ic.dst = IPaddr.newv4(b[Odst:]);
+	ic.ptype = int b[Otype];
+	ic.code = int b[Ocode];
+	ic.seq = ip->get2(b, Oseq);
+	ic.munged = 0;
+	ic.time = big 0;
+	return ic;
+}
+
+timefd: ref Sys->FD;
+
+opentime()
+{
+	timefd = sys->open("/dev/time", Sys->OREAD);
+	if(timefd == nil){
+		sys->fprint(sys->fildes(2), "ping: can't open /dev/time: %r\n");
+		raise "fail:no time";
+	}
+}
+
+nsec(): big
+{
+	buf := array[64] of byte;
+	n := sys->pread(timefd, buf, len buf, big 0);
+	if(n <= 0)
+		return big 0;
+	return big string buf[0:n] * big 1000;
+}
--- /dev/null
+++ b/appl/cmd/ip/ppp/mkfile
@@ -1,0 +1,27 @@
+<../../../../mkconfig
+
+TARG=\
+	pppclient.dis\
+	pppdial.dis\
+	pppgui.dis\
+	ppptest.dis\
+	modem.dis\
+	script.dis\
+
+MODULES=\
+	modem.m\
+	pppclient.m\
+	pppgui.m\
+	script.m\
+
+SYSMODULES=\
+	sys.m\
+	draw.m\
+	tk.m\
+	dict.m\
+	string.m\
+	lock.m\
+
+DISBIN=$ROOT/dis/ip/ppp
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/ip/ppp/modem.b
@@ -1,0 +1,468 @@
+implement Modem;
+
+include "sys.m";
+	sys: Sys;
+
+include "lock.m";
+	lock: Lock;
+	Semaphore: import lock;
+
+include "draw.m";
+
+include "modem.m";
+
+hangupcmd := "ATH0";		# was ATZH0 but some modem versions on Umec hung on ATZ (BUG: should be in modeminfo)
+
+# modem return codes
+Ok, Success, Failure, Abort, Noise, Found: con iota;
+
+maxspeed: con 115200;
+
+#
+#  modem return messages
+#
+Msg: adt {
+	text: 		string;
+	code: 		int;
+};
+
+msgs: array of Msg = array [] of {
+	("OK", 			Ok),
+	("NO CARRIER", 	Failure),
+	("ERROR", 		Failure),
+	("NO DIALTONE", Failure),
+	("BUSY", 		Failure),
+	("NO ANSWER", 	Failure),
+	("CONNECT", 	Success),
+};
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "kill") < 0)
+		sys->print("modem: can't kill %d: %r\n", pid);
+}
+
+#
+# prepare a modem port
+#
+openserial(d: ref Device)
+{
+	if (d==nil) {
+		raise "fail: device not initialized";
+		return;
+	}
+
+	d.data = nil;
+	d.ctl = nil;
+
+	d.data = sys->open(d.local, Sys->ORDWR);
+	if(d.data == nil) {
+		raise "fail: can't open "+d.local;
+		return;
+	}
+
+	d.ctl = sys->open(d.local+"ctl", Sys->ORDWR);
+	if(d.ctl == nil) {
+		raise "can't open "+d.local+"ctl";
+		return;
+	}
+
+	d.speed = maxspeed;
+	d.avail = nil;
+}
+
+#
+# shut down the monitor (if any) and return the connection
+#
+
+close(m: ref Device): ref Sys->Connection
+{
+	if(m == nil)
+		return nil;
+	if(m.pid != 0){
+		kill(m.pid);
+		m.pid = 0;
+	}
+	if(m.data == nil)
+		return nil;
+	mc := ref sys->Connection(m.data, m.ctl, nil);
+	m.ctl = nil;
+	m.data = nil;
+	return mc;
+}
+
+#
+# Send a string to the modem
+#
+
+send(d: ref Device, x: string): int
+{
+	if (d == nil)
+		return -1;
+	
+	a := array of byte x;
+	f := sys->write(d.data, a, len a);
+	if (f < 0) {
+		# let's attempt to close & reopen the modem
+		close(d);
+		openserial(d);
+		f = sys->write(d.data,a, len a);
+	}
+	sys->print("->%s\n",x);
+	return f;
+}
+
+#
+#  apply a string of commands to modem & look for a response
+#
+
+apply(d: ref Device, s: string, substr: string, secs: int): int
+{
+	m := Ok;
+	buf := "";
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		buf[len buf] = c;		# assume no Unicode
+		if(c == '\r' || i == (len s -1)){
+			if(c != '\r')
+				buf[len buf] = '\r';
+			if(send(d, buf) < 0)
+				return Abort;
+			(m, nil) = readmsg(d, secs, substr);
+			buf = "";
+		}
+	}
+	return m;
+}
+
+#
+#  get modem into command mode if it isn't already
+#
+GUARDTIME: con 1100;	# usual default for S12=50 in units of 1/50 sec; allow 100ms fuzz
+
+attention(d: ref Device): int
+{
+	for(i := 0; i < 3; i++){
+		if(apply(d, hangupcmd, nil, 2) == Ok)
+			return Ok;
+		sys->sleep(GUARDTIME);
+		if(send(d, "+++") < 0) 
+			return Abort;
+		sys->sleep(GUARDTIME);
+		(nil, msg) := readmsg(d, 0, nil);
+		if(msg != nil)
+			sys->print("status: %s\n", msg);
+	}
+	return Failure;
+}
+
+#
+#  apply a command type
+#
+
+applyspecial(d: ref Device, cmd: string): int
+{
+	if(cmd == nil)
+		return Failure;
+	return apply(d, cmd, nil, 2);
+}
+
+#
+#  hang up any connections in progress and close the device
+#
+onhook(d: ref Device)
+{
+	if(d == nil)
+		return;
+
+	# hang up the modem
+	monitoring(d);
+	if(attention(d) != Ok)
+		sys->print("modem: no attention\n");
+
+	# hangup the stream (eg, for ppp) and toggle the lines to the modem
+	if(d.ctl != nil) {
+		sys->fprint(d.ctl,"d0\n");
+		sys->fprint(d.ctl,"r0\n");
+		sys->fprint(d.ctl, "h\n");	# hangup on native serial 
+		sys->sleep(250);
+		sys->fprint(d.ctl,"r1\n");
+		sys->fprint(d.ctl,"d1\n");
+	}
+
+	close(d);
+}
+
+#
+# does string s contain t anywhere?
+#
+
+contains(s, t: string): int
+{
+	if(t == nil)
+		return 1;
+	if(s == nil)
+		return 0;
+	n := len t;
+	for(i := 0; i+n <= len s; i++)
+		if(s[i:i+n] == t)
+			return 1;
+	return 0;
+}
+
+#
+#  read till we see a message or we time out
+#
+readmsg(d: ref Device, secs: int, substr: string): (int, string)
+{
+	if (d == nil)
+		return (Abort, "device not initialized");
+	found := 0;
+	secs *= 1000;
+	limit := 1000;		# pretty arbitrary
+	s := "";
+
+	for(start := sys->millisec(); sys->millisec() <= start+secs;){
+		a := getinput(d,1);
+		if(len a == 0){
+			if(limit){
+				sys->sleep(1);
+				continue;
+			}
+			break;
+		}
+		if(a[0] == byte '\n' || a[0] == byte '\r' || limit == 0){
+			if (len s) {
+				if (s[(len s)-1] == '\r')
+					s[(len s)-1] = '\n';
+				sys->print("<-%s\n",s);
+			}
+			if(substr != nil && contains(s, substr))
+				found = 1;
+			for(k := 0; k < len msgs; k++)
+				if(len s >= len msgs[k].text &&
+				   s[0:len msgs[k].text] == msgs[k].text){
+					if(found)
+						return (Found, s);
+					return (msgs[k].code, s);
+				}
+			start = sys->millisec();
+			s = "";
+			continue;
+		}
+		s[len s] = int a[0];
+		limit--;
+	}
+	s = "No response from modem";
+	if(found)
+		return (Found, s);
+
+	return (Noise, s);
+}
+
+#
+#  get baud rate from a connect message
+#
+
+getspeed(msg: string, speed: int): int
+{
+	p := msg[7:];	# skip "CONNECT"
+	while(p[0] == ' ' || p[0] == '\t')
+		p = p[1:];
+	s := int p;
+	if(s <= 0)
+		return speed;
+	else
+		return s;
+}
+
+#
+#  set speed and RTS/CTS modem flow control
+#
+
+setspeed(d: ref Device, baud: int)
+{
+	if(d != nil && d.ctl != nil){
+		sys->fprint(d.ctl, "b%d", baud);
+		sys->fprint(d.ctl, "m1");
+	}
+}
+
+dumpa(a: array of byte): string
+{
+	s := "";
+	for(i:=0; i<len a; i++){
+		b := int a[i];
+		if(b >= ' ' && b < 16r7f)
+			s[len s] = b;
+		else
+			s += sys->sprint("\\%.2x", b);
+	}
+	return s;
+}
+
+monitoring(d: ref Device)
+{
+	# if no monitor then spawn one
+	if(d.pid == 0) {
+		pidc := chan of int;
+		spawn monitor(d, pidc);
+		d.pid = <-pidc;
+	}
+}
+
+#
+#  a process to read input from a modem.
+#
+monitor(d: ref Device, pidc: chan of int)
+{
+	openserial(d);
+	pidc <-= sys->pctl(0, nil);	# pidc can be written once only.
+	a := array[Sys->ATOMICIO] of byte;
+	for(;;) {
+		d.lock.obtain();
+		d.status = "Idle";
+		d.remote = "";
+		setspeed(d, d.speed);
+		d.lock.release();
+		# shuttle bytes
+		while((n := sys->read(d.data, a, len a)) > 0){
+			d.lock.obtain();
+			if (len d.avail < Sys->ATOMICIO) {
+				na := array[len d.avail + n] of byte;
+				na[0:] = d.avail[0:];
+				na[len d.avail:] = a[0:n];
+				d.avail = na;
+			}				
+			d.lock.release();
+		}
+		# on an error, try reopening the device
+		d.data = nil;
+		d.ctl = nil;
+		openserial(d);
+	}
+}
+
+#
+#  return up to n bytes read from the modem by monitor()
+#
+getinput(d: ref Device, n: int): array of byte
+{
+	if(d==nil || n <= 0)
+		return nil;
+	a: array of byte;
+	d.lock.obtain();
+	if(len d.avail != 0){
+		if(n > len d.avail)
+			n = len d.avail;
+		a = d.avail[0:n];
+		d.avail = d.avail[n:];
+	}
+	d.lock.release();
+	return a;
+}
+
+getc(m: ref Device, timo: int): int
+{
+	start := sys->millisec();
+	while((b  := getinput(m, 1)) == nil) {
+		if (timo && sys->millisec() > start+timo)
+			return 0;
+		sys->sleep(1);
+	}
+	return int b[0];
+}
+
+init(modeminfo: ref ModemInfo): ref Device
+{
+	if (sys == nil) {
+		sys = load Sys Sys->PATH;
+		lock = load Lock Lock->PATH;
+		if (lock == nil) {
+			raise "fail: Couldn't load lock module";
+			return nil;
+		}
+		lock->init();
+	}
+
+	newdev := ref Device;
+	newdev.lock = Semaphore.new();
+	newdev.local = modeminfo.path;
+	newdev.pid = 0;
+	newdev.t = modeminfo;
+
+	return newdev;
+}
+
+
+#
+#  dial a number
+#
+dial(d: ref Device, number: string)
+{
+	if (d==nil) {
+		raise "fail: Device not initialized";
+		return;
+	}
+
+	monitoring(d);
+
+	# modem type should already be established, but just in case
+	sys->print("Attention\n");
+	x := attention(d);
+	if (x != Ok)
+		sys->print("Attention failed\n");
+	#
+	#  extended Hayes commands, meaning depends on modem (VGA all over again)
+	#
+	sys->print("Init\n");
+	if(d.t.country != nil)
+		applyspecial(d, d.t.country);
+
+	if(d.t.init != nil)
+		applyspecial(d, d.t.init);
+
+	if(d.t.other != nil)
+		applyspecial(d, d.t.other);
+
+	applyspecial(d, d.t.errorcorrection);
+
+	compress := Abort;
+	if(d.t.mnponly != nil)
+			compress = applyspecial(d, d.t.mnponly);
+	if(d.t.compression != nil)
+			compress = applyspecial(d, d.t.compression);
+
+	rateadjust := Abort;
+	if(compress != Ok)
+		rateadjust = applyspecial(d, d.t.rateadjust);
+	applyspecial(d, d.t.flowctl);
+
+	# finally, dialout
+	sys->print("Dialing\n");
+	if((dt := d.t.dialtype) == nil)
+		dt = "ATDT";
+	if(send(d, sys->sprint("%s%s\r", dt, number)) < 0) {
+		raise "can't dial "+number;
+		return;
+	}
+
+	(i, msg) := readmsg(d, 120, nil);
+	if(i != Success) {
+		raise "fail: "+msg;
+		return;
+	}
+
+	connectspeed := getspeed(msg, d.speed);
+
+	# change line rate if not compressing
+	if(rateadjust == Ok)
+		setspeed(d, connectspeed);
+
+	if(d.ctl != nil){
+		if(d != nil)
+			sys->fprint(d.ctl, "s%d", connectspeed);	# set DCE speed (if device implements it)
+		sys->fprint(d.ctl, "c1");	# enable CD monitoring
+	}
+}
--- /dev/null
+++ b/appl/cmd/ip/ppp/modem.m
@@ -1,0 +1,41 @@
+Modem: module
+{
+	PATH:	con "/dis/ip/ppp/modem.dis";
+
+	ModemInfo: adt {
+		path:			string;
+		init:			string;
+		country:		string;
+		other:			string;
+		errorcorrection:string;
+		compression:	string;
+		flowctl:		string;
+		rateadjust:		string;
+		mnponly:		string;
+		dialtype:		string;
+	};
+
+	Device: adt {
+		lock:	ref Lock->Semaphore;
+		# modem stuff
+		ctl:	ref Sys->FD;
+		data:	ref Sys->FD;
+
+		local:	string;
+		remote:	string;
+		status:	string;
+		speed:	int;
+		t:		ref ModemInfo;
+		# input reader
+		avail:	array of byte;
+		pid:		int;
+	};
+	
+	init:		fn(i: ref ModemInfo): ref Device;
+	dial:		fn( m: ref Device, number: string);
+	getc:		fn(m: ref Device, timout: int): int;
+	getinput:	fn(m: ref Device, n: int ): array of byte;
+	send:		fn(m: ref Device, x: string): int;
+	close:	fn(m: ref Device): ref Sys->Connection;
+	onhook:		fn(m: ref Device);
+};
--- /dev/null
+++ b/appl/cmd/ip/ppp/pppclient.b
@@ -1,0 +1,216 @@
+implement PPPClient;
+
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+
+include "lock.m";
+include "modem.m";
+include "script.m";
+
+include "pppclient.m";
+
+include "translate.m";
+	translate : Translate;
+	Dict : import translate;
+	dict : ref Dict;
+
+#
+# Globals (these will have to be removed if we are going multithreaded)
+#
+
+pid := 0;
+modeminfo:	ref	Modem->ModemInfo;
+pppdir: string;
+
+ppplog(log: chan of int, errfile: string, pidc: chan of int) 
+{
+	pidc <-= sys->pctl(0, nil);				# set reset pid to our pid 
+	src := sys->open(errfile, Sys->OREAD);
+	if (src == nil)
+		raise sys->sprint("fail: Couldn't open %s: %r", errfile);
+
+	LOGBUFMAX:	con 1024;
+	buf := array[LOGBUFMAX] of byte;
+	connected := 0;
+
+    	while ((count := sys->read(src, buf, LOGBUFMAX)) > 0) {
+	    	(n, toklist) := sys->tokenize(string buf[:count],"\n");
+	    	for (;toklist != nil;toklist = tl toklist) {
+			case hd toklist {
+				"no error" =>
+					log <-= s_SuccessPPP;
+					lasterror = nil;
+					connected = 1;
+				"permission denied" =>
+					lasterror = X("Username or Password Incorrect");
+					log <-= s_Error;
+				"write to hungup channel" =>
+					lasterror = X("Remote Host Hung Up");
+					log <-= s_Error;
+				* =>
+					lasterror = X(hd toklist);
+					log <-= s_Error;
+			}
+		}
+	}
+	if(count == 0 && connected && lasterror == nil){	# should change ip/pppmedium.c instead?
+		lasterror = X("Lost Connection");
+		log <-= s_Error;
+	}
+}
+
+startppp(logchan: chan of int, pppinfo: ref PPPInfo)
+{
+	ifd := sys->open("/net/ipifc/clone", Sys->ORDWR);
+	if (ifd == nil)
+		raise "fail: Couldn't open /net/ipifc/clone";
+
+	buf := array[32] of byte;
+	n := sys->read(ifd, buf, len buf);
+	if(n <= 0)
+		raise "fail: can't read from /net/ipifc/clone";
+
+	pppdir = "/net/ipifc/" + string buf[0:n];
+	pidc := chan of int;
+	spawn ppplog(logchan, pppdir + "/err", pidc);
+	pid = <-pidc;
+	logchan <-= s_StartPPP;
+
+	if (pppinfo.ipaddr == nil)
+		pppinfo.ipaddr = "-";
+#	if (pppinfo.ipmask == nil)
+#		pppinfo.ipmask = "255.255.255.255";
+	if (pppinfo.peeraddr == nil)
+		pppinfo.peeraddr = "-";
+	if (pppinfo.maxmtu == nil)
+		pppinfo.maxmtu = "512";
+	if (pppinfo.username == nil)
+		pppinfo.username = "-";
+	if (pppinfo.password == nil)
+		pppinfo.password = "-";
+	framing := "1";
+
+	ifc := "bind ppp "+modeminfo.path+" "+ pppinfo.ipaddr+" "+pppinfo.peeraddr+" "+pppinfo.maxmtu
+			+" "+framing+" "+pppinfo.username+" "+pppinfo.password;
+
+	# send the add command
+	if (sys->fprint(ifd, "%s", ifc) < 0) {
+		sys->print("pppclient: couldn't write %s/ctl: %r\n", pppdir);
+		raise "fail: Couldn't write /net/ipifc";
+		return;
+	}
+}
+
+connect(mi: ref Modem->ModemInfo, number: string,
+		scriptinfo: ref Script->ScriptInfo, pppinfo: ref PPPInfo, logchan: chan of int)
+{
+	sys = load Sys Sys->PATH;
+
+	translate = load Translate Translate->PATH;
+	if (translate != nil) {
+		translate->init();
+		dictname := translate->mkdictname("", "pppclient");
+		(dict, nil) = translate->opendict(dictname);
+	}
+	if (pid != 0)			# yikes we are already running
+		reset();
+
+	# create a new process group
+	pid = sys->pctl( Sys->NEWPGRP, nil);
+	
+	{
+		logchan <-= s_Initialized;
+	
+		# open & init the modem
+		modeminfo = mi;
+		modem := load Modem Modem->PATH;
+		if (modem == nil) {
+			raise "fail: Couldn't load modem module";
+			return;
+		}
+	
+		modemdev := modem->init(modeminfo);
+		logchan <-= s_StartModem;
+		modem->dial(modemdev, number);
+		logchan <-= s_SuccessModem;
+
+		# if script
+		if (scriptinfo != nil) {
+			script := load Script Script->PATH;
+			if (script == nil) {
+				raise "fail: Couldn't load script module";
+				return;
+			}
+			logchan <-= s_StartScript;
+			script->execute(modem, modemdev, scriptinfo);
+			logchan <-= s_SuccessScript;
+		}
+
+		mc := modem->close(modemdev);	# keep connection open for ppp mode
+		modemdev = nil;
+		modem = nil;	# unload modem module
+
+		# if ppp
+		if (pppinfo != nil) 
+			startppp(logchan, pppinfo);
+		else
+			logchan <-= s_Done;
+	}
+	exception e{
+		"fail*" =>
+			lasterror = e;
+			sys->print("PPPclient: fatal exception: %s\n", e);
+			logchan <-= s_Error;
+			kill(pid, "killgrp");
+			exit;
+	}
+}
+
+reset()
+{
+	sys->print("reset...");
+	if(pid != 0){
+		kill(pid, "killgrp");
+		pid = 0;
+	}
+
+	if(pppdir != nil){	# shut down the PPP link
+		fd := sys->open(pppdir + "/ctl", Sys->OWRITE);
+		if(fd == nil || sys->fprint(fd, "unbind") < 0)
+			sys->print("pppclient: can't unbind: %r\n");
+		fd = nil;
+		pppdir = nil;
+	}
+
+	modem := load Modem Modem->PATH;
+	if (modem == nil) {
+		raise "fail: Couldn't load modem module";
+		return;
+	}
+	modemdev := modem->init(modeminfo);
+	if(modemdev != nil)
+		modem->onhook(modemdev);
+	modem = nil;
+
+	# clear error buffer
+	lasterror = nil;
+}
+
+kill(pid: int, msg: string)
+{
+	a := array of byte msg;
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->write(fd, a, len a) < 0)
+		sys->print("pppclient: can't %s %d: %r\n", msg, pid);
+}
+
+# Translate a string 
+
+X(s : string) : string
+{
+	if (dict== nil) return s;
+	return dict.xlate(s);
+}
+
--- /dev/null
+++ b/appl/cmd/ip/ppp/pppclient.m
@@ -1,0 +1,31 @@
+
+PPPClient:	module {
+	PATH:	con "/dis/ip/ppp/pppclient.dis";
+
+	PPPInfo: adt {
+		ipaddr:			string;
+		ipmask:			string;
+		peeraddr:		string;
+		maxmtu:			string;
+		username:		string;
+		password:		string;
+	};
+	
+	connect:	fn( mi: ref Modem->ModemInfo, number: string, 
+					scriptinfo: ref Script->ScriptInfo, 
+					pppinfo: ref PPPInfo, logchan: chan of int);
+	reset:		fn();
+
+	lasterror :string;
+
+	s_Error: con -666;
+	s_Initialized,			# Module Initialized
+	s_StartModem,			# Modem Initialized
+	s_SuccessModem,			# Modem Connected
+	s_StartScript,			# Script Executing
+	s_SuccessScript,		# Script Executed Sucessfully
+	s_StartPPP,				# PPP Started
+	s_LoginPPP,				# CHAP/PAP Authentication
+	s_SuccessPPP,			# PPP Session Established
+	s_Done: con iota;		# PPPClient Cleaningup & Exiting
+};
--- /dev/null
+++ b/appl/cmd/ip/ppp/pppdial.b
@@ -1,0 +1,283 @@
+implement PPPdial;
+
+#
+#	Module:		ispservice
+#	Purpose:		Simple PPP Dial-on-Demand
+#	Author:		Eric Van Hensbergen (ericvh@lucent.com)
+#
+# Copyright © 1998-1999 Lucent Technologies Inc.  All rights reserved.
+# Revisions copyright © 2000-2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+
+include "cfgfile.m";
+	cfg:	CfgFile;
+	ConfigFile: import cfg;
+
+include "lock.m";
+include "modem.m";
+include "script.m";
+include "pppclient.m";
+	ppp: PPPClient;
+include "pppgui.m";
+
+PPPdial: module
+{
+	init:	fn(nil: ref Draw->Context): string;
+	connect:	fn(): string;
+};
+
+context:		ref Draw->Context;
+modeminfo:		ref Modem->ModemInfo;
+pppinfo:		ref PPPClient->PPPInfo;
+scriptinfo:		ref Script->ScriptInfo;
+isp_number:		string;						# should be part of pppinfo
+lastCdir:		ref Sys->Dir;	# state of file when last read
+
+DEFAULT_ISP_DB_PATH:	con "/services/ppp/isp.cfg";	# contains pppinfo & scriptinfo
+DEFAULT_MODEM_DB_PATH:	con	"/services/ppp/modem.cfg";			# contains modeminfo
+MODEM_DB_PATH:	con	"/usr/inferno/config/modem.cfg";			# contains modeminfo
+ISP_DB_PATH:	con "/usr/inferno/config/isp.cfg";		# contains pppinfo & scriptinfo
+ISP_RETRIES:	con 5;
+
+getcfgstring(c: ref ConfigFile, key: string) :string
+{
+	l := c.getcfg(key);
+	if (l == nil)
+		return nil;
+	for(ret := ""; l != nil; l = tl l)
+		ret+= " " + hd l;
+	
+	return ret[1:];		# trim the first space
+}
+
+configinit()
+{
+	mi:	Modem->ModemInfo;
+	pppi: PPPClient->PPPInfo;
+	info: list of string;
+
+	cfg = load CfgFile CfgFile->PATH;
+	if (cfg == nil)
+		raise "fail: load CfgFile";
+
+	# Modem Configuration
+	
+	cfg->verify(DEFAULT_MODEM_DB_PATH, MODEM_DB_PATH);
+	modemcfg := cfg->init(MODEM_DB_PATH);
+	if (modemcfg == nil)
+		raise "fail: read: "+MODEM_DB_PATH;
+	modeminfo = ref mi;
+
+	modeminfo.path = getcfgstring(modemcfg, "PATH");
+	modeminfo.init = getcfgstring(modemcfg, "INIT");
+	modeminfo.country = getcfgstring(modemcfg, "COUNTRY");
+	modeminfo.other = getcfgstring(modemcfg, "OTHER");
+	modeminfo.errorcorrection = getcfgstring(modemcfg,"CORRECT");
+	modeminfo.compression = getcfgstring(modemcfg,"COMPRESS");
+	modeminfo.flowctl = getcfgstring(modemcfg,"FLOWCTL");
+	modeminfo.rateadjust = getcfgstring(modemcfg,"RATEADJ");
+	modeminfo.mnponly = getcfgstring(modemcfg,"MNPONLY");
+	modeminfo.dialtype = getcfgstring(modemcfg,"DIALING");
+	if(modeminfo.dialtype!="ATDP")
+		modeminfo.dialtype="ATDT";
+
+	cfg->verify(DEFAULT_ISP_DB_PATH, ISP_DB_PATH);
+	(ok, stat) := sys->stat(ISP_DB_PATH);
+	if(ok >= 0)
+		lastCdir = ref stat;
+	sys->print("cfg->init(%s)\n", ISP_DB_PATH);
+
+	# ISP Configuration
+	pppcfg := cfg->init(ISP_DB_PATH);
+	if (pppcfg == nil)
+		raise "fail: Couldn't load ISP configuration file: "+ISP_DB_PATH;
+	pppinfo = ref pppi;
+	isp_number = getcfgstring(pppcfg, "NUMBER");
+	pppinfo.ipaddr = getcfgstring(pppcfg,"IPADDR");
+	pppinfo.ipmask = getcfgstring(pppcfg,"IPMASK");
+	pppinfo.peeraddr = getcfgstring(pppcfg,"PEERADDR");
+	pppinfo.maxmtu = getcfgstring(pppcfg,"MAXMTU");
+	pppinfo.username = getcfgstring(pppcfg,"USERNAME");
+	pppinfo.password = getcfgstring(pppcfg,"PASSWORD");
+
+	info = pppcfg.getcfg("SCRIPT");
+	if (info != nil) {
+		scriptinfo = ref Script->ScriptInfo;
+		scriptinfo.path = hd info;
+		scriptinfo.username = pppinfo.username;
+		scriptinfo.password = pppinfo.password;
+	} else
+		scriptinfo = nil;
+
+	info = pppcfg.getcfg("TIMEOUT");
+	if (info != nil)
+		scriptinfo.timeout = int (hd info);
+
+	cfg = nil;	# might as well unload it
+}
+
+#
+# Parts of the following two functions could be generalized
+#
+
+isipaddr(a: string): int
+{
+	i, c, ac, np: int = 0;
+ 
+	for(i = 0; i < len a; i++) {
+		c = a[i];
+		if(c >= '0' && c <= '9') {
+			np = 10*np + c - '0';
+			continue;
+		}
+		if (c == '.' && np) {
+			ac++;
+	 		if (np > 255)
+				return 0;
+			np = 0;
+			continue;
+		}
+		return 0;
+	}
+	return np && np < 256 && ac == 3;
+}
+
+# check if there is an existing PPP connection
+connected(): int
+{
+	ifd := sys->open("/net/ipifc", Sys->OREAD);
+	if(ifd == nil)
+		return 0;
+
+	buf := array[1024] of byte;
+
+	for(;;) {
+		(n, d) := sys->dirread(ifd);
+		if (n <= 0)
+			return 0;
+		for(i := 0; i < n; i++)
+			if(d[i].name[0] <= '9') {
+				sfd := sys->open("/net/ipifc/"+d[i].name+"/status", Sys->OREAD);
+				if (sfd == nil)
+					continue;
+				ns := sys->read(sfd, buf, len buf);
+				if (ns <= 0)
+					continue;
+				(nflds, flds) := sys->tokenize(string buf[0:ns], " \t\r\n");
+				if(nflds < 4)
+					continue;
+				if (isipaddr(hd tl tl flds))
+					return 1;
+			}
+	}
+}
+
+#
+# called once when loaded
+#
+init(ctxt: ref Draw->Context): string
+{
+	sys = load Sys Sys->PATH;
+	{
+		ppp = load PPPClient PPPClient->PATH;
+		if (ppp == nil)
+			raise "fail: Couldn't load ppp module";
+
+		# Contruct Config Tables During Init - may want to change later
+		#	for multiple configs (Software Download Server versus ISP)
+		configinit();	
+		context = ctxt;
+	}exception e {
+	"fail:*" =>
+		return e;
+	}
+	return nil;
+}
+
+dialup_cancelled := 0;
+connecting := 0;
+
+#
+# called each time a translation is needed, to check that we're on line(!)
+# eventually this will be replaced by a packet interface that does dial-on-demand
+#
+connect(): string
+{
+	{
+		dialup_cancelled = 0;
+		(ok, stat) := sys->stat(ISP_DB_PATH);
+		if (ok < 0 || lastCdir == nil || !samefile(*lastCdir, stat))
+			configinit();
+		errc := chan of string;
+		while(!connected()){
+			if(!connecting) {
+				connecting = 1;
+				sync := chan of int;
+				spawn pppconnect(errc, sync);
+				<- sync;
+				return <-errc;
+			}else{
+				sys->sleep(2500);
+				if (dialup_cancelled)
+					return "fail: dialup cancelled";	
+			}
+		}
+	}exception e{
+	"fail:*" =>
+		return e;
+	"*" =>
+		sys->print("pppdial: caught exception: %s\n", e);
+		return "fail: internal error: "+e;
+	}
+	return nil;	
+}
+
+pppconnect(errc: chan of string, sync: chan of int)
+{
+	connecting = 1;
+	sys->pctl(Sys->NEWPGRP, nil);
+	sync <-= 0;
+	resp_chan: chan of int;
+	logger := chan of int;
+	pppgui := load PPPGUI PPPGUI->PATH;
+	for (count :=0; count < ISP_RETRIES; count++) {
+		resp_chan = pppgui->init(context, logger, ppp, nil);
+		spawn ppp->connect(modeminfo, isp_number, scriptinfo, pppinfo, logger);
+		x := <-resp_chan;
+		if (x > 0) {
+			if (x == 1) {
+				# alt needed in case calling process has been killed
+				alt {
+					errc <-= nil => ;
+					* => ;
+				}
+			} else	{		# user cancelled dial-in
+				dialup_cancelled = 1;
+				alt {
+					errc <-= "fail: dialup cancelled" => ;
+					* => ;
+				}
+			}
+			connecting = 0;
+			return;
+		}
+		# else connect failed, go around loop to try again
+	}
+	alt {
+		errc <-= "fail: dialup failed" => ;
+		* => ;
+	}
+	connecting = 0;
+}
+
+samefile(d1, d2: Sys->Dir): int
+{
+	return d1.dev==d2.dev && d1.dtype==d2.dtype &&
+			d1.qid.path==d2.qid.path && d1.qid.vers==d2.qid.vers &&
+			d1.mtime==d2.mtime;
+}
--- /dev/null
+++ b/appl/cmd/ip/ppp/pppgui.b
@@ -1,0 +1,373 @@
+#
+# Copyright © 1998 Lucent Technologies Inc.  All rights reserved.
+# Revisions copyright © 2000,2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# Originally Written by N. W. Knauft
+# Adapted by E. V. Hensbergen (ericvh@lucent.com)
+# Further adapted by Vita Nuova
+#
+
+implement PPPGUI;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "translate.m";
+	translate: Translate;
+	Dict: import translate;
+	dict: ref Dict;
+
+include "lock.m";
+include "modem.m";
+include "script.m";
+include "pppclient.m";
+	ppp: PPPClient;
+
+include "pppgui.m";
+
+#Screen constants
+BBG: con "#C0C0C0";             # Background color for button
+PBG: con "#808080";             # Background color for progress bar
+LTGRN: con "#00FF80";           # Color for progress bar
+BARW: con 216;			# Progress bar width
+BARH: con " 9";			# Progress bar height
+INCR: con 30;			# Progress bar increment size
+N_INCR: con 7;			# Number of increments in progress bar width
+BSIZE: con 25;			# Icon button size
+ISIZE: con BSIZE + 4;		# Icon window size
+DIALQUANTA : con 1000;
+ICONQUANTA : con 5000;
+
+#Globals
+pppquanta := DIALQUANTA;
+
+#Font
+FONT: con "/fonts/lucidasans/unicode.6.font";
+
+#Messages
+stat_msgs := array[] of {
+	"Initializing Modem",
+	"Dialling Service Provider",
+	"Logging Into Network",
+	"Executing Login Script",
+	"Script Execution Complete",
+	"Logging Into Network",
+	"Verifying Password",
+	"Connected",
+	"",
+};
+
+config_icon := array[] of {
+	"button .btn -text X -width "+string BSIZE+" -height "+string BSIZE+" -command {send tsk open} -bg "+BBG,
+	"pack .btn",
+
+	"pack propagate . no",
+	". configure -bd 0",
+	". unmap",
+	"update",
+};
+
+
+# Create internet connect window, spawn event handler
+init(ctxt: ref Draw->Context, stat: chan of int, pppmod: PPPClient, args: list of string): chan of int
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+
+	if (draw == nil || tk == nil || tkclient == nil) {
+		sys->fprint(sys->fildes(2), "pppgui: can't load Draw or Tk: %r\n");
+		return nil;
+	}
+
+	translate = load Translate Translate->PATH;
+	if(translate != nil) {
+		translate->init();
+		dictname := translate->mkdictname("", "pppgui");
+		dicterr: string;
+		(dict, dicterr) = translate->opendict(dictname);
+		if(dicterr != nil)
+			sys->fprint(sys->fildes(2), "pppgui: can't open %s: %s\n", dictname, dicterr);
+	}else
+		sys->fprint(sys->fildes(2), "pppgui: can't load %s: %r\n", Translate->PATH);
+	ppp = pppmod;		# set the global
+
+	tkargs := "";
+
+	if (args != nil) {
+		tkargs = hd args;
+		args = tl args;
+	} else
+		tkargs="-x 340 -y 4";
+
+	tkclient->init();
+		
+	(t, wmctl) := tkclient->toplevel(ctxt, tkargs, "PPP", Tkclient->Plain);
+
+	config_win := array[] of {
+		"frame .f",
+		"frame .fprog",
+
+		"canvas .cprog -bg "+PBG+" -bd 2 -width "+string BARW+" -height "+BARH+" -relief ridge",	
+		"pack .cprog -in .fprog -pady 6",
+
+		"label .stat -text {"+X("Initializing connection...")+"} -width 164 -font "+FONT,
+		"pack .stat -in .f -side left -fill y -anchor w",
+
+		"button .done -text {"+X("Cancel")+"} -width 60 -command {send cmd cancel} -bg "+BBG+" -font "+FONT,
+		"pack .fprog -side bottom -expand 1 -fill x",
+		"pack .done -side right -padx 1 -pady 1 -fill y -anchor e",
+		"pack .f -side left -expand 1 -padx 5 -pady 3 -fill both -anchor w",
+
+		"pack propagate . no",
+		". configure -bd 2 -relief raised -width "+string WIDTH,
+		"update",
+	};
+
+	for(i := 0; i < len config_win; i++)
+		tk->cmd(t, config_win[i]);
+
+	itkargs := "";
+	if (args != nil) {
+		itkargs = hd args;
+		args = tl args;
+	}
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "ptr" :: nil);
+
+	if (itkargs == "") {
+		x := int tk->cmd(t, ". cget x");
+		y := int tk->cmd(t, ". cget y");
+		x += WIDTH - ISIZE;
+		itkargs = "-x "+string x+" -y "+string y;
+	}
+
+	(ticon, iconctl) := tkclient->toplevel(ctxt, itkargs, "PPP", Tkclient->Plain);
+
+	for( i = 0; i < len config_icon; i++)
+		tk->cmd(ticon, config_icon[i]);
+
+	tk->cmd(ticon, "image create bitmap Network -file network.bit -maskfile network.bit");
+	tk->cmd(ticon, ".btn configure -image Network");
+	tkclient->startinput(ticon, "ptr"::nil);
+
+	chn := chan of int;
+	spawn handle_events(t, wmctl, ticon, iconctl, stat, chn);
+	return chn;
+}
+
+ppp_timer(sync: chan of int, stat: chan of int)
+{
+	for(;;) {
+		sys->sleep(pppquanta);
+		alt {
+		<-sync =>
+			return;
+		stat <-= -1 =>
+			;
+		}
+	}
+}
+
+send(cmd: chan of string, msg: string)
+{
+	cmd <-= msg;
+}
+
+# Process events and pass disconnect cmd to calling app
+handle_events(t: ref Tk->Toplevel, wmctl: chan of string, ticon: ref Tk->Toplevel, iconctl: chan of string, stat, chn: chan of int)
+{
+	sys->pctl(Sys->NEWPGRP, nil);
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	tsk := chan of string;
+	tk->namechan(ticon, tsk, "tsk");
+
+	connected := 0;
+	winmapped := 1;
+	timecount := 0;
+	xmin := 0;
+	x := 0;
+
+	iocmd := sys->file2chan("/chan", "pppgui");
+	if (iocmd == nil) {
+		sys->print("fail: pppgui: file2chan: /chan/pppgui: %r\n");
+		return;
+	}
+
+	pppquanta = DIALQUANTA;
+	sync_chan := chan of int;
+	spawn ppp_timer(sync_chan, stat);
+
+Work:
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-wmctl =>
+		tkclient->wmctl(t, s);
+
+	s := <-ticon.ctxt.kbd =>
+		tk->keyboard(ticon, s);
+	s := <-ticon.ctxt.ptr =>
+		tk->pointer(ticon, *s);
+	s := <-ticon.ctxt.ctl or
+	s = <-ticon.wreq or
+	s = <-iconctl =>
+		tkclient->wmctl(ticon, s);
+
+	(off, data, fid, wc) := <-iocmd.write =>	# remote io control
+		if (wc == nil)
+			break;
+		spawn send(cmd, string data[0:len data]);
+		wc <-= (len data, nil);
+
+	(nil, nbytes, fid, rc) := <-iocmd.read =>
+		if (rc != nil)
+			rc <-= (nil, "not readable");
+
+	press := <-cmd =>
+		case press {
+		"cancel" or "disconnect" =>
+			tk->cmd(t, ".stat configure -text 'Disconnecting...");
+			tk->cmd(t, "update");
+			ppp->reset();
+			if (!connected) {
+				# other end may have gone away
+				alt {
+					chn <-= 666 => ;
+					* => ;
+				}
+			}
+			break Work;
+		* => ;
+		}
+
+	prs := <-tsk =>
+		case prs {
+		"open" =>
+			tk->cmd(ticon, ". unmap; update");
+			tk->cmd(t, ". map; raise .; update");
+			winmapped = 1;
+			timecount = 0;
+		* => ;
+		}
+
+	s := <-stat =>
+		if (s == -1) {	# just an update event
+			if(winmapped){
+				if(!connected) {	# increment status bar
+					if (x < xmin+INCR) {
+						x++;
+						tk->cmd(t, ".cprog create rectangle 0 0 "+string x + BARH+" -fill "+LTGRN);
+					}
+				}else{
+					timecount++;
+					if(timecount > 1){
+						winmapped = 0;
+						timecount = 0;
+						tk->cmd(t, ". unmap; update");
+						tk->cmd(ticon, ". map; raise .; update");
+						continue;
+					}
+				}
+				tk->cmd(t, "raise .; update");
+			} else {
+				tk->cmd(ticon, "raise .; update");
+				timecount = 0;
+			}
+			continue;
+		}
+		if (s == ppp->s_Error) {
+			tk->cmd(t, ".stat configure -text '"+ppp->lasterror);
+			if (!winmapped) {
+				tk->cmd(ticon, ". unmap; update");
+				tk->cmd(t, ". map; raise .");
+			}
+			tk->cmd(t, "update");
+			sys->sleep(3000);	
+			ppp->reset();
+			if (!connected)
+				chn <-= 0;			# Failure	
+			break Work;
+		}
+	
+		if (s == ppp->s_Initialized)
+			tk->cmd(t,".cprog create rectangle 0 0 "+string BARW + BARH+" -fill "+PBG);
+		
+		x = xmin = s * INCR;
+		if (xmin > BARW)
+			xmin = BARW;
+		tk->cmd(t, ".cprog create rectangle 0 0 "+string xmin + BARH+" -fill "+LTGRN);
+		tk->cmd(t, "raise .; update");
+		tk->cmd(t, ".stat configure -text '"+X(stat_msgs[s]));
+
+		if (s == ppp->s_SuccessPPP || s == ppp->s_Done) {
+			if(!connected){
+				chn <-= 1;
+				connected = 1;
+			}
+			pppquanta = ICONQUANTA;
+
+			# find and display connection speed
+			speed := findrate("/dev/modemstat", "rcvrate" :: "baud" :: nil);
+			if(speed != nil)
+				tk->cmd(t, ".stat configure -text {"+X(stat_msgs[s])+" "+speed+" bps}");
+			else
+				tk->cmd(t, ".stat configure -text {"+X(stat_msgs[s])+"}");
+			tk->cmd(t, ".done configure -text Disconnect -command 'send cmd disconnect");
+			tk->cmd(t, "update");
+			sys->sleep(2000);	
+			tk->cmd(t, ". unmap; pack forget .fprog; update");
+			winmapped = 0;
+			tk->cmd(ticon, ". map; raise .; update");
+		}
+
+		tk->cmd(t, "update");
+	}
+	sync_chan <-= 1;	# stop ppp_timer
+}
+
+findrate(file: string, opt: list of string): string
+{
+	fd := sys->open(file, sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array [1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 1)
+		return nil;
+	(nil, flds) := sys->tokenize(string buf[0:n], " \t\r\n");
+	for(; flds != nil; flds = tl flds)
+		for(l := opt; l != nil; l = tl l)
+			if (hd flds == hd l)
+				return hd tl flds;
+	return nil;
+}
+
+
+
+# Translate a string 
+
+X(s : string) : string
+{
+	if (dict== nil) return s;
+	return dict.xlate(s);
+}
+
--- /dev/null
+++ b/appl/cmd/ip/ppp/pppgui.m
@@ -1,0 +1,21 @@
+#
+# Copyright © 1998 Lucent Technologies Inc.  All rights reserved.
+# Revisions copyright © 2000,2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# Originally Written by N. W. Knauft
+# Adapted by E. V. Hensbergen (ericvh@lucent.com)
+# Further adapted by Vita Nuova
+#
+
+PPPGUI: module
+{
+        PATH:	con "/dis/ip/ppp/pppgui.dis";
+
+	# Dimension constant for ISP Connect window
+	WIDTH: con 300;
+	HEIGHT: con 58;
+
+        init:	fn(ctxt: ref Draw->Context, stat: chan of int,
+			ppp: PPPClient, args: list of string): chan of int;
+};
+
--- /dev/null
+++ b/appl/cmd/ip/ppp/ppptest.b
@@ -1,0 +1,86 @@
+#    Last change:  R    24 May 2001   11:05 am
+implement PPPTest;
+
+include "sys.m";
+	sys:	Sys;
+include "draw.m";
+
+include "lock.m";
+include "modem.m";
+include "script.m";
+include "pppclient.m";
+include "pppgui.m";
+
+PPPTest: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+usage()
+{
+    sys->print("ppptest device modem_init tel user password \n");
+	sys->print("Example: ppptest /dev/modem atw2 4125678 rome xxxxxxxx\n");
+	exit;
+	
+}
+init( ctxt: ref Draw->Context, argv: list of string )
+{
+	sys = load Sys Sys->PATH;
+
+	mi:	Modem->ModemInfo;
+	pi:	PPPClient->PPPInfo;
+	tel : string;
+#	si:	Script->ScriptInfo;
+	argv = tl argv;
+    if(argv == nil)
+	    usage();
+	else
+		mi.path = hd argv;
+
+	argv = tl argv;
+	if(argv == nil)
+	    usage();
+	else
+		mi.init = hd argv;
+	argv = tl argv;
+	if(argv == nil)
+	    usage();
+	else
+	    tel = hd argv;
+	argv = tl argv;
+	if(argv == nil)
+	    usage();
+	else
+	    pi.username = hd argv;
+	argv = tl argv;
+	if(argv==nil)
+	    usage();
+	else
+	    pi.password = hd argv;
+
+
+	#si.path = "rdid.script";
+	#si.username = "ericvh";
+	#si.password = "foobar";
+	#si.timeout = 60;
+
+
+	ppp := load PPPClient PPPClient->PATH;
+
+	logger := chan of int;
+
+	spawn ppp->connect( ref mi, tel, nil, ref pi, logger );
+	
+	pppgui := load PPPGUI PPPGUI->PATH;
+	respchan := pppgui->init( ctxt, logger,ppp, nil);
+
+	event := 0;
+	while (1) {
+		event =<- respchan;
+		sys->print("GUI event received: %d\n",event);
+		if (event) {
+			sys->print("success");
+			exit;
+		} else {
+			raise "fail: Couldn't connect to ISP";
+		}
+	}	
+}
--- /dev/null
+++ b/appl/cmd/ip/ppp/script.b
@@ -1,0 +1,168 @@
+implement Script;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "lock.m";
+include "modem.m";
+	modem: Modem;
+
+include "script.m";
+
+delim:	con "-";			# expect-send delimiter
+BUFSIZE: con (1024 * 32);
+
+execute( modmod: Modem, m: ref Modem->Device, scriptinfo: ref ScriptInfo )
+{
+	sys= load Sys Sys->PATH;
+	str= load String String->PATH;
+	if (str == nil) {
+		raise "fail: couldn't load string module";
+		return;
+	}
+	modem = modmod;
+
+	if (scriptinfo.path != nil) {
+		sys->print("Executing Script %s\n",scriptinfo.path);
+		# load the script
+		scriptinfo.content = scriptload(scriptinfo.path);
+	} else {
+		sys->print("Executing Inline Script\n");
+	}
+	
+	# Check for timeout variable
+
+	if (scriptinfo.timeout == 0)
+		scriptinfo.timeout = 20;
+
+	tend := sys->millisec() + 1000*scriptinfo.timeout;
+
+	conv := scriptinfo.content;
+
+	while (conv != nil)  {
+		e, s:	string = nil;
+		p := hd conv;
+		conv = tl conv;
+		if (len p == 0)
+			continue;
+		sys->print("script: %s\n",p);
+		if (p[0] == '-') {	# just send
+			if (len p == 1)
+				continue;
+			s = p[1:];
+		} else {
+			(n, esl) := sys->tokenize(p, delim);
+			if (n > 0) {
+				e = hd esl;
+				esl = tl esl;
+				if (n > 1)
+					s = hd esl;
+			}
+		}
+		if (e  != nil) {
+			if (match(m, special(e,scriptinfo), tend-sys->millisec()) == 0) {
+				sys->print("script: match failed\n");
+				raise "fail: Script Failed";
+				return;
+			}
+		}
+		if (s != nil)
+			modem->send(m, special(s, scriptinfo));
+	}
+
+	sys->print("script: done!\n");
+}
+
+match(m: ref Modem->Device, s: string, timo: int): int
+{
+	for(;;) {
+		c := modem->getc(m, timo);
+		if (c ==  '\r')
+			c = '\n';
+		sys->print("%c",c);
+		if (c == 0)
+			return 0;
+	head:
+		while(c == s[0]) {
+			i := 1;
+			while(i < len s) {
+				c = modem->getc(m, timo);
+				if (c == '\r')
+					c = '\n';
+				sys->print("%c",c);
+				if(c == 0)
+					return 0;
+				if(c != s[i])
+					continue head;
+				i++;
+			}
+			return 1;
+		}
+		if(c == '~')
+			return 1;	# assume PPP for now
+	}
+}
+
+#
+# Expand special script sequences
+#
+special(s: string, scriptinfo: ref ScriptInfo ): string
+{
+	if (s == "$username") 					# special variable
+		s = scriptinfo.username;
+	else if (s == "$password") 
+		s = scriptinfo.password;
+ 	
+	return deparse(s);
+}
+
+deparse(s : string) : string
+{
+	r: string = "";
+	for(i:=0; i < len s; i++) {
+		c := s[i];
+		if (c == '\\'  && i+1 < len s) {
+			c = s[++i];
+			case c {
+			't' => c = '\t';
+			'n'	=> c = '\n';
+			'r'	=> c = '\r';
+			'b'	=> c = '\b';
+			'a'	=> c = '\a';
+			'v'	=> c = '\v';
+			'0'	=> c = '\0';
+			'$' => c = '$';
+			'u'	=> 
+				if (i+4 < len s) {
+					i++;
+					(c, nil) = str->toint(s[i:i+4], 16);
+					i+=3;
+				}
+			}
+		}
+		r[len r] = c;
+	}
+	return r;
+}
+
+scriptload( path: string) :list of string
+{
+	dfd := sys->open(path, Sys->OREAD);
+	if (dfd == nil) {
+		raise "fail: Script file ("+path+") not found";
+		return nil;
+	}
+
+	scriptbuf := array[BUFSIZE] of byte;
+	scriptlen := sys->read(dfd, scriptbuf, len scriptbuf);
+	if(scriptlen < 0)
+		raise "fail: can't read script: "+sys->sprint("%r");
+    
+	(nil, scriptlist) := sys->tokenize(string scriptbuf[0:scriptlen], "\n");
+	return scriptlist;
+}
--- /dev/null
+++ b/appl/cmd/ip/ppp/script.m
@@ -1,0 +1,14 @@
+Script: module {
+	PATH:	con "/dis/ip/ppp/script.dis";
+
+	ScriptInfo: adt {
+		path:			string;
+		content:		list of string;
+		timeout:		int;
+		username:		string;
+		password:		string;
+	};
+	
+	execute:	fn( modem: Modem, m: ref Modem->Device, 
+						scriptinfo: ref ScriptInfo );
+};
--- /dev/null
+++ b/appl/cmd/ip/rip.b
@@ -1,0 +1,625 @@
+implement Rip;
+
+# basic RIP implementation
+#	understands v2, sends v1
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "dial.m";
+	dial: Dial;
+
+
+include "ip.m";
+	ip: IP;
+	IPaddr, Ifcaddr, Udphdr: import ip;
+
+include "attrdb.m";
+	attrdb: Attrdb;
+
+include "arg.m";
+
+Rip: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+# rip header:
+#	op[1] version[1] pad[2]
+
+Oop: con 0;	# op: byte
+Oversion: con 1;	# version: byte
+Opad: con 2;	# 2 byte pad
+Riphdrlen: con	Opad+2;	# op[1] version[1] mbz[2]
+
+# rip route entry:
+#	type[2] tag[2] addr[4] mask[4] nexthop[4] metric[4]
+
+Otype: con 0;	# type[2]
+Otag: con Otype+2;	# tag[2] v2 or mbz v1
+Oaddr: con Otag+2;	# addr[4]
+Omask: con Oaddr+4;	# mask[4] v2 or mbz v1
+Onexthop: con Omask+4;
+Ometric: con Onexthop+4;	# metric[4]
+Ipdestlen: con Ometric+4;
+
+Maxripmsg: con 512;
+
+# operations
+OpRequest: con 1;		# want route
+OpReply: con 2;		# all or part of route table
+
+HopLimit: con 16;		# defined by protocol as `infinity'
+RoutesInPkt: con 25; 	# limit defined by protocol
+RIPport: con 520;
+
+Expired: con 180;
+Discard: con 240;
+
+OutputRate: con 60;	# seconds between routing table transmissions
+
+NetworkCost: con 1;	# assume the simple case
+
+Gateway: adt {
+	dest:	IPaddr;
+	mask:	IPaddr;
+	gateway:	IPaddr;
+	metric:	int;
+	valid:	int;
+	changed:	int;
+	local:	int;
+	time:	int;
+
+	contains:	fn(g: self ref Gateway, a: IPaddr): int;
+};
+
+netfd:	ref Sys->FD;
+routefd:	ref Sys->FD;
+AF_INET:	con 2;
+
+routes: array of ref Gateway;
+Routeinc: con 50;
+defroute: ref Gateway;
+debug := 0;
+nochange := 0;
+quiet := 1;
+myversion := 1;	# default protocol version
+logfile := "iproute";
+netdir := "/net";
+now: int;
+nets: list of ref Ifcaddr;
+addrs: list of IPaddr;
+
+syslog(nil: int, nil: string, s: string)
+{
+	sys->print("rip: %s\n", s);
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	dial = load Dial Dial->PATH;
+	ip = load IP IP->PATH;
+	ip->init();
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("ip/rip [-d] [-r]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' =>	debug++;
+		'b' =>	quiet = 0;
+		'2' =>	myversion = 2;
+		'n' =>	nochange = 1;
+		'x' =>	netdir = arg->earg();
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(args != nil)
+		quiet = 0;
+	for(; args != nil; args = tl args){
+		(ok, a) := IPaddr.parse(hd args);
+		if(ok < 0)
+			fatal(sys->sprint("invalid address: %s", hd args));
+		addrs = a :: addrs;
+	}
+	arg = nil;
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD|Sys->FORKNS, nil);
+
+	whereami();
+	addlocal();
+
+	routefd = sys->open(sys->sprint("%s/iproute", netdir), Sys->ORDWR);
+	if(routefd == nil)
+		fatal(sys->sprint("can't open %s/iproute: %r", netdir));
+	readroutes();
+
+	syslog(0, logfile, "started");
+
+	netfd = riplisten();
+
+	# broadcast request for all routes
+
+	if(!quiet){
+		sendall(OpRequest, 0);
+		spawn sender();
+	}
+
+	# read routing requests
+
+	buf := array[8192] of byte;
+	while((nb := sys->read(netfd, buf, len buf)) > 0){
+		nb -= Riphdrlen + IP->Udphdrlen;
+		if(nb < 0)
+			continue;
+		uh := Udphdr.unpack(buf, IP->Udphdrlen);
+		hdr := buf[IP->Udphdrlen:];
+		version := int hdr[Oversion];
+		if(version < 1)
+			continue;
+		bp := buf[IP->Udphdrlen + Riphdrlen:];
+		case int hdr[Oop] {
+		OpRequest =>
+			# TO DO: transmit in response to request?  only if something interesting to say...
+			;
+
+		OpReply =>
+			# wrong source port?
+			if(uh.rport != RIPport)
+				continue;
+			# my own broadcast?
+			if(ismyaddr(uh.raddr))
+				continue;
+			now = daytime->now();
+			if(debug > 1)
+				sys->fprint(sys->fildes(2), "from %s:\n", uh.raddr.text());
+			for(; (nb -= Ipdestlen) >= 0; bp = bp[Ipdestlen:])
+				unpackroute(bp, version, uh.raddr);
+		* =>
+			if(debug)
+				sys->print("rip: unexpected op: %d\n", int hdr[Oop]);
+		}
+	}
+}
+
+whereami()
+{
+	for(ifcs := ip->readipifc(netdir, -1).t0; ifcs != nil; ifcs = tl ifcs)
+		for(al := (hd ifcs).addrs; al != nil; al = tl al){
+			ifa := hd al;
+			if(!ifa.ip.isv4())
+				continue;
+			# how to tell broadcast? must be told? actually, it's in /net/iproute
+			nets = ifa :: nets;
+		}
+}
+
+ismyaddr(a: IPaddr): int
+{
+	for(l := nets; l != nil; l = tl l)
+		if((hd l).ip.eq(a))
+			return 1;
+	return 0;
+}
+
+addlocal()
+{
+	for(l := nets; l != nil; l = tl l){
+		ifc := hd l;
+		g := lookup(ifc.net);
+		g.valid = 1;
+		g.local = 1;
+		g.gateway = ifc.ip;
+		g.mask = ifc.mask;
+		g.metric = NetworkCost;
+		g.time = 0;
+		g.changed = 1;
+		if(debug)
+			syslog(0, logfile, sys->sprint("Existing: %s & %s -> %s", g.dest.text(), g.mask.masktext(), g.gateway.text()));
+	}
+}
+
+#
+# record any existing routes
+#
+readroutes()
+{
+	now = daytime->now();
+	b := bufio->fopen(routefd, Sys->OREAD);
+	while((l := b.gets('\n')) != nil){
+		(nf, flds) := sys->tokenize(l, " \t");
+		if(nf >= 5){
+			flags := hd tl tl tl flds;
+			if(flags == nil || flags[0] != '4' || contains(flags, "ibum"))
+				continue;
+			g := lookup(parseip(hd flds));
+			g.mask = parsemask(hd tl flds);
+			g.gateway = parseip(hd tl tl flds);
+			g.metric = HopLimit;
+			g.time = now;
+			g.changed = 1;
+			if(debug)
+				syslog(0, logfile, sys->sprint("Existing: %s & %s -> %s", g.dest.text(), g.mask.masktext(), g.gateway.text()));
+			if(iszero(g.dest) && iszero(g.mask)){
+				defroute = g;
+				g.local = 1;
+			}else if(defroute != nil && g.dest.eq(defroute.gateway))
+				continue;
+			else
+				g.local = !ismyaddr(g.gateway);
+		}
+	}
+}
+
+unpackroute(b: array of byte, version: int, gwa: IPaddr)
+{
+	# check that it's an IP route, valid metric, MBZ fields zero
+
+	if(b[0] != byte 0 || b[1] != byte AF_INET){
+		if(debug > 1)
+			sys->fprint(sys->fildes(2), "\t-- unknown address type %x,%x\n", int b[0], int b[1]);
+		return;
+	}
+	dest := IPaddr.newv4(b[Oaddr:]);
+	mask: IPaddr;
+	if(version == 1){
+		# check MBZ fields
+		if(ip->get2(b, 2) | ip->get4(b, Omask) | ip->get4(b, Onexthop)){
+			if(debug > 1)
+				sys->fprint(sys->fildes(2), "\t-- non-zero MBZ\n");
+			return;
+		}
+		mask = maskgen(dest);
+	}else if(version == 2){
+		if(ip->get4(b, Omask))
+			mask = IPaddr.newv4(b[Omask:]);
+		else
+			mask = maskgen(dest);
+		if(ip->get4(b, Onexthop))
+			gwa = IPaddr.newv4(b[Onexthop:]);
+	}
+	metric := ip->get4(b, Ometric);
+	if(debug > 1)
+		sys->fprint(sys->fildes(2), "\t%s %d\n", dest.text(), metric);
+	if(metric <= 0 || metric > HopLimit)
+		return;
+
+	# 1058/3.4.2: response processing
+	# ignore route if IP address is:
+	#	class D or E
+	#	net 0 (except perhaps 0.0.0.0)
+	#	net 127
+	#	broadcast address (all 1s host part)
+	# we allow host routes
+
+	if(dest.ismulticast() || dest.a[0] == byte 0 || dest.a[0] == byte 16r7F){
+		if(debug > 1)
+			sys->fprint(sys->fildes(2), "\t%s %d invalid addr\n", dest.text(), metric);
+		return;
+	}
+	if(isbroadcast(dest, mask)){
+		if(debug > 1)
+			sys->fprint(sys->fildes(2), "\t%s & %s -> broadcast\n", dest.text(), mask.masktext());
+		return;
+	}
+
+	# update the metric min(metric+NetworkCost, HopLimit)
+
+	metric += NetworkCost;
+	if(metric > HopLimit)
+		metric = HopLimit;
+
+	updateroute(dest, mask, gwa, metric);
+}
+
+updateroute(dest, mask, gwa: IPaddr, metric: int)
+{
+	# RFC1058 rules page 27-28, with optional replacement of expiring routes
+	r := lookup(dest);
+	if(r.valid){
+		if(r.local)
+			return;	# local, don't touch
+		if(r.gateway.eq(gwa)){
+			if(metric != HopLimit){
+				r.metric = metric;
+				r.time = now;
+			}else{
+				# metric == HopLimit
+				if(r.metric != HopLimit){
+					r.metric = metric;
+					r.changed = 1;
+					r.time = now - (Discard-120);
+					delroute(r);	# don't use it for routing
+					# route remains valid but advertised with metric HopLimit
+				} else if(now >= r.time+Discard){
+					delroute(r);	# finally dead
+					r.valid = 0;
+					r.changed = 1;
+				}
+			}
+		}else if(metric < r.metric ||
+			  metric != HopLimit && metric == r.metric && now > r.time+Expired/2){
+			delroute(r);
+			r.metric = metric;
+			r.gateway = gwa;
+			r.time = now;
+			addroute(r);
+		}
+	} else if(metric < HopLimit){	# new entry
+
+		# 1058/3.4.2: don't add route-to-host if host is on net/subnet
+		# for which we have at least as good a route
+
+		if(!mask.eq(ip->allbits) ||
+		   ((pr := findroute(dest)) == nil || metric <= pr.metric)){
+			r.valid = 1;
+			r.changed = 1;
+			r.time = now;
+			r.metric = metric;
+			r.dest = dest;
+			r.mask = mask;
+			r.gateway = gwa;
+			addroute(r);
+		}
+	}
+}
+
+sender()
+{
+	for(;;){
+		sys->sleep(OutputRate*1000);	# could add some random fizz
+		sendall(OpReply, 1);
+	}
+}
+
+onlist(a: IPaddr, l: list of IPaddr): int
+{
+	for(; l != nil; l = tl l)
+		if(a.eq(hd l))
+			return 1;
+	return 0;
+}
+
+sendall(op: int, changes: int)
+{
+	for(l := nets; l != nil; l = tl l){
+		if(addrs != nil && !onlist((hd l).net, addrs))
+			continue;
+		a := (hd l).net.copy();
+		b := (ip->allbits).maskn((hd l).mask);
+		for(i := 0; i < len a.a; i++)
+			a.a[i] |= b.a[i];
+		sendroutes(hd l, a, op, changes);
+	}
+	for(i := 0; i < len routes; i++)
+		if((r := routes[i]) != nil)
+			r.changed = 0;
+}
+
+zeroentry := array[Ipdestlen] of {* => byte 0};
+
+sendroutes(ifc: ref Ifcaddr, dst: IPaddr, op: int, changes: int)
+{
+	if(debug > 1)
+		sys->print("rip: send %s\n", dst.text());
+	buf := array[Maxripmsg+IP->Udphdrlen] of byte;
+	hdr := Udphdr.new();
+	hdr.lport = hdr.rport = RIPport;
+	hdr.raddr = dst;	# needn't copy
+	hdr.pack(buf, IP->Udphdrlen);
+	o := IP->Udphdrlen;
+	buf[o] = byte op;
+	buf[o+1] = byte myversion;
+	buf[o+2] = byte 0;
+	buf[o+3] = byte 0;
+	o += Riphdrlen;
+#	rips := buf[IP->Udphdrlen+Riphdrlen:];
+	if(op == OpRequest){
+		buf[o:] = zeroentry;
+		ip->put4(buf, o+Ometric, HopLimit);
+		o += Ipdestlen;
+	} else {
+		# send routes
+		for(i:=0; i<len routes; i++){
+			r := routes[i];
+			if(r == nil || !r.valid || changes && !r.changed)
+				continue;
+			if(r == defroute)
+				continue;
+			if(r.dest.eq(ifc.net) || isonnet(r.dest, ifc))
+				continue;
+			netmask := r.dest.classmask();
+			subnet := !r.mask.eq(netmask);
+			if(myversion < 2 && !r.mask.eq(ip->allbits)){
+				# if not a host route, don't let a subnet route leave its net
+				if(subnet && !netmask.eq(ifc.ip.classmask()))
+					continue;
+			}
+			if(o+Ipdestlen > IP->Udphdrlen+Maxripmsg){
+				if(sys->write(netfd, buf, o) < 0)
+					sys->fprint(sys->fildes(2), "RIP write failed: %r\n");
+				o = IP->Udphdrlen + Riphdrlen;
+			}
+			buf[o:] = zeroentry;
+			ip->put2(buf, o+Otype, AF_INET);
+			buf[o+Oaddr:] = r.dest.v4();
+			ip->put4(buf, o+Ometric, r.metric);
+			if(myversion == 2 && subnet)
+				buf[o+Omask:] = r.mask.v4();
+			o += Ipdestlen;
+		}
+	}
+	if(o > IP->Udphdrlen+Riphdrlen && sys->write(netfd, buf, o) < 0)
+		sys->fprint(sys->fildes(2), "rip: network write to %s failed: %r\n", dst.text());
+}
+
+lookup(addr: IPaddr): ref Gateway
+{
+	avail := -1;
+	for(i:=0; i<len routes; i++){
+		g := routes[i];
+		if(g == nil || !g.valid){
+			if(avail < 0)
+				avail = i;
+			continue;
+		}
+		if(g.dest.eq(addr))
+			return g;
+	}
+	if(avail < 0){
+		avail = len routes;
+		a := array[len routes+Routeinc] of ref Gateway;
+		a[0:] = routes;
+		routes = a;
+	}
+	if((g := routes[avail]) == nil){
+		g = ref Gateway;
+		routes[avail] = g;
+		g.valid = 0;
+	}
+	g.dest = addr;
+	return g;
+}
+
+findroute(a: IPaddr): ref Gateway
+{
+	pr: ref Gateway;
+	for(i:=0; i<len routes; i++){
+		r := routes[i];
+		if(r == nil || !r.valid)
+			continue;
+		if(r.contains(a) && (pr == nil || !maskle(r.mask, pr.mask)))
+			pr = r;	# more specific mask
+	}
+	return pr;
+}
+
+maskgen(addr: IPaddr): IPaddr
+{
+	net: ref Ifcaddr;
+	for(l := nets; l != nil; l = tl l){
+		ifc := hd l;
+		if(isonnet(addr, ifc) &&
+		   (net == nil || maskle(ifc.mask, net.mask)))	# less specific mask?
+			net = ifc;
+	}
+	if(net != nil)
+		return net.mask;
+	return addr.classmask();
+}
+
+isonnet(a: IPaddr, n: ref Ifcaddr): int
+{
+	return a.mask(n.mask).eq(n.net);
+}
+
+isbroadcast(a: IPaddr, mask: IPaddr): int
+{
+	h := a.maskn(mask);	# host part
+	hm := (ip->allbits).maskn(mask);	# host part of mask
+	return h.eq(hm);
+}
+
+iszero(a: IPaddr): int
+{
+	return a.eq(ip->v4noaddr) || a.eq(ip->noaddr);
+}
+
+maskle(a, b: IPaddr): int
+{
+	return a.mask(b).eq(a);
+}
+
+#
+# add ipdest mask gateway
+# add 0.0.0.0 0.0.0.0 gateway	(default)
+# delete ipdest mask
+#
+addroute(g: ref Gateway)
+{
+	if(iszero(g.mask) && iszero(g.dest))
+		g.valid = 0;	# don't change default route
+	else if(defroute != nil && defroute.gateway.eq(g.gateway)){
+		if(debug)
+			syslog(0, logfile, sys->sprint("default %s %s", g.dest.text(), g.mask.text()));	# don't need a new entry
+		g.valid = 1;
+		g.changed = 1;
+	} else {
+		if(debug)
+			syslog(0, logfile, sys->sprint("add %s %s %s", g.dest.text(), g.mask.text(), g.gateway.text()));
+		if(nochange || sys->fprint(routefd, "add %s %s %s", g.dest.text(), g.mask.text(), g.gateway.text()) > 0){
+			g.valid = 1;
+			g.changed = 1;
+		}
+	}
+}
+
+delroute(g: ref Gateway)
+{
+	if(debug)
+		syslog(0, logfile, sys->sprint("delete %s %s", g.dest.text(), g.mask.text()));
+	if(!nochange)
+		sys->fprint(routefd, "delete %s %s", g.dest.text(), g.mask.text());
+}
+
+parseip(s: string): IPaddr
+{
+	(ok, a) := IPaddr.parse(s);
+	if(ok < 0)
+		raise "bad route";
+	return a;
+}
+
+parsemask(s: string): IPaddr
+{
+	(ok, a) := IPaddr.parsemask(s);
+	if(ok < 0)
+		raise "bad route";
+	return a;
+}
+
+contains(s: string, t: string): int
+{
+	for(i := 0; i < len s; i++)
+		for(j := 0; j < len t; j++)
+			if(s[i] == t[j])
+				return 1;
+	return 0;
+}
+
+Gateway.contains(g: self ref Gateway, a: IPaddr): int
+{
+	return g.dest.eq(a.mask(g.mask));
+}
+
+riplisten(): ref Sys->FD
+{
+	addr := sys->sprint("%s/udp!*!rip", netdir);
+	c := dial->announce(addr);
+	if(c == nil)
+		fatal(sys->sprint("can't announce %s: %r", addr));
+	if(sys->fprint(c.cfd, "headers") < 0)
+		fatal(sys->sprint("can't set udp headers: %r"));
+	fd := sys->open(c.dir+"/data", Sys->ORDWR);
+	if(fd == nil)
+		fatal(sys->sprint("can't open %s: %r", c.dir+"/data"));
+	return fd;
+}
+
+fatal(s: string)
+{
+	syslog(0, logfile, s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/ip/sntp.b
@@ -1,0 +1,310 @@
+implement Sntp;
+
+#
+# rfc1361 (simple network time protocol)
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "ip.m";
+	ip: IP;
+	IPaddr: import ip;
+
+include "dial.m";
+	dial: Dial;
+
+include "timers.m";
+	timers: Timers;
+	Timer: import timers;
+
+include "arg.m";
+
+Sntp: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+debug := 0;
+
+Retries: con 4;
+Delay: con 3*1000;	# milliseconds
+
+SNTP: adt {
+	li:	int;
+	vn:	int;
+	mode:	int;
+	stratum:	int;	# level of local clock
+	poll:	int;	# log2(maximum interval in seconds between successive messages)
+	precision:	int;	# log2(seconds precision of local clock) [eg, -6 for mains, -18 for microsec]
+	rootdelay:	int;	# round trip delay in seconds to reference (16:16 fraction)
+	dispersion:	int;	# maximum error relative to primary reference
+	clockid:	string;	# reference clock identifier	
+	reftime:	big;	# local time at which clock last set/corrected
+	orgtime:	big;	# local time at which client transmitted request
+	rcvtime:	big;	# time at which request arrived at server
+	xmttime:	big;	# time server transmitted reply
+	auth:	array of byte;	# auth field (ignored by this implementation)
+
+	new:	fn(vn, mode: int): ref SNTP;
+	pack:	fn(s: self ref SNTP): array of byte;
+	unpack:	fn(a: array of byte): ref SNTP;
+};
+SNTPlen: con 4+3*4+4*8;
+
+Version: con 1;	# accepted by version 2 and version 3 servers
+Stratum: con 0;
+Poll: con 0;
+LI: con 0;
+Symmetric: con 2;
+ClientMode: con 3;
+ServerMode: con 4;
+Epoch: con big 86400*big (365*70 + 17);	# seconds between 1 Jan 1900 and 1 Jan 1970
+
+Microsec: con big 1000000;
+
+server := "$ntp";
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	ip = load IP IP->PATH;
+	timers = load Timers Timers->PATH;
+	dial = load Dial Dial->PATH;
+
+	ip->init();
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("sntp [-d] [server]");
+
+	doset := 1;
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' => debug++;
+		'i' => doset = 0;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args > 1)
+		arg->usage();
+	arg = nil;
+
+	if(args != nil)
+		server = hd args;
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD, nil);
+	stderr = sys->fildes(2);
+	timers->init(100);
+
+	conn := dial->dial(dial->netmkaddr(server, "udp", "ntp"), nil);
+	if(conn == nil){
+		sys->fprint(stderr, "sntp: can't dial %s: %r\n", server);
+		raise "fail:dial";
+	}
+
+	replies := chan of ref SNTP;
+	spawn reader(conn.dfd, replies);
+
+	for(i:=0; i<Retries; i++){
+		request := SNTP.new(Version, ClientMode);
+		request.poll = 6;
+		request.orgtime = (big time() + Epoch)<<32;
+		b := request.pack();
+		if(sys->write(conn.dfd, b, len b) != len b){
+			sys->fprint(stderr, "sntp: UDP write failed: %r\n");
+			continue;
+		}
+		t := Timer.start(Delay);
+		alt{
+		reply := <-replies =>
+			t.stop();
+			if(reply == nil)
+				quit("read error");
+			if(debug){
+				sys->fprint(stderr, "LI = %d, version = %d, mode = %d\n", reply.li, reply.vn, reply.mode);
+				if(reply.stratum == 1)
+					sys->fprint(stderr, "stratum = 1 (%s), ", reply.clockid);
+				else
+					sys->fprint(stderr, "stratum = %d, ", reply.stratum);
+				sys->fprint(stderr, "poll = %d, prec = %d\n", reply.poll, reply.precision);
+				sys->fprint(stderr, "rootdelay = %d, dispersion = %d\n", reply.rootdelay, reply.dispersion);
+			}
+			if(reply.vn == 0 || reply.vn > 3)
+				continue;	# unsupported version, ignored
+			if(reply.mode >= 6 || reply.mode == ClientMode)
+				continue;
+			now := ((reply.xmttime>>32)&16rFFFFFFFF) - Epoch;
+			if(now <= big 1120000000)
+				continue;
+			if(reply.li == 3 || reply.stratum == 0)	# unsynchronised
+				sys->fprint(stderr, "sntp: time server not synchronised to reference time\n");
+			if(debug)
+				sys->print("%bd\n", now);
+			if(doset){
+				settime("#r/rtc", now);
+				settime("/dev/time", now*Microsec);
+			}
+			quit(nil);
+		<-t.timeout =>
+			continue;
+		}
+	}
+	sys->fprint(sys->fildes(2), "sntp: no response from server %s\n", server);
+	quit("timeout");
+}
+
+reader(fd: ref Sys->FD, replies: chan of ref SNTP)
+{
+	for(;;){
+		buf := array[512] of byte;
+		nb := sys->read(fd, buf, len buf);
+		if(nb <= 0)
+			break;
+		reply := SNTP.unpack(buf[0:nb]);
+		if(reply == nil){
+			# ignore bad replies
+			if(debug)
+				sys->fprint(stderr, "sntp: invalid reply (len %d)\n", nb);
+			continue;
+		}
+		replies <-= reply;
+	}
+	if(debug)
+		sys->fprint(stderr, "sntp: UDP read failed: %r\n");
+	replies <-= nil;
+}
+
+quit(s: string)
+{
+	pid := sys->pctl(0, nil);
+	timers->shutdown();
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+	if(s != nil)
+		raise "fail:"+s;
+	exit;
+}
+
+time(): int
+{
+	n := rdn("#r/rtc");
+	if(n > big 300)	# ie, possibly set
+		return int n;
+	n = rdn("/dev/time");
+	if(n <= big 0)
+		return 0;
+	return int(n/big Microsec);
+}
+
+rdn(f: string): big
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil)
+		return big -1;
+	b := array[128] of byte;
+	n := sys->read(fd, b, len b);
+	if(n <= 0)
+		return big 0;
+	return big string b[0:n];
+}
+
+settime(f: string, t: big)
+{
+	fd := sys->open(f, Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "%bd", t);
+}
+
+get8(a: array of byte, i: int): big
+{
+	b := big ip->get4(a, i+4) & 16rFFFFFFFF;
+	return (big ip->get4(a, i) << 32) | b;
+}
+
+put8(a: array of byte, o: int, v: big)
+{
+	ip->put4(a, o, int (v>>32));
+	ip->put4(a, o+4, int v);
+}
+
+SNTP.unpack(a: array of byte): ref SNTP
+{
+	if(len a < SNTPlen)
+		return nil;
+	s := ref SNTP;
+	mode := int a[0];
+	s.li = mode>>6;
+	s.vn = (mode>>3);
+	s.mode = mode & 3;
+	s.stratum = int a[1];
+	s.poll = int a[2];
+	if(s.poll & 16r80)
+		s.poll |= ~0 << 8;
+	s.precision = int a[3];
+	if(s.precision & 16r80)
+		s.precision |= ~0 << 8;
+	s.rootdelay = ip->get4(a, 4);
+	s.dispersion = ip->get4(a, 8);
+	if(s.stratum <= 1){
+		for(i := 12; i < 16; i++)
+			if(a[i] == byte 0)
+				break;
+		s.clockid = string a[12:i];
+	}else
+		s.clockid = sys->sprint("%d.%d.%d.%d", int a[12], int a[13], int a[14], int a[15]);
+	s.reftime = get8(a, 16);
+	s.orgtime = get8(a, 24);
+	s.rcvtime = get8(a, 32);
+	s.xmttime = get8(a, 40);
+	if(len a > SNTPlen)
+		s.auth = a[48:];
+	return s;
+}
+
+SNTP.pack(s: self ref SNTP): array of byte
+{
+	a := array[SNTPlen + len s.auth] of byte;
+	a[0] = byte ((s.li<<6) | (s.vn<<3) | s.mode);
+	a[1] = byte s.stratum;
+	a[2] = byte s.poll;
+	a[3] = byte s.precision;
+	ip->put4(a, 4, s.rootdelay);
+	ip->put4(a, 8, s.dispersion);
+	ip->put4(a, 12, 0);	# clockid field
+	if(s.clockid != nil){
+		if(s.stratum <= 1){
+			b := array of byte s.clockid;
+			for(i := 0; i < len b && i < 4; i++)
+				a[12+i] = b[i];
+		}else
+			a[12:] = IPaddr.parse(s.clockid).t1.v4();
+	}
+	put8(a, 16, s.reftime);
+	put8(a, 24, s.orgtime);
+	put8(a, 32, s.rcvtime);
+	put8(a, 40, s.xmttime);
+	if(s.auth != nil)
+		a[48:] = s.auth;
+	return a;
+}
+
+SNTP.new(vn, mode: int): ref SNTP
+{
+	s := ref SNTP;
+	s.vn = vn;
+	s.mode = mode;
+	s.li = 0;
+	s.stratum = 0;
+	s.poll = 0;
+	s.precision = 0;
+	s.clockid = nil;
+	s.reftime = big 0;
+	s.orgtime = big 0;
+	s.rcvtime = big 0;
+	s.xmttime = big 0;
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/ip/tftpd.b
@@ -1,0 +1,518 @@
+implement Tftpd;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+
+include "draw.m";
+
+include "arg.m";
+
+include "dial.m";
+	dial: Dial;
+
+include "ip.m";
+	ip: IP;
+	IPaddr, Udphdr: import ip;
+
+Tftpd: module
+{
+	init: fn (nil: ref Draw->Context, argv: list of string);
+};
+
+dir:=  "/services/tftpd";
+net:=  "/net";
+
+Tftp_READ: con 1;
+Tftp_WRITE: con 2;
+Tftp_DATA: con 3;
+Tftp_ACK: con 4;
+Tftp_ERROR: con 5;
+
+Segsize: con 512;
+
+dbg := 0;
+restricted := 0;
+port := 69;
+
+Udphdrsize: con IP->Udphdrlen;
+
+tftpcon: ref Sys->Connection;
+tftpreq: ref Sys->FD;
+
+dokill(pid: int, scope: string)
+{
+	fd := sys->open("/prog/" + string pid + "/ctl", sys->OWRITE);
+	if(fd == nil)
+		fd = sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill%s", scope);
+}
+
+kill(pid: int) { dokill(pid, ""); }
+killgrp(pid: int) { dokill(pid, "grp"); }
+killme() { kill(sys->pctl(0,nil)); }
+killus() { killgrp(sys->pctl(0,nil)); }
+
+DBG(s: string)
+{
+	if(dbg)
+		sys->fprint(stderr, "tfptd: %d: %s\n", sys->pctl(0,nil), s);
+}
+
+false, true: con iota;
+
+Timer: adt {
+	KILL: con -1;
+	ALARM: con -2;
+	RETRY: con -3;
+	sig: chan of int;
+	create: fn(): ref Timer;
+	destroy: fn(t: self ref Timer);
+	set: fn(t: self ref Timer, msec, nretry: int);
+
+	ticker: fn(t: self ref Timer);
+	ticking: int;
+	wakeup: int;
+	timeout: int;
+	nretry: int;
+};
+
+Timer.create(): ref Timer
+{
+	t := ref Timer;
+	t.wakeup = 0;
+	t.ticking = false;
+	t.sig = chan of int;
+	return t;
+}
+
+Timer.destroy(t: self ref Timer)
+{
+	DBG("Timer.destroy");
+	alt {
+		t.sig <-= t.KILL =>
+			DBG("sent final msg");
+		* =>
+			DBG("couldn't send final msg");
+	}
+	DBG("Timer.destroy done");
+}
+
+Timer.ticker(t: self ref Timer)
+{
+	DBG("spawn: ticker");
+	t.ticking = true;
+	while(t.wakeup > sys->millisec()) {
+		DBG("Timer.ticker sleeping for "
+			+string (t.wakeup-sys->millisec()));
+		sys->sleep(t.wakeup-sys->millisec());
+	}
+	if(t.wakeup) {
+		DBG("Timer.ticker wakeup");
+		if(t.nretry) {
+			alt { t.sig <-= t.RETRY => ; }
+			t.ticking = false;
+			t.set(t.timeout, t.nretry-1);
+		} else
+			alt { t.sig <-= t.ALARM => ; }
+	}	
+	t.ticking = false;
+	DBG("unspawn: ticker");
+}
+
+Timer.set(t: self ref Timer, msec, nretry: int)
+{
+	DBG(sys->sprint("Timer.set(%d, %d)", msec, nretry));
+	if(msec == 0) {
+		t.wakeup = 0;
+		t.timeout = 0;
+		t.nretry = 0;
+	} else {
+		t.wakeup = sys->millisec()+msec;
+		t.timeout = msec;
+		t.nretry = nretry;
+		if(!t.ticking)
+			spawn t.ticker();
+	}
+}
+
+killer(c: chan of int, pgid: int)
+{
+	DBG("spawn: killer");
+	cmd := <- c;
+	DBG(sys->sprint("killer has awakened (flag=%d)", cmd));
+	if(cmd == Timer.ALARM) {
+		killgrp(pgid);
+		DBG(sys->sprint("group %d has been killed", pgid));
+	}
+	DBG("unspawn killer");
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD|Sys->FORKNS, nil);
+	stderr = sys->fildes(2);
+
+	dial = load Dial Dial->PATH;
+	if(dial == nil)
+		fatal("can't load Dial");
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		fatal("can't load Arg");
+
+	arg->init(args);
+	arg->setusage("tftpd [-dr] [-p port] [-h homedir] [-x network-dir]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' =>	dbg++;
+		'h' =>	dir = arg->earg();
+		'r' =>		restricted = 1;
+		'p' =>	port = int arg->earg();
+		'x' =>	net = arg->earg();
+		* =>		arg->usage();
+		}
+	args =arg->argv();
+	if(args != nil){
+		net = hd args;
+		args = tl args;
+	}
+	if(args != nil)
+		arg->usage();
+	arg = nil;
+
+	ip = load IP IP->PATH;
+	if(ip == nil)
+		fatal(sys->sprint("can't load %s: %r", IP->PATH));
+	ip->init();
+
+	if(sys->chdir(dir) < 0)
+		fatal("can't chdir to " + dir);
+
+	spawn mainthing();
+}
+
+mainthing()
+{
+	DBG("spawn: mainthing");
+	bigbuf := array[32768] of byte;
+	
+	openlisten();
+	setuser();
+	for(;;) {
+		dlen := sys->read(tftpreq, bigbuf, len bigbuf);
+		if(dlen < 0)
+			fatal("listen");
+		if(dlen < Udphdrsize)
+			continue;
+
+		hdr := Udphdr.unpack(bigbuf, Udphdrsize);
+
+		raddr := sys->sprint("%s/udp!%s!%d", net, hdr.raddr.text(), hdr.rport);
+
+		DBG(sys->sprint("raddr=%s", raddr));
+		cx := dial->dial(raddr, nil);
+		if(cx == nil)
+			fatal("dialing "+raddr);
+
+#		showbuf("bigbuf", bigbuf[0:dlen]);
+
+		op := ip->get2(bigbuf, Udphdrsize);
+		mbuf := bigbuf[Udphdrsize+2:dlen];		# get past Udphdr and op
+		dlen -= 14;
+
+		case op {
+		Tftp_READ or Tftp_WRITE =>
+			;
+		Tftp_ERROR =>
+			DBG("tftp error");
+			continue;
+		* =>
+			nak(cx.dfd, 4, "Illegal TFTP operation");
+			continue;
+		}
+
+#		showbuf("mbuf", mbuf[0:dlen]);
+
+		i := 0;
+		while(dlen > 0 && mbuf[i] != byte 0) {
+			dlen--;
+			i++;
+		}
+
+		p := i++;
+		dlen--;
+		while(dlen > 0 && mbuf[i] != byte 0) {
+			dlen--;
+			i++;
+		}
+
+		path := string mbuf[0:p];
+		mode := string mbuf[p+1:i];
+		DBG(sys->sprint("path = %s, mode = %s", path, mode));
+
+		if(dlen == 0) {
+			nak(cx.dfd, 0, "bad tftpmode");
+			continue;
+		}
+
+		if(restricted && dodgy(path)){
+			nak(cx.dfd, 4, "Permission denied");
+			continue;
+		}
+		
+		if(op == Tftp_READ)
+			spawn sendfile(cx.dfd, path, mode);
+		else
+			spawn recvfile(cx.dfd, path, mode);
+	}	
+}
+
+dodgy(path: string): int
+{
+	n := len path;
+	nd := len dir;
+	if(n == 0 ||
+	   path[0] == '#' ||
+	   path[0] == '/' && (n < nd+1 || path[0:nd] != dir || path[nd] != '/'))
+		return 1;
+	(nil, flds) := sys->tokenize(path, "/");
+	for(; flds != nil; flds = tl flds)
+		if(hd flds == "..")
+			return 1;
+	return 0;
+}
+
+showbuf(msg: string, b: array of byte)
+{
+	sys->fprint(stderr, "%s: size %d: ", msg, len b);
+	for(i:=0; i<len b; i++)
+		sys->fprint(stderr, "%.2ux ", int b[i]);
+	sys->fprint(stderr, "\n");
+	for(i=0; i<len b; i++)
+		if(int b[i] >= 32 && int b[i] <= 126) 
+			sys->fprint(stderr, " %c", int b[i]);
+		else
+			sys->fprint(stderr, " .");
+	sys->fprint(stderr, "\n");
+}
+
+sendblock(sig: chan of int, buf: array of byte, net: ref sys->FD, ksig: chan of int)
+{
+	DBG("spawn: sendblocks");
+	nbytes := 0;
+	loop: for(;;) {
+		DBG("sendblock: waiting for cmd");
+		cmd := <- sig;
+		DBG(sys->sprint("sendblock: cmd=%d", cmd));
+		case cmd {
+		Timer.KILL =>
+			DBG("sendblock: killed");
+			return;
+		Timer.RETRY =>
+			;
+		Timer.ALARM =>
+			DBG("too many retries");
+			break loop;
+		* =>
+			nbytes = cmd;
+		}
+#		showbuf("sendblock", buf[0:nbytes]);
+		ret := sys->write(net, buf, 4+nbytes);
+		DBG(sys->sprint("ret=%d", ret));
+
+		if(ret < 0) {
+			ksig <-= Timer.ALARM;
+			fatal("tftp: network write error");
+		}
+		if(ret != 4+nbytes)
+			return;
+	}
+	DBG("sendblock: exiting");
+	alt { ksig <-= Timer.ALARM => ; }
+	DBG("unspawn: sendblocks");
+}
+
+sendfile(net: ref sys->FD, name: string, mode: string)
+{
+
+	DBG(sys->sprint("spawn: sendfile: name=%s mode=%s", name, mode));
+
+	pgrp := sys->pctl(Sys->NEWPGRP, nil);
+	ack := array[1024] of byte;
+	if(name == "") {
+		nak(net, 0, "not in our database");
+		return;
+	}
+
+	file := sys->open(name, Sys->OREAD);
+	if(file == nil) {
+		DBG(sys->sprint("open failed: %s", name));
+		errbuf := sys->sprint("%r");
+		nak(net, 0, errbuf);
+		return;
+	}
+	DBG(sys->sprint("opened %s", name));
+
+	block := 0;
+	timer := Timer.create();
+	ksig := chan of int;
+	buf := array[4+Segsize] of byte;
+
+	spawn killer(ksig, pgrp);
+	spawn sendblock(timer.sig, buf, net, ksig);
+
+	mainloop: for(;;) {
+		block++;
+		buf[0:] = array[] of {byte 0, byte Tftp_DATA,
+				byte (block>>8), byte block};
+		n := sys->read(file, buf[4:], len buf-4);
+		DBG(sys->sprint("n=%d", n));
+		if(n < 0) {
+			errbuf := sys->sprint("%r");
+			nak(net, 0, errbuf);
+			break;
+		}
+		DBG(sys->sprint("signalling write of %d to block %d", n, block));
+		timer.sig <-= n;
+		for(rxl := 0; rxl < 10; rxl++) {
+			
+			timer.set(1000, 15);
+			al := sys->read(net, ack, len ack);
+			timer.set(0, 0);
+			if(al < 0) {
+				timer.sig <-= Timer.ALARM;
+				break;
+			}
+			op := (int ack[0]<<8) | int ack[1];
+			if(op == Tftp_ERROR)
+				break mainloop;
+			ackblock := (int ack[2]<<8) | int ack[3];
+			DBG(sys->sprint("got ack: block=%d ackblock=%d",
+				block, ackblock));
+			if(ackblock == block)
+				break;
+			if(ackblock == 16rffff) {
+				block--;
+				break;
+			}
+		}
+		if(n < len buf-4)
+			break;
+	}
+	timer.destroy();
+	ksig <-= Timer.KILL;
+}
+
+recvfile(fd: ref sys->FD, name: string, mode: string)
+{
+	DBG(sys->sprint("spawn: recvfile: name=%s mode=%s", name, mode));
+
+	pgrp := sys->pctl(Sys->NEWPGRP, nil);
+
+	file := sys->create(name, sys->OWRITE, 8r666);
+	if(file == nil) {
+		errbuf := sys->sprint("%r");
+		nak(fd, 0, errbuf);
+		return;
+	}
+
+	block := 0;
+	ack(fd, block);
+	block++;
+
+	buf := array[8+Segsize] of byte;
+	timer := Timer.create();
+	spawn killer(timer.sig, pgrp);
+
+	for(;;) {
+		timer.set(15000, 0);
+		DBG(sys->sprint("reading block %d", block));
+		n := sys->read(fd, buf, len buf);
+		DBG(sys->sprint("read %d bytes", n));
+		timer.set(0, 0);
+
+		if(n < 0)
+			break;
+		op := int buf[0]<<8 | int buf[1];
+		if(op == Tftp_ERROR)
+			break;
+
+#		showbuf("got", buf[0:n]);
+		n -= 4;
+		inblock := int buf[2]<<8 | int buf[3];
+#		showbuf("hdr", buf[0:4]);
+		if(op == Tftp_DATA) {
+			if(inblock == block) {
+				ret := sys->write(file, buf[4:], n);
+				if(ret < 0) {
+					errbuf := sys->sprint("%r");
+					nak(fd, 0, errbuf);
+					break;
+				}
+				block++;
+			}
+			if(inblock < block) {
+				ack(fd, inblock);
+				DBG(sys->sprint("ok: inblock=%d block=%d",
+					inblock, block));
+			} else
+				DBG(sys->sprint("FAIL: inblock=%d block=%d",
+					inblock, block));
+			ack(fd, 16rffff);
+			if(n < 512)
+				break;
+		}
+	}
+	timer.destroy();
+}
+
+ack(fd: ref Sys->FD, block: int)
+{
+	buf := array[] of {byte 0, byte Tftp_ACK, byte (block>>8), byte block};
+#	showbuf("ack", buf);
+	if(sys->write(fd, buf, 4) < 0)
+		fatal("write ack");
+}
+
+
+nak(fd: ref Sys->FD, code: int, msg: string)
+{
+sys->print("nak: %s\n", msg);
+	buf := array[128] of {byte 0, byte Tftp_ERROR, byte 0, byte code};
+	bmsg := array of byte msg;
+	buf[4:] = bmsg;
+	buf[4+len bmsg] = byte 0;
+	if(sys->write(fd, buf, 4+len bmsg+1) < 0)
+		fatal("write nak");
+}
+
+fatal(msg: string)
+{
+	sys->fprint(stderr, "tftpd: %s: %r\n", msg);
+	killus();
+	raise "fail:error";
+}
+
+openlisten()
+{
+	name := net+"/udp!*!" + string port;
+	tftpcon = dial->announce(name);
+	if(tftpcon == nil)
+		fatal("can't announce "+name);
+	if(sys->fprint(tftpcon.cfd, "headers") < 0)
+		fatal("can't set header mode");
+	tftpreq = sys->open(tftpcon.dir+"/data", sys->ORDWR);
+	if(tftpreq == nil)
+		fatal("open udp data");
+}
+
+setuser()
+{
+	f := sys->open("/dev/user", sys->OWRITE);
+	if(f != nil)
+		sys->fprint(f, "none");
+}
+
--- /dev/null
+++ b/appl/cmd/ip/virgild.b
@@ -1,0 +1,130 @@
+implement Virgild;
+
+include "sys.m";
+sys: Sys;
+
+include "draw.m";
+
+include "dial.m";
+dial: Dial;
+
+include "ip.m";
+
+Virgild: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+Udphdrsize: con IP->Udphdrlen;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dial = load Dial Dial->PATH;
+
+	stderr = sys->fildes(2);
+
+	sys->pctl(Sys->FORKNS|Sys->FORKFD, nil);
+	if(sys->chdir("/lib/ndb") < 0){
+		sys->fprint(stderr, "virgild: no database\n");
+		return;
+	}
+
+	for(;;sys->sleep(10*1000)){
+		fd := openlisten();
+		if(fd == nil)
+			return;
+
+		buf := array[512] of byte;
+		for(;;){
+			n := sys->read(fd, buf, len buf);
+			if(n <= Udphdrsize){
+				break;
+			}
+			if(n <= Udphdrsize+1)
+				continue;
+
+			# dump any cruft after the question
+			for(i := Udphdrsize; i < n; i++){
+				c := int buf[i];
+				if(c == ' ' || c == 0 || c == '\n')
+					break;
+			}
+
+			answer := query(string buf[Udphdrsize:i]);
+			if(answer == nil)
+				continue;
+
+			# reply
+			r := array of byte answer;
+			if(len r > len buf - Udphdrsize)
+				continue;
+			buf[Udphdrsize:] = r;
+			sys->write(fd, buf, Udphdrsize+len r);
+		}
+		fd = nil;
+	}
+}
+
+openlisten(): ref Sys->FD
+{
+	c := dial->announce("udp!*!virgil");
+	if(c == nil){
+		sys->fprint(stderr, "virgild: can't open port: %r\n");
+		return nil;
+	}
+
+	if(sys->fprint(c.cfd, "headers") <= 0){
+		sys->fprint(stderr, "virgild: can't set headers: %r\n");
+		return nil;
+	}
+
+	c.dfd = sys->open(c.dir+"/data", Sys->ORDWR);
+	if(c.dfd == nil) {
+		sys->fprint(stderr, "virgild: can't open data file\n");
+		return nil;
+	}
+	return c.dfd;
+}
+
+#
+#  query is userid?question
+#
+#  for now, we're ignoring userid
+#
+query(request: string): string
+{
+	(n, l) := sys->tokenize(request, "?");
+	if(n < 2){
+		sys->fprint(stderr, "virgild: bad request %s %d\n", request, n);
+		return nil;
+	}
+
+	#
+	#  until we have something better, ask cs
+	#  to translate, make the request look cs-like
+	#
+	fd := sys->open("/net/cs", Sys->ORDWR);
+	if(fd == nil){
+		sys->fprint(stderr, "virgild: can't open /net/cs - %r\n");
+		return nil;
+	}
+	q := array of byte ("tcp!" + hd(tl l) + "!1000");
+	if(sys->write(fd, q, len q) < 0){
+		sys->fprint(stderr, "virgild: can't write /net/cs - %r: %s\n", string q);
+		return nil;
+	}
+	sys->seek(fd, big 0, 0);
+	buf := array[512-Udphdrsize-len request-1] of byte;
+	n = sys->read(fd, buf, len buf);
+	if(n <= 0){
+		sys->fprint(stderr, "virgild: can't read /net/cs - %r\n");
+		return nil;
+	}
+
+	(nil, l) = sys->tokenize(string buf[0:n], " \t");
+	(nil, l) = sys->tokenize(hd(tl l), "!");
+	return request + "=" + hd l;
+}
--- /dev/null
+++ b/appl/cmd/irtest.b
@@ -1,0 +1,70 @@
+implement Irtest;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "ir.m";
+	ir: Ir;
+
+Irtest: module
+{
+	init:	fn(nil: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	x := chan of int;
+	p := chan of int;
+
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	ir = load Ir Ir->PATH;
+	if(ir == nil)
+		ir = load Ir Ir->SIMPATH;
+	if(ir == nil) {
+		sys->fprint(stderr, "load ir: %r\n");
+		return;
+	}
+
+	if(ir->init(x,p) != 0) {
+		sys->fprint(stderr, "Ir->init: %r\n");
+		return;
+	}
+	<-p;
+
+	names := array[] of {
+		"Zero",
+		"One",
+		"Two",
+		"Three",
+		"Four",
+		"Five",
+		"Six",
+		"Seven",
+		"Eight",
+		"Nine",
+		"ChanUP",
+		"ChanDN",
+		"VolUP",
+		"VolDN",
+		"FF",
+		"Rew",
+		"Up",
+		"Dn",
+		"Select",
+		"Power",
+	};
+
+	while((c := <-x) != Ir->EOF){
+		c = ir->translate(c);
+		if(c == ir->Error)
+			sys->print("Error\n");
+		else if(c >= len names)
+			sys->print("unknown %d\n", c);
+		else
+			sys->print("%s\n", names[c]);
+	}	
+}
--- /dev/null
+++ b/appl/cmd/itest.b
@@ -1,0 +1,478 @@
+implement Itest;
+
+include "sys.m";
+	sys: Sys;
+include "string.m";
+	str: String;
+include "draw.m";
+include "daytime.m";
+	daytime: Daytime;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "readdir.m";
+	readdir: Readdir;
+include "arg.m";
+include "itslib.m";
+	S_INFO, S_WARN, S_ERROR, S_FATAL, S_STIME, S_ETIME: import Itslib;
+include "env.m";
+	env: Env;
+include "sh.m";
+
+SUMFILE: con "summary";
+MSGFILE: con "msgs";
+README: con "README";
+
+configfile := "";
+cflag := -1;
+verbosity := 3;
+repcount := 1;
+recroot := "";
+display_stderr := 0;
+display_stdout := 0;
+now := 0;
+
+stdout: ref Sys->FD;
+stderr: ref Sys->FD;
+context: ref Draw->Context;
+
+Test: adt {
+	spec: string;
+	fullspec: string;
+	cmd: Command;
+	recdir: string;
+	stdout: string;
+	stderr: string;
+	nruns: int;
+	nwarns: int;
+	nerrors: int;
+	nfatals: int;
+	failed: int;
+};
+
+
+Itest: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+	context = ctxt;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		nomod(Daytime->PATH);
+	str = load String String->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		nomod(Bufio->PATH);
+	if(str == nil)
+		nomod(String->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		nomod(Readdir->PATH);
+	env = load Env Env->PATH;
+	if(env == nil)
+		nomod(Env->PATH);
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		'c' =>	cflag = toint("c", arg->arg(), 0, 9);
+		'e' =>	display_stderr++;
+		'o' =>	display_stdout++;
+		'r' =>		repcount = toint("r", arg->arg(), 0, -1);
+		'v' =>	verbosity = toint("v", arg->arg(), 0, 9);
+		'C' =>	configfile = arg->arg();
+		'R' =>	recroot = arg->arg();
+		* =>		usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	testlist : array of ref Test;
+	if (args != nil)
+		testlist = arg_tests(args);
+	else if (configfile != "")
+		testlist = config_tests(configfile);
+	if (testlist == nil)
+		fatal("No tests to run");
+	sys->pctl(Sys->FORKENV, nil);
+	if (env->setenv(Itslib->ENV_VERBOSITY, string verbosity))
+		fatal("Failed to set environment variable " + Itslib->ENV_VERBOSITY);
+	if (repcount) 
+		reps := string repcount;
+	else
+		reps = "infinite";
+	if (len testlist == 1) ts := "";
+	else ts = "s";
+	if (repcount == 1) rs := "";
+	else rs = "s";
+	mreport(0, S_INFO, 2, sys->sprint("Starting tests - %s run%s of %d test%s", reps, rs, len testlist, ts));
+	run := big 1;
+
+	if (recroot != nil) 
+		recn := highest(recroot) + 1;
+	while (repcount == 0 || run <= big repcount) {
+		mreport(1, S_INFO, 3, sys->sprint("Starting run %bd", run));
+		for (i:=0; i<len testlist; i++) {
+			t := testlist[i];
+			if (recroot != nil) {
+				t.recdir = sys->sprint("%s/%d", recroot, recn++);
+				mreport(2, S_INFO, 3, sys->sprint("Recording in %s", t.recdir));
+				rfd := sys->create(t.recdir, Sys->OREAD, Sys->DMDIR | 8r770);
+				if (rfd == nil)
+					fatal(sys->sprint("Failed to create directory %s: %r\n", t.recdir));
+				rfd = nil;
+			}
+			runtest(t);
+		}
+		mreport(1, S_INFO, 3, sys->sprint("Finished run %bd", run));
+		run++;
+	}
+	mreport(0, S_INFO, 2, "Finished tests");
+}
+
+usage()
+{
+	sys->fprint(stderr, "Usage itest [-eo] [-c cflag] [-r count] [-v vlevel] [-C cfile] [-R recroot] [testdir ...]\n");
+	raise "fail: usage";
+}
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "%s\n", s);
+	raise "fail: error";
+}
+
+nomod(mod: string)
+{
+	sys->fprint(stderr, "Failed to load %s\n", mod);
+	raise "fail: module";
+}
+
+toint(opt, s: string, min, max: int): int
+{
+	if (len s == 0 || str->take(s, "[0-9]+-") != s)
+		fatal(sys->sprint("no value specified for option %s", opt));
+	v := int s;
+	if (v < min)
+		fatal(sys->sprint("option %s value is less than minimum of %d: %d", opt, v, min));
+	if (max != -1 && v > max)
+		fatal(sys->sprint("option %s value is greater than maximum of %d: %d", opt, v, max));
+	return v;
+}
+
+arg_tests(args: list of string): array of ref Test
+{
+	al := len args;
+	ta := array[al] of ref Test;
+	for (i:=0; i<al; i++) {
+		tspec := hd args;
+		args = tl args;
+		ta[i] = ref Test(tspec, "", nil, "", "", "", 0, 0, 0, 0, 0);
+		tcheck(ta[i]);
+	}
+	return ta;
+}
+
+config_tests(cf: string): array of ref Test
+{
+	cl := linelist(cf);
+	if (cl == nil)
+		fatal("No tests in config file");
+	al := len cl;
+	ta := array[al] of ref Test;
+	for (i:=0; i<al; i++) {
+		tspec := hd cl;
+		cl = tl cl;
+		ta[i] = ref Test(tspec, "", nil, "", "", "", 0, 0, 0, 0, 0);
+		tcheck(ta[i]);
+	}
+	return ta;
+
+}
+
+highest(path: string): int
+{
+	(da, nd) := readdir->init(path, Readdir->NAME);
+	high := 0;
+	for (i:=0; i<nd; i++) {
+		n := int da[i].name;
+		if (n > high)
+			high = n;	
+	}
+	return high;
+}
+
+tcheck(t: ref Test): int
+{
+	td := t.spec;
+	if (!checkdir(td)) {
+		fatal(sys->sprint("Failed to find test %s\n", td));
+		return 0;
+	}
+	tf1 := t.spec + "/t.sh";
+	tf2 := t.spec + "/t.dis";
+	if (checkexec(tf1)) {
+		t.fullspec = tf1;
+		return 1;
+	}
+	if (checkexec(tf2)) {
+		t.fullspec = tf2;
+		return 1;
+	}
+	fatal(sys->sprint("Could not find executable files %s or %s\n", tf1, tf2));
+	return 0;
+}
+
+checkdir(d: string): int
+{
+	(ok, dir) := sys->stat(d);
+	if (ok != 0 || ! dir.qid.qtype & Sys->QTDIR)
+		return 0;
+	return 1;
+}
+
+checkexec(d: string): int
+{
+	(ok, dir) := sys->stat(d);
+	if (ok != 0 || ! dir.mode & 8r100)
+		return 0;
+	return 1;
+}
+
+
+set_cflag(f: int)
+{
+	wfile("/dev/jit", string f, 0);
+
+}
+
+runtest(t: ref Test)
+{
+	if (t.failed)
+		return;
+
+	if (cflag != -1) {
+		mreport(0, S_INFO, 7, sys->sprint("Setting cflag to %d", cflag));
+		set_cflag(cflag);
+	}
+	readme := t.spec + "/" + README;
+	mreport(2, S_INFO, 3, sys->sprint("Starting test %s cflag=%s", t.spec, rfile("/dev/jit")));
+	if (verbosity > 8)
+		display_file(readme);
+	sync := chan of int;
+	spawn monitor(t, sync);
+	<-sync;
+}
+
+monitor(t: ref Test, sync: chan of int)
+{
+	pid := sys->pctl(Sys->FORKFD|Sys->FORKNS|Sys->FORKENV|Sys->NEWPGRP, nil);
+	pa := array[2] of ref Sys->FD;
+	if (sys->pipe(pa))
+		fatal("Failed to set up pipe");
+	if (env->setenv(Itslib->ENV_MFD, string pa[0].fd))
+		fatal("Failed to set environment variable " + Itslib->ENV_MFD);
+	mlfd: ref Sys->FD;
+	if (t.recdir != nil) {
+		mfile := t.recdir+"/"+MSGFILE;
+		mlfd = sys->create(mfile, Sys->OWRITE, 8r660);
+		if (mlfd == nil)
+			fatal(sys->sprint("Failed to create %s: %r'\n", mfile));
+		t.stdout = t.recdir+"/stdout";
+		t.stderr = t.recdir+"/stderr";
+	} else {
+		t.stdout = "/tmp/itest.stdout";
+		t.stderr = "/tmp/itest.stderr";
+	}
+	cf := int rfile("/dev/jit");
+	stime := sys->millisec();
+	swhen := daytime->now();
+	etime := -1;
+	rsync := chan of int;
+	spawn runit(t.fullspec, t.stdout, t.stderr, t.spec, pa[0], rsync);
+	<-rsync;
+	pa[0] = nil;
+	(nwarns, nerrors, nfatals) := (0, 0, 0);
+	while (1) {
+		mbuf := array[Sys->ATOMICIO] of byte;
+		n := sys->read(pa[1], mbuf, len mbuf);
+		if (n <= 0) break;
+		msg := string mbuf[:n];
+		sev := int msg[0:1];
+		verb := int msg[1:2];
+		body := msg[2:];
+		if (sev == S_STIME)
+			stime = int body;
+		else if (sev == S_ETIME)
+			etime = int body;
+		else {
+			if (sev == S_WARN) {
+				nwarns++;
+				t.nwarns++;
+			}
+			else if (sev == S_ERROR) {
+				nerrors++;
+				t.nerrors++;
+			}
+			else if (sev == S_FATAL) {
+				nfatals++;
+				t.nfatals++;
+			}
+			mreport(3, sev, verb, sys->sprint("%s: %s", severs(sev), body));
+		}
+		if (mlfd != nil)
+			sys->fprint(mlfd, "%d:%s", now, msg);
+	}
+	if (etime < 0) {
+		etime = sys->millisec();
+		if (mlfd != nil)
+			sys->fprint(mlfd, "%d:%s", now, sys->sprint("%d0%d\n", S_ETIME, etime));
+	}
+	elapsed := etime-stime;
+	errsum := sys->sprint("WRN:%d ERR:%d FTL:%d", nwarns, nerrors, nfatals);
+	mreport(2, S_INFO, 3, sys->sprint("Finished test %s after %dms - %s", t.spec, elapsed, errsum));
+	if (t.recdir != "") {
+		wfile(t.recdir+"/"+SUMFILE, sys->sprint("%d %d %d %s\n", swhen, elapsed, cf, t.fullspec), 1);
+	}
+	if (display_stdout) {
+		mreport(2, 0, 0, "Stdout from test:");
+		display_file(t.stdout);
+	}
+	if (display_stderr) {
+		mreport(2, 0, 0, "Stderr from test:");
+		display_file(t.stderr);
+	}
+	sync <-= pid;
+}
+
+runit(fullspec, sofile, sefile, tpath: string, mfd: ref Sys->FD, sync: chan of int)
+{
+	pid := sys->pctl(Sys->NEWFD|Sys->FORKNS, mfd.fd::nil);
+	o, e: ref Sys->FD;
+	o = sys->create(sofile, Sys->OWRITE, 8r660);
+	if (o == nil)
+		treport(mfd, S_ERROR, 0, "Failed to open stdout: %r\n");
+	else
+		sys->dup(o.fd, 1);
+	o = nil;
+	e = sys->create(sefile, Sys->OWRITE, 8r660);
+	if (e == nil)
+		treport(mfd, S_ERROR, 0, "Failed to open stderr: %r\n");
+	else
+		sys->dup(e.fd, 2);
+	e = nil;
+	sync <-= pid;
+	args := list of {fullspec};
+	if (fullspec[len fullspec-1] == 's')
+		cmd := load Command fullspec;
+	else {
+		cmd = load Command "/dis/sh.dis";
+		args = fullspec :: args;
+	}
+	if (cmd == nil) {
+		treport(mfd, S_FATAL, 0, sys->sprint("Failed to load Command from %s", "/dis/sh.dis"));
+		return;
+	}
+	if (sys->chdir(tpath))
+		treport(mfd, S_FATAL, 0, "Failed to cd to " + tpath);
+	{
+		cmd->init(context, args);
+	} exception ex {
+		"*" =>
+		treport(mfd, S_FATAL, 0, sys->sprint("Exception %s in test %s", ex, fullspec));
+	}			
+}
+
+severs(sevs: int): string
+{
+	SEVMAP :=  array[] of {"INF", "WRN", "ERR", "FTL"};
+	if (sevs >= len SEVMAP)
+		sstr := "UNK";
+	else
+		sstr = SEVMAP[sevs];
+	return sstr;
+}
+
+
+rfile(file: string): string
+{
+	fd := sys->open(file, Sys->OREAD);
+	if (fd == nil) return nil;
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(fd, buf, len buf);
+	return string buf[:n];
+}
+
+
+wfile(file: string, text: string, create: int): int
+{
+	if (create)
+		fd := sys->create(file, Sys->OWRITE, 8r660);
+	else 
+		fd = sys->open(file, Sys->OWRITE);
+	if (fd == nil) {
+		sys->fprint(stderr, "Failed to open %s: %r\n", file);
+		return 0;
+	}
+	a := array of byte text;
+	al := len a;
+	if (sys->write(fd, a, al) != al) {
+		sys->fprint(stderr, "Failed to write to %s: %r\n", file);
+		return 0;
+	}
+	fd = nil;
+	return 1;
+}
+
+linelist(file: string): list of string
+{
+	bf := bufio->open(file, Bufio->OREAD);
+	if (bf == nil)
+		return nil;
+	cl : list of string;
+	while ((line := bf.gets('\n')) != nil) {
+		if (line[len line -1] == '\n')
+			line = line[:len line - 1];
+		cl = line :: cl;
+	}
+	bf = nil;
+	return cl;
+}
+
+display_file(file: string)
+{
+	bf := bufio->open(file, Bufio->OREAD);
+	if (bf == nil)
+		return;
+	while ((line := bf.gets('\n')) != nil) {
+		sys->print("                    %s", line);
+	}
+}
+
+mreport(indent: int, sev: int, verb: int, msg: string)
+{
+	now = daytime->now();
+	tm := daytime->local(now);
+	time := sys->sprint("%4d%02d%02d %02d:%02d:%02d", tm.year+1900, tm.mon-1, tm.mday, tm.hour, tm.min, tm.sec);
+	pad := "---"[:indent];
+	term := "";
+	if (len msg && msg[len msg-1] != '\n')
+		term = "\n";
+	if (sev || verb <= verbosity)
+		sys->print("%s %s%s%s", time, pad, msg, term);
+}
+
+
+treport(mfd: ref Sys->FD, sev: int, verb: int, msg: string)
+{
+	sys->fprint(mfd, "%d%d%s\n", sev, verb, msg);
+}
--- /dev/null
+++ b/appl/cmd/itreplay.b
@@ -1,0 +1,230 @@
+implement Itreplay;
+
+include "sys.m";
+	sys: Sys;
+include "string.m";
+	str: String;
+include "draw.m";
+include "daytime.m";
+	daytime: Daytime;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "readdir.m";
+	readdir: Readdir;
+include "arg.m";
+include "itslib.m";
+	S_INFO, S_WARN, S_ERROR, S_FATAL, S_STIME, S_ETIME: import Itslib;
+
+SUMFILE: con "summary";
+MSGFILE: con "msgs";
+
+verbosity := 3;
+display_stderr := 0;
+display_stdout := 0;
+
+stderr: ref Sys->FD;
+
+
+Itreplay: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		nomod(Daytime->PATH);
+	str = load String String->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		nomod(Bufio->PATH);
+	if(str == nil)
+		nomod(String->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		nomod(Readdir->PATH);
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		'e' =>	display_stderr++;
+		'o' =>	display_stdout++;
+		'v' =>	verbosity = toint("v", arg->arg(), 0, 9);
+		* =>		usage();
+		}
+	recdirl := arg->argv();
+	arg = nil;
+	if (recdirl == nil)
+		usage();
+	while (recdirl != nil) {
+		dir := hd recdirl;
+		recdirl = tl recdirl;
+		replay(dir);
+	}
+}
+
+usage()
+{
+	sys->fprint(stderr, "Usage: itreplay [-eo] [-v verbosity] recorddir ...\n");
+	raise "fail: usage";
+	exit;
+}
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "%s\n", s);
+	raise "fail: error";
+	exit;
+}
+
+nomod(mod: string)
+{
+	sys->fprint(stderr, "Failed to load %s\n", mod);
+	raise "fail: module";
+	exit;
+}
+
+toint(opt, s: string, min, max: int): int
+{
+	if (len s == 0 || str->take(s, "[0-9]+-") != s)
+		fatal(sys->sprint("no value specified for option %s", opt));
+	v := int s;
+	if (v < min)
+		fatal(sys->sprint("option %s value is less than minimum of %d: %d", opt, v, min));
+	if (max != -1 && v > max)
+		fatal(sys->sprint("option %s value is greater than maximum of %d: %d", opt, v, max));
+	return v;
+}
+
+replay(dir: string)
+{
+	sl := linelist(dir+"/"+SUMFILE);
+	if (sl == nil) {
+		sys->fprint(stderr, "No summary file in %s\n", dir);
+		return;
+	}
+	sline := hd sl;
+	(n, toks) := sys->tokenize(sline, " ");
+	if (n < 4) {
+		sys->fprint(stderr, "Bad summary file in %s\n", dir);
+		return;
+	}
+	when := int hd toks;
+	toks = tl toks;
+	elapsed := int hd toks;
+	toks = tl toks;
+	cflag := int hd toks;
+	toks = tl toks;
+	testspec := hd toks;
+	mreport(1, when, 0, 2, sys->sprint("Processing %s: test %s ran in %dms with cflag=%d\n", dir, testspec, elapsed, cflag));
+	replay_msgs(dir+"/"+MSGFILE, testspec, cflag);
+	if (display_stdout) {
+		mreport(2, 0, 0, 0, "Stdout from test:");
+		display_file(dir+"/stdout");
+	}
+	if (display_stderr) {
+		mreport(2, 0, 0, 0, "Stderr from test:");
+		display_file(dir+"/stderr");
+	}
+}
+
+
+replay_msgs(mfile: string, tspec: string, cflag: int)
+{
+	mf := bufio->open(mfile, Bufio->OREAD);
+	if (mf == nil)
+		return;
+	(nwarns, nerrors, nfatals) := (0, 0, 0);
+	stime := 0;
+
+	while ((line := mf.gets('\n')) != nil) {
+		(whens, rest) := str->splitl(line, ":");
+		when := int whens;
+		msg := rest[1:];
+		sev := int msg[0:1];
+		verb := int msg[1:2];
+		body := msg[2:];
+		if (sev == S_STIME) {
+			stime = int body;
+			mreport(2, when, 0, 3, sys->sprint("Starting test %s cflag=%d", tspec, cflag));
+		}
+		else if (sev == S_ETIME) {
+			uetime := int body;
+			elapsed := uetime-stime;
+			errsum := sys->sprint("WRN:%d ERR:%d FTL:%d", nwarns, nerrors, nfatals);
+			mreport(2, when+(int body-stime)/1000, 0, 3, sys->sprint("Finished test %s after %dms - %s", tspec, elapsed, errsum));
+		}
+		else {
+			if (sev == S_WARN) {
+				nwarns++;
+			}
+			else if (sev == S_ERROR) {
+				nerrors++;
+			}
+			else if (sev == S_FATAL) {
+				nfatals++;
+			}
+			mreport(3, when, sev, verb, sys->sprint("%s: %s", severs(sev), body));
+		}
+	}
+}
+
+linelist(file: string): list of string
+{
+	bf := bufio->open(file, Bufio->OREAD);
+	if (bf == nil)
+		return nil;
+	cl : list of string;
+	while ((line := bf.gets('\n')) != nil) {
+		if (line[len line -1] == '\n')
+			line = line[:len line - 1];
+		cl = line :: cl;
+	}
+	bf = nil;
+	return cl;
+}
+
+display_file(file: string)
+{
+	bf := bufio->open(file, Bufio->OREAD);
+	if (bf == nil)
+		return;
+	while ((line := bf.gets('\n')) != nil) {
+		sys->print("                    %s", line);
+	}
+}
+
+
+severs(sevs: int): string
+{
+	SEVMAP :=  array[] of {"INF", "WRN", "ERR", "FTL"};
+	if (sevs >= len SEVMAP)
+		sstr := "UNK";
+	else
+		sstr = SEVMAP[sevs];
+	return sstr;
+}
+
+
+mreport(indent: int, when: int, sev: int, verb: int, msg: string)
+{
+	time := "";
+	if (when) {
+		tm := daytime->local(when);
+		time = sys->sprint("%4d%02d%02d %02d:%02d:%02d", tm.year+1900, tm.mon-1, tm.mday, tm.hour, tm.min, tm.sec);
+	}
+	pad := "---"[:indent];
+	term := "";
+	if (len msg && msg[len msg-1] != '\n')
+		term = "\n";
+	if (sev || verb <= verbosity)
+		sys->print("%-17s %s%s%s", time, pad, msg, term);
+}
--- /dev/null
+++ b/appl/cmd/kill.b
@@ -1,0 +1,146 @@
+implement Kill;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+
+Kill: module {
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+stderr: ref Sys->FD;
+
+usage()
+{
+	sys->fprint(stderr, "usage: kill [-g] pid|module [...]\n");
+	raise "fail: usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil){
+		sys->fprint(stderr, "kill: cannot load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+
+	msg := array of byte "kill";
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		'g' =>
+			msg = array of byte "killgrp";
+		* =>
+			usage();
+		}
+
+	argv := arg->argv();
+	arg = nil;
+	if(argv == nil)
+		usage();
+	n := 0;
+	for(v := argv; v != nil; v = tl v) {
+		s := hd v;
+		if (s == nil)
+			usage();
+		if(s[0] >= '0' && s[0] <= '9')
+			n += killpid(s, msg, 1);
+		else
+			n += killmod(s, msg);
+	}
+	if (n == 0 && argv != nil)
+		raise "fail:nothing killed";
+}
+
+killpid(pid: string, msg: array of byte, sbok: int): int
+{
+	fd := sys->open("/prog/"+pid+"/ctl", sys->OWRITE);
+	if(fd == nil) {
+		err := sys->sprint("%r");
+		elen := len err;
+		if(sbok || err != "thread exited" && elen >= 14 && err[elen-14:] != "does not exist")
+			sys->fprint(stderr, "kill: cannot open /prog/%s/ctl: %r\n", pid);
+		return 0;
+	}
+
+	n := sys->write(fd, msg, len msg);
+	if(n < 0) {
+		err := sys->sprint("%r");
+		elen := len err;
+		if(sbok || err != "thread exited")
+			sys->fprint(stderr, "kill: cannot kill %s: %r\n", pid);
+		return 0;
+	}
+	return 1;
+}
+
+killmod(mod: string, msg: array of byte): int
+{
+	fd := sys->open("/prog", sys->OREAD);
+	if(fd == nil) {
+		sys->fprint(stderr, "kill: open /prog: %r\n");
+		return 0;
+	}
+
+	pids: list of string;
+	for(;;) {
+		(n, d) := sys->dirread(fd);
+		if(n <= 0) {
+			if (n < 0)
+				sys->fprint(stderr, "kill: read /prog: %r\n");
+			break;
+		}
+
+		for(i := 0; i < n; i++)
+			if (killmatch(d[i].name, mod))
+				pids = d[i].name :: pids;		
+	}
+	if (pids == nil) {
+		sys->fprint(stderr, "kill: cannot find %s\n", mod);
+		return 0;
+	}
+	n := 0;
+	for (; pids != nil; pids = tl pids)
+		if (killpid(hd pids, msg, 0)) {
+			sys->print("%s ", hd pids);
+			n++;
+		}
+	if (n > 0)
+		sys->print("\n");
+	return n;
+}
+
+killmatch(dir, mod: string): int
+{
+	status := "/prog/"+dir+"/status";
+	fd := sys->open(status, sys->OREAD);
+	if(fd == nil)
+		return 0;
+	buf := array[512] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) {
+		err := sys->sprint("%r");
+		if(err != "thread exited")
+			sys->fprint(stderr, "kill: cannot read %s: %s\n", status, err);
+		return 0;
+	}
+
+	# module name is last field
+	(nil, fields) := sys->tokenize(string buf[0:n], " ");
+	for(s := ""; fields != nil; fields = tl fields)
+		s = hd fields;
+
+	# strip builtin module, e.g. Sh[$Sys]
+	for(i := 0; i < len s; i++) {
+		if(s[i] == '[') {
+			s = s[0:i];
+			break;
+		}
+	}
+
+	return s == mod;
+}
--- /dev/null
+++ b/appl/cmd/lego/clock.b
@@ -1,0 +1,214 @@
+implement Clock;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+
+include "math.m";
+	math: Math;
+	sqrt, atan2, hypot, Degree: import math;
+
+include "tk.m";
+	tk: Tk;
+	top: ref Tk->Toplevel;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+Clock: module {
+	init:	fn(ctxt: ref Draw->Context, argl: list of string);
+};
+
+cmds := array[] of {
+	"bind . <Configure> {send win resize}",
+	"canvas .face -height 200 -width 200 -bg yellow",
+	"bind .face <ButtonPress> {send ptr %x %y}",
+	"bind .face <ButtonRelease> {send ptr release}",
+	"pack .face -expand yes -fill both",
+	"button .reset -text Reset -command {send win reset}",
+	"pack .reset -after .Wm_t.title -side right -fill y",
+	"pack propagate . no",
+};
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	math = load Math Math->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	tkclient->init();
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	clockface := sys->open("/chan/clockface", Sys->ORDWR);
+	if (clockface == nil) {
+		sys->print("open /chan/clockface failed: %r\n");
+		raise "fail:clockface";
+	}
+	tock := chan of string;
+	spawn readme(clockface, tock);
+
+	titlech: chan of string;
+	(top, titlech) = tkclient->toplevel(ctxt, "hh:mm", "", Tkclient->Appl);
+	win := chan of string;
+	ptr := chan of string;
+	tk->namechan(top, win, "win");
+	tk->namechan(top, ptr, "ptr");
+	for(i:=0; i<len cmds; i++)
+		tk->cmd(top, cmds[i]);
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "ptr"::nil);
+	drawface();
+	spawn hands(ptr, clockface);
+
+	for (;;) alt {
+	s := <-top.ctxt.kbd =>
+		tk->keyboard(top, s);
+	s := <-top.ctxt.ptr =>
+		tk->pointer(top, *s);
+	s := <-top.ctxt.ctl or
+	s = <-top.wreq or
+	s = <-titlech =>
+		tkclient->wmctl(top, s);
+	msg := <-win =>
+		case msg {
+		"resize" =>	drawface();
+		"reset" =>		sys->fprint(clockface, "reset");
+		}
+	nowis := <-tock =>
+		(n, toks) := sys->tokenize(nowis, ":");
+		if (n == 2) {
+			(hour, minute) = (int hd toks, int hd tl toks);
+			setclock();
+		}
+	}
+}
+
+readme(fd: ref Sys->FD, ch: chan of string)
+{
+	buf := array[64] of byte;
+	while ((n := sys->read(fd, buf, len buf)) > 0) {
+		if (buf[n-1] == byte '\n')
+			n--;
+		ch <-= string buf[:n];
+	}
+	ch <-= "99:99";
+}
+
+hour, minute: int;
+center, focus: Point;
+major: int;
+
+Frim:	con .98;
+Fminute:	con .90;
+Fhour:	con .45;
+Fnub:	con .05;
+
+hands(ptr: chan of string, fd: ref Sys->FD)
+{
+	for (;;) {
+		pos := <-ptr;
+		p := s2p(pos);
+		hand := "";
+		if (elinside(p, Fnub))
+			hand = nil;
+		else if (elinside(p, Fhour))
+			hand = "hour";
+		else if (elinside(p, Fminute))
+			hand = "minute";
+
+		do {
+			p = s2p(pos).sub(center);
+			angle := int (atan2(real -p.y, real p.x) / Degree);
+			if (hand != nil)
+				tkc(".face itemconfigure "+hand+" -start "+string angle+"; update");
+			case hand {
+			"hour" =>		hour = ((360+90-angle) / 30) % 12;
+			"minute" =>	minute = ((360+90-angle) / 6) % 60;
+			}
+		} while ((pos = <-ptr) != "release");
+		if (hand != nil)
+			sys->fprint(fd, "%d:%d\n", hour, minute);
+	}
+}
+
+drawface()
+{
+	elparms();
+	tkc(sys->sprint(".face configure -scrollregion {0 0 %d %d}", 2*center.x, 2*center.y));
+	tkc(".face delete all");
+	tkc(".face create oval "+elrect(Frim)+" -fill fuchsia -outline aqua -width 2");
+	for (a := 0; a < 360; a += 30)
+		tkc(".face create arc "+elrect(Frim)+" -fill aqua -outline aqua -width 2 -extent 1 -start "+string a);
+	tkc(".face create oval "+elrect(Fminute)+" -fill fuchsia -outline fuchsia");
+	tkc(".face create oval "+elrect(Fnub)+" -fill aqua -outline aqua");
+	tkc(".face create arc "+elrect(Fhour)+" -fill aqua -outline aqua -width 6 -extent 1 -tags hour");
+	tkc(".face create arc "+elrect(Fminute)+" -fill aqua -outline aqua -width 2 -extent 1 -tags minute");
+	setclock();
+}
+
+setclock()
+{
+	tkc(".face itemconfigure hour -start "+string (90 - 30*(hour%12) - minute/2));
+	tkc(".face itemconfigure minute -start "+string (90 - 6*minute));
+	tkc(sys->sprint(".Wm_t.title configure -text {%d:%.2d}", (hour+11)%12+1, minute));
+	tkc("update");
+}
+
+elparms()
+{
+	center = (int tkc(".face cget actwidth") / 2, int tkc(".face cget actheight") / 2);
+	dist := center.x*center.x - center.y*center.y;
+	if (dist > 0) {
+		major = 2 * center.x;
+		focus = (int sqrt(real dist), 0);
+	} else {
+		major = 2 * center.y;
+		focus = (0, int sqrt(real -dist));
+	}
+}
+
+elinside(p: Point, frac: real): int
+{
+	foc := mulf(focus, frac);
+	d := dist(p, center.add(foc)) + dist(p, center.sub(foc));
+	return (d < frac * real major);
+}
+
+elrect(frac: real): string
+{
+	inset := mulf(center, 1.-frac);
+	r := Rect(inset, center.mul(2).sub(inset));
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+mulf(p: Point, f: real): Point
+{
+	return (int (f * real p.x), int (f * real p.y));
+}
+
+dist(p, q: Point): real
+{
+	p = p.sub(q);
+	return hypot(real p.x, real p.y);
+}
+
+s2p(s: string): Point
+{
+	(nil, xy) := sys->tokenize(s, " ");
+	if (len xy != 2)
+		return (0, 0);
+	return (int hd xy, int hd tl xy);
+}
+
+tkc(msg: string): string
+{
+	ret := tk->cmd(top, msg);
+	if (ret != nil && ret[0] == '!')
+		sys->print("tk error? %s → %s\n", msg, ret);
+	return ret;
+}
--- /dev/null
+++ b/appl/cmd/lego/clockface.b
@@ -1,0 +1,384 @@
+# Model 1
+implement Clockface;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+Clockface: module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+hmpath:	con "motor/0";		# hour-hand motor
+mmpath:	con "motor/2";		# minute-hand motor
+allmpath:	con "motor/012";	# all motors (for stopall msg)
+
+hbpath:	con "sensor/0";	# hour-hand sensor
+mbpath:	con "sensor/2";	# minute-hand sensor
+lspath:	con "sensor/1";	# light sensor;
+
+ONTHRESH:	con 780;		# light sensor thresholds
+OFFTHRESH:	con 740;
+NCLICKS:		con 120;
+MINCLICKS:	con 2;		# min number of clicks required to stop a motor
+
+Hand: adt {
+	motor:	ref Sys->FD;
+	sensor:	ref Sys->FD;
+	fwd:		array of byte;
+	rev:		array of byte;
+	stop:		array of byte;
+	pos:		int;
+	time:		int;
+};
+
+lightsensor:	ref Sys->FD;
+allmotors:		ref Sys->FD;
+hourhand:	ref Hand;
+minutehand:	ref Hand;
+timedata:		array of byte;
+readq:		list of Sys->Rread;
+verbose		:= 0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	argv = tl argv;
+	if (len argv > 0 && hd argv == "-v") {
+		verbose++;
+		argv = tl argv;
+	}
+	if (len argv != 1) {
+		sys->print("usage: [-v] legodir\n");
+		raise "fail:usage";
+	}
+	legodir := hd argv + "/";
+
+	# set up our control file
+	f2c := sys->file2chan("/chan", "clockface");
+	if (f2c == nil) {
+		sys->print("cannot create clockface channel: %r\n");
+		return;
+	}
+
+	# get the motor files
+	log("opening motor files");
+	hm := sys->open(legodir + hmpath, Sys->OWRITE);
+	mm := sys->open(legodir +mmpath, Sys->OWRITE);
+	allmotors = sys->open(legodir + allmpath, Sys->OWRITE);
+	if (hm == nil || mm == nil || allmotors == nil) {
+		sys->print("cannot open motor files\n");
+		raise "fail:error";
+	}
+
+	# get the sensor files
+	log("opening sensor files");
+	hb := sys->open(legodir + hbpath, Sys->ORDWR);
+	mb := sys->open(legodir + mbpath, Sys->ORDWR);
+	lightsensor = sys->open(legodir + lspath, Sys->ORDWR);
+
+	if (hb == nil || mb == nil) {
+		sys->print("cannot open sensor files\n");
+		raise "fail:error";
+	}
+
+	hourhand = ref Hand(hm, hb, array of byte "r7", array of byte "f7", array of byte "s7", 0, 00);
+	minutehand = ref Hand(mm, mb, array of byte "f7", array of byte "r7", array of byte "s7", 0, 00);
+
+	log("setting sensor types");
+	setsensortypes(hourhand, minutehand, lightsensor);
+
+	# get the hands to 12 o'clock
+	reset();
+	log(sys->sprint("H %d, M %d", hourhand.pos, minutehand.pos));
+	spawn srvlink(f2c);
+}
+
+srvlink(f2c: ref Sys->FileIO)
+{
+	tick := chan of int;
+	spawn eggtimer(tick);
+
+	for (;;) alt {
+	(nil, count, fid, rc) := <-f2c.read =>
+		if (rc == nil) {
+			close(fid);
+			continue;
+		}
+		if (count < len timedata) {
+			rc <-= (nil, "read too small");
+			continue;
+		}
+		if (open(fid))
+			readq = rc :: readq;
+		else
+			rc <-= (timedata, nil);
+
+	(nil, data, fid, wc) := <-f2c.write =>
+		if (wc == nil) {
+			close(fid);
+			continue;
+		}
+		(nil, toks) := sys->tokenize(string data, ": \t\n");
+		if (len toks == 2) {
+			wc <-= (len data, nil);
+			hourhand.time = int hd toks % 12;
+			minutehand.time = int hd tl toks % 60;
+			sethands();
+		} else if (len toks == 1 && hd toks == "reset") {
+			wc <-= (len data, nil);
+			reset();
+		} else
+			wc <-= (0, "syntax is hh:mm or `reset'");
+
+	<-tick =>
+		if (++minutehand.time == 60) {
+			minutehand.time = 0;
+			hourhand.time++;
+			hourhand.time %= 12;
+		}
+		sethands();
+	}
+}
+
+readers: list of int;
+
+open(fid: int): int
+{
+	for (rlist := readers; rlist != nil; rlist = tl rlist)
+		if (hd rlist == fid)
+			return 1;
+	readers = fid :: readers;
+	return 0;
+}
+
+close(fid: int)
+{
+	rlist: list of int;
+	for (; readers != nil; readers = tl readers)
+		if (hd readers != fid)
+			rlist = hd readers :: rlist;
+	readers = rlist;
+}
+
+eggtimer(tick: chan of int)
+{
+	next := sys->millisec();
+	for (;;) {
+		next += 60*1000;
+		sys->sleep(next - sys->millisec());
+		tick <-= 1;
+	}
+}
+
+clicks(): (int, int)
+{
+	h := hourhand.time;
+	m := minutehand.time;
+	h = ((h * NCLICKS) / 12) + ((m * NCLICKS) / (12 * 60));
+	m = (m * NCLICKS) / 60;
+	return (h, m);
+}
+
+sethands()
+{
+	timedata = array of byte sys->sprint("%2d:%.2d\n", (hourhand.time+11) % 12 + 1, minutehand.time);
+	for (; readq != nil; readq = tl readq)
+		alt {
+		(hd readq) <-= (timedata, nil) => ;
+		* => ;
+		}
+
+	(hclk, mclk) := clicks();
+	for (i := 0; i < 6; i++) {
+		hdelta := clickdistance(hourhand.pos, hclk, NCLICKS);
+		mdelta := clickdistance(minutehand.pos, mclk, NCLICKS);
+		if (hdelta != 0)
+			sethand(hourhand, hdelta);
+		else if (mdelta != 0)
+			sethand(minutehand, mdelta);
+		else
+			break;
+	}
+	releaseall();
+}
+
+clickdistance(start, stop, mod: int): int
+{
+	if (start > stop)
+		stop += mod;
+	d := (stop - start) % mod;
+	if (d > mod/2)
+		d -= mod;
+	return d;
+}
+
+setsensortypes(h1, h2: ref Hand, ls: ref Sys->FD)
+{
+	button := array of byte "b0";
+	light := array of byte "l0";
+	sys->write(h1.sensor, button, len button);
+	sys->write(h2.sensor, button, len button);
+	sys->write(ls, light, len light);
+}
+
+HOUR_ADJUST: con 1;
+MINUTE_ADJUST: con 2;
+
+reset()
+{
+	# run the motors until hands are well away from 12 o'clock (below threshold)
+
+	val := readsensor(lightsensor);
+	if (val > OFFTHRESH) {
+		triggered := chan of int;
+		log("wait for hands clear of light sensor");
+		spawn lightwait(triggered, lightsensor, 0);
+		forward(minutehand);
+		reverse(hourhand);
+		val = <-triggered;
+		stopall();
+		log("sensor "+string val);
+	}
+
+	resethand(hourhand);
+	hourhand.pos += HOUR_ADJUST;
+	resethand(minutehand);
+	minutehand.pos += MINUTE_ADJUST;
+	sethands();
+}
+
+sethand(hand: ref Hand, delta: int)
+{
+	triggered := chan of int;
+	dir := 1;
+	if (delta < 0) {
+		dir = -1;
+		delta = -delta;
+	}
+	if (delta > MINCLICKS) {
+		spawn handwait(triggered, hand, delta - MINCLICKS);
+		if (dir > 0)
+			forward(hand);
+		else
+			reverse(hand);
+		<-triggered;
+		stop(hand);
+		hand.pos += dir * readsensor(hand.sensor);
+	} else {
+		startval := readsensor(hand.sensor);
+		if (dir > 0)
+			forward(hand);
+		else
+			reverse(hand);
+		stop(hand);
+		hand.pos += dir * (readsensor(hand.sensor) - startval);
+	}
+	if (hand.pos < 0)
+		hand.pos += NCLICKS;
+	hand.pos %= NCLICKS;
+}
+
+resethand(hand: ref Hand)
+{
+	triggered := chan of int;
+	val: int;
+
+	# run the hand until the light sensor is above threshold
+	log("running hand until light sensor activated");
+	spawn lightwait(triggered, lightsensor, 1);
+	forward(hand);
+	val = <-triggered;
+	stop(hand);
+	log("sensor "+string val);
+
+	startclick := readsensor(hand.sensor);
+
+	# advance until light sensor drops below threshold
+	log("running hand until light sensor clear");
+	spawn lightwait(triggered, lightsensor, 0);
+	forward(hand);
+	val = <-triggered;
+	stop(hand);
+	log("sensor "+string val);
+	
+	stopclick := readsensor(hand.sensor);
+	nclicks := stopclick - startclick;
+	log(sys->sprint("startpos %d, endpos %d (nclicks %d)", startclick, stopclick, nclicks));
+
+	hand.pos = nclicks/2;
+}
+
+stop(hand: ref Hand)
+{
+	sys->seek(hand.motor, big 0, Sys->SEEKSTART);
+	sys->write(hand.motor, hand.stop, len hand.stop);
+}
+
+stopall()
+{
+	msg := array of byte "s0s0s0";
+	sys->seek(allmotors, big 0, Sys->SEEKSTART);
+	sys->write(allmotors, msg, len msg);
+}
+
+releaseall()
+{
+	msg := array of byte "F0F0F0";
+	sys->seek(allmotors, big 0, Sys->SEEKSTART);
+	sys->write(allmotors, msg, len msg);
+}
+
+forward(hand: ref Hand)
+{
+	sys->seek(hand.motor, big 0, Sys->SEEKSTART);
+	sys->write(hand.motor, hand.fwd, len hand.fwd);
+}
+
+reverse(hand: ref Hand)
+{
+	sys->seek(hand.motor, big 0, Sys->SEEKSTART);
+	sys->write(hand.motor, hand.rev, len hand.rev);
+}
+
+readsensor(fd: ref Sys->FD): int
+{
+	buf := array[4] of byte;
+	sys->seek(fd, big 0, Sys->SEEKSTART);
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return -1;
+	return int string buf[:n];
+}
+
+handwait(reply: chan of int, hand: ref Hand, clicks: int)
+{
+	blk := array of byte ("b" + string clicks);
+	log("handwait "+string blk);
+	sys->seek(hand.sensor, big 0, Sys->SEEKSTART);
+	if (sys->write(hand.sensor, blk, len blk) != len blk)
+		sys->print("handwait write error: %r\n");
+	reply <-= readsensor(hand.sensor);
+}
+
+lightwait(reply: chan of int, fd: ref Sys->FD, on: int)
+{
+	thresh := "";
+	if (on)
+		thresh = "l>" + string ONTHRESH;
+	else
+		thresh = "l<" + string OFFTHRESH;
+	blk := array of byte thresh;
+	log("lightwait "+string blk);
+	sys->seek(fd, big 0, Sys->SEEKSTART);
+	sys->write(fd, blk, len blk);
+	reply <-= readsensor(fd);
+}
+
+log(msg: string)
+{
+	if (verbose)
+		sys->print("%s\n", msg);
+}
--- /dev/null
+++ b/appl/cmd/lego/firmdl.b
@@ -1,0 +1,294 @@
+implement RcxFirmdl;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "rcxsend.m";
+
+RcxFirmdl : module {
+	init : fn (ctxt : ref Draw->Context, argv : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+rcx : RcxSend;
+me : int;
+
+Iobuf : import bufio;
+
+Image : adt {
+	start : int;
+	offset : int;
+	length : int;
+	data : array of byte;
+};
+
+DL_HDR : con 5;			# download packet hdr size
+DL_DATA : con 16rc8;		# download packet payload size
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	me = sys->pctl(Sys->NEWPGRP, nil);
+
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		error(sys->sprint("cannot load bufio module: %r"));
+	rcx = load RcxSend RcxSend->PATH;	#"rcxsend.dis";
+	if (rcx == nil)
+		error(sys->sprint("cannot load rcx module: %r"));
+
+	argv = tl argv;
+	if (len argv != 2)
+		error("usage: portnum file");
+
+	portnum := int hd argv;
+	file := hd tl argv;
+
+	img := getimage(file);
+	cksum := sum(img.data[0:img.length]);
+	sys->print("length %.4x start %.4x \n", img.length, img.start);
+
+	err := rcx->init(portnum, 1);
+	if (err != nil)
+		error(err);
+
+	# delete firmware
+	sys->print("delete firmware\n");
+	reply : array of byte;
+	rmfirm := array [] of {byte 16r65, byte 1, byte 3, byte 5, byte 7, byte 11};
+	reply = rcx->send(rmfirm, len rmfirm, 1);
+	if (reply == nil)
+		error("delete firmware failed");
+	chkreply(reply, array [] of {byte 16r92}, "delete firmware");
+
+	# start download
+	sys->print("start download\n");
+	dlstart := array [] of {byte 16r75,
+					byte (img.start & 16rff),
+					byte ((img.start>>8) & 16rff),
+					byte (cksum & 16rff),
+					byte ((cksum>>8) & 16rff),
+					byte 0,
+	};
+	reply = rcx->send(dlstart, len dlstart, 2);
+	chkreply(reply,array [] of {byte 16r82, byte 0}, "start download");
+
+	# send the image
+	data := array [DL_HDR+DL_DATA+1] of byte;	# hdr + data + 1 byte cksum
+	seqnum := 1;
+	step := DL_DATA;
+	for (i := 0; i < img.length; i += step) {
+		data[0] = byte 16r45;
+		if (seqnum & 1)
+			# alternate ops have bit 4 set
+			data[0] |= byte 16r08;
+		if (i + step > img.length) {
+			step = img.length - i;
+			seqnum = 0;
+		}
+		sys->print(".");
+		data[1] = byte (seqnum & 16rff);
+		data[2] = byte ((seqnum >> 8) & 16rff);
+		data[3] = byte (step & 16rff);
+		data[4] = byte ((step >> 8) & 16rff);
+		data[5:] = img.data[i:i+step];
+		data[5+step] = byte (sum(img.data[i:i+step]) & 16rff);
+		reply = rcx->send(data, DL_HDR+step+1, 2);
+		chkreply(reply, array [] of {byte 16rb2, byte 0}, "tx data");
+		seqnum++;
+	}
+
+	# unlock firmware
+	sys->print("\nunlock firmware\n");
+	ulfirm := array [] of {byte 16ra5, byte 'L', byte 'E', byte 'G', byte 'O', byte 174};
+	reply = rcx->send(ulfirm, len ulfirm, 26);
+	chkreply(reply, array [] of {byte 16r52}, "unlock firmware");
+	sys->print("result: %s\n", string reply[1:]);
+
+	# all done, tidy up
+	killgrp(me);
+}
+
+chkreply(got, expect : array of byte, err : string)
+{
+	if (got == nil || len got < len expect)
+		error(err + ": short reply");
+	# RCX sometimes sets bit 3 of 'opcode' byte to prevent
+	# headers with same opcode having exactly same value - mask out
+	got[0] &= byte 16rf7;
+
+	for (i := 0; i < len expect; i++)
+		if (got[i] != expect[i]) {
+			hexdump(got);
+			error(sys->sprint("%s: reply mismatch at %d", err, i));
+		}
+}
+	
+error(msg : string)
+{
+	sys->print("%s\n", msg);
+	killgrp(me);
+}
+
+killgrp(pid : int)
+{
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil) {
+		poison := array of byte "killgrp";
+		sys->write(pctl, poison, len poison);
+	}
+	exit;
+}
+
+sum(data : array of byte) : int
+{
+	t := 0;
+	for (i := 0; i < len data; i++)
+		t += int data[i];
+	return t;
+}
+
+hexdump(data : array of byte)
+{
+	for (i := 0; i < len data; i++)
+		sys->print("%.2x ", int data[i]);
+	sys->print("\n");
+}
+
+IMGSTART : con 16r8000;
+IMGLEN : con 16r4c00;
+getimage(path : string) : ref Image
+{
+	img := ref Image (IMGSTART, IMGSTART, 0, array [IMGLEN] of {* => byte 0});
+	iob := bufio->open(path, Sys->OREAD);
+	if (iob == nil)
+		error(sys->sprint("cannot open %s: %r", path));
+
+	lnum := 0;
+	while ((s := iob.gets('\n')) != nil) {
+		lnum++;
+		slen := len s;
+		# trim trailing space
+		while (slen > 0) {
+			ch := s[slen -1];
+			if (ch == ' ' || ch == '\r' || ch == '\n') {
+				slen--;
+				continue;
+			}
+			break;
+		}
+		# ignore blank lines
+		if (slen == 0)
+			continue;
+
+		if (slen < 10)
+			# STNNAAAACC
+			error("short S-record: line " + string lnum);
+
+		s = s[0:slen];
+		t := s[1];
+		if (s[0] != 'S' || t < '0' || t > '9')
+			error("bad S-record format: line " + string lnum);
+
+		data := hex2bytes(s[2:]);
+		if (data == nil)
+			error("bad chars in S-record:  line " + string lnum);
+
+		count := int data[0];
+		cksum := int data[len data - 1];
+		if (count != len data -1)
+			error("S-record length mis-match:  line " + string lnum);
+
+		if (sum(data[0:len data -1]) & 16rff != 16rff)
+			error("bad S-record checksum:  line " + string lnum);
+
+		alen : int;
+		case t {
+		'0' =>
+			# addr[2] mname[10] ver rev desc[18] cksum
+			continue;
+		'1' =>
+			# 16-bit address, data
+			alen = 2;
+		'2' =>
+			# 24-bit address, data
+			alen = 3;
+		'3' =>
+			# 32-bit address, data
+			alen = 4;
+		'4' =>
+			# extension record
+			error("bad S-record type: line " + string lnum);
+		'5' =>
+			# data record count - ignore
+			continue;
+		'6' =>
+			# unused - ignore
+			continue;
+		'7' =>
+			img.start = wordval(data, 1, 4);
+			continue;
+		'8' =>
+			img.start = wordval(data, 1, 3);
+			continue;
+		'9' =>
+			img.start = wordval(data, 1, 2);
+			continue;
+		}
+		addr := wordval(data, 1, alen) - img.offset;
+		if (addr < 0 || addr > len img.data)
+			error("S-record address out of range: line " + string lnum);
+		img.data[addr:] = data[1+alen:1+count];
+		img.length = max(img.length, addr + count -(alen +1));
+	}
+	iob.close();
+	return img;
+}
+
+wordval(src : array of byte, s, l : int) : int
+{
+	r := 0;
+	for (i := 0; i < l; i++) {
+		r <<= 8;
+		r += int src[s+i];
+	}
+	return r;
+}
+
+max(a, b : int) : int
+{
+	if (a > b)
+		return a;
+	return b;
+}
+
+hex2bytes(s : string) : array of byte
+{
+	slen := len s;
+	if (slen & 1)
+		# should be even
+		return nil;
+	data := array [slen/2] of byte;
+	six := 0;
+	dix := 0;
+	while (six < slen) {
+		d1 := hexdigit(s[six++]);
+		d2 := hexdigit(s[six++]);
+		if (d1 == -1 || d2 == -1)
+			return nil;
+		data[dix++] = byte ((d1 << 4) + d2);
+	}
+	return data;
+}
+
+hexdigit(h : int) : int
+{
+	if (h >= '0' && h <= '9')
+		return h - '0';
+	if (h >= 'A' && h <= 'F')
+		return 10 + h - 'A';
+	if (h >= 'a' && h <= 'f')
+		return 10 + h - 'a';
+	return -1;
+}
--- /dev/null
+++ b/appl/cmd/lego/link.b
@@ -1,0 +1,603 @@
+implement LegoLink;
+
+include "sys.m";
+include "draw.m";
+include "timers.m";
+include "rcxsend.m";
+
+LegoLink : module {
+	init : fn (ctxt : ref Draw->Context, argv : list of string);
+};
+
+POLLDONT : con 0;
+POLLNOW : con 16r02;
+POLLDO : con 16r04;
+
+sys : Sys;
+timers : Timers;
+Timer : import timers;
+datain : chan of array of byte;
+errormsg : string;
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	argv = tl argv;
+	if (len argv != 1) {
+		sys->print("usage: lego/link portnum\n");
+		return;
+	}
+
+	timers = load Timers Timers->PATH; 	#"timers.dis";
+	if (timers == nil) {
+		sys->print("cannot load timers module: %r\n");
+		return;
+	}
+	portnum := int hd argv;
+	(rdfd, wrfd, err) := serialport(portnum);
+	if (err != nil) {
+		sys->print("%s\n", err);
+		return;
+	}
+
+	# set up our mount file
+	if (sys->bind("#s", "/net", Sys->MBEFORE) == -1) {
+		sys->print("failed to bind srv device: %r\n");
+		return;
+	}
+	f2c := sys->file2chan("/net", "legolink");
+	if (f2c == nil) {
+		sys->print("cannot create legolink channel: %r\n");
+		return;
+	}
+
+	datain = chan of array of byte;
+	send := chan of array of byte;
+	recv := chan of array of byte;
+	timers->init(50);
+	spawn reader(rdfd, datain);
+	consume();
+	spawn protocol(wrfd, send, recv);
+	spawn srvlink(f2c, send, recv);
+}
+
+srvlink(f2c : ref Sys->FileIO, send, recv : chan of array of byte)
+{
+	me := sys->pctl(0, nil);
+	rdfid := -1;
+	wrfid := -1;
+	buffer := array [256] of byte;
+	bix := 0;
+
+	rdblk := chan of (int, int, int, Sys->Rread);
+	readreq := rdblk;
+	wrblk := chan of (int, array of byte, int, Sys->Rwrite);
+	writereq := f2c.write;
+	wrreply : Sys->Rwrite;
+	sendblk := chan of array of byte;
+	sendchan := sendblk;
+	senddata : array of byte;
+
+	for (;;) alt {
+	data := <- recv =>
+		# got some data from brick, nil for error
+		if (data == nil) {
+			# some sort of error
+			if (wrreply != nil) {
+				wrreply <- = (0, errormsg);
+			}
+			killgrp(me);
+		}
+		if (bix + len data > len buffer) {
+			newb := array [bix + len data + 256] of byte;
+			newb[0:] = buffer;
+			buffer = newb;
+		}
+		buffer[bix:] = data;
+		bix += len data;
+		readreq = f2c.read;
+
+	(offset, count, fid, rc) := <- readreq =>
+		if (rdfid == -1)
+			rdfid = fid;
+		if (fid != rdfid) {
+			if (rc != nil)
+				rc <- = (nil, "file in use");
+			continue;
+		}
+		if (rc == nil) {
+			rdfid = -1;
+			continue;
+		}
+		if (errormsg != nil) {
+			rc <- = (nil, errormsg);
+			killgrp(me);
+		}
+		# reply with what we've got
+		if (count > bix)
+			count = bix;
+		rdata := array [count] of byte;
+		rdata[0:] = buffer[0:count];
+		buffer[0:] = buffer[count:bix];
+		bix -= count;
+		if (bix == 0)
+			readreq = rdblk;
+		alt {
+		rc <- = (rdata, nil)=>
+			;
+		* =>
+			;
+		}
+
+	(offset, data, fid, wc) := <- writereq =>
+		if (wrfid == -1)
+			wrfid = fid;
+		if (fid != wrfid) {
+			if (wc != nil)
+				wc <- = (0, "file in use");
+			continue;
+		}
+		if (wc == nil) {
+			wrfid = -1;
+			continue;
+		}
+		if (errormsg != nil) {
+			wc <- = (0, errormsg);
+			killgrp(me);
+		}
+		senddata = data;
+		sendchan = send;
+		wrreply = wc;
+		writereq = wrblk;
+
+	sendchan <- = senddata =>
+		alt {
+		wrreply <- = (len senddata, nil) =>
+			;
+		* =>
+			;
+		}
+		wrreply = nil;
+		sendchan = sendblk;
+		writereq = f2c.write;
+	}
+}
+
+killgrp(pid : int)
+{
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil) {
+		poison := array of byte "killgrp";
+		sys->write(pctl, poison, len poison);
+	}
+	exit;
+}
+
+serialport(port : int) : (ref Sys->FD, ref Sys->FD, string)
+{
+	serport := "/dev/eia" + string port;
+	serctl := serport + "ctl";
+
+	rfd := sys->open(serport, Sys->OREAD);
+	if (rfd == nil)
+		return (nil, nil, sys->sprint("cannot read %s: %r", serport));
+	wfd := sys->open(serport, Sys->OWRITE);
+	if (wfd == nil)
+		return (nil, nil, sys->sprint("cannot write %s: %r", serport));
+	ctlfd := sys->open(serctl, Sys->OWRITE);
+	if (ctlfd == nil)
+		return (nil, nil, sys->sprint("cannot open %s: %r", serctl));
+
+	config := array [] of {
+		"b2400",
+		"l8",
+		"po",
+		"m0",
+		"s1",
+		"d1",
+		"r1",
+	};
+
+	for (i := 0; i < len config; i++) {
+		cmd := array of byte config[i];
+		if (sys->write(ctlfd, cmd, len cmd) <= 0)
+			return (nil, nil, sys->sprint("serial config (%s): %r", config[i]));
+	}
+	return (rfd, wfd, nil);
+}
+
+# reader and nbread as in rcxsend.b
+reader(fd : ref Sys->FD, out : chan of array of byte)
+{
+	# with buf size of 1 there is no need
+	# for overrun code in nbread()
+
+	buf := array [1] of byte;
+	for (;;) {
+		n := sys->read(fd, buf, len buf);
+		if (n <= 0)
+			break;
+		data := array [n] of byte;
+		data[0:] = buf[0:n];
+		out <- = data;
+	}
+	out <- = nil;
+}
+
+overrun : array of byte;
+
+nbread(ms, n : int) : array of byte
+{
+	ret := array[n] of byte;
+	tot := 0;
+	if (overrun != nil) {
+		if (n < len overrun) {
+			ret[0:] = overrun[0:n];
+			overrun = overrun[n:];
+			return ret;
+		}
+		ret[0:] = overrun;
+		tot += len overrun;
+		overrun = nil;
+	}
+	tmr := timers->new(ms, 0);
+loop:
+	while (tot < n) {
+		tmr.reset();
+		alt {
+			data := <- datain =>
+				if (data == nil)
+					break loop;
+				dlen := len data;
+				if (dlen > n - tot) {
+					dlen = n - tot;
+					overrun = data[dlen:];
+				}
+				ret[tot:] = data[0:dlen];
+				tot += dlen;
+			<- tmr.tick =>
+				# reply timeout;
+				break loop;
+		}
+	}
+	tmr.destroy();
+	if (tot == 0)
+		return nil;
+	return ret[0:tot];
+}
+
+consume()
+{
+	while (nbread(300, 1024) != nil)
+		;
+}
+
+# fd: connection to remote client
+# send: from local to remote
+# recv: from remote to local
+protocol(fd : ref Sys->FD, send, recv : chan of array of byte)
+{
+	seqnum := 0;
+	towerdown := timers->new(1500, 0);
+	starttower := 1;
+	tmr := timers->new(250, 0);
+
+	for (;;) {
+		data : array of byte = nil;
+		# get data to send
+		alt {
+		data = <- send =>
+			;
+		<- tmr.tick =>
+			data = nil;
+		<- towerdown.tick =>
+			starttower = 1;
+			continue;
+		}
+			
+		poll := POLLNOW;
+		while (poll == POLLNOW) {
+			reply : array of byte;
+			(reply, poll, errormsg) = datasend(fd, seqnum++, data, starttower);
+			starttower = 0;
+			towerdown.reset();
+			if (errormsg != nil) {
+sys->print("protocol: send error: %s\n", errormsg);
+				tmr.destroy();
+				recv <- = nil;
+				return;
+			}
+			if (reply != nil) {
+				recv <- = reply;
+			}
+			if (poll == POLLNOW) {
+				# quick check to see if we have any more data
+				alt {
+				data = <- send =>
+						;
+				* =>
+						data = nil;
+				}
+			}
+		}
+		if (poll == POLLDO)
+			tmr.reset();
+		else
+			tmr.cancel();
+	}
+}
+
+TX_HDR : con 3;
+DL_HDR : con 5;	# 16r45 seqLSB seqMSB lenLSB lenMSB
+DL_CKSM : con 1;
+LN_HDR : con 1;
+LN_JUNK : con 2;
+LN_LEN : con 2;
+LN_RXLEN : con 2;
+LN_POLLMASK : con 16r06;
+LN_COMPMASK : con 16r08;
+
+
+# send a message (may be empty)
+# wait for the reply
+# returns (data, poll request, error)
+
+datasend(wrfd : ref Sys->FD, seqnum : int, data : array of byte, startup : int) : (array of byte, int, string)
+{
+if (startup) {
+	dummy := array [] of { byte 255, byte 0, byte 255, byte 0};
+	sys->write(wrfd, dummy, len dummy);
+	nbread(100, 100);
+}
+	seqnum = seqnum & 1;
+	docomp := 0;
+	if (data != nil) {
+		comp := rlencode(data);
+		if (len comp < len data) {
+			docomp = 1;
+			data = comp;
+		}
+	}
+
+	# construct the link-level data packet
+	# DL_HDR LN_HDR data cksum
+	# last byte of data is stored in cksum byte
+	llen := LN_HDR + len data;
+	blklen := LN_LEN + llen - 1;	# llen includes cksum
+	ldata := array [DL_HDR + blklen + 1] of byte;
+
+	# DL_HDR
+	if (seqnum == 0)
+		ldata[0] = byte 16r45;
+	else
+		ldata[0] = byte 16r4d;
+	ldata[1] = byte 0;				# blk number LSB
+	ldata[2] = byte 0;				# blk number MSB
+	ldata[3] = byte (blklen & 16rff);		# blk length LSB
+	ldata[4] = byte ((blklen >> 8) & 16rff);	# blk length MSB
+
+	# LN_LEN
+	ldata[5] = byte (llen & 16rff);
+	ldata[6] = byte ((llen>>8) & 16rff);
+	# LN_HDR
+	lhval := byte 0;
+	if (seqnum == 1)
+		lhval |= byte 16r01;
+	if (docomp)
+		lhval |= byte 16r08;
+	
+	ldata[7] = lhval;
+
+	# data (+cksum)
+	ldata[8:] = data;
+
+	# construct the rcx data packet
+	# TX_HDR (dn ~dn) cksum ~cksum
+	rcxlen := TX_HDR + 2*(len ldata + 1);
+	rcxdata := array [rcxlen] of byte;
+
+	rcxdata[0] = byte 16r55;
+	rcxdata[1] = byte 16rff;
+	rcxdata[2] = byte 16r00;
+	rcix := TX_HDR;
+	cksum := 0;
+	for (i := 0; i < len ldata; i++) {
+		b := ldata[i];
+		rcxdata[rcix++] = b;
+		rcxdata[rcix++] = ~b;
+		cksum += int b;
+	}
+	rcxdata[rcix++] = byte (cksum & 16rff);
+	rcxdata[rcix++] = byte (~cksum & 16rff);
+
+	# send it
+	err : string;
+	reply : array of byte;
+	for (try := 0; try < 8; try++) {
+		if (err != nil)
+			sys->print("Try %d (lasterr %s)\n", try, err);
+		err = "";
+		step := 8;
+		for (i = 0; err == nil && i < rcxlen; i += step) {
+			if (i + step > rcxlen)
+				step = rcxlen -i;
+			if (sys->write(wrfd, rcxdata[i:i+step], step) != step) {
+				return (nil, 0, "hangup");
+			}
+
+			# get the echo
+			reply = nbread(300, step);
+			if (reply == nil || len reply != step)
+				# short echo
+				err = "tower not responding";
+
+			# check the echo
+			for (ei := 0; err == nil && ei < step; ei++) {
+				if (reply[ei] != rcxdata[i+ei])
+					# echo mis-match
+					err = "serial comms error";
+			}
+		}
+		if (err != nil) {
+			consume();
+			continue;
+		}
+
+		# wait for a reply
+		replen := TX_HDR + LN_JUNK + 2*LN_RXLEN;
+		reply = nbread(300, replen);
+		if (reply == nil || len reply != replen) {
+			err = "brick not responding";
+			consume();
+			continue;
+		}
+		if (reply[0] != byte 16r55 || reply[1] != byte 16rff || reply[2] != byte 0
+		|| reply[5] != ~reply[6] || reply[7] != ~reply[8]) {
+			err = "bad reply from brick";
+			consume();
+			continue;
+		}
+		# reply[3] and reply [4] are junk, ~junk
+		# put on front of msg by rcx rom
+		replen = int reply[5] + ((int reply[7]) << 8) + 1;
+		cksum = int reply[3] + int reply[5] + int reply[7];
+		reply = nbread(200, replen * 2);
+		if (reply == nil || len reply != replen * 2) {
+			err = "short reply from brick";
+			consume();
+			continue;
+		}
+		cksum += int reply[0];
+		for (i = 1; i < replen; i++) {
+			reply[i] = reply[2*i];
+			cksum += int reply[i];
+		}
+		cksum -= int reply[replen-1];
+		if (reply[replen-1] != byte (cksum & 16rff)) {
+			err = "bad checksum from brick";
+			consume();
+			continue;
+		}
+		if ((reply[0] & byte 1) != byte (seqnum & 1)) {
+			# seqnum error
+			# we have read everything, don't bother with consume()
+			err = "bad seqnum from brick";
+			continue;
+		}
+
+		# TADA! we have a valid message
+		mdata : array of byte;
+		lnhdr := int reply[0];
+		poll := lnhdr & LN_POLLMASK;
+		if (replen > 2) {
+			# more than just hdr and cksum
+			if (lnhdr & LN_COMPMASK) {
+				mdata = rldecode(reply[1:replen-1]);
+				if (mdata == nil) {
+					err = "bad brick msg compression";
+					continue;
+				}
+			} else {
+				mdata = array [replen - 2] of byte;
+				mdata[0:] = reply[1:replen-1];
+			}
+		}
+		return (mdata, poll, nil);
+	}
+	return (nil, 0, err);
+}
+
+
+rlencode(data : array of byte) : array of byte
+{
+	srcix := 0;
+	outix := 0;
+	out := array [64] of byte;
+	val := 0;
+	nextval := -1;
+	n0 := 0;
+
+	while (srcix < len data || nextval != -1) {
+		if (nextval != -1) {
+			val = nextval;
+			nextval = -1;
+		} else {
+			val = int data[srcix];
+			if (val == 16r88)
+				nextval = 0;
+			if (val == 0) {
+				n0++;
+				srcix++;
+				if (srcix < len data && n0 < 16rff + 2)
+					continue;
+			}
+			case n0 {
+			0 =>
+				srcix++;
+			1 =>
+				val = 0;
+				nextval = -1;
+				n0 = 0;
+			2 =>
+				val = 0;
+				nextval = 0;
+				n0 = 0;
+			* =>
+				val = 16r88;
+				nextval = (n0-2);
+				n0 = 0;
+			}
+		}
+		if (outix >= len out) {
+			newout := array [2 * len out] of byte;
+			newout[0:] = out;
+			out = newout;
+		}
+		out[outix++] = byte val;
+	}
+	return out[0:outix];
+}
+
+rldecode(data : array of byte) : array of byte
+{
+	srcix := 0;
+	outix := 0;
+	out := array [64] of byte;
+
+	n0 := 0;
+	val := 0;
+	while (srcix < len data || n0 > 0) {
+		if (n0 > 0)
+			n0--;
+		else {
+			val = int data[srcix++];
+			if (val == 16r88) {
+				if (srcix >= len data)
+					# bad encoding
+					return nil;
+				n0 = int data[srcix++];
+				if (n0 > 0) {
+					n0 += 2;
+					val = 0;
+					continue;
+				}
+			}
+		}
+		if (outix >= len out) {
+			newout := array [2 * len out] of byte;
+			newout[0:] = out;
+			out = newout;
+		}
+		out[outix++] = byte val;
+	}
+	return out[0:outix];
+}
+
+hexdump(data : array of byte)
+{
+	for (i := 0; i < len data; i++)
+		sys->print("%.2x ", int data[i]);
+	sys->print("\n");
+}
--- /dev/null
+++ b/appl/cmd/lego/mkfile
@@ -1,0 +1,23 @@
+<../../../mkconfig
+
+TARG=\
+	clock.dis\
+	clockface.dis\
+	firmdl.dis\
+	link.dis\
+	rcxsend.dis\
+	send.dis\
+	timers.dis\
+
+SYSMODULES=\
+	sys.m\
+	draw.m\
+	bufio.m\
+
+MODULES=\
+	rcxsend.m\
+	timers.m\
+
+DISBIN=$ROOT/dis/lego
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/lego/rcxsend.b
@@ -1,0 +1,240 @@
+implement RcxSend;
+
+include "sys.m";
+include "timers.m";
+include "rcxsend.m";
+
+sys : Sys;
+timers : Timers;
+Timer : import timers;
+datain : chan of array of byte;
+debug : int;
+rpid : int;
+wrfd : ref Sys->FD;
+
+TX_HDR : con 3;
+TX_CKSM : con 2;
+
+init(portnum, dbg : int) : string
+{
+	debug = dbg;
+	sys = load Sys Sys->PATH;
+	timers = load Timers Timers->PATH; 	#"timers.dis";
+	if (timers == nil)
+		 return sys->sprint("cannot load timer module: %r");
+
+	rdfd : ref Sys->FD;
+	err : string;
+	(rdfd, wrfd, err) = serialport(portnum);
+	if (err != nil)
+		return err;
+
+	timers->init(50);
+	pidc := chan of int;
+	datain = chan of array of byte;
+	spawn reader(pidc, rdfd, datain);
+	rpid = <- pidc;
+	consume();
+	return nil;
+}
+
+reader(pidc : chan of int, fd : ref Sys->FD, out : chan of array of byte)
+{
+	pidc <- = sys->pctl(0, nil);
+
+	# with buf size of 1 there is no need
+	# for overrun code in nbread()
+
+	buf := array [1] of byte;
+	for (;;) {
+		n := sys->read(fd, buf, len buf);
+		if (n <= 0)
+			break;
+		data := array [n] of byte;
+		data[0:] = buf[0:n];
+		out <- = data;
+	}
+	if (debug)
+		sys->print("Reader error\n");
+}
+
+send(data : array of byte, n, rlen: int) : array of byte
+{
+	# 16r55 16rff 16r00 (d[i] ~d[i])*n cksum ~cksum
+	obuf := array [TX_HDR + (2*n ) + TX_CKSM] of byte;
+	olen := 0;
+	obuf[olen++] = byte 16r55;
+	obuf[olen++] = byte 16rff;
+	obuf[olen++] = byte 16r00;
+	cksum := 0;
+	for (i := 0; i < n; i++) {
+		obuf[olen++] = data[i];
+		obuf[olen++] = ~data[i];
+		cksum += int data[i];
+	}
+	obuf[olen++] = byte (cksum & 16rff);
+	obuf[olen++] = byte (~cksum & 16rff);
+
+	needr := rlen;
+	if (rlen > 0)
+		needr = TX_HDR + (2 * rlen) + TX_CKSM;
+	for (try := 0; try < 5; try++) {
+		ok := 1;
+		err := "";
+		reply : array of byte;
+
+		step := 8;
+		for (i = 0; ok && i < olen; i += step) {
+			if (i + step > olen)
+				step = olen -i;
+			if (sys->write(wrfd, obuf[i:i+step], step) != step) {
+				if (debug)
+					sys->print("serial tx error: %r\n");
+				return nil;
+			}
+
+			# get the echo
+			reply = nbread(200, step);
+			if (reply == nil || len reply != step) {
+				err = "short echo";
+				ok = 0;
+			}
+
+			# check the echo
+			for (ei := 0; ok && ei < step; ei++) {
+				if (reply[ei] != obuf[i+ei]) {
+					err = "bad echo";
+					ok = 0;
+				}
+			}
+		}
+
+		# get the reply
+		if (ok) {
+			if (needr == 0)
+				return nil;
+			if (needr == -1) {
+				# just get what we can
+				needr = TX_HDR + TX_CKSM;
+				reply = nbread(300, 1024);
+			} else {
+				reply = nbread(200, needr);
+			}
+			if (len reply < needr) {
+				err = "short reply";
+				ok = 0;
+			}
+		}
+		# check the reply
+		if (ok && reply[0] == byte 16r55 && reply[1] == byte 16rff && reply[2] == byte 0) {
+			cksum := int reply[len reply -TX_CKSM];
+			val := reply[TX_HDR:len reply -TX_CKSM];
+			r := array [len val / 2] of byte;
+			sum := 0;
+			for (i = 0; i < len r; i++) {
+				r[i] = val[i*2];
+				sum += int r[i];
+			}
+			if (cksum == (sum & 16rff)) {
+				return r;
+			}
+			ok = 0;
+			err = "bad cksum";
+		} else if (ok) {
+			ok = 0;
+			err = "reply header error";
+		}
+		if (debug && ok == 0 && err != nil) {
+			sys->print("try %d %s: ", try, err);
+			hexdump(reply);
+		}
+		consume();
+	}
+	return nil;
+}
+
+overrun : array of byte;
+
+nbread(ms, n : int) : array of byte
+{
+	ret := array[n] of byte;
+	tot := 0;
+	if (overrun != nil) {
+		if (n < len overrun) {
+			ret[0:] = overrun[0:n];
+			overrun = overrun[n:];
+			return ret;
+		}
+		ret[0:] = overrun;
+		tot += len overrun;
+		overrun = nil;
+	}
+	tmr := timers->new(ms, 0);
+loop:
+	while (tot < n) {
+		tmr.reset();
+		alt {
+			data := <- datain =>
+				dlen := len data;
+				if (dlen > n - tot) {
+					dlen = n - tot;
+					overrun = data[dlen:];
+				}
+				ret[tot:] = data[0:dlen];
+				tot += dlen;
+			<- tmr.tick =>
+				# reply timeout;
+				break loop;
+		}
+	}
+	tmr.destroy();
+	if (tot == 0)
+		return nil;
+	return ret[0:tot];
+}
+
+consume()
+{
+	while (nbread(300, 1024) != nil)
+		;
+}
+
+serialport(port : int) : (ref Sys->FD, ref Sys->FD, string)
+{
+	serport := "/dev/eia" + string port;
+	serctl := serport + "ctl";
+
+	rfd := sys->open(serport, Sys->OREAD);
+	if (rfd == nil)
+		return (nil, nil, sys->sprint("cannot read %s: %r", serport));
+	wfd := sys->open(serport, Sys->OWRITE);
+	if (wfd == nil)
+		return (nil, nil, sys->sprint("cannot write %s: %r", serport));
+	ctlfd := sys->open(serctl, Sys->OWRITE);
+	if (ctlfd == nil)
+		return (nil, nil, sys->sprint("cannot open %s: %r", serctl));
+
+	config := array [] of {
+		"b2400",
+		"l8",
+		"po",
+		"m0",
+		"s1",
+		"d1",
+		"r1",
+	};
+
+	for (i := 0; i < len config; i++) {
+		cmd := array of byte config[i];
+		if (sys->write(ctlfd, cmd, len cmd) <= 0)
+			return (nil, nil, sys->sprint("serial config (%s): %r", config[i]));
+	}
+	return (rfd, wfd, nil);
+}
+hexdump(data : array of byte)
+{
+	for (i := 0; i < len data; i++)
+		sys->print("%.2x ", int data[i]);
+	sys->print("\n");
+}
+
--- /dev/null
+++ b/appl/cmd/lego/rcxsend.m
@@ -1,0 +1,6 @@
+RcxSend : module {
+	PATH: con "/dis/lego/rcxsend.dis";
+
+	init: fn (pnum, dbg : int) : string;
+	send : fn (data : array of byte, slen, rlen : int) : array of byte;
+};
--- /dev/null
+++ b/appl/cmd/lego/send.b
@@ -1,0 +1,86 @@
+implement Send;
+
+include "sys.m";
+include "draw.m";
+include "rcxsend.m";
+
+Send : module {
+	init : fn (ctxt : ref Draw->Context, argv : list of string);
+};
+
+sys : Sys;
+rcx : RcxSend;
+me : int;
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	me = sys->pctl(Sys->NEWPGRP, nil);
+
+	rcx = load RcxSend "rcxsend.dis";
+	if (rcx == nil)
+		error(sys->sprint("cannot load rcx module: %r"));
+
+	argv = tl argv;
+	if (len argv < 2)
+		error("usage: send portnum XX...");
+
+	portnum := int hd argv;
+	argv = tl argv;
+
+	cmd := array [len argv] of byte;
+	for (i := 0; i < len cmd; i++) {
+		arg := hd argv;
+		argv = tl argv;
+		if (arg == nil || len arg > 2)
+			error(sys->sprint("bad arg %s\n", arg));
+		d1, d2 : int = 0;
+		d2 = hexdigit(arg[0]);
+		if (len arg == 2) {
+			d1 = d2;
+			d2 = hexdigit(arg[1]);
+		}
+		if (d1 == -1 || d2 == -1)
+			error(sys->sprint("bad arg %s\n", arg));
+		cmd[i] = byte ((d1 << 4) + d2);
+	}
+
+	rcx->init(portnum, 1);
+	reply := rcx->send(cmd, len cmd, -1);
+	hexdump(reply);
+	killgrp(me);
+}
+
+hexdigit(h : int) : int
+{
+	if (h >= '0' && h <= '9')
+		return h - '0';
+	if (h >= 'A' && h <= 'F')
+		return 10 + h - 'A';
+	if (h >= 'a' && h <= 'f')
+		return 10 + h - 'a';
+	return -1;
+}
+		
+error(msg : string)
+{
+	sys->print("%s\n", msg);
+	killgrp(me);
+}
+
+killgrp(pid : int)
+{
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil) {
+		poison := array of byte "killgrp";
+		sys->write(pctl, poison, len poison);
+	}
+	exit;
+}
+
+hexdump(data : array of byte)
+{
+	for (i := 0; i < len data; i++)
+		sys->print("%.2x ", int data[i]);
+	sys->print("\n");
+}
--- /dev/null
+++ b/appl/cmd/lego/timers.b
@@ -1,0 +1,263 @@
+# Chris Locke. June 2000
+
+# TODO: for auto-repeat timers don't set up a new sender
+# if there is already a pending sender for that timer.
+
+implement Timers;
+
+include "sys.m";
+include "timers.m";
+
+RealTimer : adt {
+	t : ref Timer;
+	nticks : int;
+	rep : int;
+	nexttick: big;
+	tick : chan of int;
+	sender : int;
+};
+
+Sender : adt {
+	tid : int;
+	idle : int;		# set by sender() when done, reset by main when about to assign work
+	ctl : chan of chan of int;
+};
+
+sys : Sys;
+acquire : chan of int;
+timers := array [4] of ref RealTimer;
+senders := array [4] of ref Sender;
+curtick := big 0;
+tickres : int;
+
+init(res : int)
+{
+	sys = load Sys Sys->PATH;
+	acquire = chan of int;
+	tickres = res;
+	spawn main();
+}
+
+new(ms, rep : int) : ref Timer
+{
+	acquire <- = 1;
+	t := do_new(ms, rep);
+	<- acquire;
+	return t;
+}
+
+Timer.destroy(t : self ref Timer)
+{
+	acquire <- = 1;
+	do_destroy(t);
+	<- acquire;
+}
+
+Timer.reset(t : self ref Timer)
+{
+	acquire <- = 1;
+	do_reset(t);
+	<- acquire;
+}
+
+Timer.cancel(t : self ref Timer)
+{
+	acquire <- = 1;
+	do_cancel(t);
+	<- acquire;
+}
+
+# only call under lock
+#
+realtimer(t : ref Timer) : ref RealTimer
+{
+	if (t.id < 0 || t.id >= len timers)
+		return nil;
+	if (timers[t.id] == nil)
+		return nil;
+	if (timers[t.id].t != t)
+		return nil;
+	return timers[t.id];
+}
+
+
+# called under lock
+#
+do_destroy(t : ref Timer)
+{
+	rt := realtimer(t);
+	if (rt == nil)
+		return;
+	clearsender(rt, t.id);
+	timers[t.id] = nil;
+}
+
+# called under lock
+#
+do_reset(t : ref Timer)
+{
+	rt := realtimer(t);
+	if (rt == nil)
+		return;
+	clearsender(rt, t.id);
+	rt.nexttick = curtick + big (rt.nticks);
+	startclk = 1;
+}
+
+# called under lock
+#
+do_cancel(t : ref Timer)
+{
+	rt := realtimer(t);
+	if (rt == nil)
+		return;
+	clearsender(rt, t.id);
+	rt.nexttick = big 0;
+}
+
+# only call under lock
+#
+clearsender(rt : ref RealTimer, tid : int)
+{
+	# check to see if there is a sender trying to deliver tick
+	if (rt.sender != -1) {
+		sender := senders[rt.sender];
+		rt.sender = -1;
+		if (sender.tid == tid && !sender.idle) {
+			# receive the tick to clear the busy state
+			alt {
+				<- rt.tick =>
+					;
+				* =>
+					;
+			}
+		}
+	}
+}
+
+# called under lock
+do_new(ms, rep : int) : ref Timer
+{
+	# find free slot
+	for (i := 0; i < len timers; i++)
+		if (timers[i] == nil)
+			break;
+	if (i == len timers) {
+		# grow the array
+		newtimers := array [len timers * 2] of ref RealTimer;
+		newtimers[0:] = timers;
+		timers = newtimers;
+	}
+	tick := chan of int;
+	t := ref Timer(i, tick);
+	nticks := ms / tickres;
+	if (nticks == 0)
+		nticks = 1;
+	rt := ref RealTimer(t, nticks, rep, big 0, tick, -1);
+	timers[i] = rt;
+	return t;
+}
+
+startclk : int;
+stopclk : int;
+
+main()
+{
+	clktick := chan of int;
+	clkctl := chan of int;
+	clkstopped := 1;
+	spawn ticker(tickres, clkctl, clktick);
+
+	for (;;) alt {
+	<- acquire =>
+		# Locking
+		acquire <- = 1;
+
+		if (clkstopped && startclk) {
+			clkstopped = 0;
+			startclk = 0;
+			clkctl <- = 1;
+		}
+
+	t := <- clktick =>
+		if (t == 0) {
+			stopclk = 0;
+			if (startclk) {
+				startclk = 0;
+				clkctl <- = 1;
+			} else {
+				clkstopped = 1;
+				continue;
+			}
+		}
+		curtick++;
+		npend := 0;
+		for (i := 0; i < len timers; i++) {
+			rt := timers[i];
+			if (rt == nil)
+				continue;
+			if (rt.nexttick == big 0)
+				continue;
+			if (rt.nexttick > curtick) {
+				npend++;
+				continue;
+			}
+			# Timeout - arrange to send the tick
+			if (rt.rep) {
+				rt.nexttick = curtick + big rt.nticks;
+				npend++;
+			} else
+				rt.nexttick = big 0;
+			si := getsender();
+			s := senders[si];
+			s.tid = i;
+			s.idle = 0;
+			rt.sender = si;
+			s.ctl <- = rt.tick;
+
+		}
+		if (!npend)
+			stopclk = 1;
+	}
+}
+
+getsender() : int
+{
+	for (i := 0; i < len senders; i++) {
+		s := senders[i];
+		if (s == nil || s.idle == 1)
+			break;
+	}
+	if (i == len senders) {
+		newsenders := array [len senders * 2] of ref Sender;
+		newsenders[0:] = senders;
+		senders = newsenders;
+	}
+	if (senders[i] == nil) {
+		s := ref Sender (-1, 1, chan of chan of int);
+		spawn sender(s);
+		senders[i] = s;
+	}
+	return i;
+}
+
+sender(me : ref Sender)
+{
+	for (;;) {
+		tickch := <- me.ctl;
+		tickch <- = 1;
+		me.idle = 1;
+	}
+}
+
+ticker(ms : int, start, tick : chan of int)
+{
+	for (;;) {
+		<- start;
+		while (!stopclk) {
+			sys->sleep(ms);
+			tick <- = 1;
+		}
+		tick <- = 0;
+	}
+}
--- /dev/null
+++ b/appl/cmd/lego/timers.m
@@ -1,0 +1,17 @@
+Timers : module{
+	PATH: con "/dis/lego/timers.dis";
+
+	Timer : adt {
+		id : int;
+		tick : chan of int;
+
+		reset : fn (t : self ref Timer);
+		cancel : fn (t : self ref Timer);
+		destroy : fn (t : self ref Timer);
+	};
+
+	init : fn (res : int);
+	new : fn(ms, rep : int) : ref Timer;
+};
+
+
--- /dev/null
+++ b/appl/cmd/limbo/arg.m
@@ -1,0 +1,50 @@
+Arg: adt
+{
+	argv:	list of string;
+	c:	int;
+	opts:	string;
+
+	init:	fn(argv: list of string): ref Arg;
+	opt:	fn(arg: self ref Arg): int;
+	arg:	fn(arg: self ref Arg): string;
+};
+
+Arg.init(argv: list of string): ref Arg
+{
+	if(argv != nil)
+		argv = tl argv;
+	return ref Arg(argv, 0, nil);
+}
+
+Arg.opt(arg: self ref Arg): int
+{
+	if(arg.opts != ""){
+		arg.c = arg.opts[0];
+		arg.opts = arg.opts[1:];
+		return arg.c;
+	}
+	if(arg.argv == nil)
+		return arg.c = 0;
+	arg.opts = hd arg.argv;
+	if(len arg.opts < 2 || arg.opts[0] != '-')
+		return arg.c = 0;
+	arg.argv = tl arg.argv;
+	if(arg.opts == "--")
+		return arg.c = 0;
+	arg.c = arg.opts[1];
+	arg.opts = arg.opts[2:];
+	return arg.c;
+}
+
+Arg.arg(arg: self ref Arg): string
+{
+	s := arg.opts;
+	arg.opts = "";
+	if(s != "")
+		return s;
+	if(arg.argv == nil)
+		return "";
+	s = hd arg.argv;
+	arg.argv = tl arg.argv;
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/limbo/asm.b
@@ -1,0 +1,263 @@
+asmentry(e: ref Decl)
+{
+	if(e == nil)
+		return;
+	bout.puts("\tentry\t"+string e.pc.pc+", "+string e.desc.id+"\n");
+}
+
+asmmod(m: ref Decl)
+{
+	bout.puts("\tmodule\t");
+	bout.puts(m.sym.name);
+	bout.putc('\n');
+	for(m = m.ty.tof.ids; m != nil; m = m.next){
+		case m.store{
+		Dglobal =>
+			bout.puts("\tlink\t-1,-1,0x"+hex(sign(m), 0)+",\".mp\"\n");
+		Dfn =>
+			bout.puts("\tlink\t"+string m.desc.id+","+string m.pc.pc+",0x"+string hex(sign(m), 0)+",\"");
+			if(m.dot.ty.kind == Tadt)
+				bout.puts(m.dot.sym.name+".");
+			bout.puts(m.sym.name+"\"\n");
+		}
+	}
+}
+
+asmpath()
+{
+	bout.puts("\tsource\t\"" + srcpath() + "\"\n");
+}
+
+asmdesc(d: ref Desc)
+{
+	for(; d != nil; d = d.next){
+		bout.puts("\tdesc\t$"+string d.id+","+string d.size+",\"");
+		e := d.nmap;
+		m := d.map;
+		for(i := 0; i < e; i++)
+			bout.puts(hex(int m[i], 2));
+		bout.puts("\"\n");
+	}
+}
+
+asmvar(size: int, d: ref Decl)
+{
+	bout.puts("\tvar\t@mp," + string size + "\n");
+
+	for(; d != nil; d = d.next)
+		if(d.store == Dglobal && d.init != nil)
+			asminitializer(d.offset, d.init);
+}
+
+asmldt(size: int, d: ref Decl)
+{
+	bout.puts("\tldts\t@ldt," + string size + "\n");
+	
+	for(; d != nil; d = d.next)
+		if(d.store == Dglobal && d.init != nil)
+			asminitializer(d.offset, d.init);
+}
+
+asminitializer(offset: int, n: ref Node)
+{
+	wild: ref Node;
+	c: ref Case;
+	lab: Label;
+	id: ref Decl;
+	i, e: int;
+
+	case n.ty.kind{
+	Tbyte =>
+		bout.puts("\tbyte\t@mp+"+string offset+","+string(int n.c.val & 16rff)+"\n");
+	Tint or
+	Tfix =>
+		bout.puts("\tword\t@mp+"+string offset+","+string(int n.c.val)+"\n");
+	Tbig =>
+		bout.puts("\tlong\t@mp+"+string offset+","+string n.c.val+" # "+string bhex(n.c.val, 16)+"\n");
+	Tstring =>
+		asmstring(offset, n.decl.sym);
+	Treal =>
+		fs := "";
+		ba := array[8] of byte;
+		export_real(ba, array[] of {n.c.rval});
+		for(i = 0; i < 8; i++)
+			fs += hex(int ba[i], 2);
+		bout.puts("\treal\t@mp+"+string offset+","+string n.c.rval+" # "+fs+"\n");
+	Tadt or
+	Tadtpick or
+	Ttuple =>
+		id = n.ty.ids;
+		for(n = n.left; n != nil; n = n.right){
+			asminitializer(offset + id.offset, n.left);
+			id = id.next;
+		}
+	Tcase =>
+		c = n.ty.cse;
+		bout.puts("\tword\t@mp+"+string offset+","+string c.nlab);
+		for(i = 0; i < c.nlab; i++){
+			lab = c.labs[i];
+			bout.puts(","+string(int lab.start.c.val)+","+string(int lab.stop.c.val+1)+","+string(lab.inst.pc));
+		}
+		if(c.iwild != nil)
+			bout.puts(","+string c.iwild.pc+"\n");
+		else
+			bout.puts(",-1\n");
+	Tcasel =>
+		c = n.ty.cse;
+		bout.puts("\tword\t@mp+"+string offset+","+string c.nlab);
+		for(i = 0; i < c.nlab; i++){
+			lab = c.labs[i];
+			bout.puts(","+string(lab.start.c.val)+","+string(lab.stop.c.val+big 1)+","+string(lab.inst.pc));
+		}
+		if(c.iwild != nil)
+			bout.puts(","+string c.iwild.pc+"\n");
+		else
+			bout.puts(",-1\n");
+	Tcasec =>
+		c = n.ty.cse;
+		bout.puts("\tword\t@mp+"+string offset+","+string c.nlab+"\n");
+		offset += IBY2WD;
+		for(i = 0; i < c.nlab; i++){
+			lab = c.labs[i];
+			asmstring(offset, lab.start.decl.sym);
+			offset += IBY2WD;
+			if(lab.stop != lab.start)
+				asmstring(offset, lab.stop.decl.sym);
+			offset += IBY2WD;
+			bout.puts("\tword\t@mp+"+string offset+","+string lab.inst.pc+"\n");
+			offset += IBY2WD;
+		}
+		if(c.iwild != nil)
+			bout.puts("\tword\t@mp+"+string offset+","+string c.iwild.pc+"\n");
+		else
+			bout.puts("\tword\t@mp+"+string offset+",-1\n");
+	Tgoto =>
+		c = n.ty.cse;
+		bout.puts("\tword\t@mp+"+string offset);
+		bout.puts(","+string(n.ty.size/IBY2WD-1));
+		for(i = 0; i < c.nlab; i++)
+			bout.puts(","+string c.labs[i].inst.pc);
+		if(c.iwild != nil)
+			bout.puts(","+string c.iwild.pc);
+		bout.puts("\n");
+	Tany =>
+		break;
+	Tarray =>
+		bout.puts("\tarray\t@mp+"+string offset+",$"+string n.ty.tof.decl.desc.id+","+string int n.left.c.val+"\n");
+		if(n.right == nil)
+			break;
+		bout.puts("\tindir\t@mp+"+string offset+",0\n");
+		c = n.right.ty.cse;
+		wild = nil;
+		if(c.wild != nil)
+			wild = c.wild.right;
+		last := 0;
+		esz := n.ty.tof.size;
+		for(i = 0; i < c.nlab; i++){
+			e = int c.labs[i].start.c.val;
+			if(wild != nil){
+				for(; last < e; last++)
+					asminitializer(esz * last, wild);
+			}
+			last = e;
+			e = int c.labs[i].stop.c.val;
+			elem := c.labs[i].node.right;
+			for(; last <= e; last++)
+				asminitializer(esz * last, elem);
+		}
+		if(wild != nil)
+			for(e = int n.left.c.val; last < e; last++)
+				asminitializer(esz * last, wild);
+		bout.puts("\tapop\n");
+	Tiface =>
+		if(LDT)
+			bout.puts("\tword\t@ldt+"+string offset+","+string int n.c.val+"\n");
+		else
+			bout.puts("\tword\t@mp+"+string offset+","+string int n.c.val+"\n");
+		offset += IBY2WD;
+		for(id = n.decl.ty.ids; id != nil; id = id.next){
+			offset = align(offset, IBY2WD);
+			if(LDT)
+				bout.puts("\text\t@ldt+"+string offset+",0x"+string hex(sign(id), 0)+",\"");
+			else
+				bout.puts("\text\t@mp+"+string offset+",0x"+string hex(sign(id), 0)+",\"");
+			dotlen := 0;
+			idlen := len array of byte id.sym.name + 1;
+			if(id.dot.ty.kind == Tadt){
+				dotlen = len array of byte id.dot.sym.name + 1;
+				bout.puts(id.dot.sym.name+".");
+			}
+			bout.puts(id.sym.name+"\"\n");
+			offset += idlen + dotlen + IBY2WD;
+		}
+	* =>
+		fatal("can't asm global "+nodeconv(n));
+	}
+}
+
+asmexc(es: ref Except)
+{
+	e: ref Except;
+
+	n := 0;
+	for(e = es; e != nil; e = e.next)
+		n++;
+	bout.puts("\texceptions\t" + string n + "\n");
+	for(e = es; e != nil; e = e.next){
+		if(!int e.p1.reach && !int e.p2.reach)
+			continue;
+		c := e.c;
+		o := e.d.offset;
+		if(e.desc != nil)
+			id := e.desc.id;
+		else
+			id = -1;
+		bout.puts("\texception\t" + string getpc(e.p1) + ", " + string getpc(e.p2) + ", " + string o + ", " + string id + ", " + string c.nlab + ", " + string e.ne + "\n");
+		for(i := 0; i < c.nlab; i++){
+			lab := c.labs[i];
+			d := lab.start.decl;
+			if(lab.start.ty.kind == Texception)
+				d = d.init.decl;
+			bout.puts("\texctab\t\"" + d.sym.name + "\", " + string lab.inst.pc + "\n");
+		}
+		if(c.iwild == nil)
+			bout.puts("\texctab\t" + "*" + ", " + string -1 + "\n");
+		else
+			bout.puts("\texctab\t" + "*" + ", " + string c.iwild.pc + "\n");
+	}
+}
+
+asmstring(offset: int, sym: ref Sym)
+{
+	bout.puts("\tstring\t@mp+"+string offset+",\"");
+	s := sym.name;
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c == '\n')
+			bout.puts("\\n");
+		else if(c == '\u0000')
+			bout.puts("\\z");
+		else if(c == '"')
+			bout.puts("\\\"");
+		else if(c == '\\')
+			bout.puts("\\\\");
+		else
+			bout.putc(c);
+	}
+	bout.puts("\"\n");
+}
+
+asminst(in: ref Inst)
+{
+	for(; in != nil; in = in.next){
+		if(in.op == INOOP)
+			continue;
+		if(in.pc % 10 == 0){
+			bout.putc('#');
+			bout.puts(string in.pc);
+			bout.putc('\n');
+		}
+		bout.puts(instconv(in));
+		bout.putc('\n');
+	}
+}
--- /dev/null
+++ b/appl/cmd/limbo/com.b
@@ -1,0 +1,1402 @@
+# back end
+
+breaks:		array of ref Inst;
+conts:		array of ref Inst;
+labels:		array of ref Decl;
+bcscps:		array of ref Node;
+labdep:		int;
+nocont:		ref Inst;
+nlabel:		int;
+
+scp:			int;
+scps:=		array[MaxScope] of ref Node;
+
+curfn:	ref Decl;
+
+pushscp(n : ref Node)
+{
+	if (scp >= MaxScope)
+		fatal("scope too deep");
+	scps[scp++] = n;
+}
+
+popscp()
+{
+	scp--;
+}
+
+curscp() : ref Node
+{
+	if (scp == 0)
+		return nil;
+	return scps[scp-1];
+}
+
+zeroscopes(stop : ref Node)
+{
+	i : int;
+	cs : ref Node;
+
+	for (i = scp-1; i >= 0; i--) {
+		cs = scps[i];
+		if (cs == stop)
+			break;
+		zcom(cs.left, nil);
+	}
+}
+
+zeroallscopes(n: ref Node, nn: array of ref Node)
+{
+	if(n == nil)
+		return;
+	for(; n != nil; n = n.right){
+		case(n.op){
+		Oscope =>
+			zeroallscopes(n.right, nn);
+			zcom(n.left, nn);
+			return;
+		Olabel or
+		Odo =>
+			zeroallscopes(n.right, nn);
+			return;
+		Oif or
+		Ofor =>
+			zeroallscopes(n.right.left, nn);
+			zeroallscopes(n.right.right, nn);
+			return;
+		Oalt or
+		Ocase or
+		Opick or
+		Oexcept =>
+			for(n = n.right; n != nil; n = n.right)
+				zeroallscopes(n.left.right, nn);
+			return;
+		Oseq =>
+			zeroallscopes(n.left, nn);
+			break;
+		Oexstmt =>
+			zeroallscopes(n.left, nn);
+			zeroallscopes(n.right, nn);
+			return;
+		* =>
+			return;
+		}
+	}
+}
+
+excs: ref Except;
+
+installexc(en: ref Node, p1: ref Inst, p2: ref Inst, zn: ref Node)
+{
+	e := ref Except;
+	e.p1 = p1;
+	e.p2 = p2;
+	e.c = en.ty.cse;
+	e.d = en.left.decl;
+	e.zn = zn;
+	e.next = excs;
+	excs = e;
+
+	ne := 0;
+	c := e.c;
+	for(i := 0; i < c.nlab; i++){
+		lab := c.labs[i];
+		if(lab.start.ty.kind == Texception)
+			ne++;
+	}
+	e.ne = ne;
+}
+
+inlist(d: ref Decl, dd: ref Decl): int
+{
+	for( ; dd != nil; dd = dd.next)
+		if(d == dd)
+			return 1;
+	return 0;
+}
+
+excdesc()
+{
+	dd, nd: ref Decl;
+
+	for(e := excs; e != nil; e = e.next){
+		if(e.zn != nil){
+			dd = nil;
+			maxo := 0;
+			for(n := e.zn ; n != nil; n = n.right){
+				d := n.decl;
+				d.locals = d.next;
+				if(!inlist(d, dd)){
+					d.next = dd;
+					dd = d;
+					o := d.offset+d.ty.size;
+					if(o > maxo)
+						maxo = o;
+				}
+			}
+			e.desc = gendesc(e.d, align(maxo, MaxAlign), dd);
+			for(d := dd; d != nil; d = nd){
+				nd = d.next;
+				d.next = d.locals;
+				d.locals = nil;
+			}
+			e.zn = nil;
+		}
+	}
+}
+
+reve(e: ref Except): ref Except
+{
+	l, n: ref Except;
+
+	l = nil;
+	for( ; e != nil; e = n){
+		n = e.next;
+		e.next = l;
+		l = e;
+	}
+	return l;
+}
+
+ckinline0(n: ref Node, d: ref Decl): int
+{
+	dd: ref Decl;
+
+	if(n == nil)
+		return 1;
+	if(n.op == Oname){
+		dd = n.decl;
+		if(d == dd)
+			return 0;
+		if(int dd.inline == 1)
+			return ckinline0(dd.init.right, d);
+		return 1;
+	}
+	return ckinline0(n.left, d) && ckinline0(n.right, d);
+}
+
+ckinline(d: ref Decl)
+{
+	d.inline = byte ckinline0(d.init.right, d);
+}
+
+modcom(entry: ref Decl)
+{
+	d, m: ref Decl;
+
+	if(errors)
+		return;
+
+	if(emitcode != "" || emitstub || emittab != "" || emitsbl != ""){
+		emit(curscope());
+		popscope();
+		return;
+	}
+
+	#
+	# scom introduces global variables for case statements
+	# and unaddressable constants, so it must be done before
+	# popping the global scope
+	#
+	gent = sys->millisec();
+	nlabel = 0;
+	maxstack = MaxTemp;
+	nocont = ref Inst;
+	genstart();
+
+	for(i := 0; i < nfns; i++)
+		if(int fns[i].inline == 1)
+			ckinline(fns[i]);
+
+	ok := 0;
+	for(i = 0; i < nfns; i++){
+		d = fns[i];
+		if(d.refs > 1 && !(int d.inline == 1 && local(d) && d.iface == nil)){
+			fns[ok++] = d;
+			fncom(d);
+		}
+	}
+	fns = fns[:ok];
+	nfns = ok;
+	if(blocks != -1)
+		fatal("blocks not nested correctly");
+	firstinst = firstinst.next;
+	if(errors)
+		return;
+
+	globals := popscope();
+	checkrefs(globals);
+	if(errors)
+		return;
+	globals = vars(globals);
+	moddataref();
+
+	nils := popscope();
+	m = nil;
+	for(d = nils; d != nil; d = d.next){
+		if(debug['n'])
+			print("nil '%s' ref %d\n", d.sym.name, d.refs);
+		if(d.refs && m == nil)
+			m = dupdecl(d);
+		d.offset = 0;
+	}
+	globals = appdecls(m, globals);
+	globals = namesort(globals);
+	globals = modglobals(impdecls.d, globals);
+	vcom(globals);
+	narrowmods();
+	ldts: ref Decl;
+	if(LDT)
+		(globals, ldts) = resolveldts(globals);
+	offset := idoffsets(globals, 0, IBY2WD);
+	if(LDT)
+		ldtoff := idindices(ldts);	# idoffsets(ldts, 0, IBY2WD);
+	for(d = nils; d != nil; d = d.next){
+		if(debug['n'])
+			print("nil '%s' ref %d\n", d.sym.name, d.refs);
+		if(d.refs)
+			d.offset = m.offset;
+	}
+
+	if(debug['g']){
+		print("globals:\n");
+		printdecls(globals);
+	}
+
+	ndata := 0;
+	for(d = globals; d != nil; d = d.next)
+		ndata++;
+	ndesc := resolvedesc(impdecls.d, offset, globals);
+	ninst := resolvepcs(firstinst);
+	modresolve();
+	if(impdecls.next != nil)
+		for(dl := impdecls; dl != nil; dl = dl.next)
+			resolvemod(dl.d);
+	nlink := resolvemod(impdecl);
+	gent = sys->millisec() - gent;
+
+	maxstack *= 10;
+	if(fixss != 0)
+		maxstack = fixss;
+
+	if(debug['s'])
+		print("%d instructions\n%d data elements\n%d type descriptors\n%d functions exported\n%d stack size\n",
+			ninst, ndata, ndesc, nlink, maxstack);
+
+	excs = reve(excs);
+
+	writet = sys->millisec();
+	if(gendis){
+		discon(XMAGIC);
+		hints := 0;
+		if(mustcompile)
+			hints |= MUSTCOMPILE;
+		if(dontcompile)
+			hints |= DONTCOMPILE;
+		if(LDT)
+			hints |= HASLDT;
+		if(excs != nil)
+			hints |= HASEXCEPT;
+		discon(hints);		# runtime hints
+		discon(maxstack);	# minimum stack extent size
+		discon(ninst);
+		discon(offset);
+		discon(ndesc);
+		discon(nlink);
+		disentry(entry);
+		disinst(firstinst);
+		disdesc(descriptors);
+		disvar(offset, globals);
+		dismod(impdecl);
+		if(LDT)
+			disldt(ldtoff, ldts);
+		if(excs != nil)
+			disexc(excs);
+		dispath();
+	}else{
+		asminst(firstinst);
+		asmentry(entry);
+		asmdesc(descriptors);
+		asmvar(offset, globals);
+		asmmod(impdecl);
+		if(LDT)
+			asmldt(ldtoff, ldts);
+		if(excs != nil)
+			asmexc(excs);
+		asmpath();
+	}
+	writet = sys->millisec() - writet;
+
+	symt = sys->millisec();
+	if(bsym != nil){
+		sblmod(impdecl);
+
+		sblfiles();
+		sblinst(firstinst, ninst);
+		sblty(adts, nadts);
+		sblfn(fns, nfns);
+		sblvar(globals);
+	}
+	symt = sys->millisec() - symt;
+
+	firstinst = nil;
+	lastinst = nil;
+
+	excs = nil;
+}
+
+fncom(decl: ref Decl)
+{
+	curfn = decl;
+	if(ispoly(decl))
+		addfnptrs(decl, 1);
+
+	#
+	# pick up the function body and compile it
+	# this code tries to clean up the parse nodes as fast as possible
+	# function is Ofunc(name, body)
+	#
+	decl.pc = nextinst();
+	tinit();
+	labdep = 0;
+	scp = 0;
+	breaks = array[maxlabdep] of ref Inst;
+	conts = array[maxlabdep] of ref Inst;
+	labels = array[maxlabdep] of ref Decl;
+	bcscps = array[maxlabdep] of ref Node;
+	
+	n := decl.init;
+	if(int decl.inline == 1)
+		decl.init = dupn(0, nosrc, n);
+	else
+		decl.init = n.left;
+	src := n.right.src;
+	src.start = src.stop - 1;
+	for(n = n.right; n != nil; n = n.right){
+		if(n.op != Oseq){
+			if(n.op == Ocall && trcom(n, nil, 1))
+				break;
+			scom(n);
+			break;
+		}
+		if(n.left.op == Ocall && trcom(n.left, n.right, 1)){
+			n = n.right;
+			if(n == nil || n.op != Oseq)
+				break;
+		}
+		else
+			scom(n.left);
+	}
+	pushblock();
+	valued := decl.ty.tof != tnone;
+	if(valued)
+		in := genrawop(src, IRAISE, nil, nil, nil);
+	else
+		in = genrawop(src, IRET, nil, nil, nil);
+	popblock();
+	reach(decl.pc);
+	if(valued && in.reach != byte 0)
+		error(src.start, "no return at end of function " + dotconv(decl));
+	# decl.endpc = lastinst;
+	if(labdep != 0)
+		fatal("unbalanced label stack");
+	breaks = nil;
+	conts = nil;
+	labels = nil;
+	bcscps = nil;
+
+	loc := declsort(appdecls(vars(decl.locals), tdecls()));
+
+	decl.offset = idoffsets(loc, decl.offset, MaxAlign);
+	for(last := decl.ty.ids; last != nil && last.next != nil; last = last.next)
+		;
+	if(last != nil)
+		last.next = loc;
+	else
+		decl.ty.ids = loc;
+
+	if(debug['f']){
+		print("fn: %s\n", decl.sym.name);
+		printdecls(decl.ty.ids);
+	}
+
+	decl.desc = gendesc(decl, decl.offset, decl.ty.ids);
+	decl.locals = loc;
+	excdesc();
+	if(decl.offset > maxstack)
+		maxstack = decl.offset;
+	if(optims)
+		optim(decl.pc, decl);
+	if(last != nil)
+		last.next = nil;
+	else
+		decl.ty.ids = nil;
+}
+
+#
+# statement compiler
+#
+scom(n: ref Node)
+{
+	b: int;
+	p, pp: ref Inst;
+	left: ref Node;
+
+	for(; n != nil; n = n.right){
+		case n.op{
+		Ocondecl or
+		Otypedecl or
+		Ovardecl or
+		Oimport or
+		Oexdecl =>
+			return;
+		Ovardecli =>
+			break;
+		Oscope =>
+			pushscp(n);
+			scom(n.right);
+			popscp();
+			zcom(n.left, nil);
+			return;
+		Olabel =>
+			scom(n.right);
+			return;
+		Oif =>
+			pushblock();
+			left = simplify(n.left);
+			if(left.op == Oconst && left.ty == tint){
+				if(left.c.val != big 0)
+					scom(n.right.left);
+				else
+					scom(n.right.right);
+				popblock();
+				return;
+			}
+			sumark(left);
+			pushblock();
+			p = bcom(left, 1, nil);
+			tfreenow();
+			popblock();
+			scom(n.right.left);
+			if(n.right.right != nil){
+				pp = p;
+				p = genrawop(lastinst.src, IJMP, nil, nil, nil);
+				patch(pp, nextinst());
+				scom(n.right.right);
+			}
+			patch(p, nextinst());
+			popblock();
+			return;
+		Ofor =>
+			n.left = left = simplify(n.left);
+			if(left.op == Oconst && left.ty == tint){
+				if(left.c.val == big 0)
+					return;
+				left.op = Onothing;
+				left.ty = tnone;
+				left.decl = nil;
+			}
+			pp = nextinst();
+			b = pushblock();
+			sumark(left);
+			p = bcom(left, 1, nil);
+			tfreenow();
+			popblock();
+
+			if(labdep >= maxlabdep)
+				fatal("label stack overflow");
+			breaks[labdep] = nil;
+			conts[labdep] = nil;
+			labels[labdep] = n.decl;
+			bcscps[labdep] = curscp();
+			labdep++;
+			scom(n.right.left);
+			labdep--;
+
+			patch(conts[labdep], nextinst());
+			if(n.right.right != nil){
+				pushblock();
+				scom(n.right.right);
+				popblock();
+			}
+			repushblock(lastinst.block);	# was b
+			patch(genrawop(lastinst.src, IJMP, nil, nil, nil), pp);	# for cprof: was left.src
+			popblock();
+			patch(p, nextinst());
+			patch(breaks[labdep], nextinst());
+			return;
+		Odo =>
+			pp = nextinst();
+
+			if(labdep >= maxlabdep)
+				fatal("label stack overflow");
+			breaks[labdep] = nil;
+			conts[labdep] = nil;
+			labels[labdep] = n.decl;
+			bcscps[labdep] = curscp();
+			labdep++;
+			scom(n.right);
+			labdep--;
+
+			patch(conts[labdep], nextinst());
+
+			left = simplify(n.left);
+			if(left.op == Onothing
+			|| left.op == Oconst && left.ty == tint){
+				if(left.op == Onothing || left.c.val != big 0){
+					pushblock();
+					p = genrawop(left.src, IJMP, nil, nil, nil);
+					popblock();
+				}else
+					p = nil;
+			}else{
+				pushblock();
+				p = bcom(sumark(left), 0, nil);
+				tfreenow();
+				popblock();
+			}
+			patch(p, pp);
+			patch(breaks[labdep], nextinst());
+			return;
+		Ocase or
+		Opick or
+		Oalt or
+		Oexcept =>
+			pushblock();
+			if(labdep >= maxlabdep)
+				fatal("label stack overflow");
+			breaks[labdep] = nil;
+			conts[labdep] = nocont;
+			labels[labdep] = n.decl;
+			bcscps[labdep] = curscp();
+			labdep++;
+			case n.op{
+			Oalt =>
+				altcom(n);
+			Ocase or
+			Opick =>
+				casecom(n);
+			Oexcept =>
+				excom(n);
+			}
+			labdep--;
+			patch(breaks[labdep], nextinst());
+			popblock();
+			return;
+		Obreak =>
+			pushblock();
+			bccom(n, breaks);
+			popblock();
+		Ocont =>
+			pushblock();
+			bccom(n, conts);
+			popblock();
+		Oseq =>
+			if(n.left.op == Ocall && trcom(n.left, n.right, 0)){
+				n = n.right;
+				if(n == nil || n.op != Oseq)
+					return;
+			}
+			else
+				scom(n.left);
+		Oret =>
+			if(n.left != nil && n.left.op == Ocall && trcom(n.left, nil, 1))
+				return;
+			pushblock();
+			if(n.left != nil){
+				n.left = simplify(n.left);
+				sumark(n.left);
+				ecom(n.left.src, retalloc(ref Node, n.left), n.left);
+				tfreenow();
+			}
+			genrawop(n.src, IRET, nil, nil, nil);
+			popblock();
+			return;
+		Oexit =>
+			pushblock();
+			genrawop(n.src, IEXIT, nil, nil, nil);
+			popblock();
+			return;
+		Onothing =>
+			return;
+		Ofunc =>
+			fatal("Ofunc");
+			return;
+		Oexstmt =>
+			pushblock();
+			pp = genrawop(n.right.src, IEXC0, nil, nil, nil);	# marker
+			p1 := nextinst();
+			scom(n.left);
+			p2 := nextinst();
+			p3 := genrawop(n.right.src, IJMP, nil, nil, nil);
+			p = genrawop(n.right.src, IEXC, nil, nil, nil);	# marker
+			p.d.decl = mkdecl(n.src, 0, n.right.ty);
+			zn := array[1] of ref Node;
+			zeroallscopes(n.left, zn);
+			scom(n.right);
+			patch(p3, nextinst());
+			installexc(n.right, p1, p2, zn[0]);
+			patch(pp, p);
+			popblock();
+			return;
+		* =>
+			pushblock();
+			n = simplify(n);
+			sumark(n);
+			ecom(n.src, nil, n);
+			tfreenow();
+			popblock();
+			return;
+		}
+	}
+}
+
+#
+# compile a break, continue
+#
+bccom(n: ref Node, bs: array of ref Inst)
+{
+	s: ref Sym;
+
+	s = nil;
+	if(n.decl != nil)
+		s = n.decl.sym;
+	ok := -1;
+	for(i := 0; i < labdep; i++){
+		if(bs[i] == nocont)
+			continue;
+		if(s == nil || labels[i] != nil && labels[i].sym == s)
+			ok = i;
+	}
+	if(ok < 0)
+		fatal("didn't find break or continue");
+	zeroscopes(bcscps[ok]);
+	p := genrawop(n.src, IJMP, nil, nil, nil);
+	p.branch = bs[ok];
+	bs[ok] = p;
+}
+
+dogoto(c: ref Case): int
+{
+	i, j, k, n, r, q, v: int;
+	l, nl: array of Label;
+	src: Src;
+
+	l = c.labs;
+	n = c.nlab;
+	if(n == 0)
+		return 0;
+	r = int l[n-1].stop.c.val - int l[0].start.c.val+1;
+	if(r >= 3 && r <= 3*n){
+		if(r != n){
+			# remove ranges, fill in gaps
+			c.nlab = r;
+			nl = c.labs = array[r] of Label;
+			k = 0;
+			v = int l[0].start.c.val-1;
+			for(i = 0; i < n; i++){
+				# p = int l[i].start.c.val;
+				q = int l[i].stop.c.val;
+				src = l[i].start.src;
+				for(j = v+1; j <= q; j++){
+					nl[k] = l[i];
+					nl[k].start = nl[k].stop = mkconst(src, big j);
+					k++;
+				}
+				v = q;
+			}
+			if(k != r)
+				fatal("bad case expansion");
+		}
+		l = c.labs;
+		for(i = 0; i < r; i++)
+			l[i].inst = nil;
+		return 1;
+	}
+	return 0;
+}
+
+fillrange(c: ref Case, nn: ref Node, in: ref Inst)
+{
+	i, j, n, p, q: int;
+	l: array of Label;
+
+	l = c.labs;
+	n = c.nlab;
+	p = int nn.left.c.val;
+	q = int nn.right.c.val;
+	for(i = 0; i < n; i++)
+		if(int l[i].start.c.val == p)
+			break;
+	if(i == n)
+		fatal("fillrange fails");
+	for(j = p; j <= q; j++)
+		l[i++].inst = in;
+}
+
+nconstqual(s1: ref Node): int
+{
+	n := 0;
+	for(; s1 != nil; s1 = s1.right){
+		for(s2 := s1.left.left; s2 != nil; s2 = s2.right)
+			if(s2.left.op == Oconst)
+				n++;
+	}
+	return n;
+}
+
+casecom(cn: ref Node)
+{
+	d: ref Decl;
+	left, p, tmp, tmpc: ref Node;
+	jmps, wild, j1, j2: ref Inst;
+
+	c := cn.ty.cse;
+
+	needwild := cn.op != Opick || nconstqual(cn.right) != cn.left.right.ty.tof.decl.tag;
+	igoto := cn.left.ty == tint && dogoto(c);
+
+	#
+	# generate global which has case labels
+	#
+	if(igoto){
+		d = mkids(cn.src, enter(".g"+string nlabel++, 0), cn.ty, nil);
+		cn.ty.kind = Tgoto;
+	}
+	else
+		d = mkids(cn.src, enter(".c"+string nlabel++, 0), cn.ty, nil);
+	d.init = mkdeclname(cn.src, d);
+	nto := ref znode;
+	nto.addable = Rmreg;
+	nto.left = nil;
+	nto.right = nil;
+	nto.op = Oname;
+	nto.ty = d.ty;
+	nto.decl = d;
+
+	tmp = nil;
+	left = cn.left;
+	left = simplify(left);
+	cn.left = left;
+	sumark(left);
+	if(debug['c'])
+		print("case %s\n", nodeconv(left));
+	ctype := cn.left.ty;
+	if(left.addable >= Rcant){
+		if(cn.op == Opick){
+			ecom(left.src, nil, left);
+			tfreenow();
+			left = mkunary(Oind, dupn(1, left.src, left.left));
+			left.ty = tint;
+			sumark(left);
+			ctype = tint;
+		}else{
+			(left, tmp) = eacom(left, nil);
+			tfreenow();
+		}
+	}
+
+	labs := c.labs;
+	nlab := c.nlab;
+
+	if(igoto){
+		if(labs[0].start.c.val != big 0){
+			tmpc = talloc(left.ty, nil);
+			if(left.addable == Radr || left.addable == Rmadr){
+				genrawop(left.src, IMOVW, left, nil, tmpc);
+				left = tmpc;
+			}
+			genrawop(left.src, ISUBW, sumark(labs[0].start), left, tmpc);
+			left = tmpc;
+		}
+		if(needwild){
+			j1 = genrawop(left.src, IBLTW, left, sumark(mkconst(left.src, big 0)), nil);
+			j2 = genrawop(left.src, IBGTW, left, sumark(mkconst(left.src, labs[nlab-1].start.c.val-labs[0].start.c.val)), nil);
+		}
+		j := nextinst();
+		genrawop(left.src, IGOTO, left, nil, nto);
+		j.d.reg = IBY2WD;
+	}
+	else{
+		op := ICASE;
+		if(ctype == tbig)
+			op = ICASEL;
+		else if(ctype == tstring)
+			op = ICASEC;
+		genrawop(left.src, op, left, nil, nto);
+	}
+	tfree(tmp);
+	tfree(tmpc);
+
+	jmps = nil;
+	wild = nil;
+	for(n := cn.right; n != nil; n = n.right){
+		j := nextinst();
+		for(p = n.left.left; p != nil; p = p.right){
+			if(debug['c'])
+				print("case qualifier %s\n", nodeconv(p.left));
+			case p.left.op{
+			Oconst =>
+				labs[findlab(ctype, p.left, labs, nlab)].inst = j;
+			Orange =>
+				labs[findlab(ctype, p.left.left, labs, nlab)].inst = j;
+				if(igoto)
+					fillrange(c, p.left, j);
+			Owild =>
+				if(needwild)
+					wild = j;
+				# else
+				#	nwarn(p.left, "default case redundant");
+			}
+		}
+
+		if(debug['c'])
+			print("case body for %s: %s\n", expconv(n.left.left), nodeconv(n.left.right));
+
+		k := nextinst();
+		scom(n.left.right);
+
+		src := lastinst.src;
+		# if(n.left.right == nil || n.left.right.op == Onothing)
+		if(k == nextinst())
+			src = n.left.left.src;
+		j = genrawop(src, IJMP, nil, nil, nil);
+		j.branch = jmps;
+		jmps = j;
+	}
+	patch(jmps, nextinst());
+	if(wild == nil && needwild)
+		wild = nextinst();
+
+	if(igoto){
+		if(needwild){
+			patch(j1, wild);
+			patch(j2, wild);
+		}
+		for(i := 0; i < nlab; i++)
+			if(labs[i].inst == nil)
+				labs[i].inst = wild;
+	}
+
+	c.iwild = wild;
+
+	d.ty.cse = c;
+	usetype(d.ty);
+	installids(Dglobal, d);
+}
+
+altcom(nalt: ref Node)
+{
+	p, op, left: ref Node;
+	jmps, wild, j: ref Inst = nil;
+
+	talt := nalt.ty;
+	c := talt.cse;
+	nlab := c.nlab;
+	nsnd := c.nsnd;
+	comm := array[nlab] of ref Node;
+	labs := array[nlab] of Label;
+	tmps := array[nlab] of ref Node;
+	c.labs = labs;
+
+	#
+	# built the type of the alt channel table
+	# note that we lie to the garbage collector
+	# if we know that another reference exists for the channel
+	#
+	is := 0;
+	ir := nsnd;
+	i := 0;
+	for(n := nalt.left; n != nil; n = n.right){
+		for(p = n.left.right.left; p != nil; p = p.right){
+			left = simplify(p.left);
+			p.left = left;
+			if(left.op == Owild)
+				continue;
+			comm[i] = hascomm(left);
+			left = comm[i].left;
+			sumark(left);
+			isptr := left.addable >= Rcant;
+			if(comm[i].op == Osnd)
+				labs[is++].isptr = isptr;
+			else
+				labs[ir++].isptr = isptr;
+			i++;
+		}
+	}
+
+	which := talloc(tint, nil);
+	tab := talloc(talt, nil);
+
+	#
+	# build the node for the address of each channel,
+	# the values to send, and the storage fro values received
+	#
+	off := ref znode;
+	adr := ref znode;
+	add := ref znode;
+	slot := ref znode;
+	off.op = Oconst;
+	off.c = ref Const(big 0, 0.0);	# jrf - added initialization
+	off.ty = tint;
+	off.addable = Rconst;
+	adr.op = Oadr;
+	adr.left = tab;
+	adr.ty = tint;
+	add.op = Oadd;
+	add.left = adr;
+	add.right = off;
+	add.ty = tint;
+	slot.op = Oind;
+	slot.left = add;
+	sumark(slot);
+
+	#
+	# compile the sending and receiving channels and values
+	#
+	is = 2*IBY2WD;
+	ir = is + nsnd*2*IBY2WD;
+	i = 0;
+	for(n = nalt.left; n != nil; n = n.right){
+		for(p = n.left.right.left; p != nil; p = p.right){
+			if(p.left.op == Owild)
+				continue;
+
+			#
+			# gen channel
+			#
+			op = comm[i];
+			if(op.op == Osnd){
+				off.c.val = big is;
+				is += 2*IBY2WD;
+			}else{
+				off.c.val = big ir;
+				ir += 2*IBY2WD;
+			}
+			left = op.left;
+
+			#
+			# this sleaze is lying to the garbage collector
+			#
+			if(left.addable < Rcant)
+				genmove(left.src, Mas, tint, left, slot);
+			else{
+				slot.ty = left.ty;
+				ecom(left.src, slot, left);
+				tfreenow();
+				slot.ty = nil;
+			}
+
+			#
+			# gen value
+			#
+			off.c.val += big IBY2WD;
+			(p.left, tmps[i]) = rewritecomm(p.left, comm[i], slot);
+
+			i++;
+		}
+	}
+
+	#
+	# stuff the number of send & receive channels into the table
+	#
+	altsrc := nalt.src;
+	altsrc.stop = (altsrc.stop & ~PosMask) | ((altsrc.stop + 3) & PosMask);
+	off.c.val = big 0;
+	genmove(altsrc, Mas, tint, sumark(mkconst(altsrc, big nsnd)), slot);
+	off.c.val += big IBY2WD;
+	genmove(altsrc, Mas, tint, sumark(mkconst(altsrc, big(nlab-nsnd))), slot);
+	off.c.val += big IBY2WD;
+
+	altop := IALT;
+	if(c.wild != nil)
+		altop = INBALT;
+	pp := genrawop(altsrc, altop, tab, nil, which);
+	pp.m.offset = talt.size;	# for optimizer
+
+	d := mkids(nalt.src, enter(".g"+string nlabel++, 0), mktype(nalt.src.start, nalt.src.stop, Tgoto, nil, nil), nil);
+	d.ty.cse = c;
+	d.init = mkdeclname(nalt.src, d);
+
+	nto := ref znode;
+	nto.addable = Rmreg;
+	nto.left = nil;
+	nto.right = nil;
+	nto.op = Oname;
+	nto.decl = d;
+	nto.ty = d.ty;
+
+	me := genrawop(altsrc, IGOTO, which, nil, nto);
+	me.d.reg = IBY2WD;		# skip the number of cases field
+	tfree(tab);
+	tfree(which);
+
+	#
+	# compile the guard expressions and bodies
+	#
+	i = 0;
+	is = 0;
+	ir = nsnd;
+	jmps = nil;
+	wild = nil;
+	for(n = nalt.left; n != nil; n = n.right){
+		j = nil;
+		for(p = n.left.right.left; p != nil; p = p.right){
+			tj := nextinst();
+			if(p.left.op == Owild){
+				wild = nextinst();
+			}else{
+				if(comm[i].op == Osnd)
+					labs[is++].inst = tj;
+				else{
+					labs[ir++].inst = tj;
+					tacquire(tmps[i]);
+				}
+				sumark(p.left);
+				if(debug['a'])
+					print("alt guard %s\n", nodeconv(p.left));
+				ecom(p.left.src, nil, p.left);
+				tfree(tmps[i]);
+				tfreenow();
+				i++;
+			}
+			if(p.right != nil){
+				tj = genrawop(lastinst.src, IJMP, nil, nil, nil);
+				tj.branch = j;
+				j = tj;
+			}
+		}
+
+		patch(j, nextinst());
+		if(debug['a'])
+			print("alt body %s\n", nodeconv(n.left.right));
+		scom(n.left);
+
+		j = genrawop(lastinst.src, IJMP, nil, nil, nil);
+		j.branch = jmps;
+		jmps = j;
+	}
+	patch(jmps, nextinst());
+	comm = nil;
+
+	c.iwild = wild;
+
+	usetype(d.ty);
+	installids(Dglobal, d);
+}
+
+excom(en: ref Node)
+{
+	ed: ref Decl;
+	p: ref Node;
+	jmps, wild: ref Inst;
+
+	ed = en.left.decl;
+	ed.ty = rtexception;
+	c := en.ty.cse;
+	labs := c.labs;
+	nlab := c.nlab;
+	jmps = nil;
+	wild = nil;
+	for(n := en.right; n != nil; n = n.right){
+		qt: ref Type = nil;
+		j := nextinst();
+		for(p = n.left.left; p != nil; p = p.right){
+			case p.left.op{
+			Oconst =>
+				labs[findlab(texception, p.left, labs, nlab)].inst = j;
+			Owild =>
+				wild = j;
+			}
+			if(qt == nil)
+				qt = p.left.ty;
+			else if(!tequal(qt, p.left.ty))
+				qt = texception;
+		}
+		if(qt != nil)
+			ed.ty = qt;
+		k := nextinst();
+		scom(n.left.right);
+		src := lastinst.src;
+		if(k == nextinst())
+			src = n.left.left.src;
+		j = genrawop(src, IJMP, nil, nil, nil);
+		j.branch = jmps;
+		jmps = j;
+	}
+	ed.ty = rtexception;
+	patch(jmps, nextinst());
+	c.iwild = wild;
+}
+
+#
+# rewrite the communication operand
+# allocate any temps needed for holding value to send or receive
+#
+rewritecomm(n, comm, slot: ref Node): (ref Node, ref Node)
+{
+	adr, tmp: ref Node;
+
+	if(n == nil)
+		return (nil, nil);
+	adr = nil;
+	if(n == comm){
+		if(comm.op == Osnd && sumark(n.right).addable < Rcant)
+			adr = n.right;
+		else{
+			adr = tmp = talloc(n.ty, nil);
+			tmp.src = n.src;
+			if(comm.op == Osnd){
+				ecom(n.right.src, tmp, n.right);
+				tfreenow();
+			}
+			else
+				trelease(tmp);
+		}
+	}
+	if(n.right == comm && n.op == Oas && comm.op == Orcv
+	&& sumark(n.left).addable < Rcant && (n.left.op != Oname || n.left.decl != nildecl))
+		adr = n.left;
+	if(adr != nil){
+		p := genrawop(comm.left.src, ILEA, adr, nil, slot);
+		p.m.offset = adr.ty.size;	# for optimizer
+		if(comm.op == Osnd)
+			p.m.reg = 1;	# for optimizer
+		return (adr, tmp);
+	}
+	(n.left, tmp) = rewritecomm(n.left, comm, slot);
+	if(tmp == nil)
+		(n.right, tmp) = rewritecomm(n.right, comm, slot);
+	return (n, tmp);
+}
+
+#
+# merge together two sorted lists, yielding a sorted list
+#
+declmerge(e, f: ref Decl): ref Decl
+{
+	d := rock := ref Decl;
+	while(e != nil && f != nil){
+		fs := f.ty.size;
+		es := e.ty.size;
+		# v := 0;
+		v := (e.link == nil) - (f.link == nil);
+		if(v == 0 && (es <= IBY2WD || fs <= IBY2WD))
+			v = fs - es;
+		if(v == 0)
+			v = e.refs - f.refs;
+		if(v == 0)
+			v = fs - es;
+		if(v == 0 && e.sym.name > f.sym.name)
+			v = -1;
+		if(v >= 0){
+			d.next = e;
+			d = e;
+			e = e.next;
+			while(e != nil && e.nid == byte 0){
+				d = e;
+				e = e.next;
+			}
+		}else{
+			d.next = f;
+			d = f;
+			f = f.next;
+			while(f != nil && f.nid == byte 0){
+				d = f;
+				f = f.next;
+			}
+		}
+		# d = d.next;
+	}
+	if(e != nil)
+		d.next = e;
+	else
+		d.next = f;
+	return rock.next;
+}
+
+#
+# recursively split lists and remerge them after they are sorted
+#
+recdeclsort(d: ref Decl, n: int): ref Decl
+{
+	if(n <= 1)
+		return d;
+	m := n / 2 - 1;
+	dd := d;
+	for(i := 0; i < m; i++){
+		dd = dd.next;
+		while(dd.nid == byte 0)
+			dd = dd.next;
+	}
+	r := dd.next;
+	while(r.nid == byte 0){
+		dd = r;
+		r = r.next;
+	}
+	dd.next = nil;
+	return declmerge(recdeclsort(d, n / 2),
+			recdeclsort(r, (n + 1) / 2));
+}
+
+#
+# sort the ids by size and number of references
+#
+declsort(d: ref Decl): ref Decl
+{
+	n := 0;
+	for(dd := d; dd != nil; dd = dd.next)
+		if(dd.nid > byte 0)
+			n++;
+	return recdeclsort(d, n);
+}
+
+nilsrc : Src;
+
+zcom1(n : ref Node, nn: array of ref Node)
+{
+	ty : ref Type;
+	d : ref Decl;
+	e : ref Node;
+
+	ty = n.ty;
+	if (!tmustzero(ty))
+		return;
+	if (n.op == Oname && n.decl.refs == 0)
+		return;
+	if (nn != nil) {
+		if(n.op != Oname)
+			error(n.src.start, "fatal: bad op in zcom1 map");
+		n.right = nn[0];
+		nn[0] = n;
+		return;
+	}
+	if (ty.kind == Tadtpick)
+		ty = ty.tof;
+	if (ty.kind == Ttuple || ty.kind == Tadt) {
+		for (d = ty.ids; d != nil; d = d.next) {
+			if (tmustzero(d.ty)) {
+				dn := n;
+				if (d.next != nil)
+					dn = dupn(0, nilsrc, n);
+				e = mkbin(Odot, dn, mkname(nilsrc, d.sym));
+				e.right.decl = d;
+				e.ty = e.right.ty = d.ty;
+				zcom1(e, nn);
+			}
+		}
+	}
+	else {
+		src := n.src;
+		n.src = nilsrc;
+		e = mkbin(Oas, n, mknil(nilsrc));
+		e.ty = e.right.ty = ty;
+		if (debug['Z'])
+			print("ecom %s\n", nodeconv(e));
+		pushblock();
+		e = simplify(e);
+		sumark(e);
+		ecom(e.src, nil, e);
+		popblock();
+		n.src = src;
+		e = nil;
+	}
+}
+
+zcom0(id : ref Decl, nn: array of ref Node)
+{
+	e := mkname(nilsrc, id.sym);
+	e.decl = id;
+	e.ty = id.ty;
+	zcom1(e, nn);
+}
+
+zcom(n : ref Node, nn: array of ref Node)
+{
+	r : ref Node;
+
+	for ( ; n != nil; n = r) {
+		r = n.right;
+		n.right = nil;
+		case (n.op) {
+			Ovardecl =>
+				last := n.left.decl;
+				for (ids := n.decl; ids != last.next; ids = ids.next)
+					zcom0(ids, nn);
+				break;
+			Oname =>
+				if (n.decl != nildecl)
+					zcom1(dupn(0, nilsrc, n), nn);
+				break;
+			Otuple =>
+				for (nt := n.left; nt != nil; nt = nt.right)
+					zcom(nt.left, nn);
+				break;
+			* =>
+				fatal("bad node in zcom()");
+				break;
+		}
+		n.right = r;
+	}
+}
+
+ret(n: ref Node, nilret: int): int
+{
+	if(n == nil)
+		return nilret;
+	if(n.op == Oseq)
+		n = n.left;
+	return n.op == Oret && n.left == nil;
+}
+
+trcom(e: ref Node, ne: ref Node, nilret: int): int
+{
+	d, id: ref Decl;
+	as, a, f, n: ref Node;
+	p: ref Inst;
+
+return 0;	# TBS
+	if(e.op != Ocall || e.left.op != Oname)
+		return 0;
+	d = e.left.decl;
+	if(d != curfn || int d.handler || ispoly(d))
+		return 0;
+	if(!ret(ne, nilret))
+		return 0;
+	pushblock();
+	id = d.ty.ids;
+	# evaluate args in same order as normal calls
+	for(as = e.right; as != nil; as = as.right){
+		a = as.left;
+		if(!(a.op == Oname && id == a.decl)){
+			if(occurs(id, as.right)){
+				f = talloc(id.ty, nil);
+				f.flags |= byte TEMP;
+			}
+			else
+				f = mkdeclname(as.src, id);
+			n = mkbin(Oas, f, a);
+			n.ty = id.ty;
+			scom(n);
+			if(int f.flags&TEMP)
+				as.left = f;
+		}
+		id = id.next;
+	}
+	id = d.ty.ids;
+	for(as = e.right; as != nil; as = as.right){
+		a = as.left;
+		if(int a.flags&TEMP){
+			f = mkdeclname(as.src, id);
+			n = mkbin(Oas, f, a);
+			n.ty = id.ty;
+			scom(n);
+			tfree(a);
+		}
+		id = id.next;
+	}
+	p = genrawop(e.src, IJMP, nil, nil, nil);
+	patch(p, d.pc);
+	popblock();
+	return 1;
+}
--- /dev/null
+++ b/appl/cmd/limbo/decls.b
@@ -1,0 +1,1177 @@
+
+storename := array[Dend] of
+{
+	Dtype =>	"type",
+	Dfn =>		"function",
+	Dglobal =>	"global",
+	Darg =>		"argument",
+	Dlocal =>	"local",
+	Dconst =>	"con",
+	Dfield =>	"field",
+	Dtag =>		"pick tag",
+	Dimport =>	"import",
+	Dunbound =>	"unbound",
+	Dundef =>	"undefined",
+	Dwundef =>	"undefined",
+};
+
+storeart := array[Dend] of
+{
+	Dtype =>	"a ",
+	Dfn =>		"a ",
+	Dglobal =>	"a ",
+	Darg =>		"an ",
+	Dlocal =>	"a ",
+	Dconst =>	"a ",
+	Dfield =>	"a ",
+	Dtag =>		"a ",
+	Dimport =>	"an ",
+	Dunbound =>	"",
+	Dundef =>	"",
+	Dwundef =>	"",
+};
+
+storespace := array[Dend] of
+{
+	Dtype =>	0,
+	Dfn =>		0,
+	Dglobal =>	1,
+	Darg =>		1,
+	Dlocal =>	1,
+	Dconst =>	0,
+	Dfield =>	1,
+	Dtag =>		0,
+	Dimport =>	0,
+	Dunbound =>	0,
+	Dundef =>	0,
+	Dwundef =>	0,
+};
+
+impdecl:	ref Decl;
+impdecls:	ref Dlist;
+scopes :=	array[MaxScope] of ref Decl;
+tails :=	array[MaxScope] of ref Decl;
+scopekind := 	array[MaxScope] of byte;
+scopenode :=	array[MaxScope] of ref Node;
+iota:		ref Decl;
+zdecl:		Decl;
+
+popscopes()
+{
+	d: ref Decl;
+
+	#
+	# clear out any decls left in syms
+	#
+	while(scope >= ScopeBuiltin){
+		for(d = scopes[scope--]; d != nil; d = d.next){
+			if(d.sym != nil){
+				d.sym.decl = d.old;
+				d.old = nil;
+			}
+		}
+	}
+
+	for(id := impdecls; id != nil; id = id.next){
+		for(d = id.d.ty.ids; d != nil; d = d.next){
+			d.sym.decl = nil;
+			d.old = nil;
+		}
+	}
+	impdecls = nil;
+
+	scope = ScopeBuiltin;
+	scopes[ScopeBuiltin] = nil;
+	tails[ScopeBuiltin] = nil;
+}
+
+declstart()
+{
+	iota = mkids(nosrc, enter("iota", 0), tint, nil);
+	iota.init = mkconst(nosrc, big 0);
+
+	scope = ScopeNils;
+	scopes[ScopeNils] = nil;
+	tails[ScopeNils] = nil;
+
+	nildecl = mkdecl(nosrc, Dglobal, tany);
+	nildecl.sym = enter("nil", 0);
+	installids(Dglobal, nildecl);
+	d := mkdecl(nosrc, Dglobal, tstring);
+	d.sym = enterstring("");
+	installids(Dglobal, d);
+
+	scope = ScopeGlobal;
+	scopes[ScopeGlobal] = nil;
+	tails[ScopeGlobal] = nil;
+}
+
+redecl(d: ref Decl)
+{
+	old := d.sym.decl;
+	if(old.store == Dwundef)
+		return;
+	error(d.src.start, "redeclaration of "+declconv(d)+", previously declared as "+storeconv(old)+" on line "+
+		lineconv(old.src.start));
+}
+
+checkrefs(d: ref Decl)
+{
+	id, m: ref Decl;
+	refs: int;
+
+	for(; d != nil; d = d.next){
+		if(d.das != byte 0)
+			d.refs--;
+		case d.store{
+		Dtype =>
+			refs = d.refs;
+			if(d.ty.kind == Tadt){
+				for(id = d.ty.ids; id != nil; id = id.next){
+					d.refs += id.refs;
+					if(id.store != Dfn)
+						continue;
+					if(id.init == nil && id.link == nil && d.importid == nil)
+						error(d.src.start, "function "+d.sym.name+"."+id.sym.name+" not defined");
+					if(superwarn && !id.refs && d.importid == nil)
+						warn(d.src.start, "function "+d.sym.name+"."+id.sym.name+" not referenced");
+				}
+			}
+			if(d.ty.kind == Tmodule){
+				for(id = d.ty.ids; id != nil; id = id.next){
+					refs += id.refs;
+					if(id.iface != nil)
+						id.iface.refs += id.refs;
+					if(id.store == Dtype){
+						for(m = id.ty.ids; m != nil; m = m.next){
+							refs += m.refs;
+							if(m.iface != nil)
+								m.iface.refs += m.refs;
+						}
+					}
+				}
+				d.refs = refs;
+			}
+			if(superwarn && !refs && d.importid == nil)
+				warn(d.src.start, declconv(d)+" not referenced");
+		Dglobal =>
+			if(superwarn && !d.refs && d.sym != nil && d.sym.name[0] != '.')
+				warn(d.src.start, declconv(d)+" not referenced");
+		Dlocal or
+		Darg =>
+			if(!d.refs && d.sym != nil && d.sym.name != nil && d.sym.name[0] != '.')
+				warn(d.src.start, declconv(d)+" not referenced");
+		Dconst =>
+			if(superwarn && !d.refs && d.sym != nil)
+				warn(d.src.start, declconv(d)+" not referenced");
+		Dfn =>
+			if(d.init == nil && d.importid == nil)
+				error(d.src.start, declconv(d)+" not defined");
+			if(superwarn && !d.refs)
+				warn(d.src.start, declconv(d)+" not referenced");
+		Dimport =>
+			if(superwarn && !d.refs)
+				warn(d.src.start, declconv(d)+" not referenced");
+		}
+		if(d.das != byte 0)
+			d.refs++;
+	}
+}
+
+vardecl(ids: ref Decl, t: ref Type): ref Node
+{
+	n := mkn(Ovardecl, mkn(Oseq, nil, nil), nil);
+	n.decl = ids;
+	n.ty = t;
+	return n;
+}
+
+vardecled(n: ref Node)
+{
+	store := Dlocal;
+	if(scope == ScopeGlobal)
+		store = Dglobal;
+	if(n.ty.kind == Texception && n.ty.cons == byte 1){
+		store = Dconst;
+		fatal("Texception in vardecled");
+	}
+	ids := n.decl;
+	installids(store, ids);
+	t := n.ty;
+	for(last := ids; ids != nil; ids = ids.next){
+		ids.ty = t;
+		last = ids;
+	}
+	n.left.decl = last;
+}
+
+condecl(ids: ref Decl, init: ref Node): ref Node
+{
+	n := mkn(Ocondecl, mkn(Oseq, nil, nil), init);
+	n.decl = ids;
+	return n;
+}
+
+condecled(n: ref Node)
+{
+	ids := n.decl;
+	installids(Dconst, ids);
+	for(last := ids; ids != nil; ids = ids.next){
+		ids.ty = tunknown;
+		last = ids;
+	}
+	n.left.decl = last;
+}
+
+exdecl(ids: ref Decl, tids: ref Decl): ref Node
+{
+	n: ref Node;
+	t: ref Type;
+
+	t = mktype(ids.src.start, ids.src.stop, Texception, nil, tids);
+	t.cons = byte 1;
+	n = mkn(Oexdecl, mkn(Oseq, nil, nil), nil);
+	n.decl = ids;
+	n.ty = t;
+	return n;
+}
+
+exdecled(n: ref Node)
+{
+	ids, last: ref Decl;
+	t: ref Type;
+
+	ids = n.decl;
+	installids(Dconst, ids);
+	t = n.ty;
+	for(last = ids; ids != nil; ids = ids.next){
+		ids.ty = t;
+		last = ids;
+	}
+	n.left.decl = last;
+}
+
+importdecl(m: ref Node, ids: ref Decl): ref Node
+{
+	n := mkn(Oimport, mkn(Oseq, nil, nil), m);
+	n.decl = ids;
+	return n;
+}
+
+importdecled(n: ref Node)
+{
+	ids := n.decl;
+	installids(Dimport, ids);
+	for(last := ids; ids != nil; ids = ids.next){
+		ids.ty = tunknown;
+		last = ids;
+	}
+	n.left.decl = last;
+}
+
+mkscope(body: ref Node): ref Node
+{
+	n := mkn(Oscope, nil, body);
+	if(body != nil)
+		n.src = body.src;
+	return n;
+}
+
+fndecl(n: ref Node, t: ref Type, body: ref Node): ref Node
+{
+	n = mkbin(Ofunc, n, body);
+	n.ty = t;
+	return n;
+}
+
+fndecled(n: ref Node)
+{
+	left := n.left;
+	if(left.op == Oname){
+		d := left.decl.sym.decl;
+		if(d == nil || d.store == Dimport){
+			d = mkids(left.src, left.decl.sym, n.ty, nil);
+			installids(Dfn, d);
+		}
+		left.decl = d;
+		d.refs++;
+	}
+	if(left.op == Odot)
+		pushscope(nil, Sother);
+	if(n.ty.polys != nil){
+		pushscope(nil, Sother);
+		installids(Dtype, n.ty.polys);
+	}
+	pushscope(nil, Sother);
+	installids(Darg, n.ty.ids);
+	n.ty.ids = popscope();
+	if(n.ty.val != nil)
+		mergepolydecs(n.ty);
+	if(n.ty.polys != nil)
+		n.ty.polys = popscope();
+	if(left.op == Odot)
+		popscope();
+}
+
+#
+# check the function declaration only
+# the body will be type checked later by fncheck
+#
+fnchk(n: ref Node): ref Decl
+{
+	bad := 0;
+	d := n.left.decl;
+	if(n.left.op == Odot)
+		d = n.left.right.decl;
+	if(d == nil)
+		fatal("decl() fnchk nil");
+	n.left.decl = d;
+	if(d.store == Dglobal || d.store == Dfield)
+		d.store = Dfn;
+	if(d.store != Dfn || d.init != nil){
+		nerror(n, "redeclaration of function "+dotconv(d)+", previously declared as "
+			+storeconv(d)+" on line "+lineconv(d.src.start));
+		if(d.store == Dfn && d.init != nil)
+			bad = 1;
+	}
+	d.init = n;
+
+	t := n.ty;
+	inadt := d.dot;
+	if(inadt != nil && (inadt.store != Dtype || inadt.ty.kind != Tadt))
+		inadt = nil;
+	if(n.left.op == Odot){
+		pushscope(nil, Sother);
+		adtp := outerpolys(n.left);
+		if(adtp != nil)
+			installids(Dtype, adtp);
+		if(!polyequal(adtp, n.decl))
+			nerror(n, "adt polymorphic type mismatch");
+		n.decl = nil;
+	}
+	t = validtype(t, inadt);
+	if(n.left.op == Odot)
+		popscope();
+	if(debug['d'])
+		print("declare function %s ty %s newty %s\n", dotconv(d), typeconv(d.ty), typeconv(t));
+	t = usetype(t);
+
+	if(!polyequal(d.ty.polys, t.polys))
+		nerror(n, "function polymorphic type mismatch");
+	if(!tcompat(d.ty, t, 0))
+		nerror(n, "type mismatch: "+dotconv(d)+" defined as "
+			+typeconv(t)+" declared as "+typeconv(d.ty)+" on line "+lineconv(d.src.start));
+	else if(!raisescompat(d.ty.eraises, t.eraises))
+		nerror(n, "raises mismatch: " + dotconv(d));
+	if(t.varargs != byte 0)
+		nerror(n, "cannot define functions with a '*' argument, such as "+dotconv(d));
+
+	t.eraises = d.ty.eraises;
+
+	d.ty = t;
+	d.offset = idoffsets(t.ids, MaxTemp, IBY2WD);
+	d.src = n.src;
+
+	d.locals = nil;
+
+	n.ty = t;
+
+	if(bad)
+		return nil;
+	return d;
+}
+
+globalas(dst: ref Node, v: ref Node, valok: int): ref Node
+{
+	if(v == nil)
+		return nil;
+	if(v.op == Oas || v.op == Odas){
+		v = globalas(v.left, v.right, valok);
+		if(v == nil)
+			return nil;
+	}else if(valok && !initable(dst, v, 0))
+		return nil;
+	case dst.op{
+	Oname =>
+		if(dst.decl.init != nil)
+			nerror(dst, "duplicate assignment to "+expconv(dst)+", previously assigned on line "
+				+lineconv(dst.decl.init.src.start));
+		if(valok)
+			dst.decl.init = v;
+		return v;
+	Otuple =>
+		if(valok && v.op != Otuple)
+			fatal("can't deal with "+nodeconv(v)+" in tuple case of globalas");
+		tv := v.left;
+		for(dst = dst.left; dst != nil; dst = dst.right){
+			globalas(dst.left, tv.left, valok);
+			if(valok)
+				tv = tv.right;
+		}
+		return v;
+	}
+	fatal("can't deal with "+nodeconv(dst)+" in globalas");
+	return nil;
+}
+
+needsstore(d: ref Decl): int
+{
+	if(!d.refs)
+		return 0;
+	if(d.importid != nil)
+		return 0;
+	if(storespace[d.store])
+		return 1;
+	return 0;
+}
+
+#
+# return the list of all referenced storage variables
+#
+vars(d: ref Decl): ref Decl
+{
+	while(d != nil && !needsstore(d))
+		d = d.next;
+	for(v := d; v != nil; v = v.next){
+		while(v.next != nil){
+			n := v.next;
+			if(needsstore(n))
+				break;
+			v.next = n.next;
+		}
+	}
+	return d;
+}
+
+#
+# declare variables from the left side of a := statement
+#
+recdasdecl(n: ref Node, store: int, nid: int): (int, int)
+{
+	r: int;
+
+	case n.op{
+	Otuple =>
+		ok := 1;
+		for(n = n.left; n != nil; n = n.right){
+			(r, nid) = recdasdecl(n.left, store, nid);
+			ok &= r;
+		}
+		return (ok, nid);
+	Oname =>
+		if(n.decl == nildecl)
+			return (1, -1);
+		d := mkids(n.src, n.decl.sym, nil, nil);
+		installids(store, d);
+		n.decl = d;
+		old := d.old;
+		if(old != nil
+		&& old.store != Dfn
+		&& old.store != Dwundef
+		&& old.store != Dundef)
+			warn(d.src.start, "redeclaration of "+declconv(d)+", previously declared as "
+				+storeconv(old)+" on line "+lineconv(old.src.start));
+		d.refs++;
+		d.das = byte 1;
+		if(nid >= 0)
+			nid++;
+		return (1, nid);
+	}
+	return (0, nid);
+}
+
+recmark(n: ref Node, nid: int): int
+{
+	case(n.op){
+	Otuple =>
+		for(n = n.left; n != nil; n = n.right)
+			nid = recmark(n.left, nid);
+	Oname =>
+		n.decl.nid = byte nid;
+		nid = 0;
+	}
+	return nid;
+}
+
+dasdecl(n: ref Node): int
+{
+	ok: int;
+
+	nid := 0;
+	store := Dlocal;
+	if(scope == ScopeGlobal)
+		store = Dglobal;
+
+	(ok, nid) = recdasdecl(n, store, nid);
+	if(!ok)
+		nerror(n, "illegal declaration expression "+expconv(n));
+	if(ok && store == Dlocal && nid > 1)
+		recmark(n, nid);
+	return ok;
+}
+
+#
+# declare global variables in nested := expressions
+#
+gdasdecl(n: ref Node)
+{
+	if(n == nil)
+		return;
+
+	if(n.op == Odas){
+		gdasdecl(n.right);
+		dasdecl(n.left);
+	}else{
+		gdasdecl(n.left);
+		gdasdecl(n.right);
+	}
+}
+
+undefed(src: Src, s: ref Sym): ref Decl
+{
+	d := mkids(src, s, tnone, nil);
+	error(src.start, s.name+" is not declared");
+	installids(Dwundef, d);
+	return d;
+}
+
+# inloop() : int
+# {
+#	for (i := scope; i > 0; i--)
+#		if (int scopekind[i] == Sloop)
+#			return 1;
+#	return 0;
+# }
+
+nested() : int
+{
+	for (i := scope; i > 0; i--)
+		if (int scopekind[i] == Sscope || int scopekind[i] == Sloop)
+			return 1;
+	return 0;
+}
+
+decltozero(n : ref Node)
+{
+	if ((scop := scopenode[scope]) != nil) {
+		if (n.right != nil && errors == 0)
+			fatal("Ovardecl/Oname/Otuple has right field\n");
+		n.right = scop.left;
+		scop.left = n;
+	}
+}
+
+pushscope(scp : ref Node, kind : int)
+{
+	if(scope >= MaxScope)
+		fatal("scope too deep");
+	scope++;
+	scopes[scope] = nil;
+	tails[scope] = nil;
+	scopenode[scope] = scp;
+	scopekind[scope] = byte kind;
+}
+
+curscope(): ref Decl
+{
+	return scopes[scope];
+}
+
+#
+# revert to old declarations for each symbol in the currect scope.
+# remove the effects of any imported adt types
+# whenever the adt is imported from a module,
+# we record in the type's decl the module to use
+# when calling members.  the process is reversed here.
+#
+popscope(): ref Decl
+{
+	for(id := scopes[scope]; id != nil; id = id.next){
+		if(id.sym != nil){
+			id.sym.decl = id.old;
+			id.old = nil;
+		}
+		if(id.importid != nil)
+			id.importid.refs += id.refs;
+		t := id.ty;
+		if(id.store == Dtype
+		&& t.decl != nil
+		&& t.decl.timport == id)
+			t.decl.timport = id.timport;
+		if(id.store == Dlocal)
+			freeloc(id);
+	}
+	return scopes[scope--];
+}
+
+#
+# make a new scope,
+# preinstalled with some previously installed identifiers
+# don't add the identifiers to the scope chain,
+# so they remain separate from any newly installed ids
+#
+# these routines assume no ids are imports
+#
+repushids(ids: ref Decl)
+{
+	if(scope >= MaxScope)
+		fatal("scope too deep");
+	scope++;
+	scopes[scope] = nil;
+	tails[scope] = nil;
+	scopenode[scope] = nil;
+	scopekind[scope] = byte Sother;
+
+	for(; ids != nil; ids = ids.next){
+		if(ids.scope != scope
+		&& (ids.dot == nil || !isimpmod(ids.dot.sym)
+			|| ids.scope != ScopeGlobal || scope != ScopeGlobal + 1))
+			fatal("repushids scope mismatch");
+		s := ids.sym;
+		if(s != nil && ids.store != Dtag){
+			if(s.decl != nil && s.decl.scope >= scope)
+				ids.old = s.decl.old;
+			else
+				ids.old = s.decl;
+			s.decl = ids;
+		}
+	}
+}
+
+#
+# pop a scope which was started with repushids
+# return any newly installed ids
+#
+popids(ids: ref Decl): ref Decl
+{
+	for(; ids != nil; ids = ids.next){
+		if(ids.sym != nil && ids.store != Dtag){
+			ids.sym.decl = ids.old;
+			ids.old = nil;
+		}
+	}
+	return popscope();
+}
+
+installids(store: int, ids: ref Decl)
+{
+	last : ref Decl = nil;
+	for(d := ids; d != nil; d = d.next){
+		d.scope = scope;
+		if(d.store == Dundef)
+			d.store = store;
+		s := d.sym;
+		if(s != nil){
+			if(s.decl != nil && s.decl.scope >= scope){
+				redecl(d);
+				d.old = s.decl.old;
+			}else
+				d.old = s.decl;
+			s.decl = d;
+		}
+		last = d;
+	}
+	if(ids != nil){
+		d = tails[scope];
+		if(d == nil)
+			scopes[scope] = ids;
+		else
+			d.next = ids;
+		tails[scope] = last;
+	}
+}
+
+lookup(sym: ref Sym): ref Decl
+{
+	s: int;
+	d: ref Decl;
+
+	for(s = scope; s >= ScopeBuiltin; s--){
+		for(d = scopes[s]; d != nil; d = d.next){
+			if(d.sym == sym)
+				return d;
+		}
+	}
+	return nil;
+}
+
+mkids(src: Src, s: ref Sym, t: ref Type, next: ref Decl): ref Decl
+{
+	d := ref zdecl;
+	d.src = src;
+	d.store = Dundef;
+	d.ty = t;
+	d.next = next;
+	d.sym = s;
+	d.nid = byte 1;
+	return d;
+}
+
+mkdecl(src: Src, store: int, t: ref Type): ref Decl
+{
+	d := ref zdecl;
+	d.src = src;
+	d.store = store;
+	d.ty = t;
+	d.nid = byte 1;
+	return d;
+}
+
+dupdecl(old: ref Decl): ref Decl
+{
+	d := ref *old;
+	d.next = nil;
+	return d;
+}
+
+dupdecls(old: ref Decl): ref Decl
+{
+	d, nd, first, last: ref Decl;
+
+	first = last = nil;
+	for(d = old; d != nil; d = d.next){
+		nd = dupdecl(d);
+		if(first == nil)
+			first = nd;
+		else
+			last.next = nd;
+		last = nd;
+	}
+	return first;
+}
+
+appdecls(d: ref Decl, dd: ref Decl): ref Decl
+{
+	if(d == nil)
+		return dd;
+	for(t := d; t.next != nil; t = t.next)
+		;
+	t.next = dd;
+	return d;
+}
+
+revids(id: ref Decl): ref Decl
+{
+	next : ref Decl;
+	d : ref Decl = nil;
+	for(; id != nil; id = next){
+		next = id.next;
+		id.next = d;
+		d = id;
+	}
+	return d;
+}
+
+idoffsets(id: ref Decl, offset: int, al: int): int
+{
+	algn := 1;
+	for(; id != nil; id = id.next){
+		if(storespace[id.store]){
+usedty(id.ty);
+			if(id.store == Dlocal && id.link != nil){
+				# id.nid always 1
+				id.offset = id.link.offset;
+				continue;
+			}
+			a := id.ty.align;
+			if(id.nid > byte 1){
+				for(d := id.next; d != nil && d.nid == byte 0; d = d.next)
+					if(d.ty.align > a)
+						a = d.ty.align;
+				algn = a;
+			}
+			offset = align(offset, a);
+			id.offset = offset;
+			offset += id.ty.size;
+			if(id.nid == byte 0 && (id.next == nil || id.next.nid != byte 0))
+				offset = align(offset, algn);
+		}
+	}
+	return align(offset, al);
+}
+
+idindices(id: ref Decl): int
+{
+	i := 0;
+	for(; id != nil; id = id.next){
+		if(storespace[id.store]){
+			usedty(id.ty);
+			id.offset = i++;
+		}
+	}
+	return i;
+}
+
+declconv(d: ref Decl): string
+{
+	if(d.sym == nil)
+		return storename[d.store] + " " + "<???>";
+	return storename[d.store] + " " + d.sym.name;
+}
+
+storeconv(d: ref Decl): string
+{
+	return storeart[d.store] + storename[d.store];
+}
+
+dotconv(d: ref Decl): string
+{
+	s: string;
+
+	if(d.dot != nil && !isimpmod(d.dot.sym)){
+		s = dotconv(d.dot);
+		if(d.dot.ty != nil && d.dot.ty.kind == Tmodule)
+			s += ".";
+		else
+			s += ".";
+	}
+	s += d.sym.name;
+	return s;
+}
+
+#
+# merge together two sorted lists, yielding a sorted list
+#
+namemerge(e, f: ref Decl): ref Decl
+{
+	d := rock := ref Decl;
+	while(e != nil && f != nil){
+		if(e.sym.name <= f.sym.name){
+			d.next = e;
+			e = e.next;
+		}else{
+			d.next = f;
+			f = f.next;
+		}
+		d = d.next;
+	}
+	if(e != nil)
+		d.next = e;
+	else
+		d.next = f;
+	return rock.next;
+}
+
+#
+# recursively split lists and remerge them after they are sorted
+#
+recnamesort(d: ref Decl, n: int): ref Decl
+{
+	if(n <= 1)
+		return d;
+	m := n / 2 - 1;
+	dd := d;
+	for(i := 0; i < m; i++)
+		dd = dd.next;
+	r := dd.next;
+	dd.next = nil;
+	return namemerge(recnamesort(d, n / 2),
+			recnamesort(r, (n + 1) / 2));
+}
+
+#
+# sort the ids by name
+#
+namesort(d: ref Decl): ref Decl
+{
+	n := 0;
+	for(dd := d; dd != nil; dd = dd.next)
+		n++;
+	return recnamesort(d, n);
+}
+
+printdecls(d: ref Decl)
+{
+	for(; d != nil; d = d.next)
+		print("%d: %s %s ref %d\n", d.offset, declconv(d), typeconv(d.ty), d.refs);
+}
+
+mergepolydecs(t: ref Type)
+{
+	n, nn: ref Node;
+	id, ids, ids1: ref Decl;
+
+	for(n = t.val; n != nil; n = n.right){
+		nn = n.left;
+		for(ids = nn.decl; ids != nil; ids = ids.next){
+			id = ids.sym.decl;
+			if(id == nil){
+				undefed(ids.src, ids.sym);
+				break;
+			}
+			if(id.store != Dtype){
+				error(ids.src.start, declconv(id) + " is not a type");
+				break;
+			}
+			if(id.ty.kind != Tpoly){
+				error(ids.src.start, declconv(id) + " is not a polymorphic type");
+				break;
+			}
+			if(id.ty.ids != nil)
+				error(ids.src.start, declconv(id) + " redefined");
+			pushscope(nil, Sother);
+			fielddecled(nn.left);
+			id.ty.ids = popscope();
+			for(ids1 = id.ty.ids; ids1 != nil; ids1 = ids1.next){
+				ids1.dot = id;
+				bindtypes(ids1.ty);
+				if(ids1.ty.kind != Tfn){
+					error(ids1.src.start, "only function types expected");
+					id.ty.ids = nil;
+				}
+			}
+		}
+	}
+	t.val = nil;
+}
+
+adjfnptrs(d: ref Decl, polys1: ref Decl, polys2: ref Decl)
+{
+	n: int;
+	id, idt, idf, arg: ref Decl;
+
+	n = 0;
+	for(id = d.ty.ids; id != nil; id = id.next)
+		n++;
+	for(idt = polys1; idt != nil; idt = idt.next)
+		for(idf = idt.ty.ids; idf != nil; idf = idf.next)
+			n -= 2;
+	for(idt = polys2; idt != nil; idt = idt.next)
+		for(idf = idt.ty.ids; idf != nil; idf = idf.next)
+			n -= 2;
+	for(arg = d.ty.ids; --n >= 0; arg = arg.next)
+		;
+	for(idt = polys1; idt != nil; idt = idt.next){
+		for(idf = idt.ty.ids; idf != nil; idf = idf.next){
+			idf.link = arg;
+			arg = arg.next.next;
+		}
+	}
+	for(idt = polys2; idt != nil; idt = idt.next){
+		for(idf = idt.ty.ids; idf != nil; idf = idf.next){
+			idf.link = arg;
+			arg = arg.next.next;
+		}
+	}
+}
+
+addptrs(polys: ref Decl, fps: ref Decl, last: ref Decl, link: int, src: Src): (ref Decl, ref Decl)
+{
+	for(idt := polys; idt != nil; idt = idt.next){
+		for(idf := idt.ty.ids; idf != nil; idf = idf.next){
+			fp := mkdecl(src, Darg, tany);
+			fp.sym = idf.sym;
+			if(link)
+				idf.link = fp;
+			if(fps == nil)
+				fps = fp;
+			else
+				last.next = fp;
+			last = fp;
+			fp = mkdecl(src, Darg, tint);
+			fp.sym = idf.sym;
+			last.next = fp;
+			last = fp;
+		}
+	}
+	return (fps, last);
+}
+
+addfnptrs(d: ref Decl, link: int)
+{
+	fps, last, polys: ref Decl;
+
+	polys = encpolys(d);
+	if(int(d.ty.flags&FULLARGS)){
+		if(link)
+			adjfnptrs(d, d.ty.polys, polys);
+		return;
+	}
+	d.ty.flags |= FULLARGS;
+	fps = last = nil;
+	(fps, last) = addptrs(d.ty.polys, fps, last, link, d.src);
+	(fps, last) = addptrs(polys, fps, last, link, d.src);
+	for(last = d.ty.ids; last != nil && last.next != nil; last = last.next)
+		;
+	if(last != nil)
+		last.next = fps;
+	else
+		d.ty.ids = fps;
+	d.offset = idoffsets(d.ty.ids, MaxTemp, IBY2WD);
+}
+
+rmfnptrs(d: ref Decl)
+{
+	n: int;
+	id, idt, idf: ref Decl;
+
+	if(int(d.ty.flags&FULLARGS))
+		d.ty.flags &= ~FULLARGS;
+	else
+		return;
+	n = 0;
+	for(id = d.ty.ids; id != nil; id = id.next)
+		n++;
+	for(idt = d.ty.polys; idt != nil; idt = idt.next)
+		for(idf = idt.ty.ids; idf != nil; idf = idf.next)
+			n -= 2;
+	for(idt = encpolys(d); idt != nil; idt = idt.next)
+		for(idf = idt.ty.ids; idf != nil; idf = idf.next)
+			n -= 2;
+	if(n == 0){
+		d.ty.ids = nil;
+		return;
+	}
+	for(id = d.ty.ids; --n > 0; id = id.next)
+		;
+	id.next = nil;
+	d.offset = idoffsets(d.ty.ids, MaxTemp, IBY2WD);
+}
+
+local(d: ref Decl): int
+{
+	for(d = d.dot; d != nil; d = d.dot)
+		if(d.store == Dtype && d.ty.kind == Tmodule)
+			return 0;
+	return 1;
+}
+
+lmodule(d: ref Decl): ref Decl
+{
+	for(d = d.dot; d != nil; d = d.dot)
+		if(d.store == Dtype && d.ty.kind == Tmodule)
+			return d;
+	return nil;
+}
+
+outerpolys(n: ref Node): ref Decl
+{
+	d: ref Decl;
+
+	if(n.op == Odot){
+		d = n.right.decl;
+		if(d == nil)
+			fatal("decl() outeradt nil");
+		d = d.dot;
+		if(d != nil && d.store == Dtype && d.ty.kind == Tadt)
+			return d.ty.polys;
+	}
+	return nil;
+}
+
+encpolys(d: ref Decl): ref Decl
+{
+	if((d = d.dot) == nil)
+		return nil;
+	return d.ty.polys;
+}
+
+fnlookup(s: ref Sym, t: ref Type): (ref Decl, ref Node)
+{
+	id: ref Decl;
+	mod: ref Node;
+
+	id = nil;
+	mod = nil;
+	if(t.kind == Tpoly || t.kind == Tmodule)
+		id = namedot(t.ids, s);
+	else if(t.kind == Tref){
+		t = t.tof;
+		if(t.kind == Tadt){
+			id = namedot(t.ids, s);
+			if(t.decl != nil && t.decl.timport != nil)
+				mod = t.decl.timport.eimport;
+		}
+		else if(t.kind == Tadtpick){
+			id = namedot(t.ids, s);
+			if(t.decl != nil && t.decl.timport != nil)
+				mod = t.decl.timport.eimport;
+			t = t.decl.dot.ty;
+			if(id == nil)
+				id = namedot(t.ids, s);
+			if(t.decl != nil && t.decl.timport != nil)
+				mod = t.decl.timport.eimport;	
+		}
+	}
+	if(id == nil){
+		id = lookup(s);
+		if(id != nil)
+			mod = id.eimport;
+	}
+	return (id, mod);
+}
+
+isimpmod(s: ref Sym): int
+{
+	d: ref Decl;
+
+	for(d = impmods; d != nil; d = d.next)
+		if(d.sym == s)
+			return 1;
+	return 0;
+}
+
+dequal(d1: ref Decl, d2: ref Decl, full: int): int
+{
+	return	d1.sym == d2.sym &&
+			d1.store == d2.store &&
+			d1.implicit == d2.implicit &&
+			d1.cyc == d2.cyc &&
+			(!full || tequal(d1.ty, d2.ty)) &&
+			(!full || d1.store == Dfn || sametree(d1.init, d2.init));
+}
+
+tzero(t: ref Type): int
+{
+	return t.kind == Texception || tmustzero(t);
+}
+
+isptr(t: ref Type): int
+{
+	return t.kind == Texception || tattr[t.kind].isptr;
+}
+
+# can d share the same stack location as another local ?
+shareloc(d: ref Decl)
+{
+	z: int;
+	t, tt: ref Type;
+	dd, res: ref Decl;
+
+	if(d.store != Dlocal || d.nid != byte 1)
+		return;
+	t = d.ty;
+	res = nil;
+	for(dd = fndecls; dd != nil; dd = dd.next){
+		if(d == dd)
+			fatal("d==dd in shareloc");
+		if(dd.store != Dlocal || dd.nid != byte 1 || dd.link != nil || dd.tref != 0)
+			continue;
+		tt = dd.ty;
+		if(t.size != tt.size || t.align != tt.align)
+			continue;
+		z = tzero(t)+tzero(tt);
+		if(z > 0)
+			continue;	# for now
+		if(t == tt || tequal(t, tt))
+			res = dd;
+		else{
+			if(z == 1)
+				continue;
+			if(z == 0 || isptr(t) || isptr(tt) || mktdesc(t) == mktdesc(tt))
+				res = dd;
+		}
+		if(res != nil){
+			d.link = res;
+			res.tref = 1;
+			return;
+		}
+	}
+	return;
+}
+
+freeloc(d: ref Decl)
+{
+	if(d.link != nil)
+		d.link.tref = 0;
+}
--- /dev/null
+++ b/appl/cmd/limbo/ecom.b
@@ -1,0 +1,2345 @@
+maxstack:	int;				# max size of a stack frame called
+
+precasttab := array[Tend] of array of ref Type;
+
+optabinit()
+{
+	ct := array[Tend] of ref Type;
+	for(i := 0; i < Tend; i++)
+		precasttab[i] = ct;
+	precasttab[Tstring] = array[Tend] of { Tbyte => tint, Tfix => treal, };
+	precasttab[Tbig] = array[Tend] of { Tbyte => tint, Tfix => treal, };
+	precasttab[Treal] = array[Tend] of { Tbyte => tint, };
+	precasttab[Tfix] = array[Tend] of { Tbyte => tint, Tstring => treal, Tbig => treal, };
+	precasttab[Tbyte] = array[Tend] of { Tstring => tint, Tbig => tint, Treal => tint, Tfix => tint, };
+
+	casttab = array[Tend] of { * => array[Tend] of {* => 0}};
+
+	casttab[Tint][Tint] = IMOVW;
+	casttab[Tbig][Tbig] = IMOVL;
+	casttab[Treal][Treal] = IMOVF;
+	casttab[Tbyte][Tbyte] = IMOVB;
+	casttab[Tstring][Tstring] = IMOVP;
+	casttab[Tfix][Tfix] = ICVTXX;	# never same type
+
+	casttab[Tint][Tbyte] = ICVTWB;
+	casttab[Tint][Treal] = ICVTWF;
+	casttab[Tint][Tstring] = ICVTWC;
+	casttab[Tint][Tfix] = ICVTXX;
+	casttab[Tbyte][Tint] = ICVTBW;
+	casttab[Treal][Tint] = ICVTFW;
+	casttab[Tstring][Tint] = ICVTCW;
+	casttab[Tfix][Tint] = ICVTXX;
+
+	casttab[Tint][Tbig] = ICVTWL;
+	casttab[Treal][Tbig] = ICVTFL;
+	casttab[Tstring][Tbig] = ICVTCL;
+	casttab[Tbig][Tint] = ICVTLW;
+	casttab[Tbig][Treal] = ICVTLF;
+	casttab[Tbig][Tstring] = ICVTLC;
+
+	casttab[Treal][Tstring] = ICVTFC;
+	casttab[Tstring][Treal] = ICVTCF;
+
+	casttab[Treal][Tfix] = ICVTFX;
+	casttab[Tfix][Treal] = ICVTXF;
+
+	casttab[Tstring][Tarray] = ICVTCA;
+	casttab[Tarray][Tstring] = ICVTAC;
+
+	#
+	# placeholders; fixed in precasttab
+	#
+	casttab[Tbyte][Tstring] = 16rff;
+	casttab[Tstring][Tbyte] = 16rff;
+	casttab[Tbyte][Treal] = 16rff;
+	casttab[Treal][Tbyte] = 16rff;
+	casttab[Tbyte][Tbig] = 16rff;
+	casttab[Tbig][Tbyte] = 16rff;
+	casttab[Tfix][Tbyte] = 16rff;
+	casttab[Tbyte][Tfix] = 16rff;
+	casttab[Tfix][Tbig] = 16rff;
+	casttab[Tbig][Tfix] = 16rff;
+	casttab[Tfix][Tstring] = 16rff;
+	casttab[Tstring][Tfix] = 16rff;
+}
+
+#
+# global variable and constant initialization checking
+#
+vcom(ids: ref Decl): int
+{
+	ok := 1;
+	for(v := ids; v != nil; v = v.next)
+		ok &= varcom(v);
+	for(v = ids; v != nil; v = v.next)
+		v.init = simplify(v.init);
+	return ok;
+}
+
+simplify(n: ref Node): ref Node
+{
+	if(n == nil)
+		return nil;
+	if(debug['F'])
+		print("simplify %s\n", nodeconv(n));
+	n = efold(rewrite(n));
+	if(debug['F'])
+		print("simplified %s\n", nodeconv(n));
+	return n;
+}
+
+isfix(n: ref Node): int
+{
+	if(n.ty.kind == Tint || n.ty.kind == Tfix){
+		if(n.op == Ocast)
+			return n.left.ty.kind == Tint || n.left.ty.kind == Tfix;
+		return 1;
+	}
+	return 0;
+}
+
+#
+# rewrite an expression to make it easiser to compile,
+# or give the correct results
+#
+rewrite(n: ref Node): ref Node
+{
+	v: big;
+	t: ref Type;
+	d: ref Decl;
+	nn: ref Node;
+
+	if(n == nil)
+		return nil;
+
+	left := n.left;
+	right := n.right;
+
+	#
+	# rewrites
+	#
+	case n.op{
+	Oname =>
+		d = n.decl;
+		if(d.importid != nil){
+			left = mkbin(Omdot, dupn(1, n.src, d.eimport), mkdeclname(n.src, d.importid));
+			left.ty = n.ty;
+			return rewrite(left);
+		}
+		if((t = n.ty).kind == Texception){
+			if(int t.cons)
+				fatal("cons in rewrite Oname");
+			n = mkbin(Oadd, n, mkconst(n.src, big(2*IBY2WD)));
+			n = mkunary(Oind, n);
+			n.ty = t;
+			n.left.ty = n.left.left.ty = tint;
+			return rewrite(n);
+		}
+	Odas =>
+		n.op = Oas;
+		return rewrite(n);
+	Oneg =>
+		n.left = rewrite(left);
+		if(n.ty == treal)
+			break;
+		left = n.left;
+		n.right = left;
+		n.left = mkconst(n.src, big 0);
+		n.left.ty = n.ty;
+		n.op = Osub;
+	Ocomp =>
+		v = big 0;
+		v = ~v;
+		n.right = mkconst(n.src, v);
+		n.right.ty = n.ty;
+		n.left = rewrite(left);
+		n.op = Oxor;
+	Oinc or
+	Odec or
+	Opreinc or
+	Opredec =>
+		n.left = rewrite(left);
+		case n.ty.kind{
+		Treal =>
+			n.right = mkrconst(n.src, 1.0);
+		Tint or
+		Tbig or
+		Tbyte or
+		Tfix =>
+			n.right = mkconst(n.src, big 1);
+			n.right.ty = n.ty;
+		* =>
+			fatal("can't rewrite inc/dec "+nodeconv(n));
+		}
+		if(n.op == Opreinc)
+			n.op = Oaddas;
+		else if(n.op == Opredec)
+			n.op = Osubas;
+	Oslice =>
+		if(right.left.op == Onothing)
+			right.left = mkconst(right.left.src, big 0);
+		n.left = rewrite(left);
+		n.right = rewrite(right);
+	Oindex =>
+		n.op = Oindx;
+		n.left = rewrite(left);
+		n.right = rewrite(right);
+		n = mkunary(Oind, n);
+		n.ty = n.left.ty;
+		n.left.ty = tint;
+	Oload =>
+		n.right = mkn(Oname, nil, nil);
+		n.right.src = n.left.src;
+		n.right.decl = n.ty.tof.decl;
+		n.right.ty = n.ty;
+		n.left = rewrite(left);
+	Ocast =>
+		if(left.ty.kind == Texception){
+			n = rewrite(left);
+			break;
+		}
+		n.op = Ocast;
+		t = precasttab[left.ty.kind][n.ty.kind];
+		if(t != nil){
+			n.left = mkunary(Ocast, left);
+			n.left.ty = t;
+			return rewrite(n);
+		}
+		n.left = rewrite(left);
+	Oraise =>
+		if(left.ty == tstring)
+			;
+		else if(left.ty.cons == byte 0)
+			break;
+		else if(left.op != Ocall || left.left.ty.kind == Tfn){
+			left = mkunary(Ocall, left);
+			left.ty = left.left.ty;
+		}
+		n.left = rewrite(left);
+	Ocall =>
+		t = left.ty;
+		if(t.kind == Tref)
+			t = t.tof;
+		if(t.kind == Tfn){
+			if(left.ty.kind == Tref){	# call by function reference
+				n.left = mkunary(Oind, left);
+				n.left.ty = t;
+				return rewrite(n);
+			}
+			d = nil;
+			if(left.op == Oname)
+				d = left.decl;
+			else if(left.op == Omdot && left.right.op == Odot)
+				d = left.right.right.decl;
+			else if(left.op == Omdot || left.op == Odot)
+				d = left.right.decl;
+			else if(left.op != Oind)
+				fatal("cannot deal with call " + nodeconv(n) + " in rewrite");
+			if(ispoly(d))
+				addfnptrs(d, 0);
+			n.left = rewrite(left);
+			if(right != nil)
+				n.right = rewrite(right);
+			if(d != nil && int d.inline == 1)
+				n = simplify(inline(n));
+			break;
+		}
+		case n.ty.kind{
+		Tref =>
+			n = mkunary(Oref, n);
+			n.ty = n.left.ty;
+			n.left.ty = n.left.ty.tof;
+			n.left.left.ty = n.left.ty;
+			return rewrite(n);
+		Tadt =>
+			n.op = Otuple;
+			n.right = nil;
+			if(n.ty.tags != nil){
+				n.left = nn = mkunary(Oseq, mkconst(n.src, big left.right.decl.tag));
+				if(right != nil){
+					nn.right = right;
+					nn.src.stop = right.src.stop;
+				}
+				n.ty = left.right.decl.ty.tof;
+			}else
+				n.left = right;
+			return rewrite(n);
+		Tadtpick =>
+			n.op = Otuple;
+			n.right = nil;
+			n.left = nn = mkunary(Oseq, mkconst(n.src, big left.right.decl.tag));
+			if(right != nil){
+				nn.right = right;
+				nn.src.stop = right.src.stop;
+			}
+			n.ty = left.right.decl.ty.tof;
+			return rewrite(n);
+		Texception =>
+			if(n.ty.cons == byte 0)
+				return n.left;
+			if(left.op == Omdot){
+				left.right.ty = left.ty;
+				left = left.right;
+			}
+			n.op = Otuple;
+			n.right = nil;
+			n.left = nn = mkunary(Oseq, left.decl.init);
+			nn.right = mkunary(Oseq, mkconst(n.src, big 0));
+			nn.right.right = right;
+			n.ty = mkexbasetype(n.ty);
+			n = mkunary(Oref, n);
+			n.ty = internaltype(mktype(n.src.start, n.src.stop, Tref, t, nil));
+			return rewrite(n);
+		* =>
+			fatal("can't deal with "+nodeconv(n)+" in rewrite/Ocall");
+		}
+	Omdot =>
+		#
+		# what about side effects from left?
+		#
+		d = right.decl;
+		case d.store{
+		Dfn =>
+			n.left = rewrite(left);
+			if(right.op == Odot){
+				n.right = dupn(1, left.src, right.right);
+				n.right.ty = d.ty;
+			}
+		Dconst or
+		Dtag or
+		Dtype =>
+			# handled by fold
+			return n;
+		Dglobal =>
+			right.op = Oconst;
+			right.c = ref Const(big d.offset, 0.);
+			right.ty = tint;
+
+			n.left = left = mkunary(Oind, left);
+			left.ty = tint;
+			n.op = Oadd;
+			n = mkunary(Oind, n);
+			n.ty = n.left.ty;
+			n.left.ty = tint;
+			n.left = rewrite(n.left);
+			return n;
+		Darg =>
+			return n;
+		* =>
+			fatal("can't deal with "+nodeconv(n)+" in rewrite/Omdot");
+		}
+	Odot =>
+		#
+		# what about side effects from left?
+		#
+		d = right.decl;
+		case d.store{
+		Dfn =>
+			if(right.left != nil){
+				n = mkbin(Omdot, dupn(1, left.src, right.left), right);
+				right.left = nil;
+				n.ty = d.ty;
+				return rewrite(n);
+			}
+			if(left.ty.kind == Tpoly){
+				n = mkbin(Omdot, mkdeclname(left.src, d.link), mkdeclname(left.src, d.link.next));
+				n.ty = d.ty;
+				return rewrite(n);
+			}
+			n.op = Oname;
+			n.decl = d;
+			n.right = nil;
+			n.left = nil;
+			return n;
+		Dconst or
+		Dtag or
+		Dtype =>
+			# handled by fold
+			return n;
+		}
+		if(istuple(left))
+			return n;	# handled by fold
+		right.op = Oconst;
+		right.c = ref Const(big d.offset, 0.);
+		right.ty = tint;
+
+		if(left.ty.kind != Tref){
+			n.left = mkunary(Oadr, left);
+			n.left.ty = tint;
+		}
+		n.op = Oadd;
+		n = mkunary(Oind, n);
+		n.ty = n.left.ty;
+		n.left.ty = tint;
+		n.left = rewrite(n.left);
+		return n;
+	Oadr =>
+		left = rewrite(left);
+		n.left = left;
+		if(left.op == Oind)
+			return left.left;
+	Otagof =>
+		if(n.decl == nil){
+			n.op = Oind;
+			return rewrite(n);
+		}
+		return n;
+	Omul or
+	Odiv =>
+		left = n.left = rewrite(left);
+		right = n.right = rewrite(right);
+		if(n.ty.kind == Tfix && isfix(left) && isfix(right)){
+			if(left.op == Ocast && tequal(left.ty, n.ty))
+				n.left = left.left;
+			if(right.op == Ocast && tequal(right.ty, n.ty))
+				n.right = right.left;
+		}
+	Oself =>
+		if(newfnptr)
+			return n;
+		if(selfdecl == nil){
+			d = selfdecl = mkids(n.src, enter(".self", 5), tany, nil);
+			installids(Dglobal, d);
+			d.refs++;
+		}
+		nn = mkn(Oload, nil, nil);
+		nn.src = n.src;
+		nn.left = mksconst(n.src, enterstring("$self"));
+		nn.ty = impdecl.ty;
+		usetype(nn.ty);
+		usetype(nn.ty.tof);
+		nn = rewrite(nn);
+		nn.op = Oself;
+		return nn;
+	Ofnptr =>
+		if(n.flags == byte 0){
+			# module
+			if(left == nil)
+				left = mkn(Oself, nil, nil);
+			return rewrite(left);
+		}
+		right.flags = n.flags;
+		n = right;
+		d = n.decl;
+		if(int n.flags == FNPTR2){
+			if(left != nil && left.op != Oname)
+				fatal("not Oname for addiface");
+			if(left == nil){
+				addiface(nil, d);
+				if(newfnptr)
+					n.flags |= byte FNPTRN;
+			}
+			else
+				addiface(left.decl, d);
+			n.ty = tint;
+			return n;
+		}
+		if(int n.flags == FNPTRA){
+			n = mkdeclname(n.src, d.link);
+			n.ty = tany;
+			return n;
+		}
+		if(int n.flags == (FNPTRA|FNPTR2)){
+			n = mkdeclname(n.src, d.link.next);
+			n.ty = tint;
+			return n;
+		}
+	Ochan =>
+		if(left == nil)
+			left = n.left = mkconst(n.src, big 0);
+		n.left = rewrite(left);
+	* =>
+		n.left = rewrite(left);
+		n.right = rewrite(right);
+	}
+
+	return n;
+}
+
+#
+# label a node with sethi-ullman numbers and addressablity
+# genaddr interprets addable to generate operands,
+# so a change here mandates a change there.
+#
+# addressable:
+#	const			Rconst	$value		 may also be Roff or Rdesc or Rnoff
+#	Asmall(local)		Rreg	value(FP)
+#	Asmall(global)		Rmreg	value(MP)
+#	ind(Rareg)		Rreg	value(FP)
+#	ind(Ramreg)		Rmreg	value(MP)
+#	ind(Rreg)		Radr	*value(FP)
+#	ind(Rmreg)		Rmadr	*value(MP)
+#	ind(Raadr)		Radr	value(value(FP))
+#	ind(Ramadr)		Rmadr	value(value(MP))
+#
+# almost addressable:
+#	adr(Rreg)		Rareg
+#	adr(Rmreg)		Ramreg
+#	add(const, Rareg)	Rareg
+#	add(const, Ramreg)	Ramreg
+#	add(const, Rreg)	Raadr
+#	add(const, Rmreg)	Ramadr
+#	add(const, Raadr)	Raadr
+#	add(const, Ramadr)	Ramadr
+#	adr(Radr)		Raadr
+#	adr(Rmadr)		Ramadr
+#
+# strangely addressable:
+#	fn			Rpc
+#	mdot(module,exp)	Rmpc
+#
+sumark(n: ref Node): ref Node
+{
+	if(n == nil)
+		return nil;
+
+	n.temps = byte 0;
+	n.addable = Rcant;
+
+	left := n.left;
+	right := n.right;
+	if(left != nil){
+		sumark(left);
+		n.temps = left.temps;
+	}
+	if(right != nil){
+		sumark(right);
+		if(right.temps == n.temps)
+			n.temps++;
+		else if(right.temps > n.temps)
+			n.temps = right.temps;
+	}
+
+	case n.op{
+	Oadr =>
+		case int left.addable{
+		int Rreg =>
+			n.addable = Rareg;
+		int Rmreg =>
+			n.addable = Ramreg;
+		int Radr =>
+			n.addable = Raadr;
+		int Rmadr =>
+			n.addable = Ramadr;
+		}
+	Oind =>
+		case int left.addable{
+		int Rreg =>
+			n.addable = Radr;
+		int Rmreg =>
+			n.addable = Rmadr;
+		int Rareg =>
+			n.addable = Rreg;
+		int Ramreg =>
+			n.addable = Rmreg;
+		int Raadr =>
+			n.addable = Radr;
+		int Ramadr =>
+			n.addable = Rmadr;
+		}
+	Oname =>
+		case n.decl.store{
+		Darg or
+		Dlocal =>
+			n.addable = Rreg;
+		Dglobal =>
+			n.addable = Rmreg;
+			if(LDT && n.decl.ty.kind == Tiface)
+				n.addable = Rldt;
+		Dtype =>
+			#
+			# check for inferface to load
+			#
+			if(n.decl.ty.kind == Tmodule)
+				n.addable = Rmreg;
+		Dfn =>
+			if(int n.flags & FNPTR){
+				if(int n.flags == FNPTR2)
+					n.addable = Roff;
+				else if(int n.flags == (FNPTR2|FNPTRN))
+					n.addable = Rnoff;
+			}
+			else
+				n.addable = Rpc;
+		* =>
+			fatal("cannot deal with "+declconv(n.decl)+" in Oname in "+nodeconv(n));
+		}
+	Omdot =>
+		n.addable = Rmpc;
+	Oconst =>
+		case n.ty.kind{
+		Tint or
+		Tfix =>
+			v := int n.c.val;
+			if(v < 0 && ((v >> 29) & 7) != 7
+			|| v > 0 && (v >> 29) != 0){
+				n.decl = globalconst(n);
+				n.addable = Rmreg;
+			}else
+				n.addable = Rconst;
+		Tbig =>
+			n.decl = globalBconst(n);
+			n.addable = Rmreg;
+		Tbyte =>
+			n.decl = globalbconst(n);
+			n.addable = Rmreg;
+		Treal =>
+			n.decl = globalfconst(n);
+			n.addable = Rmreg;
+		Tstring =>
+			n.decl = globalsconst(n);
+			n.addable = Rmreg;
+		* =>
+			fatal("cannot const in sumark "+typeconv(n.ty));
+		}
+	Oadd =>
+		if(right.addable == Rconst){
+			case int left.addable{
+			int Rareg =>
+				n.addable = Rareg;
+			int Ramreg =>
+				n.addable = Ramreg;
+			int Rreg or
+			int Raadr =>
+				n.addable = Raadr;
+			int Rmreg or
+			int Ramadr =>
+				n.addable = Ramadr;
+			}
+		}
+	}
+	if(n.addable < Rcant)
+		n.temps = byte 0;
+	else if(n.temps == byte 0)
+		n.temps = byte 1;
+	return n;
+}
+
+mktn(t: ref Type): ref Node
+{
+	n := mkn(Oname, nil, nil);
+	usedesc(mktdesc(t));
+	n.ty = t;
+	if(t.decl == nil)
+		fatal("mktn nil decl t "+typeconv(t));
+	n.decl = t.decl;
+	n.addable = Rdesc;
+	return n;
+}
+
+# does a tuple of the form (a, b, ...) form a contiguous block
+# of memory on the stack when offsets are assigned later
+# - only when (a, b, ...) := rhs and none of the names nil
+# can we guarantee this
+#
+tupblk0(n: ref Node, d: ref Decl): (int, ref Decl)
+{
+	ok, nid: int;
+
+	case(n.op){
+	Otuple =>
+		for(n = n.left; n != nil; n = n.right){
+			(ok, d) = tupblk0(n.left, d);
+			if(!ok)
+				return (0, nil);
+		}
+		return (1, d);
+	Oname =>
+		if(n.decl == nildecl)
+			return (0, nil);
+		if(d != nil && d.next != n.decl)
+			return (0, nil);
+		nid = int n.decl.nid;
+		if(d == nil && nid == 1)
+			return (0, nil);
+		if(d != nil && nid != 0)
+			return (0, nil);
+		return (1, n.decl);
+	}
+	return (0, nil);
+}
+
+# could force locals to be next to each other
+# - need to shuffle locals list
+# - later
+#
+tupblk(n: ref Node): ref Node
+{
+	ok: int;
+	d: ref Decl;
+
+	if(n.op != Otuple)
+		return nil;
+	d = nil;
+	(ok, d) = tupblk0(n, d);
+	if(!ok)
+		return nil;
+	while(n.op == Otuple)
+		n = n.left.left;
+	if(n.op != Oname || n.decl.nid == byte 1)
+		fatal("bad tupblk");
+	return n;
+}
+	
+# for cprof
+esrc(src: Src, osrc: Src, nto: ref Node): Src
+{
+	if(nto != nil && src.start != 0 && src.stop != 0)
+		return src;
+	return osrc;
+}
+
+#
+# compile an expression with an implicit assignment
+# note: you are not allowed to use nto.src
+#
+# need to think carefully about the types used in moves
+#
+ecom(src: Src, nto, n: ref Node): ref Node
+{
+	tleft, tright, tto, ttn: ref Node;
+	t: ref Type;
+	p: ref Inst;
+
+	if(debug['e']){
+		print("ecom: %s\n", nodeconv(n));
+		if(nto != nil)
+			print("ecom nto: %s\n", nodeconv(nto));
+	}
+
+	if(n.addable < Rcant){
+		#
+		# think carefully about the type used here
+		#
+		if(nto != nil)
+			genmove(src, Mas, n.ty, n, nto);
+		return nto;
+	}
+
+	left := n.left;
+	right := n.right;
+	op := n.op;
+	case op{
+	* =>
+		fatal("can't ecom "+nodeconv(n));
+		return nto;
+	Oif =>
+		p = bcom(left, 1, nil);
+		ecom(right.left.src, nto, right.left);
+		if(right.right != nil){
+			pp := p;
+			p = genrawop(right.left.src, IJMP, nil, nil, nil);
+			patch(pp, nextinst());
+			ecom(right.right.src, nto, right.right);
+		}
+		patch(p, nextinst());
+	Ocomma =>
+		ttn = left.left;
+		ecom(left.src, nil, left);
+		ecom(right.src, nto, right);
+		tfree(ttn);
+	Oname =>
+		if(n.addable == Rpc){
+			if(nto != nil)
+				genmove(src, Mas, n.ty, n, nto);
+			return nto;
+		}
+		fatal("can't ecom "+nodeconv(n));
+	Onothing =>
+		break;
+	Oused =>
+		if(nto != nil)
+			fatal("superflous used "+nodeconv(left)+" nto "+nodeconv(nto));
+		tto = talloc(left.ty, nil);
+		ecom(left.src, tto, left);
+		tfree(tto);
+	Oas =>
+		if(right.ty == tany)
+			right.ty = n.ty;
+		if(left.op == Oname && left.decl.ty == tany){
+			if(nto == nil)
+				nto = tto = talloc(right.ty, nil);
+			left = nto;
+			nto = nil;
+		}
+		if(left.op == Oinds){
+			indsascom(src, nto, n);
+			tfree(tto);
+			break;
+		}
+		if(left.op == Oslice){
+			slicelcom(src, nto, n);
+			tfree(tto);
+			break;
+		}
+
+		if(left.op == Otuple){
+			if(!tupsaliased(right, left)){
+				if((tn := tupblk(left)) != nil){
+					tn.ty = n.ty;
+					ecom(n.right.src, tn, right);
+					if(nto != nil)
+						genmove(src, Mas, n.ty, tn, nto);
+					tfree(tto);
+					break;
+				}
+				if((tn = tupblk(right)) != nil){
+					tn.ty = n.ty;
+					tuplcom(tn, left);
+					if(nto != nil)
+						genmove(src, Mas, n.ty, tn, nto);
+					tfree(tto);
+					break;
+				}
+				if(nto == nil && right.op == Otuple && left.ty.kind != Tadtpick){
+					tuplrcom(right, left);
+					tfree(tto);
+					break;
+				}
+			}
+			if(right.addable >= Ralways
+			|| right.op != Oname
+			|| tupaliased(right, left)){
+				tright = talloc(n.ty, nil);
+				ecom(n.right.src, tright, right);
+				right = tright;
+			}
+			tuplcom(right, n.left);
+			if(nto != nil)
+				genmove(src, Mas, n.ty, right, nto);
+			tfree(tright);
+			tfree(tto);
+			break;
+		}
+
+		#
+		# check for left/right aliasing and build right into temporary
+		#
+		if(right.op == Otuple){
+			if(!tupsaliased(left, right) && (tn := tupblk(right)) != nil){
+				tn.ty = n.ty;
+				right = tn;
+			}
+			else if(left.op != Oname || tupaliased(left, right))
+				right = ecom(right.src, tright = talloc(right.ty, nil), right);
+		}
+
+		#
+		# think carefully about types here
+		#
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		ecom(n.src, left, right);
+		if(nto != nil)
+			genmove(src, Mas, nto.ty, left, nto);
+		tfree(tleft);
+		tfree(tright);
+		tfree(tto);
+	Ochan =>
+		if(left != nil && left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		genchan(src, left, n.ty.tof, nto);
+		tfree(tleft);
+	Oinds =>
+		if(right.addable < Ralways){
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nil);
+		}else if(left.temps <= right.temps){
+			right = ecom(right.src, tright = talloc(right.ty, nil), right);
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nil);
+		}else{
+			(left, tleft) = eacom(left, nil);
+			right = ecom(right.src, tright = talloc(right.ty, nil), right);
+		}
+		genop(n.src, op, left, right, nto);
+		tfree(tleft);
+		tfree(tright);
+	Osnd =>
+		if(right.addable < Rcant){
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nto);
+		}else if(left.temps < right.temps){
+			(right, tright) = eacom(right, nto);
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nil);
+		}else{
+			(left, tleft) = eacom(left, nto);
+			(right, tright) = eacom(right, nil);
+		}
+		p = genrawop(n.src, ISEND, right, nil, left);
+		p.m.offset = n.ty.size;	# for optimizer
+		if(nto != nil)
+			genmove(src, Mas, right.ty, right, nto);
+		tfree(tleft);
+		tfree(tright);
+	Orcv =>
+		if(nto == nil){
+			ecom(n.src, tto = talloc(n.ty, nil), n);
+			tfree(tto);
+			return nil;
+		}
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		if(left.ty.kind == Tchan){
+			p = genrawop(src, IRECV, left, nil, nto);
+			p.m.offset = n.ty.size;	# for optimizer
+		}else{
+			recvacom(src, nto, n);
+		}
+		tfree(tleft);
+	Ocons =>
+		#
+		# another temp which can go with analysis
+		#
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nil);
+		if(!sameaddr(right, nto)){
+			ecom(right.src, tto = talloc(n.ty, nto), right);
+			genmove(src, Mcons, left.ty, left, tto);
+			if(!sameaddr(tto, nto))
+				genmove(src, Mas, nto.ty, tto, nto);
+		}else
+			genmove(src, Mcons, left.ty, left, nto);
+		tfree(tleft);
+		tfree(tto);
+	Ohd =>
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		genmove(src, Mhd, nto.ty, left, nto);
+		tfree(tleft);
+	Otl =>
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		genmove(src, Mtl, left.ty, left, nto);
+		tfree(tleft);
+	Otuple =>
+		if((tn := tupblk(n)) != nil){
+			tn.ty = n.ty;
+			genmove(src, Mas, n.ty, tn, nto);
+			break;
+		}
+		tupcom(nto, n);
+	Oadd or
+	Osub or
+	Omul or
+	Odiv or
+	Omod or
+	Oand or
+	Oor or
+	Oxor or
+	Olsh or
+	Orsh or
+	Oexp =>
+		#
+		# check for 2 operand forms
+		#
+		if(sameaddr(nto, left)){
+			if(right.addable >= Rcant)
+				(right, tright) = eacom(right, nto);
+			genop(src, op, right, nil, nto);
+			tfree(tright);
+			break;
+		}
+
+		if(opcommute[op] && sameaddr(nto, right) && n.ty != tstring){
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nto);
+			genop(src, opcommute[op], left, nil, nto);
+			tfree(tleft);
+			break;
+		}
+
+		if(right.addable < left.addable
+		&& opcommute[op]
+		&& n.ty != tstring){
+			op = opcommute[op];
+			left = right;
+			right = n.left;
+		}
+		if(left.addable < Ralways){
+			if(right.addable >= Rcant)
+				(right, tright) = eacom(right, nto);
+		}else if(right.temps <= left.temps){
+			left = ecom(left.src, tleft = talloc(left.ty, nto), left);
+			if(right.addable >= Rcant)
+				(right, tright) = eacom(right, nil);
+		}else{
+			(right, tright) = eacom(right, nto);
+			left = ecom(left.src, tleft = talloc(left.ty, nil), left);
+		}
+
+		#
+		# check for 2 operand forms
+		#
+		if(sameaddr(nto, left))
+			genop(src, op, right, nil, nto);
+		else if(opcommute[op] && sameaddr(nto, right) && n.ty != tstring)
+			genop(src, opcommute[op], left, nil, nto);
+		else
+			genop(src, op, right, left, nto);
+		tfree(tleft);
+		tfree(tright);
+	Oaddas or
+	Osubas or
+	Omulas or
+	Odivas or
+	Omodas or
+	Oexpas or
+	Oandas or
+	Ooras or
+	Oxoras or
+	Olshas or
+	Orshas =>
+		if(left.op == Oinds){
+			indsascom(src, nto, n);
+			break;
+		}
+		if(right.addable < Rcant){
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nto);
+		}else if(left.temps < right.temps){
+			(right, tright) = eacom(right, nto);
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nil);
+		}else{
+			(left, tleft) = eacom(left, nto);
+			(right, tright) = eacom(right, nil);
+		}
+		genop(n.src, op, right, nil, left);
+		if(nto != nil)
+			genmove(src, Mas, left.ty, left, nto);
+		tfree(tleft);
+		tfree(tright);
+	Olen =>
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		op = -1;
+		t = left.ty;
+		if(t == tstring)
+			op = ILENC;
+		else if(t.kind == Tarray)
+			op = ILENA;
+		else if(t.kind == Tlist)
+			op = ILENL;
+		else
+			fatal("can't len "+nodeconv(n));
+		genrawop(src, op, left, nil, nto);
+		tfree(tleft);
+	Oneg =>
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		genop(n.src, op, left, nil, nto);
+		tfree(tleft);
+	Oinc or
+	Odec =>
+		if(left.op == Oinds){
+			indsascom(src, nto, n);
+			break;
+		}
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nil);
+		if(nto != nil)
+			genmove(src, Mas, left.ty, left, nto);
+		if(right.addable >= Rcant)
+			fatal("inc/dec amount not addressable: "+nodeconv(n));
+		genop(n.src, op, right, nil, left);
+		tfree(tleft);
+	Ospawn =>
+		if(left.left.op == Oind)
+			fpcall(n.src, op, left, nto);
+		else
+			callcom(n.src, op, left, nto);
+	Oraise =>
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nil);
+		genrawop(n.src, IRAISE, left, nil, nil);
+		tfree(tleft);
+	Ocall =>
+		if(left.op == Oind)
+			fpcall(esrc(src, n.src, nto), op, n, nto);
+		else
+			callcom(esrc(src, n.src, nto), op, n, nto);
+	Oref =>
+		t = left.ty;
+		if(left.op == Oname && left.decl.store == Dfn || left.op == Omdot && left.right.op == Oname && left.right.decl.store == Dfn){	# create a function reference
+			mod, ind: ref Node;
+
+			d := left.decl;
+			if(left.op == Omdot){
+				d = left.right.decl;
+				mod = left.left;
+			}
+			else if(d.eimport != nil)
+				mod = d.eimport;
+			else{
+				mod = rewrite(mkn(Oself, nil, nil));
+				addiface(nil, d);
+			}
+			sumark(mod);
+			tto = talloc(n.ty, nto);
+			genrawop(src, INEW, mktn(usetype(tfnptr)), nil, tto);
+			tright = ref znode;
+			tright.src = src;
+			tright.op = Oind;
+			tright.left = tto;
+			tright.right = nil;
+			tright.ty = tany;
+			sumark(tright);
+			ecom(src, tright, mod);
+			ind = mkunary(Oind, mkbin(Oadd, dupn(0, src, tto), mkconst(src, big IBY2WD)));
+			ind.ty = ind.left.ty = ind.left.right.ty = tint;
+			tright.op = Oas;
+			tright.left = ind;
+			tright.right = mkdeclname(src, d);
+			tright.ty = tright.right.ty = tint;
+			sumark(tright);
+			if(mod.op == Oself && newfnptr)
+				tright.right.addable = Rnoff;
+			else
+				tright.right.addable = Roff;
+			ecom(src, nil, tright);
+			if(!sameaddr(tto, nto))
+				genmove(src, Mas, n.ty, tto, nto);
+			tfree(tto);
+			break;
+		}
+		if(left.op == Oname && left.decl.store == Dtype){
+			genrawop(src, INEW, mktn(t), nil, nto);
+			break;
+		}
+		if(t.kind == Tadt && t.tags != nil){
+			pickdupcom(src, nto, left);
+			break;
+		}
+
+		tt := t;
+		if(left.op == Oconst && left.decl.store == Dtag)
+			t = left.decl.ty.tof;
+
+		#
+		# could eliminate temp if nto does not occur
+		# in tuple initializer
+		#
+		tto = talloc(n.ty, nto);
+		genrawop(src, INEW, mktn(t), nil, tto);
+		tright = ref znode;
+		tright.op = Oind;
+		tright.left = tto;
+		tright.right = nil;
+		tright.ty = tt;
+		sumark(tright);
+		ecom(src, tright, left);
+		if(!sameaddr(tto, nto))
+			genmove(src, Mas, n.ty, tto, nto);
+		tfree(tto);
+	Oload =>
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		tright = talloc(tint, nil);
+		if(LDT)
+			genrawop(src, ILOAD, left, right, nto);
+		else{
+			genrawop(src, ILEA, right, nil, tright);
+			genrawop(src, ILOAD, left, tright, nto);
+		}
+		tfree(tleft);
+		tfree(tright);
+	Ocast =>
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		t = left.ty;
+		if(t.kind == Tfix || n.ty.kind == Tfix){
+			op = casttab[t.kind][n.ty.kind];
+			if(op == ICVTXX)
+				genfixcastop(src, op, left, nto);
+			else{
+				ttn = sumark(mkrconst(src, scale2(t, n.ty)));
+				genrawop(src, op, left, ttn, nto);
+			}
+		}
+		else
+			genrawop(src, casttab[t.kind][n.ty.kind], left, nil, nto);
+		tfree(tleft);
+	Oarray =>
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+		if(arrayz)
+			genrawop(esrc(src, left.src, nto), INEWAZ, left, mktn(n.ty.tof), nto);
+		else
+			genrawop(esrc(src, left.src, nto), INEWA, left, mktn(n.ty.tof), nto);
+		if(right != nil)
+			arraycom(nto, right);
+		tfree(tleft);
+	Oslice =>
+		tn := right.right;
+		right = right.left;
+
+		#
+		# make the left node of the slice directly addressable
+		# therefore, if it's len is taken (via tn),
+		# left's tree won't be rewritten
+		#
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nil);
+
+		if(tn.op == Onothing){
+			tn = mkn(Olen, left, nil);
+			tn.src = src;
+			tn.ty = tint;
+			sumark(tn);
+		}
+		if(tn.addable < Ralways){
+			if(right.addable >= Rcant)
+				(right, tright) = eacom(right, nil);
+		}else if(right.temps <= tn.temps){
+			tn = ecom(tn.src, ttn = talloc(tn.ty, nil), tn);
+			if(right.addable >= Rcant)
+				(right, tright) = eacom(right, nil);
+		}else{
+			(right, tright) = eacom(right, nil);
+			tn = ecom(tn.src, ttn = talloc(tn.ty, nil), tn);
+		}
+		op = ISLICEA;
+		if(nto.ty == tstring)
+			op = ISLICEC;
+
+		#
+		# overwrite the destination last,
+		# since it might be used in computing the slice bounds
+		#
+		if(!sameaddr(left, nto))
+			ecom(left.src, nto, left);
+
+		genrawop(src, op, right, tn, nto);
+		tfree(tleft);
+		tfree(tright);
+		tfree(ttn);
+	Oindx =>
+		if(right.addable < Rcant){
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nto);
+		}else if(left.temps < right.temps){
+			(right, tright) = eacom(right, nto);
+			if(left.addable >= Rcant)
+				(left, tleft) = eacom(left, nil);
+		}else{
+			(left, tleft) = eacom(left, nto);
+			(right, tright) = eacom(right, nil);
+		}
+		if(nto.addable >= Ralways)
+			nto = ecom(src, tto = talloc(nto.ty, nil), nto);
+		op = IINDX;
+		case left.ty.tof.size{
+		IBY2LG =>
+			op = IINDL;
+			if(left.ty.tof == treal)
+				op = IINDF;
+		IBY2WD =>
+			op = IINDW;
+		1 =>
+			op = IINDB;
+		}
+		genrawop(src, op, left, nto, right);
+		if(tleft != nil && tleft.decl != nil)
+			tfreelater(tleft);
+		else
+			tfree(tleft);
+		tfree(tright);
+		tfree(tto);
+	Oind =>
+		(n, tleft) = eacom(n, nto);
+		genmove(src, Mas, n.ty, n, nto);
+		tfree(tleft);
+	Onot or
+	Oandand or
+	Ooror or
+	Oeq or
+	Oneq or
+	Olt or
+	Oleq or
+	Ogt or
+	Ogeq =>
+		p = bcom(n, 1, nil);
+		genmove(src, Mas, tint, sumark(mkconst(src, big 1)), nto);
+		pp := genrawop(src, IJMP, nil, nil, nil);
+		patch(p, nextinst());
+		genmove(src, Mas, tint, sumark(mkconst(src, big 0)), nto);
+		patch(pp, nextinst());
+	Oself =>
+		if(newfnptr){
+			if(nto != nil)
+				genrawop(src, ISELF, nil, nil, nto);
+			break;
+		}
+		tn := sumark(mkdeclname(src, selfdecl));
+		p = genbra(src, Oneq, tn, sumark(mkdeclname(src, nildecl)));
+		n.op = Oload;
+		ecom(src, tn, n);
+		patch(p, nextinst());
+		genmove(src, Mas, n.ty, tn, nto);
+	}
+	return nto;
+}
+
+#
+# compile exp n to yield an addressable expression
+# use reg to build a temporary; if t is a temp, it is usable
+#
+# note that 0adr's are strange as they are only used
+# for calculating the addresses of fields within adt's.
+# therefore an Oind is the parent or grandparent of the Oadr,
+# and we pick off all of the cases where Oadr's argument is not
+# addressable by looking from the Oind.
+#
+eacom(n, t: ref Node): (ref Node, ref Node)
+{
+	reg: ref Node;
+
+	if(n.op == Ocomma){
+		tn := n.left.left;
+		ecom(n.left.src, nil, n.left);
+		nn := eacom(n.right, t);
+		tfree(tn);
+		return nn;
+	}
+
+	if(debug['e'] || debug['E'])
+		print("eacom: %s\n", nodeconv(n));
+
+	left := n.left;
+	if(n.op != Oind){
+		ecom(n.src, reg = talloc(n.ty, t), n);
+		reg.src = n.src;
+		return (reg, reg);
+	}
+
+	if(left.op == Oadd && left.right.op == Oconst){
+		if(left.left.op == Oadr){
+			(left.left.left, reg) = eacom(left.left.left, t);
+			sumark(n);
+			if(n.addable >= Rcant)
+				fatal("eacom can't make node addressable: "+nodeconv(n));
+			return (n, reg);
+		}
+		reg = talloc(left.left.ty, t);
+		ecom(left.left.src, reg, left.left);
+		left.left.decl = reg.decl;
+		left.left.addable = Rreg;
+		left.left = reg;
+		left.addable = Raadr;
+		n.addable = Radr;
+	}else if(left.op == Oadr){
+		reg = talloc(left.left.ty, t);
+		ecom(left.left.src, reg, left.left);
+
+		#
+		# sleaze: treat the temp as the type of the field, not the enclosing structure
+		#
+		reg.ty = n.ty;
+		reg.src = n.src;
+		return (reg, reg);
+	}else{
+		reg = talloc(left.ty, t);
+		ecom(left.src, reg, left);
+		n.left = reg;
+		n.addable = Radr;
+	}
+	return (n, reg);
+}
+
+#
+# compile an assignment to an array slice
+#
+slicelcom(src: Src, nto, n: ref Node): ref Node
+{
+	tleft, tright, tv: ref Node;
+
+	left := n.left.left;
+	right := n.left.right.left;
+	v := n.right;
+	if(right.addable < Ralways){
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+	}else if(left.temps <= right.temps){
+		right = ecom(right.src, tright = talloc(right.ty, nto), right);
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nil);
+	}else{
+		(left, tleft) = eacom(left, nil);		# dangle on right and v
+		right = ecom(right.src, tright = talloc(right.ty, nil), right);
+	}
+
+	case n.op{
+	Oas =>
+		if(v.addable >= Rcant)
+			(v, tv) = eacom(v, nil);
+	}
+
+	genrawop(n.src, ISLICELA, v, right, left);
+	if(nto != nil)
+		genmove(src, Mas, n.ty, left, nto);
+	tfree(tleft);
+	tfree(tv);
+	tfree(tright);
+	return nto;
+}
+
+#
+# compile an assignment to a string location
+#
+indsascom(src: Src, nto, n: ref Node): ref Node
+{
+	tleft, tright, tv, tu, u: ref Node;
+
+	left := n.left.left;
+	right := n.left.right;
+	v := n.right;
+	if(right.addable < Ralways){
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nto);
+	}else if(left.temps <= right.temps){
+		right = ecom(right.src, tright = talloc(right.ty, nto), right);
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nil);
+	}else{
+		(left, tleft) = eacom(left, nil);		# dangle on right and v
+		right = ecom(right.src, tright = talloc(right.ty, nil), right);
+	}
+
+	case n.op{
+	Oas =>
+		if(v.addable >= Rcant)
+			(v, tv) = eacom(v, nil);
+	Oinc or
+	Odec =>
+		if(v.addable >= Rcant)
+			fatal("inc/dec amount not addable");
+		u = tu = talloc(tint, nil);
+		genop(n.left.src, Oinds, left, right, u);
+		if(nto != nil)
+			genmove(src, Mas, n.ty, u, nto);
+		nto = nil;
+		genop(n.src, n.op, v, nil, u);
+		v = u;
+	Oaddas or
+	Osubas or
+	Omulas or
+	Odivas or
+	Omodas or
+	Oexpas or
+	Oandas or
+	Ooras or
+	Oxoras or
+	Olshas or
+	Orshas =>
+		if(v.addable >= Rcant)
+			(v, tv) = eacom(v, nil);
+		u = tu = talloc(tint, nil);
+		genop(n.left.src, Oinds, left, right, u);
+		genop(n.src, n.op, v, nil, u);
+		v = u;
+	}
+
+	genrawop(n.src, IINSC, v, right, left);
+	tfree(tleft);
+	tfree(tv);
+	tfree(tright);
+	tfree(tu);
+	if(nto != nil)
+		genmove(src, Mas, n.ty, v, nto);
+	return nto;
+}
+
+callcom(src: Src, op: int, n, ret: ref Node)
+{
+	tmod, tind: ref Node;
+	callee: ref Decl;
+
+	args := n.right;
+	nfn := n.left;
+	case(nfn.op){
+		Odot =>
+			callee = nfn.right.decl;
+			nfn.addable = Rpc;
+		Omdot =>
+			callee = nfn.right.decl;
+		Oname =>
+			callee = nfn.decl;
+		* =>
+			callee = nil;
+			fatal("bad call op in callcom");
+	}
+	if(nfn.addable != Rpc && nfn.addable != Rmpc)
+		fatal("can't gen call addresses");
+	if(nfn.ty.tof != tnone && ret == nil){
+		ecom(src, tmod = talloc(nfn.ty.tof, nil), n);
+		tfree(tmod);
+		return;
+	}
+	if(ispoly(callee))
+		addfnptrs(callee, 0);
+	if(nfn.ty.varargs != byte 0){
+		d := dupdecl(nfn.right.decl);
+		nfn.decl = d;
+		d.desc = gendesc(d, idoffsets(nfn.ty.ids, MaxTemp, MaxAlign), nfn.ty.ids);
+	}
+
+	frame := talloc(tint, nil);
+
+	mod := nfn.left;
+	ind := nfn.right;
+	if(nfn.addable == Rmpc){
+		if(mod.addable >= Rcant)
+			(mod, tmod) = eacom(mod, nil);		# dangle always
+		if(ind.op != Oname && ind.addable >= Ralways){
+			tind = talloc(ind.ty, nil);
+			ecom(ind.src, tind, ind);
+			ind = tind;
+		}
+		else if(ind.decl != nil && ind.decl.store != Darg)
+			ind.addable = Roff;
+	}
+
+	#
+	# stop nested uncalled frames
+	# otherwise exception handling very complicated
+	#
+	for(a := args; a != nil; a = a.right){
+		if(hascall(a.left)){
+			tn := talloc(a.left.ty, nil);
+			ecom(a.left.src, tn, a.left);
+			a.left = tn;
+			tn.flags |= byte TEMP;
+		}
+	}
+
+	#
+	# allocate the frame
+	#
+	if(nfn.addable == Rmpc && nfn.ty.varargs == byte 0){
+		genrawop(src, IMFRAME, mod, ind, frame);
+	}else if(nfn.op == Odot){
+		genrawop(src, IFRAME, nfn.left, nil, frame);
+	}else{
+		in := genrawop(src, IFRAME, nil, nil, frame);
+		in.sm = Adesc;
+		in.s.decl = nfn.decl;
+	}
+
+	#
+	# build a fake node for the argument area
+	#
+	toff := ref znode;
+	tadd := ref znode;
+	pass := ref znode;
+	toff.op = Oconst;
+	toff.c = ref Const(big 0, 0.0);	# jrf - added initialization
+	toff.addable = Rconst;
+	toff.ty = tint;
+	tadd.op = Oadd;
+	tadd.addable = Raadr;
+	tadd.left = frame;
+	tadd.right = toff;
+	tadd.ty = tint;
+	pass.op = Oind;
+	pass.addable = Radr;
+	pass.left = tadd;
+
+	#
+	# compile all the args
+	#
+	d := nfn.ty.ids;
+	off := 0;
+	for(a = args; a != nil; a = a.right){
+		off = d.offset;
+		toff.c.val = big off;
+		if(d.ty.kind == Tpoly)
+			pass.ty = a.left.ty;
+		else
+			pass.ty = d.ty;
+		ecom(a.left.src, pass, a.left);
+		d = d.next;
+		if(int a.left.flags & TEMP)
+			tfree(a.left);
+	}
+	if(off > maxstack)
+		maxstack = off;
+
+	#
+	# pass return value
+	#
+	if(ret != nil){
+		toff.c.val = big(REGRET*IBY2WD);
+		pass.ty = nfn.ty.tof;
+		p := genrawop(src, ILEA, ret, nil, pass);
+		p.m.offset = ret.ty.size;	# for optimizer
+	}
+
+	#
+	# call it
+	#
+	iop: int;
+	if(nfn.addable == Rmpc){
+		iop = IMCALL;
+		if(op == Ospawn)
+			iop = IMSPAWN;
+		genrawop(src, iop, frame, ind, mod);
+		tfree(tmod);
+		tfree(tind);
+	}else if(nfn.op == Odot){
+		iop = ICALL;
+		if(op == Ospawn)
+			iop = ISPAWN;
+		genrawop(src, iop, frame, nil, nfn.right);
+	}else{
+		iop = ICALL;
+		if(op == Ospawn)
+			iop = ISPAWN;
+		in := genrawop(src, iop, frame, nil, nil);
+		in.d.decl = nfn.decl;
+		in.dm = Apc;
+	}
+	tfree(frame);
+}
+
+#
+# initialization code for arrays
+# a must be addressable (< Rcant)
+#
+arraycom(a, elems: ref Node)
+{
+	top, out: ref Inst;
+	ri, n, wild: ref Node;
+
+	if(debug['A'])
+		print("arraycom: %s %s\n", nodeconv(a), nodeconv(elems));
+
+	# c := elems.ty.cse;
+	# don't use c.wild in case we've been inlined
+	wild = nil;
+	for(e := elems; e != nil; e = e.right)
+		for(q := e.left.left; q != nil; q = q.right)
+			if(q.left.op == Owild)
+				wild = e.left;
+	if(wild != nil)
+		arraydefault(a, wild.right);
+
+	tindex := ref znode;
+	fake := ref znode;
+	tmp := talloc(tint, nil);
+	tindex.op = Oindx;
+	tindex.addable = Rcant;
+	tindex.left = a;
+	tindex.right = nil;
+	tindex.ty = tint;
+	fake.op = Oind;
+	fake.addable = Radr;
+	fake.left = tmp;
+	fake.ty = a.ty.tof;
+
+	for(e = elems; e != nil; e = e.right){
+		#
+		# just duplicate the initializer for Oor
+		#
+		for(q = e.left.left; q != nil; q = q.right){
+			if(q.left.op == Owild)
+				continue;
+	
+			body := e.left.right;
+			if(q.right != nil)
+				body = dupn(0, nosrc, body);
+			top = nil;
+			out = nil;
+			ri = nil;
+			if(q.left.op == Orange){
+				#
+				# for(i := q.left.left; i <= q.left.right; i++)
+				#
+				ri = talloc(tint, nil);
+				ri.src = q.left.src;
+				ecom(q.left.src, ri, q.left.left);
+	
+				# i <= q.left.right;
+				n = mkn(Oleq, ri, q.left.right);
+				n.src = q.left.src;
+				n.ty = tint;
+				top = nextinst();
+				out = bcom(n, 1, nil);
+	
+				tindex.right = ri;
+			}else{
+				tindex.right = q.left;
+			}
+	
+			tindex.addable = Rcant;
+			tindex.src = q.left.src;
+			ecom(tindex.src, tmp, tindex);
+	
+			ecom(body.src, fake, body);
+	
+			if(q.left.op == Orange){
+				# i++
+				n = mkbin(Oinc, ri, sumark(mkconst(ri.src, big 1)));
+				n.ty = tint;
+				n.addable = Rcant;
+				ecom(n.src, nil, n);
+	
+				# jump to test
+				patch(genrawop(q.left.src, IJMP, nil, nil, nil), top);
+				patch(out, nextinst());
+				tfree(ri);
+			}
+		}
+	}
+	tfree(tmp);
+}
+
+#
+# default initialization code for arrays.
+# compiles to
+#	n = len a;
+#	while(n){
+#		n--;
+#		a[n] = elem;
+#	}
+#
+arraydefault(a, elem: ref Node)
+{
+	e: ref Node;
+
+	if(debug['A'])
+		print("arraydefault: %s %s\n", nodeconv(a), nodeconv(elem));
+
+	t := mkn(Olen, a, nil);
+	t.src = elem.src;
+	t.ty = tint;
+	t.addable = Rcant;
+	n := talloc(tint, nil);
+	n.src = elem.src;
+	ecom(t.src, n, t);
+
+	top := nextinst();
+	out := bcom(n, 1, nil);
+
+	t = mkbin(Odec, n, sumark(mkconst(elem.src, big 1)));
+	t.ty = tint;
+	t.addable = Rcant;
+	ecom(t.src, nil, t);
+
+	if(elem.addable >= Rcant)
+		(elem, e) = eacom(elem, nil);
+
+	t = mkn(Oindx, a, n);
+	t.src = elem.src;
+	t = mkbin(Oas, mkunary(Oind, t), elem);
+	t.ty = elem.ty;
+	t.left.ty = elem.ty;
+	t.left.left.ty = tint;
+	sumark(t);
+	ecom(t.src, nil, t);
+
+	patch(genrawop(t.src, IJMP, nil, nil, nil), top);
+
+	tfree(n);
+	tfree(e);
+	patch(out, nextinst());
+}
+
+tupcom(nto, n: ref Node)
+{
+	if(debug['Y'])
+		print("tupcom %s\nto %s\n", nodeconv(n), nodeconv(nto));
+
+	#
+	# build a fake node for the tuple
+	#
+	toff := ref znode;
+	tadd := ref znode;
+	fake := ref znode;
+	tadr := ref znode;
+	toff.op = Oconst;
+	toff.c = ref Const(big 0, 0.0);	# no val => may get fatal error below (jrf)
+	toff.ty = tint;
+	tadr.op = Oadr;
+	tadr.left = nto;
+	tadr.ty = tint;
+	tadd.op = Oadd;
+	tadd.left = tadr;
+	tadd.right = toff;
+	tadd.ty = tint;
+	fake.op = Oind;
+	fake.left = tadd;
+	sumark(fake);
+	if(fake.addable >= Rcant)
+		fatal("tupcom: bad value exp "+nodeconv(fake));
+
+	#
+	# compile all the exps
+	#
+	d := n.ty.ids;
+	for(e := n.left; e != nil; e = e.right){
+		toff.c.val = big d.offset;
+		fake.ty = d.ty;
+		ecom(e.left.src, fake, e.left);
+		d = d.next;
+	}
+}
+
+tuplcom(n, nto: ref Node)
+{
+	if(debug['Y'])
+		print("tuplcom %s\nto %s\n", nodeconv(n), nodeconv(nto));
+
+	#
+	# build a fake node for the tuple
+	#
+	toff := ref znode;
+	tadd := ref znode;
+	fake := ref znode;
+	tadr := ref znode;
+	toff.op = Oconst;
+	toff.c = ref Const(big 0, 0.0);	# no val => may get fatal error below (jrf)
+	toff.ty = tint;
+	tadr.op = Oadr;
+	tadr.left = n;
+	tadr.ty = tint;
+	tadd.op = Oadd;
+	tadd.left = tadr;
+	tadd.right = toff;
+	tadd.ty = tint;
+	fake.op = Oind;
+	fake.left = tadd;
+	sumark(fake);
+	if(fake.addable >= Rcant)
+		fatal("tuplcom: bad value exp for "+nodeconv(fake));
+
+	#
+	# compile all the exps
+	#
+	tas := ref znode;
+	d := nto.ty.ids;
+	if(nto.ty.kind == Tadtpick)
+		d = nto.ty.tof.ids.next;
+	for(e := nto.left; e != nil; e = e.right){
+		as := e.left;
+		if(as.op != Oname || as.decl != nildecl){
+			toff.c.val = big d.offset;
+			fake.ty = d.ty;
+			fake.src = as.src;
+			if(as.addable < Rcant)
+				genmove(as.src, Mas, d.ty, fake, as);
+			else{
+				tas.op = Oas;
+				tas.ty = d.ty;
+				tas.src = as.src;
+				tas.left = as;
+				tas.right = fake;
+				tas.addable = Rcant;
+				ecom(as.src, nil, tas);
+			}
+		}
+		d = d.next;
+	}
+}
+
+tuplrcom(n: ref Node, nto: ref Node)
+{
+	s, d, tas: ref Node;
+	de: ref Decl;
+
+	tas = ref znode;
+	de = nto.ty.ids;
+	for((s, d) = (n.left, nto.left); s != nil && d != nil; (s, d) = (s.right, d.right)){
+		if(d.left.op != Oname || d.left.decl != nildecl){
+			tas.op = Oas;
+			tas.ty = de.ty;
+			tas.src = s.left.src;
+			tas.left = d.left;
+			tas.right = s.left;
+			sumark(tas);
+			ecom(tas.src, nil, tas);
+		}
+		de = de.next;
+	}
+	if(s != nil || d != nil)
+		fatal("tuplrcom");
+}
+
+#
+# boolean compiler
+# fall through when condition == true
+#
+bcom(n: ref Node, iftrue: int, b: ref Inst): ref Inst
+{
+	tleft, tright: ref Node;
+
+	if(n.op == Ocomma){
+		tn := n.left.left;
+		ecom(n.left.src, nil, n.left);
+		b = bcom(n.right, iftrue, b);
+		tfree(tn);
+		return b;
+	}
+
+	if(debug['b'])
+		print("bcom %s %d\n", nodeconv(n), iftrue);
+
+	left := n.left;
+	right := n.right;
+	op := n.op;
+	case op{
+	Onothing =>
+		return b;
+	Onot =>
+		return bcom(n.left, !iftrue, b);
+	Oandand =>
+		if(!iftrue)
+			return oror(n, iftrue, b);
+		return andand(n, iftrue, b);
+	Ooror =>
+		if(!iftrue)
+			return andand(n, iftrue, b);
+		return oror(n, iftrue, b);
+	Ogt or
+	Ogeq or
+	Oneq or
+	Oeq or
+	Olt or
+	Oleq =>
+		break;
+	* =>
+		if(n.ty.kind == Tint){
+			right = mkconst(n.src, big 0);
+			right.addable = Rconst;
+			left = n;
+			op = Oneq;
+			break;
+		}
+		fatal("can't bcom "+nodeconv(n));
+		return b;
+	}
+
+	if(iftrue)
+		op = oprelinvert[op];
+
+	if(left.addable < right.addable){
+		t := left;
+		left = right;
+		right = t;
+		op = opcommute[op];
+	}
+
+	if(right.addable < Ralways){
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nil);
+	}else if(left.temps <= right.temps){
+		right = ecom(right.src, tright = talloc(right.ty, nil), right);
+		if(left.addable >= Rcant)
+			(left, tleft) = eacom(left, nil);
+	}else{
+		(left, tleft) = eacom(left, nil);
+		right = ecom(right.src, tright = talloc(right.ty, nil), right);
+	}
+	bb := genbra(n.src, op, left, right);
+	bb.branch = b;
+	tfree(tleft);
+	tfree(tright);
+	return bb;
+}
+
+andand(n: ref Node, iftrue: int, b: ref Inst): ref Inst
+{
+	if(debug['b'])
+		print("andand %s\n", nodeconv(n));
+	b = bcom(n.left, iftrue, b);
+	b = bcom(n.right, iftrue, b);
+	return b;
+}
+
+oror(n: ref Node, iftrue: int, b: ref Inst): ref Inst
+{
+	if(debug['b'])
+		print("oror %s\n", nodeconv(n));
+	bb := bcom(n.left, !iftrue, nil);
+	b = bcom(n.right, iftrue, b);
+	patch(bb, nextinst());
+	return b;
+}
+
+#
+# generate code for a recva expression
+# this is just a hacked up small alt
+#
+recvacom(src: Src, nto, n: ref Node)
+{
+	p: ref Inst;
+
+	left := n.left;
+
+	labs := array[1] of Label;
+	labs[0].isptr = left.addable >= Rcant;
+	c := ref Case;
+	c.nlab = 1;
+	c.nsnd = 0;
+	c.offset = 0;
+	c.labs = labs;
+	talt := mktalt(c);
+
+	which := talloc(tint, nil);
+	tab := talloc(talt, nil);
+
+	#
+	# build the node for the address of each channel,
+	# the values to send, and the storage for values received
+	#
+	off := ref znode;
+	adr := ref znode;
+	add := ref znode;
+	slot := ref znode;
+	off.op = Oconst;
+	off.c = ref Const(big 0, 0.0);		# jrf - added initialization
+	off.ty = tint;
+	off.addable = Rconst;
+	adr.op = Oadr;
+	adr.left = tab;
+	adr.ty = tint;
+	add.op = Oadd;
+	add.left = adr;
+	add.right = off;
+	add.ty = tint;
+	slot.op = Oind;
+	slot.left = add;
+	sumark(slot);
+
+	#
+	# gen the channel
+	# this sleaze is lying to the garbage collector
+	#
+	off.c.val = big(2*IBY2WD);
+	if(left.addable < Rcant)
+		genmove(src, Mas, tint, left, slot);
+	else{
+		slot.ty = left.ty;
+		ecom(src, slot, left);
+		slot.ty = nil;
+	}
+
+	#
+	# gen the value
+	#
+	off.c.val += big IBY2WD;
+	p = genrawop(left.src, ILEA, nto, nil, slot);
+	p.m.offset = nto.ty.size;	# for optimizer
+
+	#
+	# number of senders and receivers
+	#
+	off.c.val = big 0;
+	genmove(src, Mas, tint, sumark(mkconst(src, big 0)), slot);
+	off.c.val += big IBY2WD;
+	genmove(src, Mas, tint, sumark(mkconst(src, big 1)), slot);
+	off.c.val += big IBY2WD;
+
+	p = genrawop(src, IALT, tab, nil, which);
+	p.m.offset = talt.size;	# for optimizer
+	tfree(which);
+	tfree(tab);
+}
+
+#
+# generate code to duplicate an adt with pick fields
+# this is just a hacked up small pick
+# n is Oind(exp)
+#
+pickdupcom(src: Src, nto, n: ref Node)
+{
+	jmps: ref Inst;
+
+	if(n.op != Oind)
+		fatal("pickdupcom not Oind: " + nodeconv(n));
+
+	t := n.ty;
+	nlab := t.decl.tag;
+
+	#
+	# generate global which has case labels
+	#
+	d := mkids(src, enter(".c"+string nlabel++, 0), mktype(src.start, src.stop, Tcase, nil, nil), nil);
+	d.init = mkdeclname(src, d);
+
+	clab := ref znode;
+	clab.addable = Rmreg;
+	clab.left = nil;
+	clab.right = nil;
+	clab.op = Oname;
+	clab.ty = d.ty;
+	clab.decl = d;
+
+	#
+	# generate a temp to hold the real value
+	# then generate a case on the tag
+	#
+	orig := n.left;
+	tmp := talloc(orig.ty, nil);
+	ecom(src, tmp, orig);
+	orig = mkunary(Oind, tmp);
+	orig.ty = tint;
+	sumark(orig);
+
+	dest := mkunary(Oind, nto);
+	dest.ty = nto.ty.tof;
+	sumark(dest);
+
+	genrawop(src, ICASE, orig, nil, clab);
+
+	labs := array[nlab] of Label;
+
+	i := 0;
+	jmps = nil;
+	for(tg := t.tags; tg != nil; tg = tg.next){
+		stg := tg;
+		for(; tg.next != nil; tg = tg.next)
+			if(stg.ty != tg.next.ty)
+				break;
+		start := sumark(simplify(mkdeclname(src, stg)));
+		stop := start;
+		node := start;
+		if(stg != tg){
+			stop = sumark(simplify(mkdeclname(src, tg)));
+			node = mkbin(Orange, start, stop);
+		}
+
+		labs[i].start = start;
+		labs[i].stop = stop;
+		labs[i].node = node;
+		labs[i++].inst = nextinst();
+
+		genrawop(src, INEW, mktn(tg.ty.tof), nil, nto);
+		genmove(src, Mas, tg.ty.tof, orig, dest);
+
+		j := genrawop(src, IJMP, nil, nil, nil);
+		j.branch = jmps;
+		jmps = j;
+	}
+
+	#
+	# this should really be a runtime error
+	#
+	wild := genrawop(src, IJMP, nil, nil, nil);
+	patch(wild, wild);
+
+	patch(jmps, nextinst());
+	tfree(tmp);
+
+	if(i > nlab)
+		fatal("overflowed label tab for pickdupcom");
+
+	c := ref Case;
+	c.nlab = i;
+	c.nsnd = 0;
+	c.labs = labs;
+	c.iwild = wild;
+
+	d.ty.cse = c;
+	usetype(d.ty);
+	installids(Dglobal, d);
+}
+
+#
+# see if name n occurs anywhere in e
+#
+tupaliased(n, e: ref Node): int
+{
+	for(;;){
+		if(e == nil)
+			return 0;
+		if(e.op == Oname && e.decl == n.decl)
+			return 1;
+		if(tupaliased(n, e.left))
+			return 1;
+		e = e.right;
+	}
+	return 0;
+}
+
+#
+# see if any name in n occurs anywere in e
+#
+tupsaliased(n, e: ref Node): int
+{
+	for(;;){
+		if(n == nil)
+			return 0;
+		if(n.op == Oname && tupaliased(n, e))
+			return 1;
+		if(tupsaliased(n.left, e))
+			return 1;
+		n = n.right;
+	}
+	return 0;
+}
+
+#
+# put unaddressable constants in the global data area
+#
+globalconst(n: ref Node): ref Decl
+{
+	s := enter(".i." + hex(int n.c.val, 8), 0);
+	d := s.decl;
+	if(d == nil){
+		d = mkids(n.src, s, tint, nil);
+		installids(Dglobal, d);
+		d.init = n;
+		d.refs++;
+	}
+	return d;
+}
+
+globalBconst(n: ref Node): ref Decl
+{
+	s := enter(".B." + bhex(n.c.val, 16), 0);
+	d := s.decl;
+	if(d == nil){
+		d = mkids(n.src, s, tbig, nil);
+		installids(Dglobal, d);
+		d.init = n;
+		d.refs++;
+	}
+	return d;
+}
+
+globalbconst(n: ref Node): ref Decl
+{
+	s := enter(".b." + hex(int n.c.val & 16rff, 2), 0);
+	d := s.decl;
+	if(d == nil){
+		d = mkids(n.src, s, tbyte, nil);
+		installids(Dglobal, d);
+		d.init = n;
+		d.refs++;
+	}
+	return d;
+}
+
+globalfconst(n: ref Node): ref Decl
+{
+	ba := array[8] of byte;
+	export_real(ba, array[] of {n.c.rval});
+	fs := ".f.";
+	for(i := 0; i < 8; i++)
+		fs += hex(int ba[i], 2);
+	if(fs != ".f." + bhex(math->realbits64(n.c.rval), 16))
+		fatal("bad globalfconst number");
+	s := enter(fs, 0);
+	d := s.decl;
+	if(d == nil){
+		d = mkids(n.src, s, treal, nil);
+		installids(Dglobal, d);
+		d.init = n;
+		d.refs++;
+	}
+	return d;
+}
+
+globalsconst(n: ref Node): ref Decl
+{
+	s := n.decl.sym;
+	n.decl = nil;
+	d := s.decl;
+	if(d == nil){
+		d = mkids(n.src, s, tstring, nil);
+		installids(Dglobal, d);
+		d.init = n;
+	}
+	d.refs++;
+	n.decl = d;
+	return d;
+}
+
+#
+# make a global of type t
+# used to make initialized data
+#
+globalztup(t: ref Type): ref Decl
+{
+	z := ".z." + string t.size + ".";
+	desc := t.decl.desc;
+	for(i := 0; i < desc.nmap; i++)
+		z += hex(int desc.map[i], 2);
+	s := enter(z, 0);
+	d := s.decl;
+	if(d == nil){
+		d = mkids(t.src, s, t, nil);
+		installids(Dglobal, d);
+		d.init = nil;
+	}
+	d.refs++;
+	return d;
+}
+
+subst(d: ref Decl, e: ref Node, n: ref Node): ref Node
+{
+	if(n == nil)
+		return nil;
+	if(n.op == Oname){
+		if(d == n.decl){
+			n = dupn(0, nosrc, e);
+			n.ty = d.ty;
+		}
+		return n;
+	}
+	n.left = subst(d, e, n.left);
+	n.right = subst(d, e, n.right);
+	return n;
+}
+
+inline(n: ref Node): ref Node
+{
+	e, tn: ref Node;
+	t: ref Type;
+	d: ref Decl;
+
+if(debug['z']) sys->print("inline1: %s\n", nodeconv(n));
+	if(n.left.op == Oname)
+		d = n.left.decl;
+	else
+		d = n.left.right.decl;
+	e = d.init;
+	t = e.ty;
+	e = dupn(1, n.src, e.right.left.left);
+	n = n.right;
+	for(d = t.ids; d != nil && n != nil; d = d.next){
+		if(hasside(n.left, 0) && occurs(d, e) != 1){
+			tn = talloc(d.ty, nil);
+			e = mkbin(Ocomma, mkbin(Oas, tn, n.left), subst(d, tn, e));
+			e.ty = e.right.ty;
+			e.left.ty = d.ty;
+		}
+		else
+			e = subst(d, n.left, e);
+		n = n.right;
+	}
+	if(d != nil || n != nil)
+		fatal("bad arg match in inline()");
+if(debug['z']) sys->print("inline2: %s\n", nodeconv(e));
+	return e;
+}
+
+fpcall(src: Src, op: int, n: ref Node, ret: ref Node)
+{
+	tp, e, mod, ind: ref Node;
+
+	e = n.left.left;
+	if(e.addable >= Rcant)
+		(e, tp) = eacom(e, nil);
+	mod = mkunary(Oind, e);
+	ind = mkunary(Oind, mkbin(Oadd, dupn(0, src, e), mkconst(src, big IBY2WD)));
+	n.left = mkbin(Omdot, mod, ind);
+	n.left.ty = e.ty.tof;
+	mod.ty = ind.ty = ind.left.ty = ind.left.right.ty = tint;
+	sumark(n);
+	callcom(src, op, n, ret);
+	tfree(tp);
+}
--- /dev/null
+++ b/appl/cmd/limbo/gen.b
@@ -1,0 +1,1011 @@
+	blocks:		int;			# nesting of blocks while generating code
+	zinst:		Inst;
+	firstinst:	ref Inst;
+	lastinst:	ref Inst;
+
+include "disoptab.m";
+
+addrmode := array[int Rend] of
+{
+	int Rreg =>	Afp,
+	int Rmreg =>	Amp,
+	int Roff =>	Aoff,
+	int Rnoff =>	Anoff,
+	int Rdesc =>	Adesc,
+	int Rdescp =>	Adesc,
+	int Rconst =>	Aimm,
+	int Radr =>	Afpind,
+	int Rmadr =>	Ampind,
+	int Rpc =>	Apc,
+	int Rldt => Aldt,
+	* =>		Aerr,
+};
+
+wtemp:		ref Decl;
+bigtemp:	ref Decl;
+ntemp:		int;
+retnode:	ref Node;
+nilnode:	ref Node;
+
+blockstack:	array of int;
+blockdep:	int;
+nblocks:	int;
+ntoz:	ref Node;
+
+#znode:		Node;
+
+genstart()
+{
+	d := mkdecl(nosrc, Dlocal, tint);
+	d.sym = enter(".ret", 0);
+	d.offset = IBY2WD * REGRET;
+
+	retnode = ref znode;
+	retnode.op = Oname;
+	retnode.addable = Rreg;
+	retnode.decl = d;
+	retnode.ty = tint;
+
+	zinst.op = INOP;
+	zinst.sm = Anone;
+	zinst.dm = Anone;
+	zinst.mm = Anone;
+
+	firstinst = ref zinst;
+	lastinst = firstinst;
+
+	nilnode = ref znode;
+	nilnode.op = Oname;
+	nilnode.addable = Rmreg;
+	nilnode.decl = nildecl;
+	nilnode.ty = nildecl.ty;
+
+	blocks = -1;
+	blockdep = 0;
+	nblocks = 0;
+}
+
+#
+# manage nested control flow blocks
+#
+pushblock(): int
+{
+	if(blockdep >= len blockstack){
+		bs := array[blockdep + 32] of int;
+		bs[0:] = blockstack;
+		blockstack = bs;
+	}
+	blockstack[blockdep++] = blocks;
+	return blocks = nblocks++;
+}
+
+repushblock(b: int)
+{
+	blockstack[blockdep++] = blocks;
+	blocks = b;
+}
+
+popblock()
+{
+	blocks = blockstack[blockdep -= 1];
+}
+
+tinit()
+{
+	wtemp = nil;
+	bigtemp = nil;
+}
+
+tdecls(): ref Decl
+{
+	for(d := wtemp; d != nil; d = d.next){
+		if(d.tref != 1)
+			fatal("temporary "+d.sym.name+" has "+string(d.tref-1)+" references");
+	}
+
+	for(d = bigtemp; d != nil; d = d.next){
+		if(d.tref != 1)
+			fatal("temporary "+d.sym.name+" has "+string(d.tref-1)+" references");
+	}
+
+	return appdecls(wtemp, bigtemp);
+}
+
+talloc(t: ref Type, nok: ref Node): ref Node
+{
+	ok, d: ref Decl;
+
+	ok = nil;
+	if(nok != nil)
+		ok = nok.decl;
+	if(ok == nil || ok.tref == 0 || tattr[ok.ty.kind].isbig != tattr[t.kind].isbig || ok.ty.align != t.align)
+		ok = nil;
+	n := ref znode;
+	n.op = Oname;
+	n.addable = Rreg;
+	n.ty = t;
+	if(tattr[t.kind].isbig){
+		desc := mktdesc(t);
+		if(ok != nil && ok.desc == desc){
+			ok.tref++;
+			ok.refs++;
+			n.decl = ok;
+			return n;
+		}
+		for(d = bigtemp; d != nil; d = d.next){
+			if(d.tref == 1 && d.desc == desc && d.ty.align == t.align){
+				d.tref++;
+				d.refs++;
+				n.decl = d;
+				return n;
+			}
+		}
+		d = mkdecl(nosrc, Dlocal, t);
+		d.desc = desc;
+		d.tref = 2;
+		d.refs = 1;
+		d.sym = enter(".b"+string ntemp++, 0);
+		d.next = bigtemp;
+		bigtemp = d;
+		n.decl = d;
+		return n;
+	}
+	if(ok != nil
+	&& tattr[ok.ty.kind].isptr == tattr[t.kind].isptr
+	&& ok.ty.size == t.size){
+		ok.tref++;
+		n.decl = ok;
+		return n;
+	}
+	for(d = wtemp; d != nil; d = d.next){
+		if(d.tref == 1
+		&& tattr[d.ty.kind].isptr == tattr[t.kind].isptr
+		&& d.ty.size == t.size
+		&& d.ty.align == t.align){
+			d.tref++;
+			n.decl = d;
+			return n;
+		}
+	}
+	d = mkdecl(nosrc, Dlocal, t);
+	d.tref = 2;
+	d.refs = 1;
+	d.sym = enter(".t"+string ntemp++, 0);
+	d.next = wtemp;
+	wtemp = d;
+	n.decl = d;
+	return n;
+}
+
+tfree(n: ref Node)
+{
+	if(n == nil || n.decl == nil)
+		return;
+	d := n.decl;
+	if(d.tref == 0)
+		return;
+
+	if(d.tref == 1)
+		fatal("double free of temporary " + d.sym.name);
+	if (--d.tref == 1)
+		zcom1(n, nil);
+
+	#
+	# nil out any pointers so we don't
+	# hang onto references
+	#
+#
+# costs ~7% in instruction count
+#	if(d.tref != 1)
+#		return;
+#	if(!tattr[d.ty.kind].isbig){
+#		if(tattr[d.ty.kind].isptr){	# or tmustzero()
+#			nilnode.decl.refs++;
+#			genmove(lastinst.src, Mas, d.ty, nilnode, n);
+#		}
+#	}else{
+#		if(d.desc.nmap != 0){		# tmustzero() is better
+#			zn := ref znode;
+#			zn.op = Oname;
+#			zn.addable = Rmreg;
+#			zn.decl = globalztup(d.ty);
+#			zn.ty = d.ty;
+#			genmove(lastinst.src, Mas, d.ty, zn, n);
+#		}
+#	}
+}
+
+tfreelater(n: ref Node)
+{
+	if(n == nil || n.decl == nil)
+		return;
+	d := n.decl;
+	if(d.tref == 0)
+		return;
+
+	if(d.tref == 1)
+		fatal("double free of temporary " + d.sym.name);
+	if (--d.tref == 1){
+		nn := mkn(Oname, nil, nil);
+		*nn = *n;
+		nn.left = ntoz;
+		ntoz = nn;
+		d.tref++;
+	}
+}
+
+tfreenow()
+{
+	nn: ref Node;
+
+	for(n := ntoz; n != nil; n = nn){
+		nn = n.left;
+		n.left = nil;
+		if(n.decl.tref != 2)
+			fatal(sprint("bad free of temporary %s", n.decl.sym.name));
+		--n.decl.tref;
+		zcom1(n, nil);
+	}
+	ntoz = nil;
+}
+
+#
+# realloc a temporary after it's been released
+#
+tacquire(n: ref Node): ref Node
+{
+	if(n == nil || n.decl == nil)
+		return n;
+	d := n.decl;
+	if(d.tref == 0)
+		return n;
+	# if(d.tref != 1)
+	#	fatal("tacquire ref != 1: "+string d.tref);
+	d.tref++;
+	return n;
+}
+
+trelease(n: ref Node)
+{
+	if(n == nil || n.decl == nil)
+		return;
+	d := n.decl;
+	if(d.tref == 0)
+		return;
+	if(d.tref == 1)
+		fatal("double release of temporary " + d.sym.name);
+	d.tref--;
+}
+
+mkinst(): ref Inst
+{
+	in := lastinst.next;
+	if(in == nil){
+		in = ref zinst;
+		lastinst.next = in;
+	}
+	lastinst = in;
+	in.block = blocks;
+	if(blocks < 0)
+		fatal("mkinst no block");
+	return in;
+}
+
+nextinst(): ref Inst
+{
+	in := lastinst.next;
+	if(in != nil)
+		return in;
+	in = ref zinst;
+	lastinst.next = in;
+	return in;
+}
+
+#
+# allocate a node for returning
+#
+retalloc(n, nn: ref Node): ref Node
+{
+	if(nn.ty == tnone)
+		return nil;
+	n = ref znode;
+	n.op = Oind;
+	n.addable = Radr;
+	n.left = dupn(1, n.src, retnode);
+	n.ty = nn.ty;
+	return n;
+}
+
+genrawop(src: Src, op: int, s, m, d: ref Node): ref Inst
+{
+	in := mkinst();
+	in.op = op;
+	in.src = src;
+	if(s != nil){
+		in.s = genaddr(s);
+		in.sm = addrmode[int s.addable];
+	}
+	if(m != nil){
+		in.m = genaddr(m);
+		in.mm = addrmode[int m.addable];
+		if(in.mm == Ampind || in.mm == Afpind)
+			fatal("illegal addressing mode in register "+nodeconv(m));
+	}
+	if(d != nil){
+		in.d = genaddr(d);
+		in.dm = addrmode[int d.addable];
+	}
+	return in;
+}
+
+genop(src: Src, op: int, s, m, d: ref Node): ref Inst
+{
+	iop := disoptab[op][opind[d.ty.kind]];
+	if(iop == 0)
+		fatal("can't deal with op "+opconv(op)+" on "+nodeconv(s)+" "+nodeconv(m)+" "+nodeconv(d)+" in genop");
+	if(iop == IMULX || iop == IDIVX)
+		return genfixop(src, iop, s, m, d);
+	in := mkinst();
+	in.op = iop;
+	in.src = src;
+	if(s != nil){
+		in.s = genaddr(s);
+		in.sm = addrmode[int s.addable];
+	}
+	if(m != nil){
+		in.m = genaddr(m);
+		in.mm = addrmode[int m.addable];
+		if(in.mm == Ampind || in.mm == Afpind)
+			fatal("illegal addressing mode in register "+nodeconv(m));
+	}
+	if(d != nil){
+		in.d = genaddr(d);
+		in.dm = addrmode[int d.addable];
+	}
+	return in;
+}
+
+genbra(src: Src, op: int, s, m: ref Node): ref Inst
+{
+	t := s.ty;
+	if(t == tany)
+		t = m.ty;
+	iop := disoptab[op][opind[t.kind]];
+	if(iop == 0)
+		fatal("can't deal with op "+opconv(op)+" on "+nodeconv(s)+" "+nodeconv(m)+" in genbra");
+	in := mkinst();
+	in.op = iop;
+	in.src = src;
+	if(s != nil){
+		in.s = genaddr(s);
+		in.sm = addrmode[int s.addable];
+	}
+	if(m != nil){
+		in.m = genaddr(m);
+		in.mm = addrmode[int m.addable];
+		if(in.mm == Ampind || in.mm == Afpind)
+			fatal("illegal addressing mode in register "+nodeconv(m));
+	}
+	return in;
+}
+
+genchan(src: Src, sz: ref Node, mt: ref Type, d: ref Node): ref Inst
+{
+	reg: Addr;
+
+	regm := Anone;
+	reg.decl = nil;
+	reg.reg = 0;
+	reg.offset = 0;
+	op := chantab[mt.kind];
+	if(op == 0)
+		fatal("can't deal with op "+string mt.kind+" in genchan");
+
+	case mt.kind{
+	Tadt or
+	Tadtpick or
+	Ttuple =>
+		td := mktdesc(mt);
+		if(td.nmap != 0){
+			op++;		# sleazy
+			usedesc(td);
+			regm = Adesc;
+			reg.decl = mt.decl;
+		}else{
+			regm = Aimm;
+			reg.offset = mt.size;
+		}
+	}
+	in := mkinst();
+	in.op = op;
+	in.src = src;
+	in.s = reg;
+	in.sm = regm;
+	if(sz != nil){
+		in.m = genaddr(sz);
+		in.mm = addrmode[int sz.addable];
+	}
+	if(d != nil){
+		in.d = genaddr(d);
+		in.dm = addrmode[int d.addable];
+	}
+	return in;
+}
+
+genmove(src: Src, how: int, mt: ref Type, s, d: ref Node): ref Inst
+{
+	reg: Addr;
+
+	regm := Anone;
+	reg.decl = nil;
+	reg.reg = 0;
+	reg.offset = 0;
+	op := movetab[how][mt.kind];
+	if(op == 0)
+		fatal("can't deal with op "+string how+" on "+nodeconv(s)+" "+nodeconv(d)+" in genmove");
+
+	case mt.kind{
+	Tadt or
+	Tadtpick or
+	Ttuple or
+	Texception =>
+		if(mt.size == 0 && how == Mas)
+			return nil;
+		td := mktdesc(mt);
+		if(td.nmap != 0){
+			op++;		# sleazy
+			usedesc(td);
+			regm = Adesc;
+			reg.decl = mt.decl;
+		}else{
+			regm = Aimm;
+			reg.offset = mt.size;
+		}
+	}
+	in := mkinst();
+	in.op = op;
+	in.src = src;
+	if(s != nil){
+		in.s = genaddr(s);
+		in.sm = addrmode[int s.addable];
+	}
+	in.m = reg;
+	in.mm = regm;
+	if(d != nil){
+		in.d = genaddr(d);
+		in.dm = addrmode[int d.addable];
+	}
+	if(s.addable == Rpc)
+		in.op = IMOVPC;
+	return in;
+}
+
+patch(b, dst: ref Inst)
+{
+	n: ref Inst;
+
+	for(; b != nil; b = n){
+		n = b.branch;
+		b.branch = dst;
+	}
+}
+
+getpc(i: ref Inst): int
+{
+	if(i.pc == 0 && i != firstinst && (firstinst.op != INOOP || i != firstinst.next)){
+		do
+			i = i.next;
+		while(i != nil && i.pc == 0);
+		if(i == nil || i.pc == 0)
+			fatal("bad instruction in getpc");
+	}
+	return i.pc;
+}
+
+#
+# follow all possible paths from n,
+# marking reached code, compressing branches, and reclaiming unreached insts
+#
+reach(in: ref Inst)
+{
+	foldbranch(in);
+	last := in;
+	for(in = in.next; in != nil; in = in.next){
+		if(in.reach == byte 0)
+			last.next = in.next;
+		else
+			last = in;
+	}
+	lastinst = last;
+}
+
+foldbranch(in: ref Inst)
+{
+	while(in != nil && in.reach != byte 1){
+		in.reach = byte 1;
+		if(in.branch != nil)
+			while(in.branch.op == IJMP){
+				if(in == in.branch || in.branch == in.branch.branch)
+					break;
+				in.branch = in.branch.branch;
+			}
+		case in.op{
+		IGOTO or
+		ICASE or
+		ICASEL or
+		ICASEC or
+		IEXC =>
+			foldbranch(in.d.decl.ty.cse.iwild);
+			lab := in.d.decl.ty.cse.labs;
+			n := in.d.decl.ty.cse.nlab;
+			for(i := 0; i < n; i++)
+				foldbranch(lab[i].inst);
+			if(in.op == IEXC)
+				in.op = INOOP;
+			return;
+		IEXC0 =>
+			foldbranch(in.branch);
+			in.op = INOOP;
+			break;
+		IRET or
+		IEXIT or
+		IRAISE =>
+			return;
+		IJMP =>
+			b := in.branch;
+			case b.op{
+			ICASE or
+			ICASEL or
+			ICASEC or
+			IRET or
+			IEXIT =>
+				next := in.next;
+				*in = *b;
+				in.next = next;
+				continue;
+			}
+			foldbranch(in.branch);
+			return;
+		* =>
+			if(in.branch != nil)
+				foldbranch(in.branch);
+		}
+
+		in = in.next;
+	}
+}
+
+#
+# convert the addressable node into an operand
+# see the comment for sumark
+#
+genaddr(n: ref Node): Addr
+{
+	a: Addr;
+
+	a.reg = 0;
+	a.offset = 0;
+	a.decl = nil;
+	case int n.addable{
+	int Rreg =>
+		if(n.decl != nil)
+			a.decl = n.decl;
+		else
+			a = genaddr(n.left);
+	int Rmreg =>
+		if(n.decl != nil)
+			a.decl = n.decl;
+		else
+			a = genaddr(n.left);
+	int Rdesc =>
+		a.decl = n.ty.decl;
+	int Roff or
+	int Rnoff =>
+		a.decl = n.decl;
+	int Rconst =>
+		a.offset = int n.c.val;
+	int Radr =>
+		a = genaddr(n.left);
+	int Rmadr =>
+		a = genaddr(n.left);
+	int Rareg or
+	int Ramreg =>
+		a = genaddr(n.left);
+		if(n.op == Oadd)
+			a.reg += int n.right.c.val;
+	int Raadr or
+	int Ramadr =>
+		a = genaddr(n.left);
+		if(n.op == Oadd)
+			a.offset += int n.right.c.val;
+	int Rldt =>
+		a.decl = n.decl;
+	int Rdescp or
+	int Rpc =>
+		a.decl = n.decl;
+	* =>
+		fatal("can't deal with "+nodeconv(n)+" in genaddr");
+	}
+	return a;
+}
+
+sameaddr(n, m: ref Node): int
+{
+	if(n.addable != m.addable)
+		return 0;
+	a := genaddr(n);
+	b := genaddr(m);
+	return a.offset == b.offset && a.reg == b.reg && a.decl == b.decl;
+}
+
+resolvedesc(mod: ref Decl, length: int, id: ref Decl): int
+{
+	last: ref Desc;
+
+	g := gendesc(mod, length, id);
+	g.used = 0;
+	last = nil;
+	for(d := descriptors; d != nil; d = d.next){
+		if(!d.used){
+			if(last != nil)
+				last.next = d.next;
+			else
+				descriptors = d.next;
+			continue;
+		}
+		last = d;
+	}
+
+	g.next = descriptors;
+	descriptors = g;
+
+	descid := 0;
+	for(d = descriptors; d != nil; d = d.next)
+		d.id = descid++;
+	if(g.id != 0)
+		fatal("bad global descriptor id");
+
+	return descid;
+}
+
+resolvemod(m: ref Decl): int
+{
+	for(id := m.ty.ids; id != nil; id = id.next){
+		case id.store{
+		Dfn =>
+			id.iface.pc = id.pc;
+			id.iface.desc = id.desc;
+		Dtype =>
+			if(id.ty.kind != Tadt)
+				break;
+			for(d := id.ty.ids; d != nil; d = d.next){
+				if(d.store == Dfn){
+					d.iface.pc = d.pc;
+					d.iface.desc = d.desc;
+				}
+			}
+		}
+	}
+	# for addiface
+	for(id = m.ty.tof.ids; id != nil; id = id.next){
+		if(id.store == Dfn){
+			if(id.pc == nil)
+				id.pc = id.iface.pc;
+			if(id.desc == nil)
+				id.desc = id.iface.desc;
+		}
+	}
+	return int m.ty.tof.decl.init.c.val;
+}
+
+#
+# place the Tiface decs in another list
+#
+resolveldts(d: ref Decl): (ref Decl, ref Decl)
+{
+	d1, ld1, d2, ld2, n: ref Decl;
+
+	d1 = d2 = nil;
+	ld1 = ld2 = nil;
+	for( ; d != nil; d = n){
+		n = d.next;
+		d.next = nil;
+		if(d.ty.kind == Tiface){
+			if(d2 == nil)
+				d2 = d;
+			else
+				ld2.next = d;
+			ld2 = d;
+		}
+		else{
+			if(d1 == nil)
+				d1 = d;
+			else
+				ld1.next = d;
+			ld1 = d;
+		}
+	}
+	return (d1, d2);
+}
+
+#
+# fix up all pc's
+# finalize all data offsets
+# fix up instructions with offsets too large
+#
+resolvepcs(inst: ref Inst): int
+{
+	d: ref Decl;
+
+	pc := 0;
+	for(in := inst; in != nil; in = in.next){
+		if(in.reach == byte 0 || in.op == INOP)
+			fatal("unreachable pc: "+instconv(in));
+		if(in.op == INOOP){
+			in.pc = pc;
+			continue;
+		}
+		d = in.s.decl;
+		if(d != nil){
+			if(in.sm == Adesc){
+				if(d.desc != nil)
+					in.s.offset = d.desc.id;
+			}else
+				in.s.reg += d.offset;
+		}
+		r := in.s.reg;
+		off := in.s.offset;
+		if((in.sm == Afpind || in.sm == Ampind)
+		&& (r >= MaxReg || off >= MaxReg))
+			fatal("big offset in "+instconv(in));
+
+		d = in.m.decl;
+		if(d != nil){
+			if(in.mm == Adesc){
+				if(d.desc != nil)
+					in.m.offset = d.desc.id;
+			}else
+				in.m.reg += d.offset;
+		}
+		v := 0;
+		case int in.mm{
+		int Anone =>
+			break;
+		int Aimm or
+		int Apc or
+		int Adesc =>
+			v = in.m.offset;
+		int Aoff or
+		int Anoff =>
+			v = in.m.decl.iface.offset;
+		int Afp or
+		int Amp or
+		int Aldt =>
+			v = in.m.reg;
+			if(v < 0)
+				v = 16r8000;
+		* =>
+			fatal("can't deal with "+instconv(in)+"'s m mode");
+		}
+		if(v > 16r7fff || v < -16r8000){
+			case in.op{
+			IALT or
+			IINDX =>
+				rewritedestreg(in, IMOVW, RTemp);
+			* =>
+				op := IMOVW;
+				if(isbyteinst[in.op])
+					op = IMOVB;
+				in = rewritesrcreg(in, op, RTemp, pc++);
+			}
+		}
+
+		d = in.d.decl;
+		if(d != nil){
+			if(in.dm == Apc)
+				in.d.offset = d.pc.pc;
+			else
+				in.d.reg += d.offset;
+		}
+		r = in.d.reg;
+		off = in.d.offset;
+		if((in.dm == Afpind || in.dm == Ampind)
+		&& (r >= MaxReg || off >= MaxReg))
+			fatal("big offset in "+instconv(in));
+
+		in.pc = pc;
+		pc++;
+	}
+	for(in = inst; in != nil; in = in.next){
+		d = in.s.decl;
+		if(d != nil && in.sm == Apc)
+			in.s.offset = d.pc.pc;
+		d = in.d.decl;
+		if(d != nil && in.dm == Apc)
+			in.d.offset = d.pc.pc;
+		if(in.branch != nil){
+			in.dm = Apc;
+			in.d.offset = in.branch.pc;
+		}
+	}
+	return pc;
+}
+
+#
+# fixp up a big register constant uses as a source
+# ugly: smashes the instruction
+#
+rewritesrcreg(in: ref Inst, op: int, treg: int, pc: int): ref Inst
+{
+	a := in.m;
+	am := in.mm;
+	in.mm = Afp;
+	in.m.reg = treg;
+	in.m.decl = nil;
+
+	new := ref *in;
+
+	*in = zinst;
+	in.src = new.src;
+	in.next = new;
+	in.op = op;
+	in.s = a;
+	in.sm = am;
+	in.dm = Afp;
+	in.d.reg = treg;
+	in.pc = pc;
+	in.reach = byte 1;
+	in.block = new.block;
+	return new;
+}
+
+#
+# fix up a big register constant by moving to the destination
+# after the instruction completes
+#
+rewritedestreg(in: ref Inst, op: int, treg: int): ref Inst
+{
+	n := ref zinst;
+	n.next = in.next;
+	in.next = n;
+	n.src = in.src;
+	n.op = op;
+	n.sm = Afp;
+	n.s.reg = treg;
+	n.d = in.m;
+	n.dm = in.mm;
+	n.reach = byte 1;
+	n.block = in.block;
+
+	in.mm = Afp;
+	in.m.reg = treg;
+	in.m.decl = nil;
+
+	return n;
+}
+
+instconv(in: ref Inst): string
+{
+	if(in.op == INOP)
+		return "nop";
+	op := "";
+	if(in.op >= 0 && in.op < 256)
+		op = instname[in.op];
+	if(op == nil)
+		op = "?"+string in.op+"?";
+	s := "\t" + op + "\t";
+	comma := "";
+	if(in.sm != Anone){
+		s += addrconv(in.sm, in.s);
+		comma = ",";
+	}
+	if(in.mm != Anone){
+		s += comma;
+		s += addrconv(in.mm, in.m);
+		comma = ",";
+	}
+	if(in.dm != Anone){
+		s += comma;
+		s += addrconv(in.dm, in.d);
+	}
+
+	if(!asmsym)
+		return s;
+
+	if(in.s.decl != nil && in.sm == Adesc){
+		s += "\t#";
+		s += dotconv(in.s.decl);
+	}
+	if(0 && in.m.decl != nil){
+		s += "\t#";
+		s += dotconv(in.m.decl);
+	}
+	if(in.d.decl != nil && in.dm == Apc){
+		s += "\t#";
+		s += dotconv(in.d.decl);
+	}
+	s += "\t#";
+	s += srcconv(in.src);
+	return s;
+}
+
+addrconv(am: byte, a: Addr): string
+{
+	s := "";
+	case int am{
+	int Anone =>
+		break;
+	int Aimm or
+	int Apc or
+	int Adesc =>
+		s = "$" + string a.offset;
+	int Aoff =>
+		s = "$" + string a.decl.iface.offset;
+	int Anoff =>
+		s = "-$" + string a.decl.iface.offset;
+	int Afp =>
+		s = string a.reg + "(fp)";
+	int Afpind =>
+		s = string a.offset + "(" + string a.reg + "(fp))";
+	int Amp =>
+		s = string a.reg + "(mp)";
+	int Ampind =>
+		s = string a.offset + "(" + string a.reg + "(mp))";
+	int Aldt =>
+		s = "$" + string a.reg;
+	* =>
+		s = string a.offset + "(" + string a.reg + "(?" + string am + "?))";
+	}
+	return s;
+}
+
+genstore(src: Src, n: ref Node, offset: int)
+{
+	de := mkdecl(nosrc, Dlocal, tint);
+	de.sym = nil;
+	de.offset = offset;
+
+	d := ref znode;
+	d.op = Oname;
+	d.addable = Rreg;
+	d.decl = de;
+	d.ty = tint;
+	genrawop(src, IMOVW, n, nil, d);
+}
+
+genfixop(src: Src, op: int, s, m, d: ref Node): ref Inst
+{
+	p, a: int;
+	mm: ref Node;
+
+	if(m == nil)
+		mm = d;
+	else
+		mm = m;
+	(op, p, a) = fixop(op, mm.ty, s.ty, d.ty);
+	if(op == IMOVW){	# just zero d
+		s = sumark(mkconst(src, big 0));
+		return genrawop(src, op, s, nil, d);
+	}
+	if(op != IMULX && op != IDIVX)
+		genstore(src, sumark(mkconst(src, big a)), STemp);
+	genstore(src, sumark(mkconst(src, big p)), DTemp);
+	i := genrawop(src, op, s, m, d);
+	return i;
+}
+
+genfixcastop(src: Src, op: int, s, d: ref Node): ref Inst
+{
+	p, a: int;
+	m: ref Node;
+
+	(op, p, a) = fixop(op, s.ty, tint, d.ty);
+	if(op == IMOVW){	# just zero d
+		s = sumark(mkconst(src, big 0));
+		return genrawop(src, op, s, nil, d);
+	}
+	m = sumark(mkconst(src, big p));
+	if(op != ICVTXX)
+		genstore(src, sumark(mkconst(src, big a)), STemp);
+	return genrawop(src, op, s, m, d);
+}
--- /dev/null
+++ b/appl/cmd/limbo/isa.m
@@ -1,0 +1,247 @@
+#
+# VM instruction set
+#
+	INOP,
+	IALT,
+	INBALT,
+	IGOTO,
+	ICALL,
+	IFRAME,
+	ISPAWN,
+	IRUNT,
+	ILOAD,
+	IMCALL,
+	IMSPAWN,
+	IMFRAME,
+	IRET,
+	IJMP,
+	ICASE,
+	IEXIT,
+	INEW,
+	INEWA,
+	INEWCB,
+	INEWCW,
+	INEWCF,
+	INEWCP,
+	INEWCM,
+	INEWCMP,
+	ISEND,
+	IRECV,
+	ICONSB,
+	ICONSW,
+	ICONSP,
+	ICONSF,
+	ICONSM,
+	ICONSMP,
+	IHEADB,
+	IHEADW,
+	IHEADP,
+	IHEADF,
+	IHEADM,
+	IHEADMP,
+	ITAIL,
+	ILEA,
+	IINDX,
+	IMOVP,
+	IMOVM,
+	IMOVMP,
+	IMOVB,
+	IMOVW,
+	IMOVF,
+	ICVTBW,
+	ICVTWB,
+	ICVTFW,
+	ICVTWF,
+	ICVTCA,
+	ICVTAC,
+	ICVTWC,
+	ICVTCW,
+	ICVTFC,
+	ICVTCF,
+	IADDB,
+	IADDW,
+	IADDF,
+	ISUBB,
+	ISUBW,
+	ISUBF,
+	IMULB,
+	IMULW,
+	IMULF,
+	IDIVB,
+	IDIVW,
+	IDIVF,
+	IMODW,
+	IMODB,
+	IANDB,
+	IANDW,
+	IORB,
+	IORW,
+	IXORB,
+	IXORW,
+	ISHLB,
+	ISHLW,
+	ISHRB,
+	ISHRW,
+	IINSC,
+	IINDC,
+	IADDC,
+	ILENC,
+	ILENA,
+	ILENL,
+	IBEQB,
+	IBNEB,
+	IBLTB,
+	IBLEB,
+	IBGTB,
+	IBGEB,
+	IBEQW,
+	IBNEW,
+	IBLTW,
+	IBLEW,
+	IBGTW,
+	IBGEW,
+	IBEQF,
+	IBNEF,
+	IBLTF,
+	IBLEF,
+	IBGTF,
+	IBGEF,
+	IBEQC,
+	IBNEC,
+	IBLTC,
+	IBLEC,
+	IBGTC,
+	IBGEC,
+	ISLICEA,
+	ISLICELA,
+	ISLICEC,
+	IINDW,
+	IINDF,
+	IINDB,
+	INEGF,
+	IMOVL,
+	IADDL,
+	ISUBL,
+	IDIVL,
+	IMODL,
+	IMULL,
+	IANDL,
+	IORL,
+	IXORL,
+	ISHLL,
+	ISHRL,
+	IBNEL,
+	IBLTL,
+	IBLEL,
+	IBGTL,
+	IBGEL,
+	IBEQL,
+	ICVTLF,
+	ICVTFL,
+	ICVTLW,
+	ICVTWL,
+	ICVTLC,
+	ICVTCL,
+	IHEADL,
+	ICONSL,
+	INEWCL,
+	ICASEC,
+	IINDL,
+	IMOVPC,
+	ITCMP,
+	IMNEWZ,
+	ICVTRF,
+	ICVTFR,
+	ICVTWS,
+	ICVTSW,
+	ILSRW,
+	ILSRL,
+	IECLR,
+	INEWZ,
+	INEWAZ,
+	IRAISE,
+	ICASEL,
+	IMULX,
+	IDIVX,
+	ICVTXX,
+	IMULX0,
+	IDIVX0,
+	ICVTXX0,
+	IMULX1,
+	IDIVX1,
+	ICVTXX1,
+	ICVTFX,
+	ICVTXF,
+	IEXPW,
+	IEXPL,
+	IEXPF,
+	ISELF,
+	# add new operators here
+	MAXDIS: con iota;
+
+XMAGIC:		con 819248;	# Normal magic
+SMAGIC:		con 923426;	# Signed module
+
+AMP:		con 16r00;	# Src/Dst op addressing 
+AFP:		con 16r01;
+AIMM:		con 16r2;
+AXXX:		con 16r03;
+AIND:		con 16r04;
+AMASK:		con 16r07;
+AOFF:		con 16r08;
+AVAL:		con 16r10;
+
+ARM:		con 16rC0;	# Middle op addressing 
+AXNON:		con 16r00;
+AXIMM:		con 16r40;
+AXINF:		con 16r80;
+AXINM:		con 16rC0;
+
+DEFZ:		con 0;
+DEFB:		con 1;		# Byte 
+DEFW:		con 2;		# Word 
+DEFS:		con 3;		# Utf-string 
+DEFF:		con 4;		# Real value 
+DEFA:		con 5;		# Array 
+DIND:		con 6;		# Set index 
+DAPOP:		con 7;		# Restore address register 
+DEFL:		con 8;		# BIG 
+
+DADEPTH:	con 4;		# Array address stack size 
+
+REGLINK:	con 0;
+REGFRAME:	con 1;
+REGMOD:		con 2;
+REGTYP:		con 3;
+REGRET:		con 4;
+NREG:		con 5;
+
+IBY2WD:		con 4;
+IBY2FT:		con 8;
+IBY2LG:		con 8;
+
+MUSTCOMPILE:	con 1<<0;
+DONTCOMPILE:	con 1<<1;
+SHAREMP:	con 1<<2;
+DYNMOD:	con	1<<3;
+HASLDT0:	con	1<<4;
+HASEXCEPT:	con	1<<5;
+HASLDT:	con	1<<6;
+
+DMAX:		con 1 << 4;
+
+#define DTYPE(x)	(x>>4)
+#define DBYTE(x, l)	((x<<4)|l)
+#define DMAX		(1<<4)
+#define DLEN(x)		(x& (DMAX-1))
+
+DBYTE:		con 4;
+SRC:		con 3;
+DST:		con 0;
+
+#define SRC(x)		((x)<<3)
+#define DST(x)		((x)<<0)
+#define USRC(x)		(((x)>>3)&AMASK)
+#define UDST(x)		((x)&AMASK)
+#define UXSRC(x)	((x)&(AMASK<<3))
+#define UXDST(x)	((x)&(AMASK<<0))
--- /dev/null
+++ b/appl/cmd/limbo/lex.b
@@ -1,0 +1,1178 @@
+Leof:		con -1;
+Linestart:	con 0;
+
+Mlower,
+Mupper,
+Munder,
+Mdigit,
+Msign,
+Mexp,
+Mhex,
+Mradix:		con byte 1 << iota;
+Malpha:		con Mupper|Mlower|Munder;
+
+HashSize:	con 1024;
+
+Keywd: adt
+{
+	name:	string;
+	token:	int;
+};
+
+#
+# internals
+#
+savec:		int;
+files:		array of ref File;			# files making up the module, sorted by absolute line
+nfiles:		int;
+lastfile := 0;						# index of last file looked up
+incpath :=	array[MaxIncPath] of string;
+symbols :=	array[HashSize] of ref Sym;
+strings :=	array[HashSize] of ref Sym;
+map :=		array[256] of byte;
+bins :=		array [MaxInclude] of ref Iobuf;
+bin:		ref Iobuf;
+linestack :=	array[MaxInclude] of (int, int);
+lineno:		int;
+linepos:	int;
+bstack:		int;
+lasttok:	int;
+lastyylval:	YYSTYPE;
+dowarn:		int;
+maxerr:		int;
+dosym:		int;
+toterrors:	int;
+fabort:		int;
+srcdir:		string;
+outfile:	string;
+stderr:		ref Sys->FD;
+dontinline:	int;
+
+escmap :=	array[256] of
+{
+	'\'' =>		'\'',
+	'"' =>		'"',
+	'\\' =>		'\\',
+	'a' =>		'\a',
+	'b' =>		'\b',
+	'f' =>			'\f',
+	'n' =>		'\n',
+	'r' =>		'\r',
+	't' =>		'\t',
+	'v' =>		'\v',
+	'0' =>		'\u0000',
+
+	* =>		-1
+};
+unescmap :=	array[256] of 
+{
+	'\'' =>		'\'',
+	'"' =>		'"',
+	'\\' =>		'\\',
+	'\a' =>		'a',
+	'\b' =>		'b',
+	'\f' =>		'f',
+	'\n' =>		'n',
+	'\r' =>		'r',
+	'\t' =>		't',
+	'\v' =>		'v',
+	'\u0000' =>	'0',
+
+	* =>		0
+};
+
+keywords := array [] of
+{
+	Keywd("adt",		Ladt),
+	Keywd("alt",		Lalt),
+	Keywd("array",		Larray),
+	Keywd("big",		Ltid),
+	Keywd("break",		Lbreak),
+	Keywd("byte",		Ltid),
+	Keywd("case",		Lcase),
+	Keywd("chan",		Lchan),
+	Keywd("con",		Lcon),
+	Keywd("continue",	Lcont),
+	Keywd("cyclic",		Lcyclic),
+	Keywd("do",		Ldo),
+	Keywd("dynamic",	Ldynamic),
+	Keywd("else",		Lelse),
+	Keywd("exception",	Lexcept),
+	Keywd("exit",		Lexit),
+	Keywd("fixed",	Lfix),
+	Keywd("fn",		Lfn),
+	Keywd("for",		Lfor),
+	Keywd("hd",		Lhd),
+	Keywd("if",		Lif),
+	Keywd("implement",	Limplement),
+	Keywd("import",		Limport),
+	Keywd("include",	Linclude),
+	Keywd("int",		Ltid),
+	Keywd("len",		Llen),
+	Keywd("list",		Llist),
+	Keywd("load",		Lload),
+	Keywd("module",		Lmodule),
+	Keywd("nil",		Lnil),
+	Keywd("of",		Lof),
+	Keywd("or",		Lor),
+	Keywd("pick",		Lpick),
+	Keywd("raise",	Lraise),
+	Keywd("raises",	Lraises),
+	Keywd("real",		Ltid),
+	Keywd("ref",		Lref),
+	Keywd("return",		Lreturn),
+	Keywd("self",		Lself),
+	Keywd("spawn",		Lspawn),
+	Keywd("string",		Ltid),
+	Keywd("tagof",		Ltagof),
+	Keywd("tl",		Ltl),
+	Keywd("to",		Lto),
+	Keywd("type",		Ltype),
+	Keywd("while",		Lwhile),
+};
+
+tokwords := array[] of
+{
+	Keywd("&=",	Landeq),
+	Keywd("|=",	Loreq),
+	Keywd("^=",	Lxoreq),
+	Keywd("<<=",	Llsheq),
+	Keywd(">>=",	Lrsheq),
+	Keywd("+=",	Laddeq),
+	Keywd("-=",	Lsubeq),
+	Keywd("*=",	Lmuleq),
+	Keywd("/=",	Ldiveq),
+	Keywd("%=",	Lmodeq),
+	Keywd("**=",	Lexpeq),
+	Keywd(":=",	Ldeclas),
+	Keywd("||",	Loror),
+	Keywd("&&",	Landand),
+	Keywd("::",	Lcons),
+	Keywd("==",	Leq),
+	Keywd("!=",	Lneq),
+	Keywd("<=",	Lleq),
+	Keywd(">=",	Lgeq),
+	Keywd("<<",	Llsh),
+	Keywd(">>",	Lrsh),
+	Keywd("<-",	Lcomm),
+	Keywd("++", 	Linc),
+	Keywd("--",	Ldec),
+	Keywd("->", 	Lmdot),
+	Keywd("=>", 	Llabs),
+	Keywd("**",	Lexp),
+	Keywd("EOF",	Leof),
+};
+
+lexinit()
+{
+	for(i := 0; i < 256; i++){
+		map[i] = byte 0;
+		if(i == '_' || i > 16ra0)
+			map[i] |= Munder;
+		if(i >= 'A' && i <= 'Z')
+			map[i] |= Mupper;
+		if(i >= 'a' && i <= 'z')
+			map[i] |= Mlower;
+		if(i >= 'A' && i <= 'F' || i >= 'a' && i <= 'f')
+			map[i] |= Mhex;
+		if(i == 'e' || i == 'E')
+			map[i] |= Mexp;
+		if(i == 'r' || i == 'R')
+			map[i] |= Mradix;
+		if(i == '-' || i == '+')
+			map[i] |= Msign;
+		if(i >= '0' && i <= '9')
+			map[i] |= Mdigit;
+	}
+
+	for(i = 0; i < len keywords; i++)
+		enter(keywords[i].name, keywords[i].token);
+}
+
+cmap(c: int): byte
+{
+	if(c<0)
+		return byte 0;
+	if(c<256)
+		return map[c];
+	return Mlower;
+}
+
+lexstart(in: string)
+{
+	savec = 0;
+	bstack = 0;
+	nfiles = 0;
+	addfile(ref File(in, 1, 0, -1, nil, 0, -1));
+	bin = bins[bstack];
+	lineno = 1;
+	linepos = Linestart;
+
+	(srcdir, nil) = str->splitr(in, "/");
+}
+
+getc(): int
+{
+	if(c := savec){
+		if(savec >= 0){
+			linepos++;
+			savec = 0;
+		}
+		return c;
+	}
+	c = bin.getc();
+	if(c < 0){
+		savec = -1;
+		return savec;
+	}
+	linepos++;
+	return c;
+}
+
+#
+# dumps '\u0000' chararcters
+#
+ungetc(c: int)
+{
+	if(c > 0)
+		linepos--;
+	savec = c;
+}
+
+addinclude(s: string)
+{
+	for(i := 0; i < MaxIncPath; i++){
+		if(incpath[i] == nil){
+			incpath[i] = s;
+			return;
+		}
+	}
+	fatal("out of include path space");
+}
+
+addfile(f: ref File): int
+{
+	if(lastfile >= nfiles)
+		lastfile = 0;
+	if(nfiles >= len files){
+		nf := array[nfiles+32] of ref File;
+		nf[0:] = files;
+		files = nf;
+	}
+	files[nfiles] = f;
+	return nfiles++;
+}
+
+#
+# include a new file
+#
+includef(file: ref Sym)
+{
+	linestack[bstack] = (lineno, linepos);
+	bstack++;
+	if(bstack >= MaxInclude)
+		fatal(lineconv(lineno<<PosBits)+": include file depth too great");
+	buf := file.name;
+	if(buf[0] != '/')
+		buf = srcdir+buf;
+	b := bufio->open(buf, Bufio->OREAD);
+	for(i := 0; b == nil && i < MaxIncPath && incpath[i] != nil && file.name[0] != '/'; i++){
+		buf = incpath[i] + "/" + file.name;
+		b = bufio->open(buf, Bufio->OREAD);
+	}
+	bins[bstack] = b;
+	if(bins[bstack] == nil){
+		yyerror("can't include "+file.name+": "+sprint("%r"));
+		bstack--;
+	}else{
+		addfile(ref File(buf, lineno+1, -lineno, lineno, nil, 0, -1));
+		lineno++;
+		linepos = Linestart;
+	}
+	bin = bins[bstack];
+}
+
+#
+# we hit eof in the current file
+# revert to the file which included it.
+#
+popinclude()
+{
+	savec = 0;
+	bstack--;
+	bin = bins[bstack];
+	(oline, opos) := linestack[bstack];
+	(f, ln) := fline(oline);
+	lineno++;
+	linepos = opos;
+	addfile(ref File(f.name, lineno, ln-lineno, f.in, f.act, f.actoff, -1));
+}
+
+#
+# convert an absolute Line into a file and line within the file
+#
+fline(absline: int): (ref File, int)
+{
+	if(absline < files[lastfile].abs
+	|| lastfile+1 < nfiles && absline >= files[lastfile+1].abs){
+		lastfile = 0;
+		l := 0;
+		r := nfiles - 1;
+		while(l <= r){
+			m := (r + l) / 2;
+			s := files[m].abs;
+			if(s <= absline){
+				l = m + 1;
+				lastfile = m;
+			}else
+				r = m - 1;
+		}
+	}
+	return (files[lastfile], absline + files[lastfile].off);
+}
+
+#
+# read a comment; process #line file renamings
+#
+lexcom(): int
+{
+	i := 0;
+	buf := "";
+	while((c := getc()) != '\n'){
+		if(c == Bufio->EOF)
+			return -1;
+		buf[i++] = c;
+	}
+
+	lineno++;
+	linepos = Linestart;
+
+	if(len buf < 6
+	|| buf[len buf - 1] != '"'
+	|| buf[:5] != "line " && buf[:5] != "line\t")
+		return 0;
+	for(s := 5; buf[s] == ' ' || buf[s] == '\t'; s++)
+		;
+	if((cmap(buf[s]) & Mdigit) == byte 0)
+		return 0;
+	n := 0;
+	for(; (cmap(c = buf[s]) & Mdigit) != byte 0; s++)
+		n = n * 10 + c - '0';
+	for(; buf[s] == ' ' || buf[s] == '\t'; s++)
+		;
+	if(buf[s++] != '"')
+		return 0;
+	buf = buf[s:len buf - 1];
+	f := files[nfiles - 1];
+	if(n == f.off+lineno && buf == f.name)
+		return 1;
+	act := f.name;
+	actline := lineno + f.off;
+	if(f.act != nil){
+		actline += f.actoff;
+		act = f.act;
+	}
+	addfile(ref File(buf, lineno, n-lineno, f.in, act, actline - n, -1));
+
+	return 1;
+}
+
+curline(): Line
+{
+	return (lineno << PosBits) | (linepos & PosMask);
+}
+
+lineconv(line: Line): string
+{
+	line >>= PosBits;
+	if(line < 0)
+		return "<noline>";
+	(f, ln) := fline(line);
+	s := "";
+	if(f.in >= 0){
+		s = ": " + lineconv(f.in << PosBits);
+	}
+	if(f.act != nil)
+		s = " [ " + f.act + ":" + string(f.actoff+ln) + " ]" + s;
+	return f.name + ":" + string ln + s;
+}
+
+posconv(s: Line): string
+{
+	if(s < 0)
+		return "nopos";
+	spos := s & PosMask;
+	s >>= PosBits;
+	(f, ln) := fline(s);
+	return f.name + ":" + string ln + "." + string spos;
+}
+
+srcconv(src: Src): string
+{
+	s := posconv(src.start);
+	s[len s] = ',';
+	s += posconv(src.stop);
+	return s;
+}
+
+lexid(c: int): int
+{
+	id := "";
+	i := 0;
+	for(;;){
+		if(i < StrSize)
+			id[i++] = c;
+		c = getc();
+		if(c == Bufio->EOF
+		|| (cmap(c) & (Malpha|Mdigit)) == byte 0){
+			ungetc(c);
+			break;
+		}
+	}
+	sym := enter(id, Lid);
+	t := sym.token;
+	if(t == Lid || t == Ltid)
+		yyctxt.lval.tok.v.idval = sym;
+	return t;
+}
+
+maxfast := array[37] of
+{
+	2 =>	31,
+	4 =>	15,
+	8 =>	10,
+	10 =>	9,
+	16 =>	7,
+	32 =>	6,
+	* =>	0,
+};
+
+strtoi(t: string, bbase: big): big
+{
+	#
+	# do the first part in ints
+	#
+	v := 0;
+	bv: big;
+	base := int bbase;
+	n := maxfast[base];
+
+	neg := 0;
+	i := 0;
+	if(i < len t && t[i] == '-'){
+		neg = 1;
+		i++;
+	}else if(i < len t && t[i] == '+')
+		i++;
+
+	for(; i < len t; i++){
+		c := t[i];
+		if(c >= '0' && c <= '9')
+			c -= '0';
+		else if(c >= 'a' && c <= 'z')
+			c -= 'a' - 10;
+		else
+			c -= 'A' - 10;
+		if(c >= base){
+			yyerror("digit '"+t[i:i+1]+"' is not radix "+string base);
+			return big -1;
+		}
+		if(i < n)
+			v = v * base + c;
+		else{
+			if(i == n)
+				bv = big v;
+			bv = bv * bbase + big c;
+		}
+	}
+	if(i <= n)
+		bv = big v;
+	if(neg)
+		return -bv;
+	return bv;
+}
+
+digit(c: int, base: int): int
+{
+	ck: byte;
+	cc: int;
+
+	cc = c;
+	ck = cmap(c);
+	if((ck & Mdigit) != byte 0)
+		c -= '0';
+	else if((ck & Mlower) != byte 0)
+		c = c - 'a' + 10;
+	else if((ck & Mupper) != byte 0)
+		c = c - 'A' + 10;
+	else if((ck & Munder) != byte 0)
+		;
+	else
+		return -1;
+	if(c >= base){
+		s := "z";
+		s[0] = cc;
+		yyerror("digit '" + s + "' not radix " + string base);
+	}
+	return c;
+}
+
+strtodb(t: string, base: int): real
+{
+	num, dem, rbase: real;
+	neg, eneg, dig, exp, c, d: int;
+
+	t[len t] = 0;
+
+	num = 0.0;
+	rbase = real base;
+	neg = 0;
+	dig = 0;
+	exp = 0;
+	eneg = 0;
+
+	i := 0;
+	c = t[i++];
+	if(c == '-' || c == '+'){
+		if(c == '-')
+			neg = 1;
+		c = t[i++];
+	}
+	while((d = digit(c, base)) >= 0){
+		num = num*rbase + real d;
+		c = t[i++];
+	}
+	if(c == '.')
+		c = t[i++];
+	while((d = digit(c, base)) >= 0){
+		num = num*rbase + real d;
+		dig++;
+		c = t[i++];
+	}
+	if(c == 'e' || c == 'E'){
+		c = t[i++];
+		if(c == '-' || c == '+'){
+			if(c == '-'){
+				dig = -dig;
+				eneg = 1;
+			}
+			c = t[i++];
+		}
+		while((d = digit(c, base)) >= 0){
+			exp = exp*base + d;
+			c = t[i++];
+		}
+	}
+	exp -= dig;
+	if(exp < 0){
+		exp = -exp;
+		eneg = !eneg;
+	}
+	dem = rpow(rbase, exp);
+	if(eneg)
+		num /= dem;
+	else
+		num *= dem;
+	if(neg)
+		return -num;
+	return num;
+}
+
+#
+# parse a numeric identifier
+# format [0-9]+(r[0-9A-Za-z]+)?
+# or ([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?
+#
+lexnum(c: int): int
+{
+	Int, Radix, RadixSeen, Frac, ExpSeen, ExpSignSeen, Exp, FracB: con iota;
+
+	i := 0;
+	buf := "";
+	buf[i++] = c;
+	state := Int;
+	if(c == '.')
+		state = Frac;
+	radix := "";
+
+done:	for(;;){
+		c = getc();
+		if(c == Bufio->EOF){
+			yyerror("end of file in numeric constant");
+			return Leof;
+		}
+
+		ck := cmap(c);
+		case state{
+		Int =>
+			if((ck & Mdigit) != byte 0)
+				break;
+			if((ck & Mexp) != byte 0){
+				state = ExpSeen;
+				break;
+			}
+			if((ck & Mradix) != byte 0){
+				radix = buf;
+				buf = "";
+				i = 0;
+				state = RadixSeen;
+				break;
+			}
+			if(c == '.'){
+				state = Frac;
+				break;
+			}
+			break done;
+		RadixSeen or
+		Radix =>
+			if((ck & (Mdigit|Malpha)) != byte 0){
+				state = Radix;
+				break;
+			}
+			if(c == '.'){
+				state = FracB;
+				break;
+			}
+			break done;
+		Frac =>
+			if((ck & Mdigit) != byte 0)
+				break;
+			if((ck & Mexp) != byte 0)
+				state = ExpSeen;
+			else
+				break done;
+		FracB =>
+			if((ck & (Mdigit|Malpha)) != byte 0)
+				break;
+			break done;
+		ExpSeen =>
+			if((ck & Msign) != byte 0){
+				state = ExpSignSeen;
+				break;
+			}
+			if((ck & Mdigit) != byte 0){
+				state = Exp;
+				break;
+			}
+			break done;
+		ExpSignSeen or
+		Exp =>
+			if((ck & Mdigit) != byte 0){
+				state = Exp;
+				break;
+			}
+			break done;
+		}
+		buf[i++] = c;
+	}
+
+	ungetc(c);
+	v: big;
+	case state{
+	* =>
+		yyerror("malformed numerical constant '"+radix+buf+"'");
+		yyctxt.lval.tok.v.ival = big 0;
+		return Lconst;
+	Radix =>
+		v = strtoi(radix, big 10);
+		if(v < big 2 || v > big 36){
+			yyerror("radix '"+radix+"' is not between 2 and 36");
+			break;
+		}
+		v = strtoi(buf[1:], v);
+	Int =>
+		v = strtoi(buf, big 10);
+	Frac or
+	Exp =>
+		yyctxt.lval.tok.v.rval = real buf;
+		return Lrconst;
+	FracB =>
+		v = strtoi(radix, big 10);
+		if(v < big 2 || v > big 36){
+			yyerror("radix '"+radix+"' is not between 2 and 36");
+			break;
+		}
+		yyctxt.lval.tok.v.rval = strtodb(buf[1:], int v);
+		return Lrconst;
+	}
+	yyctxt.lval.tok.v.ival = v;
+	return Lconst;
+}
+
+escchar(): int
+{
+	c := getc();
+	if(c == Bufio->EOF)
+		return Bufio->EOF;
+	if(c == 'u'){
+		v := 0;
+		for(i := 0; i < 4; i++){
+			c = getc();
+			ck := cmap(c);
+			if(c == Bufio->EOF || (ck & (Mdigit|Mhex)) == byte 0){
+				yyerror("malformed \\u escape sequence");
+				ungetc(c);
+				break;
+			}
+			if((ck & Mdigit) != byte 0)
+				c -= '0';
+			else if((ck & Mlower) != byte 0)
+				c = c - 'a' + 10;
+			else if((ck & Mupper) != byte 0)
+				c = c - 'A' + 10;
+			v = v * 16 + c;
+		}
+		return v;
+	}
+	if(c < len escmap && (v := escmap[c]) >= 0)
+		return v;
+	s := "";
+	s[0] = c;
+	yyerror("unrecognized escape \\"+s);
+	return c;
+}
+
+lexstring()
+{
+	s := "";
+	i := 0;
+Loop:
+	for(;;){
+		case c := getc(){
+		'\\' =>
+			c = escchar();
+			if(c != Bufio->EOF)
+				s[i++] = c;
+		Bufio->EOF =>
+			yyerror("end of file in string constant");
+			break Loop;
+		'\n' =>
+			yyerror("newline in string constant");
+			lineno++;
+			linepos = Linestart;
+			break Loop;
+		'"' =>
+			break Loop;
+		* =>
+			s[i++] = c;
+		}
+	}
+	yyctxt.lval.tok.v.idval = enterstring(s);
+}
+
+lexrawstring()
+{
+	s := "";
+	i := 0;
+	startlno := lineno;
+Loop:
+	for(;;){
+		case c := getc(){
+		Bufio->EOF =>
+			t := lineno;
+			lineno = startlno;
+			yyerror("end of file in raw string constant");
+			lineno = t;
+			break Loop;
+		'\n' =>
+			s[i++] = c;
+			lineno++;
+			linepos = Linestart;
+		'`' =>
+			break Loop;
+		* =>
+			s[i++] = c;
+		}
+	}
+	yyctxt.lval.tok.v.idval = enterstring(s);
+}
+
+lex(): int
+{
+	for(;;){
+		yyctxt.lval.tok.src.start = (lineno << PosBits) | (linepos & PosMask);
+		case c := getc(){
+		Bufio->EOF =>
+			bin.close();
+			if(bstack == 0)
+				return Leof;
+			popinclude();
+		'#' =>
+			if(lexcom() < 0){
+				bin.close();
+				if(bstack == 0)
+					return Leof;
+				popinclude();
+			}
+		'\n' =>
+			lineno++;
+			linepos = Linestart;
+		' ' or
+		'\t' or
+		'\r' or
+		'\v' =>
+			;
+		'"' =>
+			lexstring();
+			return Lsconst;
+		'`' =>
+			lexrawstring();
+			return Lsconst;
+		'\'' =>
+			c = getc();
+			if(c == '\\')
+				c = escchar();
+			if(c == Bufio->EOF){
+				yyerror("end of file in character constant");
+				return Bufio->EOF;
+			}else
+				yyctxt.lval.tok.v.ival = big c;
+			c = getc();
+			if(c != '\''){
+				yyerror("missing closing '");
+				ungetc(c);
+			}
+			return Lconst;
+		'(' or
+		')' or
+		'[' or
+		']' or
+		'{' or
+		'}' or
+		',' or
+		';' or
+		'~' =>
+			return c;
+		':' =>
+			c = getc();
+			if(c == ':')
+				return Lcons;
+			if(c == '=')
+				return Ldeclas;
+			ungetc(c);
+			return ':';
+		'.' =>
+			c = getc();
+			ungetc(c);
+			if(c != Bufio->EOF && (cmap(c) & Mdigit) != byte 0)
+				return lexnum('.');
+			return '.';
+		'|' =>
+			c = getc();
+			if(c == '=')
+				return Loreq;
+			if(c == '|')
+				return Loror;
+			ungetc(c);
+			return '|';
+		'&' =>
+			c = getc();
+			if(c == '=')
+				return Landeq;
+			if(c == '&')
+				return Landand;
+			ungetc(c);
+			return '&';
+		'^' =>
+			c = getc();
+			if(c == '=')
+				return Lxoreq;
+			ungetc(c);
+			return '^';
+		'*' =>
+			c = getc();
+			if(c == '=')
+				return Lmuleq;
+			if(c == '*'){
+				c = getc();
+				if(c == '=')
+					return Lexpeq;
+				ungetc(c);
+				return Lexp;
+			}
+			ungetc(c);
+			return '*';
+		'/' =>
+			c = getc();
+			if(c == '=')
+				return Ldiveq;
+			ungetc(c);
+			return '/';
+		'%' =>
+			c = getc();
+			if(c == '=')
+				return Lmodeq;
+			ungetc(c);
+			return '%';
+		'=' =>
+			c = getc();
+			if(c == '=')
+				return Leq;
+			if(c == '>')
+				return Llabs;
+			ungetc(c);
+			return '=';
+		'!' =>
+			c = getc();
+			if(c == '=')
+				return Lneq;
+			ungetc(c);
+			return '!';
+		'>' =>
+			c = getc();
+			if(c == '=')
+				return Lgeq;
+			if(c == '>'){
+				c = getc();
+				if(c == '=')
+					return Lrsheq;
+				ungetc(c);
+				return Lrsh;
+			}
+			ungetc(c);
+			return '>';
+		'<' =>
+			c = getc();
+			if(c == '=')
+				return Lleq;
+			if(c == '-')
+				return Lcomm;
+			if(c == '<'){
+				c = getc();
+				if(c == '=')
+					return Llsheq;
+				ungetc(c);
+				return Llsh;
+			}
+			ungetc(c);
+			return '<';
+		'+' =>
+			c = getc();
+			if(c == '=')
+				return Laddeq;
+			if(c == '+')
+				return Linc;
+			ungetc(c);
+			return '+';
+		'-' =>
+			c = getc();
+			if(c == '=')
+				return Lsubeq;
+			if(c == '-')
+				return Ldec;
+			if(c == '>')
+				return Lmdot;
+			ungetc(c);
+			return '-';
+		'0' to '9' =>
+			return lexnum(c);
+		* =>
+			if((cmap(c) & Malpha) != byte 0)
+				return lexid(c);
+			s := "";
+			s[0] = c;
+			yyerror("unknown character '"+s+"'");
+		}
+	}
+}
+
+YYLEX.lex(nil: self ref YYLEX): int
+{
+	t := lex();
+	yyctxt.lval.tok.src.stop = (lineno << PosBits) | (linepos & PosMask);
+	lasttok = t;
+	lastyylval = yyctxt.lval;
+	return t;
+}
+
+toksp(t: int): string
+{
+	case(t){
+		Lconst =>
+			return sprint("%bd", lastyylval.tok.v.ival);
+		Lrconst =>
+			return sprint("%f", lastyylval.tok.v.rval);
+		Lsconst =>
+			return sprint("\"%s\"", lastyylval.tok.v.idval.name);
+		Ltid or Lid =>
+			return lastyylval.tok.v.idval.name;
+	}
+	for(i := 0; i < len keywords; i++)
+		if(t == keywords[i].token)
+			return keywords[i].name;
+	for(i = 0; i < len tokwords; i++)
+		if(t == tokwords[i].token)
+			return tokwords[i].name;
+	if(t < 0 || t > 255)
+		fatal(sprint("bad token %d in toksp()", t));
+	buf := "Z";
+	buf[0] = t;
+	return buf;
+}
+
+enterstring(name: string): ref Sym
+{
+	h := 0;
+	n := len name;
+	for(i := 0; i < n; i++){
+		c := d := name[i];
+		c ^= c << 6;
+		h += (c << 11) ^ (c >> 1);
+		h ^= (d << 14) + (d << 7) + (d << 4) + d;
+	}
+
+	h &= HashSize-1;
+	for(s := strings[h]; s != nil; s = s.next){
+		sn := s.name;
+		if(len sn == n && sn == name)
+			return s;
+	}
+
+
+	s = ref Sym;
+	s.token = -1;
+	s.name = name;
+	s.hash = h;
+	s.next = strings[h];
+	strings[h] = s;
+	return s;
+}
+
+stringcat(s, t: ref Sym): ref Sym
+{
+	return enterstring(s.name+t.name);
+}
+
+enter(name: string, token: int): ref Sym
+{
+	h := 0;
+	n := len name;
+	for(i := 0; i < n; i++){
+		c := d := name[i];
+		c ^= c << 6;
+		h += (c << 11) ^ (c >> 1);
+		h ^= (d << 14) + (d << 7) + (d << 4) + d;
+	}
+
+	h &= HashSize-1;
+	for(s := symbols[h]; s != nil; s = s.next){
+		sn := s.name;
+		if(len sn == n && sn == name)
+			return s;
+	}
+
+	if(token == 0)
+		token = Lid;
+	s = ref Sym;
+	s.token = token;
+	s.name = name;
+	s.hash = h;
+	s.next = symbols[h];
+	symbols[h] = s;
+	return s;
+}
+
+stringpr(sym: ref Sym): string
+{
+	s := sym.name;
+	n := len s;
+	if(n > 10)
+		n = 10;
+	sb := "\"";
+	for(i := 0; i < n; i++){
+		case c := s[i]{
+		'\\' or
+		'"' or
+		'\n' or
+		'\r' or
+		'\t' or
+		'\b' or
+		'\a' or
+		'\v' or
+		'\u0000' =>
+			sb[len sb] = '\\';
+			sb[len sb] = unescmap[c];
+		* =>
+			sb[len sb] = c;
+		}
+	}
+	if(n != len s)
+		sb += "...";
+	sb[len sb] = '"';
+	return sb;
+}
+
+warn(line: Line, msg: string)
+{
+	if(errors || !dowarn)
+		return;
+	fprint(stderr, "%s: warning: %s\n", lineconv(line), msg);
+}
+
+nwarn(n: ref Node, msg: string)
+{
+	if(errors || !dowarn)
+		return;
+	fprint(stderr, "%s: warning: %s\n", lineconv(n.src.start), msg);
+}
+
+error(line: Line, msg: string)
+{
+	errors++;
+	if(errors > maxerr)
+		return;
+	fprint(stderr, "%s: %s\n", lineconv(line), msg);
+	if(errors == maxerr)
+		fprint(stderr, "too many errors, stopping\n");
+}
+
+nerror(n: ref Node, msg: string)
+{
+	errors++;
+	if(errors > maxerr)
+		return;
+	fprint(stderr, "%s: %s\n", lineconv(n.src.start), msg);
+	if(errors == maxerr)
+		fprint(stderr, "too many errors, stopping\n");
+}
+
+YYLEX.error(nil: self ref YYLEX, msg: string)
+{
+	errors++;
+	if(errors > maxerr)
+		return;
+	if(lasttok != 0)
+		fprint(stderr, "%s: near ` %s ` : %s\n", lineconv(lineno<<PosBits), toksp(lasttok), msg);
+	else
+		fprint(stderr, "%s: %s\n", lineconv(lineno<<PosBits), msg);
+	if(errors == maxerr)
+		fprint(stderr, "too many errors, stopping\n");
+}
+
+yyerror(msg: string)
+{
+	yyctxt.error(msg);
+}
+
+fatal(msg: string)
+{
+	if(errors == 0 || fabort)
+		fprint(stderr, "fatal limbo compiler error: %s\n", msg);
+	if(bout != nil)
+		sys->remove(outfile);
+	if(fabort){
+		n: ref Node;
+		if(n.ty == nil);	# abort
+	}
+	raise "fail:error";
+}
+
+hex(v, n: int): string
+{
+	return sprint("%.*ux", n, v);
+}
+
+bhex(v: big, n: int): string
+{
+	return sprint("%.*bux", n, v);
+}
--- /dev/null
+++ b/appl/cmd/limbo/limbo.b
@@ -1,0 +1,3104 @@
+implement Limbo;
+
+#line	2	"limbo.y"
+include "limbo.m";
+include "draw.m";
+
+Limbo: module {
+
+	init:		fn(ctxt: ref Draw->Context, argv: list of string);
+
+	YYSTYPE: adt{
+		tok:	Tok;
+		ids:	ref Decl;
+		node:	ref Node;
+		ty:	ref Type;
+		types:	ref Typelist;
+	};
+
+	YYLEX: adt {
+		lval: YYSTYPE;
+		lex: fn(nil: self ref YYLEX): int;
+		error: fn(nil: self ref YYLEX, err: string);
+	};
+Landeq: con	57346;
+Loreq: con	57347;
+Lxoreq: con	57348;
+Llsheq: con	57349;
+Lrsheq: con	57350;
+Laddeq: con	57351;
+Lsubeq: con	57352;
+Lmuleq: con	57353;
+Ldiveq: con	57354;
+Lmodeq: con	57355;
+Lexpeq: con	57356;
+Ldeclas: con	57357;
+Lload: con	57358;
+Loror: con	57359;
+Landand: con	57360;
+Lcons: con	57361;
+Leq: con	57362;
+Lneq: con	57363;
+Lleq: con	57364;
+Lgeq: con	57365;
+Llsh: con	57366;
+Lrsh: con	57367;
+Lexp: con	57368;
+Lcomm: con	57369;
+Linc: con	57370;
+Ldec: con	57371;
+Lof: con	57372;
+Lref: con	57373;
+Lif: con	57374;
+Lelse: con	57375;
+Lfn: con	57376;
+Lexcept: con	57377;
+Lraises: con	57378;
+Lmdot: con	57379;
+Lto: con	57380;
+Lor: con	57381;
+Lrconst: con	57382;
+Lconst: con	57383;
+Lid: con	57384;
+Ltid: con	57385;
+Lsconst: con	57386;
+Llabs: con	57387;
+Lnil: con	57388;
+Llen: con	57389;
+Lhd: con	57390;
+Ltl: con	57391;
+Ltagof: con	57392;
+Limplement: con	57393;
+Limport: con	57394;
+Linclude: con	57395;
+Lcon: con	57396;
+Ltype: con	57397;
+Lmodule: con	57398;
+Lcyclic: con	57399;
+Ladt: con	57400;
+Larray: con	57401;
+Llist: con	57402;
+Lchan: con	57403;
+Lself: con	57404;
+Ldo: con	57405;
+Lwhile: con	57406;
+Lfor: con	57407;
+Lbreak: con	57408;
+Lalt: con	57409;
+Lcase: con	57410;
+Lpick: con	57411;
+Lcont: con	57412;
+Lreturn: con	57413;
+Lexit: con	57414;
+Lspawn: con	57415;
+Lraise: con	57416;
+Lfix: con	57417;
+Ldynamic: con	57418;
+
+};
+
+#line	27	"limbo.y"
+	#
+	# lex.b
+	#
+	signdump:	string;			# name of function for sig debugging
+	superwarn:	int;
+	debug:		array of int;
+	noline:		Line;
+	nosrc:		Src;
+	arrayz:		int;
+	oldcycles:	int;
+	emitcode:	string;			# emit stub routines for system module functions
+	emitdyn: int;				# emit as above but for dynamic modules
+	emitsbl:	string;			# emit symbol file for sysm modules
+	emitstub:	int;			# emit type and call frames for system modules
+	emittab:	string;			# emit table of runtime functions for this module
+	errors:		int;
+	mustcompile:	int;
+	dontcompile:	int;
+	asmsym:		int;			# generate symbols in assembly language?
+	bout:		ref Bufio->Iobuf;	# output file
+	bsym:		ref Bufio->Iobuf;	# symbol output file; nil => no sym out
+	gendis:		int;			# generate dis or asm?
+	fixss:		int;
+	newfnptr:	int;		# ISELF and -ve indices
+	optims: int;
+
+	#
+	# decls.b
+	#
+	scope:		int;
+	# impmod:		ref Sym;		# name of implementation module
+	impmods:		ref Decl;		# name of implementation module(s)
+	nildecl:	ref Decl;		# declaration for limbo's nil
+	selfdecl:	ref Decl;		# declaration for limbo's self
+
+	#
+	# types.b
+	#
+	tany:		ref Type;
+	tbig:		ref Type;
+	tbyte:		ref Type;
+	terror:		ref Type;
+	tint:		ref Type;
+	tnone:		ref Type;
+	treal:		ref Type;
+	tstring:	ref Type;
+	texception:	ref Type;
+	tunknown:	ref Type;
+	tfnptr:	ref Type;
+	rtexception:	ref Type;
+	descriptors:	ref Desc;		# list of all possible descriptors
+	tattr:		array of Tattr;
+
+	#
+	# nodes.b
+	#
+	opcommute:	array of int;
+	oprelinvert:	array of int;
+	isused:		array of int;
+	casttab:	array of array of int;	# instruction to cast from [1] to [2]
+
+	nfns:		int;			# functions defined
+	nfnexp:		int;
+	fns:		array of ref Decl;	# decls for fns defined
+	tree:		ref Node;		# root of parse tree
+
+	parset:		int;			# time to parse
+	checkt:		int;			# time to typecheck
+	gent:		int;			# time to generate code
+	writet:		int;			# time to write out code
+	symt:		int;			# time to write out symbols
+YYEOFCODE: con 1;
+YYERRCODE: con 2;
+YYMAXDEPTH: con 200;
+
+#line	1632	"limbo.y"
+
+
+include "ipints.m";
+include "crypt.m";
+
+sys:	Sys;
+	print, fprint, sprint: import sys;
+
+bufio:	Bufio;
+	Iobuf: import bufio;
+
+str:		String;
+
+crypt:Crypt;
+	md5: import crypt;
+
+math:	Math;
+	import_real, export_real, isnan: import math;
+
+yyctxt: ref YYLEX;
+
+canonnan: real;
+
+debug	= array[256] of {* => 0};
+
+noline	= -1;
+nosrc	= Src(-1, -1);
+
+infile:	string;
+
+# front end
+include "arg.m";
+include "lex.b";
+include "types.b";
+include "nodes.b";
+include "decls.b";
+
+include "typecheck.b";
+
+# back end
+include "gen.b";
+include "ecom.b";
+include "asm.b";
+include "dis.b";
+include "sbl.b";
+include "stubs.b";
+include "com.b";
+include "optim.b";
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	s: string;
+
+	sys = load Sys Sys->PATH;
+	crypt = load Crypt Crypt->PATH;
+	math = load Math Math->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil){
+		sys->print("can't load %s: %r\n", Bufio->PATH);
+		raise("fail:bad module");
+	}
+	str = load String String->PATH;
+	if(str == nil){
+		sys->print("can't load %s: %r\n", String->PATH);
+		raise("fail:bad module");
+	}
+
+	stderr = sys->fildes(2);
+	yyctxt = ref YYLEX;
+
+	math->FPcontrol(0, Math->INVAL|Math->ZDIV|Math->OVFL|Math->UNFL|Math->INEX);
+	na := array[1] of {0.};
+	import_real(array[8] of {byte 16r7f, * => byte 16rff}, na);
+	canonnan = na[0];
+	if(!isnan(canonnan))
+		fatal("bad canonical NaN");
+
+	lexinit();
+	typeinit();
+	optabinit();
+
+	gendis = 1;
+	asmsym = 0;
+	maxerr = 20;
+	ofile := "";
+	ext := "";
+
+	arg := Arg.init(argv);
+	while(c := arg.opt()){
+		case c{
+		'Y' =>
+			emitsbl = arg.arg();
+			if(emitsbl == nil)
+				usage();
+		'C' =>
+			dontcompile = 1;
+		'D' =>
+			#
+			# debug flags:
+			#
+			# a	alt compilation
+			# A	array constructor compilation
+			# b	boolean and branch compilation
+			# c	case compilation
+			# d	function declaration
+			# D	descriptor generation
+			# e	expression compilation
+			# E	addressable expression compilation
+			# f	print arguments for compiled functions
+			# F	constant folding
+			# g	print out globals
+			# m	module declaration and type checking
+			# n	nil references
+			# s	print sizes of output file sections
+			# S	type signing
+			# t	type checking function bodies
+			# T	timing
+			# v	global var and constant compilation
+			# x	adt verification
+			# Y	tuple compilation
+			# z Z	bug fixes
+			#
+			s = arg.arg();
+			for(i := 0; i < len s; i++){
+				c = s[i];
+				if(c < len debug)
+					debug[c] = 1;
+			}
+		'I' =>
+			s = arg.arg();
+			if(s == "")
+				usage();
+			addinclude(s);
+		'G' =>
+			asmsym = 1;
+		'S' =>
+			gendis = 0;
+		'a' =>
+			emitstub = 1;
+		'A' =>
+			emitstub = emitdyn = 1;
+		'c' =>
+			mustcompile = 1;
+		'e' =>
+			maxerr = 1000;
+		'f' =>
+			fabort = 1;
+		'F' =>
+			newfnptr = 1;
+		'g' =>
+			dosym = 1;
+		'i' =>
+			dontinline = 1;
+		'o' =>
+			ofile = arg.arg();
+		'O' =>
+			optims = 1;
+		's' =>
+			s = arg.arg();
+			if(s != nil)
+				fixss = int s;
+		't' =>
+			emittab = arg.arg();
+			if(emittab == nil)
+				usage();
+		'T' =>
+			emitcode = arg.arg();
+			if(emitcode == nil)
+				usage();
+		'd' =>
+			emitcode = arg.arg();
+			if(emitcode == nil)
+				usage();
+			emitdyn = 1;
+		'w' =>
+			superwarn = dowarn;
+			dowarn = 1;
+		'x' =>
+			ext = arg.arg();
+		'X' =>
+			signdump = arg.arg();
+		'z' =>
+			arrayz = 1;
+		'y' =>
+			oldcycles = 1;
+		* =>
+			usage();
+		}
+	}
+
+	addinclude("/module");
+
+	argv = arg.argv;
+	arg = nil;
+
+	if(argv == nil){
+		usage();
+	}else if(ofile != nil){
+		if(len argv != 1)
+			usage();
+		translate(hd argv, ofile, mkfileext(ofile, ".dis", ".sbl"));
+	}else{
+		pr := len argv != 1;
+		if(ext == ""){
+			ext = ".s";
+			if(gendis)
+				ext = ".dis";
+		}
+		for(; argv != nil; argv = tl argv){
+			file := hd argv;
+			(nil, s) = str->splitr(file, "/");
+			if(pr)
+				print("%s:\n", s);
+			out := mkfileext(s, ".b", ext);
+			translate(file, out, mkfileext(out, ext, ".sbl"));
+		}
+	}
+	if (toterrors > 0)
+		raise("fail:errors");
+}
+
+usage()
+{
+	fprint(stderr, "usage: limbo [-GSagwe] [-I incdir] [-o outfile] [-{T|t|d} module] [-D debug] file ...\n");
+	raise("fail:usage");
+}
+
+mkfileext(file, oldext, ext: string): string
+{
+	n := len file;
+	n2 := len oldext;
+	if(n >= n2 && file[n-n2:] == oldext)
+		file = file[:n-n2];
+	return file + ext;
+}
+
+translate(in, out, dbg: string)
+{
+	infile = in;
+	outfile = out;
+	errors = 0;
+	bins[0] = bufio->open(in, Bufio->OREAD);
+	if(bins[0] == nil){
+		fprint(stderr, "can't open %s: %r\n", in);
+		toterrors++;
+		return;
+	}
+	doemit := emitcode != "" || emitstub || emittab != "" || emitsbl != "";
+	if(!doemit){
+		bout = bufio->create(out, Bufio->OWRITE, 8r666);
+		if(bout == nil){
+			fprint(stderr, "can't open %s: %r\n", out);
+			toterrors++;
+			bins[0].close();
+			return;
+		}
+		if(dosym){
+			bsym = bufio->create(dbg, Bufio->OWRITE, 8r666);
+			if(bsym == nil)
+				fprint(stderr, "can't open %s: %r\n", dbg);
+		}
+	}
+
+	lexstart(in);
+
+	popscopes();
+	typestart();
+	declstart();
+	nfnexp = 0;
+
+	parset = sys->millisec();
+	yyparse(yyctxt);
+	parset = sys->millisec() - parset;
+
+	checkt = sys->millisec();
+	entry := typecheck(!doemit);
+	checkt = sys->millisec() - checkt;
+
+	modcom(entry);
+
+	fns = nil;
+	nfns = 0;
+	descriptors = nil;
+
+	if(debug['T'])
+		print("times: parse=%d type=%d: gen=%d write=%d symbols=%d\n",
+			parset, checkt, gent, writet, symt);
+
+	if(bout != nil)
+		bout.close();
+	if(bsym != nil)
+		bsym.close();
+	toterrors += errors;
+	if(errors && bout != nil)
+		sys->remove(out);
+	if(errors && bsym != nil)
+		sys->remove(dbg);
+}
+
+pwd(): string
+{
+	workdir := load Workdir Workdir->PATH;
+	if(workdir == nil)
+		cd := "/";
+	else
+		cd = workdir->init();
+	# sys->print("pwd: %s\n", cd);
+	return cd;
+}
+
+cleanname(s: string): string
+{
+	ls, path: list of string;
+
+	if(s == nil)
+		return nil;
+	if(s[0] != '/' && s[0] != '\\')
+		(nil, ls) = sys->tokenize(pwd(), "/\\");
+	for( ; ls != nil; ls = tl ls)
+		path = hd ls :: path;
+	(nil, ls) = sys->tokenize(s, "/\\");
+	for( ; ls != nil; ls = tl ls){
+		n := hd ls;
+		if(n == ".")
+			;
+		else if (n == ".."){
+			if(path != nil)
+				path = tl path;
+		}
+		else
+			path = n :: path;
+	}
+	p := "";
+	for( ; path != nil; path = tl path)
+		p = "/" + hd path + p;
+	if(p == nil)
+		p = "/";
+	# sys->print("cleanname: %s\n", p);
+	return p;
+}
+
+srcpath(): string
+{
+	srcp := cleanname(infile);
+	# sys->print("srcpath: %s\n", srcp);
+	return srcp;
+}
+yyexca := array[] of {-1, 1,
+	1, -1,
+	-2, 0,
+-1, 3,
+	1, 3,
+	-2, 0,
+-1, 17,
+	39, 88,
+	50, 62,
+	54, 88,
+	99, 62,
+	-2, 252,
+-1, 211,
+	59, 29,
+	71, 29,
+	-2, 0,
+-1, 230,
+	1, 2,
+	-2, 0,
+-1, 273,
+	50, 176,
+	-2, 257,
+-1, 308,
+	59, 41,
+	71, 41,
+	91, 41,
+	-2, 0,
+-1, 310,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 380,
+	50, 62,
+	99, 62,
+	-2, 252,
+-1, 381,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 387,
+	53, 71,
+	54, 71,
+	-2, 110,
+-1, 389,
+	53, 72,
+	54, 72,
+	-2, 112,
+-1, 421,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 428,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 443,
+	53, 71,
+	54, 71,
+	-2, 111,
+-1, 444,
+	53, 72,
+	54, 72,
+	-2, 113,
+-1, 452,
+	71, 279,
+	99, 279,
+	-2, 163,
+-1, 469,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 486,
+	50, 126,
+	99, 126,
+	-2, 239,
+-1, 491,
+	71, 276,
+	-2, 0,
+-1, 503,
+	59, 47,
+	71, 47,
+	-2, 0,
+-1, 508,
+	59, 41,
+	71, 41,
+	91, 41,
+	-2, 0,
+-1, 514,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 548,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 554,
+	71, 154,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 562,
+	56, 59,
+	62, 59,
+	-2, 62,
+-1, 568,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 573,
+	71, 157,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 577,
+	72, 176,
+	-2, 163,
+-1, 596,
+	71, 160,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 602,
+	71, 168,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 606,
+	72, 175,
+	85, 150,
+	86, 150,
+	87, 150,
+	89, 150,
+	90, 150,
+	91, 150,
+	-2, 0,
+-1, 609,
+	50, 62,
+	56, 171,
+	62, 171,
+	99, 62,
+	-2, 252,
+};
+YYNPROD: con 284;
+YYPRIVATE: con 57344;
+yytoknames: array of string;
+yystates: array of string;
+yydebug: con 0;
+YYLAST:	con 2727;
+yyact := array[] of {
+ 379, 591, 453, 364, 505, 384, 412, 310, 369, 314,
+ 359, 451, 449, 185,  84,  83, 432, 298, 270,  15,
+   8,  49, 213, 102, 320,  12,  42, 110,  48,  78,
+  79,  80,   4,  35, 198,  51,  23, 544, 363,   6,
+ 423,   3,   6, 486, 459, 382, 365,  14, 458,  21,
+  14, 353, 350, 293, 285, 491, 118, 225, 400, 330,
+ 286, 226,  31, 223,  46, 112, 465,  11, 105, 517,
+ 420, 419, 418, 186, 164, 165, 166, 167, 168, 169,
+ 170, 171, 172, 173, 174, 175, 176,  43, 117, 422,
+ 182, 183, 184, 599,  71,  10, 286, 205,  10, 208,
+  93, 349, 286, 601, 119, 349,  32, 114,  40, 349,
+ 294,  32, 294, 286,  44, 119, 428, 427, 426, 308,
+ 430, 429, 431, 585, 231, 232, 233, 234, 235, 236,
+ 237, 238, 239, 240, 241, 242, 309, 244, 245, 246,
+ 247, 248, 249, 250, 251, 252, 253, 254, 255, 256,
+ 257, 258, 259, 260, 261, 262, 263, 264, 265, 186,
+   6, 547, 273, 230,  37,  22, 194, 195,  14,  22,
+ 271, 485, 267, 210,   5, 483, 482, 565, 279, 481,
+ 513, 410, 284,  87, 438, 559, 424, 228, 409, 288,
+  85, 407,  94,  90, 289,  99, 269, 415, 217, 202,
+   5, 415,  47,  92,  82,  22, 209,  26, 303,  25,
+ 212,  19,  24, 218, 229, 566,  10, 354,  96, 595,
+  98,  95, 100, 572, 101,  88,  89,  86, 557, 194,
+ 195,  17,  87, 312, 311,  18, 297,  19, 187,  85,
+ 553,  77,  90, 313, 326, 305, 536,  13, 512, 112,
+ 323, 318,  92,  82, 525, 207, 490,  17,  87, 468,
+ 399,  18, 383,  23, 508,  85, 316, 215,  90,   6,
+ 498,   2, 500,  13,  88,  89,  86,  14,  92,  82,
+ 194, 195, 479, 186,  43, 467, 398, 340, 194, 195,
+  77, 114, 193, 361, 282, 499, 338, 182, 500, 535,
+  88,  89,  86, 336, 194, 195, 219, 530,  87, 211,
+ 341,  44,  87, 324, 580,  85,  77, 325,  90,  85,
+ 381, 348,  90, 206,  19,  10, 358, 357,  92,  82,
+ 579, 393,  92,  82, 604, 214, 389, 387, 391, 448,
+ 614, 194, 195, 402,  45, 539, 194, 195,  18, 392,
+  88,  89,  86, 356,  88,  89,  86, 321, 194, 195,
+ 192, 385,  72, 403, 404, 495,  77,  33, 317, 108,
+  77, 416,  73,  19,  19, 421, 436, 301, 281, 186,
+  76,  75,  45, 435,  74, 437,  18, 216, 487, 493,
+ 434, 441, 439, 115, 115, 612, 564, 116, 116, 452,
+ 488, 340, 183, 444, 443, 507, 414,  45, 316, 604,
+ 562,  18, 493, 543, 493, 603, 336, 493, 600,  70,
+ 597, 493,  63, 588, 504,  73, 473, 574, 469,  22,
+ 478, 442, 476,  76,  75,  69,  68,  74, 480,  18,
+  54,  55,  62,  60,  61,  64,  87, 433, 291, 452,
+ 290,  91, 268,  85,  91, 157,  90,  65,  66,  67,
+ 120, 489, 493, 104, 497, 493,  92,  82, 555, 540,
+ 594, 494, 186,  77, 159, 477, 168, 194, 195, 103,
+ 523, 507, 522, 515, 516, 511, 406, 510,  88,  89,
+  86,  87, 452, 527, 523, 529, 528, 487,  85, 518,
+ 533,  90, 593, 526,  77,  91,  39,  91, 532, 537,
+ 466,  92,  82, 417, 545,  91, 408, 568, 546, 541,
+ 523,  36, 552, 329, 224, 556,  91, 592, 299, 554,
+ 106, 300,  34,  88,  89,  86, 158, 401, 161, 397,
+ 162, 163, 560, 563, 441, 316, 335, 332, 201,  77,
+ 160, 159, 570, 200, 197, 577, 569, 575, 571, 573,
+  81, 477, 177,  97, 181, 179, 331, 523, 180, 583,
+ 446, 177, 584, 445, 577, 178, 587, 141, 142, 138,
+ 139, 140, 137, 135, 561, 328, 227, 346, 414, 345,
+ 596,  41, 203, 606, 598, 477, 586, 577, 602, 605,
+  91, 548, 386, 327, 607, 222, 611, 221, 549, 475,
+ 613, 474, 471, 425, 196, 477, 199,  91, 137, 135,
+  91,  91,  39,  91, 204, 138, 139, 140, 137, 135,
+  91, 183, 168, 188,  19, 220,  29,  27, 524, 243,
+ 360, 538, 307, 287,  91,  91, 368, 121,  30,  28,
+   1, 464, 272, 477, 123, 124, 125, 126, 127, 128,
+ 129, 130, 131, 132, 133, 134, 136, 274, 156, 155,
+ 154, 153, 152, 151, 149, 150, 145, 146, 147, 148,
+ 144, 143, 141, 142, 138, 139, 140, 137, 135, 315,
+ 343, 542, 582, 581, 413, 503, 502, 590,  91, 144,
+ 143, 141, 142, 138, 139, 140, 137, 135, 589, 283,
+  16, 411, 306, 355,  91,   9, 551,  87, 550, 521,
+  91, 520,   7, 450,  85, 337, 292,  90, 266, 295,
+ 296, 506, 371, 109, 107,  87, 113,  92,  82, 199,
+ 111,  91,  85,  20,  38,  90,   0,  99, 282, 342,
+   0,  91,  91, 319, 322,  92,  82,   0,   0,  88,
+  89,  86,   0,   0,   0,  91,  91,   0,   0,  91,
+  96,   0,  98,  95,   0,  77,  87,  88,  89,  86,
+   0,   0,   0,  85,   0,   0,  90,   0,   0,   0,
+   0,   0,   0,  77,   0,   0,  92,  82,   0,   0,
+   0,   0,   0,   0,   0,   0,   0, 333,  91,   0,
+ 455,   0,   0,   0,   0,   0,   0,  91,  88,  89,
+  86,  91,   0, 347,   0,  50,  91,   0,  91, 351,
+   0,   0,   0,   0,  77,   0,   0,  91,   0,   0,
+  52,  53, 454,  91,   0,   0,  59,  72,   0,   0,
+ 390,  57,  58,   0,  63,   0,   0,  73,   0,   0,
+ 395, 396,   0,   0,   0,  76,  75,  69,  68,  74,
+   0,  18,  54,  55,  62,  60,  61,  64, 405,   0,
+   0,   0,  91,   0,   0,   0,  91,   0,   0,  65,
+  66,  67, 145, 146, 147, 148, 144, 143, 141, 142,
+ 138, 139, 140, 137, 135,  77,   0,  91,   0,   0,
+   0,   0,   0, 366,   0,   0,   0, 196,   0,   0,
+  91,   0,   0,   0,   0,   0, 447,   0,  50,   0,
+ 456,   0,   0,   0,   0, 460,   0, 461,   0,   0,
+   0,   0,   0,  52,  53,  56,  97,   0,   0,  59,
+ 378,   0, 472,   0,  57,  58,   0,  63, 370,   0,
+  73,   0,   0,   0,   0,   0,   0,   0,  76,  75,
+ 380,  68,  74,   0,  18,  54,  55,  62,  60,  61,
+  64, 367, 509, 366,   0,   0,  13,   0,   0,   0,
+   0, 496,  65,  66,  67, 501,   0,   0,  50, 372,
+   0,   0,   0, 373, 374, 377, 375, 376,  77,   0,
+   0,   0,   0,  52,  53,  56, 501,   0,   0,  59,
+ 378,   0,   0,   0,  57,  58,   0,  63, 370, 534,
+  73,   0,   0,   0,   0,   0,   0,   0,  76,  75,
+ 380,  68,  74,   0,  18,  54,  55,  62,  60,  61,
+  64, 367, 470, 366,   0,   0,  13,   0,   0,   0,
+   0,   0,  65,  66,  67,   0,   0,   0,  50, 372,
+   0,   0,   0, 373, 374, 377, 375, 376,  77,   0,
+   0,   0,   0,  52,  53,  56,   0,   0,   0,  59,
+ 378,   0,   0,   0,  57,  58,   0,  63, 370,   0,
+  73,   0,   0,   0,   0,   0,   0,   0,  76,  75,
+ 380,  68,  74,   0,  18,  54,  55,  62,  60,  61,
+  64, 367, 440, 366,   0,   0,  13,   0,   0,   0,
+   0,   0,  65,  66,  67,   0,   0,   0,  50, 372,
+   0,   0,   0, 373, 374, 377, 375, 376,  77,   0,
+   0,   0,   0,  52,  53,  56,   0,   0,   0,  59,
+ 378,   0,   0,   0,  57,  58,   0,  63, 370,   0,
+  73,   0,   0,   0,   0,   0,   0,   0,  76,  75,
+ 380,  68,  74,   0,  18,  54,  55,  62,  60,  61,
+  64, 367, 362, 608,   0,   0,  13,   0,   0,   0,
+   0,   0,  65,  66,  67,   0,   0,   0,  50, 372,
+   0,   0,   0, 373, 374, 377, 375, 376,  77,   0,
+   0,   0,   0,  52,  53, 610,   0,   0,   0,  59,
+ 378,   0,   0,   0,  57,  58,   0,  63, 370,   0,
+  73,   0,   0,   0,   0,   0,   0,   0,  76,  75,
+ 609,  68,  74,   0,  18,  54,  55,  62,  60,  61,
+  64, 367, 576,   0,   0,   0,  13,   0,   0,   0,
+   0,   0,  65,  66,  67,   0,   0,  50,   0, 372,
+   0,   0,   0, 373, 374, 377, 375, 376,  77,   0,
+   0,   0,  52,  53, 454,   0,   0,   0,  59, 378,
+   0,   0,   0,  57,  58,   0,  63, 370,   0,  73,
+   0,   0,   0,   0,   0,   0,   0,  76,  75, 380,
+  68,  74,   0,  18,  54,  55,  62,  60,  61,  64,
+ 367, 366,   0,   0,   0,  13,   0,   0,   0,   0,
+   0,  65,  66,  67,   0,   0,  50,   0, 372,   0,
+   0,   0, 373, 374, 377, 375, 376,  77,   0,   0,
+   0,  52,  53,  56,   0,   0,   0,  59, 378,   0,
+   0,   0,  57,  58,   0,  63, 370,   0,  73,   0,
+   0,   0,   0,   0,   0,   0,  76,  75, 380,  68,
+  74,   0,  18,  54,  55,  62,  60,  61,  64, 367,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+  65,  66,  67,  50,   0,   0,   0, 372,   0,   0,
+   0, 373, 374, 377, 375, 376,  77,   0,  52,  53,
+  56,   0,   0,   0,  59,  72,   0,   0,   0,  57,
+  58,   0,  63,   0,   0,  73,   0,   0,   0,   0,
+   0,   0,   0,  76,  75,  69, 275,  74,   0,  18,
+  54,  55,  62,  60,  61,  64,   0,   0,   0,  50,
+   0,   0,   0,   0,   0, 278,   0, 276, 277,  67,
+   0,   0,   0,   0,  52,  53,  56,   0,   0,   0,
+  59,  72,   0,  77, 280,  57,  58,   0,  63,   0,
+   0,  73,   0,   0,   0,   0,   0,   0,   0,  76,
+  75,  69,  68,  74,   0,  18,  54,  55,  62,  60,
+  61,  64,   0,   0,  50,   0,   0,   0,   0,   0,
+   0,   0,   0,  65,  66,  67,   0,   0,   0,  52,
+  53,  56,   0,   0,   0,  59,  72,   0,   0,  77,
+  57,  58,   0,  63,   0,   0,  73,   0,   0,   0,
+   0,   0,   0,   0,  76,  75,  69,  68,  74,   0,
+  18,  54,  55,  62,  60,  61,  64,   0,   0,   0,
+  52,  53,  56,   0,   0,   0,  59,  72,  65,  66,
+  67,  57,  58,   0,  63,   0,   0,  73,   0,   0,
+   0,   0,   0,   0,  77,  76,  75,  69,  68,  74,
+   0,  18,  54,  55,  62,  60,  61,  64,   0,   0,
+   0,   0,   0,   0,   0,   0,  87,   0,   0,  65,
+  66,  67,   0,  85,   0,   0,  90,   0,  99,   0,
+   0,   0,   0,   0,   0,  77,  92,  82, 149, 150,
+ 145, 146, 147, 148, 144, 143, 141, 142, 138, 139,
+ 140, 137, 135, 463, 462,   0,   0, 101,  88,  89,
+  86, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 136,  77, 156, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 136, 567,
+ 156, 155, 154, 153, 152, 151, 149, 150, 145, 146,
+ 147, 148, 144, 143, 141, 142, 138, 139, 140, 137,
+ 135, 154, 153, 152, 151, 149, 150, 145, 146, 147,
+ 148, 144, 143, 141, 142, 138, 139, 140, 137, 135,
+   0, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 136, 558, 156, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135, 152, 151, 149, 150,
+ 145, 146, 147, 148, 144, 143, 141, 142, 138, 139,
+ 140, 137, 135,   0,   0,   0, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 136, 531,
+ 156, 155, 154, 153, 152, 151, 149, 150, 145, 146,
+ 147, 148, 144, 143, 141, 142, 138, 139, 140, 137,
+ 135, 151, 149, 150, 145, 146, 147, 148, 144, 143,
+ 141, 142, 138, 139, 140, 137, 135,   0,   0,   0,
+   0, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 136, 484, 156, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 136, 352,
+ 156, 155, 154, 153, 152, 151, 149, 150, 145, 146,
+ 147, 148, 144, 143, 141, 142, 138, 139, 140, 137,
+ 135,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 136, 344, 156, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 136, 304,
+ 156, 155, 154, 153, 152, 151, 149, 150, 145, 146,
+ 147, 148, 144, 143, 141, 142, 138, 139, 140, 137,
+ 135,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 136, 302, 156, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 136, 191,
+ 156, 155, 154, 153, 152, 151, 149, 150, 145, 146,
+ 147, 148, 144, 143, 141, 142, 138, 139, 140, 137,
+ 135,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 136, 190, 156, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0, 123, 124, 125, 126,
+ 127, 128, 129, 130, 131, 132, 133, 134, 136, 189,
+ 156, 155, 154, 153, 152, 151, 149, 150, 145, 146,
+ 147, 148, 144, 143, 141, 142, 138, 139, 140, 137,
+ 135,   0,  87,   0,   0,   0,  87,   0,   0,  85,
+   0,   0,  90, 388,   0,   0,  90,   0,   0,   0,
+   0,   0,  92, 394,   0,   0,  92,  82,   0,   0,
+   0,   0,   0,   0, 122,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,  88,  89,  86,   0,  88,  89,
+  86,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+  77,   0,   0,   0,  77, 123, 124, 125, 126, 127,
+ 128, 129, 130, 131, 132, 133, 134, 136,   0, 156,
+ 155, 154, 153, 152, 151, 149, 150, 145, 146, 147,
+ 148, 144, 143, 141, 142, 138, 139, 140, 137, 135,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0, 123, 124,
+ 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
+ 136, 578, 156, 155, 154, 153, 152, 151, 149, 150,
+ 145, 146, 147, 148, 144, 143, 141, 142, 138, 139,
+ 140, 137, 135,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 136, 519, 156, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135,   0,   0,   0, 123,
+ 124, 125, 126, 127, 128, 129, 130, 131, 132, 133,
+ 134, 136, 492, 156, 155, 154, 153, 152, 151, 149,
+ 150, 145, 146, 147, 148, 144, 143, 141, 142, 138,
+ 139, 140, 137, 135,   0,   0,   0, 339, 123, 124,
+ 125, 126, 127, 128, 129, 130, 131, 132, 133, 134,
+ 136,   0, 156, 155, 154, 153, 152, 151, 149, 150,
+ 145, 146, 147, 148, 144, 143, 141, 142, 138, 139,
+ 140, 137, 135,   0,   0,   0, 334, 123, 124, 125,
+ 126, 127, 128, 129, 130, 131, 132, 133, 134, 136,
+   0, 156, 155, 154, 153, 152, 151, 149, 150, 145,
+ 146, 147, 148, 144, 143, 141, 142, 138, 139, 140,
+ 137, 135,   0, 514, 123, 124, 125, 126, 127, 128,
+ 129, 130, 131, 132, 133, 134, 136,   0, 156, 155,
+ 154, 153, 152, 151, 149, 150, 145, 146, 147, 148,
+ 144, 143, 141, 142, 138, 139, 140, 137, 135,   0,
+ 457, 123, 124, 125, 126, 127, 128, 129, 130, 131,
+ 132, 133, 134, 136,   0, 156, 155, 154, 153, 152,
+ 151, 149, 150, 145, 146, 147, 148, 144, 143, 141,
+ 142, 138, 139, 140, 137, 135, 156, 155, 154, 153,
+ 152, 151, 149, 150, 145, 146, 147, 148, 144, 143,
+ 141, 142, 138, 139, 140, 137, 135,
+};
+yypact := array[] of {
+ 198,-1000, 370, 172,-1000, 140,-1000,-1000, 137, 135,
+ 633, 632,  12, 306, 482,-1000, 467, 550,-1000, 285,
+ -35, 130,-1000,-1000,-1000,-1000,-1000,1507,1507,1507,
+1507, 737, 595, 120, 144, 427, 404, -19, 480, 335,
+-1000, 370,  16,-1000,-1000,-1000, 401,-1000,2272,-1000,
+ 396, 497,1548,1548,1548,1548,1548,1548,1548,1548,
+1548,1548,1548,1548,1548, 530, 520, 523,1548, 376,
+1548,-1000,1507, 583,-1000,-1000,-1000, 594,2217,2162,
+2107, 288,-1000,-1000,-1000, 737, 509, 737, 508, 503,
+ 550,-1000, 551,-1000,-1000, 737,1507, 251,1507, 134,
+ 239, 550, 265, 348, 550, 236, 737, 567, 565, -36,
+-1000, 474,   7, -38,-1000,-1000,-1000, 544,-1000, 285,
+-1000, 172,-1000,1507,1507,1507,1507,1507,1507,1507,
+1507,1507,1507,1507,1507, 635,1507,1507,1507,1507,
+1507,1507,1507,1507,1507,1507,1507,1507,1507,1507,
+1507,1507,1507,1507,1507,1507,1507,1507,1507, 393,
+ 323,1396,-1000,-1000,-1000,-1000,-1000,-1000,-1000,-1000,
+-1000,-1000,-1000,-1000,-1000,-1000,-1000,1452, 318, 224,
+ 737,1507,-1000,-1000,-1000,  14,2667,-1000,1507,-1000,
+-1000,-1000,-1000,1507, 391, 389, 424, 737,  13, 424,
+ 737, 737, 583, 452, 305,2052,-1000,1507,1997,-1000,
+ 737, 640,  49,-1000,-1000, 163, 285,-1000,-1000, 370,
+ 424,-1000,-1000, 334, 273, 273, 254,-1000,-1000,-1000,
+ 172,2667,2667,2667,2667,2667,2667,2667,2667,2667,
+2667,2667,2667,1507,2667, 581, 581, 581, 581, 591,
+ 591, 545, 545, 669, 669, 669, 669, 866, 866,1624,
+1848,1794,1741,1741,1687,2688, 563, -39,-1000, 420,
+ 543, 473, -40,2667,-1000,1548, 521, 502, 737,2554,
+ 501,1548,1507, 424,2515,-1000,1507, 265, 650,1942,
+ 548, 546, 424,-1000, 737, 424, 424, 427,  10, 424,
+ 737,-1000,-1000,1887,-1000,  11, 146,-1000, 638, 223,
+1121,-1000,-1000,   5, 191,-1000, 299, 562,-1000, 424,
+-1000,2277, 424,-1000,-1000,-1000,2667,-1000,-1000,1507,
+1396,2273, 678, 424, 494, 216,-1000, 189, -41, 492,
+2667,-1000,1507,-1000,-1000, 452, 452, 424,-1000, 407,
+-1000, 424,-1000, 119,-1000,-1000, 466, 116,-1000, 110,
+-1000, 370,-1000,-1000,-1000, 463,   0,-1000, -10, 114,
+ 574,  31, 388, 388,1507,1507,1507, 112,1507,2667,
+ 376,1051,-1000,-1000, 370,-1000,-1000,-1000, 737,-1000,
+ 424, 531, 528,2667,1548, 424, 424, 269, 808,-1000,
+1507, 737,2630,   6,   2, 424, 737,-1000,1587,-1000,
+ -21,-1000,-1000,-1000, 460, 215, 188, 696,-1000,-1000,
+-1000, 981, 573, 737,-1000,1507, 572, 570,1329,1507,
+ 212, 379, 107,-1000, 104, 103,1832,  99,-1000,   3,
+-1000,-1000, 338,-1000,-1000,-1000,-1000, 424, 808, 185,
+ -44,-1000,2477, 409,1548,-1000, 424,-1000,-1000,-1000,
+ 424, 293, 737,1507,-1000, 200, 219, 422, 194, 911,
+ 436,1507, 176,2593,1507,1507, -17, 449,2424, 808,
+ 622,-1000,-1000,-1000,-1000,-1000,-1000, 193,-1000, 183,
+-1000, 808,1507, 808,1507,-1000, 235,1777, 370,1507,
+ 737, 227, 175, 639,-1000, 283, 413,-1000, 638,-1000,
+ 354,  -3,-1000,1507,1329,  89, 561, 569,-1000, 808,
+ 169,-1000, 406,2477,1507,-1000,-1000,2667,-1000,2667,
+-1000,-1000, 157,1722, 113,-1000,-1000, 351, 346,-1000,
+ 337, 106, 145,-1000,-1000,1667, 469,1507,1329,1507,
+ 152,-1000, 365,-1000,1260,-1000,2371,-1000,-1000,-1000,
+ 268, 447,-1000, 252,-1000,-1000, 808,-1000,1329,  51,
+-1000, 556,-1000,1260,-1000, 361,   0,2477, 468,-1000,
+-1000, 148,-1000, 358,-1000,1507,  21, 356,-1000,  32,
+-1000, 353,-1000,-1000,-1000,-1000,1260,-1000, 553,-1000,
+-1000,-1000,1191,-1000, 468, 333,1329, 278,   0, 376,
+1548,-1000,-1000,-1000,-1000,
+};
+yypgo := array[] of {
+   0, 528, 744, 164,  33,  24, 419,  15,  14,  46,
+ 743, 740, 736,  34, 734, 733,  27, 732,  16,   4,
+ 731, 108,   8,   0,  21,  35,  13, 728, 725,  94,
+  25,  67,  26,  12, 723,  11,   2,  38,  41,  32,
+ 722,  22,   3,   7, 721, 719, 718, 716, 715,  20,
+ 713, 712, 711,  10, 710, 708, 697,   1, 696, 695,
+ 694,   6,   5, 693, 692, 691,  19,  23, 689,   9,
+ 667,  18, 652, 651,  17, 650, 647, 646, 643,
+};
+yyr1 := array[] of {
+   0,  76,  75,  75,  38,  38,  39,  39,  39,  39,
+  39,  39,  39,  39,  39,  39,  39,  30,  30,  37,
+  37,  37,  37,  37,  37,  37,  66,  66,  48,  51,
+  51,  51,  50,  50,  50,  50,  50,  49,  49,  73,
+  73,  53,  53,  53,  52,  52,  52,  62,  62,  61,
+  61,  60,  58,  58,  58,  59,  59,  59,  19,  20,
+  20,   9,  10,  10,   6,   6,  74,  74,  74,  74,
+   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
+   1,   1,   7,   7,   8,   8,  13,  13,  21,  21,
+   2,   2,   2,   3,   3,   4,   4,  14,  14,  15,
+  15,  16,  16,  16,  16,  11,  12,  12,  12,  12,
+   5,   5,   5,   5,  40,  67,  67,  67,  41,  41,
+  41,  54,  54,  43,  43,  43,  77,  77,  42,  42,
+  42,  42,  42,  42,  42,  42,  42,  42,  42,  42,
+  42,  42,  42,  42,  42,  42,  42,  42,  42,  42,
+  17,  17,  18,  18,  44,  45,  45,  46,  47,  47,
+  63,  64,  64,  36,  36,  36,  36,  36,  55,  56,
+  56,  57,  57,  57,  57,  22,  22,  23,  23,  23,
+  23,  23,  23,  23,  23,  23,  23,  23,  23,  23,
+  23,  23,  23,  23,  23,  23,  23,  23,  23,  23,
+  23,  23,  23,  23,  23,  23,  23,  23,  23,  23,
+  23,  23,  23,  24,  24,  24,  24,  24,  24,  24,
+  24,  24,  24,  24,  24,  24,  24,  24,  24,  24,
+  24,  24,  24,  24,  24,  24,  24,  25,  25,  25,
+  78,  25,  25,  25,  25,  25,  25,  25,  25,  25,
+  25,  25,  29,  29,  31,  72,  72,  71,  71,  70,
+  70,  70,  70,  65,  65,  32,  32,  32,  32,  27,
+  27,  28,  28,  26,  26,  33,  33,  34,  34,  35,
+  35,  69,  68,  68,
+};
+yyr2 := array[] of {
+   0,   0,   5,   1,   1,   2,   2,   1,   1,   2,
+   2,   4,   4,   4,   4,   4,   6,   1,   3,   3,
+   5,   5,   4,   6,   5,   1,   4,   7,   6,   0,
+   2,   1,   4,   2,   5,   5,   1,   8,  11,   0,
+   4,   0,   2,   1,   1,   1,   5,   0,   2,   5,
+   4,   4,   2,   2,   1,   2,   4,   4,   1,   1,
+   3,   1,   1,   3,   6,   4,   1,   2,   3,   4,
+   1,   1,   1,   3,   6,   2,   3,   3,   3,   3,
+   4,   1,   1,   4,   3,   6,   1,   3,   0,   3,
+   3,   3,   5,   1,   3,   1,   5,   0,   1,   1,
+   3,   3,   3,   3,   3,   1,   1,   1,   3,   3,
+   2,   3,   2,   3,   4,   4,   2,   0,   3,   2,
+   4,   2,   4,   0,   2,   2,   3,   5,   2,   2,
+   4,   3,   4,   6,   2,   5,   7,  10,   6,   8,
+   3,   3,   3,   3,   3,   6,   5,   8,   2,   8,
+   0,   2,   0,   1,   2,   2,   4,   2,   2,   4,
+   2,   2,   4,   1,   3,   1,   3,   1,   2,   2,
+   4,   1,   1,   3,   1,   0,   1,   1,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   4,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   1,   2,   2,   2,   2,   2,   2,
+   2,   2,   2,   2,   2,   2,   2,   6,   8,   7,
+   5,   3,   6,   4,   2,   2,   2,   1,   4,   3,
+   0,   4,   3,   3,   4,   6,   2,   2,   1,   1,
+   1,   6,   1,   1,   3,   1,   3,   1,   1,   1,
+   3,   3,   2,   1,   0,   1,   1,   3,   3,   0,
+   1,   1,   2,   1,   3,   1,   2,   1,   3,   1,
+   3,   2,   2,   4,
+};
+yychk := array[] of {
+-1000, -75,  73, -38, -39,   2, -37, -40, -49, -48,
+ -29, -31, -30,  75,  -9, -66, -54,  59,  63,  39,
+ -10,  -9,  59, -39,  72,  72,  72,   4,  16,   4,
+  16,  50,  99,  61,  50,  -4,  54,  -3,  -2,  39,
+ -21,  41, -32, -31, -29,  59,  99,  72, -23, -24,
+  17, -25,  32,  33,  64,  65,  34,  43,  44,  38,
+  67,  68,  66,  46,  69,  81,  82,  83,  60,  59,
+  -6, -29,  39,  49,  61,  58,  57,  97, -23, -23,
+ -23,  -1,  60,  -7,  -8,  46,  83,  39,  81,  82,
+  49,  -6,  59, -31,  72,  77,  74,  -1,  76,  51,
+  78,  80, -67,  52,  59,  87,  50, -14,  34, -15,
+ -16, -11, -30, -12, -31,  59,  63,  -9,  40,  99,
+  59, -76,  72,   4,   5,   6,   7,   8,   9,  10,
+  11,  12,  13,  14,  15,  38,  16,  37,  34,  35,
+  36,  32,  33,  31,  30,  26,  27,  28,  29,  24,
+  25,  23,  22,  21,  20,  19,  18,  59,  39,  54,
+  53,  41,  43,  44, -24, -24, -24, -24, -24, -24,
+ -24, -24, -24, -24, -24, -24, -24,  41,  45,  45,
+  45,  41, -24, -24, -24, -26, -23,  -3,  39,  72,
+  72,  72,  72,   4,  53,  54,  -1,  45, -13,  -1,
+  45,  45, -21,  41,  -1, -23,  72,   4, -23,  72,
+  39,  70, -21, -41,  70,   2,  39, -29, -21,  70,
+  -1,  40,  40,  99,  50,  50,  99,  42, -31, -29,
+ -38, -23, -23, -23, -23, -23, -23, -23, -23, -23,
+ -23, -23, -23,   4, -23, -23, -23, -23, -23, -23,
+ -23, -23, -23, -23, -23, -23, -23, -23, -23, -23,
+ -23, -23, -23, -23, -23, -23, -27, -26,  59, -25,
+ -71, -22, -72, -23, -70,  60,  81,  82,  79, -23,
+  42,  60,  70,  -1, -23,  40,  99, -78, -23, -23,
+  59,  59,  -1,  40,  99,  -1,  -1,  -4, -74,  -1,
+  79,  72,  72, -23,  72, -13, -51,   2,  70,  87,
+ -43,  71,  70, -32, -69, -68,  -9,  34, -16,  -1,
+  -5,  84,  -1,  -5,  59,  63, -23,  40,  42,  50,
+  99,  45,  45,  -1,  42,  45, -24, -28, -26,  42,
+ -23, -41,  99,  40,  72,  41,  41,  -1, -67,  99,
+  42,  -1,  72,  40,  71, -50,  -9, -49, -66, -53,
+   2,  70,  71, -37, -42,  -9,   2,  70, -77, -22,
+  47, -17,  88,  92,  93,  95,  96,  94,  39, -23,
+  59, -43,  40,  71, -62,  62,  40,  -7,  46,  -8,
+  -1, -22, -71, -23,  60,  -1,  -1,  45,  70,  71,
+  99,  45, -23, -74, -74,  -1,  79,  72,  50,  72,
+  71, -52, -61, -60,  -9,  91, -69,  50,  72,  71,
+  70, -43,  99,  50,  72,  39,  87,  86,  85,  90,
+  89,  91, -18,  59, -18, -22, -23, -22,  72, -26,
+  71, -61,  -9,  -7,  -8,  42,  42,  -1,  70, -33,
+ -34, -35, -23, -36,  34,   2,  -1,  40,  42,  42,
+  -1,  -1,  77,  76, -73,  87,  50,  70,  71, -43,
+  71,  39,  -1, -23,  39,  39, -42,  -9, -23,  70,
+  59,  72,  72,  72,  72,  72,  40,  50,  62, -33,
+  71,  99,  55,  56,  62,  72,  -1, -23,  70,  76,
+  79,  -1, -58, -59,   2, -19, -20,  59,  70,  71,
+  51, -26,  72,   4,  40, -22, -22,  86,  50,  70,
+ -44, -45, -36, -23,  16,  71, -35, -23, -36, -23,
+  72,  72, -69, -23,  -1,  72,  71, -62,   2,  62,
+  56, -53, -65,  59,  40, -23, -42,  72,  40,  39,
+ -46, -47, -36,  71, -43,  62, -23,  71,  72,  72,
+ -19,  -9,  59, -19,  59,  71,  70,  72,  48, -22,
+ -42, -22,  71, -43,  62, -36,   2, -23,  70,  62,
+  62, -63, -64, -36, -42,  72,  40, -36,  62, -55,
+ -56, -57,  59,  34,   2,  71, -43,  62, -22,  72,
+  62,  71, -43,  62,  56, -36,  40, -57,   2,  59,
+  34, -57,  62, -42,  62,
+};
+yydef := array[] of {
+   0,  -2,   0,  -2,   4,   0,   7,   8,   0,   0,
+   0,  17,   0,   0,   0,  25,   0,  -2, 253,   0,
+  61,   0,  62,   5,   6,   9,  10,   0,   0,   0,
+   0,   0,   0,   0,   0, 117,   0,  95,  93,  97,
+ 121,   0,   0, 265, 266, 252,   0,   1,   0, 177,
+   0, 213,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0, 252,
+   0, 237,   0,   0, 248, 249, 250,   0,   0,   0,
+   0,   0,  70,  71,  72,   0,   0,   0,   0,   0,
+  88,  81,  82,  18,  19,   0,   0,   0,   0,   0,
+   0,  88,   0,   0,  88,   0,   0,   0,   0,  98,
+  99,   0,   0, 105,  17, 106, 107,   0, 254,   0,
+  63,   0,  11,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0, 269,   0,
+   0, 175, 246, 247, 214, 215, 216, 217, 218, 219,
+ 220, 221, 222, 223, 224, 225, 226,   0,   0,   0,
+   0,   0, 234, 235, 236,   0, 273, 240,   0,  13,
+  12,  14,  15,   0,   0,   0,  75,   0,   0,  86,
+   0,   0,   0,   0,   0,   0,  22,   0,   0,  26,
+   0,  -2,   0, 114, 123,   0,   0, 116, 122,   0,
+  94,  90,  91,   0,   0,   0,   0,  89, 267, 268,
+  -2, 178, 179, 180, 181, 182, 183, 184, 185, 186,
+ 187, 188, 189,   0, 191, 193, 194, 195, 196, 197,
+ 198, 199, 200, 201, 202, 203, 204, 205, 206, 207,
+ 208, 209, 210, 211, 212, 192,   0, 270, 242, 243,
+ 255,   0,   0,  -2, 258, 259,   0,   0,   0,   0,
+   0,   0,   0, 231,   0, 239,   0,   0,   0,   0,
+  73,  84,  76,  77,   0,  78,  79, 117,   0,  66,
+   0,  20,  21,   0,  24,   0,   0,  31,  -2,   0,
+  -2, 119, 123,   0,   0,  47,   0,   0, 100, 101,
+ 102,   0, 103, 104, 108, 109, 190, 238, 244, 175,
+   0,   0,   0, 262,   0,   0, 233,   0, 271,   0,
+ 274, 241,   0,  65,  16,   0,   0,  87,  80,   0,
+  83,  67,  23,   0,  28,  30,   0,   0,  36,   0,
+  43,   0, 118, 124, 125,   0,   0, 123,   0,   0,
+   0,   0, 152, 152, 175,   0, 175,   0,   0, 176,
+  -2,  -2, 115,  96, 281, 282,  92,  -2,   0,  -2,
+   0,   0, 256, 257,  70, 260, 261,   0,   0, 230,
+ 272,   0,   0,   0,   0,  68,   0,  27,   0,  33,
+  39,  42,  44,  45,   0,   0,   0, 151, 128, 129,
+ 123,  -2,   0,   0, 134,   0,   0,   0,  -2,   0,
+   0,   0,   0, 153,   0,   0,   0,   0, 148,   0,
+ 120,  48,   0,  -2,  -2, 245, 251, 227,   0,   0,
+ 275, 277,  -2,   0, 165, 167, 232,  64,  74,  85,
+  69,   0,   0,   0,  37,   0,   0,   0,   0,  -2,
+ 131,   0,   0,   0, 175, 175,   0,   0,   0,   0,
+   0, 140, 141, 142, 143, 144,  -2,   0, 283,   0,
+ 229,  -2,   0,   0,   0,  32,   0,   0,   0,   0,
+   0,   0,   0,  -2,  54,   0,  58,  59,  -2, 130,
+ 264,   0, 132,   0,  -2,   0,   0,   0, 151,   0,
+   0, 123,   0, 163,   0, 228, 278, 164, 166, 280,
+  34,  35,   0,   0,   0,  50,  51,  52,  53,  55,
+   0,   0,   0, 263, 127,   0, 135, 175,  -2, 175,
+   0, 123,   0, 146,  -2, 155,   0,  40,  46,  49,
+   0,   0,  -2,   0,  60,  38,   0, 133,  -2,   0,
+ 138,   0, 145,  -2, 158,   0, 167,  -2,   0,  56,
+  57,   0, 123,   0, 136, 175,   0,   0, 156,   0,
+ 123,   0, 171, 172, 174, 149,  -2, 161,   0, 139,
+ 159, 147,  -2, 169,   0,   0,  -2,   0, 174,  -2,
+ 172, 173, 162, 137, 170,
+};
+yytok1 := array[] of {
+   1,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,  64,   3,   3,   3,  36,  23,   3,
+  39,  40,  34,  32,  99,  33,  54,  35,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,  50,  72,
+  26,   4,  27,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,  41,   3,  42,  22,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,  70,  21,  71,  65,
+};
+yytok2 := array[] of {
+   2,   3,   5,   6,   7,   8,   9,  10,  11,  12,
+  13,  14,  15,  16,  17,  18,  19,  20,  24,  25,
+  28,  29,  30,  31,  37,  38,  43,  44,  45,  46,
+  47,  48,  49,  51,  52,  53,  55,  56,  57,  58,
+  59,  60,  61,  62,  63,  66,  67,  68,  69,  73,
+  74,  75,  76,  77,  78,  79,  80,  81,  82,  83,
+  84,  85,  86,  87,  88,  89,  90,  91,  92,  93,
+  94,  95,  96,  97,  98,
+};
+yytok3 := array[] of {
+   0
+};
+
+YYSys: module
+{
+	FD: adt
+	{
+		fd:	int;
+	};
+	fildes:		fn(fd: int): ref FD;
+	fprint:		fn(fd: ref FD, s: string, *): int;
+};
+
+yysys: YYSys;
+yystderr: ref YYSys->FD;
+
+YYFLAG: con -1000;
+
+# parser for yacc output
+
+yytokname(yyc: int): string
+{
+	if(yyc > 0 && yyc <= len yytoknames && yytoknames[yyc-1] != nil)
+		return yytoknames[yyc-1];
+	return "<"+string yyc+">";
+}
+
+yystatname(yys: int): string
+{
+	if(yys >= 0 && yys < len yystates && yystates[yys] != nil)
+		return yystates[yys];
+	return "<"+string yys+">\n";
+}
+
+yylex1(yylex: ref YYLEX): int
+{
+	c : int;
+	yychar := yylex.lex();
+	if(yychar <= 0)
+		c = yytok1[0];
+	else if(yychar < len yytok1)
+		c = yytok1[yychar];
+	else if(yychar >= YYPRIVATE && yychar < YYPRIVATE+len yytok2)
+		c = yytok2[yychar-YYPRIVATE];
+	else{
+		n := len yytok3;
+		c = 0;
+		for(i := 0; i < n; i+=2) {
+			if(yytok3[i+0] == yychar) {
+				c = yytok3[i+1];
+				break;
+			}
+		}
+		if(c == 0)
+			c = yytok2[1];	# unknown char
+	}
+	if(yydebug >= 3)
+		yysys->fprint(yystderr, "lex %.4ux %s\n", yychar, yytokname(c));
+	return c;
+}
+
+YYS: adt
+{
+	yyv: YYSTYPE;
+	yys: int;
+};
+
+yyparse(yylex: ref YYLEX): int
+{
+	if(yydebug >= 1 && yysys == nil) {
+		yysys = load YYSys "$Sys";
+		yystderr = yysys->fildes(2);
+	}
+
+	yys := array[YYMAXDEPTH] of YYS;
+
+	yyval: YYSTYPE;
+	yystate := 0;
+	yychar := -1;
+	yynerrs := 0;		# number of errors
+	yyerrflag := 0;		# error recovery flag
+	yyp := -1;
+	yyn := 0;
+
+yystack:
+	for(;;){
+		# put a state and value onto the stack
+		if(yydebug >= 4)
+			yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+
+		yyp++;
+		if(yyp >= len yys)
+			yys = (array[len yys * 2] of YYS)[0:] = yys;
+		yys[yyp].yys = yystate;
+		yys[yyp].yyv = yyval;
+
+		for(;;){
+			yyn = yypact[yystate];
+			if(yyn > YYFLAG) {	# simple state
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+				yyn += yychar;
+				if(yyn >= 0 && yyn < YYLAST) {
+					yyn = yyact[yyn];
+					if(yychk[yyn] == yychar) { # valid shift
+						yychar = -1;
+						yyp++;
+						if(yyp >= len yys)
+							yys = (array[len yys * 2] of YYS)[0:] = yys;
+						yystate = yyn;
+						yys[yyp].yys = yystate;
+						yys[yyp].yyv = yylex.lval;
+						if(yyerrflag > 0)
+							yyerrflag--;
+						if(yydebug >= 4)
+							yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+						continue;
+					}
+				}
+			}
+		
+			# default state action
+			yyn = yydef[yystate];
+			if(yyn == -2) {
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+		
+				# look through exception table
+				for(yyxi:=0;; yyxi+=2)
+					if(yyexca[yyxi] == -1 && yyexca[yyxi+1] == yystate)
+						break;
+				for(yyxi += 2;; yyxi += 2) {
+					yyn = yyexca[yyxi];
+					if(yyn < 0 || yyn == yychar)
+						break;
+				}
+				yyn = yyexca[yyxi+1];
+				if(yyn < 0){
+					yyn = 0;
+					break yystack;
+				}
+			}
+
+			if(yyn != 0)
+				break;
+
+			# error ... attempt to resume parsing
+			if(yyerrflag == 0) { # brand new error
+				yylex.error("syntax error");
+				yynerrs++;
+				if(yydebug >= 1) {
+					yysys->fprint(yystderr, "%s", yystatname(yystate));
+					yysys->fprint(yystderr, "saw %s\n", yytokname(yychar));
+				}
+			}
+
+			if(yyerrflag != 3) { # incompletely recovered error ... try again
+				yyerrflag = 3;
+	
+				# find a state where "error" is a legal shift action
+				while(yyp >= 0) {
+					yyn = yypact[yys[yyp].yys] + YYERRCODE;
+					if(yyn >= 0 && yyn < YYLAST) {
+						yystate = yyact[yyn];  # simulate a shift of "error"
+						if(yychk[yystate] == YYERRCODE)
+							continue yystack;
+					}
+	
+					# the current yyp has no shift onn "error", pop stack
+					if(yydebug >= 2)
+						yysys->fprint(yystderr, "error recovery pops state %d, uncovers %d\n",
+							yys[yyp].yys, yys[yyp-1].yys );
+					yyp--;
+				}
+				# there is no state on the stack with an error shift ... abort
+				yyn = 1;
+				break yystack;
+			}
+
+			# no shift yet; clobber input char
+			if(yydebug >= 2)
+				yysys->fprint(yystderr, "error recovery discards %s\n", yytokname(yychar));
+			if(yychar == YYEOFCODE) {
+				yyn = 1;
+				break yystack;
+			}
+			yychar = -1;
+			# try again in the same state
+		}
+	
+		# reduction by production yyn
+		if(yydebug >= 2)
+			yysys->fprint(yystderr, "reduce %d in:\n\t%s", yyn, yystatname(yystate));
+	
+		yypt := yyp;
+		yyp -= yyr2[yyn];
+#		yyval = yys[yyp+1].yyv;
+		yym := yyn;
+	
+		# consult goto table to find next state
+		yyn = yyr1[yyn];
+		yyg := yypgo[yyn];
+		yyj := yyg + yys[yyp].yys + 1;
+	
+		if(yyj >= YYLAST || yychk[yystate=yyact[yyj]] != -yyn)
+			yystate = yyact[yyg];
+		case yym {
+			
+1=>
+#line	153	"limbo.y"
+{
+		impmods = yys[yypt-1].yyv.ids;
+	}
+2=>
+#line	156	"limbo.y"
+{
+		tree = rotater(yys[yypt-0].yyv.node);
+	}
+3=>
+#line	160	"limbo.y"
+{
+		impmods = nil;
+		tree = rotater(yys[yypt-0].yyv.node);
+	}
+4=>
+yyval.node = yys[yyp+1].yyv.node;
+5=>
+#line	168	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node == nil)
+			yyval.node = yys[yypt-0].yyv.node;
+		else if(yys[yypt-0].yyv.node == nil)
+			yyval.node = yys[yypt-1].yyv.node;
+		else
+			yyval.node = mkbin(Oseq, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node);
+	}
+6=>
+#line	179	"limbo.y"
+{
+		yyval.node = nil;
+	}
+7=>
+yyval.node = yys[yyp+1].yyv.node;
+8=>
+yyval.node = yys[yyp+1].yyv.node;
+9=>
+yyval.node = yys[yyp+1].yyv.node;
+10=>
+yyval.node = yys[yyp+1].yyv.node;
+11=>
+#line	187	"limbo.y"
+{
+		yyval.node = mkbin(Oas, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node);
+	}
+12=>
+#line	191	"limbo.y"
+{
+		yyval.node = mkbin(Oas, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node);
+	}
+13=>
+#line	195	"limbo.y"
+{
+		yyval.node = mkbin(Odas, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node);
+	}
+14=>
+#line	199	"limbo.y"
+{
+		yyval.node = mkbin(Odas, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node);
+	}
+15=>
+#line	203	"limbo.y"
+{
+		yyerror("illegal declaration");
+		yyval.node = nil;
+	}
+16=>
+#line	208	"limbo.y"
+{
+		yyerror("illegal declaration");
+		yyval.node = nil;
+	}
+17=>
+yyval.node = yys[yyp+1].yyv.node;
+18=>
+#line	216	"limbo.y"
+{
+		yyval.node = mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+19=>
+#line	222	"limbo.y"
+{
+		includef(yys[yypt-1].yyv.tok.v.idval);
+		yyval.node = nil;
+	}
+20=>
+#line	227	"limbo.y"
+{
+		yyval.node = typedecl(yys[yypt-4].yyv.ids, yys[yypt-1].yyv.ty);
+	}
+21=>
+#line	231	"limbo.y"
+{
+		yyval.node = importdecl(yys[yypt-1].yyv.node, yys[yypt-4].yyv.ids);
+		yyval.node.src.start = yys[yypt-4].yyv.ids.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+22=>
+#line	237	"limbo.y"
+{
+		yyval.node = vardecl(yys[yypt-3].yyv.ids, yys[yypt-1].yyv.ty);
+	}
+23=>
+#line	241	"limbo.y"
+{
+		yyval.node = mkbin(Ovardecli, vardecl(yys[yypt-5].yyv.ids, yys[yypt-3].yyv.ty), varinit(yys[yypt-5].yyv.ids, yys[yypt-1].yyv.node));
+	}
+24=>
+#line	245	"limbo.y"
+{
+		yyval.node = condecl(yys[yypt-4].yyv.ids, yys[yypt-1].yyv.node);
+	}
+25=>
+yyval.node = yys[yyp+1].yyv.node;
+26=>
+#line	252	"limbo.y"
+{
+		yyval.node = exdecl(yys[yypt-3].yyv.ids, nil);
+	}
+27=>
+#line	256	"limbo.y"
+{
+		yyval.node = exdecl(yys[yypt-6].yyv.ids, revids(yys[yypt-2].yyv.ids));
+	}
+28=>
+#line	262	"limbo.y"
+{
+		yys[yypt-5].yyv.ids.src.stop = yys[yypt-0].yyv.tok.src.stop;
+		yyval.node = moddecl(yys[yypt-5].yyv.ids, rotater(yys[yypt-1].yyv.node));
+	}
+29=>
+#line	269	"limbo.y"
+{
+		yyval.node = nil;
+	}
+30=>
+#line	273	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node == nil)
+			yyval.node = yys[yypt-0].yyv.node;
+		else if(yys[yypt-0].yyv.node == nil)
+			yyval.node = yys[yypt-1].yyv.node;
+		else
+			yyval.node = mkn(Oseq, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node);
+	}
+31=>
+#line	282	"limbo.y"
+{
+		yyval.node = nil;
+	}
+32=>
+#line	288	"limbo.y"
+{
+		yyval.node = fielddecl(Dglobal, typeids(yys[yypt-3].yyv.ids, yys[yypt-1].yyv.ty));
+	}
+33=>
+yyval.node = yys[yyp+1].yyv.node;
+34=>
+#line	293	"limbo.y"
+{
+		yyval.node = typedecl(yys[yypt-4].yyv.ids, yys[yypt-1].yyv.ty);
+	}
+35=>
+#line	297	"limbo.y"
+{
+		yyval.node = condecl(yys[yypt-4].yyv.ids, yys[yypt-1].yyv.node);
+	}
+36=>
+yyval.node = yys[yyp+1].yyv.node;
+37=>
+#line	304	"limbo.y"
+{
+		yys[yypt-7].yyv.ids.src.stop = yys[yypt-1].yyv.tok.src.stop;
+		yyval.node = adtdecl(yys[yypt-7].yyv.ids, rotater(yys[yypt-2].yyv.node));
+		yyval.node.ty.polys = yys[yypt-4].yyv.ids;
+		yyval.node.ty.val = rotater(yys[yypt-0].yyv.node);
+	}
+38=>
+#line	311	"limbo.y"
+{
+		yys[yypt-10].yyv.ids.src.stop = yys[yypt-0].yyv.tok.src.stop;
+		yyval.node = adtdecl(yys[yypt-10].yyv.ids, rotater(yys[yypt-1].yyv.node));
+		yyval.node.ty.polys = yys[yypt-7].yyv.ids;
+		yyval.node.ty.val = rotater(yys[yypt-4].yyv.node);
+	}
+39=>
+#line	320	"limbo.y"
+{
+		yyval.node = nil;
+	}
+40=>
+#line	324	"limbo.y"
+{
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+41=>
+#line	330	"limbo.y"
+{
+		yyval.node = nil;
+	}
+42=>
+#line	334	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node == nil)
+			yyval.node = yys[yypt-0].yyv.node;
+		else if(yys[yypt-0].yyv.node == nil)
+			yyval.node = yys[yypt-1].yyv.node;
+		else
+			yyval.node = mkn(Oseq, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node);
+	}
+43=>
+#line	343	"limbo.y"
+{
+		yyval.node = nil;
+	}
+44=>
+yyval.node = yys[yyp+1].yyv.node;
+45=>
+yyval.node = yys[yyp+1].yyv.node;
+46=>
+#line	351	"limbo.y"
+{
+		yyval.node = condecl(yys[yypt-4].yyv.ids, yys[yypt-1].yyv.node);
+	}
+47=>
+#line	357	"limbo.y"
+{
+		yyval.node = nil;
+	}
+48=>
+#line	361	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node == nil)
+			yyval.node = yys[yypt-0].yyv.node;
+		else if(yys[yypt-0].yyv.node == nil)
+			yyval.node = yys[yypt-1].yyv.node;
+		else
+			yyval.node = mkn(Oseq, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node);
+	}
+49=>
+#line	372	"limbo.y"
+{
+		for(d := yys[yypt-4].yyv.ids; d != nil; d = d.next)
+			d.cyc = byte 1;
+		yyval.node = fielddecl(Dfield, typeids(yys[yypt-4].yyv.ids, yys[yypt-1].yyv.ty));
+	}
+50=>
+#line	378	"limbo.y"
+{
+		yyval.node = fielddecl(Dfield, typeids(yys[yypt-3].yyv.ids, yys[yypt-1].yyv.ty));
+	}
+51=>
+#line	384	"limbo.y"
+{
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+52=>
+#line	390	"limbo.y"
+{
+		yys[yypt-1].yyv.node.right.right = yys[yypt-0].yyv.node;
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+53=>
+#line	395	"limbo.y"
+{
+		yyval.node = nil;
+	}
+54=>
+#line	399	"limbo.y"
+{
+		yyval.node = nil;
+	}
+55=>
+#line	405	"limbo.y"
+{
+		yyval.node = mkn(Opickdecl, nil, mkn(Oseq, fielddecl(Dtag, yys[yypt-1].yyv.ids), nil));
+		typeids(yys[yypt-1].yyv.ids, mktype(yys[yypt-1].yyv.ids.src.start, yys[yypt-1].yyv.ids.src.stop, Tadtpick, nil, nil));
+	}
+56=>
+#line	410	"limbo.y"
+{
+		yys[yypt-3].yyv.node.right.right = yys[yypt-2].yyv.node;
+		yyval.node = mkn(Opickdecl, yys[yypt-3].yyv.node, mkn(Oseq, fielddecl(Dtag, yys[yypt-1].yyv.ids), nil));
+		typeids(yys[yypt-1].yyv.ids, mktype(yys[yypt-1].yyv.ids.src.start, yys[yypt-1].yyv.ids.src.stop, Tadtpick, nil, nil));
+	}
+57=>
+#line	416	"limbo.y"
+{
+		yyval.node = mkn(Opickdecl, nil, mkn(Oseq, fielddecl(Dtag, yys[yypt-1].yyv.ids), nil));
+		typeids(yys[yypt-1].yyv.ids, mktype(yys[yypt-1].yyv.ids.src.start, yys[yypt-1].yyv.ids.src.stop, Tadtpick, nil, nil));
+	}
+58=>
+#line	423	"limbo.y"
+{
+		yyval.ids = revids(yys[yypt-0].yyv.ids);
+	}
+59=>
+#line	429	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval, nil, nil);
+	}
+60=>
+#line	433	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval, nil, yys[yypt-2].yyv.ids);
+	}
+61=>
+#line	439	"limbo.y"
+{
+		yyval.ids = revids(yys[yypt-0].yyv.ids);
+	}
+62=>
+#line	445	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval, nil, nil);
+	}
+63=>
+#line	449	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval, nil, yys[yypt-2].yyv.ids);
+	}
+64=>
+#line	455	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-5].yyv.tok.src.start, yys[yypt-0].yyv.tok.src.stop, Tfix, nil, nil);
+		yyval.ty.val = mkbin(Oseq, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node);
+	}
+65=>
+#line	460	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-3].yyv.tok.src.start, yys[yypt-0].yyv.tok.src.stop, Tfix, nil, nil);
+		yyval.ty.val = yys[yypt-1].yyv.node;
+	}
+66=>
+#line	467	"limbo.y"
+{
+		yyval.types = addtype(yys[yypt-0].yyv.ty, nil);
+	}
+67=>
+#line	471	"limbo.y"
+{
+		yyval.types = addtype(yys[yypt-0].yyv.ty, nil);
+		yys[yypt-0].yyv.ty.flags |= CYCLIC;
+	}
+68=>
+#line	476	"limbo.y"
+{
+		yyval.types = addtype(yys[yypt-0].yyv.ty, yys[yypt-2].yyv.types);
+	}
+69=>
+#line	480	"limbo.y"
+{
+		yyval.types = addtype(yys[yypt-0].yyv.ty, yys[yypt-3].yyv.types);
+		yys[yypt-0].yyv.ty.flags |= CYCLIC;
+	}
+70=>
+#line	487	"limbo.y"
+{
+		yyval.ty = mkidtype(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval);
+	}
+71=>
+#line	491	"limbo.y"
+{
+		yyval.ty = yys[yypt-0].yyv.ty;
+	}
+72=>
+#line	495	"limbo.y"
+{
+		yyval.ty = yys[yypt-0].yyv.ty;
+	}
+73=>
+#line	499	"limbo.y"
+{
+		yyval.ty = mkarrowtype(yys[yypt-2].yyv.ty.src.start, yys[yypt-0].yyv.tok.src.stop, yys[yypt-2].yyv.ty, yys[yypt-0].yyv.tok.v.idval);
+	}
+74=>
+#line	503	"limbo.y"
+{
+		yyval.ty = mkarrowtype(yys[yypt-5].yyv.ty.src.start, yys[yypt-3].yyv.tok.src.stop, yys[yypt-5].yyv.ty, yys[yypt-3].yyv.tok.v.idval);
+		yyval.ty = mkinsttype(yys[yypt-5].yyv.ty.src, yyval.ty, yys[yypt-1].yyv.types);
+	}
+75=>
+#line	508	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-1].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tref, yys[yypt-0].yyv.ty, nil);
+	}
+76=>
+#line	512	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tchan, yys[yypt-0].yyv.ty, nil);
+	}
+77=>
+#line	516	"limbo.y"
+{
+		if(yys[yypt-1].yyv.ids.next == nil)
+			yyval.ty = yys[yypt-1].yyv.ids.ty;
+		else
+			yyval.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.tok.src.stop, Ttuple, nil, revids(yys[yypt-1].yyv.ids));
+	}
+78=>
+#line	523	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tarray, yys[yypt-0].yyv.ty, nil);
+	}
+79=>
+#line	527	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tlist, yys[yypt-0].yyv.ty, nil);
+	}
+80=>
+#line	531	"limbo.y"
+{
+		yys[yypt-1].yyv.ty.src.start = yys[yypt-3].yyv.tok.src.start;
+		yys[yypt-1].yyv.ty.polys = yys[yypt-2].yyv.ids;
+		yys[yypt-1].yyv.ty.eraises = yys[yypt-0].yyv.node;
+		yyval.ty = yys[yypt-1].yyv.ty;
+	}
+81=>
+yyval.ty = yys[yyp+1].yyv.ty;
+82=>
+#line	551	"limbo.y"
+{
+		yyval.ty = mkidtype(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval);
+	}
+83=>
+#line	555	"limbo.y"
+{
+		yyval.ty = mkinsttype(yys[yypt-3].yyv.tok.src, mkidtype(yys[yypt-3].yyv.tok.src, yys[yypt-3].yyv.tok.v.idval), yys[yypt-1].yyv.types);
+	}
+84=>
+#line	561	"limbo.y"
+{
+		yyval.ty = mkdottype(yys[yypt-2].yyv.ty.src.start, yys[yypt-0].yyv.tok.src.stop, yys[yypt-2].yyv.ty, yys[yypt-0].yyv.tok.v.idval);
+	}
+85=>
+#line	565	"limbo.y"
+{
+		yyval.ty = mkdottype(yys[yypt-5].yyv.ty.src.start, yys[yypt-3].yyv.tok.src.stop, yys[yypt-5].yyv.ty, yys[yypt-3].yyv.tok.v.idval);
+		yyval.ty = mkinsttype(yys[yypt-5].yyv.ty.src, yyval.ty, yys[yypt-1].yyv.types);
+	}
+86=>
+#line	572	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.ty.src, nil, yys[yypt-0].yyv.ty, nil);
+	}
+87=>
+#line	576	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-2].yyv.ids.src, nil, yys[yypt-0].yyv.ty, yys[yypt-2].yyv.ids);
+	}
+88=>
+#line	582	"limbo.y"
+{
+		yyval.ids = nil;
+	}
+89=>
+#line	586	"limbo.y"
+{
+		yyval.ids = polydecl(yys[yypt-1].yyv.ids);
+	}
+90=>
+#line	592	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.tok.src.stop, Tfn, tnone, yys[yypt-1].yyv.ids);
+	}
+91=>
+#line	596	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.tok.src.stop, Tfn, tnone, nil);
+		yyval.ty.varargs = byte 1;
+	}
+92=>
+#line	601	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-4].yyv.tok.src.start, yys[yypt-0].yyv.tok.src.stop, Tfn, tnone, yys[yypt-3].yyv.ids);
+		yyval.ty.varargs = byte 1;
+	}
+93=>
+#line	608	"limbo.y"
+{
+		yyval.ty = yys[yypt-0].yyv.ty;
+	}
+94=>
+#line	612	"limbo.y"
+{
+		yys[yypt-2].yyv.ty.tof = yys[yypt-0].yyv.ty;
+		yys[yypt-2].yyv.ty.src.stop = yys[yypt-0].yyv.ty.src.stop;
+		yyval.ty = yys[yypt-2].yyv.ty;
+	}
+95=>
+#line	620	"limbo.y"
+{
+		yyval.ty = yys[yypt-0].yyv.ty;
+	}
+96=>
+#line	624	"limbo.y"
+{
+		yyval.ty = yys[yypt-4].yyv.ty;
+		yyval.ty.val = rotater(yys[yypt-1].yyv.node);
+	}
+97=>
+#line	631	"limbo.y"
+{
+		yyval.ids = nil;
+	}
+98=>
+yyval.ids = yys[yyp+1].yyv.ids;
+99=>
+yyval.ids = yys[yyp+1].yyv.ids;
+100=>
+#line	639	"limbo.y"
+{
+		yyval.ids = appdecls(yys[yypt-2].yyv.ids, yys[yypt-0].yyv.ids);
+	}
+101=>
+#line	645	"limbo.y"
+{
+		yyval.ids = typeids(yys[yypt-2].yyv.ids, yys[yypt-0].yyv.ty);
+	}
+102=>
+#line	649	"limbo.y"
+{
+		yyval.ids = typeids(yys[yypt-2].yyv.ids, yys[yypt-0].yyv.ty);
+		for(d := yyval.ids; d != nil; d = d.next)
+			d.implicit = byte 1;
+	}
+103=>
+#line	655	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-2].yyv.node.src, enter("junk", 0), yys[yypt-0].yyv.ty, nil);
+		yyval.ids.store = Darg;
+		yyerror("illegal argument declaraion");
+	}
+104=>
+#line	661	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-2].yyv.node.src, enter("junk", 0), yys[yypt-0].yyv.ty, nil);
+		yyval.ids.store = Darg;
+		yyerror("illegal argument declaraion");
+	}
+105=>
+#line	669	"limbo.y"
+{
+		yyval.ids = revids(yys[yypt-0].yyv.ids);
+	}
+106=>
+#line	675	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval, nil, nil);
+		yyval.ids.store = Darg;
+	}
+107=>
+#line	680	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, nil, nil, nil);
+		yyval.ids.store = Darg;
+	}
+108=>
+#line	685	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval, nil, yys[yypt-2].yyv.ids);
+		yyval.ids.store = Darg;
+	}
+109=>
+#line	690	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, nil, nil, yys[yypt-2].yyv.ids);
+		yyval.ids.store = Darg;
+	}
+110=>
+#line	697	"limbo.y"
+{
+		yyval.ty = yys[yypt-0].yyv.ty;
+	}
+111=>
+#line	701	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-1].yyv.tok.src.start, yys[yypt-0].yyv.tok.src.stop, Tref, yys[yypt-0].yyv.ty, nil);
+	}
+112=>
+#line	705	"limbo.y"
+{
+		yyval.ty = yys[yypt-0].yyv.ty;
+	}
+113=>
+#line	709	"limbo.y"
+{
+		yyval.ty = mktype(yys[yypt-1].yyv.tok.src.start, yys[yypt-0].yyv.tok.src.stop, Tref, yys[yypt-0].yyv.ty, nil);
+	}
+114=>
+#line	715	"limbo.y"
+{
+		yyval.node = fndecl(yys[yypt-3].yyv.node, yys[yypt-2].yyv.ty, yys[yypt-0].yyv.node);
+		nfns++;
+		# patch up polydecs
+		if(yys[yypt-3].yyv.node.op == Odot){
+			if(yys[yypt-3].yyv.node.right.left != nil){
+				yys[yypt-2].yyv.ty.polys = yys[yypt-3].yyv.node.right.left.decl;
+				yys[yypt-3].yyv.node.right.left = nil;
+			}
+			if(yys[yypt-3].yyv.node.left.op == Oname && yys[yypt-3].yyv.node.left.left != nil){
+				yyval.node.decl = yys[yypt-3].yyv.node.left.left.decl;
+				yys[yypt-3].yyv.node.left.left = nil;
+			}
+		}
+		else{
+			if(yys[yypt-3].yyv.node.left != nil){
+				yys[yypt-2].yyv.ty.polys = yys[yypt-3].yyv.node.left.decl;
+				yys[yypt-3].yyv.node.left = nil;
+			}
+		}
+		yys[yypt-2].yyv.ty.eraises = yys[yypt-1].yyv.node;
+		yyval.node.src = yys[yypt-3].yyv.node.src;
+	}
+115=>
+#line	741	"limbo.y"
+{
+		yyval.node = mkn(Otuple, rotater(yys[yypt-1].yyv.node), nil);
+		yyval.node.src.start = yys[yypt-3].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+116=>
+#line	747	"limbo.y"
+{
+		yyval.node = mkn(Otuple, mkunary(Oseq, yys[yypt-0].yyv.node), nil);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.node.src.stop;
+	}
+117=>
+#line	753	"limbo.y"
+{
+		yyval.node = nil;
+	}
+118=>
+#line	759	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node == nil){
+			yys[yypt-1].yyv.node = mkn(Onothing, nil, nil);
+			yys[yypt-1].yyv.node.src.start = curline();
+			yys[yypt-1].yyv.node.src.stop = yys[yypt-1].yyv.node.src.start;
+		}
+		yyval.node = rotater(yys[yypt-1].yyv.node);
+		yyval.node.src.start = yys[yypt-2].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+119=>
+#line	770	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+	}
+120=>
+#line	774	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+	}
+121=>
+#line	780	"limbo.y"
+{
+		yyval.node = mkname(yys[yypt-1].yyv.tok.src, yys[yypt-1].yyv.tok.v.idval);
+		if(yys[yypt-0].yyv.ids != nil){
+			yyval.node.left = mkn(Onothing, nil ,nil);
+			yyval.node.left.decl = yys[yypt-0].yyv.ids;
+		}
+	}
+122=>
+#line	788	"limbo.y"
+{
+		yyval.node = mkbin(Odot, yys[yypt-3].yyv.node, mkname(yys[yypt-1].yyv.tok.src, yys[yypt-1].yyv.tok.v.idval));
+		if(yys[yypt-0].yyv.ids != nil){
+			yyval.node.right.left = mkn(Onothing, nil ,nil);
+			yyval.node.right.left.decl = yys[yypt-0].yyv.ids;
+		}
+	}
+123=>
+#line	798	"limbo.y"
+{
+		yyval.node = nil;
+	}
+124=>
+#line	802	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node == nil)
+			yyval.node = yys[yypt-0].yyv.node;
+		else if(yys[yypt-0].yyv.node == nil)
+			yyval.node = yys[yypt-1].yyv.node;
+		else
+			yyval.node = mkbin(Oseq, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node);
+	}
+125=>
+#line	811	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node == nil)
+			yyval.node = yys[yypt-0].yyv.node;
+		else
+			yyval.node = mkbin(Oseq, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node);
+	}
+128=>
+#line	824	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = curline();
+		yyval.node.src.stop = yyval.node.src.start;
+	}
+129=>
+#line	830	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = curline();
+		yyval.node.src.stop = yyval.node.src.start;
+	}
+130=>
+#line	836	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = curline();
+		yyval.node.src.stop = yyval.node.src.start;
+	}
+131=>
+#line	842	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node == nil){
+			yys[yypt-1].yyv.node = mkn(Onothing, nil, nil);
+			yys[yypt-1].yyv.node.src.start = curline();
+			yys[yypt-1].yyv.node.src.stop = yys[yypt-1].yyv.node.src.start;
+		}
+		yyval.node = mkscope(rotater(yys[yypt-1].yyv.node));
+	}
+132=>
+#line	851	"limbo.y"
+{
+		yyerror("illegal declaration");
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = curline();
+		yyval.node.src.stop = yyval.node.src.start;
+	}
+133=>
+#line	858	"limbo.y"
+{
+		yyerror("illegal declaration");
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = curline();
+		yyval.node.src.stop = yyval.node.src.start;
+	}
+134=>
+#line	865	"limbo.y"
+{
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+135=>
+#line	869	"limbo.y"
+{
+		yyval.node = mkn(Oif, yys[yypt-2].yyv.node, mkunary(Oseq, yys[yypt-0].yyv.node));
+		yyval.node.src.start = yys[yypt-4].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.node.src.stop;
+	}
+136=>
+#line	875	"limbo.y"
+{
+		yyval.node = mkn(Oif, yys[yypt-4].yyv.node, mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node));
+		yyval.node.src.start = yys[yypt-6].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.node.src.stop;
+	}
+137=>
+#line	881	"limbo.y"
+{
+		yyval.node = mkunary(Oseq, yys[yypt-0].yyv.node);
+		if(yys[yypt-2].yyv.node.op != Onothing)
+			yyval.node.right = yys[yypt-2].yyv.node;
+		yyval.node = mkbin(Ofor, yys[yypt-4].yyv.node, yyval.node);
+		yyval.node.decl = yys[yypt-9].yyv.ids;
+		if(yys[yypt-6].yyv.node.op != Onothing)
+			yyval.node = mkbin(Oseq, yys[yypt-6].yyv.node, yyval.node);
+	}
+138=>
+#line	891	"limbo.y"
+{
+		yyval.node = mkn(Ofor, yys[yypt-2].yyv.node, mkunary(Oseq, yys[yypt-0].yyv.node));
+		yyval.node.src.start = yys[yypt-4].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.node.src.stop;
+		yyval.node.decl = yys[yypt-5].yyv.ids;
+	}
+139=>
+#line	898	"limbo.y"
+{
+		yyval.node = mkn(Odo, yys[yypt-2].yyv.node, yys[yypt-5].yyv.node);
+		yyval.node.src.start = yys[yypt-6].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-1].yyv.tok.src.stop;
+		yyval.node.decl = yys[yypt-7].yyv.ids;
+	}
+140=>
+#line	905	"limbo.y"
+{
+		yyval.node = mkn(Obreak, nil, nil);
+		yyval.node.decl = yys[yypt-1].yyv.ids;
+		yyval.node.src = yys[yypt-2].yyv.tok.src;
+	}
+141=>
+#line	911	"limbo.y"
+{
+		yyval.node = mkn(Ocont, nil, nil);
+		yyval.node.decl = yys[yypt-1].yyv.ids;
+		yyval.node.src = yys[yypt-2].yyv.tok.src;
+	}
+142=>
+#line	917	"limbo.y"
+{
+		yyval.node = mkn(Oret, yys[yypt-1].yyv.node, nil);
+		yyval.node.src = yys[yypt-2].yyv.tok.src;
+		if(yys[yypt-1].yyv.node.op == Onothing)
+			yyval.node.left = nil;
+		else
+			yyval.node.src.stop = yys[yypt-1].yyv.node.src.stop;
+	}
+143=>
+#line	926	"limbo.y"
+{
+		yyval.node = mkn(Ospawn, yys[yypt-1].yyv.node, nil);
+		yyval.node.src.start = yys[yypt-2].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-1].yyv.node.src.stop;
+	}
+144=>
+#line	932	"limbo.y"
+{
+		yyval.node = mkn(Oraise, yys[yypt-1].yyv.node, nil);
+		yyval.node.src.start = yys[yypt-2].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-1].yyv.node.src.stop;
+	}
+145=>
+#line	938	"limbo.y"
+{
+		yyval.node = mkn(Ocase, yys[yypt-3].yyv.node, caselist(yys[yypt-1].yyv.node, nil));
+		yyval.node.src = yys[yypt-3].yyv.node.src;
+		yyval.node.decl = yys[yypt-5].yyv.ids;
+	}
+146=>
+#line	944	"limbo.y"
+{
+		yyval.node = mkn(Oalt, caselist(yys[yypt-1].yyv.node, nil), nil);
+		yyval.node.src = yys[yypt-3].yyv.tok.src;
+		yyval.node.decl = yys[yypt-4].yyv.ids;
+	}
+147=>
+#line	950	"limbo.y"
+{
+		yyval.node = mkn(Opick, mkbin(Odas, mkname(yys[yypt-5].yyv.tok.src, yys[yypt-5].yyv.tok.v.idval), yys[yypt-3].yyv.node), caselist(yys[yypt-1].yyv.node, nil));
+		yyval.node.src.start = yys[yypt-5].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-3].yyv.node.src.stop;
+		yyval.node.decl = yys[yypt-7].yyv.ids;
+	}
+148=>
+#line	957	"limbo.y"
+{
+		yyval.node = mkn(Oexit, nil, nil);
+		yyval.node.src = yys[yypt-1].yyv.tok.src;
+	}
+149=>
+#line	962	"limbo.y"
+{
+		if(yys[yypt-6].yyv.node == nil){
+			yys[yypt-6].yyv.node = mkn(Onothing, nil, nil);
+			yys[yypt-6].yyv.node.src.start = yys[yypt-6].yyv.node.src.stop = curline();
+		}
+		yys[yypt-6].yyv.node = mkscope(rotater(yys[yypt-6].yyv.node));
+		yyval.node = mkbin(Oexstmt, yys[yypt-6].yyv.node, mkn(Oexcept, yys[yypt-3].yyv.node, caselist(yys[yypt-1].yyv.node, nil)));
+	}
+150=>
+#line	977	"limbo.y"
+{
+		yyval.ids = nil;
+	}
+151=>
+#line	981	"limbo.y"
+{
+		if(yys[yypt-1].yyv.ids.next != nil)
+			yyerror("only one identifier allowed in a label");
+		yyval.ids = yys[yypt-1].yyv.ids;
+	}
+152=>
+#line	989	"limbo.y"
+{
+		yyval.ids = nil;
+	}
+153=>
+#line	993	"limbo.y"
+{
+		yyval.ids = mkids(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval, nil, nil);
+	}
+154=>
+#line	999	"limbo.y"
+{
+		yys[yypt-1].yyv.node.left.right.right = yys[yypt-0].yyv.node;
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+155=>
+#line	1006	"limbo.y"
+{
+		yyval.node = mkunary(Oseq, mkscope(mkunary(Olabel, rotater(yys[yypt-1].yyv.node))));
+	}
+156=>
+#line	1010	"limbo.y"
+{
+		yys[yypt-3].yyv.node.left.right.right = yys[yypt-2].yyv.node;
+		yyval.node = mkbin(Oseq, mkscope(mkunary(Olabel, rotater(yys[yypt-1].yyv.node))), yys[yypt-3].yyv.node);
+	}
+157=>
+#line	1017	"limbo.y"
+{
+		yys[yypt-1].yyv.node.left.right = mkscope(yys[yypt-0].yyv.node);
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+158=>
+#line	1024	"limbo.y"
+{
+		yyval.node = mkunary(Oseq, mkunary(Olabel, rotater(yys[yypt-1].yyv.node)));
+	}
+159=>
+#line	1028	"limbo.y"
+{
+		yys[yypt-3].yyv.node.left.right = mkscope(yys[yypt-2].yyv.node);
+		yyval.node = mkbin(Oseq, mkunary(Olabel, rotater(yys[yypt-1].yyv.node)), yys[yypt-3].yyv.node);
+	}
+160=>
+#line	1035	"limbo.y"
+{
+		yys[yypt-1].yyv.node.left.right = mkscope(yys[yypt-0].yyv.node);
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+161=>
+#line	1042	"limbo.y"
+{
+		yyval.node = mkunary(Oseq, mkunary(Olabel, rotater(yys[yypt-1].yyv.node)));
+	}
+162=>
+#line	1046	"limbo.y"
+{
+		yys[yypt-3].yyv.node.left.right = mkscope(yys[yypt-2].yyv.node);
+		yyval.node = mkbin(Oseq, mkunary(Olabel, rotater(yys[yypt-1].yyv.node)), yys[yypt-3].yyv.node);
+	}
+163=>
+yyval.node = yys[yyp+1].yyv.node;
+164=>
+#line	1054	"limbo.y"
+{
+		yyval.node = mkbin(Orange, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+165=>
+#line	1058	"limbo.y"
+{
+		yyval.node = mkn(Owild, nil, nil);
+		yyval.node.src = yys[yypt-0].yyv.tok.src;
+	}
+166=>
+#line	1063	"limbo.y"
+{
+		yyval.node = mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+167=>
+#line	1067	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = curline();
+		yyval.node.src.stop = yyval.node.src.start;
+	}
+168=>
+#line	1075	"limbo.y"
+{
+		yys[yypt-1].yyv.node.left.right = mkscope(yys[yypt-0].yyv.node);
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+169=>
+#line	1082	"limbo.y"
+{
+		yyval.node = mkunary(Oseq, mkunary(Olabel, rotater(yys[yypt-1].yyv.node)));
+	}
+170=>
+#line	1086	"limbo.y"
+{
+		yys[yypt-3].yyv.node.left.right = mkscope(yys[yypt-2].yyv.node);
+		yyval.node = mkbin(Oseq, mkunary(Olabel, rotater(yys[yypt-1].yyv.node)), yys[yypt-3].yyv.node);
+	}
+171=>
+#line	1093	"limbo.y"
+{
+		yyval.node = mkname(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval);
+	}
+172=>
+#line	1097	"limbo.y"
+{
+		yyval.node = mkn(Owild, nil, nil);
+		yyval.node.src = yys[yypt-0].yyv.tok.src;
+	}
+173=>
+#line	1102	"limbo.y"
+{
+		yyval.node = mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+174=>
+#line	1106	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = curline();
+		yyval.node.src.stop = yyval.node.src.start;
+	}
+175=>
+#line	1114	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = curline();
+		yyval.node.src.stop = yyval.node.src.start;
+	}
+176=>
+yyval.node = yys[yyp+1].yyv.node;
+177=>
+yyval.node = yys[yyp+1].yyv.node;
+178=>
+#line	1124	"limbo.y"
+{
+		yyval.node = mkbin(Oas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+179=>
+#line	1128	"limbo.y"
+{
+		yyval.node = mkbin(Oandas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+180=>
+#line	1132	"limbo.y"
+{
+		yyval.node = mkbin(Ooras, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+181=>
+#line	1136	"limbo.y"
+{
+		yyval.node = mkbin(Oxoras, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+182=>
+#line	1140	"limbo.y"
+{
+		yyval.node = mkbin(Olshas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+183=>
+#line	1144	"limbo.y"
+{
+		yyval.node = mkbin(Orshas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+184=>
+#line	1148	"limbo.y"
+{
+		yyval.node = mkbin(Oaddas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+185=>
+#line	1152	"limbo.y"
+{
+		yyval.node = mkbin(Osubas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+186=>
+#line	1156	"limbo.y"
+{
+		yyval.node = mkbin(Omulas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+187=>
+#line	1160	"limbo.y"
+{
+		yyval.node = mkbin(Odivas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+188=>
+#line	1164	"limbo.y"
+{
+		yyval.node = mkbin(Omodas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+189=>
+#line	1168	"limbo.y"
+{
+		yyval.node = mkbin(Oexpas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+190=>
+#line	1172	"limbo.y"
+{
+		yyval.node = mkbin(Osnd, yys[yypt-3].yyv.node, yys[yypt-0].yyv.node);
+	}
+191=>
+#line	1176	"limbo.y"
+{
+		yyval.node = mkbin(Odas, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+192=>
+#line	1180	"limbo.y"
+{
+		yyval.node = mkn(Oload, yys[yypt-0].yyv.node, nil);
+		yyval.node.src.start = yys[yypt-2].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.node.src.stop;
+		yyval.node.ty = mkidtype(yys[yypt-1].yyv.tok.src, yys[yypt-1].yyv.tok.v.idval);
+	}
+193=>
+#line	1187	"limbo.y"
+{
+		yyval.node = yyval.node = mkbin(Oexp, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+194=>
+#line	1191	"limbo.y"
+{
+		yyval.node = mkbin(Omul, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+195=>
+#line	1195	"limbo.y"
+{
+		yyval.node = mkbin(Odiv, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+196=>
+#line	1199	"limbo.y"
+{
+		yyval.node = mkbin(Omod, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+197=>
+#line	1203	"limbo.y"
+{
+		yyval.node = mkbin(Oadd, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+198=>
+#line	1207	"limbo.y"
+{
+		yyval.node = mkbin(Osub, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+199=>
+#line	1211	"limbo.y"
+{
+		yyval.node = mkbin(Orsh, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+200=>
+#line	1215	"limbo.y"
+{
+		yyval.node = mkbin(Olsh, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+201=>
+#line	1219	"limbo.y"
+{
+		yyval.node = mkbin(Olt, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+202=>
+#line	1223	"limbo.y"
+{
+		yyval.node = mkbin(Ogt, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+203=>
+#line	1227	"limbo.y"
+{
+		yyval.node = mkbin(Oleq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+204=>
+#line	1231	"limbo.y"
+{
+		yyval.node = mkbin(Ogeq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+205=>
+#line	1235	"limbo.y"
+{
+		yyval.node = mkbin(Oeq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+206=>
+#line	1239	"limbo.y"
+{
+		yyval.node = mkbin(Oneq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+207=>
+#line	1243	"limbo.y"
+{
+		yyval.node = mkbin(Oand, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+208=>
+#line	1247	"limbo.y"
+{
+		yyval.node = mkbin(Oxor, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+209=>
+#line	1251	"limbo.y"
+{
+		yyval.node = mkbin(Oor, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+210=>
+#line	1255	"limbo.y"
+{
+		yyval.node = mkbin(Ocons, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+211=>
+#line	1259	"limbo.y"
+{
+		yyval.node = mkbin(Oandand, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+212=>
+#line	1263	"limbo.y"
+{
+		yyval.node = mkbin(Ooror, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+213=>
+yyval.node = yys[yyp+1].yyv.node;
+214=>
+#line	1270	"limbo.y"
+{
+		yys[yypt-0].yyv.node.src.start = yys[yypt-1].yyv.tok.src.start;
+		yyval.node = yys[yypt-0].yyv.node;
+	}
+215=>
+#line	1275	"limbo.y"
+{
+		yyval.node = mkunary(Oneg, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+216=>
+#line	1280	"limbo.y"
+{
+		yyval.node = mkunary(Onot, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+217=>
+#line	1285	"limbo.y"
+{
+		yyval.node = mkunary(Ocomp, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+218=>
+#line	1290	"limbo.y"
+{
+		yyval.node = mkunary(Oind, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+219=>
+#line	1295	"limbo.y"
+{
+		yyval.node = mkunary(Opreinc, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+220=>
+#line	1300	"limbo.y"
+{
+		yyval.node = mkunary(Opredec, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+221=>
+#line	1305	"limbo.y"
+{
+		yyval.node = mkunary(Orcv, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+222=>
+#line	1310	"limbo.y"
+{
+		yyval.node = mkunary(Ohd, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+223=>
+#line	1315	"limbo.y"
+{
+		yyval.node = mkunary(Otl, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+224=>
+#line	1320	"limbo.y"
+{
+		yyval.node = mkunary(Olen, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+225=>
+#line	1325	"limbo.y"
+{
+		yyval.node = mkunary(Oref, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+226=>
+#line	1330	"limbo.y"
+{
+		yyval.node = mkunary(Otagof, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+	}
+227=>
+#line	1335	"limbo.y"
+{
+		yyval.node = mkn(Oarray, yys[yypt-3].yyv.node, nil);
+		yyval.node.ty = mktype(yys[yypt-5].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tarray, yys[yypt-0].yyv.ty, nil);
+		yyval.node.src = yyval.node.ty.src;
+	}
+228=>
+#line	1341	"limbo.y"
+{
+		yyval.node = mkn(Oarray, yys[yypt-5].yyv.node, yys[yypt-1].yyv.node);
+		yyval.node.src.start = yys[yypt-7].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+229=>
+#line	1347	"limbo.y"
+{
+		yyval.node = mkn(Onothing, nil, nil);
+		yyval.node.src.start = yys[yypt-5].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-4].yyv.tok.src.stop;
+		yyval.node = mkn(Oarray, yyval.node, yys[yypt-1].yyv.node);
+		yyval.node.src.start = yys[yypt-6].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+230=>
+#line	1356	"limbo.y"
+{
+		yyval.node = etolist(yys[yypt-1].yyv.node);
+		yyval.node.src.start = yys[yypt-4].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+231=>
+#line	1362	"limbo.y"
+{
+		yyval.node = mkn(Ochan, nil, nil);
+		yyval.node.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tchan, yys[yypt-0].yyv.ty, nil);
+		yyval.node.src = yyval.node.ty.src;
+	}
+232=>
+#line	1368	"limbo.y"
+{
+		yyval.node = mkn(Ochan, yys[yypt-3].yyv.node, nil);
+		yyval.node.ty = mktype(yys[yypt-5].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tchan, yys[yypt-0].yyv.ty, nil);
+		yyval.node.src = yyval.node.ty.src;
+	}
+233=>
+#line	1374	"limbo.y"
+{
+		yyval.node = mkunary(Ocast, yys[yypt-0].yyv.node);
+		yyval.node.ty = mktype(yys[yypt-3].yyv.tok.src.start, yys[yypt-0].yyv.node.src.stop, Tarray, mkidtype(yys[yypt-1].yyv.tok.src, yys[yypt-1].yyv.tok.v.idval), nil);
+		yyval.node.src = yyval.node.ty.src;
+	}
+234=>
+#line	1380	"limbo.y"
+{
+		yyval.node = mkunary(Ocast, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+		yyval.node.ty = mkidtype(yyval.node.src, yys[yypt-1].yyv.tok.v.idval);
+	}
+235=>
+#line	1386	"limbo.y"
+{
+		yyval.node = mkunary(Ocast, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+		yyval.node.ty = mkidtype(yyval.node.src, yys[yypt-1].yyv.tok.v.idval);
+	}
+236=>
+#line	1392	"limbo.y"
+{
+		yyval.node = mkunary(Ocast, yys[yypt-0].yyv.node);
+		yyval.node.src.start = yys[yypt-1].yyv.tok.src.start;
+		yyval.node.ty = yys[yypt-1].yyv.ty;
+	}
+237=>
+yyval.node = yys[yyp+1].yyv.node;
+238=>
+#line	1401	"limbo.y"
+{
+		yyval.node = mkn(Ocall, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node);
+		yyval.node.src.start = yys[yypt-3].yyv.node.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+239=>
+#line	1407	"limbo.y"
+{
+		yyval.node = yys[yypt-1].yyv.node;
+		if(yys[yypt-1].yyv.node.op == Oseq)
+			yyval.node = mkn(Otuple, rotater(yys[yypt-1].yyv.node), nil);
+		else
+			yyval.node.flags |= byte PARENS;
+		yyval.node.src.start = yys[yypt-2].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+240=>
+#line	1417	"limbo.y"
+{
+#		n := mkdeclname($1, mkids($1, enter(".fn"+string nfnexp++, 0), nil, nil));
+#		$<node>$ = fndef(n, $2);
+#		nfns++;
+	}
+241=>
+#line	1422	"limbo.y"
+{
+#		$$ = fnfinishdef($<node>3, $4);
+#		$$ = mkdeclname($1, $$.left.decl);
+		yyerror("urt unk");
+		yyval.node = nil;
+	}
+242=>
+#line	1429	"limbo.y"
+{
+		yyval.node = mkbin(Odot, yys[yypt-2].yyv.node, mkname(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval));
+	}
+243=>
+#line	1433	"limbo.y"
+{
+		yyval.node = mkbin(Omdot, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+244=>
+#line	1437	"limbo.y"
+{
+		yyval.node = mkbin(Oindex, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node);
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+245=>
+#line	1442	"limbo.y"
+{
+		if(yys[yypt-3].yyv.node.op == Onothing)
+			yys[yypt-3].yyv.node.src = yys[yypt-2].yyv.tok.src;
+		if(yys[yypt-1].yyv.node.op == Onothing)
+			yys[yypt-1].yyv.node.src = yys[yypt-2].yyv.tok.src;
+		yyval.node = mkbin(Oslice, yys[yypt-5].yyv.node, mkbin(Oseq, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node));
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+246=>
+#line	1451	"limbo.y"
+{
+		yyval.node = mkunary(Oinc, yys[yypt-1].yyv.node);
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+247=>
+#line	1456	"limbo.y"
+{
+		yyval.node = mkunary(Odec, yys[yypt-1].yyv.node);
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+248=>
+#line	1461	"limbo.y"
+{
+		yyval.node = mksconst(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval);
+	}
+249=>
+#line	1465	"limbo.y"
+{
+		yyval.node = mkconst(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.ival);
+		if(yys[yypt-0].yyv.tok.v.ival > big 16r7fffffff || yys[yypt-0].yyv.tok.v.ival < big -16r7fffffff)
+			yyval.node.ty = tbig;
+	}
+250=>
+#line	1471	"limbo.y"
+{
+		yyval.node = mkrconst(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.rval);
+	}
+251=>
+#line	1475	"limbo.y"
+{
+		yyval.node = mkbin(Oindex, yys[yypt-5].yyv.node, rotater(mkbin(Oseq, yys[yypt-3].yyv.node, yys[yypt-1].yyv.node)));
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+252=>
+#line	1482	"limbo.y"
+{
+		yyval.node = mkname(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval);
+	}
+253=>
+#line	1486	"limbo.y"
+{
+		yyval.node = mknil(yys[yypt-0].yyv.tok.src);
+	}
+254=>
+#line	1492	"limbo.y"
+{
+		yyval.node = mkn(Otuple, rotater(yys[yypt-1].yyv.node), nil);
+		yyval.node.src.start = yys[yypt-2].yyv.tok.src.start;
+		yyval.node.src.stop = yys[yypt-0].yyv.tok.src.stop;
+	}
+255=>
+yyval.node = yys[yyp+1].yyv.node;
+256=>
+#line	1501	"limbo.y"
+{
+		yyval.node = mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+257=>
+yyval.node = yys[yyp+1].yyv.node;
+258=>
+yyval.node = yys[yyp+1].yyv.node;
+259=>
+#line	1511	"limbo.y"
+{
+		yyval.node = mkn(Otype, nil, nil);
+		yyval.node.ty = mkidtype(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval);
+		yyval.node.src = yyval.node.ty.src;
+	}
+260=>
+#line	1517	"limbo.y"
+{
+		yyval.node = mkn(Otype, nil, nil);
+		yyval.node.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tarray, yys[yypt-0].yyv.ty, nil);
+		yyval.node.src = yyval.node.ty.src;
+	}
+261=>
+#line	1523	"limbo.y"
+{
+		yyval.node = mkn(Otype, nil, nil);
+		yyval.node.ty = mktype(yys[yypt-2].yyv.tok.src.start, yys[yypt-0].yyv.ty.src.stop, Tlist, yys[yypt-0].yyv.ty, nil);
+		yyval.node.src = yyval.node.ty.src;
+	}
+262=>
+#line	1529	"limbo.y"
+{
+		yyval.node = mkn(Otype, nil ,nil);
+		yyval.node.ty = yys[yypt-0].yyv.ty;
+		yyval.node.ty.flags |= CYCLIC;
+		yyval.node.src = yyval.node.ty.src;
+	}
+263=>
+#line	1538	"limbo.y"
+{
+		yyval.node = mkname(yys[yypt-0].yyv.tok.src, yys[yypt-0].yyv.tok.v.idval);
+	}
+264=>
+#line	1542	"limbo.y"
+{
+		yyval.node = nil;
+	}
+265=>
+yyval.node = yys[yyp+1].yyv.node;
+266=>
+yyval.node = yys[yyp+1].yyv.node;
+267=>
+#line	1550	"limbo.y"
+{
+		yyval.node = mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+268=>
+#line	1554	"limbo.y"
+{
+		yyval.node = mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+269=>
+#line	1560	"limbo.y"
+{
+		yyval.node = nil;
+	}
+270=>
+#line	1564	"limbo.y"
+{
+		yyval.node = rotater(yys[yypt-0].yyv.node);
+	}
+271=>
+yyval.node = yys[yyp+1].yyv.node;
+272=>
+yyval.node = yys[yyp+1].yyv.node;
+273=>
+yyval.node = yys[yyp+1].yyv.node;
+274=>
+#line	1575	"limbo.y"
+{
+		yyval.node = mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+275=>
+#line	1581	"limbo.y"
+{
+		yyval.node = rotater(yys[yypt-0].yyv.node);
+	}
+276=>
+#line	1585	"limbo.y"
+{
+		yyval.node = rotater(yys[yypt-1].yyv.node);
+	}
+277=>
+yyval.node = yys[yyp+1].yyv.node;
+278=>
+#line	1592	"limbo.y"
+{
+		yyval.node = mkbin(Oseq, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node);
+	}
+279=>
+#line	1598	"limbo.y"
+{
+		yyval.node = mkn(Oelem, nil, yys[yypt-0].yyv.node);
+		yyval.node.src = yys[yypt-0].yyv.node.src;
+	}
+280=>
+#line	1603	"limbo.y"
+{
+		yyval.node = mkbin(Oelem, rotater(yys[yypt-2].yyv.node), yys[yypt-0].yyv.node);
+	}
+281=>
+#line	1609	"limbo.y"
+{
+		if(yys[yypt-1].yyv.node.op == Oseq)
+			yys[yypt-1].yyv.node.right.left = rotater(yys[yypt-0].yyv.node);
+		else
+			yys[yypt-1].yyv.node.left = rotater(yys[yypt-0].yyv.node);
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+282=>
+#line	1619	"limbo.y"
+{
+		yyval.node = typedecl(yys[yypt-1].yyv.ids, mktype(yys[yypt-1].yyv.ids.src.start, yys[yypt-0].yyv.tok.src.stop, Tpoly, nil, nil));
+	}
+283=>
+#line	1623	"limbo.y"
+{
+		if(yys[yypt-3].yyv.node.op == Oseq)
+			yys[yypt-3].yyv.node.right.left = rotater(yys[yypt-2].yyv.node);
+		else
+			yys[yypt-3].yyv.node.left = rotater(yys[yypt-2].yyv.node);
+		yyval.node = mkbin(Oseq, yys[yypt-3].yyv.node, typedecl(yys[yypt-1].yyv.ids, mktype(yys[yypt-1].yyv.ids.src.start, yys[yypt-0].yyv.tok.src.stop, Tpoly, nil, nil)));
+	}
+		}
+	}
+
+	return yyn;
+}
--- /dev/null
+++ b/appl/cmd/limbo/limbo.m
@@ -1,0 +1,527 @@
+include "sys.m";
+include "math.m";
+include "string.m";
+include "bufio.m";
+include "isa.m";
+include "workdir.m";
+
+# internal dis ops
+IEXC: con MAXDIS;
+IEXC0: con (MAXDIS+1);
+INOOP: con (MAXDIS+2);
+
+# temporary
+LDT: con 1;
+
+STemp:		con NREG * IBY2WD;
+RTemp:		con STemp + IBY2WD;
+DTemp:		con RTemp + IBY2WD;
+MaxTemp:	con DTemp + IBY2WD;
+MaxReg:		con 1 << 16;
+MaxAlign:	con IBY2LG;
+StrSize:	con 256;
+MaxIncPath:	con 32;			# max directories in include path
+MaxScope:	con 64;			# max nested {}
+MaxInclude:	con 32;			# max nested include ""
+ScopeBuiltin,
+ScopeNils,
+ScopeGlobal:	con iota;
+
+Line:		type int;
+PosBits:	con 10;
+PosMask:	con (1 << PosBits) - 1;
+
+Src: adt
+{
+	start:	Line;
+	stop:	Line;
+};
+
+File: adt
+{
+	name:	string;
+	abs:	int;				# absolute line of start of the part of file
+	off:	int;				# offset to line in the file
+	in:	int;				# absolute line where included
+	act:	string;				# name of real file with #line fake file
+	actoff:	int;				# offset from fake line to real line
+	sbl:	int;				# symbol file number
+};
+
+Val: adt
+{
+	idval:	ref Sym;
+	ival:	big;
+	rval:	real;
+};
+
+Tok: adt
+{
+	src:	Src;
+	v:	Val;
+};
+
+#
+# addressing modes
+#
+	Aimm,					# immediate
+	Amp,					# global
+	Ampind,					# global indirect
+	Afp,					# activation frame
+	Afpind,					# frame indirect
+	Apc,					# branch
+	Adesc,					# type descriptor immediate
+	Aoff,					# offset in module description table
+	Anoff,				# above encoded as a -ve
+	Aerr,					# error
+	Anone,					# no operand
+	Aldt,					# linkage descriptor table immediate
+	Aend:		con byte iota;
+
+Addr: adt
+{
+	reg:		int;
+	offset:		int;
+	decl:		cyclic ref Decl;
+};
+
+Inst: adt
+{
+	src:		Src;
+	op:		int;			# could be a byte
+	pc:		int;
+	reach:		byte;			# could a control path reach this instruction?
+	sm:		byte;			# operand addressing modes
+	mm:		byte;
+	dm:		byte;
+	s:		cyclic Addr;		# operands
+	m:		cyclic Addr;
+	d:		cyclic Addr;
+	branch:		cyclic ref Inst;	# branch destination
+	next:		cyclic ref Inst;
+	block:		int;			# blocks nested inside
+};
+
+Case: adt
+{
+	nlab:		int;
+	nsnd:		int;
+	offset:		int;			# offset in mp
+	labs:		cyclic array of Label;
+	wild:		cyclic ref Node;	# if nothing matches
+	iwild:		cyclic ref Inst;
+};
+
+Label: adt
+{
+	node:		cyclic ref Node;
+	isptr:		int;			# true if the labelled alt channel is a pointer
+	start:		cyclic ref Node;	# value in range [start, stop) => code
+	stop:		cyclic ref Node;
+	inst:		cyclic ref Inst;
+};
+
+#
+# storage classes
+#
+	Dtype,
+	Dfn,
+	Dglobal,
+	Darg,
+	Dlocal,
+	Dconst,
+	Dfield,
+	Dtag,					# pick tags
+	Dimport,				# imported identifier
+	Dunbound,				# unbound identified
+	Dundef,
+	Dwundef,				# undefined, but don't whine
+
+	Dend:		con  iota;
+
+Decl: adt
+{
+	src:		Src;			# where declaration
+	sym:		cyclic ref Sym;		# name
+	store:		int;			# storage class
+	nid:		byte;		# block grouping for locals
+	inline:	byte;		# inline function
+	handler:	byte;		# fn has exception handler(s)
+	das:	byte;	# declared with :=
+	dot:		cyclic ref Decl;	# parent adt or module
+	ty:		cyclic ref Type;
+	refs:		int;			# number of references
+	offset:		int;
+	tag:		int;			# union tag
+
+	scope:		int;			# in which it was declared
+	next:		cyclic ref Decl;	# list in same scope, field or argument list, etc.
+	old:		cyclic ref Decl;	# declaration of the symbol in enclosing scope
+
+	eimport:	cyclic ref Node;	# expr from which imported
+	importid:	cyclic ref Decl;	# identifier imported
+	timport:	cyclic ref Decl;	# stack of identifiers importing a type
+
+	init:		cyclic ref Node;	# data initialization
+	tref:		int;			# 1 => is a tmp; >=2 => tmp in use
+	cycle:		byte;			# can create a cycle
+	cyc:		byte;			# so labelled in source
+	cycerr:		byte;			# delivered an error message for cycle?
+	implicit:	byte;			# implicit first argument in an adt?
+
+	iface:		cyclic ref Decl;	# used external declarations in a module
+
+	locals:		cyclic ref Decl;	# locals for a function
+	link:		cyclic ref Decl;			# pointer to parent function or function argument or local share or parent type dec
+	pc:		cyclic ref Inst;	# start of function
+	# endpc:		cyclic ref Inst;	# limit of function - unused
+
+# should be able to move this to Type
+	desc:		ref Desc;		# heap descriptor
+};
+
+Desc: adt
+{
+	id:		int;			# dis type identifier
+	used:		int;			# actually used in output?
+	map:		array of byte;		# byte map of pointers
+	size:		int;			# length of the object
+	nmap:		int;			# length of good bytes in map
+	next:		cyclic ref Desc;
+};
+
+Dlist: adt
+{
+	d: ref Decl;
+	next: cyclic ref Dlist;
+};
+
+Except: adt
+{
+	p1:	ref Inst;		# first pc covered
+	p2:	ref Inst;		# last pc not covered
+	c:	ref Case;		# exception case instructions
+	d:	ref Decl;		# exception definition if any
+	zn:	ref Node;		# list of nodes to zero in handler
+	desc:	ref Desc;	# descriptor map for above
+	ne:	int;			# number of exceptions (ie not strings) in case
+	next:	cyclic ref Except;
+};
+
+Sym: adt
+{
+	token:		int;
+	name:		string;
+	hash:		int;
+	next:		cyclic ref Sym;
+	decl:		cyclic ref Decl;
+	unbound:	cyclic ref Decl;	# place holder for unbound symbols
+};
+
+#
+# ops for nodes
+#
+	Oadd,
+	Oaddas,
+	Oadr,
+	Oadtdecl,
+	Oalt,
+	Oand,
+	Oandand,
+	Oandas,
+	Oarray,
+	Oas,
+	Obreak,
+	Ocall,
+	Ocase,
+	Ocast,
+	Ochan,
+	Ocomma,
+	Ocomp,
+	Ocondecl,
+	Ocons,
+	Oconst,
+	Ocont,
+	Odas,
+	Odec,
+	Odiv,
+	Odivas,
+	Odo,
+	Odot,
+	Oelem,
+	Oeq,
+	Oexcept,
+	Oexdecl,
+	Oexit,
+	Oexp,
+	Oexpas,
+	Oexstmt,
+	Ofielddecl,
+	Ofnptr,
+	Ofor,
+	Ofunc,
+	Ogeq,
+	Ogt,
+	Ohd,
+	Oif,
+	Oimport,
+	Oinc,
+	Oind,
+	Oindex,
+	Oinds,
+	Oindx,
+	Oinv,
+	Ojmp,
+	Olabel,
+	Olen,
+	Oleq,
+	Oload,
+	Olsh,
+	Olshas,
+	Olt,
+	Omdot,
+	Omod,
+	Omodas,
+	Omoddecl,
+	Omul,
+	Omulas,
+	Oname,
+	Oneg,
+	Oneq,
+	Onot,
+	Onothing,
+	Oor,
+	Ooras,
+	Ooror,
+	Opick,
+	Opickdecl,
+	Opredec,
+	Opreinc,
+	Oraise,
+	Orange,
+	Orcv,
+	Oref,
+	Oret,
+	Orsh,
+	Orshas,
+	Oscope,
+	Oself,
+	Oseq,
+	Oslice,
+	Osnd,
+	Ospawn,
+	Osub,
+	Osubas,
+	Otagof,
+	Otl,
+	Otuple,
+	Otype,
+	Otypedecl,
+	Oused,
+	Ovardecl,
+	Ovardecli,
+	Owild,
+	Oxor,
+	Oxoras,
+
+	Oend:		con iota + 1;
+
+#
+# moves
+#
+	Mas,
+	Mcons,
+	Mhd,
+	Mtl,
+
+	Mend:		con iota;
+
+#
+# addressability
+#
+	Rreg,				# v(fp)
+	Rmreg,				# v(mp)
+	Roff,				# $v
+	Rnoff,			# $v encoded as -ve
+	Rdesc,				# $v
+	Rdescp,				# $v
+	Rconst,				# $v
+	Ralways,			# preceeding are always addressable
+	Radr,				# v(v(fp))
+	Rmadr,				# v(v(mp))
+	Rcant,				# following are not quite addressable
+	Rpc,				# branch address
+	Rmpc,				# cross module branch address
+	Rareg,				# $v(fp)
+	Ramreg,				# $v(mp)
+	Raadr,				# $v(v(fp))
+	Ramadr,				# $v(v(mp))
+	Rldt,					# $v
+
+	Rend:		con byte iota;
+
+
+Const: adt
+{
+	val:		big;
+	rval:		real;
+};
+
+PARENS: con	1;
+TEMP: con	2;
+FNPTRA: con	4;	# argument
+FNPTR2: con	8;	# 2nd parameter
+FNPTRN: con	16;	# use -ve offset
+FNPTR: con	FNPTRA|FNPTR2|FNPTRN;
+
+Node: adt
+{
+	src:		Src;
+	op:		int;
+	addable:	byte;
+	flags:		byte;
+	temps:		byte;
+	left:		cyclic ref Node;
+	right:		cyclic ref Node;
+	ty:		cyclic ref Type;
+	decl:		cyclic ref Decl;
+	c:		ref Const;	# for Oconst
+};
+
+	#
+	# types visible to limbo
+	#
+	Tnone,
+	Tadt,
+	Tadtpick,			# pick case of an adt
+	Tarray,
+	Tbig,				# 64 bit int
+	Tbyte,				# 8 bit unsigned int
+	Tchan,
+	Treal,
+	Tfn,
+	Tint,				# 32 bit int
+	Tlist,
+	Tmodule,
+	Tref,
+	Tstring,
+	Ttuple,
+	Texception,
+	Tfix,
+	Tpoly,
+
+	#
+	# internal use types
+	#
+	Tainit,				# array initializers
+	Talt,				# alt channels
+	Tany,				# type of nil
+	Tarrow,				# unresolved ty->ty types
+	Tcase,				# case labels
+	Tcasel,				# case big labels
+	Tcasec,				# case string labels
+	Tdot,				# unresolved ty.id types
+	Terror,
+	Tgoto,				# goto labels
+	Tid,				# id with unknown type
+	Tiface,				# module interface
+	Texcept,			# exception handler tables
+	Tinst,			# instantiated adt
+
+	Tend:		con iota;
+
+	#
+	# marks for various phases of verifing types
+	#
+	OKbind,				# type decls are bound
+	OKverify,			# type looks ok
+	OKsized,			# started figuring size
+	OKref,				# recorded use of type
+	OKclass,			# equivalence class found
+	OKcyc,				# checked for cycles
+	OKcycsize,			# checked for cycles and size
+	OKmodref:			# started checking for a module handle
+
+			con byte 1 << iota;
+	OKmask:		con byte 16rff;
+
+	#
+	# recursive marks
+	#
+	TReq,
+	TRcom,
+	TRcyc,
+	TRvis:
+			con byte 1 << iota;
+
+# type flags
+FULLARGS: con byte 1;	# all hidden args added
+INST: con byte 2;	# instantiated adt
+CYCLIC: con byte 4;	# cyclic type
+POLY: con byte 8;	# polymorphic types inside
+NOPOLY: con byte 16;	# no polymorphic types inside
+
+# must put some picks in here
+Type: adt
+{
+	src:		Src;
+	kind:		int;
+	ok:		byte;		# set when type is verified
+	varargs:	byte;		# if a function, ends with vargs?
+	linkall:	byte;		# put all iface fns in external linkage?
+	rec:		byte;		# in the middle of recursive type
+	pr:		byte;		# in the middle of printing a recursive type
+	cons:	byte;		# exception constant
+	flags:	byte;
+	sbl:		int;		# slot in .sbl adt table
+	sig:		int;		# signature for dynamic type check
+	size:		int;		# storage required, in bytes
+	align:		int;		# alignment in bytes
+	decl:		cyclic ref Decl;
+	tof:		cyclic ref Type;
+	ids:		cyclic ref Decl;
+	tags:		cyclic ref Decl;# tagged fields in an adt
+	polys:	cyclic ref Decl;# polymorphic fields in fn or adt
+	cse:		cyclic ref Case;# case or goto labels
+	teq:		cyclic ref Type;# temporary equiv class for equiv checking
+	tcom:		cyclic ref Type;# temporary equiv class for compat checking
+	eq:		cyclic ref Teq;	# real equiv class
+	eraises:	cyclic ref Node;		# for Tfn only
+	val:		cyclic ref Node;		# for Tfix, Tfn, Tadt only
+	tlist:		cyclic ref Typelist;		# for Tinst only
+	tmap:	cyclic ref Tpair;			# for Tadt only
+};
+
+#
+# type equivalence classes
+#
+Teq: adt
+{
+	id:		int;		# for signing
+	ty:		cyclic ref Type;# an instance of the class
+	eq:		cyclic ref Teq;	# used to link eq sets
+};
+
+Tattr: adt
+{
+	isptr:		int;
+	refable:	int;
+	conable:	int;
+	isbig:		int;
+	vis:		int;		# type visible to users
+};
+
+Tpair: adt
+{
+	t1: cyclic ref Type;
+	t2: cyclic ref Type;
+	nxt: cyclic ref Tpair;
+};
+
+Typelist: adt
+{
+	t: cyclic ref Type;
+	nxt: cyclic ref Typelist;
+};
+
+Sother, Sloop, Sscope : con iota;
--- /dev/null
+++ b/appl/cmd/limbo/limbo.y
@@ -1,0 +1,1978 @@
+%{
+include "limbo.m";
+include "draw.m";
+
+%}
+
+%module Limbo
+{
+	init:		fn(ctxt: ref Draw->Context, argv: list of string);
+
+	YYSTYPE: adt{
+		tok:	Tok;
+		ids:	ref Decl;
+		node:	ref Node;
+		ty:	ref Type;
+		types:	ref Typelist;
+	};
+
+	YYLEX: adt {
+		lval: YYSTYPE;
+		lex: fn(nil: self ref YYLEX): int;
+		error: fn(nil: self ref YYLEX, err: string);
+	};
+}
+
+%{
+	#
+	# lex.b
+	#
+	signdump:	string;			# name of function for sig debugging
+	superwarn:	int;
+	debug:		array of int;
+	noline:		Line;
+	nosrc:		Src;
+	arrayz:		int;
+	oldcycles:	int;
+	emitcode:	string;			# emit stub routines for system module functions
+	emitdyn: int;				# emit as above but for dynamic modules
+	emitsbl:	string;			# emit symbol file for sysm modules
+	emitstub:	int;			# emit type and call frames for system modules
+	emittab:	string;			# emit table of runtime functions for this module
+	errors:		int;
+	mustcompile:	int;
+	dontcompile:	int;
+	asmsym:		int;			# generate symbols in assembly language?
+	bout:		ref Bufio->Iobuf;	# output file
+	bsym:		ref Bufio->Iobuf;	# symbol output file; nil => no sym out
+	gendis:		int;			# generate dis or asm?
+	fixss:		int;
+	newfnptr:	int;		# ISELF and -ve indices
+	optims: int;
+
+	#
+	# decls.b
+	#
+	scope:		int;
+	# impmod:		ref Sym;		# name of implementation module
+	impmods:		ref Decl;		# name of implementation module(s)
+	nildecl:	ref Decl;		# declaration for limbo's nil
+	selfdecl:	ref Decl;		# declaration for limbo's self
+
+	#
+	# types.b
+	#
+	tany:		ref Type;
+	tbig:		ref Type;
+	tbyte:		ref Type;
+	terror:		ref Type;
+	tint:		ref Type;
+	tnone:		ref Type;
+	treal:		ref Type;
+	tstring:	ref Type;
+	texception:	ref Type;
+	tunknown:	ref Type;
+	tfnptr:	ref Type;
+	rtexception:	ref Type;
+	descriptors:	ref Desc;		# list of all possible descriptors
+	tattr:		array of Tattr;
+
+	#
+	# nodes.b
+	#
+	opcommute:	array of int;
+	oprelinvert:	array of int;
+	isused:		array of int;
+	casttab:	array of array of int;	# instruction to cast from [1] to [2]
+
+	nfns:		int;			# functions defined
+	nfnexp:		int;
+	fns:		array of ref Decl;	# decls for fns defined
+	tree:		ref Node;		# root of parse tree
+
+	parset:		int;			# time to parse
+	checkt:		int;			# time to typecheck
+	gent:		int;			# time to generate code
+	writet:		int;			# time to write out code
+	symt:		int;			# time to write out symbols
+%}
+
+%type	<ty>	type fnarg fnargret fnargretp adtk fixtype iditype dotiditype
+%type	<ids>	ids rids nids nrids tuplist forms ftypes ftype
+		bclab bctarg ptags rptags polydec
+%type	<node>	zexp exp monexp term elist zelist celist
+		idatom idterms idterm idlist
+		initlist elemlist elem qual
+		decl topdecls topdecl fndef fbody stmt stmts qstmts qbodies cqstmts cqbodies
+		mdecl adtdecl mfield mfields field fields fnname
+		pstmts pbodies pqual pfields pfbody pdecl dfield dfields
+		eqstmts eqbodies idexc edecl raises tpoly tpolys texp export exportlist forpoly
+%type	<types>	types
+
+%right	<tok.src>	'=' Landeq Loreq Lxoreq Llsheq Lrsheq
+			Laddeq Lsubeq Lmuleq Ldiveq Lmodeq Lexpeq Ldeclas
+%left	<tok.src>	Lload
+%left	<tok.src>	Loror
+%left	<tok.src>	Landand
+%right	<tok.src>	Lcons
+%left	<tok.src>	'|'
+%left	<tok.src>	'^'
+%left	<tok.src>	'&'
+%left	<tok.src>	Leq Lneq
+%left	<tok.src>	'<' '>' Lleq Lgeq
+%left	<tok.src>	Llsh Lrsh
+%left	<tok.src>	'+' '-'
+%left	<tok.src>	'*' '/' '%'
+%right <tok.src> Lexp
+%right	<tok.src>	Lcomm
+
+%left	<tok.src>	'(' ')' '[' ']' Linc Ldec Lof Lref
+%right	<tok.src>	Lif Lelse Lfn ':' Lexcept Lraises
+%left	<tok.src>	Lmdot
+%left	<tok.src>	'.'
+
+%left	<tok.src>	Lto
+%left	<tok.src>	Lor
+
+
+%nonassoc	<tok.v.rval>	Lrconst
+%nonassoc	<tok.v.ival>	Lconst
+%nonassoc	<tok.v.idval>	Lid Ltid Lsconst
+%nonassoc	<tok.src>	Llabs Lnil
+			'!' '~' Llen Lhd Ltl Ltagof
+			'{' '}' ';'
+			Limplement Limport Linclude
+			Lcon Ltype Lmodule Lcyclic
+			Ladt Larray Llist Lchan Lself
+			Ldo Lwhile Lfor Lbreak
+			Lalt Lcase Lpick Lcont
+			Lreturn Lexit Lspawn Lraise Lfix
+			Ldynamic
+%%
+prog	: Limplement ids ';'
+	{
+		impmods = $2;
+	} topdecls
+	{
+		tree = rotater($5);
+	}
+	| topdecls
+	{
+		impmods = nil;
+		tree = rotater($1);
+	}
+	;
+
+topdecls: topdecl
+	| topdecls topdecl
+	{
+		if($1 == nil)
+			$$ = $2;
+		else if($2 == nil)
+			$$ = $1;
+		else
+			$$ = mkbin(Oseq, $1, $2);
+	}
+	;
+
+topdecl	: error ';'
+	{
+		$$ = nil;
+	}
+	| decl
+	| fndef
+	| adtdecl ';'
+	| mdecl ';'
+	| idatom '=' exp ';'
+	{
+		$$ = mkbin(Oas, $1, $3);
+	}
+	| idterm '=' exp ';'
+	{
+		$$ = mkbin(Oas, $1, $3);
+	}
+	| idatom Ldeclas exp ';'
+	{
+		$$ = mkbin(Odas, $1, $3);
+	}
+	| idterm Ldeclas exp ';'
+	{
+		$$ = mkbin(Odas, $1, $3);
+	}
+	| idterms ':' type ';'
+	{
+		yyerror("illegal declaration");
+		$$ = nil;
+	}
+	| idterms ':' type '=' exp ';'
+	{
+		yyerror("illegal declaration");
+		$$ = nil;
+	}
+	;
+
+idterms : idterm
+	| idterms ',' idterm
+	{
+		$$ = mkbin(Oseq, $1, $3);
+	}
+	;
+
+decl	: Linclude Lsconst ';'
+	{
+		includef($2);
+		$$ = nil;
+	}
+	| ids ':' Ltype type ';'
+	{
+		$$ = typedecl($1, $4);
+	}
+	| ids ':' Limport exp ';'
+	{
+		$$ = importdecl($4, $1);
+		$$.src.start = $1.src.start;
+		$$.src.stop = $5.stop;
+	}
+	| ids ':' type ';'
+	{
+		$$ = vardecl($1, $3);
+	}
+	| ids ':' type '=' exp ';'
+	{
+		$$ = mkbin(Ovardecli, vardecl($1, $3), varinit($1, $5));
+	}
+	| ids ':' Lcon exp ';'
+	{
+		$$ = condecl($1, $4);
+	}
+	| edecl
+	;
+
+edecl	: ids ':' Lexcept ';'
+	{
+		$$ = exdecl($1, nil);
+	}
+	| ids ':' Lexcept '(' tuplist ')' ';'
+	{
+		$$ = exdecl($1, revids($5));
+	}
+	;
+
+mdecl	: ids ':' Lmodule '{' mfields '}'
+	{
+		$1.src.stop = $6.stop;
+		$$ = moddecl($1, rotater($5));
+	}
+	;
+
+mfields	:
+	{
+		$$ = nil;
+	}
+	| mfields mfield
+	{
+		if($1 == nil)
+			$$ = $2;
+		else if($2 == nil)
+			$$ = $1;
+		else
+			$$ = mkn(Oseq, $1, $2);
+	}
+	| error
+	{
+		$$ = nil;
+	}
+	;
+
+mfield	: ids ':' type ';'
+	{
+		$$ = fielddecl(Dglobal, typeids($1, $3));
+	}
+	| adtdecl ';'
+	| ids ':' Ltype type ';'
+	{
+		$$ = typedecl($1, $4);
+	}
+	| ids ':' Lcon exp ';'
+	{
+		$$ = condecl($1, $4);
+	}
+	| edecl
+	;
+
+adtdecl	: ids ':' Ladt polydec '{' fields '}' forpoly
+	{
+		$1.src.stop = $7.stop;
+		$$ = adtdecl($1, rotater($6));
+		$$.ty.polys = $4;
+		$$.ty.val = rotater($8);
+	}
+	| ids ':' Ladt polydec Lfor '{' tpolys '}' '{' fields '}'
+	{
+		$1.src.stop = $11.stop;
+		$$ = adtdecl($1, rotater($10));
+		$$.ty.polys = $4;
+		$$.ty.val = rotater($7);
+	}
+	;
+
+forpoly	:
+	{
+		$$ = nil;
+	}
+	| Lfor '{' tpolys '}'
+	{
+		$$ = $3;
+	}
+	;
+
+fields	:
+	{
+		$$ = nil;
+	}
+	| fields field
+	{
+		if($1 == nil)
+			$$ = $2;
+		else if($2 == nil)
+			$$ = $1;
+		else
+			$$ = mkn(Oseq, $1, $2);
+	}
+	| error
+	{
+		$$ = nil;
+	}
+	;
+
+field	: dfield
+	| pdecl
+	| ids ':' Lcon exp ';'
+	{
+		$$ = condecl($1, $4);
+	}
+	;
+
+dfields	:
+	{
+		$$ = nil;
+	}
+	| dfields dfield
+	{
+		if($1 == nil)
+			$$ = $2;
+		else if($2 == nil)
+			$$ = $1;
+		else
+			$$ = mkn(Oseq, $1, $2);
+	}
+	;
+
+dfield	: ids ':' Lcyclic type ';'
+	{
+		for(d := $1; d != nil; d = d.next)
+			d.cyc = byte 1;
+		$$ = fielddecl(Dfield, typeids($1, $4));
+	}
+	| ids ':' type ';'
+	{
+		$$ = fielddecl(Dfield, typeids($1, $3));
+	}
+	;
+
+pdecl	: Lpick '{' pfields '}'
+	{
+		$$ = $3;
+	}
+	;
+
+pfields	: pfbody dfields
+	{
+		$1.right.right = $2;
+		$$ = $1;
+	}
+	| pfbody error
+	{
+		$$ = nil;
+	}
+	| error
+	{
+		$$ = nil;
+	}
+	;
+
+pfbody	: ptags Llabs
+	{
+		$$ = mkn(Opickdecl, nil, mkn(Oseq, fielddecl(Dtag, $1), nil));
+		typeids($1, mktype($1.src.start, $1.src.stop, Tadtpick, nil, nil));
+	}
+	| pfbody dfields ptags Llabs
+	{
+		$1.right.right = $2;
+		$$ = mkn(Opickdecl, $1, mkn(Oseq, fielddecl(Dtag, $3), nil));
+		typeids($3, mktype($3.src.start, $3.src.stop, Tadtpick, nil, nil));
+	}
+	| pfbody error ptags Llabs
+	{
+		$$ = mkn(Opickdecl, nil, mkn(Oseq, fielddecl(Dtag, $3), nil));
+		typeids($3, mktype($3.src.start, $3.src.stop, Tadtpick, nil, nil));
+	}
+	;
+
+ptags	: rptags
+	{
+		$$ = revids($1);
+	}
+	;
+
+rptags	: Lid
+	{
+		$$ = mkids($<tok.src>1, $1, nil, nil);
+	}
+	| rptags Lor Lid
+	{
+		$$ = mkids($<tok.src>3, $3, nil, $1);
+	}
+	;
+
+ids	: rids
+	{
+		$$ = revids($1);
+	}
+	;
+
+rids	: Lid
+	{
+		$$ = mkids($<tok.src>1, $1, nil, nil);
+	}
+	| rids ',' Lid
+	{
+		$$ = mkids($<tok.src>3, $3, nil, $1);
+	}
+	;
+
+fixtype	: Lfix '(' exp ',' exp ')'
+	{
+		$$ = mktype($1.start, $6.stop, Tfix, nil, nil);
+		$$.val = mkbin(Oseq, $3, $5);
+	}
+	|	Lfix '(' exp ')'
+	{
+		$$ = mktype($1.start, $4.stop, Tfix, nil, nil);
+		$$.val = $3;
+	}
+	;
+
+types	: type
+	{
+		$$ = addtype($1, nil);
+	}
+	|	Lcyclic type
+	{
+		$$ = addtype($2, nil);
+		$2.flags |= CYCLIC;
+	}
+	| types ',' type
+	{
+		$$ = addtype($3, $1);
+	}
+	| types ',' Lcyclic type
+	{
+		$$ = addtype($4, $1);
+		$4.flags |= CYCLIC;
+	}
+	;
+
+type	: Ltid
+	{
+		$$ = mkidtype($<tok.src>1, $1);
+	}
+	| iditype
+	{
+		$$ = $1;
+	}
+	| dotiditype
+	{
+		$$ = $1;
+	}
+	| type Lmdot Lid
+	{
+		$$ = mkarrowtype($1.src.start, $<tok.src>3.stop, $1, $3);
+	}
+	| type Lmdot Lid '[' types ']'
+	{
+		$$ = mkarrowtype($1.src.start, $<tok.src>3.stop, $1, $3);
+		$$ = mkinsttype($1.src, $$, $5);
+	}
+	| Lref type
+	{
+		$$ = mktype($1.start, $2.src.stop, Tref, $2, nil);
+	}
+	| Lchan Lof type
+	{
+		$$ = mktype($1.start, $3.src.stop, Tchan, $3, nil);
+	}
+	| '(' tuplist ')'
+	{
+		if($2.next == nil)
+			$$ = $2.ty;
+		else
+			$$ = mktype($1.start, $3.stop, Ttuple, nil, revids($2));
+	}
+	| Larray Lof type
+	{
+		$$ = mktype($1.start, $3.src.stop, Tarray, $3, nil);
+	}
+	| Llist Lof type
+	{
+		$$ = mktype($1.start, $3.src.stop, Tlist, $3, nil);
+	}
+	| Lfn polydec fnargretp raises
+	{
+		$3.src.start = $1.start;
+		$3.polys = $2;
+		$3.eraises = $4;
+		$$ = $3;
+	}
+	| fixtype
+#	| Lexcept
+#	{
+#		$$ = mktype($1.start, $1.stop, Texception, nil, nil);
+#		$$.cons = byte 1;
+#	}
+#	| Lexcept '(' tuplist ')'
+#	{
+#		$$ = mktype($1.start, $4.stop, Texception, nil, revids($3));
+#		$$.cons = byte 1;
+#	}
+	;
+
+iditype	: Lid
+	{
+		$$ = mkidtype($<tok.src>1, $1);
+	}
+	| Lid '[' types ']'
+	{
+		$$ = mkinsttype($<tok.src>1, mkidtype($<tok.src>1, $1), $3);
+	}
+	;
+
+dotiditype	: type '.' Lid
+	{
+		$$ = mkdottype($1.src.start, $<tok.src>3.stop, $1, $3);
+	}
+	| type '.' Lid '[' types ']'
+	{
+		$$ = mkdottype($1.src.start, $<tok.src>3.stop, $1, $3);
+		$$ = mkinsttype($1.src, $$, $5);
+	}
+	;
+
+tuplist	: type
+	{
+		$$ = mkids($1.src, nil, $1, nil);
+	}
+	| tuplist ',' type
+	{
+		$$ = mkids($1.src, nil, $3, $1);
+	}
+	;
+
+polydec	:
+	{
+		$$ = nil;
+	}
+	|	'[' ids ']'
+	{
+		$$ = polydecl($2);
+	}
+	;
+
+fnarg	: '(' forms ')'
+	{
+		$$ = mktype($1.start, $3.stop, Tfn, tnone, $2);
+	}
+	| '(' '*' ')'
+	{
+		$$ = mktype($1.start, $3.stop, Tfn, tnone, nil);
+		$$.varargs = byte 1;
+	}
+	| '(' ftypes ',' '*' ')'
+	{
+		$$ = mktype($1.start, $5.stop, Tfn, tnone, $2);
+		$$.varargs = byte 1;
+	}
+	;
+
+fnargret: fnarg %prec ':'
+	{
+		$$ = $1;
+	}
+	| fnarg ':' type
+	{
+		$1.tof = $3;
+		$1.src.stop = $3.src.stop;
+		$$ = $1;
+	}
+	;
+
+fnargretp:	fnargret %prec '='
+	{
+		$$ = $1;
+	}
+	| fnargret Lfor '{' tpolys '}'
+	{
+		$$ = $1;
+		$$.val = rotater($4);
+	}
+	;
+
+forms	:
+	{
+		$$ = nil;
+	}
+	| ftypes
+	;
+
+ftypes	: ftype
+	| ftypes ',' ftype
+	{
+		$$ = appdecls($1, $3);
+	}
+	;
+
+ftype	: nids ':' type
+	{
+		$$ = typeids($1, $3);
+	}
+	| nids ':' adtk
+	{
+		$$ = typeids($1, $3);
+		for(d := $$; d != nil; d = d.next)
+			d.implicit = byte 1;
+	}
+	| idterms ':' type
+	{
+		$$ = mkids($1.src, enter("junk", 0), $3, nil);
+		$$.store = Darg;
+		yyerror("illegal argument declaraion");
+	}
+	| idterms ':' adtk
+	{
+		$$ = mkids($1.src, enter("junk", 0), $3, nil);
+		$$.store = Darg;
+		yyerror("illegal argument declaraion");
+	}
+	;
+
+nids	: nrids
+	{
+		$$ = revids($1);
+	}
+	;
+
+nrids	: Lid
+	{
+		$$ = mkids($<tok.src>1, $1, nil, nil);
+		$$.store = Darg;
+	}
+	| Lnil
+	{
+		$$ = mkids($1, nil, nil, nil);
+		$$.store = Darg;
+	}
+	| nrids ',' Lid
+	{
+		$$ = mkids($<tok.src>3, $3, nil, $1);
+		$$.store = Darg;
+	}
+	| nrids ',' Lnil
+	{
+		$$ = mkids($3, nil, nil, $1);
+		$$.store = Darg;
+	}
+	;
+
+adtk	: Lself iditype
+	{
+		$$ = $2;
+	}
+	| Lself Lref iditype
+	{
+		$$ = mktype($<tok.src>2.start, $<tok.src>3.stop, Tref, $3, nil);
+	}
+	| Lself dotiditype
+	{
+		$$ = $2;
+	}
+	| Lself Lref dotiditype
+	{
+		$$ = mktype($<tok.src>2.start, $<tok.src>3.stop, Tref, $3, nil);
+	}
+	;
+
+fndef	: fnname fnargretp raises fbody
+	{
+		$$ = fndecl($1, $2, $4);
+		nfns++;
+		# patch up polydecs
+		if($1.op == Odot){
+			if($1.right.left != nil){
+				$2.polys = $1.right.left.decl;
+				$1.right.left = nil;
+			}
+			if($1.left.op == Oname && $1.left.left != nil){
+				$$.decl = $1.left.left.decl;
+				$1.left.left = nil;
+			}
+		}
+		else{
+			if($1.left != nil){
+				$2.polys = $1.left.decl;
+				$1.left = nil;
+			}
+		}
+		$2.eraises = $3;
+		$$.src = $1.src;
+	}
+	;
+
+raises	: Lraises '(' idlist ')'
+	{
+		$$ = mkn(Otuple, rotater($3), nil);
+		$$.src.start = $1.start;
+		$$.src.stop = $4.stop;
+	}
+	|	Lraises idatom
+	{
+		$$ = mkn(Otuple, mkunary(Oseq, $2), nil);
+		$$.src.start = $1.start;
+		$$.src.stop = $2.src.stop;
+	}
+	|	%prec Lraises
+	{
+		$$ = nil;
+	}
+	;
+
+fbody	: '{' stmts '}'
+	{
+		if($2 == nil){
+			$2 = mkn(Onothing, nil, nil);
+			$2.src.start = curline();
+			$2.src.stop = $2.src.start;
+		}
+		$$ = rotater($2);
+		$$.src.start = $1.start;
+		$$.src.stop = $3.stop;
+	}
+	| error '}'
+	{
+		$$ = mkn(Onothing, nil, nil);
+	}
+	| error '{' stmts '}'
+	{
+		$$ = mkn(Onothing, nil, nil);
+	}
+	;
+
+fnname	: Lid polydec
+	{
+		$$ = mkname($<tok.src>1, $1);
+		if($2 != nil){
+			$$.left = mkn(Onothing, nil ,nil);
+			$$.left.decl = $2;
+		}
+	}
+	| fnname '.' Lid polydec
+	{
+		$$ = mkbin(Odot, $1, mkname($<tok.src>3, $3));
+		if($4 != nil){
+			$$.right.left = mkn(Onothing, nil ,nil);
+			$$.right.left.decl = $4;
+		}
+	}
+	;
+
+stmts	:
+	{
+		$$ = nil;
+	}
+	| stmts decl
+	{
+		if($1 == nil)
+			$$ = $2;
+		else if($2 == nil)
+			$$ = $1;
+		else
+			$$ = mkbin(Oseq, $1, $2);
+	}
+	| stmts stmt
+	{
+		if($1 == nil)
+			$$ = $2;
+		else
+			$$ = mkbin(Oseq, $1, $2);
+	}
+	;
+
+elists	: '(' elist ')'
+	| elists ',' '(' elist ')'
+	;
+
+stmt	: error ';'
+	{
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = curline();
+		$$.src.stop = $$.src.start;
+	}
+	| error '}'
+	{
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = curline();
+		$$.src.stop = $$.src.start;
+	}
+	| error '{' stmts '}'
+	{
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = curline();
+		$$.src.stop = $$.src.start;
+	}
+	| '{' stmts '}'
+	{
+		if($2 == nil){
+			$2 = mkn(Onothing, nil, nil);
+			$2.src.start = curline();
+			$2.src.stop = $2.src.start;
+		}
+		$$ = mkscope(rotater($2));
+	}
+	| elists ':' type ';'
+	{
+		yyerror("illegal declaration");
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = curline();
+		$$.src.stop = $$.src.start;
+	}
+	| elists ':' type '=' exp';'
+	{
+		yyerror("illegal declaration");
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = curline();
+		$$.src.stop = $$.src.start;
+	}
+	| zexp ';'
+	{
+		$$ = $1;
+	}
+	| Lif '(' exp ')' stmt
+	{
+		$$ = mkn(Oif, $3, mkunary(Oseq, $5));
+		$$.src.start = $1.start;
+		$$.src.stop = $5.src.stop;
+	}
+	| Lif '(' exp ')' stmt Lelse stmt
+	{
+		$$ = mkn(Oif, $3, mkbin(Oseq, $5, $7));
+		$$.src.start = $1.start;
+		$$.src.stop = $7.src.stop;
+	}
+	| bclab Lfor '(' zexp ';' zexp ';' zexp ')' stmt
+	{
+		$$ = mkunary(Oseq, $10);
+		if($8.op != Onothing)
+			$$.right = $8;
+		$$ = mkbin(Ofor, $6, $$);
+		$$.decl = $1;
+		if($4.op != Onothing)
+			$$ = mkbin(Oseq, $4, $$);
+	}
+	| bclab Lwhile '(' zexp ')' stmt
+	{
+		$$ = mkn(Ofor, $4, mkunary(Oseq, $6));
+		$$.src.start = $2.start;
+		$$.src.stop = $6.src.stop;
+		$$.decl = $1;
+	}
+	| bclab Ldo stmt Lwhile '(' zexp ')' ';'
+	{
+		$$ = mkn(Odo, $6, $3);
+		$$.src.start = $2.start;
+		$$.src.stop = $7.stop;
+		$$.decl = $1;
+	}
+	| Lbreak bctarg ';'
+	{
+		$$ = mkn(Obreak, nil, nil);
+		$$.decl = $2;
+		$$.src = $1;
+	}
+	| Lcont bctarg ';'
+	{
+		$$ = mkn(Ocont, nil, nil);
+		$$.decl = $2;
+		$$.src = $1;
+	}
+	| Lreturn zexp ';'
+	{
+		$$ = mkn(Oret, $2, nil);
+		$$.src = $1;
+		if($2.op == Onothing)
+			$$.left = nil;
+		else
+			$$.src.stop = $2.src.stop;
+	}
+	| Lspawn exp ';'
+	{
+		$$ = mkn(Ospawn, $2, nil);
+		$$.src.start = $1.start;
+		$$.src.stop = $2.src.stop;
+	}
+	| Lraise zexp ';'
+	{
+		$$ = mkn(Oraise, $2, nil);
+		$$.src.start = $1.start;
+		$$.src.stop = $2.src.stop;
+	}
+	| bclab Lcase exp '{' cqstmts '}'
+	{
+		$$ = mkn(Ocase, $3, caselist($5, nil));
+		$$.src = $3.src;
+		$$.decl = $1;
+	}
+	| bclab Lalt '{' qstmts '}'
+	{
+		$$ = mkn(Oalt, caselist($4, nil), nil);
+		$$.src = $2;
+		$$.decl = $1;
+	}
+	| bclab Lpick Lid Ldeclas exp '{' pstmts '}'
+	{
+		$$ = mkn(Opick, mkbin(Odas, mkname($<tok.src>3, $3), $5), caselist($7, nil));
+		$$.src.start = $<tok.src>3.start;
+		$$.src.stop = $5.src.stop;
+		$$.decl = $1;
+	}
+	| Lexit ';'
+	{
+		$$ = mkn(Oexit, nil, nil);
+		$$.src = $1;
+	}
+	| '{' stmts '}' Lexcept idexc '{' eqstmts '}'
+	{
+		if($2 == nil){
+			$2 = mkn(Onothing, nil, nil);
+			$2.src.start = $2.src.stop = curline();
+		}
+		$2 = mkscope(rotater($2));
+		$$ = mkbin(Oexstmt, $2, mkn(Oexcept, $5, caselist($7, nil)));
+	}
+#	| stmt Lexcept idexc '{' eqstmts '}'
+#	{
+#		$$ = mkbin(Oexstmt, $1, mkn(Oexcept, $3, caselist($5, nil)));
+#	}
+	;
+
+bclab	:
+	{
+		$$ = nil;
+	}
+	| ids ':'
+	{
+		if($1.next != nil)
+			yyerror("only one identifier allowed in a label");
+		$$ = $1;
+	}
+	;
+
+bctarg	:
+	{
+		$$ = nil;
+	}
+	| Lid
+	{
+		$$ = mkids($<tok.src>1, $1, nil, nil);
+	}
+	;
+
+qstmts	: qbodies stmts
+	{
+		$1.left.right.right = $2;
+		$$ = $1;
+	}
+	;
+
+qbodies	: qual Llabs
+	{
+		$$ = mkunary(Oseq, mkscope(mkunary(Olabel, rotater($1))));
+	}
+	| qbodies stmts qual Llabs
+	{
+		$1.left.right.right = $2;
+		$$ = mkbin(Oseq, mkscope(mkunary(Olabel, rotater($3))), $1);
+	}
+	;
+
+cqstmts	: cqbodies stmts
+	{
+		$1.left.right = mkscope($2);
+		$$ = $1;
+	}
+	;
+
+cqbodies	: qual Llabs
+	{
+		$$ = mkunary(Oseq, mkunary(Olabel, rotater($1)));
+	}
+	| cqbodies stmts qual Llabs
+	{
+		$1.left.right = mkscope($2);
+		$$ = mkbin(Oseq, mkunary(Olabel, rotater($3)), $1);
+	}
+	;
+
+eqstmts	: eqbodies stmts
+	{
+		$1.left.right = mkscope($2);
+		$$ = $1;
+	}
+	;
+
+eqbodies	: qual Llabs
+	{
+		$$ = mkunary(Oseq, mkunary(Olabel, rotater($1)));
+	}
+	| eqbodies stmts qual Llabs
+	{
+		$1.left.right = mkscope($2);
+		$$ = mkbin(Oseq, mkunary(Olabel, rotater($3)), $1);
+	}
+	;
+
+qual	: exp
+	| exp Lto exp
+	{
+		$$ = mkbin(Orange, $1, $3);
+	}
+	| '*'
+	{
+		$$ = mkn(Owild, nil, nil);
+		$$.src = $1;
+	}
+	| qual Lor qual
+	{
+		$$ = mkbin(Oseq, $1, $3);
+	}
+	| error
+	{
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = curline();
+		$$.src.stop = $$.src.start;
+	}
+	;
+
+pstmts	: pbodies stmts
+	{
+		$1.left.right = mkscope($2);
+		$$ = $1;
+	}
+	;
+
+pbodies	: pqual Llabs
+	{
+		$$ = mkunary(Oseq, mkunary(Olabel, rotater($1)));
+	}
+	| pbodies stmts pqual Llabs
+	{
+		$1.left.right = mkscope($2);
+		$$ = mkbin(Oseq, mkunary(Olabel, rotater($3)), $1);
+	}
+	;
+
+pqual	: Lid
+	{
+		$$ = mkname($<tok>1.src, $1);
+	}
+	| '*'
+	{
+		$$ = mkn(Owild, nil, nil);
+		$$.src = $1;
+	}
+	| pqual Lor pqual
+	{
+		$$ = mkbin(Oseq, $1, $3);
+	}
+	| error
+	{
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = curline();
+		$$.src.stop = $$.src.start;
+	}
+	;
+
+zexp	:
+	{
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = curline();
+		$$.src.stop = $$.src.start;
+	}
+	| exp
+	;
+
+exp	: monexp
+	| exp '=' exp
+	{
+		$$ = mkbin(Oas, $1, $3);
+	}
+	| exp Landeq exp
+	{
+		$$ = mkbin(Oandas, $1, $3);
+	}
+	| exp Loreq exp
+	{
+		$$ = mkbin(Ooras, $1, $3);
+	}
+	| exp Lxoreq exp
+	{
+		$$ = mkbin(Oxoras, $1, $3);
+	}
+	| exp Llsheq exp
+	{
+		$$ = mkbin(Olshas, $1, $3);
+	}
+	| exp Lrsheq exp
+	{
+		$$ = mkbin(Orshas, $1, $3);
+	}
+	| exp Laddeq exp
+	{
+		$$ = mkbin(Oaddas, $1, $3);
+	}
+	| exp Lsubeq exp
+	{
+		$$ = mkbin(Osubas, $1, $3);
+	}
+	| exp Lmuleq exp
+	{
+		$$ = mkbin(Omulas, $1, $3);
+	}
+	| exp Ldiveq exp
+	{
+		$$ = mkbin(Odivas, $1, $3);
+	}
+	| exp Lmodeq exp
+	{
+		$$ = mkbin(Omodas, $1, $3);
+	}
+	| exp Lexpeq exp
+	{
+		$$ = mkbin(Oexpas, $1, $3);
+	}
+	| exp Lcomm '=' exp
+	{
+		$$ = mkbin(Osnd, $1, $4);
+	}
+	| exp Ldeclas exp
+	{
+		$$ = mkbin(Odas, $1, $3);
+	}
+	| Lload Lid exp %prec Lload
+	{
+		$$ = mkn(Oload, $3, nil);
+		$$.src.start = $<tok.src.start>1;
+		$$.src.stop = $3.src.stop;
+		$$.ty = mkidtype($<tok.src>2, $2);
+	}
+	| exp Lexp exp
+	{
+		$$ = $$ = mkbin(Oexp, $1, $3);
+	}
+	| exp '*' exp
+	{
+		$$ = mkbin(Omul, $1, $3);
+	}
+	| exp '/' exp
+	{
+		$$ = mkbin(Odiv, $1, $3);
+	}
+	| exp '%' exp
+	{
+		$$ = mkbin(Omod, $1, $3);
+	}
+	| exp '+' exp
+	{
+		$$ = mkbin(Oadd, $1, $3);
+	}
+	| exp '-' exp
+	{
+		$$ = mkbin(Osub, $1, $3);
+	}
+	| exp Lrsh exp
+	{
+		$$ = mkbin(Orsh, $1, $3);
+	}
+	| exp Llsh exp
+	{
+		$$ = mkbin(Olsh, $1, $3);
+	}
+	| exp '<' exp
+	{
+		$$ = mkbin(Olt, $1, $3);
+	}
+	| exp '>' exp
+	{
+		$$ = mkbin(Ogt, $1, $3);
+	}
+	| exp Lleq exp
+	{
+		$$ = mkbin(Oleq, $1, $3);
+	}
+	| exp Lgeq exp
+	{
+		$$ = mkbin(Ogeq, $1, $3);
+	}
+	| exp Leq exp
+	{
+		$$ = mkbin(Oeq, $1, $3);
+	}
+	| exp Lneq exp
+	{
+		$$ = mkbin(Oneq, $1, $3);
+	}
+	| exp '&' exp
+	{
+		$$ = mkbin(Oand, $1, $3);
+	}
+	| exp '^' exp
+	{
+		$$ = mkbin(Oxor, $1, $3);
+	}
+	| exp '|' exp
+	{
+		$$ = mkbin(Oor, $1, $3);
+	}
+	| exp Lcons exp
+	{
+		$$ = mkbin(Ocons, $1, $3);
+	}
+	| exp Landand exp
+	{
+		$$ = mkbin(Oandand, $1, $3);
+	}
+	| exp Loror exp
+	{
+		$$ = mkbin(Ooror, $1, $3);
+	}
+	;
+
+monexp	: term
+	| '+' monexp
+	{
+		$2.src.start = $1.start;
+		$$ = $2;
+	}
+	| '-' monexp
+	{
+		$$ = mkunary(Oneg, $2);
+		$$.src.start = $1.start;
+	}
+	| '!' monexp
+	{
+		$$ = mkunary(Onot, $2);
+		$$.src.start = $1.start;
+	}
+	| '~' monexp
+	{
+		$$ = mkunary(Ocomp, $2);
+		$$.src.start = $1.start;
+	}
+	| '*' monexp
+	{
+		$$ = mkunary(Oind, $2);
+		$$.src.start = $1.start;
+	}
+	| Linc monexp
+	{
+		$$ = mkunary(Opreinc, $2);
+		$$.src.start = $1.start;
+	}
+	| Ldec monexp
+	{
+		$$ = mkunary(Opredec, $2);
+		$$.src.start = $1.start;
+	}
+	| Lcomm monexp
+	{
+		$$ = mkunary(Orcv, $2);
+		$$.src.start = $1.start;
+	}
+	| Lhd monexp
+	{
+		$$ = mkunary(Ohd, $2);
+		$$.src.start = $1.start;
+	}
+	| Ltl monexp
+	{
+		$$ = mkunary(Otl, $2);
+		$$.src.start = $1.start;
+	}
+	| Llen monexp
+	{
+		$$ = mkunary(Olen, $2);
+		$$.src.start = $1.start;
+	}
+	| Lref monexp
+	{
+		$$ = mkunary(Oref, $2);
+		$$.src.start = $1.start;
+	}
+	| Ltagof monexp
+	{
+		$$ = mkunary(Otagof, $2);
+		$$.src.start = $1.start;
+	}
+	| Larray '[' exp ']' Lof type
+	{
+		$$ = mkn(Oarray, $3, nil);
+		$$.ty = mktype($1.start, $6.src.stop, Tarray, $6, nil);
+		$$.src = $$.ty.src;
+	}
+	| Larray '[' exp ']' Lof '{' initlist '}'
+	{
+		$$ = mkn(Oarray, $3, $7);
+		$$.src.start = $1.start;
+		$$.src.stop = $8.stop;
+	}
+	| Larray '[' ']' Lof '{' initlist '}'
+	{
+		$$ = mkn(Onothing, nil, nil);
+		$$.src.start = $2.start;
+		$$.src.stop = $3.stop;
+		$$ = mkn(Oarray, $$, $6);
+		$$.src.start = $1.start;
+		$$.src.stop = $7.stop;
+	}
+	| Llist Lof '{' celist '}'
+	{
+		$$ = etolist($4);
+		$$.src.start = $1.start;
+		$$.src.stop = $5.stop;
+	}
+	| Lchan Lof type
+	{
+		$$ = mkn(Ochan, nil, nil);
+		$$.ty = mktype($1.start, $3.src.stop, Tchan, $3, nil);
+		$$.src = $$.ty.src;
+	}
+	| Lchan '[' exp ']' Lof type
+	{
+		$$ = mkn(Ochan, $3, nil);
+		$$.ty = mktype($1.start, $6.src.stop, Tchan, $6, nil);
+		$$.src = $$.ty.src;
+	}
+	| Larray Lof Ltid monexp
+	{
+		$$ = mkunary(Ocast, $4);
+		$$.ty = mktype($1.start, $4.src.stop, Tarray, mkidtype($<tok.src>3, $3), nil);
+		$$.src = $$.ty.src;
+	}
+	| Ltid monexp
+	{
+		$$ = mkunary(Ocast, $2);
+		$$.src.start = $<tok.src>1.start;
+		$$.ty = mkidtype($$.src, $1);
+	}
+	| Lid monexp
+	{
+		$$ = mkunary(Ocast, $2);
+		$$.src.start = $<tok.src>1.start;
+		$$.ty = mkidtype($$.src, $1);
+	}
+	| fixtype monexp
+	{
+		$$ = mkunary(Ocast, $2);
+		$$.src.start = $<tok.src>1.start;
+		$$.ty = $1;
+	}
+	;
+
+term	: idatom
+	| term '(' zelist ')'
+	{
+		$$ = mkn(Ocall, $1, $3);
+		$$.src.start = $1.src.start;
+		$$.src.stop = $4.stop;
+	}
+	| '(' elist ')'
+	{
+		$$ = $2;
+		if($2.op == Oseq)
+			$$ = mkn(Otuple, rotater($2), nil);
+		else
+			$$.flags |= byte PARENS;
+		$$.src.start = $1.start;
+		$$.src.stop = $3.stop;
+	}
+	| Lfn fnargret
+	{
+#		n := mkdeclname($1, mkids($1, enter(".fn"+string nfnexp++, 0), nil, nil));
+#		$<node>$ = fndef(n, $2);
+#		nfns++;
+	} fbody
+	{
+#		$$ = fnfinishdef($<node>3, $4);
+#		$$ = mkdeclname($1, $$.left.decl);
+		yyerror("urt unk");
+		$$ = nil;
+	}
+	| term '.' Lid
+	{
+		$$ = mkbin(Odot, $1, mkname($<tok.src>3, $3));
+	}
+	| term Lmdot term
+	{
+		$$ = mkbin(Omdot, $1, $3);
+	}
+	| term '[' export ']'
+	{
+		$$ = mkbin(Oindex, $1, $3);
+		$$.src.stop = $4.stop;
+	}
+	| term '[' zexp ':' zexp ']'
+	{
+		if($3.op == Onothing)
+			$3.src = $4;
+		if($5.op == Onothing)
+			$5.src = $4;
+		$$ = mkbin(Oslice, $1, mkbin(Oseq, $3, $5));
+		$$.src.stop = $6.stop;
+	}
+	| term Linc
+	{
+		$$ = mkunary(Oinc, $1);
+		$$.src.stop = $2.stop;
+	}
+	| term Ldec
+	{
+		$$ = mkunary(Odec, $1);
+		$$.src.stop = $2.stop;
+	}
+	| Lsconst
+	{
+		$$ = mksconst($<tok.src>1, $1);
+	}
+	| Lconst
+	{
+		$$ = mkconst($<tok.src>1, $1);
+		if($1 > big 16r7fffffff || $1 < big -16r7fffffff)
+			$$.ty = tbig;
+	}
+	| Lrconst
+	{
+		$$ = mkrconst($<tok.src>1, $1);
+	}
+	| term '[' exportlist ',' export ']'
+	{
+		$$ = mkbin(Oindex, $1, rotater(mkbin(Oseq, $3, $5)));
+		$$.src.stop = $6.stop;
+	}
+	;
+
+idatom	: Lid
+	{
+		$$ = mkname($<tok.src>1, $1);
+	}
+	| Lnil
+	{
+		$$ = mknil($<tok.src>1);
+	}
+	;
+
+idterm	: '(' idlist ')'
+	{
+		$$ = mkn(Otuple, rotater($2), nil);
+		$$.src.start = $1.start;
+		$$.src.stop = $3.stop;
+	}
+	;
+
+exportlist	: export
+	| exportlist ',' export
+	{
+		$$ = mkbin(Oseq, $1, $3);
+	}
+	;
+
+export	: exp
+	|	texp
+	;
+
+texp	: Ltid
+	{
+		$$ = mkn(Otype, nil, nil);
+		$$.ty = mkidtype($<tok.src>1, $1);
+		$$.src = $$.ty.src;
+	}
+	| Larray Lof type
+	{
+		$$ = mkn(Otype, nil, nil);
+		$$.ty = mktype($1.start, $3.src.stop, Tarray, $3, nil);
+		$$.src = $$.ty.src;
+	}
+	| Llist Lof type
+	{
+		$$ = mkn(Otype, nil, nil);
+		$$.ty = mktype($1.start, $3.src.stop, Tlist, $3, nil);
+		$$.src = $$.ty.src;
+	}
+	| Lcyclic type
+	{
+		$$ = mkn(Otype, nil ,nil);
+		$$.ty = $2;
+		$$.ty.flags |= CYCLIC;
+		$$.src = $$.ty.src;
+	}
+	;
+
+idexc	: Lid
+	{
+		$$ = mkname($<tok.src>1, $1);
+	}
+	|	# empty
+	{
+		$$ = nil;
+	}
+	;
+
+idlist	: idterm
+	| idatom
+	| idlist ',' idterm
+	{
+		$$ = mkbin(Oseq, $1, $3);
+	}
+	| idlist ',' idatom
+	{
+		$$ = mkbin(Oseq, $1, $3);
+	}
+	;
+
+zelist	:
+	{
+		$$ = nil;
+	}
+	| elist
+	{
+		$$ = rotater($1);
+	}
+	;
+
+celist	: elist
+	| elist ','
+	;
+
+elist	: exp
+	| elist ',' exp
+	{
+		$$ = mkbin(Oseq, $1, $3);
+	}
+	;
+
+initlist	: elemlist
+	{
+		$$ = rotater($1);
+	}
+	| elemlist ','
+	{
+		$$ = rotater($1);
+	}
+	;
+
+elemlist	: elem
+	| elemlist ',' elem
+	{
+		$$ = mkbin(Oseq, $1, $3);
+	}
+	;
+
+elem	: exp
+	{
+		$$ = mkn(Oelem, nil, $1);
+		$$.src = $1.src;
+	}
+	| qual Llabs exp
+	{
+		$$ = mkbin(Oelem, rotater($1), $3);
+	}
+	;
+
+tpolys	: tpoly dfields
+	{
+		if($1.op == Oseq)
+			$1.right.left = rotater($2);
+		else
+			$1.left = rotater($2);
+		$$ = $1;
+	}
+	;
+
+tpoly	: ids Llabs
+	{
+		$$ = typedecl($1, mktype($1.src.start, $2.stop, Tpoly, nil, nil));
+	}
+	| tpoly dfields ids Llabs
+	{
+		if($1.op == Oseq)
+			$1.right.left = rotater($2);
+		else
+			$1.left = rotater($2);
+		$$ = mkbin(Oseq, $1, typedecl($3, mktype($3.src.start, $4.stop, Tpoly, nil, nil)));
+	}
+	;
+
+%%
+
+include "ipints.m";
+include "crypt.m";
+
+sys:	Sys;
+	print, fprint, sprint: import sys;
+
+bufio:	Bufio;
+	Iobuf: import bufio;
+
+str:		String;
+
+crypt:Crypt;
+	md5: import crypt;
+
+math:	Math;
+	import_real, export_real, isnan: import math;
+
+yyctxt: ref YYLEX;
+
+canonnan: real;
+
+debug	= array[256] of {* => 0};
+
+noline	= -1;
+nosrc	= Src(-1, -1);
+
+infile:	string;
+
+# front end
+include "arg.m";
+include "lex.b";
+include "types.b";
+include "nodes.b";
+include "decls.b";
+
+include "typecheck.b";
+
+# back end
+include "gen.b";
+include "ecom.b";
+include "asm.b";
+include "dis.b";
+include "sbl.b";
+include "stubs.b";
+include "com.b";
+include "optim.b";
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	s: string;
+
+	sys = load Sys Sys->PATH;
+	crypt = load Crypt Crypt->PATH;
+	math = load Math Math->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil){
+		sys->print("can't load %s: %r\n", Bufio->PATH);
+		raise("fail:bad module");
+	}
+	str = load String String->PATH;
+	if(str == nil){
+		sys->print("can't load %s: %r\n", String->PATH);
+		raise("fail:bad module");
+	}
+
+	stderr = sys->fildes(2);
+	yyctxt = ref YYLEX;
+
+	math->FPcontrol(0, Math->INVAL|Math->ZDIV|Math->OVFL|Math->UNFL|Math->INEX);
+	na := array[1] of {0.};
+	import_real(array[8] of {byte 16r7f, * => byte 16rff}, na);
+	canonnan = na[0];
+	if(!isnan(canonnan))
+		fatal("bad canonical NaN");
+
+	lexinit();
+	typeinit();
+	optabinit();
+
+	gendis = 1;
+	asmsym = 0;
+	maxerr = 20;
+	ofile := "";
+	ext := "";
+
+	arg := Arg.init(argv);
+	while(c := arg.opt()){
+		case c{
+		'Y' =>
+			emitsbl = arg.arg();
+			if(emitsbl == nil)
+				usage();
+		'C' =>
+			dontcompile = 1;
+		'D' =>
+			#
+			# debug flags:
+			#
+			# a	alt compilation
+			# A	array constructor compilation
+			# b	boolean and branch compilation
+			# c	case compilation
+			# d	function declaration
+			# D	descriptor generation
+			# e	expression compilation
+			# E	addressable expression compilation
+			# f	print arguments for compiled functions
+			# F	constant folding
+			# g	print out globals
+			# m	module declaration and type checking
+			# n	nil references
+			# s	print sizes of output file sections
+			# S	type signing
+			# t	type checking function bodies
+			# T	timing
+			# v	global var and constant compilation
+			# x	adt verification
+			# Y	tuple compilation
+			# z Z	bug fixes
+			#
+			s = arg.arg();
+			for(i := 0; i < len s; i++){
+				c = s[i];
+				if(c < len debug)
+					debug[c] = 1;
+			}
+		'I' =>
+			s = arg.arg();
+			if(s == "")
+				usage();
+			addinclude(s);
+		'G' =>
+			asmsym = 1;
+		'S' =>
+			gendis = 0;
+		'a' =>
+			emitstub = 1;
+		'A' =>
+			emitstub = emitdyn = 1;
+		'c' =>
+			mustcompile = 1;
+		'e' =>
+			maxerr = 1000;
+		'f' =>
+			fabort = 1;
+		'F' =>
+			newfnptr = 1;
+		'g' =>
+			dosym = 1;
+		'i' =>
+			dontinline = 1;
+		'o' =>
+			ofile = arg.arg();
+		'O' =>
+			optims = 1;
+		's' =>
+			s = arg.arg();
+			if(s != nil)
+				fixss = int s;
+		't' =>
+			emittab = arg.arg();
+			if(emittab == nil)
+				usage();
+		'T' =>
+			emitcode = arg.arg();
+			if(emitcode == nil)
+				usage();
+		'd' =>
+			emitcode = arg.arg();
+			if(emitcode == nil)
+				usage();
+			emitdyn = 1;
+		'w' =>
+			superwarn = dowarn;
+			dowarn = 1;
+		'x' =>
+			ext = arg.arg();
+		'X' =>
+			signdump = arg.arg();
+		'z' =>
+			arrayz = 1;
+		'y' =>
+			oldcycles = 1;
+		* =>
+			usage();
+		}
+	}
+
+	addinclude("/module");
+
+	argv = arg.argv;
+	arg = nil;
+
+	if(argv == nil){
+		usage();
+	}else if(ofile != nil){
+		if(len argv != 1)
+			usage();
+		translate(hd argv, ofile, mkfileext(ofile, ".dis", ".sbl"));
+	}else{
+		pr := len argv != 1;
+		if(ext == ""){
+			ext = ".s";
+			if(gendis)
+				ext = ".dis";
+		}
+		for(; argv != nil; argv = tl argv){
+			file := hd argv;
+			(nil, s) = str->splitr(file, "/");
+			if(pr)
+				print("%s:\n", s);
+			out := mkfileext(s, ".b", ext);
+			translate(file, out, mkfileext(out, ext, ".sbl"));
+		}
+	}
+	if (toterrors > 0)
+		raise("fail:errors");
+}
+
+usage()
+{
+	fprint(stderr, "usage: limbo [-GSagwe] [-I incdir] [-o outfile] [-{T|t|d} module] [-D debug] file ...\n");
+	raise("fail:usage");
+}
+
+mkfileext(file, oldext, ext: string): string
+{
+	n := len file;
+	n2 := len oldext;
+	if(n >= n2 && file[n-n2:] == oldext)
+		file = file[:n-n2];
+	return file + ext;
+}
+
+translate(in, out, dbg: string)
+{
+	infile = in;
+	outfile = out;
+	errors = 0;
+	bins[0] = bufio->open(in, Bufio->OREAD);
+	if(bins[0] == nil){
+		fprint(stderr, "can't open %s: %r\n", in);
+		toterrors++;
+		return;
+	}
+	doemit := emitcode != "" || emitstub || emittab != "" || emitsbl != "";
+	if(!doemit){
+		bout = bufio->create(out, Bufio->OWRITE, 8r666);
+		if(bout == nil){
+			fprint(stderr, "can't open %s: %r\n", out);
+			toterrors++;
+			bins[0].close();
+			return;
+		}
+		if(dosym){
+			bsym = bufio->create(dbg, Bufio->OWRITE, 8r666);
+			if(bsym == nil)
+				fprint(stderr, "can't open %s: %r\n", dbg);
+		}
+	}
+
+	lexstart(in);
+
+	popscopes();
+	typestart();
+	declstart();
+	nfnexp = 0;
+
+	parset = sys->millisec();
+	yyparse(yyctxt);
+	parset = sys->millisec() - parset;
+
+	checkt = sys->millisec();
+	entry := typecheck(!doemit);
+	checkt = sys->millisec() - checkt;
+
+	modcom(entry);
+
+	fns = nil;
+	nfns = 0;
+	descriptors = nil;
+
+	if(debug['T'])
+		print("times: parse=%d type=%d: gen=%d write=%d symbols=%d\n",
+			parset, checkt, gent, writet, symt);
+
+	if(bout != nil)
+		bout.close();
+	if(bsym != nil)
+		bsym.close();
+	toterrors += errors;
+	if(errors && bout != nil)
+		sys->remove(out);
+	if(errors && bsym != nil)
+		sys->remove(dbg);
+}
+
+pwd(): string
+{
+	workdir := load Workdir Workdir->PATH;
+	if(workdir == nil)
+		cd := "/";
+	else
+		cd = workdir->init();
+	# sys->print("pwd: %s\n", cd);
+	return cd;
+}
+
+cleanname(s: string): string
+{
+	ls, path: list of string;
+
+	if(s == nil)
+		return nil;
+	if(s[0] != '/' && s[0] != '\\')
+		(nil, ls) = sys->tokenize(pwd(), "/\\");
+	for( ; ls != nil; ls = tl ls)
+		path = hd ls :: path;
+	(nil, ls) = sys->tokenize(s, "/\\");
+	for( ; ls != nil; ls = tl ls){
+		n := hd ls;
+		if(n == ".")
+			;
+		else if (n == ".."){
+			if(path != nil)
+				path = tl path;
+		}
+		else
+			path = n :: path;
+	}
+	p := "";
+	for( ; path != nil; path = tl path)
+		p = "/" + hd path + p;
+	if(p == nil)
+		p = "/";
+	# sys->print("cleanname: %s\n", p);
+	return p;
+}
+
+srcpath(): string
+{
+	srcp := cleanname(infile);
+	# sys->print("srcpath: %s\n", srcp);
+	return srcp;
+}
--- /dev/null
+++ b/appl/cmd/limbo/mkfile
@@ -1,0 +1,35 @@
+<../../../mkconfig
+
+TARG=	limbo.dis\
+
+MODULES=\
+	arg.m\
+	disoptab.m\
+	isa.m\
+	limbo.m\
+	opname.m\
+	asm.b\
+	com.b\
+	decls.b\
+	dis.b\
+	ecom.b\
+	gen.b\
+	lex.b\
+	nodes.b\
+	optim.b\
+	sbl.b\
+	stubs.b\
+	typecheck.b\
+	types.b\
+
+SYSMODULES= \
+	bufio.m\
+	draw.m\
+	keyring.m\
+	math.m\
+	string.m\
+	sys.m\
+
+DISBIN=$ROOT/dis
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/limbo/nodes.b
@@ -1,0 +1,1402 @@
+include "opname.m";
+
+znode:	Node;
+
+isused = array[Oend] of
+{
+	Oas =>		1,
+	Odas =>		1,
+	Oaddas =>	1,
+	Osubas =>	1,
+	Omulas =>	1,
+	Odivas =>	1,
+	Omodas =>	1,
+	Oexpas =>	1,
+	Oandas =>	1,
+	Ooras =>	1,
+	Oxoras =>	1,
+	Olshas =>	1,
+	Onothing =>	1,
+	Orshas =>	1,
+	Oinc =>		1,
+	Odec =>		1,
+	Opreinc =>	1,
+	Opredec =>	1,
+	Ocall =>	1,
+	Oraise =>	1,
+	Ospawn =>	1,
+	Osnd =>		1,
+	Orcv =>		1,
+
+	* =>		0
+};
+
+sideeffect := array[Oend] of
+{
+	Oas =>		1,
+	Odas =>		1,
+	Oaddas =>	1,
+	Osubas =>	1,
+	Omulas =>	1,
+	Odivas =>	1,
+	Omodas =>	1,
+	Oexpas =>	1,
+	Oandas =>	1,
+	Ooras =>	1,
+	Oxoras =>	1,
+	Olshas =>	1,
+	Orshas =>	1,
+	Oinc =>		1,
+	Odec =>		1,
+	Opreinc =>	1,
+	Opredec =>	1,
+	Ocall =>	1,
+	Oraise =>	1,
+	Ospawn =>	1,
+	Osnd =>		1,
+	Orcv =>		1,
+
+	Oadr =>		1,
+	Oarray =>	1,
+	Ocast =>	1,
+	Ochan =>	1,
+	Ocons =>	1,
+	Odiv =>		1,
+	Odot =>		1,
+	Oind =>		1,
+	Oindex =>	1,
+	Oinds =>	1,
+	Oindx =>	1,
+	Olen =>		1,
+	Oload =>	1,
+	Omod =>		1,
+	Oref =>		1,
+
+	* =>		0
+};
+
+opcommute = array[Oend] of
+{
+	Oeq =>		Oeq,
+	Oneq =>		Oneq,
+	Olt =>		Ogt,
+	Ogt =>		Olt,
+	Ogeq =>		Oleq,
+	Oleq =>		Ogeq,
+	Oadd =>		Oadd,
+	Omul =>		Omul,
+	Oxor =>		Oxor,
+	Oor =>		Oor,
+	Oand =>		Oand,
+
+	* =>		0
+};
+
+oprelinvert = array[Oend] of
+{
+
+	Oeq =>		Oneq,
+	Oneq =>		Oeq,
+	Olt =>		Ogeq,
+	Ogt =>		Oleq,
+	Ogeq =>		Olt,
+	Oleq =>		Ogt,
+
+	* =>		0
+};
+
+isrelop := array[Oend] of
+{
+
+	Oeq =>		1,
+	Oneq =>		1,
+	Olt =>		1,
+	Oleq =>		1,
+	Ogt =>		1,
+	Ogeq =>		1,
+	Oandand =>	1,
+	Ooror =>	1,
+	Onot =>		1,
+
+	* =>		0
+};
+
+ipow(x: big, n: int): big
+{
+	inv: int;
+	r: big;
+
+	inv = 0;
+	if(n < 0){
+		n = -n;
+		inv = 1;
+	}
+	r = big 1;
+	for(;;){
+		if(n&1)
+			r *= x;
+		if((n >>= 1) == 0)
+			break;
+		x *= x;
+	}
+	if(inv)
+		r = big 1/r;
+	return r;
+}
+
+rpow(x: real, n: int): real
+{
+	inv: int;
+	r: real;
+
+	inv = 0;
+	if(n < 0){
+		n = -n;
+		inv = 1;
+	}
+	r = 1.0;
+	for(;;){
+		if(n&1)
+			r *= x;
+		if((n >>= 1) == 0)
+			break;
+		x *= x;
+	}
+	if(inv)
+		r = 1.0/r;
+	return r;
+}
+
+real2fix(v: real, t: ref Type): big
+{
+	return big(v/scale(t));
+}
+
+fix2fix(v: big, f: ref Type, t: ref Type): big
+{
+	return big(real v * (scale(f)/scale(t)));
+}
+
+fix2real(v: big, f: ref Type): real
+{
+	return real v * scale(f);
+}
+
+istuple(n: ref Node): int
+{
+	d: ref Decl;
+
+	case(n.op){
+	Otuple =>
+		return 1;
+	Oname =>
+		d = n.decl;
+		if(d.importid != nil)
+			d = d.importid;
+		return d.store == Dconst && (n.ty.kind == Ttuple || n.ty.kind == Tadt);
+	Odot =>
+		return 0;	# istuple(n.left);
+	}
+	return 0;
+}
+
+tuplemem(n: ref Node, d: ref Decl): ref Node
+{
+	ty: ref Type;
+	ids: ref Decl;
+
+	ty = n.ty;
+	n = n.left;
+	for(ids = ty.ids; ids != nil; ids = ids.next){
+		if(ids.sym == d.sym)
+			break;
+		else
+			n = n.right;
+	}
+	if(n == nil)
+		fatal("tuplemem cannot cope !\n");
+	return n.left;
+}
+
+varcom(v: ref Decl): int
+{
+	n := v.init;
+	n = fold(n);
+	v.init = n;
+	if(debug['v'])
+		print("variable '%s' val %s\n", v.sym.name, expconv(n));
+	if(n == nil)
+		return 1;
+
+	tn := ref znode;
+	tn.op = Oname;
+	tn.decl = v;
+	tn.src = v.src;
+	tn.ty = v.ty;
+	return initable(tn, n, 0);
+}
+
+initable(v, n: ref Node, allocdep: int): int
+{
+	case n.ty.kind{
+	Tiface or
+	Tgoto or
+	Tcase or
+	Tcasel or
+	Tcasec or
+	Talt or
+	Texcept =>
+		return 1;
+	Tint or
+	Tbig or
+	Tbyte or
+	Treal or
+	Tstring or
+	Tfix =>
+		if(n.op != Oconst)
+			break;
+		return 1;
+	Tadt or
+	Tadtpick or
+	Ttuple =>
+		if(n.op == Otuple)
+			n = n.left;
+		else if(n.op == Ocall)
+			n = n.right;
+		else
+			break;
+		for(; n != nil; n = n.right)
+			if(!initable(v, n.left, allocdep))
+				return 0;
+		return 1;
+	Tarray =>
+		if(n.op != Oarray)
+			break;
+		if(allocdep >= DADEPTH){
+			nerror(v, expconv(v)+"s initializer has arrays nested more than "+string allocdep+" deep");
+			return 0;
+		}
+		allocdep++;
+		usedesc(mktdesc(n.ty.tof));
+		if(n.left.op != Oconst){
+			nerror(v, expconv(v)+"s size is not a constant");
+			return 0;
+		}
+		for(e := n.right; e != nil; e = e.right)
+			if(!initable(v, e.left.right, allocdep))
+				return 0;
+		return 1;
+	Tany =>
+		return 1;
+	Tref or
+	Tlist or
+	Tpoly or
+	* =>
+		nerror(v, "can't initialize "+etconv(v));
+		return 0;
+	}
+	nerror(v, expconv(v)+"s initializer, "+expconv(n)+", is not a constant expression");
+	return 0;
+}
+
+#
+# merge together two sorted lists, yielding a sorted list
+#
+elemmerge(e, f: ref Node): ref Node
+{
+	r := rock := ref Node;
+	while(e != nil && f != nil){
+		if(e.left.left.c.val <= f.left.left.c.val){
+			r.right = e;
+			e = e.right;
+		}else{
+			r.right = f;
+			f = f.right;
+		}
+		r = r.right;
+	}
+	if(e != nil)
+		r.right = e;
+	else
+		r.right = f;
+	return rock.right;
+}
+
+#
+# recursively split lists and remerge them after they are sorted
+#
+recelemsort(e: ref Node, n: int): ref Node
+{
+	if(n <= 1)
+		return e;
+	m := n / 2 - 1;
+	ee := e;
+	for(i := 0; i < m; i++)
+		ee = ee.right;
+	r := ee.right;
+	ee.right = nil;
+	return elemmerge(recelemsort(e, n / 2),
+			recelemsort(r, (n + 1) / 2));
+}
+
+#
+# sort the elems by index; wild card is first
+#
+elemsort(e: ref Node): ref Node
+{
+	n := 0;
+	for(ee := e; ee != nil; ee = ee.right){
+		if(ee.left.left.op == Owild)
+			ee.left.left.c = ref Const(big -1, 0.);
+		n++;
+	}
+	return recelemsort(e, n);
+}
+
+sametree(n1: ref Node, n2: ref Node): int
+{
+	if(n1 == n2)
+		return 1;
+	if(n1 == nil || n2 == nil)
+		return 0;
+	if(n1.op != n2.op || n1.ty != n2.ty)
+		return 0;
+	if(n1.op == Oconst){
+		case(n1.ty.kind){
+		Tbig or
+		Tbyte or
+		Tint =>
+			return n1.c.val == n2.c.val;
+		Treal =>
+			return n1.c.rval == n2.c.rval;
+		Tfix =>
+			return n1.c.val == n2.c.val && tequal(n1.ty, n2.ty);
+		Tstring =>
+			return n1.decl.sym == n2.decl.sym;
+		}
+		return 0;
+	}
+	return n1.decl == n2.decl && sametree(n1.left, n2.left) && sametree(n1.right, n2.right);
+}
+
+occurs(d: ref Decl, n: ref Node): int
+{
+	if(n == nil)
+		return 0;
+	if(n.op == Oname){
+		if(d == n.decl)
+			return 1;
+		return 0;
+	}
+	return occurs(d, n.left) + occurs(d, n.right);
+}
+
+#
+# left and right subtrees the same
+#
+folds(n: ref Node): ref Node
+{
+	if(hasside(n, 1))
+		return n;
+	case(n.op){
+	Oeq or
+	Oleq or
+	Ogeq =>
+		n.c = ref Const(big 1, 0.0);
+	Osub =>
+		n.c = ref Const(big 0, 0.0);
+	Oxor or
+	Oneq or
+	Olt or
+	Ogt =>
+		n.c = ref Const(big 0, 0.0);
+	Oand or
+	Oor or
+	Oandand or
+	Ooror =>
+		return n.left;
+	* =>
+		return n;
+	}
+	n.op = Oconst;
+	n.left = n.right = nil;
+	n.decl = nil;
+	return n;
+}
+
+#
+# constant folding for typechecked expressions
+#
+fold(n: ref Node): ref Node
+{
+	if(n == nil)
+		return nil;
+	if(debug['F'])
+		print("fold %s\n", nodeconv(n));
+	n = efold(n);
+	if(debug['F'])
+		print("folded %s\n", nodeconv(n));
+	return n;
+}
+
+efold(n: ref Node): ref Node
+{
+	d: ref Decl;
+
+	if(n == nil)
+		return nil;
+
+	left := n.left;
+	right := n.right;
+	case n.op{
+	Oname =>
+		d = n.decl;
+		if(d.importid != nil)
+			d = d.importid;
+		if(d.store != Dconst){
+			if(d.store == Dtag){
+				n.op = Oconst;
+				n.ty = tint;
+				n.c = ref Const(big d.tag, 0.);
+			}
+			break;
+		}
+		case n.ty.kind{
+		Tbig =>
+			n.op = Oconst;
+			n.c = ref Const(d.init.c.val, 0.);
+		Tbyte =>
+			n.op = Oconst;
+			n.c = ref Const(big byte d.init.c.val, 0.);
+		Tint or
+		Tfix =>
+			n.op = Oconst;
+			n.c = ref Const(big int d.init.c.val, 0.);
+		Treal =>
+			n.op = Oconst;
+			n.c = ref Const(big 0, d.init.c.rval);
+		Tstring =>
+			n.op = Oconst;
+			n.decl = d.init.decl;
+		Ttuple =>
+			*n = *d.init;
+		Tadt =>
+			*n = *d.init;
+			n = rewrite(n);	# was call
+		Texception =>
+			if(n.ty.cons == byte 0)
+				fatal("non-const exception type in efold");
+			n.op = Oconst;
+		* =>
+			fatal("unknown const type "+typeconv(n.ty)+" in efold");
+		}
+	Oadd =>
+		left = efold(left);
+		right = efold(right);
+		n.left = left;
+		n.right = right;
+		if(n.ty == tstring && right.op == Oconst){
+			if(left.op == Oconst)
+				n = mksconst(n.src, stringcat(left.decl.sym, right.decl.sym));
+			else if(left.op == Oadd && left.ty == tstring && left.right.op == Oconst){
+				left.right = mksconst(n.src, stringcat(left.right.decl.sym, right.decl.sym));
+				n = left;
+			}
+		}
+	Olen =>
+		left = efold(left);
+		n.left = left;
+		if(left.ty == tstring && left.op == Oconst)
+			n = mkconst(n.src, big len left.decl.sym.name);
+	Oslice =>
+		if(right.left.op == Onothing)
+			right.left = mkconst(right.left.src, big 0);
+		n.left = efold(left);
+		n.right = efold(right);
+	Oinds =>
+		n.left = left = efold(left);
+		n.right = right = efold(right);
+		if(right.op == Oconst && left.op == Oconst){
+			;
+		}
+	Ocast =>
+		n.op = Ocast;
+		left = efold(left);
+		n.left = left;
+		if(n.ty == left.ty || n.ty.kind == Tfix && tequal(n.ty, left.ty))
+			return left;
+		if(left.op == Oconst)
+			return foldcast(n, left);
+	Odot or
+	Omdot =>
+		#
+		# what about side effects from left?
+		#
+		d = right.decl;
+		case d.store{
+		Dconst or
+		Dtag or
+		Dtype =>
+			#
+			# set it up as a name and let that case do the hard work
+			#
+			n.op = Oname;
+			n.decl = d;
+			n.left = nil;
+			n.right = nil;
+			return efold(n);
+		}
+		n.left = efold(left);
+		if(n.left.op == Otuple)
+			n = tuplemem(n.left, d);
+		else
+			n.right = efold(right);
+	Otagof =>
+		if(n.decl != nil){
+			n.op = Oconst;
+			n.left = nil;
+			n.right = nil;
+			n.c = ref Const(big n.decl.tag, 0.);			
+			return efold(n);
+		}
+		n.left = efold(left);
+	Oif =>
+		n.left = left = efold(left);
+		n.right = right = efold(right);
+		if(left.op == Oconst){
+			if(left.c.val != big 0)
+				return right.left;
+			else
+				return right.right;
+		}
+	* =>
+		n.left = efold(left);
+		n.right = efold(right);
+	}
+
+	left = n.left;
+	right = n.right;
+	if(left == nil)
+		return n;
+
+	if(right == nil){
+		if(left.op == Oconst){
+			if(left.ty == tint || left.ty == tbyte || left.ty == tbig)
+				return foldc(n);
+			if(left.ty == treal)
+				return foldr(n);
+		}
+		return n;
+	}
+
+	if(left.op == Oconst){
+		case n.op{
+		Olsh or
+		Orsh =>
+			if(left.c.val == big 0 && !hasside(right, 1))
+				return left;
+		Ooror =>
+			if(left.ty == tint || left.ty == tbyte || left.ty == tbig){
+				if(left.c.val == big 0){
+					n = mkbin(Oneq, right, mkconst(right.src, big 0));
+					n.ty = right.ty;
+					n.left.ty = right.ty;
+					return efold(n);
+				}
+				left.c.val = big 1;
+				return left;
+			}
+		Oandand =>
+			if(left.ty == tint || left.ty == tbyte || left.ty == tbig){
+				if(left.c.val == big 0)
+					return left;
+				n = mkbin(Oneq, right, mkconst(right.src, big 0));
+				n.ty = right.ty;
+				n.left.ty = right.ty;
+				return efold(n);
+			}
+		}
+	}
+	if(left.op == Oconst && right.op != Oconst
+	&& opcommute[n.op]
+	&& n.ty != tstring){
+		n.op = opcommute[n.op];
+		n.left = right;
+		n.right = left;
+		left = right;
+		right = n.right;
+	}
+	if(right.op == Oconst && left.op == n.op && left.right.op == Oconst
+	&& (n.op == Oadd || n.op == Omul || n.op == Oor || n.op == Oxor || n.op == Oand)
+	&& n.ty != tstring){
+		n.left = left.left;
+		left.left = right;
+		right = efold(left);
+		n.right = right;
+		left = n.left;
+	}
+	if(right.op == Oconst){
+		if(n.op == Oexp && left.ty == treal){
+			if(left.op == Oconst)
+				return foldr(n);
+			return n;
+		}
+		if(right.ty == tint || right.ty == tbyte || left.ty == tbig){
+			if(left.op == Oconst)
+				return foldc(n);
+			return foldvc(n);
+		}
+		if(right.ty == treal && left.op == Oconst)
+			return foldr(n);
+	}
+	if(sametree(left, right))
+		return folds(n);
+	return n;
+}
+
+#
+# does evaluating the node have any side effects?
+#
+hasside(n: ref Node, strict: int): int
+{
+	for(; n != nil; n = n.right){
+		if(sideeffect[n.op] && (strict || n.op != Oadr && n.op != Oind))
+			return 1;
+		if(hasside(n.left, strict))
+			return 1;
+	}
+	return 0;
+}
+
+hascall(n: ref Node): int
+{
+	for(; n != nil; n = n.right){
+		if(n.op == Ocall || n.op == Ospawn)
+			return 1;
+		if(hascall(n.left))
+			return 1;
+	}
+	return 0;
+}
+
+hasasgns(n: ref Node): int
+{
+	if(n == nil)
+		return 0;
+	if(n.op != Ocall && isused[n.op] && n.op != Onothing)
+		return 1;
+	return hasasgns(n.left) || hasasgns(n.right);
+}
+
+nodes(n: ref Node): int
+{
+	if(n == nil)
+		return 0;
+	return 1+nodes(n.left)+nodes(n.right);
+}
+	
+foldcast(n, left: ref Node): ref Node
+{
+	case left.ty.kind{
+	Tint =>
+		left.c.val = big int left.c.val;
+		return foldcasti(n, left);
+	Tbyte =>
+		left.c.val = big byte left.c.val;
+		return foldcasti(n, left);
+	Tbig =>
+		return foldcasti(n, left);
+	Treal =>
+		case n.ty.kind{
+		Tint or
+		Tbyte or
+		Tbig =>
+			left.c.val = big left.c.rval;
+		Tfix =>
+			left.c.val = real2fix(left.c.rval, n.ty);
+		Tstring =>
+			return mksconst(n.src, enterstring(string left.c.rval));
+		* =>
+			return n;
+		}
+	Tfix =>
+		case n.ty.kind{
+		Tint or
+		Tbyte or
+		Tbig =>
+			left.c.val = big fix2real(left.c.val, left.ty);
+		Treal =>
+			left.c.rval = fix2real(left.c.val, left.ty);
+		Tfix =>
+			if(tequal(left.ty, n.ty))
+				return left;
+			left.c.val = fix2fix(left.c.val, left.ty, n.ty);
+		Tstring =>
+			return mksconst(n.src, enterstring(string fix2real(left.c.val, left.ty)));
+		* =>
+			return n;
+		}
+		break;
+	Tstring =>
+		case n.ty.kind{
+		Tint or
+		Tbyte or
+		Tbig =>
+			left.c = ref Const(big left.decl.sym.name, 0.);
+		Treal =>
+			left.c = ref Const(big 0, real left.decl.sym.name);
+		Tfix =>
+			left.c = ref Const(real2fix(real left.decl.sym.name, n.ty), 0.);
+		* =>
+			return n;
+		}
+	* =>
+		return n;
+	}
+	left.ty = n.ty;
+	left.src = n.src;
+	return left;
+}
+
+#
+# left is some kind of int type
+#
+foldcasti(n, left: ref Node): ref Node
+{
+	case n.ty.kind{
+	Tint =>
+		left.c.val = big int left.c.val;
+	Tbyte =>
+		left.c.val = big byte left.c.val;
+	Tbig =>
+		;
+	Treal =>
+		left.c.rval = real left.c.val;
+	Tfix =>
+		left.c.val = real2fix(real left.c.val, n.ty);
+	Tstring =>
+		return mksconst(n.src, enterstring(string left.c.val));
+	* =>
+		return n;
+	}
+	left.ty = n.ty;
+	left.src = n.src;
+	return left;
+}
+
+#
+# right is a const int
+#
+foldvc(n: ref Node): ref Node
+{
+	left := n.left;
+	right := n.right;
+	case n.op{
+	Oadd or
+	Osub or
+	Oor or
+	Oxor or
+	Olsh or
+	Orsh or
+	Ooror =>
+		if(right.c.val == big 0)
+			return left;
+		if(n.op == Ooror && !hasside(left, 1))
+			return right;
+	Oand =>
+		if(right.c.val == big 0 && !hasside(left, 1))
+			return right;
+	Omul =>
+		if(right.c.val == big 1)
+			return left;
+		if(right.c.val == big 0 && !hasside(left, 1))
+			return right;
+	Odiv =>
+		if(right.c.val == big 1)
+			return left;
+	Omod =>
+		if(right.c.val == big 1 && !hasside(left, 1)){
+			right.c.val = big 0;
+			return right;
+		}
+	Oexp =>
+		if(right.c.val == big 0){
+			right.c.val = big 1;
+			return right;
+		}
+		if(right.c.val == big 1)
+			return left;
+	Oandand =>
+		if(right.c.val != big 0)
+			return left;
+		if(!hasside(left, 1))
+			return right;
+	Oneq =>
+		if(!isrelop[left.op])
+			return n;
+		if(right.c.val == big 0)
+			return left;
+		n.op = Onot;
+		n.right = nil;
+	Oeq =>
+		if(!isrelop[left.op])
+			return n;
+		if(right.c.val != big 0)
+			return left;
+		n.op = Onot;
+		n.right = nil;
+	}
+	return n;
+}
+
+#
+# left and right are const ints
+#
+foldc(n: ref Node): ref Node
+{
+	v: big;
+	rv, nb: int;
+
+	left := n.left;
+	right := n.right;
+	case n.op{
+	Oadd =>
+		v = left.c.val + right.c.val;
+	Osub =>
+		v = left.c.val - right.c.val;
+	Omul =>
+		v = left.c.val * right.c.val;
+	Odiv =>
+		if(right.c.val == big 0){
+			nerror(n, "divide by 0 in constant expression");
+			return n;
+		}
+		v = left.c.val / right.c.val;
+	Omod =>
+		if(right.c.val == big 0){
+			nerror(n, "mod by 0 in constant expression");
+			return n;
+		}
+		v = left.c.val % right.c.val;
+	Oexp =>
+		if(left.c.val == big 0 && right.c.val < big 0){
+			nerror(n, "0 to negative power in constant expression");
+			return n;
+		}
+		v = ipow(left.c.val, int right.c.val);
+	Oand =>
+		v = left.c.val & right.c.val;
+	Oor =>
+		v = left.c.val | right.c.val;
+	Oxor =>
+		v = left.c.val ^ right.c.val;
+	Olsh =>
+		v = left.c.val;
+		rv = int right.c.val;
+		if(rv < 0 || rv >= n.ty.size * 8){
+			nwarn(n, "shift amount "+string rv+" out of range");
+			rv = 0;
+		}
+		if(rv == 0)
+			break;
+		v <<= rv;
+	Orsh =>
+		v = left.c.val;
+		rv = int right.c.val;
+		nb = n.ty.size * 8;
+		if(rv < 0 || rv >= nb){
+			nwarn(n, "shift amount "+string rv+" out of range");
+			rv = 0;
+		}
+		if(rv == 0)
+			break;
+		v >>= rv;
+	Oneg =>
+		v = -left.c.val;
+	Ocomp =>
+		v = ~left.c.val;
+	Oeq =>
+		v = big(left.c.val == right.c.val);
+	Oneq =>
+		v = big(left.c.val != right.c.val);
+	Ogt =>
+		v = big(left.c.val > right.c.val);
+	Ogeq =>
+		v = big(left.c.val >= right.c.val);
+	Olt =>
+		v = big(left.c.val < right.c.val);
+	Oleq =>
+		v = big(left.c.val <= right.c.val);
+	Oandand =>
+		v = big(int left.c.val && int right.c.val);
+	Ooror =>
+		v = big(int left.c.val || int right.c.val);
+	Onot =>
+		v = big(left.c.val == big 0);
+	* =>
+		return n;
+	}
+	if(n.ty == tint)
+		v = big int v;
+	else if(n.ty == tbyte)
+		v = big byte v;
+	n.left = nil;
+	n.right = nil;
+	n.decl = nil;
+	n.op = Oconst;
+	n.c = ref Const(v, 0.);
+	return n;
+}
+
+#
+# left and right are const reals
+#
+foldr(n: ref Node): ref Node
+{
+	rv := 0.;
+	v := big 0;
+
+	left := n.left;
+	right := n.right;
+	case n.op{
+	Ocast =>
+		return n;
+	Oadd =>
+		rv = left.c.rval + right.c.rval;
+	Osub =>
+		rv = left.c.rval - right.c.rval;
+	Omul =>
+		rv = left.c.rval * right.c.rval;
+	Odiv =>
+		rv = left.c.rval / right.c.rval;
+	Oexp =>
+		rv = rpow(left.c.rval, int right.c.val);
+	Oneg =>
+		rv = -left.c.rval;
+	Oinv =>
+		if(left.c.rval == 0.0){
+			error(n.src.start, "divide by 0 in fixed point type");
+			return n;
+		}
+		rv = 1.0/left.c.rval;
+	Oeq =>
+		v = big(left.c.rval == right.c.rval);
+	Oneq =>
+		v = big(left.c.rval != right.c.rval);
+	Ogt =>
+		v = big(left.c.rval > right.c.rval);
+	Ogeq =>
+		v = big(left.c.rval >= right.c.rval);
+	Olt =>
+		v = big(left.c.rval < right.c.rval);
+	Oleq =>
+		v = big(left.c.rval <= right.c.rval);
+	* =>
+		return n;
+	}
+	n.left = nil;
+	n.right = nil;
+	n.op = Oconst;
+
+	if(isnan(rv))
+		rv = canonnan;
+
+	n.c = ref Const(v, rv);
+	return n;
+}
+
+varinit(d: ref Decl, e: ref Node): ref Node
+{
+	n := mkdeclname(e.src, d);
+	if(d.next == nil)
+		return mkbin(Oas, n, e);
+	return mkbin(Oas, n, varinit(d.next, e));
+}
+
+#
+# given: an Oseq list with left == next or the last child
+# make a list with the right == next
+# ie: Oseq(Oseq(a, b),c) ==> Oseq(a, Oseq(b, Oseq(c, nil))))
+#
+rotater(e: ref Node): ref Node
+{
+	if(e == nil)
+		return e;
+	if(e.op != Oseq)
+		return mkunary(Oseq, e);
+	e.right = mkunary(Oseq, e.right);
+	while(e.left.op == Oseq){
+		left := e.left;
+		e.left = left.right;
+		left.right = e;
+		e = left;
+	}
+	return e;
+}
+
+#
+# reverse the case labels list
+#
+caselist(s, nr: ref Node): ref Node
+{
+	r := s.right;
+	s.right = nr;
+	if(r == nil)
+		return s;
+	return caselist(r, s);
+}
+
+#
+# e is a seq of expressions; make into cons's to build a list
+#
+etolist(e: ref Node): ref Node
+{
+	if(e == nil)
+		return nil;
+	n := mknil(e.src);
+	n.src.start = n.src.stop;
+	if(e.op != Oseq)
+		return mkbin(Ocons, e, n);
+	e.right = mkbin(Ocons, e.right, n);
+	while(e.left.op == Oseq){
+		e.op = Ocons;
+		left := e.left;
+		e.left = left.right;
+		left.right = e;
+		e = left;
+	}
+	e.op = Ocons;
+	return e;
+}
+
+dupn(resrc: int, src: Src, n: ref Node): ref Node
+{
+	nn := ref *n;
+	if(resrc)
+		nn.src = src;
+	if(nn.left != nil)
+		nn.left = dupn(resrc, src, nn.left);
+	if(nn.right != nil)
+		nn.right = dupn(resrc, src, nn.right);
+	return nn;
+}
+
+mkn(op: int, left, right: ref Node): ref Node
+{
+	n := ref Node;
+	n.op = op;
+	n.flags = byte 0;
+	n.left = left;
+	n.right = right;
+	return n;
+}
+
+mkunary(op: int, left: ref Node): ref Node
+{
+	n := ref Node;
+	n.src = left.src;
+	n.op = op;
+	n.flags = byte 0;
+	n.left = left;
+	return n;
+}
+
+mkbin(op: int, left, right: ref Node): ref Node
+{
+	n := ref Node;
+	n.src.start = left.src.start;
+	n.src.stop = right.src.stop;
+	n.op = op;
+	n.flags = byte 0;
+	n.left = left;
+	n.right = right;
+	return n;
+}
+
+mkdeclname(src: Src, d: ref Decl): ref Node
+{
+	n := ref Node;
+	n.src = src;
+	n.op = Oname;
+	n.flags = byte 0;
+	n.decl = d;
+	n.ty = d.ty;
+	d.refs++;
+	return n;
+}
+
+mknil(src: Src): ref Node
+{
+	return mkdeclname(src, nildecl);
+}
+
+mkname(src: Src, s: ref Sym): ref Node
+{
+	n := ref Node;
+	n.src = src;
+	n.op = Oname;
+	n.flags = byte 0;
+	if(s.unbound == nil){
+		s.unbound = mkdecl(src, Dunbound, nil);
+		s.unbound.sym = s;
+	}
+	n.decl = s.unbound;
+	return n;
+}
+
+mkconst(src: Src, v: big): ref Node
+{
+	n := ref Node;
+	n.src = src;
+	n.op = Oconst;
+	n.flags = byte 0;
+	n.ty = tint;
+	n.c = ref Const(v, 0.);
+	return n;
+}
+
+mkrconst(src: Src, v: real): ref Node
+{
+	n := ref Node;
+	n.src = src;
+	n.op = Oconst;
+	n.flags = byte 0;
+	n.ty = treal;
+	n.c = ref Const(big 0, v);
+	return n;
+}
+
+mksconst(src: Src, s: ref Sym): ref Node
+{
+	n := ref Node;
+	n.src = src;
+	n.op = Oconst;
+	n.flags = byte 0;
+	n.ty = tstring;
+	n.decl = mkdecl(src, Dconst, tstring);
+	n.decl.sym = s;
+	return n;
+}
+
+opconv(op: int): string
+{
+	if(op < 0 || op > Oend)
+		return "op "+string op;
+	return opname[op];
+}
+
+etconv(n: ref Node): string
+{
+	s := expconv(n);
+	if(n.ty == tany || n.ty == tnone || n.ty == terror)
+		return s;
+	s += " of type ";
+	s += typeconv(n.ty);
+	return s;
+}
+
+expconv(n: ref Node): string
+{
+	return "'" + subexpconv(n) + "'";
+}
+
+subexpconv(n: ref Node): string
+{
+	if(n == nil)
+		return "";
+	s := "";
+	if(int n.flags & PARENS)
+		s[len s] = '(';
+	case n.op{
+	Obreak or
+	Ocont =>
+		s += opname[n.op];
+		if(n.decl != nil)
+			s += " "+n.decl.sym.name;
+	Oexit or
+	Owild =>
+		s += opname[n.op];
+	Onothing =>
+		;
+	Oadr or
+	Oused =>
+		s += subexpconv(n.left);
+	Oseq =>
+		s += eprintlist(n, ", ");
+	Oname =>
+		if(n.decl == nil)
+			s += "<nil>";
+		else
+			s += n.decl.sym.name;
+	Oconst =>
+		if(n.ty.kind == Tstring){
+			s += stringpr(n.decl.sym);
+			break;
+		}
+		if(n.decl != nil && n.decl.sym != nil){
+			s += n.decl.sym.name;
+			break;
+		}
+		case n.ty.kind{
+		Tbig or
+		Tint or
+		Tbyte =>
+			s += string n.c.val;
+		Treal =>
+			s += string n.c.rval;
+		Tfix =>
+			s += string n.c.val + "(" + string n.ty.val.c.rval + ")";
+		* =>
+			s += opname[n.op];
+		}
+	Ocast =>
+		s += typeconv(n.ty);
+		s[len s] = ' ';
+		s += subexpconv(n.left);
+	Otuple =>
+		if(n.ty != nil && n.ty.kind == Tadt)
+			s += n.ty.decl.sym.name;
+		s[len s] = '(';
+		s += eprintlist(n.left, ", ");
+		s[len s] = ')';
+	Ochan =>
+		if(n.left != nil){
+			s += "chan [";
+			s += subexpconv(n.left);
+			s += "] of ";
+			s += typeconv(n.ty.tof);
+		}
+		else
+			s += "chan of "+typeconv(n.ty.tof);
+	Oarray =>
+		s += "array [";
+		if(n.left != nil)
+			s += subexpconv(n.left);
+		s += "] of ";
+		if(n.right != nil){
+			s += "{";
+			s += eprintlist(n.right, ", ");
+			s += "}";
+		}else{
+			s += typeconv(n.ty.tof);
+		}
+	Oelem or
+	Olabel =>
+		if(n.left != nil){
+			s += eprintlist(n.left, " or ");
+			s += " =>";
+		}
+		s += subexpconv(n.right);
+	Orange =>
+		s += subexpconv(n.left);
+		s += " to ";
+		s += subexpconv(n.right);
+	Ospawn =>
+		s += "spawn ";
+		s += subexpconv(n.left);
+	Oraise =>
+		s += "raise ";
+		s += subexpconv(n.left);
+	Ocall =>
+		s += subexpconv(n.left);
+		s += "(";
+		s += eprintlist(n.right, ", ");
+		s += ")";
+	Oinc or
+	Odec =>
+		s += subexpconv(n.left);
+		s += opname[n.op];
+	Oindex or
+	Oindx or
+	Oinds =>
+		s += subexpconv(n.left);
+		s += "[";
+		s += subexpconv(n.right);
+		s += "]";
+	Oslice =>
+		s += subexpconv(n.left);
+		s += "[";
+		s += subexpconv(n.right.left);
+		s += ":";
+		s += subexpconv(n.right.right);
+		s += "]";
+	Oload =>
+		s += "load ";
+		s += typeconv(n.ty);
+		s += " ";
+		s += subexpconv(n.left);
+	Oref or
+	Olen or
+	Ohd or
+	Otl or
+	Otagof =>
+		s += opname[n.op];
+		s[len s] = ' ';
+		s += subexpconv(n.left);
+	* =>
+		if(n.right == nil){
+			s += opname[n.op];
+			s += subexpconv(n.left);
+		}else{
+			s += subexpconv(n.left);
+			s += opname[n.op];
+			s += subexpconv(n.right);
+		}
+	}
+	if(int n.flags & PARENS)
+		s[len s] = ')';
+	return s;
+}
+
+eprintlist(elist: ref Node, sep: string): string
+{
+	if(elist == nil)
+		return "";
+	s := "";
+	for(; elist.right != nil; elist = elist.right){
+		if(elist.op == Onothing)
+			continue;
+		if(elist.left.op == Ofnptr)
+			return s;
+		s += subexpconv(elist.left);
+		if(elist.right.left.op != Ofnptr)
+			s += sep;
+	}
+	s += subexpconv(elist.left);
+	return s;
+}
+
+nodeconv(n: ref Node): string
+{
+	return nprint(n, 0);
+}
+
+nprint(n: ref Node, indent: int): string
+{
+	if(n == nil)
+		return "";
+	s := "\n";
+	for(i := 0; i < indent; i++)
+		s[len s] = ' ';
+	case n.op{
+	Oname =>
+		if(n.decl == nil)
+			s += "<nil>";
+		else
+			s += n.decl.sym.name;
+	Oconst =>
+		if(n.decl != nil && n.decl.sym != nil)
+			s += n.decl.sym.name;
+		else
+			s += opconv(n.op);
+		if(n.ty == tint || n.ty == tbyte || n.ty == tbig)
+			s += " (" + string n.c.val + ")";
+	* =>
+		s += opconv(n.op);
+	}
+	s += " " + typeconv(n.ty) + " " + string n.addable + " " + string n.temps;
+	indent += 2;
+	s += nprint(n.left, indent);
+	s += nprint(n.right, indent);
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/limbo/opname.m
@@ -1,0 +1,109 @@
+opname := array[Oend+1] of
+{
+			"unknown",
+
+	Oadd =>		"+",
+	Oaddas =>	"+=",
+	Oadr =>		"adr",
+	Oadtdecl =>	"adtdecl",
+	Oalt =>		"alt",
+	Oand =>		"&",
+	Oandand =>	"&&",
+	Oandas =>	"&=",
+	Oarray =>	"array",
+	Oas =>		"=",
+	Obreak =>	"break",
+	Ocall =>	"call",
+	Ocase =>	"case",
+	Ocast =>	"cast",
+	Ochan =>	"chan",
+	Ocomma =>	",",
+	Ocomp =>	"~",
+	Ocondecl =>	"condecl",
+	Ocons =>	"::",
+	Oconst =>	"const",
+	Ocont =>	"continue",
+	Odas =>		":=",
+	Odec =>		"--",
+	Odiv =>		"/",
+	Odivas =>	"/=",
+	Odo =>		"do",
+	Odot =>		".",
+	Oelem =>	"elem",
+	Oeq =>		"==",
+	Oexcept =>	"except",
+	Oexdecl =>	"exdecl",
+	Oexit =>	"exit",
+	Oexp =>	"**",
+	Oexpas =>	"**=",
+	Oexstmt =>	"exstat",
+	Ofielddecl =>	"fielddecl",
+	Ofnptr =>	"fnptr",
+	Ofor =>		"for",
+	Ofunc =>	"fn(){}",
+	Ogeq =>		">=",
+	Ogt =>		">",
+	Ohd =>		"hd",
+	Oif =>		"if",
+	Oimport =>	"import",
+	Oinc =>		"++",
+	Oind =>		"*",
+	Oindex =>	"index",
+	Oinds =>	"inds",
+	Oindx =>	"indx",
+	Oinv =>	"inv",
+	Ojmp =>		"jmp",
+	Olabel =>	"label",
+	Olen =>		"len",
+	Oleq =>		"<=",
+	Oload =>	"load",
+	Olsh =>		"<<",
+	Olshas =>	"<<=",
+	Olt =>		"<",
+	Omdot =>	"->",
+	Omod =>		"%",
+	Omodas =>	"%=",
+	Omoddecl =>	"moddecl",
+	Omul =>		"*",
+	Omulas =>	"*=",
+	Oname =>	"name",
+	Oneg =>		"-",
+	Oneq =>		"!=",
+	Onot =>		"!",
+	Onothing =>	"nothing",
+	Oor =>		"|",
+	Ooras =>	"|=",
+	Ooror =>	"||",
+	Opick =>	"pick",
+	Opickdecl =>	"pickdec",
+	Opredec =>	"--",
+	Opreinc =>	"++",
+	Oraise =>	"raise",
+	Orange =>	"range",
+	Orcv =>		"<-",
+	Oref =>		"ref",
+	Oret =>		"return",
+	Orsh =>		">>",
+	Orshas =>	">>=",
+	Oscope =>	"scope",
+	Oself =>	"self",
+	Oseq =>		"seq",
+	Oslice =>	"slice",
+	Osnd =>		"<-=",
+	Ospawn =>	"spawn",
+	Osub =>		"-",
+	Osubas =>	"-=",
+	Otagof =>	"tagof",
+	Otl =>		"tl",
+	Otuple =>	"tuple",
+	Otype => "type",
+	Otypedecl =>	"typedecl",
+	Oused =>	"used",
+	Ovardecl =>	"vardecl",
+	Ovardecli =>	"vardecli",
+	Owild =>	"*",
+	Oxor =>		"^",
+	Oxoras =>	"^=",
+
+	Oend =>	"unknown"
+};
--- /dev/null
+++ b/appl/cmd/limbo/optim.b
@@ -1,0 +1,3 @@
+optim(nil: ref Inst, nil: ref Decl)
+{
+}
--- /dev/null
+++ b/appl/cmd/limbo/stubs.b
@@ -1,0 +1,575 @@
+#
+# write out some stub C code for limbo modules
+#
+emit(globals: ref Decl)
+{
+	for(m := globals; m != nil; m = m.next){
+		if(m.store != Dtype || m.ty.kind != Tmodule)
+			continue;
+		m.ty = usetype(m.ty);
+		for(d := m.ty.ids; d != nil; d = d.next){
+			d.ty = usetype(d.ty);
+			if(d.store == Dglobal || d.store == Dfn)
+				modrefable(d.ty);
+			if(d.store == Dtype && d.ty.kind == Tadt){
+				for(id := d.ty.ids; id != nil; id = id.next){
+					id.ty = usetype(id.ty);
+					modrefable(d.ty);
+				}
+			}
+		}
+	}
+	if(emitstub){
+		adtstub(globals);
+		modstub(globals);
+	}
+	if(emittab != nil)
+		modtab(globals);
+	if(emitcode != nil)
+		modcode(globals);
+	if(emitsbl != nil)
+		modsbl(globals);
+}
+
+modsbl(globals: ref Decl)
+{
+	for(d := globals; d != nil; d = d.next)
+		if(d.store == Dtype && d.ty.kind == Tmodule && d.sym.name == emitsbl)
+			break;
+
+	if(d == nil)
+		return;
+	bsym = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+
+	sblmod(d);
+	sblfiles();
+	n := 0;
+	genstart();
+	for(id := d.ty.tof.ids; id != nil; id = id.next){
+		if(id.sym.name == ".mp")
+			continue;
+		pushblock();
+		id.pc = genrawop(id.src, INOP, nil, nil, nil);
+		id.pc.pc = n++;
+		popblock();
+	}
+	firstinst = firstinst.next;
+	sblinst(firstinst, n);
+#	(adts, nadts) := findadts(globals);
+	sblty(adts, nadts);
+	fs := array[n] of ref Decl;
+	n = 0;
+	for(id = d.ty.tof.ids; id != nil; id = id.next){
+		if(id.sym.name == ".mp")
+			continue;
+		fs[n] = id;
+		n++;
+	}
+	sblfn(fs, n);
+	sblvar(nil);
+}
+
+lowercase(f: string): string
+{
+	for(i := 0; i < len f; i++)
+		if(f[i] >= 'A' && f[i] <= 'Z')
+			f[i] += 'a' - 'A';
+	return f;
+}
+
+modcode(globals: ref Decl)
+{
+	buf: string;
+
+	if(emitdyn){
+		buf = lowercase(emitcode);
+		print("#include \"%s.h\"\n", buf);
+	}
+	else{
+		print("#include <lib9.h>\n");
+		print("#include <isa.h>\n");
+		print("#include <interp.h>\n");
+		print("#include \"%smod.h\"\n", emitcode);
+	}
+	print("\n");
+
+	for(d := globals; d != nil; d = d.next)
+		if(d.store == Dtype && d.ty.kind == Tmodule && d.sym.name == emitcode)
+			break;
+
+	if(d == nil)
+		return;
+
+	#
+	# stub types
+	#
+	for(id := d.ty.ids; id != nil; id = id.next){
+		if(id.store == Dtype && id.ty.kind == Tadt){
+			id.ty = usetype(id.ty);
+			print("Type*\tT_%s;\n", id.sym.name);
+		}
+	}
+
+	#
+	# type maps
+	#
+	if(emitdyn){
+		for(id = d.ty.ids; id != nil; id = id.next)
+			if(id.store == Dtype && id.ty.kind == Tadt)
+				print("uchar %s_map[] = %s_%s_map;\n",
+					id.sym.name, emitcode, id.sym.name);
+	}
+
+	#
+	# heap allocation and garbage collection for a type
+	#
+	if(emitdyn){
+		for(id = d.ty.ids; id != nil; id = id.next)
+			if(id.store == Dtype && id.ty.kind == Tadt){
+				print("\n%s_%s*\n%salloc%s(void)\n{\n\tHeap *h;\n\n\th = heap(T_%s);\n\treturn H2D(%s_%s*, h);\n}\n", emitcode, id.sym.name, emitcode, id.sym.name, id.sym.name, emitcode, id.sym.name);
+				print("\nvoid\n%sfree%s(Heap *h, int swept)\n{\n\t%s_%s *d;\n\n\td = H2D(%s_%s*, h);\n\tfreeheap(h, swept);\n}\n", emitcode, id.sym.name, emitcode, id.sym.name, emitcode, id.sym.name);
+			}
+	}
+
+	#
+	# initialization function
+	#
+	if(emitdyn)
+		print("\nvoid\n%sinit(void)\n{\n", emitcode);
+	else{
+		print("\nvoid\n%smodinit(void)\n{\n", emitcode);
+		print("\tbuiltinmod(\"$%s\", %smodtab, %smodlen);\n", emitcode, emitcode, emitcode);
+	}
+	for(id = d.ty.ids; id != nil; id = id.next)
+		if(id.store == Dtype && id.ty.kind == Tadt){
+			if(emitdyn)
+				print("\tT_%s = dtype(%sfree%s, %s_%s_size, %s_map, sizeof(%s_map));\n",
+					id.sym.name, emitcode, id.sym.name, emitcode, id.sym.name, id.sym.name, id.sym.name);
+			else
+				print("\tT_%s = dtype(freeheap, sizeof(%s), %smap, sizeof(%smap));\n",
+					id.sym.name, id.sym.name, id.sym.name, id.sym.name);
+		}
+	print("}\n");
+
+	#
+	# end function
+	#
+	if(emitdyn){
+		print("\nvoid\n%send(void)\n{\n", emitcode);
+		for(id = d.ty.ids; id != nil; id = id.next)
+			if(id.store == Dtype && id.ty.kind == Tadt)
+				print("\tfreetype(T_%s);\n", id.sym.name);
+		print("}\n");
+	}
+
+	#
+	# stub functions
+	#
+	for(id = d.ty.tof.ids; id != nil; id = id.next){
+		print("\nvoid\n%s_%s(void *fp)\n{\n\tF_%s_%s *f = fp;\n",
+			id.dot.sym.name, id.sym.name,
+			id.dot.sym.name, id.sym.name);
+		if(id.ty.tof != tnone && tattr[id.ty.tof.kind].isptr){
+			print("\tvoid *r;\n");
+			print("\n\tr = *f->ret;\n\t*f->ret = H;\n\tdestroy(r);\n");
+		}
+		print("}\n");
+	}
+
+	if(emitdyn)
+		print("\n#include \"%smod.h\"\n", buf);
+}
+
+modtab(globals: ref Decl)
+{
+	print("typedef struct{char *name; long sig; void (*fn)(void*); int size; int np; uchar map[16];} Runtab;\n");
+	for(d := globals; d != nil; d = d.next){
+		if(d.store == Dtype && d.ty.kind == Tmodule && d.sym.name == emittab){
+			n := 0;
+			print("Runtab %smodtab[]={\n", d.sym.name);
+			for(id := d.ty.tof.ids; id != nil; id = id.next){
+				n++;
+				print("\t\"");
+				if(id.dot != d)
+					print("%s.", id.dot.sym.name);
+				print("%s\",0x%ux,%s_%s,", id.sym.name, sign(id),
+					id.dot.sym.name, id.sym.name);
+				if(id.ty.varargs != byte 0)
+					print("0,0,{0},");
+				else{
+					md := mkdesc(idoffsets(id.ty.ids, MaxTemp, MaxAlign), id.ty.ids);
+					print("%d,%d,%s,", md.size, md.nmap, mapconv(md));
+				}
+				print("\n");
+			}
+			print("\t0\n};\n");
+			print("#define %smodlen	%d\n", d.sym.name, n);
+		}
+	}
+}
+
+#
+# produce activation records for all the functions in modules
+#
+modstub(globals: ref Decl)
+{
+	for(d := globals; d != nil; d = d.next){
+		if(d.store != Dtype || d.ty.kind != Tmodule)
+			continue;
+		arg := 0;
+		for(id := d.ty.tof.ids; id != nil; id = id.next){
+			s := id.dot.sym.name + "_" + id.sym.name;
+			if(emitdyn && id.dot.dot != nil)
+				s = id.dot.dot.sym.name + "_" + s;
+			print("void %s(void*);\ntypedef struct F_%s F_%s;\nstruct F_%s\n{\n",
+				s, s, s, s);
+			print("	WORD	regs[NREG-1];\n");
+			if(id.ty.tof != tnone)
+				print("	%s*	ret;\n", ctypeconv(id.ty.tof));
+			else
+				print("	WORD	noret;\n");
+			print("	uchar	temps[%d];\n", MaxTemp-NREG*IBY2WD);
+			offset := MaxTemp;
+			for(m := id.ty.ids; m != nil; m = m.next){
+				p := "";
+				if(m.sym != nil)
+					p = m.sym.name;
+				else
+					p = "arg"+string arg;
+
+				#
+				# explicit pads for structure alignment
+				#
+				t := m.ty;
+				(offset, nil) = stubalign(offset, t.align, nil);
+				if(offset != m.offset)
+					yyerror("module stub must not contain data objects");
+					# fatal("modstub bad offset");
+				print("	%s	%s;\n", ctypeconv(t), p);
+				arg++;
+				offset += t.size;
+#ZZZ need to align?
+			}
+			if(id.ty.varargs != byte 0)
+				print("	WORD	vargs;\n");
+			print("};\n");
+		}
+		for(id = d.ty.ids; id != nil; id = id.next)
+			if(id.store == Dconst)
+				constub(id);
+	}
+}
+
+chanstub(in: string, id: ref Decl)
+{
+	print("typedef %s %s_%s;\n", ctypeconv(id.ty.tof), in, id.sym.name);
+	desc := mktdesc(id.ty.tof);
+	print("#define %s_%s_size %d\n", in, id.sym.name, desc.size);
+	print("#define %s_%s_map %s\n", in, id.sym.name, mapconv(desc));
+}
+
+#
+# produce c structs for all adts
+#
+adtstub(globals: ref Decl)
+{
+	t, tt: ref Type;
+	m, d, id: ref Decl;
+
+	for(m = globals; m != nil; m = m.next){
+		if(m.store != Dtype || m.ty.kind != Tmodule)
+			continue;
+		for(d = m.ty.ids; d != nil; d = d.next){
+			if(d.store != Dtype)
+				continue;
+			t = usetype(d.ty);
+			d.ty = t;
+			s := dotprint(d.ty.decl, '_');
+			case d.ty.kind{
+			Tadt =>
+				print("typedef struct %s %s;\n", s, s);
+			Tint or
+			Tbyte or
+			Treal or
+			Tbig or
+			Tfix =>
+				print("typedef %s %s;\n", ctypeconv(t), s);
+			}
+		}
+	}
+	for(m = globals; m != nil; m = m.next){
+		if(m.store != Dtype || m.ty.kind != Tmodule)
+			continue;
+		for(d = m.ty.ids; d != nil; d = d.next){
+			if(d.store != Dtype)
+				continue;
+			t = d.ty;
+			if(t.kind == Tadt || t.kind == Ttuple && t.decl.sym != anontupsym){
+				if(t.tags != nil){
+					pickadtstub(t);
+					continue;
+				}
+				s := dotprint(t.decl, '_');
+				print("struct %s\n{\n", s);
+
+				offset := 0;
+				for(id = t.ids; id != nil; id = id.next){
+					if(id.store == Dfield){
+						tt = id.ty;
+						(offset, nil) = stubalign(offset, tt.align, nil);
+						if(offset != id.offset)
+							fatal("adtstub bad offset");
+						print("	%s	%s;\n", ctypeconv(tt), id.sym.name);
+						offset += tt.size;
+					}
+				}
+				if(t.ids == nil){
+					print("	char	dummy[1];\n");
+					offset = 1;
+				}
+				(offset, nil)= stubalign(offset, t.align, nil);
+#ZZZ
+(offset, nil) = stubalign(offset, IBY2WD, nil);
+				if(offset != t.size && t.ids != nil)
+					fatal("adtstub: bad size");
+				print("};\n");
+
+				for(id = t.ids; id != nil; id = id.next)
+					if(id.store == Dconst)
+						constub(id);
+
+				for(id = t.ids; id != nil; id = id.next)
+					if(id.ty.kind == Tchan)
+						chanstub(s, id);
+
+				desc := mktdesc(t);
+				if(offset != desc.size && t.ids != nil)
+					fatal("adtstub: bad desc size");
+				print("#define %s_size %d\n", s, offset);
+				print("#define %s_map %s\n", s, mapconv(desc));
+#ZZZ
+if(0)
+				print("struct %s_check {int s[2*(sizeof(%s)==%s_size)-1];};\n", s, s, s);
+			}else if(t.kind == Tchan)
+				chanstub(m.sym.name, d);
+		}
+	}
+}
+
+#
+# emit an expicit pad field for aligning emitted c structs
+# according to limbo's definition
+#
+stubalign(offset: int, a: int, s: string): (int, string)
+{
+	x := offset & (a-1);
+	if(x == 0)
+		return (offset, s);
+	x = a - x;
+	if(s != nil)
+		s += sprint("uchar\t_pad%d[%d]; ", offset, x);
+	else
+		print("\tuchar\t_pad%d[%d];\n", offset, x);
+	offset += x;
+	if((offset & (a-1)) || x >= a)
+		fatal("compiler stub misalign");
+	return (offset, s);
+}
+
+constub(id: ref Decl)
+{
+	s := id.dot.sym.name + "_" + id.sym.name;
+	case id.ty.kind{
+	Tbyte =>
+		print("#define %s %d\n", s, int id.init.c.val & 16rff);
+	Tint or
+	Tfix =>
+		print("#define %s %d\n", s, int id.init.c.val);
+	Tbig =>
+		print("#define %s %bd\n", s, id.init.c.val);
+	Treal =>
+		print("#define %s %g\n", s, id.init.c.rval);
+	Tstring =>
+		print("#define %s \"%s\"\n", s, id.init.decl.sym.name);
+	}
+}
+
+mapconv(d: ref Desc): string
+{
+	s := "{";
+	for(i := 0; i < d.nmap; i++)
+		s += "0x" + hex(int d.map[i], 0) + ",";
+	if(i == 0)
+		s += "0";
+	s += "}";
+	return s;
+}
+
+dotprint(d: ref Decl, dot: int): string
+{
+	s : string;
+	if(d.dot != nil){
+		s = dotprint(d.dot, dot);
+		s[len s] = dot;
+	}
+	if(d.sym == nil)
+		return s;
+	return s + d.sym.name;
+}
+
+ckindname := array[Tend] of
+{
+	Tnone =>	"void",
+	Tadt =>		"struct",
+	Tadtpick =>	"?adtpick?",
+	Tarray =>	"Array*",
+	Tbig =>		"LONG",
+	Tbyte =>	"BYTE",
+	Tchan =>	"Channel*",
+	Treal =>	"REAL",
+	Tfn =>		"?fn?",
+	Tint =>		"WORD",
+	Tlist =>	"List*",
+	Tmodule =>	"Modlink*",
+	Tref =>		"?ref?",
+	Tstring =>	"String*",
+	Ttuple =>	"?tuple?",
+	Texception => "?exception",
+	Tfix => "WORD",
+	Tpoly => "void*",
+
+	Tainit =>	"?ainit?",
+	Talt =>		"?alt?",
+	Tany =>		"void*",
+	Tarrow =>	"?arrow?",
+	Tcase =>	"?case?",
+	Tcasel =>	"?casel?",
+	Tcasec =>	"?casec?",
+	Tdot =>		"?dot?",
+	Terror =>	"?error?",
+	Tgoto =>	"?goto?",
+	Tid =>		"?id?",
+	Tiface =>	"?iface?",
+	Texcept => "?except?",
+	Tinst =>	"?inst?",
+};
+
+ctypeconv(t: ref Type): string
+{
+	if(t == nil)
+		return "void";
+	s := "";
+	case t.kind{
+	Terror =>
+		return "type error";
+	Tref =>
+		s = ctypeconv(t.tof);
+		s += "*";
+	Tarray or
+	Tlist or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tchan or
+	Tmodule or
+	Tfix or
+	Tpoly =>
+		return ckindname[t.kind];
+	Tadtpick =>
+		return ctypeconv(t.decl.dot.ty);
+	Tadt or
+	Ttuple =>
+		if(t.decl.sym != anontupsym)
+			return dotprint(t.decl, '_');
+		s += "struct{ ";
+		offset := 0;
+		for(id := t.ids; id != nil; id = id.next){
+			tt := id.ty;
+			(offset, s) = stubalign(offset, tt.align, s);
+			if(offset != id.offset)
+				fatal("ctypeconv tuple bad offset");
+			s += ctypeconv(tt);
+			s += " ";
+			s += id.sym.name;
+			s += "; ";
+			offset += tt.size;
+		}
+		(offset, s) = stubalign(offset, t.align, s);
+		if(offset != t.size)
+			fatal(sprint("ctypeconv tuple bad t=%s size=%d offset=%d", typeconv(t), t.size, offset));
+		s += "}";
+	* =>
+		fatal("no C equivalent for type " + string t.kind);
+	}
+	return s;
+}
+
+pickadtstub(t: ref Type)
+{
+	tt: ref Type;
+	desc: ref Desc;
+	id, tg: ref Decl;
+	ok: byte;
+	offset, tgoffset: int;
+
+	buf := dotprint(t.decl, '_');
+	offset = 0;
+	for(tg = t.tags; tg != nil; tg = tg.next)
+		print("#define %s_%s %d\n", buf, tg.sym.name, offset++);
+	print("struct %s\n{\n", buf);
+	print("	int	pick;\n");
+	offset = IBY2WD;
+	for(id = t.ids; id != nil; id = id.next){
+		if(id.store == Dfield){
+			tt = id.ty;
+			(offset, nil) = stubalign(offset, tt.align, nil);
+			if(offset != id.offset)
+				fatal("pickadtstub bad offset");
+			print("	%s	%s;\n", ctypeconv(tt), id.sym.name);
+			offset += tt.size;
+		}
+	}
+	print("	union{\n");
+	for(tg = t.tags; tg != nil; tg = tg.next){
+		tgoffset = offset;
+		print("		struct{\n");
+		for(id = tg.ty.ids; id != nil; id = id.next){
+			if(id.store == Dfield){
+				tt = id.ty;
+				(tgoffset, nil) = stubalign(tgoffset, tt.align, nil);
+				if(tgoffset != id.offset)
+					fatal("pickadtstub bad offset");
+				print("			%s	%s;\n", ctypeconv(tt), id.sym.name);
+				tgoffset += tt.size;
+			}
+		}
+		if(tg.ty.ids == nil)
+			print("			char	dummy[1];\n");
+		print("		} %s;\n", tg.sym.name);
+	}
+	print("	} u;\n");
+	print("};\n");
+
+	for(id = t.ids; id != nil; id = id.next)
+		if(id.store == Dconst)
+			constub(id);
+
+	for(id = t.ids; id != nil; id = id.next)
+		if(id.ty.kind == Tchan)
+			chanstub(buf, id);
+
+	for(tg = t.tags; tg != nil; tg = tg.next){
+		ok = tg.ty.tof.ok;
+		tg.ty.tof.ok = OKverify;
+		sizetype(tg.ty.tof);
+		tg.ty.tof.ok = OKmask;
+		desc = mktdesc(tg.ty.tof);
+		tg.ty.tof.ok = ok;
+		print("#define %s_%s_size %d\n", buf, tg.sym.name, tg.ty.size);
+		print("#define %s_%s_map %s\n", buf, tg.sym.name, mapconv(desc));
+	}
+}
--- /dev/null
+++ b/appl/cmd/limbo/typecheck.b
@@ -1,0 +1,3225 @@
+fndecls:	ref Decl;
+labstack:	array of ref Node;
+maxlabdep:	int;
+inexcept:	ref Node;
+nexc: int;
+fndec: ref Decl;
+
+increfs(id: ref Decl)
+{
+	for( ; id != nil; id = id.link)
+		id.refs++;
+}
+
+fninline(d: ref Decl): int
+{
+	left, right: ref Node;
+
+	n := d.init;
+	if(dontinline || d.inline == byte -1 || d.locals != nil || ispoly(d) || n.ty.tof.kind == Tnone || nodes(n) >= 100)
+		return 0;
+	n = n.right;
+	if(n.op == Oseq && n.right == nil)
+		n = n.left;
+	# 
+	# 	inline
+	# 		(a) return e;
+	# 		(b) if(c) return e1; else return e2;
+	# 		(c) if(c) return e1; return e2;
+	# 
+	case(n.op){
+	Oret =>
+		break;
+	Oif =>
+		right = n.right;
+		if(right.right == nil || right.left.op != Oret || right.right.op != Oret || !tequal(right.left.left.ty, right.right.left.ty))
+			return 0;
+		break;
+	Oseq =>
+		left = n.left;
+		right = n.right;
+		if(left.op != Oif || left.right.right != nil || left.right.left.op != Oret || right.op != Oseq || right.right != nil || right.left.op != Oret || !tequal(left.right.left.left.ty, right.left.left.ty))
+			return 0;
+		break;
+	* =>
+		return 0;
+	}
+	if(occurs(d, n) || hasasgns(n))
+		return 0;
+	if(n.op == Oseq){
+		left.right.right = right.left;
+		n = left;
+		right = n.right;
+		d.init.right.right = nil;
+	}
+	if(n.op == Oif){
+		n.ty = right.ty = right.left.left.ty;
+		right.left = right.left.left;
+		right.right = right.right.left;
+		d.init.right.left = mkunary(Oret, n);
+	}
+	return 1;
+}
+
+rewind(n: ref Node)
+{
+	r, nn: ref Node;
+
+	r = n;
+	nn = n.left;
+	for(n = n.right; n != nil; n = n.right){
+		if(n.right == nil){
+			r.left = nn;
+			r.right = n.left;
+		}
+		else
+			nn = mkbin(Oindex, nn, n.left);
+	}
+}
+
+ckmod(n: ref Node, id: ref Decl)
+{
+	t: ref Type;
+	d, idc: ref Decl;
+	mod: ref Node;
+
+	if(id == nil)
+		fatal("can't find function: " + nodeconv(n));
+	idc = nil;
+	mod = nil;
+	if(n.op == Oname){
+		idc = id;
+		mod = id.eimport;
+	}
+	else if(n.op == Omdot)
+		mod = n.left;
+	else if(n.op == Odot){
+		idc = id.dot;
+		t = n.left.ty;
+		if(t.kind == Tref)
+			t = t.tof;
+		if(t.kind == Tadtpick)
+			t = t.decl.dot.ty;
+		d = t.decl;
+		while(d != nil && d.link != nil)
+			d = d.link;
+		if(d != nil && d.timport != nil)
+			mod = d.timport.eimport;
+		n.right.left = mod;
+	}
+	if(mod != nil && mod.ty.kind != Tmodule){
+		nerror(n, "cannot use " + expconv(n) + " as a function reference");
+		return;
+	}
+	if(mod != nil){
+		if(valistype(mod)){
+			nerror(n, "cannot use " + expconv(n) + " as a function reference because " + expconv(mod) + " is a module interface");
+			return;
+		}
+	}else if(idc != nil && idc.dot != nil && !isimpmod(idc.dot.sym)){
+		nerror(n, "cannot use " + expconv(n) + " without importing " + idc.sym.name + " from a variable");
+		return;
+	}
+	if(mod != nil)
+		modrefable(n.ty);
+}
+
+addref(n: ref Node)
+{
+	nn: ref Node;
+
+	nn = mkn(0, nil, nil);
+	*nn = *n;
+	n.op = Oref;
+	n.left = nn;
+	n.right = nil;
+	n.decl = nil;
+	n.ty = usetype(mktype(n.src.start, n.src.stop, Tref, nn.ty, nil));
+}
+
+fnref(n: ref Node, id: ref Decl)
+{
+	id.inline = byte -1;
+	ckmod(n, id);
+	addref(n);
+	while(id.link != nil)
+		id = id.link;
+	if(ispoly(id) && encpolys(id) != nil)
+		nerror(n, "cannot have a polymorphic adt function reference " + id.sym.name);
+}
+
+typecheck(checkimp: int): ref Decl
+{
+	entry, d, m: ref Decl;
+
+	if(errors)
+		return nil;
+
+	#
+	# generate the set of all functions
+	# compile one function at a time
+	#
+	gdecl(tree);
+	gbind(tree);
+	fns = array[nfns] of ref Decl;
+	i := gcheck(tree, fns, 0);
+	if(i != nfns)
+		fatal("wrong number of functions found in gcheck");
+
+	maxlabdep = 0;
+	for(i = 0; i < nfns; i++){
+		d = fns[i];
+		if(d != nil)
+			fndec = d;
+		if(d != nil)
+			fncheck(d);
+		fndec = nil;
+	}
+
+	if(errors)
+		return nil;
+
+	entry = nil;
+	if(checkimp){
+		im: ref Decl;
+		dm: ref Dlist;
+
+		if(impmods == nil){
+			yyerror("no implementation module");
+			return nil;
+		}
+		for(im = impmods; im != nil; im = im.next){
+			for(dm = impdecls; dm != nil; dm = dm.next)
+				if(dm.d.sym == im.sym)
+					break;
+			if(dm == nil || dm.d.ty == nil){
+				yyerror("no definition for implementation module "+im.sym.name);
+				return nil;
+			}
+		}
+
+		#
+		# can't check the module spec until all types and imports are determined,
+		# which happens in scheck
+		#
+		for(dm = impdecls; dm != nil; dm = dm.next){
+			im = dm.d;
+			im.refs++;
+			im.ty = usetype(im.ty);
+			if(im.store != Dtype || im.ty.kind != Tmodule){
+				error(im.src.start, "cannot implement "+declconv(im));
+				return nil;
+			}
+		}
+	
+		# now check any multiple implementations
+		impdecl = modimp(impdecls, impmods);
+
+		s := enter("init", 0);
+		for(dm = impdecls; dm != nil; dm = dm.next){
+			im = dm.d;
+			for(m = im.ty.ids; m != nil; m = m.next){
+				m.ty = usetype(m.ty);
+				m.refs++;
+	
+				if(m.sym == s && m.ty.kind == Tfn && entry == nil)
+					entry = m;
+	
+				if(m.store == Dglobal || m.store == Dfn)
+					modrefable(m.ty);
+	
+				if(m.store == Dtype && m.ty.kind == Tadt){
+					for(d = m.ty.ids; d != nil; d = d.next){
+						d.ty = usetype(d.ty);
+						modrefable(d.ty);
+						d.refs++;
+					}
+				}
+			}
+			checkrefs(im.ty.ids);
+		}
+	}
+	if(errors)
+		return nil;
+	gsort(tree);
+	tree = nil;
+	return entry;
+}
+#
+# introduce all global declarations
+# also adds all fields to adts and modules
+# note the complications due to nested Odas expressions
+#
+gdecl(n: ref Node)
+{
+	for(;;){
+		if(n == nil)
+			return;
+		if(n.op != Oseq)
+			break;
+		gdecl(n.left);
+		n = n.right;
+	}
+	case n.op{
+	Oimport =>
+		importdecled(n);
+		gdasdecl(n.right);
+	Oadtdecl =>
+		adtdecled(n);
+	Ocondecl =>
+		condecled(n);
+		gdasdecl(n.right);
+	Oexdecl =>
+		exdecled(n);
+	Omoddecl =>
+		moddecled(n);
+	Otypedecl =>
+		typedecled(n);
+	Ovardecl =>
+		vardecled(n);
+	Ovardecli =>
+		vardecled(n.left);
+		gdasdecl(n.right);
+	Ofunc =>
+		fndecled(n);
+	Oas or
+	Odas or
+	Onothing =>
+		gdasdecl(n);
+	* =>
+		fatal("can't deal with "+opconv(n.op)+" in gdecl");
+	}
+}
+
+#
+# bind all global type ids,
+# including those nested inside modules
+# this needs to be done, since we may use such
+# a type later in a nested scope, so if we bound
+# the type ids then, the type could get bound
+# to a nested declaration
+#
+gbind(n: ref Node)
+{
+	ids: ref Decl;
+
+	for(;;){
+		if(n == nil)
+			return;
+		if(n.op != Oseq)
+			break;
+		gbind(n.left);
+		n = n.right;
+	}
+	case n.op{
+	Oas or
+	Ocondecl or
+	Odas or
+	Oexdecl or
+	Ofunc or
+	Oimport or
+	Onothing or
+	Ovardecl or
+	Ovardecli =>
+		break;
+	Ofielddecl =>
+		bindtypes(n.decl.ty);
+	Otypedecl =>
+		bindtypes(n.decl.ty);
+		if(n.left != nil)
+			gbind(n.left);
+	Opickdecl =>
+		gbind(n.left);
+		d := n.right.left.decl;
+		bindtypes(d.ty);
+		repushids(d.ty.ids);
+		gbind(n.right.right);
+		# get new ids for undefined types; propagate outwards
+		ids = popids(d.ty.ids);
+		if(ids != nil)
+			installids(Dundef, ids);
+	Oadtdecl or
+	Omoddecl =>
+		bindtypes(n.ty);
+		if(n.ty.polys != nil)
+			repushids(n.ty.polys);
+		repushids(n.ty.ids);
+		gbind(n.left);
+		# get new ids for undefined types; propagate outwards
+		ids = popids(n.ty.ids);
+		if(ids != nil)
+			installids(Dundef, ids);
+		if(n.ty.polys != nil)
+			popids(n.ty.polys);
+	* =>
+		fatal("can't deal with "+opconv(n.op)+" in gbind");
+	}
+}
+
+#
+# check all of the global declarations
+# bind all type ids referred to within types at the global level
+# record decls for defined functions
+#
+gcheck(n: ref Node, fns: array of ref Decl, nfns: int): int
+{
+	ok, allok: int;
+
+	for(;;){
+		if(n == nil)
+			return nfns;
+		if(n.op != Oseq)
+			break;
+		nfns = gcheck(n.left, fns, nfns);
+		n = n.right;
+	}
+
+	case n.op{
+	Ofielddecl =>
+		if(n.decl.ty.eraises != nil)
+			raisescheck(n.decl.ty);
+	Onothing or
+	Opickdecl =>
+		break;
+	Otypedecl =>
+		tcycle(n.ty);
+	Oadtdecl or
+	Omoddecl =>
+		if(n.ty.polys != nil)
+			repushids(n.ty.polys);
+		repushids(n.ty.ids);
+		if(gcheck(n.left, nil, 0))
+			fatal("gcheck fn decls nested in modules or adts");
+		if(popids(n.ty.ids) != nil)
+			fatal("gcheck installs new ids in a module or adt");
+		if(n.ty.polys != nil)
+			popids(n.ty.polys);
+	Ovardecl =>
+		varcheck(n, 1);
+	Ocondecl =>
+		concheck(n, 1);
+	Oexdecl =>
+		excheck(n, 1);
+	Oimport =>
+		importcheck(n, 1);
+	Ovardecli =>
+		varcheck(n.left, 1);
+		(ok, allok) = echeck(n.right, 0, 1, nil);
+		if(ok){
+			if(allok)
+				n.right = fold(n.right);
+			globalas(n.right.left, n.right.right, allok);
+		}
+	Oas or
+	Odas =>
+		(ok, allok) = echeck(n, 0, 1, nil);
+		if(ok){
+			if(allok)
+				n = fold(n);
+			globalas(n.left, n.right, allok);
+		}
+	Ofunc =>
+		(ok, allok) = echeck(n.left, 0, 1, n);
+		if(ok && n.ty.eraises != nil)
+			raisescheck(n.ty);
+		d : ref Decl = nil;
+		if(ok)
+			d = fnchk(n);
+		fns[nfns++] = d;
+	* =>
+		fatal("can't deal with "+opconv(n.op)+" in gcheck");
+	}
+	return nfns;
+}
+
+#
+# check for unused expression results
+# make sure the any calculated expression has
+# a destination
+#
+checkused(n: ref Node): ref Node
+{
+	#
+	# only nil; and nil = nil; should have type tany
+	#
+	if(n.ty == tany){
+		if(n.op == Oname)
+			return n;
+		if(n.op == Oas)
+			return checkused(n.right);
+		fatal("line "+lineconv(n.src.start)+" checkused "+nodeconv(n));
+	}
+
+	if(n.op == Ocall && n.left.ty.kind == Tfn && n.left.ty.tof != tnone){
+		n = mkunary(Oused, n);
+		n.ty = n.left.ty;
+		return n;
+	}
+	if(n.op == Ocall && isfnrefty(n.left.ty)){
+		if(n.left.ty.tof.tof != tnone){
+			n = mkunary(Oused, n);
+			n.ty = n.left.ty;
+		}
+		return n;
+	}
+	if(isused[n.op] && (n.op != Ocall || n.left.ty.kind == Tfn))
+		return n;
+	t := n.ty;
+	if(t.kind == Tfn)
+		nerror(n, "function "+expconv(n)+" not called");
+	else if(t.kind == Tadt && t.tags != nil || t.kind == Tadtpick)
+		nerror(n, "expressions cannot have type "+typeconv(t));
+	else if(n.op == Otuple){
+		for(nn := n.left; nn != nil; nn = nn.right)
+			checkused(nn.left);
+	}
+	else
+		nwarn(n, "result of expression "+expconv(n)+" not used");
+	n = mkunary(Oused, n);
+	n.ty = n.left.ty;
+	return n;
+}
+
+fncheck(d: ref Decl)
+{
+	n := d.init;
+	if(debug['t'])
+		print("typecheck tree: %s\n", nodeconv(n));
+
+	fndecls = nil;
+	adtp := outerpolys(n.left);
+	if(n.left.op == Odot)
+		repushids(adtp);
+	if(d.ty.polys != nil)
+		repushids(d.ty.polys);
+	repushids(d.ty.ids);
+
+	labdep = 0;
+	labstack = array[maxlabdep] of ref Node;
+	n.right = scheck(n.right, d.ty.tof, Sother);
+	if(labdep != 0)
+		fatal("unbalanced label stack in fncheck");
+	labstack = nil;
+
+	d.locals = appdecls(popids(d.ty.ids), fndecls);
+	if(d.ty.polys != nil)
+		popids(d.ty.polys);
+	if(n.left.op == Odot)
+		popids(adtp);
+	fndecls = nil;
+
+	checkrefs(d.ty.ids);
+	checkrefs(d.ty.polys);
+	checkrefs(d.locals);
+
+	checkraises(n);
+
+	d.inline = byte fninline(d);
+}
+
+scheck(n: ref Node, ret: ref Type, kind : int): ref Node
+{
+	s: ref Sym;
+	rok: int;
+	
+	top := n;
+	last: ref Node = nil;
+	for(; n != nil; n = n.right){
+		left := n.left;
+		right := n.right;
+		case n.op{
+		Ovardecl =>
+			vardecled(n);
+			varcheck(n, 0);
+			if (nested() && tmustzero(n.decl.ty))
+				decltozero(n);
+#			else if (inloop() && tmustzero(n.decl.ty))
+#				decltozero(n);
+			return top;
+		Ovardecli =>
+			vardecled(left);
+			varcheck(left, 0);
+			echeck(right, 0, 0, nil);
+			if (nested() && tmustzero(left.decl.ty))
+				decltozero(left);
+			return top;
+		Otypedecl =>
+			typedecled(n);
+			bindtypes(n.ty);
+			tcycle(n.ty);
+			return top;
+		Ocondecl =>
+			condecled(n);
+			concheck(n, 0);
+			return top;
+		Oexdecl =>
+			exdecled(n);
+			excheck(n, 0);
+			return top;
+		Oimport =>
+			importdecled(n);
+			importcheck(n, 0);
+			return top;
+		Ofunc =>
+			fatal("scheck func");
+		Oscope =>
+			if (kind == Sother)
+				kind = Sscope;
+			pushscope(n, kind);
+			if (left != nil)
+				fatal("Oscope has left field");
+			echeck(left, 0, 0, nil);
+			n.right = scheck(right, ret, Sother);
+			d := popscope();
+			fndecls = appdecls(fndecls, d);
+			return top;
+		Olabel =>
+			echeck(left, 0, 0, nil);
+			n.right = scheck(right, ret, Sother);
+			return top;
+		Oseq =>
+			n.left = scheck(left, ret, Sother);
+			# next time will check n.right
+		Oif =>
+			(rok, nil) = echeck(left, 0, 0, nil);
+			if(rok && left.op != Onothing && left.ty != tint)
+				nerror(n, "if conditional must be an int, not "+etconv(left));
+			right.left = scheck(right.left, ret, Sother);
+			# next time will check n.right.right
+			n = right;
+		Ofor =>
+			(rok, nil) = echeck(left, 0, 0, nil);
+			if(rok && left.op != Onothing && left.ty != tint)
+				nerror(n, "for conditional must be an int, not "+etconv(left));
+			#
+			# do the continue clause before the body
+			# this reflects the ordering of declarations
+			#
+			pushlabel(n);
+			right.right = scheck(right.right, ret, Sother);
+			right.left = scheck(right.left, ret, Sloop);
+			labdep--;
+			if(n.decl != nil && !n.decl.refs)
+				nwarn(n, "label "+n.decl.sym.name+" never referenced");
+			return top;
+		Odo =>
+			(rok, nil) = echeck(left, 0, 0, nil);
+			if(rok && left.op != Onothing && left.ty != tint)
+				nerror(n, "do conditional must be an int, not "+etconv(left));
+			pushlabel(n);
+			n.right = scheck(n.right, ret, Sloop);
+			labdep--;
+			if(n.decl != nil && !n.decl.refs)
+				nwarn(n, "label "+n.decl.sym.name+" never referenced");
+			return top;
+		Oalt or
+		Ocase or
+		Opick or
+		Oexcept =>
+			pushlabel(n);
+			case n.op{
+			Oalt =>
+				altcheck(n, ret);
+			Ocase =>
+				casecheck(n, ret);
+			Opick =>
+				pickcheck(n, ret);
+			Oexcept =>
+				exccheck(n, ret);
+			}
+			labdep--;
+			if(n.decl != nil && !n.decl.refs)
+				nwarn(n, "label "+n.decl.sym.name+" never referenced");
+			return top;
+		Oret =>
+			(rok, nil) = echeck(left, 0, 0, nil);
+			if(!rok)
+				return top;
+			if(left == nil){
+				if(ret != tnone)
+					nerror(n, "return of nothing from a fn of "+typeconv(ret));
+			}else if(ret == tnone){
+				if(left.ty != tnone)
+					nerror(n, "return "+etconv(left)+" from a fn with no return type");
+			}else if(!tcompat(ret, left.ty, 0))
+				nerror(n, "return "+etconv(left)+" from a fn of "+typeconv(ret));
+			return top;
+		Obreak or
+		Ocont =>
+			s = nil;
+			if(n.decl != nil)
+				s = n.decl.sym;
+			for(i := 0; i < labdep; i++){
+				if(s == nil || labstack[i].decl != nil && labstack[i].decl.sym == s){
+					if(n.op == Ocont
+					&& labstack[i].op != Ofor && labstack[i].op != Odo)
+						continue;
+					if(s != nil)
+						labstack[i].decl.refs++;
+					return top;
+				}
+			}
+			nerror(n, "no appropriate target for "+expconv(n));
+			return top;
+		Oexit or
+		Onothing =>
+			return top;
+		Oexstmt =>
+			fndec.handler = byte 1;
+			n.left = scheck(left, ret, Sother);
+			n.right = scheck(right, ret, Sother);
+			return top;
+		* =>
+			(nil, rok) = echeck(n, 0, 0, nil);
+			if(rok)
+				n = checkused(n);
+			if(last == nil)
+				return n;
+			last.right = n;
+			return top;
+		}
+		last = n;
+	}
+	return top;
+}
+
+pushlabel(n: ref Node)
+{
+	s: ref Sym;
+
+	if(labdep >= maxlabdep){
+		maxlabdep += MaxScope;
+		labs := array[maxlabdep] of ref Node;
+		labs[:] = labstack;
+		labstack = labs;
+	}
+	if(n.decl != nil){
+		s = n.decl.sym;
+		n.decl.refs = 0;
+		for(i := 0; i < labdep; i++)
+			if(labstack[i].decl != nil && labstack[i].decl.sym == s)
+				nerror(n, "label " + s.name + " duplicated on line " + lineconv(labstack[i].decl.src.start));
+	}
+	labstack[labdep++] = n;
+}
+
+varcheck(n: ref Node, isglobal: int)
+{
+	t := validtype(n.ty, nil);
+	t = topvartype(t, n.decl, isglobal, 0);
+	last := n.left.decl;
+	for(ids := n.decl; ids != last.next; ids = ids.next){
+		ids.ty = t;
+		shareloc(ids);
+	}
+	if(t.eraises != nil)
+		raisescheck(t);
+}
+
+concheck(n: ref Node, isglobal: int)
+{
+	t: ref Type;
+	init: ref Node;
+
+	pushscope(nil, Sother);
+	installids(Dconst, iota);
+	(ok, allok) := echeck(n.right, 0, isglobal, nil);
+	popscope();
+
+	init = n.right;
+	if(!ok){
+		t = terror;
+	}else{
+		t = init.ty;
+		if(!tattr[t.kind].conable){
+			nerror(init, "cannot have a "+typeconv(t)+" constant");
+			allok = 0;
+		}
+	}
+
+	last := n.left.decl;
+	for(ids := n.decl; ids != last.next; ids = ids.next)
+		ids.ty = t;
+
+	if(!allok)
+		return;
+
+	i := 0;
+	for(ids = n.decl; ids != last.next; ids = ids.next){
+		if(ok){
+			iota.init.c.val = big i;
+			ids.init = dupn(0, nosrc, init);
+			if(!varcom(ids))
+				ok = 0;
+		}
+		i++;
+	}
+}
+
+exname(d: ref Decl): string
+{
+	s := "";
+	m: ref Sym;
+	if(d.dot != nil)
+		m = d.dot.sym;
+	else if(impmods != nil)
+		m = impmods.sym;
+	if(m != nil)
+		s += m.name+".";
+	if(fndec != nil)
+		s += fndec.sym.name+".";
+	s += string (scope-ScopeGlobal)+"."+d.sym.name;
+	return s;
+}
+
+excheck(n: ref Node, isglobal: int)
+{
+	t: ref Type;
+	ids, last: ref Decl;
+
+	t = validtype(n.ty, nil);
+	t = topvartype(t, n.decl, isglobal, 0);
+	last = n.left.decl;
+	for(ids = n.decl; ids != last.next; ids = ids.next){
+		ids.ty = t;
+		ids.init = mksconst(n.src, enterstring(exname(ids)));
+		# ids.init = mksconst(n.src, enterstring(ids.sym.name));
+	}
+}
+
+importcheck(n: ref Node, isglobal: int)
+{
+	(ok, nil) := echeck(n.right, 1, isglobal, nil);
+	if(!ok)
+		return;
+
+	m := n.right;
+	if(m.ty.kind != Tmodule || m.op != Oname){
+		nerror(n, "cannot import from "+etconv(m));
+		return;
+	}
+
+	last := n.left.decl;
+	for(id := n.decl; id != last.next; id = id.next){
+		v := namedot(m.ty.ids, id.sym);
+		if(v == nil){
+			error(id.src.start, id.sym.name+" is not a member of "+expconv(m));
+			id.store = Dwundef;
+			continue;
+		}
+		id.store = v.store;
+		v.ty = validtype(v.ty, nil);
+		id.ty = t := v.ty;
+		if(id.store == Dtype && t.decl != nil){
+			id.timport = t.decl.timport;
+			t.decl.timport = id;
+		}
+		id.init = v.init;
+		id.importid = v;
+		id.eimport = m;
+	}
+}
+
+rewcall(n: ref Node, d: ref Decl): ref Decl
+{
+	# put original function back now we're type checked
+	while(d.link != nil)
+		d = d.link;
+	if(n.op == Odot)
+		n.right.decl = d;
+	else if(n.op == Omdot){
+		n.right.right.decl = d;
+		n.right.right.ty = d.ty;
+	}
+	else
+		fatal("bad op in Ocall rewcall");
+	n.ty = n.right.ty = d.ty;
+	d.refs++;
+	usetype(d.ty);
+	return d;
+}
+
+isfnrefty(t: ref Type): int
+{
+	return t.kind == Tref && t.tof.kind == Tfn;
+}
+
+isfnref(d: ref Decl): int
+{
+	case(d.store){
+	Dglobal or
+	Darg or
+	Dlocal or
+	Dfield or
+	Dimport =>
+		return isfnrefty(d.ty);
+	}
+	return 0;
+}
+
+tagopt: int;
+
+#
+# annotate the expression with types
+#
+echeck(n: ref Node, typeok, isglobal: int, par: ref Node): (int, int)
+{
+	tg, id, callee: ref Decl;
+	t, tt: ref Type;
+	ok, allok, max, nocheck, kidsok: int;
+
+	ok = allok = 1;
+	if(n == nil)
+		return (1, 1);
+
+	if(n.op == Oseq){
+		for( ; n != nil && n.op == Oseq; n = n.right){
+			(okl, allokl) := echeck(n.left, typeok == 2, isglobal, n);
+			ok &= okl;
+			allok &= allokl;
+			n.ty = tnone;
+		}
+		if(n == nil)
+			return (ok, allok);
+	}
+
+	left := n.left;
+	right := n.right;
+
+	nocheck = 0;
+	if(n.op == Odot || n.op == Omdot || n.op == Ocall || n.op == Oref || n.op == Otagof || n.op == Oindex)
+		nocheck = 1;
+	if(n.op != Odas			# special case
+	&& n.op != Oload)		# can have better error recovery
+		(ok, allok) = echeck(left, nocheck, isglobal, n);
+	if(n.op != Odas			# special case
+	&& n.op != Odot			# special check
+	&& n.op != Omdot		# special check
+	&& n.op != Ocall		# can have better error recovery
+	&& n.op != Oindex){
+		(okr, allokr) := echeck(right, 0, isglobal, n);
+		ok &= okr;
+		allok &= allokr;
+	}
+	if(!ok){
+		n.ty = terror;
+		return (0, 0);
+	}
+
+	case n.op{
+	Odas =>
+		(ok, allok) = echeck(right, 0, isglobal, n);
+		if(!ok)
+			right.ty = terror;
+		if(!isglobal && !dasdecl(left)){
+			ok = 0;
+		}else if(!specific(right.ty) || !declasinfer(left, right.ty)){
+			nerror(n, "cannot declare "+expconv(left)+" from "+etconv(right));
+			declaserr(left);
+			ok = 0;
+		}
+		if(right.ty.kind == Texception)
+			left.ty = n.ty = mkextuptype(right.ty);
+		else{
+			left.ty = n.ty = right.ty;
+			usedty(n.ty);
+		}
+		if (nested() && tmustzero(left.ty))
+			decltozero(left);
+		return (ok, allok & ok);
+	Oseq or
+	Onothing =>
+		n.ty = tnone;
+	Owild =>
+		n.ty = tint;
+	Ocast =>
+		t = usetype(n.ty);
+		n.ty = t;
+		tt = left.ty;
+		if(tcompat(t, tt, 0)){
+			left.ty = t;
+			break;
+		}
+		if(tt.kind == Tarray){
+			if(tt.tof == tbyte && t == tstring)
+				break;
+		}else if(t.kind == Tarray){
+			if(t.tof == tbyte && tt == tstring)
+				break;
+		}else if(casttab[tt.kind][t.kind]){
+			break;
+		}
+		nerror(n, "cannot make a "+typeconv(n.ty)+" from "+etconv(left));
+		return (0, 0);
+	Ochan =>
+		n.ty = usetype(n.ty);
+		if(left != nil && left.ty.kind != Tint){
+			nerror(n, "channel size "+etconv(left)+" is not an int");
+			return (0, 0);
+		}
+	Oload =>
+		n.ty = usetype(n.ty);
+		(nil, kidsok) = echeck(left, 0, isglobal, n);
+		if(n.ty.kind != Tmodule){
+			nerror(n, "cannot load a "+typeconv(n.ty));
+			return (0, 0);
+		}
+		if(!kidsok){
+			allok = 0;
+			break;
+		}
+		if(left.ty != tstring){
+			nerror(n, "cannot load a module from "+etconv(left));
+			allok = 0;
+			break;
+		}
+if(n.ty.tof.decl.refs != 0)
+n.ty.tof.decl.refs++;
+n.ty.decl.refs++;
+		usetype(n.ty.tof);
+	Oref =>
+		t = left.ty;
+		if(t.kind != Tadt && t.kind != Tadtpick && t.kind != Tfn && t.kind != Ttuple){
+			nerror(n, "cannot make a ref from "+etconv(left));
+			return (0, 0);
+		}
+		if(!tagopt && t.kind == Tadt && t.tags != nil && valistype(left)){
+			nerror(n, "instances of ref "+expconv(left)+" must be qualified with a pick tag");
+			return (0, 0);
+		}
+		if(t.kind == Tadtpick)
+			t.tof = usetype(t.tof);
+		n.ty = usetype(mktype(n.src.start, n.src.stop, Tref, t, nil));
+	Oarray =>
+		max = 0;
+		if(right != nil){
+			max = assignindices(n);
+			if(max < 0)
+				return (0, 0);
+			if(!specific(right.left.ty)){
+				nerror(n, "type for array not specific");
+				return (0, 0);
+			}
+			n.ty = mktype(n.src.start, n.src.stop, Tarray, right.left.ty, nil);
+		}
+		n.ty = usetype(n.ty);
+
+		if(left.op == Onothing)
+			n.left = left = mkconst(n.left.src, big max);
+
+		if(left.ty.kind != Tint){
+			nerror(n, "array size "+etconv(left)+" is not an int");
+			return (0, 0);
+		}
+	Oelem =>
+		n.ty = right.ty;
+	Orange =>
+		if(left.ty != right.ty
+		|| left.ty != tint && left.ty != tstring){
+			nerror(left, "range "+etconv(left)+" to "+etconv(right)+" is not an int or string range");
+			return (0, 0);
+		}
+		n.ty = left.ty;
+	Oname =>
+		id = n.decl;
+		if(id == nil){
+			nerror(n, "name with no declaration");
+			return (0, 0);
+		}
+		if(id.store == Dunbound){
+			s := id.sym;
+			id = s.decl;
+			if(id == nil)
+				id = undefed(n.src, s);
+			# save a little space
+			s.unbound = nil;
+			n.decl = id;
+			id.refs++;
+		}
+		n.ty = id.ty = usetype(id.ty);
+		case id.store{
+		Dfn or
+		Dglobal or
+		Darg or
+		Dlocal or
+		Dimport or
+		Dfield or
+		Dtag =>
+			break;
+		Dunbound =>
+			fatal("unbound symbol found in echeck");
+		Dundef =>
+			nerror(n, id.sym.name+" is not declared");
+			id.store = Dwundef;
+			return (0, 0);
+		Dwundef =>
+			return (0, 0);
+		Dconst =>
+			if(id.init == nil){
+				nerror(n, id.sym.name+"'s value cannot be determined");
+				id.store = Dwundef;
+				return (0, 0);
+			}
+		Dtype =>
+			if(typeok)
+				break;
+			nerror(n, declconv(id)+" is not a variable");
+			return (0, 0);
+		* =>
+			fatal("echeck: unknown symbol storage");
+		}
+		
+		if(n.ty == nil){
+			nerror(n, declconv(id)+"'s type is not fully defined");
+			id.store = Dwundef;
+			return (0, 0);
+		}
+		if(id.importid != nil && valistype(id.eimport)
+		&& id.store != Dconst && id.store != Dtype && id.store != Dfn){
+			nerror(n, "cannot use "+expconv(n)+" because "+expconv(id.eimport)+" is a module interface");
+			return (0, 0);
+		}
+		if(n.ty.kind == Texception && !int n.ty.cons && par != nil && par.op != Oraise && par.op != Odot){
+			nn := mkn(0, nil, nil);
+			*nn = *n;
+			n.op = Ocast;
+			n.left = nn;
+			n.decl = nil;
+			n.ty = usetype(mkextuptype(n.ty));
+		}
+		# function name as function reference
+		if(id.store == Dfn && (par == nil || (par.op != Odot && par.op != Omdot && par.op != Ocall && par.op != Ofunc)))
+			fnref(n, id);
+	Oconst =>
+		if(n.ty == nil){
+			nerror(n, "no type in "+expconv(n));
+			return (0, 0);
+		}
+	Oas =>
+		t = right.ty;
+		if(t.kind == Texception)
+			t = mkextuptype(t);
+		if(!tcompat(left.ty, t, 1)){
+			nerror(n, "type clash in "+etconv(left)+" = "+etconv(right));
+			return (0, 0);
+		}
+		if(t == tany)
+			t = left.ty;
+		n.ty = t;
+		left.ty = t;
+		if(t.kind == Tadt && t.tags != nil || t.kind == Tadtpick)
+		if(left.ty.kind != Tadtpick || right.ty.kind != Tadtpick)
+			nerror(n, "expressions cannot have type "+typeconv(t));
+		if(left.ty.kind == Texception){
+			nerror(n, "cannot assign to an exception");
+			return (0, 0);
+		}
+		if(islval(left))
+			break;
+		return (0, 0);
+	Osnd =>
+		if(left.ty.kind != Tchan){
+			nerror(n, "cannot send on "+etconv(left));
+			return (0, 0);
+		}
+		if(!tcompat(left.ty.tof, right.ty, 0)){
+			nerror(n, "type clash in "+etconv(left)+" <-= "+etconv(right));
+			return (0, 0);
+		}
+		t = right.ty;
+		if(t == tany)
+			t = left.ty.tof;
+		n.ty = t;
+	Orcv =>
+		t = left.ty;
+		if(t.kind == Tarray)
+			t = t.tof;
+		if(t.kind != Tchan){
+			nerror(n, "cannot receive on "+etconv(left));
+			return (0, 0);
+		}
+		if(left.ty.kind == Tarray)
+			n.ty = usetype(mktype(n.src.start, n.src.stop, Ttuple, nil,
+					mkids(n.src, nil, tint, mkids(n.src, nil, t.tof, nil))));
+		else
+			n.ty = t.tof;
+	Ocons =>
+		if(right.ty.kind != Tlist && right.ty != tany){
+			nerror(n, "cannot :: to "+etconv(right));
+			return (0, 0);
+		}
+		n.ty = right.ty;
+		if(right.ty == tany)
+			n.ty = usetype(mktype(n.src.start, n.src.stop, Tlist, left.ty, nil));
+		else if(!tcompat(right.ty.tof, left.ty, 0)){
+			t = tparent(right.ty.tof, left.ty);
+			if(!tcompat(t, left.ty, 0)){
+				nerror(n, "type clash in "+etconv(left)+" :: "+etconv(right));
+				return (0, 0);
+			}
+			else
+				n.ty = usetype(mktype(n.src.start, n.src.stop, Tlist, t, nil));
+		}
+	Ohd or
+	Otl =>
+		if(left.ty.kind != Tlist || left.ty.tof == nil){
+			nerror(n, "cannot "+opconv(n.op)+" "+etconv(left));
+			return (0, 0);
+		}
+		if(n.op == Ohd)
+			n.ty = left.ty.tof;
+		else
+			n.ty = left.ty;
+	Otuple =>
+		n.ty = usetype(mktype(n.src.start, n.src.stop, Ttuple, nil, tuplefields(left)));
+	Ospawn =>
+		if(left.op != Ocall || left.left.ty.kind != Tfn && !isfnrefty(left.left.ty)){
+			nerror(left, "cannot spawn "+expconv(left));
+			return (0, 0);
+		}
+		if(left.ty != tnone){
+			nerror(left, "cannot spawn functions which return values, such as "+etconv(left));
+			return (0, 0);
+		}
+	Oraise =>
+		if(left.op == Onothing){
+			if(inexcept == nil){
+				nerror(n, expconv(n)+": empty raise not in exception handler");
+				return (0, 0);
+			}
+			n.left = dupn(1, n.src, inexcept);
+			break;
+		}
+		if(left.ty != tstring && left.ty.kind != Texception){
+			nerror(n, expconv(n)+": raise argument "+etconv(left)+" is not a string or exception");
+			return (0, 0);
+		}
+		if((left.op != Ocall || left.left.ty.kind == Tfn) && left.ty.ids != nil && int left.ty.cons){
+			nerror(n, "too few exception arguments");
+			return (0, 0);
+		}
+	Ocall =>
+		(nil, kidsok) = echeck(right, 0, isglobal, nil);
+		t = left.ty;
+		usedty(t);
+		pure := 1;
+		if(t.kind == Tref){
+			pure = 0;
+			t = t.tof;
+		}
+		if(t.kind != Tfn)
+			return callcast(n, kidsok, allok);
+		n.ty = t.tof;
+		if(!kidsok){
+			allok = 0;
+			break;
+		}
+
+		#
+		# get the name to call and any associated module
+		#
+		mod: ref Node = nil;
+		callee = nil;
+		id = nil;
+		tt = nil;
+		if(left.op == Odot){
+			callee = left.right.decl;
+			id = callee.dot;
+			right = passimplicit(left, right);
+			n.right = right;
+			tt = left.left.ty;
+			if(tt.kind == Tref)
+				tt = tt.tof;
+			ttt := tt;
+			if(tt.kind == Tadtpick)
+				ttt = tt.decl.dot.ty;
+			dd := ttt.decl;
+			while(dd != nil && dd.link != nil)
+				dd = dd.link;
+			if(dd != nil && dd.timport != nil)
+				mod = dd.timport.eimport;
+
+			#
+			# stash the import module under a rock,
+			# because we won't be able to get it later
+			# after scopes are popped
+			#
+			left.right.left = mod;
+		}else if(left.op == Omdot){
+			if(left.right.op == Odot){
+				callee = left.right.right.decl;
+				right = passimplicit(left.right, right);
+				n.right = right;
+				tt = left.right.left.ty;
+				if(tt.kind == Tref)
+					tt = tt.tof;
+			}else
+				callee = left.right.decl;
+			mod = left.left;
+		}else if(left.op == Oname){
+			callee = left.decl;
+			id = callee;
+			mod = id.eimport;
+		}else if(pure){
+			nerror(left, expconv(left)+" is not a function name");
+			allok = 0;
+			break;
+		}
+		if(pure && callee == nil)
+			fatal("can't find called function: "+nodeconv(left));
+		if(callee != nil && callee.store != Dfn && !isfnref(callee)){
+			nerror(left, expconv(left)+" is not a function");
+			allok = 0;
+			break;
+		}
+		if(mod != nil && mod.ty.kind != Tmodule){
+			nerror(left, "cannot call "+expconv(left));
+			allok = 0;
+			break;
+		}
+		if(mod != nil){
+			if(valistype(mod)){
+				nerror(left, "cannot call "+expconv(left)+" because "+expconv(mod)+" is a module interface");
+				allok = 0;
+				break;
+			}
+		}else if(id != nil && id.dot != nil && !isimpmod(id.dot.sym)){
+			nerror(left, "cannot call "+expconv(left)+" without importing "+id.sym.name+" from a variable");
+			allok = 0;
+			break;
+		}
+		if(mod != nil)
+			modrefable(left.ty);
+		if(callee != nil && callee.store != Dfn)
+			callee = nil;
+		if(t.varargs != byte 0){
+			t = mkvarargs(left, right);
+			if(left.ty.kind == Tref)
+				left.ty = usetype(mktype(t.src.start, t.src.stop, Tref, t, nil));
+			else
+				left.ty = t;
+		}
+		else if(ispoly(callee) || isfnrefty(left.ty) && left.ty.tof.polys != nil){
+			unifysrc = n.src;
+			if(!argncompat(n, t.ids, right)){
+				allok = 0;
+				break;
+			}
+			(okp, tp) := tunify(left.ty, calltype(left.ty, right, n.ty));
+			if(!okp){
+				nerror(n, "function call type mismatch (" + typeconv(left.ty)+" vs "+typeconv(calltype(left.ty, right, n.ty))+")");
+				allok = 0;
+			}
+			else{
+				(n.ty, tp) = expandtype(n.ty, nil, nil, tp);
+				n.ty = usetype(n.ty);
+				if(ispoly(callee) && tt != nil && (tt.kind == Tadt || tt.kind == Tadtpick) && int (tt.flags&INST))
+					callee = rewcall(left, callee);
+				n.right = passfns(n.src, callee, left, right, tt, tp);
+			}
+		}
+		else if(!argcompat(n, t.ids, right))
+			allok = 0;
+	Odot =>
+		t = left.ty;
+		if(t.kind == Tref)
+			t = t.tof;
+		case t.kind{
+		Tadt or
+		Tadtpick or
+		Ttuple or
+		Texception or
+		Tpoly =>
+			id = namedot(t.ids, right.decl.sym);
+			if(id == nil){
+				id = namedot(t.tags, right.decl.sym);
+				if(id != nil && !valistype(left)){
+					nerror(n, expconv(left)+" is not a type");
+					return (0, 0);
+				}
+			}
+			if(id == nil){
+				id = namedot(t.polys, right.decl.sym);
+				if(id != nil && !valistype(left)){
+					nerror(n, expconv(left)+" is not a type");
+					return (0, 0);
+				}
+			}
+			if(id == nil && t.kind == Tadtpick)
+				id = namedot(t.decl.dot.ty.ids, right.decl.sym);
+			if(id == nil){
+				for(tg = t.tags; tg != nil; tg = tg.next){
+					id = namedot(tg.ty.ids, right.decl.sym);
+					if(id != nil)
+						break;
+				}
+				if(id != nil){
+					nerror(n, "cannot yet index field "+right.decl.sym.name+" of "+etconv(left));
+					return (0, 0);
+				}
+			}
+			if(id == nil)
+				break;
+			if(id.store == Dfield && valistype(left)){
+				nerror(n, expconv(left)+" is not a value");
+				return (0, 0);
+			}
+			id.ty = validtype(id.ty, t.decl);
+			id.ty = usetype(id.ty);
+			break;
+		* =>
+			nerror(left, etconv(left)+" cannot be qualified with .");
+			return (0, 0);
+		}
+		if(id == nil){
+			nerror(n, expconv(right)+" is not a member of "+etconv(left));
+			return (0, 0);
+		}
+		if(id.ty == tunknown){
+			nerror(n, "illegal forward reference to "+expconv(n));
+			return (0, 0);
+		}
+
+		increfs(id);
+		right.decl = id;
+		n.ty = id.ty;
+		if((id.store == Dconst || id.store == Dtag) && hasside(left, 1))
+			nwarn(left, "result of expression "+etconv(left)+" ignored");
+		# function name as function reference
+		if(id.store == Dfn && (par == nil || (par.op != Omdot && par.op != Ocall && par.op != Ofunc)))
+			fnref(n, id);
+	Omdot =>
+		t = left.ty;
+		if(t.kind != Tmodule){
+			nerror(left, etconv(left)+" cannot be qualified with ->");
+			return (0, 0);
+		}
+		id = nil;
+		if(right.op == Oname){
+			id = namedot(t.ids, right.decl.sym);
+		}else if(right.op == Odot){
+			(ok, kidsok) = echeck(right, 0, isglobal, n);
+			allok &= kidsok;
+			if(!ok)
+				return (0, 0);
+			tt = right.left.ty;
+			if(tt.kind == Tref)
+				tt = tt.tof;
+			if(right.ty.kind == Tfn
+			&& tt.kind == Tadt
+			&& tt.decl.dot == t.decl)
+				id = right.right.decl;
+		}
+		if(id == nil){
+			nerror(n, expconv(right)+" is not a member of "+etconv(left));
+			return (0, 0);
+		}
+		if(id.store != Dconst && id.store != Dtype && id.store != Dtag){
+			if(valistype(left)){
+				nerror(n, expconv(left)+" is not a value");
+				return (0, 0);
+			}
+		}else if(hasside(left, 1))
+			nwarn(left, "result of expression "+etconv(left)+" ignored");
+		if(!typeok && id.store == Dtype){
+			nerror(n, expconv(n)+" is a type, not a value");
+			return (0, 0);
+		}
+		if(id.ty == tunknown){
+			nerror(n, "illegal forward reference to "+expconv(n));
+			return (0, 0);
+		}
+		id.refs++;
+		right.decl = id;
+		n.ty = id.ty = usetype(id.ty);
+		if(id.store == Dglobal)
+			modrefable(id.ty);
+		# function name as function reference
+		if(id.store == Dfn && (par == nil || (par.op != Ocall && par.op != Ofunc)))
+			fnref(n, id);
+	Otagof =>
+		n.ty = tint;
+		t = left.ty;
+		if(t.kind == Tref)
+			t = t.tof;
+		id = nil;
+		case left.op{
+		Oname =>
+			id = left.decl;
+		Odot =>
+			id = left.right.decl;
+		Omdot =>
+			if(left.right.op == Odot)
+				id = left.right.right.decl;
+		}
+		if(id != nil && id.store == Dtag
+		|| id != nil && id.store == Dtype && t.kind == Tadt && t.tags != nil)
+			n.decl = id;
+		else if(t.kind == Tadt && t.tags != nil || t.kind == Tadtpick)
+			n.decl = nil;
+		else{
+			nerror(n, "cannot get the tag value for "+etconv(left));
+			return (1, 0);
+		}
+	Oind =>
+		t = left.ty;
+		if(t.kind != Tref || (t.tof.kind != Tadt && t.tof.kind != Tadtpick && t.tof.kind != Ttuple)){
+			nerror(n, "cannot * "+etconv(left));
+			return (0, 0);
+		}
+		n.ty = t.tof;
+		for(tg = t.tof.tags; tg != nil; tg = tg.next)
+			tg.ty.tof = usetype(tg.ty.tof);
+	Oindex =>
+		if(valistype(left)){
+			tagopt = 1;
+			(nil, kidsok) = echeck(right, 2, isglobal, n);
+			tagopt = 0;
+			if(!kidsok)
+				return (0, 0);
+			if((t = exptotype(n)) == nil){
+				nerror(n, expconv(right) + " is not a type list");
+				return (0, 0);
+			}
+			if(!typeok){
+				nerror(n, expconv(left) + " is not a variable");
+				return (0, 0);
+			}
+			*n = *(n.left);
+			n.ty = usetype(t);
+			break;
+		}
+		if(0 && right.op == Oseq){		# a[e1, e2, ...]
+			# array creation to do before we allow this
+			rewind(n);
+			return echeck(n, typeok, isglobal, par);
+		}
+		t = left.ty;
+		(nil, kidsok) = echeck(right, 0, isglobal, n);
+		if(t.kind != Tarray && t != tstring){
+			nerror(n, "cannot index "+etconv(left));
+			return (0, 0);
+		}
+		if(t == tstring){
+			n.op = Oinds;
+			n.ty = tint;
+		}else{
+			n.ty = t.tof;
+		}
+		if(!kidsok){
+			allok = 0;
+			break;
+		}
+		if(right.ty != tint){
+			nerror(n, "cannot index "+etconv(left)+" with "+etconv(right));
+			allok = 0;
+			break;
+		}
+	Oslice =>
+		t = n.ty = left.ty;
+		if(t.kind != Tarray && t != tstring){
+			nerror(n, "cannot slice "+etconv(left)+" with '"+subexpconv(right.left)+":"+subexpconv(right.right)+"'");
+			return (0, 0);
+		}
+		if(right.left.ty != tint && right.left.op != Onothing
+		|| right.right.ty != tint && right.right.op != Onothing){
+			nerror(n, "cannot slice "+etconv(left)+" with '"+subexpconv(right.left)+":"+subexpconv(right.right)+"'");
+			return (1, 0);
+		}
+	Olen =>
+		t = left.ty;
+		n.ty = tint;
+		if(t.kind != Tarray && t.kind != Tlist && t != tstring){
+			nerror(n, "len requires an array, string or list in "+etconv(left));
+			return (1, 0);
+		}
+	Ocomp or
+	Onot or
+	Oneg =>
+		n.ty = left.ty;
+usedty(n.ty);
+		case left.ty.kind{
+		Tint =>
+			return (1, allok);
+		Treal or
+		Tfix =>
+			if(n.op == Oneg)
+				return (1, allok);
+		Tbig or
+		Tbyte =>
+			if(n.op == Oneg || n.op == Ocomp)
+				return (1, allok);
+		}
+		nerror(n, "cannot apply "+opconv(n.op)+" to "+etconv(left));
+		return (0, 0);
+	Oinc or
+	Odec or
+	Opreinc or
+	Opredec =>
+		n.ty = left.ty;
+		case left.ty.kind{
+		Tint or
+		Tbig or
+		Tbyte or
+		Treal =>
+			break;
+		* =>
+			nerror(n, "cannot apply "+opconv(n.op)+" to "+etconv(left));
+			return (0, 0);
+		}
+		if(islval(left))
+			break;
+		return(0, 0);
+	Oadd or
+	Odiv or
+	Omul or
+	Osub =>
+		if(mathchk(n, 1))
+			break;
+		return (0, 0);
+	Oexp or
+	Oexpas =>
+		n.ty = left.ty;
+		if(n.ty != tint && n.ty != tbig && n.ty != treal){
+			nerror(n, "exponend " + etconv(left) + " is not int or real");
+			return (0, 0);
+		}
+		if(right.ty != tint){
+			nerror(n, "exponent " + etconv(right) + " is not int");
+			return (0, 0);
+		}
+		if(n.op == Oexpas && !islval(left))
+			return (0, 0);
+		break;
+		# if(mathchk(n, 0)){
+		# 	if(n.ty != tint){
+		# 		nerror(n, "exponentiation operands not int");
+		# 		return (0, 0);
+		# 	}
+		# 	break;
+		# }
+		# return (0, 0);
+	Olsh or
+	Orsh =>
+		if(shiftchk(n))
+			break;
+		return (0, 0);
+	Oandand or
+	Ooror =>
+		if(left.ty != tint){
+			nerror(n, opconv(n.op)+"'s left operand is not an int: "+etconv(left));
+			allok = 0;
+		}
+		if(right.ty != tint){
+			nerror(n, opconv(n.op)+"'s right operand is not an int: "+etconv(right));
+			allok = 0;
+		}
+		n.ty = tint;
+	Oand or
+	Omod or
+	Oor or
+	Oxor =>
+		if(mathchk(n, 0))
+			break;
+		return (0, 0);
+	Oaddas or
+	Odivas or
+	Omulas or
+	Osubas =>
+		if(mathchk(n, 1) && islval(left))
+			break;
+		return (0, 0);
+	Olshas or
+	Orshas =>
+		if(shiftchk(n) && islval(left))
+			break;
+		return (0, 0);
+	Oandas or
+	Omodas or
+	Oxoras or
+	Ooras =>
+		if(mathchk(n, 0) && islval(left))
+			break;
+		return (0, 0);
+	Olt or
+	Oleq or
+	Ogt or
+	Ogeq =>
+		if(!mathchk(n, 1))
+			return (0, 0);
+		n.ty = tint;
+	Oeq or
+	Oneq =>
+		case left.ty.kind{
+		Tint or
+		Tbig or
+		Tbyte or
+		Treal or
+		Tstring or
+		Tref or
+		Tlist or
+		Tarray or
+		Tchan or
+		Tany or
+		Tmodule or
+		Tfix or
+		Tpoly =>
+			if(!tcompat(left.ty, right.ty, 0) && !tcompat(right.ty, left.ty, 0))
+				break;
+			t = left.ty;
+			if(t == tany)
+				t = right.ty;
+			if(t == tany)
+				t = tint;
+			if(left.ty == tany)
+				left.ty = t;
+			if(right.ty == tany)
+				right.ty = t;
+			n.ty = tint;
+usedty(n.ty);
+			return (1, allok);
+		}
+		nerror(n, "cannot compare "+etconv(left)+" to "+etconv(right));
+		return (0, 0);
+	Otype =>
+		if(!typeok){
+			nerror(n, expconv(n) + " is not a variable");
+			return (0, 0);
+		}
+		n.ty = usetype(n.ty);
+	* =>
+		fatal("unknown op in typecheck: "+opconv(n.op));
+	}
+usedty(n.ty);
+	return (1, allok);
+}
+
+#
+# n is syntactically a call, but n.left is not a fn
+# check if it's the contructor for an adt
+#
+callcast(n: ref Node, kidsok, allok: int): (int, int)
+{
+	id: ref Decl;
+
+	left := n.left;
+	right := n.right;
+	id = nil;
+	case left.op{
+	Oname =>
+		id = left.decl;
+	Omdot =>
+		if(left.right.op == Odot)
+			id = left.right.right.decl;
+		else
+			id = left.right.decl;
+	Odot =>
+		id = left.right.decl;
+	}
+	if(id == nil || (id.store != Dtype && id.store != Dtag && id.ty.kind != Texception)){
+		nerror(left, expconv(left)+" is not a function or type name");
+		return (0, 0);
+	}
+	if(id.store == Dtag)
+		return tagcast(n, left, right, id, kidsok, allok);
+	t := left.ty;
+	n.ty = t;
+	if(!kidsok)
+		return (1, 0);
+
+	if(t.kind == Tref)
+		t = t.tof;
+	tt := mktype(n.src.start, n.src.stop, Ttuple, nil, tuplefields(right));
+	if(t.kind == Tadt && tcompat(t, tt, 1)){
+		if(right == nil)
+			*n = *n.left;
+		return (1, allok);
+	}
+
+	# try an exception with args
+	tt = mktype(n.src.start, n.src.stop, Texception, nil, tuplefields(right));
+	tt.cons = byte 1;
+	if(t.kind == Texception && t.cons == byte 1 && tcompat(t, tt, 1)){
+		if(right == nil)
+			*n = *n.left;
+		return (1, allok);
+	}
+
+	# try a cast
+	if(t.kind != Texception && right != nil && right.right == nil){	# Oseq but single expression
+		right = right.left;
+		n.op = Ocast;
+		n.left = right;
+		n.right = nil;
+		n.ty = mkidtype(n.src, id.sym);
+		return echeck(n, 0, 0, nil);
+	}
+
+	nerror(left, "cannot make a "+expconv(left)+" from '("+subexpconv(right)+")'");
+	return (0, 0);
+}
+
+tagcast(n, left, right: ref Node, id: ref Decl, kidsok, allok: int): (int, int)
+{
+	left.ty = id.ty;
+	if(left.op == Omdot)
+		left.right.ty = id.ty;
+	n.ty = id.ty;
+	if(!kidsok)
+		return (1, 0);
+	id.ty.tof = usetype(id.ty.tof);
+	if(right != nil)
+		right.ty = id.ty.tof;
+	tt := mktype(n.src.start, n.src.stop, Ttuple, nil, mkids(nosrc, nil, tint, tuplefields(right)));
+	tt.ids.store = Dfield;
+	if(tcompat(id.ty.tof, tt, 1))
+		return (1, allok);
+
+	nerror(left, "cannot make a "+expconv(left)+" from '("+subexpconv(right)+")'");
+	return (0, 0);
+}
+
+valistype(n: ref Node): int
+{
+	case n.op{
+	Oname =>
+		if(n.decl.store == Dtype)
+			return 1;
+	Omdot =>
+		return valistype(n.right);
+	}
+	return 0;
+}
+
+islval(n: ref Node): int
+{
+	s := marklval(n);
+	if(s == 1)
+		return 1;
+	if(s == 0)
+		nerror(n, "cannot assign to "+expconv(n));
+	else
+		circlval(n, n);
+	return 0;
+}
+
+#
+# check to see if n is an lval
+#
+marklval(n: ref Node): int
+{
+	if(n == nil)
+		return 0;
+	case n.op{
+	Oname =>
+		return storespace[n.decl.store] && n.ty.kind != Texception;	#ZZZZ && n.decl.tagged == nil;
+	Odot =>
+		if(n.right.decl.store != Dfield)
+			return 0;
+		if(n.right.decl.cycle != byte 0 && n.right.decl.cyc == byte 0)
+			return -1;
+		if(n.left.ty.kind != Tref && marklval(n.left) == 0)
+			nwarn(n, "assignment to "+etconv(n)+" ignored");
+		return 1;
+	Omdot =>
+		if(n.right.decl.store == Dglobal)
+			return 1;
+		return 0;
+	Oind =>
+		for(id := n.ty.ids; id != nil; id = id.next)
+			if(id.cycle != byte 0 && id.cyc == byte 0)
+				return -1;
+		return 1;
+	Oslice =>
+		if(n.right.right.op != Onothing || n.ty == tstring)
+			return 0;
+		return 1;
+	Oinds =>
+		#
+		# make sure we don't change a string constant
+		#
+		case n.left.op{
+		Oconst =>
+			return 0;
+		Oname =>
+			return storespace[n.left.decl.store];
+		Odot or
+		Omdot =>
+			if(n.left.right.decl != nil)
+				return storespace[n.left.right.decl.store];
+		}
+		return 1;
+	Oindex or
+	Oindx =>
+		return 1;
+	Otuple =>
+		for(nn := n.left; nn != nil; nn = nn.right){
+			s := marklval(nn.left);
+			if(s != 1)
+				return s;
+		}
+		return 1;
+	* =>
+		return 0;
+	}
+	return 0;
+}
+
+#
+# n has a circular field assignment.
+# find it and print an error message.
+#
+circlval(n, lval: ref Node): int
+{
+	if(n == nil)
+		return 0;
+	case n.op{
+	Oname =>
+		break;
+	Odot =>
+		if(oldcycles && n.right.decl.cycle != byte 0 && n.right.decl.cyc == byte 0){
+			nerror(lval, "cannot assign to "+expconv(lval)+" because field '"+n.right.decl.sym.name
+					+"' of "+expconv(n.left)+" could complete a cycle to "+expconv(n.left));
+			return -1;
+		}
+		return 1;
+	Oind =>
+		for(id := n.ty.ids; id != nil; id = id.next){
+			if(oldcycles && id.cycle != byte 0 && id.cyc == byte 0){
+				nerror(lval, "cannot assign to "+expconv(lval)+" because field '"+id.sym.name
+					+"' of "+expconv(n)+" could complete a cycle to "+expconv(n));
+				return -1;
+			}
+		}
+		return 1;
+	Oslice =>
+		if(n.right.right.op != Onothing || n.ty == tstring)
+			return 0;
+		return 1;
+	Oindex or
+	Oinds or
+	Oindx =>
+		return 1;
+	Otuple =>
+		for(nn := n.left; nn != nil; nn = nn.right){
+			s := circlval(nn.left, lval);
+			if(s != 1)
+				return s;
+		}
+		return 1;
+	* =>
+		return 0;
+	}
+	return 0;
+}
+
+mathchk(n: ref Node, realok: int): int
+{
+	lt := n.left.ty;
+	rt := n.right.ty;
+	if(rt != lt && !tequal(lt, rt)){
+		nerror(n, "type clash in "+etconv(n.left)+" "+opconv(n.op)+" "+etconv(n.right));
+		return 0;
+	}
+	n.ty = rt;
+	case rt.kind{
+	Tint or
+	Tbig or
+	Tbyte =>
+		return 1;
+	Tstring =>
+		case n.op{
+		Oadd or
+		Oaddas or
+		Ogt or
+		Ogeq or
+		Olt or
+		Oleq =>
+			return 1;
+		}
+	Treal or
+	Tfix =>
+		if(realok)
+			return 1;
+	}
+	nerror(n, "cannot "+opconv(n.op)+" "+etconv(n.left)+" and "+etconv(n.right));
+	return 0;
+}
+
+shiftchk(n: ref Node): int
+{
+	right := n.right;
+	left := n.left;
+	n.ty = left.ty;
+	case n.ty.kind{
+	Tint or
+	Tbyte or
+	Tbig =>
+		if(right.ty.kind != Tint){
+			nerror(n, "shift "+etconv(right)+" is not an int");
+			return 0;
+		}
+		return 1;
+	}
+	nerror(n, "cannot "+opconv(n.op)+" "+etconv(left)+" by "+etconv(right));
+	return 0;
+}
+
+#
+# check for any tany's in t
+#
+specific(t: ref Type): int
+{
+	if(t == nil)
+		return 0;
+	case t.kind{
+	Terror or
+	Tnone or
+	Tint or
+	Tbig or
+	Tstring or
+	Tbyte or
+	Treal or
+	Tfn or
+	Tadt or
+	Tadtpick or
+	Tmodule or
+	Tfix =>
+		return 1;
+	Tany =>
+		return 0;
+	Tpoly =>
+		return 1;
+	Tref or
+	Tlist or
+	Tarray or
+	Tchan =>
+		return specific(t.tof);
+	Ttuple or
+	Texception =>
+		for(d := t.ids; d != nil; d = d.next)
+			if(!specific(d.ty))
+				return 0;
+		return 1;
+	}
+	fatal("unknown type in specific: "+typeconv(t));
+	return 0;
+}
+
+#
+# infer the type of all variable in n from t
+# n is the left-hand exp of a := exp
+#
+declasinfer(n: ref Node, t: ref Type): int
+{
+	if(t.kind == Texception){
+		if(int t.cons)
+			return 0;
+		t = mkextuptype(t);
+	}
+	case n.op{
+	Otuple =>
+		if(t.kind != Ttuple && t.kind != Tadt && t.kind != Tadtpick)
+			return 0;
+		ok := 1;
+		n.ty = t;
+		n = n.left;
+		ids := t.ids;
+		if(t.kind == Tadtpick)
+			ids = t.tof.ids.next;
+		for(; n != nil && ids != nil; ids = ids.next){
+			if(ids.store != Dfield)
+				continue;
+			ok &= declasinfer(n.left, ids.ty);
+			n = n.right;
+		}
+		for(; ids != nil; ids = ids.next)
+			if(ids.store == Dfield)
+				break;
+		if(n != nil || ids != nil)
+			return 0;
+		return ok;
+	Oname =>
+		topvartype(t, n.decl, 0, 0);
+		if(n.decl == nildecl)
+			return 1;
+		n.decl.ty = t;
+		n.ty = t;
+		shareloc(n.decl);
+		return 1;
+	}
+	fatal("unknown op in declasinfer: "+nodeconv(n));
+	return 0;
+}
+
+#
+# an error occured in declaring n;
+# set all decl identifiers to Dwundef
+# so further errors are squashed.
+#
+declaserr(n: ref Node)
+{
+	case n.op{
+	Otuple =>
+		for(n = n.left; n != nil; n = n.right)
+			declaserr(n.left);
+		return;
+	Oname =>
+		if(n.decl != nildecl)
+			n.decl.store = Dwundef;
+		return;
+	}
+	fatal("unknown op in declaserr: "+nodeconv(n));
+}
+
+argcompat(n: ref Node, f: ref Decl, a: ref Node): int
+{
+	for(; a != nil; a = a.right){
+		if(f == nil){
+			nerror(n, expconv(n.left)+": too many function arguments");
+			return 0;
+		}
+		if(!tcompat(f.ty, a.left.ty, 0)){
+			nerror(n, expconv(n.left)+": argument type mismatch: expected "+typeconv(f.ty)+" saw "+etconv(a.left));
+			return 0;
+		}
+		if(a.left.ty == tany)
+			a.left.ty = f.ty;
+		f = f.next;
+	}
+	if(f != nil){
+		nerror(n, expconv(n.left)+": too few function arguments");
+		return 0;
+	}
+	return 1;
+}
+
+argncompat(n: ref Node, f: ref Decl, a: ref Node): int
+{
+	for(; a != nil; a = a.right){
+		if(f == nil){
+			nerror(n, expconv(n.left)+": too many function arguments");
+			return 0;
+		}
+		f = f.next;
+	}
+	if(f != nil){
+		nerror(n, expconv(n.left)+": too few function arguments");
+		return 0;
+	}
+	return 1;
+}
+
+#
+# fn is Odot(adt, methid)
+# pass adt implicitly if needed
+# if not, any side effect of adt will be ingored
+#
+passimplicit(fname, args: ref Node): ref Node
+{
+	t := fname.ty;
+	n := fname.left;
+	if(t.ids == nil || t.ids.implicit == byte 0){
+		if(!isfnrefty(t) && hasside(n, 1))
+			nwarn(fname, "result of expression "+expconv(n)+" ignored");
+		return args;
+	}
+	if(n.op == Oname && n.decl.store == Dtype){
+		nerror(n, expconv(n)+" is a type and cannot be a self argument");
+		n = mkn(Onothing, nil, nil);
+		n.src = fname.src;
+		n.ty = t.ids.ty;
+	}
+	args = mkn(Oseq, n, args);
+	args.src = n.src;
+	return args;
+}
+
+mem(t: ref Type, d: ref Decl): int
+{
+	for( ; d != nil; d = d.next)
+		if(d.ty == t)	# was if(d.ty == t || tequal(d.ty, t))
+			return 1;
+	return 0;
+}
+
+memp(t: ref Type, f: ref Decl): int
+{
+	return mem(t, f.ty.polys) || mem(t, encpolys(f));
+}
+
+passfns0(src: Src, fun: ref Decl, args0: ref Node, args: ref Node, a: ref Node, tp: ref Tpair, polys: ref Decl): (ref Node, ref Node)
+{
+	id, idt, idf: ref Decl;
+	sym: ref Sym;
+	tt: ref Type;
+	na, mod: ref Node;
+
+	for(idt = polys; idt != nil; idt = idt.next){
+		tt = valtmap(idt.ty, tp);
+		if(tt.kind == Tpoly && fndec != nil && !memp(tt, fndec))
+			error(src.start, "cannot determine the instantiated type of " + typeconv(tt));
+		for(idf = idt.ty.ids; idf != nil; idf = idf.next){
+			sym = idf.sym;
+			(id, mod) = fnlookup(sym, tt);
+			while(id != nil && id.link != nil)
+				id = id.link;
+			if(id == nil)	# error flagged already
+				continue;
+			id.refs++;
+			id.inline = byte -1;
+			if(tt.kind == Tmodule){	# mod an actual parameter
+				for(;;){
+					if(args0 != nil && tequal(tt, args0.left.ty)){
+						mod = args0.left;
+						break;
+					}
+					if(args0 != nil)
+						args0 = args0.right;
+				}
+			}
+			if(mod == nil && (dot := lmodule(id)) != nil && !isimpmod(dot.sym))
+				error(src.start, "cannot use " + id.sym.name + " without importing " + id.dot.sym.name + " from a variable");
+
+			n := mkn(Ofnptr, mod, mkdeclname(src, id));
+			n.src = src;
+			n.decl = fun;
+			if(tt.kind == Tpoly)
+				n.flags = byte FNPTRA;
+			else
+				n.flags = byte 0;
+			na = mkn(Oseq, n, nil);
+			if(a == nil)
+				args = na;
+			else
+				a.right = na;
+
+			n = mkn(Ofnptr, mod, mkdeclname(src, id));
+			n.src = src;
+			n.decl = fun;
+			if(tt.kind == Tpoly)
+				n.flags = byte (FNPTRA|FNPTR2);
+			else
+				n.flags = byte FNPTR2;
+			a = na.right = mkn(Oseq, n, nil);
+		}
+		if(args0 != nil)
+			args0 = args0.right;
+	}
+	return (args, a);
+}
+
+passfns(src: Src, fun: ref Decl, left: ref Node, args: ref Node, adtt: ref Type, tp: ref Tpair): ref Node
+{
+	a, args0: ref Node;
+
+	a = nil;
+	args0 = args;
+	if(args != nil)
+		for(a = args; a.right != nil; a = a.right)
+			;
+	if(ispoly(fun))
+		polys := fun.ty.polys;
+	else
+		polys = left.ty.tof.polys;
+	(args, a) = passfns0(src, fun, args0, args, a, tp, polys);
+	if(adtt != nil){
+		if(ispoly(fun))
+			polys = encpolys(fun);
+		else
+			polys = nil;
+		(args, a) = passfns0(src, fun, args0, args, a, adtt.tmap, polys);
+	}
+	return args;	
+}
+
+#
+# check the types for a function with a variable number of arguments
+# last typed argument must be a constant string, and must use the
+# print format for describing arguments.
+#
+mkvarargs(n, args: ref Node): ref Type
+{
+	last: ref Decl;
+
+	nt := copytypeids(n.ty);
+	n.ty = nt;
+	f := n.ty.ids;
+	last = nil;
+	if(f == nil){
+		nerror(n, expconv(n)+"'s type is illegal");
+		return nt;
+	}
+	s := args;
+	for(a := args; a != nil; a = a.right){
+		if(f == nil)
+			break;
+		if(!tcompat(f.ty, a.left.ty, 0)){
+			nerror(n, expconv(n)+": argument type mismatch: expected "+typeconv(f.ty)+" saw "+etconv(a.left));
+			return nt;
+		}
+		if(a.left.ty == tany)
+			a.left.ty = f.ty;
+		last = f;
+		f = f.next;
+		s = a;
+	}
+	if(f != nil){
+		nerror(n, expconv(n)+": too few function arguments");
+		return nt;
+	}
+	s.left = fold(s.left);
+	s = s.left;
+	if(s.ty != tstring || s.op != Oconst){
+		nerror(args, expconv(n)+": format argument "+etconv(s)+" is not a string constant");
+		return nt;
+	}
+	fmtcheck(n, s, a);
+	va := tuplefields(a);
+	if(last == nil)
+		nt.ids = va;
+	else
+		last.next = va;
+	return nt;
+}
+
+#
+# check that a print style format string matches it's arguments
+#
+fmtcheck(f, fmtarg, va: ref Node)
+{
+	fmt := fmtarg.decl.sym;
+	s := fmt.name;
+	ns := 0;
+	while(ns < len s){
+		c := s[ns++];
+		if(c != '%')
+			continue;
+
+		verb := -1;
+		n1 := 0;
+		n2 := 0;
+		dot := 0;
+		flag := 0;
+		flags := "";
+		fmtstart := ns - 1;
+		while(ns < len s && verb < 0){
+			c = s[ns++];
+			case c{
+			* =>
+				nerror(f, expconv(f)+": invalid character "+s[ns-1:ns]+" in format '"+s[fmtstart:ns]+"'");
+				return;
+			'.' =>
+				if(dot){
+					nerror(f, expconv(f)+": invalid format '"+s[fmtstart:ns]+"'");
+					return;
+				}
+				n1 = 1;
+				dot = 1;
+				continue;
+			'*' =>
+				if(!n1)
+					n1 = 1;
+				else if(!n2 && dot)
+					n2 = 1;
+				else{
+					nerror(f, expconv(f)+": invalid format '"+s[fmtstart:ns]+"'");
+					return;
+				}
+				if(va == nil){
+					nerror(f, expconv(f)+": too few arguments for format '"+s[fmtstart:ns]+"'");
+					return;
+				}
+				if(va.left.ty.kind != Tint){
+					nerror(f, expconv(f)+": format '"+s[fmtstart:ns]+"' incompatible with argument "+etconv(va.left));
+					return;
+				}
+				va = va.right;
+			'0' to '9' =>
+				while(ns < len s && s[ns] >= '0' && s[ns] <= '9')
+					ns++;
+				if(!n1)
+					n1 = 1;
+				else if(!n2 && dot)
+					n2 = 1;
+				else{
+					nerror(f, expconv(f)+": invalid format '"+s[fmtstart:ns]+"'");
+					return;
+				}
+			'+' or
+			'-' or
+			'#' or
+			',' or
+			'b' or
+			'u' =>
+				for(i := 0; i < flag; i++){
+					if(flags[i] == c){
+						nerror(f, expconv(f)+": duplicate flag "+s[ns-1:ns]+" in format '"+s[fmtstart:ns]+"'");
+						return;
+					}
+				}
+				flags[flag++] = c;
+			'%' or
+			'r' =>
+				verb = Tnone;
+			'H' =>
+				verb = Tany;
+			'c' =>
+				verb = Tint;
+			'd' or
+			'o' or
+			'x' or
+			'X' =>
+				verb = Tint;
+				for(i := 0; i < flag; i++){
+					if(flags[i] == 'b'){
+						verb = Tbig;
+						break;
+					}
+				}
+			'e' or
+			'f' or
+			'g' or
+			'E' or
+			'G' =>
+				verb = Treal;
+			's' or
+			'q' =>
+				verb = Tstring;
+			}
+		}
+		if(verb != Tnone){
+			if(verb < 0){
+				nerror(f, expconv(f)+": incomplete format '"+s[fmtstart:ns]+"'");
+				return;
+			}
+			if(va == nil){
+				nerror(f, expconv(f)+": too few arguments for format '"+s[fmtstart:ns]+"'");
+				return;
+			}
+			ty := va.left.ty;
+			if(ty.kind == Texception)
+				ty = mkextuptype(ty);
+			case verb{
+			Tint =>
+				case ty.kind{
+				Tstring or
+				Tarray or
+				Tref or
+				Tchan or
+				Tlist or
+				Tmodule =>
+					if(c == 'x' || c == 'X')
+						verb = ty.kind;
+				}
+			Tany =>
+				if(tattr[ty.kind].isptr)
+					verb = ty.kind;
+			}
+			if(verb != ty.kind){
+				nerror(f, expconv(f)+": format '"+s[fmtstart:ns]+"' incompatible with argument "+etconv(va.left));
+				return;
+			}
+			va = va.right;
+		}
+	}
+	if(va != nil)
+		nerror(f, expconv(f)+": more arguments than formats");
+}
+
+tuplefields(n: ref Node): ref Decl
+{
+	h, last: ref Decl;
+
+	for(; n != nil; n = n.right){
+		d := mkdecl(n.left.src, Dfield, n.left.ty);
+		if(h == nil)
+			h = d;
+		else
+			last.next = d;
+		last = d;
+	}
+	return h;
+}
+
+#
+# make explicit indices for every element in an array initializer
+# return the maximum index
+# sort the indices and check for duplicates
+#
+assignindices(ar: ref Node): int
+{
+	wild, off, q: ref Node;
+
+	amax := 16r7fffffff;
+	size := dupn(0, nosrc, ar.left);
+	if(size.ty == tint){
+		size = fold(size);
+		if(size.op == Oconst)
+			amax = int size.c.val;
+	}
+
+	inits := ar.right;
+	max := -1;
+	last := -1;
+	t := inits.left.ty;
+	wild = nil;
+	nlab := 0;
+	ok := 1;
+	for(n := inits; n != nil; n = n.right){
+		if(!tcompat(t,  n.left.ty, 0)){
+			t = tparent(t, n.left.ty);
+			if(!tcompat(t, n.left.ty, 0)){
+				nerror(n.left, "inconsistent types "+typeconv(t)+" and "+typeconv(n.left.ty)+" in array initializer");
+				return -1;
+			}
+			else
+				inits.left.ty = t;
+		}
+		if(t == tany)
+			t = n.left.ty;
+
+		#
+		# make up an index if there isn't one
+		#
+		if(n.left.left == nil)
+			n.left.left = mkn(Oseq, mkconst(n.left.right.src, big(last + 1)), nil);
+
+		for(q = n.left.left; q != nil; q = q.right){
+			off = q.left;
+			if(off.ty != tint){
+				nerror(off, "array index "+etconv(off)+" is not an int");
+				ok = 0;
+				continue;
+			}
+			off = fold(off);
+			case off.op{
+			Owild =>
+				if(wild != nil)
+					nerror(off, "array index * duplicated on line "+lineconv(wild.src.start));
+				wild = off;
+				continue;
+			Orange =>
+				if(off.left.op != Oconst || off.right.op != Oconst){
+					nerror(off, "range "+expconv(off)+" is not constant");
+					off = nil;
+				}else if(off.left.c.val < big 0 || off.right.c.val >= big amax){
+					nerror(off, "array index "+expconv(off)+" out of bounds");
+					off = nil;
+				}else
+					last = int off.right.c.val;
+			Oconst =>
+				last = int off.c.val;
+				if(off.c.val < big 0 || off.c.val >= big amax){
+					nerror(off, "array index "+expconv(off)+" out of bounds");
+					off = nil;
+				}
+			Onothing =>
+				# get here from a syntax error
+				off = nil;
+			* =>
+				nerror(off, "array index "+expconv(off)+" is not constant");
+				off = nil;
+			}
+
+			nlab++;
+			if(off == nil){
+				off = mkconst(n.left.right.src, big(last));
+				ok = 0;
+			}
+			if(last > max)
+				max = last;
+			q.left = off;
+		}
+	}
+
+	#
+	# fix up types of nil elements
+	#
+	for(n = inits; n != nil; n = n.right)
+		if(n.left.ty == tany)
+			n.left.ty = t;
+
+	if(!ok)
+		return -1;
+
+	c := checklabels(inits, tint, nlab, "array index");
+	t = mktype(inits.src.start, inits.src.stop, Tainit, nil, nil);
+	inits.ty = t;
+	t.cse = c;
+
+	return max + 1;
+}
+
+#
+# check the labels of a case statment
+#
+casecheck(cn: ref Node, ret: ref Type)
+{
+	wild: ref Node;
+
+	(rok, nil) := echeck(cn.left, 0, 0, nil);
+	cn.right = scheck(cn.right, ret, Sother);
+	if(!rok)
+		return;
+	arg := cn.left;
+
+	t := arg.ty;
+	if(t != tint && t != tbig && t != tstring){
+		nerror(cn, "case argument "+etconv(arg)+" is not an int or big or string");
+		return;
+	}
+
+	wild = nil;
+	nlab := 0;
+	ok := 1;
+	for(n := cn.right; n != nil; n = n.right){
+		q := n.left.left;
+		if(n.left.right.right == nil)
+			nwarn(q, "no body for case qualifier "+expconv(q));
+		for(; q != nil; q = q.right){
+			left := fold(q.left);
+			q.left = left;
+			case left.op{
+			Owild =>
+				if(wild != nil)
+					nerror(left, "case qualifier * duplicated on line "+lineconv(wild.src.start));
+				wild = left;
+			Orange =>
+				if(left.ty != t)
+					nerror(left, "case qualifier "+etconv(left)+" clashes with "+etconv(arg));
+				else if(left.left.op != Oconst || left.right.op != Oconst){
+					nerror(left, "case range "+expconv(left)+" is not constant");
+					ok = 0;
+				}
+				nlab++;
+			* =>
+				if(left.ty != t){
+					nerror(left, "case qualifier "+etconv(left)+" clashes with "+etconv(arg));
+					ok = 0;
+				}else if(left.op != Oconst){
+					nerror(left, "case qualifier "+expconv(left)+" is not constant");
+					ok = 0;
+				}
+				nlab++;
+			}
+		}
+	}
+
+	if(!ok)
+		return;
+
+	c := checklabels(cn.right, t, nlab, "case qualifier");
+	op := Tcase;
+	if(t == tbig)
+		op = Tcasel;
+	else if(t == tstring)
+		op = Tcasec;
+	t = mktype(cn.src.start, cn.src.stop, op, nil, nil);
+	cn.ty = t;
+	t.cse = c;
+}
+
+#
+# check the labels and bodies of a pick statment
+#
+pickcheck(n: ref Node, ret: ref Type)
+{
+	qs, q, w: ref Node;
+
+	arg := n.left.right;
+	(nil, allok) := echeck(arg, 0, 0, nil);
+	if(!allok)
+		return;
+	t := arg.ty;
+	if(t.kind == Tref)
+		t = t.tof;
+	if(arg.ty.kind != Tref || t.kind != Tadt || t.tags == nil){
+		nerror(arg, "pick argument "+etconv(arg)+" is not a ref adt with pick tags");
+		return;
+	}
+	argty := usetype(mktype(arg.ty.src.start, arg.ty.src.stop, Tref, t, nil));
+
+	arg = n.left.left;
+	pushscope(nil, Sother);
+	dasdecl(arg);
+	arg.decl.ty = argty;
+	arg.ty = argty;
+
+	tags := array[t.decl.tag] of ref Node;
+	w = nil;
+	ok := 1;
+	nlab := 0;
+	for(qs = n.right; qs != nil; qs = qs.right){
+		qt : ref Node = nil;
+		for(q = qs.left.left; q != nil; q = q.right){
+			left := q.left;
+			case left.op{
+			Owild =>
+				# left.ty = tnone;
+				left.ty = t;
+				if(w != nil)
+					nerror(left, "pick qualifier * duplicated on line "+lineconv(w.src.start));
+				w = left;
+			Oname =>
+				id := namedot(t.tags, left.decl.sym);
+				if(id == nil){
+					nerror(left, "pick qualifier "+expconv(left)+" is not a member of "+etconv(arg));
+					ok = 0;
+					continue;
+				}
+
+				left.decl = id;
+				left.ty = id.ty;
+
+				if(tags[id.tag] != nil){
+					nerror(left, "pick qualifier "+expconv(left)+" duplicated on line "+lineconv(tags[id.tag].src.start));
+					ok = 0;
+				}
+				tags[id.tag] = left;
+				nlab++;
+			* =>
+				fatal("pickcheck can't handle "+nodeconv(q));
+			}
+
+			if(qt == nil)
+				qt = left;
+			else if(!tequal(qt.ty, left.ty))
+				nerror(left, "type clash in pick qualifiers "+etconv(qt)+" and "+etconv(left));
+		}
+
+		argty.tof = t;
+		if(qt != nil)
+			argty.tof = qt.ty;
+		qs.left.right = scheck(qs.left.right, ret, Sother);
+		if(qs.left.right == nil)
+			nwarn(qs.left.left, "no body for pick qualifier "+expconv(qs.left.left));
+	}
+	argty.tof = t;
+	for(qs = n.right; qs != nil; qs = qs.right)
+		for(q = qs.left.left; q != nil; q = q.right)
+			q.left = fold(q.left);
+
+	d := popscope();
+	d.refs++;
+	if(d.next != nil)
+		fatal("pickcheck: installing more than one id");
+	fndecls = appdecls(fndecls, d);
+
+	if(!ok)
+		return;
+
+	c := checklabels(n.right, tint, nlab, "pick qualifier");
+	t = mktype(n.src.start, n.src.stop, Tcase, nil, nil);
+	n.ty = t;
+	t.cse = c;
+}
+
+exccheck(en: ref Node, ret: ref Type)
+{
+	ed: ref Decl;
+	wild: ref Node;
+	qt: ref Type;
+
+	pushscope(nil, Sother);
+	if(en.left == nil)
+		en.left = mkdeclname(en.src, mkids(en.src, enter(".ex"+string nexc++, 0), texception, nil));
+	oinexcept := inexcept;
+	inexcept = en.left;
+	dasdecl(en.left);
+	en.left.ty = en.left.decl.ty = texception;
+	ed = en.left.decl;
+	# en.right = scheck(en.right, ret, Sother);
+	t := tstring;
+	wild = nil;
+	nlab := 0;
+	ok := 1;
+	for(n := en.right; n != nil; n = n.right){
+		qt = nil;
+		for(q := n.left.left; q != nil; q = q.right){
+			left := q.left;
+			case left.op{
+			Owild =>
+				left.ty = texception;
+				if(wild != nil)
+					nerror(left, "exception qualifier * duplicated on line "+lineconv(wild.src.start));
+				wild = left;
+			Orange =>
+				left.ty = tnone;
+				nerror(left, "exception qualifier "+expconv(left)+" is illegal");
+				ok = 0;
+			* =>
+				(rok, nil) := echeck(left, 0, 0, nil);
+				if(!rok){
+					ok = 0;
+					break;
+				}
+				left = q.left = fold(left);
+				if(left.ty != t && left.ty.kind != Texception){
+					nerror(left, "exception qualifier "+etconv(left)+" is not a string or exception");
+					ok = 0;
+				}else if(left.op != Oconst){
+					nerror(left, "exception qualifier "+expconv(left)+" is not constant");
+					ok = 0;
+				}
+				else if(left.ty != t)
+					left.ty = mkextype(left.ty);
+				nlab++;
+			}
+
+			if(qt == nil)
+				qt = left.ty;
+			else if(!tequal(qt, left.ty))
+				qt = texception;
+		}
+
+		if(qt != nil)
+			ed.ty = qt;
+		n.left.right = scheck(n.left.right, ret, Sother);
+		if(n.left.right.right == nil)
+			nwarn(n.left.left, "no body for exception qualifier " + expconv(n.left.left));
+	}
+	ed.ty = texception;
+	inexcept = oinexcept;
+	if(!ok)
+		return;
+	c := checklabels(en.right, texception, nlab, "exception qualifier");
+	t = mktype(en.src.start, en.src.stop, Texcept, nil, nil);
+	en.ty = t;
+	t.cse = c;
+	ed = popscope();
+	fndecls = appdecls(fndecls, ed);
+}
+
+#
+# check array and case labels for validity
+#
+checklabels(inits: ref Node, ctype: ref Type, nlab: int, title: string): ref Case
+{
+	n, q, wild: ref Node;
+
+	labs := array[nlab] of Label;
+	i := 0;
+	wild = nil;
+	for(n = inits; n != nil; n = n.right){
+		for(q = n.left.left; q != nil; q = q.right){
+			case q.left.op{
+			Oconst =>
+				labs[i].start = q.left;
+				labs[i].stop = q.left;
+				labs[i++].node = n.left;
+			Orange =>
+				labs[i].start = q.left.left;
+				labs[i].stop = q.left.right;
+				labs[i++].node = n.left;
+			Owild =>
+				wild = n.left;
+			* =>
+				fatal("bogus index in checklabels");
+			}
+		}
+	}
+
+	if(i != nlab)
+		fatal("bad label count: "+string nlab+" then "+string i);
+
+	casesort(ctype, array[nlab] of Label, labs, 0, nlab);
+	for(i = 0; i < nlab; i++){
+		p := labs[i].stop;
+		if(casecmp(ctype, labs[i].start, p) > 0)
+			nerror(labs[i].start, "unmatchable "+title+" "+expconv(labs[i].node));
+		for(e := i + 1; e < nlab; e++){
+			if(casecmp(ctype, labs[e].start, p) <= 0)
+				nerror(labs[e].start, title+" '"+eprintlist(labs[e].node.left, " or ")
+					+"' overlaps with '"+eprintlist(labs[e-1].node.left, " or ")+"' on line "
+					+lineconv(p.src.start));
+
+			#
+			# check for merging case labels
+			#
+			if(ctype != tint
+			|| labs[e].start.c.val != p.c.val+big 1
+			|| labs[e].node != labs[i].node)
+				break;
+			p = labs[e].stop;
+		}
+		if(e != i + 1){
+			labs[i].stop = p;
+			labs[i+1:] = labs[e:nlab];
+			nlab -= e - (i + 1);
+		}
+	}
+
+	c := ref Case;
+	c.nlab = nlab;
+	c.nsnd = 0;
+	c.labs = labs;
+	c.wild = wild;
+
+	return c;
+}
+
+symcmp(a: ref Sym, b: ref Sym): int
+{
+	if(a.name < b.name)
+		return -1;
+	if(a.name > b.name)
+		return 1;
+	return 0;
+}
+
+matchcmp(na: ref Node, nb: ref Node): int
+{
+	a := na.decl.sym;
+	b := nb.decl.sym;
+	la := len a.name;
+	lb := len b.name;
+	sa := la > 0 && a.name[la-1] == '*';
+	sb := lb > 0 && b.name[lb-1] == '*';
+	if(sa){
+		if(sb){
+			if(la == lb)
+				return symcmp(a, b);
+			return lb-la;
+		}
+		else
+			return 1;
+	}
+	else{
+		if(sb)
+			return -1;
+		else{
+			if(na.ty == tstring){
+				if(nb.ty == tstring)
+					return symcmp(a, b);
+				else
+					return 1;
+			}
+			else{
+				if(nb.ty == tstring)
+					return -1;
+				else
+					return symcmp(a, b);
+			}
+		}
+	}
+}
+
+casecmp(ty: ref Type, a, b: ref Node): int
+{
+	if(ty == tint || ty == tbig){
+		if(a.c.val < b.c.val)
+			return -1;
+		if(a.c.val > b.c.val)
+			return 1;
+		return 0;
+	}
+	if(ty == texception)
+		return matchcmp(a, b);
+	return symcmp(a.decl.sym, b.decl.sym);
+}
+
+casesort(t: ref Type, aux, labs: array of Label, start, stop: int)
+{
+	n := stop - start;
+	if(n <= 1)
+		return;
+	top := mid := start + n / 2;
+
+	casesort(t, aux, labs, start, top);
+	casesort(t, aux, labs, mid, stop);
+
+	#
+	# merge together two sorted label arrays, yielding a sorted array
+	#
+	n = 0;
+	base := start;
+	while(base < top && mid < stop){
+		if(casecmp(t, labs[base].start, labs[mid].start) <= 0)
+			aux[n++] = labs[base++];
+		else
+			aux[n++] = labs[mid++];
+	}
+	if(base < top)
+		aux[n:] = labs[base:top];
+	else if(mid < stop)
+		aux[n:] = labs[mid:stop];
+	labs[start:] = aux[:stop-start];
+}
+
+#
+# binary search for the label corresponding to a given value
+#
+findlab(ty: ref Type, v: ref Node, labs: array of Label, nlab: int): int
+{
+	if(nlab <= 1)
+		return 0;
+	m : int;
+	l := 1;
+	r := nlab - 1;
+	while(l <= r){
+		m = (r + l) / 2;
+		if(casecmp(ty, labs[m].start, v) <= 0)
+			l = m + 1;
+		else
+			r = m - 1;
+	}
+	m = l - 1;
+	if(casecmp(ty, labs[m].start, v) > 0
+	|| casecmp(ty, labs[m].stop, v) < 0)
+		fatal("findlab out of range");
+	return m;
+}
+
+altcheck(an: ref Node, ret: ref Type)
+{
+	n, q, left, op, wild: ref Node;
+
+	an.left = scheck(an.left, ret, Sother);
+
+	ok := 1;
+	nsnd := 0;
+	nrcv := 0;
+	wild = nil;
+	for(n = an.left; n != nil; n = n.right){
+		q = n.left.right.left;
+		if(n.left.right.right == nil)
+			nwarn(q, "no body for alt guard "+expconv(q));
+		for(; q != nil; q = q.right){
+			left = q.left;
+			case left.op{
+			Owild =>
+				if(wild != nil)
+					nerror(left, "alt guard * duplicated on line "+lineconv(wild.src.start));
+				wild = left;
+			Orange =>
+				nerror(left, "alt guard "+expconv(left)+" is illegal");
+				ok = 0;
+			* =>
+				op = hascomm(left);
+				if(op == nil){
+					nerror(left, "alt guard "+expconv(left)+" has no communication");
+					ok = 0;
+					break;
+				}
+				if(op.op == Osnd)
+					nsnd++;
+				else
+					nrcv++;
+			}
+		}
+	}
+
+	if(!ok)
+		return;
+
+	c := ref Case;
+	c.nlab = nsnd + nrcv;
+	c.nsnd = nsnd;
+	c.wild = wild;
+
+	an.ty = mktalt(c);
+}
+
+hascomm(n: ref Node): ref Node
+{
+	if(n == nil)
+		return nil;
+	if(n.op == Osnd || n.op == Orcv)
+		return n;
+	r := hascomm(n.left);
+	if(r != nil)
+		return r;
+	return hascomm(n.right);
+}
+
+raisescheck(t: ref Type)
+{
+	if(t.kind != Tfn)
+		return;
+	n := t.eraises;
+	for(nn := n.left; nn != nil; nn = nn.right){
+		(ok, nil) := echeck(nn.left, 0, 0, nil);
+		if(ok && nn.left.ty.kind != Texception)
+			nerror(n, expconv(nn.left) + ": illegal raises expression");
+	}
+}
+
+Elist: adt{
+	d: ref Decl;
+	nxt: cyclic ref Elist;
+};
+
+emerge(el1: ref Elist, el2: ref Elist): ref Elist
+{
+	f: int;
+	el, nxt: ref Elist;
+
+	for( ; el1 != nil; el1 = nxt){
+		f = 0;
+		for(el = el2; el != nil; el = el.nxt){
+			if(el1.d == el.d){
+				f = 1;
+				break;
+			}
+		}
+		nxt = el1.nxt;
+		if(!f){
+			el1.nxt = el2;
+			el2 = el1;
+		}
+	}
+	return el2;
+}
+
+equals(n: ref Node): ref Elist
+{
+	q, nn: ref Node;
+	e, el: ref Elist;
+
+	el = nil;
+	for(q = n.left.left; q != nil; q = q.right){
+		nn = q.left;
+		if(nn.op == Owild)
+			return nil;
+		if(nn.ty.kind != Texception)
+			continue;
+		e = ref Elist(nn.decl, el);
+		el = e;
+	}
+	return el;
+}
+
+caught(d: ref Decl, n: ref Node): int
+{
+	q, nn: ref Node;
+
+	for(n = n.right; n != nil; n = n.right){
+		for(q = n.left.left; q != nil; q = q.right){
+			nn = q.left;
+			if(nn.op == Owild)
+				return 1;
+			if(nn.ty.kind != Texception)
+				continue;
+			if(d == nn.decl)
+				return 1;
+		}
+	}
+	return 0;
+}
+
+raisecheck(n: ref Node, ql: ref Elist): ref Elist
+{
+	exc: int;
+	e: ref Node;
+	el, nel, nxt: ref Elist;
+
+	if(n == nil)
+		return nil;
+	el = nil;
+	for(; n != nil; n = n.right){
+		case(n.op){
+		Oscope =>
+			return raisecheck(n.right, ql);
+		Olabel or
+		Odo =>
+			return raisecheck(n.right, ql);
+		Oif or
+		Ofor =>
+			return emerge(raisecheck(n.right.left, ql),
+					        raisecheck(n.right.right, ql));
+		Oalt or
+		Ocase or
+		Opick or
+		Oexcept =>
+			exc = n.op == Oexcept;
+			for(n = n.right; n != nil; n = n.right){
+				ql = nil;
+				if(exc)
+					ql = equals(n);
+				el = emerge(raisecheck(n.left.right, ql), el);
+			}
+			return el;
+		Oseq =>
+			el = emerge(raisecheck(n.left, ql), el);
+			break;
+		Oexstmt =>
+			el = raisecheck(n.left, ql);
+			nel = nil;
+			for( ; el != nil; el = nxt){
+				nxt = el.nxt;
+				if(!caught(el.d, n.right)){
+					el.nxt = nel;
+					nel = el;
+				}
+			}		
+			return emerge(nel, raisecheck(n.right, ql));
+		Oraise =>
+			e = n.left;
+			if(e.ty != nil && e.ty.kind == Texception){
+				if(e.ty.cons == byte 0)
+					return ql;
+				if(e.op == Ocall)
+					e = e.left;
+				if(e.op == Omdot)
+					e = e.right;
+				if(e.op != Oname)
+					fatal("exception " + nodeconv(e) + " not a name");
+				el = ref Elist(e.decl, nil);
+				return el;
+			}
+			return nil;
+		* =>
+			return nil;
+		}
+	}
+	return el;
+}
+
+checkraises(n: ref Node)
+{
+	f: int;
+	d: ref Decl;
+	e, el: ref Elist;
+	es, nn: ref Node;
+
+	el = raisecheck(n.right, nil);
+	es = n.ty.eraises;
+	if(es != nil){
+		for(nn = es.left; nn != nil; nn = nn.right){
+			d = nn.left.decl;
+			f = 0;
+			for(e = el; e != nil; e = e.nxt){
+				if(d == e.d){
+					f = 1;
+					e.d = nil;
+					break;
+				}
+			}
+			if(!f)
+				nwarn(n, "function " + expconv(n.left) + " does not raise " + d.sym.name + " but declared");
+		}
+	}
+	for(e = el; e != nil; e = e.nxt)
+		if(e.d != nil)
+			nwarn(n, "function " + expconv(n.left) + " raises " + e.d.sym.name + " but not declared");
+}
+
+# sort all globals in modules now that we've finished with 'last' pointers
+# and before any code generation
+#
+gsort(n: ref Node)
+{
+	for(;;){
+		if(n == nil)
+			return;
+		if(n.op != Oseq)
+			break;
+		gsort(n.left);
+		n = n.right;
+	}
+	if(n.op == Omoddecl && int (n.ty.ok & OKverify)){
+		n.ty.ids = namesort(n.ty.ids);
+		sizeids(n.ty.ids, 0);
+	}
+}
--- /dev/null
+++ b/appl/cmd/limbo/types.b
@@ -1,0 +1,4234 @@
+
+kindname := array [Tend] of
+{
+	Tnone =>	"no type",
+	Tadt =>		"adt",
+	Tadtpick =>	"adt",
+	Tarray =>	"array",
+	Tbig =>		"big",
+	Tbyte =>	"byte",
+	Tchan =>	"chan",
+	Treal =>	"real",
+	Tfn =>		"fn",
+	Tint =>		"int",
+	Tlist =>	"list",
+	Tmodule =>	"module",
+	Tref =>		"ref",
+	Tstring =>	"string",
+	Ttuple =>	"tuple",
+	Texception => "exception",
+	Tfix => "fixed point",
+	Tpoly => "polymorphic",
+
+	Tainit =>	"array initializers",
+	Talt =>		"alt channels",
+	Tany =>		"polymorphic type",
+	Tarrow =>	"->",
+	Tcase =>	"case int labels",
+	Tcasel =>	"case big labels",
+	Tcasec =>	"case string labels",
+	Tdot =>		".",
+	Terror =>	"type error",
+	Tgoto =>	"goto labels",
+	Tid =>		"id",
+	Tiface =>	"module interface",
+	Texcept =>	"exception handler table",
+	Tinst =>	"instantiated type",
+};
+
+tattr = array[Tend] of
+{
+	#		     isptr	refable	conable	big	vis
+	Tnone =>	Tattr(0,	0,	0,	0,	0),
+	Tadt =>		Tattr(0,	1,	1,	1,	1),
+	Tadtpick =>	Tattr(0,	1,	0,	1,	1),
+	Tarray =>	Tattr(1,	0,	0,	0,	1),
+	Tbig =>		Tattr(0,	0,	1,	1,	1),
+	Tbyte =>	Tattr(0,	0,	1,	0,	1),
+	Tchan =>	Tattr(1,	0,	0,	0,	1),
+	Treal =>	Tattr(0,	0,	1,	1,	1),
+	Tfn =>		Tattr(0,	1,	0,	0,	1),
+	Tint =>		Tattr(0,	0,	1,	0,	1),
+	Tlist =>	Tattr(1,	0,	0,	0,	1),
+	Tmodule =>	Tattr(1,	0,	0,	0,	1),
+	Tref =>		Tattr(1,	0,	0,	0,	1),
+	Tstring =>	Tattr(1,	0,	1,	0,	1),
+	Ttuple =>	Tattr(0,	1,	1,	1,	1),
+	Texception => Tattr(0,	0,	0,	1,	1),
+	Tfix =>		Tattr(0,	0,	1,	0,	1),
+	Tpoly =>	Tattr(1,	0,	0,	0,	1),
+
+	Tainit =>	Tattr(0,	0,	0,	1,	0),
+	Talt =>		Tattr(0,	0,	0,	1,	0),
+	Tany =>		Tattr(1,	0,	0,	0,	0),
+	Tarrow =>	Tattr(0,	0,	0,	0,	1),
+	Tcase =>	Tattr(0,	0,	0,	1,	0),
+	Tcasel =>	Tattr(0,	0,	0,	1,	0),
+	Tcasec =>	Tattr(0,	0,	0,	1,	0),
+	Tdot =>		Tattr(0,	0,	0,	0,	1),
+	Terror =>	Tattr(0,	1,	1,	0,	0),
+	Tgoto =>	Tattr(0,	0,	0,	1,	0),
+	Tid =>		Tattr(0,	0,	0,	0,	1),
+	Tiface =>	Tattr(0,	0,	0,	1,	0),
+	Texcept =>	Tattr(0,	0,	0,	1,	0),
+	Tinst =>	Tattr(0,	1,	1,	1,	1),
+};
+
+eqclass:	array of ref Teq;
+
+ztype:		Type;
+eqrec:		int;
+eqset:		int;
+adts:		array of ref Decl;
+nadts:		int;
+anontupsym:	ref Sym;
+unifysrc:	Src;
+
+addtmap(t1: ref Type, t2: ref Type, tph: ref Tpair): ref Tpair
+{
+	tp: ref Tpair;
+
+	tp = ref Tpair;
+	tp.t1 = t1;
+	tp.t2 = t2;
+	tp.nxt = tph;
+	return tp;
+}
+
+valtmap(t: ref Type, tp: ref Tpair): ref Type
+{
+	for( ; tp != nil; tp = tp.nxt)
+		if(tp.t1 == t)
+			return tp.t2;
+	return t;
+}
+
+addtype(t: ref Type, hdl: ref Typelist): ref Typelist
+{
+	tll := ref Typelist;
+	tll.t = t;
+	tll.nxt = nil;
+	if(hdl == nil)
+		return tll;
+	for(p := hdl; p.nxt != nil; p = p.nxt)
+		;
+	p.nxt = tll;
+	return hdl;
+}
+
+typeinit()
+{
+	anontupsym = enter(".tuple", 0);
+
+	ztype.sbl = -1;
+	ztype.ok = byte 0;
+	ztype.rec = byte 0;
+
+	tbig = mktype(noline, noline, Tbig, nil, nil);
+	tbig.size = IBY2LG;
+	tbig.align = IBY2LG;
+	tbig.ok = OKmask;
+
+	tbyte = mktype(noline, noline, Tbyte, nil, nil);
+	tbyte.size = 1;
+	tbyte.align = 1;
+	tbyte.ok = OKmask;
+
+	tint = mktype(noline, noline, Tint, nil, nil);
+	tint.size = IBY2WD;
+	tint.align = IBY2WD;
+	tint.ok = OKmask;
+
+	treal = mktype(noline, noline, Treal, nil, nil);
+	treal.size = IBY2FT;
+	treal.align = IBY2FT;
+	treal.ok = OKmask;
+
+	tstring = mktype(noline, noline, Tstring, nil, nil);
+	tstring.size = IBY2WD;
+	tstring.align = IBY2WD;
+	tstring.ok = OKmask;
+
+	texception = mktype(noline, noline, Texception, nil, nil);
+	texception.size = IBY2WD;
+	texception.align = IBY2WD;
+	texception.ok = OKmask;
+
+	tany = mktype(noline, noline, Tany, nil, nil);
+	tany.size = IBY2WD;
+	tany.align = IBY2WD;
+	tany.ok = OKmask;
+
+	tnone = mktype(noline, noline, Tnone, nil, nil);
+	tnone.size = 0;
+	tnone.align = 1;
+	tnone.ok = OKmask;
+
+	terror = mktype(noline, noline, Terror, nil, nil);
+	terror.size = 0;
+	terror.align = 1;
+	terror.ok = OKmask;
+
+	tunknown = mktype(noline, noline, Terror, nil, nil);
+	tunknown.size = 0;
+	tunknown.align = 1;
+	tunknown.ok = OKmask;
+
+	tfnptr = mktype(noline, noline, Ttuple, nil, nil);
+	id := tfnptr.ids = mkids(nosrc, nil, tany, nil);
+	id.store = Dfield;
+	id.offset = 0;
+	id.sym = enter("t0", 0);
+	id.src = Src(0, 0);
+	id = tfnptr.ids.next = mkids(nosrc, nil, tint, nil);
+	id.store = Dfield;
+	id.offset = IBY2WD;
+	id.sym = enter("t1", 0);
+	id.src = Src(0, 0);
+
+	rtexception = mktype(noline, noline, Tref, texception, nil);
+	rtexception.size = IBY2WD;
+	rtexception.align = IBY2WD;
+	rtexception.ok = OKmask;
+}
+
+typestart()
+{
+	descriptors = nil;
+	nfns = 0;
+	adts = nil;
+	nadts = 0;
+	selfdecl = nil;
+	if(tfnptr.decl != nil)
+		tfnptr.decl.desc = nil;
+
+	eqclass = array[Tend] of ref Teq;
+
+	typebuiltin(mkids(nosrc, enter("int", 0), nil, nil), tint);
+	typebuiltin(mkids(nosrc, enter("big", 0), nil, nil), tbig);
+	typebuiltin(mkids(nosrc, enter("byte", 0), nil, nil), tbyte);
+	typebuiltin(mkids(nosrc, enter("string", 0), nil, nil), tstring);
+	typebuiltin(mkids(nosrc, enter("real", 0), nil, nil), treal);
+}
+
+modclass(): ref Teq
+{
+	return eqclass[Tmodule];
+}
+
+mktype(start: Line, stop: Line, kind: int, tof: ref Type, args: ref Decl): ref Type
+{
+	t := ref ztype;
+	t.src.start = start;
+	t.src.stop = stop;
+	t.kind = kind;
+	t.tof = tof;
+	t.ids = args;
+	return t;
+}
+
+nalt: int;
+mktalt(c: ref Case): ref Type
+{
+	t := mktype(noline, noline, Talt, nil, nil);
+	t.decl = mkdecl(nosrc, Dtype, t);
+	t.decl.sym = enter(".a"+string nalt++, 0);
+	t.cse = c;
+	return usetype(t);
+}
+
+#
+# copy t and the top level of ids
+#
+copytypeids(t: ref Type): ref Type
+{
+	last: ref Decl;
+
+	nt := ref *t;
+	for(id := t.ids; id != nil; id = id.next){
+		new := ref *id;
+		if(last == nil)
+			nt.ids = new;
+		else
+			last.next = new;
+		last = new;
+	}
+	return nt;
+}
+
+#
+# make each of the ids have type t
+#
+typeids(ids: ref Decl, t: ref Type): ref Decl
+{
+	if(ids == nil)
+		return nil;
+
+	ids.ty = t;
+	for(id := ids.next; id != nil; id = id.next)
+		id.ty = t;
+	return ids;
+}
+
+typebuiltin(d: ref Decl, t: ref Type)
+{
+	d.ty = t;
+	t.decl = d;
+	installids(Dtype, d);
+}
+
+fielddecl(store: int, ids: ref Decl): ref Node
+{
+	n := mkn(Ofielddecl, nil, nil);
+	n.decl = ids;
+	for(; ids != nil; ids = ids.next)
+		ids.store = store;
+	return n;
+}
+
+typedecl(ids: ref Decl, t: ref Type): ref Node
+{
+	if(t.decl == nil)
+		t.decl = ids;
+	n := mkn(Otypedecl, nil, nil);
+	n.decl = ids;
+	n.ty = t;
+	for(; ids != nil; ids = ids.next)
+		ids.ty = t;
+	return n;
+}
+
+typedecled(n: ref Node)
+{
+	installids(Dtype, n.decl);
+}
+
+adtdecl(ids: ref Decl, fields: ref Node): ref Node
+{
+	n := mkn(Oadtdecl, nil, nil);
+	t := mktype(ids.src.start, ids.src.stop, Tadt, nil, nil);
+	n.decl = ids;
+	n.left = fields;
+	n.ty = t;
+	t.decl = ids;
+	for(; ids != nil; ids = ids.next)
+		ids.ty = t;
+	return n;
+}
+
+adtdecled(n: ref Node)
+{
+	d := n.ty.decl;
+	installids(Dtype, d);
+	if(n.ty.polys != nil){
+		pushscope(nil, Sother);
+		installids(Dtype, n.ty.polys);
+	}
+	pushscope(nil, Sother);
+	fielddecled(n.left);
+	n.ty.ids = popscope();
+	if(n.ty.polys != nil)
+		n.ty.polys = popscope();
+	for(ids := n.ty.ids; ids != nil; ids = ids.next)
+		ids.dot = d;
+}
+
+fielddecled(n: ref Node)
+{
+	for(; n != nil; n = n.right){
+		case n.op{
+		Oseq =>
+			fielddecled(n.left);
+		Oadtdecl =>
+			adtdecled(n);
+			return;
+		Otypedecl =>
+			typedecled(n);
+			return;
+		Ofielddecl =>
+			installids(Dfield, n.decl);
+			return;
+		Ocondecl =>
+			condecled(n);
+			gdasdecl(n.right);
+			return;
+		Oexdecl =>
+			exdecled(n);
+			return;
+		Opickdecl =>
+			pickdecled(n);
+			return;
+		* =>
+			fatal("can't deal with "+opname[n.op]+" in fielddecled");
+		}
+	}
+}
+
+pickdecled(n: ref Node): int
+{
+	if(n == nil)
+		return 0;
+	tag := pickdecled(n.left);
+	pushscope(nil, Sother);
+	fielddecled(n.right.right);
+	d := n.right.left.decl;
+	d.ty.ids = popscope();
+	installids(Dtag, d);
+	for(; d != nil; d = d.next)
+		d.tag = tag++;
+	return tag;
+}
+
+#
+# make the tuple type used to initialize adt t
+#
+mkadtcon(t: ref Type): ref Type
+{
+	last: ref Decl;
+
+	nt := ref *t;
+	nt.ids = nil;
+	nt.kind = Ttuple;
+	for(id := t.ids; id != nil; id = id.next){
+		if(id.store != Dfield)
+			continue;
+		new := ref *id;
+		new.cyc = byte 0;
+		if(last == nil)
+			nt.ids = new;
+		else
+			last.next = new;
+		last = new;
+	}
+	last.next = nil;
+	return nt;
+}
+
+#
+# make the tuple type used to initialize t,
+# an adt with pick fields tagged by tg
+#
+mkadtpickcon(t, tgt: ref Type): ref Type
+{
+	last := mkids(tgt.decl.src, nil, tint, nil);
+	last.store = Dfield;
+	nt := mktype(t.src.start, t.src.stop, Ttuple, nil, last);
+	for(id := t.ids; id != nil; id = id.next){
+		if(id.store != Dfield)
+			continue;
+		new := ref *id;
+		new.cyc = byte 0;
+		last.next = new;
+		last = new;
+	}
+	for(id = tgt.ids; id != nil; id = id.next){
+		if(id.store != Dfield)
+			continue;
+		new := ref *id;
+		new.cyc = byte 0;
+		last.next = new;
+		last = new;
+	}
+	last.next = nil;
+	return nt;
+}
+
+#
+# make an identifier type
+#
+mkidtype(src: Src, s: ref Sym): ref Type
+{
+	t := mktype(src.start, src.stop, Tid, nil, nil);
+	if(s.unbound == nil){
+		s.unbound = mkdecl(src, Dunbound, nil);
+		s.unbound.sym = s;
+	}
+	t.decl = s.unbound;
+	return t;
+}
+
+#
+# make a qualified type for t->s
+#
+mkarrowtype(start: Line, stop: Line, t: ref Type, s: ref Sym): ref Type
+{
+	t = mktype(start, stop, Tarrow, t, nil);
+	if(s.unbound == nil){
+		s.unbound = mkdecl(Src(start, stop), Dunbound, nil);
+		s.unbound.sym = s;
+	}
+	t.decl = s.unbound;
+	return t;
+}
+
+#
+# make a qualified type for t.s
+#
+mkdottype(start: Line, stop: Line, t: ref Type, s: ref Sym): ref Type
+{
+	t = mktype(start, stop, Tdot, t, nil);
+	if(s.unbound == nil){
+		s.unbound = mkdecl(Src(start, stop), Dunbound, nil);
+		s.unbound.sym = s;
+	}
+	t.decl = s.unbound;
+	return t;
+}
+
+mkinsttype(src: Src, tt: ref Type, tyl: ref Typelist): ref Type
+{
+	t := mktype(src.start, src.stop, Tinst, tt, nil);
+	t.tlist = tyl;
+	return t;
+}
+
+#
+# look up the name f in the fields of a module, adt, or tuple
+#
+namedot(ids: ref Decl, s: ref Sym): ref Decl
+{
+	for(; ids != nil; ids = ids.next)
+		if(ids.sym == s)
+			return ids;
+	return nil;
+}
+
+#
+# complete the declaration of an adt
+# methods frames get sized in module definition or during function definition
+# place the methods at the end of the field list
+#
+adtdefd(t: ref Type)
+{
+	next, aux, store, auxhd, tagnext: ref Decl;
+
+	if(debug['x'])
+		print("adt %s defd\n", typeconv(t));
+	d := t.decl;
+	tagnext = nil;
+	store = nil;
+	for(id := t.polys; id != nil; id = id.next){
+		id.store = Dtype;
+		id.ty = verifytypes(id.ty, d, nil);
+	}
+	for(id = t.ids; id != nil; id = next){
+		if(id.store == Dtag){
+			if(t.tags != nil)
+				error(id.src.start, "only one set of pick fields allowed");
+			tagnext = pickdefd(t, id);
+			next = tagnext;
+			if(store != nil)
+				store.next = next;
+			else
+				t.ids = next;
+			continue;
+		}else{
+			id.dot = d;
+			next = id.next;
+			store = id;
+		}
+	}
+	aux = nil;
+	store = nil;
+	auxhd = nil;
+	seentags := 0;
+	for(id = t.ids; id != nil; id = next){
+		if(id == tagnext)
+			seentags = 1;
+
+		next = id.next;
+		id.dot = d;
+		id.ty = topvartype(verifytypes(id.ty, d, nil), id, 1, 1);
+		if(id.store == Dfield && id.ty.kind == Tfn)
+			id.store = Dfn;
+		if(id.store == Dfn || id.store == Dconst){
+			if(store != nil)
+				store.next = next;
+			else
+				t.ids = next;
+			if(aux != nil)
+				aux.next = id;
+			else
+				auxhd = id;
+			aux = id;
+		}else{
+			if(seentags)
+				error(id.src.start, "pick fields must be the last data fields in an adt");
+			store = id;
+		}
+	}
+	if(aux != nil)
+		aux.next = nil;
+	if(store != nil)
+		store.next = auxhd;
+	else
+		t.ids = auxhd;
+
+	for(id = t.tags; id != nil; id = id.next){
+		id.ty = verifytypes(id.ty, d, nil);
+		if(id.ty.tof == nil)
+			id.ty.tof = mkadtpickcon(t, id.ty);
+	}
+}
+
+#
+# assemble the data structure for an adt with a pick clause.
+# since the scoping rules for adt pick fields are strange,
+# we have a customized check for overlapping definitions.
+#
+pickdefd(t: ref Type, tg: ref Decl): ref Decl
+{
+	lasttg : ref Decl = nil;
+	d := t.decl;
+	t.tags = tg;
+	tag := 0;
+	while(tg != nil){
+		tt := tg.ty;
+		if(tt.kind != Tadtpick || tg.tag != tag)
+			break;
+		tt.decl = tg;
+		lasttg = tg;
+		for(; tg != nil; tg = tg.next){
+			if(tg.ty != tt)
+				break;
+			tag++;
+			lasttg = tg;
+			tg.dot = d;
+		}
+		for(id := tt.ids; id != nil; id = id.next){
+			xid := namedot(t.ids, id.sym);
+			if(xid != nil)
+				error(id.src.start, "redeclaration of "+declconv(id)+
+					" previously declared as "+storeconv(xid)+" on line "+lineconv(xid.src.start));
+			id.dot = d;
+		}
+	}
+	if(lasttg == nil){
+		error(t.src.start, "empty pick field declaration in "+typeconv(t));
+		t.tags = nil;
+	}else
+		lasttg.next = nil;
+	d.tag = tag;
+	return tg;
+}
+
+moddecl(ids: ref Decl, fields: ref Node): ref Node
+{
+	n := mkn(Omoddecl, mkn(Oseq, nil, nil), nil);
+	t := mktype(ids.src.start, ids.src.stop, Tmodule, nil, nil);
+	n.decl = ids;
+	n.left = fields;
+	n.ty = t;
+	return n;
+}
+
+moddecled(n: ref Node)
+{
+	d := n.decl;
+	installids(Dtype, d);
+	isimp := 0;
+	for(ids := d; ids != nil; ids = ids.next){
+		for(im := impmods; im != nil; im = im.next){
+			if(ids.sym == im.sym){
+				isimp = 1;
+				d = ids;
+				dm := ref Dlist;
+				dm.d = ids;
+				dm.next = nil;
+				if(impdecls == nil)
+					impdecls = dm;
+				else{
+					for(dl := impdecls; dl.next != nil; dl = dl.next)
+						;
+					dl.next = dm;
+				}
+			}
+		}
+		ids.ty = n.ty;
+	}
+	pushscope(nil, Sother);
+	fielddecled(n.left);
+
+	d.ty.ids = popscope();
+
+	#
+	# make the current module the . parent of all contained decls.
+	#
+	for(ids = d.ty.ids; ids != nil; ids = ids.next)
+		ids.dot = d;
+
+	t := d.ty;
+	t.decl = d;
+	if(debug['m'])
+		print("declare module %s\n", d.sym.name);
+
+	#
+	# add the iface declaration in case it's needed later
+	#
+	installids(Dglobal, mkids(d.src, enter(".m."+d.sym.name, 0), tnone, nil));
+
+	if(isimp){
+		for(ids = d.ty.ids; ids != nil; ids = ids.next){
+			s := ids.sym;
+			if(s.decl != nil && s.decl.scope >= scope){
+				dot := s.decl.dot;
+				if(s.decl.store != Dwundef && dot != nil && dot != d && isimpmod(dot.sym) && dequal(ids, s.decl, 0))
+					continue;
+				redecl(ids);
+				ids.old = s.decl.old;
+			}else
+				ids.old = s.decl;
+			s.decl = ids;
+			ids.scope = scope;
+		}
+	}
+}
+
+#
+# for each module in id,
+# link by field ext all of the decls for
+# functions needed in external linkage table
+# collect globals and make a tuple for all of them
+#
+mkiface(m: ref Decl): ref Type
+{
+	iface := last := ref Decl;
+	globals := glast := mkdecl(m.src, Dglobal, mktype(m.src.start, m.src.stop, Tadt, nil, nil));
+	for(id := m.ty.ids; id != nil; id = id.next){
+		case id.store{
+		Dglobal =>
+			glast = glast.next = dupdecl(id);
+			id.iface = globals;
+			glast.iface = id;
+		Dfn =>
+			id.iface = last = last.next = dupdecl(id);
+			last.iface = id;
+		Dtype =>
+			if(id.ty.kind != Tadt)
+				break;
+			for(d := id.ty.ids; d != nil; d = d.next){
+				if(d.store == Dfn){
+					d.iface = last = last.next = dupdecl(d);
+					last.iface = d;
+				}
+			}
+		}
+	}
+	last.next = nil;
+	iface = namesort(iface.next);
+
+	if(globals.next != nil){
+		glast.next = nil;
+		globals.ty.ids = namesort(globals.next);
+		globals.ty.decl = globals;
+		globals.sym = enter(".mp", 0);
+		globals.dot = m;
+		globals.next = iface;
+		iface = globals;
+	}
+
+	#
+	# make the interface type and install an identifier for it
+	# the iface has a ref count if it is loaded
+	#
+	t := mktype(m.src.start, m.src.stop, Tiface, nil, iface);
+	id = enter(".m."+m.sym.name, 0).decl;
+	t.decl = id;
+	id.ty = t;
+
+	#
+	# dummy node so the interface is initialized
+	#
+	id.init = mkn(Onothing, nil, nil);
+	id.init.ty = t;
+	id.init.decl = id;
+	return t;
+}
+
+joiniface(mt, t: ref Type)
+{
+	iface := t.ids;
+	globals := iface;
+	if(iface != nil && iface.store == Dglobal)
+		iface = iface.next;
+	for(id := mt.tof.ids; id != nil; id = id.next){
+		case id.store{
+		Dglobal =>
+			for(d := id.ty.ids; d != nil; d = d.next)
+				d.iface.iface = globals;
+		Dfn =>
+			id.iface.iface = iface;
+			iface = iface.next;
+		* =>
+			fatal("unknown store "+storeconv(id)+" in joiniface");
+		}
+	}
+	if(iface != nil)
+		fatal("join iface not matched");
+	mt.tof = t;
+}
+
+addiface(m: ref Decl, d: ref Decl)
+{
+	t: ref Type;
+	id, last, dd, lastorig: ref Decl;
+
+	if(d == nil || !local(d))
+		return;
+	modrefable(d.ty);
+	if(m == nil){
+		if(impdecls.next != nil)
+			for(dl := impdecls; dl != nil; dl = dl.next)
+				if(dl.d.ty.tof != impdecl.ty.tof)	# impdecl last
+					addiface(dl.d, d);
+		addiface(impdecl, d);
+		return;
+	}
+	t = m.ty.tof;
+	last = nil;
+	lastorig = nil;
+	for(id = t.ids; id != nil; id = id.next){
+		if(d == id || d == id.iface)
+			return;
+		last = id;
+		if(id.tag == 0)
+			lastorig = id;
+	}
+	dd = dupdecl(d);
+	if(d.dot == nil)
+		d.dot = dd.dot = m;
+	d.iface = dd;
+	dd.iface = d;
+	if(last == nil)
+		t.ids = dd;
+	else
+		last.next = dd;
+	dd.tag = 1;	# mark so not signed
+	if(lastorig == nil)
+		t.ids = namesort(t.ids);
+	else
+		lastorig.next = namesort(lastorig.next);
+}
+
+#
+# eliminate unused declarations from interfaces
+# label offset within interface
+#
+narrowmods()
+{
+	id: ref Decl;
+	for(eq := modclass(); eq != nil; eq = eq.eq){
+		t := eq.ty.tof;
+
+		if(t.linkall == byte 0){
+			last : ref Decl = nil;
+			for(id = t.ids; id != nil; id = id.next){
+				if(id.refs == 0){
+					if(last == nil)
+						t.ids = id.next;
+					else
+						last.next = id.next;
+				}else
+					last = id;
+			}
+
+			#
+			# need to resize smaller interfaces
+			#
+			resizetype(t);
+		}
+
+		offset := 0;
+		for(id = t.ids; id != nil; id = id.next)
+			id.offset = offset++;
+
+		#
+		# rathole to stuff number of entries in interface
+		#
+		t.decl.init.c = ref Const;
+		t.decl.init.c.val = big offset;
+	}
+}
+
+#
+# check to see if any data field of module m if referenced.
+# if so, mark all data in m
+#
+moddataref()
+{
+	for(eq := modclass(); eq != nil; eq = eq.eq){
+		id := eq.ty.tof.ids;
+		if(id != nil && id.store == Dglobal && id.refs)
+			for(id = eq.ty.ids; id != nil; id = id.next)
+				if(id.store == Dglobal)
+					modrefable(id.ty);
+	}
+}
+
+#
+# move the global declarations in interface to the front
+#
+modglobals(mod, globals: ref Decl): ref Decl
+{
+	#
+	# make a copy of all the global declarations
+	# 	used for making a type descriptor for globals ONLY
+	# note we now have two declarations for the same variables,
+	# which is apt to cause problems if code changes
+	#
+	# here we fix up the offsets for the real declarations
+	#
+	idoffsets(mod.ty.ids, 0, 1);
+
+	last := head := ref Decl;
+	for(id := mod.ty.ids; id != nil; id = id.next)
+		if(id.store == Dglobal)
+			last = last.next = dupdecl(id);
+
+	last.next = globals;
+	return head.next;
+}
+
+#
+# snap all id type names to the actual type
+# check that all types are completely defined
+# verify that the types look ok
+#
+validtype(t: ref Type, inadt: ref Decl): ref Type
+{
+	if(t == nil)
+		return t;
+	bindtypes(t);
+	t = verifytypes(t, inadt, nil);
+	cycsizetype(t);
+	teqclass(t);
+	return t;
+}
+
+usetype(t: ref Type): ref Type
+{
+	if(t == nil)
+		return t;
+	t = validtype(t, nil);
+	reftype(t);
+	return t;
+}
+
+internaltype(t: ref Type): ref Type
+{
+	bindtypes(t);
+	t.ok = OKverify;
+	sizetype(t);
+	t.ok = OKmask;
+	return t;
+}
+
+#
+# checks that t is a valid top-level type
+#
+topvartype(t: ref Type, id: ref Decl, tyok: int, polyok: int): ref Type
+{
+	if(t.kind == Tadt && t.tags != nil || t.kind == Tadtpick)
+		error(id.src.start, "cannot declare "+id.sym.name+" with type "+typeconv(t));
+	if(!tyok && t.kind == Tfn)
+		error(id.src.start, "cannot declare "+id.sym.name+" to be a function");
+	if(!polyok && (t.kind == Tadt || t.kind == Tadtpick) && ispolyadt(t))
+		error(id.src.start, "cannot declare " + id.sym.name + " of a polymorphic type");
+	return t;
+}
+
+toptype(src: Src, t: ref Type): ref Type
+{
+	if(t.kind == Tadt && t.tags != nil || t.kind == Tadtpick)
+		error(src.start, typeconv(t)+", an adt with pick fields, must be used with ref");
+	if(t.kind == Tfn)
+		error(src.start, "data cannot have a fn type like "+typeconv(t));
+	return t;
+}
+
+comtype(src: Src, t: ref Type, adtd: ref Decl): ref Type
+{
+	if(adtd == nil && (t.kind == Tadt || t.kind == Tadtpick) && ispolyadt(t))
+		error(src.start, "polymorphic type " + typeconv(t) + " illegal here");
+	return t;
+}
+
+usedty(t: ref Type)
+{
+	if(t != nil && (t.ok | OKmodref) != OKmask)
+		fatal("used ty " + stypeconv(t) + " " + hex(int t.ok, 2));
+}
+
+bindtypes(t: ref Type)
+{
+	id: ref Decl;
+
+	if(t == nil)
+		return;
+	if((t.ok & OKbind) == OKbind)
+		return;
+	t.ok |= OKbind;
+	case t.kind{
+	Tadt =>
+		if(t.polys != nil){
+			pushscope(nil, Sother);
+			installids(Dtype, t.polys);
+		}
+		if(t.val != nil)
+			mergepolydecs(t);
+		if(t.polys != nil){
+			popscope();
+			for(id = t.polys; id != nil; id = id.next)
+				bindtypes(id.ty);
+		}
+	Tadtpick or
+	Tmodule or
+	Terror or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tiface or
+	Tainit or
+	Talt or
+	Tcase or
+	Tcasel or
+	Tcasec or
+	Tgoto or
+	Texcept or
+	Tfix or
+	Tpoly =>
+		break;
+	Tarray or
+	Tarrow or
+	Tchan or
+	Tdot or
+	Tlist or
+	Tref =>
+		bindtypes(t.tof);
+	Tid =>
+		id = t.decl.sym.decl;
+		if(id == nil)
+			id = undefed(t.src, t.decl.sym);
+		# save a little space
+		id.sym.unbound = nil;
+		t.decl = id;
+	Ttuple or
+	Texception =>
+		for(id = t.ids; id != nil; id = id.next)
+			bindtypes(id.ty);
+	Tfn =>
+		if(t.polys != nil){
+			pushscope(nil, Sother);
+			installids(Dtype, t.polys);
+		}
+		for(id = t.ids; id != nil; id = id.next)
+			bindtypes(id.ty);
+		bindtypes(t.tof);
+		if(t.val != nil)
+			mergepolydecs(t);
+		if(t.polys != nil){
+			popscope();
+			for(id = t.polys; id != nil; id = id.next)
+				bindtypes(id.ty);
+		}
+	Tinst =>
+		bindtypes(t.tof);
+		for(tyl := t.tlist; tyl != nil; tyl = tyl.nxt)
+			bindtypes(tyl.t);
+	* =>
+		fatal("bindtypes: unknown type kind "+string t.kind);
+	}
+}
+
+#
+# walk the type checking for validity
+#
+verifytypes(t: ref Type, adtt: ref Decl, poly: ref Decl): ref Type
+{
+	id: ref Decl;
+
+	if(t == nil)
+		return nil;
+	if((t.ok & OKverify) == OKverify)
+		return t;
+	t.ok |= OKverify;
+if((t.ok & (OKverify|OKbind)) != (OKverify|OKbind))
+fatal("verifytypes bogus ok for " + stypeconv(t));
+	cyc := t.flags&CYCLIC;
+	case t.kind{
+	Terror or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tiface or
+	Tainit or
+	Talt or
+	Tcase or
+	Tcasel or
+	Tcasec or
+	Tgoto or
+	Texcept =>
+		break;
+	Tfix =>
+		n := t.val;
+		ok: int;
+		max := 0.0;
+		if(n.op == Oseq){
+			(ok, nil) = echeck(n.left, 0, 0, n);
+			(ok1, nil) := echeck(n.right, 0, 0, n);
+			if(!ok || !ok1)
+				return terror;
+			if(n.left.ty != treal || n.right.ty != treal){
+				error(t.src.start, "fixed point scale/maximum not real");
+				return terror;
+			}
+			n.right = fold(n.right);
+			if(n.right.op != Oconst){
+				error(t.src.start, "fixed point maximum not constant");
+				return terror;
+			}
+			if((max = n.right.c.rval) <= 0.0){
+				error(t.src.start, "non-positive fixed point maximum");
+				return terror;
+			}
+			n = n.left;
+		}
+		else{
+			(ok, nil) = echeck(n, 0, 0, nil);
+			if(!ok)
+				return terror;
+			if(n.ty != treal){
+				error(t.src.start, "fixed point scale not real");
+				return terror;
+			}
+		}
+		n = t.val = fold(n);
+		if(n.op != Oconst){
+			error(t.src.start, "fixed point scale not constant");
+			return terror;
+		}
+		if(n.c.rval <= 0.0){
+			error(t.src.start, "non-positive fixed point scale");
+			return terror;
+		}
+		ckfix(t, max);
+	Tref =>
+		t.tof = comtype(t.src, verifytypes(t.tof, adtt, nil), adtt);
+		if(t.tof != nil && !tattr[t.tof.kind].refable){
+			error(t.src.start, "cannot have a ref " + typeconv(t.tof));
+			return terror;
+		}
+		if(0 && t.tof.kind == Tfn && t.tof.ids != nil && int t.tof.ids.implicit)
+			error(t.src.start, "function references cannot have a self argument");
+		if(0 && t.tof.kind == Tfn && t.polys != nil)
+			error(t.src.start, "function references cannot be polymorphic");
+	Tchan or
+	Tarray or
+	Tlist =>
+		t.tof = comtype(t.src, toptype(t.src, verifytypes(t.tof, adtt, nil)), adtt);
+	Tid =>
+		t.ok &= ~OKverify;
+		t = verifytypes(idtype(t), adtt, nil);
+	Tarrow =>
+		t.ok &= ~OKverify;
+		t = verifytypes(arrowtype(t, adtt), adtt, nil);
+	Tdot =>
+		#
+		# verify the parent adt & lookup the tag fields
+		#
+		t.ok &= ~OKverify;
+		t = verifytypes(dottype(t, adtt), adtt, nil);
+	Tadt =>
+		#
+		# this is where Tadt may get tag fields added
+		#
+		adtdefd(t);
+	Tadtpick =>
+		for(id = t.ids; id != nil; id = id.next){
+			id.ty = topvartype(verifytypes(id.ty, id.dot, nil), id, 0, 1);
+			if(id.store == Dconst)
+				error(t.src.start, "cannot declare a con like "+id.sym.name+" within a pick");
+		}
+		verifytypes(t.decl.dot.ty, nil, nil);
+	Tmodule =>
+		for(id = t.ids; id != nil; id = id.next){
+			id.ty = verifytypes(id.ty, nil, nil);
+			if(id.store == Dglobal && id.ty.kind == Tfn)
+				id.store = Dfn;
+			if(id.store != Dtype && id.store != Dfn)
+				topvartype(id.ty, id, 0, 0);
+		}
+	Ttuple or
+	Texception =>
+		if(t.decl == nil){
+			t.decl = mkdecl(t.src, Dtype, t);
+			t.decl.sym = anontupsym;
+		}
+		i := 0;
+		for(id = t.ids; id != nil; id = id.next){
+			id.store = Dfield;
+			if(id.sym == nil)
+				id.sym = enter("t"+string i, 0);
+			i++;
+			id.ty = toptype(id.src, verifytypes(id.ty, adtt, nil));
+		}
+	Tfn =>
+		last : ref Decl = nil;
+		for(id = t.ids; id != nil; id = id.next){
+			id.store = Darg;
+			id.ty = topvartype(verifytypes(id.ty, adtt, nil), id, 0, 1);
+			if(id.implicit != byte 0){
+				if(poly != nil)
+					selfd := poly;
+				else
+					selfd = adtt;
+				if(selfd == nil)
+					error(t.src.start, "function is not a member of an adt, so can't use self");
+				else if(id != t.ids)
+					error(id.src.start, "only the first argument can use self");
+				else if(id.ty != selfd.ty && (id.ty.kind != Tref || id.ty.tof != selfd.ty))
+					error(id.src.start, "self argument's type must be "+selfd.sym.name+" or ref "+selfd.sym.name);
+			}
+			last = id;
+		}
+		for(id = t.polys; id != nil; id = id.next){
+			if(adtt != nil){
+				for(id1 := adtt.ty.polys; id1 != nil; id1 = id1.next){
+					if(id1.sym == id.sym)
+						id.ty = id1.ty;
+				}
+			}
+			id.store = Dtype;
+			id.ty = verifytypes(id.ty, adtt, nil);
+		}
+		t.tof = comtype(t.src, toptype(t.src, verifytypes(t.tof, adtt, nil)), adtt);
+		if(t.varargs != byte 0 && (last == nil || last.ty != tstring))
+			error(t.src.start, "variable arguments must be preceded by a string");
+		if(t.varargs != byte 0 && t.polys != nil)
+			error(t.src.start, "polymorphic functions must not have variable arguments");
+	Tpoly =>
+		for(id = t.ids; id != nil; id = id.next){
+			id.store = Dfn;
+			id.ty = verifytypes(id.ty, adtt, t.decl);
+		}
+	Tinst =>
+		t.ok &= ~OKverify;
+		t.tof = verifytypes(t.tof, adtt, nil);
+		for(tyl := t.tlist; tyl != nil; tyl = tyl.nxt)
+			tyl.t = verifytypes(tyl.t, adtt, nil);
+		(t, nil) = insttype(t, adtt, nil);
+		t = verifytypes(t, adtt, nil);
+	* =>
+		fatal("verifytypes: unknown type kind "+string t.kind);
+	}
+	if(int cyc)
+		t.flags |= CYCLIC;
+	return t;
+}
+
+#
+# resolve an id type
+#
+idtype(t: ref Type): ref Type
+{
+	id := t.decl;
+	if(id.store == Dunbound)
+		fatal("idtype: unbound decl");
+	tt := id.ty;
+	if(id.store != Dtype && id.store != Dtag){
+		if(id.store == Dundef){
+			id.store = Dwundef;
+			error(t.src.start, id.sym.name+" is not declared");
+		}else if(id.store == Dimport){
+			id.store = Dwundef;
+			error(t.src.start, id.sym.name+"'s type cannot be determined");
+		}else if(id.store != Dwundef)
+			error(t.src.start, id.sym.name+" is not a type");
+		return terror;
+	}
+	if(tt == nil){
+		error(t.src.start, stypeconv(t)+" not fully defined");
+		return terror;
+	}
+	return tt;
+}
+
+#
+# resolve a -> qualified type
+#
+arrowtype(t: ref Type, adtt: ref Decl): ref Type
+{
+	id := t.decl;
+	if(id.ty != nil){
+		if(id.store == Dunbound)
+			fatal("arrowtype: unbound decl has a type");
+		return id.ty;
+	}
+
+	#
+	# special hack to allow module variables to derive other types
+	# 
+	tt := t.tof;
+	if(tt.kind == Tid){
+		id = tt.decl;
+		if(id.store == Dunbound)
+			fatal("arrowtype: Tid's decl unbound");
+		if(id.store == Dimport){
+			id.store = Dwundef;
+			error(t.src.start, id.sym.name+"'s type cannot be determined");
+			return terror;
+		}
+
+		#
+		# forward references to module variables can't be resolved
+		#
+		if(id.store != Dtype && (id.ty.ok & OKbind) != OKbind){
+			error(t.src.start, id.sym.name+"'s type cannot be determined");
+			return terror;
+		}
+
+		if(id.store == Dwundef)
+			return terror;
+		tt = id.ty = verifytypes(id.ty, adtt, nil);
+		if(tt == nil){
+			error(t.tof.src.start, typeconv(t.tof)+" is not a module");
+			return terror;
+		}
+	}else
+		tt = verifytypes(t.tof, adtt, nil);
+	t.tof = tt;
+	if(tt == terror)
+		return terror;
+	if(tt.kind != Tmodule){
+		error(t.src.start, typeconv(tt)+" is not a module");
+		return terror;
+	}
+	id = namedot(tt.ids, t.decl.sym);
+	if(id == nil){
+		error(t.src.start, t.decl.sym.name+" is not a member of "+typeconv(tt));
+		return terror;
+	}
+	if(id.store == Dtype && id.ty != nil){
+		t.decl = id;
+		return id.ty;
+	}
+	error(t.src.start, typeconv(t)+" is not a type");
+	return terror;
+}
+
+#
+# resolve a . qualified type
+#
+dottype(t: ref Type, adtt: ref Decl): ref Type
+{
+	if(t.decl.ty != nil){
+		if(t.decl.store == Dunbound)
+			fatal("dottype: unbound decl has a type");
+		return t.decl.ty;
+	}
+	t.tof = tt := verifytypes(t.tof, adtt, nil);
+	if(tt == terror)
+		return terror;
+	if(tt.kind != Tadt){
+		error(t.src.start, typeconv(tt)+" is not an adt");
+		return terror;
+	}
+	id := namedot(tt.tags, t.decl.sym);
+	if(id != nil && id.ty != nil){
+		t.decl = id;
+		return id.ty;
+	}
+	error(t.src.start, t.decl.sym.name+" is not a pick tag of "+typeconv(tt));
+	return terror;
+}
+
+insttype(t: ref Type, adtt: ref Decl, tp: ref Tpair): (ref Type, ref Tpair)
+{
+	src := t.src;
+	if(t.tof.kind != Tadt && t.tof.kind != Tadtpick){
+		error(src.start, typeconv(t.tof) + " is not an adt");
+		return (terror, nil);
+	}
+	if(t.tof.kind == Tadt)
+		ids := t.tof.polys;
+	else
+		ids = t.tof.decl.dot.ty.polys;
+	if(ids == nil){
+		error(src.start, typeconv(t.tof) + " is not a polymorphic adt");
+		return (terror, nil);
+	}
+	for(tyl := t.tlist; tyl != nil && ids != nil; tyl = tyl.nxt){
+		tt := tyl.t;
+		if(!tattr[tt.kind].isptr){
+			error(src.start, typeconv(tt) + " is not a pointer type");
+			return (terror, nil);
+		}
+		unifysrc = src;
+		(ok, nil) := tunify(ids.ty, tt);
+		if(!ok){
+			error(src.start, "type " + typeconv(tt) + " does not match " + typeconv(ids.ty));
+			return (terror, nil);
+		}
+		# usetype(tt);
+		tt = verifytypes(tt, adtt, nil);
+		tp = addtmap(ids.ty, tt, tp);
+		ids = ids.next;
+	}
+	if(tyl != nil){
+		error(src.start, "too many actual types in instantiation");
+		return (terror, nil);
+	}
+	if(ids != nil){
+		error(src.start, "too few actual types in instantiation");
+		return (terror, nil);
+	}
+	tt := t.tof;
+	(t, nil) = expandtype(tt, t, adtt, tp);
+	if(t == tt && adtt == nil)
+		t = duptype(t);
+	if(t != tt)
+		t.tmap = tp;
+	t.src = src;
+	return (t, tp);
+}
+
+#
+# walk a type, putting all adts, modules, and tuples into equivalence classes
+#
+teqclass(t: ref Type)
+{
+	id: ref Decl;
+
+	if(t == nil || (t.ok & OKclass) == OKclass)
+		return;
+	t.ok |= OKclass;
+	case t.kind{
+	Terror or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tiface or
+	Tainit or
+	Talt or
+	Tcase or
+	Tcasel or
+	Tcasec or
+	Tgoto or
+	Texcept or
+	Tfix or
+	Tpoly =>
+		return;
+	Tref =>
+		teqclass(t.tof);
+		return;
+	Tchan or
+	Tarray or
+	Tlist =>
+		teqclass(t.tof);
+#ZZZ elim return to fix recursive chans, etc
+		if(!debug['Z'])
+			return;
+	Tadt or
+	Tadtpick or
+	Ttuple or
+	Texception =>
+		for(id = t.ids; id != nil; id = id.next)
+			teqclass(id.ty);
+		for(tg := t.tags; tg != nil; tg = tg.next)
+			teqclass(tg.ty);
+		for(id = t.polys; id != nil; id = id.next)
+			teqclass(id.ty);
+	Tmodule =>
+		t.tof = mkiface(t.decl);
+		for(id = t.ids; id != nil; id = id.next)
+			teqclass(id.ty);
+	Tfn =>
+		for(id = t.ids; id != nil; id = id.next)
+			teqclass(id.ty);
+		for(id = t.polys; id != nil; id = id.next)
+			teqclass(id.ty);
+		teqclass(t.tof);
+		return;
+	* =>
+		fatal("teqclass: unknown type kind "+string t.kind);
+	}
+
+	#
+	# find an equivalent type
+	# stupid linear lookup could be made faster
+	#
+	if((t.ok & OKsized) != OKsized)
+		fatal("eqclass type not sized: " + stypeconv(t));
+
+	for(teq := eqclass[t.kind]; teq != nil; teq = teq.eq){
+		if(t.size == teq.ty.size && tequal(t, teq.ty)){
+			t.eq = teq;
+			if(t.kind == Tmodule)
+				joiniface(t, t.eq.ty.tof);
+			return;
+		}
+	}
+
+	#
+	# if no equiv type, make one
+	#
+	eqclass[t.kind] = t.eq = ref Teq(0, t, eqclass[t.kind]);
+}
+
+#
+# record that we've used the type
+# using a type uses all types reachable from that type
+#
+reftype(t: ref Type)
+{
+	id: ref Decl;
+
+	if(t == nil || (t.ok & OKref) == OKref)
+		return;
+	t.ok |= OKref;
+	if(t.decl != nil && t.decl.refs == 0)
+		t.decl.refs++;
+	case t.kind{
+	Terror or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tiface or
+	Tainit or
+	Talt or
+	Tcase or
+	Tcasel or
+	Tcasec or
+	Tgoto or
+	Texcept or
+	Tfix or
+	Tpoly =>
+		break;
+	Tref or
+	Tchan or
+	Tarray or
+	Tlist =>
+		if(t.decl != nil){
+			if(nadts >= len adts){
+				a := array[nadts + 32] of ref Decl;
+				a[0:] = adts;
+				adts = a;
+			}
+			adts[nadts++] = t.decl;
+		}
+		reftype(t.tof);
+	Tadt or
+	Tadtpick or
+	Ttuple or
+	Texception =>
+		if(t.kind == Tadt || t.kind == Ttuple && t.decl.sym != anontupsym){
+			if(nadts >= len adts){
+				a := array[nadts + 32] of ref Decl;
+				a[0:] = adts;
+				adts = a;
+			}
+			adts[nadts++] = t.decl;
+		}
+		for(id = t.ids; id != nil; id = id.next)
+			if(id.store != Dfn)
+				reftype(id.ty);
+		for(tg := t.tags; tg != nil; tg = tg.next)
+			reftype(tg.ty);
+		for(id = t.polys; id != nil; id = id.next)
+			reftype(id.ty);
+		if(t.kind == Tadtpick)
+			reftype(t.decl.dot.ty);
+	Tmodule =>
+		#
+		# a module's elements should get used individually
+		# but do the globals for any sbl file
+		#
+		if(bsym != nil)
+			for(id = t.ids; id != nil; id = id.next)
+				if(id.store == Dglobal)
+					reftype(id.ty);
+		break;
+	Tfn =>
+		for(id = t.ids; id != nil; id = id.next)
+			reftype(id.ty);
+		for(id = t.polys; id != nil; id = id.next)
+			reftype(id.ty);
+		reftype(t.tof);
+	* =>
+		fatal("reftype: unknown type kind "+string t.kind);
+	}
+}
+
+#
+# check all reachable types for cycles and illegal forward references
+# find the size of all the types
+#
+cycsizetype(t: ref Type)
+{
+	id: ref Decl;
+
+	if(t == nil || (t.ok & (OKcycsize|OKcyc|OKsized)) == (OKcycsize|OKcyc|OKsized))
+		return;
+	t.ok |= OKcycsize;
+	case t.kind{
+	Terror or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tiface or
+	Tainit or
+	Talt or
+	Tcase or
+	Tcasel or
+	Tcasec or
+	Tgoto or
+	Texcept or
+	Tfix or
+	Tpoly =>
+		t.ok |= OKcyc;
+		sizetype(t);
+	Tref or
+	Tchan or
+	Tarray or
+	Tlist =>
+		cyctype(t);
+		sizetype(t);
+		cycsizetype(t.tof);
+	Tadt or
+	Ttuple or
+	Texception =>
+		cyctype(t);
+		sizetype(t);
+		for(id = t.ids; id != nil; id = id.next)
+			cycsizetype(id.ty);
+		for(tg := t.tags; tg != nil; tg = tg.next){
+			if((tg.ty.ok & (OKcycsize|OKcyc|OKsized)) == (OKcycsize|OKcyc|OKsized))
+				continue;
+			tg.ty.ok |= (OKcycsize|OKcyc|OKsized);
+			for(id = tg.ty.ids; id != nil; id = id.next)
+				cycsizetype(id.ty);
+		}
+		for(id = t.polys; id != nil; id = id.next)
+			cycsizetype(id.ty);
+	Tadtpick =>
+		t.ok &= ~OKcycsize;
+		cycsizetype(t.decl.dot.ty);
+	Tmodule =>
+		cyctype(t);
+		sizetype(t);
+		for(id = t.ids; id != nil; id = id.next)
+			cycsizetype(id.ty);
+		sizeids(t.ids, 0);
+	Tfn =>
+		cyctype(t);
+		sizetype(t);
+		for(id = t.ids; id != nil; id = id.next)
+			cycsizetype(id.ty);
+		for(id = t.polys; id != nil; id = id.next)
+			cycsizetype(id.ty);
+		cycsizetype(t.tof);
+		sizeids(t.ids, MaxTemp);
+#ZZZ need to align?
+	* =>
+		fatal("cycsizetype: unknown type kind "+string t.kind);
+	}
+}
+
+# check for circularity in type declarations
+# - has to be called before verifytypes
+#
+tcycle(t: ref Type)
+{
+	id: ref Decl;
+	tt: ref Type;
+	tll: ref Typelist;
+
+	if(t == nil)
+		return;
+	case(t.kind){
+	* =>
+		;
+	Tchan or
+	Tarray or
+	Tref or
+	Tlist or
+	Tdot =>
+		tcycle(t.tof);
+	Tfn or
+	Ttuple =>
+		tcycle(t.tof);
+		for(id = t.ids; id != nil; id = id.next)
+			tcycle(id.ty);
+	Tarrow =>
+		if(int(t.rec&TRvis)){
+			error(t.src.start, "circularity in definition of " + typeconv(t));
+			*t = *terror;	# break the cycle
+			return;
+		}
+		tt = t.tof;
+		t.rec |= TRvis;
+		tcycle(tt);
+		if(tt.kind == Tid)
+			tt = tt.decl.ty;
+		id = namedot(tt.ids, t.decl.sym);
+		if(id != nil)
+			tcycle(id.ty);
+		t.rec &= ~TRvis;
+	Tid =>
+		if(int(t.rec&TRvis)){
+			error(t.src.start, "circularity in definition of " + typeconv(t));
+			*t = *terror;	# break the cycle
+			return;
+		}
+		t.rec |= TRvis;
+		tcycle(t.decl.ty);
+		t.rec &= ~TRvis;
+	Tinst =>
+		tcycle(t.tof);
+		for(tll = t.tlist; tll != nil; tll = tll.nxt)
+			tcycle(tll.t);
+	}
+}
+
+#
+# marks for checking for arcs
+#
+	ArcValue,
+	ArcList,
+	ArcArray,
+	ArcRef,
+	ArcCyc,			# cycle found
+	ArcPolycyc:
+		con 1 << iota;
+
+cyctype(t: ref Type)
+{
+	if((t.ok & OKcyc) == OKcyc)
+		return;
+	t.ok |= OKcyc;
+	t.rec |= TRcyc;
+	case t.kind{
+	Terror or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tfn or
+	Tchan or
+	Tarray or
+	Tref or
+	Tlist or
+	Tfix or
+	Tpoly =>
+		break;
+	Tadt or
+	Tmodule or
+	Ttuple or
+	Texception =>
+		for(id := t.ids; id != nil; id = id.next)
+			cycfield(t, id);
+		for(tg := t.tags; tg != nil; tg = tg.next){
+			if((tg.ty.ok & OKcyc) == OKcyc)
+				continue;
+			tg.ty.ok |= OKcyc;
+			for(id = tg.ty.ids; id != nil; id = id.next)
+				cycfield(t, id);
+		}
+	* =>
+		fatal("cyctype: unknown type kind "+string t.kind);
+	}
+	t.rec &= ~TRcyc;
+}
+
+cycfield(base: ref Type, id: ref Decl)
+{
+	if(!storespace[id.store])
+		return;
+	arc := cycarc(base, id.ty);
+
+	if((arc & (ArcCyc|ArcValue)) == (ArcCyc|ArcValue)){
+		if(id.cycerr == byte 0)
+			error(base.src.start, "illegal type cycle without a reference in field "
+				+id.sym.name+" of "+stypeconv(base));
+		id.cycerr = byte 1;
+	}else if(arc & ArcCyc){
+		if((arc & ArcArray) && id.cyc == byte 0 && !(arc & ArcPolycyc)){
+			if(id.cycerr == byte 0)
+				error(base.src.start, "illegal circular reference to type "+typeconv(id.ty)
+					+" in field "+id.sym.name+" of "+stypeconv(base));
+			id.cycerr = byte 1;
+		}
+		id.cycle = byte 1;
+	}else if(id.cyc != byte 0){
+		if(id.cycerr == byte 0)
+			error(id.src.start, "spurious cyclic qualifier for field "+id.sym.name+" of "+stypeconv(base));
+		id.cycerr = byte 1;
+	}
+}
+
+cycarc(base, t: ref Type): int
+{
+	if(t == nil)
+		return 0;
+	if((t.rec & TRcyc) == TRcyc){
+		if(tequal(t, base)){
+			if(t.kind == Tmodule)
+				return ArcCyc | ArcRef;
+			else
+				return ArcCyc | ArcValue;
+		}
+		return 0;
+	}
+	t.rec |= TRcyc;
+	me := 0;
+	case t.kind{
+	Terror or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tchan or
+	Tfn or
+	Tfix or
+	Tpoly =>
+		break;
+	Tarray =>
+		me = cycarc(base, t.tof) & ~ArcValue | ArcArray;
+	Tref =>
+		me = cycarc(base, t.tof) & ~ArcValue | ArcRef;
+	Tlist =>
+		me = cycarc(base, t.tof) & ~ArcValue | ArcList;
+	Tadt or
+	Tadtpick or
+	Tmodule or
+	Ttuple or
+	Texception =>
+		me = 0;
+		arc: int;
+		for(id := t.ids; id != nil; id = id.next){
+			if(!storespace[id.store])
+				continue;
+			arc = cycarc(base, id.ty);
+			if((arc & ArcCyc) && id.cycerr == byte 0)
+				me |= arc;
+		}
+		for(tg := t.tags; tg != nil; tg = tg.next){
+			arc = cycarc(base, tg.ty);
+			if((arc & ArcCyc) && tg.cycerr == byte 0)
+				me |= arc;
+		}
+
+		if(t.kind == Tmodule)
+			me = me & ArcCyc | ArcRef | ArcPolycyc;
+		else
+			me &= ArcCyc | ArcValue | ArcPolycyc;
+	* =>
+		fatal("cycarc: unknown type kind "+string t.kind);
+	}
+	t.rec &= ~TRcyc;
+	if(int (t.flags&CYCLIC))
+		me |= ArcPolycyc;
+	return me;
+}
+
+#
+# set the sizes and field offsets for t
+# look only as deeply as needed to size this type.
+# cycsize type will clean up the rest.
+#
+sizetype(t: ref Type)
+{
+	id: ref Decl;
+	sz, al, s, a: int;
+
+	if(t == nil)
+		return;
+	if((t.ok & OKsized) == OKsized)
+		return;
+	t.ok |= OKsized;
+if((t.ok & (OKverify|OKsized)) != (OKverify|OKsized))
+fatal("sizetype bogus ok for " + stypeconv(t));
+	case t.kind{
+	* =>
+		fatal("sizetype: unknown type kind "+string t.kind);
+	Terror or
+	Tnone or
+	Tbyte or
+	Tint or
+	Tbig or
+	Tstring or
+	Tany or
+	Treal =>
+		fatal(typeconv(t)+" should have a size");
+	Tref or
+	Tchan or
+	Tarray or
+	Tlist or
+	Tmodule or
+	Tfix or
+	Tpoly =>
+		t.size = t.align = IBY2WD;
+	Tadt or
+	Ttuple or
+	Texception =>
+		if(t.tags == nil){
+#ZZZ
+			if(!debug['z']){
+				(sz, t.align) = sizeids(t.ids, 0);
+				t.size = align(sz, t.align);
+			}else{
+				(sz, nil) = sizeids(t.ids, 0);
+				t.align = IBY2LG;
+				t.size = align(sz, IBY2LG);
+			}
+			return;
+		}
+#ZZZ
+		if(!debug['z']){
+			(sz, al) = sizeids(t.ids, IBY2WD);
+			if(al < IBY2WD)
+				al = IBY2WD;
+		}else{
+			(sz, nil) = sizeids(t.ids, IBY2WD);
+			al = IBY2LG;
+		}
+		for(tg := t.tags; tg != nil; tg = tg.next){
+			if((tg.ty.ok & OKsized) == OKsized)
+				continue;
+			tg.ty.ok |= OKsized;
+#ZZZ
+			if(!debug['z']){
+				(s, a) = sizeids(tg.ty.ids, sz);
+				if(a < al)
+					a = al;
+				tg.ty.size = align(s, a);
+				tg.ty.align = a;
+			}else{
+				(s, nil) = sizeids(tg.ty.ids, sz);
+				tg.ty.size = align(s, IBY2LG);
+				tg.ty.align = IBY2LG;
+			}			
+		}
+	Tfn =>
+		t.size = 0;
+		t.align = 1;
+	Tainit =>
+		t.size = 0;
+		t.align = 1;
+	Talt =>
+		t.size = t.cse.nlab * 2*IBY2WD + 2*IBY2WD;
+		t.align = IBY2WD;
+	Tcase or
+	Tcasec =>
+		t.size = t.cse.nlab * 3*IBY2WD + 2*IBY2WD;
+		t.align = IBY2WD;
+	Tcasel =>
+		t.size = t.cse.nlab * 6*IBY2WD + 3*IBY2WD;
+		t.align = IBY2LG;
+	Tgoto =>
+		t.size = t.cse.nlab * IBY2WD + IBY2WD;
+		if(t.cse.iwild != nil)
+			t.size += IBY2WD;
+		t.align = IBY2WD;
+	Tiface =>
+		sz = IBY2WD;
+		for(id = t.ids; id != nil; id = id.next){
+			sz = align(sz, IBY2WD) + IBY2WD;
+			sz += len array of byte id.sym.name + 1;
+			if(id.dot.ty.kind == Tadt)
+				sz += len array of byte id.dot.sym.name + 1;
+		}
+		t.size = sz;
+		t.align = IBY2WD;
+	Texcept =>
+		t.size = 0;
+		t.align = IBY2WD;
+	}
+}
+
+sizeids(id: ref Decl, off: int): (int, int)
+{
+	al := 1;
+	for(; id != nil; id = id.next){
+		if(storespace[id.store]){
+			sizetype(id.ty);
+			#
+			# alignment can be 0 if we have
+			# illegal forward declarations.
+			# just patch a; other code will flag an error
+			#
+			a := id.ty.align;
+			if(a == 0)
+				a = 1;
+
+			if(a > al)
+				al = a;
+
+			off = align(off, a);
+			id.offset = off;
+			off += id.ty.size;
+		}
+	}
+	return (off, al);
+}
+
+align(off, align: int): int
+{
+	if(align == 0)
+		fatal("align 0");
+	while(off % align)
+		off++;
+	return off;
+}
+
+#
+# recalculate a type's size
+#
+resizetype(t: ref Type)
+{
+	if((t.ok & OKsized) == OKsized){
+		t.ok &= ~OKsized;
+		cycsizetype(t);
+	}
+}
+
+#
+# check if a module is accessable from t
+# if so, mark that module interface
+#
+modrefable(t: ref Type)
+{
+	id: ref Decl;
+
+	if(t == nil || (t.ok & OKmodref) == OKmodref)
+		return;
+	if((t.ok & OKverify) != OKverify)
+		fatal("modrefable unused type "+stypeconv(t));
+	t.ok |= OKmodref;
+	case t.kind{
+	Terror or
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tnone or
+	Tany or
+	Tfix or
+	Tpoly =>
+		break;
+	Tchan or
+	Tref or
+	Tarray or
+	Tlist =>
+		modrefable(t.tof);
+	Tmodule =>
+		t.tof.linkall = byte 1;
+		t.decl.refs++;
+		for(id = t.ids; id != nil; id = id.next){
+			case id.store{
+			Dglobal or
+			Dfn =>
+				modrefable(id.ty);
+			Dtype =>
+				if(id.ty.kind != Tadt)
+					break;
+				for(m := id.ty.ids; m != nil; m = m.next)
+					if(m.store == Dfn)
+						modrefable(m.ty);
+			}
+		}
+	Tfn or
+	Tadt or
+	Ttuple or
+	Texception =>
+		for(id = t.ids; id != nil; id = id.next)
+			if(id.store != Dfn)
+				modrefable(id.ty);
+		for(tg := t.tags; tg != nil; tg = tg.next){
+			# if((tg.ty.ok & OKmodref) == OKmodref)
+			#	continue;
+			tg.ty.ok |= OKmodref;
+			for(id = tg.ty.ids; id != nil; id = id.next)
+				modrefable(id.ty);
+		}
+		for(id = t.polys; id != nil; id = id.next)
+			modrefable(id.ty);
+		modrefable(t.tof);
+	Tadtpick =>
+		modrefable(t.decl.dot.ty);
+	* =>
+		fatal("modrefable: unknown type kind "+string t.kind);
+	}
+}
+
+gendesc(d: ref Decl, size: int, decls: ref Decl): ref Desc
+{
+	if(debug['D'])
+		print("generate desc for %s\n", dotconv(d));
+	if(ispoly(d))
+		addfnptrs(d, 0);
+	desc := usedesc(mkdesc(size, decls));
+	return desc;
+}
+
+mkdesc(size: int, d: ref Decl): ref Desc
+{
+	pmap := array[(size+8*IBY2WD-1) / (8*IBY2WD)] of { * => byte 0 };
+	n := descmap(d, pmap, 0);
+	if(n >= 0)
+		n = n / (8*IBY2WD) + 1;
+	else
+		n = 0;
+	return enterdesc(pmap, size, n);
+}
+
+mktdesc(t: ref Type): ref Desc
+{
+usedty(t);
+	if(debug['D'])
+		print("generate desc for %s\n", typeconv(t));
+	if(t.decl == nil){
+		t.decl = mkdecl(t.src, Dtype, t);
+		t.decl.sym = enter("_mktdesc_", 0);
+	}
+	if(t.decl.desc != nil)
+		return t.decl.desc;
+	pmap := array[(t.size+8*IBY2WD-1) / (8*IBY2WD)] of {* => byte 0};
+	n := tdescmap(t, pmap, 0);
+	if(n >= 0)
+		n = n / (8*IBY2WD) + 1;
+	else
+		n = 0;
+	d := enterdesc(pmap, t.size, n);
+	t.decl.desc = d;
+	return d;
+}
+
+enterdesc(map: array of byte, size, nmap: int): ref Desc
+{
+	last : ref Desc = nil;
+	for(d := descriptors; d != nil; d = d.next){
+		if(d.size > size || d.size == size && d.nmap > nmap)
+			break;
+		if(d.size == size && d.nmap == nmap){
+			c := mapcmp(d.map, map, nmap);
+			if(c == 0)
+				return d;
+			if(c > 0)
+				break;
+		}
+		last = d;
+	}
+
+	d = ref Desc(-1, 0, map, size, nmap, nil);
+	if(last == nil){
+		d.next = descriptors;
+		descriptors = d;
+	}else{
+		d.next = last.next;
+		last.next = d;
+	}
+	return d;
+}
+
+mapcmp(a, b: array of byte, n: int): int
+{
+	for(i := 0; i < n; i++)
+		if(a[i] != b[i])
+			return int a[i] - int b[i];
+	return 0;
+}
+
+usedesc(d: ref Desc): ref Desc
+{
+	d.used = 1;
+	return d;
+}
+
+#
+# create the pointer description byte map for every type in decls
+# each bit corresponds to a word, and is 1 if occupied by a pointer
+# the high bit in the byte maps the first word
+#
+descmap(decls: ref Decl, map: array of byte, start: int): int
+{
+	if(debug['D'])
+		print("descmap offset %d\n", start);
+	last := -1;
+	for(d := decls; d != nil; d = d.next){
+		if(d.store == Dtype && d.ty.kind == Tmodule
+		|| d.store == Dfn
+		|| d.store == Dconst)
+			continue;
+		if(d.store == Dlocal && d.link != nil)
+			continue;
+		m := tdescmap(d.ty, map, d.offset + start);
+		if(debug['D']){
+			if(d.sym != nil)
+				print("descmap %s type %s offset %d returns %d\n", d.sym.name, typeconv(d.ty), d.offset+start, m);
+			else
+				print("descmap type %s offset %d returns %d\n", typeconv(d.ty), d.offset+start, m);
+		}
+		if(m >= 0)
+			last = m;
+	}
+	return last;
+}
+
+tdescmap(t: ref Type, map: array of byte, offset: int): int
+{
+	i, e, bit: int;
+
+	if(t == nil)
+		return -1;
+
+	m := -1;
+	if(t.kind == Talt){
+		lab := t.cse.labs;
+		e = t.cse.nlab;
+		offset += IBY2WD * 2;
+		for(i = 0; i < e; i++){
+			if(lab[i].isptr){
+				bit = offset / IBY2WD % 8;
+				map[offset / (8*IBY2WD)] |= byte 1 << (7 - bit);
+				m = offset;
+			}
+			offset += 2*IBY2WD;
+		}
+		return m;
+	}
+	if(t.kind == Tcasec){
+		e = t.cse.nlab;
+		offset += IBY2WD;
+		for(i = 0; i < e; i++){
+			bit = offset / IBY2WD % 8;
+			map[offset / (8*IBY2WD)] |= byte 1 << (7 - bit);
+			offset += IBY2WD;
+			bit = offset / IBY2WD % 8;
+			map[offset / (8*IBY2WD)] |= byte 1 << (7 - bit);
+			m = offset;
+			offset += 2*IBY2WD;
+		}
+		return m;
+	}
+
+	if(tattr[t.kind].isptr){
+		bit = offset / IBY2WD % 8;
+		map[offset / (8*IBY2WD)] |= byte 1 << (7 - bit);
+		return offset;
+	}
+	if(t.kind == Tadtpick)
+		t = t.tof;
+	if(t.kind == Ttuple || t.kind == Tadt || t.kind == Texception){
+		if(debug['D'])
+			print("descmap adt offset %d\n", offset);
+		if(t.rec != byte 0)
+			fatal("illegal cyclic type "+stypeconv(t)+" in tdescmap");
+		t.rec = byte 1;
+		offset = descmap(t.ids, map, offset);
+		t.rec = byte 0;
+		return offset;
+	}
+
+	return -1;
+}
+
+tcomset: int;
+
+#
+# can a t2 be assigned to a t1?
+# any means Tany matches all types,
+# not just references
+#
+tcompat(t1, t2: ref Type, any: int): int
+{
+	if(t1 == t2)
+		return 1;
+	if(t1 == nil || t2 == nil)
+		return 0;
+	if(t2.kind == Texception && t1.kind != Texception)
+		t2 = mkextuptype(t2);
+	tcomset = 0;
+	ok := rtcompat(t1, t2, any, 0);
+	v := cleartcomrec(t1) + cleartcomrec(t2);
+	if(v != tcomset)
+		fatal("recid t1 "+stypeconv(t1)+" and t2 "+stypeconv(t2)+" not balanced in tcompat: "+string v+" "+string tcomset);
+	return ok;
+}
+
+rtcompat(t1, t2: ref Type, any: int, inaorc: int): int
+{
+	if(t1 == t2)
+		return 1;
+	if(t1 == nil || t2 == nil)
+		return 0;
+	if(t1.kind == Terror || t2.kind == Terror)
+		return 1;
+	if(t2.kind == Texception && t1.kind != Texception)
+		t2 = mkextuptype(t2);
+
+	t1.rec |= TRcom;
+	t2.rec |= TRcom;
+	case t1.kind{
+	* =>
+		fatal("unknown type "+stypeconv(t1)+" v "+stypeconv(t2)+" in rtcompat");
+		return 0;
+	Tstring =>
+		return t2.kind == Tstring || t2.kind == Tany;
+	Texception =>
+		if(t2.kind == Texception && t1.cons == t2.cons){
+			if(assumetcom(t1, t2))
+				return 1;
+			return idcompat(t1.ids, t2.ids, 0, inaorc);
+		}
+		return 0;
+	Tnone or
+	Tint or
+	Tbig or
+	Tbyte or
+	Treal =>
+		return t1.kind == t2.kind;
+	Tfix =>
+		return t1.kind == t2.kind && sametree(t1.val, t2.val);
+	Tany =>
+		if(tattr[t2.kind].isptr)
+			return 1;
+		return any;
+	Tref or
+	Tlist or
+	Tarray or
+	Tchan =>
+		if(t1.kind != t2.kind){
+			if(t2.kind == Tany)
+				return 1;
+			return 0;
+		}
+		if(t1.kind != Tref && assumetcom(t1, t2))
+			return 1;
+		return rtcompat(t1.tof, t2.tof, 0, t1.kind == Tarray || t1.kind == Tchan || inaorc);
+	Tfn =>
+		break;
+	Ttuple =>
+		if(t2.kind == Tadt && t2.tags == nil
+		|| t2.kind == Ttuple){
+			if(assumetcom(t1, t2))
+				return 1;
+			return idcompat(t1.ids, t2.ids, any, inaorc);
+		}
+		if(t2.kind == Tadtpick){
+			t2.tof.rec |= TRcom;
+			if(assumetcom(t1, t2.tof))
+				return 1;
+			return idcompat(t1.ids, t2.tof.ids.next, any, inaorc);
+		}
+		return 0;
+	Tadt =>
+		if(t2.kind == Ttuple && t1.tags == nil){
+			if(assumetcom(t1, t2))
+				return 1;
+			return idcompat(t1.ids, t2.ids, any, inaorc);
+		}
+		if(t1.tags != nil && t2.kind == Tadtpick && !inaorc)
+			t2 = t2.decl.dot.ty;
+	Tadtpick =>
+		#if(t2.kind == Ttuple)
+		#	return idcompat(t1.tof.ids.next, t2.ids, any, inaorc);
+		break;
+	Tmodule =>
+		if(t2.kind == Tany)
+			return 1;
+	Tpoly =>
+		if(t2.kind == Tany)
+			return 1;
+	}
+	return tequal(t1, t2);
+}
+
+#
+# add the assumption that t1 and t2 are compatable
+#
+assumetcom(t1, t2: ref Type): int
+{
+	r1, r2: ref Type;
+
+	if(t1.tcom == nil && t2.tcom == nil){
+		tcomset += 2;
+		t1.tcom = t2.tcom = t1;
+	}else{
+		if(t1.tcom == nil){
+			r1 = t1;
+			t1 = t2;
+			t2 = r1;
+		}
+		for(r1 = t1.tcom; r1 != r1.tcom; r1 = r1.tcom)
+			;
+		for(r2 = t2.tcom; r2 != nil && r2 != r2.tcom; r2 = r2.tcom)
+			;
+		if(r1 == r2)
+			return 1;
+		if(r2 == nil)
+			tcomset++;
+		t2.tcom = t1;
+		for(; t2 != r1; t2 = r2){
+			r2 = t2.tcom;
+			t2.tcom = r1;
+		}
+	}
+	return 0;
+}
+
+cleartcomrec(t: ref Type): int
+{
+	n := 0;
+	for(; t != nil && (t.rec & TRcom) == TRcom; t = t.tof){
+		t.rec &= ~TRcom;
+		if(t.tcom != nil){
+			t.tcom = nil;
+			n++;
+		}
+		if(t.kind == Tadtpick)
+			n += cleartcomrec(t.tof);
+		if(t.kind == Tmodule)
+			t = t.tof;
+		for(id := t.ids; id != nil; id = id.next)
+			n += cleartcomrec(id.ty);
+		for(id = t.tags; id != nil; id = id.next)
+			n += cleartcomrec(id.ty);
+		for(id = t.polys; id != nil; id = id.next)
+			n += cleartcomrec(id.ty);
+	}
+	return n;
+}
+
+#
+# id1 and id2 are the fields in an adt or tuple
+# simple structural check; ignore names
+#
+idcompat(id1, id2: ref Decl, any: int, inaorc: int): int
+{
+	for(; id1 != nil; id1 = id1.next){
+		if(id1.store != Dfield)
+			continue;
+		while(id2 != nil && id2.store != Dfield)
+			id2 = id2.next;
+		if(id2 == nil
+		|| id1.store != id2.store
+		|| !rtcompat(id1.ty, id2.ty, any, inaorc))
+			return 0;
+		id2 = id2.next;
+	}
+	while(id2 != nil && id2.store != Dfield)
+		id2 = id2.next;
+	return id2 == nil;
+}
+
+#
+# structural equality on types
+# t->recid is used to detect cycles
+# t->rec is used to clear t->recid
+#
+tequal(t1, t2: ref Type): int
+{
+	eqrec = 0;
+	eqset = 0;
+	ok := rtequal(t1, t2);
+	v := cleareqrec(t1) + cleareqrec(t2);
+	if(0 && v != eqset)
+		fatal("recid t1 "+stypeconv(t1)+" and t2 "+stypeconv(t2)+" not balanced in tequal: "+string v+" "+string eqset);
+	eqset = 0;
+	return ok;
+}
+
+rtequal(t1, t2: ref Type): int
+{
+	#
+	# this is just a shortcut
+	#
+	if(t1 == t2)
+		return 1;
+
+	if(t1 == nil || t2 == nil)
+		return 0;
+	if(t1.kind == Terror || t2.kind == Terror)
+		return 1;
+
+	if(t1.kind != t2.kind)
+		return 0;
+
+	if(t1.eq != nil && t2.eq != nil)
+		return t1.eq == t2.eq;
+
+	t1.rec |= TReq;
+	t2.rec |= TReq;
+	case t1.kind{
+	* =>
+		fatal("bogus type "+stypeconv(t1)+" vs "+stypeconv(t2)+" in rtequal");
+		return 0;
+	Tnone or
+	Tbig or
+	Tbyte or
+	Treal or
+	Tint or
+	Tstring =>
+		#
+		# this should always be caught by t1 == t2 check
+		#
+		fatal("bogus value type "+stypeconv(t1)+" vs "+stypeconv(t2)+" in rtequal");
+		return 1;
+	Tfix =>
+		return sametree(t1.val, t2.val);
+	Tref or
+	Tlist or
+	Tarray or
+	Tchan =>
+		if(t1.kind != Tref && assumeteq(t1, t2))
+			return 1;
+		return rtequal(t1.tof, t2.tof);
+	Tfn =>
+		if(t1.varargs != t2.varargs)
+			return 0;
+		if(!idequal(t1.ids, t2.ids, 0, storespace))
+			return 0;
+		# if(!idequal(t1.polys, t2.polys, 1, nil))
+		if(!pyequal(t1, t2))
+			return 0;
+		return rtequal(t1.tof, t2.tof);
+	Ttuple or
+	Texception =>
+		if(t1.kind != t2.kind || t1.cons != t2.cons)
+			return 0;
+		if(assumeteq(t1, t2))
+			return 1;
+		return idequal(t1.ids, t2.ids, 0, storespace);
+	Tadt or
+	Tadtpick or
+	Tmodule =>
+		if(assumeteq(t1, t2))
+			return 1;
+
+		#
+		# compare interfaces when comparing modules
+		#
+		if(t1.kind == Tmodule)
+			return idequal(t1.tof.ids, t2.tof.ids, 1, nil);
+
+		#
+		# picked adts; check parent,
+		# assuming equiv picked fields,
+		# then check picked fields are equiv
+		#
+		if(t1.kind == Tadtpick && !rtequal(t1.decl.dot.ty, t2.decl.dot.ty))
+			return 0;
+
+		#
+		# adts with pick tags: check picked fields for equality
+		#
+		if(!idequal(t1.tags, t2.tags, 1, nil))
+			return 0;
+
+		# if(!idequal(t1.polys, t2.polys, 1, nil))
+		if(!pyequal(t1, t2))
+			return 0;
+		return idequal(t1.ids, t2.ids, 1, storespace);
+	Tpoly =>
+		if(assumeteq(t1, t2))
+			return 1;
+		if(t1.decl.sym != t2.decl.sym)
+			return 0;
+		return idequal(t1.ids, t2.ids, 1, nil);
+	}
+}
+
+assumeteq(t1, t2: ref Type): int
+{
+	r1, r2: ref Type;
+
+	if(t1.teq == nil && t2.teq == nil){
+		eqrec++;
+		eqset += 2;
+		t1.teq = t2.teq = t1;
+	}else{
+		if(t1.teq == nil){
+			r1 = t1;
+			t1 = t2;
+			t2 = r1;
+		}
+		for(r1 = t1.teq; r1 != r1.teq; r1 = r1.teq)
+			;
+		for(r2 = t2.teq; r2 != nil && r2 != r2.teq; r2 = r2.teq)
+			;
+		if(r1 == r2)
+			return 1;
+		if(r2 == nil)
+			eqset++;
+		t2.teq = t1;
+		for(; t2 != r1; t2 = r2){
+			r2 = t2.teq;
+			t2.teq = r1;
+		}
+	}
+	return 0;
+}
+
+#
+# checking structural equality for modules, adts, tuples, and fns
+#
+idequal(id1, id2: ref Decl, usenames: int, storeok: array of int): int
+{
+	#
+	# this is just a shortcut
+	#
+	if(id1 == id2)
+		return 1;
+
+	for(; id1 != nil; id1 = id1.next){
+		if(storeok != nil && !storeok[id1.store])
+			continue;
+		while(id2 != nil && storeok != nil && !storeok[id2.store])
+			id2 = id2.next;
+		if(id2 == nil
+		|| usenames && id1.sym != id2.sym
+		|| id1.store != id2.store
+		|| id1.implicit != id2.implicit
+		|| id1.cyc != id2.cyc
+		|| (id1.dot == nil) != (id2.dot == nil)
+		|| id1.dot != nil && id2.dot != nil && id1.dot.ty.kind != id2.dot.ty.kind
+		|| !rtequal(id1.ty, id2.ty))
+			return 0;
+		id2 = id2.next;
+	}
+	while(id2 != nil && storeok != nil && !storeok[id2.store])
+		id2 = id2.next;
+	return id1 == nil && id2 == nil;
+}
+
+
+pyequal(t1: ref Type, t2: ref Type): int
+{
+	pt1, pt2: ref Type;
+	id1, id2: ref Decl;
+
+	if(t1 == t2)
+		return 1;
+	id1 = t1.polys;
+	id2 = t2.polys;
+	for(; id1 != nil; id1 = id1.next){
+		if(id2 == nil)
+			return 0;
+		pt1 = id1.ty;
+		pt2 = id2.ty;
+		if(!rtequal(pt1, pt2)){
+			if(t1.tmap != nil)
+				pt1 = valtmap(pt1, t1.tmap);
+			if(t2.tmap != nil)
+				pt2 = valtmap(pt2, t2.tmap);
+			if(!rtequal(pt1, pt2))
+				return 0;
+		}
+		id2 = id2.next;
+	}
+	return id1 == nil && id2 == nil;
+}
+
+cleareqrec(t: ref Type): int
+{
+	n := 0;
+	for(; t != nil && (t.rec & TReq) == TReq; t = t.tof){
+		t.rec &= ~TReq;
+		if(t.teq != nil){
+			t.teq = nil;
+			n++;
+		}
+		if(t.kind == Tadtpick)
+			n += cleareqrec(t.decl.dot.ty);
+		if(t.kind == Tmodule)
+			t = t.tof;
+		for(id := t.ids; id != nil; id = id.next)
+			n += cleareqrec(id.ty);
+		for(id = t.tags; id != nil; id = id.next)
+			n += cleareqrec(id.ty);
+		for(id = t.polys; id != nil; id = id.next)
+			n += cleareqrec(id.ty);
+	}
+	return n;
+}
+
+raisescompat(n1: ref Node, n2: ref Node): int
+{
+	if(n1 == n2)
+		return 1;
+	if(n2 == nil)
+		return 1;	# no need to repeat in definition if given in declaration
+	if(n1 == nil)
+		return 0;
+	for((n1, n2) = (n1.left, n2.left); n1 != nil && n2 != nil; (n1, n2) = (n1.right, n2.right)){
+		if(n1.left.decl != n2.left.decl)
+			return 0;
+	}
+	return n1 == n2;
+}
+
+# t1 a polymorphic type
+fnunify(t1: ref Type, t2: ref Type, tp: ref Tpair, swapped: int): (int, ref Tpair)
+{
+	id, ids: ref Decl;
+	sym: ref Sym;
+	ok: int;
+
+	for(ids = t1.ids; ids != nil; ids = ids.next){
+		sym = ids.sym;
+		(id, nil) = fnlookup(sym, t2);
+		if(id != nil)
+			usetype(id.ty);
+		if(id == nil){
+			if(dowarn)
+				error(unifysrc.start, "type " + typeconv(t2) + " does not have a '" + sym.name + "' function");
+			return (0, tp);
+		}
+		else if(id.ty.kind != Tfn){
+			if(dowarn)
+				error(unifysrc.start, typeconv(id.ty) + " is not a function");
+			return (0, tp);
+		}
+		else{
+			(ok, tp) = rtunify(ids.ty, id.ty, tp, !swapped);
+			if(!ok){
+				if(dowarn)
+					error(unifysrc.start, typeconv(ids.ty) + " and " + typeconv(id.ty) + " are not compatible wrt " + sym.name);
+				return (0, tp);
+			}
+		}
+	}
+	return (1, tp);
+}
+
+fncleareqrec(t1: ref Type, t2: ref Type): int
+{
+	id, ids: ref Decl;
+	n: int;
+
+	n = 0;
+	n += cleareqrec(t1);
+	n += cleareqrec(t2);
+	for(ids = t1.ids; ids != nil; ids = ids.next){
+		(id, nil) = fnlookup(ids.sym, t2);
+		if(id == nil)
+			continue;
+		else{
+			n += cleareqrec(ids.ty);
+			n += cleareqrec(id.ty);
+		}
+	}
+	return n;
+}
+
+tunify(t1: ref Type, t2: ref Type): (int, ref Tpair)
+{
+	v: int;
+	p: ref Tpair;
+
+	eqrec = 0;
+	eqset = 0;
+	(ok, tp) := rtunify(t1, t2, nil, 0);
+	v = cleareqrec(t1) + cleareqrec(t2);
+	for(p = tp; p != nil; p = p.nxt)
+		v += fncleareqrec(p.t1, p.t2);
+	if(0 && v != eqset)
+		fatal("recid t1 " + stypeconv(t1) + " and t2 " + stypeconv(t2) + " not balanced in tunify: " + string v + " " + string eqset);
+	return (ok, tp);
+}
+
+rtunify(t1: ref Type, t2: ref Type, tp: ref Tpair, swapped: int): (int, ref Tpair)
+{
+	ok: int;
+
+	t1 = valtmap(t1, tp);
+	t2 = valtmap(t2, tp);
+	if(t1 == t2)
+		return (1, tp);
+	if(t1 == nil || t2 == nil)
+		return (0, tp);
+	if(t1.kind == Terror || t2.kind == Terror)
+		return (1, tp);
+	if(t1.kind != Tpoly && t2.kind == Tpoly){
+		(t1, t2) = (t2, t1);
+		swapped = !swapped;
+	}
+	if(t1.kind == Tpoly){
+		# if(typein(t1, t2))
+		# 	 return (0, tp);
+		if(!tattr[t2.kind].isptr)
+			return (0, tp);
+		if(t2.kind != Tany)
+			tp = addtmap(t1, t2, tp);
+		return fnunify(t1, t2, tp, swapped);
+	}
+	if(t1.kind != Tany && t2.kind == Tany){
+		(t1, t2) = (t2, t1);
+		swapped = !swapped;
+	}
+	if(t1.kind == Tadt && t1.tags != nil && t2.kind == Tadtpick && !swapped)
+		t2 = t2.decl.dot.ty;
+	if(t2.kind == Tadt && t2.tags != nil && t1.kind == Tadtpick && swapped)
+		t1 = t1.decl.dot.ty;
+	if(t1.kind != Tany && t1.kind != t2.kind)
+		return (0, tp);
+	t1.rec |= TReq;
+	t2.rec |= TReq;
+	case(t1.kind){
+	* =>
+		return (tequal(t1, t2), tp);
+	Tany =>
+		return (tattr[t2.kind].isptr, tp);
+	Tref or
+	Tlist or
+	Tarray or
+	Tchan =>
+		if(t1.kind != Tref && assumeteq(t1, t2))
+			return (1, tp);
+		return rtunify(t1.tof, t2.tof, tp, swapped);
+	Tfn =>
+		(ok, tp) = idunify(t1.ids, t2.ids, tp, swapped);
+		if(!ok)
+			return (0, tp);
+		(ok, tp) = idunify(t1.polys, t2.polys, tp, swapped);
+		if(!ok)
+			return (0, tp);
+		return rtunify(t1.tof, t2.tof, tp, swapped);
+	Ttuple =>
+		if(assumeteq(t1, t2))
+			return (1, tp);
+		return idunify(t1.ids, t2.ids, tp, swapped);
+	Tadt or
+	Tadtpick =>
+		if(assumeteq(t1, t2))
+			return (1, tp);
+		(ok, tp) = idunify(t1.polys, t2.polys, tp, swapped);
+		if(!ok)
+			return (0, tp);
+		(ok, tp) = idunify(t1.tags, t2.tags, tp, swapped);
+		if(!ok)
+			return (0, tp);
+		return idunify(t1.ids, t2.ids, tp, swapped);
+	Tmodule =>
+		if(assumeteq(t1, t2))
+			return (1, tp);
+		return idunify(t1.tof.ids, t2.tof.ids, tp, swapped);
+	Tpoly =>
+		return (t1 == t2, tp);
+	}
+	return (1, tp);
+}
+
+idunify(id1: ref Decl, id2: ref Decl, tp: ref Tpair, swapped: int): (int, ref Tpair)
+{
+	ok: int;
+
+	if(id1 == id2)
+		return (1, tp);
+	for(; id1 != nil; id1 = id1.next){
+		if(id2 == nil)
+			return (0, tp);
+		(ok, tp) = rtunify(id1.ty, id2.ty, tp, swapped);
+		if(!ok)
+			return (0, tp);
+		id2 = id2.next;
+	}
+	return (id1 == nil && id2 == nil, tp);
+}
+
+polyequal(id1: ref Decl, id2: ref Decl): int
+{
+	# allow id2 list to have an optional for clause
+	ck2 := 0;
+	for(d := id2; d != nil; d = d.next)
+		if(d.ty.ids != nil)
+			ck2 = 1;
+	for(; id1 != nil; id1 = id1.next){
+		if(id2 == nil
+		|| id1.sym != id2.sym
+		|| id1.ty.decl != nil && id2.ty.decl != nil && id1.ty.decl.sym != id2.ty.decl.sym)
+			return 0;
+		if(ck2 && !idequal(id1.ty.ids, id2.ty.ids, 1, nil))
+			return 0;
+		id2 = id2.next;
+	}
+	return id1 == nil && id2 == nil;
+}
+
+calltype(f: ref Type, a: ref Node, rt: ref Type): ref Type
+{
+	t: ref Type;
+	id, first, last: ref Decl;
+
+	first = last = nil;
+	t = mktype(f.src.start, f.src.stop, Tfn, rt, nil);
+	if(f.kind == Tref)
+		t.polys = f.tof.polys;
+	else
+		t.polys = f.polys;
+	for( ; a != nil; a = a.right){
+		id = mkdecl(f.src, Darg, a.left.ty);
+		if(last == nil)
+			first = id;
+		else
+			last.next = id;
+		last = id;
+	}
+	t.ids = first;
+	if(f.kind == Tref)
+		t = mktype(f.src.start, f.src.stop, Tref, t, nil);
+	return t;
+}
+
+duptype(t: ref Type): ref Type
+{
+	nt: ref Type;
+
+	nt = ref Type;
+	*nt = *t;
+	nt.ok &= ~(OKverify|OKref|OKclass|OKsized|OKcycsize|OKcyc);
+	nt.flags |= INST;
+	nt.eq = nil;
+	nt.sbl = -1;
+	if(t.decl != nil && (nt.kind == Tadt || nt.kind == Tadtpick || nt.kind == Ttuple)){
+		nt.decl = dupdecl(t.decl);
+		nt.decl.ty = nt;
+		nt.decl.link = t.decl;
+		if(t.decl.dot != nil){
+			nt.decl.dot = dupdecl(t.decl.dot);
+			nt.decl.dot.link = t.decl.dot;
+		}
+	}
+	else
+		nt.decl = nil;
+	return nt;
+}
+
+dpolys(ids: ref Decl): int
+{
+	p: ref Decl;
+
+	for(p = ids; p != nil; p = p.next)
+		if(tpolys(p.ty))
+			return 1;
+	return 0;
+}
+
+tpolys(t: ref Type): int
+{
+	v: int;
+	tyl: ref Typelist;
+
+	if(t == nil)
+		return 0;
+	if(int(t.flags&(POLY|NOPOLY)))
+		return int(t.flags&POLY);
+	case(t.kind){
+		* =>
+			v = 0;
+			break;
+		Tarrow or
+		Tdot or
+		Tpoly =>
+			v = 1;
+			break;
+		Tref or
+		Tlist or
+		Tarray or
+		Tchan =>
+			v = tpolys(t.tof);
+			break;
+		Tid =>
+			v = tpolys(t.decl.ty);
+			break;
+		Tinst =>
+			for(tyl = t.tlist; tyl != nil; tyl = tyl.nxt)
+				if(tpolys(tyl.t)){
+					v = 1;
+					break;
+				}
+			v = tpolys(t.tof);
+			break;
+		Tfn or
+		Tadt or
+		Tadtpick or
+		Ttuple or
+		Texception =>
+			if(t.polys != nil){
+				v = 1;
+				break;
+			}
+			if(int(t.rec&TRvis))
+				return 0;
+			t.rec |= TRvis;
+			v = tpolys(t.tof) || dpolys(t.polys) || dpolys(t.ids) || dpolys(t.tags);
+			t.rec &= ~TRvis;
+			if(t.kind == Tadtpick && v == 0)
+				v = tpolys(t.decl.dot.ty);
+			break;
+	}
+	if(v)
+		t.flags |= POLY;
+	else
+		t.flags |= NOPOLY;
+	return v;
+}
+
+doccurs(ids: ref Decl, tp: ref Tpair): int
+{
+	p: ref Decl;
+
+	for(p = ids; p != nil; p = p.next){
+		if(toccurs(p.ty, tp))
+			return 1;
+	}
+	return 0;
+}
+
+toccurs(t: ref Type, tp: ref Tpair): int
+{
+	o: int;
+
+	if(t == nil)
+		return 0;
+	if(!int(t.flags&(POLY|NOPOLY)))
+		tpolys(t);
+	if(int(t.flags&NOPOLY))
+		return 0;
+	case(t.kind){
+		* =>
+			fatal("unknown type " + string t.kind + " in toccurs");
+		Tnone or
+		Tbig or
+		Tbyte or
+		Treal or
+		Tint or
+		Tstring or
+		Tfix or
+		Tmodule or
+		Terror =>
+			return 0;
+		Tarrow or
+		Tdot =>
+			return 1;
+		Tpoly =>
+			return valtmap(t, tp) != t;
+		Tref or
+		Tlist or
+		Tarray or
+		Tchan =>
+			return toccurs(t.tof, tp);
+		Tid =>
+			return toccurs(t.decl.ty, tp);
+		Tinst =>
+			for(tyl := t.tlist; tyl != nil; tyl = tyl.nxt)
+				if(toccurs(tyl.t, tp))
+					return 1;
+			return toccurs(t.tof, tp);
+		Tfn or
+		Tadt or
+		Tadtpick or
+		Ttuple or
+		Texception =>
+			if(int(t.rec&TRvis))
+				return 0;
+			t.rec |= TRvis;
+			o = toccurs(t.tof, tp) || doccurs(t.polys, tp) || doccurs(t.ids, tp) || doccurs(t.tags, tp);
+			t.rec &= ~TRvis;
+			if(t.kind == Tadtpick && o == 0)
+				o = toccurs(t.decl.dot.ty, tp);
+			return o;
+	}
+	return 0;
+}
+
+expandids(ids: ref Decl, adtt: ref Decl, tp: ref Tpair, sym: int): (ref Decl, ref Tpair)
+{
+	p, q, nids, last: ref Decl;
+
+	nids = last = nil;
+	for(p = ids; p != nil; p = p.next){
+		q = dupdecl(p);
+		(q.ty, tp) = expandtype(p.ty, nil, adtt, tp);
+		if(sym && q.ty.decl != nil)
+			q.sym = q.ty.decl.sym;
+		if(q.store == Dfn)
+			q.link = p;
+		if(nids == nil)
+			nids = q;
+		else
+			last.next = q;
+		last = q;
+	}
+	return (nids, tp);
+}
+
+expandtype(t: ref Type, instt: ref Type, adtt: ref Decl, tp: ref Tpair): (ref Type, ref Tpair)
+{
+	nt: ref Type;
+
+	if(t == nil)
+		return (nil, tp);
+	if(!toccurs(t, tp))
+		return (t, tp);
+	case(t.kind){
+		* =>
+			fatal("unknown type " + string t.kind + " in expandtype");
+		Tpoly =>
+			return (valtmap(t, tp), tp);
+		Tref or
+		Tlist or
+		Tarray or
+		Tchan =>
+			nt = duptype(t);
+			(nt.tof, tp) = expandtype(t.tof, nil, adtt, tp);
+			return (nt, tp);
+		Tid =>
+			return expandtype(idtype(t), nil, adtt, tp);
+		Tdot =>
+			return expandtype(dottype(t, adtt), nil, adtt, tp);
+		Tarrow =>
+			return expandtype(arrowtype(t, adtt), nil, adtt, tp);
+		Tinst =>
+			if((nt = valtmap(t, tp)) != t)
+				return (nt, tp);
+			(t, tp) = insttype(t, adtt, tp);
+			return expandtype(t, nil, adtt, tp);
+		Tfn or
+		Tadt or
+		Tadtpick or
+		Ttuple or
+		Texception =>
+			if((nt = valtmap(t, tp)) != t)
+				return (nt, tp);
+			if(t.kind == Tadt)
+				adtt = t.decl;
+			nt = duptype(t);
+			tp = addtmap(t, nt, tp);
+			if(instt != nil)
+				tp = addtmap(instt, nt, tp);
+			(nt.tof, tp) = expandtype(t.tof, nil, adtt, tp);
+			(nt.polys, tp) = expandids(t.polys, adtt, tp, 1);
+			(nt.ids, tp) = expandids(t.ids, adtt, tp, 0);
+			(nt.tags, tp) = expandids(t.tags, adtt, tp, 0);
+			if(t.kind == Tadt){
+				for(ids := nt.tags; ids != nil; ids = ids.next)
+					ids.ty.decl.dot = nt.decl;
+			}
+			if(t.kind == Tadtpick){
+				(nt.decl.dot.ty, tp) = expandtype(t.decl.dot.ty, nil, adtt, tp);
+			}
+			if(t.tmap != nil){
+				nt.tmap = nil;
+				for(p := t.tmap; p != nil; p = p.nxt)
+					nt.tmap = addtmap(valtmap(p.t1, tp), valtmap(p.t2, tp), nt.tmap);
+			}
+			return (nt, tp);
+	}
+	return (nil, tp);
+}
+
+#
+# create type signatures
+# sign the same information used
+# for testing type equality
+#
+sign(d: ref Decl): int
+{
+	t := d.ty;
+	if(t.sig != 0)
+		return t.sig;
+
+	if(ispoly(d))
+		rmfnptrs(d);
+
+	sigend := -1;
+	sigalloc := 1024;
+	sig: array of byte;
+	while(sigend < 0 || sigend >= sigalloc){
+		sigalloc *= 2;
+		sig = array[sigalloc] of byte;
+		eqrec = 0;
+		sigend = rtsign(t, sig, 0);
+		v := clearrec(t);
+		if(v != eqrec)
+			fatal("recid not balanced in sign: "+string v+" "+string eqrec);
+		eqrec = 0;
+	}
+
+	if(signdump != "" && dotconv(d) == signdump){
+		print("sign %s len %d\n", dotconv(d), sigend);
+		print("%s\n", string sig[:sigend]);
+	}
+
+	md5sig := array[Crypt->MD5dlen] of {* => byte 0};
+	md5(sig, sigend, md5sig, nil);
+
+	for(i := 0; i < Crypt->MD5dlen; i += 4)
+		t.sig ^= int md5sig[i+0] | (int md5sig[i+1]<<8) | (int md5sig[i+2]<<16) | (int md5sig[i+3]<<24);
+
+	if(debug['S'])
+		print("signed %s type %s len %d sig %#ux\n", dotconv(d), typeconv(t), sigend, t.sig);
+	return t.sig;
+}
+
+SIGSELF:	con byte 'S';
+SIGVARARGS:	con byte '*';
+SIGCYC:		con byte 'y';
+SIGREC:		con byte '@';
+
+sigkind := array[Tend] of
+{
+	Tnone =>	byte 'n',
+	Tadt =>		byte 'a',
+	Tadtpick =>	byte 'p',
+	Tarray =>	byte 'A',
+	Tbig =>		byte 'B',
+	Tbyte =>	byte 'b',
+	Tchan =>	byte 'C',
+	Treal =>	byte 'r',
+	Tfn =>		byte 'f',
+	Tint =>		byte 'i',
+	Tlist =>	byte 'L',
+	Tmodule =>	byte 'm',
+	Tref =>		byte 'R',
+	Tstring =>	byte 's',
+	Ttuple =>	byte 't',
+	Texception => byte 'e',
+	Tfix => byte 'x',
+	Tpoly => byte 'P',
+
+	* => 		byte 0,
+};
+
+rtsign(t: ref Type, sig: array of byte, spos: int): int
+{
+	id: ref Decl;
+
+	if(t == nil)
+		return spos;
+
+	if(spos < 0 || spos + 8 >= len sig)
+		return -1;
+
+	if(t.eq != nil && t.eq.id){
+		if(t.eq.id < 0 || t.eq.id > eqrec)
+			fatal("sign rec "+typeconv(t)+" "+string t.eq.id+" "+string eqrec);
+
+		sig[spos++] = SIGREC;
+		name := array of byte string t.eq.id;
+		if(spos + len name > len sig)
+			return -1;
+		sig[spos:] = name;
+		spos += len name;
+		return spos;
+	}
+	if(t.eq != nil){
+		eqrec++;
+		t.eq.id = eqrec;
+	}
+
+	kind := sigkind[t.kind];
+	sig[spos++] = kind;
+	if(kind == byte 0)
+		fatal("no sigkind for "+typeconv(t));
+
+	t.rec = byte 1;
+	case t.kind{
+	* =>
+		fatal("bogus type "+stypeconv(t)+" in rtsign");
+		return -1;
+	Tnone or
+	Tbig or
+	Tbyte or
+	Treal or
+	Tint or
+	Tstring or
+	Tpoly =>
+		return spos;
+	Tfix =>
+		name := array of byte string t.val.c.rval;
+		if(spos + len name - 1 >= len sig)
+			return -1;
+		sig[spos: ] = name;
+		spos += len name;
+		return spos;
+	Tref or
+	Tlist or
+	Tarray or
+	Tchan =>
+		return rtsign(t.tof, sig, spos);
+	Tfn =>
+		if(t.varargs != byte 0)
+			sig[spos++] = SIGVARARGS;
+		if(t.polys != nil)
+			spos = idsign(t.polys, 0, sig, spos);
+		spos = idsign(t.ids, 0, sig, spos);
+		if(t.eraises != nil)
+			spos = raisessign(t.eraises, sig, spos);
+		return rtsign(t.tof, sig, spos);
+	Ttuple =>
+		return idsign(t.ids, 0, sig, spos);
+	Tadt =>
+		#
+		# this is a little different than in rtequal,
+		# since we flatten the adt we used to represent the globals
+		#
+		if(t.eq == nil){
+			if(t.decl.sym.name != ".mp")
+				fatal("no t.eq field for "+typeconv(t));
+			spos--;
+			for(id = t.ids; id != nil; id = id.next){
+				spos = idsign1(id, 1, sig, spos);
+				if(spos < 0 || spos >= len sig)
+					return -1;
+				sig[spos++] = byte ';';
+			}
+			return spos;
+		}
+		if(t.polys != nil)
+			spos = idsign(t.polys, 0, sig, spos);
+		spos = idsign(t.ids, 1, sig, spos);
+		if(spos < 0 || t.tags == nil)
+			return spos;
+
+		#
+		# convert closing ')' to a ',', then sign any tags
+		#
+		sig[spos-1] = byte ',';
+		for(tg := t.tags; tg != nil; tg = tg.next){
+			name := array of byte (tg.sym.name + "=>");
+			if(spos + len name > len sig)
+				return -1;
+			sig[spos:] = name;
+			spos += len name;
+
+			spos = rtsign(tg.ty, sig, spos);
+			if(spos < 0 || spos >= len sig)
+				return -1;
+
+			if(tg.next != nil)
+				sig[spos++] = byte ',';
+		}
+		if(spos >= len sig)
+			return -1;
+		sig[spos++] = byte ')';
+		return spos;
+	Tadtpick =>
+		spos = idsign(t.ids, 1, sig, spos);
+		if(spos < 0)
+			return spos;
+		return rtsign(t.decl.dot.ty, sig, spos);
+	Tmodule =>
+		if(t.tof.linkall == byte 0)
+			fatal("signing a narrowed module");
+
+		if(spos >= len sig)
+			return -1;
+		sig[spos++] = byte '{';
+		for(id = t.tof.ids; id != nil; id = id.next){
+			if(id.tag)
+				continue;
+			if(id.sym.name == ".mp"){
+				spos = rtsign(id.ty, sig, spos);
+				if(spos < 0)
+					return -1;
+				continue;
+			}
+			spos = idsign1(id, 1, sig, spos);
+			if(spos < 0 || spos >= len sig)
+				return -1;
+			sig[spos++] = byte ';';
+		}
+		if(spos >= len sig)
+			return -1;
+		sig[spos++] = byte '}';
+		return spos;
+	}
+}
+
+idsign(id: ref Decl, usenames: int, sig: array of byte, spos: int): int
+{
+	if(spos >= len sig)
+		return -1;
+	sig[spos++] = byte '(';
+	first := 1;
+	for(; id != nil; id = id.next){
+		if(id.store == Dlocal)
+			fatal("local "+id.sym.name+" in idsign");
+
+		if(!storespace[id.store])
+			continue;
+
+		if(!first){
+			if(spos >= len sig)
+				return -1;
+			sig[spos++] = byte ',';
+		}
+
+		spos = idsign1(id, usenames, sig, spos);
+		if(spos < 0)
+			return -1;
+		first = 0;
+	}
+	if(spos >= len sig)
+		return -1;
+	sig[spos++] = byte ')';
+	return spos;
+}
+
+idsign1(id: ref Decl, usenames: int, sig: array of byte, spos: int): int
+{
+	if(usenames){
+		name := array of byte (id.sym.name+":");
+		if(spos + len name >= len sig)
+			return -1;
+		sig[spos:] = name;
+		spos += len name;
+	}
+
+	if(spos + 2 >= len sig)
+		return -1;
+
+	if(id.implicit != byte 0)
+		sig[spos++] = SIGSELF;
+
+	if(id.cyc != byte 0)
+		sig[spos++] = SIGCYC;
+
+	return rtsign(id.ty, sig, spos);
+}
+
+raisessign(n: ref Node, sig: array of byte, spos: int): int
+{
+	if(spos >= len sig)
+		return -1;
+	sig[spos++] = byte '(';
+	for(nn := n.left; nn != nil; nn = nn.right){
+		s := array of byte nn.left.decl.sym.name;
+		if(spos+len s - 1 >= len sig)
+			return -1;
+		sig[spos: ] = s;
+		spos += len s;
+		if(nn.right != nil){
+			if(spos >= len sig)
+				return -1;
+			sig[spos++] = byte ',';
+		}
+	}
+	if(spos >= len sig)
+		return -1;
+	sig[spos++] = byte ')';
+	return spos;
+}
+
+clearrec(t: ref Type): int
+{
+	id: ref Decl;
+
+	n := 0;
+	for(; t != nil && t.rec != byte 0; t = t.tof){
+		t.rec = byte 0;
+		if(t.eq != nil && t.eq.id != 0){
+			t.eq.id = 0;
+			n++;
+		}
+		if(t.kind == Tmodule){
+			for(id = t.tof.ids; id != nil; id = id.next)
+				n += clearrec(id.ty);
+			return n;
+		}
+		if(t.kind == Tadtpick)
+			n += clearrec(t.decl.dot.ty);
+		for(id = t.ids; id != nil; id = id.next)
+			n += clearrec(id.ty);
+		for(id = t.tags; id != nil; id = id.next)
+			n += clearrec(id.ty);
+		for(id = t.polys; id != nil; id = id.next)
+			n += clearrec(id.ty);
+	}
+	return n;
+}
+
+# must a variable of the given type be zeroed ? (for uninitialized declarations inside loops)
+tmustzero(t : ref Type) : int
+{
+	if(t==nil)
+		return 0;
+	if(tattr[t.kind].isptr)
+		return 1;
+	if(t.kind == Tadtpick)
+		t = t.tof;
+	if(t.kind == Ttuple || t.kind == Tadt)
+		return mustzero(t.ids);
+	return 0;
+}
+
+mustzero(decls : ref Decl) : int
+{
+	d : ref Decl;
+
+	for (d = decls; d != nil; d = d.next)
+		if (tmustzero(d.ty))
+			return 1;
+	return 0;
+}
+
+typeconv(t: ref Type): string
+{
+	if(t == nil)
+		return "nothing";
+	return tprint(t);
+}
+
+stypeconv(t: ref Type): string
+{
+	if(t == nil)
+		return "nothing";
+	return stprint(t);
+}
+
+tprint(t: ref Type): string
+{
+	id: ref Decl;
+
+	if(t == nil)
+		return "";
+	s := "";
+	if(t.kind < 0 || t.kind >= Tend){
+		s += "kind ";
+		s += string t.kind;
+		return s;
+	}
+	if(t.pr != byte 0 && t.decl != nil){
+		if(t.decl.dot != nil && !isimpmod(t.decl.dot.sym)){
+			s += t.decl.dot.sym.name;
+			s += "->";
+		}
+		s += t.decl.sym.name;
+		return s;
+	}
+	t.pr = byte 1;
+	case t.kind{
+	Tarrow =>
+		s += tprint(t.tof);
+		s += "->";
+		s += t.decl.sym.name;
+	Tdot =>
+		s += tprint(t.tof);
+		s += ".";
+		s += t.decl.sym.name;
+	Tid or
+	Tpoly =>
+		s += t.decl.sym.name;
+	Tinst =>
+		s += tprint(t.tof);
+		s += "[";
+		for(tyl := t.tlist; tyl != nil; tyl = tyl.nxt){
+			s += tprint(tyl.t);
+			if(tyl.nxt != nil)
+				s += ", ";
+		}
+		s += "]";
+	Tint or
+	Tbig or
+	Tstring or
+	Treal or
+	Tbyte or
+	Tany or
+	Tnone or
+	Terror or
+	Tainit or
+	Talt or
+	Tcase or
+	Tcasel or
+	Tcasec or
+	Tgoto or
+	Tiface or
+	Texception or
+	Texcept =>
+		s += kindname[t.kind];
+	Tfix =>
+		s += kindname[t.kind] + "(" + expconv(t.val) + ")";
+	Tref =>
+		s += "ref ";
+		s += tprint(t.tof);
+	Tchan or
+	Tarray or
+	Tlist =>
+		s += kindname[t.kind];
+		s += " of ";
+		s += tprint(t.tof);
+	Tadtpick =>
+		s += t.decl.dot.sym.name + "." + t.decl.sym.name;
+	Tadt =>
+		if(t.decl.dot != nil && !isimpmod(t.decl.dot.sym))
+			s += t.decl.dot.sym.name + "->";
+		s += t.decl.sym.name;
+		if(t.polys != nil){
+			s += "[";
+			for(id = t.polys; id != nil; id = id.next){
+				if(t.tmap != nil)
+					s += tprint(valtmap(id.ty, t.tmap));
+				else
+					s += id.sym.name;
+				if(id.next != nil)
+					s += ", ";
+			}
+			s += "]";
+		}
+	Tmodule =>
+		s += t.decl.sym.name;
+	Ttuple =>
+		s += "(";
+		for(id = t.ids; id != nil; id = id.next){
+			s += tprint(id.ty);
+			if(id.next != nil)
+				s += ", ";
+		}
+		s += ")";
+	Tfn =>
+		s += "fn";
+		if(t.polys != nil){
+			s += "[";
+			for(id = t.polys; id != nil; id = id.next){
+				s += id.sym.name;
+				if(id.next != nil)
+					s += ", ";
+			}
+			s += "]";
+		}
+		s += "(";
+		for(id = t.ids; id != nil; id = id.next){
+			if(id.sym == nil)
+				s += "nil: ";
+			else{
+				s += id.sym.name;
+				s += ": ";
+			}
+			if(id.implicit != byte 0)
+				s += "self ";
+			s += tprint(id.ty);
+			if(id.next != nil)
+				s += ", ";
+		}
+		if(t.varargs != byte 0 && t.ids != nil)
+			s += ", *";
+		else if(t.varargs != byte 0)
+			s += "*";
+		if(t.tof != nil && t.tof.kind != Tnone){
+			s += "): ";
+			s += tprint(t.tof);
+		}else
+			s += ")";
+	* =>
+		yyerror("tprint: unknown type kind "+string t.kind);
+	}
+	t.pr = byte 0;
+	return s;
+}
+
+stprint(t: ref Type): string
+{
+	if(t == nil)
+		return "";
+	s := "";
+	case t.kind{
+	Tid =>
+		s += "id ";
+		s += t.decl.sym.name;
+	Tadt or
+	Tadtpick or
+	Tmodule =>
+		return kindname[t.kind] + " " + tprint(t);
+	}
+	return tprint(t);
+}
+
+# generalize ref P.A, ref P.B to ref P
+
+# tparent(t1: ref Type, t2: ref Type): ref Type
+# {
+# 	if(t1 == nil || t2 == nil || t1.kind != Tref || t2.kind != Tref)
+# 		return t1;
+# 	t1 = t1.tof;
+# 	t2 = t2.tof;
+# 	if(t1 == nil || t2 == nil || t1.kind != Tadtpick || t2.kind != Tadtpick)
+# 		return t1;
+# 	t1 = t1.decl.dot.ty;
+# 	t2 = t2.decl.dot.ty;
+# 	if(tequal(t1, t2))
+# 		return mktype(t1.src.start, t1.src.stop, Tref, t1, nil);
+# 	return t1;
+# }
+
+tparent0(t1: ref Type, t2: ref Type): int
+{
+	id1, id2: ref Decl;
+
+	if(t1 == t2)
+		return 1;
+	if(t1 == nil || t2 == nil)
+		return 0;
+	if(t1.kind == Tadt && t2.kind == Tadtpick)
+		t2 = t2.decl.dot.ty;
+	if(t1.kind == Tadtpick && t2.kind == Tadt)
+		t1 = t1.decl.dot.ty;
+	if(t1.kind != t2.kind)
+		return 0;
+	case(t1.kind){
+	* =>
+		fatal("unknown type " + string t1.kind + " v " + string t2.kind + " in tparent");
+		break;
+	Terror or
+	Tstring or
+	Tnone or
+	Tint or
+	Tbig or
+	Tbyte or
+	Treal or
+	Tany =>
+		return 1;
+	Texception or
+	Tfix or
+	Tfn or
+	Tadt or
+	Tmodule or
+	Tpoly =>
+		return tcompat(t1, t2, 0);
+	Tref or
+	Tlist or
+	Tarray or
+	Tchan =>
+		return tparent0(t1.tof, t2.tof);
+	Ttuple =>
+		for((id1, id2) = (t1.ids, t2.ids); id1 != nil && id2 != nil; (id1, id2) = (id1.next, id2.next))
+			if(!tparent0(id1.ty, id2.ty))
+				return 0;
+		return id1 == nil && id2 == nil;
+	Tadtpick =>
+		return tequal(t1.decl.dot.ty, t2.decl.dot.ty);
+	}
+	return 0;
+}
+
+tparent1(t1: ref Type, t2: ref Type): ref Type
+{
+	t, nt: ref Type;
+	id, id1, id2, idt: ref Decl;
+
+	if(t1.kind == Tadt && t2.kind == Tadtpick)
+		t2 = t2.decl.dot.ty;
+	if(t1.kind == Tadtpick && t2.kind == Tadt)
+		t1 = t1.decl.dot.ty;
+	case(t1.kind){
+	* =>
+		return t1;
+	Tref or
+	Tlist or
+	Tarray or
+	Tchan =>
+		t = tparent1(t1.tof, t2.tof);
+		if(t == t1.tof)
+			return t1;
+		return mktype(t1.src.start, t1.src.stop, t1.kind, t, nil);
+	Ttuple =>
+		nt = nil;
+		id = nil;
+		for((id1, id2) = (t1.ids, t2.ids); id1 != nil && id2 != nil; (id1, id2) = (id1.next, id2.next)){
+			t = tparent1(id1.ty, id2.ty);
+			if(t != id1.ty){
+				if(nt == nil){
+					nt = mktype(t1.src.start, t1.src.stop, Ttuple, nil, dupdecls(t1.ids));
+					for((id, idt) = (nt.ids, t1.ids); idt != id1; (id, idt) = (id.next, idt.next))
+						;
+				}
+				id.ty = t;
+			}
+			if(id != nil)
+				id = id.next;
+		}
+		if(nt == nil)
+			return t1;
+		return nt;
+	Tadtpick =>
+		if(tequal(t1, t2))
+			return t1;
+		return t1.decl.dot.ty;
+	}
+	return t1;
+}
+
+tparent(t1: ref Type, t2: ref Type): ref Type
+{
+	if(tparent0(t1, t2))
+		return tparent1(t1, t2);
+	return t1;
+}
+
+#
+# make the tuple type used to initialize an exception type
+#
+mkexbasetype(t: ref Type): ref Type
+{
+	if(t.cons == byte 0)
+		fatal("mkexbasetype on non-constant");
+	last := mkids(t.decl.src, nil, tstring, nil);
+	last.store = Dfield;
+	nt := mktype(t.src.start, t.src.stop, Texception, nil, last);
+	nt.cons = byte 0;
+	new := mkids(t.decl.src, nil, tint, nil);
+	new.store = Dfield;
+	last.next = new;
+	last = new;
+	for(id := t.ids; id != nil; id = id.next){
+		new = ref *id;
+		new.cyc = byte 0;
+		last.next = new;
+		last = new;
+	}
+	last.next = nil;
+	return usetype(nt);
+}
+
+#
+# make an instantiated exception type
+#
+mkextype(t: ref Type): ref Type
+{
+	nt: ref Type;
+
+	if(t.cons == byte 0)
+		fatal("mkextype on non-constant");
+	if(t.tof != nil)
+		return t.tof;
+	nt = copytypeids(t);
+	nt.cons = byte 0;
+	t.tof = usetype(nt);
+	return t.tof;
+}
+
+#
+# convert an instantiated exception type to its underlying type
+#
+mkextuptype(t: ref Type): ref Type
+{
+	id: ref Decl;
+	nt: ref Type;
+
+	if(int t.cons)
+		return t;
+	if(t.tof != nil)
+		return t.tof;
+	id = t.ids;
+	if(id == nil)
+		nt = t;
+	else if(id.next == nil)
+		nt = id.ty;
+	else{
+		nt = copytypeids(t);
+		nt.cons = byte 0;
+		nt.kind = Ttuple;
+	}
+	t.tof = usetype(nt);
+	return t.tof;
+}
+
+ckfix(t: ref Type, max: real)
+{
+	s := t.val.c.rval;
+	if(max == 0.0)
+		k := (big 1<<32) - big 1;
+	else
+		k = big 2 * big (max/s) + big 1;
+	x := big 1;
+	for(p := 0; k > x; p++)
+		x *= big 2;
+	if(p == 0 || p > 32){
+		error(t.src.start, "cannot fit fixed type into an int");	
+		return;
+	}
+	if(p < 32)
+		t.val.c.rval /= real (1<<(32-p));
+}
+
+scale(t: ref Type): real
+{
+	n: ref Node;
+
+	if(t.kind == Tint || t.kind == Treal)
+		return 1.0;
+	if(t.kind != Tfix)
+		fatal("scale() on non fixed point type");
+	n = t.val;
+	if(n.op != Oconst)
+		fatal("non constant scale");
+	if(n.ty != treal)
+		fatal("non real scale");
+	return n.c.rval;
+}
+
+scale2(f: ref Type, t: ref Type): real
+{
+	return scale(f)/scale(t);
+}
+
+# put x in normal form
+nf(x: real): (int, int)
+{
+	p: int;
+	m: real;
+
+	p = 0;
+	m = x;
+	while(m >= 1.0){
+		p++;
+		m /= 2.0;
+	}
+	while(m < 0.5){
+		p--;
+		m *= 2.0;
+	}
+	m *= real (1<<16)*real (1<<15);
+	if(m >= real 16r7fffffff - 0.5)
+		return (p, 16r7fffffff);
+	return (p, int m);
+}
+
+ispow2(x: real): int
+{
+	m: int;
+
+	(nil, m) = nf(x);
+	if(m != 1<<30)
+		return 0;
+	return 1;
+}
+
+round(x: real, n: int): (int, int)
+{
+	if(n != 31)
+		fatal("not 31 in round");
+	return nf(x);
+}
+
+fixmul2(sx: real, sy: real, sr: real): (int, int, int)
+{
+	k, n, a: int;
+	alpha: real;
+
+	alpha = (sx*sy)/sr;
+	n = 31;
+	(k, a) = round(1.0/alpha, n);
+	return (IMULX, 1-k, 0);
+}
+
+fixdiv2(sx: real, sy: real, sr: real): (int, int, int)
+{
+	k, n, b: int;
+	beta: real;
+
+	beta = sx/(sy*sr);
+	n = 31;
+	(k, b) = round(beta, n);
+	return (IDIVX, k-1, 0);
+}
+
+fixmul(sx: real, sy: real, sr: real): (int, int, int)
+{
+	k, m, n, a, v: int;
+	W: big;
+	alpha, eps: real;
+
+	alpha = (sx*sy)/sr;
+	if(ispow2(alpha))
+		return fixmul2(sx, sy, sr);
+	n = 31;
+	(k, a) = round(1.0/alpha, n);
+	m = n-k;
+	if(m < -n-1)
+		return (IMOVW, 0, 0);	# result is zero whatever the values
+	v = 0;
+	W = big 0;
+	eps = real(1<<m)/(alpha*real(a)) - 1.0;
+	if(eps < 0.0){
+		v = a-1;
+		eps = -eps;
+	}
+	if(m < 0 && real(1<<n)*eps*real(a) >= real(a)-1.0+real(1<<m))
+		W = (big(1)<<(-m)) - big 1;
+	if(v != 0 || W != big 0)
+		m = m<<2|(v != 0)<<1|(W != big 0);
+	if(v == 0 && W == big 0)
+		return (IMULX0, m, a);
+	else
+		return (IMULX1, m, a);
+}
+
+fixdiv(sx: real, sy: real, sr: real): (int, int, int)
+{
+	k, m, n, b, v: int;
+	W: big;
+	beta, eps: real;
+
+	beta = sx/(sy*sr);
+	if(ispow2(beta))
+		return fixdiv2(sx, sy, sr);
+	n = 31;
+	(k, b) = round(beta, n);
+	m = k-n;
+	if(m <= -2*n)
+		return (IMOVW, 0, 0);	#result is zero whatever the values
+	v = 0;
+	W = big 0;
+	eps = (real(1<<m)*real(b))/beta - 1.0;
+	if(eps < 0.0)
+		v = 1;
+	if(m < 0)
+		W = (big(1)<<(-m)) - big 1;
+	if(v != 0 || W != big 0)
+		m = m<<2|(v != 0)<<1|(W != big 0);
+	if(v == 0 && W == big 0)
+		return (IDIVX0, m, b);
+	else
+		return (IDIVX1, m, b);
+}
+
+fixcast(sx: real, sr: real): (int, int, int)
+{
+	(op, p, a) := fixmul(sx, 1.0, sr);
+	return (op-IMULX+ICVTXX, p, a);
+}
+
+fixop(op: int, tx: ref Type, ty: ref Type, tr: ref Type): (int, int, int)
+{
+	sx, sy, sr: real;
+
+	sx = scale(tx);
+	sy = scale(ty);
+	sr = scale(tr);
+	if(op == IMULX)
+		return fixmul(sx, sy, sr);
+	else if(op == IDIVX)
+		return fixdiv(sx, sy, sr);
+	else
+		return fixcast(sx, sr);
+}
+
+ispoly(d: ref Decl): int
+{
+	if(d == nil)
+		return 0;
+	t := d.ty;
+	if(t.kind == Tfn){
+		if(t.polys != nil)
+			return 1;
+		if((d = d.dot) == nil)
+			return 0;
+		t = d.ty;
+		return t.kind == Tadt && t.polys != nil;
+	}
+	return 0;
+}
+
+ispolyadt(t: ref Type): int
+{
+	return (t.kind == Tadt || t.kind == Tadtpick) && t.polys != nil && (t.flags & INST) == byte 0;
+}
+
+polydecl(ids: ref Decl): ref Decl
+{
+	id: ref Decl;
+	t: ref Type;
+
+	for(id = ids; id != nil; id = id.next){
+		t = mktype(id.src.start, id.src.stop, Tpoly, nil, nil);
+		id.ty = t;
+		t.decl = id;
+	}
+	return ids;
+}
+
+# try to convert an expression tree to a type
+exptotype(n: ref Node): ref Type
+{
+	t, tt: ref Type;
+	d: ref Decl;
+	tll: ref Typelist;
+	src: Src;
+
+	if(n == nil)
+		return nil;
+	t = nil;
+	case(n.op){
+		Oname =>
+			if((d = n.decl) != nil && d.store == Dtype)
+				t = d.ty;
+		Otype or Ochan =>
+			t = n.ty;
+		Oref =>
+			t = exptotype(n.left);
+			if(t != nil)
+				t = mktype(n.src.start, n.src.stop, Tref, t, nil);
+		Odot =>
+			t = exptotype(n.left);
+			if(t != nil){
+				d = namedot(t.tags, n.right.decl.sym);
+				if(d == nil)
+					t = nil;
+				else
+					t = d.ty;
+			}
+			if(t == nil)
+				t = exptotype(n.right);
+		Omdot =>
+			t = exptotype(n.right);
+		Oindex =>
+			t = exptotype(n.left);
+			if(t != nil){
+				src = n.src;
+				tll = nil;
+				for(n = n.right; n != nil; n = n.right){
+					if(n.op == Oseq)
+						tt = exptotype(n.left);
+					else
+						tt = exptotype(n);
+					if(tt == nil)
+						return nil;
+					tll = addtype(tt, tll);
+					if(n.op != Oseq)
+						break;
+				}
+				t = mkinsttype(src, t, tll);
+			}
+	}
+	return t;
+}
+
+uname(im: ref Decl): string
+{
+	s := "";
+	for(p := im; p != nil; p = p.next){
+		s += p.sym.name;
+		if(p.next != nil)
+			s += "+";
+	}
+	return s;
+}
+
+# check all implementation modules have consistent declarations
+# and create their union if needed
+#
+modimp(dl: ref Dlist, im: ref Decl): ref Decl
+{
+	u, d, dd, ids, dot, last: ref Decl;
+	s: ref Sym;
+
+	if(dl.next == nil)
+		return dl.d;
+	dl0 := dl;
+	sg0 := 0;
+	un := uname(im);
+	installids(Dglobal, mkids(dl.d.src, enter(".m."+un, 0), tnone, nil));
+	u = dupdecl(dl.d);
+	u.sym = enter(un, 0);
+	u.sym.decl = u;
+	u.ty = mktype(u.src.start, u.src.stop, Tmodule, nil, nil);
+	u.ty.decl = u;
+	for( ; dl != nil; dl = dl.next){
+		d = dl.d;
+		ids = d.ty.tof.ids;	# iface
+		if(ids != nil && ids.store == Dglobal)	# .mp
+			sg := sign(ids);
+		else
+			sg = 0;
+		if(dl == dl0)
+			sg0 = sg;
+		else if(sg != sg0)
+			error(d.src.start, d.sym.name + "'s module data not consistent with that of " + dl0.d.sym.name + "\n");
+		for(ids = d.ty.ids; ids != nil; ids = ids.next){
+			s = ids.sym;
+			if(s.decl != nil && s.decl.scope >= scope){
+				if(ids == s.decl){
+					dd = dupdecl(ids);
+					if(u.ty.ids == nil)
+						u.ty.ids = dd;
+					else
+						last.next = dd;
+					last = dd;
+					continue;
+				}
+				dot = s.decl.dot;
+				if(s.decl.store != Dwundef && dot != nil && dot != d && isimpmod(dot.sym) && dequal(ids, s.decl, 1))
+					ids.refs = s.decl.refs;
+				else
+					redecl(ids);
+				ids.init = s.decl.init;
+			}
+		}
+	}
+	u.ty = usetype(u.ty);
+	return u;
+}
+
+modres(d: ref Decl)
+{
+	ids, id, n, i: ref Decl;
+	t: ref Type;
+
+	for(ids = d.ty.ids; ids != nil; ids = ids.next){
+		id = ids.sym.decl;
+		if(ids != id){
+			n = ids.next;
+			i = ids.iface;
+			t = ids.ty;
+			*ids = *id;
+			ids.next = n;
+			ids.iface = i;
+			ids.ty = t;
+		}
+	}
+}
+
+# update the fields of duplicate declarations in other implementation modules
+# and their union
+#	
+modresolve()
+{
+	dl: ref Dlist;
+
+	dl = impdecls;
+	if(dl.next == nil)
+		return;
+	for( ; dl != nil; dl = dl.next)
+		modres(dl.d);
+	modres(impdecl);
+}
--- /dev/null
+++ b/appl/cmd/listen.b
@@ -1,0 +1,266 @@
+implement Listen;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	auth: Auth;
+include "dial.m";
+	dial: Dial;
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+
+Listen: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+badmodule(p: string)
+{
+	sys->fprint(stderr(), "listen: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+serverkey: ref Keyring->Authinfo;
+verbose := 0;
+
+init(drawctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	auth = load Auth Auth->PATH;
+	if (auth == nil)
+		badmodule(Auth->PATH);
+	dial = load Dial Dial->PATH;
+	if (dial == nil)
+		badmodule(Dial->PATH);
+	sh = load Sh Sh->PATH;
+	if (sh == nil)
+		badmodule(Sh->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+	auth->init();
+	algs: list of string;
+	arg->init(argv);
+	keyfile: string;
+	initscript: string;
+	doauth := 1;
+	synchronous := 0;
+	trusted := 0;
+	arg->setusage("listen [-i {initscript}] [-Ast] [-k keyfile] [-a alg]... addr command [arg...]");
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'a' =>
+			algs = arg->earg() :: algs;
+		'A' =>
+			doauth = 0;
+		'f' or
+		'k' =>
+			keyfile = arg->earg();
+			if (! (keyfile[0] == '/' || (len keyfile > 2 &&  keyfile[0:2] == "./")))
+				keyfile = "/usr/" + user() + "/keyring/" + keyfile;
+		'i' =>
+			initscript = arg->earg();
+		'v' =>
+			verbose = 1;
+		's' =>
+			synchronous = 1;
+		't' =>
+			trusted = 1;
+		* =>
+			arg->usage();
+		}
+	}
+	if (doauth && algs == nil)
+		algs = getalgs();
+	if (algs != nil) {
+		if (keyfile == nil)
+			keyfile = "/usr/" + user() + "/keyring/default";
+		serverkey = keyring->readauthinfo(keyfile);
+		if (serverkey == nil) {
+			sys->fprint(stderr(), "listen: cannot read %s: %r\n", keyfile);
+			raise "fail:bad keyfile";
+		}
+	}
+	if(!trusted){
+		sys->unmount(nil, "/mnt/keys");	# should do for now
+		# become none?
+	}
+
+	argv = arg->argv();
+	n := len argv;
+	if (n < 2)
+		arg->usage();
+	arg = nil;
+
+	sync := chan[1] of string;
+	spawn listen(drawctxt, hd argv, tl argv, algs,  initscript, sync);
+	e := <-sync;
+	if(e != nil)
+		raise "fail:" + e;
+	if(synchronous){
+		e = <-sync;
+		if(e != nil)
+			raise "fail:" + e;
+	}
+}
+
+listen(drawctxt: ref Draw->Context, addr: string, argv: list of string,
+		algs: list of string, initscript: string, sync: chan of string)
+{
+	{
+		listen1(drawctxt, addr, argv, algs, initscript, sync);
+	} exception e {
+	"fail:*" =>
+		sync <-= e;
+	}
+}
+
+listen1(drawctxt: ref Draw->Context, addr: string, argv: list of string,
+		algs: list of string, initscript: string, sync: chan of string)
+{
+	sys->pctl(Sys->FORKFD, nil);
+
+	ctxt := Context.new(drawctxt);
+	acon := dial->announce(addr);
+	if (acon == nil) {
+		sys->fprint(stderr(), "listen: failed to announce on '%s': %r\n", addr);
+		sync <-= "cannot announce";
+		exit;
+	}
+	ctxt.set("user", nil);
+	if (initscript != nil) {
+		ctxt.setlocal("net", ref Sh->Listnode(nil, acon.dir) :: nil);
+		ctxt.run(ref Sh->Listnode(nil, initscript) :: nil, 0);
+		initscript = nil;
+	}
+
+	# make sure the shell command is parsed only once.
+	cmd := sh->stringlist2list(argv);
+	if((hd argv) != nil && (hd argv)[0] == '{'){
+		(c, e) := sh->parse(hd argv);
+		if(c == nil){
+			sys->fprint(stderr(), "listen: %s\n", e);
+			sync <-= "parse error";
+			exit;
+		}
+		cmd = ref Sh->Listnode(c, hd argv) :: tl cmd;
+	}
+
+	sync <-= nil;
+	listench := chan of (int, ref Dial->Connection);
+	authch := chan of (string, ref Dial->Connection);
+	spawn listener(listench, acon, addr);
+	for (;;) {
+		user := "";
+		ccon: ref Dial->Connection;
+		alt {
+		(lok, c) := <-listench =>
+			if (lok == -1){
+				sync <-= "listen";
+				exit;
+			}
+			if (algs != nil) {
+				spawn authenticator(authch, c, algs, addr);
+				continue;
+			}
+			ccon = c;
+		(user, ccon) = <-authch =>
+			;
+		}
+		if (user != nil)
+			ctxt.set("user", sh->stringlist2list(user :: nil));
+		ctxt.set("net", ref Sh->Listnode(nil, ccon.dir) :: nil);
+
+		# XXX could do this in a separate process too, to
+		# allow new connections to arrive and start authenticating
+		# while the shell command is still running.
+		sys->dup(ccon.dfd.fd, 0);
+		sys->dup(ccon.dfd.fd, 1);
+		ccon.dfd = ccon.cfd = nil;
+		ctxt.run(cmd, 0);
+		sys->dup(2, 0);
+		sys->dup(2, 1);
+	}
+}
+
+listener(listench: chan of (int, ref Dial->Connection), c: ref Dial->Connection, addr: string)
+{
+	for (;;) {
+		nc := dial->listen(c);
+		if (nc == nil) {
+			sys->fprint(stderr(), "listen: listen error on '%s': %r\n", addr);
+			listench <-= (-1, nc);
+			exit;
+		}
+		if (verbose)
+			sys->fprint(stderr(), "listen: got connection on %s from %s",
+					addr, readfile(nc.dir + "/remote"));
+		nc.dfd = dial->accept(nc);
+		if (nc.dfd == nil)
+			sys->fprint(stderr(), "listen: cannot open %s: %r\n", nc.dir + "/data");
+		else{
+			if(nc.cfd != nil)
+				sys->fprint(nc.cfd, "keepalive");
+			listench <-= (0, nc);
+		}
+	}
+}
+
+authenticator(authch: chan of (string, ref Dial->Connection),
+		c: ref Dial->Connection, algs: list of string, addr: string)
+{
+	err: string;
+	(c.dfd, err) = auth->server(algs, serverkey, c.dfd, 0);
+	if (c.dfd == nil) {
+		sys->fprint(stderr(), "listen: auth on %s failed: %s\n", addr, err);
+		return;
+	}
+	if (verbose)
+		sys->fprint(stderr(), "listen: authenticated on %s as %s\n", addr, err);
+	authch <-= (err, c);
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+user(): string
+{
+	u := readfile("/dev/user");
+	if (u == nil)
+		return "nobody";
+	return u;
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
+
+getalgs(): list of string
+{
+	sslctl := readfile("#D/clone");
+	if (sslctl == nil) {
+		sslctl = readfile("#D/ssl/clone");
+		if (sslctl == nil)
+			return nil;
+		sslctl = "#D/ssl/" + sslctl;
+	} else
+		sslctl = "#D/" + sslctl;
+	(nil, algs) := sys->tokenize(readfile(sslctl + "/encalgs") + " " + readfile(sslctl + "/hashalgs"), " \t\n");
+	return "none" :: algs;
+}
--- /dev/null
+++ b/appl/cmd/lockfs.b
@@ -1,0 +1,763 @@
+implement Lockfs;
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+include "styxlib.m";
+	styxlib: Styxlib;
+	Dirtab, Styxserver, Chan,
+	devdir,
+	Eperm, Ebadfid, Eexists, Enotdir, Enotfound, Einuse: import styxlib;
+include "arg.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	auth: Auth;
+include "dial.m";
+	dial: Dial;
+
+Lockfs: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+	dirgen: fn(srv: ref Styxlib->Styxserver, c: ref Styxlib->Chan,
+			tab: array of Styxlib->Dirtab, i: int): (int, Sys->Dir);
+};
+
+Elocked: con "file is locked";
+
+devgen: Dirgenmod;
+
+Openreq: adt {
+	srv: ref Styxserver;
+	tag: int;
+	omode: int;
+	c: ref Chan;
+	uproc: Uproc;
+};
+
+Lockqueue: adt {
+	h: list of ref Openreq; 
+	t: list of ref Openreq;
+	put: fn(q: self ref Lockqueue, s: ref Openreq);
+	get: fn(q: self ref Lockqueue): ref Openreq;
+	peek: fn(q: self ref Lockqueue): ref Openreq;
+	flush: fn(q: self ref Lockqueue, srv: ref Styxserver, tag: int);
+};
+
+Lockfile: adt {
+	waitq: ref Lockqueue;
+	fd: ref Sys->FD;
+	readers: int;
+	writers: int;
+	d: Sys->Dir;
+};
+
+Ureq: adt {
+	fname: string;
+	pick {
+	Open =>
+		omode: int;
+	Create =>
+		omode: int;
+		perm: int;
+	Remove =>
+	Wstat =>
+		dir: Sys->Dir;
+	}
+};
+
+Uproc: type chan of (ref Ureq, chan of (ref Sys->FD, string));
+
+maxqidpath := big 1;
+locks: list of ref Lockfile;
+lockdir: string;
+authinfo: ref Keyring->Authinfo;
+timefd: ref Sys->FD;
+
+MAXCONN: con 20;
+
+verbose := 0;
+
+usage()
+{
+	sys->fprint(stderr, "usage: lockfs [-A] [-a alg]... [-p addr] dir [mountpoint]\n");
+	raise "fail:usage";
+}
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "lockfs: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	styx = load Styx Styx->PATH;
+	if (styx == nil)
+		badmodule(Styx->PATH);
+	dial = load Dial Dial->PATH;
+	if (dial == nil)
+		badmodule(Dial->PATH);
+	styx->init();
+	styxlib = load Styxlib Styxlib->PATH;
+	if (styxlib == nil)
+		badmodule(Styxlib->PATH);
+	styxlib->init(styx);
+	devgen = load Dirgenmod "$self";
+	if (devgen == nil)
+		badmodule("self as Dirgenmod");
+	timefd = sys->open("/dev/time", sys->OREAD);
+	if (timefd == nil) {
+		sys->fprint(stderr, "lockfs: cannot open /dev/time: %r\n");
+		raise "fail:no time";
+	}
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+	arg->init(argv);
+
+	addr := "";
+	doauth := 1;
+	algs: list of string;
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'p' =>
+			addr = arg->arg();
+		'a' =>
+			alg := arg->arg();
+			if (alg == nil)
+				usage();
+			algs = alg :: algs;
+		'A' =>
+			doauth = 0;
+		'v' =>
+			verbose = 1;
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	if (argv == nil || (addr != nil && tl argv != nil))
+		usage();
+	if (addr == nil)
+		doauth = 0;		# no authentication necessary for local mount
+	if (doauth) {
+		auth = load Auth Auth->PATH;
+		if (auth == nil)
+			badmodule(Auth->PATH);
+		if ((e := auth->init()) != nil) {
+			sys->fprint(stderr, "lockfs: cannot init auth: %s\n", e);
+			raise "fail:errors";
+		}
+		keyring = load Keyring Keyring->PATH;
+		if (keyring == nil)
+			badmodule(Keyring->PATH);
+		authinfo = keyring->readauthinfo("/usr/" + user() + "/keyring/default");
+	}
+
+	mountpoint := lockdir = hd argv;
+	if (tl argv != nil)
+		mountpoint = hd tl argv;
+	if (addr != nil) {
+		if (doauth && algs == nil)
+			algs = "none" :: nil;		# XXX is this default a bad idea?
+		srvrq := chan of (ref Sys->FD, string, Uproc);
+		srvsync := chan of (int, string);
+		spawn listener(addr, srvrq, srvsync, algs);
+		(srvpid, err) := <-srvsync;
+		srvsync = nil;
+		if (srvpid == -1) {
+			sys->fprint(stderr, "lockfs: failed to start listener: %s\n", err);
+			raise "fail:errors";
+		}
+		sync := chan of int;
+		spawn server(srvrq, sync);
+		<-sync;
+	} else {
+		rq := chan of (ref Sys->FD, string, Uproc);
+		fds := array[2] of ref Sys->FD;
+		sys->pipe(fds);
+		sync := chan of int;
+		spawn server(rq, sync);
+		<-sync;
+		rq <-= (fds[0], "lock", nil);
+		rq <-= (nil, nil, nil);
+		if (sys->mount(fds[1], nil, mountpoint, Sys->MREPL | Sys->MCREATE, nil) == -1) {
+			sys->fprint(stderr, "lockfs: cannot mount: %r\n");
+			raise "fail:cannot mount";
+		}
+	}
+}
+
+server(srvrq: chan of (ref Sys->FD, string, Uproc), sync: chan of int)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+	down := 0;
+	nclient := 0;
+	tchans := array[MAXCONN] of chan of ref Tmsg;
+	srv := array[MAXCONN] of ref Styxserver;
+	uprocs := array[MAXCONN] of Uproc;
+	lockinit();
+Service:
+	for (;;) alt {
+	(fd, reqstr, uprocch) := <-srvrq =>
+		if (fd == nil) {
+			if (verbose && reqstr != nil)
+				sys->print("lockfs: localserver going down (reason: %s)\n", reqstr);
+			down = 1;
+		} else {
+			if (verbose)
+				sys->print("lockfs: got new connection (s == '%s')\n", reqstr);
+			for (i := 0; i < len tchans; i++)
+				if (tchans[i] == nil) {
+					(tchans[i], srv[i]) = Styxserver.new(fd);
+					if(verbose)
+						sys->print("svc started\n");
+					uprocs[i] = uprocch;
+					break;
+				}
+			if (i == len tchans) {
+				sys->fprint(stderr, "lockfs: too many clients\n");	# XXX expand arrays
+				if (uprocch != nil)
+					uprocch <-= (nil, nil);
+			} else
+				nclient++;
+		}
+	(n, gm) := <-tchans =>
+		if (handletmsg(srv[n], gm, uprocs[n]) == -1) {
+			tchans[n] = nil;
+			srv[n] = nil;
+			if (uprocs[n] != nil) {
+				uprocs[n] <-= (nil, nil);
+				uprocs[n] = nil;
+			}
+			if (nclient-- <= 1 && down)
+				break Service;
+		}
+	}
+	if (verbose)
+		sys->print("lockfs: finished\n");
+}
+
+dirgen(nil: ref Styxserver, nil: ref Styxlib->Chan,
+				nil: array of Dirtab, s: int): (int, Sys->Dir)
+{
+	d: Sys->Dir;
+	ll := locks;
+	for (i := 0; i < s && ll != nil; i++)
+		ll = tl ll;
+	if (ll == nil)
+		return (-1, d);
+	return (1, (hd ll).d);
+}
+		
+handletmsg(srv:  ref Styxserver, gm: ref Tmsg, uproc: Uproc): int
+{
+{
+	if (gm == nil)
+		gm = ref Tmsg.Readerror(-1, "eof");
+	if(verbose)
+		sys->print("<- %s\n", gm.text());
+	pick m := gm {
+	Readerror =>
+		# could be more efficient...
+		for (cl := srv.chanlist(); cl != nil; cl = tl cl) {
+			c := hd cl;
+			for (ll := locks; ll != nil; ll = tl ll) {
+				if ((hd ll).d.qid.path == c.qid.path) {
+					l := hd ll;
+					l.waitq.flush(srv, -1);
+					if (c.open)
+						unlocked(l);
+					break;
+				}
+			}
+		}
+		if (m.error != "eof")
+			sys->fprint(stderr, "lockfs: read error: %s\n", m.error);
+		return -1;
+	Version =>
+		srv.devversion(m);
+	Auth =>
+		srv.devauth(m);
+	Walk =>
+		c := fid2chan(srv, m.fid);
+		qids: array of Sys->Qid;
+		cc := ref *c;
+		if (len m.names > 0) {
+			qids = array[1] of Sys->Qid;	# it's just one level
+			if ((cc.qid.qtype & Sys->QTDIR) == 0) {
+				srv.reply(ref Rmsg.Error(m.tag, Enotdir));
+				break;
+			}
+			for (ll := locks; ll != nil; ll = tl ll)
+				if ((hd ll).d.name == m.names[0])
+					break;
+			if (ll == nil) {
+				srv.reply(ref Rmsg.Error(m.tag, Enotfound));
+				break;
+			}
+			d := (hd ll).d;
+			cc.qid = d.qid;
+			cc.path = d.name;
+			qids[0] = c.qid;
+		}
+		if(m.newfid != m.fid){
+			nc := srv.clone(cc, m.newfid);
+			if(nc == nil){
+				srv.reply(ref Rmsg.Error(m.tag, Einuse));
+				break;
+			}
+		}else{
+			c.qid = cc.qid;
+			c.path = cc.path;
+		}
+		srv.reply(ref Rmsg.Walk(m.tag, qids));
+	Open =>
+		c := fid2chan(srv, m.fid);
+		if (c.qid.qtype & Sys->QTDIR) {
+			srv.reply(ref Rmsg.Open(m.tag, c.qid, Styx->MAXFDATA));
+			break;
+		}
+		for (ll := locks; ll != nil; ll = tl ll)
+			if ((hd ll).d.qid.path == c.qid.path)
+				break;
+		if (ll == nil) {
+			srv.reply(ref Rmsg.Error(m.tag, Enotfound));
+			break;
+		}
+		l := hd ll;
+		req := ref Openreq(srv, m.tag, m.mode, c, uproc);
+		if (l.fd == nil || (m.mode == Sys->OREAD && l.writers == 0)) {
+			openlockfile(l, req);
+		} else {
+			l.waitq.put(req);
+		}
+		req = nil;
+	Create =>
+		c := fid2chan(srv, m.fid);
+		if ((c.qid.qtype & Sys->QTDIR) == 0) {
+			srv.reply(ref Rmsg.Error(m.tag, Enotdir));
+			break;
+		}
+		if (m.perm & Sys->DMDIR) {
+			srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			break;
+		}
+		for (ll := locks; ll != nil; ll = tl ll)
+			if ((hd ll).d.name == m.name)
+				break;
+		if (ll != nil) {
+			srv.reply(ref Rmsg.Error(m.tag, Eexists));
+			break;
+		}
+		(fd, err) := create(uproc, lockdir + "/" + m.name, m.mode, m.perm);
+		if (fd == nil) {
+			srv.reply(ref Rmsg.Error(m.tag, err));
+			break;
+		}
+		(ok, d) := sys->fstat(fd);
+		if (ok == -1) {
+			srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+			break;
+		}
+		l := ref Lockfile(ref Lockqueue, fd, 0, 0, d);
+		l.d.qid = (maxqidpath++, 0, Sys->QTFILE);
+		l.d.mtime = l.d.atime = now();
+		if (m.mode == Sys->OREAD)
+			l.readers = 1;
+		else
+			l.writers = 1;
+		locks = l :: locks;
+		c.qid.path = (hd locks).d.qid.path;
+		c.open = 1;
+		srv.reply(ref Rmsg.Create(m.tag, c.qid, Styx->MAXFDATA));
+	Read =>
+		c := fid2chan(srv, m.fid);
+		if (c.qid.qtype & Sys->QTDIR)
+			srv.devdirread(m, devgen, nil);
+		else {
+			l := qid2lock(c.qid);
+			if (l == nil)
+				srv.reply(ref Rmsg.Error(m.tag, Enotfound));
+			else {
+				d := array[m.count] of byte;
+				sys->seek(l.fd, m.offset, Sys->SEEKSTART);
+				n := sys->read(l.fd, d, m.count);
+				if (n == -1)
+					srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+				else {
+					srv.reply(ref Rmsg.Read(m.tag, d[0:n]));
+					l.d.atime = now();
+				}
+			}
+		}
+	Write =>
+		c := fid2chan(srv, m.fid);
+		if (c.qid.qtype & Sys->QTDIR) {
+			srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			break;
+		}
+		l := qid2lock(c.qid);
+		if (l == nil) {
+			srv.reply(ref Rmsg.Error(m.tag, Enotfound));
+			break;
+		}
+		sys->seek(l.fd, m.offset, Sys->SEEKSTART);
+		n := sys->write(l.fd, m.data, len m.data);
+		if (n == -1)
+			srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+		else {
+			srv.reply(ref Rmsg.Write(m.tag, n));
+			nlength := m.offset + big n;
+			if (nlength > l.d.length)
+				l.d.length = nlength;
+			l.d.mtime = now();
+			l.d.qid.vers++;
+		}
+	Clunk =>
+		c := srv.devclunk(m);
+		if (c != nil && c.open && (l := qid2lock(c.qid)) != nil)
+			unlocked(l);
+	Flush =>
+		for (ll := locks; ll != nil; ll = tl ll)
+			(hd ll).waitq.flush(srv, m.tag);
+		srv.reply(ref Rmsg.Flush(m.tag));
+	Stat =>
+		srv.devstat(m, devgen, nil);
+	Remove =>
+		c := fid2chan(srv, m.fid);
+		srv.chanfree(c);
+		if (c.qid.qtype & Sys->QTDIR) {
+			srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			break;
+		}
+		l := qid2lock(c.qid);
+		if (l == nil) {
+			srv.reply(ref Rmsg.Error(m.tag, Enotfound));
+			break;
+		}
+		if (l.fd != nil) {
+			srv.reply(ref Rmsg.Error(m.tag, Elocked));
+			break;
+		}
+		if ((err := remove(uproc, lockdir + "/" + l.d.name)) == nil) {
+			srv.reply(ref Rmsg.Error(m.tag, err));
+			break;
+		}
+		ll: list of ref Lockfile;
+		for (; locks != nil; locks = tl locks)
+			if (hd locks != l)
+				ll = hd locks :: ll;
+		locks = ll;
+		srv.reply(ref Rmsg.Remove(m.tag));
+	Wstat =>
+		c := fid2chan(srv, m.fid);
+		if (c.qid.qtype & Sys->QTDIR) {
+			srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			break;
+		}
+		l := qid2lock(c.qid);
+		if (l == nil) {
+			srv.reply(ref Rmsg.Error(m.tag, Enotfound));
+			break;
+		}
+		if ((err := wstat(uproc, lockdir + "/" + l.d.name, m.stat)) != nil) {
+			srv.reply(ref Rmsg.Error(m.tag, err));
+			break;
+		}
+		(ok, d) := sys->stat(lockdir + "/" + m.stat.name);
+		if (ok == -1) {
+			srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+			break;
+		}
+		d.qid = l.d.qid;
+		l.d = d;
+		srv.reply(ref Rmsg.Wstat(m.tag));
+	Attach =>
+		srv.devattach(m);
+	}
+	return 0;
+}
+exception e{
+	"panic:*" =>
+		sys->fprint(stderr, "lockfs: %s\n", e);
+		srv.reply(ref Rmsg.Error(gm.tag, e[len "panic:":]));
+		return 0;
+}
+}
+
+unlocked(l: ref Lockfile)
+{
+	if (l.readers > 0)
+		l.readers--;
+	else
+		l.writers--;
+	if (l.readers > 0)
+		return;
+	l.fd = nil;
+
+	# unblock all readers at the head of the queue.
+	# XXX should we queuejump other readers?
+	while ((nreq := l.waitq.peek()) != nil && l.writers == 0) {
+		if (nreq.omode != Sys->OREAD && l.readers > 0)
+			break;
+		openlockfile(l, nreq);
+		l.waitq.get();
+	}
+}
+
+openlockfile(l: ref Lockfile, req: ref Openreq): int
+{
+	err: string;
+	(l.fd, err) = open(req.uproc, lockdir + "/" + l.d.name, req.omode);
+	if (l.fd == nil) {
+		req.srv.reply(ref Rmsg.Error(req.tag, err));
+		return -1;
+	}
+	req.c.open = 1;
+	if (req.omode & Sys->OTRUNC)
+		l.d.length = big 0;
+	req.srv.reply(ref Rmsg.Open(req.tag, l.d.qid, Styx->MAXFDATA));
+	if (req.omode == Sys->OREAD)
+		l.readers++;
+	else
+		l.writers++;
+	return 0;
+}
+
+qid2lock(q: Sys->Qid): ref Lockfile
+{
+	for (ll := locks; ll != nil; ll = tl ll)
+		if ((hd ll).d.qid.path == q.path)
+			return hd ll;
+	return nil;
+}
+
+lockinit()
+{
+	fd := sys->open(lockdir, Sys->OREAD);
+	if (fd == nil)
+		return;
+
+	lockl: list of ref Lockfile;
+	# XXX if O(n²) behaviour is a problem, use Readdir module
+	for(;;){
+		(n, e) := sys->dirread(fd);
+		if(n <= 0)
+			break;
+		for (i := 0; i < n; i++) {
+			for (l := lockl; l != nil; l = tl l)
+				if ((hd l).d.name == e[i].name)
+					break;
+			if (l == nil) {
+				e[i].qid = (maxqidpath++, 0, Sys->QTFILE);
+				lockl = ref Lockfile(ref Lockqueue, nil, 0, 0, e[i]) :: lockl;
+			}
+		}
+	}
+	# remove all directories from list
+	for (locks = nil; lockl != nil; lockl = tl lockl)
+		if (((hd lockl).d.mode & Sys->DMDIR) == 0)
+			locks = hd lockl :: locks;
+}
+
+
+fid2chan(srv: ref Styxserver, fid: int): ref Chan
+{
+	c := srv.fidtochan(fid);
+	if (c == nil)
+		raise "panic:bad fid";
+	return c;
+}
+
+Lockqueue.put(q: self ref Lockqueue, s: ref Openreq)
+{
+        q.t = s :: q.t;
+}
+
+Lockqueue.get(q: self ref Lockqueue): ref Openreq
+{
+        s: ref Openreq;
+        if(q.h == nil)
+                (q.h, q.t) = (revrqlist(q.t), nil);
+
+        if(q.h != nil)
+                (s, q.h) = (hd q.h, tl q.h);
+
+        return s;
+}
+
+Lockqueue.peek(q: self ref Lockqueue): ref Openreq
+{
+	s := q.get();
+	if (s != nil)
+		q.h = s :: q.h;
+	return s;
+}
+
+doflush(l: list of ref Openreq, srv: ref Styxserver, tag: int): list of ref Openreq
+{
+	oldl := l;
+	nl: list of ref Openreq;
+	doneone := 0;
+	while (l != nil) {
+		oreq := hd l;
+		if (oreq.srv != srv || (tag != -1 && oreq.tag != tag))
+			nl = oreq :: nl;
+		else
+			doneone = 1;
+		l = tl l;
+	}
+	if (doneone)
+		return revrqlist(nl);
+	else
+		return oldl;
+}
+
+Lockqueue.flush(q: self ref Lockqueue, srv: ref Styxserver, tag: int)
+{
+	q.h = doflush(q.h, srv, tag);
+	q.t = doflush(q.t, srv, tag);
+}
+
+# or inline
+revrqlist(ls: list of ref Openreq) : list of ref Openreq
+{
+        rs: list of ref Openreq;
+        while(ls != nil){
+                rs = hd ls :: rs;
+                ls = tl ls;
+        }
+        return rs;
+}
+
+# addr should be, e.g. tcp!*!2345
+listener(addr: string, ch: chan of (ref Sys->FD, string, Uproc),
+		sync: chan of (int, string), algs: list of string)
+{
+	addr = dial->netmkaddr(addr, "tcp", "33234");
+	c := dial->announce(addr);
+	if (c == nil) {
+		sync <-= (-1, sys->sprint("cannot anounce on %s: %r", addr));
+		return;
+	}
+	sync <-= (sys->pctl(0, nil), nil);
+	for (;;) {
+		nc := dial->listen(c);
+		if (nc == nil) {
+			ch <-= (nil, sys->sprint("listen failed: %r"), nil);
+			return;
+		}
+		dfd := sys->open(nc.dir + "/data", Sys->ORDWR);
+		if (dfd != nil) {
+			if (algs == nil)
+				ch <-= (dfd, nil, nil);
+			else
+				spawn authenticator(dfd, ch, algs);
+		}
+	}
+}
+
+# authenticate a connection, setting the user id appropriately,
+# and then act as a server, performing file operations
+# on behalf of the central process.
+authenticator(dfd: ref Sys->FD, ch: chan of (ref Sys->FD, string, Uproc), algs: list of string)
+{
+	(fd, err) := auth->server(algs, authinfo, dfd, 1);
+	if (fd == nil) {
+		if (verbose)
+			sys->fprint(stderr, "lockfs: authentication failed: %s\n", err);
+		return;
+	}
+	uproc := chan of (ref Ureq, chan of (ref Sys->FD, string));
+	ch <-= (fd, err, uproc);
+	for (;;) {
+		(req, reply) := <-uproc;
+		if (req == nil)
+			exit;
+		reply <-= doreq(req);
+	}
+}
+
+create(uproc: Uproc, file: string, omode: int, perm: int): (ref Sys->FD, string)
+{
+	return proxydoreq(uproc, ref Ureq.Create(file, omode, perm));
+}
+
+open(uproc: Uproc, file: string, omode: int): (ref Sys->FD, string)
+{
+	return proxydoreq(uproc, ref Ureq.Open(file, omode));
+}
+
+remove(uproc: Uproc, file: string): string
+{
+	return proxydoreq(uproc, ref Ureq.Remove(file)).t1;
+}
+
+wstat(uproc: Uproc, file: string, d: Sys->Dir): string
+{
+	return proxydoreq(uproc, ref Ureq.Wstat(file, d)).t1;
+}
+
+proxydoreq(uproc: Uproc, req: ref Ureq): (ref Sys->FD, string)
+{
+	if (uproc == nil)
+		return doreq(req);
+	reply := chan of (ref Sys->FD, string);
+	uproc <-= (req, reply);
+	return <-reply;
+}
+
+doreq(greq: ref Ureq): (ref Sys->FD, string)
+{
+	fd: ref Sys->FD;
+	err: string;
+	pick req := greq {
+	Open =>
+		if ((fd = sys->open(req.fname, req.omode)) == nil)
+			err = sys->sprint("%r");
+	Create =>
+		if ((fd = sys->create(req.fname, req.omode, req.perm)) == nil)
+			err = sys->sprint("%r");
+	Remove =>
+		if (sys->remove(req.fname) == -1)
+			err = sys->sprint("%r");
+	Wstat =>
+		if (sys->wstat(req.fname, req.dir) == -1)
+			err = sys->sprint("%r");
+	}
+	return (fd, err);
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil){
+		sys->fprint(stderr, "lockfs: can't open /dev/user: %r\n");
+		raise "fail:no user";
+	}
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) {
+		sys->fprint(stderr, "lockfs: failed to read /dev/user: %r\n");
+		raise "fail:no user";
+	}
+
+	return string buf[0:n];	
+}
+
+now(): int
+{
+	buf := array[128] of byte;
+	sys->seek(timefd, big 0, 0);
+	if ((n := sys->read(timefd, buf, len buf)) < 0)
+		return 0;
+	return int (big string buf[0:n] / big 1000000);
+}
--- /dev/null
+++ b/appl/cmd/logfile.b
@@ -1,0 +1,259 @@
+implement Logfile;
+
+#
+# Copyright © 1999 Vita Nuova Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+
+Logfile: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+Fidrec: adt {
+	fid: 	int;		# fid of read
+	rq: 	list of (int, Sys->Rread);	# outstanding read requests
+	pos:	int;		# current position in the logfile
+};
+
+Circbuf: adt {
+	start: int;
+	data: array of byte;
+	new: fn(size: int): ref Circbuf;
+	put: fn(b: self ref Circbuf, d: array of byte): int;
+	get: fn(b: self ref Circbuf, s, n: int): (int, array of byte);
+};
+
+Fidhash: adt
+{
+	table: array of list of ref Fidrec;
+	get: fn(ht: self ref Fidhash, fid: int): ref Fidrec;
+	put: fn(ht: self ref Fidhash, fidrec: ref Fidrec);
+	del: fn(ht: self ref Fidhash, fidrec: ref Fidrec);
+	new: fn(): ref Fidhash;
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: logfile [-size] file\n");
+	raise "fail: usage";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	bufsize := Sys->ATOMICIO * 4;
+
+	if (argv != nil)
+		argv = tl argv;
+	if (argv != nil && len hd argv && (hd argv)[0] == '-' && len hd argv > 1) {
+		if ((bufsize = int ((hd argv)[1:])) <= 0) {
+			sys->fprint(stderr, "logfile: can't have a zero buffer size\n");
+			usage();
+		}
+		argv = tl argv;
+	}
+	if (argv == nil || tl argv != nil)
+		usage();
+	path := hd argv;
+
+	(dir, f) := pathsplit(path);
+	if (sys->bind("#s", dir, Sys->MBEFORE) == -1) {
+		sys->fprint(stderr, "logfile: bind #s failed: %r\n");
+		return;
+	}
+	fio := sys->file2chan(dir, f);
+	if (fio == nil) {
+		sys->fprint(stderr, "logfile: couldn't make %s: %r\n", path);
+		return;
+	}
+
+	spawn logserver(fio, bufsize);
+}
+
+logserver(fio: ref Sys->FileIO, bufsize: int)
+{
+	waitlist: list of ref Fidrec;
+	readers := Fidhash.new();
+	availcount := 0;
+	availchan := chan of int;
+	workchan := chan of (Sys->Rread, array of byte);
+	buf := Circbuf.new(bufsize);
+	for (;;) alt {
+	<-availchan =>
+		availcount++;
+	(nil, count, fid, rc) := <-fio.read =>
+		r := readers.get(fid);
+		if (rc == nil) {
+			if (r != nil)
+				readers.del(r);
+			continue;
+		}
+		if (r == nil) {
+			r = ref Fidrec(fid, nil, buf.start);
+			if (r.pos < len buf.data)
+				r.pos = len buf.data;		# first buffer's worth is garbage
+			readers.put(r);
+		}
+
+		(s, d) := buf.get(r.pos, count);
+		r.pos = s + len d;
+
+		if (d != nil) {
+			rc <-= (d, nil);
+		} else {
+			if (r.rq == nil)
+				waitlist = r :: waitlist;
+			r.rq = (count, rc) :: r.rq;
+		}
+
+	(nil, data, nil, wc) := <-fio.write =>
+		if (wc == nil)
+			continue;
+		if ((n := buf.put(data)) < len data)
+			wc <-= (n, "write too long for buffer");
+		else
+			wc <-= (n, nil);
+
+		wl := waitlist;
+		for (waitlist = nil; wl != nil; wl = tl wl) {
+			r := hd wl;
+			if (availcount == 0) {
+				spawn worker(workchan, availchan);
+				availcount++;
+			}
+			(count, rc) := hd r.rq;
+			r.rq = tl r.rq;
+
+			# optimisation: if the read request wants exactly the data provided
+			# in the write request, then use the original data buffer.
+			s: int;
+			d: array of byte;
+			if (count >= n && r.pos == buf.start + len buf.data - n)
+				(s, d) = (r.pos, data);
+			else
+				(s, d) = buf.get(r.pos, count);
+			r.pos = s + len d;
+			workchan <-= (rc, d);
+			availcount--;
+			if (r.rq != nil)
+				waitlist = r :: waitlist;
+			d = nil;
+		}
+		data = nil;
+		wl = nil;
+	}
+}
+
+worker(work: chan of (Sys->Rread, array of byte), ready: chan of int)
+{
+	for (;;) {
+		(rc, data) := <-work;	# blocks forever if the reading process is killed
+		rc <-= (data, nil);
+		(rc, data) = (nil, nil);
+		ready <-= 1;
+	}
+}
+		
+Circbuf.new(size: int): ref Circbuf
+{
+	return ref Circbuf(0, array[size] of byte);
+}
+
+# return number of bytes actually written
+Circbuf.put(b: self ref Circbuf, d: array of byte): int
+{
+	blen := len b.data;
+	# if too big to fit in buffer, truncate the write.
+	if (len d > blen)
+		d = d[0:blen];
+	dlen := len d;
+
+	offset := b.start % blen;
+	if (offset + dlen <= blen) {
+		b.data[offset:] = d;
+	} else {
+		b.data[offset:] = d[0:blen - offset];
+		b.data[0:] = d[blen - offset:];
+	}
+	b.start += dlen;
+	return dlen;
+}
+
+# return (start, data)
+Circbuf.get(b: self ref Circbuf, s, n: int): (int, array of byte)
+{
+	# if the beginning's been overrun, start from the earliest place we can.
+	# we could put some indication of elided bytes in the buffer.
+	if (s < b.start)
+		s = b.start;
+	blen := len b.data;
+	if (s + n > b.start + blen)
+		n = b.start + blen - s;
+	if (n <= 0)
+		return (s, nil);
+	o := s % blen;
+	d := array[n] of byte;
+	if (o + n <= blen)
+		d[0:] = b.data[o:o+n];
+	else {
+		d[0:] = b.data[o:];
+		d[blen - o:] = b.data[0:o+n-blen];
+	}
+	return (s, d);
+}
+
+FIDHASHSIZE: con 32;
+
+Fidhash.new(): ref Fidhash
+{
+	return ref Fidhash(array[FIDHASHSIZE] of list of ref Fidrec);
+}
+
+# put an entry in the hash table.
+# assumes there is no current entry for the fid.
+Fidhash.put(ht: self ref Fidhash, f: ref Fidrec)
+{
+	slot := f.fid & (FIDHASHSIZE-1);
+	ht.table[slot] = f :: ht.table[slot];
+}
+
+Fidhash.get(ht: self ref Fidhash, fid: int): ref Fidrec
+{
+	for (l := ht.table[fid & (FIDHASHSIZE-1)]; l != nil; l = tl l)
+		if ((hd l).fid == fid)
+			return hd l;
+	return nil;
+}
+
+Fidhash.del(ht: self ref Fidhash, f: ref Fidrec)
+{
+	slot := f.fid & (FIDHASHSIZE-1);
+	nl: list of ref Fidrec;
+	for (l := ht.table[slot]; l != nil; l = tl l)
+		if ((hd l).fid != f.fid)
+			nl = (hd l) :: nl;
+	ht.table[slot] = nl;
+}
+
+pathsplit(p: string): (string, string)
+{
+	for (i := len p - 1; i >= 0; i--)
+		if (p[i] != '/')
+			break;
+	if (i < 0)
+		return (p, nil);
+	p = p[0:i+1];
+	for (i = len p - 1; i >=0; i--)
+		if (p[i] == '/')
+			break;
+	if (i < 0)
+		return (".", p);
+	return (p[0:i+1], p[i+1:]);
+}
+
--- /dev/null
+++ b/appl/cmd/look.b
@@ -1,0 +1,384 @@
+implement Look;
+
+#
+#	Copyright © 2002 Lucent Technologies Inc.
+#	transliteration of the Plan 9 command; subject to the Lucent Public License 1.02
+#	-r option added by Caerwyn Jones to print a range
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+
+Look: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+filename := "/lib/words";
+dfile: ref Iobuf;
+bout: ref Iobuf;
+debug := 0;
+fold, direc, exact, iflag, range: int;
+rev := 1;	# -1 for reverse-ordered file, not implemented
+compare: ref fn(a, b: string): int;
+tab := '\t';
+entry: string;
+word: string;
+key: string;
+latin_fold_tab := array[64] of {
+	# 	Table to fold latin 1 characters to ASCII equivalents
+	# 	based at Rune value 0xc0
+	# 
+	#	 À    Á    Â    Ã    Ä    Å    Æ    Ç
+	#	 È    É    Ê    Ë    Ì    Í    Î    Ï
+	#	 Ð    Ñ    Ò    Ó    Ô    Õ    Ö    ×
+	#	 Ø    Ù    Ú    Û    Ü    Ý    Þ    ß
+	#	 à    á    â    ã    ä    å    æ    ç
+	#	 è    é    ê    ë    ì    í    î    ï
+	#	 ð    ñ    ò    ó    ô    õ    ö    ÷
+	#	 ø    ù    ú    û    ü    ý    þ    ÿ
+	# 
+	'a',	'a',	'a',	'a',	'a',	'a',	'a',	'c',	
+	'e',	'e',	'e',	'e',	'i',	'i',	'i',	'i',
+	'd',	'n',	'o',	'o',	'o',	'o',	'o',	0,
+	'o',	'u',	'u',	'u',	'u',	'y',	0,	0,
+	'a',	'a',	'a',	'a',	'a',	'a',	'a',	'c',
+	'e',	'e',	'e',	'e',	'i',	'i',	'i',	'i',
+	'd',	'n',	'o',	'o',	'o',	'o',	'o',	0,
+	'o',	'u',	'u',	'u',	'u',	'y',	0,	'y',
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg := load Arg Arg->PATH;
+
+	lastkey: string;
+
+	arg->init(args);
+	arg->setusage("look -[dfinx] [-r lastkey] [-t c] [string] [file]");
+	compare = acomp;
+	while((c := arg->opt()) != 0)
+		case c {
+		'D' =>
+			debug = 1;
+		'd' =>
+			direc++;
+		'f' =>
+			fold++;
+		'i' =>
+			iflag++;
+		'n' =>
+			compare = ncomp;
+		't' =>
+			tab = (arg->earg())[0];
+		'x' =>
+			exact++;
+		'r' =>
+			range++;
+			lastkey = rcanon(arg->earg());
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	bin := bufio->fopen(sys->fildes(0), Sys->OREAD); 
+	bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+	orig: string;
+	if(!iflag){
+		if(args != nil){
+			orig = hd args;
+			args = tl args;
+		}else
+			iflag++;
+	}
+	if(args == nil){
+		direc++;
+		fold++;
+	}else
+		filename = hd args;
+	if(!iflag)
+		key = rcanon(orig);
+	if(debug)
+		sys->fprint(sys->fildes(2), "orig %s key %s %s\n", orig, key, filename);
+	dfile = bufio->open(filename, Sys->OREAD);
+	if(dfile == nil){
+		sys->fprint(sys->fildes(2), "look: can't open %s\n", filename);
+		raise "fail:no dictionary";
+	}
+	if(!iflag) 
+		if(!locate() && !range)
+			raise "fail:not found";
+	do{
+		if(iflag){
+			bout.flush();
+			if((orig = bin.gets('\n')) == nil)
+				exit;
+			key = rcanon(orig);
+			if(!locate())
+				continue;
+		}
+		if(range){
+			if(compare(key, word) <= 0 && compare(word, lastkey) <= 0)
+				bout.puts(entry);
+		}else if(!exact || acomp(word, orig) == 0)
+			bout.puts(entry);
+	Matches:
+		while((entry = dfile.gets('\n')) != nil){
+			word = rcanon(entry);
+			if(range)
+				n := compare(word, lastkey);
+			else
+				n = compare(key, word);
+			if(debug)
+				sys->print("compare %d %q\n", n, word);
+			case n {
+			-2 =>
+				if(!range)
+					break Matches;
+				bout.puts(entry);
+			-1 =>
+				if(exact)
+					break Matches;
+				bout.puts(entry);
+			0 =>
+				if(!exact || acomp(word, orig) == 0)
+					bout.puts(entry);
+			* =>
+				break Matches;
+			}
+		}
+	}while(iflag);
+	bout.flush();
+}
+
+locate(): int
+{
+	bot := big 0;
+	top := dfile.seek(big 0, 2);
+	mid: big;
+Search:
+	for(;;){
+		mid = (top+bot)/big 2;
+		if(debug)
+			sys->fprint(sys->fildes(2), "locate %bd %bd %bd\n", top, mid, bot);
+		dfile.seek(mid, 0);
+		c: int;
+		do
+			c = dfile.getc();
+		while(c >= 0 && c != '\n');
+		mid = dfile.offset();
+		if((entry = dfile.gets('\n')) == nil)
+			break;
+		word = rcanon(entry);
+		if(debug)
+			sys->fprint(sys->fildes(2), "mid %bd key: %s entry: %s\n", mid, key, word);
+		n := compare(key, word);
+		if(debug)
+			sys->fprint(sys->fildes(2), "compare: %d\n", n);
+		case n {
+		-2 or -1 or 0 =>
+			if(top <= mid)
+				break Search;
+			top = mid;
+		1 or 2 =>
+			bot = mid;
+		}
+	}
+	if(debug)
+		sys->fprint(sys->fildes(2), "locate %bd %bd %bd\n", top, mid, bot);
+	bot = dfile.seek(big bot, 0);
+	while((entry = dfile.gets('\n')) != nil){
+		word = rcanon(entry);
+		if(debug)
+			sys->fprint(sys->fildes(2), "seekbot %bd key: %s entry: %s\n", bot, key, word);
+		n := compare(key, word);
+		if(debug)
+			sys->fprint(sys->fildes(2), "compare: %d\n", n);
+		case n {
+		-2 =>
+			return 0;
+		-1 =>
+			return !exact;
+		0 =>
+			return 1;
+		1 or 2 =>
+			;
+		}
+	}
+	return 0;
+}
+
+#
+#	acomp(s, t) returns:
+#		-2 if s strictly precedes t
+#		-1 if s is a prefix of t
+#		0 if s is the same as t
+#		1 if t is a prefix of s
+#		2 if t strictly precedes s
+#  
+acomp(s, t: string): int
+{
+	if(s == t)
+		return 0;
+	l := len s;
+	if(l > len t)
+		l = len t;
+	cs, ct: int;
+	for(i := 0; i < l; i++) {
+		cs = s[i];
+		ct = t[i];
+		if(cs != ct)
+			break;
+	}
+	if(i == len s)
+		return -1;
+	if(i == len t)
+		return 1;
+	if(cs < ct)
+		return -2;
+	return 2;
+}
+
+rcanon(s: string): string
+{
+	if(s != nil && s[len s - 1] == '\n')
+		s = s[0: len s - 1];
+	o := 0;
+	for(i := 0; i < len s && (r := s[i]) != tab; i++){
+		if(islatin1(r) && (mr := latin_fold_tab[r-16rc0]) != 0)
+			r = mr;
+		if(direc)
+			if(!(isalnum(r) || r == ' ' || r == '\t'))
+				continue;
+		if(fold)
+			if(isupper(r))
+				r = tolower(r);
+		if(r != s[o])	# avoid copying s unless necessary
+			s[o] = r;
+		o++;
+	}
+	if(o != i)
+		return s[0:o];
+	return s;
+}
+
+sgn(v: int): int
+{
+	if(v < 0)
+		return -1;
+	if(v > 0)
+		return 1;
+	return 0;
+}
+
+ncomp(s: string, t: string): int
+{
+	while(len s > 0 && isspace(s[0]))
+		s = s[1:];
+	while(len t > 0 && isspace(t[0]))
+		t = t[1:];
+	ssgn := tsgn := -2*rev;
+	if(s != nil && s[0] == '-'){
+		s = s[1: ];
+		ssgn = -ssgn;
+	}
+	if(t != nil && t[0] == '-'){
+		t = t[1:];
+		tsgn = -tsgn;
+	}
+	for(i := 0; i < len s && isdigit(s[i]); i++)
+		;
+	is := s[0:i];
+	js := s[i:];
+	for(i = 0; i < len t && isdigit(t[i]); i++)
+		;
+	it := t[0:i];
+	jt := t[i:];
+	a := 0;
+	i = len is;
+	j := len it;
+	if(ssgn == tsgn){
+		while(j > 0 && i > 0)
+			if((b := it[--j] - is[--i]) != 0)
+				a = b;
+	}
+	while(i > 0)
+		if(is[--i] != '0')
+			return -ssgn;
+	while(j > 0)
+		if(it[--i] != '0')
+			return tsgn;
+	if(a)
+		return sgn(a)*ssgn;
+	s = js;
+	if(len s > 0 && s[0] == '.')
+		s = s[1: ];
+	t = jt;
+	if(len t > 0 && t[0] == '.')
+		t = t[1: ];
+	if(ssgn == tsgn)
+		while((len s > 0 && isdigit(s[0])) && (len t > 0 && isdigit(t[0]))){
+			if(a = t[0] - s[0])
+				return sgn(a)*ssgn;
+			s = s[1:];
+			t = t[1:];
+		}
+	for(; len s > 0 && isdigit(s[0]); s = s[1:])
+		if(s[0] != '0')
+			return -ssgn;
+	for(; len t > 0 && isdigit(t[0]); t = t[1:])
+		if(t[0] != '0')
+			return tsgn;
+	return 0;
+}
+
+isupper(c: int): int
+{
+	return c >= 'A' && c <= 'Z';
+}
+
+islower(c: int): int
+{
+	return c >= 'a' && c <= 'z';
+}
+
+isalpha(c: int): int
+{
+	return islower(c) || isupper(c);
+}
+
+islatin1(c: int): int
+{
+	return c >= 16rC0 && c <= 16rFF;
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isalnum(c: int): int
+{
+	return isdigit(c) || islower(c) || isupper(c);
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c >= 16r0A && c <= 16r0D;
+}
+
+tolower(c: int): int
+{
+	return c-'A'+'a';
+}
--- /dev/null
+++ b/appl/cmd/ls.b
@@ -1,0 +1,332 @@
+implement Ls;
+
+include "sys.m";
+	sys: Sys;
+	FD, Dir: import Sys;
+
+include "draw.m";
+	Context: import Draw;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "readdir.m";
+	readdir: Readdir;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+
+Ls: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+PREFIX: con 16r40000000;
+
+dopt := 0;
+eopt := 0;
+lopt := 0;
+mopt := 0;
+nopt := 0;
+popt := 0;
+qopt := 0;
+sopt := 0;
+topt := 0;
+uopt := 0;
+Fopt := 0;
+Topt := 0;
+now:	int;
+sortby:	int;
+
+out: ref Bufio->Iobuf;
+stderr: ref FD;
+
+delaydir: array of ref Dir;
+delayindex: int;
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "ls: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(nil: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badmodule(Bufio->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmodule(Readdir->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		badmodule(String->PATH);
+
+	stderr = sys->fildes(2);
+	out = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+	rev := 0;
+	sortby = Readdir->NAME;
+	compact := 0;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		badmodule(Arg->PATH);
+	arg->init(argv);
+	while((o := arg->opt()) != 0){
+		case o {
+		'l' =>
+			lopt++;
+			daytime = load Daytime Daytime->PATH;
+			if(daytime == nil)
+				badmodule(Daytime->PATH);
+			now = daytime->now();
+		'p' =>
+			popt++;
+		'q' =>
+			qopt++;
+		'd' =>
+			dopt++;
+		'e' =>
+			eopt++;
+		'm' =>
+			mopt++;
+		'n' =>
+			nopt++;
+		'k' =>
+			sopt++;
+		't' =>
+			topt++;
+		'u' =>
+			uopt++;
+		's' =>
+			sortby = Readdir->SIZE;
+		'c' =>
+			compact = Readdir->COMPACT;
+		'r' =>
+			rev = Readdir->DESCENDING;
+		'T' =>
+			Topt++;
+		'F' =>
+			Fopt++;
+		* =>
+			sys->fprint(stderr, "usage: ls [-delmnpqrstucFT] [files]\n");
+			raise "fail:usage";
+		}
+	}
+	argv = arg->argv();
+	arg = nil;
+
+	if(nopt == 0) {
+		if(topt){
+			if(uopt)
+				sortby = Readdir->ATIME;
+			else
+				sortby = Readdir->MTIME;
+		}
+	} else
+		sortby = Readdir->NONE;
+	sortby |= rev|compact;
+
+	if(argv == nil) {
+		argv = list of {"."};
+		popt++;
+	}
+
+	errs := 0;
+	for(; argv != nil; argv = tl argv)
+		errs |= !ls(hd argv);
+	delaywrite();
+	out.flush();
+	if(errs != 0)
+		raise "fail:errors";
+}
+
+ls(file: string): int
+{
+	dir: Dir;
+ 	ok: int;
+
+	(ok, dir) = sys->stat(file);
+	if(ok == -1) {
+		sys->fprint(stderr, "ls: %s: %r\n", file);
+		return 0;
+	}
+	if(dopt || (dir.mode & Sys->DMDIR) == 0) {
+		# delay write: save it in the queue to sort by sortby
+		if(delayindex == 0) 
+			delaydir = array[30] of ref Dir;
+		else if(len delaydir == delayindex) {
+			tmp := array[2 * delayindex] of ref Dir;
+			tmp[0:] = delaydir;
+			delaydir = tmp;
+		}
+		(dirname, filename) := str->splitstrr(file, "/");
+		if(dirname != "") {
+			dir.name = dirname + filename;
+			dir.dev |= PREFIX;
+		}
+		delaydir[delayindex++] = ref dir; 
+		return 1;
+	}
+
+	delaywrite();
+
+	(d, n) := readdir->init(file, sortby);
+	if(n < 0){
+		sys->fprint(stderr, "ls: %s: %r\n", file);
+		return 0;
+	}
+	lsprint(file, d[0:n]);
+	return 1;
+}
+
+delaywrite() 
+{
+	if(delayindex == 0)
+		return;
+	
+	(b, n) := readdir->sortdir(delaydir[0:delayindex], sortby);
+
+	lsprint("", b[0:n]);
+	
+	delayindex = 0;
+	delaydir = nil;
+}
+
+Widths: adt {
+	vers, dev, uid, gid, muid, length, size: int;
+};
+
+dowidths(dir: array of ref Dir): ref Widths
+{
+	w := Widths(0, 0, 0, 0, 0, 0, 0);
+	for (i := 0; i < len dir; i++) {
+		n: int;
+		d := dir[i];
+		if(sopt)
+			if((n = len string ((d.length+big 1023)/big 1024)) > w.size)
+				w.size = n;
+		if(mopt)
+			if((n = len d.muid+2) > w.muid)
+				w.muid = n;
+		if(qopt)
+			if((n = len string d.qid.vers) > w.vers)
+				w.vers = n;
+		if(lopt) {
+			if((n = len string (d.dev & ~PREFIX)) > w.dev)
+				w.dev = n;
+			if((n = len d.uid) > w.uid)
+				w.uid = n;
+			if((n = len d.gid) > w.gid)
+				w.gid = n;
+			if((n = len string d.length) > w.length)
+				w.length = n;
+		}
+	}
+	return ref w;
+}
+
+
+lsprint(dirname: string, dir: array of ref Dir)
+{
+	w := dowidths(dir);
+
+	for (i := 0; i < len dir; i++)
+		lslineprint(dirname, dir[i].name, dir[i], w);
+}
+
+lslineprint(dirname, name: string, dir: ref Dir, w: ref Widths)
+{
+	if(sopt)
+		out.puts(sys->sprint("%*bd ", w.size, (dir.length+big 1023)/big 1024));
+	if(mopt){
+		out.puts(sys->sprint("[%s] ", dir.muid));
+		for(i := len dir.muid+2; i < w.muid; i++)
+			out.putc(' ');
+	}
+	if(qopt)
+		out.puts(sys->sprint("(%.16bux %*ud %.2ux) ", dir.qid.path, w.vers, dir.qid.vers, dir.qid.qtype));
+	if(Topt){
+		if(dir.mode & Sys->DMTMP)
+			out.puts("t ");
+		else
+			out.puts("- ");
+	}
+
+	file := name;
+	pf := dir.dev & PREFIX;
+	dir.dev &= ~PREFIX;
+	if(popt) {
+		if(pf)
+			(nil, file) = str->splitstrr(dir.name, "/");
+		else
+			file = dir.name;
+	} else if(dirname != "") {
+		if(dirname[len dirname-1] == '/')
+			file = dirname + file;
+		else
+			file = dirname + "/" + file;
+	}
+	if(Fopt)
+		file += fileflag(dir);
+
+
+	if(lopt) {
+		time := dir.mtime;
+		if(uopt)
+			time = dir.atime;
+		if(eopt)
+			out.puts(sys->sprint("%s %c %*d %*s %*s %*bud %d %s\n",
+				modes(dir.mode), dir.dtype, w.dev, dir.dev,
+				-w.uid, dir.uid, -w.gid, dir.gid, w.length, dir.length,
+				time, file));
+		else
+			out.puts(sys->sprint("%s %c %*d %*s %*s %*bud %s %s\n",
+				modes(dir.mode), dir.dtype, w.dev, dir.dev,
+				-w.uid, dir.uid, -w.gid, dir.gid, w.length, dir.length,
+				daytime->filet(now, time), file));
+	} else
+		out.puts(file+"\n");
+}
+
+fileflag(dir: ref Dir): string
+{
+	if(dir.qid.qtype & Sys->QTDIR)
+		return "/";
+	if(dir.mode & 8r111)
+		return "*";
+	return "";
+}
+
+mtab := array[] of {
+	"---",	"--x",	"-w-",	"-wx",
+	"r--",	"r-x",	"rw-",	"rwx"
+};
+
+modes(mode: int): string
+{
+	s: string;
+
+	if(mode & Sys->DMDIR)
+		s = "d";
+	else if(mode & Sys->DMAPPEND)
+		s = "a";
+	else if(mode & Sys->DMAUTH)
+		s = "A";
+	else
+		s = "-";
+	if(mode & Sys->DMEXCL)
+		s += "l";
+	else
+		s += "-";
+	s += mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7];
+	return s;
+}
+
--- /dev/null
+++ b/appl/cmd/lstar.b
@@ -1,0 +1,120 @@
+implement lstar;
+
+include "sys.m";
+	sys: Sys;
+	print, sprint, fprint: import sys;
+	stdin, stderr: ref sys->FD;
+include "draw.m";
+
+TBLOCK: con 512;	# tar logical blocksize
+Header: adt{
+	name: string;
+	size: int;
+	mtime: int;
+	skip: int;
+};
+
+lstar: module{
+	init:   fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Error(mess: string){
+	fprint(stderr,"lstar: %s: %r\n",mess);
+	exit;
+}
+
+
+NBLOCK: con 20;		# blocking factor for efficient read
+tarbuf := array[NBLOCK*TBLOCK] of byte;	# static buffer
+nblock := NBLOCK;			# how many blocks of data are in tarbuf
+recno := NBLOCK;			# how many blocks in tarbuf have been consumed
+getblock():array of byte{
+	if(recno>=nblock){
+		i := sys->read(stdin,tarbuf,TBLOCK*NBLOCK);
+		if(i==0)
+			return tarbuf[0:0];
+		if(i<0)
+			Error("read error");
+		if(i%TBLOCK!=0)
+			Error("blocksize error");
+		nblock = i/TBLOCK;
+		recno = 0;
+	}
+	recno++;
+	return tarbuf[(recno-1)*TBLOCK:recno*TBLOCK];
+}
+
+octal(b:array of byte):int{
+	sum := 0;
+	for(i:=0; i<len b; i++){
+		bi := int b[i];
+		if(bi==' ') continue;
+		if(bi==0) break;
+		sum = 8*sum + bi-'0';
+	}
+	return sum;
+}
+
+nullterm(b:array of byte):string{
+	for(i:=0; i<len b; i++)
+		if(b[i]==byte 0) break;
+	return string b[0:i];
+}
+
+getdir():ref Header{
+	dblock := getblock();
+	if(len dblock==0)
+		return nil;
+	if(dblock[0]==byte 0)
+		return nil;
+
+	name := nullterm(dblock[0:100]);
+	if(int dblock[345]!=0)
+		name = nullterm(dblock[345:500])+"/"+name;
+
+	magic := string(dblock[257:262]);
+	if(magic[0]!=0 && magic!="ustar")
+		Error("bad magic "+name);
+	chksum := octal(dblock[148:156]);
+	for(ci:=148; ci<156; ci++) dblock[ci] = byte ' ';
+	for(i:=0; i<TBLOCK; i++)
+		chksum -= int dblock[i];
+	if(chksum!=0)
+		Error("directory checksum error "+name);
+
+	skip := 1;
+	size := 0;
+	mtime := 0;
+	case int dblock[156]{
+	'0' or '5' or '7' or 0 =>
+		skip = 0;
+		size = octal(dblock[124:136]);
+		mtime = octal(dblock[136:148]);
+	'1' =>
+		fprint(stderr,"skipping link %s -> %s\n",name,string(dblock[157:257]));
+	'2' or 's' =>
+		fprint(stderr,"skipping symlink %s\n",name);
+	'3' or '4' or '6' =>
+		fprint(stderr,"skipping special file %s\n",name);
+	* =>
+		Error(sprint("unrecognized typeflag %d for %s",int dblock[156],name));
+	}
+	return ref Header(name,size,mtime,skip);
+}
+
+
+init(nil: ref Draw->Context, nil: list of string){
+	sys = load Sys Sys->PATH;
+	stdin = sys->fildes(0);
+	stderr = sys->fildes(2);
+	ofile: ref sys->FD;
+
+	while((file := getdir())!=nil){
+		bytes := file.size;
+		blocks := (bytes+TBLOCK-1)/TBLOCK;
+		for(; blocks>0; blocks--)
+			getblock();
+		print("%s %d %d 0\n",file.name,file.mtime,file.size);
+		ofile = nil;
+	}
+}
--- /dev/null
+++ b/appl/cmd/m4.b
@@ -1,0 +1,946 @@
+implement M4;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "sh.m";
+
+include "arg.m";
+
+M4: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+NHASH: con 131;
+
+Name: adt {
+	name:	string;
+	repl:	string;
+	impl:	ref fn(nil: array of string);
+	dol:	int;	# repl contains $[0-9]
+	asis:	int;	# replacement text not rescanned
+
+	text:	fn(n: self ref Name): string;
+};
+
+names := array[NHASH] of list of ref Name;
+
+File: adt {
+	name:	string;
+	line:	int;
+	fp:	ref Iobuf;
+};
+
+Param: adt {
+	s:	string;
+};
+
+pushedback: string;
+pushedp := 0;	# next available index in pushedback
+diverted := array[10] of string;
+curdiv := 0;
+curarg: ref Param;	# non-nil if collecting argument string
+instack: list of ref File;
+lquote := '`';
+rquote := '\'';
+initcom := "#";
+endcom := "\n";
+bout: ref Iobuf;
+sh: Sh;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+
+	define("inferno", "inferno", 0);
+
+	builtin("changecom", dochangecom);
+	builtin("changequote", dochangequote);
+	builtin("copydef", docopydef);
+	builtin("define", dodefine);
+	builtin("divert", dodivert);
+	builtin("divnum", dodivnum);
+	builtin("dnl", dodnl);
+	builtin("dumpdef", dodumpdef);
+	builtin("errprint", doerrprint);
+	builtin("eval", doeval);
+	builtin("ifdef", doifdef);
+	builtin("ifelse", doifelse);
+	builtin("include", doinclude);
+	builtin("incr", doincr);
+	builtin("index", doindex);
+	builtin("len", dolen);
+	builtin("maketemp", domaketemp);
+	builtin("sinclude", dosinclude);
+	builtin("substr", dosubstr);
+	builtin("syscmd", dosyscmd);
+	builtin("translit", dotranslit);
+	builtin("undefine", doundefine);
+	builtin("undivert", doundivert);
+
+	arg := load Arg Arg->PATH;
+	arg->setusage("m4 [-Dname[=value]] [-Qname[=value]] [-Uname] [file ...]");
+	arg->init(args);
+
+	while((o := arg->opt()) != 0){
+		case o {
+		'D' =>
+			argdefine(arg->earg(), 0);
+		'Q' =>
+			argdefine(arg->earg(), 1);
+		'U' =>
+			undefine(arg->earg());
+		* =>
+			arg->usage();
+		}
+	}
+	args = arg->argv();
+	arg = nil;
+
+	if(args != nil){
+		for(; args != nil; args = tl args){
+			f := bufio->open(hd args, Sys->OREAD);
+			if(f == nil)
+				error(sys->sprint("can't open %s: %r", hd args));
+			pushfile(hd args, f);
+			scan();
+		}
+	}else{
+		pushfile("standard input", bufio->fopen(sys->fildes(0), Sys->OREAD));
+		scan();
+	}
+	bout.flush();
+}
+
+argdefine(s: string, asis: int)
+{
+	text := "";
+	for(i := 0; i < len s; i++)
+		if(s[i] == '='){
+			text = s[i+1:];
+			break;
+		}
+	n := lookup(s[0: i]);
+	if(n != nil && n.impl != nil)
+		error(sys->sprint("can't redefine built-in %s", s[0: i]));
+	define(s[0: i], text, asis);
+}
+
+scan()
+{
+	while((c := getc()) >= 0){
+		if(isalpha(c))
+			called(c);	
+		else if(c == lquote)
+			quoted();
+		else if(initcom != nil && initcom[0] == c)
+			comment();
+		else
+			putc(c);
+	}
+}
+
+error(s: string)
+{
+	where := "";
+	if(instack != nil){
+		ios := hd instack;
+		where = sys->sprint(" %s:%d:", ios.name, ios.line);
+	}
+	sys->fprint(sys->fildes(2), "m4:%s %s\n", where, s);
+	raise "fail:error";
+}
+
+pushfile(name: string, fp: ref Iobuf)
+{
+	instack = ref File(name, 1, fp) :: instack;
+}
+
+called(c: int)
+{
+	tok: string;
+	do{
+		tok[len tok] = c;
+		c = getc();
+	}while(isalpha(c) || c >= '0' && c <= '9');
+	def := lookup(tok);
+	if(def == nil){
+		pushc(c);
+		puts(tok);
+		return;
+	}
+	if(c != '(' || def.asis){	# no parameters
+		pushc(c);
+		expand(def, array[] of {tok});
+		return;
+	}
+	# collect arguments, allowing for nested parentheses;
+	# on ')' expand definition, further expanding $n references therein
+	argstack := def.name :: nil;	# $0
+	savearg := curarg;	# save parameter (if any) for outer call
+	curarg = ref Param("");
+	nesting := 0;	# () depth
+	skipws();
+	for(;;){
+		if((c = getc()) < 0)
+			error("EOF in parameters");
+		if(isalpha(c))
+			called(c);
+		else if(c == lquote)
+			quoted();
+		else{
+			if(c == '(')
+				nesting++;
+			if(nesting > 0){
+				if(c == ')')
+					nesting--;
+				putc(c);
+			}else if(c == ','){
+				argstack = curarg.s :: argstack;
+				curarg = ref Param("");
+				skipws();
+			}else if(c == ')')
+				break;
+			else
+				putc(c);
+		}
+	}
+	argstack = curarg.s :: argstack;
+	curarg = savearg;	# restore outer parameter (if any)
+	# build arguments
+	narg := len argstack;
+	args := array[narg] of string;
+	for(; argstack != nil; argstack = tl argstack)
+		args[--narg] = hd argstack;
+	expand(def, args);
+}
+
+quoted()
+{
+	nesting :=0;
+	while((c := getc()) != rquote || nesting > 0){
+		if(c < 0)
+			error("EOF in string");
+		if(c == rquote)
+			nesting--;
+		else if(c == lquote)
+			nesting++;
+		putc(c);
+	}
+}
+
+comment()
+{
+	for(i := 1; i < len initcom; i++){
+		if((c := getc()) != initcom[i]){
+			if(c < 0)
+				error("EOF in comment");
+			pushc(c);
+			pushs(initcom[1: i]);
+			putc(initcom[0]);
+			return;
+		}
+	}
+	puts(initcom);
+	for(i = 0; i < len endcom;){
+		c := getc();
+		if(c < 0)
+			error("EOF in comment");
+		putc(c);
+		if(c == endcom[i])
+			i++;
+		else
+			i = c == endcom[0];
+	}
+}
+
+skipws()
+{
+	while(isspace(c := getc()))
+		{}
+	pushc(c);
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+}
+
+isname(s: string): int
+{
+	if(s == nil || !isalpha(s[0]))
+		return 0;
+	for(i := 1; i < len s; i++)
+		if(!(isalpha(s[i]) || s[i]>='0' && s[i]<='9'))
+			return 0;
+	return 1;
+}
+
+isalpha(c: int): int
+{
+	return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' || c > 16rA0;
+}
+
+hash(name: string): int
+{
+	h := 0;
+	for(i := 0; i < len name; i++)
+		h = h*65599 + name[i];
+	return (h & ~(1<<31)) % NHASH;
+}
+
+builtin(name: string, impl: ref fn(nil: array of string))
+{
+	h := hash(name);
+	n := ref Name(name, nil, impl, 0, 0);
+	names[h] = n :: names[h];
+}
+
+define(name: string, repl: string, asis: int)
+{
+	h := hash(name);
+	dol := hasdol(repl);
+	for(l := names[h]; l != nil; l = tl l){
+		n := hd l;
+		if(n.name == name){
+			*n = Name(name, repl, nil, dol, asis);
+			return;
+		}
+	}
+	n := ref Name(name, repl, nil, dol, asis);
+	names[h] = n :: names[h];
+}
+
+lookup(name: string): ref Name
+{
+	h := hash(name);
+	for(l := names[h]; l != nil; l = tl l)
+		if((hd l).name == name)
+			return hd l;
+	return nil;
+}
+
+undefine(name: string)
+{
+	h := hash(name);
+	rl: list of ref Name;
+	for(l := names[h]; l != nil; l = tl l){
+		if((hd l).name == name){
+			l = tl l;
+			for(; rl != nil; rl = tl rl)
+				l = hd rl :: l;
+			names[h] = l;
+			return;
+		}else
+			rl = hd l :: rl;
+	}
+}
+
+Name.text(n: self ref Name): string
+{
+	if(n.impl != nil)
+		return sys->sprint("builtin %q", n.name);
+	return sys->sprint("%c%s%c", lquote, n.repl, rquote);
+}
+
+dodumpdef(args: array of string)
+{
+	if(len args > 1){
+		for(i := 1; i < len args; i++)
+			if((n := lookup(args[i])) != nil)
+				sys->fprint(sys->fildes(2), "%q	%s\n", n.name, n.text());
+	}else{
+		for(i := 0; i < len names; i++)
+			for(l := names[i]; l != nil; l = tl l)
+				sys->fprint(sys->fildes(2), "%q %s\n", (hd l).name, (hd l).text());
+	}
+}
+
+pushs(s: string)
+{
+	for(i := len s; --i >= 0;)
+		pushedback[pushedp++] = s[i];
+}
+
+pushc(c: int)
+{
+	if(c >= 0)
+		pushedback[pushedp++] = c;
+}
+
+getc(): int
+{
+	if(pushedp > 0)
+		return pushedback[--pushedp];
+	for(; instack != nil; instack = tl instack){
+		ios := hd instack;
+		c := ios.fp.getc();
+		if(c >= 0){
+			if(c == '\n')
+				ios.line++;
+			return c;
+		}
+	}
+	return -1;
+}
+
+puts(s: string)
+{
+	if(curarg != nil)
+		curarg.s += s;
+	else if(curdiv > 0)
+		diverted[curdiv] += s;
+	else if(curdiv == 0)
+		bout.puts(s);
+}
+
+putc(c: int)
+{
+	if(curarg != nil){
+		# stow in argument collection buffer
+		curarg.s[len curarg.s] = c;
+	}else if(curdiv > 0){
+		l := len diverted[curdiv];
+		diverted[curdiv][l] = c;
+	}else if(curdiv == 0)
+		bout.putc(c);
+}
+
+expand(def: ref Name, args: array of string)
+{
+	if(def.impl != nil){
+		def.impl(args);
+		return;
+	}
+	if(def.repl == def.name || def.repl == "$0"){
+		puts(def.name);
+		return;
+	}
+	if(!def.dol || def.repl == nil){
+		pushs(def.repl);
+		return;
+	}
+	# expand $n
+	s := def.repl;
+	for(i := len s; --i >= 1;){
+		if(s[i-1] == '$' && (c := s[i]-'0') >= 0 && c <= 9){
+			if(c < len args)
+				pushs(args[c]);
+			i--;
+		}else
+			pushc(s[i]);
+	}
+	if(i >= 0)
+		pushc(s[0]);
+}
+
+hasdol(s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '$')
+			return 1;
+	return 0;
+}
+
+dodefine(args: array of string)
+{
+	if(len args > 2)
+		define(args[1], args[2], 0);
+	else if(len args > 1)
+		define(args[1], "", 0);
+}
+
+doundefine(args: array of string)
+{
+	for(i := 1; i < len args; i++)
+		undefine(args[i]);
+}
+
+docopydef(args: array of string)
+{
+	if(len args > 2 && args[1] != args[2]){
+		undefine(args[2]);
+		if((n := lookup(args[1])) != nil){
+			if(n.impl == nil)
+				define(args[2], n.repl, n.asis);
+			else
+				builtin(args[2], n.impl);
+		}else
+			define(args[2], "", 0);
+	}
+}
+
+doeval(args: array of string)
+{
+	if(len args > 1)
+		pushs(string eval(args[1]));
+}
+
+dodivert(args: array of string)
+{
+	if(len args > 1){
+		n := int args[1];
+		if(n < 0 || n >= len diverted)
+			n = -1;
+		curdiv = n;
+	}else
+		curdiv = 0;
+}
+
+dodivnum(nil: array of string)
+{
+	pushs(string curdiv);
+}
+
+doundivert(args: array of string)
+{
+	if(len args <= 1){	# do all but current, in order
+		for(i := 1; i < len diverted; i++){
+			if(i != curdiv){
+				puts(diverted[i]);
+				diverted[i] = nil;
+			}
+		}
+	}else{	# do those specified
+		for(i := 1; i < len args; i++){
+			n := int args[i];
+			if(n > 0 && n < len diverted && n != curdiv){
+				puts(diverted[n]);
+				diverted[n] = nil;
+			}
+		}
+	}
+}
+
+doifdef(args: array of string)
+{
+	if(len args < 2)
+		return;
+	n := lookup(args[1]);
+	if(n != nil)
+		pushs(args[2]);
+	else if(len args > 2)
+		pushs(args[3]);
+}
+
+doifelse(args: array of string)
+{
+	for(i := 1; i+2 < len args; i += 3){
+		if(args[i] == args[i+1]){
+			pushs(args[i+2]);
+			return;
+		}
+	}
+	if(i > 2 && i == len args-1)
+		pushs(args[i]);
+}
+
+doincr(args: array of string)
+{
+	if(len args > 1)
+		pushs(string (int args[1] + 1));
+}
+
+doindex(args: array of string)
+{
+	if(len args > 2){
+		a := args[1];
+		b := args[2];
+		for(i := 0; i+len b <= len a; i++){
+			if(a[i: i+len b] == b){
+				pushs(string i);
+				return;
+			}
+		}
+		pushs("-1");
+	}
+}
+
+doinclude(args: array of string)
+{
+	for(i := len args; --i >= 1;){
+		fp := bufio->open(args[i], Sys->OREAD);
+		if(fp == nil)
+			error(sys->sprint("can't open %s: %r", args[i]));
+		pushfile(args[i], fp);
+	}
+}
+
+dosinclude(args: array of string)
+{
+	for(i := len args; --i >= 1;){
+		fp := bufio->open(args[i], Sys->OREAD);
+		if(fp != nil)
+			pushfile(args[i], fp);
+	}
+}
+
+clip(v, l, u: int): int
+{
+	if(v < l)
+		return l;
+	if(v > u)
+		return u;
+	return v;
+}
+
+dosubstr(args: array of string)
+{
+	if(len args > 2){
+		l := len args[1];
+		o := clip(int args[2], 0, l);
+		n := l;
+		if(len args > 3)
+			n = clip(int args[3], 0, l);
+		if((n += o) > l)
+			n = l;
+		pushs(args[1][o: n]);
+	}
+}
+
+cindex(s: string, c: int): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return i;
+	return -1;
+}
+
+dotranslit(args: array of string)
+{
+	if(len args < 3)
+		return;
+	s := args[1];
+	f := args[2];
+	t := "";
+	if(len args > 3)
+		t = args[3];
+	o := "";
+	for(i := 0; i < len s; i++){
+		if((j := cindex(f, s[i])) >= 0){
+			if(j < len t)
+				o[len o] = t[j];
+		}else
+			o[len o] = s[i];
+	}
+	pushs(o);
+}
+
+doerrprint(args: array of string)
+{
+	s := "";
+	for(i := 1; i < len args; i++)
+		s += " "+args[i];
+	if(s != nil)
+		sys->fprint(sys->fildes(2), "m4:%s\n", s);
+}
+
+dolen(args: array of string)
+{
+	if(len args > 1)
+		puts(string len args[1]);
+}
+
+dochangecom(args: array of string)
+{
+	case len args {
+	1 =>
+		initcom = "";
+		endcom = "";
+	2 =>
+		initcom = args[1];
+		endcom = "\n";
+	* =>
+		initcom = args[1];
+		endcom = args[2];
+		if(endcom == "")
+			endcom = "\n";
+	}
+}
+
+dochangequote(args: array of string)
+{
+	case len args {
+	1 =>
+		lquote = '`';
+		rquote = '\'';
+	2 =>
+		if(args[1] != nil)
+			lquote = rquote = args[1][0];
+	* =>
+		if(args[1] != nil)
+			lquote = args[1][0];
+		if(args[2] != nil)
+			rquote = args[2][0];
+	}
+}
+
+dodnl(nil: array of string)
+{
+	while((c := getc()) >= 0 && c != '\n')
+		{}
+}
+
+domaketemp(args: array of string)
+{
+	if(len args > 1)
+		pushs(mktemp(args[1]));
+}
+
+dosyscmd(args: array of string)
+{
+	if(len args > 1){
+		{
+			if(sh == nil){
+				sh = load Sh Sh->PATH;
+				if(sh == nil)
+					raise sys->sprint("load: can't load %s: %r", Sh->PATH);
+			}
+			bout.flush();
+			sh->system(nil, args[1]);
+		}exception e{
+		"load:*" =>
+			error(e);
+		}
+	}
+}
+
+sysname: string;
+
+mktemp(s: string): string
+{
+	if(sysname == nil)
+		sysname = readfile("/dev/sysname", "m4");
+	# trim trailing X's
+	for (x := len s; --x >= 0;)
+		if(s[x] == 'X'){
+			while(x > 0 && s[x-1] == 'X')
+				x--;
+			s = s[0: x];
+			break;
+		}
+	# add system name, process ID and 'a'
+	if(s != nil)
+		s += ".";
+	s += sys->sprint("%s.%.10uda", sysname, sys->pctl(0, nil));
+	while(sys->stat(s).t0 >= 0){
+		if(s[len s-1] == 'z')
+			error("out of temp files: "+s);
+		s[len s-1]++;
+	}
+	return s;
+}
+
+readfile(name: string, default: string): string
+{
+	fd := sys->open(name, Sys->OREAD);
+	if(fd == nil)
+		return default;
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return default;
+	return string buf[0: n];
+}
+
+#
+# expressions provided use Limbo operators (C with signed shift and **),
+# instead of original m4 ones (where | and & were || and &&, and ^ was power),
+# but that's true of later unix m4 implementations too
+#
+
+Oeof, Ogok, Oge, Ole, One, Oeq, Opow, Oand, Oor, Orsh, Olsh, Odigits: con 'a'+iota;
+Syntax, Badeval: exception;
+evalin: string;
+evalp := 0;
+
+eval(s: string): int
+{
+	evalin = s;
+	evalp = 0;
+	looked = -1;
+	{
+		v := expr(1);
+		if(evalp < len evalin)
+			raise Syntax;
+		return v;
+	}exception{
+	Syntax =>
+		error(sys->sprint("syntax error: %q %q", evalin[0: evalp], evalin[evalp:]));
+		return 0;
+	Badeval =>
+		error(sys->sprint("zero divide in %q", evalin));
+		return 0;
+	}
+}
+
+eval1(op: int, v1, v2: int): int raises Badeval
+{
+	case op{
+	'+' =>	return v1 + v2;
+	'-' =>	return v1 - v2;
+	'*' =>		return v1 * v2;
+	'%' =>
+		if(v2 == 0)
+			raise Badeval;	# division by zero
+		return v1 % v2;
+	'/' =>
+		if(v2 == 0)
+			raise Badeval;	# division by zero
+		return v1 / v2;
+	Opow =>
+		if(v2 < 0)
+			raise Badeval;
+		return v1 ** v2;
+	'&' =>	return v1 & v2;
+	'|' =>		return v1 | v2;
+	'^' =>	return v1 ^ v2;
+	Olsh =>	return v1 << v2;
+	Orsh =>	return v1 >> v2;
+	Oand =>	return v1 && v2;
+	Oor =>	return v1 || v2;
+	'<' =>	return v1 < v2;
+	'>' =>	return v1 > v2;
+	Ole =>	return v1 <= v2;
+	Oge =>	return v1 >= v2;
+	One =>	return v1 != v2;
+	Oeq =>	return v1 == v2;
+	* =>
+		sys->print("unknown op: %c\n", op);	# shouldn't happen
+		raise Badeval;
+	}
+}
+
+priority(c: int): int
+{
+	case c {
+	Oor =>	return 1;
+	Oand =>	return 2;
+	'|' =>		return 3;
+	'^' =>	return 4;
+	'&' =>	return 5;
+	Oeq or One =>	return 6;
+	'<' or '>' or Oge or Ole => return 7;
+	Olsh or Orsh =>	return 8;
+	'+' or '-' => return 9;
+	'*' or '/' or '%' => return 10;
+	Opow =>	return 11;
+	* =>	return 0;
+	}
+}
+
+rightassoc(c: int): int
+{
+	return c == Opow;
+}
+
+expr(prec: int): int raises(Syntax, Badeval)
+{
+	{
+		v := primary();
+		while(priority(look()) >= prec){
+			op := lex();
+			r := priority(op) + !rightassoc(op);
+			v = eval1(op, v, expr(r));
+		}
+		return v;
+	}exception{
+	Syntax or Badeval =>
+		raise;
+	}
+}
+
+primary(): int raises Syntax
+{
+	{
+		case lex() {
+		'(' =>
+			v := expr(1);
+			if(lex() != ')')
+				raise Syntax;
+			return v;
+		'+' =>	
+			return primary();
+		'-' =>
+			return -primary();
+		'!' =>
+			return !primary();
+		'~' =>
+			return ~primary();
+		Odigits =>
+			return yylval;
+		* =>
+			raise Syntax;
+		}
+	}exception{
+	Syntax =>
+		raise;
+	}
+}
+
+yylval := 0;
+looked := -1;
+
+look(): int
+{
+	looked = lex();
+	return looked;
+}
+
+lex(): int
+{
+	if((c := looked) >= 0){
+		looked = -1;
+		return c;	# if Odigits, assumes yylval untouched
+	}
+	while(evalp < len evalin && isspace(evalin[evalp]))
+		evalp++;
+	if(evalp >= len evalin)
+		return Oeof;
+	case c = evalin[evalp++] {
+	'*' =>
+		return ifnext('*', Opow, '*');
+	'>' =>
+		return ifnext('=', Oge, ifnext('>', Orsh, '>'));
+	'<' =>
+		return ifnext('=', Ole, ifnext('<', Olsh, '<'));
+	'=' =>
+		return ifnext('=', Oeq, Oeq);
+	'!' =>
+		return ifnext('=', One, '!');
+	'|' =>
+		return ifnext('|', Oor, '|');
+	'&' =>
+		return ifnext('&', Oand, '&');
+	'0' to '9' =>
+		evalp--;
+		n := 0;
+		while(evalp < len evalin && (c = evalin[evalp]) >= '0' && c <= '9'){
+			n = n*10 + (c-'0');
+			evalp++;
+		}
+		yylval = n;
+		return Odigits;
+	* =>
+		return c;
+	}
+}
+
+ifnext(a, t, f: int): int
+{
+	if(evalp < len evalin && evalin[evalp] == a){
+		evalp++;
+		return t;
+	}
+	return f;
+}
--- /dev/null
+++ b/appl/cmd/man2html.b
@@ -1,0 +1,1366 @@
+implement Man2html;
+
+include "sys.m";
+	stderr: ref Sys->FD;
+	sys: Sys;
+	print, fprint, sprint: import sys;
+
+
+include "bufio.m";
+
+include "draw.m";
+
+include "daytime.m";
+	dt: Daytime;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+
+Man2html: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+Runeself: con 16r80;
+false, true: con iota;
+
+Troffspec: adt {
+	name: string;
+	value: string;
+};
+
+tspec := array [] of { Troffspec
+	("ff", "ff"),
+	("fi", "fi"),
+	("fl", "fl"),
+	("Fi", "ffi"),
+	("ru", "_"),
+	("em", "&#8212;"),
+	("14", "&#188;"),
+	("12", "&#189;"),
+	("co", "&#169;"),
+	("de", "&#176;"),
+	("dg", "&#161;"),
+	("fm", "&#180;"),
+	("rg", "&#174;"),
+#	("bu", "*"),
+	("bu", "•"),
+	("sq", "&#164;"),
+	("hy", "-"),
+	("pl", "+"),
+	("mi", "-"),
+	("mu", "&#215;"),
+	("di", "&#247;"),
+	("eq", "="),
+	("==", "=="),
+	(">=", ">="),
+	("<=", "<="),
+	("!=", "!="),
+	("+-", "&#177;"),
+	("no", "&#172;"),
+	("sl", "/"),
+	("ap", "&"),
+	("~=", "~="),
+	("pt", "oc"),
+	("gr", "GRAD"),
+	("->", "->"),
+	("<-", "<-"),
+	("ua", "^"),
+	("da", "v"),
+	("is", "Integral"),
+	("pd", "DIV"),
+	("if", "oo"),
+	("sr", "-/"),
+	("sb", "(~"),
+	("sp", "~)"),
+	("cu", "U"),
+	("ca", "(^)"),
+	("ib", "(="),
+	("ip", "=)"),
+	("mo", "C"),
+	("es", "&Oslash;"),
+	("aa", "&#180;"),
+	("ga", "`"),
+	("ci", "O"),
+	("L1", "Lucent"),
+	("sc", "&#167;"),
+	("dd", "++"),
+	("lh", "<="),
+	("rh", "=>"),
+	("lt", "("),
+	("rt", ")"),
+	("lc", "|"),
+	("rc", "|"),
+	("lb", "("),
+	("rb", ")"),
+	("lf", "|"),
+	("rf", "|"),
+	("lk", "|"),
+	("rk", "|"),
+	("bv", "|"),
+	("ts", "s"),
+	("br", "|"),
+	("or", "|"),
+	("ul", "_"),
+	("rn", " "),
+	("*p", "PI"),
+	("**", "*"),
+};
+
+	Entity: adt {
+		 name: string;
+		 value: int;
+	};
+	Entities: array of Entity;
+
+Entities = array[] of {
+		Entity( "&#161;",	'¡' ),
+		Entity( "&#162;",	'¢' ),
+		Entity( "&#163;",	'£' ),
+		Entity( "&#164;",	'¤' ),
+		Entity( "&#165;",	'¥' ),
+		Entity( "&#166;",	'¦' ),
+		Entity( "&#167;",	'§' ),
+		Entity( "&#168;",	'¨' ),
+		Entity( "&#169;",	'©' ),
+		Entity( "&#170;",	'ª' ),
+		Entity( "&#171;",	'«' ),
+		Entity( "&#172;",	'¬' ),
+		Entity( "&#173;",	'­' ),
+		Entity( "&#174;",	'®' ),
+		Entity( "&#175;",	'¯' ),
+		Entity( "&#176;",	'°' ),
+		Entity( "&#177;",	'±' ),
+		Entity( "&#178;",	'²' ),
+		Entity( "&#179;",	'³' ),
+		Entity( "&#180;",	'´' ),
+		Entity( "&#181;",	'µ' ),
+		Entity( "&#182;",	'¶' ),
+		Entity( "&#183;",	'·' ),
+		Entity( "&#184;",	'¸' ),
+		Entity( "&#185;",	'¹' ),
+		Entity( "&#186;",	'º' ),
+		Entity( "&#187;",	'»' ),
+		Entity( "&#188;",	'¼' ),
+		Entity( "&#189;",	'½' ),
+		Entity( "&#190;",	'¾' ),
+		Entity( "&#191;",	'¿' ),
+		Entity( "&Agrave;",	'À' ),
+		Entity( "&Aacute;",	'Á' ),
+		Entity( "&Acirc;",	'Â' ),
+		Entity( "&Atilde;",	'Ã' ),
+		Entity( "&Auml;",	'Ä' ),
+		Entity( "&Aring;",	'Å' ),
+		Entity( "&AElig;",	'Æ' ),
+		Entity( "&Ccedil;",	'Ç' ),
+		Entity( "&Egrave;",	'È' ),
+		Entity( "&Eacute;",	'É' ),
+		Entity( "&Ecirc;",	'Ê' ),
+		Entity( "&Euml;",	'Ë' ),
+		Entity( "&Igrave;",	'Ì' ),
+		Entity( "&Iacute;",	'Í' ),
+		Entity( "&Icirc;",	'Î' ),
+		Entity( "&Iuml;",	'Ï' ),
+		Entity( "&ETH;",	'Ð' ),
+		Entity( "&Ntilde;",	'Ñ' ),
+		Entity( "&Ograve;",	'Ò' ),
+		Entity( "&Oacute;",	'Ó' ),
+		Entity( "&Ocirc;",	'Ô' ),
+		Entity( "&Otilde;",	'Õ' ),
+		Entity( "&Ouml;",	'Ö' ),
+		Entity( "&215;",	'×' ),
+		Entity( "&Oslash;",	'Ø' ),
+		Entity( "&Ugrave;",	'Ù' ),
+		Entity( "&Uacute;",	'Ú' ),
+		Entity( "&Ucirc;",	'Û' ),
+		Entity( "&Uuml;",	'Ü' ),
+		Entity( "&Yacute;",	'Ý' ),
+		Entity( "&THORN;",	'Þ' ),
+		Entity( "&szlig;",	'ß' ),
+		Entity( "&agrave;",	'à' ),
+		Entity( "&aacute;",	'á' ),
+		Entity( "&acirc;",	'â' ),
+		Entity( "&atilde;",	'ã' ),
+		Entity( "&auml;",	'ä' ),
+		Entity( "&aring;",	'å' ),
+		Entity( "&aelig;",	'æ' ),
+		Entity( "&ccedil;",	'ç' ),
+		Entity( "&egrave;",	'è' ),
+		Entity( "&eacute;",	'é' ),
+		Entity( "&ecirc;",	'ê' ),
+		Entity( "&euml;",	'ë' ),
+		Entity( "&igrave;",	'ì' ),
+		Entity( "&iacute;",	'í' ),
+		Entity( "&icirc;",	'î' ),
+		Entity( "&iuml;",	'ï' ),
+		Entity( "&eth;",	'ð' ),
+		Entity( "&ntilde;",	'ñ' ),
+		Entity( "&ograve;",	'ò' ),
+		Entity( "&oacute;",	'ó' ),
+		Entity( "&ocirc;",	'ô' ),
+		Entity( "&otilde;",	'õ' ),
+		Entity( "&ouml;",	'ö' ),
+		Entity( "&247;",	'÷' ),
+		Entity( "&oslash;",	'ø' ),
+		Entity( "&ugrave;",	'ù' ),
+		Entity( "&uacute;",	'ú' ),
+		Entity( "&ucirc;",	'û' ),
+		Entity( "&uuml;",	'ü' ),
+		Entity( "&yacute;",	'ý' ),
+		Entity( "&thorn;",	'þ' ),
+		Entity( "&yuml;",	'ÿ' ),		# &#255;
+
+		Entity( "&#SPACE;",	' ' ),
+		Entity( "&#RS;",	'\n' ),
+		Entity( "&#RE;",	'\r' ),
+		Entity( "&quot;",	'"' ),
+		Entity( "&amp;",	'&' ),
+		Entity( "&lt;",	'<' ),
+		Entity( "&gt;",	'>' ),
+
+		Entity( "CAP-DELTA",	'Δ' ),
+		Entity( "ALPHA",	'α' ),
+		Entity( "BETA",	'β' ),
+		Entity( "DELTA",	'δ' ),
+		Entity( "EPSILON",	'ε' ),
+		Entity( "THETA",	'θ' ),
+		Entity( "MU",		'μ' ),
+		Entity( "PI",		'π' ),
+		Entity( "TAU",	'τ' ),
+		Entity( "CHI",	'χ' ),
+
+		Entity( "<-",		'←' ),
+		Entity( "^",		'↑' ),
+		Entity( "->",		'→' ),
+		Entity( "v",		'↓' ),
+		Entity( "!=",		'≠' ),
+		Entity( "<=",		'≤' ),
+		Entity( nil, 0 ),
+};
+
+
+Hit: adt {
+	glob: string;
+	chap: string;
+	mtype: string;
+	page: string;
+};
+
+Lnone, Lordered, Lunordered, Ldef, Lother: con iota;	# list types
+
+Chaps: adt {
+	name: string;
+	primary: int;
+};
+
+Types: adt {
+	name: string;
+	desc: string;
+};
+
+
+# having two separate flags here allows for inclusion of old-style formatted pages
+# under a new-style three-level tree
+Oldstyle: adt {
+	names: int;	# two-level directory tree?
+	fmt: int;		# old internal formats: e.g., "B" font means "L"; name in .TH in all caps
+};
+
+Href: adt {
+	title: string;
+	chap: string;
+	mtype: string;
+	man: string;
+};
+
+# per-thread global data
+Global: adt {
+	bufio: Bufio;
+	bin: ref Bufio->Iobuf;
+	bout: ref Bufio->Iobuf;
+	topname: string;		# name of the top level categories in the manual
+	chaps: array of Chaps;	# names of top-level partitions of this manual
+	types: array of Types;	# names of second-level partitions
+	oldstyle: Oldstyle;
+	mantitle: string;
+	mandir: string;
+	thisone: Hit;		# man page we're displaying
+	mtime: int;			# last modification time of thisone
+	href: Href;			# hrefs of components of this man page
+	hits: array of Hit;
+	nhits: int;
+	list_type: int;
+	pm: string;			# proprietary marking
+	def_goobie: string;	# deferred goobie
+	sop: int;			# output at start of paragraph?
+	sol: int;			# input at start of line?
+	broken: int;		# output at a break?
+	fill: int;			# in fill mode?
+ 	pre: int;			# in PRE block?
+	example: int;		# an example active?
+	ipd: int;			# emit inter-paragraph distance?
+	indents: int;
+	hangingdt: int;
+	curfont: string;		# current font
+	prevfont: string;		# previous font
+	lastc: int;			# previous char from input scanner
+	def_sm: int;		# amount of deferred "make smaller" request
+
+	mk_href_chap: fn(g: self ref Global, chap: string);
+	mk_href_man: fn(g: self ref Global, man: string, oldstyle: int);
+	mk_href_mtype: fn(g: self ref Global, chap, mtype: string);
+	dobreak: fn(g: self ref Global);
+	print: fn(g: self ref Global, s: string);
+	softbr: fn(g: self ref Global): string;
+	softp: fn(g: self ref Global): string;
+};
+
+header := "<HTML><HEAD>";
+initial := "";
+trailer := "</BODY></HTML>";
+
+usage()
+{
+	sys->fprint(stderr, "Usage: man2html [-h header] [-i initialtext] [-t trailer] file [section]\n");
+	raise "fail:usage";
+}
+
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	str = load String String->PATH;
+	dt = load Daytime Daytime->PATH;
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("man2html [-h header] [-t trailer] file [section]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'h' =>	header = arg->earg();
+		't' =>	trailer = arg->earg();
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+	g := Global_init();
+	page := hd args;
+	args = tl args;
+	section := "1";
+	if(args != nil)
+		section = hd args;
+	hit := Hit ("", "man", section, page);
+	domanpage(g, hit);
+	g.print(trailer+"\n");
+	g.bufio->g.bout.flush();
+}
+
+# remove markup from a string
+# doesn't handle nested/quoted delimiters
+demark(s: string): string
+{
+	t: string;
+	clean := true;
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'<' =>
+			clean = false;
+		'>' =>
+			clean = true;
+		* =>
+			if (clean)
+				t[len t] = s[i];
+		}		
+	}
+	return t;
+}
+
+
+#
+#  Convert an individual man page to HTML and output.
+#
+domanpage(g: ref Global, man: Hit)
+{
+	file := man.page;
+	g.bin = g.bufio->open(file, Bufio->OREAD);
+	g.bout = g.bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+	if (g.bin == nil) {
+		fprint(stderr, "Cannot open %s: %r\n", file);
+		return;
+	}
+	(err, info) := sys->fstat(g.bin.fd);
+	if (! err) {
+		g.mtime = info.mtime;
+	}
+	g.thisone = man;
+	while ((p := getnext(g)) != nil) {
+		c := p[0];
+		if (c == '.' && g.sol) {
+			if (g.pre) {
+				g.print("</PRE>");
+				g.pre = false;
+			}
+			dogoobie(g, false);
+			dohangingdt(g);
+		} else if (g.def_goobie != nil || g.def_sm != 0) {
+			g.bufio->g.bin.ungetc();
+			dogoobie(g, true);
+		} else if (c == '\n') {
+			g.print(p);
+			dohangingdt(g);
+		} else
+			g.print(p);
+	}
+	if (g.pm != nil) {
+		g.print("<BR><BR><BR><FONT SIZE=-2><CENTER>\n");
+		g.print(g.pm);
+		g.print("<BR></CENTER></FONT>\n");
+	}
+	closeall(g, 0);
+	rev(g, g.bin);
+}
+
+dogoobie(g: ref Global, deferred: int)
+{
+	# read line, translate special chars
+	line := getline(g);
+	if (line == nil || line == "\n")
+		return;
+
+	# parse into arguments
+	token: string;
+	argl, rargl: list of string;	# create reversed version, then invert
+	while ((line = str->drop(line, " \t\n")) != nil)
+		if (line[0] == '"') {
+			(token, line) = split(line[1:], '"');
+			rargl = token :: rargl;
+		} else {
+			(token, line) = str->splitl(line, " \t");
+			rargl = token :: rargl;
+		}
+
+	if (rargl == nil && !deferred)
+		return;
+	for ( ; rargl != nil; rargl = tl rargl)
+		argl = hd rargl :: argl;
+
+	def_sm := g.def_sm;
+	if (deferred && def_sm > 0) {
+		g.print(sprint("<FONT SIZE=-%d>", def_sm));
+		if (g.def_goobie == nil)
+			argl = "dS" :: argl;	# dS is our own local creation
+	}
+
+	subgoobie(g, argl);
+
+	if (deferred && def_sm > 0) {
+		g.def_sm = 0;
+		g.print("</FONT>");
+	}
+}
+
+subgoobie(g: ref Global, argl: list of string)
+{
+	if (g.def_goobie != nil) {
+		argl = g.def_goobie :: argl;
+		g.def_goobie = nil;
+		if (tl argl == nil)
+			return;
+	}
+
+	# the command part is at most two characters, but may be concatenated with the first arg
+	cmd := hd argl;
+	argl = tl argl;
+	if (len cmd > 2) {
+		cmd = cmd[0:2];
+		argl =  cmd[2:] :: argl;
+	}
+
+	case cmd {
+
+	"B" or "I" or "L" or "R" =>
+		font(g, cmd, argl);		# "R" macro implicitly generated by deferred R* macros
+
+	"BI" or "BL" or "BR" or
+	"IB" or "IL" or
+	"LB" or "LI" or
+	"RB" or "RI" or "RL" =>
+		altfont(g, cmd[0:1], cmd[1:2], argl, true);
+
+	"IR" or "LR" =>
+		anchor(g, cmd[0:1], cmd[1:2], argl);		# includes man page refs ("IR" is old style, "LR" is new)
+
+	"dS" =>
+		printargs(g, argl);
+		g.print("\n");
+
+	"1C" or "2C" or "DT" or "TF" =>	 # ignore these
+		return;
+
+	"ig" =>
+		while ((line := getline(g)) != nil){
+			if(len line > 1 && line[0:2] == "..")
+				break;
+		}
+		return;
+
+	"P" or "PP" or "LP" =>
+			g_PP(g);
+
+	"EE" =>	g_EE(g);
+	"EX" =>	g_EX(g);
+	"HP" =>	g_HP_TP(g, 1);
+	"IP" =>	g_IP(g, argl);
+	"PD" =>	g_PD(g, argl);
+	"PM" =>	g_PM(g, argl);
+	"RE" =>	g_RE(g);
+	"RS" =>	g_RS(g);
+	"SH" =>	g_SH(g, argl);
+	"SM" =>	g_SM(g, argl);
+	"SS" =>	g_SS(g, argl);
+	"TH" =>	g_TH(g, argl);
+	"TP" =>	g_HP_TP(g, 3);
+
+	"br" =>	g_br(g);
+	"sp" =>	g_sp(g, argl);
+	"ti" =>	g_br(g);
+	"nf" =>	g_nf(g);
+	"fi" =>	g_fi(g);
+	"ft" =>	g_ft(g, argl);
+
+	* =>		return;		# ignore unrecognized commands
+	}
+
+}
+
+g_br(g: ref Global)
+{
+	if (g.hangingdt != 0) {
+		g.print("<DD>");
+		g.hangingdt = 0;
+	} else if (g.fill && ! g.broken)
+		g.print("<BR>\n");
+	g.broken = true;
+}
+
+g_EE(g: ref Global)
+{
+	g.print("</PRE>\n");
+	g.fill = true;
+	g.broken = true;
+	g.example = false;
+}
+
+g_EX(g: ref Global)
+{
+	g.print("<PRE>");
+	if (! g.broken)
+		g.print("\n");
+	g.sop = true;
+	g.fill = false;
+	g.broken = true;
+	g.example = true;
+}
+
+g_fi(g: ref Global)
+{
+	if (g.fill)
+		return;
+	g.fill = true;
+	g.print("<P style=\"display: inline; white-space: normal\">\n");
+	g.broken = true;
+	g.sop = true;
+}
+
+g_ft(g: ref Global, argl: list of string)
+{
+	font: string;
+	arg: string;
+
+	if (argl == nil)
+		arg = "P";
+	else
+		arg = hd argl;
+
+	if (g.curfont != nil)
+		g.print(sprint("</%s>", g.curfont));
+
+	case arg {
+	"2" or "I" =>
+		font = "I";
+	"3" or "B" =>
+		font = "B";
+	"5" or "L" =>
+		font = "TT";
+	"P" =>
+		font = g.prevfont;
+	* =>
+		font = nil;
+	}
+	g.prevfont = g.curfont;
+	g.curfont = font;
+	if (g.curfont != nil)
+		if (g.fill)
+			g.print(sprint("<%s>", g.curfont));
+		else
+			g.print(sprint("<%s style=\"white-space: pre\">", g.curfont));
+}
+
+# level == 1 is a .HP; level == 3 is a .TP
+g_HP_TP(g: ref Global, level: int)
+{
+	case g.list_type {
+	Ldef =>
+		if (g.hangingdt != 0)
+			g.print("<DD>");
+		g.print(g.softbr() + "<DT>");
+	* =>
+		closel(g);
+		g.list_type = Ldef;
+		g.print("<DL compact>\n" + g.softbr() + "<DT>");
+	}
+	g.hangingdt = level;
+	g.broken = true;
+}
+
+g_IP(g: ref Global, argl: list of string)
+{
+	case g.list_type {
+
+	Lordered or Lunordered or Lother =>
+		;	# continue with an existing list
+
+	* =>
+		# figure out the type of a new list and start it
+		closel(g);
+		arg := "";
+		if (argl != nil)
+			arg = hd argl;
+		case arg {
+			"1" or "i" or "I" or "a" or "A" =>
+				g.list_type = Lordered;
+				g.print(sprint("<OL type=%s>\n", arg));
+			"*" or "•" or "&#8226;" =>
+				g.list_type = Lunordered;
+				g.print("<UL type=disc>\n");
+			"○" or "&#9675;"=>
+				g.list_type = Lunordered;
+				g.print("<UL type=circle>\n");
+			"□" or "&#9633;" =>
+				g.list_type = Lunordered;
+				g.print("<UL type=square>\n");
+			* =>
+				g.list_type = Lother;
+				g.print("<DL compact>\n");
+			}
+	}
+
+	# actually do this list item
+	case g.list_type {
+	Lother =>
+		g.print(g.softp());	# make sure there's space before each list item
+		if (argl != nil) {
+			g.print("<DT>");
+			printargs(g, argl);
+		}
+		g.print("\n<DD>");
+
+	Lordered or Lunordered =>
+		g.print(g.softp() + "<LI>");
+	}
+	g.broken = true;
+}
+
+g_nf(g: ref Global)
+{
+	if (! g.fill)
+		return;
+	g.fill = false;
+	g.print("<PRE>\n");
+	g.broken = true;
+	g.sop = true;
+	g.pre = true;
+}
+
+g_PD(g: ref Global, argl: list of string)
+{
+	if (len argl == 1 && hd argl == "0")
+		g.ipd = false;
+	else
+		g.ipd = true;
+}
+
+g_PM(g: ref Global, argl: list of string)
+{
+	code := "P";
+	if (argl != nil)
+		code = hd argl;
+	case code {
+	* =>		# includes "1" and "P"
+		g.pm = "<B>Lucent Technologies - Proprietary</B>\n" +
+			"<BR>Use pursuant to Company Instructions.\n";
+	"2" or "RS" =>
+		g.pm = "<B>Lucent Technologies - Proprietary (Restricted)</B>\n" +
+			"<BR>Solely for authorized persons having a need to know\n" +
+			"<BR>pursuant to Company Instructions.\n";
+	"3" or "RG" =>
+		g.pm = "<B>Lucent Technologies - Proprietary (Registered)</B>\n" +
+			"<BR>Solely for authorized persons having a need to know\n" +
+			"<BR>and subject to cover sheet instructions.\n";
+	"4" or "CP" =>
+		g.pm = "SEE PROPRIETARY NOTICE ON COVER PAGE\n";
+	"5" or "CR" =>
+		g.pm = "Copyright xxxx Lucent Technologies\n" +	# should fill in the year from the date register
+			"<BR>All Rights Reserved.\n";
+	"6" or "UW" =>
+		g.pm = "THIS DOCUMENT CONTAINS PROPRIETARY INFORMATION OF\n" +
+			"<BR>LUCENT TECHNOLOGIES INC. AND IS NOT TO BE DISCLOSED OR USED EXCEPT IN\n" +
+			"<BR>ACCORDANCE WITH APPLICABLE AGREEMENTS.\n" +
+			"<BR>Unpublished & Not for Publication\n";
+	}
+}
+
+g_PP(g: ref Global)
+{
+	closel(g);
+	reset_font(g);
+	p := g.softp();
+	if (p != nil)
+		g.print(p);
+	g.sop = true;
+	g.broken = true;
+}
+
+g_RE(g: ref Global)
+{
+	g.print("</DL>\n");
+	g.indents--;
+	g.broken = true;
+}
+
+g_RS(g: ref Global)
+{
+	g.print("<DL>\n<DT><DD>");
+	g.indents++;
+	g.broken = true;
+}
+
+g_SH(g: ref Global, argl: list of string)
+{
+	closeall(g, 1);		# .SH is top-level list item
+	if (g.example)
+		g_EE(g);
+	g_fi(g);
+	if (g.fill && ! g.sop)
+		g.print("<P>");
+	g.print("<DT><H4>");
+	printargs(g, argl);
+	g.print("</H4>\n");
+	g.print("<DD>\n");
+	g.sop = true;
+	g.broken = true;
+}
+
+g_SM(g: ref Global, argl: list of string)
+{
+	g.def_sm++;		# can't use def_goobie, lest we collide with a deferred font macro
+	if (argl == nil)
+		return;
+	g.print(sprint("<FONT SIZE=-%d>", g.def_sm));
+	printargs(g, argl);
+	g.print("</FONT>\n");
+	g.def_sm = 0;
+}
+
+g_sp(g: ref Global, argl: list of string)
+{
+	if (g.sop && g.fill)
+		return;
+	count := 1;
+	if (argl != nil) {
+		rcount := real hd argl;
+		count = int rcount;	# may be 0 (e.g., ".sp .5")
+		if (count == 0 && rcount > 0.0)
+			count = 1;		# force whitespace for fractional lines
+	}
+	g.dobreak();
+	for (i := 0; i < count; i++)
+		g.print("&nbsp;<BR>\n");
+	g.broken = true;
+	g.sop = count > 0;
+}
+
+g_SS(g: ref Global, argl: list of string)
+{
+	closeall(g, 1);
+	g.indents++;
+	g.print(g.softp() + "<DL><DT><FONT SIZE=3><B>");
+	printargs(g, argl);
+	g.print("</B></FONT>\n");
+	g.print("<DD>\n");
+	g.sop = true;
+	g.broken = true;
+}
+
+g_TH(g: ref Global, argl: list of string)
+{
+	if (g.oldstyle.names && len argl > 2)
+		argl = hd argl :: hd tl argl :: nil;	# ignore extra .TH args on pages in oldstyle trees
+	case len argl {
+	0 =>
+		g.oldstyle.fmt = true;
+		title(g, sprint("%s", g.href.title), false);
+	1 =>
+		g.oldstyle.fmt = true;
+		title(g, sprint("%s", hd argl), false);	# any pages use this form?
+	2 =>
+		g.oldstyle.fmt = true;
+		g.thisone.page = hd argl;
+		g.thisone.mtype = hd tl argl;
+		g.mk_href_man(hd argl, true);
+		g.mk_href_mtype(nil, hd tl argl);
+		title(g, sprint("%s(%s)", g.href.man, g.href.mtype), false);
+	* =>
+		g.oldstyle.fmt = false;
+		chap := hd tl tl argl;
+		g.mk_href_chap(chap);
+		g.mk_href_man(hd argl, false);
+		g.mk_href_mtype(chap, hd tl argl);
+		title(g, sprint("%s/%s/%s(%s)", g.href.title, g.href.chap, g.href.man, g.href.mtype), false);
+	}
+	g.print("[<a href=\"../index.html\">manual index</a>]");
+	g.print("[<a href=\"INDEX.html\">section index</a>]<p>");
+	g.print("<DL>\n");	# whole man page is just one big list
+	g.indents = 1;
+	g.sop = true;
+	g.broken = true;
+}
+
+dohangingdt(g: ref Global)
+{
+	case g.hangingdt {
+	3 =>
+		g.hangingdt--;
+	2 =>
+		g.print("<DD>");
+		g.hangingdt = 0;
+		g.broken = true;
+	}
+}
+
+# close a list, if there's one active
+closel(g: ref Global)
+{
+	case g.list_type {
+	Lordered =>
+		g.print("</OL>\n");
+		g.broken = true;
+	Lunordered =>
+		g.print("</UL>\n");
+		g.broken = true;
+	Lother or Ldef =>
+		g.print("</DL>\n");
+		g.broken = true;
+	}
+	g.list_type = Lnone;
+}
+
+closeall(g: ref Global, level: int)
+{
+	closel(g);
+	reset_font(g);
+	while (g.indents > level) {
+		g.indents--;
+		g.print("</DL>\n");
+		g.broken = true;
+	}
+}
+
+#
+# Show last revision date for a file.
+#
+rev(g: ref Global, filebuf: ref Bufio->Iobuf)
+{
+	if (g.mtime == 0) {
+		(err, info) := sys->fstat(filebuf.fd);
+		if (! err)
+			g.mtime = info.mtime;
+	}
+	if (g.mtime != 0) {
+		g.print("<P><TABLE width=\"100%\" border=0 cellpadding=10 cellspacing=0 bgcolor=\"#E0E0E0\">\n");
+		g.print("<TR>");
+		g.print(sprint("<TD align=left><FONT SIZE=-1>"));
+		g.print(sprint("%s(%s)", g.thisone.page, g.thisone.mtype));
+		g.print("</FONT></TD>\n");
+		g.print(sprint("<TD align=right><FONT SIZE=-1><I>Rev:&nbsp;&nbsp;%s</I></FONT></TD></TR></TABLE>\n",
+			dt->text(dt->gmt(g.mtime))));
+	}
+}
+
+#
+# Some font alternation macros are references to other man pages;
+# detect them (second arg contains balanced parens) and make them into hot links.
+#
+anchor(g: ref Global, f1, f2: string, argl: list of string)
+{
+	final := "";
+	link := false;
+	if (len argl == 2) {
+		(s, e) := str->splitl(hd tl argl, ")");
+		if (str->prefix("(", s) && e != nil) {
+			# emit href containing search for target first
+			# if numeric, do old style
+			link = true;
+			file := hd argl;
+			(chap, man) := split(httpunesc(file), '/');
+			if (man == nil) {
+				# given no explicit chapter prefix, use current chapter
+				man = chap;
+				chap = g.thisone.chap;
+			}
+			mtype := s[1:];
+			if (mtype == nil)
+				mtype = "-";
+			(n, toks) := sys->tokenize(mtype, ".");	# Fix section 10
+			if (n > 1) mtype = hd toks;
+			g.print(sprint("<A href=\"../%s/%s.html\">", mtype, fixlink(man)));
+
+			#
+			# now generate the name the user sees, with terminal punctuation
+			# moved after the closing </A>.
+			#
+			if (len e > 1)
+				final = e[1:];
+			argl = hd argl :: s + ")" :: nil;
+		}
+	}
+	altfont(g, f1, f2, argl, false);
+	if (link) {
+		g.print("</A>");
+		font(g, f2, final :: nil);
+	} else
+		g.print("\n");
+}
+
+
+#
+# Fix up a link
+#
+
+fixlink(l: string): string
+{
+	ll := str->tolower(l);
+	if (ll == "copyright") ll = "1" + ll;
+	(a, b) := str->splitstrl(ll, "intro");
+	if (len b == 5) ll = a + "0" + b;
+	return ll;
+}
+
+
+#
+# output argl in font f
+#
+font(g: ref Global, f: string, argl: list of string)
+{
+	if (argl == nil) {
+		g.def_goobie = f;
+		return;
+	}
+	case f {
+	"L" => 	f = "TT";
+	"R" =>	f = nil;
+	}
+	if (f != nil) 			# nil == default (typically Roman)
+		g.print(sprint("<%s>", f));
+	printargs(g, argl);
+	if (f != nil)
+		g.print(sprint("</%s>", f));
+	g.print("\n");
+	g.prevfont = f;
+}
+
+#
+# output concatenated elements of argl, alternating between fonts f1 and f2
+#
+altfont(g: ref Global, f1, f2: string, argl: list of string, newline: int)
+{
+	reset_font(g);
+	if (argl == nil) {
+		g.def_goobie = f1;
+		return;
+	}
+	case f1 {
+	"L" =>	f1 = "TT";
+	"R" =>	f1 = nil;
+	}
+	case f2 {
+	"L" =>	f2 = "TT";
+	"R" =>	f2 = nil;
+	}
+	f := f1;
+	for (; argl != nil; argl = tl argl) {
+		if (f != nil)
+			g.print(sprint("<%s>%s</%s>", f, hd argl, f));
+		else
+			g.print(hd argl);
+		if (f == f1)
+			f = f2;
+		else
+			f = f1;
+	}
+	if (newline)
+		g.print("\n");
+	g.prevfont = f;
+}
+
+# not yet implemented
+map_font(nil: ref Global, nil: string)
+{
+}
+
+reset_font(g: ref Global)
+{
+	if (g.curfont != nil) {
+		g.print(sprint("</%s>", g.curfont));
+		g.prevfont = g.curfont;
+		g.curfont = nil;
+	}
+}
+
+printargs(g: ref Global, argl: list of string)
+{
+	for (; argl != nil; argl = tl argl)
+		if (tl argl != nil)
+			g.print(hd argl + " ");
+		else
+			g.print(hd argl);
+}
+
+# any parameter can be nil
+addhit(g: ref Global, chap, mtype, page: string)
+{
+	# g.print(sprint("Adding %s / %s (%s) . . .", chap, page, mtype));		# debug
+	# always keep a spare slot at the end
+	if (g.nhits >= len g.hits - 1)
+		g.hits = (array[len g.hits + 32] of Hit)[0:] = g.hits;
+	g.hits[g.nhits].glob = chap + " " + mtype + " " + page;
+	g.hits[g.nhits].chap = chap;
+	g.hits[g.nhits].mtype = mtype;
+	g.hits[g.nhits++].page = page;
+}
+
+Global.dobreak(g: self ref Global)
+{
+	if (! g.broken) {
+		g.broken = true;
+		g.print("<BR>\n");
+	}
+}
+
+Global.print(g: self ref Global, s: string)
+{
+	g.bufio->g.bout.puts(s);
+	if (g.sop || g.broken) {
+		# first non-white space, non-HTML we print takes us past the start of the paragraph & line
+		# (or even white space, if we're in no-fill mode)
+		for (i := 0; i < len s; i++) {
+			case s[i] {
+			'<' =>
+				while (++i < len s && s[i] != '>')
+					;
+				continue;
+			' ' or '\t' or '\n' =>
+				if (g.fill)
+					continue;
+			}
+			g.sop = false;
+			g.broken = false;
+			break;
+		}
+	}
+}
+
+Global.softbr(g: self ref Global): string
+{
+	if (g.broken)
+		return nil;
+	g.broken = true;
+	return "<BR>";
+}
+
+# provide a paragraph marker, unless we're already at the start of a section
+Global.softp(g: self ref Global): string
+{
+	if (g.sop)
+		return nil;
+	else if (! g.ipd)
+		return "<BR>";
+	if (g.fill)
+		return "<P>";
+	else
+		return "<P style=\"white-space: pre\">";
+}
+
+#
+# get (remainder of) a line
+#
+getline(g: ref Global): string
+{
+	line := "";
+	while ((token := getnext(g)) != "\n") {
+		if (token == nil)
+			return line;
+		line += token;
+	}
+	return line+"\n";
+}
+
+#
+# Get next logical character.  Expand it with escapes.
+#
+getnext(g: ref Global): string
+{
+	iob := g.bufio;
+	Iobuf: import iob;
+
+	font: string;
+	token: string;
+	bin := g.bin;
+
+	g.sol = (g.lastc == '\n');
+
+	c := bin.getc();
+	if (c < 0)
+		return nil;
+	g.lastc = c;
+	if (c >= Runeself) {
+		for (i := 0;  i < len Entities; i++)
+			if (Entities[i].value == c)
+				return Entities[i].name;
+		return sprint("&#%d;", c);
+	}
+	case c {
+	'<' =>
+		return "&lt;";
+	'>' =>
+		return "&gt;";
+	'\\' =>
+		c = bin.getc();
+		if (c < 0)
+			return nil;
+		g.lastc = c;
+		case c {
+
+		' ' =>
+			return "&nbsp;";
+
+		# chars to ignore
+		'|' or '&' or '^' =>
+			return getnext(g);
+
+		# ignore arg
+		'k' =>
+			nil = bin.getc();
+			return getnext(g);
+
+		# defined strings
+		'*' =>
+			case bin.getc() {
+			'R' =>
+				return "&#174;";
+			}
+			return getnext(g);
+
+		# special chars
+		'(' =>
+			token[0] = bin.getc();
+			token[1] = bin.getc();
+			for (i := 0; i < len tspec; i++)
+				if (token == tspec[i].name)
+					return tspec[i].value;
+			return "&#191;";
+		'c' =>
+			c = bin.getc();
+			if (c < 0)
+				return nil;
+			else if (c == '\n') {
+				g.lastc = c;
+				g.sol = true;
+				token[0] = bin.getc();
+				return token;
+			}
+			# DEBUG: should there be a "return xxx" here?
+		'e' =>
+			return "\\";
+		'f' =>
+			g.lastc = c = bin.getc();
+			if (c < 0)
+				return nil;
+			case c {
+			'2' or 	'I' =>
+				font = "I";
+			'3' or 	'B' =>
+				font = "B";
+			'5' or 	'L' =>
+				font = "TT";
+			'P' =>
+				font = g.prevfont;
+			* =>					# includes '1' and 'R'
+				font = nil;
+			}
+# There are serious problems with this. We don't know the fonts properly at this stage.
+#			g.prevfont = g.curfont;
+#			g.curfont = font;
+#			if (g.prevfont != nil)
+#				token = sprint("</%s>", g.prevfont);
+#			if (g.curfont != nil)
+#				token += sprint("<%s>", g.curfont);
+			if (token == nil)
+				return "<i></i>";	# looks odd but it avoids inserting a space in <pre> text
+			return token;
+		's' =>
+			sign := '+';
+			size := 0;
+			relative := false;
+		getsize:
+			for (;;) {
+				c = bin.getc();
+				if (c < 0)
+					return nil;
+				case c {
+				'+' =>
+					relative = true;
+				'-' =>
+					sign = '-';
+					relative = true;
+				'0' to '9' =>
+					size = size * 10 + (c - '0');
+				* =>
+					bin.ungetc();
+					break getsize;
+				}
+				g.lastc = c;
+			}
+			if (size == 0)
+				token = "</FONT>";
+			else if (relative)
+				token = sprint("<FONT SIZE=%c%d>", sign, size);
+			else
+				token = sprint("<FONT SIZE=%d>", size);
+			return token;
+		}
+	}
+	token[0] = c;
+	return token;
+}
+
+#
+# Return strings before and after the left-most instance of separator;
+# (s, nil) if no match or separator is last char in s.
+#
+split(s: string, sep: int): (string, string)
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == sep)
+			return (s[:i], s[i+1:]);	# s[len s:] is a valid slice, with value == nil
+ 	return (s, nil);
+}
+
+Global_init(): ref Global
+{
+	g := ref Global;
+	g.bufio = load Bufio Bufio->PATH;
+	g.chaps = array[20] of Chaps;
+	g.types = array[20] of Types;
+	g.mantitle = "";
+	g.href.title = g.mantitle;		# ??
+	g.mtime = 0;
+	g.nhits = 0;
+	g.oldstyle.names = false;
+	g.oldstyle.fmt = false;
+	g.topname = "System";
+	g.list_type = Lnone;
+	g.def_sm = 0;
+	g.hangingdt = 0;
+	g.indents = 0;
+	g.sop = true;
+	g.broken = true;
+	g.ipd = true;
+	g.fill = true;
+	g.example = false;
+	g.pre = false;
+	g.lastc = '\n';
+	return g;
+}
+
+Global.mk_href_chap(g: self ref Global, chap: string)
+{
+	if (chap != nil)
+		g.href.chap = sprint("<A href=\"%s/%s?man=*\"><B>%s</B></A>", g.mandir, chap, chap);
+}
+
+Global.mk_href_man(g: self ref Global, man: string, oldstyle: int)
+{
+	rman := man;
+	if (oldstyle)
+		rman = str->tolower(man);	# compensate for tradition of putting titles in all CAPS
+	g.href.man = sprint("<A href=\"%s?man=%s\"><B>%s</B></A>", g.mandir, rman, man);
+}
+
+Global.mk_href_mtype(g: self ref Global, chap, mtype: string)
+{
+	g.href.mtype = sprint("<A href=\"%s/%s/%s\"><B>%s</B></A>", g.mandir, chap, mtype, mtype);
+}
+
+# We assume that anything >= Runeself is already in UTF.
+#
+httpunesc(s: string): string
+{
+	t := "";
+	for (i := 0; i < len s; i++) {
+		c := s[i];
+		if (c == '&' && i + 1 < len s) {
+			(char, rem) := str->splitl(s[i+1:], ";");
+			if (rem == nil)
+				break;	# require the terminating ';'
+			if (char == nil)
+				continue;
+			if (char[0] == '#' && len char > 1) {
+				c = int char[1:];
+				i += len char;
+				if (c < 256 && c >= 161) {
+					t[len t] = Entities[c-161].value;
+					continue;
+				}
+			} else {
+				for (j := 0; j < len Entities; j++)
+					if (Entities[j].name == char)
+						break;
+				if (j < len Entities) {
+					i += len char;
+					t[len t] = Entities[j].value;
+					continue;
+				}
+			}
+		}
+		t[len t] = c;
+	}
+	return t;
+}
+
+
+
+title(g: ref Global, t: string, search: int)
+{
+	if(search)
+		;	# not yet used
+	g.print(header+"\n");
+	g.print(sprint("<TITLE>Inferno's %s</TITLE>\n", demark(t)));
+	g.print("</HEAD>\n");
+	g.print("<BODY>"+initial+"\n");
+
+}
--- /dev/null
+++ b/appl/cmd/man2txt.b
@@ -1,0 +1,79 @@
+implement Man2txt;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "man.m";
+
+Man2txt: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+W: adt {
+	textwidth: fn(w: self ref W, text: Parseman->Text): int;
+};
+
+output: ref Iobuf;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->print("cannot load Bufio module: %r\n");
+		raise "fail:init";
+	}
+
+	stdout := sys->fildes(1);
+	output = bufio->fopen(stdout, Sys->OWRITE);
+
+	parser := load Parseman Parseman->PATH;
+	parser->init();
+
+	argv = tl argv;
+	for (; argv != nil ; argv = tl argv) {
+		fname := hd argv;
+		fd := sys->open(fname, Sys->OREAD);
+		if (fd == nil) {
+			sys->print("cannot open %s: %r\n", fname);
+			continue;
+		}
+		m := Parseman->Metrics(65, 1, 1, 1, 1, 5, 2);
+		
+		datachan := chan of list of (int, Parseman->Text);
+		w: ref W;
+		spawn parser->parseman(fd, m, 1, w, datachan);
+		for (;;) {
+			line := <- datachan;
+			if (line == nil)
+				break;
+			setline(line);
+		}
+		output.flush();
+	}
+	output.close();
+}
+
+W.textwidth(nil: self ref W, text: Parseman->Text): int
+{
+	return len text.text;
+}
+
+setline(line: list of (int, Parseman->Text))
+{
+#return;
+	offset := 0;
+	for (; line != nil; line = tl line) {
+		(indent, txt) := hd line;
+		while (offset < indent) {
+			output.putc(' ');
+			offset++;
+		}
+		output.puts(txt.text);
+		offset += len txt.text;
+	}
+	output.putc('\n');
+}
--- /dev/null
+++ b/appl/cmd/manufacture.b
@@ -1,0 +1,42 @@
+implement Manufacture;
+
+include "sys.m";
+FD, Dir: import Sys;
+sys: Sys;
+
+include "draw.m";
+draw: Draw;
+Context, Display, Font, Screen, Image, Point, Rect: import draw;
+
+Manufacture: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+stderr: ref FD;
+
+init(nil: ref Context, argv: list of string)
+{
+	s: string;
+	argv0: string;
+
+	argv0 = hd argv;
+	argv = tl argv;
+	sys = load Sys Sys->PATH;
+
+	stderr = sys->fildes(2);
+
+	fd := sys->create("/nvfs/ID", sys->OWRITE, 8r666);
+	if(fd == nil){
+		sys->fprint(stderr, "manufacture: can't create /nvfs/ID: %r\n");
+		return;
+	}
+
+	while(argv != nil) {
+		s = hd argv;
+		sys->fprint(fd, "%s", s);
+		argv = tl argv;
+		if(argv != nil)
+			sys->fprint(fd, " ");
+	}
+}
--- /dev/null
+++ b/appl/cmd/mash/builtins.b
@@ -1,0 +1,347 @@
+implement Mashbuiltin;
+
+#
+#	"builtins" builtin, defines:
+#
+#	env	- print environment or individual elements
+#	eval	- interpret arguments as mash input
+#	exit	- exit toplevel, eval or subshell
+#	load	- load a builtin
+#	prompt	- print or set prompt
+#	quote	- print arguments quoted as input for mash
+#	run	- interpret a file as mash input
+#	status	- report existence of error output
+#	time	- time the execution of a command
+#	whatis	- print variable, function and builtin
+#
+
+include	"mash.m";
+include	"mashparse.m";
+
+mashlib:	Mashlib;
+
+Cmd, Env, Stab:	import mashlib;
+sys, bufio:	import mashlib;
+
+Iobuf:	import bufio;
+
+#
+#	Interface to catch the use as a command.
+#
+init(nil: ref Draw->Context, nil: list of string)
+{
+	ssys := load Sys Sys->PATH;
+	ssys->fprint(ssys->fildes(2), "builtins: cannot run as a command\n");
+	raise "fail: error";
+}
+
+#
+#	Used by whatis.
+#
+name(): string
+{
+	return "builtins";
+}
+
+#
+#	Install commands.
+#
+mashinit(nil: list of string, lib: Mashlib, this: Mashbuiltin, e: ref Env)
+{
+	mashlib = lib;
+	e.defbuiltin("env", this);
+	e.defbuiltin("eval", this);
+	e.defbuiltin("exit", this);
+	e.defbuiltin("load", this);
+	e.defbuiltin("prompt", this);
+	e.defbuiltin("quote", this);
+	e.defbuiltin("run", this);
+	e.defbuiltin("status", this);
+	e.defbuiltin("time", this);
+	e.defbuiltin("whatis", this);
+}
+
+#
+#	Execute a builtin.
+#
+mashcmd(e: ref Env, l: list of string)
+{
+	case hd l {
+	"env" =>
+		l = tl l;
+		if (l == nil) {
+			out := e.outfile();
+			if (out == nil)
+				return;
+			prsymbs(out, e.global, "=");
+			prsymbs(out, e.local, ":=");
+			out.close();
+		} else
+			e.usage("env");
+	"eval" =>
+		eval(e, tl l);
+	"exit" =>
+		raise mashlib->EXIT;
+	"load" =>
+		l = tl l;
+		if (len l == 1)
+			e.doload(hd l);
+		else
+			e.usage("load file");
+	"prompt" =>
+		l = tl l;
+		case len l {
+		0 =>
+			mashlib->prprompt(0);
+		1 =>
+			mashlib->prompt = hd l;
+		2 =>
+			mashlib->prompt = hd l;
+			mashlib->contin = hd tl l;
+		* =>
+			e.usage("prompt [string]");
+		}
+	"quote" =>
+		l = tl l;
+		if (l != nil) {
+			out := e.outfile();
+			if (out == nil)
+				return;
+			f := 0;
+			while (l != nil) {
+				if (f)
+					out.putc(' ');
+				else
+					f = 1;
+				out.puts(mashlib->quote(hd l));
+				l = tl l;
+			}
+			out.putc('\n');
+			out.close();
+		}
+	"run" =>
+		if (!run(e, tl l))
+			e.usage("run [-] [-denx] file [arg ...]");
+	"status" =>
+		l = tl l;
+		if (l != nil)
+			status(e, l);
+		else
+			e.usage("status cmd [arg ...]");
+	"time" =>
+		l = tl l;
+		if (l != nil)
+			time(e, l);
+		else
+			e.usage("time cmd [arg ...]");
+	"whatis" =>
+		l = tl l;
+		if (l != nil) {
+			out := e.outfile();
+			if (out == nil)
+				return;
+			while (l != nil) {
+				whatis(e, out, hd l);
+				l = tl l;
+			}
+			out.close();
+		}
+	}
+}
+
+#
+#	Print a variable and its value.
+#
+prone(out: ref Iobuf, eq, s: string, v: list of string)
+{
+	out.puts(s);
+	out.putc(' ');
+	out.puts(eq);
+	if (v != mashlib->empty) {
+		do {
+			out.putc(' ');
+			out.puts(mashlib->quote(hd v));
+			v = tl v;
+		} while (v != nil);
+	}
+	out.puts(";\n");
+}
+
+#
+#	Print the contents of a symbol table.
+#
+prsymbs(out: ref Iobuf, t: ref Stab, eq: string)
+{
+	if (t == nil)
+		return;
+	for (l := t.all(); l != nil; l = tl l) {
+		s := hd l;
+		v := s.value;
+		if (v != nil)
+			prone(out, eq, s.name, v);
+	}
+}
+
+#
+#	Print variables, functions and builtins.
+#
+whatis(e: ref Env, out: ref Iobuf, s: string)
+{
+	f := 0;
+	v := e.global.find(s);
+	if (v != nil) {
+		if (v.value != nil)
+			prone(out, "=", s, v.value);
+		if (v.func != nil) {
+			out.puts("fn ");
+			out.puts(s);
+			out.puts(" { ");
+			out.puts(v.func.text());
+			out.puts(" };\n");
+		}
+		if (v.builtin != nil) {
+			out.puts("load ");
+			out.puts(v.builtin->name());
+			out.puts("; ");
+			out.puts(s);
+			out.puts(";\n");
+		}
+		f = 1;
+	}
+	if (e.local != nil) {
+		v = e.local.find(s);
+		if (v != nil) {
+			prone(out, ":=", s, v.value);
+			f = 1;
+		}
+	}
+	if (!f) {
+		out.puts(s);
+		out.puts(": not found\n");
+	}
+}
+
+#
+#	Catenate arguments and interpret as mash input.
+#
+eval(e: ref Env, l: list of string)
+{
+	s: string;
+	while (l != nil) {
+		s = s + " " + hd l;
+		l = tl l;
+	}
+	e = e.copy();
+	e.flags &= ~mashlib->EInter;
+	e.sopen(s);
+	mashlib->parse->parse(e);
+}
+
+#
+#	Interpret file as mash input.
+#
+run(e: ref Env, l: list of string): int
+{
+	f := 0;
+	if (l == nil)
+		return 0;
+	e = e.copy();
+	s := hd l;
+	while (s[0] == '-') {
+		if (s == "-")
+			f = 1;
+		else {
+			for (i := 1; i < len s; i++) {
+				case s[i] {
+				'd' =>
+					e.flags |= mashlib->EDumping;
+				'e' =>
+					e.flags |= mashlib->ERaise;
+				'n' =>
+					e.flags |= mashlib->ENoxeq;
+				'x' =>
+					e.flags |= mashlib->EEcho;
+				* =>
+					return 0;
+				}
+			}
+		}
+		l = tl l;
+		if (l == nil)
+			return 0;
+		s = hd l;
+	}
+	fd := sys->open(s, Sys->OREAD);
+	if (fd == nil) {
+		err := mashlib->errstr();
+		if (mashlib->nonexistent(err) && s[0] != '/' && s[0:2] != "./") {
+			fd = sys->open(mashlib->LIB + s, Sys->OREAD);
+			if (fd == nil)
+				err = mashlib->errstr();
+			else
+				s = mashlib->LIB + s;
+		}
+		if (fd == nil) {
+			if (!f)
+				e.report(s + ": " + err);
+			return 1;
+		}
+	}
+	e.local = Stab.new();
+	e.local.assign(mashlib->ARGS, tl l);
+	e.flags &= ~mashlib->EInter;
+	e.fopen(fd, s);
+	mashlib->parse->parse(e);
+	return 1;
+}
+
+#
+#	Run a command and report true on no error output.
+#
+status(e: ref Env, l: list of string)
+{
+	in := child(e, l);
+	if (in == nil)
+		return;
+	b := array[256] of byte;
+	n := sys->read(in, b, len b);
+	if (n != 0) {
+		while (n > 0)
+			n = sys->read(in, b, len b);
+		if (n < 0)
+			e.couldnot("read", "pipe");
+	} else
+		e.output(Mashlib->TRUE);
+}
+
+#
+#	Status env child.
+#
+child(e: ref Env, l: list of string): ref Sys->FD
+{
+	e = e.copy();
+	fds := e.pipe();
+	if (fds == nil)
+		return nil;
+	if (sys->dup(fds[0].fd, 2) < 0) {
+		e.couldnot("dup", "pipe");
+		return nil;
+	}
+	t := e.stderr;
+	e.stderr = fds[0];
+	e.runit(l, nil, nil, 0);
+	e.stderr = t;
+	sys->dup(t.fd, 2);
+	return fds[1];
+}
+
+#
+#	Time the execution of a command.
+#
+time(e: ref Env, l: list of string)
+{
+	t1 := sys->millisec();
+	e.runit(l, nil, nil, 1);
+	t2 := sys->millisec();
+	sys->fprint(e.stderr, "%.4g\n", real (t2 - t1) / 1000.0);
+}
--- /dev/null
+++ b/appl/cmd/mash/depends.b
@@ -1,0 +1,228 @@
+#
+#	Dependency/rule routines.
+#
+
+DHASH:	con 127;	# dephash size
+
+#
+#	Initialize.  "make -clear" calls this.
+#
+initdep()
+{
+	dephash = array[DHASH] of list of ref Target;
+	rules = nil;
+}
+
+#
+#	Lookup a target in dephash, maybe add it.
+#
+target(s: string, insert: int): ref Target
+{
+	h := hash->fun1(s, DHASH);
+	l := dephash[h];
+	while (l != nil) {
+		if ((hd l).target == s)
+			return hd l;
+		l = tl l;
+	}
+	if (!insert)
+		return nil;
+	t := ref Target(s, nil);
+	dephash[h] = t :: dephash[h];
+	return t;
+}
+
+adddep(s: string, d: ref Depend)
+{
+	t := target(s, 1);
+	t.depends = d :: t.depends;
+}
+
+#
+#	Dependency (:) command.
+#	Evaluate lhs and rhs, make dependency, and add to the targets.
+#
+Cmd.depend(c: self ref Cmd, e: ref Env)
+{
+	if ((e.flags & ETop) == 0) {
+		e.report("dependency not at top level");
+		return;
+	}
+	if (dephash == nil)
+		initdep();
+	w := pass1(e, c.words);
+	if (w == nil)
+		return;
+	l := pass2(e, w);
+	if (l == nil)
+		return;
+	r: list of string;
+	if (c.left.words != nil) {
+		w = pass1(e, c.left.words);
+		if (w == nil)
+			return;
+		r = pass2(e, w);
+		if (r == nil)
+			return;
+	}
+	d := ref Depend(l, r, c.left.op, c.left.left, 0);
+	while (l != nil) {
+		adddep(hd l, d);
+		l = tl l;
+	}
+}
+
+#
+#	Evaluate rule lhs and break into path components.
+#
+rulelhs(e: ref Env, i: ref Item): ref Lhs
+{
+	i = i.ieval1(e);
+	if (i == nil)
+		return nil;
+	(s, l, nil) := i.ieval2(e);
+	if (l != nil) {
+		e.report("rule pattern evaluates to a list");
+		return nil;
+	}
+	if (s == nil) {
+		e.report("rule pattern evaluates to nil");
+		return nil;
+	}
+	(n, p) := sys->tokenize(s, "/");
+	return ref Lhs(s, p, n);
+}
+
+#
+#	Rule (:~) command.
+#	First pass of rhs evaluation is done here.
+#
+Cmd.rule(c: self ref Cmd, e: ref Env)
+{
+	if (e.flags & ETop) {
+		l := rulelhs(e, c.item);
+		if (l == nil)
+			return;
+		r := c.left.item.ieval1(e);
+		if (r == nil)
+			return;
+		rules = ref Rule(l, r, c.left.op, c.left.left) :: rules;
+	} else
+		e.report("rule not at top level");
+}
+
+Target.find(s: string): ref Target
+{
+	if (dephash == nil)
+		return nil;
+	return target(s, 0);
+}
+
+#
+#	Match a path element.
+#
+matchelem(p, s: string): int
+{
+	m := len p;
+	n := len s;
+	if (m == n && p == s)
+		return 1;
+	for (i := 0; i < m; i++) {
+		if (p[i] == '*') {
+			j := i + 1;
+			if (j == m)
+				return 1;
+			q := p[j:];
+			do {
+				if (matchelem(q, s[i:]))
+					return 1;
+			} while (++i < n);
+			return 0;
+		} else if (i >= n || p[i] != s[i])
+			return 0;
+	}
+	return 0;
+}
+
+#
+#	Match a path element and return a list of sub-matches.
+#
+matches(p, s: string): (int, list of string)
+{
+	m := len p;
+	n := len s;
+	for (i := 0; i < m; i++) {
+		if (p[i] == '*') {
+			j := i + 1;
+			if (j == m)
+				return (1, s[i:] :: nil);
+			q := p[j:];
+			do {
+				(r, l) := matches(q, s[i:]);
+				if (r)
+					return (1, s[j - 1: i] :: l);
+			} while (++i < n);
+			return (0, nil);
+		} else if (i >= n || p[i] != s[i])
+			return (0, nil);
+	}
+	return (m == n, nil);
+}
+
+#
+#	Rule match.
+#
+Rule.match(r: self ref Rule, a, n: int, t: list of string): int
+{
+	l := r.lhs;
+	if (l.count != n || (l.text[0] == '/') != a)
+		return 0;
+	for (e := l.elems; e != nil; e = tl e) {
+		if (!matchelem(hd e, hd t))
+			return 0;
+		t = tl t;
+	}
+	return 1;
+}
+
+#
+#	Rule match with array of sub-matches.
+#
+Rule.matches(r: self ref Rule, t: list of string): array of string
+{
+	m: list of list of string;
+	c := 1;
+	for (e := r.lhs.elems; e != nil; e = tl e) {
+		(x, l) := matches(hd e, hd t);
+		if (!x)
+			return nil;
+		if (l != nil) {
+			c += len l;
+			m = revstrs(l) :: m;
+		}
+		t = tl t;
+	}
+	a := array[c] of string;
+	while (m != nil) {
+		for (l := hd m; l != nil; l = tl l)
+			a[--c] = hd l;
+		m = tl m;
+	}
+	return a;
+}
+
+#
+#	Return list of rules that match a string.
+#
+rulematch(s: string): list of ref Rule
+{
+	m: list of ref Rule;
+	a := s[0] == '/';
+	(n, t) := sys->tokenize(s, "/");
+	for (l := rules; l != nil; l = tl l) {
+		r := hd l;
+		if (r.match(a, n, t))
+			m = r :: m;
+	}
+	return m;
+}
--- /dev/null
+++ b/appl/cmd/mash/dump.b
@@ -1,0 +1,199 @@
+#
+#	Output routines.
+#
+
+#
+#	Echo list of strings.
+#
+echo(e: ref Env, s: list of string)
+{
+	out := e.outfile();
+	if (out == nil)
+		return;
+	out.putc('+');
+	for (t := s; t != nil; t = tl t) {
+		out.putc(' ');
+		out.puts(hd t);
+	}
+	out.putc('\n');
+	out.close();
+}
+
+#
+#	Return text representation of Word/Item/Cmd.
+#
+
+Word.word(w: self ref Word, d: string): string
+{
+	if (w == nil)
+		return nil;
+	if (d != nil)
+		return d + w.text;
+	if (w.flags & Wquoted)
+		return enquote(w.text);
+	return w.text;
+}
+
+Item.text(i: self ref Item): string
+{
+	if (i == nil)
+		return nil;
+	case i.op {
+	Icaret =>
+		return i.left.text() + " ^ " + i.right.text();
+	Iicaret =>
+		return i.left.text() + i.right.text();
+	Idollarq =>
+		return i.word.word("$\"");
+	Idollar or Imatch =>
+		return i.word.word("$");
+	Iword =>
+		return i.word.word(nil);
+	Iexpr =>
+		return "(" + i.cmd.text() + ")";
+	Ibackq =>
+		return "`" + group(i.cmd);
+	Iquote =>
+		return "\"" + group(i.cmd);
+	Iinpipe =>
+		return "<" + group(i.cmd);
+	Ioutpipe =>
+		return ">" + group(i.cmd);
+	* =>
+		return "?" + string i.op;
+	}
+}
+
+words(l: list of ref Item): string
+{
+	s: string;
+	while (l != nil) {
+		if (s == nil)
+			s = (hd l).text();
+		else
+			s = s + " " + (hd l).text();
+		l = tl l;
+	}
+	return s;
+}
+
+redir(s: string, c: ref Cmd): string
+{
+	if (c == nil)
+		return s;
+	for (l := c.redirs; l != nil; l = tl l) {
+		r := hd l;
+		s = s + " " + rdsymbs[r.op] + " " + r.word.text();
+	}
+	return s;
+}
+
+cmd2in(c: ref Cmd, s: string): string
+{
+	return c.left.text() + " " + s + " " + c.right.text();
+}
+
+group(c: ref Cmd): string
+{
+	if (c == nil)
+		return "{ }";
+	return redir("{ " + c.text() + " }", c);
+}
+
+sequence(c: ref Cmd): string
+{
+	s: string;
+	do {
+		r := c.right;
+		t := ";";
+		if (r.op == Casync) {
+			r = r.left;
+			t = "&";
+		}
+		if (s == nil)
+			s = r.text() + t;
+		else
+			s = r.text() + t + " " + s;
+		c = c.left;
+	} while (c != nil);
+	return s;
+}
+
+Cmd.text(c: self ref Cmd): string
+{
+	if (c == nil)
+		return nil;
+	case c.op {
+	Csimple =>
+		return redir(words(c.words), c);
+	Cseq =>
+		return sequence(c);
+	Cfor =>
+		return "for (" + c.item.text() + " in " + words(c.words) + ") " + c.left.text();
+	Cif =>
+		return "if (" + c.left.text() +") " + c.right.text();
+	Celse =>
+		return c.left.text() +" else " + c.right.text();
+	Cwhile =>
+		return "while (" + c.left.text() +") " + c.right.text();
+	Ccase =>
+		return redir("case " + c.left.text() + " { " + c.right.text() + "}", c);
+	Ccases =>
+		s := c.left.text();
+		if (s[len s - 1] != '&')
+			return s + "; " + c.right.text();
+		return s + " " + c.right.text();
+	Cmatched =>
+		return cmd2in(c, "=>");
+	Cdefeq =>
+		return c.item.text() + " := " + words(c.words);
+	Ceq =>
+		return c.item.text() + " = " + words(c.words);
+	Cfn =>
+		return "fn " + c.item.text() + " " + group(c.left);
+	Crescue =>
+		return "rescue " + c.item.text() + " " + group(c.left);
+	Casync =>
+		return c.left.text() + "&";
+	Cgroup =>
+		return group(c.left);
+	Clistgroup =>
+		return ":" + group(c.left);
+	Csubgroup =>
+		return "@" + group(c.left);
+	Cnop =>
+		return nil;
+	Cword =>
+		return c.item.text();
+	Ccaret =>
+		return cmd2in(c, "^");
+	Chd =>
+		return "hd " + c.left.text();
+	Clen =>
+		return "len " + c.left.text();
+	Cnot =>
+		return "!" + c.left.text();
+	Ctl =>
+		return "tl " + c.left.text();
+	Ccons =>
+		return cmd2in(c, "::");
+	Ceqeq =>
+		return cmd2in(c, "==");
+	Cnoteq =>
+		return cmd2in(c, "!=");
+	Cmatch =>
+		return cmd2in(c, "~");
+	Cpipe =>
+		return cmd2in(c, "|");
+	Cdepend =>
+		return words(c.words) + " : " + words(c.left.words) + " " + c.left.text();
+	Crule =>
+		return c.item.text() + " :~ " + c.left.item.text() + " " + c.left.text();
+	* =>
+		if (c.op >= Cprivate)
+			return "Priv+" + string (c.op - Cprivate);
+		else
+			return "?" + string c.op;
+	}
+	return nil;
+}
--- /dev/null
+++ b/appl/cmd/mash/exec.b
@@ -1,0 +1,401 @@
+#
+#	Manage the execution of a command.
+#
+
+srv:	string;		# srv file proto
+nsrv:	int = 0;	# srv file unique id
+
+#
+#	Return error string.
+#
+errstr(): string
+{
+	return sys->sprint("%r");
+}
+
+#
+#	Server thread for servefd.
+#
+server(c: ref Sys->FileIO, fd: ref Sys->FD, write: int)
+{
+	a: array of byte;
+	if (!write)
+		a = array[Sys->ATOMICIO] of byte;
+	for (;;) {
+		alt {
+		(nil, b, nil, wc) := <- c.write =>
+			if (wc == nil)
+				return;
+			if (!write) {
+				wc <- = (0, EPIPE);
+				return;
+			}
+			r := sys->write(fd, b, len b);
+			if (r < 0) {
+				wc <- = (0, errstr());
+				return;
+			}
+			wc <- = (r, nil);
+		(nil, n, nil, rc) := <- c.read =>
+			if (rc == nil)
+				return;
+			if (write) {
+				rc <- = (array[0] of byte, nil);
+				return;
+			}
+			if (n > Sys->ATOMICIO)
+				n = Sys->ATOMICIO;
+			r := sys->read(fd, a, n);
+			if (r < 0) {
+				rc <- = (nil, errstr());
+				return;
+			}
+			rc <- = (a[0:r], nil);
+		}
+	}
+}
+
+#
+#	Serve FD as a #s file.  Used to implement generators.
+#
+Env.servefd(e: self ref Env, fd: ref Sys->FD, write: int): string
+{
+	(s, c) := e.servefile(nil);
+	spawn server(c, fd, write);
+	return s;
+}
+
+#
+#	Generate name and FileIO adt for a served filed.
+#
+Env.servefile(e: self ref Env, n: string): (string, ref Sys->FileIO)
+{
+	c: ref Sys->FileIO;
+	s: string;
+	if (srv == nil) {
+		(ok, d) := sys->stat(CHAN);
+		if (ok < 0)
+			e.couldnot("stat", CHAN);
+		if (d.dtype != 's') {
+			if (sys->bind("#s", CHAN, Sys->MBEFORE) < 0)
+				e.couldnot("bind", CHAN);
+		}
+		srv = "mash." + string sys->pctl(0, nil);
+	}
+	retry := 0;
+	for (;;) {
+		if (retry || n == nil)
+			s = srv + "." + string nsrv++;
+		else
+			s = n;
+		c = sys->file2chan(CHAN, s);
+		s = CHAN + "/" + s;
+		if (c == nil) {
+			if (retry || n == nil || errstr() != EEXISTS)
+				e.couldnot("file2chan", s);
+			retry = 1;
+			continue;
+		}
+		break;
+	}
+	if (n != nil)
+		n = CHAN + "/" + n;
+	else
+		n = s;
+	if (retry && sys->bind(s, n, Sys->MREPL) < 0)
+		e.couldnot("bind", n);
+	return (n, c);
+}
+
+#
+#	Shorthand for string output.
+#
+Env.output(e: self ref Env, s: string)
+{
+	if (s == nil)
+		return;
+	out := e.outfile();
+	if (out == nil)
+		return;
+	out.puts(s);
+	out.close();
+}
+
+#
+#	Return Iobuf for stdout.
+#
+Env.outfile(e: self ref Env): ref Bufio->Iobuf
+{
+	fd := e.out;
+	if (fd == nil)
+		fd = sys->fildes(1);
+	out := bufio->fopen(fd, Bufio->OWRITE);
+	if (out == nil)
+		e.report(sys->sprint("fopen failed: %r"));
+	return out;
+}
+
+#
+#	Return FD for /dev/null.
+#
+Env.devnull(e: self ref Env): ref Sys->FD
+{
+	fd := sys->open(DEVNULL, Sys->OREAD);
+	if (fd == nil)
+		e.couldnot("open", DEVNULL);
+	return fd;
+}
+
+#
+#	Make a pipe.
+#
+Env.pipe(e: self ref Env): array of ref Sys->FD
+{
+	fds := array[2] of ref Sys->FD;
+	if (sys->pipe(fds) < 0) {
+		e.report(sys->sprint("pipe failed: %r"));
+		return nil;
+	}
+	return fds;
+}
+
+#
+#	Open wait file for an env.
+#
+waitfd(e: ref Env)
+{
+	w := "#p/" + string sys->pctl(0, nil) + "/wait";
+	fd := sys->open(w, sys->OREAD);
+	if (fd == nil)
+		e.couldnot("open", w);
+	e.wait = fd;
+}
+
+#
+#	Wait for a thread.  Perhaps propagate exception or exit.
+#
+waitfor(e: ref Env, pid: int, wc: chan of int, ec, xc: chan of string)
+{
+	if (ec != nil || xc != nil) {
+		spawn waiter(e, pid, wc);
+		if (ec == nil)
+			ec = chan of string;
+		if (xc == nil)
+			xc = chan of string;
+		alt {
+		<-wc =>
+			return;
+		x := <-ec =>
+			<-wc;
+			exitmash();
+		x := <-xc =>
+			<-wc;
+			s := x;
+			if (len s < FAILLEN || s[0:FAILLEN] != FAIL)
+				s = FAIL + s;
+			raise s;
+		}
+	} else
+		waiter(e, pid, nil);
+}
+
+#
+#	Wait for a specific pid.
+#
+waiter(e: ref Env, pid: int, wc: chan of int)
+{
+	buf := array[sys->WAITLEN] of byte;
+	for(;;) {
+		n := sys->read(e.wait, buf, len buf);
+		if (n < 0) {
+			e.report(sys->sprint("read wait: %r\n"));
+			break;
+		}
+		status := string buf[0:n];
+		if (status[len status - 1] != ':')
+			sys->fprint(e.stderr, "%s\n", status);
+		who := int status;
+		if (who != 0 && who == pid)
+			break;
+	}
+	if (wc != nil)
+		wc <-= 0;
+}
+
+#
+#	Preparse IO for a new thread.
+#	Make a new FD group and redirect stdin/stdout.
+#
+prepareio(in, out: ref sys->FD): (int, ref Sys->FD)
+{
+	fds := list of { 0, 1, 2};
+	if (in != nil)
+		fds = in.fd :: fds;
+	if (out != nil)
+		fds = out.fd :: fds;
+	pid := sys->pctl(sys->NEWFD, fds);
+	console := sys->fildes(2);
+	if (in != nil) {
+		sys->dup(in.fd, 0);
+		in = nil;
+	}
+	if (out != nil) {
+		sys->dup(out.fd, 1);
+		out = nil;
+	}
+	return (pid, console);
+}
+
+#
+#	Add ".dis" to a command if missing.
+#
+dis(s: string): string
+{
+	if (len s < 4 || s[len s - 4:] != ".dis")
+		return s + ".dis";
+	return s;
+}
+
+#
+#	Load a builtin.
+#
+Env.doload(e: self ref Env, s: string)
+{
+	file := dis(s);
+	l := load Mashbuiltin file;
+	if (l == nil) {
+		err := errstr();
+		if (nonexistent(err) && file[0] != '/' && file[0:2] != "./") {
+			l = load Mashbuiltin LIB + file;
+			if (l == nil)
+				err = errstr();
+		}
+		if (l == nil) {
+			e.report(s + ": " + err);
+			return;
+		}
+	}
+	l->mashinit("load" :: s :: nil, lib, l, e);
+}
+
+#
+#	Execute a spawned thread (dis module or builtin).
+#
+mkprog(args: list of string, e: ref Env, in, out: ref Sys->FD, wc: chan of int, ec, xc: chan of string)
+{
+	(pid, console) := prepareio(in, out);
+	wc <-= pid;
+	if (pid < 0)
+		return;
+	cmd := hd args;
+	{
+		b := e.builtin(cmd);
+		if (b != nil) {
+			e = e.copy();
+			e.in = in;
+			e.out = out;
+			e.stderr = console;
+			e.wait = nil;
+			b->mashcmd(e, args);
+		} else {
+			file := dis(cmd);
+			c := load Command file;
+			if (c == nil) {
+				err := errstr();
+				if (nonexistent(err) && file[0] != '/' && file[0:2] != "./") {
+					c = load Command "/dis/" + file;
+					if (c == nil)
+						err = errstr();
+				}
+				if (c == nil) {
+					sys->fprint(console, "%s: %s\n", file, err);
+					return;
+				}
+			}
+			c->init(gctxt, args);
+		}
+	}exception x{
+	FAILPAT =>
+		if (xc != nil)
+			xc <-= x;
+		# the command failure should be propagated silently to
+		# a higher level, where $status can be set.. - wrtp.
+		#else
+		#	sys->fprint(console, "%s: %s\n", cmd, x.name);
+		exit;
+	EPIPE =>
+		if (xc != nil)
+			xc <-= x;
+		#else
+		#	sys->fprint(console, "%s: %s\n", cmd, x.name);
+		exit;
+	EXIT =>
+		if (ec != nil)
+			ec <-= x;
+		exit;
+	}
+}
+
+#
+#	Open/create files for redirection.
+#
+redirect(e: ref Env, f: array of string, in, out: ref Sys->FD): (int, ref Sys->FD, ref Sys->FD)
+{
+	s: string;
+	err := 0;
+	if (f[Rinout] != nil) {
+		s = f[Rinout];
+		in = sys->open(s, Sys->ORDWR);
+		if (in == nil) {
+			sys->fprint(e.stderr, "%s: %r\n", s);
+			err = 1;
+		}
+		out = in;
+	} else if (f[Rin] != nil) {
+		s = f[Rin];
+		in = sys->open(s, Sys->OREAD);
+		if (in == nil) {
+			sys->fprint(e.stderr, "%s: %r\n", s);
+			err = 1;
+		}
+	}
+	if (f[Rout] != nil || f[Rappend] != nil) {
+		if (f[Rappend] != nil) {
+			s = f[Rappend];
+			out = sys->open(s, Sys->OWRITE);
+			if (out != nil)
+				sys->seek(out, big 0, Sys->SEEKEND);
+		} else {
+			s = f[Rout];
+			out = nil;
+		}
+		if (out == nil) {
+			out = sys->create(s, Sys->OWRITE, 8r666);
+			if (out == nil) {
+				sys->fprint(e.stderr, "%s: %r\n", s);
+				err = 1;
+			}
+		}
+	}
+	if (err)
+		return (0, nil, nil);
+	return (1, in, out);
+}
+
+#
+#	Spawn a command and maybe wait for it.
+#
+exec(a: list of string, e: ref Env, infd, outfd: ref Sys->FD, wait: int)
+{
+	if (wait && e.wait == nil)
+		waitfd(e);
+	wc := chan of int;
+	if (wait && (e.flags & ERaise))
+		xc := chan of string;
+	if (wait && (e.flags & ETop))
+		ec := chan of string;
+	spawn mkprog(a, e, infd, outfd, wc, ec, xc);
+	pid := <-wc;
+	if (wait)
+		waitfor(e, pid, wc, ec, xc);
+}
--- /dev/null
+++ b/appl/cmd/mash/expr.b
@@ -1,0 +1,158 @@
+#
+#	Expression evaluation.
+#
+
+#
+#	Filename pattern matching.
+#
+glob(e: ref Env, s: string): (string, list of string)
+{
+	if (filepat == nil) {
+		filepat = load Filepat Filepat->PATH;
+		if (filepat == nil)
+			e.couldnot("load", Filepat->PATH);
+	}
+	l := filepat->expand(s);
+	if (l != nil)
+		return (nil, l);
+	return (s, nil);
+}
+
+#
+#	RE pattern matching.
+#
+match(s1, s2: string): int
+{
+	(re, nil) := regex->compile(s2, 0);
+	return regex->execute(re, s1) != nil;
+}
+
+#
+#	RE match of two lists.  Two non-singleton lists never match.
+#
+match2(e: ref Env, s1: string, l1: list of string, s2: string, l2: list of string): int
+{
+	if (regex == nil) {
+		regex = load Regex Regex->PATH;
+		if (regex == nil)
+			e.couldnot("load", Regex->PATH);
+	}
+	if (s1 != nil) {
+		if (s2 != nil)
+			return match(s1, s2);
+		while (l2 != nil) {
+			if (match(s1, hd l2))
+				return 1;
+			l2 = tl l2;
+		}
+	} else if (l1 != nil) {
+		if (s2 == nil)
+			return 0;
+		while (l1 != nil) {
+			if (match(hd l1, s2))
+				return 1;
+			l1 = tl l1;
+		}
+	} else if (s2 != nil)
+		return match(nil, s2);
+	else if (l2 != nil) {
+		while (l2 != nil) {
+			if (match(nil, hd l2))
+				return 1;
+			l2 = tl l2;
+		}
+	} else
+		return 1;
+	return 0;
+}
+
+#
+#	Test list equality.  Same length and identical members.
+#
+eqlist(l1, l2: list of string): int
+{
+	while (l1 != nil && l2 != nil) {
+		if (hd l1 != hd l2)
+			return 0;
+		l1 = tl l1;
+		l2 = tl l2;
+	}
+	return l1 == nil && l2 == nil;
+}
+
+#
+#	Equality operator.
+#
+Cmd.evaleq(c: self ref Cmd, e: ref Env): int
+{
+	(s1, l1, nil) := c.left.eeval2(e);
+	(s2, l2, nil) := c.right.eeval2(e);
+	if (s1 != nil)
+		return s1 == s2;
+	if (l1 != nil)
+		return eqlist(l1, l2);
+	return s2 == nil && l2 == nil;
+}
+
+#
+#	Match operator.
+#
+Cmd.evalmatch(c: self ref Cmd, e: ref Env): int
+{
+	(s1, l1, nil) := c.left.eeval2(e);
+	(s2, l2, nil) := c.right.eeval2(e);
+	return match2(e, s1, l1, s2, l2);
+}
+
+#
+#	Catenation operator.
+#
+Item.caret(i: self ref Item, e: ref Env): (string, list of string, int)
+{
+	(s1, l1, x1) := i.left.ieval2(e);
+	(s2, l2, x2) := i.right.ieval2(e);
+	return caret(s1, l1, x1, s2, l2, x2);
+}
+
+#
+#	Caret of lists.  A singleton distributes.  Otherwise pairwise, padded with nils.
+#
+caret(s1: string, l1: list of string, x1: int, s2: string, l2: list of string, x2: int): (string, list of string, int)
+{
+	l: list of string;
+	if (s1 != nil) {
+		if (s2 != nil)
+			return (s1 + s2, nil, x1 | x2);
+		if (l2 == nil)
+			return (s1, nil, x1);
+		while (l2 != nil) {
+			l = (s1 + hd l2) :: l;
+			l2 = tl l2;
+		}
+	} else if (s2 != nil) {
+		if (l1 == nil)
+			return (s2, nil, x2);
+		while (l1 != nil) {
+			l = (hd l1 + s2) :: l;
+			l1 = tl l1;
+		}
+	} else if (l1 != nil) {
+		if (l2 == nil)
+			return (nil, l1, 0);
+		while (l1 != nil || l2 != nil) {
+			if (l1 != nil) {
+				s1 = hd l1;
+				l1 = tl l1;
+			} else
+				s1 = nil;
+			if (l2 != nil) {
+				s2 = hd l2;
+				l2 = tl l2;
+			} else
+				s2 = nil;
+			l = (s1 + s2) :: l;
+		}
+	} else if (l2 != nil)
+		return (nil, l2, 0);
+	return (nil, revstrs(l), 0);
+}
--- /dev/null
+++ b/appl/cmd/mash/eyacc.b
@@ -1,0 +1,2785 @@
+implement Yacc;
+
+include "sys.m";
+	sys: Sys;
+	print, fprint, sprint: import sys;
+	UTFmax: import Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "draw.m";
+
+Yacc: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Arg: adt
+{
+	argv:	list of string;
+	c:	int;
+	opts:	string;
+
+	init:	fn(argv: list of string): ref Arg;
+	opt:	fn(arg: self ref Arg): int;
+	arg:	fn(arg: self ref Arg): string;
+};
+
+PARSER:		con "./eyaccpar";
+OFILE:		con "tab.b";
+FILEU:		con "output";
+FILED:		con "tab.m";
+FILEDEBUG:	con "debug";
+
+# the following are adjustable
+# according to memory size
+ACTSIZE:	con 30000;
+NSTATES:	con 2000;
+TEMPSIZE:	con 2000;
+
+SYMINC:		con 50;				# increase for non-term or term
+RULEINC:	con 50;				# increase for max rule length	prodptr[i]
+PRODINC:	con 100;			# increase for productions	prodptr
+WSETINC:	con 50;				# increase for working sets	wsets
+STATEINC:	con 200;			# increase for states		statemem
+
+NAMESIZE:	con 50;
+NTYPES:		con 63;
+ISIZE:		con 400;
+
+PRIVATE:	con 16rE000;			# unicode private use
+
+# relationships which must hold:
+#	TEMPSIZE >= NTERMS + NNONTERM + 1
+#	TEMPSIZE >= NSTATES
+#
+
+NTBASE:		con 8r10000;
+ERRCODE:	con 8190;
+ACCEPTCODE:	con 8191;
+YYLEXUNK:	con 3;
+TOKSTART:	con 4;				#index of first defined token
+
+# no, left, right, binary assoc.
+NOASC, LASC, RASC, BASC: con iota;
+
+# flags for state generation
+DONE, MUSTDO, MUSTLOOKAHEAD: con iota;
+
+# flags for a rule having an action, and being reduced
+ACTFLAG:	con 16r4;
+REDFLAG:	con 16r8;
+
+# output parser flags
+YYFLAG1:	con -1000;
+
+# parse tokens
+IDENTIFIER, MARK, TERM, LEFT, RIGHT, BINARY, PREC, LCURLY, IDENTCOLON, NUMBER, START, TYPEDEF, TYPENAME, MODULE: con PRIVATE+iota;
+
+ENDFILE:	con 0;
+
+EMPTY:		con 1;
+WHOKNOWS:	con 0;
+OK:		con 1;
+NOMORE:		con -1000;
+
+# macros for getting associativity and precedence levels
+ASSOC(i: int): int
+{
+	return i & 3;
+}
+
+PLEVEL(i: int): int
+{
+	return (i >> 4) & 16r3f;
+}
+
+TYPE(i: int): int
+{
+	return (i >> 10) & 16r3f;
+}
+
+# macros for setting associativity and precedence levels
+SETASC(i, j: int): int
+{
+	return i | j;
+}
+
+SETPLEV(i, j: int): int
+{
+	return i | (j << 4);
+}
+
+SETTYPE(i, j: int): int
+{
+	return i | (j << 10);
+}
+
+# I/O descriptors
+stderr:		ref Sys->FD;
+fdefine:	ref Iobuf;			# file for module definition
+fdebug:		ref Iobuf;			# y.debug for strings for debugging
+ftable:		ref Iobuf;			# y.tab.c file
+finput:		ref Iobuf;			# input file
+foutput:	ref Iobuf;			# y.output file
+
+CodeData, CodeMod, CodeAct: con iota;
+NCode:	con 8192;
+
+Code: adt
+{
+	kind:	int;
+	data:	array of byte;
+	ndata:	int;
+	next:	cyclic ref Code;
+};
+
+codehead:	ref Code;
+codetail:	ref Code;
+
+modname:	string;				# name of module
+
+# communication variables between various I/O routines
+infile:		string;				# input file name
+numbval:	int;				# value of an input number
+tokname:	string;				# input token name, slop for runes and 0
+
+# structure declarations
+Lkset: type array of int;
+
+Pitem: adt
+{
+	prod:	array of int;
+	off:	int;				# offset within the production
+	first:	int;				# first term or non-term in item
+	prodno:	int;				# production number for sorting
+};
+
+Item: adt
+{
+	pitem:	Pitem;
+	look:	Lkset;
+};
+
+Symb: adt
+{
+	name:	string;
+	value:	int;
+};
+
+Wset: adt
+{
+	pitem:	Pitem;
+	flag:	int;
+	ws:	Lkset;
+};
+
+	# storage of names
+
+parser :=	PARSER;
+yydebug:	string;
+
+	# storage of types
+ntypes:		int;				# number of types defined
+typeset :=	array[NTYPES] of string;	# pointers to type tags
+
+	# token information
+
+ntokens :=	0;				# number of tokens
+tokset:		array of Symb;
+toklev:		array of int;			# vector with the precedence of the terminals
+
+	# nonterminal information
+
+nnonter :=	-1;				# the number of nonterminals
+nontrst:	array of Symb;
+start:		int;				# start symbol
+
+	# state information
+
+nstate := 	0;				# number of states
+pstate :=	array[NSTATES+2] of int;	# index into statemem to the descriptions of the states
+statemem :	array of Item;
+tystate :=	array[NSTATES] of int;		# contains type information about the states
+tstates :	array of int;			# states generated by terminal gotos
+ntstates :	array of int; 			# states generated by nonterminal gotos
+mstates :=	array[NSTATES] of {* => 0};	# chain of overflows of term/nonterm generation lists
+lastred: 	int; 				# number of last reduction of a state
+defact :=	array[NSTATES] of int;		# default actions of states
+
+	# lookahead set information
+
+lkst: array of Lkset;
+nolook := 0;					# flag to turn off lookahead computations
+tbitset := 0;					# size of lookahead sets
+clset: Lkset;  					# temporary storage for lookahead computations
+
+	# working set information
+
+wsets:	array of Wset;
+cwp:	int;
+
+	# storage for action table
+
+amem:	array of int;				# action table storage
+memp:	int;					# next free action table position
+indgo := array[NSTATES] of int;			# index to the stored goto table
+
+	# temporary vector, indexable by states, terms, or ntokens
+
+temp1 :=	array[TEMPSIZE] of int;		# temporary storage, indexed by terms + ntokens or states
+lineno :=	1;				# current input line number
+fatfl :=	1;  				# if on, error is fatal
+nerrors :=	0;				# number of errors
+
+	# assigned token type values
+extval :=	0;
+
+ytabc :=	OFILE;	# name of y.tab.c
+
+	# grammar rule information
+
+nprod := 1;					# number of productions
+prdptr: array of array of int;			# pointers to descriptions of productions
+levprd: array of int;				# precedence levels for the productions
+rlines: array of int;				# line number for this rule
+
+
+	# statistics collection variables
+
+zzgoent := 0;
+zzgobest := 0;
+zzacent := 0;
+zzexcp := 0;
+zzclose := 0;
+zzrrconf := 0;
+zzsrconf := 0;
+zzstate := 0;
+
+	# optimizer arrays
+yypgo:	array of array of int;
+optst:	array of array of int;
+ggreed:	array of int;
+pgo:	array of int;
+
+maxspr: int;  		# maximum spread of any entry
+maxoff: int;  		# maximum offset into a array
+maxa:	int;
+
+	# storage for information about the nonterminals
+
+pres: array of array of array of int;		# vector of pointers to productions yielding each nonterminal
+pfirst: array of Lkset;
+pempty:	array of int;				# vector of nonterminals nontrivially deriving e
+	# random stuff picked out from between functions
+
+indebug	:= 0;		# debugging flag for cpfir
+pidebug	:= 0;		# debugging flag for putitem
+gsdebug	:= 0;		# debugging flag for stagen
+cldebug	:= 0;		# debugging flag for closure
+pkdebug	:= 0;		# debugging flag for apack
+g2debug	:= 0;		# debugging for go2gen
+adb	:= 0;		# debugging for callopt
+
+Resrv : adt
+{
+	name:	string;
+	value:	int;
+};
+
+resrv := array[] of {
+	Resrv("binary",		BINARY),
+	Resrv("module",		MODULE),
+	Resrv("left",		LEFT),
+	Resrv("nonassoc",	BINARY),
+	Resrv("prec",		PREC),
+	Resrv("right",		RIGHT),
+	Resrv("start",		START),
+	Resrv("term",		TERM),
+	Resrv("token",		TERM),
+	Resrv("type",		TYPEDEF),};
+
+zznewstate := 0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	stderr = sys->fildes(2);
+
+	setup(argv);		# initialize and read productions
+
+	tbitset = (ntokens+32)/32;
+	cpres();		# make table of which productions yield a given nonterminal
+	cempty();		# make a table of which nonterminals can match the empty string
+	cpfir();		# make a table of firsts of nonterminals
+
+	stagen();		# generate the states
+
+	yypgo = array[nnonter+1] of array of int;
+	optst = array[nstate] of array of int;
+	output();		# write the states and the tables
+	go2out();
+
+	hideprod();
+	summary();
+
+	callopt();
+
+	others();
+
+	bufio->flush();
+}
+
+setup(argv: list of string)
+{
+	j, ty: int;
+
+	ytab := 0;
+	vflag := 0;
+	dflag := 0;
+	stem := 0;
+	stemc := "y";
+	foutput = nil;
+	fdefine = nil;
+	fdebug = nil;
+	arg := Arg.init(argv);
+	while(c := arg.opt()){
+		case c{
+		'v' or 'V' =>
+			vflag++;
+		'D' =>
+			yydebug = arg.arg();
+		'd' =>
+			dflag++;
+		'o' =>
+			ytab++;
+			ytabc = arg.arg();
+		's' =>
+			stem++;
+			stemc = arg.arg();
+		* =>
+			usage();
+		}
+	}
+	argv = arg.argv;
+	if(len argv != 1)
+		usage();
+	infile = hd argv;
+	finput = bufio->open(infile, Bufio->OREAD);
+	if(finput == nil)
+		error("cannot open '"+infile+"'");
+
+	openup(stemc, dflag, vflag, ytab, ytabc);
+
+	defin(0, "$end");
+	extval = PRIVATE;	# tokens start in unicode 'private use'
+	defin(0, "error");
+	defin(1, "$accept");
+	defin(0, "$unk");
+	i := 0;
+
+	for(t := gettok(); t != MARK && t != ENDFILE; )
+	case t {
+	';' =>
+		t = gettok();
+
+	START =>
+		if(gettok() != IDENTIFIER)
+			error("bad %%start construction");
+		start = chfind(1, tokname);
+		t = gettok();
+
+	TYPEDEF =>
+		if(gettok() != TYPENAME)
+			error("bad syntax in %%type");
+		ty = numbval;
+		for(;;) {
+			t = gettok();
+			case t {
+			IDENTIFIER =>
+				if((t=chfind(1, tokname)) < NTBASE) {
+					j = TYPE(toklev[t]);
+					if(j != 0 && j != ty)
+						error("type redeclaration of token "+
+							tokset[t].name);
+					else
+						toklev[t] = SETTYPE(toklev[t], ty);
+				} else {
+					j = nontrst[t-NTBASE].value;
+					if(j != 0 && j != ty)
+						error("type redeclaration of nonterminal "+
+							nontrst[t-NTBASE].name);
+					else
+						nontrst[t-NTBASE].value = ty;
+				}
+				continue;
+			',' =>
+				continue;
+			';' =>
+				t = gettok();
+			}
+			break;
+		}
+
+	MODULE =>
+		cpymodule();
+		t = gettok();
+
+	LEFT or BINARY or RIGHT or TERM =>
+		# nonzero means new prec. and assoc.
+		lev := t-TERM;
+		if(lev)
+			i++;
+		ty = 0;
+
+		# get identifiers so defined
+		t = gettok();
+
+		# there is a type defined
+		if(t == TYPENAME) {
+			ty = numbval;
+			t = gettok();
+		}
+		for(;;) {
+			case t {
+			',' =>
+				t = gettok();
+				continue;
+
+			';' =>
+				break;
+
+			IDENTIFIER =>
+				j = chfind(0, tokname);
+				if(j >= NTBASE)
+					error(tokname+" defined earlier as nonterminal");
+				if(lev) {
+					if(ASSOC(toklev[j]))
+						error("redeclaration of precedence of "+tokname);
+					toklev[j] = SETASC(toklev[j], lev);
+					toklev[j] = SETPLEV(toklev[j], i);
+				}
+				if(ty) {
+					if(TYPE(toklev[j]))
+						error("redeclaration of type of "+tokname);
+					toklev[j] = SETTYPE(toklev[j],ty);
+				}
+				t = gettok();
+				if(t == NUMBER) {
+					tokset[j].value = numbval;
+					t = gettok();
+				}
+				continue;
+			}
+			break;
+		}
+
+	LCURLY =>
+		cpycode();
+		t = gettok();
+
+	* =>
+		error("syntax error");
+	}
+	if(t == ENDFILE)
+		error("unexpected EOF before %%");
+	if(modname == nil)
+		error("missing %module specification");
+
+	moreprod();
+	prdptr[0] = array[4] of {
+		NTBASE,		# added production
+		start,		# if start is 0, we will overwrite with the lhs of the first rule
+		1,
+		0
+	};
+	nprod = 1;
+	curprod := array[RULEINC] of int;
+	t = gettok();
+	if(t != IDENTCOLON)
+		error("bad syntax on first rule");
+
+	if(!start)
+		prdptr[0][1] = chfind(1, tokname);
+
+	# read rules
+	# put into prdptr array in the format
+	# target
+	# followed by id's of terminals and non-terminals
+	# followd by -nprod
+	while(t != MARK && t != ENDFILE) {
+		mem := 0;
+		# process a rule
+		rlines[nprod] = lineno;
+		if(t == '|')
+			curprod[mem++] = prdptr[nprod-1][0];
+		else if(t == IDENTCOLON) {
+			curprod[mem] = chfind(1, tokname);
+			if(curprod[mem] < NTBASE)
+				error("token illegal on LHS of grammar rule");
+			mem++;
+		} else
+			error("illegal rule: missing semicolon or | ?");
+
+		# read rule body
+		t = gettok();
+
+		for(;;){
+			while(t == IDENTIFIER) {
+				curprod[mem] = chfind(1, tokname);
+				if(curprod[mem] < NTBASE)
+					levprd[nprod] = toklev[curprod[mem]];
+				mem++;
+				if(mem >= len curprod){
+					ncurprod := array[mem+RULEINC] of int;
+					ncurprod[0:] = curprod;
+					curprod = ncurprod;
+				}
+				t = gettok();
+			}
+			if(t == PREC) {
+				if(gettok() != IDENTIFIER)
+					error("illegal %%prec syntax");
+				j = chfind(2, tokname);
+				if(j >= NTBASE)
+					error("nonterminal "+nontrst[j-NTBASE].name+" illegal after %%prec");
+				levprd[nprod] = toklev[j];
+				t = gettok();
+			}
+			if(t != '=')
+				break;
+			levprd[nprod] |= ACTFLAG;
+			addcode(CodeAct, "\n"+string nprod+"=>");
+			cpyact(curprod, mem);
+
+			# action within rule...
+			if((t=gettok()) == IDENTIFIER) {
+				# make it a nonterminal
+				j = chfind(1, "$$"+string nprod);
+
+				#
+				# the current rule will become rule number nprod+1
+				# enter null production for action
+				#
+				prdptr[nprod] = array[2] of {j, -nprod};
+
+				# update the production information
+				nprod++;
+				moreprod();
+				levprd[nprod] = levprd[nprod-1] & ~ACTFLAG;
+				levprd[nprod-1] = ACTFLAG;
+				rlines[nprod] = lineno;
+
+				# make the action appear in the original rule
+				curprod[mem++] = j;
+				if(mem >= len curprod){
+					ncurprod := array[mem+RULEINC] of int;
+					ncurprod[0:] = curprod;
+					curprod = ncurprod;
+				}
+			}
+		}
+
+		while(t == ';')
+			t = gettok();
+		curprod[mem++] = -nprod;
+
+		# check that default action is reasonable
+		if(ntypes && !(levprd[nprod]&ACTFLAG) && nontrst[curprod[0]-NTBASE].value) {
+			# no explicit action, LHS has value
+
+			tempty := curprod[1];
+			if(tempty < 0)
+				error("must return a value, since LHS has a type");
+			else
+				if(tempty >= NTBASE)
+					tempty = nontrst[tempty-NTBASE].value;
+				else
+					tempty = TYPE(toklev[tempty]);
+			if(tempty != nontrst[curprod[0]-NTBASE].value)
+				error("default action causes potential type clash");
+			else{
+				addcodec(CodeAct, '\n');
+				addcode(CodeAct, string nprod);
+				addcode(CodeAct, "=>\ne.yyval.");
+				addcode(CodeAct, typeset[tempty]);
+				addcode(CodeAct, " = yys[yyp+1].yyv.");
+				addcode(CodeAct, typeset[tempty]);
+				addcodec(CodeAct, ';');
+			}
+		}
+		moreprod();
+		prdptr[nprod] = array[mem] of int;
+		prdptr[nprod][0:] = curprod[:mem];
+		nprod++;
+		moreprod();
+		levprd[nprod] = 0;
+	}
+
+	#
+	# end of all rules
+	# dump out the prefix code
+	#
+	ftable.puts("implement ");
+	ftable.puts(modname);
+	ftable.puts(";\n");
+
+	dumpcode(CodeMod);
+	dumpmod();
+	dumpcode(CodeAct);
+
+	ftable.puts("YYEOFCODE: con 1;\n");
+	ftable.puts("YYERRCODE: con 2;\n");
+	ftable.puts("YYMAXDEPTH: con 200;\n");	# was 150
+#	ftable.puts("yyval: YYSTYPE;\n");
+
+	#
+	# copy any postfix code
+	#
+	if(t == MARK) {
+		ftable.puts("\n#line\t");
+		ftable.puts(string lineno);
+		ftable.puts("\t\"");
+		ftable.puts(infile);
+		ftable.puts("\"\n");
+		while((c=finput.getc()) != Bufio->EOF)
+			ftable.putc(c);
+	}
+	finput.close();
+}
+
+#
+# allocate enough room to hold another production
+#
+moreprod()
+{
+	n := len prdptr;
+	if(nprod < n)
+		return;
+	n += PRODINC;
+	aprod := array[n] of array of int;
+	aprod[0:] = prdptr;
+	prdptr = aprod;
+
+	alevprd := array[n] of int;
+	alevprd[0:] = levprd;
+	levprd = alevprd;
+
+	arlines := array[n] of int;
+	arlines[0:] = rlines;
+	rlines = arlines;
+}
+
+#
+# define s to be a terminal if t=0
+# or a nonterminal if t=1
+#
+defin(nt: int, s: string): int
+{
+	val := 0;
+	if(nt) {
+		nnonter++;
+		if(nnonter >= len nontrst){
+			anontrst := array[nnonter + SYMINC] of Symb;
+			anontrst[0:] = nontrst;
+			nontrst = anontrst;
+		}
+		nontrst[nnonter] = Symb(s, 0);
+		return NTBASE + nnonter;
+	}
+
+	# must be a token
+	ntokens++;
+	if(ntokens >= len tokset){
+		atokset := array[ntokens + SYMINC] of Symb;
+		atokset[0:] = tokset;
+		tokset = atokset;
+
+		atoklev := array[ntokens + SYMINC] of int;
+		atoklev[0:] = toklev;
+		toklev = atoklev;
+	}
+	tokset[ntokens].name = s;
+	toklev[ntokens] = 0;
+
+	# establish value for token
+	# single character literal
+	if(s[0] == ' ' && len s == 1+1){
+		val = s[1];
+	}else if(s[0] == ' ' && s[1] == '\\') { # escape sequence
+		if(len s == 2+1) {
+			# single character escape sequence
+			case s[2] {
+			'\'' =>	val = '\'';
+			'"' =>	val = '"';
+			'\\' =>	val = '\\';
+			'a' =>	val = '\a';
+			'b' =>	val = '\b';
+			'n' =>	val = '\n';
+			'r' =>	val = '\r';
+			't' =>	val = '\t';
+			'v' =>	val = '\v';
+			* =>
+				error("invalid escape "+s[1:3]);
+			}
+		}else if(s[2] == 'u' && len s == 2+1+4) { # \unnnn sequence
+			val = 0;
+			s = s[3:];
+			while(s != ""){
+				c := s[0];
+				if(c >= '0' && c <= '9')
+					c -= '0';
+				else if(c >= 'a' && c <= 'f')
+					c -= 'a' - 10;
+				else if(c >= 'A' && c <= 'F')
+					c -= 'A' - 10;
+				else
+					error("illegal \\unnnn construction");
+				val = val * 16 + c;
+				s = s[1:];
+			}
+			if(val == 0)
+				error("'\\u0000' is illegal");
+		}else
+			error("unknown escape");
+	}else
+		val = extval++;
+
+	tokset[ntokens].value = val;
+	return ntokens;
+}
+
+peekline := 0;
+gettok(): int
+{
+	i, match, c: int;
+
+	tokname = "";
+	for(;;){
+		reserve := 0;
+		lineno += peekline;
+		peekline = 0;
+		c = finput.getc();
+		while(c == ' ' || c == '\n' || c == '\t' || c == '\v' || c == '\r') {
+			if(c == '\n')
+				lineno++;
+			c = finput.getc();
+		}
+
+		# skip comment
+		if(c != '#')
+			break;
+		lineno += skipcom();
+	}
+	case c {
+	Bufio->EOF =>
+		return ENDFILE;
+
+	'{' =>
+		finput.ungetc();
+		return '=';
+
+	'<' =>
+		# get, and look up, a type name (union member name)
+		i = 0;
+		while((c=finput.getc()) != '>' && c != Bufio->EOF && c != '\n')
+			tokname[i++] = c;
+		if(c != '>')
+			error("unterminated < ... > clause");
+		for(i=1; i<=ntypes; i++)
+			if(typeset[i] == tokname) {
+				numbval = i;
+				return TYPENAME;
+			}
+		ntypes++;
+		numbval = ntypes;
+		typeset[numbval] = tokname;
+		return TYPENAME;
+
+	'"' or '\'' =>
+		match = c;
+		tokname[0] = ' ';
+		i = 1;
+		for(;;) {
+			c = finput.getc();
+			if(c == '\n' || c == Bufio->EOF)
+				error("illegal or missing ' or \"" );
+			if(c == '\\') {
+				tokname[i++] = '\\';
+				c = finput.getc();
+			} else if(c == match)
+				return IDENTIFIER;
+			tokname[i++] = c;
+		}
+
+	'%' =>
+		case c = finput.getc(){
+		'%' =>	return MARK;
+		'=' =>	return PREC;
+		'{' =>	return LCURLY;
+		}
+
+		getword(c);
+		# find a reserved word
+		for(c=0; c < len resrv; c++)
+			if(tokname == resrv[c].name)
+				return resrv[c].value;
+		error("invalid escape, or illegal reserved word: "+tokname);
+
+	'0' to '9' =>
+		numbval = c - '0';
+		while(isdigit(c = finput.getc()))
+			numbval = numbval*10 + c-'0';
+		finput.ungetc();
+		return NUMBER;
+
+	* =>
+		if(isword(c) || c=='.' || c=='$')
+			getword(c);
+		else
+			return c;
+	}
+
+	# look ahead to distinguish IDENTIFIER from IDENTCOLON
+	c = finput.getc();
+	while(c == ' ' || c == '\t'|| c == '\n' || c == '\v' || c == '\r' || c == '#') {
+		if(c == '\n')
+			peekline++;
+		# look for comments
+		if(c == '#')
+			peekline += skipcom();
+		c = finput.getc();
+	}
+	if(c == ':')
+		return IDENTCOLON;
+	finput.ungetc();
+	return IDENTIFIER;
+}
+
+getword(c: int)
+{
+	i := 0;
+	while(isword(c) || isdigit(c) || c == '_' || c=='.' || c=='$') {
+		tokname[i++] = c;
+		c = finput.getc();
+	}
+	finput.ungetc();
+}
+
+#
+# determine the type of a symbol
+#
+fdtype(t: int): int
+{
+	v : int;
+	s: string;
+
+	if(t >= NTBASE) {
+		v = nontrst[t-NTBASE].value;
+		s = nontrst[t-NTBASE].name;
+	} else {
+		v = TYPE(toklev[t]);
+		s = tokset[t].name;
+	}
+	if(v <= 0)
+		error("must specify type for "+s);
+	return v;
+}
+
+chfind(t: int, s: string): int
+{
+	if(s[0] == ' ')
+		t = 0;
+	for(i:=0; i<=ntokens; i++)
+		if(s == tokset[i].name)
+			return i;
+	for(i=0; i<=nnonter; i++)
+		if(s == nontrst[i].name)
+			return NTBASE+i;
+
+	# cannot find name
+	if(t > 1)
+		error(s+" should have been defined earlier");
+	return defin(t, s);
+}
+
+#
+# saves module definition in Code
+#
+cpymodule()
+{
+	if(gettok() != IDENTIFIER)
+		error("bad %%module construction");
+	if(modname != nil)
+		error("duplicate %%module construction");
+	modname = tokname;
+
+	level := 0;
+	for(;;) {
+		if((c:=finput.getc()) == Bufio->EOF)
+			error("EOF encountered while processing %%module");
+		case c {
+		'\n' =>
+			lineno++;
+		'{' =>
+			level++;
+			if(level == 1)
+				continue;
+		'}' =>
+			level--;
+
+			# we are finished copying
+			if(level == 0)
+				return;
+		}
+		addcodec(CodeMod, c);
+	}
+}
+
+#
+# saves code between %{ and %}
+#
+cpycode()
+{
+	c := finput.getc();
+	if(c == '\n') {
+		c = finput.getc();
+		lineno++;
+	}
+	addcode(CodeData, "\n#line\t" + string lineno + "\t\"" + infile + "\"\n");
+	while(c != Bufio->EOF) {
+		if(c == '%') {
+			if((c=finput.getc()) == '}')
+				return;
+			addcodec(CodeData, '%');
+		}
+		addcodec(CodeData, c);
+		if(c == '\n')
+			lineno++;
+		c = finput.getc();
+	}
+	error("eof before %%}");
+}
+
+addcode(k: int, s: string)
+{
+	for(i := 0; i < len s; i++)
+		addcodec(k, s[i]);
+}
+
+addcodec(k, c: int)
+{
+	if(codehead == nil
+	|| k != codetail.kind
+	|| codetail.ndata >= NCode){
+		cd := ref Code(k, array[NCode+UTFmax] of byte, 0, nil);
+		if(codehead == nil)
+			codehead = cd;
+		else
+			codetail.next = cd;
+		codetail = cd;
+	}
+
+	codetail.ndata += sys->char2byte(c, codetail.data, codetail.ndata);
+}
+
+dumpcode(til: int)
+{
+	for(; codehead != nil; codehead = codehead.next){
+		if(codehead.kind == til)
+			return;
+		if(ftable.write(codehead.data, codehead.ndata) != codehead.ndata)
+			error("can't write output file");
+	}
+}
+
+#
+# write out the module declaration and any token info
+#
+dumpmod()
+{
+	if(fdefine != nil) {
+		fdefine.puts(modname);
+		fdefine.puts(": module {\n");
+	}
+	ftable.puts(modname);
+	ftable.puts(": module {\n");
+
+	for(; codehead != nil; codehead = codehead.next){
+		if(codehead.kind != CodeMod)
+			break;
+		if(ftable.write(codehead.data, codehead.ndata) != codehead.ndata)
+			error("can't write output file");
+		if(fdefine != nil && fdefine.write(codehead.data, codehead.ndata) != codehead.ndata)
+			error("can't write define file");
+	}
+
+	for(i:=TOKSTART; i<=ntokens; i++) {
+		# non-literals
+		c := tokset[i].name[0];
+		if(c != ' ' && c != '$') {
+			s := tokset[i].name+": con	"+string tokset[i].value+";\n";
+			ftable.puts(s);
+			if(fdefine != nil)
+				fdefine.puts(s);
+		}
+	}
+
+	if(fdefine != nil)
+		fdefine.puts("};\n");
+	ftable.puts("\n};\n");
+
+	if(fdebug != nil) {
+		fdebug.puts("yytoknames = array[] of {\n");
+		for(i=1; i<=ntokens; i++) {
+			if(tokset[i].name != nil)
+				fdebug.puts("\t\""+chcopy(tokset[i].name)+"\",\n");
+			else
+				fdebug.puts("\t\"\",\n");
+		}
+		fdebug.puts("};\n");
+	}
+}
+
+#
+# skip over comments
+# skipcom is called after reading a '#'
+#
+skipcom(): int
+{
+	c := finput.getc();
+	while(c != Bufio->EOF) {
+		if(c == '\n')
+			return 1;
+		c = finput.getc();
+	}
+	error("EOF inside comment");
+	return 0;
+}
+
+#
+# copy limbo action to the next ; or closing }
+#
+cpyact(curprod: array of int, max: int)
+{
+	addcode(CodeAct, "\n#line\t");
+	addcode(CodeAct, string lineno);
+	addcode(CodeAct, "\t\"");
+	addcode(CodeAct, infile);
+	addcode(CodeAct, "\"\n");
+
+	brac := 0;
+
+loop:	for(;;){
+		c := finput.getc();
+	swt:	case c {
+		';' =>
+			if(brac == 0) {
+				addcodec(CodeAct, c);
+				return;
+			}
+	
+		'{' =>
+			brac++;
+			
+		'$' =>
+			s := 1;
+			tok := -1;
+			c = finput.getc();
+	
+			# type description
+			if(c == '<') {
+				finput.ungetc();
+				if(gettok() != TYPENAME)
+					error("bad syntax on $<ident> clause");
+				tok = numbval;
+				c = finput.getc();
+			}
+			if(c == '$') {
+				addcode(CodeAct, "e.yyval");
+	
+				# put out the proper tag...
+				if(ntypes) {
+					if(tok < 0)
+						tok = fdtype(curprod[0]);
+					addcode(CodeAct, "."+typeset[tok]);
+				}
+				continue loop;
+			}
+			if(c == '-') {
+				s = -s;
+				c = finput.getc();
+			}
+			j := 0;
+			if(isdigit(c)) {
+				while(isdigit(c)) {
+					j = j*10 + c-'0';
+					c = finput.getc();
+				}
+				finput.ungetc();
+				j = j*s;
+				if(j >= max)
+					error("Illegal use of $" + string j);
+			}else if(isword(c) || c == '_' || c == '.') {
+				# look for $name
+				finput.ungetc();
+				if(gettok() != IDENTIFIER)
+					error("$ must be followed by an identifier");
+				tokn := chfind(2, tokname);
+				fnd := -1;
+				if((c = finput.getc()) != '@')
+					finput.ungetc();
+				else if(gettok() != NUMBER)
+					error("@ must be followed by number");
+				else
+					fnd = numbval;
+				for(j=1; j<max; j++){
+					if(tokn == curprod[j]) {
+						fnd--;
+						if(fnd <= 0)
+							break;
+					}
+				}
+				if(j >= max)
+					error("$name or $name@number not found");
+			}else{
+				addcodec(CodeAct, '$');
+				if(s < 0)
+					addcodec(CodeAct, '-');
+				finput.ungetc();
+				continue loop;
+			}
+			addcode(CodeAct, "yys[yypt-" + string(max-j-1) + "].yyv");
+	
+			# put out the proper tag
+			if(ntypes) {
+				if(j <= 0 && tok < 0)
+					error("must specify type of $" + string j);
+				if(tok < 0)
+					tok = fdtype(curprod[j]);
+				addcodec(CodeAct, '.');
+				addcode(CodeAct, typeset[tok]);
+			}
+			continue loop;
+
+		'}' =>
+			brac--;
+			if(brac)
+				break;
+			addcodec(CodeAct, c);
+			return;
+
+		'#' =>
+			# a comment
+			addcodec(CodeAct, c);
+			c = finput.getc();
+			while(c != Bufio->EOF) {
+				if(c == '\n') {
+					lineno++;
+					break swt;
+				}
+				addcodec(CodeAct, c);
+				c = finput.getc();
+			}
+			error("EOF inside comment");
+
+		'\''or '"' =>
+			# character string or constant
+			match := c;
+			addcodec(CodeAct, c);
+			while(c = finput.getc()) {
+				if(c == '\\') {
+					addcodec(CodeAct, c);
+					c = finput.getc();
+					if(c == '\n')
+						lineno++;
+				} else if(c == match)
+					break swt;
+				if(c == '\n')
+					error("newline in string or char const.");
+				addcodec(CodeAct, c);
+			}
+			error("EOF in string or character constant");
+
+		Bufio->EOF =>
+			error("action does not terminate");
+
+		'\n' =>
+			lineno++;
+		}
+
+		addcodec(CodeAct, c);
+	}
+}
+
+openup(stem: string, dflag, vflag, ytab: int, ytabc: string)
+{
+	buf: string;
+	if(vflag) {
+		buf = stem + "." + FILEU;
+		foutput = bufio->create(buf, Bufio->OWRITE, 8r666);
+		if(foutput == nil)
+			error("can't create " + buf);
+	}
+	if(yydebug != nil) {
+		buf = stem + "." + FILEDEBUG;
+		fdebug = bufio->create(buf, Bufio->OWRITE, 8r666);
+		if(fdebug == nil)
+			error("can't create " + buf);
+	}
+	if(dflag) {
+		buf = stem + "." + FILED;
+		fdefine = bufio->create(buf, Bufio->OWRITE, 8r666);
+		if(fdefine == nil)
+			error("can't create " + buf);
+	}
+	if(ytab == 0)
+		buf = stem + "." + OFILE;
+	else
+		buf = ytabc;
+	ftable = bufio->create(buf, Bufio->OWRITE, 8r666);
+	if(ftable == nil)
+		error("can't create file " + buf);
+}
+
+#
+# return a pointer to the name of symbol i
+#
+symnam(i: int): string
+{
+	s: string;
+	if(i >= NTBASE)
+		s = nontrst[i-NTBASE].name;
+	else
+		s = tokset[i].name;
+	if(s[0] == ' ')
+		s = s[1:];
+	return s;
+}
+
+#
+# write out error comment
+#
+error(s: string)
+{
+	nerrors++;
+	fprint(stderr, "\n fatal error: %s, %s:%d\n", s, infile, lineno);
+	if(!fatfl)
+		return;
+	summary();
+	exit;
+#	exits("error");
+}
+
+#
+# set elements 0 through n-1 to c
+#
+aryfil(v: array of int, n, c: int)
+{
+	for(i:=0; i<n; i++)
+		v[i] = c;
+}
+
+#
+# compute an array with the beginnings of productions yielding given nonterminals
+# The array pres points to these lists
+# the array pyield has the lists: the total size is only NPROD+1
+#
+cpres()
+{
+	pres = array[nnonter+1] of array of array of int;
+	curres := array[nprod] of array of int;
+	for(i:=0; i<=nnonter; i++) {
+		n := 0;
+		c := i+NTBASE;
+		fatfl = 0;  	# make undefined symbols nonfatal
+		for(j:=0; j<nprod; j++)
+			if(prdptr[j][0] == c)
+				curres[n++] = prdptr[j][1:];
+		if(n == 0)
+			error("nonterminal " + nontrst[i].name + " not defined!");
+		else{
+			pres[i] = array[n] of array of int;
+			pres[i][0:] = curres[:n];
+		}
+	}
+	fatfl = 1;
+	if(nerrors) {
+		summary();
+		exit;		#exits("error");
+	}
+}
+
+dumppres()
+{
+	for(i := 0; i <= nnonter; i++){
+		print("nonterm %d\n", i);
+		curres := pres[i];
+		for(j := 0; j < len curres; j++){
+			print("\tproduction %d:", j);
+			prd := curres[j];
+			for(k := 0; k < len prd; k++)
+				print(" %d", prd[k]);
+			print("\n");
+		}
+	}
+}
+
+#
+# mark nonterminals which derive the empty string
+# also, look for nonterminals which don't derive any token strings
+#
+cempty()
+{
+	i, p, np: int;
+	prd: array of int;
+
+	pempty = array[nnonter+1] of int;
+
+	# first, use the array pempty to detect productions that can never be reduced
+	# set pempty to WHONOWS
+	aryfil(pempty, nnonter+1, WHOKNOWS);
+
+	# now, look at productions, marking nonterminals which derive something
+more:	for(;;){
+		for(i=0; i<nprod; i++) {
+			prd = prdptr[i];
+			if(pempty[prd[0] - NTBASE])
+				continue;
+			np = len prd - 1;
+			for(p = 1; p < np; p++)
+				if(prd[p] >= NTBASE && pempty[prd[p]-NTBASE] == WHOKNOWS)
+					break;
+			# production can be derived
+			if(p == np) {
+				pempty[prd[0]-NTBASE] = OK;
+				continue more;
+			}
+		}
+		break;
+	}
+
+	# now, look at the nonterminals, to see if they are all OK
+	for(i=0; i<=nnonter; i++) {
+		# the added production rises or falls as the start symbol ...
+		if(i == 0)
+			continue;
+		if(pempty[i] != OK) {
+			fatfl = 0;
+			error("nonterminal " + nontrst[i].name + " never derives any token string");
+		}
+	}
+
+	if(nerrors) {
+		summary();
+		exit;		#exits("error");
+	}
+
+	# now, compute the pempty array, to see which nonterminals derive the empty string
+	# set pempty to WHOKNOWS
+	aryfil(pempty, nnonter+1, WHOKNOWS);
+
+	# loop as long as we keep finding empty nonterminals
+
+again:	for(;;){
+	next:	for(i=1; i<nprod; i++) {
+			# not known to be empty
+			prd = prdptr[i];
+			if(pempty[prd[0]-NTBASE] != WHOKNOWS)
+				continue;
+			np = len prd - 1;
+			for(p = 1; p < np; p++)
+				if(prd[p] < NTBASE || pempty[prd[p]-NTBASE] != EMPTY)
+					continue next;
+
+			# we have a nontrivially empty nonterminal
+			pempty[prd[0]-NTBASE] = EMPTY;
+			# got one ... try for another
+			continue again;
+		}
+		return;
+	}
+}
+
+dumpempty()
+{
+	for(i := 0; i <= nnonter; i++)
+		if(pempty[i] == EMPTY)
+			print("non-term %d %s matches empty\n", i, symnam(i+NTBASE));
+}
+
+#
+# compute an array with the first of nonterminals
+#
+cpfir()
+{
+	s, n, p, np, ch: int;
+	curres: array of array of int;
+	prd: array of int;
+
+	wsets = array[nnonter+WSETINC] of Wset;
+	pfirst = array[nnonter+1] of Lkset;
+	for(i:=0; i<=nnonter; i++) {
+		wsets[i].ws = mkset();
+		pfirst[i] = mkset();
+		curres = pres[i];
+		n = len curres;
+		# initially fill the sets
+		for(s = 0; s < n; s++) {
+			prd = curres[s];
+			np = len prd - 1;
+			for(p = 0; p < np; p++) {
+				ch = prd[p];
+				if(ch < NTBASE) {
+					setbit(pfirst[i], ch);
+					break;
+				}
+				if(!pempty[ch-NTBASE])
+					break;
+			}
+		}
+	}
+
+	# now, reflect transitivity
+	changes := 1;
+	while(changes) {
+		changes = 0;
+		for(i=0; i<=nnonter; i++) {
+			curres = pres[i];
+			n = len curres;
+			for(s = 0; s < n; s++) {
+				prd = curres[s];
+				np = len prd - 1;
+				for(p = 0; p < np; p++) {
+					ch = prd[p] - NTBASE;
+					if(ch < 0)
+						break;
+					changes |= setunion(pfirst[i], pfirst[ch]);
+					if(!pempty[ch])
+						break;
+				}
+			}
+		}
+	}
+
+	if(!indebug)
+		return;
+	if(foutput != nil){
+		for(i=0; i<=nnonter; i++) {
+			foutput.putc('\n');
+			foutput.puts(nontrst[i].name);
+			foutput.puts(": ");
+			prlook(pfirst[i]);
+			foutput.putc(' ');
+			foutput.puts(string pempty[i]);
+			foutput.putc('\n');
+		}
+	}
+}
+
+#
+# generate the states
+#
+stagen()
+{
+	# initialize
+	nstate = 0;
+	tstates = array[ntokens+1] of {* => 0};	# states generated by terminal gotos
+	ntstates = array[nnonter+1] of {* => 0};# states generated by nonterminal gotos
+	amem = array[ACTSIZE] of {* => 0};
+	memp = 0;
+
+	clset = mkset();
+	pstate[0] = pstate[1] = 0;
+	aryfil(clset, tbitset, 0);
+	putitem(Pitem(prdptr[0], 0, 0, 0), clset);
+	tystate[0] = MUSTDO;
+	nstate = 1;
+	pstate[2] = pstate[1];
+
+	#
+	# now, the main state generation loop
+	# first pass generates all of the states
+	# later passes fix up lookahead
+	# could be sped up a lot by remembering
+	# results of the first pass rather than recomputing
+	#
+	first := 1;
+	for(more := 1; more; first = 0){
+		more = 0;
+		for(i:=0; i<nstate; i++) {
+			if(tystate[i] != MUSTDO)
+				continue;
+
+			tystate[i] = DONE;
+			aryfil(temp1, nnonter+1, 0);
+
+			# take state i, close it, and do gotos
+			closure(i);
+
+			# generate goto's
+			for(p:=0; p<cwp; p++) {
+				pi := wsets[p];
+				if(pi.flag)
+					continue;
+				wsets[p].flag = 1;
+				c := pi.pitem.first;
+				if(c <= 1) {
+					if(pstate[i+1]-pstate[i] <= p)
+						tystate[i] = MUSTLOOKAHEAD;
+					continue;
+				}
+				# do a goto on c
+				putitem(wsets[p].pitem, wsets[p].ws);
+				for(q:=p+1; q<cwp; q++) {
+					# this item contributes to the goto
+					if(c == wsets[q].pitem.first) {
+						putitem(wsets[q].pitem, wsets[q].ws);
+						wsets[q].flag = 1;
+					}
+				}
+
+				if(c < NTBASE)
+					state(c);	# register new state
+				else
+					temp1[c-NTBASE] = state(c);
+			}
+
+			if(gsdebug && foutput != nil) {
+				foutput.puts(string i + ": ");
+				for(j:=0; j<=nnonter; j++)
+					if(temp1[j])
+						foutput.puts(nontrst[j].name + " " + string temp1[j] + ", ");
+				foutput.putc('\n');
+			}
+
+			if(first)
+				indgo[i] = apack(temp1[1:], nnonter-1) - 1;
+
+			more++;
+		}
+	}
+}
+
+#
+# generate the closure of state i
+#
+closure(i: int)
+{
+	zzclose++;
+
+	# first, copy kernel of state i to wsets
+	cwp = 0;
+	q := pstate[i+1];
+	for(p:=pstate[i]; p<q; p++) {
+		wsets[cwp].pitem = statemem[p].pitem;
+		wsets[cwp].flag = 1;			# this item must get closed
+		wsets[cwp].ws[0:] = statemem[p].look;
+		cwp++;
+	}
+
+	# now, go through the loop, closing each item
+	work := 1;
+	while(work) {
+		work = 0;
+		for(u:=0; u<cwp; u++) {
+			if(wsets[u].flag == 0)
+				continue;
+			# dot is before c
+			c := wsets[u].pitem.first;
+			if(c < NTBASE) {
+				wsets[u].flag = 0;
+				# only interesting case is where . is before nonterminal
+				continue;
+			}
+
+			# compute the lookahead
+			aryfil(clset, tbitset, 0);
+
+			# find items involving c
+			for(v:=u; v<cwp; v++) {
+				if(wsets[v].flag != 1
+				|| wsets[v].pitem.first != c)
+					continue;
+				pi := wsets[v].pitem.prod;
+				ipi := wsets[v].pitem.off + 1;
+				
+				wsets[v].flag = 0;
+				if(nolook)
+					continue;
+				while((ch := pi[ipi++]) > 0) {
+					# terminal symbol
+					if(ch < NTBASE) {
+						setbit(clset, ch);
+						break;
+					}
+					# nonterminal symbol
+					setunion(clset, pfirst[ch-NTBASE]);
+					if(!pempty[ch-NTBASE])
+						break;
+				}
+				if(ch <= 0)
+					setunion(clset, wsets[v].ws);
+			}
+
+			#
+			# now loop over productions derived from c
+			#
+			curres := pres[c - NTBASE];
+			n := len curres;
+			# initially fill the sets
+	nexts:		for(s := 0; s < n; s++) {
+				prd := curres[s];
+				#
+				# put these items into the closure
+				# is the item there
+				#
+				for(v=0; v<cwp; v++) {
+					# yes, it is there
+					if(wsets[v].pitem.off == 0
+					&& wsets[v].pitem.prod == prd) {
+						if(!nolook && setunion(wsets[v].ws, clset))
+							wsets[v].flag = work = 1;
+						continue nexts;
+					}
+				}
+
+				#  not there; make a new entry
+				if(cwp >= len wsets){
+					awsets := array[cwp + WSETINC] of Wset;
+					awsets[0:] = wsets;
+					wsets = awsets;
+				}
+				wsets[cwp].pitem = Pitem(prd, 0, prd[0], -prd[len prd-1]);
+				wsets[cwp].flag = 1;
+				wsets[cwp].ws = mkset();
+				if(!nolook) {
+					work = 1;
+					wsets[cwp].ws[0:] = clset;
+				}
+				cwp++;
+			}
+		}
+	}
+
+	# have computed closure; flags are reset; return
+	if(cldebug && foutput != nil) {
+		foutput.puts("\nState " + string i + ", nolook = " + string nolook + "\n");
+		for(u:=0; u<cwp; u++) {
+			if(wsets[u].flag)
+				foutput.puts("flag set!\n");
+			wsets[u].flag = 0;
+			foutput.putc('\t');
+			foutput.puts(writem(wsets[u].pitem));
+			prlook(wsets[u].ws);
+			foutput.putc('\n');
+		}
+	}
+}
+
+#
+# sorts last state,and sees if it equals earlier ones. returns state number
+#
+state(c: int): int
+{
+	zzstate++;
+	p1 := pstate[nstate];
+	p2 := pstate[nstate+1];
+	if(p1 == p2)
+		return 0;	# null state
+	# sort the items
+	k, l: int;
+	for(k = p1+1; k < p2; k++) {	# make k the biggest
+		for(l = k; l > p1; l--) {
+			if(statemem[l].pitem.prodno < statemem[l-1].pitem.prodno
+			|| statemem[l].pitem.prodno == statemem[l-1].pitem.prodno
+			&& statemem[l].pitem.off < statemem[l-1].pitem.off) {
+				s := statemem[l];
+				statemem[l] = statemem[l-1];
+				statemem[l-1] = s;
+			}else
+				break;
+		}
+	}
+
+	size1 := p2 - p1;	# size of state
+
+	if(c >= NTBASE)
+		i := ntstates[c-NTBASE];
+	else
+		i = tstates[c];
+
+look:	for(; i != 0; i = mstates[i]) {
+		# get ith state
+		q1 := pstate[i];
+		q2 := pstate[i+1];
+		size2 := q2 - q1;
+		if(size1 != size2)
+			continue;
+		k = p1;
+		for(l = q1; l < q2; l++) {
+			if(statemem[l].pitem.prod != statemem[k].pitem.prod
+			|| statemem[l].pitem.off != statemem[k].pitem.off)
+				continue look;
+			k++;
+		}
+
+		# found it
+		pstate[nstate+1] = pstate[nstate];	# delete last state
+		# fix up lookaheads
+		if(nolook)
+			return i;
+		k = p1;
+		for(l = q1; l < q2; l++) {
+			if(setunion(statemem[l].look, statemem[k].look))
+				tystate[i] = MUSTDO;
+			k++;
+		}
+		return i;
+	}
+	# state is new
+	zznewstate++;
+	if(nolook)
+		error("yacc state/nolook error");
+	pstate[nstate+2] = p2;
+	if(nstate+1 >= NSTATES)
+		error("too many states");
+	if(c >= NTBASE) {
+		mstates[nstate] = ntstates[c-NTBASE];
+		ntstates[c-NTBASE] = nstate;
+	} else {
+		mstates[nstate] = tstates[c];
+		tstates[c] = nstate;
+	}
+	tystate[nstate] = MUSTDO;
+	return nstate++;
+}
+
+putitem(p: Pitem, set: Lkset)
+{
+	p.off++;
+	p.first = p.prod[p.off];
+
+	if(pidebug && foutput != nil)
+		foutput.puts("putitem(" + writem(p) + "), state " + string nstate + "\n");
+	j := pstate[nstate+1];
+	if(j >= len statemem){
+		asm := array[j + STATEINC] of Item;
+		asm[0:] = statemem;
+		statemem = asm;
+	}
+	statemem[j].pitem = p;
+	if(!nolook){
+		s := mkset();
+		s[0:] = set;
+		statemem[j].look = s;
+	}
+	j++;
+	pstate[nstate+1] = j;
+}
+
+#
+# creates output string for item pointed to by pp
+#
+writem(pp: Pitem): string
+{
+	i: int;
+	p := pp.prod;
+	q := chcopy(nontrst[prdptr[pp.prodno][0]-NTBASE].name) + ": ";
+	npi := pp.off;
+	pi := p == prdptr[pp.prodno];
+	for(;;){
+		c := ' ';
+		if(pi == npi)
+			c = '.';
+		q[len q] = c;
+		i = p[pi++];
+		if(i <= 0)
+			break;
+		q += chcopy(symnam(i));
+	}
+
+	# an item calling for a reduction
+	i = p[npi];
+	if(i < 0)
+		q += "    (" + string -i + ")";
+	return q;
+}
+
+#
+# pack state i from temp1 into amem
+#
+apack(p: array of int, n: int): int
+{
+	#
+	# we don't need to worry about checking because
+	# we will only look at entries known to be there...
+	# eliminate leading and trailing 0's
+	#
+	off := 0;
+	for(pp := 0; pp <= n && p[pp] == 0; pp++)
+		off--;
+ 	# no actions
+	if(pp > n)
+		return 0;
+	for(; n > pp && p[n] == 0; n--)
+		;
+	p = p[pp:n+1];
+
+	# now, find a place for the elements from p to q, inclusive
+	r := len amem - len p;
+nextk:	for(rr := 0; rr <= r; rr++) {
+		qq := rr;
+		for(pp = 0; pp < len p; pp++) {
+			if(p[pp] != 0)
+				if(p[pp] != amem[qq] && amem[qq] != 0)
+					continue nextk;
+			qq++;
+		}
+
+		# we have found an acceptable k
+		if(pkdebug && foutput != nil)
+			foutput.puts("off = " + string(off+rr) + ", k = " + string rr + "\n");
+		qq = rr;
+		for(pp = 0; pp < len p; pp++) {
+			if(p[pp]) {
+				if(qq > memp)
+					memp = qq;
+				amem[qq] = p[pp];
+			}
+			qq++;
+		}
+		if(pkdebug && foutput != nil) {
+			for(pp = 0; pp <= memp; pp += 10) {
+				foutput.putc('\t');
+				for(qq = pp; qq <= pp+9; qq++)
+					foutput.puts(string amem[qq] + " ");
+				foutput.putc('\n');
+			}
+		}
+		return off + rr;
+	}
+	error("no space in action table");
+	return 0;
+}
+
+#
+# print the output for the states
+#
+output()
+{
+	c, u, v: int;
+
+	ftable.puts("yyexca := array[] of {");
+	if(fdebug != nil)
+		fdebug.puts("yystates = array [] of {\n");
+
+	noset := mkset();
+
+	# output the stuff for state i
+	for(i:=0; i<nstate; i++) {
+		nolook = tystate[i]!=MUSTLOOKAHEAD;
+		closure(i);
+
+		# output actions
+		nolook = 1;
+		aryfil(temp1, ntokens+nnonter+1, 0);
+		for(u=0; u<cwp; u++) {
+			c = wsets[u].pitem.first;
+			if(c > 1 && c < NTBASE && temp1[c] == 0) {
+				for(v=u; v<cwp; v++)
+					if(c == wsets[v].pitem.first)
+						putitem(wsets[v].pitem, noset);
+				temp1[c] = state(c);
+			} else
+				if(c > NTBASE && temp1[(c -= NTBASE) + ntokens] == 0)
+					temp1[c+ntokens] = amem[indgo[i]+c];
+		}
+		if(i == 1)
+			temp1[1] = ACCEPTCODE;
+
+		# now, we have the shifts; look at the reductions
+		lastred = 0;
+		for(u=0; u<cwp; u++) {
+			c = wsets[u].pitem.first;
+
+			# reduction
+			if(c > 0)
+				continue;
+			lastred = -c;
+			us := wsets[u].ws;
+			for(k:=0; k<=ntokens; k++) {
+				if(!bitset(us, k))
+					continue;
+				if(temp1[k] == 0)
+					temp1[k] = c;
+				else
+				if(temp1[k] < 0) { # reduce/reduce conflict
+					if(foutput != nil)
+						foutput.puts(
+							"\n" + string i + ": reduce/reduce conflict  (red'ns "
+							+ string -temp1[k] + " and " + string lastred + " ) on " + symnam(k));
+					if(-temp1[k] > lastred)
+						temp1[k] = -lastred;
+					zzrrconf++;
+				} else
+					# potential shift/reduce conflict
+					precftn(lastred, k, i);
+			}
+		}
+		wract(i);
+	}
+
+	if(fdebug != nil)
+		fdebug.puts("};\n");
+	ftable.puts("};\n");
+	ftable.puts("YYNPROD: con " + string nprod + ";\n");
+	ftable.puts("YYPRIVATE: con " + string PRIVATE + ";\n");
+	ftable.puts("yytoknames: array of string;\n");
+	ftable.puts("yystates: array of string;\n");
+	if(yydebug != nil){
+		ftable.puts("include \"y.debug\";\n");
+		ftable.puts("yydebug: con " + yydebug + ";\n");
+	}else{
+		ftable.puts("yydebug: con 0;\n");
+	}
+}
+
+#
+# decide a shift/reduce conflict by precedence.
+# r is a rule number, t a token number
+# the conflict is in state s
+# temp1[t] is changed to reflect the action
+#
+precftn(r, t, s: int)
+{
+	action: int;
+
+	lp := levprd[r];
+	lt := toklev[t];
+	if(PLEVEL(lt) == 0 || PLEVEL(lp) == 0) {
+
+		# conflict
+		if(foutput != nil)
+			foutput.puts(
+				"\n" + string s + ": shift/reduce conflict (shift "
+				+ string temp1[t] + "(" + string PLEVEL(lt) + "), red'n "
+				+ string r + "(" + string PLEVEL(lp) + ")) on " + symnam(t));
+		zzsrconf++;
+		return;
+	}
+	if(PLEVEL(lt) == PLEVEL(lp))
+		action = ASSOC(lt);
+	else if(PLEVEL(lt) > PLEVEL(lp))
+		action = RASC;  # shift
+	else
+		action = LASC;  # reduce
+	case action{
+	BASC =>  # error action
+		temp1[t] = ERRCODE;
+	LASC =>  # reduce
+		temp1[t] = -r;
+	}
+}
+
+#
+# output state i
+# temp1 has the actions, lastred the default
+#
+wract(i: int)
+{
+	p, p1: int;
+
+	# find the best choice for lastred
+	lastred = 0;
+	ntimes := 0;
+	for(j:=0; j<=ntokens; j++) {
+		if(temp1[j] >= 0)
+			continue;
+		if(temp1[j]+lastred == 0)
+			continue;
+		# count the number of appearances of temp1[j]
+		count := 0;
+		tred := -temp1[j];
+		levprd[tred] |= REDFLAG;
+		for(p=0; p<=ntokens; p++)
+			if(temp1[p]+tred == 0)
+				count++;
+		if(count > ntimes) {
+			lastred = tred;
+			ntimes = count;
+		}
+	}
+
+	#
+	# for error recovery, arrange that, if there is a shift on the
+	# error recovery token, `error', that the default be the error action
+	#
+	if(temp1[2] > 0)
+		lastred = 0;
+
+	# clear out entries in temp1 which equal lastred
+	# count entries in optst table
+	n := 0;
+	for(p=0; p<=ntokens; p++) {
+		p1 = temp1[p];
+		if(p1+lastred == 0)
+			temp1[p] = p1 = 0;
+		if(p1 > 0 && p1 != ACCEPTCODE && p1 != ERRCODE)
+			n++;
+	}
+
+	wrstate(i);
+	defact[i] = lastred;
+	flag := 0;
+	os := array[n*2] of int;
+	n = 0;
+	for(p=0; p<=ntokens; p++) {
+		if((p1=temp1[p]) != 0) {
+			if(p1 < 0) {
+				p1 = -p1;
+			} else if(p1 == ACCEPTCODE) {
+				p1 = -1;
+			} else if(p1 == ERRCODE) {
+				p1 = 0;
+			} else {
+				os[n++] = p;
+				os[n++] = p1;
+				zzacent++;
+				continue;
+			}
+			if(flag++ == 0)
+				ftable.puts("-1, " + string i + ",\n");
+			ftable.puts("\t" + string p + ", " + string p1 + ",\n");
+			zzexcp++;
+		}
+	}
+	if(flag) {
+		defact[i] = -2;
+		ftable.puts("\t-2, " + string lastred + ",\n");
+	}
+	optst[i] = os;
+}
+
+#
+# writes state i
+#
+wrstate(i: int)
+{
+	j0, j1, u: int;
+	pp, qq: int;
+
+	if(fdebug != nil) {
+		if(lastred) {
+			fdebug.puts("	nil, #" + string i + "\n");
+		} else {
+			fdebug.puts("	\"");
+			qq = pstate[i+1];
+			for(pp=pstate[i]; pp<qq; pp++){
+				fdebug.puts(writem(statemem[pp].pitem));
+				fdebug.puts("\\n");
+			}
+			if(tystate[i] == MUSTLOOKAHEAD)
+				for(u = pstate[i+1] - pstate[i]; u < cwp; u++)
+					if(wsets[u].pitem.first < 0){
+						fdebug.puts(writem(wsets[u].pitem));
+						fdebug.puts("\\n");
+					}
+			fdebug.puts("\", #" + string i + "/\n");
+		}
+	}
+	if(foutput == nil)
+		return;
+	foutput.puts("\nstate " + string i + "\n");
+	qq = pstate[i+1];
+	for(pp=pstate[i]; pp<qq; pp++){
+		foutput.putc('\t');
+		foutput.puts(writem(statemem[pp].pitem));
+		foutput.putc('\n');
+	}
+	if(tystate[i] == MUSTLOOKAHEAD) {
+		# print out empty productions in closure
+		for(u = pstate[i+1] - pstate[i]; u < cwp; u++) {
+			if(wsets[u].pitem.first < 0) {
+				foutput.putc('\t');
+				foutput.puts(writem(wsets[u].pitem));
+				foutput.putc('\n');
+			}
+		}
+	}
+
+	# check for state equal to another
+	for(j0=0; j0<=ntokens; j0++)
+		if((j1=temp1[j0]) != 0) {
+			foutput.puts("\n\t" + symnam(j0) + "  ");
+			# shift, error, or accept
+			if(j1 > 0) {
+				if(j1 == ACCEPTCODE)
+					foutput.puts("accept");
+				else if(j1 == ERRCODE)
+					foutput.puts("error");
+				else
+					foutput.puts("shift "+string j1);
+			} else
+				foutput.puts("reduce " + string -j1 + " (src line " + string rlines[-j1] + ")");
+		}
+
+	# output the final production
+	if(lastred)
+		foutput.puts("\n\t.  reduce " + string lastred + " (src line " + string rlines[lastred] + ")\n\n");
+	else
+		foutput.puts("\n\t.  error\n\n");
+
+	# now, output nonterminal actions
+	j1 = ntokens;
+	for(j0 = 1; j0 <= nnonter; j0++) {
+		j1++;
+		if(temp1[j1])
+			foutput.puts("\t" + symnam(j0+NTBASE) + "  goto " + string temp1[j1] + "\n");
+	}
+}
+
+#
+# output the gotos for the nontermninals
+#
+go2out()
+{
+	for(i := 1; i <= nnonter; i++) {
+		go2gen(i);
+
+		# find the best one to make default
+		best := -1;
+		times := 0;
+
+		# is j the most frequent
+		for(j := 0; j < nstate; j++) {
+			if(tystate[j] == 0)
+				continue;
+			if(tystate[j] == best)
+				continue;
+
+			# is tystate[j] the most frequent
+			count := 0;
+			cbest := tystate[j];
+			for(k := j; k < nstate; k++)
+				if(tystate[k] == cbest)
+					count++;
+			if(count > times) {
+				best = cbest;
+				times = count;
+			}
+		}
+
+		# best is now the default entry
+		zzgobest += times-1;
+		n := 0;
+		for(j = 0; j < nstate; j++)
+			if(tystate[j] != 0 && tystate[j] != best)
+				n++;
+		goent := array[2*n+1] of int;
+		n = 0;
+		for(j = 0; j < nstate; j++)
+			if(tystate[j] != 0 && tystate[j] != best) {
+				goent[n++] = j;
+				goent[n++] = tystate[j];
+				zzgoent++;
+			}
+
+		# now, the default
+		if(best == -1)
+			best = 0;
+		zzgoent++;
+		goent[n] = best;
+		yypgo[i] = goent;
+	}
+}
+
+#
+# output the gotos for nonterminal c
+#
+go2gen(c: int)
+{
+	i, cc, p, q: int;
+
+	# first, find nonterminals with gotos on c
+	aryfil(temp1, nnonter+1, 0);
+	temp1[c] = 1;
+	work := 1;
+	while(work) {
+		work = 0;
+		for(i=0; i<nprod; i++) {
+			# cc is a nonterminal with a goto on c
+			cc = prdptr[i][1]-NTBASE;
+			if(cc >= 0 && temp1[cc] != 0) {
+				# thus, the left side of production i does too
+				cc = prdptr[i][0]-NTBASE;
+				if(temp1[cc] == 0) {
+					  work = 1;
+					  temp1[cc] = 1;
+				}
+			}
+		}
+	}
+
+	# now, we have temp1[c] = 1 if a goto on c in closure of cc
+	if(g2debug && foutput != nil) {
+		foutput.puts(nontrst[c].name);
+		foutput.puts(": gotos on ");
+		for(i=0; i<=nnonter; i++)
+			if(temp1[i]){
+				foutput.puts(nontrst[i].name);
+				foutput.putc(' ');
+			}
+		foutput.putc('\n');
+	}
+
+	# now, go through and put gotos into tystate
+	aryfil(tystate, nstate, 0);
+	for(i=0; i<nstate; i++) {
+		q = pstate[i+1];
+		for(p=pstate[i]; p<q; p++) {
+			if((cc = statemem[p].pitem.first) >= NTBASE) {
+				# goto on c is possible
+				if(temp1[cc-NTBASE]) {
+					tystate[i] = amem[indgo[i]+c];
+					break;
+				}
+			}
+		}
+	}
+}
+
+#
+# in order to free up the mem and amem arrays for the optimizer,
+# and still be able to output yyr1, etc., after the sizes of
+# the action array is known, we hide the nonterminals
+# derived by productions in levprd.
+#
+hideprod()
+{
+	j := 0;
+	levprd[0] = 0;
+	for(i:=1; i<nprod; i++) {
+		if(!(levprd[i] & REDFLAG)) {
+			j++;
+			if(foutput != nil) {
+				foutput.puts("Rule not reduced:   ");
+				foutput.puts(writem(Pitem(prdptr[i], 0, 0, i)));
+				foutput.putc('\n');
+			}
+		}
+		levprd[i] = prdptr[i][0] - NTBASE;
+	}
+	if(j)
+		print("%d rules never reduced\n", j);
+}
+
+callopt()
+{
+	j, k, p, q: int;
+	v: array of int;
+
+	pgo = array[nnonter+1] of int;
+	pgo[0] = 0;
+	maxoff = 0;
+	maxspr = 0;
+	for(i := 0; i < nstate; i++) {
+		k = 32000;
+		j = 0;
+		v = optst[i];
+		q = len v;
+		for(p = 0; p < q; p += 2) {
+			if(v[p] > j)
+				j = v[p];
+			if(v[p] < k)
+				k = v[p];
+		}
+		# nontrivial situation
+		if(k <= j) {
+			# j is now the range
+#			j -= k;			# call scj
+			if(k > maxoff)
+				maxoff = k;
+		}
+		tystate[i] = q + 2*j;
+		if(j > maxspr)
+			maxspr = j;
+	}
+
+	# initialize ggreed table
+	ggreed = array[nnonter+1] of int;
+	for(i = 1; i <= nnonter; i++) {
+		ggreed[i] = 1;
+		j = 0;
+
+		# minimum entry index is always 0
+		v = yypgo[i];
+		q = len v - 1;
+		for(p = 0; p < q ; p += 2) {
+			ggreed[i] += 2;
+			if(v[p] > j)
+				j = v[p];
+		}
+		ggreed[i] = ggreed[i] + 2*j;
+		if(j > maxoff)
+			maxoff = j;
+	}
+
+	# now, prepare to put the shift actions into the amem array
+	for(i = 0; i < ACTSIZE; i++)
+		amem[i] = 0;
+	maxa = 0;
+	for(i = 0; i < nstate; i++) {
+		if(tystate[i] == 0 && adb > 1)
+			ftable.puts("State " + string i + ": null\n");
+		indgo[i] = YYFLAG1;
+	}
+	while((i = nxti()) != NOMORE)
+		if(i >= 0)
+			stin(i);
+		else
+			gin(-i);
+
+	# print amem array
+	if(adb > 2)
+		for(p = 0; p <= maxa; p += 10) {
+			ftable.puts(string p + "  ");
+			for(i = 0; i < 10; i++)
+				ftable.puts(string amem[p+i] + "  ");
+			ftable.putc('\n');
+		}
+
+	aoutput();
+	osummary();
+}
+
+#
+# finds the next i
+#
+nxti(): int
+{
+	max := 0;
+	maxi := 0;
+	for(i := 1; i <= nnonter; i++)
+		if(ggreed[i] >= max) {
+			max = ggreed[i];
+			maxi = -i;
+		}
+	for(i = 0; i < nstate; i++)
+		if(tystate[i] >= max) {
+			max = tystate[i];
+			maxi = i;
+		}
+	if(max == 0)
+		return NOMORE;
+	return maxi;
+}
+
+gin(i: int)
+{
+	s: int;
+
+	# enter gotos on nonterminal i into array amem
+	ggreed[i] = 0;
+
+	q := yypgo[i];
+	nq := len q - 1;
+	# now, find amem place for it
+nextgp:	for(p := 0; p < ACTSIZE; p++) {
+		if(amem[p])
+			continue;
+		for(r := 0; r < nq; r += 2) {
+			s = p + q[r] + 1;
+			if(s > maxa){
+				maxa = s;
+				if(maxa >= ACTSIZE)
+					error("a array overflow");
+			}
+			if(amem[s])
+				continue nextgp;
+		}
+		# we have found amem spot
+		amem[p] = q[nq];
+		if(p > maxa)
+			maxa = p;
+		for(r = 0; r < nq; r += 2) {
+			s = p + q[r] + 1;
+			amem[s] = q[r+1];
+		}
+		pgo[i] = p;
+		if(adb > 1)
+			ftable.puts("Nonterminal " + string i + ", entry at " + string pgo[i] + "\n");
+		return;
+	}
+	error("cannot place goto " + string i + "\n");
+}
+
+stin(i: int)
+{
+	s: int;
+
+	tystate[i] = 0;
+
+	# enter state i into the amem array
+	q := optst[i];
+	nq := len q;
+	# find an acceptable place
+nextn:	for(n := -maxoff; n < ACTSIZE; n++) {
+		flag := 0;
+		for(r := 0; r < nq; r += 2) {
+			s = q[r] + n;
+			if(s < 0 || s > ACTSIZE)
+				continue nextn;
+			if(amem[s] == 0)
+				flag++;
+			else if(amem[s] != q[r+1])
+				continue nextn;
+		}
+
+		# check the position equals another only if the states are identical
+		for(j:=0; j<nstate; j++) {
+			if(indgo[j] == n) {
+
+				# we have some disagreement
+				if(flag)
+					continue nextn;
+				if(nq == len optst[j]) {
+
+					# states are equal
+					indgo[i] = n;
+					if(adb > 1)
+						ftable.puts("State " + string i + ": entry at "
+							+ string n + " equals state " + string j + "\n");
+					return;
+				}
+
+				# we have some disagreement
+				continue nextn;
+			}
+		}
+
+		for(r = 0; r < nq; r += 2) {
+			s = q[r] + n;
+			if(s > maxa)
+				maxa = s;
+			if(amem[s] != 0 && amem[s] != q[r+1])
+				error("clobber of a array, pos'n " + string s + ", by " + string q[r+1] + "");
+			amem[s] = q[r+1];
+		}
+		indgo[i] = n;
+		if(adb > 1)
+			ftable.puts("State " + string i + ": entry at " + string indgo[i] + "\n");
+		return;
+	}
+	error("Error; failure to place state " + string i + "\n");
+}
+
+#
+# this version is for limbo
+# write out the optimized parser
+#
+aoutput()
+{
+	ftable.puts("YYLAST:\tcon "+string (maxa+1)+";\n");
+	arout("yyact", amem, maxa+1);
+	arout("yypact", indgo, nstate);
+	arout("yypgo", pgo, nnonter+1);
+}
+
+#
+# put out other arrays, copy the parsers
+#
+others()
+{
+	finput = bufio->open(parser, Bufio->OREAD);
+	if(finput == nil)
+		error("cannot find parser " + parser);
+	arout("yyr1", levprd, nprod);
+	aryfil(temp1, nprod, 0);
+
+	#
+	#yyr2 is the number of rules for each production
+	#
+	for(i:=1; i<nprod; i++)
+		temp1[i] = len prdptr[i] - 2;
+	arout("yyr2", temp1, nprod);
+
+	aryfil(temp1, nstate, -1000);
+	for(i=0; i<=ntokens; i++)
+		for(j:=tstates[i]; j!=0; j=mstates[j])
+			temp1[j] = i;
+	for(i=0; i<=nnonter; i++)
+		for(j=ntstates[i]; j!=0; j=mstates[j])
+			temp1[j] = -i;
+	arout("yychk", temp1, nstate);
+	arout("yydef", defact, nstate);
+
+	# put out token translation tables
+	# table 1 has 0-256
+	aryfil(temp1, 256, 0);
+	c := 0;
+	for(i=1; i<=ntokens; i++) {
+		j = tokset[i].value;
+		if(j >= 0 && j < 256) {
+			if(temp1[j]) {
+				print("yacc bug -- cant have 2 different Ts with same value\n");
+				print("	%s and %s\n", tokset[i].name, tokset[temp1[j]].name);
+				nerrors++;
+			}
+			temp1[j] = i;
+			if(j > c)
+				c = j;
+		}
+	}
+	for(i = 0; i <= c; i++)
+		if(temp1[i] == 0)
+			temp1[i] = YYLEXUNK;
+	arout("yytok1", temp1, c+1);
+
+	# table 2 has PRIVATE-PRIVATE+256
+	aryfil(temp1, 256, 0);
+	c = 0;
+	for(i=1; i<=ntokens; i++) {
+		j = tokset[i].value - PRIVATE;
+		if(j >= 0 && j < 256) {
+			if(temp1[j]) {
+				print("yacc bug -- cant have 2 different Ts with same value\n");
+				print("	%s and %s\n", tokset[i].name, tokset[temp1[j]].name);
+				nerrors++;
+			}
+			temp1[j] = i;
+			if(j > c)
+				c = j;
+		}
+	}
+	arout("yytok2", temp1, c+1);
+
+	# table 3 has everything else
+	ftable.puts("yytok3 := array[] of {\n");
+	c = 0;
+	for(i=1; i<=ntokens; i++) {
+		j = tokset[i].value;
+		if(j >= 0 && j < 256)
+			continue;
+		if(j >= PRIVATE && j < 256+PRIVATE)
+			continue;
+
+		ftable.puts(sprint("%4d,%4d,", j, i));
+		c++;
+		if(c%5 == 0)
+			ftable.putc('\n');
+	}
+	ftable.puts(sprint("%4d\n};\n", 0));
+
+	# copy parser text
+	while((c=finput.getc()) != Bufio->EOF) {
+		if(c == '$') {
+			if((c = finput.getc()) != 'A')
+				ftable.putc('$');
+			else { # copy actions
+				if(codehead == nil)
+					ftable.puts("* => ;");
+				else
+					dumpcode(-1);
+				c = finput.getc();
+			}
+		}
+		ftable.putc(c);
+	}
+	ftable.close();
+}
+
+arout(s: string, v: array of int, n: int)
+{
+	ftable.puts(s+" := array[] of {");
+	for(i := 0; i < n; i++) {
+		if(i%10 == 0)
+			ftable.putc('\n');
+		ftable.puts(sprint("%4d", v[i]));
+		ftable.putc(',');
+	}
+	ftable.puts("\n};\n");
+}
+
+#
+# output the summary on y.output
+#
+summary()
+{
+	if(foutput != nil) {
+		foutput.puts("\n" + string ntokens + " terminals, " + string(nnonter + 1) + " nonterminals\n");
+		foutput.puts("" + string nprod + " grammar rules, " + string nstate + "/" + string NSTATES + " states\n");
+		foutput.puts("" + string zzsrconf + " shift/reduce, " + string zzrrconf + " reduce/reduce conflicts reported\n");
+		foutput.puts("" + string len wsets + " working sets used\n");
+		foutput.puts("memory: parser " + string memp + "/" + string ACTSIZE + "\n");
+		foutput.puts(string (zzclose - 2*nstate) + " extra closures\n");
+		foutput.puts(string zzacent + " shift entries, " + string zzexcp + " exceptions\n");
+		foutput.puts(string zzgoent + " goto entries\n");
+		foutput.puts(string zzgobest + " entries saved by goto default\n");
+	}
+	if(zzsrconf != 0 || zzrrconf != 0) {
+		print("\nconflicts: ");
+		if(zzsrconf)
+			print("%d shift/reduce", zzsrconf);
+		if(zzsrconf && zzrrconf)
+			print(", ");
+		if(zzrrconf)
+			print("%d reduce/reduce", zzrrconf);
+		print("\n");
+	}
+	if(fdefine != nil)
+		fdefine.close();
+}
+
+#
+# write optimizer summary
+#
+osummary()
+{
+	if(foutput == nil)
+		return;
+	i := 0;
+	for(p := maxa; p >= 0; p--)
+		if(amem[p] == 0)
+			i++;
+
+	foutput.puts("Optimizer space used: output " + string (maxa+1) + "/" + string ACTSIZE + "\n");
+	foutput.puts(string(maxa+1) + " table entries, " + string i + " zero\n");
+	foutput.puts("maximum spread: " + string maxspr + ", maximum offset: " + string maxoff + "\n");
+}
+
+#
+# copies and protects "'s in q
+#
+chcopy(q: string): string
+{
+	s := "";
+	j := 0;
+	for(i := 0; i < len q; i++) {
+		if(q[i] == '"') {
+			s += q[j:i] + "\\";
+			j = i;
+		}
+	}
+	return s + q[j:i];
+}
+
+usage()
+{
+	fprint(stderr, "usage: yacc [-vd] [-Dn] [-o output] [-s stem] file\n");
+	exit;
+}
+
+bitset(set: Lkset, bit: int): int
+{
+	return set[bit>>5] & (1<<(bit&31));
+}
+
+setbit(set: Lkset, bit: int): int
+{
+	return set[bit>>5] |= (1<<(bit&31));
+}
+
+mkset(): Lkset
+{
+	return array[tbitset] of {* => 0};
+}
+
+#
+# set a to the union of a and b
+# return 1 if b is not a subset of a, 0 otherwise
+#
+setunion(a, b: array of int): int
+{
+	sub := 0;
+	for(i:=0; i<tbitset; i++) {
+		x := a[i];
+		y := x | b[i];
+		a[i] = y;
+		if(y != x)
+			sub = 1;
+	}
+	return sub;
+}
+
+prlook(p: Lkset)
+{
+	if(p == nil){
+		foutput.puts("\tNULL");
+		return;
+	}
+	foutput.puts(" { ");
+	for(j:=0; j<=ntokens; j++){
+		if(bitset(p, j)){
+			foutput.puts(symnam(j));
+			foutput.putc(' ');
+		}
+	}
+	foutput.putc('}');
+}
+
+#
+# utility routines
+#
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isword(c: int): int
+{
+	return c >= 16ra0 || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
+}
+
+mktemp(t: string): string
+{
+	return t;
+}
+
+#
+# arg processing
+#
+Arg.init(argv: list of string): ref Arg
+{
+	if(argv != nil)
+		argv = tl argv;
+	return ref Arg(argv, 0, "");
+}
+
+Arg.opt(arg: self ref Arg): int
+{
+	opts := arg.opts;
+	if(opts != ""){
+		arg.c = opts[0];
+		arg.opts = opts[1:];
+		return arg.c;
+	}
+	argv := arg.argv;
+	if(argv == nil)
+		return arg.c = 0;
+	opts = hd argv;
+	if(len opts < 2 || opts[0] != '-')
+		return arg.c = 0;
+	arg.argv = tl argv;
+	if(opts == "--")
+		return arg.c = 0;
+	arg.opts = opts[2:];
+	return arg.c = opts[1];
+}
+
+Arg.arg(arg: self ref Arg): string
+{
+	s := arg.opts;
+	arg.opts = "";
+	if(s != "")
+		return s;
+	argv := arg.argv;
+	if(argv == nil)
+		return "";
+	arg.argv = tl argv;
+	return hd argv;
+}
--- /dev/null
+++ b/appl/cmd/mash/eyaccpar
@@ -1,0 +1,223 @@
+YYFLAG: con -1000;
+
+# parser for yacc output
+YYENV: adt
+{
+	yylval:	ref YYSTYPE;	# lexical value
+	yyval:	YYSTYPE;		# goto value
+	yyenv:	YYETYPE;		# useer environment
+	yynerrs:	int;			# number of errors
+	yyerrflag:	int;			# error recovery flag
+	yysys:	Sys;
+	yystderr:	ref Sys->FD;
+};
+
+yytokname(yyc: int): string
+{
+	if(yyc > 0 && yyc <= len yytoknames && yytoknames[yyc-1] != nil)
+		return yytoknames[yyc-1];
+	return "<"+string yyc+">";
+}
+
+yystatname(yys: int): string
+{
+	if(yys >= 0 && yys < len yystates && yystates[yys] != nil)
+		return yystates[yys];
+	return "<"+string yys+">\n";
+}
+
+yylex1(e: ref YYENV): int
+{
+	c, yychar : int;
+	yychar = yyelex(e);
+	if(yychar <= 0)
+		c = yytok1[0];
+	else if(yychar < len yytok1)
+		c = yytok1[yychar];
+	else if(yychar >= YYPRIVATE && yychar < YYPRIVATE+len yytok2)
+		c = yytok2[yychar-YYPRIVATE];
+	else{
+		n := len yytok3;
+		c = 0;
+		for(i := 0; i < n; i+=2) {
+			if(yytok3[i+0] == yychar) {
+				c = yytok3[i+1];
+				break;
+			}
+		}
+		if(c == 0)
+			c = yytok2[1];	# unknown char
+	}
+	if(yydebug >= 3)
+		e.yysys->fprint(e.yystderr, "lex %.4ux %s\n", yychar, yytokname(c));
+	return c;
+}
+
+YYS: adt
+{
+	yyv: YYSTYPE;
+	yys: int;
+};
+
+yyparse(): int
+{
+	return yyeparse(nil);
+}
+
+yyeparse(e: ref YYENV): int
+{
+	if(e == nil)
+		e = ref YYENV;
+	if(e.yylval == nil)
+		e.yylval = ref YYSTYPE;
+	if(e.yysys == nil) {
+		e.yysys = load Sys "$Sys";
+		e.yystderr = e.yysys->fildes(2);
+	}
+
+	yys := array[YYMAXDEPTH] of YYS;
+
+	yystate := 0;
+	yychar := -1;
+	e.yynerrs = 0;
+	e.yyerrflag = 0;
+	yyp := -1;
+	yyn := 0;
+
+yystack:
+	for(;;){
+		# put a state and value onto the stack
+		if(yydebug >= 4)
+			e.yysys->fprint(e.yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+
+		yyp++;
+		if(yyp >= YYMAXDEPTH) {
+			yyerror(e, "yacc stack overflow");
+			yyn = 1;
+			break yystack;
+		}
+		yys[yyp].yys = yystate;
+		yys[yyp].yyv = e.yyval;
+
+		for(;;){
+			yyn = yypact[yystate];
+			if(yyn > YYFLAG) {	# simple state
+				if(yychar < 0)
+					yychar = yylex1(e);
+				yyn += yychar;
+				if(yyn >= 0 && yyn < YYLAST) {
+					yyn = yyact[yyn];
+					if(yychk[yyn] == yychar) { # valid shift
+						yychar = -1;
+						yyp++;
+						if(yyp >= YYMAXDEPTH) {
+							yyerror(e, "yacc stack overflow");
+							yyn = 1;
+							break yystack;
+						}
+						yystate = yyn;
+						yys[yyp].yys = yystate;
+						yys[yyp].yyv = *e.yylval;
+						if(e.yyerrflag > 0)
+							e.yyerrflag--;
+						if(yydebug >= 4)
+							e.yysys->fprint(e.yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+						continue;
+					}
+				}
+			}
+		
+			# default state action
+			yyn = yydef[yystate];
+			if(yyn == -2) {
+				if(yychar < 0)
+					yychar = yylex1(e);
+		
+				# look through exception table
+				for(yyxi:=0;; yyxi+=2)
+					if(yyexca[yyxi] == -1 && yyexca[yyxi+1] == yystate)
+						break;
+				for(yyxi += 2;; yyxi += 2) {
+					yyn = yyexca[yyxi];
+					if(yyn < 0 || yyn == yychar)
+						break;
+				}
+				yyn = yyexca[yyxi+1];
+				if(yyn < 0){
+					yyn = 0;
+					break yystack;
+				}
+			}
+
+			if(yyn != 0)
+				break;
+
+			# error ... attempt to resume parsing
+			if(e.yyerrflag == 0) { # brand new error
+				yyerror(e, "syntax error");
+				e.yynerrs++;
+				if(yydebug >= 1) {
+					e.yysys->fprint(e.yystderr, "%s", yystatname(yystate));
+					e.yysys->fprint(e.yystderr, "saw %s\n", yytokname(yychar));
+				}
+			}
+
+			if(e.yyerrflag != 3) { # incompletely recovered error ... try again
+				e.yyerrflag = 3;
+	
+				# find a state where "error" is a legal shift action
+				while(yyp >= 0) {
+					yyn = yypact[yys[yyp].yys] + YYERRCODE;
+					if(yyn >= 0 && yyn < YYLAST) {
+						yystate = yyact[yyn];  # simulate a shift of "error"
+						if(yychk[yystate] == YYERRCODE) {
+							yychar = -1;
+							continue yystack;
+						}
+					}
+	
+					# the current yyp has no shift on "error", pop stack
+					if(yydebug >= 2)
+						e.yysys->fprint(e.yystderr, "error recovery pops state %d, uncovers %d\n",
+							yys[yyp].yys, yys[yyp-1].yys );
+					yyp--;
+				}
+				# there is no state on the stack with an error shift ... abort
+				yyn = 1;
+				break yystack;
+			}
+
+			# no shift yet; clobber input char
+			if(yydebug >= 2)
+				e.yysys->fprint(e.yystderr, "error recovery discards %s\n", yytokname(yychar));
+			if(yychar == YYEOFCODE) {
+				yyn = 1;
+				break yystack;
+			}
+			yychar = -1;
+			# try again in the same state
+		}
+	
+		# reduction by production yyn
+		if(yydebug >= 2)
+			e.yysys->fprint(e.yystderr, "reduce %d in:\n\t%s", yyn, yystatname(yystate));
+	
+		yypt := yyp;
+		yyp -= yyr2[yyn];
+#		yyval = yys[yyp+1].yyv;
+		yym := yyn;
+	
+		# consult goto table to find next state
+		yyn = yyr1[yyn];
+		yyg := yypgo[yyn];
+		yyj := yyg + yys[yyp].yys + 1;
+	
+		if(yyj >= YYLAST || yychk[yystate=yyact[yyj]] != -yyn)
+			yystate = yyact[yyg];
+		case yym {
+			$A
+		}
+	}
+
+	return yyn;
+}
--- /dev/null
+++ b/appl/cmd/mash/history.b
@@ -1,0 +1,206 @@
+implement Mashbuiltin;
+
+#
+#	"history" builtin, defines:
+#
+
+include	"mash.m";
+include	"mashparse.m";
+
+mashlib:	Mashlib;
+chanfill:	ChanFill;
+
+Env:		import mashlib;
+sys, bufio:	import mashlib;
+
+Iobuf:	import bufio;
+
+Hcmd: adt
+{
+	seek:	int;
+	text:	array of byte;
+};
+
+Reader: adt
+{
+	fid:	int;
+	offset:	int;
+	hint:	int;
+	next:	cyclic ref Reader;
+};
+
+history:	array of ref Hcmd;
+lhist:		int;
+nhist:		int;
+seek:		int;
+readers:	ref Reader;
+eof :=		array[0] of byte;
+
+#
+#	Interface to catch the use as a command.
+#
+init(nil: ref Draw->Context, args: list of string)
+{
+	raise "fail: " + hd args + " not loaded";
+}
+
+#
+#	Used by whatis.
+#
+name(): string
+{
+	return "history";
+}
+
+#
+#	Install commands.
+#
+mashinit(nil: list of string, lib: Mashlib, nil: Mashbuiltin, e: ref Env)
+{
+	mashlib = lib;
+	if (mashlib->histchan != nil)
+		return;
+	mashlib->startserve = 1;
+	nhist = 0;
+	lhist = 256;
+	history = array[lhist] of ref Hcmd;
+	seek = 0;
+	(f, c) := e.servefile(mashlib->HISTF);
+	spawn servehist(f, c);
+	(f, c) = e.servefile(mashlib->MASHF);
+	spawn servemash(f, c);
+}
+
+mashcmd(nil: ref Env, nil: list of string)
+{
+}
+
+addhist(b: array of byte)
+{
+	if (nhist == lhist) {
+		n := 3 * nhist / 4;
+		part := history[:n];
+		part[:] = history[nhist - n:];
+		nhist = n;
+	}
+	history[nhist] = ref Hcmd(seek, b);
+	nhist++;
+	seek += len b;
+}
+
+getfid(fid: int, del: int): ref Reader
+{
+	prev: ref Reader;
+	for (r := readers; r != nil; r = r.next) {
+		if (r.fid == fid) {
+			if (del) {
+				if (prev == nil)
+					readers = r.next;
+				else
+					prev.next = r.next;
+				return nil;
+			}
+			return r;
+		}
+		prev = r;
+	}
+	o := 0;
+	if (nhist > 0)
+		o = history[0].seek;
+	return readers = ref Reader(fid, o, 0, readers);
+}
+
+readhist(off, count, fid: int): (array of byte, string)
+{
+	r := getfid(fid, 0);
+	off += r.offset;
+	if (nhist == 0 || off >= seek)
+		return (eof, nil);
+	i := r.hint;
+	if (i >= nhist)
+		i = nhist - 1;
+	s := history[i].seek;
+	if (off == s) {
+		r.hint = i + 1;
+		return (history[i].text, nil);
+	}
+	if (off > s) {
+		do {
+			if (++i == nhist)
+				break;
+			s = history[i].seek;
+		} while (off >= s);
+		i--;
+	} else {
+		do {
+			if (--i < 0)
+				return (eof, "data truncated");
+			s = history[i].seek;
+		} while (off < s);
+	}
+	r.hint = i + 1;
+	b := history[i].text;
+	if (off != s)
+		b = b[off - s:];
+	return (b, nil);
+}
+
+loadhist(data: array of byte, fid: int, wc: Sys->Rwrite, c: ref Sys->FileIO)
+{
+	in: ref Iobuf;
+	if (chanfill == nil)
+		chanfill = load ChanFill ChanFill->PATH;
+	if (chanfill != nil)
+		in = chanfill->init(data, fid, wc, c, mashlib->bufio);
+	if (in == nil) {
+		in = bufio->sopen(string data);
+		if (in == nil) {
+			wc <-= (0, mashlib->errstr());
+			return;
+		}
+		wc <-= (len data, nil);
+	}
+	while ((s := in.gets('\n')) != nil)
+		addhist(array of byte s);
+	in.close();
+}
+
+servehist(f: string, c: ref Sys->FileIO)
+{
+	mashlib->reap();
+	h := chan of array of byte;
+	mashlib->histchan = h;
+	for (;;) {
+		alt {
+		b := <-h =>
+			addhist(b);
+		(off, count, fid, rc) := <-c.read =>
+			if (rc == nil) {
+				getfid(fid, 1);
+				continue;
+			}
+			rc <-= readhist(off, count, fid);
+		(off, data, fid, wc) := <-c.write =>
+			if (wc != nil)
+				loadhist(data, fid, wc, c);
+		}
+	}
+}
+
+servemash(f: string, c: ref Sys->FileIO)
+{
+	mashlib->reap();
+	for (;;) {
+		alt {
+		(off, count, fid, rc) := <-c.read =>
+			if (rc != nil)
+				rc <-= (nil, "not supported");
+		(off, data, fid, wc) := <-c.write =>
+			if (wc != nil) {
+				wc <-= (len data, nil);
+				if (mashlib->servechan != nil && len data > 0)
+					mashlib->servechan <-= data;
+			}
+		}
+	}
+}
--- /dev/null
+++ b/appl/cmd/mash/lex.b
@@ -1,0 +1,547 @@
+#
+#	Lexical analyzer.
+#
+
+lexdebug	: con 0;
+
+#
+#	Import tokens from parser.
+#
+Land,
+Lat,
+Lbackq,
+Lcaret,
+Lcase,
+Lcolon,
+Lcolonmatch,
+Lcons,
+Ldefeq,
+Lelse,
+Leof,
+Leq,
+Leqeq,
+Lerror,
+Lfn,
+Lfor,
+Lgreat,
+Lgreatgreat,
+Lhd,
+Lif,
+Lin,
+Llen,
+Lless,
+Llessgreat,
+Lmatch,
+Lmatched,
+Lnot,
+Lnoteq,
+Loffcurly,
+Loffparen,
+Loncurly,
+Lonparen,
+Lpipe,
+Lquote,
+Lrescue,
+Lsemi,
+Ltl,
+Lwhile,
+Lword
+	: import Mashparse;
+
+KWSIZE:	con 31;	# keyword hashtable size
+NCTYPE:	con 128;	# character class array size
+
+ALPHA,
+NUMERIC,
+ONE,
+WS,
+META
+	:	con 1 << iota;
+
+keywords := array[] of
+{
+	("case",	Lcase),
+	("else",	Lelse),
+	("fn",		Lfn),
+	("for",	Lfor),
+	("hd",	Lhd),
+	("if",		Lif),
+	("in",		Lin),
+	("len",	Llen),
+	("rescue",	Lrescue),
+	("tl",		Ltl),
+	("while",	Lwhile)
+};
+
+ctype := array[NCTYPE] of
+{
+	0 or ' ' or '\t' or '\n' or '\r' or '\v' => WS,
+	':' or '#' or ';' or '&' or '|' or '^' or '$' or '=' or '@'
+	 	or '~'  or '`'or '{' or '}' or '(' or ')' or '<' or '>' => ONE,
+	'a' to 'z' or 'A' to 'Z' or '_' => ALPHA,
+	'0' to '9' => NUMERIC,
+	'*' or '[' or ']' or '?' => META,
+	* => 0
+};
+
+keytab:	ref HashTable;
+
+#
+#	Initialize hashtable.
+#
+initlex()
+{
+	keytab = hash->new(KWSIZE);
+	for (i := 0; i < len keywords; i++) {
+		(s, v) := keywords[i];
+		keytab.insert(s, HashVal(v, 0.0, nil));
+	}
+}
+
+#
+#	Keyword value, or -1.
+#
+keyval(i: ref Item): int
+{
+	if (i.op != Iword)
+		return -1;
+	w := i.word;
+	if (w.flags & Wquoted)
+		return -1;
+	v := keytab.find(w.text);
+	if (v == nil)
+		return -1;
+	return v.i;
+}
+
+#
+#	Attach a source file to an environment.
+#
+Env.fopen(e: self ref Env, fd: ref Sys->FD, s: string)
+{
+	in := bufio->fopen(fd, Bufio->OREAD);
+	if (in == nil)
+		e.error(sys->sprint("could not fopen %s: %r\n", s));
+	e.file = ref File(in, s, 1, 0);
+}
+
+#
+#	Attach a source string to an environment.
+#
+Env.sopen(e: self ref Env, s: string)
+{
+	in := bufio->sopen(s);
+	if (in == nil)
+		e.error(sys->sprint("Bufio->sopen failed: %r\n"));
+	e.file = ref File(in, "<string>", 1, 0);
+}
+
+#
+#	Close source file.
+#
+fclose(e: ref Env, c: int)
+{
+	if (c == Bufio->ERROR)
+		readerror(e, e.file);
+	e.file.in.close();
+	e.file = nil;
+}
+
+#
+#	Character class routines.
+#
+
+isalpha(c: int): int
+{
+	return c >= NCTYPE || (c >= 0 && (ctype[c] & ALPHA) != 0);
+}
+
+isalnum(c: int): int
+{
+	return c >= NCTYPE || (c >= 0 && (ctype[c] & (ALPHA | NUMERIC)) != 0);
+}
+
+isdigit(c: int): int
+{
+	return c >= 0 && c < NCTYPE && (ctype[c] & NUMERIC) != 0;
+}
+
+isquote(c: int): int
+{
+	return c < NCTYPE && (c < 0 || (ctype[c] & (ONE | WS | META)) != 0);
+}
+
+isspace(c: int): int
+{
+	return c >= 0 && c < NCTYPE && (ctype[c] & WS) != 0;
+}
+
+isterm(c: int): int
+{
+	return c < NCTYPE && (c < 0 || (ctype[c] & (ONE | WS)) != 0);
+}
+
+#
+#	Test for an identifier.
+#
+ident(s: string): int
+{
+	if (s == nil || !isalpha(s[0]))
+		return 0;
+	n := len s;
+	for (x := 1; x < n; x++) {
+		if (!isalnum(s[x]))
+			return 0;
+	}
+	return 1;
+}
+
+#
+#	Quote text.
+#
+enquote(s: string): string
+{
+	r := "'";
+	j := 1;
+	n := len s;
+	for (i := 0; i < n; i++) {
+		c := s[i];
+		if (c == '\'' || c == '\\')
+			r[j++] = '\\';
+		r[j++] = c;
+	}
+	r[j] = '\'';
+	return r;
+}
+
+#
+#	Quote text if needed.
+#
+quote(s: string): string
+{
+	n := len s;
+	for (i := 0; i < n; i++) {
+		if (isquote(s[i]))
+			return enquote(s);
+	}
+	return s;
+}
+
+#
+#	Test for single word and identifier.
+#
+Item.sword(i: self ref Item, e: ref Env): ref Item
+{
+	if (i.op == Iword && ident(i.word.text))
+		return i;
+	e.report("malformed identifier: " + i.text());
+	return nil;
+}
+
+readerror(e: ref Env, f: ref File)
+{
+	sys->fprint(e.stderr, "error reading %s: %r\n", f.name);
+}
+
+where(e: ref Env): string
+{
+	if ((e.flags & EInter) || e.file == nil)
+		return nil;
+	return e.file.name + ":" + string e.file.line + ": ";
+}
+
+#
+#	Suck input (on error).
+#
+Env.suck(e: self ref Env)
+{
+	if (e.file == nil)
+		return;
+	in := e.file.in;
+	while ((c := in.getc()) >= 0 && c != '\n')
+		;
+}
+
+#
+#	Lexical analyzer.
+#
+Env.lex(e: self ref Env, yylval: ref Mashparse->YYSTYPE): int
+{
+	i, r: ref Item;
+reader:
+	for (;;) {
+		if (e.file == nil)
+			return -1;
+		f := e.file;
+		in := f.in;
+		while (isspace(c := in.getc())) {
+			if (c == '\n')
+				f.line++;
+		}
+		if (c < 0) {
+			fclose(e, c);
+			return Leof;
+		}
+		case c {
+		':' =>
+			if ((d := in.getc()) == ':')
+				return Lcons;
+			if (d == '=')
+				return Ldefeq;
+			if (d == '~')
+				return Lcolonmatch;
+			if (d >= 0)
+				in.ungetc();
+			return Lcolon;
+		'#' =>
+			for (;;) {
+				if ((c = in.getc()) < 0) {
+					fclose(e, c);
+					return Leof;
+				}
+				if (c == '\n') {
+					f.line++;
+					continue reader;
+				}
+			}
+		';' =>
+			return Lsemi;
+		'&' =>
+			return Land;
+		'|' =>
+			return Lpipe;
+		'^' =>
+			return Lcaret;
+		'@' =>
+			return Lat;
+		'!' =>
+			if ((d := in.getc()) == '=')
+				return Lnoteq;
+			if (d >= 0)
+				in.ungetc();
+			return Lnot;
+		'~' =>
+			return Lmatch;
+		'=' =>
+			if ((d := in.getc()) == '>')
+				return Lmatched;
+			if (d == '=')
+				return Leqeq;
+			if (d >= 0)
+				in.ungetc();
+			return Leq;
+		'`' =>
+			return Lbackq;
+		'"' =>
+			return Lquote;
+		'{' =>
+			return Loncurly;
+		'}' =>
+			return Loffcurly;
+		'(' =>
+			return Lonparen;
+		')' =>
+			return Loffparen;
+		'<' =>
+			if ((d := in.getc()) == '>')
+				return Llessgreat;
+			if (d >= 0)
+				in.ungetc();
+			return Lless;
+		'>' =>
+			if ((d := in.getc()) == '>')
+				return Lgreatgreat;
+			if (d >= 0)
+				in.ungetc();
+			return Lgreat;
+		'\\' =>
+			if ((d := in.getc()) == '\n') {
+				f.line++;
+				continue reader;
+			}
+			if (d >= 0)
+				in.ungetc();
+		}
+		# Loop over "carets for free".
+		for (;;) {
+			if (c == '$')
+				(i, c) = getdollar(f);
+			else
+				(i, c) = getword(e, f, c);
+			if (i == nil)
+				return Lerror;
+			if (isterm(c) && c != '$')
+				break;
+			if (r != nil)
+				r = ref Item(Iicaret, nil, r, i, nil, nil);
+			else
+				r = i;
+		}
+		if (c >= 0)
+			in.ungetc();
+		if (r != nil)
+			yylval.item = ref Item(Iicaret, nil, r, i, nil, nil);
+		else if ((c = keyval(i)) >= 0)
+			return c;
+		else
+			yylval.item = i;
+		return Lword;
+	}
+}
+
+#
+#	Get $n or $word.
+#
+getdollar(f: ref File): (ref Item, int)
+{
+	s: string;
+	in := f.in;
+	l := f.line;
+	o := Idollar;
+	if (isdigit(c := in.getc())) {
+		s[0] = c;
+		n := 1;
+		while (isdigit(c = in.getc()))
+			s[n++] = c;
+		o = Imatch;
+	} else {
+		if (c == '"') {
+			o = Idollarq;
+			c = in.getc();
+		}
+		if (isalpha(c)) {
+			s[0] = c;
+			n := 1;
+			while (isalnum(c = in.getc()))
+				s[n++] = c;
+		} else {
+			if (o == Idollar)
+				s = "$";
+			else
+				s = "$\"";
+			o = Iword;
+		}
+	}
+	return (ref Item(o, ref Word(s, 0, Src(l, f.name)), nil, nil, nil, nil), c);
+}
+
+#
+#	Get word with quoting.
+#
+getword(e: ref Env, f: ref File, c: int): (ref Item, int)
+{
+	s: string;
+	in := f.in;
+	l := f.line;
+	wf := 0;
+	n := 0;
+	if (c == '\'') {
+		wf = Wquoted;
+	collect:
+		while ((c = in.getc()) >= 0) {
+			case c {
+			'\'' =>
+				c = in.getc();
+				break collect;
+			'\\' =>
+				c = in.getc();
+				if (c != '\'' && c != '\\') {
+					if (c == '\n')
+						continue collect;
+					if (c >= 0)
+						in.ungetc();
+					c = '\\';
+				}
+			'\n' =>
+				f.line++;
+				e.report("newline in quoted word");
+				return (nil, 0);
+			}
+			s[n++] = c;
+		}
+	} else {
+		do {
+			case c {
+			'*' or '[' or '?' =>
+				wf |= Wexpand;
+			}
+			s[n++] = c;
+		} while (!isterm(c = in.getc()) && c != '\'');
+	}
+	if (lexdebug && s == "exit")
+		exit;
+	return (ref Item(Iword, ref Word(s, wf, Src(l, f.name)), nil, nil, nil, nil), c);
+}
+
+#
+#	Get a line, mapping escape newline to space newline.
+#
+getline(in: ref Bufio->Iobuf): string
+{
+	if (inchan != nil) {
+		alt {
+		b := <-inchan =>
+			if (inchan == nil)
+				return nil;
+			s := string b;
+			n := len s;
+			if (n > 1) {
+				while (s[n - 2] == '\\' && s[n - 1] == '\n') {
+					s[n - 2] = ' ';
+					s[n - 1] = ' ';
+					prprompt(1);
+					b = <-inchan;
+					if (b == nil)
+						break;
+					s += string b;
+					n = len s;
+				}
+			}
+			return s;
+		b := <-servechan =>
+			s := string b;
+			sys->print("%s", s);
+			return s;
+		}
+	} else {
+		s := in.gets('\n');
+		if (s == nil)
+			return nil;
+		n := len s;
+		if (n > 1) {
+			while (s[n - 2] == '\\' && s[n - 1] == '\n') {
+				s[n - 2] = ' ';
+				s[n - 1] = ' ';
+				prprompt(1);
+				t := in.gets('\n');
+				if (t == nil)
+					break;
+				s += t;
+				n = len s;
+			}
+		}
+		return s;
+	}
+}
+
+#
+#	Interactive shell loop.
+#
+Env.interactive(e: self ref Env, fd: ref Sys->FD)
+{
+	in := bufio->fopen(fd, Sys->OREAD);
+	if (in == nil)
+		e.error(sys->sprint("could not fopen stdin: %r\n"));
+	e.flags |= EInter;
+	for (;;) {
+		prprompt(0);
+		if (startserve)
+			e.serve();
+		if ((s := getline(in)) == nil)
+			exitmash();
+		e.sopen(s);
+		parse->parse(e);
+		if (histchan != nil)
+			histchan <-= array of byte s;
+	}
+}
--- /dev/null
+++ b/appl/cmd/mash/make.b
@@ -1,0 +1,723 @@
+implement Mashbuiltin;
+
+#
+#	"make" builtin, defines:
+#
+#	depends	- print dependencies
+#	make		- make-like command
+#	match	- print details of rule matches
+#	rules		- print rules
+#
+
+include	"mash.m";
+include	"mashparse.m";
+
+verbose:	con 0;	# debug output
+
+mashlib:	Mashlib;
+
+Cmd, Env, Item, Stab:	import mashlib;
+Depend, Rule, Target:	import mashlib;
+sys, bufio, hash:		import mashlib;
+
+Iobuf:	import bufio;
+
+#
+#	Interface to catch the use as a command.
+#
+init(nil: ref Draw->Context, args: list of string)
+{
+	raise "fail: " + hd args + " not loaded";
+}
+
+#
+#	Used by whatis.
+#
+name(): string
+{
+	return "make";
+}
+
+#
+#	Install commands.
+#
+mashinit(nil: list of string, lib: Mashlib, this: Mashbuiltin, e: ref Env)
+{
+	mashlib = lib;
+	e.defbuiltin("depends", this);
+	e.defbuiltin("make", this);
+	e.defbuiltin("match", this);
+	e.defbuiltin("rules", this);
+}
+
+#
+#	Execute a builtin.
+#
+mashcmd(e: ref Env, l: list of string)
+{
+	s := hd l;
+	l = tl l;
+	case s {
+	"depends" =>
+		out := e.outfile();
+		if (out == nil)
+			return;
+		if (l == nil)
+			alldeps(out);
+		else
+			depends(out, l);
+		out.close();
+	"make" =>
+		domake(e, l);
+	"match" =>
+		domatch(e, l);
+	"rules" =>
+		out := e.outfile();
+		if (out == nil)
+			return;
+		if (l == nil)
+			allrules(out);
+		else
+			rules(out, l);
+		out.close();
+	}
+}
+
+#
+#	Node states.
+#
+SUnknown, SNoexist, SExist, SStale, SMade, SDir, SDirload
+	: con iota;
+
+#
+#	Node flags.
+#
+#	FMark	- marked as in progress
+#
+FMark
+	: con 1 << iota;
+
+Node: adt
+{
+	name:	string;
+	state:		int;
+	flags:		int;
+	mtime:	int;
+};
+
+#
+#	Step in implicit chain.
+#
+Step:	type (ref Rule, array of string, ref Node);
+
+#
+#	Implicit match.
+#
+Match: adt
+{
+	node:	ref Node;
+	path:		list of Step;
+};
+
+NSIZE:	con 127;	# node hash size
+DSIZE:	con 32;	# number of dir entries for read
+
+ntab:		array of list of ref Node;	# node hash table
+
+initnodes()
+{
+	ntab = array[NSIZE] of list of ref Node;
+}
+
+#
+#	Find node for a pathname.
+#
+getnode(s: string): ref Node
+{
+	h := hash->fun1(s, NSIZE);
+	for (l := ntab[h]; l != nil; l = tl l) {
+		n := hd l;
+		if (n.name == s)
+			return n;
+	}
+	r := ref Node(s, SUnknown, 0, 0);
+	ntab[h] = r :: ntab[h];
+	return r;
+}
+
+#
+#	Make a pathname from a dir and an entry.
+#
+mkpath(d, s: string): string
+{
+	if (d == ".")
+		return s;
+	else if (d == "/")
+		return "/" + s;
+	else
+		return d + "/" + s;
+}
+
+#
+#	Load a directory.
+#
+loaddir(s: string)
+{
+	if (verbose)
+		sys->print("loaddir %s\n", s);
+	fd := sys->open(s, Sys->OREAD);
+	if (fd == nil)
+		return;
+	for (;;) {
+		(c, dbuf) := sys->dirread(fd);
+		if(c <= 0)
+			break;
+		for (i := 0; i < c; i++) {
+			n := getnode(mkpath(s, dbuf[i].name));
+			if (dbuf[i].mode & Sys->DMDIR)
+				n.state = SDir;
+			else
+				n.state = SExist;
+			n.mtime = dbuf[i].mtime;
+		}
+	}
+}
+
+#
+#	Load a file.  Get its node, maybe stat it or loaddir.
+#
+loadfile(s: string): ref Node
+{
+	n := getnode(s);
+	if (n.state == SUnknown) {
+		if (verbose)
+			sys->print("stat %s\n", s);
+		(ok, d) := sys->stat(s);
+		if (ok >= 0) {
+			n.mtime = d.mtime;
+			if (d.mode & Sys->DMDIR) {
+				loaddir(s);
+				n.state = SDirload;
+			} else
+				n.state = SExist;
+		} else
+			n.state = SNoexist;
+	} else if (n.state == SDir) {
+		loaddir(s);
+		n.state = SDirload;
+	}
+	return n;
+}
+
+#
+#	Get the node for a file and load the directories in its path.
+#
+getfile(s: string): ref Node
+{
+	d: string;
+	n := len s;
+	while (n >= 2 && s[0:2] == "./") {
+		n -= 2;
+		s = s[2:];
+	}
+	if (n > 0 && s[0] == '/') {
+		d = "/";
+		s = s[1:];
+	} else
+		d = ".";
+	(nil, l) := sys->tokenize(s, "/");
+	for (;;) {
+		w := loadfile(d);
+		if (l == nil)
+			return w;
+		s = hd l;
+		l = tl l;
+		d = mkpath(d, s);
+	}
+}
+
+#
+#	If a dependency rule makes more than one target propogate SMade.
+#
+propagate(l: list of string)
+{
+	if (tl l == nil)
+		return ;
+	while (l != nil) {
+		s := hd l;
+		if (verbose)
+			sys->print("propogate to %s\n", s);
+		getfile(s).state = SMade;
+		l = tl l;
+	}
+}
+
+#
+#	Try to make a node, or mark it as stale.
+#	Return -1 on (reported) error, 0 on fail, 1 on success.
+#
+explicit(e: ref Env, t: ref Target, n: ref Node): int
+{
+	d: ref Depend;
+	for (l := t.depends; l != nil ; l = tl l) {
+		if ((hd l).op != Cnop) {
+			if (d != nil) {
+				e.report(sys->sprint("make: too many rules for %s", t.target));
+				return -1;
+			}
+			d = hd l;
+		}
+	}
+	for (l = t.depends; l != nil ; l = tl l) {
+		for (u := (hd l).depends; u != nil; u = tl u) {
+			s := hd u;
+			m := getfile(s);
+			x := make(e, m, s);
+			if (x < 0) {
+				sys->print("don't know how to make %s\n", s);
+				return x;
+			}
+			if (m.state == SMade || m.mtime > n.mtime) {
+				if (verbose)
+					sys->print("%s makes %s stale\n", s, t.target);
+				n.state = SStale;
+			}
+		}
+	}
+	if (d != nil) {
+		if (n.state == SNoexist || n.state == SStale) {
+			if (verbose)
+				sys->print("build %s with explicit rule\n", t.target);
+			e = e.copy();
+			e.flags |= mashlib->EEcho | Mashlib->ERaise;
+			e.flags &= ~mashlib->EInter;
+			d.cmd.xeq(e);
+			propagate(d.targets);
+			n.state = SMade;
+		} else if (verbose)
+			sys->print("%s up to date\n", t.target);
+		return 1;
+	}
+	return 0;
+}
+
+#
+#	Report multiple implicit chains of equal length.
+#
+multimatch(e: ref Env, n: ref Node, l: list of Match)
+{
+	e.report(sys->sprint("%d rules match for %s", len l, n.name));
+	f := e.stderr;
+	while (l != nil) {
+		m := hd l;
+		sys->fprint(f, "%s", m.node.name);
+		for (p := m.path; p != nil; p = tl p) {
+			(nil, nil, t) := hd p;
+			sys->fprint(f, " -> %s", t.name);
+		}
+		sys->fprint(f, "\n");
+		l = tl l;
+	}
+}
+
+cycle(e: ref Env, n: ref Node)
+{
+	e.report(sys->sprint("make: cycle in dependencies for target %s", n.name));
+}
+
+#
+#	Mark the nodes in an implicit chain.
+#
+markchain(e: ref Env, l: list of Step): int
+{
+	while (tl l != nil) {
+		(nil, nil, n) := hd l;
+		if (n.flags & FMark) {
+			cycle(e, n);
+			return 0;
+		}
+		n.flags |= FMark;
+		l = tl l;
+	}
+	return 1;
+}
+
+#
+#	Unmark the nodes in an implicit chain.
+#
+unmarkchain(l: list of Step): int
+{
+	while (tl l != nil) {
+		(nil, nil, n) := hd l;
+		n.flags &= ~FMark;
+		l = tl l;
+	}
+	return 1;
+}
+
+#
+#	Execute an implicit rule chain.
+#
+xeqmatch(e: ref Env, b, n: ref Node, l: list of Step): int
+{
+	if (!markchain(e, l))
+		return -1;
+	if (verbose)
+		sys->print("making %s for implicit rule chain\n", n.name);
+	e.args = nil;
+	x := make(e, n, n.name);
+	if (x < 0) {
+		sys->print("don't know how to make %s\n", n.name);
+		return x;
+	}
+	if (n.state == SMade || n.mtime > b.mtime || b.state == SStale) {
+		e = e.copy();
+		e.flags |= mashlib->EEcho | Mashlib->ERaise;
+		e.flags &= ~mashlib->EInter;
+		for (;;) {
+			(r, a, t) := hd l;
+			if (verbose)
+				sys->print("making %s with implicit rule\n", t.name);
+			e.args = a;
+			r.cmd.xeq(e);
+			t.state = SMade;
+			l = tl l;
+			if (l == nil)
+				break;
+			t.flags &= ~FMark;
+		}
+	} else
+		unmarkchain(l);
+	return 1;
+}
+
+#
+#	Find the shortest implicit rule chain.
+#
+implicit(e: ref Env, base: ref Node): int
+{
+	win, lose: list of Match;
+	l: list of ref Rule;
+	cand := Match(base, nil) :: nil;
+	do {
+		# cand - list of candidate chains
+		# lose - list of extended chains that lose
+		# win	 - list of extended chains that win
+		lose = nil;
+	match:
+		# for each candidate
+		for (c := cand; c != nil; c = tl c) {
+			(b, x) := hd c;
+			s := b.name;
+			# find rules that match end of chain
+			m := mashlib->rulematch(s);
+			l = nil;
+			# exclude rules already in the chain
+		exclude:
+			for (n := m; n != nil; n = tl n) {
+				r := hd n;
+				for (y := x; y != nil; y = tl y) {
+					(u, nil, nil) := hd y;
+					if (u == r)
+						continue exclude;
+				}
+				l = r :: l;
+			}
+			if (l == nil)
+				continue match;
+			(nil, t) := sys->tokenize(s, "/");
+			# for each new rule that matched
+			for (n = l; n != nil; n = tl n) {
+				r := hd n;
+				a := r.matches(t);
+				if (a == nil) {
+					e.report("rule match cock up");
+					return -1;
+				}
+				a[0] = s;
+				e.args = a;
+				# eval rhs
+				(v, nil, nil) := r.rhs.ieval2(e);
+				if (v == nil)
+					continue;
+				y := (r, a, b) :: x;
+				z := getfile(v);
+				# winner or loser
+				if (z.state != SNoexist || Target.find(v) != nil)
+					win = (z, y) :: win;
+				else
+					lose = (z, y) :: lose;
+			}
+		}
+		# winner should be unique
+		if (win != nil) {
+			if (tl win != nil) {
+				multimatch(e, base, win);
+				return -1;
+			} else {
+				(a, p) := hd win;
+				return xeqmatch(e, base, a, p);
+			}
+		}
+		# losers are candidates in next round
+		cand = lose;
+	} while (cand != nil);
+	return 0;
+}
+
+#
+#	Make a node (recursive).
+#	Return -1 on (reported) error, 0 on fail, 1 on success.
+#
+make(e: ref Env, n: ref Node, s: string): int
+{
+	if (n == nil)
+		n = getfile(s);
+	if (verbose)
+		sys->print("making %s\n", n.name);
+	if (n.state == SMade)
+		return 1;
+	if (n.flags & FMark) {
+		cycle(e, n);
+		return -1;
+	}
+	n.flags |= FMark;
+	t := Target.find(s);
+	if (t != nil) {
+		x := explicit(e, t, n);
+		if (x != 0) {
+			n.flags &= ~FMark;
+			return x;
+		}
+	}
+	x := implicit(e, n);
+	n.flags &= ~FMark;
+	if (x != 0)
+		return x;
+	if (n.state == SExist)
+		return 0;
+	return -1;
+}
+
+makelevel:	int = 0;	# count recursion
+
+#
+#	Make driver routine.  Maybe initialize and handle exceptions.
+#
+domake(e: ref Env, l: list of string)
+{
+	if ((e.flags & mashlib->ETop) == 0) {
+		e.report("make not at top level");
+		return;
+	}
+	inited := 0;
+	if (makelevel > 0)
+		inited = 1;
+	makelevel++;
+	if (l == nil)
+		l = "default" :: nil;
+	while (l != nil) {
+		s := hd l;
+		l = tl l;
+		if (s[0] == '-') {
+			case s {
+			"-clear" =>
+				mashlib->initdep();
+			* =>
+				e.report("make: unknown option: " + s);
+			}
+		} else {
+			if (!inited) {
+				initnodes();
+				inited = 1;
+			}
+			{
+				if (make(e, nil, s) < 0) {
+					sys->print("don't know how to make %s\n", s);
+					raise "fail: make error";
+				}
+			}exception x{
+			mashlib->FAILPAT =>
+				makelevel--;
+				raise x;
+			}
+		}
+	}
+	makelevel--;
+}
+
+#
+#	Print dependency/rule command.
+#
+prcmd(out: ref Iobuf, op: int, c: ref Cmd)
+{
+	if (op == Clistgroup)
+		out.putc(':');
+	if (c != nil) {
+		out.puts("{ ");
+		out.puts(c.text());
+		out.puts(" }");
+	} else
+		out.puts("{}");
+}
+
+#
+#	Print details of rule matches.
+#
+domatch(e: ref Env, l: list of string)
+{
+	out := e.outfile();
+	if (out == nil)
+		return;
+	e = e.copy();
+	while (l != nil) {
+		s := hd l;
+		out.puts(sys->sprint("%s:\n", s));
+		m := mashlib->rulematch(s);
+		(nil, t) := sys->tokenize(s, "/");
+		while (m != nil) {
+			r := hd m;
+			out.puts(sys->sprint("\tlhs %s\n", r.lhs.text));
+			a := r.matches(t);
+			if (a != nil) {
+				a[0] = s;
+				n := len a;
+				for (i := 0; i < n; i++)
+					out.puts(sys->sprint("\t$%d '%s'\n", i, a[i]));
+				e.args = a;
+				(v, w, nil) := r.rhs.ieval2(e);
+				if (v != nil)
+					out.puts(sys->sprint("\trhs '%s'\n", v));
+				else
+					out.puts(sys->sprint("\trhs list %d\n", len w));
+				if (r.cmd != nil) {
+					out.putc('\t');
+					prcmd(out, r.op, r.cmd);
+					out.puts(";\n");
+				}
+			} else
+				out.puts("\tcock up\n");
+			m = tl m;
+		}
+		l = tl l;
+	}
+	out.close();
+}
+
+#
+#	Print word list.
+#
+prwords(out: ref Iobuf, l: list of string, pre: int)
+{
+	while (l != nil) {
+		if (pre)
+			out.putc(' ');
+		out.puts(mashlib->quote(hd l));
+		if (!pre)
+			out.putc(' ');
+		l = tl l;
+	}
+}
+
+#
+#	Print dependency.
+#
+prdep(out: ref Iobuf, d: ref Depend)
+{
+	prwords(out, d.targets, 0);
+	out.putc(':');
+	prwords(out, d.depends, 1);
+	if (d.op != Cnop) {
+		out.putc(' ');
+		prcmd(out, d.op, d.cmd);
+	}
+	out.puts(";\n");
+}
+
+#
+#	Print all dependencies, avoiding duplicates.
+#
+alldep(out: ref Iobuf, d: ref Depend, pass: int)
+{
+	case pass {
+	0 =>
+		d.mark = 0;
+	1 =>
+		if (!d.mark) {
+			prdep(out, d);
+			d.mark = 1;
+		}
+	}
+}
+
+#
+#	Print all dependencies.
+#
+alldeps(out: ref Iobuf)
+{
+	a := mashlib->dephash;
+	n := len a;
+	for (p := 0; p < 2; p++)
+		for (i := 0; i < n; i++)
+			for (l := a[i]; l != nil; l = tl l)
+				for (d := (hd l).depends; d != nil; d = tl d)
+					alldep(out, hd d, p);
+}
+
+#
+#	Print dependencies.
+#
+depends(out: ref Iobuf, l: list of string)
+{
+	while (l != nil) {
+		s := hd l;
+		out.puts(s);
+		out.puts(":\n");
+		t := Target.find(s);
+		if (t != nil) {
+			for (d := t.depends; d != nil; d = tl d)
+				prdep(out, hd d);
+		}
+		l = tl l;
+	}
+}
+
+#
+#	Print rule.
+#
+prrule(out: ref Iobuf, r: ref Rule)
+{
+	out.puts(r.lhs.text);
+	out.puts(" :~ ");
+	out.puts(r.rhs.text());
+	out.putc(' ');
+	prcmd(out, r.op, r.cmd);
+	out.puts(";\n");
+}
+
+#
+#	Print all rules.
+#
+allrules(out: ref Iobuf)
+{
+	for (l := mashlib->rules; l != nil; l = tl l)
+		prrule(out, hd l);
+}
+
+#
+#	Print matching rules.
+#
+rules(out: ref Iobuf, l: list of string)
+{
+	while (l != nil) {
+		s := hd l;
+		out.puts(s);
+		out.puts(":\n");
+		r := mashlib->rulematch(s);
+		while (r != nil) {
+			prrule(out, hd r);
+			r = tl r;
+		}
+		l = tl l;
+	}
+}
--- /dev/null
+++ b/appl/cmd/mash/mash.b
@@ -1,0 +1,154 @@
+implement Mash;
+
+#
+#	mash - Inferno make/shell
+#
+#	Bruce Ellis - 1Q 98
+#
+
+include	"mash.m";
+include	"mashparse.m";
+
+#
+#	mash consists of three modules plus library modules and loadable builtins.
+#
+#	This module, Mash, loads the other two (Mashparse and Mashlib), loads
+#	the builtin "builtins", initializes things and calls the parser.
+#
+#	It has two entry points.  One is the traditional init() function and the other,
+#	tkinit, is an interface to WmMash that allows the "tk" builtin to cooperate
+#	with the command window.
+#
+
+Mash: module
+{
+	tkinit:	fn(ctxt: ref Draw->Context, top: ref Tk->Toplevel, args: list of string);
+	init:		fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+Iobuf:	import Bufio;
+
+sys:		Sys;
+lib:		Mashlib;
+parse:	Mashparse;
+
+Env, Stab:	import lib;
+
+cmd:		string;
+
+#
+#	Check for /dev/console.
+#
+isconsole(fd: ref Sys->FD): int
+{
+	(ok1, d1) := sys->fstat(fd);
+	(ok2, d2) := sys->stat(lib->CONSOLE);
+	if (ok1 < 0 || ok2 < 0)
+		return 0;
+	return d1.dtype == d2.dtype && d1.qid.path == d2.qid.path;
+}
+
+usage(e: ref Env)
+{
+	sys->fprint(e.stderr, "usage: mash [-denx] [-c command] [src [args]]\n");
+	lib->exits("usage");
+}
+
+flags(e: ref Env, l: list of string): list of string
+{
+	while (l != nil && len hd l && (s := hd l)[0] == '-') {
+		l = tl l;
+		if (s == "--")
+			break;
+		n := len s;
+		for (i := 1; i < n; i++) {
+			case s[i] {
+			'c' =>
+				if (++i < n) {
+					if (l != nil)
+						usage(e);
+					cmd = s[i:];
+				} else {
+					if (len l != 1)
+						usage(e);
+					cmd = hd l;
+				}
+				return nil;
+			'd' =>
+				e.flags |= lib->EDumping;
+			'e' =>
+				e.flags |= lib->ERaise;
+			'n' =>
+				e.flags |= lib->ENoxeq;
+			'x' =>
+				e.flags |= lib->EEcho;
+			* =>
+				usage(e);
+			}
+		}
+	}
+	return l;
+}
+
+tkinit(ctxt: ref Draw->Context, top: ref Tk->Toplevel, args: list of string)
+{
+	fd: ref Sys->FD;
+	sys = load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	lib = load Mashlib Mashlib->PATH;
+	if (lib == nil) {
+		sys->fprint(stderr, "could not load %s: %r\n", Mashlib->PATH);
+		exit;
+	}
+	parse = load Mashparse Mashparse->PATH;
+	if (parse == nil) {
+		sys->fprint(stderr, "could not load %s: %r\n", Mashparse->PATH);
+		exit;
+	}
+	e := Env.new();
+	e.stderr = stderr;
+	stderr = nil;
+	lib->initmash(ctxt, top, sys, e, lib, parse);
+	parse->init(lib);
+	boot := args == nil;
+	if (!boot)
+		args = flags(e, tl args);
+	e.doload(lib->LIB + lib->BUILTINS);
+	lib->prompt = "mash% ";
+	lib->contin = "\t";
+	if (cmd == nil && args == nil && !boot) {
+		e.global.assign(lib->MASHINIT, "true" :: nil);
+		fd = sys->open(lib->PROFILE, Sys->OREAD);
+		if (fd != nil) {
+			e.fopen(fd, lib->PROFILE);
+			parse->parse(e);
+			fd = nil;
+		}
+	}
+	e.global.assign(lib->MASHINIT, nil);
+	if (cmd == nil) {
+		if (args != nil) {
+			s := hd args;
+			args = tl args;
+			fd = sys->open(s, Sys->OREAD);
+			if (fd == nil)
+				e.couldnot("open", s);
+			e.fopen(fd, s);
+			e.global.assign(lib->ARGS, args);
+		}
+		if (fd == nil) {
+			fd = sys->fildes(0);
+			if (isconsole(fd))
+				e.interactive(fd);
+			e.fopen(fd, "<stdin>");
+			fd = nil;
+		}
+	} else
+		e.sopen(cmd);
+	parse->parse(e);
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	tkinit(ctxt, nil, args);
+}
--- /dev/null
+++ b/appl/cmd/mash/mash.m
@@ -1,0 +1,372 @@
+include	"sys.m";
+include	"bufio.m";
+include	"draw.m";
+include	"hash.m";
+include	"filepat.m";
+include	"regex.m";
+include	"sh.m";
+include	"string.m";
+include	"tk.m";
+
+#
+#	mash - Inferno make/shell
+#
+#	Bruce Ellis - 1Q 98
+#
+
+	Rin,
+	Rout,
+	Rappend,
+	Rinout,
+	Rcount
+		: con iota;	# Redirections
+
+	Icaret,
+	Iicaret,
+	Idollar,
+	Idollarq,
+	Imatch,
+	Iword,
+	Iexpr,
+	Ibackq,
+	Iquote,
+	Iinpipe,
+	Ioutpipe,
+	Iredir
+		: con iota;	# Items
+
+	Csimple,
+	Cseq,
+	Cfor,
+	Cif,
+	Celse,
+	Cwhile,
+	Ccase,
+	Ccases,
+	Cmatched,
+	Cdefeq,
+	Ceq,
+	Cfn,
+	Crescue,
+	Casync,
+	Cgroup,
+	Clistgroup,
+	Csubgroup,
+	Cnop,
+	Cword,
+	Clist,
+	Ccaret,
+	Chd,
+	Clen,
+	Cnot,
+	Ctl,
+	Ccons,
+	Ceqeq,
+	Cnoteq,
+	Cmatch,
+	Cpipe,
+	Cdepend,
+	Crule,
+	Cprivate
+		: con iota;	# Commands
+
+	Svalue,
+	Sfunc,
+	Sbuiltin
+		: con iota;	# Symbol types
+
+Mashlib: module
+{
+	PATH:	con "/dis/lib/mashlib.dis";
+
+	File: adt
+	{
+		in:	ref Bufio->Iobuf;
+		name:	string;
+		line:	int;
+		eof:	int;
+	};
+
+	Src: adt
+	{
+		line:	int;
+		file:	string;
+	};
+
+	Wquoted,
+	Wexpand
+		: con 1 << iota;
+
+	Word: adt
+	{
+		text:	string;
+		flags:	int;
+		where:	Src;
+
+		word:	fn(w: self ref Word, d: string): string;
+	};
+
+	Item: adt
+	{
+		op:		int;
+		word:		ref Word;
+		left, right:	ref Item;
+		cmd:		ref Cmd;
+		redir:		ref Redir;
+
+		item1:		fn(op: int, l: ref Item): ref Item;
+		item2:		fn(op: int, l, r: ref Item): ref Item;
+		itemc:		fn(op: int, c: ref Cmd): ref Item;
+		iteml:		fn(l: list of string): ref Item;
+		itemr:		fn(op: int, i: ref Item): ref Item;
+		itemw:		fn(s: string): ref Item;
+
+		caret:		fn(i: self ref Item, e: ref Env): (string, list of string, int);
+		ieval:		fn(i: self ref Item, e: ref Env): (string, list of string, int);
+		ieval1:		fn(i: self ref Item, e: ref Env): ref Item;
+		ieval2:		fn(i: self ref Item, e: ref Env): (string, list of string, int);
+		reval:		fn(i: self ref Item, e: ref Env): (int, string);
+		sword:		fn(i: self ref Item, e: ref Env): ref Item;
+		text:		fn(i: self ref Item): string;
+	};
+
+	Redir: adt
+	{
+		op:	int;
+		word:	ref Item;
+	};
+
+	Cmd: adt
+	{
+		op:		int;
+		words:		cyclic list of ref Item;
+		left, right:	cyclic ref Cmd;
+		item:		cyclic ref Item;
+		redirs:		cyclic list of ref Redir;
+		value:		list of string;
+		error:		int;
+
+		cmd1:		fn(op: int, l: ref Cmd): ref Cmd;
+		cmd2:		fn(op: int, l, r: ref Cmd): ref Cmd;
+		cmd1i:		fn(op: int, l: ref Cmd, i: ref Item): ref Cmd;
+		cmd1w:		fn(op: int, l: ref Cmd, w: list of ref Item): ref Cmd;
+		cmde:		fn(c: self ref Cmd, op: int, l, r: ref Cmd): ref Cmd;
+		cmdiw:		fn(op: int, i: ref Item, w: list of ref Item): ref Cmd;
+
+		assign:		fn(c: self ref Cmd, e: ref Env, def: int);
+		checkpipe:	fn(c: self ref Cmd, e: ref Env, f: int): int;
+		cmdio:		fn(c: self ref Cmd, e: ref Env, i: ref Item);
+		depend:		fn(c: self ref Cmd, e: ref Env);
+		eeval:		fn(c: self ref Cmd, e: ref Env): (string, list of string);
+		eeval1:		fn(c: self ref Cmd, e: ref Env): ref Cmd;
+		eeval2:		fn(c: self ref Cmd, e: ref Env): (string, list of string, int);
+		evaleq:		fn(c: self ref Cmd, e: ref Env): int;
+		evalmatch:	fn(c: self ref Cmd, e: ref Env): int;
+		mkcmd:		fn(c: self ref Cmd, e: ref Env, async: int): ref Cmd;
+		quote:		fn(c: self ref Cmd, e: ref Env, back: int): ref Item;
+		rotcases:	fn(c: self ref Cmd): ref Cmd;
+		rule:		fn(c: self ref Cmd, e: ref Env);
+		serve:		fn(c: self ref Cmd, e: ref Env, write: int): ref Item;
+		simple:		fn(c: self ref Cmd, e: ref Env, wait: int);
+		text:		fn(c: self ref Cmd): string;
+		truth:		fn(c: self ref Cmd, e: ref Env): int;
+		xeq:		fn(c: self ref Cmd, e: ref Env);
+		xeqit:		fn(c: self ref Cmd, e: ref Env, wait: int);
+	};
+
+	Depend: adt
+	{
+		targets:	list of string;
+		depends:	list of string;
+		op:		int;
+		cmd:		ref Cmd;
+		mark:		int;
+	};
+
+	Target: adt
+	{
+		target:		string;
+		depends:	list of ref Depend;
+
+		find:		fn(s: string): ref Target;
+	};
+
+	Lhs: adt
+	{
+		text:	string;
+		elems:	list of string;
+		count:	int;
+	};
+
+	Rule: adt
+	{
+		lhs:	ref Lhs;
+		rhs:	ref Item;
+		op:	int;
+		cmd:	ref Cmd;
+
+		match:		fn(r: self ref Rule, a, n: int, t: list of string): int;
+		matches:	fn(r: self ref Rule, t: list of string): array of string;
+	};
+
+	SHASH:	con 31;			# Symbol table hash size
+	SMASK:	con 16r7FFFFFFF;	# Mask for SHASH bits
+
+	Symb: adt
+	{
+		name:		string;
+		value:		list of string;
+		func:		ref Cmd;
+		builtin:	Mashbuiltin;
+		tag:		int;
+	};
+
+	Stab: adt
+	{
+		tab:		array of list of ref Symb;
+		wmask:	int;
+		copy:		int;
+
+		new:		fn(): ref Stab;
+		clone:		fn(t: self ref Stab): ref Stab;
+		all:		fn(t: self ref Stab): list of ref Symb;
+		assign:		fn(t: self ref Stab, s: string, v: list of string);
+		defbuiltin:	fn(t: self ref Stab, s: string, b: Mashbuiltin);
+		define:		fn(t: self ref Stab, s: string, f: ref Cmd);
+		find:		fn(t: self ref Stab, s: string): ref Symb;
+		func:		fn(t: self ref Stab, s: string): ref Cmd;
+		update:		fn(t: self ref Stab, s: string, tag: int, v: list of string, f: ref Cmd, b: Mashbuiltin): ref Symb;
+	};
+
+	ETop, EInter, EEcho, ERaise, EDumping, ENoxeq:
+		con 1 << iota;
+
+	Env: adt
+	{
+		global:		ref Stab;
+		local:		ref Stab;
+		flags:		int;
+		in, out:	ref Sys->FD;
+		stderr:		ref Sys->FD;
+		wait:		ref Sys->FD;
+		file:		ref File;
+		args:		array of string;
+		level:		int;
+
+		new:		fn(): ref Env;
+		clone:		fn(e: self ref Env): ref Env;
+		copy:		fn(e: self ref Env): ref Env;
+
+		interactive:	fn(e: self ref Env, fd: ref Sys->FD);
+
+		arg:		fn(e: self ref Env, s: string): string;
+		builtin:	fn(e: self ref Env, s: string): Mashbuiltin;
+		defbuiltin:	fn(e: self ref Env, s: string, b: Mashbuiltin);
+		define:		fn(e: self ref Env, s: string, f: ref Cmd);
+		dollar:		fn(e: self ref Env, s: string): ref Symb;
+		func:		fn(e: self ref Env, s: string): ref Cmd;
+		let:		fn(e: self ref Env, s: string, v: list of string);
+		set:		fn(e: self ref Env, s: string, v: list of string);
+
+		couldnot:	fn(e: self ref Env, what, who: string);
+		diag:		fn(e: self ref Env, s: string): string;
+		error:		fn(e: self ref Env, s: string);
+		report:		fn(e: self ref Env, s: string);
+		sopen:		fn(e: self ref Env, s: string);
+		suck:		fn(e: self ref Env);
+		undefined:	fn(e: self ref Env, s: string);
+		usage:		fn(e: self ref Env, s: string);
+
+		devnull:	fn(e: self ref Env): ref Sys->FD;
+		fopen:		fn(e: self ref Env, fd: ref Sys->FD, s: string);
+		outfile:	fn(e: self ref Env): ref Bufio->Iobuf;
+		output:		fn(e: self ref Env, s: string);
+		pipe:		fn(e: self ref Env): array of ref Sys->FD;
+		runit:		fn(e: self ref Env, s: list of string, in, out: ref Sys->FD, wait: int);
+		serve:		fn(e: self ref Env);
+		servefd:	fn(e: self ref Env, fd: ref Sys->FD, write: int): string;
+		servefile:	fn(e: self ref Env, n: string): (string, ref Sys->FileIO);
+
+		doload:		fn(e: self ref Env, s: string);
+		lex:		fn(e: self ref Env, y: ref Mashparse->YYSTYPE): int;
+		mklist:		fn(e: self ref Env, l: list of ref Item): list of ref Item;
+		mksimple:	fn(e: self ref Env, l: list of ref Item): ref Cmd;
+	};
+
+	initmash:	fn(ctxt: ref Draw->Context, top: ref Tk->Toplevel, s: Sys, e: ref Env, l: Mashlib, p: Mashparse);
+	nonexistent:	fn(s: string): int;
+
+	errstr:		fn(): string;
+	exits:		fn(s: string);
+	ident:		fn(s: string): int;
+	initdep:	fn();
+	prepareio:	fn(in, out: ref sys->FD): (int, ref Sys->FD);
+	prprompt:	fn(n: int);
+	quote:		fn(s: string): string;
+	reap:		fn();
+	revitems:	fn(l: list of ref Item): list of ref Item;
+	revstrs:	fn(l: list of string): list of string;
+	rulematch:	fn(s: string): list of ref Rule;
+
+	ARGS:		con "args";
+	BUILTINS:	con "builtins.dis";
+	CHAN:		con "/chan";
+	CONSOLE:	con "/dev/cons";
+	DEVNULL:	con "/dev/null";
+	EEXISTS:	con "file exists";
+	EPIPE:		con "write on closed pipe";
+	EXIT:		con "exit";
+	FAILPAT:	con "fail:*";
+	FAIL:		con "fail:";
+	FAILLEN:	con len FAIL;
+	HISTF:		con "history";
+	LIB:		con "/dis/lib/mash/";
+	MASHF:		con "mash";
+	MASHINIT:	con "mashinit";
+	PROFILE:	con "/lib/mashinit";
+	TRUE:		con "true";
+	MAXELEV:	con 256;
+
+	sys:		Sys;
+	bufio:		Bufio;
+	filepat:	Filepat;
+	hash:		Hash;
+	regex:		Regex;
+	str:		String;
+	tk:		Tk;
+
+	gctxt:		ref Draw->Context;
+	gtop:		ref Tk->Toplevel;
+
+	prompt:		string;
+	contin:		string;
+
+	empty:		list of string;
+
+	PIDEXIT:	con 0;
+
+	histchan:	chan of array of byte;
+	inchan:		chan of array of byte;
+	pidchan:	chan of int;
+	servechan:	chan of array of byte;
+	startserve:	int;
+
+	rules:		list of ref Rule;
+	dephash:	array of list of ref Target;
+
+	parse:		Mashparse;
+};
+
+#
+#	Interface to loadable builtin modules.  mashinit is called when a module
+#	is loaded.  mashcmd is called for a builtin as defined by Env.defbuiltin().
+#	init() is in the interface to catch the use of builtin modules as commands.
+#	name() is used by whatis.
+#
+Mashbuiltin: module
+{
+	mashinit:	fn(l: list of string, lib: Mashlib, this: Mashbuiltin, e: ref Mashlib->Env);
+	mashcmd:	fn(e: ref Mashlib->Env, l: list of string);
+	init:		fn(ctxt: ref Draw->Context, args: list of string);
+	name:		fn(): string;
+};
--- /dev/null
+++ b/appl/cmd/mash/mash.y
@@ -1,0 +1,269 @@
+%{
+include	"mash.m";
+
+#
+#	mash parser.  Thread safe.
+#
+%}
+
+%module Mashparse
+{
+	PATH:	con "/dis/lib/mashparse.dis";
+
+	init:		fn(l: Mashlib);
+	parse:	fn(e: ref Mashlib->Env);
+
+	YYSTYPE: adt
+	{
+		cmd:		ref Mashlib->Cmd;
+		item:		ref Mashlib->Item;
+		items:	list of ref Mashlib->Item;
+		flag:		int;
+	};
+
+	YYETYPE:	type ref Mashlib->Env;
+}
+
+%{
+	lib:		Mashlib;
+
+	Cmd, Item, Stab, Env:		import lib;
+%}
+
+%left				Lcase Lfor Lif Lwhile Loffparen	# low prec
+%left				Lelse
+%left				Lpipe
+%left				Leqeq Lmatch Lnoteq
+%right			Lcons
+%left				Lcaret
+%left				Lnot Lhd Ltl Llen
+%type	<flag>	term
+%type	<item>	item wgen witem word redir sword
+%type	<items>	asimple list
+%type	<cmd>	case cases cmd cmda cmds cmdt complex
+%type	<cmd>	epilog expr cbrace cobrace obrace simple
+%token	<item>	Lword
+%token			Lbackq Lcolon Lcolonmatch Ldefeq Leq Lmatched Lquote
+%token			Loncurly Lonparen Loffcurly Loffparen Lat
+%token			Lgreat Lgreatgreat Lless Llessgreat
+%token			Lfn Lin Lrescue
+%token			Land Leof Lsemi
+%token			Lerror
+
+%%
+
+script	: tcmds
+		;
+
+tcmds	: # empty
+		| tcmds xeq
+		;
+
+xeq		: cmda
+			{ $1.xeq(e.yyenv); }
+		| Leof
+		| error
+		;
+
+cmdt		: # empty
+			{ $$ = nil; }
+		| cmdt cmda
+			{ $$ = Cmd.cmd2(Cseq, $1, $2); }
+		;
+
+cmda	: cmd term
+			{ $$ = $1.mkcmd(e.yyenv, $2); }
+		;
+
+cmds		: cmdt
+		| cmdt cmd
+			{ $$ = Cmd.cmd2(Cseq, $1, $2.mkcmd(e.yyenv, 0)); }
+		;
+
+cmd		: simple
+		| complex
+		| cmd Lpipe cmd
+			{  $$ = Cmd.cmd2(Cpipe, $1, $3); }
+		;
+
+simple	: asimple
+			{ $$ = e.yyenv.mksimple($1); }
+		| asimple Lcolon list cobrace
+			{
+				$4.words = e.yyenv.mklist($3);
+				$$ = Cmd.cmd1w(Cdepend, $4, e.yyenv.mklist($1));
+			}
+		;
+
+complex	: Loncurly cmds Loffcurly epilog
+			{ $$ = $4.cmde(Cgroup, $2, nil); }
+		| Lat Loncurly cmds Loffcurly epilog
+			{ $$ = $5.cmde(Csubgroup, $3, nil); }
+		| Lfor Lonparen sword Lin list Loffparen cmd
+			{ $$ = Cmd.cmd1i(Cfor, $7, $3); $$.words = lib->revitems($5); }
+		| Lif Lonparen expr Loffparen cmd
+			{ $$ = Cmd.cmd2(Cif, $3, $5); }
+		| Lif Lonparen expr Loffparen cmd Lelse cmd
+			{ $$ = Cmd.cmd2(Cif, $3, Cmd.cmd2(Celse, $5, $7)); }
+		| Lwhile Lonparen expr Loffparen cmd
+			{ $$ = Cmd.cmd2(Cwhile, $3, $5); }
+		| Lcase expr Loncurly cases Loffcurly
+			{ $$ = Cmd.cmd2(Ccase, $2, $4.rotcases()); }
+		| sword Leq list
+			{ $$ = Cmd.cmdiw(Ceq, $1, $3); }
+		| sword Ldefeq list
+			{ $$ = Cmd.cmdiw(Cdefeq, $1, $3); }
+		| Lfn word obrace
+			{ $$ = Cmd.cmd1i(Cfn, $3, $2); }
+		| Lrescue word obrace
+			{ $$ = Cmd.cmd1i(Crescue, $3, $2); }
+		| word Lcolonmatch word cbrace
+			{
+				$4.item = $3;
+				$$ = Cmd.cmd1i(Crule, $4, $1);
+			}
+		;
+
+cbrace	: Lcolon Loncurly cmds Loffcurly
+			{ $$ = Cmd.cmd1(Clistgroup, $3); }
+		| Loncurly cmds Loffcurly
+			{ $$ = Cmd.cmd1(Cgroup, $2); }
+		;
+
+cobrace	: # empty
+			{ $$ = Cmd.cmd1(Cnop, nil); }
+		| cbrace
+		;
+
+obrace	: # empty
+			{ $$ = nil; }
+		| Loncurly cmds Loffcurly
+			{ $$ = $2; }
+		;
+
+cases		: # empty
+			{ $$ = nil; }
+		| cases case
+			{ $$ = Cmd.cmd2(Ccases, $1, $2); }
+		;
+
+case		: expr Lmatched cmda
+			{ $$ = Cmd.cmd2(Cmatched, $1, $3); }
+		;
+
+asimple	: word
+			{ $$ = $1 :: nil; }
+		| asimple item
+			{ $$ = $2 :: $1; }
+		;
+
+item		: witem
+		| redir
+		;
+
+witem	: word
+		| wgen
+		;
+
+wgen		: Lbackq Loncurly cmds Loffcurly
+			{ $$ = Item.itemc(Ibackq, $3); }
+		| Lquote Loncurly cmds Loffcurly
+			{ $$ = Item.itemc(Iquote, $3); }
+		| Lless Loncurly cmds Loffcurly
+			{ $$ = Item.itemc(Iinpipe, $3); }
+		| Lgreat Loncurly cmds Loffcurly
+			{ $$ = Item.itemc(Ioutpipe, $3); }
+		;
+
+word		: Lword
+		| word Lcaret word
+			{ $$ = Item.item2(Icaret, $1, $3); }
+		| Lonparen expr Loffparen
+			{ $$ = Item.itemc(Iexpr, $2); }
+		;
+
+sword	: Lword
+			{ $$ = $1.sword(e.yyenv); }
+		;
+
+list		: # empty
+			{ $$ = nil; }
+		| list witem
+			{ $$ = $2 :: $1; }
+		;
+
+epilog	: # empty
+			{ $$ = ref Cmd; $$.error = 0; }
+		| epilog redir
+			{ $$ = $1; $1.cmdio(e.yyenv, $2); }
+		;
+
+redir		: Lless word
+			{ $$ = Item.itemr(Rin, $2); }
+		| Lgreat word
+			{ $$ = Item.itemr(Rout, $2); }
+		| Lgreatgreat word
+			{ $$ = Item.itemr(Rappend, $2); }
+		| Llessgreat word
+			{ $$ = Item.itemr(Rinout, $2); }
+		;
+
+term		: Lsemi
+			{ $$ = 0; }
+		| Leof
+			{ $$ = 0; }
+		| Land
+			{ $$ = 1; }
+		;
+
+expr		: Lword
+			{ $$ = Cmd.cmd1i(Cword, nil, $1); }
+		| wgen
+			{ $$ = Cmd.cmd1i(Cword, nil, $1); }
+		| Lonparen expr Loffparen
+			{ $$ = $2; }
+		| expr Lcaret expr
+			{ $$ = Cmd.cmd2(Ccaret, $1, $3); }
+		| Lhd expr
+			{ $$ = Cmd.cmd1(Chd, $2); }
+		| Ltl expr
+			{ $$ = Cmd.cmd1(Ctl, $2); }
+		| Llen expr
+			{ $$ = Cmd.cmd1(Clen, $2); }
+		| Lnot expr
+			{ $$ = Cmd.cmd1(Cnot, $2); }
+		| expr Lcons expr
+			{ $$ = Cmd.cmd2(Ccons, $1, $3); }
+		| expr Leqeq expr
+			{ $$ = Cmd.cmd2(Ceqeq, $1, $3); }
+		| expr Lnoteq expr
+			{ $$ = Cmd.cmd2(Cnoteq, $1, $3); }
+		| expr Lmatch expr
+			{ $$ = Cmd.cmd2(Cmatch, $1, $3); }
+		;
+%%
+
+init(l: Mashlib)
+{
+	lib = l;
+}
+
+parse(e: ref Env)
+{
+	y := ref YYENV;
+	y.yyenv = e;
+	y.yysys = lib->sys;
+	y.yystderr = e.stderr;
+	yyeparse(y);
+}
+
+yyerror(e: ref YYENV, s: string)
+{
+	e.yyenv.report(s);
+	e.yyenv.suck();
+}
+
+yyelex(e: ref YYENV): int
+{
+	return e.yyenv.lex(e.yylval);
+}
--- /dev/null
+++ b/appl/cmd/mash/mashfile
@@ -1,0 +1,36 @@
+make -clear;
+lflags = -wg;
+
+fn lc {
+	limbo $lflags $args;
+};
+
+libsrc = depends.b dump.b exec.b expr.b lex.b misc.b serve.b symb.b xeq.b;
+bus = builtins.dis tk.dis make.dis history.dis;
+core = mash.dis mashlib.dis mashparse.dis;
+
+bulib = /dis/lib/mash;
+bulibs = $bulib/$bus;
+
+mashparse.b mashparse.m : mash.y
+{
+	eyacc -vd mash.y;
+	mv y.tab.m mashparse.m;
+	mv y.tab.b mashparse.b;
+};
+
+*.dis			:~ $1.b { lc $1.b };
+$bulib/*.dis	:~ $1.dis { cp $1.dis $bulib };
+/dis/*.dis		:~ $1.dis { cp $1.dis /dis };
+/dis/lib/*.dis	:~ $1.dis { cp $1.dis /dis/lib };
+
+$core $bus : mash.m mashparse.m;
+mashlib.dis :  $libsrc;
+
+insbu : $bulibs {};
+insdis : /dis/mash.dis /dis/lib/mashlib.dis /dis/lib/mashparse.dis {};
+
+all : eyacc.dis mash.dis mashlib.dis mashparse.dis $bus {};
+install : insbu insdis {};
+
+clean : { rm mashparse.b mashparse.m *.dis };
--- /dev/null
+++ b/appl/cmd/mash/mashlib.b
@@ -1,0 +1,60 @@
+implement Mashlib;
+
+#
+#	Mashlib	- All of the real work except for the parsing.
+#
+
+include	"mash.m";
+include	"mashparse.m";
+
+Iobuf:			import bufio;
+HashTable, HashVal:	import hash;
+
+include	"depends.b";
+include	"dump.b";
+include	"exec.b";
+include	"expr.b";
+include	"lex.b";
+include	"misc.b";
+include	"serve.b";
+include	"symb.b";
+include	"xeq.b";
+
+lib:		Mashlib;
+
+initmash(ctxt: ref Draw->Context, top: ref Tk->Toplevel, s: Sys, e: ref Env, l: Mashlib, p: Mashparse)
+{
+	gctxt = ctxt;
+	gtop = top;
+	sys = s;
+	lib = l;
+	parse = p;
+	if (top != nil) {
+		tk =  load Tk Tk->PATH;
+		if (tk == nil)
+			e.couldnot("load", Tk->PATH);
+	}
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		e.couldnot("load", Bufio->PATH);
+	hash = load Hash Hash->PATH;
+	if (hash == nil)
+		e.couldnot("load", Hash->PATH);
+	str = load String String->PATH;
+	if (str == nil)
+		e.couldnot("load", String->PATH);
+	initlex();
+	empty = "no" :: "value" :: nil;
+	startserve = 0;
+}
+
+nonexistent(e: string): int
+{
+	errs := array[] of {"does not exist", "directory entry not found"};
+	for (i := 0; i < len errs; i++){
+		j := len errs[i];
+		if (j <= len e && e[len e-j:] == errs[i])
+			return 1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/appl/cmd/mash/mashparse.b
@@ -1,0 +1,662 @@
+implement Mashparse;
+
+#line	2	"mash.y"
+include	"mash.m";
+
+#
+#	mash parser.  Thread safe.
+#
+Mashparse: module {
+
+	PATH:	con "/dis/lib/mashparse.dis";
+
+	init:		fn(l: Mashlib);
+	parse:	fn(e: ref Mashlib->Env);
+
+	YYSTYPE: adt
+	{
+		cmd:		ref Mashlib->Cmd;
+		item:		ref Mashlib->Item;
+		items:	list of ref Mashlib->Item;
+		flag:		int;
+	};
+
+	YYETYPE:	type ref Mashlib->Env;
+Lcase: con	57346;
+Lfor: con	57347;
+Lif: con	57348;
+Lwhile: con	57349;
+Loffparen: con	57350;
+Lelse: con	57351;
+Lpipe: con	57352;
+Leqeq: con	57353;
+Lmatch: con	57354;
+Lnoteq: con	57355;
+Lcons: con	57356;
+Lcaret: con	57357;
+Lnot: con	57358;
+Lhd: con	57359;
+Ltl: con	57360;
+Llen: con	57361;
+Lword: con	57362;
+Lbackq: con	57363;
+Lcolon: con	57364;
+Lcolonmatch: con	57365;
+Ldefeq: con	57366;
+Leq: con	57367;
+Lmatched: con	57368;
+Lquote: con	57369;
+Loncurly: con	57370;
+Lonparen: con	57371;
+Loffcurly: con	57372;
+Lat: con	57373;
+Lgreat: con	57374;
+Lgreatgreat: con	57375;
+Lless: con	57376;
+Llessgreat: con	57377;
+Lfn: con	57378;
+Lin: con	57379;
+Lrescue: con	57380;
+Land: con	57381;
+Leof: con	57382;
+Lsemi: con	57383;
+Lerror: con	57384;
+
+};
+
+#line	28	"mash.y"
+	lib:		Mashlib;
+
+	Cmd, Item, Stab, Env:		import lib;
+YYEOFCODE: con 1;
+YYERRCODE: con 2;
+YYMAXDEPTH: con 150;
+
+#line	244	"mash.y"
+
+
+init(l: Mashlib)
+{
+	lib = l;
+}
+
+parse(e: ref Env)
+{
+	y := ref YYENV;
+	y.yyenv = e;
+	y.yysys = lib->sys;
+	y.yystderr = e.stderr;
+	yyeparse(y);
+}
+
+yyerror(e: ref YYENV, s: string)
+{
+	e.yyenv.report(s);
+	e.yyenv.suck();
+}
+
+yyelex(e: ref YYENV): int
+{
+	return e.yyenv.lex(e.yylval);
+}
+yyexca := array[] of {-1, 1,
+	1, -1,
+	-2, 0,
+-1, 2,
+	1, 1,
+	-2, 0,
+-1, 21,
+	24, 51,
+	25, 51,
+	-2, 48,
+};
+YYNPROD: con 75;
+YYPRIVATE: con 57344;
+yytoknames: array of string;
+yystates: array of string;
+yydebug: con 0;
+YYLAST:	con 249;
+yyact := array[] of {
+   7,  20,   4,  49,  41,  47, 110,  65, 103,  95,
+  17,  24,  32, 112,  33, 146, 142,  38,  39,  28,
+  59,  60, 140, 129,  40,  64,  22,  46,  63,  35,
+  36,  34,  37, 128, 127,  38,  67,  69,  70,  71,
+  27,  26,  25,  76,  22,  75, 126, 111,  77,  74,
+  45,  80,  81,  38,  44,  78,  88,  89,  90,  91,
+  92,  68,  22,  98,  99,  93,  94,  32, 124,  33,
+  97, 106,  62, 107,  38,  39, 104, 108, 109, 104,
+  68,  40, 105,  22,  66, 105,  56, 143,  55, 116,
+ 117, 118, 119, 120,  73,  32,  32,  33,  33,  38,
+  39, 122, 132,  36, 131,  37,  40, 123,  22,  72,
+ 125,  56,  43,  55, 135, 136,  58,  57, 133,  62,
+ 134, 139,  38,   6,  62,  16,  13,  14,  15, 141,
+  66,  22,  96,  67,  69,  62,  32,  79,  33,  84,
+  83,  21,  24,  61, 147, 148, 144,  24, 149,  11,
+  22,   3,  12,  16,  13,  14,  15,  18,   2,  19,
+   1,   5,  85,  87,  86,  84,  83,   8, 101,  21,
+  54,  51,  52,  53,  48,  39,   9,  11,  22,  82,
+  12,  40,  42,  50, 137,  18,  56,  19,  55,  54,
+  51,  52,  53,  48,  39, 115, 138,  38,  39, 130,
+  40,  10,  50,  29,  40,  56,  22,  55, 102,  56,
+  31,  55,  85,  87,  86,  84,  83, 121,  23,  30,
+  85,  87,  86,  84,  83, 114,   0, 145,  85,  87,
+  86,  84,  83, 113,   0,   0,  85,  87,  86,  84,
+  83, 100,   0,   0,  85,  87,  86,  84,  83,
+};
+yypact := array[] of {
+-1000,-1000, 121,-1000,-1000,-1000,-1000,   1,-1000,-1000,
+  -3,-1000,  84,  25,  21,  -2, 173,  92,  15,  15,
+ 120,-1000, 173,-1000, 149,-1000,-1000,-1000,-1000,-1000,
+-1000,-1000, 109,-1000, 102,  33,  15,  15,-1000,  81,
+  66,  19, 149,-1000, 117, 173, 173, 151,-1000,-1000,
+ 173, 173, 173, 173, 173,  56,  52,-1000,-1000, 104,
+ 104,  15,  15, 233,-1000,  54,-1000, 109,-1000, 109,
+ 109, 109,-1000,-1000,-1000,-1000,   1,  17, -24,-1000,
+ 225, 217,-1000, 173, 173, 173, 173, 173, 209,-1000,
+-1000,-1000,-1000, 177, 177,-1000,-1000,-1000,  57,-1000,
+-1000,-1000,-1000,-1000,  40,-1000,  16,   4,   3,  -7,
+  70,-1000,-1000, 149, 149, 154,-1000, 125, 125, 125,
+ 125,-1000,  -8,-1000,-1000, -14,-1000,-1000,-1000,-1000,
+-1000,  15,  15,  70,  79, 137, 132,-1000,-1000, 201,
+-1000, -15,-1000, 149, 149, 149,-1000, 132, 132,-1000,
+};
+yypgo := array[] of {
+   0, 218, 203,   3, 208,   1, 199,  10, 201,   7,
+ 196, 195,   0,   2,   4, 182, 176,   6,   5,   8,
+ 168,   9, 167, 160, 158, 151,
+};
+yyr1 := array[] of {
+   0,  23,  24,  24,  25,  25,  25,  15,  15,  13,
+  14,  14,  12,  12,  12,  22,  22,  16,  16,  16,
+  16,  16,  16,  16,  16,  16,  16,  16,  16,  19,
+  19,  20,  20,  21,  21,  11,  11,  10,   8,   8,
+   2,   2,   4,   4,   3,   3,   3,   3,   5,   5,
+   5,   7,   9,   9,  17,  17,   6,   6,   6,   6,
+   1,   1,   1,  18,  18,  18,  18,  18,  18,  18,
+  18,  18,  18,  18,  18,
+};
+yyr2 := array[] of {
+   0,   1,   0,   2,   1,   1,   1,   0,   2,   2,
+   1,   2,   1,   1,   3,   1,   4,   4,   5,   7,
+   5,   7,   5,   5,   3,   3,   3,   3,   4,   4,
+   3,   0,   1,   0,   3,   0,   2,   3,   1,   2,
+   1,   1,   1,   1,   4,   4,   4,   4,   1,   3,
+   3,   1,   0,   2,   0,   2,   2,   2,   2,   2,
+   1,   1,   1,   1,   1,   3,   3,   2,   2,   2,
+   2,   3,   3,   3,   3,
+};
+yychk := array[] of {
+-1000, -23, -24, -25, -13,  40,   2, -12, -22, -16,
+  -8,  28,  31,   5,   6,   7,   4,  -7,  36,  38,
+  -5,  20,  29,  -1,  10,  41,  40,  39,  22,  -2,
+  -4,  -6,  -5,  -3,  34,  32,  33,  35,  20,  21,
+  27, -14, -15,  28,  29,  29,  29, -18,  20,  -3,
+  29,  17,  18,  19,  16,  34,  32,  25,  24,  -5,
+  -5,  23,  15, -18, -12,  -9,  28,  -5,  28,  -5,
+  -5,  -5,  28,  28,  30, -13, -12, -14,  -7,  20,
+ -18, -18,  28,  15,  14,  11,  13,  12, -18, -18,
+ -18, -18, -18,  -9,  -9, -21,  28, -21,  -5,  -5,
+   8, -20,  -4, -19,  22,  28, -14, -14, -14, -14,
+ -17,  30,  37,   8,   8, -11, -18, -18, -18, -18,
+ -18,   8, -14, -19,  28, -14,  30,  30,  30,  30,
+  -6,  34,  32, -17,  -9, -12, -12,  30, -10, -18,
+  30, -14,  30,   8,   9,  26,  30, -12, -12, -13,
+};
+yydef := array[] of {
+   2,  -2,  -2,   3,   4,   5,   6,   0,  12,  13,
+  15,   7,   0,   0,   0,   0,   0,   0,   0,   0,
+  38,  -2,   0,   9,   0,  60,  61,  62,  52,  39,
+  40,  41,  42,  43,   0,   0,   0,   0,  48,   0,
+   0,   0,  10,   7,   0,   0,   0,   0,  63,  64,
+   0,   0,   0,   0,   0,   0,   0,  52,  52,  33,
+  33,   0,   0,   0,  14,  31,   7,  56,   7,  57,
+  58,  59,   7,   7,  54,   8,  11,   0,   0,  51,
+   0,   0,  35,   0,   0,   0,   0,   0,   0,  67,
+  68,  69,  70,  24,  25,  26,   7,  27,   0,  49,
+  50,  16,  53,  32,   0,   7,   0,   0,   0,   0,
+  17,  54,  52,   0,   0,   0,  66,  71,  72,  73,
+  74,  65,   0,  28,   7,   0,  46,  47,  44,  45,
+  55,   0,   0,  18,   0,  20,  22,  23,  36,   0,
+  34,   0,  30,   0,   0,   0,  29,  19,  21,  37,
+};
+yytok1 := array[] of {
+   1,
+};
+yytok2 := array[] of {
+   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,
+  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,
+  22,  23,  24,  25,  26,  27,  28,  29,  30,  31,
+  32,  33,  34,  35,  36,  37,  38,  39,  40,  41,
+  42,
+};
+yytok3 := array[] of {
+   0
+};
+
+YYFLAG: con -1000;
+
+# parser for yacc output
+YYENV: adt
+{
+	yylval:	ref YYSTYPE;	# lexical value
+	yyval:	YYSTYPE;		# goto value
+	yyenv:	YYETYPE;		# useer environment
+	yynerrs:	int;			# number of errors
+	yyerrflag:	int;			# error recovery flag
+	yysys:	Sys;
+	yystderr:	ref Sys->FD;
+};
+
+yytokname(yyc: int): string
+{
+	if(yyc > 0 && yyc <= len yytoknames && yytoknames[yyc-1] != nil)
+		return yytoknames[yyc-1];
+	return "<"+string yyc+">";
+}
+
+yystatname(yys: int): string
+{
+	if(yys >= 0 && yys < len yystates && yystates[yys] != nil)
+		return yystates[yys];
+	return "<"+string yys+">\n";
+}
+
+yylex1(e: ref YYENV): int
+{
+	c, yychar : int;
+	yychar = yyelex(e);
+	if(yychar <= 0)
+		c = yytok1[0];
+	else if(yychar < len yytok1)
+		c = yytok1[yychar];
+	else if(yychar >= YYPRIVATE && yychar < YYPRIVATE+len yytok2)
+		c = yytok2[yychar-YYPRIVATE];
+	else{
+		n := len yytok3;
+		c = 0;
+		for(i := 0; i < n; i+=2) {
+			if(yytok3[i+0] == yychar) {
+				c = yytok3[i+1];
+				break;
+			}
+		}
+		if(c == 0)
+			c = yytok2[1];	# unknown char
+	}
+	if(yydebug >= 3)
+		e.yysys->fprint(e.yystderr, "lex %.4ux %s\n", yychar, yytokname(c));
+	return c;
+}
+
+YYS: adt
+{
+	yyv: YYSTYPE;
+	yys: int;
+};
+
+yyparse(): int
+{
+	return yyeparse(nil);
+}
+
+yyeparse(e: ref YYENV): int
+{
+	if(e == nil)
+		e = ref YYENV;
+	if(e.yylval == nil)
+		e.yylval = ref YYSTYPE;
+	if(e.yysys == nil) {
+		e.yysys = load Sys "$Sys";
+		e.yystderr = e.yysys->fildes(2);
+	}
+
+	yys := array[YYMAXDEPTH] of YYS;
+
+	yystate := 0;
+	yychar := -1;
+	e.yynerrs = 0;
+	e.yyerrflag = 0;
+	yyp := -1;
+	yyn := 0;
+
+yystack:
+	for(;;){
+		# put a state and value onto the stack
+		if(yydebug >= 4)
+			e.yysys->fprint(e.yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+
+		yyp++;
+		if(yyp >= YYMAXDEPTH) {
+			yyerror(e, "yacc stack overflow");
+			yyn = 1;
+			break yystack;
+		}
+		yys[yyp].yys = yystate;
+		yys[yyp].yyv = e.yyval;
+
+		for(;;){
+			yyn = yypact[yystate];
+			if(yyn > YYFLAG) {	# simple state
+				if(yychar < 0)
+					yychar = yylex1(e);
+				yyn += yychar;
+				if(yyn >= 0 && yyn < YYLAST) {
+					yyn = yyact[yyn];
+					if(yychk[yyn] == yychar) { # valid shift
+						yychar = -1;
+						yyp++;
+						if(yyp >= YYMAXDEPTH) {
+							yyerror(e, "yacc stack overflow");
+							yyn = 1;
+							break yystack;
+						}
+						yystate = yyn;
+						yys[yyp].yys = yystate;
+						yys[yyp].yyv = *e.yylval;
+						if(e.yyerrflag > 0)
+							e.yyerrflag--;
+						if(yydebug >= 4)
+							e.yysys->fprint(e.yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+						continue;
+					}
+				}
+			}
+		
+			# default state action
+			yyn = yydef[yystate];
+			if(yyn == -2) {
+				if(yychar < 0)
+					yychar = yylex1(e);
+		
+				# look through exception table
+				for(yyxi:=0;; yyxi+=2)
+					if(yyexca[yyxi] == -1 && yyexca[yyxi+1] == yystate)
+						break;
+				for(yyxi += 2;; yyxi += 2) {
+					yyn = yyexca[yyxi];
+					if(yyn < 0 || yyn == yychar)
+						break;
+				}
+				yyn = yyexca[yyxi+1];
+				if(yyn < 0){
+					yyn = 0;
+					break yystack;
+				}
+			}
+
+			if(yyn != 0)
+				break;
+
+			# error ... attempt to resume parsing
+			if(e.yyerrflag == 0) { # brand new error
+				yyerror(e, "syntax error");
+				e.yynerrs++;
+				if(yydebug >= 1) {
+					e.yysys->fprint(e.yystderr, "%s", yystatname(yystate));
+					e.yysys->fprint(e.yystderr, "saw %s\n", yytokname(yychar));
+				}
+			}
+
+			if(e.yyerrflag != 3) { # incompletely recovered error ... try again
+				e.yyerrflag = 3;
+	
+				# find a state where "error" is a legal shift action
+				while(yyp >= 0) {
+					yyn = yypact[yys[yyp].yys] + YYERRCODE;
+					if(yyn >= 0 && yyn < YYLAST) {
+						yystate = yyact[yyn];  # simulate a shift of "error"
+						if(yychk[yystate] == YYERRCODE) {
+							yychar = -1;
+							continue yystack;
+						}
+					}
+	
+					# the current yyp has no shift on "error", pop stack
+					if(yydebug >= 2)
+						e.yysys->fprint(e.yystderr, "error recovery pops state %d, uncovers %d\n",
+							yys[yyp].yys, yys[yyp-1].yys );
+					yyp--;
+				}
+				# there is no state on the stack with an error shift ... abort
+				yyn = 1;
+				break yystack;
+			}
+
+			# no shift yet; clobber input char
+			if(yydebug >= 2)
+				e.yysys->fprint(e.yystderr, "error recovery discards %s\n", yytokname(yychar));
+			if(yychar == YYEOFCODE) {
+				yyn = 1;
+				break yystack;
+			}
+			yychar = -1;
+			# try again in the same state
+		}
+	
+		# reduction by production yyn
+		if(yydebug >= 2)
+			e.yysys->fprint(e.yystderr, "reduce %d in:\n\t%s", yyn, yystatname(yystate));
+	
+		yypt := yyp;
+		yyp -= yyr2[yyn];
+#		yyval = yys[yyp+1].yyv;
+		yym := yyn;
+	
+		# consult goto table to find next state
+		yyn = yyr1[yyn];
+		yyg := yypgo[yyn];
+		yyj := yyg + yys[yyp].yys + 1;
+	
+		if(yyj >= YYLAST || yychk[yystate=yyact[yyj]] != -yyn)
+			yystate = yyact[yyg];
+		case yym {
+			
+4=>
+#line	63	"mash.y"
+{ yys[yypt-0].yyv.cmd.xeq(e.yyenv); }
+7=>
+#line	69	"mash.y"
+{ e.yyval.cmd = nil; }
+8=>
+#line	71	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Cseq, yys[yypt-1].yyv.cmd, yys[yypt-0].yyv.cmd); }
+9=>
+#line	75	"mash.y"
+{ e.yyval.cmd = yys[yypt-1].yyv.cmd.mkcmd(e.yyenv, yys[yypt-0].yyv.flag); }
+10=>
+e.yyval.cmd = yys[yyp+1].yyv.cmd;
+11=>
+#line	80	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Cseq, yys[yypt-1].yyv.cmd, yys[yypt-0].yyv.cmd.mkcmd(e.yyenv, 0)); }
+12=>
+e.yyval.cmd = yys[yyp+1].yyv.cmd;
+13=>
+e.yyval.cmd = yys[yyp+1].yyv.cmd;
+14=>
+#line	86	"mash.y"
+{  e.yyval.cmd = Cmd.cmd2(Cpipe, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+15=>
+#line	90	"mash.y"
+{ e.yyval.cmd = e.yyenv.mksimple(yys[yypt-0].yyv.items); }
+16=>
+#line	92	"mash.y"
+{
+				yys[yypt-0].yyv.cmd.words = e.yyenv.mklist(yys[yypt-1].yyv.items);
+				e.yyval.cmd = Cmd.cmd1w(Cdepend, yys[yypt-0].yyv.cmd, e.yyenv.mklist(yys[yypt-3].yyv.items));
+			}
+17=>
+#line	99	"mash.y"
+{ e.yyval.cmd = yys[yypt-0].yyv.cmd.cmde(Cgroup, yys[yypt-2].yyv.cmd, nil); }
+18=>
+#line	101	"mash.y"
+{ e.yyval.cmd = yys[yypt-0].yyv.cmd.cmde(Csubgroup, yys[yypt-2].yyv.cmd, nil); }
+19=>
+#line	103	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1i(Cfor, yys[yypt-0].yyv.cmd, yys[yypt-4].yyv.item); e.yyval.cmd.words = lib->revitems(yys[yypt-2].yyv.items); }
+20=>
+#line	105	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Cif, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+21=>
+#line	107	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Cif, yys[yypt-4].yyv.cmd, Cmd.cmd2(Celse, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd)); }
+22=>
+#line	109	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Cwhile, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+23=>
+#line	111	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Ccase, yys[yypt-3].yyv.cmd, yys[yypt-1].yyv.cmd.rotcases()); }
+24=>
+#line	113	"mash.y"
+{ e.yyval.cmd = Cmd.cmdiw(Ceq, yys[yypt-2].yyv.item, yys[yypt-0].yyv.items); }
+25=>
+#line	115	"mash.y"
+{ e.yyval.cmd = Cmd.cmdiw(Cdefeq, yys[yypt-2].yyv.item, yys[yypt-0].yyv.items); }
+26=>
+#line	117	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1i(Cfn, yys[yypt-0].yyv.cmd, yys[yypt-1].yyv.item); }
+27=>
+#line	119	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1i(Crescue, yys[yypt-0].yyv.cmd, yys[yypt-1].yyv.item); }
+28=>
+#line	121	"mash.y"
+{
+				yys[yypt-0].yyv.cmd.item = yys[yypt-1].yyv.item;
+				e.yyval.cmd = Cmd.cmd1i(Crule, yys[yypt-0].yyv.cmd, yys[yypt-3].yyv.item);
+			}
+29=>
+#line	128	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1(Clistgroup, yys[yypt-1].yyv.cmd); }
+30=>
+#line	130	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1(Cgroup, yys[yypt-1].yyv.cmd); }
+31=>
+#line	134	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1(Cnop, nil); }
+32=>
+e.yyval.cmd = yys[yyp+1].yyv.cmd;
+33=>
+#line	139	"mash.y"
+{ e.yyval.cmd = nil; }
+34=>
+#line	141	"mash.y"
+{ e.yyval.cmd = yys[yypt-1].yyv.cmd; }
+35=>
+#line	145	"mash.y"
+{ e.yyval.cmd = nil; }
+36=>
+#line	147	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Ccases, yys[yypt-1].yyv.cmd, yys[yypt-0].yyv.cmd); }
+37=>
+#line	151	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Cmatched, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+38=>
+#line	155	"mash.y"
+{ e.yyval.items = yys[yypt-0].yyv.item :: nil; }
+39=>
+#line	157	"mash.y"
+{ e.yyval.items = yys[yypt-0].yyv.item :: yys[yypt-1].yyv.items; }
+40=>
+e.yyval.item = yys[yyp+1].yyv.item;
+41=>
+e.yyval.item = yys[yyp+1].yyv.item;
+42=>
+e.yyval.item = yys[yyp+1].yyv.item;
+43=>
+e.yyval.item = yys[yyp+1].yyv.item;
+44=>
+#line	169	"mash.y"
+{ e.yyval.item = Item.itemc(Ibackq, yys[yypt-1].yyv.cmd); }
+45=>
+#line	171	"mash.y"
+{ e.yyval.item = Item.itemc(Iquote, yys[yypt-1].yyv.cmd); }
+46=>
+#line	173	"mash.y"
+{ e.yyval.item = Item.itemc(Iinpipe, yys[yypt-1].yyv.cmd); }
+47=>
+#line	175	"mash.y"
+{ e.yyval.item = Item.itemc(Ioutpipe, yys[yypt-1].yyv.cmd); }
+48=>
+e.yyval.item = yys[yyp+1].yyv.item;
+49=>
+#line	180	"mash.y"
+{ e.yyval.item = Item.item2(Icaret, yys[yypt-2].yyv.item, yys[yypt-0].yyv.item); }
+50=>
+#line	182	"mash.y"
+{ e.yyval.item = Item.itemc(Iexpr, yys[yypt-1].yyv.cmd); }
+51=>
+#line	186	"mash.y"
+{ e.yyval.item = yys[yypt-0].yyv.item.sword(e.yyenv); }
+52=>
+#line	190	"mash.y"
+{ e.yyval.items = nil; }
+53=>
+#line	192	"mash.y"
+{ e.yyval.items = yys[yypt-0].yyv.item :: yys[yypt-1].yyv.items; }
+54=>
+#line	196	"mash.y"
+{ e.yyval.cmd = ref Cmd; e.yyval.cmd.error = 0; }
+55=>
+#line	198	"mash.y"
+{ e.yyval.cmd = yys[yypt-1].yyv.cmd; yys[yypt-1].yyv.cmd.cmdio(e.yyenv, yys[yypt-0].yyv.item); }
+56=>
+#line	202	"mash.y"
+{ e.yyval.item = Item.itemr(Rin, yys[yypt-0].yyv.item); }
+57=>
+#line	204	"mash.y"
+{ e.yyval.item = Item.itemr(Rout, yys[yypt-0].yyv.item); }
+58=>
+#line	206	"mash.y"
+{ e.yyval.item = Item.itemr(Rappend, yys[yypt-0].yyv.item); }
+59=>
+#line	208	"mash.y"
+{ e.yyval.item = Item.itemr(Rinout, yys[yypt-0].yyv.item); }
+60=>
+#line	212	"mash.y"
+{ e.yyval.flag = 0; }
+61=>
+#line	214	"mash.y"
+{ e.yyval.flag = 0; }
+62=>
+#line	216	"mash.y"
+{ e.yyval.flag = 1; }
+63=>
+#line	220	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1i(Cword, nil, yys[yypt-0].yyv.item); }
+64=>
+#line	222	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1i(Cword, nil, yys[yypt-0].yyv.item); }
+65=>
+#line	224	"mash.y"
+{ e.yyval.cmd = yys[yypt-1].yyv.cmd; }
+66=>
+#line	226	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Ccaret, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+67=>
+#line	228	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1(Chd, yys[yypt-0].yyv.cmd); }
+68=>
+#line	230	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1(Ctl, yys[yypt-0].yyv.cmd); }
+69=>
+#line	232	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1(Clen, yys[yypt-0].yyv.cmd); }
+70=>
+#line	234	"mash.y"
+{ e.yyval.cmd = Cmd.cmd1(Cnot, yys[yypt-0].yyv.cmd); }
+71=>
+#line	236	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Ccons, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+72=>
+#line	238	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Ceqeq, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+73=>
+#line	240	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Cnoteq, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+74=>
+#line	242	"mash.y"
+{ e.yyval.cmd = Cmd.cmd2(Cmatch, yys[yypt-2].yyv.cmd, yys[yypt-0].yyv.cmd); }
+		}
+	}
+
+	return yyn;
+}
--- /dev/null
+++ b/appl/cmd/mash/mashparse.m
@@ -1,0 +1,56 @@
+Mashparse: module {
+
+	PATH:	con "/dis/lib/mashparse.dis";
+
+	init:		fn(l: Mashlib);
+	parse:	fn(e: ref Mashlib->Env);
+
+	YYSTYPE: adt
+	{
+		cmd:		ref Mashlib->Cmd;
+		item:		ref Mashlib->Item;
+		items:	list of ref Mashlib->Item;
+		flag:		int;
+	};
+
+	YYETYPE:	type ref Mashlib->Env;
+Lcase: con	57346;
+Lfor: con	57347;
+Lif: con	57348;
+Lwhile: con	57349;
+Loffparen: con	57350;
+Lelse: con	57351;
+Lpipe: con	57352;
+Leqeq: con	57353;
+Lmatch: con	57354;
+Lnoteq: con	57355;
+Lcons: con	57356;
+Lcaret: con	57357;
+Lnot: con	57358;
+Lhd: con	57359;
+Ltl: con	57360;
+Llen: con	57361;
+Lword: con	57362;
+Lbackq: con	57363;
+Lcolon: con	57364;
+Lcolonmatch: con	57365;
+Ldefeq: con	57366;
+Leq: con	57367;
+Lmatched: con	57368;
+Lquote: con	57369;
+Loncurly: con	57370;
+Lonparen: con	57371;
+Loffcurly: con	57372;
+Lat: con	57373;
+Lgreat: con	57374;
+Lgreatgreat: con	57375;
+Lless: con	57376;
+Llessgreat: con	57377;
+Lfn: con	57378;
+Lin: con	57379;
+Lrescue: con	57380;
+Land: con	57381;
+Leof: con	57382;
+Lsemi: con	57383;
+Lerror: con	57384;
+};
--- /dev/null
+++ b/appl/cmd/mash/misc.b
@@ -1,0 +1,313 @@
+#
+#	Miscellaneous routines.
+#
+
+Cmd.cmd1(op: int, l: ref Cmd): ref Cmd
+{
+	return ref Cmd(op, nil, l, nil, nil, nil, nil, 0);
+}
+
+Cmd.cmd2(op: int, l, r: ref Cmd): ref Cmd
+{
+	return ref Cmd(op, nil, l, r, nil, nil, nil, 0);
+}
+
+Cmd.cmd1i(op: int, l: ref Cmd, i: ref Item): ref Cmd
+{
+	return ref Cmd(op, nil, l, nil, i, nil, nil, 0);
+}
+
+Cmd.cmd1w(op: int, l: ref Cmd, w: list of ref Item): ref Cmd
+{
+	return ref Cmd(op, w, l, nil, nil, nil, nil, 0);
+}
+
+Cmd.cmde(c: self ref Cmd, op: int, l, r: ref Cmd): ref Cmd
+{
+	c.op = op;
+	c.left = l;
+	c.right = r;
+	return c;
+}
+
+Cmd.cmdiw(op: int, i: ref Item, w: list of ref Item): ref Cmd
+{
+	return ref Cmd(op, revitems(w), nil, nil, i, nil, nil, 0);
+}
+
+Pin, Pout:	con 1 << iota;
+
+rdmap := array[] of
+{
+	Rin => Pin,
+	Rout or Rappend => Pout,
+	Rinout => Pin | Pout,
+};
+
+rdsymbs := array[] of
+{
+	Rin => "<",
+	Rout => ">",
+	Rappend => ">>",
+	Rinout => "<>",
+};
+
+ionames := array[] of
+{
+	Pin => "input",
+	Pout => "ouput",
+	Pin | Pout => "input/output",
+};
+
+#
+#	Check a pipeline for ambiguities.
+#
+Cmd.checkpipe(c: self ref Cmd, e: ref Env, f: int): int
+{
+	if (c.error)
+		return 0;
+	if (c.op == Cpipe) {
+		if (!c.left.checkpipe(e, f | Pout))
+			return 0;
+		if (!c.right.checkpipe(e, f | Pin))
+			return 0;
+	}
+	if (f) {
+		t := 0;
+		for (l := c.redirs; l != nil; l = tl l)
+			t |= rdmap[(hd l).op];
+		f &= t;
+		if (f) {
+			e.report(sys->sprint("%s redirection conflicts with pipe", ionames[f]));
+			return 0;
+		}
+	}
+	return 1;
+}
+
+#
+#	Update a command with another redirection.
+#
+Cmd.cmdio(c: self ref Cmd, e: ref Env, i: ref Item)
+{
+	f := 0;
+	for (l := c.redirs; l != nil; l = tl l)
+		f |= rdmap[(hd l).op];
+	r := i.redir;
+	f &= rdmap[r.op];
+	if (f != 0) {
+		e.report(sys->sprint("repeat %s redirection", ionames[f]));
+		c.error = 1;
+	}
+	c.redirs = r :: c.redirs;
+}
+
+#
+#	Make a basic command.
+#
+Cmd.mkcmd(c: self ref Cmd, e: ref Env, async: int): ref Cmd
+{
+	if (!c.checkpipe(e, 0))
+		return nil;
+	if (async)
+		return ref Cmd(Casync, nil, c, nil, nil, nil, nil, 0);
+	else
+		return c;
+}
+
+#
+#	Rotate parse tree of cases.
+#
+Cmd.rotcases(c: self ref Cmd): ref Cmd
+{
+	l := c;
+	c = nil;
+	while (l != nil) {
+		t := l.right;
+		l.right = c;
+		c = l;
+		l = l.left;
+		c.left = t;
+	}
+	return c;
+}
+
+Item.item1(op: int, l: ref Item): ref Item
+{
+	return ref Item(op, nil, l, nil, nil, nil);
+}
+
+Item.item2(op: int, l, r: ref Item): ref Item
+{
+	return ref Item(op, nil, l, r, nil, nil);
+}
+
+Item.itemc(op: int, c: ref Cmd): ref Item
+{
+	return ref Item(op, nil, nil, nil, c, nil);
+}
+
+#
+#	Make an item from a list of strings.
+#
+Item.iteml(l: list of string): ref Item
+{
+	if (l != nil && tl l == nil)
+		return Item.itemw(hd l);
+	r: list of string;
+	while (l != nil) {
+		r = (hd l) :: r;
+		l = tl l;
+	}
+	c := ref Cmd;
+	c.op = Clist;
+	c.value = revstrs(r);
+	return Item.itemc(Iexpr, c);
+}
+
+Item.itemr(op: int, i: ref Item): ref Item
+{
+	return ref Item(Iredir, nil, nil, nil, nil, ref Redir(op, i));
+}
+
+qword:	Word = (nil, Wquoted, (0, nil));
+
+Item.itemw(s: string): ref Item
+{
+	w := ref qword;
+	w.text = s;
+	return ref Item(Iword, w, nil, nil, nil, nil);
+}
+
+revitems(l: list of ref Item): list of ref Item
+{
+	r: list of ref Item;
+	while (l != nil) {
+		r = (hd l) :: r;
+		l = tl l;
+	}
+	return r;
+}
+
+revstrs(l: list of string): list of string
+{
+	r: list of string;
+	while (l != nil) {
+		r = (hd l) :: r;
+		l = tl l;
+	}
+	return r;
+}
+
+prepend(l: list of string, r: list of string): list of string
+{
+	while (r != nil) {
+		l = (hd r) :: l;
+		r = tl r;
+	}
+	return l;
+}
+
+concat(l: list of string): string
+{
+	s := hd l;
+	for (;;) {
+		l = tl l;
+		if (l == nil)
+			return s;
+		s += " ";
+		s += hd l;
+	}
+}
+
+#
+#	Make an item list, no redirections allowed.
+#
+Env.mklist(e: self ref Env, l: list of ref Item): list of ref Item
+{
+	r: list of ref Item;
+	while (l != nil) {
+		i := hd l;
+		if (i.op == Iredir)
+			e.report("redirection in list");
+		else
+			r = i :: r;
+		l = tl l;
+	}
+	return r;
+}
+
+#
+#	Make a simple command.
+#
+Env.mksimple(e: self ref Env, l: list of ref Item): ref Cmd
+{
+	r: list of ref Item;
+	c := ref Cmd;
+	c.op = Csimple;
+	c.error = 0;
+	while (l != nil) {
+		i := hd l;
+		if (i.op == Iredir)
+			c.cmdio(e, i);
+		else
+			r = i :: r;
+		l = tl l;
+	}
+	c.words = r;
+	return c;
+}
+
+Env.diag(e: self ref Env, s: string): string
+{
+	return where(e) + s;
+}
+
+Env.usage(e: self ref Env, s: string)
+{
+	e.report("usage: " + s);
+}
+
+Env.report(e: self ref Env, s: string)
+{
+	sys->fprint(e.stderr, "%s\n", e.diag(s));
+	if (e.flags & ERaise)
+		exits("error");
+}
+
+Env.error(e: self ref Env, s: string)
+{
+	e.report(s);
+	cleanup();
+}
+
+panic(s: string)
+{
+	raise "panic: " + s;
+}
+
+prprompt(n: int)
+{
+	case n {
+	0 =>
+		sys->print("%s", prompt);
+	1 =>
+		sys->print("%s", contin);
+	}
+}
+
+Env.couldnot(e: self ref Env, what, who: string)
+{
+	sys->fprint(e.stderr, "could not %s %s: %r\n", what, who);
+	exits("system error");
+}
+
+cleanup()
+{
+	exit;
+}
+
+exits(s: string)
+{
+	raise "fail: mash " + s;
+}
--- /dev/null
+++ b/appl/cmd/mash/mkfile
@@ -1,0 +1,78 @@
+<../../../mkconfig
+
+TARG=	mash.dis\
+	mashlib.dis\
+	mashparse.dis\
+	builtins.dis\
+	history.dis\
+	make.dis\
+
+INS=	$ROOT/dis/mash.dis\
+	$ROOT/dis/lib/mashlib.dis\
+	$ROOT/dis/lib/mashparse.dis\
+	$ROOT/dis/lib/mash/builtins.dis\
+	$ROOT/dis/lib/mash/history.dis\
+	$ROOT/dis/lib/mash/make.dis\
+
+MODULES=\
+	mash.m\
+	mashparse.m\
+
+SYSMODULES=\
+	bufio.m\
+	draw.m\
+	filepat.m\
+	hash.m\
+	regex.m\
+	sh.m\
+	string.m\
+	sys.m\
+
+LIBSRC=\
+	depends.b\
+	dump.b\
+	exec.b\
+	expr.b\
+	lex.b\
+	misc.b\
+	serve.b\
+	symb.b\
+	xeq.b\
+
+all:V:		$TARG
+
+install:V:	$INS
+
+nuke:V: clean
+	rm -f $INS
+
+clean:V:
+	rm -f *.dis *.sbl
+
+uninstall:V:
+	rm -f $INS
+
+MODDIR=$ROOT/module
+SYS_MODULE=${SYSMODULES:%=$MODDIR/%}
+LIMBOFLAGS=-I$MODDIR
+
+$ROOT/dis/mash.dis:	mash.dis
+	rm -f $ROOT/dis/mash.dis && cp mash.dis $ROOT/dis/mash.dis
+
+$ROOT/dis/lib/mashlib.dis:	mashlib.dis
+	rm -f $ROOT/dis/mashlib.dis && cp mashlib.dis $ROOT/dis/lib/mashlib.dis
+
+$ROOT/dis/lib/mashparse.dis:	mashparse.dis
+	rm -f $ROOT/dis/mashparse.dis && cp mashparse.dis $ROOT/dis/lib/mashparse.dis
+
+$ROOT/dis/lib/mash/%.dis:	%.dis
+	rm -f $ROOT/dis/$stem.dis && cp $stem.dis $ROOT/dis/lib/mash/$stem.dis
+
+%.dis:		$MODULES $SYS_MODULE
+mashlib.dis:	$LIBSRC
+
+%.dis:		%.b
+	limbo $LIMBOFLAGS -gw $stem.b
+
+%.s:		%.b
+	limbo $LIMBOFLAGS -w -G -S $stem.b
--- /dev/null
+++ b/appl/cmd/mash/serve.b
@@ -1,0 +1,154 @@
+#
+#	This should be called by spawned (persistent) threads.
+#	It arranges for them to be killed at the end of the day.
+#
+reap()
+{
+	if (pidchan == nil) {
+		pidchan = chan of int;
+		spawn zombie();
+	}
+	pidchan <-= sys->pctl(0, nil);
+}
+
+#
+#	This thread records spawned threads and kills them.
+#
+zombie()
+{
+	pids := array[10] of int;
+	pidx := 0;
+	for (;;) {
+		pid := <- pidchan;
+		if (pid == PIDEXIT) {
+			for (i := 0; i < pidx; i++)
+				kill(pids[i]);
+			exit;
+		}
+		if (pidx == len pids) {
+			n := pidx * 3 / 2;
+			a := array[n] of int;
+			a[:] = pids;
+			pids = a;
+		}
+		pids[pidx++] = pid;
+	}
+}
+
+#
+#	Kill a thread.
+#
+kill(pid: int)
+{
+	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if (fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+#
+#	Exit top level, killing spawned threads.
+#
+exitmash()
+{
+	if (pidchan != nil)
+		pidchan <-= PIDEXIT;
+	exit;
+}
+
+#
+#	Slice a buffer if needed.
+#
+restrict(buff: array of byte, count: int): array of byte
+{
+	if (count < len buff)
+		return buff[:count];
+	else
+		return buff;
+}
+
+#
+#	Serve mash console reads.  Favours other programs
+#	ahead of the input loop.
+#
+serve_read(c: ref Sys->FileIO, sync: chan of int)
+{
+	s: string;
+	in := sys->fildes(0);
+	sys->pctl(Sys->NEWFD, in.fd :: nil);
+	sync <-= 0;
+	reap();
+	buff := array[Sys->ATOMICIO] of byte;
+outer:	for (;;) {
+		n := sys->read(in, buff, len buff);
+		if (n < 0) {
+			n = 0;
+			s = errstr();
+		} else
+			s = nil;
+		b := buff[:n];
+		alt {
+		(off, count, fid, rc) := <-c.read =>
+			if (rc == nil)
+				break;
+			rc <-= (restrict(b, count), s);
+			continue outer;
+		* =>
+			;
+		}
+	inner:	for (;;) {
+			alt {
+			(off, count, fid, rc) := <-c.read =>
+				if (rc == nil)
+					continue inner;
+				rc <-= (restrict(b, count), s);
+			inchan <-= b =>
+				;
+			}
+			break;
+		}
+	}
+}
+
+#
+#	Serve mash console writes.
+#
+serve_write(c: ref Sys->FileIO, sync: chan of int)
+{
+	out := sys->fildes(1);
+	sys->pctl(Sys->NEWFD, out.fd :: nil);
+	sync <-= 0;
+	reap();
+	for (;;) {
+		(off, data, fid, wc) := <-c.write;
+		if (wc == nil)
+			continue;
+		if (sys->write(out, data, len data) < 0)
+			wc <-= (0, errstr());
+		else
+			wc <-= (len data, nil);
+	}
+}
+
+#
+#	Begin serving the mash console.
+#
+Env.serve(e: self ref Env)
+{
+	if (servechan != nil)
+		return;
+	(s, c) := e.servefile(nil);
+	inchan = chan of array of byte;
+	servechan = chan of array of byte;
+	sync := chan of int;
+	spawn serve_read(c, sync);
+	spawn serve_write(c, sync);
+	<-sync;
+	<-sync;
+	if (sys->bind(s, CONSOLE, Sys->MREPL) < 0)
+		e.couldnot("bind", CONSOLE);
+	sys->pctl(Sys->NEWFD, nil);
+	e.in = sys->open(CONSOLE, sys->OREAD | sys->ORCLOSE);
+	e.out = sys->open(CONSOLE, sys->OWRITE);
+	e.stderr = sys->open(CONSOLE, sys->OWRITE);
+	e.wait = nil;
+}
--- /dev/null
+++ b/appl/cmd/mash/symb.b
@@ -1,0 +1,265 @@
+#
+#	Symbol table routines.  A symbol table becomes copy-on-write
+#	when it is cloned.  The first modification will copy the hash table.
+#	Every list is then copied on first modification.
+#
+
+#
+#	Copy a hash list.
+#
+cpsymbs(l: list of ref Symb): list of ref Symb
+{
+	r: list of ref Symb;
+	while (l != nil) {
+		r = (ref *hd l) :: r;
+		l = tl l;
+	}
+	return r;
+}
+
+#
+#	New symbol table.
+#
+Stab.new(): ref Stab
+{
+	return ref Stab(array[SHASH] of list of ref Symb, 0, 0);
+}
+
+#
+#	Clone a symbol table.  Copy Stab and mark contents copy-on-write.
+#
+Stab.clone(t: self ref Stab): ref Stab
+{
+	t.copy = 1;
+	t.wmask = SMASK;
+	return ref *t;
+}
+
+#
+#	Update symbol table entry, or add new entry.
+#
+Stab.update(t: self ref Stab, s: string, tag: int, v: list of string, f: ref Cmd, b: Mashbuiltin): ref Symb
+{
+	if (t.copy) {
+		a := array[SHASH] of list of ref Symb;
+		a[:] = t.tab[:];
+		t.tab = a;
+		t.copy = 0;
+	}
+	x := hash->fun1(s, SHASH);
+	l := t.tab[x];
+	if (t.wmask & (1 << x)) {
+		l = cpsymbs(l);
+		t.tab[x] = l;
+		t.wmask &= ~(1 << x);
+	}
+	r := l;
+	while (r != nil) {
+		h := hd r;
+		if (h.name == s) {
+			case tag {
+			Svalue =>
+				h.value = v;
+			Sfunc =>
+				h.func = f;
+			Sbuiltin =>
+				h.builtin = b;
+			}
+			return h;
+		}
+		r = tl r;
+	}
+	n := ref Symb(s, v, f, b, 0);
+	t.tab[x] = n :: l;
+	return n;
+}
+
+#
+#	Make a list of a symbol table's contents.
+#
+Stab.all(t: self ref Stab): list of ref Symb
+{
+	r: list of ref Symb;
+	for (i := 0; i < SHASH; i++) {
+		for (l := t.tab[i]; l != nil; l = tl l)
+			r = (ref *hd l) :: r;
+	}
+	return r;
+}
+
+#
+#	Assign a list of strings to a variable.  The distinguished value
+#	"empty" is used to distinguish nil value from undefined.
+#
+Stab.assign(t: self ref Stab, s: string, v: list of string)
+{
+	if (v == nil)
+		v = empty;
+	t.update(s, Svalue, v, nil, nil);
+}
+
+#
+#	Define a builtin.
+#
+Stab.defbuiltin(t: self ref Stab, s: string, b: Mashbuiltin)
+{
+	t.update(s, Sbuiltin, nil, nil, b);
+}
+
+#
+#	Define a function.
+#
+Stab.define(t: self ref Stab, s: string, f: ref Cmd)
+{
+	t.update(s, Sfunc, nil, f, nil);
+}
+
+#
+#	Symbol table lookup.
+#
+Stab.find(t: self ref Stab, s: string): ref Symb
+{
+	l := t.tab[hash->fun1(s, SHASH)];
+	while (l != nil) {
+		h := hd l;
+		if (h.name == s)
+			return h;
+		l = tl l;
+	}
+	return nil;
+}
+
+#
+#	Function lookup.
+#
+Stab.func(t: self ref Stab, s: string): ref Cmd
+{
+	v := t.find(s);
+	if (v == nil)
+		return nil;
+	return v.func;
+}
+
+#
+#	New environment.
+#
+Env.new(): ref Env
+{
+	return ref Env(Stab.new(), nil, ETop, nil, nil, nil, nil, nil, nil, 0);
+}
+
+#
+#	Clone environment.  No longer top-level or interactive.
+#
+Env.clone(e: self ref Env): ref Env
+{
+	e = e.copy();
+	e.flags &= ~(ETop | EInter);
+	e.global = e.global.clone();
+	if (e.local != nil)
+		e.local = e.local.clone();
+	return e;
+}
+
+#
+#	Copy environment.
+#
+Env.copy(e: self ref Env): ref Env
+{
+	return ref *e;
+}
+
+#
+#	Fetch $n argument.
+#
+Env.arg(e: self ref Env, s: string): string
+{
+	n := int s;
+	if (e.args == nil || n >= len e.args)
+		return "$" + s;
+	else
+		return e.args[n];
+}
+
+#
+#	Lookup builtin.
+#
+Env.builtin(e: self ref Env, s: string): Mashbuiltin
+{
+	v := e.global.find(s);
+	if (v == nil)
+		return nil;
+	return v.builtin;
+}
+
+#
+#	Define a builtin.
+#
+Env.defbuiltin(e: self ref Env, s: string, b: Mashbuiltin)
+{
+	e.global.defbuiltin(s, b);
+}
+
+#
+#	Define a function.
+#
+Env.define(e: self ref Env, s: string, f: ref Cmd)
+{
+	e.global.define(s, f);
+}
+
+#
+#	Value of a shell variable (check locals then globals).
+#
+Env.dollar(e: self ref Env, s: string): ref Symb
+{
+	if (e.local != nil) {
+		l := e.local.find(s);
+		if (l != nil && l.value != nil)
+			return l;
+	}
+	g := e.global.find(s);
+	if (g != nil && g.value != nil)
+		return g;
+	return nil;
+}
+
+#
+#	Lookup a function.
+#
+Env.func(e: self ref Env, s: string): ref Cmd
+{
+	v := e.global.find(s);
+	if (v == nil)
+		return nil;
+	return v.func;
+}
+
+#
+#	Local assignment.
+#
+Env.let(e: self ref Env, s: string, v: list of string)
+{
+	if (e.local == nil)
+		e.local = Stab.new();
+	e.local.assign(s, v);
+}
+
+#
+#	Assignment.  Update local or define global.
+#
+Env.set(e: self ref Env, s: string, v: list of string)
+{
+	if (e.local != nil && e.local.find(s) != nil)
+		e.local.assign(s, v);
+	else
+		e.global.assign(s, v);
+}
+
+#
+#	Report undefined.
+#
+Env.undefined(e: self ref Env, s: string)
+{
+	e.report(s + ": undefined");
+}
--- /dev/null
+++ b/appl/cmd/mash/tk.b
@@ -1,0 +1,603 @@
+implement Mashbuiltin;
+
+#
+#	"tk" builtin.
+#
+#	tk clear		- clears the text frame
+#	tk def button name value
+#	tk def ibutton name value image
+#	tk def menu name
+#	tk def item menu name value
+#	tk dialog title mesg default label ...
+#	tk dump			- print commands to reconstruct toolbar
+#	tk dump name ...
+#	tk env			- update tk execution env
+#	tk file title dir pattern ...
+#	tk geom
+#	tk layout name ...
+#	tk notice message
+#	tk sel			- print selection
+#	tk sget			- print snarf
+#	tk sput string		- put snarf
+#	tk string mesg		- get string
+#	tk taskbar string
+#	tk text			- print window text
+#
+
+include	"mash.m";
+include	"mashparse.m";
+include	"wmlib.m";
+include	"dialog.m";
+include	"selectfile.m";
+
+mashlib:	Mashlib;
+wmlib:		Wmlib;
+dialog:	Dialog;
+selectfile:	Selectfile;
+
+Env, Stab, Symb:	import mashlib;
+sys, bufio, tk:		import mashlib;
+gtop, gctxt, ident:	import mashlib;
+
+Iobuf:	import bufio;
+
+tkitems:	ref Stab;
+tklayout:	list of string;
+tkenv:	ref Env;
+tkserving:	int = 0;
+
+Cbutton, Cibutton, Cmenu:	con Cprivate + iota;
+
+Cmark:	con 3;
+BUTT:	con ".b.";
+
+#
+#	Interface to catch the use as a command.
+#
+init(nil: ref Draw->Context, args: list of string)
+{
+	raise "fail: " + hd args + " not loaded";
+}
+
+#
+#	Used by whatis.
+#
+name(): string
+{
+	return "tk";
+}
+
+#
+#	Install command and initialize state.
+#
+mashinit(nil: list of string, lib: Mashlib, this: Mashbuiltin, e: ref Env)
+{
+	mashlib = lib;
+	if (gctxt == nil) {
+		e.report("tk: no graphics context");
+		return;
+	}
+	if (gtop == nil) {
+		e.report("tk: not run from wmsh");
+		return;
+	}
+	wmlib = load Wmlib Wmlib->PATH;
+	if (wmlib == nil) {
+		e.report(sys->sprint("tk: could not load %s: %r", Wmlib->PATH));
+		return;
+	}
+	dialog = load Dialog Dialog->PATH;
+	if (dialog == nil) {
+		e.report(sys->sprint("tk: could not load %s: %r", Dialog->PATH));
+		return;
+	}
+	selectfile = load Selectfile Selectfile->PATH;
+	if (selectfile == nil) {
+		e.report(sys->sprint("tk: could not load %s: %r", Selectfile->PATH));
+		return;
+	}
+	wmlib->init();
+	dialog->init();
+	selectfile->init();
+	e.defbuiltin("tk", this);
+	tkitems = Stab.new();
+}
+
+#
+#	Execute the "tk" builtin.
+#
+mashcmd(e: ref Env, l: list of string)
+{
+	# must lock
+	l = tl l;
+	if (l == nil)
+		return;
+	s := hd l;
+	l = tl l;
+	case s {
+	"clear" =>
+		if (l != nil) {
+			e.usage("tk clear");
+			return;
+		}
+		clear(e);
+	"def" =>
+		define(e, l);
+	"dialog" =>
+		if (len l < 4) {
+			e.usage("tk dialog title mesg default label ...");
+			return;
+		}
+		dodialog(e, l);
+	"dump" =>
+		dump(e, l);
+	"env" =>
+		if (l != nil) {
+			e.usage("tk env");
+			return;
+		}
+		tkenv = e.clone();
+		tkenv.flags |= mashlib->ETop;
+	"file" =>
+		if (len l < 3) {
+			e.usage("tk file title dir pattern ...");
+			return;
+		}
+		dofile(e, hd l, hd tl l, tl tl l);
+	"geom" =>
+		if (l != nil) {
+			e.usage("tk geom");
+			return;
+		}
+		e.output(wmlib->geom(gtop));
+	"layout" =>
+		layout(e, l);
+	"notice" =>
+		if (len l != 1) {
+			e.usage("tk notice message");
+			return;
+		}
+		notice(hd l);
+	"sel" =>
+		if (l != nil) {
+			e.usage("tk sel");
+			return;
+		}
+		sel(e);
+	"sget" =>
+		if (l != nil) {
+			e.usage("tk sget");
+			return;
+		}
+		e.output(wmlib->snarfget());
+	"sput" =>
+		if (len l != 1) {
+			e.usage("tk sput string");
+			return;
+		}
+		wmlib->snarfput(hd l);
+	"string" =>
+		if (len l != 1) {
+			e.usage("tk string mesg");
+			return;
+		}
+		e.output(dialog->getstring(gctxt, gtop.image, hd l));
+		focus(e);
+	"taskbar" =>
+		if (len l != 1) {
+			e.usage("tk taskbar string");
+			return;
+		}
+		e.output(wmlib->taskbar(gtop, hd l));
+	"text" =>
+		if (l != nil) {
+			e.usage("tk text");
+			return;
+		}
+		text(e);
+	* =>
+		e.report(sys->sprint("tk: unknown command: %s", s));
+	}
+}
+
+#
+#	Execute tk command and check for error.
+#
+tkcmd(e: ref Env, s: string): string
+{
+	if (e != nil && (e.flags & mashlib->EDumping))
+		sys->fprint(e.stderr, "+ %s\n", s);
+	r := tk->cmd(gtop, s);
+	if (r != nil && r[0] == '!' && e != nil)
+		sys->fprint(e.stderr, "tk: %s\n\tcommand was %s\n", r[1:], s);
+	return r;
+}
+
+focus(e: ref Env)
+{
+	tkcmd(e, "focus .ft.t");
+}
+
+#
+#	Serve loop.
+#
+tkserve(mash: chan of string)
+{
+	mashlib->reap();
+	for (;;) {
+		cmd := <-mash;
+		if (mashlib->servechan != nil && len cmd > 1) {
+			cmd[len cmd - 1] = '\n';
+			mashlib->servechan <-= array of byte cmd[1:];
+		}
+	}
+}
+
+notname(e: ref Env, s: string)
+{
+	e.report(sys->sprint("tk: %s: malformed name", s));
+}
+
+#
+#	Define a button, menu or item.
+#
+define(e: ref Env, l: list of string)
+{
+	if (l == nil) {
+		e.usage("tk def definition");
+		return;
+	}
+	s := hd l;
+	l = tl l;
+	case s {
+	"button" =>
+		if (len l != 2) {
+			e.usage("tk def button name value");
+			return;
+		}
+		s = hd l;
+		if (!ident(s)) {
+			notname(e, s);
+			return;
+		}
+		i := tkitems.update(s, Svalue, tl l, nil, nil);
+		i.tag = Cbutton;
+	"ibutton" =>
+		if (len l != 3) {
+			e.usage("tk def ibutton name value path");
+			return;
+		}
+		s = hd l;
+		if (!ident(s)) {
+			notname(e, s);
+			return;
+		}
+		i := tkitems.update(s, Svalue, tl l, nil, nil);
+		i.tag = Cibutton;
+	"menu" =>
+		if (len l != 1) {
+			e.usage("tk def menu name");
+			return;
+		}
+		s = hd l;
+		if (!ident(s)) {
+			notname(e, s);
+			return;
+		}
+		i := tkitems.update(s, Svalue, nil, nil, nil);
+		i.tag = Cmenu;
+	"item" =>
+		if (len l != 3) {
+			e.usage("tk def item menu name value");
+			return;
+		}
+		s = hd l;
+		i := tkitems.find(s);
+		if (i == nil || i.tag != Cmenu) {
+			e.report(s + ": not a menu");
+			return;
+		}
+		l = tl l;
+		i.value = updateitem(i.value, hd l, hd tl l);
+	* =>
+		e.report("tk: " + s + ": unknown command");
+	}
+}
+
+#
+#	Update a menu item.
+#
+updateitem(l: list of string, c, v: string): list of string
+{
+	r: list of string;
+	while (l != nil) {
+		w := hd l;
+		l = tl l;
+		d := hd l;
+		l = tl l;
+		if (d == c) {
+			r = c :: v :: r;
+			c = nil;
+		} else
+			r = d :: w :: r;
+	}
+	if (c != nil)
+		r = c :: v :: r;
+	return mashlib->revstrs(r);
+}
+
+items(e: ref Env, l: list of string): list of ref Symb
+{
+	r: list of ref Symb;
+	while (l != nil) {
+		i := tkitems.find(hd l);
+		if (i == nil) {
+			e.report(hd l + ": not an item");
+			return nil;
+		}
+		r = i :: r;
+		l = tl l;
+	}
+	return r;
+}
+
+deleteall(e: ref Env, l: list of string)
+{
+	while (l != nil) {
+		tkcmd(e, "destroy " + BUTT + hd l);
+		l = tl l;
+	}
+}
+
+sendcmd(c: string): string
+{
+	return tk->quote("send mash " + tk->quote(c));
+}
+
+addbutton(e: ref Env, w, t, c: string)
+{
+	tkcmd(e, sys->sprint("button %s%s -%s %s -command %s", BUTT, t, w, t, sendcmd(c)));
+}
+
+addimage(e: ref Env, t, f: string)
+{
+	r := tkcmd(nil, sys->sprint("image create bitmap %s -file %s.bit -maskfile %s.mask", t, f, f));
+	if (r != nil && r[0] == '!')
+		tkcmd(e, sys->sprint("image create bitmap %s -file %s.bit", t, f));
+}
+
+additem(e: ref Env, s: ref Symb)
+{
+	case s.tag {
+	Cbutton =>
+		addbutton(e, "text", s.name, hd s.value);
+	Cibutton =>
+		addimage(e, s.name, hd tl s.value);
+		addbutton(e, "image", s.name, hd s.value);
+	Cmenu =>
+		t := s.name;
+		tkcmd(e, sys->sprint("menubutton %s%s -text %s -menu %s%s.menu -underline -1", BUTT, t, t, BUTT,t));
+		t += ".menu";
+		tkcmd(e, "menu " + BUTT + t);
+		t = BUTT + t;
+		l := s.value;
+		while (l != nil) {
+			v := sendcmd(hd l);
+			l = tl l;
+			c := tk->quote(hd l);
+			l = tl l;
+			tkcmd(e, sys->sprint("%s add command -label %s -command %s", t, c, v));
+		}
+	}
+}
+
+pack(e: ref Env, l: list of string)
+{
+	s := "pack";
+	while (l != nil) {
+		s += sys->sprint(" %s%s", BUTT, hd l);
+		l = tl l;
+	}
+	s += " -side left";
+	tkcmd(e, s);
+}
+
+propagate(e: ref Env)
+{
+	tkcmd(e, "pack propagate . 0");
+	tkcmd(e, "update");
+}
+
+unmark(r: list of ref Symb)
+{
+	while (r != nil) {
+		s := hd r;
+		case s.tag {
+		Cbutton + Cmark or Cibutton + Cmark or Cmenu + Cmark =>
+			s.tag -= Cmark;
+		}
+		r = tl r;
+	}
+}
+
+#
+#	Check that the layout tags are unique.
+#
+unique(e: ref Env, r: list of ref Symb): int
+{
+	u := 1;
+loop:
+	for (l := r; l != nil; l = tl l) {
+		s := hd l;
+		case s.tag {
+		Cbutton + Cmark or Cibutton + Cmark or Cmenu + Cmark =>
+			e.report(sys->sprint("layout: tag %s repeated", s.name));
+			u = 0;
+			break loop;
+		Cbutton or Cibutton or Cmenu =>
+			s.tag += Cmark;
+		}
+	}
+	unmark(r);
+	return u;
+}
+
+#
+#	Update the button bar layout and the environment.
+#	Maybe spawn the server.
+#
+layout(e: ref Env, l: list of string)
+{
+	r := items(e, l);
+	if (r == nil && l != nil)
+		return;
+	if (!unique(e, r))
+		return;
+	if (tklayout != nil)
+		deleteall(e, tklayout);
+	n := len r;
+	a := array[n] of ref Symb;
+	while (--n >= 0) {
+		a[n] = hd r;
+		r = tl r;
+	}
+	n = len a;
+	for (i := 0; i < n; i++)
+		additem(e, a[i]);
+	pack(e, l);
+	propagate(e);
+	tklayout = l;
+	tkenv = e.clone();
+	tkenv.flags |= mashlib->ETop;
+	if (!tkserving) {
+		tkserving = 1;
+		mash := chan of string;
+		tk->namechan(gtop, mash, "mash");
+		spawn tkserve(mash);
+		mashlib->startserve = 1;
+	}
+}
+
+dumpbutton(out: ref Iobuf, w: string, s: ref Symb)
+{
+	out.puts(sys->sprint("tk def %s %s %s", w, s.name, mashlib->quote(hd s.value)));
+	if (s.tag == Cibutton)
+		out.puts(sys->sprint(" %s", mashlib->quote(hd tl s.value)));
+	out.puts(";\n");
+}
+
+#
+#	Print commands to reconstruct toolbar.
+#
+dump(e: ref Env, l: list of string)
+{
+	r: list of ref Symb;
+	if (l != nil)
+		r = items(e, l);
+	else
+		r = tkitems.all();
+	out := e.outfile();
+	if (out == nil)
+		return;
+	while (r != nil) {
+		s := hd r;
+		case s.tag {
+		Cbutton =>
+			dumpbutton(out, "button", s);
+		Cibutton =>
+			dumpbutton(out, "ibutton", s);
+		Cmenu =>
+			t := s.name;
+			out.puts(sys->sprint("tk def menu %s;\n", t));
+			i := s.value;
+			while (i != nil) {
+				v := hd i;
+				i = tl i;
+				c := hd i;
+				i = tl i;
+				out.puts(sys->sprint("tk def item %s %s %s;\n", t, c, mashlib->quote(v)));
+			}
+		}
+		r = tl r;
+	}
+	if (l == nil) {
+		out.puts("tk layout");
+		for (l = tklayout; l != nil; l = tl l) {
+			out.putc(' ');
+			out.puts(hd l);
+		}
+		out.puts(";\n");
+	}
+	out.close();
+}
+
+clear(e: ref Env)
+{
+	tkcmd(e, ".ft.t delete 1.0 end; update");
+}
+
+dofile(e: ref Env, title, dir: string, pats: list of string)
+{
+	e.output(selectfile->filename(gctxt, gtop.image, title, pats, dir));
+}
+
+sel(e: ref Env)
+{
+	sel := tkcmd(e, ".ft.t tag ranges sel");
+	if (sel != nil) {
+		s := tkcmd(e, ".ft.t dump " + sel);
+		e.output(s);
+	}
+}
+
+text(e: ref Env)
+{
+	sel := tkcmd(e, ".ft.t tag ranges sel");
+	if (sel != nil)
+		tkcmd(e, ".ft.t tag remove sel " + sel);
+	s := tkcmd(e, ".ft.t dump 1.0 end");
+	if (sel != nil)
+		tkcmd(e, ".ft.t tag add sel " + sel);
+	e.output(s);
+}
+
+notice0 := array[] of
+{
+	"frame .f -borderwidth 2 -relief groove -padx 3 -pady 3",
+	"frame .f.f",
+	"label .f.f.l -bitmap error -foreground red",
+};
+
+notice1 := array[] of
+{
+	"button .f.b -text {  OK  } -command {send cmd done}",
+	"pack .f.f.l .f.f.m -side left -expand 1 -padx 10 -pady 10",
+	"pack .f.f .f.b -padx 10 -pady 10",
+	"pack .f",
+	"update; cursor -default",
+};
+
+notice(mesg: string)
+{
+	x := int tk->cmd(gtop, ". cget -x");
+	y := int tk->cmd(gtop, ". cget -y");
+	where := sys->sprint("-x %d -y %d", x + 30, y + 30);
+	t := tk->toplevel(gctxt.screen, where + " -borderwidth 2 -relief raised");
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	wmlib->tkcmds(t, notice0);
+	tk->cmd(t, "label .f.f.m -text '" + mesg);
+	wmlib->tkcmds(t, notice1);
+	<- cmd;
+}
+
+dodialog(e: ref Env, l: list of string)
+{
+	title := hd l;
+	l = tl l;
+	msg := hd l;
+	l = tl l;
+	x := dialog->prompt(gctxt, gtop.image, nil, title, msg, int hd l, tl l);
+	e.output(string x);
+	focus(e);
+}
--- /dev/null
+++ b/appl/cmd/mash/xeq.b
@@ -1,0 +1,543 @@
+#
+#	Command execution.
+#
+
+#
+#	Entry from parser.
+#
+Cmd.xeq(c: self ref Cmd, e: ref Env)
+{
+	if (e.flags & EDumping) {
+		s := c.text();
+		f := e.outfile();
+		f.puts(s);
+		if (s != nil && s[len s - 1] != '&')
+			f.putc(';');
+		f.putc('\n');
+		f.close();
+		f = nil;
+	}
+	if ((e.flags & ENoxeq) == 0)
+		c.xeqit(e, 1);
+}
+
+#
+#	Execute a command.  Tail recursion.
+#
+Cmd.xeqit(c: self ref Cmd, e: ref Env, wait: int)
+{
+tail:	for (;;) {
+	if (c == nil)
+		return;
+	case c.op {
+	Csimple =>
+		c.simple(e, wait);
+	Casync =>
+		e = e.clone();
+		e.in = e.devnull();
+		e.wait = nil;
+		spawn c.left.xeqit(e, 1);
+	Cgroup =>
+		if (c.redirs != nil) {
+			(ok, in, out) := mkredirs(e, c.redirs);
+			if (!ok)
+				return;
+			e = e.copy();
+			e.in = in;
+			e.out = out;
+			c.left.xeqit(e, 1);	
+		} else {
+			c = c.left;
+			continue tail;
+		}
+	Csubgroup =>
+		e = e.clone();
+		if (c.redirs != nil) {
+			(ok, in, out) := mkredirs(e, c.redirs);
+			if (!ok)
+				return;
+			e.in = in;
+			e.out = out;
+		}
+		c = c.left;
+		continue tail;
+	Cseq =>
+		c.left.xeqit(e, 1);	
+		c = c.right;
+		continue tail;
+	Cpipe =>
+		do {
+			fds := e.pipe();
+			if (fds == nil)
+				return;
+			n := e.clone();
+			n.out = fds[0];
+			c.left.xeqit(n, 0);
+			n = nil;
+			e = e.clone();
+			e.in = fds[1];
+			fds = nil;
+			c = c.right;
+		} while (c.op == Cpipe);
+		continue tail;
+	Cif =>
+		t := c.left.truth(e);
+		if (c.right.op == Celse) {
+			if (t)
+				c.right.left.xeqit(e, wait);
+			else
+				c.right.right.xeqit(e, wait);
+		} else if (t)
+			c.right.xeqit(e, wait);
+	Celse =>
+		panic("unexpected else");
+	Cwhile =>
+		while (c.left.truth(e))
+			c.right.xeqit(e, wait);
+	Cfor =>
+		(ok, l) := evalw(c.words, e);
+		if (!ok)
+			return;
+		s := c.item.word.text;
+		c = c.left;
+		while (l != nil) {
+			e.let(s, (hd l) :: nil);
+			c.xeqit(e, 1);
+			l = tl l;
+		}
+	Ccase =>
+		(s1, l1) := c.left.eeval(e);
+		r := c.right;
+		while (r != nil) {
+			l := r.left;
+			(s2, l2) := l.left.eeval(e);
+			if (match2(e, s1, l1, s2, l2)) {
+				c = l.right;
+				continue tail;
+			}
+			r = r.right;
+		}
+	Ceq =>
+		c.assign(e, 0);
+	Cdefeq =>
+		c.assign(e, 1);
+	Cfn =>
+		(s, nil, nil) := c.item.ieval(e);
+		if (!ident(s)) {
+			e.report("bad function name");
+			return;
+		}
+		e.define(s, c.left);
+	Crescue =>
+		e.report("rescue not implemented");
+	Cdepend =>
+		c.depend(e);
+	Crule =>
+		c.rule(e);
+	* =>
+		sys->print("number %d\n", c.op);
+	} return; } # tail recursion
+}
+
+#
+#	Execute quote or backquote generator.  Return generated item.
+#
+Cmd.quote(c: self ref Cmd, e: ref Env, back: int): ref Item
+{
+	e = e.copy();
+	fds := e.pipe();
+	if (fds == nil)
+		return nil;
+	e.out = fds[0];
+	in := bufio->fopen(fds[1], Bufio->OREAD);
+	if (in == nil)
+		e.couldnot("fopen", "pipe");
+	c.xeqit(e, 0);
+	fds = nil;
+	e = nil;
+	if (back) {
+		l: list of string;
+		while ((s := in.gets('\n')) != nil) {
+			(nil, r) := sys->tokenize(s, " \t\r\n");
+			l = prepend(l, r);
+		}
+		return Item.iteml(revstrs(l));
+	} else {
+		s := in.gets('\n');
+		if (s != nil && s[len s - 1] == '\n')
+			s = s[:len s - 1];
+		return Item.itemw(s);
+	}
+}
+
+#
+#	Execute serve generator.
+#
+Cmd.serve(c: self ref Cmd, e: ref Env, write: int): ref Item
+{
+	e = e.clone();
+	fds := e.pipe();
+	if (fds == nil)
+		return nil;
+	if (write)
+		e.in = fds[0];
+	else
+		e.out = fds[0];
+	s := e.servefd(fds[1], write);
+	if (s == nil)
+		return nil;
+	c.xeqit(e, 0);
+	return Item.itemw(s);
+}
+
+#
+#	Expression evaluation, first pass.
+#	Parse tree is copied and word items are evaluated.
+#	nil return for error is propagated.
+#
+Cmd.eeval1(c: self ref Cmd, e: ref Env): ref Cmd
+{
+	case c.op {
+	Cword =>
+		l := c.item.ieval1(e);
+		if (l == nil)
+			return nil;
+		return Cmd.cmd1i(Cword, nil, l);
+	Chd or Ctl or Clen or Cnot =>
+		l := c.left.eeval1(e);
+		if (l == nil)
+			return nil;
+		return Cmd.cmd1(c.op, l);
+	Ccaret or Ccons or Ceqeq or Cnoteq or Cmatch =>
+		l := c.left.eeval1(e);
+		r := c.right.eeval1(e);
+		if (l == nil || r == nil)
+			return nil;
+		return Cmd.cmd2(c.op, l, r);
+	}
+	panic("expr1: bad op");
+	return nil;
+}
+
+#
+#	Expression evaluation, second pass.
+#	Returns a tuple (singleton, list, expand flag).
+#
+Cmd.eeval2(c: self ref Cmd, e: ref Env): (string, list of string, int)
+{
+	case c.op {
+	Cword =>
+		return c.item.ieval2(e);
+	Clist =>
+		return (nil, c.value, 0);
+	Ccaret =>
+		(s1, l1, x1) := c.left.eeval2(e);
+		(s2, l2, x2) := c.right.eeval2(e);
+		return caret(s1, l1, x1, s2, l2, x2);
+	Chd =>
+		(s, l, x) := c.left.eeval2(e);
+		if (s != nil)
+			return (s, nil, x);
+		if (l != nil)
+			return (hd l, nil, 0);
+	Ctl =>
+		(s, l, nil) := c.left.eeval2(e);
+		if (s != nil)
+			break;
+		if (l != nil)
+			return (nil, tl l, 0);
+	Clen =>
+		(s, l, nil) := c.left.eeval2(e);
+		if (s != nil)
+			return ("1", nil, 0);
+		return (string len l, nil, 0);
+	Cnot =>
+		(s, l, nil) := c.left.eeval2(e);
+		if (s == nil && l == nil)
+			return (TRUE, nil, 0);
+	Ccons =>
+		(s1, l1, nil) := c.left.eeval2(e);
+		(s2, l2, nil) := c.right.eeval2(e);
+		if (s1 != nil) {
+			if (s2 != nil)
+				return (nil, s1 :: s2 :: nil, 0);
+			if (l2 != nil)
+				return (nil, s1 :: l2, 0);
+			return (s1, nil, 0);
+		} else if (l1 != nil) {
+			if (s2 != nil)
+				return (nil, prepend(s2 :: nil, revstrs(l1)), 0);
+			if (l2 != nil)
+				return (nil, prepend(l2, revstrs(l1)), 0);
+			return (nil, l1, 0);
+		} else
+			return (s2, l2, 0);
+	Ceqeq =>
+		if (c.evaleq(e))
+			return (TRUE, nil, 0);
+	Cnoteq =>
+		if (!c.evaleq(e))
+			return (TRUE, nil, 0);
+	Cmatch =>
+		if (c.evalmatch(e))
+			return (TRUE, nil, 0);
+	* =>
+		panic("expr2: bad op");
+	}
+	return (nil, nil, 0);
+}
+
+#
+#	Evaluate expression.  1st pass, 2nd pass, maybe glob.
+#
+Cmd.eeval(c: self ref Cmd, e: ref Env): (string, list of string)
+{
+	c = c.eeval1(e);
+	if (c == nil)
+		return (nil, nil);
+	(s, l, x) := c.eeval2(e);
+	if (x && s != nil)
+		(s, l) = glob(e, s);
+	return (s, l);
+}
+
+#
+#	Assignment - let or set.
+#
+Cmd.assign(c: self ref Cmd, e: ref Env, def: int)
+{
+	i := c.item;
+	if (i == nil)
+		return;
+	(ok, v) := evalw(c.words, e);
+	if (!ok)
+		return;
+	s := c.item.word.text;
+	if (def)
+		e.let(s, v);
+	else
+		e.set(s, v);
+}
+
+#
+#	Evaluate command and test for non-empty.
+#
+Cmd.truth(c: self ref Cmd, e: ref Env): int
+{
+	(s, l) := c.eeval(e);
+	return s != nil || l != nil;
+}
+
+#
+#	Evaluate word.
+#
+evalw(l: list of ref Item, e: ref Env): (int, list of string)
+{
+	if (l == nil)
+		return (1, nil);
+	w := pass1(e, l);
+	if (w == nil)
+		return (0, nil);
+	return (1, pass2(e, w));
+}
+
+#
+#	Evaluate list of items, pass 1 - reverses.
+#
+pass1(e: ref Env, l: list of ref Item): list of ref Item
+{
+	r: list of ref Item;
+	while (l != nil) {
+		i := (hd l).ieval1(e);
+		if (i == nil)
+			return nil;
+		r = i :: r;
+		l = tl l;
+	}
+	return r;
+}
+
+#
+#	Evaluate list of items, pass 2 with globbing - reverses (restores order).
+#
+pass2(e: ref Env, l: list of ref Item): list of string
+{
+	r: list of string;
+	while (l != nil) {
+		(s, t, x) := (hd l).ieval2(e);
+		if (x && s != nil)
+			(s, t) = glob(e, s);
+		if (s != nil)
+			r = s :: r;
+		else if (t != nil)
+			r = prepend(r, revstrs(t));
+		l = tl l;
+	}
+	return r;
+}
+
+#
+#	Simple command.  Maybe a function.
+#
+Cmd.simple(c: self ref Cmd, e: ref Env, wait: int)
+{
+	w := pass1(e, c.words);
+	if (w == nil)
+		return;
+	s := pass2(e, w);
+	if (s == nil)
+		return;
+	if (e.flags & EEcho)
+		echo(e, s);
+	(ok, in, out) := mkredirs(e, c.redirs);
+	if (ok)
+		e.runit(s, in, out, wait);
+}
+
+#
+#	Cmd name and arglist.  Maybe a function.
+#
+Env.runit(e: self ref Env, s: list of string, in, out: ref Sys->FD, wait: int)
+{
+	d := e.func(hd s);
+	if (d != nil) {
+		if (e.level >= MAXELEV) {
+			e.report(hd s + ": function nesting too deep");
+			return;
+		}
+		e = e.copy();
+		e.level++;
+		e.in = in;
+		e.out = out;
+		e.local = Stab.new();
+		e.local.assign(ARGS, tl s);
+		d.xeqit(e, wait);
+	} else
+		exec(s, e, in, out, wait);
+}
+
+#
+#	Item evaluation, first pass.  Copy parse tree.  Expand variables.
+#	Call first pass of expression evaluation.  Execute generators.
+#
+Item.ieval1(i: self ref Item, e: ref Env): ref Item
+{
+	if (i == nil)
+		return nil;
+	case i.op {
+	Icaret or Iicaret =>
+		l := i.left.ieval1(e);
+		r := i.right.ieval1(e);
+		if (l == nil || r == nil)
+			return nil;
+		return Item.item2(i.op, l, r);
+	Idollar or Idollarq=>
+		s := e.dollar(i.word.text);
+		if (s == nil) {
+			e.undefined(i.word.text);
+			return nil;
+		}
+		if (s.value == empty)
+			return Item.itemw(nil);
+		if (i.op == Idollar)
+			return Item.iteml(s.value);
+		else
+			return Item.itemw(concat(s.value));
+	Iword or Imatch =>
+		return i;
+	Iexpr =>
+		l := i.cmd.eeval1(e);
+		if (l == nil)
+			return nil;
+		return Item.itemc(Iexpr, l);
+	Ibackq =>
+		return i.cmd.quote(e, 1);
+	Iquote =>
+		return i.cmd.quote(e, 0);
+	Iinpipe =>
+		return i.cmd.serve(e, 0);
+	Ioutpipe =>
+		return i.cmd.serve(e, 1);
+	}
+	panic("ieval1: bad op");
+	return nil;
+}
+
+#
+#	Item evaluation, second pass.  Outer level carets.  Expand matches.
+#	Call second pass of expression evaluation.  
+#
+Item.ieval2(i: self ref Item, e: ref Env): (string, list of string, int)
+{
+	case i.op {
+	Icaret or Iicaret =>
+		return i.caret(e);
+	Imatch =>
+		return (e.arg(i.word.text), nil, 0);
+	Idollar or Idollarq =>
+		panic("ieval2: unexpected $");
+	Iword =>
+		return (i.word.text, nil, i.word.flags & Wexpand);
+	Iexpr =>
+		return i.cmd.eeval2(e);
+	Ibackq or Iinpipe or Ioutpipe =>
+		panic("ieval2: unexpected generator");
+	}
+	panic("ieval2: bad op");
+	return (nil, nil, 0);
+}
+
+#
+#	Item evaluation.
+#
+Item.ieval(i: self ref Item, e: ref Env): (string, list of string, int)
+{
+	i = i.ieval1(e);
+	if (i == nil)
+		return (nil, nil, 0);
+	return i.ieval2(e);
+}
+
+#
+#	Redirection item evaluation.
+#
+Item.reval(i: self ref Item, e: ref Env): (int, string)
+{
+	(s, l, nil) := i.ieval(e);
+	if (s == nil) {
+		if (l == nil)
+			e.report("null redirect");
+		else
+			e.report("list for redirect");
+		return (0, nil);
+	}
+	return (1, s);
+}
+
+#
+#	Make redirection names.
+#
+mkrdnames(e: ref Env, l: list of ref Redir): (int, array of string)
+{
+	f := array[Rcount] of string;
+	while (l != nil) {
+		r := hd l;
+		(ok, s) := r.word.reval(e);
+		if (!ok)
+			return (0, nil);
+		f[r.op] = s;
+		l = tl l;
+	}
+	return (1, f);
+}
+
+#
+#	Perform redirections.
+#
+mkredirs(e: ref Env, l: list of ref Redir): (int, ref Sys->FD, ref Sys->FD)
+{
+	(ok, f) := mkrdnames(e, l);
+	if (!ok)
+		return (0, nil, nil);
+	return redirect(e, f, e.in, e.out);
+}
--- /dev/null
+++ b/appl/cmd/mc.b
@@ -1,0 +1,154 @@
+implement Mc;
+
+include "sys.m";
+	sys: Sys;
+	open, read, fprint, fildes, tokenize,
+	ORDWR, OREAD, OWRITE: import sys;
+include "draw.m";
+	draw: Draw;
+	Font: import draw;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "arg.m";
+
+font: ref Font;
+columns := 65;
+tabwid := 0;
+mintab := 1;
+
+Mc: module{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if((bufio = load Bufio Bufio->PATH) == nil)
+		fatal("can't load " + Bufio->PATH);
+	draw = load Draw Draw->PATH;
+	if((arg := load Arg Arg->PATH) == nil)
+		fatal("can't load " + Arg->PATH);
+
+	getwidth(ctxt);
+	arg->init(argv);
+	arg->setusage("mc [-c columns] [file ...]");
+	while((c:=arg->opt()) != 0)
+		case c {
+		'c' =>	columns = int arg->earg() * mintab;
+		* =>		arg->usage();
+		}
+	argv = arg->argv();
+	if(len argv == 0)
+		argv = "/fd/0" :: nil;
+
+	a := array[1024] of (string, int);
+	n := 0;
+	maxwidth := 0;
+	for(; argv!=nil; argv=tl argv){
+		if((bin:=bufio->open(hd argv, OREAD)) == nil){
+			fprint(fildes(2), "mc: can't open %s: %r\n", hd argv);
+			continue;
+		}
+		while((s:=bin.gets('\n')) != nil){
+			if(s[len s-1] == '\n')
+				s = s[0:len s-1];
+			if(n == len a)
+				a = (array[n+1024] of (string, int))[0:] = a;
+			a[n].t0 = s;
+			a[n].t1 = wordsize(s);
+			if(a[n].t1 > maxwidth)
+				maxwidth = a[n].t1;
+			n++;
+		}
+		bin.close();
+	}
+	outcols(a[:n], maxwidth);
+}
+
+outcols(words: array of (string, int), maxwidth: int)
+{
+	maxwidth = nexttab(maxwidth+mintab-1);
+	numcols := columns / maxwidth;
+	if(numcols <= 0)
+		numcols = 1;
+	nwords := len words;
+	nlines := (nwords+numcols-1) / numcols;
+	bout := bufio->fopen(fildes(1), OWRITE);
+	for(i := 0; i < nlines; i++){
+		col := endcol := 0;
+		for(j:=i; j<nwords; j+=nlines){
+			endcol += maxwidth;
+			bout.puts(words[j].t0);
+			col += words[j].t1;
+			if(j+nlines < nwords){
+				while(col < endcol){
+					if(tabwid)
+						bout.putc('\t');
+					else
+						bout.putc(' ');
+					col = nexttab(col);
+				}
+			}
+		}
+		bout.putc('\n');
+	}
+	bout.close();
+}
+
+wordsize(s: string): int
+{
+	if(font != nil)
+		return font.width(s);
+	return len s;
+}
+
+nexttab(col: int): int
+{
+	if(tabwid){
+		col += tabwid;
+		col -= col%tabwid;
+		return col;
+	}
+	return col+1;
+}
+
+getwidth(ctxt: ref Draw->Context)
+{
+	if(ctxt == nil || draw == nil)
+		return;
+	if((wid := rf("/env/acmewin")) == nil)
+		return;
+	if((fd := open("/chan/" + wid + "/ctl", ORDWR)) == nil)
+		return;
+	buf := array[256] of byte;
+	if((n := read(fd, buf, len buf)) <= 0)
+		return;
+	(nf, f) := tokenize(string buf[:n], " ");
+	if(nf != 8)
+		return;
+	f0 := tl tl tl tl tl f;
+	if((font = Font.open(ctxt.display, hd tl f0)) == nil)
+		return;
+	tabwid = int hd tl tl f0;
+	mintab = font.width("0");	
+	columns = int hd f0;
+}
+
+fatal(s: string)
+{
+	fprint(fildes(2), "mc: %s: %r\n", s);
+	raise "fail:"+s;
+}
+
+rf(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	b := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, b, len b);
+	if(n < 0)
+		return nil;
+	return string b[0:n];
+}
--- /dev/null
+++ b/appl/cmd/md5sum.b
@@ -1,0 +1,65 @@
+implement MD5sum;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+
+MD5sum: module
+{
+	init: fn(nil : ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	kr = load Keyring Keyring->PATH;
+	a := tl argv;
+	err := 0;
+	if(a != nil){
+		for( ; a != nil; a = tl a) {
+			s := hd a;
+			fd := sys->open(s, Sys->OREAD);
+			if (fd == nil) {
+				sys->fprint(stderr, "md5sum: cannot open %s: %r\n", s);
+				err = 1;
+			} else
+				err |= md5sum(fd, s);
+		}
+	} else
+		err |= md5sum(sys->fildes(0), "");
+	if(err)
+		raise "fail:error";
+}
+
+md5sum(fd: ref Sys->FD, file: string): int
+{
+	err := 0;
+	buf := array[Sys->ATOMICIO] of byte;
+	state: ref Keyring->DigestState = nil;
+	nbytes := big 0;
+	while((nr := sys->read(fd, buf, len buf)) > 0){
+		state = kr->md5(buf, nr, nil, state);
+		nbytes += big nr;
+	}
+	if(nr < 0) {
+		sys->fprint(stderr, "md5sum: error reading %s: %r\n", file);
+		err = 1;
+	}
+	digest := array[Keyring->MD5dlen] of byte;
+	kr->md5(buf, 0, digest, state);
+	sum := "";
+	for(i:=0; i<len digest; i++)
+		sum += sys->sprint("%2.2ux", int digest[i]);
+	if(file != nil)
+		sys->print("%s\t%s\n", sum, file);
+	else
+		sys->print("%s\n", sum);
+	return err;
+}
--- /dev/null
+++ b/appl/cmd/mdb.b
@@ -1,0 +1,537 @@
+implement Mdb;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+	print, sprint: import sys;
+
+include "draw.m";
+include "string.m";
+	str: String;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "dis.m";
+	dis: Dis;
+	Inst, Type, Data, Link, Mod: import dis;
+	XMAGIC: import Dis;
+	MUSTCOMPILE, DONTCOMPILE: import Dis;
+	AMP, AFP, AIMM, AXXX, AIND, AMASK: import Dis;
+	ARM, AXNON, AXIMM, AXINF, AXINM: import Dis;
+	DEFB, DEFW, DEFS, DEFF, DEFA, DIND, DAPOP, DEFL: import Dis;
+disfile: string;
+m: ref Mod;
+
+Mdb: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+mfd: ref Sys->FD;
+dot := 0;
+lastaddr := 0;
+count := 1;
+
+atoi(s: string): int
+{
+        b := 10;
+        if(s == nil)
+                return 0;
+        if(s[0] == '0') {
+                b = 8;
+                s = s[1:];
+                if(s == nil)
+                        return 0;
+                if(s[0] == 'x' || s[0] == 'X') {
+                        b = 16;
+                        s = s[1:];
+                }
+        }
+        n: int;
+        (n, nil) = str->toint(s, b);
+        return n;
+}
+
+eatws(s: string): string
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] != ' ' && s[i] != '\t')
+			return s[i:];
+	return nil;
+}
+
+eatnum(s: string): string
+{
+	if(len s == 0)
+		return s;
+	while(gotnum(s) || gotalpha(s))
+		s = s[1:];
+	return s;
+}
+
+gotnum(s: string): int
+{
+	if(len s == 0)
+		return 0;
+	if(s[0] >= '0' && s[0] <= '9')
+		return 1;
+	else
+		return 0;
+}
+
+gotalpha(s: string): int
+{
+	if(len s == 0)
+		return 0;
+	if((s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z'))
+		return 1;
+	else
+		return 0;
+}
+
+getexpr(s: string): (string, int, int)
+{
+	ov: int;
+	v := 0;
+	op := '+';
+	for(;;) {
+		ov = v;
+		s = eatws(s);
+		if(s == nil)
+			return (nil, 0, 0);
+		if(s[0] == '.' || s[0] == '+' || s[0] == '^') {
+			v = dot;
+			s = s[1:];
+		} else if(s[0] == '"') {
+			v = lastaddr;
+			s = s[1:];
+		} else if(s[0] == '(') {
+			(s, v, nil) = getexpr(s[1:]);
+			s = s[1:];
+		} else if(gotnum(s)) {
+			v = atoi(s);
+			s = eatnum(s);
+		} else
+			return (s, 0, 0);
+		case op {
+		'+' => v = ov+v;
+		'-' => v = ov-v;
+		'*' => v = ov*v;
+		'%' => v = ov/v;
+		'&' => v = ov&v;
+		'|' => v = ov|v;
+		}
+		if(s == nil)
+			return (nil, v, 1);
+		case s[0] {
+		'+' or '-' or '*' or '%' or '&' or '|' =>
+			op = s[0]; s = s[1:];
+		* =>
+			return (eatws(s), v, 1);
+		}
+	}
+}
+
+lastcmd := "";
+
+docmd(s: string)
+{
+	ok: int;
+	n: int;
+	s = eatws(s);
+	(s, n, ok) = getexpr(s);
+	if(ok) {
+		dot = n; 
+		lastaddr = n;
+	}
+	count = 1;
+	if(s != nil && s[0] == ',') {
+		(s, n, ok) = getexpr(s[1:]);
+		if(ok)
+			count = n;
+	}
+	if(s == nil && (s = lastcmd) == nil) 
+		return;
+	lastcmd = s;
+	cmd := s[0];
+	case cmd {
+	'?' or '/' =>
+		case s[1] {
+		'w' =>
+			writemem(2, s[2:]);
+		'W' =>
+			writemem(4, s[2:]);
+		'i' =>
+			das();
+		* =>
+			dumpmem(s[1:], cmd);
+		}
+	'$' =>
+		case s[1] {
+		'D' =>
+			desc();
+		'h' =>
+			hdr();
+		'l' =>
+			link();
+		'i' =>
+			imports();
+		'd' =>
+			dat();
+		'H' =>
+			handlers();
+		's' =>
+			if(m != nil)
+				print("%s\n", m.srcpath);
+		}
+	'=' =>
+		dumpmem(s[1:], cmd);
+	* =>
+		sys->fprint(stderr, "invalid cmd: %c\n", cmd);
+	}
+}
+
+octal(n: int, d: int): string
+{
+	s: string;
+	do {
+		s = string (n%8) + s;
+		n /= 8;
+	} while(d-- > 1);
+	return "0" + s;
+}
+
+printable(c: int): string
+{
+	case c {
+	32 to 126 =>
+		return sprint("%c", c);
+	'\n' =>
+		return "\\n";
+	'\r' =>
+		return "\\r";
+	'\b' =>
+		return "\\b";
+	'\a' =>
+		return "\\a";
+	'\v' =>
+		return "\\v";
+	* =>
+		return sprint("\\x%2.2x", c);
+	}
+		
+}
+
+dumpmem(s: string, t: int)
+{
+	n := 0;
+	c := count;
+	while(c-- > 0) for(p:=0; p<len s; p++) {
+		fmt := s[p];
+		case fmt {
+		'b' or 'c' or 'C' =>
+			n = 1;
+		'x' or 'd' or 'u' or 'o' =>
+			n = 2; 
+		'X' or 'D' or 'U' or 'O' =>
+			n = 4;
+		's' or 'S' or 'r' or 'R' =>
+			print("'%c' format not yet supported\n", fmt);
+			continue;
+		'n' =>
+			print("\n");
+			continue;
+		'+' =>
+			dot++;
+			continue;
+		'-' =>
+			dot--;
+			continue;
+		'^' =>
+			dot -= n;
+			continue;
+		* =>
+			print("unknown format '%c'\n", fmt);
+			continue;
+		}
+		b := array[n] of byte;
+		v: int;
+		if(t == '=')
+			v = dot;
+		else {
+			sys->seek(mfd, big dot, Sys->SEEKSTART);
+			sys->read(mfd, b, len b);
+			v = 0;
+			for(i := 0; i < n; i++)
+				v |= int b[i] << (8*i);
+		}
+		case fmt {
+		'c' => print("%c", v);
+		'C' => print("%s", printable(v));
+		'b' => print("%#2.2ux ", v);
+		'x' => print("%#4.4ux ", v);
+		'X' => print("%#8.8ux ", v);
+		'd' => print("%-4d ", v);
+		'D' => print("%-8d ", v);
+		'u' => print("%-4ud ", v);
+		'U' => print("%-8ud ", v);
+		'o' => print("%s ", octal(v, 6));
+		'O' => print("%s ", octal(v, 11));
+		}
+		if(t != '=')
+			dot += n;
+	}
+	print("\n");
+}
+
+writemem(n: int, s: string)
+{
+	v: int;
+	ok: int;
+	s = eatws(s);
+	sys->seek(mfd, big dot, Sys->SEEKSTART);
+	for(;;) {
+		(s, v, ok) = getexpr(s);
+		if(!ok)
+			return;
+		b := array[n] of byte;
+		for(i := 0; i < n; i++)
+			b[i] = byte (v >> (8*i));
+		if (sys->write(mfd, b, len b) != len b)
+			sys->fprint(stderr, "mdb: write error: %r\n");
+	}
+}
+
+usage()
+{
+	sys->fprint(stderr, "usage: mdb [-w] file [command]\n");
+	raise "fail:usage";
+}
+
+writeable := 0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	str = load String String->PATH;
+	if (str == nil) {
+		sys->fprint(stderr, "mdb: cannot load %s: %r\n", String->PATH);
+		raise "fail:bad module";
+	}
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->fprint(stderr, "mdb: cannot load %s: %r\n", Bufio->PATH);
+		raise "fail:bad module";
+	}
+	dis = load Dis Dis->PATH;
+	dis->init();
+
+	if (len argv < 2)
+		usage();
+	if (argv != nil)
+		argv = tl argv;
+	if (argv != nil && len hd argv && (hd argv)[0] == '-') {
+		if (hd argv != "-w")
+			usage();
+		writeable = 1;
+		argv = tl argv;
+	}
+	if (argv == nil)
+		usage();
+	fname := hd argv;
+	argv = tl argv;
+	cmd := "";
+	if(argv != nil)
+		cmd = hd argv;
+
+	oflags := Sys->OREAD;
+	if (writeable)
+		oflags = Sys->ORDWR;
+	mfd = sys->open(fname, oflags);
+	if(mfd == nil) {
+		sys->fprint(stderr, "mdb: cannot open %s: %r\n", fname);
+		raise "fail:cannot open";
+	}
+	(m, nil) = dis->loadobj(fname);
+
+	if(cmd != nil)
+		docmd(cmd);
+	else {
+		stdin := bufio->fopen(sys->fildes(0), Sys->OREAD);
+		while ((s := stdin.gets('\n')) != nil) {
+			if (s[len s -1] == '\n')
+				s = s[0:len s - 1];
+			docmd(s);
+		}
+	}
+}
+
+link()
+{
+	if(m == nil || m.magic == 0)
+		return;
+
+	for(i := 0; i < m.lsize; i++) {
+		l := m.links[i];
+		print("	link %d,%d, 0x%ux, \"%s\"\n",
+					l.desc, l.pc, l.sig, l.name);
+	}
+}
+
+imports()
+{
+	if(m == nil || m.magic == 0)
+		return;
+
+	mi := m.imports;
+	for(i := 0; i < len mi; i++) {
+		a := mi[i];
+		for(j := 0; j < len a; j++) {
+			ai := a[j];
+			print("	import 0x%ux, \"%s\"\n", ai.sig, ai.name);
+		}
+	}
+}
+
+handlers()
+{
+	if(m == nil || m.magic == 0)
+		return;
+
+	hs := m.handlers;
+	for(i := 0; i < len hs; i++) {
+		h := hs[i];
+		tt := -1;
+		for(j := 0; j < len m.types; j++) {
+			if(h.t == m.types[j]) {
+				tt = j;
+				break;
+			}
+		}
+		print("	%d-%d, o=%d, e=%d t=%d\n", h.pc1, h.pc2, h.eoff, h.ne, tt);
+		et := h.etab;
+		for(j = 0; j < len et; j++) {
+			e := et[j];
+			if(e.s == nil)
+				print("		%d	*\n", e.pc);
+			else
+				print("		%d	\"%s\"\n", e.pc, e.s);
+		}
+	}
+}
+
+desc()
+{
+	if(m == nil || m.magic == 0)
+		return;
+
+	for(i := 0; i < m.tsize; i++) {
+		h := m.types[i];
+		s := sprint("	desc $%d, %d, \"", i, h.size);
+		for(j := 0; j < h.np; j++)
+			s += sprint("%.2ux", int h.map[j]);
+		s += "\"\n";
+		print("%s", s);
+	}
+}
+
+hdr()
+{
+	if(m == nil || m.magic == 0)
+		return;
+	s := sprint("%.8ux Version %d Dis VM\n", m.magic, m.magic - XMAGIC + 1);
+	s += sprint("%.8ux Runtime flags %s\n", m.rt, rtflag(m.rt));
+	s += sprint("%8d bytes per stack extent\n\n", m.ssize);
+
+
+	s += sprint("%8d instructions\n", m.isize);
+	s += sprint("%8d data size\n", m.dsize);
+	s += sprint("%8d heap type descriptors\n", m.tsize);
+	s += sprint("%8d link directives\n", m.lsize);
+	s += sprint("%8d entry pc\n", m.entry);
+	s += sprint("%8d entry type descriptor\n\n", m.entryt);
+
+	if(m.sign == nil)
+		s += "Module is Insecure\n";
+	print("%s", s);
+}
+
+rtflag(flag: int): string
+{
+	if(flag == 0)
+		return "";
+
+	s := "[";
+
+	if(flag & MUSTCOMPILE)
+		s += "MustCompile";
+	if(flag & DONTCOMPILE) {
+		if(flag & MUSTCOMPILE)
+			s += "|";
+		s += "DontCompile";
+	}
+	s[len s] = ']';
+
+	return s;
+}
+
+das()
+{
+	if(m == nil || m.magic == 0)
+		return;
+
+	for(i := dot;  count-- > 0 && i < m.isize; i++) {
+		if(i % 10 == 0)
+			print("#%d\n", i);
+		print("\t%s\n", dis->inst2s(m.inst[i]));
+	}
+}
+
+dat()
+{
+	if(m == nil || m.magic == 0)
+		return;
+	print("	var @mp, %d\n", m.types[0].size);
+
+	s := "";
+	for(d := m.data; d != nil; d = tl d) {
+		pick dat := hd d {
+		Bytes =>
+			s = sprint("\tbyte @mp+%d", dat.off);
+			for(n := 0; n < dat.n; n++)
+				s += sprint(",%d", int dat.bytes[n]);
+		Words =>
+			s = sprint("\tword @mp+%d", dat.off);
+			for(n := 0; n < dat.n; n++)
+				s += sprint(",%d", dat.words[n]);
+		String =>
+			s = sprint("\tstring @mp+%d, \"%s\"", dat.off, mapstr(dat.str));
+		Reals =>
+			s = sprint("\treal @mp+%d", dat.off);
+			for(n := 0; n < dat.n; n++)
+				s += sprint(", %g", dat.reals[n]);
+			break;
+		Array =>
+			s = sprint("\tarray @mp+%d,$%d,%d", dat.off, dat.typex, dat.length);
+		Aindex =>
+			s = sprint("\tindir @mp+%d,%d", dat.off, dat.index);
+		Arestore =>
+			s = "\tapop";
+			break;
+		Bigs =>
+			s = sprint("\tlong @mp+%d", dat.off);
+			for(n := 0; n < dat.n; n++)
+				s += sprint(", %bd", dat.bigs[n]);
+		}
+		print("%s\n", s);
+	}
+}
+
+mapstr(s: string): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '\n')
+			s = s[0:i] + "\\n" + s[i+1:];
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/memfs.b
@@ -1,0 +1,648 @@
+implement MemFS;
+
+include "sys.m";
+	sys: Sys;
+	OTRUNC, ORCLOSE, OREAD, OWRITE: import Sys;
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+include "styxlib.m";
+	styxlib: Styxlib;
+	Styxserver: import styxlib;
+include "draw.m";
+include "arg.m";
+
+MemFS: module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+
+blksz : con 512;
+Efull : con "filesystem full";
+
+Memfile : adt {
+	name : string;
+	owner : string;
+	qid : Sys->Qid;
+	perm : int;
+	atime : int;
+	mtime : int;
+	nopen : int;
+	data : array of array of byte;			# allocated in blks, no holes
+	length : int;
+	parent : cyclic ref Memfile;	# Dir entry linkage
+	kids : cyclic ref Memfile;
+	prev : cyclic ref Memfile;
+	next : cyclic ref Memfile;
+	hashnext : cyclic ref Memfile;	# Qid hash linkage
+};
+
+Qidhash : adt {
+	buckets : array of ref Memfile;
+	nextqid : int;
+	new : fn () : ref Qidhash;
+	add : fn (h : self ref Qidhash, mf : ref Memfile);
+	remove : fn (h : self ref Qidhash, mf : ref Memfile);
+	lookup : fn (h : self ref Qidhash, qid : Sys->Qid) : ref Memfile;
+};
+
+timefd: ref Sys->FD;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	styx = checkload(load Styx Styx->PATH, Styx->PATH);
+	styxlib = checkload(load Styxlib Styxlib->PATH, Styxlib->PATH);
+	arg := checkload(load Arg Arg->PATH, Arg->PATH);
+
+	amode := Sys->MREPL;
+	maxsz := 16r7fffffff;
+	srv := 0;
+	mntpt := "/tmp";
+
+	arg->init(argv);
+	arg->setusage("memfs [-s] [-rab] [-m size] [mountpoint]");
+	while((opt := arg->opt()) != 0) {
+		case opt{
+		's' =>
+			srv = 1;
+		'r' =>
+			amode = Sys->MREPL;
+		'a' =>
+			amode = Sys->MAFTER;
+		'b' =>
+			amode = Sys->MBEFORE;
+		'm' =>
+			maxsz = int arg->earg();
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	arg = nil;
+	if (argv != nil)
+		mntpt = hd argv;
+
+	srvfd: ref Sys->FD;
+	mntfd: ref Sys->FD;
+	if (srv)
+		srvfd = sys->fildes(0);
+	else {
+		p := array [2] of ref Sys->FD;
+		if (sys->pipe(p) == -1)
+			error(sys->sprint("cannot create pipe: %r"));
+		mntfd = p[0];
+		srvfd = p[1];
+	}
+	styx->init();
+	styxlib->init(styx);
+	timefd = sys->open("/dev/time", sys->OREAD);
+
+	(tc, styxsrv) := Styxserver.new(srvfd);
+	if (srv)
+		memfs(maxsz, tc, styxsrv, nil);
+	else {
+		sync := chan of int;
+		spawn memfs(maxsz, tc, styxsrv, sync);
+		<-sync;
+		if (sys->mount(mntfd, nil, mntpt, amode | Sys->MCREATE, nil) == -1)
+			error(sys->sprint("failed to mount onto %s: %r", mntpt));
+	}
+}
+
+checkload[T](x: T, p: string): T
+{
+	if(x == nil)
+		error(sys->sprint("cannot load %s: %r", p));
+	return x;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+error(e: string)
+{
+	sys->fprint(stderr(), "memfs: %s\n", e);
+	raise "fail:error";
+}
+
+freeblks: int;
+
+memfs(maxsz : int, tc : chan of ref Tmsg, srv : ref Styxserver, sync: chan of int)
+{
+	sys->pctl(Sys->NEWNS, nil);
+	if (sync != nil)
+		sync <-= 1;
+	freeblks = (maxsz / blksz);
+	qhash := Qidhash.new();
+
+	# init root
+	root := newmf(qhash, nil, "memfs", srv.uname, 8r755 | Sys->DMDIR);
+	root.parent = root;
+
+	while((tmsg := <-tc) != nil) {
+#		sys->print("%s\n", tmsg.text());
+	Msg:
+		pick tm := tmsg {
+		Readerror =>
+			break;
+		Version =>
+			srv.devversion(tm);
+		Auth =>
+			srv.devauth(tm);
+		Flush =>
+			srv.reply(ref Rmsg.Flush(tm.tag));
+		Walk =>
+			(err, c, mf) := fidtomf(srv, qhash, tm.fid);
+			if (err != "") {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+			nc: ref styxlib->Chan;
+			if (tm.newfid != tm.fid) {
+				nc = srv.clone(c, tm.newfid);
+				if (nc == nil) {
+					srv.reply(ref Rmsg.Error(tm.tag, "fid in use"));
+					continue;
+				}
+				c = nc;
+			}
+			qids: array of Sys->Qid;
+			if (len tm.names > 0) {
+				oqid := c.qid;
+				opath := c.path;
+				qids = array[len tm.names] of Sys->Qid;
+				wmf := mf;
+				for (i := 0; i < len tm.names; i++) {
+					wmf = dirlookup(wmf, tm.names[i]);
+					if (wmf == nil) {
+						if (nc == nil) {
+							c.qid = oqid;
+							c.path = opath;
+						} else
+							srv.chanfree(nc);
+						if (i == 0)
+							srv.reply(ref Rmsg.Error(tm.tag, Styxlib->Enotfound));
+						else
+							srv.reply(ref Rmsg.Walk(tm.tag, qids[0:i]));
+						break Msg;
+					}
+					c.qid = wmf.qid;
+					qids[i] = wmf.qid;
+				}
+			}
+			srv.reply(ref Rmsg.Walk(tm.tag, qids));
+		Open =>
+			(err, c, mf) := fidtomf(srv, qhash, tm.fid);
+			if (err == "" && c.open)
+				err = Styxlib->Eopen;
+			if (err == "" && !modeok(tm.mode, mf.perm, c.uname, mf.owner))
+				err = Styxlib->Eperm;
+			if (err == "" && (mf.perm & Sys->DMDIR) && (tm.mode & (OTRUNC|OWRITE|ORCLOSE)))
+				err = Styxlib->Eperm;
+			if (err == "" && (tm.mode & ORCLOSE)) {
+				p := mf.parent;
+				if (p == nil || !modeok(OWRITE, p.perm, c.uname, p.owner))
+					err = Styxlib->Eperm;
+			}
+
+			if (err != "") {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+
+			c.open = 1;
+			c.mode = tm.mode;
+			c.qid.vers = mf.qid.vers;
+			mf.nopen++;
+			if ((tm.mode & OTRUNC) && !(mf.perm & Sys->DMAPPEND)) {
+				# OTRUNC cannot be set for a directory
+				# always at least one blk so don't need to check fs limit
+				freeblks += (len mf.data);
+				mf.data = nil;
+				freeblks--;
+				mf.data = array[1] of {* => array [blksz] of byte};
+				mf.length = 0;
+				mf.mtime = now();
+			}
+			srv.reply(ref Rmsg.Open(tm.tag, mf.qid, Styx->MAXFDATA));
+		Create =>
+			(err, c, parent) := fidtomf(srv, qhash, tm.fid);
+			if (err == "" && c.open)
+				err = Styxlib->Eopen;
+			if (err == "" && !(parent.qid.qtype & Sys->QTDIR))
+				err = Styxlib->Enotdir;
+			if (err == "" && !modeok(OWRITE, parent.perm, c.uname, parent.owner))
+				err = Styxlib->Eperm;
+			if (err == "" && (tm.perm & Sys->DMDIR) && (tm.mode & (OTRUNC|OWRITE|ORCLOSE)))
+				err = Styxlib->Eperm;
+			if (err == "" && dirlookup(parent, tm.name) != nil)
+				err = Styxlib->Eexists;
+
+			if (err != "") {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+
+			isdir := tm.perm & Sys->DMDIR;
+			if (!isdir && freeblks <= 0) {
+				srv.reply(ref Rmsg.Error(tm.tag, Efull));
+				continue;
+			}
+
+			# modify perms as per Styx specification...
+			perm : int;
+			if (isdir)
+				perm = (tm.perm&~8r777) | (parent.perm&tm.perm&8r777);
+			else
+				perm = (tm.perm&(~8r777|8r111)) | (parent.perm&tm.perm& 8r666);
+
+			nmf := newmf(qhash, parent, tm.name, c.uname, perm);
+			if (!isdir) {
+				freeblks--;
+				nmf.data = array[1] of {* => array [blksz] of byte};
+			}
+
+			# link in the new MemFile
+			nmf.next = parent.kids;
+			if (parent.kids != nil)
+				parent.kids.prev = nmf;
+			parent.kids = nmf;
+
+			c.open = 1;
+			c.mode = tm.mode;
+			c.qid = nmf.qid;
+			nmf.nopen = 1;
+			srv.reply(ref Rmsg.Create(tm.tag, nmf.qid, Styx->MAXFDATA));
+		Read =>
+			(err, c, mf) := fidtomf(srv, qhash, tm.fid);
+			if (err == "" && !c.open)
+				err = Styxlib->Ebadfid;
+
+			if (err != "") {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+			data: array of byte = nil;
+			if (mf.perm & Sys->DMDIR)
+				data = dirdata(mf, int tm.offset, tm.count);
+			else
+				data = filedata(mf, int tm.offset, tm.count);
+			mf.atime = now();
+			srv.reply(ref Rmsg.Read(tm.tag, data));
+		Write =>
+			(err, c, mf) := fidtomf(srv, qhash, tm.fid);
+			if (c != nil && !c.open)
+				err = Styxlib->Ebadfid;
+			if (err == nil && (mf.perm & Sys->DMDIR))
+				err = Styxlib->Eperm;
+			if (err == nil)
+				err = writefile(mf, int tm.offset, tm.data);
+			if (err != nil) {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+			srv.reply(ref Rmsg.Write(tm.tag, len tm.data));
+		Clunk =>
+			(err, c, mf) := fidtomf(srv, qhash, tm.fid);
+			if (c != nil)
+				srv.chanfree(c);
+			if (err != nil) {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+			if (c.open) {
+				if (c.mode & ORCLOSE)
+					unlink(mf);
+				mf.nopen--;
+				freeblks += delfile(qhash, mf);
+			}
+			srv.reply(ref Rmsg.Clunk(tm.tag));
+		Stat =>
+			(err, nil, mf) := fidtomf(srv, qhash, tm.fid);
+			if (err != nil) {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+			srv.reply(ref Rmsg.Stat(tm.tag, fileinfo(mf)));
+		Remove =>
+			(err, c, mf) := fidtomf(srv, qhash, tm.fid);
+			if (err != nil) {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+			srv.chanfree(c);
+			parent := mf.parent;
+			if (!modeok(OWRITE, parent.perm, c.uname, parent.owner))
+				err = Styxlib->Eperm;
+			if (err == "" && (mf.perm & Sys->DMDIR) && mf.kids != nil)
+				err = "directory not empty";
+			if (err == "" && mf == root)
+				err = "root directory";
+			if (err != nil) {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+
+			unlink(mf);
+			if (c.open)
+				mf.nopen--;
+			freeblks += delfile(qhash, mf);
+			srv.reply(ref Rmsg.Remove(tm.tag));
+		Wstat =>
+			(err, c, mf) := fidtomf(srv, qhash, tm.fid);
+			stat := tm.stat;
+
+			if (err == nil && stat.name != mf.name) {
+				parent := mf.parent;
+				if (!modeok(OWRITE, parent.perm, c.uname, parent.owner))
+					err = Styxlib->Eperm;
+				else if (dirlookup(parent, stat.name) != nil)
+					err = Styxlib->Eexists;
+			}
+			if (err == nil && (stat.mode != mf.perm || stat.mtime != mf.mtime)) {
+				if (c.uname != mf.owner)
+					err = Styxlib->Eperm;
+			}
+			if (err != nil) {
+				srv.reply(ref Rmsg.Error(tm.tag, err));
+				continue;
+			}
+			isdir := mf.perm & Sys->DMDIR;
+			if(stat.name != nil)
+				mf.name = stat.name;
+			if(stat.mode != ~0)
+				mf.perm = stat.mode | isdir;
+			if(stat.mtime != ~0)
+				mf.mtime = stat.mtime;
+ 			if(stat.uid != nil)
+ 				mf.owner = stat.uid;
+			t := now();
+			mf.atime = t;
+			mf.parent.mtime = t;
+			# not supporting group id at the moment
+			srv.reply(ref Rmsg.Wstat(tm.tag));
+		Attach =>
+			c := srv.newchan(tm.fid);
+			if (c == nil) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxlib->Einuse));
+				continue;
+			}
+			c.uname = tm.uname;
+			c.qid = root.qid;
+			srv.reply(ref Rmsg.Attach(tm.tag, c.qid));
+		}
+	}
+}
+
+writefile(mf: ref Memfile, offset: int, data: array of byte): string
+{
+	if(mf.perm & Sys->DMAPPEND)
+		offset = mf.length;
+	startblk := offset/blksz;
+	nblks := ((len data + offset) - (startblk * blksz))/blksz;
+	lastblk := startblk + nblks;
+	need := lastblk + 1 - len mf.data;
+	if (need > 0) {
+		if (need > freeblks)
+			return Efull;
+		mf.data = (array [lastblk+1] of array of byte)[:] = mf.data;
+		freeblks -= need;
+	}
+	mf.length = max(mf.length, offset + len data);
+
+	# handle (possibly incomplete first block) separately
+	offset %= blksz;
+	end := min(blksz-offset, len data);
+	if (mf.data[startblk] == nil)
+		mf.data[startblk] = array [blksz] of byte;
+	mf.data[startblk++][offset:] = data[:end];
+
+	ix := blksz - offset;
+	while (ix < len data) {
+		if (mf.data[startblk] == nil)
+			mf.data[startblk] = array [blksz] of byte;
+		end = min(ix+blksz,len data);
+		mf.data[startblk++][:] = data[ix:end];
+		ix += blksz;
+	}
+	mf.mtime = now();
+	return nil;
+}
+
+filedata(mf: ref Memfile, offset, n: int): array of byte
+{
+	if (offset +n > mf.length)
+		n = mf.length - offset;
+	if (n == 0)
+		return nil;
+
+	data := array [n] of byte;
+	startblk := offset/blksz;
+	offset %= blksz;
+	rn := min(blksz - offset, n);
+	data[:] = mf.data[startblk++][offset:offset+rn];
+	ix := blksz - offset;
+	while (ix < n) {
+		rn = blksz;
+		if (ix+rn > n)
+			rn = n - ix;
+		data[ix:] = mf.data[startblk++][:rn];
+		ix += blksz;
+	}
+	return data;
+}
+
+QHSIZE: con 256;
+QHMASK: con QHSIZE-1;
+
+Qidhash.new() : ref Qidhash
+{
+	qh := ref Qidhash;
+	qh.buckets = array [QHSIZE] of ref Memfile;
+	qh.nextqid = 0;
+	return qh;
+}
+
+Qidhash.add(h : self ref Qidhash, mf : ref Memfile)
+{
+	path := h.nextqid++;
+	mf.qid = Sys->Qid(big path, 0, Sys->QTFILE);
+	bix := path & QHMASK;
+	mf.hashnext = h.buckets[bix];
+	h.buckets[bix] = mf;
+}
+
+Qidhash.remove(h : self ref Qidhash, mf : ref Memfile)
+{
+
+	bix := int mf.qid.path & QHMASK;
+	prev : ref Memfile;
+	for (cur := h.buckets[bix]; cur != nil; cur = cur.hashnext) {
+		if (cur == mf)
+			break;
+		prev = cur;
+	}
+	if (cur != nil) {
+		if (prev != nil)
+			prev.hashnext = cur.hashnext;
+		else
+			h.buckets[bix] = cur.hashnext;
+		cur.hashnext = nil;
+	}
+}
+
+Qidhash.lookup(h : self ref Qidhash, qid : Sys->Qid) : ref Memfile
+{
+	bix := int qid.path & QHMASK;
+	for (mf := h.buckets[bix]; mf != nil; mf = mf.hashnext)
+		if (mf.qid.path == qid.path)
+			break;
+	return mf;
+}
+
+newmf(qh : ref Qidhash, parent : ref Memfile, name, owner : string, perm : int) : ref Memfile
+{
+	# qid gets set by Qidhash.add()
+	t := now();
+	mf := ref Memfile (name, owner, Sys->Qid(big 0,0,Sys->QTFILE), perm, t, t, 0, nil, 0, parent, nil, nil, nil, nil);
+	qh.add(mf);
+	if(perm & Sys->DMDIR)
+		mf.qid.qtype = Sys->QTDIR;
+	return mf;
+}
+
+fidtomf(srv : ref Styxserver, qh : ref Qidhash, fid : int) : (string, ref Styxlib->Chan, ref Memfile)
+{
+	c := srv.fidtochan(fid);
+	if (c == nil)
+		return (Styxlib->Ebadfid, nil, nil);
+	mf := qh.lookup(c.qid);
+	if (mf == nil)
+		return (Styxlib->Enotfound, c, nil);
+	return (nil, c, mf);
+}
+
+unlink(mf : ref Memfile)
+{
+	parent := mf.parent;
+	if (parent == nil)
+		return;
+	if (mf.next != nil)
+		mf.next.prev = mf.prev;
+	if (mf.prev != nil)
+		mf.prev.next = mf.next;
+	else
+		mf.parent.kids = mf.next;
+	mf.parent = nil;
+	mf.prev = nil;
+	mf.next = nil;
+}
+
+delfile(qh : ref Qidhash, mf : ref Memfile) : int
+{
+	if (mf.nopen <= 0 && mf.parent == nil && mf.kids == nil
+	&& mf.prev == nil && mf.next == nil) {
+		qh.remove(mf);
+		nblks := len mf.data;
+		mf.data = nil;
+		return nblks;
+	}
+	return 0;
+}
+
+dirlookup(dir : ref Memfile, name : string) : ref Memfile
+{
+	if (name == ".")
+		return dir;
+	if (name == "..")
+		return dir.parent;
+	for (mf := dir.kids; mf != nil; mf = mf.next) {
+		if (mf.name == name)
+			break;
+	}
+	return mf;
+}
+
+access := array[] of {8r400, 8r200, 8r600, 8r100};
+modeok(mode, perm : int, user, owner : string) : int
+{
+	if(mode >= (OTRUNC|ORCLOSE|OREAD|OWRITE))
+		return 0;
+
+	# not handling groups!
+	if (user != owner)
+		perm <<= 6;
+	
+	if ((mode & OTRUNC) && !(perm & 8r200))
+		return 0;
+
+	a := access[mode &3];
+	if ((a & perm) != a)
+		return 0;
+	return 1;
+}
+
+dirdata(dir : ref Memfile, start, n : int) : array of byte
+{
+	data := array[Styx->MAXFDATA] of byte;
+	for (k := dir.kids; start > 0 && k != nil; k = k.next) {
+		a := styx->packdir(fileinfo(k));
+		start -= len a;
+	}
+	r := 0;
+	for (; r < n && k != nil; k = k.next) {
+		a := styx->packdir(fileinfo(k));
+		if(r+len a > n)
+			break;
+		data[r:] = a;
+		r += len a;
+	}
+	return data[0:r];
+}
+
+fileinfo(f : ref Memfile) : Sys->Dir
+{
+	dir := sys->zerodir;
+	dir.name = f.name;
+	dir.uid = f.owner;
+	dir.gid = "memfs";
+	dir.qid = f.qid;
+	dir.mode = f.perm;
+	dir.atime = f.atime;
+	dir.mtime = f.mtime;
+	dir.length = big f.length;
+	dir.dtype = 0;
+	dir.dev = 0;
+	return dir;
+}
+
+min(a, b : int) : int
+{
+	if (a < b)
+		return a;
+	return b;
+}
+
+max(a, b : int) : int
+{
+	if (a > b)
+		return a;
+	return b;
+}
+
+now(): int
+{
+	if (timefd == nil)
+		return 0;
+	buf := array[128] of byte;
+	sys->seek(timefd, big 0, 0);
+	n := sys->read(timefd, buf, len buf);
+	if(n < 0)
+		return 0;
+
+	t := (big string buf[0:n]) / big 1000000;
+	return int t;
+}
--- /dev/null
+++ b/appl/cmd/metamorph.b
@@ -1,0 +1,94 @@
+implement metamorph;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "string.m";
+include "imagefile.m";
+
+sys:	Sys;
+bufio:	Bufio;
+str:	String;
+draw:	Draw;
+
+FD:	import sys;
+Display: import draw;
+
+stderr:	ref FD;
+
+metamorph: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	if (bufio == nil) {
+		sys->fprint(stderr, "could not load %s: %r\n", Bufio->PATH);
+		exit;
+	}
+	draw = load Draw Draw->PATH;
+	if (draw == nil) {
+		sys->fprint(stderr, "could not load %s: %r\n", Draw->PATH);
+		exit;
+	}
+	ri := load RImagefile RImagefile->READGIFPATH;
+	if (ri == nil) {
+		sys->fprint(stderr, "could not load %s: %r\n", RImagefile->READGIFPATH);
+		exit;
+	}
+	ir := load Imageremap Imageremap->PATH;
+	if (ir == nil) {
+		sys->fprint(stderr, "could not load %s: %r\n", Imageremap->PATH);
+		exit;
+	}
+
+	if (len args < 2) {
+		sys->fprint(stderr, "Metamorph Usage:\n		metamorph <# of slides>\n\n");
+		return;
+		}
+
+	infile	:string;
+
+
+ 	(numslides, nil) := str->toint((hd (tl args)), 10);
+
+	for (count := 1;count <=numslides; count++) {
+
+		ri->init(bufio);
+		
+		if ( count < 10 )
+			infile= sys->sprint("img00%d.GIF",count);
+		if (( count >= 10 ) && ( count < 100))
+			infile= sys->sprint("img0%d.GIF",count);
+		if (count >= 100)
+			infile= sys->sprint("img%d.GIF",count);
+
+		outfile := sys->sprint("img%d.bit",count);
+		
+		inf := bufio->open(infile, Bufio->OREAD);
+		sys->print ("Reading %s\n",infile);
+		if (inf == nil) {
+			sys->fprint(stderr, "could not fopen(0): %r\n");
+			exit;
+		}
+		(gif, s) := ri->read(inf);
+		if (gif == nil) {
+			sys->fprint(stderr, "bad GIF: %s\n", s);
+			exit;
+		}
+		(im, e) := ir->remap(gif, ctxt.display, 1);
+		if (im == nil) {
+			sys->fprint(stderr, "bad remap: %s\n", e);
+			exit;
+		}
+		sys->print("Writing %s\n",outfile);
+		outf := sys->create(outfile, sys->OWRITE,438);
+		ctxt.display.writeimage(outf, im);
+		outf = nil;
+	}
+}
--- /dev/null
+++ b/appl/cmd/mk/ar.m
@@ -1,0 +1,26 @@
+#
+#	initially generated by c2l
+#
+
+Ar: module
+{
+	PATH: con "ar.dis";
+
+	ARMAG: con "!<arch>\n";
+	SARMAG: con 8;
+	ARFMAG: con "`\n";
+	SARNAME: con 16;
+
+	ar_hdr: adt{
+		name: array of byte;	#  SARNAME
+		date: array of byte;	#  12
+		uid: array of byte;	#  6
+		gid: array of byte;	#  6
+		mode: array of byte;	#  8
+		size: array of byte;	#  10
+		fmag: array of byte;	#  2
+	};
+
+	SAR_HDR: con 60;
+
+};
--- /dev/null
+++ b/appl/cmd/mk/mk.b
@@ -1,0 +1,4190 @@
+#
+#	initially generated by c2l
+#
+
+implement Mk;
+
+include "draw.m";
+
+Mk: module
+{
+	init: fn(nil: ref Draw->Context, argl: list of string);
+};
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "libc0.m";
+	libc0: Libc0;
+include "regex.m";
+	regex: Regex;
+include "ar.m";
+	ARMAG, SARMAG, ARFMAG, SARNAME, ar_hdr, SAR_HDR: import Ar;
+include "daytime.m";
+	daytime: Daytime;
+include "sh.m";
+
+init(nil: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	libc0 = load Libc0 Libc0->PATH;
+	regex = load Regex Regex->PATH;
+	daytime = load Daytime Daytime->PATH;
+	sys->pctl(Sys->FORKNS, nil);
+	main(len argl, libc0->ls2aab(argl));
+}
+
+NAMELEN: con 28;
+ERRLEN: con 64;
+PNPROC, PNGROUP : con iota;
+
+# function pointer enum for symtraverse
+ECOPY, PRINT1: con iota;
+
+Bufblock: adt{
+	next: cyclic ref Bufblock;
+	start: array of byte;
+	end: int;
+	current: int;
+};
+
+Word: adt{
+	s: array of byte;
+	next: cyclic ref Word;
+};
+
+Envy: adt{
+	name: array of byte;
+	values: ref Word;
+};
+
+Resub: adt{
+	sp: array of byte;
+	ep: array of byte;
+};
+
+Rule: adt{
+	target: array of byte;	#  one target 
+	tail: ref Word;	#  constituents of targets 
+	recipe: array of byte;	#  do it ! 
+	attr: int;	#  attributes 
+	line: int;	#  source line 
+	file: array of byte;	#  source file 
+	alltargets: ref Word;	#  all the targets 
+	rule: int;	#  rule number 
+	pat: Regex->Re;	#  reg exp goo 
+	prog: array of byte;	#  to use in out of date 
+	chain: cyclic ref Rule;	#  hashed per target 
+	next: cyclic ref Rule;
+};
+
+# 	Rule.attr	
+META, SEQ, UPD, QUIET, VIR, REGEXP, NOREC, DEL, NOVIRT: con 1<<iota;
+NREGEXP: con 10;
+
+Arc: adt{
+	flag: int;
+	n: cyclic ref Node;
+	r: ref Rule;
+	stem: array of byte;
+	prog: array of byte;
+	match: array of array of byte;
+	next: cyclic ref Arc;
+};
+
+#  Arc.flag 
+TOGO: con 1;
+
+Node: adt{
+	name: array of byte;
+	time: int;
+	flags: int;
+	prereqs: cyclic ref Arc;
+	next: cyclic ref Node;	#  list for a rule 
+};
+
+#  Node.flags 
+VIRTUAL, CYCLE, READY, CANPRETEND, PRETENDING, NOTMADE, BEINGMADE, MADE, PROBABLE, VACUOUS, NORECIPE, DELETE, NOMINUSE: con 1<<iota;
+
+Job: adt{
+	r: ref Rule;	#  master rule for job 
+	n: ref Node;	#  list of node targets 
+	stem: array of byte;
+	match: array of array of byte;
+	p: ref Word;	#  prerequistes 
+	np: ref Word;	#  new prerequistes 
+	t: ref Word;	#  targets 
+	at: ref Word;	#  all targets 
+	nproc: int;	#  slot number 
+	next: cyclic ref Job;
+};
+
+Symtab: adt{
+	space: int;
+	name: array of byte;
+	svalue: array of byte;
+	ivalue: int;
+	nvalue: ref Node;
+	rvalue: ref Rule;
+	wvalue: ref Word;
+	next: cyclic ref Symtab;
+};
+
+S_VAR	#  variable -> value 
+, S_TARGET	#  target -> rule 
+, S_TIME	#  file -> time 
+, S_PID	#  pid -> products 
+, S_NODE	#  target name -> node 
+, S_AGG	#  aggregate -> time 
+, S_BITCH	#  bitched about aggregate not there 
+, S_NOEXPORT	#  var -> noexport 
+, S_OVERRIDE	#  can't override 
+, S_OUTOFDATE	#  n1\377n2 -> 2(outofdate) or 1(not outofdate) 
+, S_MAKEFILE	#  target -> node 
+, S_MAKEVAR	#  dumpable mk variable 
+, S_EXPORTED	#  var -> current exported value 
+, S_BULKED	#  we have bulked this dir 
+, S_WESET	#  variable; we set in the mkfile 
+#  an internal mk variable (e.g., stem, target) 
+, S_INTERNAL: con iota;
+NAMEBLOCK: con 1000;
+BIGBLOCK: con 20000;
+D_PARSE, D_GRAPH, D_EXEC: con 1<<iota;
+
+MKFILE: con "mkfile";
+
+version := array[] of { byte '@', byte '(', byte '#', byte ')', byte 'm', byte 'k', byte ' ', byte 'g', byte 'e', byte 'n', byte 'e', byte 'r', byte 'a', byte 'l', byte ' ', byte 'r', byte 'e', byte 'l', byte 'e', byte 'a', byte 's', byte 'e', byte ' ', byte '4', byte ' ', byte '(', byte 'p', byte 'l', byte 'a', byte 'n', byte ' ', byte '9', byte ')', byte '\0' };
+debug: int;
+rules, metarules: ref Rule;
+nflag: int = 0;
+tflag: int = 0;
+iflag: int = 0;
+kflag: int = 0;
+aflag: int = 0;
+uflag: int = 0;
+explain: array of byte = nil;
+target1: ref Word;
+nreps: int = 1;
+jobs: ref Job;
+bout: ref Iobuf;
+patrule: ref Rule;
+
+main(argc: int, argv: array of array of byte)
+{
+	w: ref Word;
+	s: array of byte;
+	files := array[256] of array of byte;
+	f: array of array of byte = files;
+	ff: int;
+	sflag: int = 1;
+	i: int;
+	tfd: ref Sys->FD = sys->fildes(-1);
+	tb: ref Iobuf;
+	buf, whatif: ref Bufblock;
+
+	# 
+	# 	 *  start with a copy of the current environment variables
+	# 	 *  instead of sharing them
+	# 
+	bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);	 
+	buf = newbuf();
+	whatif = nil;
+	if(argc)
+		;
+	for(argv = argv[1: ]; argv[0] != nil && argv[0][0] == byte '-'; argv = argv[1: ]){
+		bufcpy(buf, argv[0], libc0->strlen(argv[0]));
+		insert(buf, ' ');
+		case(int argv[0][1]){
+		'a' =>
+			aflag = 1;
+		'd' =>
+			if(int (s = argv[0][2: ])[0])
+				while(int s[0]){
+					case(int s[0]){
+					'p' =>
+						debug |= D_PARSE;
+					'g' =>
+						debug |= D_GRAPH;
+					'e' =>
+						debug |= D_EXEC;
+					}
+					s = s[1: ];
+				}
+			else
+				debug = 16rffff;
+		'e' =>
+			explain = argv[0][2: ];
+		'f' =>
+			argv = argv[1: ];
+			if(argv[0] == nil)
+				badusage();
+			f[0] = argv[0];
+			f = f[1: ];
+			bufcpy(buf, argv[0], libc0->strlen(argv[0]));
+			insert(buf, ' ');
+		'i' =>
+			iflag = 1;
+		'k' =>
+			kflag = 1;
+		'n' =>
+			nflag = 1;
+		's' =>
+			sflag = 1;
+		't' =>
+			tflag = 1;
+		'u' =>
+			uflag = 1;
+		'w' =>
+			if(whatif == nil)
+				whatif = newbuf();
+			else
+				insert(whatif, ' ');
+			if(int argv[0][2])
+				bufcpy(whatif, argv[0][2: ], libc0->strlen(argv[0][2: ]));
+			else{
+				argv = argv[1: ];
+				if(argv[0] == nil)
+					badusage();
+				bufcpy(whatif, argv[0][0: ], libc0->strlen(argv[0][0: ]));
+			}
+		* =>
+			badusage();
+		}
+	}
+	if(aflag)
+		iflag = 1;
+	usage();
+	syminit();
+	initenv();
+	initbind();
+	openwait();
+	usage();
+	# 
+	# 		assignment args become null strings
+	# 	
+	temp: string;
+	for(i = 0; argv[i] != nil; i++)
+		if(libc0->strchr(argv[i], '=') != nil){
+			bufcpy(buf, argv[i], libc0->strlen(argv[i]));
+			insert(buf, ' ');
+			if(tfd == nil){
+				(temp, tfd) = tmpfile("/tmp/mkarg");
+				if(tfd == nil){
+					perror(array of byte temp);
+					Exit();
+				}
+				tb = bufio->fopen(tfd, Sys->OWRITE);
+			}
+			tb.puts(sys->sprint("%s\n", libc0->ab2s(argv[i])));
+			argv[i][0] = byte 0;
+		}
+	if(tfd != nil){
+		tb.flush();
+		sys->seek(tfd, big 0, 0);
+		parse(libc0->s2ab("command line args"), tfd, 1);
+		sys->remove(temp);
+	}
+	if(buf.current != 0){
+		buf.current--;
+		insert(buf, 0);
+	}
+	symlookw(libc0->s2ab("MKFLAGS"), S_VAR, stow(buf.start));
+	buf.current = 0;
+	for(i = 0; argv[i] != nil; i++){
+		if(argv[i][0] == byte 0)
+			continue;
+		if(i)
+			insert(buf, ' ');
+		bufcpy(buf, argv[i], libc0->strlen(argv[i]));
+	}
+	insert(buf, 0);
+	symlookw(libc0->s2ab("MKARGS"), S_VAR, stow(buf.start));
+	freebuf(buf);
+	if(f == files){
+		if(access(libc0->s2ab(MKFILE), Sys->OREAD) == 0)
+			parse(libc0->s2ab(MKFILE), sys->open(MKFILE, 0), 0);
+	}
+	else
+		for(ff = 0; ff < len files && files[ff] != nil; ff++)
+			parse(files[ff], sys->open(libc0->ab2s(files[ff]), 0), 0);
+	if(debug&D_PARSE){
+		dumpw(libc0->s2ab("default targets"), target1);
+		dumpr(libc0->s2ab("rules"), rules);
+		dumpr(libc0->s2ab("metarules"), metarules);
+		dumpv(libc0->s2ab("variables"));
+	}
+	if(whatif != nil){
+		insert(whatif, 0);
+		timeinit(whatif.start);
+		freebuf(whatif);
+	}
+	execinit();
+	#  skip assignment args 
+	while(argv[0] != nil && argv[0][0] == byte 0)
+		argv = argv[1: ];
+	catchnotes();
+	if(argv[0] == nil){
+		if(target1 != nil)
+			for(w = target1; w != nil; w = w.next)
+				mk(w.s);
+		else{
+			sys->fprint(sys->fildes(2), "mk: nothing to mk\n");
+			Exit();
+		}
+	}
+	else{
+		if(sflag){
+			for(; argv[0] != nil; argv = argv[1: ])
+				if(int argv[0][0])
+					mk(argv[0]);
+		}
+		else{
+			head, tail, t: ref Word;
+
+			#  fake a new rule with all the args as prereqs 
+			tail = nil;
+			t = nil;
+			for(; argv[0] != nil; argv = argv[1: ])
+				if(int argv[0][0]){
+					if(tail == nil)
+						tail = t = newword(argv[0]);
+					else{
+						t.next = newword(argv[0]);
+						t = t.next;
+					}
+				}
+			if(tail.next == nil)
+				mk(tail.s);
+			else{
+				head = newword(libc0->s2ab("command line arguments"));
+				addrules(head, tail, libc0->strdup(libc0->s2ab("")), VIR, mkinline, nil);
+				mk(head.s);
+			}
+		}
+	}
+	if(uflag)
+		prusage();
+	bout.flush();
+}
+
+badusage()
+{
+	sys->fprint(sys->fildes(2), "Usage: mk [-f file] [-n] [-a] [-e] [-t] [-k] [-i] [-d[egp]] [targets ...]\n");
+	Exit();
+}
+
+assert(s: array of byte, n: int)
+{
+	if(!n){
+		sys->fprint(sys->fildes(2), "mk: Assertion ``%s'' failed.\n", libc0->ab2s(s));
+		Exit();
+	}
+}
+
+regerror(s: array of byte)
+{
+	if(patrule != nil)
+		sys->fprint(sys->fildes(2), "mk: %s:%d: regular expression error; %s\n", libc0->ab2s(patrule.file), patrule.line, libc0->ab2s(s));
+	else
+		sys->fprint(sys->fildes(2), "mk: %s:%d: regular expression error; %s\n", libc0->ab2s(infile), mkinline, libc0->ab2s(s));
+	Exit();
+}
+
+perror(s: array of byte)
+{
+	perrors(libc0->ab2s(s));
+}
+
+perrors(s: string)
+{
+	sys->fprint(sys->fildes(2), "mk: %s: %r\n", s);
+}
+
+access(s: array of byte, mode: int): int
+{
+	fd := sys->open(libc0->ab2s(s), mode);
+	if (fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+stob(buf: array of byte, s: string)
+{
+	b := libc0->s2ab(s);
+	libc0->strncpy(buf, b, len buf);
+}
+
+tmpfile(basename: string): (string, ref Sys->FD)
+{
+	pid := sys->pctl(0, nil);
+	for(i := 0; i < 100; i++){
+		t := basename+sys->sprint("%8.8d.%.2d", pid, i);
+		fd := sys->create(t, Sys->OEXCL|Sys->ORDWR, 8r600);
+		if(fd != nil)
+			return (t, fd);
+	}
+	return (nil, nil);
+}
+
+postnote(t: int, pid: int, note: array of byte)
+{
+	if(pid == 0)
+		return;
+	fd := sys->open("#p/" + string pid + "/ctl", Sys->OWRITE);
+	if(fd == nil)
+		return;
+	s := libc0->ab2s(note);
+	if(t == PNGROUP)
+		s += "grp";
+	sys->fprint(fd, "%s", s);
+	fd = nil;
+}
+
+map(s: array of byte, n: int): int
+{
+	i := j := 0;
+	ls := libc0->strlen(s);
+	while(i < ls){
+		if(j == n)
+			return i;
+		(nil, l, nil) := sys->byte2char(s, i);
+		i += l;
+		j++;
+	}
+	return -1;
+}
+
+regadd(s: array of byte, m: array of (int, int), rm: array of Resub, n: int)
+{
+	k := len m;
+	for(i := 0; i < n; i++)
+		rm[i].sp = rm[i].ep= nil;
+	for(i = 0; i < k && i < n; i++){
+		(a, b) := m[i];
+		if(a >= 0 && b >= 0){
+			a = map(s, a);
+			b = map(s, b);
+			if(a >= 0 && b >= 0){
+				rm[i].sp = s[a: ];
+				rm[i].ep = s[b: ];
+			}
+		}
+	}
+}
+
+scopy(d: array of byte, j: int, m: array of Resub, k: int, n: int): int
+{
+	if(k >= n)
+		return 0;
+	sp := m[k].sp;
+	ep := m[k].ep;
+	if(sp == nil || ep == nil)
+		return 0;
+	c := ep[0];
+	ep[0] = byte 0;
+	libc0->strcpy(d[j: ], sp);
+	ep[0] = c;
+	return libc0->strlen(sp)-libc0->strlen(ep);
+}
+
+regsub(s: array of byte, d: array of byte, m: array of Resub, n: int)
+{
+	# libc0->strncpy(d, s, libc0->strlen(d));
+	ls := libc0->strlen(s);
+	j := 0;
+	for(i := 0; i < ls; i++){
+		case(int s[i]){
+		'\\' =>
+			if(i+1 < ls && s[i+1] >= byte '0' && s[i+1] <= byte '9'){
+				k := int s[++i]-'0';
+				j += scopy(d, j, m, k, n);
+			}
+			else
+				d[j++] = byte '\\';
+		'&' =>
+			j += scopy(d, j, m, 0, n);
+		* =>
+			d[j++] = s[i];
+		}
+	}
+	d[j] = byte 0;
+}
+
+wpid := -1;
+wfd : ref Sys->FD;
+wprocs := 0;
+
+openwait()
+{
+	pid := sys->pctl(0, nil);
+	w := sys->sprint("#p/%d/wait", pid);
+	fd := sys->open(w, Sys->OREAD);
+	if(fd == nil){
+		perrors("fd == nil in wait");
+		return;
+	}
+	wpid = pid;
+	wfd = fd;
+}
+
+addwait()
+{
+	if(wpid == sys->pctl(0, nil))
+		wprocs++;
+}
+
+wait(): (int, array of byte)
+{
+	n: int;
+
+	if(wpid != -1 && wpid != sys->pctl(0, nil)){
+		perrors(sys->sprint("wait: pid %d != pid %d", wpid, sys->pctl(0, nil)));
+		return (-1, nil);
+	}
+	if(wprocs == 0)
+		return (-1, nil);
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;){
+		if((n = sys->read(wfd, buf, len buf))<0)
+			perrors("bad read in wait");
+		status = string buf[0:n];
+		break;
+	}
+	s := "";
+	if(status[len status - 1] != ':')
+		s = status;
+	wprocs--;
+	return (int status, libc0->s2ab(s));
+}
+
+abort()
+{
+	exit;
+}
+
+execl(sh: string, name: string, a1: string, a2: string, a3: string, a4: string)
+{
+	# sys->print("execl %s : %s %s %s %s %s\n", sh, name, a1, a2, a3, a4);
+
+	c := load Command sh;
+	if(c == nil){
+		sys->fprint(sys->fildes(2), "x %s: %r\n", sh);
+		raise "fail:execl";
+	}
+	argl: list of string;
+	if(a4 != nil)
+		argl = a4 :: argl;
+	if(a3 != nil)
+		argl = a3 :: argl;
+	if(a2 != nil)
+		argl = a2 :: argl;
+	if(a1 != nil)
+		argl = a1 :: argl;
+	# argl = "-x" :: argl;
+	argl = name :: argl;
+	# argl := list of { name, a1, a2, a3, a4 };
+	if(debug&D_EXEC)
+		sys->fprint(sys->fildes(1), "executing %s with args (%s, %s, %s, %s, %s)\n", sh, name, a1, a2, a3, a4);
+	c->init(nil, argl);
+}
+
+getuser(): string
+{
+  	fd := sys->open("/dev/user", sys->OREAD);
+  	if(fd == nil)
+    		return "";
+  	buf := array[128] of byte;
+  	n := sys->read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+  	return string buf[0: n];	
+}
+
+initbind()
+{
+	f := sys->sprint("/usr/%s/lib/mkbinds", getuser());
+	b := bufio->open(f, Bufio->OREAD);
+	if(b == nil)
+		b = bufio->open("/lib/mk/binds", Bufio->OREAD);
+	if(b == nil)
+		return;
+	while((s := b.gets('\n')) != nil){
+		m := len s;
+		if(s[m-1] == '\n')
+			s = s[0: m-1];
+		(n, l) := sys->tokenize(s, " \t");
+		if(n == 2)
+			sys->bind(hd l, hd tl l, Sys->MREPL);
+	}
+}
+
+#
+# mk
+#
+
+runerrs: int;
+
+mk(target: array of byte)
+{
+	node: ref Node;
+	did: int = 0;
+
+	nproc();	#  it can be updated dynamically 
+	nrep();	#  it can be updated dynamically 
+	runerrs = 0;
+	node = graph(target);
+	if(debug&D_GRAPH){
+		dumpn(libc0->s2ab("new target\n"), node);
+		bout.flush();
+	}
+	clrmade(node);
+	while(node.flags&NOTMADE){
+		if(work(node, nil, nil))
+			did = 1;	#  found something to do 
+		else{
+			if(waitup(1, nil) > 0){
+				if(node.flags&(NOTMADE|BEINGMADE)){
+					assert(libc0->s2ab("must be run errors"), runerrs);
+					break;	#  nothing more waiting 
+				}
+			}
+		}
+	}
+	if(node.flags&BEINGMADE)
+		waitup(-1, nil);
+	while(jobs != nil)
+		waitup(-2, nil);
+	assert(libc0->s2ab("target didn't get done"), runerrs || node.flags&MADE);
+	if(did == 0)
+		bout.puts(sys->sprint("mk: '%s' is up to date\n", libc0->ab2s(node.name)));
+}
+
+clrmade(n: ref Node)
+{
+	a: ref Arc;
+
+	n.flags &= ~(CANPRETEND|PRETENDING);
+	if(libc0->strchr(n.name, '(') == nil || n.time)
+		n.flags |= CANPRETEND;
+	n.flags = n.flags&~(NOTMADE|BEINGMADE|MADE)|NOTMADE;
+	for(a = n.prereqs; a != nil; a = a.next)
+		if(a.n != nil)
+			clrmade(a.n);
+}
+
+unpretend(n: ref Node)
+{
+	n.flags = n.flags&~(NOTMADE|BEINGMADE|MADE)|NOTMADE;
+	n.flags &= ~(CANPRETEND|PRETENDING);
+	n.time = 0;
+}
+
+work(node: ref Node, p: ref Node, parc: ref Arc): int
+{
+	a, ra: ref Arc;
+	weoutofdate, ready: int;
+	did: int = 0;
+
+	# print("work(%s) flags=0x%x time=%ld\n", node->name, node->flags, node->time);/*
+	if(node.flags&BEINGMADE)
+		return did;
+	if(node.flags&MADE && node.flags&PRETENDING && p != nil && outofdate(p, parc, 0)){
+		if(explain != nil)
+			sys->fprint(sys->fildes(1), "unpretending %s(%d) because %s is out of date(%d)\n", libc0->ab2s(node.name), node.time, libc0->ab2s(p.name), p.time);
+		unpretend(node);
+	}
+	# 
+	# 		have a look if we are pretending in case
+	# 		someone has been unpretended out from underneath us
+	# 	
+	if(node.flags&MADE){
+		if(node.flags&PRETENDING){
+			node.time = 0;
+		}
+		else
+			return did;
+	}
+	#  consider no prerequsite case 
+	if(node.prereqs == nil){
+		if(node.time == 0){
+			sys->fprint(sys->fildes(2), "mk: don't know how to make '%s'\n", libc0->ab2s(node.name));
+			if(kflag){
+				node.flags |= BEINGMADE;
+				runerrs++;
+			}
+			else
+				Exit();
+		}
+		else
+			node.flags = node.flags&~(NOTMADE|BEINGMADE|MADE)|MADE;
+		return did;
+	}
+	# 
+	# 		now see if we are out of date or what
+	# 	
+	ready = 1;
+	weoutofdate = aflag;
+	ra = nil;
+	for(a = node.prereqs; a != nil; a = a.next)
+		if(a.n != nil){
+			did = work(a.n, node, a) || did;
+			if(a.n.flags&(NOTMADE|BEINGMADE))
+				ready = 0;
+			if(outofdate(node, a, 0)){
+				weoutofdate = 1;
+				if(ra == nil || ra.n == nil || ra.n.time < a.n.time)
+					ra = a;
+			}
+		}
+		else{
+			if(node.time == 0){
+				if(ra == nil)
+					ra = a;
+				weoutofdate = 1;
+			}
+		}
+	if(ready == 0)	#  can't do anything now 
+		return did;
+	if(weoutofdate == 0){
+		node.flags = node.flags&~(NOTMADE|BEINGMADE|MADE)|MADE;
+		return did;
+	}
+	# 
+	# 		can we pretend to be made?
+	# 	
+	if(iflag == 0 && node.time == 0 && node.flags&(PRETENDING|CANPRETEND) && p != nil && ra.n != nil && !outofdate(p, ra, 0)){
+		node.flags &= ~CANPRETEND;
+		node.flags = node.flags&~(NOTMADE|BEINGMADE|MADE)|MADE;
+		if(explain != nil && (node.flags&PRETENDING) == 0)
+			sys->fprint(sys->fildes(1), "pretending %s has time %d\n", libc0->ab2s(node.name), node.time);
+		node.flags |= PRETENDING;
+		return did;
+	}
+	# 
+	# 		node is out of date and we REALLY do have to do something.
+	# 		quickly rescan for pretenders
+	# 	
+	for(a = node.prereqs; a != nil; a = a.next)
+		if(a.n != nil && a.n.flags&PRETENDING){
+			if(explain != nil)
+				if(ra.n != nil)
+					bout.puts(sys->sprint("unpretending %s because of %s because of %s\n", libc0->ab2s(a.n.name), libc0->ab2s(node.name), libc0->ab2s(ra.n.name)));
+				else
+					bout.puts(sys->sprint("unpretending %s because of %s because of %s\n", libc0->ab2s(a.n.name), libc0->ab2s(node.name), "rule with no prerequisites"));
+			unpretend(a.n);
+			did = work(a.n, node, a) || did;
+			ready = 0;
+		}
+	if(ready == 0)	#  try later unless nothing has happened for -k's sake 
+		return did || work(node, p, parc);
+	did = dorecipe(node) || did;
+	return did;
+}
+
+update(fake: int, node: ref Node)
+{
+	a: ref Arc;
+
+	if(fake)
+		node.flags = node.flags&~(NOTMADE|BEINGMADE|MADE)|BEINGMADE;
+	else
+		node.flags = node.flags&~(NOTMADE|BEINGMADE|MADE)|MADE;
+	if((node.flags&VIRTUAL) == 0 && access(node.name, 0) == 0){
+		node.time = timeof(node.name, 1);
+		node.flags &= ~(CANPRETEND|PRETENDING);
+		for(a = node.prereqs; a != nil; a = a.next)
+			if(a.prog != nil)
+				outofdate(node, a, 1);
+	}
+	else{
+		node.time = 1;
+		for(a = node.prereqs; a != nil; a = a.next)
+			if(a.n != nil && outofdate(node, a, 1))
+				node.time = a.n.time;
+	}
+	# 	print("----node %s time=%ld flags=0x%x\n", node->name, node->time, node->flags);/*
+}
+
+pcmp(prog: array of byte, p: array of byte, q: array of byte): int
+{
+	buf := array[3*NAMEBLOCK] of byte;
+	pid: int;
+
+	bout.flush();
+	stob(buf, sys->sprint("%s '%s' '%s'\n", libc0->ab2s(prog), libc0->ab2s(p), libc0->ab2s(q)));
+	pid = pipecmd(buf, nil, nil);
+	apid := array[1] of int;
+	apid[0] = pid;
+	while(waitup(-3, apid) >= 0)
+		;
+	pid = apid[0];
+	if(pid)
+		return 2;
+	else
+		return 1;
+}
+
+outofdate(node: ref Node, arc: ref Arc, eval: int): int
+{
+	buf := array[3*NAMEBLOCK] of byte;
+	str: array of byte;
+	sym: ref Symtab;
+	ret: int;
+
+	str = nil;
+	if(arc.prog != nil){
+		stob(buf, sys->sprint("%s%c%s", libc0->ab2s(node.name), 8r377, libc0->ab2s(arc.n.name)));
+		sym = symlooki(buf, S_OUTOFDATE, 0);
+		if(sym == nil || eval){
+			if(sym == nil)
+				str = libc0->strdup(buf);
+			ret = pcmp(arc.prog, node.name, arc.n.name);
+			if(sym != nil)
+				sym.ivalue = ret;
+			else
+				symlooki(str, S_OUTOFDATE, ret);
+		}
+		else
+			ret = int sym.ivalue;
+		return ret-1;
+	}
+	else if(libc0->strchr(arc.n.name, '(') != nil && arc.n.time == 0)	#  missing archive member 
+		return 1;
+	else
+		return node.time < arc.n.time;
+}
+
+
+#
+# recipe
+#
+
+dorecipe(node: ref Node): int
+{
+	buf := array[BIGBLOCK] of byte;
+	n: ref Node;
+	r: ref Rule = nil;
+	a, aa: ref Arc;
+	head := ref Word;
+	ahead := ref Word;
+	lp := ref Word;
+	ln := ref Word;
+	w, ww, aw: ref Word;
+	s: ref Symtab;
+	did: int = 0;
+
+	aa = nil;
+	# 
+	# 		pick up the rule
+	# 	
+	for(a = node.prereqs; a != nil; a = a.next)
+		if(int a.r.recipe[0])
+			r = (aa = a).r;
+	# 
+	# 		no recipe? go to buggery!
+	# 	
+	if(r == nil){
+		if(!(node.flags&VIRTUAL) && !(node.flags&NORECIPE)){
+			sys->fprint(sys->fildes(2), "mk: no recipe to make '%s'\n", libc0->ab2s(node.name));
+			Exit();
+		}
+		if(libc0->strchr(node.name, '(') != nil && node.time == 0)
+			node.flags = node.flags&~(NOTMADE|BEINGMADE|MADE)|MADE;
+		else
+			update(0, node);
+		if(tflag){
+			if(!(node.flags&VIRTUAL))
+				touch(node.name);
+			else if(explain != nil)
+				bout.puts(sys->sprint("no touch of virtual '%s'\n", libc0->ab2s(node.name)));
+		}
+		return did;
+	}
+	# 
+	# 		build the node list
+	# 	
+	node.next = nil;
+	head.next = nil;
+	ww = head;
+	ahead.next = nil;
+	aw = ahead;
+	if(r.attr&REGEXP){
+		ww.next = newword(node.name);
+		aw.next = newword(node.name);
+	}
+	else{
+		for(w = r.alltargets; w != nil; w = w.next){
+			if(r.attr&META)
+				subst(aa.stem, w.s, buf);
+			else
+				libc0->strcpy(buf, w.s);
+			aw.next = newword(buf);
+			aw = aw.next;
+			if((s = symlooki(buf, S_NODE, 0)) == nil)
+				continue;	#  not a node we are interested in 
+			n = s.nvalue;
+			if(aflag == 0 && n.time){
+				for(a = n.prereqs; a != nil; a = a.next)
+					if(a.n != nil && outofdate(n, a, 0))
+						break;
+				if(a == nil)
+					continue;
+			}
+			ww.next = newword(buf);
+			ww = ww.next;
+			if(n == node)
+				continue;
+			n.next = node.next;
+			node.next = n;
+		}
+	}
+	for(n = node; n != nil; n = n.next)
+		if((n.flags&READY) == 0)
+			return did;
+	# 
+	# 		gather the params for the job
+	# 	
+	lp.next = ln.next = nil;
+	for(n = node; n != nil; n = n.next){
+		for(a = n.prereqs; a != nil; a = a.next){
+			if(a.n != nil){
+				addw(lp, a.n.name);
+				if(outofdate(n, a, 0)){
+					addw(ln, a.n.name);
+					if(explain != nil)
+						sys->fprint(sys->fildes(1), "%s(%d) < %s(%d)\n", libc0->ab2s(n.name), n.time, libc0->ab2s(a.n.name), a.n.time);
+				}
+			}
+			else{
+				if(explain != nil)
+					sys->fprint(sys->fildes(1), "%s has no prerequisites\n", libc0->ab2s(n.name));
+			}
+		}
+		n.flags = n.flags&~(NOTMADE|BEINGMADE|MADE)|BEINGMADE;
+	}
+	# print("lt=%s ln=%s lp=%s\n",wtos(head.next, ' '),wtos(ln.next, ' '),wtos(lp.next, ' '));/*
+	run(newjob(r, node, aa.stem, aa.match, lp.next, ln.next, head.next, ahead.next));
+	return 1;
+}
+
+addw(w: ref Word, s: array of byte)
+{
+	lw: ref Word;
+
+	for(lw = w; (w = w.next) != nil; lw = w){
+		if(libc0->strcmp(s, w.s) == 0)
+			return;
+	}
+	lw.next = newword(s);
+}
+
+#
+# rule
+#
+
+lr, lmr: ref Rule;
+nrules: int = 0;
+
+addrule(head: array of byte, tail: ref Word, body: array of byte, ahead: ref Word, attr: int, hline: int, prog: array of byte)
+{
+	r, rr: ref Rule;
+	sym: ref Symtab;
+	reuse: int;
+
+	r = nil;
+	reuse = 0;
+	if((sym = symlooki(head, S_TARGET, 0)) != nil){
+		for(r = sym.rvalue; r != nil; r = r.chain)
+			if(rcmp(r, head, tail) == 0){
+				reuse = 1;
+				break;
+			}
+	}
+	if(r == nil)
+		r = ref Rule;
+	r.target = head;
+	r.tail = tail;
+	r.recipe = body;
+	r.line = hline;
+	r.file = infile;
+	r.attr = attr;
+	r.alltargets = ahead;
+	r.prog = prog;
+	r.rule = nrules++;
+	if(!reuse){
+		rr = symlookr(head, S_TARGET, r).rvalue;
+		if(rr != r){
+			r.chain = rr.chain;
+			rr.chain = r;
+		}
+		else
+			r.chain = nil;
+	}
+	if(!reuse)
+		r.next = nil;
+	if(attr&REGEXP || charin(head, libc0->s2ab("%&")) != nil){
+		r.attr |= META;
+		if(reuse)
+			return;
+		if(attr&REGEXP){
+			patrule = r;
+			e := "";
+			(r.pat, e) = regex->compile(libc0->ab2s(head), 1);
+			if(e != nil)
+				perrors(sys->sprint("%s: %s", libc0->ab2s(head), e));
+		}
+		if(metarules == nil)
+			metarules = lmr = r;
+		else{
+			lmr.next = r;
+			lmr = r;
+		}
+	}
+	else{
+		if(reuse)
+			return;
+		r.pat = nil;
+		if(rules == nil)
+			rules = lr = r;
+		else{
+			lr.next = r;
+			lr = r;
+		}
+	}
+}
+
+dumpr(s: array of byte, r: ref Rule)
+{
+	bout.puts(sys->sprint("%s: start=%x\n", libc0->ab2s(s), r));
+	for(; r != nil; r = r.next){
+		bout.puts(sys->sprint("\tRule %x: %s[%d] attr=%x next=%x chain=%x alltarget='%s'", r, libc0->ab2s(r.file), r.line, r.attr, r.next, r.chain, wtostr(r.alltargets, ' ')));
+		if(r.prog != nil)
+			bout.puts(sys->sprint(" prog='%s'", libc0->ab2s(r.prog)));
+		bout.puts(sys->sprint("\n\ttarget=%s: %s\n", libc0->ab2s(r.target), wtostr(r.tail, ' ')));
+		bout.puts(sys->sprint("\trecipe@%x='%s'\n", r.recipe, libc0->ab2s(r.recipe)));
+	}
+}
+
+rcmp(r: ref Rule, target: array of byte, tail: ref Word): int
+{
+	w: ref Word;
+
+	if(libc0->strcmp(r.target, target))
+		return 1;
+	for(w = r.tail; w != nil && tail != nil; (w, tail) = (w.next, tail.next))
+		if(libc0->strcmp(w.s, tail.s))
+			return 1;
+	return w != nil || tail != nil;
+}
+
+rulecnt(): array of byte
+{
+	s: array of byte;
+
+	s = array[nrules] of byte;
+	for(i := 0; i < nrules; i++)
+		s[i] = byte 0;
+	return s;
+}
+
+#
+# graph
+#
+
+
+graph(target: array of byte): ref Node
+{
+	node: ref Node;
+	cnt: array of byte;
+
+	cnt = rulecnt();
+	node = applyrules(target, cnt);
+	cnt = nil;
+	cyclechk(node);
+	node.flags |= PROBABLE;	#  make sure it doesn't get deleted 
+	vacuous(node);
+	ambiguous(node);
+	attribute(node);
+	return node;
+}
+
+applyrules(target: array of byte, cnt: array of byte): ref Node
+{
+	sym: ref Symtab;
+	node: ref Node;
+	r: ref Rule;
+	head := ref Arc;
+	a: ref Arc = head;
+	w: ref Word;
+	stem := array[NAMEBLOCK] of byte;
+	buf := array[NAMEBLOCK] of byte;
+	rmatch := array[NREGEXP] of Resub;
+
+	# 	print("applyrules(%lux='%s')\n", target, target);/*
+	sym = symlooki(target, S_NODE, 0);
+	if(sym != nil)
+		return sym.nvalue;
+	target = libc0->strdup(target);
+	node = newnode(target);
+	head.n = nil;
+	head.next = nil;
+	sym = symlooki(target, S_TARGET, 0);
+	for(i := 0; i < NREGEXP; i++)
+		rmatch[i].sp = rmatch[i].ep = nil;
+	if(sym != nil)
+		tmp_1 := sym.rvalue;
+	else
+		tmp_1 = nil;
+	for(r = tmp_1; r != nil; r = r.chain){
+		if(r.attr&META)
+			continue;
+		if(libc0->strcmp(target, r.target))
+			continue;
+		if((r.recipe == nil || !int r.recipe[0]) && (r.tail == nil || r.tail.s == nil || !int r.tail.s[0]))	#  no effect; ignore 
+			continue;
+		if(int cnt[r.rule] >= nreps)
+			continue;
+		cnt[r.rule]++;
+		node.flags |= PROBABLE;
+		# 		if(r->attr&VIR)
+		#  *			node->flags |= VIRTUAL;
+		#  *		if(r->attr&NOREC)
+		#  *			node->flags |= NORECIPE;
+		#  *		if(r->attr&DEL)
+		#  *			node->flags |= DELETE;
+		#  
+		if(r.tail == nil || r.tail.s == nil || !int r.tail.s[0]){
+			a.next = newarc(nil, r, libc0->s2ab(""), rmatch);
+			a = a.next;
+		}
+		else
+			for(w = r.tail; w != nil; w = w.next){
+				a.next = newarc(applyrules(w.s, cnt), r, libc0->s2ab(""), rmatch);
+				a = a.next;
+			}
+		cnt[r.rule]--;
+		head.n = node;
+	}
+	for(r = metarules; r != nil; r = r.next){
+		if((r.recipe == nil || !int r.recipe[0]) && (r.tail == nil || r.tail.s == nil || !int r.tail.s[0]))	#  no effect; ignore 
+			continue;
+		if(r.attr&NOVIRT && a != head && a.r.attr&VIR)
+			continue;
+		if(r.attr&REGEXP){
+			stem[0] = byte 0;
+			patrule = r;
+			for(i = 0; i < NREGEXP; i++)
+				rmatch[i].sp = rmatch[i].ep = nil;
+			m := regex->execute(r.pat, libc0->ab2s(node.name));
+			if(m == nil)
+				continue;
+			regadd(node.name, m, rmatch, NREGEXP);
+		}
+		else{
+			if(!match(node.name, r.target, stem))
+				continue;
+		}
+		if(int cnt[r.rule] >= nreps)
+			continue;
+		cnt[r.rule]++;
+		# 		if(r->attr&VIR)
+		#  *			node->flags |= VIRTUAL;
+		#  *		if(r->attr&NOREC)
+		#  *			node->flags |= NORECIPE;
+		#  *		if(r->attr&DEL)
+		#  *			node->flags |= DELETE;
+		#  
+		if(r.tail == nil || r.tail.s == nil || !int r.tail.s[0]){
+			a.next = newarc(nil, r, stem, rmatch);
+			a = a.next;
+		}
+		else
+			for(w = r.tail; w != nil; w = w.next){
+				if(r.attr&REGEXP)
+					regsub(w.s, buf, rmatch, NREGEXP);
+				else
+					subst(stem, w.s, buf);
+				a.next = newarc(applyrules(buf, cnt), r, stem, rmatch);
+				a = a.next;
+			}
+		cnt[r.rule]--;
+	}
+	a.next = node.prereqs;
+	node.prereqs = head.next;
+	return node;
+}
+
+togo(node: ref Node)
+{
+	la, a: ref Arc;
+
+	#  delete them now 
+	la = nil;
+	for(a = node.prereqs; a != nil; (la, a) = (a, a.next))
+		if(a.flag&TOGO){
+			if(a == node.prereqs)
+				node.prereqs = a.next;
+			else
+				(la.next, a) = (a.next, la);
+		}
+}
+
+vacuous(node: ref Node): int
+{
+	la, a: ref Arc;
+	vac: int = !(node.flags&PROBABLE);
+
+	if(node.flags&READY)
+		return node.flags&VACUOUS;
+	node.flags |= READY;
+	for(a = node.prereqs; a != nil; a = a.next)
+		if(a.n != nil && vacuous(a.n) && a.r.attr&META)
+			a.flag |= TOGO;
+		else
+			vac = 0;
+	#  if a rule generated arcs that DON'T go; no others from that rule go 
+	for(a = node.prereqs; a != nil; a = a.next)
+		if((a.flag&TOGO) == 0)
+			for(la = node.prereqs; la != nil; la = la.next)
+				if(la.flag&TOGO && la.r == a.r){
+					la.flag &= ~TOGO;
+				}
+	togo(node);
+	if(vac)
+		node.flags |= VACUOUS;
+	return vac;
+}
+
+newnode(name: array of byte): ref Node
+{
+	node: ref Node;
+
+	node = ref Node;
+	symlookn(name, S_NODE, node);
+	node.name = name;
+	node.time = timeof(name, 0);
+	node.prereqs = nil;
+	if(node.time)
+		node.flags = PROBABLE;
+	else
+		node.flags = 0;
+	node.next = nil;
+	return node;
+}
+
+dumpn(s: array of byte, n: ref Node)
+{
+	buf := array[1024] of byte;
+	a: ref Arc;
+
+	if(s[0] == byte ' ')
+		stob(buf, sys->sprint("%s   ", libc0->ab2s(s)));
+	else
+		stob(buf, sys->sprint("%s   ", ""));
+	bout.puts(sys->sprint("%s%s@%x: time=%d flags=0x%x next=%x\n", libc0->ab2s(s), libc0->ab2s(n.name), n, n.time, n.flags, n.next));
+	for(a = n.prereqs; a != nil; a = a.next)
+		dumpa(buf, a);
+}
+
+trace(s: array of byte, a: ref Arc)
+{
+	sys->fprint(sys->fildes(2), "\t%s", libc0->ab2s(s));
+	while(a != nil){
+		if(a.n != nil)
+			sys->fprint(sys->fildes(2), " <-(%s:%d)- %s", libc0->ab2s(a.r.file), a.r.line, libc0->ab2s(a.n.name));
+		else
+			sys->fprint(sys->fildes(2), " <-(%s:%d)- %s", libc0->ab2s(a.r.file), a.r.line, "");
+		if(a.n != nil){
+			for(a = a.n.prereqs; a != nil; a = a.next)
+				if(int a.r.recipe[0])
+					break;
+		}
+		else
+			a = nil;
+	}
+	sys->fprint(sys->fildes(2), "\n");
+}
+
+cyclechk(n: ref Node)
+{
+	a: ref Arc;
+
+	if(n.flags&CYCLE && n.prereqs != nil){
+		sys->fprint(sys->fildes(2), "mk: cycle in graph detected at target %s\n", libc0->ab2s(n.name));
+		Exit();
+	}
+	n.flags |= CYCLE;
+	for(a = n.prereqs; a != nil; a = a.next)
+		if(a.n != nil)
+			cyclechk(a.n);
+	n.flags &= ~CYCLE;
+}
+
+ambiguous(n: ref Node)
+{
+	a: ref Arc;
+	r: ref Rule = nil;
+	la: ref Arc;
+	bad: int = 0;
+
+	la = nil;
+	for(a = n.prereqs; a != nil; a = a.next){
+		if(a.n != nil)
+			ambiguous(a.n);
+		if(a.r.recipe[0] == byte 0)
+			continue;
+		if(r == nil)
+			(r, la) = (a.r, a);
+		else{
+			if(r.recipe != a.r.recipe){
+				if(r.attr&META && !(a.r.attr&META)){
+					la.flag |= TOGO;
+					(r, la) = (a.r, a);
+				}
+				else if(!(r.attr&META) && a.r.attr&META){
+					a.flag |= TOGO;
+					continue;
+				}
+			}
+			if(r.recipe != a.r.recipe){
+				if(bad == 0){
+					sys->fprint(sys->fildes(2), "mk: ambiguous recipes for %s:\n", libc0->ab2s(n.name));
+					bad = 1;
+					trace(n.name, la);
+				}
+				trace(n.name, a);
+			}
+		}
+	}
+	if(bad)
+		Exit();
+	togo(n);
+}
+
+attribute(n: ref Node)
+{
+	a: ref Arc;
+
+	for(a = n.prereqs; a != nil; a = a.next){
+		if(a.r.attr&VIR)
+			n.flags |= VIRTUAL;
+		if(a.r.attr&NOREC)
+			n.flags |= NORECIPE;
+		if(a.r.attr&DEL)
+			n.flags |= DELETE;
+		if(a.n != nil)
+			attribute(a.n);
+	}
+	if(n.flags&VIRTUAL)
+		n.time = 0;
+}
+
+#
+# arc
+#
+
+newarc(n: ref Node, r: ref Rule, stem: array of byte, match: array of Resub): ref Arc
+{
+	a: ref Arc;
+
+	a = ref Arc;
+	a.n = n;
+	a.r = r;
+	a.stem = libc0->strdup(stem);
+	a.match = array[NREGEXP] of array of byte;
+	rcopy(a.match, match, NREGEXP);
+	a.next = nil;
+	a.flag = 0;
+	a.prog = r.prog;
+	return a;
+}
+
+dumpa(s: array of byte, a: ref Arc)
+{
+	buf := array[1024] of byte;
+
+	bout.puts(sys->sprint("%sArc@%x: n=%x r=%x flag=0x%x stem='%s'", libc0->ab2s(s), a, a.n, a.r, a.flag, libc0->ab2s(a.stem)));
+	if(a.prog != nil)
+		bout.puts(sys->sprint(" prog='%s'", libc0->ab2s(a.prog)));
+	bout.puts("\n");
+	if(a.n != nil){
+		if(s[0] == byte ' ')
+			stob(buf, sys->sprint("%s    ", libc0->ab2s(s)));
+		else
+			stob(buf, sys->sprint("%s    ", ""));
+		dumpn(buf, a.n);
+	}
+}
+
+nrep()
+{
+	sym: ref Symtab;
+	w: ref Word;
+
+	sym = symlooki(libc0->s2ab("NREP"), S_VAR, 0);
+	if(sym != nil){
+		w = sym.wvalue;
+		if(w != nil && w.s != nil && int w.s[0])
+			nreps = int string w.s;
+	}
+	if(nreps < 1)
+		nreps = 1;
+	if(debug&D_GRAPH)
+		bout.puts(sys->sprint("nreps = %d\n", nreps));
+}
+
+#
+# job
+#
+
+newjob(r: ref Rule, nlist: ref Node, stem: array of byte, match: array of array of byte, pre: ref Word, npre: ref Word, tar: ref Word, atar: ref Word): ref Job
+{
+	j: ref Job;
+
+	j = ref Job;
+	j.r = r;
+	j.n = nlist;
+	j.stem = stem;
+	j.match = match;
+	j.p = pre;
+	j.np = npre;
+	j.t = tar;
+	j.at = atar;
+	j.nproc = -1;
+	j.next = nil;
+	return j;
+}
+
+dumpj(s: array of byte, j: ref Job, all: int)
+{
+	bout.puts(sys->sprint("%s\n", libc0->ab2s(s)));
+	while(j != nil){
+		bout.puts(sys->sprint("job@%x: r=%x n=%x stem='%s' nproc=%d\n", j, j.r, j.n, libc0->ab2s(j.stem), j.nproc));
+		bout.puts(sys->sprint("\ttarget='%s' alltarget='%s' prereq='%s' nprereq='%s'\n", wtostr(j.t, ' '), wtostr(j.at, ' '), wtostr(j.p, ' '), wtostr(j.np, ' ')));
+		if(all)
+			j = j.next;
+		else
+			j = nil;
+	}
+}
+
+#
+# run
+#
+
+Event: adt{
+	pid: int;
+	job: ref Job;
+};
+
+events: array of Event;
+nevents, nrunning, nproclimit: int;
+
+Process: adt{
+	pid: int;
+	status: int;
+	b: cyclic ref Process;
+	f: cyclic ref Process;
+};
+
+phead, pfree: ref Process;
+
+run(j: ref Job)
+{
+	jj: ref Job;
+
+	if(jobs != nil){
+		for(jj = jobs; jj.next != nil; jj = jj.next)
+			;
+		jj.next = j;
+	}
+	else
+		jobs = j;
+	j.next = nil;
+	#  this code also in waitup after parse redirect 
+	if(nrunning < nproclimit)
+		sched();
+}
+
+sched()
+{
+	flags: array of byte;
+	j: ref Job;
+	buf: ref Bufblock;
+	slot: int;
+	n: ref Node;
+	e: array of Envy;
+
+	if(jobs == nil){
+		usage();
+		return;
+	}
+	j = jobs;
+	jobs = j.next;
+	if(debug&D_EXEC)
+		sys->fprint(sys->fildes(1), "firing up job for target %s\n", libc0->ab2s(wtos(j.t, ' ')));
+	slot = nextslot();
+	events[slot].job = j;
+	buf = newbuf();
+	e = buildenv(j, slot);
+	shprint(j.r.recipe, e, buf);
+	if(!tflag && (nflag || !(j.r.attr&QUIET)))
+		bout.write(buf.start, libc0->strlen(buf.start));
+	freebuf(buf);
+	if(nflag || tflag){
+		bout.flush();
+		for(n = j.n; n != nil; n = n.next){
+			if(tflag){
+				if(!(n.flags&VIRTUAL))
+					touch(n.name);
+				else if(explain != nil)
+					bout.puts(sys->sprint("no touch of virtual '%s'\n", libc0->ab2s(n.name)));
+			}
+			n.time = daytime->now();
+			n.flags = n.flags&~(NOTMADE|BEINGMADE|MADE)|MADE;
+		}
+	}
+	else{
+		if(debug&D_EXEC)
+			sys->fprint(sys->fildes(1), "recipe='%s'", libc0->ab2s(j.r.recipe));	# 
+		bout.flush();
+		if(j.r.attr&NOMINUSE)
+			flags = nil;
+		else
+			flags = libc0->s2ab("-e");
+		events[slot].pid = execsh(flags, j.r.recipe, nil, e);
+		usage();
+		nrunning++;
+		if(debug&D_EXEC)
+			sys->fprint(sys->fildes(1), "pid for target %s = %d\n", libc0->ab2s(wtos(j.t, ' ')), events[slot].pid);
+	}
+}
+
+waitup(echildok: int, retstatus: array of int): int
+{
+	e: array of Envy;
+	pid, slot: int;
+	s: ref Symtab;
+	w: ref Word;
+	j: ref Job;
+	buf := array[ERRLEN] of byte;
+	bp: ref Bufblock;
+	uarg: int = 0;
+	done: int;
+	n: ref Node;
+	p: ref Process;
+	runerrs: int;
+
+	#  first check against the proces slist 
+	if(retstatus != nil)
+		for(p = phead; p != nil; p = p.f)
+			if(p.pid == retstatus[0]){
+				retstatus[0] = p.status;
+				pdelete(p);
+				return -1;
+			}
+	#  rogue processes 
+for(;;){
+	pid = waitfor(buf);
+	if(pid == -1){
+		if(echildok > 0)
+			return 1;
+		else{
+			sys->fprint(sys->fildes(2), "mk: (waitup %d) ", echildok);
+			perrors("mk wait");
+			Exit();
+		}
+	}
+	if(debug&D_EXEC)
+		sys->fprint(sys->fildes(1), "waitup got pid=%d, status='%s'\n", pid, libc0->ab2s(buf));
+	if(retstatus != nil && pid == retstatus[0]){
+		if(int buf[0])
+			retstatus[0] = 1;
+		else
+			retstatus[0] = 0;
+		return -1;
+	}
+	slot = pidslot(pid);
+	if(slot < 0){
+		if(debug&D_EXEC)
+			sys->fprint(sys->fildes(2), "mk: wait returned unexpected process %d\n", pid);
+		if(int buf[0])
+			pnew(pid, 1);
+		else
+			pnew(pid, 0);
+		continue;
+	}
+	break;
+}
+	j = events[slot].job;
+	usage();
+	nrunning--;
+	events[slot].pid = -1;
+	if(int buf[0]){
+		e = buildenv(j, slot);
+		bp = newbuf();
+		shprint(j.r.recipe, e, bp);
+		front(bp.start);
+		sys->fprint(sys->fildes(2), "mk: %s: exit status=%s", libc0->ab2s(bp.start), libc0->ab2s(buf));
+		freebuf(bp);
+		for((n, done) = (j.n, 0); n != nil; n = n.next)
+			if(n.flags&DELETE){
+				if(done++ == 0)
+					sys->fprint(sys->fildes(2), ", deleting");
+				sys->fprint(sys->fildes(2), " '%s'", libc0->ab2s(n.name));
+				delete(n.name);
+			}
+		sys->fprint(sys->fildes(2), "\n");
+		if(kflag){
+			runerrs++;
+			uarg = 1;
+		}
+		else{
+			jobs = nil;
+			Exit();
+		}
+	}
+	for(w = j.t; w != nil; w = w.next){
+		if((s = symlooki(w.s, S_NODE, 0)) == nil)
+			continue;	#  not interested in this node 
+		update(uarg, s.nvalue);
+	}
+	if(nrunning < nproclimit)
+		sched();
+	return 0;
+}
+
+nproc()
+{
+	sym: ref Symtab;
+	w: ref Word;
+
+	if((sym = symlooki(libc0->s2ab("NPROC"), S_VAR, 0)) != nil){
+		w = sym.wvalue;
+		if(w != nil && w.s != nil && int w.s[0])
+			nproclimit = int string w.s;
+	}
+	if(1 || nproclimit < 1)
+		nproclimit = 1;
+	if(debug&D_EXEC)
+		sys->fprint(sys->fildes(1), "nprocs = %d\n", nproclimit);
+	if(nproclimit > nevents){
+		if(nevents){
+			olen := len events;
+			ne := array[nproclimit] of Event;
+			if(olen)
+				ne[0: ] = events[0: olen];
+			events = ne;
+		}
+		else
+			events = array[nproclimit] of Event;
+		while(nevents < nproclimit)
+			events[nevents++].pid = 0;
+	}
+}
+
+nextslot(): int
+{
+	i: int;
+
+	for(i = 0; i < nproclimit; i++)
+		if(events[i].pid <= 0)
+			return i;
+	assert(libc0->s2ab("out of slots!!"), 0);
+	return 0;	#  cyntax 
+}
+
+pidslot(pid: int): int
+{
+	i: int;
+
+	for(i = 0; i < nevents; i++)
+		if(events[i].pid == pid)
+			return i;
+	if(debug&D_EXEC)
+		sys->fprint(sys->fildes(2), "mk: wait returned unexpected process %d\n", pid);
+	return -1;
+}
+
+pnew(pid: int, status: int)
+{
+	p: ref Process;
+
+	if(pfree != nil){
+		p = pfree;
+		pfree = p.f;
+	}
+	else
+		p = ref Process;
+	p.pid = pid;
+	p.status = status;
+	p.f = phead;
+	phead = p;
+	if(p.f != nil)
+		p.f.b = p;
+	p.b = nil;
+}
+
+pdelete(p: ref Process)
+{
+	if(p.f != nil)
+		p.f.b = p.b;
+	if(p.b != nil)
+		p.b.f = p.f;
+	else
+		phead = p.f;
+	p.f = pfree;
+	pfree = p;
+}
+
+killchildren(msg: array of byte)
+{
+	p: ref Process;
+
+	kflag = 1;	#  to make sure waitup doesn't exit 
+	jobs = nil;	#  make sure no more get scheduled 
+	for(p = phead; p != nil; p = p.f)
+		expunge(p.pid, msg);
+	while(waitup(1, nil) == 0)
+		;
+	bout.puts(sys->sprint("mk: %s\n", libc0->ab2s(msg)));
+	Exit();
+}
+
+tslot := array[1000] of int;
+tick: int;
+
+usage()
+{
+	t: int;
+
+	t = daytime->now();
+	if(tick)
+		tslot[nrunning] += t-tick;
+	tick = t;
+}
+
+prusage()
+{
+	i: int;
+
+	usage();
+	for(i = 0; i <= nevents; i++)
+		sys->fprint(sys->fildes(1), "%d: %d\n", i, tslot[i]);
+}
+
+#
+# file
+#
+
+#  table-driven version in bootes dump of 12/31/96 
+timeof(name: array of byte, force: int): int
+{
+	if(libc0->strchr(name, '(') != nil)
+		return atimeof(force, name);	#  archive 
+	if(force)
+		return mtime(name);
+	return filetime(name);
+}
+
+touch(name: array of byte)
+{
+	bout.puts(sys->sprint("touch(%s)\n", libc0->ab2s(name)));
+	if(nflag)
+		return;
+	if(libc0->strchr(name, '(') != nil)
+		atouch(name);	#  archive 
+	else if(chgtime(name) < 0){
+		perror(name);
+		Exit();
+	}
+}
+
+delete(name: array of byte)
+{
+	if(libc0->strchr(name, '(') == nil){	#  file 
+		if(sys->remove(libc0->ab2s(name)) < 0)
+			perror(name);
+	}
+	else
+		sys->fprint(sys->fildes(2), "hoon off; mk can'tdelete archive members\n");
+}
+
+timeinit(s: array of byte)
+{
+	t: int;
+	cp: array of byte;
+	r: int;
+	c, n: int;
+
+	t = daytime->now();
+	while(int s[0]){
+		cp = s;
+		do{
+			(r, n, nil) = sys->byte2char(s, 0);
+			if(r == ' ' || r == ',' || r == '\n')
+				break;
+			s = s[n: ];
+		}while(int s[0]);
+		c = int s[0];
+		s[0] = byte 0;
+		symlooki(libc0->strdup(cp), S_TIME, t).ivalue = t;
+		if(c){
+			s[0] = byte c;
+			s = s[1: ];
+		}
+		while(int s[0]){
+			(r, n, nil) = sys->byte2char(s, 0);
+			if(r != ' ' && r != ',' && r != '\n')
+				break;
+			s = s[n: ];
+		}
+	}
+}
+
+
+#
+# parse
+#
+
+infile: array of byte;
+mkinline: int;
+
+parse(f: array of byte, fd: ref Sys->FD, varoverride: int)
+{
+	hline, v: int;
+	body: array of byte;
+	head, tail: ref Word;
+	attr, set, pid: int;
+	prog, p: array of byte;
+	newfd: ref Sys->FD;
+	in: ref Iobuf;
+	buf: ref Bufblock;
+
+	if(fd == nil){
+		perror(f);
+		Exit();
+	}
+	ipush();
+	infile = libc0->strdup(f);
+	mkinline = 1;
+	in = bufio->fopen(fd, Sys->OREAD);
+	buf = newbuf();
+	while(assline(in, buf)){
+		hline = mkinline;
+		(v, head, tail, attr, prog) = rhead(buf.start);
+		case(v){
+		'<' =>
+			p = wtos(tail, ' ');
+			if(p[0] == byte 0){
+				if(-1 >= 0)
+					sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+				else
+					sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+				sys->fprint(sys->fildes(2), "missing include file name\n");
+				Exit();
+			}
+			newfd = sys->open(libc0->ab2s(p), Sys->OREAD);
+			if(newfd == nil){
+				sys->fprint(sys->fildes(2), "warning: skipping missing include file: ");
+				perror(p);
+			}
+			else
+				parse(p, newfd, 0);
+		'|' =>
+			p = wtos(tail, ' ');
+			if(p[0] == byte 0){
+				if(-1 >= 0)
+					sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+				else
+					sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+				sys->fprint(sys->fildes(2), "missing include program name\n");
+				Exit();
+			}
+			execinit();
+			anewfd := array[1] of ref Sys->FD;
+			anewfd[0] = newfd;
+			pid = pipecmd(p, envy, anewfd);
+			newfd = anewfd[0];
+			if(newfd == nil){
+				sys->fprint(sys->fildes(2), "warning: skipping missing program file: ");
+				perror(p);
+			}
+			else
+				parse(p, newfd, 0);
+			apid := array[1] of int;
+			apid[0] = pid;
+			while(waitup(-3, apid) >= 0)
+				;
+			pid = apid[0];
+			if(pid != 0){
+				sys->fprint(sys->fildes(2), "bad include program status\n");
+				Exit();
+			}
+		':' =>
+			body = rbody(in);
+			addrules(head, tail, body, attr, hline, prog);
+		'=' =>
+			if(head.next != nil){
+				if(-1 >= 0)
+					sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+				else
+					sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+				sys->fprint(sys->fildes(2), "multiple vars on left side of assignment\n");
+				Exit();
+			}
+			if(symlooki(head.s, S_OVERRIDE, 0) != nil){
+				set = varoverride;
+			}
+			else{
+				set = 1;
+				if(varoverride)
+					symlooks(head.s, S_OVERRIDE, libc0->s2ab(""));
+			}
+			if(set){
+				# 
+				# char *cp;
+				# dumpw("tail", tail);
+				# cp = wtos(tail, ' '); print("assign %s to %s\n", head->s, cp); free(cp);
+				# 
+				setvar(head.s, tail);
+				symlooks(head.s, S_WESET, libc0->s2ab(""));
+			}
+			if(attr)
+				symlooks(head.s, S_NOEXPORT, libc0->s2ab(""));
+		* =>
+			if(hline >= 0)
+				sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), hline);
+			else
+				sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+			sys->fprint(sys->fildes(2), "expected one of :<=\n");
+			Exit();
+		}
+	}
+	fd = nil;
+	freebuf(buf);
+	ipop();
+}
+
+addrules(head: ref Word, tail: ref Word, body: array of byte, attr: int, hline: int, prog: array of byte)
+{
+	w: ref Word;
+
+	assert(libc0->s2ab("addrules args"), head != nil && body != nil);
+	#  tuck away first non-meta rule as default target
+	if(target1 == nil && !(attr&REGEXP)){
+		for(w = head; w != nil; w = w.next)
+			if(charin(w.s, libc0->s2ab("%&")) != nil)
+				break;
+		if(w == nil)
+			target1 = wdup(head);
+	}
+	for(w = head; w != nil; w = w.next)
+		addrule(w.s, tail, body, head, attr, hline, prog);
+}
+
+rhead(line: array of byte): (int, ref Word, ref Word, int, array of byte)
+{
+	h, t: ref Word;
+	attr: int;
+	prog: array of byte;
+	p, pp: array of byte;
+	sep: int;
+	r: int;
+	n: int;
+	w: ref Word;
+
+	p = charin(line, libc0->s2ab(":=<"));
+	if(p == nil)
+		return ('?', nil, nil, 0, nil);
+	sep = int p[0];
+	p[0] = byte 0;
+	p = p[1: ];
+	if(sep == '<' && p[0] == byte '|'){
+		sep = '|';
+		p = p[1: ];
+	}
+	attr = 0;
+	prog = nil;
+	if(sep == '='){
+		pp = charin(p, termchars);	#  termchars is shell-dependent 
+		if(pp != nil && pp[0] == byte '='){
+			while(p != pp){
+				(r, n, nil) = sys->byte2char(p, 0);
+				case(r){
+				* =>
+					if(-1 >= 0)
+						sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+					else
+						sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+					sys->fprint(sys->fildes(2), "unknown attribute '%c'\n", int p[0]);
+					Exit();
+				'U' =>
+					attr = 1;
+				}
+				p = p[n: ];
+			}
+			p = p[1: ];	#  skip trailing '=' 
+		}
+	}
+	if(sep == ':' && int p[0] && p[0] != byte ' ' && p[0] != byte '\t'){
+		while(int p[0]){
+			(r, n, nil) = sys->byte2char(p, 0);
+			if(r == ':')
+				break;
+			ea := p[n-1];
+			p = p[n: ];
+			case(r){
+			* =>
+				if(-1 >= 0)
+					sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+				else
+					sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+				sys->fprint(sys->fildes(2), "unknown attribute '%c'\n", int ea);
+				Exit();
+			'D' =>
+				attr |= DEL;
+			'E' =>
+				attr |= NOMINUSE;
+			'n' =>
+				attr |= NOVIRT;
+			'N' =>
+				attr |= NOREC;
+			'P' =>
+				pp = libc0->strchr(p, ':');
+				if(pp == nil || pp[0] == byte 0){
+					if(-1 >= 0)
+						sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+					else
+						sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+					sys->fprint(sys->fildes(2), "missing trailing :\n");
+					Exit();
+				}
+				pp[0] = byte 0;
+				prog = libc0->strdup(p);
+				pp[0] = byte ':';
+				p = pp;
+			'Q' =>
+				attr |= QUIET;
+			'R' =>
+				attr |= REGEXP;
+			'U' =>
+				attr |= UPD;
+			'V' =>
+				attr |= VIR;
+			}
+		}
+		if(p[0] != byte ':'){
+			if(-1 >= 0)
+				sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+			else
+				sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+			sys->fprint(sys->fildes(2), "missing trailing :\n");
+			Exit();
+		}
+		p = p[1: ];
+	}
+	h = w = stow(line);
+	if(w.s[0] == byte 0 && sep != '<' && sep != '|'){
+		if(mkinline-1 >= 0)
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline-1);
+		else
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+		sys->fprint(sys->fildes(2), "no var on left side of assignment/rule\n");
+		Exit();
+	}
+	t = stow(p);
+	return (sep, h, t, attr, prog);
+}
+
+rbody(in: ref Iobuf): array of byte
+{
+	buf: ref Bufblock;
+	r, lastr: int;
+	p: array of byte;
+
+	lastr = '\n';
+	buf = newbuf();
+	for(;;){
+		r = in.getc();
+		if(r < 0)
+			break;
+		if(lastr == '\n'){
+			if(r == '#')
+				rinsert(buf, r);
+			else if(r != ' ' && r != '\t'){
+				in.ungetc();
+				break;
+			}
+		}
+		else
+			rinsert(buf, r);
+		lastr = r;
+		if(r == '\n')
+			mkinline++;
+	}
+	insert(buf, 0);
+	p = libc0->strdup(buf.start);
+	freebuf(buf);
+	return p;
+}
+
+input: adt{
+	file: array of byte;
+	line: int;
+	next: cyclic ref input;
+};
+
+inputs: ref input = nil;
+
+ipush()
+{
+	in, me: ref input;
+
+	me = ref input;
+	me.file = infile;
+	me.line = mkinline;
+	me.next = nil;
+	if(inputs == nil)
+		inputs = me;
+	else{
+		for(in = inputs; in.next != nil;)
+			in = in.next;
+		in.next = me;
+	}
+}
+
+ipop()
+{
+	in, me: ref input;
+
+	assert(libc0->s2ab("pop input list"), inputs != nil);
+	if(inputs.next == nil){
+		me = inputs;
+		inputs = nil;
+	}
+	else{
+		for(in = inputs; in.next.next != nil;)
+			in = in.next;
+		me = in.next;
+		in.next = nil;
+	}
+	infile = me.file;
+	mkinline = me.line;
+	me = nil;
+}
+
+#
+# lex
+#
+
+# 
+#  *	Assemble a line skipping blank lines, comments, and eliding
+#  *	escaped newlines
+#  
+assline(bp: ref Iobuf, buf: ref Bufblock): int
+{
+	c, lastc: int;
+
+	buf.current = 0;
+	while((c = nextrune(bp, 1)) >= 0){
+		case(c){
+		'\r' =>	#  consumes CRs for Win95 
+			continue;
+		'\n' =>
+			if(buf.current != 0){
+				insert(buf, 0);
+				return 1;
+			}
+		#  skip empty lines 
+		'\\' or '\'' or '"' =>
+			rinsert(buf, c);
+			if(escapetoken(bp, buf, 1, c) == 0)
+				Exit();
+		'`' =>
+			if(bquote(bp, buf) == 0)
+				Exit();
+		'#' =>
+			lastc = '#';
+			while((c = bp.getb()) != '\n'){
+				if(c < 0){
+					insert(buf, 0);
+					return buf.start[0] != byte 0;
+				}
+				if(c != '\r')
+					lastc = c;
+			}
+			mkinline++;
+			if(lastc == '\\')
+				break;	#  propagate escaped newlines??
+			if(buf.current != 0){
+				insert(buf, 0);
+				return 1;
+			}
+		* =>
+			rinsert(buf, c);
+		}
+	}
+	insert(buf, 0);
+	return buf.start[0] != byte 0;
+}
+
+# 
+#  *	assemble a back-quoted shell command into a buffer
+#  
+bquote(bp: ref Iobuf, buf: ref Bufblock): int
+{
+	c, line, term, start: int;
+
+	line = mkinline;
+	while((c = bp.getc()) == ' ' || c == '\t')
+		;
+	if(c == '{'){
+		term = '}';	#  rc style 
+		while((c = bp.getc()) == ' ' || c == '\t')
+			;
+	}
+	else
+		term = '`';	#  sh style 
+	start = buf.current;
+	for(; c > 0; c = nextrune(bp, 0)){
+		if(c == term){
+			insert(buf, '\n');
+			insert(buf, 0);
+			buf.current = start;
+			execinit();
+			execsh(nil, buf.start[buf.current: ], buf, envy);
+			return 1;
+		}
+		if(c == '\n')
+			break;
+		if(c == '\'' || c == '"' || c == '\\'){
+			insert(buf, c);
+			if(!escapetoken(bp, buf, 1, c))
+				return 0;
+			continue;
+		}
+		rinsert(buf, c);
+	}
+	if(line >= 0)
+		sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), line);
+	else
+		sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+	sys->fprint(sys->fildes(2), "missing closing %c after `\n", term);
+	return 0;
+}
+
+# 
+#  *	get next character stripping escaped newlines
+#  *	the flag specifies whether escaped newlines are to be elided or
+#  *	replaced with a blank.
+#  
+savec: int;
+
+nextrune(bp: ref Iobuf, elide: int): int
+{
+	c, c2: int;
+
+	if(savec){
+		c = savec;
+		savec = 0;
+		return c;
+	}
+	for(;;){
+		c = bp.getc();
+		if(c == '\\'){
+			c2 = bp.getc();
+			if(c2 == '\r'){
+				savec = c2;
+				c2 = bp.getc();
+			}
+			if(c2 == '\n'){
+				savec = 0;
+				mkinline++;
+				if(elide)
+					continue;
+				return ' ';
+			}
+			bp.ungetc();
+		}
+		if(c == '\n')
+			mkinline++;
+		return c;
+	}
+	return 0;
+}
+
+#
+# symtab
+#
+
+NHASH: con 4099;
+HASHMUL: con 79;
+
+hash := array[NHASH] of ref Symtab;
+
+syminit()
+{
+	s: ref Symtab;
+	ss, ns: ref Symtab;
+
+	for(i := 0; i < NHASH; i++){
+		s = hash[i];
+		for(ss = s; ss != nil; ss = ns){
+			ns = s.next;
+			ss = nil;
+		}
+		hash[i] = nil;
+	}
+}
+
+symval(sym: ref Symtab): int
+{
+	return sym.svalue != nil ||
+		   sym.ivalue != 0 ||
+		   sym.nvalue != nil ||
+		   sym.rvalue != nil ||
+		   sym.wvalue != nil;
+}
+		
+symlooks(sym: array of byte, space: int, s: array of byte): ref Symtab
+{
+	return symlook(sym, space, s != nil, s, 0, nil, nil, nil);
+}
+
+symlooki(sym: array of byte, space: int, i: int): ref Symtab
+{
+	return symlook(sym, space, i != 0, nil, i, nil, nil, nil);
+}
+
+symlookn(sym: array of byte, space: int, n: ref Node): ref Symtab
+{
+	return symlook(sym, space, n != nil, nil, 0, n, nil, nil);
+}
+
+symlookr(sym: array of byte, space: int, r: ref Rule): ref Symtab
+{
+	return symlook(sym, space, r != nil, nil, 0, nil, r, nil);
+}
+
+symlookw(sym: array of byte, space: int, w: ref Word): ref Symtab
+{
+	return symlook(sym, space, w != nil, nil, 0, nil, nil, w);
+}
+
+symlook(sym: array of byte, space: int, install: int, sv: array of byte, iv: int, nv: ref Node, rv: ref Rule, wv: ref Word): ref Symtab
+{
+	h: int;
+	p: array of byte;
+	s: ref Symtab;
+
+	for((p, h) = (sym, space); int p[0]; ){
+		h *= HASHMUL;
+		h += int p[0];
+		p = p[1: ];
+	}
+	if(h < 0)
+		h = ~h;
+	h %= NHASH;
+	for(s = hash[h]; s != nil; s = s.next)
+		if(s.space == space && libc0->strcmp(s.name, sym) == 0)
+			return s;
+	if(install == 0)
+		return nil;
+	s = ref Symtab;
+	s.space = space;
+	s.name = sym;
+	s.svalue = sv;
+	s.ivalue = iv;
+	s.nvalue = nv;
+	s.rvalue = rv;
+	s.wvalue = wv;
+	s.next = hash[h];
+	hash[h] = s;
+	return s;
+}
+
+symdel(sym: array of byte, space: int)
+{
+	h: int;
+	p: array of byte;
+	s, ls: ref Symtab;
+
+	#  multiple memory leaks 
+	for((p, h) = (sym, space); int p[0]; ){
+		h *= HASHMUL;
+		h += int p[0];
+		p = p[1: ];
+	}
+	if(h < 0)
+		h = ~h;
+	h %= NHASH;
+	for((s, ls) = (hash[h], nil); s != nil; (ls, s) = (s, s.next))
+		if(s.space == space && libc0->strcmp(s.name, sym) == 0){
+			if(ls != nil)
+				ls.next = s.next;
+			else
+				hash[h] = s.next;
+			s = nil;
+		}
+}
+
+symtraverse(space: int, fnx: int)
+{
+	s: ref Symtab;
+	ss: ref Symtab;
+
+	for(i := 0; i < NHASH; i++){
+		s = hash[i];
+		for(ss = s; ss != nil; ss = ss.next)
+			if(ss.space == space){
+				if(fnx == ECOPY)
+					ecopy(ss);
+				else if(fnx == PRINT1)
+					print1(ss);
+			}
+	}
+}
+
+symstat()
+{
+	s: ref Symtab;
+	ss: ref Symtab;
+	n: int;
+	l := array[1000] of int;
+
+	for(i := 0; i < 1000; i++)
+		l[i] = 0;
+	for(i = 0; i < NHASH; i++){
+		s = hash[i];
+		for((ss, n) = (s, 0); ss != nil; ss = ss.next)
+			n++;
+		l[n]++;
+	}
+	for(n = 0; n < 1000; n++)
+		if(l[n])
+			bout.puts(sys->sprint("%d of length %d\n", l[n], n));
+}
+
+#
+# varsub
+#
+
+varsub(s: array of byte): (ref Word, array of byte)
+{
+	b: ref Bufblock;
+	w: ref Word;
+
+	if(s[0] == byte '{')	#  either ${name} or ${name: A%B==C%D}
+		return expandvar(s);
+	(b, s) = varname(s);
+	if(b == nil)
+		return (nil, s);
+	(w, s) = varmatch(b.start, s);
+	freebuf(b);
+	return (w, s);
+}
+
+# 
+#  *	extract a variable name
+#  
+varname(s: array of byte): (ref Bufblock, array of byte)
+{
+	b: ref Bufblock;
+	cp: array of byte;
+	r: int;
+	n: int;
+
+	b = newbuf();
+	cp = s;
+	for(;;){
+		(r, n, nil) = sys->byte2char(cp, 0);
+		if(!(r > ' ' && libc0->strchr(libc0->s2ab("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~"), r) == nil))
+			break;
+		rinsert(b, r);
+		cp = cp[n: ];
+	}
+	if(b.current == 0){
+		if(-1 >= 0)
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+		else
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+		sys->fprint(sys->fildes(2), "missing variable name <%s>\n", libc0->ab2s(s));
+		freebuf(b);
+		return (nil, s);
+	}
+	s = cp;
+	insert(b, 0);
+	return (b, s);
+}
+
+varmatch(name: array of byte, s: array of byte): (ref Word, array of byte)
+{
+	w: ref Word;
+	sym: ref Symtab;
+	cp: array of byte;
+
+	sym = symlooki(name, S_VAR, 0);
+	if(sym != nil){
+		#  check for at least one non-NULL value 
+		for(w = sym.wvalue; w != nil; w = w.next)
+			if(w.s != nil && int w.s[0])
+				return (wdup(w), s);
+	}
+	for(cp = s; cp[0] == byte ' ' || cp[0] == byte '\t'; cp = cp[1: ])	#  skip trailing whitespace 
+		;
+	s = cp;
+	return (nil, s);
+}
+
+expandvar(s: array of byte): (ref Word, array of byte)
+{
+	w: ref Word;
+	buf: ref Bufblock;
+	sym: ref Symtab;
+	cp, begin, end: array of byte;
+
+	begin = s;
+	s = s[1: ];	#  skip the '{' 
+	(buf, s) = varname(s);
+	if(buf == nil)
+		return (nil, s);
+	cp = s;
+	if(cp[0] == byte '}'){	#  ${name} variant
+		s[0]++;	#  skip the '}' 
+		(w, s) = varmatch(buf.start, s);
+		freebuf(buf);
+		return (w, s);
+	}
+	if(cp[0] != byte ':'){
+		if(-1 >= 0)
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+		else
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+		sys->fprint(sys->fildes(2), "bad variable name <%s>\n", libc0->ab2s(buf.start));
+		freebuf(buf);
+		return (nil, s);
+	}
+	cp = cp[1: ];
+	end = charin(cp, libc0->s2ab("}"));
+	if(end == nil){
+		if(-1 >= 0)
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+		else
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+		sys->fprint(sys->fildes(2), "missing '}': %s\n", libc0->ab2s(begin));
+		Exit();
+	}
+	end[0] = byte 0;
+	s = end[1: ];
+	sym = symlooki(buf.start, S_VAR, 0);
+	if(sym == nil || !symval(sym))
+		w = newword(buf.start);
+	else
+		w = subsub(sym.wvalue, cp, end);
+	freebuf(buf);
+	return (w, s);
+}
+
+extractpat(s: array of byte, r: array of byte, term: array of byte, end: array of byte): (ref Word, array of byte)
+{
+	save: int;
+	cp: array of byte;
+	w: ref Word;
+
+	cp = charin(s, term);
+	if(cp != nil){
+		r = cp;
+		if(cp == s)
+			return (nil, r);
+		save = int cp[0];
+		cp[0] = byte 0;
+		w = stow(s);
+		cp[0] = byte save;
+	}
+	else{
+		r = end;
+		w = stow(s);
+	}
+	return (w, r);
+}
+
+subsub(v: ref Word, s: array of byte, end: array of byte): ref Word
+{
+	nmid, ok: int;
+	head, tail, w, h, a, b, c, d: ref Word;
+	buf: ref Bufblock;
+	cp, enda: array of byte;
+
+	(a, cp) = extractpat(s, cp, libc0->s2ab("=%&"), end);
+	b = c = d = nil;
+	if(cp[0] == byte '%' || cp[0] == byte '&')
+		(b, cp) = extractpat(cp[1: ], cp, libc0->s2ab("="), end);
+	if(cp[0] == byte '=')
+		(c, cp) = extractpat(cp[1: ], cp, libc0->s2ab("&%"), end);
+	if(cp[0] == byte '%' || cp[0] == byte '&')
+		d = stow(cp[1: ]);
+	else if(int cp[0])
+		d = stow(cp);
+	head = tail = nil;
+	buf = newbuf();
+	for(; v != nil; v = v.next){
+		h = w = nil;
+		(ok, nmid, enda) = submatch(v.s, a, b, nmid, enda);
+		if(ok){
+			#  enda points to end of A match in source;
+			# 			 * nmid = number of chars between end of A and start of B
+			# 			 
+			if(c != nil){
+				h = w = wdup(c);
+				while(w.next != nil)
+					w = w.next;
+			}
+			if((cp[0] == byte '%' || cp[0] == byte '&') && nmid > 0){
+				if(w != nil){
+					bufcpy(buf, w.s, libc0->strlen(w.s));
+					bufcpy(buf, enda, nmid);
+					insert(buf, 0);
+					w.s = nil;
+					w.s = libc0->strdup(buf.start);
+				}
+				else{
+					bufcpy(buf, enda, nmid);
+					insert(buf, 0);
+					h = w = newword(buf.start);
+				}
+				buf.current = 0;
+			}
+			if(d != nil && int d.s[0]){
+				if(w != nil){
+					bufcpy(buf, w.s, libc0->strlen(w.s));
+					bufcpy(buf, d.s, libc0->strlen(d.s));
+					insert(buf, 0);
+					w.s = nil;
+					w.s = libc0->strdup(buf.start);
+					w.next = wdup(d.next);
+					while(w.next != nil)
+						w = w.next;
+					buf.current = 0;
+				}
+				else
+					h = w = wdup(d);
+			}
+		}
+		if(w == nil)
+			h = w = newword(v.s);
+		if(head == nil)
+			head = h;
+		else
+			tail.next = h;
+		tail = w;
+	}
+	freebuf(buf);
+	delword(a);
+	delword(b);
+	delword(c);
+	delword(d);
+	return head;
+}
+
+submatch(s: array of byte, a: ref Word, b: ref Word, nmid: int, enda: array of byte): (int, int, array of byte)
+{
+	w: ref Word;
+	n: int;
+	end: array of byte;
+
+	n = 0;
+	for(w = a; w != nil; w = w.next){
+		n = libc0->strlen(w.s);
+		if(libc0->strncmp(s, w.s, n) == 0)
+			break;
+	}
+	if(a != nil && w == nil)	#   a == NULL matches everything
+		return (0, nmid, enda);
+	enda = s[n: ];	#  pointer to end a A part match 
+	nmid = libc0->strlen(s)-n;	#  size of remainder of source 
+	end = enda[nmid: ];
+	onmid := nmid;
+	for(w = b; w != nil; w = w.next){
+		n = libc0->strlen(w.s);
+		if(libc0->strcmp(w.s, enda[onmid-n: ]) == 0){	# end-n
+			nmid -= n;
+			break;
+		}
+	}
+	if(b != nil && w == nil)	#  b == NULL matches everything 
+		return (0, nmid, enda);
+	return (1, nmid, enda);
+}
+
+#
+# var
+#
+
+setvar(name: array of byte, value: ref Word)
+{
+	# s := libc0->ab2s(name);
+	# if(s == "ROOT" || s == "OBJTYPE"){
+	# 	if(s[0] == 'R')
+	# 		v := "";
+	# 	else
+	# 		v = "386";
+	# 	value.s = libc0->strdup(libc0->s2ab(v));
+	# }
+
+	symlookw(name, S_VAR, value).wvalue = value;
+	symlooks(name, S_MAKEVAR, libc0->s2ab(""));
+}
+
+print1(s: ref Symtab)
+{
+	w: ref Word;
+
+	bout.puts(sys->sprint("\t%s=", libc0->ab2s(s.name)));
+	for(w = s.wvalue; w != nil; w = w.next)
+		bout.puts(sys->sprint("'%s'", libc0->ab2s(w.s)));
+	bout.puts(sys->sprint("\n"));
+}
+
+dumpv(s: array of byte)
+{
+	bout.puts(sys->sprint("%s:\n", libc0->ab2s(s)));
+	symtraverse(S_VAR, PRINT1);
+}
+
+shname(a: array of byte): array of byte
+{
+	r: int;
+	n: int;
+
+	while(int a[0]){
+		(r, n, nil) = sys->byte2char(a, 0);
+		if(!(r > ' ' && libc0->strchr(libc0->s2ab("!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~"), r) == nil))
+			break;
+		a = a[n: ];
+	}
+	return a;
+}
+
+#
+# word
+#
+
+
+newword(s: array of byte): ref Word
+{
+	w: ref Word;
+
+	w = ref Word;
+	w.s = libc0->strdup(s);
+	w.next = nil;
+	return w;
+}
+
+stow(s: array of byte): ref Word
+{
+	head, w, new: ref Word;
+
+	w = head = nil;
+	while(int s[0]){
+		(new, s) = nextword(s);
+		if(new == nil)
+			break;
+		if(w != nil)
+			w.next = new;
+		else
+			head = w = new;
+		while(w.next != nil)
+			w = w.next;
+	}
+	if(head == nil)
+		head = newword(libc0->s2ab(""));
+	return head;
+}
+
+wtos(w: ref Word, sep: int): array of byte
+{
+	buf: ref Bufblock;
+	cp: array of byte;
+
+	buf = newbuf();
+	for(; w != nil; w = w.next){
+		for(cp = w.s; int cp[0]; cp = cp[1: ])
+			insert(buf, int cp[0]);
+		if(w.next != nil)
+			insert(buf, sep);
+	}
+	insert(buf, 0);
+	cp = libc0->strdup(buf.start);
+	freebuf(buf);
+	return cp;
+}
+
+wtostr(w: ref Word, sep: int): string
+{
+	return libc0->ab2s(wtos(w, sep));
+}
+
+wdup(w: ref Word): ref Word
+{
+	v, new, base: ref Word;
+
+	v = base = nil;
+	while(w != nil){
+		new = newword(w.s);
+		if(v != nil)
+			v.next = new;
+		else
+			base = new;
+		v = new;
+		w = w.next;
+	}
+	return base;
+}
+
+delword(w: ref Word)
+{
+	v: ref Word;
+
+	while((v = w) != nil){
+		w = w.next;
+		if(v.s != nil)
+			v.s = nil;
+		v = nil;
+	}
+}
+
+# 
+#  *	break out a word from a string handling quotes, executions,
+#  *	and variable expansions.
+#  
+nextword(s: array of byte): (ref Word, array of byte)
+{
+	b: ref Bufblock;
+	head, tail, w: ref Word;
+	r, n: int;
+	cp: array of byte;
+
+	cp = s;
+	b = newbuf();
+	head = tail = nil;
+	while(cp[0] == byte ' ' || cp[0] == byte '\t')	#  leading white space 
+		cp = cp[1: ];
+	loop := 1;
+	while(loop && int cp[0]){
+		(r, n, nil) = sys->byte2char(cp, 0);
+		cp = cp[n: ];
+		case(r){
+		' ' or '\t' or '\n' =>
+			loop = 0;
+		'\\' or '\'' or '"' =>
+			cp = expandquote(cp, r, b);
+			if(cp == nil){
+				sys->fprint(sys->fildes(2), "missing closing quote: %s\n", libc0->ab2s(s));
+				Exit();
+			}
+		'$' =>
+			(w, cp) = varsub(cp);
+			if(w == nil)
+				break;
+			if(b.current != 0){
+				bufcpy(b, w.s, libc0->strlen(w.s));
+				insert(b, 0);
+				w.s = nil;
+				w.s = libc0->strdup(b.start);
+				b.current = 0;
+			}
+			if(head != nil){
+				bufcpy(b, tail.s, libc0->strlen(tail.s));
+				bufcpy(b, w.s, libc0->strlen(w.s));
+				insert(b, 0);
+				tail.s = nil;
+				tail.s = libc0->strdup(b.start);
+				tail.next = w.next;
+				w.s = nil;
+				w = nil;
+				b.current = 0;
+			}
+			else
+				tail = head = w;
+			while(tail.next != nil)
+				tail = tail.next;
+		* =>
+			rinsert(b, r);
+		}
+	}
+	s = cp;
+	if(b.current != 0){
+		if(head != nil){
+			oc := b.current;
+			cp = b.start[b.current: ];
+			bufcpy(b, tail.s, libc0->strlen(tail.s));
+			bufcpy(b, b.start, oc);
+			insert(b, 0);
+			tail.s = nil;
+			tail.s = libc0->strdup(cp);
+		}
+		else{
+			insert(b, 0);
+			head = newword(b.start);
+		}
+	}
+	freebuf(b);
+	return (head, s);
+}
+
+dumpw(s: array of byte, w: ref Word)
+{
+	bout.puts(sys->sprint("%s", libc0->ab2s(s)));
+	for(; w != nil; w = w.next)
+		bout.puts(sys->sprint(" '%s'", libc0->ab2s(w.s)));
+	bout.putb(byte '\n');
+}
+
+#
+# match
+#
+
+match(name: array of byte, template: array of byte, stem: array of byte): int
+{
+	r: int;
+	n: int;
+
+	while(int name[0] && int template[0]){
+		(r, n, nil) = sys->byte2char(template, 0);
+		if(r == '%' || r == '&')
+			break;
+		while(n--)
+			if(name[0] != template[0])
+				return 0;
+			name = name[1: ];
+			template = template[1: ];
+	}
+	if(!(template[0] == byte '%' || template[0] == byte '&'))
+		return 0;
+	n = libc0->strlen(name)-libc0->strlen(template[1: ]);
+	if(n < 0 || libc0->strcmp(template[1: ], name[n: ]))
+		return 0;
+	libc0->strncpy(stem, name, n);
+	stem[n] = byte 0;
+	if(template[0] == byte '&')
+		return charin(stem, libc0->s2ab("./")) == nil;
+	return 1;
+}
+
+subst(stem: array of byte, template: array of byte, dest: array of byte)
+{
+	r: int;
+	s: array of byte;
+	n: int;
+
+	while(int template[0]){
+		(r, n, nil) = sys->byte2char(template, 0);
+		if(r == '%' || r == '&'){
+			template = template[n: ];
+			for(s = stem; int s[0]; s = s[1: ]){
+				dest[0] = s[0];
+				dest = dest[1: ];
+			}
+		}
+		else
+			while(n--){
+				dest[0] = template[0];
+				dest = dest[1: ];
+				template = template[1: ];
+			}
+	}
+	dest[0] = byte 0;
+}
+
+#
+# os
+#
+
+shell := "/dis/sh.dis";
+shellname := "sh";
+
+pcopy(a: array of ref Sys->FD): array of ref Sys->FD
+{
+	b := array[2] of ref Sys->FD;
+	b[0: ] = a[0: 2];
+	return b;
+}
+
+readenv()
+{
+	p: array of byte;
+	envf, f: ref Sys->FD;
+	e := array[20] of Sys->Dir;
+	nam := array[NAMELEN+5] of byte;
+	i, n, lenx: int;
+	w: ref Word;
+
+	sys->pctl(Sys->FORKENV, nil);	#   use copy of the current environment variables 
+	if(sys->open("/env/autoload", Sys->OREAD) == nil){
+		fd := sys->create("/env/autoload", Sys->OWRITE, 8r666);
+		if(fd != nil)
+			sys->fprint(fd, "std");
+	}
+	envf = sys->open("/env", Sys->OREAD);
+	if(envf == nil)
+		return;
+	for(;;){
+		(n, e) = sys->dirread(envf);
+		if(n <= 0)
+			break;
+		for(i = 0; i < n; i++){
+			lenx = int e[i].length;
+			#  don't import funny names, NULL values,
+			# 				 * or internal mk variables
+			# 				 
+			if(lenx <= 0 || shname(libc0->s2ab(e[i].name))[0] != byte '\0')
+				continue;
+			if(symlooki(libc0->s2ab(e[i].name), S_INTERNAL, 0) != nil)
+				continue;
+			stob(nam, sys->sprint("/env/%s", e[i].name));
+			f = sys->open(libc0->ab2s(nam), Sys->OREAD);
+			if(f == nil)
+				continue;
+			p = array[lenx+1] of byte;
+			if(sys->read(f, p, lenx) != lenx){
+				perror(nam);
+				f = nil;
+				continue;
+			}
+			f = nil;
+			if(p[lenx-1] == byte 0)
+				lenx--;
+			else
+				p[lenx] = byte 0;
+			w = encodenulls(p, lenx);
+			p = nil;
+			p = libc0->strdup(libc0->s2ab(e[i].name));
+			setvar(p, w);
+			symlooks(p, S_EXPORTED, libc0->s2ab("")).svalue = libc0->s2ab("");
+		}
+	}
+	envf = nil;
+}
+
+#  break string of values into words at 01's or nulls
+encodenulls(s: array of byte, n: int): ref Word
+{
+	w, head: ref Word;
+	cp: array of byte;
+
+	head = w = nil;
+	while(n-- > 0){
+		for(cp = s; int cp[0] && cp[0] != byte '\u0001'; cp = cp[1: ])
+			n--;
+		cp[0] = byte 0;
+		if(w != nil){
+			w.next = newword(s);
+			w = w.next;
+		}
+		else
+			head = w = newword(s);
+		s = cp[1: ];
+	}
+	if(head == nil)
+		head = newword(libc0->s2ab(""));
+	return head;
+}
+
+#  as well as 01's, change blanks to nulls, so that rc will
+#  * treat the words as separate arguments
+#  
+exportenv(e: array of Envy)
+{
+	f: ref Sys->FD;
+	n, hasvalue: int;
+	w: ref Word;
+	sy: ref Symtab;
+	nam := array[NAMELEN+5] of byte;
+
+	for(i := 0; e[i].name != nil; i++){
+		sy = symlooki(e[i].name, S_VAR, 0);
+		if(e[i].values == nil || e[i].values.s == nil || e[i].values.s[0] == byte 0)
+			hasvalue = 0;
+		else
+			hasvalue = 1;
+		if(sy == nil && !hasvalue)	#  non-existant null symbol 
+			continue;
+		stob(nam, sys->sprint("/env/%s", libc0->ab2s(e[i].name)));
+		if(sy != nil && !hasvalue){	#  Remove from environment 
+			#  we could remove it from the symbol table
+			# 				 * too, but we're in the child copy, and it
+			# 				 * would still remain in the parent's table.
+			# 				 
+			sys->remove(libc0->ab2s(nam));
+			delword(e[i].values);
+			e[i].values = nil;	#  memory leak 
+			continue;
+		}
+		f = sys->create(libc0->ab2s(nam), Sys->OWRITE, 8r666);
+		if(f == nil){
+			sys->fprint(sys->fildes(2), "can't create %s, f=%d\n", libc0->ab2s(nam), f.fd);
+			perror(nam);
+			continue;
+		}
+		for(w = e[i].values; w != nil; w = w.next){
+			n = libc0->strlen(w.s);
+			if(n){
+				if(sys->write(f, w.s, n) != n)
+					perror(nam);
+				if(w.next != nil && sys->write(f, libc0->s2ab(" "), 1) != 1)
+					perror(nam);
+			}
+		}
+		f = nil;
+	}
+}
+
+dirtime(dir: array of byte, path: array of byte)
+{
+	i: int;
+	fd: ref Sys->FD;
+	n: int;
+	t: int;
+	db := array[32] of Sys->Dir;
+	buf := array[4096] of byte;
+
+	fd = sys->open(libc0->ab2s(dir), Sys->OREAD);
+	if(fd != nil){
+		for(;;){
+			(n, db) = sys->dirread(fd);
+			if(n <= 0)
+				break;
+			for(i = 0; i < n; i++){
+				t = db[i].mtime;
+				if(t == 0)	#  zero mode file 
+					continue;
+				stob(buf, sys->sprint("%s%s", libc0->ab2s(path), db[i].name));
+				if(symlooki(buf, S_TIME, 0) != nil)
+					continue;
+				symlooki(libc0->strdup(buf), S_TIME, t).ivalue = t;
+			}
+		}
+		fd = nil;
+	}
+}
+
+waitfor(msg: array of byte): int
+{
+	wm: array of byte;
+	pid: int;
+
+	(pid, wm) = wait();
+	if(pid > 0)
+		libc0->strncpy(msg, wm, ERRLEN);
+	return pid;
+}
+
+expunge(pid: int, msg: array of byte)
+{
+	postnote(PNPROC, pid, msg);
+}
+
+sub(cmd: array of byte, env: array of Envy): array of byte
+{
+	buf := newbuf();
+	shprint(cmd, env, buf);
+	return buf.start;
+}
+
+fork1(c1: chan of int, args: array of byte, cmd: array of byte, buf: ref Bufblock, e: array of Envy, in: array of ref Sys->FD, out: array of ref Sys->FD)
+{
+	pid: int;
+
+	c1<- = sys->pctl(Sys->FORKFD|Sys->FORKENV, nil);
+
+	{
+		if(buf != nil)
+			out[0] = nil;
+		if(sys->pipe(in) < 0){
+			perrors("pipe");
+			Exit();
+		}
+		c2 := chan of int;
+		spawn fork2(c2, cmd, pcopy(in), pcopy(out));
+		pid = <- c2;
+		addwait();
+		{
+			sys->dup(in[0].fd, 0);
+			if(buf != nil){
+				sys->dup(out[1].fd, 1);
+				out[1] = nil;
+			}
+			in[0] = nil;
+			in[1] = nil;
+			if(e != nil)
+				exportenv(e);
+			argss := libc0->ab2s(args);
+			sys->pctl(Sys->NEWFD, 0 :: 1 :: 2 :: nil);
+			if(shflags != nil)
+				execl(shell, shellname, shflags, argss, nil, nil);
+			else
+				execl(shell, shellname, argss, nil, nil, nil);
+			exit;
+		}
+	}
+}
+
+fork2(c2: chan of int, cmd: array of byte, in: array of ref Sys->FD, out: array of ref Sys->FD)
+{
+	n, p: int;
+
+	c2<- = sys->pctl(Sys->FORKFD, nil);
+
+	{
+		out[1] = nil;
+		in[0] = nil;
+		p = libc0->strlen(cmd);
+		c := 0;
+		while(c < p){	# cmd < p
+			if(debug&D_EXEC)
+				sys->fprint(sys->fildes(1), "writing '%s' to shell\n", libc0->ab2s(cmd[0: p-c]));
+			n = sys->write(in[1], cmd, p-c);	# p-cmd
+			if(n < 0)
+				break;
+			cmd = cmd[n: ];
+			c += n;
+		}
+		in[1] = nil;
+		exit;
+	}
+}
+
+execsh(args: array of byte, cmd: array of byte, buf: ref Bufblock, e: array of Envy): int
+{
+	tot, n, pid: int;
+	in := array[2] of ref Sys->FD;
+	out := array[2] of ref Sys->FD;
+
+	cmd = sub(cmd, e);
+
+	if(buf != nil && sys->pipe(out) < 0){
+		perrors("pipe");
+		Exit();
+	}
+	c1 := chan of int;
+	spawn fork1(c1, args, cmd, buf, e, in, pcopy(out));
+	pid = <-c1;
+	addwait();
+	if(buf != nil){
+		out[1] = nil;
+		tot = 0;
+		for(;;){
+			if(buf.current >= buf.end)
+				growbuf(buf);
+			n = sys->read(out[0], buf.start[buf.current: ], buf.end-buf.current);
+			if(n <= 0)
+				break;
+			buf.current += n;
+			tot += n;
+		}
+		if(tot && buf.start[buf.current-1] == byte '\n')
+			buf.current--;
+		out[0] = nil;
+	}
+	return pid;
+}
+
+fork3(c3: chan of int, cmd: array of byte, e: array of Envy, fd: array of ref Sys->FD, pfd: array of ref Sys->FD)
+{
+	c3<- = sys->pctl(Sys->FORKFD|Sys->FORKENV, nil);
+
+	{
+		if(fd != nil){
+			pfd[0] = nil;
+			sys->dup(pfd[1].fd, 1);
+			pfd[1] = nil;
+		}
+		if(e != nil)
+			exportenv(e);
+		cmds := libc0->ab2s(cmd);
+		if(shflags != nil)
+			execl(shell, shellname, shflags, "-c", cmds, nil);
+		else
+			execl(shell, shellname, "-c", cmds, nil, nil);
+		exit;
+	}
+}
+
+pipecmd(cmd: array of byte, e: array of Envy, fd: array of ref Sys->FD): int
+{
+	pid: int;
+	pfd := array[2] of ref Sys->FD;
+
+	cmd = sub(cmd, e);
+
+	if(debug&D_EXEC)
+		sys->fprint(sys->fildes(1), "pipecmd='%s'", libc0->ab2s(cmd));	# 
+	if(fd != nil && sys->pipe(pfd) < 0){
+		perrors("pipe");
+		Exit();
+	}
+	c3 := chan of int;
+	spawn fork3(c3, cmd, e, fd, pcopy(pfd));
+	pid = <- c3;
+	addwait();
+	if(fd != nil){
+		pfd[1] = nil;
+		fd[0] = pfd[0];
+	}
+	return pid;
+}
+
+Exit()
+{
+	while(wait().t0 >= 0)
+		;
+	bout.flush();
+	raise "fail:error";
+}
+
+nnote: int;
+
+notifyf(a: array of byte, msg: array of byte): int
+{
+	if(a != nil)
+		;
+	if(++nnote > 100){	#  until andrew fixes his program 
+		sys->fprint(sys->fildes(2), "mk: too many notes\n");
+		# notify(nil);
+		abort();
+	}
+	if(libc0->strcmp(msg, libc0->s2ab("interrupt")) != 0 && libc0->strcmp(msg, libc0->s2ab("hangup")) != 0)
+		return 0;
+	killchildren(msg);
+	return -1;
+}
+
+catchnotes()
+{
+	# atnotify(notifyf, 1);
+}
+
+chgtime(name: array of byte): int
+{
+	(ok, nil) := sys->stat(libc0->ab2s(name));
+	if(ok >= 0){
+		sbuf := sys->nulldir;
+		sbuf.mtime = daytime->now();
+		return sys->wstat(libc0->ab2s(name), sbuf);
+	}
+	fd := sys->create(libc0->ab2s(name), Sys->OWRITE, 8r666);
+	if(fd == nil)
+		return -1;
+	fd = nil;
+	return 0;
+}
+
+rcopy(tox: array of array of byte, match: array of Resub, n: int)
+{
+	c: int;
+	p: array of byte;
+
+	i := 0;
+	tox[0] = match[0].sp;	#  stem0 matches complete target 
+	for(i++; --n > 0; i++){
+		if(match[i].sp != nil && match[i].ep != nil){
+			p = match[i].ep;
+			c = int p[0];
+			p[0] = byte 0;
+			tox[i] = libc0->strdup(match[i].sp);
+			p[0] = byte c;
+		}
+		else
+			tox[i] = nil;
+	}
+}
+
+mkdirstat(name: array of byte): (int, Sys->Dir)
+{
+	return sys->stat(libc0->ab2s(name));
+}
+
+membername(s: array of byte, fd: ref Sys->FD, sz: int): array of byte
+{
+	if(fd == nil)
+		;
+	if(sz)
+		;
+	return s;
+}
+
+#
+# sh
+#
+
+termchars := array[] of { byte '\'', byte '=', byte ' ', byte '\t', byte '\0' };	# used in parse.c to isolate assignment attribute
+shflags := "";	#  rc flag to force non-interactive mode - was -l
+IWS: int = '\u0001';	#  inter-word separator in env - not used in plan 9 
+
+# 
+#  *	This file contains functions that depend on rc's syntax.  Most
+#  *	of the routines extract strings observing rc's escape conventions
+#  
+# 
+#  *	skip a token in single quotes.
+#  
+squote(cp: array of byte): array of byte
+{
+	r: int;
+	n, nn: int;
+
+	while(int cp[0]){
+		(r, n, nil) = sys->byte2char(cp, 0);
+		if(r == '\''){
+			(r, nn, nil) = sys->byte2char(cp[n: ], 0);
+			n += nn;
+			if(r != '\'')
+				return cp;
+		}
+		cp = cp[n: ];
+	}
+	if(-1 >= 0)	#  should never occur 
+		sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+	else
+		sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+	sys->fprint(sys->fildes(2), "missing closing '\n");
+	return nil;
+}
+
+# 
+#  *	search a string for characters in a pattern set
+#  *	characters in quotes and variable generators are escaped
+#  
+charin(cp: array of byte, pat: array of byte): array of byte
+{
+	r: int;
+	n, vargen: int;
+
+	vargen = 0;
+	while(int cp[0]){
+		(r, n, nil) = sys->byte2char(cp, 0);
+		case(r){
+		'\'' =>	#  skip quoted string 
+			cp = squote(cp[1: ]);	#  n must = 1 
+			if(cp == nil)
+				return nil;
+		'$' =>
+			if((cp[1: ])[0] == byte '{')
+				vargen = 1;
+		'}' =>
+			if(vargen)
+				vargen = 0;
+			else if(libc0->strchr(pat, r) != nil)
+				return cp;
+		* =>
+			if(vargen == 0 && libc0->strchr(pat, r) != nil)
+				return cp;
+		}
+		cp = cp[n: ];
+	}
+	if(vargen){
+		if(-1 >= 0)
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), -1);
+		else
+			sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+		sys->fprint(sys->fildes(2), "missing closing } in pattern generator\n");
+	}
+	return nil;
+}
+
+# 
+#  *	extract an escaped token.  Possible escape chars are single-quote,
+#  *	double-quote,and backslash.  Only the first is valid for rc. the
+#  *	others are just inserted into the receiving buffer.
+#  
+expandquote(s: array of byte, r: int, b: ref Bufblock): array of byte
+{
+	n: int;
+
+	if(r != '\''){
+		rinsert(b, r);
+		return s;
+	}
+	while(int s[0]){
+		(r, n, nil) = sys->byte2char(s, 0);
+		s = s[n: ];
+		if(r == '\''){
+			if(s[0] == byte '\'')
+				s = s[1: ];
+			else
+				return s;
+		}
+		rinsert(b, r);
+	}
+	return nil;
+}
+
+# 
+#  *	Input an escaped token.  Possible escape chars are single-quote,
+#  *	double-quote and backslash.  Only the first is a valid escape for
+#  *	rc; the others are just inserted into the receiving buffer.
+#  
+escapetoken(bp: ref Iobuf, buf: ref Bufblock, preserve: int, esc: int): int
+{
+	c, line: int;
+
+	if(esc != '\'')
+		return 1;
+	line = mkinline;
+	while((c = nextrune(bp, 0)) > 0){
+		if(c == '\''){
+			if(preserve)
+				rinsert(buf, c);
+			c = bp.getc();
+			if(c < 0)
+				break;
+			if(c != '\''){
+				bp.ungetc();
+				return 1;
+			}
+		}
+		rinsert(buf, c);
+	}
+	if(line >= 0)
+		sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), line);
+	else
+		sys->fprint(sys->fildes(2), "mk: %s:%d: syntax error; ", libc0->ab2s(infile), mkinline);
+	sys->fprint(sys->fildes(2), "missing closing %c\n", esc);
+	return 0;
+}
+
+# 
+#  *	copy a single-quoted string; s points to char after opening quote
+#  
+copysingle(s: array of byte, buf: ref Bufblock): array of byte
+{
+	r, n: int;
+
+	while(int s[0]){
+		(r, n, nil) = sys->byte2char(s, 0);
+		s = s[n: ];
+		rinsert(buf, r);
+		if(r == '\'')
+			break;
+	}
+	return s;
+}
+
+# 
+#  *	check for quoted strings.  backquotes are handled here; single quotes above.
+#  *	s points to char after opening quote, q.
+#  
+copyq(s: array of byte, q: int, buf: ref Bufblock): array of byte
+{
+	n: int;
+
+	if(q == '\'')	#  copy quoted string 
+		return copysingle(s, buf);
+	if(q != '`')	#  not quoted 
+		return s;
+	while(int s[0]){	#  copy backquoted string 
+		(q, n, nil) = sys->byte2char(s, 0);
+		s = s[n: ];
+		rinsert(buf, q);
+		if(q == '}')
+			break;
+		if(q == '\'')
+			s = copysingle(s, buf);	#  copy quoted string 
+	}
+	return s;
+}
+
+#
+# shprint
+#
+
+shprint(s: array of byte, env: array of Envy, buf: ref Bufblock)
+{
+	n: int;
+	r: int;
+
+	while(int s[0]){
+		(r, n, nil) = sys->byte2char(s, 0);
+		if(r == '$')
+			s = vexpand(s, env, buf);
+		else{
+			rinsert(buf, r);
+			s = s[n: ];
+			s = copyq(s, r, buf);	# handle quoted strings
+		}
+	}
+	insert(buf, 0);
+}
+
+mygetenv(name: array of byte, env: array of Envy): array of byte
+{
+	if(env == nil)
+		return nil;
+	if(symlooki(name, S_WESET, 0) == nil && symlooki(name, S_INTERNAL, 0) == nil)
+		return nil;
+	#  only resolve internal variables and variables we've set 
+	for(e := 0; env[e].name != nil; e++){
+		if(libc0->strcmp(env[e].name, name) == 0)
+			return wtos(env[e].values, ' ');
+	}
+	return nil;
+}
+
+vexpand(w: array of byte, env: array of Envy, buf: ref Bufblock): array of byte
+{
+	s: array of byte;
+	carry: byte;
+	p, q: array of byte;
+
+	assert(libc0->s2ab("vexpand no $"), w[0] == byte '$');
+	p = w[1: ];	#  skip dollar sign 
+	if(p[0] == byte '{'){
+		p = p[1: ];
+		q = libc0->strchr(p, '}');
+		if(q == nil)
+			q = libc0->strchr(p, 0);
+	}
+	else
+		q = shname(p);
+	carry = q[0];
+	q[0] = byte 0;
+	s = mygetenv(p, env);
+	q[0] = carry;
+	if(carry == byte '}')
+		q = q[1: ];
+	if(s != nil){
+		bufcpy(buf, s, libc0->strlen(s));
+		s = nil;
+	}
+	else
+		#  copy name intact
+		bufcpy(buf, w, libc0->strlen(w)-libc0->strlen(q));	# q-w
+	return q;
+}
+
+front(s: array of byte)
+{
+	t, q: array of byte;
+	i, j: int;
+	# flds := array[512] of array of byte;
+	fields: list of string;
+
+	q = libc0->strdup(s);
+	(i, fields) = sys->tokenize(libc0->ab2s(q), " \t\n");
+	flds := array[len fields] of array of byte;
+	for(j = 0; j < len flds; j++){
+		flds[j] = libc0->s2ab(hd fields);
+		fields = tl fields;
+	}
+	if(i > 5){
+		flds[4] = flds[i-1];
+		flds[3] = libc0->s2ab("...");
+		i = 5;
+	}
+	t = s;
+	for(j = 0; j < i; j++){
+		for(s = flds[j]; int s[0]; ){
+			t[0] = s[0];
+			s = s[1: ];
+			t = t[1: ];
+		}
+		t[0] = byte ' ';
+		t = t[1: ];
+	}
+	t[0] = byte 0;
+	q = nil;
+}
+
+#
+# env
+#
+
+ENVQUANTA: con 10;
+
+envy: array of Envy;
+nextv: int;
+myenv: array of array of byte;
+
+initenv()
+{
+	p: int;
+
+	myenv = array[19] of {
+		libc0->s2ab("target"),
+		libc0->s2ab("stem"),
+		libc0->s2ab("prereq"),
+		libc0->s2ab("pid"),
+		libc0->s2ab("nproc"),
+		libc0->s2ab("newprereq"),
+		libc0->s2ab("alltarget"),
+		libc0->s2ab("newmember"),
+		libc0->s2ab("stem0"),	#  must be in order from here 
+		libc0->s2ab("stem1"),
+		libc0->s2ab("stem2"),
+		libc0->s2ab("stem3"),
+		libc0->s2ab("stem4"),
+		libc0->s2ab("stem5"),
+		libc0->s2ab("stem6"),
+		libc0->s2ab("stem7"),
+		libc0->s2ab("stem8"),
+		libc0->s2ab("stem9"),
+		array of byte nil,
+	};
+
+	for(p = 0; myenv[p] != nil; p++)
+		symlooks(myenv[p], S_INTERNAL, libc0->s2ab(""));
+	readenv();	#  o.s. dependent 
+}
+
+envsize: int;
+
+envinsert(name: array of byte, value: ref Word)
+{
+	if(nextv >= envsize){
+		envsize += ENVQUANTA;
+		es := len envy;
+		ne := array[envsize] of Envy;
+		if(es)
+			ne[0: ] = envy[0: es];
+		envy = ne;
+	}
+	envy[nextv].name = name;
+	envy[nextv++].values = value;
+}
+
+envupd(name: array of byte, value: ref Word)
+{
+	e: int;
+
+	for(e = 0; envy[e].name != nil; e++)
+		if(libc0->strcmp(name, envy[e].name) == 0){
+			delword(envy[e].values);
+			envy[e].values = value;
+			return;
+		}
+	envy[e].name = name;
+	envy[e].values = value;
+	envinsert(nil, nil);
+}
+
+ecopy(s: ref Symtab)
+{
+	p: int;
+
+	if(symlooki(s.name, S_NOEXPORT, 0) != nil)
+		return;
+	for(p = 0; myenv[p] != nil; p++)
+		if(libc0->strcmp(myenv[p], s.name) == 0)
+			return;
+	envinsert(s.name, s.wvalue);
+}
+
+execinit()
+{
+	p: int;
+
+	nextv = 0;
+	for(p = 0; myenv[p] != nil; p++)
+		envinsert(myenv[p], stow(libc0->s2ab("")));
+	symtraverse(S_VAR, ECOPY);
+	envinsert(nil, nil);
+}
+
+buildenv(j: ref Job, slot: int): array of Envy
+{
+	p: int;
+	cp, qp: array of byte;
+	w, v: ref Word;
+	l: ref Word;
+	i: int;
+	buf := array[256] of byte;
+
+	envupd(libc0->s2ab("target"), wdup(j.t));
+	if(j.r.attr&REGEXP)
+		envupd(libc0->s2ab("stem"), newword(libc0->s2ab("")));
+	else
+		envupd(libc0->s2ab("stem"), newword(j.stem));
+	envupd(libc0->s2ab("prereq"), wdup(j.p));
+	stob(buf, sys->sprint("%d", sys->pctl(0, nil)));
+	envupd(libc0->s2ab("pid"), newword(buf));
+	stob(buf, sys->sprint("%d", slot));
+	envupd(libc0->s2ab("nproc"), newword(buf));
+	envupd(libc0->s2ab("newprereq"), wdup(j.np));
+	envupd(libc0->s2ab("alltarget"), wdup(j.at));
+	l = ref Word;
+	l.next = v = w = wdup(j.np);
+	while(w != nil){
+		cp = libc0->strchr(w.s, '(');
+		if(cp != nil){
+			cp = cp[1: ];
+			qp = libc0->strchr(cp, ')');
+			if(qp != nil){
+				qp[0] = byte 0;
+				libc0->strcpy(w.s, cp);
+				l.next = w;
+				l = w;
+				w = w.next;
+				continue;
+			}
+		}
+		l.next = w.next;
+		w.s = nil;
+		w = nil;
+		w = l.next;
+	}
+	v = l.next;
+	envupd(libc0->s2ab("newmember"), v);
+	#  update stem0 -> stem9 
+	for(p = 0; myenv[p] != nil; p++)
+		if(libc0->strcmp(myenv[p], libc0->s2ab("stem0")) == 0)
+			break;
+	for(i = 0; myenv[p] != nil; i++){
+		if(j.r.attr&REGEXP && j.match[i] != nil)
+			envupd(myenv[p], newword(j.match[i]));
+		else
+			envupd(myenv[p], newword(libc0->s2ab("")));
+		p++;
+	}
+	return envy;
+}
+
+#
+# dir
+#
+
+bulkmtime(dir: array of byte)
+{
+	buf := array[4096] of byte;
+	ss, s: array of byte;
+	db: Sys->Dir;
+	ok: int;
+
+	if(dir != nil){
+		s = dir;
+		if(libc0->strcmp(dir, libc0->s2ab("/")) == 0)
+			libc0->strcpy(buf, dir);
+		else
+			stob(buf, sys->sprint("%s/", libc0->ab2s(dir)));
+		(ok, db) = mkdirstat(dir);
+		if(ok >= 0 && (db.qid.qtype&Sys->QTDIR) == 0){
+			#  bugger off 
+			sys->fprint(sys->fildes(2), "mk: %s is not a directory path=%ux\n", libc0->ab2s(dir), int db.qid.path);
+			Exit();
+		}
+	}
+	else{
+		s = libc0->s2ab(".");
+		buf[0] = byte 0;
+	}
+	if(symlooki(s, S_BULKED, 0) != nil)
+		return;
+	ss = libc0->strdup(s);
+	symlooks(ss, S_BULKED, ss);
+	dirtime(s, buf);
+}
+
+mtime(name: array of byte): int
+{
+	sbuf: Sys->Dir;
+	s, ss: array of byte;
+	carry: byte;
+	ok: int;
+
+	s = libc0->strrchr(name, '/');
+	if(s == name)
+		s = s[1: ];
+	if(s != nil){
+		ss = name;
+		carry = s[0];
+		s[0] = byte 0;
+	}
+	else{
+		ss = nil;
+		carry = byte 0;
+	}
+	bulkmtime(ss);
+	if(int carry)
+		s[0] = carry;
+	(ok, sbuf) = mkdirstat(name);
+	if(ok < 0)
+		return 0;
+	return sbuf.mtime;
+}
+
+filetime(name: array of byte): int
+{
+	sym: ref Symtab;
+
+	sym = symlooki(name, S_TIME, 0);
+	if(sym != nil)
+		return sym.ivalue;	#  uggh 
+	return mtime(name);
+}
+
+#
+# archive
+#
+
+dolong: int;
+
+atimeof(force: int, name: array of byte): int
+{
+	sym: ref Symtab;
+	t: int;
+	archive, member: array of byte;
+	buf := array[512] of byte;
+
+	(archive, member) = split(name);
+	if(archive == nil)
+		Exit();
+	t = mtime(archive);
+	sym = symlooki(archive, S_AGG, 0);
+	if(sym != nil){
+		if(force || t > sym.ivalue){
+			atimes(archive);
+			sym.ivalue = t;
+		}
+	}
+	else{
+		atimes(archive);
+		#  mark the aggegate as having been done 
+		symlooks(libc0->strdup(archive), S_AGG, libc0->s2ab("")).ivalue = t;
+	}
+	#  truncate long member name to sizeof of name field in archive header 
+	if(dolong)
+		stob(buf, sys->sprint("%s(%s)", libc0->ab2s(archive), libc0->ab2s(member)));
+	else
+		stob(buf, sys->sprint("%s(%.*s)", libc0->ab2s(archive), SARNAME, libc0->ab2s(member)));
+	sym = symlooki(buf, S_TIME, 0);
+	if(sym != nil)
+		return sym.ivalue;	#  uggh 
+	return 0;
+}
+
+atouch(name: array of byte)
+{
+	archive, member: array of byte;
+	fd: ref Sys->FD;
+	i: int;
+	# h: ar_hdr;
+	t: int;
+
+	(archive, member) = split(name);
+	if(archive == nil)
+		Exit();
+	fd = sys->open(libc0->ab2s(archive), Sys->ORDWR);
+	if(fd == nil){
+		fd = sys->create(libc0->ab2s(archive), Sys->OWRITE, 8r666);
+		if(fd == nil){
+			perror(archive);
+			Exit();
+		}
+		sys->write(fd, libc0->s2ab(ARMAG), SARMAG);
+	}
+	if(symlooki(name, S_TIME, 0) != nil){
+		#  hoon off and change it in situ 
+		sys->seek(fd, big SARMAG, 0);
+		buf := array[SAR_HDR] of byte;
+		while(sys->read(fd, buf, SAR_HDR) == SAR_HDR){
+			name = buf[0: SARNAME];
+			for(i = SARNAME-1; i > 0 && name[i] == byte ' '; i--)
+				;
+			name[i+1] = byte 0;
+			if(libc0->strcmp(member, name) == 0){
+				t = SARNAME-SAR_HDR;	#  ughgghh 
+				sys->seek(fd, big t, 1);
+				sys->fprint(fd, "%-12d", daytime->now());
+				break;
+			}
+			t = int string buf[48: 58];
+			if(t&8r1)
+				t++;
+			sys->seek(fd, big t, 1);
+		}
+	}
+	fd = nil;
+}
+
+atimes(ar: array of byte)
+{
+	# h: ar_hdr;
+	t: int;
+	fd: ref Sys->FD;
+	i: int;
+	buf := array[BIGBLOCK] of byte;
+	n: array of byte;
+	name := array[SARNAME+1] of byte;
+
+	fd = sys->open(libc0->ab2s(ar), Sys->OREAD);
+	if(fd == nil)
+		return;
+	if(sys->read(fd, buf, SARMAG) != SARMAG){
+		fd = nil;
+		return;
+	}
+	b := array[SAR_HDR] of byte;
+	while(sys->read(fd, b, SAR_HDR) == SAR_HDR){
+		t = int string b[16: 28];
+		if(t == 0)	#  as it sometimes happens; thanks ken 
+			t = 1;
+		hname := b[0: SARNAME];
+		libc0->strncpy(name, hname, SARNAME);
+		for(i = SARNAME-1; i > 0 && name[i] == byte ' '; i--)
+			;
+		if(name[i] == byte '/')	#  system V bug 
+			i--;
+		name[i+1] = byte 0;
+		n = membername(name, fd, int string b[48: 58]);
+		if(n == nil){
+			dolong = 1;
+			continue;
+		}
+		stob(buf, sys->sprint("%s(%s)", libc0->ab2s(ar), libc0->ab2s(n)));
+		symlooki(libc0->strdup(buf), S_TIME, t).ivalue = t;
+		t = int string b[48: 58];
+		if(t&8r1)
+			t++;
+		sys->seek(fd, big t, 1);
+	}
+	fd = nil;
+}
+
+typex(file: array of byte): int
+{
+	fd: ref Sys->FD;
+	buf := array[SARMAG] of byte;
+
+	fd = sys->open(libc0->ab2s(file), Sys->OREAD);
+	if(fd == nil){
+		if(symlooki(file, S_BITCH, 0) == nil){
+			bout.puts(sys->sprint("%s doesn't exist: assuming it will be an archive\n", libc0->ab2s(file)));
+			symlooks(file, S_BITCH, file);
+		}
+		return 1;
+	}
+	if(sys->read(fd, buf, SARMAG) != SARMAG){
+		fd = nil;
+		return 0;
+	}
+	fd = nil;
+	return !libc0->strncmp(libc0->s2ab(ARMAG), buf, SARMAG);
+}
+
+split(name: array of byte): (array of byte, array of byte)
+{
+	member: array of byte;
+	p, q: array of byte;
+
+	p = libc0->strdup(name);
+	q = libc0->strchr(p, '(');
+	if(q != nil){
+		q[0] = byte 0;
+		q = q[1: ];
+		member = q;
+		q = libc0->strchr(q, ')');
+		if(q != nil)
+			q[0] = byte 0;
+		if(typex(p))
+			return (p, member);
+		p = nil;
+		sys->fprint(sys->fildes(2), "mk: '%s' is not an archive\n", libc0->ab2s(name));
+	}
+	return (nil, member);
+}
+
+#
+# bufblock
+#
+
+freelist: ref Bufblock;
+
+QUANTA: con 4096;
+
+newbuf(): ref Bufblock
+{
+	p: ref Bufblock;
+
+	if(freelist != nil){
+		p = freelist;
+		freelist = freelist.next;
+	}
+	else{
+		p = ref Bufblock;
+		p.start = array[QUANTA*1] of byte;
+		p.end = QUANTA;
+	}
+	p.current = 0;
+	p.start[0] = byte 0;
+	p.next = nil;
+	return p;
+}
+
+freebuf(p: ref Bufblock)
+{
+	p.next = freelist;
+	freelist = p;
+}
+
+growbuf(p: ref Bufblock)
+{
+	n: int;
+	f: ref Bufblock;
+	cp: array of byte;
+
+	n = p.end+QUANTA;
+	#  search the free list for a big buffer 
+	for(f = freelist; f != nil; f = f.next){
+		if(f.end >= n){
+			f.start[0: ] = p.start[0: p.end];
+			cp = f.start;
+			f.start = p.start;
+			p.start = cp;
+			cpi := f.end;
+			f.end = p.end;
+			p.end = cpi;
+			f.current = 0;
+			break;
+		}
+	}
+	if(f == nil){	#  not found - grow it 
+		nps := array[n] of byte;
+		for(i := 0; i < p.end; i++)
+			nps[i] = p.start[i];
+		p.start = nps;
+		p.end = n;
+	}
+	p.current = n-QUANTA;
+}
+
+bufcpy(buf: ref Bufblock, cp: array of byte, n: int)
+{
+	i := 0;
+	while(n--)
+		insert(buf, int cp[i++]);
+}
+
+insert(buf: ref Bufblock, c: int)
+{
+	if(buf.current >= buf.end)
+		growbuf(buf);
+	buf.start[buf.current++] = byte c;
+}
+
+rinsert(buf: ref Bufblock, r: int)
+{
+	n: int;
+
+	b := array[Sys->UTFmax] of byte;
+	n = sys->char2byte(r, b, 0);
+	if(buf.current+n > buf.end)
+		growbuf(buf);
+	buf.start[buf.current: ] = b[0: n];
+	buf.current += n;
+}
+
--- /dev/null
+++ b/appl/cmd/mk/mkbinds
@@ -1,0 +1,2 @@
+/appl/cmd/mk/mkconfig	/mkconfig
+/appl/cmd/mk/mksubdirs	/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/cmd/mk/mkconfig
@@ -1,0 +1,28 @@
+#
+#	Set the following 4 variables.  The host system is the system where
+#	the software will be built; the target system is where it will run.
+#	They are almost always the same.
+
+#	On Nt systems, the ROOT path MUST be of the form `drive:/path'
+ROOT=
+
+#
+#	Except for building kernels, SYSTARG must always be the same as SYSHOST
+#
+SYSHOST=Plan9		# build system OS type (Hp, Inferno, Irix, Linux, Nt, Plan9, Solaris)
+SYSTARG=$SYSHOST		# target system OS type (Hp, Inferno, Irix, Linux, Nt, Plan9, Solaris)
+
+#
+#	specify the architecture of the target system - Inferno imports it from the
+#	environment; for other systems it is usually just hard-coded
+#
+#OBJTYPE=386			# target system object type (s800, mips, 386, arm, sparc)
+OBJTYPE=386
+
+#
+#	no changes required beyond this point
+#
+OBJDIR=$SYSTARG/$OBJTYPE
+
+<$ROOT/mkfiles/mkhost-$SYSHOST			# variables appropriate for host system
+<$ROOT/mkfiles/mkfile-$SYSTARG-$OBJTYPE	# variables used to build target object type
--- /dev/null
+++ b/appl/cmd/mk/mkfile
@@ -1,0 +1,19 @@
+<../../../mkconfig
+
+TARG=	mk.dis\
+
+MODULES=\
+	ar.m\
+
+SYSMODULES= \
+	bufio.m\
+	draw.m\
+	math.m\
+	sys.m\
+	regex.m\
+	daytime.m\
+	libc0.m\
+
+DISBIN=$ROOT/dis
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/mk/mksubdirs
@@ -1,0 +1,16 @@
+all:V:	all-$SHELLTYPE
+install:V: install-$SHELLTYPE
+uninstall:V: uninstall-$SHELLTYPE
+nuke:V: nuke-$SHELLTYPE
+clean:V: clean-$SHELLTYPE
+
+%-rc %-nt %-sh:QV:
+	load std
+	for j in $DIRS {
+		if { ftest -d $j } {
+			echo 'cd' $j '; mk' $MKFLAGS $stem
+			cd $j; mk $MKFLAGS $stem; cd ..
+		} {
+			! ftest -e $j || raise $j^' not a directory'
+		}
+	}
--- /dev/null
+++ b/appl/cmd/mkdir.b
@@ -1,0 +1,75 @@
+implement Mkdir;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+
+stderr: ref Sys->FD;
+
+Mkdir: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	if(argv == nil || (argv = tl argv) == nil)
+		exit;
+	pflag := 0;
+	if(hd argv == "-p"){
+		pflag = 1;
+		argv = tl argv;
+	}
+	e := "";
+	for(; argv != nil; argv = tl argv){
+		dir := hd argv;
+		if(!pflag){
+			(ok, nil) := sys->stat(dir);
+			if(ok < 0){
+				if(mkdir(dir) < 0)
+					e = "error";
+			}else{
+				sys->fprint(stderr, "mkdir: %s already exists\n", dir);
+				e = "error";
+			}
+		}else if(mkpath(dir) < 0)
+			e = "error";
+	}
+	if(e != nil)
+		raise "fail:"+e;
+}
+
+mkpath(dir: string): int
+{
+	(nil, flds) := sys->tokenize(dir, "/");
+	s := "";
+	if(dir != "" && dir[0] != '/')
+		s = ".";
+	for(; flds != nil; flds = tl flds){
+		s += "/"+hd flds;
+		(ok, d) := sys->stat(s);
+		if(ok < 0){
+			if(mkdir(s) < 0)
+				return -1;
+		}else if((d.mode & Sys->DMDIR) == 0){
+			sys->fprint(stderr, "mkdir: can't create %s: %s not a directory\n", dir, s);
+			return -1;
+		}
+	}
+	return 0;
+}
+
+mkdir(dir: string): int
+{
+	f := sys->create(dir, Sys->OREAD, Sys->DMDIR + 8r777);
+	if(f == nil) {
+		sys->fprint(stderr, "mkdir: can't create %s: %r\n", dir);
+		return -1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/appl/cmd/mkfile
@@ -1,0 +1,227 @@
+<../../mkconfig
+
+DIRS=\
+	auth\
+	auxi\
+	avr\
+	disk\
+	fs\
+	install\
+	ip\
+	lego\
+	limbo\
+	mash\
+	mk\
+	mpc\
+	ndb\
+	owen\
+	scheduler\
+	sh\
+	spki\
+	ssh\
+	usb\
+
+TARG=\
+	9660srv.dis\
+	9export.dis\
+	9srvfs.dis\
+	9win.dis\
+	B.dis\
+	ar.dis\
+	archfs.dis\
+	auplay.dis\
+	auhdr.dis\
+	basename.dis\
+	bind.dis\
+	# bit2gif.dis\
+	bytes.dis\
+	cal.dis\
+	calc.dis\
+	cat.dis\
+	cd.dis\
+	cddb.dis\
+	chgrp.dis\
+	chmod.dis\
+	cleanname.dis\
+	cmp.dis\
+	comm.dis\
+	cook.dis\
+	cprof.dis\
+	cp.dis\
+	cpu.dis\
+	crypt.dis\
+	date.dis\
+	dbfs.dis\
+	dd.dis\
+	dial.dis\
+	diff.dis\
+	disdep.dis\
+	disdump.dis\
+	dossrv.dis\
+	du.dis\
+	echo.dis\
+	ed.dis\
+	emuinit.dis\
+	env.dis\
+	export.dis\
+	fc.dis\
+	fcp.dis\
+	fmt.dis\
+	fortune.dis\
+	freq.dis\
+	fs.dis\
+	ftest.dis\
+	ftpfs.dis\
+	getauthinfo.dis\
+	gettar.dis\
+	# gif2bit.dis\
+	grep.dis\
+	gunzip.dis\
+	gzip.dis\
+	idea.dis\
+	import.dis\
+	iostats.dis\
+	itest.dis\
+	itreplay.dis\
+	kill.dis\
+	listen.dis\
+	lockfs.dis\
+	logfile.dis\
+	look.dis\
+	ls.dis\
+	lstar.dis\
+	m4.dis\
+	man2html.dis\
+	man2txt.dis\
+	mc.dis\
+	md5sum.dis\
+	mdb.dis\
+	memfs.dis\
+	metamorph.dis\
+	mkdir.dis\
+	mntgen.dis\
+	mount.dis\
+	mouse.dis\
+	mprof.dis\
+	mv.dis\
+	netkey.dis\
+	netstat.dis\
+	newer.dis\
+	ns.dis\
+	nsbuild.dis\
+	os.dis\
+	p.dis\
+	pause.dis\
+	plumb.dis\
+	plumber.dis\
+	prof.dis\
+	ps.dis\
+	puttar.dis\
+	pwd.dis\
+	ramfile.dis\
+	randpass.dis\
+	raw2iaf.dis\
+	rawdbfs.dis\
+	rcmd.dis\
+	rdp.dis\
+	read.dis\
+	rioimport.dis\
+	rm.dis\
+	runas.dis\
+	sed.dis\
+	sendmail.dis\
+	sha1sum.dis\
+	sleep.dis\
+	sort.dis\
+	src.dis\
+	stack.dis\
+	stackv.dis\
+	stream.dis\
+	strings.dis\
+	styxchat.dis\
+	styxmon.dis\
+	styxlisten.dis\
+	sum.dis\
+	tail.dis\
+	tarfs.dis\
+	tclsh.dis\
+	tcs.dis\
+	tee.dis\
+	telnet.dis\
+	test.dis\
+	time.dis\
+	timestamp.dis\
+	tkcmd.dis\
+	touch.dis\
+	touchcal.dis\
+	tokenize.dis\
+	tr.dis\
+	trfs.dis\
+	tsort.dis\
+	unicode.dis\
+	units.dis\
+	uniq.dis\
+	unmount.dis\
+	uudecode.dis\
+	uuencode.dis\
+	vacfs.dis\
+	vacget.dis\
+	vacput.dis\
+	wav2iaf.dis\
+	wc.dis\
+	webgrab.dis\
+	wish.dis\
+	wmexport.dis\
+	wmimport.dis\
+	xargs.dis\
+	xd.dis\
+	yacc.dis\
+	zeros.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	bufio.m\
+	bundle.m\
+	daytime.m\
+	draw.m\
+	env.m\
+	filepat.m\
+	filter.m\
+	fslib.m\
+	ir.m\
+	keyring.m\
+	man.m\
+	newns.m\
+	prefab.m\
+	readdir.m\
+	regex.m\
+	security.m\
+	sh.m\
+	srv.m\
+	string.m\
+	styx.m\
+	styxlib.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+	url.m\
+	webget.m\
+	workdir.m\
+
+DISBIN=$ROOT/dis
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
+
+auhdr.dis: auplay.dis
+	rm -f auhdr.dis && cp auplay.dis auhdr.dis
+
+dbfs.dis: $MODDIR/styxservers.m
+rawdbfs.dis: $MODDIR/styxservers.m
+import.dis:	$MODDIR/encoding.m $MODDIR/factotum.m
+basename.dis: $MODDIR/names.m
+cleanname.dis: $MODDIR/names.m
+vacfs.dis:	$MODDIR/vac.m $MODDIR/venti.m
+vacget.dis:	$MODDIR/vac.m $MODDIR/venti.m
+vacput.dis:	$MODDIR/vac.m $MODDIR/venti.m
--- /dev/null
+++ b/appl/cmd/mntgen.b
@@ -1,0 +1,188 @@
+implement Mntgen;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Ebadfid, Enotfound, Eopen, Einuse: import Styxservers;
+	Styxserver, readbytes, Navigator, Fid: import styxservers;
+
+	nametree: Nametree;
+	Tree: import nametree;
+
+Mntgen: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+Qroot: con big 16rfffffff;
+
+badmodule(p: string)
+{
+	sys->fprint(sys->fildes(2), "cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+DEBUG: con 0;
+
+Entry: adt {
+	refcount: int;
+	path: big;
+};
+refcounts := array[10] of Entry;
+tree: ref Tree;
+nav: ref Navigator;
+
+uniq: int;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	if (styx == nil)
+		badmodule(Styx->PATH);
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	if (styxservers == nil)
+		badmodule(Styxservers->PATH);
+	styxservers->init(styx);
+ 
+	nametree = load Nametree Nametree->PATH;
+	if (nametree == nil)
+		badmodule(Nametree->PATH);
+	nametree->init();
+
+	navop: chan of ref Styxservers->Navop;
+	(tree, navop) = nametree->start();
+	nav = Navigator.new(navop);
+	(tchan, srv) := Styxserver.new(sys->fildes(0), nav, Qroot);
+
+	tree.create(Qroot, dir(".", Sys->DMDIR | 8r555, Qroot));
+
+	for (;;) {
+		gm := <-tchan;
+		if (gm == nil) {
+			tree.quit();
+			exit;
+		}
+		e := handlemsg(gm, srv, tree);
+		if (e != nil)
+			srv.reply(ref Rmsg.Error(gm.tag, e));
+	}
+}
+
+walk1(c: ref Fid, name: string): string
+{
+	if (name == ".."){
+		if (c.path != Qroot)
+			decref(c.path);
+		c.walk(Sys->Qid(Qroot, 0, Sys->QTDIR));
+	} else if (c.path == Qroot) {
+		(d, nil) := nav.walk(c.path, name);
+		if (d == nil)
+			d = addentry(name);
+		else
+			incref(d.qid.path);
+		c.walk(d.qid);
+	} else
+		return Enotfound;
+	return nil;
+}
+
+handlemsg(gm: ref Styx->Tmsg, srv: ref Styxserver, nil: ref Tree): string
+{
+	pick m := gm {
+	Walk =>
+		c := srv.getfid(m.fid);
+		if(c == nil)
+			return Ebadfid;
+		if(c.isopen)
+			return Eopen;
+		if(m.newfid != m.fid){
+			nc := srv.newfid(m.newfid);
+			if(nc == nil)
+				return Einuse;
+			c = c.clone(nc);
+			incref(c.path);
+		}
+		qids := array[len m.names] of Sys->Qid;
+		oldpath := c.path;
+		oldqtype := c.qtype;
+		incref(oldpath);
+		for (i := 0; i < len m.names; i++){
+			err := walk1(c, m.names[i]);
+			if (err != nil){
+				if(m.newfid != m.fid){
+					decref(c.path);
+					srv.delfid(c);
+				}
+				c.path = oldpath;
+				c.qtype = oldqtype;
+				if(i == 0)
+					return err;
+				srv.reply(ref Rmsg.Walk(m.tag, qids[0:i]));
+				return nil;
+			}
+			qids[i] = Sys->Qid(c.path, 0, c.qtype);
+		}
+		decref(oldpath);
+		srv.reply(ref Rmsg.Walk(m.tag, qids));
+	Clunk =>
+		c := srv.clunk(m);
+		if (c != nil && c.path != Qroot)
+			decref(c.path);
+	* =>
+		srv.default(gm);
+	}
+	return nil;
+}
+
+addentry(name: string): ref Sys->Dir
+{
+	for (i := 0; i < len refcounts; i++)
+		if (refcounts[i].refcount == 0)
+			break;
+	if (i == len refcounts) {
+		refcounts = (array[len refcounts * 2] of Entry)[0:] = refcounts;
+		for (j := i; j < len refcounts; j++)
+			refcounts[j].refcount = 0;
+	}
+	d := dir(name, Sys->DMDIR|8r555, big i | (big uniq++ << 32));
+	tree.create(Qroot, d);
+	refcounts[i] = (1, d.qid.path);
+	return ref d;
+}
+
+incref(q: big)
+{
+	id := int q;
+	if (id >= 0 && id < len refcounts){
+		refcounts[id].refcount++;
+	}
+}
+
+decref(q: big)
+{
+	id := int q;
+	if (id >= 0 && id < len refcounts){
+		if (--refcounts[id].refcount == 0)
+			tree.remove(refcounts[id].path);
+	}
+}
+
+Blankdir: Sys->Dir;
+dir(name: string, perm: int, qid: big): Sys->Dir
+{
+	d := Blankdir;
+	d.name = name;
+	d.uid = "me";
+	d.gid = "me";
+	d.qid.path = qid;
+	if (perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	else
+		d.qid.qtype = Sys->QTFILE;
+	d.mode = perm;
+	return d;
+}
--- /dev/null
+++ b/appl/cmd/mount.b
@@ -1,0 +1,336 @@
+implement Mount;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "keyring.m";
+include "security.m";
+include "dial.m";
+	dial: Dial;
+include "factotum.m";
+include "styxconv.m";
+include "styxpersist.m";
+include "arg.m";
+include "sh.m";
+
+Mount: module
+{
+	init:	 fn(nil: ref Draw->Context, nil: list of string);
+};
+
+verbose := 0;
+doauth := 1;
+do9 := 0;
+oldstyx := 0;
+persist := 0;
+showstyx := 0;
+quiet := 0;
+
+alg := "none";
+keyfile: string;
+spec: string;
+addr: string;
+
+fail(status, msg: string)
+{
+	sys->fprint(sys->fildes(2), "mount: %s\n", msg);
+	raise "fail:"+status;
+}
+
+nomod(mod: string)
+{
+	fail("load", sys->sprint("can't load %s: %r", mod));
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+        dial = load Dial Dial->PATH;
+        if(dial == nil)
+                 nomod(Dial->PATH);
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+
+	arg->init(args);
+	arg->setusage("mount [-a|-b] [-coA9] [-C cryptoalg] [-k keyfile] [-q] net!addr|file|{command} mountpoint [spec]");
+	flags := 0;
+	while((o := arg->opt()) != 0){
+		case o {
+		'a' =>
+			flags |= Sys->MAFTER;
+		'b' =>
+			flags |= Sys->MBEFORE;
+		'c' =>
+			flags |= Sys->MCREATE;
+		'C' =>
+			alg = arg->earg();
+		'k' or
+		'f' =>
+			keyfile = arg->earg();
+		'A' =>
+			doauth = 0;
+		'9' =>
+			doauth = 0;
+			do9 = 1;
+		'o' =>
+			oldstyx = 1;
+		'v' =>
+			verbose = 1;
+		'P' =>
+			persist = 1;
+		'S' =>
+			showstyx = 1;
+		'q' =>
+			quiet = 1;
+		*   =>
+			arg->usage();
+		}
+	}
+	args = arg->argv();
+	if(len args != 2){
+		if(len args != 3)
+			arg->usage();
+		spec = hd tl tl args;
+	}
+	arg = nil;
+	addr = hd args;
+	mountpoint := hd tl args;
+
+	if(oldstyx && do9)
+		fail("usage", "cannot combine -o and -9 options");
+
+	fd := connect(ctxt, addr);
+	ok: int;
+	if(do9){
+		fd = styxlog(fd);
+		factotum := load Factotum Factotum->PATH;
+		if(factotum == nil)
+			nomod(Factotum->PATH);
+		factotum->init();
+		ok = factotum->mount(fd, mountpoint, flags, spec, keyfile).t0;
+	}else{
+		err: string;
+		if(!persist){
+			(fd, err) = authcvt(fd);
+			if(fd == nil)
+				fail("error", err);
+		}
+		fd = styxlog(fd);
+		ok = sys->mount(fd, nil, mountpoint, flags, spec);
+	}
+	if(ok < 0 && !quiet)
+		fail("mount failed", sys->sprint("mount failed: %r"));
+}
+
+connect(ctxt: ref Draw->Context, dest: string): ref Sys->FD
+{
+	if(dest != nil && dest[0] == '{' && dest[len dest - 1] == '}'){
+		if(persist)
+			fail("usage", "cannot persistently mount a command");
+		doauth = 0;
+		return popen(ctxt, dest :: nil);
+	}
+	(n, nil) := sys->tokenize(dest, "!");
+	if(n == 1){
+		fd := sys->open(dest, Sys->ORDWR);
+		if(fd != nil){
+			if(persist)
+				fail("usage", "cannot persistently mount a file");
+			return fd;
+		}
+		if(dest[0] == '/')
+			fail("open failed", sys->sprint("can't open %s: %r", dest));
+	}
+	svc := "styx";
+	if(do9)
+		svc = "9fs";
+	dest = dial->netmkaddr(dest, "net", svc);
+	if(persist){
+		styxpersist := load Styxpersist Styxpersist->PATH;
+		if(styxpersist == nil)
+			fail("load", sys->sprint("cannot load %s: %r", Styxpersist->PATH));
+		sys->pipe(p := array[2] of ref Sys->FD);
+		(c, err) := styxpersist->init(p[0], do9, nil);
+		if(c == nil)
+			fail("error", "styxpersist: "+err);
+		spawn dialler(c, dest);
+		return p[1];
+	}
+	c := dial->dial(dest, nil);
+	if(c == nil)
+			fail("dial failed",  sys->sprint("can't dial %s: %r", dest));
+	return c.dfd;
+}
+
+dialler(dialc: chan of chan of ref Sys->FD, dest: string)
+{
+	while((reply := <-dialc) != nil){
+		if(verbose)
+			sys->print("dialling %s\n", addr);
+		c := dial->dial(dest, nil);
+		if(c == nil){
+			reply <-= nil;
+			continue;
+		}
+		(fd, err) := authcvt(c.dfd);
+		if(fd == nil && verbose)
+			sys->print("%s\n", err);
+		# XXX could check that user at the other end is still the same.
+		reply <-= fd;
+	}
+}
+
+authcvt(fd: ref Sys->FD): (ref Sys->FD, string)
+{
+	err: string;
+	if(doauth){
+		(fd, err) = authenticate(keyfile, alg, fd, addr);
+		if(fd == nil)
+			return (nil, err);
+		if(verbose)
+			sys->print("remote username is %s\n", err);
+	}
+	if(oldstyx)
+		return cvstyx(fd);
+	return (fd, nil);
+}
+
+popen(ctxt: ref Draw->Context, argv: list of string): ref Sys->FD
+{
+	sh := load Sh Sh->PATH;
+	if(sh == nil)
+		nomod(Sh->PATH);
+	sync := chan of int;
+	fds := array[2] of ref Sys->FD;
+	sys->pipe(fds);
+	spawn runcmd(sh, ctxt, argv, fds[0], sync);
+	<-sync;
+	return fds[1];
+}
+
+runcmd(sh: Sh, ctxt: ref Draw->Context, argv: list of string, stdin: ref Sys->FD, sync: chan of int)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	stdin = nil;
+	sync <-= 0;
+	sh->run(ctxt, argv);
+}
+
+cvstyx(fd: ref Sys->FD): (ref Sys->FD, string)
+{
+	styxconv := load Styxconv Styxconv->PATHNEW2OLD;
+	if(styxconv == nil)
+		return (nil, sys->sprint("cannot load %s: %r", Styxconv->PATHNEW2OLD));
+	styxconv->init();
+	p := array[2] of ref Sys->FD;
+	if(sys->pipe(p) < 0)
+		return (nil, sys->sprint("can't create pipe: %r"));
+	spawn styxconv->styxconv(p[1], fd);
+	p[1] = nil;
+	return (p[0], nil);
+}
+
+authenticate(keyfile, alg: string, dfd: ref Sys->FD, addr: string): (ref Sys->FD, string)
+{
+	cert : string;
+
+	kr := load Keyring Keyring->PATH;
+	if(kr == nil)
+		return (nil, sys->sprint("cannot load %s: %r", Keyring->PATH));
+
+	kd := "/usr/" + user() + "/keyring/";
+	if(keyfile == nil) {
+		cert = kd + dial->netmkaddr(addr, "tcp", "");
+		(ok, nil) := sys->stat(cert);
+		if (ok < 0)
+			cert = kd + "default";
+	}
+	else if(len keyfile > 0 && keyfile[0] != '/')
+		cert = kd + keyfile;
+	else
+		cert = keyfile;
+	ai := kr->readauthinfo(cert);
+	if(ai == nil)
+		return (nil, sys->sprint("cannot read %s: %r", cert));
+
+	auth := load Auth Auth->PATH;
+	if(auth == nil)
+		nomod(Auth->PATH);
+
+	err := auth->init();
+	if(err != nil)
+		return (nil, "cannot init auth: "+err);
+
+	fd: ref Sys->FD;
+	(fd, err) = auth->client(alg, ai, dfd);
+	if(fd == nil)
+		return (nil, "authentication failed: "+err);
+	return (fd, err);
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n]; 
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("#p/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "kill");
+}
+
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+
+styxlog(fd: ref Sys->FD): ref Sys->FD
+{
+	if(showstyx){
+		sys->pipe(p := array[2] of ref Sys->FD);
+		styx = load Styx Styx->PATH;
+		styx->init();
+		spawn tmsgreader(p[0], fd, p1 := chan[1] of int, p2 := chan[1] of int);
+		spawn rmsgreader(fd, p[0], p2, p1);
+		fd = p[1];
+	}
+	return fd;
+}
+
+tmsgreader(cfd, sfd: ref Sys->FD, p1, p2: chan of int)
+{
+	p1 <-= sys->pctl(0, nil);
+	m: ref Tmsg;
+	do{
+		m = Tmsg.read(cfd, 9000);
+		sys->print("%s\n", m.text());
+		d := m.pack();
+		if(sys->write(sfd, d, len d) != len d)
+			sys->print("tmsg write error: %r\n");
+	} while(m != nil && tagof(m) != tagof(Tmsg.Readerror));
+	kill(<-p2);
+}
+
+rmsgreader(sfd, cfd: ref Sys->FD, p1, p2: chan of int)
+{
+	p1 <-= sys->pctl(0, nil);
+	m: ref Rmsg;
+	do{
+		m = Rmsg.read(sfd, 9000);
+		sys->print("%s\n", m.text());
+		d := m.pack();
+		if(sys->write(cfd, d, len d) != len d)
+			sys->print("rmsg write error: %r\n");
+	} while(m != nil && tagof(m) != tagof(Tmsg.Readerror));
+	kill(<-p2);
+}
--- /dev/null
+++ b/appl/cmd/mouse.b
@@ -1,0 +1,394 @@
+implement mouse;
+# ported from plan 9's aux/mouse
+
+include "sys.m";
+	sys: Sys;
+	sprint, fprint, sleep: import sys;
+include "draw.m";
+
+stderr: ref Sys->FD;
+
+mouse: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Sleep500: 	con 500;
+Sleep1000:	con 1000;
+Sleep2000:	con 2000;
+TIMEOUT: 	con 5000;
+fail := "fail:";
+usage()
+{
+	fprint(stderr, "usage: mouse [type]\n");
+	raise fail+"usage";
+}
+
+write(fd: ref Sys->FD, buf: array of byte, n: int): int
+{
+	if (debug) {
+		sys->fprint(stderr, "write(%d) ", fd.fd);
+		for (i := 0; i < len buf; i++) {
+			sys->fprint(stderr, "'%c' ", int buf[i]);
+		}
+		sys->fprint(stderr, "\n");
+	}
+	return sys->write(fd, buf, n);
+}
+
+speeds := array[] of {"b1200", "b2400", "b4800", "b9600"};
+debug := 0;
+can9600 := 0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+{
+	if (argv == nil)
+		usage();
+
+	argv = tl argv;
+
+
+	while (argv != nil && len (arg := hd argv) > 1 && arg[0] == '-') {
+		case arg[1] {
+		'D' =>
+			debug = 1;
+		* =>
+			usage();
+		}
+		argv = tl argv;
+	}
+	if (len argv > 1)
+		usage();
+
+	p: string;
+	if (argv == nil)
+		p = mouseprobe();
+	else
+		p = hd argv;
+	if (p != nil && !isnum(p)) {
+		mouseconfig(p);
+		return;
+	}
+	if (p == nil) {
+		serial("0");
+		serial("1");
+		fprint(stderr, "mouse: no mouse detected\n");
+	} else {
+		err := serial(p);
+		fprint(stderr, "mouse: %s\n", err);
+	}
+}
+exception{
+	# this could be taken out so the shell could
+	# get an indication that the command has failed.
+	"fail:*" =>
+		;
+}
+}
+
+# probe for a serial mouse on port p;
+# return some an error string if not found. 
+serial(p: string): string
+{
+	baud := 0;
+	f := sys->sprint("/dev/eia%sctl", p);
+	if ((ctl := sys->open(f, Sys->ORDWR)) == nil)
+		return sprint("can't open %s - %r\n", f);
+
+	f = sys->sprint("/dev/eia%s", p);
+	if ((data := sys->open(f, Sys->ORDWR)) == nil)
+		return sprint("can't open %s - %r\n", f);
+
+	if(debug) fprint(stderr, "ctl=%d, data=%d\n", ctl.fd, data.fd);
+
+	if(debug) fprint(stderr, "MorW()\n");
+	mtype := MorW(ctl, data);
+	if (mtype == 0) {
+		if(debug) return "no mouse detected";
+
+		if(debug) fprint(stderr, "C()\n");
+		mtype = C(ctl, data);
+	}
+	if (mtype == 0)
+		return "no mouse detected on port "+p;
+
+	if(debug)fprint(stderr, "done eia setup\n");
+	mt := "serial " + p;
+	case mtype {
+	* =>
+		return "unknown mouse type";
+	'C' =>
+		if(debug) fprint(stderr, "Logitech 5 byte mouse\n");
+		Cbaud(ctl, data, baud);
+	'W' =>
+		if(debug) fprint(stderr, "Type W mouse\n");
+		Wbaud(ctl, data, baud);
+	'M' =>
+		if(debug) fprint(stderr, "Microsoft compatible mouse\n");
+		mt += " M";
+	}
+	mouseconfig(mt);
+	return nil;
+}
+
+mouseconfig(mt: string)
+{
+	if ((conf := sys->open("/dev/mousectl", Sys->OWRITE)) == nil) {
+		fprint(stderr, "mouse: can't open mousectl - %r\n");
+		raise fail+"open mousectl";
+	}
+	if(debug) fprint(stderr, "opened mousectl\n");
+	if (write(conf, array of byte mt, len array of byte mt) < 0) {
+		fprint(stderr, "mouse: error setting mouse type - %r\n");
+		raise fail+"write conf";
+	}
+	fprint(stderr, "mouse: configured as '%s'\n", mt);
+}
+
+isnum(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] < '0' || s[i] > '9')
+			return 0;
+	return 1;
+}
+
+mouseprobe(): string
+{
+	if ((probe := sys->open("/dev/mouseprobe", Sys->OREAD)) == nil) {
+		fprint(stderr, "mouse: can't open mouseprobe - %r\n");
+		return nil;
+	}
+	buf := array[64] of byte;
+	n := sys->read(probe, buf, len buf);
+	if (n <= 0)
+		return nil;
+	if (buf[n - 1] == byte '\n')
+		n--;
+	if(debug) fprint(stderr, "mouse probe detected mouse of type '%s'\n", string buf[0:n]);
+	return string buf[0:n];
+}
+
+readbyte(fd: ref Sys->FD): int
+{
+	buf := array[1] of byte;
+	(n, err) := timedread(fd, buf, 1, 200);
+	if (n < 0) {
+		if (err == nil)
+			return -1;
+		fprint(stderr, "mouse: readbyte failed - %s\n", err);
+		raise fail+"read failed";
+	}
+	return int buf[0];
+}
+
+slowread(fd: ref Sys->FD, buf: array of byte, nbytes: int, msg: string): int
+{
+	for (i := 0; i < nbytes; i++) {
+		if ((c := readbyte(fd)) == -1)
+			break;
+		buf[i] = byte c;
+	}
+	if(debug) dumpbuf(buf[0:i], msg);
+	return i;
+}
+
+dumpbuf(buf: array of byte, msg: string)
+{
+	sys->fprint(stderr, "%s", msg);
+	for (i := 0; i < len buf; i++)
+		sys->fprint(stderr, "#%ux ", int buf[i]);
+	sys->fprint(stderr, "\n");
+}
+
+toggleRTS(fd: ref Sys->FD)
+{
+	# reset the mouse (toggle RTS)
+	# must be >100mS
+	writes(fd, "d0");
+	sleep(10);
+	writes(fd, "r0");
+	sleep(Sleep500);
+	writes(fd, "d1");
+	sleep(10);
+	writes(fd, "r1");
+	sleep(Sleep500);
+}
+
+setupeia(fd: ref Sys->FD, baud, bits: string)
+{
+	# set the speed to 1200/2400/4800/9600 baud,
+	# 7/8-bit data, one stop bit and no parity
+
+	(abaud, abits) := (array of byte baud, array of byte bits);
+	if(debug)sys->fprint(stderr, "setupeia(%s,%s)\n", baud, bits);
+	write(fd, abaud, len abaud);
+	write(fd, abits, len abits);
+	writes(fd, "s1");
+	writes(fd, "pn");
+}
+
+# check for types M, M3 & W
+#
+# we talk to all these mice using 1200 baud
+
+MorW(ctl, data: ref Sys->FD): int
+{
+	# set up for type M, V or W
+	# flush any pending data
+
+	setupeia(ctl, "b1200", "l7");
+	toggleRTS(ctl);
+	if(debug)sys->fprint(stderr, "toggled RTS\n");
+
+	buf := array[256] of byte;
+	while (slowread(data, buf, len buf, "flush: ") > 0)
+		;
+	if(debug) sys->fprint(stderr, "done slowread\n");
+	toggleRTS(ctl);
+
+	# see if there's any data from the mouse
+	# (type M, V and W mice)
+	c := slowread(data, buf, len buf, "check M: ");
+	
+	# type M, V and W mice return "M" or "M3" after reset.
+	# check for type W by sending a 'Send Standard Configuration'
+	# command, "*?".
+	if (c > 0 && int buf[0] == 'M') {
+		writes(data, "*?");
+		c = slowread(data, buf, len buf, "check W: ");
+		# 4 bytes back indicates a type W mouse
+		if (c == 4) {
+			if (int buf[1] & (1<<4))
+				can9600 = 1;
+			setupeia(ctl, "b1200", "l8");
+			writes(data, "*U");
+			slowread(data, buf, len buf, "check W: ");
+			return 'W';
+		}
+		return 'M';
+	}
+	return 0;
+}
+
+# check for type C by seeing if it responds to the status
+# command "s".  the mouse is at an unknown speed so we
+# have to check all possible speeds.
+C(ctl, data: ref Sys->FD): int
+{
+	buf := array[256] of byte;
+	for (s := speeds; len s > 0; s = s[1:]) {
+		if (debug) sys->print("%s\n", s[0]);
+		setupeia(ctl, s[0], "l8");
+		writes(data, "s");
+		c := slowread(data, buf, len buf, "check C: ");
+		if (c >= 1 && (int buf[0] & 16rbf) == 16r0f) {
+			sleep(100);
+			writes(data, "*n");
+			sleep(100);
+			setupeia(ctl, "b1200", "l8");
+			writes(data, "s");
+			c = slowread(data, buf, len buf, "recheck C: ");
+			if (c >= 1 && (int buf[0] & 16rbf) == 16r0f) {
+				writes(data, "U");
+				return 'C';
+			}
+		}
+		sleep(100);
+	}
+	return 0;
+}
+
+Cbaud(ctl, data: ref Sys->FD, baud: int)
+{
+	buf := array[2] of byte;
+	case baud {
+	0 or 1200 =>
+		return;
+	2400 =>
+		buf[1] = byte 'o';
+	4800 =>
+		buf[1] = byte 'p';
+	9600 =>
+		buf[1] = byte 'q';
+	* =>
+		fprint(stderr, "mouse: can't set baud rate, mouse at 1200\n");
+		return;
+	}
+	buf[0] = byte '*';
+	sleep(100);
+	write(data, buf, 2);
+	sleep(100);
+	write(data, buf, 2);
+	setupeia(ctl, sys->sprint("b%d", baud), "l8");
+}
+
+Wbaud(ctl, data: ref Sys->FD, baud: int)
+{
+	case baud {
+	0 or 1200 =>
+		return;
+	* =>
+		if (baud == 9600 && can9600)
+			break;
+		fprint(stderr, "mouse: can't set baud rate, mouse at 1200\n");
+		return;
+	}
+	writes(data, "*q");
+	setupeia(ctl, "b9600", "l8");
+	slowread(data, array[32] of byte, 32, "setbaud: ");
+}
+		
+readproc(fd: ref Sys->FD, buf: array of byte, n: int,
+				pidch: chan of int, ch: chan of (int, string))
+{
+	s: string;
+	pidch <-= sys->pctl(0, nil);
+	n = sys->read(fd, buf, n);
+	if (n < 0)
+		s = sys->sprint("read: %r");
+	ch <-= (n, s);
+}
+
+sleepproc(t: int, pidch: chan of int, ch: chan of (int, string))
+{
+	pidch <-= sys->pctl(0, nil);
+	sys->sleep(t);
+	ch <-= (-1, nil);
+}
+
+timedread(fd: ref Sys->FD, buf: array of byte, n: int, t: int): (int, string)
+{
+	pidch := chan of int;
+	retch := chan of (int, string);
+	spawn readproc(fd, buf, n, pidch, retch);
+	wpid := <-pidch;
+	spawn sleepproc(t, pidch, retch);
+	spid := <-pidch;
+
+	(nr, err) := <-retch;
+	if (nr == -1 && err == nil)
+		kill(wpid);
+	else
+		kill(spid);
+	return (nr, err);
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE)) == nil) {
+		fprint(stderr, "couldn't kill %d: %r\n", pid);
+		return;
+	}
+	sys->write(fd, array of byte "kill", 4);
+}
+
+writes(fd: ref Sys->FD, s: string): int
+{
+	a := array of byte s;
+	return write(fd, a, len a);
+}
+
--- /dev/null
+++ b/appl/cmd/mpc/mkfile
@@ -1,0 +1,14 @@
+<../../../mkconfig
+
+TARG=\
+	qconfig.dis\
+	qflash.dis\
+
+SYSMODULES=\
+	sys.m\
+	draw.m\
+	string.m\
+
+DISBIN=$ROOT/dis/mpc
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/mpc/qconfig.b
@@ -1,0 +1,193 @@
+implement Configflash;
+
+#
+# this isn't a proper config program: it's currently just
+# enough to set important parameters such as ethernet address.
+# an extension is in the works.
+# --chf
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+Configflash: module
+{
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+Region: adt {
+	base:	int;
+	limit:	int;
+};
+
+#
+# structure of allocation descriptor
+#
+Fcheck:	con 0;
+Fbase:	con 4;
+Flen:		con 8;
+Ftag:		con 11;
+Fsig:		con 12;
+Fasize:	con 3*4+3+1;
+
+Tdead:	con byte 0;
+Tboot:	con byte 16r01;
+Tconf:	con byte 16r02;
+Tnone:	con byte 16rFF;
+
+flashsig := array[] of {byte 16rF1, byte 16rA5, byte 16r5A, byte 16r1F};
+noval := array[] of {0 to 3 =>byte 16rFF};	# 
+
+Ctag, Cscreen, Cconsole, Cbaud, Cether, Cea, Cend: con iota;
+config := array[] of {
+	Ctag => "#plan9.ini\n",		# current flag for qboot, don't change
+	Cscreen => "vgasize=640x480x8\n",
+	Cconsole => "console=0 lcd\n",
+	Cbaud => "baud=9600\n",
+	Cether => "ether0=type=SCC port=2 ",	# note missing \n
+	Cea => "ea=08003e400080\n",
+	Cend => "\0"	# qboot currently requires it but shouldn't
+};
+
+Param: adt {
+	name:	string;
+	index:	int;
+};
+
+params := array[] of {
+	Param("vgasize", Cscreen),
+	Param("console", Cconsole),
+	Param("ea", Cea),
+	Param("baud", Cbaud)
+};
+
+# could come from file or #F/flash/flashctl
+FLASHSEG: con 256*1024;
+bootregion := Region(0, FLASHSEG);
+
+stderr: ref Sys->FD;
+prog := "qconfig";
+damaged := 0;
+debug := 0;
+
+usage()
+{
+	sys->fprint(stderr, "Usage: %s [-D] [-f flash] [-param value ...]\n", prog);
+	exit;
+}
+
+err(s: string)
+{
+	sys->fprint(stderr, "%s: %s", prog, s);
+	if(!damaged)
+		sys->fprint(stderr, "; flash not modified\n");
+	else
+		sys->fprint(stderr, "; flash might now be invalid\n");
+	exit;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+	if(args != nil){
+		prog = hd args;
+		args = tl args;
+	}
+	str = load String String->PATH;
+	if(str == nil)
+		err(sys->sprint("can't load %s: %r", String->PATH));
+	flash := "#F/flash/flash";
+	offset := 0;
+	region := bootregion;
+	
+	for(; args != nil && (hd args)[0] == '-'; args = tl args)
+		case a := hd args {
+		"-f" =>
+			(flash, args) = argf(tl args);
+		"-D" =>
+			debug = 1;
+		* =>
+			p := lookparam(params, a[1:]);
+			if(p.index < 0)
+				err(sys->sprint("unknown config parameter: %s", a));
+			v: string;
+			(v, args) = argf(tl args);
+			config[p.index] = a[1:]+"="+v+"\n";	# would be nice to check it
+		}
+	if(len args > 0)
+		usage();
+	out := sys->open(flash, Sys->ORDWR);
+	if(out == nil)
+		err(sys->sprint("can't open %s for read/write: %r", flash));
+	# TO DO: hunt for free space and add new entry
+	plonk(out, FLASHSEG-Fasize, mkdesc(0, 128*1024, Tboot));
+	c := flatten(config);
+	if(debug)
+		sys->print("%s", c);
+	bconf := array of byte c;
+	plonk(out, FLASHSEG-Fasize*2, mkdesc(128*1024, len bconf, Tconf));
+	plonk(out, 128*1024, bconf);
+}
+
+argf(args: list of string): (string, list of string)
+{
+	if(args == nil)
+		usage();
+	return (hd args, args);
+}
+
+lookparam(options: array of Param, s: string): Param
+{
+	for(i := 0; i < len options; i++)
+		if(options[i].name == s)
+			return options[i];
+	return Param(nil, -1);
+}
+
+flatten(a: array of string): string
+{
+	s := "";
+	for(i := 0; i < len a; i++)
+		s += a[i];
+	return s;
+}
+
+plonk(out: ref Sys->FD, where: int, val: array of byte)
+{
+	if(debug){
+		sys->print("write #%ux [%d]:", where, len val);
+		for(i:=0; i<len val; i++)
+			sys->print(" %.2ux", int val[i]);
+		sys->print("\n");
+	}
+	sys->seek(out, big where, 0);
+	if(sys->write(out, val, len val) != len val)
+		err(sys->sprint("bad flash write: %r"));
+}
+
+cvt(v: int): array of byte
+{
+	a := array[4] of byte;
+	a[0] = byte (v>>24);
+	a[1] = byte (v>>16);
+	a[2] = byte (v>>8);
+	a[3] = byte (v & 16rff);
+	return a;
+}
+
+mkdesc(base: int, length: int, tag: byte): array of byte
+{
+	a := array[Fasize] of byte;
+	a[Fcheck:] = noval;
+	a[Fbase:] = cvt(base);
+	a[Flen:] = cvt(length)[1:];	# it's three bytes
+	a[Ftag] = tag;
+	a[Fsig:] = flashsig;
+	return a;
+}
--- /dev/null
+++ b/appl/cmd/mpc/qflash.b
@@ -1,0 +1,188 @@
+implement Writeflash;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+Writeflash: module
+{
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+Region: adt {
+	base:	int;
+	limit:	int;
+};
+
+# could come from file or #F/flash/flashctl
+FLASHSEG: con 256*1024;
+kernelregion := Region(FLASHSEG, FLASHSEG+2*FLASHSEG);
+bootregion := Region(0, FLASHSEG);
+
+stderr: ref Sys->FD;
+prog := "qflash";
+damaged := 0;
+
+usage()
+{
+	sys->fprint(stderr, "Usage: %s [-b] [-o offset] [-f flashdev] file\n", prog);
+	exit;
+}
+
+err(s: string)
+{
+	sys->fprint(stderr, "%s: %s", prog, s);
+	if(!damaged)
+		sys->fprint(stderr, "; flash not modified\n");
+	else
+		sys->fprint(stderr, "; flash might now be invalid\n");
+	exit;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+	if(args != nil){
+		prog = hd args;
+		args = tl args;
+	}
+	str = load String String->PATH;
+	if(str == nil)
+		err(sys->sprint("can't load %s: %r", String->PATH));
+	region := kernelregion;
+	flash := "#F/flash/flash";
+	offset := 0;
+	save := 0;
+	
+	for(; args != nil && (hd args)[0] == '-'; args = tl args)
+		case hd args {
+		"-b" =>
+			region = bootregion;
+			offset = 16r100 - 8*4;	# size of exec header
+			save = 1;
+		"-h" =>
+			region.limit += FLASHSEG;
+		"-f" =>
+			if(tl args == nil)
+				usage();
+			flash = hd args;
+			args = tl args;
+		"-o" =>
+			if(tl args == nil)
+				usage();
+			args = tl args;
+			s := hd args;
+			v: int;
+			rs: string;
+			if(str->prefix("16r", s))
+				(v, rs) = str->toint(s[3:], 16);
+			else if(str->prefix("0x", s))
+				(v, rs) = str->toint(s[2:], 16);
+			else if(str->prefix("0", s))
+				(v, rs) = str->toint(s[1:], 8);
+			else
+				(v, rs) = str->toint(s, 10);
+			if(v < 0 || len rs != 0)
+				err(sys->sprint("bad offset: %s", s));
+			offset = v;
+		"-s" =>
+			save = 1;
+		* =>
+			usage();
+		}
+	if(args == nil)
+		usage();
+	fname := hd args;
+	fd := sys->open(fname, Sys->OREAD);
+	if(fd == nil)
+		err(sys->sprint("can't open %s: %r", fname));
+	(r, dir) := sys->fstat(fd);
+	if(r < 0)
+		err(sys->sprint("can't stat %s: %r", fname));
+	length := int dir.length;
+	avail := region.limit - (region.base+offset);
+	if(length > avail)
+		err(sys->sprint("%s contents %ud bytes, exceeds flash region %ud bytes", fname, length, avail));
+	# check fname's contents...
+	where := region.base+offset;
+	saved: list of (int, array of byte);
+	if(save){
+		saved = saveflash(flash, region.base, where) :: saved;
+		saved = saveflash(flash, where+length, region.limit) :: saved;
+	}
+	for(i := (region.base+offset)/FLASHSEG; i < region.limit/FLASHSEG; i++)
+		erase(flash, i);
+	out := sys->open(flash, Sys->OWRITE);
+	if(out == nil)
+		err(sys->sprint("can't open %s for writing: %r", flash));
+	if(sys->seek(out, big where, 0) != big where)
+		err(sys->sprint("can't seek to #%6.6ux on flash: %r", where));
+	if(length)
+		sys->print("writing %ud bytes to %s at #%6.6ux\n", length, flash, where);
+	buf := array[Sys->ATOMICIO] of byte;
+	total := 0;
+	while((n := sys->read(fd, buf, len buf)) > 0) {
+		if(total+n > avail)
+			err(sys->sprint("file %s too big for region of %ud bytes", fname, avail));
+		r = sys->write(out, buf, n);
+		damaged = 1;
+		if(r != n){
+			if(r < 0)
+				err(sys->sprint("error writing %s at byte %ud: %r", flash, total));
+			else
+				err(sys->sprint("short write on %s at byte %ud", flash, total));
+		}
+		total += n;
+	}
+	if(n < 0)
+		err(sys->sprint("error reading %s: %r", fname));
+	sys->print("wrote %ud bytes from %s to flash %s (#%6.6ux-#%6.6ux)\n", total, fname, flash, region.base, region.base+total);
+	for(l := saved; l != nil; l = tl l){
+		(addr, data) := hd l;
+		n = len data;
+		if(n == 0)
+			continue;
+		sys->print("restoring %ud bytes at #%6.6ux\n", n, addr);
+		if(sys->seek(out, big addr, 0) != big addr)
+			err(sys->sprint("can't seek to #%6.6ux on %s: %r", addr, flash));
+		r = sys->write(out, data, n);
+		if(r < 0)
+			err(sys->sprint("error writing %s: %r", flash));
+		else if(r != n)
+			err(sys->sprint("short write on %s at byte %ud/%ud", flash, r, n));
+		else
+			sys->print("restored %ud bytes at #%6.6ux\n", n, addr);
+	}
+}
+
+erase(flash: string, seg: int)
+{
+	ctl := sys->open(flash+"ctl", Sys->OWRITE);
+	if(ctl == nil)
+		err(sys->sprint("can't open %sctl: %r\n", flash));
+	if(sys->fprint(ctl, "erase %ud", seg*FLASHSEG) < 0)
+		err(sys->sprint("can't erase flash %s segment %d: %r\n", flash, seg));
+}
+
+saveflash(flash: string, base: int, limit: int): (int, array of byte)
+{
+	fd := sys->open(flash, Sys->OREAD);
+	if(fd == nil)
+		err(sys->sprint("can't open %s for reading: %r", flash));
+	nb := limit - base;
+	if(nb <= 0)
+		return (base, nil);
+	if(sys->seek(fd, big base, 0) != big base)
+		err(sys->sprint("can't seek to #%6.6ux to save flash contents: %r", base));
+	saved := array[nb] of byte;
+	if(sys->read(fd, saved, len saved) != len saved)
+		err(sys->sprint("can't read flash #%6.6ux to #%6.6ux: %r", base, limit));
+	sys->print("saved %ud bytes at #%6.6ux\n", len saved, base);
+	return (base, saved);
+}
--- /dev/null
+++ b/appl/cmd/mprof.b
@@ -1,0 +1,260 @@
+implement Prof;
+
+include "sys.m"; 
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+	arg: Arg;
+include "profile.m";
+	profile: Profile;
+include "sh.m";
+
+stderr: ref Sys->FD;
+
+Prof: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+	init0: fn(nil: ref Draw->Context, argv: list of string): Profile->Prof;
+};
+
+ignored(s: string)
+{
+	sys->fprint(stderr, "mprof: warning: %s ignored\n", s);
+}
+
+exits(e: string)
+{
+	if(profile != nil)
+		profile->end();
+	raise "fail:" + e;
+}
+
+pfatal(s: string)
+{
+	sys->fprint(stderr, "mprof: %s: %s\n", s, profile->lasterror());
+	exits("error");
+}
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "mprof: cannot load %s: %r\n", p);
+	exits("bad module");
+}
+
+usage(s: string)
+{
+	sys->fprint(stderr, "mprof: %s\n", s);
+	sys->fprint(stderr, "usage: mprof [-bcMflnve] [-m modname]... [cmd arg ...]\n");
+	exits("usage");
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	init0(ctxt, argv);
+}
+
+init0(ctxt: ref Draw->Context, argv: list of string): Profile->Prof
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	arg = load Arg Arg->PATH;
+	if(arg == nil)
+		badmodule(Arg->PATH);
+	arg->init(argv);
+	profile = load Profile Profile->PATH;
+	if(profile == nil)
+		badmodule(Profile->PATH);
+	if(profile->init() < 0)
+		pfatal("cannot initialize profile device");
+
+	v := 0;
+	begin := end := 0;
+	ep := 0;
+	wm := 0;
+	mem := 0;
+	exec, mods: list of string;
+	while((c := arg->opt()) != 0){
+		case c {
+			'b' => begin = 1;
+			'c' => end = 1;
+			'M' => v |= profile->MODULE;
+			'f' => v |= profile->FUNCTION;
+			'l' => v |= profile->LINE;
+			'n' => v |= profile->FULLHDR;
+			'v' => v |= profile->VERBOSE;
+			'm' =>
+				if((s := arg->arg()) == nil)
+					usage("missing module name");
+				mods = s :: mods;
+			'e' =>
+				ep = 1;
+			'g' =>
+				wm = 1;
+			'1' =>
+				mem |= Profile->MAIN;
+			'2' =>
+				mem |= Profile->HEAP;
+			'3' =>
+				mem |= Profile->IMAGE;
+			* => 
+				usage(sys->sprint("unknown option -%c", c));
+		}
+	}
+
+	exec = arg->argv();
+
+	if(begin && end)
+		ignored("-e option");
+	if((begin || end) && v != 0)
+		ignored("output format");
+	if(begin && exec != nil)
+		begin = 0;
+	if(begin == 0 && exec == nil){
+		if(mods != nil)
+			ignored("-m option");
+		mods = nil;
+	}
+	if(end){
+		if(mods != nil)
+			ignored("-m option");
+		if(ep || exec != nil)
+			ignored("command");
+		profile->end();
+		exit;
+	}
+	
+	for( ; mods != nil; mods = tl mods)
+		profile->profile(hd mods);
+
+	if(begin){
+		if(profile->memstart(mem) < 0)
+			pfatal("cannot start profiling");
+		exit;
+	}
+	r := 0;
+	if(exec != nil){
+		if(ep)
+			profile->profile(disname(hd exec));
+		if(profile->memstart(mem) < 0)
+			pfatal("cannot start profiling");
+		# r = run(ctxt, hd exec, exec);
+		wfd := openwait(sys->pctl(0, nil));
+		ci := chan of int;
+		spawn execute(ctxt, hd exec, exec, ci);
+		epid := <- ci;
+		wait(wfd, epid);
+	}
+	if(profile->stop() < 0)
+		pfatal("cannot stop profiling");
+	if(exec == nil || r >= 0){
+		modl := profile->memstats();
+		if(modl.mods == nil)
+			pfatal("no profile information");
+		if(wm){
+			if(exec == nil){
+				if(profile->memstart(mem) < 0)
+					pfatal("cannot restart profiling");
+			}
+			else
+				profile->end();
+			return modl;
+		}
+		if(!(v&(profile->MODULE|profile->FUNCTION|profile->LINE)))
+			v |= profile->MODULE|profile->LINE;
+		if(profile->memshow(modl, v) < 0)
+			pfatal("cannot show profile");
+		if(exec == nil){
+			if(profile->memstart(mem) < 0)
+				pfatal("cannot restart profiling");
+			exit;
+		}
+	}
+	profile->end();
+	return (nil, 0, nil);
+}
+
+disname(cmd: string): string
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	if(exists(file))
+		return file;
+	if(file[0]!='/' && file[0:2]!="./")
+		file = "/dis/"+file;
+	# if(exists(file))
+	#	return file;
+	return file;
+}
+
+execute(ctxt: ref Draw->Context, cmd : string, argl : list of string, ci: chan of int)
+{
+	ci <-= sys->pctl(Sys->FORKNS|Sys->NEWFD|Sys->NEWPGRP, 0 :: 1 :: 2 :: stderr.fd :: nil);
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(stderr, "mprof: %s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(ctxt, argl);
+}
+
+# run(ctxt: ref Draw->Context, cmd : string, argl : list of string): int
+# {
+# 	file := cmd;
+# 	if(len file<4 || file[len file-4:]!=".dis")
+# 		file += ".dis";
+# 	c := load Command file;
+# 	if(c == nil) {
+# 		err := sys->sprint("%r");
+# 		if(file[0]!='/' && file[0:2]!="./"){
+# 			c = load Command "/dis/"+file;
+# 			if(c == nil)
+# 				err = sys->sprint("%r");
+# 		}
+# 		if(c == nil){
+# 			sys->fprint(stderr, "mprof: %s: %s\n", cmd, err);
+# 			return -1;
+# 		}
+# 	}
+# 	c->init(ctxt, argl);
+# 	return 0;
+# }
+
+openwait(pid : int) : ref Sys->FD
+{
+	w := sys->sprint("#p/%d/wait", pid);
+	fd := sys->open(w, Sys->OREAD);
+	if (fd == nil)
+		pfatal("fd == nil in wait");
+	return fd;
+}
+
+wait(wfd : ref Sys->FD, wpid : int)
+{
+	n : int;
+
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;) {
+		if ((n = sys->read(wfd, buf, len buf)) < 0)
+			pfatal("bad read in wait");
+		status = string buf[0:n];
+		if (int status == wpid)
+			break;
+	}
+}
+
+exists(f: string): int
+{
+	return sys->open(f, Sys->OREAD) != nil;
+}
--- /dev/null
+++ b/appl/cmd/mv.b
@@ -1,0 +1,183 @@
+implement Mv;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+
+include "draw.m";
+	draw: Draw;
+
+include "string.m";
+	str: String;
+
+
+Mv: module 
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	str = load String String->PATH;
+	if(str == nil) {
+		sys->fprint(stderr, "mv: can't load %s: %r\n", String->PATH);
+		raise "fail:load";
+	}
+
+	dirto, dirfrom: Sys->Dir;
+	todir, toelem: string;
+	if(len argv<3) {
+		sys->fprint(stderr, "usage: mv fromfile tofile\n");
+		sys->fprint(stderr, "       mv fromfile ... todir\n");
+		raise "fail:usage";
+	}
+	argv = tl argv;
+	arr := array[len argv] of string;
+	for (i:=0; argv!=nil;i++){
+		arr[i]= hd argv;
+		argv = tl argv;
+	}
+	(i,dirto)=sys->stat(arr[len arr-1]);
+	if(i >= 0 && (dirto.mode&Sys->DMDIR)){
+		(i,dirfrom)=sys->stat(arr[0]);
+		if(len arr == 2 && i >= 0 && (dirfrom.mode&Sys->DMDIR))
+			(todir,toelem)=split(arr[len arr-1]);
+		else{
+			todir = arr[len arr -1];
+			toelem = "";	# toelem will be fromelem 
+		}
+	}else
+		(todir,toelem)=split(arr[len arr-1]);
+	if(len arr > 2  && toelem != nil) {
+		sys->fprint(stderr, "mv: %s not a directory\n", arr[len arr-1]);
+		raise "fail:error";
+	}
+	failed := 0;
+	for(i=0; i < len arr-1; i++)
+		if (mv(arr[i], todir, toelem) < 0)
+			failed++;
+	if(failed)
+		raise "fail:error";
+}
+
+mv(from,todir,toelem : string): int
+{
+	(i,dirb):=sys->stat(from);
+	if(i != 0) {
+		sys->fprint(stderr, "mv: can't stat %s: %r\n", from);
+		return -1;
+	}
+	(fromdir,fromelem):=split(from);
+	fromname:= fromdir+fromelem;
+	if(toelem == nil){
+		if (todir[len todir-1]!='/')
+			todir[len todir]='/';
+		toelem = fromelem;
+	}
+	i = len toelem;
+	if(i==0){
+		sys->fprint(stderr, "mv: null last name element moving %s\n", fromname);
+		return -1;
+	}
+	toname:=todir+toelem;
+	if(samefile(fromdir, todir)){
+		if(samefile(fromname, toname)){
+			sys->fprint(stderr, "mv: %s and %s are the same\n", fromname, toname);
+			return -1;
+		}
+		(j,dirt):=sys->stat(toname);
+		if( (j == 0) && (dirb.mode&Sys->DMDIR) ){
+			sys->fprint(stderr, "mv: can't rename a directory to an existing name\n");
+			return -1;
+		}
+		if(j == 0)
+			hardremove(toname);
+		dirt = sys->nulldir;
+		dirt.name=toelem;
+		if(sys->wstat(fromname,dirt) >= 0)
+			return 0;
+		if(dirb.mode&Sys->DMDIR){
+			sys->fprint(stderr, "mv: can't rename directory %s: %r\n", fromname);
+			return -1;
+		}
+	}
+	# Renaming won't work --- have to copy
+	if(dirb.mode&Sys->DMDIR){
+		sys->fprint(stderr, "mv: %s is a directory, not copied to %s\n", fromname, toname);
+		return -1;
+	}
+	fdf := sys->open(fromname, Sys->OREAD);
+	if(fdf==nil){
+		sys->fprint(stderr, "mv: can't open %s: %r\n", fromname);
+		return -1;
+	}
+	fdt := sys->create(toname, Sys->OWRITE, dirb.mode);
+	if(fdt == nil){
+		sys->fprint(stderr, "mv: can't create %s: %r\n", toname);
+		return -1;
+	}
+	if ((stat := copy1(fdf, fdt, fromname, toname)) != -1)
+		fdf = nil;	# temp bug: sometimes can't remove open file
+		if (sys->remove(fromname) < 0) {
+			sys->fprint(stderr, "mv: can't remove %s: %r\n", fromname);
+			return -1;
+		}
+	return stat;
+}
+
+
+copy1(fdf, fdt : ref Sys->FD,from, fto : string): int
+{
+	n : int;
+	buf:=array[Sys->ATOMICIO] of byte;
+	for(;;) {
+		n = sys->read(fdf, buf, len buf);
+		if (n<=0)
+			break;
+		n1 := sys->write(fdt, buf, n);
+		if(n1 != n) {
+			sys->fprint(stderr, "mv: error writing %s: %r\n", fto);
+			return -1;
+		}
+	}
+	if(n < 0) {
+		sys->fprint(stderr, "mv: error reading %s: %r\n", from);
+		return -1;
+	}
+	return 0;
+}
+
+split(name : string): (string,string)
+{
+	(d,t) := str->splitr(name, "/");
+	if(d!=nil)
+		return(d,t);
+	else if(name=="..")
+		return("../",".");
+	else
+		return("./",name);
+}
+
+samefile(a,b : string): int
+{
+	if(a==b) 
+		return 1;
+	(i,da):=sys->stat(a);
+	(j,db):=sys->stat(b);
+	if(i < 0 || j < 0)
+		return 0;
+	i= (da.qid.path==db.qid.path && da.qid.vers==db.qid.vers &&
+		da.dev==db.dev && da.dtype==db.dtype);
+	return i;
+}
+
+hardremove(a: string)
+{
+	if(sys->remove(a) == -1){
+		sys->fprint(stderr, "mv: can't remove %s: %r\n", a);
+		raise "fail:mv";
+	}
+	do; while(sys->remove(a) != -1);
+}
--- /dev/null
+++ b/appl/cmd/ndb/cs.b
@@ -1,0 +1,686 @@
+implement Cs;
+
+#
+# Connection server translates net!machine!service into
+# /net/tcp/clone 135.104.9.53!564
+#
+
+include "sys.m";
+	sys:	Sys;
+
+include "draw.m";
+
+include "srv.m";
+	srv: Srv;
+
+include "bufio.m";
+include "attrdb.m";
+	attrdb: Attrdb;
+	Attr, Db, Dbentry, Tuples: import attrdb;
+
+include "ip.m";
+	ip: IP;
+include "ipattr.m";
+	ipattr: IPattr;
+
+include "arg.m";
+
+Cs: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+# signature of dial-on-demand module
+CSdial: module
+{
+	init:	fn(nil: ref Draw->Context): string;
+	connect:	fn(): string;
+};
+
+Reply: adt
+{
+	fid:	int;
+	pid:	int;
+	addrs:	list of string;
+	err:	string;
+};
+
+Cached: adt
+{
+	expire:	int;
+	query:	string;
+	addrs:	list of string;
+};
+
+Ncache: con 16;
+cache:= array[Ncache] of ref Cached;
+nextcache := 0;
+
+rlist: list of ref Reply;
+
+ndbfile := "/lib/ndb/local";
+ndb: ref Db;
+mntpt := "/net";
+myname: string;
+
+stderr: ref Sys->FD;
+
+verbose := 0;
+dialmod: CSdial;
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	attrdb = load Attrdb Attrdb->PATH;
+	if(attrdb == nil)
+		cantload(Attrdb->PATH);
+	attrdb->init();
+	ip = load IP IP->PATH;
+	if(ip == nil)
+		cantload(IP->PATH);
+	ip->init();
+	ipattr = load IPattr IPattr->PATH;
+	if(ipattr == nil)
+		cantload(IPattr->PATH);
+	ipattr->init(attrdb, ip);
+
+	svcname := "#scs";
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		cantload(Arg->PATH);
+	arg->init(args);
+	arg->setusage("cs [-v] [-x mntpt] [-f database] [-d dialmod]");
+	while((c := arg->opt()) != 0)
+		case c {
+		'v' or 'D' =>
+			verbose++;
+		'd' =>	# undocumented hack to replace svc/cs/cs
+			f := arg->arg();
+			if(f != nil){
+				dialmod = load CSdial f;
+				if(dialmod == nil)
+					cantload(f);
+			}
+		'f' =>
+			ndbfile = arg->earg();
+		'x' =>
+			mntpt = arg->earg();
+			svcname = "#scs"+svcpt(mntpt);
+		* =>
+			arg->usage();
+		}
+
+	if(arg->argv() != nil)
+		arg->usage();
+	arg = nil;
+
+	srv = load Srv Srv->PATH;	# hosted Inferno only
+	if(srv != nil)
+		srv->init();
+
+	sys->remove(svcname+"/cs");
+	sys->unmount(svcname, mntpt);
+	publish(svcname);
+	if(sys->bind(svcname, mntpt, Sys->MBEFORE) < 0)
+		error(sys->sprint("can't bind #s on %s: %r", mntpt));
+	file := sys->file2chan(mntpt, "cs");
+	if(file == nil)
+		error(sys->sprint("can't make %s/cs: %r", mntpt));
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	refresh();
+	if(dialmod != nil){
+		e := dialmod->init(ctxt);
+		if(e != nil)
+			error(sys->sprint("can't initialise dial-on-demand: %s", e));
+	}
+	spawn cs(file);
+}
+
+svcpt(s: string): string
+{
+	for(i:=0; i<len s; i++)
+		if(s[i] == '/')
+			s[i] = '_';
+	return s;
+}
+
+publish(dir: string)
+{
+	d := Sys->nulldir;
+	d.mode = 8r777;
+	if(sys->wstat(dir, d) < 0)
+		sys->fprint(sys->fildes(2), "cs: can't publish %s: %r\n", dir);
+}
+
+cantload(m: string)
+{
+	error(sys->sprint("cannot load %s: %r", m));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "cs: %s\n", s);
+	raise "fail:error";
+}
+
+refresh()
+{
+	myname = sysname();
+	if(ndb == nil){
+		ndb2 := Db.open(ndbfile);
+		if(ndb2 == nil){
+			err := sys->sprint("%r");
+			ndb2 = Db.open("/lib/ndb/inferno");	# try to get service map at least
+			if(ndb2 == nil)
+				sys->fprint(sys->fildes(2), "cs: warning: can't open %s: %s\n", ndbfile, err);	# continue without it
+		}
+		ndb = Db.open(mntpt+"/ndb");
+		if(ndb != nil)
+			ndb = ndb.append(ndb2);
+		else
+			ndb = ndb2;
+	}else
+		ndb.reopen();
+}
+
+sysname(): string
+{
+	t := rf("/dev/sysname");
+	if(t != nil)
+		return t;
+	t = rf("#e/sysname");
+	if(t == nil){
+		s := rf(mntpt+"/ndb");
+		if(s != nil){
+			db := Db.sopen(s);
+			if(db != nil){
+				(e, nil) := db.find(nil, "sys");
+				if(e != nil)
+					t = e.findfirst("sys");
+			}
+		}
+	}
+	if(t != nil){
+		fd := sys->open("/dev/sysname", Sys->OWRITE);
+		if(fd != nil)
+			sys->fprint(fd, "%s", t);
+	}
+	return t;
+}
+
+rf(name: string): string
+{
+	fd := sys->open(name, Sys->OREAD);
+	buf := array[512] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+	return string buf[0:n];
+}
+
+cs(file: ref Sys->FileIO)
+{
+	pidc := chan of int;
+	donec := chan of ref Reply;
+	for (;;) {
+		alt {
+		(nil, buf, fid, wc) := <-file.write =>
+			cleanfid(fid);	# each write cancels previous requests
+			if(dialmod != nil){
+				e := dialmod->connect();
+				if(e != nil){
+					if(len e > 5 && e[0:5]=="fail:")
+						e = e[5:];
+					if(e == "")
+						e = "unknown error";
+					wc <-= (0, "cs: dial on demand: "+e);
+					break;
+				}
+			}
+			if(wc != nil){
+				nbytes := len buf;
+				query := string buf;
+				if(query == "refresh"){
+					refresh();
+					wc <-= (nbytes, nil);
+					break;
+				}
+				now := time();
+				r := ref Reply;
+				r.fid = fid;
+				spawn request(r, query, nbytes, now, wc, pidc, donec);
+				r.pid = <-pidc;
+				rlist = r :: rlist;
+			}
+
+		(off, nbytes, fid, rc) := <-file.read =>
+			if(rc != nil){
+				r := findfid(fid);
+				if(r != nil)
+					reply(r, off, nbytes, rc);
+				else
+					rc <-= (nil, "unknown request");
+			} else
+				;	# cleanfid(fid);		# compensate for csendq in file2chan
+
+		r := <-donec =>
+			r.pid = 0;
+		}
+	}
+}
+
+findfid(fid: int): ref Reply
+{
+	for(rl := rlist; rl != nil; rl = tl rl){
+		r := hd rl;
+		if(r.fid == fid)
+			return r;
+	}
+	return nil;
+}
+
+cleanfid(fid: int)
+{
+	rl := rlist;
+	rlist = nil;
+	for(; rl != nil; rl = tl rl){
+		r := hd rl;
+		if(r.fid != fid)
+			rlist = r :: rlist;
+		else
+			killgrp(r.pid);
+	}
+}
+
+killgrp(pid: int)
+{
+	if(pid != 0){
+		fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+		if(fd == nil || sys->fprint(fd, "killgrp") < 0)
+			sys->fprint(stderr, "cs: can't killgrp %d: %r\n", pid);
+	}
+}
+
+request(r: ref Reply, query: string, nbytes: int, now: int, wc: chan of (int, string), pidc: chan of int, donec: chan of ref Reply)
+{
+	pidc <-= sys->pctl(Sys->NEWPGRP, nil);
+	if(query != nil && query[0] == '!'){
+		# general query
+		(r.addrs, r.err) = genquery(query[1:]);
+	}else{
+		(r.addrs, r.err) = xlate(query, now);
+		if(r.addrs == nil && r.err == nil)
+			r.err = "cs: can't translate address";
+	}
+	if(r.err != nil){
+		if(verbose)
+			sys->fprint(stderr, "cs: %s: %s\n", query, r.err);
+		wc <-= (0, r.err);
+	} else
+		wc <-= (nbytes, nil);
+	donec <-= r;
+}
+
+reply(r: ref Reply, off: int, nbytes: int, rc: chan of (array of byte, string))
+{
+	if(r.err != nil){
+		rc <-= (nil, r.err);
+		return;
+	}
+	addr: string = nil;
+	if(r.addrs != nil){
+		addr = hd r.addrs;
+		r.addrs = tl r.addrs;
+	}
+	off = 0;	# this version ignores offset
+	rc <-= reads(addr, off, nbytes);
+}
+
+#
+# return the file2chan reply for a read of the given string
+#
+reads(str: string, off, nbytes: int): (array of byte, string)
+{
+	bstr := array of byte str;
+	slen := len bstr;
+	if(off < 0 || off >= slen)
+		return (nil, nil);
+	if(off + nbytes > slen)
+		nbytes = slen - off;
+	if(nbytes <= 0)
+		return (nil, nil);
+	return (bstr[off:off+nbytes], nil);
+}
+
+lookcache(query: string, now: int): ref Cached
+{
+	for(i:=0; i<len cache; i++){
+		c := cache[i];
+		if(c != nil && c.query == query && now < c.expire){
+			if(verbose)
+				sys->print("cache: %s -> %s\n", query, hd c.addrs);
+			return c;
+		}
+	}
+	return nil;
+}
+
+putcache(query: string, addrs: list of string, now: int)
+{
+	ce := ref Cached;
+	ce.expire = now+120;
+	ce.query = query;
+	ce.addrs = addrs;
+	cache[nextcache] = ce;
+	nextcache = (nextcache+1)%Ncache;
+}
+
+xlate(address: string, now: int): (list of string, string)
+{
+	n: int;
+	l, rl, results: list of string;
+	repl, netw, mach, service: string;
+
+	ce := lookcache(address, now);
+	if(ce != nil && ce.addrs != nil)
+		return (ce.addrs, nil);
+
+	(n, l) = sys->tokenize(address, "!\n");
+	if(n < 2)
+		return (nil, "bad format request");
+
+	netw = hd l;
+	if(netw == "net")
+		netw = "tcp";	# TO DO: better (needs lib/ndb)
+	if(!isnetwork(netw))
+		return (nil, "network unavailable "+netw);
+	l = tl l;
+
+	if(!isipnet(netw)) {
+		repl = mntpt + "/" + netw + "/clone ";
+		for(;;){
+			repl += hd l;
+			if((l = tl l) == nil)
+				break;
+			repl += "!";
+		}
+		return (repl :: nil, nil);	# no need to cache
+	}
+
+	if(n != 3)
+		return (nil, "bad format request");
+	mach = hd l;
+	service = hd tl l;
+
+	if(!isnumeric(service)) {
+		s := xlatesvc(netw, service);
+		if(s == nil){
+			if(srv != nil)
+				s = srv->ipn2p(netw, service);
+			if(s == nil)
+				return (nil, "cs: can't translate service");
+		}
+		service = s;
+	}
+
+	attr := ipattr->dbattr(mach);
+	if(mach == "*")
+		l = "" :: nil;
+	else if(attr != "ip") {
+		# Symbolic server == "$SVC"
+		if(mach[0] == '$' && len mach > 1 && ndb != nil){
+			(s, nil) := ipattr->findnetattr(ndb, "sys", myname, mach[1:]);
+			if(s == nil){
+				names := dblook("infernosite", "", mach[1:]);
+				if(names == nil)
+					return (nil, "cs: can't translate "+mach);
+				s = hd names;
+			}
+			mach = s;
+			attr = ipattr->dbattr(mach);
+		}
+		if(attr == "sys"){
+			results = dblook("sys", mach, "ip");
+			if(results != nil)
+				attr = "ip";
+		}
+		if(attr != "ip"){
+			err: string;
+			(results, err) = querydns(mach, "ip");
+			if(err != nil)
+				return (nil, err);
+		}else if(results == nil)
+			results = mach :: nil;
+		l = results;
+		if(l == nil){
+			if(srv != nil)
+				l = srv->iph2a(mach);
+			if(l == nil)
+				return (nil, "cs: unknown host");
+		}
+	} else
+		l = mach :: nil;
+
+	while(l != nil) {
+		s := hd l;
+		l = tl l;
+		dnetw := netw;
+		if(s != nil){
+			(divert, err) := ipattr->findnetattr(ndb, "ip", s, "divert-"+netw);
+			if(err == nil && divert != nil){
+				dnetw = divert;
+				if(!isnetwork(dnetw))
+					return (nil, "network unavailable "+dnetw);	# XXX should only give up if all addresses fail?
+			}
+		}
+
+		if(s != "")
+			s[len s] = '!';
+		s += service;
+
+		repl = mntpt+"/"+dnetw+"/clone "+s;
+		if(verbose)
+			sys->fprint(stderr, "cs: %s!%s!%s -> %s\n", netw, mach, service, repl);
+
+		rl = repl :: rl;
+	}
+	rl = reverse(rl);
+	putcache(address, rl, now);
+	return (rl, nil);
+}
+
+querydns(name: string, rtype: string): (list of string, string)
+{
+	fd := sys->open(mntpt+"/dns", Sys->ORDWR);
+	if(fd == nil)
+		return (nil, nil);
+	if(sys->fprint(fd, "%s %s", name, rtype) < 0)
+		return (nil, sys->sprint("%r"));
+	rl: list of string;
+	buf := array[256] of byte;
+	sys->seek(fd, big 0, 0);
+	while((n := sys->read(fd, buf, len buf)) > 0){
+		# name rtype value
+		(nf, fld) := sys->tokenize(string buf[0:n], " \t");
+		if(nf != 3){
+			sys->fprint(stderr, "cs: odd result from dns: %s\n", string buf[0:n]);
+			continue;
+		}
+		rl = hd tl tl fld :: rl;
+	}
+	return (reverse(rl), nil);
+}
+
+dblook(attr: string, val: string, rattr: string): list of string
+{
+	rl: list of string;
+	ptr: ref Attrdb->Dbptr;
+	for(;;){
+		e: ref Dbentry;
+		(e, ptr) = ndb.findbyattr(ptr, attr, val, rattr);
+		if(e == nil)
+			break;
+		for(l := e.findbyattr(attr, val, rattr); l != nil; l = tl l){
+			(nil, al) := hd l;
+			for(; al != nil; al = tl al)
+				if(!inlist((hd al).val, rl))
+					rl = (hd al).val :: rl;
+		}
+	}
+	return reverse(rl);
+}
+
+inlist(s: string, l: list of string): int
+{
+	for(; l != nil; l = tl l)
+		if(hd l == s)
+			return 1;
+	return 0;
+}
+
+reverse(l: list of string): list of string
+{
+	t: list of string;
+	for(; l != nil; l = tl l)
+		t = hd l :: t;
+	return t;
+}
+
+isnumeric(a: string): int
+{
+	i, c: int;
+
+	for(i = 0; i < len a; i++) {
+		c = a[i];
+		if(c < '0' || c > '9')
+			return 0;
+	}
+	return 1;
+}
+
+nets: list of string;
+
+isnetwork(s: string) : int
+{
+	if(find(s, nets))
+		return 1;
+	(ok, nil) := sys->stat(mntpt+"/"+s+"/clone");
+	if(ok >= 0) {
+		nets = s :: nets;
+		return 1;
+	}
+	return 0;
+}
+
+find(e: string, l: list of string) : int
+{
+	for(; l != nil; l = tl l)
+		if (e == hd l)
+			return 1;
+	return 0;
+}
+
+isipnet(s: string) : int
+{
+	return s == "net" || s == "tcp" || s == "udp" || s == "il";
+}
+
+xlatesvc(proto: string, s: string): string
+{
+	if(ndb == nil || s == nil || isnumeric(s))
+		return s;
+	(e, nil) := ndb.findbyattr(nil, proto, s, "port");
+	if(e == nil)
+		return nil;
+	matches := e.findbyattr(proto, s, "port");
+	if(matches == nil)
+		return nil;
+	(ts, al) := hd matches;
+	restricted := "";
+	if(ts.hasattr("restricted"))
+		restricted = "!r";
+	if(verbose > 1)
+		sys->print("%s=%q port=%s%s\n", proto, s, (hd al).val, restricted);
+	return (hd al).val+restricted;
+}
+
+time(): int
+{
+	timefd := sys->open("/dev/time", Sys->OREAD);
+	if(timefd == nil)
+		return 0;
+	buf := array[128] of byte;
+	sys->seek(timefd, big 0, 0);
+	n := sys->read(timefd, buf, len buf);
+	if(n < 0)
+		return 0;
+	return int ((big string buf[0:n]) / big 1000000);
+}
+
+#
+# general query: attr1=val1 attr2=val2 ... finds matching tuple(s)
+#	where attr1 is the key and val1 can't be *
+#
+genquery(query: string): (list of string, string)
+{
+	(tups, err) := attrdb->parseline(query, 0);
+	if(err != nil)
+		return (nil, "bad query: "+err);
+	if(tups == nil)
+		return (nil, "bad query");
+	pairs := tups.pairs;
+	a0 := (hd pairs).attr;
+	if(a0 == "ipinfo")
+		return (nil, "ipinfo not yet supported");
+	v0 := (hd pairs).val;
+
+	# if((a0 == "dom" || a0 == "ip") && v0 != nil){
+	# 	query dns ...
+	# }
+
+	ptr: ref Attrdb->Dbptr;
+	e: ref Dbentry;
+	for(;;){
+		(e, ptr) = ndb.findpair(ptr, a0, v0);
+		if(e == nil)
+			break;
+		for(l := e.lines; l != nil; l = tl l)
+			if(qmatch(hd l, tl pairs)){
+				ls: list of string;
+				for(l = e.lines; l != nil; l = tl l)
+					ls = tuptext(hd l) :: ls;
+				return (reverse(ls), nil);
+			}
+	}
+	return  (nil, "no match");
+}
+
+#
+# see if set of tuples t contains every non-* attr/val pair
+#
+qmatch(t: ref Tuples, av: list of ref Attr): int
+{
+Match:
+	for(; av != nil; av = tl av){
+		a := hd av;
+		for(pl := t.pairs; pl != nil; pl = tl pl)
+			if((hd pl).attr == a.attr &&
+			    (a.val == "*" || a.val == (hd pl).val))
+				continue Match;
+		return 0;
+	}
+	return 1;
+}
+
+tuptext(t: ref Tuples): string
+{
+	s: string;
+	for(pl := t.pairs; pl != nil; pl = tl pl){
+		p := hd pl;
+		if(s != nil)
+			s[len s] = ' ';
+		s += sys->sprint("%s=%q", p.attr, p.val);
+	}
+	return s;
+}
--- /dev/null
+++ b/appl/cmd/ndb/csquery.b
@@ -1,0 +1,97 @@
+implement Csquery;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+
+Csquery: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: csquery [-x /net] [-s server] [address ...]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		cantload(Bufio->PATH);
+
+	net := "/net";
+	server: string;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		cantload(Arg->PATH);
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		'x' =>
+			net = arg->arg();
+			if(net == nil)
+				usage();
+		's' =>
+			server = arg->arg();
+			if(server == nil)
+				usage();
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(server == nil)
+		server = net+"/cs";
+	if(args != nil){
+		for(; args != nil; args = tl args)
+			csquery(server, hd args);
+	}else{
+		f := bufio->fopen(sys->fildes(0), Sys->OREAD);
+		if(f == nil)
+			exit;
+		for(;;){
+			sys->print("> ");
+			s := f.gets('\n');
+			if(s == nil)
+				break;
+			csquery(server, s[0:len s-1]);
+		}
+	}
+}
+
+cantload(s: string)
+{
+	sys->fprint(sys->fildes(2), "csquery: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+csquery(server: string, addr: string)
+{
+	cs := sys->open(server, Sys->ORDWR);
+	if(cs == nil){
+		sys->fprint(sys->fildes(2), "csquery: can't open %s: %r\n", server);
+		raise "fail:open";
+	}
+	stdout := sys->fildes(1);
+	b := array of byte addr;
+	if(sys->write(cs, b, len b) > 0){
+		sys->seek(cs, big 0, Sys->SEEKSTART);
+		buf := array[256] of byte;
+		while((n := sys->read(cs, buf, len buf)) > 0)
+			sys->print("%s\n", string buf[0:n]);
+		if(n == 0)
+			return;
+	}
+	sys->print("%s: %r\n", addr);
+}
--- /dev/null
+++ b/appl/cmd/ndb/dns.b
@@ -1,0 +1,1873 @@
+implement DNS;
+
+#
+# domain name service
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# RFCs: 1034, 1035, 2181, 2308
+#
+# TO DO:
+#	server side:
+#		database; inmyzone; ptr generation; separate zone transfer
+#	currently doesn't implement loony rules on case
+#	limit work
+#	check data
+#	Call
+#	ipv6
+#
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+
+include "draw.m";
+
+include "bufio.m";
+
+include "srv.m";
+	srv: Srv;
+
+include "ip.m";
+	ip: IP;
+	IPaddrlen, IPaddr, IPv4off, Udphdrlen, Udpraddr, Udpladdr, Udprport, Udplport: import ip;
+
+include "arg.m";
+
+include "attrdb.m";
+	attrdb: Attrdb;
+	Db, Dbentry, Tuples: import attrdb;
+
+include "ipattr.m";
+	ipattr: IPattr;
+	dbattr: import ipattr;
+
+include "keyring.m";
+include "security.m";
+	random: Random;
+
+include "dial.m";
+	dial: Dial;
+
+DNS: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Reply: adt
+{
+	fid:	int;
+	pid:	int;
+	query:	string;
+	attr:	string;
+	addrs:	list of string;
+	err:	string;
+};
+
+rlist: list of ref Reply;
+
+dnsfile := "/lib/ndb/local";
+myname: string;
+mntpt := "/net";
+DNSport: con 53;
+debug := 0;
+referdns := 0;
+usehost := 1;
+now: int;
+
+servers: list of string;
+
+# domain name from dns/db
+domain: string;
+dnsdomains: list of string;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		cantload(Arg->PATH);
+	dial = load Dial Dial->PATH;
+	if(dial == nil)
+		cantload(Dial->PATH);
+	arg->init(args);
+	arg->setusage("dns [-Drh] [-f dnsfile] [-x mntpt]");
+	svcname := "#sdns";
+	while((c := arg->opt()) != 0)
+		case c {
+		'D' =>
+			debug = 1;
+		'f' =>	
+			dnsfile = arg->earg();
+		'h' =>
+			usehost = 0;
+		'r' =>
+			referdns = 1;
+		'x' =>
+			mntpt = arg->earg();
+			svcname = "#sdns"+svcpt(mntpt);
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(args != nil)
+		arg->usage();
+	arg = nil;
+
+	if(usehost){
+		srv = load Srv Srv->PATH;	# hosted Inferno only
+		if(srv != nil)
+			srv->init();
+	}
+	ip = load IP IP->PATH;
+	if(ip == nil)
+		cantload(IP->PATH);
+	ip->init();
+	attrdb = load Attrdb Attrdb->PATH;
+	if(attrdb == nil)
+		cantload(Attrdb->PATH);
+	attrdb->init();
+	ipattr = load IPattr IPattr->PATH;
+	if(ipattr == nil)
+		cantload(IPattr->PATH);
+	ipattr->init(attrdb, ip);
+
+	sys->pctl(Sys->NEWPGRP | Sys->FORKFD, nil);
+
+	random = load Random Random->PATH;
+	if(random == nil)
+		cantload(Random->PATH);
+	dnsid = random->randomint(Random->ReallyRandom);	# avoid clashes
+	random = nil;
+	myname = sysname();
+	stderr = sys->fildes(2);
+	readservers();
+	now = time();
+	sys->remove(svcname+"/dns");
+	sys->unmount(svcname, mntpt);
+	publish(svcname);
+	if(sys->bind(svcname, mntpt, Sys->MBEFORE) < 0)
+		error(sys->sprint("can't bind #s on %s: %r", mntpt));
+	file := sys->file2chan(mntpt, "dns");
+	if(file == nil)
+		error(sys->sprint("can't make %s/dns: %r", mntpt));
+	sync := chan of int;
+	spawn dnscache(sync);
+	<-sync;
+	spawn dns(file);
+}
+
+publish(dir: string)
+{
+	d := Sys->nulldir;
+	d.mode = 8r777;
+	if(sys->wstat(dir, d) < 0)
+		sys->fprint(sys->fildes(2), "cs: can't publish %s: %r\n", dir);
+}
+
+svcpt(s: string): string
+{
+	for(i:=0; i<len s; i++)
+		if(s[i] == '/')
+			s[i] = '_';
+	return s;
+}
+
+cantload(s: string)
+{
+	error(sys->sprint("can't load %s: %r", s));
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "dns: %s\n", s);
+	raise "fail:error";
+}
+
+dns(file: ref Sys->FileIO)
+{
+	pidc := chan of int;
+	donec := chan of ref Reply;
+	for(;;){
+		alt {
+		(nil, buf, fid, wc) := <-file.write =>
+			now = time();
+			cleanfid(fid);	# each write cancels previous requests
+			if(wc != nil){
+				r := ref Reply;
+				r.fid = fid;
+				spawn request(r, buf, wc, pidc, donec);
+				r.pid = <-pidc;
+				rlist = r :: rlist;
+			}
+
+		(off, nbytes, fid, rc) := <-file.read =>
+			now = time();
+			if(rc != nil){
+				r := findfid(fid);
+				if(r != nil)
+					reply(r, off, nbytes, rc);
+				else
+					rc <-= (nil, "unknown request");
+			}
+
+		r := <-donec =>
+			now = time();
+			r.pid = 0;
+			if(r.err != nil)
+				cleanfid(r.fid);
+		}
+	}
+}
+
+findfid(fid: int): ref Reply
+{
+	for(rl := rlist; rl != nil; rl = tl rl){
+		r := hd rl;
+		if(r.fid == fid)
+			return r;
+	}
+	return nil;
+}
+
+cleanfid(fid: int)
+{
+	rl := rlist;
+	rlist = nil;
+	for(; rl != nil; rl = tl rl){
+		r := hd rl;
+		if(r.fid != fid)
+			rlist = r :: rlist;
+		else
+			killgrp(r.pid);
+	}
+}
+
+killgrp(pid: int)
+{
+	if(pid != 0){
+		fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+		if(fd == nil || sys->fprint(fd, "killgrp") < 0)
+			sys->fprint(stderr, "dns: can't killgrp %d: %r\n", pid);
+	}
+}
+
+request(r: ref Reply, data: array of byte, wc: chan of (int, string), pidc: chan of int, donec: chan of ref Reply)
+{
+	pidc <-= sys->pctl(Sys->NEWPGRP, nil);
+	query := string data;
+	for(i := 0; i < len query; i++)
+		if(query[i] == ' ')
+			break;
+	r.query = query[0:i];
+	for(; i < len query && query[i] == ' '; i++)
+		;
+	r.attr = query[i:];
+	attr := rrtype(r.attr);
+	if(attr < 0)
+		r.err = "unknown type";
+	else
+		(r.addrs, r.err) = dnslookup(r.query, attr);
+	if(r.addrs == nil && r.err == nil)
+		r.err = "not found";
+	if(r.err != nil){
+		if(debug)
+			sys->fprint(stderr, "dns: %s: %s\n", query, r.err);
+		wc <-= (0, "dns: "+r.err);
+	} else
+		wc <-= (len data, nil);
+	donec <-= r;
+}
+
+reply(r: ref Reply, off: int, nbytes: int, rc: chan of (array of byte, string))
+{
+	if(r.err != nil || r.addrs == nil){
+		rc <-= (nil, r.err);
+		return;
+	}
+	addr: string;
+	if(r.addrs != nil){
+		addr = hd r.addrs;
+		r.addrs = tl r.addrs;
+	}
+	off = 0;	# this version ignores offsets
+#	rc <-= reads(r.query+" "+r.attr+" "+addr, off, nbytes);
+	rc <-= reads(addr, off, nbytes);
+}
+
+#
+# return the file2chan reply for a read of the given string
+#
+reads(str: string, off, nbytes: int): (array of byte, string)
+{
+	bstr := array of byte str;
+	slen := len bstr;
+	if(off < 0 || off >= slen)
+		return (nil, nil);
+	if(off + nbytes > slen)
+		nbytes = slen - off;
+	if(nbytes <= 0)
+		return (nil, nil);
+	return (bstr[off:off+nbytes], nil);
+}
+
+sysname(): string
+{
+	t := rf("/dev/sysname");
+	if(t != nil)
+		return t;
+	t = rf("#e/sysname");
+	if(t == nil){
+		s := rf(mntpt+"/ndb");
+		if(s != nil){
+			db := Db.sopen(t);
+			if(db != nil){
+				(e, nil) := db.find(nil, "sys");
+				if(e != nil)
+					t = e.findfirst("sys");
+			}
+		}
+	}
+	if(t != nil){
+		fd := sys->open("/dev/sysname", Sys->OWRITE);
+		if(fd != nil)
+			sys->fprint(fd, "%s", t);
+	}
+	return t;
+}
+
+rf(name: string): string
+{
+	fd := sys->open(name, Sys->OREAD);
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+	return string buf[0:n];
+}
+
+samefile(d1, d2: Sys->Dir): int
+{
+	# ``it was black ... it was white!  it was dark ...  it was light! ah yes, i remember it well...''
+	return d1.dev==d2.dev && d1.dtype==d2.dtype &&
+			d1.qid.path==d2.qid.path && d1.qid.vers==d2.qid.vers &&
+			d1.mtime==d2.mtime;
+}
+
+#
+# database
+#	dnsdomain=	suffix to add to unqualified unrooted names
+#	dns=			dns server to try
+#	dom=		domain name
+#	ip=			IP address
+#	ns=			name server
+#	soa=
+#	soa=delegated
+#	infernosite=	set of site-wide parameters
+#
+
+#
+# basic Domain Name Service resolver
+#
+
+laststat := 0;	# time last stat'd (to reduce churn)
+dnsdb: ref Db;
+
+readservers(): list of string
+{
+	if(laststat != 0 && now < laststat+2*60)
+		return servers;
+	laststat = now;
+	if(dnsdb == nil){
+		db := Db.open(dnsfile);
+		if(db == nil){
+			sys->fprint(stderr, "dns: can't open %s: %r\n", dnsfile);
+			return nil;
+		}
+		dyndb := Db.open(mntpt+"/ndb");
+		if(dyndb != nil)
+			dnsdb = dyndb.append(db);
+		else
+			dnsdb = db;
+	}else{
+		if(!dnsdb.changed())
+			return servers;
+		dnsdb.reopen();
+	}
+	if((l := dblooknet("sys", myname, "dnsdomain")) == nil)
+		l = dblook("infernosite", "", "dnsdomain");
+	dnsdomains = "" :: l;
+	if((l = dblooknet("sys", myname, "dns")) == nil)
+		l = dblook("infernosite", "", "dns");
+	servers = l;
+#	zones := dblook("soa", "", "dom");
+#printlist("zones", zones);
+	if(debug)
+		printlist("dnsdomains", dnsdomains);
+	if(debug)
+		printlist("servers", servers);
+	return servers;
+}
+
+printlist(w: string, l: list of string)
+{
+	sys->print("%s:", w);
+	for(; l != nil; l = tl l)
+		sys->print(" %q", hd l);
+	sys->print("\n");
+}
+
+dblookns(dom: string): list of ref RR
+{
+	domns := dblook("dom", dom, "ns");
+	hosts: list of ref RR;
+	for(; domns != nil; domns = tl domns){
+		s := hd domns;
+		if(debug)
+			sys->print("dns db: dom=%s ns=%s\n", dom, s);
+		ipl: list of ref RR = nil;
+		addrs := dblook("dom", s, "ip");
+		for(; addrs != nil; addrs = tl addrs){
+			a := parseip(hd addrs);
+			if(a != nil){
+				ipl = ref RR.A(s, Ta, Cin, now+60, 0, a) :: ipl;
+				if(debug)
+					sys->print("dom=%s ip=%s\n", s, hd addrs);
+			}
+		}
+		if(ipl != nil){
+			# only use ones for which we've got addresses
+			cachec <-= (ipl, 0);
+			hosts = ref RR.Host(dom, Tns, Cin, now+60, 0, s) :: hosts;
+		}
+	}
+	if(hosts == nil){
+		if(debug)
+			sys->print("dns: no ns for dom=%s in db\n", dom);
+		return nil;
+	}
+	cachec <-= (hosts, 0);
+	cachec <-= Sync;
+	return hosts;
+}
+
+defaultresolvers(): list of ref NS
+{
+	resolvers := readservers();
+	al: list of ref RR;
+	for(; resolvers != nil; resolvers = tl resolvers){
+		nm := hd resolvers;
+		a := parseip(nm);
+		if(a == nil){
+			# try looking it up as a domain name with an ip address
+			for(addrs := dblook("dom", nm, "ip"); addrs != nil; addrs = tl addrs){
+				a = parseip(hd addrs);
+				if(a != nil)
+					al = ref RR.A("defaultns", Ta, Cin, now+60, 0, a) :: al;
+			}
+		}else
+			al = ref RR.A("defaultns", Ta, Cin, now+60, 0, a) :: al;
+	}
+	if(al == nil){
+		if(debug)
+			sys->print("dns: no default resolvers\n");
+		return nil;
+	}
+	return ref NS("defaultns", al, 1, now+60) :: nil;
+}
+
+dblook(attr: string, val: string, rattr: string): list of string
+{
+	rl: list of string;
+	ptr: ref Attrdb->Dbptr;
+	for(;;){
+		e: ref Dbentry;
+		(e, ptr) = dnsdb.findbyattr(ptr, attr, val, rattr);
+		if(e == nil)
+			break;
+		for(l := e.findbyattr(attr, val, rattr); l != nil; l = tl l){
+			(nil, al) := hd l;
+			for(; al != nil; al = tl al)
+				if(!inlist((hd al).val, rl))
+					rl = (hd al).val :: rl;
+		}
+	}
+	return reverse(rl);
+}
+
+#
+# starting from the ip= associated with attr=val, search over all
+# containing networks for the nearest values of rattr
+#
+dblooknet(attr: string, val: string, rattr: string): list of string
+{
+#sys->print("dblooknet: %s=%s -> %s\n", attr, val, rattr);
+	(results, nil) := ipattr->findnetattrs(dnsdb, attr, val, rattr::nil);
+	rl: list of string;
+	for(; results != nil; results = tl results){
+		(nil, nattrs) := hd results;
+		for(; nattrs != nil; nattrs = tl nattrs){
+			na := hd nattrs;
+			if(na.name == rattr){
+				for(pairs := na.pairs; pairs != nil; pairs = tl pairs)
+					if((s := (hd pairs).val) != nil && !inlist(s, rl))
+						rl = s :: rl;
+			}
+		}
+	}
+	if(rl == nil)
+		return dblook(attr, val, rattr);
+	return reverse(rl);
+}
+
+inlist(s: string, l: list of string): int
+{
+	for(; l != nil; l = tl l)
+		if(hd l == s)
+			return 1;
+	return 0;
+}
+
+reverse[T](l: list of T): list of T
+{
+	r: list of T;
+	for(; l != nil; l = tl l)
+		r = hd l :: r;
+	return r;
+}
+
+append(h: list of string, s: string): list of string
+{
+	if(h == nil)
+		return s :: nil;
+	return hd h :: append(tl h, s);
+}
+
+#
+# subset of RR types
+#
+Ta: con 1;
+Tns: con 2;
+Tcname: con 5;
+Tsoa: con 6;
+Tmb: con 7;
+Tptr: con 12;
+Thinfo: con 13;
+Tmx: con 15;
+Tall: con 255;
+
+#
+# classes
+#
+Cin: con 1;
+Call: con 255;
+
+#
+# opcodes
+#
+Oquery: con 0<<11;	# normal query
+Oinverse: con 1<<11;	# inverse query
+Ostatus:	con 2<<11;	# status request
+Omask:	con 16rF<<11;	# mask for opcode
+
+#
+# response codes
+#
+Rok:	con 0;
+Rformat:	con 1;	# format error
+Rserver:	con 2;	# server failure
+Rname:	con 3;	# bad name
+Runimplemented: con 4;	# unimplemented operation
+Rrefused:	con 5;	# permission denied, not supported
+Rmask:	con 16rF;	# mask for response
+
+#
+# other flags in opcode
+#
+Fresp:	con 1<<15;	# message is a response
+Fauth:	con 1<<10;	# true if an authoritative response
+Ftrunc:	con 1<<9;		# truncated message
+Frecurse:	con 1<<8;		# request recursion
+Fcanrecurse:	con 1<<7;	# server can recurse
+
+QR: adt {
+	name: string;
+	rtype: int;
+	class: int;
+
+	text:	fn(q: self ref QR): string;
+};
+
+RR: adt {
+	name: string;
+	rtype: int;
+	class: int;
+	ttl: int;
+	flags:	int;
+	pick {
+	Error =>
+		reason:	string;	# cached negative
+	Host =>
+		host:	string;
+	Hinfo =>
+		cpu:	string;
+		os:	string;
+	Mx =>
+		pref:	int;
+		host:	string;
+	Soa =>
+		soa:	ref SOA;
+	A or
+	Other =>
+		rdata:	array of byte;
+	}
+
+	islive:	fn(r: self ref RR): int;
+	outlives:	fn(a: self ref RR, b: ref RR): int;
+	match:	fn(a: self ref RR, b: ref RR): int;
+	text:	fn(a: self ref RR): string;
+};
+
+SOA: adt {
+	mname:	string;
+	rname:	string;
+	serial:	int;
+	refresh:	int;
+	retry:	int;
+	expire:	int;
+	minttl:	int;
+
+	text:	fn(nil: self ref SOA): string;
+};
+
+DNSmsg: adt {
+	id: 	int;
+	flags:	int;
+	qd: list of ref QR;
+	an: list of ref RR;
+	ns: list of ref RR;
+	ar: list of ref RR;
+	err: string;
+
+	pack:	fn(m: self ref DNSmsg, hdrlen: int): array of byte;
+	unpack:	fn(a: array of byte): ref DNSmsg;
+	text:	fn(m: self ref DNSmsg): string;
+};
+
+NM: adt {
+	name:	string;
+	rr:	list of ref RR;
+	stats:	ref Stats;
+};
+
+Stats: adt {
+	rtt:	int;
+};
+
+cachec: chan  of (list of ref RR, int);
+cache: array of list of ref NM;
+Sync: con (nil, 0);	# empty list sent to ensure that last cache update done
+
+hash(s: string): array of list of ref NM
+{
+	h := 0;
+	for(i:=0; i<len s; i++){	# hashpjw
+		c := s[i];
+		if(c >= 'A' && c <= 'Z')
+			c += 'a'-'A';
+		h = (h<<4) + c;
+		if((g := h & int 16rF0000000) != 0)
+			h ^= ((g>>24) & 16rFF) | g;
+	}
+	return cache[(h&~(1<<31))%len cache:];
+}
+
+lower(s: string): string
+{
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c >= 'A' && c <= 'Z'){
+			n := s;
+			for(; i < len n; i++){
+				c = n[i];
+				if(c >= 'A' && c <= 'Z')
+					n[i] = c+('a'-'A');
+			}
+			return n;
+		}
+	}
+	return s;
+}
+
+#
+# split rrl into a list of those RRs that match rr and a list of those that don't
+#
+partrrl(rr: ref RR, rrl: list of ref RR): (list of ref RR, list of ref RR)
+{
+	m: list of ref RR;
+	nm: list of ref RR;
+	name := lower(rr.name);
+	for(; rrl != nil; rrl = tl rrl){
+		t := hd rrl;
+		if(t.rtype == rr.rtype && t.class == rr.class &&
+		   (t.name == name || lower(t.name) == name))
+			m = t :: m;
+		else
+			nm = t :: nm;
+	}
+	return (m, nm);
+}
+
+copyrrl(rrl: list of ref RR): list of ref RR
+{
+	nl: list of ref RR;
+	for(; rrl != nil; rrl = tl rrl)
+		nl = ref *hd rrl :: nl;
+#	return revrrl(rrl);
+	return rrl;	# probably don't care about order
+}
+
+dnscache(sync: chan of int)
+{
+	cache = array[32] of list of ref NM;
+	cachec = chan of (list of ref RR, int);
+	sync <-= sys->pctl(0, nil);
+	for(;;){
+		(rrl, flags) := <-cachec;
+		#now = time();
+	  List:
+		while(rrl != nil){
+			rrset: list of ref RR;
+			(rrset, rrl) = partrrl(hd rrl, rrl);
+			rr := hd rrset;
+			rr.flags = flags;
+			name := lower(rr.name);
+			hb := hash(name);
+			for(ces := hb[0]; ces != nil; ces = tl ces){
+				ce := hd ces;
+				if(ce.name == name){
+					rr.name = ce.name;	# share string
+					x := ce.rr;
+					ce.rr = insertrrset(ce.rr, rr, rrset);
+					if(x != ce.rr && debug)
+						sys->print("insertrr %s:%s\n", name, rrsettext(rrset));
+					continue List;
+				}
+			}
+			if(debug)
+				sys->print("newrr %s:%s\n", name, rrsettext(rrset));
+			hb[0] = ref NM(name, rrset, nil) :: hb[0];
+		}
+	}
+}
+
+lookcache(name: string, rtype: int, rclass: int): (list of ref RR, string)
+{
+	results: list of ref RR;
+	name = lower(name);
+	for(ces := hash(name)[0]; ces != nil; ces = tl ces){
+		ce := hd ces;
+		if(ce.name == name){
+			for(zl := ce.rr; zl != nil; zl = tl zl){
+				r := hd zl;
+				if((r.rtype == rtype || r.rtype == Tall || rtype == Tall) && r.class == rclass && r.name == name && r.islive()){
+					pick ar := r {
+					Error =>
+						if(rtype != Tall || ar.reason != "resource does not exist"){
+							if(debug)
+								sys->print("lookcache: %s[%s]: !%s\n", name, rrtypename(rtype), ar.reason);
+							return (nil, ar.reason);
+						}
+					* =>
+						results = ref *r :: results;
+					}
+				}
+			}
+		}
+	}
+	if(debug)
+		sys->print("lookcache: %s[%s]: %s\n", name, rrtypename(rtype), rrsettext(results));
+	return (results, nil);
+}
+
+#
+# insert RRset new in existing list of RRsets rrl
+# if that's desirable (it's the whole RRset or nothing, see rfc2181)
+#
+insertrrset(rrl: list of ref RR, rr: ref RR, new: list of ref RR): list of ref RR
+{
+	# TO DO: expire entries
+	match := 0;
+	for(l := rrl; l != nil; l = tl l){
+		orr := hd l;
+		if(orr.rtype == rr.rtype && orr.class == rr.class){	# name already known to match
+			match = 1;
+			if(!orr.islive())
+				break;	# prefer new, unexpired data
+			if(tagof rr == tagof RR.Error && tagof orr != tagof RR.Error)
+				return rrl;	# prefer unexpired positive
+			if(rr.flags & Fauth)
+				break;	# prefer newly-arrived authoritative data
+			if(orr.flags & Fauth)
+				return rrl;		# prefer authoritative data
+			if(orr.outlives(rr))
+				return rrl;		# prefer longer-lived data
+		}
+	}
+	if(match){
+		# strip out existing RR set
+		l = rrl;
+		rrl = nil;
+		for(; l != nil; l = tl l){
+			orr := hd l;
+			if((orr.rtype != rr.rtype || orr.class != rr.class) && orr.islive()){
+				rrl = orr :: rrl;}
+		}
+	}
+	# add new RR set
+	for(; new != nil; new = tl new){
+		nrr := hd new;
+		nrr.name = rr.name;
+		rrl = nrr :: rrl;
+	}
+	return rrl;
+}
+
+rrsettext(rrl: list of ref RR): string
+{
+	s := "";
+	for(; rrl != nil; rrl = tl rrl)
+		s += " ["+(hd rrl).text()+"]";
+	return s;
+}
+
+QR.text(qr: self ref QR): string
+{
+	s := sys->sprint("%s %s", qr.name, rrtypename(qr.rtype));
+	if(qr.class != Cin)
+		s += sys->sprint(" [c=%d]", qr.class);
+	return s;
+}
+
+RR.islive(rr: self ref RR): int
+{
+	return rr.ttl >= now;
+}
+
+RR.outlives(a: self ref RR, b: ref RR): int
+{
+	return a.ttl > b.ttl;
+}
+
+RR.match(a: self ref RR, b: ref RR): int
+{
+	# compare content, not ttl
+	return a.rtype == b.rtype && a.class == b.class && a.name == b.name;
+}
+
+RR.text(rr: self ref RR): string
+{
+	s := sys->sprint("%s %s", rr.name, rrtypename(rr.rtype));
+	pick ar := rr {
+	Host =>
+		s += sys->sprint("\t%s", ar.host);
+	Hinfo =>
+		s += sys->sprint("\t%s %s", ar.cpu, ar.os);
+	Mx =>
+		s += sys->sprint("\t%ud %s", ar.pref, ar.host);
+	Soa =>
+		s += sys->sprint("\t%s", ar.soa.text());
+	A =>
+		if(len ar.rdata == 4){
+			a := ar.rdata;
+			s += sys->sprint("\t%d.%d.%d.%d", int a[0], int a[1], int a[2], int a[3]);
+		}
+	Error =>
+		s += sys->sprint("\t!%s", ar.reason);
+	}
+	return s;
+}
+
+SOA.text(soa: self ref SOA): string
+{
+	return sys->sprint("%s %s %ud %ud %ud %ud %ud", soa.mname, soa.rname,
+			soa.serial, soa.refresh, soa.retry, soa.expire, soa.minttl);
+}
+
+NS: adt {
+	name:	string;
+	addr:	list of ref RR;
+	canrecur:	int;
+	ttl:	int;
+};
+
+dnslookup(name: string, attr: int): (list of string, string)
+{
+	case attr {
+	Ta =>
+		case dbattr(name) {
+		"sys" =>
+			# could apply domains
+			;
+		"dom" =>
+			;
+		* =>
+			return (nil, "invalid host name");
+		}
+		if(srv != nil){	# try the host's map first
+			l := srv->iph2a(name);
+			if(l != nil)
+				return (fullresult(name, "ip", l), nil);
+		}
+	Tptr =>
+		if(srv != nil){	# try host's map first
+			l := srv->ipa2h(arpa2addr(name));
+			if(l != nil)
+				return (fullresult(name, "ptr", l), nil);
+		}
+	}
+	return dnslookup1(name, attr);
+}
+
+fullresult(name: string, attr: string, l: list of string): list of string
+{
+	rl: list of string;
+	for(; l != nil; l = tl l)
+		rl = sys->sprint("%s %s\t%s", name, attr, hd l) :: rl;
+	return reverse(rl);
+}
+
+arpa2addr(a: string): string
+{
+	(nil, flds) := sys->tokenize(a, ".");
+	rl: list of string;
+	for(; flds != nil && lower(s := hd flds) != "in-addr"; flds = tl flds)
+		rl = s :: rl;
+	dom: string;
+	for(; rl != nil; rl = tl rl){
+		if(dom != nil)
+			dom[len dom] = '.';
+		dom += hd rl;
+	}
+	return dom;
+}
+
+dnslookup1(label: string, attr: int): (list of string, string)
+{
+	(rrl, err) := fulldnsquery(label, attr, 0);
+	if(err != nil || rrl == nil)
+		return (nil, err);
+	r: list of string;
+	for(; rrl != nil; rrl = tl rrl)
+		r = (hd rrl).text() :: r;
+	return (reverse(r), nil);
+}
+
+trimdot(s: string): string
+{
+	while(s != nil && s[len s - 1] == '.')
+		s = s[0:len s -1];
+	return s;
+}
+
+parent(s: string): string
+{
+	if(s == "")
+		return ".";
+	for(i := 0; i < len s; i++)
+		if(s[i] == '.')
+			return s[i+1:];
+	return "";
+}
+
+rootservers(): list of ref NS
+{
+	slist := ref NS("a.root-servers.net",
+		ref RR.A("a.root-servers.net", Ta, Cin, 1<<31, 0,
+			array[] of {byte 198, byte 41, byte 0, byte 4})::nil, 0, 1<<31) :: nil;
+	return slist;
+}
+
+#
+# this broadly follows the algorithm given in RFC 1034
+# as adjusted and qualified by several other RFCs.
+# `label' is 1034's SNAME, `attr' is `STYPE'
+#
+# TO DO:
+#	keep statistics for name servers
+
+fulldnsquery(label: string, attr: int, depth: int): (list of ref RR, string)
+{
+	slist: list of ref NS;
+	fd: ref Sys->FD;
+	if(depth > 10)
+		return (nil, "dns loop");
+	ncname := 0;
+Step1:
+	for(tries:=0; tries<10; tries++){
+
+		# 1. see if in local information, and if so, return it
+		(x, err) := lookcache(label, attr, Cin);
+		if(x != nil)
+			return (x, nil);
+		if(err != nil)
+			return (nil, err);
+		if(attr != Tcname){
+			if(++ncname > 10)
+				return (nil, "cname alias loop");
+			(x, err) = lookcache(label, Tcname, Cin);
+			if(x != nil){
+				pick rx := hd x {
+				Host =>
+					label  = rx.host;
+					continue;
+				}
+			}
+		}
+
+		# 2. find the best servers to ask
+		slist = nil;
+		for(d := trimdot(label); d != "."; d = parent(d)){
+			nsl: list of ref RR;
+			(nsl, err) = lookcache(d, Tns, Cin);
+			if(nsl == nil)
+				nsl = dblookns(d);
+			# add each to slist; put ones with known addresses first
+			known: list of ref NS = nil;
+			for(; nsl != nil; nsl = tl nsl){
+				pick ns := hd nsl {
+				Host =>
+					(addrs, err2) := lookcache(ns.host, Ta, Cin);
+					if(addrs != nil)
+						known = ref NS(ns.host, addrs, 0, 1<<31) :: known;
+					else if(err2 == nil)
+						slist = ref NS(ns.host, nil, 0, 1<<31) :: slist;
+				}
+					
+			}
+			for(; known != nil; known = tl known)
+				slist = hd known :: slist;
+			if(slist != nil)
+				break;
+		}
+		# if no servers, resort to safety belt
+		if(slist == nil){
+			slist = defaultresolvers();
+			if(slist == nil){
+				slist = rootservers();
+				if(slist == nil)
+					return (nil, "no dns servers configured");
+			}
+		}
+		(id, query, err1) := mkquery(attr, Cin, label);
+		if(err1 != nil){
+			sys->fprint(stderr, "dns: %s\n", err1);
+			return (nil, err1);
+		}
+
+		if(debug)
+			printnslist(sys->sprint("ns for %s: ", d), slist);
+
+		# 3. send them queries until one returns a response
+		for(qset := slist; qset != nil; qset = tl qset){
+			ns := hd qset;
+			if(ns.addr == nil){
+				if(debug)
+					sys->print("recursive[%d] query for %s address\n", depth+1, ns.name);
+				(ns.addr, nil) = fulldnsquery(ns.name, Ta, depth+1);
+				if(ns.addr == nil)
+					continue;
+			}
+			if(fd == nil){
+				fd = udpport();
+				if(fd == nil)
+					return (nil, sys->sprint("%r"));
+			}
+			(dm, err2) := udpquery(fd, id, query, ns.name, hd ns.addr);
+			if(dm == nil){
+				sys->fprint(stderr, "dns: %s: %s\n", ns.name, err2);
+				# TO DO: remove from slist
+				continue;
+			}
+			# 4. analyse the response
+			#	a. answers the question or has Rname, cache it and return to client
+			#	b. delegation to other NS? cache and goto step 2.
+			#	c. if response is CNAME and QTYPE!=CNAME change SNAME to the
+			#		canonical name (data) of the CNAME RR and goto step 1.
+			#	d. if response is server failure or otherwise odd, delete server from SLIST
+			#		and goto step 3.
+			auth := (dm.flags & Fauth) != 0;
+			soa: ref RR.Soa;
+			(soa, dm.ns) = soaof(dm.ns);
+			if((dm.flags & Rmask) != Rok){
+				# don't repeat the request on an error
+				#  TO DO: should return `best error'
+				if(tl qset != nil && ((dm.flags & Rmask) != Rname || !auth))
+					continue;
+				cause := reason(dm.flags & Rmask);
+				if(auth && soa != nil){
+					# rfc2038 says to cache soa with cached negatives, and the
+					# negative to be retrieved for all attributes if name does not exist
+					if((ttl := soa.soa.minttl) > 0)
+						ttl += now;
+					else
+						ttl = now+10*60;
+					a := attr;
+					if((dm.flags & Rmask) == Rname)
+						a = Tall;
+					cachec <-= (ref RR.Error(label, a, Cin, ttl, auth, cause)::soa::nil, auth);
+				}
+				return (nil, cause);
+			}
+			if(dm.an != nil){
+				if(1 && dm.ns != nil)
+					cachec <-= (dm.ns, 0);
+				if(1 && dm.ar != nil)
+					cachec <-= (dm.ar, 0);
+				cachec <-= (dm.an, auth);
+				cachec <-= Sync;
+				if(isresponse(dm, attr))
+					return (dm.an, nil);
+				if(attr != Tcname && (cn := cnameof(dm)) != nil){
+					if(++ncname > 10)
+						return (nil, "cname alias loop");
+					label = cn;
+					continue Step1;
+				}
+			}
+			if(auth){
+				if(soa != nil && (ttl := soa.soa.minttl) > 0)
+					ttl += now;
+				else
+					ttl = now+10*60;
+				if(soa != nil)
+					l := soa :: nil;
+				cachec <-= (ref RR.Error(label, attr, Cin, ttl, auth, "resource does not exist")::l, auth);
+				return (nil, "resource does not exist");
+			}
+			if(isdelegation(dm)){
+				# cache valid name servers and hints
+				cachec <-= (dm.ns, 0);
+				if(dm.ar != nil)
+					cachec <-= (dm.ar, 0);
+				cachec <-= Sync;
+				continue Step1;
+			}
+		}
+	}
+	return (nil, "server failed");
+}
+
+isresponse(dn: ref DNSmsg, attr: int): int
+{
+	if(dn == nil || dn.an == nil)
+		return 0;
+	return (hd dn.an).rtype == attr;
+}
+
+cnameof(dn: ref DNSmsg): string
+{
+	if(dn != nil && dn.an != nil && (rr := hd dn.an).rtype == Tcname)
+		pick ar := rr {
+		Host =>
+			return ar.host;
+		}
+	return nil;
+}
+
+soaof(rrl: list of ref RR): (ref RR.Soa, list of ref RR)
+{
+	for(l := rrl; l != nil; l = tl l)
+		pick rr := hd l {
+		Soa =>
+			rest := tl l;
+			for(; rrl != l; rrl = tl rrl)
+				if(tagof hd rrl != tagof RR.Soa)	# (just in case)
+					rest = hd rrl :: rest;
+			return (rr, rest);
+		}
+	return (nil, rrl);
+}
+
+isdelegation(dn: ref DNSmsg): int
+{
+	if(dn.an != nil)
+		return 0;
+	for(al := dn.ns; al != nil; al = tl al)
+		if((hd al).rtype == Tns)
+			return 1;
+	return 0;
+}
+
+printnslist(prefix: string, nsl: list of ref NS)
+{
+	s := prefix;
+	for(; nsl != nil; nsl = tl nsl){
+		ns := hd nsl;
+		s += sys->sprint(" [%s %s]", ns.name, rrsettext(ns.addr));
+	}
+	sys->print("%s\n", s);
+}
+
+#
+# DNS message format
+#
+
+Udpdnslim: con 512;
+
+Labels: adt {
+	names:	list of (string, int);
+
+	new:	fn(): ref Labels;
+	look:	fn(labs: self ref Labels, s: string): int;
+	install:	fn(labs: self ref Labels, s: string, o: int);
+};
+
+Labels.new(): ref Labels
+{
+	return ref Labels;
+}
+
+Labels.look(labs: self ref Labels, s: string): int
+{
+	for(nl := labs.names; nl != nil; nl = tl nl){
+		(t, o) := hd nl;
+		if(s == t)
+			return 16rC000 | o;
+	}
+	return 0;
+}
+
+Labels.install(labs: self ref Labels, s: string, off: int)
+{
+	labs.names = (s, off) :: labs.names;
+}
+
+put2(a: array of byte, o: int, val: int): int
+{
+	if(o < 0)
+		return o;
+	if(o + 2 > len a)
+		return -o;
+	a[o] = byte (val>>8);
+	a[o+1] = byte val;
+	return o+2;
+}
+
+put4(a: array of byte, o: int, val: int): int
+{
+	if(o < 0)
+		return o;
+	if(o + 4 > len a)
+		return -o;
+	a[o] = byte (val>>24);
+	a[o+1] = byte (val>>16);
+	a[o+2] = byte (val>>8);
+	a[o+3] = byte val;
+	return o+4;
+}
+
+puta(a: array of byte, o: int, b: array of byte): int
+{
+	if(o < 0)
+		return o;
+	l := len b;
+	if(l > 255 || o+l+1 > len a)
+		return -(o+l+1);
+	a[o++] = byte l;
+	a[o:] = b;
+	return o+len b;
+}
+
+puts(a: array of byte, o: int, s: string): int
+{
+	return puta(a, o, array of byte s);
+}
+
+get2(a: array of byte, o: int): (int, int)
+{
+	if(o < 0)
+		return (0, o);
+	if(o + 2 > len a)
+		return (0, -o);
+	val := (int a[o] << 8) | int a[o+1];
+	return (val, o+2);
+}
+
+get4(a: array of byte, o: int): (int, int)
+{
+	if(o < 0)
+		return (0, o);
+	if(o + 4 > len a)
+		return (0, -o);
+	val := (((((int a[o] << 8)| int a[o+1]) << 8) | int a[o+2]) << 8) | int a[o+3];
+	return (val, o+4);
+}
+
+gets(a: array of byte, o: int): (string, int)
+{
+	if(o < 0)
+		return (nil, o);
+	if(o+1 > len a)
+		return (nil, -o);
+	l := int a[o++];
+	if(o+l > len a)
+		return (nil, -o);
+	return (string a[o:o+l], o+l);
+}
+
+putdn(a: array of byte, o: int, name: string, labs: ref Labels): int
+{
+	if(o < 0)
+		return o;
+	o0 := o;
+	while(name != "") {
+		n := labs.look(name);
+		if(n != 0){
+			o = put2(a, o, n);
+			if(o < 0)
+				return -o0;
+			return o;
+		}
+		for(l := 0; l < len name && name[l] != '.'; l++)
+			;
+		if(o+l+1 > len a)
+			return -o0;
+		labs.install(name, o);
+		a[o++] = byte l;
+		for(i := 0; i < l; i++)
+			a[o++] = byte name[i];
+		for(; l < len name && name[l] == '.'; l++)
+			;
+		name = name[l:];
+	}
+	if(o >= len a)
+		return -o0;
+	a[o++] = byte 0;
+	return o;
+}
+
+getdn(a: array of byte, o: int, depth: int): (string, int)
+{
+	if(depth > 30)
+		return (nil, -o);
+	if(o < 0)
+		return (nil, o);
+	name := "";
+	while(o < len a && (l := int a[o++]) != 0) {
+		if((l & 16rC0) == 16rC0) {		# pointer
+			if(o >= len a)
+				return (nil, -o);
+			po := ((l & 16r3F)<<8) | int a[o];
+			if(po >= len a)
+				return ("", -o);
+			o++;
+			pname: string;
+			(pname, po) = getdn(a, po, depth+1);
+			if(po < 1)
+				return (nil, -o);
+			name += pname;
+			break;
+		}
+		if((l & 16rC0) != 0)
+			return (nil, -o);	# format error
+		if(o + l > len a)
+			return (nil, -o);
+		name += string a[o:o+l];
+		o += l;
+		if(o < len a && a[o] != byte 0)
+			name += ".";
+	}
+	return (lower(name), o);
+}
+
+putqrl(a: array of byte, o: int, qrl: list of ref QR, labs: ref Labels): int
+{
+	for(; qrl != nil && o >= 0; qrl = tl qrl){
+		q := hd qrl;
+		o = putdn(a, o, q.name, labs);
+		o = put2(a, o, q.rtype);
+		o = put2(a, o, q.class);
+	}
+	return o;
+}
+
+getqrl(nq: int, a: array of byte, o: int): (list of ref QR, int)
+{
+	if(o < 0)
+		return (nil, o);
+	qrl: list of ref QR;
+	for(i := 0; i < nq; i++) {
+		qd := ref QR;
+		(qd.name, o) = getdn(a, o, 0);
+		(qd.rtype, o) = get2(a, o);
+		(qd.class, o) = get2(a, o);
+		if(o < 1)
+			break;
+		qrl = qd :: qrl;
+	}
+	q: list of ref QR;
+	for(; qrl != nil; qrl = tl qrl)
+		q = hd qrl :: q;
+	return (q, o);
+}
+
+putrrl(a: array of byte, o: int, rrl: list of ref RR, labs: ref Labels): int
+{
+	if(o < 0)
+		return o;
+	for(; rrl != nil; rrl = tl rrl){
+		rr := hd rrl;
+		o0 := o;
+		o = putdn(a, o, rr.name, labs);
+		o = put2(a, o, rr.rtype);
+		o = put2(a, o, rr.class);
+		o = put4(a, o, rr.ttl);
+		pick ar := rr {
+		Host =>
+			o = putdn(a, o, ar.host, labs);
+		Hinfo =>
+			o = puts(a, o, ar.cpu);
+			o = puts(a, o, ar.os);
+		Mx =>
+			o = put2(a, o, ar.pref);
+			o = putdn(a, o, ar.host, labs);
+		Soa =>
+			soa := ar.soa;
+			o = putdn(a, o, soa.mname, labs);
+			o = putdn(a, o, soa.rname, labs);
+			o = put4(a, o, soa.serial);
+			o = put4(a, o, soa.refresh);
+			o = put4(a, o, soa.retry);
+			o = put4(a, o, soa.expire);
+			o = put4(a, o, soa.minttl);
+		A or
+		Other =>
+			dlen := len ar.rdata;
+			o = put2(a, o, dlen);
+			if(o < 1)
+				return -o0;
+			if(o + dlen > len a)
+				return -o0;
+			a[o:] = ar.rdata;
+			o += dlen;
+		}
+	}
+	return o;
+}
+
+getrrl(nr: int, a: array of byte, o: int): (list of ref RR, int)
+{
+	if(o < 0)
+		return (nil, o);
+	rrl: list of ref RR;
+	for(i := 0; i < nr; i++) {
+		name: string;
+		rtype, rclass, ttl: int;
+		(name, o) = getdn(a, o, 0);
+		(rtype, o) = get2(a, o);
+		(rclass, o) = get2(a, o);
+		(ttl, o) = get4(a, o);
+		if(ttl <= 0)
+			ttl = 0;
+		#ttl = 1*60;
+		ttl += now;
+		dlen: int;
+		(dlen, o) = get2(a, o);
+		if(o < 1)
+			return (rrl, o);
+		if(o+dlen > len a)
+			return (rrl, -(o+dlen));
+		rr: ref RR;
+		dname: string;
+		case rtype {
+		Tsoa =>
+			soa := ref SOA;
+			(soa.mname, o) = getdn(a, o, 0);
+			(soa.rname, o) = getdn(a, o, 0);
+			(soa.serial, o) = get4(a, o);
+			(soa.refresh, o) = get4(a, o);
+			(soa.retry, o) = get4(a, o);
+			(soa.expire, o) = get4(a, o);
+			(soa.minttl, o) = get4(a, o);
+			rr = ref RR.Soa(name, rtype, rclass, ttl, 0, soa);
+		Thinfo =>
+			cpu, os: string;
+			(cpu, o) = gets(a, o);
+			(os, o) = gets(a, o);
+			rr = ref RR.Hinfo(name, rtype, rclass, ttl, 0, cpu, os);
+		Tmx =>
+			pref: int;
+			host: string;
+			(pref, o) = get2(a, o);
+			(host, o) = getdn(a, o, 0);
+			rr = ref RR.Mx(name, rtype, rclass, ttl, 0, pref, host);
+		Tcname or
+		Tns or
+		Tptr =>
+			(dname, o) = getdn(a, o, 0);
+			rr = ref RR.Host(name, rtype, rclass, ttl, 0, dname);
+		Ta =>
+			rdata := array[dlen] of byte;
+			rdata[0:] = a[o:o+dlen];
+			rr = ref RR.A(name, rtype, rclass, ttl, 0, rdata);
+			o += dlen;
+		* =>
+			rdata := array[dlen] of byte;
+			rdata[0:] = a[o:o+dlen];
+			rr = ref RR.Other(name, rtype, rclass, ttl, 0, rdata);
+			o += dlen;
+		}
+		rrl = rr :: rrl;
+	}
+	r: list of ref RR;
+	for(; rrl != nil; rrl = tl rrl)
+		r = (hd rrl) :: r;
+	return (r, o);
+}
+
+DNSmsg.pack(msg: self ref DNSmsg, hdrlen: int): array of byte
+{
+	a := array[Udpdnslim+hdrlen] of byte;
+
+	l := hdrlen;
+	l = put2(a, l, msg.id);
+	l = put2(a, l, msg.flags);
+	l = put2(a, l, len msg.qd);
+	l = put2(a, l, len msg.an);
+	l = put2(a, l, len msg.ns);
+	l = put2(a, l, len msg.ar);
+	labs := Labels.new();
+	l = putqrl(a, l, msg.qd, labs);
+	l = putrrl(a, l, msg.an, labs);
+	l = putrrl(a, l, msg.ns, labs);
+	l = putrrl(a, l, msg.ar, labs);
+	if(l < 1)
+		return nil;
+	return a[0:l];
+}
+
+DNSmsg.unpack(a: array of byte): ref DNSmsg
+{
+	msg := ref DNSmsg;
+	msg.flags = Rformat;
+	l := 0;
+	(msg.id, l) = get2(a, l);
+	(msg.flags, l) = get2(a, l);
+	if(l < 0 || l > len a){
+		msg.err = "length error";
+		return msg;
+	}
+	if(l >= len a)
+		return msg;
+
+	nqd, nan, nns, nar: int;
+	(nqd, l) = get2(a, l);
+	(nan, l) = get2(a, l);
+	(nns, l) = get2(a, l);
+	(nar, l) = get2(a, l);
+	if(l >= len a)
+		return msg;
+	(msg.qd, l) = getqrl(nqd, a, l);
+	(msg.an, l) = getrrl(nan, a, l);
+	(msg.ns, l) = getrrl(nns, a, l);
+	(msg.ar, l) = getrrl(nar, a, l);
+	if(l < 1){
+		sys->fprint(stderr, "l=%d format error\n", l);
+		msg.err = "format error";
+		return msg;
+	}
+	return msg;
+}
+
+DNSmsg.text(msg: self ref DNSmsg): string
+{
+	s := sys->sprint("id=%ud flags=#%ux[%s]\n", msg.id, msg.flags, flagtext(msg.flags));
+	s += "  QR:\n";
+	for(x := msg.qd; x != nil; x = tl x)
+		s += "\t"+(hd x).text()+"\n";
+	s += "  AN:\n";
+	for(l := msg.an; l != nil; l = tl l)
+		s += "\t"+(hd l).text()+"\n";
+	s += "  NS:\n";
+	for(l = msg.ns; l != nil; l = tl l)
+		s += "\t"+(hd l).text()+"\n";
+	s += "  AR:\n";
+	for(l = msg.ar; l != nil; l = tl l)
+		s += "\t"+(hd l).text()+"\n";
+	return s;
+}
+
+flagtext(f: int): string
+{
+	s := "";
+	if(f & Fresp)
+		s += "R";
+	if(f & Fauth)
+		s += "A";
+	if(f & Ftrunc)
+		s += "T";
+	if(f & Frecurse)
+		s += "r";
+	if(f & Fcanrecurse)
+		s += "c";
+	if((f & Fresp) == 0)
+		return s;
+	if(s != "")
+		s += ",";
+	return s+reason(f & Rmask);
+}
+
+rcodes := array[] of {
+	Rok => "no error",
+	Rformat => "format error",
+	Rserver => "server failure",
+	Rname => "name does not exist",
+	Runimplemented => "unimplemented",
+	Rrefused => "refused",
+};
+
+reason(n: int): string
+{
+	if(n < 0 || n > len rcodes)
+		return sys->sprint("error %d", n);
+	return rcodes[n];
+}
+
+rrtype(s: string): int
+{
+	case s {
+	"ip" => return Ta;
+	"ns" => return Tns;
+	"cname" => return Tcname;
+	"soa" => return Tsoa;
+	"ptr" => return Tptr;
+	"mx" => return Tmx;
+	"hinfo" => return Thinfo;
+	"all" or "any" => return Tall;
+	* => return -1;
+	}
+}
+
+rrtypename(t: int): string
+{
+	case t {
+	Ta =>	return "ip";
+	Tns =>	return "ns";
+	Tcname =>	return "cname";
+	Tsoa =>	return "soa";
+	Tptr =>	return "ptr";
+	Tmx =>	return "mx";
+	Tall =>	return "all";
+	Thinfo =>	return "hinfo";
+	* =>		return string t;
+	}
+}
+
+#
+# format of UDP head read and written in `headers' mode
+#
+Udphdrsize: con Udphdrlen;
+dnsid := 1;
+
+mkquery(qtype: int, qclass: int, name: string): (int, array of byte, string)
+{
+	qd := ref QR(name, qtype, qclass);
+	dm := ref DNSmsg;
+	dm.id = dnsid++;	# doesn't matter if two different procs use it (different fds)
+	dm.flags = Oquery;
+	if(referdns || !debug)
+		dm.flags |= Frecurse;
+	dm.qd = qd :: nil;
+	a: array of byte;
+	a = dm.pack(Udphdrsize);
+	if(a == nil)
+		return (0, nil, "dns: bad query message");	# should only happen if a name is ridiculous
+	for(i:=0; i<Udphdrsize; i++)
+		a[i] = byte 0;
+	a[Udprport] = byte (DNSport>>8);
+	a[Udprport+1] = byte DNSport;
+	return (dm.id&16rFFFF, a, nil);
+}
+
+udpquery(fd: ref Sys->FD, id: int, query: array of byte, sname: string, addr: ref RR): (ref DNSmsg, string)
+{
+	# TO DO: check address and ports?
+
+	if(debug)
+		sys->print("udp query %s\n", sname);
+	pick ar := addr {
+	A =>
+		query[Udpraddr:] = ip->v4prefix[0:IPv4off];
+		query[Udpraddr+IPv4off:] = ar.rdata[0:4];
+	* =>
+		return (nil, "not A resource");
+	}
+	dm: ref DNSmsg;
+	pidc := chan of int;
+	c := chan of array of byte;
+	spawn reader(fd, c, pidc);
+	rpid := <-pidc;
+	spawn timer(c, pidc);
+	tpid := <-pidc;
+	for(ntries := 0; ntries < 8; ntries++){
+		if(debug){
+			ipa := query[Udpraddr+IPv4off:];
+			sys->print("send udp!%d.%d.%d.%d!%d [%d] %d\n", int ipa[0], int ipa[1],
+				int ipa[2], int ipa[3], get2(query, Udprport).t0, ntries, len query);
+		}
+		n := sys->write(fd, query, len query);
+		if(n != len query)
+			return (nil, sys->sprint("udp write err: %r"));
+		buf := <-c;
+		if(buf != nil){
+			buf = buf[Udphdrsize:];
+			dm = DNSmsg.unpack(buf);
+			if(dm == nil){
+				kill(tpid);
+				kill(rpid);
+				return (nil, "bad udp reply message");
+			}
+			if(dm.flags & Fresp && dm.id == id){
+				if(dm.flags & Ftrunc && dm.ns == nil){
+					if(debug)
+						sys->print("id=%d was truncated\n", dm.id);
+				}else
+					break;
+			}else if(debug)
+				sys->print("id=%d got flags #%ux id %d\n", id, dm.flags, dm.id);
+		}else if(debug)
+			sys->print("timeout\n");
+	}
+	kill(tpid);
+	kill(rpid);
+	if(dm == nil)
+		return (nil, "no reply");
+	if(dm.err != nil){
+		sys->fprint(stderr, "bad reply: %s\n", dm.err);
+		return (nil, dm.err);
+	}
+	if(debug)
+		sys->print("reply: %s\n", dm.text());
+	return (dm, nil);
+}
+
+reader(fd: ref Sys->FD, c: chan of array of byte, pidc: chan of int)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		buf := array[4096+Udphdrsize] of byte;
+		n := sys->read(fd, buf, len buf);
+		if(n > 0){
+			if(debug)
+				sys->print("rcvd %d\n", n);
+			c <-= buf[0:n];
+		}else
+			c <-= nil;
+	}
+}
+
+timer(c: chan of array of byte, pidc: chan of int)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		sys->sleep(5*1000);
+		c <-= nil;
+	}
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+udpport(): ref Sys->FD
+{
+	conn := dial->announce(mntpt+"/udp!*!0");
+	if(conn == nil)
+		return nil;
+	if(sys->fprint(conn.cfd, "headers") < 0){
+		sys->fprint(stderr, "dns: can't set headers mode: %r\n");
+		return nil;
+	}
+	conn.dfd = sys->open(conn.dir+"/data", Sys->ORDWR);
+	if(conn.dfd == nil){
+		sys->fprint(stderr, "dns: can't open %s/data: %r\n", conn.dir);
+		return nil;
+	}
+	return conn.dfd;
+}
+
+#
+# TCP/IP can be used to get the whole of a truncated message
+#
+tcpquery(query: array of byte): (ref DNSmsg, string)
+{
+	# TO DO: check request id, ports etc.
+
+	ipa := query[Udpraddr+IPv4off:];
+	addr := sys->sprint("tcp!%d.%d.%d.%d!%d", int ipa[0], int ipa[1], int ipa[2], int ipa[3], DNSport);
+	conn := dial->dial(addr, nil);
+	if(conn == nil)
+		return (nil, sys->sprint("can't dial %s: %r", addr));
+	query = query[Udphdrsize-2:];
+	put2(query, 0, len query-2);	# replace UDP header by message length
+	n := sys->write(conn.dfd, query[Udphdrsize:], len query);
+	if(n != len query)
+		return (nil, sys->sprint("dns: %s: write err: %r", addr));
+	buf := readn(conn.dfd, 2);	# TCP/DNS record header
+	(mlen, nil) := get2(buf, 0);
+	if(mlen < 2 || mlen > 16384)
+		return (nil, sys->sprint("dns: %s: bad reply msg length=%d", addr, mlen));
+	buf = readn(conn.dfd, mlen);
+	if(buf == nil)
+		return (nil, sys->sprint("dns: %s: read err: %r", addr));
+	dm := DNSmsg.unpack(buf);
+	if(dm == nil)
+		return (nil, "dns: bad reply message");
+	if(dm.err != nil){
+		sys->fprint(stderr, "dns: %s: bad reply: %s\n", addr, dm.err);
+		return (nil, dm.err);
+	}
+	return (dm, nil);
+}
+
+readn(fd: ref Sys->FD, nb: int): array of byte
+{
+	buf:= array[nb] of byte;
+	for(n:=0; n<nb;){
+		m := sys->read(fd, buf[n:], nb-n);
+		if(m <= 0)
+			return nil;
+		n += m;
+	}
+	return buf;
+}
+
+timefd: ref Sys->FD;
+
+time(): int
+{
+	if(timefd == nil){
+		timefd = sys->open("/dev/time", Sys->OREAD);
+		if(timefd == nil)
+			return 0;
+	}
+	buf := array[128] of byte;
+	sys->seek(timefd, big 0, 0);
+	n := sys->read(timefd, buf, len buf);
+	if(n < 0)
+		return 0;
+	return int ((big string buf[0:n]) / big 1000000);
+}
+
+parseip(s: string): array of byte
+{
+	(ok, a) := IPaddr.parse(s);
+	if(ok < 0 || !a.isv4())
+		return nil;
+	return a.v4();
+}
--- /dev/null
+++ b/appl/cmd/ndb/dnsquery.b
@@ -1,0 +1,177 @@
+implement Dnsquery;
+
+#
+# Copyright © 2003 Vita Nuova Holdings LImited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+
+Dnsquery: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: dnsquery [-x /net] [-s server] [address ...]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		cantload(Bufio->PATH);
+
+	net := "/net";
+	server: string;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		cantload(Arg->PATH);
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		'x' =>
+			net = arg->arg();
+			if(net == nil)
+				usage();
+		's' =>
+			server = arg->arg();
+			if(server == nil)
+				usage();
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(server == nil)
+		server = net+"/dns";
+	if(args != nil){
+		for(; args != nil; args = tl args)
+			dnsquery(server, hd args);
+	}else{
+		f := bufio->fopen(sys->fildes(0), Sys->OREAD);
+		if(f == nil)
+			exit;
+		for(;;){
+			sys->print("> ");
+			s := f.gets('\n');
+			if(s == nil)
+				break;
+			dnsquery(server, s[0:len s-1]);
+		}
+	}
+}
+
+cantload(s: string)
+{
+	sys->fprint(sys->fildes(2), "dnsquery: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+dnsquery(server: string, query: string)
+{
+	dns := sys->open(server, Sys->ORDWR);
+	if(dns == nil){
+		sys->fprint(sys->fildes(2), "dnsquery: can't open %s: %r\n", server);
+		raise "fail:open";
+	}
+	stdout := sys->fildes(1);
+	for(i := len query; --i >= 0 && query[i] != ' ';)
+		{}
+	if(i < 0){
+		i = len query;
+		case dbattr(query) {
+		"ip" =>
+			query += " ptr";
+		* =>
+			query += " ip";
+		}
+	}
+	if(query[i+1:] == "ptr"){
+		while(i > 0 && query[i-1] == ' ')
+			i--;
+		if(!hastail(query[0:i], ".in-addr.arpa") && !hastail(query[0:i], ".IN-ADDR.ARPA"))
+			query = addr2arpa(query[0:i])+" ptr";
+	}
+	b := array of byte query;
+	if(sys->write(dns, b, len b) > 0){
+		sys->seek(dns, big 0, Sys->SEEKSTART);
+		buf := array[256] of byte;
+		while((n := sys->read(dns, buf, len buf)) > 0)
+			sys->print("%s\n", string buf[0:n]);
+		if(n == 0)
+			return;
+	}
+	sys->print("!%r\n");
+}
+
+hastail(s: string, t: string): int
+{
+	if(len s >= len t && s[len s - len t:] == t)
+		return 1;
+	return 0;
+}
+
+addr2arpa(a: string): string
+{
+	(nf, flds) := sys->tokenize(a, ".");
+	rl: list of string;
+	for(; flds != nil; flds = tl flds)
+		rl = hd flds :: rl;
+	addr: string;
+	for(; rl != nil; rl = tl rl){
+		if(addr != nil)
+			addr[len addr] = '.';
+		addr += hd rl;
+	}
+	return addr+".in-addr.arpa";
+}
+
+dbattr(s: string): string
+{
+	digit := 0;
+	dot := 0;
+	alpha := 0;
+	hex := 0;
+	colon := 0;
+	for(i := 0; i < len s; i++){
+		case c := s[i] {
+		'0' to '9' =>
+			digit = 1;
+		'a' to 'f' or 'A' to 'F' =>
+			hex = 1;
+		'.' =>
+			dot = 1;
+		':' =>
+			colon = 1;
+		* =>
+			if(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '-' || c == '&')
+				alpha = 1;
+		}
+	}
+	if(alpha){
+		if(dot)
+			return "dom";
+		return "sys";
+	}
+	if(colon)
+		return "ip";
+	if(dot){
+		if(!hex)
+			return "ip";
+		return "dom";
+	}
+	return "sys";
+}
--- /dev/null
+++ b/appl/cmd/ndb/mkfile
@@ -1,0 +1,28 @@
+<../../../mkconfig
+
+TARG=\
+	cs.dis\
+	csquery.dis\
+	dns.dis\
+	dnsquery.dis\
+	mkhash.dis\
+	query.dis\
+	registry.dis\
+	regquery.dis\
+
+SYSMODULES=\
+	sys.m\
+	draw.m\
+	bufio.m\
+	arg.m\
+	attrdb.m\
+	ip.m\
+	ipattr.m\
+	styx.m\
+	styxservers.m\
+
+MODULES=\
+
+DISBIN=$ROOT/dis/ndb
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/ndb/mkhash.b
@@ -1,0 +1,119 @@
+implement Mkhash;
+
+#
+# for compatibility, this is closely modelled on Plan 9's ndb/mkhash
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+include "attrdb.m";
+	attrdb: Attrdb;
+	Db, Dbf, Dbentry, Tuples, Attr: import attrdb;
+	attrhash: Attrhash;
+	NDBPLEN, NDBHLEN, NDBCHAIN, NDBNAP: import Attrhash;
+
+Mkhash: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	attrdb = load Attrdb Attrdb->PATH;
+	if(attrdb == nil)
+		error(sys->sprint("can't load %s: %r", Attrdb->PATH));
+	attrdb->init();
+	attrhash = load Attrhash Attrhash->PATH;
+	if(attrhash == nil)
+		error(sys->sprint("can't load %s: %r", Attrhash->PATH));
+
+	if(len args != 3)
+		error("usage: mkhash file attr");
+	args = tl args;
+	dbname := hd args;
+	args = tl args;
+	attr := hd args;
+	dbf := Dbf.open(dbname);
+	if(dbf == nil)
+		error(sys->sprint("can't open %s: %r", dbname));
+	offset := 0;
+	n := 0;
+	for(;;){
+		(e, nil, next) := dbf.readentry(offset, nil, nil, 0);
+		if(e == nil)
+			break;
+		m := len e.find(attr);
+		if(0 && m != 0)
+			sys->fprint(sys->fildes(2), "%ud [%d]\n", offset, m);
+		n += m;
+		offset = next;
+	}
+	hlen := 2*n+1;
+	chains := n*2*NDBPLEN;
+	file := array[NDBHLEN + hlen*NDBPLEN + chains] of byte;
+	tab := file[NDBHLEN:];
+	for(i:=0; i<len tab; i+=NDBPLEN)
+		put3(tab[i:], NDBNAP);
+	offset = 0;
+	chain := hlen*NDBPLEN;
+	for(;;){
+		(e, nil, next) := dbf.readentry(offset, nil, nil, 0);
+		if(e == nil)
+			break;
+		for(l := e.find(attr); l != nil; l = tl l)
+			for((nil, al) := hd l; al != nil; al = tl al)
+				chain = enter(tab, hd al, hlen, chain, offset);
+		offset = next;
+	}
+	hashfile := dbname+"."+attr;
+	hfd := sys->create(hashfile, Sys->OWRITE, 8r666);
+	if(hfd == nil)
+		error(sys->sprint("can't create %s: %r", hashfile));
+	mtime := 0;
+	if(dbf.dir != nil)
+		mtime = dbf.dir.mtime;
+	put4(file, mtime);
+	put4(file[4:], hlen);
+	if(sys->write(hfd, file, NDBHLEN+chain) != NDBHLEN+chain)
+		error(sys->sprint("error writing %s: %r", hashfile));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "mkhash: %s\n", s);
+	raise "fail:error";
+}
+
+enter(tab: array of byte, a: ref Attr, hlen: int, chain: int, offset: int): int
+{
+	o := attrhash->hash(a.val, hlen)*NDBPLEN;
+	for(; (p := attrhash->get3(tab[o:])) != NDBNAP; o = p & ~NDBCHAIN)
+		if((p & NDBCHAIN) == 0){
+			put3(tab[o:], chain | NDBCHAIN);
+			put3(tab[chain:], p);
+			put3(tab[chain+NDBPLEN:], offset);
+			return chain+2*NDBPLEN;
+		}
+	put3(tab[o:], offset);
+	return chain;
+}
+
+put3(a: array of byte, v: int)
+{
+	a[0] = byte v;
+	a[1] = byte (v>>8);
+	a[2] = byte (v>>16);
+}
+
+put4(a: array of byte, v: int)
+{
+	a[0] = byte v;
+	a[1] = byte (v>>8);
+	a[2] = byte (v>>16);
+	a[3] = byte (v>>24);
+}
--- /dev/null
+++ b/appl/cmd/ndb/query.b
@@ -1,0 +1,135 @@
+implement Query;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+
+include "attrdb.m";
+	attrdb: Attrdb;
+	Attr, Tuples, Dbentry, Db: import attrdb;
+
+include "arg.m";
+
+Query: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: query attr [value [rattr]]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	dbfile := "/lib/ndb/local";
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		badload(Arg->PATH);
+	arg->init(args);
+	arg->setusage("query [-a] [-f dbfile] attr [value [rattr]]");
+	all := 0;
+	while((o := arg->opt()) != 0)
+		case o {
+		'f' =>	dbfile = arg->earg();
+		'a' => all = 1;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	attr := hd args;
+	args = tl args;
+	value, rattr: string;
+	vflag := 0;
+	if(args != nil){
+		vflag = 1;
+		value = hd args;
+		args = tl args;
+		if(args != nil)
+			rattr = hd args;
+	}
+	arg = nil;
+
+	attrdb = load Attrdb Attrdb->PATH;
+	if(attrdb == nil)
+		badload(Attrdb->PATH);
+	err := attrdb->init();
+	if(err != nil)
+		error(sys->sprint("can't init Attrdb: %s", err));
+
+	db := Db.open(dbfile);
+	if(db == nil)
+		error(sys->sprint("can't open %s: %r", dbfile));
+	ptr: ref Attrdb->Dbptr;
+	for(;;){
+		e: ref Dbentry;
+		if(rattr != nil)
+			(e, ptr) = db.findbyattr(ptr, attr, value, rattr);
+		else if(vflag)
+			(e, ptr) = db.findpair(ptr, attr, value);
+		else
+			(e, ptr) = db.find(ptr, attr);
+		if(e == nil)
+			break;
+		if(rattr != nil){
+			matches: list of (ref Tuples, list of ref Attr);
+			if(rattr != nil)
+				matches = e.findbyattr(attr, value, rattr);
+			else
+				matches = e.find(attr);
+			for(; matches != nil; matches = tl matches){
+				(line, attrs) := hd matches;
+				if(attrs != nil)
+					printvals(attrs, all);
+				if(!all)
+					exit;
+			}
+		}else
+			printentry(e);
+		if(!all)
+			exit;
+	}
+}
+
+badload(s: string)
+{
+	error(sys->sprint("can't load %s: %r", s));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "query: %s\n", s);
+	raise "fail:error";
+}
+
+printentry(e: ref Dbentry)
+{
+	s := "";
+	for(lines := e.lines; lines != nil; lines = tl lines){
+		line := hd lines;
+		for(al := line.pairs; al != nil; al = tl al){
+			a := hd al;
+			s += sys->sprint(" %q=%q", a.attr, a.val);
+		}
+	}
+	if(s != "")
+		s = s[1:];
+	sys->print("%s\n", s);
+}
+
+printvals(al: list of ref Attr, all: int)
+{
+	for(; al != nil; al = tl al){
+		a := hd al;
+		sys->print("%q\n", a.val);
+		if(!all)
+			break;
+	}
+}
--- /dev/null
+++ b/appl/cmd/ndb/registry.b
@@ -1,0 +1,776 @@
+implement Registry;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "string.m";
+	str: String;
+include "daytime.m";
+	daytime: Daytime;
+include "bufio.m";
+include "attrdb.m";
+	attrdb: Attrdb;
+	Db, Dbf, Dbentry: import attrdb;
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Fid, Navigator, Navop: import styxservers;
+	Enotdir, Enotfound: import Styxservers;
+include "arg.m";
+
+# files:
+# 'new'
+#	write name of new service; (and possibly attribute column names)
+#		entry appears in directory of that name
+#	can then write attributes/values
+# 'index'
+#	read to get info on all services and their attributes.
+# 'find'
+#	write to set filter.
+#	read to get info on all services with matching attributes
+# 'event' (not needed initially)
+#	read to block until changes happen.
+# servicename
+#	write to change attributes (only by owner)
+#	remove to unregister service.
+
+Registry: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+Qroot,
+Qnew,
+Qindex,
+Qevent,
+Qfind,
+Qsvc:	con iota;
+
+
+Shift:	con 4;
+Mask:	con 2r1111;
+
+Egreg: con "buggy program!";
+Maxreplyidle: con 3;
+
+Service: adt {
+	id:		int;
+	slot:		int;
+	owner:	string;
+	name:	string;
+	atime:	int;
+	mtime:	int;
+	vers:		int;
+	fid:		int;		# fid that created it (NOFID if static)
+	attrs:		list of (string, string);
+
+	new:		fn(owner: string): ref Service;
+	find:		fn(id: int): ref Service;
+	remove:	fn(svc: self ref Service);
+	set:		fn(svc: self ref Service, attr, val: string);
+	get:		fn(svc: self ref Service, attr: string): string;
+};
+
+Filter: adt {
+	id:		int;	# filter ID (it's a fid)
+	attrs:		array of (string, string);
+
+	new:		fn(id: int): ref Filter;
+	find:		fn(id: int): ref Filter;
+	set:		fn(f: self ref Filter, a: array of (string, string));
+	match:	fn(f: self ref Filter, attrs: list of (string, string)): int;
+	remove:	fn(f: self ref Filter);
+};
+
+Event: adt {
+	id:		int;					# fid reading from Qevents
+	vers:		int;					# last change seen
+	m:		ref Tmsg.Read;			# outstanding read request
+
+	new:		fn(id: int): ref Event;
+	find:		fn(id: int): ref Event;
+	remove:	fn(e: self ref Event);
+	queue:	fn(e: self ref Event, m: ref Tmsg.Read): string;
+	post:		fn(vers: int);
+	flush:	fn(tag: int);
+};
+
+filters: list of ref Filter;
+events: list of ref Event;
+
+services := array[9] of ref Service;
+nservices := 0;
+idseq := 0;
+rootvers := 0;
+now: int;
+startdate: int;
+dbfile: string;
+
+srv: ref Styxserver;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	if(str == nil)
+		loaderr(String->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		loaderr(Daytime->PATH);
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		loaderr(Styx->PATH);
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	if(styxservers == nil)
+		loaderr(Styxservers->PATH);
+	styxservers->init(styx);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		loaderr(Arg->PATH);
+	arg->init(args);
+	arg->setusage("ndb/registry [-f initdb]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'f' =>	dbfile = arg->earg();
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(args != nil)
+		arg->usage();
+	arg = nil;
+
+	sys->pctl(Sys->FORKNS|Sys->NEWFD, 0::1::2::nil);
+	startdate = now = daytime->now();
+	if(dbfile != nil){
+		attrdb = load Attrdb Attrdb->PATH;
+		if(attrdb == nil)
+			loaderr(Attrdb->PATH);
+		attrdb->init();
+		db := Db.open(dbfile);
+		if(db == nil)
+			error(sys->sprint("can't open %s: %r", dbfile));
+		dbload(db);
+		db = nil;	# for now assume it's static
+		attrdb = nil;
+	}
+	navops := chan of ref Navop;
+	spawn navigator(navops);
+	tchan: chan of ref Tmsg;
+	(tchan, srv) = Styxserver.new(sys->fildes(0), Navigator.new(navops), big Qroot);
+	spawn serve(tchan, navops);
+}
+
+loaderr(p: string)
+{
+	error(sys->sprint("can't load %s: %r", p));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "registry: %s\n", s);
+	raise "fail:error";
+}
+
+serve(tchan: chan of ref Tmsg, navops: chan of ref Navop)
+{
+Serve:
+	while((gm := <-tchan) != nil){
+		now = daytime->now();
+		err := "";
+		pick m := gm {
+		Readerror =>
+			sys->fprint(sys->fildes(2), "registry: styx read error: %s\n", m.error);
+			break Serve;
+		Open =>
+			(fid, nil, nil, e) := srv.canopen(m);
+			if((err = e) != nil)
+				break;
+			if(fid.qtype & Sys->QTDIR)
+				srv.default(m);
+			else
+				open(m, fid);
+		Read =>
+			(fid, e) := srv.canread(m);
+			if((err = e) != nil)
+				break;
+			if(fid.qtype & Sys->QTDIR)
+				srv.read(m);
+			else
+				err = read(m, fid);
+		Write =>
+			(fid, e) := srv.canwrite(m);
+			if((err = e) != nil)
+				break;
+			err = write(m, fid);
+			if(err == nil)
+				srv.reply(ref Rmsg.Write(m.tag, len m.data));
+		Clunk =>
+			clunk(srv.clunk(m));
+		Remove =>
+			(fid, nil, e) := srv.canremove(m);
+			srv.delfid(fid);	# always clunked even on error
+			if((err = e) != nil)
+				break;
+			err = remove(fid);
+			if(err == nil)
+				srv.reply(ref Rmsg.Remove(m.tag));
+		Flush =>
+			Event.flush(m.oldtag);
+			srv.default(gm);
+		* =>
+			srv.default(gm);
+		}
+		if(err != "")
+			srv.reply(ref Rmsg.Error(gm.tag, err));
+	}
+	navops <-= nil;
+}
+
+open(m: ref Tmsg.Open, fid: ref Fid)
+{
+	path := int fid.path;
+	case path & Mask {
+	Qnew =>
+		svc := Service.new(fid.uname);
+		svc.fid = fid.fid;
+		fid.open(m.mode, (big ((svc.id << Shift)|Qsvc), 0, Sys->QTFILE));
+	Qevent =>
+		Event.new(fid.fid);
+		fid.open(m.mode, (fid.path, 0, Sys->QTFILE));
+	* =>
+		fid.open(m.mode, (fid.path, 0, fid.qtype));
+	}
+	srv.reply(ref Rmsg.Open(m.tag, (fid.path, 0, fid.qtype), 0));
+}
+
+read(m: ref Tmsg.Read, fid: ref Fid): string
+{
+	path := int fid.path;
+	case path & Mask {
+	Qindex =>
+		if(fid.data == nil || m.offset == big 0)
+			fid.data = getindexdata(-1, Styx->NOFID);
+		srv.reply(styxservers->readbytes(m, fid.data));
+	Qfind =>
+		if(fid.data == nil || m.offset == big 0)
+			fid.data = getindexdata(-1, fid.fid);
+		srv.reply(styxservers->readbytes(m, fid.data));
+	Qsvc =>
+		if(fid.data == nil || m.offset == big 0){
+			svc := Service.find(path >> Shift);
+			if(svc != nil)
+				svc.atime = now;
+			fid.data = getindexdata(path >> Shift, Styx->NOFID);
+		}
+		srv.reply(styxservers->readbytes(m, fid.data));
+	Qevent =>
+		e := Event.find(fid.fid);
+		if(e.vers == rootvers)
+			return e.queue(m);
+		else{
+			s := sys->sprint("%8.8d\n", rootvers);
+			e.vers = rootvers;
+			m.offset = big 0;
+			srv.reply(styxservers->readstr(m, s));
+			return nil;
+		}
+	* =>
+		return Egreg;
+	}
+	return nil;
+}
+
+write(m: ref Tmsg.Write, fid: ref Fid): string
+{
+	path := int fid.path;
+	case path & Mask {
+	Qsvc =>
+		svc := Service.find(path >> Shift);
+		if(svc == nil)
+			return Egreg;
+		s := string m.data;
+		toks := str->unquoted(s);
+		if(toks == nil)
+			return "bad syntax";
+		# first write names the service (possibly with attributes)
+		if(svc.name == nil){
+			err := svcnameok(hd toks);
+			if(err != nil)
+				return err;
+			svc.name = hd toks;
+			toks = tl toks;
+		}
+		if(len toks % 2 != 0)
+			return "odd attribute/value pairs";
+		svc.mtime = now;
+		svc.vers++;
+		for(; toks != nil; toks = tl tl toks)
+			svc.set(hd toks, hd tl toks);
+		rootvers++;
+		Event.post(rootvers);
+	Qfind =>
+		s := string m.data;
+		toks := str->unquoted(s);
+		n := len toks;
+		if(n % 2 != 0)
+			return "odd attribute/value pairs";
+		f := Filter.find(fid.fid);
+		if(n != 0){
+			a := array[n/2] of (string, string);
+			for(n=0; toks != nil; n++){
+				a[n] = (hd toks, hd tl toks);
+				toks = tl tl toks;
+			}
+			if(f == nil)
+				f = Filter.new(fid.fid);
+			f.set(a);
+		}else{
+			if(f != nil)
+				f.remove();
+		}
+	* =>
+		return Egreg;
+	}
+	return nil;
+}
+
+clunk(fid: ref Fid)
+{
+	path := int fid.path;
+	case path & Mask {
+	Qsvc =>
+		svc := Service.find(path >> Shift);
+		if(svc != nil && svc.fid == fid.fid && (fid.mode & Sys->ORCLOSE || int svc.get("persist") == 0)){
+			svc.remove();
+			if(svc.name != nil){	# otherwise there's no visible change
+				rootvers++;
+				Event.post(rootvers);
+			}
+		}
+	Qevent =>
+		if((e := Event.find(fid.fid)) != nil)
+			e.remove();
+	Qfind =>
+		if((f := Filter.find(fid.fid)) != nil)
+			f.remove();
+	}
+}
+
+remove(fid: ref Fid): string
+{
+	path := int fid.path;
+	if((path & Mask) == Qsvc){
+		svc := Service.find(path >> Shift);
+		if(fid.uname == svc.owner){
+			svc.remove();
+			rootvers++;
+			Event.post(rootvers);
+			return nil;
+		}
+	}
+	return "permission denied";
+}
+
+svcnameok(s: string): string
+{
+	# could require that a service name contains at least one (or two) '!' characters.
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c <= 32 || c == '/' || c == 16r7f)
+			return "bad character in service name";
+	}
+	case s {
+	"new" or
+	"event" or
+	"find" or
+	"index" or
+	"" =>
+		return "bad service name";
+	}
+	for(i = 0; i < nservices; i++)
+		if(services[i].name == s)
+			return "duplicate service name";
+	return nil;
+}
+
+getindexdata(id: int, filterid: int): array of byte
+{
+	f: ref Filter;
+	if(filterid != Styx->NOFID)
+		f = Filter.find(filterid);
+	s := "";
+	for(i := 0; i < nservices; i++){
+		svc := services[i];
+		if(svc == nil || svc.name == nil)
+			continue;
+		if(id == -1){
+			if(f != nil && !f.match(svc.attrs))
+				continue;
+		}else if(svc.id != id)
+			continue;
+		s += sys->sprint("%q", services[i].name);
+		for(a := svc.attrs; a != nil; a = tl a){
+			(attr, val) := hd a;
+			s += sys->sprint(" %q %q", attr, val);
+		}
+		s[len s] = '\n';
+	}
+	return array of byte s;
+}
+
+navigator(navops: chan of ref Navop)
+{
+	while((m := <-navops) != nil){
+		path := int m.path;
+		pick n := m {
+		Stat =>
+			n.reply <-= dirgen(int n.path);
+		Walk =>
+			name := n.name;
+			case path & Mask {
+			Qroot =>
+				case name{
+				".." =>
+					;	# nop
+				"new" =>
+					path = Qnew;
+				"index" =>
+					path = Qindex;
+				"event" =>
+					path = Qevent;
+				"find" =>
+					path = Qfind;
+				* =>
+					for(i := 0; i < nservices; i++)
+						if(services[i].name == name){
+							path = (services[i].id << Shift) | Qsvc;
+							break;
+						}
+					if(i == nservices){
+						n.reply <-= (nil, Enotfound);
+						continue;
+					}
+				}
+			* =>
+				if(name == ".."){
+					path = Qroot;
+					break;
+				}
+				n.reply <-= (nil, Enotdir);
+				continue;
+			}
+			n.reply <-= dirgen(path);
+		Readdir =>
+			d: array of int;
+			case path & Mask {
+			Qroot =>
+				Nstatic:	con 4;
+				d = array[Nstatic + nservices] of int;
+				d[0] = Qnew;
+				d[1] = Qindex;
+				d[2] = Qfind;
+				d[3] = Qevent;
+				nd := 0;
+				for(i := 0; i < nservices; i++)
+					if(services[i].name != nil){
+						d[nd + Nstatic] = (services[i].id<<Shift) | Qsvc;
+						nd++;
+					}
+				d = d[0:Nstatic + nd];
+			}
+			if(d == nil){
+				n.reply <-= (nil, Enotdir);
+				break;
+			}
+			for(i := n.offset; i < len d; i++){
+				(dir, err) := dirgen(d[i]);
+				if(dir == nil)
+					sys->fprint(sys->fildes(2), "registry: bad qid %#ux: %s\n", d[i], err);
+				else
+					n.reply <-= (dir, err);
+			}
+			n.reply <-= (nil, nil);
+		}
+	}
+}
+
+dirgen(path: int): (ref Sys->Dir, string)
+{
+	name: string;
+	perm: int;
+	svc: ref Service;
+	case path & Mask {
+	Qroot =>
+		name = ".";
+		perm = 8r777|Sys->DMDIR;
+	Qnew =>
+		name = "new";
+		perm = 8r666;
+	Qindex =>
+		name = "index";
+		perm = 8r444;
+	Qevent =>
+		name = "event";
+		perm = 8r444;
+	Qfind =>
+		name = "find";
+		perm = 8r666;
+	Qsvc =>
+		id := path >> Shift;
+		for(i := 0; i < nservices; i++)
+			if(services[i].id == id)
+				break;
+		if(i >= nservices)
+			return (nil, Enotfound);
+		svc = services[i];
+		name = svc.name;
+		perm = 8r644;
+	* =>
+		return (nil, Enotfound);
+	}
+	return (dir(path, name, perm, svc), nil);
+}
+
+dir(path: int, name: string, perm: int, svc: ref Service): ref Sys->Dir
+{
+	d := ref sys->zerodir;
+	d.qid.path = big path;
+	if(perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	d.mode = perm;
+	d.name = name;
+	if(svc != nil){
+		d.uid = svc.owner;
+		d.gid = svc.owner;
+		d.atime = svc.atime;
+		d.mtime = svc.mtime;
+		d.qid.vers = svc.vers;
+	}else{
+		d.uid = "registry";
+		d.gid = "registry";
+		d.atime = startdate;
+		d.mtime = startdate;
+		if(path == Qroot)
+			d.qid.vers = rootvers;
+	}
+	return d;
+}
+
+blanksvc: Service;
+Service.new(owner: string): ref Service
+{
+	if(nservices == len services){
+		s := array[nservices * 3 / 2] of ref Service;
+		s[0:] = services;
+		services = s;
+	}
+	svc := ref blanksvc;
+	svc.id = idseq++;
+	svc.owner = owner;
+	svc.atime = now;
+	svc.mtime = now;
+
+	services[nservices] = svc;
+	svc.slot = nservices;
+	nservices++;
+	return svc;
+}
+
+Service.find(id: int): ref Service
+{
+	for(i := 0; i < nservices; i++)
+		if(services[i].id == id)
+			return services[i];
+	return nil;
+}
+
+Service.remove(svc: self ref Service)
+{
+	slot := svc.slot;
+	services[slot] = nil;
+	nservices--;
+	if(slot != nservices){
+		services[slot] = services[nservices];
+		services[slot].slot = slot;
+		services[nservices] = nil;
+	}
+}
+
+Service.get(svc: self ref Service, attr: string): string
+{
+	for(a := svc.attrs; a != nil; a = tl a)
+		if((hd a).t0 == attr)
+			return (hd a).t1;
+	return nil;
+}
+
+Service.set(svc: self ref Service, attr, val: string)
+{
+	for(a := svc.attrs; a != nil; a = tl a)
+		if((hd a).t0 == attr)
+			break;
+	if(a == nil){
+		svc.attrs = (attr, val) :: svc.attrs;
+		return;
+	}
+	attrs := (attr, val) :: tl a;
+	for(a = svc.attrs; a != nil; a = tl a){
+		if((hd a).t0 == attr)
+			break;
+		attrs = hd a :: attrs;
+	}
+	svc.attrs = attrs;
+}
+
+Filter.new(id: int): ref Filter
+{
+	f := ref Filter(id, nil);
+	filters = f :: filters;
+	return f;
+}
+
+Filter.find(id: int): ref Filter
+{
+	if(id != Styx->NOFID)
+		for(fl := filters; fl != nil; fl = tl fl)
+			if((hd fl).id == id)
+				return hd fl;
+	return nil;
+}
+
+Filter.set(f: self ref Filter, a: array of (string, string))
+{
+	f.attrs = a;
+}
+
+Filter.remove(f: self ref Filter)
+{
+	rl: list of ref Filter;
+	for(l := filters; l != nil; l = tl l)
+		if((hd l).id != f.id)
+			rl = hd l :: rl;
+	filters = rl;
+}
+
+Filter.match(f: self ref Filter, attrs: list of (string, string)): int
+{
+	for(i := 0; i < len f.attrs; i++){
+		(qn, qv) := f.attrs[i];
+		for(al := attrs; al != nil; al = tl al){
+			(n, v) := hd al;
+			if(n == qn && (qv == "*" || v == qv))
+				break;
+		}
+		if(al == nil)
+			break;
+	}
+	return i == len f.attrs;
+}
+
+Event.new(id: int): ref Event
+{
+	e := ref Event(id, rootvers, nil);
+	events = e::events;
+	return e;
+}
+
+Event.find(id: int): ref Event
+{
+	for(l := events; l != nil; l = tl l)
+		if((hd l).id == id)
+			return hd l;
+	return nil;
+}
+
+Event.remove(e: self ref Event)
+{
+	rl: list of ref Event;
+	for(l := events; l != nil; l = tl l)
+		if((hd l).id != e.id)
+			rl = hd l :: rl;
+	events = rl;
+}
+
+Event.queue(e: self ref Event, m: ref Tmsg.Read): string
+{
+	if(e.m != nil)
+		return "concurrent read for event fid";
+	m.offset = big 0;
+	e.m = m;
+	return nil;
+}
+
+Event.post(vers: int)
+{
+	s := sys->sprint("%8.8d\n", vers);
+	for(l := events; l != nil; l = tl l){
+		e := hd l;
+		if(e.vers < vers && e.m != nil){
+			srv.reply(styxservers->readstr(e.m, s));
+			e.vers = vers;
+			e.m = nil;
+		}
+	}
+}
+
+Event.flush(tag: int)
+{
+	for(l := events; l != nil; l = tl l){
+		e := hd l;
+		if(e.m != nil && e.m.tag == tag){
+			e.m = nil;
+			break;
+		}
+	}
+}
+
+dbload(db: ref Db)
+{
+	ptr: ref Attrdb->Dbptr;
+	for(;;){
+		e: ref Dbentry;
+		(e, ptr) = db.find(ptr, "service");
+		if(e == nil)
+			break;
+		svcname := e.findfirst("service");
+		if(svcname == nil || svcnameok(svcname) != nil)
+			continue;
+		svc := Service.new("registry");	 # TO DO: read user's name
+		svc.name = svcname;
+		svc.fid = Styx->NOFID;
+		for(l := e.lines; l != nil; l = tl l){
+			for(al := (hd l).pairs; al != nil; al = tl al){
+				a := hd al;
+				if(a.attr != "service")
+					svc.set(a.attr, a.val);
+			}
+		}
+	}
+}
+
+# return index i >= start such that
+# s[i-1] == eoc, or len s if no such index exists.
+# eoc shouldn't be '
+qsplit(s: string, start: int, eoc: int): int
+{
+	inq := 0;
+	for(i := start; i < len s;){
+		c := s[i++];
+		if(inq){
+			if(c == '\'' && i < len s){
+				if(s[i] == '\'')
+					i++;
+				else
+					inq = 0;
+			}
+		}else{
+			if(c == eoc)
+				return i;
+			if(c == '\'')
+				inq = 1;
+		}
+	}
+	return i;
+}
--- /dev/null
+++ b/appl/cmd/ndb/regquery.b
@@ -1,0 +1,104 @@
+implement Regquery;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+
+Regquery: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		cantload(Bufio->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		cantload(String->PATH);
+
+	mntpt := "/mnt/registry";
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		cantload(Arg->PATH);
+	arg->init(args);
+	arg->setusage("regquery [-m mntpt] [-n] [attr val attr val ...]");
+	namesonly := 0;
+	while((c := arg->opt()) != 0)
+		case c {
+		'm' =>	mntpt = arg->earg();
+		'n' =>	namesonly = 1;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	finder := mntpt+"/find";
+	if(args != nil){
+		s := "";
+		for(; args != nil; args = tl args)
+			s += sys->sprint(" %q", hd args);
+		if(s != nil)
+			s = s[1:];
+		regquery(finder, s, namesonly);
+	}else{
+		f := bufio->fopen(sys->fildes(0), Sys->OREAD);
+		if(f == nil)
+			exit;
+		for(;;){
+			sys->print("> ");
+			s := f.gets('\n');
+			if(s == nil)
+				break;
+			regquery(finder, s[0:len s-1], namesonly);
+		}
+	}
+}
+
+cantload(s: string)
+{
+	sys->fprint(sys->fildes(2), "regquery: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+regquery(server: string, addr: string, namesonly: int)
+{
+	fd := sys->open(server, Sys->ORDWR);
+	if(fd == nil){
+		sys->fprint(sys->fildes(2), "regquery: can't open %s: %r\n", server);
+		raise "fail:open";
+	}
+	stdout := sys->fildes(1);
+	b := array of byte addr;
+	if(sys->write(fd, b, len b) >= 0){
+		sys->seek(fd, big 0, Sys->SEEKSTART);
+		if(namesonly){
+			bio := bufio->fopen(fd, Bufio->OREAD);
+			while((s := bio.gets('\n')) != nil){
+				l := str->unquoted(s);
+				if(l != nil)
+					sys->print("%s\n", hd l);
+			}
+			return;
+		}else{
+			buf := array[Sys->ATOMICIO] of byte;
+			while((n := sys->read(fd, buf, len buf)) > 0)
+				sys->print("%s", string buf[0:n]);
+			if(n == 0)
+				return;
+		}
+	}
+	sys->fprint(sys->fildes(2), "regquery: %r\n");
+}
--- /dev/null
+++ b/appl/cmd/netkey.b
@@ -1,0 +1,166 @@
+implement Netkey;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	keyring: Keyring;
+
+Netkey: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+ANAMELEN: con 28;
+DESKEYLEN: con 7;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+
+	if(len args > 1){
+		sys->fprint(sys->fildes(2), "usage: netkey\n");
+		raise "fail:usage";
+	}
+	(pw, err) := readconsline("Password: ", 1);
+	if(err != nil){
+		sys->fprint(sys->fildes(2), "netkey: %s\n", err);
+		raise "fail:error";
+	}
+	if(pw != nil)
+		while((chal := readconsline("challenge: ", 0).t0) != nil)
+			sys->print("response: %s\n", netcrypt(passtokey(pw), string int chal));
+}
+
+readconsline(prompt: string, raw: int): (string, string)
+{
+	fd := sys->open("/dev/cons", Sys->ORDWR);
+	if(fd == nil)
+		return (nil, sys->sprint("can't open cons: %r"));
+	sys->fprint(fd, "%s", prompt);
+	fdctl: ref Sys->FD;
+	if(raw){
+		fdctl = sys->open("/dev/consctl", sys->OWRITE);
+		if(fdctl == nil || sys->fprint(fdctl, "rawon") < 0)
+			return (nil, sys->sprint("can't open consctl: %r"));
+	}
+	line := array[256] of byte;
+	o := 0;
+	err: string;
+	buf := array[1] of byte;
+  Read:
+	while((r := sys->read(fd, buf, len buf)) > 0){
+		c := int buf[0];
+		case c {
+		16r7F =>
+			err = "interrupt";
+			break Read;
+		'\b' =>
+			if(o > 0)
+				o--;
+		'\n' or '\r' or 16r4 =>
+			break Read;
+		* =>
+			if(o > len line){
+				err = "line too long";
+				break Read;
+			}
+			line[o++] = byte c;
+		}
+	}
+	if(r < 0)
+		err = sys->sprint("can't read cons: %r");
+	if(raw){
+		sys->fprint(fdctl, "rawoff");
+		sys->fprint(fd, "\n");
+	}
+	if(err != nil)
+		return (nil, err);
+	return (string line[0:o], err);
+}
+
+#
+# duplicates auth9 but keeps this self-contained
+#
+
+netcrypt(key: array of byte, chal: string): string
+{
+	buf := array[8] of {* => byte 0};
+	a := array of byte chal;
+	if(len a > 7)
+		a = a[0:7];
+	buf[0:] = a;
+	encrypt(key, buf, len buf);
+	return sys->sprint("%.2ux%.2ux%.2ux%.2ux", int buf[0], int buf[1], int buf[2], int buf[3]);
+}
+
+passtokey(p: string): array of byte
+{
+	a := array of byte p;
+	n := len a;
+	if(n >= ANAMELEN)
+		n = ANAMELEN-1;
+	buf := array[ANAMELEN] of {* => byte ' '};
+	buf[0:] = a[0:n];
+	buf[n] = byte 0;
+	key := array[DESKEYLEN] of {* => byte 0};
+	t := 0;
+	for(;;){
+		for(i := 0; i < DESKEYLEN; i++)
+			key[i] = byte ((int buf[t+i] >> i) + (int buf[t+i+1] << (8 - (i+1))));
+		if(n <= 8)
+			return key;
+		n -= 8;
+		t += 8;
+		if(n < 8){
+			t -= 8 - n;
+			n = 8;
+		}
+		encrypt(key, buf[t:], 8);
+	}
+}
+
+parity := array[] of {
+	byte 16r01, byte 16r02, byte 16r04, byte 16r07, byte 16r08, byte 16r0b, byte 16r0d, byte 16r0e, 
+	byte 16r10, byte 16r13, byte 16r15, byte 16r16, byte 16r19, byte 16r1a, byte 16r1c, byte 16r1f, 
+	byte 16r20, byte 16r23, byte 16r25, byte 16r26, byte 16r29, byte 16r2a, byte 16r2c, byte 16r2f, 
+	byte 16r31, byte 16r32, byte 16r34, byte 16r37, byte 16r38, byte 16r3b, byte 16r3d, byte 16r3e, 
+	byte 16r40, byte 16r43, byte 16r45, byte 16r46, byte 16r49, byte 16r4a, byte 16r4c, byte 16r4f, 
+	byte 16r51, byte 16r52, byte 16r54, byte 16r57, byte 16r58, byte 16r5b, byte 16r5d, byte 16r5e, 
+	byte 16r61, byte 16r62, byte 16r64, byte 16r67, byte 16r68, byte 16r6b, byte 16r6d, byte 16r6e, 
+	byte 16r70, byte 16r73, byte 16r75, byte 16r76, byte 16r79, byte 16r7a, byte 16r7c, byte 16r7f, 
+	byte 16r80, byte 16r83, byte 16r85, byte 16r86, byte 16r89, byte 16r8a, byte 16r8c, byte 16r8f, 
+	byte 16r91, byte 16r92, byte 16r94, byte 16r97, byte 16r98, byte 16r9b, byte 16r9d, byte 16r9e, 
+	byte 16ra1, byte 16ra2, byte 16ra4, byte 16ra7, byte 16ra8, byte 16rab, byte 16rad, byte 16rae, 
+	byte 16rb0, byte 16rb3, byte 16rb5, byte 16rb6, byte 16rb9, byte 16rba, byte 16rbc, byte 16rbf, 
+	byte 16rc1, byte 16rc2, byte 16rc4, byte 16rc7, byte 16rc8, byte 16rcb, byte 16rcd, byte 16rce, 
+	byte 16rd0, byte 16rd3, byte 16rd5, byte 16rd6, byte 16rd9, byte 16rda, byte 16rdc, byte 16rdf, 
+	byte 16re0, byte 16re3, byte 16re5, byte 16re6, byte 16re9, byte 16rea, byte 16rec, byte 16ref, 
+	byte 16rf1, byte 16rf2, byte 16rf4, byte 16rf7, byte 16rf8, byte 16rfb, byte 16rfd, byte 16rfe,
+};
+
+des56to64(k56: array of byte): array of byte
+{
+	k64 := array[8] of byte;
+	hi := (int k56[0]<<24)|(int k56[1]<<16)|(int k56[2]<<8)|int k56[3];
+	lo := (int k56[4]<<24)|(int k56[5]<<16)|(int k56[6]<<8);
+
+	k64[0] = parity[(hi>>25)&16r7f];
+	k64[1] = parity[(hi>>18)&16r7f];
+	k64[2] = parity[(hi>>11)&16r7f];
+	k64[3] = parity[(hi>>4)&16r7f];
+	k64[4] = parity[((hi<<3)|int ((big lo & big 16rFFFFFFFF)>>29))&16r7f];	# watch the sign extension
+	k64[5] = parity[(lo>>22)&16r7f];
+	k64[6] = parity[(lo>>15)&16r7f];
+	k64[7] = parity[(lo>>8)&16r7f];
+	return k64;
+}
+
+encrypt(key: array of byte, data: array of byte, n: int)
+{
+	ds := keyring->dessetup(des56to64(key), nil);
+	keyring->desecb(ds, data, n, Keyring->Encrypt);
+}
--- /dev/null
+++ b/appl/cmd/netstat.b
@@ -1,0 +1,91 @@
+implement Netstat;
+
+include "sys.m";
+sys: Sys;
+FD, Dir: import sys;
+fildes, open, fstat, read, dirread, fprint, print, tokenize: import sys;
+
+include "draw.m";
+Context: import Draw;
+
+Netstat: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+stderr: ref FD;
+
+init(nil: ref Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	stderr = fildes(2);
+
+	nstat("/net/tcp", 1);
+	nstat("/net/udp", 1);
+	nstat("/net/il", 0);
+}
+
+nstat(file: string, whine: int)
+{
+	dir: Dir;
+ 	i, ok: int;
+
+	fd := open(file, sys->OREAD);
+	if(fd == nil) {
+		if(whine)
+			fprint(stderr, "netstat: %s: %r\n", file);
+		return;
+	}
+
+	(ok, dir) = fstat(fd);
+	if(ok == -1) {
+		fprint(stderr, "netstat: fstat %s: %r\n", file);
+		fd = nil;
+		return;
+	}
+	if((dir.mode&Sys->DMDIR) == 0) {
+		fprint(stderr, "netstat: not a protocol directory: %s\n", file);
+		return;
+	}
+	for(;;) {
+		(n, d) := dirread(fd);
+		if(n <= 0)
+			break;
+		for(i = 0; i < n; i++)
+			if(d[i].name[0] <= '9')
+				nsprint(file+"/"+d[i].name, d[i].uid);		
+	}
+}
+
+fc(file: string): string
+{
+	fd := open(file, sys->OREAD);
+	if(fd == nil)
+		return "??";
+
+	buf := array[64] of byte;
+	n := read(fd, buf, len buf);
+	if(n <= 1)
+		return "??";
+	if(int buf[n-1] == '\n')
+		n--;
+
+	return string buf[0:n];
+}
+
+nsprint(name, user: string)
+{
+	n: int;
+	s: list of string;
+
+	sr := fc(name+"/status");
+	(n, s) = tokenize(sr, " ");
+
+	print("%-10s %-10s %-12s %-20s %s\n",
+		name[5:],
+		user,
+		hd s,
+		fc(name+"/local"),
+		fc(name+"/remote"));
+}
--- /dev/null
+++ b/appl/cmd/newer.b
@@ -1,0 +1,36 @@
+implement Newer;
+
+#
+# test if a file is up to date
+#
+
+include "sys.m";
+
+include "draw.m";
+
+Newer: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys := load Sys Sys->PATH;
+	if(len args != 3){
+		sys->fprint(sys->fildes(2), "usage: newer newfile oldfile\n");
+		raise "fail:usage";
+	}
+	args = tl args;
+	(ok1, d1) := sys->stat(hd args);
+	if(ok1 < 0)
+		raise sys->sprint("fail:new:%r");
+	if(d1.mode & Sys->DMDIR)
+		raise "fail:new:directory";
+	(ok2, d2) := sys->stat(hd tl args);
+	if(ok2 < 0)
+		raise sys->sprint("fail:old:%r");
+	if(d2.mode & Sys->DMDIR)
+		raise "fail:old:directory";
+	if(d2.mtime > d1.mtime)
+		raise "fail:older";
+}
--- /dev/null
+++ b/appl/cmd/ns.b
@@ -1,0 +1,157 @@
+# ns - display the construction of the current namespace (loosely based on plan 9's ns)
+implement Ns;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+Ns: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+SHELLMETA: con "' \t\\$#";
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: ns [-r] [pid]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil){
+		sys->fprint(sys->fildes(2), "ns: can't load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+	arg->init(args);
+	pid := sys->pctl(0, nil);
+	raw := 0;
+	while((o := arg->opt()) != 0)
+		case o {
+		'r' =>
+			raw = 1;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(len args > 1)
+		usage();
+	if(len args > 0)
+		pid = int hd args;
+
+	nsname := sys->sprint("/prog/%d/ns", pid);
+	nsfd := sys->open(nsname, Sys->OREAD);
+	if(nsfd == nil) {
+		sys->fprint(sys->fildes(2), "ns: can't open %s: %r\n", nsname);
+		raise "fail:open";
+	}
+
+	buf := array[2048] of byte;
+	while((l := sys->read(nsfd, buf, len buf)) > 0){
+		(nstr, lstr) := sys->tokenize(string buf[0:l], " \n");
+		if(nstr < 2)
+			continue;
+		cmd := hd lstr;
+		lstr = tl lstr;
+		if(cmd == "cd" && lstr != nil){
+			sys->print("%s %s\n", cmd, quoted(hd lstr));
+			continue;
+		}
+
+		sflag := "";
+		if((hd lstr)[0] == '-') {
+			sflag = hd lstr + " ";
+			lstr = tl lstr;
+		}
+		if(len lstr < 2)
+			continue;
+
+		src := hd lstr;
+		lstr = tl lstr;
+		if(len src >= 3 && (src[0:2] == "#/" || src[0:2] == "#U")) # remove unnecesary #/'s and #U's
+			src = src[2:];
+
+		# remove "#." from beginning of destination path
+		dest := hd lstr;
+		if(dest == "#M") {
+			dest = dest[2:];
+			if(dest == "")
+				dest = "/";
+		}
+
+		if(cmd == "mount" && !raw)
+			src = netaddr(src);	# optionally rewrite network files to network address
+
+		# quote arguments if "#" found
+		sys->print("%s %s%s %s\n", cmd, sflag, quoted(src), quoted(dest));
+	} 
+	if(l < 0)
+		sys->fprint(sys->fildes(2), "ns: error reading %s: %r\n", nsname);
+}
+
+netaddr(f: string): string
+{
+	if(len f < 1 || f[0] != '/')
+		return f;
+	(nf, flds) := sys->tokenize(f, "/");	# expect /net[.alt]/proto/2/data
+	if(nf < 4)
+		return f;
+	netdir := hd flds;
+	if(netdir != "net" && netdir != "net.alt")
+		return f;
+	proto := hd tl flds;
+	d := hd tl tl flds;
+	if(hd tl tl tl flds != "data")
+		return f;
+	fd := sys->open(sys->sprint("/%s/%s/%s/remote", hd flds, proto, d), Sys->OREAD);
+	if(fd == nil)
+		return f;
+	buf := array[256] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return f;
+	if(buf[n-1] == byte '\n')
+		n--;
+	if(netdir != "net")
+		proto = "/"+netdir+"/"+proto;
+	return sys->sprint("%s!%s", proto, string buf[0:n]);
+}
+
+any(c: int, t: string): int
+{
+	for(j := 0; j < len t; j++)
+		if(c == t[j])
+			return 1;
+	return 0;
+}
+
+contains(s: string, t: string): int
+{
+	for(i := 0; i<len s; i++)
+		if(any(s[i], t))
+			return 1;
+	return 0;
+}
+
+quoted(s: string): string
+{
+	if(!contains(s, SHELLMETA))
+		return s;
+	r := "'";
+	for(i := 0; i < len s; i++){
+		if(s[i] == '\'')
+			r[len r] = '\'';
+		r[len r] = s[i];
+	}
+	r[len r] = '\'';
+	return r;
+}
--- /dev/null
+++ b/appl/cmd/nsbuild.b
@@ -1,0 +1,41 @@
+implement Nsbuild;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+include "newns.m";
+
+stderr: ref Sys->FD;
+
+Nsbuild: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	ns := load Newns "/dis/lib/newns.dis";
+	if(ns == nil) {
+		sys->fprint(stderr, "nsbuild: can't load %s: %r", Newns->PATH);
+		raise "fail:load";
+	}
+
+	if(len argv > 2) {
+		sys->fprint(stderr, "Usage: nsbuild [nsfile]\n");
+		raise "fail:usage";
+	}
+
+	nsfile := "namespace";
+	if(len argv == 2)
+		nsfile = hd tl argv;
+
+   	e := ns->newns(nil, nsfile);
+	if(e != ""){
+		sys->fprint(stderr, "nsbuild: error building namespace: %s\n", e);
+		raise "fail:newns";
+	}
+} 
--- /dev/null
+++ b/appl/cmd/os.b
@@ -1,0 +1,168 @@
+implement Os;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+
+Os: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	if(str == nil)
+		fail(sys->sprint("cannot load %s: %r", String->PATH));
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		fail(sys->sprint("cannot load %s: %r", Arg->PATH));
+
+	arg->init(args);
+	arg->setusage("os [-d dir] [-m mount] [-n] [-N nice] [-b] command [arg...]");
+
+	nice := 0;
+	nicearg: string;
+	workdir := "";
+	mntpoint := "";
+	foreground := 1;
+
+	while((opt := arg->opt()) != 0) {
+		case opt {
+		'd' =>
+			workdir = arg->earg();
+		'm' =>
+			mntpoint = arg->earg();
+		'n' =>
+			nice = 1;
+		'N' =>
+			nice = 1;
+			nicearg = sys->sprint(" %q", arg->earg());
+		'b' =>
+			foreground = 0;
+		* =>
+			arg->usage();
+		}
+	}
+	args = arg->argv();
+	if (args == nil)
+		arg->usage();
+	arg = nil;
+
+	sys->pctl(Sys->FORKNS, nil);
+	sys->bind("#p", "/prog", Sys->MREPL);		# don't worry if it fails
+	if(mntpoint == nil){
+		mntpoint = "/cmd";
+		if(sys->stat(mntpoint+"/clone").t0 == -1)
+		if(sys->bind("#C", "/", Sys->MBEFORE) < 0)
+			fail(sys->sprint("bind #C /: %r"));
+	}
+
+	cfd := sys->open(mntpoint+"/clone", sys->ORDWR);
+	if(cfd == nil)
+		fail(sys->sprint("cannot open /cmd/clone: %r"));
+	
+	buf := array[32] of byte;
+	if((n := sys->read(cfd, buf, len buf)) <= 0)
+		fail(sys->sprint("cannot read /cmd/clone: %r"));
+
+	dir := mntpoint+"/"+string buf[0:n];
+
+	wfd := sys->open(dir+"/wait", Sys->OREAD);
+	if(nice && sys->fprint(cfd, "nice%s", nicearg) < 0)
+		sys->fprint(sys->fildes(2), "os: warning: can't set nice priority: %r\n");
+
+	if(workdir != nil && sys->fprint(cfd, "dir %s", workdir) < 0)
+		fail(sys->sprint("cannot set cwd %q: %r", workdir));
+
+	if(foreground && sys->fprint(cfd, "killonclose") < 0)
+		sys->fprint(sys->fildes(2), "os: warning: cannot write killonclose: %r\n");
+
+	if(sys->fprint(cfd, "exec %s", str->quoted(args)) < 0)
+		fail(sys->sprint("cannot exec: %r"));
+
+	if(foreground){
+		if((tocmd := sys->open(dir+"/data", sys->OWRITE)) == nil)
+			fail(sys->sprint("canot open %s/data for writing: %r", dir));
+		if((fromcmd := sys->open(dir+"/data", sys->OREAD)) == nil)
+			fail(sys->sprint("cannot open %s/data for reading: %r", dir));
+		if((errcmd := sys->open(dir+"/stderr", sys->OREAD)) == nil)
+			fail(sys->sprint("cannot open %s/stderr for reading: %r", dir));
+
+		spawn copy(sync := chan of int, nil, sys->fildes(0), tocmd);
+		pid := <-sync;
+		tocmd = nil;
+
+		spawn copy(sync, nil, errcmd, sys->fildes(2));
+		epid := <-sync;
+		sync = nil;
+		errcmd = nil;
+	
+		spawn copy(nil, done := chan of int, fromcmd, sys->fildes(1));
+		fromcmd = nil;
+
+		# cfd is still open, so if we're killgrp'ed and we're on a platform
+		# (e.g. windows) where the fromcmd read is uninterruptible,
+		# cfd will be closed, so the command will be killed (due to killonclose), and
+		# the fromcmd read should complete, allowing that process to be killed.
+
+		<-done;
+		kill(pid);
+		kill(epid);
+	}
+
+	if(wfd != nil){
+		status := array[1024] of byte;
+		n = sys->read(wfd, status, len status);
+		if(n < 0)
+			fail(sys->sprint("wait error: %r"));
+		s := string status[0:n];
+		if(s != nil){
+			# pid user sys real status
+			flds := str->unquoted(s);
+			if(len flds < 5)
+				fail(sys->sprint("wait error: odd status: %q", s));
+			s = hd tl tl tl tl flds;
+			if(0)
+				sys->fprint(sys->fildes(2), "WAIT: %q\n", s);
+			if(s != nil)
+				raise "fail:host: "+s;
+		}
+	}
+}
+
+copy(sync, done: chan of int, f, t: ref Sys->FD)
+{
+	if(sync != nil)
+		sync <-= sys->pctl(0, nil);
+	buf := array[8192] of byte;
+	for(;;) {
+		r := sys->read(f, buf, len buf);
+		if(r <= 0)
+			break;
+		w := sys->write(t, buf, r);
+		if(w != r)
+			break;
+	}
+	if(done != nil)
+		done <-= 1;
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", sys->OWRITE);
+	sys->fprint(fd, "kill");
+}
+
+fail(msg: string)
+{
+	sys->fprint(sys->fildes(2), "os: %s\n", msg);
+	raise "fail:"+msg;
+}
--- /dev/null
+++ b/appl/cmd/p.b
@@ -1,0 +1,141 @@
+implement P;
+# Original by Steve Arons, based on Plan 9 p
+
+include "sys.m"; 
+	sys: Sys;
+	FD:	import Sys;
+include "draw.m";
+include "string.m";
+	str: String;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "sh.m";
+
+stderr: ref FD;
+outb, cons: ref Iobuf;
+drawctxt: ref Draw->Context;
+
+nlines := 22;	# 1/3rd 66-line nroff page (!)
+progname := "p";
+
+P: module
+{
+	init:  fn(ctxt:  ref Draw->Context, argv:  list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr, "Usage: p [-number] [file...]\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, argv:  list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		nomod(Bufio->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		nomod(String->PATH);
+	sys->pctl(Sys->FORKFD, nil);
+	drawctxt = ctxt;
+
+	stderr = sys->fildes(2);
+
+	if((stdout := sys->fildes(1)) != nil)
+		outb = bufio->fopen(stdout, bufio->OWRITE);
+	if(outb == nil){
+		sys->fprint(stderr, "p: can't open stdout: %r\n");
+		raise "fail:stdout";
+	}
+	cons = bufio->open("/dev/cons", bufio->OREAD);
+	if(cons == nil){
+		sys->fprint(stderr, "p: can't open /dev/cons: %r\n");
+		raise "fail:cons";
+	}
+
+	if(argv != nil){
+		progname = hd argv;
+		argv = tl argv;
+		if(argv != nil){
+			s := hd argv;
+			if(len s > 1 && s[0] == '-'){
+				(x, y) := str->toint(s[1:],10);
+				if(y == "" && x > 0)
+					nlines = x;
+				else
+					usage();
+				argv = tl argv;
+			}
+		}
+	}
+	if(argv == nil)
+		argv = "-" :: nil;
+	for(; argv != nil; argv = tl argv){
+		file := hd argv;
+		fd: ref Sys->FD;
+		if(file == "-"){
+			file = "stdin";
+			fd = sys->fildes(0);
+		}else
+			fd = sys->open(file, Sys->OREAD);
+		if(fd == nil){
+			sys->fprint(stderr, "%s: can't open %s: %r\n", progname, file);
+			continue;
+		}
+		page(fd);
+		fd = nil;
+	}
+}
+
+nomod(m: string)
+{
+	sys->fprint(sys->fildes(2), "%s: can't load %s: %r\n", progname, m);
+	raise "fail:load";
+}
+
+page(fd: ref Sys->FD)
+{
+	inb := bufio->fopen(fd, bufio->OREAD);
+	nl := nlines;
+	while((line := inb.gets('\n')) != nil){
+		outb.puts(line);        
+		if(--nl == 0){
+			outb.flush();
+			nl = nlines;
+			pause();
+		}
+	}
+	outb.flush();   
+}
+
+pause()
+{
+	for(;;){
+		cmdline := cons.gets('\n');
+		if(cmdline == nil || cmdline[0] == 'q') # catch ^d
+			exit;
+		else if(cmdline[0] == '!') {
+			done := chan of int;
+			spawn command(cmdline[1:], done);
+			<-done;
+		}else
+			break;
+	}
+}
+
+command(cmdline: string, done: chan of int)
+{
+	sh := load Sh Sh->PATH;
+	if(sh == nil) {
+		sys->fprint(stderr, "%s: can't load %s: %r\n", progname, Sh->PATH);
+		done <-= 0;
+		return;
+	}
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(cons.fd.fd, 0);
+	sh->system(drawctxt, cmdline);
+	done <-= 1;
+}
--- /dev/null
+++ b/appl/cmd/palm/connex.b
@@ -1,0 +1,124 @@
+implement Connex;
+
+#
+# temporary test program for palmsrv development
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "palm.m";
+	palm: Palm;
+	Record: import palm;
+	palmdb: Palmdb;
+	DB, PDB, PRC: import palmdb;
+
+include "desklink.m";
+	desklink: Desklink;
+	SysInfo: import desklink;
+
+Connex: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	palm = load Palm Palm->PATH;
+	if(palm == nil)
+		error(sys->sprint("can't load %s: %r", palm->PATH));
+	desklink = load Desklink Desklink->PATH1;
+	if(desklink == nil)
+		error(sys->sprint("can't load Desklink: %r"));
+
+	palm->init();
+
+	err: string;
+	(palmdb, err) = desklink->connect("/chan/palmsrv");
+	if(palmdb == nil)
+		error(sys->sprint("can't init Desklink: %s", err));
+	desklink->init(palm);
+	sysinfo := desklink->ReadSysInfo();
+	if(sysinfo == nil)
+		error(sys->sprint("can't read sys Info: %r"));
+	sys->print("ROM: %8.8ux locale: %8.8ux product: '%s'\n", sysinfo.romversion, sysinfo.locale, sysinfo.product);
+	user := desklink->ReadUserInfo();
+	if(user == nil)
+		error(sys->sprint("can't read user info"));
+	sys->print("userid: %d viewerid: %d lastsyncpc: %d succsync: %8.8ux lastsync: %8.8ux uname: '%s' password: %s\n",
+		user.userid, user.viewerid, user.lastsyncpc, user.succsynctime, user.lastsynctime, user.username, ba(user.password));
+	sys->print("Storage:\n");
+	for(cno:=0;;){
+		(cards, more, err) := desklink->ReadStorageInfo(cno);
+		for(i:=0; i<len cards; i++){
+			sys->print("%2d v=%d c=%d romsize=%d ramsize=%d ramfree=%d name='%s' maker='%s'\n",
+				cards[i].cardno, cards[i].version, cards[i].creation, cards[i].romsize, cards[i].ramsize,
+				cards[i].ramfree, cards[i].name, cards[i].maker);
+			cno = cards[i].cardno+1;
+		}
+		if(!more)
+			break;
+	}
+	sys->print("ROM DBs:\n");
+	listdbs(Desklink->DBListROM);
+	sys->print("RAM DBs:\n");
+	listdbs(Desklink->DBListRAM);
+
+	(db, ee) := DB.open("AddressDB", Palmdb->OREAD);
+	if(db == nil){
+		sys->print("error: AddressDB: %s\n", ee);
+		exit;
+	}
+	pdb := db.records();
+	if(pdb == nil){
+		sys->print("error: AddressDB: %r\n");
+		exit;
+	}
+	dumpfd := sys->create("dump", Sys->OWRITE, 8r600);
+	for(i:=0; (r := pdb.read(i)) != nil; i++)
+		sys->write(dumpfd, r.data, len r.data);
+#	desklink->EndOfSync(Desklink->SyncNormal);
+	desklink->hangup();
+}
+
+listdbs(sort: int)
+{
+	index := 0;
+	for(;;){
+		(dbs, more, e) := desklink->ReadDBList(0, sort, index);
+		if(dbs == nil){
+			if(e != nil)
+				sys->print("ReadDBList: %s\n", e);
+			break;
+		}
+		for(i := 0; i < len dbs; i++){
+			sys->print("#%4.4ux '%s'\n", dbs[i].index, dbs[i].name);
+			index = dbs[i].index+1;
+		}
+		if(!more)
+			break;
+	}
+}
+
+ba(a: array of byte): string
+{
+	s := "";
+	for(i := 0; i < len a; i++)
+		s += sys->sprint("%2.2ux", int a[i]);
+	return s;
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "tconn: %s\n", s);
+	fd := sys->open("/prog/"+string sys->pctl(0,nil)+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/palm/desklink.b
@@ -1,0 +1,843 @@
+implement Palmdb, Desklink;
+
+#
+# Palm Desk Link Protocol (DLP)
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# Request and response formats were extracted from
+# include/Core/System/DLCommon.h in the PalmOS SDK-5
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "daytime.m";
+	daytime: Daytime;
+	Tm: import daytime;
+
+include "palm.m";
+	palm: Palm;
+	DBInfo, Record, Resource, id2s, s2id, get2, put2, get4, put4, gets, argsize, packargs, unpackargs: import palm;
+
+include "timers.m";
+
+include "desklink.m";
+
+Maxrecbytes: con 16rFFFF;
+
+# operations defined by Palm
+
+T_ReadUserInfo, T_WriteUserInfo, T_ReadSysInfo, T_GetSysDateTime,
+T_SetSysDateTime, T_ReadStorageInfo, T_ReadDBList, T_OpenDB, T_CreateDB,
+T_CloseDB, T_DeleteDB, T_ReadAppBlock, T_WriteAppBlock, T_ReadSortBlock,
+T_WriteSortBlock, T_ReadNextModifiedRec, T_ReadRecord, T_WriteRecord,
+T_DeleteRecord, T_ReadResource, T_WriteResource, T_DeleteResource,
+T_CleanUpDatabase, T_ResetSyncFlags, T_CallApplication, T_ResetSystem,
+T_AddSyncLogEntry, T_ReadOpenDBInfo, T_MoveCategory, T_ProcessRPC,
+T_OpenConduit, T_EndOfSync, T_ResetDBIndex, T_ReadRecordIDList,
+# DLP 1.1 functions
+T_ReadNextRecInCategory, T_ReadNextModifiedRecInCategory,
+T_ReadAppPreference, T_WriteAppPreference, T_ReadNetSyncInfo,
+T_WriteNetSyncInfo, T_ReadFeature,
+# DLP 1.2 functions
+T_FindDB, T_SetDBInfo,
+# DLP 1.3 functions
+T_LoopBackTest, T_ExpSlotEnumerate, T_ExpCardPresent, T_ExpCardInfo: con 16r10+iota;
+# then there's a group of VFS requests that we don't currently use
+
+Response: con 16r80;
+
+Maxname: con 32;
+
+A1, A2: con Palm->ArgIDbase+iota;	# argument IDs have request-specific interpretation (most have only one ID)
+
+Timeout: con 30;	# seconds time out used by Palm's headers
+srvfd: ref Sys->FD;
+selfdb: Palmdb;
+
+errorlist := array [] of {
+	"no error",
+	"general Pilot system error",
+	"unknown request",
+	"out of dynamic memory on device",
+	"invalid parameter",
+	"not found",
+	"no open databases",
+	"database already open",
+	"too many open databases",
+	"database already exists",
+	"cannot open database",
+	"record previously deleted",
+	"record busy",
+	"operation not supported",
+	"unexpected error (ErrUnused1)",
+	"read only object",
+	"not enough space",
+	"size limit exceeded",
+	"sync cancelled",
+	"bad arg wrapper",
+	"argument missing",
+	"bad argument size",
+};
+
+Eshort: con "desklink protocol: response too short";
+
+debug := 0;
+
+connect(srvfile: string): (Palmdb, string)
+{
+	sys = load Sys Sys->PATH;
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return (nil, sys->sprint("can't load %s: %r", Daytime->PATH));
+	srvfd = sys->open(srvfile, Sys->ORDWR);
+	if(srvfd == nil)
+		return (nil, sys->sprint("can't open %s: %r", srvfile));
+	selfdb = load Palmdb "$self";
+	if(selfdb == nil)
+		return (nil, sys->sprint("can't load self as Palmdb: %r"));
+	return (selfdb, nil);
+}
+
+hangup(): int
+{
+	srvfd = nil;
+	return 0;
+}
+
+#
+# set the system error string
+#
+e(s: string): string
+{
+	if(s != nil){
+		s = "palm: "+s;
+		sys->werrstr(s);
+	}
+	return s;
+}
+
+#
+# sent before each conduit is opened by the desktop,
+# apparently to detect a pending cancel request (on the device)
+#
+OpenConduit(): int
+{
+	return nexec(T_OpenConduit, A1, nil);
+}
+
+#
+# end of sync on desktop
+#
+EndOfSync(status: int): int
+{
+	req := array[2] of byte;
+	put2(req, status);
+	return nexec(T_EndOfSync, A1, req);
+}
+
+ReadSysInfo(): ref SysInfo
+{
+	if((reply := dexec(T_ReadSysInfo, A1, nil, 14)) == nil)
+		return nil;
+	s := ref SysInfo;
+	s.romversion = get4(reply);
+	s.locale = get4(reply[4:]);
+	l := int reply[9];	# should be at most 4 apparently?
+	s.product = gets(reply[10:10+l]);
+	return s;
+}
+
+ReadSysInfoVer(): (int, int, int)
+{
+	req := array[4] of byte;
+	put2(req, 1);	# major version
+	put2(req, 2);	# minor version
+	if((reply := dexec(T_ReadSysInfo, A2, req, 12)) == nil)
+		return (0, 0, 0);
+	return (get4(reply), get4(reply[4:]), get4(reply[8:]));
+}
+
+ReadUserInfo(): ref User
+{
+	if((reply := dexec(T_ReadUserInfo, 0, nil, 30)) == nil)
+		return nil;
+	u := ref User;
+	u.userid = get4(reply);
+	u.viewerid = get4(reply[4:]);
+	u.lastsyncpc = get4(reply[8:]);
+	u.succsynctime = getdate(reply[12:]);
+	u.lastsynctime = getdate(reply[20:]);
+	userlen := int reply[28];
+	pwlen := int reply[29];
+	u.username = gets(reply[30:30+userlen]);
+	u.password = array[pwlen] of byte;
+	u.password[0:] = reply[30+userlen:30+userlen+pwlen];
+	return u;
+}
+
+WriteUserInfo(u: ref User, flags: int): int
+{
+	req := array[22+Maxname] of byte;
+	put4(req, u.userid);
+	put4(req[4:], u.viewerid);
+	put4(req[8:], u.lastsyncpc);
+	putdate(req[12:], u.lastsynctime);
+	req[20] = byte flags;
+	l := puts(req[22:], u.username);
+	req[21] = byte l;
+	return nexec(T_WriteUserInfo, A1, req[0:22+l]);
+}
+
+GetSysDateTime(): int
+{
+	if((reply := dexec(T_GetSysDateTime, A1, nil, 8)) == nil)
+		return -1;
+	return getdate(reply);
+}
+
+SetSysDateTime(time: int): int
+{
+	return nexec(T_SetSysDateTime, A1, putdate(array[8] of byte, time));
+}
+
+ReadStorageInfo(cardno: int): (array of ref CardInfo, int, string)
+{
+	req := array[2] of byte;
+	req[0] = byte cardno;
+	req[1] = byte 0;
+	(reply, err) := rexec(T_ReadStorageInfo, A1, req, 30);
+	if(reply == nil)
+		return (nil, 0, err);
+	nc := int reply[3];
+	if(nc <= 0)
+		return (nil, 0, nil);
+	more := int reply[1] != 0;
+	a := array[nc] of ref CardInfo;
+	p := 4;
+	for(i:=0; i<nc; i++){
+		nb: int;
+		(a[i], nb) = unpackcard(reply[p:]);
+		p += nb;
+	}
+	return (a, more, nil);
+}
+
+unpackcard(a: array of byte): (ref CardInfo, int)
+{
+	nb := int a[0];	# total size of this card's info
+	c := ref CardInfo;
+	c.cardno = int a[1];
+	c.version = get2(a[2:]);
+	c.creation = getdate(a[4:]);
+	c.romsize = get4(a[12:]);
+	c.ramsize = get4(a[16:]);
+	c.ramfree = get4(a[20:]);
+	l1 := int a[24] + 26;
+	l2 := int a[25];
+	c.name = gets(a[26:l1]);
+	c.maker = gets(a[l1:l1+l2]);
+	return (c, nb);
+}
+
+ReadDBCount(cardno: int): (int, int)
+{
+	req := array[2] of byte;
+	req[0] = byte cardno;
+	req[1] = byte 0;
+	if((reply := dexec(T_ReadStorageInfo, A2, req, 20)) == nil)
+		return (-1, -1);
+	return (get2(req[0:]), get2(req[2:]));
+}
+
+unpackdbinfo(a: array of byte): (ref DBInfo, int)
+{
+	size := int a[0];
+	misc := int a[1];
+	info := ref DBInfo;
+	info.attr = get2(a[2:]);
+	info.dtype = id2s(get4(a[4:]));
+	info.creator = id2s(get4(a[8:]));
+	info.version = get2(a[12:]);
+	info.modno = get4(a[14:]);
+	info.ctime = getdate(a[18:]);
+	info.mtime = getdate(a[26:]);
+	info.btime = getdate(a[34:]);
+	info.index = get2(a[42:]);
+	if(size > len a)
+		size = len a;
+	info.name = gets(a[44:size]);
+	return (info, size);
+}
+
+ReadDBList(cardno: int, flags: int, start: int): (array of ref DBInfo, int, string)
+{
+	req := array[4] of byte;
+	req[0] = byte (flags | DBListMultiple);
+	req[1] = byte cardno;
+	put2(req[2:], start);
+	(reply, err) := rexec(T_ReadDBList, A1, req, 48);
+	if(reply == nil || int reply[3] == 0)
+		return (nil, 0, err);
+	# lastindex[2] flags[1] actcount[1]
+	#	flags is 16r80 => more to list
+	more := (reply[2] & byte 16r80) != byte 0;
+	dbs := array[int reply[3]] of ref DBInfo;
+#sys->print("ndb=%d more=%d lastindex=#%4.4ux\n", len dbs, more, get2(reply));
+	a := reply[4:];
+	for(i := 0; i < len dbs; i++){
+		(db, n) := unpackdbinfo(a);
+		dbs[i] = db;
+		a = a[n:];
+	}
+	return (dbs, more, nil);
+}
+
+matchdb(cardno: int, flag: int, start: int, dbname: string, dtype: string, creator: string): (ref DBInfo, int)
+{
+	for(;;){
+		(dbs, more, err) := ReadDBList(cardno, flag, start);
+		if(dbs == nil)
+			break;
+		for(i := 0; i < len dbs; i++){
+			info := dbs[i];
+			if((dbname == nil || info.name == dbname) &&
+			   (dtype == nil || info.dtype == dtype) &&
+			   (creator == nil || info.creator == creator))
+				return (info, info.index);
+			start = info.index+1;
+		}
+	}
+	return (nil, 0);
+}
+
+
+FindDBInfo(cardno: int, start: int, dbname: string, dtype: string, creator: string): ref DBInfo
+{
+	if(start < 16r1000) {
+		(info, i) := matchdb(cardno, 16r80, start, dbname, dtype, creator);
+		if(info != nil)
+			return info;
+	}
+	(info, i) := matchdb(cardno, 16r40, start&~16r1000, dbname, dtype, creator);
+	if(info != nil)
+		info.index |= 16r1000;
+	return info;
+}
+
+DeleteDB(name: string): int
+{
+	(cardno, dbname) := parsedb(name);
+	req := array[2+Maxname] of byte;
+	req[0] = byte cardno;
+	req[1] = byte 0;
+	n := puts(req[2:], dbname);
+	return nexec(T_DeleteDB, A1, req[0:2+n]);
+}
+
+ResetSystem(): int
+{
+	return nexec(T_ResetSystem, 0, nil);
+}
+
+CloseDB_All(): int
+{
+	return nexec(T_CloseDB, A2, nil);
+}
+
+AddSyncLogEntry(entry: string): int
+{
+	req := array[256] of byte;
+	n := puts(req, entry);
+	return nexec(T_AddSyncLogEntry, A1, req[0:n]);
+}
+
+#
+# this implements a Palmdb->DB directly accessed using the desklink protocol
+#
+
+init(m: Palm): string
+{
+	palm = m;
+	return nil;
+}
+
+#
+# syntax is [cardno/]dbname
+# where cardno defaults to 0
+#
+parsedb(name: string): (int, string)
+{
+	(nf, flds) := sys->tokenize(name, "/");
+	if(nf > 1)
+		return (int hd flds, hd tl flds);
+	return (0, name);
+}
+
+DB.open(name: string, mode: int): (ref DB, string)
+{
+	(cardno, dbname) := parsedb(name);
+	req := array[2+Maxname] of byte;
+	req[0] = byte cardno;
+	req[1] = byte mode;
+	n := puts(req[2:], dbname);
+	(reply, err) := rexec(T_OpenDB, A1, req[0:2+n], 1);
+	if(reply == nil)
+		return (nil, err);
+	db := ref DB;
+	db.x = int reply[0];
+	inf := db.stat();
+	if(inf == nil)
+		return (nil, sys->sprint("can't get DBInfo: %r"));
+	db.attr = inf.attr;	# mainly need to know whether it's Fresource or not
+	return (db, nil);
+}
+
+DB.create(name: string, nil: int, nil: int, inf: ref DBInfo): (ref DB, string)
+{
+	(cardno, dbname) := parsedb(name);
+	req := array[14+Maxname] of byte;
+	put4(req, s2id(inf.creator));
+	put4(req[4:], s2id(inf.dtype));
+	req[8] = byte cardno;
+	req[9] = byte 0;
+	put2(req[10:], inf.attr);
+	put2(req[12:], inf.version);
+	n := puts(req[14:], dbname);
+	(reply, err) := rexec(T_CreateDB, A1, req[0:14+n], 1);
+	if(reply == nil)
+		return (nil, err);
+	db := ref DB;
+	db.x = int reply[0];
+	db.attr = inf.attr;
+	return (db, nil);
+}
+
+DB.stat(db: self ref DB): ref DBInfo
+{
+	(reply, err) := rexec(T_FindDB, A2, array[] of {byte 16r80, byte db.x}, 54);
+	if(err != nil)
+		return nil;
+	return unpackdbinfo(reply[10:]).t0;
+}
+
+DB.wstat(db: self ref DB, inf: ref DBInfo, flags: int)
+{
+	# TO DO
+}
+
+DB.close(db: self ref DB): string
+{
+	return rexec(T_CloseDB, A1, array[] of {byte db.x}, 0).t1;
+}
+
+DB.records(db: self ref DB): ref PDB
+{
+	if(db.attr & Palm->Fresource){
+		sys->werrstr("not a database file");
+		return nil;
+	}
+	return ref PDB(db);
+}
+
+DB.resources(db: self ref DB): ref PRC
+{
+	if((db.attr & Palm->Fresource) == 0){
+		sys->werrstr("not a resource file");
+		return nil;
+	}
+	return ref PRC(db);
+}
+
+DB.readidlist(db: self ref DB, sort: int): array of int
+{
+	req := array[6] of byte;
+	req[0] = byte db.x;
+	if(sort)
+		req[1] = byte 16r80;
+	else
+		req[1] = byte 0;
+	put2(req[2:], 0);
+	put2(req[4:], -1);
+	p := dexec(T_ReadRecordIDList, A1, req, 2);
+	if(p == nil)
+		return nil;
+	ret := get2(p);
+	ids := array[ret] of int;
+	p = p[8:];
+	for (i := 0; i < ret; p = p[4:])
+		ids[i++] = get4(p);
+	return ids;
+}
+
+DB.nentries(db: self ref DB): int
+{
+	if((reply := dexec(T_ReadOpenDBInfo, A1, array[] of {byte db.x}, 2)) == nil)
+		return -1;
+	return get2(reply);
+}
+
+DB.rdappinfo(db: self ref DB): (array of byte, string)
+{
+	req := array[6] of byte;
+	req[0] = byte db.x;
+	req[1] = byte 0;
+	put2(req[2:], 0);	# offset
+	put2(req[4:], -1);	# to end
+	(reply, err) := rexec(T_ReadAppBlock, A1, req, 2);
+	if(reply == nil)
+		return (nil, err);
+	if(get2(reply) < len reply-2)
+		return (nil, "short reply");
+	return (reply[2:], nil);
+}
+
+DB.wrappinfo(db: self ref DB, data: array of byte): string
+{
+	req := array[4 + len data] of byte;
+	req[0] = byte db.x;
+	req[1] = byte 0;
+	put2(req[2:], len data);
+	req[4:] = data;
+	return rexec(T_WriteAppBlock, A1, req, 0).t1;
+}
+
+DB.rdsortinfo(db: self ref DB): (array of int, string)
+{
+	req := array[6] of byte;
+	req[0] = byte db.x;
+	req[1] = byte 0;
+	put2(req[2:], 0);
+	put2(req[4:], -1);
+	(reply, err) := rexec(T_ReadSortBlock, A1, req, 2);
+	if(reply == nil)
+		return (nil, err);
+	n := len reply;
+	a := reply[2:n];
+	n = (n-2)/2;
+	s := array[n] of int;
+	for(i := 0; i < n; i++)
+		s[i] = get2(a[i*2:]);
+	return (s, nil);
+}
+
+DB.wrsortinfo(db: self ref DB, s: array of int): string
+{
+	n := len s;
+	req := array[4+2*n] of byte;
+	req[0] = byte db.x;
+	req[1] = byte 0;
+	put2(req[2:], 2*n);
+	for(i := 0; i < n; i++)
+		put2(req[2+i*2:], s[i]);
+	return rexec(T_WriteSortBlock, A1, req, 0).t1;
+}
+
+PDB.purge(db: self ref PDB): string
+{
+	return rexec(T_CleanUpDatabase, A1, array[] of {byte db.db.x}, 0).t1;
+}
+
+DB.resetsyncflags(db: self ref DB): string
+{
+	return rexec(T_ResetSyncFlags, A1, array[] of {byte db.x}, 0).t1;
+}
+
+#
+# .pdb and other data base files
+#
+
+PDB.read(db: self ref PDB, index: int): ref Record
+{
+	req := array[8] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte 0;
+	put2(req[2:], index);
+	put2(req[4:], 0);	# offset
+	put2(req[6:], Maxrecbytes);
+	return unpackrec(dexec(T_ReadRecord, A2, req, 10)).t0;
+}
+
+PDB.readid(db: self ref PDB, id: int): (ref Record, int)
+{
+	req := array[10] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte 0;
+	put4(req[2:], id);
+	put2(req[6:], 0); # offset
+	put2(req[8:], Maxrecbytes);
+	return unpackrec(dexec(T_ReadRecord, A1, req, 10));
+}
+
+PDB.write(db: self ref PDB, r: ref Record): string
+{
+	req := array[8+len r.data] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte 0;
+	put4(req[2:], r.id);
+	req[6] = byte (r.attr & Palm->Rsecret);
+	req[7] = byte r.cat;
+	req[8:] = r.data;
+	(reply, err) := rexec(T_WriteRecord, A1, req, 4);
+	if(reply == nil)
+		return err;
+	if(r.id == 0)
+		r.id = get4(reply);
+	return nil;
+}
+
+PDB.movecat(db: self ref PDB, from: int, tox: int): string
+{
+	req := array[4] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte from;
+	req[2] = byte tox;
+	req[3] = byte 0;
+	return rexec(T_MoveCategory, A1, req, 0).t1;
+}
+
+PDB.resetnext(db: self ref PDB): int
+{
+	return nexec(T_ResetDBIndex, A1, array[] of {byte db.db.x});
+}
+
+PDB.readnextmod(db: self ref PDB): (ref Record, int)
+{
+	return unpackrec(dexec(T_ReadNextModifiedRec, A1, array[] of {byte db.db.x}, 10));
+}
+
+PDB.delete(db: self ref PDB, id: int): string
+{
+	req := array[6] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte 0;
+	put4(req[2:], id);
+	return rexec(T_DeleteRecord, A1, req, 0).t1;
+}
+
+PDB.deletecat(db: self ref PDB, cat: int): string
+{
+	return rexec(T_DeleteRecord, A1, array[] of {byte db.db.x, byte 16r40, 2 to 6 => byte 0, 7=>byte cat}, 0).t1;
+}
+
+PDB.truncate(db: self ref PDB): string
+{
+	return rexec(T_DeleteRecord, A1, array[] of {byte db.db.x, byte 16r80, 2 to 7 => byte 0}, 0).t1;
+}
+
+#
+# .prc resource files
+#
+
+PRC.write(db: self ref PRC, r: ref Resource): string
+{
+	req := array[8+len r.data] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte 0;
+	put4(req[2:], r.name);
+	put2(req[6:], r.id);
+	put2(req[8:], len r.data);
+	return rexec(T_WriteResource, A1, req, 0).t1;
+}
+
+PRC.delete(db: self ref PRC, name: int, id: int): string
+{
+	req := array[8] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte 0;
+	put4(req[2:], name);
+	put4(req[6:], id);
+	return rexec(T_DeleteResource, A1, req, 0).t1;
+}
+
+PRC.readtype(db: self ref PRC, name: int, id: int): (ref Resource, int)
+{
+	req := array[12] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte 0;
+	put4(req[2:], name);
+	put2(req[6:], id);
+	put2(req[8:], 0); # Offset into record
+	put2(req[10:], Maxrecbytes);
+	return unpackresource(dexec(T_ReadResource, A2, req, 10));
+}
+
+PRC.truncate(db: self ref PRC): string
+{
+	return rexec(T_DeleteResource, A1, array[] of {byte db.db.x, byte 16r80, 2 to 7 => byte 0}, 0).t1;
+}
+
+PRC.read(db: self ref PRC, index: int): ref Resource
+{
+	req := array[8] of byte;
+	req[0] = byte db.db.x;
+	req[1] = byte 0;
+	put2(req[2:], index);
+	put2(req[4:], 0);	# offset
+	put2(req[6:], Maxrecbytes);
+	return unpackresource(dexec(T_ReadResource, A1, req, 12)).t0;
+}
+
+#
+# DL protocol
+#
+# request
+#	id: byte	# operation
+#	argc: byte	# arg count
+#	args: byte[]
+#
+# response
+#	id: byte	# cmd|16r80
+#	argc: byte	# argc response arguments follow header
+#	error: byte[2]	# error code
+#	args: byte[]
+#
+# args wrapped by Palm->packargs etc.
+#
+
+#
+# RPC exchange with device
+#
+rpc(req: array of byte): (array of (int, array of byte), string)
+{
+	if(sys->write(srvfd, req, len req) != len req)
+		return (nil, sys->sprint("link: %r"));
+	reply := array[65536] of byte;
+	nb := sys->read(srvfd, reply, len reply);
+	if(nb == 0)
+		return (nil, "link: hangup");
+	if(nb < 0)
+		return (nil, sys->sprint("link: %r"));
+	r := int reply[0];
+	if((r & Response) == 0)
+		return (nil, e(sys->sprint("received request #%2.2x not response", r)));
+	if(r != (Response|int req[0]))
+		return (nil, e(sys->sprint("wrong response #%x", r)));
+	if(nb < 4)
+		return (nil, e(Eshort));
+	rc := get2(reply[2:]);
+	if(rc != 0){
+		if(rc < 0 || rc >= len errorlist)
+			return (nil, e(sys->sprint("unknown error %d", rc)));
+		return (nil, e(errorlist[rc]));
+	}
+	argc := int reply[1];	# count of following arguments
+	if(argc == 0)
+		return (nil, nil);
+	return unpackargs(argc, reply[4:nb]);
+}
+
+rexec(cmd: int, argid: int, arg: array of byte, minlen: int): (array of byte, string)
+{
+	args: array of (int, array of byte);
+	if(arg != nil)
+		args = array[] of {(argid, arg)};
+	req := array[2+argsize(args)] of byte;
+	req[0] = byte cmd;
+	req[1] = byte len args;
+	packargs(req[2:], args);
+	(replies, err) := rpc(req);
+	if(replies == nil){
+		if(err != nil)
+			return (nil, err);
+		if(minlen > 0)
+			return (nil, e(Eshort));
+		return (nil, nil);
+	}
+	(nil, reply) := replies[0];
+	if(len reply < minlen)
+		return (nil, e(Eshort));
+	return (reply, nil);
+}
+
+dexec(cmd: int, argid: int, msg: array of byte, minlen: int): array of byte
+{
+	(reply, nil) := rexec(cmd, argid, msg, minlen);
+	return reply;
+}
+
+nexec(cmd: int, argid: int, msg: array of byte): int
+{
+	(nil, err) := rexec(cmd, argid, msg, 0);
+	if(err != nil)
+		return -1;
+	return 0;
+}
+
+unpackresource(a: array of byte): (ref Resource, int)
+{
+	nb := len a;
+	if(nb < 10)
+		return (nil, -1);
+	size := get2(a[8:]);
+	if(nb-10 < size)
+		return (nil, -1);
+	r := Resource.new(get4(a), get2(a[4:]), size);
+	r.data[0:] = a[10:10+size];
+	return (r, get2(a[6:]));
+}
+
+unpackrec(a: array of byte): (ref Record, int)
+{
+	nb := len a;
+	if(nb < 10)
+		return (nil, -1);
+	size := get2(a[6:]);
+	if(nb-10 < size)
+		return (nil, -1);
+	r := Record.new(get4(a), int a[8], int a[9], size);
+	r.data[0:] = a[10:10+size];
+	return (r, get2(a[4:]));
+}
+
+#
+# pack string (must be Latin1) as zero-terminated array of byte
+#
+puts(a: array of byte, s: string): int
+{
+	for(i := 0; i < len s && i < len a-1; i++)
+		a[i] = byte s[i];
+	a[i++] = byte 0;
+	return i;
+}
+
+#
+# the conversion via local time might be wrong,
+# since the computers might be in different time zones,
+# but is hard to avoid
+#
+
+getdate(data: array of byte): int
+{
+	yr := (int data[0] << 8) | int data[1];
+	if(yr == 0)
+		return 0;	# unspecified
+	t := ref Tm;
+	t.sec = int data[6];
+	t.min = int data[5];
+	t.hour = int data[4];
+	t.mday = int data[3];
+	t.mon = int data[2] - 1;
+	t.year = yr - 1900;
+	t.wday = 0;
+	t.yday = 0;
+	return daytime->tm2epoch(t);
+}
+
+putdate(data: array of byte, time: int): array of byte
+{
+	t := daytime->local(time);
+	y := t.year + 1900;
+	if(time == 0)
+		y = 0;	# `unchanged'
+	data[7] = byte 0; # pad
+	data[6] = byte t.sec;
+	data[5] = byte t.min;
+	data[4] = byte t.hour;
+	data[3] = byte t.mday;
+	data[2] = byte (t.mon + 1);
+	data[0] = byte ((y >> 8) & 16rff);
+	data[1] = byte (y & 16rff);
+	return data;
+}
--- /dev/null
+++ b/appl/cmd/palm/desklink.m
@@ -1,0 +1,90 @@
+
+#
+# desktop/Pilot link protocol
+#
+
+Desklink: module {
+
+	PATH1:	con "/dis/palm/desklink.dis";
+
+	User: adt {
+		userid:	int;
+		viewerid:	int;
+		lastsyncpc:	int;
+		succsynctime:	int;
+		lastsynctime:	int;
+		username: string;
+		password:	array of byte;
+	};
+
+	SysInfo: adt {
+		romversion:	int;
+		locale:	int;
+		product:	string;
+	};
+
+	CardInfo: adt {
+		cardno:	int;
+		version:	int;
+		creation:	int;
+		romsize:	int;
+		ramsize:	int;
+		ramfree:	int;
+		name:	string;
+		maker:	string;
+	};
+
+	connect:	fn(srvfile: string): (Palmdb, string);
+	hangup:	fn(): int;
+
+	#
+	# Desk Link Protocol functions (usually with the same names as in PalmOS)
+	#
+
+	ReadUserInfo:	fn(): ref User;
+	WriteUserInfo:	fn(u: ref User, flags: int): int;
+
+	# WriteUserInfo update flags
+	UserInfoModUserID:	con 16r80;
+	UserInfoModSyncPC:	con 16r40;
+	UserInfoModSyncDate:	con 16r20;
+	UserInfoModName:	con 16r10;
+	UserInfoModViewerID:	con 16r08;
+
+	ReadSysInfo:	fn(): ref SysInfo;
+	ReadSysInfoVer:	fn(): (int, int, int);	# DLP 1.2
+
+	GetSysDateTime:	fn(): int;
+	SetSysDateTime:	fn(nil: int): int;
+
+	ReadStorageInfo:	fn(cardno: int): (array of ref CardInfo, int, string);
+	ReadDBCount:		fn(cardno: int): (int, int);
+
+	ReadDBList:	fn(cardno: int, flags: int, start: int): (array of ref Palm->DBInfo, int, string);	# flags must contain DBListRAM and/or DBListROM
+	FindDBInfo:	fn(cardno: int, start: int, name: string, dtype, creator: string): ref Palm->DBInfo;
+
+	# list location and options
+	DBListRAM:	con 16r80;
+	DBListROM:	con 16r40;
+	DBListMultiple:	con 16r20;	# ok to return multiple entries
+
+	# OpenDB, CreateDB, ReadAppBlock, ... ResetSyncFlags, ReadOpenDBInfo, MoveCategory are functions in DB
+	CloseDB_All:	fn(): int;
+	DeleteDB:		fn(name: string): int;
+
+	ResetSystem:	fn(): int;
+
+	OpenConduit:	fn(): int;
+	EndOfSync:	fn(status: int): int;
+
+	# EndOfSync status parameter
+	SyncNormal, SyncOutOfMemory, SyncCancelled, SyncError, SyncIncompatible:	con iota;
+
+	AddSyncLogEntry:	fn(entry: string): int;
+
+	#
+	# Palmdb implementation
+	#
+
+	init:	fn(m: Palm): string;
+};
--- /dev/null
+++ b/appl/cmd/palm/mkfile
@@ -1,0 +1,16 @@
+<../../../mkconfig
+
+TARG=\
+	palmsrv.dis\
+	desklink.dis\
+	connex.dis\
+
+MODULES=\
+	desklink.m\
+
+SYSMODULES=\
+	palm.m\
+
+DISBIN=$ROOT/dis/palm
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/palm/palmsrv.b
@@ -1,0 +1,901 @@
+implement Palmsrv;
+
+#
+# serve up a Palm using SLP and PADP
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# forsyth@vitanuova.com
+#
+# TO DO
+#	USB and possibly other transports
+#	tickle
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "timers.m";
+	timers: Timers;
+	Timer, Sec: import timers;
+
+include "palm.m";
+
+include "arg.m";
+
+Palmsrv: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+debug := 0;
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: palm/palmsrv [-d /dev/eia0] [-s 57600]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD, nil);
+
+	device, speed: string;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		error(sys->sprint("can't load %s: %r", Arg->PATH));
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		'D' =>
+			debug++;
+		'd' =>
+			device = arg->arg();
+		's' =>
+			speed = arg->arg();
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(device == nil)
+		device = "/dev/eia0";
+	if(speed == nil)
+		speed = "57600";
+
+	dfd := sys->open(device, Sys->ORDWR);
+	if(dfd == nil)
+		error(sys->sprint("can't open %s: %r", device));
+	cfd := sys->open(device+"ctl", Sys->OWRITE);
+
+	timers = load Timers Timers->PATH;
+	if(timers == nil)
+		error(sys->sprint("can't load %s: %r", Timers->PATH));
+	srvio := sys->file2chan("/chan", "palmsrv");
+	if(srvio == nil)
+		error(sys->sprint("can't create channel /chan/palmsrv: %r"));
+	timers->init(Sec/100);
+	p := Pchan.init(dfd, cfd);
+	spawn server(srvio, p);
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "palmsrv: %s\n", s);
+	raise "fail:error";
+}
+
+Xact: adt
+{
+	fid:	int;
+	reply:	array of byte;
+	error:	string;
+};
+
+server(srv: ref Sys->FileIO, p: ref Pchan)
+{
+	actions: list of ref Xact;
+	nuser := 0;
+	for(;;)alt{
+	(nil, nbytes, fid, rc) := <-srv.read =>
+		if(rc == nil){
+			actions = delact(actions, fid);
+			break;
+		}
+		act := findact(actions, fid);
+		if(act == nil){
+			rc <-= (nil, "no transaction in progress");
+			break;
+		}
+		actions = delact(actions, fid);
+		if(p.shutdown)
+			rc <-= (nil, "link shut down");
+		else if(act.error != nil)
+			rc <-= (nil, act.error);
+		else if(act.reply != nil)
+			rc <-= (act.reply, nil);
+		else
+			rc <-= (nil, "no reply");	# probably shouldn't happen
+
+	(nil, data, fid, wc) := <-srv.write =>
+		actions = delact(actions, fid);	# discard result of any previous transaction
+		if(wc == nil){
+			if(--nuser <= 0){
+				nuser = 0;
+				p.stop();
+			}
+			break;
+		}
+		if(len data == 4 && string data == "exit"){
+			p.close();
+			wc <-= (len data, nil);
+			exit;
+		}
+		if(p.shutdown){
+			wc <-= (0, "link shut down");	# must close then reopen
+			break;
+		}
+		if(!p.started){
+			err := p.start();
+			if(err != nil){
+				wc <-= (0, sys->sprint("can't start protocol: %s", err));
+				break;
+			}
+			nuser++;
+		}
+		(result, err) := p.padp_xchg(data, 20*1000);
+		if(err != nil){
+			wc <-= (0, err);
+			break;
+		}
+		actions = ref Xact(fid, result, err) :: actions;
+		wc <-= (len data, nil);
+	}
+}
+
+findact(l: list of ref Xact, fid: int): ref Xact
+{
+	for(; l != nil; l = tl l)
+		if((a := hd l).fid == fid)
+			return a;
+	return nil;
+}
+
+delact(l: list of ref Xact, fid: int): list of ref Xact
+{
+	ol := l;
+	l = nil;
+	for(; ol != nil; ol = tl ol)
+		if((a := hd ol).fid != fid)
+			l = a :: l;
+	return l;
+}
+
+killpid(pid: int)
+{
+	if(pid != 0){
+		fd := sys->open("/prog/"+string pid+"/ctl", sys->OWRITE);
+		if(fd != nil)
+			sys->fprint(fd, "kill");
+	}
+}
+
+#
+# protocol implementation
+#	Serial Link Protocol (framing)
+#	Connection Management Protocol (wakeup, negotiation)
+#	Packet Assembly/Disassembly Protocol (reliable delivery fragmented datagram)
+#
+
+DATALIM: con 1024;
+
+# SLP packet types
+SLP_System, SLP_Unused, SLP_PAD, SLP_Loop: con iota;
+
+# SLP block content, without framing
+Sblock: adt {
+	src:	int;	# socket ID
+	dst:	int;	# socket ID
+	proto:	int;	# packet type
+	xid:	int;	# transaction ID
+	data:	array of byte;
+
+	new:	fn(): ref Sblock;
+	print:	fn(sb: self ref Sblock, dir: string);
+};
+
+#
+# Palm channel
+#
+Pchan: adt {
+	started:	int;
+	shutdown:	int;
+
+	protocol:	int;
+	lport:	byte;
+	rport:	byte;
+
+	fd:	ref Sys->FD;
+	cfd:	ref Sys->FD;
+	baud:	int;
+
+	rpid:	int;
+	lastid:	int;
+	rd:	chan of ref Sblock;
+	reply:	ref Sblock;	# data replacing lost ack
+
+	init:	fn(dfd: ref Sys->FD, cfd: ref Sys->FD): ref Pchan;
+	start:	fn(p: self ref Pchan): string;
+	stop:	fn(p: self ref Pchan);
+	close:	fn(p: self ref Pchan): int;
+	slp_read:	fn(p: self ref Pchan, nil: int): (ref Sblock, string);
+	slp_write:	fn(p: self ref Pchan, xid: int, nil: array of byte): string;
+
+	setbaud:	fn(p: self ref Pchan, nil: int);
+
+	padp_read:	fn(p: self ref Pchan, xid: int, timeout: int): (array of byte, string);
+	padp_write:	fn(p: self ref Pchan, msg: array of byte, xid: int): string;
+	padp_xchg:	fn(p: self ref Pchan, msg: array of byte, timeout: int): (array of byte, string);
+	tickle:	fn(p: self ref Pchan);
+
+	connect:	fn(p: self ref Pchan): string;
+	accept:	fn(p: self ref Pchan, baud: int): string;
+
+	nextseq:	fn(p: self ref Pchan): int;
+};
+
+Pchan.init(dfd: ref Sys->FD, cfd: ref Sys->FD): ref Pchan
+{
+	p := ref Pchan;
+	p.fd = dfd;
+	p.cfd = cfd;
+	p.baud = InitBaud;
+	p.protocol = SLP_PAD;
+	p.rport = byte 3;
+	p.lport = byte 3;
+	p.rd = chan of ref Sblock;
+	p.lastid = 0;
+	p.rpid = 0;
+	p.started = 0;
+	p.shutdown = 0;
+	return p;
+}
+
+Pchan.start(p: self ref Pchan): string
+{
+	if(p.started)
+		return nil;
+	p.shutdown = 0;
+	p.baud = InitBaud;
+	p.reply = nil;
+	ctl(p, "f");
+	ctl(p, "d1");
+	ctl(p, "r1");
+	ctl(p, "i8");
+	ctl(p, "q8192");
+	ctl(p, sys->sprint("b%d", InitBaud));
+	pidc := chan of int;
+	spawn slp_recv(p, pidc);
+	p.started = 1;
+	p.rpid = <-pidc;
+	err := p.accept(57600);
+	if(err != nil)
+		p.stop();
+	return err;
+}
+
+ctl(p: ref Pchan, s: string)
+{
+	if(p.cfd != nil)
+		sys->fprint(p.cfd, "%s", s);
+}
+
+Pchan.setbaud(p: self ref Pchan, baud: int)
+{
+	if(p.baud != baud){
+		p.baud = baud;
+		ctl(p, sys->sprint("b%d", baud));
+		sys->sleep(200);
+	}
+}
+
+Pchan.stop(p: self ref Pchan)
+{
+	p.shutdown = 0;
+	if(!p.started)
+		return;
+	killpid(p.rpid);
+	p.rpid = 0;
+	p.reply = nil;
+#	ctl(p, "f");
+#	ctl(p, "d0");
+#	ctl(p, "r0");
+#	ctl(p, sys->sprint("b%d", InitBaud));
+	p.started = 0;
+}
+	
+Pchan.close(p: self ref Pchan): int
+{
+	if(p.started)
+		p.stop();
+	p.reply = nil;
+	p.cfd = nil;
+	p.fd = nil;
+	timers->shutdown();
+	return 0;
+}
+
+# CMP protocol for connection management
+#	See include/Core/System/CMCommon.h, Palm SDK
+# There are two major versions: the original V1, still always used in wakeup messsages;
+# and V2, which is completely different (similar structure to Desklink) and used by newer devices, but the headers
+# are the same length.  Start off in V1 announcing version 2.x, then switch to that.
+# My device supports only V1, so I use that.
+
+CMPHDRLEN: con 10;	# V1: type[1] flags[1] vermajor[1] verminor[1] mbz[2] baud[4]
+					# V2: type[1] cmd[1] error[2] argc[1] mbz[1] mbz[4]
+
+# CMP V1
+Cmajor:	con 1;
+Cminor:	con 2;
+
+InitBaud: con 9600;
+
+# type
+Cwake, Cinit, Cabort, Cextended: con 1+iota;
+
+# Cinit flags
+ChangeBaud: con 16r80;
+RcvTimeout1: con 16r40;	# tell Palm to set receive timeout to 1 minute (CMP v1.1)
+RcvTimeout2:	con 16r20;	# tell Palm to set receive timeout to 2 minutes (v1.1)
+
+# Cinit and Cwake flag
+LongPacketEnable:	con 16r10;	# enable long packet support (v1.2)
+
+# Cabort flags
+WrongVersion:	con 16r80;	# incompatible com versions
+
+# CMP V2
+Carg1:		con Palm->ArgIDbase;
+Cresponse:	con 16r80;
+Cxchgprefs, Chandshake:	con 16r10+iota;
+
+Pchan.connect(p: self ref Pchan): string
+{
+	(nil, e1) := cmp_write(p, Cwake, 0, Cmajor, Cminor, 57600);
+	if(e1 != nil)
+		return e1;
+	(op, flag, nil, nil, baud, e2) := cmp_read(p, 0);
+	if(e2 != nil)
+		return e2;
+	case op {
+	Cinit=>
+		if(flag & ChangeBaud)
+			p.setbaud(baud);
+		return nil;
+
+	Cabort=>
+		return "Palm rejected connect";
+
+	* =>
+		return sys->sprint("Palm connect: reply %d", op);
+	}
+	return nil;
+}
+
+Pchan.accept(p: self ref Pchan, maxbaud: int): string
+{
+	(op, nil, major, minor, baud, err) := cmp_read(p, 0);
+	if(err != nil)
+		return err;
+	if(major != 1){
+		sys->fprint(sys->fildes(2), "palmsrv: comm version mismatch: %d.%d\n", major, minor);
+		cmp_write(p, Cabort, WrongVersion, Cmajor, 0, 0);
+		return sys->sprint("comm version mismatch: %d.%d", major, minor);
+	}
+	if(baud > maxbaud)
+		baud = maxbaud;
+	flag := 0;
+	if(baud != InitBaud)
+		flag = ChangeBaud;
+	(nil, err) = cmp_write(p, Cinit, flag, Cmajor, Cminor, baud);
+	if(err != nil)
+		return err;
+	p.setbaud(baud);
+	return nil;
+}
+
+cmp_write(p: ref Pchan, op: int, flag: int, major: int, minor: int, baud: int): (int, string)
+{
+	cmpbuf := array[CMPHDRLEN] of byte;
+	cmpbuf[0] = byte op;
+	cmpbuf[1] = byte flag;
+	cmpbuf[2] = byte major;
+	cmpbuf[3] = byte minor;
+	cmpbuf[4] = byte 0;
+	cmpbuf[5] = byte 0;
+	put4(cmpbuf[6:], baud);
+
+	if(op == Cwake)
+		return (16rFF, p.padp_write(cmpbuf, 16rFF));
+	xid := p.nextseq();
+	return (xid, p.padp_write(cmpbuf, xid));
+}
+
+cmp_read(p: ref Pchan, xid: int): (int, int, int, int, int, string)
+{
+	(c, err) := p.padp_read(xid, 20*Sec);
+	if(err != nil)
+		return (0, 0, 0, 0, 0, err);
+	if(len c != CMPHDRLEN)
+		return (0, 0, 0, 0, 0, "CMP: bad response");
+	return (int c[0], int c[1], int c[2], int c[3], get4(c[6:]), nil);
+}
+
+#
+# Palm PADP protocol
+#	``The Packet Assembly/Disassembly Protocol'' in
+#	Developing Palm OS Communications, US Robotics, 1996, pp. 53-68.
+#
+# forsyth@caldo.demon.co.uk, 1997
+#
+
+FIRST: con 16r80;
+LAST: con 16r40;
+MEMERROR: con 16r20;
+
+# packet types
+Pdata: con 1;
+Pack: con 2;
+Ptickle: con 4;
+Pabort: con 8;
+
+PADPHDRLEN: con 4;	# type[1] flags[1] size[2]
+
+RetryInterval: con 4*Sec;
+MaxRetries: con 14; # they say 14 `seconds', but later state they might need 20 for heap mgmt, so i'll assume 14 attempts (at 4sec ea)
+
+Pchan.padp_xchg(p: self ref Pchan, msg: array of byte, timeout: int): (array of byte, string)
+{
+	xid := p.nextseq();
+	err := p.padp_write(msg, xid);
+	if(err != nil)
+		return (nil, err);
+	return p.padp_read(xid, timeout);
+}
+
+#
+# PADP header
+#	type[1] flags[2] size[2], high byte first for size
+#
+# max block size is 2^16-1
+# must ack within 2 seconds
+# wait at most 10 seconds for next chunk
+# 10 retries
+#
+
+Pchan.padp_write(p: self ref Pchan, buf: array of byte, xid: int): string
+{
+	count := len buf;
+	if(count >= 1<<16)
+		return "padp: write too big";
+	p.reply = nil;
+	flags := FIRST;
+	mem := buf[0:];
+	offset := 0;
+	while(count > 0){
+		n := count;
+		if(n > DATALIM)
+			n = DATALIM;
+		else
+			flags |= LAST;
+		ob := array[PADPHDRLEN+n] of byte;
+		ob[0] = byte Pdata;
+		ob[1] = byte flags;
+		l: int;
+		if(flags & FIRST)
+			l = count;	# total size in first segment
+		else
+			l = offset;	# offset in rest
+		put2(ob[2:], l);
+		ob[PADPHDRLEN:] = mem[0:n];
+		if(debug)
+			padp_dump(ob, "Tx");
+		p.slp_write(xid, ob);
+		retries := 0;
+		for(;;){
+			(ib, nil) := p.slp_read(RetryInterval);
+			if(ib == nil){
+				sys->print("padp write: ack timeout\n");
+				retries++;
+				if(retries > MaxRetries){
+					# USR says not to give up if (flags&LAST)!=0; giving up seems safer
+					sys->print("padp write: give up\n");
+					return "PADP: no response";
+				}
+				p.slp_write(xid, ob);
+				continue;
+			}
+			if(ib.proto != SLP_PAD || len ib.data < PADPHDRLEN || ib.xid != xid && ib.xid != 16rFF){
+				sys->print("padp write: ack wrong type(%d) or xid(%d,%d), or len %d\n", ib.proto, ib.xid, xid, len ib.data);
+				continue;
+			}
+			if(ib.xid == 16rFF){	# connection management
+				if(int ib.data[0] == Ptickle)
+					continue;
+				if(int ib.data[0] == Pabort){
+					sys->print("padp write: device abort\n");
+					p.shutdown = 1;
+					return "device cancelled operation";
+				}
+			}
+			if(int ib.data[0] != Pack){
+				if(int ib.data[0] == Ptickle)
+					continue;
+				# right transaction ... if it's acceptable data, USR says to save it & treat as ack
+				sys->print("padp write: type %d, not ack\n", int ib.data[0]);
+				if(int ib.data[0] == Pdata && flags & LAST && int ib.data[1] & FIRST){
+					p.reply = ib;
+					break;
+				}
+				continue;
+			}
+			if(int ib.data[1] & MEMERROR)
+				return "padp: pilot out of memory";
+			if((flags&(FIRST|LAST)) != (int ib.data[1]&(FIRST|LAST)) ||
+			    get2(ib.data[2:]) != get2(ob[2:])){
+				sys->print("padp write: ack, wrong flags (#%x,#%x) or offset (%d,%d)\n", int ib.data[1], flags, get2(ib.data[2:]), get2(ob[2:]));
+				continue;
+			}
+			if(debug)
+				sys->print("padp write: ack %d %d\n", xid, get2(ob[2:]));
+			break;
+		}
+		mem = mem[n:];
+		count -= n;
+		offset += n;
+		flags &= ~FIRST;
+	}
+	return nil;
+}
+
+Pchan.padp_read(p: self ref Pchan,  xid, timeout: int): (array of byte, string)
+{
+	buf, mem: array of byte;
+
+	offset := 0;
+	ready := 0;
+	retries := 0;
+	ack := array[PADPHDRLEN] of byte;
+	for(;;){
+		b := p.reply;
+		if(b == nil){
+			err: string;
+			(b, err) = p.slp_read(timeout);
+			if(b == nil){
+				sys->print("padp read: timeout %d\n", retries);
+				if(++retries <= 5)
+					continue;
+				sys->print("padp read: gave up\n");
+				return (nil, err);
+			}
+			retries = 0;
+		} else
+			p.reply = nil;
+		if(debug)
+			padp_dump(b.data, "Rx");
+ 		if(len b.data < PADPHDRLEN){
+			sys->print("padp read: length\n");
+			continue;
+		}
+		if(b.proto != SLP_PAD){
+			sys->print("padp read: bad proto (%d)\n", b.proto);
+			continue;
+		}
+		if(int b.data[0] == Pabort && b.xid == 16rFF){
+			p.shutdown = 1;
+			return (nil, "device cancelled transaction");
+		}
+		if(int b.data[0] != Pdata || xid != 0 && b.xid != xid){
+			sys->print("padp read mismatch: type (%d) or xid(%d::%d)\n", int b.data[0], b.xid, xid);
+			continue;
+		}
+		f := int b.data[1];
+		o := get2(b.data[2:]);
+		if(f & FIRST){
+			buf = array[o] of byte;
+			ready = 1;
+			offset = 0;
+			o = 0;
+			mem = buf;
+			timeout = 4*Sec;
+		}
+		if(!ready || o != offset){
+			sys->print("padp read: offset %d, expected %d\n", o, offset);
+			continue;
+		}
+		n := len b.data - PADPHDRLEN;
+		if(n > len mem){
+			sys->print("padp read: record too long (%d/%d)\n", n, len mem);
+			# it's probably fatal, but retrying does no harm
+			continue;
+		}
+		mem[0:] = b.data[PADPHDRLEN:PADPHDRLEN+n];
+		mem = mem[n:];
+		offset += n;
+		ack[0:] = b.data[0:PADPHDRLEN];
+		ack[0] = byte Pack;
+		p.slp_write(xid, ack);
+		if(f & LAST)
+			break;
+	}
+	if(offset != len buf)
+		return (buf[0:offset], nil);
+	return (buf, nil);
+}
+
+Pchan.nextseq(p: self ref Pchan): int
+{
+	n := p.lastid + 1;
+	if(n >= 16rFF)
+		n = 1;
+	p.lastid = n;
+	return n;
+}
+
+Pchan.tickle(p: self ref Pchan)
+{
+	xid := p.nextseq();
+	data := array[PADPHDRLEN] of byte;
+	data[0] = byte Ptickle;
+	data[1] = byte (FIRST|LAST);
+	put2(data[2:], 0);
+	if(debug)
+		sys->print("PADP: tickle\n");
+	p.slp_write(xid, data);
+}
+
+padp_dump(data: array of byte, dir: string)
+{
+	stype: string;
+
+	case int data[0] {
+	Pdata =>	stype = "Data";
+	Pack =>	stype = "Ack";
+	Ptickle =>	stype = "Tickle";
+	Pabort =>	stype = "Abort";
+	* =>	stype = sys->sprint("#%x", int data[0]);
+	}
+
+	sys->print("PADP %s %s flags=#%x len=%d\n", stype, dir, int data[1], get2(data[2:]));
+
+	if(debug > 1 && (data[0] != byte Pack || len data > 4)){
+		data = data[4:];
+		for(i := 0; i < len data;){
+			sys->print(" %.2x", int data[i]);
+			if(++i%16 == 0)
+				sys->print("\n");
+		}
+		sys->print("\n");
+	}
+}
+
+#
+# Palm's Serial Link Protocol
+#	See include/Core/System/SerialLinkMgr.h in Palm SDK
+# 	and the description in the USR document mentioned above.
+#
+
+SLPHDRLEN: con 10;		# BE[1] EF[1] ED[1] dest[1] src[1] type[1] size[2] xid[1] check[1] body[size] crc[2]
+SLP_MTU: con SLPHDRLEN+PADPHDRLEN+DATALIM;
+
+Sblock.new(): ref Sblock
+{
+	return ref Sblock(0, 0, 0, 16rFF, nil);
+}
+
+#
+# format and write an SLP frame
+#
+Pchan.slp_write(p: self ref Pchan, xid: int, b: array of byte): string
+{
+	d := array[SLPHDRLEN] of byte;
+	cb := array[2] of byte;
+
+	nb := len b;
+	d[0] = byte 16rBE;
+	d[1] = byte 16rEF;
+	d[2] = byte 16rED;
+	d[3] = byte p.rport;
+	d[4] = byte p.lport;
+	d[5] = byte p.protocol;
+	d[6] = byte (nb >> 8);
+	d[7] = byte (nb & 16rFF);
+	d[8] = byte xid;
+	d[9] = byte 0;
+	n := 0;
+	for(i:=0; i<len d; i++)
+		n += int d[i];
+	d[9] = byte (n & 16rFF);
+	if(debug)
+		printbytes(d, "SLP Tx hdr");
+	crc := crc16(d, 0);
+	put2(cb, crc16(b, crc));
+
+	if(sys->write(p.fd, d, SLPHDRLEN) != SLPHDRLEN ||
+	   sys->write(p.fd, b, nb) != len b ||
+	   sys->write(p.fd, cb, 2) != 2)
+		return sys->sprint("%r");
+	return nil;
+}
+
+Pchan.slp_read(p: self ref Pchan, timeout: int): (ref Sblock, string)
+{
+	clock := Timer.start(timeout);
+	alt {
+	<-clock.timeout =>
+		if(debug)
+			sys->print("SLP: timeout\n");
+		return (nil, "SLP: timeout");
+	b := <-p.rd =>
+		clock.stop();
+		return (b, nil);
+	}
+}
+
+slp_recv(p: ref Pchan, pidc: chan of int)
+{
+	n: int;
+
+	pidc <-= sys->pctl(0, nil);
+	buf := array[2*SLP_MTU] of byte;
+	sb := Sblock.new();
+	rd := wr := 0;
+Work:
+	for(;;){
+
+		if(wr != rd){
+			# data already in buffer might start a new frame
+			if(rd != 0){
+				buf[0:] = buf[rd:wr];
+				wr -= rd;
+				rd = 0;
+			}
+		}else
+			rd = wr = 0;
+
+		# header
+		while(wr < SLPHDRLEN){
+			n = sys->read(p.fd, buf[wr:], SLPHDRLEN-wr);
+			if(n <= 0)
+				break Work;
+			wr += n;
+		}
+#		{for(i:=0; i<wr;i++)sys->print("%.2x", int buf[i]);sys->print("\n");}
+		if(buf[0] != byte 16rBE || buf[1] != byte 16rEF || buf[2] != byte 16rED){
+			rd++;
+			continue;
+		}
+		if(debug)
+			printbytes(buf[0:wr], "SLP Rx hdr");
+		n = 0;
+		for(i:=0; i<SLPHDRLEN-1; i++)
+			n += int buf[i];
+		if((n & 16rFF) != int buf[9]){
+			rd += 3;
+			continue;
+		}
+		hdr := buf[0:SLPHDRLEN];
+		sb.dst = int hdr[3];
+		sb.src = int hdr[4];
+		sb.proto = int hdr[5];
+		size := (int hdr[6]<<8) | int hdr[7];
+		sb.xid = int hdr[8];
+		sb.data = array[size] of byte;
+		crc := crc16(hdr, 0);
+		rd += SLPHDRLEN;
+		if(rd == wr)
+			rd = wr = 0;
+
+		# data and CRC
+		while(wr-rd < size+2){
+			n = sys->read(p.fd, buf[wr:], size+2-(wr-rd));
+			if(n <= 0)
+				break Work;
+			wr += n;
+		}
+		crc = crc16(buf[rd:rd+size], crc);
+		if(crc != get2(buf[rd+size:])){
+			if(debug)
+				sys->print("CRC error: local=#%.4ux pilot=#%.4ux\n", crc, get2(buf[rd+size:]));
+			for(; rd < wr && buf[rd] != byte 16rBE; rd++)
+				;	# hunt for next header
+			continue;
+		}
+		if(sb.proto != SLP_Loop){
+			sb.data[0:] = buf[rd:rd+size];
+			if(debug)
+				sb.print("Rx");
+			rd += size+2;
+			p.rd <-= sb;
+			sb = Sblock.new();
+		} else {
+			# should we reflect these?
+			if(debug)
+				sb.print("Loop");
+			rd += size+2;
+		}
+	}
+	p.rd <-= nil;
+}
+
+Sblock.print(b: self ref Sblock, dir: string)
+{
+	sys->print("SLP %s %d->%d len=%d proto=%d xid=#%.2x\n",
+			dir, int b.src, int b.dst, len b.data, int b.proto, int b.xid);
+}
+
+printbytes(d: array of byte, what: string)
+{
+	buf := sys->sprint("%s[", what);
+	for(i:=0; i<len d; i++)
+		buf += sys->sprint(" #%.2x", int d[i]);
+	buf += "]";
+	sys->print("%s\n", buf);
+}
+
+get4(p: array of byte): int
+{
+	return (int p[0]<<24) | (int p[1]<<16) | (int p[2]<<8) | int p[3];
+}
+
+get3(p: array of byte): int
+{
+	return (int p[1]<<16) | (int p[2]<<8) | int p[3];
+}
+
+get2(p: array of byte): int
+{
+	return (int p[0]<<8) | int p[1];
+}
+
+put4(p: array of byte, v: int)
+{
+	p[0] = byte (v>>24);
+	p[1] = byte (v>>16);
+	p[2] = byte (v>>8);
+	p[3] = byte (v & 16rFF);
+}
+
+put3(p: array of byte, v: int)
+{
+	p[0] = byte (v>>16);
+	p[1] = byte (v>>8);
+	p[2] = byte (v & 16rFF);
+}
+
+put2(p: array of byte, v: int)
+{
+	p[0] = byte (v>>8);
+	p[1] = byte (v & 16rFF);
+}
+
+# this will be done by table look up;
+# polynomial is xⁱ⁶+xⁱ⁲+x⁵+1
+
+crc16(buf: array of byte, crc: int): int
+{
+	for(j := 0; j < len buf; j++){
+		crc = crc ^ (int buf[j]) << 8;
+		for(i := 0; i < 8; i++)
+			if(crc & 16r8000)
+				crc = (crc << 1) ^ 16r1021;
+			else
+				crc = crc << 1;
+	}
+	return crc & 16rffff;
+}
--- /dev/null
+++ b/appl/cmd/pause.b
@@ -1,0 +1,17 @@
+implement Pause;
+#
+# init program to do nothing but pause
+#
+
+include "sys.m";
+include "draw.m";
+
+Pause: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	<-chan of int;
+}
--- /dev/null
+++ b/appl/cmd/plumb.b
@@ -1,0 +1,128 @@
+implement Plumb;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+include "plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg, Attr: import plumbmsg;
+
+include "workdir.m";
+	workdir: Workdir;
+
+Plumb: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	plumbmsg = load Plumbmsg Plumbmsg->PATH;
+	if(plumbmsg == nil)
+		nomod(Plumbmsg->PATH);
+	workdir = load Workdir Workdir->PATH;
+	if(workdir == nil)
+		nomod(Workdir->PATH);
+
+	if(plumbmsg->init(1, nil, 0) < 0)
+		err(sys->sprint("can't connect to plumb: %r"));
+
+	attrs: list of ref Attr;
+	input := 0;
+	m := ref Msg("plumb", nil, workdir->init(), "text", nil, nil);
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("plumb [-s src] [-d dest] [-w wdir] [-t type] [-a name val] -i | ... data ...");
+	while((c := arg->opt()) != 0)
+		case c {
+		's' =>
+			m.src = arg->earg();
+		'd' =>
+			m.dst = arg->earg();
+		'w' or 'D' =>
+			m.dir = arg->earg();
+		'i' =>
+			input++;
+		't' or 'k'=>
+			m.kind = arg->arg();
+		'a' =>
+			name := arg->earg();
+			val := arg->earg();
+			attrs = tack(attrs, ref Attr(name, val));
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(input && args != nil || !input && args == nil)
+		arg->usage();
+	arg = nil;
+
+	if(input){
+		m.data = gather(sys->fildes(0));
+		(notfound, nil) := plumbmsg->lookup(plumbmsg->string2attrs(m.attr), "action");
+		if(notfound)
+			tack(attrs, ref Attr("action", "showdata"));
+		m.attr = plumbmsg->attrs2string(attrs);
+		if(m.send() < 0)
+			err(sys->sprint("can't send message: %r"));
+		exit;
+	}
+	
+	nb := 0;
+	for(a := args; a != nil; a = tl a)
+		nb += len array of byte hd a;
+	nb += len args;
+	buf := array[nb] of byte;
+	nb = 0;
+	for(a = args; a != nil; a = tl a){
+		b := array of byte hd a;
+		buf[nb++] = byte ' ';
+		buf[nb:] = b;
+		nb += len b;
+	}
+	m.data = buf[1:];
+	m.attr = plumbmsg->attrs2string(attrs);
+	if(m.send() < 0)
+		err(sys->sprint("can't plumb message: %r"));
+}
+
+gather(fd: ref Sys->FD): array of byte
+{
+	Chunk: con 8192;	# arbitrary
+	ndata := 0;
+	buf := array[Chunk] of byte;
+	while((n := sys->read(fd, buf[ndata:], len buf - ndata)) > 0){
+		ndata += n;
+		if(len buf - ndata < Chunk){
+			t := array[len buf+Chunk] of byte;
+			t[0:] = buf[0: ndata];
+			buf = t;
+		}
+	}
+	if(n < 0)
+		err(sys->sprint("error reading input: %r"));
+	return buf[0: ndata];
+}
+
+tack(l: list of ref Attr, v: ref Attr): list of ref Attr
+{
+	if(l == nil)
+		return v :: nil;
+	return hd l :: tack(tl l, v);
+}
+
+nomod(m: string)
+{
+	err(sys->sprint("can't load %s: %r", m));
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "plumb: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/plumber.b
@@ -1,0 +1,766 @@
+implement Plumber;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "sh.m";
+
+include "regex.m";
+	regex: Regex;
+
+include "string.m";
+	str: String;
+
+include "../lib/plumbing.m";
+	plumbing: Plumbing;
+	Pattern, Rule: import plumbing;
+
+include "plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg, Attr: import plumbmsg;
+
+include "arg.m";
+
+Plumber: module
+{
+	init:	fn(ctxt: ref Draw->Context, argl: list of string);
+};
+
+Input: adt
+{
+	inc:		chan of ref Inmesg;
+	resc:		chan of int;
+	io:		ref Sys->FileIO;
+};
+
+Output: adt
+{
+	name:	string;
+	outc:		chan of string;
+	io:		ref Sys->FileIO;
+	queue:	list of array of byte;
+	started:	int;
+	startup:	string;
+	waiting:	int;
+};
+
+Port: adt
+{
+	name:		string;
+	startup:	string;
+	alwaysstart:	int;
+};
+
+Match: adt
+{
+	p0, p1:	int;
+};
+
+Inmesg: adt
+{
+	msg:		ref Msg;
+	text:		string;	# if kind is text
+	p0,p1:	int;
+	match:	array of Match;
+	port:		int;
+	startup:	string;
+	args:		list of string;
+	attrs:		list of ref Attr;
+	clearclick:	int;
+	set:		int;
+	# $ arguments
+	_n:		array of string;
+	_dir:		string;
+	_file:		string;
+};
+
+# Message status after processing
+HANDLED: con -1;
+UNKNOWN: con -2;
+NOTSTARTED: con -3;
+
+output: array of ref Output;
+
+input: ref Input;
+
+stderr: ref Sys->FD;
+pgrp: int;
+rules: list of ref Rule;
+titlectl: chan of string;
+ports: list of ref Port;
+wmstartup := 0;
+wmchan := "/chan/wm";
+verbose := 0;
+
+context: ref Draw->Context;
+
+usage()
+{
+	sys->fprint(stderr, "Usage: plumb [-vw] [-c wmchan] [initfile ...]\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	context = ctxt;
+
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	stderr = sys->fildes(2);
+
+	regex = load Regex Regex->PATH;
+	plumbing = load Plumbing Plumbing->PATH;
+	str = load String String->PATH;
+
+	err: string;
+	nogrp := 0;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'w' =>
+			wmstartup = 1;
+		'c' =>
+			if ((wmchan = arg->arg()) == nil)
+				usage();
+		'v' =>
+			verbose = 1;
+		'n' =>
+			nogrp = 1;
+		* =>
+			usage();
+		}
+	}
+	args = arg->argv();
+	arg = nil;
+
+	(rules, err) = plumbing->init(regex, args);
+	if(err != nil){
+		sys->fprint(stderr, "plumb: %s\n", err);
+		raise "fail:init";
+	}
+
+	plumbmsg = load Plumbmsg Plumbmsg->PATH;
+	plumbmsg->init(0, nil, 0);
+
+	if(nogrp)
+		pgrp = sys->pctl(0, nil);
+	else
+		pgrp = sys->pctl(sys->NEWPGRP, nil);
+
+	r := rules;
+	for(i:=0; i<len rules; i++){
+		rule := hd r;
+		r = tl r;
+		for(j:=0; j<len rule.action; j++)
+			if(rule.action[j].pred == "to" || rule.action[j].pred == "alwaysstart"){
+				p := findport(rule.action[j].arg);
+				if(p == nil){
+					p = ref Port(rule.action[j].arg, nil, rule.action[j].pred == "alwaysstart");
+					ports = p :: ports;
+				}
+				for(k:=0; k<len rule.action; k++)
+					if(rule.action[k].pred == "start")
+						p.startup = rule.action[k].arg;
+				break;
+			}
+	}
+
+	input = ref Input;
+	input.io = makefile("plumb.input");
+	if(input.io == nil)
+		shutdown();
+	input.inc = chan of ref Inmesg;
+	input.resc = chan of int;
+	spawn receiver(input);
+
+	output = array[len ports] of ref Output;
+
+	pp := ports;
+	for(i=0; i<len output; i++){
+		p := hd pp;
+		pp = tl pp;
+		output[i] = ref Output;
+		output[i].name = p.name;
+		output[i].io = makefile("plumb."+p.name);
+		if(output[i].io == nil)
+			shutdown();
+		output[i].outc = chan of string;
+		output[i].started = 0;
+		output[i].startup = p.startup;
+		output[i].waiting = 0;
+	}
+
+	# spawn so we return without needing to run plumb in background
+	spawn sender(input, output);
+}
+
+findport(name: string): ref Port
+{
+	for(p:=ports; p!=nil; p=tl p)
+		if((hd p).name == name)
+			return hd p;
+	return nil;
+}
+
+makefile(file: string): ref Sys->FileIO
+{
+	io := sys->file2chan("/chan", file);
+	if(io == nil){
+		sys->fprint(stderr, "plumb: can't establish /chan/%s: %r\n", file);
+		return nil;
+	}
+	return io;
+}
+
+receiver(input: ref Input)
+{
+
+	for(;;){
+		(nil, msg, nil, wc) := <-input.io.write;
+		if(wc == nil)
+			;	# not interested in EOF; leave channel open
+		else{
+			input.inc <-= parse(msg);
+			res := <- input.resc;
+			err := "";
+			if(res == UNKNOWN)
+				err = "no matching plumb rule";
+			wc <-= (len msg, err);
+		}
+	}
+}
+
+sender(input: ref Input, output: array of ref Output)
+{
+	outputc := array[len output] of chan of (int, int, int, Sys->Rread);
+
+	for(;;){
+		alt{
+		in := <-input.inc =>
+			if(in == nil){
+				input.resc <-= HANDLED;
+				break;
+			}
+			(j, msg) := process(in);
+			case j {
+			HANDLED =>
+				break;
+			UNKNOWN =>
+				if(in.msg.src != "acme")
+					sys->fprint(stderr, "plumb: don't know who message goes to\n");
+			NOTSTARTED =>
+				sys->fprint(stderr, "plumb: can't start application\n");
+			* =>
+				output[j].queue = append(output[j].queue, msg);
+				outputc[j] = output[j].io.read;
+			}
+			input.resc <-= j;
+		
+		(j, tmp) := <-outputc =>
+			(nil, nbytes, nil, rc) := tmp;
+			if(rc == nil)	# no interest in EOF
+				break;
+			msg := hd output[j].queue;
+			if(nbytes < len msg){
+				rc <-= (nil, "buffer too short for message");
+				break;
+			}
+			output[j].queue = tl output[j].queue;
+			if(output[j].queue == nil)
+				outputc[j] = nil;
+			rc <-= (msg, nil);
+		}
+	}
+}
+
+parse(a: array of byte): ref Inmesg
+{
+	msg := Msg.unpack(a);
+	if(msg == nil)
+		return nil;
+	i := ref Inmesg;
+	i.msg = msg;
+	if(msg.dst != nil){
+		if(control(i))
+			return nil;
+		toport(i, msg.dst);
+	}else
+		i.port = -1;
+	i.match = array[10] of { * => Match(-1, -1)};
+	i._n = array[10] of string;
+	i.attrs = plumbmsg->string2attrs(i.msg.attr);
+	return i;
+}
+
+append(l: list of array of byte, a: array of byte): list of array of byte
+{
+	if(l == nil)
+		return a :: nil;
+	return hd l :: append(tl l, a);
+}
+
+shutdown()
+{
+	fname := sys->sprint("#p/%d/ctl", pgrp);
+	if((fdesc := sys->open(fname, sys->OWRITE)) != nil)
+		sys->write(fdesc, array of byte "killgrp\n", 8);
+	raise "fail:error";
+}
+
+# Handle control messages
+control(in: ref Inmesg): int
+{
+	msg := in.msg;
+	if(msg.kind!="text" || msg.dst!="plumb")
+		return 0;
+	text := string msg.data;
+	case text {
+	"start" =>
+		start(msg.src, 1);
+	"stop" =>
+		start(msg.src, -1);
+	* =>
+		sys->fprint(stderr, "plumb: unrecognized control message from %s: %s\n", msg.src, text);
+	}
+	return 1;
+}
+
+start(port: string, startstop: int)
+{
+	for(i:=0; i<len output; i++)
+		if(port == output[i].name){
+			output[i].waiting = 0;
+			output[i].started += startstop;
+			return;
+		}
+	sys->fprint(stderr, "plumb: \"start\" message from unrecognized port %s\n", port);
+}
+
+startup(dir, prog: string, args: list of string, wait: chan of int)
+{
+	if(wmstartup){
+		fd := sys->open(wmchan, Sys->OWRITE);
+		if(fd != nil){
+			sys->fprint(fd, "s %s", str->quoted(dir :: prog :: args));
+			wait <-= 1;
+			return;
+		}
+	}
+
+	sys->pctl(Sys->NEWFD|Sys->NEWPGRP|Sys->FORKNS, list of {0, 1, 2});
+	wait <-= 1;
+	wait = nil;
+	mod := load Command prog;
+	if(mod == nil){
+		sys->fprint(stderr, "plumb: can't load %s: %r\n", prog);
+		return;
+	}
+	sys->chdir(dir);
+	mod->init(context, prog :: args);
+}
+
+# See if messages should be queued while waiting for program to connect
+shouldqueue(out: ref Output): int
+{
+	p := findport(out.name);
+	if(p == nil){
+		sys->fprint(stderr, "plumb: can't happen in shouldqueue\n");
+		return 0;
+	}
+	if(p.alwaysstart)
+		return 0;
+	return out.waiting;	
+}
+
+# Determine destination of input message, reformat for output
+process(in: ref Inmesg): (int, array of byte)
+{
+	if(!clarify(in))
+		return (UNKNOWN, nil);
+	if(in.port < 0)
+		return (UNKNOWN, nil);
+	a := in.msg.pack();
+	j := in.port;
+	if(a == nil)
+		j = UNKNOWN;
+	else if(output[j].started==0 && !shouldqueue(output[j])){
+		path: string;
+		args: list of string;
+		if(in.startup!=nil){
+			path = macro(in, in.startup);
+			args = expand(in, in.args);
+		}else if(output[j].startup != nil){
+			path = output[j].startup;
+			args = in.text :: nil;
+		}else
+			return (NOTSTARTED, nil);
+		log(sys->sprint("start %s port %s\n", path, output[j].name));
+		wait := chan of int;
+		output[j].waiting = 1;
+		spawn startup(in.msg.dir, path, args, wait);
+		<-wait;
+		return (HANDLED, nil);
+	}else{
+		if(in.msg.kind != "text")
+			text := sys->sprint("message of type %s", in.msg.kind);
+		else{
+			text = in.text;
+			for(i:=0; i<len text; i++){
+				if(text[i]=='\n'){
+					text = text[0:i];
+					break;
+				}
+				if(i > 50) {
+					text = text[0:i]+"...";
+					break;
+				}
+			}
+		}
+		log(sys->sprint("send \"%s\" to %s", text, output[j].name));
+	}
+	return (j, a);
+}
+
+# expand $arguments
+expand(in: ref Inmesg, args: list of string): list of string
+{
+	a: list of string;
+	while(args != nil){
+		a = macro(in, hd args) :: a;
+		args = tl args;
+	}
+	while(a != nil){
+		args = hd a :: args;
+		a = tl a;
+	}
+	return args;
+}
+
+# resolve all ambiguities, fill in any missing fields
+clarify(in: ref Inmesg): int
+{
+	in.clearclick = 0;
+	in.set = 0;
+	msg := in.msg;
+	if(msg.kind != "text")
+		return 0;
+	in.text = string msg.data;
+	if(msg.dst != "")
+		return 1;
+	return dorules(in, rules);
+}
+
+dorules(in: ref Inmesg, rules: list of ref Rule): int
+{
+	if (verbose)
+		log("msg: " + inmesg2s(in));
+	for(r:=rules; r!=nil; r=tl r) {
+		if(matchrule(in, hd r)){
+			applyrule(in, hd r);
+			if (verbose)
+				log("yes");
+			return 1;
+		} else if (verbose)
+			log("no");
+	}
+	return 0;
+}
+
+inmesg2s(in: ref Inmesg): string
+{
+	m := in.msg;
+	s := sys->sprint("src=%s; dst=%s; dir=%s; kind=%s; attr='%s'",
+			m.src, m.dst, m.dir, m.kind, m.attr);
+	if (m.kind == "text")
+		s += "; data='" + string m.data + "'";
+	return s;
+}
+
+matchrule(in: ref Inmesg, r: ref Rule): int
+{
+	pats := r.pattern;
+	for(i:=0; i<len in.match; i++)
+		in.match[i] = (-1,-1);
+	# no rules at all implies success, so return if any fail
+	for(i=0; i<len pats; i++)
+		if(matchpattern(in, pats[i]) == 0)
+			return 0;
+	return 1;
+}
+
+applyrule(in: ref Inmesg, r: ref Rule)
+{
+	acts := r.action;
+	for(i:=0; i<len acts; i++)
+		applypattern(in, acts[i]);
+	if(in.clearclick){
+		al: list of ref Attr;
+		for(l:=in.attrs; l!=nil; l=tl l)
+			if((hd l).name != "click")
+				al = hd l :: al;
+		in.attrs = al;
+		in.msg.attr = plumbmsg->attrs2string(al);
+		if(in.set){
+			in.text = macro(in, "$0");
+			in.msg.data = array of byte in.text;
+		}
+	}
+}
+
+matchpattern(in: ref Inmesg, p: ref Pattern): int
+{
+	msg := in.msg;
+	text: string;
+	case p.field {
+	"src" =>	text = msg.src;
+	"dst" =>	text = msg.dst;
+	"dir" =>	text = msg.dir;
+	"kind" =>	text = msg.kind;
+	"attr" =>	text = msg.attr;
+	"data" =>	text = in.text;
+	* =>
+		sys->fprint(stderr, "plumb: don't recognize pattern field %s\n", p.field);
+		return 0;
+	}
+	if (verbose)
+		log(sys->sprint("'%s' %s '%s'\n", text, p.pred, p.arg));
+	case p.pred {
+	"is" =>
+		return text == p.arg;
+	"isfile" or "isdir" =>
+		text = p.arg;
+		if(p.expand)
+			text = macro(in, text);
+		if(len text == 0)
+			return 0;
+		if(len in.msg.dir!=0 && text[0] != '/' && text[0]!='#')
+			text = in.msg.dir+"/"+text;
+		text = cleanname(text);
+		(ok, dir) := sys->stat(text);
+		if(ok < 0)
+			return 0;
+		if(p.pred=="isfile" && (dir.mode&Sys->DMDIR)==0){
+			in._file = text;
+			return 1;
+		}
+		if(p.pred=="isdir" && (dir.mode&Sys->DMDIR)!=0){
+			in._dir = text;
+			return 1;
+		}
+		return 0;
+	"matches" =>
+		(clickspecified, val) := plumbmsg->lookup(in.attrs, "click");
+		if(p.field != "data")
+			clickspecified = 0;
+		if(!clickspecified){
+			# easy case. must match whole string
+			matches := regex->execute(p.regex, text);
+			if(matches == nil)
+				return 0;
+			(p0, p1) := matches[0];
+			if(p0!=0 || p1!=len text)
+				return 0;
+			in.match = matches;
+			setvars(in, text);
+			return 1;
+		}
+		matches := clickmatch(p.regex, text, int val);
+		if(matches == nil)
+			return 0;
+		(p0, p1) := matches[0];
+		# assumes all matches are in same sequence
+		if(in.match[0].p0 != -1)
+			return p0==in.match[0].p0 && p1==in.match[0].p1;
+		in.match = matches;
+		setvars(in, text);
+		in.clearclick = 1;
+		in.set = 1;
+		return 1;
+	"set" =>
+		text = p.arg;
+		if(p.expand)
+			text = macro(in, text);
+		case p.field {
+		"src" =>	msg.src = text;
+		"dst" =>	msg.dst = text;
+		"dir" =>	msg.dir = text;
+		"kind" =>	msg.kind = text;
+		"attr" =>	msg.attr = text;
+		"data" =>	in.text = text;
+				msg.data = array of byte text;
+				msg.kind = "text";
+				in.set = 0;
+		}
+		return 1;
+	* =>
+		sys->fprint(stderr, "plumb: don't recognize pattern predicate %s\n", p.pred);
+	}
+	return 0;
+}
+
+applypattern(in: ref Inmesg, p: ref Pattern): int
+{
+	if(p.field != "plumb"){
+		sys->fprint(stderr, "plumb: don't recognize action field %s\n", p.field);
+		return 0;
+	}
+	case p.pred {
+	"to" or "alwaysstart" =>
+		if(in.port >= 0)	# already specified
+			return 1;
+		toport(in, p.arg);
+	"start" =>
+		in.startup = p.arg;
+		in.args = p.extra;
+	* =>
+		sys->fprint(stderr, "plumb: don't recognize action %s\n", p.pred);
+	}
+	return 1;
+}
+
+toport(in: ref Inmesg, name: string): int
+{
+	for(i:=0; i<len output; i++)
+		if(name == output[i].name){
+			in.msg.dst = name;
+			in.port = i;
+			return i;
+		}
+	in.port = -1;
+	sys->fprint(stderr, "plumb: unrecognized port %s\n", name);
+	return -1;
+}
+
+# simple heuristic: look for leftmost match that reaches click position
+clickmatch(re: ref Regex->Arena, text: string, click: int): array of Match
+{
+	for(i:=0; i<=click && i < len text; i++){
+		matches := regex->executese(re, text, (i, -1), i == 0, 1);
+		if(matches == nil)
+			continue;
+		(p0, p1) := matches[0];
+		
+		if(p0>=i && p1>=click)
+			return matches;
+	}
+	return nil;
+}
+
+setvars(in: ref Inmesg, text: string)
+{
+	for(i:=0; i<len in.match && in.match[i].p0>=0; i++)
+		in._n[i] = text[in.match[i].p0:in.match[i].p1];
+	for(; i<len in._n; i++)
+		in._n[i] = "";
+}
+
+macro(in: ref Inmesg, text: string): string
+{
+	word := "";
+	i := 0;
+	j := 0;
+	for(;;){
+		if(i == len text)
+			break;
+		if(text[i++] != '$')
+			continue;
+		if(i == len text)
+			break;
+		word += text[j:i-1];
+		(res, skip) := dollar(in, text[i:]);
+		word += res;
+		i += skip;
+		j = i;
+	}
+	if(j < len text)
+		word += text[j:];
+	return word;
+}
+
+dollar(in: ref Inmesg, text: string): (string, int)
+{
+	if(text[0] == '$')
+		return ("$", 1);
+	if('0'<=text[0] && text[0]<='9')
+		return (in._n[text[0]-'0'], 1);
+	if(len text < 3)
+		return ("$", 0);
+	case text[0:3] {
+	"src" =>	return (in.msg.src, 3);
+	"dst" =>	return (in.msg.dst, 3);
+	"dir" =>	return (in._dir, 3);
+	}
+	if(len text< 4)
+		return ("$", 0);
+	case text[0:4] {
+	"attr" =>	return (in.msg.attr, 4);
+	"data" =>	return (in.text, 4);
+	"file" =>	return (in._file, 4);
+	"kind" =>	return (in.msg.kind, 4);
+	}
+	return ("$", 0);
+}
+
+# compress ../ references and do other cleanups
+cleanname(name: string): string
+{
+	# compress multiple slashes
+	n := len name;
+	for(i:=0; i<n-1; i++)
+		if(name[i]=='/' && name[i+1]=='/'){
+			name = name[0:i]+name[i+1:];
+			--i;
+			n--;
+		}
+	#  eliminate ./
+	for(i=0; i<n-1; i++)
+		if(name[i]=='.' && name[i+1]=='/' && (i==0 || name[i-1]=='/')){
+			name = name[0:i]+name[i+2:];
+			--i;
+			n -= 2;
+		}
+	found: int;
+	do{
+		# compress xx/..
+		found = 0;
+		for(i=1; i<=n-3; i++)
+			if(name[i:i+3] == "/.."){
+				if(i==n-3 || name[i+3]=='/'){
+					found = 1;
+					break;
+				}
+			}
+		if(found)
+			for(j:=i-1; j>=0; --j)
+				if(j==0 || name[j-1]=='/'){
+					i += 3;		# character beyond ..
+					if(i<n && name[i]=='/')
+						++i;
+					name = name[0:j]+name[i:];
+					n -= (i-j);
+					break;
+				}
+	}while(found);
+	# eliminate trailing .
+	if(n>=2 && name[n-2]=='/' && name[n-1]=='.')
+		--n;
+	if(n == 0)
+		return ".";
+	if(n != len name)
+		name = name[0:n];
+	return name;
+}
+
+log(s: string)
+{
+	if(len s == 0)
+		return;
+	if(s[len s-1] != '\n')
+		s[len s] = '\n';
+	sys->print("plumb: %s", s);
+}
--- /dev/null
+++ b/appl/cmd/prof.b
@@ -1,0 +1,243 @@
+implement Prof;
+
+include "sys.m"; 
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+	arg: Arg;
+include "profile.m";
+	profile: Profile;
+include "sh.m";
+
+stderr: ref Sys->FD;
+
+Prof: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+	init0: fn(nil: ref Draw->Context, argv: list of string): Profile->Prof;
+};
+
+ignored(s: string)
+{
+	sys->fprint(stderr, "prof: warning: %s ignored\n", s);
+}
+
+exits(e: string)
+{
+	if(profile != nil)
+		profile->end();
+	raise "fail:" + e;
+}
+
+pfatal(s: string)
+{
+	sys->fprint(stderr, "prof: %s: %s\n", s, profile->lasterror());
+	exits("error");
+}
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "prof: cannot load %s: %r\n", p);
+	exits("bad module");
+}
+
+usage(s: string)
+{
+	sys->fprint(stderr, "prof: %s\n", s);
+	sys->fprint(stderr, "usage: prof [-bflnv] [-m modname]... [-s rate] [cmd arg ...]\n");
+	exits("usage");
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	init0(ctxt, argv);
+}
+
+init0(ctxt: ref Draw->Context, argv: list of string): Profile->Prof
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	arg = load Arg Arg->PATH;
+	if(arg == nil)
+		badmodule(Arg->PATH);
+	arg->init(argv);
+	profile = load Profile Profile->PATH;
+	if(profile == nil)
+		badmodule(Profile->PATH);
+	if(profile->init() < 0)
+		pfatal("cannot initialize profile device");
+
+	v := 0;
+	begin := 0;
+	rate := 0;
+	ep := 0;
+	wm := 0;
+	exec, mods: list of string;
+	while((c := arg->opt()) != 0){
+		case c {
+			'b' => begin = 1;
+			'f' => v |= profile->FUNCTION;
+			'l' => v |= profile->LINE;
+			'n' => v |= profile->FULLHDR;
+			'v' => v |= profile->VERBOSE;
+			's' => 
+				if((s := arg->arg()) == nil)
+					usage("missing sample rate");
+				rate = int s;
+				if(rate <= 0)
+					usage("bad sample rate: '" + s + "'");
+			'm' =>
+				if((s := arg->arg()) == nil)
+					usage("missing module name");
+				mods = s :: mods;
+			'e' =>
+				ep = 1;
+			'g' =>
+				wm = 1;
+			* => 
+				usage(sys->sprint("unknown option -%c", c));
+		}
+	}
+
+	exec = arg->argv();
+
+	if(begin && v != 0)
+		ignored("output format");
+	if(begin && exec != nil)
+		begin = 0;
+	if(begin == 0 && exec == nil){
+		if(mods != nil)
+			ignored("-m option");
+		if(rate > 0)
+			ignored("-s option");
+		mods = nil;
+		rate = 0;
+	}
+
+	if(rate > 0)
+		profile->sample(rate);
+	for( ; mods != nil; mods = tl mods)
+		profile->profile(hd mods);
+
+	if(begin){
+		if(profile->start() < 0)
+			pfatal("cannot start profiling");
+		exit;
+	}
+	r := 0;
+	if(exec != nil){
+		if(ep)
+			profile->profile(disname(hd exec));
+		if(profile->start() < 0)
+			pfatal("cannot start profiling");
+		# r = run(ctxt, hd exec, exec);
+		wfd := openwait(sys->pctl(0, nil));
+		ci := chan of int;
+		spawn execute(ctxt, hd exec, exec, ci);
+		epid := <- ci;
+		wait(wfd, epid);
+	}
+	if(profile->stop() < 0)
+		pfatal("cannot stop profiling");
+	if(exec == nil || r >= 0){
+		modl := profile->stats();
+		if(modl.mods == nil)
+			pfatal("no profile information");
+		if(wm){
+			profile->end();
+			return modl;
+		}
+		if(!(v&(profile->FUNCTION|profile->LINE)))
+			v |= profile->LINE;
+		if(profile->show(modl, v) < 0)
+			pfatal("cannot show profile");
+	}
+	profile->end();
+	return (nil, 0, nil);
+}
+
+disname(cmd: string): string
+{
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	if(exists(file))
+		return file;
+	if(file[0]!='/' && file[0:2]!="./")
+		file = "/dis/"+file;
+	# if(exists(file))
+	#	return file;
+	return file;
+}
+
+execute(ctxt: ref Draw->Context, cmd : string, argl : list of string, ci: chan of int)
+{
+	ci <-= sys->pctl(Sys->FORKNS|Sys->NEWFD|Sys->NEWPGRP, 0 :: 1 :: 2 :: stderr.fd :: nil);
+	file := cmd;
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(file[0]!='/' && file[0:2]!="./"){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(stderr, "prof: %s: %s\n", cmd, err);
+			return;
+		}
+	}
+	c->init(ctxt, argl);
+}
+
+# run(ctxt: ref Draw->Context, cmd : string, argl : list of string): int
+# {
+# 	file := cmd;
+# 	if(len file<4 || file[len file-4:]!=".dis")
+# 		file += ".dis";
+# 	c := load Command file;
+# 	if(c == nil) {
+# 		err := sys->sprint("%r");
+# 		if(file[0]!='/' && file[0:2]!="./"){
+# 			c = load Command "/dis/"+file;
+# 			if(c == nil)
+# 				err = sys->sprint("%r");
+# 		}
+# 		if(c == nil){
+# 			sys->fprint(stderr, "prof: %s: %s\n", cmd, err);
+# 			return -1;
+# 		}
+# 	}
+# 	c->init(ctxt, argl);
+# 	return 0;
+# }
+
+openwait(pid : int) : ref Sys->FD
+{
+	w := sys->sprint("#p/%d/wait", pid);
+	fd := sys->open(w, Sys->OREAD);
+	if (fd == nil)
+		pfatal("fd == nil in wait");
+	return fd;
+}
+
+wait(wfd : ref Sys->FD, wpid : int)
+{
+	n : int;
+
+	buf := array[Sys->WAITLEN] of byte;
+	status := "";
+	for(;;) {
+		if ((n = sys->read(wfd, buf, len buf)) < 0)
+			pfatal("bad read in wait");
+		status = string buf[0:n];
+		if (int status == wpid)
+			break;
+	}
+}
+
+exists(f: string): int
+{
+	return sys->open(f, Sys->OREAD) != nil;
+}
--- /dev/null
+++ b/appl/cmd/promptstring.b
@@ -1,0 +1,66 @@
+RAWON_STR := "*";
+
+RAWON : con 0;
+RAWOFF : con 1;
+
+promptstring(prompt, def: string, mode: int): string
+{
+	if(mode == RAWON || def == nil || def == "")
+		sys->fprint(stdout, "%s: ", prompt);
+	else
+		sys->fprint(stdout, "%s [%s]: ", prompt, def);
+	(eof, resp) := readline(stdin, mode);
+	if(eof)
+		exit;
+	if(resp == "")
+		resp = def;
+	return resp;
+}
+
+readline(fd: ref Sys->FD, mode: int): (int, string)
+{
+	i: int;
+	eof: int;
+	fdctl: ref Sys->FD;
+
+	eof = 0;
+	buf := array[128] of byte;
+	tmp := array[128] of byte;
+	
+	if(mode == RAWON){
+		fdctl = sys->open("/dev/consctl", sys->OWRITE);
+		if(fdctl == nil || sys->write(fdctl,array of byte "rawon",5) != 5){
+			sys->fprint(stderr, "unable to change console mode");
+			return (1,nil);
+		}
+	}
+
+	for(sofar := 0; sofar < 128; sofar += i){
+		i = sys->read(fd, tmp, 128 - sofar);
+		if(i <= 0){
+			eof = 1;
+			break;
+		}
+		if(tmp[i-1] == byte '\n'){
+			for(j := 0; j < i-1; j++){
+				buf[sofar+j] = tmp[j];
+				if(mode == RAWON && RAWON_STR != nil)
+				   sys->write(stdout,array of byte RAWON_STR,1);
+			}
+			sofar += j;
+			if(mode == RAWON)
+				sys->write(stdout,array of byte "\n",1);
+			break;
+		}
+		else {
+			for(j := 0; j < i; j++){
+				buf[sofar+j] = tmp[j];
+				if(mode == RAWON && RAWON_STR != nil)
+				   sys->write(stdout,array of byte RAWON_STR,1);
+			}
+		}		
+	}
+	if(mode == RAWON)
+		sys->write(fdctl,array of byte "rawoff",6);
+	return (eof, string buf[0:sofar]);
+}
--- /dev/null
+++ b/appl/cmd/ps.b
@@ -1,0 +1,61 @@
+implement Ps;
+
+include "sys.m";
+include "draw.m";
+
+FD, Dir: import Sys;
+Context: import Draw;
+
+Ps: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+sys: Sys;
+stderr: ref FD;
+
+init(nil: ref Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	stderr = sys->fildes(2);
+
+	sys->pctl(Sys->FORKNS, nil);
+	if(sys->chdir("/prog") < 0){
+		sys->fprint(stderr, "ps: can't chdir to /prog: %r\n");
+		raise "fail:no /prog";
+	}
+	fd := sys->open(".", sys->OREAD);
+	if(fd == nil) {
+		sys->fprint(stderr, "ps: cannot open /prog: %r\n");
+		raise "fail:no /prog";
+	}
+
+	for(;;) {
+		(n, d) := sys->dirread(fd);
+		if(n <= 0){
+			if(n < 0) {
+				sys->fprint(stderr, "ps: error reading /prog: %r\n");
+				raise "fail:error on /prog";
+			}
+			break;
+		}
+		for(i := 0; i < n; i++)
+			if(d[i].name[0] >= '0' && d[i].name[0] <= '9')
+				ps(int d[i].name);		
+	}
+}
+
+ps(pid: int)
+{
+	proc := string pid+"/status";
+	fd := sys->open(proc, sys->OREAD);
+	if(fd == nil) {	# process must have died
+		# sys->fprint(stderr, "ps: /prog/%s: %r\n", proc);
+		return;
+	}
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n > 0)
+		sys->print("%s\n", string buf[0:n]);
+}
--- /dev/null
+++ b/appl/cmd/puttar.b
@@ -1,0 +1,183 @@
+# read list of pathnames on stdin, write POSIX.1 tar on stdout
+# Copyright(c)1996 Lucent Technologies.  All Rights Reserved.
+# 22 Dec 1996 ehg@bell-labs.com
+
+implement puttar;
+include "sys.m";
+	sys: Sys;
+	print, sprint, fprint: import sys;
+	stdout, stderr: ref sys->FD;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+puttar: module{
+	init:   fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Warning(mess: string)
+{
+	fprint(stderr,"warning: puttar: %s: %r\n",mess);
+}
+
+Error(mess: string){
+	fprint(stderr,"puttar: %s: %r\n",mess);
+	exit;
+}
+
+TBLOCK: con 512;	# tar logical blocksize
+NBLOCK: con 20;		# blocking factor for efficient write
+tarbuf := array[NBLOCK*TBLOCK] of byte;	# for output
+nblock := 0;		# how many blocks of data are in tarbuf
+
+flushblocks(){
+	if(nblock<=0) return;
+	if(nblock<NBLOCK){
+		for(i:=(nblock+1)*TBLOCK;i<NBLOCK*TBLOCK;i++)
+			tarbuf[i] = byte 0;
+	}
+	i := sys->write(stdout,tarbuf,NBLOCK*TBLOCK);
+	if(i!=NBLOCK*TBLOCK)
+		Error("write error");
+	nblock = 0;
+}
+
+putblock(data:array of byte){
+	# all writes are done through here, so we can guarantee
+	#              10kbyte blocks if writing to tape device
+	if(len data!=TBLOCK)
+		Error("putblock wants TBLOCK chunks");
+	tarbuf[nblock*TBLOCK:] = data;
+	nblock++;
+	if(nblock>=NBLOCK)
+		flushblocks();
+}
+
+packname(hdr:array of byte, name:string){
+	utf := array of byte name;
+	n := len utf;
+	if(n<=100){
+		hdr[0:] = utf;
+		return;
+	}
+	for(i:=n-101; i<n && int utf[i] != '/'; i++){}
+	if(i==n) Error(sprint("%s > 100 bytes",name));
+	if(i>155) Error(sprint("%s too long\n",name));
+	hdr[0:] = utf[i+1:n];
+	hdr[345:] = utf[0:i];  # tar supplies implicit slash
+}
+
+octal(width:int, val:int):array of byte{
+	octal := array of byte "01234567";
+	a := array[width] of byte;
+	for(i:=width-1; i>=0; i--){
+		a[i] = octal[val&7];
+		val >>= 3;
+	}
+	return a;
+}
+
+chksum(hdr: array of byte):int{
+	sum := 0;
+	for(i:=0; i<len hdr; i++)
+		sum += int hdr[i];
+	return sum;
+}
+
+hdr, zeros, ibuf : array of byte;
+
+tar(file : string)
+{
+	ifile: ref sys->FD;
+
+	(rc,stat) := sys->stat(file);
+	if(rc<0){ Warning(sprint("cannot stat %s",file)); return; };
+	ifile = sys->open(file,sys->OREAD);
+	if(ifile==nil) Error(sprint("cannot open %s",file));
+	hdr[0:] = zeros;
+	packname(hdr,file);
+	hdr[100:] = octal(7,stat.mode&8r777);
+	hdr[108:] = octal(7,1);
+	hdr[116:] = octal(7,1);
+	hdr[124:] = octal(11,int stat.length);
+	hdr[136:] = octal(11,stat.mtime);
+	hdr[148:] = array of byte "        "; # for chksum
+	hdr[156] = byte '0';
+	if(stat.mode&Sys->DMDIR) hdr[156] = byte '5';
+	hdr[257:] = array of byte "ustar";
+	hdr[263:] = array of byte "00";
+	hdr[265:] = array of byte stat.uid; # assumes len uid<=32
+	hdr[297:] = array of byte stat.gid;
+	hdr[329:] = octal(8,stat.dev);
+	hdr[337:] = octal(8,int stat.qid.path);
+	hdr[148:] = octal(7,chksum(hdr));
+	hdr[155] = byte 0;
+	putblock(hdr);
+	for(bytes := int stat.length; bytes>0;){
+		n := len ibuf;  if(n>bytes) n = bytes;  # min
+		if(sys->read(ifile,ibuf,n)!=n)
+			Error(sprint("read error on %s",file));
+		nb := (n+TBLOCK-1)/TBLOCK;
+		fill := nb*TBLOCK;
+		for(i:=n; i<fill; i++) ibuf[i] = byte 0;
+		for(i=0; i<nb; i++)
+			putblock(ibuf[i*TBLOCK:(i+1)*TBLOCK]);
+		bytes -= n;
+	}
+	ifile = nil;
+}
+
+rtar(file : string)
+{
+	tar(file);
+	# recurse if directory
+	(ok, dir) := sys->stat(file);
+	if (ok < 0){
+		Warning(sprint("cannot stat %s", file));
+		return;
+	}
+	if (dir.mode & Sys->DMDIR) {
+		fd := sys->open(file, sys->OREAD);
+		if (fd == nil)
+			Error(sprint("cannot open %s", file));
+		for (;;) {
+			(n, d) := sys->dirread(fd);
+			if (n <= 0)
+				break;
+			for (i := 0; i < n; i++) {
+				if (file[len file - 1] == '/')
+					rtar(file + d[i].name);
+				else
+					rtar(file + "/" + d[i].name);
+			}
+		}
+	}
+}
+
+init(nil: ref Draw->Context, args: list of string){
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+	
+	hdr = array[TBLOCK] of byte;
+	zeros = array[TBLOCK] of {* => byte 0};
+	ibuf = array[len tarbuf] of byte;
+
+	if (tl args == nil) {
+		stdin := bufio->fopen(sys->fildes(0),bufio->OREAD);
+		if(stdin==nil) Error("can't fopen stdin");
+		while((file := stdin.gets('\n'))!=nil){
+			if(file[len file-1]=='\n') file = file[0:len file-1];
+			tar(file);
+		}
+	}
+	else {
+		for (args = tl args; args != nil; args = tl args)
+			rtar(hd args);
+	}
+	putblock(zeros);
+	putblock(zeros);	# format requires two empty blocks at end
+	flushblocks();
+}
--- /dev/null
+++ b/appl/cmd/pwd.b
@@ -1,0 +1,28 @@
+implement Pwd;
+
+include "sys.m";
+include "draw.m";
+include "workdir.m";
+
+Pwd: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys := load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	gwd := load Workdir Workdir->PATH;
+	if (gwd == nil) {
+		sys->fprint(stderr, "pwd: cannot load %s: %r\n", Workdir->PATH);
+		raise "fail:bad module";
+	}
+
+	wd := gwd->init();
+	if(wd == nil) {
+		sys->fprint(stderr, "pwd: %r\n");
+		raise "fail:error";
+	}
+	sys->print("%s\n", wd);
+}
--- /dev/null
+++ b/appl/cmd/ramfile.b
@@ -1,0 +1,97 @@
+implement Ramfile;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+# synthesise a file that can be treated just like any other
+# file. limitations of file2chan mean that it's not possible
+# to know when an open should have truncated the file, so
+# we do the only possible thing, and truncate it when we get
+# a write at offset 0. thus it can be edited with an editor,
+# but can't be used to store seekable, writable data records
+# (unless the first record is never written)
+
+# there should be some way to determine when the file should
+# go away - file2chan sends a nil channel whenever the file
+# is closed by anyone, which is not good enough.
+
+stderr: ref Sys->FD;
+
+Ramfile: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	if (len argv < 2 || len argv > 3) {
+		sys->fprint(stderr, "usage: ramfile path [data]\n");
+		return;
+	}
+	path := hd tl argv;
+	(dir, f) := pathsplit(path);
+
+	if (sys->bind("#s", dir, Sys->MBEFORE) == -1) {
+		sys->fprint(stderr, "ramfile: %r\n");
+		return;
+	}
+	fio := sys->file2chan(dir, f);
+	if (fio == nil) {
+		sys->fprint(stderr, "ramfile: file2chan failed: %r\n");
+		return;
+	}
+	data := array[0] of byte;
+	if (tl tl argv != nil)
+		data = array of byte hd tl tl argv;
+
+	spawn server(fio, data);
+	data = nil;
+}
+
+server(fio: ref Sys->FileIO, data: array of byte)
+{
+	for (;;) alt {
+	(offset, count, nil, rc) := <-fio.read =>
+		if (rc != nil) {
+			if (offset > len data)
+				rc <-= (nil, nil);
+			else {
+				end := offset + count;
+				if (end > len data)
+					end = len data;
+				rc <-= (data[offset:end], nil);
+			}
+		}
+	(offset, d, nil, wc) := <-fio.write =>
+		if (wc != nil) {
+			if (offset == 0)
+				data = array[0] of byte;
+			end := offset + len d;
+			if (end > len data) {
+				ndata := array[end] of byte;
+				ndata[0:] = data;
+				data = ndata;
+				ndata = nil;
+			}
+			data[offset:] = d;
+			wc <-= (len d, nil);
+		}
+	}
+}
+
+pathsplit(p: string): (string, string)
+{
+	for (i := len p - 1; i >= 0; i--)
+		if (p[i] != '/')
+			break;
+	if (i < 0)
+		return (p, nil);
+	p = p[0:i+1];
+	for (i = len p - 1; i >=0; i--)
+		if (p[i] == '/')
+			break;
+	if (i < 0)
+		return (".", p);
+	return (p[0:i+1], p[i+1:]);
+}
--- /dev/null
+++ b/appl/cmd/randpass.b
@@ -1,0 +1,41 @@
+implement Randpass;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+
+include "draw.m";
+
+include "ipints.m";
+	ipints: IPints;
+	IPint: import ipints;
+
+Randpass: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	ipints = load IPints IPints->PATH;
+
+	if(args != nil)
+		args = tl args;
+	pwlen := 16;
+	if(args != nil){
+		if(!isnumeric(hd args) || (pwlen = int hd args) <= 8 || pwlen > 256){
+			sys->fprint(sys->fildes(2), "Usage: randpass [password-length(<256, default=16)]\n");
+			raise "fail:usage";
+		}
+	}
+	sys->print("%s\n", IPint.random(pwlen*8).iptob64()[0: pwlen]);
+}
+
+isnumeric(s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(!(s[i]>='0' && s[i]<='9'))
+			return 0;
+	return i > 0;
+}
--- /dev/null
+++ b/appl/cmd/raw2iaf.b
@@ -1,0 +1,122 @@
+implement Raw2Iaf;
+
+include "sys.m";
+include "draw.m";
+
+sys:	Sys;
+FD:	import sys;
+stderr:	ref FD;
+
+rateK:	con "rate";
+rateV:	string = "44100";
+chanK:	con "chans";
+chanV:	string = "2";
+bitsK:	con "bits";
+bitsV:	string = "16";
+encK:	con "enc";
+encV:	string = "pcm";
+
+progV:	string;
+inV:	string = nil;
+outV:	string = nil;
+inf:	ref FD;
+outf:	ref FD;
+
+pad	:= array[] of { "  ", " ", "", "   " };
+
+Raw2Iaf: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: %s -8124 -ms -bw -aup -o out in\n", progV);
+	exit;
+}
+
+options(s: string)
+{
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'8' =>	rateV = "8000";
+		'1' =>	rateV = "11025";
+		'2' =>	rateV = "22050";
+		'4' =>	rateV = "44100";
+		'm' =>	chanV = "1";
+		's' =>	chanV = "2";
+		'b' =>	bitsV = "8";
+		'w' =>	bitsV = "16";
+		'a' =>	encV = "alaw";
+		'u' =>	encV = "ulaw";
+		'p' =>	encV = "pcm";
+		* =>	usage();
+		}
+	}
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	progV = hd argv;
+	v := tl argv;
+
+	while (v != nil) {
+		a := hd v;
+		v = tl v;
+		if (len a == 0)
+			continue;
+		if (a[0] == '-') {
+			if (len a == 1) {
+				if (inV == nil)
+					inV = "-";
+				else
+					usage();
+			}
+			else if (a[1] == 'o') {
+				if (outV != nil)
+					usage();
+				if (len a > 2)
+					outV = a[2:len a];
+				else if (v == nil)
+					usage();
+				else {
+					outV = hd v;
+					v = tl v;
+				}
+			}
+			else
+				options(a[1:len a]);
+		}
+		else if (inV == nil)
+			inV = a;
+		else
+			usage();
+	}
+	if (inV == nil || inV == "-")
+		inf = sys->fildes(0);
+	else {
+		inf = sys->open(inV, Sys->OREAD);
+		if (inf == nil) {
+			sys->fprint(stderr, "%s: could not open %s: %r\n", progV, inV);
+			exit;
+		}
+	}
+	if (outV == nil || outV == "-")
+		outf = sys->fildes(1);
+	else {
+		outf = sys->create(outV, Sys->OWRITE, 8r666);
+		if (outf == nil) {
+			sys->fprint(stderr, "%s: could not create %s: %r\n", progV, outV);
+			exit;
+		}
+	}
+	s := rateK + "\t" + rateV + "\n"
+		+  chanK + "\t" + chanV + "\n"
+		+  bitsK + "\t" + bitsV + "\n"
+		+  encK + "\t" + encV;
+	sys->fprint(outf, "%s%s\n\n", s, pad[len s % 4]);
+	if (sys->stream(inf, outf, Sys->ATOMICIO) < 0)
+		sys->fprint(stderr, "%s: data copy error: %r\n", progV);
+}
--- /dev/null
+++ b/appl/cmd/rawdbfs.b
@@ -1,0 +1,812 @@
+implement Dbfs;
+
+#
+# Copyright © 1999, 2002 Vita Nuova Limited.  All rights reserved.
+#
+
+# Enhanced to include record locking, index field generation and update notification
+
+# TO DO:
+#	make writing & reading more like real files; don't ignore offsets.
+#	open with OTRUNC should work.
+#	provide some way of compacting a dbfs file.
+
+include "sys.m";
+	sys: Sys;
+	Qid: import Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Fid, Navigator, Navop: import styxservers;
+	Enotfound, Eperm, Ebadfid, Ebadarg: import styxservers;
+
+include "string.m";
+	str: String;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "sh.m";
+	sh: Sh;
+
+Record: adt {
+	id:		int;			# file number in directory (if block is allocated)
+	offset:	int;			# start of data
+	count:	int;			# length of block (excluding header)
+	datalen:	int;			# length of data (-1 if block is free)
+	vers:		int;			# version
+
+	new:		fn(offset: int, length: int): ref Record;
+	qid:		fn(r: self ref Record): Sys->Qid;
+};
+
+# Record lock
+Lock: adt {
+	qpath: big;
+	fid:	int;
+};
+
+HEADLEN: con 10;
+MINSIZE: con 20;
+
+Database: adt {
+	file:		ref Iobuf;
+	records:	array of ref Record;
+	maxid:	int;
+	locking:	int;
+	locklist:	list of Lock;
+	indexing:	int;
+	stats:	int;
+	index:	int;
+	s_reads:	int;
+	s_writes:	int;
+	s_creates:	int;
+	s_removes:	int;
+	updcmd:	string;
+	vers:		int;
+
+	build:	fn(f: ref Iobuf, locking, indexing: int, stats: int, updcmd: string): (ref Database, string);
+	write:	fn(db: self ref Database, n: int, data: array of byte): int;
+	read:		fn(db: self ref Database, n: int): array of byte;
+	remove:	fn(db: self ref Database, n: int);
+	create:	fn(db: self ref Database, data: array of byte): ref Record;
+	updated:	fn(db: self ref Database);
+	lock:		fn(db: self ref Database, c: ref Styxservers->Fid): int;
+	unlock:	fn(db: self ref Database, c: ref Styxservers->Fid);
+	ownlock:	fn(db: self ref Database, c: ref Styxservers->Fid): int;
+};
+
+Dbfs: module
+{
+	init:	fn(ctxt: ref Draw->Context, nil: list of string);
+};
+
+Qdir, Qnew, Qdata, Qindex, Qstats: con iota;
+
+stderr: ref Sys->FD;
+database: ref Database;
+context: ref Draw->Context;
+user: string;
+Eremoved: con "file removed";
+Egreg: con "thermal problems";
+Elocked: con "open/create -- file is locked";
+
+usage()
+{
+	sys->fprint(stderr, "Usage: dbfs [-abcelrxD][-u cmd] file mountpoint\n");
+	raise "fail:usage";
+}
+
+nomod(s: string)
+{
+	sys->fprint(stderr, "dbfs: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	context = ctxt;
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		nomod(Styx->PATH);
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	if(styxservers == nil)
+		nomod(Styxservers->PATH);
+	styxservers->init(styx);
+	str = load String String->PATH;
+	if(str == nil)
+		nomod(String->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		nomod(Bufio->PATH);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	arg->init(args);
+	flags := Sys->MREPL;
+	copt := 0;
+	empty := 0;
+	locking := 0;
+	stats := 0;
+	indexing := 0;
+	updcmd := "";
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>	flags = Sys->MAFTER;
+		'b' =>	flags = Sys->MBEFORE;
+		'r' =>		flags = Sys->MREPL;
+		'c' =>	copt = 1;
+		'e' =>	empty = 1;
+		'l' =>		locking = 1;
+		'u' =>	updcmd = arg->arg();
+				if(updcmd == nil)
+					usage();
+		'x' =>	indexing = 1;
+				stats = 1;
+		'D' =>	styxservers->traceset(1);
+		* =>		usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(len args != 2)
+		usage();
+	if(copt)
+		flags |= Sys->MCREATE;
+	file := hd args;
+	args = tl args;
+	mountpt := hd args;
+
+	if(updcmd != nil){
+		sh = load Sh Sh->PATH;
+		if(sh == nil)
+			nomod(Sh->PATH);
+	}
+
+	df := bufio->open(file, Sys->ORDWR);
+	if(df == nil && empty){
+		(rc, nil) := sys->stat(file);
+		if(rc < 0)
+			df = bufio->create(file, Sys->ORDWR, 8r600);
+	}
+	if(df == nil){
+		sys->fprint(stderr, "dbfs: can't open %s: %r\n", file);
+		raise "fail:cannot open file";
+	}
+	(db, err) := Database.build(df, locking, indexing, stats, updcmd);
+	if(db == nil){
+		sys->fprint(stderr, "dbfs: can't read %s: %s\n", file, err);
+		raise "fail:cannot read db";
+	}
+	database = db;
+
+	sys->pctl(Sys->FORKFD, nil);
+
+	user = rf("/dev/user");
+	if(user == nil)
+		user = "inferno";
+
+	fds := array[2] of ref Sys->FD;
+	if(sys->pipe(fds) < 0){
+		sys->fprint(stderr, "dbfs: can't create pipe: %r\n");
+		raise "fail:pipe";
+	}
+
+	navops := chan of ref Navop;
+	spawn navigator(navops);
+
+	(tchan, srv) := Styxserver.new(fds[0], Navigator.new(navops), big Qdir);
+	fds[0] = nil;
+
+	pidc := chan of int;
+	spawn serveloop(tchan, srv, pidc, navops);
+	<-pidc;
+
+	if(sys->mount(fds[1], nil, mountpt, flags, nil) < 0) {
+		sys->fprint(stderr, "dbfs: mount failed: %r\n");
+		raise "fail:bad mount";
+	}
+}
+
+rf(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	b := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, b, len b);
+	if(n < 0)
+		return nil;
+	return string b[0:n];
+}
+
+serveloop(tchan: chan of ref Tmsg, srv: ref Styxserver, pidc: chan of int, navops: chan of ref Navop)
+{
+	pidc <-= sys->pctl(Sys->FORKNS|Sys->NEWFD, stderr.fd::1::2::database.file.fd.fd::srv.fd.fd::nil);
+#	stderr = sys->fildes(stderr.fd);
+	database.file.fd = sys->fildes(database.file.fd.fd);
+Serve:
+	while((gm := <-tchan) != nil){
+		pick m := gm {
+		Readerror =>
+			sys->fprint(stderr, "dbfs: fatal read error: %s\n", m.error);
+			break Serve;
+		Open =>
+			open(srv, m);
+		Read =>
+			(c, err) := srv.canread(m);
+			if(c == nil) {
+				srv.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			if(c.qtype & Sys->QTDIR){
+				srv.read(m);
+				break;
+			}
+			case TYPE(c.path) {
+			Qindex =>
+				if(database.index < 0) {
+					srv.reply(ref Rmsg.Error(m.tag, Eperm));
+					break;
+				}
+				if (m.offset > big 0) {
+					srv.reply(ref Rmsg.Read(m.tag, nil));
+					break;
+				}
+				reply := array of byte string ++database.index;
+				if(m.count < len reply)
+					reply = reply[:m.count];
+				srv.reply(ref Rmsg.Read(m.tag, reply));
+			Qstats =>
+				if (m.offset > big 0) {
+					srv.reply(ref Rmsg.Read(m.tag, nil));
+					break;
+				}
+				reply := array of byte sys->sprint("%d %d %d %d", database.s_reads, database.s_writes,
+												database.s_creates, database.s_removes);
+				if(m.count < len reply) reply = reply[:m.count];
+				srv.reply(ref Rmsg.Read(m.tag, reply));
+			Qdata =>
+				recno := id2recno(FILENO(c.path));
+				if(recno == -1)
+					srv.reply(ref Rmsg.Error(m.tag, Eremoved));
+				else
+					srv.reply(styxservers->readbytes(m, database.read(recno)));
+			* =>
+				srv.reply(ref Rmsg.Error(m.tag, Egreg));
+			}
+		Write =>
+			(c, err) := srv.canwrite(m);
+			if(c == nil){
+				srv.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			if(!database.ownlock(c)) {
+				# shouldn't happen: open checks
+				srv.reply(ref Rmsg.Error(m.tag, Elocked));
+				break;
+			}
+			case TYPE(c.path) {
+			Qindex =>
+				if(database.index >= 0) {
+					srv.reply(ref Rmsg.Error(m.tag, Eperm));
+					break;
+				}
+				database.index = int string m.data;
+				srv.reply(ref Rmsg.Write(m.tag, len m.data));
+			Qdata =>
+				recno := id2recno(FILENO(c.path));
+				if(recno == -1)
+					srv.reply(ref Rmsg.Error(m.tag, "phase error"));
+				else {
+					changed := 1;
+					if(database.updcmd != nil){
+						oldrec := database.read(recno);
+						changed = !eqbytes(m.data, oldrec);
+					}
+					if(changed && database.write(recno, m.data) == -1){
+						srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+						break;
+					}
+					if(changed)
+						database.updated();	# run the command before reply
+					srv.reply(ref Rmsg.Write(m.tag, len m.data));
+				}
+			* =>
+				srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			}
+		Clunk =>
+			c := srv.getfid(m.fid);
+			if(c != nil)
+				database.unlock(c);
+			srv.clunk(m);
+		Remove =>
+			c := srv.getfid(m.fid);
+			database.unlock(c);
+			if(c == nil || c.qtype & Sys->QTDIR || TYPE(c.path) != Qdata){
+				# let it diagnose all the errors
+				srv.remove(m);
+				break;
+			}
+			recno := id2recno(FILENO(c.path));
+			if(recno == -1)
+				srv.reply(ref Rmsg.Error(m.tag, "phase error"));
+			else {
+				database.remove(recno);
+				database.updated();
+				srv.reply(ref Rmsg.Remove(m.tag));
+			}
+			srv.delfid(c);
+		* =>
+			srv.default(gm);
+		}
+	}
+	navops <-= nil;		# shut down navigator
+}
+
+eqbytes(a, b: array of byte): int
+{
+	if(len a != len b)
+		return 0;
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+id2recno(id: int): int
+{
+	recs := database.records;
+	for(i := 0; i < len recs; i++)
+		if(recs[i].datalen >= 0 && recs[i].id == id)
+			return i;
+	return -1;
+}
+	
+open(srv: ref Styxserver, m: ref Tmsg.Open): ref Fid
+{
+	(c, mode, d, err) := srv.canopen(m);
+	if(c == nil){
+		srv.reply(ref Rmsg.Error(m.tag, err));
+		return nil;
+	}
+	if(TYPE(c.path) == Qnew){
+		# generate new file
+		if(c.uname != user){
+			srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			return nil;
+		}
+		r := database.create(array[0] of byte);
+		if(r == nil) {
+			srv.reply(ref Rmsg.Error(m.tag, "create -- i/o error"));
+			return nil;
+		}
+		(d, nil) = dirgen(QPATH(r.id, Qdata));
+	}
+	if(m.mode & Sys->OTRUNC) {
+		# TO DO
+	}
+	c.open(mode, d.qid);
+	if(database.locking && TYPE(c.path) == Qdata && (m.mode & (Sys->OWRITE|Sys->ORDWR))) {
+		if(!database.lock(c)) {
+			srv.reply(ref Rmsg.Error(m.tag, Elocked));
+			return nil;
+		}
+	}
+	srv.reply(ref Rmsg.Open(m.tag, d.qid, srv.iounit()));
+	return c;
+}
+
+dirslot(n: int): int
+{
+	for(i := 0; i < len database.records; i++){
+		r := database.records[i];
+		if(r != nil && r.datalen >= 0){
+			if(n == 0)
+				return i;
+			n--;
+		}
+	}
+	return -1;
+}
+
+dir(qid: Sys->Qid, name: string, length: big, uid: string, perm: int): ref Sys->Dir
+{
+	d := ref sys->zerodir;
+	d.qid = qid;
+	if(qid.qtype & Sys->QTDIR)
+		perm |= Sys->DMDIR;
+	d.mode = perm;
+	d.name = name;
+	d.uid = uid;
+	d.gid = uid;
+	d.length = length;
+	return d;
+}
+
+dirgen(p: big): (ref Sys->Dir, string)
+{
+	case TYPE(p) {
+	Qdir =>
+		return (dir(Qid(QPATH(0, Qdir),database.vers,Sys->QTDIR), "/", big 0, user, 8r700), nil);
+	Qnew =>
+		return (dir(Qid(QPATH(0, Qnew),0,Sys->QTFILE), "new", big 0, user, 8r600), nil);
+	Qindex =>
+		return (dir(Qid(QPATH(0, Qindex),0,Sys->QTFILE), "index", big 0, user, 8r600), nil);
+	Qstats =>
+		return (dir(Qid(QPATH(0, Qstats),0,Sys->QTFILE), "stats", big 0, user, 8r400), nil);
+	* =>
+		n := id2recno(FILENO(p));
+		if(n < 0 || n >= len database.records)
+			return (nil, nil);
+		r := database.records[n];
+		if(r == nil || r.datalen < 0)
+			return (nil, Enotfound);
+		l := r.datalen;
+		if(l < 0)
+			l = 0;
+		return (dir(r.qid(), sys->sprint("%d", r.id), big l, user, 8r600), nil);
+	}
+}
+
+navigator(navops: chan of ref Navop)
+{
+	while((m := <-navops) != nil){
+		pick n := m {
+		Stat =>
+			n.reply <-= dirgen(n.path);
+		Walk =>
+			if(int n.path != Qdir){
+				n.reply <-= (nil, "not a directory");
+				break;
+			}
+			case n.name {
+			".." =>
+				;	# nop
+			"new" =>
+				n.path = QPATH(0, Qnew);
+			"stats" =>
+				if(!database.indexing){
+					n.reply <-= (nil, Enotfound);
+					continue;
+				}
+				n.path = QPATH(0, Qstats);
+			"index" =>
+				if(!database.indexing){
+					n.reply <-= (nil, Enotfound);
+					continue;
+				}
+				n.path = QPATH(0, Qindex);
+			* =>
+				if(len n.name < 1 || !(n.name[0]>='0' && n.name[0]<='9')){	# weak test for now
+					n.reply <-= (nil, Enotfound);
+					continue;
+				}
+				n.path = QPATH(int n.name, Qdata);
+			}
+			n.reply <-= dirgen(n.path);
+		Readdir =>
+			if(int m.path != Qdir){
+				n.reply <-= (nil, "not a directory");
+				break;
+			}
+			o := 1;	# Qnew;
+			stats := -1;
+			indexing := -1;
+			if(database.indexing)
+				indexing = o++;
+			if(database.stats)
+				stats = o++;
+		    Dread:
+			for(i := n.offset; --n.count >= 0; i++){
+				case i {
+				0 =>
+					n.reply <-= dirgen(QPATH(0,Qnew));
+				* =>
+					if(i == indexing)
+						n.reply <-= dirgen(QPATH(0, Qindex));
+					if(i == stats)
+						n.reply <-= dirgen(QPATH(0, Qstats));
+					j := dirslot(i-o);	# n² but fine if the file will be small
+					if(j < 0)
+						break Dread;
+					r := database.records[j];
+					n.reply <-= dirgen(QPATH(r.id,Qdata));
+				}
+			}
+			n.reply <-= (nil, nil);
+		}
+	}
+}
+
+QPATH(w, q: int): big
+{
+	return big ((w<<8)|q);
+}
+
+TYPE(path: big): int
+{
+	return int path & 16rFF;
+}
+
+FILENO(path: big): int
+{
+	return (int path >> 8) & 16rFFFFFF;
+}
+
+Database.build(f: ref Iobuf, locking, indexing, stats: int, updcmd: string): (ref Database, string)
+{
+	rl: list of ref Record;
+	offset := 0;
+	maxid := 0;
+	for(;;) {
+		d := array[HEADLEN] of byte;
+		n := f.read(d, HEADLEN);
+		if(n < HEADLEN)
+			break;
+		orig := s := string d;
+		if(len s != HEADLEN)
+			return (nil, "found bad header");
+		r := ref Record;
+		r.vers = 0;
+		(r.count, s) = str->toint(s, 10);
+		(r.datalen, s) = str->toint(s, 10);
+		if(s != "\n")
+			return (nil, sys->sprint("found bad header '%s'\n", orig));
+		r.offset = offset + HEADLEN;
+		offset += r.count + HEADLEN;
+		f.seek(big offset, Bufio->SEEKSTART);
+		r.id = maxid++;
+		rl = r :: rl;
+	}
+	db := ref Database(f, array[maxid] of ref Record, maxid, locking, nil, indexing, stats, -1, 0, 0, 0, 0, updcmd, 0);
+	for(i := len db.records - 1; i >= 0; i--) {
+		db.records[i] = hd rl;
+		rl = tl rl;
+	}
+	return (db, nil);
+}
+
+Database.write(db: self ref Database, recno: int, data: array of byte): int
+{
+	db.s_writes++;
+	r := db.records[recno];
+	r.vers++;
+	if(len data <= r.count) {
+		if(r.count - len data >= HEADLEN + MINSIZE)
+			splitrec(db, recno, len data);
+		writerec(db, recno, data);
+		db.file.flush();
+	} else {
+		freerec(db, recno);
+		n := allocrec(db, len data);
+		if(n == -1)
+			return -1;		# BUG: we lose the original data in this case.
+		db.records[n].id = r.id;
+		db.write(n, data);
+	}
+	return 0;
+}
+
+Database.create(db: self ref Database, data: array of byte): ref Record
+{
+	db.s_creates++;
+	db.vers++;
+	n := allocrec(db, len data);
+	if(n < 0)
+		return nil;
+	if(db.write(n, data) < 0){
+		freerec(db, n);
+		return nil;
+	}
+	r := db.records[n];
+	r.id = db.maxid++;
+	return r;
+}
+
+Database.read(db: self ref Database, recno: int): array of byte
+{
+	db.s_reads++;
+	r := db.records[recno];
+	if(r.datalen <= 0)
+		return nil;
+	db.file.seek(big r.offset, Bufio->SEEKSTART);
+	d := array[r.datalen] of byte;
+	n := db.file.read(d, r.datalen);
+	if(n != r.datalen) {
+		sys->fprint(stderr, "dbfs: only read %d bytes (expected %d)\n", n, r.datalen);
+		return nil;
+	}
+	return d;
+}
+
+Database.remove(db: self ref Database, recno: int)
+{
+	db.s_removes++;
+	db.vers++;
+	freerec(db, recno);
+	db.file.flush();
+}
+
+Database.updated(db: self ref Database)
+{
+	if(db.updcmd != nil)
+		sh->system(context, db.updcmd);
+}
+
+# Locking - try to lock a record
+
+Database.lock(db: self ref Database, c: ref Styxservers->Fid): int
+{
+	if(TYPE(c.path) != Qdata || !db.locking)
+		return 1;
+	for(ll := db.locklist; ll != nil; ll = tl ll) {
+		lock := hd ll;
+		if(lock.qpath == c.path)
+			return lock.fid == c.fid;
+	}
+	db.locklist = (c.path, c.fid) :: db.locklist;
+	return 1;
+}
+
+
+# Locking - unlock a record
+
+Database.unlock(db: self ref Database, c: ref Styxservers->Fid)
+{
+	if(TYPE(c.path) != Qdata || !db.locking)
+		return;
+	ll := db.locklist;
+	db.locklist = nil;
+	for(; ll != nil; ll = tl ll){
+		lock := hd ll;
+		if(lock.qpath == c.path && lock.fid == c.fid){
+			# not replaced on list
+		}else
+			db.locklist = hd ll :: db.locklist;
+	}
+}
+
+
+# Locking - check if Fid c has the lock on its record
+
+Database.ownlock(db: self ref Database, c: ref Styxservers->Fid): int
+{
+	if(TYPE(c.path) != Qdata || !db.locking)
+		return 1;
+	for(ll := db.locklist; ll != nil; ll = tl ll) {
+		lock := hd ll;
+		if(lock.qpath == c.path)
+			return lock.fid == c.fid;
+	}
+	return 0;
+}
+
+Record.new(offset: int, length: int): ref Record
+{
+	return ref Record(-1, offset, length, -1, 0);
+}
+
+Record.qid(r: self ref Record): Qid
+{
+	return Qid(QPATH(r.id,Qdata), r.vers, Sys->QTFILE);
+}
+
+freerec(db: ref Database, recno: int)
+{
+	nr := len db.records;
+	db.records[recno].datalen = -1;
+	for(i := recno; i >= 0; i--)
+		if(db.records[i].datalen != -1)
+			break;
+	f := i + 1;
+	nb := 0;
+	for(i = f; i < nr; i++) {
+		if(db.records[i].datalen != -1)
+			break;
+		nb += db.records[i].count + HEADLEN;
+	}
+	db.records[f].count = nb - HEADLEN;
+	writeheader(db.file, db.records[f]);
+	# could blank out freed entries here if we cared.
+	if(i < nr && f < i)
+		db.records[f+1:] = db.records[i:];
+	db.records = db.records[0:nr - (i - f - 1)];
+}
+
+splitrec(db: ref Database, recno: int, pos: int)
+{
+	a := array[len db.records + 1] of ref Record;
+	a[0:] = db.records[0:recno+1];
+	if(recno < len db.records - 1)
+		a[recno+2:] = db.records[recno+1:];
+	db.records = a;
+	r := a[recno];
+	a[recno+1] = Record.new(r.offset + pos + HEADLEN, r.count - HEADLEN - pos);
+	r.count = pos;
+	writeheader(db.file, a[recno+1]);
+}
+
+writerec(db: ref Database, recno: int, data: array of byte): int
+{
+	db.records[recno].datalen = len data;
+	if(writeheader(db.file, db.records[recno]) == -1)
+		return -1;
+	if(db.file.write(data, len data) == Bufio->ERROR)
+		return -1;
+	return 0;
+}
+
+writeheader(f: ref Iobuf, r: ref Record): int
+{
+	f.seek(big r.offset - big HEADLEN, Bufio->SEEKSTART);
+	if(f.puts(sys->sprint("%4d %4d\n", r.count, r.datalen)) == Bufio->ERROR) {
+		sys->fprint(stderr, "dbfs: error writing header (id %d, offset %d, count %d, datalen %d): %r\n",
+					r.id, r.offset, r.count, r.datalen);
+		return -1;
+	}
+	return 0;
+}
+
+# finds or creates a record of the requisite size; does not mark it as allocated.
+allocrec(db: ref Database, nb: int): int
+{
+	if(nb < MINSIZE)
+		nb = MINSIZE;
+	best := -1;
+	n := -1;
+	for(i := 0; i < len db.records; i++) {
+		r := db.records[i];
+		if(r.datalen == -1) {
+			avail := r.count - nb;
+			if(avail >= 0 && (n == -1 || avail < best)) {
+				best = avail;
+				n = i;
+			}
+		}
+	}
+	if(n != -1)
+		return n;
+	nr := len db.records;
+	a := array[nr + 1] of ref Record;
+	a[0:] = db.records[0:];
+	offset := 0;
+	if(nr > 0)
+		offset = a[nr-1].offset + a[nr-1].count;
+	db.file.seek(big offset, Bufio->SEEKSTART);
+	if(db.file.write(array[nb + HEADLEN] of {* => byte(0)}, nb + HEADLEN) == Bufio->ERROR
+			|| db.file.flush() == Bufio->ERROR) {
+		sys->fprint(stderr, "dbfs: write of new entry failed: %r\n");
+		return -1;
+	}
+	a[nr] = Record.new(offset + HEADLEN, nb);
+	db.records = a;
+	return nr;
+}
+
+now(fd: ref Sys->FD): int
+{
+	if(fd == nil)
+		return 0;
+	buf := array[128] of byte;
+	sys->seek(fd, big 0, 0);
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return 0;
+	t := (big string buf[0:n]) / big 1000000;
+	return int t;
+}
--- /dev/null
+++ b/appl/cmd/rcmd.b
@@ -1,0 +1,160 @@
+implement Rcmd;
+
+include "sys.m";
+include "draw.m";
+include "arg.m";
+include "keyring.m";
+include "dial.m";
+include "security.m";
+
+Rcmd: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+DEFAULTALG := "none";
+sys: Sys;
+auth: Auth;
+dial: Dial;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dial = load Dial Dial->PATH;
+	if(dial == nil)
+		badmodule(Dial->PATH);
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		badmodule(Arg->PATH);
+	arg->init(argv);
+	alg: string;
+	doauth := 1;
+	exportpath := "/";
+	keyfile: string;
+	arg->setusage("rcmd [-A] [-f keyfile] [-e alg] [-x exportpath] tcp!mach cmd");
+	while((o := arg->opt()) != 0)
+		case o {
+		'e' or 'a' =>
+			alg = arg->earg();
+		'A' =>
+			doauth = 0;
+		'x' =>
+			exportpath = arg->earg();
+			(n, nil) := sys->stat(exportpath);
+			if (n == -1 || exportpath == nil)
+				arg->usage();
+		'f' =>
+			keyfile = arg->earg();
+			if (! (keyfile[0] == '/' || (len keyfile > 2 &&  keyfile[0:2] == "./")))
+				keyfile = "/usr/" + user() + "/keyring/" + keyfile;
+		*   =>
+			arg->usage();
+		}
+
+	argv = arg->argv();
+	if(argv == nil)
+		arg->usage();
+	arg = nil;
+
+	if (doauth && alg == nil)
+		alg = DEFAULTALG;
+
+	addr := hd argv;
+	argv = tl argv;
+
+	args := "";
+	while(argv != nil){
+		args += " " + hd argv;
+		argv = tl argv;
+	}
+	if(args == "")
+		args = "sh";
+
+	kr: Keyring;
+	au: Auth;
+	if (doauth) {
+		kr = load Keyring Keyring->PATH;
+		if(kr == nil)
+			badmodule(Keyring->PATH);
+		au = load Auth Auth->PATH;
+		if(au == nil)
+			badmodule(Auth->PATH);
+		if (keyfile == nil)
+			keyfile = "/usr/" + user() + "/keyring/default";
+	}
+
+	c := dial->dial(dial->netmkaddr(addr, "tcp", "rstyx"), nil);
+	if(c == nil)
+		error(sys->sprint("dial %s failed: %r", addr));
+
+	fd := c.dfd;
+	if (doauth) {
+		ai := kr->readauthinfo(keyfile);
+		#
+		# let auth->client handle nil ai
+		# if(ai == nil){
+		#	sys->fprint(stderr(), "rcmd: certificate for %s not found\n", addr);
+		#	raise "fail:no certificate";
+		# }
+		#
+
+		err := au->init();
+		if(err != nil)
+			error(err);
+
+		(fd, err) = au->client(alg, ai, c.dfd);
+		if(fd == nil){
+			sys->fprint(stderr(), "rcmd: authentication failed: %s\n", err);
+			raise "fail:auth failed";
+		}
+	}
+	t := array of byte sys->sprint("%d\n%s\n", len (array of byte args)+1, args);
+	if(sys->write(fd, t, len t) != len t){
+		sys->fprint(stderr(), "rcmd: cannot write arguments: %r\n");
+		raise "fail:bad arg write";
+	}
+
+	if(sys->export(fd, exportpath, sys->EXPWAIT) < 0) {
+		sys->fprint(stderr(), "rcmd: export: %r\n");
+		raise "fail:export failed";
+	}
+}
+
+exists(f: string): int
+{
+	(ok, nil) := sys->stat(f);
+	return ok >= 0;
+}
+
+user(): string
+{
+	sys = load Sys Sys->PATH;
+
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n];	
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+badmodule(p: string)
+{
+	sys->fprint(stderr(), "rcmd: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+error(e: string)
+{
+	sys->fprint(stderr(), "rcmd: %s\n", e);
+	raise "fail:errors";
+}
--- /dev/null
+++ b/appl/cmd/rdp.b
@@ -1,0 +1,1230 @@
+implement Rdp;
+include "sys.m";
+	sys: Sys;
+	print, sprint: import sys;
+include "draw.m";
+include "string.m";
+	str: String;
+
+df_port: con "/dev/eia0";
+df_bps: con 38400;
+
+Rdp: module
+{
+	init:	fn(nil: ref Draw->Context, arg: list of string);
+};
+
+dfd: ref sys->FD;
+cfd: ref sys->FD;
+ifd: ref sys->FD;
+pifd: ref sys->FD;
+p_isopen := 0;
+
+R_R15: con 15;
+R_PC: con 16;
+R_CPSR: con 17;
+R_SPSR: con 18;
+NREG: con 19;
+
+debug := 0;
+nocr := 0;
+tmode := 0;
+# echar := 16r1c;		# ctrl-\
+echar := 16r1d;		# ctrl-]  (because Tk grabs the ctrl-\ )
+
+bint(x: int): array of byte
+{
+	b := array[4] of byte;
+	b[0] = byte x;
+	b[1] = byte (x>>8);
+	b[2] = byte (x>>16);
+	b[3] = byte (x>>24);
+	return b;
+}
+
+intb(b: array of byte): int
+{
+	return int b[0] | (int b[1] << 8)
+		| (int b[2] << 16) | (int b[3] << 24);
+}
+
+
+statusmsg(n: int): string
+{
+	m: string;
+	case n {
+	0 => 	m = nil;
+	1 =>	m = "Reset";
+	2 =>	m = "Undefined instruction";
+	3 =>	m = "Software interrupt";
+	4 =>	m = "Prefetch abort";
+	5 =>	m = "Data abort";
+	6 =>	m = "Address exception";
+	7 =>	m = "IRQ";
+	8 =>	m = "FIQ";
+	9 =>	m = "Error";
+	10 =>	m = "Branch Through 0";
+	253 =>	m = "Insufficient privilege";
+	254 =>	m = "Unimplemented message";
+	255 =>	m = "Undefined message";
+	* =>	m = sprint("Status %d", n);
+	}
+	return m;
+}
+
+sdc: chan of (array of byte, int);
+scc: chan of int;
+
+serinp()
+{		
+	b: array of byte = nil;
+	save: array of byte = nil;
+	x := 0;
+	for(;;) {
+		m := <- scc;
+		if(m == 0) {
+			save = b[0:x];
+			continue;
+		}
+		b = nil;
+		t: int;
+		do {
+			alt {
+			m = <- scc =>
+				if(m == 0) 
+					print("<strange error>\n");
+				b = nil;
+			* =>
+				;
+			}
+			if(b == nil) {
+				if(m >= 0)
+					t = m;
+				else
+					t = -m;
+				x = 0;
+				b = array[t] of byte;
+			}
+			if(save != nil) {
+				r := len save;
+				if(r > (t-x))
+					r = t-x;
+				b[x:] = save[0:r];
+				save = save[r:];
+				if(len save == 0)
+					save = nil;
+				x += r;
+				continue;
+			}
+			r := sys->read(dfd, b[x:], t-x);
+			if(r < 0)
+				sdc <-= (array of byte sprint("fail:%r"), -1);
+			if(r == 0) 
+				sdc <-= (array of byte "fail:hangup", -1);
+			if(debug) {
+				if(r == 1)
+					print("<%ux>", int b[x]);
+				else
+					print("<%ux,%ux...(%d)>", int b[x], int b[x+1], r);
+			}
+			x += r;
+		} while(m >= 0 && x < t);
+		sdc <-= (b, x);
+	}
+}
+
+
+sreadn(n: int): array of byte
+{
+	b: array of byte;
+	if(n == 0)
+		return array[0] of byte;
+	scc <-= n;
+	(b, n) = <- sdc;
+	if(n < 0)
+		raise string b;
+	return b[0:n];
+}
+
+
+# yes, it's kind of a hack...
+fds := array[32] of ref Sys->FD;
+
+oscmd()
+{
+	arg := array[4] of int;
+	buf := array[4] of array of byte;
+	b := sreadn(5);
+	op := intb(b[:4]);
+	argd := int b[4];
+	for(i := 0; i<4; i++) {
+		t := (argd >> (i*2))&3;
+		case t {
+		0 =>	;
+		1 =>
+			arg[i] = int sreadn(1)[0];
+		2 =>
+			arg[i] = intb(sreadn(4));
+		3 =>
+			c := int sreadn(1)[0];
+			if(c < 255) {
+				buf[i] = array[c] of byte;
+				if(c <= 32) {
+					buf[i][0:] = sreadn(c);
+				} else 
+					arg[i] = intb(sreadn(4));
+			} else {
+				b: array of byte;
+				b = sreadn(8);
+				c = intb(b[:4]);
+				arg[i] = intb(b[4:8]);
+				buf[i] = array[c] of byte;
+			}
+		}
+	}
+	for(i = 0; i<4; i++)
+		if(buf[i] != nil && len buf[i] > 32) 
+			rdi_read(arg[i], buf[i], len buf[i]);
+
+	r := 0;
+	case op {
+	0 or 2 => ;
+	* =>
+		out("");
+	}
+	case op {
+	0 =>
+		if(debug)
+			print("SWI_WriteC(%d)\n", arg[0]);
+		out(string byte arg[0]);
+	2 =>
+		if(debug)
+			print("SWI_Write0(<%d>)\n", len buf[0]);
+		out(string buf[0]);
+	4 =>
+		if(debug)
+			print("SWI_ReadC()\n");
+		sys->read(ifd, b, 1);
+		r = int b[0];
+	16r66 =>
+		fname := string buf[0];
+		if(debug)
+			print("SWI_Open(%s, %d)\n", fname, arg[1]);
+		fd: ref Sys->FD;
+		case arg[1] {
+		0 or 1 =>
+			fd = sys->open(fname, Sys->OREAD);
+		2 or 3 =>
+			fd = sys->open(fname, Sys->ORDWR);
+		4 or 5 =>
+			fd = sys->open(fname, Sys->OWRITE);
+			if(fd == nil)
+				fd = sys->create(fname, Sys->OWRITE, 8r666);
+		6 or 7 =>
+			fd = sys->open(fname, Sys->OWRITE|Sys->OTRUNC);
+			if(fd == nil)
+				fd = sys->create(fname, Sys->OWRITE, 8r666);
+		8 or 9 =>
+			fd = sys->open(fname, Sys->OWRITE);
+			if(fd == nil)
+				fd = sys->create(fname, Sys->OWRITE, 8r666);
+			else
+				sys->seek(fd, big 0, Sys->SEEKEND);
+		10 or 11 =>
+			fd = sys->open(fname, Sys->ORDWR);
+			if(fd == nil)
+				fd = sys->create(fname, Sys->ORDWR, 8r666);
+			else
+				sys->seek(fd, big 0, Sys->SEEKEND);
+		}
+		if(fd != nil) {
+			r = fd.fd;
+			if(r >= len fds) {
+				print("<fd %d out of range 1-%d>\n", r, len fds);
+				r = 0;
+			} else 
+				fds[r] = fd;
+		} 
+	16r68 =>
+		if(debug)
+			print("SWI_Close(%d)\n", arg[0]);
+		if(arg[0] <= 0 || arg[0] >= len fds)
+			r = -1;
+		else {
+			if(fds[arg[0]] != nil)
+				fds[arg[0]] = nil;
+			else
+				r = -1;
+		}
+	16r69 =>
+		if(debug)
+			print("SWI_Write(%d, <%d>)\n", arg[0], len buf[1]);
+		if(arg[0] <= 0 || arg[0] >= len fds)
+			r = -1;
+		else 
+			r = sys->write(fds[arg[0]], buf[1], len buf[1]);
+		r = arg[2]-r;
+	16r6a =>
+		if(debug)
+			print("SWI_Read(%d, 0x%ux, %d)\n", arg[0], arg[1], arg[2]);
+		if(arg[0] <= 0 || arg[0] >= len fds)
+			r = -1;
+		else {
+			d := array[arg[2]] of byte;
+			r = sys->read(fds[arg[0]], d, arg[2]);
+			if(r > 0)
+				rdi_write(d, arg[1], r);
+		}
+		r = arg[2]-r;
+	16r6b =>
+		if(debug)
+			print("SWI_Seek(%d, %d)\n", arg[0], arg[1]);
+		if(arg[0] <= 0 || arg[0] >= len fds)
+			r = -1;
+		else 
+			r = int sys->seek(fds[arg[0]], big arg[1], 0);
+	16r6c =>
+		if(debug)
+			print("SWI_Flen(%d)\n", arg[0]);
+		if(arg[0] <= 0 || arg[0] >= len fds)
+			r = -1;
+		else {
+			d: Sys->Dir;
+			(r, d) = sys->fstat(fds[arg[0]]);
+			if(r >= 0)
+				r = int d.length;
+		}
+	16r6e =>
+		if(debug)
+			print("SWI_IsTTY(%d)\n", arg[0]);
+		r = 0;	# how can we detect if it's a TTY?
+	* =>
+		print("unsupported: SWI 0x%ux\n", op);
+	}
+	b = array[6] of byte;
+	b[0] = byte 16r13;
+	if(debug)
+		print("r0=%d\n", r);
+	if(r >= 0 && r <= 16rff) {
+		b[1] = byte 1;
+		b[2] = byte r;
+		sys->write(dfd, b, 3);
+	} else {
+		b[1] = byte 2;
+		b[2:] = bint(r);
+		sys->write(dfd, b, 6);
+	}
+}
+
+
+terminal()
+{
+	b := array[1024] of byte;
+	c := 3;	# num of invalid chars before resetting
+	tmode = 1;
+	for(;;) {
+		n: int;
+		b: array of byte;
+		alt {
+		scc <-= -8192 =>
+			(b, n) = <- sdc;
+		(b, n) = <- sdc =>
+			;
+		}
+		if(n < 0) 
+			raise string b;
+		c -= out(string b[:n]);
+		if(c < 0) {
+			scc <-= 0;
+			raise "rdp:tmode";
+		}
+		if(!tmode) {
+			return;
+		}
+	}
+}
+
+getreply(n: int): (array of byte, int)
+{
+	loop: for(;;) {
+		c := int sreadn(1)[0];
+		case c {
+		16r21 =>
+			oscmd();
+		16r7f =>
+			raise "rdp:reset";
+		16r5f =>
+			break loop;
+		* =>
+			print("<%ux?>", c);
+			scc <-= 0;
+			raise "rdp:tmode";
+		}
+	}
+	b := sreadn(n+1);
+	s := int b[n];
+	if(s != 0) {
+		out("");
+		print("[%s]\n", statusmsg(s));
+	}
+	return (b[:n], s);
+}
+
+outstr: string;
+tpid: int;
+
+timeout(t: int, c: chan of int)
+{
+	tpid = sys->pctl(0, nil);
+	if(t > 0)
+		sys->sleep(t);
+	c <-= 0;
+	tpid = 0;
+}
+
+bsc: chan of string;
+
+bufout()
+{
+	buf := "";
+	tc := chan of int;
+	n: int;
+	s: string;
+	for(;;) {
+		alt {
+		n = <- tc =>
+			print("%s", buf);
+			buf = "";
+		s = <- bsc =>
+			#if(tpid) {
+			#	kill(tpid);
+			#	tpid = 0;
+			#}
+			if((len buf+len s) >= 1024) {
+				print("%s", buf);
+				buf = s;
+			}
+			if(s == "" || debug) {
+				print("%s", buf);
+				buf = "";
+			} else {
+				buf += s;
+				if(tpid == 0) 
+					spawn timeout(300, tc);
+			}
+		}
+	}
+}
+
+out(s: string): int
+{
+	if(bsc == nil) {
+		bsc = chan of string;
+		spawn bufout();
+	}
+	c := 0;
+	if(nocr || tmode) {
+		n := "";
+		for(i:=0; i<len s; i++) {
+			if(!(nocr && s[i] == '\r'))
+				n[len n] = s[i];
+			if(s[i] >= 16r7f)
+				c++;
+		}
+		bsc <-= n;
+	} else
+		bsc <-= s;
+	return c;
+}
+
+reset(r: int)
+{
+	out("");
+	if(debug)
+		print("reset(%d)\n", r);
+	p_isopen = 0;
+	b := array of byte sprint("b9600");
+	sys->write(cfd, b, len b);
+	if(r) {
+		b[0] = byte 127;
+		sys->write(dfd, b, 1);
+		print("<sending reset>");
+	}
+	ok := 0;
+	s := "";
+	for(;;) {
+		n: int;
+		b: array of byte;
+		scc <-= -8192;
+		(b, n) = <- sdc;
+		if(n < 0) 
+			raise string b;
+		for(i := 0; i<n; i++) {
+			if(b[i] == byte 127) {
+				if(!ok)
+					print("\n");
+				ok = 1;
+				s = "";
+				continue;
+			}
+			if(b[i] == byte 0) {
+				if(ok && i == n-1) {
+					out(s);
+					out("");
+					return;
+				} else {
+					s = "";
+					continue;
+				}
+			}
+			if(b[i] < byte 127)
+				s += string b[i:i+1];
+			else
+				ok = 0;
+		}
+	}
+}
+
+sa1100_reset()
+{
+	rdi_write(bint(1), int 16r90030000, 4);
+}
+
+setbps(bps: int)
+{
+	# for older Emu's using setserial hacks...
+	if(bps > 38400)
+		sys->write(cfd, array of byte "b38400", 6);
+
+	out("");
+	print("<bps=%d>\n", bps);
+	b := array of byte sprint("b%d", bps);
+	if(sys->write(cfd, b, len b) != len b) 
+		print("setbps failed: %r\n");
+}
+
+rdi_open(bps: int)
+{	
+	if(debug)
+		print("rdi_open(%d)\n", bps);
+	b := array[7] of byte;
+	usehack := 0;
+	if(!p_isopen) {
+		b[0] = byte 0;
+		b[1] = byte (0 | (1<<1));
+		b[2:] = bint(0);
+		case bps {
+		9600 => b[6] = byte 1;
+		19200 => b[6] = byte 2;
+		38400 => b[6] = byte 3;
+		# 57600 => b[6] = byte 4;
+		# 115200 => b[6] = byte 5;
+		# 230400 => b[6] = byte 6;
+		* =>
+			b[6] = byte 1;
+			usehack = 1;
+		}
+		sys->write(dfd, b, 7);
+		getreply(0);
+		p_isopen = 1;
+		if(usehack)
+			sa1100_setbps(bps);
+		else
+			setbps(bps);
+	}
+}
+
+rdi_close()
+{
+	if(debug)
+		print("rdi_close()\n");
+	b := array[1] of byte;
+	if(p_isopen) {
+		b[0] = byte 1;
+		sys->write(dfd, b, 1);
+		getreply(0);
+		p_isopen = 0;
+	}
+}
+
+rdi_cpuread(reg: array of int, mask: int)
+{
+	if(debug)
+		print("rdi_cpuread(..., 0x%ux)\n", mask);
+	n := 0;
+	for(i := 0; i<NREG; i++)
+		if(mask&(1<<i))
+			n += 4;
+	b := array[6+n] of byte;
+	b[0] = byte 4;
+	b[1] = byte 255;	# current mode
+	b[2:] = bint(mask);
+	sys->write(dfd, b, 6);
+	(b, nil) = getreply(n);
+	n = 0;
+	for(i = 0; i<NREG; i++)
+		if(mask&(1<<i)) {
+			reg[i] = intb(b[n:n+4]);
+			n += 4;
+		}
+}
+
+rdi_cpuwrite(reg: array of int, mask: int)
+{
+	if(debug)
+		print("rdi_cpuwrite(..., 0x%ux)\n", mask);
+	n := 0;
+	for(i := 0; i<32; i++)
+		if(mask&(1<<i))
+			n += 4;
+	b := array[6+n] of byte;
+	b[0] = byte 5;
+	b[1] = byte 255;	# current mode
+	b[2:] = bint(mask);
+	n = 6;
+	for(i = 0; i<32; i++)
+		if(mask&(1<<i)) {
+			b[n:] = bint(reg[i]);
+			n += 4;
+		}
+	sys->write(dfd, b, n);
+	getreply(0);
+}
+
+dump(b: array of byte, n: int)
+{
+	for(i := 0; i<n; i++)
+		print(" %d: %2.2ux\n", i, int b[i]);
+}
+
+rdi_read(addr: int, b: array of byte, n: int): int
+{
+	if(debug)
+		print("rdi_read(0x%ux, ..., 0x%ux)\n", addr, n);
+	if(n == 0)
+		return 0;
+	sb := array[9] of byte;
+	sb[0] = byte 2;
+	sb[1:] = bint(addr);
+	sb[5:] = bint(n);
+	sys->write(dfd, sb, 9);
+	(b[0:], nil) = getreply(n);
+	# if error, need to read count of bytes transferred
+	return n;
+}
+
+rdi_write(b: array of byte, addr: int, n: int): int
+{
+	if(debug)
+		print("rdi_write(..., 0x%ux, 0x%ux)\n", addr, n);
+	if(n == 0)
+		return 0;
+	sb := array[9+n] of byte;
+	sb[0] = byte 3;
+	sb[1:] = bint(addr);
+	sb[5:] = bint(n);
+	sb[9:] = b[:n];
+	sys->write(dfd, sb, 9);
+	x := 0;
+	while(n) {
+		q := n;
+		if(q > 8192)
+			q = 8192;
+		r := sys->write(dfd, b[x:], q);
+		if(debug)
+			print("rdi_write: r=%d ofs=%d n=%d\n", r, x, n);
+		if(r < 0)
+			raise "fail:hangup";
+		x += r;
+		n -= r;
+	}
+	getreply(0);
+	return n;
+}
+
+rdi_execute()
+{
+	if(debug)
+		print("rdi_execute()\n");
+	sb := array[2] of byte;
+	sb[0] = byte 16r10;
+	sb[1] = byte 0;
+	sys->write(dfd, sb, 2);
+	getreply(0);
+	out("");
+}
+
+rdi_info(n: int, arg: int)
+{
+	sb := array[9] of byte;
+	sb[0] = byte 16r12;
+	sb[1:] = bint(n);
+	sb[5:] = bint(arg);
+	sys->write(dfd, sb, 9);
+	getreply(0);
+}
+
+
+regdump()
+{
+	out("");
+	reg := array[NREG] of int;
+	# rdi_cpuread(reg, 16rffff|(1<<R_PC)|(1<<R_CPSR)|(1<<R_SPSR));
+	rdi_cpuread(reg, 16rffff|(1<<R_PC)|(1<<R_CPSR));
+	for(i := 0; i < 16; i += 4)
+		print("  r%-2d=%8.8ux  r%-2d=%8.8ux  r%-2d=%8.8ux  r%-2d=%8.8ux\n",
+			i, reg[i], i+1, reg[i+1],
+			i+2, reg[i+2], i+3, reg[i+3]);
+	print("   pc=%8.8ux  psr=%8.8ux\n",
+			reg[R_PC], reg[R_CPSR]);
+}
+
+printable(b: array of byte): string
+{
+	s := "";
+	for(i := 0; i < len b; i++) 
+		if(b[i] >= byte ' ' && b[i] <= byte 126)
+			s += string b[i:i+1];
+		else
+			s += ".";
+	return s;
+}
+
+examine(a: int, n: int)
+{
+	b := array[4] of byte;
+	for(i := 0; i<n; i++) {
+		rdi_read(a, b, 4);
+		print("0x%8.8ux: 0x%8.8ux  \"%s\"\n", a, intb(b), printable(b));
+		a += 4;
+	}
+}
+
+atoi(s: string): int
+{
+	b := 10;
+	if(len s < 1)
+		return 0;
+	if(s[0] == '0') {
+		b = 8;
+		s = s[1:];
+		if(len s < 1)
+			return 0;
+		if(s[0] == 'x' || s[0] == 'X') {
+			b = 16;
+			s = s[1:];
+		}
+	}
+	n: int;
+	(n, nil) = str->toint(s, b);
+	return n;
+}
+
+regnum(s: string): int
+{
+	if(len s < 2)
+		return -1;
+	if(s[0] == 'r' && s[1] >= '0' && s[1] <= '9') 
+		return atoi(s[1:]);
+	case s {
+	"pc" => return R_PC;
+	"cpsr" or "psr" => return R_CPSR;
+	"spsr" => return R_SPSR;
+	* => return -1;
+	}
+}
+
+cmdhelp()
+{
+	print("	e <addr> [<count>]  - examine memory\n");
+	print("	d <addr> [<value>...]  - deposit values in memory\n");
+	print("	get <file> <addr>  - read file into memory at addr\n");
+	print("	load <file>  - load AIF file and set the PC\n");
+	print("	r  - print all registers\n");
+	print("	<reg>=<val>  - set register value\n");
+	print("	sb  - run builtin sboot (pc=0x40; g)\n");
+	print("	reset - trigger SA1100 software reset\n");
+	print("	bps <speed>  - change bps rate (SA1100 only)\n");
+	print("	q  - quit\n");
+}
+
+cmdmode()
+{
+	b := array[1024] of byte;
+	for(;;) {
+		print("rdp: ");
+		r := sys->read(ifd, b, len b);
+		if(r < 0)
+			raise sprint("fail:%r");
+		if(r == 0 || (r == 1 && b[0] == byte 4))
+			break;
+		n: int;
+		a: list of string;
+		(n, a) = sys->tokenize(string b[0:r], " \t\n=");
+		if(n < 1)
+			continue;
+		case hd a {
+		"sb" =>
+			sbmode();
+			rdi_execute();
+		"q" or "quit" =>
+			return;
+		"r" or "reg" =>
+			regdump();
+		"get" or "getfile" or "l" or "load" =>
+			{
+				if((hd a)[0] == 'l')
+					aifload(hd tl a, -1);
+				else
+					aifload(hd tl a, atoi(hd tl tl a));
+			}exception e{
+			"fail:*" =>
+				print("error: %s\n", e[5:]);
+				continue;
+			}
+		"g" or "go" =>
+			rdi_execute();
+		"reset" =>
+			sa1100_reset();
+		"e" =>
+			a = tl a;
+			x := atoi(hd a);
+			n = 1;
+			a = tl a;
+			if(a != nil)
+				n = atoi(hd a);
+			examine(x, n);
+		"d" =>
+			a = tl a;
+			x := atoi(hd a);
+			for(i := 2; i<n; i++) {
+				a = tl a;
+				rdi_write(bint(atoi(hd a)), x, 4);
+				x += 4;
+			}
+		"info" =>
+			a = tl a;
+			rdi_info(16r180, atoi(hd a));
+		"bps" =>
+			sa1100_setbps(atoi(hd tl a));
+		"help" or "?" =>
+			cmdhelp();
+		* =>
+			if((rn := regnum(hd a)) > -1) {
+				reg := array[NREG] of int;
+				reg[rn] = atoi(hd tl a);
+				rdi_cpuwrite(reg, 1<<rn);
+			} else
+				print("?\n");
+		}
+	}
+}
+
+sbmode()
+{
+	if(debug)
+		print("sbmode()\n");
+	reg := array[NREG] of int;
+	reg[R_PC] = 16r40;
+	rdi_cpuwrite(reg, 1<<R_PC);
+}
+
+sbmodeofs(ofs: int)
+{
+	if(debug)
+		print("sbmode(0x%ux)\n", ofs);
+	reg := array[NREG] of int;
+	reg[0] = ofs;
+	reg[R_PC] = 16r48;
+	rdi_cpuwrite(reg, (1<<0)|(1<<R_PC));
+}
+
+inp: string = "";
+
+help: con "(q)uit, (i)nt, (b)reak, !c(r), !(l)ine, !(t)erminal, (s<bps>), (.)cont, (!cmd)\n";
+
+menu(fi: ref Sys->FD)
+{
+	w := israw;
+	if(israw)
+		raw(0);
+mloop:	for(;;) {
+		out("");
+		print("rdp> ");
+		b := array[256] of byte;
+		r := sys->read(fi, b, len b);
+		case int b[0] {
+		'q' =>
+			killgrp();
+			exit;
+		'i' =>
+			b[0] = byte 16r18;
+			sys->write(dfd, b[0:1], 1);
+			break mloop;
+		'b' =>
+			sys->write(cfd, array of byte "k", 1);
+			break mloop;
+		'!' =>
+			cmd := string b[1:r-1];
+			print("!%s\n", cmd);
+			# system(cmd)
+			print("!\n");
+			break mloop;
+		'l' =>
+			w = !w;
+			break mloop;
+		'r' =>
+			nocr = !nocr;
+			break mloop;
+		'd' =>
+			debug = !debug;
+			break mloop;
+		't' =>
+			sys->write(pifd, array[] of { byte 4 }, 1);
+			sdc <-= (array of byte "rdp:tmode", -1);
+			break mloop;
+		'.' =>
+			break mloop;
+		's' =>
+			bps := atoi(string b[1:r-1]);
+			setbps(bps);
+		* =>
+			print(help);
+			continue;
+		}
+	} 
+	if(israw != w)
+		raw(w);
+}
+
+
+input()
+{
+	fi := sys->fildes(0);
+	b := array[1024] of byte;
+iloop: 	for(;;) {
+		r := sys->read(fi, b, len b);
+		if(r < 0) {
+			print("stdin: %r");
+			killgrp();
+			exit;
+		}
+		for(i:=0; i<r; i++) {
+			if(b[i] == byte echar) {
+				menu(fi);
+				continue iloop;
+			}
+		}
+		if(r == 0) {
+			b[0] = byte 4;	# ctrl-d
+			r = 1;
+		}
+		if(tmode)
+			sys->write(dfd, b, r);
+		else
+			sys->write(pifd, b, r);
+	}
+}
+
+ccfd: ref Sys->FD;
+israw := 0;
+
+raw(on: int)
+{
+	if(ccfd == nil) {
+		ccfd = sys->open("/dev/consctl", Sys->OWRITE);
+		if(ccfd == nil) { 
+			print("/dev/consctl: %r\n");
+			return;
+		}
+	}
+	if(on)
+		sys->fprint(ccfd, "rawon");
+	else
+		sys->fprint(ccfd, "rawoff");
+	israw = on;
+}
+
+killgrp()
+{
+	pid := sys->pctl(0, nil);
+	f := "/prog/"+string pid+"/ctl";
+	fd := sys->open(f, Sys->OWRITE);
+	if(fd == nil)
+		print("%s: %r\n", f);
+	else
+		sys->fprint(fd, "killgrp");
+}
+
+kill(pid: int)
+{
+	f := "/prog/"+string pid+"/ctl";
+	fd := sys->open(f, Sys->OWRITE);
+	if(fd == nil)
+		print("%s: %r\n", f);
+	else
+		sys->fprint(fd, "kill");
+}
+
+
+# Code for switching to previously unsupported bps rates:
+
+##define UTCR1	0x4
+##define UTCR2	0x8
+##define UTCR3	0xc
+##define UTDR	0x14
+##define UTSR0	0x1c
+##define UTSR1	0x20
+#
+#TEXT _startup(SB), $-4
+#	MOVW	$0x80000000,R2
+#	ORR	$0x00050000,R2
+#
+#	MOVW	$0, R1
+#	MOVW	R1, UTDR(R2)	/* send ack */
+#
+#wait:
+#	MOVW	UTSR1(R2), R1
+#	TST	$1, R1		/* TBY */
+#	BNE	wait
+#
+#	MOVW	$0x90000000,R3
+#	ORR	$0x00000010,R3
+#	MOVW	(R4),R1
+#	ADD	$0x5a000,R1	/* 100 ms */
+#delay1:
+#	MOVW	(R3),R1
+#	SUB.S	$0x5a000, R1	/* 100 ms */
+#	BLO	delay1
+# 
+#	MOVW	UTCR3(R2), R5	/* save utcr3 */
+#	MOVW	$0, R1
+#	MOVW	R1, UTCR3(R2)	/* disable xmt/rcv */
+#
+#	MOVW	R0, R1
+#	AND	$0xff, R1
+#	MOVW	R1, UTCR2(R2)
+#	MOVW	R0 >> 8, R1
+#	MOVW	R1, UTCR1(R2)
+#
+#	MOVW	$0xff, R1
+#	MOVW	R1, UTSR0(R2)	/* clear sticky bits */
+#
+#	MOVW	$3, R1
+#	MOVW	R1, UTCR3(R2)	/* enable xmt/rcv */
+#
+#	MOVW	$0, R0
+#sync:	
+#	MOVW	R0, UTDR(R2)	/* send sync char */
+#syncwait:
+#	MOVW	UTSR1(R2), R1 
+#	TST	$1, R1		/* TBY */
+#	BNE	syncwait
+#	TST	$2, R1		/* RNE */
+#	BEQ	sync
+#	MOVW	UTDR(R2), R0
+#	MOVW	R0, UTDR(R2)	/* echo rcvd char */
+#
+#	MOVW	$0xff, R1
+#	MOVW	R1, UTSR0(R2)	/* clear sticky bits */
+#	MOVW	R5, UTCR3(R2)	/* re-enable xmt/rcv and interrupts */
+#
+#	WORD	$0xef000011	/* exit */
+
+
+bpscode := array[] of {
+	16re3a22102, 16re3822805, 16re3a11000, 16re5821014,
+	16re5921020, 16re3110001, big 16r1afffffc, 16re3a33209,
+	16re3833010, 16re5941000, 16re2811a5a, 16re5931000,
+	16re2511a5a, big 16r3afffffc, 16re592500c, 16re3a11000,
+	16re582100c, 16re1a11000, 16re20110ff, 16re5821008,
+	16re1a11420, 16re5821004, 16re3a110ff, 16re582101c,
+	16re3a11003, 16re582100c, 16re3a00000, 16re5820014,
+	16re5921020, 16re3110001, big 16r1afffffc, 16re3110002,
+	big 16r0afffff9, 16re5920014, 16re5820014, 16re3a110ff,
+	16re582101c, 16re582500c, 16ref000011,
+};
+
+sa1100_setbps(bps: int)
+{
+	print("<sa1100_setbps %d>", bps);
+	nb := len bpscode*4;
+	b := array[nb] of byte;
+	for(i := 0; i < len bpscode; i++) 
+		b[i*4:] = bint(int bpscode[i]);
+	rdi_write(b, 16r8080, nb);
+	reg := array[NREG] of int;
+	d := (3686400/(bps*16))-1;
+	reg[0] = d;
+	reg[R_PC] = 16r8080;
+	rdi_cpuwrite(reg, (1<<0)|(1<<R_PC));
+	sb := array[2] of byte;
+	sb[0] = byte 16r10;
+	sb[1] = byte 0;
+	sys->write(dfd, sb, 2);
+	rb := sreadn(1);
+	setbps(bps);
+	do rb = sreadn(1);
+	while(rb[0] != byte 0);
+	sb[0] = byte 16rff;
+	sys->write(dfd, sb, 1);
+	do rb = sreadn(1);
+	while(rb[0] != sb[0]);
+	getreply(0);
+}
+
+aifload(fname: string, adr: int)
+{
+	out("");
+	if(adr < 0)
+		print("<aifload %s>\n", fname);
+	fd := sys->open(fname, Sys->OREAD);
+	if(fd == nil) 
+		raise sprint("fail:%s:%r", fname);
+	d: Sys->Dir;
+	(nil, d) = sys->fstat(fd);
+	b := array[int d.length] of byte;
+	sys->read(fd, b, len b);
+	if(adr < 0) {
+		if(len b < 128)
+			raise sprint("fail:%s:not aif", fname);
+		tsize := intb(b[20:24]);
+		dsize := intb(b[24:28]);
+		bsize := intb(b[32:36]);
+		tbase := intb(b[40:44]);
+		dbase := intb(b[52:56]);
+		print("%ux/%ux: %ux+%ux+%ux\n", tbase, dbase, tsize, dsize, bsize);
+		rdi_write(b, tbase, tsize+dsize);
+		reg := array[NREG] of int;
+		reg[R_PC] = tbase+8;
+		rdi_cpuwrite(reg, 1<<R_PC);
+	} else
+		rdi_write(b, adr, int d.length);
+}
+
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	port := df_port;
+	bps := df_bps;
+	usecmdmode := 0;
+	ofs := -1;
+	prog: string = nil;
+
+	argv = tl argv;
+	while(argv != nil) {
+		a := hd argv;
+		argv = tl argv;
+		if(len a >= 2 && a[0] == '-')
+			case a[1] {
+			'c' =>
+				usecmdmode = 1;
+			'O' =>
+				ofs = atoi(a[2:]);
+			'd' =>
+				debug = 1;
+			'p' =>
+				port = a[2:];
+			's' =>
+				bps = atoi(a[2:]);
+			'r' =>
+				nocr = 1;
+			'l' =>
+				raw(1);
+			'e' =>
+				if(a[2] == '^')
+					echar = a[3]&16r1f;
+				else
+					echar = a[2];
+			't' =>
+				tmode = 1;
+			'h' =>
+				print("usage: rdp [-crdlht] [-e<c>] [-O<ofs>] [-p<port>] [-s<bps>] [prog]\n");
+				return;
+			* =>
+				print("invalid option: %s\n", a);
+				return;
+			}
+		else
+			prog = a;
+	}
+
+	print("rdp 0.17 (port=%s, bps=%d)\n", port, bps);
+	dfd = sys->open(port, Sys->ORDWR);
+	if(dfd == nil) {
+		sys->print("open %s failed: %r\n", port);
+		return;
+	}
+	cfd = sys->open(port+"ctl", Sys->OWRITE);
+	if(cfd == nil) 
+		sys->print("warning: open %s failed: %r\n", port+"ctl");
+
+	pfd := array[2] of ref Sys->FD;
+	sys->pipe(pfd);
+	ifd = pfd[1];
+	pifd = pfd[0];
+	(scc, sdc) = (chan of int, chan of (array of byte, int));
+	spawn serinp();
+	spawn input();
+	r := 1;
+	{
+		if(tmode)
+			terminal();
+		reset(r);
+		if(!p_isopen) {
+			rdi_open(bps);
+			rdi_info(16r180, (1<<0)|(1<<1)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7)|(1<<8));
+		}
+		# print("\n<connection established>\n");
+		print("\n<contact has been made>\n");
+		if(usecmdmode) {
+			cmdmode();
+		} else {
+			if(prog != nil)
+				aifload(prog, -1); 
+			else if(ofs != -1)
+				sbmodeofs(ofs);
+			else
+				sbmode();
+			reg := array[NREG] of int;
+			# rdi_cpuread(reg, (1<<R_PC)|(1<<R_CPSR));
+			# print("<execute at %ux; cpsr=%ux>\n", reg[R_PC], reg[R_CPSR]);
+			rdi_cpuread(reg, (1<<R_PC));
+			print("<execute at %ux>\n", reg[R_PC]);
+			rdi_execute();
+		}
+		rdi_close();
+
+		# Warning: this will make Linux emu crash...
+		killgrp();
+	}exception e{
+	"fail:*" =>
+		if(israw)
+			raw(0);
+		killgrp();
+		raise e;
+	"rdp:*" =>
+		out("");
+		if(debug)
+			print("<exception: %s>\n", e);
+		case e {
+		"rdp:error" =>	;
+		"rdp:tmode" =>
+			tmode = !tmode;
+			if(tmode)
+				print("<terminal mode>\n");
+			else
+				print("<rdp mode>\n");
+		"rdp:reset" =>
+			r = 0;
+		* =>
+			r = 1;
+		}
+	}
+}
+
--- /dev/null
+++ b/appl/cmd/read.b
@@ -1,0 +1,62 @@
+implement Read;
+include "sys.m"; 
+	sys: Sys;
+include "draw.m";
+
+Read: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: read [-[ero] offset] count\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	# usage: read [-[ero] offset] count
+	count := Sys->ATOMICIO;
+	offset := big 0;
+	seeking := -1;
+	if (argv != nil)
+		argv = tl argv;
+	if (argv != nil && hd argv != nil && (hd argv)[0] == '-') {
+		if (tl argv == nil)
+			usage();
+		case hd argv {
+		"-o" =>
+			seeking = Sys->SEEKSTART;
+		"-e" =>
+			seeking = Sys->SEEKEND;
+		"-r" =>
+			seeking = Sys->SEEKRELA;
+		* =>
+			usage();
+		}
+		offset = big hd tl argv;
+		argv = tl tl argv;
+	}
+	if (argv != nil) {
+		if (tl argv != nil)
+			usage();
+		count = int hd argv;
+	}
+	fd := sys->fildes(0);
+	if (seeking != -1)
+		sys->seek(fd, offset, seeking);
+	if (count == 0)
+		return;
+	buf := array[count] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n > 0)
+		sys->write(sys->fildes(1), buf, n);
+	else {
+		if (n == -1) {
+			sys->fprint(sys->fildes(2), "read: read error: %r\n");
+			raise "fail:error";
+		}
+		raise "fail:eof";
+	}
+}
--- /dev/null
+++ b/appl/cmd/rioimport.b
@@ -1,0 +1,620 @@
+implement Rioimport;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Image, Point, Rect, Display, Screen: import draw;
+include "wmsrv.m";
+	wmsrv: Wmsrv;
+include "sh.m";
+	sh: Sh;
+include "string.m";
+	str: String;
+
+Rioimport: module{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+Client: adt{
+	ptrstarted:	int;
+	kbdstarted:	int;
+	state:		int;		# Hidden|Current
+	req:		chan of (array of byte, Sys->Rwrite);
+	resize:	chan of ref Riowin;
+	ptr:		chan of ref Draw->Pointer;
+	riowctl:	chan of (ref Riowin, int);
+	wins:	list of ref Riowin;
+	winfd:	ref Sys->FD;
+	sc: 		ref Wmsrv->Client;
+};
+
+Riowin: adt {
+	tag:		string;
+	img:		ref Image;
+	dir:		string;
+	state:	int;
+	ptrpid:	int;
+	kbdpid:	int;
+	ctlpid:	int;
+	ptrfd:	ref Sys->FD;
+	ctlfd:		ref Sys->FD;
+};
+
+Hidden, Current: con 1<<iota;
+Ptrsize: con 1+4*12;		# 'm' plus 4 12-byte decimal integers
+P9PATH: con "/n/local";
+Borderwidth: con 4;		# defined in /sys/include/draw.h
+
+display: ref Display;
+wsysseq := 0;
+screenr := Rect((0, 0), (640, 480));	# no way of getting this reliably from rio
+
+Minwinsize: con Point(100, 42);
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	sh = load Sh Sh->PATH;
+	sh->initialise();
+	str = load String String->PATH;
+	wmsrv = load Wmsrv Wmsrv->PATH;
+
+	wc := chan of (ref Draw->Context, string);
+	spawn rioproxy(wc);
+	(ctxt, err) := <-wc;
+	if(err != nil){
+		sys->fprint(sys->fildes(2), "rioimport: %s\n", err);
+		raise "fail:no display";
+	}
+	sh->run(ctxt, tl argv);
+}
+
+ebind(a, b: string, flag: int)
+{
+	if(sys->bind(a, b, flag) == -1){
+		sys->fprint(sys->fildes(2), "rioimport: cannot bind %q onto %q: %r\n", a, b);
+		raise "fail:error";
+	}
+}
+
+rioproxy(wc: chan of (ref Draw->Context, string))
+{
+	{
+		rioproxy1(wc);
+	} exception e {
+	"fail:*" =>
+		wc <-= (nil, e[5:]);
+	}
+}
+
+rioproxy1(wc: chan of (ref Draw->Context, string))
+{
+	sys->pctl(Sys->NEWFD, 0 :: 1 :: 2 :: nil);
+
+	ebind("#U*", P9PATH, Sys->MREPL);
+	display = Display.allocate(P9PATH + "/dev");
+	if(display == nil)
+		raise sys->sprint("fail:cannot allocate display: %r");
+
+
+	(wm, join, req) := wmsrv->init();
+	if(wm == nil){
+		wc <-= (nil, sys->sprint("%r"));
+		return;
+	}
+	readscreenr();
+	wc <-= (ref Draw->Context(display, nil, wm), nil);
+
+	sys->pctl(Sys->FORKNS, nil);
+	ebind("#₪", "/srv", Sys->MREPL|Sys->MCREATE);
+	if(sys->bind(P9PATH+"/dev/draw", "/dev/draw", Sys->MREPL) == -1)
+		ebind(P9PATH+"/dev", "/dev", Sys->MAFTER);
+	sh->run(nil, "mount" :: "{mntgen}" :: "/mnt" :: nil);
+
+	clients: array of ref Client;
+	nc := 0;
+	for(;;) alt{
+	(sc, rc) := <-join =>
+		if(nc != 0)
+			rc <-= "only one client available";
+		sync := chan of (ref Client, string);
+		spawn clientproc(sc,sync);
+		(c, err) := <-sync;
+		rc <-= err;
+		if(c != nil){
+			if(sc.id >= len clients)
+				clients = (array[sc.id + 1] of ref Client)[0:] = clients;
+			clients[sc.id] = c;
+		}
+	(sc, data, rc) := <-req =>
+		clients[sc.id].req <-= (data, rc);
+		if(rc == nil)
+			clients[sc.id] = nil;
+	}
+}
+zclient: Client;
+clientproc(sc: ref Wmsrv->Client, rc: chan of (ref Client, string))
+{
+	c := ref zclient;
+	c.req = chan of (array of byte, Sys->Rwrite);
+	c.resize = chan of ref Riowin;
+	c.ptr = chan of ref Draw->Pointer;
+	c.riowctl = chan of (ref Riowin, int);
+	c.sc = sc;
+	rc <-= (c, nil);
+
+loop:
+	for(;;) alt{
+	(data, drc) := <-c.req =>
+		if(drc == nil)
+			break loop;
+		err := handlerequest(c, data);
+		n := len data;
+		if(err != nil)
+			n = -1;
+		alt{
+		drc <-= (n, err) =>;
+		* =>;
+		}
+	p := <-c.ptr =>
+		sc.ptr <-= p;
+	w := <-c.resize =>
+		if((c.state & Hidden) == 0)
+			sc.ctl <-= sys->sprint("!reshape %q -1 0 0 0 0 getwin", w.tag);
+	(w, state) := <-c.riowctl =>
+		if((c.state^state)&Current)
+			sc.ctl <-= "haskbdfocus " + string ((state & Current)!=0);
+		if((c.state^state)&Hidden){
+			s := "unhide";
+			if(state&Hidden)
+				s = "hide";
+			for(wl := c.wins; wl != nil; wl = tl wl){
+				if(hd wl != w)
+					rioctl(hd wl, s);
+				if(c.state&Hidden)
+					sc.ctl <-= sys->sprint("!reshape %q -1 0 0 0 0 getwin", (hd wl).tag);
+			}
+		}
+		c.state = state;
+		w.state = state;
+	}
+	sc.stop <-= 1;
+	for(wl := c.wins; wl != nil; wl = tl wl)
+		delwin(hd wl);
+}
+
+handlerequest(c: ref Client, data: array of byte): string
+{
+	req := string data;
+#sys->print("%d: %s\n", c.sc.id, req);
+	if(req == nil)
+		return "no request";
+	args := str->unquoted(req);
+	n := len args;
+	case hd args {
+	"key" =>
+		return "permission denied";
+	"ptr" =>
+		# ptr x y
+		if(n != 3)
+			return "bad arg count";
+		if(c.ptrstarted == 0)
+			return "pointer not active";
+		for(w := c.wins; w != nil; w = tl w){
+			if((hd w).ptrfd != nil){
+				sys->fprint((hd w).ptrfd, "m%11d %11d", int hd tl args, int hd tl tl args);
+				return nil;
+			}
+		}
+		return "no windows";
+	"start" =>
+		if(n != 2)
+			return "bad arg count";
+		case hd tl args {
+		"ptr" or
+		"mouse" =>
+			if(c.ptrstarted == -1)
+				return "already started";
+			sync := chan of int;
+			for(w := c.wins; w != nil; w = tl w){
+				spawn ptrproc(hd w, c.ptr, c.resize, sync);
+				(hd w).ptrpid = <-sync;
+			}
+			c.ptrstarted = 1;
+			return nil;
+		"kbd" =>
+			if(c.kbdstarted == -1)
+				return "already started";
+			sync := chan of int;
+			for(w := c.wins; w != nil; w = tl w){
+				spawn kbdproc(hd w, c.sc.kbd, sync);
+				(hd w).kbdpid = <-sync;
+			}
+			return nil;
+		* =>
+			return "unknown input source";
+		}
+	"!reshape" =>
+		# reshape tag reqid rect [how]
+		# XXX allow "how" to specify that the origin of the window is never
+		# changed - a new window will be created instead.
+		if(n < 7)
+			return "bad arg count";
+		args = tl args;
+		tag := hd args; args = tl args;
+		args = tl args;		# skip reqid
+		r: Rect;
+		r.min.x = int hd args; args = tl args;
+		r.min.y = int hd args; args = tl args;
+		r.max.x = int hd args; args = tl args;
+		r.max.y = int hd args; args = tl args;
+		if(r.dx() < Minwinsize.x)
+			r.max.x = r.min.x + Minwinsize.x;
+		if(r.dy() < Minwinsize.y)
+			r.max.y = r.min.y + Minwinsize.y;
+
+		spec := "";
+		if(args != nil){
+			case hd args{
+			"onscreen" =>
+				r = fitrect(r, screenr).inset(-Borderwidth);
+				spec = "-r " + r2s(r);
+			"place" =>
+				r = fitrect(r, screenr).inset(-Borderwidth);
+				spec = "-dx " + string r.dx() + " -dy " + string r.dy();
+			"exact" =>
+				spec = "-r " + r2s(r.inset(-Borderwidth));
+			"max" =>
+				r = screenr;			# XXX don't obscure toolbar?
+				spec = "-r " + r2s(r.inset(Borderwidth));
+			"getwin" =>
+				;						# just get the new image
+			* =>
+				return "unkown placement method";
+			}
+		}else
+			spec = "-r " + r2s(r.inset(-Borderwidth));
+		return reshape(c, tag, spec);
+	"delete" =>
+		# delete tag
+		if(tl args == nil)
+			return "tag required";
+		tag := hd tl args;
+		nw: list of ref Riowin;
+		for(w := c.wins; w != nil; w = tl w){
+			if((hd w).tag == tag){
+				delwin(hd w);
+				wmsrv->c.sc.setimage(tag, nil);
+			}else
+				nw = hd w :: nw;
+		}
+		c.wins = nil;
+		for(; nw != nil; nw = tl nw)
+			c.wins = hd nw :: c.wins;
+	"label" =>
+		if(n != 2)
+			return "bad arg count";
+		for(w := c.wins; w != nil; w = tl w)
+			setlabel(hd w, hd tl args);
+	"raise" =>
+		for(w := c.wins; w != nil; w = tl w){
+			rioctl(hd w, "top");
+			if(tl w == nil)
+				rioctl(hd w, "current");
+		}
+	"lower" =>
+		for(w := c.wins; w != nil; w = tl w)
+			rioctl(hd w, "bottom");
+	"task" =>
+		if(n != 2)
+			return "bad arg count";
+		c.state |= Hidden;
+		for(w := c.wins; w != nil; w = tl w){
+			setlabel(hd w, hd tl args);
+			rioctl(hd w, "hide");
+		}
+	"untask" =>
+		wins: list of ref Riowin;
+		for(w := c.wins; w != nil; w = tl w)
+			wins = hd w :: wins;
+		for(; wins != nil; wins = tl wins)
+			rioctl(hd wins, "unhide");
+	"!move" =>
+		# !move tag reqid startx starty
+		if(n != 5)
+			return "bad arg count";
+		args = tl args;
+		tag := hd args; args = tl args;
+		args = tl args;
+		w := wmsrv->c.sc.window(tag);
+		if(w == nil)
+			return "no such tag";
+		return dragwin(c.ptr, c, w, Point(int hd args, int hd tl args));
+	"!size" =>
+		return "nope";
+	"kbdfocus" =>
+		if(n != 2)
+			return "bad arg count";
+		if(int hd tl args){
+			if(c.wins != nil)
+				return rioctl(hd c.wins, "current");
+		}
+		return nil;
+	* =>
+		return "unknown request";
+	}
+	return nil;
+}
+
+dragwin(ptr: chan of ref Draw->Pointer, c: ref Client, w: ref Wmsrv->Window, click: Point): string
+{
+#	if(buttons == 0)
+#		return "too late";
+	p: ref Draw->Pointer;
+	img := w.img.screen.image;
+	r := img.r;
+	off := click.sub(r.min);
+	do{
+		p = <-ptr;
+		img.origin(r.min, p.xy.sub(off));
+	} while (p.buttons != 0);
+	c.sc.ptr <-= p;
+#	buttons = 0;
+	nr: Rect;
+	nr.min = p.xy.sub(off);
+	nr.max = nr.min.add(r.size());
+	if(nr.eq(r))
+		return "not moved";
+	reshape(c, w.tag, "-r " + r2s(nr));
+	return nil;
+}
+
+rioctl(w: ref Riowin, req: string): string
+{
+	if(sys->fprint(w.ctlfd, "%s", req) == -1){
+#sys->print("rioctl fail %s: %s: %r\n", w.dir, req);
+		return sys->sprint("%r");
+}
+#sys->print("rioctl %s: %s\n", w.dir, req);
+	return nil;
+}
+
+reshape(c: ref Client, tag: string, spec: string): string
+{
+	for(wl := c.wins; wl != nil; wl = tl wl)
+		if((hd wl).tag == tag)
+			break;
+	if(wl == nil){
+		(w, e) := newwin(c, tag, spec);
+		if(w == nil){
+sys->print("can't make new win (spec %q): %s\n", spec, e);
+			return e;
+		}
+		c.wins = w :: c.wins;
+		wmsrv->c.sc.setimage(tag, w.img);
+		sync := chan of int;
+		if(c.kbdstarted){
+			spawn kbdproc(w, c.sc.kbd, sync);
+			w.kbdpid = <-sync;
+		}
+		if(c.ptrstarted){
+			spawn ptrproc(w, c.ptr, c.resize, sync);
+			w.ptrpid = <-sync;
+		}
+		return nil;
+	}
+	w := hd wl;
+	if(spec != nil){
+		e := rioctl(w, "resize " + spec);
+		if(e != nil)
+			return e;
+	}
+	getwin(w);
+	if(w.img == nil)
+		return "getwin failed";
+	wmsrv->c.sc.setimage(tag, w.img);
+	return nil;
+}
+
+zriowin: Riowin;
+newwin(c: ref Client, tag, spec: string): (ref Riowin, string)
+{
+	wsys := readfile(P9PATH + "/env/wsys");
+	if(wsys == nil)
+		return (nil, "no $wsys");
+	
+	d := "/mnt/"+string wsysseq++;
+	fd := sys->open(wsys, Sys->ORDWR);
+	if(fd == nil)
+		return (nil, sys->sprint("cannot open %q: %r\n", wsys));
+	# XXX this won't multiplex properly - srv9 should export attach files (actually that's what plan 9 should do)
+	if(sys->mount(fd, nil, d, Sys->MREPL, "new "+spec) == -1)
+		return (nil, sys->sprint("mount %q failed: %r", wsys));
+	(ok, nil) := sys->stat(d + "/winname");
+	if(ok == -1)
+		return (nil, "could not make window");
+	w := ref zriowin;
+	w.tag = tag;
+	w.dir = d;
+	getwin(w);
+	w.ctlfd = sys->open(d + "/wctl", Sys->ORDWR);
+	setlabel(w, "inferno "+string sys->pctl(0, nil)+"."+tag);
+	sync := chan of int;
+	spawn ctlproc(w, c.riowctl, sync);
+	w.ctlpid = <-sync;
+	return (w, nil);
+}
+
+setlabel(w: ref Riowin, s: string)
+{
+	fd := sys->open(w.dir + "/label", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "%s", s);
+}
+
+ctlproc(w: ref Riowin, wctl: chan of (ref Riowin, int), sync: chan of int)
+{
+	sync <-= sys->pctl(0, nil);
+	buf := array[1024] of byte;
+	for(;;){
+		n := sys->read(w.ctlfd, buf, len buf);
+		if(n <= 0)
+			break;
+		if(n > 4*12){
+			state := 0;
+			(nil, toks) := sys->tokenize(string buf[4*12:], " ");
+			if(hd toks == "current")
+				state |= Current;
+			if(hd tl toks == "hidden")
+				state |= Hidden;
+			wctl <-= (w, state);
+		}
+	}
+#sys->print("riowctl eof\n");
+}
+
+delwin(w: ref Riowin)
+{
+	sys->unmount(nil, w.dir);
+	kill(w.ptrpid, "kill");
+	kill(w.kbdpid, "kill");
+	kill(w.ctlpid, "kill");
+}
+
+getwin(w: ref Riowin): int
+{
+	s := readfile(w.dir + "/winname");
+#sys->print("getwin %s\n", s);
+	i := display.namedimage(s);
+	if(i == nil)
+		return -1;
+	scr := Screen.allocate(i, display.white, 0);
+	if(scr == nil)
+		return -1;
+	wi := scr.newwindow(i.r.inset(Borderwidth), Draw->Refnone, Draw->Nofill);
+	if(wi == nil)
+		return -1;
+	w.img = wi;
+	return 0;
+}
+
+kbdproc(w: ref Riowin, keys: chan of int, sync: chan of int)
+{
+	sys->pctl(Sys->NEWFD, nil);
+	cctl := sys->open(w.dir + "/consctl", Sys->OWRITE);
+	sys->fprint(cctl, "rawon");
+	fd := sys->open(w.dir + "/cons", Sys->OREAD);
+	if(fd == nil){
+		sync <-= -1;
+		return;
+	}
+	sync <-= sys->pctl(0, nil);
+	buf := array[12] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0){
+		s := string buf[0:n];
+		for(j := 0; j < len s; j++)
+			keys <-= int s[j];
+	}
+#sys->print("eof on kbdproc\n");
+}
+
+# fit a window rectangle to the available space.
+# try to preserve requested location if possible.
+# make sure that the window is no bigger than
+# the screen, and that its top and left-hand edges
+# will be visible at least.
+fitrect(w, r: Rect): Rect
+{
+	if(w.dx() > r.dx())
+		w.max.x = w.min.x + r.dx();
+	if(w.dy() > r.dy())
+		w.max.y = w.min.y + r.dy();
+	size := w.size();
+	if (w.max.x > r.max.x)
+		(w.min.x, w.max.x) = (r.min.x - size.x, r.max.x - size.x);
+	if (w.max.y > r.max.y)
+		(w.min.y, w.max.y) = (r.min.y - size.y, r.max.y - size.y);
+	if (w.min.x < r.min.x)
+		(w.min.x, w.max.x) = (r.min.x, r.min.x + size.x);
+	if (w.min.y < r.min.y)
+		(w.min.y, w.max.y) = (r.min.y, r.min.y + size.y);
+	return w;
+}
+
+ptrproc(w: ref Riowin, ptr: chan of ref Draw->Pointer, resize: chan of ref Riowin, sync: chan of int)
+{
+	w.ptrfd = sys->open(w.dir + "/mouse", Sys->ORDWR);
+	if(w.ptrfd == nil){
+		sync <-= -1;
+		return;
+	}
+	sync <-= sys->pctl(0, nil);
+
+	b:= array[Ptrsize] of byte;
+	while((n := sys->read(w.ptrfd, b, len b)) > 0){
+		if(n > 0 && int b[0] == 'r'){
+#sys->print("ptrproc got resize: %s\n", string b[0:n]);
+			resize <-= w;
+		}else{
+			p := bytes2ptr(b);
+			if(p != nil)
+				ptr <-= p;
+		}
+	}
+#sys->print("eof on ptrproc\n");
+}
+
+bytes2ptr(b: array of byte): ref Draw->Pointer
+{
+	if(len b < Ptrsize || int b[0] != 'm')
+		return nil;
+	x := int string b[1:13];
+	y := int string b[13:25];
+	but := int string b[25:37];
+	msec := int string b[37:49];
+	return ref Draw->Pointer (but, (x, y), msec);
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[8192] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
+
+readscreenr()
+{
+	fd := sys->open(P9PATH + "/dev/screen", Sys->OREAD);
+	if(fd == nil)
+		return ;
+	buf := array[5*12] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= len buf)
+		return;
+	screenr.min.x = int string buf[12:23];
+	screenr.min.y = int string buf[24:35];
+	screenr.max.x = int string buf[36:47];
+	screenr.max.y = int string buf[48:];
+}
+
+r2s(r: Rect): string
+{
+	return string r.min.x + " " + string r.min.y + " " +
+			string r.max.x + " " + string r.max.y;
+}
+
+kill(pid: int, note: string): int
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", note) < 0)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/appl/cmd/rm.b
@@ -1,0 +1,99 @@
+implement Rm;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+include "readdir.m";
+	readdir: Readdir;
+
+include "arg.m";
+
+Rm: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+quiet := 0;
+force := 0;
+errcount := 0;
+
+usage()
+{
+	sys->fprint(stderr, "Usage: rm [-fr] file ...\n");
+	raise "fail: usage";
+}
+allwrite := Sys->nulldir;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	allwrite.mode = 8r777 | Sys->DMDIR;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil){
+		sys->fprint(stderr, "rm: can't load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		'r' =>
+			readdir = load Readdir Readdir->PATH;
+			if(readdir == nil)
+				sys->fprint(stderr, "rm: can't load Readdir: %r\n");	# -r is regarded as optional
+		'f' =>
+			quiet = 1;
+		'F' =>
+			force = 1;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	sys->pctl(Sys->FORKNS, nil);
+	for(; args != nil; args = tl args) {
+		name := hd args;
+		if(sys->remove(name) < 0) {
+			e := sys->sprint("%r");
+			(ok, d) := sys->stat(name);
+			if(readdir != nil && ok >= 0 && (d.mode & Sys->DMDIR) != 0)
+				rmdir(name);
+			else
+				err(name, e);
+		}
+	}
+	if(errcount > 0)
+		raise "fail:errors";
+}
+
+rmdir(name: string)
+{
+	if(force)
+		sys->wstat(name, allwrite);
+	(d, n) := readdir->init(name, Readdir->NONE|Readdir->COMPACT);
+	for(i := 0; i < n; i++){
+		path := name+"/"+d[i].name;
+		if(d[i].mode & Sys->DMDIR)
+			rmdir(path);
+		else
+			remove(path);
+	}
+	remove(name);
+}
+
+remove(name: string)
+{
+	if(sys->remove(name) < 0)
+		err(name, sys->sprint("%r"));
+}
+
+err(name, e: string)
+{
+	if(!quiet) {
+		sys->fprint(stderr, "rm: %s: %s\n", name, e);
+		errcount++;
+	}
+}
--- /dev/null
+++ b/appl/cmd/runas.b
@@ -1,0 +1,60 @@
+implement Runas;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+
+sys: Sys;
+sh: Sh;
+
+Context: import sh;
+
+Runas: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(drawctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sh = load Sh Sh->PATH;
+	if (sh == nil)
+		badmodule(Sh->PATH);
+
+	if (len argv < 3)
+		usage();
+
+	argv = tl argv;
+	user := hd argv;
+	argv = tl argv;
+
+	fd := sys->open("/dev/user", Sys->OWRITE);
+	if (fd == nil)
+		error(sys->sprint("cannot open /dev/user: %r"));
+	u := array of byte user;
+	if (sys->write(fd, u, len u) != len u)
+		error(sys->sprint("cannot set user: %r"));
+	sh->run(drawctxt, argv);
+}
+
+badmodule(p: string)
+{
+	sys->fprint(stderr(), "runas: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+usage()
+{
+	sys->fprint(stderr(), "usage: runas user cmd [args...]\n");
+	raise "fail:usage";
+}
+
+error(e: string)
+{
+	sys->fprint(stderr(), "runas: %s\n", e);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/sed.b
@@ -1,0 +1,911 @@
+implement Sed;
+
+#
+# partial sed implementation borrowed from plan9 sed.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+	arg: Arg;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "regex.m";
+	regex: Regex;
+	Re: import regex;
+
+Sed : module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+
+false, true: con iota;
+bool: type int;
+
+Addr: adt {
+	pick {
+	None =>
+	Dollar =>
+	Line =>
+		line: int;
+	Regex =>
+		re: Re;
+	}
+};
+
+Sedcom: adt {
+	command: fn(c: self ref Sedcom);
+	executable: fn(c: self ref Sedcom) : int;
+
+	ad1, ad2: ref Addr;
+	negfl: bool;
+	active: int;
+
+	pick {
+	S =>
+		gfl, pfl: int;
+		re: Re;
+		b: ref Iobuf;
+		rhs: string;
+	D or CD or P or Q or EQ or G or CG or H or CH or N or CN or X or CP or L=>
+	A or C or I =>
+		text: string;
+	R =>
+		filename: string;
+	W =>
+		b: ref Iobuf;
+	Y =>
+		map: list of (int, int);
+	B or T or Lab =>
+		lab: string;
+	}
+};
+
+dflag := false;
+nflag := false;
+gflag := false;
+sflag := 0;
+
+delflag := 0;
+dolflag := 0;
+fhead := 0;
+files: list of string;
+fout: ref Iobuf;
+infile: ref Iobuf;
+jflag := 0;
+lastregex:  Re;
+linebuf: string;
+filename := "";
+lnum := 0;
+peekc := 0;
+
+holdsp := "";
+patsp := "";
+
+cmds: list of ref Sedcom;
+appendlist: list of ref Sedcom;
+bufioflush: list of ref Iobuf;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	if ((arg = load Arg Arg->PATH) == nil)
+		fatal(sys->sprint("could not load %s: %r", Arg->PATH));
+
+	if ((bufio = load Bufio Bufio->PATH) == nil)
+		fatal(sys->sprint("could not load %s: %r", Bufio->PATH));
+
+	if ((str = load String String->PATH) == nil)
+		fatal(sys->sprint("could not load %s: %r", String->PATH));
+
+	if ((regex = load Regex Regex->PATH) == nil)
+		fatal(sys->sprint("could not load %s: %r", Regex->PATH));
+
+	arg->init(args);
+
+	compfl := 0;
+	while ((c := arg->opt()) != 0)
+		case c {
+		'n' =>
+			nflag = true;
+		'g' =>
+			gflag = true;
+		'e' =>
+			if ((s := arg->arg()) == nil)
+				usage();
+			filename = "";
+			cmds = compile(bufio->sopen(s + "\n"), cmds);
+			compfl = 1;
+		'f' => if ((filename = arg->arg()) == nil)
+				usage();
+			b := bufio->open(filename, bufio->OREAD);
+			if (b == nil)
+				fatal(sys->sprint("couldn't open '%s': %r", filename));
+			cmds = compile(b, cmds);
+			compfl = 1;
+		'd' =>
+			dflag = true;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	if (compfl == 0) {
+		if (len args == 0)
+			fatal("missing pattern");
+		filename = "";
+		cmds = compile(bufio->sopen(hd args + "\n"), cmds);
+		args = tl args;
+	}
+
+	# reverse command list, we could compile addresses here if required
+	l: list of ref Sedcom;
+	for (p := cmds; p != nil; p = tl p) {
+		l = hd p :: l;
+	}
+	cmds = l;
+
+	# add files to file list (and reverse to get in right order)
+	f: list of string;
+	if (len args == 0)
+		f = "" :: f;
+	else for (; len args != 0; args = tl args)
+		f = hd args :: f;
+	for (;f != nil; f = tl f)
+		files = hd f :: files;
+
+	if ((fout = bufio->fopen(sys->fildes(1), bufio->OWRITE)) == nil)
+		fatal(sys->sprint("couldn't buffer stdout: %r"));
+	bufioflush = fout :: bufioflush;
+	lnum = 0;
+	execute(cmds);
+	exits(nil);
+}
+
+depth := 0;
+maxdepth: con 20;
+cmdend := array [maxdepth] of string;
+cmdcnt := array [maxdepth] of int;
+
+compile(b: ref Iobuf, l: list of ref Sedcom) : list of ref Sedcom
+{
+	lnum = 1;
+
+nextline:
+	for (;;) {
+		err: int;
+		(err, linebuf) = getline(b);
+		if (err < 0)
+			break;
+		
+		s := linebuf;
+
+		do {
+			rep: ref Sedcom;
+			ad1, ad2: ref Addr;
+			negfl := 0;
+
+			if (s != "")
+				s = str->drop(s, " \t;");
+
+			if (s == "" || s[0] == '#')
+				continue nextline;
+
+			# read addresses
+			(s, ad1) = address(s);
+			pick a := ad1 {
+			None =>
+				ad2 = ref Addr.None();
+			* =>
+				if (s != "" && (s[0] == ',' || s[0] == ';')) {
+					(s, ad2) = address(s[1:]);
+				}
+				else {
+					ad2 = ref Addr.None();
+				}
+			}
+
+			s = str->drop(s, " \t");
+
+			if (s != "" && str->in(s[0], "!")) {
+				negfl = true;
+				s = str->drop(s, "!");
+			}
+			s = str->drop(s, " \t");
+			if (s == "")
+				break;
+			c := s[0]; s = s[1:];
+
+			# mop up commands that got two addresses but only want one.
+			case c {
+				'a' or 'c' or 'q' or '=' or 'i' =>
+					if (tagof ad2 != tagof Addr.None)
+						fatal(sys->sprint("only one address allowed:  '%s'",
+						      linebuf));
+			}
+
+			case c {
+			* =>
+				fatal(sys->sprint("unrecognised command: '%s' (%c)",
+					linebuf, c));
+			'a' =>
+				if (s != "" && s[0] == '\\')
+					s = s[1:];
+				if (s == "" || s[0] != '\n')
+					fatal("unexpected characters in a command: " + s);
+				rep = ref Sedcom.A (ad1, ad2, negfl, 0, s[1:]);
+				s = "";
+			'c' =>
+				if (s != "" && s[0] == '\\')
+					s = s[1:];
+				if (s == "" || s[0] != '\n')
+					fatal("unexpected characters in c command: " + s);
+				rep = ref Sedcom.C (ad1, ad2, negfl, 0, s[1:]);
+				s = "";
+			'i' =>
+				if (s != "" && s[0] == '\\')
+					s = s[1:];
+				if (s == "" || s[0] != '\n')
+					fatal("unexpected characters in i command: " + s);
+				rep = ref Sedcom.I (ad1, ad2, negfl, 0, s[1:]);
+				s = "";
+			'r' =>
+				s = str->drop(s, " \t");
+				rep = ref Sedcom.R (ad1, ad2, negfl, 0, s);
+				s = "";
+			'w' =>
+				if (s != "")
+					s = str->drop(s, " \t");
+				if (s == "")
+					fatal("no filename in w command: " + linebuf);
+				bo := bufio->open(s, bufio->OWRITE);
+				if (bo == nil)
+					bo = bufio->create(s, bufio->OWRITE, 8r666);
+				if (bo == nil)
+					fatal(sys->sprint("can't create output file: '%s'", s));
+				bufioflush = bo :: bufioflush;
+				rep = ref Sedcom.W (ad1, ad2, negfl, 0, bo);
+				s = "";
+				
+			'd' =>
+				rep = ref Sedcom.D (ad1, ad2, negfl, 0);
+			'D' =>
+				rep = ref Sedcom.CD (ad1, ad2, negfl, 0);
+			'p' =>
+				rep = ref Sedcom.P (ad1, ad2, negfl, 0);
+			'P' =>
+				rep = ref Sedcom.CP (ad1, ad2, negfl, 0);
+			'q' =>
+				rep = ref Sedcom.Q (ad1, ad2, negfl, 0);
+			'=' =>
+				rep = ref Sedcom.EQ (ad1, ad2, negfl, 0);
+			'g' =>
+				rep = ref Sedcom.G (ad1, ad2, negfl, 0);
+			'G' =>
+				rep = ref Sedcom.CG (ad1, ad2, negfl, 0);
+			'h' =>
+				rep = ref Sedcom.H (ad1, ad2, negfl, 0);
+			'H' =>
+				rep = ref Sedcom.CH (ad1, ad2, negfl, 0);
+			'n' =>
+				rep = ref Sedcom.N (ad1, ad2, negfl, 0);
+			'N' =>
+				rep = ref Sedcom.CN (ad1, ad2, negfl, 0);
+			'x' =>
+				rep = ref Sedcom.X (ad1, ad2, negfl, 0);
+			'l' =>
+				rep = ref Sedcom.L (ad1, ad2, negfl, 0);
+ 			'y' =>
+				if (s == "")
+					fatal("expected args: " + linebuf);
+				seof := s[0:1];
+				s = s[1:];
+				if (s == "")
+					fatal("no lhs: " + linebuf);
+				(lhs, s2) := str->splitl(s, seof);
+				if (s2 == "")
+					fatal("no lhs terminator: " + linebuf);
+				s2 = s2[1:];
+				(rhs, s4) := str->splitl(s2, seof);
+				if (s4 == "")
+					fatal("no rhs: " + linebuf);
+				s = s4[1:];
+				if (len lhs != len rhs)
+					fatal("y command needs same length sets: " + linebuf);
+				map: list of (int, int);
+				for (i := 0; i < len lhs; i++)
+					map = (lhs[i], rhs[i]) :: map;
+				rep = ref Sedcom.Y (ad1, ad2, negfl, 0, map);
+			's' =>
+				seof := s[0:1];
+				re: Re;
+				(re, s) = recomp(s);
+				rhs: string;
+				(s, rhs) = compsub(seof + s);
+
+				gfl := gflag;
+				pfl := 0;
+
+				if (s != "" && s[0] == 'g') {
+					gfl = 1;
+					s = s[1:];
+				}
+				if (s != "" && s[0] == 'p') {
+					pfl = 1;
+					s = s[1:];
+				}
+				if (s != "" && s[0] == 'P') {
+					pfl = 2;
+					s = s[1:];
+				}
+
+				b: ref Iobuf = nil;
+				if (s != "" && s[0] == 'w') {
+					s = s[1:];
+					if (s != "")
+						s = str->drop(s, " \t");
+					if (s == "")
+						fatal("no filename in s with w: " + linebuf);
+					b = bufio->open(s, bufio->OWRITE);
+					if (b == nil)
+						b = bufio->create(s, bufio->OWRITE, 8r666);
+					if (b == nil)
+						fatal(sys->sprint("can't create output file: '%s'", s));
+					bufioflush = b :: bufioflush;
+					s = "";
+				}
+				rep = ref Sedcom.S (ad1, ad2, negfl, 0, gfl, pfl, re, b, rhs);
+			':' =>
+				if (s != "")
+					s = str->drop(s, " \t");
+				(lab, s1) := str->splitl(s, " \t;#");
+				s = s1;
+				if (lab == "")
+					fatal(sys->sprint("null label: '%s'", linebuf));
+				if (findlabel(lab))
+					fatal(sys->sprint("duplicate label: '%s'", lab));
+				rep = ref Sedcom.Lab (ad1, ad2, negfl, 0, lab);
+			'b' or 't' =>
+				if (s != "")
+					s = str->drop(s, " \t");
+				(lab, s1) := str->splitl(s, " \t;#");
+				s = s1;
+				if (c == 'b')
+					rep = ref Sedcom.B (ad1, ad2, negfl, 0, lab);
+				else
+					rep = ref Sedcom.T (ad1, ad2, negfl, 0, lab);
+			'{' =>
+				# replace { with branch to }.
+				lab := mklab(depth);
+				depth++;
+				rep = ref Sedcom.B (ad1, ad2, !negfl, 0, lab);
+				s = ";" + s;
+			'}' =>
+				if (tagof ad1 != tagof Addr.None)
+					fatal("did not expect address:" + linebuf);
+				if (--depth < 0)
+					fatal("too many }'s: " + linebuf);
+				lab := mklab(depth);
+				cmdcnt[depth]++;
+				rep = ref Sedcom.Lab ( ad1, ad2, negfl, 0, lab);
+				s = ";" + s;
+			}
+
+			l = rep :: l;
+		} while (s != nil && str->in(s[0], ";{}"));
+
+		if (s != nil)
+			fatal("leftover junk: " + s);
+	}
+	return l;
+}
+
+findlabel(lab: string) : bool
+{
+	for (l := cmds; l != nil; l = tl l)
+		pick x := hd l {
+		Lab =>
+			if (x.lab == lab)
+				return true;
+		}
+	return false;
+}
+
+mklab(depth: int): string
+{
+	return "_" + string cmdcnt[depth] + "_" + string depth;
+}
+
+Sedcom.command(c: self ref Sedcom)
+{
+	pick x := c {
+	S =>
+		m: bool;
+		(m, patsp) = substitute(x, patsp);
+		if (m) {
+			case x.pfl {
+			0 =>
+				;
+			1 =>
+				fout.puts(patsp + "\n");
+			* =>
+				l: string;
+				(l, patsp) = str->splitl(patsp, "\n");
+				fout.puts(l + "\n");
+				break;
+			}
+			if (x.b != nil)
+				x.b.puts(patsp + "\n");
+		}
+	P =>
+		fout.puts(patsp + "\n");
+	CP =>
+		(s, nil) := str->splitl(patsp, "\n");
+		fout.puts(s + "\n");
+	A =>
+		appendlist = c :: appendlist;
+	R =>
+		appendlist = c :: appendlist;
+	C =>
+		delflag++;
+		if (c.active == 1)
+			fout.puts(x.text + "\n");
+	I =>
+		fout.puts(x.text + "\n");
+	W =>
+		x.b.puts(patsp + "\n");
+	G =>
+		patsp = holdsp;
+	CG =>
+		patsp += holdsp;
+	H =>
+		holdsp = patsp;
+	CH =>
+		holdsp += patsp;
+	X =>
+		(holdsp, patsp) = (patsp, holdsp);
+	Y =>
+		# yes this is O(N²).
+		for (i := 0; i < len patsp; i++)
+			for (h := x.map; h != nil; h = tl h) {
+				(s, d) := hd h;
+				if (patsp[i] == s)
+					patsp[i] = d;
+			}
+	D =>
+		delflag++;
+	CD =>
+		# loose upto \n.
+		(s1, s2) := str->splitl(patsp, "\n");
+		if (s2 == nil)
+			patsp = s1;
+		else if (len s2 > 1)
+			patsp = s2[1:];
+		else
+			patsp = "";
+		jflag++;
+	Q =>
+		if (!nflag)
+			fout.puts(patsp + "\n");
+		arout();
+		exits(nil);
+	N =>
+		if (!nflag)
+			fout.puts(patsp + "\n");
+		arout();
+		n: int;
+		(patsp, n) = gline();
+		if (n < 0)
+			delflag++;
+	CN =>
+		arout();
+		(ns, n) := gline();
+		if (n < 0)
+			delflag++;
+		patsp += "\n" + ns;
+	EQ =>
+		fout.puts(sys->sprint("%d\n", lnum));
+	Lab =>
+		# labels don't do anything.
+	B =>
+		jflag = true;
+	T =>
+		if (sflag) {
+			sflag = false;
+			jflag = true;
+		}
+	L =>
+		col := 0;
+		cc := 0;
+		for (i := 0; i < len patsp; i++) {
+			s := "";
+			cc = patsp[i];
+			if (cc >= 16r20 && cc < 16r7F && cc != '\n')
+				s[len s] = cc;
+			else
+				s = trans(cc);
+			for (j := 0; j < len s; j++) {
+				fout.putc(s[j]);
+				if (col++ > 71) {
+					fout.puts("\\\n");
+					col = 0;
+				}
+			}
+		}
+		if (cc == ' ')
+			fout.puts("\\n");
+		fout.putc('\n');
+	* =>
+		fatal("unhandled command");
+	}
+}
+
+trans(ch: int) : string
+{
+	case ch {
+	'\b' =>
+		return "\\b";
+	'\n' =>
+		return "\\n";
+	'\r' =>
+		return "\\r";
+	'\t' =>
+		return "\\t";
+	'\\' =>
+		return "\\\\";
+	* =>
+		return sys->sprint("\\u%.4ux", ch);
+	}
+}
+
+getline(b: ref Iobuf) : (int, string)
+{
+	w : string;
+
+	lnum++;
+
+	while ((c := b.getc()) != bufio->EOF) {
+		r := c;
+		if (r == '\\') {
+			w[len w] = r;
+			if ((c = b.getc()) == bufio->EOF)
+				break;
+			r = c;
+		}
+		else if (r == '\n')
+			return (1, w);
+		w[len w] = r;
+	}
+	return (-1, w);
+}
+
+address(s: string) : (string, ref Addr)
+{
+	case s[0] {
+	'$' =>
+		return (s[1:], ref Addr.Dollar());
+	'/' =>
+		(r, s1) := recomp(s);
+		if (r == nil)
+			r = lastregex;
+		if (r == nil)
+			fatal("First RE in address may not be null");
+		return (s1, ref Addr.Regex(r));
+	'0' to '9' =>
+		(lno, ls) := str->toint(s, 10);
+		if (lno == 0)
+			fatal("line no 0 is illegal address");
+		return (ls, ref Addr.Line(lno));
+	* =>
+		return (s, ref Addr.None());
+	}
+}
+
+recomp(s :string) : (Re, string)
+{
+	expbuf := "";
+
+	seof := s[0]; s = s[1:];
+	if (s[0] == seof)
+		return (nil, s[1:]); # //
+
+	c := s[0]; s = s[1:];
+	do {
+		if (c == '\0' || c == '\n')
+			fatal("too much text: " + linebuf);
+		if (c == '\\') {
+			expbuf[len expbuf] = c;
+			c = s[0]; s = s[1:];
+			if (c == 'n')
+				c = '\n';
+		}
+		expbuf[len expbuf] = c;
+		c = s[0]; s = s[1:];
+	} while (c != seof);
+
+	(r, err) := regex->compile(expbuf, 1);
+	if (r == nil)
+		fatal(sys->sprint("%s '%s'", err, expbuf));
+
+	lastregex = r;
+
+	return (r, s);
+}
+
+compsub(s: string): (string, string)
+{
+	seof := s[0];
+	rhs := "";
+	for (i := 1; i < len s; i++) {
+		r := s[i];
+		if (r == seof)
+			break;
+		if (r == '\\') {
+			rhs[len rhs] = r;
+			if(++i >= len s)
+				break;
+			r = s[i];
+		}
+		rhs[len rhs] = r;
+	}
+	if (i >= len s)
+		fatal(sys->sprint("no closing %c in replacement text: %s", seof,  linebuf));
+	return (s[i+1:], rhs);
+}		
+
+execute(l: list of ref Sedcom)
+{
+	for (;;) {
+		n: int;
+
+		(patsp, n) = gline();
+		if (n < 0)
+			break;
+
+cmdloop:
+		for (p := l; p != nil;) {
+			c := hd p;
+			if (!c.executable()) {
+				p = tl p;
+				continue;
+			}
+
+			c.command();
+
+			if (delflag)
+				break;
+			if (jflag) {
+				jflag = 0;
+				pick x := c {
+				B or T =>
+					if (p == nil)
+						break cmdloop;
+					for (p = l; p != nil; p = tl p) {
+						pick cc := hd p {
+						Lab =>
+							if (cc.lab == x.lab)
+								continue cmdloop;
+						}
+					}
+					break cmdloop; # unmatched branch => end of script
+				* =>
+					# don't branch.
+				}
+			}
+			else
+				p = tl p;
+		}
+		if (!nflag && !delflag)
+			fout.puts(patsp + "\n");
+		arout();
+		delflag = 0;
+	}
+}
+
+Sedcom.executable(c: self ref Sedcom) : int
+{
+	if (c.active) {
+		if (c.active == 1)
+			c.active = 2;
+		pick x := c.ad2 {
+		None =>
+			c.active = 0;
+		Dollar =>
+			return !c.negfl;
+		Line =>
+			if (lnum <= x.line) {
+				if (x.line == lnum)
+					c.active = 0;
+				return !c.negfl;
+			}
+			c.active = 0;
+			return c.negfl;
+		Regex =>
+			if (match(x.re, patsp))
+				c.active = false;
+			return !c.negfl;
+		}
+	}
+	pick x := c.ad1 {
+	None =>
+		return !c.negfl;
+	Dollar =>
+		if (dolflag)
+			return !c.negfl;
+	Line =>
+		if (x.line == lnum) {
+			c.active = 1;
+			return !c.negfl;
+		}
+	Regex =>
+		if (match(x.re, patsp)) {
+			c.active = 1;
+			return !c.negfl;
+		}
+	}
+	return c.negfl;
+}
+
+arout()
+{
+	a: list of ref Sedcom;
+
+	while (appendlist != nil) {
+		a = hd appendlist :: a;
+		appendlist = tl appendlist;
+	}
+
+	for (; a != nil; a = tl a)
+		pick x := hd a {
+		A =>
+			fout.puts(x.text + "\n");
+		R =>
+			if ((b := bufio->open(x.filename, bufio->OREAD)) == nil)
+				fatal(sys->sprint("couldn't open '%s'", x.filename));
+			while ((c := b.getc()) != bufio->EOF)
+				fout.putc(c);
+			b.close();
+		* =>
+			fatal("unexpected command on appendlist");
+		}
+}
+
+match(re: Re, s: string) : bool
+{
+	return re != nil && regex->execute(re, s) != nil;
+}
+
+substitute(c: ref Sedcom.S, s: string) : (bool, string)
+{
+	if (!match(c.re, s))
+		return (false, s);
+	sflag = true;
+	start := 0;
+
+	# Beware of infinite loops: 's/$/i/g', 's/a/aa/g', 's/^/a/g'
+	do {
+		se := (start, len s);
+		if ((m := regex->executese(c.re, s, se, true, true)) == nil)
+			break;
+		(l, r) := m[0];
+		rep := "";
+		for (i := 0; i < len c.rhs; i++){
+			if (c.rhs[i] != '\\'  || i+1 == len c.rhs){
+				if (c.rhs[i] == '&')
+					rep += s[l: r];
+				else
+					rep[len rep] = c.rhs[i];
+			}else {
+				i++;
+				case c.rhs[i] {
+				'0' to '9' =>
+					n := c.rhs[i] - '0';
+					# elide if too big
+					if (n < len m) {
+						(beg, end) := m[n];
+						rep += s[beg:end];
+					}
+				'n' =>
+					rep[len rep] = '\n';
+				* =>
+					rep[len rep] = c.rhs[i];
+				}
+			}
+		}
+		s = s[0:l] + rep + s[r:];
+		start = l + len rep;
+		if(r == l)
+			start++;
+	} while (c.gfl);
+	return (true, s);
+}
+
+gline() : (string, int)
+{
+	if (infile == nil && opendatafile() < 0)
+		return (nil, -1);
+
+	sflag = false;
+	lnum++;
+
+	s := "";
+	do {
+		c := peekc;
+		if (c == 0)
+			c = infile.getc();
+		for (; c != bufio->EOF; c = infile.getc()) {
+			if (c == '\n') {
+				if ((peekc = infile.getc()) == bufio->EOF)
+					if (fhead == 0)
+						dolflag = 1;
+				return (s, 1);
+			}
+			s[len s] = c;
+		}
+		if (len s != 0) {
+			peekc = bufio->EOF;
+			if (fhead == 0)
+				dolflag = 1;
+			return (s, 1);
+		}
+		peekc = 0;
+		infile = nil;			
+	} while (opendatafile() > 0);
+	infile = nil;
+	return (nil, -1);
+}
+
+opendatafile() : int
+{
+	if (files == nil)
+		return -1;
+	if (hd files != nil) {
+		if ((infile = bufio->open(hd files, bufio->OREAD)) == nil)
+			fatal(sys->sprint("can't open '%s'", hd files));
+	}
+	else if ((infile = bufio->fopen(sys->fildes(0), bufio->OREAD)) == nil)
+		fatal("can't buffer stdin");
+
+	files = tl files;
+	return 1;	
+}
+
+dbg(s: string)
+{
+	if (dflag)
+		sys->print("dbg: %s\n", s);
+}
+
+usage()
+{
+	sys->fprint(stderr(), "usage: %s [-ngd] [-e expr] [-f file] [expr] [file...]\n",
+		arg->progname());
+	exits("usage");
+}
+
+fatal(s: string)
+{
+	f := filename;
+	if (f == nil)
+		f = "<stdin>";
+	sys->fprint(stderr(), "%s:%d %s\n", f, lnum, s);
+	exits("error");
+}
+
+exits(e: string)
+{
+	for(; bufioflush != nil; bufioflush = tl bufioflush)
+		(hd bufioflush).flush();
+	if (e != nil)
+		raise "fail:" + e;
+	exit;
+}
+
+stderr() : ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/cmd/sendmail.b
@@ -1,0 +1,252 @@
+implement Sendmail;
+
+include "sys.m";
+   	sys: Sys;
+include "draw.m";
+include "bufio.m";
+include "daytime.m";
+include "smtp.m";
+include "env.m";
+
+sprint, fprint : import sys;
+
+DEBUG : con 0;
+STRMAX : con 512;
+
+Sendmail : module
+{
+	PATH : con "/dis/sendmail.dis";
+
+	# argv is list of persons to send mail to (or nil if To: lines present in message)
+	# mail is read from standard input
+	# scans mail for headers (From: , To: , Cc: , Subject: , Re: ) where case is not sensitive
+	init: fn(ctxt : ref Draw->Context, argv : list of string);
+};
+
+init(nil : ref Draw->Context, args : list of string) {
+	from : string;
+	tos, cc : list of string = nil;
+
+  	sys = load Sys Sys->PATH;
+	smtp := load Smtp Smtp->PATH;
+  	if (smtp == nil)
+    		error(sprint("cannot load %s", Smtp->PATH), 1);
+	daytime := load Daytime Daytime->PATH;
+	if (daytime == nil)
+		error(sprint("cannot load %s", Daytime->PATH), 1);
+	msgl := readin();
+	for (ml := msgl; ml != nil; ml = tl ml) {
+		msg := hd ml;
+		lenm := len msg;
+		sol := 1;
+		for (i := 0; i < lenm; i++) {
+			if (sol) {
+				for (j := i; j < lenm; j++)
+					if (msg[j] == '\n')
+						break;
+				s := msg[i:j];
+				if (from == nil) {
+					from = match(s, "from");
+					if (from != nil)
+						from = extract(from);
+				}
+				if (tos == nil)
+					tos = lmatch(s, "to");
+				if (cc == nil)
+					cc = lmatch(s, "cc");
+				sol = 0;
+			}
+			if (msg[i] == '\n')
+				sol = 1;
+		}
+	}
+	if (tos != nil && tl args != nil)
+		error("recipients specified on To: line and as args - aborted", 1);
+	if (from == nil)
+		from = readfile("/dev/user");
+	from = adddom(from);
+	if (tos == nil)
+		tos = tl args;
+	(ok, err) := smtp->open(nil);
+  	if (ok < 0) {
+		smtp->close();
+    		error(sprint("smtp open failed: %s", err), 1);
+	}
+	dump(from, tos, cc, msgl);
+	msgl = "From " + from + "\t" + daytime->time() + "\n" :: msgl;
+	# msgl = "From: " + from + "\n" + "Date: " + daytime->time() + "\n" :: msgl;
+	(ok, err) = smtp->sendmail(from, tos, cc, msgl);
+	if (ok < 0) {
+		smtp->close();
+		error(sprint("send failed : %s", err), 0);
+	}
+	smtp->close();
+}
+
+readin() : list of string
+{
+	m : string;
+	ls : list of string;
+	nc : int;
+
+	bufio := load Bufio Bufio->PATH;
+	Iobuf : import bufio;
+	b := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+	ls = nil;
+	m = nil;
+	nc = 0;
+	while ((s := b.gets('\n')) != nil) {
+		if (nc > STRMAX) {
+			ls = m :: ls;
+			m = nil;
+			nc = 0;
+		}
+		m += s;
+		nc += len s;
+	}
+	b.close();
+	if (m != nil)
+		ls = m :: ls;
+	return rev(ls);
+}
+
+match(s: string, pat : string) : string
+{
+	ls := len s;
+	lp := len pat;
+	if (ls < lp)
+		return nil;
+	for (i := 0; i < lp; i++) {
+		c := s[i];
+		if (c >= 'A' && c <= 'Z')
+			c += 'a'-'A';
+		if (c != pat[i])
+			return nil;
+	}
+	if (i < len s && s[i] == ':')
+		i++;
+	else if (i < len s - 1 && s[i] == ' ' && s[i+1] == ':')
+		i += 2;
+	else
+		return nil;
+	while (i < len s && (s[i] == ' ' || s[i] == '\t'))
+		i++;
+	j := ls-1;
+	while (j >= 0 && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n'))
+		j--;
+	return s[i:j+1];
+}
+	
+lmatch(s : string, pat : string) : list of string
+{
+	r := match(s, pat);
+	if (r != nil) {
+		(ok, lr) := sys->tokenize(r, " ,\t");
+		return lr;
+	}
+	return nil;
+}
+	
+extract(s : string) : string
+{
+	ls := len s;
+	for(i := 0; i < ls; i++) {
+		if(s[i] == '<') {
+			for(j := i+1; j < ls; j++)
+				if(s[j] == '>')
+					break;
+			return s[i+1:j];
+		}
+	}
+	return s;
+}
+
+adddom(s : string) : string
+{
+	if (s == nil)
+		return nil;
+	for (i := 0; i < len s; i++)
+		if (s[i] == '@')
+			return s;
+	# better to get it from environment if possible
+	env := load Env Env->PATH;
+	if (env != nil && (dom := env->getenv("DOMAIN")) != nil) {
+		ldom := len dom;
+		if (dom[ldom - 1] == '\n')
+			dom = dom[0:ldom - 1];
+		return s + "@" + dom;
+	}
+	d := readfile("/usr/" + s + "/mail/domain");
+	if (d != nil) {
+		ld := len d;
+		if (d[ld - 1] == '\n')
+			d = d[0:ld - 1];
+		return s + "@" + d;
+	}
+	return s;
+}
+	
+readfile(f : string) : string
+{
+  	fd := sys->open(f, sys->OREAD);
+  	if(fd == nil)
+    		return nil;
+  	buf := array[128] of byte;
+  	n := sys->read(fd, buf, len buf);
+  	if(n < 0)
+    		return nil;
+  	return string buf[0:n];	
+}
+
+rev(l1 : list of string) : list of string
+{
+	l2 : list of string = nil;
+
+	for ( ; l1 != nil; l1 = tl l1)
+		l2 = hd l1 :: l2;
+	return l2;
+}
+
+lprint(fd : ref Sys->FD, ls : list of string)
+{
+	for ( ; ls != nil; ls = tl ls)
+		fprint(fd, "%s ", hd ls);
+	fprint(fd, "\n");
+}
+
+cfd : ref Sys->FD;
+
+opencons()
+{
+	if (cfd == nil)
+		cfd = sys->open("/dev/cons", Sys->OWRITE);
+}
+
+dump(from : string, tos : list of string, cc : list of string, msgl : list of string)
+{
+	if (DEBUG) {
+		opencons();
+		fprint(cfd, "from\n");
+		fprint(cfd, "%s\n", from);
+		fprint(cfd, "to\n");
+		lprint(cfd, tos);
+		fprint(cfd, "cc\n");
+		lprint(cfd, cc);
+		fprint(cfd, "message\n");
+		for ( ; msgl != nil; msgl = tl msgl) {
+			fprint(cfd, "%s", hd msgl);
+			fprint(cfd, "xxxx\n");
+		}
+	}
+}
+
+error(s : string, ex : int)
+{
+	if (DEBUG) {
+		opencons();
+		fprint(cfd, "sendmail: %s\n", s);
+	}
+	fprint(sys->fildes(2), "sendmail: %s\n", s);
+	if (ex)
+		exit;
+}
--- /dev/null
+++ b/appl/cmd/sh/arg.b
@@ -1,0 +1,181 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("arg: cannot load self: %r"));
+	ctxt.addbuiltin("arg", myself);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(ctxt: ref Context, nil: Sh,
+			argv: list of ref Listnode, last: int): string
+{
+	case (hd argv).word {
+	"arg" =>
+		return builtin_arg(ctxt, argv, last);
+	}
+	return nil;
+}
+
+runsbuiltin(nil: ref Sh->Context, nil: Sh,
+			nil: list of ref Listnode): list of ref Listnode
+{
+	return nil;
+}
+
+argusage(ctxt: ref Context)
+{
+	ctxt.fail("usage", "usage: arg [opts {command}]... - args");
+}
+
+builtin_arg(ctxt: ref Context, argv: list of ref Listnode, nil: int): string
+{
+	for (args := tl argv; args != nil; args = tl tl args) {
+		if ((hd args).word == "-")
+			break;
+		if ((hd args).cmd != nil && (hd args).word == nil)
+			argusage(ctxt);
+		if (tl args == nil)
+			argusage(ctxt);
+		if ((hd tl args).cmd == nil)
+			argusage(ctxt);
+	}
+	if (args == nil)
+		args = ctxt.get("*");
+	else
+		args = tl args;
+	laststatus := "";
+	ctxt.push();
+	{
+		arg := Arg.init(args);
+		while ((opt := arg.opt()) != 0) {
+			for (argt := tl argv; argt != nil && (hd argt).word != "-"; argt = tl tl argt) {
+				w := (hd argt).word;
+				argcount := 0;
+				for (e := len w - 1; e >= 0; e--) {
+					if (w[e] != '+')
+						break;
+					argcount++;
+				}
+				w = w[0:e+1];
+				if (w == nil)
+					continue;
+				for (i := 0; i < len w; i++)
+					if (w[i] == opt || w[i] == '*')
+						break;
+				if (i < len w) {
+					optstr := ""; optstr[0] = opt;
+					ctxt.setlocal("opt", ref Listnode(nil, optstr) :: nil);
+					args = arg.arg(argcount);
+					if (argcount > 0 && args == nil)
+						ctxt.fail("usage", sys->sprint("option -%c requires %d arguments", opt, argcount));
+					ctxt.setlocal("arg", args);
+					laststatus = ctxt.run(hd tl argt :: nil, 0);
+					break;
+				}
+			}
+			if (argt == nil || (hd argt).word == "-")
+				ctxt.fail("usage", sys->sprint("unknown option -%c", opt));
+		}
+		ctxt.pop();
+		ctxt.set("args", arg.args);		# XXX backward compatibility - should go
+		ctxt.set("*", arg.args);
+		return laststatus;
+	}
+	exception e{
+	"fail:*" =>
+		ctxt.pop();
+		if (e[5:] == "break")
+			return laststatus;
+		raise e;
+	}
+}
+
+Arg: adt {
+	args: list of ref Listnode;
+	curropt: string;
+	init: fn(argv: list of ref Listnode): ref Arg;
+	arg: fn(ctxt: self ref Arg, n: int): list of ref Listnode;
+	opt: fn(ctxt: self ref Arg): int;
+};
+	
+
+Arg.init(argv: list of ref Listnode): ref Arg
+{
+	return ref Arg(argv, nil);
+}
+
+# get next n option arguments (nil list if not enough arguments found)
+Arg.arg(ctxt: self ref Arg, n: int): list of ref Listnode
+{
+	if (n == 0)
+		return nil;
+
+	args: list of ref Listnode;
+	while (--n >= 0) {
+		if (ctxt.curropt != nil) {
+			args = ref Listnode(nil, ctxt.curropt) :: args;
+			ctxt.curropt = nil;
+		} else if (ctxt.args == nil)
+			return nil;
+		else {
+			args = hd ctxt.args :: args;
+			ctxt.args = tl ctxt.args;
+		}
+	}
+	r: list of ref Listnode;
+	for (; args != nil; args = tl args)
+		r = hd args :: r;
+	return r;
+}
+
+# get next option letter
+# return 0 at end of options
+Arg.opt(ctxt: self ref Arg): int
+{
+	if (ctxt.curropt != "") {
+		opt := ctxt.curropt[0];
+		ctxt.curropt = ctxt.curropt[1:];
+		return opt;
+	}
+
+	if (ctxt.args == nil)
+		return 0;
+
+	nextarg := (hd ctxt.args).word;
+	if (len nextarg < 2 || nextarg[0] != '-')
+		return 0;
+
+	if (nextarg == "--") {
+		ctxt.args = tl ctxt.args;
+		return 0;
+	}
+
+	opt := nextarg[1];
+	if (len nextarg > 2)
+		ctxt.curropt = nextarg[2:];
+	ctxt.args = tl ctxt.args;
+	return opt;
+}
--- /dev/null
+++ b/appl/cmd/sh/csv.b
@@ -1,0 +1,244 @@
+implement Shellbuiltin;
+
+# parse/generate comma-separated values.
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("csv: cannot load self: %r"));
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		ctxt.fail("bad module",
+			sys->sprint("csv: cannot load: %s: %r", Bufio->PATH));
+	ctxt.addbuiltin("getcsv", myself);
+	ctxt.addsbuiltin("csv", myself);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(c: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode, last: int): string
+{
+	return builtin_getcsv(c, cmd, last);
+}
+
+runsbuiltin(c: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode): list of ref Listnode
+{
+	return sbuiltin_csv(c, cmd);
+}
+
+builtin_getcsv(ctxt: ref Context, argv: list of ref Listnode, nil: int) : string
+{
+	n := len argv;
+	if (n != 2 || !iscmd(hd tl argv))
+		builtinusage(ctxt, "getcsv {cmd}");
+	cmd := hd tl argv :: ctxt.get("*");
+	stdin := bufio->fopen(sys->fildes(0), Sys->OREAD);
+	if (stdin == nil)
+		ctxt.fail("bad input", sys->sprint("getcsv: cannot open stdin: %r"));
+	status := "";
+	ctxt.push();
+	for(;;){
+		{
+			for (;;) {
+				line: list of ref Listnode = nil;
+				sl := readcsvline(stdin);
+				if (sl == nil)
+					break;
+				for (; sl != nil; sl = tl sl)
+					line = ref Listnode(nil, hd sl) :: line;
+				ctxt.setlocal("line", line);
+				status = setstatus(ctxt, ctxt.run(cmd, 0));
+			}
+			ctxt.pop();
+			return status;
+		}
+		exception e{
+			"fail:*" =>
+				ctxt.pop();
+				if (loopexcept(e) == BREAK)
+					return status;
+				ctxt.push();
+		}
+	}
+}
+
+CONTINUE, BREAK: con iota;
+loopexcept(ename: string): int
+{
+	case ename[5:] {
+	"break" =>
+		return BREAK;
+	"continue" =>
+		return CONTINUE;
+	* =>
+		raise ename;
+	}
+	return 0;
+}
+
+iscmd(n: ref Listnode): int
+{
+	return n.cmd != nil || (n.word != nil && n.word[0] == '{');
+}
+	
+builtinusage(ctxt: ref Context, s: string)
+{
+	ctxt.fail("usage", "usage: " + s);
+}
+
+setstatus(ctxt: ref Context, val: string): string
+{
+	ctxt.setlocal("status", ref Listnode(nil, val) :: nil);
+	return val;
+}
+
+# in csv format, is it possible to distinguish between a line containing
+# one empty field and a line containing no fields at all?
+# what does each one look like?
+readcsvline(iob: ref Iobuf): list of string
+{
+	sl: list of string;
+
+	for(;;) {
+		(s, eof) := readcsvword(iob);
+		if (sl == nil && s == nil && eof)
+			return nil;
+
+		c := Bufio->EOF;
+		if (!eof)
+			c = iob.getc();
+		sl = s :: sl;
+		if (c == '\n' || c == Bufio->EOF)
+			return sl;
+	}
+}
+
+sbuiltin_csv(nil: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	val = tl val;
+	if (val == nil)
+		return nil;
+	s := s2qv(word(hd val));
+	for (val = tl val; val != nil; val = tl val)
+		s += "," + s2qv(word(hd val));
+	return ref Listnode(nil, s) :: nil;
+}
+
+s2qv(s: string): string
+{
+	needquote := 0;
+	needscan := 0;
+	for (i := 0; i < len s; i++) {
+		c := s[i];
+		if (c == '\n' || c == ',')
+			needquote = 1;
+		else if (c == '"') {
+			needquote = 1;
+			needscan = 1;
+		}
+	}
+	if (!needquote)
+		return s;
+	if (!needscan)
+		return "\"" + s + "\"";
+	r := "\"";
+	for (i = 0; i < len s; i++) {
+		c := s[i];
+		if (c == '"')
+			r[len r] = c;
+		r[len r] = c;
+	}
+	r[len r] = '"';
+	return r;
+}
+
+readcsvword(iob: ref Iobuf): (string, int)
+{
+	s := "";
+	case c := iob.getc() {
+	'"' =>
+		for (;;) {
+			case c = iob.getc() {
+			Bufio->EOF =>
+				return (s, 1);
+			'"' =>
+				case c = iob.getc() {
+				'"' =>
+					s[len s] = '"';
+				'\n' or
+				',' =>
+					iob.ungetc();
+					return (s, 0);
+				Bufio->EOF =>
+					return (s, 1);
+				* =>
+					# illegal
+					iob.ungetc();
+					(t, eof) := readcsvword(iob);
+					return (s + t, eof);
+				}
+			* =>
+				s[len s] = c;
+			}
+		}
+	',' or
+	'\n' =>
+		iob.ungetc();
+		return (s, 0);
+	Bufio->EOF =>
+		return (nil, 1);
+	* =>
+		s[len s] = c;
+		for (;;) {
+			case c = iob.getc() {
+			',' or
+			'\n' =>
+				iob.ungetc();
+				return (s, 0);
+			'"' =>
+				# illegal
+				iob.ungetc();
+				(t, eof) := readcsvword(iob);
+				return (s + t, eof);
+			Bufio->EOF =>
+				return (s, 1);
+			* =>
+				s[len s] = c;
+			}
+		}
+	}
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
--- /dev/null
+++ b/appl/cmd/sh/doc/History
@@ -1,0 +1,14 @@
+14/11/96	started
+12/12/96	first mostly working version
+13/12/96	fixed bug in builtin_if
+14/12/96	prompt fixed, dup fixed.
+17/1/97		fiddled with shell script perm checking
+16/2/97		converted to yacc grammar
+18/2/97		got pipes and backquotes working, with only minor hacks...
+2/4/00		revamped:
+			single process, single main module; added load builtin; added ${} operator;
+			added eval and std modules
+17/4/00		added '=' and ':=' operators; removed builtin 'set' and 'local'.
+11/6/00		added tuple assignment
+2/3/01		added n-char lookahead in lexer; ':' no longer so special
+15/2/01		store environment variables in standard quoted format.
--- /dev/null
+++ b/appl/cmd/sh/echo.b
@@ -1,0 +1,96 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("echo: cannot load self: %r"));
+	ctxt.addbuiltin("echo", myself);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(ctxt: ref Context, nil: Sh,
+			argv: list of ref Listnode, last: int): string
+{
+	case (hd argv).word {
+	"echo" =>
+		return builtin_echo(ctxt, argv, last);
+	}
+	return nil;
+}
+
+runsbuiltin(nil: ref Sh->Context, nil: Sh,
+			nil: list of ref Listnode): list of ref Listnode
+{
+	return nil;
+}
+
+argusage(ctxt: ref Context)
+{
+	ctxt.fail("usage", "usage: arg [opts {command}]... - args");
+}
+
+# converted from /appl/cmd/echo.b.
+# should have exactly the same semantics.
+builtin_echo(nil: ref Context, argv: list of ref Listnode, nil: int): string
+{
+	argv = tl argv;
+	nonewline := 0;
+	if (len argv > 0) {
+		w := (hd argv).word;
+		if (w == "-n" || w == "--") {
+			nonewline = (w == "-n");
+			argv = tl argv;
+		}
+	}
+	s := "";
+	if (argv != nil) {
+		s = word(hd argv);
+		for (argv = tl argv; argv != nil; argv = tl argv)
+			s += " " + word(hd argv);
+	}
+	if (nonewline == 0) 
+		s[len s] = '\n';
+	{
+		a := array of byte s;
+		if (sys->write(sys->fildes(1), a, len a) != len a) {
+			sys->fprint(sys->fildes(2), "echo: write error: %r\n");
+			return "write error";
+		}
+		return nil;
+	}
+	exception{
+		"write on closed pipe" =>
+			sys->fprint(sys->fildes(2), "echo: write error: write on closed pipe\n");
+			return "write error";
+	}
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
--- /dev/null
+++ b/appl/cmd/sh/expr.b
@@ -1,0 +1,281 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("expr: cannot load self: %r"));
+
+	ctxt.addsbuiltin("expr", myself);
+	ctxt.addbuiltin("ntest", myself);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+EQ, GT, LT, GE, LE, PLUS, MINUS, DIVIDE, AND, TIMES, MOD,
+OR, XOR, UMINUS, SHL, SHR, NOT, BNOT, NEQ, REP, SEQ: con iota;
+
+runbuiltin(ctxt: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode, nil: int): string
+{
+	case (hd cmd).word {
+	"ntest" =>
+		if (len cmd != 2)
+			ctxt.fail("usage", "usage: ntest n");
+		if (big (hd tl cmd).word == big 0)
+			return "false";
+	}
+	return nil;
+}
+
+runsbuiltin(ctxt: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode): list of ref Listnode
+{
+	# only one sbuiltin: expr.
+	stk: list of big;
+	lastop := -1;
+	lastn := -1;
+	lastname := "";
+	radix: int;
+	(cmd, radix) = opts(ctxt, tl cmd);
+	for (; cmd != nil; cmd = tl cmd) {
+		w := (hd cmd).word;
+		op := -1;
+		nops := 2;
+		case w {
+		"+" =>
+			op = PLUS; 
+		"-" =>
+			op = MINUS;
+		"x" or "*" or "×" =>
+			op = TIMES;
+		"/" =>
+			op = DIVIDE;
+		"%" =>
+			op = MOD;
+		"and" =>
+			op = AND;
+		"or" =>
+			op = OR;
+		"xor" =>
+			op = XOR;
+		"_"=>
+			(op, nops) = (UMINUS, 1);
+		"<<" or "shl" =>
+			op = SHL;
+		">>" or "shr" =>
+			op = SHR;
+		"=" or "==" or "eq" =>
+			op = EQ;
+		"!=" or "neq" =>
+			op = NEQ;
+		">" or "gt" =>
+			op = GT;
+		"<" or "lt" =>
+			op = LT;
+		">=" or "ge" =>
+			op = GE;
+		"<=" or "le" =>
+			op = LE;
+		"!" or "not" =>
+			(op, nops) = (NOT, 1);
+		"~" =>
+			(op, nops) = (BNOT, 1);
+		"rep" =>
+			(op, nops) = (REP, 0);
+		"seq" =>
+			(op, nops) = (SEQ, 2);
+		}
+		if (op == -1)
+			stk = makenum(ctxt, w) :: stk;
+		else 
+			stk = operator(ctxt, stk, op, nops, lastop, lastn, w, lastname);
+		lastop = op;
+		lastn = nops;
+		lastname = w;
+	}
+	r: list of ref Listnode;
+	for (; stk != nil; stk = tl stk)
+		r = ref Listnode(nil, big2string(hd stk, radix)) :: r;
+	return r;
+}
+
+opts(ctxt: ref Context, cmd: list of ref Listnode): (list of ref Listnode, int)
+{
+	radix := 10;
+	if (cmd == nil)
+		return (nil, 10);
+	w := (hd cmd).word;
+	if (len w < 2)
+		return (cmd, 10);
+	if (w[0] != '-' || (w[1] >= '0' && w[1] <= '9'))
+		return (cmd, 10);
+	if (w[1] != 'r')
+		ctxt.fail("usage", "usage: expr [-r radix] [arg...]");
+	if (len w > 2)
+		w = w[2:];
+	else {
+		if (tl cmd == nil)
+			ctxt.fail("usage", "usage: expr [-r radix] [arg...]");
+		cmd = tl cmd;
+		w = (hd cmd).word;
+	}
+	r := int w;
+	if (r <= 0 || r > 36)
+		ctxt.fail("usage", "expr: invalid radix " + string r);
+	return (tl cmd, int w);
+}
+
+operator(ctxt: ref Context, stk: list of big, op, nops, lastop, lastn: int,
+		opname, lastopname: string): list of big
+{
+	al: list of big;
+	for (i := 0; i < nops; i++) {
+		if (stk == nil)
+			ctxt.fail("empty stack",
+				sys->sprint("expr: empty stack on op '%s'", opname));
+		al = hd stk :: al;
+		stk = tl stk;
+	}
+	return oper(ctxt, al, op, lastop, lastn, lastopname, stk);
+}
+
+# args are in reverse order
+oper(ctxt: ref Context, args: list of big, op, lastop, lastn: int,
+		lastopname: string, stk: list of big): list of big
+{
+	if (op == REP) {
+		if (lastop == -1 || lastop == SEQ || lastn != 2)
+			ctxt.fail("usage", "expr: bad operator for rep");
+		if (stk == nil || tl stk == nil)
+			return stk;
+		while (tl stk != nil)
+			stk = operator(ctxt, stk, lastop, 2, -1, -1, lastopname, nil);
+		return stk;
+	}
+	n2 := big 0;
+	n1 := hd args;
+	if (tl args != nil)
+		n2 = hd tl args;
+	r := big 0;
+	case op {
+	EQ =>	r = big(n1 == n2);
+	NEQ =>	r = big(n1 != n2);
+	GT =>	r = big(n1 > n2);
+	LT =>	r = big(n1 < n2);
+	GE =>	r = big(n1 >= n2);
+	LE =>	r = big(n1 <= n2);
+	PLUS =>	r = big(n1 + n2);
+	MINUS =>	r = big(n1 - n2);
+	NOT	 =>	r = big(n1 != big 0);
+	DIVIDE =>
+			if (n2 == big 0)
+				ctxt.fail("divide by zero", "expr: division by zero");
+			r = n1 / n2;
+	MOD =>
+			if (n2 == big 0)
+				ctxt.fail("divide by zero", "expr: division by zero");
+			r = n1 % n2;
+	TIMES =>	r = n1 * n2;
+	AND =>	r = n1 & n2;
+	OR =>	r = n1 | n2;
+	XOR =>	r = n1 ^ n2;
+	UMINUS => r = -n1;
+	BNOT =>	r = ~n1;
+	SHL =>	r = n1 << int n2;
+	SHR =>	r = n1 >> int n2;
+	SEQ =>	return seq(n1, n2, stk);
+	}
+	return r :: stk;
+}
+
+seq(n1, n2: big, stk: list of big): list of big
+{
+	incr := big 1;
+	if (n2 < n1)
+		incr = big -1;
+	for (; n1 != n2; n1 += incr)
+		stk = n1 :: stk;
+	return n1 :: stk;
+}
+
+makenum(ctxt: ref Context, s: string): big
+{
+	if (s == nil || (s[0] != '-' && (s[0] < '0' || s[0] > '9')))
+		ctxt.fail("usage", sys->sprint("expr: unknown operator '%s'", s));
+
+	t := s;	
+	if (neg := s[0] == '-')
+		s = s[1:];
+	radix := 10;
+	for (i := 0; i < len s && i < 3; i++) {
+		if (s[i] == 'r') {
+			radix = int s;
+			s = s[i+1:];
+			break;
+		}
+	}
+	if (radix == 10)
+		return big t;
+	if (radix == 0 || radix > 36)
+		ctxt.fail("usage", "expr: bad number " + t);
+	n := big 0;
+	for (i = 0; i < len s; i++) {
+		if ('0' <= s[i] && s[i] <= '9')
+			n = (n * big radix) + big(s[i] - '0');
+		else if ('a' <= s[i] && s[i] < 'a' + radix - 10)
+			n = (n * big radix) + big(s[i] - 'a' + 10);
+		else if ('A' <= s[i] && s[i]  < 'A' + radix - 10)
+			n = (n * big radix) + big(s[i] - 'A' + 10);
+		else
+			break;
+	}
+	if (neg)
+		return -n;
+	return n;
+}
+
+big2string(n: big, radix: int): string
+{
+	if (neg := n < big 0) {
+		n = -n;
+	}
+	s := "";
+	do {
+		c: int;
+		d := int (n % big radix);
+		if (d < 10)
+			c = '0' + d;
+		else
+			c = 'a' + d - 10;
+		s[len s] = c;
+		n /= big radix;
+	} while (n > big 0);
+	t := s;
+	for (i := len s - 1; i >= 0; i--)
+		t[len s - 1 - i] = s[i];
+	if (radix != 10)
+		t = string radix + "r" + t;
+	if (neg)
+		return "-" + t;
+	return t;
+}
--- /dev/null
+++ b/appl/cmd/sh/file2chan.b
@@ -1,0 +1,459 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "lock.m";
+	lock: Lock;
+	Semaphore: import lock;
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+
+Tag: adt {
+	tagid, blocked: int;
+	offset, fid: int;
+	pick {
+	Read =>
+		count: int;
+		rc: chan of (array of byte, string);
+	Write =>
+		data: array of byte;
+		wc: chan of (int, string);
+	}
+};
+
+taglock: ref Lock->Semaphore;
+maxtagid := 1;
+tags := array[16] of list of ref Tag;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("file2chan: cannot load self: %r"));
+
+	lock = load Lock Lock->PATH;
+	if (lock == nil) ctxt.fail("bad module", sys->sprint("file2chan: cannot load %s: %r", Lock->PATH));
+	lock->init();
+
+	taglock = Semaphore.new();
+	if (taglock == nil)
+		ctxt.fail("no lock", "file2chan: cannot make lock");
+
+
+	ctxt.addbuiltin("file2chan", myself);
+	ctxt.addbuiltin("rblock", myself);
+	ctxt.addbuiltin("rread", myself);
+	ctxt.addbuiltin("rreadone", myself);
+	ctxt.addbuiltin("rwrite", myself);
+	ctxt.addbuiltin("rerror", myself);
+	ctxt.addbuiltin("fetchwdata", myself);
+	ctxt.addbuiltin("putrdata", myself);
+	ctxt.addsbuiltin("rget", myself);
+
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(ctxt: ref Context, nil: Sh,
+			cmd: list of ref Listnode, nil: int): string
+{
+	case (hd cmd).word {
+	"file2chan" =>		return builtin_file2chan(ctxt, cmd);
+	"rblock" =>		return builtin_rblock(ctxt, cmd);
+	"rread" =>			return builtin_rread(ctxt, cmd, 0);
+	"rreadone" =>		return builtin_rread(ctxt, cmd, 1);
+	"rwrite" =>		return builtin_rwrite(ctxt, cmd);
+	"rerror" =>		return builtin_rerror(ctxt, cmd);
+	"fetchwdata" =>	return builtin_fetchwdata(ctxt, cmd);
+	"putrdata" =>		return builtin_putrdata(ctxt, cmd);
+	}
+	return nil;
+}
+
+runsbuiltin(ctxt: ref Context, nil: Sh,
+			argv: list of ref Listnode): list of ref Listnode
+{
+	# could add ${rtags} to retrieve list of currently outstanding tags
+	case (hd argv).word {
+	"rget" =>			return sbuiltin_rget(ctxt, argv);
+	}
+	return nil;
+}
+
+builtin_file2chan(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	rcmd, wcmd, ccmd: ref Listnode;
+	path: string;
+
+	n := len argv;
+	if (n < 4 || n > 5)
+		ctxt.fail("usage", "usage: file2chan file {readcmd} {writecmd} [ {closecmd} ]");
+
+	(path, argv) = ((hd tl argv).word, tl tl argv);
+	(rcmd, argv) = (hd argv, tl argv);
+	(wcmd, argv) = (hd argv, tl argv);
+	if (argv != nil)
+		ccmd = hd argv;
+	if (path == nil || !iscmd(rcmd) || !iscmd(wcmd) || (ccmd != nil && !iscmd(ccmd)))
+		ctxt.fail("usage", "usage: file2chan file {readcmd} {writecmd} [ {closecmd} ]");
+
+	(dir, f) := pathsplit(path);
+	if (sys->bind("#s", dir, Sys->MBEFORE|Sys->MCREATE) == -1) {
+		reporterror(ctxt, sys->sprint("file2chan: cannot bind #s: %r"));
+		return "no #s";
+	}
+	fio := sys->file2chan(dir, f);
+	if (fio == nil) {
+		reporterror(ctxt, sys->sprint("file2chan: cannot make %s: %r", path));
+		return "cannot make chan";
+	}
+	sync := chan of int;
+	spawn srv(sync, ctxt, fio, rcmd, wcmd, ccmd);
+	apid := <-sync;
+	ctxt.set("apid", ref Listnode(nil, string apid) :: nil);
+	if (ctxt.options() & ctxt.INTERACTIVE)
+		sys->fprint(sys->fildes(2), "%d\n", apid);
+	return nil;
+}
+
+srv(sync: chan of int, ctxt: ref Context,
+		fio: ref Sys->FileIO, rcmd, wcmd, ccmd: ref Listnode)
+{
+	ctxt = ctxt.copy(1);
+	sync <-= sys->pctl(0, nil);
+	for (;;) {
+		fid, offset, count: int;
+		rc: Sys->Rread;
+		wc: Sys->Rwrite;
+		d: array of byte;
+		t: ref Tag = nil;
+		cmd: ref Listnode = nil;
+		alt {
+		(offset, count, fid, rc) = <-fio.read =>
+			if (rc != nil) {
+				t = ref Tag.Read(0, 0, offset, fid, count, rc);
+				cmd = rcmd;
+			} else
+				continue;		# we get a close on both read and write...
+		(offset, d, fid, wc) = <-fio.write =>
+			if (wc != nil) {
+				t = ref Tag.Write(0, 0, offset, fid, d, wc);
+				cmd = wcmd;
+			}
+		}
+		if (t != nil) {
+			addtag(t);
+			ctxt.setlocal("tag", ref Listnode(nil, string t.tagid) :: nil);
+			ctxt.run(cmd :: nil, 0);
+			taglock.obtain();
+			# make a default reply if it hasn't been deliberately blocked.
+			del := 0;
+			if (t.tagid >= 0 && !t.blocked) {
+				pick mt := t {
+				Read =>
+					rreply(mt.rc, nil, "invalid read");
+				Write =>
+					wreply(mt.wc, len mt.data, nil);
+				}
+				del = 1;
+			}
+			taglock.release();
+			if (del)
+				deltag(t.tagid);
+			ctxt.setlocal("tag", nil);
+		} else if (ccmd != nil) {
+			t = ref Tag.Read(0, 0, -1, fid, -1, nil);
+			addtag(t);
+			ctxt.setlocal("tag", ref Listnode(nil, string t.tagid) :: nil);
+			ctxt.run(ccmd :: nil, 0);
+			deltag(t.tagid);
+			ctxt.setlocal("tag", nil);
+		}
+	}
+}
+
+builtin_rread(ctxt: ref Context, argv: list of ref Listnode, one: int): string
+{
+	n := len argv;
+	if (n < 2 || n > 3)
+		ctxt.fail("usage", "usage: "+(hd argv).word+" [tag] data");
+	argv = tl argv;
+
+	t := envgettag(ctxt, argv, n == 3);
+	if (t == nil)
+		ctxt.fail("bad tag", "rread: cannot find tag");
+	if (n == 3)
+		argv = tl argv;
+	mt := etr(ctxt, "rread", t);
+	arg := word(hd argv);
+	d := array of byte arg;
+	if (one) {
+		if (mt.offset >= len d)
+			d = nil;
+		else
+			d = d[mt.offset:];
+	}
+	if (len d > mt.count)
+		d = d[0:mt.count];
+	rreply(mt.rc, d, nil);
+	deltag(t.tagid);
+	return nil;
+}
+
+builtin_rwrite(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	n := len argv;
+	if (n > 3)
+		ctxt.fail("usage", "usage: rwrite [tag [count]]");
+	t := envgettag(ctxt, tl argv, n > 1);
+	if (t == nil)
+		ctxt.fail("bad tag", "rwrite: cannot find tag");
+
+	mt := etw(ctxt, "rwrite", t);
+	count := len mt.data;
+	if (n == 3) {
+		arg := word(hd tl argv);
+		if (!isnum(arg))
+			ctxt.fail("usage", "usage: freply [tag [count]]");
+		count = int arg;
+	}
+	wreply(mt.wc, count, nil);
+	deltag(t.tagid);
+	return nil;
+}
+
+builtin_rblock(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if (len argv > 1)
+		ctxt.fail("usage", "usage: rblock [tag]");
+	t := envgettag(ctxt, argv, argv != nil);
+	if (t == nil)
+		ctxt.fail("bad tag", "rblock: cannot find tag");
+	t.blocked = 1;
+	return nil;
+}
+
+sbuiltin_rget(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	n := len argv;
+	if (n < 2 || n > 3)
+		ctxt.fail("usage", "usage: rget (data|count|offset|fid) [tag]");
+	argv = tl argv;
+	t := envgettag(ctxt, tl argv, tl argv != nil);
+	if (t == nil)
+		ctxt.fail("bad tag", "rget: cannot find tag");
+	s := "";
+	case (hd argv).word {
+	"data" =>
+		s = string etw(ctxt, "rget", t).data;
+	"count" =>
+		s = string etr(ctxt, "rget", t).count;
+	"offset" =>
+		s = string t.offset;
+	"fid" =>
+		s = string t.fid;
+	* =>
+		ctxt.fail("usage", "usage: rget (data|count|offset|fid) [tag]");
+	}
+
+	return ref Listnode(nil, s) :: nil;
+}
+
+builtin_fetchwdata(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if (len argv > 1)
+		ctxt.fail("usage", "usage: fetchwdata [tag]");
+	t := envgettag(ctxt, argv, argv != nil);
+	if (t == nil)
+		ctxt.fail("bad tag", "fetchwdata: cannot find tag");
+	d := etw(ctxt, "fetchwdata", t).data;
+	sys->write(sys->fildes(1), d, len d);
+	return nil;
+}
+
+builtin_putrdata(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	argv = tl argv;
+	if (len argv > 1)
+		ctxt.fail("usage", "usage: putrdata [tag]");
+	t := envgettag(ctxt, argv, argv != nil);
+	if (t == nil)
+		ctxt.fail("bad tag", "putrdata: cannot find tag");
+	mt := etr(ctxt, "putrdata", t);
+	buf := array[mt.count] of byte;
+	n := 0;
+	fd := sys->fildes(0);
+	while (n < mt.count) {
+		nr := sys->read(fd, buf[n:mt.count], mt.count - n);
+		if (nr <= 0)
+			break;
+		n += nr;
+	}
+
+	rreply(mt.rc, buf[0:n], nil);
+	deltag(t.tagid);
+	return nil;
+}
+
+builtin_rerror(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	# usage: ferror [tag] error
+	n := len argv;
+	if (n < 2 || n > 3)
+		ctxt.fail("usage", "usage: ferror [tag] error");
+	t := envgettag(ctxt, tl argv, n == 3);
+	if (t == nil)
+		ctxt.fail("bad tag", "rerror: cannot find tag");
+	if (n == 3)
+		argv = tl argv;
+	err := word(hd tl argv);
+	pick mt := t {
+	Read =>
+		rreply(mt.rc, nil, err);
+	Write =>
+		wreply(mt.wc, 0, err);
+	}
+	deltag(t.tagid);
+	return nil;
+}
+
+envgettag(ctxt: ref Context, args: list of ref Listnode, useargs: int): ref Tag
+{
+	tagid: int;
+	if (useargs)
+		tagid = int (hd args).word;
+	else {
+		args = ctxt.get("tag");
+		if (args == nil || tl args != nil)
+			return nil;
+		tagid = int (hd args).word;
+	}
+	return gettag(tagid);
+}
+
+etw(ctxt: ref Context, cmd: string, t: ref Tag): ref Tag.Write
+{
+	pick mt := t {
+	Write =>	return mt;
+	}
+	ctxt.fail("bad tag", cmd + ": inappropriate tag id");
+	return nil;
+}
+
+etr(ctxt: ref Context, cmd: string, t: ref Tag): ref Tag.Read
+{
+	pick mt := t {
+	Read =>	return mt;
+	}
+	ctxt.fail("bad tag", cmd + ": inappropriate tag id");
+	return nil;
+}
+
+wreply(wc: chan of (int, string), count: int, err: string)
+{
+	alt {
+	wc <-= (count, err) => ;
+	* => ;
+	}
+}
+
+rreply(rc: chan of (array of byte, string), d: array of byte, err: string)
+{
+	alt {
+	rc <-= (d, err) => ;
+	* => ;
+	}
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
+
+isnum(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] > '9' || s[i] < '0')
+			return 0;
+	return 1;
+}
+
+iscmd(n: ref Listnode): int
+{
+	return n.cmd != nil || (n.word != nil && n.word[0] == '}');
+}
+
+addtag(t: ref Tag)
+{
+	taglock.obtain();
+	t.tagid = maxtagid++;
+	slot := t.tagid % len tags;
+	tags[slot] = t :: tags[slot];
+	taglock.release();
+}
+
+deltag(tagid: int)
+{
+	taglock.obtain();
+	slot := tagid % len tags;
+	nwl: list of ref Tag;
+	for (wl := tags[slot]; wl != nil; wl = tl wl)
+		if ((hd wl).tagid != tagid)
+			nwl = hd wl :: nwl;
+		else
+			(hd wl).tagid = -1;
+	tags[slot] = nwl;
+	taglock.release();
+}
+
+gettag(tagid: int): ref Tag
+{
+	slot := tagid % len tags;
+	for (wl := tags[slot]; wl != nil; wl = tl wl)
+		if ((hd wl).tagid == tagid)
+			return hd wl;
+	return nil;
+}
+
+pathsplit(p: string): (string, string)
+{
+	for (i := len p - 1; i >= 0; i--)
+		if (p[i] != '/')
+			break;
+	if (i < 0)
+		return (p, nil);
+	p = p[0:i+1];
+	for (i = len p - 1; i >=0; i--)
+		if (p[i] == '/')
+			break;
+	if (i < 0)
+		return (".", p);
+	return (p[0:i+1], p[i+1:]);
+}
+
+reporterror(ctxt: ref Context, err: string)
+{
+	if (ctxt.options() & ctxt.VERBOSE)
+		sys->fprint(sys->fildes(2), "%s\n", err);
+}
--- /dev/null
+++ b/appl/cmd/sh/mkfile
@@ -1,0 +1,66 @@
+<../../../mkconfig
+
+TARG=sh.dis\
+	arg.dis\
+	expr.dis\
+	mpexpr.dis\
+	file2chan.dis\
+	mload.dis\
+	regex.dis\
+	sexprs.dis\
+	std.dis\
+	string.dis\
+	tk.dis\
+	echo.dis\
+	csv.dis\
+	test.dis\
+
+INS=	$ROOT/dis/sh.dis\
+	$ROOT/dis/sh/arg.dis\
+	$ROOT/dis/sh/expr.dis\
+	$ROOT/dis/sh/file2chan.dis\
+	$ROOT/dis/sh/mload.dis\
+	$ROOT/dis/sh/mpexpr.dis\
+	$ROOT/dis/sh/regex.dis\
+	$ROOT/dis/sh/std.dis\
+	$ROOT/dis/sh/string.dis\
+#	$ROOT/dis/sh/tk.dis\
+	$ROOT/dis/sh/echo.dis\
+	$ROOT/dis/sh/csv.dis\
+	$ROOT/dis/sh/test.dis\
+
+SYSMODULES=\
+	bufio.m\
+	draw.m\
+	env.m\
+	filepat.m\
+	lock.m\
+	sexprs.m\
+	sh.m\
+	string.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+
+DISBIN=$ROOT/dis/sh
+
+<$ROOT/mkfiles/mkdis
+
+all:V:		$TARG
+
+install:V:	$INS
+	cp $DISBIN/sh.dis $DISBIN/..
+
+nuke:V: clean
+	rm -f $INS
+
+clean:V:
+	rm -f *.dis *.sbl
+
+uninstall:V:
+	rm -f $INS
+
+$ROOT/dis/sh.dis:	sh.dis
+	rm -f $ROOT/dis/sh.dis && cp sh.dis $ROOT/dis/sh.dis
+
+%.dis: ${SYSMODULES:%=$MODDIR/%}
--- /dev/null
+++ b/appl/cmd/sh/mload.b
@@ -1,0 +1,348 @@
+implement Sh;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	myself: Shellbuiltin;
+	mysh: Sh;
+
+Namespace: adt {
+	name: string;
+	madecmd: array of int;
+	mods: list of (string, Shellbuiltin);
+	builtins: array of list of (string, Shellbuiltin);
+};
+Builtin, Sbuiltin: con iota;
+
+namespaces: list of ref Namespace;
+pending: list of (string, int, Shellbuiltin);
+lock: chan of int;
+BUILTINPATH: con "/dis/sh";
+
+initbuiltin(c: ref Sh->Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	mysh = load Sh "$self";
+	myself = load Shellbuiltin "$self";
+	sh->c.addbuiltin("mload", myself);
+	sh->c.addbuiltin("munload", myself);
+	lock = chan[1] of int;
+	return nil;
+}
+
+runbuiltin(ctxt: ref Sh->Context, nil: Sh,
+			argv: list of ref Sh->Listnode, last: int): string
+{
+	cmd := (hd argv).word;
+	case cmd {
+	"mload" or "munload" =>
+		if(tl argv == nil)
+			ctxt.fail("usage", "usage: "+cmd+" name [module...]");
+
+		# by doing this lock, we're relying on modules not to invoke a command
+		# in initbuiltin that calls back into mload. since they shouldn't be running
+		# any commands in initbuiltin anyway, this seems like a reasonable assumption.
+		lock <-= 1;
+		{
+			name := (hd tl argv).word;
+			for(argv = tl tl argv; argv != nil; argv = tl argv){
+				if((hd argv).cmd != nil)
+					ctxt.fail("usage", "usage: "+cmd+" namespace [module...]");
+				if(cmd == "mload")
+					mload(ctxt, name, (hd argv).word);
+				else
+					munload(ctxt, name, (hd argv).word);
+			}
+		}exception{
+		"fail:*" =>
+			<-lock;
+			raise;
+		}
+		<-lock;
+		return nil;
+	* =>
+		if(len argv < 2)
+			ctxt.fail("usage", sys->sprint("usage: %s command", (hd argv).word));
+
+		b := lookup(ctxt, (hd argv).word, (hd tl argv).word, Builtin, nil);
+		return b->runbuiltin(ctxt, mysh, tl argv, last);
+	}
+}
+
+mload(ctxt: ref Sh->Context, name, modname: string): string
+{
+	ns := nslookup(name);
+	if(ns == nil){
+		ns = ref Namespace(name, array[2] of {* => 0}, nil, array[2] of list of (string, Shellbuiltin));
+		namespaces = ns :: namespaces;
+	}
+	for(nsm := ns.mods; nsm != nil; nsm = tl nsm)
+		if((hd nsm).t0 == modname)
+			return nil;
+	path := modname;
+	if (len path < 4 || path[len path-4:] != ".dis")
+		path += ".dis";
+	if (path[0] != '/' && path[0:2] != "./")
+		path = BUILTINPATH + "/" + path;
+	mod := load Shellbuiltin path;
+	if (mod == nil)
+		ctxt.fail("bad module", sys->sprint("load: cannot load %s: %r", path));
+	s := mod->initbuiltin(ctxt, mysh);
+	if(s != nil){
+		munload(ctxt, name, modname);
+		pending = nil;
+		ctxt.fail("init", "mload: init "+modname+" failed: "+s);
+	}
+	mod = mod->getself();
+	ns.mods = (modname, mod) :: ns.mods;
+	for(; pending != nil; pending = tl pending){
+		(cmd, which, pmod) := hd pending;
+		if(pmod != mod)
+			sys->fprint(sys->fildes(2), "mload: unexpected module when loading %#q", name);
+		else
+			lookup(ctxt, name, cmd, which, mod);
+	}
+		
+	return nil;
+}
+
+munload(ctxt: ref Sh->Context, name, modname: string): string
+{
+	ns := nslookup(name);
+	if(ns == nil){
+		sys->fprint(sys->fildes(2), "munload: no such namespace %#q\n", name);
+		return "fail";
+	}
+	nm: list of (string, Shellbuiltin);
+	mod: Shellbuiltin;
+	for(m := ns.mods; m != nil; m = tl m)
+		if((hd m).t0 == modname)
+			mod = (hd m).t1;
+		else
+			nm = hd m :: nm;
+	if(mod == nil){
+		sys->fprint(sys->fildes(2), "munload: no such module %#q\n", modname);
+		return "fail";
+	}
+	ns.mods = nm;
+	for(i := 0; i < 2; i++){
+		nb: list of (string, Shellbuiltin) = nil;
+		for(b := ns.builtins[i]; b != nil; b = tl b)
+			if((hd b).t1 != mod)
+				nb = hd b :: nb;
+		ns.builtins[i] = nb;
+		if(ns.builtins[i] == nil){
+			if(i == Builtin)
+				sh->ctxt.removebuiltin(name, myself);
+			else
+				sh->ctxt.removesbuiltin(name, myself);
+		}
+			
+	}
+	return nil;
+}
+
+
+runsbuiltin(ctxt: ref Sh->Context, nil: Sh,
+			argv: list of ref Sh->Listnode): list of ref Sh->Listnode
+{
+	if(len argv < 2)
+		ctxt.fail("usage", sys->sprint("usage: %s command", (hd argv).word));
+	b := lookup(ctxt, (hd argv).word, (hd tl argv).word, Sbuiltin, nil);
+	return b->runsbuiltin(ctxt, mysh, tl argv);
+}
+
+searchns(mod: Shellbuiltin): string
+{
+	for(m := namespaces; m != nil; m = tl m)
+		for(b := (hd m).mods; b != nil; b = tl b)
+			if((hd b).t1 == mod)
+				return (hd m).name;
+	return nil;
+}
+
+lookup(ctxt: ref Sh->Context, name, cmd: string, which: int, sb: Shellbuiltin): Shellbuiltin
+{
+	for(m := namespaces; m != nil; m = tl m)
+		if((hd m).name == name)
+			break;
+	if(m == nil)
+		ctxt.fail("unknown", sys->sprint("unknown namespace %q", name));
+	ns := hd m;
+	for(b := ns.builtins[which]; b != nil; b = tl b)
+			if((hd b).t0 == cmd)
+				break;
+	if(b == nil){
+		if(sb != nil){
+			ns.builtins[which] = (cmd, sb) :: ns.builtins[which];
+			if(!ns.madecmd[which]){
+				if(which == Builtin)
+					sh->ctxt.addbuiltin(name, myself);
+				else
+					sh->ctxt.addsbuiltin(name, myself);
+				ns.madecmd[which] = 1;
+			}
+			return sb;
+		}
+		ctxt.fail("unknown cmd", sys->sprint("unknown command %q", cmd));
+	}
+	return (hd b).t1;
+}
+
+Context.addbuiltin(c: self ref Context, modname: string, mod: Shellbuiltin)
+{
+	name := searchns(mod);
+	if(name == nil)
+		pending = (modname, Builtin, mod) :: pending;
+	else
+		lookup(c, name, modname, Builtin, mod);
+}
+
+Context.addsbuiltin(c: self ref Context, modname: string, mod: Shellbuiltin)
+{
+	name := searchns(mod);
+	if(name == nil)
+		pending = (modname, Sbuiltin, mod) :: pending;
+	else
+		lookup(c, name, modname, Sbuiltin, mod);
+}
+
+Context.removebuiltin(c: self ref Context, nil: string, nil: Shellbuiltin)
+{
+	c.fail("nope", "mload: remove builtin not implemented");
+}
+
+Context.removesbuiltin(c: self ref Context, nil: string, nil: Shellbuiltin)
+{
+	c.fail("nope", "mload: remove sbuiltin not implemented");
+}
+
+Context.addmodule(nil: self ref Context, name: string, nil: Shellbuiltin)
+{
+	sys->fprint(sys->fildes(2), "mload: addmodule not allowed (%s)\n", name);
+}
+
+nslookup(name: string): ref Namespace
+{
+	for(m := namespaces; m != nil; m = tl m)
+		if((hd m).name == name)
+			return hd m;
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+initialise()
+{
+	return sh->initialise();
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	return sh->init(ctxt, argv);
+}
+
+system(ctxt: ref Draw->Context, cmd: string): string
+{
+	return sh->system(ctxt, cmd);
+}
+
+run(ctxt: ref Draw->Context, argv: list of string): string
+{
+	return sh->run(ctxt, argv);
+}
+	
+parse(s: string): (ref Cmd, string)
+{
+	return sh->parse(s);
+}
+
+cmd2string(c: ref Cmd): string
+{
+	return sh->cmd2string(c);
+}
+
+list2stringlist(nl: list of ref Listnode): list of string
+{
+	return sh->list2stringlist(nl);
+}
+
+stringlist2list(sl: list of string): list of ref Listnode
+{
+	return sh->stringlist2list(sl);
+}
+
+quoted(val: list of ref Listnode, quoteblocks: int): string
+{
+	return sh->quoted(val, quoteblocks);
+}
+
+Context.new(drawcontext: ref Draw->Context): ref Context
+{
+	return sh->Context.new(drawcontext);
+}
+
+Context.get(c: self ref Context, name: string): list of ref Listnode
+{
+	return sh->c.get(name);
+}
+
+Context.set(c: self ref Context, name: string, val: list of ref Listnode)
+{
+	return sh->c.set(name, val);
+}
+
+Context.setlocal(c: self ref Context, name: string, val: list of ref Listnode)
+{
+	return sh->c.setlocal(name, val);
+}
+
+Context.envlist(c: self ref Context): list of (string, list of ref Listnode)
+{
+	return sh->c.envlist();
+}
+
+Context.push(c: self ref Context)
+{
+	return sh->c.push();
+}
+
+Context.pop(c: self ref Context)
+{
+	return sh->c.pop();
+}
+
+Context.copy(c: self ref Context, copyenv: int): ref Context
+{
+	return sh->c.copy(copyenv);
+}
+
+Context.run(c: self ref Context, args: list of ref Listnode, last: int): string
+{
+	return sh->c.run(args, last);
+}
+
+Context.fail(c: self ref Context, ename, msg: string)
+{
+	return sh->c.fail(ename, msg);
+}
+
+Context.options(c: self ref Context): int
+{
+	return sh->c.options();
+}
+
+Context.setoptions(c: self ref Context, flags, on: int): int
+{
+	return sh->c.setoptions(flags, on);
+}
--- /dev/null
+++ b/appl/cmd/sh/mpexpr.b
@@ -1,0 +1,435 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "keyring.m";
+	keyring: Keyring;
+	IPint: import keyring;
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+
+Big: type ref IPint;
+Zero: Big;
+One: Big;
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("expr: cannot load self: %r"));
+
+	Zero = IPint.inttoip(0);
+	One = IPint.inttoip(1);
+	ctxt.addsbuiltin("expr", myself);
+	ctxt.addbuiltin("ntest", myself);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+EQ, GT, LT, GE, LE, PLUS, MINUS, DIVIDE, AND, TIMES, MOD,
+OR, XOR, UMINUS, SHL, SHR, NOT, BNOT, NEQ, REP, SEQ,
+BITS, EXPMOD, INVERT, RAND, EXP: con iota;
+
+runbuiltin(ctxt: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode, nil: int): string
+{
+	case (hd cmd).word {
+	"ntest" =>
+		if (len cmd != 2)
+			ctxt.fail("usage", "usage: ntest n");
+		if(strtoip(ctxt, (hd tl cmd).word).eq(Zero))
+			return "false";
+	}
+	return nil;
+}
+
+runsbuiltin(ctxt: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode): list of ref Listnode
+{
+	# only one sbuiltin: expr.
+	stk: list of Big;
+	lastop := -1;
+	lastn := -1;
+	lastname := "";
+	radix: int;
+	(cmd, radix) = opts(ctxt, tl cmd);
+	for (; cmd != nil; cmd = tl cmd) {
+		w := (hd cmd).word;
+		op := -1;
+		nops := 2;
+		case w {
+		"+" =>
+			op = PLUS; 
+		"-" =>
+			op = MINUS;
+		"x" or "*" or "×" =>
+			op = TIMES;
+		"/" =>
+			op = DIVIDE;
+		"%" =>
+			op = MOD;
+		"and" =>
+			op = AND;
+		"or" =>
+			op = OR;
+		"xor" =>
+			op = XOR;
+		"_"=>
+			(op, nops) = (UMINUS, 1);
+		"<<" or "shl" =>
+			op = SHL;
+		">>" or "shr" =>
+			op = SHR;
+		"=" or "==" or "eq" =>
+			op = EQ;
+		"!=" or "neq" =>
+			op = NEQ;
+		">" or "gt" =>
+			op = GT;
+		"<" or "lt" =>
+			op = LT;
+		">=" or "ge" =>
+			op = GE;
+		"<=" or "le" =>
+			op = LE;
+		"!" or "not" =>
+			(op, nops) = (NOT, 1);
+		"~" =>
+			(op, nops) = (BNOT, 1);
+		"rep" =>
+			(op, nops) = (REP, 0);
+		"seq" =>
+			(op, nops) = (SEQ, 2);
+		"bits" =>
+			(op, nops) = (BITS, 1);
+		"expmod" =>
+			(op, nops) = (EXPMOD, 3);
+		"invert" =>
+			(op, nops) = (INVERT, 2);
+		"rand" =>
+			(op, nops) = (RAND, 1);
+		"exp" or "xx" or "**" =>
+			(op, nops) = (EXP, 2);
+		}
+		if (op == -1){
+			if (w == nil || (w[0] != '-' && (w[0] < '0' || w[0] > '9')))
+				ctxt.fail("usage", sys->sprint("expr: unknown operator '%s'", w));
+			stk = strtoip(ctxt, w) :: stk;
+		}else
+			stk = operator(ctxt, stk, op, nops, lastop, lastn, w, lastname);
+		lastop = op;
+		lastn = nops;
+		lastname = w;
+	}
+	r: list of ref Listnode;
+	for (; stk != nil; stk = tl stk)
+		r = ref Listnode(nil, iptostr(hd stk, radix)) :: r;
+	return r;
+}
+
+opts(ctxt: ref Context, cmd: list of ref Listnode): (list of ref Listnode, int)
+{
+	if (cmd == nil)
+		return (nil, 10);
+	w := (hd cmd).word;
+	if (len w < 2)
+		return (cmd, 10);
+	if (w[0] != '-' || (w[1] >= '0' && w[1] <= '9'))
+		return (cmd, 10);
+	if (w[1] != 'r')
+		ctxt.fail("usage", "usage: expr [-r radix] [arg...]");
+	if (len w > 2)
+		w = w[2:];
+	else {
+		if (tl cmd == nil)
+			ctxt.fail("usage", "usage: expr [-r radix] [arg...]");
+		cmd = tl cmd;
+		w = (hd cmd).word;
+	}
+	r := int w;
+	if (r <= 0 || (r > 36 && r != 64))
+		ctxt.fail("usage", "expr: invalid radix " + string r);
+	return (tl cmd, int w);
+}
+
+operator(ctxt: ref Context, stk: list of Big, op, nops, lastop, lastn: int,
+		opname, lastopname: string): list of Big
+{
+	al: list of Big;
+	for (i := 0; i < nops; i++) {
+		if (stk == nil)
+			ctxt.fail("empty stack",
+				sys->sprint("expr: empty stack on op '%s'", opname));
+		al = hd stk :: al;
+		stk = tl stk;
+	}
+	return oper(ctxt, al, op, lastop, lastn, lastopname, stk);
+}
+
+# args are in reverse order
+oper(ctxt: ref Context, args: list of Big, op, lastop, lastn: int,
+		lastopname: string, stk: list of Big): list of Big
+{
+	if (op == REP) {
+		if (lastop == -1 || lastop == SEQ || lastn != 2)
+			ctxt.fail("usage", "expr: bad operator for rep");
+		if (stk == nil || tl stk == nil)
+			return stk;
+		while (tl stk != nil)
+			stk = operator(ctxt, stk, lastop, 2, -1, -1, lastopname, nil);
+		return stk;
+	}
+	n3 := Zero;
+	n2 := Zero;
+	n1 := hd args;
+	if (tl args != nil){
+		n2 = hd tl args;
+		if(tl tl args != nil)
+			n3 = hd tl tl args;
+	}
+	r := Zero;
+	case op {
+	EQ =>	r = mki(n1.eq(n2));
+	NEQ =>	r = mki(!n1.eq(n2));
+	GT =>	r = mki(n1.cmp(n2) > 0);
+	LT =>	r = mki(n1.cmp(n2) < 0);
+	GE =>	r = mki(n1.cmp(n2) >= 0);
+	LE =>	r = mki(n1.cmp(n2) <= 0);
+	PLUS =>	r = n1.add(n2);
+	MINUS =>	r = n1.sub(n2);
+	NOT	 =>	r = mki(n1.eq(Zero));
+	DIVIDE =>
+			if (n2.eq(Zero))
+				ctxt.fail("divide by zero", "expr: division by zero");
+			(r, nil) = n1.div(n2);
+	MOD =>
+			if (n2.eq(Zero))
+				ctxt.fail("divide by zero", "expr: division by zero");
+			(nil, r) = n1.div(n2);
+	TIMES =>
+			r = n1.mul(n2);
+	AND =>	r = bitop(ipand, n1, n2);
+	OR =>	r = bitop(ipor, n1, n2);
+	XOR =>	r = bitop(ipxor, n1, n2);
+	UMINUS => r = n1.neg();
+	BNOT =>	r = n1.neg().sub(One);
+	SHL =>	r = n1.shl(n2.iptoint());
+	SHR =>	r = n1.shr(n2.iptoint());
+	SEQ =>	return seq(n1, n2, stk);
+	BITS =>	r = mki(n1.bits());
+	EXPMOD =>	r = n1.expmod(n2, n3);
+	EXP =>	r = n1.expmod(n2, nil);
+	RAND =>	r = IPint.random(0, n1.iptoint());
+	INVERT =>	r = n1.invert(n2);
+	}
+	return r :: stk;
+}
+
+# won't work if op(0, 0) != 0
+bitop(op: ref fn(n1, n2: Big): Big, n1, n2: Big): Big
+{
+	bits := max(n1.bits(), n2.bits());
+	return signedmag(op(twoscomp(n1, bits), twoscomp(n2, bits)), bits);
+}	
+
+onebits(n: int): Big
+{
+	return One.shl(n).sub(One);
+}
+
+# return a two's complement version of n,
+# sign-extended to b bits if negative.
+# sign bit is at 1<<b.
+twoscomp(n: Big, b: int): Big
+{
+	if(n.cmp(Zero) >= 0)
+		return n;
+	return n.not().ori(onebits(b).xor(onebits(n.bits()))).add(One);
+}
+
+# return conventional representation of n,
+# where n is in two's complement form in b bits.
+signedmag(n: Big, b: int): Big
+{
+	if(n.and(One.shl(b)).eq(Zero))
+		return n;
+	return n.sub(One).not().and(onebits(b)).neg();
+}
+
+max(x, y: int): int
+{
+	if(x > y)
+		return x;
+	else
+		return y;
+}
+
+seq(n1, n2: Big, stk: list of Big): list of Big
+{
+	incr := mki(1);
+	if (n2.cmp(n1) < 0)
+		incr = mki(-1);
+	for (; !n1.eq(n2); n1 = n1.add(incr))
+		stk = n1 :: stk;
+	return n1 :: stk;
+}
+
+strtoip(ctxt: ref Context, s: string): Big
+{
+	t := s;	
+	if (neg := s[0] == '-')
+		s = s[1:];
+	radix := 10;
+	for (i := 0; i < len s && i < 3; i++) {
+		if (s[i] == 'r') {
+			radix = int s;
+			s = s[i+1:];
+			break;
+		}
+	}
+	if (radix == 10)
+		return IPint.strtoip(s, 10);
+	if (radix == 0 || (radix > 36 && radix != 64))
+		ctxt.fail("usage", "expr: bad number " + t);
+	n := Zero;
+	case radix {
+	10 or 16 or 64 =>
+		n = IPint.strtoip(s, radix);
+	* =>
+		r := mki(radix);
+		for (i = 0; i < len s; i++) {
+			if ('0' <= s[i] && s[i] <= '9')
+				n = n.mul(r).add(mki(s[i] - '0'));
+			else if ('a' <= s[i] && s[i] < 'a' + radix - 10)
+				n = n.mul(r).add(mki(s[i] - 'a' + 10));
+			else if ('A' <= s[i] && s[i]  < 'A' + radix - 10)
+				n = n.mul(r).add(mki(s[i] - 'A' + 10));
+			else
+				break;
+		}
+	}
+	if(neg)
+		return n.neg();
+	return n;
+}
+
+iptostr(n: Big, radix: int): string
+{
+	neg := n.cmp(Zero) < 0;
+	t: string;
+	case radix {
+	2 or 4 or 16 or 32 =>
+		b := n.iptobebytes();
+		rbits := log2(radix);
+		bits := roundup(n.bits(), rbits);
+		for(i := bits - rbits; i >= 0; i -= rbits){
+			d := 0;
+			for(j := 0; j < rbits; j++)
+				d |= getbit(b, i+j) << j;
+			t[len t] = digit(d);
+		}
+	10 =>
+		return n.iptostr(radix);
+	64 =>
+		t = n.iptostr(radix);
+		if(neg)
+			t = t[1:];
+	* =>
+		if(neg)
+			n = n.neg();
+		r := mki(radix);
+		s: string;
+		do{
+			d: Big;
+			(n, d) = n.div(r);
+			s[len s] = digit(d.iptoint());
+		}while(n.cmp(Zero) > 0);
+		t = s;
+		for (i := len s - 1; i >= 0; i--)
+			t[len s - 1 - i] = s[i];
+	}
+	t = string radix + "r" + t;
+	if (neg)
+		return "-" + t;
+	return t;
+}
+
+mki(i: int): Big
+{
+	return IPint.inttoip(i);
+}
+
+b2s(b: array of byte): string
+{
+	s := "";
+	for(i := 0; i < len b; i++)
+		s += sys->sprint("%.2x", int b[i]);
+	return s;
+}
+
+# count from least significant bit.
+getbit(b: array of byte, bit: int): int
+{
+	if((i := bit >> 3) >= len b){
+		return 0;
+	}else{
+		return (int b[len b - i -1] >> (bit&7)) & 1;
+	}
+}
+
+digit(d: int): int
+{
+	if(d < 10)
+		return '0' + d;
+	else
+		return 'a' + d - 10;
+}
+
+log2(x: int): int
+{
+	case x {
+	2 =>	return 1;
+	4 =>	return 2;
+	8 => return 3;
+	16 => return 4;
+	32 => return 5;
+	}
+	return 0;
+}
+
+roundup(n: int, m: int): int
+{
+	return m*((n+m-1)/m);
+}
+
+# these functions are to get around the fact that the limbo compiler isn't
+# currently considering ref fn(x: self X, ...) compatible with ref fn(x: X, ...).
+ipand(n1, n2: Big): Big
+{
+	return n1.and(n2);
+}
+
+ipor(n1, n2: Big): Big
+{
+	return n1.ori(n2);
+}
+
+
+ipxor(n1, n2: Big): Big
+{
+	return n1.xor(n2);
+}
--- /dev/null
+++ b/appl/cmd/sh/regex.b
@@ -1,0 +1,220 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+include "regex.m";
+	regex: Regex;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("regex: cannot load self: %r"));
+	regex = load Regex Regex->PATH;
+	if (regex == nil)
+		ctxt.fail("bad module",
+			sys->sprint("regex: cannot load %s: %r", Regex->PATH));
+	ctxt.addbuiltin("match", myself);
+	ctxt.addsbuiltin("re", myself);
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(ctxt: ref Context, nil: Sh,
+			argv: list of ref Listnode, nil: int): string
+{
+	case (hd argv).word {
+	"match" =>
+		return builtin_match(ctxt, argv);
+	}
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+runsbuiltin(ctxt: ref Context, nil: Sh,
+			argv: list of ref Listnode): list of ref Listnode
+{
+	name := (hd argv).word;
+	case name {
+	"re" =>
+		return sbuiltin_re(ctxt, argv);
+	}
+	return nil;
+}
+
+sbuiltin_re(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	if (tl argv == nil)
+		ctxt.fail("usage", "usage: re (g|v|s|sg|m|mg|M) arg...");
+	argv = tl argv;
+	w := (hd argv).word;
+	case w {
+	"g" or
+	"v" =>
+		return sbuiltin_sel(ctxt, argv, w == "v");
+	"s" or
+	"sg" =>
+		return sbuiltin_sub(ctxt, argv, w == "sg");
+	"m" =>
+		return sbuiltin_match(ctxt, argv, 0);
+	"mg" =>
+		return sbuiltin_gmatch(ctxt, argv);
+	"M" =>
+		return sbuiltin_match(ctxt, argv, 1);
+	* =>
+		ctxt.fail("usage", "usage: re (g|v|s|sg|m|mg|M) arg...");
+		return nil;
+	}
+}
+
+sbuiltin_match(ctxt: ref Context, argv: list of ref Listnode, aflag: int): list of ref Listnode
+{
+	if (len argv != 3)
+		ctxt.fail("usage", "usage: re " + (hd argv).word + " arg");
+	argv = tl argv;
+	re := getregex(ctxt, word(hd argv), aflag);
+	w := word(hd tl argv);
+	a := regex->execute(re, w);
+	if (a == nil)
+		return nil;
+	ret: list of ref Listnode;
+	for (i := len a - 1; i >= 0; i--)
+		ret = ref Listnode(nil, elem(a, i, w)) :: ret;
+	return ret;
+}
+
+sbuiltin_gmatch(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	if (len argv != 3)
+		ctxt.fail("usage", "usage: re mg arg");
+	argv = tl argv;
+	re := getregex(ctxt, word(hd argv), 0);
+	w := word(hd tl argv);
+	ret, nret: list of ref Listnode;
+	beg := 0;
+	while ((a := regex->executese(re, w, (beg, len w), beg == 0, 1)) != nil) {
+		(s, e) := a[0];
+		ret = ref Listnode(nil, w[s:e]) :: ret;
+		if (s == e)
+			break;
+		beg = e;
+	}
+	for (; ret != nil; ret = tl ret)
+		nret = hd ret :: nret;
+	return nret;
+}
+
+sbuiltin_sel(ctxt: ref Context, argv: list of ref Listnode, vflag: int): list of ref Listnode
+{
+	cmd := (hd argv).word;
+	argv = tl argv;
+	if (argv == nil)
+		ctxt.fail("usage", "usage: " + cmd + " regex [arg...]");
+	re := getregex(ctxt, word(hd argv), 0);
+	ret, nret: list of ref Listnode;
+	for (argv = tl argv; argv != nil; argv = tl argv)
+		if (vflag ^ (regex->execute(re, word(hd argv)) != nil))
+			ret = hd argv :: ret;
+	for (; ret != nil; ret = tl ret)
+		nret = hd ret :: nret;
+	return nret;
+}
+
+sbuiltin_sub(ctxt: ref Context, argv: list of ref Listnode, gflag: int): list of ref Listnode
+{
+	cmd := (hd argv).word;
+	argv = tl argv;
+	if (argv == nil || tl argv == nil)
+		ctxt.fail("usage", "usage: " + cmd + " regex subs [arg...]");
+	re := getregex(ctxt, word(hd argv), 1);
+	subs := word(hd tl argv);
+	ret, nret: list of ref Listnode;
+	for (argv = tl tl argv; argv != nil; argv = tl argv)
+		ret = ref Listnode(nil, substitute(word(hd argv), re, subs, gflag).t1) :: ret;
+	for (; ret != nil; ret = tl ret)
+		nret = hd ret :: nret;
+	return nret;
+}
+
+builtin_match(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	if (tl argv == nil)
+		ctxt.fail("usage", "usage: match regexp [arg...]");
+	re := getregex(ctxt, word(hd tl argv), 0);
+	for (argv = tl tl argv; argv != nil; argv = tl argv)
+		if (regex->execute(re, word(hd argv)) == nil)
+			return "no match";
+	return nil;
+}
+
+substitute(w: string, re: Regex->Re, subs: string, gflag: int): (int, string)
+{
+	matched := 0;
+	s := "";
+	beg := 0;
+	do {
+		a := regex->executese(re, w, (beg, len w), beg == 0, 1);
+		if (a == nil)
+			break;
+		matched = 1;
+		s += w[beg:a[0].t0];
+		for (i := 0; i < len subs; i++) {
+			if (subs[i] != '\\' || i == len subs - 1)
+				s[len s] = subs[i];
+			else {
+				c := subs[++i];
+				if (c < '0' || c > '9')
+					s[len s] = c;
+				else
+					s += elem(a, c - '0', w);
+			}
+		}
+		beg = a[0].t1;
+		if (a[0].t0 == a[0].t1)
+			break;
+	} while (gflag && beg < len w);
+	return (matched, s + w[beg:]);
+}
+
+elem(a: array of (int, int), i: int, w: string): string
+{
+	if (i < 0 || i >= len a)
+		return nil;		# XXX could raise failure here. (invalid backslash escape)
+	(s, e) := a[i];
+	if (s == -1)
+		return nil;
+	return w[s:e];
+}
+
+# XXX could do regex caching here if it was worth it.
+getregex(ctxt: ref Context, res: string, flag: int): Regex->Re
+{
+	(re, err) := regex->compile(res, flag);
+	if (re == nil)
+		ctxt.fail("bad regex", "regex: bad regex \"" + res + "\": " + err);
+	return re;
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
--- /dev/null
+++ b/appl/cmd/sh/sexprs.b
@@ -1,0 +1,271 @@
+implement Shellbuiltin;
+
+# parse/generate sexprs.
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "sexprs.m";
+	sexprs: Sexprs;
+	Sexp: import sexprs;
+
+# getsexprs cmd
+# islist val
+# ${els se}
+# ${text se}
+# ${textels se}
+
+# ${mktext val}
+# ${mklist [val...]}
+# ${mktextlist [val...]}
+
+Maxerrs: con 10;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("sexpr: cannot load self: %r"));
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		ctxt.fail("bad module", sys->sprint("sexpr: cannot load: %s: %r", Bufio->PATH));
+	sexprs = load Sexprs Sexprs->PATH;
+	if(sexprs == nil)
+		ctxt.fail("bad module", sys->sprint("sexpr: cannot load: %s: %r", Sexprs->PATH));
+	sexprs->init();
+	ctxt.addbuiltin("getsexprs", myself);
+	ctxt.addbuiltin("islist", myself);
+	ctxt.addsbuiltin("els", myself);
+	ctxt.addsbuiltin("text", myself);
+	ctxt.addsbuiltin("b64", myself);
+	ctxt.addsbuiltin("textels", myself);
+	ctxt.addsbuiltin("mktext", myself);
+	ctxt.addsbuiltin("mklist", myself);
+	ctxt.addsbuiltin("mktextlist", myself);
+
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(c: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode, nil: int): string
+{
+	case (hd cmd).word {
+	"getsexprs" =>
+		return builtin_getsexprs(c, tl cmd);
+	"islist" =>
+		return builtin_islist(c, tl cmd);
+	}
+	return nil;
+}
+
+runsbuiltin(c: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode): list of ref Listnode
+{
+	case (hd cmd).word {
+	"els" =>
+		return sbuiltin_els(c, tl cmd);
+	"text" =>
+		return sbuiltin_text(c, tl cmd);
+	"b64" =>
+		return sbuiltin_b64(c, tl cmd);
+	"textels" =>
+		return sbuiltin_textels(c, tl cmd);
+	"mktext" =>
+		return sbuiltin_mktext(c, tl cmd);
+	"mklist" =>
+		return sbuiltin_mklist(c, tl cmd);
+	"mktextlist" =>
+		return sbuiltin_mktextlist(c, tl cmd);
+	}
+	return nil;
+}
+
+builtin_getsexprs(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	n := len argv;
+	if (n != 1 || !iscmd(hd argv))
+		builtinusage(ctxt, "getsexprs {cmd}");
+	cmd := hd argv :: ctxt.get("*");
+	stdin := bufio->fopen(sys->fildes(0), Sys->OREAD);
+	if (stdin == nil)
+		ctxt.fail("bad input", sys->sprint("getsexprs: cannot open stdin: %r"));
+	status := "";
+	nerrs := 0;
+	ctxt.push();
+	for(;;){
+		{
+			for (;;) {
+				(se, err) := Sexp.read(stdin);
+				if(err != nil){
+					sys->fprint(sys->fildes(2), "getsexprs: error on read: %s\n", err);
+					if(++nerrs > Maxerrs)
+						raise "fail:too many errors";
+					continue;
+				}
+				if(se == nil)
+					break;
+				nerrs = 0;
+				ctxt.setlocal("sexp", ref Listnode(nil, se.text()) :: nil);
+				status = setstatus(ctxt, ctxt.run(cmd, 0));
+			}
+			ctxt.pop();
+			return status;
+		}exception e{
+		"fail:*" =>
+			ctxt.pop();
+			if (loopexcept(e) == BREAK)
+				return status;
+			ctxt.push();
+		}
+	}
+}
+
+builtin_islist(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	if(argv == nil || tl argv != nil)
+		builtinusage(ctxt, "islist sexp");
+	w := word(hd argv);
+	if(w != nil && w[0] =='(')
+		return nil;
+	if(parse(ctxt, hd argv).islist())
+		return nil;
+	return "not a list";
+}
+
+CONTINUE, BREAK: con iota;
+loopexcept(ename: string): int
+{
+	case ename[5:] {
+	"break" =>
+		return BREAK;
+	"continue" =>
+		return CONTINUE;
+	* =>
+		raise ename;
+	}
+	return 0;
+}
+
+iscmd(n: ref Listnode): int
+{
+	return n.cmd != nil || (n.word != nil && n.word[0] == '{');
+}
+	
+builtinusage(ctxt: ref Context, s: string)
+{
+	ctxt.fail("usage", "usage: " + s);
+}
+
+setstatus(ctxt: ref Context, val: string): string
+{
+	ctxt.setlocal("status", ref Listnode(nil, val) :: nil);
+	return val;
+}
+
+sbuiltin_els(ctxt: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	if (val == nil || tl val != nil)
+		builtinusage(ctxt, "els sexp");
+	r, rr: list of ref Listnode;
+	for(els := parse(ctxt, hd val).els(); els != nil; els = tl els)
+		r = ref Listnode(nil, (hd els).text()) :: r;
+	for(; r != nil; r = tl r)
+		rr = hd r :: rr;
+	return rr;
+}
+
+sbuiltin_text(ctxt: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	if(val == nil || tl val != nil)
+		builtinusage(ctxt, "text sexp");
+	return ref Listnode(nil, parse(ctxt, hd val).astext()) :: nil;
+}
+
+sbuiltin_b64(ctxt: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	if(val == nil || tl val != nil)
+		builtinusage(ctxt, "b64 sexp");
+	return ref Listnode(nil, parse(ctxt, hd val).b64text()) :: nil;
+}
+
+sbuiltin_textels(ctxt: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	if (val == nil || tl val != nil)
+		builtinusage(ctxt, "textels sexp");
+	r, rr: list of ref Listnode;
+	for(els := parse(ctxt, hd val).els(); els != nil; els = tl els)
+		r = ref Listnode(nil, (hd els).astext()) :: r;
+	for(; r != nil; r = tl r)
+		rr = hd r :: rr;
+	return rr;
+}
+
+sbuiltin_mktext(ctxt: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	if (val == nil || tl val != nil)
+		builtinusage(ctxt, "mktext sexp");
+	return ref Listnode(nil, (ref Sexp.String(word(hd val), nil)).text()) :: nil;
+}
+
+sbuiltin_mklist(nil: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	if(val == nil)
+		return ref Listnode(nil, "()") :: nil;
+	s := "(" + word(hd val);
+	for(val = tl val; val != nil; val = tl val)
+		s += " " + word(hd val);
+	s[len s] = ')';
+	return ref Listnode(nil, s) :: nil;
+}
+
+sbuiltin_mktextlist(nil: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	if(val == nil)
+		return ref Listnode(nil, "()") :: nil;
+	s := "(" + (ref Sexp.String(word(hd val), nil)).text();
+	for(val = tl val; val != nil; val = tl val)
+		s += " " + (ref Sexp.String(word(hd val), nil)).text();
+	s[len s] = ')';
+	return ref Listnode(nil, s) :: nil;
+}
+
+parse(ctxt: ref Context, val: ref Listnode): ref Sexp
+{
+	(se, rest, err) := Sexp.parse(word(val));
+	if(rest != nil){
+		for(i := 0; i < len rest; i++)
+			if(rest[i] != ' ' && rest[i] != '\t' && rest[i] != '\n')
+				ctxt.fail("bad sexp", sys->sprint("extra text found at end of s-expression %#q", word(val)));
+	}
+	if(err != nil)
+		ctxt.fail("bad sexp", err);
+	return se;
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
--- /dev/null
+++ b/appl/cmd/sh/sh.b
@@ -1,0 +1,2875 @@
+implement Sh;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+include "string.m";
+	str: String;
+include "filepat.m";
+	filepat: Filepat;
+include "env.m";
+	env: Env;
+include "sh.m";
+	myself: Sh;
+	myselfbuiltin: Shellbuiltin;
+
+YYSTYPE: adt {
+	node:	ref Node;
+	word:	string;
+
+	redir:	ref Redir;
+	optype:	int;
+};
+
+YYLEX: adt {
+	lval:			YYSTYPE;
+	err:			string;	# if error has occurred
+	errline:		int;		# line it occurred on.
+	path:			string;	# name of file that's being read.
+
+	# free caret state
+	wasdollar:		int;
+	atendword:	int;
+	eof:			int;
+	cbuf:			array of int;	# last chars read
+	ncbuf:		int;			# number of chars in cbuf
+
+	f:			ref Bufio->Iobuf;
+	s:			string;
+	strpos: 		int;			# string pos/cbuf index
+
+	linenum:		int;
+	prompt:		string;
+	lastnl:		int;
+
+	initstring:		fn(s: string): ref YYLEX;
+	initfile:		fn(fd: ref Sys->FD, path: string): ref YYLEX;
+	lex:			fn(l: self ref YYLEX): int;
+	error:		fn(l: self ref YYLEX, err: string);
+	getc:			fn(l: self ref YYLEX): int;
+	ungetc:		fn(l: self ref YYLEX);
+
+	EOF:			con -1;
+};
+
+Options: adt {
+	lflag,
+	nflag:		int;
+	ctxtflags:		int;
+	carg:			string;
+};
+
+
+	# module definition is in shell.m
+DUP: con	57346;
+REDIR: con	57347;
+WORD: con	57348;
+OP: con	57349;
+END: con	57350;
+ERROR: con	57351;
+ANDAND: con	57352;
+OROR: con	57353;
+YYEOFCODE: con 1;
+YYERRCODE: con 2;
+YYMAXDEPTH: con 200;
+
+
+
+EPERM: con "permission denied";
+EPIPE: con "write on closed pipe";
+
+LIBSHELLRC: con "/lib/sh/profile";
+BUILTINPATH: con "/dis/sh";
+
+DEBUG: con 0;
+
+ENVSEP: con 0;				# word seperator in external environment
+ENVHASHSIZE: con 7;		# XXX profile usage of this...
+OAPPEND: con 16r80000;		# make sure this doesn't clash with O* constants in sys.m
+OMASK: con 7;
+
+usage()
+{
+	sys->fprint(stderr(), "usage: sh [-ilexn] [-c command] [file [arg...]]\n");
+	raise "fail:usage";
+}
+
+badmodule(path: string)
+{
+	sys->fprint(sys->fildes(2), "sh: cannot load %s: %r\n", path);
+	raise "fail:bad module" ;
+}
+
+initialise()
+{
+	if (sys == nil) {
+		sys = load Sys Sys->PATH;
+
+		filepat = load Filepat Filepat->PATH;
+		if (filepat == nil) badmodule(Filepat->PATH);
+
+		str = load String String->PATH;
+		if (str == nil) badmodule(String->PATH);
+
+		bufio = load Bufio Bufio->PATH;
+		if (bufio == nil) badmodule(Bufio->PATH);
+
+		myself = load Sh "$self";
+		if (myself == nil) badmodule("$self(Sh)");
+
+		myselfbuiltin = load Shellbuiltin "$self";
+		if (myselfbuiltin == nil) badmodule("$self(Shellbuiltin)");
+
+		env = load Env Env->PATH;
+	}
+}
+blankopts: Options;
+init(drawcontext: ref Draw->Context, argv: list of string)
+{
+	initialise();
+	opts := blankopts;
+	if (argv != nil) {
+		if ((hd argv)[0] == '-')
+			opts.lflag++;
+		argv = tl argv;
+	}
+
+	interactive := 0;
+loop: while (argv != nil && hd argv != nil && (hd argv)[0] == '-') {
+		for (i := 1; i < len hd argv; i++) {
+			c := (hd argv)[i];
+			case c {
+			'i' =>
+				interactive = Context.INTERACTIVE;
+			'l' =>
+				opts.lflag++;	# login (read $home/lib/profile)
+			'n' =>
+				opts.nflag++;	# don't fork namespace
+			'e' =>
+				opts.ctxtflags |= Context.ERROREXIT;
+			'x' =>
+				opts.ctxtflags |= Context.EXECPRINT;
+			'c' =>
+				arg: string;
+				if (i < len hd argv - 1) {
+					arg = (hd argv)[i + 1:];
+				} else if (tl argv == nil || hd tl argv == "") {
+					usage();
+				} else {
+					arg = hd tl argv;
+					argv = tl argv;
+				}
+				argv = tl argv;
+				opts.carg = arg;
+				continue loop;
+			}
+		}
+		argv = tl argv;
+	}
+
+	sys->pctl(Sys->FORKFD, nil);
+	if (!opts.nflag)
+		sys->pctl(Sys->FORKNS, nil);
+	ctxt := Context.new(drawcontext);
+	ctxt.setoptions(opts.ctxtflags, 1);
+
+	# if login shell, run standard init script
+	if (opts.lflag)
+		runscript(ctxt, LIBSHELLRC, nil, 0);
+	if (opts.carg != nil) {
+		status := ctxt.run(stringlist2list("{" + opts.carg + "}" :: argv), !interactive);
+		if (!interactive) {
+			if (status != nil)
+				raise "fail:" + status;
+			exit;
+		}
+		setstatus(ctxt, status);
+	}
+	if (argv == nil) {
+		if (isconsole(sys->fildes(0)))
+			interactive |= ctxt.INTERACTIVE;
+		ctxt.setoptions(interactive, 1);
+		runfile(ctxt, sys->fildes(0), "stdin", nil);
+	} else {
+		ctxt.setoptions(interactive, 1);
+		runscript(ctxt, hd argv, stringlist2list(tl argv), 1);
+	}
+}
+
+parse(s: string): (ref Node, string)
+{
+	initialise();
+	
+	lex := YYLEX.initstring(s);
+
+	return doparse(lex, "", 0);
+}
+
+system(drawctxt: ref Draw->Context, cmd: string): string
+{
+	initialise();
+	{
+		(n, err) := parse(cmd);
+		if (err != nil)
+			return err;
+		if (n == nil)
+			return nil;
+		return Context.new(drawctxt).run(ref Listnode(n, nil) :: nil, 0);
+	} exception e {
+	"fail:*" =>
+		return failurestatus(e);
+	}
+}
+
+run(drawctxt: ref Draw->Context, argv: list of string): string
+{
+	initialise();
+	{
+		return Context.new(drawctxt).run(stringlist2list(argv), 0);
+	} exception e {
+	"fail:*" =>
+		return failurestatus(e);
+	}
+}
+
+isconsole(fd: ref Sys->FD): int
+{
+	(ok1, d1) := sys->fstat(fd);
+	(ok2, d2) := sys->stat("/dev/cons");
+	if (ok1 < 0 || ok2 < 0)
+		return 0;
+	return d1.dtype == d2.dtype && d1.qid.path == d2.qid.path;
+}
+
+runscript(ctxt: ref Context, path: string, args: list of ref Listnode, reporterr: int)
+{
+	{
+		fd := sys->open(path, Sys->OREAD);
+		if (fd != nil)
+			runfile(ctxt, fd, path, args);
+		else if (reporterr)
+			ctxt.fail("bad script path", sys->sprint("sh: cannot open %s: %r", path));
+	} exception {
+	"fail:*" =>
+		if(!reporterr)
+			return;
+		raise;
+	}
+}
+
+runfile(ctxt: ref Context, fd: ref Sys->FD, path: string, args: list of ref Listnode)
+{
+	ctxt.push();
+	{
+		ctxt.setlocal("0", stringlist2list(path :: nil));
+		ctxt.setlocal("*", args);
+		lex := YYLEX.initfile(fd, path);
+		if (DEBUG) debug(sprint("parse(interactive == %d)", (ctxt.options() & ctxt.INTERACTIVE) != 0));
+		prompt := "" :: "" :: nil;
+		laststatus: string;
+		while (!lex.eof) {
+			interactive := ctxt.options() & ctxt.INTERACTIVE;
+			if (interactive) {
+				prompt = list2stringlist(ctxt.get("prompt"));
+				if (prompt == nil)
+					prompt = "; " :: "" :: nil;
+	
+				sys->fprint(stderr(), "%s", hd prompt);
+				if (tl prompt == nil) {
+					prompt = hd prompt :: "" :: nil;
+				}
+			}
+			(n, err) := doparse(lex, hd tl prompt, !interactive);
+			if (err != nil) {
+				sys->fprint(stderr(), "sh: %s\n", err);
+				if (!interactive)
+					raise "fail:parse error";
+			} else if (n != nil) {
+				if (interactive) {
+					{
+						laststatus = walk(ctxt, n, 0);
+					} exception e2 {
+					"fail:*" =>
+						laststatus = failurestatus(e2);
+					}
+				} else
+					laststatus = walk(ctxt, n, 0);
+				setstatus(ctxt, laststatus);
+				if ((ctxt.options() & ctxt.ERROREXIT) && laststatus != nil)
+					break;
+			}
+		}
+		if (laststatus != nil)
+			raise "fail:" + laststatus;
+		ctxt.pop();
+	}
+	exception {
+	"fail:*" =>
+		ctxt.pop();
+		raise;
+	}
+}
+
+nonexistent(e: string): int
+{
+	errs := array[] of {"does not exist", "directory entry not found"};
+	for (i := 0; i < len errs; i++){
+		j := len errs[i];
+		if (j <= len e && e[len e-j:] == errs[i])
+			return 1;
+	}
+	return 0;
+}
+
+Redirword: adt {
+	fd: ref Sys->FD;
+	w: string;
+	r: Redir;
+};
+
+Redirlist: adt {
+	r: list of Redirword;
+};
+
+pipe2cmd(n: ref Node): ref Node
+{
+	if (n == nil || n.ntype != n_PIPE)
+		return n;
+	return mk(n_ADJ, mk(n_BLOCK,n,nil), mk(n_VAR,ref Node(n_WORD,nil,nil,"*",nil),nil));
+}
+
+walk(ctxt: ref Context, n: ref Node, last: int): string
+{
+	if (DEBUG) debug(sprint("walking: %s", cmd2string(n)));
+	# avoid tail recursion stack explosion
+	while (n != nil && n.ntype == n_SEQ) {
+		status := walk(ctxt, n.left, 0);
+		if (ctxt.options() & ctxt.ERROREXIT && status != nil)
+			raise "fail:" + status;
+		setstatus(ctxt, status);
+		n = n.right;
+	}
+	if (n == nil)
+		return nil;
+	case (n.ntype) {
+	n_PIPE =>
+		return waitfor(ctxt, walkpipeline(ctxt, n, nil, -1));
+	n_ASSIGN or n_LOCAL =>
+		assign(ctxt, n);
+		return nil;
+	* =>
+		bg := 0;
+		if (n.ntype == n_NOWAIT) {
+			bg = 1;
+			n = pipe2cmd(n.left);
+		}
+
+		redirs := ref Redirlist(nil);
+		line := glob(glom(ctxt, n, redirs, nil));
+
+		if (bg) {
+			startchan := chan of (int, ref Expropagate);
+			spawn runasync(ctxt, 1, line, redirs, startchan);
+			(pid, nil) := <-startchan;
+			redirs = nil;
+			if (DEBUG) debug("started background process "+ string pid);
+			ctxt.set("apid", ref Listnode(nil, string pid) :: nil);
+			return nil;
+		} else {
+			return runsync(ctxt, line, redirs, last);
+		}
+	}
+}
+
+assign(ctxt: ref Context, n: ref Node): list of ref Listnode
+{
+	redirs := ref Redirlist;
+	val: list of ref Listnode;
+	if (n.right != nil && (n.right.ntype == n_ASSIGN || n.right.ntype == n_LOCAL))
+		val = assign(ctxt, n.right);
+	else
+		val = glob(glom(ctxt, n.right, redirs, nil));
+	vars := glom(ctxt, n.left, redirs, nil);
+	if (vars == nil)
+		ctxt.fail("bad assign", "sh: nil variable name");
+	if (redirs.r != nil)
+		ctxt.fail("bad assign", "sh: redirections not allowed in assignment");
+	tval := val;
+	for (; vars != nil; vars = tl vars) {
+		vname := deglob((hd vars).word);
+		if (vname == nil) 
+			ctxt.fail("bad assign", "sh: bad variable name");
+		v: list of ref Listnode = nil;
+		if (tl vars == nil)
+			v = tval;
+		else if (tval != nil)
+			v = hd tval :: nil;
+		if (n.ntype == n_ASSIGN)
+			ctxt.set(vname, v);
+		else
+			ctxt.setlocal(vname, v);
+		if (tval != nil)
+			tval = tl tval;
+	}
+	return val;
+}
+
+walkpipeline(ctxt: ref Context, n: ref Node, wrpipe: ref Sys->FD, wfdno: int): list of int
+{
+	if (n == nil)
+		return nil;
+
+	fds := array[2] of ref Sys->FD;
+	pids: list of int;
+	rfdno := -1;
+	if (n.ntype == n_PIPE) {
+		if (sys->pipe(fds) == -1)
+			ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
+		nwfdno := -1;
+		if (n.redir != nil) {
+			(fd1, fd2) := (n.redir.fd2, n.redir.fd1);
+			if (fd2 == -1)
+				(fd1, fd2) = (fd2, fd1);
+			(nwfdno, rfdno) = (fd2, fd1);
+		}
+		pids = walkpipeline(ctxt, n.left, fds[1], nwfdno);
+		fds[1] = nil;
+		n = n.right;
+	}
+	r := ref Redirlist(nil);
+	rlist := glob(glom(ctxt, n, r, nil));
+	if (fds[0] != nil) {
+		if (rfdno == -1)
+			rfdno = 0;
+		r.r = Redirword(fds[0], nil, Redir(Sys->OREAD, rfdno, -1)) :: r.r;
+	}
+	if (wrpipe != nil) {
+		if (wfdno == -1)
+			wfdno = 1;
+		r.r = Redirword(wrpipe, nil, Redir(Sys->OWRITE, wfdno, -1)) :: r.r;
+	}
+	startchan := chan of (int, ref Expropagate);
+	spawn runasync(ctxt, 1, rlist, r, startchan);
+	(pid, nil) := <-startchan;
+	if (DEBUG) debug("started pipe process "+string pid);
+	return pid :: pids;
+}
+
+makeredir(f: string, mode: int, fd: int): Redirword
+{
+	return Redirword(nil, f, Redir(mode, fd, -1));
+}
+
+glom(ctxt: ref Context, n: ref Node, redirs: ref Redirlist, onto: list of ref Listnode)
+		: list of ref Listnode
+{
+	if (n == nil) return nil;
+
+	if (n.ntype != n_ADJ)
+		return listjoin(glomoperation(ctxt, n, redirs), onto);
+
+	nlist := glom(ctxt, n.right, redirs, onto);
+
+	if (n.left.ntype != n_ADJ) {
+		# if it's a terminal node
+		nlist = listjoin(glomoperation(ctxt, n.left, redirs), nlist);
+	} else
+		nlist = glom(ctxt, n.left, redirs, nlist);
+	return nlist;
+}
+
+listjoin(left, right: list of ref Listnode): list of ref Listnode
+{
+	l: list of ref Listnode;
+	for (; left != nil; left = tl left)
+		l = hd left :: l;
+	for (; l != nil; l = tl l)
+		right = hd l :: right;
+	return right;
+}
+
+pipecmd(ctxt: ref Context, cmd: list of ref Listnode, redir: ref Redir): ref Sys->FD
+{
+	if(redir.fd2 != -1 || (redir.rtype & OAPPEND))
+		ctxt.fail("bad redir", "sh: bad redirection");
+	r := *redir;
+	case redir.rtype {
+	Sys->OREAD =>
+		r.rtype = Sys->OWRITE;
+	Sys->OWRITE =>
+		r.rtype = Sys->OREAD;
+	}
+			
+	p := array[2] of ref Sys->FD;
+	if(sys->pipe(p) == -1)
+		ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
+	startchan := chan of (int, ref Expropagate);
+	spawn runasync(ctxt, 1, cmd, ref Redirlist((p[1], nil, r) :: nil), startchan);
+	p[1] = nil;
+	<-startchan;
+	return p[0];
+}
+
+glomoperation(ctxt: ref Context, n: ref Node, redirs: ref Redirlist): list of ref Listnode
+{
+	if (n == nil)
+		return nil;
+
+	nlist: list of ref Listnode;
+	case n.ntype {
+	n_WORD =>
+		nlist = ref Listnode(nil, n.word) :: nil;
+	n_REDIR =>
+		wlist := glob(glom(ctxt, n.left, ref Redirlist(nil), nil));
+		if (len wlist != 1)
+			ctxt.fail("bad redir", "sh: single redirection operand required");
+		if((hd wlist).cmd != nil){
+			fd := pipecmd(ctxt, wlist, n.redir);
+			redirs.r = Redirword(fd, nil, (n.redir.rtype, fd.fd, -1)) :: redirs.r;
+			nlist = ref Listnode(nil, "/fd/"+string fd.fd) :: nil;
+		}else{
+			redirs.r = Redirword(nil, (hd wlist).word, *n.redir) :: redirs.r;
+		}
+	n_DUP =>
+		redirs.r = Redirword(nil, "", *n.redir) :: redirs.r;
+	n_LIST =>
+		nlist = glom(ctxt, n.left, redirs, nil);
+	n_CONCAT =>
+		nlist = concat(ctxt, glom(ctxt, n.left, redirs, nil), glom(ctxt, n.right, redirs, nil));
+	n_VAR or n_SQUASH or n_COUNT =>
+		arg := glom(ctxt, n.left, ref Redirlist(nil), nil);
+		if (len arg == 1 && (hd arg).cmd != nil)
+			nlist = subsbuiltin(ctxt, (hd arg).cmd.left);
+		else if (len arg != 1 || (hd arg).word == nil)
+			ctxt.fail("bad $ arg", "sh: bad variable name");
+		else
+			nlist = ctxt.get(deglob((hd arg).word));
+		case n.ntype {
+		n_VAR =>;
+		n_COUNT =>
+			nlist = ref Listnode(nil, string len nlist) :: nil;
+		n_SQUASH =>
+			# XXX could squash with first char of $ifs, perhaps
+			nlist = ref Listnode(nil, squash(list2stringlist(nlist), " ")) :: nil;
+		}
+	n_BQ or n_BQ2 =>
+		arg := glom(ctxt, n.left, ref Redirlist(nil), nil);
+		seps := "";
+		if (n.ntype == n_BQ) {
+			seps = squash(list2stringlist(ctxt.get("ifs")), "");
+			if (seps == nil)
+				seps = " \t\n\r";
+		}
+		(nlist, nil) = bq(ctxt, glob(arg), seps);
+	n_BLOCK =>
+		nlist = ref Listnode(n, "") :: nil;
+	n_ASSIGN or n_LOCAL =>
+		ctxt.fail("bad assign", "sh: assignment in invalid context");
+	* =>
+		panic("bad node type "+string n.ntype+" in glomop");
+	}
+	return nlist;
+}
+
+subsbuiltin(ctxt: ref Context, n: ref Node): list of ref Listnode
+{
+	if (n == nil || n.ntype == n_SEQ ||
+			n.ntype == n_PIPE || n.ntype == n_NOWAIT)
+		ctxt.fail("bad $ arg", "sh: invalid argument to ${} operator");
+	r := ref Redirlist;
+	cmd := glob(glom(ctxt, n, r, nil));
+	if (r.r != nil)
+		ctxt.fail("bad $ arg", "sh: redirection not allowed in substitution");
+	r = nil;
+	if (cmd == nil || (hd cmd).word == nil || (hd cmd).cmd != nil)
+		ctxt.fail("bad $ arg", "sh: bad builtin name");
+
+	(nil, bmods) := findbuiltin(ctxt.env.sbuiltins, (hd cmd).word);
+	if (bmods == nil)
+		ctxt.fail("builtin not found",
+			sys->sprint("sh: builtin %s not found", (hd cmd).word));
+	return (hd bmods)->runsbuiltin(ctxt, myself, cmd);
+}
+
+
+getbq(nil: ref Context, fd: ref Sys->FD, seps: string): list of ref Listnode
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	buflen := 0;
+	while ((n := sys->read(fd, buf[buflen:], len buf - buflen)) > 0) {
+		buflen += n;
+		if (buflen == len buf) {
+			nbuf := array[buflen * 2] of byte;
+			nbuf[0:] = buf[0:];
+			buf = nbuf;
+		}
+	}
+	l: list of string;
+	if (seps != nil)
+		(nil, l) = sys->tokenize(string buf[0:buflen], seps);
+	else
+		l = string buf[0:buflen] :: nil;
+	buf = nil;
+	return stringlist2list(l);
+}
+
+bq(ctxt: ref Context, cmd: list of ref Listnode, seps: string): (list of ref Listnode, string)
+{
+	fds := array[2] of ref Sys->FD;
+	if (sys->pipe(fds) == -1)
+		ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
+
+	r := rdir(fds[1]);
+	fds[1] = nil;
+	startchan := chan of (int, ref Expropagate);
+	spawn runasync(ctxt, 0, cmd, r, startchan);
+	(exepid, exprop) := <-startchan;
+	r = nil;
+	bqlist := getbq(ctxt, fds[0], seps);
+	waitfor(ctxt, exepid :: nil);
+	if (exprop.name != nil)
+		raise exprop.name;
+	return (bqlist, nil);
+}
+
+rdir(fd: ref Sys->FD): ref Redirlist
+{
+	return  ref Redirlist(Redirword(fd, nil, Redir(Sys->OWRITE, 1, -1)) :: nil);
+}
+
+
+concatwords(p1, p2: ref Listnode): ref Listnode
+{
+	if (p1.word == nil && p1.cmd != nil)
+		p1.word = cmd2string(p1.cmd);
+	if (p2.word == nil && p2.cmd != nil)
+		p2.word = cmd2string(p2.cmd);
+	return ref Listnode(nil, p1.word + p2.word);
+}
+
+concat(ctxt: ref Context, nl1, nl2: list of ref Listnode): list of ref Listnode
+{
+	if (nl1 == nil || nl2 == nil) {
+		if (nl1 == nil && nl2 == nil)
+			return nil;
+		ctxt.fail("bad concatenation", "sh: null list in concatenation");
+	}
+
+	ret: list of ref Listnode;
+	if (tl nl1 == nil || tl nl2 == nil) {
+		for (p1 := nl1; p1 != nil; p1 = tl p1)
+			for (p2 := nl2; p2 != nil; p2 = tl p2)
+				ret = concatwords(hd p1, hd p2) :: ret;
+	} else {
+		if (len nl1 != len nl2)
+			ctxt.fail("bad concatenation", "sh: lists of differing sizes can't be concatenated");
+		while (nl1 != nil) {
+			ret = concatwords(hd nl1, hd nl2) :: ret;
+			(nl1, nl2) = (tl nl1, tl nl2);
+		}
+	}
+	return revlist(ret);
+}
+
+Expropagate: adt {
+	name: string;
+};
+
+runasync(ctxt: ref Context, copyenv: int, argv: list of ref Listnode, redirs: ref Redirlist,
+		startchan: chan of (int, ref Expropagate))
+{
+	status: string;
+
+	pid := sys->pctl(sys->FORKFD, nil);
+	if (DEBUG) debug(sprint("in async (len redirs: %d)", len redirs.r));
+	ctxt = ctxt.copy(copyenv);
+	exprop := ref Expropagate;
+	{
+		newfdl := doredirs(ctxt, redirs);
+		redirs = nil;
+		if (newfdl != nil)
+			sys->pctl(Sys->NEWFD, newfdl);
+		# stop the old waitfd from holding the intermediate
+		# file descriptor group open.
+		ctxt.waitfd = waitfd();
+		# N.B. it's important that the sync is done here, not
+		# before doredirs, as otherwise there's some sort of
+		# race condition that leads to pipe non-completion.
+		startchan <-= (pid, exprop);
+		startchan = nil;
+		status = ctxt.run(argv, copyenv);
+	} exception e {
+	"fail:*" =>
+		exprop.name = e;
+		if (startchan != nil)
+			startchan <-= (pid, exprop);
+		raise e;
+	}
+	if (status != nil) {
+		# don't propagate bad status as an exception.
+		raise "fail:" + status;
+	}
+}
+
+runsync(ctxt: ref Context, argv: list of ref Listnode,
+		redirs: ref Redirlist, last: int): string
+{
+	if (DEBUG) debug(sys->sprint("in sync (len redirs: %d; last: %d)", len redirs.r, last));
+	if (redirs.r != nil && !last) {
+		# a new process is required to shield redirection side effects
+		startchan := chan of (int, ref Expropagate);
+		spawn runasync(ctxt, 0, argv, redirs, startchan);
+		(pid, exprop) := <-startchan;
+		redirs = nil;
+		r := waitfor(ctxt, pid :: nil);
+		if (exprop.name != nil)
+			raise exprop.name;
+		return r;
+	} else {
+		newfdl := doredirs(ctxt, redirs);
+		redirs = nil;
+		if (newfdl != nil)
+			sys->pctl(Sys->NEWFD, newfdl);
+		return ctxt.run(argv, last);
+	}
+}
+
+absolute(p: string): int
+{
+	if (len p < 2)
+		return 0;
+	if (p[0] == '/' || p[0] == '#')
+		return 1;
+	if (len p < 3 || p[0] != '.')
+		return 0;
+	if (p[1] == '/')
+		return 1;
+	if (p[1] == '.' && p[2] == '/')
+		return 1;
+	return 0;
+}
+
+runexternal(ctxt: ref Context, args: list of ref Listnode, last: int): string
+{
+	progname := (hd args).word;
+	disfile := 0;
+	if (len progname >= 4 && progname[len progname-4:] == ".dis")
+		disfile = 1;
+	pathlist: list of string;
+	if (absolute(progname))
+		pathlist = list of {""};
+	else if ((pl := ctxt.get("path")) != nil)
+		pathlist = list2stringlist(pl);
+	else
+		pathlist = list of {"/dis", "."};
+
+	err := "";
+	do {
+		path: string;
+		if (hd pathlist != "")
+			path = hd pathlist + "/" + progname;
+		else
+			path = progname;
+
+		npath := path;
+		if (!disfile)
+			npath += ".dis";
+		mod := load Command npath;
+		if (mod != nil) {
+			argv := list2stringlist(args);
+			export(ctxt.env.localenv);
+
+			if (last) {
+				{
+					sys->pctl(Sys->NEWFD, ctxt.keepfds);
+					mod->init(ctxt.drawcontext, argv);
+					exit;
+				} exception e {
+				EPIPE =>
+					return EPIPE;
+				"fail:*" =>
+					return failurestatus(e);
+				}
+			}
+			extstart := chan of int;
+			spawn externalexec(mod, ctxt.drawcontext, argv, extstart, ctxt.keepfds);
+			pid := <-extstart;
+			if (DEBUG) debug("started external externalexec; pid is "+string pid);
+			return waitfor(ctxt, pid :: nil);
+		}
+		err = sys->sprint("%r");
+		if (nonexistent(err)) {
+			# try and run it as a shell script
+			if (!disfile && (fd := sys->open(path, Sys->OREAD)) != nil) {
+				(ok, info) := sys->fstat(fd);
+				# make permission checking more accurate later
+				if (ok == 0 && (info.mode & Sys->DMDIR) == 0
+						&& (info.mode & 8r111) != 0)
+					return runhashpling(ctxt, fd, path, tl args, last);
+			};
+			err = sys->sprint("%r");
+		}
+		pathlist = tl pathlist;
+	} while (pathlist != nil && nonexistent(err));
+	diagnostic(ctxt, sys->sprint("%s: %s", progname, err));
+	return err;
+}
+
+failurestatus(e: string): string
+{
+	s := e[5:];
+	while(s != nil && (s[0] == ' ' || s[0] == '\t'))
+		s = s[1:];
+	if(s != nil)
+		return s;
+	return "failed";
+}
+
+runhashpling(ctxt: ref Context, fd: ref Sys->FD,
+		path: string, argv: list of ref Listnode, last: int): string
+{
+	header := array[1024] of byte;
+	n := sys->read(fd, header, len header);
+	for (i := 0; i < n; i++)
+		if (header[i] == byte '\n')
+			break;
+	if (i == n || i < 3 || header[0] != byte('#') || header[1] != byte('!')) {
+		diagnostic(ctxt, "bad script header on " + path);
+		return "bad header";
+	}
+	(nil, args) := sys->tokenize(string header[2:i], " \t");
+	if (args == nil) {
+		diagnostic(ctxt, "empty header on " + path);
+		return "bad header";
+	}
+	header = nil;
+	fd = nil;
+	nargs: list of ref Listnode;
+	for (; args != nil; args = tl args)
+		nargs = ref Listnode(nil, hd args) :: nargs;
+	nargs = ref Listnode(nil, path) :: nargs;
+	for (; argv != nil; argv = tl argv)
+		nargs = hd argv :: nargs;
+	return runexternal(ctxt, revlist(nargs), last);
+}
+
+runblock(ctxt: ref Context, args: list of ref Listnode, last: int): string
+{
+	# block execute (we know that hd args represents a block)
+	cmd := (hd args).cmd;
+	if (cmd == nil) {
+		# parse block from first argument
+		lex := YYLEX.initstring((hd args).word);
+
+		err: string;
+		(cmd, err) = doparse(lex, "", 0);
+		if (cmd == nil)
+			ctxt.fail("parse error", "sh: "+err);
+
+		(hd args).cmd = cmd;
+	}
+	# now we've got a parsed block
+	ctxt.push();
+	{
+		ctxt.setlocal("0", hd args :: nil);
+		ctxt.setlocal("*", tl args);
+		if (cmd != nil && cmd.ntype == n_BLOCK)
+			cmd = cmd.left;
+		status := walk(ctxt, cmd, last);
+		ctxt.pop();
+		return status;
+	} exception {
+	"fail:*" =>
+		ctxt.pop();
+		raise;
+	}
+}
+
+trybuiltin(ctxt: ref Context, args: list of ref Listnode, lseq: int)
+		: (int, string)
+{
+	(nil, bmods) := findbuiltin(ctxt.env.builtins, (hd args).word);
+	if (bmods == nil)
+		return (0, nil);
+	return (1, (hd bmods)->runbuiltin(ctxt, myself, args, lseq));
+}
+
+keepfdstr(ctxt: ref Context): string
+{
+	s := "";
+	for (f := ctxt.keepfds; f != nil; f = tl f) {
+		s += string hd f;
+		if (tl f != nil)
+			s += ",";
+	}
+	return s;
+}
+
+externalexec(mod: Command,
+		drawcontext: ref Draw->Context, argv: list of string, startchan: chan of int, keepfds: list of int)
+{
+	if (DEBUG) debug(sprint("externalexec(%s,... [%d args])", hd argv, len argv));
+	sys->pctl(Sys->NEWFD, keepfds);
+	startchan <-= sys->pctl(0, nil);
+	{
+		mod->init(drawcontext, argv);
+	}
+	exception {
+	EPIPE =>
+		raise "fail:" + EPIPE;
+	}
+}
+
+dup(ctxt: ref Context, fd1, fd2: int): int
+{
+	# shuffle waitfd out of the way if it's being attacked
+	if (ctxt.waitfd.fd == fd2) {
+		ctxt.waitfd = waitfd();
+		if (ctxt.waitfd.fd == fd2)
+			panic(sys->sprint("reopen of waitfd gave same fd (%d)", ctxt.waitfd.fd));
+	}
+	return sys->dup(fd1, fd2);
+}
+
+doredirs(ctxt: ref Context, redirs: ref Redirlist): list of int
+{
+	if (redirs.r == nil)
+		return nil;
+	keepfds := ctxt.keepfds;
+	rl := redirs.r;
+	redirs = nil;
+	for (; rl != nil; rl = tl rl) {
+		(rfd, path, (mode, fd1, fd2)) := hd rl;
+		if (path == nil && rfd == nil) {
+			# dup
+			if (fd1 == -1 || fd2 == -1)
+				ctxt.fail("bad redir", "sh: invalid dup");
+
+			if (dup(ctxt, fd2, fd1) == -1)
+				ctxt.fail("bad redir", sys->sprint("sh: cannot dup: %r"));
+			keepfds = fd1 :: keepfds;
+			continue;
+		}
+		# redir
+		if (fd1 == -1) {
+			if ((mode & OMASK) == Sys->OWRITE)
+				fd1 = 1;
+			else
+				fd1 = 0;
+		}
+		if (rfd == nil) {
+			(append, omode) := (mode & OAPPEND, mode & ~OAPPEND);
+			err := "";
+			case mode {
+			Sys->OREAD =>
+				rfd = sys->open(path, omode);
+			Sys->OWRITE | OAPPEND or
+			Sys->ORDWR =>
+				rfd = sys->open(path, omode);
+				err = sprint("%r");
+				if (rfd == nil && nonexistent(err)) {
+					rfd = sys->create(path, omode, 8r666);
+					err = nil;
+				}
+			Sys->OWRITE =>
+				rfd = sys->create(path, omode, 8r666);
+				err = sprint("%r");
+				if (rfd == nil && err == EPERM) {
+					# try open; can't create on a file2chan (pipe)
+					rfd = sys->open(path, omode);
+					nerr := sprint("%r");
+					if(!nonexistent(nerr))
+						err = nerr;
+				}
+			}
+			if (rfd == nil) {
+				if (err == nil)
+					err = sprint("%r");
+				ctxt.fail("bad redir", sys->sprint("sh: cannot open %s: %s", path, err));
+			}
+			if (append)
+				sys->seek(rfd, big 0, Sys->SEEKEND);	# not good enough, but alright for some purposes.
+		}
+		# XXX what happens if rfd.fd == fd1?
+		# it probably gets closed automatically... which is not what we want!
+		dup(ctxt, rfd.fd, fd1);
+		keepfds = fd1 :: keepfds;
+	}
+	ctxt.keepfds = keepfds;
+	return ctxt.waitfd.fd :: keepfds;
+}
+
+
+waitfd(): ref Sys->FD
+{
+	wf := string sys->pctl(0, nil) + "/wait";
+	waitfd := sys->open("#p/"+wf, Sys->OREAD);
+	if (waitfd == nil)
+		waitfd = sys->open("/prog/"+wf, Sys->OREAD);
+	if (waitfd == nil)
+		panic(sys->sprint("cannot open wait file: %r"));
+	return waitfd;
+}
+
+waitfor(ctxt: ref Context, pids: list of int): string
+{
+	if (pids == nil)
+		return nil;
+	status := array[len pids] of string;
+	wcount := len status;
+	buf := array[Sys->WAITLEN] of byte;
+	onebad := 0;
+	for(;;){
+		n := sys->read(ctxt.waitfd, buf, len buf);
+		if(n < 0)
+			panic(sys->sprint("error on wait read: %r"));
+		(who, line, s) := parsewaitstatus(ctxt, string buf[0:n]);
+		if (s != nil) {
+			if (len s >= 5 && s[0:5] == "fail:")
+				s = failurestatus(s);
+			else
+				diagnostic(ctxt, line);
+		}
+		for ((i, pl) := (0, pids); pl != nil; (i, pl) = (i+1, tl pl))
+			if (who == hd pl)
+				break;
+		if (i < len status) {
+			# wait returns two records for a killed process...
+			if (status[i] == nil || s != "killed") {
+				onebad += s != nil;
+				status[i] = s;
+				if (wcount-- <= 1)
+					break;
+			}
+		}
+	}
+	if (!onebad)
+		return nil;
+	r := status[len status - 1];
+	for (i := len status - 2; i >= 0; i--)
+		r += "|" + status[i];
+	return r;
+}
+
+parsewaitstatus(ctxt: ref Context, status: string): (int, string, string)
+{
+	for (i := 0; i < len status; i++)
+		if (status[i] == ' ')
+			break;
+	if (i == len status - 1 || status[i+1] != '"')
+		ctxt.fail("bad wait read",
+			sys->sprint("sh: bad exit status '%s'", status));
+
+	for (i+=2; i < len status; i++)
+		if (status[i] == '"')
+			break;
+	if (i > len status - 2 || status[i+1] != ':')
+		ctxt.fail("bad wait read",
+			sys->sprint("sh: bad exit status '%s'", status));
+
+	return (int status, status, status[i+2:]);
+}
+
+panic(s: string)
+{
+	sys->fprint(stderr(), "sh panic: %s\n", s);
+	raise "panic";
+}
+
+diagnostic(ctxt: ref Context, s: string)
+{
+	if (ctxt.options() & Context.VERBOSE)
+		sys->fprint(stderr(), "sh: %s\n", s);
+}
+
+
+Context.new(drawcontext: ref Draw->Context): ref Context
+{
+	initialise();
+	if (env != nil)
+		env->clone();
+	ctxt := ref Context(
+		ref Environment(
+			ref Builtins(nil, 0),
+			ref Builtins(nil, 0),
+			nil,
+			newlocalenv(nil)
+		),
+		waitfd(),
+		drawcontext,
+		0 :: 1 :: 2 :: nil
+	);
+	myselfbuiltin->initbuiltin(ctxt, myself);
+	ctxt.env.localenv.flags = ctxt.VERBOSE;
+	for (vl := ctxt.get("autoload"); vl != nil; vl = tl vl)
+		if ((hd vl).cmd == nil && (hd vl).word != nil)
+			loadmodule(ctxt, (hd vl).word);
+	return ctxt;
+}
+
+Context.copy(ctxt: self ref Context, copyenv: int): ref Context
+{
+	# XXX could check to see that we are definitely in a
+	# new process, because there'll be problems if not (two processes
+	# simultaneously reading the same wait file)
+	nctxt := ref Context(ctxt.env, waitfd(), ctxt.drawcontext, ctxt.keepfds);
+			
+	if (copyenv) {
+		if (env != nil)
+			env->clone();
+		nctxt.env = ref Environment(
+			copybuiltins(ctxt.env.sbuiltins),
+			copybuiltins(ctxt.env.builtins),
+			ctxt.env.bmods,
+			copylocalenv(ctxt.env.localenv)
+		);
+	}
+	return nctxt;
+}
+
+Context.set(ctxt: self ref Context, name: string, val: list of ref Listnode)
+{
+	e := ctxt.env.localenv;
+	idx := hashfn(name, len e.vars);
+	for (;;) {
+		v := hashfind(e.vars, idx, name);
+		if (v == nil) {
+			if (e.pushed == nil) {
+				flags := Var.CHANGED;
+				if (noexport(name))
+					flags |= Var.NOEXPORT;
+				hashadd(e.vars, idx, ref Var(name, val, flags));
+				return;
+			}
+		} else {
+			v.val = val;
+			v.flags |= Var.CHANGED;
+			return;
+		}
+		e = e.pushed;
+	}
+}
+
+Context.get(ctxt: self ref Context, name: string): list of ref Listnode
+{
+	if (name == nil)
+		return nil;
+
+	idx := -1;
+	# cope with $1, $2, etc
+	if (name[0] > '0' && name[0] <= '9') {
+		i: int;
+		for (i = 0; i < len name; i++)
+			if (name[i] < '0' || name[i] > '9')
+				break;
+		if (i >= len name) {
+			idx = int name - 1;
+			name = "*";
+		}
+	}
+
+	v := varfind(ctxt.env.localenv, name);
+	if (v != nil) {
+		if (idx != -1)
+			return index(v.val, idx);
+		return v.val;
+	}
+	return nil;
+}
+
+Context.envlist(ctxt: self ref Context): list of (string, list of ref Listnode)
+{
+	t := array[ENVHASHSIZE] of list of ref Var;
+	for (e := ctxt.env.localenv; e != nil; e = e.pushed) {
+		for (i := 0; i < len e.vars; i++) {
+			for (vl := e.vars[i]; vl != nil; vl = tl vl) {
+				v := hd vl;
+				idx := hashfn(v.name, len e.vars);
+				if (hashfind(t, idx, v.name) == nil)
+					hashadd(t, idx, v);
+			}
+		}
+	}
+
+	l: list of (string, list of ref Listnode);
+	for (i := 0; i < ENVHASHSIZE; i++) {
+		for (vl := t[i]; vl != nil; vl = tl vl) {
+			v := hd vl;
+			l = (v.name, v.val) :: l;
+		}
+	}
+	return l;
+}
+
+Context.setlocal(ctxt: self ref Context, name: string, val: list of ref Listnode)
+{
+	e := ctxt.env.localenv;
+	idx := hashfn(name, len e.vars);
+	v := hashfind(e.vars, idx, name);
+	if (v == nil) {
+		flags := Var.CHANGED;
+		if (noexport(name))
+			flags |= Var.NOEXPORT;
+		hashadd(e.vars, idx, ref Var(name, val, flags));
+	} else {
+		v.val = val;
+		v.flags |= Var.CHANGED;
+	}
+}
+
+
+Context.push(ctxt: self ref Context)
+{
+	ctxt.env.localenv = newlocalenv(ctxt.env.localenv);
+}
+
+Context.pop(ctxt: self ref Context)
+{
+	if (ctxt.env.localenv.pushed == nil)
+		panic("unbalanced contexts in shell environment");
+	else {
+		oldv := ctxt.env.localenv.vars;
+		ctxt.env.localenv = ctxt.env.localenv.pushed;
+		for (i := 0; i < len oldv; i++) {
+			for (vl := oldv[i]; vl != nil; vl = tl vl) {
+				if ((v := varfind(ctxt.env.localenv, (hd vl).name)) != nil)
+					v.flags |= Var.CHANGED;
+				else
+					ctxt.set((hd vl).name, nil);
+			}
+		}
+	}
+}
+
+Context.run(ctxt: self ref Context, args: list of ref Listnode, last: int): string
+{
+	if (args == nil || ((hd args).cmd == nil && (hd args).word == nil))
+		return nil;
+	cmd := hd args;
+	if (cmd.cmd != nil || cmd.word[0] == '{')	# }
+		return runblock(ctxt, args, last);
+
+	if (ctxt.options() & ctxt.EXECPRINT)
+		sys->fprint(stderr(), "%s\n", quoted(args, 0));
+	(doneit, status) := trybuiltin(ctxt, args, last);
+	if (!doneit)
+		status = runexternal(ctxt, args, last);
+
+	return status;
+}
+
+Context.addmodule(ctxt: self ref Context, name: string, mod: Shellbuiltin)
+{
+	mod->initbuiltin(ctxt, myself);
+	ctxt.env.bmods = (name, mod->getself()) :: ctxt.env.bmods;
+}
+
+Context.addbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
+{
+	addbuiltin(c.env.builtins, name, mod);
+}
+
+Context.removebuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
+{
+	removebuiltin(c.env.builtins, name, mod);
+}
+
+Context.addsbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
+{
+	addbuiltin(c.env.sbuiltins, name, mod);
+}
+
+Context.removesbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
+{
+	removebuiltin(c.env.sbuiltins, name, mod);
+}
+
+varfind(e: ref Localenv, name: string): ref Var
+{
+	idx := hashfn(name, len e.vars);
+	for (; e != nil; e = e.pushed)
+		for (vl := e.vars[idx]; vl != nil; vl = tl vl)
+			if ((hd vl).name == name)
+				return hd vl;
+	return nil;
+}
+
+Context.fail(ctxt: self ref Context, ename: string, err: string)
+{
+	if (ctxt.options() & Context.VERBOSE)
+		sys->fprint(stderr(), "%s\n", err);
+	raise "fail:" + ename;
+}
+
+Context.setoptions(ctxt: self ref Context, flags, on: int): int
+{
+	old := ctxt.env.localenv.flags;
+	if (on)
+		ctxt.env.localenv.flags |= flags;
+	else
+		ctxt.env.localenv.flags &= ~flags;
+	return old;
+}
+
+Context.options(ctxt: self ref Context): int
+{
+	return ctxt.env.localenv.flags;
+}
+
+hashfn(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
+
+hashfind(ht: array of list of ref Var, idx: int, n: string): ref Var
+{
+	for (ent := ht[idx]; ent != nil; ent = tl ent)
+		if ((hd ent).name == n)
+			return hd ent;
+	return nil;
+}
+
+hashadd(ht: array of list of ref Var, idx: int, v: ref Var)
+{
+	ht[idx] = v :: ht[idx];
+}
+
+copylocalenv(e: ref Localenv): ref Localenv
+{
+	nvars := array[len e.vars] of list of ref Var;
+	flags := e.flags;
+	for (; e != nil; e = e.pushed)
+		for (i := 0; i < len nvars; i++)
+			for (vl := e.vars[i]; vl != nil; vl = tl vl) {
+				idx := hashfn((hd vl).name, len nvars);
+				if (hashfind(nvars, idx, (hd vl).name) == nil)
+					hashadd(nvars, idx, ref *(hd vl));
+			}
+	return ref Localenv(nvars, nil, flags);
+}
+
+newlocalenv(pushed: ref Localenv): ref Localenv
+{
+	e := ref Localenv(array[ENVHASHSIZE] of list of ref Var, pushed, 0);
+	if (pushed == nil && env != nil) {
+		for (vl := env->getall(); vl != nil; vl = tl vl) {
+			(name, val) := hd vl;
+			hashadd(e.vars, hashfn(name, len e.vars), ref Var(name, envstringtoval(val), 0));
+		}
+	}
+	if (pushed != nil)
+		e.flags = pushed.flags;
+	return e;
+}
+
+copybuiltins(b: ref Builtins): ref Builtins
+{
+	nb := ref Builtins(array[b.n] of (string, list of Shellbuiltin), b.n);
+	nb.ba[0:] = b.ba[0:b.n];
+	return nb;
+}
+
+findbuiltin(b: ref Builtins, name: string): (int, list of Shellbuiltin)
+{
+	lo := 0;
+	hi := b.n - 1;
+	while (lo <= hi) {
+		mid := (lo + hi) / 2;
+		(bname, bmod) := b.ba[mid];
+		if (name < bname)
+			hi = mid - 1;
+		else if (name > bname)
+			lo = mid + 1;
+		else
+			return (mid, bmod);
+	}
+	return (lo, nil);
+}
+
+removebuiltin(b: ref Builtins, name: string, mod: Shellbuiltin)
+{
+	(n, bmods) := findbuiltin(b, name);
+	if (bmods == nil)
+		return;
+	if (hd bmods == mod) {
+		if (tl bmods != nil)
+			b.ba[n] = (name, tl bmods);
+		else {
+			b.ba[n:] = b.ba[n+1:b.n];
+			b.ba[--b.n] = (nil, nil);
+		}
+	}
+}
+
+addbuiltin(b: ref Builtins, name: string, mod: Shellbuiltin)
+{
+	if (mod == nil || (name == "builtin" && mod != myselfbuiltin))
+		return;
+	(n, bmods) := findbuiltin(b, name);
+	if (bmods != nil) {
+		if (hd bmods == myselfbuiltin)
+			b.ba[n] = (name, mod :: bmods);
+		else
+			b.ba[n] = (name, mod :: nil);
+	} else {
+		if (b.n == len b.ba) {
+			nb := array[b.n + 10] of (string, list of Shellbuiltin);
+			nb[0:] = b.ba[0:b.n];
+			b.ba = nb;
+		}
+		b.ba[n+1:] = b.ba[n:b.n];
+		b.ba[n] = (name, mod :: nil);
+		b.n++;
+	}
+}
+
+removebuiltinmod(b: ref Builtins, mod: Shellbuiltin)
+{
+	j := 0;
+	for (i := 0; i < b.n; i++) {
+		(name, bmods) := b.ba[i];
+		if (hd bmods == mod)
+			bmods = tl bmods;
+		if (bmods != nil)
+			b.ba[j++] = (name, bmods);
+	}
+	b.n = j;
+	for (; j < i; j++)
+		b.ba[j] = (nil, nil);
+}
+
+export(e: ref Localenv)
+{
+	if (env == nil)
+		return;
+	if (e.pushed != nil)
+		export(e.pushed);
+
+	for (i := 0; i < len e.vars; i++) {
+		for (vl := e.vars[i]; vl != nil; vl = tl vl) {
+			v := hd vl;
+			# a bit inefficient: a local variable will get several putenvs.
+			if ((v.flags & Var.CHANGED) && !(v.flags & Var.NOEXPORT)) {
+				setenv(v.name, v.val);
+				v.flags &= ~Var.CHANGED;
+			}
+		}
+	}
+}
+
+noexport(name: string): int
+{
+	case name {
+		"0" or "*" or "status" => return 1;
+	}
+	return 0;
+}
+
+index(val: list of ref Listnode, k: int): list of ref Listnode
+{
+	for (; k > 0 && val != nil; k--)
+		val = tl val;
+	if (val != nil)
+		val = hd val :: nil;
+	return val;
+}
+
+getenv(name: string): list of ref Listnode
+{
+	if (env == nil)
+		return nil;
+	return envstringtoval(env->getenv(name));
+}
+
+envstringtoval(v: string): list of ref Listnode
+{
+	return stringlist2list(str->unquoted(v));
+}
+
+XXXenvstringtoval(v: string): list of ref Listnode
+{
+	if (len v == 0)
+		return nil;
+	start := len v;
+	val: list of ref Listnode;
+	for (i := start - 1; i >= 0; i--) {
+		if (v[i] == ENVSEP) {
+			val = ref Listnode(nil, v[i+1:start]) :: val;
+			start = i;
+		}
+	}
+	return ref Listnode(nil, v[0:start]) :: val;
+}
+
+setenv(name: string, val: list of ref Listnode)
+{
+	if (env == nil)
+		return;
+	env->setenv(name, quoted(val, 1));
+}
+
+
+containswildchar(s: string): int
+{
+	# try and avoid being fooled by GLOB characters in quoted
+	# text. we'll only be fooled if the GLOB char is followed
+	# by a wildcard char, or another GLOB.
+	for (i := 0; i < len s; i++) {
+		if (s[i] == GLOB && i < len s - 1) {
+			case s[i+1] {
+			'*' or '[' or '?' or GLOB =>
+				return 1;
+			}
+		}
+	}
+	return 0;
+}
+
+patquote(word: string): string
+{
+	outword := "";
+	for (i := 0; i < len word; i++) {
+		case word[i] {
+		'[' or '*' or '?' or '\\' =>
+			outword[len outword] = '\\';
+		GLOB =>
+			i++;
+			if (i >= len word)
+				return outword;
+			if(word[i] == '[' && i < len word - 1 && word[i+1] == '~')
+				word[i+1] = '^';
+		}
+		outword[len outword] = word[i];
+	}
+	return outword;
+}
+
+deglob(s: string): string
+{
+	j := 0;
+	for (i := 0; i < len s; i++) {
+		if (s[i] != GLOB) {
+			if (i != j)		# a worthy optimisation???
+				s[j] = s[i];
+			j++;
+		}
+	}
+	if (i == j)
+		return s;
+	return s[0:j];
+}
+
+glob(nl: list of ref Listnode): list of ref Listnode
+{
+	new: list of ref Listnode;
+	while (nl != nil) {
+		n := hd nl;
+		if (containswildchar(n.word)) {
+			qword := patquote(n.word);
+			files := filepat->expand(qword);
+			if (files == nil)
+				files = deglob(n.word) :: nil;
+			while (files != nil) {
+				new = ref Listnode(nil, hd files) :: new;
+				files = tl files;
+			}
+		} else
+			new = n :: new;
+		nl = tl nl;
+	}
+	ret := revlist(new);
+	return ret;
+}
+
+
+list2stringlist(nl: list of ref Listnode): list of string
+{
+	ret: list of string = nil;
+
+	while (nl != nil) {
+		newel: string;
+		el := hd nl;
+		if (el.word != nil || el.cmd == nil)
+			newel = el.word;
+		else
+			el.word = newel = cmd2string(el.cmd);
+		ret = newel::ret;
+		nl = tl nl;
+	}
+
+	sl := revstringlist(ret);
+	return sl;
+}
+
+stringlist2list(sl: list of string): list of ref Listnode
+{
+	ret: list of ref Listnode;
+
+	while (sl != nil) {
+		ret = ref Listnode(nil, hd sl) :: ret;
+		sl = tl sl;
+	}
+	return revlist(ret);
+}
+
+revstringlist(l: list of string): list of string
+{
+	t: list of string;
+
+	while(l != nil) {
+		t = hd l :: t;
+		l = tl l;
+	}
+	return t;
+}
+
+revlist(l: list of ref Listnode): list of ref Listnode
+{
+	t: list of ref Listnode;
+
+	while(l != nil) {
+		t = hd l :: t;
+		l = tl l;
+	}
+	return t;
+}
+
+
+fdassignstr(isassign: int, redir: ref Redir): string
+{
+	l: string = nil;
+	if (redir.fd1 >= 0)
+		l = string redir.fd1;
+	
+	if (isassign) {
+		r: string = nil;
+		if (redir.fd2 >= 0)
+			r = string redir.fd2;
+		return "[" + l + "=" + r + "]";
+	}
+	return "[" + l + "]";
+}
+
+redirstr(rtype: int): string
+{
+	case rtype {
+	* or
+	Sys->OREAD =>	return "<";
+	Sys->OWRITE =>	return ">";
+	Sys->OWRITE|OAPPEND =>	return ">>";
+	Sys->ORDWR =>	return "<>";
+	}
+}
+
+cmd2string(n: ref Node): string
+{
+	if (n == nil)
+		return "";
+
+	s: string;
+	case n.ntype {
+	n_BLOCK =>	s = "{" + cmd2string(n.left) + "}";
+	n_VAR =>		s = "$" + cmd2string(n.left);
+				# XXX can this ever occur?
+				if (n.right != nil)
+					s += "(" + cmd2string(n.right) + ")";
+	n_SQUASH =>	s = "$\"" + cmd2string(n.left);
+	n_COUNT =>	s = "$#" + cmd2string(n.left);
+	n_BQ =>		s = "`" + cmd2string(n.left);
+	n_BQ2 =>		s = "\"" + cmd2string(n.left);
+	n_REDIR =>	s = redirstr(n.redir.rtype);
+				if (n.redir.fd1 != -1)
+					s += fdassignstr(0, n.redir);
+				s += cmd2string(n.left);
+	n_DUP =>		s = redirstr(n.redir.rtype) + fdassignstr(1, n.redir);
+	n_LIST =>		s = "(" + cmd2string(n.left) + ")";
+	n_SEQ =>		s = cmd2string(n.left) + ";" + cmd2string(n.right);
+	n_NOWAIT =>	s = cmd2string(n.left) + "&";
+	n_CONCAT =>	s = cmd2string(n.left) + "^" + cmd2string(n.right);
+	n_PIPE =>		s = cmd2string(n.left) + "|";
+				if (n.redir != nil && (n.redir.fd1 != -1 || n.redir.fd2 != -1))
+					s += fdassignstr(n.redir.fd2 != -1, n.redir);
+				s += cmd2string(n.right);
+	n_ASSIGN =>	s = cmd2string(n.left) + "=" + cmd2string(n.right);
+	n_LOCAL =>	s = cmd2string(n.left) + ":=" + cmd2string(n.right);
+	n_ADJ =>		s = cmd2string(n.left) + " " + cmd2string(n.right);
+	n_WORD =>	s = quote(n.word, 1);
+	* =>			s = sys->sprint("unknown%d", n.ntype);
+	}
+	return s;
+}
+
+quote(s: string, glob: int): string
+{
+	needquote := 0;
+	t := "";
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'{' or '}' or '(' or ')' or '`' or '&' or ';' or '=' or '>' or '<' or '#' or
+		'|' or '*' or '[' or '?' or '$' or '^' or ' ' or '\t' or '\n' or '\r' =>
+			needquote = 1;
+		'\'' =>
+			t[len t] = '\'';
+			needquote = 1;
+		GLOB =>
+			if (glob) {
+				if (i < len s - 1)
+					i++;
+			}
+		}
+		t[len t] = s[i];
+	}
+	if (needquote || t == nil)
+		t = "'" + t + "'";
+	return t;
+}
+
+squash(l: list of string, sep: string): string
+{
+	if (l == nil)
+		return nil;
+	s := hd l;
+	for (l = tl l; l != nil; l = tl l)
+		s += sep + hd l;
+	return s;
+}
+
+debug(s: string)
+{
+	if (DEBUG) sys->fprint(stderr(), "%s\n", string sys->pctl(0, nil) + ": " + s);
+}
+
+
+initbuiltin(c: ref Context, nil: Sh): string
+{
+	names := array[] of {"load", "unload", "loaded", "builtin", "syncenv", "whatis", "run", "exit", "@"};
+	for (i := 0; i < len names; i++)
+		c.addbuiltin(names[i], myselfbuiltin);
+	c.addsbuiltin("loaded", myselfbuiltin);
+	c.addsbuiltin("quote", myselfbuiltin);
+	c.addsbuiltin("bquote", myselfbuiltin);
+	c.addsbuiltin("unquote", myselfbuiltin);
+	c.addsbuiltin("builtin", myselfbuiltin);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+runsbuiltin(ctxt: ref Context, nil: Sh, argv: list of ref Listnode): list of ref Listnode
+{
+	case (hd argv).word {
+	"loaded" =>	return sbuiltin_loaded(ctxt, argv);
+	"bquote" =>	return sbuiltin_quote(ctxt, argv, 0);
+	"quote" =>	return sbuiltin_quote(ctxt, argv, 1);
+	"unquote" =>	return sbuiltin_unquote(ctxt, argv);
+	"builtin" =>	return sbuiltin_builtin(ctxt, argv);
+	}
+	return nil;
+}
+
+runbuiltin(ctxt: ref Context, nil: Sh, args: list of ref Listnode, lseq: int): string
+{
+	status := "";
+	name := (hd args).word;
+	case name {
+	"load" =>		status = builtin_load(ctxt, args, lseq);
+	"loaded" =>	status = builtin_loaded(ctxt, args, lseq);
+	"unload" =>	status = builtin_unload(ctxt, args, lseq);
+	"builtin" =>	status = builtin_builtin(ctxt, args, lseq);
+	"whatis" =>	status = builtin_whatis(ctxt, args, lseq);
+	"run" =>		status = builtin_run(ctxt, args, lseq);
+	"exit" =>		status = builtin_exit(ctxt, args, lseq);
+	"syncenv" =>	export(ctxt.env.localenv);
+	"@" =>		status = builtin_subsh(ctxt, args, lseq);
+	}
+	return status;
+}
+
+sbuiltin_loaded(ctxt: ref Context, nil: list of ref Listnode): list of ref Listnode
+{
+	v: list of ref Listnode;
+	for (bl := ctxt.env.bmods; bl != nil; bl = tl bl) {
+		(name, nil) := hd bl;
+		v = ref Listnode(nil, name) :: v;
+	}
+	return v;
+}
+
+sbuiltin_quote(nil: ref Context, argv: list of ref Listnode, quoteblocks: int): list of ref Listnode
+{
+	return ref Listnode(nil, quoted(tl argv, quoteblocks)) :: nil;
+}
+
+sbuiltin_builtin(ctxt: ref Context, args: list of ref Listnode): list of ref Listnode
+{
+	if (args == nil || tl args == nil)
+		builtinusage(ctxt, "builtin command [args ...]");
+	name := (hd tl args).word;
+	(nil, mods) := findbuiltin(ctxt.env.sbuiltins, name);
+	for (; mods != nil; mods = tl mods)
+		if (hd mods == myselfbuiltin)
+			return (hd mods)->runsbuiltin(ctxt, myself, tl args);
+	ctxt.fail("builtin not found", sys->sprint("sh: builtin %s not found", name));
+	return nil;
+}
+
+sbuiltin_unquote(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	argv = tl argv;
+	if (argv == nil || tl argv != nil)
+		builtinusage(ctxt, "unquote arg");
+	
+	arg := (hd argv).word;
+	if (arg == nil && (hd argv).cmd != nil)
+		arg = cmd2string((hd argv).cmd);
+	return stringlist2list(str->unquoted(arg));
+}
+
+getself(): Shellbuiltin
+{
+	return myselfbuiltin;
+}
+
+builtinusage(ctxt: ref Context, s: string)
+{
+	ctxt.fail("usage", "sh: usage: " + s);
+}
+
+builtin_exit(nil: ref Context, nil: list of ref Listnode, nil: int): string
+{
+	# XXX using this primitive can cause
+	# environment stack not to be popped properly.
+	exit;
+}
+
+builtin_subsh(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args == nil)
+		return nil;
+	startchan := chan of (int, ref Expropagate);
+	spawn runasync(ctxt, 0, tl args, ref Redirlist, startchan);
+	(exepid, exprop) := <-startchan;
+	status := waitfor(ctxt, exepid :: nil);
+	if (exprop.name != nil)
+		raise exprop.name;
+	return status;
+}
+
+builtin_loaded(ctxt: ref Context, nil: list of ref Listnode, nil: int): string
+{
+	b := ctxt.env.builtins;
+	for (i := 0; i < b.n; i++) {
+		(name, bmods) := b.ba[i];
+		sys->print("%s\t%s\n", name, modname(ctxt, hd bmods));
+	}
+	b = ctxt.env.sbuiltins;
+	for (i = 0; i < b.n; i++) {
+		(name, bmods) := b.ba[i];
+		sys->print("${%s}\t%s\n", name, modname(ctxt, hd bmods));
+	}
+	return nil;
+}
+
+builtin_load(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args == nil || (hd tl args).word == nil)
+		builtinusage(ctxt, "load path...");
+	args = tl args;
+	if (args == nil)
+		builtinusage(ctxt, "load path...");
+	for (; args != nil; args = tl args) {
+		s := loadmodule(ctxt, (hd args).word);
+		if (s != nil)
+			raise "fail:" + s;
+	}
+	return nil;
+}
+
+builtin_unload(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args == nil)
+		builtinusage(ctxt, "unload path...");
+	status := "";
+	for (args = tl args; args != nil; args = tl args)
+		if ((s := unloadmodule(ctxt, (hd args).word)) != nil)
+			status = s;
+	return status;
+}
+
+builtin_run(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args == nil || (hd tl args).word == nil)
+		builtinusage(ctxt, "run path");
+	ctxt.push();
+	{
+		ctxt.setoptions(ctxt.INTERACTIVE, 0);
+		runscript(ctxt, (hd tl args).word, tl tl args, 1);
+		ctxt.pop();
+		return nil;
+	} exception e {
+	"fail:*" =>
+		ctxt.pop();
+		return failurestatus(e);
+	}
+}
+
+builtin_whatis(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (len args < 2)
+		builtinusage(ctxt, "whatis name ...");
+	err := "";
+	for (args = tl args; args != nil; args = tl args)
+		if ((e := whatisit(ctxt, hd args)) != nil)
+			err = e;
+	return err;
+}
+
+whatisit(ctxt: ref Context, el: ref Listnode): string
+{
+	if (el.cmd != nil) {
+		sys->print("%s\n", cmd2string(el.cmd));
+		return nil;
+	}
+	found := 0;
+	name := el.word;
+	if (name != nil && name[0] == '{') {	#}
+		sys->print("%s\n", name);
+		return nil;;
+	}
+	if (name == nil)
+		return nil;		# XXX questionable
+	w: string;
+	val := ctxt.get(name);
+	if (val != nil) {
+		found++;
+		w += sys->sprint("%s=%s\n", quote(name, 0), quoted(val, 0));
+	}
+	(nil, mods) := findbuiltin(ctxt.env.sbuiltins, name);
+	if (mods != nil) {
+		mod := hd mods;
+		if (mod == myselfbuiltin)
+			w += "${builtin " + name + "}\n";
+		else {
+			mw := mod->whatis(ctxt, myself, name, Shellbuiltin->SBUILTIN);
+			if (mw == nil)
+				mw = "${" + name + "}";
+			w += "load " + modname(ctxt, mod) + "; " + mw + "\n";
+		}
+		found++;
+	}
+	(nil, mods) = findbuiltin(ctxt.env.builtins, name);
+	if (mods != nil) {
+		mod := hd mods;
+		if (mod == myselfbuiltin)
+			sys->print("builtin %s\n", name);
+		else {
+			mw := mod->whatis(ctxt, myself, name, Shellbuiltin->BUILTIN);
+			if (mw == nil)
+				mw = name;
+			w += "load " + modname(ctxt, mod) + "; " + mw + "\n";
+		}
+		found++;
+	} else {
+		disfile := 0;	
+		if (len name >= 4 && name[len name-4:] == ".dis")
+			disfile = 1;
+		pathlist: list of string;
+		if (len name >= 2 && (name[0] == '/' || name[0:2] == "./"))
+			pathlist = list of {""};
+		else if ((pl := ctxt.get("path")) != nil)
+			pathlist = list2stringlist(pl);
+		else
+			pathlist = list of {"/dis", "."};
+	
+		foundpath := "";
+		while (pathlist != nil) {
+			path: string;
+			if (hd pathlist != "")
+				path = hd pathlist + "/" + name;
+			else
+				path = name;
+			if (!disfile && (fd := sys->open(path, Sys->OREAD)) != nil) {
+				if (executable(sys->fstat(fd), 8r111)) {
+					foundpath = path;
+					break;
+				}
+			}
+			if (!disfile)
+				path += ".dis";
+			if (executable(sys->stat(path), 8r444)) {
+				foundpath = path;
+				break;
+			}
+			pathlist = tl pathlist;
+		}
+		if (foundpath != nil)
+			w += foundpath + "\n";
+	}
+	for (bmods := ctxt.env.bmods; bmods != nil; bmods = tl bmods) {
+		(modname, mod) := hd bmods;
+		if ((mw := mod->whatis(ctxt, myself, name, Shellbuiltin->OTHER)) != nil)
+			w += "load " + modname + "; " + mw + "\n";
+	}
+	if (w == nil) {
+		sys->fprint(stderr(), "%s: not found\n", name);
+		return "not found";
+	}
+	sys->print("%s", w);
+	return nil;
+}
+
+builtin_builtin(ctxt: ref Context, args: list of ref Listnode, last: int): string
+{
+	if (len args < 2)
+		builtinusage(ctxt, "builtin command [args ...]");
+	name := (hd tl args).word;
+	if (name == nil || name[0] == '{') {
+		diagnostic(ctxt, name + " not found");
+		return "not found";
+	}
+	(nil, mods) := findbuiltin(ctxt.env.builtins, name);
+	for (; mods != nil; mods = tl mods)
+		if (hd mods == myselfbuiltin)
+			return (hd mods)->runbuiltin(ctxt, myself, tl args, last);
+	if (ctxt.options() & ctxt.EXECPRINT)
+		sys->fprint(stderr(), "%s\n", quoted(tl args, 0));
+	return runexternal(ctxt, tl args, last);
+}
+
+modname(ctxt: ref Context, mod: Shellbuiltin): string
+{
+	for (ml := ctxt.env.bmods; ml != nil; ml = tl ml) {
+		(bname, bmod) := hd ml;
+		if (bmod == mod)
+			return bname;
+	}
+	return "builtin";
+}
+
+loadmodule(ctxt: ref Context, name: string): string
+{
+	# avoid loading the same module twice (it's convenient
+	# to have load be a null-op if the module required is already loaded)
+	for (bl := ctxt.env.bmods; bl != nil; bl = tl bl) {
+		(bname, nil) := hd bl;
+		if (bname == name)
+			return nil;
+	}
+	path := name;
+	if (len path < 4 || path[len path-4:] != ".dis")
+		path += ".dis";
+	if (path[0] != '/' && path[0:2] != "./")
+		path = BUILTINPATH + "/" + path;
+	mod := load Shellbuiltin path;
+	if (mod == nil) {
+		diagnostic(ctxt, sys->sprint("load: cannot load %s: %r", path));
+		return "bad module";
+	}
+	s := mod->initbuiltin(ctxt, myself);
+	ctxt.env.bmods = (name, mod->getself()) :: ctxt.env.bmods;
+	if (s != nil) {
+		unloadmodule(ctxt, name);
+		diagnostic(ctxt, "load: module init failed: " + s);
+	}
+	return s;
+}
+
+unloadmodule(ctxt: ref Context, name: string): string
+{
+	bl: list of (string, Shellbuiltin);
+	mod: Shellbuiltin;
+	for (cl := ctxt.env.bmods; cl != nil; cl = tl cl) {
+		(bname, bmod) := hd cl;
+		if (bname == name)
+			mod = bmod;
+		else
+			bl = hd cl :: bl;
+	}
+	if (mod == nil) {
+		diagnostic(ctxt, sys->sprint("module %s not found", name));
+		return "not found";
+	}
+	for (ctxt.env.bmods = nil; bl != nil; bl = tl bl)
+		ctxt.env.bmods = hd bl :: ctxt.env.bmods;
+	removebuiltinmod(ctxt.env.builtins, mod);
+	removebuiltinmod(ctxt.env.sbuiltins, mod);
+	return nil;
+}
+
+executable(s: (int, Sys->Dir), mode: int): int
+{
+	(ok, info) := s;
+	return ok != -1 && (info.mode & Sys->DMDIR) == 0
+			&& (info.mode & mode) != 0;
+}
+
+quoted(val: list of ref Listnode, quoteblocks: int): string
+{
+	s := "";
+	for (; val != nil; val = tl val) {
+		el := hd val;
+		if (el.cmd == nil || (quoteblocks && el.word != nil))
+			s += quote(el.word, 0);
+		else {
+			cmd := cmd2string(el.cmd);
+			if (quoteblocks)
+				cmd = quote(cmd, 0);
+			s += cmd;
+		}
+		if (tl val != nil)
+			s[len s] = ' ';
+	}
+	return s;
+}
+
+setstatus(ctxt: ref Context, val: string): string
+{
+	ctxt.setlocal("status", ref Listnode(nil, val) :: nil);
+	return val;
+}
+
+
+doparse(l: ref YYLEX, prompt: string, showline: int): (ref Node, string)
+{
+	l.prompt = prompt;
+	l.err = nil;
+	l.lval.node = nil;
+	yyparse(l);
+	l.lastnl = 0;		# don't print secondary prompt next time
+	if (l.err != nil) {
+		s: string;
+		if (l.err == nil)
+			l.err = "unknown error";
+		if (l.errline > 0 && showline)
+			s = sys->sprint("%s:%d: %s", l.path, l.errline, l.err);
+		else
+			s = l.path + ": parse error: " + l.err;
+		return (nil, s);
+	}
+	return (l.lval.node, nil);
+}
+
+blanklex: YYLEX;	# for hassle free zero initialisation
+
+YYLEX.initstring(s: string): ref YYLEX
+{
+	ret := ref blanklex;
+	ret.s = s;
+	ret.path="internal";
+	ret.strpos = 0;
+	return ret;
+}
+
+YYLEX.initfile(fd: ref Sys->FD, path: string): ref YYLEX
+{
+	lex := ref blanklex;
+	lex.f = bufio->fopen(fd, bufio->OREAD);
+	lex.path = path;
+	lex.cbuf = array[2] of int;		# number of characters of pushback
+	lex.linenum = 1;
+	lex.prompt = "";
+	return lex;
+}
+
+YYLEX.error(l: self ref YYLEX, s: string)
+{
+	if (l.err == nil) {
+		l.err = s;
+		l.errline = l.linenum;
+	}
+}
+
+NOTOKEN: con -1;
+
+YYLEX.lex(l: self ref YYLEX): int
+{
+	# the following are allowed a free caret:
+	# $, word and quoted word;
+	# also, allowed chrs in unquoted word following dollar are [a-zA-Z0-9*_]
+	endword := 0;
+	wasdollar := 0;
+	tok := NOTOKEN;
+	while (tok == NOTOKEN) {
+		case c := l.getc() {
+		l.EOF =>
+			tok = END;
+		'\n' =>
+			tok = '\n';
+		'\r' or '\t' or ' ' =>
+			;
+		'#' =>
+			while ((c = l.getc()) != '\n' && c != l.EOF)
+				;
+			l.ungetc();
+		';' =>	tok = ';';
+		'&' =>
+			c = l.getc();
+			if(c == '&')
+				tok = ANDAND;
+			else{
+				l.ungetc();
+				tok = '&';
+			}
+		'^' =>	tok = '^';
+		'{' =>	tok = '{';
+		'}' =>	tok = '}';
+		')' =>	tok = ')';
+		'(' => tok = '(';
+		'=' => (tok, l.lval.optype) = ('=', n_ASSIGN);
+		'$' =>
+			if (l.atendword) {
+				l.ungetc();
+				tok = '^';
+				break;
+			}
+			case (c = l.getc()) {
+			'#' =>
+				l.lval.optype = n_COUNT;
+			'"' =>
+				l.lval.optype = n_SQUASH;
+			* =>
+				l.ungetc();
+				l.lval.optype = n_VAR;
+			}
+			tok = OP;
+			wasdollar = 1;
+		'"' or '`'=>
+			if (l.atendword) {
+				tok = '^';
+				l.ungetc();
+				break;
+			}
+			tok = OP;
+			if (c == '"')
+				l.lval.optype = n_BQ2;
+			else
+				l.lval.optype = n_BQ;
+		'>' or '<' =>
+			rtype: int;
+			nc := l.getc();
+			if (nc == '>') {
+				if (c == '>')
+					rtype = Sys->OWRITE | OAPPEND;
+				else
+					rtype = Sys->ORDWR;
+				nc = l.getc();
+			} else if (c == '>')
+				rtype = Sys->OWRITE;
+			else
+				rtype = Sys->OREAD;
+			tok = REDIR;
+			if (nc == '[') {
+				(tok, l.lval.redir) = readfdassign(l);
+				if (tok == ERROR)
+					(l.err, l.errline) = ("syntax error in redirection", l.linenum);
+			} else {
+				l.ungetc();
+				l.lval.redir = ref Redir(-1, -1, -1);
+			}
+			if (l.lval.redir != nil)
+				l.lval.redir.rtype = rtype;
+		'|' =>
+			tok = '|';
+			l.lval.redir = nil;
+			if ((c = l.getc()) == '[') {
+				(tok, l.lval.redir) = readfdassign(l);
+				if (tok == ERROR) {
+					(l.err, l.errline) = ("syntax error in pipe redirection", l.linenum);
+					return tok;
+				}
+				tok = '|';
+			} else if(c == '|')
+				tok = OROR;
+			else
+				l.ungetc();
+
+		'\'' =>
+			if (l.atendword) {
+				l.ungetc();
+				tok = '^';
+				break;
+			}
+			startline := l.linenum;
+			s := "";
+			for(;;) {
+				while ((nc := l.getc()) != '\'' && nc != l.EOF)
+					s[len s] = nc;
+				if (nc == l.EOF) {
+					(l.err, l.errline) = ("unterminated string literal", startline);
+					return ERROR;
+				}
+				if (l.getc() != '\'') {
+					l.ungetc();
+					break;
+				}
+				s[len s] = '\'';	# 'xxx''yyy' becomes WORD(xxx'yyy)
+			}
+			l.lval.word = s;
+			tok = WORD;
+			endword = 1;
+
+		* =>
+			if (c == ':') {
+				if (l.getc() == '=') {
+					tok = '=';
+					l.lval.optype = n_LOCAL;
+					break;
+				}
+				l.ungetc();
+			}
+			if (l.atendword) {
+				l.ungetc();
+				tok = '^';
+				break;
+			}
+			allowed: string;
+			if (l.wasdollar)
+				allowed = "a-zA-Z0-9*_";
+			else
+				allowed = "^\n \t\r|$'#<>;^(){}`&=\"";
+			word := "";
+			loop: do {
+				case c {
+				'*' or '?' or '[' or GLOB =>
+					word[len word] = GLOB;
+				':' =>
+					nc := l.getc();
+					l.ungetc();
+					if (nc == '=')
+						break loop;
+				}
+				word[len word] = c;
+			} while ((c = l.getc()) != l.EOF && str->in(c, allowed));
+			l.ungetc();
+			l.lval.word = word;
+			tok = WORD;
+			endword = 1;
+		}
+		l.atendword = endword;
+		l.wasdollar = wasdollar;
+	}
+	return tok;
+}
+
+tokstr(t: int): string
+{
+	s: string;
+	case t {
+	'\n' => s = "'\\n'";
+	33 to 127 => s = sprint("'%c'", t);
+	DUP=>	s = "DUP";
+	REDIR =>s = "REDIR";
+	WORD =>	s = "WORD";
+	OP =>	s = "OP";
+	END =>	s = "END";
+	ERROR=>	s = "ERROR";
+	* =>
+		s = "<unknowntok"+ string t + ">";
+	}
+	return s;
+}
+
+YYLEX.ungetc(lex: self ref YYLEX)
+{
+	lex.strpos--;
+	if (lex.f != nil) {
+		lex.ncbuf++;
+		if (lex.strpos < 0)
+			lex.strpos = len lex.cbuf - 1;
+	}
+}
+		
+YYLEX.getc(lex: self ref YYLEX): int
+{
+	if (lex.eof)				# EOF sticks
+		return lex.EOF;
+	c: int;
+	if (lex.f != nil) {
+		if (lex.ncbuf > 0) {
+			c = lex.cbuf[lex.strpos++];
+			if (lex.strpos >= len lex.cbuf)
+				lex.strpos = 0;
+			lex.ncbuf--;
+		} else {
+			if (lex.lastnl && lex.prompt != nil)
+				sys->fprint(stderr(), "%s", lex.prompt);
+			c = bufio->lex.f.getc();
+			if (c == bufio->ERROR || c == bufio->EOF) {
+				lex.eof = 1;
+				c = lex.EOF;
+			} else if (c == '\n')
+				lex.linenum++;
+			lex.lastnl = (c == '\n');
+			lex.cbuf[lex.strpos++] = c;
+			if (lex.strpos >= len lex.cbuf)
+				lex.strpos = 0;
+		}
+	} else {
+		if (lex.strpos >= len lex.s) {
+			lex.eof = 1;
+			c = lex.EOF;
+		} else
+			c = lex.s[lex.strpos++];
+	}
+	return c;
+}
+
+readnum(lex: ref YYLEX): int
+{
+	sum := nc := 0;
+	while ((c := lex.getc()) >= '0' && c <= '9') {
+		sum = (sum * 10) + (c - '0');
+		nc++;
+	}
+	lex.ungetc();
+	if (nc == 0)
+		return -1;
+	return sum;
+}
+
+readfdassign(lex: ref YYLEX): (int, ref Redir)
+{
+	n1 := readnum(lex);
+	if ((c := lex.getc()) != '=') {
+		if (c == ']')
+			return (REDIR, ref Redir(-1, n1, -1));
+
+		return (ERROR, nil);
+	}
+	n2 := readnum(lex);
+	if (lex.getc() != ']')
+		return (ERROR, nil);
+	return (DUP, ref Redir(-1, n1, n2));
+}
+
+mkseq(left, right: ref Node): ref Node
+{
+	if (left != nil && right != nil)
+		return mk(n_SEQ, left, right);
+	else if (left == nil)
+		return right;
+	return left;
+}
+
+mk(ntype: int, left, right: ref Node): ref Node
+{
+	return ref Node(ntype, left, right, nil, nil);
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+yyexca := array[] of {-1, 0,
+	8, 17,
+	10, 17,
+	11, 17,
+	12, 17,
+	14, 17,
+	15, 17,
+	16, 17,
+	-2, 0,
+-1, 1,
+	1, -1,
+	-2, 0,
+};
+YYNPROD: con 45;
+YYPRIVATE: con 57344;
+yytoknames: array of string;
+yystates: array of string;
+yydebug: con 0;
+YYLAST:	con 93;
+yyact := array[] of {
+  12,  10,  15,   4,   5,  40,   8,  11,   9,   7,
+  30,  31,  54,   6,  50,  35,  34,  32,  33,  21,
+  36,  38,  34,  41,  43,  22,  29,   3,  28,  13,
+  14,  16,  17,  20,  37,  42,   1,  23,  45,  51,
+  44,  47,  48,  18,  39,  19,  41,  43,  56,  30,
+  31,  46,  58,  57,  59,  60,  49,  13,  14,  16,
+  17,  53,  13,  14,  16,  17,   2,  52,   0,  16,
+  17,  18,  27,  19,  16,  17,  18,  52,  19,   0,
+  26,  18,   0,  19,  24,  25,  18,  26,  19,   0,
+  55,  24,  25,
+};
+yypact := array[] of {
+  25,-1000,  11,  11,  69,  58,  18,  14,-1000,  58,
+  58,-1000,   5,-1000,  68,-1000,-1000,  68,-1000,  58,
+-1000,-1000,-1000,-1000,-1000,-1000,  58,-1000,  58,-1000,
+  -1,-1000,-1000,  68,-1000,  -1,-1000,  -5,  63,-1000,
+  -9,  76,  58,-1000,  18,  14,  53,-1000,  58,  63,
+-1000,  -1,-1000,  53,-1000,-1000,-1000,-1000,-1000,  -1,
+-1000,
+};
+yypgo := array[] of {
+   0,   1,   0,  44,   8,   6,  36,   7,  35,   4,
+   9,   2,  66,   5,  34,  13,   3,  33,  21,
+};
+yyr1 := array[] of {
+   0,   6,   6,  17,  17,  12,  12,  13,  13,   9,
+   9,   8,   8,  16,  16,  15,  15,  10,  10,  10,
+   5,   5,   5,   5,   7,   7,   7,   1,   1,   4,
+   4,   4,  14,  14,   3,   3,   3,   2,   2,  11,
+  11,  11,  11,  18,  18,
+};
+yyr2 := array[] of {
+   0,   2,   2,   1,   1,   1,   2,   1,   2,   2,
+   2,   1,   2,   1,   3,   1,   3,   0,   1,   4,
+   1,   2,   1,   1,   3,   3,   2,   1,   2,   1,
+   2,   2,   1,   2,   2,   3,   3,   1,   4,   1,
+   2,   3,   3,   0,   2,
+};
+yychk := array[] of {
+-1000,  -6, -12,   2, -16,  -9, -15, -10,  -5,  -4,
+  -1,  -7,  -2,   4,   5, -11,   6,   7,  18,  20,
+ -17,   8,  14, -17,  15,  16,  11, -12,  10,  12,
+  -2,  -1,  -5,  13,  17,  -2, -11, -14, -18,  -3,
+ -13, -16,  -8,  -9, -15, -10, -18,  -7,  -4, -18,
+  19,  -2,  14, -18,  21,  14, -13,  -5, -11,  -2,
+  -1,
+};
+yydef := array[] of {
+  -2,  -2,   0,   0,   5,  17,  13,  15,  18,  20,
+  22,  23,  29,  27,   0,  37,  39,   0,  43,  17,
+   1,   3,   4,   2,   9,  10,  17,   6,  17,  43,
+  30,  31,  21,  26,  43,  28,  40,   0,  32,  43,
+   0,   7,  17,  11,  14,  16,   0,  24,  25,   0,
+  41,  34,  44,  33,  42,  12,   8,  19,  38,  35,
+  36,
+};
+yytok1 := array[] of {
+   1,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+  14,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,  16,   3,
+  18,  19,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,  15,
+   3,  13,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,  17,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,  20,  12,  21,
+};
+yytok2 := array[] of {
+   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,
+};
+yytok3 := array[] of {
+   0
+};
+
+YYSys: module
+{
+	FD: adt
+	{
+		fd:	int;
+	};
+	fildes:		fn(fd: int): ref FD;
+	fprint:		fn(fd: ref FD, s: string, *): int;
+};
+
+yysys: YYSys;
+yystderr: ref YYSys->FD;
+
+YYFLAG: con -1000;
+
+
+yytokname(yyc: int): string
+{
+	if(yyc > 0 && yyc <= len yytoknames && yytoknames[yyc-1] != nil)
+		return yytoknames[yyc-1];
+	return "<"+string yyc+">";
+}
+
+yystatname(yys: int): string
+{
+	if(yys >= 0 && yys < len yystates && yystates[yys] != nil)
+		return yystates[yys];
+	return "<"+string yys+">\n";
+}
+
+yylex1(yylex: ref YYLEX): int
+{
+	c : int;
+	yychar := yylex.lex();
+	if(yychar <= 0)
+		c = yytok1[0];
+	else if(yychar < len yytok1)
+		c = yytok1[yychar];
+	else if(yychar >= YYPRIVATE && yychar < YYPRIVATE+len yytok2)
+		c = yytok2[yychar-YYPRIVATE];
+	else{
+		n := len yytok3;
+		c = 0;
+		for(i := 0; i < n; i+=2) {
+			if(yytok3[i+0] == yychar) {
+				c = yytok3[i+1];
+				break;
+			}
+		}
+		if(c == 0)
+			c = yytok2[1];	# unknown char
+	}
+	if(yydebug >= 3)
+		yysys->fprint(yystderr, "lex %.4ux %s\n", yychar, yytokname(c));
+	return c;
+}
+
+YYS: adt
+{
+	yyv: YYSTYPE;
+	yys: int;
+};
+
+yyparse(yylex: ref YYLEX): int
+{
+	if(yydebug >= 1 && yysys == nil) {
+		yysys = load YYSys "$Sys";
+		yystderr = yysys->fildes(2);
+	}
+
+	yys := array[YYMAXDEPTH] of YYS;
+
+	yyval: YYSTYPE;
+	yystate := 0;
+	yychar := -1;
+	yynerrs := 0;		# number of errors
+	yyerrflag := 0;		# error recovery flag
+	yyp := -1;
+	yyn := 0;
+
+yystack:
+	for(;;){
+		# put a state and value onto the stack
+		if(yydebug >= 4)
+			yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+
+		yyp++;
+		if(yyp >= len yys)
+			yys = (array[len yys * 2] of YYS)[0:] = yys;
+		yys[yyp].yys = yystate;
+		yys[yyp].yyv = yyval;
+
+		for(;;){
+			yyn = yypact[yystate];
+			if(yyn > YYFLAG) {	# simple state
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+				yyn += yychar;
+				if(yyn >= 0 && yyn < YYLAST) {
+					yyn = yyact[yyn];
+					if(yychk[yyn] == yychar) { # valid shift
+						yychar = -1;
+						yyp++;
+						if(yyp >= len yys)
+							yys = (array[len yys * 2] of YYS)[0:] = yys;
+						yystate = yyn;
+						yys[yyp].yys = yystate;
+						yys[yyp].yyv = yylex.lval;
+						if(yyerrflag > 0)
+							yyerrflag--;
+						if(yydebug >= 4)
+							yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+						continue;
+					}
+				}
+			}
+		
+			# default state action
+			yyn = yydef[yystate];
+			if(yyn == -2) {
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+		
+				# look through exception table
+				for(yyxi:=0;; yyxi+=2)
+					if(yyexca[yyxi] == -1 && yyexca[yyxi+1] == yystate)
+						break;
+				for(yyxi += 2;; yyxi += 2) {
+					yyn = yyexca[yyxi];
+					if(yyn < 0 || yyn == yychar)
+						break;
+				}
+				yyn = yyexca[yyxi+1];
+				if(yyn < 0){
+					yyn = 0;
+					break yystack;
+				}
+			}
+
+			if(yyn != 0)
+				break;
+
+			# error ... attempt to resume parsing
+			if(yyerrflag == 0) { # brand new error
+				yylex.error("syntax error");
+				yynerrs++;
+				if(yydebug >= 1) {
+					yysys->fprint(yystderr, "%s", yystatname(yystate));
+					yysys->fprint(yystderr, "saw %s\n", yytokname(yychar));
+				}
+			}
+
+			if(yyerrflag != 3) { # incompletely recovered error ... try again
+				yyerrflag = 3;
+	
+				# find a state where "error" is a legal shift action
+				while(yyp >= 0) {
+					yyn = yypact[yys[yyp].yys] + YYERRCODE;
+					if(yyn >= 0 && yyn < YYLAST) {
+						yystate = yyact[yyn];  # simulate a shift of "error"
+						if(yychk[yystate] == YYERRCODE)
+							continue yystack;
+					}
+	
+					# the current yyp has no shift onn "error", pop stack
+					if(yydebug >= 2)
+						yysys->fprint(yystderr, "error recovery pops state %d, uncovers %d\n",
+							yys[yyp].yys, yys[yyp-1].yys );
+					yyp--;
+				}
+				# there is no state on the stack with an error shift ... abort
+				yyn = 1;
+				break yystack;
+			}
+
+			# no shift yet; clobber input char
+			if(yydebug >= 2)
+				yysys->fprint(yystderr, "error recovery discards %s\n", yytokname(yychar));
+			if(yychar == YYEOFCODE) {
+				yyn = 1;
+				break yystack;
+			}
+			yychar = -1;
+			# try again in the same state
+		}
+	
+		# reduction by production yyn
+		if(yydebug >= 2)
+			yysys->fprint(yystderr, "reduce %d in:\n\t%s", yyn, yystatname(yystate));
+	
+		yypt := yyp;
+		yyp -= yyr2[yyn];
+		yym := yyn;
+	
+		# consult goto table to find next state
+		yyn = yyr1[yyn];
+		yyg := yypgo[yyn];
+		yyj := yyg + yys[yyp].yys + 1;
+	
+		if(yyj >= YYLAST || yychk[yystate=yyact[yyj]] != -yyn)
+			yystate = yyact[yyg];
+		case yym {
+			
+1=>
+{yylex.lval.node = yys[yypt-1].yyv.node; return 0;}
+2=>
+{yylex.lval.node = nil; return 0;}
+5=>
+yyval.node = yys[yyp+1].yyv.node;
+6=>
+{yyval.node = mkseq(yys[yypt-1].yyv.node, yys[yypt-0].yyv.node); }
+7=>
+yyval.node = yys[yyp+1].yyv.node;
+8=>
+{yyval.node = mkseq(yys[yypt-1].yyv.node, yys[yypt-0].yyv.node); }
+9=>
+{yyval.node = yys[yypt-1].yyv.node; }
+10=>
+{yyval.node = ref Node(n_NOWAIT, yys[yypt-1].yyv.node, nil, nil, nil); }
+11=>
+yyval.node = yys[yyp+1].yyv.node;
+12=>
+{yyval.node = yys[yypt-1].yyv.node; }
+13=>
+yyval.node = yys[yyp+1].yyv.node;
+14=>
+{
+		yyval.node = mk(n_ADJ,
+				mk(n_ADJ,
+					ref Node(n_WORD,nil,nil,"or",nil),
+					mk(n_BLOCK, yys[yypt-2].yyv.node, nil)
+				),
+				mk(n_BLOCK,yys[yypt-0].yyv.node,nil)
+			);
+	}
+15=>
+yyval.node = yys[yyp+1].yyv.node;
+16=>
+{
+		yyval.node = mk(n_ADJ,
+				mk(n_ADJ,
+					ref Node(n_WORD,nil,nil,"and",nil),
+					mk(n_BLOCK, yys[yypt-2].yyv.node, nil)
+				),
+				mk(n_BLOCK,yys[yypt-0].yyv.node,nil)
+			);
+	}
+17=>
+{yyval.node = nil;}
+18=>
+yyval.node = yys[yyp+1].yyv.node;
+19=>
+{yyval.node = ref Node(n_PIPE, yys[yypt-3].yyv.node, yys[yypt-0].yyv.node, nil, yys[yypt-2].yyv.redir); }
+20=>
+yyval.node = yys[yyp+1].yyv.node;
+21=>
+{yyval.node = mk(n_ADJ, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node); }
+22=>
+yyval.node = yys[yyp+1].yyv.node;
+23=>
+yyval.node = yys[yyp+1].yyv.node;
+24=>
+{yyval.node = mk(yys[yypt-1].yyv.optype, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node); }
+25=>
+{yyval.node = mk(yys[yypt-1].yyv.optype, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node); }
+26=>
+{yyval.node = mk(yys[yypt-0].yyv.optype, yys[yypt-1].yyv.node, nil); }
+27=>
+{yyval.node = ref Node(n_DUP, nil, nil, nil, yys[yypt-0].yyv.redir); }
+28=>
+{yyval.node = ref Node(n_REDIR, yys[yypt-0].yyv.node, nil, nil, yys[yypt-1].yyv.redir); }
+29=>
+yyval.node = yys[yyp+1].yyv.node;
+30=>
+{yyval.node = mk(n_ADJ, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node); }
+31=>
+{yyval.node = mk(n_ADJ, yys[yypt-1].yyv.node, yys[yypt-0].yyv.node); }
+32=>
+{yyval.node = nil;}
+33=>
+yyval.node = yys[yyp+1].yyv.node;
+34=>
+{yyval.node = yys[yypt-0].yyv.node; }
+35=>
+{yyval.node = mk(n_ADJ, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node); }
+36=>
+{yyval.node = mk(n_ADJ, yys[yypt-2].yyv.node, yys[yypt-0].yyv.node); }
+37=>
+yyval.node = yys[yyp+1].yyv.node;
+38=>
+{yyval.node = mk(n_CONCAT, yys[yypt-3].yyv.node, yys[yypt-0].yyv.node); }
+39=>
+{yyval.node = ref Node(n_WORD, nil, nil, yys[yypt-0].yyv.word, nil); }
+40=>
+{yyval.node = mk(yys[yypt-1].yyv.optype, yys[yypt-0].yyv.node, nil); }
+41=>
+{yyval.node = mk(n_LIST, yys[yypt-1].yyv.node, nil); }
+42=>
+{yyval.node = mk(n_BLOCK, yys[yypt-1].yyv.node, nil); }
+		}
+	}
+
+	return yyn;
+}
--- /dev/null
+++ b/appl/cmd/sh/sh.y
@@ -1,0 +1,2628 @@
+%{
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+include "string.m";
+	str: String;
+include "filepat.m";
+	filepat: Filepat;
+include "env.m";
+	env: Env;
+include "sh.m";
+	myself: Sh;
+	myselfbuiltin: Shellbuiltin;
+
+YYSTYPE: adt {
+	node:	ref Node;
+	word:	string;
+
+	redir:	ref Redir;
+	optype:	int;
+};
+
+YYLEX: adt {
+	lval:			YYSTYPE;
+	err:			string;	# if error has occurred
+	errline:		int;		# line it occurred on.
+	path:			string;	# name of file that's being read.
+
+	# free caret state
+	wasdollar:		int;
+	atendword:	int;
+	eof:			int;
+	cbuf:			array of int;	# last chars read
+	ncbuf:		int;			# number of chars in cbuf
+
+	f:			ref Bufio->Iobuf;
+	s:			string;
+	strpos: 		int;			# string pos/cbuf index
+
+	linenum:		int;
+	prompt:		string;
+	lastnl:		int;
+
+	initstring:		fn(s: string): ref YYLEX;
+	initfile:		fn(fd: ref Sys->FD, path: string): ref YYLEX;
+	lex:			fn(l: self ref YYLEX): int;
+	error:		fn(l: self ref YYLEX, err: string);
+	getc:			fn(l: self ref YYLEX): int;
+	ungetc:		fn(l: self ref YYLEX);
+
+	EOF:			con -1;
+};
+
+Options: adt {
+	lflag,
+	nflag:		int;
+	ctxtflags:		int;
+	carg:			string;
+};
+
+%}
+
+%module Sh {
+	# module definition is in shell.m
+}
+
+%token DUP REDIR WORD OP END ERROR ANDAND OROR
+
+%type <node> redir word nlsimple simple cmd shell assign
+%type <node> cmdsan cmdsa pipe comword line body list and2 or2
+%type <redir> DUP REDIR '|'
+%type <optype> OP '='
+%type <word> WORD
+
+%start shell
+%%
+shell:	line end		{yylex.lval.node = $line; return 0;}
+	| error end		{yylex.lval.node = nil; return 0;}
+end:	END
+	| '\n'
+line:	or2
+	| cmdsa line		{$$ = mkseq($cmdsa, $line); }
+body:	or2
+	| cmdsan body		{$$ = mkseq($cmdsan, $body); }
+cmdsa: 	or2  ';'		{$$ = $or2; }
+	| or2 '&'			{$$ = ref Node(n_NOWAIT, $or2, nil, nil, nil); }
+cmdsan:	cmdsa
+	| or2 '\n'			{$$ = $or2; }
+or2:	and2
+	| or2 OROR and2 {
+		$$ = mk(n_ADJ,
+				mk(n_ADJ,
+					ref Node(n_WORD,nil,nil,"or",nil),
+					mk(n_BLOCK, $or2, nil)
+				),
+				mk(n_BLOCK,$and2,nil)
+			);
+	}
+and2: pipe
+	| and2 ANDAND pipe {
+		$$ = mk(n_ADJ,
+				mk(n_ADJ,
+					ref Node(n_WORD,nil,nil,"and",nil),
+					mk(n_BLOCK, $and2, nil)
+				),
+				mk(n_BLOCK,$pipe,nil)
+			);
+	}
+pipe:					{$$ = nil;}
+	| cmd
+	| pipe '|' optnl cmd	{$$ = ref Node(n_PIPE, $pipe, $cmd, nil, $2); }
+cmd:	simple
+	| redir cmd		{$$ = mk(n_ADJ, $redir, $cmd); }
+	| redir
+	| assign
+assign: word '=' assign	{$$ = mk($2, $word, $assign); }
+	| word '=' simple	{$$ = mk($2, $word, $simple); }
+	| word '='			{$$ = mk($2, $word, nil); }
+redir:	DUP			{$$ = ref Node(n_DUP, nil, nil, nil, $DUP); }
+	| REDIR word		{$$ = ref Node(n_REDIR, $word, nil, nil, $REDIR); }
+simple:	word
+	| simple word		{$$ = mk(n_ADJ, $simple, $word); }
+	| simple redir		{$$ = mk(n_ADJ, $simple, $redir); }
+list:	optnl			{$$ = nil;}
+	| nlsimple optnl
+nlsimple: optnl word		{$$ = $word; }
+	| nlsimple optnl word	{$$ = mk(n_ADJ, $nlsimple, $word); }
+	| nlsimple optnl redir  {$$ = mk(n_ADJ, $nlsimple, $redir); }
+word:	comword
+	| word '^' optnl comword	{$$ = mk(n_CONCAT, $word, $comword); }
+comword: WORD		{$$ = ref Node(n_WORD, nil, nil, $WORD, nil); }
+	| OP comword		{$$ = mk($OP, $comword, nil); }
+	| '(' list ')'			{$$ = mk(n_LIST, $list, nil); }
+	| '{' body '}'		{$$ = mk(n_BLOCK, $body, nil); }
+optnl:  # null
+	| optnl '\n'
+%%
+
+EPERM: con "permission denied";
+EPIPE: con "write on closed pipe";
+
+#SHELLRC: con "lib/profile";
+LIBSHELLRC: con "/lib/sh/profile";
+BUILTINPATH: con "/dis/sh";
+
+DEBUG: con 0;
+
+ENVSEP: con 0;				# word seperator in external environment
+ENVHASHSIZE: con 7;		# XXX profile usage of this...
+OAPPEND: con 16r80000;		# make sure this doesn't clash with O* constants in sys.m
+OMASK: con 7;
+
+usage()
+{
+	sys->fprint(stderr(), "usage: sh [-ilexn] [-c command] [file [arg...]]\n");
+	raise "fail:usage";
+}
+
+badmodule(path: string)
+{
+	sys->fprint(sys->fildes(2), "sh: cannot load %s: %r\n", path);
+	raise "fail:bad module" ;
+}
+
+initialise()
+{
+	if (sys == nil) {
+		sys = load Sys Sys->PATH;
+
+		filepat = load Filepat Filepat->PATH;
+		if (filepat == nil) badmodule(Filepat->PATH);
+
+		str = load String String->PATH;
+		if (str == nil) badmodule(String->PATH);
+
+		bufio = load Bufio Bufio->PATH;
+		if (bufio == nil) badmodule(Bufio->PATH);
+
+		myself = load Sh "$self";
+		if (myself == nil) badmodule("$self(Sh)");
+
+		myselfbuiltin = load Shellbuiltin "$self";
+		if (myselfbuiltin == nil) badmodule("$self(Shellbuiltin)");
+
+		env = load Env Env->PATH;
+	}
+}
+blankopts: Options;
+init(drawcontext: ref Draw->Context, argv: list of string)
+{
+	initialise();
+	opts := blankopts;
+	if (argv != nil) {
+		if ((hd argv)[0] == '-')
+			opts.lflag++;
+		argv = tl argv;
+	}
+
+	interactive := 0;
+loop: while (argv != nil && hd argv != nil && (hd argv)[0] == '-') {
+		for (i := 1; i < len hd argv; i++) {
+			c := (hd argv)[i];
+			case c {
+			'i' =>
+				interactive = Context.INTERACTIVE;
+			'l' =>
+				opts.lflag++;	# login (read $home/lib/profile)
+			'n' =>
+				opts.nflag++;	# don't fork namespace
+			'e' =>
+				opts.ctxtflags |= Context.ERROREXIT;
+			'x' =>
+				opts.ctxtflags |= Context.EXECPRINT;
+			'c' =>
+				arg: string;
+				if (i < len hd argv - 1) {
+					arg = (hd argv)[i + 1:];
+				} else if (tl argv == nil || hd tl argv == "") {
+					usage();
+				} else {
+					arg = hd tl argv;
+					argv = tl argv;
+				}
+				argv = tl argv;
+				opts.carg = arg;
+				continue loop;
+			}
+		}
+		argv = tl argv;
+	}
+
+	sys->pctl(Sys->FORKFD, nil);
+	if (!opts.nflag)
+		sys->pctl(Sys->FORKNS, nil);
+	ctxt := Context.new(drawcontext);
+	ctxt.setoptions(opts.ctxtflags, 1);
+	if (opts.carg != nil) {
+		status := ctxt.run(stringlist2list("{" + opts.carg + "}" :: argv), !interactive);
+		if (!interactive) {
+			if (status != nil)
+				raise "fail:" + status;
+			exit;
+		}
+		setstatus(ctxt, status);
+	}
+
+	# if login shell, run standard init script
+	if (opts.lflag)
+		runscript(ctxt, LIBSHELLRC, nil, 0);
+
+	if (argv == nil) {
+#		if (opts.lflag)
+#			runscript(ctxt, SHELLRC, nil, 0);
+		if (isconsole(sys->fildes(0)))
+			interactive |= ctxt.INTERACTIVE;
+		ctxt.setoptions(interactive, 1);
+		runfile(ctxt, sys->fildes(0), "stdin", nil);
+	} else {
+		ctxt.setoptions(interactive, 1);
+		runscript(ctxt, hd argv, stringlist2list(tl argv), 1);
+	}
+}
+
+# XXX should this refuse to parse a non braced-block?
+parse(s: string): (ref Node, string)
+{
+	initialise();
+	
+	lex := YYLEX.initstring(s);
+
+	return doparse(lex, "", 0);
+}
+
+system(drawctxt: ref Draw->Context, cmd: string): string
+{
+	initialise();
+	{
+		(n, err) := parse(cmd);
+		if (err != nil)
+			return err;
+		if (n == nil)
+			return nil;
+		return Context.new(drawctxt).run(ref Listnode(n, nil) :: nil, 0);
+	} exception e {
+	"fail:*" =>
+		return failurestatus(e);
+	}
+}
+
+run(drawctxt: ref Draw->Context, argv: list of string): string
+{
+	initialise();
+	{
+		return Context.new(drawctxt).run(stringlist2list(argv), 0);
+	} exception e {
+	"fail:*" =>
+		return failurestatus(e);
+	}
+}
+
+isconsole(fd: ref Sys->FD): int
+{
+	(ok1, d1) := sys->fstat(fd);
+	(ok2, d2) := sys->stat("/dev/cons");
+	if (ok1 < 0 || ok2 < 0)
+		return 0;
+	return d1.dtype == d2.dtype && d1.qid.path == d2.qid.path;
+}
+
+# run commands from file _path_
+runscript(ctxt: ref Context, path: string, args: list of ref Listnode, reporterr: int)
+{
+	{
+		fd := sys->open(path, Sys->OREAD);
+		if (fd != nil)
+			runfile(ctxt, fd, path, args);
+		else if (reporterr)
+			ctxt.fail("bad script path", sys->sprint("sh: cannot open %s: %r", path));
+	} exception {
+	"fail:*" =>
+		if(!reporterr)
+			return;
+		raise;
+	}
+}
+
+# run commands from the opened file fd.
+# if interactive is non-zero, print a command prompt at appropriate times.
+runfile(ctxt: ref Context, fd: ref Sys->FD, path: string, args: list of ref Listnode)
+{
+	ctxt.push();
+	{
+		ctxt.setlocal("0", stringlist2list(path :: nil));
+		ctxt.setlocal("*", args);
+		lex := YYLEX.initfile(fd, path);
+		if (DEBUG) debug(sprint("parse(interactive == %d)", (ctxt.options() & ctxt.INTERACTIVE) != 0));
+		prompt := "" :: "" :: nil;
+		laststatus: string;
+		while (!lex.eof) {
+			interactive := ctxt.options() & ctxt.INTERACTIVE;
+			if (interactive) {
+				prompt = list2stringlist(ctxt.get("prompt"));
+				if (prompt == nil)
+					prompt = "; " :: "" :: nil;
+	
+				sys->fprint(stderr(), "%s", hd prompt);
+				if (tl prompt == nil) {
+					prompt = hd prompt :: "" :: nil;
+				}
+			}
+			(n, err) := doparse(lex, hd tl prompt, !interactive);
+			if (err != nil) {
+				sys->fprint(stderr(), "sh: %s\n", err);
+				if (!interactive)
+					raise "fail:parse error";
+			} else if (n != nil) {
+				if (interactive) {
+					{
+						laststatus = walk(ctxt, n, 0);
+					} exception e2 {
+					"fail:*" =>
+						laststatus = failurestatus(e2);
+					}
+				} else
+					laststatus = walk(ctxt, n, 0);
+				setstatus(ctxt, laststatus);
+				if ((ctxt.options() & ctxt.ERROREXIT) && laststatus != nil)
+					break;
+			}
+		}
+		if (laststatus != nil)
+			raise "fail:" + laststatus;
+		ctxt.pop();
+	}
+	exception {
+	"fail:*" =>
+		ctxt.pop();
+		raise;
+	}
+}
+
+nonexistent(e: string): int
+{
+	errs := array[] of {"does not exist", "directory entry not found"};
+	for (i := 0; i < len errs; i++){
+		j := len errs[i];
+		if (j <= len e && e[len e-j:] == errs[i])
+			return 1;
+	}
+	return 0;
+}
+
+Redirword: adt {
+	fd: ref Sys->FD;
+	w: string;
+	r: Redir;
+};
+
+Redirlist: adt {
+	r: list of Redirword;
+};
+
+# a hack so that the structure of walk() doesn't change much
+# to accomodate echo|wc&
+# transform the above into {echo|wc}$*&
+# which should amount to exactly the same thing.
+pipe2cmd(n: ref Node): ref Node
+{
+	if (n == nil || n.ntype != n_PIPE)
+		return n;
+	return mk(n_ADJ, mk(n_BLOCK,n,nil), mk(n_VAR,ref Node(n_WORD,nil,nil,"*",nil),nil));
+}
+
+# walk a node tree.
+# last is non-zero if this walk is the last action
+# this shell process will take before exiting (i.e. redirections
+# don't require a new process to avoid side effects)
+walk(ctxt: ref Context, n: ref Node, last: int): string
+{
+	if (DEBUG) debug(sprint("walking: %s", cmd2string(n)));
+	# avoid tail recursion stack explosion
+	while (n != nil && n.ntype == n_SEQ) {
+		status := walk(ctxt, n.left, 0);
+		if (ctxt.options() & ctxt.ERROREXIT && status != nil)
+			raise "fail:" + status;
+		setstatus(ctxt, status);
+		n = n.right;
+	}
+	if (n == nil)
+		return nil;
+	case (n.ntype) {
+	n_PIPE =>
+		return waitfor(ctxt, walkpipeline(ctxt, n, nil, -1));
+	n_ASSIGN or n_LOCAL =>
+		assign(ctxt, n);
+		return nil;
+	* =>
+		bg := 0;
+		if (n.ntype == n_NOWAIT) {
+			bg = 1;
+			n = pipe2cmd(n.left);
+		}
+
+		redirs := ref Redirlist(nil);
+		line := glob(glom(ctxt, n, redirs, nil));
+
+		if (bg) {
+			startchan := chan of (int, ref Expropagate);
+			spawn runasync(ctxt, 1, line, redirs, startchan);
+			(pid, nil) := <-startchan;
+			redirs = nil;
+			if (DEBUG) debug("started background process "+ string pid);
+			ctxt.set("apid", ref Listnode(nil, string pid) :: nil);
+			return nil;
+		} else {
+			return runsync(ctxt, line, redirs, last);
+		}
+	}
+}
+
+assign(ctxt: ref Context, n: ref Node): list of ref Listnode
+{
+	redirs := ref Redirlist;
+	val: list of ref Listnode;
+	if (n.right != nil && (n.right.ntype == n_ASSIGN || n.right.ntype == n_LOCAL))
+		val = assign(ctxt, n.right);
+	else
+		val = glob(glom(ctxt, n.right, redirs, nil));
+	vars := glom(ctxt, n.left, redirs, nil);
+	if (vars == nil)
+		ctxt.fail("bad assign", "sh: nil variable name");
+	if (redirs.r != nil)
+		ctxt.fail("bad assign", "sh: redirections not allowed in assignment");
+	tval := val;
+	for (; vars != nil; vars = tl vars) {
+		vname := deglob((hd vars).word);
+		if (vname == nil) 
+			ctxt.fail("bad assign", "sh: bad variable name");
+		v: list of ref Listnode = nil;
+		if (tl vars == nil)
+			v = tval;
+		else if (tval != nil)
+			v = hd tval :: nil;
+		if (n.ntype == n_ASSIGN)
+			ctxt.set(vname, v);
+		else
+			ctxt.setlocal(vname, v);
+		if (tval != nil)
+			tval = tl tval;
+	}
+	return val;
+}
+
+walkpipeline(ctxt: ref Context, n: ref Node, wrpipe: ref Sys->FD, wfdno: int): list of int
+{
+	if (n == nil)
+		return nil;
+
+	fds := array[2] of ref Sys->FD;
+	pids: list of int;
+	rfdno := -1;
+	if (n.ntype == n_PIPE) {
+		if (sys->pipe(fds) == -1)
+			ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
+		nwfdno := -1;
+		if (n.redir != nil) {
+			(fd1, fd2) := (n.redir.fd2, n.redir.fd1);
+			if (fd2 == -1)
+				(fd1, fd2) = (fd2, fd1);
+			(nwfdno, rfdno) = (fd2, fd1);
+		}
+		pids = walkpipeline(ctxt, n.left, fds[1], nwfdno);
+		fds[1] = nil;
+		n = n.right;
+	}
+	r := ref Redirlist(nil);
+	rlist := glob(glom(ctxt, n, r, nil));
+	if (fds[0] != nil) {
+		if (rfdno == -1)
+			rfdno = 0;
+		r.r = Redirword(fds[0], nil, Redir(Sys->OREAD, rfdno, -1)) :: r.r;
+	}
+	if (wrpipe != nil) {
+		if (wfdno == -1)
+			wfdno = 1;
+		r.r = Redirword(wrpipe, nil, Redir(Sys->OWRITE, wfdno, -1)) :: r.r;
+	}
+	startchan := chan of (int, ref Expropagate);
+	spawn runasync(ctxt, 1, rlist, r, startchan);
+	(pid, nil) := <-startchan;
+	if (DEBUG) debug("started pipe process "+string pid);
+	return pid :: pids;
+}
+
+makeredir(f: string, mode: int, fd: int): Redirword
+{
+	return Redirword(nil, f, Redir(mode, fd, -1));
+}
+
+# expand substitution operators in a node list
+glom(ctxt: ref Context, n: ref Node, redirs: ref Redirlist, onto: list of ref Listnode)
+		: list of ref Listnode
+{
+	if (n == nil) return nil;
+
+	if (n.ntype != n_ADJ)
+		return listjoin(glomoperation(ctxt, n, redirs), onto);
+
+	nlist := glom(ctxt, n.right, redirs, onto);
+
+	if (n.left.ntype != n_ADJ) {
+		# if it's a terminal node
+		nlist = listjoin(glomoperation(ctxt, n.left, redirs), nlist);
+	} else
+		nlist = glom(ctxt, n.left, redirs, nlist);
+	return nlist;
+}
+
+listjoin(left, right: list of ref Listnode): list of ref Listnode
+{
+	l: list of ref Listnode;
+	for (; left != nil; left = tl left)
+		l = hd left :: l;
+	for (; l != nil; l = tl l)
+		right = hd l :: right;
+	return right;
+}
+
+pipecmd(ctxt: ref Context, cmd: list of ref Listnode, redir: ref Redir): ref Sys->FD
+{
+	if(redir.fd2 != -1 || (redir.rtype & OAPPEND))
+		ctxt.fail("bad redir", "sh: bad redirection");
+	r := *redir;
+	case redir.rtype {
+	Sys->OREAD =>
+		r.rtype = Sys->OWRITE;
+	Sys->OWRITE =>
+		r.rtype = Sys->OREAD;
+	}
+			
+	p := array[2] of ref Sys->FD;
+	if(sys->pipe(p) == -1)
+		ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
+	startchan := chan of (int, ref Expropagate);
+	spawn runasync(ctxt, 1, cmd, ref Redirlist((p[1], nil, r) :: nil), startchan);
+	p[1] = nil;
+	<-startchan;
+	return p[0];
+}
+
+glomoperation(ctxt: ref Context, n: ref Node, redirs: ref Redirlist): list of ref Listnode
+{
+	if (n == nil)
+		return nil;
+
+	nlist: list of ref Listnode;
+	case n.ntype {
+	n_WORD =>
+		nlist = ref Listnode(nil, n.word) :: nil;
+	n_REDIR =>
+		wlist := glob(glom(ctxt, n.left, ref Redirlist(nil), nil));
+		if (len wlist != 1)
+			ctxt.fail("bad redir", "sh: single redirection operand required");
+		if((hd wlist).cmd != nil){
+			fd := pipecmd(ctxt, wlist, n.redir);
+			redirs.r = Redirword(fd, nil, (n.redir.rtype, fd.fd, -1)) :: redirs.r;
+			nlist = ref Listnode(nil, "/fd/"+string fd.fd) :: nil;
+		}else{
+			redirs.r = Redirword(nil, (hd wlist).word, *n.redir) :: redirs.r;
+		}
+	n_DUP =>
+		redirs.r = Redirword(nil, "", *n.redir) :: redirs.r;
+	n_LIST =>
+		nlist = glom(ctxt, n.left, redirs, nil);
+	n_CONCAT =>
+		nlist = concat(ctxt, glom(ctxt, n.left, redirs, nil), glom(ctxt, n.right, redirs, nil));
+	n_VAR or n_SQUASH or n_COUNT =>
+		arg := glom(ctxt, n.left, ref Redirlist(nil), nil);
+		if (len arg == 1 && (hd arg).cmd != nil)
+			nlist = subsbuiltin(ctxt, (hd arg).cmd.left);
+		else if (len arg != 1 || (hd arg).word == nil)
+			ctxt.fail("bad $ arg", "sh: bad variable name");
+		else
+			nlist = ctxt.get(deglob((hd arg).word));
+		case n.ntype {
+		n_VAR =>;
+		n_COUNT =>
+			nlist = ref Listnode(nil, string len nlist) :: nil;
+		n_SQUASH =>
+			# XXX could squash with first char of $ifs, perhaps
+			nlist = ref Listnode(nil, squash(list2stringlist(nlist), " ")) :: nil;
+		}
+	n_BQ or n_BQ2 =>
+		arg := glom(ctxt, n.left, ref Redirlist(nil), nil);
+		seps := "";
+		if (n.ntype == n_BQ) {
+			seps = squash(list2stringlist(ctxt.get("ifs")), "");
+			if (seps == nil)
+				seps = " \t\n\r";
+		}
+		(nlist, nil) = bq(ctxt, glob(arg), seps);
+	n_BLOCK =>
+		nlist = ref Listnode(n, "") :: nil;
+	n_ASSIGN or n_LOCAL =>
+		ctxt.fail("bad assign", "sh: assignment in invalid context");
+	* =>
+		panic("bad node type "+string n.ntype+" in glomop");
+	}
+	return nlist;
+}
+
+subsbuiltin(ctxt: ref Context, n: ref Node): list of ref Listnode
+{
+	if (n == nil || n.ntype == n_SEQ ||
+			n.ntype == n_PIPE || n.ntype == n_NOWAIT)
+		ctxt.fail("bad $ arg", "sh: invalid argument to ${} operator");
+	r := ref Redirlist;
+	cmd := glob(glom(ctxt, n, r, nil));
+	if (r.r != nil)
+		ctxt.fail("bad $ arg", "sh: redirection not allowed in substitution");
+	r = nil;
+	if (cmd == nil || (hd cmd).word == nil || (hd cmd).cmd != nil)
+		ctxt.fail("bad $ arg", "sh: bad builtin name");
+
+	(nil, bmods) := findbuiltin(ctxt.env.sbuiltins, (hd cmd).word);
+	if (bmods == nil)
+		ctxt.fail("builtin not found",
+			sys->sprint("sh: builtin %s not found", (hd cmd).word));
+	return (hd bmods)->runsbuiltin(ctxt, myself, cmd);
+}
+
+#
+# backquote substitution (could be done in a builtin)
+#
+
+getbq(nil: ref Context, fd: ref Sys->FD, seps: string): list of ref Listnode
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	buflen := 0;
+	while ((n := sys->read(fd, buf[buflen:], len buf - buflen)) > 0) {
+		buflen += n;
+		if (buflen == len buf) {
+			nbuf := array[buflen * 2] of byte;
+			nbuf[0:] = buf[0:];
+			buf = nbuf;
+		}
+	}
+	l: list of string;
+	if (seps != nil)
+		(nil, l) = sys->tokenize(string buf[0:buflen], seps);
+	else
+		l = string buf[0:buflen] :: nil;
+	buf = nil;
+	return stringlist2list(l);
+}
+
+bq(ctxt: ref Context, cmd: list of ref Listnode, seps: string): (list of ref Listnode, string)
+{
+	fds := array[2] of ref Sys->FD;
+	if (sys->pipe(fds) == -1)
+		ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
+
+	r := rdir(fds[1]);
+	fds[1] = nil;
+	startchan := chan of (int, ref Expropagate);
+	spawn runasync(ctxt, 0, cmd, r, startchan);
+	(exepid, exprop) := <-startchan;
+	r = nil;
+	bqlist := getbq(ctxt, fds[0], seps);
+	waitfor(ctxt, exepid :: nil);
+	if (exprop.name != nil)
+		raise exprop.name;
+	return (bqlist, nil);
+}
+
+# get around compiler temporaries bug
+rdir(fd: ref Sys->FD): ref Redirlist
+{
+	return  ref Redirlist(Redirword(fd, nil, Redir(Sys->OWRITE, 1, -1)) :: nil);
+}
+
+#
+# concatenation
+#
+
+concatwords(p1, p2: ref Listnode): ref Listnode
+{
+	if (p1.word == nil && p1.cmd != nil)
+		p1.word = cmd2string(p1.cmd);
+	if (p2.word == nil && p2.cmd != nil)
+		p2.word = cmd2string(p2.cmd);
+	return ref Listnode(nil, p1.word + p2.word);
+}
+
+concat(ctxt: ref Context, nl1, nl2: list of ref Listnode): list of ref Listnode
+{
+	if (nl1 == nil || nl2 == nil) {
+		if (nl1 == nil && nl2 == nil)
+			return nil;
+		ctxt.fail("bad concatenation", "sh: null list in concatenation");
+	}
+
+	ret: list of ref Listnode;
+	if (tl nl1 == nil || tl nl2 == nil) {
+		for (p1 := nl1; p1 != nil; p1 = tl p1)
+			for (p2 := nl2; p2 != nil; p2 = tl p2)
+				ret = concatwords(hd p1, hd p2) :: ret;
+	} else {
+		if (len nl1 != len nl2)
+			ctxt.fail("bad concatenation", "sh: lists of differing sizes can't be concatenated");
+		while (nl1 != nil) {
+			ret = concatwords(hd nl1, hd nl2) :: ret;
+			(nl1, nl2) = (tl nl1, tl nl2);
+		}
+	}
+	return revlist(ret);
+}
+
+Expropagate: adt {
+	name: string;
+};
+
+# run an asynchronous process, first redirecting its I/O
+# as specified in _redirs_.
+# it sends its process ID down _startchan_ before executing.
+# it has to jump through one or two hoops to make sure
+# Sys->FD ref counting is done correctly. this code
+# is more sensitive than you might think.
+runasync(ctxt: ref Context, copyenv: int, argv: list of ref Listnode, redirs: ref Redirlist,
+		startchan: chan of (int, ref Expropagate))
+{
+	status: string;
+
+	pid := sys->pctl(sys->FORKFD, nil);
+	if (DEBUG) debug(sprint("in async (len redirs: %d)", len redirs.r));
+	ctxt = ctxt.copy(copyenv);
+	exprop := ref Expropagate;
+	{
+		newfdl := doredirs(ctxt, redirs);
+		redirs = nil;
+		if (newfdl != nil)
+			sys->pctl(Sys->NEWFD, newfdl);
+		# stop the old waitfd from holding the intermediate
+		# file descriptor group open.
+		ctxt.waitfd = waitfd();
+		# N.B. it's important that the sync is done here, not
+		# before doredirs, as otherwise there's some sort of
+		# race condition that leads to pipe non-completion.
+		startchan <-= (pid, exprop);
+		startchan = nil;
+		status = ctxt.run(argv, copyenv);
+	} exception e {
+	"fail:*" =>
+		exprop.name = e;
+		if (startchan != nil)
+			startchan <-= (pid, exprop);
+		raise e;
+	}
+	if (status != nil) {
+		# don't propagate bad status as an exception.
+		raise "fail:" + status;
+	}
+}
+
+# run a synchronous process
+runsync(ctxt: ref Context, argv: list of ref Listnode,
+		redirs: ref Redirlist, last: int): string
+{
+	if (DEBUG) debug(sys->sprint("in sync (len redirs: %d; last: %d)", len redirs.r, last));
+	if (redirs.r != nil && !last) {
+		# a new process is required to shield redirection side effects
+		startchan := chan of (int, ref Expropagate);
+		spawn runasync(ctxt, 0, argv, redirs, startchan);
+		(pid, exprop) := <-startchan;
+		redirs = nil;
+		r := waitfor(ctxt, pid :: nil);
+		if (exprop.name != nil)
+			raise exprop.name;
+		return r;
+	} else {
+		newfdl := doredirs(ctxt, redirs);
+		redirs = nil;
+		if (newfdl != nil)
+			sys->pctl(Sys->NEWFD, newfdl);
+		return ctxt.run(argv, last);
+	}
+}
+
+# path is prefixed with: "/", "#", "./" or "../"
+absolute(p: string): int
+{
+	if (len p < 2)
+		return 0;
+	if (p[0] == '/' || p[0] == '#')
+		return 1;
+	if (len p < 3 || p[0] != '.')
+		return 0;
+	if (p[1] == '/')
+		return 1;
+	if (p[1] == '.' && p[2] == '/')
+		return 1;
+	return 0;
+}
+
+runexternal(ctxt: ref Context, args: list of ref Listnode, last: int): string
+{
+	progname := (hd args).word;
+	disfile := 0;
+	if (len progname >= 4 && progname[len progname-4:] == ".dis")
+		disfile = 1;
+	pathlist: list of string;
+	if (absolute(progname))
+		pathlist = list of {""};
+	else if ((pl := ctxt.get("path")) != nil)
+		pathlist = list2stringlist(pl);
+	else
+		pathlist = list of {"/dis", "."};
+
+	err := "";
+	do {
+		path: string;
+		if (hd pathlist != "")
+			path = hd pathlist + "/" + progname;
+		else
+			path = progname;
+
+		npath := path;
+		if (!disfile)
+			npath += ".dis";
+		mod := load Command npath;
+		if (mod != nil) {
+			argv := list2stringlist(args);
+			export(ctxt.env.localenv);
+
+			if (last) {
+				{
+					sys->pctl(Sys->NEWFD, ctxt.keepfds);
+					mod->init(ctxt.drawcontext, argv);
+					exit;
+				} exception e {
+				EPIPE =>
+					return EPIPE;
+				"fail:*" =>
+					return failurestatus(e);
+				}
+			}
+			extstart := chan of int;
+			spawn externalexec(mod, ctxt.drawcontext, argv, extstart, ctxt.keepfds);
+			pid := <-extstart;
+			if (DEBUG) debug("started external externalexec; pid is "+string pid);
+			return waitfor(ctxt, pid :: nil);
+		}
+		err = sys->sprint("%r");
+		if (nonexistent(err)) {
+			# try and run it as a shell script
+			if (!disfile && (fd := sys->open(path, Sys->OREAD)) != nil) {
+				(ok, info) := sys->fstat(fd);
+				# make permission checking more accurate later
+				if (ok == 0 && (info.mode & Sys->DMDIR) == 0
+						&& (info.mode & 8r111) != 0)
+					return runhashpling(ctxt, fd, path, tl args, last);
+			};
+			err = sys->sprint("%r");
+		}
+		pathlist = tl pathlist;
+	} while (pathlist != nil && nonexistent(err));
+	diagnostic(ctxt, sys->sprint("%s: %s", progname, err));
+	return err;
+}
+
+failurestatus(e: string): string
+{
+	s := e[5:];
+	while(s != nil && (s[0] == ' ' || s[0] == '\t'))
+		s = s[1:];
+	if(s != nil)
+		return s;
+	return "failed";
+}
+
+runhashpling(ctxt: ref Context, fd: ref Sys->FD,
+		path: string, argv: list of ref Listnode, last: int): string
+{
+	header := array[1024] of byte;
+	n := sys->read(fd, header, len header);
+	for (i := 0; i < n; i++)
+		if (header[i] == byte '\n')
+			break;
+	if (i == n || i < 3 || header[0] != byte('#') || header[1] != byte('!')) {
+		diagnostic(ctxt, "bad script header on " + path);
+		return "bad header";
+	}
+	(nil, args) := sys->tokenize(string header[2:i], " \t");
+	if (args == nil) {
+		diagnostic(ctxt, "empty header on " + path);
+		return "bad header";
+	}
+	header = nil;
+	fd = nil;
+	nargs: list of ref Listnode;
+	for (; args != nil; args = tl args)
+		nargs = ref Listnode(nil, hd args) :: nargs;
+	nargs = ref Listnode(nil, path) :: nargs;
+	for (; argv != nil; argv = tl argv)
+		nargs = hd argv :: nargs;
+	return runexternal(ctxt, revlist(nargs), last);
+}
+
+runblock(ctxt: ref Context, args: list of ref Listnode, last: int): string
+{
+	# block execute (we know that hd args represents a block)
+	cmd := (hd args).cmd;
+	if (cmd == nil) {
+		# parse block from first argument
+		lex := YYLEX.initstring((hd args).word);
+
+		err: string;
+		(cmd, err) = doparse(lex, "", 0);
+		if (cmd == nil)
+			ctxt.fail("parse error", "sh: "+err);
+
+		(hd args).cmd = cmd;
+	}
+	# now we've got a parsed block
+	ctxt.push();
+	{
+		ctxt.setlocal("0", hd args :: nil);
+		ctxt.setlocal("*", tl args);
+		if (cmd != nil && cmd.ntype == n_BLOCK)
+			cmd = cmd.left;
+		status := walk(ctxt, cmd, last);
+		ctxt.pop();
+		return status;
+	} exception {
+	"fail:*" =>
+		ctxt.pop();
+		raise;
+	}
+}
+
+# return (ok, val) where ok is non-zero is builtin was found,
+# val is return status of builtin
+trybuiltin(ctxt: ref Context, args: list of ref Listnode, lseq: int)
+		: (int, string)
+{
+	(nil, bmods) := findbuiltin(ctxt.env.builtins, (hd args).word);
+	if (bmods == nil)
+		return (0, nil);
+	return (1, (hd bmods)->runbuiltin(ctxt, myself, args, lseq));
+}
+
+keepfdstr(ctxt: ref Context): string
+{
+	s := "";
+	for (f := ctxt.keepfds; f != nil; f = tl f) {
+		s += string hd f;
+		if (tl f != nil)
+			s += ",";
+	}
+	return s;
+}
+
+externalexec(mod: Command,
+		drawcontext: ref Draw->Context, argv: list of string, startchan: chan of int, keepfds: list of int)
+{
+	if (DEBUG) debug(sprint("externalexec(%s,... [%d args])", hd argv, len argv));
+	sys->pctl(Sys->NEWFD, keepfds);
+	startchan <-= sys->pctl(0, nil);
+	{
+		mod->init(drawcontext, argv);
+	}
+	exception {
+	EPIPE =>
+		raise "fail:" + EPIPE;
+	}
+}
+
+dup(ctxt: ref Context, fd1, fd2: int): int
+{
+	# shuffle waitfd out of the way if it's being attacked
+	if (ctxt.waitfd.fd == fd2) {
+		ctxt.waitfd = waitfd();
+		if (ctxt.waitfd.fd == fd2)
+			panic(sys->sprint("reopen of waitfd gave same fd (%d)", ctxt.waitfd.fd));
+	}
+	return sys->dup(fd1, fd2);
+}
+
+# with thanks to tiny/sh.b
+# return error status if redirs failed
+doredirs(ctxt: ref Context, redirs: ref Redirlist): list of int
+{
+	if (redirs.r == nil)
+		return nil;
+	keepfds := ctxt.keepfds;
+	rl := redirs.r;
+	redirs = nil;
+	for (; rl != nil; rl = tl rl) {
+		(rfd, path, (mode, fd1, fd2)) := hd rl;
+		if (path == nil && rfd == nil) {
+			# dup
+			if (fd1 == -1 || fd2 == -1)
+				ctxt.fail("bad redir", "sh: invalid dup");
+
+			if (dup(ctxt, fd2, fd1) == -1)
+				ctxt.fail("bad redir", sys->sprint("sh: cannot dup: %r"));
+			keepfds = fd1 :: keepfds;
+			continue;
+		}
+		# redir
+		if (fd1 == -1) {
+			if ((mode & OMASK) == Sys->OWRITE)
+				fd1 = 1;
+			else
+				fd1 = 0;
+		}
+		if (rfd == nil) {
+			(append, omode) := (mode & OAPPEND, mode & ~OAPPEND);
+			err := "";
+			case mode {
+			Sys->OREAD =>
+				rfd = sys->open(path, omode);
+			Sys->OWRITE | OAPPEND or
+			Sys->ORDWR =>
+				rfd = sys->open(path, omode);
+				err = sprint("%r");
+				if (rfd == nil && nonexistent(err)) {
+					rfd = sys->create(path, omode, 8r666);
+					err = nil;
+				}
+			Sys->OWRITE =>
+				rfd = sys->create(path, omode, 8r666);
+				err = sprint("%r");
+				if (rfd == nil && err == EPERM) {
+					# try open; can't create on a file2chan (pipe)
+					rfd = sys->open(path, omode);
+					nerr := sprint("%r");
+					if(!nonexistent(nerr))
+						err = nerr;
+				}
+			}
+			if (rfd == nil) {
+				if (err == nil)
+					err = sprint("%r");
+				ctxt.fail("bad redir", sys->sprint("sh: cannot open %s: %s", path, err));
+			}
+			if (append)
+				sys->seek(rfd, big 0, Sys->SEEKEND);	# not good enough, but alright for some purposes.
+		}
+		# XXX what happens if rfd.fd == fd1?
+		# it probably gets closed automatically... which is not what we want!
+		dup(ctxt, rfd.fd, fd1);
+		keepfds = fd1 :: keepfds;
+	}
+	ctxt.keepfds = keepfds;
+	return ctxt.waitfd.fd :: keepfds;
+}
+
+#
+# waiter utility routines
+#
+
+waitfd(): ref Sys->FD
+{
+	wf := string sys->pctl(0, nil) + "/wait";
+	waitfd := sys->open("#p/"+wf, Sys->OREAD);
+	if (waitfd == nil)
+		waitfd = sys->open("/prog/"+wf, Sys->OREAD);
+	if (waitfd == nil)
+		panic(sys->sprint("cannot open wait file: %r"));
+	return waitfd;
+}
+
+waitfor(ctxt: ref Context, pids: list of int): string
+{
+	if (pids == nil)
+		return nil;
+	status := array[len pids] of string;
+	wcount := len status;
+	buf := array[Sys->WAITLEN] of byte;
+	onebad := 0;
+	for(;;){
+		n := sys->read(ctxt.waitfd, buf, len buf);
+		if(n < 0)
+			panic(sys->sprint("error on wait read: %r"));
+		(who, line, s) := parsewaitstatus(ctxt, string buf[0:n]);
+		if (s != nil) {
+			if (len s >= 5 && s[0:5] == "fail:")
+				s = failurestatus(s);
+			else
+				diagnostic(ctxt, line);
+		}
+		for ((i, pl) := (0, pids); pl != nil; (i, pl) = (i+1, tl pl))
+			if (who == hd pl)
+				break;
+		if (i < len status) {
+			# wait returns two records for a killed process...
+			if (status[i] == nil || s != "killed") {
+				onebad += s != nil;
+				status[i] = s;
+				if (wcount-- <= 1)
+					break;
+			}
+		}
+	}
+	if (!onebad)
+		return nil;
+	r := status[len status - 1];
+	for (i := len status - 2; i >= 0; i--)
+		r += "|" + status[i];
+	return r;
+}
+
+parsewaitstatus(ctxt: ref Context, status: string): (int, string, string)
+{
+	for (i := 0; i < len status; i++)
+		if (status[i] == ' ')
+			break;
+	if (i == len status - 1 || status[i+1] != '"')
+		ctxt.fail("bad wait read",
+			sys->sprint("sh: bad exit status '%s'", status));
+
+	for (i+=2; i < len status; i++)
+		if (status[i] == '"')
+			break;
+	if (i > len status - 2 || status[i+1] != ':')
+		ctxt.fail("bad wait read",
+			sys->sprint("sh: bad exit status '%s'", status));
+
+	return (int status, status, status[i+2:]);
+}
+
+panic(s: string)
+{
+	sys->fprint(stderr(), "sh panic: %s\n", s);
+	raise "panic";
+}
+
+diagnostic(ctxt: ref Context, s: string)
+{
+	if (ctxt.options() & Context.VERBOSE)
+		sys->fprint(stderr(), "sh: %s\n", s);
+}
+
+#
+# Sh environment stuff
+#
+
+Context.new(drawcontext: ref Draw->Context): ref Context
+{
+	initialise();
+	if (env != nil)
+		env->clone();
+	ctxt := ref Context(
+		ref Environment(
+			ref Builtins(nil, 0),
+			ref Builtins(nil, 0),
+			nil,
+			newlocalenv(nil)
+		),
+		waitfd(),
+		drawcontext,
+		0 :: 1 :: 2 :: nil
+	);
+	myselfbuiltin->initbuiltin(ctxt, myself);
+	ctxt.env.localenv.flags = ctxt.VERBOSE;
+	for (vl := ctxt.get("autoload"); vl != nil; vl = tl vl)
+		if ((hd vl).cmd == nil && (hd vl).word != nil)
+			loadmodule(ctxt, (hd vl).word);
+	return ctxt;
+}
+
+Context.copy(ctxt: self ref Context, copyenv: int): ref Context
+{
+	# XXX could check to see that we are definitely in a
+	# new process, because there'll be problems if not (two processes
+	# simultaneously reading the same wait file)
+	nctxt := ref Context(ctxt.env, waitfd(), ctxt.drawcontext, ctxt.keepfds);
+			
+	if (copyenv) {
+		if (env != nil)
+			env->clone();
+		nctxt.env = ref Environment(
+			copybuiltins(ctxt.env.sbuiltins),
+			copybuiltins(ctxt.env.builtins),
+			ctxt.env.bmods,
+			copylocalenv(ctxt.env.localenv)
+		);
+	}
+	return nctxt;
+}
+
+Context.set(ctxt: self ref Context, name: string, val: list of ref Listnode)
+{
+	e := ctxt.env.localenv;
+	idx := hashfn(name, len e.vars);
+	for (;;) {
+		v := hashfind(e.vars, idx, name);
+		if (v == nil) {
+			if (e.pushed == nil) {
+				flags := Var.CHANGED;
+				if (noexport(name))
+					flags |= Var.NOEXPORT;
+				hashadd(e.vars, idx, ref Var(name, val, flags));
+				return;
+			}
+		} else {
+			v.val = val;
+			v.flags |= Var.CHANGED;
+			return;
+		}
+		e = e.pushed;
+	}
+}
+
+Context.get(ctxt: self ref Context, name: string): list of ref Listnode
+{
+	if (name == nil)
+		return nil;
+
+	idx := -1;
+	# cope with $1, $2, etc
+	if (name[0] > '0' && name[0] <= '9') {
+		i: int;
+		for (i = 0; i < len name; i++)
+			if (name[i] < '0' || name[i] > '9')
+				break;
+		if (i >= len name) {
+			idx = int name - 1;
+			name = "*";
+		}
+	}
+
+	v := varfind(ctxt.env.localenv, name);
+	if (v != nil) {
+		if (idx != -1)
+			return index(v.val, idx);
+		return v.val;
+	}
+	return nil;
+}
+
+# return the whole environment.
+Context.envlist(ctxt: self ref Context): list of (string, list of ref Listnode)
+{
+	t := array[ENVHASHSIZE] of list of ref Var;
+	for (e := ctxt.env.localenv; e != nil; e = e.pushed) {
+		for (i := 0; i < len e.vars; i++) {
+			for (vl := e.vars[i]; vl != nil; vl = tl vl) {
+				v := hd vl;
+				idx := hashfn(v.name, len e.vars);
+				if (hashfind(t, idx, v.name) == nil)
+					hashadd(t, idx, v);
+			}
+		}
+	}
+
+	l: list of (string, list of ref Listnode);
+	for (i := 0; i < ENVHASHSIZE; i++) {
+		for (vl := t[i]; vl != nil; vl = tl vl) {
+			v := hd vl;
+			l = (v.name, v.val) :: l;
+		}
+	}
+	return l;
+}
+
+Context.setlocal(ctxt: self ref Context, name: string, val: list of ref Listnode)
+{
+	e := ctxt.env.localenv;
+	idx := hashfn(name, len e.vars);
+	v := hashfind(e.vars, idx, name);
+	if (v == nil) {
+		flags := Var.CHANGED;
+		if (noexport(name))
+			flags |= Var.NOEXPORT;
+		hashadd(e.vars, idx, ref Var(name, val, flags));
+	} else {
+		v.val = val;
+		v.flags |= Var.CHANGED;
+	}
+}
+
+
+Context.push(ctxt: self ref Context)
+{
+	ctxt.env.localenv = newlocalenv(ctxt.env.localenv);
+}
+
+Context.pop(ctxt: self ref Context)
+{
+	if (ctxt.env.localenv.pushed == nil)
+		panic("unbalanced contexts in shell environment");
+	else {
+		oldv := ctxt.env.localenv.vars;
+		ctxt.env.localenv = ctxt.env.localenv.pushed;
+		for (i := 0; i < len oldv; i++) {
+			for (vl := oldv[i]; vl != nil; vl = tl vl) {
+				if ((v := varfind(ctxt.env.localenv, (hd vl).name)) != nil)
+					v.flags |= Var.CHANGED;
+				else
+					ctxt.set((hd vl).name, nil);
+			}
+		}
+	}
+}
+
+Context.run(ctxt: self ref Context, args: list of ref Listnode, last: int): string
+{
+	if (args == nil || ((hd args).cmd == nil && (hd args).word == nil))
+		return nil;
+	cmd := hd args;
+	if (cmd.cmd != nil || cmd.word[0] == '{')	# }
+		return runblock(ctxt, args, last);
+
+	if (ctxt.options() & ctxt.EXECPRINT)
+		sys->fprint(stderr(), "%s\n", quoted(args, 0));
+	(doneit, status) := trybuiltin(ctxt, args, last);
+	if (!doneit)
+		status = runexternal(ctxt, args, last);
+
+	return status;
+}
+
+Context.addmodule(ctxt: self ref Context, name: string, mod: Shellbuiltin)
+{
+	mod->initbuiltin(ctxt, myself);
+	ctxt.env.bmods = (name, mod->getself()) :: ctxt.env.bmods;
+}
+
+Context.addbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
+{
+	addbuiltin(c.env.builtins, name, mod);
+}
+
+Context.removebuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
+{
+	removebuiltin(c.env.builtins, name, mod);
+}
+
+Context.addsbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
+{
+	addbuiltin(c.env.sbuiltins, name, mod);
+}
+
+Context.removesbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
+{
+	removebuiltin(c.env.sbuiltins, name, mod);
+}
+
+varfind(e: ref Localenv, name: string): ref Var
+{
+	idx := hashfn(name, len e.vars);
+	for (; e != nil; e = e.pushed)
+		for (vl := e.vars[idx]; vl != nil; vl = tl vl)
+			if ((hd vl).name == name)
+				return hd vl;
+	return nil;
+}
+
+Context.fail(ctxt: self ref Context, ename: string, err: string)
+{
+	if (ctxt.options() & Context.VERBOSE)
+		sys->fprint(stderr(), "%s\n", err);
+	raise "fail:" + ename;
+}
+
+Context.setoptions(ctxt: self ref Context, flags, on: int): int
+{
+	old := ctxt.env.localenv.flags;
+	if (on)
+		ctxt.env.localenv.flags |= flags;
+	else
+		ctxt.env.localenv.flags &= ~flags;
+	return old;
+}
+
+Context.options(ctxt: self ref Context): int
+{
+	return ctxt.env.localenv.flags;
+}
+
+hashfn(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
+
+# the following two functions cheat by getting the caller
+# to calculate the actual hash function. this is to avoid
+# the hash function being calculated once in every scope
+# of a context until the variable is found (or stored).
+hashfind(ht: array of list of ref Var, idx: int, n: string): ref Var
+{
+	for (ent := ht[idx]; ent != nil; ent = tl ent)
+		if ((hd ent).name == n)
+			return hd ent;
+	return nil;
+}
+
+hashadd(ht: array of list of ref Var, idx: int, v: ref Var)
+{
+	ht[idx] = v :: ht[idx];
+}
+
+copylocalenv(e: ref Localenv): ref Localenv
+{
+	nvars := array[len e.vars] of list of ref Var;
+	flags := e.flags;
+	for (; e != nil; e = e.pushed)
+		for (i := 0; i < len nvars; i++)
+			for (vl := e.vars[i]; vl != nil; vl = tl vl) {
+				idx := hashfn((hd vl).name, len nvars);
+				if (hashfind(nvars, idx, (hd vl).name) == nil)
+					hashadd(nvars, idx, ref *(hd vl));
+			}
+	return ref Localenv(nvars, nil, flags);
+}
+
+# make new local environment. if it's got no pushed levels,
+# then get all variables from the global environment.
+newlocalenv(pushed: ref Localenv): ref Localenv
+{
+	e := ref Localenv(array[ENVHASHSIZE] of list of ref Var, pushed, 0);
+	if (pushed == nil && env != nil) {
+		for (vl := env->getall(); vl != nil; vl = tl vl) {
+			(name, val) := hd vl;
+			hashadd(e.vars, hashfn(name, len e.vars), ref Var(name, envstringtoval(val), 0));
+		}
+	}
+	if (pushed != nil)
+		e.flags = pushed.flags;
+	return e;
+}
+
+copybuiltins(b: ref Builtins): ref Builtins
+{
+	nb := ref Builtins(array[b.n] of (string, list of Shellbuiltin), b.n);
+	nb.ba[0:] = b.ba[0:b.n];
+	return nb;
+}
+
+findbuiltin(b: ref Builtins, name: string): (int, list of Shellbuiltin)
+{
+	lo := 0;
+	hi := b.n - 1;
+	while (lo <= hi) {
+		mid := (lo + hi) / 2;
+		(bname, bmod) := b.ba[mid];
+		if (name < bname)
+			hi = mid - 1;
+		else if (name > bname)
+			lo = mid + 1;
+		else
+			return (mid, bmod);
+	}
+	return (lo, nil);
+}
+
+removebuiltin(b: ref Builtins, name: string, mod: Shellbuiltin)
+{
+	(n, bmods) := findbuiltin(b, name);
+	if (bmods == nil)
+		return;
+	if (hd bmods == mod) {
+		if (tl bmods != nil)
+			b.ba[n] = (name, tl bmods);
+		else {
+			b.ba[n:] = b.ba[n+1:b.n];
+			b.ba[--b.n] = (nil, nil);
+		}
+	}
+}
+
+# add builtin; if it already exists, then replace it. if mod is nil then remove it.
+# builtins that refer to myselfbuiltin are special - they
+# are never removed, neither are they entirely replaced, only covered.
+# no external module can redefine the name "builtin"
+addbuiltin(b: ref Builtins, name: string, mod: Shellbuiltin)
+{
+	if (mod == nil || (name == "builtin" && mod != myselfbuiltin))
+		return;
+	(n, bmods) := findbuiltin(b, name);
+	if (bmods != nil) {
+		if (hd bmods == myselfbuiltin)
+			b.ba[n] = (name, mod :: bmods);
+		else
+			b.ba[n] = (name, mod :: nil);
+	} else {
+		if (b.n == len b.ba) {
+			nb := array[b.n + 10] of (string, list of Shellbuiltin);
+			nb[0:] = b.ba[0:b.n];
+			b.ba = nb;
+		}
+		b.ba[n+1:] = b.ba[n:b.n];
+		b.ba[n] = (name, mod :: nil);
+		b.n++;
+	}
+}
+
+removebuiltinmod(b: ref Builtins, mod: Shellbuiltin)
+{
+	j := 0;
+	for (i := 0; i < b.n; i++) {
+		(name, bmods) := b.ba[i];
+		if (hd bmods == mod)
+			bmods = tl bmods;
+		if (bmods != nil)
+			b.ba[j++] = (name, bmods);
+	}
+	b.n = j;
+	for (; j < i; j++)
+		b.ba[j] = (nil, nil);
+}
+
+export(e: ref Localenv)
+{
+	if (env == nil)
+		return;
+	if (e.pushed != nil)
+		export(e.pushed);
+
+	for (i := 0; i < len e.vars; i++) {
+		for (vl := e.vars[i]; vl != nil; vl = tl vl) {
+			v := hd vl;
+			# a bit inefficient: a local variable will get several putenvs.
+			if ((v.flags & Var.CHANGED) && !(v.flags & Var.NOEXPORT)) {
+				setenv(v.name, v.val);
+				v.flags &= ~Var.CHANGED;
+			}
+		}
+	}
+}
+
+noexport(name: string): int
+{
+	case name {
+		"0" or "*" or "status" => return 1;
+	}
+	return 0;
+}
+
+index(val: list of ref Listnode, k: int): list of ref Listnode
+{
+	for (; k > 0 && val != nil; k--)
+		val = tl val;
+	if (val != nil)
+		val = hd val :: nil;
+	return val;
+}
+
+getenv(name: string): list of ref Listnode
+{
+	if (env == nil)
+		return nil;
+	return envstringtoval(env->getenv(name));
+}
+
+envstringtoval(v: string): list of ref Listnode
+{
+	return stringlist2list(str->unquoted(v));
+}
+
+XXXenvstringtoval(v: string): list of ref Listnode
+{
+	if (len v == 0)
+		return nil;
+	start := len v;
+	val: list of ref Listnode;
+	for (i := start - 1; i >= 0; i--) {
+		if (v[i] == ENVSEP) {
+			val = ref Listnode(nil, v[i+1:start]) :: val;
+			start = i;
+		}
+	}
+	return ref Listnode(nil, v[0:start]) :: val;
+}
+
+setenv(name: string, val: list of ref Listnode)
+{
+	if (env == nil)
+		return;
+	env->setenv(name, quoted(val, 1));
+}
+
+#
+# globbing and general wildcard handling
+#
+
+containswildchar(s: string): int
+{
+	# try and avoid being fooled by GLOB characters in quoted
+	# text. we'll only be fooled if the GLOB char is followed
+	# by a wildcard char, or another GLOB.
+	for (i := 0; i < len s; i++) {
+		if (s[i] == GLOB && i < len s - 1) {
+			case s[i+1] {
+			'*' or '[' or '?' or GLOB =>
+				return 1;
+			}
+		}
+	}
+	return 0;
+}
+
+# remove GLOBs, and quote other wildcard characters
+patquote(word: string): string
+{
+	outword := "";
+	for (i := 0; i < len word; i++) {
+		case word[i] {
+		'[' or '*' or '?' or '\\' =>
+			outword[len outword] = '\\';
+		GLOB =>
+			i++;
+			if (i >= len word)
+				return outword;
+			if(word[i] == '[' && i < len word - 1 && word[i+1] == '~')
+				word[i+1] = '^';
+		}
+		outword[len outword] = word[i];
+	}
+	return outword;
+}
+
+# get rid of GLOB characters
+deglob(s: string): string
+{
+	j := 0;
+	for (i := 0; i < len s; i++) {
+		if (s[i] != GLOB) {
+			if (i != j)		# a worthy optimisation???
+				s[j] = s[i];
+			j++;
+		}
+	}
+	if (i == j)
+		return s;
+	return s[0:j];
+}
+
+# expand wildcards in _nl_
+glob(nl: list of ref Listnode): list of ref Listnode
+{
+	new: list of ref Listnode;
+	while (nl != nil) {
+		n := hd nl;
+		if (containswildchar(n.word)) {
+			qword := patquote(n.word);
+			files := filepat->expand(qword);
+			if (files == nil)
+				files = deglob(n.word) :: nil;
+			while (files != nil) {
+				new = ref Listnode(nil, hd files) :: new;
+				files = tl files;
+			}
+		} else
+			new = n :: new;
+		nl = tl nl;
+	}
+	ret := revlist(new);
+	return ret;
+}
+
+#
+# general list manipulation utility routines
+#
+
+# return string equivalent of nl
+list2stringlist(nl: list of ref Listnode): list of string
+{
+	ret: list of string = nil;
+
+	while (nl != nil) {
+		newel: string;
+		el := hd nl;
+		if (el.word != nil || el.cmd == nil)
+			newel = el.word;
+		else
+			el.word = newel = cmd2string(el.cmd);
+		ret = newel::ret;
+		nl = tl nl;
+	}
+
+	sl := revstringlist(ret);
+	return sl;
+}
+
+stringlist2list(sl: list of string): list of ref Listnode
+{
+	ret: list of ref Listnode;
+
+	while (sl != nil) {
+		ret = ref Listnode(nil, hd sl) :: ret;
+		sl = tl sl;
+	}
+	return revlist(ret);
+}
+
+revstringlist(l: list of string): list of string
+{
+	t: list of string;
+
+	while(l != nil) {
+		t = hd l :: t;
+		l = tl l;
+	}
+	return t;
+}
+
+revlist(l: list of ref Listnode): list of ref Listnode
+{
+	t: list of ref Listnode;
+
+	while(l != nil) {
+		t = hd l :: t;
+		l = tl l;
+	}
+	return t;
+}
+
+#
+# node to string conversion functions
+#
+
+fdassignstr(isassign: int, redir: ref Redir): string
+{
+	l: string = nil;
+	if (redir.fd1 >= 0)
+		l = string redir.fd1;
+	
+	if (isassign) {
+		r: string = nil;
+		if (redir.fd2 >= 0)
+			r = string redir.fd2;
+		return "[" + l + "=" + r + "]";
+	}
+	return "[" + l + "]";
+}
+
+redirstr(rtype: int): string
+{
+	case rtype {
+	* or
+	Sys->OREAD =>	return "<";
+	Sys->OWRITE =>	return ">";
+	Sys->OWRITE|OAPPEND =>	return ">>";
+	Sys->ORDWR =>	return "<>";
+	}
+}
+
+cmd2string(n: ref Node): string
+{
+	if (n == nil)
+		return "";
+
+	s: string;
+	case n.ntype {
+	n_BLOCK =>	s = "{" + cmd2string(n.left) + "}";
+	n_VAR =>		s = "$" + cmd2string(n.left);
+				# XXX can this ever occur?
+				if (n.right != nil)
+					s += "(" + cmd2string(n.right) + ")";
+	n_SQUASH =>	s = "$\"" + cmd2string(n.left);
+	n_COUNT =>	s = "$#" + cmd2string(n.left);
+	n_BQ =>		s = "`" + cmd2string(n.left);
+	n_BQ2 =>		s = "\"" + cmd2string(n.left);
+	n_REDIR =>	s = redirstr(n.redir.rtype);
+				if (n.redir.fd1 != -1)
+					s += fdassignstr(0, n.redir);
+				s += cmd2string(n.left);
+	n_DUP =>		s = redirstr(n.redir.rtype) + fdassignstr(1, n.redir);
+	n_LIST =>		s = "(" + cmd2string(n.left) + ")";
+	n_SEQ =>		s = cmd2string(n.left) + ";" + cmd2string(n.right);
+	n_NOWAIT =>	s = cmd2string(n.left) + "&";
+	n_CONCAT =>	s = cmd2string(n.left) + "^" + cmd2string(n.right);
+	n_PIPE =>		s = cmd2string(n.left) + "|";
+				if (n.redir != nil && (n.redir.fd1 != -1 || n.redir.fd2 != -1))
+					s += fdassignstr(n.redir.fd2 != -1, n.redir);
+				s += cmd2string(n.right);
+	n_ASSIGN =>	s = cmd2string(n.left) + "=" + cmd2string(n.right);
+	n_LOCAL =>	s = cmd2string(n.left) + ":=" + cmd2string(n.right);
+	n_ADJ =>		s = cmd2string(n.left) + " " + cmd2string(n.right);
+	n_WORD =>	s = quote(n.word, 1);
+	* =>			s = sys->sprint("unknown%d", n.ntype);
+	}
+	return s;
+}
+
+# convert s into a suitable format for reparsing.
+# if glob is true, then GLOB chars are significant.
+# XXX it might be faster in the more usual cases 
+# to run through the string first and only build up
+# a new string once we've discovered it's necessary.
+quote(s: string, glob: int): string
+{
+	needquote := 0;
+	t := "";
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'{' or '}' or '(' or ')' or '`' or '&' or ';' or '=' or '>' or '<' or '#' or
+		'|' or '*' or '[' or '?' or '$' or '^' or ' ' or '\t' or '\n' or '\r' =>
+			needquote = 1;
+		'\'' =>
+			t[len t] = '\'';
+			needquote = 1;
+		GLOB =>
+			if (glob) {
+				if (i < len s - 1)
+					i++;
+			}
+		}
+		t[len t] = s[i];
+	}
+	if (needquote || t == nil)
+		t = "'" + t + "'";
+	return t;
+}
+
+squash(l: list of string, sep: string): string
+{
+	if (l == nil)
+		return nil;
+	s := hd l;
+	for (l = tl l; l != nil; l = tl l)
+		s += sep + hd l;
+	return s;
+}
+
+debug(s: string)
+{
+	if (DEBUG) sys->fprint(stderr(), "%s\n", string sys->pctl(0, nil) + ": " + s);
+}
+
+#
+# built-in commands
+#
+
+initbuiltin(c: ref Context, nil: Sh): string
+{
+	names := array[] of {"load", "unload", "loaded", "builtin", "syncenv", "whatis", "run", "exit", "@"};
+	for (i := 0; i < len names; i++)
+		c.addbuiltin(names[i], myselfbuiltin);
+	c.addsbuiltin("loaded", myselfbuiltin);
+	c.addsbuiltin("quote", myselfbuiltin);
+	c.addsbuiltin("bquote", myselfbuiltin);
+	c.addsbuiltin("unquote", myselfbuiltin);
+	c.addsbuiltin("builtin", myselfbuiltin);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+runsbuiltin(ctxt: ref Context, nil: Sh, argv: list of ref Listnode): list of ref Listnode
+{
+	case (hd argv).word {
+	"loaded" =>	return sbuiltin_loaded(ctxt, argv);
+	"bquote" =>	return sbuiltin_quote(ctxt, argv, 0);
+	"quote" =>	return sbuiltin_quote(ctxt, argv, 1);
+	"unquote" =>	return sbuiltin_unquote(ctxt, argv);
+	"builtin" =>	return sbuiltin_builtin(ctxt, argv);
+	}
+	return nil;
+}
+
+runbuiltin(ctxt: ref Context, nil: Sh, args: list of ref Listnode, lseq: int): string
+{
+	status := "";
+	name := (hd args).word;
+	case name {
+	"load" =>		status = builtin_load(ctxt, args, lseq);
+	"loaded" =>	status = builtin_loaded(ctxt, args, lseq);
+	"unload" =>	status = builtin_unload(ctxt, args, lseq);
+	"builtin" =>	status = builtin_builtin(ctxt, args, lseq);
+	"whatis" =>	status = builtin_whatis(ctxt, args, lseq);
+	"run" =>		status = builtin_run(ctxt, args, lseq);
+	"exit" =>		status = builtin_exit(ctxt, args, lseq);
+	"syncenv" =>	export(ctxt.env.localenv);
+	"@" =>		status = builtin_subsh(ctxt, args, lseq);
+	}
+	return status;
+}
+
+sbuiltin_loaded(ctxt: ref Context, nil: list of ref Listnode): list of ref Listnode
+{
+	v: list of ref Listnode;
+	for (bl := ctxt.env.bmods; bl != nil; bl = tl bl) {
+		(name, nil) := hd bl;
+		v = ref Listnode(nil, name) :: v;
+	}
+	return v;
+}
+
+sbuiltin_quote(nil: ref Context, argv: list of ref Listnode, quoteblocks: int): list of ref Listnode
+{
+	return ref Listnode(nil, quoted(tl argv, quoteblocks)) :: nil;
+}
+
+sbuiltin_builtin(ctxt: ref Context, args: list of ref Listnode): list of ref Listnode
+{
+	if (args == nil || tl args == nil)
+		builtinusage(ctxt, "builtin command [args ...]");
+	name := (hd tl args).word;
+	(nil, mods) := findbuiltin(ctxt.env.sbuiltins, name);
+	for (; mods != nil; mods = tl mods)
+		if (hd mods == myselfbuiltin)
+			return (hd mods)->runsbuiltin(ctxt, myself, tl args);
+	ctxt.fail("builtin not found", sys->sprint("sh: builtin %s not found", name));
+	return nil;
+}
+
+sbuiltin_unquote(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	argv = tl argv;
+	if (argv == nil || tl argv != nil)
+		builtinusage(ctxt, "unquote arg");
+	
+	arg := (hd argv).word;
+	if (arg == nil && (hd argv).cmd != nil)
+		arg = cmd2string((hd argv).cmd);
+	return stringlist2list(str->unquoted(arg));
+}
+
+getself(): Shellbuiltin
+{
+	return myselfbuiltin;
+}
+
+builtinusage(ctxt: ref Context, s: string)
+{
+	ctxt.fail("usage", "sh: usage: " + s);
+}
+
+builtin_exit(nil: ref Context, nil: list of ref Listnode, nil: int): string
+{
+	# XXX using this primitive can cause
+	# environment stack not to be popped properly.
+	exit;
+}
+
+builtin_subsh(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args == nil)
+		return nil;
+	startchan := chan of (int, ref Expropagate);
+	spawn runasync(ctxt, 0, tl args, ref Redirlist, startchan);
+	(exepid, exprop) := <-startchan;
+	status := waitfor(ctxt, exepid :: nil);
+	if (exprop.name != nil)
+		raise exprop.name;
+	return status;
+}
+
+builtin_loaded(ctxt: ref Context, nil: list of ref Listnode, nil: int): string
+{
+	b := ctxt.env.builtins;
+	for (i := 0; i < b.n; i++) {
+		(name, bmods) := b.ba[i];
+		sys->print("%s\t%s\n", name, modname(ctxt, hd bmods));
+	}
+	b = ctxt.env.sbuiltins;
+	for (i = 0; i < b.n; i++) {
+		(name, bmods) := b.ba[i];
+		sys->print("${%s}\t%s\n", name, modname(ctxt, hd bmods));
+	}
+	return nil;
+}
+
+# it's debateable whether this should throw an exception or
+# return a failed exit status - however, most scripts don't
+# check the status and do need the module they're loading,
+# so i think the exception is probably more useful...
+builtin_load(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args == nil || (hd tl args).word == nil)
+		builtinusage(ctxt, "load path...");
+	args = tl args;
+	if (args == nil)
+		builtinusage(ctxt, "load path...");
+	for (; args != nil; args = tl args) {
+		s := loadmodule(ctxt, (hd args).word);
+		if (s != nil)
+			raise "fail:" + s;
+	}
+	return nil;
+}
+
+builtin_unload(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args == nil)
+		builtinusage(ctxt, "unload path...");
+	status := "";
+	for (args = tl args; args != nil; args = tl args)
+		if ((s := unloadmodule(ctxt, (hd args).word)) != nil)
+			status = s;
+	return status;
+}
+
+builtin_run(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args == nil || (hd tl args).word == nil)
+		builtinusage(ctxt, "run path");
+	ctxt.push();
+	{
+		ctxt.setoptions(ctxt.INTERACTIVE, 0);
+		runscript(ctxt, (hd tl args).word, tl tl args, 1);
+		ctxt.pop();
+		return nil;
+	} exception e {
+	"fail:*" =>
+		ctxt.pop();
+		return failurestatus(e);
+	}
+}
+
+# four categories:
+# environment variables
+# substitution builtins
+# braced blocks
+# builtins (including those defined by externally loaded modules)
+# or external programs
+# other
+builtin_whatis(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (len args < 2)
+		builtinusage(ctxt, "whatis name ...");
+	err := "";
+	for (args = tl args; args != nil; args = tl args)
+		if ((e := whatisit(ctxt, hd args)) != nil)
+			err = e;
+	return err;
+}
+
+whatisit(ctxt: ref Context, el: ref Listnode): string
+{
+	if (el.cmd != nil) {
+		sys->print("%s\n", cmd2string(el.cmd));
+		return nil;
+	}
+	found := 0;
+	name := el.word;
+	if (name != nil && name[0] == '{') {	#}
+		sys->print("%s\n", name);
+		return nil;;
+	}
+	if (name == nil)
+		return nil;		# XXX questionable
+	w: string;
+	val := ctxt.get(name);
+	if (val != nil) {
+		found++;
+		w += sys->sprint("%s=%s\n", quote(name, 0), quoted(val, 0));
+	}
+	(nil, mods) := findbuiltin(ctxt.env.sbuiltins, name);
+	if (mods != nil) {
+		mod := hd mods;
+		if (mod == myselfbuiltin)
+			w += "${builtin " + name + "}\n";
+		else {
+			mw := mod->whatis(ctxt, myself, name, Shellbuiltin->SBUILTIN);
+			if (mw == nil)
+				mw = "${" + name + "}";
+			w += "load " + modname(ctxt, mod) + "; " + mw + "\n";
+		}
+		found++;
+	}
+	(nil, mods) = findbuiltin(ctxt.env.builtins, name);
+	if (mods != nil) {
+		mod := hd mods;
+		if (mod == myselfbuiltin)
+			sys->print("builtin %s\n", name);
+		else {
+			mw := mod->whatis(ctxt, myself, name, Shellbuiltin->BUILTIN);
+			if (mw == nil)
+				mw = name;
+			w += "load " + modname(ctxt, mod) + "; " + mw + "\n";
+		}
+		found++;
+	} else {
+		disfile := 0;	
+		if (len name >= 4 && name[len name-4:] == ".dis")
+			disfile = 1;
+		pathlist: list of string;
+		if (len name >= 2 && (name[0] == '/' || name[0:2] == "./"))
+			pathlist = list of {""};
+		else if ((pl := ctxt.get("path")) != nil)
+			pathlist = list2stringlist(pl);
+		else
+			pathlist = list of {"/dis", "."};
+	
+		foundpath := "";
+		while (pathlist != nil) {
+			path: string;
+			if (hd pathlist != "")
+				path = hd pathlist + "/" + name;
+			else
+				path = name;
+			if (!disfile && (fd := sys->open(path, Sys->OREAD)) != nil) {
+				if (executable(sys->fstat(fd), 8r111)) {
+					foundpath = path;
+					break;
+				}
+			}
+			if (!disfile)
+				path += ".dis";
+			if (executable(sys->stat(path), 8r444)) {
+				foundpath = path;
+				break;
+			}
+			pathlist = tl pathlist;
+		}
+		if (foundpath != nil)
+			w += foundpath + "\n";
+	}
+	for (bmods := ctxt.env.bmods; bmods != nil; bmods = tl bmods) {
+		(modname, mod) := hd bmods;
+		if ((mw := mod->whatis(ctxt, myself, name, Shellbuiltin->OTHER)) != nil)
+			w += "load " + modname + "; " + mw + "\n";
+	}
+	if (w == nil) {
+		sys->fprint(stderr(), "%s: not found\n", name);
+		return "not found";
+	}
+	sys->print("%s", w);
+	return nil;
+}
+
+# execute a command ignoring names defined by externally defined modules
+builtin_builtin(ctxt: ref Context, args: list of ref Listnode, last: int): string
+{
+	if (len args < 2)
+		builtinusage(ctxt, "builtin command [args ...]");
+	name := (hd tl args).word;
+	if (name == nil || name[0] == '{') {
+		diagnostic(ctxt, name + " not found");
+		return "not found";
+	}
+	(nil, mods) := findbuiltin(ctxt.env.builtins, name);
+	for (; mods != nil; mods = tl mods)
+		if (hd mods == myselfbuiltin)
+			return (hd mods)->runbuiltin(ctxt, myself, tl args, last);
+	if (ctxt.options() & ctxt.EXECPRINT)
+		sys->fprint(stderr(), "%s\n", quoted(tl args, 0));
+	return runexternal(ctxt, tl args, last);
+}
+
+modname(ctxt: ref Context, mod: Shellbuiltin): string
+{
+	for (ml := ctxt.env.bmods; ml != nil; ml = tl ml) {
+		(bname, bmod) := hd ml;
+		if (bmod == mod)
+			return bname;
+	}
+	return "builtin";
+}
+
+loadmodule(ctxt: ref Context, name: string): string
+{
+	# avoid loading the same module twice (it's convenient
+	# to have load be a null-op if the module required is already loaded)
+	for (bl := ctxt.env.bmods; bl != nil; bl = tl bl) {
+		(bname, nil) := hd bl;
+		if (bname == name)
+			return nil;
+	}
+	path := name;
+	if (len path < 4 || path[len path-4:] != ".dis")
+		path += ".dis";
+	if (path[0] != '/' && path[0:2] != "./")
+		path = BUILTINPATH + "/" + path;
+	mod := load Shellbuiltin path;
+	if (mod == nil) {
+		diagnostic(ctxt, sys->sprint("load: cannot load %s: %r", path));
+		return "bad module";
+	}
+	s := mod->initbuiltin(ctxt, myself);
+	ctxt.env.bmods = (name, mod->getself()) :: ctxt.env.bmods;
+	if (s != nil) {
+		unloadmodule(ctxt, name);
+		diagnostic(ctxt, "load: module init failed: " + s);
+	}
+	return s;
+}
+
+unloadmodule(ctxt: ref Context, name: string): string
+{
+	bl: list of (string, Shellbuiltin);
+	mod: Shellbuiltin;
+	for (cl := ctxt.env.bmods; cl != nil; cl = tl cl) {
+		(bname, bmod) := hd cl;
+		if (bname == name)
+			mod = bmod;
+		else
+			bl = hd cl :: bl;
+	}
+	if (mod == nil) {
+		diagnostic(ctxt, sys->sprint("module %s not found", name));
+		return "not found";
+	}
+	for (ctxt.env.bmods = nil; bl != nil; bl = tl bl)
+		ctxt.env.bmods = hd bl :: ctxt.env.bmods;
+	removebuiltinmod(ctxt.env.builtins, mod);
+	removebuiltinmod(ctxt.env.sbuiltins, mod);
+	return nil;
+}
+
+executable(s: (int, Sys->Dir), mode: int): int
+{
+	(ok, info) := s;
+	return ok != -1 && (info.mode & Sys->DMDIR) == 0
+			&& (info.mode & mode) != 0;
+}
+
+quoted(val: list of ref Listnode, quoteblocks: int): string
+{
+	s := "";
+	for (; val != nil; val = tl val) {
+		el := hd val;
+		if (el.cmd == nil || (quoteblocks && el.word != nil))
+			s += quote(el.word, 0);
+		else {
+			cmd := cmd2string(el.cmd);
+			if (quoteblocks)
+				cmd = quote(cmd, 0);
+			s += cmd;
+		}
+		if (tl val != nil)
+			s[len s] = ' ';
+	}
+	return s;
+}
+
+setstatus(ctxt: ref Context, val: string): string
+{
+	ctxt.setlocal("status", ref Listnode(nil, val) :: nil);
+	return val;
+}
+
+#
+# beginning of parser routines
+#
+
+doparse(l: ref YYLEX, prompt: string, showline: int): (ref Node, string)
+{
+	l.prompt = prompt;
+	l.err = nil;
+	l.lval.node = nil;
+	yyparse(l);
+	l.lastnl = 0;		# don't print secondary prompt next time
+	if (l.err != nil) {
+		s: string;
+		if (l.err == nil)
+			l.err = "unknown error";
+		if (l.errline > 0 && showline)
+			s = sys->sprint("%s:%d: %s", l.path, l.errline, l.err);
+		else
+			s = l.path + ": parse error: " + l.err;
+		return (nil, s);
+	}
+	return (l.lval.node, nil);
+}
+
+blanklex: YYLEX;	# for hassle free zero initialisation
+
+YYLEX.initstring(s: string): ref YYLEX
+{
+	ret := ref blanklex;
+	ret.s = s;
+	ret.path="internal";
+	ret.strpos = 0;
+	return ret;
+}
+
+YYLEX.initfile(fd: ref Sys->FD, path: string): ref YYLEX
+{
+	lex := ref blanklex;
+	lex.f = bufio->fopen(fd, bufio->OREAD);
+	lex.path = path;
+	lex.cbuf = array[2] of int;		# number of characters of pushback
+	lex.linenum = 1;
+	lex.prompt = "";
+	return lex;
+}
+
+YYLEX.error(l: self ref YYLEX, s: string)
+{
+	if (l.err == nil) {
+		l.err = s;
+		l.errline = l.linenum;
+	}
+}
+
+NOTOKEN: con -1;
+
+YYLEX.lex(l: self ref YYLEX): int
+{
+	# the following are allowed a free caret:
+	# $, word and quoted word;
+	# also, allowed chrs in unquoted word following dollar are [a-zA-Z0-9*_]
+	endword := 0;
+	wasdollar := 0;
+	tok := NOTOKEN;
+	while (tok == NOTOKEN) {
+		case c := l.getc() {
+		l.EOF =>
+			tok = END;
+		'\n' =>
+			tok = '\n';
+		'\r' or '\t' or ' ' =>
+			;
+		'#' =>
+			while ((c = l.getc()) != '\n' && c != l.EOF)
+				;
+			l.ungetc();
+		';' =>	tok = ';';
+		'&' =>
+			c = l.getc();
+			if(c == '&')
+				tok = ANDAND;
+			else{
+				l.ungetc();
+				tok = '&';
+			}
+		'^' =>	tok = '^';
+		'{' =>	tok = '{';
+		'}' =>	tok = '}';
+		')' =>	tok = ')';
+		'(' => tok = '(';
+		'=' => (tok, l.lval.optype) = ('=', n_ASSIGN);
+		'$' =>
+			if (l.atendword) {
+				l.ungetc();
+				tok = '^';
+				break;
+			}
+			case (c = l.getc()) {
+			'#' =>
+				l.lval.optype = n_COUNT;
+			'"' =>
+				l.lval.optype = n_SQUASH;
+			* =>
+				l.ungetc();
+				l.lval.optype = n_VAR;
+			}
+			tok = OP;
+			wasdollar = 1;
+		'"' or '`'=>
+			if (l.atendword) {
+				tok = '^';
+				l.ungetc();
+				break;
+			}
+			tok = OP;
+			if (c == '"')
+				l.lval.optype = n_BQ2;
+			else
+				l.lval.optype = n_BQ;
+		'>' or '<' =>
+			rtype: int;
+			nc := l.getc();
+			if (nc == '>') {
+				if (c == '>')
+					rtype = Sys->OWRITE | OAPPEND;
+				else
+					rtype = Sys->ORDWR;
+				nc = l.getc();
+			} else if (c == '>')
+				rtype = Sys->OWRITE;
+			else
+				rtype = Sys->OREAD;
+			tok = REDIR;
+			if (nc == '[') {
+				(tok, l.lval.redir) = readfdassign(l);
+				if (tok == ERROR)
+					(l.err, l.errline) = ("syntax error in redirection", l.linenum);
+			} else {
+				l.ungetc();
+				l.lval.redir = ref Redir(-1, -1, -1);
+			}
+			if (l.lval.redir != nil)
+				l.lval.redir.rtype = rtype;
+		'|' =>
+			tok = '|';
+			l.lval.redir = nil;
+			if ((c = l.getc()) == '[') {
+				(tok, l.lval.redir) = readfdassign(l);
+				if (tok == ERROR) {
+					(l.err, l.errline) = ("syntax error in pipe redirection", l.linenum);
+					return tok;
+				}
+				tok = '|';
+			} else if(c == '|')
+				tok = OROR;
+			else
+				l.ungetc();
+
+		'\'' =>
+			if (l.atendword) {
+				l.ungetc();
+				tok = '^';
+				break;
+			}
+			startline := l.linenum;
+			s := "";
+			for(;;) {
+				while ((nc := l.getc()) != '\'' && nc != l.EOF)
+					s[len s] = nc;
+				if (nc == l.EOF) {
+					(l.err, l.errline) = ("unterminated string literal", startline);
+					return ERROR;
+				}
+				if (l.getc() != '\'') {
+					l.ungetc();
+					break;
+				}
+				s[len s] = '\'';	# 'xxx''yyy' becomes WORD(xxx'yyy)
+			}
+			l.lval.word = s;
+			tok = WORD;
+			endword = 1;
+
+		* =>
+			if (c == ':') {
+				if (l.getc() == '=') {
+					tok = '=';
+					l.lval.optype = n_LOCAL;
+					break;
+				}
+				l.ungetc();
+			}
+			if (l.atendword) {
+				l.ungetc();
+				tok = '^';
+				break;
+			}
+			allowed: string;
+			if (l.wasdollar)
+				allowed = "a-zA-Z0-9*_";
+			else
+				allowed = "^\n \t\r|$'#<>;^(){}`&=\"";
+			word := "";
+			loop: do {
+				case c {
+				'*' or '?' or '[' or GLOB =>
+					word[len word] = GLOB;
+				':' =>
+					nc := l.getc();
+					l.ungetc();
+					if (nc == '=')
+						break loop;
+				}
+				word[len word] = c;
+			} while ((c = l.getc()) != l.EOF && str->in(c, allowed));
+			l.ungetc();
+			l.lval.word = word;
+			tok = WORD;
+			endword = 1;
+		}
+		l.atendword = endword;
+		l.wasdollar = wasdollar;
+	}
+#	sys->print("token %s\n", tokstr(tok));
+	return tok;
+}
+
+tokstr(t: int): string
+{
+	s: string;
+	case t {
+	'\n' => s = "'\\n'";
+	33 to 127 => s = sprint("'%c'", t);
+	DUP=>	s = "DUP";
+	REDIR =>s = "REDIR";
+	WORD =>	s = "WORD";
+	OP =>	s = "OP";
+	END =>	s = "END";
+	ERROR=>	s = "ERROR";
+	* =>
+		s = "<unknowntok"+ string t + ">";
+	}
+	return s;
+}
+
+YYLEX.ungetc(lex: self ref YYLEX)
+{
+	lex.strpos--;
+	if (lex.f != nil) {
+		lex.ncbuf++;
+		if (lex.strpos < 0)
+			lex.strpos = len lex.cbuf - 1;
+	}
+}
+		
+YYLEX.getc(lex: self ref YYLEX): int
+{
+	if (lex.eof)				# EOF sticks
+		return lex.EOF;
+	c: int;
+	if (lex.f != nil) {
+		if (lex.ncbuf > 0) {
+			c = lex.cbuf[lex.strpos++];
+			if (lex.strpos >= len lex.cbuf)
+				lex.strpos = 0;
+			lex.ncbuf--;
+		} else {
+			if (lex.lastnl && lex.prompt != nil)
+				sys->fprint(stderr(), "%s", lex.prompt);
+			c = bufio->lex.f.getc();
+			if (c == bufio->ERROR || c == bufio->EOF) {
+				lex.eof = 1;
+				c = lex.EOF;
+			} else if (c == '\n')
+				lex.linenum++;
+			lex.lastnl = (c == '\n');
+			lex.cbuf[lex.strpos++] = c;
+			if (lex.strpos >= len lex.cbuf)
+				lex.strpos = 0;
+		}
+	} else {
+		if (lex.strpos >= len lex.s) {
+			lex.eof = 1;
+			c = lex.EOF;
+		} else
+			c = lex.s[lex.strpos++];
+	}
+	return c;
+}
+
+# read positive decimal number; return -1 if no number found.
+readnum(lex: ref YYLEX): int
+{
+	sum := nc := 0;
+	while ((c := lex.getc()) >= '0' && c <= '9') {
+		sum = (sum * 10) + (c - '0');
+		nc++;
+	}
+	lex.ungetc();
+	if (nc == 0)
+		return -1;
+	return sum;
+}
+
+# return tuple (toktype, lhs, rhs).
+# -1 signifies no number present.
+# '[' char has already been read.
+readfdassign(lex: ref YYLEX): (int, ref Redir)
+{
+	n1 := readnum(lex);
+	if ((c := lex.getc()) != '=') {
+		if (c == ']')
+			return (REDIR, ref Redir(-1, n1, -1));
+
+		return (ERROR, nil);
+	}
+	n2 := readnum(lex);
+	if (lex.getc() != ']')
+		return (ERROR, nil);
+	return (DUP, ref Redir(-1, n1, n2));
+}
+
+mkseq(left, right: ref Node): ref Node
+{
+	if (left != nil && right != nil)
+		return mk(n_SEQ, left, right);
+	else if (left == nil)
+		return right;
+	return left;
+}
+
+mk(ntype: int, left, right: ref Node): ref Node
+{
+	return ref Node(ntype, left, right, nil, nil);
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/cmd/sh/std.b
@@ -1,0 +1,812 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+include "filepat.m";
+	filepat: Filepat;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+builtinnames := array[] of {
+	"if", "while", "~", "!", "apply", "for",
+	"status", "pctl", "fn", "subfn", "and", "or",
+	"raise", "rescue", "flag", "getlines", "no",
+};
+
+sbuiltinnames := array[] of {
+	"hd", "tl", "index", "split", "join", "pid", "parse", "env", "pipe",
+};
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("std: cannot load self: %r"));
+	filepat = load Filepat Filepat->PATH;
+	if (filepat == nil)
+		ctxt.fail("bad module",
+			sys->sprint("std: cannot load: %s: %r", Filepat->PATH));
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		ctxt.fail("bad module",
+			sys->sprint("std: cannot load: %s: %r", Bufio->PATH));
+	names := builtinnames;
+ 	for (i := 0; i < len names; i++)
+		ctxt.addbuiltin(names[i], myself);
+	names = sbuiltinnames;
+	for (i = 0; i < len names; i++)
+		ctxt.addsbuiltin(names[i], myself);
+	env := ctxt.envlist();
+	for (; env != nil; env = tl env) {
+		(name, val) := hd env;
+		if (len name > 3 && name[0:3] == "fn-")
+			fndef(ctxt, name[3:], val, 0);
+		if (len name > 4 && name[0:4] == "sfn-")
+			fndef(ctxt, name[4:], val, 1);
+	}
+	return nil;
+}
+
+whatis(c: ref Sh->Context, sh: Sh, name: string, wtype: int): string
+{
+	ename, fname: string;
+	case wtype {
+	BUILTIN =>
+		(ename, fname) = ("fn-", "fn ");
+	SBUILTIN =>
+		(ename, fname) = ("sfn-", "subfn ");
+	OTHER =>
+		return nil;
+	}
+
+	val := c.get(ename + name);
+	if (val != nil)
+		return fname + name + " " + sh->quoted(hd val :: nil, 0);
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(c: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode, last: int): string
+{
+	status: string;
+	name := (hd cmd).word;
+	val := c.get("fn-" + name);
+	if (val != nil)
+		return c.run(hd val :: tl cmd, last);
+	case name {
+	"if" =>		status = builtin_if(c, cmd, last);
+	"while" =>		status = builtin_while(c, cmd, last);
+	"and" =>		status = builtin_and(c, cmd, last);
+	"apply" =>	status = builtin_apply(c, cmd, last);
+	"for" =>		status = builtin_for(c, cmd, last);
+	"or" =>		status = builtin_or(c, cmd, last);
+	"!" =>		status = builtin_not(c, cmd, last);
+	"fn" =>		status = builtin_fn(c, cmd, last, 0);
+	"subfn" =>	status = builtin_fn(c, cmd, last, 1);
+	"~" =>		status = builtin_twiddle(c, cmd, last);
+	"status" =>	status = builtin_status(c, cmd, last);
+	"pctl" =>		status = builtin_pctl(c, cmd, last);
+	"raise" =>		status = builtin_raise(c, cmd, last);
+	"rescue" =>	status = builtin_rescue(c, cmd, last);
+	"flag" =>		status = builtin_flag(c, cmd, last);
+	"getlines" =>	status = builtin_getlines(c, cmd, last);
+	"no" =>		status = builtin_no(c, cmd, last);
+	}
+	return status;
+}
+
+runsbuiltin(c: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode): list of ref Listnode
+{
+	name := (hd cmd).word;
+	val := c.get("sfn-" + name);
+	if (val != nil)
+		return runsubfn(c, val, tl cmd);
+	case name {
+	"pid" =>
+		return ref Listnode(nil, string sys->pctl(0, nil)) :: nil;
+	"hd" =>
+		if (tl cmd == nil)
+			return nil;
+		return hd tl cmd :: nil;
+	"tl" =>
+		if (tl cmd == nil)
+			return nil;
+		return tl tl cmd;
+	"index" =>
+		return sbuiltin_index(c, cmd);
+	"split" =>
+		return sbuiltin_split(c, cmd);
+	"join" =>
+		return sbuiltin_join(c, cmd);
+	"parse" =>
+		return sbuiltin_parse(c, cmd);
+	"env" =>
+		return sbuiltin_env(c, cmd);
+	"pipe" =>
+		return sbuiltin_pipe(c, cmd);
+	}
+	return nil;
+}
+
+runsubfn(ctxt: ref Context, body, args: list of ref Listnode): list of ref Listnode
+{
+	if (body == nil)
+		return nil;
+	ctxt.push();
+	{
+		ctxt.setlocal("result", nil);
+		ctxt.run(hd body :: args, 0);
+		result := ctxt.get("result");
+		ctxt.pop();
+		return result;
+	} exception e {
+	"fail:*" =>
+		ctxt.pop();
+		raise e;
+	}
+}
+
+sbuiltin_index(ctxt: ref Context, val: list of ref Listnode): list of ref Listnode
+{
+	if (len val < 2 || (hd tl val).word == nil)
+		builtinusage(ctxt, "index num list");
+	k := int (hd tl val).word - 1;
+	val = tl tl val;
+	for (; k > 0 && val != nil; k--)
+		val = tl val;
+	if (val != nil)
+		val = hd val :: nil;
+	return val;
+}
+
+# return a parsed version of a string, raising a "parse error" exception if
+# it fails. the string must be a braced command block.
+sbuiltin_parse(ctxt: ref Context, args: list of ref Listnode): list of ref Listnode
+{
+	if (len args != 2)
+		builtinusage(ctxt, "parse arg");
+	args = tl args;
+	if ((hd args).cmd != nil)
+		return ref Listnode((hd args).cmd, nil) :: nil;
+	w := (hd args).word;
+	if (w == nil || w[0] != '{')	#}
+		ctxt.fail("parse error", "parse: argument must be a braced block");
+	(n, err) := sh->parse(w);
+	if (err != nil)
+		ctxt.fail("parse error", "parse: " + err);
+	return ref Listnode(n, nil) :: nil;
+}
+
+sbuiltin_env(ctxt: ref Context, nil: list of ref Listnode): list of ref Listnode
+{
+	vl: list of string;
+	for (e := ctxt.envlist(); e != nil; e = tl e) {
+		(n, v) := hd e;
+		if (v != nil)		# XXX this is debatable... someone might want to see null local vars.
+			vl = n :: vl;
+	}
+	return sh->stringlist2list(vl);
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
+
+# usage: split [separators] value
+sbuiltin_split(ctxt: ref Context, args: list of ref Listnode): list of ref Listnode
+{
+	n := len args;
+	if (n < 2  || n > 3)
+		builtinusage(ctxt, "split [separators] value");
+	seps: string;
+	if (n == 2) {
+		ifs := ctxt.get("ifs");
+		if (ifs == nil)
+			ctxt.fail("usage", "split: $ifs not set");
+		seps = word(hd ifs);
+	} else {
+		args = tl args;
+		seps = word(hd args);
+	}
+	(nil, toks) := sys->tokenize(word(hd tl args), seps);
+	return sh->stringlist2list(toks);
+}
+
+sbuiltin_join(ctxt: ref Context, args: list of ref Listnode): list of ref Listnode
+{
+	args = tl args;
+	if (args == nil)
+		builtinusage(ctxt, "join separator [arg...]");
+	seps := word(hd args);
+	if (tl args == nil)
+		return ref Listnode(nil, nil) :: nil;
+	s := word(hd tl args);
+	for (args = tl tl args; args != nil; args = tl args)
+		s += seps + word(hd args);
+	return ref Listnode(nil, s) :: nil;
+}
+
+builtin_fn(ctxt: ref Context, args: list of ref Listnode, nil: int, issub: int): string
+{
+	n := len args;
+	title := (hd args).word;
+	if (n < 2)
+		builtinusage(ctxt, title + " [name...] [{body}]");
+	for (al := tl args; tl al != nil; al = tl al)
+		if ((hd al).cmd != nil)
+			builtinusage(ctxt, title + " [name...] [{body}]");
+	if ((hd al).cmd != nil) {
+		cmd := hd al :: nil;
+		for (al = tl args; tl al != nil; al = tl al)
+			fndef(ctxt, (hd al).word, cmd, issub);
+	} else {
+		for (al = tl args; al != nil; al = tl al)
+			fnundef(ctxt, (hd al).word, issub);
+	}
+	return nil;
+}
+
+fndef(ctxt: ref Context, name: string, cmd: list of ref Listnode, issub: int)
+{
+	if (cmd == nil)
+		return;
+	if (issub) {
+		ctxt.set("sfn-" + name, cmd);
+		ctxt.addsbuiltin(name, myself);
+	} else {
+		ctxt.set("fn-" + name, cmd);
+		ctxt.addbuiltin(name, myself);
+	}
+}
+
+fnundef(ctxt: ref Context, name: string, issub: int)
+{
+	if (issub) {
+		ctxt.set("sfn-" + name, nil);
+		ctxt.removesbuiltin(name, myself);
+	} else {
+		ctxt.set("fn-" + name, nil);
+		ctxt.removebuiltin(name, myself);
+	}
+}
+
+builtin_flag(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	n := len args;
+	if (n < 2 || n > 3 || len (hd tl args).word != 1)
+		builtinusage(ctxt, "flag [vxei] [+-]");
+	flag := (hd tl args).word[0];
+	p := "";
+	if (n == 3)
+		p = (hd tl tl args).word;
+	mask := 0;
+	case flag {
+	'v' =>	mask = Context.VERBOSE;
+	'x' =>	mask = Context.EXECPRINT;
+	'e' =>	mask = Context.ERROREXIT;
+	'i' =>		mask = Context.INTERACTIVE;
+	* =>		builtinusage(ctxt, "flag [vxei] [+-]");
+	}
+	case p {
+	"" =>		if (ctxt.options() & mask)
+				return nil;
+			return "not set";
+	"-" =>	ctxt.setoptions(mask, 0);
+	"+" =>	ctxt.setoptions(mask, 1);
+	* =>		builtinusage(ctxt, "flag [vxei] [+-]");
+	}
+	return nil;
+}
+
+builtin_no(nil: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args != nil)
+		return "yes";
+	return nil;
+}
+
+iscmd(n: ref Listnode): int
+{
+	return n.cmd != nil || (n.word != nil && n.word[0] == '{');
+}
+
+builtin_if(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	args = tl args;
+	nargs := len args;
+	if (nargs < 2)
+		builtinusage(ctxt, "if {cond} {action} [{cond} {action}]... [{elseaction}]");
+
+	status: string;
+	dolstar := ctxt.get("*");
+	while (args != nil) {
+		cmd: ref Listnode = nil;
+		if (tl args == nil) {
+			cmd = hd args;
+			args = tl args;
+		} else {
+			if (!iscmd(hd args))
+				builtinusage(ctxt, "if [{cond} {action}]... [{elseaction}]");
+
+			status = ctxt.run(hd args :: dolstar, 0);
+			if (status == nil) {
+				cmd = hd tl args;
+				args = nil;
+			} else
+				args = tl tl args;
+			setstatus(ctxt, status);
+		}
+		if (cmd != nil) {
+			if (!iscmd(cmd))
+				builtinusage(ctxt, "if [{cond} {action}]... [{elseaction}]");
+
+			status = ctxt.run(cmd :: dolstar, 0);
+		}
+	}
+	return status;	
+}
+
+builtin_or(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	s: string;
+	dolstar := ctxt.get("*");
+	for (args = tl args; args != nil; args = tl args) {
+		if (!iscmd(hd args))
+			builtinusage(ctxt, "or [{cmd} ...]");
+		if ((s = ctxt.run(hd args :: dolstar, 0)) == nil)
+			return nil;
+		else
+			setstatus(ctxt, s);
+	}
+	return s;
+}
+
+builtin_and(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	dolstar := ctxt.get("*");
+	for (args = tl args; args != nil; args = tl args) {
+		if (!iscmd(hd args))
+			builtinusage(ctxt, "and [{cmd} ...]");
+		if ((s := ctxt.run(hd args :: dolstar, 0)) != nil)
+			return s;
+		else
+			setstatus(ctxt, nil);
+	}
+	return nil;
+}
+
+builtin_while(ctxt: ref Context, args: list of ref Listnode, nil: int) : string
+{
+	args = tl args;
+	if (len args != 2 || !iscmd(hd args) || !iscmd(hd tl args))
+		builtinusage(ctxt, "while {condition} {cmd}");
+
+	dolstar := ctxt.get("*");
+	cond := hd args :: dolstar;
+	action := hd tl args :: dolstar;
+	status := "";
+	
+	for(;;){
+		{
+			while (ctxt.run(cond, 0) == nil)
+				status = setstatus(ctxt, ctxt.run(action, 0));
+			return status;
+		} exception e{
+		"fail:*" =>
+			if (loopexcept(e) == BREAK)
+				return status;
+		}
+	}
+}
+
+builtin_getlines(ctxt: ref Context, argv: list of ref Listnode, nil: int) : string
+{
+	n := len argv;
+	if (n < 2  || n > 3)
+		builtinusage(ctxt, "getlines [separators] {cmd}");
+	argv = tl argv;
+	seps := "\n";
+	if (n == 3) {
+		seps = word(hd argv);
+		argv = tl argv;
+	}
+	if (len seps == 0)
+		builtinusage(ctxt, "getlines [separators] {cmd}");
+	if (!iscmd(hd argv))
+		builtinusage(ctxt, "getlines [separators] {cmd}");
+	cmd := hd argv :: ctxt.get("*");
+	stdin := bufio->fopen(sys->fildes(0), Sys->OREAD);
+	if (stdin == nil)
+		ctxt.fail("bad input", sys->sprint("getlines: cannot open stdin: %r"));
+	status := "";
+	ctxt.push();
+	for(;;){
+		{
+			for (;;) {
+				s: string;
+				if (len seps == 1)
+					s = stdin.gets(seps[0]);
+				else
+					s = stdin.gett(seps);
+				if (s == nil)
+					break;
+				# make sure we don't lose the last unterminated line
+				lastc := s[len s - 1];
+				if (lastc == seps[0])
+					s = s[0:len s - 1];
+				else for (i := 1; i < len seps; i++) {
+					if (lastc == seps[i]) {
+						s = s[0:len s - 1];
+						break;
+					}
+				}
+				ctxt.setlocal("line", ref Listnode(nil, s) :: nil);
+				status = setstatus(ctxt, ctxt.run(cmd, 0));
+			}
+			ctxt.pop();
+			return status;
+		} exception e {
+		"fail:*" =>
+			ctxt.pop();
+			if (loopexcept(e) == BREAK)
+				return status;
+			ctxt.push();
+		}
+	}
+}
+
+# usage: raise [name]
+builtin_raise(ctxt: ref Context, args: list of ref Listnode, nil: int) : string
+{
+	ename: ref Listnode;
+	if (tl args == nil) {
+		e := ctxt.get("exception");
+		if (e == nil)
+			ctxt.fail("bad raise context", "raise: no exception found");
+		ename = (hd e);
+	} else
+		ename = hd tl args;
+	if (ename.word == nil && ename.cmd != nil)
+		ctxt.fail("bad raise context", "raise: bad exception name");
+	xraise("fail:" + ename.word);
+	return nil;
+}
+
+# usage: rescue pattern rescuecmd cmd
+builtin_rescue(ctxt: ref Context, args: list of ref Listnode, last: int) : string
+{
+	args = tl args;
+	if (len args != 3 || !iscmd(hd tl args) || !iscmd(hd tl tl args))
+		builtinusage(ctxt, "rescue pattern {rescuecmd} {cmd}");
+	if ((hd args).word == nil && (hd args).cmd != nil)
+		ctxt.fail("usage", "rescue: bad pattern");
+	dolstar := ctxt.get("*");
+	handler := hd tl args :: dolstar;
+	code := hd tl tl args :: dolstar;
+	{
+		return ctxt.run(code, 0);
+	} exception e {
+	"fail:*" =>
+		ctxt.push();
+		ctxt.set("exception", ref Listnode(nil, e[5:]) :: nil);
+		{
+			status := ctxt.run(handler, last);
+			ctxt.pop();
+			return status;
+		} exception {
+		"fail:*" =>
+			ctxt.pop();
+			raise e;
+		}
+	}
+}
+
+builtin_not(ctxt: ref Context, args: list of ref Listnode, last: int): string
+{
+	# syntax: ! cmd [args...]
+	args = tl args;
+	if (args == nil || ctxt.run(args, last) == nil)
+		return "false";
+	return "";
+}
+
+builtin_for(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	Usage: con "for var in [item...] {cmd}";
+	args = tl args;
+	if (args == nil)
+		builtinusage(ctxt, Usage);
+	var := (hd args).word;
+	if (var == nil)
+		ctxt.fail("bad assign", "for: bad variable name");
+	args = tl args;
+	if (args == nil || (hd args).word != "in")
+		builtinusage(ctxt, Usage);
+	args = tl args;
+	if (args == nil)
+		builtinusage(ctxt, Usage);
+	for (eargs := args; tl eargs != nil; eargs = tl eargs)
+			;
+	cmd := hd eargs;
+	if (!iscmd(cmd))
+		builtinusage(ctxt, Usage);
+
+	status := "";
+	dolstar := ctxt.get("*");
+	for(;;){
+		{
+			for (; tl args != nil; args = tl args) {
+				ctxt.setlocal(var, hd args :: nil);
+				status = setstatus(ctxt, ctxt.run(cmd :: dolstar, 0));
+			}
+			return status;
+		} exception e {
+		"fail:*" =>
+			if (loopexcept(e) == BREAK)
+				return status;
+			args = tl args;
+		}
+	}
+}
+
+CONTINUE, BREAK: con iota;
+loopexcept(ename: string): int
+{
+	case ename[5:] {
+	"break" =>
+		return BREAK;
+	"continue" =>
+		return CONTINUE;
+	* =>
+		raise ename;
+	}
+	return 0;
+}
+
+builtin_apply(ctxt: ref Context, args: list of ref Listnode, nil: int): string
+{
+	args = tl args;
+	if (args == nil || !iscmd(hd args))
+		builtinusage(ctxt, "apply {cmd} [val...]");
+
+	status := "";
+	cmd := hd args;
+	for(;;){
+		{
+			for (args = tl args; args != nil; args = tl args)
+				status = setstatus(ctxt, ctxt.run(cmd :: hd args :: nil, 0));
+
+			return status;
+		} exception e{
+		"fail:*" =>
+			if (loopexcept(e) == BREAK)
+				return status;
+		}
+	}
+}
+
+builtin_status(nil: ref Context, args: list of ref Listnode, nil: int): string
+{
+	if (tl args != nil)
+		return (hd tl args).word;
+	return "";
+}
+
+pctlnames := array[] of {
+	("newfd", Sys->NEWFD),
+	("forkfd", Sys->FORKFD),
+	("newns", Sys->NEWNS),
+	("forkns", Sys->FORKNS),
+	("newpgrp", Sys->NEWPGRP),
+	("nodevs", Sys->NODEVS)
+};
+
+builtin_pctl(ctxt: ref Context, argv: list of ref Listnode, nil: int): string
+{
+	if (len argv < 2)
+		builtinusage(ctxt, "pctl option... [fdnum...]");
+
+	finalmask := 0;
+	fdlist: list of int;
+	for (argv = tl argv; argv != nil; argv = tl argv) {
+		w := (hd argv).word;
+		if (isnum(w))
+			fdlist = int w :: fdlist;
+		else {
+			for (i := 0; i < len pctlnames; i++) {
+				(name, mask) := pctlnames[i];
+				if (name == w) {
+					finalmask |= mask;
+					break;
+				}
+			}
+			if (i == len pctlnames)
+				ctxt.fail("usage", "pctl: unknown flag " + w);
+		}
+	}
+	sys->pctl(finalmask, fdlist);
+	return nil;
+}
+
+# usage: ~ value pattern...
+builtin_twiddle(ctxt: ref Context, argv: list of ref Listnode, nil: int): string
+{
+	argv = tl argv;
+	if (argv == nil)
+		builtinusage(ctxt, "~ word [pattern...]");
+	if (tl argv == nil)
+		return "no match";
+	w := word(hd argv);
+
+	for (argv = tl argv; argv != nil; argv = tl argv)
+		if (filepat->match(word(hd argv), w))
+			return "";
+
+	return "no match";
+}
+
+#builtin_echo(ctxt: ref Context, argv: list of ref Listnode, nil: int): string
+#{
+#	argv = tl argv;
+#	nflag := 0;
+#	if (argv != nil && word(hd argv) == "-n") {
+#		nflag = 1;
+#		argv = tl argv;
+#	}
+#	s: string;
+#	if (argv != nil) {
+#		s = word(hd argv);
+#		for (argv = tl argv; argv != nil; argv = tl argv)
+#			s += " " + word(hd argv);
+#	}
+#	e: int;
+#	if (nflag)
+#		e = sys->print("%s", s);
+#	else
+#		e = sys->print("%s\n", s);
+#	if (e == -1) {
+#		err := sys->sprint("%r");
+#		if (ctxt.options() & ctxt.VERBOSE)
+#			sys->fprint(sys->fildes(2), "echo: write error: %s\n", err);
+#		return err;
+#	}
+#	return nil;
+#}
+
+ENOEXIST: con "file does not exist";
+TMPDIR: con "/tmp/pipes";
+sbuiltin_pipe(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	n: int;
+	if (len argv != 3 || !iscmd(hd tl tl argv))
+		builtinusage(ctxt, "pipe (from|to|fdnum) {cmd}");
+	s := (hd tl argv).word;
+	case s {
+	"from" =>
+		n = 1;
+	"to" =>
+		n = 0;
+	* =>
+		if (!isnum(s))
+			builtinusage(ctxt, "pipe (from|to|fdnum) {cmd}");
+		n = int s;
+	}
+	pipeid := ctxt.get("pipeid");
+	seq: int;
+	if (pipeid == nil)
+		seq = 0;
+	else
+		seq = int (hd pipeid).word;
+	id := "pipe." + string sys->pctl(0, nil) + "." + string seq;
+	ctxt.set("pipeid", ref Listnode(nil, string ++seq) :: nil);
+	mkdir(TMPDIR);
+	d := "/tmp/" + id + "d";
+	if (mkdir(d) == -1)
+		ctxt.fail("bad pipe", sys->sprint("pipe: cannot make %s: %r", d));
+	if (sys->bind("#|", d, Sys->MREPL) == -1) {
+		sys->remove(d);
+		ctxt.fail("bad pipe", sys->sprint("pipe: cannot bind pipe onto %s: %r", d));
+	}
+	if (rename(d + "/data", id + "x") == -1 || rename(d + "/data1", id + "y")) {
+		sys->unmount(nil, d);
+		sys->remove(d);
+		ctxt.fail("bad pipe", sys->sprint("pipe: cannot rename pipe: %r"));
+	}
+	if (sys->bind(d, TMPDIR, Sys->MBEFORE) == -1) {
+		sys->unmount(nil, d);
+		sys->remove(d);
+		ctxt.fail("bad pipe", sys->sprint("pipe: cannot bind pipe dir: %r"));
+	}
+	sys->unmount(nil, d);
+	sys->remove(d);
+	sync := chan of string;
+	spawn runpipe(sync, ctxt, n, TMPDIR + "/" + id + "x", hd tl tl argv);
+	if ((e := <-sync) != nil)
+		ctxt.fail("bad pipe", e);
+	return ref Listnode(nil, TMPDIR + "/" + id + "y") :: nil;
+}
+
+mkdir(f: string): int
+{
+	if (sys->create(f, Sys->OREAD, Sys->DMDIR | 8r777) == nil)
+		return -1;
+	return 0;
+}
+
+runpipe(sync: chan of string, ctxt: ref Context, fdno: int, p: string, cmd: ref Listnode)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	ctxt = ctxt.copy(1);
+	if ((fd := sys->open(p, Sys->ORDWR)) == nil) {
+		sync <-= sys->sprint("cannot open %s: %r", p);
+		exit;
+	}
+	sys->dup(fd.fd, fdno);
+	fd = nil;
+	sync <-= nil;
+	ctxt.run(cmd :: ctxt.get("*"), 1);
+}
+
+rename(x, y: string): int
+{
+	(ok, nil) := sys->stat(x);
+	if (ok == -1)
+		return -1;
+	inf := sys->nulldir;
+	inf.name = y;
+	if (sys->wstat(x, inf) == -1)
+		return -1;
+	return 0;
+}
+	
+builtinusage(ctxt: ref Context, s: string)
+{
+	ctxt.fail("usage", "usage: " + s);
+}
+
+setstatus(ctxt: ref Context, val: string): string
+{
+	ctxt.setlocal("status", ref Listnode(nil, val) :: nil);
+	return val;
+}
+
+# same as sys->raise(), but check that length of error string is
+# acceptable, and truncate as appropriate.
+xraise(s: string)
+{
+	d := array of byte s;
+	if (len d > Sys->WAITLEN)
+		raise string d[0:Sys->WAITLEN];
+	else {
+		d = nil;
+		raise s;
+	}
+}
+
+isnum(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] > '9' || s[i] < '0')
+			return 0;
+	return 1;
+}
+
--- /dev/null
+++ b/appl/cmd/sh/string.b
@@ -1,0 +1,212 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+include "string.m";
+	str: String;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("string: cannot load self: %r"));
+	str = load String String->PATH;
+	if (str == nil)
+		ctxt.fail("bad module",
+			sys->sprint("string: cannot load %s: %r", String->PATH));
+	ctxt.addbuiltin("prefix", myself);
+	ctxt.addbuiltin("in", myself);
+	names := array[] of {
+	"splitl", "splitr", "drop", "take", "splitstrl", "splitstrr",
+	"tolower", "toupper", "len", "alen", "slice", "fields",
+	"padl", "padr",
+	};
+	for (i := 0; i < len names; i++)
+		ctxt.addsbuiltin(names[i], myself);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(ctxt: ref Context, nil: Sh,
+			argv: list of ref Listnode, nil: int): string
+{
+	case (hd argv).word {
+	"prefix" =>
+		(a, b) := earg2("prefix", ctxt, argv);
+		if (!str->prefix(a, b))
+			return "false";
+	"in" =>
+		(a, b) := earg2("in", ctxt, argv);
+		if (a == nil || !str->in(a[0], b))
+			return "false";
+	}
+	return nil;
+}
+
+runsbuiltin(ctxt: ref Context, nil: Sh,
+			argv: list of ref Listnode): list of ref Listnode
+{
+	name := (hd argv).word;
+	case name {
+	"splitl" =>
+		(a, b) := earg2("splitl", ctxt, argv);
+		return mk2(str->splitl(a, b));
+	"splitr" =>
+		(a, b) := earg2("splitr", ctxt, argv);
+		return mk2(str->splitr(a, b));
+	"drop" =>
+		(a, b) := earg2("drop", ctxt, argv);
+		return mk1(str->drop(a, b));
+	"take" =>
+		(a, b) := earg2("take", ctxt, argv);
+		return mk1(str->take(a, b));
+	"splitstrl" =>
+		(a, b) := earg2("splitstrl", ctxt, argv);
+		return mk2(str->splitstrl(a, b));
+	"splitstrr" =>
+		(a, b) := earg2("splitstrr", ctxt, argv);
+		return mk2(str->splitstrr(a, b));
+	"tolower" =>
+		return mk1(str->tolower(earg1("tolower", ctxt, argv)));
+	"toupper" =>
+		return mk1(str->toupper(earg1("tolower", ctxt, argv)));
+	"len" =>
+		return mk1(string len earg1("len", ctxt, argv));
+	"alen" =>
+		return mk1(string len array of byte earg1("alen", ctxt, argv));
+	"slice" =>
+		return sbuiltin_slice(ctxt, argv);
+	"fields" =>
+		return sbuiltin_fields(ctxt, argv);
+	"padl" =>
+		return sbuiltin_pad(ctxt, argv, -1);
+	"padr" =>
+		return sbuiltin_pad(ctxt, argv, 1);
+	}
+	return nil;
+}
+
+sbuiltin_pad(ctxt: ref Context, argv: list of ref Listnode, dir: int): list of ref Listnode
+{
+	if (tl argv == nil || !isnum((hd tl argv).word))
+		ctxt.fail("usage", "usage: " + (hd argv).word + " n [arg...]");
+
+	argv = tl argv;
+	n := int (hd argv).word * dir;
+	s := "";
+	for (argv = tl argv; argv != nil; argv = tl argv) {
+		s += word(hd argv);
+		if (tl argv != nil)
+			s[len s] = ' ';
+	}
+	if (n != 0)
+		s =  sys->sprint("%*s", n, s);
+	return ref Listnode(nil, s) :: nil;
+}
+
+sbuiltin_fields(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	argv = tl argv;
+	if (len argv != 2)
+		ctxt.fail("usage", "usage: fields cl s");
+	cl := word(hd argv);
+	s := word(hd tl argv);
+
+	r: list of string;
+
+	n := 0;
+	for (i := 0; i < len s; i++) {
+		if (str->in(s[i], cl)) {
+			r = s[n:i] :: r;
+			n = i + 1;
+		}
+	}
+	r = s[n:i] :: r;
+	rl: list of ref Listnode;
+	for (; r != nil; r = tl r)
+		rl = ref Listnode(nil, hd r) :: rl;
+	return rl;
+}
+
+
+sbuiltin_slice(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	argv = tl argv;
+	if (len argv != 3 || !isnum((hd argv).word) ||
+			(hd tl argv).word != "end" && !isnum((hd tl argv).word))
+		ctxt.fail("usage", "usage: slice start end arg");
+	n1 := int (hd argv).word;
+	n2: int;
+	s := word(hd tl tl argv);
+	r := "";
+	if ((hd tl argv).word == "end")
+		n2 = len s;
+	else
+		n2 = int (hd tl argv).word;
+	if (n2 > len s)
+		n2 = len s;
+	if (n1 > len s)
+		n1 = len s;
+	if (n2 > n1)
+		r = s[n1:n2];
+	return mk1(r);
+}
+
+earg2(cmd: string, ctxt: ref Context, argv: list of ref Listnode): (string, string)
+{
+	argv = tl argv;
+	if (len argv != 2)
+		ctxt.fail("usage", "usage: " + cmd + " arg1 arg2");
+	return (word(hd argv), word(hd tl argv));
+}
+
+earg1(cmd: string, ctxt: ref Context, argv: list of ref Listnode): string
+{
+	if (len argv != 2)
+		ctxt.fail("usage", "usage: " + cmd + " arg");
+	return word(hd tl argv);
+}
+
+mk2(x: (string, string)): list of ref Listnode
+{
+	(a, b) := x;
+	return ref Listnode(nil, a) :: ref Listnode(nil, b) :: nil;
+}
+
+mk1(x: string): list of ref Listnode
+{
+	return ref Listnode(nil, x) :: nil;
+}
+
+isnum(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] > '9' || s[i] < '0')
+			return 0;
+	return 1;
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
--- /dev/null
+++ b/appl/cmd/sh/test.b
@@ -1,0 +1,96 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+include "itslib.m";
+	itslib: Itslib;
+	Tconfig, S_INFO, S_WARN, S_ERROR, S_FATAL: import itslib;
+
+tconf: ref Tconfig;
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	itslib = load Itslib Itslib->PATH;
+	if (itslib != nil)
+		tconf = itslib->init();
+	sh = shmod;
+	myself = load Shellbuiltin "$self";
+	if (myself == nil)
+		ctxt.fail("bad module", sys->sprint("its: cannot load self: %r"));
+	ctxt.addbuiltin("report", myself);
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+
+
+runbuiltin(ctxt: ref Sh->Context, nil: Sh,
+			cmd: list of ref Sh->Listnode, nil: int): string
+{
+	case (hd cmd).word {
+	"report" =>
+		if (len cmd < 4)
+			rusage(ctxt);
+		cmd = tl cmd;
+		sevstr := (hd cmd).word;
+		sev := sevtran(sevstr);
+		if (sev < 0)
+			rusage(ctxt);
+		cmd = tl cmd;
+		verb := (hd cmd).word;
+		cmd = tl cmd;
+		mtext := "";
+		i := 0;
+		while (len cmd) {
+			msg :=  (hd cmd).word;
+			cmd = tl cmd;
+			if (i++ > 0)
+				mtext = mtext + " ";
+			mtext = mtext + msg;
+		}
+		if (tconf != nil)
+			tconf.report(int sev, int verb, mtext);
+		else
+			sys->fprint(sys->fildes(2), "[itslib missing] %s %s\n", sevstr, mtext);
+	}
+	return nil;
+}
+
+
+runsbuiltin(nil: ref Sh->Context, nil: Sh,
+			nil: list of ref Sh->Listnode): list of ref Listnode
+{
+	return nil;
+}
+
+
+sevtran(sname: string): int
+{
+	SEVMAP :=  array[] of {"INF", "WRN", "ERR", "FTL"};
+	for (i:=0; i<len SEVMAP; i++)
+		if (sname == SEVMAP[i])
+			return i;
+	return -1;
+}
+
+rusage(ctxt: ref Context)
+{
+	ctxt.fail("usage", "usage: report INF|WRN|ERR|FTL verbosity message[...]");
+}
+
--- /dev/null
+++ b/appl/cmd/sh/tk.b
@@ -1,0 +1,438 @@
+implement Shellbuiltin;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "sh.m";
+	sh: Sh;
+	Listnode, Context: import sh;
+	myself: Shellbuiltin;
+
+tklock: chan of int;
+
+chans := array[23] of list of (string, chan of string);
+wins := array[16] of list of (int, ref Tk->Toplevel);
+winid := 0;
+
+badmodule(ctxt: ref Context, p: string)
+{
+	ctxt.fail("bad module", sys->sprint("tk: cannot load %s: %r", p));
+}
+
+initbuiltin(ctxt: ref Context, shmod: Sh): string
+{
+	sys = load Sys Sys->PATH;
+	sh = shmod;
+
+	myself = load Shellbuiltin "$self";
+	if (myself == nil) badmodule(ctxt, "self");
+
+	tk = load Tk Tk->PATH;
+	if (tk == nil) badmodule(ctxt, Tk->PATH);
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil) badmodule(ctxt, Tkclient->PATH);
+	tkclient->init();
+
+	tklock = chan[1] of int;
+
+	ctxt.addbuiltin("tk", myself);
+	ctxt.addbuiltin("chan", myself);
+	ctxt.addbuiltin("send", myself);
+
+	ctxt.addsbuiltin("tk", myself);
+	ctxt.addsbuiltin("recv", myself);
+	ctxt.addsbuiltin("alt", myself);
+	ctxt.addsbuiltin("tkquote", myself);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+getself(): Shellbuiltin
+{
+	return myself;
+}
+
+runbuiltin(ctxt: ref Context, nil: Sh,
+			cmd: list of ref Listnode, nil: int): string
+{
+	case (hd cmd).word {
+	"tk" =>		return builtin_tk(ctxt, cmd);
+	"chan" =>		return builtin_chan(ctxt, cmd);
+	"send" =>		return builtin_send(ctxt, cmd);
+	}
+	return nil;
+}
+
+runsbuiltin(ctxt: ref Context, nil: Sh,
+			cmd: list of ref Listnode): list of ref Listnode
+{
+	case (hd cmd).word {
+	"tk" =>		return sbuiltin_tk(ctxt, cmd);
+	"recv" =>		return sbuiltin_recv(ctxt, cmd);
+	"alt" =>		return sbuiltin_alt(ctxt, cmd);
+	"tkquote" =>	return sbuiltin_tkquote(ctxt, cmd);
+	}
+	return nil;
+}
+
+builtin_tk(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	# usage:	tk window _title_ _options_
+	#		tk wintitle _winid_ _title_
+	#		tk _winid_ _cmd_
+	if (tl argv == nil)
+		ctxt.fail("usage", "usage: tk (<winid>|window|onscreen|winctlwintitle|del|namechan) args...");
+	argv = tl argv;
+	w := (hd argv).word;
+	case w {
+	"window" =>
+		remark(ctxt, string makewin(ctxt, tl argv));
+	"wintitle" =>
+		argv = tl argv;
+		# change the title of a window
+		if (len argv != 2 || !isnum((hd argv).word))
+			ctxt.fail("usage", "usage: tk wintitle winid title");
+		tkclient->settitle(egetwin(ctxt, hd argv), word(hd tl argv));
+	"winctl" =>
+		argv = tl argv;
+		if (len argv != 2 || !isnum((hd argv).word))
+			ctxt.fail("usage", "usage: tk winctl winid cmd");
+		wid := (hd argv).word;
+		win := egetwin(ctxt, hd argv);
+		rq := word(hd tl argv);
+		if (rq == "exit") {
+			delwin(int wid);
+			delchan(wid);
+		}
+		tkclient->wmctl(win, rq);
+	"onscreen" =>
+		argv = tl argv;
+		if (len argv < 1 || !isnum((hd argv).word))
+			ctxt.fail("usage", "usage: tk onscreen winid [how]");
+		how := "";
+		if(tl argv != nil)
+			how = word(hd tl argv);
+		win := egetwin(ctxt, hd argv);
+		tkclient->startinput(win, "ptr" :: "kbd" :: nil);
+		tkclient->onscreen(win, how);
+	"namechan" =>
+		argv = tl argv;
+		n := len argv;
+		if (n < 2 || n > 3 || !isnum((hd argv).word))
+			ctxt.fail("usage", "usage: tk namechan winid chan [name]");
+		name: string;
+		if (n == 3)
+			name = word(hd tl tl argv);
+		else
+			name = word(hd tl argv);
+		tk->namechan(egetwin(ctxt, hd argv), egetchan(ctxt, hd tl argv), name);
+
+	"del" =>
+		if (len argv < 2)
+			ctxt.fail("usage", "usage: tk del id...");
+		for (argv = tl argv; argv != nil; argv = tl argv) {
+			id := (hd argv).word;
+			if (isnum(id))
+				delwin(int id);
+			delchan(id);
+		}
+	* =>
+		e := tkcmd(ctxt, argv);
+		if (e != nil)
+			remark(ctxt, e);
+		if (e != nil && e[0] == '!')
+			return e;
+	}
+	return nil;
+}
+
+remark(ctxt: ref Context, s: string)
+{
+	if (ctxt.options() & ctxt.INTERACTIVE)
+		sys->print("%s\n", s);
+}
+
+# create a new window (and its associated channel)
+makewin(ctxt: ref Context, argv: list of ref Listnode): int
+{
+	if (argv == nil)
+		ctxt.fail("usage", "usage: tk window title options");
+
+	if (ctxt.drawcontext == nil)
+		ctxt.fail("no draw context", sys->sprint("tk: no graphics context available"));
+
+	(title, options) := (word(hd argv), concat(tl argv));
+	(top, topchan) := tkclient->toplevel(ctxt.drawcontext, options, title, Tkclient->Appl);
+	newid := addwin(top);
+	addchan(string newid, topchan);
+	return newid;
+}
+
+builtin_chan(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	# create a new channel
+	argv = tl argv;
+	if (argv == nil)
+		ctxt.fail("usage", "usage: chan name....");
+	for (; argv != nil; argv = tl argv) {
+		name := (hd argv).word;
+		if (name == nil || isnum(name))
+			ctxt.fail("bad chan", "tk: bad channel name "+q(name));
+		if (addchan(name, chan of string) == nil)
+			ctxt.fail("bad chan", "tk: channel "+q(name)+" already exists");
+	}
+	return nil;
+}
+
+builtin_send(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	if (len argv != 3)
+		ctxt.fail("usage", "usage: send chan arg");
+	argv = tl argv;
+	c := egetchan(ctxt, hd argv);
+	c <-= word(hd tl argv);
+	return nil;
+}
+
+
+sbuiltin_tk(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	# usage:	tk _winid_ _command_
+	#		tk window _title_ _options_
+	argv = tl argv;
+	if (argv == nil)
+		ctxt.fail("usage", "tk (window|wid) args");
+	case (hd argv).word {
+	"window" =>
+		return ref Listnode(nil, string makewin(ctxt, tl argv)) :: nil;
+	"winids" =>
+		ret: list of ref Listnode;
+		for (i := 0; i < len wins; i++)
+			for (wl := wins[i]; wl != nil; wl = tl wl)
+				ret = ref Listnode(nil, string (hd wl).t0) :: ret;
+		return ret;
+	* =>
+		return ref Listnode(nil, tkcmd(ctxt, argv)) :: nil;
+	}
+}
+
+sbuiltin_alt(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	# usage: alt chan ...
+	argv = tl argv;
+	if (argv == nil)
+		ctxt.fail("usage", "usage: alt chan...");
+	nc := len argv;
+	kbd := array[nc] of chan of int;
+	ptr := array[nc] of chan of ref Draw->Pointer;
+	ca := array[nc * 3] of chan of string;
+	win := array[nc] of ref Tk->Toplevel;
+	
+	cname := array[nc] of string;
+	i := 0;
+	for (; argv != nil; argv = tl argv) {
+		w := (hd argv).word;
+		ca[i*3] = egetchan(ctxt, hd argv);
+		cname[i] = w;
+		if(isnum(w)){
+			win[i] = egetwin(ctxt, hd argv);
+			ca[i*3+1] = win[i].ctxt.ctl;
+			ca[i*3+2] = win[i].wreq;
+			ptr[i] = win[i].ctxt.ptr;
+			kbd[i] = win[i].ctxt.kbd;
+		}
+		i++;
+	}
+	for(;;) alt{
+	(n, key) := <-kbd =>
+		tk->keyboard(win[n], key);
+	(n, p) := <-ptr =>
+		tk->pointer(win[n], *p);
+	(n, v) := <-ca =>
+		return ref Listnode(nil, cname[n/3]) :: ref Listnode(nil, v) :: nil;
+	}
+}
+
+sbuiltin_recv(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	# usage: recv chan
+	if (len argv != 2)
+		ctxt.fail("usage", "usage: recv chan");
+	ch := hd tl argv;
+	c := egetchan(ctxt, ch);
+	if(!isnum(ch.word))
+		return ref Listnode(nil, <-c) :: nil;
+
+	win := egetwin(ctxt, ch);
+	for(;;)alt{
+	key := <-win.ctxt.kbd =>
+		tk->keyboard(win, key);
+	p := <-win.ctxt.ptr =>
+		tk->pointer(win, *p);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-c =>
+		return ref Listnode(nil, s) :: nil;
+	}
+}
+
+sbuiltin_tkquote(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
+{
+	if (len argv != 2)
+		ctxt.fail("usage", "usage: tkquote arg");
+	return ref Listnode(nil, tk->quote(word(hd tl argv))) :: nil;
+}
+
+tkcmd(ctxt: ref Context, argv: list of ref Listnode): string
+{
+	if (argv == nil || !isnum((hd argv).word))
+		ctxt.fail("usage", "usage: tk winid command");
+
+	return tk->cmd(egetwin(ctxt, hd argv), concat(tl argv));
+}
+
+hashfn(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
+
+q(s: string): string
+{
+	return "'" + s + "'";
+}
+
+egetchan(ctxt: ref Context, n: ref Listnode): chan of string
+{
+	if ((c := getchan(n.word)) == nil)
+		ctxt.fail("bad chan", "tk: bad channel name "+ q(n.word));
+	return c;
+}
+
+# assumes that n.word has been checked and found to be numeric.
+egetwin(ctxt: ref Context, n: ref Listnode): ref Tk->Toplevel
+{
+	wid := int n.word;
+	if (wid < 0 || (top := getwin(wid)) == nil)
+		ctxt.fail("bad win", "tk: unknown window id " + q(n.word));
+	return top;
+}
+
+getchan(name: string): chan of string
+{
+	n := hashfn(name, len chans);
+	for (cl := chans[n]; cl != nil; cl = tl cl) {
+		(cname, c) := hd cl;
+		if (cname == name)
+			return c;
+	}
+	return nil;
+}
+
+addchan(name: string, c: chan of string): chan of string
+{
+	n := hashfn(name, len chans);
+	tklock <-= 1;
+	if (getchan(name) == nil)
+		chans[n] = (name, c) :: chans[n];
+	<-tklock;
+	return c;
+}
+
+delchan(name: string)
+{
+	n := hashfn(name, len chans);
+	tklock <-= 1;
+	ncl: list of (string, chan of string);
+	for (cl := chans[n]; cl != nil; cl = tl cl) {
+		(cname, nil) := hd cl;
+		if (cname != name)
+			ncl = hd cl :: ncl;
+	}
+	chans[n] = ncl;
+	<-tklock;
+}
+
+addwin(top: ref Tk->Toplevel): int
+{
+	tklock <-= 1;
+	id := winid++;
+	slot := id % len wins;
+	wins[slot] = (id, top) :: wins[slot];
+	<-tklock;
+	return id;
+}
+
+delwin(id: int)
+{
+	tklock <-= 1;
+	slot := id % len wins;
+	nwl: list of (int, ref Tk->Toplevel);
+	for (wl := wins[slot]; wl != nil; wl = tl wl) {
+		(wid, nil) := hd wl;
+		if (wid != id)
+			nwl = hd wl :: nwl;
+	}
+	wins[slot] = nwl;
+	<-tklock;
+}
+
+getwin(id: int): ref Tk->Toplevel
+{
+	slot := id % len wins;
+	for (wl := wins[slot]; wl != nil; wl = tl wl) {
+		(wid, top) := hd wl;
+		if (wid == id)
+			return top;
+	}
+	return nil;
+}
+
+word(n: ref Listnode): string
+{
+	if (n.word != nil)
+		return n.word;
+	if (n.cmd != nil)
+		n.word = sh->cmd2string(n.cmd);
+	return n.word;
+}
+
+isnum(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] > '9' || s[i] < '0')
+			return 0;
+	return 1;
+}
+
+concat(argv: list of ref Listnode): string
+{
+	if (argv == nil)
+		return nil;
+	s := word(hd argv);
+	for (argv = tl argv; argv != nil; argv = tl argv)
+		s += " " + word(hd argv);
+	return s;
+}
+
+lockproc(c: chan of int)
+{
+	sys->pctl(Sys->NEWFD|Sys->NEWNS, nil);
+	for(;;){
+		c <-= 1;
+		<-c;
+	}
+}
--- /dev/null
+++ b/appl/cmd/sha1sum.b
@@ -1,0 +1,65 @@
+implement SHA1sum;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+
+SHA1sum: module
+{
+	init: fn(nil : ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	kr = load Keyring Keyring->PATH;
+	a := tl argv;
+	err := 0;
+	if(a != nil){
+		for( ; a != nil; a = tl a) {
+			s := hd a;
+			fd := sys->open(s, Sys->OREAD);
+			if (fd == nil) {
+				sys->fprint(stderr, "sha1sum: cannot open %s: %r\n", s);
+				err = 1;
+			} else
+				err |= sha1sum(fd, s);
+		}
+	} else
+		err |= sha1sum(sys->fildes(0), "");
+	if(err)
+		raise "fail:error";
+}
+
+sha1sum(fd: ref Sys->FD, file: string): int
+{
+	err := 0;
+	buf := array[Sys->ATOMICIO] of byte;
+	state: ref Keyring->DigestState = nil;
+	nbytes := big 0;
+	while((nr := sys->read(fd, buf, len buf)) > 0){
+		state = kr->sha1(buf, nr, nil, state);
+		nbytes += big nr;
+	}
+	if(nr < 0) {
+		sys->fprint(stderr, "sha1sum: error reading %s: %r\n", file);
+		err = 1;
+	}
+	digest := array[Keyring->SHA1dlen] of byte;
+	kr->sha1(buf, 0, digest, state);
+	sum := "";
+	for(i:=0; i<len digest; i++)
+		sum += sys->sprint("%2.2ux", int digest[i]);
+	if(file != nil)
+		sys->print("%s\t%s\n", sum, file);
+	else
+		sys->print("%s\n", sum);
+	return err;
+}
--- /dev/null
+++ b/appl/cmd/sleep.b
@@ -1,0 +1,46 @@
+implement Sleep;
+
+include "sys.m";
+sys: Sys;
+
+include "draw.m";
+
+Sleep: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil || argv == nil)
+		return;
+	argv = tl argv;
+	if(argv != nil && isvalid(hd argv)){
+		t := int hd argv;
+		if(t > 16r7fffffff / 1000)
+			t = 16r7fffffff / 1000;
+		sys->sleep(t * 1000);
+	} else {
+		sys->fprint(sys->fildes(2), "usage: sleep time\n");
+		raise "fail:usage";
+	}
+}
+
+isvalid(t: string): int
+{
+	l := len t;
+	if(l > 0 && (t[0] == '-' || t[0] == '+'))
+		x := 1;
+	else
+		x = 0;
+	ok := 0;
+	while(x < l) {
+		d := t[x];
+		if(d < '0' || d > '9')
+			return 0;
+		ok = 1;
+		x++;
+	}
+	return ok;
+}
--- /dev/null
+++ b/appl/cmd/sort.b
@@ -1,0 +1,129 @@
+implement Sort;
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+include "draw.m";
+include "arg.m";
+
+Sort: module
+{
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: sort [-n] [file]\n");
+	raise "fail:usage";
+}
+
+Incr: con 2000;		# growth quantum for record array
+
+init(nil : ref Draw->Context, args : list of string)
+{
+	bio : ref Bufio->Iobuf;
+
+	sys = load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	bufio := load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->fprint(stderr, "sort: cannot load %s: %r\n", Bufio->PATH);
+		raise "fail:bad module";
+	}
+	Iobuf: import bufio;
+	arg := load Arg Arg->PATH;
+	if (arg == nil) {
+		sys->fprint(stderr, "sort: cannot load %s: %r\n", Arg->PATH);
+		raise "fail:bad module";
+	}
+
+	nflag := 0;
+	rflag := 0;
+	arg->init(args);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'n' =>
+			nflag = 1;
+		'r' =>
+			rflag = 1;
+		* =>
+			usage();
+		}
+	}
+	args = arg->argv();
+	if (len args > 1)
+		usage();
+	if (args != nil) {
+		bio = bufio->open(hd args, Bufio->OREAD);
+		if (bio == nil) {
+			sys->fprint(stderr, "sort: cannot open %s: %r\n", hd args);
+			raise "fail:open file";
+		}
+	}
+	else
+		bio = bufio->fopen(sys->fildes(0), Bufio->OREAD);
+	a := array[Incr] of string;
+	n := 0;
+	while ((s := bio.gets('\n')) != nil) {
+		if (n >= len a) {
+			b := array[len a + Incr] of string;
+			b[0:] = a;
+			a = b;
+		}
+		a[n++] = s;
+	}
+	if (nflag)
+		mergesortnumeric(a, array[n] of string, n);
+	else
+		mergesort(a, array[n] of string, n);
+
+	stdout := bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+	if (rflag) {
+		for (i := n-1; i >= 0; i--)
+			stdout.puts(a[i]);
+	} else {
+		for (i := 0; i < n; i++)
+			stdout.puts(a[i]);
+	}
+	stdout.close();
+}
+
+mergesort(a, b: array of string, r: int)
+{
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m], m);
+		mergesort(a[m:r], b[m:r], r-m);
+		b[0:] = a[0:r];
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (b[i] > b[j])
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+mergesortnumeric(a, b: array of string, r: int)
+{
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesortnumeric(a[0:m], b[0:m], m);
+		mergesortnumeric(a[m:r], b[m:r], r-m);
+		b[0:] = a[0:r];
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (int b[i] > int b[j])
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
--- /dev/null
+++ b/appl/cmd/spki/mkfile
@@ -1,0 +1,22 @@
+<../../../mkconfig
+
+TARG=\
+	verify.dis\
+
+SYSMODULES=\
+	arg.m\
+	keyring.m\
+	security.m\
+	rand.m\
+	sys.m\
+	draw.m\
+	bufio.m\
+	string.m\
+	styx.m\
+	styxservers.m\
+	sexprs.m\
+	spki.m\
+
+DISBIN=$ROOT/dis/spki
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/spki/verify.b
@@ -1,0 +1,107 @@
+implement Verify;
+
+#
+# Copyright © 2004 Vita Nuova Holdings Limited
+#
+
+# work in progress
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+	IPint: import kr;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "sexprs.m";
+	sexprs: Sexprs;
+	Sexp: import sexprs;
+
+include "spki.m";
+	spki: SPKI;
+	Hash, Key, Cert, Name, Subject, Signature, Seqel, Toplev, Valid: import spki;
+	dump: import spki;
+
+	verifier: Verifier;
+	Speaksfor: import verifier;
+
+include "encoding.m";
+	base64: Encoding;
+
+Verify: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+debug := 0;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	bufio = load Bufio Bufio->PATH;
+	sexprs = load Sexprs Sexprs->PATH;
+	spki = load SPKI SPKI->PATH;
+	verifier = load Verifier Verifier->PATH;
+	base64 = load Encoding Encoding->BASE64PATH;
+
+	sexprs->init();
+	spki->init();
+	verifier->init();
+
+	f := bufio->fopen(sys->fildes(0), Sys->OREAD);
+	for(;;){
+		(e, err) := Sexp.read(f);
+		if(e == nil && err == nil)
+			break;
+		if(err != nil)
+			error(sys->sprint("invalid s-expression: %s", err));
+		(top, diag) := spki->parse(e);
+		if(diag != nil)
+			error(sys->sprint("invalid SPKI structure: %s", diag));
+		pick t := top {
+		C =>
+			if(debug)
+				sys->print("cert: %s\n", t.v.text());
+			a := spki->hashexp(e, "md5");
+		Sig =>
+			sys->print("got signature %q\n", t.v.text());
+		K =>
+			sys->print("got key %q\n", t.v.text());
+		Seq =>
+			els := t.v;
+			if(debug){
+				sys->print("(sequence");
+				for(; els != nil; els = tl els)
+					sys->print(" %s", (hd els).text());
+				sys->print(")");
+			}
+			(claim, rem, whynot) := verifier->verify(t.v);
+			if(whynot != nil){
+				if(rem == nil)
+					s := "end of sequence";
+				else
+					s = (hd rem).text();
+				sys->fprint(sys->fildes(2), "verify: failed to verify at %#q: %s\n", s, whynot);
+			}else{
+				if(claim.regarding != nil)
+					scope := sys->sprint(" regarding %q", claim.regarding.text());
+				sys->print("verified: %q speaks for %q%s\n", claim.subject.text(), claim.name.text(), scope);
+			}
+		* =>
+			sys->print("unexpected SPKI type: %q\n", e.text());
+		}
+	}
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "verify: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/src.b
@@ -1,0 +1,96 @@
+implement Src;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "dis.m";
+	dis: Dis;
+
+Src: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dis = load Dis Dis->PATH;
+
+	if(dis != nil){
+		dis->init();
+		for(argv = tl argv; argv != nil; argv = tl argv){
+			s := src(hd argv);
+			if(s == nil)
+				s = "?";
+			sys->print("%s:	%s\n", hd argv, s);
+		}
+	}
+}
+
+src(progname: string): string
+{
+	disfile := 0;
+	if (len progname >= 4 && progname[len progname-4:] == ".dis")
+		disfile = 1;
+	pathlist: list of string;
+	if (absolute(progname))
+		pathlist = list of {""};
+	else
+		pathlist = list of {"/dis", "."};
+
+	err := "";
+	do {
+		path: string;
+		if (hd pathlist != "")
+			path = hd pathlist + "/" + progname;
+		else
+			path = progname;
+
+		npath := path;
+		if (!disfile)
+			npath += ".dis";
+		src := dis->src(npath);
+		if(src != nil)
+			return src;
+		err = sys->sprint("%r");
+		if (nonexistent(err)) {
+			# try and find it as a shell script
+			if (!disfile) {
+				(ok, info) := sys->stat(path);
+				if (ok == 0 && (info.mode & Sys->DMDIR) == 0
+						&& (info.mode & 8r111) != 0)
+					return path;
+				else
+					err = sys->sprint("%r");
+			}
+		}
+		pathlist = tl pathlist;
+	} while (pathlist != nil && nonexistent(err));
+	return nil;
+}
+
+absolute(p: string): int
+{
+	if (len p < 2)
+		return 0;
+	if (p[0] == '/' || p[0] == '#')
+		return 1;
+	if (len p < 3 || p[0] != '.')
+		return 0;
+	if (p[1] == '/')
+		return 1;
+	if (p[1] == '.' && p[2] == '/')
+		return 1;
+	return 0;
+}
+
+nonexistent(e: string): int
+{
+	errs := array[] of {"does not exist", "directory entry not found"};
+	for (i := 0; i < len errs; i++){
+		j := len errs[i];
+		if (j <= len e && e[len e-j:] == errs[i])
+			return 1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/appl/cmd/stack.b
@@ -1,0 +1,184 @@
+implement Command;
+
+include "sys.m";
+	sys: Sys;
+	print, fprint, FD: import sys;
+	stderr: ref FD;
+
+include "draw.m";
+
+include "debug.m";
+	debug: Debug;
+	Prog, Module, Exp: import debug;
+
+include "arg.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "env.m";
+	env: Env;
+
+include "string.m";
+	str: String;
+
+include "dis.m";
+	dism: Dis;
+
+Command: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: stack [-v] pid\n");
+	raise "fail:usage";
+}
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "stack: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+sbldirs: list of (string, string);
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		badmodule(Bufio->PATH);
+	debug = load Debug Debug->PATH;
+	if(debug == nil)
+		badmodule(Debug->PATH);
+	env = load Env Env->PATH;
+	if (env != nil) {
+		str = load String String->PATH;
+		if (str == nil)
+			badmodule(String->PATH);
+	}
+	bout := bufio->fopen(sys->fildes(1), Sys->OWRITE);
+
+	arg->init(argv);
+	verbose := 0;
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'v' =>
+			verbose = 1;
+		'p' =>
+			dispath := arg->arg();
+			sblpath := arg->arg();
+			if (dispath == nil || sblpath == nil)
+				usage();
+			sbldirs = (addslash(dispath), addslash(sblpath)) :: sbldirs;
+		* =>
+			usage();
+		}
+	}
+	if (env != nil && (pathl := env->getenv("sblpath")) != nil) {
+		toks := str->unquoted(pathl);
+		for (; toks != nil && tl toks != nil; toks = tl tl toks)
+			sbldirs = (addslash(hd toks), addslash(hd tl toks)) :: sbldirs;
+	}
+	t: list of (string, string);
+	for (; sbldirs != nil; sbldirs = tl sbldirs)
+		t = hd sbldirs :: t;
+	sbldirs = t;
+
+	argv = arg->argv();
+	if(argv == nil)
+		usage();
+
+	debug->init();
+
+	(p, err) := debug->prog(int hd argv);
+	if(err != nil){
+		fprint(stderr, "stack: %s\n", err);
+		return;
+	}
+	stk: array of ref Exp;
+	(stk, err) = p.stack();
+
+	if(err != nil){
+		fprint(stderr, "stack: %s\n", err);
+		return;
+	}
+
+	for(i := 0; i < len stk; i++){
+		stdsym(stk[i].m);
+		stk[i].m.stdsym();
+		stk[i].findsym();
+		bout.puts(stk[i].name + "(");
+		vs := stk[i].expand();
+		if(verbose && vs != nil){
+			for(j := 0; j < len vs; j++){
+				if(vs[j].name == "args"){
+					d := vs[j].expand();
+					s := "";
+					for(j = 0; j < len d; j++) {
+						bout.puts(sys->sprint("%s%s=%s", s, d[j].name, d[j].val().t0));
+						s = ", ";
+					}
+					break;
+				}
+			}
+		}
+		bout.puts(sys->sprint(") %s\n", stk[i].srcstr()));
+		if(verbose && vs != nil){
+			for(j := 0; j < len vs; j++){
+				if(vs[j].name == "locals"){
+					d := vs[j].expand();
+					for(j = 0; j < len d; j++)
+						bout.puts("\t" + d[j].name + "=" + d[j].val().t0 + "\n");
+					break;
+				}
+			}
+		}
+	}
+	bout.flush();
+}
+
+stdsym(m: ref Module)
+{
+	dis := m.dis();
+	if(dism == nil){
+		dism = load Dis Dis->PATH;
+		if(dism != nil)
+			dism->init();
+	}
+	if(dism != nil && (sp := dism->src(dis)) != nil){
+		sp = sp[0: len sp - 1] + "sbl";
+		(sym, nil) := debug->sym(sp);
+		if (sym != nil) {
+			m.addsym(sym);
+			return;
+		}
+	}
+	for (sbl := sbldirs; sbl != nil; sbl = tl sbl) {
+		(dispath, sblpath) := hd sbl;
+		if (len dis > len dispath && dis[0:len dispath] == dispath) {
+			sblpath = sblpath + dis[len dispath:];
+			if (len sblpath > 4 && sblpath[len sblpath - 4:] == ".dis")
+				sblpath = sblpath[0:len sblpath - 4] + ".sbl";
+			(sym, nil) := debug->sym(sblpath);
+			if (sym != nil) {
+				m.addsym(sym);
+				return;
+			}
+		}
+	}
+}
+			
+addslash(p: string): string
+{
+	if (p != nil && p[len p - 1] != '/')
+		p[len p] = '/';
+	return p;
+}
--- /dev/null
+++ b/appl/cmd/stackv.b
@@ -1,0 +1,502 @@
+implement Stackv;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "debug.m";
+	debug: Debug;
+	Prog, Module, Exp: import debug;
+	Tadt, Tarray, Tbig, Tbyte, Treal,
+	Tfn, Tint, Tlist,
+	Tref, Tstring, Tslice: import Debug;
+include "arg.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+stderr: ref Sys->FD;
+stdout: ref Iobuf;
+
+hasht := array[97] of (int, array of int);
+
+Stackv: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+maxrecur := 16r7ffffffe;
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "stackv: cannot load %q: %r\n", p);
+	raise "fail:bad module";
+}
+
+currp: ref Prog;
+showtypes := 1;
+showsource := 0;
+showmodule := 0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	debug = load Debug Debug->PATH;
+	if(debug == nil)
+		badmodule(Debug->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		badmodule(Bufio->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+	stdout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+
+	arg->init(argv);
+	arg->setusage("stackv [-Tlm] [-r maxdepth] [-s dis sbl]... [pid[.sym]...] ...");
+	sblfile := "";
+	while((opt := arg->opt()) != 0){
+		case opt {
+		's' =>
+			arg->earg();	# XXX make it a list of maps from dis to sbl later
+			sblfile = arg->earg();
+		'l' =>
+			showsource = 1;
+		'm' =>
+			showmodule = 1;
+		'r' =>
+			maxrecur = int arg->earg();
+		'T' =>
+			showtypes = 0;
+		* =>
+			arg->usage();
+		}
+	}
+	debug->init();
+	argv = arg->argv();
+	printpids := len argv > 1;
+	if(printpids)
+		maxrecur++;
+	for(; argv != nil; argv = tl argv)
+		db(sys->tokenize(hd argv, ".").t1, printpids);
+}
+
+db(toks: list of string, printpid: int): int
+{
+	if(toks == nil){
+		sys->fprint(stderr, "stackv: bad pid\n");
+		return -1;
+	}
+	if((pid := int hd toks) <= 0){
+		sys->fprint(stderr, "stackv: bad pid %q\n", hd toks);
+		return -1;
+	}
+	err: string;
+	p: ref Prog;
+
+	# reuse process if possible
+	if(currp == nil || currp.id != pid){
+		(currp, err) = debug->prog(pid);
+		if(err != nil){
+			sys->fprint(stderr, "stackv: %s\n", err);
+			return -1;
+		}
+		if(currp == nil){
+			sys->fprint(stderr, "stackv: nil prog from pid %d\n", pid);
+			return -1;
+		}
+	}
+	p = currp;
+	stk: array of ref Exp;
+	(stk, err) = p.stack();
+	if(err != nil){
+		sys->fprint(stderr, "stackv: %s\n", err);
+		return -1;
+	}
+	for (i := 0; i < len stk; i++) {
+		stk[i].m.stdsym();
+		stk[i].findsym();
+	}
+	depth := 0;
+	if(printpid){
+		stdout.puts(sys->sprint("prog %d {\n", pid));	# }
+		depth++;
+	}
+	pexp(stk, tl toks, depth);
+	if(printpid)
+		stdout.puts("}\n");
+	stdout.flush();
+	return 0;
+}
+
+pexp(stk: array of ref Exp, toks: list of string, depth: int)
+{
+	if(toks == nil){
+		for (i := 0; i < len stk; i++)
+			pfn(stk[i], depth);
+	}else{
+		exp := stackfindsym(stk, toks, depth);
+		if(exp == nil)
+			return;
+		pname(exp, depth, nil);
+		stdout.putc('\n');
+	}
+}
+
+stackfindsym(stk: array of ref Exp, toks: list of string, depth: int): ref Exp
+{
+	fname := hd toks;
+	toks = tl toks;
+	for(i := 0; i < len stk; i++){
+		s := stk[i].name;
+		if(s == fname)
+			break;
+		if(hasdot(s) && toks != nil && s == fname+"."+hd toks){
+			fname += "."+hd toks;
+			toks = tl toks;
+			break;
+		}
+	}
+	if(i == len stk){
+		indent(depth);
+		stdout.puts("function not found\n");
+		return nil;
+	}
+	if(toks == nil)
+		return stk[i];
+	stk = stk[i].expand();
+	if(hd toks == "module"){
+		if((e := getname(stk, "module")) == nil){
+			indent(depth);
+			stdout.puts(sys->sprint("no module declarations in function %q\n", fname));
+		}else if((e = symfindsym(e, tl toks, depth)) != nil)
+			return e;
+		return nil;
+	}
+	for(t := "locals" :: "args" :: "module" :: nil; t != nil; t = tl t){
+		if((e := getname(stk, hd t)) == nil)
+			continue;
+		if((e = symfindsym(e, toks, depth)) != nil)
+			return e;
+	}
+	indent(depth);
+	stdout.puts(sys->sprint("symbol %q not found in function %q\n", hd toks, fname));
+	return nil;
+}
+
+hasdot(s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '.')
+			return 1;
+	return 0;
+}
+
+symfindsym(e: ref Exp, toks: list of string, depth: int): ref Exp
+{
+	if(toks == nil)
+		return e;
+	exps := e.expand();
+	for(i := 0; i < len exps; i++)
+		if(exps[i].name == hd toks)
+			return symfindsym(exps[i], tl toks, depth);
+	return nil;
+}
+
+pfn(exp: ref Exp, depth: int)
+{
+	(v, w) := exp.val();
+	if(!w || v == nil){
+		indent(depth);
+		stdout.puts(sys->sprint("no value for fn %q\n", exp.name));
+		return;
+	}
+	exps := exp.expand();
+	indent(depth);
+	stdout.puts("["+exp.srcstr()+"]\n");
+	indent(depth);
+	stdout.puts(symname(exp)+"(");
+	if((e := getname(exps, "args")) != nil){
+		args := e.expand();
+		for(i := 0; i < len args; i++){
+			pname(args[i], depth+1, nil);
+			if(i != len args - 1)
+				stdout.puts(", ");
+		}
+	}
+	stdout.puts(")\n");
+	indent(depth);
+	stdout.puts("{\n");	# }
+	if((e = getname(exps, "locals")) != nil){
+		locals := e.expand();
+		for(i := 0; i < len locals; i++){
+			indent(depth+1);
+			pname(locals[i], depth+1, nil);
+			stdout.puts("\n");
+		}
+	}
+	if(showmodule && (e = getname(exps, "module")) != nil){
+		mvars := e.expand();
+		for(i := 0; i < len mvars; i++){
+			indent(depth+1);
+			pname(mvars[i], depth+1, "module.");
+			stdout.puts("\n");
+		}
+	}
+	indent(depth);
+	stdout.puts("}\n");
+}
+
+getname(exps: array of ref Exp, name: string): ref Exp
+{
+	for(i := 0; i < len exps; i++)
+		if(exps[i].name == name)
+			return exps[i];
+	return nil;
+}
+
+strval(v: string): string
+{
+	for(i := 0; i < len v; i++)
+		if(v[i] == '"')
+			break;
+	if(i < len v)
+		v = v[i:];
+	return v;
+}
+
+pname(exp: ref Exp, depth: int, prefix: string)
+{
+	name := prefix+symname(exp);
+	(v, w) := exp.val();
+	if (!w && v == nil) {
+		stdout.puts(sys->sprint("%s: %s = novalue", symname(exp), exp.typename()));
+		return;
+	}
+	case exp.kind() {
+	Tfn =>
+		pfn(exp, depth);
+	Tint =>
+		stdout.puts(sys->sprint("%s := %s", name, v));
+	Tstring =>
+		stdout.puts(sys->sprint("%s := %s", name, strval(v)));
+	Tbyte or
+	Tbig or
+	Treal =>
+		stdout.puts(sys->sprint("%s := %s %s", name, exp.typename(), v));
+	* =>
+		if(showtypes)
+			stdout.puts(sys->sprint("%s: %s = ", name, exp.typename()));
+		else
+			stdout.puts(sys->sprint("%s := ", name));
+		pval(exp, v, w, depth);
+	}
+}
+
+srcstr(src: ref Debug->Src): string
+{
+	if(src == nil)
+		return nil;
+	if(src.start.file != src.stop.file)
+		return sys->sprint("%q:%d.%d,%q:%d.%d", src.start.file, src.start.line, src.start.pos, src.stop.file, src.stop.line, src.stop.pos);
+	if(src.start.line != src.stop.line)
+		return sys->sprint("%q:%d.%d,%d.%d", src.start.file, src.start.line, src.start.pos, src.stop.line, src.stop.pos);
+	return sys->sprint("%q:%d.%d,%d", src.start.file, src.start.line, src.start.pos, src.stop.pos);
+}
+
+pval(exp: ref Exp, v: string, w: int, depth: int)
+{
+	if(depth >= maxrecur){
+		stdout.puts(v);
+		return;
+	}
+	case exp.kind() {
+	Tarray =>
+		if(pref(v)){
+			if(depth+1 >= maxrecur)
+				stdout.puts(v+"{...}");
+			else{
+				stdout.puts(v+"{\n");
+				indent(depth+1);
+				parray(exp, depth+1);
+				stdout.puts("\n");
+				indent(depth);
+				stdout.puts("}");
+			}
+		}
+	Tlist =>
+		if(v == "nil")
+			stdout.puts("nil");
+		else
+		if(depth+1 >= maxrecur)
+			stdout.puts(v+"{...}");
+		else{
+			stdout.puts("{\n");
+			indent(depth+1);
+			plist(exp, v, w, depth+1);
+			stdout.puts("\n");
+			indent(depth);
+			stdout.puts("}");
+		}
+	Tadt =>
+		pgenval(exp, nil, w, depth);
+	Tref =>
+		if(pref(v))
+			pgenval(exp, v, w, depth);
+	Tstring =>
+		stdout.puts(strval(v));
+	* =>
+		pgenval(exp, v, w, depth);
+	}
+}
+
+parray(exp: ref Exp, depth: int)
+{
+	exps := exp.expand();
+	for(i := 0; i < len exps; i++){
+		e := exps[i];
+		(v, w) := e.val();
+		if(e.kind() == Tslice)
+			parray(e, depth);
+		else{
+			pval(e, v, w, depth);
+			stdout.puts(", ");
+		}
+	}
+}
+
+plist(exp: ref Exp, v: string, w: int, depth: int)
+{
+	while(w && v != "nil"){
+		exps := exp.expand();
+		h := getname(exps, "hd");
+		if(h == nil)
+			break;
+		(hv, vw) := h.val();
+		if(pref(v) == 0)
+			return;
+		stdout.puts(v+"(");
+		pval(h, hv, vw, depth);
+		stdout.puts(") :: ");
+		h = nil;
+		exp = getname(exps, "tl");
+		(v, w) = exp.val();
+	}
+	stdout.puts("nil");
+}
+
+pgenval(exp: ref Exp, v: string, w: int, depth: int)
+{
+	if(w){
+		exps := exp.expand();
+		if(len exps == 0)
+			stdout.puts(v);
+		else{
+			stdout.puts(v+"{\n");		# }
+			if (len exps > 0){
+				if(depth >= maxrecur){
+					indent(depth);
+					stdout.puts(sys->sprint("...[%d]\n", len exps));
+				}else{
+					for (i := 0; i < len exps; i++){
+						indent(depth+1);
+						pname(exps[i], depth+1, nil);
+						stdout.puts("\n");
+					}
+				}
+			}
+			indent(depth);		# {
+			stdout.puts("}");
+		}
+	}else
+		stdout.puts(v);
+}
+
+symname(exp: ref Exp): string
+{
+	if(showsource == 0)
+		return exp.name;
+	return exp.name+"["+srcstr(exp.src())+"]";
+}
+
+indent(n: int)
+{
+	while(n-- > 0)
+		stdout.putc('\t');
+}
+
+ref2int(v: string): int
+{
+	if(v == nil)
+		error("bad empty value for ref");
+	i := 0;
+	n := len v;
+	if(v[0] == '@')
+		i = 1;
+	else{
+		# skip array bounds
+		if(v[0] == '['){
+			for(; i < n && v[i] != ']'; i++)
+				;
+			if(i >= n - 2 || v[i+1] != ' ' || v[i+2] != '@')
+				error("bad value for ref: "+v);
+			i += 3;
+		}
+	}
+	if(n - i > 8)
+		error("64-bit pointers?");
+	p := 0;
+	for(; i < n; i++){
+		c := v[i];
+		case c {
+		'0' to '9' =>
+			p = (p << 4) + (c - '0');
+		'a' to 'f' =>
+			p = (p << 4) + (c - 'a' + 10);
+		* =>
+			error("bad value for ref: "+v);
+		}
+	}
+	return p;
+}
+
+pref(v: string): int
+{
+	if(v == "nil"){
+		stdout.puts("nil");
+		return 0;
+	}
+	if(addref(ref2int(v)) == 0){
+		stdout.puts(v);
+		stdout.puts("(qv)");
+		return 0;
+	}
+	return 1;
+}
+
+# hash table implementation that tries to be reasonably
+# parsimonious on memory usage.
+addref(v: int): int
+{
+	slot := (v & 16r7fffffff) % len hasht;
+	(n, a) := hasht[slot];
+	for(i := 0; i < n; i++)
+		if(a[i] == v)
+			return 0;
+	if(n == len a){
+		if(n == 0)
+			n = 3;
+		t := array[n*3/2] of int;
+		t[0:] = a;
+		hasht[slot].t1 = t;
+		a = t;
+	}
+	a[hasht[slot].t0++] = v;
+	return 1;
+}
+
+error(e: string)
+{
+	sys->fprint(sys->fildes(2), "stackv: error: %s\n", e);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/stream.b
@@ -1,0 +1,97 @@
+#
+# stream data from files
+#
+# Copyright © 2000 Vita Nuova Limited.  All rights reserved.
+#
+
+implement Stream;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+Stream: module
+{
+	init:	fn(nil: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+usage()
+{
+	sys->fprint(stderr, "Usage: stream [-a] [-b bufsize] file1 [file2]\n");
+	fail("usage");
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	bsize := 0;
+	sync := chan of int;
+	if(argv != nil)
+		argv = tl argv;
+	for(; argv != nil && len hd argv && (s := hd argv)[0] == '-' && len s > 1; argv = tl argv)
+		case s[1] {
+		'b' =>
+			if(len s > 2)
+				bsize = int s[2:];
+			else if((argv = tl argv) != nil)
+				bsize = int hd argv;
+			else
+				usage();
+		'a' =>
+			sync = nil;
+		* =>
+			usage();
+		}
+	if(bsize <= 0 || bsize > 2*1024*1024)
+		bsize = Sys->ATOMICIO;
+	argc := len argv;
+	if(argc < 1)
+		usage();
+
+	if(argc > 1){
+		f1 := eopen(hd argv, Sys->ORDWR);
+		f2 := eopen(hd tl argv, Sys->ORDWR);
+		spawn stream(f1, f2, bsize, sync);
+		spawn stream(f2, f1, bsize, sync);
+	}else{
+		f2 := sys->fildes(1);
+		if(f2 == nil) {
+			sys->fprint(stderr, "stream: can't access standard output: %r\n");
+			fail("stdout");
+		}
+		f1 := eopen(hd argv, Sys->OREAD);
+		spawn stream(f1, f2, bsize, sync);
+	}
+	if(sync != nil){	# count them back in
+		<-sync;
+		if(argc > 1)
+			<-sync;
+	}
+}
+
+stream(source: ref Sys->FD, sink: ref Sys->FD, bufsize: int, sync: chan of int)
+{
+	if(sys->stream(source, sink, bufsize) < 0)
+		sys->fprint(stderr, "stream: error streaming data: %r\n");
+	if(sync != nil)
+		sync <-= 1;
+}
+
+eopen(name: string, mode: int): ref Sys->FD
+{
+	fd := sys->open(name, mode);
+	if(fd == nil){
+		sys->fprint(stderr, "stream: can't open %s: %r\n", name);
+		fail("open");
+	}
+	return fd;
+}
+
+fail(s: string)
+{
+	raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/cmd/strings.b
@@ -1,0 +1,87 @@
+#
+#	initially generated by c2l
+#
+
+implement Strings;
+
+include "draw.m";
+
+Strings: module
+{
+	init: fn(nil: ref Draw->Context, argl: list of string);
+};
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+MINSPAN: con 6;
+BUFSIZE: con 70;
+
+init(nil: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	argc := len argl;
+	if(argc < 2){
+		stringit("");
+		exit;
+	}
+	argl = tl argl;
+	for(i := 1; i < argc; i++){
+		if(argc > 2)
+			sys->print("%s:\n", hd argl);
+		stringit(hd argl);
+		argl = tl argl;
+	}
+}
+
+stringit(str: string)
+{
+	cnt := 0;
+	c: int;
+	buf := string array[BUFSIZE] of { * => byte 'z' };
+
+	if(str == nil)
+		fin := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+	else
+		fin = bufio->open(str, Bufio->OREAD);
+	if(fin == nil){
+		sys->fprint(sys->fildes(2), "cannot open %s\n", str);
+		return;
+	}
+	start := big -1;
+	posn := fin.offset();
+	while((c = fin.getc()) >= 0){
+		if(isprint(c)){
+			if(start == big -1)
+				start = posn;
+			buf[cnt++] = c;
+			if(cnt == BUFSIZE){
+				sys->print("%8bd: %s ...\n", start, buf[0: cnt]);
+				start = big -1;
+				cnt = 0;
+			}
+		}
+		else{
+			if(cnt >= MINSPAN)
+				sys->print("%8bd: %s\n", start, buf[0: cnt]);
+			start = big -1;
+			cnt = 0;
+		}
+		posn = fin.offset();
+	}
+	if(cnt >= MINSPAN)
+		sys->print("%8bd: %s\n", start, buf[0: cnt]);
+	fin = nil;
+}
+
+isprint(r: int): int
+{
+	if(r >= ' ' && r < 16r7f || r > 16ra0)
+		return 1;
+	else
+		return 0;
+}
--- /dev/null
+++ b/appl/cmd/styxchat.b
@@ -1,0 +1,546 @@
+implement Styxchat;
+
+#
+# Copyright © 2002,2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+include "string.m";
+	str: String;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "dial.m";
+	dial: Dial;
+
+include "arg.m";
+
+Styxchat: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+msgsize := 64*1024;
+nexttag := 1;
+verbose := 0;
+
+stdin: ref Sys->FD;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	str = load String String->PATH;
+	bufio = load Bufio Bufio->PATH;
+	dial = load Dial Dial->PATH;
+	styx->init();
+
+	client := 1;
+	addr := 0;
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("styxchat [-nsv] [-m messagesize] [dest]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'm' =>
+			msgsize = atoi(arg->earg());
+		's' =>
+			client = 0;
+		'n' =>
+			addr = 1;
+		'v' =>
+			verbose++;
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	fd: ref Sys->FD;
+	if(args == nil){
+		fd = sys->fildes(0);
+		stdin = sys->open("/dev/cons", Sys->ORDWR);
+		if (stdin == nil)
+			err(sys->sprint("can't open /dev/cons: %r"));
+		sys->dup(stdin.fd, 1);
+	}else{
+		if(tl args != nil)
+			arg->usage();
+		stdin = sys->fildes(0);
+		dest := hd args;
+		if(addr){
+			dest = dial->netmkaddr(dest, "net", "styx");
+			if (client){
+				c := dial->dial(dest, nil);
+				if(c == nil)
+					err(sys->sprint("can't dial %s: %r", dest));
+				fd = c.dfd;
+			}else{
+				lc := dial->announce(dest);
+				if(lc == nil)
+					err(sys->sprint("can't announce %s: %r", dest));
+				c := dial->listen(lc);
+				if(c == nil)
+					err(sys->sprint("can't listen on %s: %r", dest));
+				fd = dial->accept(c);
+				if(fd == nil)
+					err(sys->sprint("can't open %s/data: %r", c.dir));
+			}
+		}else{
+			fd = sys->open(dest, Sys->ORDWR);
+			if(fd == nil)
+				err(sys->sprint("can't open %s: %r", dest));
+		}
+	}
+	sys->pctl(Sys->NEWPGRP, nil);
+	if(client){
+		spawn Rreader(fd);
+		Twriter(fd);
+	}else{
+		spawn Treader(fd);
+		Rwriter(fd);
+	}
+}
+
+quit(e: int)
+{
+	fd := sys->open("/prog/"+string sys->pctl(0, nil)+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+	if(e)
+		raise "fail:error";
+	exit;
+}
+
+Rreader(fd: ref Sys->FD)
+{
+	while((m := Rmsg.read(fd, msgsize)) != nil){
+		sys->print("<- %s\n%s", m.text(), Rdump(m));
+		if(tagof m == tagof Rmsg.Readerror)
+			quit(1);
+	}
+	sys->print("styxchat: server hungup\n");
+}
+
+Twriter(fd: ref Sys->FD)
+{
+	in := bufio->fopen(stdin, Sys->OREAD);
+	while((l := in.gets('\n')) != nil){
+		if(l != nil && l[0] == '#')
+			continue;
+		(t, err) := Tparse(l);
+		if(t == nil){
+			if(err != nil)
+				sys->print("?%s\n", err);
+		}else{
+			if(t.tag == 0)
+				t.tag = nexttag;
+			a := t.pack();
+			if(a != nil){
+				sys->print("-> %s\n%s", t.text(), Tdump(t));
+				n := len a;
+				if(n <= msgsize){
+					if(sys->write(fd, a, len a) != len a)
+						sys->print("?write error to server: %r\n");
+					if(t.tag != Styx->NOTAG && t.tag != ~0)
+						nexttag++;
+				}else
+					sys->print("?message bigger than agreed: %d bytes\n", n);
+			}else
+				sys->fprint(sys->fildes(2), "styxchat: T-message conversion failed\n");
+		}
+	}
+}
+
+Rdump(m: ref Rmsg): string
+{
+	if(!verbose)
+		return "";
+	pick r :=m {
+	Read =>
+		return dump(r.data, len r.data, verbose>1);
+	* =>
+		return "";
+	}
+}
+
+Tdump(m: ref Tmsg): string
+{
+	if(!verbose)
+		return "";
+	pick t := m {
+	Write =>
+		return dump(t.data, len t.data, verbose>1);
+	* =>
+		return "";
+	}
+}
+
+isprint(c: int): int
+{
+	return c >= 16r20 && c < 16r7F || c == '\n' || c == '\t' || c == '\r';
+}
+
+textdump(a: array of byte, lim: int): string
+{
+	s := "\ttext(\"";
+	for(i := 0; i < lim; i++)
+		case c := int a[i] {
+		'\t' =>
+			s += "\\t";
+		'\n' =>
+			s += "\\n";
+		'\r' =>
+			s += "\\r";
+		'"' =>
+			s += "\\\"";
+		* =>
+			if(isprint(c))
+				s[len s] = c;
+			else
+				s += sys->sprint("\\u%4.4ux", c);
+		}
+	s += "\")\n";
+	return s;
+}
+
+dump(a: array of byte, lim: int, text: int): string
+{
+	if(a == nil)
+		return "";
+	if(len a < lim)
+		lim = len a;
+	printable := 1;
+	for(i := 0; i < lim; i++)
+		if(!isprint(int a[i])){
+			printable = 0;
+			break;
+		}
+	if(printable)
+		return textdump(a, lim);
+	s := "\tdump(";
+	for(i = 0; i < lim; i++)
+		s += sys->sprint("%2.2ux", int a[i]);
+	s += ")\n";
+	if(text)
+		s += textdump(a, lim);
+	return s;
+}
+
+val(s: string): int
+{
+	if(s == "~0")
+		return ~0;
+	return atoi(s);
+}
+
+bigval(s: string): big
+{
+	if(s == "~0")
+		return ~ big 0;
+	return atob(s);
+}
+
+fid(s: string): int
+{
+	if(s == "nofid" || s == "NOFID")
+		return Styx->NOFID;
+	return val(s);
+}
+
+tag(s: string): int
+{
+	if(s == "~0" || s == "notag" || s == "NOTAG")
+		return Styx->NOTAG;
+	return atoi(s);
+}
+
+dir(name: string, uid: string, gid: string, mode: int, mtime: int, length: big): Sys->Dir
+{
+	d := sys->zerodir;
+	d.name = name;
+	d.uid = uid;
+	d.gid = gid;
+	d.mode = mode;
+	d.mtime = mtime;
+	d.length = length;
+	return d;
+}
+
+Tparse(s: string): (ref Tmsg, string)
+{
+	args := str->unquoted(s);
+	if(args == nil)
+		return (nil, nil);
+	argc := len args;
+	av := array[argc] of string;
+	for(i:=0; args != nil; args = tl args)
+		av[i++] = hd args;
+	case av[0] {
+	"Tversion" =>
+		if(argc != 3)
+			return (nil, "usage: Tversion messagesize version");
+		return (ref Tmsg.Version(Styx->NOTAG, atoi(av[1]), av[2]), nil);
+	"Tauth" =>
+		if(argc != 4)
+			return (nil, "usage: Tauth afid uname aname");
+		return (ref Tmsg.Auth(0, fid(av[1]), av[2], av[3]), nil);
+	"Tflush" =>
+		if(argc != 2)
+			return (nil, "usage: Tflush oldtag");
+		return (ref Tmsg.Flush(0, tag(av[1])), nil);
+	"Tattach" =>
+		if(argc != 5)
+			return (nil, "usage: Tattach fid afid uname aname");
+		return (ref Tmsg.Attach(0, fid(av[1]), fid(av[2]), av[3], av[4]), nil);
+	"Twalk" =>
+		if(argc < 3)
+			return (nil, "usage: Twalk fid newfid [name...]");
+		names: array of string;
+		if(argc > 3)
+			names = av[3:];
+		return (ref Tmsg.Walk(0, fid(av[1]), fid(av[2]), names), nil);
+	"Topen" =>
+		if(argc != 3)
+			return (nil, "usage: Topen fid mode");
+		return (ref Tmsg.Open(0, fid(av[1]), atoi(av[2])), nil);
+	"Tcreate" =>
+		if(argc != 5)
+			return (nil, "usage: Tcreate fid name perm mode");
+		return (ref Tmsg.Create(0, fid(av[1]), av[2], atoi(av[3]), atoi(av[4])), nil);
+	"Tread" =>
+		if(argc != 4)
+			return (nil, "usage: Tread fid offset count");
+		return (ref Tmsg.Read(0, fid(av[1]), atob(av[2]), atoi(av[3])), nil);
+	"Twrite" =>
+		if(argc != 4)
+			return (nil, "usage: Twrite fid offset data");
+		return (ref Tmsg.Write(0, fid(av[1]), atob(av[2]), array of byte av[3]), nil);
+	"Tclunk" =>
+		if(argc != 2)
+			return (nil, "usage: Tclunk fid");
+		return (ref Tmsg.Clunk(0, fid(av[1])), nil);
+	"Tremove" =>
+		if(argc != 2)
+			return (nil, "usage: Tremove fid");
+		return (ref Tmsg.Remove(0, fid(av[1])), nil);
+	"Tstat" =>
+		if(argc != 2)
+			return (nil, "usage: Tstat fid");
+		return (ref Tmsg.Stat(0, fid(av[1])), nil);
+	"Twstat" =>
+		if(argc != 8)
+			return (nil, "usage: Twstat fid name uid gid mode mtime length");
+		return (ref Tmsg.Wstat(0, fid(av[1]), dir(av[2], av[3], av[4], val(av[5]), val(av[6]), bigval(av[7]))), nil);
+	"nexttag" =>
+		if(argc < 2)
+			return (nil, sys->sprint("next tag is %d", nexttag));
+		nexttag = tag(av[1]);
+		return (nil, nil);
+	"dump" =>
+		verbose++;
+		return (nil, nil);
+	* =>
+		return (nil, "unknown message type");
+	}
+}
+
+#
+# server side
+#
+
+Treader(fd: ref Sys->FD)
+{
+	while((m := Tmsg.read(fd, msgsize)) != nil){
+		sys->print("<- %s\n", m.text());
+		if(tagof m == tagof Tmsg.Readerror)
+			quit(1);
+	}
+	sys->print("styxchat: clients hungup\n");
+}
+
+Rwriter(fd: ref Sys->FD)
+{
+	in := bufio->fopen(stdin, Sys->OREAD);
+	while((l := in.gets('\n')) != nil){
+		if(l != nil && l[0] == '#')
+			continue;
+		(r, err) := Rparse(l);
+		if(r == nil){
+			if(err != nil)
+				sys->print("?%s\n", err);
+		}else{
+			a := r.pack();
+			if(a != nil){
+				sys->print("-> %s\n", r.text());
+				n := len a;
+				if(n <= msgsize){
+					if(sys->write(fd, a, len a) != len a)
+						sys->print("?write error to clients: %r\n");
+				}else
+					sys->print("?message bigger than agreed: %d bytes\n", n);
+			}else
+				sys->fprint(sys->fildes(2), "styxchat: R-message conversion failed\n");
+		}
+	}
+}
+
+qid(s: string): Sys->Qid
+{
+	(nf, flds) := sys->tokenize(s, ".");
+	q := Sys->Qid(big 0, 0, 0);
+	if(nf < 1)
+		return q;
+	q.path = atob(hd flds);
+	if(nf < 2)
+		return q;
+	q.vers = atoi(hd tl flds);
+	if(nf < 3)
+		return q;
+	q.qtype = mode(hd tl tl flds);
+	return q;
+}
+
+mode(s: string): int
+{
+	if(len s > 0 && s[0] >= '0' && s[0] <= '9')
+		return atoi(s);
+	mode := 0;
+	for(i := 0; i < len s; i++){
+		case s[i] {
+		'd' =>
+			mode |= Sys->QTDIR;
+		'a' =>
+			mode |= Sys->QTAPPEND;
+		'u' =>
+			mode |= Sys->QTAUTH;
+		'l' =>
+			mode |= Sys->QTEXCL;
+		'f' =>
+			;
+		* =>
+			sys->fprint(sys->fildes(2), "styxchat: unknown mode character %c, ignoring\n", s[i]);
+		}
+	}
+	return mode;
+}
+
+rdir(a: array of string): Sys->Dir
+{
+	d := sys->zerodir;
+	d.qid = qid(a[0]);
+	d.mode = atoi(a[1]) | (d.qid.qtype<<24);
+	d.atime = atoi(a[2]);
+	d.mtime = atoi(a[3]);
+	d.length = atob(a[4]);
+	d.name = a[5];
+	d.uid = a[6];
+	d.gid = a[7];
+	d.muid = a[8];
+	return d;
+}
+
+Rparse(s: string): (ref Rmsg, string)
+{
+	args := str->unquoted(s);
+	if(args == nil)
+		return (nil, nil);
+	argc := len args;
+	av := array[argc] of string;
+	for(i:=0; args != nil; args = tl args)
+		av[i++] = hd args;
+	case av[0] {
+	"Rversion" =>
+		if(argc != 4)
+			return (nil, "usage: Rversion tag messagesize version");
+		return (ref Rmsg.Version(tag(av[1]), atoi(av[2]), av[3]), nil);
+	"Rauth" =>
+		if(argc != 3)
+			return (nil, "usage: Rauth tag aqid");
+		return (ref Rmsg.Auth(tag(av[1]), qid(av[2])), nil);
+	"Rflush" =>
+		if(argc != 2)
+			return (nil, "usage: Rflush tag");
+		return (ref Rmsg.Flush(tag(av[1])), nil);
+	"Rattach" =>
+		if(argc != 3)
+			return (nil, "usage: Rattach tag qid");
+		return (ref Rmsg.Attach(tag(av[1]), qid(av[2])), nil);
+	"Rwalk" =>
+		if(argc < 2)
+			return (nil, "usage: Rwalk tag [qid ...]");
+		qids := array[argc-2] of Sys->Qid;
+		for(i = 0; i < len qids; i++)
+			qids[i] = qid(av[i+2]);
+		return (ref Rmsg.Walk(tag(av[1]), qids), nil);
+	"Ropen" =>
+		if(argc != 4)
+			return (nil, "usage: Ropen tag qid iounit");
+		return (ref Rmsg.Open(tag(av[1]), qid(av[2]), atoi(av[3])), nil);
+	"Rcreate" =>
+		if(argc != 4)
+			return (nil, "usage: Rcreate tag qid iounit");
+		return (ref Rmsg.Create(tag(av[1]), qid(av[2]), atoi(av[3])), nil);
+	"Rread" =>
+		if(argc != 3)
+			return (nil, "usage: Rread tag data");
+		return (ref Rmsg.Read(tag(av[1]), array of byte av[2]), nil);
+	"Rwrite" =>
+		if(argc != 3)
+			return (nil, "usage: Rwrite tag count");
+		return (ref Rmsg.Write(tag(av[1]), atoi(av[2])), nil);
+	"Rclunk" =>
+		if(argc != 2)
+			return (nil, "usage: Rclunk tag");
+		return (ref Rmsg.Clunk(tag(av[1])), nil);
+	"Rremove" =>
+		if(argc != 2)
+			return (nil, "usage: Rremove tag");
+		return (ref Rmsg.Remove(tag(av[1])), nil);
+	"Rstat" =>
+		if(argc != 11)
+			return (nil, "usage: Rstat tag qid mode atime mtime length name uid gid muid");
+		return (ref Rmsg.Stat(tag(av[1]), rdir(av[2:])), nil);
+	"Rwstat" =>
+		if(argc != 8)
+			return (nil, "usage: Rwstat tag");
+		return (ref Rmsg.Wstat(tag(av[1])), nil);
+	"Rerror" =>
+		if(argc != 3)
+			return (nil, "usage: Rerror tag ename");
+		return (ref Rmsg.Error(tag(av[1]), av[2]), nil);
+	"dump" =>
+		verbose++;
+		return (nil, nil);
+	* =>
+		return (nil, "unknown message type");
+	}
+}
+
+atoi(s: string): int
+{
+	(i, nil) := str->toint(s, 0);
+	return i;
+}
+
+# atoi with traditional unix semantics for octal and hex.
+atob(s: string): big
+{
+	(b, nil) := str->tobig(s, 0);
+	return b;
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "styxchat: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/styxlisten.b
@@ -1,0 +1,252 @@
+implement Styxlisten;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	auth: Auth;
+include "dial.m";
+	dial: Dial;
+include "arg.m";
+include "sh.m";
+
+Styxlisten: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+badmodule(p: string)
+{
+	sys->fprint(stderr(), "styxlisten: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+verbose := 0;
+passhostnames := 0;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	auth = load Auth Auth->PATH;
+	if (auth == nil)
+		badmodule(Auth->PATH);
+	dial = load Dial Dial->PATH;
+	if (dial == nil)
+		badmodule(Dial->PATH);
+	if ((e := auth->init()) != nil)
+		error("auth init failed: " + e);
+	keyring = load Keyring Keyring->PATH;
+	if (keyring == nil)
+		badmodule(Keyring->PATH);
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+
+	arg->init(argv);
+	arg->setusage("styxlisten [-a alg]... [-Atsv] [-k keyfile] address cmd [arg...]");
+
+	algs: list of string;
+	doauth := 1;
+	synchronous := 0;
+	trusted := 0;
+	keyfile := "";
+
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'v' =>
+			verbose = 1;
+		'a' =>
+			algs = arg->earg() :: algs;
+		'f' or
+		'k' =>
+			keyfile = arg->earg();
+			if (! (keyfile[0] == '/' || (len keyfile > 2 &&  keyfile[0:2] == "./")))
+				keyfile = "/usr/" + user() + "/keyring/" + keyfile;
+		'h' =>
+			passhostnames = 1;
+		't' =>
+			trusted = 1;
+		's' =>
+			synchronous = 1;
+		'A' =>
+			doauth = 0;
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	if (len argv < 2)
+		arg->usage();
+	arg = nil;
+	if (doauth && algs == nil)
+		algs = getalgs();
+	addr := dial->netmkaddr(hd argv, "tcp", "styx");
+	cmd := tl argv;
+
+	authinfo: ref Keyring->Authinfo;
+	if (doauth) {
+		if (keyfile == nil)
+			keyfile = "/usr/" + user() + "/keyring/default";
+		authinfo = keyring->readauthinfo(keyfile);
+		if (authinfo == nil)
+			error(sys->sprint("cannot read %s: %r", keyfile));
+	}
+
+	c := dial->announce(addr);
+	if (c == nil)
+		error(sys->sprint("cannot announce on %s: %r", addr));
+	if(!trusted){
+		sys->unmount(nil, "/mnt/keys");	# should do for now
+		# become none?
+	}
+
+	lsync := chan[1] of int;
+	if(synchronous)
+		listener(c, popen(ctxt, cmd, lsync), authinfo, algs, lsync);
+	else
+		spawn listener(c, popen(ctxt, cmd, lsync), authinfo, algs, lsync);
+}
+
+listener(c: ref Dial->Connection, mfd: ref Sys->FD, authinfo: ref Keyring->Authinfo, algs: list of string, lsync: chan of int)
+{
+	lsync <-= sys->pctl(0, nil);
+	for (;;) {
+		nc := dial->listen(c);
+		if (nc == nil)
+			error(sys->sprint("listen failed: %r"));
+		if (verbose)
+			sys->fprint(stderr(), "styxlisten: got connection from %s",
+					readfile(nc.dir + "/remote"));
+		dfd := dial->accept(nc);
+		if (dfd != nil) {
+			if(nc.cfd != nil)
+				sys->fprint(nc.cfd, "keepalive");
+			hostname: string;
+			if(passhostnames){
+				hostname = readfile(nc.dir + "/remote");
+				if(hostname != nil)
+					hostname = hostname[0:len hostname - 1];
+			}
+			if (algs == nil) {
+				sync := chan of int;
+				spawn exportproc(sync, mfd, nil, hostname, dfd);
+				<-sync;
+			} else
+				spawn authenticator(dfd, authinfo, mfd, algs, hostname);
+		}
+	}
+}
+
+# authenticate a connection and set the user id.
+authenticator(dfd: ref Sys->FD, authinfo: ref Keyring->Authinfo, mfd: ref Sys->FD,
+		algs: list of string, hostname: string)
+{
+	# authenticate and change user id appropriately
+	(fd, err) := auth->server(algs, authinfo, dfd, 1);
+	if (fd == nil) {
+		if (verbose)
+			sys->fprint(stderr(), "styxlisten: authentication failed: %s\n", err);
+		return;
+	}
+	if (verbose)
+		sys->fprint(stderr(), "styxlisten: client authenticated as %s\n", err);
+	sync := chan of int;
+	spawn exportproc(sync, mfd, err, hostname, fd);
+	<-sync;
+}
+
+exportproc(sync: chan of int, fd: ref Sys->FD, uname, hostname: string, dfd: ref Sys->FD)
+{
+	sys->pctl(Sys->NEWFD | Sys->NEWNS, 2 :: fd.fd :: dfd.fd :: nil);
+	fd = sys->fildes(fd.fd);
+	dfd = sys->fildes(dfd.fd);
+	sync <-= 1;
+
+	# XXX unfortunately we cannot pass through the aname from
+	# the original attach, an inherent shortcoming of this scheme.
+	if (sys->mount(fd, nil, "/", Sys->MREPL|Sys->MCREATE, hostname) == -1)
+		error(sys->sprint("cannot mount for user '%s': %r\n", uname));
+
+	sys->export(dfd, "/", Sys->EXPWAIT);
+}
+
+error(e: string)
+{
+	sys->fprint(stderr(), "styxlisten: %s\n", e);
+	raise "fail:error";
+}
+	
+
+popen(ctxt: ref Draw->Context, argv: list of string, lsync: chan of int): ref Sys->FD
+{
+	sync := chan of int;
+	fds := array[2] of ref Sys->FD;
+	sys->pipe(fds);
+	spawn runcmd(ctxt, argv, fds[0], sync, lsync);
+	<-sync;
+	return fds[1];
+}
+
+runcmd(ctxt: ref Draw->Context, argv: list of string, stdin: ref Sys->FD,
+		sync: chan of int, lsync: chan of int)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	stdin = nil;
+	sync <-= 0;
+	sh := load Sh Sh->PATH;
+	e := sh->run(ctxt, argv);
+	kill(<-lsync, "kill");		# kill listener, as command has exited
+	if(verbose){
+		if(e != nil)
+			sys->fprint(stderr(), "styxlisten: command exited with error: %s\n", e);
+		else
+			sys->fprint(stderr(), "styxlisten: command exited\n");
+	}
+}
+
+kill(pid: int, how: string)
+{
+	sys->fprint(sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE), "%s", how);
+}
+
+user(): string
+{
+	if ((s := readfile("/dev/user")) == nil)
+		return "none";
+	return s;
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
+
+getalgs(): list of string
+{
+	sslctl := readfile("#D/clone");
+	if (sslctl == nil) {
+		sslctl = readfile("#D/ssl/clone");
+		if (sslctl == nil)
+			return nil;
+		sslctl = "#D/ssl/" + sslctl;
+	} else
+		sslctl = "#D/" + sslctl;
+	(nil, algs) := sys->tokenize(readfile(sslctl + "/encalgs") + " " + readfile(sslctl + "/hashalgs"), " \t\n");
+	return "none" :: algs;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/cmd/styxmon.b
@@ -1,0 +1,110 @@
+implement Styxmon;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+include "sh.m";
+include "arg.m";
+
+Styxmon: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "styxmon: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+showdata := 0;
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		badmod(Styx->PATH);
+	styx->init();
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		badmod(Arg->PATH);
+	arg->init(argv);
+	arg->setusage("usage: styxmon [-d] cmd [arg...]");
+	while((opt := arg->opt()) != 0){
+		case opt{
+		'd' =>
+			showdata = 1;
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	if(argv == nil)
+		arg->usage();
+	fd0 := sys->fildes(0);
+	fd1 := popen(ctxt, argv);
+	sync := chan of int;
+	spawn msgtx(fd0, fd1, sync, "tmsg");
+	<-sync;
+	spawn msgtx(fd1, fd0, sync, "rmsg");
+	<-sync;
+}
+
+msgtx(f0, f1: ref Sys->FD, sync: chan of int, what: string)
+{
+	sys->pctl(Sys->NEWFD|Sys->NEWNS, 2 :: f0.fd :: f1.fd :: nil);
+	sync <-= 1;
+	f0 = sys->fildes(f0.fd);
+	f1 = sys->fildes(f1.fd);
+	stderr := sys->fildes(2);
+	for (;;) {
+		(d, err) := styx->readmsg(f0, 0);
+		if(d == nil){
+			if(err != nil)
+				sys->fprint(stderr, "styxmon: error from %s: %s\n", what, err);
+			else
+				sys->fprint(stderr, "styxmon: eof from %s\n", what);
+			exit;
+		}
+		if(styx->istmsg(d)){
+			(n, m) := Tmsg.unpack(d);
+			if(n != len d){
+				sys->fprint(stderr, "styxmon: %s message error (%d/%d)\n", what, n, len d);
+			}else{
+				sys->fprint(stderr, "%s\n", m.text());
+			}
+		}else{
+			(n, m) := Rmsg.unpack(d);
+			if(n != len d){
+				sys->fprint(stderr, "styxmon: %s message error (%d/%d)\n", what, n, len d);
+				if(m != nil)
+					sys->fprint(stderr, "err: %s\n", m.text());
+			}else{
+				sys->fprint(stderr, "%s\n", m.text());
+			}
+		}
+		sys->write(f1, d, len d);
+	}
+}
+
+popen(ctxt: ref Draw->Context, argv: list of string): ref Sys->FD
+{
+	sync := chan of int;
+	fds := array[2] of ref Sys->FD;
+	sys->pipe(fds);
+	spawn runcmd(ctxt, argv, fds[0], sync);
+	<-sync;
+	return fds[1];
+}
+
+runcmd(ctxt: ref Draw->Context, argv: list of string, stdin: ref Sys->FD, sync: chan of int)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	stdin = nil;
+	sync <-= 0;
+	sh := load Sh Sh->PATH;
+	sh->run(ctxt, argv);
+}
--- /dev/null
+++ b/appl/cmd/sum.b
@@ -1,0 +1,59 @@
+implement Sum;
+
+include "sys.m";
+include "draw.m";
+include "crc.m";
+
+Sum : module
+{
+	init : fn(nil : ref Draw->Context, argv : list of string);
+};
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	sys := load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+	crcm := load Crc Crc->PATH;
+	crcs := crcm->init(0, 0);
+	a := tl argv;
+	buf := array[Sys->ATOMICIO] of byte;
+	err := 0;
+	for ( ; a != nil; a = tl a) {
+		s := hd a;
+		(ok, d) := sys->stat(s);
+		if (ok < 0) {
+			sys->fprint(stderr, "sum: cannot get status of %s: %r\n", s);
+			err = 1;
+			continue;
+		}
+		if (d.mode & Sys->DMDIR)
+			continue;
+		fd := sys->open(s, Sys->OREAD);
+		if (fd == nil) {
+			sys->fprint(stderr, "sum: cannot open %s: %r\n", s);
+			err = 1;
+			continue;
+		}
+		crc := 0;
+		nbytes := big 0;
+		while((nr := sys->read(fd, buf, len buf)) > 0){
+			crc = crcm->crc(crcs, buf, nr);
+			nbytes += big nr;
+		}
+		if(nr < 0) {
+			sys->fprint(stderr, "sum: error reading %s: %r\n", s);
+			err = 1;
+		}
+		# encode the length but make n==0 not 0
+		l := int (nbytes & big 16rFFFFFFFF);
+		buf[0] = byte((l>>24)^16rCC);
+		buf[1] = byte((l>>16)^16r55);
+		buf[2] = byte((l>>8)^16rCC);
+		buf[3] = byte(l^16r55);
+		crc = crcm->crc(crcs, buf, 4);
+		sys->print("%.8ux %6bd %s\n", crc, nbytes, s);
+		crcm->reset(crcs);
+	}
+	if(err)
+		raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/tail.b
@@ -1,0 +1,379 @@
+implement Tail;
+
+include "sys.m";
+sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+bufmod : Bufio;
+Iobuf : import bufmod;
+
+include "string.m";
+	str : String;
+
+count, anycount, follow : int;
+file : ref sys->FD;
+bout : ref Iobuf;
+BSize : con 8*1024;
+
+BEG, END, CHARS, LINES , FWD, REV : con iota;
+ 
+origin := END;
+units := LINES;
+dir := FWD;
+
+
+Tail: module
+{
+	init:	fn(nil: ref Draw->Context, argv: list of string);
+};
+
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	bufmod = load Bufio Bufio->PATH;
+	seekable : int;
+	bout = bufmod->fopen(sys->fildes(1),bufmod->OWRITE);
+	argv=parse(tl argv);
+	if(dir==REV && (units==CHARS || follow || origin==BEG))
+		fail("incompatible options");
+	if(!anycount){
+		if (dir==REV)
+			count= 16r7fffffff;
+		else
+			count = 10;
+	}
+	if(origin==BEG && units==LINES && count>0)
+		count--;
+	if(len argv > 1)
+		usage();
+	if(argv == nil || hd argv == "-") {
+		file = sys->fildes(0);
+		seekable = 0;
+	}
+	else {
+		if((file=sys->open(hd argv,sys->OREAD)) == nil )
+			fatal(hd argv);
+		(nil, stat) := sys->fstat(file);
+		seekable = sys->seek(file,big 0,sys->SEEKSTART) == big 0 && stat.length > big 0;
+	}
+
+	if(!seekable && origin==END)
+		keep();
+	else if(!seekable && origin==BEG)
+		skip();
+	else if(units==CHARS && origin==END){
+		tseek(big -count, Sys->SEEKEND);
+		copy();
+	}
+	else if(units==CHARS && origin==BEG){
+		tseek(big count, Sys->SEEKSTART);
+		copy();
+	}
+	else if(units==LINES && origin==END)
+		reverse();
+	else if(units==LINES && origin==BEG)
+		skip();
+	if(follow){
+		if(seekable){
+			d : sys->Dir;
+			d.length=big -1;
+			for(;;){
+				d=trunc(d.length);
+				copy();
+				sys->sleep(5000);
+			}
+		}else{
+			for(;;){
+				copy();
+				sys->sleep(5000);
+			}
+		}
+	}
+	exit;
+}
+
+
+trunc(length : big) : sys->Dir
+{
+	(nil,d):=sys->fstat(file);
+	if(d.length < length)
+		d.length = tseek(big 0, sys->SEEKSTART);
+	return d;
+}
+
+
+skip()	# read past head of the file to find tail 
+{
+	n : int;
+	buf := array[BSize] of byte;
+	if(units == CHARS) {
+		for( ; count>0; count -=n) {
+			if (count<BSize) 
+				n=count;
+			else
+				n=BSize;
+			n = tread(buf, n);
+			if(n == 0)
+				return;
+		}
+	} else { # units == LINES
+		i:=0;
+		n=0;
+		while(count > 0) {
+			n = tread(buf, BSize);
+			if(n == 0)
+				return;
+			for(i=0; i<n && count>0; i++)
+				if(buf[i]==byte '\n')
+					count--;
+		}
+		twrite(buf[i:n]);
+	}
+	copy();
+}
+
+
+copy()
+{
+	buf := array[BSize] of byte;
+	while((n := tread(buf, BSize)) > 0){
+		twrite(buf[0:n]);
+	}
+	bout.flush();	
+}
+
+
+keep()	# read whole file, keeping the tail 
+{	# complexity=length(file)*length(tail).  could be linear
+	j, k : int;
+	length:=0;
+	buf : array of byte;
+	tbuf : array of byte;
+	bufsize := 0;
+	for(n:=1; n;) {
+		if(length+BSize > bufsize ) {
+			bufsize += 2*BSize;
+			tbuf = array[bufsize+1] of byte;
+			tbuf[0:]=buf[0:];
+			buf = tbuf;
+		}
+		for( ; n && length<bufsize; length+=n)
+			n = tread(buf[length:], bufsize-length);
+		if(count >= length)
+			continue;
+		if(units == CHARS)
+			j = length - count;
+		else{ # units == LINES 
+			if (int buf[length-1]=='\n')
+				j =  length-1;
+			else
+				j=length;
+			for(k=0; j>0; j--)
+				if(int buf[j-1] == '\n')
+					if(++k >= count)
+						break;
+		}
+		length-=j;
+		buf[0:]=buf[j:j+length];
+	}
+	if(dir == REV) {
+		if(length>0 && buf[length-1]!= byte '\n')
+			buf[length++] = byte '\n';
+		for(j=length-1 ; j>0; j--)
+			if(buf[j-1] == byte '\n') {
+				twrite(buf[j:length]);
+				if(--count <= 0)
+					return;
+				length = j;
+			}
+	}
+	if(count > 0 && length > 0)
+		twrite(buf[0:length]);
+	bout.flush();
+}
+
+reverse()	# count backward and print tail of file 
+{
+	length := 0;
+	n := 0;
+	buf : array of byte;
+	pos := tseek(big 0, sys->SEEKEND);
+	bufsize := 0;
+	for(first:=1; pos>big 0 && count>0; first=0) {
+		if (pos>big BSize)
+			n = BSize;
+		else
+			n = int pos;
+		pos -= big n;
+		if(length+2*n > bufsize) {
+			bufsize += BSize*((length+2*n-bufsize+BSize-1)/BSize);
+			tbuf := array[bufsize+1] of byte;
+			tbuf[0:] = buf;
+			buf = tbuf;
+		}
+		length += n;
+		abuf := array[length] of byte;
+		abuf[0:] = buf[0:length];
+		buf[n:] = abuf;
+		tseek(pos, sys->SEEKSTART);
+		if(tread(buf, n) != n)
+			fatal("length error");
+		if(first && buf[length-1]!= byte '\n')
+			buf[length++] = byte '\n';
+		for(n=length-1 ; n>0 && count>0; n--)
+			if(buf[n-1] == byte '\n') {
+				count--;
+				if(dir == REV){
+					twrite(buf[n:length]);
+					bout.flush();
+				}
+				length = n;
+			}
+	}
+	if(dir == FWD) {
+		if (n==0)
+			tseek(big 0 , sys->SEEKSTART);
+		else
+			tseek(pos+big n+big 1, sys->SEEKSTART);
+			
+		copy();
+	} else if(count > 0)
+		twrite(buf[0:length]);
+	bout.flush();
+}
+
+
+tseek(o : big, p: int) : big
+{
+	o = sys->seek(file, o, p);
+	if(o == big -1)
+		fatal("");
+	return o;
+}
+
+
+tread(buf: array of byte, n: int): int
+{
+	r := sys->read(file, buf, n);
+	if(r == -1)
+		fatal("");
+	return r;
+}
+
+
+twrite(buf:array of byte)
+{
+	str1:= string buf;
+	if(bout.puts(str1)!=len str1)
+		fatal("");
+}
+
+
+		
+fatal(s : string)
+{
+	sys->fprint(sys->fildes(2), "tail: %s: %r\n", s);
+	exit;
+}
+
+fail(s : string)
+{
+	sys->fprint(sys->fildes(2), "tail: %s\n", s);
+	exit;
+}
+
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: tail [-n N] [-c N] [-f] [-r] [+-N[bc][fr]] [file]\n");
+	exit;
+}
+
+
+getnumber(s: string) : int
+{
+	i:=0;
+	if (len s == 0) return 0;
+	if(s[i]=='-' || s[i]=='+') {
+		if (len s == 1)
+			return 0;
+		i++;
+	}
+	if(!(s[i]>='0' && s[i]<='9'))
+		return 0;
+	if(s[0] == '+')
+		origin = BEG;
+	if(anycount++)
+		fail("excess option");
+	if (s[0]=='-')
+		s=s[1:];
+	(count,nil) = str->toint(s,10);
+	if(count < 0){	# protect int args (read, fwrite) 
+		fail("too big");
+	}
+	return 1;
+}
+	
+parse(args : list of string) : list of string 
+{
+	for(; args!=nil ; args = tl args ) {
+		hdarg := hd args;
+		if(getnumber(hdarg))
+			suffix(hdarg);
+		else if(len hdarg > 1 && hdarg[0] == '-')
+			case (hdarg[1]) {
+			 'c' or 'n'=>
+				if (hdarg[1]=='c')
+					units = CHARS;
+				if(len hdarg>2 && getnumber(hdarg[2:]))
+					;
+				else if(tl args != nil && getnumber(hd tl args)) {
+					args = tl args;
+				} else
+					usage();
+			 'r' =>
+				dir = REV;
+			 'f' =>
+				follow++;
+			 '-' =>
+				args = tl args;
+			}
+		else
+			break;
+	}
+	return args;
+}
+
+
+suffix(s : string)
+{
+	i:=0;
+	while(i < len s && str->in(s[i],"0123456789+-"))
+		i++;
+	if (i==len s)
+		return;
+	if (s[i]=='b')
+		if((count*=1024) < 0)
+			fail("too big");
+	if (s[i]=='c' || s[i]=='b')
+		units = CHARS;
+	if (s[i]=='l' || s[i]=='c' || s[i]=='b')
+		i++;
+	if (i<len s){
+		case s[i] {
+		 'r'=>
+			dir = REV;
+			return;
+		 'f'=>
+			follow++;
+			return;
+		}
+	}
+	i++;
+	if (i<len s)
+		usage();
+}
--- /dev/null
+++ b/appl/cmd/tarfs.b
@@ -1,0 +1,438 @@
+implement Tarfs;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "arg.m";
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+include "styxservers.m";
+	styxservers: Styxservers;
+	Fid, Styxserver, Navigator, Navop: import styxservers;
+	Enotfound: import styxservers;
+
+Tarfs: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+File: adt {
+	x:	int;
+	name:	string;
+	mode:	int;
+	uid:	string;
+	gid:	string;
+	mtime:	int;
+	length:	big;
+	offset:	big;
+	parent:	cyclic ref File;
+	children:	cyclic list of ref File;
+
+	find:		fn(f: self ref File, name: string): ref File;
+	enter:	fn(d: self ref File, f: ref File);
+	stat:		fn(d: self ref File): ref Sys->Dir;
+};
+
+tarfd: ref Sys->FD;
+pflag: int;
+root: ref File;
+files: array of ref File;
+pathgen: int;
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "tarfs: %s\n", s);
+	raise "fail:error";
+}
+
+checkload[T](m: T, path: string)
+{
+	if(m == nil)
+		error(sys->sprint("can't load %s: %r", path));
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+	styx = load Styx Styx->PATH;
+	checkload(styx, Styx->PATH);
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	checkload(styxservers, Styxservers->PATH);
+	styxservers->init(styx);
+	daytime = load Daytime Daytime->PATH;
+	checkload(daytime, Daytime->PATH);
+
+	arg := load Arg Arg->PATH;
+	checkload(arg, Arg->PATH);
+	arg->setusage("tarfs [-a|-b|-ac|-bc] [-D] file mountpoint");
+	arg->init(args);
+	flags := Sys->MREPL;
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>	flags = Sys->MAFTER;
+		'b' =>	flags = Sys->MBEFORE;
+		'D' =>	styxservers->traceset(1);
+		'p' =>	pflag++;
+		* =>		arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 2)
+		arg->usage();
+	arg = nil;
+
+	file := hd args;
+	args = tl args;
+	mountpt := hd args;
+
+	sys->pctl(Sys->FORKFD, nil);
+
+	files = array[100] of ref File;
+	root = files[0] = ref File;
+	root.x = 0;
+	root.name = "/";
+	root.mode = Sys->DMDIR | 8r555;
+	root.uid = "0";
+	root.gid = "0";
+	root.length = big 0;
+	root.offset = big 0;
+	root.mtime = 0;
+	pathgen = 1;
+
+	tarfd = sys->open(file, Sys->OREAD);
+	if(tarfd == nil)
+		error(sys->sprint("can't open %s: %r", file));
+	if(readtar(tarfd) < 0)
+		error(sys->sprint("error reading %s: %r", file));
+
+	fds := array[2] of ref Sys->FD;
+	if(sys->pipe(fds) < 0)
+		error(sys->sprint("can't create pipe: %r"));
+
+	navops := chan of ref Navop;
+	spawn navigator(navops);
+
+	(tchan, srv) := Styxserver.new(fds[0], Navigator.new(navops), big 0);
+	fds[0] = nil;
+
+	pidc := chan of int;
+	spawn server(tchan, srv, pidc, navops);
+	<-pidc;
+
+	if(sys->mount(fds[1], nil, mountpt, flags, nil) < 0)
+		error(sys->sprint("can't mount tarfs: %r"));
+}
+
+server(tchan: chan of ref Tmsg, srv: ref Styxserver, pidc: chan of int, navops: chan of ref Navop)
+{
+	pidc <-= sys->pctl(Sys->FORKNS|Sys->NEWFD, 1::2::srv.fd.fd::tarfd.fd::nil);
+Serve:
+	while((gm := <-tchan) != nil){
+		root.mtime = daytime->now();
+		pick m := gm {
+		Readerror =>
+			sys->fprint(sys->fildes(2), "tarfs: mount read error: %s\n", m.error);
+			break Serve;
+		Read =>
+			(c, err) := srv.canread(m);
+			if(c == nil){
+				srv.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			if(c.qtype & Sys->QTDIR){
+				srv.default(m);	# does readdir
+				break;
+			}
+			f := files[int c.path];
+			n := m.count;
+			if(m.offset + big n > f.length)
+				n = int (f.length - m.offset);
+			if(n <= 0){
+				srv.reply(ref Rmsg.Read(m.tag, nil));
+				break;
+			}
+			a := array[n] of byte;
+			sys->seek(tarfd, f.offset+m.offset, 0);
+			n = sys->read(tarfd, a, len a);
+			if(n < 0)
+				srv.reply(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+			else
+				srv.reply(ref Rmsg.Read(m.tag, a[0:n]));
+		* =>
+			srv.default(gm);
+		}
+	}
+	navops <-= nil;		# shut down navigator
+}
+
+File.enter(dir: self ref File, f: ref File)
+{
+	if(pathgen >= len files){
+		t := array[pathgen+50] of ref File;
+		t[0:] = files;
+		files = t;
+	}
+	if(0)
+		sys->print("enter %s, %s [#%ux %bd]\n", dir.name, f.name, f.mode, f.length);
+	f.x = pathgen;
+	f.parent = dir;
+	dir.children = f :: dir.children;
+	files[pathgen++] = f;
+}
+
+File.find(f: self ref File, name: string): ref File
+{
+	for(g := f.children; g != nil; g = tl g)
+		if((hd g).name == name)
+			return hd g;
+	return nil;
+}
+
+File.stat(f: self ref File): ref Sys->Dir
+{
+	d := ref sys->zerodir;
+	d.mode = f.mode;
+	if(pflag) {
+		d.mode &= 16rff<<24;
+		d.mode |= 8r444;
+		if(f.mode & Sys->DMDIR)
+			d.mode |= 8r111;
+	}
+	d.qid.path = big f.x;
+	d.qid.qtype = f.mode>>24;
+	d.name = f.name;
+	d.uid = f.uid;
+	d.gid = f.gid;
+	d.muid = d.uid;
+	d.length = f.length;
+	d.mtime = f.mtime;
+	d.atime = root.mtime;
+	return d;
+}
+
+split(s: string): (string, string)
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '/'){
+			for(j := i+1; j < len s && s[j] == '/';)
+				j++;
+			return (s[0:i], s[j:]);
+		}
+	return (nil, s);
+}
+
+putfile(f: ref File)
+{
+	orign := n := f.name;
+	df := root;
+	for(;;){
+		(d, rest) := split(n);
+		if(d == ".") {
+			n = rest;
+			continue;
+		}
+		if(d == "..") {
+			warn(sys->sprint("ignoring %q", orign));
+			return;
+		}
+		if(d == nil || rest == nil){
+			f.name = n;
+			break;
+		}
+		g := df.find(d);
+		if(g == nil){
+			g = ref *f;
+			g.name = d;
+			g.mode |= Sys->DMDIR;
+			df.enter(g);
+		}
+		n = rest;
+		df = g;
+	}
+	if(f.name != "." && f.name != "..")
+		df.enter(f);
+}
+
+navigator(navops: chan of ref Navop)
+{
+	while((m := <-navops) != nil){
+		pick n := m {
+		Stat =>
+			n.reply <-= (files[int n.path].stat(), nil);
+		Walk =>
+			f := files[int n.path];
+			if((f.mode & Sys->DMDIR) == 0){
+				n.reply <-= (nil, "not a directory");
+				break;
+			}
+			case n.name {
+			".." =>
+				if(f.parent != nil)
+					f = f.parent;
+				n.reply <-= (f.stat(), nil);
+			* =>
+				f = f.find(n.name);
+				if(f != nil)
+					n.reply <-= (f.stat(), nil);
+				else
+					n.reply <-= (nil, Enotfound);
+			}
+		Readdir =>
+			f := files[int n.path];
+			if((f.mode & Sys->DMDIR) == 0){
+				n.reply <-= (nil, "not a directory");
+				break;
+			}
+			g := f.children;
+			for(i := n.offset; i > 0 && g != nil; i--)
+				g = tl g;
+			for(; --n.count >= 0 && g != nil; g = tl g)
+				n.reply <-= ((hd g).stat(), nil);
+			n.reply <-= (nil, nil);
+		}
+	}
+}
+
+Blocksize: con 512;
+Namelen: con 100;
+Userlen: con 32;
+
+Oname: con 0;
+Omode: con Namelen;
+Ouid: con Omode+8;
+Ogid: con Ouid+8;
+Osize: con Ogid+8;
+Omtime: con Osize+12;
+Ochksum: con Omtime+12;
+Olinkflag: con Ochksum+8;
+Olinkname: con Olinkflag+1;
+# POSIX extensions follow
+Omagic: con Olinkname+Namelen;	# ustar
+Ouname: con Omagic+8;
+Ogname: con Ouname+Userlen;
+Omajor: con Ogname+Userlen;
+Ominor: con Omajor+8;
+Oend: con Ominor+8;
+
+readtar(fd: ref Sys->FD): int
+{
+	buf := array[Blocksize] of byte;
+	offset := big 0;
+	for(;;){
+		sys->seek(fd, offset, 0);
+		n := sys->read(fd, buf, len buf);
+		if(n == 0)
+			break;
+		if(n < 0)
+			return -1;
+		if(n < len buf){
+			sys->werrstr(sys->sprint("short read: expected %d, got %d", len buf, n));
+			return -1;
+		}
+		if(buf[0] == byte 0)
+			break;
+		offset += big Blocksize;
+		mode := int octal(buf[Omode:Ouid]);
+		linkflag := int buf[Olinkflag];
+		# don't use linkname
+		if((mode & 8r170000) == 8r40000)
+			linkflag = '5';
+		mode &= 8r777;
+		case linkflag {
+		'1' or '2' or 's' =>		# ignore links and symbolic links
+			continue;
+		'3' or '4' or '6' =>	# special file or fifo (leave them, but empty)
+			;
+		'5' =>
+			mode |= Sys->DMDIR;
+		}
+		f := ref File;
+		f.name = ascii(buf[Oname:Omode]);
+		while(len f.name > 0 && f.name[0] == '/')
+			f.name = f.name[1:];
+		while(len f.name > 0 && f.name[len f.name-1] == '/'){
+			mode |= Sys->DMDIR;
+			f.name = f.name[:len f.name-1];
+		}
+		f.mode = mode;
+		f.uid = string octal(buf[Ouid:Ogid]);
+		f.gid = string octal(buf[Ogid:Osize]);
+		f.length = octal(buf[Osize:Omtime]);
+		if(f.length < big 0)
+			error(sys->sprint("tar file size is negative: %s", f.name));
+		if(mode & Sys->DMDIR)
+			f.length = big 0;
+		f.mtime = int octal(buf[Omtime:Ochksum]);
+		sum := int octal(buf[Ochksum:Olinkflag]);
+		if(sum != checksum(buf))
+			error(sys->sprint("checksum error on %s", f.name));
+		f.offset = offset;
+		offset += f.length;
+		v := int (f.length % big Blocksize);
+		if(v != 0)
+			offset += big (Blocksize-v);
+
+		if(ascii(buf[Omagic:Ouname]) == "ustar" && string buf[Omagic+6:Omagic+8] == "00") {
+			f.uid = ascii(buf[Ouname:Ogname]);
+			f.gid = ascii(buf[Ogname:Omajor]);
+		}
+			
+		putfile(f);
+	}
+	return 0;
+}
+
+ascii(b: array of byte): string
+{
+	top := 0;
+	for(i := 0; i < len b && b[i] != byte 0; i++)
+		if(int b[i] >= 16r80)
+			top = 1;
+	if(top)
+		;	# TO DO: do it by hand if not utf-8
+	return string b[0:i];
+}
+
+octal(b: array of byte): big
+{
+	v := big 0;
+	for(i := 0; i < len b && b[i] == byte ' '; i++)
+		;
+	for(; i < len b && b[i] != byte 0 && b[i] != byte ' '; i++){
+		c := int b[i];
+		if(!(c >= '0' && c <= '7'))
+			error(sys->sprint("bad octal value in tar header: %s (%c)", string b, c));
+		v = (v<<3) | big (c-'0');
+	}
+	return v;
+}
+
+checksum(b: array of byte): int
+{
+	c := 0;
+	for(i := 0; i < Ochksum; i++)
+		c += int b[i];
+	for(; i < Olinkflag; i++)
+		c += ' ';
+	for(; i < len b; i++)
+		c += int b[i];
+	return c;
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+}
--- /dev/null
+++ b/appl/cmd/tclsh.b
@@ -1,0 +1,48 @@
+implement Tclsh;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufmod : Bufio;
+Iobuf : import bufmod;
+
+include "tk.m";
+
+include "../lib/tcl.m";
+	tcl : Tcl_Core;
+
+Tclsh: module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(ctxt: ref Draw->Context, argv : list of string){
+	sys=load Sys Sys->PATH;
+	tcl=load Tcl_Core Tcl_Core->PATH;
+	if (tcl==nil){
+		sys->print("Cannot load Tcl (%r)\n");
+		exit;
+	}	
+	bufmod=load Bufio Bufio->PATH;
+	if (bufmod==nil){
+		sys->print("Cannot load Bufio (%r)\n");
+		exit;
+	}	
+	lines:=chan of string;
+	tcl->init(ctxt,argv);
+	new_inp := "tcl%";
+	spawn tcl->grab_lines(nil,nil,lines);
+	for(;;){
+		alt{
+			line := <-lines =>
+				line = tcl->prepass(line);
+				msg:= tcl->evalcmd(line,0);
+				if (msg!=nil)
+					sys->print("%s\n",msg);
+				sys->print("%s ", new_inp);
+				tcl->clear_error();
+		}
+	}
+}
--- /dev/null
+++ b/appl/cmd/tcs.b
@@ -1,0 +1,204 @@
+implement Tcs;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "convcs.m";
+	convcs: Convcs;
+
+Tcs: module
+{
+	init: fn (nil: ref Draw->Context, args: list of string);
+};
+
+stderr: ref Sys->FD;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	if ((arg := load Arg Arg->PATH) == nil)
+		badmodule(Arg->PATH);
+	if ((bufio = load Bufio Bufio->PATH) == nil)
+		badmodule(Bufio->PATH);
+	if ((convcs = load Convcs Convcs->PATH) == nil)
+		badmodule(Convcs->PATH);
+
+	arg->init(args);
+	arg->setusage("tcs [-C configfile] [-l] [-f ics] [-t ocs] file ...");
+	lflag := 0;
+	vflag := 0;
+	ics := "utf8";
+	ocs := "utf8";
+	csfile := "";
+	while ((c := arg->opt()) != 0) {
+		case c {
+		'C' =>
+			csfile = arg->arg();
+		'f' =>
+			ics = arg->arg();
+		'l' =>
+			lflag = 1;
+		't' =>
+			ocs = arg->arg();
+		'v' =>
+			vflag = 1;
+		* =>
+			arg->usage();
+		}
+	}
+	file := arg->arg();
+
+	out := bufio->fopen(sys->fildes(1), Sys->OWRITE);
+	err := convcs->init(csfile);
+	if (err != nil) {
+		sys->fprint(stderr, "convcs: %s\n", err);
+		raise "fail:init";
+	}
+
+	if (lflag) {
+		if (file != nil)
+			dumpaliases(out, file, vflag);
+		else
+			dumpconvs(out, vflag);
+		return;
+	}
+	
+	stob: Stob;
+	btos: Btos;
+	(stob, err) = convcs->getstob(ocs);
+	if (err != nil) {
+		sys->fprint(stderr, "tcs: %s: %s\n", ocs, err);
+		raise "fail:badarg";
+	}
+	(btos, err) = convcs->getbtos(ics);
+	if (err != nil) {
+		sys->fprint(stderr, "tcs: %s: %s\n", ics, err);
+		raise "fail:badarg";
+	}
+
+	fd: ref Sys->FD;
+	if (file == nil) {
+		fd = sys->fildes(0);
+		file = "standard input";
+	} else
+		fd = open(file);
+
+	inbuf := array [Sys->ATOMICIO] of byte;
+	for(;;){
+		btoss: Convcs->State = nil;
+		stobs: Convcs->State = nil;
+
+		unc := 0;
+		nc: int;
+		s: string;
+		while ((n := sys->read(fd, inbuf[unc:], len inbuf - unc)) > 0) {
+			n += unc;		# include unconsumed prefix
+			(btoss, s, nc) = btos->btos(btoss, inbuf[0:n], -1);
+			if (s != nil)
+				stobs = output(out, stob, stobs, s);
+			# copy down unconverted part of buffer
+			unc = n - nc;
+			if (unc > 0 && nc > 0)
+				inbuf[0:] = inbuf[nc: n];
+		}
+		if (n < 0) {
+			sys->fprint(stderr, "tcs: error reading %s: %r\n", file);
+			raise "fail:read error";
+		}
+
+		# flush conversion state
+		(nil, s, nil) = btos->btos(btoss, inbuf[0: unc], 0);
+		if(s != nil)
+			stobs = output(out, stob, stobs, s);
+		output(out, stob, stobs, "");
+
+		if(out.flush() != 0) {
+			sys->fprint(stderr, "tcs: write error: %r\n");
+			raise "fail:write error";
+		}
+		file = arg->arg();
+		if (file == nil)
+			break;
+		fd = open(file);
+	}
+}
+
+output(out: ref Iobuf, stob: Stob, stobs: Convcs->State, s: string): Convcs->State
+{
+	outbuf: array of byte;
+	(stobs, outbuf) = stob->stob(stobs, s);
+	if(outbuf != nil)
+		out.write(outbuf, len outbuf);
+	return stobs;
+}
+
+badmodule(s: string)
+{
+	sys->fprint(stderr, "tcs: cannot load module %s: %r\n", s);
+	raise "fail:init";
+}
+
+dumpconvs(out: ref Iobuf, verbose: int)
+{
+	first := 1;
+	for (csl := convcs->enumcs(); csl != nil; csl = tl csl) {
+		(name, desc, mode) := hd csl;
+		if (!verbose) {
+			if (!first)
+				out.putc(' ');
+			out.puts(name);
+		} else {
+			ms := "";
+			case mode {
+			Convcs->BTOS =>
+				ms = "(from)";
+			Convcs->STOB =>
+				ms = "(to)";
+			}
+			out.puts(sys->sprint("%s%s\t%s\n", name, ms, desc));
+		}
+		first = 0;
+	}
+	if (!verbose)
+		out.putc('\n');
+	out.flush();
+}
+
+dumpaliases(out: ref Iobuf, cs: string, verbose: int)
+{
+	(desc, asl) := convcs->aliases(cs);
+	if (asl == nil) {
+		sys->fprint(stderr, "%s\n", desc);
+		return;
+	}
+
+	if (verbose) {
+		out.puts(desc);
+		out.putc('\n');
+	}
+	first := 1;
+	for (; asl != nil; asl = tl asl) {
+		a := hd asl;
+		if (!first)
+			out.putc(' ');
+		out.puts(a);
+		first = 0;
+	}
+	out.putc('\n');
+	out.flush();
+}
+
+open(path: string): ref Sys->FD
+{
+	fd := sys->open(path, Bufio->OREAD);
+	if (fd == nil) {
+		sys->fprint(stderr, "tcs: cannot open %s: %r\n", path);
+		raise "fail:open";
+	}
+	return fd;
+}
--- /dev/null
+++ b/appl/cmd/tee.b
@@ -1,0 +1,79 @@
+implement Tee;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "arg.m";
+
+Tee: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+File: adt
+{
+	fd:	ref Sys->FD;
+	name:	string;
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "Usage: tee [-a] [file ...]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		err(sys->sprint("can't load %s: %r", Arg->PATH));
+
+	append := 0;
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		'a' =>	append = 1;
+		* =>		usage();
+		}
+	names := arg->argv();
+	arg = nil;
+
+	fd0 := sys->fildes(0);
+	if(fd0 == nil)
+		err("no standard input");
+	nf := 0;
+	files := array[len names + 1] of ref File;
+	for(; names != nil; names = tl names){
+		f := hd names;
+		fd: ref Sys->FD;
+		if(append){
+			fd = sys->open(f, Sys->OWRITE);
+			if(fd != nil)
+				sys->seek(fd, big 0, 2);
+			else
+				fd = sys->create(f, Sys->OWRITE, 8r666);
+		}else
+			fd = sys->create(f, Sys->OWRITE, 8r666 );
+		if(fd == nil)
+			err(sys->sprint("cannot open %s: %r", f));
+		files[nf++] = ref File(fd, f);
+	}
+	files[nf++] = ref File(sys->fildes(1), "standard output");
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd0, buf, len buf)) > 0){
+		for(i := 0; i < nf; i++)
+			if(sys->write(files[i].fd, buf, n) != n)
+				err(sys->sprint("error writing %s: %r", files[i].name));
+	}
+	if(n < 0)
+		err(sys->sprint("read error: %r"));
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "tee: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/telnet.b
@@ -1,0 +1,469 @@
+implement Telnet;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "dial.m";
+	dial: Dial;
+	Connection: import dial;
+
+Telnet: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+Debug: con 0;
+
+Inbuf: adt {
+	fd:	ref Sys->FD;
+	out:	ref Outbuf;
+	buf:	array of byte;
+	ptr:	int;
+	nbyte:	int;
+};
+
+Outbuf: adt {
+	buf:	array of byte;
+	ptr:	int;
+};
+
+BS:		con 8;		# ^h backspace character
+BSW:		con 23;		# ^w bacspace word
+BSL:		con 21;		# ^u backspace line
+EOT:		con 4;		# ^d end of file
+ESC:		con 27;		# hold mode
+
+net:	ref Connection;
+stdin, stdout, stderr: ref Sys->FD;
+
+# control characters
+Se:			con 240;	# end subnegotiation
+NOP:			con 241;
+Mark:		con 242;	# data mark
+Break:		con 243;
+Interrupt:		con 244;
+Abort:		con 245;	# TENEX ^O
+AreYouThere:	con 246;
+Erasechar:	con 247;	# erase last character
+Eraseline:		con 248;	# erase line
+GoAhead:		con 249;	# half duplex clear to send
+Sb:			con 250;	# start subnegotiation
+Will:			con 251;
+Wont:		con 252;
+Do:			con 253;
+Dont:		con 254;
+Iac:			con 255;
+
+# options
+Binary, Echo, SGA, Stat, Timing,
+Det, Term, EOR, Uid, Outmark,
+Ttyloc, M3270, Padx3, Window, Speed,
+Flow, Line, Xloc, Extend: con iota;
+
+Opt: adt
+{
+	name:	string;
+	code:	int;
+	noway:	int;	
+	remote:	int;		# remote value
+	local:	int;		# local value
+};
+
+opt := array[] of
+{
+	Binary =>		Opt("binary",			0,	0,	0, 	0),
+	Echo	=>		Opt("echo",			1,  	0, 	0,	0),
+	SGA	=>		Opt("suppress go ahead",	3,  	0, 	0,	0),
+	Stat =>		Opt("status",			5,  	1, 	0,	0),
+	Timing =>		Opt("timing",			6,  	1, 	0,	0),
+	Det=>		Opt("det",				20, 	1, 	0,	0),
+	Term =>		Opt("terminal",			24, 	0, 	0,	0),
+	EOR =>		Opt("end of record",		25, 	1, 	0,	0),
+	Uid =>		Opt("uid",				26, 	1, 	0,	0),
+	Outmark => 	Opt("outmark",			27, 	1, 	0,	0),
+	Ttyloc =>		Opt("ttyloc",			28, 	1, 	0,	0),
+	M3270 =>		Opt("3270 mode",		29, 	1, 	0,	0),
+	Padx3 =>		Opt("pad x.3",			30, 	1, 	0,	0),
+	Window =>	Opt("window size",		31, 	1, 	0,	0),
+	Speed =>		Opt("speed",			32, 	1, 	0,	0),
+	Flow	=>		Opt("flow control",		33, 	1, 	0,	0),
+	Line	=>		Opt("line mode",		34, 	1, 	0,	0),
+	Xloc	=>		Opt("X display loc",		35, 	1, 	0,	0),
+	Extend =>		Opt("Extended",		255,	1, 	0,	0),
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: telnet host [port]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	stdout = sys->fildes(1);
+	stdin = sys->fildes(0);
+	dial = load Dial Dial->PATH;
+
+	if (len argv < 2)
+		usage();
+	argv = tl argv;
+	host := hd argv;
+	argv = tl argv;
+	port := "23";
+	if(argv != nil)
+		port = hd argv;
+	connect(host, port);
+}
+
+ccfd: ref Sys->FD;
+connect(addr: string, port: string)
+{
+	dest := dial->netmkaddr(addr, "tcp", port);
+	net = dial->dial(dest, nil);
+	if(net == nil) {
+		sys->fprint(stderr, "telnet: can't dial %s: %r\n", dest);
+		raise "fail:dial";
+	}
+	sys->fprint(stderr, "telnet: connected to %s\n", addr);
+
+	raw(1);
+	pidch := chan of int;
+	finished := chan of int;
+	spawn fromnet(pidch, finished);
+	spawn fromuser(pidch, finished);
+	pids := array[2] of {* => <-pidch};
+	kill(pids[<-finished == pids[0]]);
+	raw(0);
+}
+
+
+fromuser(pidch, finished: chan of int)
+{
+	pidch <-= sys->pctl(0, nil);
+	b := array[1024] of byte;
+	while((n := sys->read(stdin, b, len b)) > 0) {
+		if (opt[Echo].remote == 0)
+			sys->write(stdout, b, n);
+		sys->write(net.dfd, b, n);
+	}
+	sys->fprint(stderr, "telnet: error reading stdin: %r\n");
+	finished <-= sys->pctl(0, nil);
+}
+
+getc(b: ref Inbuf): int
+{
+	if(b.nbyte == 0) {
+		if(b.out != nil)
+			flushout(b.out);
+		b.nbyte = sys->read(b.fd, b.buf, len b.buf);
+		if(b.nbyte <= 0)
+			return -1;
+		b.ptr = 0;
+	}
+	b.nbyte--;
+	return int b.buf[b.ptr++];
+}
+
+putc(b: ref Outbuf, c: int)
+{
+	b.buf[b.ptr++] = byte c;
+	if(b.ptr == len b.buf)
+		flushout(b);
+}
+
+flushout(b: ref Outbuf)
+{
+	sys->write(stdout, b.buf, b.ptr);
+	b.ptr = 0;
+}
+
+BUFSIZE: con 2048;
+fromnet(pidch, finished: chan of int)
+{
+	pidch <-= sys->pctl(0, nil);
+	conout := ref Outbuf(array[BUFSIZE] of byte, 0);
+	netinp := ref Inbuf(net.dfd, conout, array[BUFSIZE] of byte, 0, 0);
+
+loop:	for(;;) {
+		c := getc(netinp);	
+		case c {
+		-1 =>
+			break loop;
+		Iac  =>
+			c = getc(netinp);
+			if(c != Iac) {
+				flushout(conout);
+				if(control(netinp, c) < 0)
+					break loop;
+			} else
+				putc(conout, c);
+		* =>
+			putc(conout, c);
+		}
+	}
+	sys->fprint(stderr, "telnet: remote host closed connection\n");
+	finished <-= sys->pctl(0, nil);
+}
+
+control(bp: ref Inbuf, c: int): int
+{
+	r := 0;
+	case c {
+	AreYouThere =>
+		sys->fprint(net.dfd, "Inferno telnet\r\n");
+	Sb =>
+		r = sub(bp);
+	Will =>
+		r = will(bp);
+	Wont =>
+		r = wont(bp);
+	Do =>
+		r = doit(bp);
+	Dont =>
+		r = dont(bp);
+	Se =>
+		sys->fprint(stderr, "telnet: SE without an SB\n");
+	-1 =>
+		r = -1;
+	}
+
+	return r;
+}
+
+sub(bp: ref Inbuf): int
+{
+	subneg: string;
+	i := 0;
+	for(;;){
+		c := getc(bp);
+		if(c == Iac) {
+			c = getc(bp);
+			if(c == Se)
+				break;
+			subneg[i++] = Iac;
+		}
+		if(c < 0)
+			return -1;
+		subneg[i++] = c;
+	}
+	if(i == 0)
+		return 0;
+
+	if (Debug)
+		sys->fprint(stderr, "telnet: sub(%s, %d, n = %d)\n", optname(subneg[0]), subneg[1], i);
+
+	for(i = 0; i < len opt; i++)
+		if(opt[i].code == subneg[0])
+			break;
+
+	if(i >= len opt)
+		return 0;
+
+	case i {
+	Term =>
+		sbsend(opt[Term].code, array of byte "network");	
+	}
+
+	return 0;
+}
+
+sbsend(code: int, data: array of byte): int
+{
+	buf := array[4+len data+2] of byte;
+	o := 4+len data;
+
+	buf[0] = byte Iac;
+	buf[1] = byte Sb;
+	buf[2] = byte code;
+	buf[3] = byte 0;
+	buf[4:] = data;
+	buf[o] = byte Iac;
+	o++;
+	buf[o] = byte Se;
+
+	return sys->write(net.dfd, buf, len buf);
+}
+
+will(bp: ref Inbuf): int
+{
+	c := getc(bp);
+	if(c < 0)
+		return -1;
+
+	if (Debug)
+		sys->fprint(stderr, "telnet: will(%s)\n", optname(c));
+
+	for(i := 0; i < len opt; i++)
+		if(opt[i].code == c)
+			break;
+
+	if(i >= len opt) {
+		send3(bp, Iac, Dont, c);
+		return 0;
+	}
+
+	rv := 0;
+	if(opt[i].noway)
+		send3(bp, Iac, Dont, c);
+	else
+	if(opt[i].remote == 0)
+		rv |= send3(bp, Iac, Do, c);
+
+	if(opt[i].remote == 0)
+		rv |= change(bp, i, Will);
+	opt[i].remote = 1;
+	return rv;
+}
+
+wont(bp: ref Inbuf): int
+{
+	c := getc(bp);
+	if(c < 0)
+		return -1;
+
+	if (Debug)
+		sys->fprint(stderr, "telnet: wont(%s)\n", optname(c));
+
+	for(i := 0; i < len opt; i++)
+		if(opt[i].code == c)
+			break;
+
+	if(i >= len opt)
+		return 0;
+
+	rv := 0;
+	if(opt[i].remote) {
+		rv |= change(bp, i, Wont);
+		rv |= send3(bp, Iac, Dont, c);
+	}
+	opt[i].remote = 0;
+	return rv;
+}
+
+doit(bp: ref Inbuf): int
+{
+	c := getc(bp);
+	if(c < 0)
+		return -1;
+
+	if (Debug)
+		sys->fprint(stderr, "telnet: do(%s)\n", optname(c));
+
+	for(i := 0; i < len opt; i++)
+		if(opt[i].code == c)
+			break;
+
+	if(i >= len opt || opt[i].noway) {
+		send3(bp, Iac, Wont, c);
+		return 0;
+	}
+	rv := 0;
+	if(opt[i].local == 0) {
+		rv |= change(bp, i, Do);
+		rv |= send3(bp, Iac, Will, c);
+	}
+	opt[i].local = 1;
+	return rv;
+}
+
+dont(bp: ref Inbuf): int
+{
+	c := getc(bp);
+	if(c < 0)
+		return -1;
+
+	if (Debug)
+		sys->fprint(stderr, "telnet: dont(%s)\n", optname(c));
+
+	for(i := 0; i < len opt; i++)
+		if(opt[i].code == c)
+			break;
+
+	if(i >= len opt || opt[i].noway)
+		return 0;
+
+	rv := 0;
+	if(opt[i].local){
+		opt[i].local = 0;
+		rv |= change(bp, i, Dont);
+		rv |= send3(bp, Iac, Wont, c);
+	}
+	opt[i].local = 0;
+	return rv;
+}
+
+change(bp: ref Inbuf, o: int, what: int): int
+{
+	if(bp != nil)
+		{}
+	if(o != 0)
+		{}
+	if(what != 0)
+		{}
+	return 0;
+}
+
+send3(bp: ref Inbuf, c0: int, c1: int, c2: int): int
+{
+	if (Debug)
+		sys->fprint(stderr, "telnet: reply(%s(%s))\n", negname(c1), optname(c2));
+		
+	buf := array[3] of byte;
+
+	buf[0] = byte c0;
+	buf[1] = byte c1;
+	buf[2] = byte c2;
+
+	if (sys->write(bp.fd, buf, 3) != 3)
+		return -1;
+	return 0;
+}
+
+kill(pid: int): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if (fd == nil)
+		return -1;
+	if (sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+negname(c: int): string
+{
+	t := "Unknown";
+	case c {
+	Will =>	t = "will";
+	Wont =>	t = "wont";
+	Do =>	t = "do";
+	Dont =>	t = "dont";
+	}
+	return t;
+}
+
+optname(c: int): string
+{
+	for (i := 0; i < len opt; i++)
+		if (opt[i].code == c)
+			return opt[i].name;
+	return "unknown";
+}
+
+raw(on: int)
+{
+	if(ccfd == nil) {
+		ccfd = sys->open("/dev/consctl", Sys->OWRITE);
+		if(ccfd == nil) {
+			sys->fprint(stderr, "telnet: cannot open /dev/consctl: %r\n");
+			return;
+		}
+	}
+	if(on)
+		sys->fprint(ccfd, "rawon");
+	else
+		sys->fprint(ccfd, "rawoff");
+}
--- /dev/null
+++ b/appl/cmd/test.b
@@ -1,0 +1,284 @@
+implement Test;
+
+#
+#	venerable
+#		test expression
+#
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+
+include "draw.m";
+
+include "daytime.m";
+	daytime: Daytime;
+
+Test: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+gargs: list of string;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	if(args == nil)
+		return;
+	gargs = tl args;
+
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	if(gargs == nil)
+		raise "fail:usage";
+	if(!e())
+		raise "fail:false";
+}
+
+nextarg(mt: int): string
+{
+	if(gargs == nil){
+		if(mt)
+			return nil;
+		synbad("argument expected");
+	}
+	s := hd gargs;
+	gargs = tl gargs;
+	return s;
+}
+
+nextintarg(): (int, int)
+{
+	if(gargs != nil && isint(hd gargs))
+		return (1, int nextarg(0));
+	return (0, 0);
+}
+
+isnextarg(s: string): int
+{
+	if(gargs != nil && hd gargs == s){
+		gargs = tl gargs;
+		return 1;
+	}
+	return 0;
+}
+
+e(): int
+{
+	p1 := e1();
+	if(isnextarg("-o"))
+		return p1 || e();
+	return p1;
+}
+
+e1(): int
+{
+	p1 := e2();
+	if(isnextarg("-a"))
+		return p1 && e1();
+	return p1;
+}
+
+e2(): int
+{
+	if(isnextarg("!"))
+		return !e2();
+	return e3();
+}
+
+e3(): int
+{
+	a := nextarg(0);
+	case a {
+	"(" =>
+		p1 := e();
+		if(nextarg(0) != ")")
+			synbad(") expected");
+		return p1;
+	"-A" =>
+		return hasmode(nextarg(0), Sys->DMAPPEND);
+	"-L" =>
+		return hasmode(nextarg(0), Sys->DMEXCL);
+	"-T" =>
+		return hasmode(nextarg(0), Sys->DMTMP);
+	"-f" =>
+		f := nextarg(0);
+		return exists(f) && !hasmode(f, Sys->DMDIR);
+	"-d" =>
+		return hasmode(nextarg(0), Sys->DMDIR);
+	"-r" =>
+		return sys->open(nextarg(0), Sys->OREAD) != nil;
+	"-w" =>
+		return sys->open(nextarg(0), Sys->OWRITE) != nil;
+	"-x" =>
+		fd := sys->open(nextarg(0), Sys->OREAD);
+		if(fd == nil)
+			return 0;
+		(ok, d) := sys->fstat(fd);
+		if(ok < 0)
+			return 0;
+		return (d.mode & 8r111) != 0;
+	"-e" =>
+		return exists(nextarg(0));
+	"-s" =>
+		(ok, d) := sys->stat(nextarg(0));
+		if(ok < 0)
+			return 0;
+		return d.length > big 0;
+	"-t" =>
+		(ok, fd) := nextintarg();
+		if(!ok)
+			return iscons(1);
+		return iscons(fd);
+	"-n" =>
+		return nextarg(0) != "";
+	"-z" =>
+		return nextarg(0) == "";
+	* =>
+		p2 := nextarg(1);
+		if(p2 == nil)
+			return a != nil;
+		case p2 {
+		"=" =>
+			return nextarg(0) == a;
+		"!=" =>
+			return nextarg(0) != a;
+		"-older" =>
+			return isolder(nextarg(0), a);
+		"-ot" =>
+			return isolderthan(a, nextarg(0));
+		"-nt" =>
+			return isnewerthan(a, nextarg(0));
+		}
+
+		if(!isint(a))
+			return a != nil;
+
+		int1 := int a;
+		(ok, int2) := nextintarg();
+		if(ok){
+			case p2 {
+			"-eq" =>
+				return int1 == int2;
+			"-ne" =>
+				return int1 != int2;
+			"-gt" =>
+				return int1 > int2;
+			"-lt" =>
+				return int1 < int2;
+			"-ge" =>
+				return int1 >= int2;
+			"-le" =>
+				return int1 <= int2;
+			}
+		}
+
+		synbad("unknown operator " + p2);
+		return 0;
+	}
+}
+
+synbad(s: string)
+{
+	sys->fprint(stderr, "test: bad syntax: %s\n", s);
+	raise "fail:bad syntax";
+}
+
+isint(s: string): int
+{
+	if(s == nil)
+		return 0;
+	for(i := 0; i < len s; i++)
+		if(s[i] < '0' || s[i] > '9')
+			return 0;
+	return 1;
+}
+
+exists(f: string): int
+{
+	return sys->stat(f).t0 >= 0;
+}
+
+hasmode(f: string, m: int): int
+{
+	(ok, d) := sys->stat(f);
+	if(ok < 0)
+		return 0;
+	return (d.mode & m) != 0;
+}
+
+iscons(fno: int): int
+{
+	fd := sys->fildes(fno);
+	if(fd == nil)
+		return 0;
+	s := sys->fd2path(fd);
+	n := len "/dev/cons";
+	return s == "#c/cons" || len s >= n && s[len s-n:] == "/dev/cons";
+}
+
+isolder(t: string, f: string): int
+{
+	(ok, dir) := sys->stat(f);
+	if(ok < 0)
+		return 0;
+
+	n := 0;
+	for(i := 0; i < len t;){
+		for(j := i; j < len t; j++)
+			if(!(t[j] >= '0' && t[j] <= '9'))
+				break;
+		if(i == j)
+			synbad("bad time syntax, "+t);
+		m := int t[i:j];
+		i = j;
+		if(i == len t){
+			n = m;
+			break;
+		}
+		case t[i++] {
+		'y' =>	n += m*12*30*24*3600;
+		'M' =>	n += m*30*24*3600;
+		'd' =>	n += m*24*3600;
+		'h' =>	n += m*3600;
+		'm' =>	n += m*60;
+		's' =>		n += m;
+		* =>		synbad("bad time syntax, "+t);
+		}
+	}
+
+	return dir.mtime+n < now();
+}
+
+isolderthan(a: string, b: string): int
+{
+	(aok, ad) := sys->stat(a);
+	if(aok < 0)
+		return 0;
+	(bok, bd) := sys->stat(b);
+	if(bok < 0)
+		return 0;
+	return ad.mtime < bd.mtime;
+}
+
+isnewerthan(a: string, b: string): int
+{
+	(aok, ad) := sys->stat(a);
+	if(aok < 0)
+		return 0;
+	(bok, bd) := sys->stat(b);
+	if(bok < 0)
+		return 0;
+	return ad.mtime > bd.mtime;
+}
+
+now(): int
+{
+	if(daytime == nil){
+		daytime = load Daytime Daytime->PATH;
+		if(daytime == nil)
+			synbad(sys->sprint("can't load %s: %r", Daytime->PATH));
+	}
+	return daytime->now();
+}
--- /dev/null
+++ b/appl/cmd/time.b
@@ -1,0 +1,97 @@
+implement Time;
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+
+FD: import Sys;
+Context: import Draw;
+
+Time: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+sys: Sys;
+stderr, waitfd: ref FD;
+
+init(ctxt: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	stderr = sys->fildes(2);
+
+	waitfd = sys->open("#p/"+string sys->pctl(0, nil)+"/wait", sys->OREAD);
+	if(waitfd == nil){
+		sys->fprint(stderr, "time: open wait: %r\n");
+		return;
+	}
+
+	argv = tl argv;
+
+	if(argv == nil) {
+		sys->fprint(stderr, "usage: time cmd ...\n");
+		return;
+	}
+
+	file := hd argv;
+
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	t0 := sys->millisec();
+
+	c := load Command file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(1){
+			c = load Command "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil) {
+			sys->fprint(stderr, "time: %s: %s\n", hd argv, err);
+			return;
+		}
+	}
+
+	t1 := sys->millisec();
+
+	pidc := chan of int;
+
+	spawn cmd(ctxt, c, pidc, argv);
+	waitfor(<-pidc);
+
+	t2 := sys->millisec();
+
+	f1 := real (t1 - t0) /1000.;
+	f2 := real (t2 - t1) /1000.;
+	sys->fprint(stderr, "%.4gl %.4gr %.4gt\n", f1, f2, f1+f2);
+}
+
+cmd(ctxt: ref Context, c: Command, pidc: chan of int, argv: list of string)
+{
+	pidc <-= sys->pctl(0, nil);
+	c->init(ctxt, argv);
+}
+
+waitfor(pid: int)
+{
+	buf := array[sys->WAITLEN] of byte;
+	status := "";
+	for(;;){
+		n := sys->read(waitfd, buf, len buf);
+		if(n < 0) {
+			sys->fprint(stderr, "sh: read wait: %r\n");
+			return;
+		}
+		status = string buf[0:n];
+		if(status[len status-1] != ':')
+			sys->fprint(stderr, "%s\n", status);
+		who := int status;
+		if(who != 0) {
+			if(who == pid)
+				return;
+		}
+	}
+}
--- /dev/null
+++ b/appl/cmd/timestamp.b
@@ -1,0 +1,42 @@
+implement Timestamp;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+Timestamp: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+timefd: ref Sys->FD;
+starttime: big;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	note: string;
+	if(len argv > 1)
+		note = hd tl argv + " ";
+
+	timefd = sys->open("/dev/time", Sys->OREAD);
+	starttime = now();
+
+	sys->print("%.10bd %sstart %bd\n", now(), note, starttime);
+
+	iob := bufio->fopen(sys->fildes(0), Sys->OREAD);
+	while((s := iob.gets('\n')) != nil)
+		sys->print("%.10bd %s%s", now(), note, s);
+}
+
+now(): big
+{
+	buf := array[24] of byte;
+	n := sys->pread(timefd, buf, len buf, big 0);
+	if(n <= 0)
+		return big 0;
+	return big string buf[0:n] / big 1000 - starttime;
+}
--- /dev/null
+++ b/appl/cmd/tkcmd.b
@@ -1,0 +1,190 @@
+implement Tkcmd;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+	draw: Draw;
+	Display, Image, Point: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "bufio.m";
+include "arg.m";
+
+Tkcmd : module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->print("usage: tkcmd [-iu] [toplevelarg]\n");
+	raise "fail:usage";
+}
+
+badmodule(m: string)
+{
+		sys->fprint(stderr, "tkcmd: cannot load %s: %r\n", m);
+		raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys  = load Sys  Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk   Tk->PATH;
+	if (tk == nil)
+		badmodule(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient==nil)
+		badmodule(Tkclient->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+
+	arg->init(argv);
+	update := 1;
+	interactive := isconsole(sys->fildes(0));
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'i' =>
+			interactive = 1;
+		'u' =>
+			update = 0;
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	arg = nil;
+	tkarg := "";
+	if (argv != nil) {
+		if (tl argv != nil)
+			usage();
+		tkarg = hd argv;
+	}
+	
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+	shellit(ctxt, tkarg, interactive, update);
+}
+
+isconsole(fd: ref Sys->FD): int
+{
+	(ok1, d1) := sys->fstat(fd);
+	(ok2, d2) := sys->stat("/dev/cons");
+	if (ok1 < 0 || ok2 < 0)
+		return 0;
+	return d1.dtype == d2.dtype && d1.qid.path == d2.qid.path;
+}
+
+shellit(ctxt: ref Draw->Context, arg: string, interactive, update: int)
+{
+	(Wwsh, winctl) := tkclient->toplevel(ctxt, arg, "Tk", Tkclient->Appl);
+	tkclient->onscreen(Wwsh, nil);
+	tkclient->startinput(Wwsh, "ptr" :: "kbd" :: nil);
+	wm := Wwsh.ctxt;
+	if(update)
+		tk->cmd(Wwsh, "update");
+	ps1 := "";
+	ps2 := "";
+	if (!interactive)
+		ps1 = ps2 = "";
+
+	lines := chan of string;
+	sync := chan of int;
+	spawn grab_lines(ps1, ps2, lines, sync);
+	output := chan of string;
+	tk->namechan(Wwsh, output, "stdout");
+	pid := <-sync;
+Loop:
+	for(;;) alt {
+	c := <-wm.kbd =>
+		tk->keyboard(Wwsh, c);
+	m := <-wm.ptr =>
+		tk->pointer(Wwsh, *m);
+	c := <-wm.ctl or
+	c = <-Wwsh.wreq =>
+		tkclient->wmctl(Wwsh, c);
+	line := <-lines =>
+		if (line == nil)
+			break Loop;
+		if (line[0] == '#')
+			break;
+		line = line[0:len line - 1];
+		result := tk->cmd(Wwsh, line);
+		if (result != nil)
+			sys->print("#%s\n", result);
+		if (update)
+			tk->cmd(Wwsh, "update");
+		sys->print("%s", ps1);
+	menu := <-winctl =>
+		tkclient->wmctl(Wwsh, menu);
+	s := <-output =>
+		sys->print("#<stdout>%s\n", s);
+		sys->print("%s", ps1);
+	}
+}
+
+grab_lines(new_inp, unfin: string, lines: chan of string, sync: chan of int)
+{
+	sync <-= sys->pctl(0, nil);
+	{
+		bufmod := load Bufio Bufio->PATH;
+		Iobuf:	import bufmod;
+		if (bufmod == nil) {
+			lines <-= nil;
+			return;
+		}
+		sys->print("%s", new_inp);
+		iob := bufmod->fopen(sys->fildes(0),bufmod->OREAD);
+		if (iob==nil){
+			sys->fprint(stderr, "tkcmd: cannot open stdin for reading.\n");
+			lines <-= nil;
+			return;
+		}
+		line := "";
+		while((input := iob.gets('\n')) != nil) {
+			line+=input;
+			if (!finished(line,0))
+				sys->print("%s", unfin);
+			else{
+				lines <-= line;
+				line=nil;
+			}
+		}
+		lines <-= nil;
+	}exception e{
+	"*" =>
+		sys->fprint(stderr, "tkcmd: fail: %s\n", e);
+		lines <-= nil;
+	}
+}
+
+# returns 1 if the line has matching braces, brackets and 
+# double-quotes and does not end in "\\\n"
+finished(s : string, termchar : int) : int {
+	cb:=0;
+	dq:=0;
+	sb:=0;
+	if (s==nil) return 1;
+	if (termchar=='}') cb++;
+	if (termchar==']') sb++;
+	if (len s > 1 && s[len s -2]=='\\')
+		return 0;
+	if (s[0]=='{') cb++;
+	if (s[0]=='}' && cb>0) cb--;
+	if (s[0]=='[') sb++;
+	if (s[0]==']' && sb>0) sb--;
+	if (s[0]=='"') dq=1-dq;
+	for(i:=1;i<len s;i++){
+		if (s[i]=='{' && s[i-1]!='\\') cb++;
+		if (s[i]=='}' && s[i-1]!='\\' && cb>0) cb--;
+		if (s[i]=='[' && s[i-1]!='\\') sb++;
+		if (s[i]==']' && s[i-1]!='\\' && sb>0) sb--;
+		if (s[i]=='"' && s[i-1]!='\\') dq=1-dq;
+	}
+	return (cb==0 && sb==0 && dq==0);
+}
--- /dev/null
+++ b/appl/cmd/tokenize.b
@@ -1,0 +1,33 @@
+implement Tokenize;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+Tokenize: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+stderr: ref Sys->FD;
+
+usage()
+{
+	  sys->fprint(stderr, "Usage: tokenize string delimiters\n");
+	  raise "fail: usage";
+}
+
+init(nil: ref Draw->Context, args : list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	if(args != nil)
+		args = tl args;
+	if(len args != 2)
+		usage();
+	(nil, l) := sys->tokenize(hd args, hd tl args);
+	for(; l != nil; l = tl l)
+		sys->print("%s\n", hd l);
+}
--- /dev/null
+++ b/appl/cmd/touch.b
@@ -1,0 +1,77 @@
+implement Touch;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "arg.m";
+
+stderr: ref Sys->FD;
+
+Touch: module
+{
+	init: fn(ctxt: ref Draw->Context, argl: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	force := 1;
+	status := 0;
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		cantload(Daytime->PATH);
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		cantload(Arg->PATH);
+	arg->init(args);
+	arg->setusage("touch [-c] [-t time] file ...");
+	now := daytime->now();
+	while((c := arg->opt()) != 0)
+		case c {
+		't' =>		now = int arg->earg();
+		'c' =>	force = 0;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+	for(; args != nil; args = tl args)
+		status += touch(force, hd args, now);
+	if(status)
+		raise "fail:touch";
+}
+
+cantload(s: string)
+{
+	sys->fprint(stderr, "touch: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+touch(force: int, name: string, now: int): int
+{
+	dir := sys->nulldir;
+	dir.mtime = now;
+	(rc, nil) := sys->stat(name);
+	if(rc >= 0){
+		if(sys->wstat(name, dir) >= 0)
+			return 0;
+		force = 0;	# we don't want to create it: it's there, we just can't wstat it
+	}
+	if(force == 0) {
+		sys->fprint(stderr, "touch: %s: cannot change time: %r\n", name);
+		return 1;
+	}
+	if((fd := sys->create(name, Sys->OREAD|Sys->OEXCL, 8r666)) == nil) {
+		sys->fprint(stderr, "touch: %s: cannot create: %r\n", name);
+		return 1;
+	}
+	sys->fwstat(fd, dir);
+	return 0;
+}
--- /dev/null
+++ b/appl/cmd/touchcal.b
@@ -1,0 +1,278 @@
+implement Touchcal;
+
+#
+# calibrate a touch screen
+#
+# Copyright © 2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys:	Sys;
+
+include "draw.m";
+	draw:	Draw;
+	Display, Font, Image, Point, Pointer, Rect: import draw;
+
+include "tk.m";
+
+include "wmclient.m";
+	wmclient: Wmclient;
+	Window: import wmclient;
+
+include "translate.m";
+	translate: Translate;
+	Dict: import translate;
+
+Touchcal: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+
+Margin: con 20;
+
+prompt:= "Please tap the centre\nof the cross\nwith the stylus";
+
+mousepid := 0;
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	r: Rect;
+	disp: ref Image;
+
+	if(args != nil)
+		args = tl args;
+	debug := args != nil && hd args == "-d";
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	if(draw == nil)
+		err(sys->sprint("no Draw module: %r"));
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD, nil);
+	translate = load Translate Translate->PATH;
+	if(translate != nil){
+		translate->init();
+		(dict, nil) := translate->opendict(translate->mkdictname("", "touchcal"));
+		if(dict != nil)
+			prompt = dict.xlate(prompt);
+		dict = nil;
+		translate = nil;
+	}
+
+	display: ref Display;
+	win: ref Window;
+	ptr: chan of ref Pointer;
+	if(ctxt != nil){
+		display = ctxt.display;
+		wmclient = load Wmclient Wmclient->PATH;
+		if(wmclient == nil)
+			err(sys->sprint("cannot load %s: %r", Wmclient->PATH));
+		wmclient->init();
+		win = wmclient->window(ctxt, "Touchcal", Wmclient->Plain);
+		win.reshape(ctxt.display.image.r);
+		ptr = chan of ref Pointer;
+		win.onscreen("exact");
+		win.startinput("ptr"::nil);
+		pidc := chan of int;
+		ptr = win.ctxt.ptr;
+		display = ctxt.display;
+		disp = win.image;
+		r = disp.r;
+	}else{
+		# standalone, catch them ourselves
+		display = draw->Display.allocate(nil);
+		disp = display.image;
+		r = disp.r;
+		mfd := sys->open("/dev/pointer", Sys->OREAD);
+		if(mfd == nil)
+			err(sys->sprint("can't open /dev/pointer: %r"));
+		pidc := chan of int;
+		ptr = chan of ref Pointer;
+		spawn rawmouse(mfd, ptr, pidc);
+		mousepid = <-pidc;
+	}
+	white := display.white;
+	black := display.black;
+	red := display.color(Draw->Red);
+	disp.draw(r, white, nil, r.min);
+	samples := array[4] of Point;
+	points := array[4] of Point;
+	points[0] = (r.min.x+Margin, r.min.y+Margin);
+	points[1] = (r.max.x-Margin, r.min.y+Margin);
+	points[2] = (r.max.x-Margin, r.max.y-Margin);
+	points[3] = (r.min.x+Margin, r.max.y-Margin);
+	midpoint := Point((r.min.x+r.max.x)/2, (r.min.y+r.max.y)/2);
+	refx := FX((points[1].x - points[0].x) + (points[2].x - points[3].x), 1);
+	refy := FX((points[3].y - points[0].y) + (points[2].y - points[1].y), 1);
+	ctl := sys->open("/dev/touchctl", Sys->ORDWR);
+	if(ctl == nil)
+		ctl = sys->open("/dev/null", Sys->ORDWR);
+	if(ctl == nil)
+		err(sys->sprint("can't open /dev/touchctl: %r"));
+	#oldvalues := array[128] of byte;
+	#nr := sys->read(ctl, oldvalues, len oldvalues);
+	#if(nr < 0)
+	#	err(sys->sprint("can't read old values from /dev/touchctl: %r"));
+	#oldvalues = oldvalues[0:nr];
+	sys->fprint(ctl, "X %d %d %d\nY %d %d %d\n", FX(1,1), 0, 0, 0, FX(1,1), 0);	# identity
+	font := Font.open(display, sys->sprint("/fonts/lucida/unicode.%d.font", 6+(r.dx()/512)));
+	if(font == nil)
+		font = Font.open(display, "*default*");
+	if(font != nil){
+		drawtext(disp, midpoint, black, font, prompt);
+		font = nil;
+	}
+	for(;;) {
+		tm := array[] of {0 to 2 =>array[] of {0, 0, 0}};
+		for(i := 0; i < 4; i++){
+			cross(disp, points[i], red);
+			samples[i] = getpoint(ptr);
+			cross(disp, points[i], white);
+		}
+		# first, rotate if necessary
+		rotate := 0;
+		if(abs(samples[1].x-samples[2].x) > 80 && abs(samples[2].y-samples[3].y) > 80){
+			rotate = 1;
+			for(i = 0; i < len samples; i++)
+				samples[i] = (samples[i].y, samples[i].x);
+		}
+		# calculate scaling and offset transformations
+		actx := (samples[1].x-samples[0].x)+(samples[2].x-samples[3].x);
+		acty := (samples[3].y-samples[0].y)+(samples[2].y-samples[1].y);
+		if(actx == 0 || acty == 0)
+			continue;		# either the user or device is not trying
+		tm[0][rotate] = refx/actx;
+		tm[0][2] = FX(points[0].x - XF(tm[0][rotate]*samples[0].x), 1);
+		tm[1][1-rotate] = refy/acty;
+		tm[1][2] = FX(points[0].y - XF(tm[1][1-rotate]*samples[0].y), 1);
+		cross(disp, midpoint, red);
+		m := getpoint(ptr);
+		cross(disp, midpoint, white);
+		p := Point(ptmap(tm[0], m.x, m.y), ptmap(tm[1], m.x, m.y));
+		if(debug){
+			for(k:=0; k<4; k++)
+				sys->print("%d %d,%d %d,%d\n", k, points[k].x,points[k].y, samples[k].x, samples[k].y);
+			if(rotate)
+				sys->print("rotated\n");
+			sys->print("rx=%d ax=%d ry=%d ay=%d tm[0][0]=%d\n", refx, actx, refy, acty, tm[0][0]);
+			sys->print("%g %g %g\n%g %g %g\n",
+				G(tm[0][0]), G(tm[0][1]), G(tm[0][2]),
+				G(tm[1][0]), G(tm[1][1]), G(tm[1][2]));
+			sys->print("%d %d -> %d %d (%d %d)\n", m.x, m.y, p.x, p.y, midpoint.x, midpoint.y);
+		}
+		if(abs(p.x-midpoint.x) > 5 || abs(p.y-midpoint.y) > 5)
+			continue;
+		printmat(sys->fildes(1), tm);
+		if(debug || printmat(ctl, tm) >= 0){
+			disp.draw(r, white, nil, r.min);
+			break;
+		}
+		sys->fprint(sys->fildes(2), "touchcal: can't set calibration: %r\n");
+	}
+	if(mousepid > 0)
+		kill(mousepid);
+}
+
+printmat(fd: ref Sys->FD, tm: array of array of int): int
+{
+	return sys->fprint(fd, "X %d %d %d\nY %d %d %d\n",
+		    tm[0][0], tm[0][1], tm[0][2],
+		    tm[1][0], tm[1][1], tm[1][2]);
+}
+
+FX(a, b: int): int
+{
+	return (a << 16)/b;
+}
+
+XF(v: int): int
+{
+	return v>>16;
+}
+
+G(v: int): real
+{
+	return real v / 65536.0;
+}
+
+ptmap(m: array of int, x, y: int): int
+{
+	return XF(m[0]*x + m[1]*y + m[2]);
+}
+
+rawmouse(fd: ref Sys->FD, mc: chan of ref Pointer, pidc: chan of int)
+{
+	pidc <-= sys->pctl(0, nil);
+	buf := array[64] of byte;
+	for(;;){
+		n := sys->read(fd, buf, len buf);
+		if(n <= 0)
+			err(sys->sprint("can't read /dev/pointer: %r"));
+
+		if(int buf[0] != 'm' || n < 1+3*12)
+			continue;
+
+		x := int string buf[ 1:13];
+		y := int string buf[12:25];
+		b := int string buf[24:37];
+		mc <-= ref Pointer(b, (x,y), 0);
+	}
+}
+
+getpoint(mousec: chan of ref Pointer): Point
+{
+	p := Point(0,0);
+	while((m := <-mousec).buttons == 0)
+		p = m.xy;
+	n := 0;
+	do{
+		if(abs(p.x-m.xy.x) > 10 || abs(p.y-m.xy.y) > 10){
+			n = 0;
+			p = m.xy;
+		}else{
+			p = p.mul(n).add(m.xy).div(n+1);
+			n++;
+		}
+	}while((m = <-mousec).buttons & 7);
+	return p;
+}
+
+cross(im: ref Image, p: Point, col: ref Image)
+{
+	im.line(p.sub((0,10)), p.add((0,10)), Draw->Endsquare, Draw->Endsquare, 0, col, col.r.min);
+	im.line(p.sub((10,0)), p.add((10,0)), Draw->Endsquare, Draw->Endsquare, 0, col, col.r.min);
+	im.flush(Draw->Flushnow);
+}
+
+drawtext(im: ref Image, p: Point, col: ref Image, font: ref Font, text: string)
+{
+	(n, lines) := sys->tokenize(text, "\n");
+	p = p.sub((0, (n+1)*font.height));
+	for(; lines != nil; lines = tl lines){
+		s := hd lines;
+		w := font.width(s);
+		im.text(p.sub((w/2, 0)), col, col.r.min, font, s);
+		p = p.add((0, font.height));
+	}
+}
+
+abs(x: int): int
+{
+	if(x < 0)
+		return -x;
+	return x;
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "touchcal: %s\n", s);
+	if(mousepid > 0)
+		kill(mousepid);
+	raise "fail:touch";
+}
--- /dev/null
+++ b/appl/cmd/tr.b
@@ -1,0 +1,318 @@
+implement Tr;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "arg.m";
+	arg: Arg;
+
+Tr: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Pcb: adt {	# Control block controlling specification parse
+	spec: string;	# specification string
+	end:	int;	# its length
+	current:	int;	# current parse point
+	last:	int;	# last Rune returned
+	final:	int;	# final Rune in a span
+
+	new:	fn(nil: string): ref Pcb;
+	rewind:	fn(nil: self ref Pcb);
+	getc:	fn(nil: self ref Pcb): int;
+	canon:	fn(nil: self ref Pcb): int;
+};
+
+bits := array [] of { byte 1, byte 2, byte 4, byte 8, byte 16, byte 32, byte 64, byte 128 };
+
+SETBIT(a: array of byte, c: int)
+{
+	a[c>>3] |= bits[c & 7];
+}
+
+CLEARBIT(a: array of byte, c: int)
+{
+	a[c>>3] &= ~bits[c & 7];
+}
+
+BITSET(a: array of byte, c: int): int
+{
+	return int (a[c>>3] & bits[c&7]);
+}
+
+
+f := array[(Sys->Runemax+1)/8] of byte;
+t := array[(Sys->Runemax+1)/8] of byte;
+
+pto, pfrom: ref Pcb;
+
+cflag := 0;
+dflag := 0;
+sflag := 0;
+stderr: ref Sys->FD;
+
+ib: ref Iobuf;
+ob: ref Iobuf;
+
+usage()
+{
+	sys->fprint(stderr, "Usage: tr [-sdc] [from-set [to-set]]\n");
+	raise "fail: usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	arg = load Arg Arg->PATH;
+	arg->init(args);
+	while((c := arg->opt()) != 0)
+		case c {
+		's' => sflag = 1;
+		'd' => dflag = 1;
+		'c' => cflag = 1;
+		* => usage();
+	}
+	args = arg->argv();
+	argc := len args;
+	if(args != nil){
+		pfrom = Pcb.new(hd args);
+		args = tl args;
+	}
+	if(args != nil){
+		pto = Pcb.new(hd args);
+		args = tl args;
+	}
+	if(args != nil)
+		usage();
+	ib = bufio->fopen(sys->fildes(0), Bufio->OREAD);
+	ob = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+	if(dflag) {
+		if(sflag && argc != 2 || !sflag && argc != 1)
+			usage();
+		delete();
+	} else {
+		if(argc != 2)
+			usage();
+		if(cflag)
+			complement();
+		else
+			translit();
+	}
+	if(ob.flush() == Bufio->ERROR)
+		error(sys->sprint("write error: %r"));
+}
+
+delete()
+{
+	if (cflag) {
+		for(i := 0; i < len f; i++)
+			f[i] = byte 16rFF;
+		while ((c := pfrom.canon()) >= 0)
+			CLEARBIT(f, c);
+	} else {
+		while ((c := pfrom.canon()) >= 0)
+			SETBIT(f, c);
+	}
+	if (sflag) {
+		while ((c := pto.canon()) >= 0)
+			SETBIT(t, c);
+	}
+
+	last := Sys->Runemax+1;
+	while ((c := ib.getc()) >= 0) {
+		if(!BITSET(f, c) && (c != last || !BITSET(t,c))) {
+			last = c;
+			ob.putc(c);
+		}
+	}
+}
+
+complement()
+{
+	lastc := 0;
+	high := 0;
+	while ((from := pfrom.canon()) >= 0) {
+		if (from > high)
+			high = from;
+		SETBIT(f, from);
+	}
+	while ((cto := pto.canon()) >= 0) {
+		if (cto > high)
+			high = cto;
+		SETBIT(t,cto);
+	}
+	pto.rewind();
+	p := array[high+1] of int;
+	for (i := 0; i <= high; i++){
+		if (!BITSET(f,i)) {
+			if ((cto = pto.canon()) < 0)
+				cto = lastc;
+			else
+				lastc = cto;
+			p[i] = cto;
+		} else
+			p[i] = i;
+	}
+	if (sflag){
+		lastc = Sys->Runemax+1;
+		while ((from = ib.getc()) >= 0) {
+			if (from > high)
+				from = cto;
+			else
+				from = p[from];
+			if (from != lastc || !BITSET(t,from)) {
+				lastc = from;
+				ob.putc(from);
+			}
+		}
+	} else {
+		while ((from = ib.getc()) >= 0){
+			if (from > high)
+				from = cto;
+			else
+				from = p[from];
+			ob.putc(from);
+		}
+	}
+}
+
+translit()
+{
+	lastc := 0;
+	high := 0;
+	while ((from := pfrom.canon()) >= 0)
+		if (from > high)
+			high = from;
+	pfrom.rewind();
+	p := array[high+1] of int;
+	for (i := 0; i <= high; i++)
+		p[i] = i;
+	while ((from = pfrom.canon()) >= 0) {
+		if ((cto := pto.canon()) < 0)
+			cto = lastc;
+		else
+			lastc = cto;
+		if (BITSET(f,from) && p[from] != cto)
+			error("ambiguous translation");
+		SETBIT(f,from);
+		p[from] = cto;
+		SETBIT(t,cto);
+	}
+	while ((cto := pto.canon()) >= 0)
+		SETBIT(t,cto);
+	if (sflag){
+		lastc = Sys->Runemax+1;
+		while ((from = ib.getc()) >= 0) {
+			if (from <= high)
+				from = p[from];
+			if (from != lastc || !BITSET(t,from)) {
+				lastc = from;
+				ob.putc(from);
+			}
+		}
+				
+	} else {
+		while ((from = ib.getc()) >= 0) {
+			if (from <= high)
+				from = p[from];
+			ob.putc(from);
+		}
+	}
+}
+
+Pcb.new(s: string): ref Pcb
+{
+	return ref Pcb(s, len s, 0, -1, -1);
+}
+
+Pcb.rewind(p: self ref Pcb)
+{
+	p.current = 0;
+	p.last = p.final = -1;
+}
+
+Pcb.getc(p: self ref Pcb): int
+{
+	if(p.current >= p.end)
+		return -1;
+	s := p.current;
+	r := p.spec[s++];
+	if(r == '\\' && s < p.end){
+		n := 0;
+		if ((r = p.spec[s]) == 'x') {
+			s++;
+			for (i := 0; i < 6 && s < p.end; i++) {
+				p.current = s;
+				r = p.spec[s++];
+				if ('0' <= r && r <= '9')
+					n = 16*n + r - '0';
+				else if ('a' <= r && r <= 'f')
+					n = 16*n + r - 'a' + 10;
+				else if ('A' <= r && r <= 'F')
+					n = 16*n + r - 'A' + 10;
+				else {
+					if (i == 0)
+						return 'x';
+					return n;
+				}
+			}
+			r = n;
+		} else {
+			for(i := 0; i < 3 && s < p.end; i++) {
+				p.current = s;
+				r = p.spec[s++];
+				if('0' <= r && r <= '7')
+					n = 8*n + r - '0';
+				else {
+					if (i == 0)
+						return r;
+					return n;
+				}
+			}
+			if(n > 0377)
+				error("char>0377");
+			r = n;
+		}
+	}
+	p.current = s;
+	return r;
+}
+
+Pcb.canon(p: self ref Pcb): int
+{
+	if (p.final >= 0) {
+		if (p.last < p.final)
+			return ++p.last;
+		p.final = -1;
+	}
+	if (p.current >= p.end)
+		return -1;
+	if(p.spec[p.current] == '-' && p.last >= 0 && p.current+1 < p.end){
+		p.current++;
+		r := p.getc();
+		if (r < p.last)
+			error ("Invalid range specification");
+		if (r > p.last) {
+			p.final = r;
+			return ++p.last;
+		}
+	}
+	r := p.getc();
+	p.last = r;
+	return p.last;
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "tr: %s\n", s);
+	raise "fail: error";
+}
--- /dev/null
+++ b/appl/cmd/trfs.b
@@ -1,0 +1,213 @@
+implement Trfs;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+
+Trfs: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Fid: adt {
+	fid:	int;
+	isdir:	int;
+	aux:	int;
+};
+
+Table: adt[T] {
+	items: array of list of (int, T);
+	nilval: T;
+
+	new: fn(nslots: int, nilval: T): ref Table[T];
+	add:	fn(t: self ref Table, id: int, x: T): int;
+	del:	fn(t: self ref Table, id: int): T;
+	find:	fn(t: self ref Table, id: int): T;
+};
+
+NBspace: con 16r00A0;	# Unicode `no-break' space (looks like a faint box in some fonts)
+NBspacelen: con 2;		# length of it in utf-8
+
+msize: int;
+lock: chan of int;
+fids: ref Table[ref Fid];
+tfids: ref Table[ref Fid];
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	
+	if(len args != 3){
+		sys->fprint(sys->fildes(2), "usage: trfs dir mountpoint\n");
+		raise "fail:usage";
+	}
+	dir := hd tl args;
+	mntpt := hd tl tl args;
+	p := array[2] of ref Sys->FD;
+	q := array[2] of ref Sys->FD;
+	fids = Table[ref Fid].new(11, nil);
+	tfids = Table[ref Fid].new(11, nil);
+	lock = chan[1] of int;
+
+	styx->init();
+	sys->pipe(p);
+	sys->pipe(q);
+	if(sys->export(q[0], dir, Sys->EXPASYNC) < 0)
+		fatal("can't export " + dir);
+	spawn trfsin(p[1], q[1]);
+	spawn trfsout(p[1], q[1]);
+	if(sys->mount(p[0], nil, mntpt, Sys->MREPL|Sys->MCREATE, nil) < 0)
+		fatal("can't mount on " + mntpt);
+}
+
+trfsin(cfd, sfd: ref Sys->FD)
+{
+	while((t:=Tmsg.read(cfd, msize)) != nil){
+		pick m := t {
+		Clunk or
+		Remove =>
+			fids.del(m.fid);
+		Create =>
+			fid := ref Fid(m.fid, 0, 0);
+			fids.add(m.fid, fid);
+			addtfid(m.tag, fid);
+			m.name = tr(m.name, NBspace, ' ');
+		Open =>
+			fid := ref Fid(m.fid, 0, 0);
+			fids.add(m.fid, fid);
+			addtfid(m.tag, fid);
+		Read =>
+			fid := fids.find(m.fid);
+			addtfid(m.tag, fid);
+			if(fid.isdir){
+				m.count /= NBspacelen;	# translated strings might grow by this much
+				if(m.offset == big 0)
+					fid.aux = 0;
+				m.offset -= big fid.aux;
+			}
+		Walk =>
+			for(i:=0; i<len m.names; i++)
+				m.names[i] = tr(m.names[i], NBspace, ' ');
+		Wstat =>
+			m.stat.name = tr(m.stat.name, NBspace, ' ');
+		}
+		sys->write(sfd, t.pack(), t.packedsize());
+	}
+}
+		
+trfsout(cfd, sfd: ref Sys->FD)
+{
+	b := array[Styx->MAXFDATA] of byte;
+	while((r := Rmsg.read(sfd, msize)) != nil){
+		pick m := r {
+		Version =>
+			msize = m.msize;
+			if(msize > len b)
+				b = array[msize] of byte;	# a bit more than needed but doesn't matter
+		Create or
+		Open =>
+			fid := deltfid(m.tag);
+			fid.isdir = m.qid.qtype & Sys->QTDIR;
+		Read =>
+			fid := deltfid(m.tag);
+			if(fid.isdir){
+				bs := 0;
+				for(n := 0; n < len m.data; ){
+					(ds, d) := styx->unpackdir(m.data[n:]);
+					if(ds <= 0)
+						break;
+					d.name = tr(d.name, ' ', NBspace);
+					b[bs:] = styx->packdir(d);
+					bs += styx->packdirsize(d);
+					n += ds;
+				}
+				fid.aux += bs-n;
+				m.data = b[0:bs];
+			}
+		Stat =>
+			m.stat.name = tr(m.stat.name, ' ', NBspace);
+		}
+		sys->write(cfd, r.pack(), r.packedsize());
+	}
+}
+
+tr(name: string, c1, c2: int): string
+{
+	for(i:=0; i<len name; i++)
+		if(name[i] == c1)
+			name[i] = c2;
+	return name;
+}
+
+Table[T].new(nslots: int, nilval: T): ref Table[T]
+{
+	if(nslots == 0)
+		nslots = 13;
+	return ref Table[T](array[nslots] of list of (int, T), nilval);
+}
+
+Table[T].add(t: self ref Table[T], id: int, x: T): int
+{
+	slot := id % len t.items;
+	for(q := t.items[slot]; q != nil; q = tl q)
+		if((hd q).t0 == id)
+			return 0;
+	t.items[slot] = (id, x) :: t.items[slot];
+	return 1;
+}
+
+Table[T].del(t: self ref Table[T], id: int): T
+{
+	p: list of (int, T);
+	slot := id % len t.items;
+	for(q := t.items[slot]; q != nil; q = tl q){
+		if((hd q).t0 == id){
+			t.items[slot] = join(p, tl q);
+			return (hd q).t1;
+		}
+		p = hd q :: p;
+	}
+	return t.nilval;
+}
+
+Table[T].find(t: self ref Table[T], id: int): T
+{
+	for(p := t.items[id % len t.items]; p != nil; p = tl p)
+		if((hd p).t0 == id)
+			return (hd p).t1;
+	return t.nilval;
+}
+
+join[T](x, y: list of (int, T)): list of (int, T)
+{
+	for(; x != nil; x = tl x)
+		y = hd x :: y;
+	return y;
+}
+
+addtfid(t: int, fid: ref Fid)
+{
+	lock <-= 1;
+	tfids.add(t, fid);
+	<- lock;
+}
+
+deltfid(t: int): ref Fid
+{
+	lock <-= 1;
+	r := tfids.del(t);
+	<- lock;
+	return r;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "trfs: %s: %r\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/tsort.b
@@ -1,0 +1,133 @@
+implement Tsort;
+
+#
+# tsort -- topological sort
+#
+# convert a partial ordering into a linear ordering
+#
+# Copyright © 2004 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+Tsort: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Item: adt {
+	name:	string;
+	mark:	int;
+	succ:	cyclic list of ref Item;	# node's successors
+
+	precede:	fn(a: self ref Item, b: ref Item);
+};
+
+Q: adt {
+	item:	ref Item;
+	next:	cyclic ref Q;
+};
+
+items, itemt: ref Q;	# use a Q not a list only to keep input order
+nitem := 0;
+bout: ref Iobuf;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	bout = bufio->fopen(sys->fildes(1), Sys->OWRITE);
+	input();
+	output();
+	bout.flush();
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "tsort: %s\n", s);
+	raise "fail:error";
+}
+
+input()
+{
+	b := bufio->fopen(sys->fildes(0), Sys->OREAD);
+	while((line := b.gets('\n')) != nil){
+		(nil, fld) := sys->tokenize(line, " \t\n");
+		if(fld != nil){
+			a := finditem(hd fld);
+			while((fld = tl fld) != nil)
+				a.precede(finditem(hd fld));
+		}
+	}
+}
+
+Item.precede(a: self ref Item, b: ref Item)
+{
+	if(a != b){
+		for(l := a.succ; l != nil; l = tl l)
+			if((hd l) == b)
+				return;
+		a.succ = b :: a.succ;
+	}
+}
+
+finditem(s: string): ref Item
+{
+	# would use a hash table for large sets
+	for(il := items; il != nil; il = il.next)
+		if(il.item.name == s)
+			return il.item;
+	i := ref Item;
+	i.name = s;
+	i.mark = 0;
+	if(items != nil)
+		itemt = itemt.next = ref Q(i, nil);
+	else
+		itemt = items = ref Q(i, nil);
+	nitem++;
+	return i;
+}
+
+dep: list of ref Item;
+
+output()
+{
+	for(k := items; k != nil; k = k.next)
+		if((q := k.item).mark == 0)
+			visit(q, nil);
+	for(; dep != nil; dep = tl dep)
+		bout.puts((hd dep).name+"\n");
+}
+
+# visit q's successors depth first
+# parents is only used to print any cycles, and since it matches
+# the stack, the recursion could be eliminated
+visit(q: ref Item, parents: list of ref Item)
+{
+	q.mark = 2;
+	parents = q :: parents;
+	for(sl := q.succ; sl != nil; sl = tl sl)
+		if((s := hd sl).mark == 0)
+			visit(s, parents);
+		else if(s.mark == 2){
+			sys->fprint(sys->fildes(2), "tsort: cycle in input\n");
+			rl: list of ref Item;
+			for(l := parents;; l = tl l){	# reverse to be closer to input order
+				rl = hd l :: rl;
+				if(hd l == s)
+					break;
+			}
+			for(l = rl; l != nil; l = tl l)
+				sys->fprint(sys->fildes(2), "tsort: %s\n", (hd l).name);
+		}
+	q.mark = 1;
+	dep = q :: dep;
+}
--- /dev/null
+++ b/appl/cmd/unicode.b
@@ -1,0 +1,162 @@
+implement Unicode;
+
+include "sys.m";
+sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+
+Unicode: module
+{
+	init: fn(c: ref Draw->Context, v: list of string);
+};
+
+usage: con "unicode { [-t] hex hex ... | hexmin-hexmax ... | [-n] char ... }";
+hex: con "0123456789abcdefABCDEF";
+numout:= 0;
+text:= 0;
+out: ref Bufio->Iobuf;
+stderr: ref sys->FD;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	stderr = sys->fildes(2);
+
+	if(str==nil || bufio==nil){
+		sys->fprint(stderr, "unicode: can't load String or Bufio module: %r\n");
+		return;
+	}
+
+	if(argv == nil){
+		sys->fprint(stderr, "usage: %s\n", usage);
+		return;
+	}
+	argv = tl argv;
+	while(argv != nil) {
+		s := hd argv;
+		if(s != nil && s[0] != '-')
+			break;
+		case s{
+		"-n" =>
+			numout = 1;
+		"-t" =>
+			text = 1;
+		}
+		argv = tl argv;
+	}
+	if(argv == nil){
+		sys->fprint(stderr, "usage: %s\n", usage);
+		return;
+	}
+
+	out = bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+
+	if(!numout && oneof(hd argv, '-'))
+		range(argv);
+	else if(numout || oneof(hex, (hd argv)[0]) == 0)
+		nums(argv);
+	else
+		chars(argv);
+	out.flush();
+}
+
+oneof(s: string, c: int): int
+{
+	for(i:=0; i<len s; i++)
+		if(s[i] == c)
+			return 1;
+	return 0;
+}
+
+badrange(q: string)
+{
+	sys->fprint(stderr, "unicode: bad range %s\n", q);
+}
+
+range(argv: list of string)
+{
+	min, max: int;
+
+	while(argv != nil){
+		q := hd argv;
+		if(oneof(hex, q[0]) == 0){
+			badrange(q);
+			return;
+		}
+		(min, q) = str->toint(q,16);
+		if(min<0 || min>Sys->Runemax || len q==0 || q[0]!='-'){
+			badrange(hd argv);
+			return;
+		}
+		q = q[1:];
+		if(oneof(hex, q[0]) == 0){
+			badrange(hd argv);
+			return;
+		}
+		(max, q) = str->toint(q,16);
+		if(max<0 || max>Sys->Runemax || max<min || len q>0){
+			badrange(hd argv);
+			return;
+		}
+		i := 0;
+		do{
+			out.puts(sys->sprint("%.4x %c", min, min));
+			i++;
+			if(min==max || (i&7)==0)
+				out.puts("\n");
+			else
+				out.puts("\t");
+			min++;
+		}while(min<=max);
+		argv = tl argv;
+	}
+}
+
+
+nums(argv: list of string)
+{
+	while(argv != nil){
+		q := hd argv;
+		for(i:=0; i<len q; i++)
+			out.puts(sys->sprint("%.4x\n", q[i]));
+		argv = tl argv;
+	}
+}
+
+badvalue(s: string)
+{
+	sys->fprint(stderr, "unicode: bad unicode value %s\n", s);
+}
+
+chars(argv: list of string)
+{
+	m: int;
+
+	while(argv != nil){
+		q := hd argv;
+		if(oneof(hex, q[0]) == 0){
+			badvalue(hd argv);
+			return;
+		}
+		(m, q) = str->toint(q, 16);
+		if(m<0 || m>Sys->Runemax || len q>0){
+			badvalue(hd argv);
+			return;
+		}
+		out.puts(sys->sprint("%c", m));
+		if(!text)
+			out.puts("\n");
+		argv = tl argv;
+	}
+}
--- /dev/null
+++ b/appl/cmd/uniq.b
@@ -1,0 +1,79 @@
+implement Uniq;
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+include "draw.m";
+include "arg.m";
+
+Uniq: module
+{
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+usage()
+{
+	fail("usage", sys->sprint("usage: uniq [-ud] [file]"));
+}
+
+init(nil : ref Draw->Context, args : list of string)
+{
+	bio : ref Bufio->Iobuf;
+
+	sys = load Sys Sys->PATH;
+	bufio := load Bufio Bufio->PATH;
+	if (bufio == nil)
+		fail("bad module", sys->sprint("uniq: cannot load %s: %r", Bufio->PATH));
+	Iobuf: import bufio;
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		fail("bad module", sys->sprint("uniq: cannot load %s: %r", Arg->PATH));
+
+	uflag := 0;
+	dflag := 0;
+	arg->init(args);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'u' =>
+			uflag = 1;
+		'd' =>
+			dflag = 1;
+		* =>
+			usage();
+		}
+	}
+	args = arg->argv();
+	if (len args > 1)
+		usage();
+	if (args != nil) {
+		bio = bufio->open(hd args, Bufio->OREAD);
+		if (bio == nil)
+			fail("open file", sys->sprint("uniq: cannot open %s: %r\n", hd args));
+	} else
+		bio = bufio->fopen(sys->fildes(0), Bufio->OREAD);
+
+	stdout := bufio->fopen(sys->fildes(1), Bufio->OWRITE);
+	if (!(uflag || dflag))
+		uflag = dflag = 1;
+	prev := "";
+	n := 0;
+	while ((s := bio.gets('\n')) != nil) {
+		if (s == prev)
+			n++;
+		else {
+			if ((uflag && n == 1) || (dflag && n > 1))
+				stdout.puts(prev);
+			n = 1;
+			prev = s;
+		}
+	}
+	if ((uflag && n == 1) || (dflag && n > 1))
+		stdout.puts(prev);
+	stdout.close();
+}
+
+fail(ex, msg: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", msg);
+	raise "fail:"+ex;
+}
--- /dev/null
+++ b/appl/cmd/units.b
@@ -1,0 +1,1061 @@
+implement Units;
+
+#line	2	"units.y"
+#
+# subject to the Lucent Public License 1.02
+#
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "math.m";
+	math: Math;
+
+include "arg.m";
+
+Ndim: con 15;	# number of dimensions
+Nvar: con 203;	# hash table size
+Maxe: con 695.0;	# log of largest number
+
+Node: adt
+{
+	val:	real;
+	dim:	array of int;	# [Ndim] schar
+
+	mk:	fn(v: real): Node;
+	text:	fn(n: self Node): string;
+	add:	fn(a: self Node, b: Node): Node;
+	sub:	fn(a: self Node, b: Node): Node;
+	mul:	fn(a: self Node, b: Node): Node;
+	div:	fn(a: self Node, b: Node): Node;
+	xpn:	fn(a: self Node, b: int): Node;
+	copy: fn(a: self Node): Node;
+};
+Var: adt
+{
+	name:	string;
+	node:	Node;
+};
+Prefix: adt
+{
+	val:	real;
+	pname:	string;
+};
+
+digval := 0;
+fi: ref Iobuf;
+fund := array[Ndim] of ref Var;
+line: string;
+lineno := 0;
+linep := 0;
+nerrors := 0;
+peekrune := 0;
+retnode1: Node;
+retnode2: Node;
+retnode: Node;
+sym: string;
+vars := array[Nvar] of list of ref Var;
+vflag := 0;
+
+YYSTYPE: adt {
+	node:	Node;
+	var:	ref Var;
+	numb:	int;
+	val:	real;
+};
+
+YYLEX: adt {
+	lval: YYSTYPE;
+	lex: fn(l: self ref YYLEX): int;
+	error: fn(l: self ref YYLEX, msg: string);
+};
+  
+Units: module {
+
+	init:	fn(nil: ref Draw->Context, args: list of string);
+VAL: con	57346;
+VAR: con	57347;
+SUP: con	57348;
+
+};
+YYEOFCODE: con 1;
+YYERRCODE: con 2;
+YYMAXDEPTH: con 200;
+
+#line	203	"units.y"
+
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	math = load Math Math->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("units [-v] [file]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'v' => vflag = 1;
+		* => arg->usage();
+	}
+	args = arg->argv();
+	arg = nil;
+
+	file := "/lib/units";
+	if(args != nil)
+		file = hd args;
+	fi = bufio->open(file, Sys->OREAD);
+	if(fi == nil) {
+		sys->fprint(sys->fildes(2), "units: cannot open %s: %r\n", file);
+		raise "fail:open";
+	}
+	lex := ref YYLEX;
+
+	#
+	# read the 'units' file to
+	# develop a database
+	#
+	lineno = 0;
+	for(;;) {
+		lineno++;
+		if(readline())
+			break;
+		if(len line == 0 || line[0] == '/')
+			continue;
+		peekrune = ':';
+		yyparse(lex);
+	}
+
+	#
+	# read the console to
+	# print ratio of pairs
+	#
+	fi = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	lineno = 0;
+	for(;;) {
+		if(lineno & 1)
+			sys->print("you want: ");
+		else
+			sys->print("you have: ");
+		if(readline())
+			break;
+		peekrune = '?';
+		nerrors = 0;
+		yyparse(lex);
+		if(nerrors)
+			continue;
+		if(lineno & 1) {
+			isspcl: int;
+			(isspcl, retnode) = specialcase(retnode2, retnode1);
+			if(isspcl)
+				sys->print("\tis %s\n", retnode.text());
+			else {
+				retnode = retnode2.div(retnode1);
+				sys->print("\t* %s\n", retnode.text());
+				retnode = retnode1.div(retnode2);
+				sys->print("\t/ %s\n", retnode.text());
+			}
+		} else
+			retnode2 = retnode1.copy();
+		lineno++;
+	}
+	sys->print("\n");
+}
+
+YYLEX.lex(lex: self ref YYLEX): int
+{
+	c := peekrune;
+	peekrune = ' ';
+
+	while(c == ' ' || c == '\t'){
+		if(linep >= len line)
+			return 0;	# -1?
+		c = line[linep++];
+	}
+	case c {
+	'0' to '9' or '.' =>
+		digval = c;
+		(lex.lval.val, peekrune) = readreal(gdigit, lex);
+		return VAL;
+	'×' =>
+		return '*';
+	'÷' =>
+		return '/';
+	'¹' or
+	'ⁱ' =>
+		lex.lval.numb = 1;
+		return SUP;
+	'²' or
+	'⁲' =>
+		lex.lval.numb = 2;
+		return SUP;
+	'³' or
+	'⁳' =>
+		lex.lval.numb = 3;
+		return SUP;
+	* =>
+		if(ralpha(c)){
+			sym = "";
+			for(i:=0;; i++) {
+				sym[i] = c;
+				if(linep >= len line){
+					c = ' ';
+					break;
+				}
+				c = line[linep++];
+				if(!ralpha(c))
+					break;
+			}
+			peekrune = c;
+			lex.lval.var = lookup(0);
+			return VAR;
+		}
+	}
+	return c;
+}
+
+#
+# all characters that have some
+# meaning. rest are usable as names
+#
+ralpha(c: int): int
+{
+	case c {
+	0 or
+	'+'  or
+	'-'  or
+	'*'  or
+	'/'  or
+	'['  or
+	']'  or
+	'('  or
+	')'  or
+	'^'  or
+	':'  or
+	'?'  or
+	' '  or
+	'\t'  or
+	'.'  or
+	'|'  or
+	'#'  or
+	'¹'  or
+	'ⁱ'  or
+	'²'  or
+	'⁲'  or
+	'³'  or
+	'⁳'  or
+	'×'  or
+	'÷'  =>
+		return 0;
+	}
+	return 1;
+}
+
+gdigit(nil: ref YYLEX): int
+{
+	c := digval;
+	if(c) {
+		digval = 0;
+		return c;
+	}
+	if(linep >= len line)
+		return 0;
+	return line[linep++];
+}
+
+YYLEX.error(lex: self ref YYLEX, s: string)
+{
+	#
+	# hack to intercept message from yaccpar
+	#
+	if(s == "syntax error") {
+		lex.error(sys->sprint("syntax error, last name: %s", sym));
+		return;
+	}
+	sys->print("%d: %s\n\t%s\n", lineno, line, s);
+	nerrors++;
+	if(nerrors > 5) {
+		sys->print("too many errors\n");
+		raise "fail:errors";
+	}
+}
+
+yyerror(s: string)
+{
+	l := ref YYLEX;
+	l.error(s);
+}
+
+Node.mk(v: real): Node
+{
+	return (v, array[Ndim] of {* => 0});
+}
+
+Node.add(a: self Node, b: Node): Node
+{
+	c := Node.mk(fadd(a.val, b.val));
+	for(i:=0; i<Ndim; i++) {
+		d := a.dim[i];
+		c.dim[i] = d;
+		if(d != b.dim[i])
+			yyerror("add must be like units");
+	}
+	return c;
+}
+
+Node.sub(a: self Node, b: Node): Node
+{
+	c := Node.mk(fadd(a.val, -b.val));
+	for(i:=0; i<Ndim; i++) {
+		d := a.dim[i];
+		c.dim[i] = d;
+		if(d != b.dim[i])
+			yyerror("sub must be like units");
+	}
+	return c;
+}
+
+Node.mul(a: self Node, b: Node): Node
+{
+	c := Node.mk(fmul(a.val, b.val));
+	for(i:=0; i<Ndim; i++)
+		c.dim[i] = a.dim[i] + b.dim[i];
+	return c;
+}
+
+Node.div(a: self Node, b: Node): Node
+{
+	c := Node.mk(fdiv(a.val, b.val));
+	for(i:=0; i<Ndim; i++)
+		c.dim[i] = a.dim[i] - b.dim[i];
+	return c;
+}
+
+Node.xpn(a: self Node, b: int): Node
+{
+	c := Node.mk(1.0);
+	if(b < 0) {
+		b = -b;
+		for(i:=0; i<b; i++)
+			c = c.div(a);
+	} else
+		for(i:=0; i<b; i++)
+			c = c.mul(a);
+	return c;
+}
+
+Node.copy(a: self Node): Node
+{
+	c := Node.mk(a.val);
+	c.dim[0:] = a.dim;
+	return c;
+}
+
+specialcase(a, b: Node): (int, Node)
+{
+	c := Node.mk(0.0);
+	d1 := 0;
+	d2 := 0;
+	for(i:=1; i<Ndim; i++) {
+		d := a.dim[i];
+		if(d) {
+			if(d != 1 || d1)
+				return (0, c);
+			d1 = i;
+		}
+		d = b.dim[i];
+		if(d) {
+			if(d != 1 || d2)
+				return (0, c);
+			d2 = i;
+		}
+	}
+	if(d1 == 0 || d2 == 0)
+		return (0, c);
+
+	if(fund[d1].name == "°C" &&
+	   fund[d2].name == "°F" &&
+	   b.val == 1.0) {
+		c = b.copy();
+		c.val = a.val * 9. / 5. + 32.;
+		return (1, c);
+	}
+
+	if(fund[d1].name == "°F" &&
+	   fund[d2].name == "°C" &&
+	   b.val == 1.0) {
+		c = b.copy();
+		c.val = (a.val - 32.) * 5. / 9.;
+		return (1, c);
+	}
+	return (0, c);
+}
+
+printdim(d: int, n: int): string
+{
+	s := "";
+	if(n) {
+		v := fund[d];
+		if(v != nil)
+			s += " "+v.name;
+		else
+			s += sys->sprint(" [%d]", d);
+		case n {
+		1 =>
+			;
+		2 =>
+			s += "²";
+		3 =>
+			s += "³";
+		4 =>
+			s += "⁴";
+		* =>
+			s += sys->sprint("^%d", n);
+		}
+	}
+	return s;
+}
+
+Node.text(n: self Node): string
+{
+	str := sys->sprint("%.7g", n.val);
+	f := 0;
+	for(i:=1; i<len n.dim; i++) {
+		d := n.dim[i];
+		if(d > 0)
+			str += printdim(i, d);
+		else if(d < 0)
+			f = 1;
+	}
+
+	if(f) {
+		str += " /";
+		for(i=1; i<len n.dim; i++) {
+			d := n.dim[i];
+			if(d < 0)
+				str += printdim(i, -d);
+		}
+	}
+
+	return str;
+}
+
+readline(): int
+{
+	linep = 0;
+	line = "";
+	for(i:=0;; i++) {
+		c := fi.getc();
+		if(c < 0)
+			return 1;
+		if(c == '\n')
+			return 0;
+		line[i] = c;
+	}
+}
+
+lookup(f: int): ref Var
+{
+	h := 0;
+	for(i:=0; i < len sym; i++)
+		h = h*13 + sym[i];
+	if(h < 0)
+		h ^= int 16r80000000;
+	h %= len vars;
+
+	for(vl:=vars[h]; vl != nil; vl = tl vl)
+		if((hd vl).name == sym)
+			return hd vl;
+	if(f)
+		return nil;
+	v := ref Var(sym, Node.mk(0.0));
+	vars[h] = v :: vars[h];
+
+	p := 1.0;
+	for(;;) {
+		p = fmul(p, pname());
+		if(p == 0.0)
+			break;
+		w := lookup(1);
+		if(w != nil) {
+			v.node = w.node.copy();
+			v.node.val = fmul(v.node.val, p);
+			break;
+		}
+	}
+	return v;
+}
+
+prefix: array of Prefix = array[] of {
+	(1e-24,	"yocto"),
+	(1e-21,	"zepto"),
+	(1e-18,	"atto"),
+	(1e-15,	"femto"),
+	(1e-12,	"pico"),
+	(1e-9,	"nano"),
+	(1e-6,	"micro"),
+	(1e-6,	"μ"),
+	(1e-3,	"milli"),
+	(1e-2,	"centi"),
+	(1e-1,	"deci"),
+	(1e1,	"deka"),
+	(1e2,	"hecta"),
+	(1e2,	"hecto"),
+	(1e3,	"kilo"),
+	(1e6,	"mega"),
+	(1e6,	"meg"),
+	(1e9,	"giga"),
+	(1e12,	"tera"),
+	(1e15,	"peta"),
+	(1e18,	"exa"),
+	(1e21,	"zetta"),
+	(1e24,	"yotta")
+};
+
+pname(): real
+{
+	#
+	# rip off normal prefices
+	#
+Pref:
+	for(i:=0; i < len prefix; i++) {
+		p := prefix[i].pname;
+		for(j:=0; j < len p; j++)
+			if(j >= len sym || p[j] != sym[j])
+				continue Pref;
+		sym = sym[j:];
+		return prefix[i].val;
+	}
+
+	#
+	# rip off 's' suffixes
+	#
+	for(j:=0; j < len sym; j++)
+		;
+	j--;
+	# j>1 is special hack to disallow ms finding m
+	if(j > 1 && sym[j] == 's') {
+		sym = sym[0:j];
+		return 1.0;
+	}
+	return 0.0;
+}
+
+#
+# reads a floating-point number
+#
+
+readreal[T](f: ref fn(t: T): int, vp: T): (real, int)
+{
+	s := "";
+	c := f(vp);
+	while(c == ' ' || c == '\t')
+		c = f(vp);
+	if(c == '-' || c == '+'){
+		s[len s] = c;
+		c = f(vp);
+	}
+	start := len s;
+	while(c >= '0' && c <= '9'){
+		s[len s] = c;
+		c = f(vp);
+	}
+	if(c == '.'){
+		s[len s] = c;
+		c = f(vp);
+		while(c >= '0' && c <= '9'){
+			s[len s] = c;
+			c = f(vp);
+		}
+	}
+	if(len s > start && (c == 'e' || c == 'E')){
+		s[len s] = c;
+		c = f(vp);
+		if(c == '-' || c == '+'){
+			s[len s] = c;
+			c = f(vp);
+		}
+		while(c >= '0' && c <= '9'){
+			s[len s] = c;
+			c = f(vp);
+		}
+	}
+	return (real s, c);
+}
+
+#
+# careful floating point
+#
+
+fmul(a, b: real): real
+{
+	l: real;
+
+	if(a <= 0.0) {
+		if(a == 0.0)
+			return 0.0;
+		l = math->log(-a);
+	} else
+		l = math->log(a);
+
+	if(b <= 0.0) {
+		if(b == 0.0)
+			return 0.0;
+		l += math->log(-b);
+	} else
+		l += math->log(b);
+
+	if(l > Maxe) {
+		yyerror("overflow in multiply");
+		return 1.0;
+	}
+	if(l < -Maxe) {
+		yyerror("underflow in multiply");
+		return 0.0;
+	}
+	return a*b;
+}
+
+fdiv(a, b: real): real
+{
+	l: real;
+
+	if(a <= 0.0) {
+		if(a == 0.0)
+			return 0.0;
+		l = math->log(-a);
+	} else
+		l = math->log(a);
+
+	if(b <= 0.0) {
+		if(b == 0.0) {
+			yyerror("division by zero");
+			return 1.0;
+		}
+		l -= math->log(-b);
+	} else
+		l -= math->log(b);
+
+	if(l > Maxe) {
+		yyerror("overflow in divide");
+		return 1.0;
+	}
+	if(l < -Maxe) {
+		yyerror("underflow in divide");
+		return 0.0;
+	}
+	return a/b;
+}
+
+fadd(a, b: real): real
+{
+	return a + b;
+}
+yyexca := array[] of {-1, 1,
+	1, -1,
+	-2, 0,
+};
+YYNPROD: con 21;
+YYPRIVATE: con 57344;
+yytoknames: array of string;
+yystates: array of string;
+yydebug: con 0;
+YYLAST:	con 41;
+yyact := array[] of {
+   8,  10,   7,   9,  16,  17,  12,  11,  20,  21,
+  15,  31,  23,   6,   4,  12,  11,  22,  13,   5,
+   1,  27,  28,   0,  14,  30,  29,  13,  20,  20,
+  25,  26,   0,  24,  18,  19,  16,  17,   2,   0,
+   3,
+};
+yypact := array[] of {
+  31,-1000,   9,  11,   2,  26,  22,  11,   3,  -3,
+-1000,-1000,-1000,  11,  26,-1000,  11,  11,  11,  11,
+   3,-1000,  11,  11,  -6,  22,  22,  11,  11,  -3,
+-1000,-1000,
+};
+yypgo := array[] of {
+   0,  20,  19,   1,   3,   0,   2,  13,
+};
+yyr1 := array[] of {
+   0,   1,   1,   1,   1,   2,   2,   2,   7,   7,
+   7,   6,   6,   5,   5,   5,   4,   4,   3,   3,
+   3,
+};
+yyr2 := array[] of {
+   0,   3,   3,   2,   1,   1,   3,   3,   1,   3,
+   3,   1,   2,   1,   2,   3,   1,   3,   1,   1,
+   3,
+};
+yychk := array[] of {
+-1000,  -1,   7,   9,   5,  -2,  -7,  -6,  -5,  -4,
+  -3,   5,   4,  16,  -2,   8,  10,  11,  12,  13,
+  -5,   6,  14,  15,  -2,  -7,  -7,  -6,  -6,  -4,
+  -3,  17,
+};
+yydef := array[] of {
+   0,  -2,   0,   4,   0,   3,   5,   8,  11,  13,
+  16,  18,  19,   0,   1,   2,   0,   0,   0,   0,
+  12,  14,   0,   0,   0,   6,   7,   9,  10,  15,
+  17,  20,
+};
+yytok1 := array[] of {
+   1,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   8,   3,   3,   3,   3,
+  16,  17,  12,  10,   3,  11,   3,  13,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   7,   3,
+   3,   3,   3,   9,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,  14,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   3,   3,   3,   3,   3,
+   3,   3,   3,   3,  15,
+};
+yytok2 := array[] of {
+   2,   3,   4,   5,   6,
+};
+yytok3 := array[] of {
+   0
+};
+
+YYSys: module
+{
+	FD: adt
+	{
+		fd:	int;
+	};
+	fildes:		fn(fd: int): ref FD;
+	fprint:		fn(fd: ref FD, s: string, *): int;
+};
+
+yysys: YYSys;
+yystderr: ref YYSys->FD;
+
+YYFLAG: con -1000;
+
+# parser for yacc output
+
+yytokname(yyc: int): string
+{
+	if(yyc > 0 && yyc <= len yytoknames && yytoknames[yyc-1] != nil)
+		return yytoknames[yyc-1];
+	return "<"+string yyc+">";
+}
+
+yystatname(yys: int): string
+{
+	if(yys >= 0 && yys < len yystates && yystates[yys] != nil)
+		return yystates[yys];
+	return "<"+string yys+">\n";
+}
+
+yylex1(yylex: ref YYLEX): int
+{
+	c : int;
+	yychar := yylex.lex();
+	if(yychar <= 0)
+		c = yytok1[0];
+	else if(yychar < len yytok1)
+		c = yytok1[yychar];
+	else if(yychar >= YYPRIVATE && yychar < YYPRIVATE+len yytok2)
+		c = yytok2[yychar-YYPRIVATE];
+	else{
+		n := len yytok3;
+		c = 0;
+		for(i := 0; i < n; i+=2) {
+			if(yytok3[i+0] == yychar) {
+				c = yytok3[i+1];
+				break;
+			}
+		}
+		if(c == 0)
+			c = yytok2[1];	# unknown char
+	}
+	if(yydebug >= 3)
+		yysys->fprint(yystderr, "lex %.4ux %s\n", yychar, yytokname(c));
+	return c;
+}
+
+YYS: adt
+{
+	yyv: YYSTYPE;
+	yys: int;
+};
+
+yyparse(yylex: ref YYLEX): int
+{
+	if(yydebug >= 1 && yysys == nil) {
+		yysys = load YYSys "$Sys";
+		yystderr = yysys->fildes(2);
+	}
+
+	yys := array[YYMAXDEPTH] of YYS;
+
+	yyval: YYSTYPE;
+	yystate := 0;
+	yychar := -1;
+	yynerrs := 0;		# number of errors
+	yyerrflag := 0;		# error recovery flag
+	yyp := -1;
+	yyn := 0;
+
+yystack:
+	for(;;){
+		# put a state and value onto the stack
+		if(yydebug >= 4)
+			yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+
+		yyp++;
+		if(yyp >= len yys)
+			yys = (array[len yys * 2] of YYS)[0:] = yys;
+		yys[yyp].yys = yystate;
+		yys[yyp].yyv = yyval;
+
+		for(;;){
+			yyn = yypact[yystate];
+			if(yyn > YYFLAG) {	# simple state
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+				yyn += yychar;
+				if(yyn >= 0 && yyn < YYLAST) {
+					yyn = yyact[yyn];
+					if(yychk[yyn] == yychar) { # valid shift
+						yychar = -1;
+						yyp++;
+						if(yyp >= len yys)
+							yys = (array[len yys * 2] of YYS)[0:] = yys;
+						yystate = yyn;
+						yys[yyp].yys = yystate;
+						yys[yyp].yyv = yylex.lval;
+						if(yyerrflag > 0)
+							yyerrflag--;
+						if(yydebug >= 4)
+							yysys->fprint(yystderr, "char %s in %s", yytokname(yychar), yystatname(yystate));
+						continue;
+					}
+				}
+			}
+		
+			# default state action
+			yyn = yydef[yystate];
+			if(yyn == -2) {
+				if(yychar < 0)
+					yychar = yylex1(yylex);
+		
+				# look through exception table
+				for(yyxi:=0;; yyxi+=2)
+					if(yyexca[yyxi] == -1 && yyexca[yyxi+1] == yystate)
+						break;
+				for(yyxi += 2;; yyxi += 2) {
+					yyn = yyexca[yyxi];
+					if(yyn < 0 || yyn == yychar)
+						break;
+				}
+				yyn = yyexca[yyxi+1];
+				if(yyn < 0){
+					yyn = 0;
+					break yystack;
+				}
+			}
+
+			if(yyn != 0)
+				break;
+
+			# error ... attempt to resume parsing
+			if(yyerrflag == 0) { # brand new error
+				yylex.error("syntax error");
+				yynerrs++;
+				if(yydebug >= 1) {
+					yysys->fprint(yystderr, "%s", yystatname(yystate));
+					yysys->fprint(yystderr, "saw %s\n", yytokname(yychar));
+				}
+			}
+
+			if(yyerrflag != 3) { # incompletely recovered error ... try again
+				yyerrflag = 3;
+	
+				# find a state where "error" is a legal shift action
+				while(yyp >= 0) {
+					yyn = yypact[yys[yyp].yys] + YYERRCODE;
+					if(yyn >= 0 && yyn < YYLAST) {
+						yystate = yyact[yyn];  # simulate a shift of "error"
+						if(yychk[yystate] == YYERRCODE)
+							continue yystack;
+					}
+	
+					# the current yyp has no shift onn "error", pop stack
+					if(yydebug >= 2)
+						yysys->fprint(yystderr, "error recovery pops state %d, uncovers %d\n",
+							yys[yyp].yys, yys[yyp-1].yys );
+					yyp--;
+				}
+				# there is no state on the stack with an error shift ... abort
+				yyn = 1;
+				break yystack;
+			}
+
+			# no shift yet; clobber input char
+			if(yydebug >= 2)
+				yysys->fprint(yystderr, "error recovery discards %s\n", yytokname(yychar));
+			if(yychar == YYEOFCODE) {
+				yyn = 1;
+				break yystack;
+			}
+			yychar = -1;
+			# try again in the same state
+		}
+	
+		# reduction by production yyn
+		if(yydebug >= 2)
+			yysys->fprint(yystderr, "reduce %d in:\n\t%s", yyn, yystatname(yystate));
+	
+		yypt := yyp;
+		yyp -= yyr2[yyn];
+#		yyval = yys[yyp+1].yyv;
+		yym := yyn;
+	
+		# consult goto table to find next state
+		yyn = yyr1[yyn];
+		yyg := yypgo[yyn];
+		yyj := yyg + yys[yyp].yys + 1;
+	
+		if(yyj >= YYLAST || yychk[yystate=yyact[yyj]] != -yyn)
+			yystate = yyact[yyg];
+		case yym {
+			
+1=>
+#line	90	"units.y"
+{
+		f := yys[yypt-1].yyv.var.node.dim[0];
+		yys[yypt-1].yyv.var.node = yys[yypt-0].yyv.node.copy();
+		yys[yypt-1].yyv.var.node.dim[0] = 1;
+		if(f)
+			yyerror(sys->sprint("redefinition of %s", yys[yypt-1].yyv.var.name));
+		else if(vflag)
+			sys->print("%s\t%s\n", yys[yypt-1].yyv.var.name, yys[yypt-1].yyv.var.node.text());
+	}
+2=>
+#line	100	"units.y"
+{
+		for(i:=1; i<Ndim; i++)
+			if(fund[i] == nil)
+				break;
+		if(i >= Ndim) {
+			yyerror("too many dimensions");
+			i = Ndim-1;
+		}
+		fund[i] = yys[yypt-1].yyv.var;
+
+		f := yys[yypt-1].yyv.var.node.dim[0];
+		yys[yypt-1].yyv.var.node = Node.mk(1.0);
+		yys[yypt-1].yyv.var.node.dim[0] = 1;
+		yys[yypt-1].yyv.var.node.dim[i] = 1;
+		if(f)
+			yyerror(sys->sprint("redefinition of %s", yys[yypt-1].yyv.var.name));
+		else if(vflag)
+			sys->print("%s\t#\n", yys[yypt-1].yyv.var.name);
+	}
+3=>
+#line	120	"units.y"
+{
+		retnode1 = yys[yypt-0].yyv.node.copy();
+	}
+4=>
+#line	124	"units.y"
+{
+		retnode1 = Node.mk(1.0);
+	}
+5=>
+yyval.node = yys[yyp+1].yyv.node;
+6=>
+#line	131	"units.y"
+{
+		yyval.node = yys[yypt-2].yyv.node.add(yys[yypt-0].yyv.node);
+	}
+7=>
+#line	135	"units.y"
+{
+		yyval.node = yys[yypt-2].yyv.node.sub(yys[yypt-0].yyv.node);
+	}
+8=>
+yyval.node = yys[yyp+1].yyv.node;
+9=>
+#line	142	"units.y"
+{
+		yyval.node = yys[yypt-2].yyv.node.mul(yys[yypt-0].yyv.node);
+	}
+10=>
+#line	146	"units.y"
+{
+		yyval.node = yys[yypt-2].yyv.node.div(yys[yypt-0].yyv.node);
+	}
+11=>
+yyval.node = yys[yyp+1].yyv.node;
+12=>
+#line	153	"units.y"
+{
+		yyval.node = yys[yypt-1].yyv.node.mul(yys[yypt-0].yyv.node);
+	}
+13=>
+yyval.node = yys[yyp+1].yyv.node;
+14=>
+#line	160	"units.y"
+{
+		yyval.node = yys[yypt-1].yyv.node.xpn(yys[yypt-0].yyv.numb);
+	}
+15=>
+#line	164	"units.y"
+{
+		for(i:=1; i<Ndim; i++)
+			if(yys[yypt-0].yyv.node.dim[i]) {
+				yyerror("exponent has units");
+				yyval.node = yys[yypt-2].yyv.node;
+				break;
+			}
+		if(i >= Ndim) {
+			i = int yys[yypt-0].yyv.node.val;
+			if(real i != yys[yypt-0].yyv.node.val)
+				yyerror("exponent not integral");
+			yyval.node = yys[yypt-2].yyv.node.xpn(i);
+		}
+	}
+16=>
+yyval.node = yys[yyp+1].yyv.node;
+17=>
+#line	182	"units.y"
+{
+		yyval.node = yys[yypt-2].yyv.node.div(yys[yypt-0].yyv.node);
+	}
+18=>
+#line	188	"units.y"
+{
+		if(yys[yypt-0].yyv.var.node.dim[0] == 0) {
+			yyerror(sys->sprint("undefined %s", yys[yypt-0].yyv.var.name));
+			yyval.node = Node.mk(1.0);
+		} else
+			yyval.node = yys[yypt-0].yyv.var.node.copy();
+	}
+19=>
+#line	196	"units.y"
+{
+		yyval.node = Node.mk(yys[yypt-0].yyv.val);
+	}
+20=>
+#line	200	"units.y"
+{
+		yyval.node = yys[yypt-1].yyv.node;
+	}
+		}
+	}
+
+	return yyn;
+}
--- /dev/null
+++ b/appl/cmd/units.y
@@ -1,0 +1,771 @@
+%{
+#
+# subject to the Lucent Public License 1.02
+#
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "math.m";
+	math: Math;
+
+include "arg.m";
+
+Ndim: con 15;	# number of dimensions
+Nvar: con 203;	# hash table size
+Maxe: con 695.0;	# log of largest number
+
+Node: adt
+{
+	val:	real;
+	dim:	array of int;	# [Ndim] schar
+
+	mk:	fn(v: real): Node;
+	text:	fn(n: self Node): string;
+	add:	fn(a: self Node, b: Node): Node;
+	sub:	fn(a: self Node, b: Node): Node;
+	mul:	fn(a: self Node, b: Node): Node;
+	div:	fn(a: self Node, b: Node): Node;
+	xpn:	fn(a: self Node, b: int): Node;
+	copy: fn(a: self Node): Node;
+};
+Var: adt
+{
+	name:	string;
+	node:	Node;
+};
+Prefix: adt
+{
+	val:	real;
+	pname:	string;
+};
+
+digval := 0;
+fi: ref Iobuf;
+fund := array[Ndim] of ref Var;
+line: string;
+lineno := 0;
+linep := 0;
+nerrors := 0;
+peekrune := 0;
+retnode1: Node;
+retnode2: Node;
+retnode: Node;
+sym: string;
+vars := array[Nvar] of list of ref Var;
+vflag := 0;
+
+YYSTYPE: adt {
+	node:	Node;
+	var:	ref Var;
+	numb:	int;
+	val:	real;
+};
+
+YYLEX: adt {
+	lval: YYSTYPE;
+	lex: fn(l: self ref YYLEX): int;
+	error: fn(l: self ref YYLEX, msg: string);
+};
+  
+%}
+%module Units
+{
+	init:	fn(nil: ref Draw->Context, args: list of string);
+}
+
+%type	<node>	prog expr expr0 expr1 expr2 expr3 expr4
+
+%token	<val>	VAL
+%token	<var>	VAR
+%token	<numb>	SUP
+%%
+prog:
+	':' VAR expr
+	{
+		f := $2.node.dim[0];
+		$2.node = $3.copy();
+		$2.node.dim[0] = 1;
+		if(f)
+			yyerror(sys->sprint("redefinition of %s", $2.name));
+		else if(vflag)
+			sys->print("%s\t%s\n", $2.name, $2.node.text());
+	}
+|	':' VAR '#'
+	{
+		for(i:=1; i<Ndim; i++)
+			if(fund[i] == nil)
+				break;
+		if(i >= Ndim) {
+			yyerror("too many dimensions");
+			i = Ndim-1;
+		}
+		fund[i] = $2;
+
+		f := $2.node.dim[0];
+		$2.node = Node.mk(1.0);
+		$2.node.dim[0] = 1;
+		$2.node.dim[i] = 1;
+		if(f)
+			yyerror(sys->sprint("redefinition of %s", $2.name));
+		else if(vflag)
+			sys->print("%s\t#\n", $2.name);
+	}
+|	'?' expr
+	{
+		retnode1 = $2.copy();
+	}
+|	'?'
+	{
+		retnode1 = Node.mk(1.0);
+	}
+
+expr:
+	expr4
+|	expr '+' expr4
+	{
+		$$ = $1.add($3);
+	}
+|	expr '-' expr4
+	{
+		$$ = $1.sub($3);
+	}
+
+expr4:
+	expr3
+|	expr4 '*' expr3
+	{
+		$$ = $1.mul($3);
+	}
+|	expr4 '/' expr3
+	{
+		$$ = $1.div($3);
+	}
+
+expr3:
+	expr2
+|	expr3 expr2
+	{
+		$$ = $1.mul($2);
+	}
+
+expr2:
+	expr1
+|	expr2 SUP
+	{
+		$$ = $1.xpn($2);
+	}
+|	expr2 '^' expr1
+	{
+		for(i:=1; i<Ndim; i++)
+			if($3.dim[i]) {
+				yyerror("exponent has units");
+				$$ = $1;
+				break;
+			}
+		if(i >= Ndim) {
+			i = int $3.val;
+			if(real i != $3.val)
+				yyerror("exponent not integral");
+			$$ = $1.xpn(i);
+		}
+	}
+
+expr1:
+	expr0
+|	expr1 '|' expr0
+	{
+		$$ = $1.div($3);
+	}
+
+expr0:
+	VAR
+	{
+		if($1.node.dim[0] == 0) {
+			yyerror(sys->sprint("undefined %s", $1.name));
+			$$ = Node.mk(1.0);
+		} else
+			$$ = $1.node.copy();
+	}
+|	VAL
+	{
+		$$ = Node.mk($1);
+	}
+|	'(' expr ')'
+	{
+		$$ = $2;
+	}
+%%
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	math = load Math Math->PATH;
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	arg->setusage("units [-v] [file]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'v' => vflag = 1;
+		* => arg->usage();
+	}
+	args = arg->argv();
+	arg = nil;
+
+	file := "/lib/units";
+	if(args != nil)
+		file = hd args;
+	fi = bufio->open(file, Sys->OREAD);
+	if(fi == nil) {
+		sys->fprint(sys->fildes(2), "units: cannot open %s: %r\n", file);
+		raise "fail:open";
+	}
+	lex := ref YYLEX;
+
+	#
+	# read the 'units' file to
+	# develop a database
+	#
+	lineno = 0;
+	for(;;) {
+		lineno++;
+		if(readline())
+			break;
+		if(len line == 0 || line[0] == '/')
+			continue;
+		peekrune = ':';
+		yyparse(lex);
+	}
+
+	#
+	# read the console to
+	# print ratio of pairs
+	#
+	fi = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	lineno = 0;
+	for(;;) {
+		if(lineno & 1)
+			sys->print("you want: ");
+		else
+			sys->print("you have: ");
+		if(readline())
+			break;
+		peekrune = '?';
+		nerrors = 0;
+		yyparse(lex);
+		if(nerrors)
+			continue;
+		if(lineno & 1) {
+			isspcl: int;
+			(isspcl, retnode) = specialcase(retnode2, retnode1);
+			if(isspcl)
+				sys->print("\tis %s\n", retnode.text());
+			else {
+				retnode = retnode2.div(retnode1);
+				sys->print("\t* %s\n", retnode.text());
+				retnode = retnode1.div(retnode2);
+				sys->print("\t/ %s\n", retnode.text());
+			}
+		} else
+			retnode2 = retnode1.copy();
+		lineno++;
+	}
+	sys->print("\n");
+}
+
+YYLEX.lex(lex: self ref YYLEX): int
+{
+	c := peekrune;
+	peekrune = ' ';
+
+	while(c == ' ' || c == '\t'){
+		if(linep >= len line)
+			return 0;	# -1?
+		c = line[linep++];
+	}
+	case c {
+	'0' to '9' or '.' =>
+		digval = c;
+		(lex.lval.val, peekrune) = readreal(gdigit, lex);
+		return VAL;
+	'×' =>
+		return '*';
+	'÷' =>
+		return '/';
+	'¹' or
+	'ⁱ' =>
+		lex.lval.numb = 1;
+		return SUP;
+	'²' or
+	'⁲' =>
+		lex.lval.numb = 2;
+		return SUP;
+	'³' or
+	'⁳' =>
+		lex.lval.numb = 3;
+		return SUP;
+	* =>
+		if(ralpha(c)){
+			sym = "";
+			for(i:=0;; i++) {
+				sym[i] = c;
+				if(linep >= len line){
+					c = ' ';
+					break;
+				}
+				c = line[linep++];
+				if(!ralpha(c))
+					break;
+			}
+			peekrune = c;
+			lex.lval.var = lookup(0);
+			return VAR;
+		}
+	}
+	return c;
+}
+
+#
+# all characters that have some
+# meaning. rest are usable as names
+#
+ralpha(c: int): int
+{
+	case c {
+	0 or
+	'+'  or
+	'-'  or
+	'*'  or
+	'/'  or
+	'['  or
+	']'  or
+	'('  or
+	')'  or
+	'^'  or
+	':'  or
+	'?'  or
+	' '  or
+	'\t'  or
+	'.'  or
+	'|'  or
+	'#'  or
+	'¹'  or
+	'ⁱ'  or
+	'²'  or
+	'⁲'  or
+	'³'  or
+	'⁳'  or
+	'×'  or
+	'÷'  =>
+		return 0;
+	}
+	return 1;
+}
+
+gdigit(nil: ref YYLEX): int
+{
+	c := digval;
+	if(c) {
+		digval = 0;
+		return c;
+	}
+	if(linep >= len line)
+		return 0;
+	return line[linep++];
+}
+
+YYLEX.error(lex: self ref YYLEX, s: string)
+{
+	#
+	# hack to intercept message from yaccpar
+	#
+	if(s == "syntax error") {
+		lex.error(sys->sprint("syntax error, last name: %s", sym));
+		return;
+	}
+	sys->print("%d: %s\n\t%s\n", lineno, line, s);
+	nerrors++;
+	if(nerrors > 5) {
+		sys->print("too many errors\n");
+		raise "fail:errors";
+	}
+}
+
+yyerror(s: string)
+{
+	l := ref YYLEX;
+	l.error(s);
+}
+
+Node.mk(v: real): Node
+{
+	return (v, array[Ndim] of {* => 0});
+}
+
+Node.add(a: self Node, b: Node): Node
+{
+	c := Node.mk(fadd(a.val, b.val));
+	for(i:=0; i<Ndim; i++) {
+		d := a.dim[i];
+		c.dim[i] = d;
+		if(d != b.dim[i])
+			yyerror("add must be like units");
+	}
+	return c;
+}
+
+Node.sub(a: self Node, b: Node): Node
+{
+	c := Node.mk(fadd(a.val, -b.val));
+	for(i:=0; i<Ndim; i++) {
+		d := a.dim[i];
+		c.dim[i] = d;
+		if(d != b.dim[i])
+			yyerror("sub must be like units");
+	}
+	return c;
+}
+
+Node.mul(a: self Node, b: Node): Node
+{
+	c := Node.mk(fmul(a.val, b.val));
+	for(i:=0; i<Ndim; i++)
+		c.dim[i] = a.dim[i] + b.dim[i];
+	return c;
+}
+
+Node.div(a: self Node, b: Node): Node
+{
+	c := Node.mk(fdiv(a.val, b.val));
+	for(i:=0; i<Ndim; i++)
+		c.dim[i] = a.dim[i] - b.dim[i];
+	return c;
+}
+
+Node.xpn(a: self Node, b: int): Node
+{
+	c := Node.mk(1.0);
+	if(b < 0) {
+		b = -b;
+		for(i:=0; i<b; i++)
+			c = c.div(a);
+	} else
+		for(i:=0; i<b; i++)
+			c = c.mul(a);
+	return c;
+}
+
+Node.copy(a: self Node): Node
+{
+	c := Node.mk(a.val);
+	c.dim[0:] = a.dim;
+	return c;
+}
+
+specialcase(a, b: Node): (int, Node)
+{
+	c := Node.mk(0.0);
+	d1 := 0;
+	d2 := 0;
+	for(i:=1; i<Ndim; i++) {
+		d := a.dim[i];
+		if(d) {
+			if(d != 1 || d1)
+				return (0, c);
+			d1 = i;
+		}
+		d = b.dim[i];
+		if(d) {
+			if(d != 1 || d2)
+				return (0, c);
+			d2 = i;
+		}
+	}
+	if(d1 == 0 || d2 == 0)
+		return (0, c);
+
+	if(fund[d1].name == "°C" &&
+	   fund[d2].name == "°F" &&
+	   b.val == 1.0) {
+		c = b.copy();
+		c.val = a.val * 9. / 5. + 32.;
+		return (1, c);
+	}
+
+	if(fund[d1].name == "°F" &&
+	   fund[d2].name == "°C" &&
+	   b.val == 1.0) {
+		c = b.copy();
+		c.val = (a.val - 32.) * 5. / 9.;
+		return (1, c);
+	}
+	return (0, c);
+}
+
+printdim(d: int, n: int): string
+{
+	s := "";
+	if(n) {
+		v := fund[d];
+		if(v != nil)
+			s += " "+v.name;
+		else
+			s += sys->sprint(" [%d]", d);
+		case n {
+		1 =>
+			;
+		2 =>
+			s += "²";
+		3 =>
+			s += "³";
+		4 =>
+			s += "⁴";
+		* =>
+			s += sys->sprint("^%d", n);
+		}
+	}
+	return s;
+}
+
+Node.text(n: self Node): string
+{
+	str := sys->sprint("%.7g", n.val);
+	f := 0;
+	for(i:=1; i<len n.dim; i++) {
+		d := n.dim[i];
+		if(d > 0)
+			str += printdim(i, d);
+		else if(d < 0)
+			f = 1;
+	}
+
+	if(f) {
+		str += " /";
+		for(i=1; i<len n.dim; i++) {
+			d := n.dim[i];
+			if(d < 0)
+				str += printdim(i, -d);
+		}
+	}
+
+	return str;
+}
+
+readline(): int
+{
+	linep = 0;
+	line = "";
+	for(i:=0;; i++) {
+		c := fi.getc();
+		if(c < 0)
+			return 1;
+		if(c == '\n')
+			return 0;
+		line[i] = c;
+	}
+}
+
+lookup(f: int): ref Var
+{
+	h := 0;
+	for(i:=0; i < len sym; i++)
+		h = h*13 + sym[i];
+	if(h < 0)
+		h ^= int 16r80000000;
+	h %= len vars;
+
+	for(vl:=vars[h]; vl != nil; vl = tl vl)
+		if((hd vl).name == sym)
+			return hd vl;
+	if(f)
+		return nil;
+	v := ref Var(sym, Node.mk(0.0));
+	vars[h] = v :: vars[h];
+
+	p := 1.0;
+	for(;;) {
+		p = fmul(p, pname());
+		if(p == 0.0)
+			break;
+		w := lookup(1);
+		if(w != nil) {
+			v.node = w.node.copy();
+			v.node.val = fmul(v.node.val, p);
+			break;
+		}
+	}
+	return v;
+}
+
+prefix: array of Prefix = array[] of {
+	(1e-24,	"yocto"),
+	(1e-21,	"zepto"),
+	(1e-18,	"atto"),
+	(1e-15,	"femto"),
+	(1e-12,	"pico"),
+	(1e-9,	"nano"),
+	(1e-6,	"micro"),
+	(1e-6,	"μ"),
+	(1e-3,	"milli"),
+	(1e-2,	"centi"),
+	(1e-1,	"deci"),
+	(1e1,	"deka"),
+	(1e2,	"hecta"),
+	(1e2,	"hecto"),
+	(1e3,	"kilo"),
+	(1e6,	"mega"),
+	(1e6,	"meg"),
+	(1e9,	"giga"),
+	(1e12,	"tera"),
+	(1e15,	"peta"),
+	(1e18,	"exa"),
+	(1e21,	"zetta"),
+	(1e24,	"yotta")
+};
+
+pname(): real
+{
+	#
+	# rip off normal prefices
+	#
+Pref:
+	for(i:=0; i < len prefix; i++) {
+		p := prefix[i].pname;
+		for(j:=0; j < len p; j++)
+			if(j >= len sym || p[j] != sym[j])
+				continue Pref;
+		sym = sym[j:];
+		return prefix[i].val;
+	}
+
+	#
+	# rip off 's' suffixes
+	#
+	for(j:=0; j < len sym; j++)
+		;
+	j--;
+	# j>1 is special hack to disallow ms finding m
+	if(j > 1 && sym[j] == 's') {
+		sym = sym[0:j];
+		return 1.0;
+	}
+	return 0.0;
+}
+
+#
+# reads a floating-point number
+#
+
+readreal[T](f: ref fn(t: T): int, vp: T): (real, int)
+{
+	s := "";
+	c := f(vp);
+	while(c == ' ' || c == '\t')
+		c = f(vp);
+	if(c == '-' || c == '+'){
+		s[len s] = c;
+		c = f(vp);
+	}
+	start := len s;
+	while(c >= '0' && c <= '9'){
+		s[len s] = c;
+		c = f(vp);
+	}
+	if(c == '.'){
+		s[len s] = c;
+		c = f(vp);
+		while(c >= '0' && c <= '9'){
+			s[len s] = c;
+			c = f(vp);
+		}
+	}
+	if(len s > start && (c == 'e' || c == 'E')){
+		s[len s] = c;
+		c = f(vp);
+		if(c == '-' || c == '+'){
+			s[len s] = c;
+			c = f(vp);
+		}
+		while(c >= '0' && c <= '9'){
+			s[len s] = c;
+			c = f(vp);
+		}
+	}
+	return (real s, c);
+}
+
+#
+# careful floating point
+#
+
+fmul(a, b: real): real
+{
+	l: real;
+
+	if(a <= 0.0) {
+		if(a == 0.0)
+			return 0.0;
+		l = math->log(-a);
+	} else
+		l = math->log(a);
+
+	if(b <= 0.0) {
+		if(b == 0.0)
+			return 0.0;
+		l += math->log(-b);
+	} else
+		l += math->log(b);
+
+	if(l > Maxe) {
+		yyerror("overflow in multiply");
+		return 1.0;
+	}
+	if(l < -Maxe) {
+		yyerror("underflow in multiply");
+		return 0.0;
+	}
+	return a*b;
+}
+
+fdiv(a, b: real): real
+{
+	l: real;
+
+	if(a <= 0.0) {
+		if(a == 0.0)
+			return 0.0;
+		l = math->log(-a);
+	} else
+		l = math->log(a);
+
+	if(b <= 0.0) {
+		if(b == 0.0) {
+			yyerror("division by zero");
+			return 1.0;
+		}
+		l -= math->log(-b);
+	} else
+		l -= math->log(b);
+
+	if(l > Maxe) {
+		yyerror("overflow in divide");
+		return 1.0;
+	}
+	if(l < -Maxe) {
+		yyerror("underflow in divide");
+		return 0.0;
+	}
+	return a/b;
+}
+
+fadd(a, b: real): real
+{
+	return a + b;
+}
--- /dev/null
+++ b/appl/cmd/unmount.b
@@ -1,0 +1,44 @@
+implement Unmount;
+
+include "sys.m";
+include "draw.m";
+
+FD: import Sys;
+Context: import Draw;
+
+Unmount: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+sys: Sys;
+stderr: ref FD;
+
+usage()
+{
+	sys->fprint(stderr, "Usage: unmount [source] target\n");
+}
+
+init(nil: ref Context, argv: list of string)
+{
+	r: int;
+
+	sys = load Sys Sys->PATH;
+
+	stderr = sys->fildes(2);
+
+	argv = tl argv;
+
+	case len argv {
+	* =>
+		usage();
+		return;
+	1 =>
+		r = sys->unmount(nil, hd argv);
+	2 =>
+		r = sys->unmount(hd argv, hd tl argv);
+	};
+
+	if(r < 0)
+		sys->fprint(stderr, "unmount: %r\n");
+}
--- /dev/null
+++ b/appl/cmd/usb/mkfile
@@ -1,0 +1,11 @@
+<../../../mkconfig
+
+TARG=\
+	usbd.dis\
+
+SYSMODULES=\
+	usb.m\
+
+DISBIN=$ROOT/dis/usb
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/cmd/usb/usbd.b
@@ -1,0 +1,835 @@
+implement Usbd;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "string.m";
+	str: String;
+include "lock.m";
+	lock: Lock;
+	Semaphore: import lock;
+include "arg.m";
+	arg: Arg;
+
+include "usb.m";
+	usb: Usb;
+	Device, Configuration, Endpt: import Usb;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+Detached, Attached, Enabled, Assigned, Configured: con (iota);
+
+Usbd: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+Hub: adt {
+	nport, pwrmode, compound, pwrms, maxcurrent, removable, pwrctl: int;
+	ports: cyclic ref DDevice;
+};
+	
+DDevice: adt {
+	port: int;
+	pids: list of int;
+	parent: cyclic ref DDevice;
+	next: cyclic ref DDevice;
+	cfd, setupfd, rawfd: ref Sys->FD;
+	id: int;
+	ls: int;
+	state: int;
+	ep: array of ref Endpt;
+	config: array of ref Usb->Configuration;
+	hub: Hub;
+	mod: UsbDriver;
+	d: ref Device;
+};
+
+Line: adt {
+	level: int;
+	command: string;
+	value: int;
+	svalue: string;
+};
+
+ENUMERATE_POLL_INTERVAL: con 1000;
+FAILED_ENUMERATE_RETRY_INTERVAL: con 10000;
+
+verbose: int;
+debug: int;
+stderr: ref Sys->FD;
+
+usbportfd: ref Sys->FD;
+usbctlfd: ref Sys->FD;
+usbctl0: ref Sys->FD;
+usbsetup0: ref Sys->FD;
+
+usbbase: string;
+
+configsema, setupsema, treesema: ref Semaphore;
+
+
+# UHCI style status which is returned by the driver.
+UHCIstatus_Suspend: con 1 << 12;
+UHCIstatus_PortReset: con 1 << 9;
+UHCIstatus_SlowDevice: con 1 << 8;
+UHCIstatus_ResumeDetect: con 1 << 6;
+UHCIstatus_PortEnableChange: con 1 << 3;   
+UHCIstatus_PortEnable: con 1 << 2;
+UHCIstatus_ConnectStatusChange: con 1 << 1;	
+UHCIstatus_DevicePresent: con 1 << 0;
+
+obt()
+{
+#	sys->fprint(stderr, "%d waiting\n", sys->pctl(0, nil));
+	setupsema.obtain();
+#	sys->fprint(stderr, "%d got\n", sys->pctl(0, nil));
+}
+
+rel()
+{
+#	sys->fprint(stderr, "%d releasing\n", sys->pctl(0, nil));
+	setupsema.release();
+}
+
+hubid(hub: ref DDevice): int
+{
+	if (hub == nil)
+		return 0;
+	return hub.id;
+}
+
+hubfeature(d: ref DDevice, p: int, feature: int, on: int): int
+{
+	rtyp: int;
+	if (p == 0)
+		rtyp = Usb->Rclass;
+	else
+		rtyp = Usb->Rclass | Usb->Rother;
+	obt();
+	rv := usb->setclear_feature(d.setupfd, rtyp, feature, p, on);
+	rel();
+	return rv;
+}
+
+portpower(hub: ref DDevice, port: int, on: int)
+{
+	if (verbose)
+		sys->fprint(stderr, "portpower %d/%d %d\n", hubid(hub), port, on);
+	if (hub == nil)
+		return;
+	if (port)
+		hubfeature(hub, port, Usb->PORT_POWER, on);
+}
+
+countrootports(): int
+{
+	sys->seek(usbportfd, big 0, Sys->SEEKSTART);
+	buf := array [256] of byte;
+	n := sys->read(usbportfd, buf, len buf);
+	if (n <= 0) {
+		sys->fprint(stderr, "usbd: countrootports: error reading root port status\n");
+		exit;
+	}
+	(nv, nil) := sys->tokenize(string buf[0: n], "\n");
+	if (nv < 1) {
+		sys->fprint(stderr, "usbd: countrootports: strange root port status\n");
+		exit;
+	}
+	return nv;
+}
+
+portstatus(hub: ref DDevice, port: int): int
+{
+	rv: int;
+#	setupsema.obtain();
+	obt();
+	if (hub == nil) {
+		sys->seek(usbportfd, big 0, Sys->SEEKSTART);
+		buf := array [256] of byte;
+		n := sys->read(usbportfd, buf, len buf);
+		if (n < 1) {
+			sys->fprint(stderr, "usbd: portstatus: read error\n");
+			rel();
+			return 0;
+		}
+		(nil, l) := sys->tokenize(string buf[0: n], "\n");
+		for(; l != nil; l = tl l){
+			(nv, f) := sys->tokenize(hd l, " ");
+			if(nv < 2){
+				sys->fprint(stderr, "usbd: portstatus: odd status line\n");
+				rel();
+				return 0;
+			}
+			if(int hd f == port){
+				(rv, nil) = usb->strtol(hd tl f, 16);
+				# the status change bits are not used so mask them off
+				rv &= 16rffff;
+				break;
+			}
+		}
+		if (l == nil) {
+			sys->fprint(stderr, "usbd: portstatus: no status for port %d\n", port);
+			rel();
+			return 0;
+		}
+	}
+	else
+		rv = usb->get_status(hub.setupfd, port);
+#	setupsema.release();
+	rel();
+	if (rv < 0)
+		return 0;
+	return rv;
+}
+
+portenable(hub: ref DDevice, port: int, enable: int)
+{
+	if (verbose)
+		sys->fprint(stderr, "portenable %d/%d %d\n", hubid(hub), port, enable);
+	if (hub == nil) {
+		if (enable)
+			sys->fprint(usbctlfd, "enable %d", port);
+		else
+			sys->fprint(usbctlfd, "disable %d", port);
+		return;
+	}
+	if (port)
+		hubfeature(hub, port, Usb->PORT_ENABLE, enable);
+}
+
+portreset(hub: ref DDevice, port: int)
+{
+	if (verbose)
+		sys->fprint(stderr, "portreset %d/%d\n", hubid(hub), port);
+	if (hub == nil) {
+		if(0)sys->fprint(usbctlfd, "reset %d", port);
+		for (i := 0; i < 4; ++i) {
+	  		sys->sleep(20);			# min 10 milli second reset recovery.
+	  		s := portstatus(hub, port);
+	  		if ((s & UHCIstatus_PortReset) == 0)		# only leave when reset is finished.
+				break;
+		}
+		return;
+	}
+	if (port)
+		hubfeature(hub, port, Usb->PORT_RESET, 1);
+	return;
+}
+
+devspeed(d: ref DDevice)
+{
+	sys->fprint(d.cfd, "speed %d", !d.ls);
+	if (debug) {
+		s: string;
+		if (d.ls)
+			s = "low";
+		else
+			s = "high";
+		sys->fprint(stderr, "%d: set speed %s\n", d.id, s);
+	}
+}
+
+devmaxpkt0(d: ref DDevice, size: int)
+{
+	sys->fprint(d.cfd, "maxpkt 0 %d", size);
+	if (debug)
+		sys->fprint(stderr, "%d: set maxpkt0 %d\n", d.id, size);
+}
+
+closedev(d: ref DDevice)
+{
+	d.cfd = usbctl0;
+	d.rawfd = nil;
+	d.setupfd = usbsetup0;
+}
+
+openusb(f: string, mode: int): ref Sys->FD
+{
+	fd := sys->open(usbbase + f, mode);
+	if (fd == nil) {
+		sys->fprint(stderr, "usbd: can't open %s: %r\n", usbbase + f);
+		raise "fail:open";
+	}
+	return fd;
+}
+
+opendevf(id: int, f: string, mode: int): ref Sys->FD
+{
+	fd := sys->open(usbbase + string id + "/" + f, mode);
+	if (fd == nil) {
+		sys->fprint(stderr, "usbd: can't open %s: %r\n", usbbase + string id + "/" + f);
+		exit;
+	}
+	return fd;
+}
+
+kill(pid: int): int
+{
+	if (debug)
+		sys->print("killing %d\n", pid);
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if (fd == nil) {
+		sys->print("kill: open failed\n");
+		return -1;
+	}
+	if (sys->write(fd, array of byte "kill", 4) != 4) {
+		sys->print("kill: write failed\n");
+		return -1;
+	}
+	return 0;
+}
+
+rdetach(d: ref DDevice)
+{
+	if (d.mod != nil) {
+		d.mod->shutdown();
+		d.mod = nil;
+	}
+	while (d.pids != nil) {
+		if (verbose)
+			sys->fprint(stderr, "kill %d\n", hd d.pids);
+		kill(hd d.pids);
+		d.pids = tl d.pids;
+	}
+	if (d.parent != nil) {
+		last, hp: ref DDevice;
+		last = nil;
+		hp = d.parent.hub.ports;
+		while (hp != nil && hp != d)
+			hp = hp.next;
+		if (last != nil)
+			last.next = d.next;
+		else
+			d.parent.hub.ports = d.next;
+	}
+	if (d.hub.ports != nil) {
+		for (c := d.hub.ports; c != nil; c = c.next) {
+			c.parent = nil;
+			rdetach(c);
+		}
+	}
+	d.state = Detached;
+	if (sys->fprint(d.cfd, "detach") < 0)
+		sys->fprint(stderr, "detach failed\n");
+	d.cfd = nil;
+	d.rawfd = nil;
+	d.setupfd = nil;
+}
+
+detach(d: ref DDevice)
+{
+	configsema.obtain();
+	treesema.obtain();
+	obt();
+#	setupsema.obtain();
+
+	if (verbose)
+		sys->fprint(stderr, "detach %d\n", d.id);
+	rdetach(d);
+	if (verbose)
+		sys->fprint(stderr, "detach %d done\n", d.id);
+#	setupsema.release();
+	rel();
+	treesema.release();
+	configsema.release();
+}
+
+readnum(fd: ref Sys->FD): int
+{
+	buf := array [16] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return -1;
+	(rv , nil) := usb->strtol(string buf[0: n], 0);
+	return rv;
+}
+
+setaddress(d: ref DDevice): int
+{
+	if (d.state == Assigned)
+		return d.id;
+	closedev(d);
+	d.id = 0;
+	d.cfd = openusb("new", Sys->ORDWR);
+	id := readnum(d.cfd);
+	if (id <= 0) {
+		if (debug)
+			sys->fprint(stderr, "usbd: usb/new ID: %r\n");
+		d.cfd = nil;
+		return -1;
+	}
+#	setupsema.obtain();
+	obt();
+	if (usb->set_address(d.setupfd, id) < 0) {
+#		setupsema.release();
+		rel();
+		return -1;
+	}
+#	setupsema.release();
+	rel();
+	d.id = id;
+	d.state = Assigned;
+	return id;
+}
+
+#optstring(d: ref DDevice, langids: list of int, desc: string, index: int)
+#{
+#	if (index) {
+#		buf := array [256] of byte;
+#		while (langids != nil) {
+#			nr := usb->get_descriptor(d.setupfd, Usb->Rstandard, Usb->STRING, index, hd langids, buf);
+#			if (nr > 2) {
+#				sys->fprint(stderr, "%s: ", desc);
+#				usbdump->desc(d, -1, buf[0: nr]);
+#			}
+#			langids = tl langids;
+#		}
+#	}
+#}
+
+langid(d: ref DDevice): (list of int)
+{
+	l: list of int;
+	buf := array [256] of byte;
+	nr := usb->get_standard_descriptor(d.setupfd, Usb->STRING, 0, buf);
+	if (nr < 4)
+		return nil;
+	if (nr & 1)
+		nr--;
+	l = nil;
+	for (i := nr - 2; i >= 2; i -= 2)
+		l = usb->get2(buf[i:]) :: l;
+	return l;
+}
+
+describedevice(d: ref DDevice): int
+{
+	obt();
+	devmaxpkt0(d, 64);				# guess 64 byte max packet to avoid overrun on read
+	for (x := 0; x < 3; x++) {			# retry 3 times
+		d.d = usb->get_parsed_device_descriptor(d.setupfd);
+		if (d.d != nil)
+			break;
+		sys->sleep(200);			# tolerate out of spec. devices
+	}
+
+	if (d.d == nil) {
+		rel();
+		return -1;
+	}
+
+	if (d.d.maxpkt0 != 64) {
+		devmaxpkt0(d, d.d.maxpkt0);
+		d.d = usb->get_parsed_device_descriptor(d.setupfd);
+		if (d.d == nil) {
+			rel();
+			return -1;
+		}
+	}
+
+	rel();
+
+	if (verbose) {
+		sys->fprint(stderr, "usb %x.%x", d.d.usbmajor, d.d.usbminor);
+		sys->fprint(stderr, " class %d subclass %d proto %d [%s] max0 %d",
+			d.d.class, d.d.subclass, d.d.proto,
+			usb->sclass(d.d.class, d.d.subclass, d.d.proto), d.d.maxpkt0);
+		sys->fprint(stderr, " vendor 0x%.4x product 0x%.4x rel %x.%x",
+			d.d.vid, d.d.did, d.d.relmajor, d.d.relminor);
+		sys->fprint(stderr, " nconf %d", d.d.nconf);
+		sys->fprint(stderr, "\n");
+		obt();
+		l := langid(d);
+		if (l != nil) {
+			l2 := l;
+			sys->fprint(stderr, "langids [");
+			while (l2 != nil) {
+				sys->fprint(stderr, " %d", hd l2);
+				l2 = tl l2;
+			}
+			sys->fprint(stderr, "]\n");
+		}
+#		optstring(d, l, "manufacturer", int buf[14]);
+#		optstring(d, l, "product", int buf[15]);
+#		optstring(d, l, "serial number", int buf[16]);
+		rel();
+	}
+	return 0;
+}
+
+describehub(d: ref DDevice): int
+{
+	b := array [256] of byte;
+#	setupsema.obtain();
+	obt();
+	nr := usb->get_class_descriptor(d.setupfd, 0, 0, b);
+	if (nr < Usb->DHUBLEN) {
+#		setupsema.release();
+		rel();
+		sys->fprint(stderr, "usbd: error reading hub descriptor: got %d of %d\n", nr, Usb->DHUBLEN);
+		return -1;
+	}
+#	setupsema.release();
+	rel();
+	if (verbose)
+		sys->fprint(stderr, "nport %d charac 0x%.4ux pwr %dms current %dmA remov 0x%.2ux pwrctl 0x%.2ux",
+			int b[2], usb->get2(b[3:]), int b[5] * 2, int b[6] * 2, int b[7], int b[8]);
+	d.hub.nport = int b[2];
+	d.hub.pwrms = int b[5] * 2;
+	d.hub.maxcurrent = int b[6] * 2;
+	char := usb->get2(b[3:]);
+	d.hub.pwrmode = char & 3;
+	d.hub.compound = (char & 4) != 0;
+	d.hub.removable = int b[7];
+	d.hub.pwrctl = int b[8];
+	return 0;
+}
+
+loadconfig(d: ref DDevice, n: int): int
+{
+	obt();
+	d.config[n] = usb->get_parsed_configuration_descriptor(d.setupfd, n);
+	if (d.config[n] == nil) {
+		rel();
+		sys->fprint(stderr, "usbd: error reading configuration descriptor\n");
+		return -1;
+	}
+	rel();
+	if (verbose)
+		usb->dump_configuration(stderr, d.config[n]);
+	return 0;
+}
+
+#setdevclass(d: ref DDevice, n: int)
+#{
+#	dd := d.config[n];
+#	if (dd != nil)
+#		sys->fprint(d.cfd, "class %d %d %d %d %d", d.d.nconf, n, dd.class, dd.subclass, dd.proto);
+#}
+
+setconfig(d: ref DDevice, n: int): int
+{
+	obt();
+	rv := usb->set_configuration(d.setupfd, n);
+	rel();
+	if (rv < 0)
+		return -1;
+	d.state = Configured;
+	return 0;
+}
+
+configure(hub: ref DDevice, port: int): ref DDevice
+{
+	configsema.obtain();
+	portreset(hub, port);
+	sys->sleep(300);				# long sleep necessary for strange hardware....
+#	sys->sleep(20);
+	s := portstatus(hub, port);
+	s = portstatus(hub, port);
+
+	if (debug)
+		sys->fprint(stderr, "port %d status 0x%ux\n", port, s);
+
+	if ((s & UHCIstatus_DevicePresent) == 0) {
+		configsema.release();
+		return nil;
+	}
+
+	if ((s & UHCIstatus_PortEnable) == 0) {
+		if (debug)
+			sys->fprint(stderr, "hack: re-enabling port %d\n", port);
+		portenable(hub, port, 1);
+		s = portstatus(hub, port);
+		if (debug)
+			sys->fprint(stderr, "port %d status now 0x%.ux\n", port, s);
+	}
+
+	d := ref DDevice;
+	d.port = port;
+	d.cfd = usbctl0;
+	d.setupfd = usbsetup0;
+	d.id = 0;
+	if (hub == nil)
+		d.ls = (s & UHCIstatus_SlowDevice) != 0;
+	else
+		d.ls = (s & (1 << 9)) != 0;
+	d.state = Enabled;
+	devspeed(d);
+	if (describedevice(d) < 0) {
+		portenable(hub, port, 0);
+		configsema.release();
+		return nil;
+	}
+	if (setaddress(d) < 0) {
+		portenable(hub, port, 0);
+		configsema.release();
+		return nil;
+	}
+	d.setupfd = opendevf(d.id, "setup", Sys->ORDWR);
+	d.cfd = opendevf(d.id, "ctl", Sys->ORDWR);
+	devspeed(d);
+	devmaxpkt0(d, d.d.maxpkt0);
+	d.config = array [d.d.nconf] of ref Configuration;
+	for (i := 0; i < d.d.nconf; i++) {
+		loadconfig(d, i);
+#		setdevclass(d, i);
+	}
+	if (hub != nil) {
+		treesema.obtain();
+		d.parent = hub;
+		d.next = hub.hub.ports;
+		hub.hub.ports = d;
+		treesema.release();
+	}
+	configsema.release();
+	return d;
+}
+
+enumerate(hub: ref DDevice, port: int)
+{
+	if (hub != nil)
+		hub.pids = sys->pctl(0, nil) :: hub.pids;
+	reenumerate := 0;
+	for (;;) {
+		if (verbose)
+			sys->fprint(stderr, "enumerate: starting\n");
+		if ((portstatus(hub, port) & UHCIstatus_DevicePresent) == 0) {
+			if (verbose)
+				sys->fprint(stderr, "%d: port %d empty\n", hubid(hub), port);
+			do {
+				sys->sleep(ENUMERATE_POLL_INTERVAL);
+			} while ((portstatus(hub, port) & UHCIstatus_DevicePresent) == 0);
+		}
+		if (verbose)
+			sys->fprint(stderr, "%d: port %d attached\n", hubid(hub), port);
+		# Δt3 (TATTDB) guarantee 100ms after attach detected
+		sys->sleep(200);
+		d := configure(hub, port);
+		if (d == nil) {
+			if (verbose)
+				sys->fprint(stderr, "%d: can't configure port %d\n", hubid(hub), port);
+		}
+		else if (d.d.class == Usb->CL_HUB) {
+			i: int;
+			if (setconfig(d, 1) < 0) {
+				if (verbose)
+					sys->fprint(stderr, "%d: can't set configuration for hub on port %d\n", hubid(hub), port);
+				detach(d);
+				d = nil;
+			}
+			else if (describehub(d) < 0) {
+				if (verbose)
+					sys->fprint(stderr, "%d: failed to describe hub on port %d\n", hubid(hub), port);
+				detach(d);
+				d = nil;
+			}
+			else {
+				for (i = 1; i <= d.hub.nport; i++)
+					portpower(d, i, 1);
+				sys->sleep(d.hub.pwrms);
+				for (i = 1; i <= d.hub.nport; i++)
+					spawn enumerate(d, i);
+			}
+		}
+		else if (d.d.nconf >= 1 && (path := searchdriverdatabase(d.d, d.config[0])) != nil) {
+			d.mod = load UsbDriver path;
+			if (d.mod == nil)
+				sys->fprint(stderr, "usbd: failed to load %s\n", path);
+			else {
+				rv := d.mod->init(usb, d.setupfd, d.cfd, d.d, d.config, usbbase + string d.id + "/");
+				if (rv == -11) {
+					sys->fprint(stderr, "usbd: %s: reenumerate\n", path);
+					d.mod = nil;
+					reenumerate = 1;
+				}	
+				else if (rv < 0) {
+					sys->fprint(stderr, "usbd: %s:init failed\n", path);
+					d.mod = nil;
+				}
+				else if (verbose)
+					sys->fprint(stderr, "%s running\n", path);
+			}
+		}
+		else if (setconfig(d, 1) < 0) {
+			if (verbose)
+				sys->fprint(stderr, "%d: can't set configuration for port %d\n", hubid(hub), port);
+			detach(d);
+			d = nil;
+		}
+		if (!reenumerate) {
+			if (d != nil) {
+				# wait for it to be unplugged
+				while (portstatus(hub, port) & UHCIstatus_DevicePresent)
+					sys->sleep(ENUMERATE_POLL_INTERVAL);
+			}
+			else {
+				# wait a bit and prod it again
+				if (portstatus(hub, port) & UHCIstatus_DevicePresent)
+					sys->sleep(FAILED_ENUMERATE_RETRY_INTERVAL);
+			}
+		}
+		if (d != nil) {
+			detach(d);
+			d = nil;
+		}
+		reenumerate = 0;
+	}
+}
+
+lines: array of Line;
+
+searchdriverdatabase(d: ref Device, conf: ref Configuration): string
+{
+	backtracking := 0;
+	level := 0;
+	for (i := 0; i < len lines; i++) {
+		if (verbose > 1)
+			sys->fprint(stderr, "search line %d: lvl %d cmd %s val %d (back %d lvl %d)\n",
+				i, lines[i].level, lines[i].command, lines[i].value, backtracking, level);
+		if (backtracking) {
+			if (lines[i].level > level)
+				continue;
+			backtracking = 0;
+		}
+		if (lines[i].level != level) {
+			level = 0;
+			backtracking = 1;
+		}
+		case lines[i].command {
+		"class" =>
+			if (d.class != 0) {
+				if (lines[i].value != d.class)
+					backtracking = 1;
+			}
+			else if (lines[i].value != (hd conf.iface[0].altiface).class)
+				backtracking = 1;
+		"subclass" =>
+			if (d.class != 0) {
+				if (lines[i].value != d.subclass)
+					backtracking = 1;
+			}
+			else if (lines[i].value != (hd conf.iface[0].altiface).subclass)
+				backtracking = 1;
+		"proto" =>
+			if (d.class != 0) {
+				if (lines[i].value != d.proto)
+					backtracking = 1;
+			}
+			else if (lines[i].value != (hd conf.iface[0].altiface).proto)
+				backtracking = 1;
+		"vendor" =>
+			if (lines[i].value != d.vid)
+				backtracking  =1;
+		"product" =>
+			if (lines[i].value != d.did)
+				backtracking  =1;
+		"load" =>
+			return lines[i].svalue;
+		* =>
+			continue;
+		}
+		if (!backtracking)
+			level++;
+	}
+	return nil;
+}
+
+loaddriverdatabase()
+{
+	newlines: array of Line;
+
+	if (bufio == nil)
+		bufio = load Bufio Bufio->PATH;
+
+	iob := bufio->open(Usb->DATABASEPATH, Sys->OREAD);
+	if (iob == nil) {
+		sys->fprint(stderr, "usbd: couldn't open %s: %r\n", Usb->DATABASEPATH);
+		return;
+	}
+	lines = array[100] of Line;
+	lc := 0;
+	while ((line := iob.gets('\n')) != nil) {
+		if (line[0] == '#')
+			continue;
+		level := 0;
+		while (line[0] == '\t') {
+			level++;
+			line = line[1:];
+		}
+		(n, l) := sys->tokenize(line[0: len line - 1], "\t ");
+		if (n != 2)
+			continue;
+		if (lc >= len lines) {
+			newlines = array [len lines * 2] of Line;
+			newlines[0:] = lines[0: len lines];
+			lines = newlines;
+		}
+		lines[lc].level = level;
+		lines[lc].command = hd l;
+		case hd l {
+		"class" or "subclass" or "proto" or "vendor" or "product" =>
+			(lines[lc].value, nil) = usb->strtol(hd tl l, 0);
+		"load" =>
+			lines[lc].svalue = hd tl l;
+		* =>
+			continue;
+		}
+		lc++;
+	}
+	if (verbose)
+		sys->fprint(stderr, "usbd: loaded %d lines\n", lc);
+	newlines = array [lc] of Line;
+	newlines[0:] = lines[0 : lc];
+	lines = newlines;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	usbbase = "/dev/usbh/";
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+
+	lock = load Lock Lock->PATH;
+	lock->init();
+
+	usb = load Usb Usb->PATH;
+	usb->init();
+
+	arg = load Arg Arg->PATH;
+
+	stderr = sys->fildes(2);
+
+	verbose = 0;
+	debug = 0;
+
+	arg->init(args);
+	arg->setusage("usbd [-dv] [-i interface]");
+	while ((c := arg->opt()) != 0)
+		case c {
+		'v' => verbose = 1;
+		'd' => debug = 1;
+		'i' => usbbase = arg->earg() + "/";
+		* => arg->usage();
+		}
+	args = arg->argv();
+
+	usbportfd = openusb("port", Sys->OREAD);
+	usbctlfd = sys->open(usbbase + "ctl", Sys->OWRITE);
+	if(usbctlfd == nil)
+		usbctlfd = openusb("port", Sys->OWRITE);
+	usbctl0 = opendevf(0, "ctl", Sys->ORDWR);
+	usbsetup0 = opendevf(0, "setup", Sys->ORDWR);
+	setupsema = Semaphore.new();
+	configsema = Semaphore.new();
+	treesema = Semaphore.new();
+	loaddriverdatabase();
+	ports := countrootports();
+	if (verbose)
+		sys->print("%d root ports found\n", ports);
+	for (p := 2; p <= ports; p++)
+		spawn enumerate(nil, p);
+	if (p >= 1)
+		enumerate(nil, 1);
+}
--- /dev/null
+++ b/appl/cmd/uudecode.b
@@ -1,0 +1,132 @@
+implement Uudecode;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "string.m";
+	str : String;
+include "bufio.m";
+	bufio : Bufio;
+	Iobuf : import bufio;
+
+Uudecode : module
+{
+	init : fn(nil : ref Draw->Context, argv : list of string);
+};
+
+fatal(s : string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+	exit;
+}
+
+usage()
+{
+	fatal("usage: uudecode [ -p ] [ encodedfile... ]");
+}
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	fd : ref Sys->FD;
+
+	tostdout := 0;
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	bufio = load Bufio Bufio->PATH;
+	argv = tl argv;
+	if (argv != nil && hd argv == "-p") {
+		tostdout = 1;
+		argv = tl argv;
+	}
+	if (argv != nil) {
+		for (; argv != nil; argv = tl argv) {
+			fd = sys->open(hd argv, Sys->OREAD);
+			if (fd == nil)
+				fatal(sys->sprint("cannot open %s", hd argv));
+			decode(fd, tostdout);
+		}
+	}
+	else
+		decode(sys->fildes(0), tostdout);
+}
+
+code(c : byte) : int
+{
+	return (int c - ' ')&16r3f;
+}
+
+LEN : con 45;
+			
+decode(ifd : ref Sys->FD, tostdout : int)
+{
+	mode : int;
+	ofile : string;
+
+	bio := bufio->fopen(ifd, Bufio->OREAD);
+	if (bio == nil)
+		fatal("cannot open input for buffered io: %r");
+	while ((s := bio.gets('\n')) != nil) {
+		if (len s >= 6 && s[0:6] == "begin ") {
+			(n, l) := sys->tokenize(s, " \n");
+			if (n < 3)
+				fatal("bad begin line");
+			(mode, nil) = str->toint(hd tl l, 8);
+			ofile = hd tl tl l;
+			break;
+		}
+	}
+	if (ofile == nil)
+		fatal("no begin line");
+	if (tostdout)
+		ofd := sys->fildes(1);
+	else {
+		if (ofile[0] == '~')	# ~user/file
+			ofile = "/usr/" + ofile[1:];
+		ofd = sys->create(ofile, Sys->OWRITE, 8r666);
+		if (ofd == nil)
+			fatal(sys->sprint("cannot create %s: %r", ofile));
+	}
+	ob := array[LEN] of byte;
+	while ((s = bio.gets('\n')) != nil) {
+		b := array of byte s;
+		n := code(b[0]);
+		if (n == 0 && (len b != 2 || b[1] != byte '\n'))
+			fatal("bad 0 count line");
+		if (n <= 0)
+			break;
+		if (n > LEN)
+			fatal("too many bytes on line");
+		e := 0; f := 0;
+		if (n%3 == 1) {
+			e = 2; f = 4;
+		}
+		else if (n%3 == 2) {
+			e = 3; f = 4;
+		}
+		if (len b < 4*(n/3)+e+2 || len b > 4*(n/3)+f+2)
+			fatal("bad uuencode count");
+		b = b[1:];
+		i := 0;
+		nl := n;
+		for (j := 0; nl > 0; j += 4) {
+			if (nl >= 1)
+				ob[i++] = byte (code(b[j+0])<<2 | code(b[j+1])>>4);
+			if (nl >= 2)
+				ob[i++] = byte (code(b[j+1])<<4 | code(b[j+2])>>2);
+			if (nl >= 3)
+				ob[i++] = byte (code(b[j+2])<<6 | code(b[j+3])>>0);
+			nl -= 3;
+		}
+		if (sys->write(ofd, ob, i) != i)
+			fatal("bad write to output: %r");	
+	}
+	s = bio.gets('\n');
+	if (s == nil || len s < 4 || s[0:4] != "end\n")
+		fatal("missing end line");
+	if (!tostdout) {
+		d := sys->nulldir;
+		d.mode = mode;
+		if (sys->fwstat(ofd, d) < 0)
+			fatal(sys->sprint("cannot wstat %s: %r", ofile));
+	}
+}
--- /dev/null
+++ b/appl/cmd/uuencode.b
@@ -1,0 +1,101 @@
+implement Uuencode;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+
+Uuencode : module
+{
+	init : fn(nil : ref Draw->Context, argv : list of string);
+};
+
+fatal(s : string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+	exit;
+}
+
+usage()
+{
+	fatal("usage: uuencode [ sourcefile ] remotefile");
+}
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	fd : ref Sys->FD;
+	mode : int;
+
+	sys = load Sys Sys->PATH;
+	argv = tl argv;
+	if (argv == nil)
+		usage();
+	if (tl argv != nil) {
+		fd = sys->open(hd argv, Sys->OREAD);
+		if (fd == nil)
+			fatal(sys->sprint("cannot open %s", hd argv));
+		(ok, d) := sys->fstat(fd);
+		if (ok < 0)
+			fatal(sys->sprint("cannot stat %s: %r", hd argv));
+		if (d.mode & Sys->DMDIR)
+			fatal("cannot uuencode a directory");
+		mode = d.mode;
+		argv = tl argv;
+	}
+	else {
+		fd = sys->fildes(0);
+		mode = 8r666;
+	}
+	if (tl argv != nil)
+		usage();
+	sys->print("begin %o %s\n", mode, hd argv);
+	encode(fd);
+	sys->print("end\n");
+}
+
+LEN : con 45;
+
+code(c : int) : byte
+{
+	return byte ((c&16r3f) + ' ');
+}
+
+encode(ifd : ref Sys->FD)
+{
+	c, d, e : int;
+
+	ofd := sys->fildes(1);
+	ib := array[LEN] of byte;
+	ob := array[4*LEN/3 + 2] of byte;
+	for (;;) {
+		n := sys->read(ifd, ib, LEN);
+		if (n < 0)
+			fatal("cannot read input file: %r");
+		if (n == 0)
+			break;
+		i := 0;
+		ob[i++] = code(n);
+		for (j := 0; j < n; j += 3) {
+			c = int ib[j];
+			ob[i++] = code((0<<6)&16r00 | (c>>2)&16r3f);
+			if (j+1 < n)
+				d = int ib[j+1];
+			else
+				d = 0;
+			ob[i++] = code((c<<4)&16r30 | (d>>4)&16r0f);
+			if (j+2 < n)
+				e = int ib[j+2];
+			else
+				e = 0;
+			ob[i++] = code((d<<2)&16r3c | (e>>6)&16r03);
+			ob[i++] = code((e<<0)&16r3f | (0>>8)&16r00);
+		}
+		ob[i++] = byte '\n';
+		if (sys->write(ofd, ob, i) != i)
+			fatal("bad write to output: %r");
+	}
+	ob[0] = code(0);
+	ob[1] = byte '\n';
+	if (sys->write(ofd, ob, 2) != 2)
+		fatal("bad write to output: %r");
+}
+
--- /dev/null
+++ b/appl/cmd/vacfs.b
@@ -1,0 +1,467 @@
+implement Vacfs;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "dial.m";
+	dial: Dial;
+include "string.m";
+	str: String;
+include "daytime.m";
+	dt: Daytime;
+include "venti.m";
+	venti: Venti;
+	Root, Entry, Score, Session: import venti;
+	Roottype, Dirtype, Pointertype0, Datatype: import venti;
+include "vac.m";
+	vac: Vac;
+	Direntry, Vacdir, Vacfile: import vac;
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Fid, Navigator, Navop, Enotfound: import styxservers;
+
+Vacfs: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+addr := "$venti";
+dflag: int;
+pflag: int;
+session: ref Session;
+
+ss: ref Styxserver;
+
+Elem: adt {
+	qid:	int;
+	de: 	ref Direntry;
+	size:	big;
+	pick {
+	File =>	vf: 	ref Vacfile;
+	Dir =>	vd:	ref Vacdir;
+		pqid:	int;
+		offset:	int;
+		nprev:	int;
+		prev:	array of ref Sys->Dir;
+	}
+
+	new:	fn(nqid: int, vd: ref Vacdir, de: ref Direntry, pqid: int): ref Elem;
+	stat:	fn(e: self ref Elem): ref Sys->Dir;
+};
+
+Qdir: adt {
+	qid:	int;
+	cqids:	list of (string, int); # name, qid
+};
+
+elems := array[512] of list of ref Elem;
+qids := array[512] of list of ref Qdir;
+lastqid := 0;
+qidscores: list of (string, int);
+
+
+childget(qid: int, name: string): ref Elem
+{
+	for(l := qids[qid % len qids]; l != nil; l = tl l) {
+		if((hd l).qid != qid)
+			continue;
+		for(m := (hd l).cqids; m != nil; m = tl m) {
+			(cname, cq) := hd m;
+			if(name == cname)
+				return get(cq);
+		}
+	}
+	return nil;
+}
+
+childput(qid: int, name: string): int
+{
+	qd: ref Qdir;
+	for(l := qids[qid % len qids]; l != nil; l = tl l)
+		if((hd l).qid == qid) {
+			qd = hd l;
+			break;
+		}
+	if(qd == nil) {
+		qd = ref Qdir(qid, nil);
+		qids[qid % len qids] = qd::nil;
+	}
+	qd.cqids = (name, ++lastqid)::qd.cqids;
+	return lastqid;
+}
+
+scoreget(score: string): ref Elem
+{
+	for(l := qidscores; l != nil; l = tl l) {
+		(s, n) := hd l;
+		if(s == score)
+			return get(n);
+	}
+	return nil;
+}
+
+scoreput(score: string): int
+{
+	qidscores = (score, ++lastqid)::qidscores;
+	return lastqid;
+}
+
+
+Elem.new(nqid: int, vd: ref Vacdir, de: ref Direntry, pqid: int): ref Elem
+{
+	(e, me) := vd.open(de);
+	if(e == nil)
+		return nil;
+	if(de.mode & Vac->Modedir)
+		return ref Elem.Dir(nqid, de, e.size, Vacdir.new(session, e, me), pqid, 0, 0, nil);
+	return ref Elem.File(nqid, de, e.size, Vacfile.new(session, e));
+}
+
+Elem.stat(e: self ref Elem): ref Sys->Dir
+{
+	d := e.de.mkdir();
+	d.qid.path = big e.qid;
+	d.length = e.size;
+	return d;
+}
+
+walk(ed: ref Elem.Dir, name: string): (ref Elem, string)
+{
+	if(name == "..")
+		return (get(ed.pqid), nil);
+
+	if(ed.qid == 0) {
+		ne := scoreget(name);
+		if(ne == nil) {
+			(ok, score) := Score.parse(name);
+			if(ok != 0)
+				return (nil, "bad score: "+name);
+
+			(vd, de, err) := vac->vdroot(session, score);
+			if(err != nil)
+				return (nil, err);
+
+			nqid := scoreput(name);
+			ne = ref Elem.Dir(nqid, de, big 0, vd, ed.qid, 0, 0, nil);
+			set(ne);
+		}
+		return (ne, nil);
+	}
+
+	de := ed.vd.walk(name);
+	if(de == nil)
+		return (nil, sprint("%r"));
+	ne := childget(ed.qid, de.elem);
+	if(ne == nil) {
+		nqid := childput(ed.qid, de.elem);
+		ne = Elem.new(nqid, ed.vd, de, ed.qid);
+		if(ne == nil)
+			return (nil, sprint("%r"));
+		set(ne);
+	}
+	return (ne, nil);
+}
+
+get(qid: int): ref Elem
+{
+	for(l := elems[qid % len elems]; l != nil; l = tl l)
+		if((hd l).qid == qid)
+			return hd l;
+	return nil;
+}
+
+set(e: ref Elem)
+{
+	elems[e.qid % len elems] = e::elems[e.qid % len elems];
+}
+
+getfile(qid: int): ref Elem.File
+{
+	pick file := get(qid) {
+	File =>	return file;
+	}
+	fail("internal error, getfile");
+	return nil;
+}
+
+getdir(qid: int): ref Elem.Dir
+{
+	pick d := get(qid) {
+	Dir =>	return d;
+	}
+	fail("internal error, getdir");
+	return nil;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	dial = load Dial Dial->PATH;
+	str = load String String->PATH;
+	dt = load Daytime Daytime->PATH;
+	venti = load Venti Venti->PATH;
+	styx = load Styx Styx->PATH;
+	styxservers = load Styxservers Styxservers->PATH;
+	vac = load Vac Vac->PATH;
+	venti->init();
+	vac->init();
+	styx->init();
+	styxservers->init(styx);
+
+	sys->pctl(sys->NEWPGRP, nil);
+	if(venti == nil || vac == nil)
+		fail("loading venti,vac");
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-Ddp] [-a addr] [[tag:]score]");
+	while((ch := arg->opt()) != 0)
+		case ch {
+		'D' =>	styxservers->traceset(1);
+		'a' =>	addr = arg->earg();
+		'd' =>	vac->dflag = dflag++;
+		'p' =>	pflag++;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args > 1)
+		arg->usage();
+
+	score: ref Score;
+	if(len args == 1) {
+		(tag, scorestr) := str->splitstrr(hd args, ":");
+		if(tag != nil)
+			tag = tag[:len tag-1];
+		if(tag == nil)
+			tag = "vac";
+		if(tag != "vac")
+			fail("bad score type: "+tag);
+		(ok, s) := Score.parse(scorestr);
+		if(ok != 0)
+			fail("bad score: "+scorestr);
+		score = ref s;
+	}
+
+	addr = dial->netmkaddr(addr, "net", "venti");
+	cc := dial->dial(addr, nil);
+	if(cc == nil)
+		fail(sprint("dialing %s: %r", addr));
+	say("have connection");
+
+	fd := cc.dfd;
+	session = Session.new(fd);
+	if(session == nil)
+		fail(sprint("handshake: %r"));
+	say("have handshake");
+
+	rqid := 0;
+	red: ref Elem;
+	if(args == nil) {
+		de := Direntry.new();
+		de.uid = de.gid = de.mid = user();
+		de.ctime = de.atime = de.mtime = dt->now();
+		de.mode = Vac->Modedir|8r755;
+		de.emode = Sys->DMDIR|8r755;
+		red = ref Elem.Dir(rqid, de, big 0, nil, rqid, 0, 0, nil);
+	} else {
+		(vd, de, err) := vac->vdroot(session, *score);
+		if(err != nil)
+			fail(err);
+		rqid = ++lastqid;
+		red = ref Elem.Dir(rqid, de, big 0, vd, rqid, 0, 0, nil);
+	}
+	set(red);
+	say(sprint("have root, qid=%d", rqid));
+
+	navchan := chan of ref Navop;
+	nav := Navigator.new(navchan);
+	spawn navigator(navchan);
+
+	msgc: chan of ref Tmsg;
+	(msgc, ss) = Styxserver.new(sys->fildes(0), nav, big rqid);
+
+	for(;;) {
+		mm := <-msgc;
+		if(mm == nil)
+			fail("eof");
+
+		pick m := mm {
+		Readerror =>
+			fail("read error: "+m.error);
+
+		Read =>
+			if(dflag) say(sprint("have read, offset=%ubd count=%d", m.offset, m.count));
+			(c, err) := ss.canread(m);
+			if(c == nil){
+				ss.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			if(c.qtype & Sys->QTDIR){
+				ss.default(m);
+				break;
+			}
+
+			ef := getfile(int c.path);
+			n := m.count;
+			a := array[n] of byte;
+			have := ef.vf.pread(a, n, m.offset);
+			if(have < 0) {
+				ss.reply(ref Rmsg.Error(m.tag, sprint("%r")));
+				break;
+			}
+			ss.reply(ref Rmsg.Read(m.tag, a[:have]));
+
+		Open =>
+			(c, mode, f, err) := canopen(m);
+			if(c == nil){
+				ss.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			c.open(mode, f.qid);
+			ss.reply(ref Rmsg.Open(m.tag, f.qid, ss.iounit()));
+
+
+		* =>
+			ss.default(m);
+		}
+	}
+}
+
+canopen(m: ref Tmsg.Open): (ref Fid, int, ref Sys->Dir, string)
+{
+	c := ss.getfid(m.fid);
+	if(c == nil)
+		return (nil, 0, nil, Styxservers->Ebadfid);
+	if(c.isopen)
+		return (nil, 0, nil, Styxservers->Eopen);
+	(f, err) := ss.t.stat(c.path);
+	if(f == nil)
+		return (nil, 0, nil, err);
+	mode := styxservers->openmode(m.mode);
+	if(mode == -1)
+		return (nil, 0, nil, Styxservers->Ebadarg);
+	if(mode != Sys->OREAD && f.qid.qtype & Sys->QTDIR)
+		return (nil, 0, nil, Styxservers->Eperm);
+	if(!pflag && !styxservers->openok(c.uname, m.mode, f.mode, f.uid, f.gid))
+		return (nil, 0, nil, Styxservers->Eperm);
+	if(m.mode & Sys->ORCLOSE)
+		return (nil, 0, nil, Styxservers->Eperm);
+	return (c, mode, f, err);
+}
+
+navigator(c: chan of ref Navop)
+{
+loop:
+	for(;;) {
+		navop := <- c;
+		if(dflag) say(sprint("have navop, path=%bd", navop.path));
+		pick n := navop {
+		Stat =>
+			if(dflag) say(sprint("have stat"));
+			n.reply <-= (get(int n.path).stat(), nil);
+
+		Walk =>
+			if(dflag) say(sprint("have walk, name=%q", n.name));
+			ed := getdir(int n.path);
+			(ne, err) := walk(ed, n.name);
+			if(err != nil) {
+				n.reply <-= (nil, err);
+				break;
+			}
+			n.reply <-= (ne.stat(), nil);
+
+		Readdir =>
+			if(dflag) say(sprint("have readdir path=%bd offset=%d count=%d", n.path, n.offset, n.count));
+			if(n.path == big 0) {
+				n.reply <-= (nil, nil);
+				break;
+			}
+			ed := getdir(int n.path);
+			if(n.offset == 0) {
+				ed.vd.rewind();
+				ed.offset = 0;
+				ed.nprev = 0;
+				ed.prev = array[0] of ref Sys->Dir;
+			}
+			skip := n.offset-ed.offset;
+			if(skip > 0) {
+				ed.prev = ed.prev[skip:];
+				ed.nprev -= skip;
+				ed.offset += skip;
+			}
+			if(len ed.prev < n.count) {
+				newprev := array[n.count] of ref Sys->Dir;
+				newprev[:] = ed.prev;
+				ed.prev = newprev;
+			}
+			while(ed.nprev < n.count) {
+				(ok, de) := ed.vd.readdir();
+				if(ok < 0) {
+					say(sprint("readdir error: %r"));
+					n.reply <-= (nil, sprint("reading directory: %r"));
+					continue loop;
+				}
+				if(de == nil)
+					break;
+				ne := childget(ed.qid, de.elem);
+				if(ne == nil) {
+					nqid := childput(ed.qid, de.elem);
+					ne = Elem.new(nqid, ed.vd, de, ed.qid);
+					if(ne == nil) {
+						n.reply <-= (nil, sprint("%r"));
+						continue loop;
+					}
+					set(ne);
+				}
+				d := ne.stat();
+				ed.prev[ed.nprev++] = d;
+				n.reply <-= (d, nil);
+			}
+			n.reply <-= (nil, nil);
+		}
+	}
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd != nil)
+	if((n := sys->read(fd, d := array[128] of byte, len d)) > 0)
+		return string d[:n];
+	return "nobody";
+}
+
+pid(): int
+{
+	return sys->pctl(0, nil);
+}
+
+killgrp(pid: int)
+{
+	sys->fprint(sys->open(sprint("/prog/%d/ctl", pid), sys->OWRITE), "killgrp");
+}
+
+say(s: string)
+{
+	if(dflag)
+		warn(s);
+}
+
+fd2: ref Sys->FD;
+warn(s: string)
+{
+	if(fd2 == nil)
+		fd2 = sys->fildes(2);
+	sys->fprint(fd2, "%s\n", s);
+}
+
+fail(s: string)
+{
+	warn(s);
+	killgrp(pid());
+	raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/cmd/vacget.b
@@ -1,0 +1,205 @@
+implement Vacget;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "arg.m";
+include "dial.m";
+	dial: Dial;
+include "string.m";
+	str: String;
+include "venti.m";
+	venti: Venti;
+	Root, Entry, Score, Session: import venti;
+include "vac.m";
+	vac: Vac;
+	Direntry, Vacdir, Vacfile: import vac;
+
+Vacget: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+addr := "$venti";
+dflag: int;
+vflag: int;
+pflag: int;
+tflag: int;
+
+session: ref Session;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg := load Arg Arg->PATH;
+	dial = load Dial Dial->PATH;
+	str = load String String->PATH;
+	venti = load Venti Venti->PATH;
+	vac = load Vac Vac->PATH;
+	if(venti == nil || vac == nil)
+		fail("loading venti,vac");
+	venti->init();
+	vac->init();
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-dtv] [-a addr] [tag:]score");
+	while((c := arg->opt()) != 0)
+		case c {
+		'a' =>	addr = arg->earg();
+		'd' =>	vac->dflag = dflag++;
+		'p' =>	pflag++;
+		't' =>	tflag++;
+		'v' =>	vflag++;
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+
+	(tag, scorestr) := str->splitstrr(hd args, ":");
+	if(tag != nil)
+		tag = tag[:len tag-1];
+	if(tag == nil)
+		tag = "vac";
+	if(tag != "vac")
+		fail("bad score type: "+tag);
+
+	(sok, score) := Score.parse(scorestr);
+	if(sok != 0)
+		fail("bad score: "+scorestr);
+	say("have score");
+
+	addr = dial->netmkaddr(addr, "net", "venti");
+	cc := dial->dial(addr, nil);
+	if(cc == nil)
+		fail(sprint("dialing %s: %r", addr));
+	say("have connection");
+
+	fd := cc.dfd;
+	session = Session.new(fd);
+	if(session == nil)
+		fail(sprint("handshake: %r"));
+	say("have handshake");
+
+	(vd, nil, err) := vac->vdroot(session, score);
+	if(err != nil)
+		fail(err);
+
+	say("starting walk");
+	walk(".", vd);
+}
+
+create(path: string, omode: int, de: ref Direntry): ref Sys->FD
+{
+	perm := Sys->DMDIR | Sys->DMAPPEND | Sys->DMEXCL | Sys->DMTMP;
+	perm &= de.emode;
+	perm |= 8r666;
+	if(de.emode & Sys->DMDIR)
+		perm |= 8r777;
+	fd := sys->create(path, omode, perm);
+	if(fd == nil)
+		return nil;
+	if(pflag) {
+		d := sys->nulldir;
+		d.uid = de.uid;
+		d.gid = de.gid;
+		d.mode = de.emode;
+		if(sys->fwstat(fd, d) != 0) {
+			warn(sprint("fwstat %s for uid/gid/mode: %r", path));
+			d.uid = d.gid = "";
+			sys->fwstat(fd, d);
+		}
+	}
+	return fd;
+}
+
+walk(path: string, vd: ref Vacdir)
+{
+	say("start of walk: "+path);
+	for(;;) {
+		(n, de) := vd.readdir();
+		if(n < 0)
+			fail(sprint("reading direntry in %s: %r", path));
+		if(n == 0)
+			break;
+		if(dflag) say("walk: have direntry, elem="+de.elem);
+		newpath := path+"/"+de.elem;
+		(e, me) := vd.open(de);
+		if(e == nil)
+			fail(sprint("reading entry for %s: %r", newpath));
+
+		oflags := de.mode&~(vac->Modeperm|vac->Modeappend|vac->Modeexcl|vac->Modedir|vac->Modesnapshot);
+		if(oflags)
+			warn(sprint("%s: not all bits in mode can be set: 0x%x", newpath, oflags));
+
+		if(tflag || vflag)
+			sys->print("%s\n", newpath);
+
+		if(me != nil) {
+			if(!tflag)
+				create(newpath, Sys->OREAD, de);
+				# ignore error, possibly for already existing dir.  
+				# if creating really failed, writing files in the dir will fail later on.
+			walk(newpath, Vacdir.new(session, e, me));
+		} else {
+			if(tflag)
+				continue;
+			say("writing file");
+			fd := create(newpath, sys->OWRITE, de);
+			if(fd == nil)
+				fail(sprint("creating %s: %r", newpath));
+			bio := bufio->fopen(fd, bufio->OWRITE);
+			if(bio == nil)
+				fail(sprint("bufio fopen %s: %r", newpath));
+
+			buf := array[sys->ATOMICIO] of byte;
+			vf := Vacfile.new(session, e);
+			for(;;) {
+				rn := vf.read(buf, len buf);
+				if(rn == 0)
+					break;
+				if(rn < 0)
+					fail(sprint("reading vac %s: %r", newpath));
+				wn := bio.write(buf, rn);
+				if(wn != rn)
+					fail(sprint("writing local %s: %r", newpath));
+			}
+			bok := bio.flush();
+			bio.close();
+			if(bok == bufio->ERROR || bok == bufio->EOF)
+				fail(sprint("bufio close: %r"));
+
+			if(pflag) {
+				d := sys->nulldir;
+				d.mtime = de.mtime;
+				if(sys->fwstat(fd, d) < 0)
+					warn(sprint("fwstat %s for mtime: %r", newpath));
+			}
+			fd = nil;
+		}
+	}
+}
+
+say(s: string)
+{
+	if(dflag)
+		warn(s);
+}
+
+fd2: ref Sys->FD;
+warn(s: string)
+{
+	if(fd2 == nil)
+		fd2 = sys->fildes(2);
+	sys->fprint(fd2, "%s\n", s);
+}
+
+fail(s: string)
+{
+	warn(s);
+	raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/cmd/vacput.b
@@ -1,0 +1,329 @@
+implement Vacput;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "daytime.m";
+	dt: Daytime;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "dial.m";
+	dial: Dial;
+include "string.m";
+	str: String;
+include "tables.m";
+	tables: Tables;
+	Strhash: import tables;
+include "venti.m";
+	venti: Venti;
+	Root, Entry, Score, Session: import venti;
+include "vac.m";
+	vac: Vac;
+	Direntry, File, Sink, MSink: import vac;
+
+Vacput: module {
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+addr := "$venti";
+dflag: int;
+iflag: int;
+vflag: int;
+xflag: int;
+blocksize := vac->Dsize;
+uid: string;
+gid: string;
+
+pathgen: big;
+
+bout: ref Iobuf;
+session: ref Session;
+name := "vac";
+itab,
+xtab: ref Strhash[string]; # include/exclude paths
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	dt = load Daytime Daytime->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg := load Arg Arg->PATH;
+	dial = load Dial Dial->PATH;
+	str = load String String->PATH;
+	tables = load Tables Tables->PATH;
+	venti = load Venti Venti->PATH;
+	vac = load Vac Vac->PATH;
+	if(venti == nil || vac == nil)
+		fail("loading venti,vac");
+	venti->init();
+	vac->init();
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-dv] [-i | -x] [-a addr] [-b blocksize] [-n name] [-u uid] [-g gid] path ...");
+	while((c := arg->opt()) != 0)
+		case c {
+		'a' =>	addr = arg->earg();
+		'b' =>	blocksize = int arg->earg();
+		'n' =>	name = arg->earg();
+		'd' =>	vac->dflag = dflag++;
+		'i' =>	iflag++;
+		'v' =>	vflag++;
+		'x' =>	xflag++;
+		'g' =>	gid = arg->earg();
+		'u' =>	uid = arg->earg();
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args == 0)
+		arg->usage();
+	if(iflag && xflag) {
+		warn("cannot have both -i and -x");
+		arg->usage();
+	}
+
+	if(vflag)
+		bout = bufio->fopen(sys->fildes(1), bufio->OWRITE);
+
+	if(iflag || xflag) {
+		t := readpaths();
+		if(iflag)
+			itab = t;
+		else
+			xtab = t;
+	}
+
+	addr = dial->netmkaddr(addr, "net", "venti");
+	cc := dial->dial(addr, nil);
+	if(cc == nil)
+		fail(sprint("dialing %s: %r", addr));
+	say("have connection");
+
+	fd := cc.dfd;
+	session = Session.new(fd);
+	if(session == nil)
+		fail(sprint("handshake: %r"));
+	say("have handshake");
+
+	topde: ref Direntry;
+	if(len args == 1 && ((nil, d) := sys->stat(hd args)).t0 == 0 && (d.mode&Sys->DMDIR)) {
+		topde = Direntry.mk(d);
+		topde.elem = name;
+	} else {
+		topde = Direntry.new();
+		topde.elem = name;
+		topde.uid = topde.gid = user();
+		topde.mode = 8r777|Vac->Modedir;
+		topde.mtime = topde.atime = 0;
+	}
+	topde.qid = pathgen++;
+	if(uid != nil)
+		topde.uid = uid;
+	if(gid != nil)
+		topde.gid = gid;
+	topde.ctime = dt->now();
+
+	s := Sink.new(session, blocksize);
+	ms := MSink.new(session, blocksize);
+	for(; args != nil; args = tl args)
+		writepath(hd args, s, ms);
+	say("tree written");
+
+	if(vflag && bout.flush() == bufio->ERROR)
+		fail(sprint("write stdout: %r"));
+
+	e0 := s.finish();
+	if(e0 == nil)
+		fail(sprint("writing top entry: %r"));
+	e1 := ms.finish();
+	if(e1 == nil)
+		fail(sprint("writing top meta entry: %r"));
+	topde.qidspace = 1;
+	topde.qidoff = big 0;
+	topde.qidmax = pathgen;
+	s2 := MSink.new(session, blocksize);
+	if(s2.add(topde) < 0)
+		fail(sprint("adding direntry for top entries: %r"));
+	e2 := s2.finish();
+	say("top meta entry written, "+e2.score.text());
+
+ 	td := array[venti->Entrysize*3] of byte;
+ 	td[0*venti->Entrysize:] = e0.pack();
+ 	td[1*venti->Entrysize:] = e1.pack();
+ 	td[2*venti->Entrysize:] = e2.pack();
+	(tok, tscore) := session.write(venti->Dirtype, td);
+	if(tok < 0)
+		fail(sprint("writing top-level entries: %r"));
+
+	root := ref Root(venti->Rootversion, name, "vac", tscore, blocksize, nil);
+	rd := root.pack();
+	if(rd == nil)
+		fail(sprint("root pack: %r"));
+	(rok, rscore) := session.write(venti->Roottype, rd);
+	if(rok < 0)
+		fail(sprint("writing root score: %r"));
+	sys->print("vac:%s\n", rscore.text());
+	if(session.sync() < 0)
+		fail(sprint("syncing server: %r"));
+}
+
+readpaths(): ref Strhash[string]
+{
+	t := Strhash[string].new(199, nil);
+	b := bufio->fopen(sys->fildes(0), bufio->OREAD);
+	if(b == nil)
+		fail(sprint("fopen: %r"));
+	for(;;) {
+		s := b.gets('\n');
+		if(s == nil)
+			break;
+		if(s[len s-1] == '\n')
+			s = s[:len s-1];
+		t.add(s, s);
+	}
+	return t;
+}
+
+usepath(p: string): int
+{
+	if(itab != nil)
+		return itab.find(p) != nil;
+	if(xtab != nil)
+		return xtab.find(p) == nil;
+	return 1;
+}
+
+writepath(path: string, s: ref Sink, ms: ref MSink)
+{
+	if(!usepath(path))
+		return;
+
+	if(vflag && bout.puts(path+"\n") == bufio->ERROR)
+		fail(sprint("write stdout: %r"));
+
+	fd := sys->open(path, sys->OREAD);
+	if(fd == nil)
+		fail(sprint("opening %s: %r", path));
+	(ok, dir) := sys->fstat(fd);
+	if(ok < 0)
+		fail(sprint("fstat %s: %r", path));
+	if(dir.mode&sys->DMAUTH)
+		return warn(path+": is auth file, skipping");
+	if(dir.mode&sys->DMTMP)
+		return warn(path+": is temporary file, skipping");
+
+	e, me: ref Entry;
+	de: ref Direntry;
+	qid := pathgen++;
+	if(dir.mode & sys->DMDIR) {
+		ns := Sink.new(session, blocksize);
+		nms := MSink.new(session, blocksize);
+		for(;;) {
+			(n, dirs) := sys->dirread(fd);
+			if(n == 0)
+				break;
+			if(n < 0)
+				fail(sprint("dirread %s: %r", path));
+			for(i := 0; i < len dirs; i++) {
+				d := dirs[i];
+				npath := path+"/"+d.name;
+				writepath(npath, ns, nms);
+			}
+		}
+		e = ns.finish();
+		if(e == nil)
+			fail(sprint("error flushing dirsink for %s: %r", path));
+		me = nms.finish();
+		if(me == nil)
+			fail(sprint("error flushing metasink for %s: %r", path));
+	} else {
+		e = writefile(path, fd);
+		if(e == nil)
+			fail(sprint("error flushing filesink for %s: %r", path));
+	}
+
+	case dir.name {
+	"/" =>	dir.name = "root";
+	"." =>	dir.name = "dot";
+	}
+	de = Direntry.mk(dir);
+	de.qid = qid;
+	if(uid != nil)
+		de.uid = uid;
+	if(gid != nil)
+		de.gid = gid;
+
+	i := s.add(e);
+	if(i < 0)
+		fail(sprint("adding entry to sink: %r"));
+	mi := 0;
+	if(me != nil)
+		mi = s.add(me);
+	if(mi < 0)
+		fail(sprint("adding mentry to sink: %r"));
+	de.entry = i;
+	de.mentry = mi;
+	i = ms.add(de);
+	if(i < 0)
+		fail(sprint("adding direntry to msink: %r"));
+}
+
+writefile(path: string, fd: ref Sys->FD): ref Entry
+{
+	bio := bufio->fopen(fd, bufio->OREAD);
+	if(bio == nil)
+		fail(sprint("bufio opening %s: %r", path));
+
+	f := File.new(session, venti->Datatype, blocksize);
+	for(;;) {
+		buf := array[blocksize] of byte;
+		n := 0;
+		while(n < len buf) {
+			want := len buf - n;
+			have := bio.read(buf[n:], want);
+			if(have == 0)
+				break;
+			if(have < 0)
+				fail(sprint("reading %s: %r", path));
+			n += have;
+		}
+
+		if(f.write(buf[:n]) < 0)
+			fail(sprint("writing %s: %r", path));
+		if(n != len buf)
+			break;
+	}
+	bio.close();
+	return f.finish();
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd != nil)
+	if((n := sys->read(fd, d := array[128] of byte, len d)) > 0)
+		return string d[:n];
+	return "nobody";
+}
+
+fd2: ref Sys->FD;
+warn(s: string)
+{
+	if(fd2 == nil)
+		fd2 = sys->fildes(2);
+	sys->fprint(fd2, "%s\n", s);
+}
+
+say(s: string)
+{
+	if(dflag)
+		warn(s);
+}
+
+fail(s: string)
+{
+	warn(s);
+	raise "fail:"+s;
+}
--- /dev/null
+++ b/appl/cmd/wav2iaf.b
@@ -1,0 +1,171 @@
+implement Wav2Iaf;
+
+include "sys.m";
+include "draw.m";
+include	"bufio.m";
+
+sys:	Sys;
+FD:	import sys;
+bufio:	Bufio;
+Iobuf:	import bufio;
+
+stderr:	ref FD;
+inf:	ref Iobuf;
+prog:	string;
+buff4:	array of byte;
+
+pad	:= array[] of { "  ", " ", "", "   " };
+
+Wav2Iaf: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+ioerror()
+{
+	sys->fprint(stderr, "%s: read error: %r\n", prog);
+	exit;
+}
+
+shortfile(diag: string)
+{
+	sys->fprint(stderr, "%s: short read: %s\n", prog, diag);
+	exit;
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "%s: bad wave file: %s\n", prog, s);
+	exit;
+}
+
+get(c: int, s: string)
+{
+	n := inf.read(buff4, c);
+	if (n < 0)
+		ioerror();
+	if (n != c)
+		shortfile("expected " + s);
+}
+
+gets(c: int, s: string) : string
+{
+	get(c, s);
+	return string buff4[0:c];
+}
+
+need(s: string)
+{
+	get(4, s);
+	if (string buff4 != s) {
+		sys->fprint(stderr, "%s: not a wave file\n", prog);
+		exit;
+	}
+}
+
+getl(s: string) : int
+{
+	get(4, s);
+	return int buff4[0] + (int buff4[1] << 8) + (int buff4[2] << 16) + (int buff4[3] << 24);
+}
+
+getw(s: string) : int
+{
+	get(2, s);
+	return int buff4[0] + (int buff4[1] << 8);
+}
+
+skip(n: int)
+{
+	while (n > 0) {
+		inf.getc();
+		n--;
+	}
+}
+
+bufcp(s, d: ref Iobuf, n: int)
+{
+	while (n > 0) {
+		b := s.getb();
+		if (b < 0) {
+			if (b == Bufio->EOF)
+				sys->fprint(stderr, "%s: short input file\n", prog);
+			else
+				sys->fprint(stderr, "%s: read error: %r\n", prog);
+			exit;
+		}
+		d.putb(byte b);
+		n--;
+	}
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	l: int;
+	a: string;
+
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	prog = hd argv;
+	argv = tl argv;
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		sys->fprint(stderr, "%s: could not load %s: %r\n", prog, Bufio->PATH);
+	if (argv == nil) {
+		inf = bufio->fopen(sys->fildes(0), Bufio->OREAD);
+		if (inf == nil) {
+			sys->fprint(stderr, "%s: could not fopen stdin: %r\n", prog);
+			exit;
+		}
+	}
+	else if (tl argv != nil) {
+		sys->fprint(stderr, "usage: %s [infile]\n", prog);
+		exit;
+	}
+	else {
+		inf = bufio->open(hd argv, Sys->OREAD);
+		if (inf == nil) {
+			sys->fprint(stderr, "%s: could not open %s: %r\n", prog, hd argv);
+			exit;
+		}
+	}
+	buff4 = array[4] of byte;
+	need("RIFF");
+	getl("length");
+	need("WAVE");
+	for (;;) {
+		a = gets(4, "tag");
+		l = getl("length");
+		if (a == "fmt ")
+			break;
+		skip(l);
+	}
+	if (getw("format") != 1)
+		error("not PCM");
+	chans := getw("channels");
+	rate := getl("rate");
+	getl("AvgBytesPerSec");
+	getw("BlockAlign");
+	bits := getw("bits");
+	l -= 16;
+	do {
+		skip(l);
+		a = gets(4, "tag");
+		l = getl("length");
+	}
+	while (a != "data");
+	outf := bufio->fopen(sys->fildes(1), Sys->OWRITE);
+	if (outf == nil) {
+		sys->fprint(stderr, "%s: could not fopen stdout: %r\n", prog);
+		exit;
+	}
+	s := "rate\t" + string rate + "\n"
+		+  "chans\t" + string chans + "\n"
+		+  "bits\t" + string bits + "\n"
+		+  "enc\tpcm";
+	outf.puts(s);
+	outf.puts(pad[len s % 4]);
+	outf.puts("\n\n");
+	bufcp(inf, outf, l);
+	outf.flush();
+}
--- /dev/null
+++ b/appl/cmd/wc.b
@@ -1,0 +1,303 @@
+implement Wc;
+
+#
+# wc -- count things in utf-encoded text files
+# Bugs:
+#	The only white space characters recognized are ' ', '\t' and '\n', even though
+#	ISO 10646 has many more blanks scattered through it.
+#	Should count characters that cannot occur in any rune (hex f0-ff) separately.
+#	Should count non-canonical runes (e.g. hex c1,80 instead of hex 40).
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+Wc: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+NBUF:	con 8*1024;
+
+stderr:	ref Sys->FD;
+nline, tnline, pline: int;
+nword, tnword, pword: int;
+nchar, tnchar, pchar: int;
+nbadr, tnbadr, pbadr: int;
+nbyte, tnbyte, pbyte: int;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	for(argv = tl argv; argv != nil; argv = tl argv){
+		arg := hd argv;
+		if(len arg < 2 || arg[0] != '-' || arg[1] == '-')
+			break;
+		for(i := 1; i < len arg; i++){
+			case arg[i]{
+			'l' => pline++;
+			'w' => pword++;
+			'c' => pchar++;
+			'e' => pbadr++;
+			'b' => pbyte++;
+			* =>
+				sys->fprint(stderr, "usage: wc [-lwcbe] [file ...]\n");
+				raise "fail:usage";
+			}
+		}
+	}
+	if(pline+pword+pchar+pbadr+pbyte == 0)
+		pline = pword = pchar = 1;
+	argc := len argv;
+	if(argc == 0)
+		count(sys->fildes(0), "");
+	else{
+		for(; argv != nil; argv = tl argv){
+			name := hd argv;
+			f := sys->open(name, sys->OREAD);
+			if(f == nil)
+				sys->fprint(stderr, "wc: can't open %s: %r\n", name);
+			else{
+				count(f, name);
+				tnline += nline;
+				tnword += nword;
+				tnchar += nchar;
+				tnbadr += nbadr;
+				tnbyte += nbyte;
+				f = nil;
+			}
+		}
+		if(argc > 1)
+			report(tnline, tnword, tnchar, tnbadr, tnbyte, "total");
+	}
+	exit;
+}
+report(nline, nword, nchar, nbadr, nbyte: int, fname: string)
+{
+	line := "";
+	if(pline)
+		line += sys->sprint(" %7d", nline);
+	if(pword)
+		line += sys->sprint(" %7d", nword);
+	if(pchar)
+		line += sys->sprint(" %7d", nchar);
+	if(pbadr)
+		line += sys->sprint(" %7d", nbadr);
+	if(pbyte)
+		line += sys->sprint(" %7d", nbyte);
+	if(fname != nil)
+		line += sys->sprint(" %s", fname);
+	sys->print("%s\n", line[1:]);
+}
+#
+# How it works.  Start in statesp.  Each time we read a character,
+# increment various counts, and do state transitions according to the
+# following table.  If we're not in statesp or statewd when done, the
+# file ends with a partial rune.
+#        |                character
+#  state |09,20| 0a  |00-7f|80-bf|c0-df|e0-ef|f0-ff
+# -------+-----+-----+-----+-----+-----+-----+-----
+# statesp|ASP  |ASPN |AWDW |AWDWX|AC2W |AC3W |AWDWX
+# statewd|ASP  |ASPN |AWD  |AWDX |AC2  |AC3  |AWDX
+# statec2|ASPX |ASPNX|AWDX |AWDR |AC2X |AC3X |AWDX
+# statec3|ASPX |ASPNX|AWDX |AC2R |AC2X |AC3X |AWDX
+#
+			# actions
+	AC2,		# enter statec2
+	AC2R,		# enter statec2, don't count a rune
+	AC2W,		# enter statec2, count a word
+	AC2X,		# enter statec2, count a bad rune
+	AC3,		# enter statec3
+	AC3W,		# enter statec3, count a word
+	AC3X,		# enter statec3, count a bad rune
+	ASP,		# enter statesp
+	ASPN,		# enter statesp, count a newline
+	ASPNX,		# enter statesp, count a newline, count a bad rune
+	ASPX,		# enter statesp, count a bad rune
+	AWD,		# enter statewd
+	AWDR,		# enter statewd, don't count a rune
+	AWDW,		# enter statewd, count a word
+	AWDWX,		# enter statewd, count a word, count a bad rune
+	AWDX:		# enter statewd, count a bad rune
+		con byte iota;
+
+statesp := array[256] of{	# looking for the start of a word
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 00-07
+AWDW, ASP,  ASPN, AWDW, AWDW, AWDW, AWDW, AWDW,	# 08-0f
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 10-17
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 18-1f
+ASP,  AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 20-27
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 28-2f
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 30-37
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 38-3f
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 40-47
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 48-4f
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 50-57
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 58-5f
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 60-67
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 68-6f
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 70-77
+AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW, AWDW,	# 78-7f
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# 80-87
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# 88-8f
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# 90-97
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# 98-9f
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# a0-a7
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# a8-af
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# b0-b7
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# b8-bf
+AC2W, AC2W, AC2W, AC2W, AC2W, AC2W, AC2W, AC2W,	# c0-c7
+AC2W, AC2W, AC2W, AC2W, AC2W, AC2W, AC2W, AC2W,	# c8-cf
+AC2W, AC2W, AC2W, AC2W, AC2W, AC2W, AC2W, AC2W,	# d0-d7
+AC2W, AC2W, AC2W, AC2W, AC2W, AC2W, AC2W, AC2W,	# d8-df
+AC3W, AC3W, AC3W, AC3W, AC3W, AC3W, AC3W, AC3W,	# e0-e7
+AC3W, AC3W, AC3W, AC3W, AC3W, AC3W, AC3W, AC3W,	# e8-ef
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# f0-f7
+AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,AWDWX,# f8-ff
+};
+statewd := array[256] of {	# looking for the next character in a word
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 00-07
+AWD,  ASP,  ASPN, AWD,  AWD,  AWD,  AWD,  AWD,	# 08-0f
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 10-17
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 18-1f
+ASP,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 20-27
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 28-2f
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 30-37
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 38-3f
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 40-47
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 48-4f
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 50-57
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 58-5f
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 60-67
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 68-6f
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 70-77
+AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,  AWD,	# 78-7f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 80-87
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 88-8f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 90-97
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 98-9f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# a0-a7
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# a8-af
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# b0-b7
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# b8-bf
+AC2,  AC2,  AC2,  AC2,  AC2,  AC2,  AC2,  AC2,	# c0-c7
+AC2,  AC2,  AC2,  AC2,  AC2,  AC2,  AC2,  AC2,	# c8-cf
+AC2,  AC2,  AC2,  AC2,  AC2,  AC2,  AC2,  AC2,	# d0-d7
+AC2,  AC2,  AC2,  AC2,  AC2,  AC2,  AC2,  AC2,	# d8-df
+AC3,  AC3,  AC3,  AC3,  AC3,  AC3,  AC3,  AC3,	# e0-e7
+AC3,  AC3,  AC3,  AC3,  AC3,  AC3,  AC3,  AC3,	# e8-ef
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# f0-f7
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# f8-ff
+};
+statec2 := array[256] of {	# looking for 10xxxxxx to complete a rune
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 00-07
+AWDX, ASPX, ASPNX,AWDX, AWDX, AWDX, AWDX, AWDX,	# 08-0f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 10-17
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 18-1f
+ASPX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 20-27
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 28-2f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 30-37
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 38-3f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 40-47
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 48-4f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 50-57
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 58-5f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 60-67
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 68-6f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 70-77
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 78-7f
+AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR,	# 80-87
+AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR,	# 88-8f
+AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR,	# 90-97
+AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR,	# 98-9f
+AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR,	# a0-a7
+AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR,	# a8-af
+AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR,	# b0-b7
+AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR, AWDR,	# b8-bf
+AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X,	# c0-c7
+AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X,	# c8-cf
+AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X,	# d0-d7
+AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X,	# d8-df
+AC3X, AC3X, AC3X, AC3X, AC3X, AC3X, AC3X, AC3X,	# e0-e7
+AC3X, AC3X, AC3X, AC3X, AC3X, AC3X, AC3X, AC3X,	# e8-ef
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# f0-f7
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# f8-ff
+};
+statec3 := array[256] of {	# looking for 10xxxxxx,10xxxxxx to complete a rune
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 00-07
+AWDX, ASPX, ASPNX,AWDX, AWDX, AWDX, AWDX, AWDX,	# 08-0f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 10-17
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 18-1f
+ASPX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 20-27
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 28-2f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 30-37
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 38-3f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 40-47
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 48-4f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 50-57
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 58-5f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 60-67
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 68-6f
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 70-77
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# 78-7f
+AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R,	# 80-87
+AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R,	# 88-8f
+AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R,	# 90-97
+AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R,	# 98-9f
+AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R,	# a0-a7
+AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R,	# a8-af
+AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R,	# b0-b7
+AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R, AC2R,	# b8-bf
+AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X,	# c0-c7
+AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X,	# c8-cf
+AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X,	# d0-d7
+AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X, AC2X,	# d8-df
+AC3X, AC3X, AC3X, AC3X, AC3X, AC3X, AC3X, AC3X,	# e0-e7
+AC3X, AC3X, AC3X, AC3X, AC3X, AC3X, AC3X, AC3X,	# e8-ef
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# f0-f7
+AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX, AWDX,	# f8-ff
+};
+buf := array[NBUF] of byte;
+count(f: ref Sys->FD, name: string)
+{
+	state := statesp;
+	nline = nword = nchar = nbadr = nbyte = 0;
+	n := 0;
+	for(;;){
+		n = sys->read(f, buf, NBUF);
+		if(n <= 0)
+			break;
+		nbyte += n;
+		nchar += n;	# might be too large, gets decreased later
+		i := 0;
+		do{
+			case int state[int buf[i++]]{
+			int AC2 =>   state = statec2;
+			int AC2R =>  state = statec2; nchar--;
+			int AC2W =>  state = statec2; nword++;
+			int AC2X =>  state = statec2;          nbadr++;
+			int AC3 =>   state = statec3;
+			int AC3W =>  state = statec3; nword++;
+			int AC3X =>  state = statec3;          nbadr++;
+			int ASP =>   state = statesp;
+			int ASPN =>  state = statesp; nline++;
+			int ASPNX => state = statesp; nline++; nbadr++;
+			int ASPX =>  state = statesp;          nbadr++;
+			int AWD =>   state = statewd;
+			int AWDR =>  state = statewd; nchar--;
+			int AWDW =>  state = statewd; nword++;
+			int AWDWX => state = statewd; nword++; nbadr++;
+			int AWDX =>  state = statewd;          nbadr++;
+			}
+		}while(i < n);
+	}
+	if(state!=statesp && state!=statewd)
+		nbadr++;
+	if(n < 0)
+		sys->fprint(stderr, "wc: error reading %s: %r\n", name);
+	report(nline, nword, nchar, nbadr, nbyte, name);
+}
--- /dev/null
+++ b/appl/cmd/webgrab.b
@@ -1,0 +1,571 @@
+# Webgrab -- for getting html pages and the subordinate files (images, frame children)
+# they refer to (using "src=..." in a tag) into the local file space.
+# Assume http: scheme if none specified.
+# Usage:
+#	webgrab [-r] [-v] [-o stem] url
+#  If stem is specified, file will be saved in stem.html and images will
+#  go in stem_1.jpg (or .gif, ...), stem_2.jpg, etc.
+#  If stem is not specified, derive it from url (see getstem comment, below).
+# If -r is specified, get "raw", i.e., no image fetching/html munging.
+# If -v is specified (verbose), print some progress information,
+# with more if -vv is given.
+
+implement Webgrab;
+
+include "sys.m";
+	sys: Sys;
+	FD: import sys;
+
+include "draw.m";
+
+include "string.m";
+	S: String;
+
+include "url.m";
+	U: Url;
+	ParsedUrl: import U;
+
+include "daytime.m";
+	DT: Daytime;
+
+include "bufio.m";
+	B: Bufio;
+
+include "dial.m";
+	D: Dial;
+
+include "arg.m";
+
+Webgrab: module
+{
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+stderr: ref FD;
+verbose := 0;
+postbody : string;
+
+httpproxy: ref Url->ParsedUrl;
+noproxydoms: list of string;	# domains that don't require proxy
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	S = load String String->PATH;
+	U = load Url Url->PATH;
+	DT = load Daytime Daytime->PATH;
+	D = load Dial Dial->PATH;
+	B = load Bufio Bufio->PATH;
+	arg := load Arg Arg->PATH;
+	if(S == nil || U == nil || DT == nil || B == nil || arg == nil)
+		error_exit("can't load a module");
+	U->init();
+	stem := "";
+	rawflag := 0;
+	arg->init(args);
+	arg->setusage("webgrab [-r] [-v[v]] [-p postbody] [-o stem] url");
+	url := "";
+	while((o := arg->opt()) != 0)
+		case o {
+		'r' =>
+			rawflag = 1;
+		'v' =>
+			verbose++;
+		'o' =>
+			stem = arg->earg();
+		'p' =>
+			postbody = arg->earg();
+		* =>
+			arg->usage();
+		}
+	args = arg->argv();
+	if(len args != 1)
+		arg->usage();
+	url = hd args;
+	arg = nil;
+	(nil,xr) := S->splitstrl(url,"//");
+	(nil,yr) := S->splitl(url,":");
+	if(xr == "" && yr == "")
+		url = "http://" + url;
+	u := U->makeurl(url);
+	if(stem == "")
+		stem = getstem(u);
+	readconfig();
+	grab(u, stem, rawflag);
+}
+
+readconfig()
+{
+	cfgio := B->open("/services/webget/config", sys->OREAD);
+	if(cfgio != nil) {
+		for(;;) {
+			line := B->cfgio.gets('\n');
+			if(line == "") {
+				B->cfgio.close();
+				break;
+			}
+			if(line[0]=='#')
+				continue;
+			(key, val) := S->splitl(line, " \t=");
+			val = S->take(S->drop(val, " \t="), "^\r\n");
+			if(val == "")
+				continue;
+			case key {
+			"httpproxy" =>
+				if(val == "none")
+					continue;
+				# val should be host or host:port
+				httpproxy = U->makeurl("http://" + val);
+				if(verbose)
+					sys->fprint(stderr, "Using http proxy %s\n", httpproxy.tostring());
+			"noproxy" or
+			"noproxydoms" =>
+				(nil, noproxydoms) = sys->tokenize(val, ";, \t");
+			}
+		}
+	}
+}
+
+# Make up a stem for forming save-file-names, based on url u.
+# Use the last non-nil component of u.path, without a final extension,
+# else use the host.  Then, if the stem still contains a '.' (e.g., www.lucent)
+# use the part after the final '.'.
+# Finally, if all else fails, use use "grabout".
+getstem(u: ref ParsedUrl) : string
+{
+	stem := "";
+	if(u.path != "") {
+		(l, r) := S->splitr(u.path, "/");
+		if(r == "") {
+			# path ended with '/'; try next to last component
+			if(l != "")
+				(l, r) = S->splitr(l[0:len l - 1], "/");
+		}
+		if(r != "")
+			stem = r;
+	}
+	if(stem == "")
+		stem = u.host;
+	if(stem != "") {
+		ext: string;
+		(stem, ext) = S->splitr(stem, ".");
+		if(stem == "")
+			stem = ext;
+		else
+			stem = stem[0:len stem - 1];
+		(nil, stem) = S->splitr(stem, ".");
+	}
+	if(stem == "")
+		stem = "grabout";
+	return stem;
+}
+
+grab(u: ref ParsedUrl, stem: string, rawflag: int)
+{
+	(err, contents, fd, actual) := httpget(u);
+	if(err != "")
+		error_exit(err);
+	ish := is_html(contents);
+	if(ish)
+		contents = addfetchcomment(contents, u, actual);
+	if(rawflag || !ish) {
+		writebytes(stem, contents, fd);
+		return;
+	}
+	# get subordinates, modify contents
+	subs : list of (string, string);
+	(contents, subs)  = subfix(contents, stem);
+	writebytes(stem + ".html", contents, fd);
+	for(l := subs; l != nil; l = tl l) {
+		(fname, suburl) := hd l;
+		subu := U->makeurl(suburl);
+		subu.makeabsolute(actual);
+		(suberr, subcontents, subfd, nil) := httpget(subu);
+		if(suberr != "") {
+			sys->fprint(stderr, "webgrab: can't fetch subordinate %s from %s: %s\n", fname, subu.tostring(), suberr);
+			continue;
+		}
+		writebytes(fname, subcontents, subfd);
+	}
+}
+
+# Fix the html in array a so that referenced subordinate files (SRC= or BACKGROUND= fields of tags)
+# are replaced with local names (stem_1.xxx, stem_2.xxx, etc.),
+# and return the fixed array along with a list of (local name, subordinate url)
+# of images to be fetched.
+subfix(a: array of byte, stem: string) : (array of byte, list of (string, string))
+{
+	alen := len a;
+	if(alen == 0)
+		return (a, nil);
+	nsubs := 0;
+	newa := array[alen + 1000] of byte;
+	newai := 0;
+	j := 0;
+	intag := 0;
+	incom := 0;
+	quote := 0;
+	subs : list of (string, string) = nil;
+	for(i := 0; i < alen; i++) {
+		c := int a[i];
+		if(incom) {
+			if(amatch(a, i, alen, "-->")) {
+				incom = 0;
+				i = i+2;
+			}
+		}
+		else if(intag) {
+			if(quote==0 && (amatch(a, i, alen, "src") || amatch(a, i, alen, "background"))) {
+				v := "";
+				eqi := 0;
+				if(amatch(a, i, alen, "src"))
+					k := i+3;
+				else
+					k = i+10;
+				for(; k < alen; k++)
+					if(!iswhite(int a[k]))
+						break;
+				if(k < alen && int a[k] == '=') {
+					eqi = k;
+					k++;
+					while(k<alen && iswhite(int a[k]))
+						k++;
+					if(k<alen) {
+						kstart := k;
+						c = int a[k];
+						if(c == '\'' || c== '"') {
+							quote = int a[k++];
+							while(k<alen && (int a[k])!=quote)
+								k++;
+							v = string a[kstart+1:k];
+							k++;
+						}
+						else {
+							while(k<alen && !iswhite(int a[k]) && int a[k] != '>')
+								k++;
+							v = string a[kstart:k];
+						}
+					}
+				}
+				if(v != "") {
+					f := "";
+					for(l := subs; l != nil; l = tl l) {
+						(ff,uu) := hd l;
+						if(v == uu) {
+							f = ff;
+							break;
+						}
+					}
+					if(f == "") {
+						nsubs++;
+						f = stem + "_" + string nsubs + getsuff(v);
+						subs = (f, v) :: subs;
+					}
+					# should check for newa too small
+					newa[newai:] = a[j:eqi+1];
+					newai += eqi+1-j;
+					xa := array of byte f;
+					newa[newai:] = xa;
+					newai += len xa;
+					j = k;
+				}
+				i = k-1;
+			}
+			if(c == '>' && quote == 0)
+				intag = 0;
+			if(quote) {
+				if(quote == c)
+					quote = 0;
+			else if(c == '"' || c == '\'')
+				quote = c;
+			}
+		}
+		else if(c == '<')
+			intag = 1;
+	}
+	if(nsubs == 0)
+		return (a, nil);
+	if(i > j) {
+		newa[newai:] = a[j:i];
+		newai += i-j;
+	}
+	ans := array[newai] of byte;
+	ans[0:] = newa[0:newai];
+	anssubs : list of (string, string) = nil;
+	for(ll := subs; ll != nil; ll = tl ll)
+		anssubs = hd ll :: anssubs;
+	return (ans, anssubs);
+}
+
+# add c after all f's in a
+fixnames(a: array of byte, f: string, c: byte)
+{
+	alen := len a;
+	n := alen - len f;
+	for(i := 0; i < n; i++) {
+		if(amatch(a, i, alen, f)) {
+			a[i+len f] = c;
+		}
+	}
+}
+
+amatch(a: array of byte, i, alen: int, s: string) : int
+{
+	slen := len s;
+	for(k := 0; i+k < alen && k < slen; k++) {
+		c := int a[i+k];
+		if(c >= 'A' && c <= 'Z')
+			c = c + (int 'a' - int 'A');
+		if(c != s[k])
+			break;
+	}
+	if(k == slen) {
+		return 1;
+	}
+	return 0;
+}
+
+getsuff(ustr: string) : string
+{
+	u := U->makeurl(ustr);
+	if(u.path != "") {
+		for(i := len u.path - 1; i >= 0; i--) {
+			c := u.path[i];
+			if(c == '.')
+				return u.path[i:];
+			if(c == '/')
+				break;
+		}
+	}
+	return "";
+}
+
+iswhite(c: int) : int
+{
+	return (c==' ' || c=='\t' || c=='\n' || c=='\r');
+}
+
+# Add a comment to end of a giving date and source of fetch
+addfetchcomment(a: array of byte, u, actu: ref ParsedUrl) : array of byte
+{
+	now := DT->text(DT->local(DT->now()));
+	ustr := u.tostring();
+	actustr := actu.tostring();
+	comment := "\n<!-- Fetched " + now + " from " + ustr;
+	if(ustr != actustr)
+		comment += ", redirected to " + actustr;
+	comment += " -->\n";
+	acom := array of byte comment;
+	newa := array[len a + len acom] of byte;
+	newa[0:] = a;
+	newa[len a:] = acom;
+	return newa;
+}
+
+# Get u, return (error string, body, actual url of source, after redirection)
+httpget(u: ref ParsedUrl) : (string, array of byte, ref Sys->FD, ref ParsedUrl)
+{
+	ans, body : array of byte;
+	restfd: ref Sys->FD;
+	req : string;
+	
+	for(redir := 0; redir < 10; redir++) {
+		if(u.port == "")
+			u.port = "80";	# default IP port for HTTP
+		if(verbose)
+			sys->fprint(stderr, "connecting to %s\n", u.host);
+		dialhost, port: string;
+
+		if(httpproxy != nil && need_proxy(u.host)) {
+			dialhost = httpproxy.host;
+			port = httpproxy.port;
+		}
+		else {
+			dialhost = u.host;
+			port = u.port;
+		}
+		dest := D->netmkaddr(dialhost, "tcp", port);
+		net := D->dial(dest, nil);
+		if(net == nil)
+			return (sys->sprint("can't dial %s: %r", dest), nil, nil, nil);
+			
+		# prepare request
+		if(u.query != ""){
+			u.query = "?" + u.query;
+		}
+
+		if (postbody == nil){
+			if(httpproxy == nil || !need_proxy(u.host)){
+				req = sys->sprint("GET /%s%s HTTP/1.0\r\n"+
+						"Host: %s\r\n"+
+						"User-agent: Inferno/webgrab\r\n"+
+						"Cache-Control: no-cache\r\n"+
+						"Pragma: no-cache\r\n\r\n",
+						u.path, u.query, u.host);
+			}else{
+				req = sys->sprint("GET http:///%s%s HTTP/1.0\r\n"+
+						"Host: %s\r\n"+
+						"User-agent: Inferno/webgrab\r\n"+
+						"Cache-Control: no-cache\r\n"+
+						"Pragma: no-cache\r\n\r\n",
+						u.host, u.path, u.host);
+			}
+		}else{
+				req = sys->sprint("POST /%s HTTP/1.0\r\n"+
+						"Host: %s\r\n"+
+						"Content-type: application/x-www-form-urlencoded\r\n"+
+						"Content-length: %d\r\n"+
+						"User-agent: Inferno/webgrab\r\n"+
+						"\r\n"+"%s",
+						u.path, u.host, len postbody, postbody);
+
+		}
+
+		if(verbose)
+			sys->fprint(stderr, "writing request: %s\n", req);
+		areq := array of byte req;
+		n := sys->write(net.dfd, areq, len areq);
+		if(n != len areq)
+			return (sys->sprint("write problem: %r"), nil, nil, nil);
+		(ans, restfd) = readbytes(net.dfd);
+		(status, rest) := stripline(ans);
+		if(verbose)
+			sys->fprint(stderr, "response: %s\n", status);
+		(vers, statusrest) := S->splitl(status, " ");
+		if(!S->prefix("HTTP/", vers))
+			return ("bad reply status: " + status, rest, restfd, nil);
+		code := int statusrest;
+		location := "";
+		body = rest;
+		for(;;) {
+			hline: string;
+			(hline, body) = stripline(body);
+			if(hline == "")
+				break;
+			if(verbose > 1)
+				sys->fprint(stderr, "%s\n", hline);
+			if(!iswhite(hline[0])) {
+				(hname, hrest) := S->splitl(hline, ":");
+				if(hrest != "") {
+					hname = S->tolower(hname);
+					hval := S->drop(hrest, ": \t");
+					hval = S->take(hval, "^ \t");
+					if(hname == "location")
+						location = hval;
+				}
+			}
+		}
+		if(code != 200) {
+			if((code == 300 || code == 301 || code == 302) && location != "") {
+				# MultipleChoices, MovedPerm, or MovedTemp
+				if(verbose)
+					sys->fprint(stderr, "redirect to %s\n", location);
+				u = U->makeurl(location);
+				continue; 
+			}
+			return ("status not ok: " + status, rest, restfd, u);
+		}
+		break;
+	}
+	return ("", body, restfd, u);
+}
+
+
+need_proxy(h: string) : int
+{
+	doml := noproxydoms;
+	if(doml == nil)
+		return 1;		# all domains need proxy
+
+	lh := len h;
+	for(dom := hd doml; doml != nil; doml = tl doml) {
+		ld := len dom;
+		if(lh >= ld && h[lh-ld:] == dom)
+			return 0;	# domain is on the noproxy list
+	}
+
+	return 1;
+}
+
+# Simple guess test for HTML: first non-white byte is '<'
+is_html(a: array of byte) : int
+{
+	for(i := 0; i < len a; i++)
+		if(!iswhite(int a[i]))
+			break;
+	if(i < len a && a[i] == byte '<')
+		return 1;
+	return 0;
+}
+
+readbytes(fd: ref Sys->FD) : (array of byte, ref Sys->FD)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	i := 0;
+	avail := len buf;
+	while (avail > 0) {
+		n := sys->read(fd, buf[i:], avail);
+		if(n <= 0) {
+			fd = nil;
+			break;
+		}
+		i += n;
+		avail -= n;
+	}
+	return (buf[0:i], fd);
+}
+
+writebytes(f: string, a: array of byte, fd: ref Sys->FD)
+{
+	ofd: ref Sys->FD;
+	if (f == "-")
+		ofd = sys->fildes(1);
+	else
+		ofd = sys->create(f, Sys->OWRITE, 8r666);
+	if(ofd == nil) {
+		sys->fprint(stderr, "webgrab: can't create %s: %r\n", f);
+		return;
+	}
+	i := 0;
+	clen := len a;
+	while(i < clen) {
+		n := sys->write(ofd, a[i:], clen-i);
+		if(n < 0) {
+			sys->fprint(stderr, "webgrab: write error: %r\n");
+			return;
+		}
+		i += n;
+	}
+	if(fd != nil) {
+		buf := array[Sys->ATOMICIO] of byte;
+		while((n := sys->read(fd, buf, len buf)) > 0) {
+			if(sys->write(ofd, buf, n) != n) {
+				sys->fprint(stderr, "webgrab: write error: %r\n");
+				return;
+			}
+		}
+		if(n < 0) {
+			sys->fprint(stderr, "webgrab: read error: %r\n");
+			return;
+		}
+		clen += n;
+	}
+	if (f != "-")
+		sys->fprint(stderr, "created %s, %d bytes\n", f, clen);
+}
+
+stripline(b: array of byte) : (string, array of byte)
+{
+	n := len b - 1;
+	for(i := 0; i < n; i++)
+		if(b[i] == byte '\r' && b[i+1] == byte '\n')
+			return (string b[0:i], b[i+2:]);
+	return ("", b);
+}
+
+error_exit(msg: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", msg);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/cmd/wish.b
@@ -1,0 +1,191 @@
+implement Test;
+
+include "sys.m";
+include "draw.m";
+draw: Draw;
+Screen, Display, Image: import draw;
+include "tk.m";
+
+Test: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+tk: Tk;
+sys: Sys;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	cmd: string;
+
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+
+	display := Display.allocate(nil);
+	if(display == nil) {
+		sys->print("can't initialize display: %r\n");
+		return;
+	}
+
+	disp := display.image;
+	screen := Screen.allocate(disp, display.rgb(161, 195, 209), 1);
+	if(screen == nil) {
+		sys->print("can't allocate screen: %r\n");
+		return;
+	}
+	fd := sys->open("/dev/pointer", sys->OREAD);
+	if(fd == nil) {
+		sys->print("open: %s: %r\n", "/dev/pointer");
+		sys->print("run wm/wish instead\n");
+		return;
+	}
+
+	t := tk->toplevel(display, "");
+	spawn mouse(t, fd);
+	spawn keyboard(t);
+	disp.draw(disp.r, screen.fill, nil, disp.r.min);
+
+	input := array[8192] of byte;
+	stdin := sys->fildes(0);
+
+	if(argv != nil)
+		argv = tl argv;
+	while(argv != nil) {
+		exec(t, hd argv);
+		argv = tl argv;
+	}
+
+	for(;;) {
+		tk->cmd(t, "update");
+
+		prompt := '%';
+		if(cmd != nil)
+			prompt = '>';
+		sys->print("%c ", prompt);
+
+		n := sys->read(stdin, input, len input);
+		if(n <= 0)
+			break;
+		if(n == 1)
+			continue;
+		cmd += string input[0:n-1];
+		if(cmd[len cmd-1] != '\\') {
+			cmd = esc(cmd);
+			s := tk->cmd(t, cmd);
+			if(len s != 0)
+				sys->print("%s\n", s);
+			cmd = nil;
+			continue;
+		}
+		cmd = cmd[0:len cmd-1];
+	}
+}
+
+esc(s: string): string
+{
+	c: int;
+
+	for(i := 0; i < len s; i++) {
+		if(s[i] != '\\')
+			continue;
+		case s[i+1] {
+		'n'=>	c = '\n';
+		't'=>	c = '\t';
+		'b'=>	c = '\b';
+		'\\'=>	c = '\\';
+		* =>	c = 0;
+		}
+		if(c != 0) {
+			s[i] = c;
+			s = s[0:i+1]+s[i+2:len s];
+		}
+	}
+	return s;
+}
+
+exec(t: ref Tk->Toplevel, path: string)
+{
+	fd := sys->open(path, sys->OREAD);
+	if(fd == nil) {
+		sys->print("open: %s: %r\n", path);
+		return;
+	}
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0) {
+		sys->print("fstat: %s: %r\n", path);
+		return;
+	}
+	buf := array[int d.length] of byte;
+	if(sys->read(fd, buf, len buf) < 0) {
+		sys->print("read: %s: %r\n", path);
+		return;
+	}
+	(n, l) := sys->tokenize(string buf, "\n");
+	buf = nil;
+	n = -1;
+	for(; l != nil; l = tl l) {
+		n++;
+		s := hd l;
+		if(len s == 0 || s[0] == '#')
+			continue;
+
+		while(s[len s-1] == '\\') {
+			s = s[0:len s-1];
+			if(tl l != nil) {
+				l = tl l;
+				s = s + hd l;
+			}
+			else
+				break;
+		}
+
+		s = tk->cmd(t, esc(s));
+
+		if(len s != 0 && s[0] == '!') {
+			sys->print("%s:%d %s\n", path, n, s);
+			sys->print("%s:%d %s\n", path, n, hd l);
+		}
+	}
+}
+
+mouse(t: ref Tk->Toplevel, fd: ref Sys->FD)
+{
+	n := 0;
+	buf := array[100] of byte;
+	for(;;) {
+		n = sys->read(fd, buf, len buf);
+		if(n <= 0)
+			break;
+
+		if(int buf[0] == 'm' && n >= 1+3*12) {
+			x := int(string buf[ 1:13]);
+			y := int(string buf[12:25]);
+			b := int(string buf[24:37]);
+			tk->pointer(t, Draw->Pointer(b, Draw->Point(x, y), sys->millisec()));
+		}
+	}
+}
+
+keyboard(t: ref Tk->Toplevel)
+{
+	dfd := sys->open("/dev/keyboard", sys->OREAD);
+	if(dfd == nil)
+		return;
+
+	b:= array[1] of byte;
+	buf := array[10] of byte;
+	i := 0;
+	for(;;) {
+		n := sys->read(dfd, buf[i:], len buf - i);
+		if(n < 1)
+			break;
+		i += n;
+		while(i >0 && (nutf := sys->utfbytes(buf, i)) > 0){
+			s := string buf[0:nutf];
+			tk->keyboard(t, int s[0]);
+			buf[0:] = buf[nutf:i];
+			i -= nutf;
+		}
+	}
+}
--- /dev/null
+++ b/appl/cmd/wmexport.b
@@ -1,0 +1,556 @@
+implement Wmexport;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Wmcontext, Image: import draw;
+include "wmlib.m";
+	wmlib: Wmlib;
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Fid, Navigator, Navop: import styxservers;
+	Enotdir, Enotfound: import Styxservers;
+
+Wmexport: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+# filesystem looks like:
+#	clone
+#	1
+#		wmctl
+#		keyboard
+#		pointer
+#		winname
+
+badmodule(p: string)
+{
+	sys->fprint(sys->fildes(2), "wmexport: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+user := "me";
+qidseq := 1;
+imgseq := 0;
+
+pidregister: chan of (int, int);
+flush: chan of (int, int, chan of int);
+
+makeconn: chan of chan of (ref Conn, string);
+delconn: chan of ref Conn;
+reqpool: list of chan of (ref Tmsg, ref Conn, ref Fid);
+reqidle: int;
+reqdone: chan of chan of (ref Tmsg, ref Conn, ref Fid);
+
+srv: ref Styxserver;
+ctxt: ref Draw->Context;
+
+conns: array of ref Conn;
+nconns := 0;
+
+Qerror, Qroot, Qdir, Qclone, Qwmctl, Qptr, Qkbd, Qwinname: con iota;
+Shift: con 4;
+Mask: con 16rf;
+
+Maxreqidle: con 3;
+Maxreplyidle: con 3;
+
+Conn: adt {
+	wm:		ref Wmcontext;
+	iname:	string;				# name of image
+	n:		int;
+	nreads:	int;
+};
+
+# initial connection provides base-name (fid?) for images.
+# full name could be:
+#	window.fid.tag
+
+init(drawctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	ctxt = drawctxt;
+	if(ctxt == nil || ctxt.wm == nil){
+		sys->fprint(sys->fildes(2), "wmexport: no window manager context\n");
+		raise "fail:no wm";
+	}
+	draw = load Draw Draw->PATH;
+	styx = load Styx Styx->PATH;
+	if (styx == nil)
+		badmodule(Styx->PATH);
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	if (styxservers == nil)
+		badmodule(Styxservers->PATH);
+	styxservers->init(styx);
+
+	wmlib = load Wmlib Wmlib->PATH;
+	if(wmlib == nil)
+		badmodule(Wmlib->PATH);
+	wmlib->init();
+
+	sys->pctl(Sys->FORKNS|Sys->NEWPGRP, nil);		# fork pgrp?
+
+	ctxt = drawctxt;
+	navops := chan of ref Navop;
+	spawn navigator(navops);
+	tchan: chan of ref Tmsg;
+	(tchan, srv) = Styxserver.new(sys->fildes(0), Navigator.new(navops), big Qroot);
+	srv.replychan = chan of ref Styx->Rmsg;
+	spawn replymarshal(srv.replychan);
+	spawn serve(tchan, navops);
+}
+
+serve(tchan: chan of ref Tmsg, navops: chan of ref Navop)
+{
+	pidregister = chan of (int, int);
+	makeconn = chan of chan of (ref Conn, string);
+	delconn = chan of ref Conn;
+	flush = chan of (int, int, chan of int);
+	reqdone = chan of chan of (ref Tmsg, ref Conn, ref Fid);
+	spawn flushproc(flush);
+
+Serve:
+	for(;;)alt{
+	gm := <-tchan =>
+		if(gm == nil)
+			break Serve;
+		pick m := gm {
+		Readerror =>
+			sys->fprint(sys->fildes(2), "wmexport: fatal read error: %s\n", m.error);
+			break Serve;
+		Open =>
+			(fid, mode, d, err) := srv.canopen(m);
+			if(err != nil)
+				srv.reply(ref Rmsg.Error(m.tag, err));
+			else if(fid.qtype & Sys->QTDIR)
+				srv.default(m);
+			else
+				request(ctxt, m, fid);
+		Read =>
+			(fid, err) := srv.canread(m);
+			if(err != nil)
+				srv.reply(ref Rmsg.Error(m.tag, err));
+			else if(fid.qtype & Sys->QTDIR)
+				srv.read(m);
+			else
+				request(ctxt, m, fid);
+		Write =>
+			(fid, err) := srv.canwrite(m);
+			if(err != nil)
+				srv.reply(ref Rmsg.Error(m.tag, err));
+			else
+				request(ctxt, m, fid);
+		Flush =>
+			done := chan of int;
+			flush <-= (m.tag, m.oldtag, done);
+			<-done;
+		Clunk =>
+			request(ctxt, m, srv.clunk(m));
+		* =>
+			srv.default(gm);
+		}
+	rc := <-makeconn =>
+		if(nconns >= len conns)
+			conns = (array[len conns + 5] of ref Conn)[0:] = conns;
+		wm := wmlib->connect(ctxt);
+		if(wm == nil)				# XXX this can't happen - give wmlib->connect an error return
+			rc <-= (nil, "cannot connect");
+		else{
+			c := ref Conn(wm, nil, qidseq++, 0);
+			conns[nconns++] = c;
+			rc <-= (c, nil);
+		}
+	c := <-delconn =>
+		for(i := 0; i < nconns; i++)
+			if(conns[i] == c)
+				break;
+		nconns--;
+		if(i < nconns)
+			conns[i] = conns[nconns];
+		conns[nconns] = nil;
+	reqpool = <-reqdone :: reqpool =>
+		if(reqidle++ > Maxreqidle){
+			hd reqpool <-= (nil, nil, nil);
+			reqpool = tl reqpool;
+			reqidle--;
+		}
+	}
+	navops <-= nil;
+	kill(sys->pctl(0, nil), "killgrp");
+}
+
+nameimage(nil: ref Conn, img: ref Draw->Image): string
+{
+	if(img.iname != nil)
+		return img.iname;
+	for(i := 0; i < 100; i++){
+		s := "inferno." + string imgseq++;
+		if(img.name(s, 1) > 0)
+			return s;
+		if(img.iname != nil)
+			return img.iname;		# a competing process has done it for us.
+	}
+sys->print("wmexport: no image names: %r\n");
+raise "panic";
+}
+
+request(nil: ref Draw->Context, m: ref Styx->Tmsg, fid: ref Fid)
+{
+	n := int fid.path >> Shift;
+	conn: ref Conn;
+	for(i := 0; i < nconns; i++){
+		if(conns[i].n == n){
+			conn = conns[i];
+			break;
+		}
+	}
+	c: chan of (ref Tmsg, ref Conn, ref Fid);
+	if(reqpool == nil){
+		c = chan of (ref Tmsg, ref Conn, ref Fid);
+		spawn requestproc(c);
+	}else{
+		(c, reqpool) = (hd reqpool, tl reqpool);
+		reqidle--;
+	}
+	c <-= (m, conn, fid);
+}
+
+requestproc(req: chan of (ref Tmsg, ref Conn, ref Fid))
+{
+	pid := sys->pctl(0, nil);
+	for(;;){
+		(gm, c, fid) := <-req;
+		if(gm == nil)
+			break;
+		pidregister <-= (pid, gm.tag);
+		path := int fid.path;
+		pick m := gm {
+		Read =>
+			if(c == nil)
+				srv.replydirect(ref Rmsg.Error(m.tag, "connection is dead"));
+			case path & Mask {
+			Qwmctl =>
+				# first read gets number of connection.
+				m.offset = big 0;
+				if(c.nreads++ == 0)
+					srv.replydirect(styxservers->readstr(m, string c.n));
+				else
+					srv.replydirect(styxservers->readstr(m, <-c.wm.ctl));
+			Qptr =>
+				m.offset = big 0;
+				p := <-c.wm.ptr;
+				srv.replydirect(styxservers->readbytes(m,
+					sys->aprint("m%11d %11d %11d %11ud ", p.xy.x, p.xy.y, p.buttons, p.msec)));
+			Qkbd =>
+				m.offset = big 0;
+				s := "";
+				s[0] = <-c.wm.kbd;
+				srv.replydirect(styxservers->readstr(m, s));
+			Qwinname =>
+				m.offset = big 0;
+				srv.replydirect(styxservers->readstr(m, c.iname));
+			* =>
+				srv.replydirect(ref Rmsg.Error(m.tag, "what was i thinking1?"));
+			}
+		Write =>
+			if(c == nil)
+				srv.replydirect(ref Rmsg.Error(m.tag, "connection is dead"));
+			case path & Mask {
+			Qwmctl =>
+				if(sys->write(c.wm.connfd, m.data, len m.data) == -1){
+					srv.replydirect(ref Rmsg.Error(m.tag, sys->sprint("%r")));
+					break;
+				}
+				if(len m.data > 0 && int m.data[0] == '!'){
+					i := <-c.wm.images;
+					if(i == nil)
+						i = <-c.wm.images;
+					c.iname = nameimage(c, i);
+				}
+				srv.replydirect(ref Rmsg.Write(m.tag, len m.data));
+			* =>
+				srv.replydirect(ref Rmsg.Error(m.tag, "what was i thinking2?"));
+			}
+		Open =>
+			if(c == nil && path != Qclone)
+				srv.replydirect(ref Rmsg.Error(m.tag, "connection is dead"));
+			err: string;
+			q := qid(path);
+			case path & Mask {
+			Qclone =>
+				cch := chan of (ref Conn, string);
+				makeconn <-= cch;
+				(c, err) = <-cch;
+				if(c != nil)
+					q = qid(Qwmctl | (c.n << Shift));
+			Qptr =>
+				if(sys->fprint(c.wm.connfd, "start ptr") == -1)
+					err = sys->sprint("%r");
+			Qkbd =>
+				if(sys->fprint(c.wm.connfd, "start kbd") == -1)
+					err = sys->sprint("%r");
+			Qwmctl =>
+				;
+			Qwinname =>
+				;
+			* =>
+				err = "what was i thinking3?";
+			}
+			if(err != nil)
+				srv.replydirect(ref Rmsg.Error(m.tag, err));
+			else{
+				srv.replydirect(ref Rmsg.Open(m.tag, q, 0));
+				fid.open(m.mode, q);
+			}
+		Clunk =>
+			case path & Mask {
+			Qwmctl =>
+				if(c != nil)
+					delconn <-= c;
+			}
+		* =>
+			srv.replydirect(ref Rmsg.Error(gm.tag, "oh dear"));	
+		}
+		pidregister <-= (pid, -1);
+		reqdone <-= req;
+	}
+}
+
+qid(path: int): Sys->Qid
+{
+	return dirgen(path).t0.qid;
+}
+		
+replyproc(c: chan of ref Rmsg, replydone: chan of chan of ref Rmsg)
+{
+	# hmm, this could still send a reply out-of-order with a flush
+	while((m := <-c) != nil){
+		srv.replydirect(m);
+		replydone <-= c;
+	}
+}
+
+# deal with reply messages coming from styxservers.
+replymarshal(c: chan of ref Styx->Rmsg)
+{
+	replypool: list of chan of ref Rmsg;
+	n := 0;
+	replydone := chan of chan of ref Rmsg;
+	for(;;) alt{
+	m := <-c =>
+		c: chan of ref Rmsg;
+		if(replypool == nil){
+			c = chan of ref Rmsg;
+			spawn replyproc(c, replydone);
+		}else{
+			(c, replypool) = (hd replypool, tl replypool);
+			n--;
+		}
+		c <-= m;
+	replypool = <-replydone :: replypool =>
+		if(++n > Maxreplyidle){
+			hd replypool <-= nil;
+			replypool = tl replypool;
+			n--;
+		}
+	}
+}
+
+navigator(navops: chan of ref Navop)
+{
+	while((m := <-navops) != nil){
+		path := int m.path;
+		pick n := m {
+		Stat =>
+			n.reply <-= dirgen(int n.path);
+		Walk =>
+			name := n.name;
+			case path & Mask {
+			Qdir =>
+				dp := path & ~Mask;
+				case name {
+				".." =>
+					path = Qroot;
+				"wmctl" =>
+					path = Qwmctl | dp;
+				"pointer" =>
+					path = Qptr | dp;
+				"keyboard" =>
+					path = Qkbd | dp;
+				"winname" =>
+					path = Qwinname | dp;
+				* =>
+					path = Qerror;
+				}
+			Qroot =>
+				case name{
+				"clone" =>
+					path = Qclone;
+				* =>
+					x := int name;
+					path = Qerror;
+					if(string x == name){
+						for(i := 0; i < nconns; i++)
+							if(conns[i].n == x){
+								path = (x << Shift) | Qdir;
+								break;
+							}
+					}
+				}
+			}
+			n.reply <-= dirgen(path);
+		Readdir =>
+			err := "";
+			d: array of int;
+			case path & Mask {
+			Qdir =>
+				d = array[] of {Qwmctl, Qptr, Qkbd, Qwinname};
+				for(i := 0; i < len d; i++)
+					d[i] |= path & ~Mask;
+			Qroot =>
+				d = array[nconns + 1] of int;
+				d[0] = Qclone;
+				for(i := 0; i < nconns; i++)
+					d[i + 1] = (conns[i].n<<Shift) | Qdir;
+			}
+			if(d == nil){
+				n.reply <-= (nil, Enotdir);
+				break;
+			}
+			for (i := n.offset; i < len d; i++)
+				n.reply <-= dirgen(d[i]);
+			n.reply <-= (nil, nil);
+		}
+	}
+}
+
+dirgen(path: int): (ref Sys->Dir, string)
+{
+	name: string;
+	perm: int;
+	case path & Mask {
+	Qroot =>
+		name = ".";
+		perm = 8r555|Sys->DMDIR;
+	Qdir =>
+		name = string (path >> Shift);
+		perm = 8r555|Sys->DMDIR;
+	Qclone =>
+		name = "clone";
+		perm = 8r666;
+	Qwmctl =>
+		name = "wmctl";
+		perm = 8r666;
+	Qptr =>
+		name = "pointer";
+		perm = 8r444;
+	Qkbd =>
+		name = "keyboard";
+		perm = 8r444;
+	Qwinname =>
+		name = "winname";
+		perm = 8r444;
+	* =>
+		return (nil, Enotfound);
+	}
+	return (dir(path, name, perm), nil);
+}
+
+dir(path: int, name: string, perm: int): ref Sys->Dir
+{
+	d := ref sys->zerodir;
+	d.qid.path = big path;
+	if(perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	d.mode = perm;
+	d.name = name;
+	d.uid = user;
+	d.gid = user;
+	return d;
+}
+
+flushproc(flush: chan of (int, int, chan of int))
+{
+	a: array of (int, int);		# (pid, tag)
+	n := 0;
+	for(;;)alt{
+	(pid, tag) := <-pidregister =>
+		if(tag == -1){
+			for(i := 0; i < n; i++)
+				if(a[i].t0 == pid)
+					break;
+			n--;
+			if(i < n)
+				a[i] = a[n];
+		}else{
+			if(n >= len a){
+				na := array[n + 5] of (int, int);
+				na[0:] = a;
+				a = na;
+			}
+			a[n++] = (pid, tag);
+		}
+	(tag, oldtag, done) := <-flush =>
+		for(i := 0; i < n; i++)
+			if(a[i].t1 == oldtag){
+				spawn doflush(tag, a[i].t0, done);
+				break;
+			}
+		if(i == n)
+			spawn doflush(tag, -1, done);
+	}
+}
+
+doflush(tag: int, pid: int, done: chan of int)
+{
+	if(pid != -1){
+		kill(pid, "kill");
+		pidregister <-= (pid, -1);
+	}
+	srv.replydirect(ref Rmsg.Flush(tag));
+	done <-= 1;
+}
+
+# return number of characters from s that will fit into
+# max bytes when encoded as utf-8.
+fullutf(s: string, max: int): int
+{
+	Bit1:	con 7;
+	Bitx:	con 6;
+	Bit2:	con 5;
+	Bit3:	con 4;
+	Bit4:	con 3;
+	Rune1:	con (1<<(Bit1+0*Bitx))-1;		# 0000 0000 0111 1111
+	Rune2:	con (1<<(Bit2+1*Bitx))-1;		# 0000 0111 1111 1111
+	Rune3:	con (1<<(Bit3+2*Bitx))-1;		# 1111 1111 1111 1111
+	nb := 0;
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c <= Rune1)
+			nb += 1;
+		else if(c <= Rune2)
+			nb += 2;
+		else
+			nb += 3;
+		if(nb > max)
+			break;
+	}
+	return i;
+}
+
+kill(pid: int, note: string): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", note) < 0)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/appl/cmd/wmimport.b
@@ -1,0 +1,64 @@
+implement Wmimport;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+include "arg.m";
+include "wmlib.m";
+include "sh.m";
+
+# turn wmexport namespace into a Draw->Context.
+# usage: wmimport [-d /dev/draw] [-w /mnt/wm] cmd [arg...]
+
+Wmimport: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	wmlib := load Wmlib Wmlib->PATH;
+	wmlib->init();
+	sh := load Sh Sh->PATH;
+	arg := load Arg Arg->PATH;
+
+	devdraw := "/dev";
+	mntwm := "/mnt/wm";
+	arg->init(argv);
+	arg->setusage("wmimport [-d /dev] [-w /mnt/wm] cmd [arg...]");
+	while((opt := arg->opt()) != 0){
+		case opt{
+		'd' =>
+			devdraw = arg->earg();
+		'w' =>
+			mntwm = arg->earg();
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	if(argv == nil)
+		arg->usage();
+	arg = nil;
+	(ok, nil) := sys->stat(mntwm + "/clone");
+	if(ok == -1){
+		sys->fprint(sys->fildes(2), "wmimport: no wm at %s\n", mntwm);
+		raise "fail:no wm";
+	}
+	(ctxt, err) := wmlib->importdrawcontext(devdraw, mntwm);
+	if(ctxt == nil){
+		sys->fprint(sys->fildes(2), "wmimport: remote connect failed; %s\n", err);
+		raise "fail:error";
+	}
+
+	e := sh->run(ctxt, argv);
+	if(e != nil)
+		raise "fail:" + e;
+}
+
--- /dev/null
+++ b/appl/cmd/xargs.b
@@ -1,0 +1,86 @@
+# apply cmd to args list read from stdin
+# obc
+implement Xargs;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+Xargs: module
+{
+        init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+stderr: ref Sys->FD;
+
+usage()
+{
+	sys->fprint(stderr, "Usage: xargs command [command args] <[list of last command arg]\n");
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil){
+		sys->fprint(stderr, "xargs: can't load Bufio: %r\n");
+		exit;
+	}
+	if(args != nil)
+		args = tl args;
+	if (args == nil) {
+		usage();
+		return;
+	}
+	cmd := hd args;
+	args = tl args;
+	if(len cmd < 4 || cmd[len cmd -4:]!=".dis")
+		cmd += ".dis";
+	sh := load Command cmd;
+	if (sh == nil){
+		cmd = "/dis/"+cmd;
+		sh = load Command cmd;
+	}
+	if (sh == nil){
+		sys->fprint(stderr, "xargs: can't load %s: %r\n", cmd);
+		exit;
+	}
+
+	stdin := sys->fildes(0);
+	if(stdin == nil){
+		sys->fprint(stderr, "xargs: no standard input\n");
+		exit;
+	}
+	b := bufio->fopen(stdin, Bufio->OREAD);
+	while((t := b.gets('\n')) != nil){
+		(nil, rargs) := sys->tokenize(t, " \t\n");
+		if (rargs == nil)
+			continue;
+		if (args == nil)
+			rargs = cmd :: rargs;
+		else
+			rargs = append(cmd :: args, rargs);
+		sh->init(ctxt, rargs);		# BUG: process environment?
+	}
+}
+
+reverse[T](l: list of T): list of T
+{
+	t: list of T;
+	for(; l != nil; l = tl l)
+		t = hd l :: t;
+	return t;
+}
+
+append(h, t: list of string) : list of string
+{
+	r := reverse(h);
+	for(; r != nil; r = tl r)
+		t = hd r :: t;
+	return t;
+}
--- /dev/null
+++ b/appl/cmd/xd.b
@@ -1,0 +1,316 @@
+implement Xd;
+
+#
+# based on Plan9 xd
+#
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+Xd: module  
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+Iobuf : import bufio;
+stdin, stdout, stderr : ref Sys->FD;
+
+wbytes := array [] of {
+	1,
+	2,
+	4,
+	8,
+};
+fmtchars : con "odx";
+fmtbases := array [] of {
+	8,
+	10,
+	16,
+};
+fwidths := array [] of {
+	3,	# 1o
+	3,	# 1d
+	2,	# 1x
+	6,	# 2o
+	5,	# 2d
+	4,	# 2x
+	11,	# 4o
+	10,	# 4d
+	8,	# 4x
+	22,	# 8o
+	20,	# 8d
+	16,	# 8x
+};
+
+bytepos := array [16] of { * => 0 };
+
+formats := array [10] of (int, int, int);	# (nbytes, base, fieldwidth)
+nformats := 0;
+addrbase := 16;
+repeats := 0;
+swab := 0;
+flush := 0;
+addr := big 0;
+output : ref Iobuf;
+pad : string;
+
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdin  = sys->fildes(0);
+	stdout = sys->fildes(1);
+	stderr = sys->fildes(2);
+
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->fprint(stderr, "cannot load bufio: %r\n");
+		raise "fail:init";
+	}
+	output = bufio->fopen(stdout, Sys->OWRITE);
+	if (argv == nil)
+		raise "fail:bad argv";
+
+	pad = string array [32] of { * => byte ' ' };
+
+	for (argv = tl argv; argv != nil; argv = tl argv) {
+		arg := hd argv;
+		if (arg == nil)
+			continue;
+		if (arg[0] != '-')
+			break;
+
+		if (len arg == 2) {
+			case arg[1] {
+			'c' =>
+				addformat(0, 256);
+			'r' =>
+				repeats = 1;
+			's' =>
+				swab = 1;
+			'u' =>
+				flush = 1;
+			* =>
+				usage();
+			}
+			continue;
+		}
+		# XXX should allow -x1, -x
+		if (len arg == 3) {
+			n := 0;
+			baseix := strchr(fmtchars,arg[2]);
+			if (baseix == -1)
+				usage();
+			case arg[1] {
+			'a' =>
+				addrbase = fmtbases[baseix];
+				continue;
+			'b' or '1' =>	n = 0;
+			'w' or '2' =>	n = 1;
+			'l' or '4' =>	n = 2;
+			'v' or '8' =>	n = 3;
+			* =>
+				usage();
+			}
+			addformat(n, baseix);
+			continue;
+		}
+		usage();
+	}
+	if (nformats == 0)
+		addformat(2, 2);	# "4x"
+
+	if (argv == nil)
+		dump(nil, 0);
+	else if (tl argv == nil)
+		dump(hd argv, 0);
+	else {
+		for (; argv != nil; argv = tl argv) {
+			dump(hd argv, 1);
+		}
+	}
+}
+
+usage()
+{
+	sys->fprint(stderr, "usage: xd [-u] [-r] [-s] [-a{odx}] [-c|{b1w2l4v8}{odx}] ... file ...\n");
+	raise "fail:usage";
+}
+
+strchr(s : string, ch : int) : int
+{
+	for (ix := 0; ix < len s; ix++)
+		if (s[ix] == ch)
+			return ix;
+	return -1;
+}
+
+addformat(widix, baseix : int)
+{
+	nbytes := wbytes[widix];
+	if (nformats >= len formats) {
+		sys->fprint(stderr, "xd: too many formats\n");
+		raise "fail:error";
+	}
+	fw : int;
+	if (baseix == 256) {
+		# special -c case
+		formats[nformats++] = (nbytes, 256, 2);
+		fw = 2;
+	} else {
+		fw = fwidths[baseix + (widix *len fmtbases)];
+		formats[nformats++] = (nbytes, fmtbases[baseix], fw);
+	}
+	bpos := 0;
+	for (ix := 0; ix < 16; ix += nbytes) {
+		if (bytepos[ix] >= bpos)
+			bpos = bytepos[ix];
+		else {
+			d := bpos - bytepos[ix];
+			for (dix := ix; dix < 16; dix++)
+				bytepos[dix] += d;
+		}
+		bpos += fw + 1;
+	}
+}
+
+dump(path : string, title : int)
+{
+	input := bufio->fopen(stdin, Sys->OREAD);
+	zeros := array [16] of {* => byte 0};
+
+	if (path != nil) {
+		input = bufio->open(path, Sys->OREAD);
+		if (input == nil) {
+			sys->fprint(stderr, "xd: cannot open %s: %r\n", path);
+			raise "fail:cannot open";
+		}
+	}
+
+	if (title) {
+		output.puts(path);
+		output.putc('\n');
+	}
+
+	addr = big 0;
+	star := 0;
+	obuf: array of byte;
+
+	for (;;) {
+		n := 0;
+		buf := array [16] of byte;
+		while (n < 16 && (r := input.read(buf[n:], 16 - n)) > 0)
+			n += r;
+		if (n < 16)
+			buf[n:] = zeros[n:];
+		if (swab)
+			doswab(buf);
+		if (n == 16 && repeats) {
+			if (obuf != nil && buf[0]==obuf[0]) {
+				for (i := 0; i < 16; i++)
+					if (obuf[i] != buf[i])
+						break;
+				if (i == 16) {
+					addr += big 16;
+					if (star == 0) {
+						star++;
+						output.puts("*\n");
+					}
+					continue;
+				}
+			}
+			obuf = buf;
+			star = 0;
+		}
+		for (fmt := 0; fmt < nformats; fmt++) {
+			if (fmt == 0)
+				output.puts(big2str(addr, 7, addrbase, '0'));
+			else
+				output.puts(big2str(addr, 7, addrbase, ' '));
+			output.putc(' ');
+			(w, b, fw) := formats[fmt];
+			pdata(fw, w, b, n, buf);
+			output.putc('\n');
+			if (flush)
+				output.flush();
+		}
+		addr += big n;
+		if (n < 16) {
+			output.puts(big2str(addr, 7, addrbase, '0'));
+			output.putc('\n');
+			if (flush)
+				output.flush();
+			break;
+		}
+	}
+	output.flush();
+}
+
+hexchars : con "0123456789abcdef";
+
+big2str(b : big, minw, base, padc  : int) : string
+{
+	s := "";
+	do {
+		d := int (b % big base);
+		s[len s] = hexchars[d];
+		b /= big base;
+	} while (b > big 0);
+	t := "";
+	if (len s < minw)
+		t = string array [minw] of { * => byte padc };
+	else
+		t = s;
+	for (i := len s - 1; i >= 0; i--)
+		t[len t - 1 - i] = s[i];
+	return t;
+}
+
+pdata(fw, n, base, dlen : int, data : array of byte)
+{
+	nout := 0;
+	text := "";
+
+	for (i := 0; i < dlen; i += n) {
+		if (i != 0) {
+			padlen := bytepos[i] - nout;
+			output.puts(pad[0:padlen]);
+			nout += padlen;
+		}
+		if (base == 256) {
+			# special -c case
+			ch := int data[i];
+			case ch {
+			'\t' =>	text = "\\t";
+			'\r' =>	text = "\\r";
+			'\n' =>	text = "\\n";
+			'\b' =>	text = "\\b";
+			* =>
+				if (ch >= 16r7f || ' ' > ch)
+					text = sys->sprint("%.2x", ch);
+				else
+					text = sys->sprint("%c", ch);
+			}
+		} else {
+			v := big data[i];
+			for (ix := 1; ix < n; ix++)
+				v = (v << 8) + big data[i+ix];
+			text = big2str(v, fw, base, '0');
+		}
+		output.puts(text);
+		nout += len text;
+	}
+}
+
+doswab(b : array of byte)
+{
+	ix := 0;
+	for (i := 0; i < 4; i++) {
+		(b[ix], b[ix+3]) = (b[ix+3], b[ix]);
+		(b[ix+1], b[ix+2]) = (b[ix+2], b[ix+1]);
+		ix += 4;
+	}
+}
--- /dev/null
+++ b/appl/cmd/yacc.b
@@ -1,0 +1,2809 @@
+implement Yacc;
+
+include "sys.m";
+	sys: Sys;
+	print, fprint, sprint: import sys;
+	UTFmax: import Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "draw.m";
+
+Yacc: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Arg: adt
+{
+	argv:	list of string;
+	c:	int;
+	opts:	string;
+
+	init:	fn(argv: list of string): ref Arg;
+	opt:	fn(arg: self ref Arg): int;
+	arg:	fn(arg: self ref Arg): string;
+};
+
+PARSER:		con "/lib/yaccpar";
+OFILE:		con "tab.b";
+FILEU:		con "output";
+FILED:		con "tab.m";
+FILEDEBUG:	con "debug";
+
+# the following are adjustable
+# according to memory size
+ACTSIZE:	con 30000;
+NSTATES:	con 2000;
+TEMPSIZE:	con 2000;
+
+SYMINC:		con 50;				# increase for non-term or term
+RULEINC:	con 50;				# increase for max rule length	prodptr[i]
+PRODINC:	con 100;			# increase for productions	prodptr
+WSETINC:	con 50;				# increase for working sets	wsets
+STATEINC:	con 200;			# increase for states		statemem
+
+NAMESIZE:	con 50;
+NTYPES:		con 63;
+ISIZE:		con 400;
+
+PRIVATE:	con 16rE000;			# unicode private use
+
+# relationships which must hold:
+#	TEMPSIZE >= NTERMS + NNONTERM + 1
+#	TEMPSIZE >= NSTATES
+#
+
+NTBASE:		con 8r10000;
+ERRCODE:	con 8190;
+ACCEPTCODE:	con 8191;
+YYLEXUNK:	con 3;
+TOKSTART:	con 4;				#index of first defined token
+
+# no, left, right, binary assoc.
+NOASC, LASC, RASC, BASC: con iota;
+
+# flags for state generation
+DONE, MUSTDO, MUSTLOOKAHEAD: con iota;
+
+# flags for a rule having an action, and being reduced
+ACTFLAG:	con 16r4;
+REDFLAG:	con 16r8;
+
+# output parser flags
+YYFLAG1:	con -1000;
+
+# parse tokens
+IDENTIFIER, MARK, TERM, LEFT, RIGHT, BINARY, PREC, LCURLY, IDENTCOLON, NUMBER, START, TYPEDEF, TYPENAME, MODULE: con PRIVATE+iota;
+
+ENDFILE:	con 0;
+
+EMPTY:		con 1;
+WHOKNOWS:	con 0;
+OK:		con 1;
+NOMORE:		con -1000;
+
+# macros for getting associativity and precedence levels
+ASSOC(i: int): int
+{
+	return i & 3;
+}
+
+PLEVEL(i: int): int
+{
+	return (i >> 4) & 16r3f;
+}
+
+TYPE(i: int): int
+{
+	return (i >> 10) & 16r3f;
+}
+
+# macros for setting associativity and precedence levels
+SETASC(i, j: int): int
+{
+	return i | j;
+}
+
+SETPLEV(i, j: int): int
+{
+	return i | (j << 4);
+}
+
+SETTYPE(i, j: int): int
+{
+	return i | (j << 10);
+}
+
+# I/O descriptors
+stderr:		ref Sys->FD;
+fdefine:	ref Iobuf;			# file for module definition
+fdebug:		ref Iobuf;			# y.debug for strings for debugging
+ftable:		ref Iobuf;			# y.tab.c file
+finput:		ref Iobuf;			# input file
+foutput:	ref Iobuf;			# y.output file
+
+CodeData, CodeMod, CodeAct: con iota;
+NCode:	con 8192;
+
+Code: adt
+{
+	kind:	int;
+	data:	array of byte;
+	ndata:	int;
+	next:	cyclic ref Code;
+};
+
+codehead:	ref Code;
+codetail:	ref Code;
+
+modname:	string;				# name of module
+suppressmod: 	int;					# suppress module definition
+stacksize := 200;
+
+# communication variables between various I/O routines
+infile:		string;				# input file name
+numbval:	int;				# value of an input number
+tokname:	string;				# input token name, slop for runes and 0
+
+# structure declarations
+Lkset: type array of int;
+
+Pitem: adt
+{
+	prod:	array of int;
+	off:	int;				# offset within the production
+	first:	int;				# first term or non-term in item
+	prodno:	int;				# production number for sorting
+};
+
+Item: adt
+{
+	pitem:	Pitem;
+	look:	Lkset;
+};
+
+Symb: adt
+{
+	name:	string;
+	value:	int;
+};
+
+Wset: adt
+{
+	pitem:	Pitem;
+	flag:	int;
+	ws:	Lkset;
+};
+
+	# storage of names
+
+parser :=	PARSER;
+yydebug:	string;
+
+	# storage of types
+ntypes:		int;				# number of types defined
+typeset :=	array[NTYPES] of string;	# pointers to type tags
+
+	# token information
+
+ntokens :=	0;				# number of tokens
+tokset:		array of Symb;
+toklev:		array of int;			# vector with the precedence of the terminals
+
+	# nonterminal information
+
+nnonter :=	-1;				# the number of nonterminals
+nontrst:	array of Symb;
+start:		int;				# start symbol
+
+	# state information
+
+nstate := 	0;				# number of states
+pstate :=	array[NSTATES+2] of int;	# index into statemem to the descriptions of the states
+statemem :	array of Item;
+tystate :=	array[NSTATES] of int;		# contains type information about the states
+tstates :	array of int;			# states generated by terminal gotos
+ntstates :	array of int; 			# states generated by nonterminal gotos
+mstates :=	array[NSTATES] of {* => 0};	# chain of overflows of term/nonterm generation lists
+lastred: 	int; 				# number of last reduction of a state
+defact :=	array[NSTATES] of int;		# default actions of states
+
+	# lookahead set information
+
+lkst: array of Lkset;
+nolook := 0;					# flag to turn off lookahead computations
+tbitset := 0;					# size of lookahead sets
+clset: Lkset;  					# temporary storage for lookahead computations
+
+	# working set information
+
+wsets:	array of Wset;
+cwp:	int;
+
+	# storage for action table
+
+amem:	array of int;				# action table storage
+memp:	int;					# next free action table position
+indgo := array[NSTATES] of int;			# index to the stored goto table
+
+	# temporary vector, indexable by states, terms, or ntokens
+
+temp1 :=	array[TEMPSIZE] of int;		# temporary storage, indexed by terms + ntokens or states
+lineno :=	1;				# current input line number
+fatfl :=	1;  				# if on, error is fatal
+nerrors :=	0;				# number of errors
+
+	# assigned token type values
+extval :=	0;
+
+ytabc :=	OFILE;	# name of y.tab.c
+
+	# grammar rule information
+
+nprod := 1;					# number of productions
+prdptr: array of array of int;			# pointers to descriptions of productions
+levprd: array of int;				# precedence levels for the productions
+rlines: array of int;				# line number for this rule
+
+
+	# statistics collection variables
+
+zzgoent := 0;
+zzgobest := 0;
+zzacent := 0;
+zzexcp := 0;
+zzclose := 0;
+zzrrconf := 0;
+zzsrconf := 0;
+zzstate := 0;
+
+	# optimizer arrays
+yypgo:	array of array of int;
+optst:	array of array of int;
+ggreed:	array of int;
+pgo:	array of int;
+
+maxspr: int;  		# maximum spread of any entry
+maxoff: int;  		# maximum offset into a array
+maxa:	int;
+
+	# storage for information about the nonterminals
+
+pres: array of array of array of int;		# vector of pointers to productions yielding each nonterminal
+pfirst: array of Lkset;
+pempty:	array of int;				# vector of nonterminals nontrivially deriving e
+	# random stuff picked out from between functions
+
+indebug	:= 0;		# debugging flag for cpfir
+pidebug	:= 0;		# debugging flag for putitem
+gsdebug	:= 0;		# debugging flag for stagen
+cldebug	:= 0;		# debugging flag for closure
+pkdebug	:= 0;		# debugging flag for apack
+g2debug	:= 0;		# debugging for go2gen
+adb	:= 0;		# debugging for callopt
+
+Resrv : adt
+{
+	name:	string;
+	value:	int;
+};
+
+resrv := array[] of {
+	Resrv("binary",		BINARY),
+	Resrv("module",		MODULE),
+	Resrv("left",		LEFT),
+	Resrv("nonassoc",	BINARY),
+	Resrv("prec",		PREC),
+	Resrv("right",		RIGHT),
+	Resrv("start",		START),
+	Resrv("term",		TERM),
+	Resrv("token",		TERM),
+	Resrv("type",		TYPEDEF),};
+
+zznewstate := 0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	stderr = sys->fildes(2);
+
+	setup(argv);		# initialize and read productions
+
+	tbitset = (ntokens+32)/32;
+	cpres();		# make table of which productions yield a given nonterminal
+	cempty();		# make a table of which nonterminals can match the empty string
+	cpfir();		# make a table of firsts of nonterminals
+
+	stagen();		# generate the states
+
+	yypgo = array[nnonter+1] of array of int;
+	optst = array[nstate] of array of int;
+	output();		# write the states and the tables
+	go2out();
+
+	hideprod();
+	summary();
+
+	callopt();
+
+	others();
+
+	if(fdefine != nil)
+		fdefine.close();
+	if(fdebug != nil)
+		fdebug.close();
+	if(ftable != nil)
+		ftable.close();
+	if(foutput != nil)
+		foutput.close();
+}
+
+setup(argv: list of string)
+{
+	j, ty: int;
+
+	ytab := 0;
+	vflag := 0;
+	dflag := 0;
+	stem := 0;
+	stemc := "y";
+	foutput = nil;
+	fdefine = nil;
+	fdebug = nil;
+	arg := Arg.init(argv);
+	while(c := arg.opt()){
+		case c{
+		'v' or 'V' =>
+			vflag++;
+		'D' =>
+			yydebug = arg.arg();
+		'd' =>
+			dflag++;
+		'n' =>
+			stacksize = int arg.arg();
+		'o' =>
+			ytab++;
+			ytabc = arg.arg();
+		's' =>
+			stem++;
+			stemc = arg.arg();
+		'm' =>
+			suppressmod++;
+		* =>
+			usage();
+		}
+	}
+	argv = arg.argv;
+	if(len argv != 1)
+		usage();
+	if (suppressmod && dflag) {
+		sys->fprint(stderr, "yacc: -m and -d are exclusive\n");
+		usage();
+	}
+	if (stacksize < 1) {
+		sys->fprint(stderr, "yacc: stack size too small\n");
+		usage();
+	}
+	infile = hd argv;
+	finput = bufio->open(infile, Bufio->OREAD);
+	if(finput == nil)
+		error("cannot open '"+infile+"'");
+
+	openup(stemc, dflag, vflag, ytab, ytabc);
+
+	defin(0, "$end");
+	extval = PRIVATE;	# tokens start in unicode 'private use'
+	defin(0, "error");
+	defin(1, "$accept");
+	defin(0, "$unk");
+	i := 0;
+
+	for(t := gettok(); t != MARK && t != ENDFILE; )
+	case t {
+	';' =>
+		t = gettok();
+
+	START =>
+		if(gettok() != IDENTIFIER)
+			error("bad %%start construction");
+		start = chfind(1, tokname);
+		t = gettok();
+
+	TYPEDEF =>
+		if(gettok() != TYPENAME)
+			error("bad syntax in %%type");
+		ty = numbval;
+		for(;;) {
+			t = gettok();
+			case t {
+			IDENTIFIER =>
+				if((t=chfind(1, tokname)) < NTBASE) {
+					j = TYPE(toklev[t]);
+					if(j != 0 && j != ty)
+						error("type redeclaration of token "+
+							tokset[t].name);
+					else
+						toklev[t] = SETTYPE(toklev[t], ty);
+				} else {
+					j = nontrst[t-NTBASE].value;
+					if(j != 0 && j != ty)
+						error("type redeclaration of nonterminal "+
+							nontrst[t-NTBASE].name);
+					else
+						nontrst[t-NTBASE].value = ty;
+				}
+				continue;
+			',' =>
+				continue;
+			';' =>
+				t = gettok();
+			}
+			break;
+		}
+
+	MODULE =>
+		cpymodule();
+		t = gettok();
+
+	LEFT or BINARY or RIGHT or TERM =>
+		# nonzero means new prec. and assoc.
+		lev := t-TERM;
+		if(lev)
+			i++;
+		ty = 0;
+
+		# get identifiers so defined
+		t = gettok();
+
+		# there is a type defined
+		if(t == TYPENAME) {
+			ty = numbval;
+			t = gettok();
+		}
+		for(;;) {
+			case t {
+			',' =>
+				t = gettok();
+				continue;
+
+			';' =>
+				break;
+
+			IDENTIFIER =>
+				j = chfind(0, tokname);
+				if(j >= NTBASE)
+					error(tokname+" defined earlier as nonterminal");
+				if(lev) {
+					if(ASSOC(toklev[j]))
+						error("redeclaration of precedence of "+tokname);
+					toklev[j] = SETASC(toklev[j], lev);
+					toklev[j] = SETPLEV(toklev[j], i);
+				}
+				if(ty) {
+					if(TYPE(toklev[j]))
+						error("redeclaration of type of "+tokname);
+					toklev[j] = SETTYPE(toklev[j],ty);
+				}
+				t = gettok();
+				if(t == NUMBER) {
+					tokset[j].value = numbval;
+					t = gettok();
+				}
+				continue;
+			}
+			break;
+		}
+
+	LCURLY =>
+		cpycode();
+		t = gettok();
+
+	* =>
+		error("syntax error");
+	}
+	if(t == ENDFILE)
+		error("unexpected EOF before %%");
+	if(modname == nil)
+		error("missing %module specification");
+
+	moreprod();
+	prdptr[0] = array[4] of {
+		NTBASE,		# added production
+		start,		# if start is 0, we will overwrite with the lhs of the first rule
+		1,
+		0
+	};
+	nprod = 1;
+	curprod := array[RULEINC] of int;
+	t = gettok();
+	if(t != IDENTCOLON)
+		error("bad syntax on first rule");
+
+	if(!start)
+		prdptr[0][1] = chfind(1, tokname);
+
+	# read rules
+	# put into prdptr array in the format
+	# target
+	# followed by id's of terminals and non-terminals
+	# followd by -nprod
+	while(t != MARK && t != ENDFILE) {
+		mem := 0;
+		# process a rule
+		rlines[nprod] = lineno;
+		if(t == '|')
+			curprod[mem++] = prdptr[nprod-1][0];
+		else if(t == IDENTCOLON) {
+			curprod[mem] = chfind(1, tokname);
+			if(curprod[mem] < NTBASE)
+				error("token illegal on LHS of grammar rule");
+			mem++;
+		} else
+			error("illegal rule: missing semicolon or | ?");
+
+		# read rule body
+		t = gettok();
+
+		for(;;){
+			while(t == IDENTIFIER) {
+				curprod[mem] = chfind(1, tokname);
+				if(curprod[mem] < NTBASE)
+					levprd[nprod] = toklev[curprod[mem]];
+				mem++;
+				if(mem >= len curprod){
+					ncurprod := array[mem+RULEINC] of int;
+					ncurprod[0:] = curprod;
+					curprod = ncurprod;
+				}
+				t = gettok();
+			}
+			if(t == PREC) {
+				if(gettok() != IDENTIFIER)
+					error("illegal %%prec syntax");
+				j = chfind(2, tokname);
+				if(j >= NTBASE)
+					error("nonterminal "+nontrst[j-NTBASE].name+" illegal after %%prec");
+				levprd[nprod] = toklev[j];
+				t = gettok();
+			}
+			if(t != '=')
+				break;
+			levprd[nprod] |= ACTFLAG;
+			addcode(CodeAct, "\n"+string nprod+"=>");
+			cpyact(curprod, mem);
+
+			# action within rule...
+			if((t=gettok()) == IDENTIFIER) {
+				# make it a nonterminal
+				j = chfind(1, "$$"+string nprod);
+
+				#
+				# the current rule will become rule number nprod+1
+				# enter null production for action
+				#
+				prdptr[nprod] = array[2] of {j, -nprod};
+
+				# update the production information
+				nprod++;
+				moreprod();
+				levprd[nprod] = levprd[nprod-1] & ~ACTFLAG;
+				levprd[nprod-1] = ACTFLAG;
+				rlines[nprod] = lineno;
+
+				# make the action appear in the original rule
+				curprod[mem++] = j;
+				if(mem >= len curprod){
+					ncurprod := array[mem+RULEINC] of int;
+					ncurprod[0:] = curprod;
+					curprod = ncurprod;
+				}
+			}
+		}
+
+		while(t == ';')
+			t = gettok();
+		curprod[mem++] = -nprod;
+
+		# check that default action is reasonable
+		if(ntypes && !(levprd[nprod]&ACTFLAG) && nontrst[curprod[0]-NTBASE].value) {
+			# no explicit action, LHS has value
+
+			tempty := curprod[1];
+			if(tempty < 0)
+				error("must return a value, since LHS has a type");
+			else
+				if(tempty >= NTBASE)
+					tempty = nontrst[tempty-NTBASE].value;
+				else
+					tempty = TYPE(toklev[tempty]);
+			if(tempty != nontrst[curprod[0]-NTBASE].value)
+				error("default action causes potential type clash");
+			else{
+				addcodec(CodeAct, '\n');
+				addcode(CodeAct, string nprod);
+				addcode(CodeAct, "=>\nyyval.");
+				addcode(CodeAct, typeset[tempty]);
+				addcode(CodeAct, " = yys[yyp+1].yyv.");
+				addcode(CodeAct, typeset[tempty]);
+				addcodec(CodeAct, ';');
+			}
+		}
+		moreprod();
+		prdptr[nprod] = array[mem] of int;
+		prdptr[nprod][0:] = curprod[:mem];
+		nprod++;
+		moreprod();
+		levprd[nprod] = 0;
+	}
+
+	#
+	# end of all rules
+	# dump out the prefix code
+	#
+	ftable.puts("implement ");
+	ftable.puts(modname);
+	ftable.puts(";\n");
+
+	dumpcode(CodeMod);
+	dumpmod();
+	dumpcode(CodeAct);
+
+	ftable.puts("YYEOFCODE: con 1;\n");
+	ftable.puts("YYERRCODE: con 2;\n");
+	ftable.puts("YYMAXDEPTH: con " + string stacksize + ";\n");	# was 150
+	#ftable.puts("yyval: YYSTYPE;\n");
+
+	#
+	# copy any postfix code
+	#
+	if(t == MARK) {
+		ftable.puts("\n#line\t");
+		ftable.puts(string lineno);
+		ftable.puts("\t\"");
+		ftable.puts(infile);
+		ftable.puts("\"\n");
+		while((c=finput.getc()) != Bufio->EOF)
+			ftable.putc(c);
+	}
+	finput.close();
+}
+
+#
+# allocate enough room to hold another production
+#
+moreprod()
+{
+	n := len prdptr;
+	if(nprod < n)
+		return;
+	n += PRODINC;
+	aprod := array[n] of array of int;
+	aprod[0:] = prdptr;
+	prdptr = aprod;
+
+	alevprd := array[n] of int;
+	alevprd[0:] = levprd;
+	levprd = alevprd;
+
+	arlines := array[n] of int;
+	arlines[0:] = rlines;
+	rlines = arlines;
+}
+
+#
+# define s to be a terminal if t=0
+# or a nonterminal if t=1
+#
+defin(nt: int, s: string): int
+{
+	val := 0;
+	if(nt) {
+		nnonter++;
+		if(nnonter >= len nontrst){
+			anontrst := array[nnonter + SYMINC] of Symb;
+			anontrst[0:] = nontrst;
+			nontrst = anontrst;
+		}
+		nontrst[nnonter] = Symb(s, 0);
+		return NTBASE + nnonter;
+	}
+
+	# must be a token
+	ntokens++;
+	if(ntokens >= len tokset){
+		atokset := array[ntokens + SYMINC] of Symb;
+		atokset[0:] = tokset;
+		tokset = atokset;
+
+		atoklev := array[ntokens + SYMINC] of int;
+		atoklev[0:] = toklev;
+		toklev = atoklev;
+	}
+	tokset[ntokens].name = s;
+	toklev[ntokens] = 0;
+
+	# establish value for token
+	# single character literal
+	if(s[0] == ' ' && len s == 1+1){
+		val = s[1];
+	}else if(s[0] == ' ' && s[1] == '\\') { # escape sequence
+		if(len s == 2+1) {
+			# single character escape sequence
+			case s[2] {
+			'\'' =>	val = '\'';
+			'"' =>	val = '"';
+			'\\' =>	val = '\\';
+			'a' =>	val = '\a';
+			'b' =>	val = '\b';
+			'n' =>	val = '\n';
+			'r' =>	val = '\r';
+			't' =>	val = '\t';
+			'v' =>	val = '\v';
+			* =>
+				error("invalid escape "+s[1:3]);
+			}
+		}else if(s[2] == 'u' && len s == 2+1+4) { # \unnnn sequence
+			val = 0;
+			s = s[3:];
+			while(s != ""){
+				c := s[0];
+				if(c >= '0' && c <= '9')
+					c -= '0';
+				else if(c >= 'a' && c <= 'f')
+					c -= 'a' - 10;
+				else if(c >= 'A' && c <= 'F')
+					c -= 'A' - 10;
+				else
+					error("illegal \\unnnn construction");
+				val = val * 16 + c;
+				s = s[1:];
+			}
+			if(val == 0)
+				error("'\\u0000' is illegal");
+		}else
+			error("unknown escape");
+	}else
+		val = extval++;
+
+	tokset[ntokens].value = val;
+	return ntokens;
+}
+
+peekline := 0;
+gettok(): int
+{
+	i, match, c: int;
+
+	tokname = "";
+	for(;;){
+		lineno += peekline;
+		peekline = 0;
+		c = finput.getc();
+		while(c == ' ' || c == '\n' || c == '\t' || c == '\v' || c == '\r') {
+			if(c == '\n')
+				lineno++;
+			c = finput.getc();
+		}
+
+		# skip comment
+		if(c != '#')
+			break;
+		lineno += skipcom();
+	}
+	case c {
+	Bufio->EOF =>
+		return ENDFILE;
+
+	'{' =>
+		finput.ungetc();
+		return '=';
+
+	'<' =>
+		# get, and look up, a type name (union member name)
+		i = 0;
+		while((c=finput.getc()) != '>' && c != Bufio->EOF && c != '\n')
+			tokname[i++] = c;
+		if(c != '>')
+			error("unterminated < ... > clause");
+		for(i=1; i<=ntypes; i++)
+			if(typeset[i] == tokname) {
+				numbval = i;
+				return TYPENAME;
+			}
+		ntypes++;
+		numbval = ntypes;
+		typeset[numbval] = tokname;
+		return TYPENAME;
+
+	'"' or '\'' =>
+		match = c;
+		tokname[0] = ' ';
+		i = 1;
+		for(;;) {
+			c = finput.getc();
+			if(c == '\n' || c == Bufio->EOF)
+				error("illegal or missing ' or \"" );
+			if(c == '\\') {
+				tokname[i++] = '\\';
+				c = finput.getc();
+			} else if(c == match)
+				return IDENTIFIER;
+			tokname[i++] = c;
+		}
+
+	'%' =>
+		case c = finput.getc(){
+		'%' =>	return MARK;
+		'=' =>	return PREC;
+		'{' =>	return LCURLY;
+		}
+
+		getword(c);
+		# find a reserved word
+		for(c=0; c < len resrv; c++)
+			if(tokname == resrv[c].name)
+				return resrv[c].value;
+		error("invalid escape, or illegal reserved word: "+tokname);
+
+	'0' to '9' =>
+		numbval = c - '0';
+		while(isdigit(c = finput.getc()))
+			numbval = numbval*10 + c-'0';
+		finput.ungetc();
+		return NUMBER;
+
+	* =>
+		if(isword(c) || c=='.' || c=='$')
+			getword(c);
+		else
+			return c;
+	}
+
+	# look ahead to distinguish IDENTIFIER from IDENTCOLON
+	c = finput.getc();
+	while(c == ' ' || c == '\t'|| c == '\n' || c == '\v' || c == '\r' || c == '#') {
+		if(c == '\n')
+			peekline++;
+		# look for comments
+		if(c == '#')
+			peekline += skipcom();
+		c = finput.getc();
+	}
+	if(c == ':')
+		return IDENTCOLON;
+	finput.ungetc();
+	return IDENTIFIER;
+}
+
+getword(c: int)
+{
+	i := 0;
+	while(isword(c) || isdigit(c) || c == '_' || c=='.' || c=='$') {
+		tokname[i++] = c;
+		c = finput.getc();
+	}
+	finput.ungetc();
+}
+
+#
+# determine the type of a symbol
+#
+fdtype(t: int): int
+{
+	v : int;
+	s: string;
+
+	if(t >= NTBASE) {
+		v = nontrst[t-NTBASE].value;
+		s = nontrst[t-NTBASE].name;
+	} else {
+		v = TYPE(toklev[t]);
+		s = tokset[t].name;
+	}
+	if(v <= 0)
+		error("must specify type for "+s);
+	return v;
+}
+
+chfind(t: int, s: string): int
+{
+	if(s[0] == ' ')
+		t = 0;
+	for(i:=0; i<=ntokens; i++)
+		if(s == tokset[i].name)
+			return i;
+	for(i=0; i<=nnonter; i++)
+		if(s == nontrst[i].name)
+			return NTBASE+i;
+
+	# cannot find name
+	if(t > 1)
+		error(s+" should have been defined earlier");
+	return defin(t, s);
+}
+
+#
+# saves module definition in Code
+#
+cpymodule()
+{
+	if(gettok() != IDENTIFIER)
+		error("bad %%module construction");
+	if(modname != nil)
+		error("duplicate %%module construction");
+	modname = tokname;
+
+	level := 0;
+	for(;;) {
+		if((c:=finput.getc()) == Bufio->EOF)
+			error("EOF encountered while processing %%module");
+		case c {
+		'\n' =>
+			lineno++;
+		'{' =>
+			level++;
+			if(level == 1)
+				continue;
+		'}' =>
+			level--;
+
+			# we are finished copying
+			if(level == 0)
+				return;
+		}
+		addcodec(CodeMod, c);
+	}
+	if(codehead == nil || codetail.kind != CodeMod)
+		addcodec(CodeMod, '\n');	# ensure we add something
+}
+
+#
+# saves code between %{ and %}
+#
+cpycode()
+{
+	c := finput.getc();
+	if(c == '\n') {
+		c = finput.getc();
+		lineno++;
+	}
+	addcode(CodeData, "\n#line\t" + string lineno + "\t\"" + infile + "\"\n");
+	while(c != Bufio->EOF) {
+		if(c == '%') {
+			if((c=finput.getc()) == '}')
+				return;
+			addcodec(CodeData, '%');
+		}
+		addcodec(CodeData, c);
+		if(c == '\n')
+			lineno++;
+		c = finput.getc();
+	}
+	error("eof before %%}");
+}
+
+addcode(k: int, s: string)
+{
+	for(i := 0; i < len s; i++)
+		addcodec(k, s[i]);
+}
+
+addcodec(k, c: int)
+{
+	if(codehead == nil
+	|| k != codetail.kind
+	|| codetail.ndata >= NCode){
+		cd := ref Code(k, array[NCode+UTFmax] of byte, 0, nil);
+		if(codehead == nil)
+			codehead = cd;
+		else
+			codetail.next = cd;
+		codetail = cd;
+	}
+
+	codetail.ndata += sys->char2byte(c, codetail.data, codetail.ndata);
+}
+
+dumpcode(til: int)
+{
+	for(; codehead != nil; codehead = codehead.next){
+		if(codehead.kind == til)
+			return;
+		if(ftable.write(codehead.data, codehead.ndata) != codehead.ndata)
+			error("can't write output file");
+	}
+}
+
+#
+# write out the module declaration and any token info
+#
+dumpmod()
+{
+	if(fdefine != nil) {
+		fdefine.puts(modname);
+		fdefine.puts(": module {\n");
+	}
+	if (!suppressmod) {
+		ftable.puts(modname);
+		ftable.puts(": module {\n");
+	}
+
+	for(; codehead != nil; codehead = codehead.next){
+		if(codehead.kind != CodeMod)
+			break;
+		if(ftable.write(codehead.data, codehead.ndata) != codehead.ndata)
+			error("can't write output file");
+		if(fdefine != nil && fdefine.write(codehead.data, codehead.ndata) != codehead.ndata)
+			error("can't write define file");
+	}
+
+	for(i:=TOKSTART; i<=ntokens; i++) {
+		# non-literals
+		c := tokset[i].name[0];
+		if(c != ' ' && c != '$') {
+			s := tokset[i].name+": con	"+string tokset[i].value+";\n";
+			ftable.puts(s);
+			if(fdefine != nil)
+				fdefine.puts(s);
+		}
+	}
+
+	if(fdefine != nil)
+		fdefine.puts("};\n");
+	if (!suppressmod)
+		ftable.puts("\n};\n");
+
+	if(fdebug != nil) {
+		fdebug.puts("yytoknames = array[] of {\n");
+		for(i=1; i<=ntokens; i++) {
+			if(tokset[i].name != nil)
+				fdebug.puts("\t\""+chcopy(tokset[i].name)+"\",\n");
+			else
+				fdebug.puts("\t\"\",\n");
+		}
+		fdebug.puts("};\n");
+	}
+}
+
+#
+# skip over comments
+# skipcom is called after reading a '#'
+#
+skipcom(): int
+{
+	c := finput.getc();
+	while(c != Bufio->EOF) {
+		if(c == '\n')
+			return 1;
+		c = finput.getc();
+	}
+	error("EOF inside comment");
+	return 0;
+}
+
+#
+# copy limbo action to the next ; or closing }
+#
+cpyact(curprod: array of int, max: int)
+{
+	addcode(CodeAct, "\n#line\t");
+	addcode(CodeAct, string lineno);
+	addcode(CodeAct, "\t\"");
+	addcode(CodeAct, infile);
+	addcode(CodeAct, "\"\n");
+
+	brac := 0;
+
+loop:	for(;;){
+		c := finput.getc();
+	swt:	case c {
+		';' =>
+			if(brac == 0) {
+				addcodec(CodeAct, c);
+				return;
+			}
+	
+		'{' =>
+			brac++;
+			
+		'$' =>
+			s := 1;
+			tok := -1;
+			c = finput.getc();
+	
+			# type description
+			if(c == '<') {
+				finput.ungetc();
+				if(gettok() != TYPENAME)
+					error("bad syntax on $<ident> clause");
+				tok = numbval;
+				c = finput.getc();
+			}
+			if(c == '$') {
+				addcode(CodeAct, "yyval");
+	
+				# put out the proper tag...
+				if(ntypes) {
+					if(tok < 0)
+						tok = fdtype(curprod[0]);
+					addcode(CodeAct, "."+typeset[tok]);
+				}
+				continue loop;
+			}
+			if(c == '-') {
+				s = -s;
+				c = finput.getc();
+			}
+			j := 0;
+			if(isdigit(c)) {
+				while(isdigit(c)) {
+					j = j*10 + c-'0';
+					c = finput.getc();
+				}
+				finput.ungetc();
+				j = j*s;
+				if(j >= max)
+					error("Illegal use of $" + string j);
+			}else if(isword(c) || c == '_' || c == '.') {
+				# look for $name
+				finput.ungetc();
+				if(gettok() != IDENTIFIER)
+					error("$ must be followed by an identifier");
+				tokn := chfind(2, tokname);
+				fnd := -1;
+				if((c = finput.getc()) != '@')
+					finput.ungetc();
+				else if(gettok() != NUMBER)
+					error("@ must be followed by number");
+				else
+					fnd = numbval;
+				for(j=1; j<max; j++){
+					if(tokn == curprod[j]) {
+						fnd--;
+						if(fnd <= 0)
+							break;
+					}
+				}
+				if(j >= max)
+					error("$name or $name@number not found");
+			}else{
+				addcodec(CodeAct, '$');
+				if(s < 0)
+					addcodec(CodeAct, '-');
+				finput.ungetc();
+				continue loop;
+			}
+			addcode(CodeAct, "yys[yypt-" + string(max-j-1) + "].yyv");
+	
+			# put out the proper tag
+			if(ntypes) {
+				if(j <= 0 && tok < 0)
+					error("must specify type of $" + string j);
+				if(tok < 0)
+					tok = fdtype(curprod[j]);
+				addcodec(CodeAct, '.');
+				addcode(CodeAct, typeset[tok]);
+			}
+			continue loop;
+
+		'}' =>
+			brac--;
+			if(brac)
+				break;
+			addcodec(CodeAct, c);
+			return;
+
+		'#' =>
+			# a comment
+			addcodec(CodeAct, c);
+			c = finput.getc();
+			while(c != Bufio->EOF) {
+				if(c == '\n') {
+					lineno++;
+					break swt;
+				}
+				addcodec(CodeAct, c);
+				c = finput.getc();
+			}
+			error("EOF inside comment");
+
+		'\''or '"' =>
+			# character string or constant
+			match := c;
+			addcodec(CodeAct, c);
+			while(c = finput.getc()) {
+				if(c == '\\') {
+					addcodec(CodeAct, c);
+					c = finput.getc();
+					if(c == '\n')
+						lineno++;
+				} else if(c == match)
+					break swt;
+				if(c == '\n')
+					error("newline in string or char const.");
+				addcodec(CodeAct, c);
+			}
+			error("EOF in string or character constant");
+
+		Bufio->EOF =>
+			error("action does not terminate");
+
+		'\n' =>
+			lineno++;
+		}
+
+		addcodec(CodeAct, c);
+	}
+}
+
+openup(stem: string, dflag, vflag, ytab: int, ytabc: string)
+{
+	buf: string;
+	if(vflag) {
+		buf = stem + "." + FILEU;
+		foutput = bufio->create(buf, Bufio->OWRITE, 8r666);
+		if(foutput == nil)
+			error("can't create " + buf);
+	}
+	if(yydebug != nil) {
+		buf = stem + "." + FILEDEBUG;
+		fdebug = bufio->create(buf, Bufio->OWRITE, 8r666);
+		if(fdebug == nil)
+			error("can't create " + buf);
+	}
+	if(dflag) {
+		buf = stem + "." + FILED;
+		fdefine = bufio->create(buf, Bufio->OWRITE, 8r666);
+		if(fdefine == nil)
+			error("can't create " + buf);
+	}
+	if(ytab == 0)
+		buf = stem + "." + OFILE;
+	else
+		buf = ytabc;
+	ftable = bufio->create(buf, Bufio->OWRITE, 8r666);
+	if(ftable == nil)
+		error("can't create file " + buf);
+}
+
+#
+# return a pointer to the name of symbol i
+#
+symnam(i: int): string
+{
+	s: string;
+	if(i >= NTBASE)
+		s = nontrst[i-NTBASE].name;
+	else
+		s = tokset[i].name;
+	if(s[0] == ' ')
+		s = s[1:];
+	return s;
+}
+
+#
+# write out error comment
+#
+error(s: string)
+{
+	nerrors++;
+	fprint(stderr, "yacc: fatal error: %s, %s:%d\n", s, infile, lineno);
+	if(!fatfl)
+		return;
+	summary();
+	raise "fail:error";
+}
+
+#
+# set elements 0 through n-1 to c
+#
+aryfil(v: array of int, n, c: int)
+{
+	for(i:=0; i<n; i++)
+		v[i] = c;
+}
+
+#
+# compute an array with the beginnings of productions yielding given nonterminals
+# The array pres points to these lists
+# the array pyield has the lists: the total size is only NPROD+1
+#
+cpres()
+{
+	pres = array[nnonter+1] of array of array of int;
+	curres := array[nprod] of array of int;
+	for(i:=0; i<=nnonter; i++) {
+		n := 0;
+		c := i+NTBASE;
+		fatfl = 0;  	# make undefined symbols nonfatal
+		for(j:=0; j<nprod; j++)
+			if(prdptr[j][0] == c)
+				curres[n++] = prdptr[j][1:];
+		if(n == 0)
+			error("nonterminal " + nontrst[i].name + " not defined!");
+		else{
+			pres[i] = array[n] of array of int;
+			pres[i][0:] = curres[:n];
+		}
+	}
+	fatfl = 1;
+	if(nerrors) {
+		summary();
+		raise "fail:error";
+	}
+}
+
+dumppres()
+{
+	for(i := 0; i <= nnonter; i++){
+		print("nonterm %d\n", i);
+		curres := pres[i];
+		for(j := 0; j < len curres; j++){
+			print("\tproduction %d:", j);
+			prd := curres[j];
+			for(k := 0; k < len prd; k++)
+				print(" %d", prd[k]);
+			print("\n");
+		}
+	}
+}
+
+#
+# mark nonterminals which derive the empty string
+# also, look for nonterminals which don't derive any token strings
+#
+cempty()
+{
+	i, p, np: int;
+	prd: array of int;
+
+	pempty = array[nnonter+1] of int;
+
+	# first, use the array pempty to detect productions that can never be reduced
+	# set pempty to WHONOWS
+	aryfil(pempty, nnonter+1, WHOKNOWS);
+
+	# now, look at productions, marking nonterminals which derive something
+more:	for(;;){
+		for(i=0; i<nprod; i++) {
+			prd = prdptr[i];
+			if(pempty[prd[0] - NTBASE])
+				continue;
+			np = len prd - 1;
+			for(p = 1; p < np; p++)
+				if(prd[p] >= NTBASE && pempty[prd[p]-NTBASE] == WHOKNOWS)
+					break;
+			# production can be derived
+			if(p == np) {
+				pempty[prd[0]-NTBASE] = OK;
+				continue more;
+			}
+		}
+		break;
+	}
+
+	# now, look at the nonterminals, to see if they are all OK
+	for(i=0; i<=nnonter; i++) {
+		# the added production rises or falls as the start symbol ...
+		if(i == 0)
+			continue;
+		if(pempty[i] != OK) {
+			fatfl = 0;
+			error("nonterminal " + nontrst[i].name + " never derives any token string");
+		}
+	}
+
+	if(nerrors) {
+		summary();
+		raise "fail:error";
+	}
+
+	# now, compute the pempty array, to see which nonterminals derive the empty string
+	# set pempty to WHOKNOWS
+	aryfil(pempty, nnonter+1, WHOKNOWS);
+
+	# loop as long as we keep finding empty nonterminals
+
+again:	for(;;){
+	next:	for(i=1; i<nprod; i++) {
+			# not known to be empty
+			prd = prdptr[i];
+			if(pempty[prd[0]-NTBASE] != WHOKNOWS)
+				continue;
+			np = len prd - 1;
+			for(p = 1; p < np; p++)
+				if(prd[p] < NTBASE || pempty[prd[p]-NTBASE] != EMPTY)
+					continue next;
+
+			# we have a nontrivially empty nonterminal
+			pempty[prd[0]-NTBASE] = EMPTY;
+			# got one ... try for another
+			continue again;
+		}
+		return;
+	}
+}
+
+dumpempty()
+{
+	for(i := 0; i <= nnonter; i++)
+		if(pempty[i] == EMPTY)
+			print("non-term %d %s matches empty\n", i, symnam(i+NTBASE));
+}
+
+#
+# compute an array with the first of nonterminals
+#
+cpfir()
+{
+	s, n, p, np, ch: int;
+	curres: array of array of int;
+	prd: array of int;
+
+	wsets = array[nnonter+WSETINC] of Wset;
+	pfirst = array[nnonter+1] of Lkset;
+	for(i:=0; i<=nnonter; i++) {
+		wsets[i].ws = mkset();
+		pfirst[i] = mkset();
+		curres = pres[i];
+		n = len curres;
+		# initially fill the sets
+		for(s = 0; s < n; s++) {
+			prd = curres[s];
+			np = len prd - 1;
+			for(p = 0; p < np; p++) {
+				ch = prd[p];
+				if(ch < NTBASE) {
+					setbit(pfirst[i], ch);
+					break;
+				}
+				if(!pempty[ch-NTBASE])
+					break;
+			}
+		}
+	}
+
+	# now, reflect transitivity
+	changes := 1;
+	while(changes) {
+		changes = 0;
+		for(i=0; i<=nnonter; i++) {
+			curres = pres[i];
+			n = len curres;
+			for(s = 0; s < n; s++) {
+				prd = curres[s];
+				np = len prd - 1;
+				for(p = 0; p < np; p++) {
+					ch = prd[p] - NTBASE;
+					if(ch < 0)
+						break;
+					changes |= setunion(pfirst[i], pfirst[ch]);
+					if(!pempty[ch])
+						break;
+				}
+			}
+		}
+	}
+
+	if(!indebug)
+		return;
+	if(foutput != nil){
+		for(i=0; i<=nnonter; i++) {
+			foutput.putc('\n');
+			foutput.puts(nontrst[i].name);
+			foutput.puts(": ");
+			prlook(pfirst[i]);
+			foutput.putc(' ');
+			foutput.puts(string pempty[i]);
+			foutput.putc('\n');
+		}
+	}
+}
+
+#
+# generate the states
+#
+stagen()
+{
+	# initialize
+	nstate = 0;
+	tstates = array[ntokens+1] of {* => 0};	# states generated by terminal gotos
+	ntstates = array[nnonter+1] of {* => 0};# states generated by nonterminal gotos
+	amem = array[ACTSIZE] of {* => 0};
+	memp = 0;
+
+	clset = mkset();
+	pstate[0] = pstate[1] = 0;
+	aryfil(clset, tbitset, 0);
+	putitem(Pitem(prdptr[0], 0, 0, 0), clset);
+	tystate[0] = MUSTDO;
+	nstate = 1;
+	pstate[2] = pstate[1];
+
+	#
+	# now, the main state generation loop
+	# first pass generates all of the states
+	# later passes fix up lookahead
+	# could be sped up a lot by remembering
+	# results of the first pass rather than recomputing
+	#
+	first := 1;
+	for(more := 1; more; first = 0){
+		more = 0;
+		for(i:=0; i<nstate; i++) {
+			if(tystate[i] != MUSTDO)
+				continue;
+
+			tystate[i] = DONE;
+			aryfil(temp1, nnonter+1, 0);
+
+			# take state i, close it, and do gotos
+			closure(i);
+
+			# generate goto's
+			for(p:=0; p<cwp; p++) {
+				pi := wsets[p];
+				if(pi.flag)
+					continue;
+				wsets[p].flag = 1;
+				c := pi.pitem.first;
+				if(c <= 1) {
+					if(pstate[i+1]-pstate[i] <= p)
+						tystate[i] = MUSTLOOKAHEAD;
+					continue;
+				}
+				# do a goto on c
+				putitem(wsets[p].pitem, wsets[p].ws);
+				for(q:=p+1; q<cwp; q++) {
+					# this item contributes to the goto
+					if(c == wsets[q].pitem.first) {
+						putitem(wsets[q].pitem, wsets[q].ws);
+						wsets[q].flag = 1;
+					}
+				}
+
+				if(c < NTBASE)
+					state(c);	# register new state
+				else
+					temp1[c-NTBASE] = state(c);
+			}
+
+			if(gsdebug && foutput != nil) {
+				foutput.puts(string i + ": ");
+				for(j:=0; j<=nnonter; j++)
+					if(temp1[j])
+						foutput.puts(nontrst[j].name + " " + string temp1[j] + ", ");
+				foutput.putc('\n');
+			}
+
+			if(first)
+				indgo[i] = apack(temp1[1:], nnonter-1) - 1;
+
+			more++;
+		}
+	}
+}
+
+#
+# generate the closure of state i
+#
+closure(i: int)
+{
+	zzclose++;
+
+	# first, copy kernel of state i to wsets
+	cwp = 0;
+	q := pstate[i+1];
+	for(p:=pstate[i]; p<q; p++) {
+		wsets[cwp].pitem = statemem[p].pitem;
+		wsets[cwp].flag = 1;			# this item must get closed
+		wsets[cwp].ws[0:] = statemem[p].look;
+		cwp++;
+	}
+
+	# now, go through the loop, closing each item
+	work := 1;
+	while(work) {
+		work = 0;
+		for(u:=0; u<cwp; u++) {
+			if(wsets[u].flag == 0)
+				continue;
+			# dot is before c
+			c := wsets[u].pitem.first;
+			if(c < NTBASE) {
+				wsets[u].flag = 0;
+				# only interesting case is where . is before nonterminal
+				continue;
+			}
+
+			# compute the lookahead
+			aryfil(clset, tbitset, 0);
+
+			# find items involving c
+			for(v:=u; v<cwp; v++) {
+				if(wsets[v].flag != 1
+				|| wsets[v].pitem.first != c)
+					continue;
+				pi := wsets[v].pitem.prod;
+				ipi := wsets[v].pitem.off + 1;
+				
+				wsets[v].flag = 0;
+				if(nolook)
+					continue;
+				while((ch := pi[ipi++]) > 0) {
+					# terminal symbol
+					if(ch < NTBASE) {
+						setbit(clset, ch);
+						break;
+					}
+					# nonterminal symbol
+					setunion(clset, pfirst[ch-NTBASE]);
+					if(!pempty[ch-NTBASE])
+						break;
+				}
+				if(ch <= 0)
+					setunion(clset, wsets[v].ws);
+			}
+
+			#
+			# now loop over productions derived from c
+			#
+			curres := pres[c - NTBASE];
+			n := len curres;
+			# initially fill the sets
+	nexts:		for(s := 0; s < n; s++) {
+				prd := curres[s];
+				#
+				# put these items into the closure
+				# is the item there
+				#
+				for(v=0; v<cwp; v++) {
+					# yes, it is there
+					if(wsets[v].pitem.off == 0
+					&& wsets[v].pitem.prod == prd) {
+						if(!nolook && setunion(wsets[v].ws, clset))
+							wsets[v].flag = work = 1;
+						continue nexts;
+					}
+				}
+
+				#  not there; make a new entry
+				if(cwp >= len wsets){
+					awsets := array[cwp + WSETINC] of Wset;
+					awsets[0:] = wsets;
+					wsets = awsets;
+				}
+				wsets[cwp].pitem = Pitem(prd, 0, prd[0], -prd[len prd-1]);
+				wsets[cwp].flag = 1;
+				wsets[cwp].ws = mkset();
+				if(!nolook) {
+					work = 1;
+					wsets[cwp].ws[0:] = clset;
+				}
+				cwp++;
+			}
+		}
+	}
+
+	# have computed closure; flags are reset; return
+	if(cldebug && foutput != nil) {
+		foutput.puts("\nState " + string i + ", nolook = " + string nolook + "\n");
+		for(u:=0; u<cwp; u++) {
+			if(wsets[u].flag)
+				foutput.puts("flag set!\n");
+			wsets[u].flag = 0;
+			foutput.putc('\t');
+			foutput.puts(writem(wsets[u].pitem));
+			prlook(wsets[u].ws);
+			foutput.putc('\n');
+		}
+	}
+}
+
+#
+# sorts last state,and sees if it equals earlier ones. returns state number
+#
+state(c: int): int
+{
+	zzstate++;
+	p1 := pstate[nstate];
+	p2 := pstate[nstate+1];
+	if(p1 == p2)
+		return 0;	# null state
+	# sort the items
+	k, l: int;
+	for(k = p1+1; k < p2; k++) {	# make k the biggest
+		for(l = k; l > p1; l--) {
+			if(statemem[l].pitem.prodno < statemem[l-1].pitem.prodno
+			|| statemem[l].pitem.prodno == statemem[l-1].pitem.prodno
+			&& statemem[l].pitem.off < statemem[l-1].pitem.off) {
+				s := statemem[l];
+				statemem[l] = statemem[l-1];
+				statemem[l-1] = s;
+			}else
+				break;
+		}
+	}
+
+	size1 := p2 - p1;	# size of state
+
+	if(c >= NTBASE)
+		i := ntstates[c-NTBASE];
+	else
+		i = tstates[c];
+
+look:	for(; i != 0; i = mstates[i]) {
+		# get ith state
+		q1 := pstate[i];
+		q2 := pstate[i+1];
+		size2 := q2 - q1;
+		if(size1 != size2)
+			continue;
+		k = p1;
+		for(l = q1; l < q2; l++) {
+			if(statemem[l].pitem.prod != statemem[k].pitem.prod
+			|| statemem[l].pitem.off != statemem[k].pitem.off)
+				continue look;
+			k++;
+		}
+
+		# found it
+		pstate[nstate+1] = pstate[nstate];	# delete last state
+		# fix up lookaheads
+		if(nolook)
+			return i;
+		k = p1;
+		for(l = q1; l < q2; l++) {
+			if(setunion(statemem[l].look, statemem[k].look))
+				tystate[i] = MUSTDO;
+			k++;
+		}
+		return i;
+	}
+	# state is new
+	zznewstate++;
+	if(nolook)
+		error("yacc state/nolook error");
+	pstate[nstate+2] = p2;
+	if(nstate+1 >= NSTATES)
+		error("too many states");
+	if(c >= NTBASE) {
+		mstates[nstate] = ntstates[c-NTBASE];
+		ntstates[c-NTBASE] = nstate;
+	} else {
+		mstates[nstate] = tstates[c];
+		tstates[c] = nstate;
+	}
+	tystate[nstate] = MUSTDO;
+	return nstate++;
+}
+
+putitem(p: Pitem, set: Lkset)
+{
+	p.off++;
+	p.first = p.prod[p.off];
+
+	if(pidebug && foutput != nil)
+		foutput.puts("putitem(" + writem(p) + "), state " + string nstate + "\n");
+	j := pstate[nstate+1];
+	if(j >= len statemem){
+		asm := array[j + STATEINC] of Item;
+		asm[0:] = statemem;
+		statemem = asm;
+	}
+	statemem[j].pitem = p;
+	if(!nolook){
+		s := mkset();
+		s[0:] = set;
+		statemem[j].look = s;
+	}
+	j++;
+	pstate[nstate+1] = j;
+}
+
+#
+# creates output string for item pointed to by pp
+#
+writem(pp: Pitem): string
+{
+	i: int;
+	p := pp.prod;
+	q := chcopy(nontrst[prdptr[pp.prodno][0]-NTBASE].name) + ": ";
+	npi := pp.off;
+	pi := p == prdptr[pp.prodno];
+	for(;;){
+		c := ' ';
+		if(pi == npi)
+			c = '.';
+		q[len q] = c;
+		i = p[pi++];
+		if(i <= 0)
+			break;
+		q += chcopy(symnam(i));
+	}
+
+	# an item calling for a reduction
+	i = p[npi];
+	if(i < 0)
+		q += "    (" + string -i + ")";
+	return q;
+}
+
+#
+# pack state i from temp1 into amem
+#
+apack(p: array of int, n: int): int
+{
+	#
+	# we don't need to worry about checking because
+	# we will only look at entries known to be there...
+	# eliminate leading and trailing 0's
+	#
+	off := 0;
+	for(pp := 0; pp <= n && p[pp] == 0; pp++)
+		off--;
+ 	# no actions
+	if(pp > n)
+		return 0;
+	for(; n > pp && p[n] == 0; n--)
+		;
+	p = p[pp:n+1];
+
+	# now, find a place for the elements from p to q, inclusive
+	r := len amem - len p;
+nextk:	for(rr := 0; rr <= r; rr++) {
+		qq := rr;
+		for(pp = 0; pp < len p; pp++) {
+			if(p[pp] != 0)
+				if(p[pp] != amem[qq] && amem[qq] != 0)
+					continue nextk;
+			qq++;
+		}
+
+		# we have found an acceptable k
+		if(pkdebug && foutput != nil)
+			foutput.puts("off = " + string(off+rr) + ", k = " + string rr + "\n");
+		qq = rr;
+		for(pp = 0; pp < len p; pp++) {
+			if(p[pp]) {
+				if(qq > memp)
+					memp = qq;
+				amem[qq] = p[pp];
+			}
+			qq++;
+		}
+		if(pkdebug && foutput != nil) {
+			for(pp = 0; pp <= memp; pp += 10) {
+				foutput.putc('\t');
+				for(qq = pp; qq <= pp+9; qq++)
+					foutput.puts(string amem[qq] + " ");
+				foutput.putc('\n');
+			}
+		}
+		return off + rr;
+	}
+	error("no space in action table");
+	return 0;
+}
+
+#
+# print the output for the states
+#
+output()
+{
+	c, u, v: int;
+
+	ftable.puts("yyexca := array[] of {");
+	if(fdebug != nil)
+		fdebug.puts("yystates = array [] of {\n");
+
+	noset := mkset();
+
+	# output the stuff for state i
+	for(i:=0; i<nstate; i++) {
+		nolook = tystate[i]!=MUSTLOOKAHEAD;
+		closure(i);
+
+		# output actions
+		nolook = 1;
+		aryfil(temp1, ntokens+nnonter+1, 0);
+		for(u=0; u<cwp; u++) {
+			c = wsets[u].pitem.first;
+			if(c > 1 && c < NTBASE && temp1[c] == 0) {
+				for(v=u; v<cwp; v++)
+					if(c == wsets[v].pitem.first)
+						putitem(wsets[v].pitem, noset);
+				temp1[c] = state(c);
+			} else
+				if(c > NTBASE && temp1[(c -= NTBASE) + ntokens] == 0)
+					temp1[c+ntokens] = amem[indgo[i]+c];
+		}
+		if(i == 1)
+			temp1[1] = ACCEPTCODE;
+
+		# now, we have the shifts; look at the reductions
+		lastred = 0;
+		for(u=0; u<cwp; u++) {
+			c = wsets[u].pitem.first;
+
+			# reduction
+			if(c > 0)
+				continue;
+			lastred = -c;
+			us := wsets[u].ws;
+			for(k:=0; k<=ntokens; k++) {
+				if(!bitset(us, k))
+					continue;
+				if(temp1[k] == 0)
+					temp1[k] = c;
+				else
+				if(temp1[k] < 0) { # reduce/reduce conflict
+					if(foutput != nil)
+						foutput.puts(
+							"\n" + string i + ": reduce/reduce conflict  (red'ns "
+							+ string -temp1[k] + " and " + string lastred + " ) on " + symnam(k));
+					if(-temp1[k] > lastred)
+						temp1[k] = -lastred;
+					zzrrconf++;
+				} else
+					# potential shift/reduce conflict
+					precftn(lastred, k, i);
+			}
+		}
+		wract(i);
+	}
+
+	if(fdebug != nil)
+		fdebug.puts("};\n");
+	ftable.puts("};\n");
+	ftable.puts("YYNPROD: con " + string nprod + ";\n");
+	ftable.puts("YYPRIVATE: con " + string PRIVATE + ";\n");
+	ftable.puts("yytoknames: array of string;\n");
+	ftable.puts("yystates: array of string;\n");
+	if(yydebug != nil){
+		ftable.puts("include \"y.debug\";\n");
+		ftable.puts("yydebug: con " + yydebug + ";\n");
+	}else{
+		ftable.puts("yydebug: con 0;\n");
+	}
+}
+
+#
+# decide a shift/reduce conflict by precedence.
+# r is a rule number, t a token number
+# the conflict is in state s
+# temp1[t] is changed to reflect the action
+#
+precftn(r, t, s: int)
+{
+	action: int;
+
+	lp := levprd[r];
+	lt := toklev[t];
+	if(PLEVEL(lt) == 0 || PLEVEL(lp) == 0) {
+
+		# conflict
+		if(foutput != nil)
+			foutput.puts(
+				"\n" + string s + ": shift/reduce conflict (shift "
+				+ string temp1[t] + "(" + string PLEVEL(lt) + "), red'n "
+				+ string r + "(" + string PLEVEL(lp) + ")) on " + symnam(t));
+		zzsrconf++;
+		return;
+	}
+	if(PLEVEL(lt) == PLEVEL(lp))
+		action = ASSOC(lt);
+	else if(PLEVEL(lt) > PLEVEL(lp))
+		action = RASC;  # shift
+	else
+		action = LASC;  # reduce
+	case action{
+	BASC =>  # error action
+		temp1[t] = ERRCODE;
+	LASC =>  # reduce
+		temp1[t] = -r;
+	}
+}
+
+#
+# output state i
+# temp1 has the actions, lastred the default
+#
+wract(i: int)
+{
+	p, p1: int;
+
+	# find the best choice for lastred
+	lastred = 0;
+	ntimes := 0;
+	for(j:=0; j<=ntokens; j++) {
+		if(temp1[j] >= 0)
+			continue;
+		if(temp1[j]+lastred == 0)
+			continue;
+		# count the number of appearances of temp1[j]
+		count := 0;
+		tred := -temp1[j];
+		levprd[tred] |= REDFLAG;
+		for(p=0; p<=ntokens; p++)
+			if(temp1[p]+tred == 0)
+				count++;
+		if(count > ntimes) {
+			lastred = tred;
+			ntimes = count;
+		}
+	}
+
+	#
+	# for error recovery, arrange that, if there is a shift on the
+	# error recovery token, `error', that the default be the error action
+	#
+	if(temp1[2] > 0)
+		lastred = 0;
+
+	# clear out entries in temp1 which equal lastred
+	# count entries in optst table
+	n := 0;
+	for(p=0; p<=ntokens; p++) {
+		p1 = temp1[p];
+		if(p1+lastred == 0)
+			temp1[p] = p1 = 0;
+		if(p1 > 0 && p1 != ACCEPTCODE && p1 != ERRCODE)
+			n++;
+	}
+
+	wrstate(i);
+	defact[i] = lastred;
+	flag := 0;
+	os := array[n*2] of int;
+	n = 0;
+	for(p=0; p<=ntokens; p++) {
+		if((p1=temp1[p]) != 0) {
+			if(p1 < 0) {
+				p1 = -p1;
+			} else if(p1 == ACCEPTCODE) {
+				p1 = -1;
+			} else if(p1 == ERRCODE) {
+				p1 = 0;
+			} else {
+				os[n++] = p;
+				os[n++] = p1;
+				zzacent++;
+				continue;
+			}
+			if(flag++ == 0)
+				ftable.puts("-1, " + string i + ",\n");
+			ftable.puts("\t" + string p + ", " + string p1 + ",\n");
+			zzexcp++;
+		}
+	}
+	if(flag) {
+		defact[i] = -2;
+		ftable.puts("\t-2, " + string lastred + ",\n");
+	}
+	optst[i] = os;
+}
+
+#
+# writes state i
+#
+wrstate(i: int)
+{
+	j0, j1, u: int;
+	pp, qq: int;
+
+	if(fdebug != nil) {
+		if(lastred) {
+			fdebug.puts("	nil, #" + string i + "\n");
+		} else {
+			fdebug.puts("	\"");
+			qq = pstate[i+1];
+			for(pp=pstate[i]; pp<qq; pp++){
+				fdebug.puts(writem(statemem[pp].pitem));
+				fdebug.puts("\\n");
+			}
+			if(tystate[i] == MUSTLOOKAHEAD)
+				for(u = pstate[i+1] - pstate[i]; u < cwp; u++)
+					if(wsets[u].pitem.first < 0){
+						fdebug.puts(writem(wsets[u].pitem));
+						fdebug.puts("\\n");
+					}
+			fdebug.puts("\", #" + string i + "/\n");
+		}
+	}
+	if(foutput == nil)
+		return;
+	foutput.puts("\nstate " + string i + "\n");
+	qq = pstate[i+1];
+	for(pp=pstate[i]; pp<qq; pp++){
+		foutput.putc('\t');
+		foutput.puts(writem(statemem[pp].pitem));
+		foutput.putc('\n');
+	}
+	if(tystate[i] == MUSTLOOKAHEAD) {
+		# print out empty productions in closure
+		for(u = pstate[i+1] - pstate[i]; u < cwp; u++) {
+			if(wsets[u].pitem.first < 0) {
+				foutput.putc('\t');
+				foutput.puts(writem(wsets[u].pitem));
+				foutput.putc('\n');
+			}
+		}
+	}
+
+	# check for state equal to another
+	for(j0=0; j0<=ntokens; j0++)
+		if((j1=temp1[j0]) != 0) {
+			foutput.puts("\n\t" + symnam(j0) + "  ");
+			# shift, error, or accept
+			if(j1 > 0) {
+				if(j1 == ACCEPTCODE)
+					foutput.puts("accept");
+				else if(j1 == ERRCODE)
+					foutput.puts("error");
+				else
+					foutput.puts("shift "+string j1);
+			} else
+				foutput.puts("reduce " + string -j1 + " (src line " + string rlines[-j1] + ")");
+		}
+
+	# output the final production
+	if(lastred)
+		foutput.puts("\n\t.  reduce " + string lastred + " (src line " + string rlines[lastred] + ")\n\n");
+	else
+		foutput.puts("\n\t.  error\n\n");
+
+	# now, output nonterminal actions
+	j1 = ntokens;
+	for(j0 = 1; j0 <= nnonter; j0++) {
+		j1++;
+		if(temp1[j1])
+			foutput.puts("\t" + symnam(j0+NTBASE) + "  goto " + string temp1[j1] + "\n");
+	}
+}
+
+#
+# output the gotos for the nontermninals
+#
+go2out()
+{
+	for(i := 1; i <= nnonter; i++) {
+		go2gen(i);
+
+		# find the best one to make default
+		best := -1;
+		times := 0;
+
+		# is j the most frequent
+		for(j := 0; j < nstate; j++) {
+			if(tystate[j] == 0)
+				continue;
+			if(tystate[j] == best)
+				continue;
+
+			# is tystate[j] the most frequent
+			count := 0;
+			cbest := tystate[j];
+			for(k := j; k < nstate; k++)
+				if(tystate[k] == cbest)
+					count++;
+			if(count > times) {
+				best = cbest;
+				times = count;
+			}
+		}
+
+		# best is now the default entry
+		zzgobest += times-1;
+		n := 0;
+		for(j = 0; j < nstate; j++)
+			if(tystate[j] != 0 && tystate[j] != best)
+				n++;
+		goent := array[2*n+1] of int;
+		n = 0;
+		for(j = 0; j < nstate; j++)
+			if(tystate[j] != 0 && tystate[j] != best) {
+				goent[n++] = j;
+				goent[n++] = tystate[j];
+				zzgoent++;
+			}
+
+		# now, the default
+		if(best == -1)
+			best = 0;
+		zzgoent++;
+		goent[n] = best;
+		yypgo[i] = goent;
+	}
+}
+
+#
+# output the gotos for nonterminal c
+#
+go2gen(c: int)
+{
+	i, cc, p, q: int;
+
+	# first, find nonterminals with gotos on c
+	aryfil(temp1, nnonter+1, 0);
+	temp1[c] = 1;
+	work := 1;
+	while(work) {
+		work = 0;
+		for(i=0; i<nprod; i++) {
+			# cc is a nonterminal with a goto on c
+			cc = prdptr[i][1]-NTBASE;
+			if(cc >= 0 && temp1[cc] != 0) {
+				# thus, the left side of production i does too
+				cc = prdptr[i][0]-NTBASE;
+				if(temp1[cc] == 0) {
+					  work = 1;
+					  temp1[cc] = 1;
+				}
+			}
+		}
+	}
+
+	# now, we have temp1[c] = 1 if a goto on c in closure of cc
+	if(g2debug && foutput != nil) {
+		foutput.puts(nontrst[c].name);
+		foutput.puts(": gotos on ");
+		for(i=0; i<=nnonter; i++)
+			if(temp1[i]){
+				foutput.puts(nontrst[i].name);
+				foutput.putc(' ');
+			}
+		foutput.putc('\n');
+	}
+
+	# now, go through and put gotos into tystate
+	aryfil(tystate, nstate, 0);
+	for(i=0; i<nstate; i++) {
+		q = pstate[i+1];
+		for(p=pstate[i]; p<q; p++) {
+			if((cc = statemem[p].pitem.first) >= NTBASE) {
+				# goto on c is possible
+				if(temp1[cc-NTBASE]) {
+					tystate[i] = amem[indgo[i]+c];
+					break;
+				}
+			}
+		}
+	}
+}
+
+#
+# in order to free up the mem and amem arrays for the optimizer,
+# and still be able to output yyr1, etc., after the sizes of
+# the action array is known, we hide the nonterminals
+# derived by productions in levprd.
+#
+hideprod()
+{
+	j := 0;
+	levprd[0] = 0;
+	for(i:=1; i<nprod; i++) {
+		if(!(levprd[i] & REDFLAG)) {
+			j++;
+			if(foutput != nil) {
+				foutput.puts("Rule not reduced:   ");
+				foutput.puts(writem(Pitem(prdptr[i], 0, 0, i)));
+				foutput.putc('\n');
+			}
+		}
+		levprd[i] = prdptr[i][0] - NTBASE;
+	}
+	if(j)
+		print("%d rules never reduced\n", j);
+}
+
+callopt()
+{
+	j, k, p, q: int;
+	v: array of int;
+
+	pgo = array[nnonter+1] of int;
+	pgo[0] = 0;
+	maxoff = 0;
+	maxspr = 0;
+	for(i := 0; i < nstate; i++) {
+		k = 32000;
+		j = 0;
+		v = optst[i];
+		q = len v;
+		for(p = 0; p < q; p += 2) {
+			if(v[p] > j)
+				j = v[p];
+			if(v[p] < k)
+				k = v[p];
+		}
+		# nontrivial situation
+		if(k <= j) {
+			# j is now the range
+#			j -= k;			# call scj
+			if(k > maxoff)
+				maxoff = k;
+		}
+		tystate[i] = q + 2*j;
+		if(j > maxspr)
+			maxspr = j;
+	}
+
+	# initialize ggreed table
+	ggreed = array[nnonter+1] of int;
+	for(i = 1; i <= nnonter; i++) {
+		ggreed[i] = 1;
+		j = 0;
+
+		# minimum entry index is always 0
+		v = yypgo[i];
+		q = len v - 1;
+		for(p = 0; p < q ; p += 2) {
+			ggreed[i] += 2;
+			if(v[p] > j)
+				j = v[p];
+		}
+		ggreed[i] = ggreed[i] + 2*j;
+		if(j > maxoff)
+			maxoff = j;
+	}
+
+	# now, prepare to put the shift actions into the amem array
+	for(i = 0; i < ACTSIZE; i++)
+		amem[i] = 0;
+	maxa = 0;
+	for(i = 0; i < nstate; i++) {
+		if(tystate[i] == 0 && adb > 1)
+			ftable.puts("State " + string i + ": null\n");
+		indgo[i] = YYFLAG1;
+	}
+	while((i = nxti()) != NOMORE)
+		if(i >= 0)
+			stin(i);
+		else
+			gin(-i);
+
+	# print amem array
+	if(adb > 2)
+		for(p = 0; p <= maxa; p += 10) {
+			ftable.puts(string p + "  ");
+			for(i = 0; i < 10; i++)
+				ftable.puts(string amem[p+i] + "  ");
+			ftable.putc('\n');
+		}
+
+	aoutput();
+	osummary();
+}
+
+#
+# finds the next i
+#
+nxti(): int
+{
+	max := 0;
+	maxi := 0;
+	for(i := 1; i <= nnonter; i++)
+		if(ggreed[i] >= max) {
+			max = ggreed[i];
+			maxi = -i;
+		}
+	for(i = 0; i < nstate; i++)
+		if(tystate[i] >= max) {
+			max = tystate[i];
+			maxi = i;
+		}
+	if(max == 0)
+		return NOMORE;
+	return maxi;
+}
+
+gin(i: int)
+{
+	s: int;
+
+	# enter gotos on nonterminal i into array amem
+	ggreed[i] = 0;
+
+	q := yypgo[i];
+	nq := len q - 1;
+	# now, find amem place for it
+nextgp:	for(p := 0; p < ACTSIZE; p++) {
+		if(amem[p])
+			continue;
+		for(r := 0; r < nq; r += 2) {
+			s = p + q[r] + 1;
+			if(s > maxa){
+				maxa = s;
+				if(maxa >= ACTSIZE)
+					error("a array overflow");
+			}
+			if(amem[s])
+				continue nextgp;
+		}
+		# we have found amem spot
+		amem[p] = q[nq];
+		if(p > maxa)
+			maxa = p;
+		for(r = 0; r < nq; r += 2) {
+			s = p + q[r] + 1;
+			amem[s] = q[r+1];
+		}
+		pgo[i] = p;
+		if(adb > 1)
+			ftable.puts("Nonterminal " + string i + ", entry at " + string pgo[i] + "\n");
+		return;
+	}
+	error("cannot place goto " + string i + "\n");
+}
+
+stin(i: int)
+{
+	s: int;
+
+	tystate[i] = 0;
+
+	# enter state i into the amem array
+	q := optst[i];
+	nq := len q;
+	# find an acceptable place
+nextn:	for(n := -maxoff; n < ACTSIZE; n++) {
+		flag := 0;
+		for(r := 0; r < nq; r += 2) {
+			s = q[r] + n;
+			if(s < 0 || s > ACTSIZE)
+				continue nextn;
+			if(amem[s] == 0)
+				flag++;
+			else if(amem[s] != q[r+1])
+				continue nextn;
+		}
+
+		# check the position equals another only if the states are identical
+		for(j:=0; j<nstate; j++) {
+			if(indgo[j] == n) {
+
+				# we have some disagreement
+				if(flag)
+					continue nextn;
+				if(nq == len optst[j]) {
+
+					# states are equal
+					indgo[i] = n;
+					if(adb > 1)
+						ftable.puts("State " + string i + ": entry at "
+							+ string n + " equals state " + string j + "\n");
+					return;
+				}
+
+				# we have some disagreement
+				continue nextn;
+			}
+		}
+
+		for(r = 0; r < nq; r += 2) {
+			s = q[r] + n;
+			if(s > maxa)
+				maxa = s;
+			if(amem[s] != 0 && amem[s] != q[r+1])
+				error("clobber of a array, pos'n " + string s + ", by " + string q[r+1] + "");
+			amem[s] = q[r+1];
+		}
+		indgo[i] = n;
+		if(adb > 1)
+			ftable.puts("State " + string i + ": entry at " + string indgo[i] + "\n");
+		return;
+	}
+	error("Error; failure to place state " + string i + "\n");
+}
+
+#
+# this version is for limbo
+# write out the optimized parser
+#
+aoutput()
+{
+	ftable.puts("YYLAST:\tcon "+string (maxa+1)+";\n");
+	arout("yyact", amem, maxa+1);
+	arout("yypact", indgo, nstate);
+	arout("yypgo", pgo, nnonter+1);
+}
+
+#
+# put out other arrays, copy the parsers
+#
+others()
+{
+	finput = bufio->open(parser, Bufio->OREAD);
+	if(finput == nil)
+		error("cannot find parser " + parser);
+	arout("yyr1", levprd, nprod);
+	aryfil(temp1, nprod, 0);
+
+	#
+	#yyr2 is the number of rules for each production
+	#
+	for(i:=1; i<nprod; i++)
+		temp1[i] = len prdptr[i] - 2;
+	arout("yyr2", temp1, nprod);
+
+	aryfil(temp1, nstate, -1000);
+	for(i=0; i<=ntokens; i++)
+		for(j:=tstates[i]; j!=0; j=mstates[j])
+			temp1[j] = i;
+	for(i=0; i<=nnonter; i++)
+		for(j=ntstates[i]; j!=0; j=mstates[j])
+			temp1[j] = -i;
+	arout("yychk", temp1, nstate);
+	arout("yydef", defact, nstate);
+
+	# put out token translation tables
+	# table 1 has 0-256
+	aryfil(temp1, 256, 0);
+	c := 0;
+	for(i=1; i<=ntokens; i++) {
+		j = tokset[i].value;
+		if(j >= 0 && j < 256) {
+			if(temp1[j]) {
+				print("yacc bug -- cant have 2 different Ts with same value\n");
+				print("	%s and %s\n", tokset[i].name, tokset[temp1[j]].name);
+				nerrors++;
+			}
+			temp1[j] = i;
+			if(j > c)
+				c = j;
+		}
+	}
+	for(i = 0; i <= c; i++)
+		if(temp1[i] == 0)
+			temp1[i] = YYLEXUNK;
+	arout("yytok1", temp1, c+1);
+
+	# table 2 has PRIVATE-PRIVATE+256
+	aryfil(temp1, 256, 0);
+	c = 0;
+	for(i=1; i<=ntokens; i++) {
+		j = tokset[i].value - PRIVATE;
+		if(j >= 0 && j < 256) {
+			if(temp1[j]) {
+				print("yacc bug -- cant have 2 different Ts with same value\n");
+				print("	%s and %s\n", tokset[i].name, tokset[temp1[j]].name);
+				nerrors++;
+			}
+			temp1[j] = i;
+			if(j > c)
+				c = j;
+		}
+	}
+	arout("yytok2", temp1, c+1);
+
+	# table 3 has everything else
+	ftable.puts("yytok3 := array[] of {\n");
+	c = 0;
+	for(i=1; i<=ntokens; i++) {
+		j = tokset[i].value;
+		if(j >= 0 && j < 256)
+			continue;
+		if(j >= PRIVATE && j < 256+PRIVATE)
+			continue;
+
+		ftable.puts(sprint("%4d,%4d,", j, i));
+		c++;
+		if(c%5 == 0)
+			ftable.putc('\n');
+	}
+	ftable.puts(sprint("%4d\n};\n", 0));
+
+	# copy parser text
+	while((c=finput.getc()) != Bufio->EOF) {
+		if(c == '$') {
+			if((c = finput.getc()) != 'A')
+				ftable.putc('$');
+			else { # copy actions
+				if(codehead == nil)
+					ftable.puts("* => ;");
+				else
+					dumpcode(-1);
+				c = finput.getc();
+			}
+		}
+		ftable.putc(c);
+	}
+	ftable.close();
+}
+
+arout(s: string, v: array of int, n: int)
+{
+	ftable.puts(s+" := array[] of {");
+	for(i := 0; i < n; i++) {
+		if(i%10 == 0)
+			ftable.putc('\n');
+		ftable.puts(sprint("%4d", v[i]));
+		ftable.putc(',');
+	}
+	ftable.puts("\n};\n");
+}
+
+#
+# output the summary on y.output
+#
+summary()
+{
+	if(foutput != nil) {
+		foutput.puts("\n" + string ntokens + " terminals, " + string(nnonter + 1) + " nonterminals\n");
+		foutput.puts("" + string nprod + " grammar rules, " + string nstate + "/" + string NSTATES + " states\n");
+		foutput.puts("" + string zzsrconf + " shift/reduce, " + string zzrrconf + " reduce/reduce conflicts reported\n");
+		foutput.puts("" + string len wsets + " working sets used\n");
+		foutput.puts("memory: parser " + string memp + "/" + string ACTSIZE + "\n");
+		foutput.puts(string (zzclose - 2*nstate) + " extra closures\n");
+		foutput.puts(string zzacent + " shift entries, " + string zzexcp + " exceptions\n");
+		foutput.puts(string zzgoent + " goto entries\n");
+		foutput.puts(string zzgobest + " entries saved by goto default\n");
+	}
+	if(zzsrconf != 0 || zzrrconf != 0) {
+		print("\nconflicts: ");
+		if(zzsrconf)
+			print("%d shift/reduce", zzsrconf);
+		if(zzsrconf && zzrrconf)
+			print(", ");
+		if(zzrrconf)
+			print("%d reduce/reduce", zzrrconf);
+		print("\n");
+	}
+	if(fdefine != nil)
+		fdefine.close();
+}
+
+#
+# write optimizer summary
+#
+osummary()
+{
+	if(foutput == nil)
+		return;
+	i := 0;
+	for(p := maxa; p >= 0; p--)
+		if(amem[p] == 0)
+			i++;
+
+	foutput.puts("Optimizer space used: output " + string (maxa+1) + "/" + string ACTSIZE + "\n");
+	foutput.puts(string(maxa+1) + " table entries, " + string i + " zero\n");
+	foutput.puts("maximum spread: " + string maxspr + ", maximum offset: " + string maxoff + "\n");
+}
+
+#
+# copies and protects "'s in q
+#
+chcopy(q: string): string
+{
+	s := "";
+	j := 0;
+	for(i := 0; i < len q; i++) {
+		if(q[i] == '"') {
+			s += q[j:i] + "\\";
+			j = i;
+		}
+	}
+	return s + q[j:i];
+}
+
+usage()
+{
+	fprint(stderr, "usage: yacc [-vdm] [-Dn] [-o output] [-s stem] file\n");
+	raise "fail:usage";
+}
+
+bitset(set: Lkset, bit: int): int
+{
+	return set[bit>>5] & (1<<(bit&31));
+}
+
+setbit(set: Lkset, bit: int): int
+{
+	return set[bit>>5] |= (1<<(bit&31));
+}
+
+mkset(): Lkset
+{
+	return array[tbitset] of {* => 0};
+}
+
+#
+# set a to the union of a and b
+# return 1 if b is not a subset of a, 0 otherwise
+#
+setunion(a, b: array of int): int
+{
+	sub := 0;
+	for(i:=0; i<tbitset; i++) {
+		x := a[i];
+		y := x | b[i];
+		a[i] = y;
+		if(y != x)
+			sub = 1;
+	}
+	return sub;
+}
+
+prlook(p: Lkset)
+{
+	if(p == nil){
+		foutput.puts("\tNULL");
+		return;
+	}
+	foutput.puts(" { ");
+	for(j:=0; j<=ntokens; j++){
+		if(bitset(p, j)){
+			foutput.puts(symnam(j));
+			foutput.putc(' ');
+		}
+	}
+	foutput.putc('}');
+}
+
+#
+# utility routines
+#
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isword(c: int): int
+{
+	return c >= 16ra0 || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
+}
+
+mktemp(t: string): string
+{
+	return t;
+}
+
+#
+# arg processing
+#
+Arg.init(argv: list of string): ref Arg
+{
+	if(argv != nil)
+		argv = tl argv;
+	return ref Arg(argv, 0, "");
+}
+
+Arg.opt(arg: self ref Arg): int
+{
+	opts := arg.opts;
+	if(opts != ""){
+		arg.c = opts[0];
+		arg.opts = opts[1:];
+		return arg.c;
+	}
+	argv := arg.argv;
+	if(argv == nil)
+		return arg.c = 0;
+	opts = hd argv;
+	if(len opts < 2 || opts[0] != '-')
+		return arg.c = 0;
+	arg.argv = tl argv;
+	if(opts == "--")
+		return arg.c = 0;
+	arg.opts = opts[2:];
+	return arg.c = opts[1];
+}
+
+Arg.arg(arg: self ref Arg): string
+{
+	s := arg.opts;
+	arg.opts = "";
+	if(s != "")
+		return s;
+	argv := arg.argv;
+	if(argv == nil)
+		return "";
+	arg.argv = tl argv;
+	return hd argv;
+}
--- /dev/null
+++ b/appl/cmd/zeros.b
@@ -1,0 +1,68 @@
+implement Zeros;
+
+include "sys.m";
+	sys: Sys;
+include "arg.m";
+	arg: Arg;
+include "string.m";
+	str: String;
+include "keyring.m";
+include "security.m";
+	random: Random;
+
+include "draw.m";
+
+Zeros: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	z: array of byte;
+	i: int;
+	sys = load Sys Sys->PATH;
+	arg = load Arg Arg->PATH;
+	str = load String String->PATH;
+
+	if(sys == nil || arg == nil)
+		return;
+
+	bs := 0;
+	n := 0;
+	val := 0;
+	rflag := 0;
+	arg->init(argv);
+	while ((c := arg->opt()) != 0)
+		case c {
+		'r' => rflag = 1;
+		'v' => (val, nil) = str->toint(arg->arg(), 16);
+		* => raise sys->sprint("fail:unknown option (%c)\n", c);
+		}
+	argv = arg->argv();
+	if(len argv >= 1)
+		bs = int hd argv;
+	else
+		bs = 1;
+	if (len argv >= 2)
+		n = int hd tl argv;
+	else
+		n = 1;
+	if(bs == 0 || n == 0) {
+		sys->fprint(sys->fildes(2), "usage: zeros [-r] [-v value] blocksize [number]\n");
+		raise "fail:usage";
+	}
+	if (rflag) {
+		random = load Random Random->PATH;
+		if (random == nil)
+			raise "fail:no security module\n";
+		z = random->randombuf(random->NotQuiteRandom, bs);
+	}
+	else {
+		z = array[bs] of byte;
+		for(i=0;i<bs;i++)
+			z[i] = byte val;
+	}
+	for(i=0;i<n;i++)
+		sys->write(sys->fildes(1), z, bs);
+}
--- /dev/null
+++ b/appl/collab/clients/chat.b
@@ -1,0 +1,224 @@
+implement Chat;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+Chat: module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+stderr: ref Sys->FD;
+
+tksetup := array [] of {
+	"frame .f",
+	"text .f.t -state disabled -wrap word -yscrollcommand {.f.sb set}",
+	"scrollbar .f.sb -orient vertical -command {.f.t yview}",
+	"entry .e -bg white",
+	"bind .e <Key-\n> {send cmd send}",
+	"pack .f.sb -in .f -side left -fill y",
+	"pack .f.t -in .f -side left -fill both -expand 1",
+	"pack .f -side top -fill both -expand 1",
+	"pack .e -side bottom -fill x",
+	"pack propagate . 0",
+	"update",
+};
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmodule(Draw->PATH);
+
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmodule(Tk->PATH);
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmodule(Tkclient->PATH);
+
+
+	if (len args < 2) {
+		sys->fprint(stderr, "usage: chat [servicedir] room\n");
+		raise "fail:init";
+	}
+	args = tl args;
+
+	servicedir := "/n/remote/services";
+	if(len args == 2)
+		(servicedir, args) = (hd args, tl args);
+	room := hd args;
+
+	tkclient->init();
+	(win, winctl) := tkclient->toplevel(ctxt, nil, sys->sprint("Chat %s", room), Tkclient->Appl);
+
+	cmd := chan of string;
+	tk->namechan(win, cmd, "cmd");
+	tkcmds(win, tksetup);
+	tkcmd(win, ". configure -height 300");
+	fittoscreen(win);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+	msgs := chan of string;
+	conn := chan of (string, ref Sys->FD);
+	spawn connect(servicedir, room, msgs, conn);
+	msgsfd: ref Sys->FD;
+
+	for (;;) alt {
+	(e, fd) := <-conn =>
+		if (msgsfd == nil) {
+			if (e == nil) {
+				output(win, "*** connected");
+				msgsfd = fd;
+			} else
+				output(win, "*** " + e);
+		} else {
+			output(win, "*** disconnected");
+			msgsfd = nil;
+		}
+
+	txt := <-msgs =>
+		output(win, txt);
+
+	<- cmd =>
+		msg := tkcmd(win, ".e get");
+		if (msgsfd != nil && msg != nil) {
+			tkcmd(win, ".f.t see end");
+			tkcmd(win, ".e delete 0 end");
+			tkcmd(win, "update");
+			d := array of byte msg;
+			sys->write(msgsfd, d, len d);
+		}
+
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-winctl =>
+		tkclient->wmctl(win, s);
+	}
+}
+
+err(s: string)
+{
+	sys->fprint(stderr, "chat: %s\n", s);
+	raise "fail:err";
+}
+
+badmodule(path: string)
+{
+	err(sys->sprint("can't load module %s: %r", path));
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++)
+		tkcmd(t, cmds[i]);
+}
+
+tkcmd(t: ref Tk->Toplevel, cmd: string): string
+{
+	s := tk->cmd(t, cmd);
+	if (s != nil && s[0] == '!')
+		sys->fprint(stderr, "chat: tk error: %s [%s]\n", s, cmd);
+	return s;
+}
+
+connect(dir, name: string, msgs: chan of string, conn: chan of (string, ref Sys->FD))
+{
+	(ctlfd, srvdir, emsg) := opensvc(dir, "chat", name);
+	if(ctlfd == nil) {
+		conn <-= (emsg, nil);
+		return;
+	}
+	srvpath := srvdir+"/msgs";
+	msgsfd := sys->open(srvpath, Sys->ORDWR);
+	if(msgsfd == nil) {
+		conn <-= (sys->sprint("internal error: can't open %s: %r", srvpath), nil);
+		return;
+	}
+	conn <-= (nil, msgsfd);
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(msgsfd, buf, len buf)) > 0)
+		msgs <-= string buf[0:n];
+	conn <-= (nil, nil);
+}
+
+opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string)
+{
+	ctlfd := sys->open(dir+"/ctl", Sys->ORDWR);
+	if(ctlfd == nil)
+		return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir));
+	if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0)
+		return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name));
+	buf := array [32] of byte;
+	sys->seek(ctlfd, big 0, Sys->SEEKSTART);
+	n := sys->read(ctlfd, buf, len buf);
+	if (n <= 0)
+		return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir));
+	return (ctlfd, dir+"/"+string buf[0:n], nil);
+}
+
+firstmsg := 1;
+output(win: ref Tk->Toplevel, txt: string)
+{
+	if (firstmsg)
+		firstmsg = 0;
+	else
+		txt = "\n" + txt;
+	yview := tkcmd(win, ".f.t yview");
+	(nil, toks) := sys->tokenize(yview, " ");
+	toks = tl toks;
+
+	tkcmd(win, ".f.t insert end '" + txt);
+	if (hd toks == "1")
+		tkcmd(win, ".f.t see end");
+	tkcmd(win, "update");
+}
+
+KEYBOARDH: con 90;
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	Point, Rect: import draw;
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y)- KEYBOARDH);
+	bd := int tkcmd(win, ". cget -bd");
+	winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2,
+				int tkcmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
--- /dev/null
+++ b/appl/collab/clients/poll.b
@@ -1,0 +1,282 @@
+implement Poll;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "arg.m";
+
+Poll: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Maxanswer: con 4;
+
+contents := array[] of {
+	"frame .f",
+	"frame .a",
+	"radiobutton .a.a1 -state disabled -variable answer -value A -text {A} -command {send entry A}",
+	"radiobutton .a.a2 -state disabled -variable answer -value B -text {B} -command {send entry B}",
+	"radiobutton .a.a3 -state disabled -variable answer -value C -text {C} -command {send entry C}",
+	"radiobutton .a.a4 -state disabled -variable answer -value D -text {D} -command {send entry D}",
+	"pack .a.a1 -side top -fill x -expand 1",
+	"pack .a.a2 -side top -fill x -expand 1",
+	"pack .a.a3 -side top -fill x -expand 1",
+	"pack .a.a4 -side top -fill x -expand 1",
+	"pack .a -side top -fill both -expand 1",
+	"pack .f -side top -fill both",
+};
+
+dbcontents := array[] of {
+	"text .f.t -state disabled -wrap word -yscrollcommand {.f.sb set} -height 4h",
+	"scrollbar .f.sb -orient vertical -command {.f.t yview}",
+	"pack .f.sb -side left -fill y",
+	"pack .f.t -side left -fill both -expand 1",
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: poll [-d] [servicedir] pollname\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil){
+		sys->fprint(sys->fildes(2), "poll: can't load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+	arg->init(args);
+	debug := 0;
+	while((ch := arg->opt()) != 0)
+		case ch {
+		'd' =>
+			debug = 1;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	if(len args < 1)
+		usage();
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	servicedir := "/n/remote/services";
+	if(len args == 2)
+		(servicedir, args) = (hd args, tl args);
+	pollname := hd args;
+
+	(cfd, dir, emsg) := opensvc(servicedir, "mpx", pollname);
+	if(cfd == nil){
+		sys->fprint(sys->fildes(2), "poll: can't access poll %s: %s\n", pollname, emsg);
+		raise "fail:error";
+	}
+	fd := sys->open(dir+"/leaf", Sys->ORDWR);
+	if(fd == nil){
+		sys->fprint(sys->fildes(2), "poll: can't open %s/leaf: %r\n", dir);
+		raise "fail:open";
+	}
+
+	tkclient->init();
+	dialog->init();
+	(frame, wmctl) := tkclient->toplevel(ctxt, nil, sys->sprint("Poll %s", pollname), Tkclient->Appl);
+	entry := chan of string;
+	tk->namechan(frame, entry, "entry");
+	tkcmds(frame, contents);
+	if(debug)
+		tkcmds(frame, dbcontents);
+	tkcmd(frame, "pack propagate . 0");
+	fittoscreen(frame);
+	tk->cmd(frame, "update");
+	tkclient->onscreen(frame, nil);
+	tkclient->startinput(frame, "kbd"::"ptr"::nil);
+
+	in := chan of string;
+	spawn reader(fd, in);
+	first := 1;
+	lastval := -1;
+	qno := -1;
+	for(;;)
+		alt{
+		s := <-frame.ctxt.kbd =>
+			tk->keyboard(frame, s);
+		s := <-frame.ctxt.ptr =>
+			tk->pointer(frame, *s);
+		s := <-frame.ctxt.ctl or
+		s = <-frame.wreq or
+		s = <-wmctl =>
+			tkclient->wmctl(frame, s);
+
+		msg := <-entry =>
+			if(fd == nil){
+				dialog->prompt(ctxt, frame.image, "error -fg red", "Error", "Lost connection to polling station", 0, "Dismiss"::nil);
+				break;
+			}
+			n := msg[0]-'A';
+			lastval = n;
+			selectonly(frame, n, Maxanswer, "disabled");
+			if(qno >= 0) {
+				# send our answer to the polling station
+				if(sys->fprint(fd, "%d %s", qno, msg) < 0){
+					sys->fprint(sys->fildes(2), "poll: write error: %r\n");
+					fd = nil;
+				}
+				qno = -1;	# only one go at it
+			}
+
+		s := <-in =>
+			if(s != nil){
+				if(debug){
+					t := s;
+					if(!first)
+						t = "\n"+t;
+					first = 0;
+					tk->cmd(frame, ".f.t insert end '" + t);
+					tk->cmd(frame, ".f.t see end");
+					tk->cmd(frame, "update");
+				}
+				(nf, flds) := sys->tokenize(s, " ");
+				if(nf > 1 && hd flds == "error:"){
+					dialog->prompt(ctxt, frame.image, "error -fg red", "Error", sys->sprint("polling station reports: %s", s), 0, "Dismiss"::nil);
+					break;
+				}
+				if(nf < 4)
+					break;
+				# seq clientid op name data
+				op, name: string;
+				flds = tl flds;	# ignore seq
+				flds = tl flds;	# ignore clientid
+				(op, flds) = (hd flds, tl flds);
+				(name, flds) = (hd flds, tl flds);
+				case op {
+				"M" =>
+					# poll qno nanswer opt
+					# stop qno
+					selectonly(frame, -1, Maxanswer, "disabled");
+					if(len flds < 2)
+						break;
+					(op, flds) = (hd flds, tl flds);
+					(s, flds) = (hd flds, tl flds);
+					case op {
+					"poll" =>
+						qno = int s;
+						(s, flds) = (hd flds, tl flds);
+						n := int s;
+						if(n > Maxanswer)
+							n = Maxanswer;
+						if(n < 2)
+							n = 2;
+						selectonly(frame, -1, n, "normal");
+						lastval = -1;
+					"stop" =>
+						selectonly(frame, lastval, Maxanswer, "disabled");
+					}
+				"L" =>
+					dialog->prompt(ctxt, frame.image, "error -fg red", "Notice", sys->sprint("Poller (%s) has gone", name), 0, "Exit"::nil);
+					tkclient->wmctl(frame, "exit");
+				}
+			}else{
+				dialog->prompt(ctxt, frame.image, "error -fg red", "Notice", "Polling station closed", 0, "Exit"::nil);
+				tkclient->wmctl(frame, "exit");
+			}
+		}
+}
+
+selectonly(t: ref Tk->Toplevel, n: int, top: int, state: string)
+{
+	for(i := 0; i < top; i++){
+		path := sys->sprint(".a.a%d", i+1);
+		if(i != n)
+			tkcmd(t, path+" deselect");
+		else
+			tkcmd(t, path+" select");
+		tkcmd(t, path+" configure -state "+state);
+	}
+	tk->cmd(t, "update");
+}
+
+reader(fd: ref Sys->FD, c: chan of string)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0)
+		c <-= string buf[0:n];
+	if(n < 0)
+		c <-= sys->sprint("error: %r");
+	c <-= nil;
+}
+
+opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string)
+{
+	ctlfd := sys->open(dir+"/ctl", Sys->ORDWR);
+	if(ctlfd == nil)
+		return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir));
+	if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0)
+		return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name));
+	buf := array [32] of byte;
+	sys->seek(ctlfd, big 0, Sys->SEEKSTART);
+	n := sys->read(ctlfd, buf, len buf);
+	if (n <= 0)
+		return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir));
+	return (ctlfd, dir+"/"+string buf[0:n], nil);
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++)
+		tkcmd(t, cmds[i]);
+}
+
+tkcmd(t: ref Tk->Toplevel, cmd: string): string
+{
+	s := tk->cmd(t, cmd);
+	if (s != nil && s[0] == '!')
+		sys->fprint(sys->fildes(2), "poll: tk error: %s [%s]\n", s, cmd);
+	return s;
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	draw := load Draw Draw->PATH;
+	Point, Rect: import draw;
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+	bd := int tkcmd(win, ". cget -bd");
+	winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2,
+				int tkcmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
--- /dev/null
+++ b/appl/collab/clients/poller.b
@@ -1,0 +1,330 @@
+implement Poller;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Rect, Point: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "arg.m";
+
+Poller: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Maxanswer: con 4;	# Tk below isn't parametrised, but could be
+
+contents := array[] of {
+	"frame .f",
+	"frame .f.n",
+	"label .f.l -anchor nw -text {Number of answers: }",
+	"radiobutton .f.n.a2 -text {2} -variable nanswer -value 2",
+	"radiobutton .f.n.a3 -text {3} -variable nanswer -value 3",
+	"radiobutton .f.n.a4 -text {4} -variable nanswer -value 4",
+	"pack .f.n.a2 .f.n.a3 .f.n.a4 -side left",
+
+	"frame .f.b",
+	"button .f.b.start -text {Start} -command {send cmd start}",
+	"button .f.b.stop -text {Stop} -state disabled -command {send cmd stop}",
+	"pack .f.b.start .f.b.stop -side left",
+
+	"canvas .f.c -height 230 -width 200",
+
+	"pack .f.l -side top -fill x",
+	"pack .f.n -side top -fill x",
+	"pack .f.b -side top -fill x -expand 1",
+	"pack .f.c -side top -pady 2",
+	"pack .f -side top -fill both -expand 1",
+};
+
+dbcontents := array[] of {
+	"text .f.t -state disabled -wrap word -height 4h -yscrollcommand {.f.sb set}",	# message log
+	"scrollbar .f.sb -orient vertical -command {.f.t yview}",
+	"pack .f.sb -side left -fill y",
+	"pack .f.t -side left -fill both",
+};
+
+Bar: adt {
+	frame:	ref Tk->Toplevel;
+	canvas:	string;
+	border:	string;
+	inside:	string;
+	label:	string;
+	r:	Rect;
+	v:	real;
+
+	draw:	fn(nil: self ref Bar);
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: poller [-d] [servicedir] pollname\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil){
+		sys->fprint(sys->fildes(2), "poller: can't load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+	arg->init(args);
+	debug := 0;
+	while((ch := arg->opt()) != 0)
+		case ch {
+		'd' =>
+			debug = 1;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	if(len args < 1)
+		usage();
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	servicedir := "/n/remote/services";
+	if(len args == 2)
+		(servicedir, args) = (hd args, tl args);
+	pollname := hd args;
+
+	(cfd, dir, emsg) := opensvc(servicedir, "mpx", pollname);
+	if(cfd == nil){
+		sys->fprint(sys->fildes(2), "poller: can't access polling station %s: %s\n", pollname, emsg);
+		raise "fail:error";
+	}
+	fd := sys->open(dir+"/root", Sys->ORDWR);
+	if(fd == nil){
+		sys->fprint(sys->fildes(2), "poller: can't open %s/root: %r\n", dir);
+		raise "fail:open";
+	}
+
+	tkclient->init();
+	dialog->init();
+	(frame, wmctl) := tkclient->toplevel(ctxt, nil, sys->sprint("Poller: %s", pollname), Tkclient->Appl);
+	cmd := chan of string;
+	tk->namechan(frame, cmd, "cmd");
+	tkcmds(frame, contents);
+	if(debug)
+		tkcmds(frame, dbcontents);
+	tkcmd(frame, "pack propagate . 0");
+	fittoscreen(frame);
+	tk->cmd(frame, "update");
+	tkclient->onscreen(frame, nil);
+	tkclient->startinput(frame, "kbd"::"ptr"::nil);
+
+	bars := mkbars(frame, ".f.c", Maxanswer);
+	count: array of int;
+
+	in := chan of string;
+	spawn reader(fd, in);
+	first := 1;
+	qno := 0;
+	nanswer := 0;
+	opt := 0;
+	total := 0;
+	for(;;)
+		alt{
+		s := <-frame.ctxt.kbd =>
+			tk->keyboard(frame, s);
+		s := <-frame.ctxt.ptr =>
+			tk->pointer(frame, *s);
+		s := <-frame.ctxt.ctl or
+		s = <-frame.wreq or
+		s = <-wmctl =>
+			tkclient->wmctl(frame, s);
+
+		c := <-cmd =>
+			if(fd == nil){
+				dialog->prompt(ctxt, frame.image, "error -fg red", "Error", "Lost connection to polling station", 0, "Dismiss"::nil);
+				break;
+			}
+			case c {
+			"start" =>
+				s := tkcmd(frame, "variable nanswer");
+				if(s == nil || s[0] == '!'){
+					dialog->prompt(ctxt, frame.image, "error -fg red", "Error", "Please select number of answers", 0, "Ok"::nil);
+					break;
+				}
+				nanswer = int s;
+				count = array[Maxanswer] of {* => 0};
+				total = 0;
+				qno++;
+				#opt = (int tkcmd(frame, "variable none") << 1) | int tkcmd(frame, "variable all");
+				tkcmd(frame, ".f.b.start configure -state disabled");
+				tkcmd(frame, ".f.b.stop configure -state normal");
+				if(sys->fprint(fd, "poll %d %d %d", qno, nanswer, opt) <= 0)
+					sys->fprint(sys->fildes(2), "poller: write error: %r\n");
+			"stop" =>
+				tkcmd(frame, ".f.b.stop configure -state disabled");
+				tkcmd(frame, "update");
+				if(sys->fprint(fd, "stop %d", qno) <= 0)
+					sys->fprint(sys->fildes(2), "poller: write error: %r\n");
+				# stop ...
+				tkcmd(frame, ".f.b.start configure -state normal");
+			}
+			tk->cmd(frame, "update");
+
+		s := <-in =>
+			if(s != nil){
+				if(debug){
+					t := s;
+					if(!first)
+						t = "\n"+t;
+					first = 0;
+					tkcmd(frame, ".f.t insert end '" + t);
+					tkcmd(frame, ".f.t see end");
+					tkcmd(frame, "update");
+				}
+				r := getresult(s, qno);
+				if(r < 0)
+					break;
+				if(r >= 0 && r < len count){
+					count[r]++;
+					total++;
+					for(i:=0; i < len count; i++){
+						bars[i].v = real count[i]/real total;
+						bars[i].draw();
+					}
+					tk->cmd(frame, "update");
+				}
+				#sys->print("%d %d\n", qno, r);
+			}else
+				fd = nil;
+		}
+}
+
+mkbars(t: ref Tk->Toplevel, canvas: string, nbars: int): array of ref Bar
+{
+	x := 0;
+	a := array[nbars] of ref Bar;
+	for(i := 0; i < nbars; i++){
+		b := ref Bar(t, canvas, nil, nil, nil, Rect((x,2),(x+20,202)), 0.0);
+		b.border = tkcmd(t, sys->sprint("%s create rectangle %d %d %d %d",
+			canvas, b.r.min.x,b.r.min.y,b.r.max.x,b.r.max.y));
+		r := b.r.inset(1);
+		b.inside = tkcmd(t, sys->sprint("%s create rectangle %d %d %d %d -fill red",
+			canvas, r.max.x, r.max.y,r.max.x,r.max.y));
+		b.label = tkcmd(t, sys->sprint("%s create text %d %d -justify center -anchor n -text '0%%",
+			canvas, (r.min.x+r.max.x)/2, r.max.y+4));
+		a[i] = b;
+		x += 50;
+	}
+	tk->cmd(t, "update");
+	return a;
+}
+
+Bar.draw(b: self ref Bar)
+{
+	r := b.r.inset(2);
+	y := r.max.y - int (b.v * real r.dy());
+	tkcmd(b.frame, sys->sprint("%s coords %s %d %d %d %d",
+		b.canvas, b.inside, r.min.x, y, r.max.x, r.max.y));
+	tkcmd(b.frame, sys->sprint("%s itemconfigure %s -text '%.0f%%",
+		b.canvas, b.label, b.v*100.0));
+}
+
+getresult(msg: string, qno: int): int
+{
+	(nf, flds) := sys->tokenize(msg, " ");
+	if(nf < 5 || hd flds == "error:")
+		return -1;	# not of interest
+	op := hd tl tl flds;
+	flds = tl tl tl flds;
+	if(op != "m")
+		return -1; # not a message from leaf
+	if(len flds < 3)
+		return -1;	# bad format
+	flds = tl flds;	# ignore user name
+	if(int hd flds != qno)
+		return -1;	# not current question
+	result := hd tl flds;
+	if(result[0] >= 'A' && result[0] <= 'D')
+		return result[0]-'A';
+	return -1;
+}
+	
+reader(fd: ref Sys->FD, c: chan of string)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0)
+		c <-= string buf[0:n];
+	if(n < 0)
+		c <-= sys->sprint("error: %r");
+	c <-= nil;
+}
+
+opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string)
+{
+	ctlfd := sys->open(dir+"/ctl", Sys->ORDWR);
+	if(ctlfd == nil)
+		return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir));
+	if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0)
+		return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name));
+	buf := array [32] of byte;
+	sys->seek(ctlfd, big 0, Sys->SEEKSTART);
+	n := sys->read(ctlfd, buf, len buf);
+	if (n <= 0)
+		return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir));
+	return (ctlfd, dir+"/"+string buf[0:n], nil);
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++)
+		tkcmd(t, cmds[i]);
+}
+
+tkcmd(t: ref Tk->Toplevel, cmd: string): string
+{
+	s := tk->cmd(t, cmd);
+	if (s != nil && s[0] == '!')
+		sys->fprint(sys->fildes(2), "poller: tk error: %s [%s]\n", s, cmd);
+	return s;
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+	bd := int tkcmd(win, ". cget -bd");
+	winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2,
+				int tkcmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
--- /dev/null
+++ b/appl/collab/clients/whiteboard.b
@@ -1,0 +1,586 @@
+implement Whiteboard;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Screen, Display, Image, Rect, Point, Font: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+Whiteboard: module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+ERASEWIDTH: con 6;
+
+
+stderr: ref Sys->FD;
+srvfd: ref Sys->FD;
+disp: ref Display;
+font: ref Draw->Font;
+drawctxt: ref Draw->Context;
+
+tksetup := array[] of {
+	"frame .f -bd 2",
+	"frame .c -bg white -width 234 -height 279",
+	"menu .penmenu",
+	".penmenu add command -command {send cmd pen 0} -bitmap @/icons/whiteboard/0.bit",
+	".penmenu add command -command {send cmd pen 1} -bitmap @/icons/whiteboard/1.bit",
+	".penmenu add command -command {send cmd pen 2} -bitmap @/icons/whiteboard/2.bit",
+	".penmenu add command -command {send cmd pen erase} -bitmap @/icons/whiteboard/erase.bit",
+	"menubutton .pen -menu .penmenu -bitmap @/icons/whiteboard/1.bit",
+	"button .colour -bg black -activebackground black -command {send cmd getcolour}",
+	"pack .c -in .f",
+	"pack .f -side top -anchor center",
+	"pack .pen -side left",
+	"pack .colour -side left -fill both -expand 1",
+	"update",
+};
+
+tkconnected := array[] of {
+	"bind .c <Button-1> {send cmd down %x %y}",
+	"bind .c <ButtonRelease-1> {send cmd up %x %y}",
+	"update",
+};
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+
+	if (len args < 2) {
+		sys->fprint(stderr, "Usage: whiteboard [servicedir] id\n");
+		raise "fail:init";
+	}
+
+	args = tl args;
+	servicedir := "/n/remote/services";
+	if(len args == 2)
+		(servicedir, args) = (hd args, tl args);
+	wbid := hd args;
+
+	disp = ctxt.display;
+	if (disp == nil) {
+		sys->fprint(stderr, "bad Draw->Context\n");
+		raise "fail:init";
+	}
+	drawctxt = ctxt;
+
+	tkclient->init();
+	(win, winctl) := tkclient->toplevel(ctxt, nil, "Whiteboard", 0);
+	font = Font.open(disp, tkcmd(win, ". cget -font"));
+	if(font == nil)
+		font = Font.open(disp, "*default*");
+	cmd := chan of string;
+	tk->namechan(win, cmd, "cmd");
+	tkcmds(win, tksetup);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd" :: "ptr" :: nil);
+	cimage := makeimage(win);
+
+	sc := chan of array of (Point, Point);
+	cc := chan of (string, ref Image, ref Sys->FD, ref Sys->FD);
+	connected := 0;
+	sfd: ref Sys->FD;
+	ctlfd: ref Sys->FD;	# must keep this open to keep service active
+
+	showtext(cimage, "connecting...");
+	spawn connect(servicedir, wbid, cc);
+
+	err: string;
+	strokeimg: ref Image;
+Connect:
+	for (;;) alt {
+	(err, strokeimg, sfd, ctlfd) = <-cc =>
+		if (err == nil)
+			break Connect;
+		else
+			showtext(cimage, "Error: " + err);
+
+	s := <-winctl or
+	s = <-win.wreq or
+	s = <-win.ctxt.ctl =>
+		oldimg := win.image;
+		err = tkclient->wmctl(win, s);
+		if(s[0] == '!' && err == nil && win.image != oldimg){
+			cimage = makeimage(win);
+			showtext(cimage, "connecting...");
+		}
+	p := <-win.ctxt.ptr =>
+		tk->pointer(win, *p);
+	c := <-win.ctxt.kbd =>
+		tk->keyboard(win, c);
+	}
+
+	tkcmd(win, ".c configure -width " + string strokeimg.r.dx());
+	tkcmd(win, ".c configure -height " + string strokeimg.r.dy());
+	tkcmds(win, tkconnected);
+	tkcmd(win, "update");
+	cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min);
+
+	strokesin := chan of (int, int, array of Point);
+	strokesout := chan of (int, int, Point, Point);
+	spawn reader(sfd, strokesin);
+	spawn writer(sfd, strokesout);
+
+	pendown := 0;
+	p0, p1: Point;
+
+	getcolour := 0;
+	white := disp.white;
+	whitepen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, Draw->White);
+	pencolour := Draw->Black;
+	penwidth := 1;
+	erase := 0;
+	drawpen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour);
+
+	for (;;) alt {
+	s := <-winctl or
+	s = <-win.ctxt.ctl or
+	s = <-win.wreq =>
+		oldimg := win.image;
+		err = tkclient->wmctl(win, s);
+		if(s[0] == '!' && err == nil && win.image != oldimg){
+			cimage = makeimage(win);
+			cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min);
+		}
+	p := <-win.ctxt.ptr =>
+		tk->pointer(win, *p);
+	c := <-win.ctxt.kbd =>
+		tk->keyboard(win, c);
+	(colour, width, strokes) := <-strokesin =>
+		if (strokes == nil)
+			tkclient->settitle(win, "Whiteboard (Disconnected)");
+		else {
+			pen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, colour);
+			drawstrokes(cimage, cimage.r.min, pen, width, strokes);
+			drawstrokes(strokeimg, strokeimg.r.min, pen, width, strokes);
+		}
+
+	c := <-cmd =>
+		(nil, toks) := sys->tokenize(c, " ");
+		case hd toks {
+		"down" =>
+			toks = tl toks;
+			x := int hd toks;
+			y := int hd tl toks;
+			if (!pendown) {
+				pendown = 1;
+				p0 = Point(x, y);
+				continue;
+			}
+			p1 = Point(x, y);
+			if (p1.x == p0.x && p1.y == p0.y)
+				continue;
+			pen := drawpen;
+			colour := pencolour;
+			width := penwidth;
+			if (erase) {
+				pen = whitepen;
+				colour = Draw->White;
+				width = ERASEWIDTH;
+			}
+			drawstroke(cimage, cimage.r.min, p0, p1, pen, width);
+			drawstroke(strokeimg, strokeimg.r.min, p0, p1, pen, width);
+			strokesout <-= (colour, width, p0, p1);
+			p0 = p1;
+		"up" =>
+			pendown = 0;
+
+		"getcolour" =>
+			pendown = 0;
+			if (!getcolour)
+				spawn colourmenu(cmd);
+		"colour" =>
+			pendown = 0;
+			getcolour = 0;
+			toks = tl toks;
+			if (toks == nil)
+				# colourmenu was dismissed
+				continue;
+			erase = 0;
+			tkcmd(win, ".pen configure -bitmap @/icons/whiteboard/" + string penwidth + ".bit");
+			tkcmd(win, "update");
+			pencolour = int hd toks;
+			toks = tl toks;
+			tkcolour := hd toks;
+			drawpen = disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour);
+			tkcmd(win, ".colour configure -bg " + tkcolour + " -activebackground " + tkcolour);
+			tkcmd(win, "update");
+
+		"pen" =>
+			pendown = 0;
+			p := hd tl toks;
+			i := "";
+			if (p == "erase") {
+				erase = 1;
+				i = "erase.bit";
+			} else {
+				erase = 0;
+				penwidth = int p;
+				i = p + ".bit";
+			}
+			tkcmd(win, ".pen configure -bitmap @/icons/whiteboard/" + i);
+			tkcmd(win, "update");
+		}
+
+	}
+}
+
+makeimage(win: ref Tk->Toplevel): ref Draw->Image
+{
+	if(win.image == nil)
+		return nil;
+	scr := Screen.allocate(win.image, win.image.display.white, 0);
+	w := scr.newwindow(tk->rect(win, ".c", Tk->Local), Draw->Refnone, Draw->Nofill);
+	return w;
+}
+
+showtext(img: ref Image, s: string)
+{
+	r := img.r;
+	r.max.y = img.r.min.y + font.height;
+	img.draw(r, disp.white, nil, (0, 0));
+	img.text(r.min, disp.black, (0, 0), font, s);
+}
+
+penmenu(t: ref Tk->Toplevel, p: Point)
+{
+	topy := int tkcmd(t, ".penmenu yposition 0");
+	boty := int tkcmd(t, ".penmenu yposition end");
+	dy := boty - topy;
+	p.y -= dy;
+	tkcmd(t, ".penmenu post " + string p.x + " " + string p.y);
+}
+
+colourcmds := array[] of {
+	"label .l -height 10",
+	"frame .c -height 224 -width 224",
+	"pack .l -fill x -expand 1",
+	"pack .c -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+	"bind .c <Button-1> {send cmd push %x %y}",
+	"bind .c <ButtonRelease-1> {send cmd release}",
+};
+
+lastcolour := "255";
+lasttkcolour := "#000000";
+
+colourmenu(c: chan of string)
+{
+	(t, winctl) := tkclient->toplevel(drawctxt, nil, "Whiteboard", Tkclient->OK);
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	tkcmds(t, colourcmds);
+	tkcmd(t, ".l configure -bg " + lasttkcolour);
+	tkcmd(t, "update");
+	tkclient->onscreen(t, "onscreen");
+	tkclient->startinput(t, "kbd" :: "ptr" :: nil);
+
+	drawcolours(t.image, tk->rect(t, ".c", Tk->Local));
+
+	for(;;) alt {
+	p := <-t.ctxt.ptr =>
+		tk->pointer(t, *p);
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-winctl or
+	s = <-t.ctxt.ctl or
+	s = <-t.wreq =>
+		case s{
+		"ok" =>
+			c <-= "colour " + lastcolour + " " + lasttkcolour;
+			return;
+		"exit" =>
+			c <-= "colour";
+			return;
+		* =>
+			oldimage := t.image;
+			e := tkclient->wmctl(t, s);
+			if(s[0] == '!' && e == nil && oldimage != t.image)
+				drawcolours(t.image, tk->rect(t, ".c", Tk->Local));
+		}
+
+	press := <-cmd =>
+		(n, word) := sys->tokenize(press, " ");
+		case hd word {
+		"push" =>
+			(lastcolour, lasttkcolour) = color(int hd tl word, int hd tl tl word, tk->rect(t, ".c", 0).size());
+			tkcmd(t, ".l configure -bg " + lasttkcolour);
+		}
+	}
+}
+
+drawcolours(img: ref Image, cr: Rect)
+{
+	# use writepixels because it's much faster than allocating all those colors.
+	tmp := disp.newimage(((0,0),(cr.dx(),cr.dy()/16+1)), Draw->CMAP8, 0, 0);
+	if(tmp == nil)
+		return;
+	buf := array[tmp.r.dx()*tmp.r.dy()] of byte;
+	dx := cr.dx();
+	dy := cr.dy();
+	for(y:=0; y<16; y++){
+		for(i:=tmp.r.dx()-1; i>=0; --i)
+			buf[i] = byte (16*y+(16*i)/dx);
+		for(k:=tmp.r.dy()-1; k>=1; --k)
+			buf[dx*k:] = buf[0:dx];
+		tmp.writepixels(tmp.r, buf);
+		r: Rect;
+		r.min.x = cr.min.x;
+		r.max.x = cr.max.x;
+		r.min.y = cr.min.y+(dy*y)/16;
+		r.max.y = cr.min.y+(dy*(y+1))/16;
+		img.draw(r, tmp, nil, tmp.r.min);
+	}
+}
+
+color(x, y: int, size: Point): (string, string)
+{
+	x = (16*x)/size.x;
+	y = (16*y)/size.y;
+	col := 16*y+x;
+	(r, g, b) := disp.cmap2rgb(col);
+	tks := sys->sprint("#%.2x%.2x%.2x", r, g, b);
+	return (string disp.cmap2rgba(col), tks);
+}
+
+opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string)
+{
+	ctlfd := sys->open(dir+"/ctl", Sys->ORDWR);
+	if(ctlfd == nil)
+		return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir));
+	if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0)
+		return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name));
+	buf := array [32] of byte;
+	sys->seek(ctlfd, big 0, Sys->SEEKSTART);
+	n := sys->read(ctlfd, buf, len buf);
+	if (n <= 0)
+		return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir));
+	return (ctlfd, dir+"/"+string buf[0:n], nil);
+}
+
+connect(dir, name: string, res: chan of (string, ref Image, ref Sys->FD, ref Sys->FD))
+{
+	(ctlfd, srvdir, emsg) := opensvc(dir, "whiteboard", name);
+	if(ctlfd == nil) {
+		res <-= (emsg, nil, nil, nil);
+		return;
+	}
+
+	bitpath := srvdir + "/wb.bit";
+	strokepath := srvdir + "/strokes";
+
+	sfd := sys->open(strokepath, Sys->ORDWR);
+	if (sfd == nil) {
+		err := sys->sprint("cannot open whiteboard data: %r");
+		res <-= (err, nil, nil, nil);
+		srvfd = nil;
+		return;
+	}
+
+	bfd := sys->open(bitpath, Sys->OREAD);
+	if (bfd == nil) {
+		err := sys->sprint("cannot open whiteboard image: %r");
+		res <-= (err, nil, nil, nil);
+		srvfd = nil;
+		return;
+	}
+
+	img := disp.readimage(bfd);
+	if (img == nil) {
+		err := sys->sprint("cannot read whiteboard image: %r");
+		res <-= (err, nil, nil, nil);
+		srvfd = nil;
+		return;
+	}
+sys->print("read image ok\n");
+
+	# make sure image is depth 8 (because of image.line() bug)
+	if (img.depth != 8) {
+sys->print("depth is %d, not 8\n", img.depth);
+		nimg := disp.newimage(img.r, Draw->CMAP8, 0, 0);
+		if (nimg == nil) {
+			res <-= ("cannot allocate local image", nil, nil, nil);
+			srvfd = nil;
+			return;
+		}
+		nimg.draw(nimg.r, img, nil, img.r.min);
+		img = nimg;
+	}
+
+	res <-= (nil, img, sfd, ctlfd);
+}
+
+reader(fd: ref Sys->FD, sc: chan of (int, int, array of Point))
+{
+	buf := array [Sys->ATOMICIO] of byte;
+
+	for (;;) {
+		n := sys->read(fd, buf, len buf);
+		if (n <= 0) {
+			sc <-= (0, 0, nil);
+			return;
+		}
+		s := string buf[0:n];
+		(npts, toks) := sys->tokenize(s, " ");
+		if (npts & 1)
+			# something wrong
+			npts--;
+		if (npts < 6)
+			# ignore
+			continue;
+
+		colour, width: int;
+		(colour, toks) = (int hd toks, tl toks);
+		(width, toks) = (int hd toks, tl toks);
+		pts := array [(npts - 2)/ 2] of Point;
+		for (i := 0; toks != nil; i++) {
+			x, y: int;
+			(x, toks) = (int hd toks, tl toks);
+			(y, toks) = (int hd toks, tl toks);
+			pts[i] = Point(x, y);
+		}
+		sc <-= (colour, width, pts);
+		pts = nil;
+	}
+}
+
+Wmsg: adt {
+	data: array of byte;
+	datalen: int;
+	next: cyclic ref Wmsg;
+};
+
+writer(fd: ref Sys->FD, sc: chan of (int, int, Point, Point))
+{
+	lastcol := -1;
+	lastw := -1;
+	lastpt := Point(-1, -1);
+	curmsg: ref Wmsg;
+	nextmsg: ref Wmsg;
+
+	eofc := chan of int;
+	wc := chan of ref Wmsg;
+	wseof := 0;
+	spawn wslave(fd, wc, eofc);
+
+	for (;;) {
+		colour := -1;
+		width := 0;
+		p0, p1: Point;
+
+		if (curmsg == nil || wseof)
+			(colour, width, p0, p1) = <-sc;
+		else alt {
+		wseof = <-eofc =>
+			;
+
+		(colour, width, p0, p1) = <-sc =>
+			;
+
+		wc <-= curmsg =>
+			curmsg = curmsg.next;
+			continue;
+		}
+
+		newseq := 0;
+		if (curmsg == nil) {
+			curmsg = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
+			nextmsg = curmsg;
+			newseq = 1;
+		}
+
+		if (colour != lastcol || width != lastw || p0.x != lastpt.x || p0.y != lastpt.y)
+			newseq = 1;
+
+		d: array of byte = nil;
+		if (!newseq) {
+			d = sys->aprint(" %d %d", p1.x, p1.y);
+			if (nextmsg.datalen + len d >= Sys->ATOMICIO) {
+				nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
+				nextmsg = nextmsg.next;
+				newseq = 1;
+			}
+		}
+		if (newseq) {
+			d = sys->aprint(" %d %d %d %d %d %d", colour, width, p0.x, p0.y, p1.x, p1.y);
+			if (nextmsg.datalen != 0) {
+				nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
+				nextmsg = nextmsg.next;
+			}
+		}
+		nextmsg.data[nextmsg.datalen:] = d;
+		nextmsg.datalen += len d;
+		lastcol = colour;
+		lastw = width;
+		lastpt = p1;
+	}
+}
+
+wslave(fd: ref Sys->FD, wc: chan of ref Wmsg, eof: chan of int)
+{
+	for (;;) {
+		wm := <-wc;
+		n := sys->write(fd, wm.data, wm.datalen);
+		if (n != wm.datalen)
+			break;
+	}
+	eof <-= 1;
+}
+
+drawstroke(img: ref Image, offset, p0, p1: Point, pen: ref Image, width: int)
+{
+	p0 = p0.add(offset);
+	p1 = p1.add(offset);
+	img.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, p0);
+}
+
+drawstrokes(img: ref Image, offset: Point, pen: ref Image, width: int, pts: array of Point)
+{
+	if (len pts < 2)
+		return;
+	p0, p1: Point;
+	p0 = pts[0].add(offset);
+	for (i := 1; i < len pts; i++) {
+		p1 = pts[i].add(offset);
+		img.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, p0);
+		p0 = p1;
+	}
+}
+
+badmod(mod: string)
+{
+	sys->fprint(stderr, "cannot load %s: %r\n", mod);
+	raise "fail:bad module";
+}
+
+tkcmd(t: ref Tk->Toplevel, cmd: string): string
+{
+	s := tk->cmd(t, cmd);
+	if (s != nil && s[0] == '!') {
+		sys->fprint(stderr, "%s\n", cmd);
+		sys->fprint(stderr, "tk error: %s\n", s);
+	}
+	return s;
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++)
+		tkcmd(t, cmds[i]);
+}
--- /dev/null
+++ b/appl/collab/collabsrv.b
@@ -1,0 +1,176 @@
+implement Collabsrv;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+include "security.m";
+	auth: Auth;
+
+include "srvmgr.m";
+include "proxy.m";
+
+include "arg.m";
+
+Collabsrv: module
+{
+	init: fn (ctxt: ref Draw->Context, args: list of string);
+};
+
+authinfo: ref Keyring->Authinfo;
+
+stderr: ref Sys->FD;
+Srvreq, Srvreply: import Srvmgr;
+
+usage()
+{
+	sys->fprint(stderr, "usage: collabsrv [-k keyfile] [-n netaddress] [dir]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+
+	(err, user) := user();
+	if (err != nil)
+		error(err);
+
+	netaddr := "tcp!*!9999";
+	keyfile := "/usr/" + user + "/keyring/default";
+	root := "/services/collab";
+
+	arg := load Arg Arg->PATH;
+	arg->init(args);
+	while ((opt := arg->opt()) != 0)
+		case opt {
+		'k' =>
+			keyfile = arg->arg();
+			if (keyfile == nil)
+				usage();
+			if (keyfile[0] != '/' && (len keyfile < 2 || keyfile[0:2] != "./"))
+				keyfile = "/usr/" + user + "/keyring/" + keyfile;
+		'n' =>
+			netaddr = arg->arg();
+			if (netaddr == nil)
+				usage();
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+	if(args != nil)
+		root = hd args;
+
+	auth = load Auth Auth->PATH;
+	if (auth == nil)
+		badmodule(Auth->PATH);
+
+	kr := load Keyring Keyring->PATH;
+	if (kr == nil)
+		badmodule(Keyring->PATH);
+	
+	srvmgr := load Srvmgr Srvmgr->PATH;
+	if (srvmgr == nil)
+		badmodule(Srvmgr->PATH);
+
+	err = auth->init();
+	if (err != nil)
+		error(sys->sprint("failed to init Auth: %s", err));
+
+	authinfo = kr->readauthinfo(keyfile);
+	kr = nil;
+	if (authinfo == nil)
+		error(sys->sprint("cannot read %s: %r", keyfile));
+
+	netaddr = netmkaddr(netaddr, "tcp", "9999");
+	(ok, c) := sys->announce(netaddr);
+	if (ok < 0)
+		error(sys->sprint("cannot announce %s: %r", netaddr));
+
+	rc: chan of ref Srvreq;
+	(err, rc) = srvmgr->init(root);
+	if (err != nil)
+		error(err);
+
+	sys->print("Srvmgr started\n");
+
+	for (;;) {
+		(okl, nc) := sys->listen(c);
+		if (okl < 0) {
+			sys->print("listen failed: %r\n");
+			sys->sleep(1000);
+			return;
+		}
+		fd := sys->open(nc.dir+"/data", Sys->ORDWR);
+		if(nc.cfd != nil)
+			sys->fprint(nc.cfd, "keepalive");
+		nc.cfd = nil;
+		if (fd != nil)
+			spawn newclient(rc, fd, root);
+		fd = nil;
+	}
+}
+
+badmodule(path: string)
+{
+	error(sys->sprint("cannot load module %s: %r", path));
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "collabsrv: %s\n", s);
+	raise "fail:error";
+}
+
+user(): (string, string)
+{
+	sys = load Sys Sys->PATH;
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return (sys->sprint("can't open /dev/user: %r"), nil);
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return (sys->sprint("failed to read /dev/user: %r"), nil);
+	return (nil, string buf[0:n]);	
+}
+
+newclient(rc: chan of ref Srvreq, fd: ref Sys->FD, root: string)
+{
+	algs := "none" :: "clear" :: "md4" :: "md5" :: nil;
+	sys->print("new client\n");
+	proxy := load Proxy Proxy->PATH;
+	if (proxy == nil) {
+		sys->fprint(stderr, "collabsrv: cannot load %s: %r\n", Proxy->PATH);
+		return;
+	}
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS|Sys->FORKENV, nil);
+	s := "";
+	(fd, s) = auth->server(algs, authinfo, fd, 1);
+	if (fd == nil){
+		sys->fprint(stderr, "collabsrv: cannot authenticate: %s\n", s);
+		return;
+	}
+	sys->fprint(stderr, "uname: %s\n", s);
+	spawn proxy->init(root, fd, rc, s);
+}
+
+netmkaddr(addr, net, svc: string): string
+{
+	if(net == nil)
+		net = "net";
+	(n, l) := sys->tokenize(addr, "!");
+	if(n <= 1){
+		if(svc== nil)
+			return sys->sprint("%s!%s", net, addr);
+		return sys->sprint("%s!%s!%s", net, addr, svc);
+	}
+	if(svc == nil || n > 2)
+		return addr;
+	return sys->sprint("%s!%s", addr, svc);
+}
--- /dev/null
+++ b/appl/collab/connect.b
@@ -1,0 +1,156 @@
+implement Connect;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+include "keyring.m";
+include "security.m";
+include "arg.m";
+
+Connect: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+remotedir := "/n/remote";
+localdir := "/n/ftree/collab";
+COLLABPORT: con "9999";	# TO DO: needs symbolic name in services
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "Usage: connect [-v] [-C cryptoalg] [-k keyring] [net!addr [localdir]]\n");
+	raise "fail:usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+
+	vflag := 0;
+	alg := "none";
+	keyfile := "";
+	netaddr := "$collab";
+
+	arg := load Arg Arg->PATH;
+	if(arg != nil){
+		arg->init(args);
+		while((c := arg->opt()) != 0)
+			case c {
+			'C' =>
+				alg = arg->arg();
+			'k' =>
+				keyfile = arg->arg();
+			'v' =>
+				vflag++;
+			* =>
+				usage();
+			}
+	}
+	args = arg->argv();
+	arg = nil;
+
+	if(args != nil){
+		netaddr = hd args;
+		args = tl args;
+		if(args != nil)
+			localdir = hd args;
+	}
+
+	if(vflag)
+		sys->print("connect: dial %s\n", netaddr);
+	(fd, user) := authdial(netaddr, keyfile, alg);
+	if(vflag)
+		sys->print("remote username is %s\n", user);
+	if(sys->mount(fd, nil, remotedir, Sys->MREPL, nil) < 0)
+		error(sys->sprint("can't mount %s on %s: %r", netaddr, remotedir));
+	fd = nil;
+
+	connectdir := remotedir+"/collab";
+	if (sys->bind(connectdir, localdir, Sys->MCREATE|Sys->MREPL) < 0){
+		error(sys->sprint("cannot bind %s onto %s: %r\n", connectdir, localdir));
+		raise "fail:error";
+	}
+
+	# if something such as ftree is running and watching for changes, tell it about this one
+	fd = sys->open("/chan/nsupdate", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "/n/ftree/collab");
+	if(vflag)
+		sys->print("collab connected\n");
+}
+
+authdial(addr, keyfile, alg: string): (ref Sys->FD, string)
+{
+	cert : string;
+
+	kr := load Keyring Keyring->PATH;
+	if(kr == nil)
+		error(sys->sprint("cannot load %s: %r", Keyring->PATH));
+
+	kd := "/usr/" + user() + "/keyring/";
+	if (keyfile == nil) {
+		cert = kd + netmkaddr(addr, "tcp", "");
+		(ok, nil) := sys->stat(cert);
+		if (ok < 0)
+			cert = kd + "default";
+	}
+	else if (len keyfile > 0 && keyfile[0] != '/')
+		cert = kd + keyfile;
+	else
+		cert = keyfile;
+	ai := kr->readauthinfo(cert);
+	if (ai == nil)
+		error(sys->sprint("cannot read authentication data from %s: %r", cert));
+
+	au := load Auth Auth->PATH;
+	if(au == nil)
+		error(sys->sprint("cannot load %s: %r", Auth->PATH));
+	err := au->init();
+	if(err != nil)
+		error(sys->sprint("cannot init Auth: %s", err));
+
+	(ok, c) := sys->dial(netmkaddr(addr, "tcp", COLLABPORT), nil);
+	if(ok < 0)
+		error(sys->sprint("can't dial %s: %r", addr));
+	(fd, id_or_err) := au->client(alg, ai, c.dfd);
+	if(fd == nil)
+		error(sys->sprint("authentication failed: %s", id_or_err));
+
+	return (fd, id_or_err);
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n]; 
+}
+
+netmkaddr(addr, net, svc: string): string
+{
+	if(net == nil)
+		net = "net";
+	(n, l) := sys->tokenize(addr, "!");
+	if(n <= 1){
+		if(svc== nil)
+			return sys->sprint("%s!%s", net, addr);
+		return sys->sprint("%s!%s!%s", net, addr, svc);
+	}
+	if(svc == nil || n > 2)
+		return addr;
+	return sys->sprint("%s!%s", addr, svc);
+}
+
+error(m: string)
+{
+	sys->fprint(sys->fildes(2), "connect: %s\n", m);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/collab/lib/messages.b
@@ -1,0 +1,86 @@
+implement Messages;
+
+#
+# message queues and their users
+#
+
+include "messages.m";
+
+clientidgen := 1;
+
+init()
+{
+	clientidgen = 1;
+}
+
+Msglist.new(): ref Msglist
+{
+	msgs := ref Msglist;
+	msgs.tail = ref Msg;	# valid Msg when .next != nil
+	return msgs;
+}
+
+Msglist.queue(msgs: self ref Msglist): ref Msg
+{
+	return msgs.tail;
+}
+
+Msglist.wait(msgs: self ref Msglist, u: ref User, rd: ref Readreq)
+{
+	msgs.readers = (u, rd) :: msgs.readers;	# list reversed, but currently does not matter
+}
+
+Msglist.write(msgs: self ref Msglist, m: ref Msg): list of (ref User, ref Readreq)
+{
+	tail := msgs.tail;
+	tail.from = m.from;
+	tail.data = m.data;
+	tail.next = ref Msg(nil, nil, nil);
+	msgs.tail = tail.next;	# next message will be formed in tail.next
+	rl := msgs.readers;
+	msgs.readers = nil;
+	return rl;
+}
+
+Msglist.flushtag(msgs: self ref Msglist, tag: int)
+{
+	rl := msgs.readers;
+	msgs.readers = nil;
+	for(; rl != nil; rl = tl rl){
+		(nil, req) := hd rl;
+		if(req.tag != tag)
+			msgs.readers = hd rl :: msgs.readers;
+	}
+}
+
+Msglist.flushfid(msgs: self ref Msglist, fid: int)
+{
+	rl := msgs.readers;
+	msgs.readers = nil;
+	for(; rl != nil; rl = tl rl){
+		(nil, req) := hd rl;
+		if(req.fid != fid)
+			msgs.readers = hd rl :: msgs.readers;
+	}
+}
+
+User.new(fid: int, name: string): ref User
+{
+	return ref User(clientidgen++, fid, name, nil);
+}
+
+User.initqueue(u: self ref User, msgs: ref Msglist)
+{
+	u.queue = msgs.tail;
+}
+
+User.read(u: self ref User): ref Msg
+{
+	if((m := u.queue).next != nil){
+		u.queue = m.next;
+		m = ref *m;	# copy to ensure no aliasing
+		m.next = nil;
+		return m;
+	}
+	return nil;
+}
--- /dev/null
+++ b/appl/collab/lib/messages.m
@@ -1,0 +1,42 @@
+Messages:  module
+{
+	PATH:	con "/dis/collab/lib/messages.dis";
+
+	Msg: adt {
+		from:	cyclic ref User;
+		data:		array of byte;
+		next:		cyclic ref Msg;
+	};
+
+	Msglist: adt {
+		tail:		ref Msg;
+		readers:	list of (ref User, ref Readreq);
+
+		new:		fn(): ref Msglist;
+		flushfid:	fn(nil: self ref Msglist, fid: int);
+		flushtag:	fn(nil: self ref Msglist, tag: int);
+		wait:		fn(nil: self ref Msglist, u: ref User, r: ref Readreq);
+		write:	fn(nil: self ref Msglist, m: ref Msg): list of (ref User, ref Readreq);
+		queue:	fn(nil: self ref Msglist): ref Msg;
+	};
+
+	Readreq: adt {
+		tag:	int;
+		fid:	int;
+		count:	int;
+		offset:	big;
+	};
+
+	User: adt {
+		id:	int;
+		fid:	int;
+		name:	string;
+		queue:	cyclic ref Msg;
+
+		new:	fn(fid: int, name: string): ref User;
+		initqueue:	fn(nil: self ref User, msgs: ref Msglist);
+		read:	fn(nil: self ref User): ref Msg;
+	};
+
+	init:	fn();
+};
--- /dev/null
+++ b/appl/collab/mkfile
@@ -1,0 +1,62 @@
+<../../mkconfig
+
+SERVERS=\
+	servers/chatsrv.dis \
+	servers/memfssrv.dis \
+	servers/mpx.dis \
+	servers/wbsrv.dis \
+
+CLIENTS=\
+	clients/chat.dis \
+	clients/poll.dis \
+	clients/poller.dis \
+	clients/whiteboard.dis \
+
+LIB=\
+	lib/messages.dis \
+
+MAIN=\
+	collabsrv.dis \
+	connect.dis \
+	proxy.dis \
+	srvmgr.dis \
+
+MODULES=\
+	proxy.m\
+	service.m\
+	srvmgr.m\
+
+SYSMODULES=\
+	arg.m\
+	cfg.m\
+	draw.m\
+	keyring.m\
+	security.m\
+	sys.m\
+
+DEST=$ROOT/dis/collab
+
+ALL = $SERVERS $CLIENTS $LIB $MAIN
+
+all:V:	$ALL
+
+install:V: ${SERVERS:%=$DEST/%} \
+	${CLIENTS:%=$DEST/%} \
+	${LIB:%=$DEST/%} \
+	${MAIN:%=$DEST/%}
+
+$DEST/%.dis:	%.dis
+	cp $stem.dis $target
+
+%.dis:		$MODULES ${SYSMODULES:%=$ROOT/module/%}
+
+%.dis:	%.b
+	limbo -gw -I$ROOT/module -Ilib -I. -o $stem.dis $stem.b
+
+$ENGINES $MAIN $LIB: service.m srvmgr.m proxy.m lib/messages.m
+
+clean:NV:
+	rm -f *.dis *.sbl */*.dis */*.sbl
+
+nuke:NV: clean
+	cd $DEST && rm -f $ALL
--- /dev/null
+++ b/appl/collab/proxy.b
@@ -1,0 +1,177 @@
+implement Proxy;
+
+include "sys.m";
+	sys: Sys;
+
+include "srvmgr.m";
+include "proxy.m";
+
+Srvreq, Srvreply: import Srvmgr;
+
+init(root: string, fd: ref Sys->FD, rc: chan of ref Srvreq, user: string)
+{
+	sys = load Sys Sys->PATH;
+
+	sys->chdir(root);
+	sys->bind("export/services", "export/services", Sys->MCREATE);
+	sys->bind("#s", "export/services", Sys->MBEFORE);
+
+	ctlio := sys->file2chan("export/services", "ctl");
+
+	hangup := chan of int;
+	spawn export(fd, "export", hangup);
+	fd = nil;
+
+	for (;;) alt {
+	<- hangup =>
+		# closedown all clients
+		sys->print("client exit [%s]\n", user);
+		rmclients(rc);
+		return;
+	(offset, count, fid, r) := <- ctlio.read =>
+		client := fid2client(fid);
+		if (r == nil) {
+			if (client != nil)
+				rmclient(rc, client);
+			continue;
+		}
+		if (client == nil) {
+			rreply(r, (nil, "service not set"));
+			continue;
+		}
+		rreply(r, reads(client.path, offset, count));
+
+	(offset, data, fid, w) := <- ctlio.write =>
+		client := fid2client(fid);
+		if (w == nil) {
+			if (client != nil)
+				rmclient(rc, client);
+			continue;
+		}
+		if (client != nil) {
+			wreply(w, (0, "service set"));
+			continue;
+		}
+		err := newclient(rc, user, fid, string data);
+		if (err != nil)
+			wreply(w, (0, err));
+		else
+			wreply(w, (len data, nil));
+	}
+	
+}
+
+rreply(rc: chan of (array of byte, string), reply: (array of byte, string))
+{
+	alt {
+	rc <-= reply =>;
+	* =>;
+	}
+}
+
+wreply(wc: chan of (int, string), reply: (int, string))
+{
+	alt {
+	wc <-= reply=>;
+	* =>;
+	}
+}
+
+reads(str: string, off, nbytes: int): (array of byte, string)
+{
+	bstr := array of byte str;
+	slen := len bstr;
+	if(off < 0 || off >= slen)
+		return (nil, nil);
+	if(off + nbytes > slen)
+		nbytes = slen - off;
+	if(nbytes <= 0)
+		return (nil, nil);
+	return (bstr[off:off+nbytes], nil);
+}
+
+export(exportfd: ref Sys->FD, dir: string, done: chan of int)
+{
+	sys->export(exportfd, dir, Sys->EXPWAIT);
+	done <-= 1;
+}
+
+Client: adt {
+	fid: int;
+	path: string;
+	sname: string;
+	id: string;
+};
+
+clients: list of ref Client;
+freepaths: list of string;
+nextpath := 0;
+
+fid2client(fid: int): ref Client
+{
+	for(cl := clients; cl != nil; cl = tl cl)
+		if ((c := hd cl).fid == fid)
+			return c;
+	return nil;
+}
+
+newclient(rc: chan of ref Srvreq, user: string, fid: int, cmd: string): string
+{
+sys->print("new Client %s [%s]\n", user, cmd);
+	for (i := 0; i < len cmd; i++)
+		if (cmd[i] == ' ')
+			break;
+	if (i == 0 || i == len cmd)
+		return "bad command";
+
+	sname := cmd[:i];
+	id := cmd[i:];
+	reply := chan of Srvreply;
+	rc <-= ref Srvreq.Acquire(sname, id, user, reply);
+	(err, root, fd) := <- reply;
+	if (err != nil)
+		return err;
+
+	path := "";
+	if (freepaths != nil)
+		(path, freepaths) = (hd freepaths, tl freepaths);
+	else
+		path = string nextpath++;
+
+	sys->mount(fd, nil, "mnt", Sys->MREPL, nil);	# connection to the active service fs
+	mkdir("export/services/"+path);
+	sys->bind("mnt/"+root, "export/services/"+path, Sys->MREPL|Sys->MCREATE);
+	sys->unmount("mnt", nil);
+	clients = ref Client(fid, path, sname, id) :: clients;
+	return nil;
+}
+
+rmclient(rc: chan of ref Srvreq, client: ref Client)
+{
+sys->print("rmclient [%s %s]\n", client.sname, client.id);
+	nl: list of ref Client;
+	for(cl := clients; cl != nil; cl = tl cl)
+		if((c := hd cl) == client){
+			sys->unmount("export/services/" + client.path, nil);
+			freepaths = client.path :: freepaths;
+			rc <-= ref Srvreq.Release(client.sname, client.id);
+		} else
+			nl = c :: nl;
+	clients = nl;
+}
+
+rmclients(rc: chan of ref Srvreq)
+{
+	for(cl := clients; cl != nil; cl = tl cl){
+		c := hd cl;
+sys->print("rmclients [%s %s]\n", c.sname, c.id);
+		rc <-= ref Srvreq.Release(c.sname, c.id);
+	}
+	clients = nil;
+}
+
+mkdir(path: string)
+{
+	sys->print("mkdir [%s]\n", path);
+	sys->create(path, Sys->OREAD, 8r777 | Sys->DMDIR);
+}
--- /dev/null
+++ b/appl/collab/proxy.m
@@ -1,0 +1,5 @@
+Proxy: module
+{
+	PATH:	con "/dis/collab/proxy.dis";
+	init:	fn (root: string, fd: ref Sys->FD, rc: chan of ref Srvmgr->Srvreq, user: string);
+};
--- /dev/null
+++ b/appl/collab/runcollab
@@ -1,0 +1,8 @@
+#!/dis/sh
+
+load std
+and {~ $#* 0} {echo usage: runcollab collabname >[2=1]; exit usage}
+pctl forkns
+or {bind -bc /services/$1 /services/collab} {exit fail}
+or {bind /dis/collab/servers /services/collab/servers} {exit fail}
+collab/collabsrv /services/collab
--- /dev/null
+++ b/appl/collab/servers/chatsrv.b
@@ -1,0 +1,263 @@
+implement Service;
+
+#
+# simple text-based chat service
+#
+
+include "sys.m";
+	sys: Sys;
+	Qid: import Sys;
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import Styx;
+
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Navigator: import styxservers;
+	nametree: Nametree;
+	Tree: import nametree;
+
+include "../service.m";
+
+Qdir, Qusers, Qmsgs: con iota;
+
+tc: chan of ref Tmsg;
+srv: ref Styxserver;
+
+user := "inferno";
+
+dir(name: string, perm: int, path: int): Sys->Dir
+{
+	d := sys->zerodir;
+	d.name = name;
+	d.uid = user;
+	d.gid = user;
+	d.qid.path = big path;
+	if(perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	else
+		d.qid.qtype = Sys->QTFILE;
+	d.mode = perm;
+	return d;
+}
+
+init(nil: list of string): (string, string, ref Sys->FD)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		return (sys->sprint("can't load %s: %r", Styx->PATH), nil, nil);
+	styxservers = load Styxservers Styxservers->PATH;
+	if(styxservers == nil)
+		return (sys->sprint("can't load %s: %r", Styxservers->PATH), nil, nil);
+	nametree = load Nametree Nametree->PATH;
+	if(nametree == nil)
+		return (sys->sprint("can't load %s: %r", Nametree->PATH), nil, nil);
+	styx->init();
+	styxservers->init(styx);
+	nametree->init();
+
+	(tree, treeop) := nametree->start();
+	tree.create(big Qdir, dir(".", Sys->DMDIR|8r555, Qdir));
+	tree.create(big Qdir, dir("users", 8r444, Qusers));
+	tree.create(big Qdir, dir("msgs", 8r666, Qmsgs));
+	
+	p := array [2] of ref Sys->FD;
+	if (sys->pipe(p) < 0){
+		tree.quit();
+		return (sys->sprint("cannot create pipe: %r"), nil, nil);
+	}
+
+	nextmsg = ref Msg (0, nil, nil, nil);
+
+	(tc, srv) = Styxserver.new(p[1], Navigator.new(treeop), big Qdir);
+	spawn chatsrv(tree);
+
+	return (nil, "/", p[0]);
+}
+
+chatsrv(tree: ref Tree)
+{
+	while((tmsg := <-tc) != nil){
+		pick tm := tmsg {
+		Readerror =>
+			break;
+		Flush =>
+			cancelpending(tm.tag);
+			srv.reply(ref Rmsg.Flush(tm.tag));
+		Open =>
+			c := srv.open(tm);
+			if (c == nil)
+				break;
+			if (int c.path == Qmsgs){
+				newmsgclient(tm.fid, c.uname);
+				#root[0].qid.vers++;		# TO DO
+			}
+		Read =>
+			c := srv.getfid(tm.fid);
+			if (c == nil || !c.isopen) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid));
+				break;
+			}
+			case int c.path {
+			Qdir =>
+				srv.read(tm);
+			Qmsgs =>
+				mc := getmsgclient(tm.fid);
+				if (mc == nil) {
+					srv.reply(ref Rmsg.Error(tm.tag, "internal error -- lost client"));
+					continue;
+				}
+				tm.offset = big 0;
+				msg := getnextmsg(mc);
+				if (msg == nil) {
+					if(mc.pending != nil)
+						srv.reply(ref Rmsg.Error(tm.tag, "read already pending"));
+					else
+						mc.pending = tm;
+					continue;
+				}
+				srv.reply(styxservers->readstr(tm, msg));
+			Qusers =>
+				srv.reply(styxservers->readstr(tm, usernames()));
+			* =>
+				srv.reply(ref Rmsg.Error(tm.tag, "phase error -- bad path"));
+			}
+		Write =>
+			c := srv.getfid(tm.fid);
+			if (c == nil || !c.isopen) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid));
+				continue;
+			}
+			if (int c.path != Qmsgs) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Eperm));
+				continue;
+			}
+			writemsgclients(tm.fid, c.uname, string tm.data);
+			srv.reply(ref Rmsg.Write(tm.tag, len tm.data));
+		Clunk =>
+			c := srv.clunk(tm);
+			if (c != nil && int c.path == Qmsgs){
+				closemsgclient(tm.fid);
+				# root[0].qid.vers++;		# TO DO
+			}
+		* =>
+			srv.default(tmsg);
+		}
+	}
+	tree.quit();
+	sys->print("chatsrv exit\n");
+}
+
+Msg: adt {
+	fromfid: int;
+	from: string;
+	msg: string;
+	next: cyclic ref Msg;
+};
+
+Msgclient: adt {
+	fid: int;
+	name: string;
+	nextmsg: ref Msg;
+	pending: ref Tmsg.Read;
+	next: cyclic ref Msgclient;
+};
+
+nextmsg: ref Msg;
+msgclients: ref Msgclient;
+
+usernames(): string
+{
+	s := "";
+	for (c := msgclients; c != nil; c = c.next)
+		s += c.name+"\n";
+	return s;
+}
+
+newmsgclient(fid: int, name: string)
+{
+	writemsgclients(fid, nil, "+++ " + name + " has arrived");
+	msgclients = ref Msgclient(fid, name, nextmsg, nil, msgclients);
+}
+
+getmsgclient(fid: int): ref Msgclient
+{
+	for (c := msgclients; c != nil; c = c.next)
+		if (c.fid == fid)
+			return c;
+	return nil;
+}
+
+cancelpending(tag: int)
+{
+	for (c := msgclients; c != nil; c = c.next)
+		if((tm := c.pending) != nil && tm.tag == tag){
+			c.pending = nil;
+			break;
+		}
+}
+
+closemsgclient(fid: int)
+{
+	prev: ref Msgclient;
+	s := "";
+	for (c := msgclients; c != nil; c = c.next) {
+		if (c.fid == fid) {
+			if (prev == nil)
+				msgclients = c.next;
+			else 
+				prev.next = c.next;
+			s = "--- " + c.name + " has left";
+			break;
+		}
+		prev = c;
+	}
+	if (s != nil)
+		writemsgclients(fid, nil, s);
+}
+
+writemsgclients(fromfid: int, from: string, msg: string)
+{
+	nm := ref Msg(0, nil, nil, nil);
+	nextmsg.fromfid = fromfid;
+	nextmsg.from = from;
+	nextmsg.msg = msg;
+	nextmsg.next = nm;
+
+	for (c := msgclients; c != nil; c = c.next) {
+		if (c.pending != nil) {
+			s := msgtext(c, nextmsg);
+			srv.reply(styxservers->readstr(c.pending, s));
+			c.pending = nil;
+			c.nextmsg = nm;
+		}
+	}
+	nextmsg = nm;
+}
+
+getnextmsg(mc: ref Msgclient): string
+{
+# uncomment next two lines to eliminate queued messages to self
+#	while(mc.nextmsg.next != nil && mc.nextmsg.fromfid == mc.fid)
+#		mc.nextmsg = mc.nextmsg.next;
+	if ((m := mc.nextmsg).next != nil){
+		mc.nextmsg = m.next;
+		return msgtext(mc, m);
+	}
+	return nil;
+}
+
+msgtext(mc: ref Msgclient, m: ref Msg): string
+{
+	prefix := "";
+	if (m.from != nil) {
+		# not a system message
+		if (mc.fid == m.fromfid)
+			prefix = "<you>: ";
+		else
+			prefix = m.from + ": ";
+	}
+	return prefix + m.msg;
+}
--- /dev/null
+++ b/appl/collab/servers/memfssrv.b
@@ -1,0 +1,20 @@
+implement Service;
+
+include "sys.m";
+include "../service.m";
+include "memfs.m";
+
+init(nil : list of string) : (string, string, ref Sys->FD)
+{
+	sys := load Sys Sys->PATH;
+	memfs := load MemFS MemFS->PATH;
+	if (memfs == nil) {
+		err := sys->sprint("cannot load %s: %r", MemFS->PATH);
+		return (err, nil, nil);
+	}
+	err := memfs->init();
+	if (err != nil)
+		return (err, nil, nil);
+	fd := memfs->newfs(1024 * 512);
+	return (nil, "/", fd);
+}
--- /dev/null
+++ b/appl/collab/servers/mpx.b
@@ -1,0 +1,301 @@
+implement Service;
+
+#
+# 1 to many and many to 1 multiplexor
+#
+
+include "sys.m";
+	sys: Sys;
+	Qid: import Sys;
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Navigator: import styxservers;
+	nametree: Nametree;
+	Tree: import nametree;
+
+include "service.m";
+
+include "messages.m";
+	messages: Messages;
+	Msg, Msglist, Readreq, User: import messages;
+
+Qdir, Qroot, Qusers, Qleaf: con iota;
+
+srv: ref Styxserver;
+clientidgen := 0;
+
+Einactive: con "not currently active";
+
+toleaf: ref Msglist;
+toroot: ref Msglist;
+userlist: list of ref User;
+
+user := "inferno";
+
+dir(name: string, perm: int, path: int): Sys->Dir
+{
+	d := sys->zerodir;
+	d.name = name;
+	d.uid = user;
+	d.gid = user;
+	d.qid.path = big path;
+	if(perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	else
+		d.qid.qtype = Sys->QTFILE;
+	d.mode = perm;
+	return d;
+}
+
+init(nil: list of string): (string, string, ref Sys->FD)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		return (sys->sprint("can't load %s: %r", Styx->PATH), nil, nil);
+	styxservers = load Styxservers Styxservers->PATH;
+	if(styxservers == nil)
+		return (sys->sprint("can't load %s: %r", Styxservers->PATH), nil, nil);
+	nametree = load Nametree Nametree->PATH;
+	if(nametree == nil)
+		return (sys->sprint("can't load %s: %r", Nametree->PATH), nil, nil);
+	styx->init();
+	styxservers->init(styx);
+styxservers->traceset(1);
+	nametree->init();
+	messages = load Messages Messages->PATH;
+	if(messages == nil)
+		return (sys->sprint("can't load %s: %r", Messages->PATH), nil, nil);
+
+	(tree, treeop) := nametree->start();
+	tree.create(big Qdir, dir(".", Sys->DMDIR|8r555, Qdir));
+	tree.create(big Qdir, dir("leaf", 8r666, Qleaf));
+	tree.create(big Qdir, dir("root", 8r666, Qroot));
+	tree.create(big Qdir, dir("users", 8r444, Qusers));
+	
+	p := array [2] of ref Sys->FD;
+	if (sys->pipe(p) < 0){
+		tree.quit();
+		return (sys->sprint("can't create pipe: %r"), nil, nil);
+	}
+
+	toleaf = Msglist.new();
+	toroot = Msglist.new();
+
+	tc: chan of ref Tmsg;
+	(tc, srv) = Styxserver.new(p[1], Navigator.new(treeop), big Qdir);
+	spawn mpx(tc, tree);
+
+	return (nil, "/", p[0]);
+}
+
+mpx(tc: chan of ref Tmsg, tree: ref Tree)
+{
+	root: ref User;
+	while((tmsg := <-tc) != nil){
+		pick tm := tmsg {
+		Readerror =>
+			break;
+		Open =>
+			c := srv.getfid(tm.fid);
+			if(c == nil || c.isopen){
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid));
+				continue;
+			}
+			case int c.path {
+			Qroot =>
+				if(root != nil){
+					srv.reply(ref Rmsg.Error(tm.tag, sys->sprint("interaction already directed by %s", root.name)));
+					continue;
+				}
+				c = srv.open(tm);
+				if (c == nil)
+					continue;
+				root = ref User(0, tm.fid, c.uname, nil);
+				root.initqueue(toroot);
+			Qleaf =>
+				if(root == nil){
+					srv.reply(ref Rmsg.Error(tm.tag, Einactive));
+					continue;
+				}
+				c = srv.open(tm);
+				if (c == nil)
+					continue;
+				userarrives(tm.fid, c.uname);
+				# mpxdir[1].qid.vers++;	# TO DO
+			* =>
+				srv.open(tm);
+			}
+		Read =>
+			c := srv.getfid(tm.fid);
+			if (c == nil || !c.isopen) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid));
+				continue;
+			}
+			case int c.path {
+			Qdir =>
+				srv.read(tm);
+			Qroot =>
+				tm.offset = big 0;
+				m := qread(toroot, root, tm, 1);
+				if(m != nil)
+					srv.reply(ref Rmsg.Read(tm.tag, m.data));
+			Qleaf =>
+				u := fid2user(tm.fid);
+				if (u == nil) {
+					srv.reply(ref Rmsg.Error(tm.tag, "internal error -- lost user"));
+					continue;
+				}
+				tm.offset = big 0;
+				m := qread(toleaf, u, tm, 0);
+				if(m == nil){
+					if(root == nil)
+						srv.reply(ref Rmsg.Read(tm.tag, nil));
+					else
+						qread(toleaf, u, tm, 1);	# put us on the wait queue
+				}else
+					srv.reply(ref Rmsg.Read(tm.tag, m.data));
+			Qusers =>
+				srv.reply(styxservers->readstr(tm, usernames()));
+			* =>
+				srv.reply(ref Rmsg.Error(tm.tag, "phase error -- bad path"));
+			}
+		Write =>
+			c := srv.getfid(tm.fid);
+			if (c == nil || !c.isopen) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid));
+				continue;
+			}
+			case int c.path {
+			Qroot =>
+				qwrite(toleaf, msg(root, 'M', tm.data));
+				srv.reply(ref Rmsg.Write(tm.tag, len tm.data));
+			Qleaf =>
+				u := fid2user(tm.fid);
+				if(u == nil) {
+					srv.reply(ref Rmsg.Error(tm.tag, "internal error -- lost user"));
+					continue;
+				}
+				if(root == nil){
+					srv.reply(ref Rmsg.Error(tm.tag, Einactive));
+					continue;
+				}
+				qwrite(toroot, msg(u, 'm', tm.data));
+				srv.reply(ref Rmsg.Write(tm.tag, len tm.data));
+			* =>
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Eperm));
+			}
+		Flush =>
+			cancelpending(tm.tag);
+			srv.reply(ref Rmsg.Flush(tm.tag));
+		Clunk =>
+			c := srv.getfid(tm.fid);
+			if(c.isopen){
+				case int c.path {
+				Qroot =>
+					# shut down?
+					qwrite(toleaf, msg(root, 'L', nil));
+					root = nil;
+				Qleaf =>
+					userleaves(tm.fid);
+					# mpxdir[1].qid.vers++;	# TO DO
+				}
+			}
+		* =>
+			srv.default(tmsg);
+		}
+	}
+	tree.quit();
+	sys->print("mpx exit\n");
+}
+
+mpxseqgen := 0;
+
+time(): int
+{
+	return ++mpxseqgen;	# server time; assumes 2^31-1 is large enough
+}
+
+userarrives(fid: int, name: string)
+{
+	u := User.new(fid, name);
+	qwrite(toroot, msg(u, 'a', nil));
+	u.initqueue(toleaf);	# sees leaf messages from now on
+	userlist = u :: userlist;
+}
+
+fid2user(fid: int): ref User
+{
+	for(ul := userlist; ul != nil; ul = tl ul)
+		if((u := hd ul).fid == fid)
+			return u;
+	return nil;
+}
+
+userleaves(fid: int)
+{
+	ul := userlist;
+	userlist = nil;
+	u: ref User;
+	for(; ul != nil; ul = tl ul)
+		if((hd ul).fid != fid)
+			userlist = hd ul :: userlist;
+		else
+			u = hd ul;
+	if(u != nil)
+		qwrite(toroot, msg(u, 'l', nil));
+}
+
+usernames(): string
+{
+	s := "";
+	for(ul := userlist; ul != nil; ul = tl ul){
+		u := hd ul;
+		s += string u.id+" "+u.name+"\n";
+	}
+	return s;
+}
+
+qwrite(msgs: ref Msglist, m: ref Msg)
+{
+	pending := msgs.write(m);
+	for(; pending != nil; pending = tl pending){
+		(u, req) := hd pending;
+		m = u.read();	# must succeed, or the code is wrong
+		data := m.data;
+		if(req.count < len data)
+			data = data[0:req.count];
+		srv.reply(ref Rmsg.Read(req.tag, data));
+	}
+}
+
+qread(msgs: ref Msglist, u: ref User, tm: ref Tmsg.Read, wait: int): ref Msg
+{
+	m := u.read();
+	if(m != nil){
+		if(tm.count < len m.data)
+			m.data = m.data[0:tm.count];
+	}else if(wait)
+		msgs.wait(u, ref Readreq(tm.tag, tm.fid, tm.count, tm.offset));
+	return m;
+}
+
+cancelpending(tag: int)
+{
+	toroot.flushtag(tag);
+	toleaf.flushtag(tag);
+}
+
+msg(u: ref User, op: int, data: array of byte): ref Msg
+{
+	a := sys->aprint("%ud %d %c %s ", time(), u.id, op, u.name);
+	m := ref Msg(u, array[len a + len data] of byte, nil);
+	m.data[0:] = a;
+	m.data[len a:] = data;
+	return m;
+}
--- /dev/null
+++ b/appl/collab/servers/wbsrv.b
@@ -1,0 +1,226 @@
+implement Service;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Chans, Display, Image, Rect, Point : import draw;
+
+include "../service.m";
+
+WBW : con 234;
+WBH : con 279;
+
+init(nil : list of string) : (string, string, ref Sys->FD)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		return ("cannot load Draw module", nil, nil);
+
+	p := array [2] of ref Sys->FD;
+	if (sys->pipe(p) == -1)
+		return (sys->sprint("cannot create pipe: %r"), nil, nil);
+
+	display := Display.allocate(nil);
+	if (display == nil)
+		return (sys->sprint("cannot allocate display: %r"), nil, nil);
+
+	r := Rect(Point(0,0), Point(WBW, WBH));
+	wb := display.newimage(r, Draw->CMAP8, 0, Draw->White);
+	if (wb == nil)
+		return (sys->sprint("cannot allocate whiteboard image: %r"), nil, nil);
+
+	nextmsg = ref Msg (nil, nil);
+	spawn wbsrv(p[1], wb);
+	return (nil, "/chan", p[0]);
+}
+
+wbsrv(fd : ref Sys->FD, wb: ref Image)
+{
+	sys->pctl(Sys->FORKNS, nil);
+	sys->unmount(nil, "/chan");
+	sys->bind("#s", "/chan", Sys->MREPL);
+
+	bit := sys->file2chan("/chan", "wb.bit");
+	strokes := sys->file2chan("/chan", "strokes");
+	
+	hangup := chan of int;
+	spawn export(fd, hangup);
+
+	nwbbytes := draw->bytesperline(wb.r, wb.depth) * wb.r.dy();
+	bithdr := sys->aprint("%11s %11d %11d %11d %11d ", wb.chans.text(), 0, 0, WBW, WBH);
+
+	for (;;) alt {
+	<-hangup =>
+		sys->print("whiteboard:hangup\n");
+		return;
+		
+	(offset, count, fid, r) := <-bit.read =>
+		if (r == nil) {
+			closeclient(fid);
+			continue;
+		}
+		c := getclient(fid);
+		if (c == nil) {
+			# new client
+			c = newclient(fid);
+			data := array [len bithdr + nwbbytes] of byte;
+			data[0:] = bithdr;
+			wb.readpixels(wb.r, data[len bithdr:]);
+			c.bitdata = data;
+		}
+		if (offset >= len c.bitdata) {
+			rreply(r, (nil, nil));
+			continue;
+		}
+		rreply(r, (c.bitdata[offset:], nil));
+
+	(offset, data, fid, w) := <-bit.write =>
+		if (w != nil)
+			wreply(w, (0, "permission denied"));
+
+	(offset, count, fid, r) := <-strokes.read =>
+		if (r == nil) {
+			closeclient(fid);
+			continue;
+		}
+		c := getclient(fid);
+		if (c == nil) {
+			c = newclient(fid);
+			c.nextmsg = nextmsg;
+		}
+		d := c.nextmsg.data;
+		if (d == nil) {
+			c.pending = r;
+			c.pendlen = count;
+			continue;
+		}
+		c.nextmsg = c.nextmsg.next;
+		rreply(r, (d, nil));
+
+	(offset, data, fid, w) := <-strokes.write =>
+		if (w == nil) {
+			closeclient(fid);
+			continue;
+		}
+		err := drawstrokes(wb, data);
+		if (err != nil) {
+			wreply(w, (0, err));
+			continue;
+		}
+		wreply(w, (len data, nil));
+		writeclients(data);
+	}
+}
+
+rreply(rc: chan of (array of byte, string), reply: (array of byte, string))
+{
+	alt {
+	rc <-= reply =>;
+	* =>;
+	}
+}
+
+wreply(wc: chan of (int, string), reply: (int, string))
+{
+	alt {
+	wc <-= reply=>;
+	* =>;
+	}
+}
+
+export(fd : ref Sys->FD, done : chan of int)
+{
+	sys->export(fd, "/", Sys->EXPWAIT);
+	done <-= 1;
+}
+
+Msg : adt {
+	data : array of byte;
+	next : cyclic ref Msg;
+};
+
+Client : adt {
+	fid : int;
+	bitdata : array of byte;		# bit file client
+	nextmsg : ref Msg;			# strokes file client
+	pending : Sys->Rread;
+	pendlen : int;
+};
+
+nextmsg : ref Msg;
+clients : list of ref Client;
+
+newclient(fid : int) : ref Client
+{
+	c := ref Client(fid, nil, nil, nil, 0);
+	clients = c :: clients;
+	return c;
+}
+
+getclient(fid : int) : ref Client
+{
+	for(cl := clients; cl != nil; cl = tl cl)
+		if((c := hd cl).fid == fid)
+			return c;
+	return nil;
+}
+
+closeclient(fid : int)
+{
+	nl: list of ref Client;
+	for(cl := clients; cl != nil; cl = tl cl)
+		if((hd cl).fid != fid)
+			nl = hd cl :: nl;
+	clients = nl;
+}
+
+writeclients(data : array of byte)
+{
+	nm := ref Msg(nil, nil);
+	nextmsg.data = data;
+	nextmsg.next = nm;
+
+	for(cl := clients; cl != nil; cl = tl cl){
+		if ((c := hd cl).pending != nil) {
+			n := c.pendlen;
+			if (n > len data)
+				n = len data;
+			alt{
+			c.pending <-= (data[0:n], nil) => ;
+			* => ;
+			}
+			c.pending = nil;
+			c.nextmsg = nm;
+		}
+	}
+	nextmsg = nm;
+}
+
+# data: colour width p0 p1 pn*
+
+drawstrokes(wb: ref Image, data : array of byte) : string
+{
+	(n, toks) := sys->tokenize(string data, " ");
+	if (n < 6 || n & 1)
+		return "bad data";
+
+	colour, width, x, y : int;
+	(colour, toks) = (int hd toks, tl toks);
+	(width, toks) = (int hd toks, tl toks);
+	(x, toks) = (int hd toks, tl toks);
+	(y, toks) = (int hd toks, tl toks);
+	pen := wb.display.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, colour);
+	p0 := Point(x, y);
+	while (toks != nil) {
+		(x, toks) = (int hd toks, tl toks);
+		(y, toks) = (int hd toks, tl toks);
+		p1 := Point(x, y);
+		# could use poly() instead of line()
+		wb.line(p0, p1, Draw->Endsquare, Draw->Endsquare, width, pen, pen.r.min);
+		p0 = p1;
+	}
+	return nil;
+}
--- /dev/null
+++ b/appl/collab/service.m
@@ -1,0 +1,4 @@
+Service: module
+{
+	init: fn (args: list of string): (string, string, ref Sys->FD);
+};
--- /dev/null
+++ b/appl/collab/srvmgr.b
@@ -1,0 +1,190 @@
+implement Srvmgr;
+
+include "sys.m";
+	sys: Sys;
+
+include "srvmgr.m";
+include "service.m";
+include "cfg.m";
+
+Srvinfo: adt {
+	name: string;
+	path: string;
+	args: list of string;
+};
+
+services: list of ref Srvinfo;
+
+init(srvdir: string): (string, chan of ref Srvreq)
+{
+	sys = load Sys Sys->PATH;
+	cfg := load Cfg Cfg->PATH;
+	cfgpath := srvdir + "/services.cfg";
+	if (cfg == nil)
+		return (sys->sprint("cannot load %s: %r", Cfg->PATH), nil);
+	err := cfg->init(cfgpath);
+	if (err != nil)
+		return (err, nil);
+
+	(err, services) = parsecfg(cfgpath, srvdir, cfg);
+	if (err != nil)
+		return (err, nil);
+
+	rc := chan of ref Srvreq;
+	spawn srv(rc);
+	return (nil, rc);
+}
+
+parsecfg(p, srvdir: string, cfg: Cfg): (string, list of ref Srvinfo)
+{
+	srvlist: list of ref Srvinfo;
+	Record, Tuple: import cfg;
+
+	for (slist := cfg->getkeys(); slist != nil; slist = tl slist) {
+		name := hd slist;
+		matches := cfg->lookup(name);
+		if (len matches > 1) {
+			(nil, duplicate) := hd tl matches;
+			primary := hd duplicate.tuples;
+			lnum := primary.lnum;
+			err := sys->sprint("%s:%d: duplicate service name %s", p, lnum, name);
+			return (err, nil);
+		}
+		(nil, r) := hd matches;
+		lnum := (hd r.tuples).lnum;
+
+		(path, tuple) := r.lookup("path");
+		if (path == nil) {
+			err := sys->sprint("%s:%d: missing path for service %s", p, lnum, name);
+			return (err, nil);
+		}
+		if (path[0] != '/')
+			path = srvdir + "/" + path;
+
+		args: list of string = nil;
+		for (tuples := tl r.tuples; tuples != nil; tuples = tl tuples) {
+			t := hd tuples;
+			arg := t.lookup("arg");
+			if (arg != nil)
+				args = arg :: args;
+		}
+		nargs: list of string = nil;
+		for (; args != nil; args = tl args)
+			nargs = hd args :: nargs;
+		srvlist = ref Srvinfo(name, path, args) ::srvlist;
+	}
+	if (srvlist == nil) {
+		err := sys->sprint("%s: no services", p);
+		return (err, nil);
+	}
+	return (nil, srvlist);
+}
+	
+srv(rc: chan of ref Srvreq)
+{
+	for (;;) {
+		req := <- rc;
+		id := req.sname + " " + req.id;
+		pick r := req {
+		Acquire =>
+			# r.user not used, but could control access
+			service := acquire(id);
+			err := "";
+			if (service.fd == nil) {
+				(err, service.root, service.fd) = startservice(req.sname);
+				if (err != nil)
+					release(id);
+			}
+			r.reply <-= (err, service.root, service.fd);
+		Release =>
+			release(id);
+		}
+	}
+}
+
+#
+# returns (error, service root, service FD)
+#
+startservice(name: string): (string, string, ref Sys->FD)
+{
+sys->print("startservice [%s]\n", name);
+	srv: ref Srvinfo;
+	for (sl := services; sl != nil; sl = tl sl) {
+		s := hd sl;
+		if (s.name == name) {
+			srv = s;
+			break;
+		}
+	}
+	if (srv == nil)
+		return ("unknown service", nil, nil);
+
+	service := load Service srv.path;
+	if (service == nil) {
+		err := sys->sprint("cannot load %s: %r", srv.path);
+		return (err, nil, nil);
+	}
+
+	return service->init(srv.args);
+}
+
+Srvmap: adt {
+	id: string;
+	root: string;
+	fd: ref Sys->FD;
+	nref: int;
+	next: cyclic ref Srvmap;
+};
+
+PRIME: con 211;
+buckets := array[PRIME] of ref Srvmap;
+
+hash(id: string): int
+{
+	# HashPJW
+	h := 0;
+	for (i := 0; i < len id; i++) {
+		h = (h << 4) + id[i];
+		g := h & int 16rf0000000;
+		if (g != 0) {
+			h = h ^ ((g >> 24) & 16rff);
+			h = h ^ g;
+		}
+	}
+	if (h < 0)
+		h &= ~(1<<31);
+	return int (h % PRIME);
+}
+
+acquire(id: string): ref Srvmap
+{
+	h := hash(id);
+	for (p := buckets[h]; p != nil; p = p.next)
+		if (p.id == id) {
+			p.nref++;
+			return p;
+		}
+	p = ref Srvmap(id, nil, nil, 1, buckets[h]);
+	buckets[h] = p;
+	return p;
+}
+
+release(id: string)
+{
+	h :=hash(id);
+	prev: ref Srvmap;
+	for (p := buckets[h]; p != nil; p = p.next) {
+		if (p.id == id){
+			p.nref--;
+			if (p.nref == 0) {
+				sys->print("release [%s]\n", p.id);
+				if (prev == nil)
+					buckets[h] = p.next;
+				else
+					prev.next = p.next;
+			}
+			return;
+		}
+		prev = p;
+	}
+}
--- /dev/null
+++ b/appl/collab/srvmgr.m
@@ -1,0 +1,22 @@
+Srvmgr: module
+{
+	PATH:	con "/dis/collab/srvmgr.dis";
+	Srvreq: adt {
+		sname: string;
+		id: string;
+		pick {
+		Acquire =>
+			uname: string;
+			reply: chan of Srvreply;
+		Release =>
+		}
+	};
+
+	Srvreply: type (
+		string,		# error
+		string,		# root path
+		ref Sys->FD	# styx fd
+	);
+
+	init: fn(cfg: string): (string, chan of ref Srvreq);
+};
--- /dev/null
+++ b/appl/demo/camera/camera.b
@@ -1,0 +1,2557 @@
+implement Camera;
+
+include "sys.m";
+	sys : Sys;
+include "daytime.m";
+	daytime: Daytime;
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Fid, Navigator, Navop: import styxservers;
+	Styxserver, Eexists, Eperm, Ebadfid, Enotdir, Enotfound, Ebadarg: import styxservers;
+	nametree: Nametree;
+	Tree: import nametree;
+include "string.m";
+	str : String;
+include "draw.m";
+include "arg.m";
+
+Camera : module {
+	init : fn (nil : ref Draw->Context, argv : list of string);
+};
+
+cdp_get_product_info: 			con 16r01;
+cdp_get_image_specifications:	con 16r02;
+cdp_get_camera_status:			con 16r03;
+cdp_set_product_info:			con 16r05;
+cdp_get_camera_capabilities:		con 16r10;
+cdp_get_camera_state:			con 16r11;
+cdp_set_camera_state:			con 16r12;
+cdp_get_camera_defaults:		con 16r13;
+cdp_set_camera_defaults:		con 16r14;
+cdp_restore_camera_states:		con 16r15;
+cdp_get_scene_analysis:			con 16r18;
+cdp_get_power_mode:			con 16r19;
+cdp_set_power_mode:			con 16r1a;
+cdp_get_s1_mode:				con 16r1d;
+cdp_set_s1_mode:				con 16r1e;
+cdp_start_capture:				con 16r30;
+cdp_get_file_list:				con 16r40;
+cdp_get_new_file_list:			con 16r41;
+cdp_get_file_data:				con 16r42;
+cdp_erase_file:					con 16r43;
+cdp_get_storage_status:			con 16r44;
+cdp_set_file_data:				con 16r47;
+cdp_get_file_tag:				con 16r48;
+cdp_set_user_file_tag:			con 16r49;
+cdp_get_clock:					con 16r70;
+cdp_set_clock:					con 16r71;
+cdp_get_error:					con 16r78;
+cdp_get_interface_timeout:		con 16r90;
+cdp_set_interface_timeout:		con 16r91;
+
+cdp_header_len:				con 12;
+
+T_DIR: con 0;
+T_CTL: con 1;
+T_ABILITIES: con 2;
+T_TIME: con 3;
+T_JPGDIR: con 4;
+T_JPG: con 5;
+T_STORAGE: con 6;
+T_POWER: con 7;
+T_THUMB: con 8;
+T_THUMBDIR: con 9;
+T_STATE: con 10;
+T_INTERFACE: con 11;
+
+MAXFILESIZE : con 5000000;
+TIMEOUT : con 4000;
+
+nextjpgqid, nexttmbqid, dirqid, Qctl, Qabl, Qstore: int;
+Qstate, Qtime, Qjpgdir, Qpwr, Qthumbdir, Qinterface : int;
+
+error_table := array [] of {
+	"No Error",
+	"Unimplemented",
+	"Unsupported Version",
+	"Application Timeout",
+	"Internal Error",
+	"Parameter Error",
+	"File System Null",
+	"File Not Found",
+	"Data Section Not Found",
+	"Invalid File Type",
+	"Unknown Drive",
+	"Drive Not Mounted",
+	"System Busy",
+	"Battery Low",
+};
+
+bintro := array [] of {
+	byte 16ra5,
+	byte 16r5a,
+	byte 16r00,
+	byte 16rc8,
+	byte 16r00,
+	byte 16r02,
+	byte 16rc9,
+};
+
+bak := array [] of {
+	byte 16r5a,	# 2 byte header
+	byte 16ra5,
+	byte 16r55,	# I/F Type
+	byte 16r00,	# Comm Flag
+	byte 16r00,
+	byte 16r00,
+	byte 16r00,
+	byte 16r00,
+	byte 16r00,
+	byte 16r00,
+	byte 16r00,
+	byte 16r00,
+	byte 16r00,
+};
+
+pwl := array[] of {
+	byte 0,
+	byte 0,
+};
+
+pak := array [] of {
+	byte 0,
+	byte 0,
+};
+
+SERIAL, USB, IRDA: con (1<<iota);
+BEACON, BEACONRESULT: con (1<<iota);
+
+Camera_adt: adt {
+	port_type: 	int;
+	port_num:	int;
+	command:	int;
+	mode: 		int;
+	fd:			ref Sys->FD;
+	ctlfd:			ref Sys->FD;
+	cdp:			array of byte;
+	bufbytes:		int;
+	baud:		int;
+	dfs, hfs:		int;		# device and host frame sizes
+	stat:			int;	# eia status file
+};
+
+statopt := array[] of {
+	"status",
+	"stat",
+};
+
+DL_QUANTA: con 20000;
+
+TOUT: con -1729;
+
+Partialtag: adt {
+	offset, length, filesize: int;
+};
+
+Cfile: adt {
+	driveno: int;
+	pathname: array of byte;
+	dosname: array of byte;
+	filelength: int;
+	filestatus: int;
+	thumblength: int;
+	thumbqid: int;
+};
+
+Fitem : adt {
+	qid: Sys->Qid;
+	cf: Cfile;
+};
+
+C: Camera_adt;
+
+filelist: array of Fitem;
+reslength: int;
+currentstate := "";
+wait : int;
+usecache := 0;
+connected : int;
+recon := 0;
+verbosity := 4;
+interfacepath := "";
+interfacepaths : array of string;
+camname := "";
+gpid : int;
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	err: string;
+	sys = load Sys Sys->PATH;
+	gpid = sys->pctl(Sys->NEWPGRP, nil);
+
+	str = load String String->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	styx = load Styx Styx->PATH;
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	styxservers->init(styx);
+	nametree = load Nametree Nametree->PATH;
+	nametree->init();
+	arg := load Arg Arg->PATH;
+
+	filelist = array[200] of Fitem;
+	C.port_num = 0;			# XXXXX from argv
+	C.port_type = SERIAL;		# Serial only for now
+	C.baud = 115200;
+	C.dfs = C.hfs = 1023;
+	C.cdp = array [DL_QUANTA] of byte;
+	C.mode = BEACON;
+
+	ex.pnum = -1;
+	ex.offset = -1;
+	cachelist = nil;
+
+	nextjpgqid = getqid(1, T_JPG);
+	nexttmbqid = getqid(1, T_THUMB);
+	dirqid = getqid(1,T_JPGDIR);
+	Qctl = getqid(Qroot,T_CTL);
+	Qabl = getqid(Qroot,T_ABILITIES);
+	Qstore = getqid(Qroot,T_STORAGE);
+	Qtime = getqid(Qroot,T_TIME);
+	Qstate = getqid(Qroot,T_STATE);
+	Qpwr = getqid(Qroot,T_POWER);	
+	Qjpgdir = getqid(Qroot,T_JPGDIR);
+	Qthumbdir = getqid(Qroot,T_THUMBDIR);
+	Qinterface = getqid(Qroot,T_INTERFACE);
+	
+	camname = "Camera";
+	extrafilelist: list of string = nil;
+	arg->init(argv);
+	arg->setusage("camera [-b baud] [-F framesize] [-f extrafiles] [-p port] [-n name] [-v verbosity]");
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'n' =>
+			camname = arg->earg();
+		'v' =>
+			verbosity = int arg->earg();
+		'F' =>
+			C.dfs = C.hfs = int arg->earg();
+		'b' =>
+			C.baud = int arg->earg();
+		'p' =>
+			C.port_num = int arg->earg();
+		'c' =>
+			usecache = 1;
+		'f' =>
+			extrafilelist = arg->earg() :: extrafilelist;
+		* =>
+			arg->usage();
+		}
+	}
+	arg = nil;
+	interfacepaths = array[len extrafilelist] of string;
+	# sys->print("INTERFACEPATHS: %d\n", len extrafilelist);
+	for (i := 0; i < len interfacepaths; i++) {
+		interfacepaths[i] = hd extrafilelist;
+		# sys->print("INTERFACEPATH %d: %s\n", i, hd extrafilelist);
+		extrafilelist = tl extrafilelist;
+	}
+	
+	print(sys->sprint("Trying to connect to eia%d...\n",C.port_num),2);
+	case C.port_type {
+		SERIAL =>
+			# open port and return fd
+			(C.fd, C.ctlfd, err) = serialport(C.port_num);
+			if (C.fd == nil) {
+				print("Could not open serial port\n",1);
+				exit;
+			}
+		USB =>
+			;
+		IRDA =>
+			;
+		* =>
+			;
+	}
+	if (connect() != 0) {;
+		print("Connection failed\n",1);
+		exit;
+	}
+	recon = 0;
+	print("Connected!\n",2);
+	set_interface_timeout();
+	set_camera_properties();
+	get_file_list();
+	connected = 1;
+	ignoreabls = nil;
+	get_camera_capabilities();
+	sync := chan of int;
+	spawn serveloop(sys->fildes(0), sync);
+	<-sync;
+}
+
+set_camera_properties()
+{
+	for (i := 0; i < len set_camera_props; i++)
+		set_camera_state(set_camera_props[i].t0,set_camera_props[i].t1);
+}
+
+set_camera_props := array[] of {
+	("mcap", 0),
+	("acpd", 65535),
+	("actc", 65535),
+	("btpd", 65535),
+	("bttc", 65535),
+	("flty", 1246774599),
+	("ssvl", 0),
+};
+
+argval(argv: list of string, arg: string): string
+{
+	if (arg == "") return "";
+	if (arg[0] != '-') arg = "-" + arg;
+	while (argv != nil) {
+		if (hd argv == arg && tl argv != nil && (hd tl argv)[0] != '-')
+			return tonext(tl argv);
+		argv = tl argv;
+	}
+	return "";
+}
+
+tonext(los: list of string): string
+{
+	s := "";
+	while (los != nil) {
+		if ((hd los)[0] != '-') s += " " + hd los;
+		else break;
+		los = tl los;
+	}
+	if (s != "") s = s[1:];
+	return s;
+}
+
+int2hex(i:int): int
+{
+	i2 := 0;
+	s := string i;
+	for (k := 0; k < len s; k++)
+		i2 = (i2 * 16) + int s[k:k+1];
+	return i2;
+}
+
+connect(): int
+{	
+	connected = 0;
+	datain := chan of array of byte;
+	pchan := chan of int;
+	tick := chan of int;
+	reset(C.ctlfd);
+
+	spawn timer2(tick,TIMEOUT * 2);
+	tpid := <-tick;
+
+	spawn beacon_intro(datain, pchan, C.fd);
+	pid := <- pchan;
+	# beacon phase
+	Beacon: for (;;) {
+		alt {
+			buf := <- datain =>
+				# got some data
+				case C.mode {
+					BEACON =>
+						if (beacon_ok(buf)) {
+							print("Got beacon\n",3);
+							beacon_ack(C);
+							spawn beacon_result(datain, pchan, C.fd);
+							pid = <-pchan;
+							C.mode = BEACONRESULT;
+							break;
+						}
+						else {
+							print("resetting\n",3);
+							reset(C.ctlfd);
+						}
+					BEACONRESULT =>
+						kill(tpid);
+
+						print("Checking beacon result\n",3);
+						if (beacon_comp(buf, C) == 0) {
+							return 0;
+							break Beacon;
+						}
+						return -1;
+				}
+			<- tick =>
+				kill(pid);
+				return -1;		# failure
+		}
+	}
+}
+
+CTL, ABILITIES, DATA, JPG, PIC, TIME, CONV: con iota;
+NAME, FSIZE, PHOTO, THUMB: con iota;
+
+Qdir : con iota;
+
+contains(s: string, test: string): int
+{
+	num :=0;
+	if (len test > len s) return 0;
+	for (i := 0; i < (1 + (len s) - (len test)); i++) {
+		if (test == s[i:i+len test]) num++;
+	}
+	return num;
+}
+
+abilitiesfilter := array[] of {
+	"Time Format",
+	"Date Format",
+	"File Type",
+	"Video",
+	"Media",
+	"Sound",
+	"Volume",
+	"Reset Camera",
+	"Slide",
+	"Timelapse",
+	"Burst",
+	"Power",
+	"Sleep",
+};
+
+ignoreabls : list of string;
+
+defattr : list of (string, int);
+defaultattr, currentattr: array of (string, int);
+
+filterabls(pname, desc: string): int
+{
+	for (i := 0; i < len abilitiesfilter; i++) {
+		if (contains(desc, abilitiesfilter[i])) {
+			ignoreabls = pname :: ignoreabls;
+			return 1;
+		}
+	}
+	return 0;
+}
+
+mountit(dfd, mountfd: ref sys->FD, sync: chan of int)
+{
+	sys->pctl(sys->NEWNS | sys->NEWFD, 2 :: dfd.fd :: mountfd.fd :: nil);
+	sync <-= 1;
+	mountfd = sys->fildes(mountfd.fd);
+	dfd = sys->fildes(dfd.fd);
+	if (sys->mount(mountfd, nil, "/", sys->MREPL | sys->MCREATE, nil) == -1) {
+		sys->fprint(sys->fildes(2), "cannot mount\n");
+		spawn exporterror(dfd, sys->sprint("%r"));
+	} else {
+		sync = chan of int;
+		spawn exportpath(sync, dfd);
+		<-sync;
+	}
+}
+
+exporterror(dfd: ref Sys->FD, error: string)
+{
+	tmsg := Tmsg.read(dfd, 0);
+	if (tmsg == nil) {
+		sys->fprint(sys->fildes(2), "exporterror() EOF\n");
+		exit;
+	}
+	pick t := tmsg {
+	Readerror =>
+		sys->fprint(sys->fildes(2), "exporterror() Readerror\n");
+	* =>
+		reply: ref Rmsg = ref Rmsg.Error(tmsg.tag, error);
+		data := reply.pack();
+		sys->write(dfd, data, len data);
+	}
+}
+
+exportpath(sync: chan of int, dfd: ref sys->FD)
+{
+	sync <-= 1;
+	sys->export(dfd, "/", Sys->EXPWAIT);
+}
+
+Qroot : con int iota;
+
+ss : ref Styxserver;
+uid: string;
+
+exitfid := -1;
+
+getuid()
+{
+	buf := array [100] of byte;
+	fd := sys->open("/dev/user", Sys->OREAD);
+	uidlen := sys->read(fd, buf, len buf);
+	uid = string buf[0: uidlen];
+}
+
+dir(name: string, perm: int, length: int, qid: int): Sys->Dir
+{
+	d := sys->zerodir;
+	d.name = name;
+	d.uid = uid;
+	d.gid = uid;
+	d.qid.path = big qid;
+	if (perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	else {
+		d.qid.qtype = Sys->QTFILE;
+		d.length = big length;
+	}
+	d.mode = perm;
+	d.atime = d.mtime = daytime->now();
+	return d;
+}
+
+User: adt {
+	attachfid: int;
+	attr: array of (string, int);
+};
+
+users : array of User;
+
+getuser(fid: int): int
+{
+	for (i := 0; i < len users; i++)
+		if (users[i].attachfid == fid)
+			return i;
+	return -1;
+}
+
+getattr(pname: string): int
+{
+	for (i := 0; i < len defaultattr; i++)
+		if (defaultattr[i].t0 == pname)
+			return i;
+	return -1;
+}
+
+serveloop(fd : ref sys->FD, sync: chan of int)
+{
+	tchan: chan of ref Tmsg;
+	srv: ref Styxserver;
+	echan := chan of string;
+	users = array[20] of { * => User (-1, nil) };
+	sys->pctl(Sys->FORKNS, nil);
+	sync <-= 1;
+	print("serveloop\n",5);
+	getuid();
+	(tree, treeop) := nametree->start();
+	tree.create(big Qroot, dir(".",8r555 | sys->DMDIR,0,Qroot));
+	tree.create(big Qroot, dir("ctl",8r222,0,Qctl));
+	tree.create(big Qroot, dir("abilities",8r444,0,Qabl));
+	tree.create(big Qroot, dir("storage",8r444,0,Qstore));
+	tree.create(big Qroot, dir("power",8r444,0,Qpwr));
+	tree.create(big Qroot, dir("date",8r666,0,Qtime));
+	tree.create(big Qroot, dir("state",8r666,0,Qstate));
+	tree.create(big Qroot, dir("jpg",8r777 | sys->DMDIR,0,Qjpgdir));
+	tree.create(big Qroot, dir("thumb",8r777 | sys->DMDIR,0,Qthumbdir));
+	for (j := 0; j < len interfacepaths; j++) {
+		(n, idir) := sys->stat(interfacepaths[j]);
+		if (n != -1) {
+			idir.qid.path = big Qinterface;
+			# intdir := dir("",8r777,0,Qinterface);
+			# intdir.name = idir.name;
+			# intdir.length = idir.length;
+			# intdir.atime = idir.atime;
+			# intdir.mtime = idir.mtime;
+			tree.create(big Qroot, idir);
+			Qinterface += 1<<4;
+		}
+	}
+
+	tmsgqueue := Tmsgqueue.new(50);
+
+	(tchan, srv) = Styxserver.new(fd,Navigator.new(treeop), big Qroot);
+	fd = nil;
+
+	gm, lastgm: ref Tmsg;
+	gm = nil;
+
+	oldfiles = nil;
+	updatetree(tree);
+
+	print("serveloop loop\n",5);
+	alivechan := chan of int;
+	spawn keepalive(alivechan);
+	alivepid := <-alivechan;
+	retryit := 0;
+	notries := 0;
+	readfid := -1;
+	serveloop: for (;;) {
+		wait = daytime->now();
+		if (notries > 5) retryit = 0;
+		if (retryit) {
+			gm = lastgm;
+			notries++;
+		}
+		else {
+			notries = 0;
+			loop: for (;;) {
+				gm = tmsgqueue.pop(readfid);
+				if (gm != nil)
+					break;
+				alt {
+				gm = <-tchan =>
+					break loop;
+				c := <-alivechan =>
+					for (;;) {
+						s := get_clock();
+						wait = daytime->now();
+						# print(sys->sprint("got alivechan: %s",s),1);
+						if (recon) {
+							killchan := chan of int;
+							spawn noresponse(tchan,srv,killchan);
+							reconnect(-1);
+							killchan <-= 1;
+						}
+						else
+							break;
+					}
+				}
+			}
+		}
+		lastgm = gm;
+		retryit = 0;
+		if (gm == nil) {
+			sys->print("exiting!\n");
+			break serveloop;		# nil => EOF => last mount was unmounted
+		}
+		print(sys->sprint("Got new GM %s tag: %d\n", gm.text(), gm.tag),4);
+		# print(sys->sprint("Got new GM %s tag: %d\n", gm.text(), gm.tag),2);
+
+		if (!connected) {
+			srv.reply(ref Rmsg.Error(gm.tag, "Could not connect to camera"));
+			print("Error: not connected to camera\n",1);
+		}
+		else pick m := gm {
+		Readerror =>
+			print(sys->sprint( "camera: fatal read error: %s\n", m.error),1);
+			break serveloop;
+		Attach =>
+			nu := getuser(-1);
+			if (nu == -1) {
+				srv.reply(ref Rmsg.Error(m.tag, "Camera in use"));
+				break;
+			}
+			m.uname = string nu;
+			srv.default(m);
+			myattr := array[len currentattr] of (string, int);
+			for (i := 0; i < len myattr; i++)
+				myattr[i] = currentattr[i];
+			users[nu] = User (m.fid, myattr);
+			print("adding user "+string nu, 2);
+		Clunk =>
+			nu := getuser(m.fid);
+			if (nu != -1) {
+				users[nu] = User (-1, nil);
+				print("removing user "+string nu, 2);
+			}
+			if (m.fid == readfid) {
+				# sys->print("readfid clunk: %d\n",readfid);
+				readfid = -1;
+			}
+			srv.default(gm);
+		Remove =>
+			print("Removing file\n",3);
+			f := srv.getfid(m.fid);
+			if (f == nil) {
+				print("Remove: Invalid fid\n",1);
+				srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+				break;
+			}
+			ftype := gettype(int f.path);
+			if (ftype != T_JPG) {
+				srv.reply(ref Rmsg.Error(m.tag, "Cannot remove file"));
+				break;
+			}
+			else {
+				for (i := 0; i < reslength; i++) {
+					if (f.path == filelist[i].qid.path) {
+						print("removing filelist\n",5);
+						if (erase_file(filelist[i].cf) != 0) {
+							if (!recon) 
+								srv.reply(ref Rmsg.Error(m.tag, "Cannot remove file"));
+							break;
+						}
+					
+						srv.delfid(f);
+						if (get_file_list() != 0)
+							srv.reply(ref Rmsg.Error(m.tag, "Cannot read files"));
+						else {
+							updatetree(tree);
+							srv.reply(ref Rmsg.Remove(m.tag));
+						}
+						break;
+					}
+				}
+			}
+		Read =>
+			print("got read request in serveloop\n",6);
+			(f,e) := srv.canread(m);
+			if(f == nil)
+				break;
+			if (f.qtype & Sys->QTDIR) {
+				print("reading directory\n",5);
+				srv.read(m);
+				break;
+			}
+			data : array of byte;
+			case gettype(int f.path) {
+			T_INTERFACE =>
+				(dir, intdata) := readinterface(int f.path, m.offset, m.count);
+				if (dir != nil && m.offset == big 0) {
+					dir.qid.path = f.path;
+					tree.wstat(f.path, *dir);
+				}
+				srv.reply(ref Rmsg.Read(m.tag, intdata));
+			T_POWER =>
+				print("reading power mode...\n",3);
+				data = array of byte get_power_mode();
+				if (!recon) srv.reply(styxservers->readbytes(m, data));
+
+			T_TIME =>
+				print("reading clock...\n",3);
+				data = array of byte get_clock();
+				if (!recon)	
+					srv.reply(styxservers->readbytes(m, data));
+
+			T_ABILITIES =>
+				data = array of byte get_camera_capabilities();
+				if (!recon)
+					srv.reply(styxservers->readbytes(m, data));
+
+			T_JPG =>
+				# sys->print("Read Jpg: user %d\n", int f.uname);
+				if (readfid != -1 && readfid != m.fid) {
+					tmsgqueue.push(m);
+					# sys->print("in use!\n");
+					# srv.reply(ref Rmsg.Error(m.tag, "Camera in use, please wait"));
+					break;
+				}
+				readfid = m.fid;
+				data = photoread2(f.path, m,tree,0);
+				if (!recon)
+					srv.reply(ref Rmsg.Read(m.tag, data));
+	
+			T_THUMB =>
+				if (readfid != -1 && readfid != m.fid) {
+					# srv.reply(ref Rmsg.Error(m.tag, "Camera in use, please wait"));
+					tmsgqueue.push(m);
+					break;
+				}
+				readfid = m.fid;
+				# sys->print("Read Thumb: user %d\n", int f.uname);
+				data = photoread2(f.path, m,tree,1);
+				if (!recon)
+					srv.reply(ref Rmsg.Read(m.tag, data));
+
+			T_STATE =>
+				if (currentstate == "") srv.reply(ref Rmsg.Error(m.tag, "No state requested"));
+				else {
+					data = array of byte get_camera_state(currentstate,int m.offset);
+					if (!recon)
+						srv.reply(ref Rmsg.Read(m.tag, data));
+				}
+
+			T_STORAGE =>
+				data = array of byte get_storage_status();
+				if (!recon) {
+					if (len data == 0)
+						srv.reply(ref Rmsg.Error(m.tag, "Could not read storage status"));
+					else
+						srv.reply(styxservers->readbytes(m, data));
+				}
+			* =>
+				srv.reply(ref Rmsg.Error(m.tag, "Cannot read file"));
+			}
+			# if (readfid != -1)
+			# 	sys->print("readfid set: %d\n",readfid);
+		Write =>
+			print("got write request in serveloop\n",6);
+
+			(f,e) := srv.canwrite(m);
+			if(f == nil) {
+				print("cannot write to file\n",1);
+				break;
+			}
+			wtype := gettype(int f.path);
+			(n, s) := sys->tokenize(string m.data, " \t\n");
+			if (wtype == T_TIME) {
+				if (set_clock(string m.data) != 0)
+					srv.reply(ref Rmsg.Error(m.tag, "Invalid date time format\n" + 
+										"Usage: MM/DD/YY HH/MM/SS\n"));
+				else srv.reply(ref Rmsg.Write(m.tag, len m.data));
+				
+			}
+			else if (wtype == T_CTL) {
+				err := "";
+				case hd s {
+				"refresh" =>
+					# for (i := 0; i < reslength; i++) {
+					#	tree.remove(filelist[i].qid.path);
+					#	tree.remove(big filelist[i].cf.thumbqid);
+					# }
+					if (get_file_list() != 0)
+						err = "Error: Could not read from camera";
+					else 
+						updatetree(tree);
+						# for (i = 0; i < reslength; i++) 
+						#	buildfilelist(tree, i);
+				"snap" =>
+					nu := int f.uname;
+					print(sys->sprint("User %d taking photo\n",nu),2);
+					for (i := 0; i < len currentattr; i++) {
+						# sys->print("user: %s=%d current: %s=%d\n",
+						# 	users[nu].attr[i].t0,users[nu].attr[i].t1,
+						#	currentattr[i].t0,currentattr[i].t1);
+						if (users[nu].attr[i].t1 != currentattr[i].t1) {
+							set_camera_state(users[nu].attr[i].t0, users[nu].attr[i].t1);
+							sys->sleep(100);
+						}
+					}
+					e1 := capture();
+					if (e1 == -1) {
+						err = "Cannot communicate with camera";
+						break;
+					}
+					if (e1 != 0) { 
+						err = "Error: "+error_table[e1];
+						break;
+					}
+					sys->sleep(4000);
+					if (get_file_list() != 0) {
+						err = "Error: Could not read from camera";
+						break;
+					}
+					updatetree(tree);
+				* =>
+					if (n == 2) {	# assume that it is a (string, int) tuple
+						na := getattr(hd s);
+						if (na == -1)
+							err = "Invalid command name '"+hd s+"'";
+						else {
+							e1 := set_camera_state(hd s, int hd tl s);
+							if (e1 != nil)
+								err = e;
+							else
+								users[int f.uname].attr[na].t1 = int hd tl s;
+						}
+					}
+					
+				}
+
+				if (!recon) {
+					if (err != "") {
+						print(err+"\n",1);
+						srv.reply(ref Rmsg.Error(m.tag, err));
+					}
+					else srv.reply(ref Rmsg.Write(m.tag, len m.data));
+				}
+			}
+			else if (wtype == T_STATE) {
+				if (s != nil)
+					currentstate = hd s;
+				srv.reply(ref Rmsg.Write(m.tag, len m.data));
+			}
+			else srv.reply(ref Rmsg.Error(m.tag, "Could not write to file"));
+		Wstat =>
+			print("Got Wstat command in serveloop\n",6);
+			srv.reply(ref Rmsg.Error(m.tag, "Wstat failed"));
+		* =>
+			srv.default(gm);
+		}
+		if (recon) {
+			retryit = 1;
+			ok :=	reconnect(4);
+			if (!ok) {
+				srv.reply(ref Rmsg.Error(gm.tag, "Could not connect to camera"));
+				killchan := chan of int;
+				spawn noresponse(tchan,srv,killchan);
+				reconnect(-1);
+				killchan <-= 1;
+				retryit = 0;
+				sys->sleep(100);
+			}
+		}
+	}
+	tree.quit();
+	kill(alivepid);
+	killg(gpid);
+}
+
+Tmsgqueue: adt {
+	start, end, length: int;
+	a : array of ref Tmsg.Read;
+	new: fn (n: int): ref Tmsgqueue;
+	push: fn (t: self ref Tmsgqueue, t: ref Tmsg.Read): int;
+	pop: fn (t: self ref Tmsgqueue, readfid: int): ref Tmsg.Read;
+};
+
+Tmsgqueue.new(n: int): ref Tmsgqueue
+{
+	t : Tmsgqueue;
+	t.start = 0;
+	t.end = 0;
+	t.length = 0;
+	t.a = array[n] of ref Tmsg.Read;
+	return ref t;
+}
+
+Tmsgqueue.push(t: self ref Tmsgqueue,newt: ref Tmsg.Read): int
+{
+	if (t.length >= len t.a)
+		return -1;
+	t.a[t.end] = newt;
+	t.end++;
+	if (t.end >= len t.a)
+		t.end = 0;
+	t.length++;
+	return 0;
+}
+
+Tmsgqueue.pop(t: self ref Tmsgqueue, readfid: int): ref Tmsg.Read
+{
+	if (t.length == 0)
+		return nil;
+	m := t.a[t.start];
+	if (readfid != -1 && readfid != m.fid)
+		return nil;
+	t.start++;
+	if (t.start >= len t.a)
+		t.start = 0;
+	t.length--;
+	return m;
+}
+
+noresponse(tchan: chan of ref Tmsg, srv: ref Styxservers->Styxserver, killchan : chan of int)
+{
+	for (;;) alt {
+		k := <- killchan =>
+			return;
+		gm := <- tchan =>
+			print("noresponse: Returning Error\n",1);
+			srv.reply(ref Rmsg.Error(gm.tag, "Could not connect to camera"));
+			sys->sleep(100);
+	}
+}
+
+photoread2(qid: big, m: ref Tmsg.Read, tree: ref Nametree->Tree, isthumb: int): array of byte
+{
+	photonum := -1;
+	data : array of byte;
+	# sys->print("photoread: qid: %d resl: %d\n",int qid,reslength);
+	for (i := 0; i < reslength; i++) {
+		# sys->print("%d: %s %d\n",i, sconv(filelist[i].cf.dosname),int filelist[i].qid.path);
+		if (!isthumb && qid == filelist[i].qid.path) {
+			photonum = i;
+			break;
+		}
+		else if (isthumb && int qid == filelist[i].cf.thumbqid) {
+			photonum = i;
+			break;
+		}
+	}
+	if (photonum >= reslength || photonum < 0) {
+		print(sys->sprint( "error: photonum = %d (reslength = %d)\n", photonum,reslength),1);
+		return nil;
+	}
+	offset := int m.offset;
+	dosname := filelist[photonum].cf.dosname;
+	filelen := filelist[photonum].cf.filelength;
+	for (k := 0; k < 5; k++) {
+		if (filelen == 0) {
+			get_file_size(photonum);
+			print(sys->sprint("\tFilelen: %d => ",filelen),5);
+			filelen = filelist[photonum].cf.filelength;
+			print(sys->sprint("%d\n",filelen),5);
+			tree.wstat(qid,
+					dir(str->tolower(sconv(filelist[photonum].cf.dosname)),
+					8r444,
+					filelen,
+					int qid));
+			sys->sleep(1000);
+		}
+		else break;
+	}
+	if (filelen == 0 && !isthumb) return nil; # doesn't matter if filesize is wrong for thumbnail
+	if (isthumb) filelen = filelist[photonum].cf.thumblength;
+	if (usecache && cachesize(dosname, isthumb) == filelen) {
+#		print(sys->sprint("Is cached!\n");
+		n := m.count;
+		filesize := cachesize(dosname,isthumb);
+		if (offset >= filesize) return nil;
+		if (offset+m.count >= filesize) n = filesize - offset;
+		data = array[n] of byte;
+		fd := sys->open(cachename(dosname,isthumb), sys->OREAD);
+		if (fd == nil) cachedel(dosname,isthumb);
+		else {
+			sys->seek(fd,m.offset,sys->SEEKSTART);
+			sys->read(fd,data,len data);
+			fd = nil;
+			return data;
+		}
+	}
+#	print(sys->sprint("Is NOT cached!\n");
+
+	if (photonum == ex.pnum && offset == ex.offset && ex.isthumb == isthumb) 
+		data = ex.data;
+	else if (isthumb)
+		data = getthumb(photonum, offset, m.count);
+	else if (!isthumb)
+		data = getpicture2(photonum, offset, m.count);
+	if (len data > m.count) {
+		ex.pnum = photonum;
+		ex.offset = offset + m.count;
+		ex.data = array[len data - m.count] of byte;
+		ex.data[0:] = data[m.count:len data];
+		ex.isthumb = isthumb;
+		data = data[:m.count];
+	}
+	if (usecache) {
+		fd : ref sys->FD;
+		cname := cachename(dosname,isthumb);
+	
+		if (offset == 0)
+			fd = sys->create(cname,sys->OWRITE,8r666);
+		else {
+			fd = sys->open(cname,sys->OWRITE);
+			if (fd != nil)
+				sys->seek(fd,big 0,sys->SEEKEND);
+		}
+		if (fd != nil) {
+			i = sys->write(fd,data,len data);
+			fd = nil;
+		}
+		(n, dir) := sys->stat(cname);
+		if (n == 0) {
+			cacheadd(dosname,isthumb,int dir.length);
+		}
+	}
+	return data;
+}
+
+cachelist : list of (string, int, int);
+
+cacheprint()
+{
+	tmp := cachelist;
+	print("cache:\n",3);
+	while (tmp != nil) {
+		(dn,i1,i2) := hd tmp;
+		print(sys->sprint("\t%s %d %d\n",dn,i1,i2),3);
+		tmp = tl tmp;
+	}
+}
+
+cacheclean()
+{
+	tmp : list of (string, int,int);
+	tmp = nil;
+	while (cachelist != nil) {
+		(dosnm,it,fl) := hd cachelist;
+		for (i := 0; i < reslength; i++) {
+			filelen := filelist[i].cf.filelength;
+			if (it) filelen = filelist[i].cf.thumblength;
+			if (sconv(filelist[i].cf.dosname) == dosnm && filelen == fl) {
+				tmp = (dosnm,it,fl) :: tmp;
+				break;
+			}
+		}
+		cachelist = tl cachelist;
+	}
+	cachelist = tmp;
+}	
+
+cacheadd(dosname1: array of byte, isthumb, filelen: int)
+{
+	dosname := sconv(dosname1);
+	tmp : list of (string, int,int);
+	tmp = nil;
+	updated := 0;
+	while (cachelist != nil) {
+		(dosnm,it,fl) := hd cachelist;
+		if (dosname == dosnm && it == isthumb) {
+			updated = 1;
+			tmp = (dosnm,it,filelen) :: tmp;
+		}
+		else
+			tmp = (dosnm,it,fl) :: tmp;
+		cachelist = tl cachelist;
+	}
+	if (updated == 0)
+		tmp = (dosname,isthumb,filelen) :: tmp;
+	cachelist = tmp;
+}
+
+
+cachedel(dosname1: array of byte, isthumb: int)
+{
+	dosname := sconv(dosname1);
+	tmp : list of (string, int,int);
+	tmp = nil;
+	while (cachelist != nil) {
+		(dosnm,it,filelen) := hd cachelist;
+		if (dosname != dosnm || it != isthumb)
+			tmp = (dosnm,it,filelen) :: tmp;
+		cachelist = tl cachelist;
+	}
+	cachelist = tmp;
+}
+
+cachesize(dosname1: array of byte, isthumb: int): int
+{
+	dosname := sconv(dosname1);
+	tmp := cachelist;
+	while (tmp != nil) {
+		(dosnm,it,filelen) := hd tmp;
+		if (dosname == dosnm && isthumb == it) return filelen;
+		tmp = tl tmp;
+	}
+	return -1;
+}
+
+cachename(dosname: array of byte, isthumb: int): string
+{
+	name := "/tmp/" + str->tolower(sconv(dosname));
+	if (isthumb) name = jpg2bit(name);
+	name[len name - 1] = '~';
+	return name;
+}
+
+poll_and_wait(): int
+{
+	print("poll and wait\n",7);
+	write_n(C.fd, pwl, len pwl);
+#	sys->sleep(100);
+	if (read_n_to(C.fd, pak, len pak,TIMEOUT) < 0) {
+ 		print("poll_and_wait: unexpected read failure, exiting...\n",1);
+ 		return -1;
+	}
+	return 0;
+}
+
+send_packet(): int
+{
+	# computing packet size
+	to_write := C.bufbytes;
+
+	# send the first packet
+	pwl[0] = byte ((1<<5)|(1<<4)|(1<<3)|(1<<2)|(to_write>>8));
+	pwl[1] = byte (to_write&16rff);
+
+	if (poll_and_wait() != 0)
+		return -1;
+#	pak[1] == byte 2; ?
+	pak[1] = byte 2;
+
+	wrote_here := write_n(C.fd, C.cdp, to_write);
+	if (wrote_here != to_write)
+		return -1;
+	return 0;
+}
+
+send_message(): int
+{	
+	v:= 0;
+	rc := chan of int;
+	tc := chan of int;
+
+	spawn timer2(tc,6000);
+	tpid := <- tc;
+	spawn write_message(rc);
+	rpid := <- rc;
+
+	try := 0;
+	alt {
+		<- tc =>
+			kill(rpid);
+			print("error: write timeout\n",1);
+			v = -2;
+			break;
+		v = <- rc =>
+			kill(tpid);
+			break;
+	}
+	return v;
+}
+
+write_message(rc: chan of int)
+{	
+	print("writing msg...\n",6);
+	rc <- = sys->pctl(0, nil);	
+	if (send_packet() != 0) {
+		rc <-= -1;
+		return;
+	}
+	pwl[0] = byte 0;
+	pwl[1] = byte 0;
+	wrote_here := write_n(C.fd, pwl, 2);
+	if (wrote_here != 2) {
+		rc <-= -1;
+		return;
+	}
+	rc <-= 0;
+	print("written\n",6);
+}
+
+extra: adt {
+	pnum: int;
+	offset: int;
+	length: int;
+	data: array of byte;
+	isthumb: int;
+};
+
+ex : extra;
+
+getthumb(photonum, offset, maxlength: int): array of byte
+{
+	if (offset != 0) return nil;
+	print("getting thumbnail\n",3);
+	thumbdata: array of byte;
+	err, h, w, ttype: int;
+	file := filelist[photonum].cf;
+	filesize := 13020;
+	if (offset > 0) {
+		filesize = file.thumblength;
+		if (offset >= filesize) return nil;
+	}
+	for(;;){
+		print(sys->sprint("Filesize: %d offset: %d\n",filesize, offset),5);
+		if (offset + maxlength > filesize)
+			maxlength = filesize - offset;
+		l := maxlength;
+	
+		C.command = cdp_get_file_data;
+		C.bufbytes = build_cdp_header(C.cdp, 68);
+		off := cdp_header_len;
+		off = set_int(C.cdp[off:], file.driveno, off);
+		off = set_fstring(C.cdp[off:], file.pathname, off);
+		off = set_dosname(C.cdp[off:], file.dosname, off);
+		off = set_int(C.cdp[off:], 1, off);
+	
+		off = set_int(C.cdp[off:], offset, off);
+		off = set_int(C.cdp[off:], l, off);
+		off = set_int(C.cdp[off:], filesize, off);
+	
+		print(sys->sprint( "getthumbdata %d %d %d\n", offset, maxlength, filesize),5);
+		send_message();
+#		sys->sleep(2000);
+		if ((err = receive_message()) != 0) {
+			print(sys->sprint("Error %d\n", err),1);
+			return nil;
+		}
+		off = cdp_header_len;
+		print(sys->sprint( "bufbytes  = %d\n", C.bufbytes),5);
+		tmpoffset: int;
+		(tmpoffset, off) = get_int(C.cdp[off:], off);
+		(l, off) = get_int(C.cdp[off:], off);
+		(filesize, off) = get_int(C.cdp[off:], off);
+		print(sys->sprint( "getthumbdata returning %d %d %d\n", offset, l, filesize),5);
+	
+		if (offset == 0) {
+			(filesize, off) = get_int(C.cdp[off:off+4], off);
+			(h, off) = get_int(C.cdp[off:off+4], off);
+			(w, off) = get_int(C.cdp[off:off+4], off);
+			(ttype, off) = get_int(C.cdp[off:off+4], off);
+			filelist[photonum].cf.thumblength = filesize;
+			thumbdata = array[filesize] of byte;
+			print(sys->sprint("Thumb (%d,%d) size: %d type: %d\n",w,h,filesize,ttype),5);
+		}
+		if (offset + l > filesize) l = filesize - offset;
+		print(sys->sprint( "Making array of size: %d\n", l),5);
+		thumbdata[offset:] = C.cdp[off:off+l];
+		offset += l;
+		if (offset >= filesize) break;
+	}
+	return thumb2bit(thumbdata,w,h);
+}
+
+getpicture2(photonum, offset, maxlength: int): array of byte
+{
+	file := filelist[photonum].cf;
+	filesize := int file.filelength;
+	print("getting image\n",3);
+	print(sys->sprint("Filesize: %d offset: %d\n",filesize, offset),5);
+	if (offset >= filesize) return nil;
+	if (offset + maxlength > filesize)
+		maxlength = filesize - offset;
+	l := maxlength;
+	C.command = cdp_get_file_data;
+	C.bufbytes = build_cdp_header(C.cdp, 68);
+	off := cdp_header_len;
+	off = set_int(C.cdp[off:], file.driveno, off);
+	off = set_fstring(C.cdp[off:], file.pathname, off);
+	off = set_dosname(C.cdp[off:], file.dosname, off);
+	off = set_int(C.cdp[off:], 0, off);
+
+	off = set_int(C.cdp[off:], offset, off);
+	off = set_int(C.cdp[off:], l, off);
+	off = set_int(C.cdp[off:], filesize, off);
+
+	print(sys->sprint( "getfiledata %d %d %d\n", offset, maxlength, filesize),5);
+	send_message();
+	if ((err := receive_message()) != 0) {
+		print(sys->sprint("Error %d\n", err),1);
+		return nil;
+	}
+	off = cdp_header_len;
+	print(sys->sprint( "bufbytes  = %d\n", C.bufbytes),5);
+	(offset, off) = get_int(C.cdp[off:], off);
+	(l, off) = get_int(C.cdp[off:], off);
+	(filesize, off) = get_int(C.cdp[off:], off);
+	print(sys->sprint( "getfiledata returning %d %d %d\n", offset, maxlength, filesize),5);
+	filedata := array[l] of byte;
+	filedata[0:] = C.cdp[off:off+l];
+	return filedata;
+}
+
+erase_file(file: Cfile): int
+{
+	C.command = cdp_erase_file;
+	C.bufbytes = build_cdp_header(C.cdp, 52);
+	
+	off := cdp_header_len;
+	off = set_int(C.cdp[off:], file.driveno, off);
+	off = set_fstring(C.cdp[off:], file.pathname, off);
+	off = set_dosname(C.cdp[off:], file.dosname, off);
+	send_message();
+#	sys->sleep(1000);
+	if (receive_message() != 0)
+		return -1;
+	return 0;
+}
+
+
+set_power_mode(): int
+{
+	C.command = cdp_set_power_mode;
+	C.bufbytes = build_cdp_header(C.cdp, 0);
+	return (send_message());
+}
+
+get_storage_status(): string
+{
+	s := "";
+
+	C.command = cdp_get_storage_status;
+	C.bufbytes = build_cdp_header(C.cdp, 0);
+	send_message();
+#	sys->sleep(2000);
+	if (receive_message() != 0) return "";
+	off := cdp_header_len;
+	taken, available, raw : int;
+	(taken, off) = get_int(C.cdp[off:], off);
+	(available, off) = get_int(C.cdp[off:], off);
+	(raw, off) = get_int(C.cdp[off:], off);
+	s += sys->sprint("Picture Memory\n\tused:\t%d\n\tfree:\t%d",taken,available);
+	if (raw == -1)
+		s += "\n";
+	else
+		s += sys->sprint(" (compressed)\n\t\t%d (raw)\n",raw);
+
+	return s;
+}
+
+get_power_mode(): string
+{
+	mode: int;
+
+	C.command = cdp_get_power_mode;
+	C.bufbytes = build_cdp_header(C.cdp, 0);
+	send_message();
+#	sys->sleep(2000);
+	if (receive_message() != 0) return "Could not read power mode";
+	off := cdp_header_len;
+	(mode, off) = get_int(C.cdp[off:], off);
+	return sys->sprint("Power Mode = %d\n", mode);
+}
+
+set_clock_data(s:string): int
+{
+	err := 0;
+	if (s == "") {
+		tm := daytime->local(daytime->now());
+		off := cdp_header_len;
+		C.cdp[cdp_header_len+0] = byte 0;
+		C.cdp[cdp_header_len+1] = byte int2hex(tm.mon+1);
+		C.cdp[cdp_header_len+2] = byte int2hex(tm.mday);
+		C.cdp[cdp_header_len+3] = byte int2hex(tm.year);
+		C.cdp[cdp_header_len+4] = byte 0;
+		C.cdp[cdp_header_len+5] = byte int2hex(tm.hour);
+		C.cdp[cdp_header_len+6] = byte int2hex(tm.min);
+		C.cdp[cdp_header_len+7] = byte int2hex(tm.sec);
+	}
+	else {
+		(n,datetime) := sys->tokenize(s," ");
+		if (n != 2) return 1;
+		off := 0;
+		for (i := 0; i < 2; i++) {
+			(n2,data) := sys->tokenize(hd datetime, "./:");
+			if (n2 != 3) return 1;
+			off++;
+			for (i2 := 0; i2 < 3; i2++) {
+				C.cdp[cdp_header_len+off] = byte int2hex(int hd data);
+				off++;
+				data = tl data;
+			}
+			datetime = tl datetime;
+		}
+	}
+	return 0;
+}
+
+set_clock(s:string): int
+{
+	C.command = cdp_set_clock;
+	C.bufbytes = build_cdp_header(C.cdp, 8);
+	if (set_clock_data(s)) return 1;
+	send_message();
+	if (receive_message() != 0) return 1;
+	return 0;
+}
+
+addzeros(s: string): string
+{
+	s[len s] = ' ';
+	rs := "";
+	start := 0;
+	isnum := 0;
+	for (i := 0; i < len s; i++) {
+		if (s[i] < '0' || s[i] > '9') {
+			if (isnum && i - start < 2) rs[len rs] = '0';
+			rs += s[start:i+1];
+			start = i+1;
+			isnum = 0;
+		}
+		else isnum = 1;
+	}
+	i = len rs - 1;
+	while (i >= 0 && rs[i] == ' ') i--;
+	return rs[:i+1];
+}	
+
+get_clock(): string
+{
+	C.command = cdp_get_clock;
+	C.bufbytes = build_cdp_header(C.cdp, 0);
+	send_message();
+	if (receive_message() != 0)
+		return "Could not read clock\n";
+	s := sys->sprint("%x/%x/%x %x:%x:%x", int C.cdp[13],int C.cdp[14],
+		int C.cdp[15], int C.cdp[17], int C.cdp[18], int C.cdp[19]);
+	return "date is "+addzeros(s)+"\n";
+}
+
+get_file_list(): int
+{
+	getoldfiledata();
+	print("getting file list\n",3);
+	C.command = cdp_get_file_list;
+	C.bufbytes = build_cdp_header(C.cdp, 56);
+	setfiledata();
+	send_message();
+	if (receive_message() != 0)
+		return -1;
+	display_filelist();
+	return 0;
+}
+
+setfiledata()
+{
+	off := cdp_header_len;
+	off = set_int(C.cdp[off:], 1, off);						# ascending order
+	off = set_int(C.cdp[off:], 1, off);						# drive a: internal RAM disk
+	off = set_fstring(C.cdp[off:], array of byte "", off);		# set pathname to null
+	off = set_dosname(C.cdp[off:], array of byte "", off);		# set Dos filename to null 
+}
+
+get_file_size(i: int): int
+{
+	C.command = cdp_get_file_list;
+	C.bufbytes = build_cdp_header(C.cdp, 56);
+	setfiledata2(i);
+	send_message();
+	if (receive_message() != 0) return -1;
+	display_filelist();
+	return 0;
+}
+
+setfiledata2(i: int)
+{
+	off := cdp_header_len;
+	off = set_int(C.cdp[off:], 1, off);						# ascending order
+	off = set_int(C.cdp[off:], 1, off);						# drive a: internal RAM disk
+	off = set_fstring(C.cdp[off:], filelist[i].cf.pathname, off);	# set pathname
+	off = set_dosname(C.cdp[off:], filelist[i].cf.dosname, off);	# set Dos filename
+}
+
+set_interface_timeout()
+{
+	print("Setting Interface timeout\n",3);
+	C.command = cdp_set_interface_timeout;
+	C.bufbytes = build_cdp_header(C.cdp, 8);
+	off := cdp_header_len;
+	off = set_int(C.cdp[off:], 100, off);
+	off = set_int(C.cdp[off:], 5, off);
+	send_message();
+#	sys->sleep(1000);
+	receive_message();
+}
+
+display_filelist(): string
+{
+	off, i: int;
+
+	off = cdp_header_len;
+	(reslength, off) = get_int(C.cdp[off:], off);
+	s := sys->sprint("Number of entries: %d\n", reslength);
+	for (i = 0; i < reslength; i++) {
+		(filelist[i].cf.driveno, off) = get_int(C.cdp[off:], off);
+		(filelist[i].cf.pathname, off) = get_fstring(C.cdp[off:], off);
+		(filelist[i].cf.dosname, off) = get_dosname(C.cdp[off:], off);
+		(filelist[i].cf.filelength, off) = get_int(C.cdp[off:], off);
+		(filelist[i].cf.filestatus, off) = get_int(C.cdp[off:], off);
+		if (filelist[i].cf.filelength < 0 || filelist[i].cf.filelength > MAXFILESIZE)
+			filelist[i].cf.filelength = 0;
+		s += sys->sprint("\t%d, %s, %s, %d\n", filelist[i].cf.driveno,
+				string filelist[i].cf.pathname,
+				string filelist[i].cf.dosname,
+				filelist[i].cf.filelength);
+	}
+	print(s,5);
+	if (usecache)
+		cacheclean();
+	return s;
+}
+
+get_camera_capabilities(): string
+{
+	print("Get capabilities\n",3);
+	C.command = cdp_get_camera_capabilities;
+	C.bufbytes = build_cdp_header(C.cdp, 0);
+	send_message();
+#	sys->sleep(500);
+	if (receive_message() != -1)
+		return capabilities();
+	print("Error recieving abilities message\n",1);
+	return "";
+}
+
+Capability: adt {
+	pname: string;
+	d: string;
+	pick {
+		List =>
+			t: list of (string, int);
+		Range =>
+			min, max, default, current: int;
+		}
+};
+
+caplist: list of ref Capability;
+
+print_camera_capabilities(): string
+{
+	rs := "";
+#	p : ref Capability;
+
+	pick p := hd caplist{
+	List =>
+		rs += sys->sprint("Pname = %s ", p.pname);
+	Range =>
+		rs += sys->sprint("Pname = %s  min = %d  max = %d  default = %d ", p.pname, 
+				p.min, p.max, p.default);
+	}
+#	p := tl p;
+	return rs;
+}
+
+capabilities(): string
+{
+	off, i, ncaps, t: int;
+	l, m, n: int;
+	pname, desc: array of byte;
+	s: array of byte;
+	rs := "";
+	off = cdp_header_len;
+	(ncaps, off) = get_int(C.cdp[off:], off);
+	if (ncaps > 200)
+		return "error reading capabilities\n";
+	rs += sys->sprint("i = %d\n", i);
+	firsttime := 0;
+	if (ignoreabls == nil)
+		firsttime = 1;
+	for (j := 0; j < ncaps; j++) {
+		line := "";
+		(pname, off) = get_pname(C.cdp[off:], off);
+		line += sys->sprint("%s,  ", string pname);
+		(t, off) = get_int(C.cdp[off:], off);
+		(desc, off) = get_fstring(C.cdp[off:], off);
+		line += sys->sprint("%s:  ", string desc);
+		fact := "";
+		case t {
+			1 =>
+				t: list of (string, int);
+
+				(l, off) = get_int(C.cdp[off:], off);
+				(m, off) = get_int(C.cdp[off:], off);
+				line += sys->sprint("items: %d  factory: %d\n", l, m);
+
+				for (k := 0; k < l; k++) {
+					(s, off) = get_fstring(C.cdp[off:], off);
+					(n, off) = get_int(C.cdp[off:], off);
+					line += sys->sprint("		%s: %d\n", string s, n);
+					if (m == n)
+						fact = sconv(s);
+					t = (sconv(s), n) :: t;
+				}
+				cl := ref Capability.List (sconv(pname), sconv(desc), t);
+			2 =>
+				(l, off) = get_int(C.cdp[off:], off);
+				(m, off) = get_int(C.cdp[off:], off);
+				(n, off) = get_int(C.cdp[off:], off);
+				line += sys->sprint("min: %d   max: %d   factory:%d\n", l, m, n);
+				fact = string n;
+			3 =>
+				(l, off) = get_int(C.cdp[off:], off);
+				case l {
+					7 =>
+						(s, off) = get_dosname(C.cdp[off:], off);
+					8 =>
+						(s, off) = get_fstring(C.cdp[off:], off);
+					* =>
+						line += sys->sprint("Invalid type %d\n", l);
+						break;
+				}
+				fact = string s;
+				line += sys->sprint("%s\n", string s);
+			4 to 8 =>
+				break;
+			9 =>
+				break;
+			* =>
+				line += sys->sprint("Invalid type %d\n", t);
+				break;
+		}
+		if (firsttime) {
+			if (!filterabls(sconv(pname), string desc))
+				defattr = (sconv(pname), int fact) :: defattr;
+		}
+		if (!isin(ignoreabls, string pname))
+			rs += line;
+	}
+	if (firsttime) {
+		defaultattr = array[len defattr] of (string, int);
+		currentattr = array[len defattr] of (string, int);
+		i = 0;
+		for (;defattr != nil; defattr = tl defattr) {
+			defaultattr[i] = hd defattr;
+			currentattr[i++] = hd defattr;
+		}
+	}
+	return rs;
+}
+
+isin(los: list of string, s: string): int
+{
+	for (;los !=nil; los = tl los)
+		if (hd los == s)
+			return 1;
+	return 0;
+}
+
+set_capture_data(): int
+{
+	C.cdp[cdp_header_len+0] = byte 0;
+	C.cdp[cdp_header_len+1] = byte 0;
+	C.cdp[cdp_header_len+2] = byte 0;
+	C.cdp[cdp_header_len+3] = byte 0;
+	return 4;
+}
+
+get_camera_state(pname: string,offset: int): string
+{
+	if (offset != 0) return "";
+	print(sys->sprint( "get_camera_state(%s)\n", pname),3);
+	C.command = cdp_get_camera_state;
+	off := cdp_header_len;
+	if (pname == "")
+		C.bufbytes = build_cdp_header(C.cdp, 0);
+	else {
+		if (len pname != 4)
+			return "Invalid command name: "+pname+"\n";
+		C.cdp[off+0] = byte pname[0];
+		C.cdp[off+1] = byte pname[1];
+		C.cdp[off+2] = byte pname[2];
+		C.cdp[off+3] = byte pname[3];
+		C.bufbytes = build_cdp_header(C.cdp, 4);
+	}
+	send_message();
+	if (receive_message() != 0) return "Could not read state: "+pname+"\n";
+	off = cdp_header_len;
+	rlen: int;
+	(rlen, off) = get_int(C.cdp[off:],off);
+	s := "";
+	rlen = 1;
+	if (pname == "") {
+		for (q := off; q < len C.cdp; q++) {
+			s[0] = int C.cdp[q];
+			if (s[0] > 0) print(sys->sprint("%s",s),5);
+		}
+		print("\n",5);
+	}
+	for (i := 0; i < rlen; i++) {
+		name, data: array of byte;
+		type1, tmp: int;
+		(name,off) = get_pname(C.cdp[off:],off);
+		(type1,off) = get_int(C.cdp[off:],off);
+		print(sys->sprint( "%d: %s - %d\n", i,pname,type1),5);
+		case type1 {
+			1 to 5 =>
+				(tmp,off) = get_int(C.cdp[off:],off);
+				data = array of byte string tmp;
+			6 =>
+				(data,off) = get_pname(C.cdp[off:],off);
+			7 =>
+				(data,off) = get_dosname(C.cdp[off:],off);
+			8 =>
+				(data,off) = get_fstring(C.cdp[off:],off);
+			* =>
+				data = array of byte "!ERROR!";
+		}
+		# if (string data == "!ERROR!") return "";
+#		if (rlen == 1)
+#			s = string data;
+#		else s += sys->sprint("%s: %s\n",string name, string data);
+		s += sys->sprint("%s: %s\n",string name, string data);
+	}
+	return s;
+}
+
+
+set_camera_state(pname: string, val: int): string
+{
+	print(sys->sprint( "set_camera_state(%s, %d)\n", pname, val),3);
+	if (len pname != 4)
+		return "Command name must be 4 characters";
+	off := cdp_header_len;
+	C.cdp[off+0] = byte pname[0];
+	C.cdp[off+1] = byte pname[1];
+	C.cdp[off+2] = byte pname[2];
+	C.cdp[off+3] = byte pname[3];
+	off += 4;
+	off = set_int(C.cdp[off:], val, off);
+
+	C.command = cdp_set_camera_state;
+	C.bufbytes = build_cdp_header(C.cdp, 8);
+	send_message();
+#	sys->sleep(1000);
+	if ((e := receive_message()) == 0) {
+		na := getattr(pname);
+		if (na != -1)
+			currentattr[na].t1 =  val;
+		return nil;
+	}
+	else
+		return error_table[e];
+}
+
+capture(): int
+{
+	C.command = cdp_get_camera_status;
+	C.bufbytes = build_cdp_header(C.cdp, 0);
+	send_message();
+#	sys->sleep(1000);
+	if (receive_message() != 0)
+		return -1;
+
+	d := set_capture_data();
+	C.command = cdp_start_capture;
+	C.bufbytes = build_cdp_header(C.cdp, d);
+	send_message();
+#	sys->sleep(3000);
+	return receive_message();
+}
+
+dump_message()
+{
+	print(sys->sprint("	Message length = %d\n", C.bufbytes),5);
+	print(sys->sprint("	CDP Length = %d\n", (int C.cdp[2]<<8)+(int C.cdp[3])),5);
+	print(sys->sprint("	CDP Version = %d\n", int C.cdp[4]),5);
+	print(sys->sprint("	CDP Command = %x\n", int ((C.cdp[8]<<8)|(C.cdp[9]))),5);
+	print(sys->sprint("	CDP Result Code = %d\n", int ((C.cdp[10]<<8)|(C.cdp[11]))),5);
+}
+
+build_cdp_header(cdp: array of byte, x: int): int
+{
+	cdp[4] = byte 0;
+	cdp[5] = byte 0;
+	cdp[6] = byte 0;
+	cdp[7] = byte 0;
+	cdp[8] = byte ((C.command>>8)&16rff);
+	cdp[9] = byte (C.command&16rff);
+	cdp[10] = byte 0;
+	cdp[11] = byte 0;
+
+	l := 8 + x;
+	cdp[0] = byte ((l>>24)&16rff);
+	cdp[1] = byte ((l>>16)&16rff);
+	cdp[2] = byte ((l>>8)&16rff);
+	cdp[3] = byte (l&16rff);
+
+	return 12+x;
+}
+
+poll_and_reply(nak: int): int
+{
+	print("poll and reply\n",7);
+	if ((read_n_to(C.fd, pwl, len pwl,TIMEOUT) < 0) && nak) {
+		pak[0] = byte 0;	
+		pak[1] = byte 2;		# reject
+		write_n(C.fd, pak, len pak);
+		return 0;
+	}
+	pak[0] = byte 0;
+	pak[1] = byte 1;
+	write_n(C.fd, pak, len pak);
+
+	return 1;
+}
+
+receive_packet(buf: array of byte): int
+{
+	print("receive_packet\n",6);
+	if (!poll_and_reply(!0)) {
+		print("Poll and reply failed\n",1);
+		return -1;
+	}
+
+	l := int (((int pwl[0]&3)<<8)|(int pwl[1]));
+	C.bufbytes += l;
+	r := read_n_to(C.fd, buf, l,TIMEOUT);
+	if (r != l) {
+		print(sys->sprint( "could not read packet (read %d, expected %d)\n", r, l),1);
+		return -1;
+	}
+	return 0;
+}
+
+receive_message(): int
+{
+	print("read_message\n",6);
+	C.bufbytes = 0;
+	if (receive_packet(C.cdp[0:]) != 0) {
+		recon = 1;
+		print("receive packet failed\n",1);
+		return 3;
+		# raise "error: receive packet failed";
+	}
+	dump_message();	
+	rc := int C.cdp[9];
+	if ((~rc&16rff) != (C.command&16rff)) {
+		print("command & return are different\n",1);
+		consume(C.fd);
+		return 3;
+		# raise "error: command and return command are not the same\n";
+	}
+	message_len := (int C.cdp[2]<<8)+(int C.cdp[3]);
+
+	while (C.bufbytes < message_len) {
+		if (receive_packet(C.cdp[C.bufbytes:]) != 0) {
+			print("Packet is too short\n",1);
+			recon = 1;
+			return 3;
+			# raise "error: receive packet2 failed";
+		}
+	}
+#	sys->sleep(500);
+	read_n_to(C.fd, pak, len pak, TIMEOUT);
+	return  (int ((C.cdp[10]<<8)|(C.cdp[11])));  # result code
+}
+
+reset(fd: ref Sys->FD)
+{
+	sys->fprint(fd, "d1");
+	sys->sleep(20);
+	sys->fprint(fd, "d0");
+	sys->fprint(fd, "b9600");
+}
+
+kill(pid: int)
+{	
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil)
+		sys->write(pctl, array of byte "kill", len "kill");
+}
+
+killg(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "killgrp");
+}
+
+#dump_buf(buf: array of byte, i: int)
+#{
+#	for (j := 0; j < i; j++)
+#		sys->fprint(sys->fildes(2), "%x ", int buf[j]);
+#	sys->fprint(sys->fildes(2), "\n");
+#}
+
+serialport(port : int) : (ref Sys->FD, ref Sys->FD, string)
+{
+	C.fd = nil;
+	C.ctlfd = nil;
+	C.mode = BEACON;
+
+	serport := "/dev/eia" + string port;
+	serctl := serport + "ctl";
+
+	for (i := 0; i < len statopt; i++) {
+		statfd := sys->open("/dev/eia"+string port+statopt[i],sys->OREAD);
+		if (statfd != nil)
+			C.stat = i;
+		statfd = nil;
+	}
+	readstat();	
+
+	fd := sys->open(serport, Sys->ORDWR);
+	if (fd == nil)
+		return (nil, nil, sys->sprint("cannot read %s: %r", serport));
+	ctlfd := sys->open(serctl, Sys->OWRITE);
+	if (ctlfd == nil)
+		return (nil, nil, sys->sprint("cannot open %s: %r", serctl));
+
+	config := array [] of {
+		"b9600",
+		"l8",
+		"p0",
+		"m0",
+		"s1",
+		"r1",
+		"i1",
+		"f",
+	};
+
+	for (i = 0; i < len config; i++) {
+		if (sys->fprint(ctlfd,"%s", config[i]) < 0)
+			print(sys->sprint("serial config (%s): %r\n", config[i]),3);
+	}
+	sys->sleep(100);
+	consume(fd);
+	sys->fprint(ctlfd, "d1");
+	sys->sleep(40);
+	sys->fprint(ctlfd, "d0");
+	return (fd, ctlfd, nil);
+}
+
+consume(fd: ref sys->FD)
+{
+	if (fd != nil) {
+		print("Consuming...\n",6);
+		read_n_to(fd, array[1000] of byte, 1000, 1000);
+	}
+}
+
+beacon_intro(data: chan of array of byte, pchan: chan of int, fd: ref Sys->FD)
+{
+	buf := array[64] of byte;
+	cbuf: array of byte;
+	pid := sys->pctl(0, nil);
+#	print(sys->sprint("b_intro: starting %d\n",pid);
+	pchan <-= pid;
+	failed := array[len bintro] of { * => byte 0 };
+	# discard characters until lead in character reached
+	print(sys->sprint("\tWaiting for: %d...\n",int bintro[0]),3);
+	do {
+		n := read_n_to(fd, buf, 1, TIMEOUT);
+		if (n == -1) {
+			data <- = failed;
+			return;
+		}
+		print(sys->sprint("\tGot: %d\n",int buf[0]),5);
+	} while (buf[0] != bintro[0]);
+	print("Getting beacon\n",3);
+	# read the next 6 bytes of beacon
+	i := read_n_to(fd, buf[1:], 6,TIMEOUT);
+	for (k := 0; k < i; k++) 
+		print(sys->sprint("\tRead %d: %d (wanted %d)\n",k+1, int buf[1+k], int bintro[1+k]),5);
+	if (i != 6) {
+		print("Error reading beacon\n",3);
+		exit;
+	}
+	else {
+		print("sending beacon\n",3);
+		cbuf = buf[0:7];
+		data <- = cbuf;	
+	}
+
+}
+
+beacon_result(data: chan of array of byte, pchan: chan of int, fd: ref Sys->FD)
+{
+	buf := array[64] of byte;
+	cbuf: array of byte;
+	pid := sys->pctl(0, nil);
+	pchan <-= pid;
+
+	# read the next 10 bytes of beacon
+	p := 0;
+	intro := 1;
+	for (;;) {
+		i := read_n_to(fd, buf[p:], 1, TIMEOUT);
+		if (intro) {
+			if (buf[p] != bintro[p]) {
+				intro = 0;
+				buf[0] = buf[p];
+				p = 1;
+			}
+			else {
+				p++;
+				if (p >= len bintro) p = 0;
+			}
+		}
+		else p++;
+		if (p == 10) break;
+	}
+			
+	for (k := 0; k < p; k++) print(sys->sprint("\tRead %d: %d\n",k, int buf[k]),5);
+	if (p != 10) {
+		print("Error reading beacon result\n",3);
+		exit;
+	}
+	else {
+		print("reading beacon result\n",3);
+		cbuf = buf[0:10];
+		data <- = cbuf;	
+	}
+}
+
+beacon_comp(buf: array of byte, C: Camera_adt): int
+{
+	speed: string;
+
+	case int buf[0] {
+		0 =>
+			C.baud = (int buf[2]<<24)|(int buf[3]<<16)|(int buf[4]<<8)|(int buf[5]);
+			C.dfs = (int buf[6]<<8)|(int buf[7]);
+			C.hfs = (int buf[8]<<8)|(int buf[9]);
+			# do baud rate change here
+			sys->sleep(1000);
+
+			case C.baud {
+				115200 =>
+					speed = "b115200";
+				57600 =>
+					speed = "b57600";
+				38400 =>
+					speed = "b38400";
+				19200 =>
+					speed = "b19200";
+				* =>
+					speed = "b9600";
+			}
+			print(sys->sprint("Connection Details:\n  Baud rate:\t%dbps\n",C.baud),3);
+			print(sys->sprint("  Host frame size:\t%dbytes\n",C.hfs),3);
+			print(sys->sprint("  Device frame size:\t%dbytes\n",C.dfs),3);
+			if (sys->fprint(C.ctlfd,"%s", speed) < 0) {
+				print(sys->sprint("Error setting baud rate %s\n", speed),3);
+				return -1;
+			}
+		-1 =>
+			print("Incompatible Data Rate\n",1);
+			return -1;
+		-2 =>
+			print("Device does not support these modes\n",1);
+			return -2;
+		* =>
+			print(sys->sprint("I'm here!? buf[0] = %d\n",int buf[0]),1);
+			return -1;
+	}
+	return 0;
+}
+
+read_n(fd: ref Sys->FD, buf: array of byte, n: int, res: chan of int)
+{
+	pid := sys->pctl(0, nil);
+#	print(sys->sprint("read_n: starting %d\n",pid);
+	res <-= pid;
+	print(sys->sprint( "read_n %d\n", n),7);
+	nread := 0;
+	while (nread < n) {
+		i := sys->read(fd, buf[nread:], n-nread);
+		sys->sleep(1);
+		if (i <= 0) 
+			break;
+		nread += i;
+	}
+	res <-= nread;
+#	print(sys->sprint("read_n: ending %d\n",pid);
+}
+
+read_n2(fd: ref Sys->FD, buf: array of byte, n: int): int
+{
+	print(sys->sprint( "read_n2 %d\n", n),7);
+	nread := 0;
+	while (nread < n) {
+		i := sys->read(fd, buf[nread:], n-nread);
+		sys->sleep(1);
+		if (i <= 0) 
+			break;
+		nread += i;
+	}
+	return nread;
+}
+
+read_n_to(fd: ref Sys->FD, buf: array of byte, n,t : int): int
+{	
+	v:= 0;
+	rc := chan of int;
+	tc := chan of int;
+
+	spawn timer2(tc,t);
+	tpid := <- tc;
+	spawn read_n(fd, buf, n, rc);
+	rpid := <- rc;
+
+	try := 0;
+	alt {
+		<- tc =>
+			kill(rpid);
+			print(sys->sprint( "error: read_n timeout\n"),1);
+			recon = 1;
+			return -1;
+		v = <- rc =>
+			kill(tpid);
+			break;
+	}
+	return v;
+}
+
+write_n(fd: ref Sys->FD, buf: array of byte, n: int): int
+{
+	print(sys->sprint("write_n %d\n", n),7);
+	nwrite := 0;
+	while (nwrite < n) {
+		i := sys->write(fd, buf[nwrite:], n-nwrite);
+		sys->sleep(1);
+		if (i <= 0) {
+			print(sys->sprint("Error returned by write: %r\n"),1);
+			readstat();
+#			recon = 1;
+			return nwrite;
+		}
+		nwrite += i;
+	}
+	print(sys->sprint("write_n returning %d\n", nwrite),7);
+	return nwrite;	
+}
+
+readstat()
+{
+	consume(C.fd);
+	print("Serial status: ",5);
+	statfd := sys->open("/dev/eia"+string C.port_num+statopt[C.stat], sys->OREAD);
+	buf := array[100] of byte;
+	if (statfd != nil) {
+		for (;;) {
+			k := sys->read(statfd,buf,len buf);
+			if (k > 0) print(string buf[:k],2);
+			else break;
+		}
+		print("\n",2);
+	}
+	else print("cannot read serial status\n",1);
+}
+
+beacon_ack(C: Camera_adt)
+{
+	# set speed
+	i := C.baud;
+	bak[4] = byte ((i>>24)&16rff);
+	bak[5] = byte ((i>>16)&16rff);
+	bak[6] = byte ((i>>8)&16rff);
+	bak[7] = byte (i&16rff);
+
+	# set frame size to device
+	i = C.dfs;
+	bak[8] = byte ((i>>8)&16rff);
+	bak[9] = byte (i&16rff);
+
+	# set frame size to host
+	i = C.hfs;
+	bak[10] = byte ((i>>8)&16rff);
+	bak[11] = byte (i&16rff);
+	bak[12] = check_sum(bak, 12);
+
+	if (write_n(C.fd, bak, len bak) != len bak) {
+		print("Error writing beacon acknowledgement\n",3);
+		exit;
+	}
+	print("beacon acknowledgement written\n",3);
+}
+
+# timer thread send tick <- = 0 to kill
+
+timer2(tick: chan of int, delay: int)
+{
+	pid := sys->pctl(0, nil);
+	tick <-= pid;
+	sys->sleep(delay);
+	tick <- = TOUT;
+}
+
+beacon_ok(buf: array of byte): int
+{
+
+	for (i := 0; i < len bintro; i++) {
+		if (buf[i] != bintro[i]) {
+			print(sys->sprint("Beacon failed on byte %d: %d (wanted %d)\n",i,int buf[i],int bintro[i]),3);
+			return 0;
+		}
+	}
+	print("Beacon passed\n",3);
+	return 1;
+}
+
+check_sum(buf: array of byte, l: int): byte
+{
+	sum := 0;
+ 	for (i := 0; i < l; i++) 
+		sum += int buf[i];
+  	return byte (sum&16rff);
+}
+
+
+set_int(b: array of byte, i, off: int): int
+{
+	b[0] = byte (i>>24&16rff);
+	b[1] = byte (i>>16&16rff);
+	b[2] = byte (i>>8&16rff);
+	b[3] = byte (i&16rff);
+
+	return (off+4);
+}
+
+set_fstring(b: array of byte, s: array of byte, off: int): int
+{
+	for (i := 0; i < 32; i++)
+		b[i] = byte 0;
+	for (i = 0; i < len s; i++)
+		b[i] = s[i];
+	return (off+32);
+}
+
+set_dosname(b: array of byte, s: array of byte, off: int): int
+{
+	for (i := 0; i < 16; i++)
+		b[i] = byte 0;
+	for (i = 0; i < len s; i++)
+		b[i] = s[i];
+	return (off+16);
+}
+
+get_tag(b: array of byte, off: int): (int, Partialtag)
+{
+	tag: Partialtag;
+	(off, tag.offset) = get_int(b, off);
+	(off, tag.length) = get_int(b, off);
+	(off, tag.filesize) = get_int(b, off);
+	return (off, tag);
+}
+
+get_int(b: array of byte, off: int): (int, int)
+{
+	return (get_int2(b), off+4);
+}
+
+get_int2(b: array of byte): int
+{
+	i := (int b[0]<<24)|(int b[1]<<16)|(int b[2]<<8)|(int b[3]);
+	return i;
+}
+
+
+get_pname(b: array of byte, off: int): (array of byte, int)
+{
+	return get_string(b, off, 4);
+}
+
+get_dosname(b: array of byte, off: int): (array of byte, int)
+{
+	return get_string(b, off, 16);
+}
+
+get_string(b: array of byte, off: int, l: int): (array of byte, int)
+{
+	s := array[l] of byte;
+	s[0:] = b[0:l];
+	return (s, off+l);
+}
+
+get_fstring(b: array of byte, off: int): (array of byte, int)
+{
+	return get_string(b, off, 32);
+}
+
+sconv(b: array of byte): string
+{
+	s := string b;
+	i := len s-1;
+	while (i >= 0 && s[i] == 0)
+		i--;
+	return s[0:i+1];
+}
+
+name2dos(s: string): array of byte
+{
+	return array of byte str->toupper(s);
+}
+
+getqid(i, ftype: int): int
+{
+	qid := (i<<4) + ftype;
+	return qid;
+}
+
+gettype(qid: int): int
+{
+	ftype := qid & 15;
+	return ftype;
+}
+
+cutdir(ab:array of byte): string
+{
+	s := sconv(ab);
+	for (i := 0; i < len s-1; i++)
+		if (s[i] == '/')
+			return s[i+1:len s - 1];
+	return "";
+}
+
+convert_thumb(w,h: int, data: array of byte): array of byte
+{
+	rgb := array[w * h * 3] of byte;
+	index := 0;
+	rgbi := 0;
+	for (i := 0; i < (w * h) / 2; i++) {
+
+		cb := real data[index];
+		y := real data[index+1];
+		cr := real data[index+2];
+
+		rb := conv(y + (1.77200 * (cb - 128.0)));
+		gb := conv(y - (0.34414 * (cb - 128.0)) - (0.71414 * (cr - 128.0)));
+		bb := conv(y + (1.4020 * (cr - 128.0)));
+
+		for (loop := 0; loop < 2; loop++) {
+			rgb[rgbi++] = rb;
+			rgb[rgbi++] = gb;
+			rgb[rgbi++] = bb;
+		}
+		index += 4;
+	}
+	return rgb;
+}
+
+conv(a: real): byte
+{
+	r := int a;
+	if (r < 0) r = -r;
+	if (r > 255) r = 255;
+	return byte r;
+}
+
+thumb2bit(buf: array of byte, w,h: int):  array of byte
+{
+	convbuf := convert_thumb(w,h,buf);
+	# assume thumbs are small so we wont gain much by compressing them
+	bitarray := array [60+len convbuf] of byte;
+	# assume chans = RGB24
+	bitarray[:] = array of byte sys->sprint("%11s %11d %11d %11d %11d ", "r8g8b8", 0, 0, w, h);
+	bitarray[60:] = convbuf;
+	return bitarray;
+}
+
+jpg2bit(s: string): string
+{
+	if (len s < 4) return s;
+	if (s[len s - 4:] != ".jpg") return s;
+	return s[:len s - 4]+".bit";
+}
+
+oldfiles : list of (string, int, int);
+
+getoldfiledata()
+{
+	oldfiles = nil;
+	for(i := 0; i < reslength; i++)
+		oldfiles = (str->tolower(sconv(filelist[i].cf.dosname)),
+				int filelist[i].qid.path,
+				filelist[i].cf.thumbqid) :: oldfiles;
+}
+
+updatetree(tree: ref Nametree->Tree)
+{
+	for (i := 0; i < reslength; i++) {
+		name := str->tolower(sconv(filelist[i].cf.dosname));
+		found := 0;
+		tmp : list of (string, int, int) = nil;
+		for (; oldfiles != nil; oldfiles = tl oldfiles) {
+			(oldname, oldqid, oldthumbqid) := hd oldfiles;
+			# sys->print("'%s' == '%s'?\n",name,oldname);
+			if (name == oldname) {
+				found = 1;	
+				filelist[i].qid = (big oldqid, 0, sys->QTFILE);
+				filelist[i].cf.thumbqid = oldthumbqid;
+			}
+			else
+				tmp = hd oldfiles :: tmp;
+		}
+		oldfiles = tmp;
+		# sys->print("len oldfiles: %d\n",len oldfiles);
+		if (found)
+			updateintree(tree, name, i);
+		else
+			addtotree(tree, name, i);
+		
+	}
+	for (; oldfiles != nil; oldfiles = tl oldfiles) {
+		(oldname, oldqid, oldthumbqid) := hd oldfiles;
+		# sys->print("remove from tree: %s\n",oldname);
+		tree.remove(big oldqid);
+		tree.remove(big oldthumbqid);
+	}
+}
+
+updateintree(tree: ref Nametree->Tree, name: string, i: int)
+{
+	# sys->print("update tree: %s\n",name);
+	tree.wstat(filelist[i].qid.path, 
+			dir(name, 
+				8r444,
+				filelist[i].cf.filelength,
+				int filelist[i].qid.path));
+	tree.wstat(big filelist[i].cf.thumbqid,
+			dir(jpg2bit(name),
+				8r444,
+				13020,
+				filelist[i].cf.thumbqid));
+}
+
+addtotree(tree: ref Nametree->Tree, name: string, i: int)
+{
+	# sys->print("addtotree: %s\n",name);
+	nextjpgqid += 1<<4;
+	filelist[i].qid = (big nextjpgqid, 0, sys->QTFILE);
+	parentqid := Qjpgdir;
+	tree.create(big parentqid,
+				dir(name,
+				8r444,
+				filelist[i].cf.filelength,
+				nextjpgqid));
+
+	nexttmbqid += 1<<4;
+	filelist[i].cf.thumbqid = nexttmbqid;
+	tree.create(big Qthumbdir,
+			dir(jpg2bit(name),
+			8r444,
+			13020,
+			nexttmbqid));
+}
+
+keepalive(alivechan: chan of int)
+{	
+	alivechan <-= sys->pctl(0,nil);
+	for (;;) {
+		sys->sleep(300000);
+		now := daytime->now();
+		print(sys->sprint("Alive: %d idle seconds\n",now-wait),6);
+		if (now < wait)
+			wait = now - 300;
+		if (now - wait >= 300)
+			alivechan <-= 1;
+	}
+}
+
+reconnect(n: int): int
+{
+	attempt := 0;
+	connected = 0;
+	delay := 100;
+	to5 := 0;
+	for (;;) {
+		print(sys->sprint( "Attempting to reconnect (attempt %d)\n",++attempt),2);
+		sys->sleep(100);
+		(C.fd, C.ctlfd, nil) = serialport(C.port_num);
+		if (C.fd == nil || C.ctlfd == nil)
+			print(sys->sprint("Could not open serial port\n"),3);
+		else if (connect() == 0) {
+			set_interface_timeout();
+			connected = 1;
+			print("Reconnected!\n",2);
+			break;
+		}
+		if (n != -1 && attempt >= n)
+			break;
+		if (++to5 >= 5) {
+			delay  *= 2;
+			to5 = 0;
+			if (delay > 600000)
+				delay = 600000;
+		}
+		sys->sleep(delay);
+	}
+	recon = 0;
+	return connected;
+}
+
+# 1: errors
+# 2: connection
+# 3: main procs
+
+print(s: string, v: int)
+{
+	if (s != nil && s[len s - 1] == '\n')
+		s = s[:len s - 1];
+	if (v <= verbosity)
+		sys->fprint(sys->fildes(2), "%s (%s)\n",s,camname);
+}
+
+readinterface(qid : int, offset: big, size: int): (ref sys->Dir, array of byte)
+{
+	i := qid >> 4;
+	buf := array[size] of byte;
+	fd := sys->open(interfacepaths[i], sys->OREAD);
+	if (fd == nil)
+		return (nil,nil);
+	(n, dir) := sys->fstat(fd);
+	if (offset >= dir.length)
+		return (nil,nil);
+	sys->seek(fd, offset, sys->SEEKSTART);
+	i = sys->read(fd,buf,size);
+	return (ref dir, buf[:i]);
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[8192] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
binary files /dev/null b/appl/demo/camera/camload.bit differ
binary files /dev/null b/appl/demo/camera/camproc.bit differ
--- /dev/null
+++ b/appl/demo/camera/mkfile
@@ -1,0 +1,42 @@
+<../../../mkconfig
+
+TARG=\
+		camera.dis\
+		camload.bit\
+		camproc.bit\
+		tkinterface.dis\
+
+SHTARG=\
+		runcam.sh\
+
+MODULES=\
+
+SYSMODULES= \
+	arg.m\
+	bufio.m\
+	daytime.m\
+	draw.m\
+	grid/readjpg.m\
+	readdir.m\
+	selectfile.m\
+	string.m\
+	styx.m\
+	styxservers.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+
+DISBIN=$ROOT/dis/demo/camera
+
+<$ROOT/mkfiles/mkdis
+
+$DISBIN/%.bit:		%.bit
+	rm -f $target && cp $stem.bit $target
+
+SHFILES=${SHTARG:%.sh=$DISBIN/%}
+install:V:	$SHFILES
+%.install:V:	$DISBIN/%
+%.installall:V:	$DISBIN/%
+
+$DISBIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/demo/camera/runcam.sh
@@ -1,0 +1,3 @@
+#!/dis/sh
+
+grid/register -a resource Camera -a name Y2K -a model 'Kodak DC260' -a 'Image resource' 1 {demo/camera/camera.dis -v 2 -p 0 -n Y2K -f /dis/demo/camera/tkinterface.dis -f /dis/demo/camera/readjpg.dis -f /dis/demo/camera/camload.bit -f /dis/demo/camera/camproc.bit}
--- /dev/null
+++ b/appl/demo/camera/runcamlocal.sh
@@ -1,0 +1,3 @@
+#!/dis/sh
+
+mount -A {demo/camera/camera.dis -v 2 -p 0} /n/remote
--- /dev/null
+++ b/appl/demo/camera/tkinterface.b
@@ -1,0 +1,2508 @@
+# Sort out timing with taking photo & getting jpg/thumbnail - make sure it gets the right one when 2photos have been taken & sort out 'cannot communicate with camera' error
+
+implement tkinterface;
+
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "sys.m";
+	sys : Sys;
+include "daytime.m";
+	daytime: Daytime;
+include "readdir.m";
+	readdir: Readdir;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include "string.m";
+	str : String;
+include "draw.m";
+	draw: Draw;
+	Context, Display, Point, Rect, Image, Screen, Font: import draw;
+include "grid/readjpg.m";
+	readjpg: Readjpg;
+
+display : ref draw->Display;
+context : ref draw->Context;
+camerapath := "";
+savepath := "";
+tmppath := "/tmp/";
+usecache := 1;
+working := 0;
+processing := 0;
+coords: draw->Rect;
+DONE : con 1;
+KILLED : con 2;
+font: ref Draw->Font;
+tkfont := "";
+tkfontb := "";
+tkfontf := "";
+ssize := 3;
+maxsize : Point;
+nilrect := Draw->Rect((0,0),(0,0));
+runwithoutcam := 0;
+toplevels : list of (ref Tk->Toplevel, string, list of int, int) = nil;
+procimg : ref Draw->Image;
+loadimg: ref Draw->Image;
+
+tkinterface : module {
+	init : fn (ctxt : ref Draw->Context, argv : list of string);
+};
+
+init(ctxt : ref Draw->Context, argv : list of string)
+{
+	display = ctxt.display;
+	context = ctxt;
+
+	sys = load Sys Sys->PATH;
+#	sys->pctl(Sys->NEWPGRP, nil);
+#	sys->pctl(Sys->FORKNS, nil);
+
+	str = load String String->PATH;
+	readdir = load Readdir Readdir->PATH;
+	daytime = load Daytime Daytime->PATH;
+	bufio = load Bufio Bufio->PATH;
+	
+	str = load String String->PATH;
+	draw = load Draw Draw->PATH;
+
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	tkclient->init();
+	selectfile = load Selectfile Selectfile->PATH;
+	selectfile->init();
+	readjpg = load Readjpg Readjpg->PATH;
+	readjpg->init(display);
+	font = draw->Font.open(display, "/fonts/charon/plain.small.font");
+	runfrom := hd argv;
+	p := isat2(runfrom,"/");
+	savepath = runfrom[:p+1];
+	argv = tl argv;
+	while (argv != nil) {
+		if (camerapath == "" && (hd argv)[0] == '/') camerapath = hd argv;
+		if (hd argv == "nocache") usecache = 0;
+		argv = tl argv;
+	}
+	if (camerapath == "")
+		camerapath = "./";
+	if (camerapath != "" && camerapath[len camerapath - 1] != '/')
+		camerapath[len camerapath] = '/';
+
+	r := display.image.r;
+#	if (r.dx() < 800 || r.dy() < 600) ssize = 2;
+	if (r.dx() < 400 || r.dy() < 300) ssize = 1;
+	maxsize = (r.dx(), r.dy());
+
+	if (ssize == 1) {
+		tkfont = "/fonts/charon/plain.tiny.font";
+		tkfontb = "/fonts/charon/bold.tiny.font";
+		tkfontf = "/fonts/pelm/unicode.8.font";
+	}
+	else if (ssize == 2) {
+		tkfont = "/fonts/charon/plain.small.font";
+		tkfontb = "/fonts/charon/bold.small.font";
+		tkfontf = "/fonts/pelm/unicode.8.font";
+	}
+	else {
+		tkfont = "/fonts/charon/plain.normal.font";
+		tkfontb = "/fonts/charon/bold.normal.font";
+		tkfontf = "/fonts/pelm/unicode.8.font";
+	}
+	if ((sys->stat(tkfont)).t0 == -1)
+		tkfont = "";
+	else tkfont = " -font " + tkfont;
+	if ((sys->stat(tkfontb)).t0 == -1)
+		tkfontb = "";
+	else tkfontb = " -font " + tkfontb;
+	if ((sys->stat(tkfontf)).t0 == -1)
+		tkfontf = "";
+	else tkfontf = " -font " + tkfontf;
+
+	procimg = display.open("camproc.bit");
+	loadimg = display.open("camload.bit");
+
+	spawn tkstuff();
+}
+
+# Tk stuff
+
+thumbscr := array[] of {
+	"frame .f",
+	"frame .fthumb -bg white",
+	"frame .f.finfo",
+	"frame .f.fsnap",
+	"menubutton .f.fsnap.fsettings.mb2 -text {Selected\n(0 files)} -menu .m2 @",
+	"menu .m2 @",
+	".m2 add command -text {Select All} -command {send butchan selectall 1}",
+	".m2 add command -text {Select None} -command {send butchan selectall 0}",
+	".m2 add command -text {Invert Selection} -command {send butchan invert}",
+	".m2 add command -text {Refresh Files} -command {send butchan refresh}",
+	"menu .m @",
+
+	"frame .f.fsnap.fsettings -borderwidth 1 -relief raised",
+	"menubutton .f.fsnap.fsettings.mb -text {Settings} -menu .m &",
+	"button .f.fsnap.fsettings.b -text {Information} -command {send butchan info} &",
+	"grid .f.fsnap.fsettings.b -row 0 -column 0 -sticky ew",
+	"grid .f.fsnap.fsettings.mb -row 1 -column 0 -sticky ew",
+	"grid .f.fsnap.fsettings.mb2 -row 2 -column 0 -sticky ew",
+
+	"frame .f.fsnap.fstore -borderwidth 1 -relief raised",
+	"label .f.fsnap.fstore.l1 -text {  Photos taken: } @",
+	"label .f.fsnap.fstore.l2 -text {  Remaining: } @",
+	"label .f.fsnap.fstore.l3 -text {  } @",
+	"label .f.fsnap.fstore.l4 -text {  } @",
+	"grid .f.fsnap.fstore.l1 -row 0 -column 0 -sticky w",
+	"grid .f.fsnap.fstore.l2 -row 1 -column 0 -sticky w",
+	"grid .f.fsnap.fstore.l3 -row 0 -column 1 -sticky w",
+	"grid .f.fsnap.fstore.l4 -row 1 -column 1 -sticky w",
+
+	"frame .f.fsnap.ftime -borderwidth 1 -relief raised",
+	"label .f.fsnap.ftime.l1 -text {Local: } @",
+	"label .f.fsnap.ftime.l2 -text {Camera: } @",
+	"label .f.fsnap.ftime.l3",
+	"label .f.fsnap.ftime.l4",
+	"checkbutton .f.fsnap.ftime.cb -text {Set camera to local time} -variable time &",
+	"button .f.fsnap.ftime.b -text {refresh} -command {send butchan gettime} &",
+	"grid .f.fsnap.ftime.l1 -row 0 -column 0 -sticky w",
+	"grid .f.fsnap.ftime.l2 -row 1 -column 0 -sticky w",
+	"grid .f.fsnap.ftime.l3 -row 0 -column 1 -sticky w",
+	"grid .f.fsnap.ftime.l4 -row 1 -column 1 -sticky w",
+	"grid .f.fsnap.ftime.cb -row 2 -column 0 -columnspan 2",
+	"grid .f.fsnap.ftime.b -row 3 -column 0 -columnspan 2",
+
+	"button .f.fsnap.b -text {Take Photo} -command {send butchan snap} &",
+	"grid columnconfigure .f.fsnap 2 -minsize 150",
+	"frame .f.fcom",
+	"frame .f.f1 -background #0d0d0d1a",
+	"canvas .f.f1.c1 -yscrollcommand {.f.f1.sb1 set} -height 255 -width 542 -bg white",
+	".f.f1.c1 create window 0 0 -window .fthumb -anchor nw",
+	"scrollbar .f.f1.sb1 -command {.f.f1.c1 yview}",
+
+#	"frame .f.f2",
+#	"canvas .f.f2.c1 -width 556 -height 304",
+#	".f.f2.c1 create window 0 0 -window .f.fsnap -anchor nw",
+
+	"grid .f.fsnap -column 0 -row 0",
+	"grid .f.f1 -column 0 -row 1",
+	"grid .f.f1.c1 -column 0 -row 0",
+	"grid .f.f1.sb1 -column 1 -row 0 -sticky ns",
+#	"grid .f.f2 -column 0 -row 0",
+#	"grid .f.f2.c1 -column 0 -row 0 -sticky ew",
+	"bind .Wm_t <ButtonPress-1> +{focus .}",
+	"bind .Wm_t.title <ButtonPress-1> +{focus .}",
+};
+
+lastpath := "";
+
+Aitem: adt {
+	pname,desc: string;
+	dtype,factory: int;
+	read, location: string;
+	data: list of (string, int);
+};
+LIST: con 0;
+MINMAX: con 1;
+OTHER: con 2;
+
+noabilities := 0;	
+abilities : array of Aitem;
+
+getdesc(l : list of string): list of string
+{
+	s := "";
+	while(hd l != "min" && hd l != "items" && tl l != nil) {
+		s += hd l + " ";
+		l = tl l;
+	}
+	while (s[len s - 1] == ' ' || s[len s - 1] == '\n')
+		s = s[:len s -1];
+	l = s :: l;
+	return l;
+}
+
+inflist : list of (string, string);
+ablmenu : array of string;
+
+getabilities()
+{
+	inflist = nil;
+	abilities = array[200] of Aitem;	
+	fd := bufio->open(camerapath+"abilities", bufio->OREAD);
+	if (runwithoutcam)
+		fd = bufio->open("/usr/danny/camera/abls", bufio->OREAD);
+	i := 0;
+	for (;;) {
+		take := 0;
+		s := fd.gets('\n');
+		if (s == "") break;
+		(n, lst) := sys->tokenize(s," ,:\t\n");
+		abilities[i].data = nil;
+		abilities[i].read = "";
+		if (lst != nil && len hd lst == 4) {
+			abilities[i].pname = hd lst;
+			lst = getdesc(tl lst);
+			abilities[i].desc = hd lst;
+			if (hd tl lst == "items") {
+				abilities[i].dtype = LIST;
+				abilities[i].factory = int hd tl tl tl tl lst;
+				noitems := int hd tl tl lst;
+				for (k := 0; k < noitems; k++) {
+					s = fd.gets('\n');
+					(n2, lst2) := sys->tokenize(s,",:\t\n");
+					name := hd lst2;
+					val := int hd tl lst2;
+					if (k == 0) {
+						if (abilities[i].pname == "ssiz")
+							abilities[i].factory = val;
+						else if (abilities[i].pname == "scpn")
+							abilities[i].factory = val;
+					}	
+					if (val == abilities[i].factory && noitems > 1) name += " *";
+					abilities[i].data = (name, val) :: abilities[i].data;
+				}
+				if (noitems < 2) {
+					inflist = (abilities[i].desc, (hd abilities[i].data).t0) :: inflist;
+					take = 1;
+				}
+			}
+			else if (hd tl lst == "min") {
+				abilities[i].dtype = MINMAX;
+				abilities[i].factory = int hd tl tl tl tl tl tl lst;
+				min := int hd tl tl lst;
+				max := int hd tl tl tl tl lst;
+				mul := 1;
+				while (max > 200000) {
+					min /= 10;
+					max /= 10;
+					mul *= 10;
+				}
+				abilities[i].data = ("min", min) :: abilities[i].data;
+				abilities[i].data = ("max", max) :: abilities[i].data;
+				abilities[i].data = ("mul", mul) :: abilities[i].data;
+			}
+			else {
+				inflist = (abilities[i].desc,list2string(tl lst)) :: inflist;
+				take = 1;
+			}
+			if (take || 
+				abilities[i].desc == "Time Format" ||
+				abilities[i].desc == "Date Format" ||
+				abilities[i].desc == "File Type" ||
+				contains(abilities[i].desc,"Video") ||
+				contains(abilities[i].desc,"Media") ||
+				contains(abilities[i].desc,"Sound") ||
+				contains(abilities[i].desc,"Volume") ||
+				contains(abilities[i].desc,"Slide") ||
+				contains(abilities[i].desc,"Timelapse") ||
+				contains(abilities[i].desc,"Burst") ||
+				contains(abilities[i].desc,"Power") ||
+				contains(abilities[i].desc,"Sleep"))
+					i--;
+			i++;
+		}	
+	}
+	noabilities = i;
+}
+
+isat(s: string, test: string): int
+{
+	num := -1;
+	if (len test > len s) return -1;
+	for (i := 0; i < (1 + (len s) - (len test)); i++) {
+		if (num == -1 && test == s[i:i+len test]) num = i;
+	}
+	return num;
+}
+
+isat2(s: string, test: string): int
+{
+	num := -1;
+	if (len test > len s) return -1;
+	for (i := len s - len test; i >= 0; i--) {
+		if (num == -1 && test == s[i:i+len test]) num = i;
+	}
+	return num;
+}
+
+
+nomatches(s: string): int
+{
+	n := 0;
+	for (i := 0; i < noabilities; i++) {
+		test := abilities[i].desc;
+		if (len s <= len test && test[:len s] == s) n++;
+	}
+	return n;
+}
+
+matches(s1,s2: string): int
+{
+	if (len s1 < len s2) return 0;
+	if (s1[:len s2] == s2) return 1;
+	return 0;
+}
+
+biggestmatch(nm: int, s: string, l: int): string
+{
+	bigmatch := s;
+	match := s[:l];
+	for (;;) {
+		if (bigmatch == match) break;
+		if (nomatches(bigmatch) == nm) return bigmatch;
+		p := isat2(bigmatch," ");
+		if (p < len match) break;
+		bigmatch = bigmatch[:p];
+	}
+	return match;
+}
+
+getabllist(): array of string
+{
+	los : list of string;
+	los = nil;
+	for (i := 0; i < noabilities; i++) {
+		p := 0;
+		p2 := 0;
+		nm : int;
+		for (;;) {
+			nm = -1;
+			tmpl := los;
+			while (tmpl != nil) {
+				if (matches(abilities[i].desc, hd tmpl)) nm = 0;
+				tmpl = tl tmpl;
+			}
+			if (nm == 0) break;
+			p += p2;
+			tmp := abilities[i].desc[p:];
+			p2 = isat(tmp, " ");
+			if (p2 == -1) p2 = len tmp;
+			else p2++;
+			nm = nomatches(abilities[i].desc[:p+p2]);
+			if (nm <= 5) break;
+		}
+		if (nm > 0) {
+			listitem := biggestmatch(nm, abilities[i].desc,p+p2);
+			los = listitem :: los;
+		}
+	}
+	ar := array[len los] of string;
+	for (i = len ar - 1; i >= 0; i--) {
+		ar[i] = hd los;
+		los = tl los;
+	}
+	return ar;
+}
+
+buildabilitiesframes(top: ref Tk->Toplevel)
+{
+	ablmenu = getabllist();
+	tkcmd(top, ".m add command -text {Refresh Main Screen} -command {send butchan refreshstate}");
+	tkcmd(top, ".m add command -text {Reset Camera} -command {send butchan reset}");
+	for (k := 0; k < len ablmenu; k++) {
+		if (len ablmenu[k] > 4 && (ablmenu[k][:4] == "Zoom" || ablmenu[k][:5] == "Still")) 
+ 			buildabilitiesframe(top,k,"butchan");
+		else
+			tkcmd(top, ".m add command -text {"+ablmenu[k]+
+				"} -command {send butchan abls "+string k+"}");
+	}
+	tkcmd(top, "menu .mthumb "+tkfont);
+	tkcmd(top, ".mthumb add command -label {Selection (88 files)}");
+	tkcmd(top, ".mthumb add separator");
+	for (k = nothumbs; k < len menu; k++)
+		 tkcmd(top, ".mthumb add command -text {"+menu[k].text+"} " +
+				"-command {send butchan}");
+
+}
+
+buildabilitiesframe(top: ref Tk->Toplevel,k: int, chanout: string)
+{
+	nm := string nomatches(ablmenu[k]);
+	count2 := 0;
+	for (i := 0; i < noabilities; i++) {
+		if (matches(abilities[i].desc,ablmenu[k])) {
+
+			frame : string;
+			case abilities[i].pname {
+				"scpn" or "ssiz" or "zpos" =>
+					frame = ".f.fsnap.f"+abilities[i].pname;
+					tkcmd(top, "frame "+frame+" -borderwidth 1 -relief raised");
+				* =>
+					frame = ".f";
+					if (count2 == 0)  { 
+						tkcmd(top, "frame "+frame);
+						tkcmd(top, "label "+frame+".l -text {"+ablmenu[k]+"}"+tkfontb);
+						tkcmd(top, "grid "+frame+".l -row 0 -column 0 -columnspan "+nm);
+					}
+					frame = frame + ".f"+string count2;
+					tkcmd(top, "frame "+frame+" -borderwidth 1 -relief raised");
+					tkcmd(top, "grid "+frame+" -row 1 -column "+string count2+ " -sticky nsew");
+					mul := getval(abilities[i].data,"mul");
+					s := abilities[i].desc[len ablmenu[k]:];
+					if (mul != 1 && abilities[i].dtype == MINMAX)
+						s += " (x"+string mul+")";
+					tkcmd(top, "label "+frame+".l -text {"+s+"}"+tkfont);
+					tkcmd(top, "grid "+frame+".l -row 0 -column 0 -sticky nw");
+			}
+				
+			if (abilities[i].dtype == MINMAX) {
+				abilities[i].location = frame+".sc";
+				min := getval(abilities[i].data,"min");
+				max := getval(abilities[i].data,"max");
+				tkcmd(top, sys->sprint("scale %s.sc -to %d -from %d %s", frame,min,max,tkfont));
+				tkcmd(top, "bind "+frame+".sc <ButtonPress-3> {send " +
+					chanout + " scaleval " + string i + " %X %Y}");
+				tkcmd(top, "grid "+frame+".sc -row 1 -column 0");		
+			}
+			else if (abilities[i].dtype == LIST) {
+				tkcmd(top, "frame "+frame+".frb");
+				tkcmd(top, "grid "+frame+".frb -row 1 -column 0");
+				tmp := abilities[i].data;
+				row := 0;
+				while (tmp != nil) {
+					(name, val) := hd tmp;
+					s := sys->sprint("radiobutton %s.frb.rb%d -text {%s} -value %d -variable %s  -height %d %s",frame,row,name,val,abilities[i].pname,24 - (3*(3-ssize)), tkfont);
+					tkcmd(top,s);
+					tkcmd(top, sys->sprint("grid %s.frb.rb%d -row %d -column 0 -sticky w",
+						frame,row,row));
+					tmp = tl tmp;
+					row++;
+				}
+			}
+			tkcmd(top, "button "+frame+".bs -text {Set} -command "+
+				"{send "+chanout+" set "+string i+"}"+butheight+tkfont);
+			tkcmd(top, "grid "+frame+".bs -row 2 -column 0 -sticky ew");
+			if (abilities[i].dtype == MINMAX) {
+				tkcmd(top, "button "+frame+".bf -text {Default} -command "+
+					"{send "+chanout+" setdef "+string i+"}"+butheight+tkfont);
+				tkcmd(top, "grid "+frame+".bf -row 3 -column 0 -sticky ew");
+			}
+			count2++;
+		}
+	}
+}
+
+getvaluescr := array[] of {
+	"frame .f -height 84 -width 114 -borderwidth 2 -relief raised",
+	"label .f.l1 -text {Enter Value:} @",
+	"entry .f.e1 -width 100 -bg white @",
+	"button .f.b1 -text { ok } -command {send chanin ok} &",
+	"button .f.b2 -text cancel -command {send chanin cancel} &",
+	"grid .f.l1 -column 1 -row 0 -columnspan 2 -padx 0 -sticky w",
+	"grid .f.e1 -column 1 -row 1 -columnspan 2 -padx 0 -pady 5",
+	"grid .f.b1 -column 1 -row 2 -padx 0",
+	"grid .f.b2 -column 2 -row 2 -padx 0",
+	"grid columnconfigure .f 1 -minsize 20",
+	"grid columnconfigure .f 2 -minsize 20",
+	"grid columnconfigure .f 3 -minsize 5",
+	"grid rowconfigure .f 0 -minsize 20",
+	"grid rowconfigure .f 1 -minsize 20",
+	"grid rowconfigure .f 2 -minsize 20",
+	"grid columnconfigure .f 0 -minsize 5",
+	"bind .f.e1 <Key> {send chanin key %s}",
+	"focus .f.e1",
+	"pack .f",
+	"update",
+};
+
+getvaluescreen(x,y: string): int
+{
+	x = string ((int x) - 55);
+	y = string ((int y) - 30);
+	(top, nil) := tkclient->toplevel(context, "-x "+x+" -y "+y, nil, tkclient->Plain);
+	chanin := chan of string;
+	tk->namechan(top, chanin, "chanin");
+	for (tk1 := 0; tk1 < len getvaluescr; tk1++)
+		tkcmd(top, getvaluescr[tk1]);
+	tkclient->onscreen(top, "exact");
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	for(;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <- chanin =>
+			if (inp == "ok") return int tkcmd(top, ".f.e1 get");
+			else if (inp == "cancel") return -1;
+			else if (inp[:3] == "key") {
+				s := " ";
+				s[0] = int inp[4:];
+				if (s[0] == '\n') return int tkcmd(top, ".f.e1 get");
+				if (s[0] >= '0' && s[0] <= '9') {
+					tkcmd(top, ".f.e1 delete sel.first sel.last");
+					tkcmd(top, ".f.e1 insert insert {"+s+"}; update");
+				}
+			}
+		}	
+	}
+}
+
+infoscreen()
+{
+	(top, titlebar) := tkclient->toplevel(context, "", "Information", Tkclient->Hide);
+	tmp := inflist;
+	tkcmd(top, "frame .f");
+	tkcmd(top, "label .f.l -text {Information}");
+	tkcmd(top, "grid .f.l -row 0 -column 0 -columnspan 2");
+	tkcmd(top, "frame .f.finfo -borderwidth 1 -relief raised");
+	tkcmd(top, "grid .f.finfo");
+	infrow := 0;
+	while (tmp != nil) {
+		infrow++;
+		s := string infrow;
+		(d1,d2) := hd tmp;
+		tkcmd(top, "label .f.finfo.l"+s+"1 -text {"+d1+"}");
+		tkcmd(top, "label .f.finfo.l"+s+"2 -text {"+d2+"}");
+		tkcmd(top, "grid .f.finfo.l"+s+"1 -row "+s+" -column 0 -sticky w");
+		tkcmd(top, "grid .f.finfo.l"+s+"2 -row "+s+" -column 1 -sticky e");
+		tmp = tl tmp;
+	}
+	tkcmd(top, "pack .f; update");
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	main: for(;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		title := <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <- titlebar =>
+			if (title == "exit") break main;
+			tkclient->wmctl(top, title);
+		}
+	}
+}
+
+settingsscreen(k: int, ctlchan: chan of int)
+{
+	low := toplevels;
+	for (;low != nil; low = tl low) {
+		(tplvl, name, nil,nil) := hd low;
+		if (name == ablmenu[k]) {
+			tkcmd(tplvl, "raise .; focus .; update");
+			ctlchan <-= DONE;
+			return;
+		}
+	}
+	pid := sys->pctl(0, nil);
+	(top, titlebar) := tkclient->toplevel(context, "", "Config", Tkclient->Appl);
+	chanin := chan of string;
+	tk->namechan(top,chanin, "chanin");
+	buildabilitiesframe(top,k, "chanin");
+	tkcmd(top,"bind .Wm_t <ButtonPress-1> +{focus .}");
+	tkcmd(top,"bind .Wm_t.title <ButtonPress-1> +{focus .}");
+	tkcmd(top, "pack .f; update");
+	err := 0;
+	allread := 1;
+	l : list of int = nil;
+	for (i := 0; i < noabilities; i++) {
+		if (matches(abilities[i].desc, ablmenu[k])) {
+			l = i :: l;
+			if (abilities[i].read != "")
+				setmystate(top,i,abilities[i].read);
+			else
+				allread = 0;
+		}
+	}
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	if (!allread) {
+		spawn workingscreen2(getcoords(top),pid, ctlchan,0);
+		ltmp := l;
+		for (;ltmp != nil; ltmp = tl ltmp) {
+			if (abilities[hd ltmp].read == "" && getstate(top, hd ltmp) == -1) {
+				err = 1;
+				break;
+			}
+		}
+	}
+	if (!err)
+		spawn settingsloop(top,chanin,titlebar,k,l);
+	ctlchan <-= DONE;
+}
+
+settingsloop(top: ref Tk->Toplevel, chanin,titlebar: chan of string, k: int, abls: list of int)
+{
+	tkcmd(top, "focus .Wm_t");
+	pid := sys->pctl(0,nil);
+	addtoplevel(top,ablmenu[k], abls, pid);
+	ctlchan := chan of int;
+	main: for(;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <- chanin =>
+			tkcmd(top, "focus .");
+			(n, lst) := sys->tokenize(inp, " \t\n");
+			case hd lst {
+				"scaleval" =>
+					i := int hd tl lst;
+					val := getvaluescreen(hd tl tl lst, hd tl tl tl lst);
+					if (val != -1) tkcmd(top, abilities[i].location+" set "+string val+";update");
+				"set" or "setdef" =>
+					if (working)
+						dialog(" Camera is busy! ", 2,-1,getcoords(top));
+					else {
+						spawn set(top, int hd tl lst, hd lst, ctlchan);
+						<-ctlchan;
+						working = 0;
+					}
+			}
+			clearbuffer(chanin);
+		title := <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <- titlebar =>
+			if (title == "exit") break main;
+			tkclient->wmctl(top, title);
+		}
+	}
+	deltoplevel(top);
+}
+
+clearbuffer(c: chan of string)
+{
+	tc := chan of int;
+	spawn timer(tc);
+	main: for (;;) alt {
+		del := <-c => ;
+		tick := <-tc =>		
+			break main;
+	}	
+}
+
+timer(tick: chan of int)
+{
+	sys->sleep(100);
+	tick <- = 1;
+}
+
+getval(l: list of (string,int), s: string): int
+{
+	while (l != nil) {
+		(name,val) := hd l;
+		if (name == s) return val;
+		l = tl l;
+	}
+	return -2;
+}
+
+list2string(l : list of string): string
+{
+	s := "";
+	while (l != nil) {
+		s += " " + hd l;
+		l = tl l;
+	}
+	if (s != "") return s[1:];
+	return s;
+}
+
+JPG: con 0;
+THUMB: con 1;
+
+Imgloaded: adt {
+	name: string;
+	imgtype: int;
+};
+
+nofiles := 0;
+filelist := array[200] of string;
+thumbimg := array[200] of ref draw->Image;
+selected := array[200] of { * => 0 };
+noselected := 0;
+fnew : list of int;
+imgloaded :  list of Imgloaded;
+maxwidth, maxheight: int;
+nothumbs := 0;
+
+nocamera(): int
+{
+	(n,dir) := sys->stat(camerapath+"ctl");
+	if (n != -1) return 0;
+	return 1;
+}
+
+startuptkstuff(top: ref Tk->Toplevel, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(coords,pid, ctlchan,1);
+	getabilities();
+	(dirs,n) := readdir->init(camerapath+"thumb", readdir->NAME);
+	if (n == -1) nothumbs = 1;
+	buildabilitiesframes(top);
+	refreshfilelist(top,0);
+	ctlchan <-= DONE;
+}
+
+tibuild := 0;
+butheight := "";
+
+tkstuff()
+{
+	if (!runwithoutcam && nocamera()) {
+		dialog("Cannot find camera!",0,-1,nilrect);
+		exit;
+	}
+	(win, titlebar) := tkclient->toplevel(context, "", "Camera", Tkclient->Appl);
+	tkcmd(win, "frame .test");
+	if (tkcmd(win, ".test cget -bg") == "#ffffffff")
+		tibuild = 1;
+	tkcmd(win, "destroy .test");
+	butheight = " -height "+string (16 + (5*tibuild) - (3*(3-ssize)));
+	butchan := chan of string;
+	tk->namechan(win, butchan, "butchan");
+	for (tk1 := 0; tk1 < len thumbscr; tk1++)
+		tkcmd(win, thumbscr[tk1]);
+	coords = display.image.r;
+	ctlchan := chan of int;
+	imgloaded = nil;
+
+	spawn startuptkstuff(win, ctlchan);
+	e := <- ctlchan;
+	if (e == KILLED) {
+		dialog("Cancel during load!",0,-1,coords);
+		exit;
+	}
+	working = 0;
+	spawn mainscreen(win, 1, ctlchan);
+	<- ctlchan;
+	working = 0;
+
+	processing = 0;	
+	tkcmd(win, "pack propagate . 0");
+	resizemain(win,1);
+	tkcmd(win, "pack .f; update; focus .");
+	coords = getcoords(win);
+	loadimg = nil;
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	main: for (;;) {
+		alt {
+		s := <-win.ctxt.kbd =>
+			tk->keyboard(win, s);
+		s := <-win.ctxt.ptr =>
+			tk->pointer(win, *s);
+		inp := <-butchan =>
+			tkcmd(win, "focus .");
+			(n, lst) := sys->tokenize(inp, "\t\n ");
+			case hd lst {
+
+				# Communicates internally
+
+				"scaleval" =>
+					i := int hd tl lst;
+					val := getvaluescreen(hd tl tl lst, hd tl tl tl lst);
+					if (val != -1) tkcmd(win, abilities[i].location+" set "+string val);
+				"info" =>
+					spawn infoscreen();
+				"unload" =>
+					i := int hd tl lst;
+					for (k := 0; k < nofiles; k++) {
+						if (i == k || (i == -1 && selected[k])) {
+							delloaded(filelist[k],JPG);
+							delloaded(filelist[k],THUMB);
+						}
+					}
+				"invert" =>
+					nf := 0;
+					for (i := 0; i < nofiles; i++)
+						selected[i] = 1 - selected[i];
+					doselect(win);
+				"selectall" =>
+					val := int hd tl lst;
+					for (i := 0; i < nofiles; i++)
+						selected[i] = val;
+					doselect(win);
+				"select" =>
+					i := int hd tl lst;
+					selected[i] = 1 - selected[i];
+					doselect(win);
+				"selectonly" =>
+					i := int hd tl lst;
+					val := selected[i];
+					for (k := 0; k < nofiles; k++)
+						selected[k] = 0;
+					if (noselected - val == 0) selected[i] = 1 - val;
+					else selected[i] = 1;
+					doselect(win);
+				"menu" =>
+					i := int hd tl lst;
+					if (selected[i] && noselected > 1) i = -1;
+					title := "Selection ("+string noselected+" files)";
+					if (i != -1) title = filelist[i]+".jpg";
+					si := string i;
+						tkcmd(win, ".mthumb entryconfigure 0 -text {"+title+"}");
+					for (k := nothumbs; k < len menu; k++)
+						tkcmd(win, ".mthumb entryconfigure "+string (2+k-nothumbs)+
+							" -command {send butchan "+	menu[k].com+" "+si+"}"); 
+					tkcmd(win, ".mthumb post "+hd tl tl lst+" "+hd tl tl tl lst);
+				* =>
+					if (!processing) 
+						spawn dealwithcamera(win, lst);
+			}
+			tkcmd(win, "update");
+			clearbuffer(butchan);
+		title := <-win.ctxt.ctl or
+		title = <-win.wreq or
+		title = <-titlebar =>
+			if (title == "exit")
+				break main;
+			err := tkclient->wmctl(win, title);
+			if (err == nil && title == "!size") {
+				(n, lst) := sys->tokenize(title, " ");
+				if (hd tl lst == ".")
+					resizemain(win,0);
+			}
+			coords = getcoords(win);
+		}	
+	}
+	for (; toplevels != nil; toplevels = tl toplevels) {
+		(nil, nil, nil, pid) := hd toplevels;
+		if (pid != -1)
+			kill(pid);
+	}
+	while (imgloaded != nil) {
+		(fname, ftype) := hd imgloaded;
+		sys->remove(tmppath+fname+"."+string ftype+"~");
+		imgloaded = tl imgloaded;
+	}
+	tkcmd(win, "destroy .");
+	exit;
+}
+
+dealwithcamera(win: ref Tk->Toplevel, lst: list of string)
+{
+	ctlchan := chan of int;
+	processing = 1;
+	case hd lst {
+		"gettime" =>
+			spawn refreshtime(win, ctlchan);
+			<- ctlchan;
+		"show" =>
+			spawn loadthumb(win,int hd tl lst,ctlchan);
+			<- ctlchan;
+		"snap" =>
+			selected[nofiles+1] = 0;
+			spawn takephoto(win, ctlchan);
+			<- ctlchan;
+			working = 0;
+			if (fnew == nil)
+				break;
+			spawn waittilready(camerapath+"jpg/"+filelist[hd fnew]+".jpg", ctlchan);
+			e := <- ctlchan;
+			working = 0;
+			if (e == DONE) {
+				spawn loadnewthumb(win, ctlchan);
+			 	<- ctlchan;
+			 	working = 0;
+			}
+		"abls" =>
+			spawn settingsscreen(int hd tl lst, ctlchan);
+			<- ctlchan;
+		"set" or "setdef" =>
+			spawn set(win, int hd tl lst, hd lst, ctlchan);
+			<- ctlchan;
+		"del" =>
+			spawn delete(win, int hd tl lst, ctlchan);
+			<- ctlchan;
+		"view" =>
+			i := int hd tl lst;
+			unnew(win, i);
+			if (i == -1) multiview();
+			else vw(i);
+		"refresh" =>
+			spawn refresh(win, ctlchan);
+			<- ctlchan;
+		"refreshstate" =>
+			spawn mainscreen(win, 0, ctlchan);
+			<- ctlchan;
+		"dnld" =>
+			i := int hd tl lst;
+			unnew(win, i);
+			if (i == -1) multidownload();
+			else dnld(i, "");
+		"reset" =>
+			if (dialog("reset camera to default settings?",1,-1,coords)) {
+				spawn resetcam(win,1, ctlchan);
+				<- ctlchan;
+			}
+	}
+	processing = 0;
+	working = 0;
+}
+
+refresh(top: ref Tk->Toplevel, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(coords,pid, ctlchan,0);
+	refreshfilelist(top,1);
+	ctlchan <-= DONE;
+}
+
+delete(top: ref Tk->Toplevel, i: int, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	ok : int;
+	s := "";
+	loi : list of int;
+	loi = nil;
+	if (i == -1) {
+		for (k := 0; k < nofiles; k++)
+			if (selected[k]) s+= filelist[k]+".jpg\n";
+		if (!dialog("Delete Selected files?\n\n"+s,1,-1,coords)) {
+			ctlchan <-= DONE;
+			return;
+		}
+	}
+	else if (!dialog("Delete "+filelist[i]+".jpg?",1,i,coords)) {
+		ctlchan <-= DONE;
+		return;
+	}
+	spawn workingscreen2(coords,pid, ctlchan,0);
+	s = "";
+	for (k := 0; k < nofiles; k++) {
+		if ((i == -1 && selected[k]) || k == i) {
+			s += filelist[k]+".jpg ";
+			ok = sys->remove(camerapath+
+					"jpg/"+filelist[k]+".jpg");
+			if (ok == -1) s+="failed\n";
+			else {
+				s+="ok\n";
+				loi = k :: loi;
+			}
+		}
+	}
+	if (loi == nil && i != -1) {
+		dialog("cannot remove "+filelist[i]+".jpg?",0,i,coords);
+		ctlchan <-= DONE;
+		return;
+	}
+	while (loi != nil) {
+		delloaded(filelist[hd loi],JPG);
+		delloaded(filelist[hd loi],THUMB);
+		delselect(hd loi);
+		loi = tl loi;
+	}
+	refreshfilelist(top,0);
+	getstore(top);
+	if (i == -1) dialog("Files deleted:\n\n"+s,0,-1,coords);
+	ctlchan <-= DONE;
+}
+
+delselect(n: int)
+{
+	for (i := n; i < nofiles - 1; i++)
+		selected[i] = selected[i+1];
+	selected[nofiles - 1] = 0;
+}
+
+doselect(top: ref Tk->Toplevel)
+{
+	n := 0;
+	for (i := 0; i < nofiles; i++) {	
+		col := "white";
+		if (selected[i]) {
+			col = "blue";
+			n++;
+		}
+		tkcmd(top,".fthumb.p"+string i+" configure -bg "+col);
+	}
+	noselected = n;
+	s := " files";
+	if (n == 1) s = " file";
+	tkcmd(top, ".f.fsnap.fsettings.mb2 configure -text {Selected\n("+string n+s+")}");
+}
+
+takephoto(top: ref Tk->Toplevel, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(coords,pid, ctlchan,0);
+	fd := sys->open(camerapath+"ctl",sys->OWRITE);
+	if (fd != nil) {
+		e := sys->fprint(fd, "snap");
+		if (e < 0) {
+			dialog("Could not take photo",0,-1,coords);
+			getstore(top);
+		}
+		else {
+			getstore(top);
+			n := nofiles;
+			for (i := 0; i < 5; i++) {
+				refreshfilelist(top,1);
+				sys->sleep(1000);
+				if (nofiles > n)
+					break;
+			}
+		}
+	}
+	ctlchan <-= DONE;
+}
+
+unnew(top: ref Tk->Toplevel, i: int)
+{
+	if (fnew == nil)
+		return;
+	tmp : list of int = nil;
+	for (;fnew != nil; fnew = tl fnew) {
+		if (i == -1 && selected[hd fnew])
+			i = hd fnew;
+		if (hd fnew == i)
+			tkcmd(top, ".fthumb.mb"+string hd fnew+" configure -fg black; update");
+		else
+			tmp = hd fnew :: tmp;
+	}
+	fnew = tmp;
+}
+
+refreshtime(top: ref Tk->Toplevel, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(coords,pid, ctlchan,0);
+	if (!samedate(top) && tkcmd(top, "variable time") == "1") settime();
+	gettime(top);
+	ctlchan <-= DONE;
+}
+
+addtoplevel(top: ref Tk->Toplevel, name: string, abls: list of int, pid: int)
+{
+	ltmp := toplevels;
+	isin := 0;
+	for (;ltmp != nil; ltmp = tl ltmp) {
+		(tplvl, nil, nil, nil) := hd ltmp;
+		if (tplvl == top) isin = 1;
+	}
+	if (!isin)
+		toplevels = (top, name, abls, pid) :: toplevels;
+}
+
+deltoplevel(top: ref Tk->Toplevel)
+{
+	ltmp : list of (ref Tk->Toplevel, string, list of int, int) = nil;;
+	for (;toplevels != nil; toplevels = tl toplevels) {
+		(tplvl, nm, loi, p) := hd toplevels;
+		if (tplvl != top) 
+			ltmp = (tplvl, nm, loi, p) :: ltmp;
+	}
+	toplevels = ltmp;
+}
+
+resetcam(top: ref Tk->Toplevel, show: int, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(coords,pid, ctlchan,0);
+	for (i := 0; i < noabilities; i++)
+		setstate(i, string abilities[i].factory);
+	if (show) {
+		ltmp := toplevels;
+		for (;ltmp != nil; ltmp = tl ltmp) {
+			(tplvl, nm, loi, p) := hd ltmp;
+			for (; loi != nil; loi = tl loi)
+				setmystate(tplvl, hd loi, string abilities[hd loi].factory);
+		}
+		if (top != nil)
+			getstore(top);
+	}
+	ctlchan <-= DONE;
+}
+
+set(top: ref Tk->Toplevel, i: int, s: string, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(getcoords(top),pid, ctlchan,0);
+
+	val : string;
+	if (s == "setdef") {
+		val = string abilities[i].factory;
+		setmystate(top,i,val);
+	}
+	else {
+		if (abilities[i].dtype == MINMAX) {
+			val = tkcmd(top, abilities[i].location+" get");
+			mul := getval(abilities[i].data, "mul");
+			val = string (int val * mul);
+		}
+		else {
+			val = tkcmd(top, "variable "+abilities[i].pname);	
+		}
+	}
+
+	e := setstate(i,val);
+	if (e == 2) getstore(top);
+	else if (e == 0)
+		dialog("cannot communicate with camera",0,-1,coords);
+	ctlchan <-= DONE;
+}
+
+setstate(i: int, val: string): int
+{
+	fd := sys->open(camerapath+"ctl",sys->OWRITE);
+	if (fd != nil) {
+		sys->fprint(fd, "%s %s",abilities[i].pname,val);
+		abilities[i].read = val;
+		if (abilities[i].pname == "ssiz" || abilities[i].pname == "scpn") return 2;
+		return 1;
+	}
+	else return 0;
+}
+
+getfirst(s: string): string
+{
+	(n, lst) := sys->tokenize(s," \n\t");
+	if (lst == nil) return "";
+	return hd lst;
+}
+
+getabl(pname: string): int
+{
+	for (i := 0; i < noabilities; i++)
+		if (abilities[i].pname == pname) return i;
+	return -1;
+}
+
+getstate(top: ref Tk->Toplevel, i: int): int
+{
+	fd := sys->open(camerapath+"state", sys->OWRITE);
+	if (fd != nil) {
+		sys->fprint(fd ,"%s", abilities[i].pname);
+		sys->sleep(500);
+		fdi := bufio->open(camerapath+"state",sys->OREAD);
+		if (fdi != nil) {
+			s := fdi.gets('\n');
+			if (s != nil) {
+				(n,lst) := sys->tokenize(s,":\n");
+				val := hd tl lst;
+				setmystate(top,i,val);
+			}
+			return 0;
+		}
+	}
+	dialog("cannot communicate with camera",0,-1,coords);
+	return -1;
+}
+
+setmystate(top: ref Tk->Toplevel, i: int, val: string)
+{
+	abilities[i].read = val;
+	if (abilities[i].dtype == LIST)
+		tkcmd(top, "variable "+abilities[i].pname+" "+val);
+	else if (abilities[i].dtype == MINMAX) {
+		mul := getval(abilities[i].data, "mul");
+		tkcmd(top, abilities[i].location+" set "+string((int val)/mul));
+	}
+	tkcmd(top, "update");
+}
+
+max(a,b: int): int
+{
+	if (a > b) return a;
+	return b;
+}
+
+refreshfilelist(win: ref Tk->Toplevel, refresh: int): int
+{
+	if (refresh) {
+		fd := sys->open(camerapath+"ctl",sys->OWRITE);
+		if (fd == nil) {
+			dialog("cannot communicate with camera",0,-1,coords);
+			return -1;
+		}
+		else
+			sys->fprint(fd, "refresh");
+	}
+	oldlist := filelist[:nofiles];
+	for (i := 0; i < nofiles; i++) {
+		si := string i;
+		tk->cmd(win, "grid forget .fthumb.mb"+si+" .fthumb.p"+si);
+		tk->cmd(win, "destroy .fthumb.mb"+si+" .fthumb.p"+si+" .mthumb"+si);
+	}
+	(dirs,n) := readdir->init(camerapath+"jpg", readdir->NAME);
+	if (n == -1)
+		return -1;
+	nofiles = n;
+	row := 0;
+	col := 0;
+	nocols := -1;
+	w1 := int tkcmd(win, ".f.f1.c1 cget -width");
+	w := 0;
+	fnew = nil;
+	for (i = 0; i < nofiles; i++) {
+		filelist[i] = dirs[i].name;
+		if (len filelist[i] > 3 && filelist[i][len filelist[i] - 4] == '.')
+			filelist[i] = filelist[i][:len filelist[i]-4];
+		
+		isnew := 1;
+		for (k := 0; k < len oldlist; k++) {
+			if (filelist[i] == oldlist[k]) {
+				isnew = 0;
+				break;
+			}
+		}
+		si := string i;
+		tkcmd(win, "menubutton .fthumb.mb"+si+" -bg white " +
+			"-text {"+filelist[i]+".jpg} -menu .mthumb"+si+tkfontf);
+		if (isnew && refresh) {
+			fnew = i :: fnew;
+			tkcmd(win, ".fthumb.mb"+si+" configure -fg red");
+		}
+		thumbimg[i] = display.newimage(Rect((0,0),(90,90)),draw->RGB24,0,int 16rffcc00ff);
+		e := tkcmd(win,"panel .fthumb.p"+si+" -borderwidth 2 -bg white"+
+					" -height 90 -width 90 -relief raised");
+		tk->putimage(win,".fthumb.p"+si, thumbimg[i],nil);
+		tkcmd(win, "bind .fthumb.p"+si+" <Double-Button-1> {send butchan view "+si+"}");
+		tkcmd(win, "bind .fthumb.p"+si+" <ButtonPress-1> {send butchan selectonly "+si+"}");
+		tkcmd(win, "bind .fthumb.p"+si+" <ButtonPress-2> {send butchan select "+si+"}");
+		tkcmd(win, "bind .fthumb.p"+si+" <ButtonPress-3> {send butchan menu "+si+" %X %Y}");
+		thisw := int tkcmd(win, ".fthumb.mb"+si+" cget -width");
+		w += max(94, thisw);
+		if ((nocols == -1 && w >= w1-(col*2)) || col == nocols) {
+			nocols = col;
+			col = 0;
+			row+=2;
+			w = thisw;
+		}
+		if (col == 0)
+			tkcmd(win, "grid rowconfigure .fthumb "+string (row+1)+
+						" -minsize "+string (105 - 2*(3-ssize)));
+
+		tkcmd(win, "grid .fthumb.mb"+si+" -row "+string row+" -column "+string col);
+		tkcmd(win, "grid .fthumb.p"+si+" -row "+string (row+1)+" -column "+string col+" -sticky n");
+
+		tkcmd(win, "menu .mthumb"+si+tkfont);
+		for (k = nothumbs; k < len menu; k++)
+			tkcmd(win, ".mthumb"+si+" add command -text {"+menu[k].text+"} " +
+				"-command {send butchan "+menu[k].com+" "+si+"}");
+		
+		if (isloaded(filelist[i],THUMB) && usecache)
+			loadthumbnail(win,i);
+		col++;
+	}
+	if (row == 0)
+		nocols = col;
+	doselect(win);
+	size := tkcmd(win, "grid size .fthumb");
+	csize := int size[:isat(size, " ")];
+	rsize := int size[isat(size, " ")+1:];
+	if (csize > nocols)
+		tkcmd(win, "grid columndelete .fthumb "+string nocols+" "+string csize);
+	if (rsize > row+1)
+		tkcmd(win, "grid rowdelete .fthumb "+string (row+2)+" "+string rsize);
+	height := string (2 + int tkcmd(win, ".fthumb cget -height"));
+	width := tkcmd(win, ".f.f1.c1 cget -width");
+	colsize : int;
+	if (nocols > 0) colsize = int width / nocols;
+	else colsize = int width;
+	for (i = 0; i < nocols; i++)
+			tkcmd(win, "grid columnconfigure .fthumb "+string i+" -minsize "+string colsize);
+
+	tkcmd(win, ".f.f1.c1 configure -scrollregion { 0 0 "+width+" "+height+"}");
+	tkcmd(win, "update");
+	return 0;
+}
+
+Mtype: adt {
+	text, com: string;
+};
+
+menu := array[] of {
+	Mtype ("Show Thumbnail", "show"),
+	Mtype ("Download", "dnld"),
+	Mtype ("View", "view"),
+	Mtype ("Delete", "del"),
+	Mtype ("Clear Cache", "unload"),
+	Mtype ("Refresh Files", "refresh"),
+};
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	if (cmd[len cmd - 1] == '$')
+		cmd = cmd[:len cmd - 1] + tkfontb;
+	else if (cmd[len cmd - 1] == '@')
+		cmd = cmd[:len cmd - 1] + tkfont;
+	if (cmd[len cmd - 1] == '&')
+		cmd = cmd[:len cmd - 1] + butheight+tkfont;
+	
+	e := tk->cmd(top, cmd);
+	if (e != "" && e[0] == '!') sys->print("tk error: '%s': %s\n",cmd,e);
+	return e;
+}
+
+loadnewthumb(top: ref Tk->Toplevel, ctlchan: chan of int)
+{
+	pid := sys->pctl(0,nil);
+	spawn workingscreen2(coords,pid, ctlchan,0);
+	getstore(top);
+	for (tmp := fnew; tmp != nil; tmp = tl tmp)
+		loadthumbnail(top,hd tmp);
+	ctlchan <-= DONE;
+}
+
+loadthumb(top: ref Tk->Toplevel, i: int, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(coords,pid, ctlchan,0);
+	if (i == -1) {
+		for (k := 0; k < nofiles; k++)
+			if (selected[k])
+				if (loadthumbnail(top, k) != 0) break;
+	}
+	else loadthumbnail(top, i);
+	ctlchan <-= DONE;
+}
+
+loadthumbnail(top: ref Tk->Toplevel, i: int): int
+{
+	fd : ref sys->FD;
+	if (usecache && isloaded(filelist[i],THUMB))
+		fd  = sys->open(tmppath+filelist[i]+"."+string THUMB+"~",sys->OREAD);
+	else fd = sys->open(camerapath+"thumb/"+filelist[i]+".bit",sys->OREAD);
+	if (fd == nil) {
+		if (usecache && isloaded(filelist[i],THUMB)) {
+			delloaded(filelist[i],THUMB);
+			return loadthumbnail(top,i);
+		}
+		else dialog("cannot open "+filelist[i]+".bit",0,-1,coords);
+		return -2;
+	}
+	image := display.readimage(fd);
+	if (image == nil) {
+		if (usecache && isloaded(filelist[i],THUMB)) {
+			delloaded(filelist[i],THUMB);
+			return loadthumbnail(top,i);
+		}
+		else dialog("Could not load thumbnail: "+filelist[i]+".jpg",0,-1,coords);
+		return -1;
+	}
+	else {
+		p := Point((90-image.r.max.x)/2,(90-image.r.max.y)/2);
+		thumbimg[i].draw(image.r.addpt(p), image,nil,(0,0));
+		si := string i;
+		tkcmd(top,".fthumb.p"+si+" dirty");
+		fd = nil;
+		n := -1;
+		if (usecache) {
+			fd  = sys->create(tmppath+filelist[i]+"."+string THUMB+"~",sys->OWRITE,8r666);
+			n = display.writeimage(fd, image);
+		}
+		x := int tkcmd(top, ".fthumb.mb"+string i+" cget -actx");
+		y := int tkcmd(top, ".fthumb.mb"+string i+" cget -acty");
+		h := int tkcmd(top, ".fthumb.mb"+string i+" cget -height");
+		x1 := int tkcmd(top, ".fthumb cget -actx");
+		y1 := int tkcmd(top, ".fthumb cget -acty");
+		tkcmd(top, ".f.f1.c1 see "+string (x-x1)+" " +string (y-y1)+
+			" "+string (x-x1+90)+" " +string (y-y1+h+102)+"; update");
+		if (!usecache || n == 0) imgloaded = (filelist[i],THUMB) :: imgloaded;
+	}
+	return 0;
+}
+
+isloaded(name: string, ftype: int): int
+{
+	tmp := imgloaded;
+	while (tmp != nil) {
+		ic := hd tmp;
+		if (ic.name == name && ic.imgtype == ftype) return 1;
+		tmp = tl tmp;
+	}
+	return 0;
+}
+
+delloaded(name: string, ftype: int)
+{
+	tmp :  list of Imgloaded;
+	tmp = nil;
+	while (imgloaded != nil) {
+		ic := hd imgloaded;
+		if (ic.name != name || ic.imgtype != ftype)
+			tmp = ic :: tmp;
+		else sys->remove(tmppath+ic.name+"."+string ic.imgtype+"~");
+		imgloaded = tl imgloaded;
+	}
+	imgloaded = tmp;
+}
+
+dialog(msg: string, diagtype, img: int, r: Rect): int
+{
+	if (diagtype == 2)
+		diagtype = 0;
+	else 
+		working = 0;
+	tmpimg : ref draw->Image;
+	out := 0;
+	title := "Dialog";
+	if (diagtype == 0) title = "Alert!";
+	(win, titlebar) := tkclient->toplevel(context, "" , title, Tkclient->Appl);
+	diagchan := chan of string;
+	tk->namechan(win, diagchan, "diagchan");
+	tkcmd(win, "frame .f");
+	tkcmd(win, "label .f.l -text {"+msg+"}"+tkfont);
+	tkcmd(win, "button .f.bo -text { ok } -command {send diagchan ok} "+butheight+tkfont);
+	tkcmd(win, "button .f.bc -text {cancel} -command {send diagchan cancel}"+butheight+tkfont);
+	if (img >= 0 && isloaded(filelist[img], THUMB) && usecache) {
+		fd := sys->open(tmppath+filelist[img]+"."+string THUMB+"~", sys->OREAD);
+		if (fd != nil) {
+			tmpimg = display.readimage(fd);
+			tkcmd(win,"panel .f.p -height "+string tmpimg.r.max.y+
+				" -width "+string tmpimg.r.max.x+" -borderwidth 2 -relief raised");
+			tk->putimage(win,".f.p", tmpimg, nil);
+			tkcmd(win, "grid .f.p -row 1 -column 0 -columnspan 2 -padx 5 -pady 5");
+		}
+	}
+	tkcmd(win, "grid .f.l -row 0 -column 0 -columnspan 2 -padx 10 -pady 5");
+	if (diagtype == 1) {
+		tkcmd(win, "grid .f.bo -row 2 -column 0 -padx 5 -pady 5");
+		tkcmd(win, "grid .f.bc -row 2 -column 1 -padx 5 -pady 5");
+	}
+	else 	tkcmd(win, "grid .f.bo -row 2 -column 0 -columnspan 2 -padx 5 -pady 5");
+	if (!r.eq(nilrect))
+		centrewin(win, r, 1);
+	else
+		tkcmd(win, "pack .f; focus .; update");
+	tkclient->onscreen(win, "exact");
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	main: for (;;) {
+		alt {
+		s := <-win.ctxt.kbd =>
+			tk->keyboard(win, s);
+		s := <-win.ctxt.ptr =>
+			tk->pointer(win, *s);
+		inp := <-diagchan =>
+			if (inp == "ok") {
+				out = 1;
+				break main;
+			}
+			if (inp == "cancel")
+				break main;
+
+		title = <-win.ctxt.ctl or
+		title = <-win.wreq or
+		title = <-titlebar =>
+			if (title == "exit")
+				break main;
+			else
+				tkclient->wmctl(win, title);
+		}
+	}
+	return out;
+}	
+
+snapscr := array[] of {
+	"label .f.fsnap.ltime -text {Date and Time} $",
+	"label .f.fsnap.lstore -text {Memory Status} $",
+	"label .f.fsnap.lzpos -text {Zoom} $",
+	"label .f.fsnap.lssiz -text {Resolution} $",
+	"label .f.fsnap.lscpn -text {Compression} $",
+	"grid .f.fsnap.ltime -row 0 -column 0 -sticky sw",
+	"grid .f.fsnap.lstore -row 0 -column 1 -sticky sw",
+	"grid .f.fsnap.lscpn -row 2 -column 0 -sticky sw",
+	"grid .f.fsnap.lssiz -row 2 -column 1 -sticky sw",
+	"grid .f.fsnap.lzpos -row 2 -column 2 -sticky sw",
+
+	"grid .f.fsnap.ftime -row 1 -column 0  -sticky nsew",
+	"grid .f.fsnap.fstore -row 1 -column 1  -sticky nsew",
+	"grid .f.fsnap.fsettings -row 1 -column 2  -sticky nsew",
+	"grid .f.fsnap.fscpn -row 3 -column 0  -sticky nsew",
+	"grid .f.fsnap.fssiz -row 3 -column 1 -sticky nsew",
+	"grid .f.fsnap.fzpos -row 3 -column 2  -sticky nsew",
+	"grid .f.fsnap.b -row 4 -column 0 -columnspan 3",
+	"grid rowconfigure .f.fsnap 0 -minsize 30",
+	"grid rowconfigure .f.fsnap 2 -minsize 30",
+	"grid rowconfigure .f.fsnap 4 -minsize 30",
+
+	"update",
+};
+
+mainscreen(win: ref Tk->Toplevel, opt: int, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(coords, pid, ctlchan, opt);
+	if (opt == 1) {
+		for (tk1 := 0; tk1 < len snapscr; tk1++)
+			tkcmd(win, snapscr[tk1]);
+
+		gettime(win);
+		if (samedate(win)) tkcmd(win, "variable time 1; update");
+	}
+	getstore(win);
+	lst := getabl("scpn") :: getabl("ssiz") :: getabl("zpos") :: nil;
+	if (getstate(win, hd tl tl lst) == 0);
+		if (getstate(win, hd tl lst) == 0);
+			getstate(win, hd lst);
+	if (opt == 1) {
+		addtoplevel(win, "", lst, -1);
+		height := tkcmd(win, ".f.fsnap cget -height");
+		width := tkcmd(win, ".f.fsnap cget -width");
+#		tkcmd(win, ".f.f2.c1 configure -scrollregion { 0 0 "+width+" "+height+"}");
+#		tkcmd(win, ".f.f2.c1 configure -height "+height+"}");
+	}
+	ctlchan <-= DONE;
+}
+
+kill(pid: int)
+{	
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil)
+		sys->write(pctl, array of byte "kill", len "kill");
+}
+
+gettime(win: ref Tk->Toplevel)
+{
+	tkcmd(win,".f.fsnap.ftime.l3 configure -text {}"+tkfont);
+	tkcmd(win,".f.fsnap.ftime.l4 configure -text {}"+tkfont);
+	fdi := bufio->open(camerapath+"date",sys->OREAD);
+	if (fdi != nil) {
+		s := fdi.gets('\n');
+		if (s != nil) {
+			if (s[len s - 1] == '\n') s = s[:len s - 1];
+			tm := daytime->local(daytime->now());
+			time := sys->sprint("%d/%d/%d %d:%d:%d", tm.mon+1, tm.mday, tm.year-100,
+							tm.hour,tm.min,tm.sec);
+			ltime = addzeros(time);
+			ctime = addzeros(s[len "date is ":]);
+			tk->cmd(win,".f.fsnap.ftime.l3 configure -text {"+ltime+"}");
+			tk->cmd(win,".f.fsnap.ftime.l4 configure -text {"+ctime+"}");
+		}
+	}
+	if (len ltime < 16)
+		ltime = "??/??/?? ??:??:??";
+	if (len ctime < 16)
+		ctime = "??/??/?? ??:??:??";
+	tkcmd(win, "update");
+}
+
+addzeros(s: string): string
+{
+	s[len s] = ' ';
+	rs := "";
+	start := 0;
+	isnum := 0;
+	for (i := 0; i < len s; i++) {
+		if (s[i] < '0' || s[i] > '9') {
+			if (isnum && i - start < 2) rs[len rs] = '0';
+			rs += s[start:i+1];
+			start = i+1;
+			isnum = 0;
+		}
+		else isnum = 1;
+	}
+	i = len rs - 1;
+	while (i >= 0 && rs[i] == ' ') i--;
+	return rs[:i+1];
+}	
+
+samedate(win: ref Tk->Toplevel): int
+{
+	s1 := tkcmd(win, ".f.fsnap.ftime.l3 cget -text");
+	s2 := tkcmd(win, ".f.fsnap.ftime.l4 cget -text");
+	if (s1 == "" || s1 == "") return 0;
+	if (s1[:len s1 - 3] == s2[:len s2 - 3]) return 1;
+	return 0;
+}
+
+settime()
+{
+	tm := daytime->local(daytime->now());
+	fd := sys->open(camerapath+"date", sys->OWRITE);
+	if (fd != nil) {
+		sys->fprint(fd, "%s", addzeros(sys->sprint("%d/%d/%d %d:%d:%d"
+			,tm.mon+1, tm.mday, tm.year-100, tm.hour,tm.min,tm.sec)));
+	}
+}
+
+getstore(win: ref Tk->Toplevel)
+{
+	fdi := bufio->open(camerapath+"storage",sys->OREAD);
+	if (fdi != nil) {
+		for(i := 0; i < 3; i++) {
+			s := fdi.gets('\n');
+			if (s == nil) break;
+			if (i > 0) {
+				(n,lst) := sys->tokenize(s,"\t\n:");
+				val := string int hd tl lst;
+				if (i == 2 && val == "0") 
+					tkcmd(win, ".f.fsnap.b configure -state disabled");
+				else tkcmd(win, ".f.fsnap.b configure -state normal");
+				tkcmd(win,".f.fsnap.fstore.l"+string (2+i)+" configure -text {"+val+"  }");
+			}
+		}
+		tkcmd(win, "update");	
+	}
+}
+
+contains(s: string, test: string): int
+{
+	num :=0;
+	if (len test > len s) return 0;
+	for (i := 0; i < (1 + (len s) - (len test)); i++) {
+		if (test == s[i:i+len test]) num++;
+	}
+	return num;
+}
+
+multidownload()
+{
+	getpath := selectfile->filename(context,
+							display.image,
+							"Multiple download to directory...", 
+							nil,
+							lastpath);
+	if (getpath == "" || getpath[0] != '/' || getpath[len getpath - 1] != '/')
+		return;
+	s := "";
+	for (k := 0; k < nofiles; k++) {
+		if (selected[k]) {
+			e := dnld(k,getpath);
+			if (e != 1) 
+				s += filelist[k]+".jpg ";
+			if (e == 3) {
+				s += "cancelled\n";
+				break;
+			}
+			else if (e == 0)
+				s += "failed\n";
+			working = 0;
+		}
+	}
+	if (s != "") s = ":\n\n"+s;
+	dialog("Multiple download complete"+s,0,-1,coords);
+}
+
+downloading := "";
+
+dnld(i: int, path: string): int
+{
+	ctlchan := chan of int;
+	ctlchans := chan of string;
+	chanout := chan of string;
+	spawn downloadscreen(coords, i, ctlchans, chanout);
+	spawn download(i,path,ctlchan, ctlchans, chanout);
+	pid := <-ctlchan;
+	alt {
+		s := <-ctlchans =>
+			chanout <-= "!done!";
+			if (s == "kill") {
+				if (downloading != "") {
+					(n,lst) := sys->tokenize(downloading, " \t\n");
+					for(;lst != nil; lst = tl lst)
+						sys->remove(hd lst);
+				}
+				kill(pid);
+				return 3;
+			}
+			else return dnld(i, "!"+s);
+		e := <-ctlchan =>
+			chanout <-= "!show!";
+			chanout <-= "!done!";
+			return e;
+	}
+	return 0;
+}
+
+filelenrefresh(filename: string): int
+{
+	fd := sys->open(camerapath+"ctl",sys->OWRITE);
+	if (fd != nil) {
+		sys->fprint(fd, "refresh");
+		(n, dir) := sys->stat(filename);
+		if (n == -1)
+			return -1;
+		return int dir.length;
+	}
+	return -1;
+}
+
+testfilesize(filename: string): int
+{
+	e := filelenrefresh(filename);
+	if (e == 0) {
+		e2 := dialog("Camera is still processing image\nwait until ready?",1,-1,coords);
+		if (e2 == 0)
+			return 0;
+		ctlchan := chan of int;
+		spawn waittilready(filename, ctlchan);
+		e3 := <- ctlchan;
+		working = 0;
+		if (e3 == KILLED)
+			return 0;
+		return testfilesize(filename);
+	}
+	else return e;
+}
+
+waittilready(filename: string, ctlchan: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	spawn workingscreen2(coords,pid,ctlchan,0);
+	for (;;) {
+		if (filelenrefresh(filename) != 0)
+			break;
+		sys->sleep(2000);
+	}
+	ctlchan <-= DONE;
+}
+
+download(i: int, path: string, ctlchan: chan of int, ctlchans, chanout: chan of string)
+{
+	ctlchan <-= sys->pctl(0, nil);
+	downloading = "";
+	savename : string;
+	if (path == "") {
+		savename = selectfile->filename(context,
+								display.image,
+								"Save "+filelist[i]+".jpg to directory...", 
+								"*.jpg" :: "*.jpeg" :: nil,
+								lastpath);
+		if (savename == "" || savename[0] != '/') {
+			ctlchan <-= 0;
+			return;
+		}
+	}
+	else savename = path;
+
+	# Used when retrying due to cache copy failing
+	if (savename[0] == '!') {
+		delloaded(filelist[i],JPG);
+		savename = savename[1:];
+		path = "";
+	}
+	confirm := 1;
+	# Don't confirm overwrite
+	if (savename[0] == '$') {
+		confirm = 0;
+		savename = savename[1:];
+	}
+
+	if (savename[len savename - 1] == '/')
+		savename += filelist[i]+".jpg";
+
+	if (!hasext(savename, ".jpg"))
+		savename += ".jpg";
+
+	p := isat2(savename,"/");
+	lastpath = savename[:p+1];
+
+	filename := camerapath+"jpg/"+filelist[i]+".jpg";
+	filesize := testfilesize(filename);
+	cached := 0;
+	if (filesize > 0 && isloaded(filelist[i],JPG) && usecache) {
+		cachefilename := tmppath+filelist[i]+"."+string JPG+"~";
+		if (testfilesize(cachefilename) == filesize) {
+			cached = 1;
+			filename = cachefilename;
+		}
+		else delloaded(filelist[i],JPG);
+	}
+	fd := sys->open(filename, sys->OREAD);
+	if (filesize < 1 || fd == nil) {
+		ctlchan <-= -1;
+		return;
+	 }
+
+	read := 0;
+	cancel : int;
+	buf : array of byte;
+	fd2, fd3 : ref sys->FD = nil;
+	cachename := tmppath+filelist[i]+"."+string JPG+"~";
+	if (confirm) (fd2, cancel) = create(savename, coords);
+	else fd2 = sys->create(savename,sys->OWRITE, 8r666);
+	if (fd2 == nil) {
+		ctlchan <-= cancel;
+		return;
+	}
+	if (usecache && !cached)
+		fd3 = sys->create(cachename,sys->OWRITE,8r666);
+	chanout <-= "!show!";
+	chanout <-= "l2 Downloading...";
+	chanout <-= "pc 0";
+	n : int;
+	downloading = savename;
+	if (fd3 != nil)
+		downloading += " "+cachename;
+	loop: for(;;) {
+		rlen := 8192;
+		if (read + rlen >= filesize) rlen = filesize - read;
+		buf = array[rlen] of byte;
+		n = sys->read(fd,buf,len buf);
+		read += n;
+		sout := "pc "+string ( (100*read)/filesize);
+		chanout <-= sout;
+		if (n < 1) break loop;
+		written := 0;
+		while (written < n) {
+			n2 := sys->write(fd2,buf,n);
+			if (n2 < 1) break loop;
+			if (fd3 != nil) sys->write(fd3,buf,n);
+			written += n2;
+		}
+	}
+	chanout <-= "pc 100";
+	downloading = "";
+	fd = nil;
+	fd2 = nil;
+	if (read < filesize || n == -1) {
+		if (cached) {
+			ctlchans <-= savename;
+			return;
+		}
+		sys->remove(savename);
+		sys->remove(cachename);
+		if (path == "")
+			dialog(sys->sprint("Download Failed: %s.jpg\nread %d of %d bytes\n",
+					filelist[i],read,filesize), 0, i,coords);
+		ctlchan <-= 0;
+		return;
+	}
+	
+	# save it in cache 
+	if (usecache)
+		imgloaded = (filelist[i],JPG) :: imgloaded;
+	if (path == "") dialog(filelist[i]+".jpg downloaded",0,i,coords);
+	ctlchan <-= 1;
+}
+
+downloadscr := array[] of {
+	"frame .f -borderwidth 2 -relief raised",
+	"label .f.l1 -text { } @",
+	"label .f.l2 -text {Waiting...} @",
+	"button .f.b -text {Cancel} -command {send ctlchans kill} &",
+	"grid .f.l1 -row 0 -column 0 -columnspan 2 -pady 5",
+	"grid .f.l2 -row 2 -column 1 -sticky w -padx 10",
+	"grid .f.p -row 3 -column 1 -columnspan 1 -padx 10",
+	"grid .f.b -row 4 -column 0 -pady 5 -columnspan 2",
+};
+
+downloadscreen(r: Rect, i: int, ctlchans, chanin: chan of string)
+{
+	working = 1;
+	<- chanin;
+	(top, nil) := tkclient->toplevel(context,"", nil, tkclient->Plain);
+	progr := Rect((0,0),(100,15));
+	imgbg := display.newimage(progr,draw->CMAP8,1,draw->Black);
+	black := display.newimage(progr,draw->CMAP8,1,draw->Black);
+	white := display.newimage(progr,draw->CMAP8,1,draw->White);
+	imgfg := display.newimage(progr,draw->CMAP8,1,draw->Blue);
+	tkcmd(top, "panel .f.p -width 100 -height 15 -bg white -borderwidth 2 - relief raised");
+	tk->putimage(top, ".f.p",imgbg,nil);
+	tk->namechan(top, ctlchans, "ctlchans");
+	for (tk1 := 0; tk1 < len downloadscr; tk1++)
+		tkcmd(top, downloadscr[tk1]);
+	tmpimg : ref Image = nil;
+	if (i >= 0 && isloaded(filelist[i], THUMB) && usecache)
+		tmpimg = display.open(tmppath+filelist[i]+"."+string THUMB+"~");
+	if (tmpimg == nil)
+		tmpimg = procimg;
+	if (tmpimg != nil) {
+		w := tmpimg.r.dx();
+		h := tmpimg.r.dy();
+		tkcmd(top, "panel .f.p2 -width "+string w+" -height "+string h+
+					" -borderwidth 2 -relief raised");
+		tk->putimage(top, ".f.p2", tmpimg, nil);
+		tkcmd(top, "grid .f.p2 -row 2 -column 0 -rowspan 2 -sticky e");
+		tkcmd(top, "grid columnconfigure .f 0 -minsize "+string (w + 14));
+	}
+
+	tkcmd(top, ".f.l1 configure -text {"+filelist[i]+".jpg}");
+	centrewin(top,r,1);
+	oldcoords := coords;
+	tkclient->onscreen(top, "exact");
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+
+	main: for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		text := <-chanin =>
+			if (!oldcoords.eq(coords)) {
+				centrewin(top,coords,0);
+				oldcoords = coords;
+			}
+			if (text == "!done!") break main;
+			if (text[:2] == "pc") {
+				val := int text[3:];
+				imgbg.draw(((0,0),(val,15)), imgfg,nil,(0,0));
+				if (val != 100)
+					imgbg.draw(((val+1,0),(100,15)), black,nil,(0,0));
+				imgbg.text((42,1),white,(0,0),font, text[3:]+"%");
+				tkcmd(top,".f.p dirty; update");
+			}
+			else if (text[:2] == "l2")
+				tkcmd(top, ".f.l2 configure -text {"+text[3:]+"}; update");
+		}
+	}
+	working = 0;
+}
+
+centrewin(top: ref Tk->Toplevel, r: Rect, first: int)
+{
+	s := "";
+	if (first)
+		s = "pack .f;";
+	w := int tkcmd(top, ".f cget -width");
+	h := int tkcmd(top, ".f cget -height");
+	tmp := tk->cmd(top, ".Wm_t cget -height");
+	if (tmp != "" && tmp[0] != '!') {
+		h += int tmp;
+		s += "focus .;";
+	}
+	px := r.min.x + ((r.max.x - r.min.x - w) / 2);
+	py := r.min.y + ((r.max.y - r.min.y - h) / 2);
+	tkcmd(top, ". configure -x "+string px+" -y "+string py);
+	tkcmd(top, s+"raise .; update");
+}
+
+workingscr2 := array[] of {
+	"frame .f -borderwidth 2 -relief raised",
+	"label .f.l3 -text { } -width 220 -height 2",
+	"label .f.l -text {Please Wait} @",
+	"label .f.l2 -text {|} -width 20 @",
+	"button .f.b -text {Cancel} -command {send chanin kill} &",
+	"grid .f.l -row 1 -column 0 -sticky e",
+	"grid .f.l2 -row 1 -column 1 -sticky w",
+	"grid .f.b -pady 5 -row 3 -column 0 -columnspan 2",
+	"grid .f.l3 -row 4 -column 0 -columnspan 2",
+	"grid rowconfigure .f 1 -minsize 80",
+};
+
+workingscreen2(r : Rect, pid: int, ctlchan: chan of int, loading: int)
+{
+	(top, nil) := tkclient->toplevel(context,"",nil, tkclient->Plain);
+	chanin := chan of string;
+	tk->namechan(top, chanin, "chanin");
+	for (tk1 := 0; tk1 < len workingscr2; tk1++)
+		tkcmd(top, workingscr2[tk1]);
+
+	if (loading) {
+#		loadimg := display.open("camload.bit");
+		if (loadimg != nil) {
+			w := loadimg.r.dx();
+			h := loadimg.r.dy();
+			tkcmd(top, "panel .f.p -width "+string w+" -height "+string h+
+						" -borderwidth 2 -relief raised");
+			tk->putimage(top, ".f.p", loadimg, nil);
+			tkcmd(top, "grid .f.p -row 2 -column 0 -columnspan 2 -pady 5 -padx 20");
+			tkcmd(top, "grid forget .f.l .f.l2; grid rowconfigure .f 1 -minsize 20");
+		}
+	}
+	else {
+		if (procimg != nil) {
+			w := procimg.r.dx();
+			h := procimg.r.dy();
+			tkcmd(top, "panel .f.p -width "+string w+" -height "+string h+
+						" -borderwidth 2 -relief raised");
+			tk->putimage(top, ".f.p", procimg, nil);
+			tkcmd(top, "grid .f.p -row 2 -column 0 -columnspan 2");
+			tkcmd(top, "grid rowconfigure .f 1 -minsize 30");
+			tkcmd(top, "grid rowconfigure .f 2 -minsize 50");
+		}
+	}
+
+	centrewin(top,r,1);
+	spawn workingupdate(top,chanin);
+	tkclient->onscreen(top, "exact");
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+
+	main: for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <-chanin =>
+			if (inp == "done") break main;
+			if (inp == "kill") {
+				working = 0;
+				if (pid != -1) kill(pid);
+				ctlchan <-= KILLED;
+				<-chanin;
+				break main;
+			}
+		}
+	}
+}
+
+workingupdate(top: ref Tk->Toplevel, chanout: chan of string)
+{
+	show := array[] of { "/", "-", "\\\\", "|", };
+	if (working) {
+		chanout <-= "done";
+		return;
+	}
+	working = 1;
+	oldcoords := coords;
+	hidden := 0;
+	loop: for(;;) {
+		for (i := 0; i < 4; i++) {
+			sys->sleep(100);
+			tkcmd(top, ".f.l2 configure -text {"+show[i]+"}; update");
+			if (!working) break loop;
+			if (!oldcoords.eq(coords)) {
+				centrewin(top, coords,0);
+				oldcoords = coords;
+			}
+		}
+	}
+	chanout <-= "done";
+}
+
+scrollx := 0;
+scrolly := 0;
+
+resizemain(top: ref Tk->Toplevel, init: int)
+{
+	h, w: int;
+	if (init) {
+		growheight(top, 4000);
+		h = int tkcmd(top, ".f.fsnap cget -height") +
+			int tkcmd(top, ".Wm_t cget -height") +
+			2 * (124 - (5*(3-ssize)));
+		if (h > display.image.r.dy())
+			h = display.image.r.dy();
+		w = display.image.r.dx();
+	}
+	else {
+		r := tk->rect(top, ".", 0);
+		h = r.dy();
+		w = r.dx();	
+	}
+
+	ht := int tkcmd(top, ".Wm_t cget -height");
+
+	hf := int tkcmd(top, ".f cget -height");
+	wf := int tkcmd(top, ".f cget -width");
+	wsb := int tkcmd(top, ".f.f1.sb1 cget -width");
+
+	growwidth(top, w - 4);
+	ws := int tkcmd(top, ".f.fsnap cget -width");
+	if (w > ws + 4)
+		w = ws + 4;
+	shrinkwidth(top,w - 4);
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	if (w < ws || init)
+		w = ws + 4;
+		
+	hmax := ((3*(h - ht))/5) - 4;
+	growheight(top, hmax);
+	shrinkheight(top, hmax);
+	hs := int tkcmd(top, ".f.fsnap cget -height");
+
+	hmb := int tkcmd(top, ".f.fsnap.fsettings.mb cget -height");
+	if (h < ht+hs + 107 + hmb) h = ht+hs+107 + hmb;
+
+#	hc2 = int tkcmd(top, ".f.fsnap cget -height");
+	wc2 := int tkcmd(top, ".f.fsnap cget -width");
+
+	hc1 := h - ht - hs - 4;
+	wc1 := w-wsb-4;
+#	wc1 = wc2 - wsb;
+	tkcmd(top, ".f.f1.c1 configure -height "+string hc1+" -width "+string wc1);
+#	tkcmd(top, ".f.f2.c1 configure -height "+string hc2+" -width "+string wc2);
+	if (w < wc2 + 4)
+		w = wc2 + 4;
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	hs = int tkcmd(top, ".f.fsnap cget -height");
+		
+	tkcmd(top, ". configure -height "+string h+" -width "+string w+"; update");
+	refreshfilelist(top, 0);
+}
+
+growwidth(top: ref Tk->Toplevel, wc2: int)
+{
+	ws := int tkcmd(top, ".f.fsnap cget -width");
+	if (wc2 > ws && reducew[2]) {
+		tkcmd(top, ".f.fsnap.ftime.l1 configure -text {Local:}");
+		tkcmd(top, ".f.fsnap.ftime.l2 configure -text {Camera:}");
+		tkcmd(top, ".f.fsnap.ftime.cb configure -text {Set to local time}");
+		reducew[2] = 0;
+	}
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	if (wc2 > ws && reducew[1]) {
+		tkcmd(top, ".f.fsnap.ftime.l3 configure -text {"+ltime+"}");
+		tkcmd(top, ".f.fsnap.ftime.l4 configure -text {"+ctime+"}");
+		tkcmd(top, ".f.fsnap.ftime.cb configure -text {Set camera to local time}");
+		reducew[1] = 0;
+	}
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	if (wc2 > ws && reducew[0]) {
+		tkcmd(top, ".f.fsnap.fstore.l1 configure -text {  Photos taken:}");
+		reducew[0] = 0;
+	}
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	if (wc2 > ws) {
+		wfs += wc2 - ws;
+		if (wfs > 125-(20*(3-ssize))) wfs = 125-(20*(3-ssize));
+		tkcmd(top, "grid columnconfigure .f.fsnap 2 -minsize "+string wfs);
+	}
+}
+
+growheight(top: ref Tk->Toplevel, hc2: int)
+{
+	hs := int tkcmd(top, ".f.fsnap cget -height");
+	if (hc2 > hs) {
+		tk->cmd(top, "grid .f.fsnap.fsettings.mb2 -row 2 -column 0 -sticky ew");
+		tk->cmd(top, "grid .f.fsnap.ftime.cb -row 2 -column 0 -columnspan 2");
+		tk->cmd(top, "grid .f.fsnap.ftime.b -row 3 -column 0 -columnspan 2");
+	}
+	hs = int tkcmd(top, ".f.fsnap cget -height");
+	if (hc2 > hs) {
+		hsc := int tkcmd(top, ".f.fsnap.fzpos.sc cget -height");
+		hsc += hc2 - hs;
+		if (hsc > 88-(10*(3-ssize))) hsc = 88-(10*(3-ssize));
+		tkcmd(top, ".f.fsnap.fzpos.sc configure -height "+string hsc);
+	}
+	hs = int tkcmd(top, ".f.fsnap cget -height");
+	if (hc2 > hs) {
+		hfs += hc2 - hs;
+		if (hfs > 30 - (5*(3-ssize))) hfs = 30- (5*(3-ssize));
+		tkcmd(top, "grid rowconfigure .f.fsnap 0 -minsize "+string hfs);
+		tkcmd(top, "grid rowconfigure .f.fsnap 2 -minsize "+string hfs);
+		tkcmd(top, "grid rowconfigure .f.fsnap 4 -minsize "+string hfs);
+	}
+}
+
+shrinkheight(top: ref Tk->Toplevel, hc2: int)
+{
+	hs := int tkcmd(top, ".f.fsnap cget -height");
+	if (hc2 < hs) {
+		hfs -= hs - hc2;
+		if (hfs < 15) hfs = 15;
+		tkcmd(top, "grid rowconfigure .f.fsnap 0 -minsize "+string hfs);
+		tkcmd(top, "grid rowconfigure .f.fsnap 2 -minsize "+string hfs);
+		tkcmd(top, "grid rowconfigure .f.fsnap 4 -minsize "+string hfs);
+	}
+	hs = int tkcmd(top, ".f.fsnap cget -height");
+	if (hc2 < hs) {
+		hsc := int tkcmd(top, ".f.fsnap.fzpos.sc cget -height");
+		hsc -= hs - hc2;
+		if (hsc < 55-(5*(3-ssize))) hsc = 55-(5*(3-ssize));
+		tkcmd(top, ".f.fsnap.fzpos.sc configure -height "+string hsc);
+	}
+	hs = int tkcmd(top, ".f.fsnap cget -height");
+	if (hc2 < hs) {
+		tk->cmd(top, "grid forget .f.fsnap.fsettings.mb2");
+		tk->cmd(top, "grid forget .f.fsnap.ftime.cb");
+		tk->cmd(top, "grid forget .f.fsnap.ftime.b");
+	}
+}
+
+shrinkwidth(top: ref Tk->Toplevel, wc2: int)
+{
+	ws := int tkcmd(top, ".f.fsnap cget -width");
+	wib := int tkcmd(top, ".f.fsnap.fsettings.b cget -width");
+	if (wc2 < ws) {
+		diff := ws - wc2;
+		wfs -= diff;
+		if (wfs < wib) wfs = wib;
+		tkcmd(top, "grid columnconfigure .f.fsnap 2 -minsize "+string wfs);
+	}
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	if (wc2 < ws) {
+		tkcmd(top, ".f.fsnap.fstore.l1 configure -text {  Taken:}");
+		reducew[0] = 1;
+	}
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	if (wc2 < ws) {
+		tkcmd(top, ".f.fsnap.ftime.l3 configure -text {"+ltime[len ltime - 8:]+"}");
+		tkcmd(top, ".f.fsnap.ftime.l4 configure -text {"+ctime[len ctime - 8:]+"}");
+		tkcmd(top, ".f.fsnap.ftime.cb configure -text {Set to local time}");
+		reducew[1] = 1;
+	}
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	if (wc2 < ws) {
+		tkcmd(top, ".f.fsnap.ftime.l1 configure -text {C:}");
+		tkcmd(top, ".f.fsnap.ftime.l2 configure -text {}");
+		tkcmd(top, ".f.fsnap.ftime.l3 configure -text {"+ctime[len ctime - 17:len ctime - 8]+"}");
+		tkcmd(top, ".f.fsnap.ftime.cb configure -text {Set local}");
+		reducew[2] = 1;
+	}
+	ws = int tkcmd(top, ".f.fsnap cget -width");
+	if (wc2 > ws) {
+		wfs = 125-(20*(3-ssize));
+		tkcmd(top, "grid columnconfigure .f.fsnap 2 -minsize "+string wfs);
+	}
+}
+
+ltime, ctime: string;
+wfs := 150;
+hfs := 30;
+reducew := array[10] of { * => 0 };
+
+getcoords(top: ref Tk->Toplevel): Rect
+{
+	h := int tkcmd(top, ". cget -height");
+	w := int tkcmd(top, ". cget -width");
+	x := int tkcmd(top, ". cget -actx");
+	y := int tkcmd(top, ". cget -acty");
+	r := Rect((x,y),(x+w,y+h));
+	return r;
+}
+
+viewscr := array[] of {
+	"frame .f -bg",
+	"canvas .f.c -yscrollcommand {.f.sy set} -xscrollcommand {.f.sx set} -height 300 -width 500",
+	"scrollbar .f.sx -command {.f.c xview} -orient horizontal",
+	"scrollbar .f.sy -command {.f.c yview}",
+	"grid .f.c -row 0 -column 0",
+	"grid .f.sy -row 0 -column 1 -sticky ns",
+	"grid .f.sx -row 1 -column 0 -sticky ew",
+	"bind .Wm_t <ButtonPress-1> +{focus .}",
+	"bind .Wm_t.title <ButtonPress-1> +{focus .}",
+	"pack propagate . 0",
+	"menu .m @",
+	".m add command -text {Save As...}",
+	".m add separator",
+	".m add command -text {bit} -command {send butchan save bit}",
+	".m add command -text {jpeg} -command {send butchan save jpg}",
+
+};
+
+resizeview(top: ref Tk->Toplevel, wp,hp: int)
+{
+	w := int tkcmd(top, ". cget -width");
+	h := int tkcmd(top, ". cget -height");
+	hs := int tkcmd(top, ".f.sx cget -height");
+	ws := int tkcmd(top, ".f.sy cget -width");
+	ht := int tkcmd(top, ".Wm_t cget -height");
+	wc := w - ws - 4;
+	hc := h - hs - ht - 6;
+	wpc := wc - wp;
+	hpc := hc - hp;
+	if (wpc > 0) {
+		wc -= wpc;
+		w -= wpc;
+	}
+	if (hpc > 0) {
+		hc -= hpc;
+		h -= hpc;
+	}
+	tkcmd(top, ". configure -height "+string h+" -width "+string w);
+	tkcmd(top, ".f.c configure -height "+string hc+" -width "+string wc);
+	tkcmd(top, "update");
+}
+
+multiview()
+{
+	s := "";
+	for (k := 0; k < nofiles; k++) {
+		if (selected[k]) {
+			e := vw(k);
+			if (e != 0)
+				s += filelist[k]+".jpg ";
+			if (e == 3) {
+				s += "cancelled\n";
+				break;
+			}
+			else if (e == -1)
+				s += "failed\n";
+		}
+	}
+	if (s != "")
+		dialog("Multiple view complete:\n\n"+s,0,-1,coords);
+}
+
+vw(i: int): int
+{
+	# raise window if it is already open
+	low := toplevels;
+	for(; low != nil; low = tl low) {
+		(tplvl, name, nil, nil) := hd low;
+		if (filelist[i]+".jpg" == name) {
+			tkcmd(tplvl, "raise .; focus .; update");
+			return 0;
+		}
+	}
+
+	ctlchan := chan of int;
+	ctlchans := chan of string;
+	chanout := chan of string;
+	chanin := chan of string;
+	spawn downloadscreen(coords, i, ctlchans, chanout);
+	chanout <-= "!show!";
+	spawn view(i,ctlchan, chanin, chanout);
+	pid := <-ctlchan;
+	killed := 0;
+	for (;;) alt {
+		s := <-ctlchans =>
+			if (s == "kill") {
+				chanin <-= "kill";
+				killed = 1;
+			}
+		e := <-ctlchan =>
+			chanout <-= "!done!";
+			if (killed)
+				return 3;
+			if (e == -1)
+				dialog(sys->sprint("Cannot read file: %s.jpg\n%r",filelist[i]),0,i,coords);
+			if (e == -2) return vw(i);
+			else return e;
+	}
+	return 0;
+}
+
+view(i: int, ctlchan: chan of int, chanin, chanout: chan of string)
+{
+	ctlchan <-= sys->pctl(0, nil);
+	titlename := filelist[i]+".jpg";
+
+	filename := camerapath+"jpg/"+filelist[i]+".jpg";
+	filesize := testfilesize(filename);
+	cached := 0;
+	if (filesize > 0 && isloaded(filelist[i],JPG) && usecache) {
+		cachefilename := tmppath+filelist[i]+"."+string JPG+"~";
+		if (testfilesize(cachefilename) == filesize) {
+			cached = 1;
+			filename = cachefilename;
+		}
+		else delloaded(filelist[i],JPG);
+	}
+	if (filesize < 1) {
+		ctlchan <-= -1;
+		return;
+	 }
+
+	img: ref Image;
+	cachepath := "";
+	if (!cached && usecache)
+		cachepath = tmppath+filelist[i]+"."+string JPG+"~";
+	img = readjpg->jpg2img(filename, cachepath, chanin, chanout);
+	if(img == nil) {
+		if (cachepath != nil)
+			sys->remove(cachepath);
+		if (!cached)
+			ctlchan <-= -1;
+		else {
+			delloaded(filelist[i], JPG);
+			ctlchan <-= -2;
+		}
+		return;
+	}
+	else {
+		chanout <-= "l2 Displaying";
+		if (cachepath != "")
+			imgloaded = (filelist[i], JPG) :: imgloaded;
+		(t, titlechan) := tkclient->toplevel(context, "", titlename, Tkclient->Appl);
+		butchan := chan of string;
+		tk->namechan(t, butchan, "butchan");
+		tkcmd(t, "focus .Wm_t; update");
+		for (tk1 := 0; tk1 < len viewscr; tk1++)
+			tkcmd(t, viewscr[tk1]);
+		w := img.r.dx();
+		h :=  img.r.dy();
+		tkcmd(t, "panel .p -width "+string w+" -height "+string h);
+		tk->putimage(t, ".p",img,nil);
+		tkcmd(t, "bind .p <ButtonPress-2> {send butchan move %X %Y}");
+		tkcmd(t, "bind .p <ButtonRelease-2> {send butchan release}");
+		tkcmd(t, "bind .p <ButtonPress-3> {send butchan menu %X %Y}");
+		tkcmd(t, ".f.c create window 0 0 -window .p -anchor nw");
+		tkcmd(t, ".f.c configure -scrollregion {0 0 "+string w+" "+string h+"}");
+		ctlchan <-= 0;
+		addtoplevel(t,titlename,nil, sys->pctl(0,nil));
+
+		h1 := 300;
+		w1 := 500;
+		ht := int tkcmd(t, ".Wm_t cget -height");
+		if (h1 > display.image.r.dy() - ht) h1 = display.image.r.dy() - ht;
+		if (w1 > display.image.r.dx()) w1 = display.image.r.dx();
+		tkcmd(t, ". configure -width "+string w1+" -height "+string h1);
+		resizeview(t,w,h);
+		tkcmd(t, "pack .f; update");
+		scrolling := 0;
+		origin := Point (0,0);
+		tkclient->onscreen(t, nil);
+		tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+		loop: for(;;) alt{
+			s := <-t.ctxt.kbd =>
+				tk->keyboard(t, s);
+			s := <-t.ctxt.ptr =>
+				tk->pointer(t, *s);
+			inp := <- butchan =>
+				(n, lst) := sys->tokenize(inp, " \t\n");
+				case hd lst {
+					"save" =>
+						ftype := "."+hd tl lst;
+						savename := selectfile->filename(context,
+								display.image,
+								"Save "+filelist[i]+ftype+" to directory...", 
+								"*"+ftype :: nil,
+								lastpath);
+						if (savename != "" && savename[0] == '/') {
+							lastpath = savename[:isat2(savename,"/")+1];
+							if (savename[len savename - 1] == '/')
+								savename += filelist[i]+ftype;
+
+							if (!hasext(savename, ftype))
+								savename += ftype;
+							(fd, cancel) := create(savename, getcoords(t));
+							if (fd != nil) {
+								n2 := -1;
+								if (ftype == ".bit")
+									n2 = display.writeimage(fd,img);
+								if (ftype == ".jpg")
+									n2 = 1 - dnld(i, "$"+savename);
+								if (n2 == 0) {
+									dialog(filelist[i]+ftype+" saved",0,i,getcoords(t));
+									break;
+								}
+								dialog("Could not save: "+filelist[i]+ftype,0,i,getcoords(t));
+							}
+							if (!cancel)
+								dialog("Could not save: "+filelist[i]+ftype,0,i,getcoords(t));
+							break;
+						}
+						
+					"menu" =>
+						tkcmd(t, ".m post "+hd tl lst+" "+hd tl tl lst);
+					"release" =>
+						scrolling = 0;
+					"move" =>
+						newpoint := Point (int hd tl lst, int hd tl tl lst);
+
+						if (scrolling) {
+							diff := (origin.sub(newpoint)).mul(2);
+							tkcmd(t, ".f.c xview scroll "+string diff.x+" units");
+							tkcmd(t, ".f.c yview scroll "+string diff.y+" units");
+							origin = newpoint;
+							# clearbuffer(butchan);
+						}
+						else {
+							origin = newpoint;
+							scrolling = 1;
+						}
+				}
+	
+			s := <-t.ctxt.ctl or
+			s = <-t.wreq or
+			s = <-titlechan =>
+				if (s == "exit")
+					break loop;
+				e := tkclient->wmctl(t, s);
+				if (e == nil && s[0] == '!')
+					resizeview(t,w,h);
+		}		
+		deltoplevel(t);
+	}
+}
+
+create(filename: string, co: Rect): (ref sys->FD, int)
+{
+	(n,dir) := sys->stat(filename);
+	if (n != -1 && !dialog("overwrite "+filename+"?",1,-1,co))
+		return (nil,1);
+	return (sys->create(filename,sys->OWRITE,8r666), 0);
+}
+
+hasext(name,ext: string): int
+{
+	if (len name >= len ext && name[len name - len ext:] == ext)
+		return 1;
+	return 0;
+}
--- /dev/null
+++ b/appl/demo/chat/chat.b
@@ -1,0 +1,203 @@
+implement Chat;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+Chat: module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+stderr: ref Sys->FD;
+
+tksetup := array [] of {
+	"frame .f",
+	"text .f.t -state disabled -wrap word -yscrollcommand {.f.sb set}",
+	"scrollbar .f.sb -orient vertical -command {.f.t yview}",
+	"entry .e -bg white",
+	"bind .e <Key-\n> {send cmd send}",
+	"pack .f.sb -in .f -side left -fill y",
+	"pack .f.t -in .f -side left -fill both -expand 1",
+	"pack .f -side top -fill both -expand 1",
+	"pack .e -side bottom -fill x",
+	"pack propagate . 0",
+	"update",
+};
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmodule(Draw->PATH);
+
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmodule(Tk->PATH);
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmodule(Tkclient->PATH);
+
+
+	if (args == nil || tl args == nil) {
+		sys->fprint(stderr, "usage: chat [servicedir]\n");
+		raise "fail:init";
+	}
+	args = tl args;
+
+	servicedir := ".";
+	if(args != nil)
+		servicedir = hd args;
+
+	tkclient->init();
+	(win, winctl) := tkclient->toplevel(ctxt, nil, "Chat", Tkclient->Appl);
+
+	cmd := chan of string;
+	tk->namechan(win, cmd, "cmd");
+	tkcmds(win, tksetup);
+	tkcmd(win, ". configure -height 300");
+	fittoscreen(win);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+	msgs := chan of string;
+	conn := chan of (string, ref Sys->FD);
+	spawn connect(servicedir, msgs, conn);
+	msgsfd: ref Sys->FD;
+
+	for (;;) alt {
+	(e, fd) := <-conn =>
+		if (msgsfd == nil) {
+			if (e == nil) {
+				output(win, "*** connected");
+				msgsfd = fd;
+			} else
+				output(win, "*** " + e);
+		} else {
+			output(win, "*** disconnected");
+			msgsfd = nil;
+		}
+
+	txt := <-msgs =>
+		output(win, txt);
+
+	<- cmd =>
+		msg := tkcmd(win, ".e get");
+		if (msgsfd != nil && msg != nil) {
+			tkcmd(win, ".f.t see end");
+			tkcmd(win, ".e delete 0 end");
+			tkcmd(win, "update");
+			d := array of byte msg;
+			sys->write(msgsfd, d, len d);
+		}
+
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-winctl =>
+		tkclient->wmctl(win, s);
+	}
+}
+
+err(s: string)
+{
+	sys->fprint(stderr, "chat: %s\n", s);
+	raise "fail:err";
+}
+
+badmodule(path: string)
+{
+	err(sys->sprint("can't load module %s: %r", path));
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++)
+		tkcmd(t, cmds[i]);
+}
+
+tkcmd(t: ref Tk->Toplevel, cmd: string): string
+{
+	s := tk->cmd(t, cmd);
+	if (s != nil && s[0] == '!')
+		sys->fprint(stderr, "chat: tk error: %s [%s]\n", s, cmd);
+	return s;
+}
+
+connect(dir: string, msgs: chan of string, conn: chan of (string, ref Sys->FD))
+{
+	srvpath := dir+"/msgs";
+	msgsfd := sys->open(srvpath, Sys->ORDWR);
+	if(msgsfd == nil) {
+		conn <-= (sys->sprint("internal error: can't open %s: %r", srvpath), nil);
+		return;
+	}
+	conn <-= (nil, msgsfd);
+	buf := array[Sys->ATOMICIO] of byte;
+	while((n := sys->read(msgsfd, buf, len buf)) > 0)
+		msgs <-= string buf[0:n];
+	conn <-= (nil, nil);
+}
+
+firstmsg := 1;
+output(win: ref Tk->Toplevel, txt: string)
+{
+	if (firstmsg)
+		firstmsg = 0;
+	else
+		txt = "\n" + txt;
+	yview := tkcmd(win, ".f.t yview");
+	(nil, toks) := sys->tokenize(yview, " ");
+	toks = tl toks;
+
+	tkcmd(win, ".f.t insert end '" + txt);
+	if (hd toks == "1")
+		tkcmd(win, ".f.t see end");
+	tkcmd(win, "update");
+}
+
+KEYBOARDH: con 90;
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	Point, Rect: import draw;
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y)- KEYBOARDH);
+	bd := int tkcmd(win, ". cget -bd");
+	winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2,
+				int tkcmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
--- /dev/null
+++ b/appl/demo/chat/chatclient.sh
@@ -1,0 +1,20 @@
+#!/dis/sh
+load std
+autoload=std
+ndb/cs
+
+chatroom=$1
+
+fn ck {
+	or {$*} {
+		echo chatclient: exiting >[1=2]
+		raise error
+	}
+}
+user="{cat /dev/user}
+
+ck mount -A 'tcp!$registry!registry' /mnt/registry
+ck /dis/grid/remotelogon wm/wm {
+	k = /usr/$user/keyring/default
+	grid/find -a resource chat -a pk `{getpk -s $k} Enter {demo/chat/chat /n/client} Shell {wm/sh}
+}
--- /dev/null
+++ b/appl/demo/chat/chatsrv.b
@@ -1,0 +1,268 @@
+implement Chatsrv;
+
+#
+# simple text-based chat service
+#
+
+include "sys.m";
+	sys: Sys;
+	Qid: import Sys;
+
+include "draw.m";
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import Styx;
+
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Navigator: import styxservers;
+	nametree: Nametree;
+	Tree: import nametree;
+
+Chatsrv : module {
+	init : fn (ctxt : ref Draw->Context, args : list of string);
+};
+
+Qdir, Qusers, Qmsgs: con iota;
+
+tc: chan of ref Tmsg;
+srv: ref Styxserver;
+
+user := "inferno";
+
+dir(name: string, perm: int, path: int): Sys->Dir
+{
+	d := sys->zerodir;
+	d.name = name;
+	d.uid = user;
+	d.gid = user;
+	d.qid.path = big path;
+	if(perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	else
+		d.qid.qtype = Sys->QTFILE;
+	d.mode = perm;
+	return d;
+}
+
+badmod(path: string)
+{
+	sys->fprint(sys->fildes(1), "chatsrv: cannot load %s: %r\n", path);
+	exit;
+}
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		badmod(Styx->PATH);
+	styxservers = load Styxservers Styxservers->PATH;
+	if(styxservers == nil)
+		badmod(Styxservers->PATH);
+	nametree = load Nametree Nametree->PATH;
+	if(nametree == nil)
+		badmod(Nametree->PATH);
+	styx->init();
+	styxservers->init(styx);
+	nametree->init();
+
+	(tree, treeop) := nametree->start();
+	tree.create(big Qdir, dir(".", Sys->DMDIR|8r555, Qdir));
+	tree.create(big Qdir, dir("users", 8r444, Qusers));
+	tree.create(big Qdir, dir("msgs", 8r666, Qmsgs));
+	
+	nextmsg = ref Msg (0, nil, nil, nil);
+	keptmsg = nextmsg;
+
+	(tc, srv) = Styxserver.new(sys->fildes(0), Navigator.new(treeop), big Qdir);
+	chatsrv(tree);
+}
+
+chatsrv(tree: ref Tree)
+{
+	while((tmsg := <-tc) != nil){
+		pick tm := tmsg {
+		Readerror =>
+			break;
+		Flush =>
+			cancelpending(tm.tag);
+			srv.reply(ref Rmsg.Flush(tm.tag));
+		Open =>
+			c := srv.open(tm);
+			if (c == nil)
+				break;
+			if (int c.path == Qmsgs){
+				newmsgclient(tm.fid, c.uname);
+				#root[0].qid.vers++;		# TO DO
+			}
+		Read =>
+			c := srv.getfid(tm.fid);
+			if (c == nil || !c.isopen) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid));
+				break;
+			}
+			case int c.path {
+			Qdir =>
+				srv.read(tm);
+			Qmsgs =>
+				mc := getmsgclient(tm.fid);
+				if (mc == nil) {
+					srv.reply(ref Rmsg.Error(tm.tag, "internal error -- lost client"));
+					continue;
+				}
+				tm.offset = big 0;
+				msg := getnextmsg(mc);
+				if (msg == nil) {
+					if(mc.pending != nil)
+						srv.reply(ref Rmsg.Error(tm.tag, "read already pending"));
+					else
+						mc.pending = tm;
+					continue;
+				}
+				srv.reply(styxservers->readstr(tm, msg));
+			Qusers =>
+				srv.reply(styxservers->readstr(tm, usernames()));
+			* =>
+				srv.reply(ref Rmsg.Error(tm.tag, "phase error -- bad path"));
+			}
+		Write =>
+			c := srv.getfid(tm.fid);
+			if (c == nil || !c.isopen) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Ebadfid));
+				continue;
+			}
+			if (int c.path != Qmsgs) {
+				srv.reply(ref Rmsg.Error(tm.tag, Styxservers->Eperm));
+				continue;
+			}
+			writemsgclients(tm.fid, c.uname, string tm.data);
+			srv.reply(ref Rmsg.Write(tm.tag, len tm.data));
+		Clunk =>
+			c := srv.clunk(tm);
+			if (c != nil && int c.path == Qmsgs){
+				closemsgclient(tm.fid);
+				# root[0].qid.vers++;		# TO DO
+			}
+		* =>
+			srv.default(tmsg);
+		}
+	}
+	tree.quit();
+	sys->print("chatsrv exit\n");
+}
+
+Msg: adt {
+	fromfid: int;
+	from: string;
+	msg: string;
+	next: cyclic ref Msg;
+};
+
+Msgclient: adt {
+	fid: int;
+	name: string;
+	nextmsg: ref Msg;
+	pending: ref Tmsg.Read;
+	next: cyclic ref Msgclient;
+};
+
+NKEPT: con 6;
+keptcount := 0;
+nextmsg: ref Msg;
+keptmsg: ref Msg;
+msgclients: ref Msgclient;
+
+usernames(): string
+{
+	s := "";
+	for (c := msgclients; c != nil; c = c.next)
+		s += c.name+"\n";
+	return s;
+}
+
+newmsgclient(fid: int, name: string)
+{
+	writemsgclients(fid, nil, "+++ " + name + " has arrived");
+	msgclients = ref Msgclient(fid, name, keptmsg, nil, msgclients);
+}
+
+getmsgclient(fid: int): ref Msgclient
+{
+	for (c := msgclients; c != nil; c = c.next)
+		if (c.fid == fid)
+			return c;
+	return nil;
+}
+
+cancelpending(tag: int)
+{
+	for (c := msgclients; c != nil; c = c.next)
+		if((tm := c.pending) != nil && tm.tag == tag){
+			c.pending = nil;
+			break;
+		}
+}
+
+closemsgclient(fid: int)
+{
+	prev: ref Msgclient;
+	s := "";
+	for (c := msgclients; c != nil; c = c.next) {
+		if (c.fid == fid) {
+			if (prev == nil)
+				msgclients = c.next;
+			else 
+				prev.next = c.next;
+			s = "--- " + c.name + " has left";
+			break;
+		}
+		prev = c;
+	}
+	if (s != nil)
+		writemsgclients(fid, nil, s);
+}
+
+writemsgclients(fromfid: int, from: string, msg: string)
+{
+	nm := ref Msg(0, nil, nil, nil);
+	nextmsg.fromfid = fromfid;
+	nextmsg.from = from;
+	nextmsg.msg = msg;
+	nextmsg.next = nm;
+
+	for (c := msgclients; c != nil; c = c.next) {
+		if (c.pending != nil) {
+			s := msgtext(nextmsg);
+			srv.reply(styxservers->readstr(c.pending, s));
+			c.pending = nil;
+			c.nextmsg = nm;
+		}
+	}
+	nextmsg = nm;
+	if (keptcount < NKEPT)
+		keptcount++;
+	else
+		keptmsg = keptmsg.next;
+}
+
+getnextmsg(mc: ref Msgclient): string
+{
+# uncomment next two lines to eliminate queued messages to self
+#	while(mc.nextmsg.next != nil && mc.nextmsg.fromfid == mc.fid)
+#		mc.nextmsg = mc.nextmsg.next;
+	if ((m := mc.nextmsg).next != nil){
+		mc.nextmsg = m.next;
+		return msgtext(m);
+	}
+	return nil;
+}
+
+msgtext(m: ref Msg): string
+{
+	prefix := "";
+	if (m.from != nil)
+		prefix = m.from + ": ";
+	return prefix + m.msg;
+}
--- /dev/null
+++ b/appl/demo/chat/mkfile
@@ -1,0 +1,30 @@
+<../../../mkconfig
+
+TARG=\
+		chat.dis\
+		chatsrv.dis\
+
+SHTARG=\
+		chatclient.sh\
+
+MODULES=\
+
+SYSMODULES= \
+	draw.m\
+	styx.m\
+	styxservers.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+
+DISBIN=$ROOT/dis/demo/chat
+
+<$ROOT/mkfiles/mkdis
+
+SHFILES=${SHTARG:%.sh=$DISBIN/%}
+install:V:	$SHFILES
+%.install:V:	$DISBIN/%
+%.installall:V:	$DISBIN/%
+
+$DISBIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/demo/cpupool/mkfile
@@ -1,0 +1,26 @@
+<../../../mkconfig
+
+TARG=\
+		regpoll.dis\
+
+SHTARG=\
+		runrstyx.sh\
+
+MODULES=\
+
+SYSMODULES= \
+	draw.m\
+	registries.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/demo/cpupool
+
+<$ROOT/mkfiles/mkdis
+
+SHFILES=${SHTARG:%.sh=$DISBIN/%}
+install:V:	$SHFILES
+%.install:V:	$DISBIN/%
+%.installall:V:	$DISBIN/%
+
+$DISBIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/demo/cpupool/regpoll.b
@@ -1,0 +1,63 @@
+implement RegPoll;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "registries.m";
+	registries: Registries;
+	Attributes, Service: import registries;
+
+RegPoll: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	registries = load Registries Registries->PATH;
+	if (registries == nil)
+		badmod(Registries->PATH);
+	registries->init();
+	
+	if (len argv != 3)
+		usage();
+
+	regaddr := hd tl argv;
+	action := hd tl tl argv;
+	if (action != "up" && action != "down")
+		usage();
+
+	sys->pctl(sys->FORKNS, nil);
+	sys->unmount(nil, "/mnt/registry");
+	svc := ref Service(hd tl argv, Attributes.new(("auth", "none") :: nil));
+	for (;;) {
+		a := svc.attach(nil, nil);
+		if (a != nil && sys->mount(a.fd, nil, "/mnt/registry", Sys->MREPL, nil) != -1) {
+			if (action == "up")
+				return;
+			else
+				break;
+		}
+		sys->sleep(30000);
+	}
+	for (;;) {
+		fd := sys->open("/mnt/registry/new", sys->OREAD);
+		sys->sleep(30000);
+		if (fd == nil)
+			return;
+	}
+}
+
+badmod(path: string)
+{
+	sys->print("RegPoll: failed to load: %s\n",path);
+	exit;
+}
+
+usage()
+{
+	sys->print("usage: regpoll regaddr up | down\n");
+	raise "fail:usage";
+}
--- /dev/null
+++ b/appl/demo/cpupool/runrstyx.sh
@@ -1,0 +1,50 @@
+#!/dis/sh
+fn bindfs {
+	# this may be useful as a general purpose cmd
+	(mntpt dirs)=$*
+	memfs $mntpt
+	for d in $dirs {
+		parts=${split / $d}
+		fpath=''
+		for p in $parts {
+			fpath=$fpath^/^$p
+			if {! ftest -e $mntpt^$fpath} {
+				if {ftest -d $fpath} {
+					mkdir $mntpt^$fpath
+				} {
+					if {! ftest -e $fpath} {
+						echo $fpath does not exist >[1=2]
+						raise 'fail:errors'
+					}
+				}
+			}
+		}
+		if {! ftest -d $d} {
+			touch $mntpt/$d
+		}
+		bind $d $mntpt^$d
+	}
+}
+
+fn x {
+	echo tcp!^$2
+}
+
+bindfs /tmp /dis /n/client /dev /prog
+listen -A  `{x `{ndb/csquery tcp!^`{cat /dev/sysname}^!rstyx}} {
+		@{
+			load std
+			pctl forkns nodevs
+			bind /tmp /
+			runas rstyx {auxi/rstyxd}
+		}&
+	}
+
+while {} {
+	demo/cpupool/regpoll tcp!200.1.1.104!6676 up
+	echo Registering Rstyx service
+	mount -A 'tcp!200.1.1.104!6676' /mnt/registry
+	echo `{x `{ndb/csquery tcp!^`{cat /dev/sysname}^!rstyx}} proto styx auth none persist 1 resource '''Rstyx resource''' name `{cat /dev/sysname} > /mnt/registry/new
+	demo/cpupool/regpoll tcp!200.1.1.104!6676 down
+	echo Registry gone down
+}
--- /dev/null
+++ b/appl/demo/lego/clockface.b
@@ -1,0 +1,367 @@
+# Model 1
+implement Clockface;
+
+include "sys.m";
+include "draw.m";
+
+Clockface : module {
+	init : fn (ctxt : ref Draw->Context, argv : list of string);
+};
+
+sys : Sys;
+
+hmpath : con "motor/0";		# hour-hand motor
+mmpath : con "motor/2";		# minute-hand motor
+allmpath : con "motor/012";	# all motors (for stopall msg)
+
+hbpath : con "sensor/0";		# hour-hand sensor
+mbpath : con "sensor/2";		# minute-hand sensor
+lspath: con "sensor/1";		# light sensor;
+
+ONTHRESH : con 780;		# light sensor thresholds
+OFFTHRESH : con 740;
+NCLICKS : con 120;
+MINCLICKS : con 2;			# min number of clicks required to stop a motor
+
+Hand : adt {
+	motor : ref Sys->FD;
+	sensor : ref Sys->FD;
+	fwd : array of byte;
+	rev : array of byte;
+	stop : array of byte;
+	pos : int;
+};
+
+lightsensor : ref Sys->FD;
+allmotors : ref Sys->FD;
+hourhand : ref Hand;
+minutehand : ref Hand;
+
+reqch: chan of (string, chan of int);
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	argv = tl argv;
+	if (len argv != 1) {
+		sys->print("usage: lego_dir\n");
+		raise("fail:usage");
+	}
+
+	# set up our control file
+	if (sys->bind("#s", ".", Sys->MBEFORE) == -1) {
+		sys->print("failed to bind srv device: %r\n");
+		return;
+	}
+	f2c := sys->file2chan(".", "clockface");
+	if (f2c == nil) {
+		sys->print("cannot create legolink channel: %r\n");
+		return;
+	}
+
+	legodir := hd argv;
+	if (legodir[len legodir -1] != '/')
+		legodir[len legodir] = '/';
+
+	# get the motor files
+	sys->print("opening motor files\n");
+	hm := sys->open(legodir + hmpath, Sys->OWRITE);
+	mm := sys->open(legodir +mmpath, Sys->OWRITE);
+	allmotors = sys->open(legodir + allmpath, Sys->OWRITE);
+	if (hm == nil || mm == nil || allmotors == nil) {
+		sys->print("cannot open motor files\n");
+		raise("fail:error");
+	}
+
+	# get the sensor files
+	sys->print("opening sensor files\n");
+	hb := sys->open(legodir + hbpath, Sys->ORDWR);
+	mb := sys->open(legodir + mbpath, Sys->ORDWR);
+	lightsensor = sys->open(legodir + lspath, Sys->ORDWR);
+
+	if (hb == nil || mb == nil) {
+		sys->print("cannot open sensor files\n");
+		raise("fail:error");
+	}
+
+	hourhand = ref Hand(hm, hb, array of byte "f7", array of byte "r7", array of byte "s7", 0);
+	minutehand = ref Hand(mm, mb, array of byte "f7", array of byte "r7", array of byte "s7", 0);
+
+	sys->print("setting sensor types\n");
+	setsensortypes(hourhand, minutehand, lightsensor);
+
+	reqch = chan of (string, chan of int);
+	spawn sethands();
+#	reqch <-= ("reset", nil);
+	spawn srvlink(f2c);
+}
+
+srvlink(f2c : ref Sys->FileIO)
+{
+	for (;;) alt {
+	(offset, count, fid, rc) := <- f2c.read =>
+		if (rc == nil)
+			continue;
+		if (offset != 0) {
+			rc <-= (nil, nil);
+			continue;
+		}
+		rc <- = (array of byte gettime(), nil);
+
+	(offset, data, fid, wc) := <- f2c.write =>
+		if (wc == nil)
+			continue;
+		if (offset != 0) {
+			wc <-= (0, "bad offset");
+			continue;
+		}
+		spawn settime(wc, string data, len data);
+	}
+}
+
+gettime(): string
+{
+	hpos := hourhand.pos;
+	mpos := minutehand.pos;
+
+	h := 12 * hpos / NCLICKS;
+	m := 60 * mpos / NCLICKS;
+
+	time := "??:??";
+	for (hadj := -1; hadj <= 1; hadj++) {
+		hpos2 := (((h+hadj) * NCLICKS) / 12) + ((m * NCLICKS) / (12 * 60));
+		dhpos := hpos - hpos2;
+		if (dhpos >= -2 && dhpos <= 2) {
+			# allow 2 clicks of imprecision either way
+			time = sys->sprint("%.2d:%.2d", h+hadj, m);
+			break;
+		}	
+	}
+	return sys->sprint("%s %d %d", time, hpos*360/NCLICKS, mpos*360/NCLICKS);
+}
+
+settime(wc: Sys->Rwrite, time: string, wn: int)
+{
+	done := chan of int;
+	reqch <-= (time, done);
+	<- done;
+	wc <-= (wn, nil);
+}
+
+str2clicks(s : string) : (int, int)
+{
+	h, m : int = 0;
+	(n, toks) := sys->tokenize(s, ":");
+	if (n > 1) {
+		h = int hd toks;
+		toks = tl toks;
+		n--;
+	}
+	if (n > 0) {
+		m = int hd toks;
+	}
+	h = ((h * NCLICKS) / 12) + ((m * NCLICKS) / (12 * 60));
+	m = (m * NCLICKS)/60;
+	return (h, m);
+}
+
+sethands()
+{
+	for (;;) {
+		(time, rc) := <- reqch;
+		if (time == "reset" || time == "reset\n") {
+			reset();
+			time = "12:00";
+		}
+		(hclk, mclk) := str2clicks(time);
+		for (i := 0; i < 6; i++) {
+			hdelta := clickdistance(hourhand.pos, hclk, NCLICKS);
+			mdelta := clickdistance(minutehand.pos, mclk, NCLICKS);
+			if (hdelta == 0 && mdelta == 0)
+				break;
+			if (hdelta != 0)
+				sethand(hourhand, hdelta);
+			if (mdelta != 0)
+				sethand(minutehand, mdelta);
+		}
+		releaseall();
+		if (rc != nil)
+			rc <- = 1;
+	}
+}
+
+clickdistance(start, stop, mod : int) : int
+{
+	if (start > stop)
+		stop += mod;
+	d := (stop - start) % mod;
+	if (d > mod/2)
+		d -= mod;
+	return d;
+}
+
+setsensortypes(h1, h2 : ref Hand, ls : ref Sys->FD)
+{
+	button := array of byte "b0";
+	light := array of byte "l0";
+
+	sys->seek(h1.sensor, big 0, Sys->SEEKSTART);
+	sys->write(h1.sensor, button, len button);
+	sys->seek(h2.sensor, big 0, Sys->SEEKSTART);
+	sys->write(h2.sensor, button, len button);
+	sys->seek(ls, big 0, Sys->SEEKSTART);
+	sys->write(ls, light, len light);
+}
+
+HOUR_ADJUST : con 1;
+MINUTE_ADJUST : con 3;
+reset()
+{
+	# run the motors until hands are well away from 12 o'clock (below threshold)
+	setsensortypes(hourhand, minutehand, lightsensor);
+	val := readsensor(lightsensor);
+	if (val > OFFTHRESH) {
+		triggered := chan of int;
+		sys->print("wait for hands clear of light sensor\n");
+		spawn lightwait(triggered, lightsensor, 0);
+		forward(minutehand);
+		reverse(hourhand);
+		val = <- triggered;
+		stopall();
+		sys->print("sensor %d\n", val);
+	}
+
+	resethand(hourhand);
+	hourhand.pos += HOUR_ADJUST;
+	resethand(minutehand);
+	minutehand.pos += MINUTE_ADJUST;
+}
+
+sethand(hand : ref Hand, delta : int)
+{
+	triggered := chan of int;
+	dir := 1;
+	if (delta < 0) {
+		dir = -1;
+		delta = -delta;
+	}
+	if (delta > MINCLICKS) {
+		spawn handwait(triggered, hand, delta - MINCLICKS);
+		if (dir > 0)
+			forward(hand);
+		else
+			reverse(hand);
+		<- triggered;
+		stop(hand);
+	hand.pos += dir * readsensor(hand.sensor);
+	} else {
+		startval := readsensor(hand.sensor);
+		if (dir > 0)
+			forward(hand);
+		else
+			reverse(hand);
+		stop(hand);
+		hand.pos += dir * (readsensor(hand.sensor) - startval);
+	}
+	if (hand.pos < 0)
+		hand.pos += NCLICKS;
+	hand.pos %= NCLICKS;
+}
+
+resethand(hand : ref Hand)
+{
+	triggered := chan of int;
+	val : int;
+
+	# run the hand until the light sensor is above threshold
+	sys->print("running hand until light sensor activated\n");
+	spawn lightwait(triggered, lightsensor, 1);
+	forward(hand);
+	val = <- triggered;
+	stop(hand);
+	sys->print("sensor %d\n", val);
+
+	startclick := readsensor(hand.sensor);
+
+	# advance until light sensor drops below threshold
+	sys->print("running hand until light sensor clear\n");
+	spawn lightwait(triggered, lightsensor, 0);
+	forward(hand);
+	val = <- triggered;
+	stop(hand);
+	sys->print("sensor %d\n", val);
+	
+	stopclick := readsensor(hand.sensor);
+	nclicks := stopclick - startclick;
+	sys->print("startpos %d, endpos %d (nclicks %d)\n", startclick, stopclick, nclicks);
+
+	hand.pos = nclicks/2;
+}
+
+stop(hand : ref Hand)
+{
+	sys->seek(hand.motor, big 0, Sys->SEEKSTART);
+	sys->write(hand.motor, hand.stop, len hand.stop);
+}
+
+stopall()
+{
+	msg := array of byte "s0s0s0";
+	sys->seek(allmotors, big 0, Sys->SEEKSTART);
+	sys->write(allmotors, msg, len msg);
+}
+
+releaseall()
+{
+	msg := array of byte "F0F0F0";
+	sys->seek(allmotors, big 0, Sys->SEEKSTART);
+	sys->write(allmotors, msg, len msg);
+}
+
+forward(hand : ref Hand)
+{
+	sys->seek(hand.motor, big 0, Sys->SEEKSTART);
+	sys->write(hand.motor, hand.fwd, len hand.fwd);
+}
+
+reverse(hand : ref Hand)
+{
+	sys->seek(hand.motor, big 0, Sys->SEEKSTART);
+	sys->write(hand.motor, hand.rev, len hand.rev);
+}
+
+readsensor(fd : ref Sys->FD) : int
+{
+	buf := array [4] of byte;
+	sys->seek(fd, big 0, Sys->SEEKSTART);
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return -1;
+	return int string buf[0:n];
+}
+
+handwait(reply : chan of int, hand : ref Hand, clicks : int)
+{
+	blk := array of byte ("b" + string clicks);
+	sys->seek(hand.sensor, big 0, Sys->SEEKSTART);
+	sys->print("handwait(%s)\n", string blk);
+	if (sys->write(hand.sensor, blk, len blk) != len blk)
+	sys->print("handwait write error: %r\n");
+	reply <- = readsensor(hand.sensor);
+}
+
+lightwait(reply : chan of int, fd : ref Sys->FD, on : int)
+{
+	thresh := "";
+	if (on)
+		thresh = "l>" + string ONTHRESH;
+	else
+		thresh = "l<" + string OFFTHRESH;
+	blk := array of byte (thresh);
+	sys->print("lightwait(%s)\n", string blk);
+	sys->seek(fd, big 0, Sys->SEEKSTART);
+	sys->write(fd, blk, len blk);
+	reply <- = readsensor(fd);
+}
--- /dev/null
+++ b/appl/demo/lego/clockreg.sh
@@ -1,0 +1,17 @@
+#!/dis/sh.dis
+load std
+
+port=$1
+DIR=/dis/demo/lego
+pctl forkns newpgrp
+
+cd $DIR
+if { firmdl $port /dis/demo/lego/styx.srec } {
+	legolink $port
+	memfs /tmp
+	cd /tmp
+	mount -o -A /net/legolink /n/remote
+	$DIR/clockface /n/remote
+	echo reset > clockface
+	grid/register -a resource Robot -a name 'Lego Clock' {export /tmp}
+}
--- /dev/null
+++ b/appl/demo/lego/firmdl.b
@@ -1,0 +1,294 @@
+implement RcxFirmdl;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+include "rcxsend.m";
+
+RcxFirmdl : module {
+	init : fn (ctxt : ref Draw->Context, argv : list of string);
+};
+
+sys : Sys;
+bufio : Bufio;
+rcx : RcxSend;
+me : int;
+
+Iobuf : import bufio;
+
+Image : adt {
+	start : int;
+	offset : int;
+	length : int;
+	data : array of byte;
+};
+
+DL_HDR : con 5;			# download packet hdr size
+DL_DATA : con 16rc8;		# download packet payload size
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	me = sys->pctl(Sys->NEWPGRP, nil);
+
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		error(sys->sprint("cannot load bufio module: %r"));
+	rcx = load RcxSend "rcxsend.dis";
+	if (rcx == nil)
+		error(sys->sprint("cannot load rcx module: %r"));
+
+	argv = tl argv;
+	if (len argv != 2)
+		error("usage: portnum file");
+
+	portnum := int hd argv;
+	file := hd tl argv;
+
+	img := getimage(file);
+	cksum := sum(img.data[0:img.length]);
+	sys->print("length %.4x start %.4x \n", img.length, img.start);
+
+	err := rcx->init(portnum, 1);
+	if (err != nil)
+		error(err);
+
+	# delete firmware
+	sys->print("delete firmware\n");
+	reply : array of byte;
+	rmfirm := array [] of {byte 16r65, byte 1, byte 3, byte 5, byte 7, byte 11};
+	reply = rcx->send(rmfirm, len rmfirm, 1);
+	if (reply == nil)
+		error("delete firmware failed");
+	chkreply(reply, array [] of {byte 16r92}, "delete firmware");
+
+	# start download
+	sys->print("start download\n");
+	dlstart := array [] of {byte 16r75,
+					byte (img.start & 16rff),
+					byte ((img.start>>8) & 16rff),
+					byte (cksum & 16rff),
+					byte ((cksum>>8) & 16rff),
+					byte 0,
+	};
+	reply = rcx->send(dlstart, len dlstart, 2);
+	chkreply(reply,array [] of {byte 16r82, byte 0}, "start download");
+
+	# send the image
+	data := array [DL_HDR+DL_DATA+1] of byte;	# hdr + data + 1 byte cksum
+	seqnum := 1;
+	step := DL_DATA;
+	for (i := 0; i < img.length; i += step) {
+		data[0] = byte 16r45;
+		if (seqnum & 1)
+			# alternate ops have bit 4 set
+			data[0] |= byte 16r08;
+		if (i + step > img.length) {
+			step = img.length - i;
+			seqnum = 0;
+		}
+		sys->print(".");
+		data[1] = byte (seqnum & 16rff);
+		data[2] = byte ((seqnum >> 8) & 16rff);
+		data[3] = byte (step & 16rff);
+		data[4] = byte ((step >> 8) & 16rff);
+		data[5:] = img.data[i:i+step];
+		data[5+step] = byte (sum(img.data[i:i+step]) & 16rff);
+		reply = rcx->send(data, DL_HDR+step+1, 2);
+		chkreply(reply, array [] of {byte 16rb2, byte 0}, "tx data");
+		seqnum++;
+	}
+
+	# unlock firmware
+	sys->print("\nunlock firmware\n");
+	ulfirm := array [] of {byte 16ra5, byte 'L', byte 'E', byte 'G', byte 'O', byte 174};
+	reply = rcx->send(ulfirm, len ulfirm, 26);
+	chkreply(reply, array [] of {byte 16r52}, "unlock firmware");
+	sys->print("result: %s\n", string reply[1:]);
+
+	# all done, tidy up
+	killgrp(me);
+}
+
+chkreply(got, expect : array of byte, err : string)
+{
+	if (got == nil || len got < len expect)
+		error(err + ": short reply");
+	# RCX sometimes sets bit 3 of 'opcode' byte to prevent
+	# headers with same opcode having exactly same value - mask out
+	got[0] &= byte 16rf7;
+
+	for (i := 0; i < len expect; i++)
+		if (got[i] != expect[i]) {
+			hexdump(got);
+			error(sys->sprint("%s: reply mismatch at %d", err, i));
+		}
+}
+	
+error(msg : string)
+{
+	sys->print("%s\n", msg);
+	killgrp(me);
+	raise "fail:error" ;
+}
+
+killgrp(pid : int)
+{
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil) {
+		poison := array of byte "killgrp";
+		sys->write(pctl, poison, len poison);
+	}
+}
+
+sum(data : array of byte) : int
+{
+	t := 0;
+	for (i := 0; i < len data; i++)
+		t += int data[i];
+	return t;
+}
+
+hexdump(data : array of byte)
+{
+	for (i := 0; i < len data; i++)
+		sys->print("%.2x ", int data[i]);
+	sys->print("\n");
+}
+
+IMGSTART : con 16r8000;
+IMGLEN : con 16r4c00;
+getimage(path : string) : ref Image
+{
+	img := ref Image (IMGSTART, IMGSTART, 0, array [IMGLEN] of {* => byte 0});
+	iob := bufio->open(path, Sys->OREAD);
+	if (iob == nil)
+		error(sys->sprint("cannot open %s: %r", path));
+
+	lnum := 0;
+	while ((s := iob.gets('\n')) != nil) {
+		lnum++;
+		slen := len s;
+		# trim trailing space
+		while (slen > 0) {
+			ch := s[slen -1];
+			if (ch == ' ' || ch == '\r' || ch == '\n') {
+				slen--;
+				continue;
+			}
+			break;
+		}
+		# ignore blank lines
+		if (slen == 0)
+			continue;
+
+		if (slen < 10)
+			# STNNAAAACC
+			error("short S-record: line " + string lnum);
+
+		s = s[0:slen];
+		t := s[1];
+		if (s[0] != 'S' || t < '0' || t > '9')
+			error("bad S-record format: line " + string lnum);
+
+		data := hex2bytes(s[2:]);
+		if (data == nil)
+			error("bad chars in S-record:  line " + string lnum);
+
+		count := int data[0];
+		cksum := int data[len data - 1];
+		if (count != len data -1)
+			error("S-record length mis-match:  line " + string lnum);
+
+		if (sum(data[0:len data -1]) & 16rff != 16rff)
+			error("bad S-record checksum:  line " + string lnum);
+
+		alen : int;
+		case t {
+		'0' =>
+			# addr[2] mname[10] ver rev desc[18] cksum
+			continue;
+		'1' =>
+			# 16-bit address, data
+			alen = 2;
+		'2' =>
+			# 24-bit address, data
+			alen = 3;
+		'3' =>
+			# 32-bit address, data
+			alen = 4;
+		'4' =>
+			# extension record
+			error("bad S-record type: line " + string lnum);
+		'5' =>
+			# data record count - ignore
+			continue;
+		'6' =>
+			# unused - ignore
+			continue;
+		'7' =>
+			img.start = wordval(data, 1, 4);
+			continue;
+		'8' =>
+			img.start = wordval(data, 1, 3);
+			continue;
+		'9' =>
+			img.start = wordval(data, 1, 2);
+			continue;
+		}
+		addr := wordval(data, 1, alen) - img.offset;
+		if (addr < 0 || addr > len img.data)
+			error("S-record address out of range: line " + string lnum);
+		img.data[addr:] = data[1+alen:1+count];
+		img.length = max(img.length, addr + count -(alen +1));
+	}
+	iob.close();
+	return img;
+}
+
+wordval(src : array of byte, s, l : int) : int
+{
+	r := 0;
+	for (i := 0; i < l; i++) {
+		r <<= 8;
+		r += int src[s+i];
+	}
+	return r;
+}
+
+max(a, b : int) : int
+{
+	if (a > b)
+		return a;
+	return b;
+}
+
+hex2bytes(s : string) : array of byte
+{
+	slen := len s;
+	if (slen & 1)
+		# should be even
+		return nil;
+	data := array [slen/2] of byte;
+	six := 0;
+	dix := 0;
+	while (six < slen) {
+		d1 := hexdigit(s[six++]);
+		d2 := hexdigit(s[six++]);
+		if (d1 == -1 || d2 == -1)
+			return nil;
+		data[dix++] = byte ((d1 << 4) + d2);
+	}
+	return data;
+}
+
+hexdigit(h : int) : int
+{
+	if (h >= '0' && h <= '9')
+		return h - '0';
+	if (h >= 'A' && h <= 'F')
+		return 10 + h - 'A';
+	if (h >= 'a' && h <= 'f')
+		return 10 + h - 'a';
+	return -1;
+}
--- /dev/null
+++ b/appl/demo/lego/legolink.b
@@ -1,0 +1,601 @@
+implement LegoLink;
+
+include "sys.m";
+include "draw.m";
+include "timers.m";
+include "rcxsend.m";
+
+LegoLink : module {
+	init : fn (ctxt : ref Draw->Context, argv : list of string);
+};
+
+POLLDONT : con 0;
+POLLNOW : con 16r02;
+POLLDO : con 16r04;
+
+sys : Sys;
+timers : Timers;
+Timer : import timers;
+datain : chan of array of byte;
+errormsg : string;
+
+error(msg: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", msg);
+	raise "fail:error";
+}
+
+init(nil : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	argv = tl argv;
+	if (len argv != 1)
+		error("usage: legolink portnum");
+
+	timers = load Timers "timers.dis";
+	if (timers == nil)
+		error(sys->sprint("cannot load timers module: %r"));
+
+	portnum := int hd argv;
+	(rdfd, wrfd, err) := serialport(portnum);
+	if (err != nil)
+		error(err);
+
+	# set up our mount file
+	if (sys->bind("#s", "/net", Sys->MBEFORE) == -1)
+		error(sys->sprint("failed to bind srv device: %r"));
+
+	f2c := sys->file2chan("/net", "legolink");
+	if (f2c == nil)
+		error(sys->sprint("cannot create legolink channel: %r"));
+
+	datain = chan of array of byte;
+	send := chan of array of byte;
+	recv := chan of array of byte;
+	timers->init(50);
+	spawn reader(rdfd, datain);
+	consume();
+	spawn protocol(wrfd, send, recv);
+	spawn srvlink(f2c, send, recv);
+}
+
+srvlink(f2c : ref Sys->FileIO, send, recv : chan of array of byte)
+{
+	me := sys->pctl(0, nil);
+	rdfid := -1;
+	wrfid := -1;
+	buffer := array [256] of byte;
+	bix := 0;
+
+	rdblk := chan of (int, int, int, Sys->Rread);
+	readreq := rdblk;
+	wrblk := chan of (int, array of byte, int, Sys->Rwrite);
+	writereq := f2c.write;
+	wrreply : Sys->Rwrite;
+	sendblk := chan of array of byte;
+	sendchan := sendblk;
+	senddata : array of byte;
+
+	for (;;) alt {
+	data := <- recv =>
+		# got some data from brick, nil for error
+		if (data == nil) {
+			# some sort of error
+			if (wrreply != nil) {
+				wrreply <- = (0, errormsg);
+			}
+			killgrp(me);
+		}
+		if (bix + len data > len buffer) {
+			newb := array [bix + len data + 256] of byte;
+			newb[0:] = buffer;
+			buffer = newb;
+		}
+		buffer[bix:] = data;
+		bix += len data;
+		readreq = f2c.read;
+
+	(offset, count, fid, rc) := <- readreq =>
+		if (rdfid == -1)
+			rdfid = fid;
+		if (fid != rdfid) {
+			if (rc != nil)
+				rc <- = (nil, "file in use");
+			continue;
+		}
+		if (rc == nil) {
+			rdfid = -1;
+			continue;
+		}
+		if (errormsg != nil) {
+			rc <- = (nil, errormsg);
+			killgrp(me);
+		}
+		# reply with what we've got
+		if (count > bix)
+			count = bix;
+		rdata := array [count] of byte;
+		rdata[0:] = buffer[0:count];
+		buffer[0:] = buffer[count:bix];
+		bix -= count;
+		if (bix == 0)
+			readreq = rdblk;
+		alt {
+		rc <- = (rdata, nil)=>
+			;
+		* =>
+			;
+		}
+
+	(offset, data, fid, wc) := <- writereq =>
+		if (wrfid == -1)
+			wrfid = fid;
+		if (fid != wrfid) {
+			if (wc != nil)
+				wc <- = (0, "file in use");
+			continue;
+		}
+		if (wc == nil) {
+			wrfid = -1;
+			continue;
+		}
+		if (errormsg != nil) {
+			wc <- = (0, errormsg);
+			killgrp(me);
+		}
+		senddata = data;
+		sendchan = send;
+		wrreply = wc;
+		writereq = wrblk;
+
+	sendchan <- = senddata =>
+		alt {
+		wrreply <- = (len senddata, nil) =>
+			;
+		* =>
+			;
+		}
+		wrreply = nil;
+		sendchan = sendblk;
+		writereq = f2c.write;
+	}
+}
+
+killgrp(pid : int)
+{
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil) {
+		poison := array of byte "killgrp";
+		sys->write(pctl, poison, len poison);
+	}
+	exit;
+}
+
+serialport(port : int) : (ref Sys->FD, ref Sys->FD, string)
+{
+	serport := "/dev/eia" + string port;
+	serctl := serport + "ctl";
+
+	rfd := sys->open(serport, Sys->OREAD);
+	if (rfd == nil)
+		return (nil, nil, sys->sprint("cannot read %s: %r", serport));
+	wfd := sys->open(serport, Sys->OWRITE);
+	if (wfd == nil)
+		return (nil, nil, sys->sprint("cannot write %s: %r", serport));
+	ctlfd := sys->open(serctl, Sys->OWRITE);
+	if (ctlfd == nil)
+		return (nil, nil, sys->sprint("cannot open %s: %r", serctl));
+
+	config := array [] of {
+		"b2400",
+		"l8",
+		"po",
+		"m0",
+		"s1",
+		"d1",
+		"r1",
+	};
+
+	for (i := 0; i < len config; i++) {
+		cmd := array of byte config[i];
+		if (sys->write(ctlfd, cmd, len cmd) <= 0)
+			return (nil, nil, sys->sprint("serial config (%s): %r", config[i]));
+	}
+	return (rfd, wfd, nil);
+}
+
+# reader and nbread as in rcxsend.b
+reader(fd : ref Sys->FD, out : chan of array of byte)
+{
+	# with buf size of 1 there is no need
+	# for overrun code in nbread()
+
+	buf := array [1] of byte;
+	for (;;) {
+		n := sys->read(fd, buf, len buf);
+		if (n <= 0)
+			break;
+		data := array [n] of byte;
+		data[0:] = buf[0:n];
+		out <- = data;
+	}
+	out <- = nil;
+}
+
+overrun : array of byte;
+
+nbread(ms, n : int) : array of byte
+{
+	ret := array[n] of byte;
+	tot := 0;
+	if (overrun != nil) {
+		if (n < len overrun) {
+			ret[0:] = overrun[0:n];
+			overrun = overrun[n:];
+			return ret;
+		}
+		ret[0:] = overrun;
+		tot += len overrun;
+		overrun = nil;
+	}
+	tmr := timers->new(ms, 0);
+loop:
+	while (tot < n) {
+		tmr.reset();
+		alt {
+			data := <- datain =>
+				if (data == nil)
+					break loop;
+				dlen := len data;
+				if (dlen > n - tot) {
+					dlen = n - tot;
+					overrun = data[dlen:];
+				}
+				ret[tot:] = data[0:dlen];
+				tot += dlen;
+			<- tmr.tick =>
+				# reply timeout;
+				break loop;
+		}
+	}
+	tmr.destroy();
+	if (tot == 0)
+		return nil;
+	return ret[0:tot];
+}
+
+consume()
+{
+	while (nbread(300, 1024) != nil)
+		;
+}
+
+# fd: connection to remote client
+# send: from local to remote
+# recv: from remote to local
+protocol(fd : ref Sys->FD, send, recv : chan of array of byte)
+{
+	seqnum := 0;
+	towerdown := timers->new(1500, 0);
+	starttower := 1;
+	tmr := timers->new(250, 0);
+
+	for (;;) {
+		data : array of byte = nil;
+		# get data to send
+		alt {
+		data = <- send =>
+			;
+		<- tmr.tick =>
+			data = nil;
+		<- towerdown.tick =>
+			starttower = 1;
+			continue;
+		}
+			
+		poll := POLLNOW;
+		while (poll == POLLNOW) {
+			reply : array of byte;
+			(reply, poll, errormsg) = datasend(fd, seqnum++, data, starttower);
+			starttower = 0;
+			towerdown.reset();
+			if (errormsg != nil) {
+sys->print("protocol: send error: %s\n", errormsg);
+				tmr.destroy();
+				recv <- = nil;
+				return;
+			}
+			if (reply != nil) {
+				recv <- = reply;
+			}
+			if (poll == POLLNOW) {
+				# quick check to see if we have any more data
+				alt {
+				data = <- send =>
+						;
+				* =>
+						data = nil;
+				}
+			}
+		}
+		if (poll == POLLDO)
+			tmr.reset();
+		else
+			tmr.cancel();
+	}
+}
+
+TX_HDR : con 3;
+DL_HDR : con 5;	# 16r45 seqLSB seqMSB lenLSB lenMSB
+DL_CKSM : con 1;
+LN_HDR : con 1;
+LN_JUNK : con 2;
+LN_LEN : con 2;
+LN_RXLEN : con 2;
+LN_POLLMASK : con 16r06;
+LN_COMPMASK : con 16r08;
+
+
+# send a message (may be empty)
+# wait for the reply
+# returns (data, poll request, error)
+
+datasend(wrfd : ref Sys->FD, seqnum : int, data : array of byte, startup : int) : (array of byte, int, string)
+{
+if (startup) {
+	dummy := array [] of { byte 255, byte 0, byte 255, byte 0};
+	sys->write(wrfd, dummy, len dummy);
+	nbread(100, 100);
+}
+	seqnum = seqnum & 1;
+	docomp := 0;
+	if (data != nil) {
+		comp := rlencode(data);
+		if (len comp < len data) {
+			docomp = 1;
+			data = comp;
+		}
+	}
+
+	# construct the link-level data packet
+	# DL_HDR LN_HDR data cksum
+	# last byte of data is stored in cksum byte
+	llen := LN_HDR + len data;
+	blklen := LN_LEN + llen - 1;	# llen includes cksum
+	ldata := array [DL_HDR + blklen + 1] of byte;
+
+	# DL_HDR
+	if (seqnum == 0)
+		ldata[0] = byte 16r45;
+	else
+		ldata[0] = byte 16r4d;
+	ldata[1] = byte 0;				# blk number LSB
+	ldata[2] = byte 0;				# blk number MSB
+	ldata[3] = byte (blklen & 16rff);		# blk length LSB
+	ldata[4] = byte ((blklen >> 8) & 16rff);	# blk length MSB
+
+	# LN_LEN
+	ldata[5] = byte (llen & 16rff);
+	ldata[6] = byte ((llen>>8) & 16rff);
+	# LN_HDR
+	lhval := byte 0;
+	if (seqnum == 1)
+		lhval |= byte 16r01;
+	if (docomp)
+		lhval |= byte 16r08;
+	
+	ldata[7] = lhval;
+
+	# data (+cksum)
+	ldata[8:] = data;
+
+	# construct the rcx data packet
+	# TX_HDR (dn ~dn) cksum ~cksum
+	rcxlen := TX_HDR + 2*(len ldata + 1);
+	rcxdata := array [rcxlen] of byte;
+
+	rcxdata[0] = byte 16r55;
+	rcxdata[1] = byte 16rff;
+	rcxdata[2] = byte 16r00;
+	rcix := TX_HDR;
+	cksum := 0;
+	for (i := 0; i < len ldata; i++) {
+		b := ldata[i];
+		rcxdata[rcix++] = b;
+		rcxdata[rcix++] = ~b;
+		cksum += int b;
+	}
+	rcxdata[rcix++] = byte (cksum & 16rff);
+	rcxdata[rcix++] = byte (~cksum & 16rff);
+
+	# send it
+	err : string;
+	reply : array of byte;
+	for (try := 0; try < 8; try++) {
+		if (err != nil)
+			sys->print("Try %d (lasterr %s)\n", try, err);
+		err = "";
+		step := 8;
+		for (i = 0; err == nil && i < rcxlen; i += step) {
+			if (i + step > rcxlen)
+				step = rcxlen -i;
+			if (sys->write(wrfd, rcxdata[i:i+step], step) != step) {
+				return (nil, 0, "hangup");
+			}
+
+			# get the echo
+			reply = nbread(300, step);
+			if (reply == nil || len reply != step)
+				# short echo
+				err = "tower not responding";
+
+			# check the echo
+			for (ei := 0; err == nil && ei < step; ei++) {
+				if (reply[ei] != rcxdata[i+ei])
+					# echo mis-match
+					err = "serial comms error";
+			}
+		}
+		if (err != nil) {
+			consume();
+			continue;
+		}
+
+		# wait for a reply
+		replen := TX_HDR + LN_JUNK + 2*LN_RXLEN;
+		reply = nbread(300, replen);
+		if (reply == nil || len reply != replen) {
+			err = "brick not responding";
+			consume();
+			continue;
+		}
+		if (reply[0] != byte 16r55 || reply[1] != byte 16rff || reply[2] != byte 0
+		|| reply[5] != ~reply[6] || reply[7] != ~reply[8]) {
+			err = "bad reply from brick";
+			consume();
+			continue;
+		}
+		# reply[3] and reply [4] are junk, ~junk
+		# put on front of msg by rcx rom
+		replen = int reply[5] + ((int reply[7]) << 8) + 1;
+		cksum = int reply[3] + int reply[5] + int reply[7];
+		reply = nbread(200, replen * 2);
+		if (reply == nil || len reply != replen * 2) {
+			err = "short reply from brick";
+			consume();
+			continue;
+		}
+		cksum += int reply[0];
+		for (i = 1; i < replen; i++) {
+			reply[i] = reply[2*i];
+			cksum += int reply[i];
+		}
+		cksum -= int reply[replen-1];
+		if (reply[replen-1] != byte (cksum & 16rff)) {
+			err = "bad checksum from brick";
+			consume();
+			continue;
+		}
+		if ((reply[0] & byte 1) != byte (seqnum & 1)) {
+			# seqnum error
+			# we have read everything, don't bother with consume()
+			err = "bad seqnum from brick";
+			continue;
+		}
+
+		# TADA! we have a valid message
+		mdata : array of byte;
+		lnhdr := int reply[0];
+		poll := lnhdr & LN_POLLMASK;
+		if (replen > 2) {
+			# more than just hdr and cksum
+			if (lnhdr & LN_COMPMASK) {
+				mdata = rldecode(reply[1:replen-1]);
+				if (mdata == nil) {
+					err = "bad brick msg compression";
+					continue;
+				}
+			} else {
+				mdata = array [replen - 2] of byte;
+				mdata[0:] = reply[1:replen-1];
+			}
+		}
+		return (mdata, poll, nil);
+	}
+	return (nil, 0, err);
+}
+
+
+rlencode(data : array of byte) : array of byte
+{
+	srcix := 0;
+	outix := 0;
+	out := array [64] of byte;
+	val := 0;
+	nextval := -1;
+	n0 := 0;
+
+	while (srcix < len data || nextval != -1) {
+		if (nextval != -1) {
+			val = nextval;
+			nextval = -1;
+		} else {
+			val = int data[srcix];
+			if (val == 16r88)
+				nextval = 0;
+			if (val == 0) {
+				n0++;
+				srcix++;
+				if (srcix < len data && n0 < 16rff + 2)
+					continue;
+			}
+			case n0 {
+			0 =>
+				srcix++;
+			1 =>
+				val = 0;
+				nextval = -1;
+				n0 = 0;
+			2 =>
+				val = 0;
+				nextval = 0;
+				n0 = 0;
+			* =>
+				val = 16r88;
+				nextval = (n0-2);
+				n0 = 0;
+			}
+		}
+		if (outix >= len out) {
+			newout := array [2 * len out] of byte;
+			newout[0:] = out;
+			out = newout;
+		}
+		out[outix++] = byte val;
+	}
+	return out[0:outix];
+}
+
+rldecode(data : array of byte) : array of byte
+{
+	srcix := 0;
+	outix := 0;
+	out := array [64] of byte;
+
+	n0 := 0;
+	val := 0;
+	while (srcix < len data || n0 > 0) {
+		if (n0 > 0)
+			n0--;
+		else {
+			val = int data[srcix++];
+			if (val == 16r88) {
+				if (srcix >= len data)
+					# bad encoding
+					return nil;
+				n0 = int data[srcix++];
+				if (n0 > 0) {
+					n0 += 2;
+					val = 0;
+					continue;
+				}
+			}
+		}
+		if (outix >= len out) {
+			newout := array [2 * len out] of byte;
+			newout[0:] = out;
+			out = newout;
+		}
+		out[outix++] = byte val;
+	}
+	return out[0:outix];
+}
+
+hexdump(data : array of byte)
+{
+	for (i := 0; i < len data; i++)
+		sys->print("%.2x ", int data[i]);
+	sys->print("\n");
+}
--- /dev/null
+++ b/appl/demo/lego/mkfile
@@ -1,0 +1,36 @@
+<../../../mkconfig
+
+TARG=\
+		clockface.dis\
+		firmdl.dis\
+		legolink.dis\
+		rcxsend.dis\
+		timers.dis\
+		styx.srec\
+
+SHTARG=\
+		clockreg.sh\
+
+MODULES=\
+	rcxsend.m\
+
+SYSMODULES= \
+	bufio.m\
+	draw.m\
+	sys.m\
+	timers.m\
+
+DISBIN=$ROOT/dis/demo/lego
+
+<$ROOT/mkfiles/mkdis
+
+$DISBIN/%.srec:	%.srec
+	rm -f $target && cp $stem.srec $target
+
+SHFILES=${SHTARG:%.sh=$DISBIN/%}
+install:V:	$SHFILES
+%.install:V:	$DISBIN/%
+%.installall:V:	$DISBIN/%
+
+$DISBIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/demo/lego/rcxsend.b
@@ -1,0 +1,240 @@
+implement RcxSend;
+
+include "sys.m";
+include "timers.m";
+include "rcxsend.m";
+
+sys : Sys;
+timers : Timers;
+Timer : import timers;
+datain : chan of array of byte;
+debug : int;
+rpid : int;
+wrfd : ref Sys->FD;
+
+TX_HDR : con 3;
+TX_CKSM : con 2;
+
+init(portnum, dbg : int) : string
+{
+	debug = dbg;
+	sys = load Sys Sys->PATH;
+	timers = load Timers "timers.dis";
+	if (timers == nil)
+		 return sys->sprint("cannot load timer module: %r");
+
+	rdfd : ref Sys->FD;
+	err : string;
+	(rdfd, wrfd, err) = serialport(portnum);
+	if (err != nil)
+		return err;
+
+	timers->init(50);
+	pidc := chan of int;
+	datain = chan of array of byte;
+	spawn reader(pidc, rdfd, datain);
+	rpid = <- pidc;
+	consume();
+	return nil;
+}
+
+reader(pidc : chan of int, fd : ref Sys->FD, out : chan of array of byte)
+{
+	pidc <- = sys->pctl(0, nil);
+
+	# with buf size of 1 there is no need
+	# for overrun code in nbread()
+
+	buf := array [1] of byte;
+	for (;;) {
+		n := sys->read(fd, buf, len buf);
+		if (n <= 0)
+			break;
+		data := array [n] of byte;
+		data[0:] = buf[0:n];
+		out <- = data;
+	}
+	if (debug)
+		sys->print("Reader error\n");
+}
+
+send(data : array of byte, n, rlen: int) : array of byte
+{
+	# 16r55 16rff 16r00 (d[i] ~d[i])*n cksum ~cksum
+	obuf := array [TX_HDR + (2*n ) + TX_CKSM] of byte;
+	olen := 0;
+	obuf[olen++] = byte 16r55;
+	obuf[olen++] = byte 16rff;
+	obuf[olen++] = byte 16r00;
+	cksum := 0;
+	for (i := 0; i < n; i++) {
+		obuf[olen++] = data[i];
+		obuf[olen++] = ~data[i];
+		cksum += int data[i];
+	}
+	obuf[olen++] = byte (cksum & 16rff);
+	obuf[olen++] = byte (~cksum & 16rff);
+
+	needr := rlen;
+	if (rlen > 0)
+		needr = TX_HDR + (2 * rlen) + TX_CKSM;
+	for (try := 0; try < 5; try++) {
+		ok := 1;
+		err := "";
+		reply : array of byte;
+
+		step := 8;
+		for (i = 0; ok && i < olen; i += step) {
+			if (i + step > olen)
+				step = olen -i;
+			if (sys->write(wrfd, obuf[i:i+step], step) != step) {
+				if (debug)
+					sys->print("serial tx error: %r\n");
+				return nil;
+			}
+
+			# get the echo
+			reply = nbread(200, step);
+			if (reply == nil || len reply != step) {
+				err = "short echo";
+				ok = 0;
+			}
+
+			# check the echo
+			for (ei := 0; ok && ei < step; ei++) {
+				if (reply[ei] != obuf[i+ei]) {
+					err = "bad echo";
+					ok = 0;
+				}
+			}
+		}
+
+		# get the reply
+		if (ok) {
+			if (needr == 0)
+				return nil;
+			if (needr == -1) {
+				# just get what we can
+				needr = TX_HDR + TX_CKSM;
+				reply = nbread(300, 1024);
+			} else {
+				reply = nbread(200, needr);
+			}
+			if (len reply < needr) {
+				err = "short reply";
+				ok = 0;
+			}
+		}
+		# check the reply
+		if (ok && reply[0] == byte 16r55 && reply[1] == byte 16rff && reply[2] == byte 0) {
+			cksum := int reply[len reply -TX_CKSM];
+			val := reply[TX_HDR:len reply -TX_CKSM];
+			r := array [len val / 2] of byte;
+			sum := 0;
+			for (i = 0; i < len r; i++) {
+				r[i] = val[i*2];
+				sum += int r[i];
+			}
+			if (cksum == (sum & 16rff)) {
+				return r;
+			}
+			ok = 0;
+			err = "bad cksum";
+		} else if (ok) {
+			ok = 0;
+			err = "reply header error";
+		}
+		if (debug && ok == 0 && err != nil) {
+			sys->print("try %d %s: ", try, err);
+			hexdump(reply);
+		}
+		consume();
+	}
+	return nil;
+}
+
+overrun : array of byte;
+
+nbread(ms, n : int) : array of byte
+{
+	ret := array[n] of byte;
+	tot := 0;
+	if (overrun != nil) {
+		if (n < len overrun) {
+			ret[0:] = overrun[0:n];
+			overrun = overrun[n:];
+			return ret;
+		}
+		ret[0:] = overrun;
+		tot += len overrun;
+		overrun = nil;
+	}
+	tmr := timers->new(ms, 0);
+loop:
+	while (tot < n) {
+		tmr.reset();
+		alt {
+			data := <- datain =>
+				dlen := len data;
+				if (dlen > n - tot) {
+					dlen = n - tot;
+					overrun = data[dlen:];
+				}
+				ret[tot:] = data[0:dlen];
+				tot += dlen;
+			<- tmr.tick =>
+				# reply timeout;
+				break loop;
+		}
+	}
+	tmr.destroy();
+	if (tot == 0)
+		return nil;
+	return ret[0:tot];
+}
+
+consume()
+{
+	while (nbread(300, 1024) != nil)
+		;
+}
+
+serialport(port : int) : (ref Sys->FD, ref Sys->FD, string)
+{
+	serport := "/dev/eia" + string port;
+	serctl := serport + "ctl";
+
+	rfd := sys->open(serport, Sys->OREAD);
+	if (rfd == nil)
+		return (nil, nil, sys->sprint("cannot read %s: %r", serport));
+	wfd := sys->open(serport, Sys->OWRITE);
+	if (wfd == nil)
+		return (nil, nil, sys->sprint("cannot write %s: %r", serport));
+	ctlfd := sys->open(serctl, Sys->OWRITE);
+	if (ctlfd == nil)
+		return (nil, nil, sys->sprint("cannot open %s: %r", serctl));
+
+	config := array [] of {
+		"b2400",
+		"l8",
+		"po",
+		"m0",
+		"s1",
+		"d1",
+		"r1",
+	};
+
+	for (i := 0; i < len config; i++) {
+		cmd := array of byte config[i];
+		if (sys->write(ctlfd, cmd, len cmd) <= 0)
+			return (nil, nil, sys->sprint("serial config (%s): %r", config[i]));
+	}
+	return (rfd, wfd, nil);
+}
+hexdump(data : array of byte)
+{
+	for (i := 0; i < len data; i++)
+		sys->print("%.2x ", int data[i]);
+	sys->print("\n");
+}
+
--- /dev/null
+++ b/appl/demo/lego/rcxsend.m
@@ -1,0 +1,4 @@
+RcxSend : module {
+	init: fn (pnum, dbg : int) : string;
+	send : fn (data : array of byte, slen, rlen : int) : array of byte;
+};
--- /dev/null
+++ b/appl/demo/lego/styx.srec
@@ -1,0 +1,329 @@
+S00C0000737479782E7372656340
+S11880006DF06DF16DF26DF35E00967E6D736D726D716D7054AD
+S118801570446F20796F7520627974652C207768656E20492057
+S10B802A6B6E6F636B3F0000F5
+S11880326DF60D761B870D621B8279014000790029F25E00965F
+S1188047AC6B039A0A6F62FFFE1D23470819226B829A084012E9
+S118805C6B039A08790200091D234E060B036B839A086F61FF76
+S1188071FE6B819A0A6B039A08790200097900FFFF1D234F02CC
+S11880860D100B876D7654706DF60D760D02790330026DF3790F
+S118809B01301F79001FF25E0096BC0B87790027C85E0096D480
+S11880B06D7654706DF60D766DF40D047901300679001B625EB4
+S11880C50096DE0D405E00808E5E0080320D0046F86D746D7656
+S11880DA54706DF60D766DF40D041900684B473C0D1147380B7A
+S11880EF040CBA8AD0AA09422E0CB8F00088D090FF1B01684BC7
+S118810447200D11471C0B040CBA8AD0AA0942120D0209220901
+S118811922092009000CBAF200092040D66D746D7654706DF617
+S118812E0D761B871B876DF46DF56FE0FFFE0D146FE0FFFC0DEA
+S1188143444C16FA2D688A6F62FFFE0D230B036FE3FFFC170CE8
+S118815817040B040D444610FA306F63FFFC68BA0B036FE3FFC5
+S118816DFC404C790527101D454F407901000A0D505E0096EA0C
+S11881820D051D454EF0402E0D510D405E0096EA0C8A8A306F7C
+S118819763FFFC68BA0B036FE3FFFC0D510D405E0096FC0D0448
+S11881AC7901000A0D505E0096EA0D050D5546CE6F62FFFC6F38
+S11881C163FFFE19326FE2FFFC6F60FFFC6D756D740B870B87FD
+S11881D66D7654706DF60D761B876DF46DF50D040D156FE2FF1B
+S11881EBFE7900000A5E00972269846F8500026F62FFFE6F8241
+S118820000046F6200046F82000619226F8200086A0AA40A8AB5
+S1188215016A8AA40A6D756D740B876D7654706DF60D765E006D
+S118822A979C6A0AA40A8AFF6A8AA40A6D7654706DF60D7669C5
+S118823F00790198885E008FD6790000016D7654706DF60D76C2
+S11882541B871B876DF46DF50D056FE1FFFE6FE2FFFC40266990
+S1188269546F61FFFC0D406F62FFFE5D200D00470E6F4200082A
+S118827E69D20D405E0082244006790500080945695246D66DFD
+S1188293756D740B870B876D7654706DF60D761B871B876DF426
+S11882A86DF56FE0FFFE6FE1FFFC19557904A3DC6F62FFFC6F1F
+S11882BD61FFFE0D405E0082508C0E94000B05790200021D25D0
+S11882D24FE46D756D740B870B876D7654706DF60D7679030070
+S11882E70C19376DF46DF56FE2FFFE6F6400040D1147045A0077
+S11882FC83FE6E080002F00088F990FF6FE0FFFC6F65FFFC094E
+S11883115509550955190509557902A3D009520D256F62FFFE7D
+S118832646045A0083FE684BF300790200621D23470C79020088
+S118833B6C1D23470A5A0083FE79020060400219226FE2FFF8B1
+S11883500B046F63FFFE1B030D3346045A0083FE684AAA3E46D3
+S118836504FA014006AA3C460C18AA6EEAFFFB0B041B034006FB
+S118837AFA016EEAFFFB0D3346045A0083FE7901823A0D50881D
+S118838F0C900019226FE3FFF45E0082506F62FFFC8A00921091
+S11883A46FE2FFF66F61FFF6790019C45E0096DEFA0168DA6EE2
+S11883B96AFFF96EDA00016F63FFF40D310D405E0080DC6FD0B7
+S11883CE000A19226FD200026FD200046EDA0006FA016EDA0038
+S11883E3086E6AFFFB6EDA00096F61FFF6790019465E0096DEE7
+S11883F86F60FFFE40047900FFFF6D756D747903000C09376DED
+S118840D7654706DF60D767903000619376DF46DF56FE0FFFE55
+S11884226FE1FFFC0D256F6400085E0090340D031D45443219C6
+S1188437546F6000041D0443046F6400046F62000609526FE243
+S118844C00060D300B800B000D426F6100066FE3FFFA5E0097D9
+S1188461AE6F63FFFA4002194468BC0D420C2A18226EBA0001DE
+S118847618AA6EBA00020B840B046DF419226DF26F62FFFC6F2D
+S118848B61FFFEF80F5E0090480B870B876D756D7479030006D4
+S11884A009376D7654706DF60D767903000C19376DF46DF56FEC
+S11884B5E0FFF66FE1FFF40D257904FFF809646F6100060D4060
+S11884CA5E00812C6DF06DF46F6200046DF20D526F61FFF46F0B
+S11884DF60FFF65E0084108F0697006D756D747903000C093786
+S11884F46D7654706DF60D766E0A0009471019116F0300046FFB
+S118850902000A1D234514400E19116F0300046F02000A1D230B
+S118851E4404790100010D106D7654706DF60D761B876DF46D67
+S1188533F56FE1FFFE0D256E0A0002F2008AF992FF0D240944BD
+S118854809440944192409447902A3D009420D246E4A00084686
+S118855D067900FFFF404E0D405E0084F80D0047206F420004AA
+S11885726DF26F6200066DF26F6200040D516F60FFFE5E00847A
+S1188587A60B870B8740226F6200066DF26F6200040D516F6077
+S118859CFFFE5E0081DA6F42000C6F8200086FC0000C0B871974
+S11885B1006D756D740B876D7654706DF60D761B876DF46DF56A
+S11885C67903A400683A4D045A008686EA7F68BA19440D4509E0
+S11885DB5509550955194509557902A3D009520D256E5A00086F
+S11885F00D430B036FE3FFFE0CAA46045A0086768C0094100D32
+S1188605520D41790014C05E0096AC0D520D41790014C05E0077
+S118861A96AC0D520D41790014C05E0096AC0D520D4179001431
+S118862FC05E0096AC0D505E0084F80D00473840306F54000CD0
+S11886446F4200086FD2000C6F41000269406F5200046DF26F29
+S11886594200066DF26F4200045E0084A60D405E0082240B8741
+S118866E0B876F52000C46CA6F64FFFE790200021D244E045A4A
+S11886830085D86D756D740B876D7654706DF60D766DF46DF5DC
+S11886980D050D246F600004680BF300790200661D2347244E73
+S11886AD127902002D1D234750790200461D23472440327902CA
+S11886C200721D23470E0B021D23470E4022790200014010794F
+S11886D7020002400A7902000340047902000469926E0A000187
+S11886EC8AD0AA074304190040146E0A0001F2008AD092FF69F7
+S1188701C2FA0168DA790000016D756D746D7654706DF60D7696
+S11887167903001A19376DF46DF56FE2FFEE0D1147045A008818
+S118872B1A6EE9FFF26EE9FFF16EE9FFF06E0A0002AA054704D2
+S11887405A0087CC790200066F60FFEE1D2047045A00881A7939
+S118875504FFF409647905FFFA09657902FFF009626FE2FFECB1
+S118876A6F6300046DF30D420D516F60FFEC5E0086900B870D46
+S118877F0046045A00881A0D611B811B817900FFF109606F6252
+S118879400040B826DF21B826FE200047902FFF609625E00862B
+S11887A9900B870D00476A0D611B817900FFF209606F63000424
+S11887BE0B830B836DF37902FFF809624044790200026F63FF77
+S11887D3EE1D2346426E0B0002F3001B837900FFF409606FE0A7
+S11887E8FFEA0D3209227905FFFA09650D5109217904FFF00943
+S11887FD640D4009306F6300046DF36F63FFEA09230D325E00BF
+S118881286900B870D0046067900FFFF405019337904FFF40980
+S1188827647900FFF009606FE0FFE86F60FFE86C0A6FE0FFE86B
+S118883C0CAA471E0D318900912069426DF2695279001A4E6F7B
+S1188851E3FFE65E0096BC0B876F63FFE60B840B850B037902A5
+S118886600021D234FC66F60FFEE6D756D747903001A09376DE0
+S118887B7654706DF60D760D036B00A408400269000D00470896
+S11888906F0200021D3246F26D7654706DF60D761B876DF46DD8
+S11888A5F50D056FE1FFFE7900000E5E0097220D0446087900F0
+S11888BA02135E0080B418AA6ECA00046FC500026B02A4086948
+S11888CFC20D4088059000790200086F61FFFE5E0097AE6B8482
+S11888E4A4080D406D756D740B876D7654706DF60D766F03002E
+S11888F9026F1200021D23470419004004790000016D765470D8
+S118890E6DF60D766DF40D04790088F20D415E00829E7903A419
+S118892308401269301D40460A690269B25E00979C400E0D0326
+S1188938693246EA790002325E0080B46D746D7654706DF60D24
+S118894D766DF46DF50D040D156F4200044608790002395E0090
+S118896280B46F440004401C6940790198DC5E0097D20D004703
+S11889770A0D5546040D40400C1B058C0A9400694246E0190064
+S118898C6D756D746D7654706DF60D761B876DF46DF50D056F9C
+S11889A1E1FFFE6E5A000446226E5A0005AA0943087900024B1A
+S11889B65E0080B46E5A0005F20009226F229A0C6F220004461A
+S11889CB067900FFFF403E0D20690247360D0469406F61FFFEFC
+S11889E05E0097D20D00461E6E4A00026EDA00056F4200041872
+S11889F5BB0D224702FB806EDB000879000001400A8C0A94007C
+S1188A0A694246CC19006D756D740B876D7654706DF60D761B80
+S1188A1F876DF46DF56FE0FFFE0D157902007419110D505E00B2
+S1188A3498086F62FFFE69210D505E00981E0D50881C900079B6
+S1188A490400050D427901995E5E0097AE0D50883890000D42AC
+S1188A5E7901995E5E0097AE6F63FFFE6E3A00026EDA00546F67
+S1188A73320004FBB60D224702FB6D6EDB005CFA016EDA005DDE
+S1188A886F63FFFE6F32000418BB0D224702FB806EDB005F6D86
+S1188A9D756D740B876D7654706DF60D766DF46DF50D040D1555
+S1188AB26E4A000447067900FFFF40266E4A0005AA0943087991
+S1188AC700026B5E0080B46E4A0005F20009226F209A0C0D512A
+S1188ADC5E008A1A790000016D756D746D7654706DF60D766E47
+S1188AF10A000446107369460C6E0A0008470A89FFA901420491
+S1188B061900400AFA016E8A0004790000016D7654706DF60D6B
+S1188B1B767903000A19376DF46DF56FE0FFFE6FE1FFFC6FE24A
+S1188B30FFFA6E0A0005AA094308790002875E0080B46F60FF56
+S1188B45FE6E0A0005F20009226F229A0C6FE2FFF86E0A000880
+S1188B5AEA8046045A008C3A6E0A00044720790100746F60008E
+S1188B6F045E0098320D004610790100746F60FFFA5E00983280
+S1188B840D0047087900FFFF5A008C6C790100746F6000045E94
+S1188B990098446FE00004790100746F60FFFA5E0098446FE055
+S1188BAEFFFA5E0090346FE0FFF66F64FFF60B840B041955403B
+S1188BC30C0D415E008A1A8C7494000B056F6200041D2544122C
+S1188BD86F61FFFA09516F60FFF85E00894A0D0046DA0D5209D5
+S1188BED22092209221952092209220925095509556F63FFF685
+S1188C0268BD0D520C2A10021E226EBA000118AA6EBA00020B2D
+S1188C17850B056DF519226DF26F63FFFE6F3200026F61FFFC76
+S1188C2CF80F5E00904819000B870B8740326F60FFF86F03000B
+S1188C410647246F6200046DF26F60FFFA6DF06F60FFFE6F0213
+S1188C5600026F61FFFC6F60FFF85D300B870B8740047900FF05
+S1188C6BFF6D756D747903000A09376D7654706DF60D761B8739
+S1188C806DF46DF50D046FE1FFFE0D256E4A0008EA8046066EA4
+S1188C954A000446067900FFFF403C6E4A0005AA094308790005
+S1188CAA02B35E0080B46E4A0005F20009226F209A0C6F0200EA
+S1188CBF0847166F6200046DF26F0300080D526F61FFFE5D30D0
+S1188CD40B8740047900FFFF6D756D740B876D7654706DF60DCE
+S1188CE9761B876DF46DF56F63000446087900FFFF5A008DD243
+S1188CFE0D25194419116F6300040D5209326FE2FFFE40386806
+S1188D135BAB8846186F6200041B821D214CD4688B0B0018AACB
+S1188D28688A0B000B8140180CBB46040B0440106F6200041BF1
+S1188D3D021D214CB4688B0B000B010B0519336F62FFFE1D2567
+S1188D52450479030001685A46040D3347B2790200011D2447F9
+S1188D67124E060D4447584038790200021D24471A402E6F62C7
+S1188D7C00041B021D214D045A008CF618AA688A0B000B014047
+S1188D91346F6200041B821D214D045A008CF618AA688A0B00F9
+S1188DA640186F6200041B821D214D045A008CF6FA88688A0B00
+S1188DBB000CCA8AFE688A0B000B8119440D3346045A008D12D8
+S1188DD00D106D756D740B876D7654706DF60D761B876DF46D1B
+S1188DE5F50D040D2519226FE2FFFE0D55475E681AAA8847149E
+S1188DFA68CA0B010B046F62FFFE0B026FE2FFFE1B0540E20B9D
+S1188E0F016818F0000D00461468CA0B046F62FFFE0B026FE205
+S1188E24FFFE0B011B8540C40D030B836F62FFFE09326FE2FF91
+S1188E39FE0D321B030B011B854FAC188868C80B040D321B03E2
+S1188E4E4EF6409E6F60FFFE6D756D740B876D7654706DF60DB1
+S1188E63766DF46DF56A0CA40E47045A008F0079059F586B027F
+S1188E78A4106DF20D527901009379009EC35E008CE66B80A429
+S1188E8D060B870D004C206B02A4100D5179009EC35E0097AEBF
+S1188EA26B02A4106B82A40679029EC268AC0D23400879039E7E
+S1188EB7C2FA0868BA6A0AA40A47047D3070206838F0006A0A0E
+S1188ECCA40F4602C80168B86B00A4060D020B026B82A4066A77
+S1188EE10BA4076A8B9EC00C2A10021E226A8A9EC10B800B00FE
+S1188EF66B80A406FA016A8AA40E6D756D746D7654706DF60D53
+S1188F0B766DF46DF55E008E606B02A4106B82A40C79059EC02E
+S1188F206B04A406471A6DF46DF51922790117767900343E5E70
+S1188F350098560B870B870C8846E66D756D746D7654706DF684
+S1188F4A0D766B02A4106B03A40C1D32430E79009F580D3109F5
+S1188F5F0119325E0097AE6B02A40C6B03A41019230D326B8263
+S1188F74A41018AA6A8AA40E6D7654706DF60D766DF46DF50D6B
+S1188F89246F6500046B03A41079029F5809320D2368B86EB98D
+S1188F9E00010C1918116EB900020D44470E0B830B030D520D94
+S1188FB3410D305E0097AE6B02A4100B820B0209526B82A410CD
+S1188FC818AA6A8AA40E6D756D746D7654706DF60D766DF46D0A
+S1188FDDF50D156B04A41079029F5809420D24FA0368CA6EC8EE
+S1188FF200010C0818006EC800020D505E009874790200401D62
+S1189007204F020D200B840B040D020D510D405E0097AE6B024A
+S118901CA4108A4392006B82A41018AA6A8AA40E6D756D746DEF
+S11890317654706DF60D766B00A41079029F5D09020D206D7655
+S118904654706DF60D766DF46DF56F6400046B03A41079059F8E
+S118905B5809350D5368B86EB900010C1918116EB900026EBA1F
+S118907000030C2A18226EBA00040D4447108B0593006F6200AC
+S1189085060D410D305E0097AE6B02A4108A0592006F60000687
+S118909A09026B82A41018AA6A8AA40E6D756D746D7654706DD2
+S11890AFF60D7669021D12470419004004790000016D765470CC
+S11890C46DF60D760D01790090AE5E00829E6D7654706DF60D53
+S11890D9767903000E19376DF46DF50D050D14790000021D049C
+S11890EE4E08790003935E0080B46C5A6EEAFFFD6E5B00010C82
+S1189103B318BB685AF20014AB14236FE3FFFA1B841B040B858A
+S11891186E6BFFFDF3000D33470A790200041D23470E40446DE0
+S118912DF319226F61FFFAF801402E790200021D244708790045
+S1189142039D5E0080B46E5800010C801888685AF20014A8146B
+S1189157205E0090C419226DF26F61FFFAF8055E008F800B87CE
+S118916C5A009496790200011D244E08790003A45E0080B46E33
+S11891815800010C801888685AF20014A814206FE0FFF81B84C7
+S11891960B855E00887E6FE0FFF46E6BFFFDF3008BFA93FF7932
+S11891AB0200161D2343045A00948E09336F3299DA592079024C
+S11891C000381D244708790003AA5E0080B46F63FFF447087989
+S11891D50199635A0094567901988E6F60FFF85E00889C6FE009
+S11891EAFFF4790200086DF2880590006DF06F62FFF86F61FF86
+S11891FFFAF81D5A0094840D444708790003B65E0080B46F62A1
+S1189214FFF446087901996E5A0094566F60FFF45E00890E6E16
+S11892296BFFFDAB1446087901997A5A00945619226DF26DF2EE
+S118923E6F62FFF86F61FFFAF8135A009484790200021D244704
+S118925308790003C45E0080B46E5C00010CC418CC685AF200F5
+S118926814AC14240D405E00887E6F62FFF446045A0092186EC4
+S118927D2A00044708790199875A0094560D0047045A0091D466
+S11892926F61FFF4890591000D405E00889C19226DF26DF26FAA
+S11892A762FFF86F61FFFAF8075A0094847902001C1D244708F4
+S11892BC790003D35E0080B40D516F60FFF45E0089940D0046CA
+S11892D108790199935A009456790200086DF26F63FFF48B055B
+S11892E693006DF36F62FFF86F61FFFAF8095A0094840D4447E0
+S11892FB08790003DA5E0080B47904A3580D416F60FFF45E0084
+S11893108AA60D004608790199A05A009456790200746DF26D07
+S1189325F46F62FFF86F61FFFAF8175A009484790200211D244C
+S118933A4708790003E25E0080B4790199AB5A0094567902005E
+S118934F011D244708790003E65E0080B468596F60FFF45E009F
+S11893648AEC0D004608790199B85A009456790200086DF26FBF
+S118937960FFF4880590006DF06F62FFF86F61FFFAF80B5A0020
+S118938E94847902000A1D244708790003ED5E0080B46E5A00D6
+S11893A3010CA218AA6859F1006E5B00090CB318BB6E5800085C
+S11893B8F000148B14036DF3149A14126F61FFFA6F60FFF45ED9
+S11893CD008B180B870D004D045A009496790199C35A00945656
+S11893E27902000A1D244E08790003F45E0080B46E5A00010C7F
+S11893F7A218AA6FE2FFF6685AF2006F60FFF614A814206FE0FC
+S118940CFFF66E5B00090CB318BB6E5A0008F20014AB14238DA9
+S11894210B95008CF594FF1D434710790003F96FE3FFF25E00B1
+S118943680B46F63FFF26DF50D326F61FFF66F60FFF45E008C14
+S118944B7A0B870D004C0E790199CE6F60FFFA5E008FD64036B3
+S11894606EE8FFFE0C0810001E006EE8FFFF790200026DF20D21
+S1189475621B826DF26F62FFF86F61FFFAF8115E0090480B871E
+S118948A0B874008790004045E0080B46D756D747903000E0986
+S118949F376D7654706DF60D766DF46DF50D04684BF3007902FB
+S11894B400451D23470C7902004D1D2347045A009548790200C2
+S11894C9051D214708790004125E0080B479039A28683DF500FF
+S11894DE6A0A9A290CA218AA14AD14250D5209326E4B00036E10
+S11894F3AB000118BB6A0AA40F4602FB016A8BA40F5E008F4899
+S1189508790200011D254F326A0A9A2AEA08471E79019A2B79C4
+S118951D049AC00D521B020D405E008DDC0D050D510D405E002C
+S118953290D6400C79009A2B0D511B015E0090D65E008F0840BD
+S1189547087900042C5E0080B46D756D746D7654706DF60D7678
+S118955C7903005219376DF45E00970E7900FFB0096079020068
+S11895714019115E0098087902A4007901FFB6096179003B9A73
+S11895865E0096AC790029645E0096D4790014985E0096D479F8
+S118959B0200016DF26DF27902FFB609627901FFB4096179004B
+S11895B030D05E0098560B870B87790230026DF2192279013041
+S11895C51F79001FF25E0096BC0B877901300779001B625E009D
+S11895DA96DE790027C85E0096D419446DF479029A2879011748
+S11895EF717900327C5E0096BC0B876A8CA40F6B84A4066B8458
+S1189604A4106A8CA40E405C19227901FFAF0961790034265E57
+S11896190096AC6E6AFFAFF2000CAA473E18AA6EEAFFAE790201
+S118962EFFAE09626DF2790200107901FFF00961790033B05E94
+S11896430096BC0B876E69FFAEF1006B029A200B026B829A20DA
+S11896581B017900FFF009605E0094A45E0085BC5E0080320DBA
+S114966D00469C19006D747903005209376D76547057
+S118967E6DF60D7679029A207903A4121D32470A188868A80B2B
+S1189693021D3246F85E009558FA016A8AFFCC6B0200005D2040
+S10796A86D76547013
+S11396AC6DF66DF20D165D000D600B876D765470C2
+S11896BC6DF66F7300046DF36DF20D165D000D600B870B876D0F
+S10696D176547058
+S10D96D46DF65D000D606D765470B4
+S10F96DE6DF60D165D000D606D76547085
+S11596EA6DF56DF60D060D155F520D606D766D755470C9
+S11596FC6DF56DF60D060D155F500D606D766D755470B9
+S118970E6DF60D767903A4127902EF00193269B26D7654706D46
+S1189723F60D766DF46DF50D040B04ECFE0B841D04455C79001D
+S1189738A4120D01404C69024D4209217902EEFF1D2142144068
+S118974D0E690209326982691209211D51420469134CEE6903E9
+S11897621D4345220D428A0692001D23450A0D02094219436908
+S1189777A369846902C28069820B804010E27F09210D107905B0
+S118978CEEFF1D5043AC19006D756D746D7654706DF60D766FA3
+S11097A102FFFEE27F6F82FFFE6D765470C2
+S11897AE6DF60D766DF40D140D010D0309231D31470A6C4A6833
+S11297C38A0B001D3046F60D106D746D765470D0
+S11897D26DF60D76401C680B681A1CAB430679000001401E1C43
+S11897E7AB44067900FFFF40140B000B01680A46E0681A19005F
+S10F97FC0CAA47047900FFFF6D7654703E
+S11898086DF60D760D0309231D3047066CB91D3046FA6D7654A2
+S104981D70D6
+S117981E6DF60D760D036C1A68BA0B030CAA46F66D765470ED
+S11598326DF56DF60D060D155F4C0D606D766D75547085
+S11598446DF56DF60D060D155F4E0D606D766D75547071
+S11898566DF66F7300066DF36F7300066DF36DF20D165D000D1A
+S10C986B608F0697006D765470BD
+S11798746DF60D76193340020B036C0A46FA0D306D765470C0
+S1189888726573657400000000800000000098DC000099120005
+S118989D00000098DA020000000000871298D803000000000032
+S11898B2871298D6040000000000871298D20500000000008703
+S11898C712000000000000000000003031320032003100300050
+S11898DC2E2E000098DC000099120000000098DA0700000085FA
+S11898F12A82E098D808000000852A82E098D609000000852A23
+S118990682E00000000000000000000098DC0000994800000091
+S118991B0099410100989600000000993A060098E000000000D9
+S11899300000000000000000000073656E736F72006D6F746FC5
+S1189945720000995C00009912000000000000000000000000F7
+S118995A00002F006C65676F0066696420696E20757365006E19
+S118996F6F2073756368206669640063616E27742072656D6FAA
+S118998476650063616E277420636C6F6E65006E6F20737563A9
+S118999968206E616D650063616E277420737461740063616EB1
+S11899AE2774206372656174650063616E2774206F70656E00D2
+S11899C363616E277420726561640063616E277420777269744F
+S11899D86500924C948E92B4948E934C948E9334948E93909408
+S11899ED8E93E2948E9206948E9206948E92F8948E948E948ED8
+S1099A02948E948E91BEC7
+S1189A080000FFFF9948991C98A098AA98B498BE992698EA98BC
+S1069A1DF498FEB8
+S90380007C
--- /dev/null
+++ b/appl/demo/lego/timers.b
@@ -1,0 +1,263 @@
+# Chris Locke. June 2000
+
+# TODO: for auto-repeat timers don't set up a new sender
+# if there is already a pending sender for that timer.
+
+implement Timers;
+
+include "sys.m";
+include "timers.m";
+
+RealTimer : adt {
+	t : ref Timer;
+	nticks : int;
+	rep : int;
+	nexttick: big;
+	tick : chan of int;
+	sender : int;
+};
+
+Sender : adt {
+	tid : int;
+	idle : int;		# set by sender() when done, reset by main when about to assign work
+	ctl : chan of chan of int;
+};
+
+sys : Sys;
+acquire : chan of int;
+timers := array [4] of ref RealTimer;
+senders := array [4] of ref Sender;
+curtick := big 0;
+tickres : int;
+
+init(res : int)
+{
+	sys = load Sys Sys->PATH;
+	acquire = chan of int;
+	tickres = res;
+	spawn main();
+}
+
+new(ms, rep : int) : ref Timer
+{
+	acquire <- = 1;
+	t := do_new(ms, rep);
+	<- acquire;
+	return t;
+}
+
+Timer.destroy(t : self ref Timer)
+{
+	acquire <- = 1;
+	do_destroy(t);
+	<- acquire;
+}
+
+Timer.reset(t : self ref Timer)
+{
+	acquire <- = 1;
+	do_reset(t);
+	<- acquire;
+}
+
+Timer.cancel(t : self ref Timer)
+{
+	acquire <- = 1;
+	do_cancel(t);
+	<- acquire;
+}
+
+# only call under lock
+#
+realtimer(t : ref Timer) : ref RealTimer
+{
+	if (t.id < 0 || t.id >= len timers)
+		return nil;
+	if (timers[t.id] == nil)
+		return nil;
+	if (timers[t.id].t != t)
+		return nil;
+	return timers[t.id];
+}
+
+
+# called under lock
+#
+do_destroy(t : ref Timer)
+{
+	rt := realtimer(t);
+	if (rt == nil)
+		return;
+	clearsender(rt, t.id);
+	timers[t.id] = nil;
+}
+
+# called under lock
+#
+do_reset(t : ref Timer)
+{
+	rt := realtimer(t);
+	if (rt == nil)
+		return;
+	clearsender(rt, t.id);
+	rt.nexttick = curtick + big (rt.nticks);
+	startclk = 1;
+}
+
+# called under lock
+#
+do_cancel(t : ref Timer)
+{
+	rt := realtimer(t);
+	if (rt == nil)
+		return;
+	clearsender(rt, t.id);
+	rt.nexttick = big 0;
+}
+
+# only call under lock
+#
+clearsender(rt : ref RealTimer, tid : int)
+{
+	# check to see if there is a sender trying to deliver tick
+	if (rt.sender != -1) {
+		sender := senders[rt.sender];
+		rt.sender = -1;
+		if (sender.tid == tid && !sender.idle) {
+			# receive the tick to clear the busy state
+			alt {
+				<- rt.tick =>
+					;
+				* =>
+					;
+			}
+		}
+	}
+}
+
+# called under lock
+do_new(ms, rep : int) : ref Timer
+{
+	# find free slot
+	for (i := 0; i < len timers; i++)
+		if (timers[i] == nil)
+			break;
+	if (i == len timers) {
+		# grow the array
+		newtimers := array [len timers * 2] of ref RealTimer;
+		newtimers[0:] = timers;
+		timers = newtimers;
+	}
+	tick := chan of int;
+	t := ref Timer(i, tick);
+	nticks := ms / tickres;
+	if (nticks == 0)
+		nticks = 1;
+	rt := ref RealTimer(t, nticks, rep, big 0, tick, -1);
+	timers[i] = rt;
+	return t;
+}
+
+startclk : int;
+stopclk : int;
+
+main()
+{
+	clktick := chan of int;
+	clkctl := chan of int;
+	clkstopped := 1;
+	spawn ticker(tickres, clkctl, clktick);
+
+	for (;;) alt {
+	<- acquire =>
+		# Locking
+		acquire <- = 1;
+
+		if (clkstopped && startclk) {
+			clkstopped = 0;
+			startclk = 0;
+			clkctl <- = 1;
+		}
+
+	t := <- clktick =>
+		if (t == 0) {
+			stopclk = 0;
+			if (startclk) {
+				startclk = 0;
+				clkctl <- = 1;
+			} else {
+				clkstopped = 1;
+				continue;
+			}
+		}
+		curtick++;
+		npend := 0;
+		for (i := 0; i < len timers; i++) {
+			rt := timers[i];
+			if (rt == nil)
+				continue;
+			if (rt.nexttick == big 0)
+				continue;
+			if (rt.nexttick > curtick) {
+				npend++;
+				continue;
+			}
+			# Timeout - arrange to send the tick
+			if (rt.rep) {
+				rt.nexttick = curtick + big rt.nticks;
+				npend++;
+			} else
+				rt.nexttick = big 0;
+			si := getsender();
+			s := senders[si];
+			s.tid = i;
+			s.idle = 0;
+			rt.sender = si;
+			s.ctl <- = rt.tick;
+
+		}
+		if (!npend)
+			stopclk = 1;
+	}
+}
+
+getsender() : int
+{
+	for (i := 0; i < len senders; i++) {
+		s := senders[i];
+		if (s == nil || s.idle == 1)
+			break;
+	}
+	if (i == len senders) {
+		newsenders := array [len senders * 2] of ref Sender;
+		newsenders[0:] = senders;
+		senders = newsenders;
+	}
+	if (senders[i] == nil) {
+		s := ref Sender (-1, 1, chan of chan of int);
+		spawn sender(s);
+		senders[i] = s;
+	}
+	return i;
+}
+
+sender(me : ref Sender)
+{
+	for (;;) {
+		tickch := <- me.ctl;
+		tickch <- = 1;
+		me.idle = 1;
+	}
+}
+
+ticker(ms : int, start, tick : chan of int)
+{
+	for (;;) {
+		<- start;
+		while (!stopclk) {
+			sys->sleep(ms);
+			tick <- = 1;
+		}
+		tick <- = 0;
+	}
+}
--- /dev/null
+++ b/appl/demo/lego/timers.m
@@ -1,0 +1,15 @@
+Timers : module{
+	Timer : adt {
+		id : int;
+		tick : chan of int;
+
+		reset : fn (t : self ref Timer);
+		cancel : fn (t : self ref Timer);
+		destroy : fn (t : self ref Timer);
+	};
+
+	init : fn (res : int);
+	new : fn(ms, rep : int) : ref Timer;
+};
+
+
--- /dev/null
+++ b/appl/demo/mkfile
@@ -1,0 +1,14 @@
+<../../mkconfig
+
+DIRS=\
+	camera\
+	chat\
+	cpupool\
+	lego\
+	ns\
+	odbc\
+	spree\
+	whiteboard\
+
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/demo/ns/mkfile
@@ -1,0 +1,27 @@
+<../../../mkconfig
+
+TARG=\
+		ns.dis\
+
+SHTARG=\
+		runns.sh\
+
+MODULES=\
+
+SYSMODULES= \
+	arg.m\
+	draw.m\
+	sh.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/demo/ns
+
+<$ROOT/mkfiles/mkdis
+
+SHFILES=${SHTARG:%.sh=$DISBIN/%}
+install:V:	$SHFILES
+%.install:V:	$DISBIN/%
+%.installall:V:	$DISBIN/%
+
+$DISBIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/demo/ns/ns.b
@@ -1,0 +1,124 @@
+implement Ns;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+include "sh.m";
+	sh: Sh;
+
+ns : list of string;
+
+Ns: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	sh = load Sh Sh->PATH;
+	if (sh == nil)
+		badmod(Sh->PATH);
+	# sys->pctl(sys->FORKNS, nil);
+	sys->unmount(nil, "/n/remote");
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmod(Arg->PATH);
+
+	arg->init(argv);
+	arg->setusage("ns [-v] [-r relpath] paths...");
+	verbose := 0;
+	relpath := "";
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'v' =>
+			verbose = 1;
+		'r' =>
+			relpath = arg->earg();
+			if (relpath == nil)
+				arg->usage();
+			if (relpath[len relpath - 1] != '/')
+				relpath[len relpath] = '/';
+		* =>
+			arg->usage();
+		}
+	}
+
+	ns = arg->argv();
+	arg = nil;
+	if (ns == nil) {
+		sys->fprint(fdout(), "error no namespace selected\n");
+		exit;
+	}
+	spawn buildns(relpath, verbose);
+}
+
+fdout(): ref sys->FD
+{
+	return sys->fildes(1);
+}
+
+buildns(relpath: string, verbose: int)
+{
+	# sys->pctl(sys->FORKNS, nil);
+	if (sh->run(nil, "memfs"::"/n/remote"::nil) != nil) {
+		sys->fprint(fdout(), "error MemFS mount failed\n");
+		exit;
+	}
+	for (tmpl := ns; tmpl != nil; tmpl = tl tmpl) {
+		nspath := hd tmpl;
+		if (nspath[len nspath - 1] != '/')
+			nspath[len nspath] = '/';
+
+		bindpath := nspath;
+		if (bindpath[:len relpath] == relpath) {
+			bindpath = "/n/remote/"+bindpath[len relpath:];
+			if (createdir(bindpath) != -1) {
+				if (sys->bind(nspath, bindpath, sys->MBEFORE | sys->MCREATE) == -1) {
+					if (sys->bind(nspath, bindpath, sys->MBEFORE) == -1)
+						sys->fprint(fdout(), "error bind failed %s: %r\n",bindpath);
+					else if (verbose)
+						sys->fprint(fdout(), "data nspath %s\n", nspath);
+				}
+				else if (verbose)
+					sys->fprint(fdout(), "data nspath %s\n", nspath);
+			}
+			else
+				sys->fprint(fdout(), "error create failed %s\n",bindpath);
+		}
+	}
+	spawn exportns();
+}
+
+exportns()
+{
+	sys->export(sys->fildes(0), "/n/remote", sys->EXPWAIT);
+}
+
+createdir(path: string): int
+{
+	(nil, lst) := sys->tokenize(path, "/");
+	npath := "";
+	for (; lst != nil; lst = tl lst) {
+		(n, nil) := sys->stat(npath + "/" + hd lst);
+		if (n == -1) {
+			fd := sys->create(npath + "/" + hd lst, sys->OREAD, 8r777 | sys->DMDIR);
+			if (fd == nil)
+				return -1;
+		}
+		npath += "/" + hd lst;
+	}
+	return 0;
+}
+
+badmod(path: string)
+{
+	sys->fprint(fdout(), "error Ns: failed to load: %s\n",path);
+	exit;
+}
--- /dev/null
+++ b/appl/demo/ns/runns.sh
@@ -1,0 +1,7 @@
+#!/dis/sh
+load std
+if {~ $#* 0} {
+	echo usage: runns path0 path1 ... pathn
+	raise usage
+}
+grid/register -a resource Namespace 'grid/srv/ns '^$"* | grid/srv/monitor 1 'Namespace'
--- /dev/null
+++ b/appl/demo/odbc/mkfile
@@ -1,0 +1,31 @@
+<../../../mkconfig
+
+TARG=\
+		odbcmnt.dis\
+
+SHTARG=\
+		runodbc.sh\
+
+MODULES=\
+
+SYSMODULES= \
+	arg.m\
+	convcs.m\
+	daytime.m\
+	draw.m\
+	string.m\
+	styx.m\
+	styxservers.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/demo/odbc
+
+<$ROOT/mkfiles/mkdis
+
+SHFILES=${SHTARG:%.sh=$DISBIN/%}
+install:V:	$SHFILES
+%.install:V:	$DISBIN/%
+%.installall:V:	$DISBIN/%
+
+$DISBIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/demo/odbc/odbcmnt.b
@@ -1,0 +1,428 @@
+implement Odbcmnt;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+include "arg.m";
+include "string.m";
+	str: String;
+include "daytime.m";
+	daytime: Daytime;
+include "convcs.m";
+	convcs : Convcs;
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Navigator: import styxservers;
+	nametree: Nametree;
+	Tree: import nametree;
+
+Column: adt {
+	name: string;
+	ctype: string;
+	size: int;
+};
+
+Qroot: con iota;
+WINCHARSET := "windows-1252";		# BUG: odbc.c should do the conversion!
+
+Odbcmnt: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		notloaded(Arg->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if (daytime == nil)
+		notloaded(Daytime->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		notloaded(String->PATH);
+	convcs = load Convcs Convcs->PATH;
+	if(convcs == nil)
+		notloaded(Convcs->PATH);
+	cserr := convcs->init(nil);
+	if (cserr != nil)
+		err("convcs init failed " + cserr);
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		notloaded(Styx->PATH);
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	if(styxservers == nil)
+		notloaded(Styxservers->PATH);
+	styxservers->init(styx);
+	nametree = load Nametree Nametree->PATH;
+	if(nametree == nil)
+		notloaded(Nametree->PATH);
+	nametree->init();
+	addr := "127.0.0.1";
+	arg->init(argv);
+	stype := "ODBC";
+	while((o := arg->opt()) != 0)
+		case o {
+		'a' =>
+			addr = arg->earg();
+		*   =>
+			usage();
+		}
+
+	argv = arg->argv();
+	arg = nil;
+	sys->pctl(Sys->FORKNS | sys->NEWPGRP, nil);
+	dbdir := do_mount(netmkaddr(addr, "tcp", "6700"));
+	(cfd, cdir) := do_clone(dbdir);
+	sources := find_sources(cdir);
+	sys->print("Found %d sources\n", len sources);
+	spawn serveloop(dbdir, sources, sys->fildes(0));
+}
+
+netmkaddr(addr, net, svc: string): string
+{
+	if(net == nil)
+		net = "net";
+	(n, l) := sys->tokenize(addr, "!");
+	if(n <= 1){
+		if(svc== nil)
+			return sys->sprint("%s!%s", net, addr);
+		return sys->sprint("%s!%s!%s", net, addr, svc);
+	}
+	if(svc == nil || n > 2)
+		return addr;
+	return sys->sprint("%s!%s", addr, svc);
+}
+
+split1(s, delim: string): (string, string)
+{
+	(l, r) := str->splitl(s, delim);
+	return (l, str->drop(r, delim)); 
+}
+
+notloaded(s: string)
+{
+	err(sys->sprint("failed to load %s: %r", s));
+}
+
+usage()
+{
+	sys->fprint(stderr, "Usage: odbcmnt [ -a address ]\n");
+	raise "fail:usage";
+}
+
+do_mount(addr: string): string
+{
+	(ok, c) := sys->dial(addr, nil);
+	remdir := "/n/remote";
+	if (ok < 0)
+		err(sys->sprint("failed to dial odbc server on %s: %r", addr));
+	if (sys->mount(c.dfd, nil, remdir, 0, nil) < 0)
+		err(sys->sprint("failed to mount odbc server on %s: %r", addr));
+	dbdir := remdir + "/db";
+	return dbdir;
+}
+
+
+do_clone(dbdir: string): (ref Sys->FD, string)
+{
+	newfile := dbdir + "/new";
+	cfd := sys->open(newfile, Sys->OREAD);
+	if (cfd == nil)
+		err(sys->sprint("failed to open  %s: %r", newfile));
+	cname := read_fd(cfd);
+	if (cname == nil)
+		err("failed to find clone directory name");
+	return(cfd, dbdir + "/" + cname);
+}
+
+dir(name: string, perm: int, length: int, qid: int): Sys->Dir
+{
+	uid := read_file("/dev/user");
+	d := sys->zerodir;
+	d.name = name;
+	d.uid = uid;
+	d.gid = uid;
+	d.qid.path = big qid;
+	if (perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	else {
+		d.qid.qtype = Sys->QTFILE;
+		d.length = big length;
+	}
+	d.mode = perm;
+	d.atime = d.mtime = daytime->now();
+	return d;
+}
+
+newconv(dbdir, source: string): (ref Sys->FD, ref Sys->FD, ref Sys->FD, ref Sys->FD, string)
+{
+	err := "";
+	(clonefd, cdir) := do_clone(dbdir);
+	ctlf := cdir + "/ctl";
+	ctlfd := sys->open(ctlf, Sys->ORDWR);
+	if (ctlfd == nil)
+		err = sys->sprint("Failed to open %s: %r", ctlf);
+	cmdf := cdir + "/cmd";
+	cmdfd := sys->open(cmdf, Sys->ORDWR);
+	if (cmdfd == nil)
+		err = sys->sprint("Failed to open %s: %r", cmdf);
+	dataf := cdir + "/data";
+	datafd := sys->open(dataf, Sys->ORDWR);
+	if (datafd == nil)
+		err = sys->sprint("Failed to open %s: %r", dataf);
+	if (write_fd(ctlfd, "connect " + source) < 0)
+		err = sys->sprint("failed to connect to %s: %r", source);
+	return (clonefd, ctlfd, cmdfd, datafd, err);
+}
+
+SRCDIR: con 1;
+SQL: con 2;
+TABLE: con 3;
+TABLEDIR: con 4;
+COLUMN: con 5;
+
+gettype(fid: big): int
+{
+	return int fid & 7;
+}
+
+SrcFD: adt {
+	clonefd, ctlfd, cmdfd, datafd: ref sys->FD;
+};
+
+serveloop(dbdir: string, sources: list of string, confd: ref sys->FD)
+{
+	srcqid := 0;
+	sqlqid := 0;
+	tableqid := 0;
+	colqid := 0;
+	tabledirqid := 0;
+	(bs, cserr) := convcs->getbtos(WINCHARSET);
+	if (bs == nil)
+		err("getbtos error: " + cserr);
+	(tree, treeop) := nametree->start();
+	tree.create(big Qroot, dir(".",8r555 | sys->DMDIR,0,Qroot));
+	contents: list of string;
+	srcfds := array[len sources] of SrcFD;
+	i := 0;
+	for (sl := sources; sl!=nil; sl=tl sl) {
+		(srcname, srcdriver) := split1(hd sl, ":");
+		# Don't do anything with 'srvdriver' - could make a driver 
+		#    file to read - but does anyone care about it?
+		(clonefd, ctlfd, cmdfd, datafd, e) := newconv(dbdir, srcname);
+		if (e != nil)
+			sys->fprint(sys->fildes(2), "Odbcmnt: %s\n",e);
+		else {
+			srcfds[i] = (clonefd, ctlfd, cmdfd, datafd);
+			sys->print("%s\n",srcname);
+			Qsrc := SRCDIR + (srcqid++<<3);
+			tree.create(big Qroot, dir(srcname,8r555 | sys->DMDIR,0,Qsrc));
+			Qtabledir := TABLEDIR + (tabledirqid++<<3);
+			tree.create(big Qsrc, dir("tables",8r555 | sys->DMDIR,0,Qtabledir));
+			Qsql := SQL + (sqlqid++<<3);
+			tree.create(big Qsrc, dir("sql",8r666,0, Qsql));
+			
+			tables := find_tables(srcfds[i].cmdfd, srcfds[i].datafd);
+			if (tables == nil)
+				err(sys->sprint("failed to find tables: %r"));
+			if (write_fd(srcfds[i].ctlfd, "headings") < 0)
+				err(sys->sprint("failed to write to ctl file: %r"));
+			sys->print("\tBuilding tree...");
+			for (tlist:=tables; tlist!=nil; tlist=tl tlist) {
+				table := hd tlist;
+				Qtable := TABLE + (tableqid++<<3);
+				tree.create(big Qtabledir, dir(table,8r555 | sys->DMDIR,0,Qtable));
+				columns := find_columns(srcfds[i].cmdfd, srcfds[i].datafd, table);
+				for (clist:=columns; clist!=nil; clist=tl clist) {
+					column := hd clist;
+					Qcol := COLUMN + (colqid<<3);
+					tree.create(big Qtable, dir(column.name,8r555,0,Qcol));
+					data := sys->sprint("%s %d\n", column.ctype, column.size);
+					contents = data :: contents;
+					colqid++;
+				}
+			}
+			sys->print("done\n");
+		}
+		i++;
+	}
+	colcontent := array[colqid] of string;
+	for (i = colqid - 1; i >= 0; i--) {
+		colcontent[i] = hd contents;
+		contents = tl contents;
+	}
+	(tchan, srv) := Styxserver.new(confd, Navigator.new(treeop), big Qroot);
+	sys->pctl(Sys->FORKNS|Sys->FORKFD, nil);
+	gm: ref Tmsg;
+	buf := array[Sys->ATOMICIO] of byte;
+	serverloop: for (;;) {
+		gm = <-tchan;
+		if (gm == nil)
+			break serverloop;
+		pick m := gm {
+		Readerror =>
+			sys->fprint(sys->fildes(2), "odbcmnt: fatal read error: %s\n", m.error);
+			break serverloop;
+		Read =>
+			c := srv.getfid(m.fid);
+			if(c.qtype & Sys->QTDIR){
+				srv.read(m);	# does readdir
+				break;
+			}
+			case gettype(c.path) {
+				SQL =>
+					srcno := int c.path >> 3;
+					sys->seek(srcfds[srcno].datafd, m.offset, Sys->SEEKSTART);
+					n := sys->read(srcfds[srcno].datafd, buf, len buf);
+					if (n >= 0) {
+						(state, s, err) := bs->btos(nil, buf[:n], -1);
+						r := ref Rmsg.Read(gm.tag, array of byte s);
+						srv.reply(r);
+					} else
+						srv.reply(ref Rmsg.Error(gm.tag, sys->sprint("%r")));
+					break;
+			COLUMN =>
+				srv.reply(styxservers->readstr(m, colcontent[int c.path>>3]));
+				* =>
+					srv.default(gm);
+			}
+		Write =>
+			c := srv.getfid(m.fid);
+			case gettype(c.path) {
+				SQL =>
+					srcno := int c.path >> 3;
+					n := sys->write(srcfds[srcno].cmdfd, m.data, len m.data);
+					if (n == len m.data)
+						srv.reply(ref Rmsg.Write(m.tag, n));
+					else
+						srv.reply(ref Rmsg.Error(gm.tag, sys->sprint("%r")));
+					break;
+				* =>
+					srv.default(gm);
+			}
+
+		* =>
+			srv.default(gm);
+		}
+	}
+	tree.quit();
+}
+
+find_tables(cmdfd, datafd: ref Sys->FD): list of string
+{
+	tlist: list of string;
+	if (write_fd(cmdfd, "tables") < 0)
+		err(sys->sprint("failed to write to cmd file: %r"));
+	while((rec := read_fd(datafd)) != nil) {
+		fields := atokenize(rec, "|");
+		if (len fields < 4)
+			err("bad table name");
+		tname := fields[2];
+		tlist = tname :: tlist;
+	}
+	return tlist;
+}
+
+
+find_columns(cmdfd, datafd: ref Sys->FD, table: string): list of Column
+{
+	clist: list of Column;
+	if (write_fd(cmdfd, "columns " + table) < 0)
+		err(sys->sprint("failed to write to cmd file: %r"));
+	while((rec := read_fd(datafd)) != nil) {
+		fields := atokenize(rec, "|");
+		if (len fields < 3)
+			err("bad column name");
+		cname :=fields[3];
+		ctype := "";
+		if (len fields > 5)
+			ctype = fields[5];
+		csize := 0;
+		if (len fields > 6)
+			csize = int fields[6];
+		clist = (fields[3], ctype, csize) :: clist;
+	}
+	return clist;
+}
+
+atokenize(s: string, delim: string): array of string
+{
+	if (s == nil)
+		return nil;
+	dl := len delim;
+	r: list of string;
+	l: string;
+	for (;;) {
+		(l, s) = str->splitstrl(s, delim);
+		r = l :: r;
+		if (s == nil || s == delim)
+			break;
+		s = s[dl:];
+	}
+	a := array[len r] of string;
+	for (i:=len r-1; i>=0; i--) {
+		a[i] = hd r;
+		r = tl r;
+	}
+	return a;
+}
+
+find_sources(cdir: string): list of string
+{
+	sfile := cdir+"/sources";
+	fd := sys->open(sfile, Sys->OREAD);
+	if (fd == nil)
+		err(sys->sprint("failed to open  %s: %r", sfile));
+	s := read_fd(fd);
+	(n, lines) := sys->tokenize(s, "\n");
+	return lines;
+}
+
+err(s: string)
+{
+	sys->fprint(stderr, "odbcgw: %s\n", s);
+	raise "fail:error";
+}
+
+read_fd(fd: ref Sys->FD): string
+{
+	MAX : con Sys->ATOMICIO;
+	buf := array[MAX] of byte;
+#	sys->seek(fd, big 0, Sys->SEEKSTART);
+	size := sys->read(fd, buf, MAX);
+	if (size <= 0) {
+#		if (size < 0)
+#			sys->fprint(stderr, "read_fd error: %r\n");
+		return nil;
+	}
+	return string buf[0:size];
+}
+
+read_file(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil)
+		return nil;
+	return read_fd(fd);
+}
+
+write_fd(fd: ref Sys->FD, s: string): int
+{
+	a := array of byte s;
+	if (sys->write(fd, a, len a) != len a)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/appl/demo/odbc/runodbc.sh
@@ -1,0 +1,17 @@
+#!/dis/sh
+
+mount -A {auxi/odbcmnt -a tcp!200.1.1.113} /mnt/odbc
+
+fn splitrun {
+	if {! ~ $#* 0} {
+		(hd tl) = $*
+		echo Registering $hd
+		grid/register -a resource ODBC -a name $hd '{export /mnt/odbc/'^$hd^'}'
+		splitrun $tl
+	}
+}
+
+cd /mnt/odbc
+sources=`{ls}
+splitrun $sources
+
--- /dev/null
+++ b/appl/demo/spree/mkfile
@@ -1,0 +1,23 @@
+<../../../mkconfig
+
+TARG=\
+
+SHTARG=\
+		spreeclient.sh\
+
+MODULES=\
+
+SYSMODULES= \
+
+DISBIN=$ROOT/dis/demo/spree
+
+<$ROOT/mkfiles/mkdis
+
+SHFILES=${SHTARG:%.sh=$DISBIN/%}
+all:V: $SHTARG
+install:V:	$SHFILES
+%.install:V:	$DISBIN/%
+%.installall:V:	$DISBIN/%
+
+$DISBIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/demo/spree/spreeclient.sh
@@ -1,0 +1,44 @@
+#!/dis/sh
+load std
+autoload=std
+ndb/cs
+
+fn ck {
+	or {$*} {
+		echo spreeclient: exiting >[1=2]
+		raise error
+	}
+}
+user="{cat /dev/user}
+
+fn notice {
+	or {~ $#* 1} {
+		echo usage: notice arg >[1=2]
+		raise usage
+	}
+	t := $*
+	run /lib/sh/win
+	tkwin Notice {
+		x text .t -yscrollcommand {.s set}
+		x scrollbar .s -orient vertical -command {.t yview}
+		x pack .s -side left -fill y
+		x pack .t -side top -fill both -expand 1
+		x .t insert 1.0 ${tkquote $t}
+		tk onscreen $wid
+		chan c; {} ${recv c}
+	}
+}
+
+ck mount -A 'tcp!$registry!registry' /mnt/registry
+ck /dis/grid/remotelogon wm/wm {
+	k = /usr/$user/keyring/default
+	addrs=`{ndb/regquery resource spree auth.signer `{getpk -s $k}}
+	if{~ $#addrs 0} {
+		notice 'No spree servers found'
+	}
+	if {mount ${hd $addrs} /n/remote} {
+		spree/joinsession 0
+	} {
+		notice 'Cannot access spree server'
+	}
+}
--- /dev/null
+++ b/appl/demo/whiteboard/mkfile
@@ -1,0 +1,28 @@
+<../../../mkconfig
+
+TARG=\
+		wbsrv.dis\
+		whiteboard.dis\
+
+SHTARG=\
+		runwb.sh\
+
+MODULES=\
+
+SYSMODULES= \
+	draw.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+
+DISBIN=$ROOT/dis/demo/whiteboard
+
+<$ROOT/mkfiles/mkdis
+
+SHFILES=${SHTARG:%.sh=$DISBIN/%}
+install:V:	$SHFILES
+%.install:V:	$DISBIN/%
+%.installall:V:	$DISBIN/%
+
+$DISBIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/demo/whiteboard/runwb.sh
@@ -1,0 +1,7 @@
+#!/dis/sh.dis
+load std
+pctl forkns
+memfs /tmp
+cp /dis/wm/whiteboard.dis /tmp
+/dis/auxi/wbsrv /tmp $2
+grid/register -a resource Whiteboard -a size 600x400 -a name $1 {export /tmp}
--- /dev/null
+++ b/appl/demo/whiteboard/wbsrv.b
@@ -1,0 +1,268 @@
+implement Wbserve;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Chans, Display, Image, Rect, Point : import draw;
+
+Wbserve : module {
+	init : fn (ctxt : ref Draw->Context, args : list of string);
+};
+
+WBW : con 600;
+WBH : con 400;
+
+savefile := "";
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmod(Draw->PATH);
+
+	if (args == nil || tl args == nil)
+		error("usage: wbsrv mntpt [savefile]");
+	args = tl args;
+	mntpt := hd args;
+	args = tl args;
+
+	display := Display.allocate(nil);
+	if (display == nil)
+		error(sys->sprint("cannot allocate display: %r"));
+
+	bg: ref Draw->Image;
+	if (args != nil) {
+		savefile = hd args;
+		bg = display.open(savefile);
+	}
+	r := Rect(Point(0,0), Point(WBW, WBH));
+	wb := display.newimage(r, Draw->CMAP8, 0, Draw->White);
+	if (wb == nil)
+		error(sys->sprint("cannot allocate whiteboard image: %r"));
+	if (bg != nil) {
+		wb.draw(bg.r, bg, nil, Point(0,0));
+		bg = nil;
+	}
+
+	nextmsg = ref Msg (nil, nil);
+
+	sys->bind("#s", mntpt, Sys->MBEFORE);
+
+	bit := sys->file2chan(mntpt, "wb.bit");
+	strokes := sys->file2chan(mntpt, "strokes");
+
+	spawn srv(wb, bit, strokes);
+	if (savefile != nil)
+		spawn saveit(display, wb);
+}
+
+srv(wb: ref Image, bit, strokes: ref Sys->FileIO)
+{
+	nwbbytes := draw->bytesperline(wb.r, wb.depth) * wb.r.dy();
+	bithdr := sys->aprint("%11s %11d %11d %11d %11d ", wb.chans.text(), 0, 0, WBW, WBH);
+
+	for (;;) alt {
+	(offset, count, fid, r) := <-bit.read =>
+		if (r == nil) {
+			closeclient(fid);
+			continue;
+		}
+		c := getclient(fid);
+		if (c == nil) {
+			# new client
+			c = newclient(fid);
+			data := array [len bithdr + nwbbytes] of byte;
+			data[0:] = bithdr;
+			wb.readpixels(wb.r, data[len bithdr:]);
+			c.bitdata = data;
+		}
+		if (offset >= len c.bitdata) {
+			rreply(r, (nil, nil));
+			continue;
+		}
+		rreply(r, (c.bitdata[offset:], nil));
+
+	(offset, data, fid, w) := <-bit.write =>
+		if (w != nil)
+			wreply(w, (0, "permission denied"));
+
+	(offset, count, fid, r) := <-strokes.read =>
+		if (r == nil) {
+			closeclient(fid);
+			continue;
+		}
+		c := getclient(fid);
+		if (c == nil) {
+			c = newclient(fid);
+			c.nextmsg = nextmsg;
+		}
+		d := c.nextmsg.data;
+		if (d == nil) {
+			c.pending = r;
+			c.pendlen = count;
+			continue;
+		}
+		c.nextmsg = c.nextmsg.next;
+		rreply(r, (d, nil));
+
+	(offset, data, fid, w) := <-strokes.write =>
+		if (w == nil) {
+			closeclient(fid);
+			continue;
+		}
+		err := drawstrokes(wb, data);
+		if (err != nil) {
+			wreply(w, (0, err));
+			continue;
+		}
+		wreply(w, (len data, nil));
+		writeclients(data);
+	}
+}
+
+rreply(rc: chan of (array of byte, string), reply: (array of byte, string))
+{
+	alt {
+	rc <-= reply =>;
+	* =>;
+	}
+}
+
+wreply(wc: chan of (int, string), reply: (int, string))
+{
+	alt {
+	wc <-= reply=>;
+	* =>;
+	}
+}
+
+export(fd : ref Sys->FD, done : chan of int)
+{
+	sys->export(fd, "/", Sys->EXPWAIT);
+	done <-= 1;
+}
+
+Msg : adt {
+	data : array of byte;
+	next : cyclic ref Msg;
+};
+
+Client : adt {
+	fid : int;
+	bitdata : array of byte;		# bit file client
+	nextmsg : ref Msg;			# strokes file client
+	pending : Sys->Rread;
+	pendlen : int;
+};
+
+nextmsg : ref Msg;
+clients : list of ref Client;
+
+newclient(fid : int) : ref Client
+{
+	c := ref Client(fid, nil, nil, nil, 0);
+	clients = c :: clients;
+	return c;
+}
+
+getclient(fid : int) : ref Client
+{
+	for(cl := clients; cl != nil; cl = tl cl)
+		if((c := hd cl).fid == fid)
+			return c;
+	return nil;
+}
+
+closeclient(fid : int)
+{
+	nl: list of ref Client;
+	for(cl := clients; cl != nil; cl = tl cl)
+		if((hd cl).fid != fid)
+			nl = hd cl :: nl;
+	clients = nl;
+}
+
+writeclients(data : array of byte)
+{
+	nm := ref Msg(nil, nil);
+	nextmsg.data = data;
+	nextmsg.next = nm;
+
+	for(cl := clients; cl != nil; cl = tl cl){
+		if ((c := hd cl).pending != nil) {
+			n := c.pendlen;
+			if (n > len data)
+				n = len data;
+			alt{
+			c.pending <-= (data[0:n], nil) => ;
+			* => ;
+			}
+			c.pending = nil;
+			c.nextmsg = nm;
+		}
+	}
+	nextmsg = nm;
+}
+
+# data: colour width p0 p1 pn*
+
+pencol: int;
+pen: ref Image;
+
+drawstrokes(wb: ref Image, data : array of byte) : string
+{
+	(n, toks) := sys->tokenize(string data, " ");
+	if (n < 6 || n & 1)
+		return "bad data";
+
+	colour, width, x, y : int;
+	(colour, toks) = (int hd toks, tl toks);
+	(width, toks) = (int hd toks, tl toks);
+	(x, toks) = (int hd toks, tl toks);
+	(y, toks) = (int hd toks, tl toks);
+	if (pen == nil || colour != pencol) {
+		pencol = colour;
+		pen = wb.display.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencol);
+	}
+	p0 := Point(x, y);
+	while (toks != nil) {
+		(x, toks) = (int hd toks, tl toks);
+		(y, toks) = (int hd toks, tl toks);
+		p1 := Point(x, y);
+		# could use poly() instead of line()
+		wb.line(p0, p1, Draw->Enddisc, Draw->Enddisc, width, pen, pen.r.min);
+		p0 = p1;
+	}
+	return nil;
+}
+
+error(e: string)
+{
+	sys->fprint(stderr(), "wbsrv: %s\n", e);
+	raise "fail:error";
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+badmod(path: string)
+{
+	sys->fprint(stderr(), "wbsrv: cannot load %s: %r\n", path);
+	exit;
+}
+
+saveit(display: ref Display, img: ref Image)
+{
+	for (;;) {
+		sys->sleep(300000);
+		fd := sys->open(savefile, sys->OWRITE);
+		if (fd == nil)
+			exit;
+		display.writeimage(fd, img);
+	}
+}
--- /dev/null
+++ b/appl/demo/whiteboard/whiteboard.b
@@ -1,0 +1,605 @@
+implement Whiteboard;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Screen, Display, Image, Rect, Point, Font: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+Whiteboard: module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+ERASEWIDTH: con 6;
+
+
+stderr: ref Sys->FD;
+srvfd: ref Sys->FD;
+disp: ref Display;
+font: ref Draw->Font;
+drawctxt: ref Draw->Context;
+
+tksetup := array[] of {
+	"frame .f -bd 2",
+	"frame .c -bg white -width 600 -height 400",
+	"menu .penmenu",
+	".penmenu add command -command {send cmd pen 0} -image pen0",
+	".penmenu add command -command {send cmd pen 1} -image pen1",
+	".penmenu add command -command {send cmd pen 2} -image pen2",
+	".penmenu add command -command {send cmd pen erase} -image erase",
+	"menubutton .pen -menu .penmenu -image pen1",
+	"button .colour -bg black -activebackground black -command {send cmd getcolour}",
+	"pack .c -in .f",
+	"pack .f -side top -anchor center",
+	"pack .pen -side left",
+	"pack .colour -side left -fill both -expand 1",
+	"update",
+};
+
+tkconnected := array[] of {
+	"bind .c <Button-1> {send cmd down %x %y}",
+	"bind .c <ButtonRelease-1> {send cmd up %x %y}",
+	"update",
+};
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+
+	args = tl args;
+	servicedir := ".";
+	if(args != nil)
+		(servicedir, args) = (hd args, tl args);
+
+	disp = ctxt.display;
+	if (disp == nil) {
+		sys->fprint(stderr, "bad Draw->Context\n");
+		raise "fail:init";
+	}
+	drawctxt = ctxt;
+
+	tkclient->init();
+	(win, winctl) := tkclient->toplevel(ctxt, nil, "Whiteboard", 0);
+	font = Font.open(disp, tkcmd(win, ". cget -font"));
+	if(font == nil)
+		font = Font.open(disp, "*default*");
+	cmd := chan of string;
+	tk->namechan(win, cmd, "cmd");
+	mkpenimgs(win);
+	tkcmds(win, tksetup);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd" :: "ptr" :: nil);
+	cimage := makeimage(win);
+
+	sc := chan of array of (Point, Point);
+	cc := chan of (string, ref Image, ref Sys->FD);
+	connected := 0;
+	sfd: ref Sys->FD;
+
+	showtext(cimage, "connecting...");
+	spawn connect(servicedir, cc);
+
+	err: string;
+	strokeimg: ref Image;
+Connect:
+	for (;;) alt {
+	(err, strokeimg, sfd) = <-cc =>
+		if (err == nil)
+			break Connect;
+		else
+			showtext(cimage, "Error: " + err);
+
+	s := <-winctl or
+	s = <-win.wreq or
+	s = <-win.ctxt.ctl =>
+		oldimg := win.image;
+		err = tkclient->wmctl(win, s);
+		if(s[0] == '!' && err == nil && win.image != oldimg){
+			cimage = makeimage(win);
+			showtext(cimage, "connecting...");
+		}
+	p := <-win.ctxt.ptr =>
+		tk->pointer(win, *p);
+	c := <-win.ctxt.kbd =>
+		tk->keyboard(win, c);
+	}
+
+	tkcmd(win, ".c configure -width " + string strokeimg.r.dx());
+	tkcmd(win, ".c configure -height " + string strokeimg.r.dy());
+	tkcmds(win, tkconnected);
+	tkcmd(win, "update");
+	cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min);
+
+	strokesin := chan of (int, int, array of Point);
+	strokesout := chan of (int, int, Point, Point);
+	spawn reader(sfd, strokesin);
+	spawn writer(sfd, strokesout);
+
+	pendown := 0;
+	p0, p1: Point;
+
+	getcolour := 0;
+	white := disp.white;
+	whitepen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, Draw->White);
+	pencolour := Draw->Black;
+	penwidth := 1;
+	erase := 0;
+	drawpen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour);
+
+	for (;;) alt {
+	s := <-winctl or
+	s = <-win.ctxt.ctl or
+	s = <-win.wreq =>
+		oldimg := win.image;
+		err = tkclient->wmctl(win, s);
+		if(s[0] == '!' && err == nil && win.image != oldimg){
+			cimage = makeimage(win);
+			cimage.draw(cimage.r, strokeimg, nil, strokeimg.r.min);
+		}
+	p := <-win.ctxt.ptr =>
+		tk->pointer(win, *p);
+	c := <-win.ctxt.kbd =>
+		tk->keyboard(win, c);
+	(colour, width, strokes) := <-strokesin =>
+		if (strokes == nil)
+			tkclient->settitle(win, "Whiteboard (Disconnected)");
+		else {
+			pen := disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, colour);
+			drawstrokes(cimage, cimage.r.min, pen, width, strokes);
+			drawstrokes(strokeimg, strokeimg.r.min, pen, width, strokes);
+		}
+
+	c := <-cmd =>
+		(nil, toks) := sys->tokenize(c, " ");
+		action := hd toks;
+		case action {
+		"up" or
+		"down" =>
+			toks = tl toks;
+			x := int hd toks;
+			y := int hd tl toks;
+			if (action == "down") {
+				if (!pendown) {
+					pendown = 1;
+					p0 = Point(x, y);
+					continue;
+				}
+			} else
+				pendown = 0;
+			p1 = Point(x, y);
+			if (pendown && p1.x == p0.x && p1.y == p0.y)
+				continue;
+			pen := drawpen;
+			colour := pencolour;
+			width := penwidth;
+			if (erase) {
+				pen = whitepen;
+				colour = Draw->White;
+				width = ERASEWIDTH;
+			}
+			drawstroke(cimage, cimage.r.min, p0, p1, pen, width);
+			drawstroke(strokeimg, strokeimg.r.min, p0, p1, pen, width);
+			strokesout <-= (colour, width, p0, p1);
+			p0 = p1;
+
+		"getcolour" =>
+			pendown = 0;
+			if (!getcolour)
+				spawn colourmenu(cmd);
+		"colour" =>
+			pendown = 0;
+			getcolour = 0;
+			toks = tl toks;
+			if (toks == nil)
+				# colourmenu was dismissed
+				continue;
+			erase = 0;
+			tkcmd(win, ".pen configure -image pen" + string penwidth);
+			tkcmd(win, "update");
+			pencolour = int hd toks;
+			toks = tl toks;
+			tkcolour := hd toks;
+			drawpen = disp.newimage(Rect(Point(0,0), Point(1,1)), Draw->CMAP8, 1, pencolour);
+			tkcmd(win, ".colour configure -bg " + tkcolour + " -activebackground " + tkcolour);
+			tkcmd(win, "update");
+
+		"pen" =>
+			pendown = 0;
+			p := hd tl toks;
+			i := "";
+			if (p == "erase") {
+				erase = 1;
+				i = "erase";
+			} else {
+				erase = 0;
+				penwidth = int p;
+				i = "pen"+p;
+			}
+			tkcmd(win, ".pen configure -image " + i);
+			tkcmd(win, "update");
+		}
+
+	}
+}
+
+makeimage(win: ref Tk->Toplevel): ref Draw->Image
+{
+	if(win.image == nil)
+		return nil;
+	scr := Screen.allocate(win.image, win.image.display.white, 0);
+	w := scr.newwindow(tk->rect(win, ".c", Tk->Local), Draw->Refnone, Draw->Nofill);
+	return w;
+}
+
+showtext(img: ref Image, s: string)
+{
+	r := img.r;
+	r.max.y = img.r.min.y + font.height;
+	img.draw(r, disp.white, nil, (0, 0));
+	img.text(r.min, disp.black, (0, 0), font, s);
+}
+
+penmenu(t: ref Tk->Toplevel, p: Point)
+{
+	topy := int tkcmd(t, ".penmenu yposition 0");
+	boty := int tkcmd(t, ".penmenu yposition end");
+	dy := boty - topy;
+	p.y -= dy;
+	tkcmd(t, ".penmenu post " + string p.x + " " + string p.y);
+}
+
+colourcmds := array[] of {
+	"label .l -height 10",
+	"frame .c -height 224 -width 224",
+	"pack .l -fill x -expand 1",
+	"pack .c -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+	"bind .c <Button-1> {send cmd push %x %y}",
+	"bind .c <ButtonRelease-1> {send cmd release}",
+};
+
+lastcolour := "255";
+lasttkcolour := "#000000";
+
+colourmenu(c: chan of string)
+{
+	(t, winctl) := tkclient->toplevel(drawctxt, nil, "Whiteboard", Tkclient->OK);
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	tkcmds(t, colourcmds);
+	tkcmd(t, ".l configure -bg " + lasttkcolour);
+	tkcmd(t, "update");
+	tkclient->onscreen(t, "onscreen");
+	tkclient->startinput(t, "kbd" :: "ptr" :: nil);
+
+	drawcolours(t.image, tk->rect(t, ".c", Tk->Local));
+
+	for(;;) alt {
+	p := <-t.ctxt.ptr =>
+		tk->pointer(t, *p);
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-winctl or
+	s = <-t.ctxt.ctl or
+	s = <-t.wreq =>
+		case s{
+		"ok" =>
+			c <-= "colour " + lastcolour + " " + lasttkcolour;
+			return;
+		"exit" =>
+			c <-= "colour";
+			return;
+		* =>
+			oldimage := t.image;
+			e := tkclient->wmctl(t, s);
+			if(s[0] == '!' && e == nil && oldimage != t.image)
+				drawcolours(t.image, tk->rect(t, ".c", Tk->Local));
+		}
+
+	press := <-cmd =>
+		(n, word) := sys->tokenize(press, " ");
+		case hd word {
+		"push" =>
+			(lastcolour, lasttkcolour) = color(int hd tl word, int hd tl tl word, tk->rect(t, ".c", 0).size());
+			tkcmd(t, ".l configure -bg " + lasttkcolour);
+		}
+	}
+}
+
+drawcolours(img: ref Image, cr: Rect)
+{
+	# use writepixels because it's much faster than allocating all those colors.
+	tmp := disp.newimage(((0,0),(cr.dx(),cr.dy()/16+1)), Draw->CMAP8, 0, 0);
+	if(tmp == nil)
+		return;
+	buf := array[tmp.r.dx()*tmp.r.dy()] of byte;
+	dx := cr.dx();
+	dy := cr.dy();
+	for(y:=0; y<16; y++){
+		for(i:=tmp.r.dx()-1; i>=0; --i)
+			buf[i] = byte (16*y+(16*i)/dx);
+		for(k:=tmp.r.dy()-1; k>=1; --k)
+			buf[dx*k:] = buf[0:dx];
+		tmp.writepixels(tmp.r, buf);
+		r: Rect;
+		r.min.x = cr.min.x;
+		r.max.x = cr.max.x;
+		r.min.y = cr.min.y+(dy*y)/16;
+		r.max.y = cr.min.y+(dy*(y+1))/16;
+		img.draw(r, tmp, nil, tmp.r.min);
+	}
+}
+
+color(x, y: int, size: Point): (string, string)
+{
+	x = (16*x)/size.x;
+	y = (16*y)/size.y;
+	col := 16*y+x;
+	(r, g, b) := disp.cmap2rgb(col);
+	tks := sys->sprint("#%.2x%.2x%.2x", r, g, b);
+	return (string disp.cmap2rgba(col), tks);
+}
+
+opensvc(dir: string, svc: string, name: string): (ref Sys->FD, string, string)
+{
+	ctlfd := sys->open(dir+"/ctl", Sys->ORDWR);
+	if(ctlfd == nil)
+		return (nil, nil, sys->sprint("can't open %s/ctl: %r", dir));
+	if(sys->fprint(ctlfd, "%s %s", svc, name) <= 0)
+		return (nil, nil, sys->sprint("can't access %s service %s: %r", svc, name));
+	buf := array [32] of byte;
+	sys->seek(ctlfd, big 0, Sys->SEEKSTART);
+	n := sys->read(ctlfd, buf, len buf);
+	if (n <= 0)
+		return (nil, nil, sys->sprint("%s/ctl: protocol error: %r", dir));
+	return (ctlfd, dir+"/"+string buf[0:n], nil);
+}
+
+connect(dir: string, res: chan of (string, ref Image, ref Sys->FD))
+{
+	bitpath := dir + "/wb.bit";
+	strokepath := dir + "/strokes";
+
+	sfd := sys->open(strokepath, Sys->ORDWR);
+	if (sfd == nil) {
+		err := sys->sprint("cannot open whiteboard data: %r");
+		res <-= (err, nil, nil);
+		srvfd = nil;
+		return;
+	}
+
+	bfd := sys->open(bitpath, Sys->OREAD);
+	if (bfd == nil) {
+		err := sys->sprint("cannot open whiteboard image: %r");
+		res <-= (err, nil, nil);
+		srvfd = nil;
+		return;
+	}
+
+	img := disp.readimage(bfd);
+	if (img == nil) {
+		err := sys->sprint("cannot read whiteboard image: %r");
+		res <-= (err, nil, nil);
+		srvfd = nil;
+		return;
+	}
+
+	# make sure image is depth 8 (because of image.line() bug)
+	if (img.depth != 8) {
+		nimg := disp.newimage(img.r, Draw->CMAP8, 0, 0);
+		if (nimg == nil) {
+			res <-= ("cannot allocate local image", nil, nil);
+			srvfd = nil;
+			return;
+		}
+		nimg.draw(nimg.r, img, nil, img.r.min);
+		img = nimg;
+	}
+
+	res <-= (nil, img, sfd);
+}
+
+mkpenimgs(win: ref Tk->Toplevel)
+{
+	ZP := Point(0,0);
+	pr := Rect((0,0), (13,14));
+	ir := pr.inset(2);
+	midx := ir.dx()/2 + ir.min.x;
+	start := Point(midx, ir.min.y);
+	end := Point(midx, ir.max.y-1);
+
+	i0 := disp.newimage(pr, Draw->GREY1, 0, Draw->White);
+	i1 := disp.newimage(pr, Draw->GREY1, 0, Draw->Black);
+	i2 := disp.newimage(pr, Draw->GREY1, 0, Draw->Black);
+	i3 := disp.newimage(pr, Draw->GREY1, 0, Draw->Black);
+
+	i0.draw(ir, disp.black, nil, ZP);
+	i1.line(start, end, Draw->Endsquare, Draw->Endsquare, 0, disp.white, ZP);
+	i2.line(start, end, Draw->Endsquare, Draw->Endsquare, 1, disp.white, ZP);
+	i3.line(start, end, Draw->Endsquare, Draw->Endsquare, 2, disp.white, ZP);
+
+	tk->cmd(win, "image create bitmap erase");
+	tk->cmd(win, "image create bitmap pen0");
+	tk->cmd(win, "image create bitmap pen1");
+	tk->cmd(win, "image create bitmap pen2");
+
+	tk->putimage(win, "erase", i0, nil);
+	tk->putimage(win, "pen0", i1, nil);
+	tk->putimage(win, "pen1", i2, nil);
+	tk->putimage(win, "pen2", i3, nil);
+}
+
+reader(fd: ref Sys->FD, sc: chan of (int, int, array of Point))
+{
+	buf := array [Sys->ATOMICIO] of byte;
+
+	for (;;) {
+		n := sys->read(fd, buf, len buf);
+		if (n <= 0) {
+			sc <-= (0, 0, nil);
+			return;
+		}
+		s := string buf[0:n];
+		(npts, toks) := sys->tokenize(s, " ");
+		if (npts & 1)
+			# something wrong
+			npts--;
+		if (npts < 6)
+			# ignore
+			continue;
+
+		colour, width: int;
+		(colour, toks) = (int hd toks, tl toks);
+		(width, toks) = (int hd toks, tl toks);
+		pts := array [(npts - 2)/ 2] of Point;
+		for (i := 0; toks != nil; i++) {
+			x, y: int;
+			(x, toks) = (int hd toks, tl toks);
+			(y, toks) = (int hd toks, tl toks);
+			pts[i] = Point(x, y);
+		}
+		sc <-= (colour, width, pts);
+		pts = nil;
+	}
+}
+
+Wmsg: adt {
+	data: array of byte;
+	datalen: int;
+	next: cyclic ref Wmsg;
+};
+
+writer(fd: ref Sys->FD, sc: chan of (int, int, Point, Point))
+{
+	lastcol := -1;
+	lastw := -1;
+	lastpt := Point(-1, -1);
+	curmsg: ref Wmsg;
+	nextmsg: ref Wmsg;
+
+	eofc := chan of int;
+	wc := chan of ref Wmsg;
+	wseof := 0;
+	spawn wslave(fd, wc, eofc);
+
+	for (;;) {
+		colour := -1;
+		width := 0;
+		p0, p1: Point;
+
+		if (curmsg == nil || wseof)
+			(colour, width, p0, p1) = <-sc;
+		else alt {
+		wseof = <-eofc =>
+			;
+
+		(colour, width, p0, p1) = <-sc =>
+			;
+
+		wc <-= curmsg =>
+			curmsg = curmsg.next;
+			continue;
+		}
+
+		newseq := 0;
+		if (curmsg == nil) {
+			curmsg = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
+			nextmsg = curmsg;
+			newseq = 1;
+		}
+
+		if (colour != lastcol || width != lastw || p0.x != lastpt.x || p0.y != lastpt.y)
+			newseq = 1;
+
+		d: array of byte = nil;
+		if (!newseq) {
+			d = sys->aprint(" %d %d", p1.x, p1.y);
+			if (nextmsg.datalen + len d >= Sys->ATOMICIO) {
+				nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
+				nextmsg = nextmsg.next;
+				newseq = 1;
+			}
+		}
+		if (newseq) {
+			d = sys->aprint(" %d %d %d %d %d %d", colour, width, p0.x, p0.y, p1.x, p1.y);
+			if (nextmsg.datalen != 0) {
+				nextmsg.next = ref Wmsg(array [Sys->ATOMICIO] of byte, 0, nil);
+				nextmsg = nextmsg.next;
+			}
+		}
+		nextmsg.data[nextmsg.datalen:] = d;
+		nextmsg.datalen += len d;
+		lastcol = colour;
+		lastw = width;
+		lastpt = p1;
+	}
+}
+
+wslave(fd: ref Sys->FD, wc: chan of ref Wmsg, eof: chan of int)
+{
+	for (;;) {
+		wm := <-wc;
+		n := sys->write(fd, wm.data, wm.datalen);
+		if (n != wm.datalen)
+			break;
+	}
+	eof <-= 1;
+}
+
+drawstroke(img: ref Image, offset, p0, p1: Point, pen: ref Image, width: int)
+{
+	p0 = p0.add(offset);
+	p1 = p1.add(offset);
+	img.line(p0, p1, Draw->Enddisc, Draw->Enddisc, width, pen, p0);
+}
+
+drawstrokes(img: ref Image, offset: Point, pen: ref Image, width: int, pts: array of Point)
+{
+	if (len pts < 2)
+		return;
+	p0, p1: Point;
+	p0 = pts[0].add(offset);
+	for (i := 1; i < len pts; i++) {
+		p1 = pts[i].add(offset);
+		img.line(p0, p1, Draw->Enddisc, Draw->Enddisc, width, pen, p0);
+		p0 = p1;
+	}
+}
+
+badmod(mod: string)
+{
+	sys->fprint(stderr, "cannot load %s: %r\n", mod);
+	raise "fail:bad module";
+}
+
+tkcmd(t: ref Tk->Toplevel, cmd: string): string
+{
+	s := tk->cmd(t, cmd);
+	if (s != nil && s[0] == '!') {
+		sys->fprint(stderr, "%s\n", cmd);
+		sys->fprint(stderr, "tk error: %s\n", s);
+	}
+	return s;
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++)
+		tkcmd(t, cmds[i]);
+}
--- /dev/null
+++ b/appl/ebook/checkxml.b
@@ -1,0 +1,132 @@
+implement Checkxml;
+
+# simple minded xml checker - checks for basic nestedness, and
+# prints out more informative context on the error messages than
+# the usual xml parser.
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+include "xml.m";
+	xml: Xml;
+	Parser, Item, Locator: import xml;
+
+stderr: ref Sys->FD;
+Checkxml: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	xml = load Xml Xml->PATH;
+	if (xml == nil) {
+		sys->fprint(stderr, "checkxml: cannot load %s: %r\n", Xml->PATH);
+		raise "fail:bad module";
+	}
+	xml->init();
+	if (len argv < 2) {
+		sys->fprint(stderr, "usage: checkxml file...\n");
+		raise "fail:usage";
+	}
+	err := 0;
+	for (argv = tl argv; argv != nil; argv = tl argv) {
+		err = check(hd argv) || err;
+	}
+	if (err)
+		raise "fail:errors";
+}
+
+warningproc(warningch: chan of (Locator, string), finch: chan of int, tagstackch: chan of ref Item.Tag)
+{
+	nw := 0;
+	stack: list of ref Item.Tag;
+	for (;;) {
+		alt {
+		(loc, w) := <-warningch =>
+			if (w == nil) {
+				finch <-= nw;
+				exit;
+			}
+			printerror(loc, w, stack);
+			nw++;
+		item := <-tagstackch =>
+			if (item != nil)
+				stack = item :: stack;
+			else
+				stack = tl stack;
+		}
+	}
+}
+
+printerror(loc: Locator, e: string, tagstack: list of ref Item.Tag)
+{
+	if (tagstack != nil) {
+		sys->print("%s:%d: %s\n", loc.systemid, loc.line, e);
+		for (il := tagstack; il != nil; il = tl il)
+			sys->print("\t%s:%s: <%s>\n", loc.systemid, o2l(loc.systemid, (hd il).fileoffset), (hd il).name);
+	}
+}
+
+# convert file offset to line number... not very efficient, but we don't really care.
+o2l(f: string, o: int): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil)
+		return "#" + string o;
+	buf := array[o] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n < o)
+		return "#" + string o;
+	nl := 1;
+	for (i := 0; i < len buf; i++)
+		if (buf[i] == byte '\n')
+			nl++;
+	return string nl;
+}
+
+check(f: string): int
+{
+	spawn warningproc(
+			warningch := chan of (Locator, string),
+			finch := chan of int,
+			tagstackch := chan of ref Item.Tag
+	);
+	(x, e) := xml->open(f, warningch, nil);
+	if (x == nil) {
+		sys->fprint(stderr, "%s: %s\n", f, e);
+		return -1;
+	}
+	{
+		parse(x, tagstackch, warningch);
+		warningch <-= (*ref Locator, nil);
+		return <-finch;
+	} exception ex {
+	"error" =>
+		warningch <-= (*ref Locator, nil);
+		<-finch;
+		return -1;
+	}
+}
+
+parse(x: ref Xml->Parser, tagstackch: chan of ref Item.Tag, warningch: chan of (Locator, string))
+{
+	for (;;) {
+		item := x.next();
+		if (item == nil)
+			return;
+		pick i := item {
+		Error =>
+			warningch <-= (i.loc, i.msg);
+			raise "error";
+		Tag =>
+			tagstackch <-= i;
+			x.down();
+			parse(x, tagstackch, warningch);
+			x.up();
+			tagstackch <-= nil;
+		}
+	}
+}
--- /dev/null
+++ b/appl/ebook/cssfont.b
@@ -1,0 +1,179 @@
+implement CSSfont;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Font: import draw;
+include "units.m";
+	units: Units;
+include "cssfont.m";
+
+# locally available font styles
+BOLD, CW, ITALIC, PLAIN, NSTYLES: con iota;
+NSIZES: con 5;			# number of locally available font sizes
+
+fonts := array[] of {
+	PLAIN => array[] of {
+		"/fonts/charon/plain.tiny.font",
+		"/fonts/charon/plain.small.font",
+		"/fonts/charon/plain.normal.font",
+		"/fonts/charon/plain.large.font",
+		"/fonts/charon/plain.vlarge.font",
+	},
+	BOLD => array[] of {
+		"/fonts/charon/bold.tiny.font",
+		"/fonts/charon/bold.small.font",
+		"/fonts/charon/bold.normal.font",
+		"/fonts/charon/bold.large.font",
+		"/fonts/charon/bold.vlarge.font",
+		},
+	CW => array[] of {
+		"/fonts/charon/cw.tiny.font",
+		"/fonts/charon/cw.small.font",
+		"/fonts/charon/cw.normal.font",
+		"/fonts/charon/cw.large.font",
+		"/fonts/charon/cw.vlarge.font",
+		},
+	ITALIC => array[] of {
+		"/fonts/charon/italic.tiny.font",
+		"/fonts/charon/italic.small.font",
+		"/fonts/charon/italic.normal.font",
+		"/fonts/charon/italic.large.font",
+		"/fonts/charon/italic.vlarge.font",
+	},
+};
+
+fontinfo := array[NSTYLES] of array of ref Font;
+sizechoice := array[NSTYLES] of array of byte;
+
+init(displ: ref Draw->Display)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	units = load Units Units->PATH;
+	if (units == nil) {
+		sys->fprint(sys->fildes(2), "cssfont: cannot load %s: %r\n", Units->PATH);
+		raise "fail:bad module";
+	}
+	units->init();
+
+	for (i := 0; i < len fonts; i++) {
+		fpaths := fonts[i];
+		fontinfo[i] = array[len fpaths] of ref Font;
+
+		# could make this process lazier, only computing sizes
+		# when a font of a particular class was asked for.
+		maxheight := 0;
+		for (j := 0; j < len fpaths; j++) {
+			if ((fontinfo[i][j] = f := Font.open(displ, fpaths[j])) == nil) {
+				sys->fprint(sys->fildes(2), "cssfont: font %s unavailable: %r\n", fpaths[j]);
+				raise "fail:font unavailable";
+			}
+			if (f.height > maxheight)
+				maxheight = f.height;
+		}
+		sizechoice[i] = array[maxheight + 1] of byte;
+		for (j = 0; j < maxheight + 1; j++)
+			sizechoice[i][j] = byte matchheight(j, fontinfo[i]);
+	}
+
+#	for (i = 0; i < NSTYLES; i++) {
+#		sys->print("class %d\n", i);
+#		for (j := 0; j < NSIZES; j++) {
+#			sys->print("	height %d; translates to %d [%d]\n",
+#				fontinfo[i][j].height,
+#				int sizechoice[i][fontinfo[i][j].height],
+#				fontinfo[i][int sizechoice[i][fontinfo[i][j].height]].height);
+#		}
+#	}
+}
+
+# find the closest match to a given desired height from the choices given.
+matchheight(desired: int, choices: array of ref Font): int
+{
+	n := len choices;
+	if (desired <= choices[0].height)
+		return 0;
+	if (desired >= choices[n - 1].height)
+		return n - 1;
+	for (i := 1; i < n; i++) {
+		if (desired >= choices[i - 1].height &&
+				desired <= choices[i].height) {
+			if (desired - choices[i - 1].height <
+					choices[i].height - desired)
+				return i - 1;
+			else
+				return i;
+		}
+	}
+	sys->fprint(sys->fildes(2), "cssfont: can't happen!\n");
+	raise "error";
+	return -1;		# should never happen
+}
+
+# get an appropriate font given the css specification.
+getfont(spec: Spec, parentem, parentex: int): (string, int, int)
+{
+#sys->print("getfont size:%s family:%s; style:%s; weight:%s -> ",
+#		spec.size, spec.family, spec.style, spec.weight);
+	class := getclass(spec);
+	i := choosesize(class, spec.size, parentem, parentex);
+
+#sys->print("%s (height:%d)\n", fonts[class][i], fontinfo[class][i].height);
+
+	# XXX i suppose we should really find out what height(widgth?) the 'x' is.
+	return (fonts[class][i], fontinfo[class][i].height, fontinfo[class][i].height);
+}
+
+getclass(spec: Spec): int
+{
+	if (spec.family == "monospace")
+		return CW;
+	if (spec.style == "italic")
+		return ITALIC;
+	if (spec.weight == "bold")
+		return BOLD;
+	return PLAIN;
+}
+
+choosesize(class: int, size: string, parentem, parentex: int): int
+{
+	if (size != nil && (size[0] >= '0' && size[0] <= '9')) {
+		(height, nil) := units->length(size, parentem, parentex, nil);
+		choices := sizechoice[class];
+		if (height > len choices)
+			height = len choices - 1;
+		return int choices[height];
+	}
+	case size {
+	"xx-small" or
+	"x-small" =>
+		return 0;
+	"small" =>
+		return 1;
+	"medium" =>
+		return 2;
+	"large" =>
+		return 3;
+	"x-large" or
+	"xx-large" =>
+		return 4;
+	"larger" or
+	"smaller" =>
+		choice := sizechoice[class];
+		if (parentem >= len choice)
+			parentem = len choice - 1;
+		i := int choice[parentem];
+		if (size[0] == 's') {
+			if (i > 0)
+				i--;
+		} else {
+			if (i < len fonts[class] - 1)
+				i++;
+		}
+		return i;
+	* =>
+		sys->fprint(sys->fildes(2), "cssfont: unknown font size spec '%s'\n", size);
+		return 2;
+	}
+}
--- /dev/null
+++ b/appl/ebook/cssfont.m
@@ -1,0 +1,9 @@
+CSSfont: module {
+	PATH: con "/dis/ebook/cssfont.dis";
+	Spec: adt {
+		family, style, weight, size: string;
+	};
+
+	init:		fn(displ: ref Draw->Display);
+	getfont:	fn(spec: Spec, parentem, parentex: int): (string, int, int);
+};
--- /dev/null
+++ b/appl/ebook/cssparser.b
@@ -1,0 +1,143 @@
+implement CSSparser;
+
+include "sys.m";
+	sys: Sys;
+include "string.m";
+	str: String;
+include "css.m";
+	css: CSS;
+	Stylesheet, Statement, Select, Value: import css;
+include "cssparser.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	css = load CSS CSS->PATH;
+	if (css == nil) {
+		sys->fprint(sys->fildes(2), "cssparser: cannot load %s: %r\n", CSS->PATH);
+		raise "fail:bad module";
+	}
+	css->init(1);
+}
+
+parse(s: string): list of (string, list of Decl)
+{
+	(stylesheet, e) := css->parse(s);
+	if (stylesheet == nil) {
+		warning("error parsing stylesheet: " + e);
+		return nil;
+	}
+	rules, r: list of (string, list of Decl);
+	for (stl := stylesheet.statements; stl != nil; stl = tl stl) {
+		pick st := hd stl {
+		Ruleset =>
+			rules = ruleset2rule(st, rules);
+		}
+	}
+	for (; rules != nil; rules = tl rules)
+		r = hd rules :: r;
+	return r;
+}
+
+ruleset2rule(statement: ref Statement.Ruleset, onto: list of (string, list of Decl)): list of (string, list of Decl)
+{
+	d := makedecls(statement.decls);
+	
+	names: list of string;
+	for (sels := statement.selectors; sels != nil; sels = tl sels) {
+		csel := hd sels;
+		if (len csel != 1) {
+			warning("context-specific selectors not allowed");
+			continue;
+		}
+		(nil, l) := hd csel;
+		if ((name := selector2name(l)) != nil)
+			names = name :: names;
+	}
+	for (; names != nil; names = tl names)
+		onto = (hd names, d) :: onto;
+	
+	return onto;
+}
+
+makedecls(decls: list of ref CSS->Decl): list of Decl
+{
+	d: list of Decl;
+	for (; decls != nil; decls = tl decls) {
+		nd: Decl;
+		nd.name = (hd decls).property;
+		nd.important = (hd decls).important;
+		s := "";
+		for (vals := (hd decls).values; vals != nil; vals = tl vals) {
+			vs: string;
+			pick v := hd vals {
+			Percentage =>
+				vs = v.value + "%";
+			String or
+			Number or
+			Url or
+			Unicoderange =>
+				vs = v.value;
+			Hexcolour =>
+				vs = rgb2s(v.rgb);
+			RGB =>
+				vs = rgb2s(v.rgb);
+			Ident =>
+				vs = v.name;
+			Unit =>
+				vs = v.value + v.units;
+			}
+			if (s != nil)
+				s[len s] = (hd vals).sep;
+			s += vs;
+		}
+		nd.val = s;
+		d = nd :: d;
+	}
+	return d;
+}
+
+rgb2s(rgb: (int, int, int)): string
+{
+	(r, g, b) := rgb;
+	return sys->sprint("#%.2x%.2x%.2x", r, g, b);
+}
+
+warning(s: string)
+{
+	sys->fprint(sys->fildes(2), "cssparser: %s\n", s);
+}
+
+selector2name(sel: list of ref Select): string
+{
+	tag: string;
+	class: string;
+	pseudo: string;
+
+	for (; sel != nil; sel = tl sel) {
+		pick v := hd sel {
+		Element =>
+			tag = v.name;
+		Class =>
+			class = "." + v.name;
+		Pseudo =>
+			class = ":" + v.name;
+		* =>
+			warning("unknown selector type " + string tagof(hd sel));
+		}
+	}
+	return tag + class + pseudo;
+}
+
+parsedecl(s: string): list of Decl
+{
+	if (s == nil)
+		return nil;
+	(d, e) := css->parsedecl(s);
+	if (d == nil) {
+		warning(e);
+		return nil;
+	}
+	return makedecls(d);
+}
--- /dev/null
+++ b/appl/ebook/cssparser.m
@@ -1,0 +1,11 @@
+CSSparser: module {
+	PATH: con "/dis/ebook/cssparser.dis";
+	Decl: adt {
+		name:		string;
+		important:	int;
+		val:			string;
+	};
+	init:		fn();
+	parse:	fn(s: string): list of (string, list of Decl);
+	parsedecl:	fn(s: string): list of Decl;
+};
--- /dev/null
+++ b/appl/ebook/dtd/oebdoc101.dtd
@@ -1,0 +1,524 @@
+<!--
+    Document Type Definition for the Open eBook document version 1.0.1
+
+    Version:  1.0.1
+    Revision: 20010201-x
+
+    Authors:  Gunter Hille <hille@abc.de>
+              Ben Trafford <bent@exemplary.net>
+              Garret Wilson <garret@globalmentor.com>
+
+    Usage:
+        <?xml version="1.0"?>
+        <!DOCTYPE html PUBLIC
+          "+//ISBN 0-9673008-1-9//DTD OEB 1.0.1 Document//EN"
+          "http://openebook.org/dtds/oeb-1.0.1/oebdoc101.dtd">
+        <html>
+        ...
+        </html>
+
+    References:
+      This DTD has been derived from XHTML 1.0 and HTML 4.0.
+      It is a pure subset of neither.
+      Transitional XHTML 1.0 DTD at http://www.w3.org/TR/xhtml1/DTD/transitional.dtd
+-->
+
+<!-- ******** Character Mnemonic Entities ******** -->
+
+<!-- OEB supports all XHTML mnemonics, but uses only one entity file. -->
+<!ENTITY % OEBEntities PUBLIC "+//ISBN 0-9673008-1-9//DTD OEB 1.0 Entities//EN" "oeb1.ent">
+%OEBEntities;
+
+<!-- ******** Attribute Types ******** -->
+
+<!-- Color: A color specification. -->
+<!ENTITY % Color "CDATA">
+
+<!-- Coords: Comma-separated coordinates for image maps. -->
+<!ENTITY % Coords "CDATA">
+
+<!-- LanguageCode: An RFC1766 language code. -->
+<!ENTITY % LanguageCode "NMTOKEN">
+
+<!-- Length: Number of pixels or percentage in one dimension. -->
+<!ENTITY % Length "CDATA">
+
+<!-- LinkTypes: List of types of document link types, used by "rel" and "rev". -->
+<!ENTITY % LinkTypes "CDATA">
+
+<!-- MediaType: An RFC2045 media type. -->
+<!ENTITY % MediaType "CDATA">
+
+<!-- MediaDest: Intended media destination. -->
+<!ENTITY % MediaDest "CDATA">
+
+<!-- A string of one or more digits. -->
+<!ENTITY % Number "CDATA">
+
+<!-- ObjectAlign: Non-text multidirectional alignment options. -->
+<!ENTITY % ObjectAlign "(top|middle|bottom|left|right)">
+
+<!-- ObjectHAlign: Horizontal non-text alignment options. -->
+<!ENTITY % ObjectHAlign "(left|center|right)">
+
+<!-- Shape: Shapes available for image maps. -->
+<!ENTITY % Shape "(rect|circle|poly|default)">
+
+<!-- StyleData: Style data (e.g. CSS) -->
+<!ENTITY % StyleData "CDATA">
+
+<!-- Text: Character data for such attributes as "title" and "alt". -->
+<!ENTITY % Text "CDATA">
+
+<!-- TextHAlign: Horizontal text alignment options. -->
+<!ENTITY % TextHAlign "(left|center|right|justify)">
+
+<!-- TextVAlign: Vertical text alignment options. -->
+<!ENTITY % TextVAlign "(top|middle|bottom)">
+
+<!-- URI: An RFC2396 Uniform Resource Identifier. -->
+<!ENTITY % URI "CDATA">
+
+<!-- A list of URIs separated by spaces. -->
+<!ENTITY % URIList "CDATA">
+
+<!-- ******** Common Attributes ******** -->
+
+<!-- InternationalAttributes: Attributes for internationalization.
+  xml:lang:     XML language code.
+-->
+<!ENTITY % InternationalAttributes
+  "xml:lang %LanguageCode; #IMPLIED"
+>
+
+<!-- CoreAttributes: Most common attributes used by many elements.
+  id:       ID unique to the entire document.
+  class:    List of classes.
+  style:    Style data.
+  title:    Title or additional information.
+-->
+<!ENTITY % CoreAttributes
+  "id     ID            #IMPLIED
+  class   CDATA         #IMPLIED
+  style   %StyleData;   #IMPLIED
+  title   %Text;        #IMPLIED"
+>
+
+<!-- CommonAttributes: Common attributes used by many elements.
+  CoreAttributes:   Most common attributes.
+  InternationalAttributes:         Internationalization attributes.
+-->
+<!ENTITY % CommonAttributes
+  "%CoreAttributes;
+  %InternationalAttributes;"
+>
+
+<!-- ******** Common Elements ******** -->
+
+<!-- HeadingElements: <h1>..<h6> -->
+<!ENTITY % HeadingElements "h1|h2|h3|h4|h5|h6">
+
+<!-- ListElements: Elements for lists. -->
+<!ENTITY % ListElements "ul|ol|dl">
+
+<!-- PhraseElements: Inline elements that contain a phrase of text.
+  Note that while in current HTML implementations many PhraseElements
+  are rendered identically to FontStyleElements counterparts (such as
+  <em> and <i>, the former do not connotate rendering styles.
+-->
+<!ENTITY % PhraseElements
+  "em | strong | dfn | code | q | sub | sup | samp | kbd | var | cite"
+>
+
+<!-- FontStyleElements: Inline font style elements.
+  Note that many FontStyleElements have been deprecated in favor of
+  their PhraseElements counterparts.
+-->
+<!ENTITY % FontStyleElements
+  "tt | i | b | big | small | u | s | strike |font"
+>
+
+<!-- BlockElements: Elements at the block level. -->
+<!ENTITY % BlockElements
+  "%HeadingElements; | %ListElements; | p | pre | hr | blockquote
+  | center | div | table"
+>
+
+<!-- InlineElements: Elements that are inline. -->
+<!ENTITY % InlineElements
+  "%PhraseElements; | %FontStyleElements; | a | br | span | img | object | map"
+>
+
+<!-- BlockOrInlineElements: Elements that can be either block or inline. -->
+<!ENTITY % BlockOrInlineElements "script">
+
+<!-- %FlowElements: Both block and inline elements, including those that can be both. -->
+<!ENTITY % FlowElements "%BlockElements; | %InlineElements; | %BlockOrInlineElements;">
+
+<!-- ******** OEB Document Elements ******** -->
+
+<!ELEMENT br EMPTY>
+<!ATTLIST br
+  %CoreAttributes;
+  clear (left|all|right|none) "none"
+>
+
+<!ELEMENT span (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST span %CommonAttributes;>
+
+<!ELEMENT b (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST b %CommonAttributes;>
+
+<!ELEMENT big (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST big %CommonAttributes;>
+
+<!ELEMENT i (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST i %CommonAttributes;>
+
+<!ELEMENT small (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST small %CommonAttributes;>
+
+<!ELEMENT sub (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST sub %CommonAttributes;>
+
+<!ELEMENT sup (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST sup %CommonAttributes;>
+
+<!ELEMENT tt (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST tt %CommonAttributes;>
+
+<!ELEMENT font (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST font
+  %CommonAttributes;
+  size    CDATA     #IMPLIED
+  color   %Color;   #IMPLIED
+  face    CDATA     #IMPLIED
+>
+
+<!ELEMENT s (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST s %CommonAttributes;>
+
+<!ELEMENT strike (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST strike %CommonAttributes;>
+
+<!ELEMENT u (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST u %CommonAttributes;>
+
+<!ELEMENT cite (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST cite %CommonAttributes;>
+
+<!ELEMENT code (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST code %CommonAttributes;>
+
+<!ELEMENT dfn (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST dfn %CommonAttributes;>
+
+<!ELEMENT em (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST em %CommonAttributes;>
+
+<!ELEMENT kbd (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST kbd %CommonAttributes;>
+
+<!ELEMENT q (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST q
+  %CommonAttributes;
+  cite %URI; #IMPLIED
+>
+
+<!ELEMENT samp (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST samp %CommonAttributes;>
+
+<!ELEMENT strong (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST strong %CommonAttributes;>
+
+<!ELEMENT var (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST var %CommonAttributes;>
+
+<!ELEMENT div (#PCDATA | %FlowElements;)*>
+<!ATTLIST div
+  %CommonAttributes;
+  align %TextHAlign; #IMPLIED
+>
+
+<!ELEMENT p (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST p
+  %CommonAttributes;
+  align %TextHAlign; #IMPLIED
+>
+
+<!ELEMENT hr EMPTY >
+<!ATTLIST hr
+  %CommonAttributes;
+  align   %ObjectHAlign;  #IMPLIED
+  size    CDATA           #IMPLIED
+  width   %Length;        #IMPLIED
+>
+
+<!ELEMENT center (#PCDATA | %FlowElements;)*>
+<!ATTLIST center %CommonAttributes;>
+
+<!ELEMENT blockquote (#PCDATA | %FlowElements;)*>
+<!ATTLIST blockquote
+  %CommonAttributes;
+  cite %URI; #IMPLIED
+>
+
+<!ELEMENT pre
+  (#PCDATA | %PhraseElements; | a | br | span | map | tt | i | b | u | s)*
+>
+<!ATTLIST pre
+  %CommonAttributes;
+  xml:space (preserve) #FIXED "preserve"
+>
+
+<!-- Heading Elements -->
+
+<!ELEMENT h1 (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST h1
+  %CommonAttributes;
+
+  align %TextHAlign; #IMPLIED
+>
+
+<!ELEMENT h2 (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST h2
+  %CommonAttributes;
+  align %TextHAlign; #IMPLIED
+>
+
+<!ELEMENT h3 (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST h3
+  %CommonAttributes;
+  align %TextHAlign; #IMPLIED
+>
+
+<!ELEMENT h4 (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST h4
+  %CommonAttributes;
+  align %TextHAlign; #IMPLIED
+>
+
+<!ELEMENT h5 (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST h5
+  %CommonAttributes;
+  align %TextHAlign; #IMPLIED
+>
+
+<!ELEMENT h6 (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST h6
+  %CommonAttributes;
+  align %TextHAlign; #IMPLIED
+>
+
+<!ELEMENT script (#PCDATA)>
+<!ATTLIST script
+  xml:space (preserve) #FIXED "preserve"
+>
+
+<!ELEMENT style (#PCDATA)>
+<!ATTLIST style
+  %InternationalAttributes;
+  type        %MediaType;   #FIXED  "text/x-oeb1-css"
+  title       %Text;        #IMPLIED
+  xml:space (preserve) #FIXED "preserve"
+>
+
+<!ELEMENT img  EMPTY >
+<!ATTLIST img
+  %CommonAttributes;
+  align     %ObjectAlign;   #IMPLIED
+  border    CDATA           #IMPLIED
+  hspace    CDATA           #IMPLIED
+  vspace    CDATA           #IMPLIED
+  src       %URI;           #REQUIRED
+  alt       %Text;          #REQUIRED
+  longdesc  %URI;           #IMPLIED
+  height    %Length;        #IMPLIED
+  width     %Length;        #IMPLIED
+  usemap    %URI;           #IMPLIED
+>
+
+<!ELEMENT a
+   (#PCDATA | %FontStyleElements; | %PhraseElements; | %BlockOrInlineElements;
+   | br | span | object | img | map)*
+>
+<!ATTLIST a
+  %CommonAttributes;
+  name            NMTOKEN       #IMPLIED
+  href            %URI;         #IMPLIED
+  rel             %LinkTypes;   #IMPLIED
+  rev             %LinkTypes;   #IMPLIED
+>
+
+<!ELEMENT base  EMPTY>
+<!ATTLIST base
+  href %URI; #REQUIRED
+>
+
+<!ELEMENT link  EMPTY>
+<!ATTLIST link
+  %CommonAttributes;
+  href    %URI;         #IMPLIED
+  type    %MediaType;   #REQUIRED
+  rel     %LinkTypes;   #IMPLIED
+  rev     %LinkTypes;   #IMPLIED
+  media   %MediaDest;   #IMPLIED
+>
+
+<!-- The CommonAttributes entity is not used here because in this case
+  the "id" attribute is required. -->
+<!ELEMENT map ((%BlockElements; | %BlockOrInlineElements;)+ | area+)>
+<!ATTLIST map
+  %InternationalAttributes;
+  id      ID            #REQUIRED
+  class   CDATA         #IMPLIED
+  style   %StyleData;   #IMPLIED
+  title   %Text;        #IMPLIED
+  name    NMTOKEN       #IMPLIED
+>
+
+<!ELEMENT area  EMPTY>
+<!ATTLIST area
+  %CommonAttributes;
+  href    %URI;     #IMPLIED
+  shape   %Shape;   "rect"
+  coords  %Coords;  #IMPLIED
+  nohref  (nohref)  #IMPLIED
+  alt     %Text;    #REQUIRED
+>
+
+<!ELEMENT object
+  (#PCDATA | %BlockElements; | %InlineElements; | %BlockOrInlineElements; | param)*
+>
+<!ATTLIST object
+  %CommonAttributes;
+  classid   %URI;           #IMPLIED
+  codebase  %URI;           #IMPLIED
+  data      %URI;           #IMPLIED
+  type      %MediaType;     #IMPLIED
+  codetype  %MediaType;     #IMPLIED
+  archive   %URIList;       #IMPLIED
+  height    %Length;        #IMPLIED
+  width     %Length;        #IMPLIED
+  usemap    %URI;           #IMPLIED
+  align     %ObjectAlign;   #IMPLIED
+  border    CDATA           #IMPLIED
+  hspace    CDATA           #IMPLIED
+  vspace    CDATA           #IMPLIED
+>
+
+<!ELEMENT param EMPTY >
+<!ATTLIST param
+  id          ID                  #IMPLIED
+  name        CDATA               #REQUIRED
+  value       CDATA               #IMPLIED
+  valuetype   (data|ref|object)   "data"
+  type        %MediaType;         #IMPLIED
+>
+
+<!ELEMENT dl  (dt|dd)+ >
+<!ATTLIST dl %CommonAttributes;>
+
+<!ELEMENT dt (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST dt %CommonAttributes;>
+
+<!ELEMENT dd (#PCDATA | %FlowElements;)*>
+<!ATTLIST dd %CommonAttributes;>
+
+<!ELEMENT ol (li)+ >
+<!ATTLIST ol
+  %CommonAttributes;
+  type (1|a|A|i|I) #IMPLIED
+>
+
+<!ELEMENT ul (li)+>
+<!ATTLIST ul %CommonAttributes;>
+
+<!ELEMENT li (#PCDATA | %FlowElements;)*>
+<!ATTLIST li %CommonAttributes;>
+
+<!ELEMENT table  (caption?, tr+) >
+<!ATTLIST table
+  %CommonAttributes;
+  summary       %Text;          #IMPLIED
+  width         %Length;        #IMPLIED
+  border        CDATA           #IMPLIED
+  cellspacing   %Length;        #IMPLIED
+  cellpadding   %Length;        #IMPLIED
+  align         %ObjectHAlign;  #IMPLIED
+  bgcolor       %Color;         #IMPLIED
+>
+
+<!ELEMENT caption (#PCDATA | %InlineElements; | %BlockOrInlineElements;)*>
+<!ATTLIST caption %CommonAttributes;>
+
+<!ELEMENT tr  ( th | td )+ >
+<!ATTLIST tr
+  %CommonAttributes;
+  valign    %TextVAlign;    #IMPLIED
+  bgcolor   %Color;         #IMPLIED
+>
+
+<!ELEMENT th (#PCDATA | %FlowElements;)*>
+<!ATTLIST th
+  %CommonAttributes;
+  abbr      %Text;          #IMPLIED
+  rowspan   %Number;        "1"
+  colspan   %Number;        "1"
+  align     %TextHAlign;    #IMPLIED
+  valign    %TextVAlign;    #IMPLIED
+  nowrap    (nowrap)        #IMPLIED
+  bgcolor   %Color;         #IMPLIED
+  width     %Length;        #IMPLIED
+  height    %Length;        #IMPLIED
+>
+
+<!ELEMENT td (#PCDATA | %FlowElements;)*>
+<!ATTLIST td
+  %CommonAttributes;
+  abbr      %Text;          #IMPLIED
+  rowspan   %Number;        "1"
+  colspan   %Number;        "1"
+  align     %TextHAlign;    #IMPLIED
+  valign    %TextVAlign;    #IMPLIED
+  nowrap    (nowrap)        #IMPLIED
+  bgcolor   %Color;         #IMPLIED
+  width     %Length;        #IMPLIED
+  height    %Length;        #IMPLIED
+>
+
+<!ELEMENT title  (#PCDATA)>
+<!ATTLIST title %InternationalAttributes;>
+
+<!ELEMENT meta  EMPTY >
+<!ATTLIST meta
+  %InternationalAttributes;
+  name      NMTOKEN   #IMPLIED
+  content   CDATA     #REQUIRED
+  scheme    CDATA     #IMPLIED
+>
+
+<!-- HeadElements: Elements that can appear many places within <head>.-->
+<!ENTITY % HeadElements "script | style | meta | link | object">
+
+<!-- <head> can have the common head elements (always optional),
+  with one <title> and an optional <base> interspersed.
+-->
+<!ELEMENT head ((%HeadElements;)*,
+ ((title, (%HeadElements;)*, (base, (%HeadElements;)*)?)
+ | (base, (%HeadElements;)*, (title, (%HeadElements;)*))))
+>
+<!ATTLIST head %InternationalAttributes;>
+
+<!ELEMENT body (#PCDATA | %FlowElements;)*>
+<!ATTLIST body
+  %CommonAttributes;
+  bgcolor   %Color;   #IMPLIED
+  text      %Color;   #IMPLIED
+>
+
+<!ELEMENT html (head?, body)>
+<!ATTLIST html
+  %InternationalAttributes;
+  xmlns         %URI;   #FIXED  "http://openebook.org/namespaces/oeb-document/1.0/"
+>
--- /dev/null
+++ b/appl/ebook/dtd/oebpkg101.dtd
@@ -1,0 +1,280 @@
+<!--
+    Document Type Definition for the Open eBook package version 1.0.1
+
+    Version:  1.0.1
+    Revision: 20010201-x
+
+    Authors:  Steve DeRose <steven_derose@brown.edu>
+              Gunter Hille <hille@abc.de>
+              Ben Trafford <bent@exemplary.net>
+              Garret Wilson <garret@globalmentor.com>
+
+    Usage:
+        <?xml version="1.0"?>
+        <!DOCTYPE package
+          PUBLIC "+//ISBN 0-9673008-1-9//DTD OEB 1.0.1 Package//EN"
+          "http://openebook.org/dtds/oeb-1.0.1/oebpkg101.dtd">
+        <package unique-identifier="foo">
+          metadata
+          manifest
+          spine
+          tours
+          guide
+        </package>
+
+    References:
+      Transitional XHTML 1.0 DTD at http://www.w3.org/TR/xhtml1/DTD/transitional.dtd
+-->
+
+<!-- ******** Character Mnemonic Entities ******** -->
+
+<!-- OEB supports all XHTML mnemonics, but uses only one entity file. -->
+<!ENTITY % OEBEntities PUBLIC "+//ISBN 0-9673008-1-9//DTD OEB 1.0 Entities//EN" "oeb1.ent">
+%OEBEntities;
+
+<!-- ******** Attribute Types ******** -->
+
+<!-- LanguageCode: An RFC1766 language code. -->
+<!ENTITY % LanguageCode "NMTOKEN">
+
+<!-- URI: An RFC2396 Uniform Resource Identifier. -->
+<!ENTITY % URI "CDATA">
+
+<!-- ******** Common Attributes ******** -->
+
+<!-- InternationalAttributes: Attributes for internationalization.
+  xml:lang:     XML language code.
+-->
+<!ENTITY % InternationalAttributes
+  "xml:lang %LanguageCode; #IMPLIED"
+>
+
+<!-- CoreAttributes: Most common attributes used by many elements.
+  id:       ID unique to the entire document.
+-->
+<!ENTITY % CoreAttributes "id ID #IMPLIED">
+
+<!-- CommonAttributes: Common attributes used by many elements.
+  CoreAttributes:   Most common attributes.
+  InternationalAttributes:         Internationalization attributes.
+-->
+<!ENTITY % CommonAttributes
+  "%CoreAttributes;
+  %InternationalAttributes;"
+>
+
+<!-- DCNamespaceAttribute: Attribute that declare the Dublin Core
+  namespace. Used mostly on each <dc:XXX> element to accomodate XML
+  parsers which require this unnecessarily. -->
+<!ENTITY % DCNamespaceAttribute
+  "xmlns:dc   %URI;   #FIXED  'http://purl.org/dc/elements/1.0/'"
+>
+
+<!-- ******** OEB Package Elements ******** -->
+
+<!-- A package must have metadata, a manifest, and a spine,
+	and optionally may include a tours and/or a guide section. -->
+<!ELEMENT package	(metadata, manifest, spine, tours?, guide?)>
+<!ATTLIST package
+  %CommonAttributes;
+  unique-identifier   IDREF	#REQUIRED
+  xmlns         %URI;   #FIXED  "http://openebook.org/namespaces/oeb-package/1.0/"
+>
+
+<!-- The metadata section must have dc-metadata but may or may not
+  include x-metadata. -->
+<!ELEMENT  metadata	(dc-metadata, x-metadata?)>
+
+<!-- These elements are optional in <dc-metadata>. -->
+<!ENTITY % DCMetadataOptionalElements
+  "dc:Contributor  | dc:Creator | dc:Subject | dc:Description
+  | dc:Publisher |  dc:Date | dc:Type | dc:Format | dc:Source
+  | dc:Language | dc:Relation | dc:Coverage | dc:Rights"
+>
+
+<!-- These are the optional and required elements of <dc-metadata>. -->
+<!ENTITY % DCMetadataGeneralElements
+  "%DCMetadataOptionalElements; | dc:Title |dc:Identifier"
+>
+
+<!-- A dc-metadata section must have a dc:Title and a
+  dc:Identifier, and optionally other dc:XXX elements, all in any
+  order. -->
+<!ELEMENT dc-metadata ((%DCMetadataOptionalElements;)*,
+  ((dc:Title, (%DCMetadataOptionalElements; | dc:Title)*,
+    (dc:Identifier, (%DCMetadataGeneralElements;)*)) |
+  (dc:Identifier, (%DCMetadataOptionalElements; | dc:Identifier)*,
+    (dc:Title, (%DCMetadataGeneralElements;)*))))
+>
+
+<!ATTLIST dc-metadata
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+  xmlns:oebpackage    %URI; #FIXED "http://openebook.org/namespaces/oeb-package/1.0/"
+>
+
+<!-- A dc:Contributor element may optionally have a role and/or a file-as
+  attribute. -->
+<!ELEMENT dc:Contributor (#PCDATA)>
+<!ATTLIST dc:Contributor
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+  role      NMTOKEN   #IMPLIED
+  file-as   CDATA	    #IMPLIED
+>
+
+<!ELEMENT dc:Title (#PCDATA)>
+<!ATTLIST dc:Title
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Creator (#PCDATA)>
+<!ATTLIST dc:Creator
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+  role      NMTOKEN   #IMPLIED
+  file-as   CDATA     #IMPLIED
+>
+
+<!ELEMENT dc:Subject (#PCDATA)>
+<!ATTLIST dc:Subject
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Description (#PCDATA)>
+<!ATTLIST dc:Description
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Publisher (#PCDATA)>
+<!ATTLIST dc:Publisher
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Date	(#PCDATA)>
+<!ATTLIST dc:Date
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+  event NMTOKEN	#IMPLIED
+>
+
+<!ELEMENT dc:Type (#PCDATA)>
+<!ATTLIST dc:Type
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Format (#PCDATA)>
+<!ATTLIST dc:Format
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Identifier (#PCDATA)>
+<!ATTLIST dc:Identifier
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+  scheme NMTOKEN #IMPLIED
+>
+
+<!ELEMENT dc:Source (#PCDATA)>
+<!ATTLIST dc:Source
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Language (#PCDATA)>
+<!ATTLIST dc:Language
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Relation (#PCDATA)>
+<!ATTLIST dc:Relation
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Coverage	(#PCDATA)>
+<!ATTLIST dc:Coverage
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!ELEMENT dc:Rights (#PCDATA)>
+<!ATTLIST dc:Rights
+  %CommonAttributes;
+  %DCNamespaceAttribute;
+>
+
+<!-- The <x-metadata> element must have at least one <meta> element. -->
+<!ELEMENT x-metadata (meta+)>
+<!ATTLIST x-metadata %CommonAttributes;>
+
+<!ELEMENT meta EMPTY>
+<!ATTLIST meta
+  %CommonAttributes;
+  name      NMTOKEN   #REQUIRED
+  content   CDATA     #REQUIRED
+  scheme    CDATA     #IMPLIED
+>
+
+<!-- The manifest must contain one or more items. -->
+<!ELEMENT manifest (item+)>
+<!ATTLIST manifest %CommonAttributes;>
+
+<!-- The CommonAttributes entity is not used here because in this case
+  the "id" attribute is required. -->
+<!ELEMENT item EMPTY>
+<!ATTLIST item
+  %InternationalAttributes;
+	id            ID      #REQUIRED
+	href          CDATA   #REQUIRED
+	media-type    CDATA   #REQUIRED
+	fallback      IDREF   #IMPLIED
+>
+
+<!-- The spine must contain one or more itemrefs. -->
+<!ELEMENT spine	(itemref+)>
+<!ATTLIST spine	%CommonAttributes;>
+
+<!ELEMENT itemref	EMPTY>
+<!ATTLIST itemref
+  %CommonAttributes;
+  idref	IDREF	#REQUIRED
+>
+
+<!-- The tours element must have one or more tour elements. -->
+<!ELEMENT tours	(tour+)>
+<!ATTLIST tours	%CommonAttributes;>
+
+<!-- Each tour of the set must contain at least one site. -->
+<!ELEMENT tour (site+)>
+<!ATTLIST tour
+  %CommonAttributes;
+  title	CDATA	#REQUIRED
+>
+
+<!-- Each site in a tour must have a title and an href. -->
+<!ELEMENT site EMPTY>
+<!ATTLIST site
+  %CommonAttributes;
+  title   CDATA   #REQUIRED
+  href    CDATA   #REQUIRED
+>
+
+<!-- The guide element must have one or more reference elements. -->
+<!ELEMENT guide	(reference+)>
+<!ATTLIST guide	%CommonAttributes;>
+
+<!ELEMENT reference	EMPTY>
+<!ATTLIST reference
+  %CommonAttributes;
+  type    NMTOKEN   #REQUIRED
+  title   CDATA     #REQUIRED
+  href    CDATA     #REQUIRED
+>
--- /dev/null
+++ b/appl/ebook/ebook.b
@@ -1,0 +1,1893 @@
+implement Ebook;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "keyboard.m";
+include "url.m";
+	url: Url;
+	ParsedUrl: import url;
+include "xml.m";
+include "stylesheet.m";
+include "cssparser.m";
+include "oebpackage.m";
+	oebpackage: OEBpackage;
+	Package: import oebpackage;
+include "reader.m";
+	reader: Reader;
+	Datasource, Mark, Block: import reader;
+include "profile.m";
+	profile: Profile;
+include "arg.m";
+
+Doprofile: con 0;
+
+# TO DO
+# - error notices.
+# + indexes based on display size and font information.
+# - navigation by spine contents
+# - navigation by guide, tour contents
+# - searching?
+
+Ebook: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Font: con "/fonts/charon/plain.small.font";
+LASTPAGE: con 16r7fffffff;
+
+Book: adt {
+	win: ref Tk->Toplevel;
+	evch: string;
+	size:	Point;
+	w: string;
+	showannot: int;
+
+	d: ref Document;
+	pkg: ref OEBpackage->Package;
+	fallbacks: list of (string, string);
+	item: ref OEBpackage->Item;
+	page: int;
+	indexprogress: chan of int;
+
+	sequence: list of ref OEBpackage->Item;		# currently selected sequence
+
+	new:		fn(f: string, win: ref Tk->Toplevel, w: string, evch: string, size: Point,
+					indexprogress: chan of int): (ref Book, string);
+	gotolink:	fn(book: self ref Book, where: string): string;
+	gotopage:	fn(book: self ref Book, page: int);
+	goto:	fn(book: self ref Book, m: ref Bookmark);
+	mark:	fn(book: self ref Book): ref Bookmark;
+	forward:	fn(book: self ref Book);
+	back:	fn(book: self ref Book);
+	showannotations: fn(book: self ref Book, showannot: int);
+	show:	fn(book: self ref Book, item: ref OEBpackage->Item);
+	title:		fn(book: self ref Book): string;
+};
+
+Bookmark: adt {
+	item:		ref OEBpackage->Item;
+	page:	int;		# XXX should be fileoffset
+};
+
+Document: adt {
+	w:		string;
+	p:		ref Page;		# current page
+	firstmark:	ref Mark;		# start  of first element on current page
+	endfirstmark:	ref Mark;	# end of first element on current page
+	lastmark:	ref Mark;		# start of last element on current page
+	endlastmark:	ref Mark;	# end of last element on current page (nil if we're there)
+	nextoffset:	int;		# y offset of first element on next page
+	datasrc:	ref Datasource;
+	indexed:	int;
+	pagenum:	int;
+	size:		Point;
+	index:	ref Index;
+	annotations: array of ref Annotation;
+	showannot: int;
+	item:		ref OEBpackage->Item;
+	fallbacks:	list of (string, string);
+	indexprogress: chan of int;
+
+	new:		fn(i: ref OEBpackage->Item, fallbacks: list of (string, string),
+				win: ref Tk->Toplevel, w: string, size: Point, evch: string,
+				indexprogress: chan of int): (ref Document, string);
+	fileoffset:	fn(d: self ref Document): int;
+	title:		fn(d: self ref Document): string;
+	goto:	fn(d: self ref Document, n: int): int;
+	gotooffset:	fn(d: self ref Document, o: int);
+	gotolink:	fn(d: self ref Document, name: string): int;
+
+	addannotation: fn(d: self ref Document, a: ref Annotation);
+	delannotation: fn(d: self ref Document, a: ref Annotation);
+	getannotation: fn(d: self ref Document, fileoffset: int): ref Annotation;
+	updateannotation: fn(d: self ref Document, a: ref Annotation);
+	showannotations: fn(d: self ref Document, show: int);
+	writeannotations: fn(d: self ref Document): string;
+};
+
+
+Index: adt {
+	rq:		chan of (int, chan of (int, (ref Mark, int)));
+	linkrq:	chan of (string, chan of int);
+	indexed:	chan of (array of (ref Mark, int), ref Links);
+	d:		ref Datasource;
+	size:		Point;
+	length:	int;			# length of index file
+	f:		string;		# name of index file
+
+	new:		fn(i: ref OEBpackage->Item, d:  ref Datasource, size: Point, force: int,
+				indexprogress: chan of int): ref Index;
+	get:		fn(i: self ref Index, n: int): (int, (ref Mark, int));
+	getlink:	fn(i: self ref Index, name: string): int;
+	abort:	fn(i: self ref Index);
+	stop:	fn(i: self ref Index);
+};
+
+Page: adt {
+	win:		ref Tk->Toplevel;
+	w:		string;
+	min, max:	int;
+	height:	int;
+	yorigin:	int;
+	bmargin:	int;
+
+	new:		fn(win: ref Tk->Toplevel, w: string): ref Page;
+	del:		fn(p: self ref Page);
+	append:	fn(p: self ref Page, b: Block);
+	remove:	fn(p: self ref Page, atend: int):  Block;
+	scrollto:	fn(p: self ref Page, y: int);
+	count:	fn(p: self ref Page): int;
+	bbox:	fn(p: self ref Page, n: int): Rect;
+	bboxw:	fn(p: self ref Page, w: string): Rect;
+	canvasr:	fn(p: self ref Page, r: Rect): Rect;
+	window:	fn(p: self ref Page, n: int): string;
+	maxy:	fn(p: self ref Page): int;
+	conceal:	fn(p: self ref Page, y: int);
+	visible:	fn(p: self ref Page): int;
+	getblock:	fn(p: self ref Page, n: int): Block;
+};
+
+Annotationwidth: con "20w";
+Spikeradius: con 3;
+
+Annotation: adt {
+	fileoffset: int;
+	text: string;
+};
+
+stderr: ref Sys->FD;
+warningch: chan of (Xml->Locator, string);
+debug := 0;
+
+usage()
+{
+	sys->fprint(stderr, "usage: ebook [-m] bookfile\n");
+	raise "fail:usage";
+}
+
+Flatopts: con "-bg white -relief flat -activebackground white -activeforeground black";
+Menubutopts: con "-bg white -relief ridge -activebackground white -activeforeground black";
+
+gctxt: ref Draw->Context;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	gctxt = ctxt;
+	loadmods();
+
+	size := Point(400, 600);
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		badmodule(Arg->PATH);
+	arg->init(argv);
+	while((opt := arg->opt()) != 0)
+		case opt {
+		'm' =>
+			size = Point(240, 320);
+		'd' =>
+			debug = 1;
+		* =>
+			usage();
+		}
+	argv = arg->argv();
+	arg = nil;
+	if (len argv != 1)
+		usage();
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	reader->init(ctxt.display);
+	(win, ctlchan) := tkclient->toplevel(ctxt, nil, hd argv, Tkclient->Hide);
+	cch := chan of string;
+	tk->namechan(win, cch, "c");
+
+	evch := chan of string;
+	tk->namechan(win, evch, "evch");
+
+	cmd(win, "frame .f -bg white");
+	cmd(win, "button .f.up -text {↑} -command {send evch up}" + Flatopts);
+	cmd(win, "button .f.down -text {↓} -command {send evch down}" + Flatopts);
+	cmd(win, "button .f.next -text {→} -command {send evch forward}" + Flatopts);
+	cmd(win, "button .f.prev -text {←} -command {send evch back}" + Flatopts);
+	cmd(win, "label .f.pagenum -text 0 -bg white -relief flat  -bd 0 -width 8w -anchor e");
+	cmd(win, "menubutton .f.annot -menu .f.annot.m " + Menubutopts + " -text {Opts}");
+	cmd(win, "menu .f.annot.m");
+	cmd(win, ".f.annot.m add checkbutton -text {Annotations} -command {send evch annot} -variable annot");
+	cmd(win, ".f.annot.m invoke 0");
+	cmd(win, "pack .f.annot -side left");
+	cmd(win, "pack .f.pagenum .f.down .f.up  .f.next .f.prev -side right");
+	cmd(win, "focus .");
+	cmd(win, "bind .Wm_t <Button-1> +{focus .}");
+	cmd(win, "bind .Wm_t.title <Button-1> +{focus .}");
+	cmd(win, sys->sprint("bind . <Key-%c> {send evch up}", Keyboard->Up));
+	cmd(win, sys->sprint("bind . <Key-%c> {send evch down}", Keyboard->Down));
+	cmd(win, sys->sprint("bind . <Key-%c> {send evch forward}", Keyboard->Right));
+	cmd(win, sys->sprint("bind . <Key-%c> {send evch back}", Keyboard->Left));
+	cmd(win, "pack .f -side top -fill x");
+
+	# pack a temporary frame to see what size we're actually allocated.
+	cmd(win, "frame .tmp");
+	cmd(win, "pack .tmp -side top -fill both -expand 1");
+	cmd(win, "pack propagate . 0");
+	cmd(win, ". configure -width " + string size.x + " -height " + string size.y);
+#	fittoscreen(win);
+	size.x = int cmd(win, ".tmp cget -actwidth");
+	size.y = int cmd(win, ".tmp cget -actheight");
+	cmd(win, "destroy .tmp");
+
+	spawn showpageproc(win, ".f.pagenum", indexprogress := chan of int, pageprogress := chan of string);
+
+	(book, e) := Book.new(hd argv, win, ".d", "evch", size, indexprogress);
+	if (book == nil) {
+		pageprogress <-= nil;
+		sys->fprint(sys->fildes(2), "ebook: cannot open book: %s\n", e);
+		raise "fail:error";
+	}
+	if (book.pkg.guide != nil) {
+		makemenu(win, ".f.guide", "Guide", book.pkg.guide);
+		cmd(win, "pack .f.guide -before .f.pagenum -side left");
+	}
+		
+	cmd(win, "pack .d -side top -fill both -expand 1");
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	warningch = chan of (Xml->Locator, string);
+	spawn warningproc(warningch);
+	spawn handlerproc(book, evch, exitedch := chan of int, pageprogress);
+	for (;;) alt {
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-ctlchan =>
+		if (s == "exit") {
+			evch <-= "exit";
+			<-exitedch;
+		}
+		tkclient->wmctl(win, s);
+	}
+}
+
+makemenu(win: ref Tk->Toplevel, w: string, title: string, items: list of ref OEBpackage->Reference)
+{
+	cmd(win, "menubutton " + w + " -menu " + w + ".m " + Menubutopts + " -text '" + title);
+	m := w + ".m";
+	cmd(win, "menu " + m);
+	for (; items != nil; items = tl items) {
+		item := hd items;
+		# assumes URLs can't have '{}' in them.
+		cmd(win, m + " add command -text " + tk->quote(item.title) +
+			" -command {send evch goto " + item.href + "}");
+	}
+}
+
+loadmods()
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	str = load String String->PATH;
+	if (str == nil)
+		badmodule(String->PATH);
+
+	url = load Url Url->PATH;
+	if (url == nil)
+		badmodule(Url->PATH);
+	url->init();
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmodule(Tkclient->PATH);
+	tkclient->init();
+
+	reader = load Reader Reader->PATH;
+	if (reader == nil)
+		badmodule(Reader->PATH);
+
+	xml := load Xml Xml->PATH;
+	if (xml == nil)
+		badmodule(Xml->PATH);
+	xml->init();
+
+	oebpackage = load OEBpackage OEBpackage->PATH;
+	if (oebpackage == nil)
+		badmodule(OEBpackage->PATH);
+	oebpackage->init(xml);
+
+	if (Doprofile) {
+		profile = load Profile Profile->PATH;
+		if (profile == nil)
+			badmodule(Profile->PATH);
+		profile->init();
+		profile->sample(10);
+	}
+}
+
+showpageproc(win: ref Tk->Toplevel, w: string, indexprogress: chan of int, pageprogress: chan of string)
+{
+	page := "0";
+	indexed: int;
+	for (;;) {
+		alt {
+		page = <-pageprogress =>;
+		indexed = <-indexprogress =>;
+		}
+		if (page == nil)
+			exit;
+		cmd(win, w + " configure -text {" + page + "/" + string indexed + "}");
+		cmd(win, "update");
+	}
+}
+
+handlerproc(book: ref Book, evch: chan of string, exitedch: chan of int, pageprogress: chan of string)
+{
+	win := book.win;
+	newplace(book, pageprogress);
+	hist, fhist: list of ref Bookmark;
+	cmd(win, "update");
+	for (;;) {
+		(w, c) := splitword(<-evch);
+		if (Doprofile)
+			profile->start();
+#sys->print("event '%s' '%s'\n", w, c);
+		(olditem, oldpage) := (book.item, book.page);
+		case w {
+		"exit" =>
+			book.show(nil);		# force annotations to be written out.
+			exitedch <-= 1;
+			exit;
+		"forward" =>
+			book.forward();
+		"back" =>
+			book.back();
+		"up" =>
+			if (hist != nil) {
+				bm := book.mark();
+				book.goto(hd hist);
+				(hist, fhist) = (tl hist, bm :: fhist);
+			}
+		"down" =>
+			if (fhist != nil) {
+				bm := book.mark();
+				book.goto(hd fhist);
+				(hist, fhist) = (bm :: hist, tl fhist);
+			}
+		"goto" =>
+			(hist, fhist) = (book.mark() :: hist, nil);
+			e := book.gotolink(c);
+			if (e != nil)
+				notice("error getting link: " + e);
+
+		"ds" =>			# an event from a datasource-created widget
+			if (book.d == nil) {
+				oops("stray event 'ds " + c + "'");
+				break;
+			}
+			event := book.d.datasrc.event(c);
+			if (event == nil) {
+				oops(sys->sprint("nil event on 'ds %s'", c));
+				break;
+			}
+			pick ev := event {
+			Link =>
+				if (ev.url != nil) {
+					(hist, fhist) = (book.mark() :: hist, nil);
+					e := book.gotolink(ev.url);
+					if (e != nil)
+						notice("error getting link: " + e);
+				}
+			Texthit =>
+				a := ref Annotation(ev.fileoffset, nil);
+				spawn excessevents(evch);
+				editannotation(win, a);
+				evch <-= nil;
+				book.d.addannotation(a);
+			}
+		"annotclick" =>
+			a := book.d.getannotation(int c);
+			if (a == nil) {
+				notice("cannot find annotation at " + c);
+				break;
+			}
+			editannotation(win, a);
+			book.d.updateannotation(a);
+		"annot" =>
+			book.showannotations(int cmd(win, "variable annot"));
+		* =>
+			oops(sys->sprint("unknown event  '%s' '%s'", w, c));
+		}
+		if (olditem != book.item || oldpage != book.page)
+			newplace(book, pageprogress);
+		cmd(win, "update");
+		cmd(win, "focus .");
+		if (Doprofile)
+			profile->stop();
+	}
+}
+
+excessevents(evch: chan of string)
+{
+	while ((s := <-evch) != nil)
+		oops("excess: " + s);
+}
+
+newplace(book: ref Book, pageprogress: chan of string)
+{
+	pageprogress <-= book.item.id + "." + string (book.page + 1);
+	tkclient->settitle(book.win, book.title());
+}
+
+editannotation(pwin: ref Tk->Toplevel, annot: ref Annotation)
+{
+	(win, ctlchan) := tkclient->toplevel(gctxt,
+			"-x " + cmd(pwin, ". cget -actx") +
+			" -y " + cmd(pwin, ". cget -acty"), "Annotation", Tkclient->Appl);
+	cmd(win, "scrollbar .s -orient vertical -command {.t yview}");
+	cmd(win, "text .t -yscrollcommand {.s set}");
+	cmd(win, "pack .s -side left -fill y");
+	cmd(win, "pack .t -side top -fill both -expand 1");
+	cmd(win, "pack propagate . 0");
+	cmd(win, ". configure -width " + cmd(pwin, ". cget -width"));
+	cmd(win, ".t insert end '" + annot.text);
+	cmd(win, "update");
+	# XXX tk bug forces us to do this here rather than earlier
+	cmd(win, "focus .t");
+	cmd(win, "update");
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	for (;;) alt {
+	c := <-win.ctxt.kbd =>
+		tk->keyboard(win, c);
+	c := <-win.ctxt.ptr =>
+		tk->pointer(win, *c);
+	c := <-win.ctxt.ctl or
+	c = <-win.wreq or
+	c = <-ctlchan =>
+		case c {
+		"task" =>
+			cmd(pwin, ". unmap");
+			tkclient->wmctl(win, c);
+			cmd(pwin, ". map");
+			cmd(win, "raise .");
+			cmd(win, "update");
+		"exit" =>
+			annot.text = trim(cmd(win, ".t get 1.0 end"));
+			return;
+		* =>
+			tkclient->wmctl(win, c);
+		}
+	}
+}
+
+warningproc(c: chan of (Xml->Locator, string))
+{
+	for (;;) {
+		(loc, msg) := <-c;
+		if (msg == nil)
+			break;
+		warning(sys->sprint("%s:%d: %s", loc.systemid, loc.line, msg));
+	}
+}
+
+openpackage(f: string): (ref OEBpackage->Package, string)
+{
+	(pkg, e) := oebpackage->open(f, warningch);
+	if (pkg == nil)
+		return (nil, e);
+	nmissing := pkg.locate();
+	if (nmissing > 0)
+		warning(string nmissing + " items missing from manifest");
+	for (i := pkg.manifest; i != nil; i = tl i)
+		(hd i).file = cleanname((hd i).file);
+	return (pkg, nil);
+}
+
+blankbook: Book;
+Book.new(f: string, win: ref Tk->Toplevel, w: string, evch: string, size: Point,
+			indexprogress: chan of int): (ref Book, string)
+{
+	(pkg, e) := openpackage(f);
+	if (pkg == nil)
+		return (nil, e);
+	# give section numbers to all the items in the manifest.
+	# items in the spine are named sequentially;
+	# other items are given letters corresponding to their order in the manifest.
+	for (items := pkg.manifest; items != nil; items = tl items)
+		(hd items).id = nil;
+	i := 1;
+	for (items = pkg.spine; items != nil; items = tl items)
+		(hd items).id = string i++;
+	i = 0;
+	for (items = pkg.manifest; items != nil; items = tl items) {
+		if ((hd items).id == nil) {
+			c := 'A';
+			if (i >= 26)
+				c = 'α';
+			(hd items).id = sys->sprint("%c", c + i);
+			i++;
+		}
+	}
+	fallbacks: list of (string, string);
+	for (items = pkg.manifest; items != nil; items = tl items) {
+		item := hd items;
+		if (item.fallback != nil)
+			fallbacks = (item.file, item.fallback.file) :: fallbacks;
+	}
+
+	book := ref blankbook;
+	book.win = win;
+	book.evch = evch;
+	book.size = size;
+	book.w = w;
+	book.pkg = pkg;
+	book.sequence = pkg.spine;
+	book.fallbacks = fallbacks;
+	book.indexprogress = indexprogress;
+
+	cmd(win, "frame " + w + " -bg white");
+
+	if (book.sequence != nil) {
+		book.show(hd book.sequence);
+		if (book.d != nil)
+			book.page = book.d.goto(0);
+	}
+	return (book, nil);
+}
+
+Book.title(book: self ref Book): string
+{
+	if (book.d != nil)
+		return book.d.title();
+	return nil;
+}
+
+Book.mark(book: self ref Book): ref Bookmark
+{
+	if (book.d != nil)
+		return ref Bookmark(book.item, book.page);
+	return nil;
+}
+
+Book.goto(book: self ref Book, m: ref Bookmark)
+{
+	if (m != nil) {
+		book.show(m.item);
+		book.gotopage(m.page);
+	}
+}
+
+Book.gotolink(book: self ref Book, href: string): string
+{
+	fromfile: string;
+	if (book.item != nil)
+		fromfile = book.item.file;
+	(u, err) := makerelativeurl(fromfile, href);
+	if (u == nil)
+		return err;
+	if (book.d == nil || book.item.file != u.path) {
+		for (i := book.pkg.manifest; i != nil; i = tl i)
+			if ((hd i).file == u.path)
+				break;
+		if (i == nil)
+			return "item '" + u.path + "' not found in manifest";
+		book.show(hd i);
+	}
+	if (book.d != nil) {
+		if (u.frag != nil) {
+			if (book.d.gotolink(u.frag) == -1) {
+				warning(sys->sprint("link '%s' not found in '%s'", u.frag, book.item.file));
+				book.d.goto(0);
+			} else
+				book.page = book.d.pagenum;
+		} else
+			book.d.goto(0);
+		book.page = book.d.pagenum;
+	}
+	return nil;	
+}
+
+makerelativeurl(fromfile: string, href: string): (ref ParsedUrl, string)
+{
+	dir := "";
+	for(n := len fromfile; --n >= 0;) {
+		if(fromfile[n] == '/') {
+			dir = fromfile[0:n+1];
+			break;
+		}
+	}
+	u := url->makeurl(href);
+	if(u.scheme != Url->FILE && u.scheme != Url->NOSCHEME)
+		return (nil, sys->sprint("URL scheme %s not yet supported", url->schemes[u.scheme]));
+	if(u.host != "localhost" && u.host != nil)
+		return (nil, "non-local URLs not supported");
+	path := u.path;
+	if (path == nil)
+		u.path = fromfile;
+	else {
+		if(u.pstart != "/")
+			path = dir+path;	# TO DO: security
+		(ok, d) := sys->stat(path);
+		if(ok < 0)
+			return (nil, sys->sprint("'%s': %r", path));
+		u.path = path;
+	}
+	return (u, nil);
+}
+
+Book.gotopage(book: self ref Book, page: int)
+{
+	if (book.d != nil)
+		book.page = book.d.goto(page);
+}
+
+#if (goto(next page)) doesn't move on) {
+#	if (currentdocument is in sequence and it's not the last) {
+#		close(document);
+#		open(next in sequence)
+#		goto(page 0)
+#	}
+#}
+Book.forward(book: self ref Book)
+{
+	if (book.item == nil)
+		return;
+	if (book.d != nil) {
+		n := book.d.goto(book.page + 1);
+		if (n > book.page) {
+			book.page = n;
+			return;
+		}
+	}
+
+	# can't move further on, so try for next in sequence.
+	for (seq := book.sequence; seq != nil; seq = tl seq)
+		if (hd seq == book.item)
+			break;
+	# not found in current sequence, or nothing following it: nowhere to go.
+	if (seq == nil || tl seq == nil)
+		return;
+	book.show(hd tl seq);
+	if (book.d != nil)
+		book.page = book.d.goto(0);
+}
+
+Book.back(book: self ref Book)
+{
+	if (book.item == nil)
+		return;
+	if (book.d != nil) {
+		n := book.d.goto(book.page - 1);
+		if (n < book.page) {
+			book.page = n;
+			return;
+		}
+	}
+
+	# can't move back, so try for previous in sequence
+	prev: ref OEBpackage->Item;
+	for (seq := book.sequence; seq != nil; (prev, seq) = (hd seq, tl seq))
+		if (hd seq == book.item)
+			break;
+
+	# not found in current sequence, or no previous: nowhere to go
+	if (seq == nil || prev == nil)
+		return;
+
+	book.show(prev);
+	if (book.d != nil)
+		book.page = book.d.goto(LASTPAGE);
+}
+
+Book.show(book: self ref Book, item: ref OEBpackage->Item)
+{
+	if (book.item == item)
+		return;
+	if (book.d != nil) {
+		book.d.writeannotations();
+		book.d.index.stop();
+		cmd(book.win, "destroy " + book.d.w);
+		book.d = nil;
+	}
+	if (item == nil)
+		return;
+
+	(d, e) := Document.new(item,  book.fallbacks, book.win, book.w + ".d", book.size, book.evch, book.indexprogress);
+	if (d == nil) {
+		notice(sys->sprint("cannot load item %s: %s", item.href, e));
+		return;
+	}
+	d.showannotations(book.showannot);
+	cmd(book.win, "pack " + book.w + ".d -fill both");
+	book.page = -1;
+	book.d = d;
+	book.item = item;
+}
+
+Book.showannotations(book: self ref Book, showannot: int)
+{
+	book.showannot = showannot;
+	if (book.d != nil)
+		book.d.showannotations(showannot);
+}
+
+#actions:
+#	goto link
+#		if (link is to current document) {
+#			goto(link)
+#		} else {
+#			close(document)
+#			open(linked-to document)
+#			goto(link);
+#		}
+#
+#	next page
+#		if (goto(next page)) doesn't move on) {
+#			if (currentdocument is in sequence and it's not the last) {
+#				close(document);
+#				open(next in sequence)
+#				goto(page 0)
+#			}
+#		}
+#
+#	previous page
+#		if (page > 0) {
+#			goto(page - 1);
+#		} else {
+#			if (currentdocument is in sequence and it's not the first) {
+#				close(document)
+#				open(previous in sequence)
+#				goto(last page)
+#			}
+
+displayannotation(d: ref Document, r: Rect, annot: ref Annotation)
+{
+	tag := "o" + string annot.fileoffset;
+	(win, w) := (d.p.win, d.p.w);
+	a := cmd(win, w + " create text 0 0 -anchor nw -tags {annot " + tag + "}" +
+			" -width " + Annotationwidth +
+			" -text '" + annot.text);
+	er := s2r(cmd(win, w + " bbox " + a));
+	delta := er.min;
+
+	# desired rectangle for text entry box
+	er = Rect((r.min.x - Spikeradius, r.max.y), (r.min.x - Spikeradius + er.dx(), r.max.y + er.dy()));
+	# make sure it's on screen
+	if (er.max.x > d.size.x)
+		er = er.subpt((er.max.x - d.size.x, 0));
+
+	cmd(win, w + " create polygon" +
+		" " + p2s(er.min) +
+		" " + p2s((r.min.x - Spikeradius, er.min.y)) +
+		" " + p2s(r.min) +
+		" " + p2s((r.min.x + Spikeradius, er.min.y)) +
+		" " + p2s((er.max.x, er.min.y)) +
+		" " + p2s(er.max) +
+		" " + p2s((er.min.x, er.max.y)) +
+		" -fill yellow -tags {annot " + tag + "}");
+	cmd(win, w + " coords " + a + " " + p2s(er.min.sub(delta)));
+	cmd(win, w + " bind " + tag + " <Button-1> {" + w + " raise " + tag + "}");
+	cmd(win, w + " bind " + tag + " <Double-Button-1> {send evch annotclick " + string annot.fileoffset + "}");
+	cmd(win, w + " raise " + a);
+}
+
+badmodule(s: string)
+{
+	sys->fprint(stderr, "ebook: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+blankdoc: Document;
+Document.new(i: ref OEBpackage->Item, fallbacks: list of (string, string),
+		win: ref Tk->Toplevel, w: string, size: Point, evch: string,
+		indexprogress: chan of int): (ref Document, string)
+{
+	if (i.mediatype != "text/x-oeb1-document")
+		return (nil, "invalid mediatype: " + i.mediatype);
+	if (i.file == nil)
+		return (nil, "not found: " + i.missing);
+
+	(datasrc, e) := Datasource.new(i.file, fallbacks, win, size.x, evch, warningch);
+	if (datasrc == nil)
+		return (nil, e);
+
+	d := ref blankdoc;
+	d.item = i;
+	d.w = w;
+	d.p = Page.new(win, w + ".p");
+	d.datasrc = datasrc;
+	d.pagenum = -1;
+	d.size = size;
+	d.indexprogress = indexprogress;
+	d.index = Index.new(i, datasrc, size, 0, indexprogress);
+	cmd(win, "frame " + w + " -width " + string size.x + " -height " + string size.y);
+	cmd(win, "pack propagate " + w + " 0");
+	cmd(win, "pack " + w + ".p -side top -fill both");
+	d.annotations = readannotations(i.file + ".annot");
+	d.showannot = 0;
+	return (d, nil);
+}
+
+Document.fileoffset(nil: self ref Document): int
+{
+	# get nearest file offset corresponding to top of current page.
+	# XXX
+	return 0;
+}
+
+Document.gotooffset(nil: self ref Document, nil: int)
+{
+#	d.goto(d.index.pageforfileoffset(offset));
+	# XXX
+}
+
+Document.title(d: self ref Document): string
+{
+	return d.datasrc.title;
+}
+
+Document.gotolink(d: self ref Document, name: string): int
+{
+	n := d.index.getlink(name);
+	if (n != -1)
+		return d.goto(n);
+	return -1;
+}
+
+# this is much too involved for its own good.
+Document.goto(d: self ref Document, n: int): int
+{
+	win := d.datasrc.win;
+	pw := d.w + ".p";
+	if (n == d.pagenum)
+		return n;
+
+	m: ref Mark;
+	offset := -999;
+
+	# before committing ourselves, make sure that the page exists.
+	(n, (m, offset)) = d.index.get(n);
+	if (m == nil || n == d.pagenum)
+		return d.pagenum;
+
+	b: Block;
+	# remove appropriate element, in case we want to use it in the new page.
+	if (n > d.pagenum)
+		b = d.p.remove(1);
+	else
+		b = d.p.remove(0);
+
+	# destroy the old page and make a new one.
+	d.p.del();
+	d.p = Page.new(win, pw);
+	cmd(win, "pack " + pw + " -side top -fill both -expand 1");
+
+	if (n == d.pagenum + 1 && d.lastmark != nil) {
+if(debug)sys->print("page 1 forward\n");
+		# sanity check:
+		# if d.nextoffset or d.lastmark doesn't match the offset and mark we've obtained
+		# fpr this page from the index, then the index is invalid, so reindex and recurse
+		if (d.nextoffset != offset || !d.lastmark.eq(m)) {
+			notice(sys->sprint("invalid index, reindexing; (index offset: %d, actually %d; mark: %d, actually: %d)\n",
+				offset, d.nextoffset, d.lastmark.fileoffset(), m.fileoffset()));
+			d.index.abort();
+			d.index = Index.new(d.item, d.datasrc, d.size, 1, d.indexprogress);
+			d.pagenum = -1;
+			d.firstmark = d.endfirstmark = d.lastmark = d.endlastmark = nil;
+			d.nextoffset = 0;
+			return d.goto(n);
+		}
+
+		# if moving to the next page, we don't need to look up in the index;
+		# just continue on from where we currently are, transferring the
+		# last item on the current page to the first on the next.
+		d.p.append(b);
+		b.w = nil;
+		d.p.scrollto(d.nextoffset);
+		d.firstmark = d.lastmark;
+		if (d.endlastmark != nil) {
+			d.endfirstmark = d.endlastmark;
+			d.datasrc.goto(d.endfirstmark);
+		} else
+			d.endfirstmark = d.datasrc.mark();
+		(d.lastmark, nil) = fillpage(d.p, d.size, d.datasrc, d.firstmark, nil, nil);
+		d.endlastmark = nil;
+		offset = d.nextoffset;
+	} else {
+		d.p.scrollto(offset);
+		if (n == d.pagenum - 1) {
+if(debug)sys->print("page 1 back\n");
+			# moving to the previous page: re-use the first item on
+			# the current page as the last on the previous.
+			newendfirst: ref Mark;
+			if (!m.eq(d.firstmark)) {
+				d.datasrc.goto(m);
+				newendfirst = fillpageupto(d.p, d.datasrc, d.firstmark);
+			} else
+				newendfirst = d.endfirstmark;
+			d.p.append(b);
+			b.w = nil;
+			(d.endfirstmark, d.lastmark, d.endlastmark) =
+				(newendfirst, d.firstmark, d.endfirstmark);
+		} else if (n > d.pagenum && m.eq(d.lastmark)) {
+if(debug)sys->print("page forward, same start element\n");
+			# moving forward: if new page starts with same element
+			# that this page ends with, then reuse it.
+			d.p.append(b);
+			b.w = nil;
+			if (d.endlastmark != nil) {
+				d.datasrc.goto(d.endlastmark);
+				d.endfirstmark = d.endlastmark;
+			} else
+				d.endfirstmark = d.datasrc.mark();
+			
+			(d.lastmark, nil) = fillpage(d.p, d.size, d.datasrc, m, nil, nil);
+			d.endlastmark = nil;
+		} else {
+if(debug)sys->print("page goto arbitrary\n");
+			# XXX could optimise when moving several pages back,
+			# by limiting fillpage so that it stopped if it got to d.firstmark,
+			# upon which we could re-use the first widget from the current page.
+			d.datasrc.goto(m);
+			(d.lastmark, d.endfirstmark) = fillpage(d.p, d.size, d.datasrc, m, nil, nil);
+			if (d.endfirstmark == nil)
+				d.endfirstmark = d.datasrc.mark();
+			d.endlastmark = nil;
+		}
+		d.firstmark = m;
+	}
+	d.nextoffset = coverpartialline(d.p, d.datasrc, d.size);
+	if (b.w != nil)
+		cmd(win, "destroy " + b.w);
+	d.pagenum = n;
+	if (d.showannot)
+		makeannotations(d, currentannotations(d));
+if (debug)sys->print("page %d; firstmark is %d; yoffset: %d, nextoffset: %d; %d items\n", n, d.firstmark.fileoffset(), d.p.yorigin, d.nextoffset, d.p.count());
+if(debug)sys->print("now at page %d, offset: %d, nextoffset: %d\n", n, d.p.yorigin, d.nextoffset);
+	return n;
+}
+
+# fill up a page of size _size_ from d;
+# m1 marks the start of the first item (already on the page).
+# m2 marks the end of the item marked by m1.
+# return (lastmark¸ endfirstmark)
+# endfirstmark marks the end of the first item placed on the page;
+# lastmark marks the start of the last item that overlaps
+# the end of the page (or nil at eof).
+fillpage(p: ref Page, size: Point, d: ref Datasource,
+		m1, m2: ref Mark, linkch: chan of (string, string, string)): (ref Mark, ref Mark)
+{
+	endfirst: ref Mark;
+	err: string;
+	b: Block;
+	while (p.maxy() < size.y) {
+		m1 = d.mark();
+		# if we've been round once and only once,
+		# then m1 marks the end of the first element
+		if (b.w != nil && endfirst == nil)
+			endfirst = m1;
+		(b, err) = d.next(linkch);
+		if (err != nil) {
+			notice(err);
+			return (nil, endfirst);
+		}
+		if (b.w == nil)
+			return (nil, endfirst);
+		p.append(b);
+	}
+	if (endfirst == nil)
+		endfirst = m2;
+	return (m1, endfirst);
+}
+
+# fill a page up until a mark is reached (which is known to be on the page).
+# return endfirstmark.
+fillpageupto(p: ref Page, d: ref Datasource, upto: ref Mark): ref Mark
+{
+	endfirstmark: ref Mark;
+	while (!d.atmark(upto)) {
+		(b, err) := d.next(nil);
+		if (b.w == nil) {
+			notice("unexpected EOF");
+			return nil;
+		}
+		p.append(b);
+		if (endfirstmark == nil)
+			endfirstmark = d.mark();
+	}
+	return endfirstmark;
+}
+
+# cover the last partial line on the page; return the y offset
+# of the start of that line in the item containing it. (including top margin)
+coverpartialline(p: ref Page, d: ref Datasource, size: Point): int
+{
+	# conceal any trailing partially concealed line.
+	lastn := p.count() - 1;
+	b := p.getblock(lastn);
+	r := p.bbox(lastn);
+	if (r.max.y >= size.y) {
+		if (r.min.y < size.y) {
+			offset := d.linestart(p.window(lastn), size.y - r.min.y);
+			# guard against items larger than the whole page.
+			if (r.min.y + offset <= 0)
+				return size.y - r.min.y;
+			p.conceal(r.min.y + offset);
+			# if before first line, ensure that we get whole of top margin on next page.
+			if (offset == 0) {
+				p.conceal(size.y);
+				return 0;
+			}
+			return offset + b.tmargin;
+		} else {
+			p.conceal(size.y);
+			return 0;		# ensure that we get whole of top margin on next page.
+		}
+	}
+	p.conceal(size.y);
+	return r.dy() + b.tmargin;
+}
+
+Document.getannotation(d: self ref Document, fileoffset: int): ref Annotation
+{
+	annotations := d.annotations;
+	for (i := 0; i < len annotations; i++)
+		if (annotations[i].fileoffset == fileoffset)
+			return annotations[i];
+	return nil;
+}
+
+Document.showannotations(d: self ref Document, show: int)
+{
+	if (!show == !d.showannot)
+		return;
+	d.showannot = show;
+	if (show) {
+		makeannotations(d, currentannotations(d));
+	} else {
+		cmd(d.datasrc.win, d.p.w + " delete annot");
+	}
+}
+
+Document.updateannotation(d: self ref Document, annot: ref Annotation)
+{
+	if (annot.text == nil)
+		d.delannotation(annot);
+	if (d.showannot) {
+		# XXX this loses the z-order of the annotation
+		cmd(d.datasrc.win, d.p.w + " delete o" + string annot.fileoffset);
+		if (annot.text != nil)
+			makeannotations(d, array[] of {annot});
+	}
+}
+
+Document.delannotation(d: self ref Document, annot: ref Annotation)
+{
+	for (i := 0; i < len d.annotations; i++)
+		if (d.annotations[i].fileoffset == annot.fileoffset)
+			break;
+	if (i == len d.annotations) {
+		oops("trying to delete non-existent annotation");
+		return;
+	}
+	d.annotations[i:] = d.annotations[i+1:];
+	d.annotations[len d.annotations - 1] = nil;
+	d.annotations = d.annotations[0:len d.annotations - 1];
+}
+
+Document.writeannotations(d: self ref Document): string
+{
+	if ((iob := bufio->create(d.item.file + ".annot", Sys->OWRITE, 8r666)) == nil)
+		return sys->sprint("cannot create %s.annot: %r\n", d.item.file);
+	a: list of string;
+	for (i := 0; i < len d.annotations; i++)
+		a = string d.annotations[i].fileoffset :: d.annotations[i].text :: a;
+	iob.puts(str->quoted(a));
+	iob.close();
+	return nil;
+}
+
+Document.addannotation(d: self ref Document, a: ref Annotation)
+{
+	if (a.text == nil)
+		return;
+	annotations := d.annotations;
+	for (i := 0; i < len annotations; i++)
+		if (annotations[i].fileoffset >= a.fileoffset)
+			break;
+	if (i < len annotations && annotations[i].fileoffset == a.fileoffset) {
+		oops("there's already an annotation there");
+		return;
+	}
+	newa := array[len annotations + 1] of ref Annotation;
+	newa[0:] = annotations[0:i];
+	newa[i] = a;
+	newa[i + 1:] = annotations[i:];
+	d.annotations = newa;
+	d.updateannotation(a);
+}
+
+makeannotations(d: ref Document, annots: array of ref Annotation)
+{
+	n := d.p.count();
+	endy := d.p.visible();
+	for (i := j := 0; i < n && j < len annots; ) {
+		do {
+			(ok, r) := d.datasrc.rectforfileoffset(d.p.window(i), annots[j].fileoffset);
+			# XXX this assumes that y-origins at increasing offsets are monotonically increasing;
+			# this ain't necessarily the case (think tables)
+			if (!ok)
+				break;
+			r = r.addpt((0, d.p.bbox(i).min.y));
+			if (r.min.y >= 0 && r.max.y <= endy)
+				displayannotation(d, d.p.canvasr(r), annots[j]);
+			j++;
+		} while (j < len annots);
+		i++;
+	}
+}
+
+# get all annotations on current page, arranged in fileoffset order.
+currentannotations(d: ref Document): array of ref Annotation
+{
+	if (d.firstmark == nil)
+		return nil;
+	o1 := d.firstmark.fileoffset();
+	o2: int;
+	if (d.endlastmark != nil)
+		o2 = d.endlastmark.fileoffset();
+	else
+		o2 = d.datasrc.fileoffset();
+	annotations := d.annotations;
+	for (i := 0; i < len annotations; i++)
+		if (annotations[i].fileoffset >= o1)
+			break;
+	a1 := i;
+	for (; i < len annotations; i++)
+		if (annotations[i].fileoffset > o2)
+			break;
+	return annotations[a1:i];
+}
+
+readannotations(f: string): array of ref Annotation
+{
+	s: string;
+	if ((iob := bufio->open(f, Sys->OREAD)) == nil)
+		return nil;
+	while ((c := iob.getc()) >= 0)
+		s[len s] = c;
+	a := str->unquoted(s);
+	n := len a / 2;
+	annotations := array[n] of ref Annotation;
+	for (i := n - 1; i >= 0; i--) {
+		annotations[i] = ref Annotation(int hd a, hd tl a);
+		a = tl tl a;
+	}
+	return annotations;
+}
+
+Index.new(item: ref OEBpackage->Item, d:  ref Datasource, size: Point,
+		force: int, indexprogress: chan of int): ref Index
+{
+	i := ref Index;
+	i.rq = chan of (int, chan of (int, (ref Mark, int)));
+	i.linkrq = chan of (string, chan of int);
+	f := item.file + ".i";
+	i.length = 0;
+	(ok, sinfo) := sys->stat(item.file);
+	if (ok != -1)
+		i.length = int sinfo.length;
+	if (!force) {
+		indexf := bufio->open(f, Sys->OREAD);
+		if (indexf != nil) {
+			(pages, links, err) := readindex(indexf, i.length, size, d);
+			indexprogress <-= len pages;
+			if (err != nil)
+				warning(sys->sprint("cannot read index file %s: %s", f, err));
+			else {
+				spawn preindexeddealerproc(i.rq, i.linkrq, pages, links);
+				return i;
+			}
+		}
+	}
+#sys->print("reindexing %s\n", f);
+	i.d = d.copy();
+	i.size = size;
+	i.f = f;
+	i.indexed = chan of (array of (ref Mark, int), ref Links);
+	spawn indexproc(i.d, size,
+		c := chan of (ref Mark, int),
+		linkch := chan of string);
+	spawn indexdealerproc(i.f, c, i.rq, i.linkrq, chan of (int, chan of int), linkch, i.indexed, indexprogress);
+#	i.get(LASTPAGE);
+	return i;
+}
+
+Index.abort(i: self ref Index)
+{
+	i.rq <-= (0, nil);
+	# XXX kill off old indexing proc too.
+}
+
+Index.stop(i: self ref Index)
+{
+	if (i.indexed != nil) {
+		# wait for indexing to complete, so that we can write it out without interruption.
+		(pages, links) := <-i.indexed;
+		writeindex(i.d, i.length, i.size, i.f, pages, links);
+		
+	}
+	i.rq <-= (0, nil);
+}
+
+preindexeddealerproc(rq: chan of (int, chan of (int, (ref Mark, int))), linkrq: chan of (string, chan of int),
+		pages: array of (ref Mark, int), links: ref Links)
+{
+	for (;;) alt {
+	(n, reply) := <-rq =>
+		if (reply == nil)
+			exit;
+		if (n < 0)
+			n = 0;
+		else if (n >= len pages)
+			n = len pages - 1;
+		# XXX are we justified in assuming there's at least one page?
+		reply <-= (n, pages[n]);
+	(name, reply) := <-linkrq =>
+		reply <-= links.get(name);
+	}
+}
+		
+readindex(indexf: ref Iobuf, length: int, size: Point, d: ref Datasource): (array of (ref Mark, int), ref Links, string)
+{
+	# n pages
+	s := indexf.gets('\n');
+	(n, toks) := sys->tokenize(s, " ");
+	if (n != 2 || hd tl toks != "pages\n" || int hd toks < 1)
+		return (nil, nil, "invalid index file");
+	npages := int hd toks;
+
+	# size x y
+	s = indexf.gets('\n');
+	(n, toks) = sys->tokenize(s, " ");
+	if (n != 3 || hd toks != "size")
+		return (nil, nil, "invalid index file");
+	if (int hd tl toks != size.x || int hd tl tl toks != size.y)
+		return (nil, nil, "index for different sized window");
+	
+	# length n
+	s = indexf.gets('\n');
+	(n, toks) = sys->tokenize(s, " ");
+	if (n != 2 || hd toks != "length")
+		return (nil, nil, "invalid index file");
+	if (int hd tl toks != length)
+		return (nil, nil, "index for file of different length");
+	
+	pages := array[npages] of (ref Mark, int);
+	for (i := 0; i < npages; i++) {
+		ms := indexf.gets('\n');
+		os := indexf.gets('\n');
+		if (ms == nil || os == nil)
+			return (nil, nil, "premature EOF on index");
+		(m, o) := (d.str2mark(ms), int os);
+		if (m == nil)
+			return (nil, nil, "invalid mark");
+		pages[i] = (m, o);
+	}
+	(links, err) := Links.read(indexf);
+	if (links == nil)
+		return (nil, nil, "readindex: " + err);
+	return (pages, links, nil);
+}
+
+# index format:
+# %d pages
+# size %d %d
+# length %d
+# page0mark
+# page0yoffset
+# page1mark
+# ....
+# linkname pagenum
+# ...
+writeindex(d: ref Datasource, length: int, size: Point, f: string, pages: array of (ref Mark, int), links: ref Links)
+{
+	indexf := bufio->create(f, Sys->OWRITE, 8r666);
+	if (indexf == nil) {
+		notice(sys->sprint("cannot create index '%s': %r", f));
+		return;
+	}
+	indexf.puts(string len pages + " pages\n");
+	indexf.puts(sys->sprint("size %d %d\n", size.x, size.y));
+	indexf.puts(sys->sprint("length %d\n", length));
+	for (i := 0; i < len pages; i++) {
+		(m, o) := pages[i];
+		indexf.puts(d.mark2str(m));
+		indexf.putc('\n');
+		indexf.puts(string o);
+		indexf.putc('\n');
+	}
+	links.write(indexf);
+	indexf.close();
+}
+
+Index.get(i: self ref Index, n: int): (int, (ref Mark, int))
+{
+	c := chan of (int, (ref Mark, int));
+	i.rq <-= (n, c);
+	return <-c;
+}
+
+Index.getlink(i: self ref Index, name: string): int
+{
+	c := chan of int;
+	i.linkrq <-= (name, c);
+	return <-c;
+}
+
+# deal out indexes as and when they become available.
+indexdealerproc(nil: string,
+	c: chan of (ref Mark, int),
+	rq: chan of (int, chan of (int, (ref Mark, int))),
+	linkrq: chan of (string, chan of int),
+	offsetrq: chan of (int, chan of int),
+	linkch: chan of string,
+	indexed: chan of (array of (ref Mark, int), ref Links),
+	indexprogress: chan of int)
+{
+	pages := array[4] of (ref Mark, int);
+	links := Links.new();
+	rqs: list of (int, chan of (int, (ref Mark, int)));
+	linkrqs: list of (string, chan of int);
+	indexedch := chan of (array of (ref Mark, int), ref Links);
+	npages := 0;
+	finished := 0;
+	for (;;) alt {
+	(m, offset) := <-c =>
+		if (m == nil) {
+if(debug)sys->print("finished indexing; %d pages\n", npages);
+			indexedch = indexed;
+			pages = pages[0:npages];
+			finished = 1;
+			for (; linkrqs != nil; linkrqs = tl linkrqs)
+				(hd linkrqs).t1 <-= -1;
+		} else {
+			if (npages == len pages)
+				pages = (array[npages * 2] of (ref Mark, int))[0:] = pages;
+			pages[npages++] = (m, offset);
+			indexprogress <-= npages;
+		}
+		r := rqs;
+		for (rqs = nil; r != nil; r = tl r) {
+			(n, reply) := hd r;
+			if (n < npages)
+				reply <-= (n, pages[n]);
+			else if (finished)
+				reply <-= (npages - 1, pages[npages - 1]);
+			else
+				rqs = hd r :: rqs;
+		}
+	(name, reply) := <-linkrq =>
+		n := links.get(name);
+		if (n != -1)
+			reply <-= n;
+		else if (finished)
+			reply <-= -1;
+		else
+			linkrqs = (name, reply) :: linkrqs;
+	(offset, reply) := <-offsetrq =>
+		reply <-= -1;		# XXX fix it.
+#		if (finished && (npages == 0 || offset >= pages[npages - 1].fileoffset
+#		if (i := 0; i < npages; i++)
+
+	(n, reply) := <-rq =>
+		if (reply == nil)
+			exit;
+		if (n < 0)
+			n = 0;
+		if (n < npages)
+			reply <-= (n, pages[n]);
+		else if (finished)
+			reply <-= (npages - 1, pages[npages - 1]);
+		else
+			rqs = (n, reply) :: rqs;
+	name := <-linkch =>
+		links.put(name, npages - 1);
+		r := linkrqs;
+		for (linkrqs = nil; r != nil; r = tl r) {
+			(rqname, reply) := hd r;
+			if (rqname == name)
+				reply <-= npages - 1;
+			else
+				linkrqs = hd r :: linkrqs;
+		}
+	indexedch <-= (pages, links) =>
+		;
+	}
+}
+
+# accumulate links temporarily while filling a page.
+linkproc(linkch: chan of (string, string, string),
+		terminate: chan of int,
+		reply: chan of list of (string, string, string))
+{
+	links: list of (string, string, string);
+	for (;;) {
+		alt {
+		<-terminate =>
+			exit;
+		(name, w, where) := <-linkch =>
+			if (name != nil) {
+				links = (name, w, where) :: links;
+			} else {
+				reply <-= links;
+				links = nil;
+			}
+		}
+	}
+}
+
+# generate index values for each page and send them on
+# to indexdealerproc to be served up on demand.
+indexproc(d: ref Datasource, size: Point, c: chan of (ref Mark, int),
+		linkpagech: chan of string)
+{
+	spawn linkproc(linkch := chan of (string, string, string),
+			terminate := chan of int,
+			reply := chan of list of (string, string, string));
+	win := d.win;
+	p := Page.new(win, ".ip");
+
+	mark := d.mark();
+	c <-= (mark, 0);
+
+	links: list of (string, string, string);	# (linkname, widgetname, tag)
+	for (;;) {
+startoffset := mark.fileoffset();
+		(mark, nil) = fillpage(p, size, d, mark, nil, linkch);
+
+		offset := coverpartialline(p, d, size);
+if (debug)sys->print("page index %d items starting at %d, nextyoffset: %d\n", p.count(), startoffset, offset);
+		linkch <-= (nil, nil, nil);
+		for (l := <-reply; l != nil; l = tl l)
+			links = hd l :: links;
+		links = sendlinks(p, size, d, links, linkpagech);
+		if (mark == nil)
+			break;
+		c <-= (mark, offset);
+		b := p.remove(1);
+		p.del();
+		p = Page.new(win, ".ip");
+		p.append(b);
+		p.scrollto(offset);
+	}
+	p.del();
+	terminate <-= 1;
+	c <-= (nil, 0);
+}
+
+# send down ch the name of all the links that reside on the current page.
+# return any links that were not on the current page.
+sendlinks(p: ref Page, nil: Point, d: ref Datasource,
+	links: list of (string, string, string), ch: chan of string): list of (string, string, string)
+{
+	nlinks: list of (string, string, string);
+	vy := p.visible();
+	for (; links != nil; links = tl links) {
+		(name, w, where) := hd links;
+		r := p.bboxw(w);
+		y := r.min.y + d.linkoffset(w, where);
+		if (y < vy)
+			ch <-= name;
+		else
+			nlinks = hd links :: nlinks;
+	}
+	return nlinks;
+}
+
+Links: adt {
+	a: array of list of (string, int);
+	new: fn(): ref Links;
+	read: fn(iob: ref Iobuf): (ref Links, string);
+	get:	fn(l: self ref Links, name: string): int;
+	put:	fn(l: self ref Links, name: string, pagenum: int);
+	write: fn(l: self ref Links, iob: ref Iobuf);
+};
+
+Links.new(): ref Links
+{
+	return ref Links(array[31] of list of (string, int));
+}
+
+Links.write(l: self ref Links, iob: ref Iobuf)
+{
+	for (i := 0; i < len l.a; i++) {
+		for (ll := l.a[i]; ll != nil; ll = tl ll) {
+			(name, page) := hd ll;
+			iob.puts(sys->sprint("%s %d\n", name, page));
+		}
+	}
+}
+
+Links.read(iob: ref Iobuf): (ref Links, string)
+{
+	l := Links.new();
+	while ((s := iob.gets('\n')) != nil) {
+		(n, toks) := sys->tokenize(s, " ");
+		if (n != 2)
+			return (nil, "expected 2 words, got " + string n);
+		l.put(hd toks, int hd tl toks);
+	}
+	return (l, nil);
+}
+
+Links.get(l: self ref Links, name: string): int
+{
+	for (ll := l.a[hashfn(name, len l.a)]; ll != nil; ll = tl ll)
+		if ((hd ll).t0 == name)
+			return (hd ll).t1;
+	return -1;
+}
+
+Links.put(l: self ref Links, name: string, pageno: int)
+{
+	v := hashfn(name, len l.a);
+	l.a[v] = (name, pageno) :: l.a[v];
+}
+
+blankpage: Page;
+Page.new(win: ref Tk->Toplevel, w: string): ref Page
+{
+	cmd(win, "canvas " + w + " -bg white");
+	col := cmd(win, w + " cget -bg");
+	cmd(win, w + " create rectangle -1 -1 -1 -1 -fill " + col + " -outline " + col + " -tags conceal");
+	p := ref blankpage;
+	p.win = win;
+	p.w = w;
+	setscrollregion(p);
+	return p;
+}
+
+Page.del(p: self ref Page)
+{
+	n := p.count();
+	for (i := 0; i < n; i++)
+		cmd(p.win, "destroy " + p.window(i));
+	cmd(p.win, "destroy " + p.w);
+}
+
+# convert a rectangle as returned by Page.window()
+# to a rectangle in canvas coordinates
+Page.canvasr(p: self ref Page, r: Rect): Rect
+{
+	return r.addpt((0, p.yorigin));
+}
+
+Pagewidth: con 5000;		# max page width
+
+# create an area on the page, from y downwards.
+Page.conceal(p: self ref Page, y: int)
+{
+	cmd(p.win, p.w + " coords conceal 0 " + string (y + p.yorigin) +
+			" " + string Pagewidth +
+			" " + string p.height);
+	cmd(p.win, p.w + " raise conceal");
+}
+
+# return vertical space in the page that's not concealed.
+Page.visible(p: self ref Page): int
+{
+	r := s2r(cmd(p.win, p.w + " coords conceal"));
+	return r.min.y - p.yorigin;
+}
+	
+Page.window(p: self ref Page, n: int): string
+{
+	return cmd(p.win, p.w + " itemcget n" + string (n + p.min) + " -window");
+}
+
+Page.append(p: self ref Page, b: Block)
+{
+	h := int cmd(p.win, b.w + " cget -height") + 2 * int cmd(p.win, b.w + " cget -bd");
+
+	n := p.max++;
+	y := p.height;
+
+	gap := p.bmargin;
+	if (b.tmargin > gap)
+		gap = b.tmargin;
+
+	cmd(p.win, p.w + " create window 0 " + string (y + gap) + " -window " + b.w +
+			" -tags {elem" +
+				" n" + string n +
+				" t" + string b.tmargin +
+				" b" + string  b.bmargin +
+				"} -anchor nw");
+
+	p.height += h + gap;
+	p.bmargin = b.bmargin;
+	setscrollregion(p);
+}
+
+Page.remove(p: self ref Page, atend: int): Block
+{
+	if (p.min == p.max)
+		return Block(nil, 0, 0);
+	n: int;
+	if (atend) 
+		n = --p.max;
+	else
+		n = p.min++;
+
+	b := getblock(p, n);
+	h := int cmd(p.win, b.w + " cget -height") + 2 * int cmd(p.win, b.w + " cget -bd");
+
+	if (p.min == p.max) {
+		p.bmargin = 0;
+		h += b.tmargin;
+	} else if (atend) {
+		c := getblock(p, p.max - 1);
+		if (c.bmargin > b.tmargin)
+			h += c.bmargin;
+		else
+			h += b.tmargin;
+		p.bmargin = c.bmargin;
+	} else {
+		c := getblock(p, p.min);
+		if (c.tmargin > b.bmargin)
+			h += c.tmargin;
+		else
+			h += b.bmargin;
+		h += b.tmargin;
+	}
+
+	p.height -= h;
+	cmd(p.win, p.w + " delete n" + string n);
+	if (!atend)
+		cmd(p.win, p.w + " move elem 0 -" + string h);
+	setscrollregion(p);
+
+	return b;
+}
+
+getblock(p: ref Page, n: int): Block
+{
+	tag := "n" + string n;
+	b := Block(cmd(p.win, p.w + " itemcget " + tag + " -window"), 0, 0);
+	(nil, toks) := sys->tokenize(cmd(p.win, p.w + " gettags " + tag), " ");
+	for (; toks != nil; toks = tl toks) {
+		c := (hd toks)[0];
+		if (c == 't')
+			b.tmargin = int (hd toks)[1:];
+		else if (c == 'b')
+			b.bmargin = int (hd toks)[1:];
+	}
+	return b;
+}
+
+# scroll the page so y is at the top left visible in the canvas widget.
+Page.scrollto(p: self ref Page, y: int)
+{
+	p.yorigin = y;
+	setscrollregion(p);
+	cmd(p.win, p.w + " yview moveto 0");
+}
+
+# return max y coord of bottom of last item, where y=0
+# is at top visible part of canvas.
+Page.maxy(p: self ref Page): int
+{
+	return p.height - p.yorigin;
+}
+
+Page.count(p: self ref Page): int
+{
+	return p.max - p.min;
+}
+
+# XXX what should bbox do about margins? ignoring seems ok for the moment.
+Page.bbox(p: self ref Page, n: int): Rect
+{
+	if (p.count() == 0)
+		return ((0, 0), (0, 0));
+	tag := "n" + string (n + p.min);
+	return s2r(cmd(p.win, p.w + " bbox " + tag)).subpt((0, p.yorigin));
+}
+
+Page.bboxw(p: self ref Page, w: string): Rect
+{
+	# XXX inefficient algorithm. do better later.
+	n := p.count();
+	for (i := 0; i < n; i++)
+		if (p.window(i) == w)
+			return p.bbox(i);
+	sys->fprint(sys->fildes(2), "ebook: bboxw requested for invalid window %s\n", w);
+	return ((0, 0), (0, 0));
+}
+
+Page.getblock(p: self ref Page, n: int): Block
+{
+	return getblock(p, n + p.min);
+}
+
+printpage(p: ref Page)
+{
+	n := p.count();
+	for (i := 0; i < n; i++) {
+		r := p.bbox(i);
+		dx := r.max.sub(r.min);
+		sys->print("	%d: %s %d %d +%d +%d\n", i, p.window(i), 
+			r.min.x, r.min.y, dx.x, dx.y);
+	}
+	sys->print("	conceal: %s\n", cmd(p.win, p.w + " bbox conceal"));
+}
+
+setscrollregion(p: ref Page)
+{
+	cmd(p.win, p.w + " configure -scrollregion {0 " + string p.yorigin + " " + string Pagewidth + " " + string p.height + "}");
+}
+
+notice(s: string)
+{
+	sys->print("notice: %s\n", s);
+}
+
+warning(s: string)
+{
+	notice("warning: " + s);
+}
+
+oops(s: string)
+{
+	sys->print("oops: %s\n", s);
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+#	sys->print("%ux	%s\n", win, s);
+	r := tk->cmd(win, s);
+#	sys->print("	-> %s\n", r);
+	if (len r > 0 && r[0] == '!') {
+		sys->fprint(stderr, "ebook: error executing '%s': %s\n", s, r);
+		raise "tk error";
+	}
+	return r;
+}
+
+s2r(s: string): Rect
+{
+	(n, toks) := sys->tokenize(s, " ");
+	if (n != 4) {
+		sys->print("'%s' is not a rectangle!\n", s);
+		raise "bad conversion";
+	}
+	r: Rect;
+	(r.min.x, toks) = (int hd toks, tl toks);
+	(r.min.y, toks) = (int hd toks, tl toks);
+	(r.max.x, toks) = (int hd toks, tl toks);
+	(r.max.y, toks) = (int hd toks, tl toks);
+	return r;
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+r2s(r: Rect): string
+{
+	return string r.min.x + " " + string r.min.y + " " +
+			string r.max.x + " " + string r.max.y;
+}
+	
+trim(s: string): string
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] != ' ' && s[i] != '\t' && s[i] != '\n')
+			break;
+	return s[0:i+1];
+}
+
+splitword(s: string): (string, string)
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == ' ')
+			return (s[0:i], s[i + 1:]);
+	return (s, nil);
+}
+
+# compress ../ references and do other cleanups
+cleanname(name: string): string
+{
+	# compress multiple slashes
+	n := len name;
+	for(i:=0; i<n-1; i++)
+		if(name[i]=='/' && name[i+1]=='/'){
+			name = name[0:i]+name[i+1:];
+			--i;
+			n--;
+		}
+	#  eliminate ./
+	for(i=0; i<n-1; i++)
+		if(name[i]=='.' && name[i+1]=='/' && (i==0 || name[i-1]=='/')){
+			name = name[0:i]+name[i+2:];
+			--i;
+			n -= 2;
+		}
+	found: int;
+	do{
+		# compress xx/..
+		found = 0;
+		for(i=1; i<=n-3; i++)
+			if(name[i:i+3] == "/.."){
+				if(i==n-3 || name[i+3]=='/'){
+					found = 1;
+					break;
+				}
+			}
+		if(found)
+			for(j:=i-1; j>=0; --j)
+				if(j==0 || name[j-1]=='/'){
+					i += 3;		# character beyond ..
+					if(i<n && name[i]=='/')
+						++i;
+					name = name[0:j]+name[i:];
+					n -= (i-j);
+					break;
+				}
+	} while(found);
+	# eliminate trailing .
+	if(n>=2 && name[n-2]=='/' && name[n-1]=='.')
+		--n;
+	if(n == 0)
+		return ".";
+	if(n != len name)
+		name = name[0:n];
+	return name;
+}
+
+
+hashfn(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
--- /dev/null
+++ b/appl/ebook/mimeimage.b
@@ -1,0 +1,96 @@
+implement Mimeimage;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Image: import draw;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "imagefile.m";
+	imageremap: Imageremap;
+include "mimeimage.m";
+
+display: ref Draw->Display;
+
+imagemodules := array[] of {
+	("gif", RImagefile->READGIFPATH),
+	("jpeg", RImagefile->READJPGPATH),
+	("jpg", RImagefile->READJPGPATH),
+	("xbm", RImagefile->READXBMPATH),		# not actually a mime type.
+	("pic", RImagefile->READPICPATH),
+	("png", RImagefile->READPNGPATH),
+};
+
+badmodule(p: string)
+{
+	sys->fprint(sys->fildes(2), "mimeimage: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(displ: ref Draw->Display)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmodule(Draw->PATH);
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		badmodule(Bufio->PATH);
+	imageremap = load Imageremap Imageremap->PATH;
+	if (imageremap == nil)
+		badmodule(Imageremap->PATH);
+	str = load String String->PATH;
+	if (str == nil)
+		badmodule(String->PATH);
+
+	display = displ;
+}
+
+imagesize(mediatype, file: string): (Draw->Point, string)
+{
+	(img, e) := image(mediatype, file);
+	if (img == nil)
+		return ((0, 0), e);
+	return ((img.r.dx(), img.r.dy()), nil);
+}
+
+image(mediatype, file: string): (ref Draw->Image, string)
+{
+	if (mediatype == nil) {
+		for (i := len file - 1; i >= 0; i--)
+			if (file[i] == '.')
+				break;
+		if (i >= 0)
+			mediatype = str->tolower(file[i + 1:]);
+	}
+	# special case for native image type
+	if (mediatype == "bit") {
+		img := draw->display.open(file);
+		err: string;
+		if (img == nil)
+			err = sys->sprint("%r");
+		return (img, err);
+	}
+	iob := bufio->open(file, Sys->OREAD);
+	if (iob == nil)
+		return (nil, sys->sprint("%r"));
+	for (i := 0; i < len imagemodules; i++)
+		if (imagemodules[i].t0 == mediatype)
+			break;
+	if (i == len imagemodules)
+		return (nil, "unrecognised image type");
+
+	# XXX should probably cache the image modules, but do we really want to
+	# pay the price?
+	mod := load RImagefile imagemodules[i].t1;
+	if (mod == nil)
+		return (nil, sys->sprint("cannot load %s: %r", imagemodules[i].t1));
+	mod->init(bufio);
+	(raw, e) := mod->read(iob);
+	if (raw == nil)
+		return (nil, e);
+	return imageremap->remap(raw, display, 1);
+}
--- /dev/null
+++ b/appl/ebook/mimeimage.m
@@ -1,0 +1,7 @@
+Mimeimage: module {
+	PATH: con "/dis/ebook/mimeimage.dis";
+	init: fn(displ: ref Draw->Display);
+	image: fn(mediatype, file: string): (ref Draw->Image, string);
+	imagesize: fn(mediatype, file: string): (Draw->Point, string);
+};
+
--- /dev/null
+++ b/appl/ebook/mkfile
@@ -1,0 +1,36 @@
+<../../mkconfig
+
+TARG=\
+	cssfont.dis\
+	cssparser.dis\
+	ebook.dis\
+	mimeimage.dis\
+	oebpackage.dis\
+	reader.dis\
+	strmap.dis\
+	stylesheet.dis\
+	table.dis\
+	units.dis\
+
+MODULES=\
+	cssfont.m\
+	cssparser.m\
+	mimeimage.m\
+	oebpackage.m\
+	reader.m\
+	strcache.m\
+	strmap.m\
+	stylesheet.m\
+	units.m\
+	table.m\
+
+SYSMODULES=\
+	css.m\
+	draw.m\
+	sys.m\
+	tk.m\
+	xml.m\
+
+DISBIN=$ROOT/dis/ebook
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/ebook/oebpackage.b
@@ -1,0 +1,276 @@
+implement OEBpackage;
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+
+include "url.m";
+	url: Url;
+	ParsedUrl: import url;
+
+include "xml.m";
+	xml: Xml;
+	Attributes, Locator, Parser: import xml;
+
+include "oebpackage.m";
+
+OEBpkgtype: con "http://openebook.org/dtds/oeb-1.0.1/oebpkg101.dtd";
+OEBdoctype: con "http://openebook.org/dtds/oeb-1.0.1/oebdoc101.dtd";
+
+OEBpkg, OEBdoc: con iota;
+Laxchecking: con 1;
+
+init(xmlm: Xml)
+{
+	sys = load Sys Sys->PATH;
+	url = load Url Url->PATH;
+	if(url != nil)
+		url->init();
+	xml = xmlm;
+}
+
+open(f: string, warnings: chan of (Xml->Locator, string)): (ref Package, string)
+{
+	(x, e) := xml->open(f, warnings, nil);
+	if(x == nil)
+		return (nil, e);
+	xi := x.next();
+	if(xi == nil)
+		return (nil, "not valid XML");
+	pick d := xi {
+	Process =>
+		if(d.target != "xml")
+			return (nil, "not an XML file");
+	* =>
+		return (nil, "unexpected file structure");
+	}
+	# XXX i don't understand this 3-times loop...
+	# seems to me that something like the following (correct) document
+	# will fail:
+	# <?xml><!DOCTYPE ...><package> ....</package>
+	# i.e. no space between the doctype declaration and the
+	# start of the package tag.
+	for(i := 0; i < 3; i++){
+		xi = x.next();
+		if(xi == nil)
+			return (nil, "not OEB package");
+		pick d := xi {
+		Text =>
+			;	# usual XML extraneous punctuation cruft
+		Doctype =>
+			if(!d.public || len d.params < 2)
+				return (nil, "not an OEB document or package");
+			case doctype(hd tl d.params, Laxchecking) {
+			OEBpkg =>
+				break;
+			OEBdoc =>
+				# it's a document; make it into a simple package
+				p := ref Package;
+				p.file = f;
+				p.uniqueid = d.name;
+				p.manifest = p.spine = ref Item("doc", f, "text/x-oeb1-document", nil, f, nil) :: nil;
+				return (p, nil);
+			* =>
+				return (nil, "unexpected DOCTYPE for OEB package: " + hd tl d.params  );
+			}
+		* =>
+			return (nil, "not OEB package (no DOCTYPE)");
+		}
+	}
+	p := ref Package;
+	p.file = f;
+
+	# package[@unique-identifier[IDREF], Metadata, Manifest, Spine, Tours?, Guide?]
+	if((tag := next(x, "package")) == nil)
+		return (nil, "can't find OEB package");
+	p.uniqueid = tag.attrs.get("unique-identifier");
+	spine: list of string;
+	fallbacks: list of (ref Item, string);
+	x.down();
+	while((tag = next(x, nil)) != nil){
+		x.down();
+		case tag.name {
+		"metadata" =>
+			while((tag = next(x, nil)) != nil)
+				if(tag.name == "dc-metadata"){
+					x.down();
+					while((tag = next(x, nil)) != nil && (s := text(x)) != nil)
+						p.meta = (tag.name, tag.attrs, s) :: p.meta;
+					x.up();
+				}
+		"manifest" =>
+			while((tag = next(x, "item")) != nil){
+				a := tag.attrs;
+				p.manifest = ref Item(a.get("id"), a.get("href"), a.get("media-type"), nil, nil, nil) :: p.manifest;
+				fallback := a.get("fallback");
+				if (fallback != nil)
+					fallbacks = (hd p.manifest, fallback) :: fallbacks;
+			}
+		"spine" =>
+			while((tag = next(x, "itemref")) != nil)
+				if((id := tag.attrs.get("idref")) != nil)
+					spine = id :: spine;
+		"guide" =>
+			while((tag = next(x, "reference")) != nil){
+				a := tag.attrs;
+				p.guide = ref Reference(a.get("type"), a.get("title"), a.get("href")) :: p.guide;
+			}
+		"tours" =>
+			;	# ignore for now
+		}
+		x.up();
+	}
+	x.up();
+
+	# deal with fallbacks, and make sure they're not circular.
+	
+	for (; fallbacks != nil; fallbacks = tl fallbacks) {
+		(item, fallbackid) := hd fallbacks;
+		fallback := lookitem(p.manifest, fallbackid);
+		for (fi := fallback; fi != nil; fi = fi.fallback)
+			if (fi == item)
+				break;
+		if (fi == nil)
+			item.fallback = fallback;
+		else
+			sys->print("warning: circular fallback reference\n");
+	}
+
+	# we'll assume it doesn't require a hash table
+	for(; spine != nil; spine = tl spine)
+		if((item := lookitem(p.manifest, hd spine)) != nil)
+			p.spine = item :: p.spine;
+		else
+			p.spine = ref Item(hd spine, nil, nil, nil, nil, "item in OEB spine but not listed in manifest") :: p.spine;
+	guide := p.guide;
+	for(p.guide = nil; guide != nil; guide = tl guide)
+		p.guide = hd guide :: p.guide;
+	return (p, nil);
+}
+
+doctype(s: string, lax: int): int
+{
+	case s {
+	OEBpkgtype =>
+		return OEBpkg;
+	OEBdoctype =>
+		return OEBdoc;
+	* =>
+		if (!lax)
+			return -1;
+		if (contains(s, "oebpkg1"))
+			return OEBpkg;
+		if (contains(s, "oebdoc1"));
+			return OEBdoc;
+		return -1;
+	}
+}
+
+# does s1 contain s2
+contains(s1, s2: string): int
+{
+	if (len s2 > len s1)
+		return 0;
+	n := len s1 - len s2 + 1;
+search:
+	for (i := 0; i < n ; i++) {
+		for (j := 0; j < len s2; j++)
+			if (s1[i + j] != s2[j])
+				continue search;
+		return 1;
+	}
+	return 0;
+}
+	
+
+lookitem(items: list of ref Item, id: string): ref Item
+{
+	for(; items != nil; items = tl items){
+		item := hd items;
+		if(item.id == id)
+			return item;
+	}
+	return nil;
+}
+
+next(x: ref Parser, s: string): ref Xml->Item.Tag
+{
+	while ((t0 := x.next()) != nil) {
+		pick t1 := t0 {
+		Error =>
+			sys->print("oebpackage: error: %s:%d: %s\n", t1.loc.systemid, t1.loc.line, t1.msg);
+		Tag =>
+			if (s == nil || s == t1.name)
+				return t1;
+		}
+	}
+	return nil;
+}
+
+text(x: ref Parser): string
+{
+	s: string;
+	x.down();
+loop:
+	while ((t0 := x.next()) != nil) {
+		pick t1 := t0 {
+		Error =>
+			sys->print("oebpackage: error: %s:%d: %s\n", t1.loc.systemid, t1.loc.line, t1.msg);
+		Text =>
+			s = t1.ch;
+			break loop;
+		}
+	}
+	x.up();
+	return s;
+}	
+
+Package.getmeta(p: self ref Package, n: string): list of (Xml->Attributes, string)
+{
+	r: list of (Xml->Attributes, string);
+	for(meta := p.meta; meta != nil; meta = tl meta){
+		(name, a, value) := hd meta;
+		if(name == n)
+			r = (a, value) :: r;
+	}
+	# r is in file order because p.meta is reversed
+	return r;
+}
+
+Package.locate(p: self ref Package): int
+{
+	dir := "./";
+	for(n := len p.file; --n >= 0;)
+		if(p.file[n] == '/'){
+			dir = p.file[0:n+1];
+			break;
+		}
+	nmissing := 0;
+	for(items := p.manifest; items != nil; items = tl items){
+		item := hd items;
+		err := "";
+		if(item.href != nil){
+			u := url->makeurl(item.href);
+			if(u.scheme != Url->FILE && u.scheme != Url->NOSCHEME)
+				err = sys->sprint("URL scheme %s not yet supported", url->schemes[u.scheme]);
+			else if(u.host != "localhost" && u.host != nil)
+				err = "non-local URLs not supported";
+			else{
+				path := u.path;
+				if(u.pstart != "/")
+					path = dir+path;	# TO DO: security
+				(ok, d) := sys->stat(path);
+				if(ok >= 0)
+					item.file = path;
+				else
+					err = sys->sprint("%r");
+			}
+		}else
+			err = "no location specified (missing HREF)";
+		if(err != nil)
+			nmissing++;
+		item.missing = err;
+	}
+	return nmissing;
+}
--- /dev/null
+++ b/appl/ebook/oebpackage.m
@@ -1,0 +1,34 @@
+OEBpackage: module {
+	PATH:	con "/dis/ebook/oebpackage.dis";
+
+	Package: adt {
+		file:	string;
+		uniqueid:	string;
+		meta:	list of (string, Xml->Attributes, string);	# dublin-core metadata
+		manifest:	list of ref Item;			# all items in the ebook
+		spine:	list of ref Item;			# reading order
+		guide:	list of ref Reference;
+
+		getmeta:	fn(p: self ref Package, n: string): list of (Xml->Attributes, string);
+		locate:	fn(p: self ref Package): int;
+	};
+
+	Item: adt {
+		id:	string;
+		# can we assume that the href doesn't end in #idref?
+		href: string;
+		mediatype:	string;
+		fallback: cyclic ref Item;
+		file:	string;	# local file name
+		missing:	string;	# if it's missing, why
+	};
+
+	Reference: adt {
+		sort:	string;		# XXX what's this?
+		title:	string;
+		href:	string;
+	};
+
+	init:	fn(xml: Xml);
+	open:	fn(f: string, warnings: chan of (Xml->Locator,string)): (ref Package, string);
+};
--- /dev/null
+++ b/appl/ebook/reader.b
@@ -1,0 +1,1797 @@
+implement Reader;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Image, Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "wmlib.m";
+	wmlib: Wmlib;
+include "string.m";
+	str:	String;
+include "imagefile.m";
+include "xml.m";
+	xml: Xml;
+	Attributes, Locator, Parser, Item: import xml;
+include "strmap.m";
+	strmap: Strmap;
+	Map: import strmap;
+include "hash.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "mimeimage.m";
+	mimeimage: Mimeimage;
+include "cssparser.m";
+	cssparser: CSSparser;
+include "cssfont.m";
+	cssfont: CSSfont;
+include "stylesheet.m";
+	stylesheet: Stylesheet;
+	Style, Sheet: import stylesheet;
+include "table.m";
+	table: Table;
+include "url.m";
+	url: Url;
+	ParsedUrl: import url;
+include "units.m";
+	units: Units;
+include "reader.m";
+
+# TO DO:
+# - image links.
+# - client-side image maps
+# - subscript, superscript (which css attributes do they correspond to?)
+# - limit the size of the image cache.
+
+stderr: ref Sys->FD;
+maxblockid := 0;		# assume that increments of this are atomic
+
+OEBpkgtype: con "http://openebook.org/dtds/oeb-1.0.1/oebpkg101.dtd";
+OEBdoctype: con "http://openebook.org/dtds/oeb-1.0.1/oebdoc101.dtd";
+OEBpkg, OEBdoc: con iota;
+Laxchecking: con 1;
+
+RULE: con 'r';
+TABLE: con 'b';
+TEXT: con 't';
+IMAGE: con 'i';
+MARK: con 'm';
+VSPACE: con 'v';
+
+INDENT: con 20;
+
+Sbackground_color,
+Sborder,			# none, solid, dotted, dashed, double, groove, ridge, inset, outset, [thin, medium, thick, <abs size>]
+#Sclear,			# none, left, right, both
+Scolor,
+#Sdisplay,			# block, inline, none, oeb-page-head, oeb-page-foot
+#Sfloat,			# left, right, none
+Sfont_family,		# serif, sans-serif, monospace
+Sfont_size,		# xx-small...xx-large, smaller, larger, <abs size>
+Sfont_style,		# normal, italic
+Sfont_weight,		# normal, bold
+Sheight,
+Sline_height,		# normal, <number>, <length>
+Slist_style_type,	# decimal, lower-roman, upper-roman, lower-alpha, upper-alpha, none
+Smargin_bottom,
+Smargin_top,
+Smargin_left,
+Smargin_right,
+# Soeb_column_number,	# auto, 1
+# Spage_break_before,	# auto, always, left, right
+# Spage_break_inside,	# auto, avoid
+Stext_align,		# left, right, center, justify
+Stext_decoration,		# none, underline, line-through
+Stext_indent,
+Svertical_align,		# top, middle, bottom
+Swidth,
+Snumstyles: con iota;
+
+stylenames := array[] of {
+	Sbackground_color => "background-color",
+	Sborder => "border",
+#	Sclear => "clear",
+	Scolor => "color",
+#	Sdisplay => "display",
+#	Sfloat => "float",
+	Sfont_family => "font-family",
+	Sfont_size => "font-size",
+	Sfont_style => "font-style",
+	Sfont_weight => "font-weight",
+	Sheight => "height",
+	Sline_height => "line-height",
+	Slist_style_type => "list-style-type",
+	Smargin_bottom => "margin-bottom",
+	Smargin_left => "margin-left",
+	Smargin_right => "margin-right",
+	Smargin_top => "margin-top",
+#	Soeb_column_number => "oeb-column-number",
+#	Spage_break_before => "page-break-before",
+#	Spage_break_inside => "page-break-inside",
+	Stext_align => "text-align",
+	Stext_decoration => "text-decoration",
+	Stext_indent => "text-indent",
+	Svertical_align => "vertical-align",
+	Swidth => "width",
+};
+
+# constants for %flow elements
+Ea, Eb, Ebig, Eblockquote, Ebr, Ecenter, Ecite, Ecode, Edfn,
+Ediv, Edl, Eem, Efont, Eh1, Eh2, Eh3, Eh4, Eh5, Eh6, Ehr, Ei, Eimg,
+Einput, Ekbd, Elabel, Emap, Eobject, Eol, Ep, Epre, Eq, Es, Esamp,
+Escript, Eselect, Esmall, Espan, Estrike, Estrong, Esub, Esup, Etable,
+Ett, Eu, Eul, Evar, Ent, Enumflowtags: con iota;
+
+flownames := array[] of {
+	Ea => "a",
+	Eb => "b",
+	Ebig => "big",
+	Eblockquote => "blockquote",
+	Ebr => "br",
+	Ecenter => "center",
+	Ecite => "cite",
+	Ecode => "code",
+	Edfn => "dfn",
+	Ediv => "div",
+	Edl => "dl",
+	Eem => "em",
+	Efont => "font",
+	Eh1 => "h1",
+	Eh2 => "h2",
+	Eh3 => "h3",
+	Eh4 => "h4",
+	Eh5 => "h5",
+	Eh6 => "h6",
+	Ehr => "hr",
+	Ei => "i",
+	Eimg => "img",
+	Einput => "input",
+	Ekbd => "kbd",
+	Elabel => "label",
+	Emap => "map",
+	Eobject => "object",
+	Eol => "ol",
+	Ep => "p",
+	Epre => "pre",
+	Eq => "q",
+	Es => "s",
+	Esamp => "samp",
+	Escript => "script",
+	Eselect => "select",
+	Esmall => "small",
+	Espan => "span",
+	Estrike => "strike",
+	Estrong => "strong",
+	Esub => "sub",
+	Esup => "sup",
+	Etable => "table",
+	Ett => "tt",
+	Eu => "u",
+	Eul => "ul",
+	Evar => "var",
+};
+tagmap: ref Map;
+
+isblocklevel := array[Enumflowtags] of {
+	* => byte 0,
+	Eul => byte 1,
+	Eol => byte 1,
+	Eh1 => byte 1,
+	Eh2 => byte 1,
+	Eh3 => byte 1,
+	Eh4 => byte 1,
+	Eh5 => byte 1,
+	Eh6 => byte 1,
+	Epre => byte 1,
+	Edl => byte 1,
+	Ediv => byte 1,
+	Ecenter => byte 1,
+	Eblockquote => byte 1,
+	Ehr => byte 1,
+	Etable => byte 1,
+	Ep => byte 1,
+};
+
+inherited := array[Snumstyles] of {
+	Scolor => byte 1,
+	Sfont_family => byte 1,
+	Sfont_size => byte 1,
+	Sfont_style => byte 1,
+	Sfont_weight => byte 1,
+	Sline_height => byte 1,
+	Slist_style_type => byte 1,
+#	Soeb_column_number => byte 1,
+#	Spage_break_before => byte 1,
+#	Spage_break_inside => byte 1,
+	Stext_align => byte 1,
+	Stext_decoration => byte 1,
+	Stext_indent => byte 1,
+};
+
+defaults := array[] of {
+	Sborder => "solid",
+	Scolor => "black",
+	Sfont_family => "sans-serif",
+	Sfont_size => "medium",
+	Sfont_weight => "normal",
+	Sfont_style => "normal",
+	Sheight => "normal",
+	Sline_height => "normal",
+	Slist_style_type => "none",
+#	Soeb_column_number => "auto",	# ?
+#	Spage_break_before => "auto",	# ?
+	Stext_decoration => "none",
+};
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "reader: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(displ: ref Draw->Display)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	tk = load Tk Tk->PATH;
+	draw = load Draw Draw->PATH;
+
+	wmlib = load Wmlib Wmlib->PATH;
+	if (wmlib == nil)
+		badmodule(Wmlib->PATH);
+
+	str = load String String->PATH;
+	if (str == nil)
+		badmodule(String->PATH);
+
+	xml = load Xml Xml->PATH;
+	if (xml == nil)
+		badmodule(Xml->PATH);
+	xml->init();
+
+	mimeimage = load Mimeimage Mimeimage->PATH;
+	if (mimeimage == nil)
+		badmodule(Mimeimage->PATH);
+	mimeimage->init(displ);
+
+	url = load Url Url->PATH;
+	if (url == nil)
+		badmodule(Url->PATH);
+	url->init();
+
+	cssparser = load CSSparser CSSparser->PATH;
+	if (cssparser == nil)
+		badmodule(CSSparser->PATH);
+	cssparser->init();
+
+	cssfont = load CSSfont CSSfont->PATH;
+	if (cssfont == nil)
+		badmodule(CSSfont->PATH);
+	cssfont->init(displ);
+
+	stylesheet = load Stylesheet Stylesheet->PATH;
+	if (stylesheet == nil)
+		badmodule(Stylesheet->PATH);
+	stylesheet->init(stylenames);
+
+	table = load Table Table->PATH;
+	if (table == nil)
+		badmodule(Table->PATH);
+	table->init();
+
+	units = load Units Units->PATH;
+	if (units == nil)
+		badmodule(Units->PATH);
+	units->init();
+
+	strmap = load Strmap Strmap->PATH;
+	if (strmap == nil)
+		badmodule(Strmap->PATH);
+
+	tagmap = Map.new(flownames);
+}
+
+blankdatasource: Datasource;
+
+Datasource.new(f: string, fallbacks: list of (string, string), win: ref Tk->Toplevel, width: int, evch: string,
+		warningch: chan of (Locator, string)): (ref Datasource, string)
+{
+	d := ref blankdatasource;
+	(x, e) := xml->open(f, warningch, "pre");
+	if (x == nil)
+		return (nil, e);
+	d.x = x;
+	d.warningch = warningch;
+	d.fallbacks = fallbacks;
+	d.win = win;
+	d.width = width;
+	d.filename = f;
+	d.evch = evch;
+	d.stylesheet = Sheet.new();
+	style := d.stylesheet.newstyle();
+	style.attrs[0:] = defaults;
+	d.styles = style :: nil;
+	d.fontinfo = ref Fontinfo(nil, 0, 0) :: nil;
+	rules := cssparser->parse(readfile("/lib/ebook/default.css"));
+	d.stylesheet.addrules(rules, Stylesheet->DEFAULT);
+	{
+		if ((e = startdocument(d)) != nil)
+			return (nil, e);
+		d.startmark = d.mark();
+		return (d, nil);
+	}
+	exception{
+		"error" =>
+			return (nil, d.error);
+	}
+}
+
+# make an independent copy of a datasource and rewind it to the beginning.
+Datasource.copy(d: self ref Datasource): ref Datasource
+{
+	newd := ref *d;
+	(x, e) := xml->open(d.filename, d.warningch, "pre");
+	if (x == nil)
+		error(d, "cannot copy " + d.filename + ": " + e);
+	newd.x = x;
+	newd.goto(d.startmark);
+	return newd;
+}
+
+readfile(f: string): string
+{
+	if ((fd := sys->open(f, Sys->OREAD)) == nil)
+		return nil;
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0)
+		return nil;
+	if(d.length > big (128*1024))	# let's keep within bounds
+		return nil;
+	l := int d.length;
+	buf := array[l] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return nil;
+	return string buf[0:n];
+}
+
+error(d: ref Datasource, e: string)
+{
+	d.error = sys->sprint("%s:%d: %s", d.x.loc.systemid, d.x.loc.line, e);
+	raise "error";
+}
+
+warning(d: ref Datasource, e: string)
+{
+	if (d.warningch != nil)
+		d.warningch <-= (d.x.loc, e);
+	else
+		sys->print("(nil warningch) %s\n", e);
+}
+
+Datasource.next(d: self ref Datasource, linkch: chan of (string, string, string)): (Block, string)
+{
+	{
+		w := ".t" + string maxblockid++;
+		d.t = Text.new(d.win, w, d.width, d.evch);
+		d.t.style = hd d.styles;
+		d.t.fontinfo = hd d.fontinfo;
+		d.linkch = linkch;
+		m := d.imark;
+		if ((gi := d.item) == nil) {
+			m = d.x.mark();
+			gi = nextitem(d, 1);
+		}
+		d.item = nil;
+		d.imark = nil;
+		if (gi == nil) {
+			cmd(d.win, "destroy " + w);
+			return ((nil, 0, 0), d.error);
+		}
+		first := 1;
+		for (;;) {
+			pick i := gi {
+			Text =>
+				e_inline_text(d, i);
+			Tag =>
+				tagid := tagmap.i(i.name);
+				if (tagid == -1) {
+					warning(d, "unknown tag '" + i.name + "'; expected %flow");
+					continue;
+				}
+				if (int isblocklevel[tagid]) {
+					if (!first) {
+						d.t.finalise(0);
+						d.item = i;
+						d.imark = m;
+						b := Block(w, d.t.outertmargin, d.t.outerbmargin);
+						d.t = nil;
+						return (b, nil);
+					}
+					e_block(d, tagid, i);
+				} else
+					e_inline(d, tagid, i);
+			}
+			# XXX sending links when getting an item here is not correct,
+			# as if it's a block level item with an id, then the link marker
+			# will go at the end of the current block rather than the
+			# beginning of the next block as it should.
+			m = d.x.mark();
+			gi = nextitem(d, 1);
+			if (gi == nil) {
+				d.t.finalise(0);
+				b := Block(w, d.t.outertmargin, d.t.outerbmargin);
+				d.t = nil;
+				return (b, nil);
+			}
+			first = 0;
+		}
+	}
+	exception{
+		"error" =>
+			return ((nil, 0, 0), d.error);
+	}
+}
+
+Datasource.linestart(d: self ref Datasource, w: string, y: int): int
+{
+	if (w[1] == 't') {
+		# given a text widget and a y-coord inside it, adjust the
+		# y-coord so it refers to the start of the line holding the y-coord.
+		(n, toks) := sys->tokenize(cmd(d.win, w + " dlineinfo @0," + string y), " ");
+		if (n >= 5) {
+			# dlineinfo gives fields: x y width height baseline
+			return int hd tl toks;
+		}
+	}
+	return y;
+}
+
+Datasource.linkoffset(d: self ref Datasource, w: string, m: string): int
+{
+	(n, toks) := sys->tokenize(cmd(d.win, w + " dlineinfo " + m), " ");
+	if (n >= 5)
+		return int hd tl toks;
+	return -1;
+}
+
+# return a "best-effort" file offset 
+#Datasource.fileoffsetnearyoffset(t: self ref Datasource, w: string, yoffset: int): int
+Datasource.fileoffsetnearyoffset(nil: self ref Datasource, nil: string, nil: int): int
+{
+	# as we can't find out what embedded widget is at a given index,
+	# we'll go first through all the embedded widgets checking to see which
+	# ones are hit by the y-coord.
+	return 0;
+}
+
+Datasource.rectforfileoffset(d: self ref Datasource, w: string, fileoffset: int): (int, Draw->Rect)
+{
+	r := Rect((0, 0), (0, 0));
+	case widgettype(w) {
+	IMAGE or
+	MARK or
+	RULE =>
+		return (0, r);
+	TABLE =>
+#sys->print("rectforfileoffset requested for table...\n");
+		return (0, r);
+	TEXT =>
+		# find greatest fileoffset in text less than fileoffset
+		(nil, toks) := sys->tokenize(cmd(d.win, w + " tag names"), " ");
+		max := -1;
+		for (; toks != nil; toks = tl toks) {
+			if ((hd toks)[0] == 'o') {
+				o := int (hd toks)[1:];
+				if (o <= fileoffset && o > max)
+					max = o;
+			}
+		}
+		if (max == -1)
+			return (0, r);
+
+		idx := cmd(d.win, w + " index o" + string max + ".first");
+
+		# check whether we've hit an embedded widget.
+		ew := tk->cmd(d.win, w + " window cget " + idx + " -window");
+		if (ew[0] != '!') {
+			(ok, t) := d.rectforfileoffset(ew, fileoffset);
+			if (ok)
+				t = t.addpt(s2r(cmd(d.win, w + " bbox " + idx)).min);
+			return (ok, t);
+		}
+
+		idx = cmd(d.win, sys->sprint("%s index {%s + %d chars}",
+				w, idx, fileoffset - max));
+	
+		# check that the offset index isn't beyond the end of the
+		# range (in which case the offset we're looking for isn't
+		# contained in this text widget.)
+		if (int cmd(d.win, sys->sprint("%s compare %s >= o%d.last", w, idx, max)))
+			return (0, ((0, 0), (0, 0)));
+
+		r = s2r(cmd(d.win, w + " bbox " + idx));
+		r.max = r.min.add(r.max);
+		return (1, r);
+	* =>
+		sys->print("unknown widget type %s\n", w);
+		return (0, r);
+	}
+}
+
+# get file offset 
+#Datasource.fileoffsetat(d: self ref Datasource, y: int): int
+#{
+#}	
+
+Datasource.event(d: self ref Datasource, e: string): ref Event
+{
+	case e[0] {
+	'l' =>
+		return ref Event.Link(e[2:]);
+	't' =>
+		(nil, toks) := sys->tokenize(e, " ");
+		toks = tl toks;
+		w := hd toks;
+		bd := int cmd(d.win, w + " cget -borderwidth");
+		p := Point(int hd tl toks, int hd tl tl toks).
+			sub((int cmd(d.win, w + " cget -actx") + bd, int cmd(d.win, w + " cget -acty") + bd));;
+		i := cmd(d.win, sys->sprint("%s index @%d,%d", w, p.x, p.y));
+		tags := cmd(d.win, w + " tag names " + i);
+		(nil, toks) = sys->tokenize(tags, " ");
+		for (; toks != nil; toks = tl toks)
+			if ((hd toks)[0] == 'o')
+				break;
+		if (toks != nil && hd toks != "o-1") {
+			idx := rangestart(d.win, w, hd toks, i);
+			if (idx == nil)
+				sys->print("couldn't find range start of %s\n", hd toks);
+			else
+				return ref Event.Texthit(
+						int (hd toks)[1:] +
+						len cmd(d.win, w + " get " + idx + " " + i)
+					);
+		}
+	}
+	return nil;
+}
+
+Datasource.mark(d: self ref Datasource): ref Mark
+{
+	if (d.item != nil) {
+		if (d.imark == nil) {
+			sys->print("oops, imark shouldn't be nil\n");
+		}
+		return ref Mark(d.imark);
+	} else
+		return ref Mark(d.x.mark());
+}
+
+Datasource.goto(d: self ref Datasource, m: ref Mark)
+{
+	d.x.goto(m.xmark);
+	d.item = nil;
+	d.imark = nil;
+}
+
+Datasource.fileoffset(d: self ref Datasource): int
+{
+	if (d.item != nil)
+		return d.item.fileoffset;
+	else
+		return d.x.fileoffset;
+}
+
+# XXX this might not be correct in the presence of Mark.item
+Datasource.atmark(d: self ref Datasource, m: ref Mark): int
+{
+	if (m == nil)
+		return 0;
+	return m.fileoffset() == d.fileoffset();
+}
+
+Datasource.str2mark(d: self ref Datasource, s: string): ref Mark
+{
+	m := d.x.str2mark(s);
+	if (m == nil)
+		return nil;
+	return ref Mark(m);
+}
+
+Datasource.mark2str(nil: self ref Datasource, m: ref Mark): string
+{
+	return xml->m.xmark.str();
+}
+
+Mark.eq(m1: self ref Mark, m2: ref Mark): int
+{
+	if (m1 == nil || m2 == nil)
+		return 0;
+	return m1.fileoffset() == m2.fileoffset();
+}
+
+Mark.fileoffset(m: self ref Mark): int
+{
+	return m.xmark.offset;
+}
+
+rangestart(win: ref Tk->Toplevel, w: string, tag: string, idx: string): string
+{
+	# find the start of the range of _tag_ covering _idx_.
+
+	# first find the end of the tag range.
+	(nil, toks) := sys->tokenize(cmd(win, w + " tag nextrange " + tag + " " + idx), " ");
+	if (toks == nil)
+		return nil;
+
+	# find the start of the tag range
+	(nil, toks) = sys->tokenize(cmd(win, w + " tag prevrange " + tag + " " + hd tl toks), " ");
+	if (toks == nil)
+		return nil;
+	return hd toks;
+}
+
+startdocument(d: ref Datasource): string
+{
+	(item, dtype, err) := xmldocument(d);
+	if (err != nil)
+		error(d, err);
+	if (doctype(dtype, Laxchecking) != OEBdoc)
+		error(d, "invalid document type: " + dtype);
+	if (item == nil)
+		error(d, "unexpected EOF");
+	i: ref Item.Tag;
+	pick xi := item {
+	Tag =>
+		i = xi;
+	* =>
+		i = nexttag(d, 0);
+	}
+	if (i == nil || i.name != "html")
+		error(d, "no html body");
+		
+	down(d, i, 0);
+	return starthtml(d);
+}
+
+# mostly pinched from oebpackage.b;
+# return (item, dtd, error) where item is the first item that's not part of
+# the prolog.
+xmldocument(d: ref Datasource): (ref Item, string, string)
+{
+	dtd := "";
+	x := d.x;
+	for (xi := x.next(); xi != nil; xi = x.next()) {
+		pick i := xi {
+		Process =>
+			if (i.target != "xml")
+				return (nil, nil, "not an XML file");		# XXX actually according to spec, this declaration is optional.
+		Text =>
+			if (i.ch != nil)
+				return (i, dtd, nil);
+		Doctype =>
+			if (!i.public || len i.params < 2)
+				return (nil, nil, "invalid document type");
+			dtd = hd tl i.params;
+		Stylesheet =>
+# XXX			etc etc.
+		Error =>
+			error(d, i.msg);		# XXX should show locator held in i, not as added by error()
+		* =>
+			return (xi, dtd, nil);
+		}
+	}
+	return (nil, dtd, nil);
+}
+#
+#	xi := x.next();
+#	if(xi == nil)
+#		return (nil, "not valid XML");
+#	pick i := xi {
+#	Process =>
+#		if(i.target != "xml")
+#			return (nil, "not an XML file");		# XXX actually according to spec, this declaration is optional.
+#	* =>
+#		return (nil, "unexpected file structure");
+#	}
+#
+#	xi = x.next();
+#	if (xi == nil)
+#		return (nil, "invalid document");
+#	if (tagof(xi) == tagof(Item.Text)) {		# XXX limbo compiler bug: tagof(Xml->Item.Text) is invalid.
+#		xi = x.next();
+#		if (xi == nil)
+#			return (nil, "invalid document");
+#	}
+#	pick i := xi {
+#	Doctype =>
+#		if (!i.public || len i.params < 2)
+#			return (nil, "invalid document type");
+#		return (hd tl i.params, nil);
+#	}
+#	return (nil, "not OEB document (no DOCTYPE)");
+#}
+
+starthtml(d: ref Datasource): string
+{
+	# both <head> and <body> tags are optional, so if we
+	# get something that's neither of them, then we assume
+	# that we're in <body> and therefore need do no header processing.
+	# that's probably wrong... how *can* <head> be optional
+	# without arbitrary lookahead?
+
+	# question: if a tag isn't explicitly there, but implied because it's
+	# optional, does it still have style attributes applied to it?
+
+	item: ref Item.Tag;
+	startitem := nextnonblank(d, 0);
+	if (startitem == nil)
+		return nil;
+	pick i := startitem {
+	Tag =>
+		if (i.name == "head" || i.name == "body")
+			item = i;
+	}
+	if (item == nil) {
+		d.item = startitem;
+		return nil;
+	}
+	if (item.name == "body") {
+		down(d, item, 0);
+		return nil;
+	}
+	e_head(d, item);
+	startitem = nextnonblank(d, 0);
+	if (startitem == nil)
+		return nil;
+	pick i := startitem {
+	Tag =>
+		if (i.name == "body") {
+			down(d, i, 0);
+			return nil;
+		}
+	}
+	d.item = startitem;
+	return nil;
+}
+
+e_head(d: ref Datasource, i: ref Item.Tag)
+{
+	down(d, i, 0);
+	while ((t0 := nexttag(d, 0)) != nil) {
+		case t0.name {
+		"title" =>
+			e_title(d, t0);
+		"link" =>
+			e_link(d, t0);
+		"style" =>
+			e_style(d, t0);
+		}
+	}
+	up(d, 0);
+}
+
+e_title(d: ref Datasource, i: ref Item.Tag)
+{
+	down(d, i, 0);
+	t0 := nextnonblank(d, 0);
+	if (t0 != nil) {
+		pick t := t0 {
+		Text =>
+			d.title = t.ch;
+		* =>
+			warning(d, "invalid tag in title");
+		}
+	}
+	up(d, 0);
+}
+
+e_style(d: ref Datasource, i: ref Item.Tag)
+{
+	ltype := i.attrs.get("type");
+	if (ltype != "text/x-oeb1-css" && ltype != "text/css") {
+		warning(d, "unknown stylesheet type " + ltype);
+		return;
+	}
+	down(d, i, 0);
+	t0 := nextnonblank(d, 0);
+	if (t0 != nil) {
+		pick t := t0 {
+		Text =>
+			d.stylesheet.addrules(cssparser->parse(t.ch), Stylesheet->AUTHOR);
+		* =>
+			warning(d, "invalid tag in style");
+		}
+	}
+	up(d, 0);
+}
+
+e_link(d: ref Datasource, i: ref Item.Tag)
+{
+	rel := i.attrs.get("rel");
+	ltype := i.attrs.get("type");
+	where := i.attrs.get("href");
+
+	if (rel != "stylesheet")
+		return;
+	if (ltype != "text/x-oeb1-css" && ltype != "text/css") {
+		warning(d, "unknown stylesheet type " + ltype);
+		return;
+	}
+	file := href(d.filename, where);
+	if (file == nil) {
+		warning(d, "cannot find stylesheet " + where);
+		return;
+	}
+	
+	rules := cssparser->parse(readfile(file));
+	d.stylesheet.addrules(rules, Stylesheet->AUTHOR);
+}
+	
+
+e_block(d: ref Datasource, tagid: int, i: ref Item.Tag)
+{
+	down(d, i, 1);
+
+	case tagid {
+	# %list
+	Eul or
+	Eol =>
+		e_list(d, i);
+
+	# %heading
+	Eh1 or Eh2 or Eh3 or Eh4 or Eh5 or Eh6 =>
+		e_inline_flow(d);
+
+	Ediv or
+	Ecenter or
+	Eblockquote =>
+		while ((fi := nextitem(d, 1)) != nil)
+			e_flow(d, fi);
+
+	# %preformatted
+	Epre =>
+		e_inline_flow(d);
+	Edl =>
+		e_dl(d, i);
+	Ehr =>
+		w := d.t.widgetname(RULE);
+		width: int;
+		a := (hd d.styles).attrs;
+		if (a[Swidth] != nil)
+			width = length(hd d.fontinfo, a[Swidth]);
+		else
+			width = d.width;
+		cmd(d.win, "frame " + w + " -bg " + a[Scolor] +
+				" -width " + string width + " -height 3");
+		d.t.addwidget(w, i.fileoffset, 0);
+	Etable =>
+		e_table(d, i);
+	Ep =>
+		e_inline_flow(d);
+	* =>
+		warning(d, "unknown tag '" + i.name+ "'");
+	}
+	up(d, 1);
+}
+
+length(fi: ref Fontinfo, s: string): int
+{
+	return units->length(s, fi.em, fi.ex, nil).t0;
+}
+
+e_table(d: ref Datasource, i: ref Item.Tag)
+{
+	si := nexttag(d, 1);
+
+	# optional caption (ignore)
+	if (si != nil && si.name == "caption")
+		si = nexttag(d, 1);
+
+	if (si == nil) {
+		warning(d, "empty table");
+		return;
+	}
+	dim := Point(0, 0);		# table dimensions
+	pos := Point(0, 0);		# current position in table
+	celllist: list of (Point, ref Table->Cell);
+	# XXX BUG table rows with ids all get marked at the top of the table.
+	# would need to change the sendlink() scheme in order to fix that.
+	# something like: datasource has a current "marking scheme";
+	# in the case of the table widget the marking scheme creates a canvas
+	# widget tagged after the id and the row/col numbers; this is then
+	# placed into position when the table is laid out.
+	rspan := array[10] of {* => 0};
+	for (; si != nil; si = nexttag(d, 1)) {
+		if (si.name != "tr") {
+			warning(d, "non-tr tag <" + si.name + "> found in table body");
+			continue;
+		}
+		down(d, si, 0);
+		pos.x = 0;
+		for (ti := nexttag(d, 1); ti != nil; ti = nexttag(d, 1)) {
+			if (ti.name != "td" && ti.name != "th") {
+				warning(d, "invalid cell <" + ti.name + "> in table");
+				continue;
+			}
+			down(d, ti, 0);
+			oldt := d.t;
+
+			# XXX what do we do about text widget widths in table cells
+			# where no width is specified?
+			d.t = Text.new(d.t.win, oldt.widgetname(TEXT), 0, d.t.evch);
+			d.t.style = oldt.style;
+			d.t.fontinfo = oldt.fontinfo;
+			for (t0 := nextitem(d, 1); t0 != nil; t0 = nextitem(d, 1))
+				e_flow(d, t0);
+			up(d, 0);
+			d.t.finalise(1);
+
+			span := Point(int ti.attrs.get("colspan"), int ti.attrs.get("rowspan"));
+			if (span.x < 1)
+				span.x = 1;
+			if (span.y < 1)
+				span.y = 1;
+
+			# find a column it can go in.
+			for (; pos.x < len rspan; pos.x++)
+				if (rspan[pos.x] <= 0)
+					break;
+			celllist = (pos, table->newcell(d.t.w, span)) :: celllist;
+			if (span.y > 1) {
+				if (len rspan < pos.x + span.x)
+					rspan = (array[pos.x + span.x] of int)[0:] = rspan;
+				for (x := pos.x; x < pos.x + span.x; x++)
+					rspan[x] = span.y;
+			}
+			pos.x += span.x;
+			if (pos.y + span.y > dim.y)
+				dim.y = pos.y + span.y;
+			d.t = oldt;
+		}
+		if (pos.x > dim.x)
+			dim.x = pos.x;
+		pos.y++;
+		for (x := 0; x < len rspan; x++)
+			rspan[x]--;
+		up(d, 0);
+	}
+
+	if (dim.y == 0 || dim.x == 0) {
+		warning(d, "empty table");
+		return;
+	}
+	cells := array[dim.x] of {* => array[dim.y] of ref Table->Cell};
+	for (; celllist != nil; celllist = tl celllist) {
+		(p, cell) := hd celllist;
+		cells[p.x][p.y] = cell;
+	}
+	w := d.t.widgetname(TABLE);
+	table->layout(cells, d.t.win, w);
+	d.t.addwidget(w, i.fileoffset, 0);
+}
+
+e_flow(d: ref Datasource, gi: ref Item)
+{
+	pick i := gi {
+	Text =>
+		e_inline_text(d, i);
+	Tag =>
+		tagid := tagmap.i(i.name);
+		if (tagid == -1)
+			warning(d, "unkown tag '" + i.name + "'; expected %flow");
+		else if (int isblocklevel[tagid])
+			e_block(d, tagid, i);
+		else
+			e_inline(d, tagid, i);
+	}
+}
+
+# (%inline;)*
+e_inline_flow(d: ref Datasource)
+{
+	while ((gi := nextitem(d, 1)) != nil) {
+		pick i := gi {
+		Text =>
+			e_inline_text(d, i);
+		Tag =>
+			e_inline(d, -1, i);
+		}
+	}
+}
+
+e_inline(d: ref Datasource, tagid: int, i: ref Item.Tag)
+{
+	if (tagid == -1)
+		tagid = tagmap.i(i.name);
+	case tagid {
+	# %phrase
+	Eem or
+	Estrong or
+	Edfn or
+	Ecode or
+	Esamp or
+	Ekbd or
+	Evar or
+	Ecite or
+
+	# %fontstyle
+	Ett or
+	Ei or
+	Eb or
+	Eu or
+	Es or
+	Estrike or
+	Ebig or
+	Esmall or
+	Espan or
+	Eq or
+	Esub or
+	Esup =>
+		down(d, i, 0);
+		e_inline_flow(d);
+		up(d, 0);
+	# %special
+	Ea =>
+		down(d, i, 0);
+		if ((href := i.attrs.get("href")) != nil)
+			d.t.href = " " + href;
+		if ((name := i.attrs.get("name")) != nil)
+			sendlink(d, name);
+		e_inline_flow(d);
+		d.t.href = nil;		# nesting of <a> not allowed so it's ok.
+		up(d, 0);
+	Eimg =>
+		e_image(d, i);
+	Eobject =>
+		if (e_object(d, i) == -1) {
+			down(d, i, 0);
+			e_object_contents(d, i);
+			up(d, 0);
+		}
+	Efont =>		
+		;
+
+	Ebr =>
+		d.t.linebreak();	
+	Escript or
+	Emap =>
+		sys->fprint(stderr, "script or map unimplemented\n");
+		d.t.addtext("e_special", 1, 1, i.fileoffset);
+	* =>
+		warning(d, "invalid inline element '" + i.name + "'");
+	}
+}
+
+e_image(d: ref Datasource, i: ref Item.Tag)
+{
+	file := href(d.filename, i.attrs.get("src"));
+	if (file == nil) {
+		warning(d, "cannot display image " + i.attrs.get("src"));
+		return;
+	}
+	if ((w := image(d, nil, file)) == nil)
+		return;
+	d.t.addwidget(w, i.fileoffset, 0);
+}
+
+e_object(d: ref Datasource, i: ref Item.Tag): int
+{
+	(class, mtype) := mimetype(i.attrs.get("type"));
+	if (class != "image")
+		return -1;
+
+	data := i.attrs.get("data");
+	if (data == nil)
+		return -1;
+
+	file := href(d.filename, data);
+	if (file == nil)
+		return -1;
+
+	if ((w := image(d, mtype, file)) == nil)
+		return -1;
+	d.t.addwidget(w, i.fileoffset, 0);
+	return 0;
+}
+
+e_object_contents(d: ref Datasource, nil: ref Item.Tag)
+{
+	# PARAM tags should be before any data, according to comment in the dtd.
+loop:
+	while ((t0 := nextitem(d, 1)) != nil) {
+		pick t1 := t0 {
+		Tag =>
+			if (t1.name != "param")
+				break loop;
+		* =>
+			break loop;
+		}
+	}
+	for (; t0 != nil; t0 = nextitem(d, 1))
+		e_flow(d, t0);
+}
+
+# XXX this has not been implemented from the standard so it's probably wrong.
+mimetype(s: string): (string, string)
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == '/')
+			break;
+	if (i >= len s)
+		return (s, nil);
+	return (s[0:i], s[i+1:]);
+}
+
+# read an image and return it as a widgetname
+image(d: ref Datasource, mediatype, f: string): string
+{
+	t := d.t;
+	# XXX could make a special case here for pic images, as
+	# they can be read directly by tk, hence faster & less space.
+	if (tk->cmd(t.win, "image width " + f)[0] == '!') {
+		# if it's not cached, create it.
+		(img, e) := mimeimage->image(mediatype, f);
+		if (img == nil) {
+			# try fallback
+			for (fall := d.fallbacks; fall != nil; fall = tl fall)
+				if ((hd fall).t0 == f)
+					return image(d, nil, (hd fall).t1);
+
+			warning(d, sys->sprint("cannot read image %s: %s", f, e));
+			return nil;
+		}
+		tk->cmd(t.win, "image create bitmap " + f);
+		if ((e = tk->putimage(t.win, f, img, nil)) != nil) {
+			warning(d, sys->sprint("imageput on %s failed: %s", f, e));
+			return nil;
+		}
+	}
+	w := t.widgetname(IMAGE);
+	cmd(t.win, "label " + w + " -image " + f);
+	if (t.href != nil) {
+		cmd(t.win, "bind " + w + " <ButtonRelease-1> " +
+			tk->quote("send " + t.evch + " ds l" + t.href));
+	}
+	return w;
+}
+
+sendlink(d: ref Datasource, name: string)
+{
+	if (d.linkch != nil) {
+		# this won't work when we're embedded in a canvas.
+		# N.B. it's crucial that name is non-nil!
+		d.linkch <-= (name, d.t.w, d.t.addmark());
+	} else {
+		# you might not think that a zero-sized widget could make any
+		# difference to the text layout, but you'd be wrong.
+		d.t.addmark();
+	}
+}
+
+e_inline_text(d: ref Datasource, i: ref Item.Text)
+{
+	d.t.addtext(i.ch, i.ws1, i.ws2, i.fileoffset);
+}
+
+# attributes that have percentage values that refer to the width of
+# their enclosing block.  the whole thing is inevitably a crock.
+# text-indent for example is supposed to take the width from its
+# immediate ancestor...  whose width is probably determined by the
+# assigned width.  eurgh.
+blocksizerelative := array[] of {
+	Sheight,
+	Smargin_left,
+	Smargin_right,
+#	Smargin_top,
+#	Smargin_bottom,
+	Swidth,
+	Stext_indent,
+};
+
+down(d: ref Datasource, i: ref Item.Tag, isblock: int)
+{
+#sys->print("down('%s', %d)\n", i.name, isblock);
+	if (i == nil) {
+		sys->print("nil tag\n");
+		raise "oops";
+	}
+	d.x.down();
+	d.tags = i :: d.tags;
+
+	style := getstyle(d);
+	a := style.attrs;
+	fi := *(hd d.fontinfo);
+
+	# make relative units into absolute units so that the derived
+	#  values are inherited as per the standard.
+
+	# font size is relative to the parent font size, not the current font size.
+	fontsize := a[Sfont_size];
+	if (units->isrelative(fontsize)) {
+		(nil, fontsize) = units->length(fontsize, fi.em, fi.ex,
+			(hd d.styles).attrs[Sfont_size]);
+		a[Sfont_size] = fontsize;
+	}
+
+	# XXX later
+	#	Sborder
+
+	(path, em, ex) := cssfont->getfont((a[Sfont_family], a[Sfont_style],
+			a[Sfont_weight], a[Sfont_size]), fi.em, fi.ex);
+	# symbolic font names are turned into their size so we only
+	# have to do the work once.
+	if (fontsize != nil && (fontsize[0] < '0' || fontsize[0] > '9'))
+		a[Sfont_size] = fontsize = string em + "px";
+
+	# de-relativise widths
+	for (j := 0; j < len blocksizerelative; j++) {
+		attr := blocksizerelative[j];
+		if (units->isrelative(a[attr]))
+			(nil, a[attr]) = units->length(a[attr], em, ex, string d.width);
+	}
+
+	d.fontinfo = ref Fontinfo(path, em, ex) :: d.fontinfo;
+	d.styles = style :: d.styles;
+	if (d.t != nil) {
+		d.t.fontinfo = hd d.fontinfo;
+		d.t.style = style;
+		if (isblock)
+			d.t.startblock();
+	}
+}
+
+up(d: ref Datasource, isblock: int)
+{
+	oldstyle: ref Stylesheet->Style;
+	d.x.up();
+#sys->print("up('%s', %d)\n", (hd d.tags).name, isblock);
+	d.tags = tl d.tags;
+
+	(oldstyle, d.styles) = (hd d.styles, tl d.styles);
+	d.fontinfo = tl d.fontinfo;
+
+	if (d.t != nil) {
+		d.t.style = hd d.styles;
+		d.t.fontinfo = hd d.fontinfo;
+		if (isblock)
+			d.t.endblock();
+	}
+}
+
+# definition list
+e_dl(d: ref Datasource, nil: ref Item.Tag)
+{
+	while ((li := nexttag(d, 1)) != nil) {
+		if (li.name == "dt") {
+			down(d, li, 1);
+			e_inline_flow(d);
+			up(d, 1);
+		} else if (li.name == "dd") {
+			down(d, li, 1);
+			while ((i := nextitem(d, 1)) != nil)
+				e_flow(d, i);
+			up(d, 1);
+		} else
+			warning(d, "unexpected list element '" + li.name + "', expected <dt>");
+	}
+}
+
+nexttag(d: ref Datasource, sendid: int): ref Item.Tag
+{
+	while ((gi := nextitem(d, sendid)) != nil) {
+		pick i := gi {
+		Tag =>
+			return i;
+		}
+	}
+	return nil;
+}
+
+nextnonblank(d: ref Datasource, sendid: int): ref Item
+{
+	while ((gi := nextitem(d, sendid)) != nil) {
+		pick i := gi {
+		Text =>
+			if (i.ch != nil)
+				return i;
+		Tag =>
+			return i;
+		}
+	}
+	return nil;
+}
+
+nextitem(d: ref Datasource, sendid: int): ref Xml->Item
+{
+	for (;;) {
+		if ((gi := d.x.next()) == nil)
+			return nil;
+		pick i := gi {
+		Tag =>
+			if (sendid && (id := i.attrs.get("id")) != nil)
+				sendlink(d, id);
+			return i;
+		Text =>
+			return i;
+		Error =>
+			error(d, i.msg);		# XXX should show locator held in i, not as added by error()
+		Process =>
+			sys->print("processing request: target: '%s'; data: '%s'\n",
+				i.target, i.data);
+			# XXX recognise some types of processing (e.g. stylesheets) here?
+		Stylesheet =>
+			# ignore it outside the prolog
+		Doctype =>
+			# ignore it outside the prolog
+		* =>
+			sys->print("reader: unknown tag of type %d\n", tagof(gi));
+		}
+	}
+}
+
+e_list(d: ref Datasource, nil: ref Item.Tag)
+{
+	n := 0;
+	while ((li := nexttag(d, 1)) != nil) {
+		if (li.name != "li") {
+			warning(d, "unexpected list element '" + li.name + "'");
+			continue;
+		}
+		down(d, li, 1);
+		listheader(d.t, hd d.styles, n);
+		while ((fi := nextitem(d, 1)) != nil)
+			e_flow(d, fi);
+		up(d, 1);
+		n++;
+	}
+}
+
+#what about inheritance vs. units.
+#	e.g.
+#	<ul style="font-size: 150%"><li style="font-size: 150%">hello</li></ul>
+#	"hello" should come out 2.25 times the size of the font outside <ul>;
+#
+#	therefore all units must be resolved properly for each tag;
+#	we can't just let them be lazy until the properties are actually needed.
+#	hmm.
+#
+#	actually we only need to resolve relative elements, and those
+#	measured with respect to current font size.
+#
+#	e.g. 150%, 10em, larger
+
+listheader(t: ref Text, style: ref Style, n: int)
+{
+	s: string;
+	case ty := style.attrs[Slist_style_type] {
+	* or
+	"disc" =>
+		s = "•";
+	"square" =>
+		s = "∎";
+	"circle" =>
+		s = "∘";
+	"decimal" =>
+		s = string (n + 1) + ".";
+	"lower-alpha" or
+	"upper-alpha" =>
+		let := 'A';
+		if(ty[0] == 'l')
+			let = 'a';
+		a := ".";
+		for(; n > 25; n /= 26)
+			a[len a] = n%26 + let;
+		for(i := len a; --i >= 0;)
+			s[len s] = a[i];
+	"lower-roman" or
+	"upper-roman" =>
+		if((s = roman(n)) == nil)
+			s = sys->sprint("%d", n);	# better arabic than nothing
+		s += ".";
+		if (ty[0] == 'l')
+			s = str->tolower(s);
+	}
+	s[len s] = ' ';
+	t.addtext(s, 0, 0, -1);
+}
+
+#
+# derived from Python function by Mark Pilgrim
+#	``do ut des''
+#
+roman(n: int): string
+{
+	if(n <= 0 || n > 3999)
+		return nil;
+	map := array[] of {
+			(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"),
+			(90, "XC"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"),
+			(5, "V"), (4, "IV"), (1, "I")};
+	s := "";
+	for(i := 0; i < len map; i++){
+		(m, v) := map[i];
+		while(n >= m){
+			s += v;
+			n -= m;
+		}
+	}
+	return s;
+}
+
+blanktext: Text;
+Text.new(win: ref Tk->Toplevel, w: string, width: int, evch: string): ref Text
+{
+	t := ref blanktext;
+	t.win = win;
+	t.w = w;
+	t.tags = array[23] of list of (string, int);
+	t.startofline = 1;
+	t.evch = evch;
+	t.margins = t.margin :: nil;
+	cmd(win, "text " + t.w +
+		" -relief flat -bd 0 -propagate 1 " +
+		" -wrap word -bg white");
+	if (width > 0)
+		cmd(win, t.w + " configure -width " + string width);
+	cmd(win, t.w + " tag bind t <ButtonRelease-1> {send " + t.evch + " ds t %W %X %Y}");
+#sys->print("****** new text %s\n", w);
+	return t;
+}
+
+Text.addtext(t: self ref Text, text: string, ws1, ws2: int, fileoffset: int)
+{
+	if (text != nil) {
+		if (t.needspace) {
+			t.vspace(t.margin.b);
+			t.margin.b = 0;
+			t.needspace = 0;
+		}
+#sys->print("%s addtext '%s'\n", t.w, sanitise(text));
+		s := t.w + " insert end ";
+
+		# we add some leading whitespace if the last text added finished with whitespace
+		# or this text starts with whitespace and this isn't the first item
+		# on the line.
+		if (ws1 && !t.startofline)
+			text = " " + text;			# XXX might be faster to do two inserts.
+		if (ws2)
+			text += " ";
+		s += tk->quote(text) + " {";
+		s += t.gettag(textattrs(t)) + " o" + string fileoffset;
+		if (t.href != nil)
+			s += " " + t.gettag(t.href);
+		else
+			s += " t";
+		s += "}";
+		cmd(t.win, s);
+		t.startofline = 0;
+	}
+	t.lastwhite = ws2;
+}
+
+sanitise(s: string): string
+{
+	if (len s > 30)
+		s = s[0:30] + "...";
+	return s;
+}
+
+Text.linebreak(t: self ref Text)
+{
+	cmd(t.win, t.w + " insert end {\n}");
+	t.startofline = 1;
+#sys->print("linebreak: startofline == 1\n");
+	t.lastwhite = 0;
+}
+
+Text.startblock(t: self ref Text)
+{
+	a := t.style.attrs;
+	m: Margin;
+	m.b = length(t.fontinfo, a[Smargin_bottom]);
+	m.l = length(t.fontinfo, a[Smargin_left]);
+	m.r = length(t.fontinfo, a[Smargin_right]);
+	m.textindent = length(t.fontinfo, a[Stext_indent]);
+
+	tmargin := length(t.fontinfo, t.style.attrs[Smargin_top]);
+	if (tl t.margins != nil) {
+		# merge top and bottom margins
+		if (t.margin.b > tmargin)
+			tmargin = t.margin.b;
+		t.vspace(tmargin);
+	} else
+		t.outertmargin = tmargin;
+
+	t.margins = m :: t.margins;
+	t.margin.l += m.l;
+	t.margin.r += m.r;
+	t.margin.textindent = m.textindent;
+	t.margin.b = 0;
+#	XXX check for margin overflow
+# MINWIDTH: con 40;
+#	if (t.lmargin + t.rmargin >= t.width)
+}
+
+Text.endblock(t: self ref Text)
+{
+	# spit out any left-over bottom margin
+	if (t.needspace) {
+		t.vspace(t.margin.b);
+		t.needspace = 0;
+	}
+	m: Margin;
+	(m, t.margins) = (hd t.margins, tl t.margins);
+#sys->print("%s end block; bottom: %d, previous bottom: %d\n", t.w, m.b, t.margin.b);
+	t.margin.l -= m.l;
+	t.margin.r -= m.r;
+	t.margin.b = m.b;
+	t.margin.textindent = (hd t.margins).textindent;
+	t.needspace = 1;
+}
+
+Text.finalise(t: self ref Text, addvspace: int)
+{
+	if (addvspace) {
+		t.vspace(t.margin.b);
+		t.margin.b = 0;
+	}
+	# get rid of any trailing newline (this doesn't work for null-sized text widgets.
+	if (tk->cmd(t.win, t.w + " get {end - 1 chars} end") == "\n") {
+#		sys->print("deleting last newline\n");
+		cmd(t.win, t.w + " delete {end - 1 chars} end");
+	}
+	t.outerbmargin = t.margin.b;
+}
+
+Text.vspace(t: self ref Text, h: int)
+{
+#sys->print("vspace %d (startofline: %d)\n", h, t.startofline);
+	if (!t.startofline)
+		cmd(t.win, t.w + " insert end {\n}");
+	if (h > 0) {
+		# XXX this is unfortunately inefficient for something that's used so
+		# much, but i can't think of another way of creating a line
+		# of arbitrary height without adding a trailing newline
+		# (which mucks things up at the end of the text widget).
+		w := t.widgetname(VSPACE);
+		cmd(t.win, "frame " + w + " -height " + string h); # + " -width 100 -bg red");
+		tag :=  t.gettag("-lineheight " + string h);
+		t.addwidget(w, -1, 1);
+		cmd(t.win, t.w + " tag add " + tag + " {end - 1 chars}");
+		cmd(t.win, t.w + " insert end {\n} " + tag);
+	}
+#sys->print("vspace: start of line: 1\n");
+	t.startofline = 1;
+	t.lastwhite = 0;
+}
+
+# add zero sized, invisible item to mark a place
+# that can then be retrieved with linkoffset when
+# the text widget has actually been rendered.
+Text.addmark(t: self ref Text): string
+{
+	w := t.widgetname(MARK);
+	cmd(t.win, "frame " + w);
+	t.addwidget(w, -1, 1);
+	return w;
+}
+
+widgettype(w: string): int
+{
+	for (i := len w - 1; i >= 0; i--) {
+		c := w[i];
+		if (c < '0' || c > '9')
+			return c;
+	}
+	return '.';
+}
+
+Text.widgetname(t: self ref Text, c: int): string
+{
+	s := t.w + ".";
+	s[len s] = c;
+	return s + string t.max++;
+}
+
+Text.addwidget(t: self ref Text, w: string, fileoffset: int, invisible: int)
+{
+	align: string;
+#	case t.style.attrs[Svertical_align] {
+#	"top" =>
+#		align = " -align top";
+#	"bottom =>
+#		align = " -align bottom";
+#	"middle" =>
+#		align = "-align center";
+#	}
+	cmd(t.win, t.w + " window create end -window " + w + align);
+	# apparently no way to add tags to an embedded window when it's created.
+	cmd(t.win, t.w + " tag add o" + string fileoffset + " " + w);
+	t.startofline = !invisible;
+#sys->print("addwidget: startofline %d\n", t.startofline);
+}
+
+getstyle(d: ref Datasource): ref Style
+{
+	style := d.stylesheet.newstyle();
+	style.attrs[0:] = defaults;
+	parent := hd d.styles;
+	for (i := 0; i < len stylenames; i++)
+		if (int inherited[i])
+			style.attrs[i] = parent.attrs[i];
+
+	# push inline style information here
+	tag := hd d.tags;
+	style.add(tag.name, tag.attrs.get("class"));
+	style.adddecls(cssparser->parsedecl(tag.attrs.get("style")));
+	return style;
+}
+
+# N.B. Text.gettag() relies on the fact that the string this returns
+# starts with '-'
+textattrs(t: ref Text): string
+{
+	a := t.style.attrs;
+	s := "-font " + t.fontinfo.path +
+		" -fg " + a[Scolor] +
+		" -lmargin1 " + string (t.margin.textindent + t.margin.l) +
+		" -lmargin2 " + string t.margin.l +
+		" -rmargin " + string t.margin.r;
+	v := a[Stext_decoration];
+	if (v == "underline")
+		s += " -underline 1";
+	else if (v == "line-through")
+		s += " -overstrike 1";
+	v = a[Sline_height];
+	if (v != "normal") {
+		# special case: when line height is an unadorned number,
+		# it is relative, but is inherited as is, not as derived, so we
+		# need to derive the value here.
+		# it's not clear whether the size should be proportional to the derived or
+		# the specified font size; using the derived font size seems more reasonable.
+		(l, nil) := units->length(v, 0, 0, string t.fontinfo.em + "px");
+		s += " -lineheight " + string l;
+	}
+
+	v = a[Sbackground_color];
+	if (v != nil)
+		s += " -bg " + a[Sbackground_color];
+	v = a[Stext_align];
+	if (v != nil && v != "justify")
+		s += " -justify " + v;
+	return s;
+}
+
+# get a tag for a particular sort of text; if s begins with a '-', then it's a set
+# of configuration options; otherwise it's a URL link (prefixed with a space)
+Text.gettag(t: self ref Text, s: string): string
+{
+	v := hashfn(s, len t.tags);
+	for (l := t.tags[v]; l != nil; l = tl l)
+		if ((hd l).t0 == s)
+			return "t" + string (hd l).t1;
+	t.tags[v] = (s, t.max) :: t.tags[v];
+	tag := "t" + string t.max++;
+	if (s[0] == '-') {
+		cmd(t.win, t.w + " tag configure " + tag + " " + s);
+	} else {
+		cmd(t.win, t.w + " tag bind " + tag + " <ButtonRelease-1> " +
+			tk->quote("send " + t.evch + " ds l" + s));
+	}
+	return tag;
+}
+
+# XXX this isn't sufficient, in the presence of the object tag's codebase attribute.
+href(fromfile: string, href: string): string
+{
+	(u, e) := makerelativeurl(fromfile, href);
+	if (u == nil)
+		return nil;
+	return u.path;
+}
+
+# copied from ebook.b; XXX what module should implement this,
+makerelativeurl(fromfile: string, href: string): (ref ParsedUrl, string)
+{
+	dir := "./";
+	for(n := len fromfile; --n >= 0;) {
+		if(fromfile[n] == '/') {
+			dir = fromfile[0:n+1];
+			break;
+		}
+	}
+	u := url->makeurl(href);
+	if(u.scheme != Url->FILE && u.scheme != Url->NOSCHEME)
+		return (nil, sys->sprint("URL scheme %s not yet supported", url->schemes[u.scheme]));
+	if(u.host != "localhost" && u.host != nil)
+		return (nil, "non-local URLs not supported");
+	path := u.path;
+	if (path == nil)
+		u.path = fromfile;
+	else {
+		if(u.pstart == "/")
+			path = "/" + path;
+		else
+			path = dir+path;	# TO DO: security
+		(ok, d) := sys->stat(path);
+		if(ok < 0)
+			return (nil, sys->sprint("'%s': %r", path));
+		u.path = path;
+	}
+	return (u, nil);
+}
+
+s2r(s: string): Draw->Rect
+{
+	(n, toks) := sys->tokenize(s, " ");
+	if (n != 4)
+		return ((0, 0), (0, 0));
+	r: Draw->Rect;
+	(r.min.x, toks) = (int hd toks, tl toks);
+	(r.min.y, toks) = (int hd toks, tl toks);
+	(r.max.x, toks) = (int hd toks, tl toks);
+	(r.max.y, toks) = (int hd toks, tl toks);
+	return r;
+}
+
+doctype(s: string, lax: int): int
+{
+	case s {
+	OEBpkgtype =>
+		return OEBpkg;
+	OEBdoctype =>
+		return OEBdoc;
+	* =>
+		if (!lax)
+			return -1;
+		if (contains(s, "oebpkg1"))
+			return OEBpkg;
+		if (contains(s, "oebdoc1"));
+			return OEBdoc;
+		sys->print("'%s' doesn't contain '%s' or ''%s'\n", s, "oebpkg1", "oebdoc1");
+		return -1;
+	}
+}
+
+# does s1 contain s2
+contains(s1, s2: string): int
+{
+	if (len s2 > len s1)
+		return 0;
+	n := len s1 - len s2 + 1;
+search:
+	for (i := 0; i < n ; i++) {
+		for (j := 0; j < len s2; j++)
+			if (s1[i + j] != s2[j])
+				continue search;
+		return 1;
+	}
+	return 0;
+}
+	
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+#	sys->print("	%s\n", s);
+	r := tk->cmd(win, s);
+#	sys->print("		-> %s\n", r);
+	if (len r > 0 && r[0] == '!') {
+		sys->fprint(stderr, "error executing '%s': %s\n", s, r);
+		raise "tk error";
+	}
+	return r;
+}
+
+hashfn(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
--- /dev/null
+++ b/appl/ebook/reader.m
@@ -1,0 +1,120 @@
+
+# the public interface to this looks like:
+#	Reader: module {
+#		PATH: con "/dis/ebook/reader.dis";
+#		init: fn(displ: ref Draw->Display);
+#		Datasource: adt {
+#			new:		fn(f: string, win: ref Tk->Toplevel, width: int, evch: string): (ref Datasource, string);
+#			copy:	fn(d: self ref Datasource): ref Datasource;
+#			mark:	fn(d: self ref Datasource): ref Mark;
+#			goto:	fn(d: self ref Datasource, m: ref Mark);
+#			atmark:	fn(d: self ref Datasource, m: ref Mark): int;
+#			next:		fn(d: self ref Datasource, linkch: chan of (string, string, string)): string;
+#	
+#			linestart:	fn(d: self ref Datasource, w: string, y: int): int;
+#			linkoffset:	fn(d: self ref Datasource, w: string, s: string): int;
+#		};
+#	};
+
+
+Reader: module {
+	PATH: con "/dis/ebook/reader.dis";
+	init: fn(displ: ref Draw->Display);
+	Datasource: adt {
+		x:		ref Xml->Parser;
+		t:		ref Text;
+		title:		string;
+		win:		ref Tk->Toplevel;
+		evch:	string;			# tk channel on which to send events
+		tags:		list of ref Xml->Item.Tag;
+		width:	int;
+		filename:	string;
+		error:	string;
+		item:		ref Xml->Item;
+		imark:	ref Xml->Mark;		# mark at start of item
+		fontinfo:	list of ref Fontinfo;
+		styles:	list of ref Stylesheet->Style;
+		stylesheet:	ref Stylesheet->Sheet;
+		linkch:	chan of (string, string, string);		# (linkname, widgetname, internal reference)
+		warningch: chan of (Xml->Locator, string);
+		startmark:	ref Reader->Mark;	# mark start of body for copy()
+		fallbacks:	list of (string, string);
+
+		# public interface consists solely of the following few methods, along with the Mark adt.
+		new:		fn(f: string, fallbacks: list of (string, string), win: ref Tk->Toplevel, width: int, evch: string, warningch: chan of (Xml->Locator, string)): (ref Datasource, string);
+		copy:	fn(d: self ref Datasource): ref Datasource;
+		mark:	fn(d: self ref Datasource): ref Mark;
+		str2mark:	fn(d: self ref Datasource, s: string): ref Mark;
+		mark2str:	fn(d: self ref Datasource, m: ref Mark): string;
+		goto:	fn(d: self ref Datasource, m: ref Mark);
+		atmark:	fn(d: self ref Datasource, m: ref Mark): int;
+		next:		fn(d: self ref Datasource, linkch: chan of (string, string, string)): (Block, string);
+		fileoffset:	fn(d: self ref Datasource): int;
+
+		rectforfileoffset:	fn(t: self ref Datasource, w: string, fileoffset: int): (int, Draw->Rect);
+		fileoffsetnearyoffset:	fn(t: self ref Datasource, w: string, yoffset: int): int;
+		linestart:	fn(d: self ref Datasource, w: string, y: int): int;
+		linkoffset:	fn(d: self ref Datasource, w: string, s: string): int;
+		event:	fn(d: self ref Datasource, e: string): ref Event;
+	};
+
+	Block: adt {
+		w: string;
+		tmargin, bmargin: int;
+	};
+
+	Event: adt {
+		pick {
+		Link =>
+			url:	string;
+		Texthit =>
+			fileoffset:	int;
+		}
+	};
+		
+	Mark: adt {
+		xmark:	ref Xml->Mark;
+		eq:	fn(m1: self ref Mark, m2: ref Mark): int;
+		fileoffset:	fn(m: self ref Mark): int;
+	};
+
+	Text: adt {
+		win:		ref Tk->Toplevel;
+		w:		string;
+		tags:		array of list of (string, int);	# hash of (attrs, tagnum); holds all currently used tags in widget
+		max:		int;			# max tagnum
+		href:		string;
+		margins:		list of Margin;		# margins of enclosing blocks
+		margin:		Margin;			# current margin values
+		outertmargin:	int;
+		outerbmargin:	int;
+		needspace:	int;			# vspace waiting
+		lastwhite:		int;			# did the last text hold trailing whitespace?
+		startofline:	int;
+		style:	ref Stylesheet->Style;
+		fontinfo:	ref Fontinfo;
+		evch:	string;
+
+		new:		fn(win: ref Tk->Toplevel, w: string, width: int, evch: string): ref Text;
+		addtext:	fn(t: self ref Text, text: string, ws1, ws2: int, fileoffset: int);
+		gettag:	fn(t: self ref Text,  s: string): string;
+		linebreak:	fn(t: self ref Text);
+		addmark:	fn(t: self ref Text): string;
+		widgetname:	fn(t: self ref Text, t: int): string;
+		addwidget:	fn(t: self ref Text, w: string, fileoffset: int, invisible: int);
+		startblock:	fn(t: self ref Text);
+		endblock:		fn(t: self ref Text);
+		finalise:	fn(t: self ref Text, addvspace: int);
+		vspace:	fn(t: self ref Text, h: int);
+	};
+
+	Margin: adt {
+		l, r, b, textindent: int;
+	};
+
+	Fontinfo: adt {
+		path:		string;
+		em:		int;
+		ex:		int;
+	};
+};
--- /dev/null
+++ b/appl/ebook/strcache.m
@@ -1,0 +1,6 @@
+Strcache: module {
+	PATH: con "/dis/ebook/./strcache.dis";
+	init:		fn(n: int);
+	cache:	fn(s: string): string;
+	flush:	fn(): string;
+};
--- /dev/null
+++ b/appl/ebook/strmap.b
@@ -1,0 +1,40 @@
+implement Strmap;
+include "strmap.m";
+
+Map.new(a: array of string): ref Map
+{
+	map := ref Map(a, array[31] of list of (string, int));
+	# enter all style names in hash table for reverse lookup
+	s2i := map.s2i;
+	for (i := 0; i < len a; i++) {
+		if (a[i] != nil) {
+			v := hashfn(a[i], len s2i);
+			s2i[v] = (a[i], i) :: s2i[v];
+		}
+	}
+	return map;
+}
+
+Map.s(map: self ref Map, i: int): string
+{
+	return map.i2s[i];
+}
+
+Map.i(map: self ref Map, s: string): int
+{
+	v := hashfn(s, len map.s2i);
+	for (l := map.s2i[v]; l != nil; l = tl l)
+		if ((hd l).t0 == s)
+			return (hd l).t1;
+	return -1;
+}
+
+hashfn(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
--- /dev/null
+++ b/appl/ebook/strmap.m
@@ -1,0 +1,12 @@
+
+Strmap: module {
+	PATH: con "/dis/ebook/strmap.dis";
+	Map: adt {
+		i2s:	array of string;
+		s2i:	array of list of (string, int);
+	
+		new:	fn(a: array of string): ref Map;
+		s:	fn(map: self ref Map, i: int): string;
+		i:	fn(map: self ref Map, s: string): int;
+	};
+};
--- /dev/null
+++ b/appl/ebook/stylesheet.b
@@ -1,0 +1,156 @@
+implement Stylesheet;
+
+include "sys.m";
+	sys: Sys;
+include "stylesheet.m";
+include "strmap.m";
+	strmap: Strmap;
+	Map: import strmap;
+include "cssparser.m";
+	Decl: import CSSparser;
+
+stylemap: ref Map;
+numstyles: int;
+
+RULEHASH:	con 23;
+
+# specificity:
+# bits 0-26	declaration order
+# bit 27		class count	(0 or 1)
+# bit 28		tagname count	(0 or 1)
+# bit 28		id count		(0 or 1)	(inline style only)
+# bit 29-30	origin		(0, 1 or 2 - default, reader, author)	
+# bit 31		importance
+
+# order of these as implied by CSS1 §3.2
+TAG,
+CLASS,
+ID:			con 1 << (iota + 26);
+ORIGINSHIFT:	con 29;
+IMPORTANCE:	con 1<<30;
+
+init(a: array of string)
+{
+	sys = load Sys Sys->PATH;
+	strmap = load Strmap Strmap->PATH;
+	if (strmap == nil) {
+		sys->fprint(sys->fildes(2), "stylesheet: cannot load %s: %r\n", Strmap->PATH);
+		raise "fail:bad module";
+	}
+	stylemap = Map.new(a);
+	numstyles = len a;
+}
+
+Sheet.new(): ref Sheet
+{
+	return ref Sheet(array[RULEHASH] of list of Rule, 0);
+}
+
+Sheet.addrules(sheet: self ref Sheet, rules: list of (string, list of Decl), origin: int)
+{
+	origin <<= ORIGINSHIFT;
+	for (; rules != nil; rules = tl rules) {
+		(sel, decls) := hd rules;
+		(tag, class) := selector(sel);
+		(key, sub) := (tag, "");
+		specificity := sheet.ruleid++;
+		if (tag != nil)
+			specificity |= TAG;
+		if (class != nil) {
+			specificity |= CLASS;
+			(key, sub) = ("." + class, tag);
+		}
+		specificity |= origin;
+
+		attrs: list of (int, int, string);
+		for (; decls != nil; decls = tl decls) {
+			d := mkdecl(hd decls, specificity);
+			if (d.attrid != -1)
+				attrs = d :: attrs;
+		}
+
+		n := hashfn(key, RULEHASH);
+		sheet.rules[n] = (key, sub, attrs) :: sheet.rules[n];
+	}
+}
+
+# assume selector is well-formed, having been previously parsed.
+selector(s: string): (string, string)
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == '.')
+			break;
+	if (i == len s)
+		return (s, nil);
+	return (s[0:i], s[i + 1:]);
+}
+
+
+Sheet.newstyle(sheet: self ref Sheet): ref Style
+{
+	return ref Style(sheet, array[numstyles] of string, array[numstyles] of {* => 0});
+}
+
+adddecl(style: ref Style, d: Ldecl)
+{
+	if (d.specificity > style.spec[d.attrid]) {
+		style.attrs[d.attrid]  = d.val;
+		style.spec[d.attrid] = d.specificity;
+	}
+}
+
+Style.add(style: self ref Style, tag, class: string)
+{
+	rules := style.sheet.rules;
+	if (class != nil) {
+		key := "." + class;
+		v := hashfn(key, RULEHASH);
+		for (r := rules[v]; r != nil; r = tl r)
+			if ((hd r).key == key && ((hd r).sub == nil || (hd r).sub == tag))
+				for (decls := (hd r).decls; decls != nil; decls = tl decls)
+					adddecl(style, hd decls);
+	}
+	v := hashfn(tag, RULEHASH);
+	for (r := rules[v]; r != nil; r = tl r)
+		if ((hd r).key == tag)
+			for (decls := (hd r).decls; decls != nil; decls = tl decls)
+				adddecl(style, hd decls);
+}
+
+# add a specific set of attributes to a style;
+# attrs is list of (attrname, important, val).
+Style.adddecls(style: self ref Style, decls: list of Decl)
+{
+	specificity := ID | (AUTHOR << ORIGINSHIFT);
+	for (; decls != nil; decls = tl decls) {
+		d := mkdecl(hd decls, specificity);
+		if (d.attrid != -1)
+			adddecl(style, d);
+	}
+}
+
+Style.addone(style: self ref Style, attrid: int, origin: int, val: string)
+{
+	# XXX specificity is probably wrong here.
+	adddecl(style, (attrid, origin << ORIGINSHIFT, val));
+}
+
+# convert a declaration from extern (attrname, important, val) form
+# to intern (attrid, specificity, val) form.
+# XXX could warn for unknown attribute here...
+mkdecl(d: Decl, specificity: int): Ldecl
+{
+	if (d.important)
+		specificity |= IMPORTANCE;
+	return (stylemap.i(d.name), specificity, d.val);
+}
+
+hashfn(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
--- /dev/null
+++ b/appl/ebook/stylesheet.m
@@ -1,0 +1,42 @@
+Stylesheet: module {
+	PATH: con "/dis/ebook/stylesheet.dis";
+	DEFAULT, READER, AUTHOR: con iota;
+	init:		fn(stylenames: array of string);
+
+	Style: adt {
+		sheet:	ref Sheet;
+		attrs:		array of string;		# values
+		spec:	array of int;		# specificity
+
+		add:		fn(style: self ref Style, tag, class: string);
+		adddecls: fn(style: self ref Style, decls: list of CSSparser->Decl);
+		addone:	fn(style: self ref Style, attr: int, origin: int, val: string);
+	};
+
+	Sheet: adt {
+		new:		fn(): ref Sheet;
+		addrules:	fn(sheet: self ref Sheet,
+			rules: list of (string, list of CSSparser->Decl), origin: int);
+		newstyle:	fn(sheet: self ref Sheet): ref Style;
+
+		# private from here
+		rules:	array of list of Rule;
+		ruleid:	int;		# sequential ordering of rules
+
+	};
+
+	# private from here
+
+	# declaration as stored internally.
+	Ldecl:	adt {
+		attrid:		int;
+		specificity:	int;
+		val:			string;
+	};
+	
+	Rule: adt {
+		key:		string;		# hash key: "tagname" or ".classname"
+		sub:		string;		# tag name if rule is for a tag-specific class
+		decls:	list of Ldecl;
+	};
+};
--- /dev/null
+++ b/appl/ebook/table.b
@@ -1,0 +1,176 @@
+implement Table;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "table.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+}
+
+newcell(w: string, span: Draw->Point): ref Cell
+{
+	return ref Cell(w, span, (0, 0));
+}
+
+layout(cells: array of array of ref Cell, win: ref Tk->Toplevel, w: string)
+{
+	if (len cells == 0)
+		return;
+	dim := Point(len cells, len cells[0]);
+	for (y := 0; y < dim.y; y++) {
+		for (x := 0; x < dim.x; x++) {
+			cell := cells[x][y];
+			# XXX should take into account cell padding
+			if (cell != nil) {
+				cell.sizereq = getsize(win, cell.w);
+# sys->print("cell %d %d size %s span %s\n", x, y, p2s(cell.sizereq), p2s(cell.span));
+			}
+#  else
+# sys->print("cell %d %d blank\n", x, y);
+		}
+	}
+
+	colwidths := array[dim.x] of {* => 0};
+	# calculate column widths (ignoring multi-column cells)
+	for (x := 0; x < dim.x; x++) {
+		for (y = 0; y < dim.y; y++) {
+			cell := cells[x][y];
+			if (cell != nil && cell.span.x == 1 && cell.sizereq.x > colwidths[x])
+				colwidths[x] = cell.sizereq.x;
+		}
+	}
+
+	# now check that multi-column cells fit in their columns
+	colexpand := array[dim.x] of {* => 1};
+	for (x = 0; x < dim.x; x++) {
+		for (y = 0; y < dim.y; y++) {
+			cell := cells[x][y];
+			if (cell != nil && cell.span.x > 1)
+				expandwidths(x, cell.sizereq.x, cell.span.x, colwidths, colexpand);
+		}
+	}
+	colexpand = nil;
+
+	rowheights := array[dim.y] of {* => 0};
+	# calculate row heights (ignoring multi-row cells)
+	for (y = 0; y < dim.y; y++) {
+		for (x = 0; x < dim.x; x++) {
+			cell := cells[x][y];
+			if (cell != nil && cell.span.y == 1 && cell.sizereq.y > rowheights[y])
+				rowheights[y] = cell.sizereq.y;
+		}
+	}
+
+# 	for (i := 0; i < len colwidths; i++)
+# 		sys->print("colwidth %d -> %d\n", i, colwidths[i]);
+# 	for (i = 0; i < len rowheights; i++)
+# 		sys->print("rowheight %d -> %d\n", i, rowheights[i]);
+
+	rowexpand := array[dim.y] of {* => 1};
+	# now check that multi-row cells fit in their columns
+	for (y = 0; y < dim.y; y++) {
+		for (x = 0; x < dim.x; x++) {
+			cell := cells[x][y];
+			if (cell != nil && cell.span.y > 1)
+				expandwidths(y, cell.sizereq.y, cell.span.y, rowheights, rowexpand);
+		}
+	}
+
+#	if (rowequalise)
+#		equalise(rowheights, dim.y);
+
+#	if (colequalise)
+#		equalise(colwidths, dim.x);
+
+	# calculate total width and height (including cell padding)
+	totsize := Point(0, 0);
+	for (x = 0; x < dim.x; x++)
+		totsize.x += colwidths[x];
+	for (y = 0; y < dim.y; y++)
+		totsize.y += rowheights[y];
+
+	cmd(win, "canvas " + w + " -width " + string totsize.x + " -bg white -height " + string totsize.y);
+	p := Point(0, 0);
+	for (y = 0; y < dim.y; y++) {
+		p.x = 0;
+		for (x = 0; x < dim.x; x++) {
+			cell := cells[x][y];
+			if (cell != nil) {
+				cellsize := Point(0, 0);
+				span := cell.span;
+				for (xx := 0; xx < span.x; xx++)
+					cellsize.x += colwidths[x + xx];
+				for (yy := 0; yy < span.y; yy++)
+					cellsize.y += rowheights[y + yy];
+# sys->print("cell [%d %d] %d %d +[%d %d]\n", x, y, p.x, p.y, cellsize.x, cellsize.y);
+				cmd(win, w + " create window " + p2s(p) +
+					" -anchor nw -window " + cell.w +
+					" -width " + string cellsize.x +
+					" -height " + string cellsize.y);
+			}
+			p.x += colwidths[x];
+		}
+		p.y += rowheights[y];
+	}
+}
+
+expandwidths(x: int, cellwidth, xcells: int, widths: array of int, expand: array of int)
+{
+	# find out total space available for cell
+	share := 0;
+	tot := 0;
+	endx := x + xcells;
+	for (xx := x; xx < endx; xx++) {
+		tot += widths[xx];
+		if (expand[xx])
+			share++;
+	}
+	slack := cellwidth - tot;
+
+	# if enough space, then nothing to do.
+	if (slack <= 0)
+		return;
+
+	# allocate extra space amongst all cols that
+	# want to expand. (if any do, otherwise share it
+	# out between all of them)
+	if (share == 0)
+		share = xcells;
+	for (xx = x; xx < endx; xx++) {
+		m := slack / share;
+		widths[xx] += m;
+		slack -= m;
+		share--;
+	}
+}
+
+getsize(win: ref Tk->Toplevel, w: string): Point
+{
+	bd := 2 * int cmd(win, w + " cget -bd");
+	return Point(int cmd(win, w + " cget -width") + bd,
+			int cmd(win, w + " cget -height") + bd);
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+#	sys->print("%ux	%s\n", win, s);
+	r := tk->cmd(win, s);
+	if (len r > 0 && r[0] == '!') {
+		sys->fprint(sys->fildes(2), "error executing '%s': %s\n", s, r);
+		raise "tk error";
+	}
+	return r;
+}
--- /dev/null
+++ b/appl/ebook/table.m
@@ -1,0 +1,11 @@
+Table: module {
+	PATH: con "/dis/ebook/table.dis";
+	Cell: adt {
+		w: string;
+		span: Draw->Point;
+		sizereq: Draw->Point;
+	};
+	init:	fn();
+	newcell:	fn(w: string, span: Draw->Point): ref Cell;
+	layout: fn(cells: array of array of ref Cell, win: ref Tk->Toplevel, w: string);
+};
--- /dev/null
+++ b/appl/ebook/tst.txt
@@ -1,0 +1,379 @@
+[-xdragon @themail.com-]      [-Urban Chaos- Walkthrough-] [-version 1-] [-
+Playstation Version-]
+
+
+This is the Glitchiest and worst playing Playstation game I have ever played and I'm sorry to say will go unfinished. If the gaming companies would take the time to make sure a game is done right in all phases of devolopment then maybe I'd finish the walkthrough. I've had it with this stupid excuse for a Playstation game and will not make a full walkthrough. Is it also so much to ask to put in save areas throughout one level instead of level to level? Maybe someone will invent a plug in or something for a system that would allow you to save wherever ya damn please in a game. This game is sorry in all aspects. The top 10 reasons not to play this game:
+
+10. The controls suck.
+
+9.  The cameras in close quarters are terrible.
+
+8.  There is no way to save the game within a level. If you die you     must go back and refind all the powerups you got before dying.
+
+7.  The shotgun ammo is few and far between making you revert to your
+    puny 4 shots to kill enemies gun. ( I think a cap gun would kill     the enemies quicker than the pistol in this game.)
+
+6.  The stealth mode is pretty much pointless as sneaking on one enemy     always brings 1 more in right behind you. When this happens then they still see you. Also shooting enemies from behind when they aren't aware you're there still takes the same amount of ammo to kill them.
+
+5.  It's too easy to fall off of high buildings and tall structures
+because the controls suck ( refer to number 10)
+
+
+4.  The A-I is some of the stupidest I have ever seen as the enemies
+    wait most times for you to arrest their pals before attacking.
+
+
+3.  The graphics aren't anything to write home about. With grainy and
+    distant graphics it adds to how lousy this game plays.
+
+
+2.  The clipping isn't terrible it's beyond terrible. The game engine
+    is worse than the Tomb Raider engine was and just doesen't flow
+    smoothly enough with the gameplay. Lets also see some inside levels
+    as in, inside of buildings.
+
+1.  Number 1 reason this game probably sucks is probably because it was
+    rushed and it has entirely tooo many bugs in it. Again I'll ask,
+    isn't that what game testers are being paid to do is tell the
+    devolopers what needs improvement? 
+Top 10 reasons to Play the game:
+
+10. Some of the situations are pretty cool.
+
+
+9.  The shotgun makes a realistic sound that resembles that of a real
+    shotgun.
+
+
+8.  Rescuing people in danger might help you devolop morality. YEah
+    right.
+
+
+7.  I think that's all for the top 10 list.
+
+
+
+Cheats
+
+Cheat mode (PAL version):
+Hold Triangle + Circle + Square + X and press Right during game play to
+refill your energy and unlock all weapons.
+
+Level select (PAL version):
+Press L1 + R1 + Select + Start at the new game screen. All areas will
+now be unlocked at the map screen.
+
+*The above cheats are as far as I know, for PAL versions of Urban Chaos. They may still work for the NTSC versions of the game, but that's not a garuntee.* 
+Now every item and powerup isn't covered in this walkthrough and I'm aware of that. Please don't email me telling me what I missed cause this game is kinda confusing at times. 
+Abbreviations- UCPD: Urban Chaos Police Department.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+I don't know any of the PC commands or key functions for this game, as this walkthrough was written for the Playstation console. I realize the PC version was out first,but my computer isn't compatible for that,so the Playstation version will have to do.
+
+[-This walkthrough is  Copyright € 2000 XDragon walkthrough Guides. This took me some time to complete and is still taking me some time so please don't rip me off by posing as me to post the guide elsewhere. Anyone wishing to post my walkthrough on their website is welcome without the consent of me XDragon. Feel free to post this as you wish, but don't change or modify it in any way, shape, or form. Also before anyone plays this game make sure you are at least 18 as it contains graphic scenes of violence and Language. This may appeal to some children who aren't old enough to play the game but just remember that violence is no laughing matter. If you think it's fun to kill or maim someone in real life you are a sick, perverted individual. The first part of this walkthrough will be the weapons listing in the game. Enjoy the game and walkthrough.-]
+
+[-Weapons-]
+
+Although you begin some missions with a weapon, you should be able to find better weapons fairly easily. You can often disarm an armed assailant during a brawl; when you see the weapon fall to the ground, walk over and press the action button to pick it up. You may also find ammo behind objects. 
+Remember: Even after a weapon is in your inventory, you must select it before you can use it. 
+[-Firearms-]
+
+All guns work in the same way: When a target presents itself, D'arci or Roper will auto-draw their firearm and a targeting icon will appear around the target to denote that you are aimed and ready to fire. You cannot fire until the target has appeared- the delay depends on the type of gun used and the range. If you fire before the target appears , there is a percentage  chance that you may miss your target.
+
+When targeting , D'arci will challenge any foe in her sights. Civilians normally freeze when ordered, and lay down for search when you press the action button. Guilty suspects may run or challenge you. Be warned: The UCPD comes down hard on officers who shoot innocent people. 
+Warning: A firearm can be knocked out of your hand if you are punched or kicked while holding it. 
+Pistol- This is a good all-around weapon. It has good short to mid-range accuracy with a high fire rate.
+
+Shotgun- Devestating at short-range, this weapon has a slow reload time. 
+Assault Rifle- The best weapon for combat, the assault rifle has good fire rate with mid-range distance and excellent accuracy.
+
+Pistol Clip- Pistol Clips give the pistol a full cartridge of  bullets.
+
+Shotgun Shell- Shotgun shells vary in amount when found.
+
+Rifle Clip- Rifle Clips give the assault rifle a full cartridge of bullets.
+
+[-Explosives-]
+
+Hand Grenade- Press the punch button to pull the pin; Press a second time to throw. The grenade has a 6 second fuse. Once it is primed, you must throw it toward the target or be blown to bits.
+
+Time Bomb- General-purpose explosives for all manner of demolition work. You either receive these at the start of a mission, or find them during the level. Press the punch button to place the bomb. You then have 5 seconds to clear the blast radius. These explosives have a devestating short-range blast and must be used as warrented by the mission briefing.
+
+[-Other Weapons/Items-]
+
+If you have a gun or run out of ammo, there are other ways to improve your chances in a street brawl. If you can disarm an opponet who is wielding a bat or a knife, you can pick them up and add them to your inventory. (Just my opinion but wouldn't this be corrupting evidence by using a criminals own weapon?)
+You can also pick up large objects such as crates and drums and throw them. Press the action button while standing close to the object to pick it up. Move into position, then press a second time to throw.
+
+Knife- A shotr-ranged weapon used in hand-to-hand combat. In the hands of an expierienced user it can be deadly on sneak attacks.
+
+Baseball Bat- Another hand-to-hand combat weapon, used to stun or drop opponets.
+
+[-Power-Ups-]
+
+Each mission has power-ups that give an immediate boost when collected. Collect power-ups during each mission to cumulatively enhance D'arci and Roper to their maximum abilities. These are espicially useful in later missions.
+
+Medikit- Increases health by 50%.
+
+Stamina- Restores Full health and increases your total health bar over time.
+
+Reflexes/Accuracy- Accrues skill throughout the game. After a certain time your reaction times and shooting accuracy will improve.
+
+Strength/Damage- Accrues power throughout the game. After a time, your character will be able to take and inflict more damage during combat.
+
+Constitution/Speed- Delivers a short-term boost and long-term gain. Your character's overall speed and maximum running distance improve with each power-
+up.
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+Okay there was a moves section in the book but I decided to skip it for now and go to the main walkthrough for now. 
+                                 [-Oval Circuit-]
+[-Driving Bronze-]
+
+[-Complete 3 Laps within the alloted time.-]
+
+This first area is to test your skills and you can choose from Driving Bronze which is extremely easy since the Cruiser is easy to control. Just make 3 easy laps around the course and you'll have completed the first level.
+
+
+
+                                [-Advanced Circuit-]
+[-Driving Silver-]
+
+[-Complete 2 laps within the alloted time.-]
+
+ Now you'll have driving Silver so do that one next. This one I found to be a bit more tedious, just follow the arrows and turn when they show up on screen. The one part you'll have to back up and I don't understand why but you must do it on both laps. When you finally finish the second lap you must back the car into the space where you first began the race. To me it looks like Eidos made the car phiysics after a Tonka Truck. It looks and drives just like a truck.
+
+                              [-Driving Gold-]
+
+                 [-Complete 2 laps within the alloted time.-]
+
+
+By this time the Driving Gold and Physical appear on the menu so do the Driving Gold next and get it over with.  Here you have a truck to drive around the advanced track and must stop and reverse, very senseless but you must and get around the track before the timer runs             out.  This track is a bit more difficult and I made it around the second try with 6 seconds to spare. You must do a reversal stop twice on each lap here which makes it harder. When you make it around the track and finish you must do the same reversal into the spot where you began to end this level.
+
+
+                            [-Assault Course-]
+[-Training-]
+
+You may now enter the Rat Catch or enter training. I completed the training mission with 4 seconds to spare on the second try. You must try not to hesitate on this course and make sure you use the slide thing towards the middle of the course to get full advantage. Near the end there is a lot of jumps so just try, and make it even though the controls are horribly sloppy.
+ 
+                           [-Combat Center-]
+                           [-Combat Bronze-]
+
+
+ Completing this opens up the RTA, West District. Move now to the combat trainer if you choose and try your best to complete it. First you must simply do a slide kick to take the enemy down. Do this by pressing the O button for running and then when close to opponet press triangle to take him down. The next is a little trickier, you must get in a 3 combination punch to take the opponet down. Try throwing a few practice swings and then when you're ready approach and knock this guy down. What worked for me was I swung 2 punches and missed and the 3rd punch hit and was the charm and knocked the guy down. Next you must do a 3 kick combo with the triangle button. This is the same as the 3 punch combo from before just needing to land the last kick takes this guy down. Next is a guy you must take down and then press the action button when his health meter is blanked out and arrest him before he gets back up. Now you must approach 4 guys and they are on top of a building with a ladder if you're having trouble finding them. Use side kicks to take them down and kick 3 of the 4 guys and then the last guy just use action button and you will win this fight. 
+                        [-Combat Silver-]
+
+[-Use Your Skills to eliminate or arrest all opponet targets. Silver award pits you against waves of increasingly difficult opponets. You must defeat each wave within the time limits.-]
+
+There is 2 guys to get in this first section so take care of them by knocking them down and then using action to throw them and arrest them. Arresting them for this area is your best bet unless you just have something to prove by beating them senseless. When the first 2 guys are down you get 3 more targets 2 guys on the ground and one on a building. Take them down the same way and get in the quick arrests and then 3 more targets and time extensions are added. Once you get these 3 guys arrested another guy is targeted. He tries running away from you so use your run/slide to take him down. Turn around quickly and make the arrest and this area is done.
+
+[-Combat Gold-]
+
+[-Use Your Skills to eliminate or arrest all opponet targets. Silver award pits you against waves of increasingly difficult opponets. You must defeat each wave within the time limits.-]
+
+The basic key to this area is the same as before, just use the action and punch buttons and make an easy arrest. There then will be 4 new very easy targets and one has a gun. The first guy is by himself so take care of him easily. The next 3 one has a gun and the other 2 have weapons. Lucky for you they usually wait for you to arrest one of their buddies before attacking you. Make short work of these guys and make 4 easy arrests and then 5 new targets and a time extension is added. These next 2 guys are very evasive so try and use the gun you can get from the last guy and shoot them. The second guy you probably will have to arrest. The other 2 are as simple as putting the smack down and arresting them quickly to get another time extension and 5 new targets. ( had some troubles with this level so I will finish it in the future.)
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+
+                               [-West District-]
+[-RTA-]
+
+Okay Stern, I guess you're ready to hit the streets now, Call just came in: Reports of an abandoned car holding up traffic on Zarb Avenue. We need you to get over there pronto and return it to the station for impounding. We also got a mugging team working the area, and we need at least one of those guys caught, Thats All. 
+  [-Duties-]
+-Bring the car back to the district station.
+-Arrest at least one mugger.
+
+To search a suspect press and hold the 0 button until the search box bleeps out. Also if you're left without a weapon or are low on health, arrest a pedestrian and search them and you just might get what you're looking for. Don't beat, maim, or kill a pedestrian as this goes against you when on different missions. 
+In the station when you gain control of D'arci you can arrest the girl sitting on the chair kinda funny. Speak with the other girls that look just like the one you arrested and I don't think these other 2 are arrestable. You can beat them up a little and they run away, however. Also if you speak with the officer behind the desk he says he's gonna kick the other guys asses if they give you a hard time. Now leave the police station and speak with the officer standing out here if you like and hop the railing with the 0 button. Run to the sidewalk and speak with a guy who runs when he sees you. Run after him and so a slide kick and take his knife so you'll have a weapon. 
+Most of the people on the sidewalk don't have anything interesting to say so ignore them. Go now to the park where 2 guys are standing around talking and speak with them. Go around the park area over a railing and to an area where a van is parked. Darci will see some guy pissing on a tree and you will need to do a slide kick on him. Arrest him and you will receive a strength increase. Searching this suspect doesen't yield anything useful but go ahead if you want to see for yourself. Turn around now and there is a tree in the middle area here. There is a reflexes powerup here also to get.
+
+Leave the park now to the street and go left. Go right up the next road and get on the left side of the road along the sidewalk. Go left into an alley and go back a small ways and take a stamina powerup. Turn back around and once back to the street go left. Get to the right side of the road now and run up a small ways to an opening on the right. Take the constitution powerup and now all statistics have been increased by 1. Once to the street run left up a small ways and you're back at the park. Run up this road a ways until you see an alley, so run into it and an the dispatcher radios to you that there has been a homicide.
+
+Run up the alley a bit and you will be right at the scene of the homicide. Speak with the people there and then search the dead guy if you want to. Turn around after searching the guy and then go left near a green dumpster is another constitution powerup to take. Go through the alley and then go right up the street and then another right. The dispatcher radios that he has found the mugger and to get on it quick. Now for the sake of keeping this walkthrough in check follow the red arrow to the car crash site first. Once to the taxi car you gotta get in it and take it to the compound yard.
+
+Take it back to the Police station and the guy will say RTA vehicle secured. Now follow the mugger arrow and when you find him use the slide kick to knock him down. Quickly get on him and use the O button to arrest him and this level is complete. I will only cover the areas that are sufficient to complete the levels. There is other stuff to check on in most levels but they aren't necessary aspects of completing the game. Save if you like and then enter the RTA: The Jump.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+[-The Jump-]
+
+Stern I gotta tell you today's been a weird day. Get this- There's a jumper hanging off a building off of Keaton Ave., Get over there and get him down- but Darci, do it subtle. O'Byrne says the guy's as nervous as a turkey at Thanksgiving_ wont let anyone approach him directly. We need this kook taken out of trouble. If the press gets hold of this they'll hang our asses out to dry.
+
+[-Duties-]
+-Bring down the suicide threat.
+
+Watch the cut-scene and then leave the station going right and run up the street a ways. You'll get a call to head to Kleetus Ave. so follow the blue arrow there. Speak with the cop and you'll find out his car was stolen. Follow the green arrow now that is to Brooks tower and climb the ladder to the top. Once up here go right, climbing the small ledges and then jump over a duct sticking out. When you land walk forward to an opening on the left and to the left is a strength, constitution and a another strength powerup. CLimb the ladder to the left now and go to the top. Up here is another strength powerup for you to get and you should be at 4 with strenght. Go up the ladder and then take a right or left to another open section of the roof. Back here is another ladder to climb so climb it and near the ledge is a reflex powerup. Best way to get it is to crouch and crawl to it to avoid falling down.
+
+Jump up on the wire here and slide while remembering to hold the X button until Darci lands. When she lands walk up a small ways and a cut-scene with the loser wannabe fireman ensues. This level is completed and you can now save the game. 
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+[-Gun Hunt-]
+
+An honest citizen has informed us that he saw a man dumping a firearm at the back of the Eagle building. He can't remember the exact spot so you'll need to search the area for the weapon. This weapon may be linked to a recent murder, so it is imperative that you bring it back to the station once found.
+
+[-Duties-]
+-Find weapon and bring it back to the station.
+
+This time you get to use a cruiser so choose from the 2 that are parked here and then follow the green arrow. When you get to the open fenced area you must leave the vehicle and go on pursuit by foot. Speak with everyone in the park. Deeks is in the middle section of the park and looks kinda like Uncle Jesse from the Dukes of Hazzard. Speak with him and then follow the blue arrow. Make sure you get back in your crusier too as it's more convienient to drive then run. Follow the arrow to an alley and enter here and climb the ladder in this area. Once to the top a guy runs after you so use triangle to take him down and take a health pack if you want to. Fall back down into the alley now and go around through it and on the left behind a dumpster is a gun. Take the gun and then run up the alley a ways and the dispatcher says to take out an armed guy. 
+Follow the green arrow and equip the gun you got to kill the bad guy once you find him. Once he's dead you must go back to the district so follow the red arrow there. When you turn around and run up the street another call comes in about another suspect in a diner. Follow the green arrow to the diner and then once you encounter number 2 and take him down by arresting him the other runs in after you. Take care of number 2 by arresting him also and then search both suspects. Head over to the district by following the red arrow. Before entering the district area if you choose speak to the guy standing near the tree on the sidewalk. He'll give you a healthpack and now run towards the district and the level ends.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+[-Trouble In The Park-]
+
+Darci- You gotta get your ass over to Bushy Park- Theres been a whole bunch of gang-banger loitering with intent,residents are getting nervous, they think things are going to explode any minute.
+
+
+[-Duties-]
+-Attend disturbance in park.
+
+Follow the green arrow in a cruiser and on the way there is a crazy guy shooting for no apparent reason so either run him over or get out and fight him and arrest him. Near the park is an alley and some thugs to run over. Allow the cruiser to fight these battles by going forward and in reverse to inflict more damage. Get out when the thugs get very low on energy and fight them until you can make an easier arrest. Drive to the park now and exit the vehicle and a guy says to stay out. Once in the middle area of the park there is 3 enemies to fight and it's extremely easy to defeat and arrest them as when one is down the other 2 wait for you to cuff the first and so on. A cut-scene ensues and a guy runs in behind you. He helps you out and you only need to fight 2 more guys that run in from behind. Another cut-scene ensues and you find out the mystery man is Roper. This ends the level and now you can save if you like.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+                                 [-Baseball Ground-]         [-Gatecrasher-]
+
+Stern, we got a tip off of a meet going down in the grounds of the UC "Black Cats" Stadium, Our switch says that 3 gang leaders are meeting to carve up their turf after the recent gang wars in Bushy Park, We need At least One of these gang bosses brought in and worked over, Darci- The rest,Well,we wont complain if you bust a few heads.. Heh Heh.
+
+[-Duties-]
+-Arrest at least 1 of the gang bosses.
+
+Turn around and head around the corner on the sidewalk and when you get near a phone booth a gangster apporaches from the rear. Beat him up or just arrest him and take the gun ammo to add to yours. Go left into the small alley near the ladder and another gangster approaches from the right so take him out. Another apporaches soon after so take him down or just arrest him. Climb the ladder to the lower roof and then just jump up to the taller roof. Get the gangster here and then search him to receive a pistol and some extra ammo.
+
+Equip the pistol and another gangster bearing a weapon approaches from the left so shoot him. Hop off the building or take the ladder down to the street. Go left up the road and cross to the right side when theres no traffic. In the alley to the right is a health pack if you need it. Turn back around and head out of the alley and another gangster is here so take him down. Cross the street and 2 more gangsters are around to take care of them and collect the ammo and knife off of the 2 thugs. Run right up the road and right in front of the police district 2 more thugs charge at you so take care of them. Head up the road and take out another gangster and then speak with Officer Mills if you like.
+
+Head now to the rear entrance and to the right is a sidewalk so walk down it behind the steel bar fence and take the stamina powerup. A gangster appears behind you so get him and then head out this direction and take a right back on the street. Get the gangster here and he gives you a 50% health medpack. Now head up to the street to another alley and a car is parked here. An old man looking just like Deeks comes out and gets all mad about you being near his car. A gangster runs in from behind so take him out and then the old man. You'll get radioded from dispatch that you made a good arrest.
+
+Now head to a back alley with the truck and a regular cruiser parked near an alley. Walk back in the alley and speak with one of the officers. 3 gangsters approach from behind and the other 2 officers will assist you. One gangster has a shotgun so collect it and then head right and the other 2 officers will follow you. Another gangster approaches and then once he's beat the other 2 guys commend you so now search all visible suspects. Turn around now and in this back alley is a reflexes powerup and a Constitution powerup. Jump up the green dumpster here and climb the ledge here to find some ammo for the gun. Turn left on the same ledge and take a med-pack towards the left and back of the ledge.
+
+Fall off this ledge and the first green dumpster on the left, climb up. Get to this ledge and take the ammo clip here and then fall off this ledge to the ground. Head now in the police vehicle to the rear access near the steps leading up and now you must exit the vehicle. Go up the steps and enter the doorway after taking care of the gangster guarding it. This building is some sort of store and you can arrest the people here just for kicks but nothing important in here. Leave here and hop the fence and another gangster is here so beat him up. Turn right now and a med-pack is ahead and to the left near the fence so get it. Another gangster jumps down so take him out and he has a med-pack too in case you need it. 
+Run up the street now and take your first right into a park and a building is here. Run to the right and to the rear of the building is a constitution powerup. Turn around now after getting that and fight another gangster here, take his health pack if you need to. Head back to the street and to the right a small ways down is another alley with a large box and a small box. Enter the alley and get some ammo and then leave this alley. Run up the street a ways and straight back on the right sidewalk is an alley. Enter here and she will say God whats that smell? Walk to the back left part of this alley and search the decaying body. Get the shotgun and then beat up the gangster that apporaches from behind.
+
+Turn around and leave the alley and get the gangster that runs up to you. Turn right and head up the road to another type of alley that has a big fence at the end. Go to the end and a cut-scene ensues and you need to head to the club. Go to the street and run up the sidewalk until you see some stairs going up. Go up the stairs and speak with the people who are standing outside. A gangster runs up from behind so beat him before he starts injuring innocent people. Enter the club now and hear the crappy disco music. Speak with the guy standing around next to the guy dancing to get a laugh. The dancing dude doesen't speak so head into the main area of the club. 
+If you want the character to dance don't touch the controls for a while and she starts groovin too. Go to the bar and speak with the drunk and then the bartender behind the counter. Go to the balcony near the last dancer girl and talk to Mister Clobby boots and a cut-scene ensues. Leave the club and head up the road past the main entrance to an alley nearby. Run up the steps and kill enemies and then head around to a roof area near a ladder and climb up here. Once on the roof jump to the other roof and one around this ledge is stamina power-up. Now remember that place I mentioned didn't mean anything where you could arrest those people for kicks? Head back to it and speak with Wild Bill so you can enter the alley that is the back entrance to the ball field.
+
+Hop over the railing after exiting the resturant and beat up the gangster which leads Bill to speak to you giving you the go ahead. By this time you'll be radioed by dispatch about an armed assailant. Run towards the white arrow and it takes you seconds to find the armed suspect. I got him from behind when he was shooting at an innocent and snapped his neck. Arrest him and you'll be rewarded by either strength or stamina I can't remember which. Get in the squad car near the alley and then drive to where the back alley near the club was. As soon as you get out a gangster apporaches so either get back in the car and run him over multiple times or just beat him up and make the easy arrest.
+
+Enter the alley now open and behind the second green dumpster along the right wall is some ammo. Climb the dumpster and there is a health pack up here. Turn the red knob up here and then head left along this small ledge and take a right and then jump left and you'll see how friggin awkward the controls for this game are. Once making the jump left jump up the next ledge and to the left is a ladder to climb up. Once up top take out the bad guy and then go right jumping to the lower roof. Over here go around the building and if you see a ladder to the left then ignore it. The DAMN controls are so damn poor and sloppy I fell down to my death naturally. It's damn amazing that it's the littlest things that make a game so unplayable as this. Lousy controls just lousy. Anyways head to the right side and get the grenade down here. 
+Fall down to the bleachers section now and hop the fence and head to the parking lot killing 2 thugs on your way. Get in either the taxi cab or any vehicle and take a left and hit the guy up here. Just go nuts and shoot everyone in sight up here. Head to the infield where 2 vans are parked and this is the final showdown. Note that there is 4 crime bosses here so you'll only need to take down one with fists and arrest and the level will end. Save now if you like and go to.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$          
+                            [-Rio Canal-]
+[-Arms Breaker-]
+
+Stern, we may have got a break. We got confirmed reports that the 'Cats have 2 arms caches somewhere in your sector-that means you have to handle this on your own! Find out where the Hell those weapons are kept before that pain in the ass Gordansky and UCNN broadcast our screw-up all over the state.
+
+[-Duties-]
+-Destroy the 2 caches of weapons.
+
+Beginning the level speak with the Hot Dog vendor and listen to his smart remarks and arrest him for being such a smart ass. Turn around and behind a tree on the right is a health pack so take or remember this spot when needed. Now follow the red marker to where Roper is. Cross the road when safe and behind the building near the water over the railing is 3 gangsters so get them. One has a health pack on him if you get too badly hurt. If you got really injured go to the beginning of the level now and take the health pack that's up here. Turn back around and head for Roper which is the red marker. 
+Equip your pistol and when you make a right to the next street another gangster approaches so shoot him. Follow the red marker to where Roper is and he gives you some explosives. Now go to an alley with a do not enter sign in front of it and back here you'll fight a gangster. In the Pissy hideout behind some couches is a stamina power-up. Leave that alley and take your first right to the street and an immediate right to another alley. Beat up 3 bad guys here and arrest the main bad guy to neutrilize the threat to the citizen being beaten.
+
+Turn right and go up the ladder and take a constitution power-up at the top. Hop back to the ground and leave this alley the same way you entered back to the street. Look for a building nearby with a guy standing around and he'll say Damn for no reason. Head back to an alley and there is a ladder to the left. Climb the ladder and get some ammo and then fall back to the ground. Head to the right side of this area and climb another ladder and receive a strength power-up. Across the street is an alley with an exclamation point sign. Enter here and there is some funny business happening in an alley. A hooker and a horny dude are doing it and they get caught. Arrest the hooker for kicks and then a gangster is around the corner to the right. Beat him up or arrest him and take his gun. Turn right up the next alley and back here a guy is taking a leak.
+
+2 gangsters attack from the rear and peeing man runs away so beat the gangsters up and take ones ammo by searching. Turn around and go up the steps and take some ammo up here. Go left now and hop the fence and to the right is a van you can get in and drive around if you choose. Go back to where you began the level near all the cabs and across the road is an alley on the right. Back in the alley a ways is some more explosives to take. Go back to the alley where you got the van from and right where it was parked is a ladder that's too high to reach on the right. Drive the van under the ladder and then climb on the van and jump and climb the ladder. There is a gangster up here so beat him up and then continue onward. 
+Follow the blue marker now and continue across the metal catwalk till you get to a ladder so fall here. Go left around the building and there is some explosives to get. Go back where you landed and up here is some big crates that in the cut-
+scene before starting this level showed. Use the explosives on these crates to blow this weapon cache. Now head back to where you first began the level near the cabs and eliminate the pick-up team. Head back to where you used the van as a stoop to climb the ladder and then reclimb it again. Instead of going the way where you blew up the last cache, now go left when you reach the top of the building. Fall down off the roof and fight one guy. There is a van back here and just behind it is the last weapons cache so blow it up and complete the level. Make sure you use the explosives to blow up the cache thats the only way to end the level.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+                                [-Southside Beach-]
+                                [-Media Trouble-]
+
+Stern, that dumb-ass reporter Gordansky snooped a bit too close to the Wild Cats, and got herself collared for her trouble. This is urgent, we need you on the scene ASAP- get Gordansky back to the station and teach those damn Wild Cats a lesson while you're at it. I don't think I need to tell you what a crucifying we'd get from UCNN if we fail on this one...!
+
+[-Duties-]
+-Get Gordansky back to the station.
+
+Get the Strenght powerup near the edge of the roof and then use the wire to slide down to a lower building. Turn left now and walk to the edge of the roof. On the other side is a water pump to jump over to so jump there. Once on this side head to the right corner and slide down the wire to the road. Turn right and head straight till you get to the corner where you can make a left. See the red Marker that says Roper on it? Head for Roper now, he's next to a fountain with 2 Basilisks in the center up some stairs. When you're done speaking with him, turn around and beat up the gangster behind you. 
+Turn back around now and run in Ropers direction passing him and hop the fence back here. Cross the road at the crosswalk till you get to the right side of the road. To make sure you're following correctly stop when you cross the road and face left. If I'm correct you should see a street sign with interstate 35 on it to the left and behind you. Turn around now and beat up the thug behind you. Turn back around and head up the sidewalk until you see a mailbox near a break in the guard rails of the road. When you go to turn left here some thugs approach from behind so beat them up. Head up the sidewalk until you can go left and head down this street. 
+When you get near a iron looking fence to the right then hop over the smaller fence where there is some grass and a tree. Head up this way and Darci will swear about them covering the back way too. Run straight up here a small ways and quickly shoot 2 guys with M-16's that come running after you. Go around the tall fance to yout left with barbed wire at the top. Go right and get near the right wall just under the porch of the beach house. Make yourself seen at the next corner and then run back and hide by the wall. Wait for the jerk with the M-16 comes around and then blast him. If you didn't listen to what I had said you might have killed the reporter, Gordansky by accident. 
+Keep this stupid broad on the sidewalks at all times and away from the alleys and such. She's just a flower and is easily killed so don't try running either as she is a slow person too. Follow the Green marker to the police station and the level ends after a cut-scene. The best way to get to the station is just use the sidewalks. Cross the street when you need to but make sure nothing is coming or she'll get hit. If you encounter any guys with guns make sure you stop and have Gordansky covered or it's level over. 
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+                           [-West District-]
+                           [-Urban Shakedown-]
+
+You may have hit the jackpot with that last collar, Stern. The big badass gang boss you brought in squeled like a stuck pig one O'Byrne and Kowalski went to work on him in the cell. Apparently the Wildcats got an itching to take over this part of Union City. We don't know why this area's so important to them, but it seems like the 'Cats are assembling a virtual gang-banger ARMY to sweep across the area, get your butt on the streets and give them all a proper UCPD welcome!
+
+[-Duties-]
+-Eliminate All Wild Cats.
+
+
+Speak with Officer Schwartz and he will help you on this level.
+Equip your gun right away at the start of the level and head right and cross the street and shoot the guy here. A small opening is here with a powerup so take it. Follow the arrow up the road a bit till you encounter 4 bad guys. When you begin shooting 1 guy runs away so take the 3 down quickly. Run towards the last guy and he heads into an alley. There is flashing things on the ground and they are explosives that will kill you. Hop over them and make the last kill and time is extended and new targets are now needed to take down. 
+Go right up the road and forget the gun now cause it slows you down and runs out of ammo quickly. There is a guy on the right side of the street and after that a guy on the left side so take them out first. Collect ammo and then on the right sidewalk is 2 more guys to take down so do that. Across the street still on the right is a shotgun guy so do a slide technique and quickly make the arrest. Follow the white marker down a ways until you can enter an alley to the right. Avoid the bombs on the ground and when you make your first right JUMP. 4 guys to take down now so hurry and beat them up or shoot them.
+
+After they are finished you have more extended time and more targets, respectively. Leave this alley the way you entered and cross the road and climb the fence. Once over it equip a good weapon and take out the 2 shotgun dudes in this area and get some good shotgun ammo. Shoot another guy that jumps the fence near another exit and another to the right near the exit. Turn back around and enter the park again and to the right is another shotgun dude so shoot him. Time is extended and more targets are added. Follow the blue marker a little ways up and take down 2 guys near the park once again.
+
+Re-enter the alley from earlier or the last time your time was extended. There is a ladder here so climb it and go left and fall off the small ledge of roof. Here, there is a fence to climb so climb over it and run back a small ways. Shoot or take down 4 guys here, if you choose to shoot them the shotgun takes 4 shots to bring each one down. Better just to arrest these guys. Once you take them all down your time is once again extended and guess what? More targets what else? Follow the red and blue markers past an alley with a exclamation mark on it. To the right just as you're exiting the alley is 2 more enemies here so take them down or have schwartz do it. Follow the white and green markers now. 
+Now run up the road a bit and another guy pops out so get him. Up a small ways is your final guy in an alley so either shoot him or let Schwartz do the dirty work for you. This ends the level so save if you like.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+                                [-Gangiano-]
+                                [-Auto Destruct-]
+
+Jeez Darci, Terrorists have planted a car bomb in Union City! But those damn gangs have covered the area, and we can't get our bomb disposal teams in there to diffuse it! We need you to use your fighting skills to get into the area and your driving skills to get out! Feel up to it kid?
+
+[-Duties-]
+-Get the car bomb back to the engineers.
+
+Ignore all bad guys here and run like the wind to the first car, just remember to follow the colored markers to find it. Get in the car and when you drive off a van might try and block your way so swerve right to avoid him. Swerve left immediately afterwards as to avoid the guy with a shotgun shooting at the car. Drive back to the police station and park it in the pyloned area next to the truck and then leave the vehicle. Another coded call is patched through so now follow the next marker to the next car. Again you'll want to avoid the guys around by not fighting anyone as time is crucial here.
+
+When you bring this car back you just need to park it in the pyloned area in the road instead. The bombers are spotted now so leave the car if you haven't already and follow the blue marker to the bomber. Once you get to the bombers take them down and a regular enemy may be here too. Fight them and arrest all of them and now you must follow the new blue marker to the Wild Cat den. Search the green dumpster on the left wall in the back of the alley for the video. The rusty one has nothing so head to first set in the alley. Head back to the Sarge which is the white marker and speak with him and this short level comes to an end so save if you choose.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+
+                                [-Botanical Gardens-]
+                                [-Grim Gardens-]
+
+Sgt. Bryant was investigating drugs ring downtown for the last 2 months. 3 days ago, some scumbag from inside the force tipped off the dealers and quickly stashed the narcotics, worth in excess of $3 million, inside the Botanical Gardens. Word has reached us that gang groups are loitering around the gardens, ready to swoop and take them from under our noses. This is personal between us and them. Darci- we want to hit them where it hurts- when you find the stashes, destroy the poison!
+
+[-Duties-]
+-Destroy both narcotics supplies.
+
+Cross the street and speak with Gordansky and she'll tip you off about the location of the explosives. Head up the road near where you began and there is a black van parked near a building. You can get in the van and drive it around if you like. The building is actually a gas station so that will help you find it even better. Before going on a Sunday drive however, Speak with the 2 guys standing on the street corner right where you began the level. After speaking with the last guy a armed suspect that has gunned down a officer is spotted. Now enter the van and follow the white marker to the suspect. Once you gain up to him either run him over to take him down the first time, or leave the vehicle and arrest him with brute force. Don't kill the guy either even though he killed a fellow officer. 
+Head back towards the van but don't ger in it yet, instead walk up the steps where the resturant is and near the downed officer is a reflexes powerup. Head back to the gas station where you got the van and across the street is a tall wall to park near. Park the van over here and jump on it to climb the wall and you're in the area to get the explosives. Once you fall down this area you'll see a big opening to the right but don't go this way yet. Head around the doorway back and to the left and take out the grenade dude and take his grenade. 
+Now in this room is 2 switches on the right wall so go ahead and activets them. Enter the closer gate to the left and up the steps is 4 guys to take down. Further back and left is an elevator but it doesen't work so now turn back around. Now head to the big opening I mentioned not to take earlier. By this time you'll see a white power switch marker so follow it it's white. Run around this area and enter the center stairway and go up it. Turn right at the top of the steps and go down a smaller set of steps. Face left and you'll a switch on a wall next to a ladder so activate it and the life works now.
+
+Climb the ladder now and get a stamina powerup at the top. Follow the explosives marker that is in red and take the explosives that are down a set of steps close to where you are now. Once taken you get a call from the chief about Koots so leave this area at the now main gates that are opened if you activated the switch like I instructed. Get in the van that's just around the left corner while running on the sidewalk. Follow the red marker to the next suspect and run this guy over. Quickly leave the van and make the arrest or if he gets up beat him to submission. Head back to where the gardens are and just for kicks make sure you take the van and enter the gate. Take your first right in the switch room and then past this opened gate and drive up the steps till you can go no further.
+
+Leave the vehicle and go to where the elevator is which is back and to the left. On your way there take out a wildcat and then hop on the elevator. Once up top here run out and from the left a guy runs after you so use the slide kick deal and arrest him. Search him and take his grenade. Back and to the left is another guy so shoot him but don't even think of searching him. The damn game screwed me when I tried walking a teency bit forward when close to this guy and I fell over the fence and friggin died. Head right and climb the ladder and to the right of the ledge here on the roof is another elevator to get on. Once at the bottom of this elevator a gangster with a knife fights you.
+
+Get him whichever way you choose and then back and to the left of this room is the first stash of drugs. Switch to explosives and place it close to the boxes and then run away and you now only have 1 more stash left. CLimb the ladder and then when going left jump on the box sticking up. Land on the steel catwalk and cross it and then jump left and collect the Stamina Powerup here. Go right along the small catwalk and be careful these friggin controls are for the birds as it takes small effort to fall here and have to go back and redo this entire part again. Turn right and then jump to the other roof and go down the ladder. Fight 3 guys here and destroy the other stash of drugs here and the level ends. Save if you like.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+                                [-Southside Beach-]
+                                [-Semtex-]
+
+Just go on the beat, but be careful. The underworld seems to be seething with rumors that the wildcats are planning something big for this section of Southside UC. Oh, we also got a call from your snitch, Deeks. Says he wants to meet you to pass on some "personal info". Hell if I know what he means!
+
+[-Duties-] -Meet Deeks.
+
+Follow the red Deeks marker and meet up with him. Once speaking with him some Wildcats show up so you must fight them. Speak with Deeks after getting rid of the Wildcats and then follow the Red Marker to Officer Molko. Speak with Molko and he says speak with Roper so cross the road and speak with him. Take the shotgun and then follow Roper and he is a very slow runner. The first 2 guys are easy so allow Roper to take them down. Now equip the shotgun and you'll go into an alley with many wildcats so help Roper take them out. Climb the ladder up the fire escape and then turn right and climb another ladder. Turn around once up here and jump to the other side. Go along the walk and head right to an opening in the roof to another roof and jump over here. Take the stamina powerup thats over here.
+
+Turn right now and head up the large steps and take the constitution powerup over here. Jump to the other building now and then hop on the large box. Hop up the other box and take another constitution powerup. Head back down the way you came up and follow the green Semtex marker. Go to a fence with barbed wire and an opening and this is where you'll need to be. Roper radios you so when you hear Roper speak with you you'll know your in the right area. Walk in a small ways and some guy says there aint nothin here for you copper. Fight 5 bad guys here and collect all ammo. Turn around and on a crate a small ways up is a health pack so take it if needed. Turn back around now and head through a gate and be prepared. There is a ton of Wildcats here and they won't always wait for you to arrest their buddies.
+
+Take care of them whichever way you wish and then head around to where there is lots more wildcats. Take them out and then go back and up the ramped area and shoot another guy behind some barrels. About this time you'll find out who's been tipping the Wildcats off. Shoot him when the cut-scene ends and then hop up where he was standing and then to the left another Wildcat hops down so take him out. Run up a small ways and an open area to the right houses a reflex powerup. Shoot another guy by the ladder going down and then go down yourself. Once down here go up the sloped and flush out all opposition surrounding the building in the middle.
+
+The first building you went around has a barrel in the back of it so shoot it and the green marker will dissappear. Shoot more Wildcats and kill em all baby. Head to the furthest building back now and shoot the barrel here and the level ends. Save if you choose.
+
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
+
+                               [-Rio Canal-]
+                               [-Cop Rescue-]
+
+Damn Wildcats have Us against the wall, Officer Stern! They say they've got 2 UCPD officers held, and are claiming they are gonna shoot them if we don't leave them alone. We can't let the uniform down like this Stern! This is a top priority case for a rookie, Stern, but I got faith in you lady- get Johnson and Beck out of there unharmed, or else you'll lose your badge. 
+[-Duties-]
+-Bring both officers back to safety.
+
+Just my opinion but isn't this sarge just a bit harsh saying she'll lose her badge. All the good she's done in the game thusfar and the jerk still calls her a rookie. Obviuosly this would be based on a game or a movie cause otherwise Darci would already have had the sarges job. 
+Turn around when you gain control of Darci and there is an alley with 2 green dumpsters. Enter this alley and back a small ways is a constitution powerup. Turn around once again and another alley just to the right and behind the police vehicles is another alley to enter. Go up the ladder and up here is a reflexes powerup. Run right now when you turn around and jump to the other side of the building and jump up the middle area and get a strength powerup. Hop off the ledge now and go up and left and receive a ammo clip/bullet. Hop off the building safely and run along the sidewalk and shoot some WIldcats. Head up a smaller ways and shoot another cat. Hop the fence and take the reflexes powerup in this fenced in area.
+
+Go to where a long level ago there was a switch that opened a gate so hit the switch and get the powerup here. Now head back to the safe zone and make your way to a resturant and shoot 2 guys inside. Behind the counter is a stamina powerup so take it. Follow the red marker now and make your way through the alleys. This part with the alleys is really tough so be aware of your surroundings and pretty much just blast everyone ya see. Just about every Wildcat is armed with a shotgun and they move in quickly. Best way to rid them is to try and make very quick arrests. Take their ammo and stock up so you can use more ammo later in this level. When you finally find the officer you'll get control of Roper. Roper should have been the main character in this game in my opinion. He's much faster and wayyy tougher. Follow the blue marker to where Beck is.
+
+He will be unconscious in an alley and you must carry him back to the station. When you meet up with more Wildcats on the way to the station use the action button to put Beck down so you can take care of business. Don't go too far away from Beck when fighting cause you need him to complete the level. The best method when carrying Beck is to just stay on the sidewalks and avoid fighting. When you encounter some Wildcats, run in a zig zag pattern to avoid being hit till you make it to the parker police vehicles. You make it here with Roper and now you must make it back with This other guy. It'd make it easier if this guy was unconscious too that way you wouldn't worry about him dying.
+
+Okay go up the alley exit and shoot the 1 land mine to blow them all up or your officer guy gets you both blown to smithereens. 
--- /dev/null
+++ b/appl/ebook/understandingoeb.opf
@@ -1,0 +1,65 @@
+<?xml version="1.0"?>
+<!DOCTYPE package PUBLIC "+//ISBN 0-9673008-1-9//DTD OEB 1.0.1 Package//EN" "http://openebook.org/dtds/oeb-1.0.1/oebpkg101.dtd">
+<package unique-identifier="understandingoebpackage">
+  <metadata>
+    <dc-metadata xmlns:dc="http://purl.org/dc/elements/1.0/" xmlns:oebpackage="http://openebook.org/namespaces/oeb-package/1.0/">
+      <dc:Title>Understanding OEB</dc:Title>
+			<dc:Type>Tutorial</dc:Type>
+			<dc:Identifier id="understandingoebpackage" scheme="url">http://www.globalmentor.com/publishing/understandingoeb/</dc:Identifier>
+			<dc:Creator role="aut">Garret Wilson</dc:Creator>
+			<dc:Creator role="bkp">Mentor Publishing</dc:Creator>
+			<dc:Creator role="spn">GlobalMentor, Inc.</dc:Creator>
+			<dc:Contributor role="aui">David Ornstein</dc:Contributor>
+			<dc:Rights>Copyright &copy; 2000-2001 Garret Wilson. All Rights Reserved.</dc:Rights>
+			<dc:Date>2001-07-11</dc:Date>
+      <dc:Subject>OEB</dc:Subject>
+      <dc:Subject>eBooks</dc:Subject>
+      <dc:Subject>ePublishing</dc:Subject>
+    </dc-metadata>
+	</metadata>
+  
+  <manifest>
+    <!--OEB Documents-->
+    <item id="title" href="title.html" media-type="text/x-oeb1-document" />
+    <item id="toc" href="toc.html" media-type="text/x-oeb1-document" />
+    <item id="foreword" href="foreword.html" media-type="text/x-oeb1-document" />
+    <item id="preface" href="preface.html" media-type="text/x-oeb1-document" />
+    <item id="chapter1" href="chapter1.html" media-type="text/x-oeb1-document" />
+    <item id="chapter2" href="chapter2.html" media-type="text/x-oeb1-document" />
+    <item id="chapter3" href="chapter3.html" media-type="text/x-oeb1-document" />
+    <item id="chapter4" href="chapter4.html" media-type="text/x-oeb1-document" />
+    <item id="chapter5" href="chapter5.html" media-type="text/x-oeb1-document" />
+    <item id="chapter6" href="chapter6.html" media-type="text/x-oeb1-document" />
+    <!--Images-->
+    <item id="OEBActivityDiagram" href="OEBActivityDiagram.png" media-type="image/png" />
+    <item id="OEBClassDiagram" href="OEBClassDiagram.png" media-type="image/png" />
+    <item id="OEBPublicationClassDiagram" href="OEBPublicationClassDiagram.png" media-type="image/png" />
+    <item id="ReadingSystemClassDiagram" href="ReadingSystemClassDiagram.png" media-type="image/png" />
+  </manifest>
+
+  <spine>
+  	<itemref idref="title" />
+  	<itemref idref="toc" />
+  	<itemref idref="foreword" />
+  	<itemref idref="preface" />
+  	<itemref idref="chapter1" />
+  	<itemref idref="chapter2" />
+  	<itemref idref="chapter3" />
+  	<itemref idref="chapter4" />
+  	<itemref idref="chapter5" />
+  	<itemref idref="chapter6" />
+  </spine>
+
+  <guide>
+    <reference type="toc" title="Table of Contents" href="toc.html" />
+    <reference type="foreword" title="Foreword" href="foreword.html" />
+    <reference type="preface" title="Preface" href="preface.html" />
+    <reference type="other.chapter1" title="Chapter 1" href="chapter1.html" />
+    <reference type="other.chapter2" title="Chapter 2" href="chapter2.html" />
+    <reference type="other.chapter3" title="Chapter 3" href="chapter3.html" />
+    <reference type="other.chapter4" title="Chapter 4" href="chapter4.html" />
+		<reference type="other.chapter5" title="Chapter 5" href="chapter5.html" />
+		<reference type="other.chapter6" title="Chapter 6" href="chapter6.html" />
+  </guide>
+  
+</package>
--- /dev/null
+++ b/appl/ebook/units.b
@@ -1,0 +1,146 @@
+implement Units;
+include "sys.m";
+	sys: Sys;
+include "units.m";
+
+Dpi:	con 100;			# pixels per inch on an average display (pinched from tk)
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+# return length in pixels, and string equivalent;
+# makes sure that string equiv is specified in absolute units
+# (not in terms of percentage or font size)
+# XXX give this a proper testing.
+length(s: string, emsize, xsize: int, relative: string): (int, string)
+{
+	(n, units) := units(s);
+	case units {
+	Uem =>
+		px := (n * emsize);
+		return (px / SCALE, n2s(px) + "px");
+	Uex =>
+		px := (n * xsize);
+		return (px / SCALE, n2s(px) + "px");
+	Upx =>
+		return (n / SCALE, s);
+	Uin =>
+		return ((n * Dpi) / SCALE, s);
+	Ucm =>
+		return ((n * Dpi * 100) / (2540 * SCALE), s);
+	Umm =>
+		return ((n * Dpi * 10) / (254 * SCALE), s);
+	Upt =>
+		return ((n * Dpi) / (72 * SCALE), s);
+	Upc =>
+		return ((n * Dpi * 12) / (72 * SCALE), s);
+	Upercent or
+	Unone =>
+		# treat no units as relative factor.
+		# the only place this is used is for "line_height" in css, i believe;
+		# otherwise an unadorned number is not legal.
+		if (relative == nil)
+			return (0, nil);
+		(rn, rs) := length(relative, 0, 0, nil);
+		px := (n * rn) / SCALE;
+		if (units == Upercent)
+			px /= 100;
+		return (px, string px + "px");
+	}
+	return (n / SCALE, s);
+}
+
+# return non-relative for unadorned numbers, as it's not defined so anything's ok.
+isrelative(s: string): int
+{
+	n := len s;
+	if (n < 2)
+		return 0;
+	if (s[n - 1] == '%')
+		return 1;
+	case s[n - 2:] {
+	"em" or
+	"ex" =>
+		return 1;
+	}
+	return 0;
+}
+
+n2s(n: int): string
+{
+	(i, f) := (n / SCALE, n % SCALE);
+	if (f == 0)
+		return string i;
+	if (f < 0)
+		f = -f;
+	return string i + "." + sys->sprint("%.3d", f);
+}
+
+Uem, Uex, Upx, Uin, Ucm, Umm, Upt, Upc, Upercent, Unone: con iota;
+
+SCALE: con 1000;
+
+units(s: string): (int, int)
+{
+	# XXX what should we do on error?
+	if (s == nil)
+		return (0, -1);
+	i := 0;
+
+	# optional leading sign
+	neg := 0;
+	if (s[0] == '-' || s[0] == '+') {
+		neg = s[0] == '-';
+		i++;
+	}
+
+	n := 0;
+	for (; i < len s; i++) {
+		c := s[i];
+		if (c < '0' || c > '9')
+			break;
+		n = (n * 10) + (c - '0');
+	}
+	n *= SCALE;
+	if (i < len s && s[i] == '.') {
+		i++;
+		mul := 100;
+		for (; i < len s; i++) {
+			c := s[i];
+			if (c < '0' || c > '9')
+				break;
+			n += (c - '0') * mul;
+			mul /= 10;
+		}
+	}
+	units := Unone;
+	if (i < len s) {
+		case s[i:] {
+		"em" =>
+			units = Uem;
+		"ex" =>
+			units = Uex;
+		"px" =>
+			units = Upx;
+		"in" =>
+			units = Uin;
+		"cm" =>
+			units = Ucm;
+		"mm" =>
+			units = Umm;
+		"pt" =>
+			units = Upt;
+		"pc" =>
+			units = Upc;
+		"%" =>
+			units = Upercent;
+		* =>
+			return (0, -1);
+		}
+	}
+	if (neg)
+		n = -n;
+	return (n, units);
+}
--- /dev/null
+++ b/appl/ebook/units.m
@@ -1,0 +1,6 @@
+Units: module {
+	PATH: con "/dis/ebook/units.dis";
+	init:	fn();
+	length: fn(s: string, emsize, exsize: int, relative: string): (int, string);
+	isrelative: fn(s: string): int;
+};
--- /dev/null
+++ b/appl/examples/minitel/README
@@ -1,0 +1,209 @@
+Minitel Emulation for Inferno
+
+This directory contains the source of `miniterm', a minitel emulator
+for Inferno.  Miniterm is written in Limbo.  The main components are:
+
+	miniterm.m	- common constants
+	miniterm.b	- terminal emulator, messaging and Minitel `protocol`
+	event.[mb]	- inter-module message format
+	keyb.b		- Minitel keyboard module
+	modem.b		- Minitel modem module
+	screen.b		- Minitel screen module
+	socket.b		- Minitel socket module
+	arg.m 		- basic command line argument handling
+	mdisplay.[mb]	- Videotex display module
+	swkeyb.[mb]	- Minitel aware software keyboard
+
+	fonts.tgz	which expands into:
+
+	fonts/minitel	- external and subfont directory (`bind -b' into /fonts)
+	fonts/minitel/f40x25	- 40 column external font
+	fonts/minitel/14x17
+	fonts/minitel/14x17xoe
+	fonts/minitel/14x17arrow
+	fonts/minitel/f40x25g1	- 40 column semigraphic external font
+	fonts/minitel/vid14x17
+	fonts/minitel/f40x25h	- 40 column double height external font
+	fonts/minitel/14x34
+	fonts/minitel/14x34xoe
+	fonts/minitel/14x34arrow
+	fonts/minitel/f40x25w	- 40 column double width external font
+	fonts/minitel/28x17
+	fonts/minitel/28x17xoe
+	fonts/minitel/28x17arrow
+	fonts/minitel/f40x25s	- 40 column double size external font
+	fonts/minitel/28x34xoe
+	fonts/minitel/28x34arrow
+	fonts/minitel/f80x25	- 80 column external font
+	fonts/minitel/8x12
+	fonts/minitel/8x12xoe
+	fonts/minitel/8x12arrow
+
+The fonts subdirectory should be bound into /fonts:
+	bind -b fonts /fonts
+or the directory fonts/minitel copied to /fonts/minitel before invoking the emulator.
+The names of the external fonts are
+known to the Videotex display module.  Similarly, the files:
+	/dev/modem
+	/dev/modemctl
+are known to the modem module, but you can ignore them if
+(as is almost certain) you are using the Internet-minitel gateway
+and you haven't got appropriate modem hardware anyway.
+
+To build
+	mkdir /usr/inferno/dis/wm/minitel
+	mk install
+
+The code models the structure outlined in the Minitel 1B specification
+provided by France Telecom.  However, much more interpretation was
+required to display the majority of screens currently seen on Minitel.
+Additional information (although sketchy) was found on the Internet by
+searching for Minitel or Videotex and also by examination of the codes
+sent by minitel servers and experimenting with replies.  There must be
+some more up to date information somewhere! 
+
+We don't support downloadable fonts, but correctly filter them out.
+
+The file miniterm.b contains the code for the minitel `terminal' with
+which the other modules communicate.  The keyboard, modem, socket,
+screen and terminal are run as separate threads which communicate by
+calling:
+	send(e: ref Event)
+The clue to the intermodule communication is in Terminal.run which
+does something like:
+	for(;;) {
+		ev =<- t.in =>
+			eva := protocol(ev);
+			while(len eva > 0) {
+				post(eva[0]);
+				eva = eva[1:];
+			}
+		# then deliver any `posted' messages (without blocking)
+	}
+An Event `ev' may typically be an Edata type (say from the modem) or
+an Eproto type for internal interpretation.  In the call:
+	eva := protocol(ev)
+The function protocol() dissects Edata messages to produce an inline
+sequence of Edata and Eproto messages.  The function post() queues
+messages for delivery to the appropriate modules.  For example, data
+from the modem might be destined for the screen and the socket module.
+Messages are queued until they can be delivered. That way the line:
+	ev =<- t.in
+is executed in a timely way and the other modules can be written to
+make blocking writes (via send()) and to service reads when they are
+ready.
+
+In many places in the code lines appear with comments like:
+		if(p.skip < 1 || p.skip > 127)	# 5.0
+These refer to sections of the Minitel specification which explain the
+code.
+
+The mdisplay code provides a Videotex display using Inferno
+primitives.  The screen, keyboard and modem modules interpret data as
+described in the equivalent section of the Minitel specification.  The
+socket module has not been implemented but currently performs a `null'
+function and could easily be added if required.
+
+
+- Namespace
+We always expect the fonts to appear in /fonts and the softmodem
+to appear as /dev/modem and /dev/modemctl.
+
+- Invocation
+If invoked with no argument, miniterm uses the France Telecom
+internet gateway by default (tcp!193.252.252.250!513).
+If the argument starts with `modem' then 
+a direct connection through /dev/modem will be established.
+
+An argument beginning with anything other than `modem' will
+be assumed to be an address suitable for dial(). For example:
+
+	wm/minitel/miniterm tcp!193.252.252.250!513
+
+will connect to the current France Telecom internet server.
+
+For direct connections a modem `init' string and an optional
+phone number can follow the modem prefix, as in:
+
+	wm/minitel/miniterm modem!F3!3615
+
+or
+
+	wm/minitel/miniterm modem!F3!01133836431414
+
+The `F3' is the code which instructs the softmodem to enable V.23
+and needs to be passed when connecting to the FT servers.
+To use pulse dialing instead of tone dialing the phone number
+can be prefixed with a 'P' as in:
+
+	wm/minitel/miniterm modem!F3!P3614
+
+If the parameter specifies a network connection or a direct connection
+with a phone number the software will attempt to connect immediately.
+If Cx/Fin is used to disconnect and then re-connect it will use the
+same IP address for a network connection or prompt for a new
+phone number in the case of a direct connection. When prompting
+for a new number the top row of the screen is used to allow the user
+to edit the last used number. Simple editing is available, and the minitel
+keys do the obvious things.
+
+
+
+** Notes on the 15th December 1998 Release **
+
+- Software keyboard
+A version of the software keyboard which understands some of
+the minitel keyboard mappings is included. For example, hitting 'A' results
+in a capital 'A' on the screen in spite of the Videotex case mapping.
+
+- Minitel function keys
+The minitel keys are displayed on the right hand side of the screen
+in 40 column mode on a network connection 
+and can be swapped to the left hand side by hitting the <- key.
+In direct dial mode and 80 column network mode the keys are
+displayed at the bottom of the screen.
+In network mode they are re-displayed as appropriate on 40 to 80
+column mode changes.
+
+
+Known Omission
+-------------
+- Error Correction (direct dial only)
+There is no screen button to enable error correction in the release.
+If a server asks for error correction it will be enabled. It looks as though
+we need to include a key to enable it. Without it direct dial screens are
+occasionally corrupted.
+
+- Software Keyboard Handling
+We need to add some code to update the software keyboard and
+bring it to the foreground on a mode change.
+
+- Full 80 column support
+I am aware of some screens which don't look correct in 80 column
+mode (and others that do). See `EMAIL' then choose USENET and
+press SUITE a few times. I believe it behaves as specified but as we
+have seen with the 40 column Videotex mode the specification
+is not sufficient to display most of the minitel screens correctly.
+80 column support needs just a little more work.
+It may be, too, that the 80 column font could be made much more
+readable by utilising a few more pixels on the screen now that we
+are able to cover the toolbar.
+
+- Full toolbar integration
+Experimentation will show whether there needs to be more
+integration with the toolbar.
+
+Known Bugs
+----------
+- Softmodem disconnection
+Often, the modem does not hangup correctly.
+
+- Choose `USA' from a network connection
+USA (from a network connection) gives an `iC' in bottom left hand
+corner of screen. Possibly a server issue. Doesn't occur when
+connecting directly. The server is really sending this sequence.
+Both the FT emulator and their explorer plug-in suffer from it too.
+
+
+John Bates
+Vita Nuova Limited
--- /dev/null
+++ b/appl/examples/minitel/event.b
@@ -1,0 +1,19 @@
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+Event.str(ev: self ref Event) : string
+{
+	s := "?";
+	pick e := ev {
+		Edata =>
+			s = sprint("Edata %d = ", len e.data);
+			for(i:=0; i<len e.data; i++)
+				s += hex(int e.data[i], 2) + " ";
+		Equit =>
+			s = "Equit";
+		Eproto =>
+			s = sprint("Eproto %ux (%s)", e.cmd, e.s);
+	}
+	return s;
+}
--- /dev/null
+++ b/appl/examples/minitel/event.m
@@ -1,0 +1,19 @@
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+Event: adt {
+	path: int;					# path for delivery
+	from: int;					# sending module (for reply)
+	pick {
+		Edata =>
+			data: array of byte;
+		Eproto =>
+			cmd: int;
+			s: string;
+			a0, a1, a2: int;		# parameters
+		Equit =>
+	}
+
+	str: 	fn(e: self ref Event) : string;	# convert to readable form
+};
--- /dev/null
+++ b/appl/examples/minitel/keyb.b
@@ -1,0 +1,367 @@
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+# special keyboard operations
+Extend,				# enable cursor and editing keys and control chars
+C0keys,				# cursor keys send BS,HT,LF and VT
+Invert				# case inversion
+	: con 1 << iota;
+
+Keyb: adt {
+	m:		ref Module;			# common attributes
+	in:		chan of ref Event;
+
+	cmd:		chan of string;			# from Tk (keypresses and focus)
+	spec:	int;					# special keyboard extensions
+
+	init:		fn(k: self ref Keyb, toplevel: ref Tk->Toplevel);
+	reset:	fn(k: self ref Keyb);
+	run:		fn(k: self ref Keyb);
+	quit:		fn(k: self ref Keyb);
+	map:		fn(k: self ref Keyb, key:int): array of byte;
+};
+
+Keyb.init(k: self ref Keyb, toplevel: ref Tk->Toplevel)
+{
+	k.in = chan of ref Event;
+	k.cmd = chan of string;
+	tk->namechan(toplevel, k.cmd, "keyb");		# Tk -> keyboard
+	k.reset();
+}
+
+Keyb.reset(k: self ref Keyb)
+{
+	k.m = ref Module(Pmodem|Psocket, 0);
+}
+
+ask(in: chan of string, out: chan of string)
+{
+	keys: string;
+
+	T.mode = Videotex;
+	S.setmode(Videotex);
+#	clear(S);
+	prompt: con "Numéroter: ";
+	number := M.lastdialstr;
+	S.msg(prompt);
+
+Input:
+	for(;;) {
+		n := len prompt + len number;
+		# guard length must be > len prompt
+		if (n > 30)
+			n -= 30;
+		else
+			n = 0;
+		S.msg(prompt + number[n:]);
+		keys = <- in;
+		if (keys == nil)
+			return;
+
+		keys = canoncmd(keys);
+
+		case keys {
+		"connect"  or "send" =>
+			break Input;
+		"correct" =>
+			if(len number > 0)
+				number = number[0: len number -1];
+		"cancel" =>
+			number = "";
+			break Input;
+		"repeat" or "index" or "guide" or "next" or "previous" =>
+			;
+		* =>
+			number += keys;
+		}
+	}
+
+	S.msg(nil);
+	for (;;) alt {
+	out <- = number =>
+		return;
+	keys = <- in =>
+		if (keys == nil)
+			return;
+	}
+}
+
+Keyb.run(k: self ref Keyb)
+{
+	dontask := chan of string;
+	askchan := dontask;
+	askkeys := chan of string;
+Runloop:
+	for(;;){
+		alt {
+		ev := <- k.in =>
+			pick e := ev {
+			Equit =>
+				break Runloop;
+			Eproto =>
+				case e.cmd {
+				Creset =>
+					k.reset();
+				Cproto =>
+					case e.a0 {
+					START =>
+						case e.a1 {
+						LOWERCASE =>
+							k.spec |= Invert;
+						}
+					STOP =>
+						case e.a1 {
+						LOWERCASE =>
+							k.spec &= ~Invert;
+						}
+					}
+				* => break;
+				}
+			}
+		cmd := <- k.cmd =>
+			if(debug['k'] > 0) {
+				fprint(stderr, "Tk %s\n", cmd);
+			}
+			(n, args) := sys->tokenize(cmd, " ");
+			if(n >0)
+				case hd args {
+				"key" =>
+					(key, nil) := toint(hd tl args, 16);
+					if(askchan != dontask) {
+						s := minikey(key);
+						if (s == nil)
+							s[0] = key;
+						askkeys <-= s;
+						break;
+					}
+					keys := k.map(key);
+					if(keys != nil) {
+						send(ref Event.Edata(k.m.path, Mkeyb, keys));
+					}
+				"skey" =>		# minitel key hit (soft key)
+					if(hd tl args == "Exit") {
+						if(askchan != dontask) {
+							askchan = dontask;
+							askkeys <-= nil;
+						}
+						if(T.state == Online || T.state == Connecting) {
+							seq := keyseq("connect");
+							if(seq != nil) {
+								send(ref Event.Edata(k.m.path, Mkeyb, seq));
+								send(ref Event.Edata(k.m.path, Mkeyb, seq));
+							}
+							send(ref Event.Eproto(Pmodem, Mkeyb, Cdisconnect, "", 0,0,0));
+						}
+						send(ref Event.Equit(0, 0));
+						break;
+					} 
+					if(askchan != dontask) {
+						askkeys <-= hd tl args;
+						break;
+					}
+					case hd tl args {
+					"Connect" =>
+						case T.state {
+						Local =>
+							if(M.connect == Network)
+								send(ref Event.Eproto(Pmodem, Mkeyb, Cconnect, "", 0,0,0));
+							else {
+								askchan = chan of string;
+								spawn ask(askkeys, askchan);
+							}
+						Connecting =>
+							send(ref Event.Eproto(Pmodem, Mkeyb, Cdisconnect, "", 0,0,0));
+						Online =>
+							seq := keyseq("connect");
+							if(seq != nil)
+								send(ref Event.Edata(k.m.path, Mkeyb, seq));
+						}
+					* =>
+						seq := keyseq(hd tl args);
+						if(seq != nil)
+							send(ref Event.Edata(k.m.path, Mkeyb, seq));
+					}
+				"click" =>		# fetch a word from the display
+					x := int hd tl args;
+					y := int hd tl tl args;
+					word := disp->GetWord(Point(x, y));
+					if(word != nil) {
+						if (askchan != dontask) {
+							askkeys <- = word;
+							break;
+						}
+						if (T.state == Local) {
+							if (canoncmd(word) == "connect") {
+								if(M.connect == Network)
+									send(ref Event.Eproto(Pmodem, Mkeyb, Cconnect, "", 0,0,0));
+								else {
+									askchan = chan of string;
+									spawn ask(askkeys, askchan);
+								}
+								break;
+							}
+						}
+						seq := keyseq(word);
+						if(seq != nil)
+							send(ref Event.Edata(k.m.path, Mkeyb, seq));
+						else {
+							send(ref Event.Edata(k.m.path, Mkeyb, array of byte word ));
+							send(ref Event.Edata(k.m.path, Mkeyb, keyseq("send")));
+						}
+					}		
+						
+				}
+		dialstr := <-askchan =>
+			askchan = dontask;
+			if(dialstr != nil) {
+				M.dialstr = dialstr;
+				send(ref Event.Eproto(Pmodem, Mkeyb, Cconnect, "", 0,0,0));
+			}
+		}
+	}
+	send(nil);	
+}
+
+
+# Perform mode specific key translation
+# returns nil on invalid keypress,
+Keyb.map(nil: self ref Keyb, key: int): array of byte
+{
+	# hardware to minitel keyboard mapping
+	cmd := minikey(key);
+	if (cmd != nil) {
+		seq := keyseq(cmd);
+		if(seq != nil)
+			return seq;
+	}
+
+	# alphabetic (with case mapping)
+	case T.mode {
+	Videotex =>
+		if(key >= 'A' && key <= 'Z')
+			return array [] of { byte ('a' + (key - 'A'))};
+		if(key >= 'a' && key <= 'z')
+			return array [] of {byte ('A' + (key - 'a'))};
+	Mixed or Ascii =>
+		if(key >= 'A' && key <= 'Z' || key >= 'a' && key <= 'z')
+			return array [] of {byte key};
+	};
+
+	# Numeric
+	if(key >= '0' && key <= '9')
+		return array [] of {byte key};
+
+	# Control-A -> Control-Z, Esc - columns 0 and 1
+	if(key >= 16r00 && key <=16r1f)
+		case T.mode {
+		Videotex =>
+			return nil;
+		Mixed or Ascii =>
+			return array [] of {byte key};
+		}
+
+	# miscellaneous key mapping
+	case key {
+	16r20	=> ;										# space
+	16ra3	=> return array [] of { byte 16r19, byte 16r23 };		# pound
+	'!' or '"' or '#' or '$'
+	or '%' or '&' or '\'' or '(' or ')' 
+	or '*' or '+' or ',' or '-'
+	or '.' or ':' or ';' or '<'
+	or '=' or '>' or '?' or '@'  => ;
+	KF13 =>	# request for error correction - usually Fnct M + C
+		if((M.spec&Ecp) == 0 && T.state == Online && T.connect == Direct) {
+fprint(stderr, "requesting Ecp\n");
+			return array [] of { byte SEP, byte 16r4a };
+		}
+		return nil;
+	*		=> return nil;
+	}
+	return array [] of {byte key};
+}
+
+Keyb.quit(k: self ref Keyb)
+{
+	if(k==nil);
+}
+
+canoncmd(s : string) : string
+{
+	s = tolower(s);
+	case s {
+	"connect" or "cx/fin" or
+	"connexion" or "fin"		=> return "connect";
+	"send" or "envoi" 		=> return "send";
+	"repeat" or "repetition"	=> return "repeat";
+	"index" or "sommaire" or "somm"
+						=> return "index";
+	"guide"				=> return "guide";
+	"correct" or "correction"	=> return "correct";
+	"cancel" or "annulation" or "annul" or "annu"
+						=> return "cancel";
+	"next" or "suite"		=> return "next";
+	"previous" or "retour" or "retou"
+						=> return "previous";
+	}
+	return s;
+}
+
+# map softkey names to the appropriate byte sequences
+keyseq(skey: string): array of byte
+{
+	b2 := 0;
+	asterisk := 0;
+	if(skey == nil || len skey == 0)
+		return nil;
+	if(skey[0] == '*') {
+		asterisk = 1;
+		skey = skey[1:];
+	}
+	skey = canoncmd(skey);
+	case skey {
+	"connect" 	=> b2 = 16r49;
+	"send"  		=> b2 = 16r41;
+	"repeat"		=> b2 = 16r43;
+	"index"		=> b2 = 16r46;
+	"guide"		=> b2 = 16r44;
+	"correct"		=> b2 = 16r47;
+	"cancel"		=> b2 = 16r45;
+	"next"		=> b2 = 16r48;
+	"previous" 	=> b2 = 16r42;
+	}
+	if(b2) {
+		if(asterisk)
+			return array [] of { byte '*', byte SEP, byte b2};
+		else
+			return array [] of { byte SEP, byte b2};
+	} else
+		return nil;
+}
+
+# map hardware or software keyboard presses to minitel functions
+minikey(key: int): string
+{
+	case key {
+	Kup or KupPC =>
+		return"previous";
+	Kdown or KdownPC =>
+		return "next";
+	Kenter =>
+		return "send";
+	Kback =>
+		return "correct";
+	Kesc =>
+		return "cancel";
+	KF1 =>
+		return "guide";
+	KF2 =>
+		return "connect";
+	KF3 =>
+		return "repeat";
+	KF4 =>
+		return "index";
+	* =>
+		return nil;
+	}
+}
--- /dev/null
+++ b/appl/examples/minitel/miniterm.b
@@ -1,0 +1,1190 @@
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+implement Miniterm;
+
+include "sys.m";
+	sys: Sys;
+	print, fprint, sprint, read: import sys;
+include "draw.m";
+	draw: Draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "dial.m";
+	dial: Dial;
+
+include "miniterm.m";
+
+Miniterm: module
+{
+	init:		fn(ctxt: ref Draw->Context, argv: list of string);
+
+};
+
+pgrp: 		int 			= 0;
+debug:		array of int	= array[256] of {* => 0};
+stderr:		ref Sys->FD;
+
+# Minitel terminal identification request - reply sequence
+TERMINALID1 := array [] of {
+	byte SOH,
+	byte 'S', byte 'X', byte '1', byte 'H', byte 'N',
+	byte EOT
+};
+TERMINALID2 := array [] of {
+	byte SOH,
+	byte 'C', byte 'g', byte '1',
+	byte EOT
+};
+
+# Minitel module identifiers
+Mscreen, Mmodem, Mkeyb, Msocket, Nmodule: con iota;
+Pscreen, Pmodem, Pkeyb, Psocket: con (1 << iota);
+Modname := array [Nmodule] of {
+	Mscreen		=> "S",
+	Mmodem		=> "M",
+	Mkeyb 		=> "K",
+	Msocket		=> "C",
+	*			=> "?",
+};
+
+# attributes common to all modules
+Module: adt {
+	path:		int;					# bitset to connected modules
+	disabled:	int;
+};
+
+# A BufChan queues events from the terminal to the modules
+BufChan: adt {
+	path:		int;					# id bit
+	ch:		chan of ref Event;		# set to `in' or `dummy' channel 
+	ev:		ref Event;				# next event to send
+	in:		chan of ref Event;		# real channel for Events to the device
+	q:		array of ref Event;		# subsequent events to send
+};
+
+# holds state information for the minitel `protocol` (chapter 6)
+PState: adt {
+	state:		int;
+	arg:			array of int;		# up to 3 arguments: X,Y,Z
+	nargs:		int;				# expected number of arguments
+	n:			int;				# progress
+	skip:			int;				# transparency; bytes to skip
+};
+PSstart, PSesc, PSarg: con iota;	# states
+
+# Terminal display modes
+Videotex, Mixed, Ascii,
+
+# Connection methods
+Direct, Network,
+
+# Terminal connection states
+Local, Connecting, Online,
+
+# Special features
+Echo
+	: con (1 << iota);
+
+Terminal: adt {
+	in:		chan of ref Event;
+	out:		array of ref BufChan;	# buffered output to the minitel modules
+
+	mode:	int;					# display mode
+	state:	int;					# connection state
+	spec:	int;					# special features
+	connect:	int;					# Direct, or Network
+	toplevel:	ref Tk->Toplevel;
+	cmd:		chan of string;			# from Tk
+	proto:	array of ref PState;		# minitel protocol state
+	netaddr:	string;				# network address to dial
+	buttonsleft: int;				# display buttons on the LHS (40 cols)
+	terminalid: array of byte;			# ENQROM response
+	kbctl:	chan of string;			# softkeyboard control
+	kbmode:	string;				# softkeyboard mode
+
+	init:		fn(t: self ref Terminal, toplevel: ref Tk->Toplevel, connect: int);
+	run:		fn(t: self ref Terminal, done: chan of int);
+	reset:	fn(t: self ref Terminal);
+	quit:		fn(t: self ref Terminal);
+	layout:	fn(t: self ref Terminal, cols: int);
+	setkbmode:	fn(t: self ref Terminal, tmode: int);
+};
+
+include "arg.m";
+include "event.m";
+include "event.b";
+
+include "keyb.b";
+include "modem.b";
+include "socket.b";
+include "screen.b";
+
+K:		ref Keyb;
+M:		ref Modem;
+C:		ref Socket;
+S:		ref Screen;
+T:		ref Terminal;
+Modules:	array of ref Module;
+
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	s: string;
+	netaddr: string = nil;
+
+	sys = load Sys Sys->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	tkclient->init();
+	draw = load Draw Draw->PATH;
+	dial = load Dial Dial->PATH;
+	stderr = sys->fildes(2);
+	pgrp = sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+
+	arg := load Arg Arg->PATH;
+	arg->init(argv);
+	arg->setusage("miniterm [netaddr]");
+	while((c := arg->opt()) != 0){
+		case c {
+		'D' =>
+			s = arg->earg();
+			for(i := 0; i < len s; i++){
+				c = s[i];
+				if(c < len debug)
+					debug[c] += 1;
+			}
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	if(len argv > 0) {
+		netaddr = hd argv;
+		argv = tl argv;
+	}
+
+	if(argv != nil)
+		arg->usage();
+	arg = nil;
+
+	# usage:	miniterm modem[!init[!number]]
+	#	or	miniterm tcp!a.b.c.d
+	connect: int;
+	initstr := dialstr := string nil;
+	if(netaddr == nil)
+		netaddr = "tcp!pdc.minitelfr.com!513";	# gateway
+	(nil, words) := sys->tokenize(netaddr, "!");
+	if(len words == 0) {
+		connect = Direct;
+		words = "modem" :: nil;
+	}
+	if(hd words == "modem") {
+		connect = Direct;
+		words = tl words;
+		if(words != nil) {
+			initstr = hd words;
+			words = tl words;
+			if(words != nil)
+				dialstr = hd words;
+		}
+		if(initstr == "*")
+			initstr = nil;
+		if(dialstr == "*")
+			dialstr = nil;
+	} else {
+		connect = Network;
+		dialstr = netaddr;
+	}
+
+	T = ref Terminal;
+	K = ref Keyb;
+	M = ref Modem;
+	C = ref Socket;
+	S = ref Screen;
+	Modules = array [Nmodule] of {
+		Mscreen	=> S.m,
+		Mmodem	=> M.m,
+		Mkeyb 	=> K.m,
+		Msocket	=> C.m,
+	};
+
+	toplevel := tk->toplevel(ctxt.display, "");
+	inittk(toplevel, connect);
+
+	T.init(toplevel, connect);
+	K.init(toplevel);
+	M.init(connect, initstr, dialstr);
+	C.init();
+	case connect {
+	Direct =>
+		S.init(ctxt, Rect((0,0), (640,425)), Rect((0,0), (640,425)));
+	Network =>
+		S.init(ctxt, Rect((0,0), (596,440)), Rect((0,50), (640,350)));
+	}
+
+	done := chan of int;
+	spawn K.run();
+	spawn M.run();
+	spawn C.run();
+	spawn S.run();
+	spawn T.run(done);
+	<- done;
+
+	# now tidy up
+	K.quit();
+	M.quit();
+	C.quit();
+	S.quit();
+	T.quit();
+}
+
+# the keyboard module handles keypresses and focus
+BTN40x25: con "-height 24 -font {/fonts/lucidasans/unicode.6.font}";
+BTNCTL: con "-width 60 -height 20 -font {/fonts/lucidasans/unicode.7.font}";
+BTNMAIN: con "-width 80 -height 20 -font {/fonts/lucidasans/unicode.7.font}";
+
+tkinitbs := array[] of {
+	"button .cxfin -text {Cx/Fin} -command {send keyb skey Connect}",
+	"button .done -text {Quitter} -command {send keyb skey Exit}",
+	"button .hup -text {Raccr.} -command {send term hangup}",
+	"button .somm -text {Somm.} -command {send keyb skey Index}",
+	"button .guide -text {Guide} -command {send keyb skey Guide}",
+	"button .annul -text {Annul.} -command {send keyb skey Cancel}",
+	"button .corr -text {Corr.} -command {send keyb skey Correct}",
+	"button .retour -text {Retour} -command {send keyb skey Previous}",
+	"button .suite -text {Suite} -command {send keyb skey Next}",
+	"button .repet -text {Répét.} -command {send keyb skey Repeat}",
+	"button .envoi -text {Envoi} -command {send keyb skey Send}",
+	"button .play -text {P} -command {send term play}",
+#	"button .db -text {D} -command {send term debug}" ,
+	"button .kb -text {Clavier} -command {send term keyboard}",
+	"button .move -text {<-} -command {send term buttonsleft} " + BTN40x25,
+};
+
+tkinitdirect := array [] of {
+	". configure -background black -height 480 -width 640",
+
+	".cxfin configure " + BTNCTL,
+	".hup configure " + BTNCTL,
+	".done configure " + BTNCTL,
+	".somm configure " + BTNMAIN,
+	".guide configure " + BTNMAIN,
+	".annul configure " + BTNMAIN,
+	".corr configure " + BTNMAIN,
+	".retour configure " + BTNMAIN,
+	".suite configure " + BTNMAIN,
+	".repet configure " + BTNMAIN,
+	".envoi configure " + BTNMAIN,
+#	".play configure " + BTNCTL,
+#	".db configure " + BTNCTL,
+	".kb configure " + BTNCTL,
+
+	"canvas .c -height 425 -width 640 -background black",
+	"bind .c <Configure> {send term resize}",
+	"bind .c <Key> {send keyb key %K}",
+	"bind .c <FocusIn> {send keyb focusin}",
+	"bind .c <FocusOut> {send keyb focusout}",
+	"bind .c <ButtonRelease> {focus .c; send keyb click %x %y}",
+	"frame .k -height 55 -width 640 -background black",
+	"pack propagate .k no",
+	"frame .klhs -background black",
+	"frame .krhs -background black",
+	"frame .krows -background black",
+	"frame .k1 -background black",
+	"frame .k2 -background black",
+	"pack .cxfin -in .klhs -anchor w -pady 4",
+	"pack .hup -in .klhs -anchor w",
+	"pack .somm .annul .retour .repet -in .k1 -side left -padx 2",
+	"pack .guide .corr .suite .envoi -in .k2 -side left -padx 2",
+	"pack .kb -in .krhs -anchor e -pady 4",
+	"pack .done -in .krhs -anchor e",
+	"pack .k1 -in .krows -pady 4",
+	"pack .k2 -in .krows",
+	"pack .klhs .krows .krhs -in .k -side left -expand 1 -fill x",
+	"pack .c .k",
+	"focus .c",
+	"update",
+};
+
+tkinitip := array [] of {
+	". configure -background black -height 440 -width 640",
+
+	# ip 40x25 mode support
+	"canvas .c40 -height 440 -width 596 -background black",
+	"bind .c40 <Configure> {send term resize}",
+	"bind .c40 <Key> {send keyb key %K}",
+	"bind .c40 <FocusIn> {send keyb focusin}",
+	"bind .c40 <FocusOut> {send keyb focusout}",
+	"bind .c40 <ButtonRelease> {focus .c40; send keyb click %x %y}",
+	"frame .k -height 427 -width 44 -background black",
+	"frame .gap1 -background black",
+	"frame .gap2 -background black",
+	"pack propagate .k no",
+
+	# ip 80x25 mode support
+	"frame .padtop -height 50",
+	"canvas .c80 -height 300 -width 640 -background black",
+	"bind .c80 <Configure> {send term resize}",
+	"bind .c80 <Key> {send keyb key %K}",
+	"bind .c80 <FocusIn> {send keyb focusin}",
+	"bind .c80 <FocusOut> {send keyb focusout}",
+	"bind .c80 <ButtonRelease> {focus .c80; send keyb click %x %y}",
+	"frame .k80 -height 90 -width 640 -background black",
+	"pack propagate .k80 no",
+	"frame .klhs -background black",
+	"frame .krows -background black",
+	"frame .krow1 -background black",
+	"frame .krow2 -background black",
+	"frame .krhs -background black",
+	"pack .krow1 .krow2 -in .krows -pady 2",
+	"pack .klhs -in .k80 -side left",
+	"pack .krows -in .k80 -side left -expand 1",
+	"pack .krhs -in .k80 -side left",
+};
+
+tkip40x25show := array [] of {
+	".cxfin configure " + BTN40x25,
+	".hup configure " + BTN40x25,
+	".done configure " + BTN40x25,
+	".somm configure " + BTN40x25,
+	".guide configure " + BTN40x25,
+	".annul configure " + BTN40x25,
+	".corr configure " + BTN40x25,
+	".retour configure " + BTN40x25,
+	".suite configure " + BTN40x25,
+	".repet configure " + BTN40x25,
+	".envoi configure " + BTN40x25,
+	".play configure " + BTN40x25,
+#	".db configure " + BTN40x25,
+	".kb configure " + BTN40x25,
+	"pack .cxfin -in .k -side top -fill x",
+	"pack .gap1 -in .k -side top -expand 1",
+	"pack .guide .repet .somm .annul .corr .retour .suite .envoi -in .k -side top -fill x",
+	"pack .gap2 -in .k -side top -expand 1",
+	"pack .done .hup .kb .move -in .k -side bottom -pady 2 -fill x",
+#	"pack .db -in .k -side bottom",
+};
+
+tkip40x25lhs := array [] of {
+	".move configure -text {->} -command {send term buttonsright}",
+	"pack .k .c40 -side left",
+	"focus .c40",
+	"update",
+};
+
+tkip40x25rhs := array [] of {
+	".move configure -text {<-} -command {send term buttonsleft}",
+	"pack .c40 .k -side left",
+	"focus .c40",
+	"update",
+};
+
+tkip40x25hide := array [] of {
+	"pack forget .k .c40",
+};
+
+tkip80x25show := array [] of {
+	".cxfin configure " + BTNCTL,
+	".hup configure " + BTNCTL,
+	".done configure " + BTNCTL,
+	".somm configure " + BTNMAIN,
+	".guide configure " + BTNMAIN,
+	".annul configure " + BTNMAIN,
+	".corr configure " + BTNMAIN,
+	".retour configure " + BTNMAIN,
+	".suite configure " + BTNMAIN,
+	".repet configure " + BTNMAIN,
+	".envoi configure " + BTNMAIN,
+#	".play configure " + BTNCTL,
+#	".db configure " + BTNCTL,
+	".kb configure " + BTNCTL,
+
+	"pack .cxfin .hup -in .klhs -anchor w -pady 2",
+	"pack .somm .annul .retour .repet -in .krow1 -side left -padx 2",
+	"pack .guide .corr .suite .envoi -in .krow2 -side left -padx 2",
+	"pack .done .kb -in .krhs -anchor e -pady 2",
+	"pack .padtop .c80 .k80 -side top",
+	"focus .c80",
+	"update",
+};
+
+tkip80x25hide := array [] of {
+	"pack forget .padtop .c80 .k80",
+};
+
+inittk(toplevel: ref Tk->Toplevel, connect: int)
+{
+	tkcmds(toplevel, tkinitbs);
+	if(connect == Direct)
+		tkcmds(toplevel, tkinitdirect);
+	else
+		tkcmds(toplevel, tkinitip);
+}
+
+Terminal.layout(t: self ref Terminal, cols: int)
+{
+	if(t.connect == Direct)
+		return;
+	if(cols == 80) {
+		tkcmds(t.toplevel, tkip40x25hide);
+		tkcmds(t.toplevel, tkip80x25show);
+	} else {
+		tkcmds(t.toplevel, tkip80x25hide);
+		tkcmds(t.toplevel, tkip40x25show);
+		if (t.buttonsleft)
+			tkcmds(t.toplevel, tkip40x25lhs);
+		else
+			tkcmds(t.toplevel, tkip40x25rhs);
+	}
+}
+
+Terminal.init(t: self ref Terminal, toplevel: ref Tk->Toplevel, connect: int)
+{
+	t.in = chan of ref Event;
+	t.proto = array [Nmodule] of {
+		Mscreen	=>	ref PState(PSstart, array [] of {0,0,0}, 0, 0, 0),
+		Mmodem	=>	ref PState(PSstart, array [] of {0,0,0}, 0, 0, 0),
+		Mkeyb	=>	ref PState(PSstart, array [] of {0,0,0}, 0, 0, 0),
+		Msocket	=>	ref PState(PSstart, array [] of {0,0,0}, 0, 0, 0),
+	};
+
+	t.toplevel = toplevel;
+	t.connect = connect;
+	if (t.connect == Direct)
+		t.spec = 0;
+	else
+		t.spec = Echo;
+	t.cmd = chan of string;
+	tk->namechan(t.toplevel, t.cmd, "term");		# Tk -> terminal
+	t.state = Local;
+	t.buttonsleft = 0;
+	t.kbctl = nil;
+	t.kbmode = "minitel";
+	t.reset();
+}
+
+Terminal.reset(t: self ref Terminal)
+{
+	t.mode = Videotex;
+}
+
+Terminal.run(t: self ref Terminal, done: chan of int)
+{
+	t.out = array [Nmodule] of {
+		Mscreen	=> ref BufChan(Pscreen, nil, nil, S.in, array [0] of ref Event),
+		Mmodem	=> ref BufChan(Pmodem, nil, nil, M.in, array [0] of ref Event),
+		Mkeyb 	=> ref BufChan(Pkeyb, nil, nil, K.in, array [0] of ref Event),
+		Msocket	=> ref BufChan(Psocket, nil, nil, C.in, array [0] of ref Event),
+	};
+	modcount := Nmodule;
+	if(debug['P'])
+		post(ref Event.Eproto(Pmodem, 0, Cplay, "play", 0,0,0));
+Evloop:
+	for(;;) {
+		ev: ref Event = nil;
+		post(nil);
+		alt {
+		# recv message from one of the modules
+		ev =<- t.in =>
+			if(ev == nil) {			# modules ack Equit with nil
+				if(--modcount == 0)
+					break Evloop;
+				continue;
+			}
+			pick e := ev {
+			Equit =>		# close modules down
+				post(ref Event.Equit(Pscreen|Pmodem|Pkeyb|Psocket,0));
+				continue;
+			}
+
+			eva := protocol(ev);
+			while(len eva > 0) {
+				post(eva[0]);
+				eva = eva[1:];
+			}
+
+		# send message to `plumbed' modules
+		t.out[Mscreen].ch	<- = t.out[Mscreen].ev	=>
+			t.out[Mscreen].ev = nil;
+		t.out[Mmodem].ch	<- = t.out[Mmodem].ev	=>
+			t.out[Mmodem].ev = nil;
+		t.out[Mkeyb].ch		<- = t.out[Mkeyb].ev		=>
+			t.out[Mkeyb].ev = nil;
+		t.out[Msocket].ch	<- = t.out[Msocket].ev	=>
+			t.out[Msocket].ev = nil;
+
+		# recv message from Tk
+		cmd := <- t.cmd =>
+			(n, word) := sys->tokenize(cmd, " ");
+			if(n >0)
+				case hd word {
+				"resize" =>	;
+				"play" => # for testing only
+					post(ref Event.Eproto(Pmodem, Mmodem, Cplay, "play", 0,0,0));
+				"keyboard" =>
+					if (t.kbctl == nil) {
+						e: string;
+						(e, t.kbctl) = kb(t);
+						if (e != nil)
+							sys->print("cannot start keyboard: %s\n", e);
+					} else
+						t.kbctl <- = "click";
+				"hangup" =>
+					if(T.state == Online || T.state == Connecting)
+						post(ref Event.Eproto(Pmodem, 0, Cdisconnect, "",0,0,0));
+				"buttonsleft" =>
+					tkcmds(t.toplevel, tkip40x25lhs);
+					t.buttonsleft = 1;
+					if(S.image != nil)
+						draw->(S.image.origin)(Point(0,0), Point(44, 0));
+					if (t.kbctl != nil)
+						t.kbctl <- = "fg";
+				"buttonsright" =>
+					tkcmds(t.toplevel, tkip40x25rhs);
+					t.buttonsleft = 0;
+					if(S.image != nil)
+						draw->(S.image.origin)(Point(0,0), Point(0, 0));
+					if (t.kbctl != nil)
+						t.kbctl <- = "fg";
+				"debug" =>
+					debug['s'] ^= 1;
+					debug['m'] ^= 1;
+				}
+		}
+
+	}
+	if (t.kbctl != nil)
+		t.kbctl <- = "quit";
+	t.kbctl = nil;
+	done <-= 0;
+}
+
+kb(t: ref Terminal): (string, chan of string)
+{
+	s := chan of string;
+	spawn dokb(t, s);
+	e := <- s;
+	if (e != nil)
+		return (e, nil);
+	return (nil, s);
+}
+
+Terminal.setkbmode(t: self ref Terminal, tmode: int)
+{
+	case tmode {
+	Videotex =>
+		t.kbmode = "minitel";
+	Mixed or Ascii =>
+		t.kbmode = "standard";
+	}
+	if(t.kbctl != nil) {
+		t.kbctl <-= "mode";
+		t.kbctl <-= "fg";
+	}
+}
+
+include "swkeyb.m";
+dokb(t: ref Terminal, c: chan of string)
+{
+	keyboard := load Keyboard Keyboard->PATH;
+	if (keyboard == nil) {
+		c <- = "cannot load keyboard";
+		return;
+	}
+
+	kbctl := chan of string;
+	(top, m) := tkclient->toplevel(S.ctxt, "", "Keyboard", 0);
+	tk->cmd(top, "pack .Wm_t -fill x");
+	tk->cmd(top, "update");
+	keyboard->chaninit(top, S.ctxt, ".keys", kbctl);
+	tk->cmd(top, "pack .keys");
+
+	kbctl <-= t.kbmode ;
+
+	kbon := 1;
+	c <- = nil;	# all ok, we are now ready to accept commands
+
+	for (;;) alt {
+	mcmd := <- m =>
+		if (mcmd == "exit") {
+			if (kbon) {
+				tk->cmd(top, ". unmap; update");
+				kbon = 0;
+			}
+		} else
+			tkclient->wmctl(top, mcmd);
+	kbcmd := <- c =>
+		case kbcmd {
+		"fg" =>
+			if (kbon)
+				tk->cmd(top, "raise .;update");
+		"click" =>
+			if (kbon) {
+				tk->cmd(top, ". unmap; update");
+				kbon = 0;
+			} else {
+				tk->cmd(top, ". map; raise .");
+				kbon = 1;
+			}
+		"mode" =>
+			kbctl <- = t.kbmode;
+		"quit"	=>
+			kbctl <- = "kill";
+			top = nil;
+			# ensure tkclient not blocked on a send to us (probably overkill!)
+			alt {
+				<- m =>	;
+				* =>	;
+			}
+			return;
+		}
+	}
+}
+
+
+Terminal.quit(nil: self ref Terminal)
+{
+}
+
+# a minitel module sends an event to the terminal for routing
+send(e: ref Event)
+{
+	if(debug['e'] && e != nil)
+		fprint(stderr, "%s: -> %s\n", Modname[e.from], e.str());
+	T.in <- = e;
+}
+
+# post an event to one or more modules
+post(e: ref Event)
+{
+	i,l: int;
+	for(i=0; i<Nmodule; i++) {
+		# `ev' is cleared once sent, reload it from the front of `q'
+		b: ref BufChan = T.out[i];
+		l = len b.q;
+		if(b.ev == nil && l != 0) {
+			b.ev = b.q[0];
+			na := array [l-1] of ref Event;
+			na[0:] = b.q[1:];
+			b.q = na;
+		}
+		if (e != nil) {
+			if(e.path & b.path) {
+				if(debug['e'] > 0) {
+					pick de := e {
+					* =>
+						fprint(stderr, "[%s<-%s] %s\n", Modname[i], Modname[e.from], e.str());
+					}
+				}
+				if(b.ev == nil)		# nothing queued
+					b.ev = e;
+				else {				# enqueue it
+					l = len b.q;
+					na := array [l+1] of ref Event;
+					na[0:] = b.q[0:];
+					na[l] = e;
+					b.q = na;
+				}
+			}
+		}
+		# set a dummy channel if nothing to send
+		if(b.ev == nil)
+			b.ch = chan of ref Event;
+		else
+			b.ch = b.in;
+	}
+}
+
+# run the terminal protocol
+protocol(ev: ref Event): array of ref Event
+{
+	# Introduced by the following sequences, the minitel protocol can be
+	# embedded in any normal data sequence
+	# ESC,0x39,X
+	# ESC,0x3a,X,Y
+	# ESC,0x3b,X,Y,Z
+	# ESC,0x61	- cursor position request
+
+	ea := array [0] of ref Event;	# resulting sequence of Events
+	changed := 0;				# if set, results are found in `ea'
+
+	pick e := ev {
+	Edata =>
+		d0 := 0;				# offset of start of last data sequence
+		p := T.proto[e.from];
+		for(i:=0; i<len e.data; i++) {
+			ch := int e.data[i];
+#			if(debug['p'])
+#				fprint(stderr, "protocol: [%s] %d %ux (%c)\n", Modname[e.from], p.state, ch, ch);
+			if(p.skip > 0) {		# in transparency mode
+				if(ch == 0 && e.from == Mmodem)	# 5.0
+					continue;
+				p.skip--;
+				continue;
+			}
+			case p.state {
+			PSstart =>
+				if(ch == ESC) {
+					p.state = PSesc;
+					changed = 1;
+					if(i > d0)
+						ea = eappend(ea, ref Event.Edata(e.path, e.from, e.data[d0:i]));
+					d0 = i+1;
+				}
+			PSesc =>
+				p.state = PSarg;
+				p.n = 0;
+				d0 = i+1;
+				changed = 1;
+				if(ch >= 16r39 && ch <= 16r3b)	#PRO1,2,3
+					p.nargs = ch - 16r39 + 1;
+				else if(ch == 16r61)			# cursor position request
+					p.nargs = 0;
+				else if(ch == ESC) {
+					ea = eappend(ea, ref Event.Edata(e.path, e.from, array [] of { byte ESC }));
+					p.state = PSesc;
+				} else {
+					# false alarm, restore as data
+					ea = eappend(ea, ref Event.Edata(e.path, e.from, array [] of { byte ESC, byte ch }));
+					p.state = PSstart;
+				}
+			PSarg =>		# expect `nargs' bytes
+				d0 = i+1;
+				changed =1;
+				if(p.n < p.nargs)
+					p.arg[p.n++] = ch;
+				if(p.n == p.nargs) {
+					# got complete protocol sequence
+					pe := proto(e.from, p);
+					if(pe != nil)
+						ea = eappend(ea, pe);
+					p.state = PSstart;
+				}
+			}
+		}
+		if(changed) {			# some interpretation, results in `ea'
+			if(i > d0)
+				ea = eappend(ea, ref Event.Edata(e.path, e.from, e.data[d0:i]));
+			return ea;
+		}
+		ev = e;
+		return array [] of {ev};
+	}
+	return array [] of {ev};
+}
+
+# append to an Event array
+eappend(ea: array of ref Event, e: ref Event): array of ref Event
+{
+	l := len ea;
+	na := array [l+1] of ref Event;
+	na[0:] = ea[0:];
+	na[l] = e;
+	return na;
+}
+
+# act on a received protocol sequence
+# some sequences are handled here by the terminal and result in a posted reply
+# others are returned `inline' as Eproto events with the normal data stream.
+proto(from: int, p: ref PState): ref Event
+{
+	if(debug['p']) {
+		fprint(stderr, "PRO%d: %ux", p.nargs, p.arg[0]);
+		if(p.nargs > 1)
+			fprint(stderr, " %ux", p.arg[1]);
+		if(p.nargs > 2)
+			fprint(stderr, " %ux", p.arg[2]);
+		fprint(stderr, " (%s)\n", Modname[from]);
+	}
+	case p.nargs {
+	0 =>							# cursor position request ESC 0x61
+		reply := array [] of { byte US, byte S.pos.y, byte S.pos.x };
+		post(ref Event.Edata(Pmodem, from, reply));
+	1 =>
+		case p.arg[0] {
+		PROTOCOLSTATUS =>	;
+		ENQROM =>				# identification request
+			post(ref Event.Edata(Pmodem, from, T.terminalid));
+			if(T.terminalid == TERMINALID1)
+				T.terminalid = TERMINALID2;
+		SETRAM1 or SETRAM2 =>	;
+		FUNCTIONINGSTATUS =>		# 11.3
+			PRO2(Pmodem, from, REPFUNCTIONINGSTATUS, osb());
+		CONNECT =>	;
+		DISCONNECT =>
+			return ref Event.Eproto(Pscreen, from, Cscreenoff, "",0,0,0);
+		RESET =>					# reset the minitel terminal
+			all := Pscreen|Pmodem|Pkeyb|Psocket;
+			post(ref Event.Eproto(all, from, Creset, "",0,0,0));	# check
+			T.reset();
+			reply := array [] of { byte SEP, byte 16r5E };
+			post(ref Event.Edata(Pmodem, from, reply));
+		}
+	2 =>
+		case p.arg[0] {
+		TO =>					# request for module status
+			PRO3(Pmodem, from, FROM, p.arg[1], psb(p.arg[1]));
+		NOBROADCAST =>	;
+		BROADCAST =>	;
+		TRANSPARENCY =>			# transparency mode - skip bytes
+			p.skip = p.arg[1];
+			if(p.skip < 1 || p.skip > 127)	# 5.0
+				p.skip = 0;
+			else {
+				reply := array [] of { byte SEP, byte 16r57 };
+				post(ref Event.Edata(Pmodem, from, reply));
+			}
+		KEYBOARDSTATUS =>
+			if(p.arg[1] == RxKeyb)
+				PRO3(Pmodem, from, REPKEYBOARDSTATUS, RxKeyb, kosb());
+		START =>
+			x := osb();
+			if(p.arg[1] == PROCEDURE)
+				x |= 16r04;
+			if(p.arg[1] == SCROLLING)
+				x |= 16r02;
+			PRO2(Pmodem, from, REPFUNCTIONINGSTATUS, x);
+			case p.arg[1] {
+			PROCEDURE =>			# activate error correction procedure
+				sys->print("activate error correction\n");
+				return ref Event.Eproto(Pmodem, from, Cstartecp, "",0,0,0);
+			SCROLLING =>			# set screen to scroll
+				return ref Event.Eproto(Pscreen, from, Cproto, "",START,SCROLLING,0);
+			LOWERCASE =>			# set keyb to invert case
+				return ref Event.Eproto(Pkeyb, from, Cproto, "",START,LOWERCASE,0);
+			}
+		STOP =>
+			x := osb();	
+			if(p.arg[1] == SCROLLING)
+				x &= ~16r02;
+			PRO2(Pmodem, from, REPFUNCTIONINGSTATUS, osb());
+			case p.arg[1] {
+			PROCEDURE =>			# deactivate error correction procedure
+				sys->print("deactivate error correction\n");
+				return ref Event.Eproto(Pmodem, from, Cstopecp, "",0,0,0);
+			SCROLLING =>			# set screen to no scroll
+				return ref Event.Eproto(Pscreen, from, Cproto, "",STOP,SCROLLING,0);
+			LOWERCASE =>			# set keyb to not invert case
+				return ref Event.Eproto(Pkeyb, from, Cproto, "",STOP,LOWERCASE,0);
+			}
+		COPY =>					# copy screen to socket
+			# not implemented
+			;
+		MIXED =>					# change video mode (12.1)
+			case p.arg[1] {
+			MIXED1 =>			# videotex -> mixed
+				reply := array [] of { byte SEP, byte 16r70 };
+				return ref Event.Eproto(Pscreen, from, Cproto, "",MIXED,MIXED1,0);
+			MIXED2 =>			# mixed -> videotex
+				reply := array [] of { byte SEP, byte 16r71 };
+				return ref Event.Eproto(Pscreen, from, Cproto, "",MIXED,MIXED2,0);
+			}
+		ASCII =>					# change video mode (12.2)
+			# TODO
+			;
+		}
+	3 =>
+		case p.arg[0] {
+		OFF or ON =>				# link, unlink, enable, disable
+			modcmd(p.arg[0], p.arg[1], p.arg[2]);
+			PRO3(Pmodem, from, FROM, p.arg[1], psb(TxCode(p.arg[1])));
+		START =>	
+			case p.arg[1] {
+			RxKeyb =>			# keyboard mode
+				case p.arg[2] {
+				ETEN =>			# extended keyboard
+					K.spec |= Extend;
+				C0 =>			# cursor control key coding from col 0
+					K.spec |= C0keys;
+				}
+				PRO3(Pmodem, from, REPKEYBOARDSTATUS, RxKeyb, kosb());
+			}
+		STOP =>					# keyboard mode
+			case p.arg[1] {
+			RxKeyb =>			# keyboard mode
+				case p.arg[2] {
+				ETEN =>			# extended keyboard
+					K.spec &= ~Extend;
+				C0 =>			# cursor control key coding from col 0
+					K.spec &= ~C0keys;
+				}
+				PRO3(Pmodem, from, REPKEYBOARDSTATUS, RxKeyb, kosb());
+			}
+		}
+	}
+	return nil;
+}
+
+# post a PRO3 sequence to all modules on `path'
+PRO3(path, from, x, y, z: int)
+{
+	data := array [] of { byte ESC, byte 16r3b, byte x, byte y, byte z};
+	post(ref Event.Edata(path, from, data));
+}
+
+# post a PRO2 sequence to all modules on `path'
+PRO2(path, from, x, y: int)
+{
+	data := array [] of { byte ESC, byte 16r3a, byte x, byte y};
+	post(ref Event.Edata(path, from, data));
+}
+
+# post a PRO1 sequence to all modules on `path'
+PRO1(path, from, x: int)
+{
+	data := array [] of { byte ESC, byte 16r39, byte x};
+	post(ref Event.Edata(path, from, data));
+}
+
+# make or break links between modules, or enable and disable
+modcmd(cmd, from, targ: int)
+{
+	from = RxTx(from);
+	targ = RxTx(targ);
+	if(from == targ)						# enable or disable module
+		if(cmd == ON)
+			Modules[from].disabled = 0;
+		else
+			Modules[from].disabled = 1;
+	else 								# modify path
+		if(cmd == ON)
+			Modules[from].path |= (1<<targ);
+		else
+			Modules[from].path &= ~(1<<targ);
+}
+
+# determine the path status byte (3.4)
+# if bit 3 of `code' is set then a receive path status byte is returned
+# otherwise a transmit path status byte
+psb(code: int): int
+{
+	this := RxTx(code);
+	b := 16r40;			# bit 6 always set
+	if(code == RxCode(code)) { 	# want a receive path status byte
+		mask := (1<<this);
+		if(Modules[Mscreen].path & mask)
+			b |= 16r01;
+		if(Modules[Mkeyb].path & mask)
+			b |= 16r02;
+		if(Modules[Mmodem].path & mask)
+			b |= 16r04;
+		if(Modules[Msocket].path & mask)
+			b |= 16r08;
+	} else {
+		mod := Modules[this];
+		if(mod.path & Mscreen)
+			b |= 16r01;
+		if(mod.path & Mkeyb)
+			b |= 16r02;
+		if(mod.path & Mmodem)
+			b |= 16r04;
+		if(mod.path & Msocket)
+			b |= 16r08;
+	}
+#	if(parity(b))
+#		b ^= 16r80;
+	return b;
+}
+
+# convert `code' to a receive code by setting bit 3
+RxCode(code: int): int
+{
+	return (code | 16r08)&16rff;
+}
+
+# covert `code' to a send code by clearing bit 3
+TxCode(code: int): int
+{
+	return (code & ~16r08)&16rff;
+}
+
+# return 0 on even parity, 1 otherwise
+# only the bottom 8 bits are considered
+parity(b: int): int
+{
+	bits := 8;
+	p := 0;
+	while(bits-- > 0) {
+		if(b&1)
+			p ^= 1;
+		b >>= 1;
+	}
+	return p;
+}
+
+# convert Rx or Tx code to a module code
+RxTx(code: int): int
+{
+	rv := 0;
+	case code {
+	TxScreen or RxScreen	=> rv = Mscreen;
+	TxKeyb or RxKeyb		=> rv = Mkeyb;
+	TxModem or RxModem	=> rv = Mmodem;
+	TxSocket or RxSocket	=> rv = Msocket;
+	* =>
+		fatal("invalid module code");
+	}
+	return rv;
+}
+
+# generate an operating status byte (11.2)
+osb(): int
+{
+	b := 16r40;
+	if(S.cols == 80)
+		b |= 16r01;
+	if(S.spec & Scroll)
+		b |= 16r02;
+	if(M.spec & Ecp)
+		b |= 16r04;
+	if(K.spec & Invert)
+		b |= 16r08;
+#	if(parity(b))
+#		b ^= 16r80;
+	return b;
+}
+
+# generate a keyboard operating status byte (9.1.2)
+kosb(): int
+{
+	b := 16r40;
+	if(K.spec & Extend)
+		b |= 16r01;
+	if(K.spec & C0keys)
+		b |= 16r04;
+#	if(parity(b))
+#		b ^= 16r80;
+	return b;
+}
+
+hex(v, n: int): string
+{
+	return sprint("%.*ux", n, v);
+}
+
+tostr(ch: int): string
+{
+	str := "";
+	str[0] = ch;
+	return str;
+}
+
+toint(s: string, base: int): (int, string)
+{
+	if(base < 0 || base > 36)
+		return (0, s);
+
+	c := 0;
+	for(i := 0; i < len s; i++) {
+		c = s[i];
+		if(c != ' ' && c != '\t' && c != '\n')
+			break;
+	}
+
+	neg := 0;
+	if(c == '+' || c == '-') {
+		if(c == '-')
+			neg = 1;
+		i++;
+	}
+
+	ok := 0;
+	n := 0;
+	for(; i < len s; i++) {
+		c = s[i];
+		v := base;
+		case c {
+		'a' to 'z' =>
+			v = c - 'a' + 10;
+		'A' to 'Z' =>
+			v = c - 'A' + 10;
+		'0' to '9' =>
+			v = c - '0';
+		}
+		if(v >= base)
+			break;
+		ok = 1;
+		n = n * base + v;
+	}
+
+	if(!ok)
+		return (0, s);
+	if(neg)
+		n = -n;
+	return (n, s[i:]);
+}
+
+tolower(s: string): string
+{
+	r := s;
+	for(i := 0; i < len r; i++) {
+		c := r[i];
+		if(c >= int 'A' && c <= int 'Z')
+			r[i] = r[i] + (int 'a' - int 'A');
+	}
+	return r;
+}
+
+# duplicate `ch' exactly `n' times
+dup(ch, n: int): string
+{
+	str := "";
+	for(i:=0; i<n; i++)
+		str[i] = ch;
+	return str;
+}
+
+fatal(msg: string)
+{
+	fprint(stderr, "fatal: %s\n", msg);
+	exits(msg);
+}
+
+exits(s: string)
+{
+	if(s==nil);
+#	raise "fail: miniterm " + s;
+	fd := sys->open("#p/" + string pgrp + "/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+	exit;
+}
+
+# Minitel byte MSB and LSB classification (p.87)
+MSB(ch: int): int
+{
+	return (ch&16r70)>>4;
+}
+LSB(ch: int): int
+{
+	return (ch&16r0f);
+}
+
+# Minitel character set classification (p.92)
+ISC0(ch: int): int
+{
+	msb := (ch&16r70)>>4;
+	return msb == 0 || msb == 1;
+}
+
+ISC1(ch: int): int
+{
+	return ch >= 16r40 && ch <= 16r5f;
+}
+
+ISG0(ch: int): int
+{
+	# 0x20 (space) and 0x7f (DEL) are not in G0
+	return ch > 16r20 && ch < 16r7f;
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+	n := len cmds;
+	for (ix := 0; ix < n; ix++)
+		tk->cmd(t, cmds[ix]);
+}
--- /dev/null
+++ b/appl/examples/minitel/miniterm.m
@@ -1,0 +1,120 @@
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+# Common control bytes
+NUL:		con 16r00;
+SOH:		con 16r01;
+EOT:		con 16r04;
+ENQ:		con 16r05;
+BEL:		con 16r07;
+BS:		con 16r08;
+HT:		con 16r09;
+LF:		con 16r0a;
+VT:		con 16r0b;
+FF:		con 16r0c;
+CR:		con 16r0d;
+SO:		con 16r0e;
+SI:		con 16r0f;
+DLE:		con 16r10;
+CON:	con 16r11;
+XON:		con 16r11;
+REP:		con 16r12;
+SEP:		con 16r13;
+XOFF:	con 16r13;
+COFF:	con 16r14;
+NACK:	con 16r15;
+SYN:		con 16r16;
+CAN:		con 16r18;
+SS2:		con 16r19;
+SUB:		con 16r1a;
+ESC:		con 16r1b;
+SS3:		con 16r1d;
+RS:		con 16r1e;
+US:		con 16r1f;
+
+SP:		con 16r20;
+DEL:		con 16r7f;
+
+# Minitel Protocol - some are duplicated (chapter 6)
+ASCII:			con 16r31;
+MIXED:			con 16r32;
+ETEN:			con 16r41;
+C0:				con 16r43;
+SCROLLING:		con 16r43;
+PROCEDURE:		con 16r44;
+LOWERCASE:		con 16r45;
+OFF:				con 16r60;
+ON:				con 16r61;
+TO:				con 16r62;
+FROM:			con 16r63;
+NOBROADCAST:	con 16r64;
+BROADCAST:		con 16r65;
+NONRETURN:		con 16r64;
+RETURN:			con 16r65;
+TRANSPARENCY:	con 16r66;
+DISCONNECT:		con 16r67;
+CONNECT:		con 16r68;
+START:			con 16r69;
+STOP:			con 16r6a;
+KEYBOARDSTATUS:	con 16r72;
+REPKEYBOARDSTATUS:	con 16r73;
+FUNCTIONINGSTATUS:	con 16r72;
+REPFUNCTIONINGSTATUS:	con 16r73;
+EXCHANGERATESTATUS:	con 16r74;
+REPEXCHANGERATESTATUS:	con 16r75;
+PROTOCOLSTATUS:	con 16r76;
+REPPROTOCOLSTATUS: 	con 16r77;
+SETRAM1:			con 16r78;
+SETRAM2:			con 16r79;
+ENQROM:			con 16r7b;
+COPY:			con 16r7c;
+ASCII1:			con 16r7d;
+MIXED1:			con 16r7d;
+MIXED2:			con 16r7e;
+RESET:			con 16r7f;
+
+# Module send and receive codes (chapter 6)
+TxScreen:			con 16r50;
+TxKeyb:			con 16r51;
+TxModem:		con 16r52;
+TxSocket:			con 16r53;
+RxScreen:			con 16r58;
+RxKeyb:			con 16r59;
+RxModem:		con 16r5a;
+RxSocket:			con 16r5b;
+
+# Internal Event.Eproto command constants
+Cplay,			# for testing
+Cconnect,			# e.s contains the address to dial
+Cdisconnect,		# 
+Crequestecp,		# ask server to start ecp
+Creset,			# reset module
+Cstartecp,			# start error correction
+Cstopecp,			# stop error correction
+Cproto,			# minitel protocol
+Ccursor,			# update screen cursor
+Cindicators,		# update row 0 indicators
+
+# softmodem bug: Cscreenoff, Cscreenon
+Cscreenoff,		# screen: ignore data
+Cscreenon,		# screen: don't ignore data
+
+Clast
+	: con iota;
+
+# Special keys - hardware returned byte
+KupPC:		con	16r0203;		# pc emu
+KdownPC:		con	16r0204;		# pc emu
+Kup:		con	16rE012;
+Kdown:	con	16rE013;
+Kenter:	con	16r000a;
+Kback:	con	16r0008;
+Kesc:	con	16r001b;
+KF1:		con	16rE041;
+KF2:		con	16rE042;
+KF3:		con	16rE043;
+KF4:		con	16rE044;
+KF13:	con	16rE04D;
+
+
--- /dev/null
+++ b/appl/examples/minitel/mkfile
@@ -1,0 +1,24 @@
+<../../../mkconfig
+
+TARG=\
+	mdisplay.dis\
+	miniterm.dis\
+	swkeyb.dis\
+
+MODULES=\
+	mdisplay.m\
+	miniterm.m\
+	event.m\
+	swkeyb.m\
+
+SYSMODULES=\
+	arg.m\
+	sys.m\
+	debug.m\
+	draw.m\
+	tk.m\
+	wmlib.m\
+
+DISBIN=$ROOT/dis/wm/minitel
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/examples/minitel/modem.b
@@ -1,0 +1,620 @@
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+#modem states for direct connection
+MSstart, MSdialing, MSconnected, MSdisconnecting,
+
+# special features
+Ecp								# error correction
+	: con (1 << iota);
+
+Ecplen: con 17;	# error correction block length: data[15], crc, validation (=0)
+
+Modem: adt {
+	m:		ref Module;			# common attributes
+	in:		chan of ref Event;
+
+	connect:	int;					# None, Direct, Network
+	state:	int;					# modem dialing state
+	saved:	string;				# response, so far (direct dial)
+	initstr:	string;				# softmodem init string (direct dial)
+	dialstr:	string;				# softmodem dial string (direct dial)
+	lastdialstr:	string;
+
+	spec:	int;					# special features
+	fd:		ref Sys->FD;			# modem data file, if != nil
+	cfd:		ref Sys->FD;			# modem ctl file, if != nil (direct dial only)
+	devpath:	string;				# path to the modem;
+	avail:	array of byte;			# already read
+	rd:		chan of array of byte;	# reader -> rd
+	pid:		int;					# reader pid if != 0
+
+	seq:		int;					# ECP block sequence number
+	waitsyn:	int;					# awaiting restart SYN SYN ... sequence
+	errforce:	int;
+	addparity:	int;					# must add parity to outgoing data
+
+	init:		fn(m: self ref Modem, connect: int, initstr, dialstr: string);
+	reset:	fn(m: self ref Modem);
+	run:		fn(m: self ref Modem);
+	quit:		fn(m: self ref Modem);
+	runstate:	fn(m: self ref Modem, data: array of byte);
+	write:	fn(m: self ref Modem, data: array of byte):int;	# to network
+	reader:	fn(m: self ref Modem, pidc: chan of int);
+};
+
+partab: array of byte;
+
+dump(a: array of byte, n: int): string
+{
+	s := sys->sprint("[%d]", n);
+	for(i := 0; i < n; i++)
+		s += sys->sprint(" %.2x", int a[i]);
+	return s;
+}
+
+Modem.init(m: self ref Modem, connect: int, initstr, dialstr: string)
+{
+	partab = array[128] of byte;
+	for(c := 0; c < 128; c++)
+		if(parity(c))
+			partab[c] = byte (c | 16r80);
+		else
+			partab[c] = byte c;
+	m.in = chan of ref Event;
+	m.connect = connect;
+	m.state = MSstart;
+	m.initstr = initstr;
+	m.dialstr = dialstr;
+	m.pid = 0;
+	m.spec = 0;
+	m.seq = 0;
+	m.waitsyn = 0;
+	m.errforce = 0;
+	m.addparity = 0;
+	m.avail = array[0] of byte;
+	m.rd = chan of array of byte;
+	m.reset();
+}
+
+Modem.reset(m: self ref Modem)
+{
+	m.m = ref Module(Pscreen, 0);
+}
+
+Modem.run(m: self ref Modem)
+{
+	if(m.dialstr != nil)
+		send(ref Event.Eproto(Pmodem, Mmodem, Cconnect, "", 0,0,0));
+Runloop:
+	for(;;){
+		alt {
+		ev := <- m.in =>
+			pick e := ev {
+			Equit =>
+				break Runloop;
+			Edata =>
+				if(debug['m'] > 0)
+					fprint(stderr, "Modem <- %s\n", e.str());
+				m.write(e.data);
+				if(T.state == Local || T.spec & Echo) {	# loopback
+					if(e.from == Mkeyb) {
+						send(ref Event.Eproto(Pscreen, Mkeyb, Ccursor, "", 0,0,0));
+						send(ref Event.Edata(Pscreen, Mkeyb, e.data));
+					}
+				}
+			Eproto =>
+				case e.cmd {
+				Creset =>
+					m.reset();
+				Cconnect =>
+					if(m.pid != 0)
+						break;
+					m.addparity = 1;
+					T.state = Connecting;
+					send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
+
+					case m.connect {
+					Direct =>
+						S.msg("Appel "+m.dialstr+" ...");
+						dev := "/dev/modem";
+						if(openmodem(m, dev) < 0) {
+							S.msg("Modem non prêt");
+							T.state = Local;
+							send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
+							break;
+						}
+						m.state = MSdialing;
+						m.saved = "";
+						dialout(m);
+						T.terminalid = TERMINALID2;
+					Network =>	
+						S.msg("Connexion au serveur ...");
+						if(debug['m'] > 0 || debug['M'] > 0)
+							sys->print("dial(%s)\n", m.dialstr);
+						cx := dial->dial(m.dialstr, "");
+						if (cx == nil){
+							S.msg("Echec de la connexion");
+							T.state = Local;
+							send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
+							if(debug['m'] > 0)
+								sys->print("can't dial %s: %r\n", m.dialstr);
+							break;
+						}
+						m.fd = cx.dfd;
+						m.cfd = cx.cfd;
+						if(len m.dialstr >= 3 && m.dialstr[0:3] == "tcp")
+							m.addparity = 0;	# Internet gateway apparently doesn't require parity
+						if(m.fd != nil) {
+							S.msg(nil);
+							m.state = MSconnected;
+							T.state = Online;
+							send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
+						}
+						T.terminalid = TERMINALID1;
+					}
+					if(m.fd != nil) {
+						pidc := chan of int;
+						spawn m.reader(pidc);
+						m.pid = <-pidc;
+					}
+				Cdisconnect =>
+					if(m.pid != 0) {
+						S.msg("Déconnexion ...");
+						m.state = MSdisconnecting;
+					}
+					if(m.connect == Direct)
+						hangup(m);
+					else
+						nethangup(m);
+				Cplay =>			# for testing
+					case e.s {
+					"play" =>
+						replay(m);
+					}
+				Crequestecp =>
+					if(m.spec & Ecp){	# for testing: if already active, force an error
+						m.errforce = 1;
+						break;
+					}
+					m.write(array[] of {byte SEP, byte 16r4A});
+sys->print("sending request for ecp\n");
+				Cstartecp =>
+					m.spec |= Ecp;
+					m.seq = 0;	# not in spec
+					m.waitsyn = 0;	# not in spec
+				Cstopecp =>
+					m.spec &= ~Ecp;
+				* => break;
+				}
+			}
+		b := <- m.rd =>
+			if(debug['m'] > 0){
+				fprint(stderr, "Modem -> %s\n", dump(b,len b));
+			}
+			if(b == nil) {
+				m.pid = 0;
+				case m.state {
+				MSdialing =>
+					S.msg("Echec appel");
+				MSdisconnecting =>
+					S.msg(nil);
+				}
+				m.state = MSstart;
+				T.state = Local;
+				send(ref Event.Eproto(Pscreen, Mmodem, Cscreenon, "",0,0,0));
+				send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
+				break;
+			}
+			m.runstate(b);
+		}
+	}
+	if(m.pid != 0)
+		kill(m.pid);
+	send(nil);	
+}
+
+Modem.quit(nil: self ref Modem)
+{
+}
+
+Modem.runstate(m: self ref Modem, data: array of byte)
+{
+	if(debug['m']>0)
+		sys->print("runstate %d %s\n", m.state, dump(data, len data));
+	case m.state {
+	MSstart =>	;
+	MSdialing =>
+		for(i:=0; i<len data; i++) {
+			ch := int data[i];
+			if(ch != '\n' && ch != '\r') {
+				m.saved[len m.saved] = ch;
+				continue;
+			}
+			(code, str) := seenreply(m.saved);
+			case code {
+			Noise or Ok =>	;
+			Success =>
+				S.msg(nil);
+				m.state = MSconnected;
+				T.state = Online;
+				send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
+			Failure =>
+				hangup(m);
+				S.msg(str);
+				m.state = MSstart;
+				T.state = Local;
+				send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0));
+			}
+			m.saved = "";
+		}
+	MSconnected =>
+		send(ref Event.Edata(m.m.path, Mmodem, data));
+	MSdisconnecting =>	;
+	}
+}
+
+Modem.write(m: self ref Modem, data: array of byte): int
+{
+	if(m.fd == nil)
+		return -1;
+	if(len data == 0)
+		return 0;
+	if(m.addparity){
+		# unfortunately must copy data to add parity for direct modem connection
+		pa := array[len data] of byte;
+		for(i := 0; i<len data; i++)
+			pa[i] = partab[int data[i] & 16r7F];
+		data = pa;
+	}
+	if(debug['m']>0)
+		sys->print("WRITE %s\n", dump(data, len data));
+	return sys->write(m.fd, data, len data);
+}
+
+#
+# minitel error correction protocol
+#
+# SYN, SYN, block number	start of retransmission
+# NUL ignored
+# DLE escapes {DLE, SYN, NACK, NUL}
+# NACK, block	restart request
+#
+
+crctab: array of int;
+Crcpoly: con 16r9;	# crc7 = x^7+x^3+1
+
+# precalculate the CRC7 remainder for all bytes
+
+mktabs()
+{
+	crctab = array[256] of int;
+	for(c := 0; c < 256; c++){
+		v := c;
+		crc := 0;
+		for(i := 0; i < 8; i++){
+			crc <<= 1;		# align remainder's MSB with value's
+			if((v^crc) & 16r80)
+				crc ^= Crcpoly;
+			v <<= 1;
+		}
+		crctab[c] = (crc<<1) & 16rFE;	# pre-align the result to save <<1 later
+	}
+}
+
+# return the index of the first non-NUL character (the start of a block)
+
+nextblock(a: array of byte, i: int, n: int): int
+{
+	for(; i < n; i++)
+		if(a[i] != byte NUL)
+			break;
+	return i;
+}
+
+# return the data in the ecp block in a[0:Ecplen] (return nil for bad format)
+
+decode(a: array of byte): array of byte
+{
+	if(debug['M']>0)
+		sys->print("DECODE: %s\n", dump(a, Ecplen));
+	badpar := 0;
+	oldcrc := int a[Ecplen-2];
+	crc := 0;
+	op := 0;
+	dle := 0;
+	for(i:=0; i<Ecplen-2; i++){	# first byte is high-order byte of polynomial (MSB first)
+		c := int a[i];
+		nc := c & 16r7F;	# strip parity
+		if((c^int partab[nc]) & 16r80)
+			badpar++;
+		crc = crctab[crc ^ c];
+		# collapse DLE sequences
+		if(!dle){
+			if(nc == DLE && i+1 < Ecplen-2){
+				dle = 1;
+				continue;
+			}
+			if(nc == NUL)
+				continue;	# strip non-escaped NULs
+		}
+		dle = 0;
+		a[op++] = byte nc;
+	}
+	if(badpar){
+		if(debug['E'] > 0)
+			sys->print("bad parity\n");
+		return nil;	
+	}
+	crc = (crc>>1)&16r7F;
+	if(int partab[crc] != oldcrc){
+		if(debug['E'] > 0)
+			sys->print("bad crc: in %ux got %ux\n", oldcrc, int partab[crc]);
+		return nil;
+	}
+	b := array[op] of byte;
+	b[0:] = a[0:op];
+	if(debug['M'] > 0)
+		sys->print("OUT: %s [%x :: %x]\n", dump(b,op), crc, oldcrc);
+	return b;
+}
+
+Modem.reader(m: self ref Modem, pidc: chan of int)
+{
+	pidc <-= sys->pctl(0, nil);
+	if(crctab == nil)
+		mktabs();
+	a := array[Sys->ATOMICIO] of byte;
+	inbuf := 0;
+	while(m.fd != nil) {
+		while((n := read(m.fd, a[inbuf:], len a-inbuf)) > 0){
+			n += inbuf;
+			inbuf = 0;
+			if((m.spec & Ecp) == 0){
+				b := array[n] of byte;
+				for(i := 0; i<n; i++)
+					b[i] = byte (int a[i] & 16r7F);	# strip parity
+				m.rd <-= b;
+			}else{
+				#sys->print("IN: %s\n", dump(a,n));
+				i := 0;
+				if(m.waitsyn){
+					sys->print("seeking SYN #%x\n", m.seq);
+					syn := byte (SYN | 16r80);
+					lim := n-3;
+					for(; i <= lim; i++)
+						if(a[i] == syn && a[i+1] == syn && (int a[i+2]&16r0F) == m.seq){
+							i += 3;
+							m.waitsyn = 0;
+							sys->print("found SYN #%x@%d\n", m.seq, i-3);
+							break;
+						}
+				}
+				lim := n-Ecplen;
+				for(; (i = nextblock(a, i, n)) <= lim; i += Ecplen){
+					b := decode(a[i:]);
+					if(m.errforce || b == nil){
+						m.errforce = 0;
+						b = array[2] of byte;
+						b[0] = byte NACK;
+						b[1] = byte (m.seq | 16r40);
+						sys->print("NACK #%x\n", m.seq);
+						m.write(b);
+						m.waitsyn = 1;
+						i = n;		# discard rest of block
+						break;
+					}
+					m.seq = (m.seq+1) & 16rF;	# mod 16 counter
+					m.rd <-= b;
+				}
+				if(i < n){
+					a[0:] = a[i:n];
+					inbuf = n-i;
+				}
+			}
+		}
+		if(n <= 0)
+			break;
+	}
+#	m.fd = nil;
+	m.rd <-= nil;
+}
+
+playfd: ref Sys->FD;
+in_code, in_char: con iota;
+
+replay(m: ref Modem)
+{
+	buf := array[8192] of byte;
+	DMAX:	con 10;
+	d := 0;
+	da := array[DMAX] of byte;
+	playfd = nil;
+	if(playfd == nil)
+		playfd = sys->open("minitel.txt", Sys->OREAD);
+	if(playfd == nil)
+		return;
+	nl := 1;
+	discard := 1;
+	state := in_code;
+	hs := "";
+	start := 0;
+mainloop:
+	for(;;) {
+		n := sys->read(playfd, buf, len buf);
+		if(n <= 0)
+			break;
+		for(i:=0; i<n; i++) {
+			ch := int buf[i];
+			if(nl)
+				case ch {
+				'>' =>	discard = 0;
+				'<' =>	discard = 1;
+						if(start)
+							sys->sleep(1000);
+				'{' =>		start = 1;
+				'}' =>		break mainloop;
+				}
+			if(ch == '\n')
+				nl = 1;
+			else
+				nl = 0;
+			if(discard)
+				continue;
+			if(!start)
+				continue;
+			if(state == in_code && ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z')))
+				hs[len hs] = ch;
+			else if(ch == '(') {
+				state = in_char;
+				(v, nil) := toint(hs, 16);
+				da[d++] = byte v;
+				if(d == DMAX) {
+					send(ref Event.Edata(m.m.path, Mmodem, da));
+					d = 0;
+					da = array[DMAX] of byte;
+					sys->sleep(50);
+				}
+				hs = "";
+			}else if(ch == ')')
+				state = in_code;
+		}
+	}
+	playfd = nil;
+
+}
+
+kill(pid : int)
+{
+	prog := "#p/" + string pid + "/ctl";
+	fd := sys->open(prog, Sys->OWRITE);
+	if (fd != nil) {
+		cmd := array of byte "kill";
+		sys->write(fd, cmd, len cmd);
+	}
+}
+
+
+# Modem stuff
+
+
+# modem return codes
+Ok, Success, Failure, Noise, Found: con iota;
+
+#
+#  modem return messages
+#
+Msg: adt {
+	text: string;
+	trans: string;
+	code: int;
+};
+
+msgs: array of Msg = array [] of {
+	("OK",			"Ok", Ok),
+	("NO CARRIER",		"No carrier", Failure),
+	("ERROR",			"Bad modem command", Failure),
+	("NO DIALTONE",	"No dial tone", Failure),
+	("BUSY",			"Busy tone", Failure),
+	("NO ANSWER",		"No answer", Failure),
+	("CONNECT",		"", Success),
+};
+
+msend(m: ref Modem, x: string): int
+{
+	a := array of byte x;
+	return sys->write(m.fd, a, len a);
+}
+
+#
+#  apply a string of commands to modem
+#
+apply(m: ref Modem, s: string): int
+{
+	buf := "";
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		buf[len buf] = c;	# assume no Unicode
+		if(c == '\r' || i == (len s -1)){
+			if(c != '\r')
+				buf[len buf] = '\r';
+			if(msend(m, buf) < 0)
+				return Failure;
+			buf = "";
+		}
+	}
+	return Ok;
+}
+
+openmodem(m: ref Modem, dev: string): int
+{
+	m.fd = sys->open(dev, Sys->ORDWR);
+	m.cfd = sys->open(dev+"ctl", Sys->ORDWR);
+	if(m.fd == nil || m.cfd == nil)
+		return -1;
+#	hangup(m);
+#	m.fd = sys->open(dev, Sys->ORDWR);
+#	m.cfd = sys->open(dev+"ctl", Sys->ORDWR);
+#	if(m.fd == nil || m.cfd == nil)
+#		return -1;
+	return 0;
+}
+
+hangup(m: ref Modem)
+{
+	sys->sleep(1020);
+	msend(m, "+++");
+	sys->sleep(1020);
+	apply(m, "ATH0");
+	m.fd = nil;
+#	sys->write(m.cfd, array of byte "f", 1);
+	sys->write(m.cfd, array of byte "h", 1);
+	m.cfd = nil;
+	# HACK: shannon softmodem "off-hook" bug fix
+	sys->open("/dev/modem", Sys->OWRITE);
+}
+
+nethangup(m: ref Modem)
+{
+	m.fd = nil;
+	sys->write(m.cfd, array of byte "hangup", 6);
+	m.cfd = nil;
+}
+
+
+#
+#  check `s' for a known reply or `substr'
+#
+seenreply(s: string): (int, string)
+{
+	for(k := 0; k < len msgs; k++)
+		if(len s >= len msgs[k].text && s[0:len msgs[k].text] == msgs[k].text) {
+			return (msgs[k].code, msgs[k].trans);
+		}
+	return (Noise, s);
+}
+
+contains(s, t: string): int
+{
+	if(t == nil)
+		return 1;
+	if(s == nil)
+		return 0;
+	n := len t;
+	for(i := 0; i+n <= len s; i++)
+		if(s[i:i+n] == t)
+			return 1;
+	return 0;
+}
+
+dialout(m: ref Modem)
+{
+	if(m.initstr != nil)
+		apply(m, "AT"+m.initstr);
+	if(m.dialstr != nil) {
+		apply(m, "ATD"+m.dialstr);
+		m.lastdialstr = m.dialstr;
+		m.dialstr = nil;
+	}
+}
--- /dev/null
+++ b/appl/examples/minitel/screen.b
@@ -1,0 +1,1610 @@
+#
+# Occasional references are made to sections and tables in the
+# France Telecom Minitel specification
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+include "mdisplay.m";
+
+disp: MDisplay;
+
+Rect, Point		: import Draw;
+
+# display character sets
+videotex, semigraphic, french, american	:import MDisplay;
+
+# display foreground colour attributes
+fgBlack, fgBlue, fgRed, fgMagenta,
+fgGreen, fgCyan, fgYellow, fgWhite		:import MDisplay;
+
+# display background colour attributes
+bgBlack, bgBlue, bgRed, bgMagenta,
+bgGreen, bgCyan, bgYellow, bgWhite	:import MDisplay;
+
+fgMask, bgMask : import MDisplay;
+
+# display formatting attributes
+attrB, attrW, attrH, attrP, attrF, attrC, attrL, attrD	:import MDisplay;
+
+# Initial attributes - white on black
+ATTR0:	con fgWhite|bgBlack&~(attrB|attrW|attrH|attrP|attrF|attrC|attrL|attrD);
+
+# special features
+Cursor, Scroll, Insert
+	: con (1 << iota);
+
+# Screen states
+Sstart, Sss2, Sesc, Srepeat, Saccent, Scsi0, Scsi1, Sus0, Sus1, Sskip,
+Siso2022, Siso6429, Stransparent, Sdrcs, Sconceal, Swaitfor
+		: con iota;
+
+# Filter states
+FSstart, FSesc, FSsep, FS6429, FS2022: con iota;
+
+Screen: adt {
+	m:		ref Module;			# common attributes
+	ctxt:		ref Draw->Context;
+	in:		chan of ref Event;		# from the terminal
+
+	image:	ref Draw->Image;		# Mdisplay image
+	dispr40, dispr80: Rect;			# 40 and 80 column display region
+	oldtmode:	int;					# old terminal mode
+	rows:	int;					# number of screen rows (25 for minitel)
+	cols:		int;					# number of screen cols (40 or 80)
+	cset:		int;					# current display charset
+
+	pos:		Point;				# current writing position (x:1, y:0)
+	attr:		int;					# display attribute set
+	spec:	int;					# special features
+	savepos:	Point;				# `pos' before moving to row zero
+	saveattr:	int;					# `attr' before moving to row zero
+	savech:	int;					# last character `Put'
+	delimit:	int;					# attr changed, make next space a delimiter
+	cursor:	int;					# update cursor soon
+
+	state:	int;					# recogniser state
+	a0:		int;					# recogniser arg 0
+	a1:		int;					# recogniser arg 1
+
+	fstate: int;						# filter state
+	fsaved: array of byte;			# filter `chars so far'
+	badp: int;						# filter because of bad parameter
+
+	ignoredata: int;					# ignore data from
+
+	init:		fn(s: self ref Screen, ctxt: ref Draw->Context, r40, r80: Rect);
+	reset:	fn(s: self ref Screen);
+	run:		fn(s: self ref Screen);
+	quit:		fn(s: self ref Screen);
+	setmode:	fn(s: self ref Screen, tmode: int);
+	runstate:	fn(s: self ref Screen, data: array of byte);
+	put:		fn(s: self ref Screen, str: string);
+	msg:		fn(s: self ref Screen, str: string);
+};
+
+Screen.init(s: self ref Screen, ctxt: ref Draw->Context, r40, r80: Rect)
+{
+	disp =  load MDisplay MDisplay->PATH;
+	if(disp == nil)
+		fatal("can't load the display module: "+MDisplay->PATH);
+
+	s.m = ref Module(0, 0);
+	s.ctxt = ctxt;
+	s.dispr40 = r40;
+	s.dispr80 = r80;
+	s.oldtmode = -1;
+	s.in = chan of ref Event;
+	disp->Init(s.ctxt);
+	s.reset();
+	s.pos = Point(1, 1);
+	s.savech = 0;
+	s.cursor = 1;
+	s.ignoredata = 0;
+	s.fstate = FSstart;
+}
+
+Screen.reset(s: self ref Screen)
+{
+	s.setmode(T.mode);
+	indicators(s);
+	s.state = Sstart;
+}
+
+Screen.run(s: self ref Screen)
+{
+Runloop:
+	for(;;) alt {
+	ev := <- s.in =>
+		pick e := ev {
+		Equit =>
+			break Runloop;
+		Eproto =>
+			case e.cmd {
+			Creset =>
+				s.reset();
+			Cproto =>
+				case e.a0 {
+				START =>
+					case e.a1 {
+					SCROLLING =>
+						s.spec |= Scroll;
+					}
+				STOP =>
+					case e.a1 {
+					SCROLLING =>
+						s.spec &= ~Scroll;
+					}
+				MIXED =>
+					case e.a1 {
+					MIXED1 =>		# videotex -> mixed
+						if(T.mode != Mixed)
+							s.setmode(Mixed);
+						T.mode = Mixed;
+					MIXED2 =>		# mixed -> videotex
+						if(T.mode != Videotex)
+							s.setmode(Videotex);
+						T.mode = Videotex;
+					}
+				}
+			Ccursor =>			# update the cursor soon
+				s.cursor = 1;
+			Cindicators =>
+				indicators(s);
+			Cscreenoff =>
+				s.ignoredata = 1;
+				s.state = Sstart;
+			Cscreenon =>
+				s.ignoredata = 0;
+			* => break;
+			}
+		Edata =>
+			if(s.ignoredata)
+				continue;
+			oldpos := s.pos;
+			oldspec := s.spec;
+			da := filter(s, e.data);
+			while(len da > 0) {
+				s.runstate(da[0]); 
+				da = da[1:];
+			}
+
+			if(s.pos.x != oldpos.x || s.pos.y != oldpos.y || (s.spec&Cursor)^(oldspec&Cursor))
+				s.cursor = 1;		
+			if(s.cursor) {
+				if(s.spec & Cursor)
+					disp->Cursor(s.pos);
+				else
+					disp->Cursor(Point(-1,-1));
+				s.cursor = 0;
+				refresh();
+			} else if(e.from == Mkeyb)
+				refresh();
+		}
+	}
+	send(nil);	
+}
+
+# row0 indicators	(1.2.2)
+indicators(s: ref Screen)
+{
+	col: int;
+	ch: string;
+
+	attr := fgWhite|bgBlack;
+	case T.state {
+	Local =>
+		ch = "F";
+	Connecting =>
+		ch = "C";
+		attr |= attrF;
+	Online =>
+		ch = "C";
+	}
+	if(s.cols == 40) {
+		col = 39;
+		attr |= attrP;
+	} else
+		col = 77;
+	disp->Put(ch, Point(col, 0), videotex, attr, 0);
+}
+
+Screen.setmode(s: self ref Screen, tmode: int)
+{
+	dispr: Rect;
+	delims: int;
+	ulheight: int;
+	s.rows = 25;
+	s.spec = 0;
+	s.attr = s.saveattr = ATTR0;
+	s.delimit = 0;
+	s.pos = s.savepos = Point(-1, -1);
+	s.cursor = 1;
+	case tmode {
+	Videotex =>
+		s.cset = videotex;
+		s.cols = 40;
+		dispr = s.dispr40;
+		delims = 1;
+		ulheight = 2;
+		s.pos = Point(1,1);
+		s.spec &= ~Cursor;
+	Mixed =>
+#		s.cset  = french;
+		s.cset  = videotex;
+		s.cols = 80;
+		dispr = s.dispr80;
+		delims = 0;
+		ulheight = 1;
+		s.spec |= Scroll;
+		s.pos = Point(1, 1);
+	Ascii =>
+		s.cset = french;
+		s.cols = 80;
+		dispr = s.dispr80;
+		delims = 0;
+		ulheight = 1;
+	};
+	if(tmode != s.oldtmode) {
+		(nil, s.image) = disp->Mode(((0,0),(0,0)), 0, 0, 0, 0, nil);
+		T.layout(s.cols);
+		fontpath := sprint("/fonts/minitel/f%dx%d", s.cols, s.rows);
+		(nil, s.image) = disp->Mode(dispr, s.cols, s.rows, ulheight, delims, fontpath);
+		T.setkbmode(tmode);
+	}
+	disp->Reveal(0);	# concealing enabled (1.2.2)
+	disp->Cursor(Point(-1,-1));
+	s.oldtmode = tmode;
+}
+
+Screen.quit(nil: self ref Screen)
+{
+	disp->Quit();
+}
+
+Screen.runstate(s: self ref Screen, data: array of byte)
+{
+	while(len data > 0)
+		case T.mode {
+		Videotex =>
+			data = vstate(s, data);
+		Mixed =>
+			data = mstate(s, data);
+		Ascii =>
+			data = astate(s, data);
+		};
+}
+
+# process a byte from set C0
+vc0(s: ref Screen, ch: int)
+{
+	case ch {
+#	SOH =>							# not in spec, wait for 16r04
+#		s.a0 = 16r04;
+#		s.state = Swaitfor;
+	SS2 =>
+		s.state = Sss2;
+	SYN =>
+		s.state = Sss2;					# not in the spec, but acts like SS2
+	ESC =>
+		s.state = Sesc;
+	SO =>
+		s.cset = semigraphic;
+		s.attr &= ~(attrH|attrW|attrP);	# 1.2.4.2
+		s.attr &= ~attrL;				# 1.2.4.3
+	SI =>
+		s.cset = videotex;
+		s.attr &= ~attrL;				# 1.2.4.3
+		s.attr &= ~(attrH|attrW|attrP);			# some servers seem to assume this too
+	SEP or SS3 =>					# 1.2.7
+		s.state = Sskip;
+	BS =>
+		if(s.pos.x == 1) {
+			if(s.pos.y == 0)
+				break;
+			if(s.pos.y == 1)
+				s.pos.y = s.rows - 1;
+			else
+				s.pos.y -= 1;
+			s.pos.x = s.cols;
+		} else
+			s.pos.x -= 1;
+	HT =>
+		if(s.pos.x == s.cols) {
+			if(s.pos.y == 0)
+				break;
+			if(s.pos.y == s.rows - 1)
+				s.pos.y = 1;
+			else
+				s.pos.y += 1;
+			s.pos.x = 1;
+		} else
+			s.pos.x += 1;
+	LF =>
+		if(s.pos.y == s.rows - 1)
+			if(s.spec&Scroll)
+				scroll(1, 1);
+			else
+				s.pos.y = 1;
+		else if(s.pos.y == 0) {		# restore attributes on leaving row zero
+			s.pos = s.savepos;
+			s.attr = s.saveattr;
+		} else
+			s.pos.y += 1;
+	VT =>
+		if(s.pos.y == 1)
+			if(s.spec&Scroll)
+				scroll(1, -1);
+			else
+				s.pos.y = s.rows - 1;
+		else if(s.pos.y == 0)
+			break;
+		else
+			s.pos.y -= 1;
+	CR =>
+		s.pos.x = 1;
+	CAN =>
+		cols := s.cols - s.pos.x + 1;
+		disp->Put(dup(' ', cols), Point(s.pos.x,s.pos.y), s.cset, s.attr, 0);
+	US =>
+		# expect US row, col
+		s.state = Sus0;
+	FF =>
+		s.cset = videotex;
+		s.attr = ATTR0;
+		s.pos = Point(1,1);
+		s.spec &= ~Cursor;
+		s.cursor = 1;
+		clear(s);
+	RS =>
+		s.cset = videotex;
+		s.attr = ATTR0;
+		s.pos = Point(1,1);
+		s.spec &= ~Cursor;
+		s.cursor = 1;
+	CON =>
+		s.spec |= Cursor;
+		s.cursor = 1;
+	COFF =>
+		s.spec &= ~Cursor;
+		s.cursor = 1;
+	REP =>
+		# repeat
+		s.state = Srepeat;
+	NUL =>
+		# padding character - ignore, but may appear anywhere
+		;
+	BEL =>
+		# ah ...
+		;
+	}
+}
+
+# process a byte from the set c1 - introduced by the ESC character
+vc1(s: ref Screen, ch: int)
+{
+	if(ISC0(ch)) {
+		s.state = Sstart;
+		vc0(s, ch);
+		return;
+	}
+	if(ch >= 16r20 && ch <= 16r2f) {
+		if(ch == 16r25)
+			s.state = Stransparent;
+		else if(ch == 16r23)
+			s.state = Sconceal;
+		else
+			s.state = Siso2022;
+		s.a0 = s.a1 = 0;
+		return;
+	}
+
+	fg := bg := -1;
+	case ch {
+	16r35 or
+	16r36 or
+	16r37 =>
+		s.state = Sskip;				# skip next char unless C0
+		return;
+		
+	16r5b =>						# CSI sequence
+		s.a0 = s.a1 = 0;
+		if(s.pos.y > 0)				# 1.2.5.2
+			s.state = Scsi0;
+		return;
+
+	# foreground colour	
+	16r40 =>	fg = fgBlack;
+	16r41 =>	fg = fgRed;
+	16r42 =>	fg = fgGreen;
+	16r43 =>	fg = fgYellow;
+	16r44 =>	fg = fgBlue;
+	16r45 =>	fg = fgMagenta;
+	16r46 =>	fg = fgCyan;
+	16r47 =>	fg = fgWhite;
+
+	# background colour
+	16r50 =>	bg = bgBlack;
+	16r51 =>	bg = bgRed;
+	16r52 =>	bg = bgGreen;
+	16r53 =>	bg = bgYellow;
+	16r54 =>	bg = bgBlue;
+	16r55 =>	bg = bgMagenta;
+	16r56 =>	bg = bgCyan;
+	16r57 =>	bg = bgWhite;
+
+	# flashing
+	16r48 =>	s.attr |= attrF;
+	16r49 =>	s.attr &= ~attrF;
+
+	# conceal (serial attribute)
+	16r58 =>	s.attr |= attrC;
+			s.delimit = 1;
+	16r5f =>	s.attr &= ~attrC;
+			s.delimit = 1;
+
+	# start lining (+separated graphics) (serial attribute)
+	16r5a =>	s.attr |= attrL;
+			s.delimit = 1;
+	16r59 =>	s.attr &= ~attrL;
+			s.delimit = 1;
+
+	# reverse polarity
+	16r5d =>	s.attr |= attrP;
+	16r5c =>	s.attr &= ~attrP;
+
+	# normal size
+	16r4c =>
+		s.attr &= ~(attrW|attrH);
+
+	# double height
+	16r4d =>
+		if(s.pos.y < 2)
+			break;
+		s.attr &= ~(attrW|attrH);
+		s.attr |= attrH;
+
+	# double width
+	16r4e =>
+		if(s.pos.y < 1)
+			break;
+		s.attr &= ~(attrW|attrH);
+		s.attr |= attrW;
+
+	# double size
+	16r4f =>
+		if(s.pos.y < 2)
+			break;
+		s.attr |= (attrW|attrH);
+	}
+	if(fg >= 0) {
+		s.attr &= ~fgMask;
+		s.attr |= fg;
+	}
+	if(bg >= 0) {
+		s.attr &= ~bgMask;
+		s.attr |= bg;
+		s.delimit = 1;
+	}
+	s.state = Sstart;
+}
+
+
+# process a SS2 character
+vss2(s: ref Screen, ch: int)
+{
+	if(ISC0(ch)) {
+		s.state = Sstart;
+		vc0(s, ch);
+		return;
+	}
+	case ch {
+	16r41 or	# grave				# 5.1.2 
+	16r42 or	# acute
+	16r43 or	# circumflex
+	16r48 or	# umlaut
+	16r4b =>	# cedilla
+		s.a0 = ch;
+		s.state = Saccent;
+		return;
+	16r23 =>	ch = '£';				# Figure 2.8
+	16r24 =>	ch = '$';
+	16r26 =>	ch = '#';
+	16r27 =>	ch = '§';
+	16r2c =>	ch = 16rc3;	# '←';
+	16r2d =>	ch = 16rc0;	# '↑';
+	16r2e =>	ch = 16rc4;	# '→';
+	16r2f =>	ch = 16rc5;	# '↓';
+	16r30 =>	ch = '°';
+	16r31 =>	ch = '±';
+	16r38 =>	ch = '÷';
+	16r3c =>	ch = '¼';
+	16r3d =>	ch = '½';
+	16r3e =>	ch = '¾';
+	16r7a =>	ch = 'œ';
+	16r6a =>	ch = 'Œ';
+	16r7b =>	ch = 'ß';
+	}
+	s.put(tostr(ch));
+	s.savech = ch;
+	s.state = Sstart;
+}
+
+# process CSI functions
+vcsi(s: ref Screen, ch: int)
+{
+	case s.state {
+	Scsi0 =>
+		case ch {
+		# move cursor up n rows, stop at top of screen
+		'A' =>
+			s.pos.y -= s.a0;
+			if(s.pos.y < 1)
+				s.pos.y = 1;
+
+		# move cursor down n rows, stop at bottom of screen
+		'B' =>
+			s.pos.y += s.a0;
+			if(s.pos.y >= s.rows)
+				s.pos.y = s.rows - 1;
+
+		# move cursor n columns right, stop at edge of screen
+		'C' =>
+			s.pos.x += s.a0;
+			if(s.pos.x > s.cols)
+				s.pos.x = s.cols;
+
+		# move cursor n columns left, stop at edge of screen
+		'D' =>
+			s.pos.x -= s.a0;
+			if(s.pos.x < 1)
+				s.pos.x = 1;
+
+		# direct cursor addressing
+		';' =>	
+			s.state = Scsi1;
+			return;
+
+		'J' =>
+			case s.a0 {
+			# clears from the cursor to the end of the screen inclusive
+			0 =>
+				rowclear(s.pos.y, s.pos.x, s.cols);
+				for(r:=s.pos.y+1; r<s.rows; r++)
+					rowclear(r, 1, s.cols);
+			# clears from the beginning of the screen to the cursor inclusive
+			1 =>
+				for(r:=1; r<s.pos.y; r++)
+					rowclear(r, 1, s.cols);
+				rowclear(s.pos.y, 1, s.pos.x);
+			# clears the entire screen
+			2 =>
+				clear(s);
+			}
+
+		'K' => 
+			case s.a0 {
+			# clears from the cursor to the end of the row
+			0 =>	rowclear(s.pos.y, s.pos.x, s.cols);
+
+			# clears from the start of the row to the cursor
+			1 => rowclear(s.pos.y, 1, s.pos.x);
+
+			# clears the entire row in which the cursor is positioned
+			2 => rowclear(s.pos.y, 1, s.cols);
+			}
+
+		# deletes n characters from cursor position
+		'P' =>
+			rowclear(s.pos.y, s.pos.x, s.pos.x+s.a0-1);
+
+		# inserts n characters from cursor position
+		'@' =>
+			disp->Put(dup(' ', s.a0), Point(s.pos.x,s.pos.y), s.cset, s.attr, 1);
+
+		# starts cursor insert mode
+		'h' =>
+			if(s.a0 == 4)
+				s.spec |= Insert;
+
+		'l' =>		# ends cursor insert mode
+			if(s.a0 == 4)
+				s.spec &= ~Insert;
+
+		# deletes n rows from cursor row
+		'M' =>
+			scroll(s.pos.y, s.a0);
+
+	 	# inserts n rows from cursor row
+		'L' =>
+			scroll(s.pos.y, -1*s.a0);
+		}
+		s.state = Sstart;
+	Scsi1 =>
+		case ch {
+		# direct cursor addressing
+		'H' =>
+			if(s.a0 > 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols)
+				s.pos = Point(s.a1, s.a0);
+		}
+		s.state = Sstart;
+	}
+}
+
+# Screen state - Videotex mode
+vstate(s: ref Screen, data: array of byte): array of byte
+{
+	i: int;
+	for(i = 0; i < len data; i++) {
+		ch := int data[i];
+		
+		if(debug['s']) {
+			cs:="";
+			if(s.cset==videotex) cs = "v"; else cs="s";
+			fprint(stderr, "vstate %d, %ux (%c) %.4ux %.4ux %s (%d,%d)\n", s.state, ch, ch, s.attr, s.spec, cs, s.pos.y, s.pos.x);
+		}
+		case s.state {
+		Sstart =>
+			if(ISG0(ch) || ch == SP) {
+				n := 0;
+				str := "";
+				while(i < len data) {
+					ch = int data[i];
+					if(ISG0(ch) || ch == SP)
+						str[n++] = int data[i++];
+					else {
+						i--;
+						break;
+					}
+				}
+				if(n > 0) {
+					if(debug['s'])
+						fprint(stderr, "vstate puts(%s)\n", str);
+					s.put(str);
+					s.savech = str[n-1];
+				}
+			} else if(ISC0(ch))
+				vc0(s, ch);
+			else if(ch == DEL) {
+				if(s.cset == semigraphic)
+					ch = 16r5f;
+				s.put(tostr(ch));
+				s.savech = ch;
+			}
+		Sss2 =>
+			if(ch == NUL)			# 1.2.6.1
+				continue;
+			if(s.cset == semigraphic)	# 1.2.3.4
+				continue;
+			vss2(s, ch);
+		Sesc =>
+			if(ch == NUL)
+				continue;
+			vc1(s, ch);
+		Srepeat =>
+			# byte from `columns' 4 to 7 gives repeat count on 6 bits
+			# of the last `Put' character
+			if(ch == NUL)
+				continue;
+			if(ISC0(ch)) {
+				s.state = Sstart;
+				vc0(s, ch);
+				break;
+			}
+			if(ch >= 16r40 && ch <= 16r7f)
+				s.put(dup(s.savech, (ch-16r40))); 
+			s.state = Sstart;
+		Saccent =>
+			case s.a0 {
+			16r41 =>	# grave
+				case ch {
+				'a' =>	ch = 'à';
+				'e' =>	ch = 'è';
+				'u' =>	ch = 'ù';
+				}
+			16r42 =>	# acute
+				case ch {
+				'e' =>	ch = 'é';
+				}
+			16r43 =>	# circumflex
+				case ch {
+				'a' =>	ch = 'â';
+				'e' =>	ch = 'ê';
+				'i' =>		ch = 'î';
+				'o' =>	ch = 'ô';
+				'u' =>	ch = 'û';
+				}
+			16r48 =>	# umlaut
+				case ch {
+				'a' =>	ch = 'ä';
+				'e' =>	ch = 'ë';
+				'i' =>		ch = 'ï';
+				'o' =>	ch = 'ö';
+				'u' =>	ch = 'ü';
+				}
+			16r4b =>	# cedilla
+				case ch {
+				'c' =>	ch = 'ç';
+				}
+			}
+			s.put(tostr(ch));
+			s.savech = ch;
+			s.state = Sstart;
+		Scsi0 =>
+			if(ch >= 16r30 && ch <= 16r39) {
+				s.a0 *= 10;
+				s.a0 += (ch - 16r30);
+			} else if((ch >= 16r20 && ch <= 16r29) || (ch >= 16r3a && ch <= 16r3f)) {	# 1.2.7
+				s.a0 = 0;
+				s.state = Siso6429;
+			} else
+				vcsi(s, ch);
+		Scsi1 =>
+			if(ch >= 16r30 && ch <= 16r39) {
+				s.a1 *= 10;
+				s.a1 += (ch - 16r30);
+			} else
+				vcsi(s, ch);
+		Sus0 =>
+			if(ch == 16r23) {		# start DRCS definition
+				s.state = Sdrcs;
+				s.a0 = 0;
+				break;
+			}
+			if(ch >= 16r40 && ch < 16r80)
+				s.a0 = (ch - 16r40);
+			else if(ch >= 16r30 && ch <= 16r32)
+				s.a0 = (ch - 16r30);
+			else
+				s.a0 = -1;
+			s.state = Sus1;
+		Sus1 =>
+			if(ch >= 16r40 && ch < 16r80)
+				s.a1 = (ch - 16r40);
+			else if(ch >= 16r30 && ch <= 16r39) {
+				s.a1 = (ch - 16r30);
+				s.a0 = s.a0*10 + s.a1;	# shouldn't be used any more
+				s.a1 = 1;
+			} else
+				s.a1 = -1;
+			# US row, col : this is how you get to row zero
+			if(s.a0 >= 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols) {
+				if(s.a0 == 0 && s.pos.y > 0) {
+					s.savepos = s.pos;
+					s.saveattr = s.attr;
+				}
+				s.pos = Point(s.a1, s.a0);
+				s.delimit = 0;		# 1.2.5.3, don't reset serial attributes
+				s.attr = ATTR0;
+				s.cset = videotex;
+			}
+			s.state = Sstart;
+		Sskip =>
+			# swallow the next character unless from C0
+			s.state = Sstart;
+			if(ISC0(ch))
+				vc0(s, ch);
+		Swaitfor =>
+			# ignore characters until the character in a0 inclusive
+			if(ch == s.a0)
+				s.state = Sstart;
+		Siso2022 =>
+			# 1.2.7
+			# swallow (upto) 3 characters from column 2,
+			# then 1 character from columns 3 to 7
+			if(ch == NUL)
+				continue;
+			if(ISC0(ch)) {
+				s.state = Sstart;
+				vc0(s, ch);
+				break;
+			}
+			s.a0++;
+			if(s.a0 <= 3) {
+				if(ch >= 16r20 && ch <= 16r2f)
+					break;
+			}
+			if (s.a0 <= 4 && ch >= 16r30 && ch <= 16r7f) {
+					s.state = Sstart;
+					break;
+			}
+			s.state = Sstart;
+			s.put(tostr(DEL));
+		Siso6429 =>
+			# 1.2.7
+			# swallow characters from column 3,
+			# or column 2, then 1 from column 4 to 7
+			if(ISC0(ch)) {
+				s.state = Sstart;
+				vc0(s, ch);
+				break;
+			}
+			if(ch >= 16r20 && ch <= 16r3f)
+					break;
+			if(ch >= 16r40 && ch <= 16r7f) {
+				s.state = Sstart;
+				break;
+			}
+			s.state = Sstart;	
+			s.put(tostr(DEL));
+		Stransparent =>
+			# 1.2.7
+			# ignore all codes until ESC, 25, 40 or ESC, 2F, 3F
+			# progress in s.a0 and s.a1
+			match := array [] of {
+					array [] of { ESC,	16r25,	16r40 },
+					array [] of { ESC,	16r2f,	16r3f },
+			};
+			if(ch == ESC) {
+				s.a0 = s.a1 = 1;
+				break;
+			}
+			if(ch == match[0][s.a0])
+				s.a0++;
+			else
+				s.a0 = 0;
+			if(ch == match[1][s.a1])
+				s.a1++;
+			else
+				s.a1 = 0;
+			if(s.a0 == 3 || s.a1 == 3)
+				s.state = Sstart;
+		Sdrcs =>
+			if(s.a0 > 0) {			# fixed number of bytes to skip in a0
+				s.a0--;
+				if(s.a0 == 0) {
+					s.state = Sstart;
+					break;
+				}
+			} else if(ch == US)		# US XX YY - end of DRCS
+				s.state = Sus0;
+			else if(ch == 16r20)	# US 23 20 20 20 4[23] 49
+				s.a0 = 4;
+		Sconceal =>
+			# 1.2.4.4
+			# ESC 23 20 58 - Conceal fields
+			# ESC 23 20 5F - Reveal fields
+			# ESC 23 21 XX - Filter
+			# progress in s.a0
+			case s.a0 {
+			0 =>
+				if(ch == 16r20 || ch == 16r21)
+					s.a0 = ch;
+			16r20 =>
+				case ch {
+				16r58 =>
+					disp->Reveal(0);
+					disp->Refresh();
+				16r5f =>
+					disp->Reveal(1);
+					disp->Refresh();
+				}
+				s.state = Sstart;
+			16r21 =>
+				s.state = Sstart;
+			}
+		}
+	}
+	if (i < len data)
+		return data[i:];
+	else
+		return nil;
+}
+
+# Screen state - Mixed mode
+mstate(s: ref Screen, data: array of byte): array of byte
+{
+	i: int;
+Stateloop:
+	for(i = 0; i < len data; i++) {
+		ch := int data[i];
+		
+		if(debug['s']) {
+			cs:="";
+			if(s.cset==videotex) cs = "v"; else cs="s";
+			fprint(stderr, "mstate %d, %ux (%c) %.4ux %.4ux %s (%d,%d)\n", s.state, ch, ch, s.attr, s.fstate, cs, s.pos.y, s.pos.x);
+		}
+		case s.state {
+		Sstart =>
+			if(ISG0(ch) || ch == SP) {
+				n := 0;
+				str := "";
+				while(i < len data) {
+					ch = int data[i];
+					if(ISG0(ch) || ch == SP)
+						str[n++] = int data[i++];
+					else {
+						i--;
+						break;
+					}
+				}
+				if(n > 0) {
+					if(debug['s'])
+						fprint(stderr, "mstate puts(%s)\n", str);
+					s.put(str);
+					s.savech = str[n-1];
+				}
+			} else if(ISC0(ch))
+				mc0(s, ch);
+			else if(ch == DEL) {
+				if(s.cset == semigraphic)
+					ch = 16r5f;
+				s.put(tostr(ch));
+				s.savech = ch;
+			}
+		Sesc =>
+			if(ch == NUL)
+				continue;
+			mc1(s, ch);
+		Scsi0 =>
+			if(ch >= 16r30 && ch <= 16r39) {
+				s.a0 *= 10;
+				s.a0 += (ch - 16r30);
+			} else if(ch == '?') {
+				s.a0 = '?';
+			} else 
+				mcsi(s, ch);
+			if(T.mode != Mixed)	# CSI ? { changes to Videotex mode
+				break Stateloop;
+		Scsi1 =>
+			if(ch >= 16r30 && ch <= 16r39) {
+				s.a1 *= 10;
+				s.a1 += (ch - 16r30);
+			} else
+				mcsi(s, ch);
+		Sus0 =>
+			if(ch >= 16r40 && ch < 16r80)
+				s.a0 = (ch - 16r40);
+			else if(ch >= 16r30 && ch <= 16r32)
+				s.a0 = (ch - 16r30);
+			else
+				s.a0 = -1;
+			s.state = Sus1;
+		Sus1 =>
+			if(ch >= 16r40 && ch < 16r80)
+				s.a1 = (ch - 16r40);
+			else if(ch >= 16r30 && ch <= 16r39) {
+				s.a1 = (ch - 16r30);
+				s.a0 = s.a0*10 + s.a1;	# shouldn't be used any more
+				s.a1 = 1;
+			} else
+				s.a1 = -1;
+			# US row, col : this is how you get to row zero
+			if(s.a0 >= 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols) {
+				if(s.a0 == 0 && s.pos.y > 0) {
+					s.savepos = s.pos;
+					s.saveattr = s.attr;
+				}
+				s.pos = Point(s.a1, s.a0);
+				s.delimit = 0;		# 1.2.5.3, don't reset serial attributes
+				s.attr = ATTR0;
+				s.cset = videotex;
+			}
+			s.state = Sstart;
+		Siso6429 =>
+			# 1.2.7
+			# swallow characters from column 3,
+			# or column 2, then 1 from column 4 to 7
+			if(ISC0(ch)) {
+				s.state = Sstart;
+				mc0(s, ch);
+				break;
+			}
+			if(ch >= 16r20 && ch <= 16r3f)
+					break;
+			if(ch >= 16r40 && ch <= 16r7f) {
+				s.state = Sstart;
+				break;
+			}
+			s.state = Sstart;	
+			s.put(tostr(DEL));
+		}
+	}
+	if (i < len data)
+		return data[i:];
+	else
+		return nil;
+	return nil;
+}
+
+# process a byte from set C0 - Mixed mode
+mc0(s: ref Screen, ch: int)
+{
+	case ch {
+	ESC =>
+		s.state = Sesc;
+	SO =>
+#		s.cset = french;
+		;
+	SI =>
+#		s.cset = american;
+		;
+	BS =>
+		if(s.pos.x > 1)
+			s.pos.x -= 1;
+	HT =>
+		s.pos.x += 8;
+		if(s.pos.x > s.cols)
+			s.pos.x = s.cols;
+	LF or VT or FF =>
+		if(s.pos.y == s.rows - 1)
+			if(s.spec&Scroll)
+				scroll(1, 1);
+			else
+				s.pos.y = 1;
+		else if(s.pos.y == 0) {		# restore attributes on leaving row zero
+			if(ch == LF) {		# 4.5
+				s.pos = s.savepos;
+				s.attr = s.saveattr;
+			}
+		} else
+			s.pos.y += 1;
+	CR =>
+		s.pos.x = 1;
+	CAN or SUB =>	# displays the error symbol - filled in rectangle
+		disp->Put(dup(16r5f, 1), Point(s.pos.x,s.pos.y), s.cset, s.attr, 0);
+	NUL =>
+		# padding character - ignore, but may appear anywhere
+		;
+	BEL =>
+		# ah ...
+		;
+	XON =>	# screen copying
+		;
+	XOFF =>	# screen copying
+		;
+	US =>
+		# expect US row, col
+		s.state = Sus0;
+	}
+}
+
+# process a byte from the set c1 - introduced by the ESC character - Mixed mode
+mc1(s: ref Screen, ch: int)
+{
+	if(ISC0(ch)) {
+		s.state = Sstart;
+		mc0(s, ch);
+		return;
+	}
+	case ch {
+	16r5b =>						# CSI sequence
+		s.a0 = s.a1 = 0;
+		if(s.pos.y > 0)				# 1.2.5.2
+			s.state = Scsi0;
+		return;
+
+	16r44 or						# IND like LF
+	16r45 =>						# NEL like CR LF
+		if(ch == 16r45)
+			s.pos.x = 1;
+		if(s.pos.y == s.rows - 1)
+			if(s.spec&Scroll)
+				scroll(1, 1);
+			else
+				s.pos.y = 1;
+		else if(s.pos.y == 0) {		# restore attributes on leaving row zero
+			s.pos = s.savepos;
+			s.attr = s.saveattr;
+		} else
+			s.pos.y += 1;
+	16r4d =>						# RI
+		if(s.pos.y == 1)
+			if(s.spec&Scroll)
+				scroll(1, -1);
+			else
+				s.pos.y = s.rows - 1;
+		else if(s.pos.y == 0)
+			break;
+		else
+			s.pos.y -= 1;
+	}
+	s.state = Sstart;
+}
+
+
+# process CSI functions - Mixed mode
+mcsi(s: ref Screen, ch: int)
+{
+	case s.state {
+	Scsi0 =>
+		case ch {
+		# move cursor up n rows, stop at top of screen
+		'A' =>
+			if(s.a0 == 0)
+				s.a0 = 1;
+			s.pos.y -= s.a0;
+			if(s.pos.y < 1)
+				s.pos.y = 1;
+
+		# move cursor down n rows, stop at bottom of screen
+		'B' =>
+			if(s.a0 == 0)
+				s.a0 = 1;
+			s.pos.y += s.a0;
+			if(s.pos.y >= s.rows)
+				s.pos.y = s.rows - 1;
+
+		# move cursor n columns right, stop at edge of screen
+		'C' =>
+			if(s.a0 == 0)
+				s.a0 = 1;
+			s.pos.x += s.a0;
+			if(s.pos.x > s.cols)
+				s.pos.x = s.cols;
+
+		# move cursor n columns left, stop at edge of screen
+		'D' =>
+			if(s.a0 == 0)
+				s.a0 = 1;
+			s.pos.x -= s.a0;
+			if(s.pos.x < 1)
+				s.pos.x = 1;
+
+		# second parameter
+		';' =>	
+			s.state = Scsi1;
+			return;
+
+		'J' =>
+			case s.a0 {
+			# clears from the cursor to the end of the screen inclusive
+			0 =>
+				rowclear(s.pos.y, s.pos.x, s.cols);
+				for(r:=s.pos.y+1; r<s.rows; r++)
+					rowclear(r, 1, s.cols);
+			# clears from the beginning of the screen to the cursor inclusive
+			1 =>
+				for(r:=1; r<s.pos.y; r++)
+					rowclear(r, 1, s.cols);
+				rowclear(s.pos.y, 1, s.pos.x);
+			# clears the entire screen
+			2 =>
+				clear(s);
+			}
+
+		'K' => 
+			case s.a0 {
+			# clears from the cursor to the end of the row
+			0 =>	rowclear(s.pos.y, s.pos.x, s.cols);
+
+			# clears from the start of the row to the cursor
+			1 => rowclear(s.pos.y, 1, s.pos.x);
+
+			# clears the entire row in which the cursor is positioned
+			2 => rowclear(s.pos.y, 1, s.cols);
+			}
+
+		# inserts n characters from cursor position
+		'@' =>
+			disp->Put(dup(' ', s.a0), Point(s.pos.x,s.pos.y), s.cset, s.attr, 1);
+
+		# starts cursor insert mode
+		'h' =>
+			if(s.a0 == 4)
+				s.spec |= Insert;
+
+		'l' =>		# ends cursor insert mode
+			if(s.a0 == 4)
+				s.spec &= ~Insert;
+
+	 	# inserts n rows from cursor row
+		'L' =>
+			scroll(s.pos.y, -1*s.a0);
+			s.pos.x = 1;
+
+		# deletes n rows from cursor row
+		'M' =>
+			scroll(s.pos.y, s.a0);
+			s.pos.x = 1;
+
+		# deletes n characters from cursor position
+		'P' =>
+			rowclear(s.pos.y, s.pos.x, s.pos.x+s.a0-1);
+
+		# select Videotex mode
+		'{' =>
+			if(s.a0 == '?') {
+				T.mode = Videotex;
+				s.setmode(T.mode);
+			}
+
+		# display attributes
+		'm' =>
+			case s.a0 {
+			0 =>		s.attr &= ~(attrL|attrF|attrP|attrB);
+			1 =>		s.attr |= attrB;
+			4 =>		s.attr |= attrL;
+			5 =>		s.attr |= attrF;
+			7 =>		s.attr |= attrP;
+			22 =>	s.attr &= ~attrB;
+			24 =>	s.attr &= ~attrL;
+			25 =>	s.attr &= ~attrF;
+			27 =>	s.attr &= ~attrP;
+			}
+		# direct cursor addressing
+		'H' =>
+			if(s.a0 == 0)
+				s.a0 = 1;
+			if(s.a1 == 0)
+				s.a1 = 1;
+			if(s.a0 > 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols)
+				s.pos = Point(s.a1, s.a0);
+		}
+		s.state = Sstart;
+	Scsi1 =>
+		case ch {
+		# direct cursor addressing
+		'H' =>
+			if(s.a0 == 0)
+				s.a0 = 1;
+			if(s.a1 == 0)
+				s.a1 = 1;
+			if(s.a0 > 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols)
+				s.pos = Point(s.a1, s.a0);
+		}
+		s.state = Sstart;
+	}
+}
+
+
+# Screen state - ASCII mode
+astate(nil: ref Screen, nil: array of byte): array of byte
+{
+	return nil;
+}
+
+# Put a string in the current attributes to the current writing position
+Screen.put(s: self ref Screen, str: string)
+{
+	while((l := len str) > 0) {
+		n := s.cols - s.pos.x + 1;		# characters that will fit on this row
+		if(s.attr & attrW) {
+			if(n > 1)				# fit normal width character in last column
+				n /= 2;
+		}
+		if(n > l)
+			n = l;
+		if(s.delimit) {		# set delimiter bit on 1st space (if any)
+			for(i:=0; i<n; i++)
+				if(str[i] == ' ')
+					break;
+			if(i > 0) {
+				disp->Put(str[0:i], s.pos, s.cset, s.attr, s.spec&Insert);
+				incpos(s, i);
+			}
+			if(i < n) {
+				if(debug['s']) {
+					cs:="";
+					if(s.cset==videotex) cs = "v"; else cs="s";
+					fprint(stderr, "D %ux %s\n", s.attr|attrD, cs);
+				}
+				disp->Put(tostr(str[i]), s.pos, s.cset, s.attr|attrD, s.spec&Insert);
+				incpos(s, 1);
+				s.delimit = 0;
+				# clear serial attributes once used
+				# hang onto background attribute - needed for semigraphics
+				case s.cset {
+				videotex =>
+					s.attr &= ~(attrL|attrC);
+				semigraphic =>
+					s.attr &= ~(attrC);
+				}
+			}
+			if(i < n-1) {
+				disp->Put(str[i+1:n], s.pos, s.cset, s.attr, s.spec&Insert);
+				incpos(s, n-(i+1));
+			}
+		} else {
+			disp->Put(str[0:n], s.pos, s.cset, s.attr, s.spec&Insert);
+			incpos(s, n);
+		}
+		if(n < len str)
+			str = str[n:];
+		else
+			str = nil;
+	}
+#	if(T.state == Local || T.spec&Echo)
+#		refresh();
+}
+
+# increment the current writing position by `n' cells.
+# caller must ensure that `n' characters can fit
+incpos(s: ref Screen, n: int)
+{
+	if(s.attr & attrW)
+		s.pos.x += 2*n;
+	else
+		s.pos.x += n;
+	if(s.pos.x > s.cols)
+		if(s.pos.y == 0)			# no wraparound from row zero
+			s.pos.x = s.cols;
+		else {
+			s.pos.x = 1;
+			if(s.pos.y == s.rows - 1 && s.spec&Scroll) {
+				if(s.attr & attrH) {
+					scroll(1, 2);
+				} else {
+					scroll(1, 1);
+					rowclear(s.pos.y, 1, s.cols);
+				}
+			} else {
+				if(s.attr & attrH)
+					s.pos.y += 2;
+				else
+					s.pos.y += 1;
+				if(s.pos.y >= s.rows)
+					s.pos.y -= (s.rows-1);
+			}
+		}
+}
+
+# clear row `r' from `first' to `last' column inclusive
+rowclear(r, first, last: int)
+{
+	# 16r5f is the semi-graphic black rectangle
+	disp->Put(dup(16r5f, last-first+1), Point(first,r), semigraphic, fgBlack, 0);
+#	disp->Put(dup(' ', last-first+1), Point(first,r), S.cset, fgBlack, 0);
+}
+
+clear(s: ref Screen)
+{
+	for(r:=1; r<s.rows; r++)
+		rowclear(r, 1, s.cols);
+}
+
+# called to suggest a display update
+refresh()
+{
+	disp->Refresh();
+}
+
+# scroll the screen
+scroll(topline, nlines: int)
+{
+	disp->Scroll(topline, nlines);
+	disp->Refresh();
+}
+
+# filter the specified ISO6429 and ISO2022 codes from the screen input
+# TODO: filter some ISO2022 sequences
+filter(s: ref Screen, data: array of byte): array of array of byte
+{
+	case T.mode {
+	Videotex =>
+		return vfilter(s, data);
+	Mixed =>
+		return mfilter(s, data);
+	Ascii =>
+		return afilter(s, data);
+	}
+	return nil;
+}
+
+# filter the specified ISO6429 and ISO2022 codes from the screen input
+vfilter(s: ref Screen, data: array of byte): array of array of byte
+{
+	ba := array [0] of array of byte;
+	changed := 0;
+
+	d0 := 0;
+	for(i:=0; i<len data; i++) {
+		ch := int data[i];
+		case s.fstate {
+		FSstart =>
+			if(ch == ESC) {
+				s.fstate = FSesc;
+				changed = 1;
+				if(i > d0)
+					ba = dappend(ba, data[d0:i]);
+				d0 = i+1;
+			}
+		FSesc =>
+			d0 = i+1;
+			changed = 1;
+			if(ch == '[') {
+				s.fstate = FS6429;
+				s.fsaved = array [0] of byte;
+				s.badp = 0;
+#			} else if(ch == 16r20) {
+#				s.fstate = FS2022;
+#				s.fsaved = array [0] of byte;
+				s.badp = 0;
+			} else if(ch == ESC) {
+				ba = dappend(ba, array [] of { byte ESC });
+				s.fstate = FSesc;
+			} else {
+				# false alarm - don't filter
+				ba = dappend(ba, array [] of { byte ESC, byte ch });
+				s.fstate = FSstart;
+			}
+		FS6429 =>	# filter out invalid CSI sequences
+			d0 = i+1;
+			changed = 1;
+			if(ch >= 16r20 && ch <= 16r3f) {
+				if((ch < 16r30 || ch > 16r39) && ch != ';')
+					s.badp = 1;
+				a := array [len s.fsaved + 1] of byte;
+				a[0:] = s.fsaved[0:];
+				a[len a - 1] = byte ch;
+				s.fsaved = a;
+			} else {
+				valid := 1;
+				case  ch {
+				'A' =>	;
+				'B' =>	;
+				'C' =>	;
+				'D' =>	;
+				'H' =>	;	
+				'J' =>		;
+				'K' =>	; 
+				'P' =>	;
+				'@' =>	;
+				'h' =>	;
+				'l' =>		;	
+				'M' =>	;
+				'L' =>	;
+				* =>
+					valid = 0;
+				}
+				if(s.badp)
+					valid = 0;
+				if(debug['f'])
+					fprint(stderr, "vfilter %d: %s%c\n", valid, string s.fsaved, ch);
+				if(valid) {		# false alarm - don't filter
+					ba = dappend(ba, array [] of { byte ESC, byte '[' });
+					ba = dappend(ba, s.fsaved);
+					ba = dappend(ba, array [] of { byte ch } );
+				}
+				s.fstate = FSstart;
+			} 
+		FS2022 =>	;
+		}
+	}
+	if(changed) {
+		if(i > d0)
+			ba = dappend(ba, data[d0:i]);
+		return ba;
+	}
+	return array [] of { data };
+}
+
+# filter the specified ISO6429 and ISO2022 codes from the screen input - Videotex
+mfilter(s: ref Screen, data: array of byte): array of array of byte
+{
+	ba := array [0] of array of byte;
+	changed := 0;
+
+	d0 := 0;
+	for(i:=0; i<len data; i++) {
+		ch := int data[i];
+		case s.fstate {
+		FSstart =>
+			case ch {
+			ESC =>
+				s.fstate = FSesc;
+				changed = 1;
+				if(i > d0)
+					ba = dappend(ba, data[d0:i]);
+				d0 = i+1;
+			SEP =>
+				s.fstate = FSsep;
+				changed = 1;
+				if(i > d0)
+					ba = dappend(ba, data[d0:i]);
+				d0 = i+1;
+			}
+		FSesc =>
+			d0 = i+1;
+			changed = 1;
+			if(ch == '[') {
+				s.fstate = FS6429;
+				s.fsaved = array [0] of byte;
+				s.badp = 0;
+			} else if(ch == ESC) {
+				ba = dappend(ba, array [] of { byte ESC });
+				s.fstate = FSesc;
+			} else {
+				# false alarm - don't filter
+				ba = dappend(ba, array [] of { byte ESC, byte ch });
+				s.fstate = FSstart;
+			}
+		FSsep =>
+			d0 = i+1;
+			changed = 1;
+			if(ch == ESC) {
+				ba = dappend(ba, array [] of { byte SEP });
+				s.fstate = FSesc;
+			} else if(ch == SEP) {
+				ba = dappend(ba, array [] of { byte SEP });
+				s.fstate = FSsep;
+			} else {
+				if(ch >= 16r00 && ch <= 16r1f)
+					ba = dappend(ba, array [] of { byte SEP , byte ch });
+				# consume the character
+				s.fstate = FSstart;
+			}
+		FS6429 =>	# filter out invalid CSI sequences
+			d0 = i+1;
+			changed = 1;
+			if(ch >= 16r20 && ch <= 16r3f) {
+				if((ch < 16r30 || ch > 16r39) && ch != ';' && ch != '?')
+					s.badp = 1;
+				a := array [len s.fsaved + 1] of byte;
+				a[0:] = s.fsaved[0:];
+				a[len a - 1] = byte ch;
+				s.fsaved = a;
+			} else {
+				valid := 1;
+				case  ch {
+				'm' =>	;
+				'A' =>	;
+				'B' =>	;
+				'C' =>	;
+				'D' =>	;
+				'H' =>	;
+				'J' =>		;
+				'K' =>	; 
+				'@' =>	;
+				'h' =>	;
+				'l' =>		;	
+				'L' =>	;
+				'M' =>	;
+				'P' =>	;
+				'{' =>	# allow CSI ? {
+					n := len s.fsaved;
+					if(n == 0 || s.fsaved[n-1] != byte '?')
+						s.badp = 1;
+				* =>
+					valid = 0;
+				}
+				if(s.badp)	# only decimal params
+					valid = 0;
+				if(debug['f'])
+					fprint(stderr, "mfilter %d: %s%c\n", valid, string s.fsaved, ch);
+				if(valid) {		# false alarm - don't filter
+					ba = dappend(ba, array [] of { byte ESC, byte '[' });
+					ba = dappend(ba, s.fsaved);
+					ba = dappend(ba, array [] of { byte ch } );
+				}
+				s.fstate = FSstart;
+			} 
+		FS2022 =>	;
+		}
+	}
+	if(changed) {
+		if(i > d0)
+			ba = dappend(ba, data[d0:i]);
+		return ba;
+	}
+	return array [] of { data };
+}
+
+# filter the specified ISO6429 and ISO2022 codes from the screen input - Videotex
+afilter(nil: ref Screen, data: array of byte): array of array of byte
+{
+	return array [] of { data };
+}
+
+# append to an array of array of byte
+dappend(ba: array of array of byte, b: array of byte): array of array of byte
+{
+	l := len ba;
+	na := array [l+1] of array of byte;
+	na[0:] = ba[0:];
+	na[l] = b;
+	return na;
+}
+
+# Put a diagnostic string to row 0
+Screen.msg(s: self ref Screen, str: string)
+{
+	blank := string array [s.cols -4] of {* => byte ' '};
+	n := len str;
+	if(n > s.cols - 4)
+		n = s.cols - 4;
+	disp->Put(blank, Point(1, 0), videotex, 0, 0);
+	if(str != nil)
+		disp->Put(str[0:n], Point(1, 0), videotex, fgWhite|attrB, 0);
+	disp->Refresh();
+}
--- /dev/null
+++ b/appl/examples/minitel/socket.b
@@ -1,0 +1,49 @@
+#
+# Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+#
+
+Socket: adt {
+	m:		ref Module;		# common attributes
+	in:		chan of ref Event;
+
+	init:		fn(c: self ref Socket);
+	reset:	fn(c: self ref Socket);
+	run:		fn(c: self ref Socket);
+	quit:		fn(c: self ref Socket);
+};
+
+Socket.init(c: self ref Socket)
+{
+	c.in = chan of ref Event;
+	c.reset();
+}
+
+Socket.reset(c: self ref Socket)
+{
+	c.m = ref Module(Pscreen, 0);
+}
+
+Socket.run(c: self ref Socket)
+{
+Runloop:
+	for(;;){
+		ev := <- c.in;
+		pick e := ev {
+		Equit =>
+			break Runloop;
+		Eproto =>
+			case e.cmd {
+			Creset =>
+				c.reset();
+			* => break;
+			}
+		Edata =>
+		}
+	}
+	send(nil);	
+}
+
+Socket.quit(c: self ref Socket)
+{
+	if(c==nil);
+}
--- /dev/null
+++ b/appl/examples/minitel/swkeyb.b
@@ -1,0 +1,370 @@
+###
+### This data and information is not to be used as the basis of manufacture,
+### or be reproduced or copied, or be distributed to another party, in whole
+### or in part, without the prior written consent of Lucent Technologies.
+###
+### (C) Copyright 1997 Lucent Technologies
+###
+### Written by N. W. Knauft
+###
+#
+# Revisions Copyright © 1998 Vita Nuova Limited.
+
+implement Keyboard;
+
+include "sys.m";
+        sys: Sys;
+
+include "draw.m";
+        draw: Draw;
+
+include "tk.m";
+        tk: Tk;
+
+include "tkclient.m";
+        tkclient: Tkclient;
+
+include "swkeyb.m";
+
+#Icon path
+ICPATH: con "keybd/";
+
+#Font
+FONT: con "/fonts/lucidasans/latin1.7.font";
+SPECFONT: con "/fonts/lucidasans/latin1.6.font";
+
+# Dimension constants
+KBDWIDTH: con 360;
+KBDHEIGHT: con 120;
+KEYSIZE: con "19";
+KEYSPACE: con 5;
+KEYBORDER: con 1;
+KEYGAP: con KEYSPACE - (2 * KEYBORDER);
+ENDGAP: con 2 - KEYBORDER;
+
+# Row size constants (cumulative)
+ROW1: con 14;
+ROW2: con 28;
+ROW3: con 41;
+ROW4: con 53;
+NKEYS: con 63;
+
+#Special key number constants
+DELKEY: con 13;
+TABKEY: con 14;
+BACKSLASHKEY: con 27;
+CAPSLOCKKEY: con 28 ;
+RETURNKEY: con 40;
+LSHIFTKEY: con 41;
+RSHIFTKEY: con 52;
+ESCKEY: con 53;
+CTRLKEY: con 54;
+METAKEY: con 55;
+ALTKEY: con 56;
+SPACEKEY: con 57;
+ENTERKEY: con 58;
+LEFTKEY: con 59;
+RIGHTKEY: con 60;
+DOWNKEY: con 61;
+UPKEY: con 62;
+
+#Special key code constants
+CAPSLOCK: con -1 ;
+SHIFT: con -2;
+CTRL: con -3;
+ALT: con -4;
+META: con -5;
+MAGIC_PREFIX: con 256;
+ARROW_OFFSET: con 57344;
+ARROW_PREFIX: con ARROW_OFFSET + 18;
+
+#Special key width constants
+DELSIZE: con 44;
+TABSIZE: con 32;
+BACKSLASHSIZE: con 31;
+CAPSLOCKSIZE: con 44;
+RETURNSIZE: con 43;
+LSHIFTSIZE: con 56;
+RSHIFTSIZE: con 55;
+ESCSIZE: con 21;
+CTRLSIZE: con 23;
+METASIZE: con 38;
+ALTSIZE: con 22;
+SPACESIZE: con 100;
+ENTERSIZE: con 31;
+
+#Arrow key code constants
+UP: con ARROW_PREFIX;
+DOWN: con ARROW_PREFIX + 1;
+LEFT: con ARROW_PREFIX + 2;
+RIGHT: con ARROW_PREFIX + 3;
+
+direction:= array[] of {"up", "down", "left", "right"};
+row_dimensions:= array[] of {0, ROW1, ROW2, ROW3, ROW4, NKEYS};
+
+special_keys:= array[] of {
+	(DELKEY, DELSIZE),
+	(TABKEY, TABSIZE),
+	(BACKSLASHKEY, BACKSLASHSIZE),
+	(CAPSLOCKKEY, CAPSLOCKSIZE),
+	(RETURNKEY, RETURNSIZE),
+	(LSHIFTKEY, LSHIFTSIZE),
+	(RSHIFTKEY, RSHIFTSIZE),
+	(ESCKEY, ESCSIZE),
+	(CTRLKEY, CTRLSIZE),
+	(METAKEY, METASIZE),
+	(ALTKEY, ALTSIZE),
+	(SPACEKEY, SPACESIZE),
+	(ENTERKEY, ENTERSIZE),
+};
+
+keys:= array[] of {
+	# Unshifted
+	"`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Delete",
+	"Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\\\",
+	"CapLoc", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "\'", "Return",
+	"Shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "Shift",
+	"Esc", "Ctrl", " ", "Alt", " ", "Enter", "<-", "->", "v", "^",
+	# Shifted
+	"~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+", "Delete",
+	"Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\\{", "\\}", "|",
+	"CapLoc", "A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "\"", "Return",
+	"Shift", "Z", "X", "C", "V", "B", "N", "M", "<", ">", "?", "Shift",
+	"Esc", "Ctrl", " ", "Alt", " ", "Enter", "<-", "->", "v", "^",
+};
+
+keyvals:= array[] of {
+	# Unshifted
+	'`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b',
+	'\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\',
+	CAPSLOCK, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '\n',
+	SHIFT, 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', SHIFT,
+	27, CTRL, META, ALT, 32, '\n', LEFT, RIGHT, DOWN, UP,
+	# Shifted
+	'~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '\b',
+	'\t', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|',
+	CAPSLOCK, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '\n',
+	SHIFT, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', SHIFT,
+	27, CTRL, META, ALT, 32, '\n', LEFT, RIGHT, DOWN, UP,
+};
+
+rowlayout := array[] of {
+	"frame .f1",
+	"frame .f2",
+	"frame .f3",
+	"frame .f4",
+	"frame .f5",
+	"frame .dummy0 -height " + string (ENDGAP),
+	"frame .dummy1 -height " + string KEYGAP,
+	"frame .dummy2 -height " + string KEYGAP,
+	"frame .dummy3 -height " + string KEYGAP,
+	"frame .dummy4 -height " + string KEYGAP,
+	"frame .dummy5 -height " + string (ENDGAP + 1),
+};
+
+# Move key flags
+move_key_enabled := 0;
+meta_active := 0;
+
+# Create keyboard widget, spawn keystroke handler
+initialize(t: ref Tk->Toplevel, ctxt : ref Draw->Context, dot: string): chan of string
+{
+	return chaninit(t, ctxt, dot, chan of string);
+}
+
+chaninit(t: ref Tk->Toplevel, ctxt : ref Draw->Context, dot: string, rc: chan of string): chan of string
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+
+	tkclient->init();
+
+	tk->cmd(t, "frame " + dot + " -bd 2 -relief raised -width " + string KBDWIDTH 
+		+ " -height " + string KBDHEIGHT);
+	tkcmds(t, rowlayout);
+
+	for(i := 0; i < NKEYS; i++) {
+		tk->cmd(t, "button .b" + string i + " -font " + FONT + " -width " + KEYSIZE
+		    + " -height " + KEYSIZE + " -bd " + string KEYBORDER);
+
+		tk->cmd(t, ".b" + string i + " configure -text {" + keys[i] +
+					"} -command 'send keypress " + string keyvals[i]);
+	}
+
+	for(i = 0; i < len special_keys; i++) {
+		(keynum, keysize) := special_keys[i];
+		tk->cmd(t, ".b" + string keynum + " configure -font " + SPECFONT + " -width " + string keysize);
+	}
+
+	tk->cmd(t, "image create bitmap Capslock_on -file " + ICPATH + "capson.bit -maskfile " + ICPATH + "capson.bit");
+	tk->cmd(t, "image create bitmap Capslock_off -file " + ICPATH + "capsoff.bit -maskfile " + ICPATH + "capsoff.bit");
+	tk->cmd(t, "image create bitmap Left_arrow -file " + ICPATH + "larrow.bit -maskfile " + ICPATH + "larrow.bit");
+	tk->cmd(t, "image create bitmap Right_arrow -file " + ICPATH + "rarrow.bit -maskfile " + ICPATH + "rarrow.bit");
+	tk->cmd(t, "image create bitmap Down_arrow -file " + ICPATH + "darrow.bit -maskfile " + ICPATH + "darrow.bit");
+	tk->cmd(t, "image create bitmap Up_arrow -file " + ICPATH + "uarrow.bit -maskfile " + ICPATH + "uarrow.bit");
+	tk->cmd(t, "image create bitmap Move_on -file " + ICPATH + "moveon.bit -maskfile " + ICPATH + "moveon.bit");
+	tk->cmd(t, "image create bitmap Move_off -file " + ICPATH + "moveoff.bit -maskfile " + ICPATH + "moveoff.bit");
+	tk->cmd(t, "image create bitmap None -file " + ICPATH + "none.bit -maskfile " + ICPATH + "none.bit");
+	tk->cmd(t, ".b" + string CAPSLOCKKEY + " configure -image Capslock_off");
+	tk->cmd(t, ".b" + string LEFTKEY + " configure -image Left_arrow");
+	tk->cmd(t, ".b" + string RIGHTKEY + " configure -image Right_arrow");
+	tk->cmd(t, ".b" + string DOWNKEY + " configure -image Down_arrow");
+	tk->cmd(t, ".b" + string UPKEY + " configure -image Up_arrow");
+
+	for(j:=1; j < len row_dimensions; j++) {
+		rowstart := row_dimensions[j-1];
+		rowend := row_dimensions[j];
+		for(i=rowstart; i<rowend; i++) {
+			if (i == rowstart) {
+				tk->cmd(t, "frame .f" + string j + ".dummy -width " + string ENDGAP);
+				tk->cmd(t, "pack .f" + string j + ".dummy -side left");
+			}
+			tk->cmd(t, "pack .b" + string i + " -in .f" + string j + " -side left");
+			if (i == rowend-1)
+				tk->cmd(t, "frame .f" + string j + ".dummy" + string i + " -width " + string ENDGAP);
+			else
+				tk->cmd(t, "frame .f" + string j + ".dummy" + string i + " -width " + string KEYGAP);
+			tk->cmd(t, "pack .f" + string j + ".dummy" + string i + " -side left");
+		}
+	}
+
+	tk->cmd(t, "pack .dummy0 .f1 .dummy1 .f2 .dummy2 .f3 .dummy3 .f4 .dummy4 .f5 .dummy5 -in " + dot);
+	tk->cmd(t,"update");
+
+	key := chan of string;
+	spawn handle_keyclicks(t, ctxt, key, rc);
+	return key;
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+	for(i := 0; i < len cmds; i++)
+		tk->cmd(t, cmds[i]);
+}
+
+# Process key clicks and hand keycodes off to Tk
+handle_keyclicks(t: ref Tk->Toplevel, ctxt : ref Draw->Context, sc, rc: chan of string)
+{
+	keypress := chan of string;
+	tk->namechan(t, keypress, "keypress");
+
+	minitel := 0;
+	caps_locked := 0;
+	shifted := 0;
+	ctrl_active := 0;
+	alt_active := 0;
+
+Work:
+	for(;;){
+		alt {
+		k := <-keypress =>
+			(n, cmdstr) := sys->tokenize(k, " \t\n");
+			keycode := int hd cmdstr;
+			case keycode {
+			    CAPSLOCK =>
+				redisplay_keyboard(t, minitel, caps_locked ^= 1, caps_locked);
+				shifted = 0;
+				ctrl_active = 0;
+				alt_active = 0;
+			    SHIFT =>
+				redisplay_keyboard(t, minitel, (shifted ^= 1) ^ caps_locked, caps_locked);
+			    CTRL =>
+				ctrl_active ^= 1;
+				if (shifted) {
+					redisplay_keyboard(t, minitel, caps_locked, caps_locked);
+					shifted = 0;
+				}
+				alt_active = 0;
+			    ALT =>
+				alt_active ^= 1;
+				if (shifted) {
+					redisplay_keyboard(t, minitel, caps_locked, caps_locked);
+					shifted = 0;
+				}
+				ctrl_active = 0;
+			    META =>
+				if (move_key_enabled) {
+					if (meta_active ^= 1)
+						tk->cmd(t, ".b" + string METAKEY + " configure -image Move_on");
+					else
+						tk->cmd(t, ".b" + string METAKEY + " configure -image Move_off");
+				}
+				redisplay_keyboard(t, minitel, caps_locked, caps_locked);
+				shifted = 0;
+				ctrl_active = 0;
+				alt_active = 0;
+			    * =>
+				if (ctrl_active) {
+					keycode &= 16r1F;
+					ctrl_active = 0;
+				} else if (alt_active) {
+					keycode += MAGIC_PREFIX;
+					alt_active = 0;
+				}
+				if (meta_active && UP <= keycode && keycode <= RIGHT) {
+					spawn send_move_msg(direction[keycode - ARROW_PREFIX], sc);
+				} else 
+					tk->keyboard(t, keycode);
+				if (shifted) {
+					redisplay_keyboard(t, minitel, caps_locked, caps_locked);
+					shifted = 0;
+				}
+			}
+		s := <-rc =>
+			case s {
+			"kill" =>
+				break Work;
+			"minitel" =>
+				if (!minitel) {
+					minitel = 1;
+					redisplay_keyboard(t, minitel, shifted, caps_locked);
+				}
+			"standard" =>
+				if (minitel) {
+					minitel = 0;
+					redisplay_keyboard(t, minitel, shifted, caps_locked);
+				}
+			}
+		}
+	}
+}
+
+send_move_msg(dir: string, ch: chan of string)
+{
+	ch <-= dir;
+}
+
+
+# Redisplay keyboard to reflect current state (shifted or unshifted)
+redisplay_keyboard(t: ref Tk->Toplevel, minitel, shifted, caps_locked: int)
+{
+	base: int;
+
+	if (shifted)
+		base = NKEYS;
+	else
+		base = 0;
+
+	for(i:=0; i<NKEYS; i++) {
+		n := base + i;
+		val := keyvals[n];
+		key := keys[n];
+		if (minitel) {
+			if (val >= int 'A' && val <= int 'Z') {
+				key = keys[n-NKEYS];
+			} else if (val >= int 'a' && val <= int 'z') {
+				key = keys[n+NKEYS];
+			}
+		 }
+	
+		tk->cmd(t, ".b" + string i + " configure -text {" + key
+       			     + "} -command 'send keypress " + string val);
+  	}
+	if (caps_locked)
+		tk->cmd(t, ".b" + string CAPSLOCKKEY + " configure -image Capslock_on");
+	else
+		tk->cmd(t, ".b" + string CAPSLOCKKEY + " configure -image Capslock_off");
+	tk->cmd(t, "update");
+}
--- /dev/null
+++ b/appl/examples/minitel/swkeyb.m
@@ -1,0 +1,21 @@
+###
+### This data and information is not to be used as the basis of manufacture,
+### or be reproduced or copied, or be distributed to another party, in whole
+### or in part, without the prior written consent of Lucent Technologies.
+###
+### (C) Copyright 1997 Lucent Technologies
+###
+### Written by N. W. Knauft
+###
+
+# Revisions Copyright © 1998 Vita Nuova Limited.
+
+Keyboard: module
+{
+        PATH:           con "/dis/wm/minitel/swkeyb.dis";
+
+        initialize:     fn(t: ref Tk->Toplevel, ctxt : ref Draw->Context,
+				dot: string): chan of string;
+        chaninit:       fn(t: ref Tk->Toplevel, ctxt : ref Draw->Context,
+				dot: string, rc: chan of string): chan of string;
+};
--- /dev/null
+++ b/appl/grid/README
@@ -1,0 +1,2 @@
+these are just simple demonstrations of possibilities: our production computational grid software
+does not use these programs
--- /dev/null
+++ b/appl/grid/blurdemo.b
@@ -1,0 +1,977 @@
+implement Blurdemo;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Rect, Image: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "readdir.m";
+	readdir: Readdir;
+include "sh.m";
+include "registries.m";
+	registries: Registries;
+	Registry, Attributes, Service: import registries;
+include "grid/pathreader.m";
+	reader: PathReader;
+include "grid/browser.m";
+	browser: Browser;
+	Browse, Select, File, Parameter,
+	DESELECT, SELECT, TOGGLE: import browser;
+include "grid/srvbrowse.m";
+	srvbrowse: Srvbrowse;
+include "grid/announce.m";
+	announce: Announce;
+include "grid/readjpg.m";
+	readjpg: Readjpg;
+
+srvfilter: list of list of (string, string);
+currstep: int;
+
+currsrv: ref Service;
+currattach: ref Registries->Attached;
+ctxt: ref Draw->Context;
+display: ref Draw->Display;
+sysname : string;
+
+IMAGE: con 0;
+MOUNT: con 4;
+
+imgcache: ref Image;
+br: ref Browse;
+sel: ref Select;
+
+Blurdemo : module {
+	init : fn (context : ref Draw->Context, argv : list of string);
+	readpath: fn (dir: File): (array of ref sys->Dir, int);
+};
+
+init(context : ref Draw->Context, argv: list of string)
+{
+	ctxt = context;
+	display = ctxt.display;
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil)
+		badmod(Readdir->PATH);
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmod(Draw->PATH);
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+	tkclient->init();
+	registries = load Registries Registries->PATH;
+	if (registries == nil)
+		badmod(Registries->PATH);
+	registries->init();
+	browser = load Browser Browser->PATH;
+	if (browser == nil)
+		badmod(Browser->PATH);
+	browser->init();
+	srvbrowse = load Srvbrowse Srvbrowse->PATH;
+	if (srvbrowse == nil)
+		badmod(Srvbrowse->PATH);
+	srvbrowse->init();
+	announce = load Announce Announce->PATH;
+	if (announce == nil)
+		badmod(Announce->PATH);
+	announce->init();
+	reader = load PathReader "$self";
+	if (reader == nil)
+		badmod("PathReader");
+	readjpg = load Readjpg Readjpg->PATH;
+	if (readjpg == nil)
+		badmod(Readjpg->PATH);
+	readjpg->init(display);
+	sys->pctl(sys->FORKNS | sys->NEWPGRP, nil);
+	if (ctxt == nil) {
+		sys->print("no draw context found!\n");
+		exit;
+	}
+	sysname = readfile("/dev/sysname");
+	if (sysname == "")
+		sysname = "Localhost";
+	imgcache = nil;
+	setsrvfilter();
+	root := "/";
+	currsrv = nil;
+	
+	attribs := ("resource", "Cpu Pool") :: nil;
+	lcpupool := srvbrowse->find(attribs :: nil);
+	if (lcpupool == nil) {
+		browser->dialog(ctxt, nil, "ok" :: nil, "Alert","Cannot find a Cpu Pool Resource");
+		raise "fail: error cannot find a Cpu Pool resource";
+	}
+
+	(top, titlebar) := tkclient->toplevel(ctxt,"","BlurDemo", tkclient->Appl);
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	browsechan := chan of string;
+	tk->namechan(top, browsechan, "browsechan");
+	selectchan := chan of string;
+	tk->namechan(top, selectchan, "selectchan");
+	br = Browse.new(top, "browsechan", "services/", "Services", 1, reader);
+	bropened := array[] of {
+		"services/",
+		"services/Data source/",
+		"services/Camera/",
+		"/n/remote/",
+		"/" ,
+	};
+	for (i := 0; i < len bropened; i++)
+		br.addopened(File (bropened[i], nil), 1);
+
+	sel = Select.new(top, "selectchan");
+
+	for (ik := 0; ik < len mainscreen; ik++)
+		tkcmd(top,mainscreen[ik]);
+
+	currstep = -1;
+	
+	sel.addframe("image", "Select a '.bit' image");
+
+	changestep(top, IMAGE, nil);
+
+	tkcmd(top, "pack .f -fill both -expand 1; pack propagate . 0");
+	released := 1;
+	title := "";
+	resize(top, ref Rect ((0,0), (400,400)));
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	tkpath: string;
+	selected := array[2] of File;
+	if (tl argv != nil)
+		spawn initimg(butchan, hd tl argv);
+
+	main: for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <-browsechan =>
+			(nil, lst) := sys->tokenize(inp, " \n\t");
+			if (len lst > 1)
+				tkpath = hd tl lst;
+			selected[0] = br.getselected(0);
+			selected[1] = br.getselected(1);
+			br.defaultaction(lst, nil);
+			i = -1;
+			if (!File.eq(selected[0], br.getselected(0)))
+				i = 0;
+			if (!File.eq(selected[1], br.getselected(1)))
+				i = 1;
+			if (i != -1) {
+				sel.select(sel.currfname,nil,DESELECT);
+				actionbutton(top, br.selected[i].file.path, br.selected[i].tkpath);
+			}
+			tkcmd(top, "update");
+		inp := <-selectchan =>
+			(nil, lst) := sys->tokenize(inp, " \n\t");
+			case hd lst {
+				"but3" =>
+					tkpath = hd tl lst;
+					x := string (int hd tl tl lst - 5);
+					y := string (int hd tl tl tl lst - 5);
+
+					path := tkcmd(top, tkpath+" cget -text");
+					s := blursrvc.attrs.get("name") + " ("+blursrvc.addr+")";
+					tk->cmd(top, "destroy .m2");
+					tkcmd(top, "menu .m2 -font /fonts/charon/plain.normal.font");
+					tkcmd(top, ".m2 add command -label {"+path+"}");
+					tkcmd(top, ".m2 add separator");
+					tkcmd(top, ".m2 add command -label {"+s+"}");
+					tkcmd(top, ".m2 post "+x+" "+y);					
+				"double1" =>
+					tkpath = hd tl lst;
+					path := tkcmd(top, tkpath+" cget -text");
+					qid := "";
+					(n, nil) := sys->tokenize(path, "/");
+					if (currstep == IMAGE) {
+						qid = srvbrowse->getqid(blursrvc);
+						(res,name) := srvbrowse->getresname(blursrvc);
+						path = "services/"+res+"/"+name+"/";
+					}
+					else if (currsrv.addr != blursrvc.addr)
+						break;
+					else if (blursrvc.addr != "Local Machine")
+						path = "/n/remote" + path;
+					tkpath = br.gotoselectfile(File(path,qid));
+					if (tkpath != nil) {
+						sel.select(sel.currfname, nil, DESELECT);
+						actionbutton(top, path, tkpath);
+					}
+				"but1" =>
+					if (currstep == IMAGE)
+						br.selectfile(0, DESELECT, File (nil, nil), nil);
+					else
+						br.selectfile(1, DESELECT, File (nil, nil), nil);
+					sel.defaultaction(lst);
+					actionbutton(top, sel.getselected(sel.currfname), hd tl lst);
+				* =>
+					sel.defaultaction(lst);
+			}
+			tkcmd(top, "update");
+		inp := <-butchan =>
+			# sys->print("inp: %s\n",inp);
+			(nil, lst) := sys->tokenize(inp, " \n\t");
+			if (len lst > 1)
+				tkpath = hd tl lst;
+			case hd lst {
+				"refresh" =>
+					# ! check to see if anything is mounted first
+					if (currstep == IMAGE) {
+						# addlocalservice();
+						srvbrowse->refreshservices(srvfilter);
+					}
+					br.refresh();
+				"back" =>
+					changestep(top, IMAGE, nil);
+				"run" =>
+					spawn run(ctxt, getcoords(top));
+				"preview" =>
+					spawn previewwin(top, butchan, hd tl lst);
+				"add" =>
+					additem(top, hd tl lst, int hd tl tl lst);
+				"del" =>
+					sel.delselection("image", hd tl lst);
+					tkcmd (top, ".f.ftop.bn configure -state disabled");
+					blurimage = nil;
+					blurtkpath = nil;
+					blursrvc = nil;
+					actionbutton(top, sel.getselected(sel.currfname), hd tl lst);
+				"mount" =>
+					file := br.getpath(tkpath);
+					(nsrv, lsrv) := sys->tokenize(file.path, "/");
+					if (currstep != IMAGE)
+						break;
+					if (nsrv != 3)
+						break;
+					if (hd tl tl lsrv != "Local Filestore") {
+						ok := mountsrv(file.path, file.qid, getcoords(top));
+						if (!ok)
+							break;
+						changestep(top, MOUNT, hd tl tl lsrv);
+					}
+					else {
+						srv : Service;
+						srv.attrs = Attributes.new(("name", sysname) :: nil);
+						srv.addr = "Local Machine";
+						currsrv = ref srv;
+						changestep(top, MOUNT, hd tl tl lsrv);
+					}
+			}
+			tkcmd(top, "update");
+
+		title = <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <-titlebar =>
+			if (title == "exit")
+				break main;
+			e := tkclient->wmctl(top, title);
+			if (e == nil && title[0] == '!') {
+				(nil, lst) := sys->tokenize(title, " \t\n");
+				if (len lst >= 2 && hd lst == "!size" && hd tl lst == ".")
+					resize(top, nil);
+			}
+		}
+	}
+	currattach = nil;
+	killg(sys->pctl(0,nil));
+}
+
+resize(top: ref Tk->Toplevel, r: ref Draw->Rect)
+{
+	if (r != nil) {
+		sw := (*r).dx();
+		sh := (*r).dy();
+		ww := int tkcmd(top, ". cget -actwidth");
+		wh := int tkcmd(top, ". cget -actheight");
+		if (ww > sw)
+			tkcmd(top, ". configure -x 0 -width "+string sw);
+		if (wh > sh)
+			tkcmd(top, ". configure -y 0 -height "+string sh);
+	}
+	w := int tkcmd(top, ".fselect cget -actwidth");
+	h := int tkcmd(top, ".fselect cget -actheight");
+	sel.resize(w,h);
+}
+
+nactionbuttons := 0;
+actionbutton(top: ref Tk->Toplevel, path, tkpath: string)
+{
+	for (i := 0; i < nactionbuttons; i++) {
+		tkcmd(top, "grid forget .f.ftop.baction"+string i);
+		tkcmd(top, "destroy .f.ftop.baction"+string i);
+	}
+	if (path == nil) {
+		nactionbuttons = 0;
+		return;
+	}
+	buttons : list of (string,string) = nil;
+	(n, nil) := sys->tokenize(path, "/");
+	if (len tkpath > 8 && tkpath[:8] == ".fselect")
+		buttons = ("Remove", "del "+tkpath) :: buttons;
+	else {
+		if (currstep == IMAGE) {
+			if (n == 3)
+				buttons = ("Mount", "mount "+tkpath) :: buttons;
+		}
+		else {
+			if (len path > 4) {
+				if (path[len path - 4:] == ".bit") {
+					buttons = ("Select", "add "+path+" 0") ::
+							("Preview", "preview "+path) :: buttons;
+				}
+				else if (path[len path - 4:] == ".jpg")
+					buttons = ("Select", "add "+path+" 0") :: buttons;
+			}
+		}
+	}
+	nactionbuttons = len buttons;
+	for (i = 0; i < nactionbuttons; i++) {
+		name := ".f.ftop.baction"+string i+" ";
+		(text,cmd) := hd buttons;
+		tkcmd(top, "button "+name+"-text {"+text+"} "+
+				"-font /fonts/charon/bold.normal.font "+
+				"-command {send butchan "+cmd+"}");
+		tkcmd(top, "grid "+name+" -row 0 -column "+string (4+i));
+		buttons = tl buttons;
+	}
+}
+
+initimg(butchan: chan of string, imgpath: string)
+{
+	srv : Service;
+	srv.attrs = Attributes.new(("name", sysname) :: nil);
+	srv.addr = "Local Machine";
+	currsrv = ref srv;
+	butchan <-= "add "+imgpath+" 0";
+	butchan <-= "back";
+}
+
+blurimage := "";
+blurtkpath := "";
+blursrvc: ref Service;
+
+additem(top: ref Tk->Toplevel, path: string, overwrite: int)
+{
+	if (blurimage != nil) {
+		if (overwrite || browser->dialog(ctxt, top, "ok" :: "cancel" :: nil,
+			"Alert","Replace existing image '"
+			+nopath(blurimage)+"' with '"+nopath(path)+"'?") == 0) {
+			sel.delselection("image", blurtkpath);
+		}
+		else
+			return;
+	}
+	imgpath := path;
+	if (currsrv.addr != "Local Machine")
+		path = path[len "/n/remote":];
+	blurtkpath = sel.addselection("image", path, nil, 0);
+	tkcmd(top, "update");
+	blurimage = path;
+	blursrvc = currsrv;
+	if (overwrite)
+		spawn getpreview(blurtkpath, nil, imgcache);
+	else
+		spawn getpreview(blurtkpath, imgpath, nil);
+}
+
+nopath(file: string): string
+{
+	return file[len browser->prevpath(file):];
+}
+
+runscr := array[] of {
+	"frame .f",
+	"frame .f.f1",
+	"label .f.f1.l -text {Select no of CPUs} -font /fonts/charon/plain.normal.font",
+	"scale .f.f1.s -orient horizontal -height 16 -showvalue 0 -from 1 -to 20 -command {.f.f1.ls configure -text}",
+	"label .f.f1.ls -text {1} -font /fonts/charon/plain.normal.font -width 30",
+	"button .f.f1.b -text {Run} -font /fonts/charon/plain.normal.font -command {send butchan go}",
+	"pack .f.f1.l .f.f1.s .f.f1.ls .f.f1.b -side left",
+	"frame .f.f2",
+	"text .f.f2.t -width 250 -height 150 -borderwidth 1 -bg white -font /fonts/charon/plain.normal.font -yscrollcommand { .f.f2.sy set }",
+	"scrollbar .f.f2.sy -command { .f.f2.t yview }",
+	"pack .f.f2.sy -side left -fill y",
+	"pack .f.f2.t -fill both -expand 1",
+	"bind .Wm_t <Button-1> +{focus .Wm_t}",
+	"bind .Wm_t.title <Button-1> +{focus .Wm_t}",
+	"focus .Wm_t",
+	"pack .f.f1 -side top",
+	"pack .f.f2 -fill both -expand 1",
+};
+
+run(ctxt: ref Draw->Context, coords: draw->Rect)
+{
+	(top, titlectl) := tkclient->toplevel(ctxt, "", nil, tkclient->Resize);
+	butchan := chan of string;
+	sync := chan of int;
+	quit := chan of int;
+	tk->namechan(top, butchan, "butchan");
+	tkcmds(top, runscr);
+	tkcmd(top, ". configure "+getcentre(top, coords));
+	tkcmd(top, "pack .f -fill both -expand 1; pack propagate . 0; focus .; update");
+	tkclient->onscreen(top, "exact");
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	done := 1;
+	loop: for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		<-sync =>
+			tkcmd(top, ".f.f1.b configure -state normal; update");
+			done = 1;
+		inp := <-butchan =>
+			(nil, lst) := sys->tokenize(inp, " \n\t");
+			case hd lst {
+				"go" =>
+					tkcmd(top, ".f.f1.b configure -state disabled");
+					ncpus := int tkcmd(top, ".f.f1.s get");
+					done = 0;
+					spawn startit(ncpus, butchan, sync, quit);
+				"output" =>
+					tkcmd(top, ".f.f2.t insert end {"+inp[len "output ":]+"}");
+				"error" =>
+					tkcmd(top, ".f.f2.t insert end {Error: "+inp[len "error ":]+"\n}");
+					tkcmd(top, ".f.f1.b configure -state normal");
+				"fewcpu" =>
+					i := browser->dialog(ctxt, top, "ok" :: "cancel" :: nil, "Alert",
+							"Only found "+hd tl lst+" cpus available. Continue?");
+					quit <-= i;
+					if (i == 1)
+						return;
+			}
+			tkcmd(top, "update");
+		s := <-top.ctxt.ctl or
+		s = <-top.wreq or
+		s = <- titlectl =>
+			if (s == "exit") {
+				if (done)
+					return;
+				break loop;
+			}
+			else
+				tkclient->wmctl(top, s);
+		}
+	}
+	top = nil;
+	for (;;) alt {
+		<- butchan =>
+			;
+		<-sync =>
+			return;
+	}
+}
+
+startit(ncpus: int, butchan: chan of string, sync, quit: chan of int)
+{
+	imgattached : ref Registries->Attached;
+	imgpath := blurimage;
+	if (blursrvc.addr != "Local Machine") {
+		imgattached = blursrvc.attach(nil, nil);
+		if (imgattached == nil) {
+			butchan <-= "error cannot connect to data source: "+blursrvc.attrs.get("name");
+			return;
+		}
+		if (sys->mount(imgattached.fd, nil, "/n/local", sys->MREPL, nil) == -1) {
+			butchan <-= sys->sprint("error img mount failed: %r");
+			return;
+		}
+		imgpath = "/n/local" + imgpath;
+		butchan <-= "output Found image namespace\n";
+	}
+	sys->pctl(sys->FORKNS, nil);
+	attribs := ("resource", "Cpu Pool") :: nil;
+	lsrv := srvbrowse->find(attribs :: nil);
+	if (lsrv == nil) {
+		butchan <-= "error cannot find Cpu Pool resource";
+		return;
+	}
+	cpupoolsrvc := hd lsrv;
+	attached := cpupoolsrvc.attach(nil, nil);
+	if (attached == nil) {
+		butchan <-= "error cannot connect to Cpu Pool resource";
+		return;
+	}
+	if (sys->mount(attached.fd, nil, "/n/remote", sys->MREPL, nil) == -1) {
+		butchan <-= sys->sprint("error Cpu Pool mount failed: %r");
+		return;
+	}
+	butchan <-= "output Connected to Cpu Pool resource\n";
+	if (blurimage[len blurimage - 4:] == ".jpg") {
+		butchan <-= "output Converting jpg => bit image\n";
+		chanin := chan of string;
+		killchan := chan of int;
+		spawn jpgprog(butchan, chanin, killchan);
+		img := readjpg->jpg2img(imgpath, "", chan of string, chanin);
+		killchan <-= 1;
+		butchan <-= "output \n";
+		if (img == nil) {
+			butchan <-= "error Error converting jpg";
+			return;
+		}
+		sys->remove("/n/remote/data/blurimage.bit");
+		fd := sys->create("/n/remote/data/blurimage.bit", sys->OWRITE, 8r666);
+		if (fd == nil || display.writeimage(fd, img) == -1) {
+			butchan <-= sys->sprint("error Error saving bit: %r");
+			return;
+		}
+		imgpath = "/n/remote/data/blurimage.bit";
+	}
+	afd := array[ncpus] of ref sys->FD;
+	ngot := 0;
+	for (i := 0; i < ncpus; i++) {
+		afd[ngot] = sys->open("/n/remote/clone", sys->ORDWR);
+		if (afd[ngot] == nil)
+			break;
+		ngot++;
+	}
+	if (ngot == 0) {
+		butchan <-= "error no cpu resources available";
+		return;
+	}
+	if (ngot < ncpus) {
+		butchan <-= "fewcpu "+string ngot;
+		q := <-quit;
+		if (q)
+			return;
+	}
+	butchan <-= "output Found "+string ngot+" Cpu resource(s)\n";
+	sh := load Sh Sh->PATH;
+	if (sh == nil)
+		badmod(Sh->PATH);
+	sys->create("/n/remote/data/blur", sys->OREAD, 8r777 | sys->DMDIR);
+	done := chan of int;
+	for (i = 0; i < ngot; i++)
+		spawn go(afd[i], i, butchan, done);
+	err := sh->run(ctxt, "/dis/grid/demo/blur.dis" :: "/n/remote/data" :: imgpath :: nil);
+	if (err != nil)
+		butchan <-= "error "+err;
+	finished := 0;
+	for (;;) {
+		<-done;
+		finished++;
+		if (finished == ngot)
+			break;
+	}
+	sys->unmount(nil, "/n/remote");
+	butchan <-= "output Finished\n";
+	sync <-= 1;
+}
+
+jpgprog(butchan, chanin: chan of string, killchan: chan of int)
+{
+	i := 0;
+	for (;;) alt {
+		<-killchan =>
+			return;
+		<-chanin =>
+			i = (i+1) % 2;
+			if (i)
+				butchan <-= "output .";	
+	}
+}
+
+go(fd: ref sys->FD, id: int, butchan: chan of string, done: chan of int)
+{
+	op := "output Cpu "+string id+": ";
+	sys->fprint(fd, "/dis/grid/demo/blur.dis /data/");
+	buf := array[sys->ATOMICIO] of byte;
+	sys->seek(fd, big 0, sys->SEEKSTART);
+	i := sys->read(fd, buf, len buf);
+	if (i < 1)
+		sys->print("Error reading dir name: %r\n");
+	dir := string buf[:i];
+	if (dir[len dir - 1] == '\n')
+		dir = dir[:len dir -1];
+	fdout := sys->open("/n/remote/"+dir+"/data", sys->OREAD);
+	if (fdout == nil) {
+		butchan <-= op+"Cannot read from stdout";
+		done <-= 1;
+		return;
+	}
+	for (;;) {
+		i = sys->read(fdout, buf, len buf);
+		if (i < 1)
+			break;
+		s := string buf[:i];
+		if (s[len s - 1] != '\n')
+			s[len s] = '\n';
+		butchan <-= op+s;
+	}
+	done <-= 1;
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "kill");
+}
+
+killg(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "killgrp");
+}
+
+mainscreen := array[] of {
+	"frame .f",
+	"frame .f.ftop",
+	"variable opt command",
+	"button .f.ftop.bp -text {Services} -command {send butchan back} -font /fonts/charon/bold.normal.font -state disabled -state disabled",
+	"button .f.ftop.bn -text {Run} -command {send butchan run} -font /fonts/charon/bold.normal.font -state disabled",
+	"button .f.ftop.br -text {Refresh} -command {send butchan refresh} -font /fonts/charon/bold.normal.font",
+ 	"grid .f.ftop.br .f.ftop.bp .f.ftop.bn -row 0",
+	"grid columnconfigure .f.ftop 3 -minsize 30",
+	"label .f.l -text { } -height 1 -bg red",
+	"grid .f.l -row 1 -column 0 -sticky ew",
+	"grid .f.ftop -row 0 -column 0 -pady 2 -sticky w",
+	"grid .fbrowse -in .f -row 2 -column 0 -sticky nsew",
+	"grid .fselect -in .f -row 3 -column 0 -sticky nsew",
+	"grid columnconfigure .f 0 -weight 1",
+	"grid rowconfigure .f 2 -weight 1",
+	"grid rowconfigure .f 3 -weight 1",
+
+	"bind .Wm_t <Button-1> +{focus .Wm_t}",
+	"bind .Wm_t.title <Button-1> +{focus .Wm_t}",
+	"focus .Wm_t",
+};
+
+readpath(dir: File): (array of ref sys->Dir, int)
+{
+	if (currstep == MOUNT) {
+		(dirs, nil) := readdir->init(dir.path, readdir->NAME | readdir->COMPACT);
+		dirs2 := array[len dirs] of ref sys->Dir;
+		num := 0;
+		for (i := 0; i < len dirs; i++)
+			if (dirs[i].mode & sys->DMDIR || 
+				(len dirs[i].name > 4 && (
+					dirs[i].name[len dirs[i].name - 4:] == ".bit" || 
+					dirs[i].name[len dirs[i].name - 4:] == ".jpg")))
+				dirs2[num++] = dirs[i];
+		return (dirs2[:num], 0);
+	}
+	else
+		return srvbrowse->servicepath2Dir(dir.path, int dir.qid);
+	return (nil, 0);
+}
+
+badmod(path: string)
+{
+	sys->print("Blurdemo: failed to load %s: %r\n",path);
+	exit;
+}
+
+mountscr := array[] of {
+	"frame .f -borderwidth 2 -relief raised",
+	"text .f.t -width 200 -height 60 -borderwidth 1 -bg white -font /fonts/charon/plain.normal.font",
+	"button .f.b -text {Cancel} -command {send butchan cancel} -width 70 -font /fonts/charon/plain.normal.font",
+	"grid .f.t -row 0 -column 0 -padx 10 -pady 10",
+	"grid .f.b -row 1 -column 0 -sticky n",
+	"grid rowconfigure .f 1 -minsize 30",
+};
+
+mountsrv(srvpath, qid: string, coords: draw->Rect):int
+{
+	(top, nil) := tkclient->toplevel(ctxt, "", nil, tkclient->Plain);
+	ctlchan := chan of string;
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	tkcmds(top, mountscr);
+	tkcmd(top, ". configure "+getcentre(top, coords)+"; pack .f; update");
+	spawn mountit(srvpath, qid, ctlchan);
+	pid := int <-ctlchan;
+	tkclient->onscreen(top, "exact");
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		e := <- ctlchan =>
+			if (e[0] == '!') {
+				tkcmd(top, ".f.t insert end {"+e[1:]+"}");
+				tkcmd(top, ".f.b configure -text {close}; update");
+				pid = -1;
+			}
+			else if (e == "ok")
+				return 1;
+			else
+				tkcmd(top, ".f.t insert end {"+e+"}; update");
+		<- butchan =>
+			if (pid != -1)
+				kill(pid);
+			return 0;
+		}
+	}
+	return 0;
+}
+
+mountit(srvpath, qid: string, ctlchan: chan of string)
+{
+	ctlchan <-= string sys->pctl(0,nil);
+
+	n := 0;
+	(nil, lst) := sys->tokenize(srvpath, "/");
+	stype := hd tl lst;
+	name := hd tl tl lst;
+	addr := "";
+	ctlchan <-= "Connecting...\n";
+	lsrv := srvbrowse->servicepath2Service(srvpath, qid);
+	if (len lsrv < 1) {
+		ctlchan <-= "!could not find service";
+		return;
+	}
+	srvc := hd lsrv;
+	currattach = srvc.attach(nil, nil);
+	if (currattach == nil) {
+		ctlchan <-= "!attach failed";
+		return;
+	}
+	ctlchan <-= "Mounting...\n";
+	if (sys->mount(currattach.fd, nil, "/n/remote", sys->MREPL, nil) != -1) {
+		ctlchan <-= "ok";
+		currsrv = srvc;
+	}
+	else
+		ctlchan <-= "!mount failed";
+}
+
+getcoords(top: ref Tk->Toplevel): draw->Rect
+{
+	h := int tkcmd(top, ". cget -height");
+	w := int tkcmd(top, ". cget -width");
+	x := int tkcmd(top, ". cget -actx");
+	y := int tkcmd(top, ". cget -acty");
+	r := draw->Rect((x,y),(x+w,y+h));
+	return r;
+}
+
+getcentre(top: ref Tk->Toplevel, winr: draw->Rect): string
+{
+	h := int tkcmd(top, ".f cget -height");
+	w := int tkcmd(top, ".f cget -width");
+	midx := winr.min.x + (winr.dx() / 2);
+	midy := winr.min.y + (winr.dy() / 2);
+	newx := midx - (w/2);
+	newy := midy - (h/2);
+	return "-x "+string newx+" -y "+string newy;
+}
+
+changestep(top: ref Tk->Toplevel, step: int, label: string)
+{
+	root, rlabel: string;
+	if (step == MOUNT) {
+		tkcmd (top, ".f.ftop.bp configure -state normal");
+		br.changeview(2);
+			rlabel = label;
+		if (currsrv.addr == "Local Machine")
+			root = "/";
+		else
+			root = "/n/remote/";
+	}
+	else if (step == IMAGE) {
+		br.changeview(1);
+		if (currsrv != nil) {
+			sys->unmount(nil, "/n/remote");
+			currattach = nil;
+			currsrv = nil;
+		}
+		srvbrowse->refreshservices(srvfilter);
+		root = "services/";
+		rlabel = "Image Services";
+		sel.showframe("image");
+		tkcmd (top, ".f.ftop.bp configure -state disabled");
+		# addlocalservice();
+		sel.select("image", nil, DESELECT);
+	}
+	currstep = step;
+	br.selectfile(1, DESELECT, File (nil, nil), nil);
+	br.selectfile(0, DESELECT,File (nil, nil), nil);
+	actionbutton(top, nil, nil);	
+
+	br.newroot(root, rlabel);
+	if (currstep == MOUNT)
+		br.selectfile(0, SELECT, File (root, nil), ".fbrowse.fl.f0.l");
+	tkcmd(top, "update");
+}
+
+addlocalservice()
+{
+	lsrv : Service;
+	attrs := ("resource", "Data source") ::
+		("name", "Local Filestore") ::
+		("type", "styx") :: nil;
+	lsrv.attrs = Attributes.new(attrs);
+	lsrv.addr = "@your local filestore";
+	srvbrowse->addservice(ref lsrv);
+}
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != "" && e[0] == '!')
+		sys->print("Tk error: '%s': %s\n",cmd,e);
+	return e;
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	for (j := 0; j < len a; j++)
+		tkcmd(top, a[j]);
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[8192] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[:n];	
+}
+
+setsrvfilter()
+{
+	imagefilter := ("proto", "styx") :: ("auth", "none") :: ("Image resource", "1") :: nil;
+	srvfilter = imagefilter :: nil;	
+}
+
+getpreview(tkpath, imgpath: string, img: ref Image)
+{
+	if (imgpath != nil && imgpath[len imgpath - 4:] == ".jpg") {
+		tkcmd (sel.top, ".f.ftop.bn configure -state normal");
+		return;
+	}
+	if (img == nil) {
+		img = display.open(imgpath);
+		if (img == nil) {
+			browser->dialog(ctxt, sel.top, "ok" :: nil, "Alert",
+				sys->sprint("Invalid '.bit' image: %r"));
+			sel.delselection("image", blurtkpath);
+			blurimage = nil;
+			blursrvc = nil;
+			return;
+		}
+	}
+	previmg := preview(img, 100);
+	tk->cmd(sel.top, "destroy .preview");
+	tkcmd(sel.top, "image create bitmap .preview");
+	tk->putimage(sel.top, ".preview", previmg, nil);
+	tkcmd(sel.top, sys->sprint("%s configure -image .preview -width %d -height %d",
+		tkpath, previmg.r.dx(), previmg.r.dy()));
+	tkcmd(sel.top, "grid forget "+tkpath+"; grid "+tkpath+" -row 1 "+
+			"-column 0 -columnspan 3 -pady 5 -sticky ew;");
+	sel.setscrollr(sel.currfname);
+	tkcmd (sel.top, ".f.ftop.bn configure -state normal");
+	tkcmd(sel.top, "update;");
+}
+
+preview(img: ref Image, maxsize: int): ref Image
+{
+	mx := max(img.r.dx(), img.r.dy());
+	if (mx <= maxsize) {
+		imgcache = img;
+		return img;
+	}
+	prevr := Rect ((0,0), (img.r.dx()*maxsize/mx, img.r.dy()*maxsize/mx));
+	tmpimg := display.newimage(img.r, Draw->RGB24, 0, Draw->White);
+	previmg := display.newimage(prevr, Draw->RGB24, 0, Draw->White);
+	tmpimg.draw(img.r, img, nil, (0,0));
+
+	getr := Rect ((0,0), (img.r.dx() / prevr.dx(), img.r.dy() / prevr.dy()));
+
+	nopixels := getr.dx() * getr.dy();
+	getrgb := array[nopixels * 3] of byte;
+	newrgb := array[3] of byte;
+	for (y := 0; y < prevr.dy(); y++) {
+		for (x := 0; x < prevr.dx(); x++) {
+			tmpimg.readpixels(getr.addpt((x*getr.dx(), y*getr.dy())), getrgb);
+			tmprgb := array[] of { 0, 0, 0 };
+			for (i := 0; i < len getrgb; i++)
+				tmprgb[i%3] += int getrgb[i];
+			for (i = 0; i < 3; i++)
+				newrgb[i] = byte (tmprgb[i] / nopixels);
+			previmg.writepixels(((x,y),(x+1,y+1)), newrgb);
+		}
+	}
+	imgcache = previmg;
+	return previmg;
+}
+
+max(a,b: int): int
+{
+	if (a > b)
+		return a;
+	return b;
+}
+
+previewscr := array[] of {
+	"frame .f",
+	"panel .f.p -borderwidth 2 -relief raised",
+	"button .f.bs -text Select -font /fonts/charon/plain.normal.font -command {send prevchan select} -state disabled",
+	"button .f.bc -text Close -font /fonts/charon/plain.normal.font -command {send prevchan close} -state disabled",
+	"pack .f",
+	"grid .f.p -row 0 -column 0 -columnspan 2 -padx 5 -pady 5",
+	"grid .f.bs .f.bc -row 1 -padx 5 -pady 5",
+	"update",
+};
+
+previewwin(oldtop: ref Tk->Toplevel, chanout: chan of string, path: string)
+{
+	(top, titlectl) := tkclient->toplevel(ctxt, "", "Loading...", 0);
+	prevchan := chan of string;
+	tk->namechan(top, prevchan, "prevchan");
+	tkclient->onscreen(top, "exact");
+
+	img := display.open(path);
+	if (img == nil) {
+		browser->dialog(ctxt, oldtop, "ok" :: nil, "Alert", "Invalid '.bit' image");
+		return;
+	}
+	
+	previmg := preview(img, 100);
+	tkcmds(top, previewscr);
+	tk->putimage(top, ".f.p", previmg, nil);
+	tkcmd(top, ".Wm_t.title configure -text Preview");
+	tkcmd(top, ".f.p dirty; update");
+	browser->setcentre(oldtop, top);
+	tkcmd(top, ".f.bs configure -state normal; .f.bc configure -state normal");
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+
+	main: for(;;) alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		s := <- prevchan =>
+			if (s == "select")
+				chanout <-= "add "+path+" 1";
+			break main;
+		s := <-top.ctxt.ctl or
+		s = <-top.wreq or
+		s = <- titlectl =>
+			if (s == "exit")
+				break main;
+			else
+				tkclient->wmctl(top, s);
+	}
+}
--- /dev/null
+++ b/appl/grid/cpupool.b
@@ -1,0 +1,922 @@
+implement CpuPool;
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys : Sys;
+include "daytime.m";
+	daytime: Daytime;
+include "styx.m";
+	styx: Styx;
+	Rmsg, Tmsg: import styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Fid, Navigator, Navop: import styxservers;
+	Styxserver: import styxservers;
+	nametree: Nametree;
+	Tree: import nametree;
+include "draw.m";
+include "dial.m";
+	dial: Dial;
+include "sh.m";
+include "arg.m";
+include "registries.m";
+	registries: Registries;
+	Registry, Attributes, Service: import registries;
+include "grid/announce.m";
+	announce: Announce;
+include "readdir.m";
+	readdir: Readdir;
+
+TEST: con 0;
+
+RUN : con "#!/dis/sh\n" +
+		"load std\n" +
+		"if {~ $#* 0} {\n" +
+		"	echo usage: run.sh cmd args\n"+
+		"	raise usage\n" +
+		"}\n"+
+		"CMD = $*\n" +
+		"{echo $CMD; dir=`{read -o 0}; cat <[0=3] > $dir/data& catpid=$apid;"+
+		" cat $dir/data >[1=4]; kill $catpid >[2] /dev/null} <[3=0] >[4=1] <> clone >[1=0]\n";
+
+EMPTYDIR: con "#//dev";
+rootpath := "/tmp/cpupool/";
+rstyxreg: ref Registry;
+registered: ref Registries->Registered;
+
+CpuSession: adt {
+	proxyid, fid, cpuid, omode, written, finished: int;
+	stdoutopen, stdinopen: int;
+	stdinchan, stdoutchan: chan of array of byte;
+	closestdin,closestdout, readstdout, sync: chan of int;
+	rcmdfinishedstdin, rcmdfinishedstdout: chan of int;
+	fio: ref sys->FileIO;
+	pids: list of int;
+};
+
+NILCPUSESSION: con CpuSession (-1, -1,-1, 0, 0, 0, 0, 0, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil);
+
+cpusession: array of CpuSession;
+poolchanin : chan of string;
+poolchanout : chan of int;
+
+conids : array of int;
+
+CpuPool: module {
+	init: fn (nil : ref Draw->Context, argv: list of string);
+};
+
+init(nil : ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if (daytime == nil)
+		badmod(Daytime->PATH);
+	dial = load Dial Dial->PATH;
+	if (dial == nil)
+		badmod(Dial->PATH);
+	styx = load Styx Styx->PATH;
+	if (styx == nil)
+		badmod(Styx->PATH);
+	styx->init();
+	styxservers = load Styxservers Styxservers->PATH;
+	if (styxservers == nil)
+		badmod(Styxservers->PATH);
+	styxservers->init(styx);
+	nametree = load Nametree Nametree->PATH;
+	if (nametree == nil)
+		badmod(Nametree->PATH);
+	nametree->init();
+	registries = load Registries Registries->PATH;
+	if (registries == nil)
+		badmod(Registries->PATH);
+	registries->init();
+	announce = load Announce Announce->PATH;
+	if (announce == nil)
+		badmod(Announce->PATH);
+	announce->init();
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil)
+		badmod(Readdir->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmod(Arg->PATH);
+	sys->pctl(Sys->FORKNS | sys->NEWPGRP, nil);
+	sys->unmount(nil, "/n/remote");
+	getuid();
+	sys->chdir(EMPTYDIR);
+	cpusession = array[500] of { * => NILCPUSESSION };
+	attrs := Attributes.new(("proto", "styx") :: ("auth", "none") :: ("resource","Cpu Pool") :: nil);
+
+	arg->init(argv);
+	arg->setusage("cpupool [-a attributes] [rootdir]");
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'a' =>
+			attr := arg->earg();
+			val := arg->earg();
+			attrs.set(attr, val);
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	arg = nil;
+	
+	if (argv != nil)
+		rootpath = hd argv;
+	if (rootpath[len rootpath - 1] != '/')
+		rootpath[len rootpath] = '/';
+	(n, dir) := sys->stat(rootpath);
+	if (n == -1 || !(dir.mode & sys->DMDIR))
+		error("Invalid tmp path: "+rootpath);
+
+	rstyxreg = Registry.new("/mnt/rstyxreg");
+	if (rstyxreg == nil)
+		error("Could not find Rstyx Registry");
+
+	reg := Registry.connect(nil, nil, nil);
+	if (reg == nil)
+		error("Could not find registry");
+	(myaddr, c) := announce->announce();
+	if (myaddr == nil)
+		error(sys->sprint("cannot announce: %r"));
+	persist := 0;
+	err: string;
+	(registered, err) = reg.register(myaddr, attrs, persist);
+	if (err != nil) 
+		error("could not register with registry: "+err);
+	conids = array[200] of { * => -1 };
+	poolchanin = chan of string;
+	poolchanout = chan of int;
+	userchan := chan of int;
+	spawn listener(c);
+	spawn cpupoolloop(poolchanin, poolchanout);
+}
+
+attrval(s: string): (string, string)
+{
+	for (i := 0; i < len s; i++) {
+		if (s[i] == '=')
+			return (s[:i], s[i+1:]);
+	}
+	return (nil, s);
+}
+
+uid: string;
+Qroot : con 0;
+Qclone: con 1;
+
+Qdata: con 2;
+Qsh: con 3;
+Qrun: con 4;
+Qcpu: con 5;
+Qsessdir: con 6;
+Qsessdat: con 7;
+
+getuid()
+{
+	buf := array [100] of byte;
+	fd := sys->open("/dev/user", Sys->OREAD);
+	uidlen := sys->read(fd, buf, len buf);
+	uid = string buf[0: uidlen];
+}
+
+dir(name: string, perm: int, length: int, qid: int): Sys->Dir
+{
+	d := sys->zerodir;
+	d.name = name;
+	d.uid = uid;
+	d.gid = uid;
+	d.qid.path = big qid;
+	if (perm & Sys->DMDIR)
+		d.qid.qtype = Sys->QTDIR;
+	else {
+		d.qid.qtype = Sys->QTFILE;
+		d.length = big length;
+	}
+	d.mode = perm;
+	d.atime = d.mtime = daytime->now();
+	return d;
+}
+
+defaultdirs := array[] of {
+	("dis", 1),
+	("dev", 1),
+	("fonts", 1),
+	("mnt", 0),
+	("prog", 0),
+};
+
+serveloop(fd : ref sys->FD, cmdchan: chan of (int, string, chan of int), exitchan, sync: chan of int, proxyid: int)
+{
+	if (TEST)
+		sys->fprint(sys->fildes(2), "starting serveloop");
+	tchan: chan of ref Tmsg;
+	srv: ref Styxserver;
+	(tree, treeop) := nametree->start();
+	tree.create(big Qroot, dir(".",8r555 | sys->DMDIR,0,Qroot));
+	tree.create(big Qroot, dir("clone",8r666,0,Qclone));
+	tree.create(big Qroot, dir("run.sh",8r555,0,Qrun));
+	tree.create(big Qroot, dir("cpu",8r444,0,Qcpu));
+	tree.create(big Qroot, dir("data",8r777 | sys->DMDIR,0,Qdata));
+	tree.create(big Qroot, dir("runtime",8r444 | sys->DMDIR,0,Qsh));
+
+	for (i := 0; i < len defaultdirs; i++)
+		tree.create(big Qroot, dir(defaultdirs[i].t0,8r555 | sys->DMDIR ,0,8 + (i<<4)));
+
+	(tchan, srv) = Styxserver.new(fd,Navigator.new(treeop), big Qroot);
+	fd = nil;
+	datafids : list of Datafid = nil;
+	sync <-= 1;
+	gm: ref Tmsg;
+	loop: for (;;) {
+		alt {
+		<-exitchan =>
+			break loop;
+	
+		gm = <-tchan =>
+		
+		if (gm == nil)
+			break loop;
+		# sys->fprint(sys->fildes(2), "Got new GM %s tag: %d\n", gm.text(), gm.tag);
+
+		pick m := gm {
+		Readerror =>
+			sys->fprint(sys->fildes(2), "cpupool: fatal read error: %s\n", m.error);
+			exit;
+		Clunk =>
+			deldf: Datafid;
+			(datafids, deldf) = delfid(datafids, m.fid);
+			if (deldf.sessid != -1) {
+				if (deldf.omode == sys->OREAD || deldf.omode == sys->ORDWR)
+					cpusession[deldf.sessid].sync <-= STDOUTCLOSE;
+				else if (deldf.omode == sys->OWRITE || deldf.omode == sys->ORDWR)
+					cpusession[deldf.sessid].sync <-= STDINCLOSE;
+			}
+			else {	
+				sessid := getsession(m.fid);
+				if (sessid != -1)
+					cpusession[sessid].sync <-= CLONECLOSE;
+			}
+			srv.default(gm);
+		Open =>
+			(f, nil, d, err) := srv.canopen(m);
+			if(f == nil) {
+				srv.reply(ref Rmsg.Error(m.tag, err));
+				break;
+			}
+			ind := int f.uname;
+			mode := m.mode & 3;
+			case int f.path  & 15 {
+				Qclone =>
+					if (mode == sys->OREAD) {
+						srv.reply(ref Rmsg.Error(m.tag, "ctl cannot be open as read only"));
+						break;
+					}
+					poolchanin <-= "request";
+					cpuid := <-poolchanout;
+					if (cpuid == -1)
+						srv.reply(ref Rmsg.Error(m.tag, "no free resources"));
+					else {
+						sessid := getsession(-1);
+						cpusession[sessid].fid = m.fid;
+						cpusession[sessid].cpuid = cpuid;
+						cpusession[sessid].omode = mode;
+						cpusession[sessid].sync = chan of int;
+						cpusession[sessid].proxyid = proxyid;
+						spawn sessionctl(sessid, tree);
+						Qdir := Qsessdir | (sessid<<4);
+						tree.create(big Qroot, dir(string sessid,
+							8r777 | sys->DMDIR,0, Qdir));
+						tree.create(big Qdir, dir("data",	8r666,0, Qsessdat | (sessid<<4)));
+						if (TEST)
+							sys->fprint(sys->fildes(2), "New Session %d\n\tcpuid: %d\n"
+								,sessid,cpuid);
+						srv.default(gm);
+					}
+				Qsessdat =>
+					err = "";
+					sessid := (int f.path)>>4;					
+					datafids = addfid(datafids, Datafid(sessid, m.fid, mode));
+					if (cpusession[sessid].finished)
+						err = "session already finished";
+					else if (mode == sys->OREAD || mode == sys->ORDWR) {
+						if (cpusession[sessid].stdoutopen == -1)
+							err = "pipe closed";
+						else
+							cpusession[sessid].sync <-= STDOUTOPEN;
+					}
+					else if (mode == sys->OWRITE || mode == sys->ORDWR) {
+						if (cpusession[sessid].stdinopen == -1)
+							err = "pipe closed";
+						else
+							cpusession[sessid].sync <-= STDINOPEN;
+					}
+					# sys->fprint(sys->fildes(2), 
+					#		"Open: Data: sessid %d, stdout %d stdin %d: err: '%s'\n",
+					#		sessid,cpusession[sessid].stdoutopen,
+					#		cpusession[sessid].stdinopen, err);
+					if (err == nil)
+						srv.default(gm);
+					else
+						srv.reply(ref Rmsg.Error(m.tag, err));
+				* =>
+					# sys->print("Open: %s tag: %d\n", gm.text(), gm.tag);
+					srv.default(gm);
+			}
+		Write =>
+			(f,e) := srv.canwrite(m);
+			if(f == nil) {
+				# sys->print("breaking! %r\n");
+				break;
+			}
+			case int f.path & 15 {
+				Qsessdat =>
+					sessid := (int f.path)>>4;
+					# sys->fprint(sys->fildes(2), "Write: Data %d len: %d\n",
+					#	sessid,len m.data);
+					spawn datawrite(sessid,srv,m);
+				Qclone =>
+					sessid := getsession(m.fid);
+					# sys->fprint(sys->fildes(2), "Write: clone %d\n",sessid);
+					spawn clonewrite(sessid,srv, m, cmdchan);
+				* =>
+					srv.default(gm);					
+			}
+
+		Read =>
+			(f,e) := srv.canread(m);
+			if(f == nil)
+				break;
+			case int f.path & 15 {
+				Qclone =>
+					sessid := getsession(m.fid);
+					# sys->fprint(sys->fildes(2), "Read: clone %d\n",sessid);
+					srv.reply(styxservers->readbytes(m, array of byte (string sessid + "\n")));
+				Qsessdat =>
+					sessid := (int f.path)>>4;
+					# sys->fprint(sys->fildes(2), "Read: data session: %d\n",sessid);
+					if (cpusession[sessid].finished)
+						srv.reply(ref Rmsg.Error(m.tag, "session finished"));
+					else
+						spawn dataread(sessid, srv, m);
+				Qrun =>
+					srv.reply(styxservers->readbytes(m, array of byte RUN));
+				Qcpu =>
+					poolchanin <-= "refresh";
+					s := (string ncpupool) + "\n";
+					srv.reply(styxservers->readbytes(m, array of byte s));
+				* =>
+					srv.default(gm);					
+			}
+
+		* =>
+			srv.default(gm);
+		}
+		}
+	}
+	if (TEST)
+		sys->fprint(sys->fildes(2), "leaving serveloop...\n");
+	tree.quit();
+	for (i = 0; i < len cpusession; i++) {
+		if (cpusession[i].proxyid == proxyid) {
+			#Tear it down!
+			if (TEST)
+				sys->fprint(sys->fildes(2), "Killing off session %d\n",i);
+			poolchanin <-= "free "+string cpusession[i].cpuid;
+			for (; cpusession[i].pids != nil; cpusession[i].pids = tl cpusession[i].pids)
+				kill(hd cpusession[i].pids);
+			cpusession[i] = NILCPUSESSION;
+		}
+	}
+	if (TEST)
+		sys->fprint(sys->fildes(2), "serveloop exited\n");
+}
+
+dataread(sessid: int, srv: ref Styxserver, m: ref Tmsg.Read)
+{
+	cpusession[sessid].readstdout <-= 1;
+	data := <- cpusession[sessid].stdoutchan;
+	srv.reply(ref Rmsg.Read(m.tag, data));
+}
+
+datawrite(sessid: int, srv: ref Styxserver, m: ref Tmsg.Write)
+{
+	# sys->fprint(sys->fildes(2), "Writing to Stdin %d (%d)\n'%s'\n",
+	#	len m.data, m.tag, string m.data);
+	cpusession[sessid].stdinchan <-= m.data;
+	# sys->fprint(sys->fildes(2), "Written to Stdin %d!\n",m.tag);
+	srv.reply(ref Rmsg.Write(m.tag, len m.data));
+}
+
+clonewrite(sessid: int, srv: ref Styxserver, m: ref Tmsg.Write, cmdchan: chan of (int, string, chan of int))
+{
+	if (cpusession[sessid].written) {
+		srv.reply(ref Rmsg.Error(m.tag, "session already started"));
+		return;
+	}
+	rc := chan of int;
+	cmdchan <-= (sessid, string m.data, rc);
+	i := <-rc;
+	# sys->fprint(sys->fildes(2), "Sending write\n");
+	srv.reply(ref Rmsg.Write(m.tag, i));
+}
+
+badmod(path: string)
+{
+	sys->fprint(sys->fildes(1), "error CpuPool: failed to load: %s\n",path);
+	exit;
+}
+
+listener(c: ref Sys->Connection)
+{
+	for (;;) {
+		nc := dial->listen(c);
+		if (nc == nil)
+			error(sys->sprint("listen failed: %r"));
+		dfd := dial->accept(nc);
+		if (dfd != nil) {
+			sync := chan of int;
+			sys->print("got new connection!\n");
+			spawn proxy(sync, dfd);
+			<-sync;
+		}
+	}
+}
+
+proxy(sync: chan of int, dfd: ref Sys->FD)
+{
+	proxypid := sys->pctl(0, nil);
+	sys->pctl(sys->FORKNS, nil);
+	sys->chdir(EMPTYDIR);
+	sync <-= 1;
+
+	sync = chan of int;
+	fds := array[2] of ref sys->FD;
+	sys->pipe(fds);
+	cmdchan := chan of (int, string, chan of int);
+	exitchan := chan of int;
+	killsrvloop := chan of int;
+	spawn serveloop(fds[0], cmdchan, killsrvloop, sync, proxypid);
+	<-sync;
+
+	if (sys->mount(fds[1], nil, "/n/remote", Sys->MREPL | sys->MCREATE, nil) == -1)
+		error(sys->sprint("cannot mount mountfd: %r"));
+
+	conid := getconid(-1);
+	conids[conid] = 1;
+	setupworkspace(conid);
+	
+	spawn exportns(dfd, conid, exitchan);
+	for (;;) alt {
+		(sessid, cmd, reply) := <-cmdchan =>
+			spawn runit(conid, sessid, cmd, reply);
+		e := <-exitchan =>
+			killsrvloop <-= 1;
+			return;
+	}
+}
+
+getconid(id: int): int
+{
+	for (i := 0; i < len conids; i++)
+		if (conids[i] == id)
+			return i;
+	return -1;
+}
+
+exportns(dfd: ref Sys->FD, conid: int, exitchan: chan of int)
+{
+	sys->export(dfd, "/n/remote", sys->EXPWAIT);
+	if (TEST)
+		sys->fprint(sys->fildes(2), "Export Finished!\n");
+	conids[conid] = -1;
+	exitchan <-= 1;
+}
+
+error(e: string)
+{
+	sys->fprint(sys->fildes(2), "CpuPool: %s: %r\n", e);
+	raise "fail:error";
+}
+
+setupworkspace(pathid: int)
+{
+	path := rootpath + string pathid;
+	sys->create(path, sys->OREAD, 8r777 | sys->DMDIR);
+	delpath(path, 0);
+	sys->create(path + "/data", sys->OREAD, 8r777 | sys->DMDIR);
+	if (sys->bind(path+"/data", "/n/remote/data",
+			sys->MREPL | sys->MCREATE) == -1)
+		sys->fprint(sys->fildes(2), "data bind error %r\n");
+	sys->create(path + "/runtime", sys->OREAD, 8r777 | sys->DMDIR);
+	if (sys->bind(path+"/runtime", "/n/remote/runtime", sys->MREPL) == -1)
+		sys->fprint(sys->fildes(2), "runtime bind error %r\n");
+	for (i := 0; i < len defaultdirs; i++) {
+		if (defaultdirs[i].t1 == 1) {
+			sys->create(path+"/"+defaultdirs[i].t0, sys->OREAD, 8r777 | sys->DMDIR);
+			if (sys->bind("/"+defaultdirs[i].t0, 
+					"/n/remote/"+defaultdirs[i].t0, sys->MREPL) == -1)
+				sys->fprint(sys->fildes(2), "dir bind error %r\n");
+		}
+	}
+}
+
+delpath(path: string, incl: int)
+{
+	if (path[len path - 1] != '/')
+		path[len path] = '/';
+	(dirs, n) := readdir->init(path, readdir->NONE | readdir->COMPACT);
+	for (i := 0; i < n; i++) {
+		if (dirs[i].mode & sys->DMDIR)
+			delpath(path + dirs[i].name, 1);
+		else
+			sys->remove(path + dirs[i].name);
+	}
+	if (incl)
+		sys->remove(path);
+}
+
+runit(id, sessid: int, cmd: string, sync: chan of int)
+{
+	# sys->print("got runit!\n");
+	cpusession[sessid].sync <-= PID;
+	cpusession[sessid].sync <-=  sys->pctl(sys->FORKNS, nil);
+	if (!TEST && sys->bind("/net.alt", "/net", sys->MREPL) == -1) {
+			sys->fprint(sys->fildes(2), "cpupool net.alt bind failed: %r\n");
+			sync <-= -1;
+			return;
+	}
+	path := rootpath + string id;
+	runfile := "/runtime/start"+string cpusession[sessid].cpuid+".sh";
+	sh := load Sh Sh->PATH;
+	if(sh == nil) {
+		sys->fprint(sys->fildes(2), "Failed to load sh: %r\n");
+		sync <-= -1;
+		return;
+	}
+
+	sys->remove(path+runfile);
+	fd := sys->create(path+runfile, sys->OWRITE, 8r777);
+	if (fd == nil) {
+		sync <-= -1;
+		return;
+	}
+	sys->fprint(fd, "#!/dis/sh\n");
+	sys->fprint(fd, "bind /prog /n/client/prog\n");
+	sys->fprint(fd, "bind /n/client /\n");
+	sys->fprint(fd, "cd /\n");
+	sys->fprint(fd, "%s\n", cmd);
+
+	if (sys->bind("#s", "/n/remote/runtime", Sys->MBEFORE|Sys->MCREATE) == -1) {
+		sys->fprint(sys->fildes(2), "cpupool: %r\n");
+		return;
+	}
+
+	cpusession[sessid].fio = sys->file2chan("/n/remote/runtime", "mycons");
+	if (cpusession[sessid].fio == nil) {
+		sys->fprint(sys->fildes(2), "cpupool: file2chan failed: %r\n");
+		return;
+	}
+
+	if (sys->bind("/n/remote/runtime/mycons", "/n/remote/dev/cons", sys->MREPL) == -1)
+		sys->fprint(sys->fildes(2), "cons bind error %r\n");
+	cpusession[sessid].written = 1;
+
+	cpusession[sessid].stdinchan = chan of array of byte;
+	cpusession[sessid].closestdin = chan of int;
+	cpusession[sessid].rcmdfinishedstdin = chan of int;
+	spawn devconsread(sessid);
+
+	cpusession[sessid].stdoutchan = chan of array of byte;
+	cpusession[sessid].closestdout = chan of int;
+	cpusession[sessid].readstdout = chan of int;
+	cpusession[sessid].rcmdfinishedstdout = chan of int;
+	spawn devconswrite(sessid);
+
+	# Let it know that session channels have been created & can be listened on...
+	sync <-= len cmd;
+
+	# would prefer that it were authenticated
+	if (TEST)
+		sys->print("ABOUT TO RCMD\n");
+	sh->run(nil, "rcmd" :: "-A" :: "-e" :: "/n/remote" :: 
+				cpupool[cpusession[sessid].cpuid].srvc.addr ::
+				"sh" :: "-c" :: "/n/client"+runfile :: nil);
+	if (TEST)
+		sys->print("DONE RCMD\n");
+
+	sys->remove(path+runfile);
+	sys->unmount(nil, "/n/remote/dev/cons");
+	cpusession[sessid].rcmdfinishedstdin <-= 1;
+	cpusession[sessid].rcmdfinishedstdout <-= 1;
+	cpusession[sessid].sync <-= FINISHED;
+}
+
+CLONECLOSE: con 0;
+FINISHED: con 1;
+STDINOPEN: con 2;
+STDINCLOSE: con 3;
+STDOUTOPEN: con 4;
+STDOUTCLOSE: con 5;
+PID: con -2;
+
+sessionctl(sessid: int, tree: ref Nametree->Tree)
+{
+	cpusession[sessid].pids = sys->pctl(0, nil) :: nil;
+	clone := 1;
+	closed := 0;
+	main: for (;;) {
+		i := <-cpusession[sessid].sync;
+		case i {
+		PID =>
+			pid := <-cpusession[sessid].sync;
+			if (TEST)
+				sys->fprint(sys->fildes(2), "adding PID: %d\n", pid);
+			cpusession[sessid].pids = pid :: cpusession[sessid].pids;
+		STDINOPEN =>
+			cpusession[sessid].stdinopen++;
+			if (TEST)
+				sys->fprint(sys->fildes(2), "%d: Open stdin: => %d\n",
+					sessid, cpusession[sessid].stdinopen);
+		STDOUTOPEN =>
+			cpusession[sessid].stdoutopen++;
+			if (TEST)
+				sys->fprint(sys->fildes(2), "%d: Open stdout: => %d\n",
+					sessid, cpusession[sessid].stdoutopen);
+		STDINCLOSE =>
+			cpusession[sessid].stdinopen--;
+			if (TEST)
+				sys->fprint(sys->fildes(2), "%d: Close stdin: => %d\n",
+					sessid, cpusession[sessid].stdinopen);
+			if (cpusession[sessid].stdinopen == 0) {
+				cpusession[sessid].stdinopen = -1;
+				cpusession[sessid].closestdin <-= 1;
+			}
+			# sys->fprint(sys->fildes(2), "Clunk: stdin (in %d: out %d\n",
+			#	cpusession[sessid].stdinopen, cpusession[sessid].stdoutopen);
+		STDOUTCLOSE =>
+			cpusession[sessid].stdoutopen--;
+			if (TEST)
+				sys->fprint(sys->fildes(2), "%d: Close stdout: => %d\n",
+					sessid, cpusession[sessid].stdoutopen);
+			if (cpusession[sessid].stdoutopen == 0) {
+				cpusession[sessid].stdoutopen = -1;
+				cpusession[sessid].closestdout <-= 1;
+			}
+			#sys->fprint(sys->fildes(2), "Clunk: stdout (in %d: out %d\n",
+			#	cpusession[sessid].stdinopen, cpusession[sessid].stdoutopen);
+		CLONECLOSE =>
+			if (TEST)
+				sys->fprint(sys->fildes(2), "%d: Close clone\n", sessid);
+			clone = 0;
+			#sys->fprint(sys->fildes(2), "Clunk: clone (in %d: out %d\n",
+			#	cpusession[sessid].stdinopen, cpusession[sessid].stdoutopen);
+		FINISHED =>
+			if (TEST)
+				sys->fprint(sys->fildes(2), "%d: Rcmd finished", sessid);
+			
+			cpusession[sessid].finished = 1;
+			poolchanin <-= "free "+string cpusession[sessid].cpuid;
+			if (closed)
+				break main;
+		}
+		if (cpusession[sessid].stdinopen <= 0 &&
+			cpusession[sessid].stdoutopen <= 0 &&
+			clone == 0) {
+			
+			closed = 1;
+			tree.remove(big (Qsessdir | (sessid<<4)));
+			tree.remove(big (Qsessdat | (sessid<<4)));
+			if (cpusession[sessid].finished || !cpusession[sessid].written)
+				break main;
+		}
+	}
+	if (!cpusession[sessid].finished)	# ie never executed anything
+		poolchanin <-= "free "+string cpusession[sessid].cpuid;
+	cpusession[sessid] = NILCPUSESSION;
+	if (TEST)
+		sys->fprint(sys->fildes(2), "closing session %d\n",sessid);
+}
+
+devconswrite(sessid: int)
+{
+	cpusession[sessid].sync <-= PID;
+	cpusession[sessid].sync <-= sys->pctl(0, nil);
+	stdouteof := 0;
+	file2chaneof := 0;
+	rcmddone := 0;
+	main: for (;;) alt {
+	<-cpusession[sessid].rcmdfinishedstdout =>
+		rcmddone = 1;
+		if (file2chaneof)
+			break main;
+	<-cpusession[sessid].closestdout =>
+		stdouteof = 1;
+	(offset, d, fid, wc) := <-cpusession[sessid].fio.write =>
+		if (wc != nil) {
+			# sys->fprint(sys->fildes(2), "stdout: '%s'\n", string d);
+			if (stdouteof) {
+				# sys->fprint(sys->fildes(2), "stdout: sending EOF\n");
+				wc <-= (0, nil);
+				continue;
+			}
+			alt {
+				<-cpusession[sessid].closestdout =>
+					# sys->print("got closestdout\n");
+					wc <-= (0, nil);
+					stdouteof = 1;
+				<-cpusession[sessid].readstdout =>
+					cpusession[sessid].stdoutchan <-= d;
+					wc <-= (len d, nil);
+			}
+		}
+		else {
+			# sys->fprint(sys->fildes(2), "got nil wc\n");
+			file2chaneof = 1;
+			if (rcmddone)
+				break main;
+		}
+	}
+	# No more input at this point as rcmd has finished;
+	if (stdouteof || cpusession[sessid].stdoutopen == 0) {
+		# sys->print("leaving devconswrite\n");
+		return;
+	}
+	for (;;) alt {
+		<-cpusession[sessid].closestdout =>
+			# sys->print("got closestdout\n");
+			# sys->print("leaving devconswrite\n");
+			return;
+		<- cpusession[sessid].readstdout =>
+			cpusession[sessid].stdoutchan <-= nil;
+	}
+}
+
+devconsread(sessid: int)
+{
+	cpusession[sessid].sync <-= PID;
+	cpusession[sessid].sync <-= sys->pctl(0, nil);
+	stdineof := 0;
+	file2chaneof := 0;
+	rcmddone := 0;
+	main: for (;;) alt {
+	<-cpusession[sessid].rcmdfinishedstdin =>
+		rcmddone = 1;
+		if (file2chaneof)
+			break main;
+	<-cpusession[sessid].closestdin =>
+		# sys->print("got stdin close\n");
+		stdineof = 1;
+	(offset, count, fid, rc) := <-cpusession[sessid].fio.read =>
+		if (rc != nil) {
+			# sys->fprint(sys->fildes(2), "devconsread: '%d %d'\n", count, offset);
+			if (stdineof) {
+				rc <-= (nil, nil);
+				continue;
+			}
+			alt {
+			data := <-cpusession[sessid].stdinchan =>
+				# sys->print("got data len %d\n", len data);
+				rc <-= (data, nil);
+			<-cpusession[sessid].closestdin =>
+				# sys->print("got stdin close\n");
+				stdineof = 1;
+				rc <-= (nil, nil);
+			}
+		}
+		else {
+			# sys->print("got nil rc\n");
+			file2chaneof = 1;
+			if (rcmddone)
+				break main;
+		}
+	}
+	if (!stdineof && cpusession[sessid].stdinopen != 0)
+		<-cpusession[sessid].closestdin;
+	# sys->fprint(sys->fildes(2), "Leaving devconsread\n");
+}
+
+Srvcpool: adt {
+	srvc: ref Service;
+	inuse: int;
+};
+
+cpupool: array of Srvcpool;
+ncpupool := 0;
+
+cpupoolloop(chanin: chan of string, chanout: chan of int)
+{
+	cpupool = array[200] of Srvcpool;
+	for (i := 0; i < len cpupool; i++)
+		cpupool[i] = Srvcpool (nil, 0);
+	wait := 0;
+	for (;;) {
+		inp := <-chanin;
+		# sys->print("poolloop: '%s'\n",inp);
+		(nil, lst) := sys->tokenize(inp, " \t\n");
+		case hd lst {
+		"refresh" =>
+			if (daytime->now() - wait >= 60) {
+				refreshcpupool();
+				wait = daytime->now();
+			}
+		"request" =>
+			if (daytime->now() - wait >= 60) {
+				refreshcpupool();
+				wait = daytime->now();
+			}
+			found := -1;
+			# sys->print("found %d services...\n", ncpupool);
+			for (i = 0; i < ncpupool; i++) {
+				if (!cpupool[i].inuse) {
+					found = i;
+					cpupool[i].inuse = 1;
+					break;
+				}
+			}
+			# sys->print("found service %d\n", found);
+			chanout <-= found;
+		"free" =>
+			if (TEST)
+				sys->print("freed service %d\n", int hd tl lst);
+			cpupool[int hd tl lst].inuse = 0;
+		}
+	}
+}
+
+refreshcpupool()
+{
+	(lsrv, err) := rstyxreg.find(("resource", "Rstyx resource") :: nil);
+	# sys->print("found %d resources\n",len lsrv);
+	if (err != nil)
+		return;
+	tmp := array[len cpupool] of Srvcpool;
+	ntmp := len lsrv;
+	i := 0;
+	for (;lsrv != nil; lsrv = tl lsrv)
+		tmp[i++] = Srvcpool(hd lsrv, 0);
+	min := 0;
+	for (i = 0; i < ntmp; i++) {
+		for (j := min; j < ncpupool; j++) {
+			if (tmp[i].srvc.addr == cpupool[j].srvc.addr) {
+				if (j == min)
+					min++;
+				tmp[i].inuse = cpupool[j].inuse;
+			}
+		}
+	}
+	ncpupool = ntmp;	
+	for (i = 0; i < ntmp; i++)
+		cpupool[i] = tmp[i];
+	# sys->print("ncpupool: %d\n",ncpupool);
+}
+
+getsession(fid: int): int
+{
+	for (i := 0; i < len cpusession; i++)
+		if (cpusession[i].fid == fid)
+			return i;
+	return -1;
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "kill");
+}
+
+killg(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "killgrp");
+}
+
+delfid(datafids: list of Datafid, fid: int): (list of Datafid, Datafid)
+{
+	rdf := Datafid (-1, -1, -1);
+	tmp : list of Datafid = nil;
+	for (; datafids != nil; datafids = tl datafids) {
+		testdf := hd datafids;
+		if (testdf.fid == fid)
+			rdf = testdf;
+		else
+			tmp = testdf :: tmp;
+	}
+	return (tmp, rdf);
+}
+
+addfid(datafids: list of Datafid, df: Datafid): list of Datafid
+{
+	(datafids, nil) = delfid(datafids, df.fid);
+	return df :: datafids;
+}
+
+Datafid: adt {
+	sessid, fid, omode: int;
+};
--- /dev/null
+++ b/appl/grid/demo/block.b
@@ -1,0 +1,212 @@
+implement Block;
+
+include "sys.m";
+	sys : Sys;
+include "daytime.m";
+	daytime: Daytime;
+include "draw.m";
+	draw: Draw;
+	Chans, Context, Display, Point, Rect, Image, Screen, Font: import draw;
+include "readdir.m";
+	readdir: Readdir;
+include "grid/demo/exproc.m";
+	exproc: Exproc;
+include "grid/demo/block.m";
+
+timeout := 50;
+WAITING: con -1;
+DONE: con -2;
+path := "";
+
+init(pathname: string, ep: Exproc)
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmod(Draw->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if (daytime == nil)
+		badmod(Daytime->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil)
+		badmod(Readdir->PATH);
+	if (pathname == "")
+		err("no path given");
+	if (pathname[len pathname - 1] != '/')
+		pathname[len pathname] = '/';
+	path = pathname;
+	exproc = ep;
+	if (exproc == nil)
+		badmod("Exproc");
+	sys->create(path, sys->OREAD, 8r777 | sys->DMDIR);
+	(n, nil) := sys->stat(path);
+	if (n == -1)
+		sys->print("Cannot find path: %s\n",path);
+}
+
+slave()
+{
+	buf := array[8192] of byte;
+	for(;;) {
+		(n, nil) := sys->stat(path+"working");
+		if (n == -1)
+			sys->sleep(1000);
+		else {
+			fd := sys->open(path + "data.dat", sys->OREAD);
+			if (fd != nil) {
+				s := "";
+				for (;;) {
+					i := sys->read(fd, buf, len buf);
+					if (i < 1)
+						break;
+					s += string buf[:i];
+				}
+				(nil, lst) := sys->tokenize(s, "\n");
+				exproc->getslavedata(lst);
+				break;
+			}
+		}
+	}
+	doneblocks := 0;
+	loop: for (;;) {
+		(dirs, nil) := readdir->init(path+"todo", readdir->NAME);
+		if (len dirs == 0) {
+			(n, nil) := sys->stat(path + "working");
+			if (n == -1)
+				break loop;
+			sys->sleep(2000);
+		}
+		for (i := 0; i < len dirs; i++) {
+			fd := sys->create(path+dirs[i].name, sys->OREAD, 8r777 | sys->DMDIR);
+			if (fd != nil) {
+				(nil, lst) := sys->tokenize(dirs[i].name, ".");
+				exproc->doblock(int hd tl lst, dirs[i].name);
+				doneblocks++;
+			}
+			(n, nil) := sys->stat(path + "working");
+			if (n == -1)
+				break loop;
+		}
+	}
+	sys->print("Finished: %d blocks\n",doneblocks);
+}
+
+writedata(s: string)
+{
+	fd := sys->create(path+"data.dat", sys->OWRITE, 8r666);
+	if (fd != nil)
+		sys->fprint(fd, "%s", s);
+	else
+		err("could not create data.dat");
+	fd = nil;
+}
+
+masterinit(noblocks: int)
+{
+	sys->create(path+"todo", sys->OREAD, 8r777 | sys->DMDIR);
+	sys->create(path+"working", sys->OWRITE, 8r666);
+	for (i := 0; i < noblocks; i++)
+		makefile(i, "");
+}
+
+reader(noblocks: int, chanout: chan of string, sync: chan of int)
+{
+	sync <-= sys->pctl(0,nil);
+	starttime := daytime->now();
+	times := array[noblocks] of { * => WAITING };
+	let := array[noblocks] of { * => "a" };
+	buf := array[50] of byte;
+	result := 0;
+	for (;;) {
+		nodone := 0;
+		for (i := 0; i < noblocks; i++) {
+			if (times[i] != DONE) {
+				(n,nil) := sys->stat(path+"block."+string i+"."+let[i]+"/done");
+				if (n == -1) {
+					(n2, nil) := sys->stat(path+"block."+string i+"."+let[i]);
+					if (n2 != -1) {
+						now := daytime->now();
+						if (times[i] == WAITING)
+							times[i] = now;
+						else if (now - times[i] > timeout) {
+							let[i] = makefile(i, let[i]);
+							times[i] = WAITING;
+						}
+					}
+				}
+				else {
+					sys->remove(path +"todo/block."+string i+"."+let[i]);
+					if (exproc->readblock(i, path+"block."+string i+"."+let[i]+"/", chanout) == -1) {
+						let[i] = makefile(i, let[i]);
+						times[i] = WAITING;
+					}
+					else {
+						times[i] = DONE;
+						nodone++;
+					}
+				}
+			}
+			else
+				nodone++;
+		}
+		if (nodone == noblocks)
+			break;
+		chanout <-= string ((nodone*100)/noblocks);
+		sys->sleep(1000);
+	}
+	endtime := daytime->now();
+	chanout <-= "100";
+	spawn exproc->finish(endtime - starttime, chanout);
+}
+
+makefile(block: int, let: string): string
+{
+	if (let == "")
+		let = "a";
+	else {
+		sys->remove(path +"todo/block."+string block+"."+let);
+		let[0]++;
+	}
+	name := path+"todo/block."+string block+"."+let;
+	fd :=	sys->create(name, sys->OREAD, 8r666);
+	if (fd == nil)
+		sys->print("Error creating: '%s'\n",name);
+	return let;
+}
+
+err(s: string)
+{
+	sys->print("Error: '%s'\n",s);
+	exit;
+}
+
+cleanfiles(delpath: string)
+{	
+	buf := array[8192] of byte;
+	if (delpath == "")
+		return;
+	if (delpath[len delpath - 1] != '/')
+		delpath[len delpath] = '/';
+	(dirs, n) := readdir->init(delpath, readdir->NAME);
+	for (i := 0; i < len dirs; i++) {
+		if (dirs[i].mode & sys->DMDIR)
+			cleanfiles(delpath+dirs[i].name+"/");
+		sys->remove(delpath+dirs[i].name);
+	}
+}
+
+isin(l: list of string, s: string): int
+{
+	for(tmpl := l; tmpl != nil; tmpl = tl tmpl)
+		if (hd tmpl == s)
+			return 1;
+	return 0;
+}
+
+badmod(path: string)
+{
+	sys->print("Block: failed to load: %s\n",path);
+	exit;
+}
--- /dev/null
+++ b/appl/grid/demo/blur.b
@@ -1,0 +1,654 @@
+implement Blur;
+
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "sys.m";
+	sys : Sys;
+include "daytime.m";
+	daytime: Daytime;
+include "draw.m";
+	draw: Draw;
+	Display, Chans, Point, Rect, Image: import draw;
+include "readdir.m";
+	readdir: Readdir;
+include "grid/demo/exproc.m";
+	exproc: Exproc;
+include "grid/demo/block.m";
+	block: Block;
+
+display : ref draw->Display;
+context : ref draw->Context;
+path := "/tmp/blur/";
+
+Blur : module {
+	init : fn (ctxt : ref Draw->Context, nil : list of string);
+	getslavedata : fn (lst: list of string);
+	doblock : fn (block: int, bpath: string);
+	readblock : fn (block: int, dir: string, chanout: chan of string): int;
+	finish : fn (waittime: int, tkchan: chan of string);
+};
+
+init(ctxt : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmod(Draw->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if (daytime == nil)
+		badmod(Daytime->PATH);
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+	tkclient->init();
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil)
+		badmod(Readdir->PATH);
+	exproc = load Exproc "$self";
+	if (exproc == nil)
+		badmod(sys->sprint("Exproc: %r"));
+	block = load Block Block->PATH;
+	if (block == nil)
+		badmod(Block->PATH);
+	if (ctxt == nil) {
+		display = Display.allocate(nil);
+		if (display == nil)
+			usage(sys->sprint("failed to get a display: %r"));
+		context = nil;
+	}
+	else {
+		display = ctxt.display;
+		context = ctxt;
+	}
+	spawn blurit(argv);
+}
+
+blurit(argv: list of string)
+{
+	mast := 0;
+	size = 12;
+	blocks = Point (10,6);
+	filename := "";
+
+	argv = tl argv;
+	if (len argv > 2)
+		usage("too many arguments");
+	
+	for (; argv != nil; argv = tl argv) {
+		(n,dir) := sys->stat(hd argv);
+		if (n == -1)
+			usage("file/directory '"+hd argv+"' does not exist");
+		if (dir.mode & sys->DMDIR)
+			path = hd argv;
+		else {
+			filename = hd argv;
+			mast = 1;
+		}
+	}
+	if (mast && context == nil)
+		usage("nil context - cannot be used as master");
+	if (path[len path - 1] != '/')
+		path[len path] = '/';
+	if (len path < 5 || path[len path - 5:] != "blur/")
+		path += "blur/";
+	block->init(path, exproc);
+	if (mast)
+		spawn master(filename);
+	else {
+		sys->print("starting slave\n");
+		spawn block->slave();
+	}
+}
+
+usage(err: string)
+{
+	sys->print("usage: blur [dir] [image]\n");
+	if (err != nil) {
+		sys->print("Error: %s\n",err);
+		raise "fail:error";
+	}
+	else
+		exit;
+}
+
+getslavedata(lst: list of string)
+{
+	if (lst == nil || len lst < 5)
+		block->err("Cannot read data file");
+	size = int hd lst;
+	blocks = Point(int hd tl lst, int hd tl tl lst);
+	bsize = Point(int hd tl tl tl lst, int hd tl tl tl tl lst);
+	blockimg = display.newimage(((0,0),bsize), draw->RGB24,0,draw->Red);
+}
+
+blocks, bsize: Draw->Point;
+size: int;
+newimg: ref Draw->Image;
+
+getxy(i, w: int): (int, int)
+{
+	y := i / w;
+	x := i - (y * w);
+	return (x,y);
+}
+
+master(filename: string)
+{
+	block->cleanfiles(path);
+	img := display.open(filename);
+	if (img == nil)
+		block->err("cannot read image: "+filename);
+	if (img.chans.depth() != 24)
+			block->err("wrong image depth! (must be 24bit)\n");
+	sys->create(path, sys->OREAD, 8r777 | sys->DMDIR);
+
+	blocks.x = img.r.dx() / 70;
+	if (blocks.x < 1)
+		blocks.x = 1;
+	blocks.y = img.r.dy() / 70;
+	if (blocks.y < 1)
+		blocks.y = 1;
+
+	bsize = Point(img.r.dx()/blocks.x, img.r.dy()/blocks.y);
+		
+	data := sys->sprint("%d\n%d\n%d\n%d\n%d\n",size,blocks.x,blocks.y,bsize.x,bsize.y);
+	noblocks := blocks.x * blocks.y;
+
+	n := 0;
+	for (y := 0; y < blocks.y; y++) {
+		for (x := 0; x < blocks.x; x++) {
+			r2 := Rect(((x*bsize.x)-size, (y*bsize.y)-size), 
+					(((1+x)*bsize.x)+size, ((1+y)*bsize.y)+size));
+			if (r2.min.x < 0)
+				r2.min.x = 0;
+			if (r2.min.y < 0)
+				r2.min.y = 0;
+			if (r2.max.x > img.r.max.x)
+				r2.max.x = img.r.max.x;
+			if (r2.max.y > img.r.max.y)
+				r2.max.y = img.r.max.y;
+
+			tmpimg := display.newimage(r2,draw->RGB24,0,draw->Black);
+			tmpimg.draw(r2, img, nil, r2.min);
+			fdtmp := sys->create(path+"imgdata."+string n+".bit", sys->OWRITE, 8r666);			
+			if (fdtmp == nil)
+				sys->print("couldn't write image: '%s' %r\n",path+"imgdata."+string n+".bit");
+			display.writeimage(fdtmp, tmpimg);
+			n++;
+		}
+	}
+	block->writedata(data);
+	block->masterinit(noblocks);
+		
+	(top, titlebar) := tkclient->toplevel(context, "", "Blur", Tkclient->Hide);
+	tkcmd(top, "frame .f");
+	r2 := Rect((0,0),(blocks.x*bsize.x,blocks.y*bsize.y));
+	newimg = display.newimage(r2,draw->RGB24,0,draw->Black);
+	newimg.draw(r2,img,nil,(0,0));
+	tkcmd(top, sys->sprint("panel .f.p -height %d -width %d", r2.dy(), r2.dx()));
+	tk->putimage(top, ".f.p", newimg, nil);
+	tkcmd(top, "label .f.l1 -text {Processed: }");
+	tkcmd(top, "label .f.l2 -text {0%} -width 30");
+	tkcmd(top, "grid .f.p -row 0 -column 0 -columnspan 2");
+	tkcmd(top, "grid .f.l1 -row 1 -column 0 -sticky e");
+	tkcmd(top, "grid .f.l2 -row 1 -column 1 -sticky w");
+	tkcmd(top, "pack .f");
+	tkcmd(top, "bind .Wm_t <Button-1> +{focus .}");
+	tkcmd(top, "bind .Wm_t.title <Button-1> +{focus .}");
+	tkcmd(top, "focus .; update");
+
+	tkchan := chan of string;
+	sync := chan of int;
+	spawn block->reader(noblocks, tkchan, sync);
+	readerpid := <-sync;
+	spawn window(top, titlebar, newimg, tkchan, readerpid);
+}
+
+blockimg: ref Draw->Image;
+
+doblock(block: int, bpath: string)
+{
+	(x,y) := getxy(block, blocks.x);
+	procimg := display.open(path+"imgdata."+string block+".bit");
+	if (procimg == nil)
+		sys->print("Error nil image! '%s' %r\n",path+"imgdata."+string block+".bit");
+	blurred := procblock(procimg, x,y,0,size,bsize);
+	sketched := procblock(procimg, x,y,1,3,bsize);
+	for (i := 0; i < len blurred; i++) {
+		if (sketched[i] != byte 127)
+			blurred[i] = sketched[i];
+	}
+	blockimg.writepixels(((0,0),bsize), blurred);
+	fd := sys->create(path + bpath+"/img.bit",sys->OWRITE,8r666);
+	display.writeimage(fd, blockimg);
+	fd = nil;
+	sys->create(path + bpath+"/done", sys->OWRITE, 8r666);
+}
+
+window(top: ref Tk->Toplevel, titlebar: chan of string, 
+		img: ref Image, tkchan: chan of string, readerpid: int)
+{
+	total := blocks.x * blocks.y;
+	done := 0;
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	finished := 0;
+	main: for(;;) alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <- tkchan =>
+			(n, lst) := sys->tokenize(inp, " \n\t");
+			case hd lst {
+				"done" =>
+					done++;
+					tkcmd(top, ".f.l2 configure -text {"+string ((100*done)/total)+"%}");
+					tkcmd(top, ".f.p dirty");
+				"time" =>
+					tkcmd(top, ".f.l1 configure -text {Time taken:}");
+					tkcmd(top, ".f.l2 configure -text {"+hd tl lst+"} -width 80");
+					finished = 1;
+				* =>
+					tkcmd(top, ".f.l2 configure -text {"+inp+"%}");
+			}
+			tkcmd(top, "update");
+
+		title := <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <- titlebar =>
+			if (title == "exit") {
+				if (finished) {
+					kill(readerpid);
+					break main;
+				}
+			}
+			else
+				tkclient->wmctl(top, title);
+	}
+	spawn block->cleanfiles(path);
+}
+
+readblock(block: int, dir: string, chanout: chan of string): int
+{
+	img := display.open(dir+"img.bit");
+	if (img == nil)
+		return -1;
+	(ix,iy) := getxy(block, blocks.x);
+	newimg.draw(img.r.addpt(Point(ix*bsize.x, iy*bsize.y)),img,nil,(0,0));
+	chanout <-= "done";
+	return 0;
+}
+
+finish(waittime: int, tkchan: chan of string) 
+{
+	hrs := waittime / 360; 
+	mins := (waittime - (360 * hrs)) / 60;
+	secs := waittime - (360 * hrs) - (60 * mins);
+	time := addzeros(sys->sprint("%d:%d:%d",hrs,mins,secs));
+	if (hrs == 0) time = time[3:];
+	tkchan <-= "time "+time;
+	block->cleanfiles(path);
+}
+
+procblock(procimg: ref Image, x,y, itype, size: int, bsize: Point): array of byte
+{
+	r := Rect((x*bsize.x, y*bsize.y), ((1+x)*bsize.x, (1+y)*bsize.y));
+	r2 : Rect;
+	if (itype == 0)
+		r2 = procimg.r;
+	else
+		r2 = Rect((x*bsize.x, y*bsize.y), (((1+x)*bsize.x)+1, ((1+y)*bsize.y)+1));
+	if (r2.min.x < 0)
+		r2.min.x = 0;
+	if (r2.min.y < 0)
+		r2.min.y = 0;
+	if (r2.max.x > procimg.r.max.x)
+		r2.max.x = procimg.r.max.x;
+	if (r2.max.y > procimg.r.max.y)
+		r2.max.y = procimg.r.max.y;
+
+	buf := array[3 * r2.dx() * r2.dy()] of byte;
+	procimg.readpixels(r2,buf);
+	pad := Rect((r.min.x-r2.min.x, r.min.y-r2.min.y), (r2.max.x - r.max.x, r2.max.y-r.max.y));
+	if (itype == 0)
+		return blurblock(size,r,pad,buf);
+	if (itype == 1)
+		return gradblock(10,r,pad,buf);
+	return nil;
+}
+
+makepic(buf: array of int, w,nw,nh: int): array of byte
+{
+	newbuf := array[3*nw*nh] of byte;
+	n := 0;
+	for (y := 0; y < nh; y++) {
+		for (x := 0; x < nw; x++) {
+			val := byte buf[(y*w)+x];
+			if (val < byte 0) val = -val;
+			if (val > byte 255) val = byte 255;
+			for (i := 0; i < 3; i++)
+				newbuf[n++] = val;
+		}
+	}
+	return newbuf;
+}
+
+gradblock(threshold: int, r, pad: Rect, buffer: array of byte) : array of byte
+{
+	gradbufx := array[3] of array of int; 
+	gradbufy := array[3] of array of int;
+	width: int;
+	cleaning := 3;
+	for (rgb := 0; rgb < 3; rgb++) {
+
+		greybuf := array[len buffer] of { * => 0 };
+		n := 0;
+		width = r.dx()+pad.max.x;
+		for (y := 0; y < r.dy()+pad.max.y; y++) {
+			for (x := 0; x < r.dx()+pad.max.x; x++) {
+				greybuf[n++] = int buffer[(3* ((y*width) + x ))+rgb];
+			}	
+		}
+	
+		for(i := 0; i < 2; i++) {
+			padx := pad.max.x;
+			pady := pad.max.y;
+			width = r.dx();
+			height := r.dy();
+			gradbuf: array of int;
+			(gradbuf, width, height, padx, pady) = getgrad(greybuf, i, width,height, padx, pady);
+			width = r.dx();
+			if (i == 0) {
+				gradbufx[rgb] = clean(hyster(gradbuf,1,width,threshold), width,5,4);
+				for (k := 0; k < cleaning; k++)
+					gradbufx[rgb] = clean(gradbufx[rgb], width,2,2);
+			}
+			else {
+				gradbufy[rgb] = clean(hyster(gradbuf, 0,width,threshold), width,5,4);
+				for (k := 0; k < cleaning; k++)
+					gradbufy[rgb] = clean(gradbufy[rgb], width,2,2);
+			}
+		}
+	
+	}
+	newbuf := array[len gradbufx[0]] of int;
+	for (i := 0; i < len newbuf; i++) {
+		val := 127;
+		n := 0;
+		for (rgb = 0; rgb < 3; rgb++) {
+			if (gradbufx[rgb][i] != 127) {
+				n++;
+				val = gradbufx[rgb][i];
+			}
+			else if (gradbufy[rgb][i] != 127) {
+				val = gradbufy[rgb][i];
+				n++;
+			}
+		}
+		if (n > 1)
+			newbuf[i] = val;
+		else
+			newbuf[i] = 127;
+	}
+	if (sat(newbuf) > 25 && threshold > 4)
+		return gradblock(threshold - 2,r,pad,buffer);
+	return makepic(newbuf,width,r.dx(),r.dy());
+}
+
+X: con 0;
+Y: con 1;
+
+getgrad(buf: array of int, dir, w,h, px, py: int): (array of int, int, int, int, int)
+{
+	npx := px - 1;
+	npy := py - 1;
+	if (npx < 0) npx = 0;
+	if (npy < 0) npy = 0;
+	gradbuf := array[(w+npx)*(h+npy)] of int;
+	n := 0;
+	val1, val2: int;
+	for (y := 0; y < h+npy; y++) {
+		for (x := 0; x < w+npx; x++) {
+			val1 = buf[(y*(w+px)) + x];
+			if ((dir == X && x-w >= npx) ||
+				(dir == Y && y-h >= npy))
+				val2 = val1;
+			else
+				val2 = buf[((y+dir)*(w+px)) + x + 1 - dir];
+			gradbuf[n++] = val2 - val1;
+		}	
+	}
+	return (norm(gradbuf,0,255), w, h, px,py);
+}
+
+sat(a: array of int): int
+{
+	n := 0;
+	for (i := 0; i < len a; i++)
+		if (a[i] != 127)
+			n++;
+	return (100 * n)/ len a;
+}
+
+hyster(a: array of int, gox, width: int, lim: int): array of int
+{
+	min, max: int;
+	av := 0;
+	for (i := 0; i < len a; i++) {
+		if (i == 0)
+			min = max = a[i];
+		if (a[i] < min)
+			min = a[i];
+		if (a[i] > max)
+			max = a[i];
+		av += a[i];
+	}
+#	sys->print("%d/%d = %d\n",av,len a,av / len a);
+	av = av/len a;
+	upper := av + ((max-av)/lim);
+	lower := av - ((av-min)/ lim);
+	low := 0;
+#	sys->print("len a: %d %d %d %d\n",len a,av,min,max);
+	i = 0;
+	x := 0;
+	y := 0;
+	height := len a / width;
+	newline := 1;
+#	sys->print("width: %d gox: %d\n",width,gox);
+	for (k := 0; k < len a; k++) {
+		i = (y*width) + x;
+		if (newline) {
+#			if (a[i] < av) low = 1;
+#			else low = 0;
+			low = a[i] > av;
+			newline = 0;
+		}
+		oldlow := low;
+		if (low == 0) {
+			if (a[i] > upper)
+				low = 1;
+		}
+		else if (low == 1) {
+			if (a[i] < lower)
+				low = 0;
+		}
+#		sys->print("a[i]: %d bound: %d %d low %d => %d\n",a[i],lower,upper,oldlow,low);
+		if (oldlow == low)
+			a[i] =127;
+		else
+			a[i] = low * 255;
+
+		if (gox) {
+			i++;
+			x++;
+			if (x == width) {
+				x = 0;
+				y++;
+				newline = 1;
+			}
+		}
+		else {
+			i += width;
+			y++;
+			if (y == height) {
+#				sys->print("y: %d\n",y);
+				y = 0;
+				i = x;
+				x++;
+				newline = 1;
+			}
+		}
+	}
+	return a;
+}
+
+clean(a: array of int, width, r, d: int): array of int
+{
+	height := len a / width;
+	csize := (2*r) ** 2;
+	for (y := 0; y < height; y++) {
+		for (x := 0; x < width; x++) {
+			i := (width*y)+x;
+			if (a[i] != 127) {
+				sx := x - r;
+				if (sx < 0) sx = 0;
+				ex := x + r;
+				if (ex > width) ex = width;
+				sy := y - r;
+				if (sy < 0) sy = 0;
+				ey := y + r;
+				n := 0;
+				if (ey > height) ey = height;
+				for (iy := sy; iy < ey; iy++) {
+					for (ix := sx; ix < ex; ix++) {
+						if (a[(width*iy)+ix] == a[i])
+							n++;
+					}
+				}
+				#sys->print("%f\n",real ((ex-sx)*(ey-sy))/ real csize);
+#				if (n < int (real d * (real ((ex-sx)*(ey-sy))/ real csize)))
+				if (n < d)
+					a[i] = 127;
+			}
+		}
+	}
+	return a;
+}
+
+
+norm(a: array of int, lower, upper: int): array of int
+{
+	min, max: int;
+	for (i := 0; i < len a; i++) {
+		if (i == 0)
+			min = max = a[i];
+		if (a[i] < min)
+			min = a[i];
+		if (a[i] > max)
+			max = a[i];
+	}
+	multi : real = (real (upper - lower)) / (real (max - min));
+	add := real (lower - min);
+	for (i = 0; i < len a; i++) {
+		a[i] = int ((add + real a[i]) * multi);
+		if (a[i] < lower)
+			a[i] = lower;
+		if (a[i] > upper)
+			a[i] = upper;
+	}
+	return a;
+}
+
+opt := 2;
+
+blurblock(size: int, r, pad: Rect, buffer: array of byte) : array of byte
+{
+	newbuf := array[3 * r.dx() * r.dy()] of byte;
+	n := 0;
+	width := r.dx()+pad.min.x+pad.max.x;
+	for (y := 0; y < r.dy(); y++) {
+		for (x := 0; x < r.dx(); x++) {
+			r2 := Rect((x-size,y-size),(x+size+1,y+size+1));
+			if (r2.min.x < -pad.min.x)
+				r2.min.x = -pad.min.x;
+			if (r2.min.y < -pad.min.y)
+				r2.min.y = -pad.min.y;
+			if (r2.max.x > r.dx()+pad.max.x)
+				r2.max.x = r.dx()+pad.max.x;
+			if (r2.max.y > r.dy()+pad.max.y)
+				r2.max.y = r.dy()+pad.max.y;
+			nosamples := r2.dx()*r2.dy();
+
+			r2.min.x += pad.min.x;
+			r2.min.y += pad.min.y;
+			r2.max.x += pad.min.x;
+			r2.max.y += pad.min.y;
+			pixel := array[3] of { * => 0};
+			for (sy := r2.min.y; sy < r2.max.y; sy++) {
+				for (sx := r2.min.x; sx < r2.max.x; sx++) {
+					for (i := 0; i < 3; i++)
+						pixel[i] += int buffer[(3* ( ((sy)*width) + (sx) ) )+ i];
+				}
+			}
+			for (i := 0; i < 3; i++) {
+				if (opt == 0)
+					newbuf[n++] = byte (pixel[i] / nosamples);
+				if (opt == 1)
+					newbuf[n++] = byte (255 - (pixel[i] / nosamples));
+				if (opt == 2)
+					newbuf[n++] = byte (63 + (pixel[i] / (2*nosamples)));
+
+			}
+
+		}
+	}
+	return newbuf;
+}
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != "" && e[0] == '!') sys->print("tk error: '%s': %s\n",cmd,e);
+	return e;
+}
+
+addzeros(s: string): string
+{
+	s[len s] = ' ';
+	rs := "";
+	start := 0;
+	isnum := 0;
+	for (i := 0; i < len s; i++) {
+		if (s[i] < '0' || s[i] > '9') {
+			if (isnum && i - start < 2) rs[len rs] = '0';
+			rs += s[start:i+1];
+			start = i+1;
+			isnum = 0;
+		}
+		else isnum = 1;
+	}
+	i = len rs - 1;
+	while (i >= 0 && rs[i] == ' ') i--;
+	return rs[:i+1];
+}
+
+kill(pid: int)
+{	
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil)
+		sys->write(pctl, array of byte "kill", len "kill");
+}
+
+badmod(path: string)
+{
+	sys->print("Blur: failed to load: %s\n",path);
+	exit;
+}
--- /dev/null
+++ b/appl/grid/demo/mkfile
@@ -1,0 +1,32 @@
+<../../../mkconfig
+
+TARG=\
+	block.dis\
+	blur.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	daytime.m\
+	draw.m\
+	grid/demo/block.m\
+	grid/demo/exproc.m\
+	readdir.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+
+DISBIN=$ROOT/dis/grid/demo
+
+<$ROOT/mkfiles/mkdis
+
+$ROOT/dis/grid/demo/blur.dis:	blur.dis
+	rm -f $target && cp blur.dis $target
+$ROOT/dis/grid/demo/block.dis:	block.dis
+	rm -f $target && cp block.dis $target
+
+blur.dis:	blur.b $MODULE $SYS_MODULE
+	limbo $LIMBOFLAGS -c -gw blur.b
+
+block.dis:	block.b $MODULE $SYS_MODULE
+	limbo $LIMBOFLAGS -c -gw block.b
--- /dev/null
+++ b/appl/grid/find.b
@@ -1,0 +1,262 @@
+implement Find;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "arg.m";
+include "sh.m";
+include "registries.m";
+	registries: Registries;
+	Registry, Attributes, Service: import registries;
+include "grid/announce.m";
+	announce: Announce;
+
+Find: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(sys->FORKNS | sys->NEWPGRP, nil);
+	draw = load Draw Draw->PATH;
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmod(Arg->PATH);
+	if (draw == nil)
+		badmod(Draw->PATH);
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+	tkclient->init();
+	registries = load Registries Registries->PATH;
+	if (registries == nil)
+		badmod(Registries->PATH);
+	registries->init();
+
+	command := "";
+	attrs := Attributes.new(nil);
+	arg->init(argv);
+	arg->setusage("find [-a attributes] action1 { cmd [args...] } .. actionN { cmd [args...] }");
+	title := "a resource";
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		't' =>
+			title = arg->earg();
+		'a' =>
+			attr := arg->earg();
+			val := arg->earg();
+			attrs.set(attr, val);
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	if (argv == nil || len argv % 2)
+		arg->usage();
+	arg = nil;
+	
+	cmds := array[len argv / 2] of (string, string);
+	for (i := 0; i < len cmds; i++) {
+		cmds[i] = (hd argv, hd tl argv);
+		argv = tl tl argv;
+	}
+
+	reg := Registry.connect(nil, nil, nil);
+	if (reg == nil)
+		error(ctxt, ((0,0),(0,0)), "Could not find registry");
+	(matches, err) := reg.find(attrs.attrs);
+	if (err != nil)
+		error(ctxt, ((0,0),(0,0)), "Registry error: "+err);
+	spawn tkwin(ctxt, matches, cmds, title);	
+}
+
+mainscr := array[] of {
+	"frame .f",
+	"frame .f.flb",
+	"listbox .f.flb.lb1 -yscrollcommand {.f.flb.sb1 set} -selectmode single -bg white -selectbackground blue -font /fonts/charon/plain.normal.font",
+	"bind .f.flb.lb1 <Double-Button-1> {send butchan double %y}",
+	"scrollbar .f.flb.sb1 -command {.f.flb.lb1 yview}",
+	"pack .f.flb.sb1 -fill y -side left",
+	"pack .f.flb.lb1 -fill both -expand 1",
+	"frame .f.fb",
+	"pack .f.flb -fill both -expand 1 -side top",
+	"pack .f.fb",
+	"pack .f -fill both -expand 1",
+};
+
+errscr := array[] of {
+	"frame .f",
+	"frame .f.fl",
+	"label .f.fl.l1 -text {} -font /fonts/charon/plain.normal.font ",
+	"label .f.fl.l2 -text {Please try again later} -font /fonts/charon/plain.normal.font",
+	"pack .f.fl.l1 .f.fl.l2 -side top",
+	"button .f.b -text { Close } -command {send butchan exit} "+
+		"-font /fonts/charon/bold.normal.font",
+	"grid .f.fl -row 0 -column 0 -padx 10 -pady 5",
+	"grid .f.b -row 1 -column 0 -pady 5",
+	"pack .f",
+};
+
+tkwin(ctxt: ref Draw->Context, lsrv: list of ref Service, cmds: array of (string, string), title: string)
+{
+	(top, titlectl) := tkclient->toplevel(ctxt, "", "Find "+title, tkclient->Appl);
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	if (lsrv == nil) {
+		tkcmds(top, errscr);
+		tkcmd(top, ".f.fl.l1 configure -text {Could not find "+title+"}");
+	}
+	else {
+		tkcmds(top, mainscr);
+		for (tmp := lsrv; tmp != nil; tmp = tl tmp)
+			tkcmd(top, ".f.flb.lb1 insert end {"+(hd tmp).attrs.get("name")+"}");
+		for (i := 0; i < len cmds; i++) {
+			si := string i;
+			tkcmd(top, "button .f.fb.b"+si+" -font /fonts/charon/bold.normal.font "+
+			"-text {"+cmds[i].t0+"} -command {send butchan go "+si+"}");
+			tkcmd(top, "grid .f.fb.b"+si+" -row 0 -column "+si+" -padx 5 -pady 5");
+		}
+		tkcmd(top, ".f.flb.lb1 selection set 0");
+		tkcmd(top, "pack propagate . 0");
+	}
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	for(;;) alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <- butchan =>
+			(nil, lst) := sys->tokenize(inp, " \t\n");
+			case hd lst {
+				"exit" =>
+					return;
+				"go" =>
+					n := int hd tl lst;
+					id := tkcmd(top, ".f.flb.lb1 curselection");
+					if (id != nil)
+						connect(ctxt, lsrv, cmds[n].t1 :: nil, tk->rect(top, ".",0), int id);
+				"double" =>
+					y := hd tl lst;
+					id := int tkcmd(top, ".f.flb.lb1 nearest "+y);
+					connect(ctxt, lsrv, cmds[0].t1 :: nil, tk->rect(top, ".",0), id);
+			}
+		s := <-top.ctxt.ctl or
+		s = <-top.wreq or
+		s = <- titlectl =>
+			if (s == "exit")
+				exit;
+			else
+				tkclient->wmctl(top, s);
+	}
+}
+
+connect(ctxt: ref Draw->Context, lsrv: list of ref Service, argv: list of string, r: Rect, id: int)
+{
+	for (tmp := lsrv; tmp != nil; tmp = tl tmp) {
+		if (id-- == 0) {
+			spawn mountit(ctxt, hd tmp, argv, r);
+			break;
+		}
+	}
+}
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != "" && e[0] == '!') sys->print("tk error: '%s': %s\n",cmd,e);
+	return e;
+}
+
+tkcmds(top: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++)
+		tkcmd(top, cmds[i]);
+}
+
+mountit(ctxt: ref Draw->Context, srv: ref Registries->Service, argv: list of string, r: Rect)
+{
+	sys->pctl(Sys->FORKNS| Sys->NEWPGRP, nil);
+	attached := srv.attach(nil,nil);
+	if (attached != nil) {
+		if (sys->mount(attached.fd, nil, "/n/client", sys->MREPL, nil) != -1) {
+			sh := load Sh Sh->PATH;
+			if (sh == nil)
+				badmod(Sh->PATH);
+			sys->chdir("/n/client");
+			err := sh->run(ctxt, argv);
+			if (err != nil)
+				error(ctxt, r, "failed to run: "+err);			
+		}
+		else
+			error(ctxt, r, sys->sprint("failed to mount: %r"));			
+	}
+	else
+		error(ctxt, r, sys->sprint("could not connect"));
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+badmod(path: string)
+{
+	sys->fprint(stderr(), "Find: cannot load %s: %r\n", path);
+	exit;
+}
+
+errorwin := array[] of {
+	"frame .f",
+	"label .f.l -font /fonts/charon/plain.normal.font",
+	"button .f.b -text {Ok} -font /fonts/charon/bold.normal.font "+
+		"-command {send butchan ok}",
+	"pack .f.l .f.b -side top -padx 5 -pady 5",
+	"pack .f",
+};
+
+error(ctxt: ref Draw->Context, oldr: Draw->Rect, errstr: string)
+{
+	(top, titlectl) := tkclient->toplevel(ctxt, "", "Error", tkclient->Appl);
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	tkcmds(top, errorwin);
+	tkcmd(top, ".f.l configure -text {"+errstr+"}");
+	r := tk->rect(top, ".", 0);
+	newx := ((oldr.dx() - r.dx())/2) + oldr.min.x;
+	if (newx < 0)
+		newx = 0;
+	newy := ((oldr.dy() - r.dy())/2) + oldr.min.y;
+	if (newy < 0)
+		newy = 0;
+	tkcmd(top, ". configure -x "+string newx+" -y "+string newy);
+	tkclient->onscreen(top, "exact");
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	for(;;) alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		<- butchan =>
+			tkclient->wmctl(top, "exit");
+		s := <-top.ctxt.ctl or
+		s = <-top.wreq or
+		s = <- titlectl =>
+			tkclient->wmctl(top, s);
+	}
+}	
--- /dev/null
+++ b/appl/grid/jpg2bit.b
@@ -1,0 +1,47 @@
+implement jpg2bit;
+
+include "sys.m";
+	sys : Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context, Display, Point, Rect, Image, Screen, Font: import draw;
+
+include "grid/readjpg.m";
+	readjpg: Readjpg;
+
+display : ref draw->Display;
+screen : ref draw->Screen;
+context : ref draw->Context;
+
+jpg2bit : module {
+	init : fn (ctxt : ref Draw->Context, argv : list of string);
+};
+
+init(ctxt : ref Draw->Context, argv : list of string)
+{
+	display = ctxt.display;
+	screen = ctxt.screen;
+	context = ctxt;
+
+	sys = load Sys Sys->PATH;
+	readjpg = load Readjpg Readjpg->PATH;
+	readjpg->init(display);
+	
+	draw = load Draw Draw->PATH;
+	argv = tl argv;
+	if (argv == nil) exit;
+	filename := hd argv;
+	filename2 : string;
+	if (tl argv == nil) {
+		if (len filename > 3) filename2 = filename[:len filename - 4];
+		filename2 += ".bit";
+	}
+	else filename2 = hd tl argv;
+	img := readjpg->jpg2img(hd argv, "", chan of string, nil);
+	fd := sys->create(filename2, sys->OWRITE,8r666);
+	if (fd != nil)
+		display.writeimage(fd,img);
+
+}
+
--- /dev/null
+++ b/appl/grid/lib/announce.b
@@ -1,0 +1,44 @@
+implement Announce;
+include "sys.m";
+	sys:	Sys;
+include "dial.m";
+	dial: Dial;
+include "grid/announce.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+announce(): (string, ref Sys->Connection)
+{
+	sysname := readfile("/dev/sysname");
+	c := dial->announce("tcp!*!0");
+	if(c == nil)
+		return (nil, nil);
+	local := readfile(c.dir + "/local");
+	if(local == nil)
+		return (nil, nil);
+	for(i := len local - 1; i >= 0; i--)
+		if(local[i] == '!')
+			break;
+	port := local[i+1:];
+	if(port == nil)
+		return (nil, nil);
+	if(port[len port - 1] == '\n')
+		port = port[0:len port - 1];
+	return ("tcp!" + sysname + "!" + port, c);
+}
+
+
+readfile(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil)
+		return nil;
+	buf := array[8192] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return nil;
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/grid/lib/browser.b
@@ -1,0 +1,1170 @@
+implement Browser;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+	draw: Draw;
+	Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "./pathreader.m";
+include "./browser.m";
+
+entryheight := "";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmod(Draw->PATH);
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+	tkclient->init();
+}
+
+Browse.new(top: ref Tk->Toplevel, tkchanname, root, rlabel: string, nopanes: int, reader: PathReader): ref Browse
+{
+	b : Browse;
+	b.top = top;
+	b.tkchan = tkchanname;
+	if (nopanes < 1 || nopanes > 2)
+		return nil;
+	b.nopanes = 2;
+	b.bgnorm = bgnorm;
+	b.bgselect = bgselect;
+	b.selected = array[2] of { * => Selected (File(nil, nil), nil) };
+	b.opened = (root, nil) :: nil;
+	if (root == nil)
+		return nil;
+	if (root[len root - 1] != '/')
+		root[len root] = '/';
+	b.pane0width = "2 3";
+	b.root = root;
+	b.rlabel = rlabel;
+	b.reader = reader;
+	b.pane1 = File (nil, "-123");
+	b.released = 1;
+	tkcmds(top, pane0scr);
+
+	tkcmds(top, pane1scr);
+	tkcmd(top, "bind .fbrowse.lmov <Button-1> {send "+b.tkchan+" movdiv %X}");
+
+	tkcmd(top, "label .fbrowse.l -text { }  -anchor w -width 0" +
+		" -font /fonts/charon/plain.normal.font");
+	tkcmd(top, ".fbrowse.l configure -height "+tkcmd(top, ".fbrowse.l cget -height"));
+	tkcmd(top, "grid .fbrowse.l -row 0 -column 0 -sticky ew -pady 2 -columnspan 4");
+	rb := ref b;
+	rb.newroot(b.root, b.rlabel);
+	rb.changeview(nopanes);
+	setbrowsescrollr(rb);
+	return rb;
+}
+
+Browse.refresh(b: self ref Browse)
+{
+	scrval := tkcmd(b.top, ".fbrowse.sy1 get");
+	p := isat(scrval, " ");
+	p1 := b.pane1;
+	b.newroot(b.root, b.rlabel);
+	setbrowsescrollr(b);
+	if (b.nopanes == 2)
+		popdirpane1(b, p1);
+	b.selectfile(1,DESELECT, File (nil, nil), nil);
+	b.selectfile(0,DESELECT, File (nil, nil), nil);
+	tkcmd(b.top, ".fbrowse.c1 yview moveto "+scrval[:p]+"; update");
+}
+
+bgnorm := "white";
+bgselect := "#5555FF";
+
+ft := " -font /fonts/charon/plain.normal.font";
+fts := " -font /fonts/charon/plain.tiny.font";
+ftb := " -font /fonts/charon/bold.normal.font";
+
+Browse.gotoselectfile(b: self ref Browse, file: File): string
+{
+	(dir, tkpath) := b.gotopath(file, 0);
+	if (tkpath == nil)
+		return nil;
+	# Select dir
+	tkpath += ".l";
+	if (dir.qid != nil)
+		tkpath += "Q" + dir.qid;
+	b.selectfile(0, SELECT, dir, tkpath);
+
+	# If it is a file, select the file too
+	if (!File.eq(file, dir)) {
+		slaves := tkcmd(b.top, "grid slaves .fbrowse.fl2");
+		(nil, lst) := sys->tokenize(slaves, " ");
+		for (; lst != nil; lst = tl lst) {
+			if (File.eq(file, *b.getpath(hd lst))) {
+				b.selectfile(1, SELECT, file, hd lst);
+				tkpath = hd lst;
+				break;
+			}
+		}
+		pane1see(b);
+	}
+	return tkpath;
+}
+
+pane1see(b: ref Browse)
+{
+	f := b.selected[1].tkpath;
+	if (f == "")
+		return;
+	x1 := int tkcmd(b.top, f+" cget -actx") - int tkcmd(b.top, ".fbrowse.fl2 cget -actx");
+	y1 := int tkcmd(b.top, f+" cget -acty") - int tkcmd(b.top, ".fbrowse.fl2 cget -acty");
+	x2 := x1 + int tkcmd(b.top, f+" cget -actwidth");
+	y2 := y1 + int tkcmd(b.top, f+" cget -actheight");
+	tkcmd(b.top, sys->sprint(".fbrowse.c2 see %d %d %d %d", x1,y1,x2,y2));
+}
+
+Browse.opendir(b: self ref Browse, file: File, tkpath: string, action: int): int
+{
+	curr := tkcmd(b.top, tkpath+".lp cget -text");
+	if ((action == OPEN || action == TOGGLE) && curr == "+") {
+		tkcmd(b.top, tkpath+".lp configure -text {-} -relief sunken");
+		popdirpane0(b, file, tkpath);
+		seeframe(b.top, tkpath);
+		b.addopened(file, 1);
+		setbrowsescrollr(b);
+		return 1;
+	}
+	else if ((action == CLOSE || action == TOGGLE) && curr == "-") {
+		tkcmd(b.top, tkpath+".lp configure -text {+} -relief raised");
+		slaves := tkcmd(b.top, "grid slaves "+tkpath+" -column 1");
+		p := isat(slaves, " ");
+		if (p != -1)
+			tkcmd(b.top, "destroy "+slaves[p:]);
+		slaves = tkcmd(b.top, "grid slaves "+tkpath+" -column 2");
+		if (slaves != "")
+			tkcmd(b.top, "destroy "+slaves);
+		b.addopened(file, 0);
+		setbrowsescrollr(b);
+		return 1;
+	}
+	return 0;
+}
+
+Browse.addopened(b: self ref Browse, file: File, add: int)
+{
+	tmp : list of File = nil;
+	for (; b.opened != nil; b.opened = tl b.opened) {
+		dir := hd b.opened;
+		if (!File.eq(file, dir))
+			tmp = dir :: tmp;
+	}
+	if (add)
+		tmp = file :: tmp;
+	b.opened = tmp;
+}
+
+Browse.changeview(b: self ref Browse, nopanes: int)
+{
+	if (b.nopanes == nopanes)
+		return;
+#	w := int tkcmd(b.top, ".fbrowse cget -actwidth");
+#	ws := int tkcmd(b.top, ".fbrowse.sy1 cget -width");
+	if (nopanes == 1) {
+		b.pane0width = tkcmd(b.top, ".fbrowse.c1 cget -actwidth") + " " +
+						tkcmd(b.top, ".fbrowse.c2 cget -actwidth");
+		tkcmd(b.top, "grid forget .fbrowse.sx2 .fbrowse.c2 .fbrowse.lmov");
+		tkcmd(b.top, "grid columnconfigure .fbrowse 3 -weight 0");
+	}
+	else {
+		(nil, wlist) := sys->tokenize(b.pane0width, " ");
+		tkcmd(b.top, "grid columnconfigure .fbrowse 1 -weight "+hd wlist);
+		tkcmd(b.top, "grid columnconfigure .fbrowse 3 -weight "+hd tl wlist);
+
+		tkcmd(b.top, "grid .fbrowse.sx2 -row 3 -column 3 -sticky ew");
+		tkcmd(b.top, "grid .fbrowse.c2 -row 2 -column 3 -sticky nsew");
+		tkcmd(b.top, "grid .fbrowse.lmov -row 2 -column 2 -rowspan 2 -sticky ns");
+	}
+	b.nopanes = nopanes;
+}
+
+Browse.selectfile(b: self ref Browse, pane, action: int, file: File, tkpath: string)
+{
+	if (action == SELECT && b.selected[pane].tkpath == tkpath)
+		return;
+	if (b.selected[pane].tkpath != nil)
+		tk->cmd(b.top, b.selected[pane].tkpath+" configure -bg "+bgnorm);
+	if ((action == TOGGLE && b.selected[pane].tkpath == tkpath) || action == DESELECT) {
+		if (pane == 0)
+			popdirpane1(b, File (nil,nil));
+		b.selected[pane] = (File(nil, nil), nil);
+		return;
+	}
+	b.selected[pane] = (file, tkpath);
+	tkcmd(b.top, tkpath+" configure -bg "+bgselect);
+	if (pane == 0)
+		popdirpane1(b, file);
+}
+
+Browse.resize(b: self ref Browse)
+{
+ 	p1 := b.pane1;
+ 	b.pane1 = File (nil, nil);
+ 
+ 	if (p1.path != "")
+ 		popdirpane1(b, p1);
+
+	if (b.selected[1].tkpath != nil) {
+		s := b.selected[1];
+		b.selectfile(1, DESELECT, s.file, s.tkpath);
+		b.selectfile(1, SELECT, s.file, s.tkpath);
+	}
+}
+
+setbrowsescrollr(b: ref Browse)
+{
+	h := tkcmd(b.top, ".fbrowse.fl cget -height");
+	w := tkcmd(b.top, ".fbrowse.fl cget -width");
+	tkcmd(b.top, ".fbrowse.c1 configure -scrollregion {0 0 "+w+" "+h+"}");
+	if (b.nopanes == 2) {
+		h = tkcmd(b.top, ".fbrowse.fl2 cget -height");
+		w = tkcmd(b.top, ".fbrowse.fl2 cget -width");
+		tkcmd(b.top, ".fbrowse.c2 configure -scrollregion {0 0 "+w+" "+h+"}");
+	}
+}
+
+seeframe(top: ref Tk->Toplevel, frame: string)
+{
+	x := int tkcmd(top, frame+" cget -actx") - int tkcmd(top, ".fbrowse.fl cget -actx");
+	y := int tkcmd(top, frame+" cget -acty")  - int tkcmd(top, ".fbrowse.fl cget -acty");
+	w := int tkcmd(top, frame+" cget -width");
+	h := int tkcmd(top, frame+" cget -height");
+	wc := int tkcmd(top, ".fbrowse.c1 cget -width");
+	hc := int tkcmd(top, ".fbrowse.c1 cget -height");
+	if (w > wc)
+		w = wc;
+	if (h > hc)
+		h = hc;
+	tkcmd(top, sys->sprint(".fbrowse.c1 see %d %d %d %d",x,y,x+w,y+h));
+}
+
+# Goes to selected dir OR dir containing selected file
+Browse.gotopath(b: self ref Browse, file: File, openfinal: int): (File, string)
+{
+	tkpath := ".fbrowse.fl.f0";
+	path := b.root;
+	testqid := "";
+	testpath := "";
+	close : list of string;
+	trackbacklist : list of (string, list of string, list of string) = nil;
+	trackback := 0;
+	enddir := "";
+	if (file.path[len file.path - 1] != '/') {
+		# i.e. is not a directory
+		p := isatback(file.path, "/");
+		enddir = file.path[:p + 1];
+	}
+	if (enddir == path) {
+		if (!dircontainsfile(b, File (path, nil), file))
+			return (File (nil, nil), nil);
+	}
+	else {
+		for(;;) {
+			lst : list of string;
+			if (trackback) {
+				(path, lst, close) = hd trackbacklist;
+				trackbacklist = tl trackbacklist;
+				if (close != nil)
+					b.opendir(File (hd close, hd tl close), hd tl tl close, CLOSE);
+				trackback = 0;
+			}
+			else {
+				frames := tkcmd(b.top, "grid slaves "+tkpath+" -column 1");
+				(nil, lst) = sys->tokenize(frames, " ");
+				if (lst != nil)
+					lst = tl lst; # ignore first frame (name of parent dir);
+			}
+			found := 0;
+			hasdups := 1;
+			for (; lst != nil; lst = tl lst) {
+				testpath = path;
+				if (hasdups) {
+					labels := tkcmd(b.top, "grid slaves "+hd lst+" -row 0");
+					(nil, lst2) := sys->tokenize(labels, " ");
+					testpath += tkcmd(b.top, hd tl lst2+" cget -text") + "/";
+					testqid = getqidfromlabel(hd tl lst2);
+					if (testqid == nil)
+						hasdups = 0;
+				}
+				else
+					testpath += tkcmd(b.top, hd lst+".l cget -text") + "/";
+				if (len testpath <= len file.path && file.path[:len testpath] == testpath) {
+					opened := 0;
+					close = nil;
+					if (openfinal || testpath != file.path)
+						opened = b.opendir(File(testpath, testqid), hd lst, OPEN);
+					if (opened)
+						close = testpath :: testqid :: hd lst :: nil;
+					if (tl lst != nil && hasdups)
+						trackbacklist = (path, tl lst, close) :: trackbacklist;
+					tkpath = hd lst;
+					path = testpath;
+					found = 1;
+					break;
+				}
+			}
+			if (enddir != nil && path == enddir)
+				if (dircontainsfile(b, File(testpath, testqid), file))
+					break;
+			if (!found) {
+				if (trackbacklist == nil)
+					return (File (nil, nil), nil);
+				trackback = 1;
+			}
+			else if (testpath == file.path && testqid == file.qid)
+				break;
+		}
+	}
+	seeframe(b.top, tkpath);
+	dir := File (path, testqid);
+	popdirpane1(b, dir);
+	return (dir, tkpath);
+}
+
+dircontainsfile(b: ref Browse, dir, file: File): int
+{
+	(files, hasdups) := b.reader->readpath(dir);
+	for (j := 0; j < len files; j++) {					
+		if (files[j].name == file.path[len dir.path:] && 
+				(!hasdups || files[j].qid.path == big file.qid))
+			return 1;
+	}
+	return 0;
+}
+
+Browse.getpath(b: self ref Browse, f: string): ref File
+{
+	if (len f < 11 || f[:11] != ".fbrowse.fl")
+		return nil;
+	(nil, lst) := sys->tokenize(f, ".");
+	lst = tl lst;
+	if (hd lst == "fl2") {
+		# i.e. is in pane 1
+		qid := getqidfromlabel(f);
+		return ref File (b.pane1.path + tk->cmd(b.top, f+" cget -text"), qid);
+	}
+	tkpath := ".fbrowse.fl.f0";
+	path := b.root;
+	lst = tl tl lst;
+#	sys->print("getpath: %s %s\n",tkpath, path);
+	qid := "";
+	for (; lst != nil; lst = tl lst) {
+		tkpath += "."+hd lst;
+		if ((hd lst)[0] == 'l') {
+			qid = getqidfromlabel(tkpath);
+			if (qid != nil)
+				qid = "Q" + qid;
+			if (len hd lst - len qid > 1)
+				path += tk->cmd(b.top, tkpath+" cget -text");
+		}
+		else if ((hd lst)[0] == 'f') {
+			qid = getqidfromframe(b,tkpath);
+			if (qid != nil)
+				qid = "Q"+qid;
+			path += tk->cmd(b.top, tkpath+".l"+qid+" cget -text") + "/";
+		}
+#		sys->print("getpath: %s %s\n",tkpath, path);
+	}
+	# Temporary hack!
+	if (qid != nil)
+		qid = qid[1:];
+	return ref File (path, qid);
+}
+
+setroot(b: ref Browse, rlabel, root: string)
+{
+	b.root = root;
+	b.rlabel = rlabel;
+	makedir(b, File (root, nil), ".fbrowse.fl.f0", rlabel, "0");
+	tkcmd(b.top, "grid forget .fbrowse.fl.f0.lp");
+}
+
+getqidfromframe(b: ref Browse, frame: string): string
+{
+	tmp := tkcmd(b.top, "grid slaves "+frame+" -row 0");
+	(nil, lst) := sys->tokenize(tmp, " \t\n");
+	if (lst == nil)
+		return nil;
+	return getqidfromlabel(hd tl lst);
+}
+
+getqidfromlabel(label: string): string
+{
+	p := isatback(label, "Q");
+	if (p != -1)
+		return label[p+1:];
+	return nil;
+}
+
+popdirpane0(b: ref Browse, dir : File, frame: string)
+{
+	(dirs, hasdups) := b.reader->readpath(dir);
+	for (i := 0; i < len dirs; i++) {
+		si := string i;
+		f : string;
+		dirqid := string dirs[i].qid.path;
+		if (!hasdups)
+			dirqid = nil;
+		if (dirs[i].mode & sys->DMDIR) {
+			f = frame + ".f"+si;
+			makedir(b, File (dir.path+dirs[i].name, dirqid), f, dirs[i].name, string (i+1));
+		}
+		else {
+			if (b.nopanes == 1) {
+				f = frame + ".l"+si;
+				makefile(b, f, dirs[i].name, string (i+1), dirqid);
+			}
+		}
+	}
+	dirs = nil;
+}
+
+isopened(b: ref Browse, dir: File): int
+{
+	for (tmp := b.opened; tmp != nil; tmp = tl tmp) {
+		if (File.eq(hd tmp, dir))
+			return 1;
+	}
+	return 0;
+}
+
+makefile(b: ref Browse, f, name, row, qid: string)
+{
+	if (qid != nil)
+		f += "Q" + qid;
+	bgcol := bgnorm;
+#	if (f == selected[0].t1)
+#		bgcol = bgselect;
+	p := isat(name, "\0");
+	if (p != -1) {
+		tkcmd(b.top, "label "+f+" -text {"+name[:p]+"} -bg "+bgcol+ft);
+		tkcmd(b.top, "label "+f+"b -text {"+name[p+1:]+"} -bg "+bgcol+ft);
+		tkcmd(b.top, "grid "+f+" -row "+row+" -column 1 -sticky w -padx 5 -pady 2");
+		tkcmd(b.top, "grid "+f+"b -row "+row+" -column 2 -sticky w -pady 2");
+		tkcmd(b.top, "bind "+f+" <Button-2> {send "+b.tkchan+" but2pane1 "+f+"}");
+		tkcmd(b.top, "bind "+f+" <ButtonRelease-2> {send "+b.tkchan+" release}");
+	}
+	else {
+		tkcmd(b.top, "label "+f+" -text {"+name+"} -bg "+bgcol+ft);
+		tkcmd(b.top, "grid "+f+" -row "+row+" -column 1 -sticky w -padx 5 -pady 2");
+	}
+	tkcmd(b.top, "bind "+f+" <Button-1> {send "+b.tkchan+" but1pane0 "+f+"}");
+	tkcmd(b.top, "bind "+f+" <ButtonRelease-1> {send "+b.tkchan+" release}");
+	tkcmd(b.top, "bind "+f+" <Button-2> {send "+b.tkchan+" but2pane0 "+f+"}");
+	tkcmd(b.top, "bind "+f+" <ButtonRelease-2> {send "+b.tkchan+" release}");
+	tkcmd(b.top, "bind "+f+" <Button-3> {send "+b.tkchan+" but3pane0 "+f+"}");
+	tkcmd(b.top, "bind "+f+" <ButtonRelease-3> {send "+b.tkchan+" release}");
+}
+
+Browse.defaultaction(b: self ref Browse, lst: list of string, rfile: ref File)
+{
+	tkpath: string;
+	file: File;
+	if (len lst > 1) {
+		tkpath = hd tl lst;
+		if (len tkpath > 11 && tkpath[:11] == ".fbrowse.fl") {
+			if (rfile == nil)
+				file = *b.getpath(tkpath);
+			else
+				file = *rfile;
+		}
+	}
+	case hd lst {
+		"release" =>
+			b.released = 1;
+		"open" or "double1pane0" =>
+			if (file.path == b.root)
+				break;
+			if (b.released) {
+				b.selectfile(0, DESELECT, File(nil, nil), nil);
+				b.selectfile(1, DESELECT, File(nil, nil), nil);
+				b.opendir(file, prevframe(tkpath), TOGGLE);
+				b.selectfile(0, SELECT, file, tkpath);
+				b.released = 0;
+			}
+		"double1pane1" =>
+			b.gotoselectfile(file);
+		"but1pane0" =>
+			if (b.released) {
+				b.selectfile(1, DESELECT, File(nil, nil), nil);
+				b.selectfile(0, TOGGLE, file, tkpath);
+				b.released = 0;
+			}
+ 		"but1pane1" =>
+			if (b.released) {
+				b.selectfile(1, TOGGLE, file, tkpath);
+				b.released = 0;
+			}
+ 		"movdiv" =>
+			movdiv(b, int hd tl lst);
+	}
+}
+
+prevframe(tkpath: string): string
+{
+	end := len tkpath;
+	for (;;) {
+		p := isatback(tkpath[:end], ".");
+		if (tkpath[p+1] == 'f')
+			return tkpath[:end];
+		end = p;
+	}
+	return nil;
+}
+
+makedir(b: ref Browse, dir: File, f, name, row: string)
+{
+	bgcol := bgnorm;
+	if (f == ".fbrowse.fl.f0")
+		dir = File (b.root, nil);
+#	if (name == "")
+#		name = path;
+	if (dir.path[len dir.path - 1] != '/')
+		dir.path[len dir.path] = '/';
+	if (File.eq(dir, b.selected[0].file))
+		bgcol = bgselect;
+	tkcmd(b.top, "frame "+f+" -bg white");
+	label := f+".l";
+	if (dir.qid != nil)
+		label += "Q" + dir.qid;
+	tkcmd(b.top, "label "+label+" -text {"+name+"} -bg "+bgcol+ftb);
+	if (isopened(b, dir)) {
+		popdirpane0(b, dir, f);
+		tkcmd(b.top, "label "+f+".lp -text {-} -borderwidth 1 -relief sunken -height 8 -width 8"+fts);
+	}
+	else tkcmd(b.top, "label "+f+".lp -text {+} -borderwidth 1 -relief raised -height 8 -width 8"+fts);
+	tkcmd(b.top, "bind "+label+" <Button-1> {send "+b.tkchan+" but1pane0 "+label+"}");
+	tkcmd(b.top, "bind "+label+" <Double-Button-1> {send "+b.tkchan+" double1pane0 "+label+"}");
+	tkcmd(b.top, "bind "+label+" <ButtonRelease-1> {send "+b.tkchan+" release}");
+	tkcmd(b.top, "bind "+label+" <Button-3> {send "+b.tkchan+" but3pane0 "+label+"}");
+	tkcmd(b.top, "bind "+label+" <ButtonRelease-3> {send "+b.tkchan+" release}");
+	tkcmd(b.top, "bind "+label+" <Button-2> {send "+b.tkchan+" but2pane0 "+label+"}");
+	tkcmd(b.top, "bind "+label+" <ButtonRelease-2> {send "+b.tkchan+" release}");
+
+	tkcmd(b.top, "bind "+f+".lp <Button-1> {send "+b.tkchan+" open "+label+"}");
+	tkcmd(b.top, "bind "+f+".lp <ButtonRelease-1> {send "+b.tkchan+" release}");
+	tkcmd(b.top, "grid "+f+".lp -row 0 -column 0");
+	tkcmd(b.top, "grid "+label+" -row 0 -column 1 -sticky w -padx 5 -pady 2 -columnspan 2");
+	tkcmd(b.top, "grid "+f+" -row "+row+" -column 1 -sticky w -padx 5 -columnspan 2");
+}
+
+popdirpane1(b: ref Browse, dir: File)
+{
+#	if (path == b.pane1.path && qid == b.pane1.qid)
+#		return;
+	b.pane1 = dir;
+	labelset(b, ".fbrowse.l", prevpath(dir.path+"/"));
+	if (b.nopanes == 1)
+		return;
+	tkcmd(b.top, "destroy .fbrowse.fl2; frame .fbrowse.fl2 -bg white");
+	tkcmd(b.top, ".fbrowse.c2 create window 0 0 -window .fbrowse.fl2 -anchor nw");
+	if (dir.path == nil) {
+		setbrowsescrollr(b);
+		return;
+	}
+	(dirs, hasdups) := b.reader->readpath(dir);
+#	if (path[len path - 1] == '/')
+#		path = path[:len path - 1];
+#	tkcmd(b.top, "label .fbrowse.fl2.l -text {"+path+"}");
+	row := 0;
+	col := 0;
+	tkcmd(b.top, ".fbrowse.c2 see 0 0");
+	ni := 0;
+	n := (int tkcmd(b.top, ".fbrowse.c2 cget -actheight")) / 21;
+	for (i := 0; i < len dirs; i++) {
+
+		f := ".fbrowse.fl2.l"+string ni;
+		if (hasdups)
+			f += "Q" + string dirs[i].qid.path;
+		name := dirs[i].name;
+		isdir := dirs[i].mode & sys->DMDIR;
+		if (isdir)
+			name[len name]= '/';
+		bgcol := bgnorm;
+		# Sort this out later
+		# if (path+"/"+name == selected[1].t0) {
+		#	bgcol = bgselect;
+		#	selected[1].t1 = f;
+		#}
+		tkcmd(b.top, "label "+f+" -text {"+name+"} -bg "+bgcol+ft);
+		tkcmd(b.top, "bind "+f+" <Double-Button-1> {send "+b.tkchan+" double1pane1 "+f+"}");
+		tkcmd(b.top, "bind "+f+" <Button-1> {send "+b.tkchan+" but1pane1 "+f+"}");
+		tkcmd(b.top, "bind "+f+" <ButtonRelease-1> {send "+b.tkchan+" release}");
+		tkcmd(b.top, "bind "+f+" <Button-3> {send "+b.tkchan+" but3pane1 "+f+" %X %Y}");
+		tkcmd(b.top, "bind "+f+" <ButtonRelease-3> {send "+b.tkchan+" release}");
+		tkcmd(b.top, "grid "+f+" -row "+string row+" -column "+string col+
+					" -sticky w -padx 10 -pady 2");
+		row++;
+		if (row >= n) {
+			row = 0;
+			col++;
+		}		
+		ni++;
+	}
+
+	dirs = nil;
+	setbrowsescrollr(b);
+}
+
+pane0scr := array[] of {
+	"frame .fbrowse",
+
+	"scrollbar .fbrowse.sy1 -command {.fbrowse.c1 yview}",
+	"scrollbar .fbrowse.sx1 -command {.fbrowse.c1 xview} -orient horizontal",
+	"canvas .fbrowse.c1 -yscrollcommand {.fbrowse.sy1 set} -xscrollcommand {.fbrowse.sx1 set} -bg white -width 50 -height 20 -borderwidth 2 -relief sunken -xscrollincrement 10 -yscrollincrement 21",
+	"grid .fbrowse.sy1 -row 2 -column 0 -sticky ns -rowspan 2",
+	"grid .fbrowse.sx1 -row 3 -column 1 -sticky ew",
+	"grid .fbrowse.c1 -row 2 -column 1 -sticky nsew",
+	"grid rowconfigure .fbrowse 2 -weight 1",
+	"grid columnconfigure .fbrowse 1 -weight 2",
+
+};
+
+pane1scr := array[] of {
+#	".fbrowse.c1 configure -width 146",
+	"frame .fbrowse.fl2 -bg white",
+	"label .fbrowse.fl2.l -text {}",
+	"scrollbar .fbrowse.sx2 -command {.fbrowse.c2 xview} -orient horizontal",
+	"label .fbrowse.lmov -text { } -relief sunken -borderwidth 2 -width 5",
+	
+	"canvas .fbrowse.c2 -xscrollcommand {.fbrowse.sx2 set} -bg white -width 50 -height 20 -borderwidth 2 -relief sunken -xscrollincrement 10 -yscrollincrement 21",
+	".fbrowse.c2 create window 0 0 -window .fbrowse.fl2 -anchor nw",
+	"grid .fbrowse.sx2 -row 3 -column 3 -sticky ew",
+	"grid .fbrowse.c2 -row 2 -column 3 -sticky nsew",
+	"grid .fbrowse.lmov -row 2 -column 2 -rowspan 2 -sticky ns",
+	"grid columnconfigure .fbrowse 3 -weight 3",
+};
+
+Browse.newroot(b: self ref Browse, root, rlabel: string)
+{
+	tk->cmd(b.top, "destroy .fbrowse.fl");
+	tkcmd(b.top, "frame .fbrowse.fl -bg white");
+	tkcmd(b.top, ".fbrowse.c1 create window 0 0 -window .fbrowse.fl -anchor nw");
+	b.pane1 = File (root, nil);
+	setroot(b, rlabel, root);
+	setbrowsescrollr(b);
+}
+
+Browse.showpath(b: self ref Browse, on: int)
+{
+	if (on == b.showpathlabel)
+		return;
+	if (on) {
+		b.showpathlabel = 1;
+		if (b.pane1.path != nil)
+			labelset(b, ".fbrowse.l", prevpath(b.pane1.path+"/"));
+	}
+	else {
+		b.showpathlabel = 0;
+		tkcmd(b.top, ".fbrowse.l configure -text {}");
+	}	
+}
+
+Browse.getselected(b: self ref Browse, pane: int): File
+{
+	return b.selected[pane].file;
+}
+
+labelset(b: ref Browse, label, text: string)
+{
+	if (!b.showpathlabel)
+		return;
+	if (text != nil) {
+		tmp := b.rlabel;
+		if (tmp[len tmp - 1] != '/')
+			tmp[len tmp] = '/';
+		text = tmp + text[len b.root:];
+	}
+	tkcmd(b.top, label + " configure -text {"+text+"}");
+}
+
+movdiv(b: ref Browse, x: int)
+{
+	x1 := int tkcmd(b.top, ".fbrowse.lmov cget -actx");
+	x2 := x1 + int tkcmd(b.top, ".fbrowse.lmov cget -width");
+	diff := 0;
+	if (x < x1)
+		diff = x - x1;
+	if (x > x2)
+		diff = x - x2;
+	if (abs(diff) > 5) {
+		w1 := int tkcmd(b.top, ".fbrowse.c1 cget -actwidth");
+		w2 := int tkcmd(b.top, ".fbrowse.c2 cget -actwidth");
+		if (w1 + diff < 36)
+			diff = 36 - w1;
+		if (w2 - diff < 36)
+			diff = w2 - 36;
+		w1 += diff;
+		w2 -= diff;
+		# sys->print("w1: %d w2: %d\n",w1,w2);
+		tkcmd(b.top, "grid columnconfigure .fbrowse 1 -weight "+string w1);
+		tkcmd(b.top, "grid columnconfigure .fbrowse 3 -weight "+string w2);
+	}
+}
+
+
+dialog(ctxt: ref draw->Context, oldtop: ref Tk->Toplevel, butlist: list of string, title, msg: string): int
+{
+	(top, titlebar) := tkclient->toplevel(ctxt, "", title, tkclient->Popup);
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	tkcmd(top, "frame .f");
+	tkcmd(top, "label .f.l -text {"+msg+"} -font /fonts/charon/plain.normal.font");
+	tkcmd(top, "bind .Wm_t <Button-1> +{focus .}");
+	tkcmd(top, "bind .Wm_t.title <Button-1> +{focus .}");
+
+	l := len butlist;
+	tkcmd(top, "grid .f.l -row 0 -column 0 -columnspan "+string l+" -sticky w -padx 10 -pady 5");
+	i := 0;
+	for(; butlist != nil; butlist = tl butlist) {
+		si := string i;
+		tkcmd(top, "button .f.b"+si+" -text {"+hd butlist+"} "+
+			"-font /fonts/charon/plain.normal.font -command {send butchan "+si+"}");
+		tkcmd(top, "grid .f.b"+si+" -row 1 -column "+si+" -padx 5 -pady 5");
+		i++;
+	}
+	placement := "";
+	if (oldtop != nil) {
+		setcentre(oldtop, top);
+		placement = "exact";
+	}
+	tkcmd(top, "pack .f; update; focus .");
+	tkclient->onscreen(top, placement);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <- butchan =>
+			tkcmd(oldtop, "focus .");
+			return int inp;
+		title = <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <-titlebar =>
+			if (title == "exit") {
+				tkcmd(oldtop, "focus .");
+				return -1;
+			}
+			tkclient->wmctl(top, title);
+		}
+	}
+}
+######################## Select Functions #########################
+
+
+setselectscrollr(s: ref Select, f: string)
+{
+	h := tkcmd(s.top, f+" cget -height");
+	w := tkcmd(s.top, f+" cget -width");
+	tkcmd(s.top, ".fselect.c configure -scrollregion {0 0 "+w+" "+h+"}");
+}
+
+Select.setscrollr(s: self ref Select, fname: string)
+{
+	frame := getframe(s, fname);
+	if (frame != nil)
+		setselectscrollr(s,frame.path);
+}
+
+Select.new(top: ref Tk->Toplevel, tkchanname: string): ref Select
+{
+	s: Select;
+	s.top = top;
+	s.tkchan = tkchanname;
+	s.frames = nil;
+	s.currfname = nil;
+	s.currfid = nil;
+	tkcmds(top, selectscr);
+	if (entryheight == nil) {
+		tkcmd(top, "entry .fselect.test");
+		entryheight = " -height " + tkcmd(top, ".fselect.test cget -height");
+		tkcmd(top, "destroy .fselect.test");
+	}
+	for (i := 1; i < 4; i++)
+		tkcmd(top, "bind .fselect.c <ButtonRelease-"+string i+"> {send "+s.tkchan+" release}");
+	return ref s;
+}
+
+selectscr := array[] of {
+	"frame .fselect",
+	"scrollbar .fselect.sy -command {.fselect.c yview}",
+	"scrollbar .fselect.sx -command {.fselect.c xview} -orient horizontal",
+	"canvas .fselect.c -yscrollcommand {.fselect.sy set} -xscrollcommand {.fselect.sx set} -bg white -width 414 -borderwidth 2 -relief sunken -height 180 -xscrollincrement 10 -yscrollincrement 19",
+
+	"grid .fselect.sy -row 0 -column 0 -sticky ns -rowspan 2",
+	"grid .fselect.sx -row 1 -column 1 -sticky ew",
+	"grid .fselect.c -row 0 -column 1",
+};
+
+Select.addframe(s: self ref Select, fname, title: string)
+{
+	if (isat(fname, " ") != -1)
+		return;
+	f := ".fselect.f"+fname;
+	tkcmd(s.top, "frame "+f+" -bg white");
+	if (title != nil){
+		tkcmd(s.top, "label "+f+".l -text {"+title+"} -bg white "+
+			"-font /fonts/charon/bold.normal.font; "+
+			"grid "+f+".l -row 0 -column 0 -columnspan 3 -sticky w");
+	}
+	fr: Frame;
+	fr.name = fname;
+	fr.path = f;
+	fr.selected = nil;
+	s.frames = ref fr :: s.frames;
+}
+
+getframe(s: ref Select, fname: string): ref Frame
+{
+	for (tmp := s.frames; tmp != nil; tmp = tl tmp)
+		if ((hd tmp).name == fname)
+			return hd tmp;
+	return nil;
+}
+
+Select.delframe(s: self ref Select, fname: string)
+{
+	if (s.currfname == fname) {
+		tkcmd(s.top, ".fselect.c delete " + s.currfid);
+		s.currfid = nil;
+		s.currfname = nil;
+	}
+	f := getframe(s,fname);
+	if (f != nil) {
+		tkcmd(s.top, "destroy "+f.path);
+		tmp: list of ref Frame = nil;
+		for (;s.frames != nil; s.frames = tl s.frames) {
+			if ((hd s.frames).name != fname)
+				tmp = hd s.frames :: tmp;
+		}
+		s.frames = tmp;
+	}
+}
+
+Select.showframe(s: self ref Select, fname: string)
+{
+	if (s.currfid != nil)
+		tkcmd(s.top, ".fselect.c delete " + s.currfid);
+	f := getframe(s, fname);
+	if (f != nil) {
+		s.currfid = tkcmd(s.top, ".fselect.c create window 0 0 "+
+				"-window "+f.path+" -anchor nw");
+		s.currfname = fname;
+	}
+}
+
+Select.addselection(s: self ref Select, fname, text: string, lp: list of ref Parameter, allowdups: int): string
+{
+	fr := getframe(s, fname);
+	if (fr == nil)
+		return nil;
+	f := fr.path;
+	if (!allowdups) {
+		slv := tkcmd(s.top, "grid slaves "+f+" -column 0");
+		(nil, slaves) := sys->tokenize(slv, " \t\n");
+		for (; slaves != nil; slaves = tl slaves) {
+			if (text == tkcmd(s.top, hd slaves+" cget -text"))
+				return nil;
+		}
+	}
+	font := " -font /fonts/charon/plain.normal.font";
+	fontb := " -font /fonts/charon/bold.normal.font";
+	(id, row) := newselected(s.top, f);
+	sid := string id;
+	label := f+".l"+sid;
+	tkcmd(s.top, "label "+label+" -text {"+text+"} -bg white"+entryheight+font);
+	gridpack := label+" ";
+	paramno := 0;
+	for (; lp != nil; lp = tl lp) {
+		spn := string paramno;
+		pframe := f+".f"+sid+"P"+spn;
+		tkcmd(s.top, "frame "+pframe+" -bg white");
+		pick p := hd lp {
+		ArgIn =>
+			tkp1 := pframe+".lA";
+			tkp2 := pframe+".eA";
+
+			tkcmd(s.top, "label "+tkp1+" -text {"+p.name+"} "+
+					"-bg white "+entryheight+fontb);
+			tkcmd(s.top, "entry "+tkp2+" -bg white -width 50 "+
+					"-borderwidth 1"+entryheight+font);
+			if (p.initval != nil)
+				tkcmd(s.top, tkp2+" insert end {"+p.initval+"}");
+			tkcmd(s.top, "grid "+tkp1+" "+tkp2+" -row 0");
+			
+		IntIn =>
+			tkp1 := pframe+".sI";
+			tkp2 := pframe+".lI";
+			tkcmd(s.top, "scale "+tkp1+" -showvalue 0 -orient horizontal -height 20"+
+				" -from "+string p.min+" -to "+string p.max+" -command {send "+
+				s.tkchan+" scale "+tkp2+"}");
+			tkcmd(s.top, tkp1+" set "+string p.initval);
+			tkcmd(s.top, "label "+tkp2+" -text {"+string p.initval+"} "+
+					"-bg white "+entryheight+fontb);
+			tkcmd(s.top, "grid "+tkp1+" "+tkp2+" -row 0");
+			
+		}
+		gridpack += " "+pframe;
+		paramno++;
+	}
+	tkcmd(s.top, "grid "+gridpack+" -row "+row+" -sticky w");
+	
+	sendstr := " " + label + " %X %Y}";
+	tkcmd(s.top, "bind "+label+" <Double-Button-1> {send "+s.tkchan+" double1"+sendstr);
+	tkcmd(s.top, "bind "+label+" <Button-1> {send "+s.tkchan+" but1"+sendstr);
+	tkcmd(s.top, "bind "+label+" <ButtonRelease-1> {send "+s.tkchan+" release}");
+	tkcmd(s.top, "bind "+label+" <Button-2> {send "+s.tkchan+" but2"+sendstr);
+	tkcmd(s.top, "bind "+label+" <ButtonRelease-2> {send "+s.tkchan+" release}");
+	tkcmd(s.top, "bind "+label+" <Button-3> {send "+s.tkchan+" but3"+sendstr);
+	tkcmd(s.top, "bind "+label+" <ButtonRelease-3> {send "+s.tkchan+" release}");
+	setselectscrollr(s, f);
+	if (s.currfname == fname) {
+		y := int tkcmd(s.top, label+"  cget -acty") -
+			int tkcmd(s.top, f+" cget -acty");
+		h := int tkcmd(s.top, label+"  cget -height");
+		tkcmd(s.top, ".fselect.c see 0 "+string (h+y));
+	}
+	return label;
+}
+
+newselected(top: ref Tk->Toplevel, frame: string): (int, string)
+{
+	(n, slaves) := sys->tokenize(tkcmd(top, "grid slaves "+frame+" -column 0"), " \t\n");
+	id := 0;
+	slaves = tl slaves; # Ignore Title
+	for (;;) {
+		if (isin(slaves, frame+".l"+string id))
+			id++;
+		else break;
+	}
+	return (id, string n);
+}
+
+isin(l: list of string, test: string): int
+{
+	for(tmpl := l; tmpl != nil; tmpl = tl tmpl)
+		if (hd tmpl == test)
+			return 1;
+	return 0;
+}
+
+Select.delselection(s: self ref Select, fname, tkpath: string)
+{
+	f := getframe(s, fname);
+	(row, nil) := getrowcol(s.top, tkpath);
+	slaves := tkcmd(s.top, "grid slaves "+f.path+" -row "+row);
+	# sys->print("row %s: deleting: %s\n",row,slaves);
+	tkcmd(s.top, "grid rowdelete "+f.path+" "+row);
+	tkcmd(s.top, "destroy "+slaves);
+	# Select the next one if the item deleted was selected
+	if (f.selected == tkpath) {
+		f.selected = nil;
+		for (;;) {
+			slaves = tkcmd(s.top, "grid slaves "+f.path+" -row "+row);
+			if (slaves != nil)
+				break;
+			r := (int row) - 1;
+			if (r < 1)
+				return;
+			row = string r;
+		}
+		(nil, lst) := sys->tokenize(slaves, " ");
+		if (lst != nil)
+			s.select(fname, hd lst, SELECT);
+	}
+}
+
+getrowcol(top: ref Tk->Toplevel, s: string): (string, string)
+{
+	row := "";
+	col := "";
+	(nil, lst) := sys->tokenize(tkcmd(top, "grid info "+s), " \t\n");
+	for (; lst != nil; lst = tl lst) {	
+		if (hd lst == "-row")
+			row = hd tl lst;
+		else if (hd lst == "-column")
+			col = hd tl lst;
+	}
+	return (row, col);
+}
+
+Select.select(s: self ref Select, fname, tkpath: string, action: int)
+{
+	f := getframe(s, fname);
+	if (action == SELECT && f.selected == tkpath)
+		return;
+	if (f.selected != nil)
+		tkcmd(s.top, f.selected+" configure -bg "+bgnorm);
+	if ((action == TOGGLE && f.selected == tkpath) || action == DESELECT)
+		f.selected = nil;
+	else {
+		tkcmd(s.top, tkpath+" configure -bg "+bgselect);
+		f.selected = tkpath;
+	}
+}
+
+Select.defaultaction(s: self ref Select, lst: list of string)
+{
+	case hd lst {
+		"but1" =>
+			s.select(s.currfname, hd tl lst, TOGGLE);
+		"scale" =>
+			tkcmd(s.top, hd tl lst+" configure -text {"+hd tl tl lst+"}");
+	}
+}
+
+Select.getselected(s: self ref Select, fname: string): string
+{
+	f := getframe(s, fname);
+	return f.selected;
+}
+
+Select.getselection(s: self ref Select, fname: string): list of (string, list of ref Parameter)
+{
+	retlist : list of (string, list of ref Parameter) = nil;
+	row := 1;
+	f := getframe(s, fname);
+	for (;;) {
+		slaves := tkcmd(s.top, "grid slaves "+f.path+" -row "+string (row++));
+		# sys->print("slaves: %s\n",slaves);
+		if (slaves == nil || slaves[0] == '!')
+			break;
+		(nil, lst) := sys->tokenize(slaves, " ");
+		tkpath := hd lst;
+		lst = tl lst;
+		lp : list of ref Parameter = nil;
+		for (; lst != nil; lst = tl lst) {
+			pslaves := tkcmd(s.top, "grid slaves "+hd lst);
+			(nil, plist) := sys->tokenize(pslaves, " ");
+			# sys->print("slaves of %s - hd plist: '%s'\n",hd lst, hd plist);
+			case (hd plist)[len hd plist - 3:] {
+				".eA" or ".lA" =>
+					argname := tkcmd(s.top, hd lst+".lA cget -text");
+					argval := tkcmd(s.top, hd lst+".eA get");
+					lp = ref Parameter.ArgOut(argname, argval) :: lp;
+				".sI" or ".lI" =>
+					val := int tkcmd(s.top, hd lst+".lI cget -text");
+					lp = ref Parameter.IntOut(val) :: lp;
+			}
+		}
+		retlist = (tkpath, lp) :: retlist;	
+	}
+	return retlist;
+}
+
+Select.resize(s: self ref Select, width, height: int)
+{
+	ws := int tkcmd(s.top, ".fselect.sy cget -width");
+	hs := int tkcmd(s.top, ".fselect.sx cget -height");
+
+	tkcmd(s.top, ".fselect.c configure -width "+string (width - ws - 8)+
+			" -height "+string (height - hs - 8));
+	f := getframe(s, s.currfname);
+	if (f != nil)
+		setselectscrollr(s, f.path);
+
+	tkcmd(s.top, "update");
+}
+
+File.eq(a,b: File): int
+{
+	if (a.path != b.path || a.qid != b.qid)
+		return 0;
+	return 1;
+}
+
+
+######################## General Functions ########################
+
+setcentre(top1, top2: ref Tk->Toplevel)
+{
+	x1 := int tkcmd(top1, ". cget -actx");
+	y1 := int tkcmd(top1, ". cget -acty");
+	h1 := int tkcmd(top1, ". cget -height");
+	w1 := int tkcmd(top1, ". cget -width");
+
+	h2 := int tkcmd(top2, ".f cget -height");
+	w2 := int tkcmd(top2, ".f cget -width");
+
+	newx := (x1 + (w1 / 2)) - (w2/2);
+	newy := (y1 + (h1 / 2)) - (h2/2);
+	tkcmd(top2, ". configure -x "+string newx+" -y "+string newy);
+}
+
+abs(x: int): int
+{
+	if (x < 0)
+		return -x;
+	return x;
+}
+
+prevpath(path: string): string
+{
+	if (path == nil)
+		return nil;
+	p := isatback(path[:len path - 1], "/");
+	if (p == -1)
+		return nil;
+	return path[:p+1];
+}
+
+isat(s, test: string): int
+{
+	if (len test > len s)
+		return -1;
+	for (i := 0; i < (1 + len s - len test); i++)
+		if (test == s[i:i+len test])
+			return i;
+	return -1;
+}
+
+isatback(s, test: string): int
+{
+	if (len test > len s)
+		return -1;
+	for (i := len s - len test; i >= 0; i--)
+		if (test == s[i:i+len test])
+			return i;
+	return -1;
+}
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != "" && e[0] == '!')
+		sys->print("Tk error: '%s': %s\n",cmd,e);
+	return e;
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	for (j := 0; j < len a; j++)
+		tkcmd(top, a[j]);
+}
+
+badmod(path: string)
+{
+	sys->print("Browser: failed to load: %s\n",path);
+	exit;
+}
--- /dev/null
+++ b/appl/grid/lib/browser.m
@@ -1,0 +1,97 @@
+Browser: module {
+
+	PATH: con "/dis/scheduler/browser.dis";
+
+	DESELECT: con 0;
+	SELECT: con 1;
+	TOGGLE: con 2;
+	OPEN: con 3;
+	CLOSE: con 4;
+
+	init: fn ();
+	dialog: fn (ctxt: ref draw->Context, oldtop: ref Tk->Toplevel, butlist: list of string, title, msg: string): int;
+	prevpath: fn (path: string): string;
+	setcentre: fn (top1, top2: ref Tk->Toplevel);
+
+	Browse: adt {
+		new: fn (top: ref Tk->Toplevel, tkchanname, root, rlabel: string, nopanes: int, reader: PathReader): ref Browse;
+		refresh: fn (b: self ref Browse);
+		defaultaction: fn (b: self ref Browse, lst: list of string, f: ref File);
+		getpath: fn (b: self ref Browse, tkpath: string): ref File;
+		opendir: fn (b: self ref Browse, file: File, tkpath: string, action: int): int;
+		newroot: fn (b: self ref Browse, root, rlabel: string);
+		changeview: fn (b: self ref Browse, nopanes: int);
+		selectfile: fn (b: self ref Browse, pane, action: int, file: File, tkpath: string);
+		gotoselectfile: fn (b: self ref Browse, file: File): string;
+		gotopath: fn (b: self ref Browse, dir: File, openfinal: int): (File, string);
+		getselected: fn (b: self ref Browse, pane: int): File;
+		addopened: fn (b: self ref Browse, file: File, add: int);
+		showpath: fn (b: self ref Browse, on: int);
+		resize: fn (b: self ref Browse);
+		top: ref Tk->Toplevel;
+		tkchan: string;
+		bgnorm, bgselect: string;
+		nopanes: int;
+		selected: array of Selected;
+		opened: list of File;
+		root, rlabel: string;
+		reader: PathReader;
+		pane1: File;
+		pane0width: string;
+		width: int;
+		showpathlabel: int;
+		released: int;
+	};
+
+	SELECTED: con 0;
+	UNSELECTED: con 1;
+	ALL: con 2;
+
+	Select: adt {
+		new: fn (top: ref Tk->Toplevel, tkchanname: string): ref Select;
+		addframe: fn (s: self ref Select, fname, title: string);
+		showframe: fn (s: self ref Select, fname: string);
+		delframe: fn (s: self ref Select, fname: string);
+		addselection: fn (s: self ref Select, fname, text: string, lp: list of ref Parameter, allowdups: int): string;
+		delselection: fn (s: self ref Select, fname, tkpath: string);
+		getselection: fn (s: self ref Select, fname: string): list of (string, list of ref Parameter);
+		getselected: fn (s: self ref Select, fname: string): string;
+		select: fn (s: self ref Select, fname, tkpath: string, action: int);
+		defaultaction: fn (s: self ref Select, lst: list of string);
+		resize: fn (s: self ref Select, width, height: int);
+		setscrollr: fn (s: self ref Select, fname: string);
+		top: ref Tk->Toplevel;
+		tkchan: string;
+		currfname, currfid: string;
+		frames: list of ref Frame;
+	};
+
+	Frame: adt {
+		name: string;
+		path: string;
+		selected: string;
+	};
+
+	Parameter: adt {
+		pick {
+		ArgIn =>
+			name, initval: string;
+		ArgOut =>
+			name, val: string;
+		IntIn =>
+			min, max, initval: int;
+		IntOut =>
+			val: int;
+		}
+	};
+
+	File: adt {
+		eq: fn (a,b: File): int;
+		path, qid: string;
+	};
+
+	Selected: adt {
+		file: File;
+		tkpath: string;
+	};
+};
--- /dev/null
+++ b/appl/grid/lib/fbrowse.b
@@ -1,0 +1,387 @@
+implement FBrowse;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+	draw: Draw;
+	Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "readdir.m";
+	readdir: Readdir;
+include "workdir.m";
+include "sh.m";
+	sh: Sh;
+include "grid/pathreader.m";
+	reader: PathReader;
+include "grid/browser.m";
+	browser: Browser;
+	Browse, Select, File, Parameter,
+	DESELECT, SELECT, TOGGLE: import browser;
+include "grid/fbrowse.m";
+
+br: ref Browse;
+
+init(ctxt : ref Draw->Context, title, root, currdir: string): string
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmod(Draw->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil)
+		badmod(Readdir->PATH);
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+	tkclient->init();
+	workdir := load Workdir Workdir->PATH;
+	if (workdir == nil)
+		badmod(Workdir->PATH);
+	sh = load Sh Sh->PATH;
+	if (sh == nil)
+		badmod(Sh->PATH);
+	browser = load Browser Browser->PATH;
+	if (browser == nil)
+		badmod(Browser->PATH);
+	browser->init();
+	reader = load PathReader "$self";
+	if (reader == nil)
+		sys->print("cannot load reader!\n");
+	sys->pctl(sys->NEWPGRP, nil);
+	if (root == nil)
+		root = "/";
+	sys->chdir(root);
+	if (currdir == nil)
+		currdir = workdir->init();
+	if (root[len root - 1] != '/')
+		root[len root] = '/';
+	if (currdir[len currdir - 1] != '/')
+		currdir[len currdir] = '/';
+	
+	(top, titlebar) := tkclient->toplevel(ctxt,"", title , tkclient->OK | tkclient->Appl);
+	browsechan := chan of string;
+	tk->namechan(top, browsechan, "browsechan");
+	br = Browse.new(top, "browsechan", root, root, 2, reader);
+	br.addopened(File (root, nil), 1);
+	br.gotoselectfile(File (currdir, nil));
+	for (ik := 0; ik < len mainscreen; ik++)
+		tkcmd(top,mainscreen[ik]);
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	
+	tkcmd(top, "pack .f -fill both -expand 1; pack propagate . 0");
+	tkcmd(top, ". configure -height 300 -width 300");
+
+	tkcmd(top, "update");
+	released := 1;
+	title = "";
+	
+	tkclient->onscreen(top, nil);
+	resize(top, ctxt.display.image);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+
+	path: string;
+
+	main: for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <-browsechan =>
+			(nil, lst) := sys->tokenize(inp, " \n\t");
+			selected := br.getselected(1);
+			case hd lst {
+				"double1pane1" =>
+					tkpath := hd tl lst;
+					file := br.getpath(tkpath);
+					br.defaultaction(lst, file);
+					(n, dir) := sys->stat(file.path);
+					if (n == -1 || dir.mode & sys->DMDIR)
+						break;
+					if ((len dir.name > 4 && dir.name[len dir.name - 4:] == ".dis") || 
+						dir.mode & 8r111)
+						spawn send(butchan, "run "+tkpath);
+					else if (dir.mode & 8r222)
+						spawn send(butchan, "write "+tkpath);
+					else if (dir.mode & 8r444)
+							spawn send(butchan, "open "+tkpath);
+				* =>
+					br.defaultaction(lst, nil);
+			}
+			if (!File.eq(selected, br.getselected(1)))
+				actionbutton(top, br.selected[1].file.path, br.selected[1].tkpath);
+			tkcmd(top, "update");
+		inp := <-butchan =>
+			(nil, lst) := sys->tokenize(inp, " \n\t");
+			case hd lst {
+				"refresh" =>
+					br.refresh();
+				"shell" =>
+					path = br.getselected(1).path;
+					if (path == nil)
+						sys->chdir(root);
+					else
+						sys->chdir(path);
+					sh->run(ctxt, "/dis/wm/sh.dis" :: nil);
+
+				"run" =>
+					spawn run(ctxt, br.getselected(1).path);
+				"read" =>							
+					wtitle := tkcmd(top, hd tl lst+" cget text");
+					spawn openfile(ctxt, br.getselected(1).path, wtitle,0);
+				"write" =>
+					wtitle := tkcmd(top, hd tl lst+" cget text");
+					spawn openfile(ctxt, br.getselected(1).path, wtitle,1);
+			}
+			tkcmd(top, "update");
+		
+		title = <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <-titlebar =>
+			if (title == "exit" || title == "ok")
+				break main;
+			e := tkclient->wmctl(top, title);
+			if (e != nil && e[0] == '!')
+				br.resize();
+		}
+	}
+	if (title == "ok")
+		return br.getselected(1).path;
+	return "";
+}
+
+send(chanout: chan of string, s: string)
+{
+	chanout <-= s;
+}
+
+resize(top: ref Tk->Toplevel, img: ref Draw->Image)
+{
+	if (img != nil) {
+		scw := img.r.dx();
+		sch := img.r.dy();
+		ww := int tkcmd(top, ". cget -width");
+		wh := int tkcmd(top, ". cget -height");
+		if (ww > scw)
+			tkcmd(top, ". configure -x 0 -width "+string scw);
+		if (wh > sch)
+			tkcmd(top, ". configure -y 0 -height "+string sch);
+	}
+}
+
+mainscreen := array[] of {
+	"frame .f",
+	"frame .f.ftop",
+	"button .f.ftop.bs -text {Shell} -command {send butchan shell} -font /fonts/charon/bold.normal.font",
+	"button .f.ftop.br -text {Refresh} -command {send butchan refresh} -font /fonts/charon/bold.normal.font",
+	"grid .f.ftop.bs .f.ftop.br -row 0",
+	"grid columnconfigure .f.ftop 2 -minsize 30",
+	"grid .f.ftop -row 0 -column 0 -pady 2 -sticky w",
+	"label .f.l -text { } -height 1 -bg red",
+	"grid .f.l -row 1 -sticky ew",
+	"grid .fbrowse -in .f -row 2 -column 0 -sticky nsew",
+	"grid rowconfigure .f 2 -weight 1",
+	"grid columnconfigure .f 0 -weight 1",
+
+	"bind .Wm_t <Button-1> +{focus .Wm_t}",
+	"bind .Wm_t.title <Button-1> +{focus .Wm_t}",
+	"focus .Wm_t",
+};
+
+readpath(file: File): (array of ref sys->Dir, int)
+{
+	(dirs, nil) := readdir->init(file.path, readdir->NAME | readdir->COMPACT);
+	return (dirs, 0);
+}
+
+run(ctxt: ref Draw->Context, file: string)
+{
+	sys->pctl(sys->FORKNS | sys->NEWPGRP, nil);
+	sys->chdir(browser->prevpath(file));
+	sh->run(ctxt, file :: nil);
+}
+
+openscr := array[] of {
+	"frame .f",
+	"scrollbar .f.sy -command {.f.t yview}",
+	"text .f.t -yscrollcommand {.f.sy set} -bg white -font /fonts/charon/plain.normal.font",
+	"pack .f.sy -side left -fill y",
+	"pack .f.t -fill both -expand 1",
+	"bind .Wm_t <Button-1> +{focus .Wm_t}",
+	"bind .Wm_t.title <Button-1> +{focus .Wm_t}",
+	"focus .f.t",
+};
+
+fopensize := ("", "");
+
+plumbing := array[] of {
+	("bit", "wm/view"),
+	("jpg", "wm/view"),
+};
+
+freader(top: ref Tk->Toplevel, fd: ref sys->FD, sync: chan of int)
+{
+	sync <-= sys->pctl(0,nil);
+	buf := array[8192] of byte;
+	for (;;) {
+		i := sys->read(fd, buf, len buf);
+		if (i < 1)
+			return;
+		s :="";
+		for (j := 0; j < i; j++) {
+			c := int buf[j];
+			if (c == '{' || c == '}')
+				s[len s] = '\\';
+			s[len s] = c;
+		}
+		tk->cmd(top, ".f.t insert end {"+s+"}; update");
+	}
+}
+
+openfile(ctxt: ref draw->Context, file, title: string, writeable: int)
+{
+	ext := getext(file);
+	plumb := getplumb(ext);
+	if (plumb != nil) {
+		sh->run(ctxt, plumb :: file :: nil);
+		return;
+	}
+	button := tkclient->Appl;
+	if (writeable)
+		button = button | tkclient->OK;
+	(top, titlebar) := tkclient->toplevel(ctxt, "", title, button);
+	tkcmds(top, openscr);
+	tkcmd(top,"pack .f -fill both -expand 1");
+	tkcmd(top,"pack propagate . 0");
+	(w,h) := fopensize;
+	if (w != "" && h != "")
+		tkcmd(top, ". configure -width "+w+" -height "+h);
+	killpid := -1;
+	fd := sys->open(file, sys->OREAD);
+	if (fd != nil) {
+		sync := chan of int;
+		spawn freader(top, fd, sync);
+		killpid = <-sync;
+	}
+	tkcmd(top, "update");
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	main: for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+
+		title = <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <-titlebar =>
+			if (title == "exit" || title == "ok")
+				break main;
+			tkclient->wmctl(top, title);
+		}
+	}
+	if (killpid != -1)
+		kill(killpid);
+	fopensize = (tkcmd(top, ". cget -width"), tkcmd(top, ". cget -height"));
+	if (title == "ok") {
+		(n, dir) := sys->stat(file);
+		if (n != -1) {
+			fd = sys->create(file, sys->OWRITE, dir.mode);
+			if (fd != nil) {
+				s := tkcmd(top, ".f.t get 1.0 end");
+				sys->fprint(fd,"%s",s);
+				fd = nil;
+			}
+		}
+	}
+}	
+
+badmod(path: string)
+{
+	sys->print("FBrowse: failed to load: %s\n",path);
+	exit;
+}
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != "" && e[0] == '!')
+		sys->print("Tk error: '%s': %s\n",cmd,e);
+	return e;
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	for (j := 0; j < len a; j++)
+		tkcmd(top, a[j]);
+}
+
+nactionbuttons := 0;
+actionbutton(top: ref Tk->Toplevel, path, tkpath: string)
+{
+	(n, dir) := sys->stat(path);
+	for (i := 0; i < nactionbuttons; i++) {
+		tkcmd(top, "grid forget .f.ftop.baction"+string i);
+		tkcmd(top, "destroy .f.ftop.baction"+string i);
+	}
+	if (path == nil || n == -1 || dir.mode & sys->DMDIR) {
+		nactionbuttons = 0;
+		return;
+	}
+	buttons : list of (string,string) = nil;
+	
+	if (dir.mode & 8r222)
+		buttons = ("Open", "write "+tkpath) :: buttons;
+	else if (dir.mode & 8r444)
+		buttons = ("Open", "read "+tkpath) :: buttons;
+	if (len dir.name > 4 && dir.name[len dir.name - 4:] == ".dis" || dir.mode & 8r111)
+		buttons = ("Run", "run "+tkpath) :: buttons;
+
+	nactionbuttons = len buttons;
+	for (i = 0; i < nactionbuttons; i++) {
+		name := ".f.ftop.baction"+string i+" ";
+		(text,cmd) := hd buttons;
+		tkcmd(top, "button "+name+"-text {"+text+"} "+
+				"-font /fonts/charon/bold.normal.font "+
+				"-command {send butchan "+cmd+"}");
+		tkcmd(top, "grid "+name+" -row 0 -column "+string (4+i));
+		buttons = tl buttons;
+	}
+}
+
+getext(file: string): string
+{
+	(nil, lst) := sys->tokenize(file, ".");
+	for (; tl lst != nil; lst = tl lst)
+		;
+	return hd lst;
+}
+
+getplumb(ext: string): string
+{
+	for (i := 0; i < len plumbing; i++)
+		if (ext == plumbing[i].t0)
+			return plumbing[i].t1;
+	return nil;
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "kill");
+}
--- /dev/null
+++ b/appl/grid/lib/mkfile
@@ -1,0 +1,27 @@
+<../../../mkconfig
+
+TARG=	announce.dis\
+		browser.dis\
+		fbrowse.dis\
+		srvbrowse.dis\
+
+MODULES=\
+
+SYSMODULES= \
+	draw.m\
+	grid/announce.m\
+	grid/browser.m\
+	grid/fbrowse.m\
+	grid/pathreader.m\
+	grid/srvbrowse.m\
+	readdir.m\
+	registries.m\
+	sh.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+	workdir.m\
+
+DISBIN=$ROOT/dis/grid/lib
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/grid/lib/pathreader.m
@@ -1,0 +1,3 @@
+PathReader : module {
+	readpath: fn (dir: Browser->File): (array of ref sys->Dir, int);
+};
--- /dev/null
+++ b/appl/grid/lib/srvbrowse.b
@@ -1,0 +1,719 @@
+implement Srvbrowse;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+	draw: Draw;
+	Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "grid/srvbrowse.m";
+include "registries.m";
+	registries: Registries;
+	Registry, Attributes, Service: import registries;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmod(Draw->PATH);
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+	tkclient->init();
+	registries = load Registries Registries->PATH;
+	if (registries == nil)
+		badmod(Registries->PATH);
+	registries->init();
+	reg = Registry.new("/mnt/registry");
+	if (reg == nil) {
+		reg = Registry.connect(nil, nil, nil);
+		if (reg == nil)
+			error("Could not find registry");
+	}
+	qids = array[511] of { * => "" };
+}
+
+reg : ref Registry;
+qids : array of string;
+
+# Qid stuff is a bit rubbish at the mo but waiting for registries to change: 
+#	currently address is unique but will not be in the future so waiting
+#	for another id to uniquely identify a resource
+
+addqid(srvc: ref Service): int
+{
+	addr := srvc.addr;
+	qid := addr2qid(addr);
+	for (;;) {
+		if (qids[qid] == nil)
+			break;
+		else if (qids[qid] == addr)
+			return qid;
+		qid++;
+		if (qid >= len qids)
+			qid = 0;				
+	}
+	qids[qid] = addr;
+#	sys->print("adding %s (%s) to %d\n",srvc.attrs.get("resource"), addr, qid);
+	return qid;
+}
+
+getqid(srvc: ref Service): string
+{
+	addr := srvc.addr;
+	qid := addr2qid(addr);
+	startqid := qid;
+	for (;;) {
+		if (qids[qid] == addr)
+			return string qid;
+		qid++;
+		if (qid == startqid)
+			break;
+		if (qid >= len qids)
+			qid = 0;				
+	}
+	return nil;
+}
+
+addr2qid(addr: string): int
+{
+	qid := 0;
+	# assume addr starts 'tcp!...'
+	for (i := 4; i < len addr; i++) {
+		qid += addr[i] * 2**(i%10);
+		qid = qid % len qids;
+	}
+	return qid;
+}
+
+addservice(srvc: ref Service)
+{
+	services = srvc :: services;
+	addqid(srvc);
+}
+
+find(filter: list of list of (string, string)): list of ref Service
+{
+	lsrv : list of ref Service = nil;
+	if (filter == nil)
+		(lsrv, nil) = reg.services();
+	else {
+		for (; filter != nil; filter = tl filter) {
+			attr := hd filter;
+			(s, nil) := reg.find(attr);		
+			for (; s != nil; s = tl s)
+				lsrv = hd s :: lsrv;
+		}
+	}
+	return sortservices(lsrv);
+}
+
+refreshservices(filter: list of list of (string, string))
+{
+	services = find(filter);
+}
+
+servicepath2Service(path, qid: string): list of ref Service
+{
+	srvl : list of ref Service = nil;
+	(nil, lst) := sys->tokenize(path, "/");
+	pname: string;
+	l := len lst;
+	if (l < 2 || l > 3)
+		return nil;
+	presource := hd tl lst;
+	if (l == 3)
+		pname = hd tl tl lst;
+	
+	for (tmpl := services; tmpl != nil; tmpl = tl tmpl) {
+		srvc := hd tmpl;
+		(resource, name) := getresname(srvc);
+		if (l == 2) {
+			if (resource == presource)
+				srvl = srvc :: srvl;
+		}
+		else if (l == 3) {
+			if (resource == presource) {
+				if (name == pname && qid == getqid(srvc)) {
+					srvl = srvc :: srvl;
+					break;
+				}
+			}
+		}
+	}
+	return srvl;
+}
+
+servicepath2Dir(path: string, qid: int): (array of ref sys->Dir, int)
+{
+	# sys->print("srvcPath2Dir: '%s' %d\n",path, qid);
+	res : list of (string, string) = nil;
+	(nil, lst) := sys->tokenize(path, "/");
+	presource, pname: string;
+	pattrib := 0;
+	l := len lst;
+	if (l > 1)
+		presource = hd tl lst;
+	if (l > 2)
+		pname = hd tl tl lst;
+	if (l == 4 && hd tl tl tl lst == "attributes")
+			pattrib = 1;	
+	for (tmpl := services; tmpl != nil; tmpl = tl tmpl) {
+		srvc := hd tmpl;
+		(resource, name) := getresname(srvc);
+		if (l == 1) {
+			if (!isin(res, resource))
+				res = (resource, nil) :: res;
+		}
+		else if (l == 2) {
+			if (resource == presource)
+				res = (name, string getqid(srvc)) :: res;
+		}
+		else if (l == 3) {
+			if (resource == presource && name == pname) {
+				if (qid == int getqid(srvc)) {
+					if (srvc.addr[0] == '@')
+						res = (srvc.addr[1:], string getqid(srvc)) :: res;
+					else {
+						if (srvc.attrs != nil)
+							res = ("attributes", string getqid(srvc)) :: res;
+						res = ("address:\0"+srvc.addr+"}", string getqid(srvc)) :: res;
+					}
+					break;
+				}
+			}
+		}
+		else if (l == 4) {
+			if (resource == presource && name == pname && pattrib) {
+				if (qid == int getqid(srvc)) {
+					for (tmpl2 := srvc.attrs.attrs; tmpl2 != nil; tmpl2 = tl tmpl2) {
+						(attrib, val) := hd tmpl2;
+						if (attrib != "name" && attrib != "resource")
+							res = (attrib+":\0"+val, string getqid(srvc)) :: res;
+					}
+					break;
+				}
+			}
+		}
+	}
+	resa := array [len res] of ref sys->Dir;
+	i := len resa - 1;
+	for (; res != nil; res = tl res) {
+		dir : sys->Dir;
+		qid: string;
+		(dir.name, qid) = hd res;
+		if (l < 3 || dir.name == "attributes")
+			dir.mode = 8r777 | sys->DMDIR;
+		else
+			dir.mode = 8r777;
+		if (qid != nil)
+			dir.qid.path = big qid;
+		resa[i--] = ref dir;
+	}
+	dups := 0;
+	if (l >= 2)
+		dups = 1;
+	return (resa, dups);
+}
+
+isin(l: list of (string, string), s: string): int
+{
+	for (; l != nil; l = tl l)
+		if ((hd l).t0 == s)
+			return 1;
+	return 0;
+}
+
+getresname(srvc: ref Service): (string, string)
+{
+	resource := srvc.attrs.get("resource");
+	if (resource == nil)
+		resource = "Other";
+	name := srvc.attrs.get("name");
+	if (name == nil)
+		name = "?????";
+	return (resource,name);
+}
+
+badmod(path: string)
+{
+	sys->print("Srvbrowse: failed to load: %s\n",path);
+	exit;
+}
+
+sortservices(lsrv: list of ref Service): list of ref Service
+{
+	a := array[len lsrv] of ref Service;
+	i := 0;
+	for (; lsrv != nil; lsrv = tl lsrv) {
+		addqid(hd lsrv);
+		a[i++] = hd lsrv;
+	}
+	heapsort(a);
+	lsrvsorted: list of ref Service = nil;
+	for (i = len a - 1; i >= 0; i--)
+		lsrvsorted = a[i] :: lsrvsorted;
+	return lsrvsorted;
+}
+
+
+heapsort(a: array of ref Service)
+{
+	for (i := (len a / 2) - 1; i >= 0; i--)
+		movedownheap(a, i, len a - 1);
+
+	for (i = len a - 1; i > 0; i--) {
+		tmp := a[0];
+		a[0] = a[i];
+		a[i] = tmp;
+		movedownheap(a, 0, i - 1);
+	}
+}
+
+movedownheap(a: array of ref Service, root, end: int)
+{
+	max: int;
+	while (2*root <= end) {
+		r2 := root * 2;
+		if (2*root == end || comp(a[r2], a[r2+1]) == GT)
+			max = r2;
+		else
+			max = r2 + 1;
+
+		if (comp(a[root], a[max]) == LT) {
+			tmp := a[root];
+			a[root] = a[max];
+			a[max] = tmp;
+			root = max;
+		}
+		else
+			break;
+	}	
+}
+
+LT: con -1;
+EQ: con 0;
+GT: con 1;
+
+comp(a1, a2: ref Service): int
+{
+	(resource1, name1) := getresname(a1);
+	(resource2, name2) := getresname(a2);
+	if (resource1 < resource2)
+		return LT;
+	if (resource1 > resource2)
+		return GT;
+	if (name1 < name2)
+		return LT;
+	if (name1 > name2)
+		return GT;
+	return EQ;
+}
+
+error(e: string)
+{
+	sys->fprint(sys->fildes(2), "Srvbrowse: %s\n", e);
+	raise "fail:error";
+}
+
+searchscr := array[] of {
+	"frame .f",
+	"scrollbar .f.sy -command {.f.c yview}",
+	"scrollbar .f.sx -command {.f.c xview} -orient horizontal",
+	"canvas .f.c -yscrollcommand {.f.sy set} -xscrollcommand {.f.sx set} -bg white -width 414 -borderwidth 2 -relief sunken -height 180 -xscrollincrement 10 -yscrollincrement 19",
+	"grid .f.sy -row 0 -column 0 -sticky ns -rowspan 2",
+	"grid .f.sx -row 1 -column 1 -sticky ew",
+	"grid .f.c -row 0 -column 1",
+	"pack .f -fill both -expand 1 ; pack propagate . 0; update",
+};
+
+SEARCH, RESULTS: con iota;
+
+searchwin(ctxt: ref Draw->Context, chanout: chan of string, filter: list of list of (string, string))
+{
+	(top, titlebar) := tkclient->toplevel(ctxt,"","Search", tkclient->Appl);
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	tkcmds(top, searchscr);
+	makesearchframe(top);
+	flid := setframe(top, ".fsearch", nil);
+	selected := "";
+	lresults : list of ref Service = nil;
+	resultstart := 0;
+	resize(top, 368,220);
+	maxresults := getmaxresults(top);
+	currmode := SEARCH;
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+
+	main: for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <-butchan =>
+			(nil, lst) := sys->tokenize(inp, " ");
+			case hd lst {
+				"key" =>
+					s := " ";
+					id := hd tl lst;
+					nv := hd tl tl lst;
+					tkp : string;
+					if (id != "-1")
+						tkp = ".fsearch.ea"+nv+id;
+					else
+						tkp = ".fsearch.e"+nv;
+					char := int hd tl tl tl lst;
+					s[0] = char;
+					if (char == '\n' || char == '\t') {
+						newtkp := ".fsearch";
+						if (nv == "n")
+							newtkp += ".eav"+id;
+						else if (nv == "v") {
+							newid := string ((int id)+1);
+							e := tk->cmd(top, ".fsearch.ean"+newid+" cget -width");
+							if (e == "" || e[0] == '!') {
+								insertattribrow(top);
+								newtkp += ".ean"+newid;
+							}
+							else
+								newtkp += ".ean"+newid;
+						}
+						focus(top, newtkp);
+					}
+					else {
+						tkcmd(top, tkp+" insert insert {"+s+"}");
+						tkcmd(top, tkp+" see "+tkcmd(top, tkp+" index insert"));
+					}
+				"go" =>
+					lresults = search(top, filter);
+					resultstart = 0;
+					makeresultsframe(top, lresults, 0, maxresults);
+					selected = nil;
+					flid = setframe(top, ".fresults", flid);
+					currmode = RESULTS;
+					if (chanout != nil)
+						chanout <-= "search search";
+				"prev" =>
+					selected = nil;
+					resultstart -= maxresults;
+					if (resultstart < 0)
+						resultstart = 0;
+					makeresultsframe(top, lresults, resultstart, maxresults);
+					flid = setframe(top, ".fresults", flid);
+				"next" =>
+					selected = nil;
+					if (resultstart < 0)
+						resultstart = 0;
+					resultstart += maxresults;
+					if (resultstart >= len lresults)
+						resultstart -= maxresults;
+					makeresultsframe(top, lresults, resultstart, maxresults);
+					flid = setframe(top, ".fresults", flid);
+				"backto" =>
+					flid = setframe(top, ".fsearch", flid);
+					tkcmd(top, ".f.c see 0 "+tkcmd(top, ".fsearch cget -height"));
+					currmode = SEARCH;
+				"new" =>
+					resetsearchscr(top);
+					tkcmd(top, ".f.c see 0 0");
+					setscrollr(top, ".fsearch");
+				"select" =>
+					if (selected != nil)
+						tkcmd(top, selected+" configure -bg white");
+					if (selected == hd tl lst)
+						selected = nil;
+					else {
+						selected = hd tl lst;
+						tkcmd(top, hd tl lst+" configure -bg #5555FF");
+						if (chanout != nil)
+							chanout <-= "search select " +
+										tkcmd(top, selected+" cget -text") +													" " + hd tl tl lst;
+					}
+			}
+			tkcmd(top, "update");
+		title := <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <-titlebar =>
+			if (title == "exit" || title == "ok")
+				break main;
+			e := tkclient->wmctl(top, title);
+			if (e == nil && title[0] == '!') {
+				(nil, lst) := sys->tokenize(title, " \t\n");
+				if (len lst >= 2 && hd lst == "!size" && hd tl lst == ".") {
+					resize(top, -1,-1);
+					maxresults = getmaxresults(top);
+					if (currmode == RESULTS) {
+						makeresultsframe(top, lresults, resultstart, maxresults);
+						flid = setframe(top, ".fresults", flid);
+						tkcmd(top, "update");
+					}
+				}
+			}
+		}
+	}
+
+}
+
+getmaxresults(top: ref Tk->Toplevel): int
+{
+	val := ((int tkcmd(top, ".f.c cget -height")) - 65)/17;
+	if (val < 1)
+		return 1;
+	return val;
+}
+
+setframe(top: ref Tk->Toplevel, f, oldflid: string): string
+{
+	if (oldflid != nil)
+		tkcmd(top, ".f.c delete " + oldflid);
+	newflid := tkcmd(top, ".f.c create window 0 0 -window "+f+" -anchor nw");
+	setscrollr(top, f);
+	return newflid;
+}
+
+setscrollr(top: ref Tk->Toplevel, f: string)
+{
+	h := tkcmd(top, f+" cget -height");
+	w := tkcmd(top, f+" cget -width");
+	tkcmd(top, ".f.c configure -scrollregion {0 0 "+w+" "+h+"}");
+}
+
+resize(top: ref Tk->Toplevel, width, height: int)
+{
+	if (width == -1) {
+		width = int tkcmd(top, ". cget -width");
+		height = int tkcmd(top, ". cget -height");
+	}
+	else
+		tkcmd(top, sys->sprint(". configure -width %d -height %d", width, height));
+	htitle := int tkcmd(top, ".f cget -acty") - int tkcmd(top, ". cget -acty");
+	height -= htitle;
+	ws := int tkcmd(top, ".f.sy cget -width");
+	hs := int tkcmd(top, ".f.sx cget -height");
+
+	tkcmd(top, ".f.c configure -width "+string (width - ws - 8)+
+			" -height "+string (height - hs - 8));
+
+	tkcmd(top, "update");
+}
+
+makesearchframe(top: ref Tk->Toplevel)
+{
+	font := " -font /fonts/charon/plain.normal.font";
+	fontb := " -font /fonts/charon/bold.normal.font";
+	f := ".fsearch";
+
+	tkcmd(top, "frame "+f+" -bg white");
+	tkcmd(top, "label "+f+".l -text {Search for Resource Attributes} -bg white" + fontb);
+	tkcmd(top, "grid "+f+".l -row 0 -column 0 -columnspan 3 -sticky nw");
+
+	tkcmd(top, "grid rowconfigure "+f+" 0 -minsize 30");
+	tkcmd(top, "frame "+f+".fgo -bg white");
+	tkcmd(top, "button "+f+".bs -text {Search} -command {send butchan go} "+font);
+	tkcmd(top, "button "+f+".bc -text {Clear} -command {send butchan new} "+font);
+	tkcmd(top, "grid "+f+".bs -row 3 -column 0 -sticky e -padx 2 -pady 5");
+	tkcmd(top, "grid "+f+".bc -row 3 -column 1 -sticky w -pady 5");
+	
+	tkcmd(top, "label "+f+".la1 -text {name} -bg white "+fontb);
+	tkcmd(top, "label "+f+".la2 -text {value} -bg white "+fontb);
+
+	tkcmd(top, "grid "+f+".la1 "+f+".la2 -row 1");
+
+	insertattribrow(top);
+}
+
+insertattribrow(top: ref Tk->Toplevel)
+{
+	(n, nil) := sys->tokenize(tkcmd(top, "grid slaves .fsearch -column 1"), " \t\n");
+	row := string (n);
+	sn := string (n - 2);
+	fsn := ".fsearch.ean"+sn;
+	fsv := ".fsearch.eav"+sn;
+	font := " -font /fonts/charon/plain.normal.font";
+	tkcmd(top, "entry "+fsn+" -width 170 -borderwidth 0 "+font);
+	tkcmd(top, "bind "+fsn+" <Key> {send butchan key "+sn+" n %s}");
+	tkcmd(top, "entry "+fsv+" -width 170 -borderwidth 0 "+font);
+	tkcmd(top, "bind "+fsv+" <Key> {send butchan key "+sn+" v %s}");
+	tkcmd(top, "grid rowinsert .fsearch "+row);
+	tkcmd(top, "grid "+fsn+" -column 0 -row "+row+" -sticky w -pady 1 -padx 2");
+	tkcmd(top, "grid "+fsv+" -column 1 -row "+row+" -sticky w -pady 1");
+	setscrollr(top, ".fsearch");
+}
+
+min(a,b: int): int
+{
+	if (a < b)
+		return a;
+	return b;
+}
+
+max(a,b: int): int
+{
+	if (a > b)
+		return a;
+	return b;
+}
+
+makeresultsframe(top: ref Tk->Toplevel, lsrv: list of ref Service, resultstart, maxresults: int)
+{
+	font := " -font /fonts/charon/plain.normal.font";
+	fontb := " -font /fonts/charon/bold.normal.font";
+	f := ".fresults";
+	nresults := len lsrv;
+	row := 0;
+	n := 0;
+	tk->cmd(top, "destroy "+f);
+	tkcmd(top, "frame "+f+" -bg white");
+	title := "Search Results";
+	if (nresults > 0) {
+		from := resultstart+1;
+		too := min(resultstart+maxresults, nresults);
+		if (from == too)
+			title += sys->sprint(" (displaying match %d of %d)", from, nresults);
+		else
+			title += sys->sprint(" (displaying matches %d - %d of %d)", from, too, nresults);
+	}
+	tkcmd(top, "label "+f+".l -text {"+title+"} -bg white -anchor w" + fontb);
+	w1 := int tkcmd(top, f+".l cget -width");
+	w2 := int tkcmd(top, ".f.c cget -width");
+	tkcmd(top, f+".l configure -width "+string max(w1,w2));
+	tkcmd(top, "grid "+f+".l -row 0 -column 0 -columnspan 3 -sticky nw");
+
+	tkcmd(top, "grid rowconfigure "+f+" 0 -minsize 30");
+	tkcmd(top, "frame "+f+".f -bg white");
+	for (; lsrv != nil; lsrv = tl lsrv) {
+		if (n >= resultstart && n < resultstart + maxresults) {
+			srvc := hd lsrv;
+			(resource, name) := getresname(srvc);
+			qid := getqid(srvc);
+			if (qid == nil)
+				qid = string addqid(srvc);
+			label := f+".f.lQ"+qid;
+			tkcmd(top, "label "+label+" -bg white -text {services/"+
+				resource+"/"+name+"/}"+font);
+			tkcmd(top, "grid "+label+" -row "+string row+" -column 0 -sticky w");
+			tkcmd(top, "bind "+label+" <Button-1> {send butchan select "+label+" "+qid+"}");
+			row++;
+		}
+		n++;
+	}
+	if (nresults == 0) {
+		tkcmd(top, "label "+f+".f.l0 -bg white -text {No matches found}"+font);
+		tkcmd(top, "grid "+f+".f.l0 -row 0 -column 0 -columnspan 3 -sticky w");
+	}
+	else {
+		tkcmd(top, "button "+f+".bprev -text {<<} "+
+				"-command {send butchan prev}"+font);
+		if (resultstart == 0)
+			tkcmd(top, f+".bprev configure -state disabled");
+		tkcmd(top, "button "+f+".bnext -text {>>} "+
+				"-command {send butchan next}"+font);
+		if (resultstart + maxresults >= nresults)
+			tkcmd(top, f+".bnext configure -state disabled");
+		tkcmd(top, "grid "+f+".bprev -column 0 -row 2 -padx 5 -pady 5");
+		tkcmd(top, "grid "+f+".bnext -column 2 -row 2 -padx 5 -pady 5");
+	}
+	tkcmd(top, "grid "+f+".f -row 1 -column 0 -columnspan 3 -sticky nw");
+	tkcmd(top, "grid rowconfigure "+f+" 1 -minsize "+string (maxresults*17));
+	tkcmd(top, "button "+f+".bsearch -text {Back to Search} "+
+			"-command {send butchan backto}"+font);
+	tkcmd(top, "grid "+f+".bsearch -column 1 -row 2 -padx 5 -pady 5");
+}
+
+focus(top: ref Tk->Toplevel, newtkp: string)
+{
+	tkcmd(top, "focus "+newtkp);
+	x1 := int tkcmd(top, newtkp + " cget -actx")
+			- int tkcmd(top, ".fsearch cget -actx");
+	y1 := int tkcmd(top, newtkp + " cget -acty")
+			- int tkcmd(top, ".fsearch cget -acty");
+	x2 := x1 + int tkcmd(top, newtkp + " cget -width");
+	y2 := y1 + int tkcmd(top, newtkp + " cget -height") + 45;
+	tkcmd(top, sys->sprint(".f.c see %d %d %d %d", x1,y1-30,x2,y2));
+}
+
+search(top: ref Tk->Toplevel, filter: list of list of (string, string)): list of ref Service
+{	
+	searchattrib: list of (string, string) = nil;
+	(n, nil) := sys->tokenize(tkcmd(top, "grid slaves .fsearch -column 0"), " \t\n");
+	for (i := 0; i < n - 3; i++) {
+		attrib := tkcmd(top, ".fsearch.ean"+string i+" get");
+		val := tkcmd(top, ".fsearch.eav"+string i+" get");
+		if (val == nil)
+			val = "*";
+		if (attrib != nil)
+			searchattrib = (attrib, val) :: searchattrib;
+	}
+	tmp : list of list of (string, string) = nil;
+	for (; filter != nil; filter = tl filter) {
+		l := hd filter;
+		for (tmp2 := searchattrib; tmp2 != nil; tmp2 = tl tmp2)
+			l = hd tmp2 :: l;
+		tmp = l :: tmp;
+	}
+	filter = tmp;
+	if (filter == nil)
+		filter = searchattrib :: nil;
+	return find(filter);
+}
+
+getitem(l : list of (string, ref Service), testid: string): ref Service
+{
+	for (; l != nil; l = tl l) {
+		(id, srvc) := hd l;
+		if (testid == id)
+			return srvc;
+	}
+	return nil;
+}
+
+delitem(l : list of (string, ref Service), testid: string): list of (string, ref Service)
+{
+	l2 : list of (string, ref Service) = nil;
+	for (; l != nil; l = tl l) {
+		(id, srvc) := hd l;
+		if (testid != id)
+			l2 = (id, srvc) :: l2;
+	}
+	return l2;
+}
+
+resetsearchscr(top: ref Tk->Toplevel)
+{
+	(n, nil) := sys->tokenize(tkcmd(top, "grid slaves .fsearch -column 1"), " \t\n");
+	for (i := 1; i < n - 2; i++)
+		tkcmd(top, "destroy .fsearch.ean"+string i+" .fsearch.eav"+string i);
+	s := " delete 0 end";
+	tkcmd(top, ".fsearch.ean0"+s);
+	tkcmd(top, ".fsearch.eav0"+s);
+}
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != "" && e[0] == '!')
+		sys->print("Tk error: '%s': %s\n",cmd,e);
+	return e;
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	for (j := 0; j < len a; j++)
+		tkcmd(top, a[j]);
+}
--- /dev/null
+++ b/appl/grid/mkfile
@@ -1,0 +1,56 @@
+<../../mkconfig
+
+DIRS=\
+	demo\
+	lib\
+
+TARG=\
+		blurdemo.dis\
+		cpupool.dis\
+		find.dis\
+		jpg2bit.dis\
+		query.dis\
+		readjpg.dis\
+		register.dis\
+		reglisten.dis\
+		regstyxlisten.dis\
+		remotelogon.dis\
+		usercreatesrv.dis\
+
+MODULES=\
+
+SYSMODULES= \
+	arg.m\
+	daytime.m\
+	draw.m\
+	grid/announce.m\
+	grid/browser.m\
+	grid/fbrowse.m\
+	grid/pathreader.m\
+	grid/readjpg.m\
+	grid/srvbrowse.m\
+	keyring.m\
+	newns.m\
+	readdir.m\
+	registries.m\
+	security.m\
+	sh.m\
+	string.m\
+	styx.m\
+	styxservers.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+	workdir.m\
+
+DISBIN=$ROOT/dis/grid
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
+
+$ROOT/dis/demo/readjpg.dis:	readjpg.dis
+	rm -f $target && cp readjpg.dis $target
+
+readjpg.dis:	readjpg.b $MODULE $SYS_MODULE
+	limbo $LIMBOFLAGS -c -gw readjpg.b
+
--- /dev/null
+++ b/appl/grid/query.b
@@ -1,0 +1,399 @@
+implement Query;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Rect, Image: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "readdir.m";
+	readdir: Readdir;
+include "sh.m";
+include "workdir.m";
+include "registries.m";
+	registries: Registries;
+	Service: import registries;
+include "grid/pathreader.m";
+	reader: PathReader;
+include "grid/browser.m";
+	browser: Browser;
+	Browse, File: import browser;
+include "grid/srvbrowse.m";
+	srvbrowse: Srvbrowse;
+include "grid/fbrowse.m";
+include "grid/announce.m";
+	announce: Announce;
+
+srvfilter : list of list of (string, string);
+
+Query : module {
+	init : fn (context : ref Draw->Context, nil : list of string);
+	readpath: fn (dir: File): (array of ref sys->Dir, int);
+};
+
+realinit()
+{
+	sys = load Sys Sys->PATH;
+	if (sys == nil)
+		badmod(Sys->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil)
+		badmod(Readdir->PATH);
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		badmod(Draw->PATH);
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+	tkclient->init();
+	workdir := load Workdir Workdir->PATH;
+	if (workdir == nil)
+		badmod(Workdir->PATH);
+	registries = load Registries Registries->PATH;
+	if (registries == nil)
+		badmod(Registries->PATH);
+	registries->init();
+	browser = load Browser Browser->PATH;
+	if (browser == nil)
+		badmod(Browser->PATH);
+	browser->init();
+	srvbrowse = load Srvbrowse Srvbrowse->PATH;
+	if (srvbrowse == nil)
+		badmod(Srvbrowse->PATH);
+	srvbrowse->init();
+	announce = load Announce Announce->PATH;
+	if (announce == nil)
+		badmod(Announce->PATH);
+	announce->init();
+	reader = load PathReader "$self";
+	if (reader == nil)
+		badmod("PathReader");
+}
+
+init(ctxt : ref Draw->Context, nil: list of string)
+{
+	realinit();	
+	spawn start(ctxt, 1);
+}
+
+start(ctxt: ref Draw->Context, standalone: int)
+{
+	sys->pctl(sys->FORKNS | sys->NEWPGRP, nil);
+	if (ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+
+	if (standalone)
+		sys->create("/tmp/query", sys->OREAD, sys->DMDIR | 8r777);
+	root := "/";
+	(top, titlebar) := tkclient->toplevel(ctxt,"","Query", tkclient->Appl);
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	browsechan := chan of string;
+	tk->namechan(top, browsechan, "browsechan");
+	br := Browse.new(top, "browsechan", "services/", "Services", 1, reader);
+	br.addopened(File ("services/", nil), 1);
+	srvbrowse->refreshservices(srvfilter);
+	br.refresh();
+
+	for (ik := 0; ik < len mainscreen; ik++)
+		tkcmd(top,mainscreen[ik]);
+
+	tkcmd(top, "pack .f -fill both -expand 1; pack propagate . 0");
+	released := 1;
+	title := "";
+	resize(top, 400,400);
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	tkpath: string;
+	main: for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		inp := <-browsechan =>
+			(nil, lst) := sys->tokenize(inp, " \n\t");
+			if (len lst > 1)
+				tkpath = hd tl lst;
+			selected := br.getselected(0);
+			br.defaultaction(lst, nil);
+			if (!File.eq(selected, br.getselected(0)))
+				actionbutton(top, br.selected[0].file.path, br.selected[0].tkpath);		
+			tkcmd(top, "update");
+		inp := <-butchan =>
+			# sys->print("inp: %s\n",inp);
+			(nil, lst) := sys->tokenize(inp, " \n\t");
+			if (len lst > 1)
+				tkpath = hd tl lst;
+			case hd lst {
+				"search" =>
+					if (tl lst == nil)
+						spawn srvbrowse->searchwin(ctxt, butchan, nil);
+					else {
+						if (hd tl lst == "select") {
+							file := hd tl tl lst;
+							for (tmp := tl tl tl lst; tl tmp != nil; tmp = tl tmp)
+								file += " "+hd tmp;
+							qid := hd tmp;
+							br.gotoselectfile(File (file, qid));
+							actionbutton(top, br.selected[0].file.path, br.selected[0].tkpath);		
+						}
+						else if (hd tl lst == "search") {
+							srvbrowse->refreshservices(srvfilter);
+							br.refresh();			
+						}
+					}
+				"refresh" =>
+					# ! check to see if anything is mounted first
+					srvbrowse->refreshservices(srvfilter);
+					br.refresh();
+				"mount" =>
+					file := *br.getpath(tkpath);
+					(nsrv, lsrv) := sys->tokenize(file.path, "/");
+					if (nsrv == 3)
+						spawn mountsrv(ctxt, file, getcoords(top));
+			}
+			tkcmd(top, "update");
+
+		title = <-top.ctxt.ctl or
+		title = <-top.wreq or
+		title = <-titlebar =>
+			if (title == "exit")
+				break main;
+			e := tkclient->wmctl(top, title);
+			if (e == nil && title[0] == '!')
+				(nil, lst) := sys->tokenize(title, " \t\n");
+		}
+	}
+	killg(sys->pctl(0,nil));
+}
+
+resize(top: ref Tk->Toplevel, w, h: int)
+{
+	tkcmd(top, ". configure -x 0 -width "+string min(top.screenr.dx(), w));
+	tkcmd(top, ". configure -y 0 -height "+string min(top.screenr.dy(), h));
+}
+
+min(a, b: int): int
+{
+	if (a < b)
+		return a;
+	return b;
+}
+
+nactionbuttons := 0;
+actionbutton(top: ref Tk->Toplevel, path, tkpath: string)
+{
+	for (i := 0; i < nactionbuttons; i++) {
+		tkcmd(top, "grid forget .f.ftop.baction"+string i);
+		tkcmd(top, "destroy .f.ftop.baction"+string i);
+	}
+	if (path == nil) {
+		nactionbuttons = 0;
+		return;
+	}
+	(n, nil) := sys->tokenize(path, "/");
+	buttons : list of (string, string) = nil;
+	if (n == 3)
+		buttons = ("Mount", "mount "+tkpath) :: buttons;
+
+	nactionbuttons = len buttons;
+	for (i = 0; i < nactionbuttons; i++) {
+		name := ".f.ftop.baction"+string i+" ";
+		(text,cmd) := hd buttons;
+		tkcmd(top, "button "+name+"-text {"+text+"} "+
+				"-font /fonts/charon/bold.normal.font "+
+				"-command {send butchan "+cmd+"}");
+		tkcmd(top, "grid "+name+" -row 0 -column "+string (4+i));
+		buttons = tl buttons;
+	}
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "kill");
+}
+
+killg(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(fd, "killgrp");
+}
+
+mainscreen := array[] of {
+	"frame .f",
+	"frame .f.ftop",
+	"variable opt command",
+	"button .f.ftop.br -text {Refresh} -command {send butchan refresh} -font /fonts/charon/bold.normal.font",
+	"button .f.ftop.bs -text {Search} -command {send butchan search} -font /fonts/charon/bold.normal.font",
+  	"grid .f.ftop.br .f.ftop.bs -row 0",
+	"grid columnconfigure .f.ftop 3 -minsize 30",
+	"label .f.l -text { } -height 1 -bg red",
+	"grid .f.l -row 1 -column 0 -sticky ew",
+	"grid .f.ftop -row 0 -column 0 -pady 2 -sticky w",
+	"grid .fbrowse -in .f -row 2 -column 0 -sticky nsew",
+	
+	"grid columnconfigure .f 0 -weight 1",
+	"grid rowconfigure .f 2 -weight 1",
+
+	"bind .Wm_t <Button-1> +{focus .Wm_t}",
+	"bind .Wm_t.title <Button-1> +{focus .Wm_t}",
+	"focus .Wm_t",
+};
+
+readpath(dir: File): (array of ref sys->Dir, int)
+{
+	return srvbrowse->servicepath2Dir(dir.path, int dir.qid);
+}
+
+badmod(path: string)
+{
+	sys->print("Query: failed to load %s: %r\n",path);
+	exit;
+}
+
+mountscr := array[] of {
+	"frame .f -borderwidth 2 -relief raised",
+	"text .f.t -width 200 -height 60 -borderwidth 1 -bg white -font /fonts/charon/plain.normal.font",
+	"button .f.b -text {Cancel} -command {send butchan cancel} -width 70 -font /fonts/charon/plain.normal.font",
+	"grid .f.t -row 0 -column 0 -padx 10 -pady 10",
+	"grid .f.b -row 1 -column 0 -sticky n",
+	"grid rowconfigure .f 1 -minsize 30",
+};
+
+mountsrv(ctxt: ref Draw->Context, srvfile: File, coords: draw->Rect)
+{
+	(top, nil) := tkclient->toplevel(ctxt, "", nil, tkclient->Plain);
+	ctlchan := chan of string;
+	butchan := chan of string;
+	tk->namechan(top, butchan, "butchan");
+	tkcmds(top, mountscr);
+	tkcmd(top, ". configure "+getcentre(top, coords)+"; pack .f; update");
+	spawn mountit(ctxt, srvfile, ctlchan);
+	pid := int <-ctlchan;
+	tkclient->onscreen(top, "exact");
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	for (;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		e := <- ctlchan =>
+			if (e[0] == '!') {
+				tkcmd(top, ".f.t insert end {"+e[1:]+"}");
+				tkcmd(top, ".f.b configure -text {close}; update");
+				pid = -1;
+			}
+			else if (e == "ok")
+				return;
+			else
+				tkcmd(top, ".f.t insert end {"+e+"}; update");
+		<- butchan =>
+			if (pid != -1)
+				kill(pid);
+			return;
+		}
+	}
+}
+
+mountit(ctxt: ref Draw->Context, srvfile: File, ctlchan: chan of string)
+{
+	ctlchan <-= string sys->pctl(0,nil);
+
+	n := 0;
+	(nil, lst) := sys->tokenize(srvfile.path, "/");
+	stype := hd tl lst;
+	name := hd tl tl lst;
+	addr := "";
+	ctlchan <-= "Connecting...\n";
+	lsrv := srvbrowse->servicepath2Service(srvfile.path, srvfile.qid);
+	if (len lsrv < 1) {
+		ctlchan <-= "!could not find service";
+		return;
+	}
+	srvc := hd lsrv;
+
+	ctlchan <-= "Mounting...\n";
+	
+	id := 0;
+	dir : string;
+	for (;;) {
+		dir = "/tmp/query/"+string id;
+		(n2, nil) := sys->stat(dir);
+		if (n2 == -1) {
+			fdtmp := sys->create(dir, sys->OREAD, sys->DMDIR | 8r777);
+			if (fdtmp != nil)
+				break;
+		}
+		else {
+			(dirs2, nil) := readdir->init(dir, readdir->NAME | readdir->COMPACT);
+			if (len dirs2 == 0)
+				break;
+		}
+		id++;
+	}
+	attached := srvc.attach(nil, nil);
+	if (attached == nil) {
+		ctlchan <-= sys->sprint("!could not connect: %r");
+		return;
+	}
+	if (sys->mount(attached.fd, nil, dir, sys->MREPL, nil) != -1) {
+		ctlchan <-= "ok";
+		fbrowse := load FBrowse FBrowse->PATH;
+		if (fbrowse == nil)
+			badmod(FBrowse->PATH);
+		fbrowse->init(ctxt, srvfile.path, dir, dir);
+		sys->unmount(nil, dir);
+		attached = nil;
+	}
+	else
+		ctlchan <-= sys->sprint("!mount failed: %r");
+}
+
+getcoords(top: ref Tk->Toplevel): draw->Rect
+{
+	h := int tkcmd(top, ". cget -height");
+	w := int tkcmd(top, ". cget -width");
+	x := int tkcmd(top, ". cget -actx");
+	y := int tkcmd(top, ". cget -acty");
+	r := draw->Rect((x,y),(x+w,y+h));
+	return r;
+}
+
+getcentre(top: ref Tk->Toplevel, winr: draw->Rect): string
+{
+	h := int tkcmd(top, ".f cget -height");
+	w := int tkcmd(top, ".f cget -width");
+	midx := winr.min.x + (winr.dx() / 2);
+	midy := winr.min.y + (winr.dy() / 2);
+	newx := midx - (w/2);
+	newy := midy - (h/2);
+	return "-x "+string newx+" -y "+string newy;
+}
+
+tkcmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != "" && e[0] == '!')
+		sys->print("Tk error: '%s': %s\n",cmd,e);
+	return e;
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	for (j := 0; j < len a; j++)
+		tkcmd(top, a[j]);
+}
--- /dev/null
+++ b/appl/grid/readjpg.b
@@ -1,0 +1,1146 @@
+implement Readjpg;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Image: import draw;
+include "grid/readjpg.m";
+	
+display: ref Display;
+slowread: int;
+zeroints := array[64] of { * => 0 };
+
+init(disp: ref Draw->Display)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	display = disp;
+	init_tabs();
+}
+
+fjpg2img(fd: ref sys->FD, cachepath: string, chanin, chanout: chan of string): ref Image
+{
+	if (fd == nil) return nil;
+	sync := chan of int;
+	imgchan := chan of ref Image;
+	is := newImageSource(0,0);
+	spawn slowreads(is,fd,cachepath, sync, chanout);
+	srpid := <- sync;
+	if (srpid == -1) return nil;
+	spawn getjpegimg(is, chanout, imgchan, sync);
+	gjipid := <- sync;
+
+	for (;;) alt {
+		ctl := <- chanin =>
+			if (ctl == "kill") {
+				if (srpid != -1) kill(srpid);
+				kill(gjipid);
+				return nil;
+			}
+		img := <- imgchan =>
+			if (srpid != -1) kill(srpid);
+			return img;
+		err := <- sync =>
+			if (err == 0) srpid = -1;
+			else {
+				kill(gjipid);
+				return nil;
+			}
+	}
+}
+
+jpg2img(filename, cachepath: string, chanin, chanout: chan of string): ref Image
+{
+	fd := sys->open(filename, sys->OREAD);
+	return fjpg2img(fd, cachepath, chanin, chanout);
+}
+
+kill(pid: int)
+{	
+	pctl := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (pctl != nil)
+		sys->write(pctl, array of byte "kill", len "kill");
+}
+
+filelength(fd : ref sys->FD): int
+{
+	(n, dir) := sys->fstat(fd);
+	if (n == -1) return -1;
+	filelen := int dir.length;
+	return filelen;
+}
+
+slowreads(is: ref ImageSource, fd : ref sys->FD, cachepath: string, sync: chan of int, chanout: chan of string)
+{
+	filelen := filelength(fd);
+	if (filelen < 1) {
+		sync <-= -1;
+		return;
+	}
+	is.data = array[filelen] of byte;
+	slowread = 0;
+
+	sync <-= sys->pctl(0, nil);
+
+	cachefd : ref sys->FD = nil;
+	if (cachepath != "") cachefd = sys->create(cachepath, sys->OWRITE, 8r666);
+	if (chanout != nil) {
+		chanout <-= "l2 Loading...";
+		chanout <-= "pc 0";
+	}
+	i : int;
+	for (;;) {
+		i = sys->read(fd,is.data[slowread:], 8192);
+		if (i < 1) break;
+		if (cachefd != nil)
+			sys->write(cachefd, is.data[slowread:],i);
+		slowread += i;
+		if (chanout != nil)
+			chanout <-= "pc "+string ((slowread*100)/filelen);
+		sys->sleep(0);
+	}
+	if (i == -1 || slowread == 0) {
+		sync <-= -1;
+		return;
+	}
+	newdata := array[slowread] of byte;
+	newdata = is.data[:slowread];
+	is.data = newdata;
+	if (cachepath != "" && slowread < filelen)
+		sys->remove(cachepath);
+	sync <-= 0;
+}
+
+wait4data(n: int)
+{
+	for(;;) {
+		if (slowread > n) break;
+		sys->sleep(100);
+	}
+}
+
+newImageSource(w, h: int) : ref ImageSource
+{
+	is := ref ImageSource(
+		w,h,		# width, height
+		0,0,		# origw, origh
+		0,		# i
+		nil,		# jhdr
+		nil		# data
+		);
+	return is;
+}
+
+getjpeghdr(is: ref ImageSource)
+{
+	h := ref Jpegstate(
+		0, 0,		# sr, cnt
+		0,		# Nf
+		nil,		# comp
+		byte 0,	# mode,
+		0, 0,		# X, Y
+		nil,		# qt
+		nil, nil,	# dcht, acht
+		0,		# Ns
+		nil,		# scomp
+		0, 0,		# Ss, Se
+		0, 0,		# Ah, Al
+		0, 0,		# ri, nseg
+		nil,		# nblock
+		nil, nil,	# dccoeff, accoeff
+		0, 0, 0, 0	# nacross, ndown, Hmax, Vmax
+		);
+	is.jstate = h;
+	if(jpegmarker(is) != SOI)
+		sys->print("Error: Jpeg expected SOI marker\n");
+	(m, n) := jpegtabmisc(is);
+	if(!(m == SOF || m == SOF2))
+		sys->print("Error: Jpeg expected Frame marker");
+	nil = getc(is);		# sample precision
+	h.Y = getbew(is);
+	h.X = getbew(is);
+	h.Nf = getc(is);
+	h.comp = array[h.Nf] of Framecomp;
+	h.nblock = array[h.Nf] of int;
+	for(i:=0; i<h.Nf; i++) {
+		h.comp[i].C = getc(is);
+		(H, V) := nibbles(getc(is));
+		h.comp[i].H = H;
+		h.comp[i].V = V;
+		h.comp[i].Tq = getc(is);
+		h.nblock[i] =H*V;
+	}
+	h.mode = byte m;
+	is.origw = h.X;
+	is.origh = h.Y;
+	setdims(is);
+	if(n != 6+3*h.Nf)
+		sys->print("Error: Jpeg bad SOF length");
+}
+
+setdims(is: ref ImageSource)
+{
+	sw := is.origw;
+	sh := is.origh;
+	dw := is.width;
+	dh := is.height;
+	if(dw == 0 && dh == 0) {
+		dw = sw;
+		dh = sh;
+	}
+	else if(dw == 0 || dh == 0) {
+		if(dw == 0) {
+			dw = int ((real sw) * (real dh/real sh));
+			if(dw == 0)
+				dw = 1;
+		}
+		else {
+			dh = int ((real sh) * (real dw/real sw));
+			if(dh == 0)
+				dh = 1;
+		}
+	}
+	is.width = dw;
+	is.height = dh;
+}
+
+jpegmarker(is: ref ImageSource) : int
+{
+	if(getc(is) != 16rFF)
+		sys->print("Error: Jpeg expected marker");
+	return getc(is);
+}
+
+getbew(is: ref ImageSource) : int
+{
+	c0 := getc(is);
+	c1 := getc(is);
+	return (c0<<8) + c1;
+}
+
+getn(is: ref ImageSource, n: int) : (array of byte, int)
+{
+	if (is.i + n > slowread - 1) wait4data(is.i + n);
+	a := is.data;
+	i := is.i;
+	if(i + n <= len a)
+		is.i += n;
+#	else
+#		sys->print("Error: premature eof");
+	return (a, i);
+}
+
+# Consume tables and miscellaneous marker segments,
+# returning the marker id and length of the first non-such-segment
+# (after having consumed the marker).
+# May raise "premature eof" or other exception.
+jpegtabmisc(is: ref ImageSource) : (int, int)
+{
+	h := is.jstate;
+	m, n : int;
+Loop:
+	for(;;) {
+		h.nseg++;
+		m = jpegmarker(is);
+		n = 0;
+		if(m != EOI)
+			n = getbew(is) - 2;
+		case m {
+		SOF or SOF2 or SOS or EOI =>
+			break Loop;
+
+		APPn+0 =>
+			if(h.nseg==1 && n >= 6) {
+				(buf, i) := getn(is, 6);
+				n -= 6;
+				if(string buf[i:i+4]=="JFIF") {
+					vers0 := int buf[i+5];
+					vers1 := int buf[i+6];
+					if(vers0>1 || vers1>2)
+						sys->print("Error: Jpeg unimplemented version");
+				}
+			}
+
+		APPn+1 to APPn+15 =>
+			;
+
+		DQT =>
+			jpegquanttables(is, n);
+			n = 0;
+
+		DHT =>
+			jpeghuffmantables(is, n);
+			n = 0;
+
+		DRI =>
+			h.ri =getbew(is);
+			n -= 2;
+
+		COM =>
+			;
+
+		* =>
+			sys->print("Error: Jpeg unexpected marker");
+		}
+		if(n > 0)
+			getn(is, n);
+	}
+	return (m, n);
+}
+
+# Consume huffman tables, raising exception on error.
+jpeghuffmantables(is: ref ImageSource, n: int)
+{
+	h := is.jstate;
+	if(h.dcht == nil) {
+		h.dcht = array[4] of ref Huffman;
+		h.acht = array[4] of ref Huffman;
+	}
+	for(l:= 0; l < n; )
+		l += jpeghuffmantable(is);
+	if(l != n)
+		sys->print("Error: Jpeg huffman table bad length");
+}
+
+jpeghuffmantable(is: ref ImageSource) : int
+{
+	t := ref Huffman;
+	h := is.jstate;
+	(Tc, th) := nibbles(getc(is));
+	if(Tc > 1)
+		sys->print("Error: Jpeg unknown Huffman table class");
+	if(th>3 || (h.mode==byte SOF && th>1))
+		sys->print("Error: Jpeg unknown Huffman table index");
+	if(Tc == 0)
+		h.dcht[th] = t;
+	else
+		h.acht[th] = t;
+
+	# flow chart C-2
+	(b, bi) := getn(is, 16);
+	numcodes := array[16] of int;
+	nsize := 0;
+	for(i:=0; i<16; i++)
+		nsize += (numcodes[i] = int b[bi+i]);
+	t.size = array[nsize+1] of int;
+	k := 0;
+	for(i=1; i<=16; i++) {
+		n :=numcodes[i-1];
+		for(j:=0; j<n; j++)
+			t.size[k++] = i;
+	}
+	t.size[k] = 0;
+
+	# initialize HUFFVAL
+	t.val = array[nsize] of int;
+	(b, bi) = getn(is, nsize);
+	for(i=0; i<nsize; i++)
+		t.val[i] = int b[bi++];
+
+	# flow chart C-3
+	t.code = array[nsize+1] of int;
+	k = 0;
+	code := 0;
+	si := t.size[0];
+	for(;;) {
+		do
+			t.code[k++] = code++;
+		while(t.size[k] == si);
+		if(t.size[k] == 0)
+			break;
+		do {
+			code <<= 1;
+			si++;
+		} while(t.size[k] != si);
+	}
+
+	# flow chart F-25
+	t.mincode = array[17] of int;
+	t.maxcode = array[17] of int;
+	t.valptr = array[17] of int;
+	i = 0;
+	j := 0;
+    F25:
+	for(;;) {
+		for(;;) {
+			i++;
+			if(i > 16)
+				break F25;
+			if(numcodes[i-1] != 0)
+				break;
+			t.maxcode[i] = -1;
+		}
+		t.valptr[i] = j;
+		t.mincode[i] = t.code[j];
+		j += int numcodes[i-1]-1;
+		t.maxcode[i] = t.code[j];
+		j++;
+	}
+
+	# create byte-indexed fast path tables
+	t.value = array[256] of int;
+	t.shift = array[256] of int;
+	maxcode := t.maxcode;
+	# stupid startup algorithm: just run machine for each byte value
+  Bytes:
+	for(v:=0; v<256; v++){
+		cnt := 7;
+		m := 1<<7;
+		code = 0;
+		sr := v;
+		i = 1;
+		for(;;i++){
+			if(sr & m)
+				code |= 1;
+			if(code <= maxcode[i])
+				break;
+			code <<= 1;
+			m >>= 1;
+			if(m == 0){
+				t.shift[v] = 0;
+				t.value[v] = -1;
+				continue Bytes;
+			}
+			cnt--;
+		}
+		t.shift[v] = 8-cnt;
+		t.value[v] = t.val[t.valptr[i]+(code-t.mincode[i])];
+	}
+
+	return nsize+17;
+}
+
+jpegquanttables(is: ref ImageSource, n: int)
+{
+	h := is.jstate;
+	if(h.qt == nil)
+		h.qt = array[4] of array of int;
+	for(l:=0; l<n; )
+		l += jpegquanttable(is);
+	if(l != n)
+		sys->print("Error: Jpeg quant table bad length");
+}
+
+jpegquanttable(is: ref ImageSource): int
+{
+	(pq, tq) := nibbles(getc(is));
+	if(pq > 1)
+		sys->print("Error: Jpeg unknown quantization table class");
+	if(tq > 3)
+		sys->print("Error: Jpeg bad quantization table index");
+	q := array[64] of int;
+	is.jstate.qt[tq] = q;
+	for(i:=0; i<64; i++) {
+		if(pq == 0)
+			q[i] =getc(is);
+		else
+			q[i] = getbew(is);
+	}
+	return 1+(64*(1+pq));;
+}
+
+# Have just read Frame header.
+# Now expect:
+#	((tabl/misc segment(s))* (scan header) (entropy coded segment)+)+ EOI
+getjpegimg(is:ref ImageSource,chanout:chan of string,imgchan: chan of ref Image,sync: chan of int)
+{
+	sync <-= sys->pctl(0, nil);
+	getjpeghdr(is);
+	h := is.jstate;
+	chans: array of array of byte = nil;
+	for(;;) {
+		(m, n) := jpegtabmisc(is);
+		if(m == EOI)
+			break;
+		if(m != SOS)
+			sys->print("Error: Jpeg expected start of scan");
+
+		h.Ns = getc(is);
+		scomp := array[h.Ns] of Scancomp;
+		for(i := 0; i < h.Ns; i++) {
+			scomp[i].C = getc(is);
+			(scomp[i].tdc, scomp[i].tac) = nibbles(getc(is));
+		}
+		h.scomp = scomp;
+		h.Ss = getc(is);
+		h.Se = getc(is);
+		(h.Ah, h.Al) = nibbles(getc(is));
+		if(n != 4+h.Ns*2)
+			sys->print("Error: Jpeg SOS header wrong length");
+
+		if(h.mode == byte SOF) {
+			if(chans != nil)
+				sys->print("Error: Jpeg baseline has > 1 scan");
+			chans = jpegbaselinescan(is, chanout);
+		}
+	}
+	if(chans == nil)
+		sys->print("Error: jpeg has no image");
+	width := is.width;
+	height := is.height;
+	if(width != h.X || height != h.Y) {
+		for(k := 0; k < len chans; k++)
+			chans[k] = resample(chans[k], h.X, h.Y, width, height);
+	}
+
+	r := remapYCbCr(chans, chanout);
+	im := newimage24(width, height);
+	im.writepixels(im.r, r);
+	imgchan <-= im;
+}
+
+newimage24(w, h: int) : ref Image
+{
+	im := display.newimage(((0,0),(w,h)), Draw->RGB24, 0, Draw->White);
+	if(im == nil)
+		sys->print("Error: out of memory");
+	return im;
+}
+
+remapYCbCr(chans: array of array of byte, chanout: chan of string): array of byte
+{
+	Y := chans[0];
+	Cb := chans[1];
+	Cr := chans[2];
+
+	rgb := array [3*len Y] of byte;
+	bix := 0;
+	lY := len Y;
+	n := lY / 20;
+	count := 0;
+	for (i := 0; i < lY; i++) {
+		if ((count == 0 || count >= n ) && chanout != nil) {
+			chanout <-= "l2 Processing...";
+			chanout <-= "pc "+string ((100*i)/ lY);
+			count = 0;
+		}
+		count++;
+		y := int Y[i];
+		cb := int Cb[i];
+		cr := int Cr[i];
+		r := y + Cr2r[cr];
+		g := y - Cr2g[cr] - Cb2g[cb];
+		b := y + Cb2b[cb];
+
+		rgb[bix++] = clampb[b+CLAMPBOFF];
+		rgb[bix++] = clampb[g+CLAMPBOFF];
+		rgb[bix++] = clampb[r+CLAMPBOFF];
+	}
+	if (chanout != nil) chanout <-= "pc 100";
+	return rgb;
+}
+
+zig := array[64] of {
+	0, 1, 8, 16, 9, 2, 3, 10, 17, # 0-7
+	24, 32, 25, 18, 11, 4, 5, # 8-15
+	12, 19, 26, 33, 40, 48, 41, 34, # 16-23
+	27, 20, 13, 6, 7, 14, 21, 28, # 24-31
+	35, 42, 49, 56, 57, 50, 43, 36, # 32-39
+	29, 22, 15, 23, 30, 37, 44, 51, # 40-47
+	58, 59, 52, 45, 38, 31, 39, 46, # 48-55
+	53, 60, 61, 54, 47, 55, 62, 63 # 56-63
+};
+
+jpegbaselinescan(is: ref ImageSource,chanout: chan of string) : array of array of byte
+{
+	h := is.jstate;
+	Ns := h.Ns;
+	if(Ns != h.Nf)
+		sys->print("Error: Jpeg baseline needs Ns==Nf");
+	if(!(Ns==3 || Ns==1))
+		sys->print("Error: Jpeg baseline needs Ns==1 or 3");
+
+	
+	chans := array[h.Nf] of array of byte;
+	for(k:=0; k<h.Nf; k++)
+		chans[k] = array[h.X*h.Y] of byte;
+
+	# build per-component arrays
+	Td := array[Ns] of int;
+	Ta := array[Ns] of int;
+	data := array[Ns] of array of array of int;
+	H := array[Ns] of int;
+	V := array[Ns] of int;
+	DC := array[Ns] of int;
+
+	# compute maximum H and V
+	Hmax := 0;
+	Vmax := 0;
+	for(comp:=0; comp<Ns; comp++) {
+		if(h.comp[comp].H > Hmax)
+			Hmax = h.comp[comp].H;
+		if(h.comp[comp].V > Vmax)
+			Vmax = h.comp[comp].V;
+	}
+	# initialize data structures
+	allHV1 := 1;
+	for(comp=0; comp<Ns; comp++) {
+		# JPEG requires scan components to be in same order as in frame,
+		# so if both have 3 we know scan is Y Cb Cr and there's no need to
+		# reorder
+		Td[comp] = h.scomp[comp].tdc;
+		Ta[comp] = h.scomp[comp].tac;
+		H[comp] = h.comp[comp].H;
+		V[comp] = h.comp[comp].V;
+		nblock := H[comp]*V[comp];
+		if(nblock != 1)
+			allHV1 = 0;
+
+		# data[comp]: needs (3+nblock)*4 + nblock*(3+8*8)*4 bytes
+
+		data[comp] = array[nblock] of array of int;
+		DC[comp] = 0;
+		for(m:=0; m<nblock; m++)
+			data[comp][m] = array[8*8] of int;
+	}
+
+	ri := h.ri;
+
+	h.cnt = 0;
+	h.sr = 0;
+	nacross := ((h.X+(8*Hmax-1))/(8*Hmax));
+	nmcu := ((h.Y+(8*Vmax-1))/(8*Vmax))*nacross;
+	n1 := 0;
+	n2 := nmcu / 20;
+	for(mcu:=0; mcu<nmcu; ) {
+		if ((n1 == 0 || n1 >= n2) && chanout != nil && slowread == len is.data) {
+			chanout <-= "l2 Scanning... ";
+			chanout <-= "pc "+string ((100*mcu)/nmcu);
+			n1 = 0;
+		}
+		n1 ++;
+		for(comp=0; comp<Ns; comp++) {
+			dcht := h.dcht[Td[comp]];
+			acht := h.acht[Ta[comp]];
+			qt := h.qt[h.comp[comp].Tq];
+
+			for(block:=0; block<H[comp]*V[comp]; block++) {
+				# F-22
+				t := jdecode(is, dcht);
+				diff := jreceive(is, t);
+				DC[comp] += diff;
+
+				# F-23
+				zz := data[comp][block];
+				zz[0:] = zeroints;
+				zz[0] = qt[0]*DC[comp];
+				k = 1;
+
+				for(;;) {
+					rs := jdecode(is, acht);
+					(rrrr, ssss) := nibbles(rs);
+					if(ssss == 0){
+						if(rrrr != 15)
+							break;
+						k += 16;
+					}else{
+						k += rrrr;
+						z := jreceive(is, ssss);
+						zz[zig[k]] = z*qt[k];
+						if(k == 63)
+							break;
+						k++;
+					}
+				}
+
+				idct(zz);
+			}
+		}
+
+		# rotate colors to RGB and assign to bytes
+		colormap(h, chans, data[0], data[1], data[2], mcu, nacross, Hmax, Vmax, H, V);
+
+		# process restart marker, if present
+		mcu++;
+		if(ri>0 && mcu<nmcu && mcu%ri==0){
+			jrestart(is, mcu);
+			for(comp=0; comp<Ns; comp++)
+				DC[comp] = 0;
+		}
+	}
+	if (chanout != nil) chanout <-= "pc 100";
+	return chans;
+}
+
+jrestart(is: ref ImageSource, mcu: int)
+{
+	h := is.jstate;
+	ri := h.ri;
+	restart := mcu/ri-1;
+	rst, nskip: int;
+	nskip = 0;
+	do {
+		do{
+			rst = jnextborm(is);
+			nskip++;
+		}while(rst>=0 && rst!=16rFF);
+		if(rst == 16rFF){
+			rst = jnextborm(is);
+			nskip++;
+		}
+	} while(rst>=0 && (rst&~7)!= RST);
+	if(nskip != 2 || rst < 0 || ((rst&7) != (restart&7)))
+		sys->print("Error: Jpeg restart problem");
+	h.cnt = 0;
+	h.sr = 0;
+}
+
+jc1: con 2871;		# 1.402 * 2048
+jc2: con 705;		# 0.34414 * 2048
+jc3: con 1463;		# 0.71414 * 2048
+jc4: con 3629;		# 1.772 * 2048
+
+CLAMPBOFF: con 300;
+NCLAMPB: con CLAMPBOFF+256+CLAMPBOFF;
+CLAMPNOFF: con 64;
+NCLAMPN: con CLAMPNOFF+256+CLAMPNOFF;
+
+clampb: array of byte;		# clamps byte values
+
+init_tabs()
+{
+	j: int;
+	clampb = array[NCLAMPB] of byte;
+	for(j=0; j<CLAMPBOFF; j++)
+		clampb[j] = byte 0;
+	for(j=0; j<256; j++)
+		clampb[CLAMPBOFF+j] = byte j;
+	for(j=0; j<CLAMPBOFF; j++)
+		clampb[CLAMPBOFF+256+j] = byte 16rFF;
+}
+
+
+# Fills in pixels (x,y) for x = minx=8*Hmax*(mcu%nacross), minx+1, ..., minx+8*Hmax-1 (or h.X-1, if less)
+# and for y = miny=8*Vmax*(mcu/nacross), miny+1, ..., miny+8*Vmax-1 (or h.Y-1, if less)
+colormap(h: ref Jpegstate, chans: array of array of byte, data0, data1, data2: array of array of int, mcu, nacross, Hmax, Vmax: int,  H, V: array of int)
+{
+	rpic := chans[0];
+	gpic := chans[1];
+	bpic := chans[2];
+	minx := 8*Hmax*(mcu%nacross);
+	dx := 8*Hmax;
+	if(minx+dx > h.X)
+		dx = h.X-minx;
+	miny := 8*Vmax*(mcu/nacross);
+	dy := 8*Vmax;
+	if(miny+dy > h.Y)
+		dy = h.Y-miny;
+	pici := miny*h.X+minx;
+	H0 := H[0];
+	H1 := H[1];
+	H2 := H[2];
+	for(y:=0; y<dy; y++) {
+		t := y*V[0];
+		b0 := H0*(t/(8*Vmax));
+		y0 := 8*((t/Vmax)&7);
+		t = y*V[1];
+		b1 := H1*(t/(8*Vmax));
+		y1 := 8*((t/Vmax)&7);
+		t = y*V[2];
+		b2 := H2*(t/(8*Vmax));
+		y2 := 8*((t/Vmax)&7);
+		x0 := 0;
+		x1 := 0;
+		x2 := 0;
+		for(x:=0; x<dx; x++) {
+			rpic[pici+x] = clampb[data0[b0][y0+x0++*H0/Hmax] + 128 + CLAMPBOFF];
+			gpic[pici+x] = clampb[data1[b1][y1+x1++*H1/Hmax] + 128 + CLAMPBOFF];
+			bpic[pici+x] = clampb[data2[b2][y2+x2++*H2/Hmax] + 128 + CLAMPBOFF];
+			if(x0*H0/Hmax >= 8){
+				x0 = 0;
+				b0++;
+			}
+			if(x1*H1/Hmax >= 8){
+				x1 = 0;
+				b1++;
+			}
+			if(x2*H2/Hmax >= 8){
+				x2 = 0;
+				b2++;
+			}
+		}
+		pici += h.X;
+	}
+}
+
+# decode next 8-bit value from entropy-coded input.  chart F-26
+jdecode(is: ref ImageSource, t: ref Huffman): int
+{
+	h := is.jstate;
+	maxcode := t.maxcode;
+	if(h.cnt < 8)
+		jnextbyte(is);
+	# fast lookup
+	code := (h.sr>>(h.cnt-8))&16rFF;
+	v := t.value[code];
+	if(v >= 0){
+		h.cnt -= t.shift[code];
+		return v;
+	}
+
+	h.cnt -= 8;
+	if(h.cnt == 0)
+		jnextbyte(is);
+	h.cnt--;
+	cnt := h.cnt;
+	m := 1<<cnt;
+	sr := h.sr;
+	code <<= 1;
+	i := 9;
+	for(;;i++){
+		if(sr & m)
+			code |= 1;
+		if(code <= maxcode[i])
+			break;
+		code <<= 1;
+		m >>= 1;
+		if(m == 0){
+			sr = jnextbyte(is);
+			m = 16r80;
+			cnt = 8;
+		}
+		cnt--;
+	}
+	h.cnt = cnt;
+	return t.val[t.valptr[i]+(code-t.mincode[i])];
+}
+
+# load next byte of input
+jnextbyte(is: ref ImageSource): int
+{
+	b :=getc(is);
+
+	if(b == 16rFF) {
+		b2 :=getc(is);
+		if(b2 != 0) {
+			if(b2 == int DNL)
+				sys->print("Error: Jpeg  DNL marker unimplemented");
+			# decoder is reading into marker; satisfy it and restore state
+			ungetc2(is, byte b);
+		}
+	}
+	h := is.jstate;
+	h.cnt += 8;
+	h.sr = (h.sr<<8)| b;
+	return b;
+}
+
+ungetc2(is: ref ImageSource, nil: byte)
+{
+	if(is.i < 2) {
+		if(is.i != 1)
+			sys->print("Error: EXInternal: ungetc2 past beginning of buffer");
+		is.i = 0;
+	}
+	else
+		is.i -= 2;
+}
+
+
+getc(is: ref ImageSource) : int
+{
+	if(is.i >= len is.data) {
+		sys->print("Error: premature eof");
+	}
+	if (is.i >= slowread)
+		wait4data(is.i);
+	return int is.data[is.i++];
+}
+
+# like jnextbyte, but look for marker too
+jnextborm(is: ref ImageSource): int
+{
+	b :=getc(is);
+
+	if(b == 16rFF)
+		return b;
+	h := is.jstate;
+	h.cnt += 8;
+	h.sr = (h.sr<<8)| b;
+	return b;
+}
+
+# return next s bits of input, MSB first, and level shift it
+jreceive(is: ref ImageSource, s: int): int
+{
+	h := is.jstate;
+	while(h.cnt < s)
+		jnextbyte(is);
+	h.cnt -= s;
+	v := h.sr >> h.cnt;
+	m := (1<<s);
+	v &= m-1;
+	# level shift
+	if(v < (m>>1))
+		v += ~(m-1)+1;
+	return v;
+}
+
+nibbles(c: int) : (int, int)
+{
+	return (c>>4, c&15);
+
+}
+
+# Scaled integer implementation.
+# inverse two dimensional DCT, Chen-Wang algorithm
+# (IEEE ASSP-32, pp. 803-816, Aug. 1984)
+# 32-bit integer arithmetic (8 bit coefficients)
+# 11 mults, 29 adds per DCT
+#
+# coefficients extended to 12 bit for IEEE1180-1990
+# compliance
+
+W1:	con 2841;	# 2048*sqrt(2)*cos(1*pi/16)
+W2:	con 2676;	# 2048*sqrt(2)*cos(2*pi/16)
+W3:	con 2408;	# 2048*sqrt(2)*cos(3*pi/16)
+W5:	con 1609;	# 2048*sqrt(2)*cos(5*pi/16)
+W6:	con 1108;	# 2048*sqrt(2)*cos(6*pi/16)
+W7:	con 565;	# 2048*sqrt(2)*cos(7*pi/16)
+
+W1pW7:	con 3406;	# W1+W7
+W1mW7:	con 2276;	# W1-W7
+W3pW5:	con 4017;	# W3+W5
+W3mW5:	con 799;	# W3-W5
+W2pW6:	con 3784;	# W2+W6
+W2mW6:	con 1567;	# W2-W6
+
+R2:	con 181;	# 256/sqrt(2)
+
+idct(b: array of int)
+{
+	# transform horizontally
+	for(y:=0; y<8; y++){
+		eighty := y<<3;
+		# if all non-DC components are zero, just propagate the DC term
+		if(b[eighty+1]==0)
+		if(b[eighty+2]==0 && b[eighty+3]==0)
+		if(b[eighty+4]==0 && b[eighty+5]==0)
+		if(b[eighty+6]==0 && b[eighty+7]==0){
+			v := b[eighty]<<3;
+			b[eighty+0] = v;
+			b[eighty+1] = v;
+			b[eighty+2] = v;
+			b[eighty+3] = v;
+			b[eighty+4] = v;
+			b[eighty+5] = v;
+			b[eighty+6] = v;
+			b[eighty+7] = v;
+			continue;
+		}
+		# prescale
+		x0 := (b[eighty+0]<<11)+128;
+		x1 := b[eighty+4]<<11;
+		x2 := b[eighty+6];
+		x3 := b[eighty+2];
+		x4 := b[eighty+1];
+		x5 := b[eighty+7];
+		x6 := b[eighty+5];
+		x7 := b[eighty+3];
+		# first stage
+		x8 := W7*(x4+x5);
+		x4 = x8 + W1mW7*x4;
+		x5 = x8 - W1pW7*x5;
+		x8 = W3*(x6+x7);
+		x6 = x8 - W3mW5*x6;
+		x7 = x8 - W3pW5*x7;
+		# second stage
+		x8 = x0 + x1;
+		x0 -= x1;
+		x1 = W6*(x3+x2);
+		x2 = x1 - W2pW6*x2;
+		x3 = x1 + W2mW6*x3;
+		x1 = x4 + x6;
+		x4 -= x6;
+		x6 = x5 + x7;
+		x5 -= x7;
+		# third stage
+		x7 = x8 + x3;
+		x8 -= x3;
+		x3 = x0 + x2;
+		x0 -= x2;
+		x2 = (R2*(x4+x5)+128)>>8;
+		x4 = (R2*(x4-x5)+128)>>8;
+		# fourth stage
+		b[eighty+0] = (x7+x1)>>8;
+		b[eighty+1] = (x3+x2)>>8;
+		b[eighty+2] = (x0+x4)>>8;
+		b[eighty+3] = (x8+x6)>>8;
+		b[eighty+4] = (x8-x6)>>8;
+		b[eighty+5] = (x0-x4)>>8;
+		b[eighty+6] = (x3-x2)>>8;
+		b[eighty+7] = (x7-x1)>>8;
+	}
+	# transform vertically
+	for(x:=0; x<8; x++){
+		# if all non-DC components are zero, just propagate the DC term
+		if(b[x+8*1]==0)
+		if(b[x+8*2]==0 && b[x+8*3]==0)
+		if(b[x+8*4]==0 && b[x+8*5]==0)
+		if(b[x+8*6]==0 && b[x+8*7]==0){
+			v := (b[x+8*0]+32)>>6;
+			b[x+8*0] = v;
+			b[x+8*1] = v;
+			b[x+8*2] = v;
+			b[x+8*3] = v;
+			b[x+8*4] = v;
+			b[x+8*5] = v;
+			b[x+8*6] = v;
+			b[x+8*7] = v;
+			continue;
+		}
+		# prescale
+		x0 := (b[x+8*0]<<8)+8192;
+		x1 := b[x+8*4]<<8;
+		x2 := b[x+8*6];
+		x3 := b[x+8*2];
+		x4 := b[x+8*1];
+		x5 := b[x+8*7];
+		x6 := b[x+8*5];
+		x7 := b[x+8*3];
+		# first stage
+		x8 := W7*(x4+x5) + 4;
+		x4 = (x8+W1mW7*x4)>>3;
+		x5 = (x8-W1pW7*x5)>>3;
+		x8 = W3*(x6+x7) + 4;
+		x6 = (x8-W3mW5*x6)>>3;
+		x7 = (x8-W3pW5*x7)>>3;
+		# second stage
+		x8 = x0 + x1;
+		x0 -= x1;
+		x1 = W6*(x3+x2) + 4;
+		x2 = (x1-W2pW6*x2)>>3;
+		x3 = (x1+W2mW6*x3)>>3;
+		x1 = x4 + x6;
+		x4 -= x6;
+		x6 = x5 + x7;
+		x5 -= x7;
+		# third stage
+		x7 = x8 + x3;
+		x8 -= x3;
+		x3 = x0 + x2;
+		x0 -= x2;
+		x2 = (R2*(x4+x5)+128)>>8;
+		x4 = (R2*(x4-x5)+128)>>8;
+		# fourth stage
+		b[x+8*0] = (x7+x1)>>14;
+		b[x+8*1] = (x3+x2)>>14;
+		b[x+8*2] = (x0+x4)>>14;
+		b[x+8*3] = (x8+x6)>>14;
+		b[x+8*4] = (x8-x6)>>14;
+		b[x+8*5] = (x0-x4)>>14;
+		b[x+8*6] = (x3-x2)>>14;
+		b[x+8*7] = (x7-x1)>>14;
+	}
+}
+
+resample(src: array of byte, sw, sh: int, dw, dh: int) : array of byte
+{
+	if(src == nil || sw == 0 || sh == 0 || dw == 0 || dh == 0)
+		return src;
+	xfac := real sw / real dw;
+	yfac := real sh / real dh;
+	totpix := dw*dh;
+	dst := array[totpix] of byte;
+	dindex := 0;
+
+	# precompute index in src row corresponding to each index in dst row
+	sindices := array[dw] of int;
+	dx := 0.0;
+	for(x := 0; x < dw; x++) {
+		sx := int dx;
+		dx += xfac;
+		if(sx >= sw)
+			sx = sw-1;
+		sindices[x] = sx;
+	}
+	dy := 0.0;
+	for(y := 0; y < dh; y++) {
+		sy := int dy;
+		dy += yfac;
+		if(sy >= sh)
+			sy = sh-1;
+		soffset := sy * sw;
+		for(x = 0; x < dw; x++)
+			dst[dindex++] = src[soffset + sindices[x]];
+	}
+
+	return dst;
+}
+
+Cr2r := array [256] of {
+	-179, -178, -177, -175, -174, -172, -171, -170, -168, -167, -165, -164, -163, -161, -160, -158,
+	-157, -156, -154, -153, -151, -150, -149, -147, -146, -144, -143, -142, -140, -139, -137, -136,
+	-135, -133, -132, -130, -129, -128, -126, -125, -123, -122, -121, -119, -118, -116, -115, -114,
+	-112, -111, -109, -108, -107, -105, -104, -102, -101, -100, -98, -97, -95, -94, -93, -91,
+	-90, -88, -87, -86, -84, -83, -81, -80, -79, -77, -76, -74, -73, -72, -70, -69,
+	-67, -66, -64, -63, -62, -60, -59, -57, -56, -55, -53, -52, -50, -49, -48, -46,
+	-45, -43, -42, -41, -39, -38, -36, -35, -34, -32, -31, -29, -28, -27, -25, -24,
+	-22, -21, -20, -18, -17, -15, -14, -13, -11, -10, -8, -7, -6, -4, -3, -1,
+	0, 1, 3, 4, 6, 7, 8, 10, 11, 13, 14, 15, 17, 18, 20, 21,
+	22, 24, 25, 27, 28, 29, 31, 32, 34, 35, 36, 38, 39, 41, 42, 43,
+	45, 46, 48, 49, 50, 52, 53, 55, 56, 57, 59, 60, 62, 63, 64, 66,
+	67, 69, 70, 72, 73, 74, 76, 77, 79, 80, 81, 83, 84, 86, 87, 88,
+	90, 91, 93, 94, 95, 97, 98, 100, 101, 102, 104, 105, 107, 108, 109, 111,
+	112, 114, 115, 116, 118, 119, 121, 122, 123, 125, 126, 128, 129, 130, 132, 133,
+	135, 136, 137, 139, 140, 142, 143, 144, 146, 147, 149, 150, 151, 153, 154, 156,
+	157, 158, 160, 161, 163, 164, 165, 167, 168, 170, 171, 172, 174, 175, 177, 178,
+};
+
+Cr2g := array [256] of {
+	-91, -91, -90, -89, -89, -88, -87, -86, -86, -85, -84, -84, -83, -82, -81, -81,
+	-80, -79, -79, -78, -77, -76, -76, -75, -74, -74, -73, -72, -71, -71, -70, -69,
+	-69, -68, -67, -66, -66, -65, -64, -64, -63, -62, -61, -61, -60, -59, -59, -58,
+	-57, -56, -56, -55, -54, -54, -53, -52, -51, -51, -50, -49, -49, -48, -47, -46,
+	-46, -45, -44, -44, -43, -42, -41, -41, -40, -39, -39, -38, -37, -36, -36, -35,
+	-34, -34, -33, -32, -31, -31, -30, -29, -29, -28, -27, -26, -26, -25, -24, -24,
+	-23, -22, -21, -21, -20, -19, -19, -18, -17, -16, -16, -15, -14, -14, -13, -12,
+	-11, -11, -10, -9, -9, -8, -7, -6, -6, -5, -4, -4, -3, -2, -1, -1,
+	0, 1, 1, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9, 9, 10, 11,
+	11, 12, 13, 14, 14, 15, 16, 16, 17, 18, 19, 19, 20, 21, 21, 22,
+	23, 24, 24, 25, 26, 26, 27, 28, 29, 29, 30, 31, 31, 32, 33, 34,
+	34, 35, 36, 36, 37, 38, 39, 39, 40, 41, 41, 42, 43, 44, 44, 45,
+	46, 46, 47, 48, 49, 49, 50, 51, 51, 52, 53, 54, 54, 55, 56, 56,
+	57, 58, 59, 59, 60, 61, 61, 62, 63, 64, 64, 65, 66, 66, 67, 68,
+	69, 69, 70, 71, 71, 72, 73, 74, 74, 75, 76, 76, 77, 78, 79, 79,
+	80, 81, 81, 82, 83, 84, 84, 85, 86, 86, 87, 88, 89, 89, 90, 91,
+};
+
+Cb2g := array [256] of {
+	-44, -44, -43, -43, -43, -42, -42, -42, -41, -41, -41, -40, -40, -40, -39, -39,
+	-39, -38, -38, -38, -37, -37, -36, -36, -36, -35, -35, -35, -34, -34, -34, -33,
+	-33, -33, -32, -32, -32, -31, -31, -31, -30, -30, -30, -29, -29, -29, -28, -28,
+	-28, -27, -27, -26, -26, -26, -25, -25, -25, -24, -24, -24, -23, -23, -23, -22,
+	-22, -22, -21, -21, -21, -20, -20, -20, -19, -19, -19, -18, -18, -18, -17, -17,
+	-17, -16, -16, -15, -15, -15, -14, -14, -14, -13, -13, -13, -12, -12, -12, -11,
+	-11, -11, -10, -10, -10, -9, -9, -9, -8, -8, -8, -7, -7, -7, -6, -6,
+	-6, -5, -5, -4, -4, -4, -3, -3, -3, -2, -2, -2, -1, -1, -1, 0,
+	0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5,
+	6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11,
+	11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16,
+	17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22,
+	22, 22, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27,
+	28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 32, 32, 32, 33,
+	33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 37, 38, 38, 38,
+	39, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 43, 44,
+};
+
+Cb2b := array [256] of {
+	-227, -225, -223, -222, -220, -218, -216, -214, -213, -211, -209, -207, -206, -204, -202, -200,
+	-198, -197, -195, -193, -191, -190, -188, -186, -184, -183, -181, -179, -177, -175, -174, -172,
+	-170, -168, -167, -165, -163, -161, -159, -158, -156, -154, -152, -151, -149, -147, -145, -144,
+	-142, -140, -138, -136, -135, -133, -131, -129, -128, -126, -124, -122, -120, -119, -117, -115,
+	-113, -112, -110, -108, -106, -105, -103, -101, -99, -97, -96, -94, -92, -90, -89, -87,
+	-85, -83, -82, -80, -78, -76, -74, -73, -71, -69, -67, -66, -64, -62, -60, -58,
+	-57, -55, -53, -51, -50, -48, -46, -44, -43, -41, -39, -37, -35, -34, -32, -30,
+	-28, -27, -25, -23, -21, -19, -18, -16, -14, -12, -11, -9, -7, -5, -4, -2,
+	0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 18, 19, 21, 23, 25, 27,
+	28, 30, 32, 34, 35, 37, 39, 41, 43, 44, 46, 48, 50, 51, 53, 55,
+	57, 58, 60, 62, 64, 66, 67, 69, 71, 73, 74, 76, 78, 80, 82, 83,
+	85, 87, 89, 90, 92, 94, 96, 97, 99, 101, 103, 105, 106, 108, 110, 112,
+	113, 115, 117, 119, 120, 122, 124, 126, 128, 129, 131, 133, 135, 136, 138, 140,
+	142, 144, 145, 147, 149, 151, 152, 154, 156, 158, 159, 161, 163, 165, 167, 168,
+	170, 172, 174, 175, 177, 179, 181, 183, 184, 186, 188, 190, 191, 193, 195, 197,
+	198, 200, 202, 204, 206, 207, 209, 211, 213, 214, 216, 218, 220, 222, 223, 225,
+};
--- /dev/null
+++ b/appl/grid/register.b
@@ -1,0 +1,244 @@
+implement Register;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "dial.m";
+	dial: Dial;
+include "registries.m";
+	registries: Registries;
+	Registry, Attributes, Service: import registries;
+include "grid/announce.m";
+	announce: Announce;
+include "arg.m";
+
+registered: ref Registries->Registered;
+
+Register: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sys->pctl(sys->FORKNS | sys->NEWPGRP, nil);
+	dial = load Dial Dial->PATH;
+	if (dial == nil)
+		badmod(Dial->PATH);
+	registries = load Registries Registries->PATH;
+	if (registries == nil)
+		badmod(Registries->PATH);
+	registries->init();
+	announce = load Announce Announce->PATH;
+	if (announce == nil)
+		badmod(Announce->PATH);
+	announce->init();
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmod(Arg->PATH);
+
+	attrs := Attributes.new(("proto", "styx") :: ("auth", "none") :: ("resource","Cpu Pool") :: nil);
+	maxusers := -1;
+	autoexit := 0;
+	myaddr := "";
+	arg->init(argv);
+	arg->setusage("register [-u maxusers] [-e exit threshold] [-a attributes] { program }");
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'm' =>
+			attrs.set("memory", memory());
+		'u' =>
+			if ((maxusers = int arg->earg()) <= 0)
+				arg->usage();
+		'e' =>
+			if ((autoexit = int arg->earg()) < 0)
+				arg->usage();
+		'A' =>
+			myaddr = arg->earg();
+		'a' =>
+			attr := arg->earg();
+			val := arg->earg();
+			attrs.set(attr, val);
+		}
+	}
+	argv = arg->argv();
+	if (argv == nil)
+		arg->usage();
+	(nil, plist) := sys->tokenize(hd argv, "{} \t\n");
+	arg = nil;	
+	sysname := readfile("/dev/sysname");
+	reg: ref Registry;
+	reg = Registry.new("/mnt/registry");
+	if (reg == nil)
+		reg = Registry.connect(nil, nil, nil);
+	if (reg == nil)
+		error(sys->sprint("Could not find registry: %r\nMake sure that ndb/cs has been started and there is a registry announcing on the machine specified in /lib/ndb/local"));
+
+	c : ref Sys->Connection;
+	if (myaddr == nil) {
+		(addr, conn) := announce->announce();
+		if (addr == nil)
+			error(sys->sprint("cannot announce: %r"));
+		myaddr = addr;
+		c = conn;
+	}
+	else {
+		n: int;
+		c = dial->announce(myaddr);
+		if (c == nil)
+			error(sys->sprint("cannot announce: %r"));
+		(n, nil) = sys->tokenize(myaddr, "*");
+		if (n > 1) {
+			(nil, lst) := sys->tokenize(myaddr, "!");
+			if (len lst >= 3)
+				myaddr = "tcp!" + sysname +"!" + hd tl tl lst;
+		}
+	}
+	persist := 0;
+	if (attrs.get("name") == nil)
+		attrs.set("name", sysname);
+	err: string;
+	(registered, err) = reg.register(myaddr, attrs, persist);
+	if (err != nil) 
+		error("could not register with registry: "+err);
+
+	mountfd := popen(ctxt, plist);
+	spawn listener(c, mountfd, maxusers);
+}
+
+listener(c: ref Sys->Connection, mountfd: ref sys->FD, maxusers: int)
+{
+	for (;;) {
+		nc := dial->listen(c);
+		if (nc == nil)
+			error(sys->sprint("listen failed: %r"));
+		if (maxusers != -1 && nusers >= maxusers) {
+			sys->fprint(stderr(), "register: maxusers (%d) exceeded!\n", nusers);
+			dial->reject(nc, "server overloaded");
+		}else if ((dfd := dial->accept(nc)) != nil) {
+			sync := chan of int;
+			addr := readfile(nc.dir + "/remote");
+			if (addr == nil)
+				addr = "unknown";
+			if (addr[len addr - 1] == '\n')
+				addr = addr[:len addr - 1];
+			spawn proxy(sync, dfd, mountfd, addr);
+			<-sync;
+		}
+	}
+}
+
+proxy(sync: chan of int, dfd, mountfd: ref sys->FD, addr: string)
+{
+	pid := sys->pctl(Sys->NEWFD | Sys->NEWNS, 1 :: 2 :: mountfd.fd :: dfd.fd :: nil);
+	dfd = sys->fildes(dfd.fd);
+	mountfd = sys->fildes(mountfd.fd);
+	sync <-= 1;
+	done := chan of int;
+	spawn exportit(dfd, done);
+	if (sys->mount(mountfd, nil, "/", sys->MREPL | sys->MCREATE, addr) == -1)
+		sys->fprint(stderr(), "register: proxy mount failed: %r\n");
+	nusers++;
+	<-done;
+	nusers--;
+}
+
+nusers := 0;
+clock(tick: chan of int)
+{
+	for (;;) {
+		sys->sleep(2000);
+		tick <-= 1;
+	}
+}
+
+exportit(dfd: ref sys->FD, done: chan of int)
+{
+	sys->export(dfd, "/", sys->EXPWAIT);
+	done <-= 1;
+}
+
+popen(ctxt: ref Draw->Context, argv: list of string): ref Sys->FD
+{
+	sync := chan of int;
+	fds := array[2] of ref Sys->FD;
+	sys->pipe(fds);
+	spawn runcmd(ctxt, argv, fds[0], sync);
+	<-sync;
+	return fds[1];
+}
+
+runcmd(ctxt: ref Draw->Context, argv: list of string, stdin: ref Sys->FD, sync: chan of int)
+{
+	pid := sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	stdin = nil;
+	sync <-= 0;
+	sh := load Sh Sh->PATH;
+	sh->run(ctxt, argv);
+}
+
+error(e: string)
+{
+	sys->fprint(stderr(), "register: %s\n", e);
+	raise "fail:error";
+}
+
+user(): string
+{
+	if ((s := readfile("/dev/user")) == nil)
+		return "none";
+	return s;
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[8192] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+badmod(path: string)
+{
+	sys->fprint(stderr(), "Register: cannot load %s: %r\n", path);
+	exit;
+}
+
+killg(pid: int)
+{
+	if ((fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil) {
+		sys->fprint(fd, "killgrp");
+		fd = nil;
+	}
+}
+
+memory(): string
+{
+	buf := array[1024] of byte;
+	s := readfile("/dev/memory");
+	(nil, lst) := sys->tokenize(s, " \t\n");
+	if (len lst > 2) {
+		mem := int hd tl lst;
+		mem /= (1024*1024);
+		return string mem + "mb";
+	}
+	return "not known";
+}
--- /dev/null
+++ b/appl/grid/reglisten.b
@@ -1,0 +1,310 @@
+implement Listen;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+include "keyring.m";
+	keyring: Keyring;
+include "dial.m";
+	dial: Dial;
+include "security.m";
+	auth: Auth;
+include "sh.m";
+	sh: Sh;
+	Context: import sh;
+include "registries.m";
+	registries: Registries;
+	Registry, Attributes: import registries;
+
+Listen: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+badmodule(p: string)
+{
+	sys->fprint(stderr(), "listen: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+serverkey: ref Keyring->Authinfo;
+verbose := 0;
+
+registered: ref Registries->Registered;
+
+init(drawctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	auth = load Auth Auth->PATH;
+	if (auth == nil)
+		badmodule(Auth->PATH);
+	sh = load Sh Sh->PATH;
+	if (sh == nil)
+		badmodule(Sh->PATH);
+	dial = load Dial Dial->PATH;
+	if (dial == nil)
+		badmodule(Dial->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+	auth->init();
+	algs: list of string;
+	arg->init(argv);
+	keyfile: string;
+	initscript: string;
+	doauth := 1;
+	synchronous := 0;
+	trusted := 0;
+	regattrs: list of (string, string);
+	arg->setusage("listen [-i {initscript}] [-Ast] [-f keyfile] [-a alg]... addr command [arg...]");
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'a' =>
+			algs = arg->earg() :: algs;
+		'A' =>
+			doauth = 0;
+		'f' =>
+			keyfile = arg->earg();
+			if (! (keyfile[0] == '/' || (len keyfile > 2 &&  keyfile[0:2] == "./")))
+				keyfile = "/usr/" + user() + "/keyring/" + keyfile;
+		'i' =>
+			initscript = arg->earg();
+		'v' =>
+			verbose = 1;
+		's' =>
+			synchronous = 1;
+		't' =>
+			trusted = 1;
+		'r' =>
+			a := arg->earg();
+			v := arg->earg();
+			regattrs = (a, v) :: regattrs;
+		* =>
+			arg->usage();
+		}
+	}
+	if(regattrs != nil){
+		registries = load Registries Registries->PATH;
+		if(registries == nil)
+			badmodule(Registries->PATH);
+		registries->init();
+	}
+
+	if (doauth && algs == nil)
+		algs = getalgs();
+	if (algs != nil) {
+		if (keyfile == nil)
+			keyfile = "/usr/" + user() + "/keyring/default";
+		serverkey = keyring->readauthinfo(keyfile);
+		if (serverkey == nil) {
+			sys->fprint(stderr(), "listen: cannot read %s: %r\n", keyfile);
+			raise "fail:bad keyfile";
+		}
+	}
+	if(!trusted){
+		sys->unmount(nil, "/mnt/keys");	# should do for now
+		# become none?
+	}
+
+	argv = arg->argv();
+	n := len argv;
+	if (n < 2)
+		arg->usage();
+	arg = nil;
+
+	sync := chan[1] of string;
+	spawn listen(drawctxt, hd argv, tl argv, algs, regattrs, initscript, sync);
+	e := <-sync;
+	if(e != nil)
+		raise "fail:" + e;
+	if(synchronous){
+		e = <-sync;
+		if(e != nil)
+			raise "fail:" + e;
+	}
+}
+
+listen(drawctxt: ref Draw->Context, addr: string, argv: list of string,
+		algs: list of string, regattrs: list of (string, string),
+		initscript: string, sync: chan of string)
+{
+	{
+		listen1(drawctxt, addr, argv, algs, regattrs, initscript, sync);
+	} exception e {
+	"fail:*" =>
+		sync <-= e;
+	}
+}
+
+listen1(drawctxt: ref Draw->Context, addr: string, argv: list of string,
+		algs: list of string, regattrs: list of (string, string),
+		initscript: string, sync: chan of string)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	if(regattrs != nil){
+		sys->pctl(Sys->FORKNS, nil);
+		registry := Registry.new("/mnt/registry");
+		if(registry == nil)
+			registry = Registry.connect(nil, nil, nil);
+		if(registry == nil){
+			sys->fprint(stderr(), "reglisten: cannot register: %r\n");
+			sync <-= "cannot register";
+			exit;
+		}
+		err: string;
+		myaddr := addr;
+		(n, lst) := sys->tokenize(myaddr, "!");
+		if (n == 3 && hd tl lst == "*") {
+			sysname := readfile("/dev/sysname");
+			if (sysname != nil && sysname[len sysname - 1] == '\n')
+				sysname = sysname[:len sysname - 1];
+			myaddr = hd lst + "!" + sysname + "!" + hd tl tl lst;
+		}
+		(registered, err) = registry.register(myaddr, Attributes.new(regattrs), 0);
+		if(registered == nil){
+			sys->fprint(stderr(), "reglisten: cannot register %s: %s\n", myaddr, err);
+			sync <-= "cannot register";
+			exit;
+		}
+	}
+
+	ctxt := Context.new(drawctxt);
+	acon := dial->announce(addr);
+	if (acon == nil) {
+		sys->fprint(stderr(), "listen: failed to announce on '%s': %r\n", addr);
+		sync <-= "cannot announce";
+		exit;
+	}
+	ctxt.set("user", nil);
+	if (initscript != nil) {
+		ctxt.setlocal("net", ref Sh->Listnode(nil, acon.dir) :: nil);
+		ctxt.run(ref Sh->Listnode(nil, initscript) :: nil, 0);
+		initscript = nil;
+	}
+
+	# make sure the shell command is parsed only once.
+	cmd := sh->stringlist2list(argv);
+	if((hd argv) != nil && (hd argv)[0] == '{'){
+		(c, e) := sh->parse(hd argv);
+		if(c == nil){
+			sys->fprint(stderr(), "listen: %s\n", e);
+			sync <-= "parse error";
+			exit;
+		}
+		cmd = ref Sh->Listnode(c, hd argv) :: tl cmd;
+	}
+
+	sync <-= nil;
+	listench := chan of (int, ref Sys->Connection);
+	authch := chan of (string, ref Sys->Connection);
+	spawn listener(listench, acon, addr);
+	for (;;) {
+		user := "";
+		ccon: ref Sys->Connection;
+		alt {
+		(lok, c) := <-listench =>
+			if (lok == -1)
+				sync <-= "listen";
+			if (algs != nil) {
+				spawn authenticator(authch, c, algs, addr);
+				continue;
+			}
+			ccon = c;
+		(user, ccon) = <-authch =>
+			;
+		}
+		if (user != nil)
+			ctxt.set("user", sh->stringlist2list(user :: nil));
+		ctxt.set("net", ref Sh->Listnode(nil, ccon.dir) :: nil);
+
+		# XXX could do this in a separate process too, to
+		# allow new connections to arrive and start authenticating
+		# while the shell command is still running.
+		sys->dup(ccon.dfd.fd, 0);
+		sys->dup(ccon.dfd.fd, 1);
+		ccon.dfd = ccon.cfd = nil;
+		ctxt.run(cmd, 0);
+		sys->dup(2, 0);
+		sys->dup(2, 1);
+	}
+}
+
+listener(listench: chan of (int, ref Sys->Connection), c: ref Sys->Connection, addr: string)
+{
+	for (;;) {
+		nc := dial->listen(c);
+		if (nc == nil) {
+			sys->fprint(stderr(), "listen: listen error on '%s': %r\n", addr);
+			listench <-= (-1, nc);
+			exit;
+		}
+		if (verbose)
+			sys->fprint(stderr(), "listen: got connection on %s from %s",
+					addr, readfile(nc.dir + "/remote"));
+		nc.dfd = dial->accept(nc);
+		if (nc.dfd == nil)
+			sys->fprint(stderr(), "listen: cannot accept: %r\n");
+		else
+			listench <-= (0, nc);
+	}
+}
+
+authenticator(authch: chan of (string, ref Sys->Connection),
+		c: ref Sys->Connection, algs: list of string, addr: string)
+{
+	err: string;
+	(c.dfd, err) = auth->server(algs, serverkey, c.dfd, 0);
+	if (c.dfd == nil) {
+		sys->fprint(stderr(), "listen: auth on %s failed: %s\n", addr, err);
+		return;
+	}
+	if (verbose)
+		sys->fprint(stderr(), "listen: authenticated on %s as %s\n", addr, err);
+	authch <-= (err, c);
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+user(): string
+{
+	u := readfile("/dev/user");
+	if (u == nil)
+		return "nobody";
+	return u;
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
+
+getalgs(): list of string
+{
+	sslctl := readfile("#D/clone");
+	if (sslctl == nil) {
+		sslctl = readfile("#D/ssl/clone");
+		if (sslctl == nil)
+			return nil;
+		sslctl = "#D/ssl/" + sslctl;
+	} else
+		sslctl = "#D/" + sslctl;
+	(nil, algs) := sys->tokenize(readfile(sslctl + "/encalgs") + " " + readfile(sslctl + "/hashalgs"), " \t\n");
+	return "none" :: algs;
+}
--- /dev/null
+++ b/appl/grid/regstyxlisten.b
@@ -1,0 +1,269 @@
+implement Styxlisten;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	auth: Auth;
+include "registries.m";
+	registries: Registries;
+	Registry, Service, Attributes: import registries;
+include "dial.m";
+	dial: Dial;
+include "arg.m";
+include "sh.m";
+
+Styxlisten: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+badmodule(p: string)
+{
+	sys->fprint(stderr(), "styxlisten: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+verbose := 0;
+registered: ref Registries->Registered;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	auth = load Auth Auth->PATH;
+	if (auth == nil)
+		badmodule(Auth->PATH);
+	if ((e := auth->init()) != nil)
+		error("auth init failed: " + e);
+	keyring = load Keyring Keyring->PATH;
+	if (keyring == nil)
+		badmodule(Keyring->PATH);
+	dial = load Dial Dial->PATH;
+	if (dial == nil)
+		badmodule(Dial->PATH);
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+
+	arg->init(argv);
+	arg->setusage("styxlisten [-a alg]... [-Atsv] [-r attr val]... [-f keyfile] address cmd [arg...]");
+
+	algs: list of string;
+	doauth := 1;
+	synchronous := 0;
+	trusted := 0;
+	keyfile := "";
+	regattrs: list of (string, string);
+
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'v' =>
+			verbose = 1;
+		'a' =>
+			algs = arg->earg() :: algs;
+		'f' =>
+			keyfile = arg->earg();
+			if (! (keyfile[0] == '/' || (len keyfile > 2 &&  keyfile[0:2] == "./")))
+				keyfile = "/usr/" + user() + "/keyring/" + keyfile;
+		't' =>
+			trusted = 1;
+		'r' =>
+			a := arg->earg();
+			v := arg->earg();
+			regattrs = (a, v) :: regattrs;
+		's' =>
+			synchronous = 1;
+		'A' =>
+			doauth = 0;
+		}
+	}
+	argv = arg->argv();
+	if (len argv < 2)
+		arg->usage();
+	arg = nil;
+	if(regattrs != nil){
+		registries = load Registries Registries->PATH;
+		if(registries == nil)
+			badmodule(Registries->PATH);
+		registries->init();
+	}
+
+	if (doauth && algs == nil)
+		algs = getalgs();
+	addr := dial->netmkaddr(hd argv, "tcp", "styx");
+	cmd := tl argv;
+
+	authinfo: ref Keyring->Authinfo;
+	if (doauth) {
+		if (keyfile == nil)
+			keyfile = "/usr/" + user() + "/keyring/default";
+		authinfo = keyring->readauthinfo(keyfile);
+		if (authinfo == nil)
+			error(sys->sprint("cannot read %s: %r", keyfile));
+	}
+
+	c := dial->announce(addr);
+	if (dial == nil)
+		error(sys->sprint("cannot announce on %s: %r", addr));
+
+	if(regattrs != nil){
+		registry := Registry.new("/mnt/registry");
+		if(registry == nil)
+			registry = Registry.connect(nil, nil, nil);
+		if(registry == nil)
+			error(sys->sprint("cannot register: %r"));
+		err: string;
+		(registered, err) = registry.register(addr, Attributes.new(regattrs), 0);
+		if(registered == nil)
+			error("cannot register "+addr+": "+err);
+	}
+	if(!trusted){
+		sys->unmount(nil, "/mnt/keys");	# should do for now
+		# become none?
+	}
+
+	lsync := chan[1] of int;
+	if(synchronous)
+		listener(c, popen(ctxt, cmd, lsync), authinfo, algs, lsync);
+	else
+		spawn listener(c, popen(ctxt, cmd, lsync), authinfo, algs, lsync);
+}
+
+listener(c: ref Sys->Connection, mfd: ref Sys->FD, authinfo: ref Keyring->Authinfo, algs: list of string, lsync: chan of int)
+{
+	lsync <-= sys->pctl(0, nil);
+	for (;;) {
+		nc := dial->listen(c);
+		if (nc == nil)
+			error(sys->sprint("listen failed: %r"));
+		if (verbose)
+			sys->fprint(stderr(), "styxlisten: got connection from %s",
+					readfile(nc.dir + "/remote"));
+		dfd := dial->accept(nc);
+		if (dfd != nil) {
+			if (algs == nil) {
+				sync := chan of int;
+				spawn exportproc(sync, mfd, nil, dfd);
+				<-sync;
+			} else
+				spawn authenticator(dfd, authinfo, mfd, algs);
+		}
+	}
+}
+
+# authenticate a connection and set the user id.
+authenticator(dfd: ref Sys->FD, authinfo: ref Keyring->Authinfo, mfd: ref Sys->FD, algs: list of string)
+{
+	# authenticate and change user id appropriately
+	(fd, err) := auth->server(algs, authinfo, dfd, 1);
+	if (fd == nil) {
+		if (verbose)
+			sys->fprint(stderr(), "styxlisten: authentication failed: %s\n", err);
+		return;
+	}
+	if (verbose)
+		sys->fprint(stderr(), "styxlisten: client authenticated as %s\n", err);
+	sync := chan of int;
+	spawn exportproc(sync, mfd, err, dfd);
+	<-sync;
+}
+
+exportproc(sync: chan of int, fd: ref Sys->FD, uname: string, dfd: ref Sys->FD)
+{
+	sys->pctl(Sys->NEWFD | Sys->NEWNS, 2 :: fd.fd :: dfd.fd :: nil);
+	fd = sys->fildes(fd.fd);
+	dfd = sys->fildes(dfd.fd);
+	sync <-= 1;
+
+	# XXX unfortunately we cannot pass through the aname from
+	# the original attach, an inherent shortcoming of this scheme.
+	if (sys->mount(fd, nil, "/", Sys->MREPL|Sys->MCREATE, nil) == -1)
+		error(sys->sprint("cannot mount for user '%s': %r\n", uname));
+
+	sys->export(dfd, "/", Sys->EXPWAIT);
+}
+
+error(e: string)
+{
+	sys->fprint(stderr(), "styxlisten: %s\n", e);
+	raise "fail:error";
+}
+	
+popen(ctxt: ref Draw->Context, argv: list of string, lsync: chan of int): ref Sys->FD
+{
+	sync := chan of int;
+	fds := array[2] of ref Sys->FD;
+	sys->pipe(fds);
+	spawn runcmd(ctxt, argv, fds[0], sync, lsync);
+	<-sync;
+	return fds[1];
+}
+
+runcmd(ctxt: ref Draw->Context, argv: list of string, stdin: ref Sys->FD,
+		sync: chan of int, lsync: chan of int)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(stdin.fd, 0);
+	stdin = nil;
+	sync <-= 0;
+	sh := load Sh Sh->PATH;
+	e := sh->run(ctxt, argv);
+	kill(<-lsync, "kill");		# kill listener, as command has exited
+	if(verbose){
+		if(e != nil)
+			sys->fprint(stderr(), "styxlisten: command exited with error: %s\n", e);
+		else
+			sys->fprint(stderr(), "styxlisten: command exited\n");
+	}
+}
+
+kill(pid: int, how: string)
+{
+	sys->fprint(sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE), "%s", how);
+}
+
+user(): string
+{
+	if ((s := readfile("/dev/user")) == nil)
+		return "none";
+	return s;
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[1024] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+
+	return string buf[0:n];	
+}
+
+getalgs(): list of string
+{
+	sslctl := readfile("#D/clone");
+	if (sslctl == nil) {
+		sslctl = readfile("#D/ssl/clone");
+		if (sslctl == nil)
+			return nil;
+		sslctl = "#D/ssl/" + sslctl;
+	} else
+		sslctl = "#D/" + sslctl;
+	(nil, algs) := sys->tokenize(readfile(sslctl + "/encalgs") + " " + readfile(sslctl + "/hashalgs"), " \t\n");
+	return "none" :: algs;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/grid/remotelogon.b
@@ -1,0 +1,430 @@
+implement WmLogon;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+#
+# get a certificate to enable remote access.
+#
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Screen, Display, Image, Context, Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "arg.m";
+include "sh.m";
+include "dial.m";
+	dial: Dial;
+include "newns.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	login: Login;
+include "registries.m";
+	registries: Registries;
+	Registry, Attributes: import registries;
+
+
+# XXX where to put the certificate: is the username already set to
+# something appropriate, with a home directory and keyring directory in that?
+
+# how do we find out the signer; presumably from the registry?
+# should do that before signing on; if we can't get it, then prompt for it.
+WmLogon: module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+registry: ref Registry;
+usr := "";
+passwd := "";
+loginaddr := "";
+signerpkhash := "";
+
+cfg := array[] of {
+	"frame .f -bd 2 -relief raised",
+	"label .f.p -bitmap @/icons/inferno.bit -borderwidth 2 -relief raised",
+	"label .f.ul -text {User Name:} -anchor w",
+	"entry .f.ue -bg white -width 10w",
+	"label .f.pl -text {Password:} -anchor w",
+	"entry .f.pe -bg white -show *",
+	"checkbutton .f.ck -variable newuser -text {New}",
+	"frame .f.f -borderwidth 2 -relief raised",
+	"frame .f.u",
+	"pack .f.ue -in .f.u -side left -expand 1 -fill x",
+	"pack .f.ck -in .f.u -side left",
+	"grid .f.ul -row 0 -column 0 -sticky e -in .f.f",
+	"grid .f.u -row 0 -column 1 -sticky ew -in .f.f",
+	"grid .f.pl -row 1 -column 0 -sticky e -in .f.f",
+	"grid .f.pe -row 1 -column 1 -sticky ew -in .f.f",
+	"pack .f.p .f.f -fill x",
+	"bind .f.ue <Key-\n> {focus .f.pe}",
+	"bind .f.ue {<Key-\t>} {focus .f.pe}",
+	"bind .f.pe <Key-\n> {send panelcmd ok}",
+	"bind .f.pe {<Key-\t>} {focus .f.ue}",
+	"focus .f.ue",
+};
+
+notecfg := array[] of {
+	"frame .n -bd 2 -relief raised",
+	"frame .n.f",
+	"label .n.f.m -anchor nw",
+	"label .n.f.l -bitmap error -foreground red",
+	"button .n.b -text Continue -command {send notecmd done}",
+	"focus .n.f",
+	"bind .n.f <Key-\n> {send notecmd done}",
+	"pack .n.f.l .n.f.m -side left -expand 1",
+	"pack .n.f .n.b",
+};
+
+checkload[T](x: T, p: string): T
+{
+	if(x == nil)
+		error(sys->sprint("cannot load %s: %r\n", p));
+	return x;
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = checkload(load Draw Draw->PATH, Draw->PATH);
+	tk = checkload(load Tk Tk->PATH, Tk->PATH);
+	tkclient = checkload(load Tkclient Tkclient->PATH, Tkclient->PATH);
+	tkclient->init();
+	login = checkload(load Login Login->PATH, Login->PATH);
+	keyring = checkload(load Keyring Keyring->PATH, Keyring->PATH);
+	dial = checkload(load Dial Dial->PATH, Dial->PATH);
+	registries = checkload(load Registries Registries->PATH, Registries->PATH);
+	registries->init();
+
+	arg := load Arg Arg->PATH;
+	if(arg != nil){
+		arg->init(argv);
+		arg->setusage("usage: logon [-u user] [-p passwd] [-a loginaddr] command [arg...]]\n");
+		while((opt := arg->opt()) != 0){
+			case opt{
+			'a' =>
+				loginaddr = arg->earg();
+			'k' =>
+				signerpkhash = arg->earg();
+			'u' =>
+				usr = arg->earg();
+			'p' =>
+				passwd = arg->earg();
+			* =>
+				arg->usage();
+			}
+		}
+		argv = arg->argv();
+		arg = nil;
+	} else {
+		if(tl argv != nil)
+			sys->fprint(stderr(), "remotelogon: cannot load %s: %r; ignoring arguments\n", Arg->PATH);
+		argv = nil;
+	}
+	sys->pctl(Sys->FORKNS, nil);
+
+	sync := chan of (ref Keyring->Authinfo, string);
+	spawn logon(ctxt, sync);
+	(key, err) := <-sync;
+	if(key == nil)
+		raise "fail:" + err;
+	registry = nil;
+	servekeyfile(key);
+
+	errch := chan of string;
+	spawn exec(ctxt, argv, errch);
+	err = <-errch;
+	if (err != nil)
+		error(err);
+}
+
+# run in a separate process so that we keep the outer namespace unsullied by
+# mounted registries.
+logon(ctxt: ref Draw->Context, sync: chan of (ref Keyring->Authinfo, string))
+{
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+
+	{
+		logon1(ctxt, sync);
+	} exception e {
+	"fail:*" =>
+		sync <-= (nil, e[5:]);
+	}
+}
+
+logon1(ctxt: ref Draw->Context, sync: chan of (ref Keyring->Authinfo, string))
+{
+	if(ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+
+	(top, ctl) := tkclient->toplevel(ctxt, nil, nil, Tkclient->Plain);
+	tkclient->startinput(top, "kbd" :: "ptr" :: nil);
+	tkclient->onscreen(top, "onscreen");
+	stop := chan of int;
+	spawn tkclient->handler(top, stop);
+	if(usr != nil){
+		fa := loginaddr;
+		if(fa == nil)
+			fa = findloginresource(top, signerpkhash);
+		if(getauthinfo(top, fa, 0, sync)){
+			cleanup();
+			stop <-= 1;
+			exit;
+		}
+	}
+
+	cmd(top, "canvas .c -buffer none -bg #777777");
+	cmd(top, "pack .c -fill both -expand 1");
+	enter := makepanel(top);
+	for(;;) {
+		cmd(top, "focus .f.ue; update");
+		<-enter;
+		usr = cmd(top, ".f.ue get");
+		if(usr == nil) {
+			notice(top, "You must supply a user name to login");
+			continue;
+		}
+		passwd = cmd(top, ".f.pe get");
+
+		if(getauthinfo(top, loginaddr, int cmd(top, "variable newuser"), sync)){
+			cleanup();
+			stop <-= 1;
+			exit;
+		}
+		cmd(top, ".f.ue delete 0 end");
+		cmd(top, ".f.pe delete 0 end");
+	}
+}
+
+findloginresource(top: ref Tk->Toplevel, signerpkhash: string): string
+{
+	mountregistry();
+	attrs := ("resource", "login")::nil;
+	if(signerpkhash != nil)
+		attrs = ("pk", signerpkhash) :: attrs;
+	(svc, err) := registry.find(attrs);
+	if(svc == nil){
+		notice(top, "cannot find name of login server");
+		return nil;
+	}
+	return (hd svc).addr;
+}
+
+cleanup()
+{
+	# get rid of spurious mouse/kbd reading processes.
+	# XXX should probably implement "stop" ctl message in wmlib
+	sys->fprint(sys->open("/prog/"+string sys->pctl(0, nil)+"/ctl", Sys->OWRITE), "killgrp");
+}
+
+getauthinfo(top: ref Tk->Toplevel, addr: string, newuser: int, sync: chan of (ref Keyring->Authinfo, string)): int
+{
+	if(newuser)
+		if(createuser(top, usr, passwd, signerpkhash) == 0)
+			return 0;
+
+	if(addr == nil){
+		addr = findloginresource(top, signerpkhash);
+		if(addr == nil)
+			return 0;
+	}
+	(err, info) := login->login(usr, passwd, addr);
+	if(info == nil){
+		notice(top, "Login failed:\n" + err);
+		return 0;
+	}
+	sync <-= (info, nil);
+	return 1;
+}
+
+createuser(top: ref Tk->Toplevel, user, passwd: string, signerpkhash: string): int
+{
+	mountregistry();
+	attrs := ("resource", "createuser")::nil;
+	if(signerpkhash != nil)
+		attrs = ("signer", signerpkhash) :: attrs;
+	(svcs, err) := registry.find(attrs);
+	if(svcs == nil){
+		notice(top, "cannot find name of login server");
+		return 0;
+	}
+	addr := (hd svcs).addr;
+	c := dial->dial(addr, nil);
+	if(c == nil){
+		notice(top, sys->sprint("cannot dial %s: %r", addr));
+		return 0;
+	}
+	if(sys->mount(c.dfd, nil, "/tmp", Sys->MREPL, nil) == -1){
+		notice(top, sys->sprint("cannot mount %s: %r", addr));
+		return 0;
+	}
+	fd := sys->open("/tmp/createuser", Sys->OWRITE);
+	if(fd == nil){
+		notice(top, sys->sprint("cannot open createuser: %r"));
+		return 0;
+	}
+	if(sys->fprint(fd, "%q %q", user, passwd) <= 0){
+		notice(top, sys->sprint("cannot create user: %r"));
+		return 0;
+	}
+	signerpkhash = (hd svcs).attrs.get("signer");
+	return 1;
+}
+
+servekeyfile(info: ref Keyring->Authinfo)
+{
+	keys := "/usr/" + user() + "/keyring";
+	if(sys->bind("#s", keys, Sys->MBEFORE) == -1)
+		error(sys->sprint("cannot bind #s: %r"));
+	fio := sys->file2chan(keys, "default");
+	if(fio == nil)
+		error(sys->sprint("cannot make %s: %r", keys + "/default"));
+	sync := chan of int;
+	spawn infofile(fio, sync);
+	<-sync;
+
+	if(keyring->writeauthinfo(keys + "/default", info) == -1)
+		error(sys->sprint("cannot write %s: %r", keys + "/default"));
+}
+
+mountregistry()
+{
+	if(registry == nil)
+		registry = Registry.new("/mnt/registry");
+	if(registry == nil)
+		registry = Registry.connect(nil, nil, nil);
+	if(registry == nil){
+		sys->fprint(stderr(), "logon: cannot contact registry: %r\n");
+		raise "fail:no registry";
+	}
+}
+
+infofile(fileio: ref Sys->FileIO, sync: chan of int)
+{
+	sys->pctl(Sys->NEWPGRP|Sys->NEWFD|Sys->NEWNS, nil);
+	sync <-= 1;
+
+	infodata: array of byte;
+	for(;;) alt {
+	(off, nbytes, fid, rc) := <-fileio.read =>
+		if(rc == nil)
+			break;
+		if(off > len infodata)
+			off = len infodata;
+		rc <-= (infodata[off:], nil);
+
+	(off, data, fid, wc) := <-fileio.write =>
+		if(wc == nil)
+			break;
+
+		if(off != len infodata){
+			wc <-= (0, "cannot be rewritten");
+		} else {
+			nid := array[len infodata+len data] of byte;
+			nid[0:] = infodata;
+			nid[len infodata:] = data;
+			infodata = nid;
+			wc <-= (len data, nil);
+		}
+	}
+}
+
+exec(ctxt: ref Draw->Context, argv: list of string, errch: chan of string)
+{
+	sys->pctl(sys->NEWFD, 0 :: 1 :: 2 :: nil);
+	if(argv == nil)
+		argv = "/dis/wm/wm.dis" :: nil;
+	else {
+		sh := load Sh Sh->PATH;
+		if(sh != nil){
+			sh->run(ctxt, "{$* &}" :: argv);
+			errch <-= nil;
+			exit;
+		}
+	}
+	{
+		cmd := load Command hd argv;
+		if (cmd == nil) {
+			errch <-= sys->sprint("cannot load %s: %r", hd argv);
+		} else {
+			errch <-= nil;
+			spawn cmd->init(ctxt, argv);
+		}
+	}exception{
+	"fail:*" =>
+		exit;
+	}
+}
+
+makepanel(top: ref Tk->Toplevel): chan of string
+{
+	c := chan of string;
+	tk->namechan(top, c, "panelcmd");
+
+	for(i := 0; i < len cfg; i++)
+		cmd(top, cfg[i]);
+	centre(top, ".f");
+	return c;
+}
+
+centre(top: ref Tk->Toplevel, w: string): string
+{
+	ir := tk->rect(top, w, Tk->Required);
+	r := tk->rect(top, ".", 0);
+	org := Point(r.dx() / 2 - ir.dx() / 2, r.dy() / 3 - ir.dy() / 2);
+	if (org.y < 0)
+		org.y = 0;
+	if(org.x < 0)
+		org.x = 0;
+	return cmd(top, ".c create window "+string org.x+" "+string org.y+" -window "+w+" -anchor nw");
+}
+
+notice(top: ref Tk->Toplevel, message: string)
+{
+	if(top == nil)
+		error(message);
+	c := chan of string;
+	tk->namechan(top, c, "notecmd");
+	for(i := 0; i < len notecfg; i++)
+		cmd(top, notecfg[i]);
+	cmd(top, ".n.f.m configure -text '" + message);
+	id := centre(top, ".n");
+	cmd(top, "update");
+	<-c;
+	cmd(top, ".c delete " + id);
+	cmd(top, "destroy .n");
+	cmd(top, "update");
+}
+
+error(e: string)
+{
+	sys->fprint(stderr(), "remotelogon: %s\n", e);
+	raise "fail:error";
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	buf := array[8192] of byte;
+	if((n := sys->read(fd, buf, len buf)) > 0)
+		return string buf[0:n];
+	return "none";
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr(), "remotelogon: tk error on '%s': %s\n", s, e);
+	return e;
+}
--- /dev/null
+++ b/appl/grid/usercreatesrv.b
@@ -1,0 +1,93 @@
+implement Usercreatesrv;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "string.m";
+	str: String;
+include "keyring.m";
+	keyring: Keyring;
+
+# create insecure users.
+
+Usercreatesrv: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	keyring = load Keyring Keyring->PATH;
+
+	sys->pctl(Sys->FORKNS, nil);
+
+	fio := export();
+	for(;;) alt {
+	(nil, nil, nil, rc) := <-fio.read =>
+		if(rc != nil)
+			rc <-= (nil, "permission denied");
+	(nil, data, fid, wc) := <-fio.write =>
+		# request:
+		# username email
+		if(wc == nil)
+			break;
+		toks := str->unquoted(string data);
+		if(len toks != 2){
+			wc <-= (0, "invalid request");
+			break;
+		}
+		uname := hd toks; toks = tl toks;
+		password := array of byte hd toks; toks = tl toks;
+		secret := array[Keyring->SHA1dlen] of byte;
+		keyring->sha1(password, len password, secret, nil);
+#		email := hd toks; toks = tl toks;
+#		e := checkemail(email);
+#		if(e != nil){
+#			wc <-= (0, e);
+#			break;
+#		}
+		dir := "/mnt/keys/" + uname;
+		if(sys->create(dir, Sys->OREAD, Sys->DMDIR|8r777) == nil){
+			wc <-= (0, sys->sprint("cannot create account: %r"));
+			break;
+		}
+		sys->write(sys->create(dir + "/secret", Sys->OWRITE, 8r600), secret, len secret);
+		wc <-= (len data, nil);
+#		sys->print("create %q %q\n", uname, email);
+	}
+}
+
+checkemail(addr: string): string
+{
+	for(i := 0; i < len addr; i++)
+		if(addr[i] == '@')
+			 break;
+	if(i == len addr)
+		return "email address does not contain an '@' character";
+	return nil;
+}
+
+export(): ref Sys->FileIO
+{
+	sys->bind("#s", "/chan", Sys->MREPL|Sys->MCREATE);
+	fio := sys->file2chan("/chan", "createuser");
+	w := sys->nulldir;
+	w.mode = 8r222;
+	sys->wstat("/chan/createuser", w);
+	sync := chan of int;
+	spawn exportproc(sync);
+	<-sync;
+	return fio;
+}
+
+exportproc(sync: chan of int)
+{
+	sys->pctl(Sys->FORKNS|Sys->NEWFD, 0 :: nil);
+	sync <-= 0;
+	sys->export(sys->fildes(0), "/chan", Sys->EXPWAIT);
+}
--- /dev/null
+++ b/appl/lib/NOTICE
@@ -1,0 +1,25 @@
+This copyright NOTICE applies to all files in this directory and
+subdirectories, unless another copyright notice appears in a given
+file or subdirectory.  If you take substantial code from this software to use in
+other programs, you must somehow include with it an appropriate
+copyright notice that includes the copyright notice and the other
+notices below.  It is fine (and often tidier) to do that in a separate
+file such as NOTICE, LICENCE or COPYING.
+
+Copyright © 1995-1999 Lucent Technologies Inc.
+Portions Copyright © 1997-2000 Vita Nuova Limited
+Portions Copyright © 2000-2010 Vita Nuova Holdings Limited
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License (`LGPL') as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
--- /dev/null
+++ b/appl/lib/arg.b
@@ -1,0 +1,118 @@
+implement Arg;
+
+#
+# Copyright © 1997 Roger Peppe
+#
+
+include "sys.m";
+include "arg.m";
+
+name:= "";
+args: list of string;
+usagemsg:="";
+printusage := 1;
+
+curropt: string;
+
+init(argv: list of string)
+{
+	(curropt, args, name) = (nil, nil, nil);
+	if (argv == nil)
+		return;
+	name = hd argv;
+	args = tl argv;
+}
+
+setusage(u: string)
+{
+	usagemsg = u;
+	printusage = u != nil;
+}
+
+progname(): string
+{
+	return name;
+}
+
+# don't allow any more options after this function is invoked
+argv(): list of string
+{
+	ret := args;
+	args = nil;
+	return ret;
+}
+
+earg(): string
+{
+	if (curropt != nil) {
+		ret := curropt;
+		curropt = nil;
+		return ret;
+	}
+
+	if (args == nil)
+		usage();
+
+	ret := hd args;
+	args = tl args;
+	return ret;
+}
+
+# get next option argument
+arg(): string
+{
+	if (curropt != nil) {
+		ret := curropt;
+		curropt = nil;
+		return ret;
+	}
+
+	if (args == nil)
+		return nil;
+
+	ret := hd args;
+	args = tl args;
+	return ret;
+}
+
+# get next option letter
+# return 0 at end of options
+opt(): int
+{
+	if (curropt != nil) {
+		opt := curropt[0];
+		curropt = curropt[1:];
+		return opt;
+	}
+
+	if (args == nil)
+		return 0;
+
+	nextarg := hd args;
+	if (len nextarg < 2 || nextarg[0] != '-')
+		return 0;
+
+	if (nextarg == "--") {
+		args = tl args;
+		return 0;
+	}
+
+	opt := nextarg[1];
+	if (len nextarg > 2)
+		curropt = nextarg[2:];
+	args = tl args;
+	return opt;
+}
+
+usage()
+{
+	if(printusage){
+		if(usagemsg != nil)
+			u := "usage: "+usagemsg;
+		else
+			u = name + ": argument expected";
+		sys := load Sys Sys->PATH;
+		sys->fprint(sys->fildes(2), "%s\n", u);
+	}
+	raise "fail:usage";
+}
--- /dev/null
+++ b/appl/lib/asn1.b
@@ -1,0 +1,1030 @@
+implement ASN1;
+
+include "sys.m";
+	sys: Sys;
+
+include "asn1.m";
+
+# Masks
+TAG_MASK : con 16r1F;
+CONSTR_MASK : con 16r20;
+CLASS_MASK : con 16rC0;
+
+# Decoding errors
+OK, ESHORT, ETOOBIG, EVALLEN, ECONSTR, EPRIM, EINVAL, EUNIMPL: con iota;
+
+debug : con 0;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+# Decode the whole array as a BER encoding of an ASN1 type.
+# If there's an error, the return string will contain the error.
+# Depending on the error, the returned elem may or may not
+# be nil.
+decode(a: array of byte) : (string, ref Elem)
+{
+	(ecode, i, elem) := ber_decode(a, 0, len a);
+	return (errstr(ecode, i, len a), elem);
+}
+
+# Like decode, but continue decoding after first element
+# of array ends.
+decode_seq(a: array of byte) : (string, list of ref Elem)
+{
+	(ecode, i, elist) := seq_decode(a, 0, len a, -1, 1);
+	return (errstr(ecode, i, len a), elist);
+}
+
+# Decode the whole array as a BER encoding of an ASN1 value,
+# (i.e., the part after the tag and length).
+# Assume the value is encoded as universal tag "kind".
+# The constr arg is 1 if the value is constructed, 0 if primitive.
+# If there's an error, the return string will contain the error.
+# Depending on the error, the returned value may or may not
+# be nil.
+decode_value(a: array of byte, kind, constr: int) : (string, ref Value)
+{
+	n := len a;
+	(ecode, i, val) := value_decode(a, 0, n, n, kind, constr);
+	return (errstr(ecode, i, len a), val);
+}
+
+# The rest of the decoding routines take the array (a), the
+# starting position (i), and the ending position +1 (n).
+# They return (err code, new i, [... varies]).
+
+# for debugging
+ber_ind := "";
+ber_ind_save := "";
+
+# Decode an ASN1 (tag, length, value).
+ber_decode(a: array of byte, i, n: int) : (int, int, ref Elem)
+{
+	if(debug) {
+		ber_ind_save = ber_ind;
+		ber_ind = ber_ind + "  ";
+		sys->print("%sber_decode, byte %d\n", ber_ind, i);
+	}
+	err, length: int;
+	tag : Tag;
+	val : ref Value;
+	elem : ref Elem = nil;
+	(err, i, tag) = tag_decode(a, i, n);
+	if(err == OK) {
+		(err, i, length) = length_decode(a, i, n);
+		if(err == OK) {
+			if(debug)
+				sys->print("%sgot tag %s, length %d, now at byte %d\n",
+						ber_ind, tag.tostring(), length, i);
+			if(tag.class == Universal)
+				(err, i, val) = value_decode(a, i, n, length, tag.num, tag.constr);
+			else
+				(err, i, val) = value_decode(a, i, n, length, OCTET_STRING, 0);
+			if(val != nil)
+				elem = ref Elem(tag, val);
+		}
+	}
+	if(debug) {
+		sys->print("%send ber_decode, byte %d\n", ber_ind, i);
+		if(val != nil) {
+			sys->print("%sdecode result:\n", ber_ind);
+			print_elem(elem);
+		}
+		if(err != OK)
+			sys->print("%serror: %s\n", ber_ind, errstr(err, i, i));
+		ber_ind = ber_ind_save;
+	}
+	return (err, i, elem);
+}
+
+# Decode a tag field.  As well as Tag, return an int that
+# is 1 if the type is constructed, 0 if not.
+tag_decode(a: array of byte, i, n: int) : (int, int, Tag)
+{
+	err := OK;
+	class, num, constr: int;
+	if(n-i >= 2) {
+		v := int a[i++];
+		class = v & CLASS_MASK;
+		if(v & CONSTR_MASK)
+			constr = 1;
+		else
+			constr = 0;
+		num = v & TAG_MASK;
+		if(num == TAG_MASK)
+			# long tag number
+			(err, i, num) = uint7_decode(a, i, n);
+	}
+	else
+		err = ESHORT;
+	return (err, i, Tag(class, num, constr));
+}
+
+# Decode a length field.  Assume it fits in a Limbo int.
+# If "indefinite length", return -1.
+length_decode(a: array of byte, i, n: int) : (int, int, int)
+{
+	err := OK;
+	num := 0;
+	if(i < n) {
+		v := int a[i++];
+		if(v & 16r80)
+			return int_decode(a, i, n, v&16r7F, 1);
+		else if(v == 16r80)
+			num = -1;
+		else
+			num = v;
+	}
+	else
+		err = ESHORT;
+	return (err, i, num);
+}
+
+# Decode a value according to the encoding of the Universal
+# type with number "kind" and constructed/primitive according
+# to "constr", with given length (may be -1, for "indefinite").
+value_decode(a: array of byte, i, n, length, kind, constr: int) : (int, int, ref Value)
+{
+	err := OK;
+	val : ref Value;
+	va : array of byte;
+	if(length == -1) {
+		if(!constr)
+			err = EINVAL;
+	}
+	else if(i+length > n)
+		err = EVALLEN;
+	if(err != OK)
+		return (err, i, nil);
+	case kind {
+	0 =>
+		# marker for end of indefinite constructions
+		if(length == 0)
+			val = ref Value.EOC;
+		else
+			err = EINVAL;
+	BOOLEAN =>
+		if(constr)
+			err = ECONSTR;
+		else if(length != 1)
+			err = EVALLEN;
+		else {
+			val = ref Value.Bool(int a[0]);
+			i++;
+		}
+	INTEGER or ENUMERATED =>
+		if(constr)
+			err = ECONSTR;
+		else if(length <= 4) {
+			num : int;
+			(err, i, num) = int_decode(a, i, i+length, length, 0);
+			if(err == OK)
+				val = ref Value.Int(num);
+		}
+		else {
+			va = array[length] of byte;
+			va[0:] = a[i:i+length];
+			val = ref Value.BigInt(va);
+			i += length;
+		}
+	BIT_STRING =>
+		if(constr) {
+			if(length == -1 && i+2 <= n && a[i] == byte 0 && a[i+1] == byte 0) {
+				val = ref Value.BitString(0, nil);
+				i += 2;
+			}
+			else
+				# TODO: recurse and concat results
+				err = EUNIMPL;
+		}
+		else {
+			if(length < 2) {
+				if(length == 1 && a[0] == byte 0) {
+					val = ref Value.BitString(0, nil);
+					i ++;
+				}
+				else
+					err = EINVAL;
+			}
+			else {
+				bitsunused := int a[i];
+				if(bitsunused > 7)
+					err = EINVAL;
+				else if(length > 16r0FFFFFFF)
+					err = ETOOBIG;
+				else {
+					va = array[length-1] of byte;
+					va[0:] = a[i+1:i+length];
+					val = ref Value.BitString(bitsunused, va);
+					i += length;
+				}
+			}
+		}
+	OCTET_STRING or ObjectDescriptor =>
+		(err, i, va) = octet_decode(a, i, n, length, constr);
+		if(err == OK)
+			val = ref Value.Octets(va);
+	NULL =>
+		if(constr)
+			err = ECONSTR;
+		else if(length != 0)
+			err = EVALLEN;
+		else
+			val = ref Value.Null;
+	OBJECT_ID =>
+		if(constr)
+			err = ECONSTR;
+		else if (length == 0)
+			err = EVALLEN;
+		else {
+			subids : list of int = nil;
+			iend := i+length;
+			while(i < iend) {
+				x : int;
+				(err, i, x) = uint7_decode(a, i, n);
+				if(err != OK)
+					break;
+				subids = x :: subids;
+			}
+			if(err == OK) {
+				if(i != iend)
+					err = EVALLEN;
+				else {
+					m := len subids;
+					ia := array[m+1] of int;
+					while(subids != nil) {
+						y := hd subids;
+						subids = tl subids;
+						if(m == 1) {
+							ia[1] = y % 40;
+							ia[0] = y / 40;
+						}
+						else
+							ia[m--] = y;
+					}
+					val = ref Value.ObjId(ref Oid(ia));
+				}
+			}
+		}
+	EXTERNAL or EMBEDDED_PDV =>
+		# TODO: parse this internally
+		va = array[length] of byte;
+		va[0:] = a[i:i+length];
+		val = ref Value.Other(va);
+		i += length;
+	REAL =>
+		# let the appl decode, with math module
+		if(constr)
+			err = ECONSTR;
+		else {
+			va = array[length] of byte;
+			va[0:] = a[i:i+length];
+			val = ref Value.Real(va);
+			i += length;
+		}
+	SEQUENCE or SET=>
+		vl : list of ref Elem;
+		(err, i, vl) = seq_decode(a, i, n, length, constr);
+		if(err == OK) {
+			if(kind == SEQUENCE)
+				val = ref Value.Seq(vl);
+			else
+				val = ref Value.Set(vl);
+		}
+	NumericString or PrintableString or TeletexString
+	or VideotexString or IA5String or UTCTime
+	or GeneralizedTime or GraphicString or VisibleString
+	or GeneralString or UniversalString or BMPString =>
+		(err, i, va) = octet_decode(a, i, n, length, constr);
+		if(err == OK)
+			# sometimes wrong: need to do char set conversion
+			val = ref Value.String(string va);
+		
+	* =>
+		va = array[length] of byte;
+		va[0:] = a[i:i+length];
+		val = ref Value.Other(va);
+		i += length;
+	}
+	return (err, i, val);
+}
+
+# Decode an int in format where count bytes are
+# concatenated to form value.
+# Although ASN1 allows any size integer, we return
+# an error if the result doesn't fit in a Limbo int.
+# If unsigned is not set, make sure to propagate sign bit.
+int_decode(a: array of byte, i, n, count, unsigned: int) : (int, int, int)
+{
+	err := OK;
+	num := 0;
+	if(n-i >= count) {
+		if((count > 4) || (unsigned && count == 4 && (int a[i] & 16r80)))
+			err = ETOOBIG;
+		else {
+			if(!unsigned && count > 0 && count < 4 && (int a[i] & 16r80))
+				num = -1;		# all bits set
+			for(j := 0; j < count; j++) {
+				v := int a[i++];
+				num = (num << 8) | v;
+			}
+		}
+	}
+	else
+		err = ESHORT;
+	return (err, i, num);
+}
+
+# Decode an unsigned int in format where each
+# byte except last has high bit set, and remaining
+# seven bits of each byte are concatenated to form value.
+# Although ASN1 allows any size integer, we return
+# an error if the result doesn't fit in a Limbo int.
+uint7_decode(a: array of byte, i, n: int) : (int, int, int)
+{
+	err := OK;
+	num := 0;
+	more := 1;
+	while(more && i < n) {
+		v := int a[i++];
+		if(num & 16r7F000000) {
+			err = ETOOBIG;
+			break;
+		}
+		num <<= 7;
+		more = v & 16r80;
+		num |= (v & 16r7F);
+	}
+	if(n == i)
+		err = ESHORT;
+	return (err, i, num);
+}
+
+# Decode an octet string, recursively if constr.
+# We've already checked that length==-1 implies constr==1,
+# and otherwise that specified length fits within a[i..n].
+octet_decode(a: array of byte, i, n, length, constr: int) : (int, int, array of byte)
+{
+	err := OK;
+	va : array of byte;
+	if(length >= 0 && !constr) {
+		va = array[length] of byte;
+		va[0:] = a[i:i+length];
+		i += length;
+	}
+	else {
+		# constructed, either definite or indefinite length
+		lva : list of array of byte = nil;
+		elem : ref Elem;
+		istart := i;
+		totbytes := 0;
+	    cloop:
+		for(;;) {
+			if(length >= 0 && i >= istart+length) {
+				if(i != istart+length)
+					err = EVALLEN;
+				break cloop;
+			}
+			oldi := i;
+			(err, i, elem) = ber_decode(a, i, n);
+			if(err != OK)
+				break;
+			pick v := elem.val {
+				Octets =>
+					lva = v.bytes :: lva;
+					totbytes += len v.bytes;
+				EOC =>
+					if(length != -1) {
+						i = oldi;
+						err = EINVAL;
+					}
+					break cloop;
+				* =>
+					i = oldi;
+					err = EINVAL;
+					break cloop;
+			}
+		}
+		if(err == OK) {
+			va = array[totbytes] of byte;
+			j := totbytes;
+			while(lva != nil) {
+				x := hd lva;
+				lva = tl lva;
+				m := len x;
+				va[j-m:] = x[0:];
+				j -= m;
+			}
+		}
+	}
+	return (err, i, va);
+}
+
+# Decode a sequence or set.
+# We've already checked that length==-1 implies constr==1,
+# and otherwise that specified length fits within a[i..n].
+seq_decode(a : array of byte, i, n, length, constr: int) : (int, int, list of ref Elem)
+{
+	err := OK;
+	ans : list of ref Elem = nil;
+	if(!constr)
+		err = EPRIM;
+	else {
+		# constructed, either definite or indefinite length
+		lve : list of ref Elem = nil;
+		elem : ref Elem;
+		istart := i;
+	    cloop:
+		for(;;) {
+			if(length >= 0 && i >= istart+length) {
+				if(i != istart+length)
+					err = EVALLEN;
+				break cloop;
+			}
+			oldi := i;
+			(err, i, elem) = ber_decode(a, i, n);
+			if(err != OK)
+				break;
+			pick v := elem.val {
+				EOC =>
+					if(length != -1) {
+						i = oldi;
+						err = EINVAL;
+					}
+					break cloop;
+				* =>
+					lve = elem :: lve;
+			}
+		}
+		if(err == OK) {
+			# reverse back to original order
+			while(lve != nil) {
+				e := hd lve;
+				lve = tl lve;
+				ans = e :: ans;
+			}
+		}
+	}
+	return (err, i, ans);
+}
+
+# Encode e by BER rules
+encode(e: ref Elem) : (string, array of byte)
+{
+	(err, n) := enc(nil, e, 0, 1);
+	if(err != "")
+		return (err, nil);
+	b := array[n] of byte;
+	enc(b, e, 0, 0);
+	return ("", b);
+}
+
+# Encode e into array b, only putting in bytes if !lenonly.
+# Start at loc i, return index after.
+enc(b: array of byte, e: ref Elem, i, lenonly: int) : (string, int)
+{
+	(err, vlen, constr) := val_enc(b, e, 0, 1);
+	if(err != "")
+		return (err, i);
+	tag := e.tag;
+	v := tag.class | constr;
+	if(tag.num < 31) {
+		if(!lenonly)
+			b[i] = byte (v | tag.num);
+		i++;
+	}
+	else {
+		if(!lenonly)
+			b[i] = byte (v | 31);
+		if(tag.num < 0)
+			return ("negative tag number", i);
+		i = uint7_enc(b, tag.num, i+1, lenonly);
+	}
+	if(vlen < 16r80) {
+		if(!lenonly)
+			b[i] = byte vlen;
+		i++;
+	}
+	else {
+		ilen := int_enc(b, vlen, 1, 0, 1);
+		if(!lenonly) {
+			b[i] = byte (16r80 | ilen);
+			i = int_enc(b, vlen, 1, i+1, 0);
+		}
+		else
+			i += 1+ilen;
+	}
+	if(!lenonly)
+		val_enc(b, e, i, 0);
+	i += vlen;
+	return ("", i);
+}
+
+# Encode e.val into array b, only putting in bytes if !lenonly.
+# Start at loc i, return (err, index after, constructed or primitive)
+val_enc(b: array of byte, e: ref Elem, i, lenonly: int) : (string, int, int)
+{
+	kind := e.tag.num;
+	cl := e.tag.class;
+	ok := 1;
+	v : int;
+	bb : array of byte;
+	constr := 0;
+	if(cl != Universal) {
+		pick vv := e.val {
+		Bool =>
+			kind = BOOLEAN;
+		Int =>
+			kind = INTEGER;
+		BigInt =>
+			kind = INTEGER;
+		Octets =>
+			kind = OCTET_STRING;
+		Real =>
+			kind = REAL;
+		Other =>
+			kind = OCTET_STRING;
+		BitString =>
+			kind = BIT_STRING;
+		Null =>
+			kind = NULL;
+		ObjId =>
+			kind = OBJECT_ID;
+		String =>
+			kind = UniversalString;
+		Seq =>
+			kind = SEQUENCE;
+		Set =>
+			kind = SET;
+		}
+	}
+	case kind {
+	BOOLEAN =>
+		(ok, v) = e.is_int();
+		if(ok) {
+			if(v != 0)
+				v = 255;
+			i = int_enc(b, v, 1, i, lenonly);
+		}
+	INTEGER or ENUMERATED =>
+		(ok, v) = e.is_int();
+		if(ok)
+			i = int_enc(b, v, 0, i, lenonly);
+		else {
+			(ok, bb) = e.is_bigint();
+			if(ok) {
+				if(!lenonly)
+					b[i:] = bb;
+				i += len bb;
+			}
+		}
+	BIT_STRING =>
+		(ok, v, bb) = e.is_bitstring();
+		if(ok) {
+			if(bb == nil) {
+				if(!lenonly)
+					b[i] = byte 0;
+				i++;
+			}
+			else {
+				if(v < 0 || v > 7)
+					ok = 0;
+				else {
+					if(!lenonly) {
+						b[i] = byte v;
+						b[i+1:] = bb;
+					}
+					i += 1 + len bb;
+				}
+			}
+		}
+	OCTET_STRING or ObjectDescriptor or EXTERNAL or REAL
+	or EMBEDDED_PDV =>
+		pick vv := e.val {
+		Octets or Real or Other =>
+			if(!lenonly && vv.bytes != nil)
+					b[i:] = vv.bytes;
+			i += len vv.bytes;
+		 * =>
+			ok = 0;
+		}
+	NULL =>
+		;
+	OBJECT_ID =>
+		oid : ref Oid;
+		(ok, oid) = e.is_oid();
+		if(ok) {
+			n := len oid.nums;
+			for(k := 0; k < n; k++) {
+				v = oid.nums[k];
+				if(k == 0) {
+					v *= 40;
+					if(n > 1)
+						v += oid.nums[++k];
+				}
+				i = uint7_enc(b, v, i, lenonly);
+			}
+		}
+	SEQUENCE or SET =>
+		pick vv := e.val {
+		Seq or Set =>
+			constr = CONSTR_MASK;
+			for(l := vv.l; l != nil; l = tl l) {
+				err : string;
+				(err, i) = enc(b, hd l, i, lenonly);
+				if(err != "")
+					return (err, i, 0);
+			}
+	}
+	NumericString or PrintableString or TeletexString
+	or VideotexString or IA5String or UTCTime
+	or GeneralizedTime or GraphicString or VisibleString
+	or GeneralString or UniversalString or BMPString =>
+		pick vv := e.val {
+			String =>
+				bb = array of byte vv.s;
+				if(!lenonly && bb != nil)
+					b[i:] = bb;
+				i += len bb;
+			* =>
+				ok = 0;
+		}
+	* =>
+		ok = 0;
+	}
+	if(!ok)
+		return ("bad value for encoding kind", i, constr);
+	return ("", i, constr);
+}
+
+# Encode num as unsigned 7 bit values with top bit 1 on all bytes
+# except last, into array b, only putting in bytes if !lenonly.
+# Start at loc i, return index after.
+uint7_enc(b: array of byte, num, i, lenonly: int) : int
+{
+	n := 1;
+	v := num>>7;
+	while(v > 0) {
+		v >>= 7;
+		n++;
+	}
+	if(lenonly)
+		i += n;
+	else {
+		for(k := (n-1)*7; k > 0; k -= 7)
+			b[i++] = byte ((num>>k) | 16r80);
+		b[i++] = byte (num & 16r7F);
+	}
+	return i;
+}
+
+# Encode num as unsigned or signed integer into array b,
+# only putting in bytes if !lenonly.
+# Encoding is length followed by bytes to concatenate.
+# Start at loc i, return index after.
+int_enc(b: array of byte, num, unsigned, i, lenonly: int) : int
+{
+	v := num;
+	if(v < 0)
+		v = -(v+1);
+	n := 1;
+	prevv := v;
+	v >>= 8;
+	while(v > 0) {
+		prevv = v;
+		v >>= 8;
+		n++;
+	}
+	if(!unsigned && (prevv & 16r80))
+		n++;
+	if(lenonly)
+		i += n;
+	else {
+		for(k := (n-1)*8; k >= 0; k -= 8)
+			b[i++] = byte (num>>k);
+	}
+	return i;
+}
+
+# Compare two arrays of integers; return true if they match
+intarr_eq(a: array of int, b: array of int) : int
+{
+	alen := len a;
+	if(alen != len b)
+		return 0;
+	for(i := 0; i < alen; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+# Look for o in tab; if found, return index, else return -1.
+oid_lookup(o: ref Oid, tab: array of Oid) : int
+{
+	for(i := 0; i < len tab; i++)
+		if(intarr_eq(o.nums, tab[i].nums))
+			return i;
+	return -1;
+}
+
+# If e is a SEQUENCE, return (1, e's element list)
+# else return (error, nil).
+Elem.is_seq(e: self ref Elem) : (int, list of ref Elem)
+{
+	if(e.tag.class == Universal && e.tag.num == SEQUENCE) {
+		pick v := e.val {
+		Seq =>
+			return (1, v.l);
+		}
+	}
+	return (0, nil);
+}
+
+# If e is a SET, return (1, e's element list)
+# else return (error, nil).
+Elem.is_set(e: self ref Elem) : (int, list of ref Elem)
+{
+	if(e.tag.class == Universal && e.tag.num == SET) {
+		pick v := e.val {
+		Set =>
+			return (1, v.l);
+		}
+	}
+	return (0, nil);
+}
+
+# If e is an INTEGER that fits in a limbo int, return (1, val)
+# else return (0, 0l).
+Elem.is_int(e: self ref Elem) : (int, int)
+{
+	if(e.tag.class == Universal && (e.tag.num == INTEGER || e.tag.num == BOOLEAN)) {
+		pick v := e.val {
+		Bool or
+		Int =>
+			return (1, v.v);
+		}
+	}
+	return (0, 0);
+}
+
+# If e is an INTEGER that doesn't fit in a limbo int, return (1, bytes),
+# or even if it does fit, return it as an array of bytes.
+# else return (0, nil).
+Elem.is_bigint(e: self ref Elem) : (int, array of byte)
+{
+	if(e.tag.class == Universal && e.tag.num == INTEGER) {
+		pick v := e.val {
+		BigInt =>
+			return (1, v.bytes);
+		Int =>
+			x := v.v;
+			a := array[4] of byte;
+			for(i := 0; i < 4; i++)
+				a[i] = byte ((x >> (8*(3-i))) & 16rFF);
+			for(j := 0; j < 3; j++)
+				if(a[j] != byte 0)
+					break;
+			return (1, a[j:]);
+		}
+	}
+	return (0, nil);
+}
+
+# If e is a bitstring, return (1, unused bits, bytes containing bit string),
+# else return (0, nil)
+Elem.is_bitstring(e: self ref Elem) : (int, int, array of byte)
+{
+	if(e.tag.class == Universal && e.tag.num == BIT_STRING) {
+		pick v := e.val {
+		BitString =>
+			return (1, v.unusedbits, v.bits);
+		}
+	}
+	return (0, 0, nil);
+}
+
+# If e is an octetstring, return (1, bytes),
+# else return (0, nil)
+Elem.is_octetstring(e: self ref Elem) : (int, array of byte)
+{
+	if(e.tag.class == Universal && e.tag.num == OCTET_STRING) {
+		pick v := e.val {
+		Octets =>
+			return (1, v.bytes);
+		}
+	}
+	return (0, nil);
+}
+
+# If e is an object id, return (1, ref Oid),
+# else return (0, nil)
+Elem.is_oid(e: self  ref Elem) : (int, ref Oid)
+{
+	if(e.tag.class == Universal && e.tag.num == OBJECT_ID) {
+		pick v := e.val {
+		ObjId =>
+			return (1, v.id);
+		}
+	}
+	return (0, nil);
+}
+
+# If e is some kind of string (excluding times), return (1, string),
+# else return (0, "")
+Elem.is_string(e: self ref Elem) : (int, string)
+{
+	if(e.tag.class == Universal) {
+		case e.tag.num {
+		NumericString or PrintableString or TeletexString
+		or VideotexString or IA5String or GraphicString
+		or VisibleString or GeneralString or UniversalString
+		or BMPString =>
+		pick v := e.val {
+			String =>
+				return (1, v.s);
+			}
+		}
+	}
+	return (0, nil);
+}
+
+# If e is some kind of time, return (1, string),
+# else return (0, "")
+Elem.is_time(e: self ref Elem) : (int, string)
+{
+	if(e.tag.class == Universal
+	   && (e.tag.num == UTCTime || e.tag.num == GeneralizedTime)) {
+		pick v := e.val {
+		String =>
+			return (1, v.s);
+		}
+	}
+	return (0, nil);
+}
+
+# Return printable error string for code ecode.
+# i is position where error is first noted.
+# n is the end of the passed data: if i!=n then
+# we didn't use all the data and an error should
+# be returned about that.
+errstr(ecode, i, n: int) : string
+{
+	if(ecode == OK && i == n)
+		return "";
+	err := "BER decode: ";
+	case ecode {
+		OK =>
+			err += "OK";
+		ESHORT =>
+			err += "need more data";
+		ETOOBIG =>
+			err += "value exceeds implementation limit";
+		EVALLEN =>
+			err += "value has wrong length";
+		ECONSTR =>
+			err += "value is constructed, should be primitive";
+		EPRIM =>
+			err += "value is primitive";
+		EINVAL =>
+			err += "value encoding invalid";
+		* =>
+			err += "unknown error " + string ecode;
+	}
+	if(err == "" && i != n)
+		err += "extra data";
+	err += " at byte " + string i;
+	return err;
+}
+
+# Printing functions, for debugging
+
+Tag.tostring(t: self Tag) : string
+{
+	ans := "";
+	snum := string t.num;
+	if(t.class == Universal) {
+		case t.num {
+		BOOLEAN => ans = "BOOLEAN";
+		INTEGER => ans = "INTEGER";
+		BIT_STRING => ans = "BIT STRING";
+		OCTET_STRING => ans = "OCTET STRING";
+		NULL => ans = "NULL";
+		OBJECT_ID => ans = "OBJECT IDENTIFER";
+		ObjectDescriptor => ans = "OBJECT_DES";
+		EXTERNAL => ans = "EXTERNAL";
+		REAL => ans = "REAL";
+		ENUMERATED => ans = "ENUMERATED";
+		EMBEDDED_PDV => ans = "EMBEDDED PDV";
+		SEQUENCE => ans = "SEQUENCE";
+		SET => ans = "SET";
+		NumericString => ans = "NumericString";
+		PrintableString => ans = "PrintableString";
+		TeletexString => ans = "TeletexString";
+		VideotexString => ans = "VideotexString";
+		IA5String => ans = "IA5String";
+		UTCTime => ans = "UTCTime";
+		GeneralizedTime => ans = "GeneralizedTime";
+		GraphicString => ans = "GraphicString";
+		VisibleString => ans = "VisibleString";
+		GeneralString => ans = "GeneralString";
+		UniversalString => ans = "UniversalString";
+		BMPString => ans = "BMPString";
+		* => ans = "UNIVERSAL " + snum;
+		}
+	}
+	else {
+		case t.class {
+		Application =>
+			ans = "APPLICATION " + snum;
+		Context =>
+			ans = "CONTEXT "+ snum;
+		Private =>
+			ans = "PRIVATE " + snum;
+		}
+	}
+	return ans;
+}
+
+Elem.tostring(e: self ref Elem) : string
+{
+	return estring(e, "");
+}
+
+Value.tostring(v: self ref Value) : string
+{
+	return vstring(v, "");
+}
+
+estring(e: ref Elem, indent: string) : string
+{
+	return indent + e.tag.tostring() + " " + vstring(e.val, indent);
+}
+
+vstring(val: ref Value, indent: string) : string
+{
+	ans := "";
+	pick v := val {
+		Bool or Int =>
+			ans += string v.v;
+		Octets or BigInt or Real or Other =>
+			ans += bastring(v.bytes, indent + "\t");
+		BitString =>
+			ans += " bits (unused " +string v.unusedbits + ")" +  bastring(v.bits, indent + "\t");
+		Null  or EOC =>
+			;
+		ObjId =>
+			ans += v.id.tostring();
+		String =>
+			ans += "\"" + v.s + "\"";
+		Seq or Set =>
+			ans += "{\n";
+			newindent := indent + "\t";
+			l := v.l;
+			while(l != nil) {
+				if(ans[len ans-1] != '\n')
+					ans[len ans] = '\n';
+				ans += estring(hd l, newindent);
+				l = tl l;
+			}
+			if(ans[len ans-1] != '\n')
+				ans[len ans] = '\n';
+			ans += indent + "}";
+	}
+	return ans;
+}
+
+bastring(a: array of byte, indent: string) : string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	ans := indent;
+	nlindent := "\n" + indent;
+	for(i := 0; i < len a; i++) {
+		if(i < len a - 1 && i%10 == 0)
+			ans += nlindent ;
+		ans += sys->sprint("%2x ", int a[i]);
+	}
+	return ans;
+}
+
+Oid.tostring(o: self ref Oid) : string
+{
+	ans := "";
+	for(i := 0; i < len o.nums; i++) {
+		ans += string o.nums[i];
+		if(i < len o.nums - 1)
+			ans[len ans] = '.';
+	}
+	return ans;
+}
+
+print_elem(e: ref Elem)
+{
+	s := e.tostring();
+	a := array of byte s;
+	sys->write(sys->fildes(1), a, len a);
+	sys->print("\n");
+}
--- /dev/null
+++ b/appl/lib/attrdb.b
@@ -1,0 +1,486 @@
+implement Attrdb;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "attrdb.m";
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		return sys->sprint("can't load Bufio: %r");
+	return nil;
+}
+
+parseentry(s: string, lno: int): (ref Dbentry, int, string)
+{
+	(nil, flds) := sys->tokenize(s, "\n");
+	lines: list of ref Tuples;
+	errs: string;
+	for(; flds != nil; flds = tl flds){
+		(ts, err) := parseline(hd flds, lno);
+		if(ts != nil)
+			lines = ts :: lines;
+		else if(err != nil && errs == nil)
+			errs = err;
+		lno++;
+	}
+	return (ref Dbentry(0, lines), lno, errs);
+}
+
+parseline(s: string, lno: int): (ref Tuples, string)
+{
+	attrs: list of ref Attr;
+	quote := 0;
+	word := "";
+	lastword := "";
+	name := "";
+
+Line:
+	for(i := 0; i < len s; i++) {
+		if(quote) {
+			if(s[i] == quote) {
+				if(i+1 >= len s || s[i+1] != quote){
+					quote = 0;
+					continue;
+				}
+				i++;
+			}
+			word[len word] = s[i];
+			continue;
+		}
+		case s[i] {
+		'\'' or '\"' =>
+			quote = s[i];
+		'#' =>
+			break Line;
+		' ' or '\t' or '\n' =>
+			if(word == nil)
+				continue;
+			if(lastword != nil) {
+				# lastword space word space
+				attrs = ref Attr(lastword, nil, 0) :: attrs;
+			}
+			lastword = word;
+			word = nil;
+
+			if(name != nil) {
+				# name = lastword space
+				attrs = ref Attr(name, lastword, 0) :: attrs;
+				name = lastword = nil;
+			}
+		'=' =>
+			if(lastword == nil) {
+				# word=
+				lastword = word;
+				word = nil;
+			}
+			if(word != nil) {
+				# lastword word=
+				attrs = ref Attr(lastword, nil, 0) :: attrs;
+				lastword = word;
+				word = nil;
+			}
+			if(lastword == nil)
+				return (nil, "empty name");
+			name = lastword;
+			lastword = nil;
+		* =>
+			word[len word] = s[i];
+		}
+	}
+	if(quote)
+		return (nil, "missing quote");
+
+	if(lastword == nil) {
+		lastword = word;
+		word = nil;
+	}
+
+	if(name == nil) {
+		name = lastword;
+		lastword = nil;
+	}
+
+	if(name != nil)
+		attrs = ref Attr(name, lastword, 0) :: attrs;
+
+	if(attrs == nil)
+		return (nil, nil);
+
+	return (ref Tuples(lno, rev(attrs)), nil);
+
+}
+
+Tuples.hasattr(ts: self ref Tuples, attr: string): int
+{
+	for(pl := ts.pairs; pl != nil; pl = tl pl){
+		a := hd pl;
+		if(a.attr == attr)
+			return 1;
+	}
+	return 0;
+}
+
+Tuples.haspair(ts: self ref Tuples, attr: string, value: string): int
+{
+	for(pl := ts.pairs; pl != nil; pl = tl pl){
+		a := hd pl;
+		if(a.attr == attr && a.val == value)
+			return 1;
+	}
+	return 0;
+}
+
+Tuples.find(ts: self ref Tuples, attr: string): list of ref Attr
+{
+	ra: list of ref Attr;
+	for(pl := ts.pairs; pl != nil; pl = tl pl){
+		a := hd pl;
+		if(a.attr == attr)
+			ra = a :: ra;
+	}
+	return rev(ra);
+}
+
+Tuples.findbyattr(ts: self ref Tuples, attr: string, value: string, rattr: string): list of ref Attr
+{
+	if(ts.haspair(attr, value))
+		return ts.find(rattr);
+	return nil;
+}
+
+Dbentry.find(e: self ref Dbentry, attr: string): list of (ref Tuples, list of ref Attr)
+{
+	rrt: list of (ref Tuples, list of ref Attr);
+	for(lines := e.lines; lines != nil; lines = tl lines){
+		l := hd lines;
+		if((ra := l.find(attr)) != nil)
+			rrt = (l, rev(ra)) :: rrt;
+	}
+	rt: list of (ref Tuples, list of ref Attr);
+	for(; rrt != nil; rrt = tl rrt)
+		rt = hd rrt :: rt;
+	return rt;
+}
+
+Dbentry.findfirst(e: self ref Dbentry, attr: string): string
+{
+	for(lines := e.lines; lines != nil; lines = tl lines){
+		l := hd lines;
+		for(pl := l.pairs; pl != nil; pl = tl pl)
+			if((hd pl).attr == attr)
+				return (hd pl).val;
+	}
+	return nil;
+}
+
+Dbentry.findpair(e: self ref Dbentry, attr: string, value: string): list of ref Tuples
+{
+	rts: list of ref Tuples;
+	for(lines := e.lines; lines != nil; lines = tl lines){
+		l := hd lines;
+		if(l.haspair(attr, value))
+			rts = l :: rts;
+	}
+	for(; rts != nil; rts = tl rts)
+		lines = hd rts :: lines;
+	return lines;
+}
+
+Dbentry.findbyattr(e: self ref Dbentry, attr: string, value: string, rattr: string): list of (ref Tuples, list of ref Attr)
+{
+	rm: list of (ref Tuples, list of ref Attr);	# lines with attr=value and rattr
+	rnm: list of (ref Tuples, list of ref Attr);	# lines with rattr alone
+	for(lines := e.lines; lines != nil; lines = tl lines){
+		l := hd lines;
+		ra: list of ref Attr = nil;
+		match := 0;
+		for(pl := l.pairs; pl != nil; pl = tl pl){
+			a := hd pl;
+			if(a.attr == attr && a.val == value)
+				match = 1;
+			if(a.attr == rattr)
+				ra = a :: ra;
+		}
+		if(ra != nil){
+			if(match)
+				rm = (l, rev(ra)) :: rm;
+			else
+				rnm = (l, rev(ra)) :: rnm;
+		}
+	}
+	rt: list of (ref Tuples, list of ref Attr);
+	for(; rnm != nil; rnm = tl rnm)
+		rt = hd rnm :: rt;
+	for(; rm != nil; rm = tl rm)
+		rt = hd rm :: rt;
+	return rt;
+}
+
+Dbf.open(path: string): ref Dbf
+{
+	df := ref Dbf;
+	df.lockc = chan[1] of int;
+	df.fd = bufio->open(path, Bufio->OREAD);
+	if(df.fd == nil)
+		return nil;
+	df.name = path;
+	(ok, d) := sys->fstat(df.fd.fd);
+	if(ok >= 0)
+		df.dir = ref d;
+	# TO DO: indices
+	return df;
+}
+
+Dbf.sopen(data: string): ref Dbf
+{
+	df := ref Dbf;
+	df.lockc = chan[1] of int;
+	df.fd = bufio->sopen(data);
+	if(df.fd == nil)
+		return nil;
+	df.name = nil;
+	df.dir = nil;
+	return df;
+}
+
+Dbf.reopen(df: self ref Dbf): int
+{
+	lock(df);
+	if(df.name == nil){
+		unlock(df);
+		return 0;
+	}
+	fd := bufio->open(df.name, Bufio->OREAD);
+	if(fd == nil){
+		unlock(df);
+		return -1;
+	}
+	df.fd = fd;
+	df.dir = nil;
+	(ok, d) := sys->fstat(fd.fd);
+	if(ok >= 0)
+		df.dir = ref d;
+	# TO DO: cache, hash tables
+	unlock(df);
+	return 0;
+}
+
+Dbf.changed(df: self ref Dbf): int
+{
+	r: int;
+
+	lock(df);
+	if(df.name == nil){
+		unlock(df);
+		return 0;
+	}
+	(ok, d) := sys->stat(df.name);
+	if(ok < 0)
+		r = df.fd != nil || df.dir == nil;
+	else
+		r = df.dir == nil || !samefile(*df.dir, d);
+	unlock(df);
+	return r;
+}
+
+samefile(d1, d2: Sys->Dir): int
+{
+	# ``it was black ... it was white!  it was dark ...  it was light! ah yes, i remember it well...''
+	return d1.dev==d2.dev && d1.dtype==d2.dtype &&
+			d1.qid.path==d2.qid.path && d1.qid.vers==d2.qid.vers &&
+			d1.mtime == d2.mtime;
+}
+
+flatten(ts: list of (ref Tuples, list of ref Attr), attr: string): list of ref Attr
+{
+	l: list of ref Attr;
+	for(; ts != nil; ts = tl ts){
+		(line, nil) := hd ts;
+		t := line.find(attr);
+		for(; t != nil; t = tl t)
+			l = hd t :: l;
+	}
+	return rev(l);
+}
+
+Db.open(path: string): ref Db
+{
+	df := Dbf.open(path);
+	if(df == nil)
+		return nil;
+	db := ref Db(df :: nil);
+	(e, nil) := db.findpair(nil, "database", "");
+	if(e != nil){
+		files := flatten(e.find("file"), "file");
+		if(files != nil){
+			dbs: list of ref Dbf;
+			for(; files != nil; files = tl files){
+				name := (hd files).val;
+				if(name == path && df != nil){
+					dbs = df :: dbs;
+					df = nil;
+				}else if((tf := Dbf.open(name)) != nil)
+					dbs = tf :: dbs;
+			}
+			db.dbs = rev(dbs);
+			if(df != nil)
+				db.dbs = df :: db.dbs;
+		}
+	}
+	return db;
+}
+
+Db.sopen(data: string): ref Db
+{
+	df := Dbf.sopen(data);
+	if(df == nil)
+		return nil;
+	return ref Db(df :: nil);
+}
+
+Db.append(db1: self ref Db, db2: ref Db): ref Db
+{
+	if(db1 == nil)
+		return db2;
+	if(db2 == nil)
+		return db1;
+	dbs := db2.dbs;
+	for(rl := rev(db1.dbs); rl != nil; rl = tl rl)
+		dbs = hd rl :: dbs;
+	return ref Db(dbs);
+}
+
+Db.reopen(db: self ref Db): int
+{
+	f := 0;
+	for(dbs := db.dbs; dbs != nil; dbs = tl dbs)
+		if((hd dbs).reopen() < 0)
+			f = -1;
+	return f;
+}
+
+Db.changed(db: self ref Db): int
+{
+	f := 0;
+	for(dbs := db.dbs; dbs != nil; dbs = tl dbs)
+		f |= (hd dbs).changed();
+	return f;
+}
+
+isentry(l: string): int
+{
+	return l!=nil && l[0]!='\t' && l[0]!='\n' && l[0]!=' ' && l[0]!='#';
+}
+
+Dbf.readentry(dbf: self ref Dbf, offset: int, attr: string, value: string, useval: int): (ref Dbentry, int, int)
+{
+	lock(dbf);
+	fd := dbf.fd;
+	fd.seek(big offset, 0);
+	lines: list of ref Tuples;
+	match := attr == nil;
+	while((l := fd.gets('\n')) != nil){
+		while(isentry(l)){
+			lines = nil;
+			do{
+				offset = int fd.offset();
+				(t, nil) := parseline(l, 0);
+				if(t != nil){
+					lines = t :: lines;
+					if(!match){
+						if(useval)
+							match = t.haspair(attr, value);
+						else
+							match = t.hasattr(attr);
+					}
+				}
+				l = fd.gets('\n');
+			}while(l != nil && !isentry(l));
+			if(match && lines != nil){
+				rl := lines;
+				for(lines = nil; rl != nil; rl = tl rl)
+					lines = hd rl :: lines;
+				unlock(dbf);
+				return (ref Dbentry(0, lines), 1, offset);
+			}
+		}
+	}
+	unlock(dbf);
+	return (nil, 0, int fd.offset());
+}
+
+nextentry(db: ref Db, ptr: ref Dbptr, attr: string, value: string, useval: int): (ref Dbentry, ref Dbptr)
+{
+	if(ptr == nil){
+		ptr = ref Dbptr.Direct(db.dbs, nil, 0);
+		# TO DO: index
+	}
+	while(ptr.dbs != nil){
+		offset: int;
+		dbf := hd ptr.dbs;
+		pick p := ptr {
+		Direct =>
+			offset = p.offset;
+		Hash =>
+			raise "not done yet";
+		}
+		(e, match, next) := dbf.readentry(offset, attr, value, useval);
+		if(match)
+			return (e, ref Dbptr.Direct(ptr.dbs, nil, next));
+		if(e == nil)
+			ptr = ref Dbptr.Direct(tl ptr.dbs, nil, 0);
+		else
+			ptr = ref Dbptr.Direct(ptr.dbs, nil, next);
+	}
+	return (nil, ptr);
+}
+
+Db.find(db: self ref Db, ptr: ref Dbptr, attr: string): (ref Dbentry, ref Dbptr)
+{
+	return nextentry(db, ptr, attr, nil, 0);
+}
+
+Db.findpair(db: self ref Db, ptr: ref Dbptr, attr: string, value: string): (ref Dbentry, ref Dbptr)
+{
+	return nextentry(db, ptr, attr, value, 1);
+}
+
+Db.findbyattr(db: self ref Db, ptr: ref Dbptr, attr: string, value: string, rattr: string): (ref Dbentry, ref Dbptr)
+{
+	for(;;){
+		e: ref Dbentry;
+		(e, ptr) = nextentry(db, ptr, attr, value, 1);
+		if(e == nil || e.find(rattr) != nil)
+			return (e, ptr);
+	}
+}
+
+rev[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+lock(dbf: ref Dbf)
+{
+	dbf.lockc <-= 1;
+}
+
+unlock(dbf: ref Dbf)
+{
+	<-dbf.lockc;
+}
--- /dev/null
+++ b/appl/lib/attrhash.b
@@ -1,0 +1,109 @@
+implement Attrhash, Attrindex;
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+
+include "attrdb.m";
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		return sys->sprint("can't load %s: %r", Bufio->PATH);
+	return nil;
+}
+
+attrindex(): Attrindex
+{
+	return load Attrindex "$self";
+}
+
+Index.open(dbf: Attrdb->Dbf, attr: string, fd: ref Sys->FD): ref Index
+{
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0 || dbf.dir == nil || dbf.dir.mtime > d.mtime)
+		return nil;
+	length := int d.length;
+	if(length < NDBHLEN)
+		return nil;
+	buf := array[length] of byte;
+	if(sys->read(fd, buf, len buf) != len buf)
+		return nil;
+	mtime := get4(buf);
+	if(mtime != dbf.dir.mtime)
+		return nil;
+	size := get3(buf[4:]);
+	return ref Index(fd, attr, d.mtime, size, buf[8:]);
+}
+
+#Index.firstoff(ind: self ref Index, val: string): ref Attrdb->Dbptr
+#{
+#	o := hash(val, ind.size)*NDBPLEN;
+#	p := get3(tab[o:]);
+#	if(p == NDBNAP)
+#		return nil;
+#	if((p & NDBCHAIN) == 0)
+#		return ref Attrdb.Direct(p);
+#	p &= ~NDBCHAIN;
+#	return ref Attrdb.Hash(get3(tab[p:]), get3(tab[p+NDBPLEN:]));
+#}
+
+#Index.nextoff(ind: self ref Index, val: string, ptr: ref Attrdb->Dbptr): (int, ref Attrdb->Dbptr)
+#{
+#	pick p := ptr {
+#	Hash =>
+#		o := get3(tab[p.current:]);
+#		if((o & NDBCHAIN) == 0)
+#			return (o, ref Attrdb.Direct(p.next));
+#		o &= ~NDBCHAIN;
+#		o1 := get3(tab[o:]);
+#		o2 := get3(tab[o+NDBPLEN:]);
+#		
+
+#	o := hash(val, ind.size)*NDBPLEN;
+#	p := get3(tab[o:]);
+#	if(p == NDBNAP)
+#		return nil;
+#	for(; (p := get3(tab[o:])) != NDBNAP; o = p & ~NDBCHAIN)
+#		if((p & NDBCHAIN) == 0){
+#			put3(tab[o:], chain | NDBCHAIN);
+#			put3(tab[chain:], p);
+#			put3(tab[chain+NDBPLEN:], offset);
+#			return chain+2*NDBPLEN;
+#		}
+#	return nil;
+#}
+
+#
+# this must be the same hash function used by Plan 9's ndb
+#
+hash(s: string, hlen: int): int
+{
+	h := 0;
+	for(i := 0; i < len s; i++)
+		if(s[i] >= 16r80){
+			# could optimise by calculating utf ourselves
+			a := array of byte s;
+			for(i=0; i<len a; i++)
+				h = (h*13) + int a[i] - 'a';
+			break;
+		}else
+			h = (h*13) + s[i]-'a';
+	if(h < 0)
+		return int((big h & big 16rFFFFFFFF)%big hlen);
+	return h%hlen;
+}
+
+get3(a: array of byte): int
+{
+	return (int a[2]<<16) | (int a[1]<<8) | int a[0];
+}
+
+get4(a: array of byte): int
+{
+	return (int a[3]<<24) | (int a[2]<<16) | (int a[1]<<8) | int a[0];
+}
--- /dev/null
+++ b/appl/lib/auth.b
@@ -1,0 +1,326 @@
+# Inferno authentication protocol
+implement Auth;
+
+include "sys.m";
+	sys: Sys;
+
+include "keyring.m";
+
+include "security.m";
+	ssl: SSL;
+
+init(): string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	return nil;	
+}
+
+server(algs: list of string, ai: ref Keyring->Authinfo, fd: ref Sys->FD, setid: int): (ref Sys->FD, string)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	kr := load Keyring Keyring->PATH;
+	if(kr == nil)
+		return (nil, sys->sprint("%r"));
+
+	# mutual authentication
+	(id_or_err, secret) := kr->auth(fd, ai, setid); 
+
+	if(secret == nil){
+		if(ai == nil && id_or_err == "no authentication information")
+			id_or_err = "no server certificate";
+		return (nil, id_or_err);
+	}
+	if(0)
+		sys->fprint(sys->fildes(2), "secret is %s\n", dump(secret));
+
+	# have got a secret, get algorithm from client
+	# check if the client algorithm is in the server algorithm list
+	# client algorithm ::= ident (' ' ident)*
+	# where ident is defined by ssl(3)
+	algbuf := string kr->getmsg(fd);
+	if(algbuf == nil)
+		return (nil, sys->sprint("can't read client ssl algorithm: %r"));
+	alg := "";
+	(nil, calgs) := sys->tokenize(algbuf, " /");
+	for(; calgs != nil; calgs = tl calgs){
+		calg := hd calgs;
+		if(algs != nil){	# otherwise we suck it and see
+			for(sl := algs; sl != nil; sl = tl sl)
+				if(hd sl == calg)
+					break;
+			if(sl == nil)
+				return (nil, "unsupported client algorithm: " + calg);
+		}
+		alg += calg + " ";
+	}
+	if(alg != nil)
+		alg = alg[0:len alg - 1];
+
+	# don't push ssl if server supports nossl
+	if(alg == nil || alg == "none")
+		return (fd, id_or_err);
+
+	# push ssl and turn on algorithms
+	ssl = load SSL SSL->PATH;
+	if(ssl == nil)
+		return (nil, sys->sprint("can't load ssl: %r"));
+	(c, err) := pushssl(fd, secret, secret, alg);
+	if(c == nil)
+		return (nil, "push ssl: " + err);
+	return (c, id_or_err);
+}
+
+client(alg: string, ai: ref Keyring->Authinfo, fd: ref Sys->FD): (ref Sys->FD, string)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	kr := load Keyring Keyring->PATH;
+	if(kr == nil)
+		return (nil, sys->sprint("%r"));
+
+	if(alg == nil)
+		alg = "none";
+
+	# mutual authentication
+	(id_or_err, secret) := kr->auth(fd, ai, 0);
+	if(secret == nil)
+		return (nil, id_or_err);
+
+	# send algorithm
+	buf := array of byte alg;
+	if(kr->sendmsg(fd, buf, len buf) < 0)
+		return (nil, sys->sprint("can't send ssl algorithm: %r"));
+
+	# don't push ssl if server supports no ssl connection
+	if(alg == "none")
+		return (fd, id_or_err);
+
+	# push ssl and turn on algorithm
+	ssl = load SSL SSL->PATH;
+	if(ssl == nil)
+		return (nil, sys->sprint("can't load ssl: %r"));
+	(c, err) := pushssl(fd, secret, secret, alg);
+	if(c == nil)
+		return (nil, "push ssl: " + err);
+	return (c, id_or_err);
+}
+
+auth(ai: ref Keyring->Authinfo, keyspec: string, alg: string, fd: ref Sys->FD): (ref Sys->FD, ref Keyring->Authinfo, string)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	kr := load Keyring Keyring->PATH;
+	if(kr == nil)
+		return (nil, nil, sys->sprint("can't load %s: %r", Keyring->PATH));
+	if(alg == nil)
+		alg = "none";
+	if(ai == nil && keyspec != nil){
+		ai = key(keyspec);
+		if(ai == nil)
+			return (nil, nil, sys->sprint("can't obtain key: %r"));
+	}
+
+	# mutual authentication
+	(id_or_err, secret) := kr->auth(fd, ai, 0);
+	if(secret == nil)
+		return (nil, nil, id_or_err);
+
+	# send algorithm
+	buf := array of byte alg;
+	if(kr->sendmsg(fd, buf, len buf) < 0)
+		return (nil, nil, sys->sprint("can't send ssl algorithm: %r"));
+
+	if(0){		# TO DO
+		hisalg := string kr->getmsg(fd);
+		if(hisalg == nil)
+			return (nil, nil, sys->sprint("can't get remote algorithm: %r"));
+		# TO DO: compare the two, sort it out if they aren't equal
+	}
+
+	# don't push ssl if server supports no ssl connection
+	if(alg == "none")
+		return (fd, nil, id_or_err);
+
+	# push ssl and turn on algorithm
+	ssl = load SSL SSL->PATH;
+	if(ssl == nil)
+		return (nil, nil, sys->sprint("can't load ssl: %r"));
+	(c, err) := pushssl(fd, secret, secret, alg);
+	if(c == nil)
+		return (nil, nil, "push ssl: " + err);
+	return (c, nil, id_or_err);
+}
+
+dump(b: array of byte): string
+{
+	s := "";
+	for(i := 0; i < len b; i++)
+		s += sys->sprint("%.2ux", int b[i]);
+	return s;
+}
+
+# push an SSLv2 Record Layer onto the fd
+pushssl(fd: ref Sys->FD, secretin, secretout: array of byte, alg: string): (ref Sys->FD, string)
+{
+	(err, c) := ssl->connect(fd);
+	if(err != nil)
+		return (nil, "can't connect ssl: " + err);
+
+	err = ssl->secret(c, secretin, secretout);
+	if(err != nil)
+		return (nil, "can't write secret: " + err);
+
+	if(sys->fprint(c.cfd, "alg %s", alg) < 0)
+		return (nil, sys->sprint("can't push algorithm %s: %r", alg));
+
+	return (c.dfd, nil);
+}
+
+key(keyspec: string): ref Keyring->Authinfo
+{
+	f := keyfile(keyspec);
+	if(f == nil)
+		return nil;
+	kr := load Keyring Keyring->PATH;
+	if(kr == nil){
+		sys->werrstr(sys->sprint("can't load %s: %r", Keyring->PATH));
+		return nil;
+	}
+	return kr->readauthinfo(f);
+}
+
+#
+# look for key in old style keyring directory;
+# closest match to [net!]addr[!svc]
+#
+
+keyfile(keyspec: string): string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	al := parseattr(keyspec);
+	keyname := get(al, "key");
+	if(keyname != nil){
+		# explicit keyname overrides rest of spec
+		if(keyname[0] == '/' ||
+		   len keyname > 2 && keyname[0:2]=="./" ||
+		   len keyname > 3 && keyname[0:3]=="../")
+			return keyname;	# don't add directory
+		return keydir()+keyname;
+	}
+	net := "net";
+	svc := get(al, "service");
+	addr := get(al, "server");
+	(nf, flds) := sys->tokenize(addr, "!");	# compatibility
+	if(nf > 1){
+		net = hd flds;
+		addr = hd tl flds;
+	}
+	if(addr != nil)
+		keyname = addr;
+	else
+		keyname = "default";
+	kd := keydir();
+	dom := get(al, "dom");
+	if(dom != nil){
+		if((cert := exists(kd+dom)) != nil)
+			return cert;
+	}
+	if(keyname == "default")
+		return kd+"default";
+	if(net == "net")
+		l := "net!" :: "tcp!" :: nil;
+	else
+		l = net+"!" :: nil;
+	if(svc != nil){
+		for(nl := l; nl != nil; nl = tl nl){
+			cert := exists(kd+(hd nl)+keyname+"!"+svc);	# most specific
+			if(cert != nil)
+				return cert;
+		}
+	}
+	for(nl := l; nl != nil; nl = tl nl){
+		cert := exists(kd+(hd nl)+keyname);
+		if(cert != nil)
+			return cert;
+	}
+	cert := exists(kd+keyname);	# unadorned
+	if(cert != nil)
+		return cert;
+	if(keyname != "default"){
+		cert = exists(kd+"default");
+		if(cert != nil)
+			return cert;
+	}
+	return kd+keyname;
+}
+
+keydir(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	b := array[Sys->NAMEMAX] of byte;
+	nr := sys->read(fd, b, len b);
+	if(nr <= 0){
+		sys->werrstr("can't read /dev/user");
+		return nil;
+	}
+	user := string b[0:nr];
+	return "/usr/" + user + "/keyring/";
+}
+
+exists(f: string): string
+{
+	(ok, nil) := sys->stat(f);
+	if(0)sys->fprint(sys->fildes(2), "exists: %q %d\n", f, ok>=0);
+	if(ok >= 0)
+		return f;
+	return nil;
+}
+
+Aattr, Aval, Aquery: con iota;
+
+Attr: adt {
+	tag:	int;
+	name:	string;
+	val:	string;
+};
+
+parseattr(s: string): list of ref Attr
+{
+	(nil, fld) := sys->tokenize(s, " \t\n");	# should do quoting; later
+	rfld := fld;
+	for(fld = nil; rfld != nil; rfld = tl rfld)
+		fld = (hd rfld) :: fld;
+	attrs: list of ref Attr;
+	for(; fld != nil; fld = tl fld){
+		n := hd fld;
+		a := "";
+		tag := Aattr;
+		for(i:=0; i<len n; i++)
+			if(n[i] == '='){
+				a = n[i+1:];
+				n = n[0:i];
+				tag = Aval;
+			}
+		if(len n == 0)
+			continue;
+		if(tag == Aattr && len n > 1 && n[len n-1] == '?'){
+			tag = Aquery;
+			n = n[0:len n-1];
+		}
+		attrs = ref Attr(tag, n, a) :: attrs;
+	}
+	return attrs;
+}
+
+get(al: list of ref Attr, n: string): string
+{
+	for(; al != nil; al = tl al)
+		if((a := hd al).name == n && a.tag == Aval)
+			return a.val;
+	return nil;
+}
--- /dev/null
+++ b/appl/lib/auth9.b
@@ -1,0 +1,342 @@
+implement Auth9;
+
+#
+# elements of Plan 9 authentication
+#
+# this is a near transliteration of Plan 9 source, subject to the Lucent Public License 1.02
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "ipints.m";
+
+include "crypt.m";
+
+include "auth9.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+put2(a: array of byte, v: int)
+{
+	a[0] = byte v;
+	a[1] = byte (v>>8);
+}
+
+get2(a: array of byte): int
+{
+	return (int a[1]<<8) | int a[0];
+}
+
+put4(a: array of byte, v: int)
+{
+	a[0] = byte v;
+	a[1] = byte (v>>8);
+	a[2] = byte (v>>16);
+	a[3] = byte (v>>24);
+}
+
+get4(a: array of byte): int
+{
+	return (int a[3]<<24) | (int a[2]<<16) | (int a[1]<<8) | int a[0];
+}
+
+puts(a: array of byte, s: string, n: int)
+{
+	b := array of byte s;
+	l := len b;
+	if(l > n)
+		b = b[0:n];
+	a[0:] = b;
+	for(; l < n; l++)
+		a[l] = byte 0;
+}
+
+gets(a: array of byte, n: int): string
+{
+	for(i:=0; i<n; i++)
+		if(a[i] == byte 0)
+			break;
+	return string a[0:i];
+}
+
+geta(a: array of byte, n: int): array of byte
+{
+	b := array[n] of byte;
+	b[0:] = a[0:n];
+	return b;
+}
+
+Authenticator.pack(f: self ref Authenticator, key: array of byte): array of byte
+{
+	p := array[AUTHENTLEN] of {* => byte 0};
+	p[0] = byte f.num;
+	p[1:] = f.chal;
+	put4(p[1+CHALLEN:], f.id);
+	if(key != nil)
+		encrypt(key, p, len p);
+	return p;
+}
+
+Authenticator.unpack(a: array of byte, key: array of byte): (int, ref Authenticator)
+{
+	if(key != nil)
+		decrypt(key, a, AUTHENTLEN);
+	f := ref Authenticator;
+	f.num = int a[0];
+	f.chal = geta(a[1:], CHALLEN);
+	f.id = get4(a[1+CHALLEN:]);
+	return (AUTHENTLEN, f);
+}
+
+Passwordreq.pack(f: self ref Passwordreq, key: array of byte): array of byte
+{
+	a := array[PASSREQLEN] of {* => byte 0};
+	a[0] = byte f.num;
+	a[1:] = f.old;
+	a[1+ANAMELEN:] = f.new;
+	a[1+2*ANAMELEN] = byte f.changesecret;
+	a[1+2*ANAMELEN+1:] = f.secret;
+	if(key != nil)
+		encrypt(key, a, len a);
+	return a;
+}
+
+Passwordreq.unpack(a: array of byte, key: array of byte): (int, ref Passwordreq)
+{
+	if(key != nil)
+		decrypt(key, a, PASSREQLEN);
+	f := ref Passwordreq;
+	f.num = int a[0];
+	f.old = geta(a[1:], ANAMELEN);
+	f.old[ANAMELEN-1] = byte 0;
+	f.new = geta(a[1+ANAMELEN:], ANAMELEN);
+	f.new[ANAMELEN-1] = byte 0;
+	f.changesecret = int a[1+2*ANAMELEN];
+	f.secret = geta(a[1+2*ANAMELEN+1:], SECRETLEN);
+	f.secret[SECRETLEN-1] = byte 0;
+	return (PASSREQLEN, f);
+}
+
+Ticket.pack(f: self ref Ticket, key: array of byte): array of byte
+{
+	a := array[TICKETLEN] of {* => byte 0};
+	a[0] = byte f.num;
+	a[1:] = f.chal;
+	puts(a[1+CHALLEN:], f.cuid, ANAMELEN);
+	puts(a[1+CHALLEN+ANAMELEN:], f.suid, ANAMELEN);
+	a[1+CHALLEN+2*ANAMELEN:] = f.key;
+	if(key != nil)
+		encrypt(key, a, len a);
+	return a;
+}
+
+Ticket.unpack(a: array of byte, key: array of byte): (int, ref Ticket)
+{
+	if(key != nil)
+		decrypt(key, a, TICKETLEN);
+	f := ref Ticket;
+	f.num = int a[0];
+	f.chal = geta(a[1:], CHALLEN);
+	f.cuid = gets(a[1+CHALLEN:], ANAMELEN);
+	f.suid = gets(a[1+CHALLEN+ANAMELEN:], ANAMELEN);
+	f.key = geta(a[1+CHALLEN+2*ANAMELEN:], DESKEYLEN);
+	return (TICKETLEN, f);
+}
+
+Ticketreq.unpack(a: array of byte): (int, ref Ticketreq)
+{
+	f := ref Ticketreq;
+	f.rtype = int a[0];
+	f.authid = gets(a[1:], ANAMELEN);
+	f.authdom = gets(a[1+ANAMELEN:], DOMLEN);
+	f.chal = geta(a[1+ANAMELEN+DOMLEN:], CHALLEN);
+	f.hostid = gets(a[1+ANAMELEN+DOMLEN+CHALLEN:], ANAMELEN);
+	f.uid = gets(a[1+ANAMELEN+DOMLEN+CHALLEN+ANAMELEN:], ANAMELEN);
+	return (TICKREQLEN, f);
+}
+
+Ticketreq.pack(f: self ref Ticketreq): array of byte
+{
+	a := array[TICKREQLEN] of {* => byte 0};
+	a[0] = byte f.rtype;
+	puts(a[1:], f.authid, ANAMELEN);
+	puts(a[1+ANAMELEN:], f.authdom, DOMLEN);
+	a[1+ANAMELEN+DOMLEN:] = f.chal;
+	puts(a[1+ANAMELEN+DOMLEN+CHALLEN:], f.hostid, ANAMELEN);
+	puts(a[1+ANAMELEN+DOMLEN+CHALLEN+ANAMELEN:], f.uid, ANAMELEN);
+	return a;
+}
+
+netcrypt(key: array of byte, chal: string): string
+{
+	buf := array[8] of {* => byte 0};
+	a := array of byte chal;
+	if(len a > 7)
+		a = a[0:7];
+	buf[0:] = a;
+	encrypt(key, buf, len buf);
+	return sys->sprint("%.2ux%.2ux%.2ux%.2ux", int buf[0], int buf[1], int buf[2], int buf[3]);
+}
+
+passtokey(p: string): array of byte
+{
+	a := array of byte p;
+	n := len a;
+	if(n >= ANAMELEN)
+		n = ANAMELEN-1;
+	buf := array[ANAMELEN] of {* => byte ' '};
+	buf[0:] = a[0:n];
+	buf[n] = byte 0;
+	key := array[DESKEYLEN] of {* => byte 0};
+	t := 0;
+	for(;;){
+		for(i := 0; i < DESKEYLEN; i++)
+			key[i] = byte ((int buf[t+i] >> i) + (int buf[t+i+1] << (8 - (i+1))));
+		if(n <= 8)
+			return key;
+		n -= 8;
+		t += 8;
+		if(n < 8){
+			t -= 8 - n;
+			n = 8;
+		}
+		encrypt(key, buf[t:], 8);
+	}
+}
+
+parity := array[] of {
+	byte 16r01, byte 16r02, byte 16r04, byte 16r07, byte 16r08, byte 16r0b, byte 16r0d, byte 16r0e, 
+	byte 16r10, byte 16r13, byte 16r15, byte 16r16, byte 16r19, byte 16r1a, byte 16r1c, byte 16r1f, 
+	byte 16r20, byte 16r23, byte 16r25, byte 16r26, byte 16r29, byte 16r2a, byte 16r2c, byte 16r2f, 
+	byte 16r31, byte 16r32, byte 16r34, byte 16r37, byte 16r38, byte 16r3b, byte 16r3d, byte 16r3e, 
+	byte 16r40, byte 16r43, byte 16r45, byte 16r46, byte 16r49, byte 16r4a, byte 16r4c, byte 16r4f, 
+	byte 16r51, byte 16r52, byte 16r54, byte 16r57, byte 16r58, byte 16r5b, byte 16r5d, byte 16r5e, 
+	byte 16r61, byte 16r62, byte 16r64, byte 16r67, byte 16r68, byte 16r6b, byte 16r6d, byte 16r6e, 
+	byte 16r70, byte 16r73, byte 16r75, byte 16r76, byte 16r79, byte 16r7a, byte 16r7c, byte 16r7f, 
+	byte 16r80, byte 16r83, byte 16r85, byte 16r86, byte 16r89, byte 16r8a, byte 16r8c, byte 16r8f, 
+	byte 16r91, byte 16r92, byte 16r94, byte 16r97, byte 16r98, byte 16r9b, byte 16r9d, byte 16r9e, 
+	byte 16ra1, byte 16ra2, byte 16ra4, byte 16ra7, byte 16ra8, byte 16rab, byte 16rad, byte 16rae, 
+	byte 16rb0, byte 16rb3, byte 16rb5, byte 16rb6, byte 16rb9, byte 16rba, byte 16rbc, byte 16rbf, 
+	byte 16rc1, byte 16rc2, byte 16rc4, byte 16rc7, byte 16rc8, byte 16rcb, byte 16rcd, byte 16rce, 
+	byte 16rd0, byte 16rd3, byte 16rd5, byte 16rd6, byte 16rd9, byte 16rda, byte 16rdc, byte 16rdf, 
+	byte 16re0, byte 16re3, byte 16re5, byte 16re6, byte 16re9, byte 16rea, byte 16rec, byte 16ref, 
+	byte 16rf1, byte 16rf2, byte 16rf4, byte 16rf7, byte 16rf8, byte 16rfb, byte 16rfd, byte 16rfe,
+};
+
+des56to64(k56: array of byte): array of byte
+{
+	k64 := array[8] of byte;
+	hi := (int k56[0]<<24)|(int k56[1]<<16)|(int k56[2]<<8)|int k56[3];
+	lo := (int k56[4]<<24)|(int k56[5]<<16)|(int k56[6]<<8);
+
+	k64[0] = parity[(hi>>25)&16r7f];
+	k64[1] = parity[(hi>>18)&16r7f];
+	k64[2] = parity[(hi>>11)&16r7f];
+	k64[3] = parity[(hi>>4)&16r7f];
+	k64[4] = parity[((hi<<3)|int ((big lo & big 16rFFFFFFFF)>>29))&16r7f];	# watch the sign extension
+	k64[5] = parity[(lo>>22)&16r7f];
+	k64[6] = parity[(lo>>15)&16r7f];
+	k64[7] = parity[(lo>>8)&16r7f];
+	return k64;
+}
+
+encrypt(key: array of byte, data: array of byte, n: int)
+{
+	if(n < 8)
+		return;
+	crypt := load Crypt Crypt->PATH;
+	ds := crypt->dessetup(des56to64(key), nil);
+	n--;
+	r := n % 7;
+	n /= 7;
+	j := 0;
+	for(i := 0; i < n; i++){
+		crypt->desecb(ds, data[j:], 8, Crypt->Encrypt);
+		j += 7;
+	}
+	if(r)
+		crypt->desecb(ds, data[j-7+r:], 8, Crypt->Encrypt);
+}
+
+decrypt(key: array of byte, data: array of byte, n: int)
+{
+	if(n < 8)
+		return;
+	crypt := load Crypt Crypt->PATH;
+	ds := crypt->dessetup(des56to64(key), nil);
+	n--;
+	r := n % 7;
+	n /= 7;
+	j := n*7;
+	if(r)
+		crypt->desecb(ds, data[j-7+r:], 8, Crypt->Decrypt);
+	for(i := 0; i < n; i++){
+		j -= 7;
+		crypt->desecb(ds, data[j:], 8, Crypt->Decrypt);
+	}
+}
+
+readn(fd: ref Sys->FD, nb: int): array of byte
+{
+	buf:= array[nb] of byte;
+	if(sys->readn(fd, buf, nb) != nb)
+		return nil;
+	return buf;
+}
+
+pbmsg: con "AS protocol botch";
+
+_asgetticket(fd: ref Sys->FD, tr: ref Ticketreq, key: array of byte): (ref Ticket, array of byte)
+{
+	a := tr.pack();
+	if(sys->write(fd, a, len a) < 0){
+		sys->werrstr(pbmsg);
+		return (nil, nil);
+	}
+	a = _asrdresp(fd, 2*TICKETLEN);
+	if(a == nil)
+		return (nil, nil);
+	(nil, t) := Ticket.unpack(a, key);
+	return (t, a[TICKETLEN:]);	# can't unpack both since the second uses server key
+}
+
+_asrdresp(fd: ref Sys->FD, n: int): array of byte
+{
+	b := array[1] of byte;
+	if(sys->read(fd, b, 1) != 1){
+		sys->werrstr(pbmsg);
+		return nil;
+	}
+
+	buf: array of byte;
+	case int b[0] {
+	AuthOK =>
+		buf = readn(fd, n);
+	AuthOKvar =>
+		b = readn(fd, 5);
+		if(b == nil)
+			break;
+		n = int string b;
+		if(n<= 0 || n > 4096)
+			break;
+		buf = readn(fd, n);
+	AuthErr =>
+		b = readn(fd, 64);
+		if(b == nil)
+			break;
+		for(i:=0; i<len b && b[i] != byte 0; i++)
+			;
+		sys->werrstr(sys->sprint("remote: %s", string b[0:i]));
+		return nil;
+	* =>
+		sys->werrstr(sys->sprint("%s: resp %d", pbmsg, int b[0]));
+		return nil;
+	}
+	if(buf == nil)
+		sys->werrstr(pbmsg);
+	return buf;
+}
--- /dev/null
+++ b/appl/lib/bloomfilter.b
@@ -1,0 +1,72 @@
+implement Bloomfilter;
+
+include "sys.m";
+	sys: Sys;
+include "sets.m";
+	sets: Sets;
+	Set: import sets;
+include "keyring.m";
+	keyring: Keyring;
+include "bloomfilter.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	sets = load Sets Sets->PATH;
+	sets->init();
+	keyring = load Keyring Keyring->PATH;
+}
+
+Bits: adt {
+	d: array of byte;
+	n: int;			# bits used
+	get:	fn(bits: self ref Bits, n: int): int;
+};
+
+filter(d: array of byte, logm, k: int): Set
+{
+	if(logm < 3 || logm > 30)
+		raise "invalid bloom filter size";
+	nb := 1 << logm;
+	f := array[nb / 8 + 1] of {* => byte 0};	# one extra zero to make sure set's not inverted.
+	bits := hashbits(d, logm * k);
+	while(k--){
+		v := bits.get(logm);
+		f[v >> 3] |= byte 1 << (v & 7);
+	}
+	return sets->bytes2set(f);
+}
+
+hashbits(data: array of byte, n: int): ref Bits
+{
+	d := array[((n + 7) / 8)] of byte;
+	digest := array[Keyring->SHA1dlen] of byte;
+	state := keyring->sha1(data, len data, nil, nil);
+	extra := array[2] of byte;
+	e := 0;
+	for(i := 0; i < len d; i += Keyring->SHA1dlen){
+		extra[0] = byte e;
+		extra[1] = byte (e>>8);
+		e++;
+		state = keyring->sha1(extra, len extra, digest, state);
+		if(i + Keyring->SHA1dlen > len d)
+			digest = digest[0:len d - i];
+		d[i:] = digest;
+	}
+	return ref Bits(d, 0);
+}
+
+# XXX could be more efficient.
+Bits.get(bits: self ref Bits, n: int): int
+{
+	d := bits.d;
+	v := 0;
+	nb := bits.n;
+	for(i := 0; i < n; i++){
+		j := nb + i;
+		if(int d[j >> 3] & (1 << (j & 7)))
+			v |= (1 << i);
+	}
+	bits.n += n;
+	return v;
+}
--- /dev/null
+++ b/appl/lib/bufio.b
@@ -1,0 +1,534 @@
+implement Bufio;
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+
+UTFself:	con 16r80;	# ascii and UTF sequences are the same (<)
+Maxrune:	con 8;	# could probably be Sys->UTFmax
+Bufsize:	con Sys->ATOMICIO;
+
+Filler: adt
+{
+	iobuf:	ref Iobuf;
+	fill:	BufioFill;
+	next:	cyclic ref Filler;
+};
+
+fillers:	ref Filler;
+
+create(filename: string, mode, perm: int): ref Iobuf
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+	if ((fd := sys->create(filename, mode, perm)) == nil)
+		return nil;
+	return ref Iobuf(fd, array[Bufsize+Maxrune] of byte, 0, 0, 0, big 0, big 0, mode, mode);
+}
+
+open(filename: string, mode: int): ref Iobuf
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+	if ((fd := sys->open(filename, mode)) == nil)
+		return nil;
+	return ref Iobuf(fd, array[Bufsize+Maxrune] of byte, 0, 0, 0, big 0, big 0, mode, mode);
+}
+
+fopen(fd: ref Sys->FD, mode: int): ref Iobuf
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+	if ((filpos := sys->seek(fd, big 0, 1)) < big 0)
+		filpos = big 0;
+	return ref Iobuf(fd, array[Bufsize+Maxrune] of byte, 0, 0, 0, filpos, filpos, mode, mode);
+}
+
+sopen(input: string): ref Iobuf
+{
+	return aopen(array of byte input);
+}
+
+aopen(b: array of byte): ref Iobuf
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+	return ref Iobuf(nil, b, 0, len b, 0, big 0, big 0, OREAD, OREAD);
+}
+
+readchunk(b: ref Iobuf): int
+{
+	if (b.fd == nil){
+		if ((f := filler(b)) != nil){
+			if ((n := f.fill->fill(b)) == EOF)
+				nofill(b);
+			return n;
+		}
+		return EOF;
+	}
+	if (b.filpos != b.bufpos + big b.size) {
+		s := b.bufpos + big b.size;
+		if (sys->seek(b.fd, s, 0) != s)
+			return ERROR;
+		b.filpos = s;
+	}
+	i := len b.buffer - b.size - 1;
+	if(i > Bufsize)
+		i = Bufsize;
+	if ((i = sys->read(b.fd, b.buffer[b.size:], i)) <= 0) {
+		if(i < 0)
+			return ERROR;
+		return EOF;
+	}
+	b.size += i;
+	b.filpos += big i;
+	return i;
+}
+
+writechunk(b: ref Iobuf): int
+{
+	err := (b.fd == nil);
+	if (b.filpos != b.bufpos) {
+		if (sys->seek(b.fd, b.bufpos, 0) != b.bufpos)
+			err = 1;
+		b.filpos = b.bufpos;
+	}
+	if ((size := b.size) > Bufsize)
+		size = Bufsize;
+	if (sys->write(b.fd, b.buffer, size) != size)
+		err = 1;
+	b.filpos += big size;
+	b.size -= size;
+	if (b.size) {
+		b.dirty = 1;
+		b.buffer[0:] = b.buffer[Bufsize:Bufsize+b.size];
+	} else
+		b.dirty = 0;
+	b.bufpos += big size;
+	b.index -= size;
+	if(err)
+		return ERROR;
+	return size;
+}
+
+Iobuf.close(b: self ref Iobuf)
+{
+	if (b.fd == nil) {
+		nofill(b);
+		return;
+	}
+	if (b.dirty)
+		b.flush();
+	b.fd = nil;
+	b.buffer = nil;
+}
+
+Iobuf.flush(b: self ref Iobuf): int
+{
+	if (b.fd == nil)
+		return ERROR;
+	if (b.lastop == OREAD){
+		b.bufpos = b.filpos;
+		b.size = 0;
+		return 0;
+	}
+	while (b.dirty) {
+		if (writechunk(b) < 0)
+			return ERROR;
+		if (b.index < 0) {
+			b.bufpos += big b.index;
+			b.index = 0;
+		}
+	}
+	return 0;
+}
+
+Iobuf.seek(b: self ref Iobuf, off: big, start: int): big
+{
+	npos: big;
+
+	if (b.fd == nil){
+		if(filler(b) != nil)
+			return big ERROR;
+	}
+	case (start) {
+	0 =>	# absolute address
+		npos = off;
+	1 =>	# offset from current location
+		npos = b.bufpos + big b.index + off;
+		off = npos;
+		start = Sys->SEEKSTART;
+	2 =>	# offset from EOF
+		npos = big -1;
+	* =>	return big ERROR;
+	}
+	if (b.bufpos <= npos && npos < b.bufpos + big b.size) {
+		b.index = int(npos - b.bufpos);
+		return npos;
+	}
+	if (b.fd == nil || b.dirty && b.flush() < 0)
+		return big ERROR;
+	b.size = 0;
+	b.index = 0;
+	if ((s := sys->seek(b.fd, off, start)) < big 0) {
+		b.bufpos = b.filpos;
+		return big ERROR;
+	}
+	b.bufpos = b.filpos = s;
+	return s;
+}
+
+Iobuf.offset(b: self ref Iobuf): big
+{
+	return b.bufpos + big b.index;
+}
+
+write2read(b: ref Iobuf): int
+{
+	while (b.dirty) 
+		if (b.flush() < 0)
+			return ERROR;
+	b.bufpos = b.filpos;
+	b.size = 0;
+	b.lastop = OREAD;
+	if ((r := readchunk(b)) < 0)
+		return r;
+	if (b.index > b.size)
+		return EOF;
+	return 0;
+}
+
+Iobuf.read(b: self ref Iobuf, buf: array of byte, n: int): int
+{
+	if (b.mode == OWRITE)
+		return ERROR;
+	if (b.lastop != OREAD){
+		if ((r := write2read(b)) < 0)
+			return r;
+	}
+	k := n;
+	while (b.size - b.index < k) {
+		buf[0:] = b.buffer[b.index:b.size];
+		buf = buf[b.size - b.index:];
+		k -= b.size - b.index;
+
+		b.bufpos += big b.size;
+		b.index = 0;
+		b.size = 0;
+		if ((r := readchunk(b)) < 0) {
+			if(r == EOF || n != k)
+				return n-k;
+			return ERROR;
+		}
+	}
+	buf[0:] = b.buffer[b.index:b.index+k];
+	b.index += k;
+	return n;
+}
+
+Iobuf.getb(b: self ref Iobuf): int
+{
+	if(b.lastop != OREAD){
+		if(b.mode == OWRITE)
+			return ERROR;
+		if((r := write2read(b)) < 0)
+			return r;
+	}
+	if (b.index == b.size) {
+		b.bufpos += big b.index;
+		b.index = 0;
+		b.size = 0;
+		if ((r := readchunk(b)) < 0)
+			return r;
+	}
+	return int b.buffer[b.index++];
+}
+
+Iobuf.ungetb(b: self ref Iobuf): int
+{
+	if(b.mode == OWRITE || b.lastop != OREAD)
+		return ERROR;
+	b.index--;
+	return 1;
+}
+
+Iobuf.getc(b: self ref Iobuf): int
+{
+	r, i, s:	int;
+
+	if(b.lastop != OREAD){
+		if(b.mode == OWRITE)
+			return ERROR;
+		if((r = write2read(b)) < 0)
+			return r;
+	}
+	for(;;) {
+		if(b.index < b.size) {
+			r = int b.buffer[b.index];
+			if(r < UTFself){
+				b.index++;
+				return r;
+			}
+			(r, i, s) = sys->byte2char(b.buffer[0:b.size], b.index);
+			if (i != 0) {
+				b.index += i;
+				return r;
+			}
+			b.buffer[0:] = b.buffer[b.index:b.size];
+		}
+		b.bufpos += big b.index;
+		b.size -= b.index;
+		b.index = 0;
+		if ((r = readchunk(b)) < 0)
+			return r;
+	}
+	# Not reached:
+	return -1;
+}
+
+Iobuf.ungetc(b: self ref Iobuf): int
+{
+	if(b.index == 0 || b.mode == OWRITE || b.lastop != OREAD)
+		return ERROR;
+	stop := b.index - Sys->UTFmax;
+	if(stop < 0)
+		stop = 0;
+	buf := b.buffer[0:b.size];
+	for(i := b.index-1; i >= stop; i--){
+		(nil, n, s) := sys->byte2char(buf, i);
+		if(s && i + n == b.index){
+			b.index = i;
+			return 1;
+		}
+	}
+	b.index--;
+
+	return 1;
+}
+
+# optimised when term < UTFself (common case)
+tgets(b: ref Iobuf, t: int): string
+{
+	str: string;
+	term := byte t;
+	for(;;){
+		start := b.index;
+		end := start + sys->utfbytes(b.buffer[start:], b.size-start);
+		buf := b.buffer;
+		# XXX could speed up by adding extra byte to end of buffer and
+		# placing a sentinel there (eliminate one test, perhaps 35% speedup).
+		# (but not when we've been given the buffer externally)
+		for(i := start; i < end; i++){
+			if(buf[i] == term){
+				i++;
+				str += string buf[start:i];
+				b.index = i;
+				return str;
+			}
+		}
+		str += string buf[start:i];
+		if(i < b.size)
+			b.buffer[0:] = buf[i:b.size];
+		b.size -= i;
+		b.bufpos += big i;
+		b.index = 0;
+		if(readchunk(b) < 0)
+			break;
+	}
+	return str;
+}
+		
+Iobuf.gets(b: self ref Iobuf, term: int): string
+{
+	i: int;
+
+	if(b.mode == OWRITE)
+		return nil;
+	if(b.lastop != OREAD && write2read(b) < 0)
+		return nil;
+#	if(term < UTFself)
+#		return tgets(b, term);
+	str: string;
+	ch := -1;
+	for (;;) {
+		start := b.index;
+		n := 0;
+		while(b.index < b.size){
+			(ch, i, nil) = sys->byte2char(b.buffer[0:b.size], b.index);
+			if(i == 0)	# too few bytes for full Rune
+				break;
+			n += i;
+			b.index += i;
+			if(ch == term)
+				break;
+		}
+		if(n > 0)
+			str += string b.buffer[start:start+n];
+		if(ch == term)
+			return str;
+		b.buffer[0:] = b.buffer[b.index:b.size];
+		b.bufpos += big b.index;
+		b.size -= b.index;
+		b.index = 0;
+		if (readchunk(b) < 0)
+			break;
+	}
+	return str;	# nil at EOF
+}
+
+Iobuf.gett(b: self ref Iobuf, s: string): string
+{
+	r := "";
+	if (b.mode == OWRITE || (ch := b.getc()) < 0)
+		return nil;
+	do {
+		r[len r] = ch;
+		for (i:=0; i<len(s); i++)
+			if (ch == s[i])
+				return r;
+	} while ((ch = b.getc()) >= 0);
+	return r;
+}
+
+read2write(b: ref Iobuf)
+{
+	# last operation was a read
+	b.bufpos += big b.index;
+	b.size = 0;
+	b.index = 0;
+	b.lastop = OWRITE;
+}
+
+Iobuf.write(b: self ref Iobuf, buf: array of byte, n: int): int
+{
+	if(b.lastop != OWRITE) {
+		if(b.mode == OREAD)
+			return ERROR;
+		read2write(b);
+	}
+	start := 0;
+	k := n;
+	while(k > 0){
+		nw := Bufsize - b.index;
+		if(nw > k)
+			nw = k;
+		end := start + nw;
+		b.buffer[b.index:] = buf[start:end];
+		start = end;
+		b.index += nw;
+		k -= nw;
+		if(b.index > b.size)
+			b.size = b.index;
+		b.dirty = 1;
+		if(b.size == Bufsize && writechunk(b) < 0)
+			return ERROR;
+	}
+	return n;
+}
+
+Iobuf.putb(b: self ref Iobuf, c: byte): int
+{
+	if(b.lastop != OWRITE) {
+		if(b.mode == OREAD)
+			return ERROR;
+		read2write(b);
+	}
+	b.buffer[b.index++] = c;
+	if(b.index > b.size)
+		b.size = b.index;
+	b.dirty = 1;
+	if(b.size >= Bufsize) {
+		if (b.fd == nil)
+			return ERROR;
+		if (writechunk(b) < 0)
+			return ERROR;
+	}
+	return 0;
+}
+
+Iobuf.putc(b: self ref Iobuf, c: int): int
+{
+	if(b.lastop != OWRITE) {
+		if (b.mode == OREAD)
+			return ERROR;
+		read2write(b);
+	}
+	if(c < UTFself)
+		b.buffer[b.index++] = byte c;
+	else
+		b.index += sys->char2byte(c, b.buffer, b.index);
+	if (b.index > b.size)
+		b.size = b.index;
+	b.dirty = 1;
+	if (b.size >= Bufsize) {
+		if (writechunk(b) < 0)
+			return ERROR;
+	}
+	return 0;
+}
+
+Iobuf.puts(b: self ref Iobuf, s: string): int
+{
+	if(b.lastop != OWRITE) {
+		if (b.mode == OREAD)
+			return ERROR;
+		read2write(b);
+	}
+	n := len s;
+	if (n == 0)
+		return 0;
+	ind := b.index;
+	buf := b.buffer;
+	for(i := 0; i < n; i++){
+		c := s[i];
+		if(c < UTFself)
+			buf[ind++] = byte c;
+		else
+			ind += sys->char2byte(c, buf, ind);
+		if(ind >= Bufsize){
+			b.index = ind;
+			if(ind > b.size)
+				b.size = ind;
+			b.dirty = 1;
+			if(writechunk(b) < 0)
+				return ERROR;
+			ind = b.index;
+		}
+	}
+	b.dirty = b.index != ind;
+	b.index = ind;
+	if (ind > b.size)
+		b.size = ind;
+	return n;
+}
+
+filler(b: ref Iobuf): ref Filler
+{
+	for (f := fillers; f != nil; f = f.next)
+		if(f.iobuf == b)
+			return f;
+	return nil;
+}
+
+Iobuf.setfill(b: self ref Iobuf, fill: BufioFill)
+{
+	if ((f := filler(b)) != nil)
+		f.fill = fill;
+	else
+		fillers = ref Filler(b, fill, fillers);
+}
+
+nofill(b: ref Iobuf)
+{
+	prev: ref Filler;
+	for(f := fillers; f != nil; f = f.next) {
+		if(f.iobuf == b) {
+			if (prev == nil)
+				fillers = f.next;
+			else
+				prev.next = f.next;
+		}
+		prev = f;
+	}
+}
--- /dev/null
+++ b/appl/lib/cfg.b
@@ -1,0 +1,234 @@
+implement Cfg;
+
+include "sys.m";
+include "bufio.m";
+include "cfg.m";
+
+sys : Sys;
+bufio : Bufio;
+
+Iobuf : import bufio;
+ENOMOD : con "cannot load module";
+EBADPATH : con "bad path";
+
+
+init(path : string) : string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		return sys->sprint("%s: %r", ENOMOD);
+
+	iob := bufio->open(path, Sys->OREAD);
+	if (iob == nil)
+		return sys->sprint("%s: %r", EBADPATH);
+
+	# parse the config file
+	r : list of ref Tuple;
+	lnum := 0;
+	rlist : list of list of ref Tuple;
+
+	while ((line := iob.gets('\n')) != nil) {
+		lnum++;
+		(tuple, err) := parseline(line, lnum);
+		if (err != nil)
+			return sys->sprint("%s:%d: %s", path, lnum, err);
+
+		if (tuple == nil)
+			continue;
+		if (line[0] != ' ' && line[0] != '\t') {
+			# start of a new record
+			if (r != nil)
+				rlist = r :: rlist;
+			r = nil;
+		}
+		r = tuple :: r;
+	}
+	if (r != nil)
+		rlist = r :: rlist;
+	for (; rlist != nil; rlist = tl rlist)
+		insert(hd rlist);
+	return nil;
+}
+
+parseline(s : string, lnum : int) : (ref Tuple, string)
+{
+	attrs : list of Attr;
+	quote := 0;
+	word := "";
+	lastword := "";
+	name := "";
+
+loop:
+	for (i := 0 ; i < len s; i++) {
+		if (quote) {
+			if (s[i] == quote) {
+				if (i + 1 < len s && s[i+1] == quote) {
+					word[len word] = quote;
+					i++;
+				} else {
+					quote = 0;
+					continue;
+				}
+			} else
+				word[len word] = s[i];
+			continue;
+		}
+		case s[i] {
+		'\'' or '\"' =>
+			quote = s[i];
+			continue;
+		'#' =>
+			break loop;
+		' ' or '\t' or '\n' or '\r' =>
+			if (word == nil)
+				continue;
+			if (lastword != nil) {
+				# lastword space word space
+				attrs = Attr(lastword, nil) :: attrs;
+			}
+			lastword = word;
+			word = nil;
+
+			if (name != nil) {
+				# name = lastword space
+				attrs = Attr(name, lastword) :: attrs;
+				name = lastword = nil;
+			}
+		'=' =>
+			if (lastword == nil) {
+				# word=
+				lastword = word;
+				word = nil;
+			}
+			if (word != nil) {
+				# lastword word=
+				attrs = Attr(lastword, nil) :: attrs;
+				lastword = word;
+				word = nil;
+			}
+			if (lastword == nil)
+				return (nil, "empty name");
+			name = lastword;
+			lastword = nil;
+		* =>
+			word[len word] = s[i];
+		}
+	}
+	if (quote)
+		return (nil, "missing quote");
+
+	if (lastword == nil) {
+		lastword = word;
+		word = nil;
+	}
+
+	if (name == nil) {
+		name = lastword;
+		lastword = nil;
+	}
+
+	if (name != nil)
+		attrs = Attr(name, lastword) :: attrs;
+
+	if (attrs == nil)
+		return (nil, nil);
+
+	fattrs : list of Attr;
+	for (; attrs != nil; attrs = tl attrs)
+		fattrs = hd attrs :: fattrs;
+	return (ref Tuple(lnum, fattrs), nil);
+}
+
+lookup(name : string) : list of (string, ref Record)
+{
+	l := buckets[hash(name)];
+	for (; l != nil; l = tl l) {
+		hr := hd l;
+		if (hr.name != name)
+			continue;
+		return hr.vrecs;
+	}
+	return nil;
+}
+
+Record.lookup(r : self ref Record, name : string) : (string, ref Tuple)
+{
+	for (ts := r.tuples; ts != nil; ts = tl ts) {
+		t := hd ts;
+		for (as := t.attrs; as != nil; as = tl as) {
+			a := hd as;
+			if (a.name == name)
+				return (a.value, t);
+		}
+	}
+	return (nil, nil);
+}
+
+Tuple.lookup(t : self ref Tuple, name : string) : string
+{
+	for (as := t.attrs; as != nil; as = tl as) {
+		a := hd as;
+		if (a.name == name)
+			return a.value;
+	}
+	return nil;
+}
+
+reset()
+{
+	keynames = nil;
+	buckets = array[HSIZE+1] of list of ref HRecord;
+}
+
+# Record hash table
+HRecord : adt {
+	name : string;
+	vrecs : list of (string, ref Record);
+};
+
+keynames : list of string;
+
+HSIZE : con 16rff;	# must be (2^n)-1 due to hash() defn
+buckets := array [HSIZE+1] of list of ref HRecord;
+
+hash(name : string) : int
+{
+	# maybe use hashPJW?
+	h := 0;
+	for (i := 0; i < len name; i++)
+		h = (h + name[i]) & HSIZE;
+	return h;
+}
+
+insert(rtups : list of ref Tuple) {
+	# tuple list is currently in reverse order
+	ftups : list of ref Tuple;
+	for (; rtups != nil; rtups = tl rtups)
+		ftups = hd rtups :: ftups;
+	
+	maintuple := hd ftups;
+	mainattr := hd maintuple.attrs;
+	name := mainattr.name;
+	value := mainattr.value;
+
+	# does name already exist?
+	hr : ref HRecord;
+	h := hash(name);
+	l := buckets[h];
+	for (; l != nil; l = tl l) {
+		hr = hd l;
+		if (hr.name == name)
+			break;
+	}
+	if (l == nil) {
+		keynames = name :: keynames;
+		buckets[h] = ref HRecord(name, (value, ref Record(ftups))::nil) :: buckets[h];
+	} else
+		hr.vrecs = (value, ref Record(ftups)) :: hr.vrecs;
+}
+
+getkeys() : list of string
+{
+	return keynames;
+}
--- /dev/null
+++ b/appl/lib/cfgfile.b
@@ -1,0 +1,152 @@
+implement CfgFile;
+
+#
+# Copyright © 1996-1999 Lucent Technologies Inc.  All rights reserved.
+# Revisions Copyright © 2000 Vita Nuova Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+	Dir: import sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "cfgfile.m";
+
+# Detect/Copy/Create
+verify(default: string, path: string): ref Sys->FD
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	fd := sys->open(path, Sys->ORDWR);
+	if(fd == nil) {
+		fd = sys->create(path, Sys->ORDWR, 8r666);
+		if(fd == nil)
+			return nil;
+		# copy default configuration file (if present)
+		ifd := sys->open(default, Sys->OREAD);
+		if(ifd != nil){
+			buf := array[Sys->ATOMICIO] of byte;
+			while((n := sys->read(ifd, buf, len buf)) > 0)
+				if(sys->write(fd, buf, n) != n)
+					return nil;
+			ifd = nil;
+		}
+	}
+	return fd;
+}
+
+init(file: string): ref ConfigFile
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+
+	if(sys == nil || bufio == nil)
+		return nil;
+
+	me := ref ConfigFile;
+	me.file = file;
+	me.readonly = 0;
+	f := bufio->open(file, Sys->ORDWR);
+	if(f == nil) {
+		f = bufio->open(file, Sys->OREAD);
+		if (f == nil)
+			return nil;
+		me.readonly = 1;
+	}
+
+	while((l := f.gett("\r\n")) != nil) {
+		if(l[(len l)-1] == '\n')
+			l = l[0:(len l)-1];
+		me.lines = l :: me.lines;
+	}
+
+	return me;
+}
+
+ConfigFile.flush(me: self ref ConfigFile): string
+{
+	if(me.readonly)
+		return "file is read only";
+	if((fd := sys->create(me.file,Sys->OWRITE,0644)) == nil)
+		return sys->sprint("%r");
+	if((f := bufio->fopen(fd,Sys->OWRITE)) == nil)
+		return sys->sprint("%r");
+
+	l := me.lines;
+	while(l != nil) {
+		f.puts(hd l+"\n");
+		l = tl l;
+	}
+	if(f.flush() == -1)
+		return sys->sprint("%r");
+
+	return nil;
+}
+
+ConfigFile.getcfg(me:self ref ConfigFile, field:string): list of string
+{
+	l := me.lines;
+
+	while(l != nil) {
+		(n,fields) := sys->tokenize(hd l," \t");
+		if(n >= 1 && field == hd fields)
+			return tl fields;
+		l = tl l;
+	}
+	return nil;
+}
+
+ConfigFile.setcfg(me:self ref ConfigFile, field:string, val:string)
+{
+	l := me.lines;
+	newlist: list of string;
+
+	if(me.readonly)
+		return; # should return "file is read only";
+
+	matched := 0;
+
+	while(l != nil) {
+		(n,fields) := sys->tokenize(hd l," \t");
+		if(!matched && n >= 1 && field == hd fields) {
+			newlist = field+"\t"+val::newlist;
+			matched = 1;
+		}
+		else
+			newlist = hd l::newlist;
+		l = tl l;
+	}
+	if(!matched)
+		newlist = field+"\t"+val::newlist;
+
+	me.lines = newlist;
+}
+
+ConfigFile.delete(me:self ref ConfigFile, field:string)
+{
+	l := me.lines;
+	newlist: list of string;
+
+	if(me.readonly)
+		return; # should return "file is read only";
+
+	matched := 0;
+
+	while(l != nil) {
+		(n,fields) := sys->tokenize(hd l," \t");
+		if(!matched && n >= 1 && field == hd fields) {
+			matched = 1;
+		}
+		else
+			newlist = hd l::newlist;
+		l = tl l;
+	}
+
+	me.lines = newlist;
+}
--- /dev/null
+++ b/appl/lib/chanfill.b
@@ -1,0 +1,52 @@
+implement ChanFill;
+
+#
+#	Iobuf fill routine to serve a file2chan.
+#
+
+include	"sys.m";
+include	"bufio.m";
+
+myfill:	BufioFill;
+bufio:	Bufio;
+fid:	int;
+wc:	Sys->Rwrite;
+fio:	ref Sys->FileIO;
+
+Iobuf:	import bufio;
+
+init(data: array of byte, f: int, c: Sys->Rwrite, r: ref Sys->FileIO, b: Bufio): ref Iobuf
+{
+	if (myfill == nil)
+		myfill = load BufioFill SELF;
+	bufio = b;
+	i := bufio->sopen(string data);
+	fid = f;
+	wc = c;
+	fio = r;
+	i.setfill(myfill);
+	wc <-= (len data, nil);
+	return i;
+}
+
+fill(b: ref Iobuf): int
+{
+	for (;;) {
+		(nil, data, f, c) := <-fio.write;
+		if (f != fid) {
+			if (c != nil)
+				c <-= (0, "file busy");
+			continue;
+		}
+		if (c == nil)
+			return Bufio->EOF;
+		c <-= (len data, nil);
+		i := len data;
+		if (i == 0)
+			continue;
+		b.buffer[b.size:] = data;
+		b.size += i;
+		b.filpos += big i;
+		return i;
+	}
+}
--- /dev/null
+++ b/appl/lib/complete.b
@@ -1,0 +1,92 @@
+implement Complete;
+
+# Limbo translation by caerwyn of libcomplete on Plan 9
+# Subject to the Lucent Public License 1.02
+
+include "sys.m";
+	sys: Sys;
+
+include "string.m";
+	str: String;
+
+include "complete.m";
+
+include "readdir.m";
+	readdir: Readdir;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	readdir = load Readdir Readdir->PATH;
+}
+
+
+longestprefixlength(a, b: string, n: int): int
+{
+	for(i := 0; i < n; i++)
+		if(a[i] != b[i])
+			break;
+	return i;
+}
+
+complete(dir, s: string): (ref Completion, string)
+{
+	if(str->splitl(s, "/").t1 != nil)
+		return (nil, "slash character in name argument to complete()");
+
+	(da, n) := readdir->init(dir, Readdir->COMPACT);
+	if(n < 0)
+		return (nil, sys->sprint("%r"));
+	if(n == 0)
+		return (nil, nil);
+
+
+	c := ref Completion(0, 0, nil, 0, nil);
+
+	name := array[n] of string;
+	mode := array[n] of int;
+	length := len s;
+	nfile := 0;
+	minlen := 1000000;
+	for(i := 0; i < n; i++)
+		if(str->prefix(s,da[i].name)){
+			name[nfile] = da[i].name;
+			mode[nfile] = da[i].mode;
+			if(minlen > len da[i].name)
+				minlen = len da[i].name;
+			nfile++;
+		}
+
+	if(nfile > 0){
+		# report interesting results
+		# trim length back to longest common initial string
+		for(i = 1; i < nfile; i++)
+			minlen = longestprefixlength(name[0], name[i], minlen);
+
+		c.complete = (nfile == 1);
+		c.advance = c.complete || (minlen > length);
+		c.str = name[0][length:minlen];
+		if(c.complete){
+			if(mode[0]&Sys->DMDIR)
+				c.str[minlen++ - length] = '/';
+			else
+				c.str[minlen++ - length] = ' ';
+		}
+		c.nmatch = nfile;
+	}else{
+		# no match: return all names
+		for(i = 0; i < n; i++){
+			name[i] = da[i].name;
+			mode[i] = da[i].mode;
+		}
+		nfile = n;
+		c.nmatch = 0;
+	}
+	c.filename = name;
+	for(i = 0; i < nfile; i++)
+		if(mode[i] & Sys->DMDIR)
+			c.filename[i] += "/";
+
+	return (c, nil);
+}
--- /dev/null
+++ b/appl/lib/convcs/8bit_stob.b
@@ -1,0 +1,24 @@
+implement Stob;
+
+include "sys.m";
+include "convcs.m";
+
+sys : Sys;
+
+init(nil : string) : string
+{
+	sys = load Sys Sys->PATH;
+	return nil;
+}
+
+stob(nil : Convcs->State, str : string) : (Convcs->State, array of byte)
+{
+	b := array [len str] of byte;
+	for (i := 0; i < len str; i++) {
+		ch := str[i];
+		if (ch > 255)
+			ch = '?';
+		b[i] = byte ch;
+	}
+	return (nil, b);
+}
--- /dev/null
+++ b/appl/lib/convcs/big5_btos.b
@@ -1,0 +1,87 @@
+implement Btos;
+
+include "sys.m";
+include "convcs.m";
+
+# Big5 consists of 89 fonts of 157 chars each
+BIG5MAX : con 13973;
+BIG5FONT : con 157;
+
+BIG5DATA : con "/lib/convcs/big5";
+
+MAXINT : con 16r7fffffff;
+BADCHAR : con 16rFFFD;
+
+big5map := "";
+
+init(nil : string) : string
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->open(BIG5DATA, Sys->OREAD);
+	if (fd == nil)
+		return sys->sprint("%s: %r", BIG5DATA);
+
+	buf := array[BIG5MAX * Sys->UTFmax] of byte;
+	nread := 0;
+	for (;nread < len buf;) {
+		n := sys->read(fd, buf[nread:], Sys->ATOMICIO);
+		if (n <= 0)
+			break;
+		nread += n;
+	}
+	big5map = string buf[:nread];
+	if (len big5map != BIG5MAX) {
+		big5map = nil;
+		return sys->sprint("%s: corrupt data", BIG5DATA);
+	}
+	return nil;
+}
+
+btos(nil : Convcs->State, b : array of byte, n : int) : (Convcs->State, string, int)
+{
+	nbytes := 0;
+	str := "";
+
+	if (n == -1)
+		n = MAXINT;
+
+	font := -1;
+	for (i := 0; i < len b && len str < n; i++) {
+		c := int b[i];
+		if (font == -1) {
+			# idle state
+			if(c >= 16rA1){
+				font = c;
+				continue;
+			}
+			if(c == 26)
+				c = '\n';
+			str[len str] = c;
+			nbytes = i + 1;
+			continue;
+		} else {
+			# seen a font spec
+			f := font;
+			font = -1;
+			ch := Sys->UTFerror;
+			if(c >= 64 && c <= 126)
+				c -= 64;
+			else if(c >= 161 && c <= 254)
+				c = c-161 + 63;
+			else
+				# bad big5 char
+				f = 255;
+			if(f <= 254) {
+				f -= 161;
+				ix := f*BIG5FONT + c;
+				if(ix < len big5map)
+					ch = big5map[ix];
+				if (ch == -1)
+					ch = BADCHAR;
+			}
+			str[len str] = ch;
+			nbytes = i + 1;
+		}
+	}
+	return (nil, str, nbytes);
+}
--- /dev/null
+++ b/appl/lib/convcs/big5_stob.b
@@ -1,0 +1,93 @@
+implement Stob;
+
+include "sys.m";
+include "convcs.m";
+
+
+# Big5 consists of 89 fonts of 157 chars each
+BIG5MAX : con 13973;
+BIG5FONT : con 157;
+NFONTS : con 89;
+
+BIG5DATA : con "/lib/convcs/big5";
+
+big5map := "";
+r2fontchar : array of byte;
+
+# NOTE: could be more memory friendly during init()
+# by building the r2fontchar mapping table on the fly
+# instead of building it from the complete big5map string
+
+init(nil : string) : string
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->open(BIG5DATA, Sys->OREAD);
+	if (fd == nil)
+		return sys->sprint("%s: %r", BIG5DATA);
+
+	buf := array[BIG5MAX * Sys->UTFmax] of byte;
+	nread := 0;
+	for (;nread < len buf;) {
+		n := sys->read(fd, buf[nread:], Sys->ATOMICIO);
+		if (n <= 0)
+			break;
+		nread += n;
+	}
+	big5map = string buf[:nread];
+	buf = nil;
+	if (len big5map != BIG5MAX) {
+		big5map = nil;
+		return sys->sprint("%s: corrupt data", BIG5DATA);
+	}
+	r2fontchar = array [2 * 16r10000] of { * => byte 16rff};
+	for (i := 0; i < len big5map; i++) {
+		f := i / BIG5FONT;
+		c := i % BIG5FONT;
+		ix := 2*big5map[i];
+		r2fontchar[ix] = byte f;
+		r2fontchar[ix+1] = byte c;
+	}
+	return nil;
+}
+
+stob(nil : Convcs->State, str : string) : (Convcs->State, array of byte)
+{
+	buf := array [1024] of byte;
+	nb := 0;
+	cbuf := array [2] of byte;
+	nc := 0;
+	for (i := 0; i < len str; i++) {
+		c := str[i];
+		nc = 0;
+		if (c < 128) {
+#			if (c == '\n')		# not sure abou this
+#				c = 26;
+			cbuf[nc++] = byte c;
+		} else {
+			ix := 2*c;
+			f := int r2fontchar[ix];
+			c = int r2fontchar[ix+1];
+			if (f >= NFONTS) {
+				# no mapping of unicode character to big5
+				cbuf[nc++] = byte '?';
+			} else {
+				f += 16rA1;
+				cbuf[nc++] = byte f;
+				if (c <= 62)
+					c += 64;
+				else
+					c += 16rA1 - 63;
+				cbuf[nc++] = byte c;
+			}
+		}
+		if (nc + nb > len buf)
+			buf = ((array [len buf * 2] of byte)[:] = buf);
+		buf[nb:] = cbuf[:nc];
+		nb += nc;
+	}
+	if (nb == 0)
+		return (nil, nil);
+	r := array [nb] of byte;
+	r[:] = buf[:nb];
+	return (nil, r);
+}
--- /dev/null
+++ b/appl/lib/convcs/convcs.b
@@ -1,0 +1,165 @@
+implement Convcs;
+
+include "sys.m";
+include "cfg.m";
+include "convcs.m";
+
+DEFCSFILE : con "/lib/convcs/charsets";
+
+sys : Sys;
+cfg : Cfg;
+
+Record, Tuple : import cfg;
+
+init(csfile : string) : string
+{
+	sys = load Sys Sys->PATH;
+	cfg = load Cfg Cfg->PATH;
+	if (cfg == nil)
+		return sys->sprint("cannot load module %s: %r", Cfg->PATH);
+	if (csfile == nil)
+		csfile = DEFCSFILE;
+	err := cfg->init(csfile);
+	if (err != nil) {
+		cfg = nil;
+		return err;
+	}
+	return nil;
+}
+
+getbtos(cs : string) : (Btos, string)
+{
+	cs = normalize(cs);
+	(rec, err) := csalias(cs);
+	if (err != nil)
+		return (nil, err);
+
+	(path, btostup) := rec.lookup("btos");
+	if (path == nil)
+		return (nil, sys->sprint("no converter for %s", cs));
+	arg := btostup.lookup("arg");
+
+	btos := load Btos path;
+	if (btos == nil)
+		return (nil, sys->sprint("cannot load converter: %r"));
+	err = btos->init(arg);
+	if (err != nil)
+		return (nil, err);
+	return (btos, nil);
+}
+
+getstob(cs : string) : (Stob, string)
+{
+	cs = normalize(cs);
+	(rec, err) := csalias(cs);
+	if (err != nil)
+		return (nil, err);
+
+	(path, stobtup) := rec.lookup("stob");
+	if (path == nil)
+		return (nil, sys->sprint("no converter for %s", cs));
+	arg := stobtup.lookup("arg");
+
+	stob := load Stob path;
+	if (stob == nil)
+		return (nil, sys->sprint("cannot load converter: %r"));
+	err = stob->init(arg);
+	if (err != nil)
+		return (nil, err);
+	return (stob, nil);
+}
+
+csalias(cs : string) : (ref Cfg->Record, string)
+{
+	# search out charset record - allow for one level of renaming
+	for (i := 0; i < 2; i++) {
+		recs := cfg->lookup(cs);
+		if (recs == nil)
+			return (nil, sys->sprint("unknown charset %s", cs));
+		(val, rec) := hd recs;
+		if (val != nil) {
+			cs = val;
+			continue;
+		}
+		return (rec, nil);
+	}
+	return (nil, sys->sprint("too man aliases for %s\n", cs));
+}
+
+enumcs() : list of (string, string, int)
+{
+	d : list of (string, string, int);
+	for (csl := cfg->getkeys(); csl != nil; csl = tl csl) {
+		cs := hd csl;
+		recs := cfg->lookup(cs);
+		if (recs == nil)
+			continue;	# shouldn't happen!
+		(val, rec) := hd recs;
+		if (val != nil)
+			# an alias - ignore
+			continue;
+
+		(btos, nil) := rec.lookup("btos");
+		(stob, nil) := rec.lookup("stob");
+
+		if (btos == nil && stob == nil)
+			continue;
+		mode := 0;
+		if (btos != nil)
+			mode = BTOS;
+		if (stob != nil)
+			mode |= STOB;
+
+		(desc, nil) := rec.lookup("desc");
+		if (desc == nil)
+			desc = cs;
+
+		d = (cs, desc, mode) :: d;
+	}
+	# d is in reverse order to that in the csfile file
+	l : list of (string, string, int);
+	for (; d != nil; d = tl d)
+		l = hd d :: l;
+	return l;
+}
+
+aliases(cs : string) : (string, list of string)
+{
+	cs = normalize(cs);
+	(mainrec, err) := csalias(cs);
+	if (err != nil)
+		return (err, nil);
+
+	cs = (hd (hd mainrec.tuples).attrs).name;
+
+	(desc, nil) := mainrec.lookup("desc");
+	if (desc == nil)
+		desc = cs;
+
+	al := cs :: nil;
+	for (csl := cfg->getkeys(); csl != nil; csl = tl csl) {
+		name := hd csl;
+		recs := cfg->lookup(name);
+		if (recs == nil)
+			continue;	# shouldn't happen!
+		(val, nil) := hd recs;
+		if (val != cs)
+			continue;
+		al = name :: al;
+	}
+
+	r : list of string;
+	for (; al != nil; al = tl al)
+		r = hd al :: r;
+	return (desc, r);
+}
+
+normalize(s : string) : string
+{
+	for (i := 0; i < len s; i++) {
+		r := s[i];
+		if (r >= 'A' && r <= 'Z')
+			s[i] = r + ('a' - 'A');
+	}
+	return s;
+}
--- /dev/null
+++ b/appl/lib/convcs/cp932_btos.b
@@ -1,0 +1,179 @@
+implement Btos;
+
+# encoding details
+# (Traditional) Shift-JIS
+#
+# 00..1f	control characters
+# 20		space
+# 21..7f	JIS X 0201:1976/1997 roman (see notes)
+# 80		undefined
+# 81..9f	lead byte of JIS X 0208-1983 or JIS X 0202:1990/1997
+# a0		undefined
+# a1..df	JIS X 0201:1976/1997 katakana
+# e0..ea	lead byte of JIS X 0208-1983 or JIS X 0202:1990/1997
+# eb..ff	undefined
+#
+# CP932 (windows-31J)
+#
+# this encoding scheme extends Shift-JIS in the following way
+#
+# eb..ec	undefined (marked as lead bytes - see notes below)
+# ed..ee	lead byte of NEC-selected IBM extended characters
+# ef		undefined (marked as lead byte - see notes below)
+# f0..f9	lead byte of User defined GAIJI (see note below)
+# fa..fc	lead byte of IBM extended characters
+# fd..ff	undefined
+#
+#
+# Notes
+#
+# JISX 0201:1976/1997 roman
+#	this is the same as ASCII but with 0x5c (ASCII code for '\')
+#	representing the Yen currency symbol '¥' (U+00a5)
+#	This mapping is contentious, some conversion packages implent it
+#	others do not.
+#	The mapping files from The Unicode Consortium show cp932 mapping
+#	plain ascii in the range 00..7f whereas shift-jis maps 16r5c ('\') to the yen
+#	symbol (¥) and 16r7e ('~') to overline (¯)
+#
+# CP932 double-byte character codes:
+#
+# eb-ec, ef, f0-f9:
+# 	Marked as DBCS LEAD BYTEs in the unicode mapping data
+#	obtained from:
+#		https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT
+#
+# 	but there are no defined mappings for codes in this range.
+# 	It is not clear whether or not an implementation should
+# 	consume one or two bytes before emitting an error char.
+#
+
+include "sys.m";
+include "convcs.m";
+
+sys : Sys;
+
+MAXINT : con 16r7fffffff;
+BADCHAR : con 16rFFFD;
+
+KANAPAGES : con 1;
+KANAPAGESZ : con 63;
+KANACHAR0 : con 16ra1;
+
+CP932PAGES : con 45;		# 81..84, 87..9f, e0..ea, ed..ee, fa..fc
+CP932PAGESZ : con 189;		# 40..fc (including 7f)
+CP932CHAR0 : con 16r40;
+
+
+shiftjis := 0;
+page0 := array [256] of { * => BADCHAR };
+cp932 : string;
+dbcsoff := array [256] of { * => -1 };
+
+init(arg : string) : string
+{
+	sys = load Sys Sys->PATH;
+	shiftjis = arg == "shiftjis";
+
+	(error, kana) := getmap("/lib/convcs/jisx0201kana", KANAPAGESZ, KANAPAGES);
+	if (error != nil)
+		return error;
+
+	(error, cp932) = getmap("/lib/convcs/cp932", CP932PAGESZ, CP932PAGES);
+	if (error != nil)
+		return error;
+
+	# jisx0201kana is mapped into 16rA1..16rDF
+	for (i := 0; i < KANAPAGESZ; i++)
+		page0[i + KANACHAR0] = kana[i];
+
+	# 00..7f same as ascii in cp932
+	for (i = 0; i <= 16r7f; i++)
+		page0[i] = i;
+	if (shiftjis) {
+		# shift-jis uses JIS X 0201 for the ASCII range
+		# this is the same as ASCII apart from
+		# 16r5c ('\') maps to yen symbol (¥) and 16r7e ('~') maps to overline (¯)
+		page0['\\'] = '¥';
+		page0['~'] = '¯';
+	}
+
+	# pre-calculate DBCS page numbers to mapping file page numbers
+	# and mark codes in page0 that are DBCS lead bytes
+	pnum := 0;
+	for (i = 16r81; i <= 16r84; i++){
+		page0[i] = -1;
+		dbcsoff[i] = pnum++;
+	}
+	for (i = 16r87; i <= 16r9f; i++){
+		page0[i] = -1;
+		dbcsoff[i] = pnum++;
+	}
+	for (i = 16re0; i <= 16rea; i++) {
+		page0[i] = -1;
+		dbcsoff[i] = pnum++;
+	}
+	if (!shiftjis) {
+		# add in cp932 extensions
+		for (i = 16red; i <= 16ree; i++) {
+			page0[i] = -1;
+			dbcsoff[i] = pnum++;
+		}
+		for (i = 16rfa; i <= 16rfc; i++) {
+			page0[i] = -1;
+			dbcsoff[i] = pnum++;
+		}
+	}
+	return nil;
+}
+
+btos(nil : Convcs->State, b : array of byte, n : int) : (Convcs->State, string, int)
+{
+	nbytes := 0;
+	str := "";
+
+	if (n == -1)
+		n = MAXINT;
+
+	for (i := 0; i < len b && len str < n; i++) {
+		b1 := int b[i];
+		ch := page0[b1];
+		if (ch != -1) {
+			str[len str] = ch;
+			nbytes++;
+			continue;
+		}
+		# DBCS
+		i++;
+		if (i >= len b)
+			break;
+		pnum := dbcsoff[b1];
+		ix := (int b[i]) - CP932CHAR0;
+		if (pnum == -1 || ix < 0 || ix >= CP932PAGESZ)
+			str[len str] = BADCHAR;
+		else
+			str[len str] = cp932[(pnum * CP932PAGESZ)+ix];
+		nbytes += 2;
+	}
+	return (nil, str, nbytes);
+}
+
+getmap(path : string, pgsz, npgs : int) : (string, string)
+{
+	fd := sys->open(path, Sys->OREAD);
+	if (fd == nil)
+		return (sys->sprint("%s: %r", path), nil);
+
+	buf := array[(pgsz * npgs) * Sys->UTFmax] of byte;
+	nread := 0;
+	for (;nread < len buf;) {
+		n := sys->read(fd, buf[nread:], Sys->ATOMICIO);
+		if (n <= 0)
+			break;
+		nread += n;
+	}
+	map := string buf[:nread];
+	if (len map != (pgsz * npgs))
+		return (sys->sprint("%s: bad data", path), nil);
+	return (nil, map);
+}
--- /dev/null
+++ b/appl/lib/convcs/cp_btos.b
@@ -1,0 +1,45 @@
+implement Btos;
+
+include "sys.m";
+include "convcs.m";
+
+sys : Sys;
+codepage : string;
+
+init(arg : string) : string
+{
+	sys = load Sys Sys->PATH;
+	if (arg == nil)
+		return "codepage path required";
+	fd := sys->open(arg, Sys->OREAD);
+	if (fd == nil)
+		return sys->sprint("%s: %r", arg);
+
+	buf := array[Sys->UTFmax * 256] of byte;
+	nread := 0;
+	for (;nread < len buf;) {
+		toread := len buf - nread;
+		n := sys->read(fd, buf[nread:], toread);
+		if (n <= 0)
+			break;
+		nread += n;
+	}
+	codepage = string buf[0:nread];
+	if (len codepage != 256) {
+		codepage = nil;
+		return sys->sprint("%s: bad codepage", arg);
+	}
+	return nil;
+}
+
+btos(nil : Convcs->State, b : array of byte, n : int) : (Convcs->State, string, int)
+{
+	s := "";
+	if (n == -1 || n > len b)
+		# consume all available characters
+		n = len b;
+
+	for (i := 0; i < n; i++)
+		s[len s] = codepage[int b[i]];
+	return (nil, s, n);
+}
--- /dev/null
+++ b/appl/lib/convcs/cp_stob.b
@@ -1,0 +1,49 @@
+implement Stob;
+
+include "sys.m";
+include "convcs.m";
+
+sys : Sys;
+map : array of byte;
+
+ERRCHAR : con 16rFFFD;
+
+init(arg : string) : string
+{
+	sys = load Sys Sys->PATH;
+	if (arg == nil)
+		return "codepage path required";
+	fd := sys->open(arg, Sys->OREAD);
+	if (fd == nil)
+		return sys->sprint("%s: %r", arg);
+
+	buf := array[Sys->UTFmax * 256] of byte;
+	nread := 0;
+	for (;nread < len buf;) {
+		toread := len buf - nread;
+		n := sys->read(fd, buf[nread:], toread);
+		if (n <= 0)
+			break;
+		nread += n;
+	}
+	codepage := string buf[0:nread];
+	if (len codepage != 256) {
+		codepage = nil;
+		return sys->sprint("%s: bad codepage", arg);
+	}
+	buf = nil;
+	map = array[16r10000] of { * => byte '?' };
+	for (i := 0; i < 256; i++)
+		map[codepage[i]] = byte i;
+	return nil;
+}
+
+stob(nil : Convcs->State, str : string) : (Convcs->State, array of byte)
+{
+	b := array [len str] of byte;
+	n := len str;
+
+	for (i := 0; i < n; i++)
+		b[i] = map[str[i]];
+	return (nil, b);
+}
--- /dev/null
+++ b/appl/lib/convcs/euc-jp_btos.b
@@ -1,0 +1,162 @@
+implement Btos;
+
+# EUC-JP is based on ISO2022 but only uses the 8 bit stateless encoding.
+# Thus, only the following ISO2022 shift functions are used:
+#	SINGLE-SHIFT TWO
+#	SINGLE-SHIFT THREE
+#
+# The initial state is G0 mapped into GL and G1 mapped into GR
+# SINGLE-SHIFT TWO maps G2 into GR for one code-point encoding
+# SINGLE-SHIFT THREE maps G3 into GR for one code-point encoding
+#
+# EUC-JP has pre-assigned code elements (G0..G3) that are never re-assigned
+# by means on ISO2022 code-identification functions (escape sequences)
+#
+#	G0 =	ASCII
+#	G1 = JIS X 0208
+#	G2 = JIS X 0201 Kana
+#	G3 = JIS X 0212
+
+include "sys.m";
+include "convcs.m";
+
+sys : Sys;
+
+SS2 : con 16r8E;	# ISO2022 SINGLE-SHIFT TWO
+SS3 : con 16r8F;	# ISO2022 SINGLE-SHIFT THREE
+
+MAXINT : con 16r7fffffff;
+BADCHAR : con 16rFFFD;
+
+G1PATH : con "/lib/convcs/jisx0208-1997";
+G2PATH : con "/lib/convcs/jisx0201kana";
+G3PATH : con "/lib/convcs/jisx0212";
+
+g1map : string;
+g2map : string;
+g3map : string;
+
+G1PAGESZ : con 94;
+G1NPAGES : con 84;
+G1PAGE0 : con 16rA1;
+G1CHAR0 : con 16rA1;
+
+G2PAGESZ : con 63;
+G2NPAGES : con 1;
+G2CHAR0 : con 16rA1;
+
+G3PAGESZ : con 94;
+G3NPAGES : con 77;
+G3PAGE0 : con 16rA1;
+G3CHAR0 : con 16rA1;
+
+init(nil : string) : string
+{
+	sys = load Sys Sys->PATH;
+
+	error := "";
+	(error, g1map) = getmap(G1PATH, G1PAGESZ, G1NPAGES);
+	if (error != nil)
+		return error;
+	(error, g2map) = getmap(G2PATH, G2PAGESZ, G2NPAGES);
+	if (error != nil)
+		return error;
+	(error, g3map) = getmap(G3PATH, G3PAGESZ, G3NPAGES);
+	return error;
+}
+
+getmap(path : string, pgsz, npgs : int) : (string, string)
+{
+	fd := sys->open(path, Sys->OREAD);
+	if (fd == nil)
+		return (sys->sprint("%s: %r", path), nil);
+
+	buf := array[(pgsz * npgs) * Sys->UTFmax] of byte;
+	nread := 0;
+	for (;nread < len buf;) {
+		n := sys->read(fd, buf[nread:], Sys->ATOMICIO);
+		if (n <= 0)
+			break;
+		nread += n;
+	}
+	map := string buf[:nread];
+	if (len map != (pgsz * npgs))
+		return (sys->sprint("%s: bad data", path), nil);
+	return (nil, map);
+}
+
+btos(nil : Convcs->State, b : array of byte, n : int) : (Convcs->State, string, int)
+{
+	nbytes := 0;
+	str := "";
+
+	if (n == -1)
+		n = MAXINT;
+
+	codelen := 1;
+	codeix := 0;
+	G0, G1, G2, G3 : con iota;
+	state := G0;
+	bytes := array [3] of int;
+
+	while (len str < n) {
+		for (i := nbytes + codeix; i < len b && codeix < codelen; i++)
+			bytes[codeix++]= int b[i];
+
+		if (codeix != codelen)
+			break;
+
+		case state {
+		G0 =>
+			case bytes[0] {
+			0 to 16r7f =>
+				str[len str] = bytes[0];
+			G1PAGE0 to G1PAGE0+G1NPAGES =>
+				state = G1;
+				codelen = 2;
+				continue;
+			SS2 =>
+				state = G2;
+				codelen = 2;
+				continue;
+			SS3 =>
+				state = G3;
+				codelen = 3;
+				continue;
+			* =>
+				str[len str] = BADCHAR;
+			}
+		G1 =>
+			# double byte encoding
+			page := bytes[0] - G1PAGE0;
+			char := bytes[1] - G1CHAR0;
+			str[len str] = g1map[(page * G1PAGESZ) + char];
+		G2 =>
+			# single byte encoding (byte 0 == SS2)
+			char := bytes[1] - G2CHAR0;
+			if (char < 0 || char >= len g2map)
+				char = BADCHAR;
+			else
+				char = g2map[char];
+			str[len str] = char;
+		G3 =>
+			# double byte encoding (byte 0 == SS3)
+			page := bytes[1] - G3PAGE0;
+			char := bytes[2] - G3CHAR0;
+			if (page < 0 || page >= G3NPAGES) {
+				# first byte is wrong - backup
+				i--;
+				str[len str] = BADCHAR;
+			} else if (char >= G3PAGESZ)
+				str[len str] = BADCHAR;
+			else
+				str[len str] = g3map[(page * G3PAGESZ)+char];
+		}
+
+		state = G0;
+		nbytes = i;
+		codelen = 1;
+		codeix = 0;
+	}
+	return (nil, str, nbytes);
+}
--- /dev/null
+++ b/appl/lib/convcs/gb2312_btos.b
@@ -1,0 +1,70 @@
+implement Btos;
+
+include "sys.m";
+include "convcs.m";
+
+GBMAX : con 8795;
+
+GBDATA : con "/lib/convcs/gb2312";
+
+MAXINT : con 16r7fffffff;
+BADCHAR : con 16rFFFD;
+
+gbmap := "";
+
+init(nil : string): string
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->open(GBDATA, Sys->OREAD);
+	if (fd == nil)
+		return sys->sprint("%s: %r", GBDATA);
+
+	buf := array[GBMAX * Sys->UTFmax] of byte;
+	nread := 0;
+	for (;nread < len buf;) {
+		n := sys->read(fd, buf[nread:], Sys->ATOMICIO);
+		if (n <= 0)
+			break;
+		nread += n;
+	}
+	gbmap = string buf[:nread];
+	if (len gbmap != GBMAX) {
+		gbmap = nil;
+		return sys->sprint("%s: corrupt data", GBDATA);
+	}
+	return nil;
+}
+
+btos(nil : Convcs->State, b : array of byte, n : int) : (Convcs->State, string, int)
+{
+	nbytes := 0;
+	str := "";
+
+	if (n == -1)
+		n = MAXINT;
+
+	font := -1;
+	for (i := 0; i < len b && len str < n; i++) {
+		c := int b[i];
+		ch := Sys->UTFerror;
+		if (font == -1) {
+			# idle state
+			if (c >= 16rA1) {
+				font = c;
+				continue;
+			}
+			ch = c;
+		} else {
+			# seen a font spec
+			if (c >= 16rA1) {
+				ix := (font - 16rA0)*100 + (c-16rA0);
+				ch = gbmap[ix];
+			}
+			font = -1;
+		}
+		str[len str] = ch;
+		nbytes = i + 1;
+	}
+	return (nil, str, nbytes);
+}
+
--- /dev/null
+++ b/appl/lib/convcs/genbig5.b
@@ -1,0 +1,1790 @@
+# generate the Big5 character set converter data file
+implement GenBIG5;
+
+include "sys.m";
+include "draw.m";
+
+BIG5DATA: con "/lib/convcs/big5";
+
+GenBIG5 : module {
+	init : fn (ctxt : ref Draw->Context, args : list of string);
+};
+
+init(nil : ref Draw->Context, nil : list of string)
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->create(BIG5DATA, Sys->OWRITE, 8r644);
+	if (fd == nil) {
+		sys->print("cannot create %s: %r\n", BIG5DATA);
+		return;
+	}
+	s := "";
+	for (i := 0; i < len tabbig5; i++)
+		s[len s] = tabbig5[i];
+
+	buf := array of byte s;
+	for (i = 0; i < len buf;) {
+		towrite := Sys->ATOMICIO;
+		if (len buf - i < Sys->ATOMICIO)
+			towrite = len buf -i;
+		n := sys->write(fd, buf[i:], towrite);
+		if (n <= 0) {
+			sys->print("error writing %s: %r", BIG5DATA);
+			return;
+		}
+		i += n;
+	}
+}
+
+
+ERRchar : con 16rFFFD;
+
+tabbig5 := array[] of {
+16r3000,16rff0c,16r3001,16r3002,16rff0e,16r30fb,16rff1b,16rff1a,
+16rff1f,16rff01,16rfe30,16r2026,16r2025,16rfe50,16rfe51,16rfe52,
+16r00b7,16rfe54,16rfe55,16rfe56,16rfe57,16rfe32,16r2013,16rfe31,
+16r2014,16rfe33,ERRchar,16rfe34,16rfe4f,16rff08,16rff09,16rfe35,
+16rfe36,16rff5b,16rff5d,16rfe37,16rfe38,16r3014,16r3015,16rfe39,
+16rfe3a,16r3010,16r3011,16rfe3b,16rfe3c,16r300a,16r300b,16rfe3d,
+16rfe3e,16r3008,16r3009,16rfe3f,16rfe40,16r300c,16r300d,16rfe41,
+16rfe42,16r300e,16r300f,16rfe43,16rfe44,16rfe59,16rfe5a,16rfe5b,
+16rfe5c,16rfe5d,16rfe5e,16r2018,16r2019,16r201c,16r201d,16r301d,
+16r301e,16r2035,16r2032,16rff03,16rff06,16rff0a,16r203b,16r00a7,
+16r3003,16r25cb,16r25cf,16r25b3,16r25b2,16r25ce,16r2606,16r2605,
+16r25c7,16r25c6,16r25a1,16r25a0,16r25bd,16r25bc,16r32a3,16r2105,
+16r203e,ERRchar,16rff3f,ERRchar,16rfe49,16rfe4a,16rfe4d,16rfe4e,
+16rfe4b,16rfe4c,16r0023,16r0026,16r002a,16rff0b,16rff0d,16r00d7,
+16r00f7,16r00b1,16r221a,16rff1c,16rff1e,16rff1d,16r2264,16r2265,
+16r2260,16r221e,16r2252,16r2261,16rfe62,16rfe63,16rfe64,16rfe65,
+16rfe66,16r223c,16r2229,16r222a,16r22a5,16r2220,16r221f,16r22bf,
+16r33d2,16r33d1,16r222b,16r222e,16r2235,16r2234,16r2640,16r2642,
+16r2641,16r2609,16r2191,16r2193,16r2190,16r2192,16r2196,16r2197,
+16r2199,16r2198,16r2225,16r2223,ERRchar,ERRchar,16rff0f,16rff3c,
+16rff04,16r00a5,16r3012,16r00a2,16r00a3,16rff05,16rff20,16r2103,
+16r2109,16r0024,16r0025,16r0040,16r33d5,16r339c,16r339d,16r339e,
+16r33ce,16r33a1,16r338e,16r338f,16r33c4,16r00b0,16r5159,16r515b,
+16r515e,16r515d,16r5161,16r5163,16r55e7,16r74e9,16r7cce,16r2581,
+16r2582,16r2583,16r2584,16r2585,16r2586,16r2587,16r2588,16r258f,
+16r258e,16r258d,16r258c,16r258b,16r258a,16r2589,16r253c,16r2534,
+16r252c,16r2524,16r251c,16r2594,16r2500,16r2502,16r2595,16r250c,
+16r2510,16r2514,16r2518,16r256d,16r256e,16r2570,16r256f,16r2550,
+16r255e,16r256a,16r2561,16r25e2,16r25e3,16r25e5,16r25e4,16r2571,
+16r2572,16r2573,16rff10,16rff11,16rff12,16rff13,16rff14,16rff15,
+16rff16,16rff17,16rff18,16rff19,16r2160,16r2161,16r2162,16r2163,
+16r2164,16r2165,16r2166,16r2167,16r2168,16r2169,16r3021,16r3022,
+16r3023,16r3024,16r3025,16r3026,16r3027,16r3028,16r3029,ERRchar,
+16r5344,ERRchar,16rff21,16rff22,16rff23,16rff24,16rff25,16rff26,
+16rff27,16rff28,16rff29,16rff2a,16rff2b,16rff2c,16rff2d,16rff2e,
+16rff2f,16rff30,16rff31,16rff32,16rff33,16rff34,16rff35,16rff36,
+16rff37,16rff38,16rff39,16rff3a,16rff41,16rff42,16rff43,16rff44,
+16rff45,16rff46,16rff47,16rff48,16rff49,16rff4a,16rff4b,16rff4c,
+16rff4d,16rff4e,16rff4f,16rff50,16rff51,16rff52,16rff53,16rff54,
+16rff55,16rff56,16rff57,16rff58,16rff59,16rff5a,16r0391,16r0392,
+16r0393,16r0394,16r0395,16r0396,16r0397,16r0398,16r0399,16r039a,
+16r039b,16r039c,16r039d,16r039e,16r039f,16r03a0,16r03a1,16r03a3,
+16r03a4,16r03a5,16r03a6,16r03a7,16r03a8,16r03a9,16r03b1,16r03b2,
+16r03b3,16r03b4,16r03b5,16r03b6,16r03b7,16r03b8,16r03b9,16r03ba,
+16r03bb,16r03bc,16r03bd,16r03be,16r03bf,16r03c0,16r03c1,16r03c3,
+16r03c4,16r03c5,16r03c6,16r03c7,16r03c8,16r03c9,16r3105,16r3106,
+16r3107,16r3108,16r3109,16r310a,16r310b,16r310c,16r310d,16r310e,
+16r310f,16r3110,16r3111,16r3112,16r3113,16r3114,16r3115,16r3116,
+16r3117,16r3118,16r3119,16r311a,16r311b,16r311c,16r311d,16r311e,
+16r311f,16r3120,16r3121,16r3122,16r3123,16r3124,16r3125,16r3126,
+16r3127,16r3128,16r3129,16r02d9,16r02c9,16r02ca,16r02c7,16r02cb,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r4e00,
+16r4e59,16r4e01,16r4e03,16r4e43,16r4e5d,16r4e86,16r4e8c,16r4eba,
+16r513f,16r5165,16r516b,16r51e0,16r5200,16r5201,16r529b,16r5315,
+16r5341,16r535c,16r53c8,16r4e09,16r4e0b,16r4e08,16r4e0a,16r4e2b,
+16r4e38,16r51e1,16r4e45,16r4e48,16r4e5f,16r4e5e,16r4e8e,16r4ea1,
+16r5140,16r5203,16r52fa,16r5343,16r53c9,16r53e3,16r571f,16r58eb,
+16r5915,16r5927,16r5973,16r5b50,16r5b51,16r5b53,16r5bf8,16r5c0f,
+16r5c22,16r5c38,16r5c71,16r5ddd,16r5de5,16r5df1,16r5df2,16r5df3,
+16r5dfe,16r5e72,16r5efe,16r5f0b,16r5f13,16r624d,16r4e11,16r4e10,
+16r4e0d,16r4e2d,16r4e30,16r4e39,16r4e4b,16r5c39,16r4e88,16r4e91,
+16r4e95,16r4e92,16r4e94,16r4ea2,16r4ec1,16r4ec0,16r4ec3,16r4ec6,
+16r4ec7,16r4ecd,16r4eca,16r4ecb,16r4ec4,16r5143,16r5141,16r5167,
+16r516d,16r516e,16r516c,16r5197,16r51f6,16r5206,16r5207,16r5208,
+16r52fb,16r52fe,16r52ff,16r5316,16r5339,16r5348,16r5347,16r5345,
+16r535e,16r5384,16r53cb,16r53ca,16r53cd,16r58ec,16r5929,16r592b,
+16r592a,16r592d,16r5b54,16r5c11,16r5c24,16r5c3a,16r5c6f,16r5df4,
+16r5e7b,16r5eff,16r5f14,16r5f15,16r5fc3,16r6208,16r6236,16r624b,
+16r624e,16r652f,16r6587,16r6597,16r65a4,16r65b9,16r65e5,16r66f0,
+16r6708,16r6728,16r6b20,16r6b62,16r6b79,16r6bcb,16r6bd4,16r6bdb,
+16r6c0f,16r6c34,16r706b,16r722a,16r7236,16r723b,16r7247,16r7259,
+16r725b,16r72ac,16r738b,16r4e19,16r4e16,16r4e15,16r4e14,16r4e18,
+16r4e3b,16r4e4d,16r4e4f,16r4e4e,16r4ee5,16r4ed8,16r4ed4,16r4ed5,
+16r4ed6,16r4ed7,16r4ee3,16r4ee4,16r4ed9,16r4ede,16r5145,16r5144,
+16r5189,16r518a,16r51ac,16r51f9,16r51fa,16r51f8,16r520a,16r52a0,
+16r529f,16r5305,16r5306,16r5317,16r531d,16r4edf,16r534a,16r5349,
+16r5361,16r5360,16r536f,16r536e,16r53bb,16r53ef,16r53e4,16r53f3,
+16r53ec,16r53ee,16r53e9,16r53e8,16r53fc,16r53f8,16r53f5,16r53eb,
+16r53e6,16r53ea,16r53f2,16r53f1,16r53f0,16r53e5,16r53ed,16r53fb,
+16r56db,16r56da,16r5916,16r592e,16r5931,16r5974,16r5976,16r5b55,
+16r5b83,16r5c3c,16r5de8,16r5de7,16r5de6,16r5e02,16r5e03,16r5e73,
+16r5e7c,16r5f01,16r5f18,16r5f17,16r5fc5,16r620a,16r6253,16r6254,
+16r6252,16r6251,16r65a5,16r65e6,16r672e,16r672c,16r672a,16r672b,
+16r672d,16r6b63,16r6bcd,16r6c11,16r6c10,16r6c38,16r6c41,16r6c40,
+16r6c3e,16r72af,16r7384,16r7389,16r74dc,16r74e6,16r7518,16r751f,
+16r7528,16r7529,16r7530,16r7531,16r7532,16r7533,16r758b,16r767d,
+16r76ae,16r76bf,16r76ee,16r77db,16r77e2,16r77f3,16r793a,16r79be,
+16r7a74,16r7acb,16r4e1e,16r4e1f,16r4e52,16r4e53,16r4e69,16r4e99,
+16r4ea4,16r4ea6,16r4ea5,16r4eff,16r4f09,16r4f19,16r4f0a,16r4f15,
+16r4f0d,16r4f10,16r4f11,16r4f0f,16r4ef2,16r4ef6,16r4efb,16r4ef0,
+16r4ef3,16r4efd,16r4f01,16r4f0b,16r5149,16r5147,16r5146,16r5148,
+16r5168,16r5171,16r518d,16r51b0,16r5217,16r5211,16r5212,16r520e,
+16r5216,16r52a3,16r5308,16r5321,16r5320,16r5370,16r5371,16r5409,
+16r540f,16r540c,16r540a,16r5410,16r5401,16r540b,16r5404,16r5411,
+16r540d,16r5408,16r5403,16r540e,16r5406,16r5412,16r56e0,16r56de,
+16r56dd,16r5733,16r5730,16r5728,16r572d,16r572c,16r572f,16r5729,
+16r5919,16r591a,16r5937,16r5938,16r5984,16r5978,16r5983,16r597d,
+16r5979,16r5982,16r5981,16r5b57,16r5b58,16r5b87,16r5b88,16r5b85,
+16r5b89,16r5bfa,16r5c16,16r5c79,16r5dde,16r5e06,16r5e76,16r5e74,
+16r5f0f,16r5f1b,16r5fd9,16r5fd6,16r620e,16r620c,16r620d,16r6210,
+16r6263,16r625b,16r6258,16r6536,16r65e9,16r65e8,16r65ec,16r65ed,
+16r66f2,16r66f3,16r6709,16r673d,16r6734,16r6731,16r6735,16r6b21,
+16r6b64,16r6b7b,16r6c16,16r6c5d,16r6c57,16r6c59,16r6c5f,16r6c60,
+16r6c50,16r6c55,16r6c61,16r6c5b,16r6c4d,16r6c4e,16r7070,16r725f,
+16r725d,16r767e,16r7af9,16r7c73,16r7cf8,16r7f36,16r7f8a,16r7fbd,
+16r8001,16r8003,16r800c,16r8012,16r8033,16r807f,16r8089,16r808b,
+16r808c,16r81e3,16r81ea,16r81f3,16r81fc,16r820c,16r821b,16r821f,
+16r826e,16r8272,16r827e,16r866b,16r8840,16r884c,16r8863,16r897f,
+16r9621,16r4e32,16r4ea8,16r4f4d,16r4f4f,16r4f47,16r4f57,16r4f5e,
+16r4f34,16r4f5b,16r4f55,16r4f30,16r4f50,16r4f51,16r4f3d,16r4f3a,
+16r4f38,16r4f43,16r4f54,16r4f3c,16r4f46,16r4f63,16r4f5c,16r4f60,
+16r4f2f,16r4f4e,16r4f36,16r4f59,16r4f5d,16r4f48,16r4f5a,16r514c,
+16r514b,16r514d,16r5175,16r51b6,16r51b7,16r5225,16r5224,16r5229,
+16r522a,16r5228,16r52ab,16r52a9,16r52aa,16r52ac,16r5323,16r5373,
+16r5375,16r541d,16r542d,16r541e,16r543e,16r5426,16r544e,16r5427,
+16r5446,16r5443,16r5433,16r5448,16r5442,16r541b,16r5429,16r544a,
+16r5439,16r543b,16r5438,16r542e,16r5435,16r5436,16r5420,16r543c,
+16r5440,16r5431,16r542b,16r541f,16r542c,16r56ea,16r56f0,16r56e4,
+16r56eb,16r574a,16r5751,16r5740,16r574d,16r5747,16r574e,16r573e,
+16r5750,16r574f,16r573b,16r58ef,16r593e,16r599d,16r5992,16r59a8,
+16r599e,16r59a3,16r5999,16r5996,16r598d,16r59a4,16r5993,16r598a,
+16r59a5,16r5b5d,16r5b5c,16r5b5a,16r5b5b,16r5b8c,16r5b8b,16r5b8f,
+16r5c2c,16r5c40,16r5c41,16r5c3f,16r5c3e,16r5c90,16r5c91,16r5c94,
+16r5c8c,16r5deb,16r5e0c,16r5e8f,16r5e87,16r5e8a,16r5ef7,16r5f04,
+16r5f1f,16r5f64,16r5f62,16r5f77,16r5f79,16r5fd8,16r5fcc,16r5fd7,
+16r5fcd,16r5ff1,16r5feb,16r5ff8,16r5fea,16r6212,16r6211,16r6284,
+16r6297,16r6296,16r6280,16r6276,16r6289,16r626d,16r628a,16r627c,
+16r627e,16r6279,16r6273,16r6292,16r626f,16r6298,16r626e,16r6295,
+16r6293,16r6291,16r6286,16r6539,16r653b,16r6538,16r65f1,16r66f4,
+16r675f,16r674e,16r674f,16r6750,16r6751,16r675c,16r6756,16r675e,
+16r6749,16r6746,16r6760,16r6753,16r6757,16r6b65,16r6bcf,16r6c42,
+16r6c5e,16r6c99,16r6c81,16r6c88,16r6c89,16r6c85,16r6c9b,16r6c6a,
+16r6c7a,16r6c90,16r6c70,16r6c8c,16r6c68,16r6c96,16r6c92,16r6c7d,
+16r6c83,16r6c72,16r6c7e,16r6c74,16r6c86,16r6c76,16r6c8d,16r6c94,
+16r6c98,16r6c82,16r7076,16r707c,16r707d,16r7078,16r7262,16r7261,
+16r7260,16r72c4,16r72c2,16r7396,16r752c,16r752b,16r7537,16r7538,
+16r7682,16r76ef,16r77e3,16r79c1,16r79c0,16r79bf,16r7a76,16r7cfb,
+16r7f55,16r8096,16r8093,16r809d,16r8098,16r809b,16r809a,16r80b2,
+16r826f,16r8292,16r828b,16r828d,16r898b,16r89d2,16r8a00,16r8c37,
+16r8c46,16r8c55,16r8c9d,16r8d64,16r8d70,16r8db3,16r8eab,16r8eca,
+16r8f9b,16r8fb0,16r8fc2,16r8fc6,16r8fc5,16r8fc4,16r5de1,16r9091,
+16r90a2,16r90aa,16r90a6,16r90a3,16r9149,16r91c6,16r91cc,16r9632,
+16r962e,16r9631,16r962a,16r962c,16r4e26,16r4e56,16r4e73,16r4e8b,
+16r4e9b,16r4e9e,16r4eab,16r4eac,16r4f6f,16r4f9d,16r4f8d,16r4f73,
+16r4f7f,16r4f6c,16r4f9b,16r4f8b,16r4f86,16r4f83,16r4f70,16r4f75,
+16r4f88,16r4f69,16r4f7b,16r4f96,16r4f7e,16r4f8f,16r4f91,16r4f7a,
+16r5154,16r5152,16r5155,16r5169,16r5177,16r5176,16r5178,16r51bd,
+16r51fd,16r523b,16r5238,16r5237,16r523a,16r5230,16r522e,16r5236,
+16r5241,16r52be,16r52bb,16r5352,16r5354,16r5353,16r5351,16r5366,
+16r5377,16r5378,16r5379,16r53d6,16r53d4,16r53d7,16r5473,16r5475,
+16r5496,16r5478,16r5495,16r5480,16r547b,16r5477,16r5484,16r5492,
+16r5486,16r547c,16r5490,16r5471,16r5476,16r548c,16r549a,16r5462,
+16r5468,16r548b,16r547d,16r548e,16r56fa,16r5783,16r5777,16r576a,
+16r5769,16r5761,16r5766,16r5764,16r577c,16r591c,16r5949,16r5947,
+16r5948,16r5944,16r5954,16r59be,16r59bb,16r59d4,16r59b9,16r59ae,
+16r59d1,16r59c6,16r59d0,16r59cd,16r59cb,16r59d3,16r59ca,16r59af,
+16r59b3,16r59d2,16r59c5,16r5b5f,16r5b64,16r5b63,16r5b97,16r5b9a,
+16r5b98,16r5b9c,16r5b99,16r5b9b,16r5c1a,16r5c48,16r5c45,16r5c46,
+16r5cb7,16r5ca1,16r5cb8,16r5ca9,16r5cab,16r5cb1,16r5cb3,16r5e18,
+16r5e1a,16r5e16,16r5e15,16r5e1b,16r5e11,16r5e78,16r5e9a,16r5e97,
+16r5e9c,16r5e95,16r5e96,16r5ef6,16r5f26,16r5f27,16r5f29,16r5f80,
+16r5f81,16r5f7f,16r5f7c,16r5fdd,16r5fe0,16r5ffd,16r5ff5,16r5fff,
+16r600f,16r6014,16r602f,16r6035,16r6016,16r602a,16r6015,16r6021,
+16r6027,16r6029,16r602b,16r601b,16r6216,16r6215,16r623f,16r623e,
+16r6240,16r627f,16r62c9,16r62cc,16r62c4,16r62bf,16r62c2,16r62b9,
+16r62d2,16r62db,16r62ab,16r62d3,16r62d4,16r62cb,16r62c8,16r62a8,
+16r62bd,16r62bc,16r62d0,16r62d9,16r62c7,16r62cd,16r62b5,16r62da,
+16r62b1,16r62d8,16r62d6,16r62d7,16r62c6,16r62ac,16r62ce,16r653e,
+16r65a7,16r65bc,16r65fa,16r6614,16r6613,16r660c,16r6606,16r6602,
+16r660e,16r6600,16r660f,16r6615,16r660a,16r6607,16r670d,16r670b,
+16r676d,16r678b,16r6795,16r6771,16r679c,16r6773,16r6777,16r6787,
+16r679d,16r6797,16r676f,16r6770,16r677f,16r6789,16r677e,16r6790,
+16r6775,16r679a,16r6793,16r677c,16r676a,16r6772,16r6b23,16r6b66,
+16r6b67,16r6b7f,16r6c13,16r6c1b,16r6ce3,16r6ce8,16r6cf3,16r6cb1,
+16r6ccc,16r6ce5,16r6cb3,16r6cbd,16r6cbe,16r6cbc,16r6ce2,16r6cab,
+16r6cd5,16r6cd3,16r6cb8,16r6cc4,16r6cb9,16r6cc1,16r6cae,16r6cd7,
+16r6cc5,16r6cf1,16r6cbf,16r6cbb,16r6ce1,16r6cdb,16r6cca,16r6cac,
+16r6cef,16r6cdc,16r6cd6,16r6ce0,16r7095,16r708e,16r7092,16r708a,
+16r7099,16r722c,16r722d,16r7238,16r7248,16r7267,16r7269,16r72c0,
+16r72ce,16r72d9,16r72d7,16r72d0,16r73a9,16r73a8,16r739f,16r73ab,
+16r73a5,16r753d,16r759d,16r7599,16r759a,16r7684,16r76c2,16r76f2,
+16r76f4,16r77e5,16r77fd,16r793e,16r7940,16r7941,16r79c9,16r79c8,
+16r7a7a,16r7a79,16r7afa,16r7cfe,16r7f54,16r7f8c,16r7f8b,16r8005,
+16r80ba,16r80a5,16r80a2,16r80b1,16r80a1,16r80ab,16r80a9,16r80b4,
+16r80aa,16r80af,16r81e5,16r81fe,16r820d,16r82b3,16r829d,16r8299,
+16r82ad,16r82bd,16r829f,16r82b9,16r82b1,16r82ac,16r82a5,16r82af,
+16r82b8,16r82a3,16r82b0,16r82be,16r82b7,16r864e,16r8671,16r521d,
+16r8868,16r8ecb,16r8fce,16r8fd4,16r8fd1,16r90b5,16r90b8,16r90b1,
+16r90b6,16r91c7,16r91d1,16r9577,16r9580,16r961c,16r9640,16r963f,
+16r963b,16r9644,16r9642,16r96b9,16r96e8,16r9752,16r975e,16r4e9f,
+16r4ead,16r4eae,16r4fe1,16r4fb5,16r4faf,16r4fbf,16r4fe0,16r4fd1,
+16r4fcf,16r4fdd,16r4fc3,16r4fb6,16r4fd8,16r4fdf,16r4fca,16r4fd7,
+16r4fae,16r4fd0,16r4fc4,16r4fc2,16r4fda,16r4fce,16r4fde,16r4fb7,
+16r5157,16r5192,16r5191,16r51a0,16r524e,16r5243,16r524a,16r524d,
+16r524c,16r524b,16r5247,16r52c7,16r52c9,16r52c3,16r52c1,16r530d,
+16r5357,16r537b,16r539a,16r53db,16r54ac,16r54c0,16r54a8,16r54ce,
+16r54c9,16r54b8,16r54a6,16r54b3,16r54c7,16r54c2,16r54bd,16r54aa,
+16r54c1,16r54c4,16r54c8,16r54af,16r54ab,16r54b1,16r54bb,16r54a9,
+16r54a7,16r54bf,16r56ff,16r5782,16r578b,16r57a0,16r57a3,16r57a2,
+16r57ce,16r57ae,16r5793,16r5955,16r5951,16r594f,16r594e,16r5950,
+16r59dc,16r59d8,16r59ff,16r59e3,16r59e8,16r5a03,16r59e5,16r59ea,
+16r59da,16r59e6,16r5a01,16r59fb,16r5b69,16r5ba3,16r5ba6,16r5ba4,
+16r5ba2,16r5ba5,16r5c01,16r5c4e,16r5c4f,16r5c4d,16r5c4b,16r5cd9,
+16r5cd2,16r5df7,16r5e1d,16r5e25,16r5e1f,16r5e7d,16r5ea0,16r5ea6,
+16r5efa,16r5f08,16r5f2d,16r5f65,16r5f88,16r5f85,16r5f8a,16r5f8b,
+16r5f87,16r5f8c,16r5f89,16r6012,16r601d,16r6020,16r6025,16r600e,
+16r6028,16r604d,16r6070,16r6068,16r6062,16r6046,16r6043,16r606c,
+16r606b,16r606a,16r6064,16r6241,16r62dc,16r6316,16r6309,16r62fc,
+16r62ed,16r6301,16r62ee,16r62fd,16r6307,16r62f1,16r62f7,16r62ef,
+16r62ec,16r62fe,16r62f4,16r6311,16r6302,16r653f,16r6545,16r65ab,
+16r65bd,16r65e2,16r6625,16r662d,16r6620,16r6627,16r662f,16r661f,
+16r6628,16r6631,16r6624,16r66f7,16r67ff,16r67d3,16r67f1,16r67d4,
+16r67d0,16r67ec,16r67b6,16r67af,16r67f5,16r67e9,16r67ef,16r67c4,
+16r67d1,16r67b4,16r67da,16r67e5,16r67b8,16r67cf,16r67de,16r67f3,
+16r67b0,16r67d9,16r67e2,16r67dd,16r67d2,16r6b6a,16r6b83,16r6b86,
+16r6bb5,16r6bd2,16r6bd7,16r6c1f,16r6cc9,16r6d0b,16r6d32,16r6d2a,
+16r6d41,16r6d25,16r6d0c,16r6d31,16r6d1e,16r6d17,16r6d3b,16r6d3d,
+16r6d3e,16r6d36,16r6d1b,16r6cf5,16r6d39,16r6d27,16r6d38,16r6d29,
+16r6d2e,16r6d35,16r6d0e,16r6d2b,16r70ab,16r70ba,16r70b3,16r70ac,
+16r70af,16r70ad,16r70b8,16r70ae,16r70a4,16r7230,16r7272,16r726f,
+16r7274,16r72e9,16r72e0,16r72e1,16r73b7,16r73ca,16r73bb,16r73b2,
+16r73cd,16r73c0,16r73b3,16r751a,16r752d,16r754f,16r754c,16r754e,
+16r754b,16r75ab,16r75a4,16r75a5,16r75a2,16r75a3,16r7678,16r7686,
+16r7687,16r7688,16r76c8,16r76c6,16r76c3,16r76c5,16r7701,16r76f9,
+16r76f8,16r7709,16r770b,16r76fe,16r76fc,16r7707,16r77dc,16r7802,
+16r7814,16r780c,16r780d,16r7946,16r7949,16r7948,16r7947,16r79b9,
+16r79ba,16r79d1,16r79d2,16r79cb,16r7a7f,16r7a81,16r7aff,16r7afd,
+16r7c7d,16r7d02,16r7d05,16r7d00,16r7d09,16r7d07,16r7d04,16r7d06,
+16r7f38,16r7f8e,16r7fbf,16r8004,16r8010,16r800d,16r8011,16r8036,
+16r80d6,16r80e5,16r80da,16r80c3,16r80c4,16r80cc,16r80e1,16r80db,
+16r80ce,16r80de,16r80e4,16r80dd,16r81f4,16r8222,16r82e7,16r8303,
+16r8305,16r82e3,16r82db,16r82e6,16r8304,16r82e5,16r8302,16r8309,
+16r82d2,16r82d7,16r82f1,16r8301,16r82dc,16r82d4,16r82d1,16r82de,
+16r82d3,16r82df,16r82ef,16r8306,16r8650,16r8679,16r867b,16r867a,
+16r884d,16r886b,16r8981,16r89d4,16r8a08,16r8a02,16r8a03,16r8c9e,
+16r8ca0,16r8d74,16r8d73,16r8db4,16r8ecd,16r8ecc,16r8ff0,16r8fe6,
+16r8fe2,16r8fea,16r8fe5,16r8fed,16r8feb,16r8fe4,16r8fe8,16r90ca,
+16r90ce,16r90c1,16r90c3,16r914b,16r914a,16r91cd,16r9582,16r9650,
+16r964b,16r964c,16r964d,16r9762,16r9769,16r97cb,16r97ed,16r97f3,
+16r9801,16r98a8,16r98db,16r98df,16r9996,16r9999,16r4e58,16r4eb3,
+16r500c,16r500d,16r5023,16r4fef,16r5026,16r5025,16r4ff8,16r5029,
+16r5016,16r5006,16r503c,16r501f,16r501a,16r5012,16r5011,16r4ffa,
+16r5000,16r5014,16r5028,16r4ff1,16r5021,16r500b,16r5019,16r5018,
+16r4ff3,16r4fee,16r502d,16r502a,16r4ffe,16r502b,16r5009,16r517c,
+16r51a4,16r51a5,16r51a2,16r51cd,16r51cc,16r51c6,16r51cb,16r5256,
+16r525c,16r5254,16r525b,16r525d,16r532a,16r537f,16r539f,16r539d,
+16r53df,16r54e8,16r5510,16r5501,16r5537,16r54fc,16r54e5,16r54f2,
+16r5506,16r54fa,16r5514,16r54e9,16r54ed,16r54e1,16r5509,16r54ee,
+16r54ea,16r54e6,16r5527,16r5507,16r54fd,16r550f,16r5703,16r5704,
+16r57c2,16r57d4,16r57cb,16r57c3,16r5809,16r590f,16r5957,16r5958,
+16r595a,16r5a11,16r5a18,16r5a1c,16r5a1f,16r5a1b,16r5a13,16r59ec,
+16r5a20,16r5a23,16r5a29,16r5a25,16r5a0c,16r5a09,16r5b6b,16r5c58,
+16r5bb0,16r5bb3,16r5bb6,16r5bb4,16r5bae,16r5bb5,16r5bb9,16r5bb8,
+16r5c04,16r5c51,16r5c55,16r5c50,16r5ced,16r5cfd,16r5cfb,16r5cea,
+16r5ce8,16r5cf0,16r5cf6,16r5d01,16r5cf4,16r5dee,16r5e2d,16r5e2b,
+16r5eab,16r5ead,16r5ea7,16r5f31,16r5f92,16r5f91,16r5f90,16r6059,
+16r6063,16r6065,16r6050,16r6055,16r606d,16r6069,16r606f,16r6084,
+16r609f,16r609a,16r608d,16r6094,16r608c,16r6085,16r6096,16r6247,
+16r62f3,16r6308,16r62ff,16r634e,16r633e,16r632f,16r6355,16r6342,
+16r6346,16r634f,16r6349,16r633a,16r6350,16r633d,16r632a,16r632b,
+16r6328,16r634d,16r634c,16r6548,16r6549,16r6599,16r65c1,16r65c5,
+16r6642,16r6649,16r664f,16r6643,16r6652,16r664c,16r6645,16r6641,
+16r66f8,16r6714,16r6715,16r6717,16r6821,16r6838,16r6848,16r6846,
+16r6853,16r6839,16r6842,16r6854,16r6829,16r68b3,16r6817,16r684c,
+16r6851,16r683d,16r67f4,16r6850,16r6840,16r683c,16r6843,16r682a,
+16r6845,16r6813,16r6818,16r6841,16r6b8a,16r6b89,16r6bb7,16r6c23,
+16r6c27,16r6c28,16r6c26,16r6c24,16r6cf0,16r6d6a,16r6d95,16r6d88,
+16r6d87,16r6d66,16r6d78,16r6d77,16r6d59,16r6d93,16r6d6c,16r6d89,
+16r6d6e,16r6d5a,16r6d74,16r6d69,16r6d8c,16r6d8a,16r6d79,16r6d85,
+16r6d65,16r6d94,16r70ca,16r70d8,16r70e4,16r70d9,16r70c8,16r70cf,
+16r7239,16r7279,16r72fc,16r72f9,16r72fd,16r72f8,16r72f7,16r7386,
+16r73ed,16r7409,16r73ee,16r73e0,16r73ea,16r73de,16r7554,16r755d,
+16r755c,16r755a,16r7559,16r75be,16r75c5,16r75c7,16r75b2,16r75b3,
+16r75bd,16r75bc,16r75b9,16r75c2,16r75b8,16r768b,16r76b0,16r76ca,
+16r76cd,16r76ce,16r7729,16r771f,16r7720,16r7728,16r77e9,16r7830,
+16r7827,16r7838,16r781d,16r7834,16r7837,16r7825,16r782d,16r7820,
+16r781f,16r7832,16r7955,16r7950,16r7960,16r795f,16r7956,16r795e,
+16r795d,16r7957,16r795a,16r79e4,16r79e3,16r79e7,16r79df,16r79e6,
+16r79e9,16r79d8,16r7a84,16r7a88,16r7ad9,16r7b06,16r7b11,16r7c89,
+16r7d21,16r7d17,16r7d0b,16r7d0a,16r7d20,16r7d22,16r7d14,16r7d10,
+16r7d15,16r7d1a,16r7d1c,16r7d0d,16r7d19,16r7d1b,16r7f3a,16r7f5f,
+16r7f94,16r7fc5,16r7fc1,16r8006,16r8018,16r8015,16r8019,16r8017,
+16r803d,16r803f,16r80f1,16r8102,16r80f0,16r8105,16r80ed,16r80f4,
+16r8106,16r80f8,16r80f3,16r8108,16r80fd,16r810a,16r80fc,16r80ef,
+16r81ed,16r81ec,16r8200,16r8210,16r822a,16r822b,16r8228,16r822c,
+16r82bb,16r832b,16r8352,16r8354,16r834a,16r8338,16r8350,16r8349,
+16r8335,16r8334,16r834f,16r8332,16r8339,16r8336,16r8317,16r8340,
+16r8331,16r8328,16r8343,16r8654,16r868a,16r86aa,16r8693,16r86a4,
+16r86a9,16r868c,16r86a3,16r869c,16r8870,16r8877,16r8881,16r8882,
+16r887d,16r8879,16r8a18,16r8a10,16r8a0e,16r8a0c,16r8a15,16r8a0a,
+16r8a17,16r8a13,16r8a16,16r8a0f,16r8a11,16r8c48,16r8c7a,16r8c79,
+16r8ca1,16r8ca2,16r8d77,16r8eac,16r8ed2,16r8ed4,16r8ecf,16r8fb1,
+16r9001,16r9006,16r8ff7,16r9000,16r8ffa,16r8ff4,16r9003,16r8ffd,
+16r9005,16r8ff8,16r9095,16r90e1,16r90dd,16r90e2,16r9152,16r914d,
+16r914c,16r91d8,16r91dd,16r91d7,16r91dc,16r91d9,16r9583,16r9662,
+16r9663,16r9661,16r965b,16r965d,16r9664,16r9658,16r965e,16r96bb,
+16r98e2,16r99ac,16r9aa8,16r9ad8,16r9b25,16r9b32,16r9b3c,16r4e7e,
+16r507a,16r507d,16r505c,16r5047,16r5043,16r504c,16r505a,16r5049,
+16r5065,16r5076,16r504e,16r5055,16r5075,16r5074,16r5077,16r504f,
+16r500f,16r506f,16r506d,16r515c,16r5195,16r51f0,16r526a,16r526f,
+16r52d2,16r52d9,16r52d8,16r52d5,16r5310,16r530f,16r5319,16r533f,
+16r5340,16r533e,16r53c3,16r66fc,16r5546,16r556a,16r5566,16r5544,
+16r555e,16r5561,16r5543,16r554a,16r5531,16r5556,16r554f,16r5555,
+16r552f,16r5564,16r5538,16r552e,16r555c,16r552c,16r5563,16r5533,
+16r5541,16r5557,16r5708,16r570b,16r5709,16r57df,16r5805,16r580a,
+16r5806,16r57e0,16r57e4,16r57fa,16r5802,16r5835,16r57f7,16r57f9,
+16r5920,16r5962,16r5a36,16r5a41,16r5a49,16r5a66,16r5a6a,16r5a40,
+16r5a3c,16r5a62,16r5a5a,16r5a46,16r5a4a,16r5b70,16r5bc7,16r5bc5,
+16r5bc4,16r5bc2,16r5bbf,16r5bc6,16r5c09,16r5c08,16r5c07,16r5c60,
+16r5c5c,16r5c5d,16r5d07,16r5d06,16r5d0e,16r5d1b,16r5d16,16r5d22,
+16r5d11,16r5d29,16r5d14,16r5d19,16r5d24,16r5d27,16r5d17,16r5de2,
+16r5e38,16r5e36,16r5e33,16r5e37,16r5eb7,16r5eb8,16r5eb6,16r5eb5,
+16r5ebe,16r5f35,16r5f37,16r5f57,16r5f6c,16r5f69,16r5f6b,16r5f97,
+16r5f99,16r5f9e,16r5f98,16r5fa1,16r5fa0,16r5f9c,16r607f,16r60a3,
+16r6089,16r60a0,16r60a8,16r60cb,16r60b4,16r60e6,16r60bd,16r60c5,
+16r60bb,16r60b5,16r60dc,16r60bc,16r60d8,16r60d5,16r60c6,16r60df,
+16r60b8,16r60da,16r60c7,16r621a,16r621b,16r6248,16r63a0,16r63a7,
+16r6372,16r6396,16r63a2,16r63a5,16r6377,16r6367,16r6398,16r63aa,
+16r6371,16r63a9,16r6389,16r6383,16r639b,16r636b,16r63a8,16r6384,
+16r6388,16r6399,16r63a1,16r63ac,16r6392,16r638f,16r6380,16r637b,
+16r6369,16r6368,16r637a,16r655d,16r6556,16r6551,16r6559,16r6557,
+16r555f,16r654f,16r6558,16r6555,16r6554,16r659c,16r659b,16r65ac,
+16r65cf,16r65cb,16r65cc,16r65ce,16r665d,16r665a,16r6664,16r6668,
+16r6666,16r665e,16r66f9,16r52d7,16r671b,16r6881,16r68af,16r68a2,
+16r6893,16r68b5,16r687f,16r6876,16r68b1,16r68a7,16r6897,16r68b0,
+16r6883,16r68c4,16r68ad,16r6886,16r6885,16r6894,16r689d,16r68a8,
+16r689f,16r68a1,16r6882,16r6b32,16r6bba,16r6beb,16r6bec,16r6c2b,
+16r6d8e,16r6dbc,16r6df3,16r6dd9,16r6db2,16r6de1,16r6dcc,16r6de4,
+16r6dfb,16r6dfa,16r6e05,16r6dc7,16r6dcb,16r6daf,16r6dd1,16r6dae,
+16r6dde,16r6df9,16r6db8,16r6df7,16r6df5,16r6dc5,16r6dd2,16r6e1a,
+16r6db5,16r6dda,16r6deb,16r6dd8,16r6dea,16r6df1,16r6dee,16r6de8,
+16r6dc6,16r6dc4,16r6daa,16r6dec,16r6dbf,16r6de6,16r70f9,16r7109,
+16r710a,16r70fd,16r70ef,16r723d,16r727d,16r7281,16r731c,16r731b,
+16r7316,16r7313,16r7319,16r7387,16r7405,16r740a,16r7403,16r7406,
+16r73fe,16r740d,16r74e0,16r74f6,16r74f7,16r751c,16r7522,16r7565,
+16r7566,16r7562,16r7570,16r758f,16r75d4,16r75d5,16r75b5,16r75ca,
+16r75cd,16r768e,16r76d4,16r76d2,16r76db,16r7737,16r773e,16r773c,
+16r7736,16r7738,16r773a,16r786b,16r7843,16r784e,16r7965,16r7968,
+16r796d,16r79fb,16r7a92,16r7a95,16r7b20,16r7b28,16r7b1b,16r7b2c,
+16r7b26,16r7b19,16r7b1e,16r7b2e,16r7c92,16r7c97,16r7c95,16r7d46,
+16r7d43,16r7d71,16r7d2e,16r7d39,16r7d3c,16r7d40,16r7d30,16r7d33,
+16r7d44,16r7d2f,16r7d42,16r7d32,16r7d31,16r7f3d,16r7f9e,16r7f9a,
+16r7fcc,16r7fce,16r7fd2,16r801c,16r804a,16r8046,16r812f,16r8116,
+16r8123,16r812b,16r8129,16r8130,16r8124,16r8202,16r8235,16r8237,
+16r8236,16r8239,16r838e,16r839e,16r8398,16r8378,16r83a2,16r8396,
+16r83bd,16r83ab,16r8392,16r838a,16r8393,16r8389,16r83a0,16r8377,
+16r837b,16r837c,16r8386,16r83a7,16r8655,16r5f6a,16r86c7,16r86c0,
+16r86b6,16r86c4,16r86b5,16r86c6,16r86cb,16r86b1,16r86af,16r86c9,
+16r8853,16r889e,16r8888,16r88ab,16r8892,16r8896,16r888d,16r888b,
+16r8993,16r898f,16r8a2a,16r8a1d,16r8a23,16r8a25,16r8a31,16r8a2d,
+16r8a1f,16r8a1b,16r8a22,16r8c49,16r8c5a,16r8ca9,16r8cac,16r8cab,
+16r8ca8,16r8caa,16r8ca7,16r8d67,16r8d66,16r8dbe,16r8dba,16r8edb,
+16r8edf,16r9019,16r900d,16r901a,16r9017,16r9023,16r901f,16r901d,
+16r9010,16r9015,16r901e,16r9020,16r900f,16r9022,16r9016,16r901b,
+16r9014,16r90e8,16r90ed,16r90fd,16r9157,16r91ce,16r91f5,16r91e6,
+16r91e3,16r91e7,16r91ed,16r91e9,16r9589,16r966a,16r9675,16r9673,
+16r9678,16r9670,16r9674,16r9676,16r9677,16r966c,16r96c0,16r96ea,
+16r96e9,16r7ae0,16r7adf,16r9802,16r9803,16r9b5a,16r9ce5,16r9e75,
+16r9e7f,16r9ea5,16r9ebb,16r50a2,16r508d,16r5085,16r5099,16r5091,
+16r5080,16r5096,16r5098,16r509a,16r6700,16r51f1,16r5272,16r5274,
+16r5275,16r5269,16r52de,16r52dd,16r52db,16r535a,16r53a5,16r557b,
+16r5580,16r55a7,16r557c,16r558a,16r559d,16r5598,16r5582,16r559c,
+16r55aa,16r5594,16r5587,16r558b,16r5583,16r55b3,16r55ae,16r559f,
+16r553e,16r55b2,16r559a,16r55bb,16r55ac,16r55b1,16r557e,16r5589,
+16r55ab,16r5599,16r570d,16r582f,16r582a,16r5834,16r5824,16r5830,
+16r5831,16r5821,16r581d,16r5820,16r58f9,16r58fa,16r5960,16r5a77,
+16r5a9a,16r5a7f,16r5a92,16r5a9b,16r5aa7,16r5b73,16r5b71,16r5bd2,
+16r5bcc,16r5bd3,16r5bd0,16r5c0a,16r5c0b,16r5c31,16r5d4c,16r5d50,
+16r5d34,16r5d47,16r5dfd,16r5e45,16r5e3d,16r5e40,16r5e43,16r5e7e,
+16r5eca,16r5ec1,16r5ec2,16r5ec4,16r5f3c,16r5f6d,16r5fa9,16r5faa,
+16r5fa8,16r60d1,16r60e1,16r60b2,16r60b6,16r60e0,16r611c,16r6123,
+16r60fa,16r6115,16r60f0,16r60fb,16r60f4,16r6168,16r60f1,16r610e,
+16r60f6,16r6109,16r6100,16r6112,16r621f,16r6249,16r63a3,16r638c,
+16r63cf,16r63c0,16r63e9,16r63c9,16r63c6,16r63cd,16r63d2,16r63e3,
+16r63d0,16r63e1,16r63d6,16r63ed,16r63ee,16r6376,16r63f4,16r63ea,
+16r63db,16r6452,16r63da,16r63f9,16r655e,16r6566,16r6562,16r6563,
+16r6591,16r6590,16r65af,16r666e,16r6670,16r6674,16r6676,16r666f,
+16r6691,16r667a,16r667e,16r6677,16r66fe,16r66ff,16r671f,16r671d,
+16r68fa,16r68d5,16r68e0,16r68d8,16r68d7,16r6905,16r68df,16r68f5,
+16r68ee,16r68e7,16r68f9,16r68d2,16r68f2,16r68e3,16r68cb,16r68cd,
+16r690d,16r6912,16r690e,16r68c9,16r68da,16r696e,16r68fb,16r6b3e,
+16r6b3a,16r6b3d,16r6b98,16r6b96,16r6bbc,16r6bef,16r6c2e,16r6c2f,
+16r6c2c,16r6e2f,16r6e38,16r6e54,16r6e21,16r6e32,16r6e67,16r6e4a,
+16r6e20,16r6e25,16r6e23,16r6e1b,16r6e5b,16r6e58,16r6e24,16r6e56,
+16r6e6e,16r6e2d,16r6e26,16r6e6f,16r6e34,16r6e4d,16r6e3a,16r6e2c,
+16r6e43,16r6e1d,16r6e3e,16r6ecb,16r6e89,16r6e19,16r6e4e,16r6e63,
+16r6e44,16r6e72,16r6e69,16r6e5f,16r7119,16r711a,16r7126,16r7130,
+16r7121,16r7136,16r716e,16r711c,16r724c,16r7284,16r7280,16r7336,
+16r7325,16r7334,16r7329,16r743a,16r742a,16r7433,16r7422,16r7425,
+16r7435,16r7436,16r7434,16r742f,16r741b,16r7426,16r7428,16r7525,
+16r7526,16r756b,16r756a,16r75e2,16r75db,16r75e3,16r75d9,16r75d8,
+16r75de,16r75e0,16r767b,16r767c,16r7696,16r7693,16r76b4,16r76dc,
+16r774f,16r77ed,16r785d,16r786c,16r786f,16r7a0d,16r7a08,16r7a0b,
+16r7a05,16r7a00,16r7a98,16r7a97,16r7a96,16r7ae5,16r7ae3,16r7b49,
+16r7b56,16r7b46,16r7b50,16r7b52,16r7b54,16r7b4d,16r7b4b,16r7b4f,
+16r7b51,16r7c9f,16r7ca5,16r7d5e,16r7d50,16r7d68,16r7d55,16r7d2b,
+16r7d6e,16r7d72,16r7d61,16r7d66,16r7d62,16r7d70,16r7d73,16r5584,
+16r7fd4,16r7fd5,16r800b,16r8052,16r8085,16r8155,16r8154,16r814b,
+16r8151,16r814e,16r8139,16r8146,16r813e,16r814c,16r8153,16r8174,
+16r8212,16r821c,16r83e9,16r8403,16r83f8,16r840d,16r83e0,16r83c5,
+16r840b,16r83c1,16r83ef,16r83f1,16r83f4,16r8457,16r840a,16r83f0,
+16r840c,16r83cc,16r83fd,16r83f2,16r83ca,16r8438,16r840e,16r8404,
+16r83dc,16r8407,16r83d4,16r83df,16r865b,16r86df,16r86d9,16r86ed,
+16r86d4,16r86db,16r86e4,16r86d0,16r86de,16r8857,16r88c1,16r88c2,
+16r88b1,16r8983,16r8996,16r8a3b,16r8a60,16r8a55,16r8a5e,16r8a3c,
+16r8a41,16r8a54,16r8a5b,16r8a50,16r8a46,16r8a34,16r8a3a,16r8a36,
+16r8a56,16r8c61,16r8c82,16r8caf,16r8cbc,16r8cb3,16r8cbd,16r8cc1,
+16r8cbb,16r8cc0,16r8cb4,16r8cb7,16r8cb6,16r8cbf,16r8cb8,16r8d8a,
+16r8d85,16r8d81,16r8dce,16r8ddd,16r8dcb,16r8dda,16r8dd1,16r8dcc,
+16r8ddb,16r8dc6,16r8efb,16r8ef8,16r8efc,16r8f9c,16r902e,16r9035,
+16r9031,16r9038,16r9032,16r9036,16r9102,16r90f5,16r9109,16r90fe,
+16r9163,16r9165,16r91cf,16r9214,16r9215,16r9223,16r9209,16r921e,
+16r920d,16r9210,16r9207,16r9211,16r9594,16r958f,16r958b,16r9591,
+16r9593,16r9592,16r958e,16r968a,16r968e,16r968b,16r967d,16r9685,
+16r9686,16r968d,16r9672,16r9684,16r96c1,16r96c5,16r96c4,16r96c6,
+16r96c7,16r96ef,16r96f2,16r97cc,16r9805,16r9806,16r9808,16r98e7,
+16r98ea,16r98ef,16r98e9,16r98f2,16r98ed,16r99ae,16r99ad,16r9ec3,
+16r9ecd,16r9ed1,16r4e82,16r50ad,16r50b5,16r50b2,16r50b3,16r50c5,
+16r50be,16r50ac,16r50b7,16r50bb,16r50af,16r50c7,16r527f,16r5277,
+16r527d,16r52df,16r52e6,16r52e4,16r52e2,16r52e3,16r532f,16r55df,
+16r55e8,16r55d3,16r55e6,16r55ce,16r55dc,16r55c7,16r55d1,16r55e3,
+16r55e4,16r55ef,16r55da,16r55e1,16r55c5,16r55c6,16r55e5,16r55c9,
+16r5712,16r5713,16r585e,16r5851,16r5858,16r5857,16r585a,16r5854,
+16r586b,16r584c,16r586d,16r584a,16r5862,16r5852,16r584b,16r5967,
+16r5ac1,16r5ac9,16r5acc,16r5abe,16r5abd,16r5abc,16r5ab3,16r5ac2,
+16r5ab2,16r5d69,16r5d6f,16r5e4c,16r5e79,16r5ec9,16r5ec8,16r5f12,
+16r5f59,16r5fac,16r5fae,16r611a,16r610f,16r6148,16r611f,16r60f3,
+16r611b,16r60f9,16r6101,16r6108,16r614e,16r614c,16r6144,16r614d,
+16r613e,16r6134,16r6127,16r610d,16r6106,16r6137,16r6221,16r6222,
+16r6413,16r643e,16r641e,16r642a,16r642d,16r643d,16r642c,16r640f,
+16r641c,16r6414,16r640d,16r6436,16r6416,16r6417,16r6406,16r656c,
+16r659f,16r65b0,16r6697,16r6689,16r6687,16r6688,16r6696,16r6684,
+16r6698,16r668d,16r6703,16r6994,16r696d,16r695a,16r6977,16r6960,
+16r6954,16r6975,16r6930,16r6982,16r694a,16r6968,16r696b,16r695e,
+16r6953,16r6979,16r6986,16r695d,16r6963,16r695b,16r6b47,16r6b72,
+16r6bc0,16r6bbf,16r6bd3,16r6bfd,16r6ea2,16r6eaf,16r6ed3,16r6eb6,
+16r6ec2,16r6e90,16r6e9d,16r6ec7,16r6ec5,16r6ea5,16r6e98,16r6ebc,
+16r6eba,16r6eab,16r6ed1,16r6e96,16r6e9c,16r6ec4,16r6ed4,16r6eaa,
+16r6ea7,16r6eb4,16r714e,16r7159,16r7169,16r7164,16r7149,16r7167,
+16r715c,16r716c,16r7166,16r714c,16r7165,16r715e,16r7146,16r7168,
+16r7156,16r723a,16r7252,16r7337,16r7345,16r733f,16r733e,16r746f,
+16r745a,16r7455,16r745f,16r745e,16r7441,16r743f,16r7459,16r745b,
+16r745c,16r7576,16r7578,16r7600,16r75f0,16r7601,16r75f2,16r75f1,
+16r75fa,16r75ff,16r75f4,16r75f3,16r76de,16r76df,16r775b,16r776b,
+16r7766,16r775e,16r7763,16r7779,16r776a,16r776c,16r775c,16r7765,
+16r7768,16r7762,16r77ee,16r788e,16r78b0,16r7897,16r7898,16r788c,
+16r7889,16r787c,16r7891,16r7893,16r787f,16r797a,16r797f,16r7981,
+16r842c,16r79bd,16r7a1c,16r7a1a,16r7a20,16r7a14,16r7a1f,16r7a1e,
+16r7a9f,16r7aa0,16r7b77,16r7bc0,16r7b60,16r7b6e,16r7b67,16r7cb1,
+16r7cb3,16r7cb5,16r7d93,16r7d79,16r7d91,16r7d81,16r7d8f,16r7d5b,
+16r7f6e,16r7f69,16r7f6a,16r7f72,16r7fa9,16r7fa8,16r7fa4,16r8056,
+16r8058,16r8086,16r8084,16r8171,16r8170,16r8178,16r8165,16r816e,
+16r8173,16r816b,16r8179,16r817a,16r8166,16r8205,16r8247,16r8482,
+16r8477,16r843d,16r8431,16r8475,16r8466,16r846b,16r8449,16r846c,
+16r845b,16r843c,16r8435,16r8461,16r8463,16r8469,16r846d,16r8446,
+16r865e,16r865c,16r865f,16r86f9,16r8713,16r8708,16r8707,16r8700,
+16r86fe,16r86fb,16r8702,16r8703,16r8706,16r870a,16r8859,16r88df,
+16r88d4,16r88d9,16r88dc,16r88d8,16r88dd,16r88e1,16r88ca,16r88d5,
+16r88d2,16r899c,16r89e3,16r8a6b,16r8a72,16r8a73,16r8a66,16r8a69,
+16r8a70,16r8a87,16r8a7c,16r8a63,16r8aa0,16r8a71,16r8a85,16r8a6d,
+16r8a62,16r8a6e,16r8a6c,16r8a79,16r8a7b,16r8a3e,16r8a68,16r8c62,
+16r8c8a,16r8c89,16r8cca,16r8cc7,16r8cc8,16r8cc4,16r8cb2,16r8cc3,
+16r8cc2,16r8cc5,16r8de1,16r8ddf,16r8de8,16r8def,16r8df3,16r8dfa,
+16r8dea,16r8de4,16r8de6,16r8eb2,16r8f03,16r8f09,16r8efe,16r8f0a,
+16r8f9f,16r8fb2,16r904b,16r904a,16r9053,16r9042,16r9054,16r903c,
+16r9055,16r9050,16r9047,16r904f,16r904e,16r904d,16r9051,16r903e,
+16r9041,16r9112,16r9117,16r916c,16r916a,16r9169,16r91c9,16r9237,
+16r9257,16r9238,16r923d,16r9240,16r923e,16r925b,16r924b,16r9264,
+16r9251,16r9234,16r9249,16r924d,16r9245,16r9239,16r923f,16r925a,
+16r9598,16r9698,16r9694,16r9695,16r96cd,16r96cb,16r96c9,16r96ca,
+16r96f7,16r96fb,16r96f9,16r96f6,16r9756,16r9774,16r9776,16r9810,
+16r9811,16r9813,16r980a,16r9812,16r980c,16r98fc,16r98f4,16r98fd,
+16r98fe,16r99b3,16r99b1,16r99b4,16r9ae1,16r9ce9,16r9e82,16r9f0e,
+16r9f13,16r9f20,16r50e7,16r50ee,16r50e5,16r50d6,16r50ed,16r50da,
+16r50d5,16r50cf,16r50d1,16r50f1,16r50ce,16r50e9,16r5162,16r51f3,
+16r5283,16r5282,16r5331,16r53ad,16r55fe,16r5600,16r561b,16r5617,
+16r55fd,16r5614,16r5606,16r5609,16r560d,16r560e,16r55f7,16r5616,
+16r561f,16r5608,16r5610,16r55f6,16r5718,16r5716,16r5875,16r587e,
+16r5883,16r5893,16r588a,16r5879,16r5885,16r587d,16r58fd,16r5925,
+16r5922,16r5924,16r596a,16r5969,16r5ae1,16r5ae6,16r5ae9,16r5ad7,
+16r5ad6,16r5ad8,16r5ae3,16r5b75,16r5bde,16r5be7,16r5be1,16r5be5,
+16r5be6,16r5be8,16r5be2,16r5be4,16r5bdf,16r5c0d,16r5c62,16r5d84,
+16r5d87,16r5e5b,16r5e63,16r5e55,16r5e57,16r5e54,16r5ed3,16r5ed6,
+16r5f0a,16r5f46,16r5f70,16r5fb9,16r6147,16r613f,16r614b,16r6177,
+16r6162,16r6163,16r615f,16r615a,16r6158,16r6175,16r622a,16r6487,
+16r6458,16r6454,16r64a4,16r6478,16r645f,16r647a,16r6451,16r6467,
+16r6434,16r646d,16r647b,16r6572,16r65a1,16r65d7,16r65d6,16r66a2,
+16r66a8,16r669d,16r699c,16r69a8,16r6995,16r69c1,16r69ae,16r69d3,
+16r69cb,16r699b,16r69b7,16r69bb,16r69ab,16r69b4,16r69d0,16r69cd,
+16r69ad,16r69cc,16r69a6,16r69c3,16r69a3,16r6b49,16r6b4c,16r6c33,
+16r6f33,16r6f14,16r6efe,16r6f13,16r6ef4,16r6f29,16r6f3e,16r6f20,
+16r6f2c,16r6f0f,16r6f02,16r6f22,16r6eff,16r6eef,16r6f06,16r6f31,
+16r6f38,16r6f32,16r6f23,16r6f15,16r6f2b,16r6f2f,16r6f88,16r6f2a,
+16r6eec,16r6f01,16r6ef2,16r6ecc,16r6ef7,16r7194,16r7199,16r717d,
+16r718a,16r7184,16r7192,16r723e,16r7292,16r7296,16r7344,16r7350,
+16r7464,16r7463,16r746a,16r7470,16r746d,16r7504,16r7591,16r7627,
+16r760d,16r760b,16r7609,16r7613,16r76e1,16r76e3,16r7784,16r777d,
+16r777f,16r7761,16r78c1,16r789f,16r78a7,16r78b3,16r78a9,16r78a3,
+16r798e,16r798f,16r798d,16r7a2e,16r7a31,16r7aaa,16r7aa9,16r7aed,
+16r7aef,16r7ba1,16r7b95,16r7b8b,16r7b75,16r7b97,16r7b9d,16r7b94,
+16r7b8f,16r7bb8,16r7b87,16r7b84,16r7cb9,16r7cbd,16r7cbe,16r7dbb,
+16r7db0,16r7d9c,16r7dbd,16r7dbe,16r7da0,16r7dca,16r7db4,16r7db2,
+16r7db1,16r7dba,16r7da2,16r7dbf,16r7db5,16r7db8,16r7dad,16r7dd2,
+16r7dc7,16r7dac,16r7f70,16r7fe0,16r7fe1,16r7fdf,16r805e,16r805a,
+16r8087,16r8150,16r8180,16r818f,16r8188,16r818a,16r817f,16r8182,
+16r81e7,16r81fa,16r8207,16r8214,16r821e,16r824b,16r84c9,16r84bf,
+16r84c6,16r84c4,16r8499,16r849e,16r84b2,16r849c,16r84cb,16r84b8,
+16r84c0,16r84d3,16r8490,16r84bc,16r84d1,16r84ca,16r873f,16r871c,
+16r873b,16r8722,16r8725,16r8734,16r8718,16r8755,16r8737,16r8729,
+16r88f3,16r8902,16r88f4,16r88f9,16r88f8,16r88fd,16r88e8,16r891a,
+16r88ef,16r8aa6,16r8a8c,16r8a9e,16r8aa3,16r8a8d,16r8aa1,16r8a93,
+16r8aa4,16r8aaa,16r8aa5,16r8aa8,16r8a98,16r8a91,16r8a9a,16r8aa7,
+16r8c6a,16r8c8d,16r8c8c,16r8cd3,16r8cd1,16r8cd2,16r8d6b,16r8d99,
+16r8d95,16r8dfc,16r8f14,16r8f12,16r8f15,16r8f13,16r8fa3,16r9060,
+16r9058,16r905c,16r9063,16r9059,16r905e,16r9062,16r905d,16r905b,
+16r9119,16r9118,16r911e,16r9175,16r9178,16r9177,16r9174,16r9278,
+16r9280,16r9285,16r9298,16r9296,16r927b,16r9293,16r929c,16r92a8,
+16r927c,16r9291,16r95a1,16r95a8,16r95a9,16r95a3,16r95a5,16r95a4,
+16r9699,16r969c,16r969b,16r96cc,16r96d2,16r9700,16r977c,16r9785,
+16r97f6,16r9817,16r9818,16r98af,16r98b1,16r9903,16r9905,16r990c,
+16r9909,16r99c1,16r9aaf,16r9ab0,16r9ae6,16r9b41,16r9b42,16r9cf4,
+16r9cf6,16r9cf3,16r9ebc,16r9f3b,16r9f4a,16r5104,16r5100,16r50fb,
+16r50f5,16r50f9,16r5102,16r5108,16r5109,16r5105,16r51dc,16r5287,
+16r5288,16r5289,16r528d,16r528a,16r52f0,16r53b2,16r562e,16r563b,
+16r5639,16r5632,16r563f,16r5634,16r5629,16r5653,16r564e,16r5657,
+16r5674,16r5636,16r562f,16r5630,16r5880,16r589f,16r589e,16r58b3,
+16r589c,16r58ae,16r58a9,16r58a6,16r596d,16r5b09,16r5afb,16r5b0b,
+16r5af5,16r5b0c,16r5b08,16r5bee,16r5bec,16r5be9,16r5beb,16r5c64,
+16r5c65,16r5d9d,16r5d94,16r5e62,16r5e5f,16r5e61,16r5ee2,16r5eda,
+16r5edf,16r5edd,16r5ee3,16r5ee0,16r5f48,16r5f71,16r5fb7,16r5fb5,
+16r6176,16r6167,16r616e,16r615d,16r6155,16r6182,16r617c,16r6170,
+16r616b,16r617e,16r61a7,16r6190,16r61ab,16r618e,16r61ac,16r619a,
+16r61a4,16r6194,16r61ae,16r622e,16r6469,16r646f,16r6479,16r649e,
+16r64b2,16r6488,16r6490,16r64b0,16r64a5,16r6493,16r6495,16r64a9,
+16r6492,16r64ae,16r64ad,16r64ab,16r649a,16r64ac,16r6499,16r64a2,
+16r64b3,16r6575,16r6577,16r6578,16r66ae,16r66ab,16r66b4,16r66b1,
+16r6a23,16r6a1f,16r69e8,16r6a01,16r6a1e,16r6a19,16r69fd,16r6a21,
+16r6a13,16r6a0a,16r69f3,16r6a02,16r6a05,16r69ed,16r6a11,16r6b50,
+16r6b4e,16r6ba4,16r6bc5,16r6bc6,16r6f3f,16r6f7c,16r6f84,16r6f51,
+16r6f66,16r6f54,16r6f86,16r6f6d,16r6f5b,16r6f78,16r6f6e,16r6f8e,
+16r6f7a,16r6f70,16r6f64,16r6f97,16r6f58,16r6ed5,16r6f6f,16r6f60,
+16r6f5f,16r719f,16r71ac,16r71b1,16r71a8,16r7256,16r729b,16r734e,
+16r7357,16r7469,16r748b,16r7483,16r747e,16r7480,16r757f,16r7620,
+16r7629,16r761f,16r7624,16r7626,16r7621,16r7622,16r769a,16r76ba,
+16r76e4,16r778e,16r7787,16r778c,16r7791,16r778b,16r78cb,16r78c5,
+16r78ba,16r78ca,16r78be,16r78d5,16r78bc,16r78d0,16r7a3f,16r7a3c,
+16r7a40,16r7a3d,16r7a37,16r7a3b,16r7aaf,16r7aae,16r7bad,16r7bb1,
+16r7bc4,16r7bb4,16r7bc6,16r7bc7,16r7bc1,16r7ba0,16r7bcc,16r7cca,
+16r7de0,16r7df4,16r7def,16r7dfb,16r7dd8,16r7dec,16r7ddd,16r7de8,
+16r7de3,16r7dda,16r7dde,16r7de9,16r7d9e,16r7dd9,16r7df2,16r7df9,
+16r7f75,16r7f77,16r7faf,16r7fe9,16r8026,16r819b,16r819c,16r819d,
+16r81a0,16r819a,16r8198,16r8517,16r853d,16r851a,16r84ee,16r852c,
+16r852d,16r8513,16r8511,16r8523,16r8521,16r8514,16r84ec,16r8525,
+16r84ff,16r8506,16r8782,16r8774,16r8776,16r8760,16r8766,16r8778,
+16r8768,16r8759,16r8757,16r874c,16r8753,16r885b,16r885d,16r8910,
+16r8907,16r8912,16r8913,16r8915,16r890a,16r8abc,16r8ad2,16r8ac7,
+16r8ac4,16r8a95,16r8acb,16r8af8,16r8ab2,16r8ac9,16r8ac2,16r8abf,
+16r8ab0,16r8ad6,16r8acd,16r8ab6,16r8ab9,16r8adb,16r8c4c,16r8c4e,
+16r8c6c,16r8ce0,16r8cde,16r8ce6,16r8ce4,16r8cec,16r8ced,16r8ce2,
+16r8ce3,16r8cdc,16r8cea,16r8ce1,16r8d6d,16r8d9f,16r8da3,16r8e2b,
+16r8e10,16r8e1d,16r8e22,16r8e0f,16r8e29,16r8e1f,16r8e21,16r8e1e,
+16r8eba,16r8f1d,16r8f1b,16r8f1f,16r8f29,16r8f26,16r8f2a,16r8f1c,
+16r8f1e,16r8f25,16r9069,16r906e,16r9068,16r906d,16r9077,16r9130,
+16r912d,16r9127,16r9131,16r9187,16r9189,16r918b,16r9183,16r92c5,
+16r92bb,16r92b7,16r92ea,16r92ac,16r92e4,16r92c1,16r92b3,16r92bc,
+16r92d2,16r92c7,16r92f0,16r92b2,16r95ad,16r95b1,16r9704,16r9706,
+16r9707,16r9709,16r9760,16r978d,16r978b,16r978f,16r9821,16r982b,
+16r981c,16r98b3,16r990a,16r9913,16r9912,16r9918,16r99dd,16r99d0,
+16r99df,16r99db,16r99d1,16r99d5,16r99d2,16r99d9,16r9ab7,16r9aee,
+16r9aef,16r9b27,16r9b45,16r9b44,16r9b77,16r9b6f,16r9d06,16r9d09,
+16r9d03,16r9ea9,16r9ebe,16r9ece,16r58a8,16r9f52,16r5112,16r5118,
+16r5114,16r5110,16r5115,16r5180,16r51aa,16r51dd,16r5291,16r5293,
+16r52f3,16r5659,16r566b,16r5679,16r5669,16r5664,16r5678,16r566a,
+16r5668,16r5665,16r5671,16r566f,16r566c,16r5662,16r5676,16r58c1,
+16r58be,16r58c7,16r58c5,16r596e,16r5b1d,16r5b34,16r5b78,16r5bf0,
+16r5c0e,16r5f4a,16r61b2,16r6191,16r61a9,16r618a,16r61cd,16r61b6,
+16r61be,16r61ca,16r61c8,16r6230,16r64c5,16r64c1,16r64cb,16r64bb,
+16r64bc,16r64da,16r64c4,16r64c7,16r64c2,16r64cd,16r64bf,16r64d2,
+16r64d4,16r64be,16r6574,16r66c6,16r66c9,16r66b9,16r66c4,16r66c7,
+16r66b8,16r6a3d,16r6a38,16r6a3a,16r6a59,16r6a6b,16r6a58,16r6a39,
+16r6a44,16r6a62,16r6a61,16r6a4b,16r6a47,16r6a35,16r6a5f,16r6a48,
+16r6b59,16r6b77,16r6c05,16r6fc2,16r6fb1,16r6fa1,16r6fc3,16r6fa4,
+16r6fc1,16r6fa7,16r6fb3,16r6fc0,16r6fb9,16r6fb6,16r6fa6,16r6fa0,
+16r6fb4,16r71be,16r71c9,16r71d0,16r71d2,16r71c8,16r71d5,16r71b9,
+16r71ce,16r71d9,16r71dc,16r71c3,16r71c4,16r7368,16r749c,16r74a3,
+16r7498,16r749f,16r749e,16r74e2,16r750c,16r750d,16r7634,16r7638,
+16r763a,16r76e7,16r76e5,16r77a0,16r779e,16r779f,16r77a5,16r78e8,
+16r78da,16r78ec,16r78e7,16r79a6,16r7a4d,16r7a4e,16r7a46,16r7a4c,
+16r7a4b,16r7aba,16r7bd9,16r7c11,16r7bc9,16r7be4,16r7bdb,16r7be1,
+16r7be9,16r7be6,16r7cd5,16r7cd6,16r7e0a,16r7e11,16r7e08,16r7e1b,
+16r7e23,16r7e1e,16r7e1d,16r7e09,16r7e10,16r7f79,16r7fb2,16r7ff0,
+16r7ff1,16r7fee,16r8028,16r81b3,16r81a9,16r81a8,16r81fb,16r8208,
+16r8258,16r8259,16r854a,16r8559,16r8548,16r8568,16r8569,16r8543,
+16r8549,16r856d,16r856a,16r855e,16r8783,16r879f,16r879e,16r87a2,
+16r878d,16r8861,16r892a,16r8932,16r8925,16r892b,16r8921,16r89aa,
+16r89a6,16r8ae6,16r8afa,16r8aeb,16r8af1,16r8b00,16r8adc,16r8ae7,
+16r8aee,16r8afe,16r8b01,16r8b02,16r8af7,16r8aed,16r8af3,16r8af6,
+16r8afc,16r8c6b,16r8c6d,16r8c93,16r8cf4,16r8e44,16r8e31,16r8e34,
+16r8e42,16r8e39,16r8e35,16r8f3b,16r8f2f,16r8f38,16r8f33,16r8fa8,
+16r8fa6,16r9075,16r9074,16r9078,16r9072,16r907c,16r907a,16r9134,
+16r9192,16r9320,16r9336,16r92f8,16r9333,16r932f,16r9322,16r92fc,
+16r932b,16r9304,16r931a,16r9310,16r9326,16r9321,16r9315,16r932e,
+16r9319,16r95bb,16r96a7,16r96a8,16r96aa,16r96d5,16r970e,16r9711,
+16r9716,16r970d,16r9713,16r970f,16r975b,16r975c,16r9766,16r9798,
+16r9830,16r9838,16r983b,16r9837,16r982d,16r9839,16r9824,16r9910,
+16r9928,16r991e,16r991b,16r9921,16r991a,16r99ed,16r99e2,16r99f1,
+16r9ab8,16r9abc,16r9afb,16r9aed,16r9b28,16r9b91,16r9d15,16r9d23,
+16r9d26,16r9d28,16r9d12,16r9d1b,16r9ed8,16r9ed4,16r9f8d,16r9f9c,
+16r512a,16r511f,16r5121,16r5132,16r52f5,16r568e,16r5680,16r5690,
+16r5685,16r5687,16r568f,16r58d5,16r58d3,16r58d1,16r58ce,16r5b30,
+16r5b2a,16r5b24,16r5b7a,16r5c37,16r5c68,16r5dbc,16r5dba,16r5dbd,
+16r5db8,16r5e6b,16r5f4c,16r5fbd,16r61c9,16r61c2,16r61c7,16r61e6,
+16r61cb,16r6232,16r6234,16r64ce,16r64ca,16r64d8,16r64e0,16r64f0,
+16r64e6,16r64ec,16r64f1,16r64e2,16r64ed,16r6582,16r6583,16r66d9,
+16r66d6,16r6a80,16r6a94,16r6a84,16r6aa2,16r6a9c,16r6adb,16r6aa3,
+16r6a7e,16r6a97,16r6a90,16r6aa0,16r6b5c,16r6bae,16r6bda,16r6c08,
+16r6fd8,16r6ff1,16r6fdf,16r6fe0,16r6fdb,16r6fe4,16r6feb,16r6fef,
+16r6f80,16r6fec,16r6fe1,16r6fe9,16r6fd5,16r6fee,16r6ff0,16r71e7,
+16r71df,16r71ee,16r71e6,16r71e5,16r71ed,16r71ec,16r71f4,16r71e0,
+16r7235,16r7246,16r7370,16r7372,16r74a9,16r74b0,16r74a6,16r74a8,
+16r7646,16r7642,16r764c,16r76ea,16r77b3,16r77aa,16r77b0,16r77ac,
+16r77a7,16r77ad,16r77ef,16r78f7,16r78fa,16r78f4,16r78ef,16r7901,
+16r79a7,16r79aa,16r7a57,16r7abf,16r7c07,16r7c0d,16r7bfe,16r7bf7,
+16r7c0c,16r7be0,16r7ce0,16r7cdc,16r7cde,16r7ce2,16r7cdf,16r7cd9,
+16r7cdd,16r7e2e,16r7e3e,16r7e46,16r7e37,16r7e32,16r7e43,16r7e2b,
+16r7e3d,16r7e31,16r7e45,16r7e41,16r7e34,16r7e39,16r7e48,16r7e35,
+16r7e3f,16r7e2f,16r7f44,16r7ff3,16r7ffc,16r8071,16r8072,16r8070,
+16r806f,16r8073,16r81c6,16r81c3,16r81ba,16r81c2,16r81c0,16r81bf,
+16r81bd,16r81c9,16r81be,16r81e8,16r8209,16r8271,16r85aa,16r8584,
+16r857e,16r859c,16r8591,16r8594,16r85af,16r859b,16r8587,16r85a8,
+16r858a,16r8667,16r87c0,16r87d1,16r87b3,16r87d2,16r87c6,16r87ab,
+16r87bb,16r87ba,16r87c8,16r87cb,16r893b,16r8936,16r8944,16r8938,
+16r893d,16r89ac,16r8b0e,16r8b17,16r8b19,16r8b1b,16r8b0a,16r8b20,
+16r8b1d,16r8b04,16r8b10,16r8c41,16r8c3f,16r8c73,16r8cfa,16r8cfd,
+16r8cfc,16r8cf8,16r8cfb,16r8da8,16r8e49,16r8e4b,16r8e48,16r8e4a,
+16r8f44,16r8f3e,16r8f42,16r8f45,16r8f3f,16r907f,16r907d,16r9084,
+16r9081,16r9082,16r9080,16r9139,16r91a3,16r919e,16r919c,16r934d,
+16r9382,16r9328,16r9375,16r934a,16r9365,16r934b,16r9318,16r937e,
+16r936c,16r935b,16r9370,16r935a,16r9354,16r95ca,16r95cb,16r95cc,
+16r95c8,16r95c6,16r96b1,16r96b8,16r96d6,16r971c,16r971e,16r97a0,
+16r97d3,16r9846,16r98b6,16r9935,16r9a01,16r99ff,16r9bae,16r9bab,
+16r9baa,16r9bad,16r9d3b,16r9d3f,16r9e8b,16r9ecf,16r9ede,16r9edc,
+16r9edd,16r9edb,16r9f3e,16r9f4b,16r53e2,16r5695,16r56ae,16r58d9,
+16r58d8,16r5b38,16r5f5d,16r61e3,16r6233,16r64f4,16r64f2,16r64fe,
+16r6506,16r64fa,16r64fb,16r64f7,16r65b7,16r66dc,16r6726,16r6ab3,
+16r6aac,16r6ac3,16r6abb,16r6ab8,16r6ac2,16r6aae,16r6aaf,16r6b5f,
+16r6b78,16r6baf,16r7009,16r700b,16r6ffe,16r7006,16r6ffa,16r7011,
+16r700f,16r71fb,16r71fc,16r71fe,16r71f8,16r7377,16r7375,16r74a7,
+16r74bf,16r7515,16r7656,16r7658,16r7652,16r77bd,16r77bf,16r77bb,
+16r77bc,16r790e,16r79ae,16r7a61,16r7a62,16r7a60,16r7ac4,16r7ac5,
+16r7c2b,16r7c27,16r7c2a,16r7c1e,16r7c23,16r7c21,16r7ce7,16r7e54,
+16r7e55,16r7e5e,16r7e5a,16r7e61,16r7e52,16r7e59,16r7f48,16r7ff9,
+16r7ffb,16r8077,16r8076,16r81cd,16r81cf,16r820a,16r85cf,16r85a9,
+16r85cd,16r85d0,16r85c9,16r85b0,16r85ba,16r85b9,16r85a6,16r87ef,
+16r87ec,16r87f2,16r87e0,16r8986,16r89b2,16r89f4,16r8b28,16r8b39,
+16r8b2c,16r8b2b,16r8c50,16r8d05,16r8e59,16r8e63,16r8e66,16r8e64,
+16r8e5f,16r8e55,16r8ec0,16r8f49,16r8f4d,16r9087,16r9083,16r9088,
+16r91ab,16r91ac,16r91d0,16r9394,16r938a,16r9396,16r93a2,16r93b3,
+16r93ae,16r93ac,16r93b0,16r9398,16r939a,16r9397,16r95d4,16r95d6,
+16r95d0,16r95d5,16r96e2,16r96dc,16r96d9,16r96db,16r96de,16r9724,
+16r97a3,16r97a6,16r97ad,16r97f9,16r984d,16r984f,16r984c,16r984e,
+16r9853,16r98ba,16r993e,16r993f,16r993d,16r992e,16r99a5,16r9a0e,
+16r9ac1,16r9b03,16r9b06,16r9b4f,16r9b4e,16r9b4d,16r9bca,16r9bc9,
+16r9bfd,16r9bc8,16r9bc0,16r9d51,16r9d5d,16r9d60,16r9ee0,16r9f15,
+16r9f2c,16r5133,16r56a5,16r58de,16r58df,16r58e2,16r5bf5,16r9f90,
+16r5eec,16r61f2,16r61f7,16r61f6,16r61f5,16r6500,16r650f,16r66e0,
+16r66dd,16r6ae5,16r6add,16r6ada,16r6ad3,16r701b,16r701f,16r7028,
+16r701a,16r701d,16r7015,16r7018,16r7206,16r720d,16r7258,16r72a2,
+16r7378,16r737a,16r74bd,16r74ca,16r74e3,16r7587,16r7586,16r765f,
+16r7661,16r77c7,16r7919,16r79b1,16r7a6b,16r7a69,16r7c3e,16r7c3f,
+16r7c38,16r7c3d,16r7c37,16r7c40,16r7e6b,16r7e6d,16r7e79,16r7e69,
+16r7e6a,16r7f85,16r7e73,16r7fb6,16r7fb9,16r7fb8,16r81d8,16r85e9,
+16r85dd,16r85ea,16r85d5,16r85e4,16r85e5,16r85f7,16r87fb,16r8805,
+16r880d,16r87f9,16r87fe,16r8960,16r895f,16r8956,16r895e,16r8b41,
+16r8b5c,16r8b58,16r8b49,16r8b5a,16r8b4e,16r8b4f,16r8b46,16r8b59,
+16r8d08,16r8d0a,16r8e7c,16r8e72,16r8e87,16r8e76,16r8e6c,16r8e7a,
+16r8e74,16r8f54,16r8f4e,16r8fad,16r908a,16r908b,16r91b1,16r91ae,
+16r93e1,16r93d1,16r93df,16r93c3,16r93c8,16r93dc,16r93dd,16r93d6,
+16r93e2,16r93cd,16r93d8,16r93e4,16r93d7,16r93e8,16r95dc,16r96b4,
+16r96e3,16r972a,16r9727,16r9761,16r97dc,16r97fb,16r985e,16r9858,
+16r985b,16r98bc,16r9945,16r9949,16r9a16,16r9a19,16r9b0d,16r9be8,
+16r9be7,16r9bd6,16r9bdb,16r9d89,16r9d61,16r9d72,16r9d6a,16r9d6c,
+16r9e92,16r9e97,16r9e93,16r9eb4,16r52f8,16r56a8,16r56b7,16r56b6,
+16r56b4,16r56bc,16r58e4,16r5b40,16r5b43,16r5b7d,16r5bf6,16r5dc9,
+16r61f8,16r61fa,16r6518,16r6514,16r6519,16r66e6,16r6727,16r6aec,
+16r703e,16r7030,16r7032,16r7210,16r737b,16r74cf,16r7662,16r7665,
+16r7926,16r792a,16r792c,16r792b,16r7ac7,16r7af6,16r7c4c,16r7c43,
+16r7c4d,16r7cef,16r7cf0,16r8fae,16r7e7d,16r7e7c,16r7e82,16r7f4c,
+16r8000,16r81da,16r8266,16r85fb,16r85f9,16r8611,16r85fa,16r8606,
+16r860b,16r8607,16r860a,16r8814,16r8815,16r8964,16r89ba,16r89f8,
+16r8b70,16r8b6c,16r8b66,16r8b6f,16r8b5f,16r8b6b,16r8d0f,16r8d0d,
+16r8e89,16r8e81,16r8e85,16r8e82,16r91b4,16r91cb,16r9418,16r9403,
+16r93fd,16r95e1,16r9730,16r98c4,16r9952,16r9951,16r99a8,16r9a2b,
+16r9a30,16r9a37,16r9a35,16r9c13,16r9c0d,16r9e79,16r9eb5,16r9ee8,
+16r9f2f,16r9f5f,16r9f63,16r9f61,16r5137,16r5138,16r56c1,16r56c0,
+16r56c2,16r5914,16r5c6c,16r5dcd,16r61fc,16r61fe,16r651d,16r651c,
+16r6595,16r66e9,16r6afb,16r6b04,16r6afa,16r6bb2,16r704c,16r721b,
+16r72a7,16r74d6,16r74d4,16r7669,16r77d3,16r7c50,16r7e8f,16r7e8c,
+16r7fbc,16r8617,16r862d,16r861a,16r8823,16r8822,16r8821,16r881f,
+16r896a,16r896c,16r89bd,16r8b74,16r8b77,16r8b7d,16r8d13,16r8e8a,
+16r8e8d,16r8e8b,16r8f5f,16r8faf,16r91ba,16r942e,16r9433,16r9435,
+16r943a,16r9438,16r9432,16r942b,16r95e2,16r9738,16r9739,16r9732,
+16r97ff,16r9867,16r9865,16r9957,16r9a45,16r9a43,16r9a40,16r9a3e,
+16r9acf,16r9b54,16r9b51,16r9c2d,16r9c25,16r9daf,16r9db4,16r9dc2,
+16r9db8,16r9e9d,16r9eef,16r9f19,16r9f5c,16r9f66,16r9f67,16r513c,
+16r513b,16r56c8,16r56ca,16r56c9,16r5b7f,16r5dd4,16r5dd2,16r5f4e,
+16r61ff,16r6524,16r6b0a,16r6b61,16r7051,16r7058,16r7380,16r74e4,
+16r758a,16r766e,16r766c,16r79b3,16r7c60,16r7c5f,16r807e,16r807d,
+16r81df,16r8972,16r896f,16r89fc,16r8b80,16r8d16,16r8d17,16r8e91,
+16r8e93,16r8f61,16r9148,16r9444,16r9451,16r9452,16r973d,16r973e,
+16r97c3,16r97c1,16r986b,16r9955,16r9a55,16r9a4d,16r9ad2,16r9b1a,
+16r9c49,16r9c31,16r9c3e,16r9c3b,16r9dd3,16r9dd7,16r9f34,16r9f6c,
+16r9f6a,16r9f94,16r56cc,16r5dd6,16r6200,16r6523,16r652b,16r652a,
+16r66ec,16r6b10,16r74da,16r7aca,16r7c64,16r7c63,16r7c65,16r7e93,
+16r7e96,16r7e94,16r81e2,16r8638,16r863f,16r8831,16r8b8a,16r9090,
+16r908f,16r9463,16r9460,16r9464,16r9768,16r986f,16r995c,16r9a5a,
+16r9a5b,16r9a57,16r9ad3,16r9ad4,16r9ad1,16r9c54,16r9c57,16r9c56,
+16r9de5,16r9e9f,16r9ef4,16r56d1,16r58e9,16r652c,16r705e,16r7671,
+16r7672,16r77d7,16r7f50,16r7f88,16r8836,16r8839,16r8862,16r8b93,
+16r8b92,16r8b96,16r8277,16r8d1b,16r91c0,16r946a,16r9742,16r9748,
+16r9744,16r97c6,16r9870,16r9a5f,16r9b22,16r9b58,16r9c5f,16r9df9,
+16r9dfa,16r9e7c,16r9e7d,16r9f07,16r9f77,16r9f72,16r5ef3,16r6b16,
+16r7063,16r7c6c,16r7c6e,16r883b,16r89c0,16r8ea1,16r91c1,16r9472,
+16r9470,16r9871,16r995e,16r9ad6,16r9b23,16r9ecc,16r7064,16r77da,
+16r8b9a,16r9477,16r97c9,16r9a62,16r9a65,16r7e9c,16r8b9c,16r8eaa,
+16r91c5,16r947d,16r947e,16r947c,16r9c77,16r9c78,16r9ef7,16r8c54,
+16r947f,16r9e1a,16r7228,16r9a6a,16r9b31,16r9e1b,16r9e1e,16r7c72,
+16r30fe,16r309d,16r309e,16r3005,16r3041,16r3042,16r3043,16r3044,
+16r3045,16r3046,16r3047,16r3048,16r3049,16r304a,16r304b,16r304c,
+16r304d,16r304e,16r304f,16r3050,16r3051,16r3052,16r3053,16r3054,
+16r3055,16r3056,16r3057,16r3058,16r3059,16r305a,16r305b,16r305c,
+16r305d,16r305e,16r305f,16r3060,16r3061,16r3062,16r3063,16r3064,
+16r3065,16r3066,16r3067,16r3068,16r3069,16r306a,16r306b,16r306c,
+16r306d,16r306e,16r306f,16r3070,16r3071,16r3072,16r3073,16r3074,
+16r3075,16r3076,16r3077,16r3078,16r3079,16r307a,16r307b,16r307c,
+16r307d,16r307e,16r307f,16r3080,16r3081,16r3082,16r3083,16r3084,
+16r3085,16r3086,16r3087,16r3088,16r3089,16r308a,16r308b,16r308c,
+16r308d,16r308e,16r308f,16r3090,16r3091,16r3092,16r3093,16r30a1,
+16r30a2,16r30a3,16r30a4,16r30a5,16r30a6,16r30a7,16r30a8,16r30a9,
+16r30aa,16r30ab,16r30ac,16r30ad,16r30ae,16r30af,16r30b0,16r30b1,
+16r30b2,16r30b3,16r30b4,16r30b5,16r30b6,16r30b7,16r30b8,16r30b9,
+16r30ba,16r30bb,16r30bc,16r30bd,16r30be,16r30bf,16r30c0,16r30c1,
+16r30c2,16r30c3,16r30c4,16r30c5,16r30c6,16r30c7,16r30c8,16r30c9,
+16r30ca,16r30cb,16r30cc,16r30cd,16r30ce,16r30cf,16r30d0,16r30d1,
+16r30d2,16r30d3,16r30d4,16r30d5,16r30d6,16r30d7,16r30d8,16r30d9,
+16r30da,16r30db,16r30dc,16r30dd,16r30de,16r30df,16r30e0,16r30e1,
+16r30e2,16r30e3,16r30e4,16r30e5,16r30e6,16r30e7,16r30e8,16r30e9,
+16r30ea,16r30eb,16r30ec,16r30ed,16r30ee,16r30ef,16r30f0,16r30f1,
+16r30f2,16r30f3,16r30f4,16r30f5,16r30f6,16r0414,16r0415,16r0401,
+16r0416,16r0417,16r0418,16r0419,16r041a,16r041b,16r041c,16r0423,
+16r0424,16r0425,16r0426,16r0427,16r0428,16r0429,16r042a,16r042b,
+16r042c,16r042d,16r042e,16r042f,16r0430,16r0431,16r0432,16r0433,
+16r0434,16r0435,16r0451,16r0436,16r0437,16r0438,16r0439,16r043a,
+16r043b,16r043c,16r043d,16r043e,16r043f,16r0440,16r0441,16r0442,
+16r0443,16r0444,16r0445,16r0446,16r0447,16r0448,16r0449,16r044a,
+16r044b,16r044c,16r044d,16r044e,16r044f,16r2460,16r2461,16r2462,
+16r2463,16r2464,16r2465,16r2466,16r2467,16r2468,16r2469,16r2474,
+16r2475,16r2476,16r2477,16r2478,16r2479,16r247a,16r247b,16r247c,
+16r247d,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+16r4e42,16r4e5c,16r51f5,16r531a,16r5382,16r4e07,16r4e0c,16r4e47,
+16r4e8d,16r56d7,16rfa0c,16r5c6e,16r5f73,16r4e0f,16r5187,16r4e0e,
+16r4e2e,16r4e93,16r4ec2,16r4ec9,16r4ec8,16r5198,16r52fc,16r536c,
+16r53b9,16r5720,16r5903,16r592c,16r5c10,16r5dff,16r65e1,16r6bb3,
+16r6bcc,16r6c14,16r723f,16r4e31,16r4e3c,16r4ee8,16r4edc,16r4ee9,
+16r4ee1,16r4edd,16r4eda,16r520c,16r531c,16r534c,16r5722,16r5723,
+16r5917,16r592f,16r5b81,16r5b84,16r5c12,16r5c3b,16r5c74,16r5c73,
+16r5e04,16r5e80,16r5e82,16r5fc9,16r6209,16r6250,16r6c15,16r6c36,
+16r6c43,16r6c3f,16r6c3b,16r72ae,16r72b0,16r738a,16r79b8,16r808a,
+16r961e,16r4f0e,16r4f18,16r4f2c,16r4ef5,16r4f14,16r4ef1,16r4f00,
+16r4ef7,16r4f08,16r4f1d,16r4f02,16r4f05,16r4f22,16r4f13,16r4f04,
+16r4ef4,16r4f12,16r51b1,16r5213,16r5209,16r5210,16r52a6,16r5322,
+16r531f,16r534d,16r538a,16r5407,16r56e1,16r56df,16r572e,16r572a,
+16r5734,16r593c,16r5980,16r597c,16r5985,16r597b,16r597e,16r5977,
+16r597f,16r5b56,16r5c15,16r5c25,16r5c7c,16r5c7a,16r5c7b,16r5c7e,
+16r5ddf,16r5e75,16r5e84,16r5f02,16r5f1a,16r5f74,16r5fd5,16r5fd4,
+16r5fcf,16r625c,16r625e,16r6264,16r6261,16r6266,16r6262,16r6259,
+16r6260,16r625a,16r6265,16r65ef,16r65ee,16r673e,16r6739,16r6738,
+16r673b,16r673a,16r673f,16r673c,16r6733,16r6c18,16r6c46,16r6c52,
+16r6c5c,16r6c4f,16r6c4a,16r6c54,16r6c4b,16r6c4c,16r7071,16r725e,
+16r72b4,16r72b5,16r738e,16r752a,16r767f,16r7a75,16r7f51,16r8278,
+16r827c,16r8280,16r827d,16r827f,16r864d,16r897e,16r9099,16r9097,
+16r9098,16r909b,16r9094,16r9622,16r9624,16r9620,16r9623,16r4f56,
+16r4f3b,16r4f62,16r4f49,16r4f53,16r4f64,16r4f3e,16r4f67,16r4f52,
+16r4f5f,16r4f41,16r4f58,16r4f2d,16r4f33,16r4f3f,16r4f61,16r518f,
+16r51b9,16r521c,16r521e,16r5221,16r52ad,16r52ae,16r5309,16r5363,
+16r5372,16r538e,16r538f,16r5430,16r5437,16r542a,16r5454,16r5445,
+16r5419,16r541c,16r5425,16r5418,16r543d,16r544f,16r5441,16r5428,
+16r5424,16r5447,16r56ee,16r56e7,16r56e5,16r5741,16r5745,16r574c,
+16r5749,16r574b,16r5752,16r5906,16r5940,16r59a6,16r5998,16r59a0,
+16r5997,16r598e,16r59a2,16r5990,16r598f,16r59a7,16r59a1,16r5b8e,
+16r5b92,16r5c28,16r5c2a,16r5c8d,16r5c8f,16r5c88,16r5c8b,16r5c89,
+16r5c92,16r5c8a,16r5c86,16r5c93,16r5c95,16r5de0,16r5e0a,16r5e0e,
+16r5e8b,16r5e89,16r5e8c,16r5e88,16r5e8d,16r5f05,16r5f1d,16r5f78,
+16r5f76,16r5fd2,16r5fd1,16r5fd0,16r5fed,16r5fe8,16r5fee,16r5ff3,
+16r5fe1,16r5fe4,16r5fe3,16r5ffa,16r5fef,16r5ff7,16r5ffb,16r6000,
+16r5ff4,16r623a,16r6283,16r628c,16r628e,16r628f,16r6294,16r6287,
+16r6271,16r627b,16r627a,16r6270,16r6281,16r6288,16r6277,16r627d,
+16r6272,16r6274,16r6537,16r65f0,16r65f4,16r65f3,16r65f2,16r65f5,
+16r6745,16r6747,16r6759,16r6755,16r674c,16r6748,16r675d,16r674d,
+16r675a,16r674b,16r6bd0,16r6c19,16r6c1a,16r6c78,16r6c67,16r6c6b,
+16r6c84,16r6c8b,16r6c8f,16r6c71,16r6c6f,16r6c69,16r6c9a,16r6c6d,
+16r6c87,16r6c95,16r6c9c,16r6c66,16r6c73,16r6c65,16r6c7b,16r6c8e,
+16r7074,16r707a,16r7263,16r72bf,16r72bd,16r72c3,16r72c6,16r72c1,
+16r72ba,16r72c5,16r7395,16r7397,16r7393,16r7394,16r7392,16r753a,
+16r7539,16r7594,16r7595,16r7681,16r793d,16r8034,16r8095,16r8099,
+16r8090,16r8092,16r809c,16r8290,16r828f,16r8285,16r828e,16r8291,
+16r8293,16r828a,16r8283,16r8284,16r8c78,16r8fc9,16r8fbf,16r909f,
+16r90a1,16r90a5,16r909e,16r90a7,16r90a0,16r9630,16r9628,16r962f,
+16r962d,16r4e33,16r4f98,16r4f7c,16r4f85,16r4f7d,16r4f80,16r4f87,
+16r4f76,16r4f74,16r4f89,16r4f84,16r4f77,16r4f4c,16r4f97,16r4f6a,
+16r4f9a,16r4f79,16r4f81,16r4f78,16r4f90,16r4f9c,16r4f94,16r4f9e,
+16r4f92,16r4f82,16r4f95,16r4f6b,16r4f6e,16r519e,16r51bc,16r51be,
+16r5235,16r5232,16r5233,16r5246,16r5231,16r52bc,16r530a,16r530b,
+16r533c,16r5392,16r5394,16r5487,16r547f,16r5481,16r5491,16r5482,
+16r5488,16r546b,16r547a,16r547e,16r5465,16r546c,16r5474,16r5466,
+16r548d,16r546f,16r5461,16r5460,16r5498,16r5463,16r5467,16r5464,
+16r56f7,16r56f9,16r576f,16r5772,16r576d,16r576b,16r5771,16r5770,
+16r5776,16r5780,16r5775,16r577b,16r5773,16r5774,16r5762,16r5768,
+16r577d,16r590c,16r5945,16r59b5,16r59ba,16r59cf,16r59ce,16r59b2,
+16r59cc,16r59c1,16r59b6,16r59bc,16r59c3,16r59d6,16r59b1,16r59bd,
+16r59c0,16r59c8,16r59b4,16r59c7,16r5b62,16r5b65,16r5b93,16r5b95,
+16r5c44,16r5c47,16r5cae,16r5ca4,16r5ca0,16r5cb5,16r5caf,16r5ca8,
+16r5cac,16r5c9f,16r5ca3,16r5cad,16r5ca2,16r5caa,16r5ca7,16r5c9d,
+16r5ca5,16r5cb6,16r5cb0,16r5ca6,16r5e17,16r5e14,16r5e19,16r5f28,
+16r5f22,16r5f23,16r5f24,16r5f54,16r5f82,16r5f7e,16r5f7d,16r5fde,
+16r5fe5,16r602d,16r6026,16r6019,16r6032,16r600b,16r6034,16r600a,
+16r6017,16r6033,16r601a,16r601e,16r602c,16r6022,16r600d,16r6010,
+16r602e,16r6013,16r6011,16r600c,16r6009,16r601c,16r6214,16r623d,
+16r62ad,16r62b4,16r62d1,16r62be,16r62aa,16r62b6,16r62ca,16r62ae,
+16r62b3,16r62af,16r62bb,16r62a9,16r62b0,16r62b8,16r653d,16r65a8,
+16r65bb,16r6609,16r65fc,16r6604,16r6612,16r6608,16r65fb,16r6603,
+16r660b,16r660d,16r6605,16r65fd,16r6611,16r6610,16r66f6,16r670a,
+16r6785,16r676c,16r678e,16r6792,16r6776,16r677b,16r6798,16r6786,
+16r6784,16r6774,16r678d,16r678c,16r677a,16r679f,16r6791,16r6799,
+16r6783,16r677d,16r6781,16r6778,16r6779,16r6794,16r6b25,16r6b80,
+16r6b7e,16r6bde,16r6c1d,16r6c93,16r6cec,16r6ceb,16r6cee,16r6cd9,
+16r6cb6,16r6cd4,16r6cad,16r6ce7,16r6cb7,16r6cd0,16r6cc2,16r6cba,
+16r6cc3,16r6cc6,16r6ced,16r6cf2,16r6cd2,16r6cdd,16r6cb4,16r6c8a,
+16r6c9d,16r6c80,16r6cde,16r6cc0,16r6d30,16r6ccd,16r6cc7,16r6cb0,
+16r6cf9,16r6ccf,16r6ce9,16r6cd1,16r7094,16r7098,16r7085,16r7093,
+16r7086,16r7084,16r7091,16r7096,16r7082,16r709a,16r7083,16r726a,
+16r72d6,16r72cb,16r72d8,16r72c9,16r72dc,16r72d2,16r72d4,16r72da,
+16r72cc,16r72d1,16r73a4,16r73a1,16r73ad,16r73a6,16r73a2,16r73a0,
+16r73ac,16r739d,16r74dd,16r74e8,16r753f,16r7540,16r753e,16r758c,
+16r7598,16r76af,16r76f3,16r76f1,16r76f0,16r76f5,16r77f8,16r77fc,
+16r77f9,16r77fb,16r77fa,16r77f7,16r7942,16r793f,16r79c5,16r7a78,
+16r7a7b,16r7afb,16r7c75,16r7cfd,16r8035,16r808f,16r80ae,16r80a3,
+16r80b8,16r80b5,16r80ad,16r8220,16r82a0,16r82c0,16r82ab,16r829a,
+16r8298,16r829b,16r82b5,16r82a7,16r82ae,16r82bc,16r829e,16r82ba,
+16r82b4,16r82a8,16r82a1,16r82a9,16r82c2,16r82a4,16r82c3,16r82b6,
+16r82a2,16r8670,16r866f,16r866d,16r866e,16r8c56,16r8fd2,16r8fcb,
+16r8fd3,16r8fcd,16r8fd6,16r8fd5,16r8fd7,16r90b2,16r90b4,16r90af,
+16r90b3,16r90b0,16r9639,16r963d,16r963c,16r963a,16r9643,16r4fcd,
+16r4fc5,16r4fd3,16r4fb2,16r4fc9,16r4fcb,16r4fc1,16r4fd4,16r4fdc,
+16r4fd9,16r4fbb,16r4fb3,16r4fdb,16r4fc7,16r4fd6,16r4fba,16r4fc0,
+16r4fb9,16r4fec,16r5244,16r5249,16r52c0,16r52c2,16r533d,16r537c,
+16r5397,16r5396,16r5399,16r5398,16r54ba,16r54a1,16r54ad,16r54a5,
+16r54cf,16r54c3,16r830d,16r54b7,16r54ae,16r54d6,16r54b6,16r54c5,
+16r54c6,16r54a0,16r5470,16r54bc,16r54a2,16r54be,16r5472,16r54de,
+16r54b0,16r57b5,16r579e,16r579f,16r57a4,16r578c,16r5797,16r579d,
+16r579b,16r5794,16r5798,16r578f,16r5799,16r57a5,16r579a,16r5795,
+16r58f4,16r590d,16r5953,16r59e1,16r59de,16r59ee,16r5a00,16r59f1,
+16r59dd,16r59fa,16r59fd,16r59fc,16r59f6,16r59e4,16r59f2,16r59f7,
+16r59db,16r59e9,16r59f3,16r59f5,16r59e0,16r59fe,16r59f4,16r59ed,
+16r5ba8,16r5c4c,16r5cd0,16r5cd8,16r5ccc,16r5cd7,16r5ccb,16r5cdb,
+16r5cde,16r5cda,16r5cc9,16r5cc7,16r5cca,16r5cd6,16r5cd3,16r5cd4,
+16r5ccf,16r5cc8,16r5cc6,16r5cce,16r5cdf,16r5cf8,16r5df9,16r5e21,
+16r5e22,16r5e23,16r5e20,16r5e24,16r5eb0,16r5ea4,16r5ea2,16r5e9b,
+16r5ea3,16r5ea5,16r5f07,16r5f2e,16r5f56,16r5f86,16r6037,16r6039,
+16r6054,16r6072,16r605e,16r6045,16r6053,16r6047,16r6049,16r605b,
+16r604c,16r6040,16r6042,16r605f,16r6024,16r6044,16r6058,16r6066,
+16r606e,16r6242,16r6243,16r62cf,16r630d,16r630b,16r62f5,16r630e,
+16r6303,16r62eb,16r62f9,16r630f,16r630c,16r62f8,16r62f6,16r6300,
+16r6313,16r6314,16r62fa,16r6315,16r62fb,16r62f0,16r6541,16r6543,
+16r65aa,16r65bf,16r6636,16r6621,16r6632,16r6635,16r661c,16r6626,
+16r6622,16r6633,16r662b,16r663a,16r661d,16r6634,16r6639,16r662e,
+16r670f,16r6710,16r67c1,16r67f2,16r67c8,16r67ba,16r67dc,16r67bb,
+16r67f8,16r67d8,16r67c0,16r67b7,16r67c5,16r67eb,16r67e4,16r67df,
+16r67b5,16r67cd,16r67b3,16r67f7,16r67f6,16r67ee,16r67e3,16r67c2,
+16r67b9,16r67ce,16r67e7,16r67f0,16r67b2,16r67fc,16r67c6,16r67ed,
+16r67cc,16r67ae,16r67e6,16r67db,16r67fa,16r67c9,16r67ca,16r67c3,
+16r67ea,16r67cb,16r6b28,16r6b82,16r6b84,16r6bb6,16r6bd6,16r6bd8,
+16r6be0,16r6c20,16r6c21,16r6d28,16r6d34,16r6d2d,16r6d1f,16r6d3c,
+16r6d3f,16r6d12,16r6d0a,16r6cda,16r6d33,16r6d04,16r6d19,16r6d3a,
+16r6d1a,16r6d11,16r6d00,16r6d1d,16r6d42,16r6d01,16r6d18,16r6d37,
+16r6d03,16r6d0f,16r6d40,16r6d07,16r6d20,16r6d2c,16r6d08,16r6d22,
+16r6d09,16r6d10,16r70b7,16r709f,16r70be,16r70b1,16r70b0,16r70a1,
+16r70b4,16r70b5,16r70a9,16r7241,16r7249,16r724a,16r726c,16r7270,
+16r7273,16r726e,16r72ca,16r72e4,16r72e8,16r72eb,16r72df,16r72ea,
+16r72e6,16r72e3,16r7385,16r73cc,16r73c2,16r73c8,16r73c5,16r73b9,
+16r73b6,16r73b5,16r73b4,16r73eb,16r73bf,16r73c7,16r73be,16r73c3,
+16r73c6,16r73b8,16r73cb,16r74ec,16r74ee,16r752e,16r7547,16r7548,
+16r75a7,16r75aa,16r7679,16r76c4,16r7708,16r7703,16r7704,16r7705,
+16r770a,16r76f7,16r76fb,16r76fa,16r77e7,16r77e8,16r7806,16r7811,
+16r7812,16r7805,16r7810,16r780f,16r780e,16r7809,16r7803,16r7813,
+16r794a,16r794c,16r794b,16r7945,16r7944,16r79d5,16r79cd,16r79cf,
+16r79d6,16r79ce,16r7a80,16r7a7e,16r7ad1,16r7b00,16r7b01,16r7c7a,
+16r7c78,16r7c79,16r7c7f,16r7c80,16r7c81,16r7d03,16r7d08,16r7d01,
+16r7f58,16r7f91,16r7f8d,16r7fbe,16r8007,16r800e,16r800f,16r8014,
+16r8037,16r80d8,16r80c7,16r80e0,16r80d1,16r80c8,16r80c2,16r80d0,
+16r80c5,16r80e3,16r80d9,16r80dc,16r80ca,16r80d5,16r80c9,16r80cf,
+16r80d7,16r80e6,16r80cd,16r81ff,16r8221,16r8294,16r82d9,16r82fe,
+16r82f9,16r8307,16r82e8,16r8300,16r82d5,16r833a,16r82eb,16r82d6,
+16r82f4,16r82ec,16r82e1,16r82f2,16r82f5,16r830c,16r82fb,16r82f6,
+16r82f0,16r82ea,16r82e4,16r82e0,16r82fa,16r82f3,16r82ed,16r8677,
+16r8674,16r867c,16r8673,16r8841,16r884e,16r8867,16r886a,16r8869,
+16r89d3,16r8a04,16r8a07,16r8d72,16r8fe3,16r8fe1,16r8fee,16r8fe0,
+16r90f1,16r90bd,16r90bf,16r90d5,16r90c5,16r90be,16r90c7,16r90cb,
+16r90c8,16r91d4,16r91d3,16r9654,16r964f,16r9651,16r9653,16r964a,
+16r964e,16r501e,16r5005,16r5007,16r5013,16r5022,16r5030,16r501b,
+16r4ff5,16r4ff4,16r5033,16r5037,16r502c,16r4ff6,16r4ff7,16r5017,
+16r501c,16r5020,16r5027,16r5035,16r502f,16r5031,16r500e,16r515a,
+16r5194,16r5193,16r51ca,16r51c4,16r51c5,16r51c8,16r51ce,16r5261,
+16r525a,16r5252,16r525e,16r525f,16r5255,16r5262,16r52cd,16r530e,
+16r539e,16r5526,16r54e2,16r5517,16r5512,16r54e7,16r54f3,16r54e4,
+16r551a,16r54ff,16r5504,16r5508,16r54eb,16r5511,16r5505,16r54f1,
+16r550a,16r54fb,16r54f7,16r54f8,16r54e0,16r550e,16r5503,16r550b,
+16r5701,16r5702,16r57cc,16r5832,16r57d5,16r57d2,16r57ba,16r57c6,
+16r57bd,16r57bc,16r57b8,16r57b6,16r57bf,16r57c7,16r57d0,16r57b9,
+16r57c1,16r590e,16r594a,16r5a19,16r5a16,16r5a2d,16r5a2e,16r5a15,
+16r5a0f,16r5a17,16r5a0a,16r5a1e,16r5a33,16r5b6c,16r5ba7,16r5bad,
+16r5bac,16r5c03,16r5c56,16r5c54,16r5cec,16r5cff,16r5cee,16r5cf1,
+16r5cf7,16r5d00,16r5cf9,16r5e29,16r5e28,16r5ea8,16r5eae,16r5eaa,
+16r5eac,16r5f33,16r5f30,16r5f67,16r605d,16r605a,16r6067,16r6041,
+16r60a2,16r6088,16r6080,16r6092,16r6081,16r609d,16r6083,16r6095,
+16r609b,16r6097,16r6087,16r609c,16r608e,16r6219,16r6246,16r62f2,
+16r6310,16r6356,16r632c,16r6344,16r6345,16r6336,16r6343,16r63e4,
+16r6339,16r634b,16r634a,16r633c,16r6329,16r6341,16r6334,16r6358,
+16r6354,16r6359,16r632d,16r6347,16r6333,16r635a,16r6351,16r6338,
+16r6357,16r6340,16r6348,16r654a,16r6546,16r65c6,16r65c3,16r65c4,
+16r65c2,16r664a,16r665f,16r6647,16r6651,16r6712,16r6713,16r681f,
+16r681a,16r6849,16r6832,16r6833,16r683b,16r684b,16r684f,16r6816,
+16r6831,16r681c,16r6835,16r682b,16r682d,16r682f,16r684e,16r6844,
+16r6834,16r681d,16r6812,16r6814,16r6826,16r6828,16r682e,16r684d,
+16r683a,16r6825,16r6820,16r6b2c,16r6b2f,16r6b2d,16r6b31,16r6b34,
+16r6b6d,16r8082,16r6b88,16r6be6,16r6be4,16r6be8,16r6be3,16r6be2,
+16r6be7,16r6c25,16r6d7a,16r6d63,16r6d64,16r6d76,16r6d0d,16r6d61,
+16r6d92,16r6d58,16r6d62,16r6d6d,16r6d6f,16r6d91,16r6d8d,16r6def,
+16r6d7f,16r6d86,16r6d5e,16r6d67,16r6d60,16r6d97,16r6d70,16r6d7c,
+16r6d5f,16r6d82,16r6d98,16r6d2f,16r6d68,16r6d8b,16r6d7e,16r6d80,
+16r6d84,16r6d16,16r6d83,16r6d7b,16r6d7d,16r6d75,16r6d90,16r70dc,
+16r70d3,16r70d1,16r70dd,16r70cb,16r7f39,16r70e2,16r70d7,16r70d2,
+16r70de,16r70e0,16r70d4,16r70cd,16r70c5,16r70c6,16r70c7,16r70da,
+16r70ce,16r70e1,16r7242,16r7278,16r7277,16r7276,16r7300,16r72fa,
+16r72f4,16r72fe,16r72f6,16r72f3,16r72fb,16r7301,16r73d3,16r73d9,
+16r73e5,16r73d6,16r73bc,16r73e7,16r73e3,16r73e9,16r73dc,16r73d2,
+16r73db,16r73d4,16r73dd,16r73da,16r73d7,16r73d8,16r73e8,16r74de,
+16r74df,16r74f4,16r74f5,16r7521,16r755b,16r755f,16r75b0,16r75c1,
+16r75bb,16r75c4,16r75c0,16r75bf,16r75b6,16r75ba,16r768a,16r76c9,
+16r771d,16r771b,16r7710,16r7713,16r7712,16r7723,16r7711,16r7715,
+16r7719,16r771a,16r7722,16r7727,16r7823,16r782c,16r7822,16r7835,
+16r782f,16r7828,16r782e,16r782b,16r7821,16r7829,16r7833,16r782a,
+16r7831,16r7954,16r795b,16r794f,16r795c,16r7953,16r7952,16r7951,
+16r79eb,16r79ec,16r79e0,16r79ee,16r79ed,16r79ea,16r79dc,16r79de,
+16r79dd,16r7a86,16r7a89,16r7a85,16r7a8b,16r7a8c,16r7a8a,16r7a87,
+16r7ad8,16r7b10,16r7b04,16r7b13,16r7b05,16r7b0f,16r7b08,16r7b0a,
+16r7b0e,16r7b09,16r7b12,16r7c84,16r7c91,16r7c8a,16r7c8c,16r7c88,
+16r7c8d,16r7c85,16r7d1e,16r7d1d,16r7d11,16r7d0e,16r7d18,16r7d16,
+16r7d13,16r7d1f,16r7d12,16r7d0f,16r7d0c,16r7f5c,16r7f61,16r7f5e,
+16r7f60,16r7f5d,16r7f5b,16r7f96,16r7f92,16r7fc3,16r7fc2,16r7fc0,
+16r8016,16r803e,16r8039,16r80fa,16r80f2,16r80f9,16r80f5,16r8101,
+16r80fb,16r8100,16r8201,16r822f,16r8225,16r8333,16r832d,16r8344,
+16r8319,16r8351,16r8325,16r8356,16r833f,16r8341,16r8326,16r831c,
+16r8322,16r8342,16r834e,16r831b,16r832a,16r8308,16r833c,16r834d,
+16r8316,16r8324,16r8320,16r8337,16r832f,16r8329,16r8347,16r8345,
+16r834c,16r8353,16r831e,16r832c,16r834b,16r8327,16r8348,16r8653,
+16r8652,16r86a2,16r86a8,16r8696,16r868d,16r8691,16r869e,16r8687,
+16r8697,16r8686,16r868b,16r869a,16r8685,16r86a5,16r8699,16r86a1,
+16r86a7,16r8695,16r8698,16r868e,16r869d,16r8690,16r8694,16r8843,
+16r8844,16r886d,16r8875,16r8876,16r8872,16r8880,16r8871,16r887f,
+16r886f,16r8883,16r887e,16r8874,16r887c,16r8a12,16r8c47,16r8c57,
+16r8c7b,16r8ca4,16r8ca3,16r8d76,16r8d78,16r8db5,16r8db7,16r8db6,
+16r8ed1,16r8ed3,16r8ffe,16r8ff5,16r9002,16r8fff,16r8ffb,16r9004,
+16r8ffc,16r8ff6,16r90d6,16r90e0,16r90d9,16r90da,16r90e3,16r90df,
+16r90e5,16r90d8,16r90db,16r90d7,16r90dc,16r90e4,16r9150,16r914e,
+16r914f,16r91d5,16r91e2,16r91da,16r965c,16r965f,16r96bc,16r98e3,
+16r9adf,16r9b2f,16r4e7f,16r5070,16r506a,16r5061,16r505e,16r5060,
+16r5053,16r504b,16r505d,16r5072,16r5048,16r504d,16r5041,16r505b,
+16r504a,16r5062,16r5015,16r5045,16r505f,16r5069,16r506b,16r5063,
+16r5064,16r5046,16r5040,16r506e,16r5073,16r5057,16r5051,16r51d0,
+16r526b,16r526d,16r526c,16r526e,16r52d6,16r52d3,16r532d,16r539c,
+16r5575,16r5576,16r553c,16r554d,16r5550,16r5534,16r552a,16r5551,
+16r5562,16r5536,16r5535,16r5530,16r5552,16r5545,16r550c,16r5532,
+16r5565,16r554e,16r5539,16r5548,16r552d,16r553b,16r5540,16r554b,
+16r570a,16r5707,16r57fb,16r5814,16r57e2,16r57f6,16r57dc,16r57f4,
+16r5800,16r57ed,16r57fd,16r5808,16r57f8,16r580b,16r57f3,16r57cf,
+16r5807,16r57ee,16r57e3,16r57f2,16r57e5,16r57ec,16r57e1,16r580e,
+16r57fc,16r5810,16r57e7,16r5801,16r580c,16r57f1,16r57e9,16r57f0,
+16r580d,16r5804,16r595c,16r5a60,16r5a58,16r5a55,16r5a67,16r5a5e,
+16r5a38,16r5a35,16r5a6d,16r5a50,16r5a5f,16r5a65,16r5a6c,16r5a53,
+16r5a64,16r5a57,16r5a43,16r5a5d,16r5a52,16r5a44,16r5a5b,16r5a48,
+16r5a8e,16r5a3e,16r5a4d,16r5a39,16r5a4c,16r5a70,16r5a69,16r5a47,
+16r5a51,16r5a56,16r5a42,16r5a5c,16r5b72,16r5b6e,16r5bc1,16r5bc0,
+16r5c59,16r5d1e,16r5d0b,16r5d1d,16r5d1a,16r5d20,16r5d0c,16r5d28,
+16r5d0d,16r5d26,16r5d25,16r5d0f,16r5d30,16r5d12,16r5d23,16r5d1f,
+16r5d2e,16r5e3e,16r5e34,16r5eb1,16r5eb4,16r5eb9,16r5eb2,16r5eb3,
+16r5f36,16r5f38,16r5f9b,16r5f96,16r5f9f,16r608a,16r6090,16r6086,
+16r60be,16r60b0,16r60ba,16r60d3,16r60d4,16r60cf,16r60e4,16r60d9,
+16r60dd,16r60c8,16r60b1,16r60db,16r60b7,16r60ca,16r60bf,16r60c3,
+16r60cd,16r60c0,16r6332,16r6365,16r638a,16r6382,16r637d,16r63bd,
+16r639e,16r63ad,16r639d,16r6397,16r63ab,16r638e,16r636f,16r6387,
+16r6390,16r636e,16r63af,16r6375,16r639c,16r636d,16r63ae,16r637c,
+16r63a4,16r633b,16r639f,16r6378,16r6385,16r6381,16r6391,16r638d,
+16r6370,16r6553,16r65cd,16r6665,16r6661,16r665b,16r6659,16r665c,
+16r6662,16r6718,16r6879,16r6887,16r6890,16r689c,16r686d,16r686e,
+16r68ae,16r68ab,16r6956,16r686f,16r68a3,16r68ac,16r68a9,16r6875,
+16r6874,16r68b2,16r688f,16r6877,16r6892,16r687c,16r686b,16r6872,
+16r68aa,16r6880,16r6871,16r687e,16r689b,16r6896,16r688b,16r68a0,
+16r6889,16r68a4,16r6878,16r687b,16r6891,16r688c,16r688a,16r687d,
+16r6b36,16r6b33,16r6b37,16r6b38,16r6b91,16r6b8f,16r6b8d,16r6b8e,
+16r6b8c,16r6c2a,16r6dc0,16r6dab,16r6db4,16r6db3,16r6e74,16r6dac,
+16r6de9,16r6de2,16r6db7,16r6df6,16r6dd4,16r6e00,16r6dc8,16r6de0,
+16r6ddf,16r6dd6,16r6dbe,16r6de5,16r6ddc,16r6ddd,16r6ddb,16r6df4,
+16r6dca,16r6dbd,16r6ded,16r6df0,16r6dba,16r6dd5,16r6dc2,16r6dcf,
+16r6dc9,16r6dd0,16r6df2,16r6dd3,16r6dfd,16r6dd7,16r6dcd,16r6de3,
+16r6dbb,16r70fa,16r710d,16r70f7,16r7117,16r70f4,16r710c,16r70f0,
+16r7104,16r70f3,16r7110,16r70fc,16r70ff,16r7106,16r7113,16r7100,
+16r70f8,16r70f6,16r710b,16r7102,16r710e,16r727e,16r727b,16r727c,
+16r727f,16r731d,16r7317,16r7307,16r7311,16r7318,16r730a,16r7308,
+16r72ff,16r730f,16r731e,16r7388,16r73f6,16r73f8,16r73f5,16r7404,
+16r7401,16r73fd,16r7407,16r7400,16r73fa,16r73fc,16r73ff,16r740c,
+16r740b,16r73f4,16r7408,16r7564,16r7563,16r75ce,16r75d2,16r75cf,
+16r75cb,16r75cc,16r75d1,16r75d0,16r768f,16r7689,16r76d3,16r7739,
+16r772f,16r772d,16r7731,16r7732,16r7734,16r7733,16r773d,16r7725,
+16r773b,16r7735,16r7848,16r7852,16r7849,16r784d,16r784a,16r784c,
+16r7826,16r7845,16r7850,16r7964,16r7967,16r7969,16r796a,16r7963,
+16r796b,16r7961,16r79bb,16r79fa,16r79f8,16r79f6,16r79f7,16r7a8f,
+16r7a94,16r7a90,16r7b35,16r7b47,16r7b34,16r7b25,16r7b30,16r7b22,
+16r7b24,16r7b33,16r7b18,16r7b2a,16r7b1d,16r7b31,16r7b2b,16r7b2d,
+16r7b2f,16r7b32,16r7b38,16r7b1a,16r7b23,16r7c94,16r7c98,16r7c96,
+16r7ca3,16r7d35,16r7d3d,16r7d38,16r7d36,16r7d3a,16r7d45,16r7d2c,
+16r7d29,16r7d41,16r7d47,16r7d3e,16r7d3f,16r7d4a,16r7d3b,16r7d28,
+16r7f63,16r7f95,16r7f9c,16r7f9d,16r7f9b,16r7fca,16r7fcb,16r7fcd,
+16r7fd0,16r7fd1,16r7fc7,16r7fcf,16r7fc9,16r801f,16r801e,16r801b,
+16r8047,16r8043,16r8048,16r8118,16r8125,16r8119,16r811b,16r812d,
+16r811f,16r812c,16r811e,16r8121,16r8115,16r8127,16r811d,16r8122,
+16r8211,16r8238,16r8233,16r823a,16r8234,16r8232,16r8274,16r8390,
+16r83a3,16r83a8,16r838d,16r837a,16r8373,16r83a4,16r8374,16r838f,
+16r8381,16r8395,16r8399,16r8375,16r8394,16r83a9,16r837d,16r8383,
+16r838c,16r839d,16r839b,16r83aa,16r838b,16r837e,16r83a5,16r83af,
+16r8388,16r8397,16r83b0,16r837f,16r83a6,16r8387,16r83ae,16r8376,
+16r839a,16r8659,16r8656,16r86bf,16r86b7,16r86c2,16r86c1,16r86c5,
+16r86ba,16r86b0,16r86c8,16r86b9,16r86b3,16r86b8,16r86cc,16r86b4,
+16r86bb,16r86bc,16r86c3,16r86bd,16r86be,16r8852,16r8889,16r8895,
+16r88a8,16r88a2,16r88aa,16r889a,16r8891,16r88a1,16r889f,16r8898,
+16r88a7,16r8899,16r889b,16r8897,16r88a4,16r88ac,16r888c,16r8893,
+16r888e,16r8982,16r89d6,16r89d9,16r89d5,16r8a30,16r8a27,16r8a2c,
+16r8a1e,16r8c39,16r8c3b,16r8c5c,16r8c5d,16r8c7d,16r8ca5,16r8d7d,
+16r8d7b,16r8d79,16r8dbc,16r8dc2,16r8db9,16r8dbf,16r8dc1,16r8ed8,
+16r8ede,16r8edd,16r8edc,16r8ed7,16r8ee0,16r8ee1,16r9024,16r900b,
+16r9011,16r901c,16r900c,16r9021,16r90ef,16r90ea,16r90f0,16r90f4,
+16r90f2,16r90f3,16r90d4,16r90eb,16r90ec,16r90e9,16r9156,16r9158,
+16r915a,16r9153,16r9155,16r91ec,16r91f4,16r91f1,16r91f3,16r91f8,
+16r91e4,16r91f9,16r91ea,16r91eb,16r91f7,16r91e8,16r91ee,16r957a,
+16r9586,16r9588,16r967c,16r966d,16r966b,16r9671,16r966f,16r96bf,
+16r976a,16r9804,16r98e5,16r9997,16r509b,16r5095,16r5094,16r509e,
+16r508b,16r50a3,16r5083,16r508c,16r508e,16r509d,16r5068,16r509c,
+16r5092,16r5082,16r5087,16r515f,16r51d4,16r5312,16r5311,16r53a4,
+16r53a7,16r5591,16r55a8,16r55a5,16r55ad,16r5577,16r5645,16r55a2,
+16r5593,16r5588,16r558f,16r55b5,16r5581,16r55a3,16r5592,16r55a4,
+16r557d,16r558c,16r55a6,16r557f,16r5595,16r55a1,16r558e,16r570c,
+16r5829,16r5837,16r5819,16r581e,16r5827,16r5823,16r5828,16r57f5,
+16r5848,16r5825,16r581c,16r581b,16r5833,16r583f,16r5836,16r582e,
+16r5839,16r5838,16r582d,16r582c,16r583b,16r5961,16r5aaf,16r5a94,
+16r5a9f,16r5a7a,16r5aa2,16r5a9e,16r5a78,16r5aa6,16r5a7c,16r5aa5,
+16r5aac,16r5a95,16r5aae,16r5a37,16r5a84,16r5a8a,16r5a97,16r5a83,
+16r5a8b,16r5aa9,16r5a7b,16r5a7d,16r5a8c,16r5a9c,16r5a8f,16r5a93,
+16r5a9d,16r5bea,16r5bcd,16r5bcb,16r5bd4,16r5bd1,16r5bca,16r5bce,
+16r5c0c,16r5c30,16r5d37,16r5d43,16r5d6b,16r5d41,16r5d4b,16r5d3f,
+16r5d35,16r5d51,16r5d4e,16r5d55,16r5d33,16r5d3a,16r5d52,16r5d3d,
+16r5d31,16r5d59,16r5d42,16r5d39,16r5d49,16r5d38,16r5d3c,16r5d32,
+16r5d36,16r5d40,16r5d45,16r5e44,16r5e41,16r5f58,16r5fa6,16r5fa5,
+16r5fab,16r60c9,16r60b9,16r60cc,16r60e2,16r60ce,16r60c4,16r6114,
+16r60f2,16r610a,16r6116,16r6105,16r60f5,16r6113,16r60f8,16r60fc,
+16r60fe,16r60c1,16r6103,16r6118,16r611d,16r6110,16r60ff,16r6104,
+16r610b,16r624a,16r6394,16r63b1,16r63b0,16r63ce,16r63e5,16r63e8,
+16r63ef,16r63c3,16r649d,16r63f3,16r63ca,16r63e0,16r63f6,16r63d5,
+16r63f2,16r63f5,16r6461,16r63df,16r63be,16r63dd,16r63dc,16r63c4,
+16r63d8,16r63d3,16r63c2,16r63c7,16r63cc,16r63cb,16r63c8,16r63f0,
+16r63d7,16r63d9,16r6532,16r6567,16r656a,16r6564,16r655c,16r6568,
+16r6565,16r658c,16r659d,16r659e,16r65ae,16r65d0,16r65d2,16r667c,
+16r666c,16r667b,16r6680,16r6671,16r6679,16r666a,16r6672,16r6701,
+16r690c,16r68d3,16r6904,16r68dc,16r692a,16r68ec,16r68ea,16r68f1,
+16r690f,16r68d6,16r68f7,16r68eb,16r68e4,16r68f6,16r6913,16r6910,
+16r68f3,16r68e1,16r6907,16r68cc,16r6908,16r6970,16r68b4,16r6911,
+16r68ef,16r68c6,16r6914,16r68f8,16r68d0,16r68fd,16r68fc,16r68e8,
+16r690b,16r690a,16r6917,16r68ce,16r68c8,16r68dd,16r68de,16r68e6,
+16r68f4,16r68d1,16r6906,16r68d4,16r68e9,16r6915,16r6925,16r68c7,
+16r6b39,16r6b3b,16r6b3f,16r6b3c,16r6b94,16r6b97,16r6b99,16r6b95,
+16r6bbd,16r6bf0,16r6bf2,16r6bf3,16r6c30,16r6dfc,16r6e46,16r6e47,
+16r6e1f,16r6e49,16r6e88,16r6e3c,16r6e3d,16r6e45,16r6e62,16r6e2b,
+16r6e3f,16r6e41,16r6e5d,16r6e73,16r6e1c,16r6e33,16r6e4b,16r6e40,
+16r6e51,16r6e3b,16r6e03,16r6e2e,16r6e5e,16r6e68,16r6e5c,16r6e61,
+16r6e31,16r6e28,16r6e60,16r6e71,16r6e6b,16r6e39,16r6e22,16r6e30,
+16r6e53,16r6e65,16r6e27,16r6e78,16r6e64,16r6e77,16r6e55,16r6e79,
+16r6e52,16r6e66,16r6e35,16r6e36,16r6e5a,16r7120,16r711e,16r712f,
+16r70fb,16r712e,16r7131,16r7123,16r7125,16r7122,16r7132,16r711f,
+16r7128,16r713a,16r711b,16r724b,16r725a,16r7288,16r7289,16r7286,
+16r7285,16r728b,16r7312,16r730b,16r7330,16r7322,16r7331,16r7333,
+16r7327,16r7332,16r732d,16r7326,16r7323,16r7335,16r730c,16r742e,
+16r742c,16r7430,16r742b,16r7416,16r741a,16r7421,16r742d,16r7431,
+16r7424,16r7423,16r741d,16r7429,16r7420,16r7432,16r74fb,16r752f,
+16r756f,16r756c,16r75e7,16r75da,16r75e1,16r75e6,16r75dd,16r75df,
+16r75e4,16r75d7,16r7695,16r7692,16r76da,16r7746,16r7747,16r7744,
+16r774d,16r7745,16r774a,16r774e,16r774b,16r774c,16r77de,16r77ec,
+16r7860,16r7864,16r7865,16r785c,16r786d,16r7871,16r786a,16r786e,
+16r7870,16r7869,16r7868,16r785e,16r7862,16r7974,16r7973,16r7972,
+16r7970,16r7a02,16r7a0a,16r7a03,16r7a0c,16r7a04,16r7a99,16r7ae6,
+16r7ae4,16r7b4a,16r7b3b,16r7b44,16r7b48,16r7b4c,16r7b4e,16r7b40,
+16r7b58,16r7b45,16r7ca2,16r7c9e,16r7ca8,16r7ca1,16r7d58,16r7d6f,
+16r7d63,16r7d53,16r7d56,16r7d67,16r7d6a,16r7d4f,16r7d6d,16r7d5c,
+16r7d6b,16r7d52,16r7d54,16r7d69,16r7d51,16r7d5f,16r7d4e,16r7f3e,
+16r7f3f,16r7f65,16r7f66,16r7fa2,16r7fa0,16r7fa1,16r7fd7,16r8051,
+16r804f,16r8050,16r80fe,16r80d4,16r8143,16r814a,16r8152,16r814f,
+16r8147,16r813d,16r814d,16r813a,16r81e6,16r81ee,16r81f7,16r81f8,
+16r81f9,16r8204,16r823c,16r823d,16r823f,16r8275,16r833b,16r83cf,
+16r83f9,16r8423,16r83c0,16r83e8,16r8412,16r83e7,16r83e4,16r83fc,
+16r83f6,16r8410,16r83c6,16r83c8,16r83eb,16r83e3,16r83bf,16r8401,
+16r83dd,16r83e5,16r83d8,16r83ff,16r83e1,16r83cb,16r83ce,16r83d6,
+16r83f5,16r83c9,16r8409,16r840f,16r83de,16r8411,16r8406,16r83c2,
+16r83f3,16r83d5,16r83fa,16r83c7,16r83d1,16r83ea,16r8413,16r83c3,
+16r83ec,16r83ee,16r83c4,16r83fb,16r83d7,16r83e2,16r841b,16r83db,
+16r83fe,16r86d8,16r86e2,16r86e6,16r86d3,16r86e3,16r86da,16r86ea,
+16r86dd,16r86eb,16r86dc,16r86ec,16r86e9,16r86d7,16r86e8,16r86d1,
+16r8848,16r8856,16r8855,16r88ba,16r88d7,16r88b9,16r88b8,16r88c0,
+16r88be,16r88b6,16r88bc,16r88b7,16r88bd,16r88b2,16r8901,16r88c9,
+16r8995,16r8998,16r8997,16r89dd,16r89da,16r89db,16r8a4e,16r8a4d,
+16r8a39,16r8a59,16r8a40,16r8a57,16r8a58,16r8a44,16r8a45,16r8a52,
+16r8a48,16r8a51,16r8a4a,16r8a4c,16r8a4f,16r8c5f,16r8c81,16r8c80,
+16r8cba,16r8cbe,16r8cb0,16r8cb9,16r8cb5,16r8d84,16r8d80,16r8d89,
+16r8dd8,16r8dd3,16r8dcd,16r8dc7,16r8dd6,16r8ddc,16r8dcf,16r8dd5,
+16r8dd9,16r8dc8,16r8dd7,16r8dc5,16r8eef,16r8ef7,16r8efa,16r8ef9,
+16r8ee6,16r8eee,16r8ee5,16r8ef5,16r8ee7,16r8ee8,16r8ef6,16r8eeb,
+16r8ef1,16r8eec,16r8ef4,16r8ee9,16r902d,16r9034,16r902f,16r9106,
+16r912c,16r9104,16r90ff,16r90fc,16r9108,16r90f9,16r90fb,16r9101,
+16r9100,16r9107,16r9105,16r9103,16r9161,16r9164,16r915f,16r9162,
+16r9160,16r9201,16r920a,16r9225,16r9203,16r921a,16r9226,16r920f,
+16r920c,16r9200,16r9212,16r91ff,16r91fd,16r9206,16r9204,16r9227,
+16r9202,16r921c,16r9224,16r9219,16r9217,16r9205,16r9216,16r957b,
+16r958d,16r958c,16r9590,16r9687,16r967e,16r9688,16r9689,16r9683,
+16r9680,16r96c2,16r96c8,16r96c3,16r96f1,16r96f0,16r976c,16r9770,
+16r976e,16r9807,16r98a9,16r98eb,16r9ce6,16r9ef9,16r4e83,16r4e84,
+16r4eb6,16r50bd,16r50bf,16r50c6,16r50ae,16r50c4,16r50ca,16r50b4,
+16r50c8,16r50c2,16r50b0,16r50c1,16r50ba,16r50b1,16r50cb,16r50c9,
+16r50b6,16r50b8,16r51d7,16r527a,16r5278,16r527b,16r527c,16r55c3,
+16r55db,16r55cc,16r55d0,16r55cb,16r55ca,16r55dd,16r55c0,16r55d4,
+16r55c4,16r55e9,16r55bf,16r55d2,16r558d,16r55cf,16r55d5,16r55e2,
+16r55d6,16r55c8,16r55f2,16r55cd,16r55d9,16r55c2,16r5714,16r5853,
+16r5868,16r5864,16r584f,16r584d,16r5849,16r586f,16r5855,16r584e,
+16r585d,16r5859,16r5865,16r585b,16r583d,16r5863,16r5871,16r58fc,
+16r5ac7,16r5ac4,16r5acb,16r5aba,16r5ab8,16r5ab1,16r5ab5,16r5ab0,
+16r5abf,16r5ac8,16r5abb,16r5ac6,16r5ab7,16r5ac0,16r5aca,16r5ab4,
+16r5ab6,16r5acd,16r5ab9,16r5a90,16r5bd6,16r5bd8,16r5bd9,16r5c1f,
+16r5c33,16r5d71,16r5d63,16r5d4a,16r5d65,16r5d72,16r5d6c,16r5d5e,
+16r5d68,16r5d67,16r5d62,16r5df0,16r5e4f,16r5e4e,16r5e4a,16r5e4d,
+16r5e4b,16r5ec5,16r5ecc,16r5ec6,16r5ecb,16r5ec7,16r5f40,16r5faf,
+16r5fad,16r60f7,16r6149,16r614a,16r612b,16r6145,16r6136,16r6132,
+16r612e,16r6146,16r612f,16r614f,16r6129,16r6140,16r6220,16r9168,
+16r6223,16r6225,16r6224,16r63c5,16r63f1,16r63eb,16r6410,16r6412,
+16r6409,16r6420,16r6424,16r6433,16r6443,16r641f,16r6415,16r6418,
+16r6439,16r6437,16r6422,16r6423,16r640c,16r6426,16r6430,16r6428,
+16r6441,16r6435,16r642f,16r640a,16r641a,16r6440,16r6425,16r6427,
+16r640b,16r63e7,16r641b,16r642e,16r6421,16r640e,16r656f,16r6592,
+16r65d3,16r6686,16r668c,16r6695,16r6690,16r668b,16r668a,16r6699,
+16r6694,16r6678,16r6720,16r6966,16r695f,16r6938,16r694e,16r6962,
+16r6971,16r693f,16r6945,16r696a,16r6939,16r6942,16r6957,16r6959,
+16r697a,16r6948,16r6949,16r6935,16r696c,16r6933,16r693d,16r6965,
+16r68f0,16r6978,16r6934,16r6969,16r6940,16r696f,16r6944,16r6976,
+16r6958,16r6941,16r6974,16r694c,16r693b,16r694b,16r6937,16r695c,
+16r694f,16r6951,16r6932,16r6952,16r692f,16r697b,16r693c,16r6b46,
+16r6b45,16r6b43,16r6b42,16r6b48,16r6b41,16r6b9b,16rfa0d,16r6bfb,
+16r6bfc,16r6bf9,16r6bf7,16r6bf8,16r6e9b,16r6ed6,16r6ec8,16r6e8f,
+16r6ec0,16r6e9f,16r6e93,16r6e94,16r6ea0,16r6eb1,16r6eb9,16r6ec6,
+16r6ed2,16r6ebd,16r6ec1,16r6e9e,16r6ec9,16r6eb7,16r6eb0,16r6ecd,
+16r6ea6,16r6ecf,16r6eb2,16r6ebe,16r6ec3,16r6edc,16r6ed8,16r6e99,
+16r6e92,16r6e8e,16r6e8d,16r6ea4,16r6ea1,16r6ebf,16r6eb3,16r6ed0,
+16r6eca,16r6e97,16r6eae,16r6ea3,16r7147,16r7154,16r7152,16r7163,
+16r7160,16r7141,16r715d,16r7162,16r7172,16r7178,16r716a,16r7161,
+16r7142,16r7158,16r7143,16r714b,16r7170,16r715f,16r7150,16r7153,
+16r7144,16r714d,16r715a,16r724f,16r728d,16r728c,16r7291,16r7290,
+16r728e,16r733c,16r7342,16r733b,16r733a,16r7340,16r734a,16r7349,
+16r7444,16r744a,16r744b,16r7452,16r7451,16r7457,16r7440,16r744f,
+16r7450,16r744e,16r7442,16r7446,16r744d,16r7454,16r74e1,16r74ff,
+16r74fe,16r74fd,16r751d,16r7579,16r7577,16r6983,16r75ef,16r760f,
+16r7603,16r75f7,16r75fe,16r75fc,16r75f9,16r75f8,16r7610,16r75fb,
+16r75f6,16r75ed,16r75f5,16r75fd,16r7699,16r76b5,16r76dd,16r7755,
+16r775f,16r7760,16r7752,16r7756,16r775a,16r7769,16r7767,16r7754,
+16r7759,16r776d,16r77e0,16r7887,16r789a,16r7894,16r788f,16r7884,
+16r7895,16r7885,16r7886,16r78a1,16r7883,16r7879,16r7899,16r7880,
+16r7896,16r787b,16r797c,16r7982,16r797d,16r7979,16r7a11,16r7a18,
+16r7a19,16r7a12,16r7a17,16r7a15,16r7a22,16r7a13,16r7a1b,16r7a10,
+16r7aa3,16r7aa2,16r7a9e,16r7aeb,16r7b66,16r7b64,16r7b6d,16r7b74,
+16r7b69,16r7b72,16r7b65,16r7b73,16r7b71,16r7b70,16r7b61,16r7b78,
+16r7b76,16r7b63,16r7cb2,16r7cb4,16r7caf,16r7d88,16r7d86,16r7d80,
+16r7d8d,16r7d7f,16r7d85,16r7d7a,16r7d8e,16r7d7b,16r7d83,16r7d7c,
+16r7d8c,16r7d94,16r7d84,16r7d7d,16r7d92,16r7f6d,16r7f6b,16r7f67,
+16r7f68,16r7f6c,16r7fa6,16r7fa5,16r7fa7,16r7fdb,16r7fdc,16r8021,
+16r8164,16r8160,16r8177,16r815c,16r8169,16r815b,16r8162,16r8172,
+16r6721,16r815e,16r8176,16r8167,16r816f,16r8144,16r8161,16r821d,
+16r8249,16r8244,16r8240,16r8242,16r8245,16r84f1,16r843f,16r8456,
+16r8476,16r8479,16r848f,16r848d,16r8465,16r8451,16r8440,16r8486,
+16r8467,16r8430,16r844d,16r847d,16r845a,16r8459,16r8474,16r8473,
+16r845d,16r8507,16r845e,16r8437,16r843a,16r8434,16r847a,16r8443,
+16r8478,16r8432,16r8445,16r8429,16r83d9,16r844b,16r842f,16r8442,
+16r842d,16r845f,16r8470,16r8439,16r844e,16r844c,16r8452,16r846f,
+16r84c5,16r848e,16r843b,16r8447,16r8436,16r8433,16r8468,16r847e,
+16r8444,16r842b,16r8460,16r8454,16r846e,16r8450,16r870b,16r8704,
+16r86f7,16r870c,16r86fa,16r86d6,16r86f5,16r874d,16r86f8,16r870e,
+16r8709,16r8701,16r86f6,16r870d,16r8705,16r88d6,16r88cb,16r88cd,
+16r88ce,16r88de,16r88db,16r88da,16r88cc,16r88d0,16r8985,16r899b,
+16r89df,16r89e5,16r89e4,16r89e1,16r89e0,16r89e2,16r89dc,16r89e6,
+16r8a76,16r8a86,16r8a7f,16r8a61,16r8a3f,16r8a77,16r8a82,16r8a84,
+16r8a75,16r8a83,16r8a81,16r8a74,16r8a7a,16r8c3c,16r8c4b,16r8c4a,
+16r8c65,16r8c64,16r8c66,16r8c86,16r8c84,16r8c85,16r8ccc,16r8d68,
+16r8d69,16r8d91,16r8d8c,16r8d8e,16r8d8f,16r8d8d,16r8d93,16r8d94,
+16r8d90,16r8d92,16r8df0,16r8de0,16r8dec,16r8df1,16r8dee,16r8dd0,
+16r8de9,16r8de3,16r8de2,16r8de7,16r8df2,16r8deb,16r8df4,16r8f06,
+16r8eff,16r8f01,16r8f00,16r8f05,16r8f07,16r8f08,16r8f02,16r8f0b,
+16r9052,16r903f,16r9044,16r9049,16r903d,16r9110,16r910d,16r910f,
+16r9111,16r9116,16r9114,16r910b,16r910e,16r916e,16r916f,16r9248,
+16r9252,16r9230,16r923a,16r9266,16r9233,16r9265,16r925e,16r9283,
+16r922e,16r924a,16r9246,16r926d,16r926c,16r924f,16r9260,16r9267,
+16r926f,16r9236,16r9261,16r9270,16r9231,16r9254,16r9263,16r9250,
+16r9272,16r924e,16r9253,16r924c,16r9256,16r9232,16r959f,16r959c,
+16r959e,16r959b,16r9692,16r9693,16r9691,16r9697,16r96ce,16r96fa,
+16r96fd,16r96f8,16r96f5,16r9773,16r9777,16r9778,16r9772,16r980f,
+16r980d,16r980e,16r98ac,16r98f6,16r98f9,16r99af,16r99b2,16r99b0,
+16r99b5,16r9aad,16r9aab,16r9b5b,16r9cea,16r9ced,16r9ce7,16r9e80,
+16r9efd,16r50e6,16r50d4,16r50d7,16r50e8,16r50f3,16r50db,16r50ea,
+16r50dd,16r50e4,16r50d3,16r50ec,16r50f0,16r50ef,16r50e3,16r50e0,
+16r51d8,16r5280,16r5281,16r52e9,16r52eb,16r5330,16r53ac,16r5627,
+16r5615,16r560c,16r5612,16r55fc,16r560f,16r561c,16r5601,16r5613,
+16r5602,16r55fa,16r561d,16r5604,16r55ff,16r55f9,16r5889,16r587c,
+16r5890,16r5898,16r5886,16r5881,16r587f,16r5874,16r588b,16r587a,
+16r5887,16r5891,16r588e,16r5876,16r5882,16r5888,16r587b,16r5894,
+16r588f,16r58fe,16r596b,16r5adc,16r5aee,16r5ae5,16r5ad5,16r5aea,
+16r5ada,16r5aed,16r5aeb,16r5af3,16r5ae2,16r5ae0,16r5adb,16r5aec,
+16r5ade,16r5add,16r5ad9,16r5ae8,16r5adf,16r5b77,16r5be0,16r5be3,
+16r5c63,16r5d82,16r5d80,16r5d7d,16r5d86,16r5d7a,16r5d81,16r5d77,
+16r5d8a,16r5d89,16r5d88,16r5d7e,16r5d7c,16r5d8d,16r5d79,16r5d7f,
+16r5e58,16r5e59,16r5e53,16r5ed8,16r5ed1,16r5ed7,16r5ece,16r5edc,
+16r5ed5,16r5ed9,16r5ed2,16r5ed4,16r5f44,16r5f43,16r5f6f,16r5fb6,
+16r612c,16r6128,16r6141,16r615e,16r6171,16r6173,16r6152,16r6153,
+16r6172,16r616c,16r6180,16r6174,16r6154,16r617a,16r615b,16r6165,
+16r613b,16r616a,16r6161,16r6156,16r6229,16r6227,16r622b,16r642b,
+16r644d,16r645b,16r645d,16r6474,16r6476,16r6472,16r6473,16r647d,
+16r6475,16r6466,16r64a6,16r644e,16r6482,16r645e,16r645c,16r644b,
+16r6453,16r6460,16r6450,16r647f,16r643f,16r646c,16r646b,16r6459,
+16r6465,16r6477,16r6573,16r65a0,16r66a1,16r66a0,16r669f,16r6705,
+16r6704,16r6722,16r69b1,16r69b6,16r69c9,16r69a0,16r69ce,16r6996,
+16r69b0,16r69ac,16r69bc,16r6991,16r6999,16r698e,16r69a7,16r698d,
+16r69a9,16r69be,16r69af,16r69bf,16r69c4,16r69bd,16r69a4,16r69d4,
+16r69b9,16r69ca,16r699a,16r69cf,16r69b3,16r6993,16r69aa,16r69a1,
+16r699e,16r69d9,16r6997,16r6990,16r69c2,16r69b5,16r69a5,16r69c6,
+16r6b4a,16r6b4d,16r6b4b,16r6b9e,16r6b9f,16r6ba0,16r6bc3,16r6bc4,
+16r6bfe,16r6ece,16r6ef5,16r6ef1,16r6f03,16r6f25,16r6ef8,16r6f37,
+16r6efb,16r6f2e,16r6f09,16r6f4e,16r6f19,16r6f1a,16r6f27,16r6f18,
+16r6f3b,16r6f12,16r6eed,16r6f0a,16r6f36,16r6f73,16r6ef9,16r6eee,
+16r6f2d,16r6f40,16r6f30,16r6f3c,16r6f35,16r6eeb,16r6f07,16r6f0e,
+16r6f43,16r6f05,16r6efd,16r6ef6,16r6f39,16r6f1c,16r6efc,16r6f3a,
+16r6f1f,16r6f0d,16r6f1e,16r6f08,16r6f21,16r7187,16r7190,16r7189,
+16r7180,16r7185,16r7182,16r718f,16r717b,16r7186,16r7181,16r7197,
+16r7244,16r7253,16r7297,16r7295,16r7293,16r7343,16r734d,16r7351,
+16r734c,16r7462,16r7473,16r7471,16r7475,16r7472,16r7467,16r746e,
+16r7500,16r7502,16r7503,16r757d,16r7590,16r7616,16r7608,16r760c,
+16r7615,16r7611,16r760a,16r7614,16r76b8,16r7781,16r777c,16r7785,
+16r7782,16r776e,16r7780,16r776f,16r777e,16r7783,16r78b2,16r78aa,
+16r78b4,16r78ad,16r78a8,16r787e,16r78ab,16r789e,16r78a5,16r78a0,
+16r78ac,16r78a2,16r78a4,16r7998,16r798a,16r798b,16r7996,16r7995,
+16r7994,16r7993,16r7997,16r7988,16r7992,16r7990,16r7a2b,16r7a4a,
+16r7a30,16r7a2f,16r7a28,16r7a26,16r7aa8,16r7aab,16r7aac,16r7aee,
+16r7b88,16r7b9c,16r7b8a,16r7b91,16r7b90,16r7b96,16r7b8d,16r7b8c,
+16r7b9b,16r7b8e,16r7b85,16r7b98,16r5284,16r7b99,16r7ba4,16r7b82,
+16r7cbb,16r7cbf,16r7cbc,16r7cba,16r7da7,16r7db7,16r7dc2,16r7da3,
+16r7daa,16r7dc1,16r7dc0,16r7dc5,16r7d9d,16r7dce,16r7dc4,16r7dc6,
+16r7dcb,16r7dcc,16r7daf,16r7db9,16r7d96,16r7dbc,16r7d9f,16r7da6,
+16r7dae,16r7da9,16r7da1,16r7dc9,16r7f73,16r7fe2,16r7fe3,16r7fe5,
+16r7fde,16r8024,16r805d,16r805c,16r8189,16r8186,16r8183,16r8187,
+16r818d,16r818c,16r818b,16r8215,16r8497,16r84a4,16r84a1,16r849f,
+16r84ba,16r84ce,16r84c2,16r84ac,16r84ae,16r84ab,16r84b9,16r84b4,
+16r84c1,16r84cd,16r84aa,16r849a,16r84b1,16r84d0,16r849d,16r84a7,
+16r84bb,16r84a2,16r8494,16r84c7,16r84cc,16r849b,16r84a9,16r84af,
+16r84a8,16r84d6,16r8498,16r84b6,16r84cf,16r84a0,16r84d7,16r84d4,
+16r84d2,16r84db,16r84b0,16r8491,16r8661,16r8733,16r8723,16r8728,
+16r876b,16r8740,16r872e,16r871e,16r8721,16r8719,16r871b,16r8743,
+16r872c,16r8741,16r873e,16r8746,16r8720,16r8732,16r872a,16r872d,
+16r873c,16r8712,16r873a,16r8731,16r8735,16r8742,16r8726,16r8727,
+16r8738,16r8724,16r871a,16r8730,16r8711,16r88f7,16r88e7,16r88f1,
+16r88f2,16r88fa,16r88fe,16r88ee,16r88fc,16r88f6,16r88fb,16r88f0,
+16r88ec,16r88eb,16r899d,16r89a1,16r899f,16r899e,16r89e9,16r89eb,
+16r89e8,16r8aab,16r8a99,16r8a8b,16r8a92,16r8a8f,16r8a96,16r8c3d,
+16r8c68,16r8c69,16r8cd5,16r8ccf,16r8cd7,16r8d96,16r8e09,16r8e02,
+16r8dff,16r8e0d,16r8dfd,16r8e0a,16r8e03,16r8e07,16r8e06,16r8e05,
+16r8dfe,16r8e00,16r8e04,16r8f10,16r8f11,16r8f0e,16r8f0d,16r9123,
+16r911c,16r9120,16r9122,16r911f,16r911d,16r911a,16r9124,16r9121,
+16r911b,16r917a,16r9172,16r9179,16r9173,16r92a5,16r92a4,16r9276,
+16r929b,16r927a,16r92a0,16r9294,16r92aa,16r928d,16r92a6,16r929a,
+16r92ab,16r9279,16r9297,16r927f,16r92a3,16r92ee,16r928e,16r9282,
+16r9295,16r92a2,16r927d,16r9288,16r92a1,16r928a,16r9286,16r928c,
+16r9299,16r92a7,16r927e,16r9287,16r92a9,16r929d,16r928b,16r922d,
+16r969e,16r96a1,16r96ff,16r9758,16r977d,16r977a,16r977e,16r9783,
+16r9780,16r9782,16r977b,16r9784,16r9781,16r977f,16r97ce,16r97cd,
+16r9816,16r98ad,16r98ae,16r9902,16r9900,16r9907,16r999d,16r999c,
+16r99c3,16r99b9,16r99bb,16r99ba,16r99c2,16r99bd,16r99c7,16r9ab1,
+16r9ae3,16r9ae7,16r9b3e,16r9b3f,16r9b60,16r9b61,16r9b5f,16r9cf1,
+16r9cf2,16r9cf5,16r9ea7,16r50ff,16r5103,16r5130,16r50f8,16r5106,
+16r5107,16r50f6,16r50fe,16r510b,16r510c,16r50fd,16r510a,16r528b,
+16r528c,16r52f1,16r52ef,16r5648,16r5642,16r564c,16r5635,16r5641,
+16r564a,16r5649,16r5646,16r5658,16r565a,16r5640,16r5633,16r563d,
+16r562c,16r563e,16r5638,16r562a,16r563a,16r571a,16r58ab,16r589d,
+16r58b1,16r58a0,16r58a3,16r58af,16r58ac,16r58a5,16r58a1,16r58ff,
+16r5aff,16r5af4,16r5afd,16r5af7,16r5af6,16r5b03,16r5af8,16r5b02,
+16r5af9,16r5b01,16r5b07,16r5b05,16r5b0f,16r5c67,16r5d99,16r5d97,
+16r5d9f,16r5d92,16r5da2,16r5d93,16r5d95,16r5da0,16r5d9c,16r5da1,
+16r5d9a,16r5d9e,16r5e69,16r5e5d,16r5e60,16r5e5c,16r7df3,16r5edb,
+16r5ede,16r5ee1,16r5f49,16r5fb2,16r618b,16r6183,16r6179,16r61b1,
+16r61b0,16r61a2,16r6189,16r619b,16r6193,16r61af,16r61ad,16r619f,
+16r6192,16r61aa,16r61a1,16r618d,16r6166,16r61b3,16r622d,16r646e,
+16r6470,16r6496,16r64a0,16r6485,16r6497,16r649c,16r648f,16r648b,
+16r648a,16r648c,16r64a3,16r649f,16r6468,16r64b1,16r6498,16r6576,
+16r657a,16r6579,16r657b,16r65b2,16r65b3,16r66b5,16r66b0,16r66a9,
+16r66b2,16r66b7,16r66aa,16r66af,16r6a00,16r6a06,16r6a17,16r69e5,
+16r69f8,16r6a15,16r69f1,16r69e4,16r6a20,16r69ff,16r69ec,16r69e2,
+16r6a1b,16r6a1d,16r69fe,16r6a27,16r69f2,16r69ee,16r6a14,16r69f7,
+16r69e7,16r6a40,16r6a08,16r69e6,16r69fb,16r6a0d,16r69fc,16r69eb,
+16r6a09,16r6a04,16r6a18,16r6a25,16r6a0f,16r69f6,16r6a26,16r6a07,
+16r69f4,16r6a16,16r6b51,16r6ba5,16r6ba3,16r6ba2,16r6ba6,16r6c01,
+16r6c00,16r6bff,16r6c02,16r6f41,16r6f26,16r6f7e,16r6f87,16r6fc6,
+16r6f92,16r6f8d,16r6f89,16r6f8c,16r6f62,16r6f4f,16r6f85,16r6f5a,
+16r6f96,16r6f76,16r6f6c,16r6f82,16r6f55,16r6f72,16r6f52,16r6f50,
+16r6f57,16r6f94,16r6f93,16r6f5d,16r6f00,16r6f61,16r6f6b,16r6f7d,
+16r6f67,16r6f90,16r6f53,16r6f8b,16r6f69,16r6f7f,16r6f95,16r6f63,
+16r6f77,16r6f6a,16r6f7b,16r71b2,16r71af,16r719b,16r71b0,16r71a0,
+16r719a,16r71a9,16r71b5,16r719d,16r71a5,16r719e,16r71a4,16r71a1,
+16r71aa,16r719c,16r71a7,16r71b3,16r7298,16r729a,16r7358,16r7352,
+16r735e,16r735f,16r7360,16r735d,16r735b,16r7361,16r735a,16r7359,
+16r7362,16r7487,16r7489,16r748a,16r7486,16r7481,16r747d,16r7485,
+16r7488,16r747c,16r7479,16r7508,16r7507,16r757e,16r7625,16r761e,
+16r7619,16r761d,16r761c,16r7623,16r761a,16r7628,16r761b,16r769c,
+16r769d,16r769e,16r769b,16r778d,16r778f,16r7789,16r7788,16r78cd,
+16r78bb,16r78cf,16r78cc,16r78d1,16r78ce,16r78d4,16r78c8,16r78c3,
+16r78c4,16r78c9,16r799a,16r79a1,16r79a0,16r799c,16r79a2,16r799b,
+16r6b76,16r7a39,16r7ab2,16r7ab4,16r7ab3,16r7bb7,16r7bcb,16r7bbe,
+16r7bac,16r7bce,16r7baf,16r7bb9,16r7bca,16r7bb5,16r7cc5,16r7cc8,
+16r7ccc,16r7ccb,16r7df7,16r7ddb,16r7dea,16r7de7,16r7dd7,16r7de1,
+16r7e03,16r7dfa,16r7de6,16r7df6,16r7df1,16r7df0,16r7dee,16r7ddf,
+16r7f76,16r7fac,16r7fb0,16r7fad,16r7fed,16r7feb,16r7fea,16r7fec,
+16r7fe6,16r7fe8,16r8064,16r8067,16r81a3,16r819f,16r819e,16r8195,
+16r81a2,16r8199,16r8197,16r8216,16r824f,16r8253,16r8252,16r8250,
+16r824e,16r8251,16r8524,16r853b,16r850f,16r8500,16r8529,16r850e,
+16r8509,16r850d,16r851f,16r850a,16r8527,16r851c,16r84fb,16r852b,
+16r84fa,16r8508,16r850c,16r84f4,16r852a,16r84f2,16r8515,16r84f7,
+16r84eb,16r84f3,16r84fc,16r8512,16r84ea,16r84e9,16r8516,16r84fe,
+16r8528,16r851d,16r852e,16r8502,16r84fd,16r851e,16r84f6,16r8531,
+16r8526,16r84e7,16r84e8,16r84f0,16r84ef,16r84f9,16r8518,16r8520,
+16r8530,16r850b,16r8519,16r852f,16r8662,16r8756,16r8763,16r8764,
+16r8777,16r87e1,16r8773,16r8758,16r8754,16r875b,16r8752,16r8761,
+16r875a,16r8751,16r875e,16r876d,16r876a,16r8750,16r874e,16r875f,
+16r875d,16r876f,16r876c,16r877a,16r876e,16r875c,16r8765,16r874f,
+16r877b,16r8775,16r8762,16r8767,16r8769,16r885a,16r8905,16r890c,
+16r8914,16r890b,16r8917,16r8918,16r8919,16r8906,16r8916,16r8911,
+16r890e,16r8909,16r89a2,16r89a4,16r89a3,16r89ed,16r89f0,16r89ec,
+16r8acf,16r8ac6,16r8ab8,16r8ad3,16r8ad1,16r8ad4,16r8ad5,16r8abb,
+16r8ad7,16r8abe,16r8ac0,16r8ac5,16r8ad8,16r8ac3,16r8aba,16r8abd,
+16r8ad9,16r8c3e,16r8c4d,16r8c8f,16r8ce5,16r8cdf,16r8cd9,16r8ce8,
+16r8cda,16r8cdd,16r8ce7,16r8da0,16r8d9c,16r8da1,16r8d9b,16r8e20,
+16r8e23,16r8e25,16r8e24,16r8e2e,16r8e15,16r8e1b,16r8e16,16r8e11,
+16r8e19,16r8e26,16r8e27,16r8e14,16r8e12,16r8e18,16r8e13,16r8e1c,
+16r8e17,16r8e1a,16r8f2c,16r8f24,16r8f18,16r8f1a,16r8f20,16r8f23,
+16r8f16,16r8f17,16r9073,16r9070,16r906f,16r9067,16r906b,16r912f,
+16r912b,16r9129,16r912a,16r9132,16r9126,16r912e,16r9185,16r9186,
+16r918a,16r9181,16r9182,16r9184,16r9180,16r92d0,16r92c3,16r92c4,
+16r92c0,16r92d9,16r92b6,16r92cf,16r92f1,16r92df,16r92d8,16r92e9,
+16r92d7,16r92dd,16r92cc,16r92ef,16r92c2,16r92e8,16r92ca,16r92c8,
+16r92ce,16r92e6,16r92cd,16r92d5,16r92c9,16r92e0,16r92de,16r92e7,
+16r92d1,16r92d3,16r92b5,16r92e1,16r92c6,16r92b4,16r957c,16r95ac,
+16r95ab,16r95ae,16r95b0,16r96a4,16r96a2,16r96d3,16r9705,16r9708,
+16r9702,16r975a,16r978a,16r978e,16r9788,16r97d0,16r97cf,16r981e,
+16r981d,16r9826,16r9829,16r9828,16r9820,16r981b,16r9827,16r98b2,
+16r9908,16r98fa,16r9911,16r9914,16r9916,16r9917,16r9915,16r99dc,
+16r99cd,16r99cf,16r99d3,16r99d4,16r99ce,16r99c9,16r99d6,16r99d8,
+16r99cb,16r99d7,16r99cc,16r9ab3,16r9aec,16r9aeb,16r9af3,16r9af2,
+16r9af1,16r9b46,16r9b43,16r9b67,16r9b74,16r9b71,16r9b66,16r9b76,
+16r9b75,16r9b70,16r9b68,16r9b64,16r9b6c,16r9cfc,16r9cfa,16r9cfd,
+16r9cff,16r9cf7,16r9d07,16r9d00,16r9cf9,16r9cfb,16r9d08,16r9d05,
+16r9d04,16r9e83,16r9ed3,16r9f0f,16r9f10,16r511c,16r5113,16r5117,
+16r511a,16r5111,16r51de,16r5334,16r53e1,16r5670,16r5660,16r566e,
+16r5673,16r5666,16r5663,16r566d,16r5672,16r565e,16r5677,16r571c,
+16r571b,16r58c8,16r58bd,16r58c9,16r58bf,16r58ba,16r58c2,16r58bc,
+16r58c6,16r5b17,16r5b19,16r5b1b,16r5b21,16r5b14,16r5b13,16r5b10,
+16r5b16,16r5b28,16r5b1a,16r5b20,16r5b1e,16r5bef,16r5dac,16r5db1,
+16r5da9,16r5da7,16r5db5,16r5db0,16r5dae,16r5daa,16r5da8,16r5db2,
+16r5dad,16r5daf,16r5db4,16r5e67,16r5e68,16r5e66,16r5e6f,16r5ee9,
+16r5ee7,16r5ee6,16r5ee8,16r5ee5,16r5f4b,16r5fbc,16r619d,16r61a8,
+16r6196,16r61c5,16r61b4,16r61c6,16r61c1,16r61cc,16r61ba,16r61bf,
+16r61b8,16r618c,16r64d7,16r64d6,16r64d0,16r64cf,16r64c9,16r64bd,
+16r6489,16r64c3,16r64db,16r64f3,16r64d9,16r6533,16r657f,16r657c,
+16r65a2,16r66c8,16r66be,16r66c0,16r66ca,16r66cb,16r66cf,16r66bd,
+16r66bb,16r66ba,16r66cc,16r6723,16r6a34,16r6a66,16r6a49,16r6a67,
+16r6a32,16r6a68,16r6a3e,16r6a5d,16r6a6d,16r6a76,16r6a5b,16r6a51,
+16r6a28,16r6a5a,16r6a3b,16r6a3f,16r6a41,16r6a6a,16r6a64,16r6a50,
+16r6a4f,16r6a54,16r6a6f,16r6a69,16r6a60,16r6a3c,16r6a5e,16r6a56,
+16r6a55,16r6a4d,16r6a4e,16r6a46,16r6b55,16r6b54,16r6b56,16r6ba7,
+16r6baa,16r6bab,16r6bc8,16r6bc7,16r6c04,16r6c03,16r6c06,16r6fad,
+16r6fcb,16r6fa3,16r6fc7,16r6fbc,16r6fce,16r6fc8,16r6f5e,16r6fc4,
+16r6fbd,16r6f9e,16r6fca,16r6fa8,16r7004,16r6fa5,16r6fae,16r6fba,
+16r6fac,16r6faa,16r6fcf,16r6fbf,16r6fb8,16r6fa2,16r6fc9,16r6fab,
+16r6fcd,16r6faf,16r6fb2,16r6fb0,16r71c5,16r71c2,16r71bf,16r71b8,
+16r71d6,16r71c0,16r71c1,16r71cb,16r71d4,16r71ca,16r71c7,16r71cf,
+16r71bd,16r71d8,16r71bc,16r71c6,16r71da,16r71db,16r729d,16r729e,
+16r7369,16r7366,16r7367,16r736c,16r7365,16r736b,16r736a,16r747f,
+16r749a,16r74a0,16r7494,16r7492,16r7495,16r74a1,16r750b,16r7580,
+16r762f,16r762d,16r7631,16r763d,16r7633,16r763c,16r7635,16r7632,
+16r7630,16r76bb,16r76e6,16r779a,16r779d,16r77a1,16r779c,16r779b,
+16r77a2,16r77a3,16r7795,16r7799,16r7797,16r78dd,16r78e9,16r78e5,
+16r78ea,16r78de,16r78e3,16r78db,16r78e1,16r78e2,16r78ed,16r78df,
+16r78e0,16r79a4,16r7a44,16r7a48,16r7a47,16r7ab6,16r7ab8,16r7ab5,
+16r7ab1,16r7ab7,16r7bde,16r7be3,16r7be7,16r7bdd,16r7bd5,16r7be5,
+16r7bda,16r7be8,16r7bf9,16r7bd4,16r7bea,16r7be2,16r7bdc,16r7beb,
+16r7bd8,16r7bdf,16r7cd2,16r7cd4,16r7cd7,16r7cd0,16r7cd1,16r7e12,
+16r7e21,16r7e17,16r7e0c,16r7e1f,16r7e20,16r7e13,16r7e0e,16r7e1c,
+16r7e15,16r7e1a,16r7e22,16r7e0b,16r7e0f,16r7e16,16r7e0d,16r7e14,
+16r7e25,16r7e24,16r7f43,16r7f7b,16r7f7c,16r7f7a,16r7fb1,16r7fef,
+16r802a,16r8029,16r806c,16r81b1,16r81a6,16r81ae,16r81b9,16r81b5,
+16r81ab,16r81b0,16r81ac,16r81b4,16r81b2,16r81b7,16r81a7,16r81f2,
+16r8255,16r8256,16r8257,16r8556,16r8545,16r856b,16r854d,16r8553,
+16r8561,16r8558,16r8540,16r8546,16r8564,16r8541,16r8562,16r8544,
+16r8551,16r8547,16r8563,16r853e,16r855b,16r8571,16r854e,16r856e,
+16r8575,16r8555,16r8567,16r8560,16r858c,16r8566,16r855d,16r8554,
+16r8565,16r856c,16r8663,16r8665,16r8664,16r879b,16r878f,16r8797,
+16r8793,16r8792,16r8788,16r8781,16r8796,16r8798,16r8779,16r8787,
+16r87a3,16r8785,16r8790,16r8791,16r879d,16r8784,16r8794,16r879c,
+16r879a,16r8789,16r891e,16r8926,16r8930,16r892d,16r892e,16r8927,
+16r8931,16r8922,16r8929,16r8923,16r892f,16r892c,16r891f,16r89f1,
+16r8ae0,16r8ae2,16r8af2,16r8af4,16r8af5,16r8add,16r8b14,16r8ae4,
+16r8adf,16r8af0,16r8ac8,16r8ade,16r8ae1,16r8ae8,16r8aff,16r8aef,
+16r8afb,16r8c91,16r8c92,16r8c90,16r8cf5,16r8cee,16r8cf1,16r8cf0,
+16r8cf3,16r8d6c,16r8d6e,16r8da5,16r8da7,16r8e33,16r8e3e,16r8e38,
+16r8e40,16r8e45,16r8e36,16r8e3c,16r8e3d,16r8e41,16r8e30,16r8e3f,
+16r8ebd,16r8f36,16r8f2e,16r8f35,16r8f32,16r8f39,16r8f37,16r8f34,
+16r9076,16r9079,16r907b,16r9086,16r90fa,16r9133,16r9135,16r9136,
+16r9193,16r9190,16r9191,16r918d,16r918f,16r9327,16r931e,16r9308,
+16r931f,16r9306,16r930f,16r937a,16r9338,16r933c,16r931b,16r9323,
+16r9312,16r9301,16r9346,16r932d,16r930e,16r930d,16r92cb,16r931d,
+16r92fa,16r9325,16r9313,16r92f9,16r92f7,16r9334,16r9302,16r9324,
+16r92ff,16r9329,16r9339,16r9335,16r932a,16r9314,16r930c,16r930b,
+16r92fe,16r9309,16r9300,16r92fb,16r9316,16r95bc,16r95cd,16r95be,
+16r95b9,16r95ba,16r95b6,16r95bf,16r95b5,16r95bd,16r96a9,16r96d4,
+16r970b,16r9712,16r9710,16r9799,16r9797,16r9794,16r97f0,16r97f8,
+16r9835,16r982f,16r9832,16r9924,16r991f,16r9927,16r9929,16r999e,
+16r99ee,16r99ec,16r99e5,16r99e4,16r99f0,16r99e3,16r99ea,16r99e9,
+16r99e7,16r9ab9,16r9abf,16r9ab4,16r9abb,16r9af6,16r9afa,16r9af9,
+16r9af7,16r9b33,16r9b80,16r9b85,16r9b87,16r9b7c,16r9b7e,16r9b7b,
+16r9b82,16r9b93,16r9b92,16r9b90,16r9b7a,16r9b95,16r9b7d,16r9b88,
+16r9d25,16r9d17,16r9d20,16r9d1e,16r9d14,16r9d29,16r9d1d,16r9d18,
+16r9d22,16r9d10,16r9d19,16r9d1f,16r9e88,16r9e86,16r9e87,16r9eae,
+16r9ead,16r9ed5,16r9ed6,16r9efa,16r9f12,16r9f3d,16r5126,16r5125,
+16r5122,16r5124,16r5120,16r5129,16r52f4,16r5693,16r568c,16r568d,
+16r5686,16r5684,16r5683,16r567e,16r5682,16r567f,16r5681,16r58d6,
+16r58d4,16r58cf,16r58d2,16r5b2d,16r5b25,16r5b32,16r5b23,16r5b2c,
+16r5b27,16r5b26,16r5b2f,16r5b2e,16r5b7b,16r5bf1,16r5bf2,16r5db7,
+16r5e6c,16r5e6a,16r5fbe,16r5fbb,16r61c3,16r61b5,16r61bc,16r61e7,
+16r61e0,16r61e5,16r61e4,16r61e8,16r61de,16r64ef,16r64e9,16r64e3,
+16r64eb,16r64e4,16r64e8,16r6581,16r6580,16r65b6,16r65da,16r66d2,
+16r6a8d,16r6a96,16r6a81,16r6aa5,16r6a89,16r6a9f,16r6a9b,16r6aa1,
+16r6a9e,16r6a87,16r6a93,16r6a8e,16r6a95,16r6a83,16r6aa8,16r6aa4,
+16r6a91,16r6a7f,16r6aa6,16r6a9a,16r6a85,16r6a8c,16r6a92,16r6b5b,
+16r6bad,16r6c09,16r6fcc,16r6fa9,16r6ff4,16r6fd4,16r6fe3,16r6fdc,
+16r6fed,16r6fe7,16r6fe6,16r6fde,16r6ff2,16r6fdd,16r6fe2,16r6fe8,
+16r71e1,16r71f1,16r71e8,16r71f2,16r71e4,16r71f0,16r71e2,16r7373,
+16r736e,16r736f,16r7497,16r74b2,16r74ab,16r7490,16r74aa,16r74ad,
+16r74b1,16r74a5,16r74af,16r7510,16r7511,16r7512,16r750f,16r7584,
+16r7643,16r7648,16r7649,16r7647,16r76a4,16r76e9,16r77b5,16r77ab,
+16r77b2,16r77b7,16r77b6,16r77b4,16r77b1,16r77a8,16r77f0,16r78f3,
+16r78fd,16r7902,16r78fb,16r78fc,16r78f2,16r7905,16r78f9,16r78fe,
+16r7904,16r79ab,16r79a8,16r7a5c,16r7a5b,16r7a56,16r7a58,16r7a54,
+16r7a5a,16r7abe,16r7ac0,16r7ac1,16r7c05,16r7c0f,16r7bf2,16r7c00,
+16r7bff,16r7bfb,16r7c0e,16r7bf4,16r7c0b,16r7bf3,16r7c02,16r7c09,
+16r7c03,16r7c01,16r7bf8,16r7bfd,16r7c06,16r7bf0,16r7bf1,16r7c10,
+16r7c0a,16r7ce8,16r7e2d,16r7e3c,16r7e42,16r7e33,16r9848,16r7e38,
+16r7e2a,16r7e49,16r7e40,16r7e47,16r7e29,16r7e4c,16r7e30,16r7e3b,
+16r7e36,16r7e44,16r7e3a,16r7f45,16r7f7f,16r7f7e,16r7f7d,16r7ff4,
+16r7ff2,16r802c,16r81bb,16r81c4,16r81cc,16r81ca,16r81c5,16r81c7,
+16r81bc,16r81e9,16r825b,16r825a,16r825c,16r8583,16r8580,16r858f,
+16r85a7,16r8595,16r85a0,16r858b,16r85a3,16r857b,16r85a4,16r859a,
+16r859e,16r8577,16r857c,16r8589,16r85a1,16r857a,16r8578,16r8557,
+16r858e,16r8596,16r8586,16r858d,16r8599,16r859d,16r8581,16r85a2,
+16r8582,16r8588,16r8585,16r8579,16r8576,16r8598,16r8590,16r859f,
+16r8668,16r87be,16r87aa,16r87ad,16r87c5,16r87b0,16r87ac,16r87b9,
+16r87b5,16r87bc,16r87ae,16r87c9,16r87c3,16r87c2,16r87cc,16r87b7,
+16r87af,16r87c4,16r87ca,16r87b4,16r87b6,16r87bf,16r87b8,16r87bd,
+16r87de,16r87b2,16r8935,16r8933,16r893c,16r893e,16r8941,16r8952,
+16r8937,16r8942,16r89ad,16r89af,16r89ae,16r89f2,16r89f3,16r8b1e,
+16r8b18,16r8b16,16r8b11,16r8b05,16r8b0b,16r8b22,16r8b0f,16r8b12,
+16r8b15,16r8b07,16r8b0d,16r8b08,16r8b06,16r8b1c,16r8b13,16r8b1a,
+16r8c4f,16r8c70,16r8c72,16r8c71,16r8c6f,16r8c95,16r8c94,16r8cf9,
+16r8d6f,16r8e4e,16r8e4d,16r8e53,16r8e50,16r8e4c,16r8e47,16r8f43,
+16r8f40,16r9085,16r907e,16r9138,16r919a,16r91a2,16r919b,16r9199,
+16r919f,16r91a1,16r919d,16r91a0,16r93a1,16r9383,16r93af,16r9364,
+16r9356,16r9347,16r937c,16r9358,16r935c,16r9376,16r9349,16r9350,
+16r9351,16r9360,16r936d,16r938f,16r934c,16r936a,16r9379,16r9357,
+16r9355,16r9352,16r934f,16r9371,16r9377,16r937b,16r9361,16r935e,
+16r9363,16r9367,16r9380,16r934e,16r9359,16r95c7,16r95c0,16r95c9,
+16r95c3,16r95c5,16r95b7,16r96ae,16r96b0,16r96ac,16r9720,16r971f,
+16r9718,16r971d,16r9719,16r979a,16r97a1,16r979c,16r979e,16r979d,
+16r97d5,16r97d4,16r97f1,16r9841,16r9844,16r984a,16r9849,16r9845,
+16r9843,16r9925,16r992b,16r992c,16r992a,16r9933,16r9932,16r992f,
+16r992d,16r9931,16r9930,16r9998,16r99a3,16r99a1,16r9a02,16r99fa,
+16r99f4,16r99f7,16r99f9,16r99f8,16r99f6,16r99fb,16r99fd,16r99fe,
+16r99fc,16r9a03,16r9abe,16r9afe,16r9afd,16r9b01,16r9afc,16r9b48,
+16r9b9a,16r9ba8,16r9b9e,16r9b9b,16r9ba6,16r9ba1,16r9ba5,16r9ba4,
+16r9b86,16r9ba2,16r9ba0,16r9baf,16r9d33,16r9d41,16r9d67,16r9d36,
+16r9d2e,16r9d2f,16r9d31,16r9d38,16r9d30,16r9d45,16r9d42,16r9d43,
+16r9d3e,16r9d37,16r9d40,16r9d3d,16r7ff5,16r9d2d,16r9e8a,16r9e89,
+16r9e8d,16r9eb0,16r9ec8,16r9eda,16r9efb,16r9eff,16r9f24,16r9f23,
+16r9f22,16r9f54,16r9fa0,16r5131,16r512d,16r512e,16r5698,16r569c,
+16r5697,16r569a,16r569d,16r5699,16r5970,16r5b3c,16r5c69,16r5c6a,
+16r5dc0,16r5e6d,16r5e6e,16r61d8,16r61df,16r61ed,16r61ee,16r61f1,
+16r61ea,16r61f0,16r61eb,16r61d6,16r61e9,16r64ff,16r6504,16r64fd,
+16r64f8,16r6501,16r6503,16r64fc,16r6594,16r65db,16r66da,16r66db,
+16r66d8,16r6ac5,16r6ab9,16r6abd,16r6ae1,16r6ac6,16r6aba,16r6ab6,
+16r6ab7,16r6ac7,16r6ab4,16r6aad,16r6b5e,16r6bc9,16r6c0b,16r7007,
+16r700c,16r700d,16r7001,16r7005,16r7014,16r700e,16r6fff,16r7000,
+16r6ffb,16r7026,16r6ffc,16r6ff7,16r700a,16r7201,16r71ff,16r71f9,
+16r7203,16r71fd,16r7376,16r74b8,16r74c0,16r74b5,16r74c1,16r74be,
+16r74b6,16r74bb,16r74c2,16r7514,16r7513,16r765c,16r7664,16r7659,
+16r7650,16r7653,16r7657,16r765a,16r76a6,16r76bd,16r76ec,16r77c2,
+16r77ba,16r78ff,16r790c,16r7913,16r7914,16r7909,16r7910,16r7912,
+16r7911,16r79ad,16r79ac,16r7a5f,16r7c1c,16r7c29,16r7c19,16r7c20,
+16r7c1f,16r7c2d,16r7c1d,16r7c26,16r7c28,16r7c22,16r7c25,16r7c30,
+16r7e5c,16r7e50,16r7e56,16r7e63,16r7e58,16r7e62,16r7e5f,16r7e51,
+16r7e60,16r7e57,16r7e53,16r7fb5,16r7fb3,16r7ff7,16r7ff8,16r8075,
+16r81d1,16r81d2,16r81d0,16r825f,16r825e,16r85b4,16r85c6,16r85c0,
+16r85c3,16r85c2,16r85b3,16r85b5,16r85bd,16r85c7,16r85c4,16r85bf,
+16r85cb,16r85ce,16r85c8,16r85c5,16r85b1,16r85b6,16r85d2,16r8624,
+16r85b8,16r85b7,16r85be,16r8669,16r87e7,16r87e6,16r87e2,16r87db,
+16r87eb,16r87ea,16r87e5,16r87df,16r87f3,16r87e4,16r87d4,16r87dc,
+16r87d3,16r87ed,16r87d8,16r87e3,16r87a4,16r87d7,16r87d9,16r8801,
+16r87f4,16r87e8,16r87dd,16r8953,16r894b,16r894f,16r894c,16r8946,
+16r8950,16r8951,16r8949,16r8b2a,16r8b27,16r8b23,16r8b33,16r8b30,
+16r8b35,16r8b47,16r8b2f,16r8b3c,16r8b3e,16r8b31,16r8b25,16r8b37,
+16r8b26,16r8b36,16r8b2e,16r8b24,16r8b3b,16r8b3d,16r8b3a,16r8c42,
+16r8c75,16r8c99,16r8c98,16r8c97,16r8cfe,16r8d04,16r8d02,16r8d00,
+16r8e5c,16r8e62,16r8e60,16r8e57,16r8e56,16r8e5e,16r8e65,16r8e67,
+16r8e5b,16r8e5a,16r8e61,16r8e5d,16r8e69,16r8e54,16r8f46,16r8f47,
+16r8f48,16r8f4b,16r9128,16r913a,16r913b,16r913e,16r91a8,16r91a5,
+16r91a7,16r91af,16r91aa,16r93b5,16r938c,16r9392,16r93b7,16r939b,
+16r939d,16r9389,16r93a7,16r938e,16r93aa,16r939e,16r93a6,16r9395,
+16r9388,16r9399,16r939f,16r938d,16r93b1,16r9391,16r93b2,16r93a4,
+16r93a8,16r93b4,16r93a3,16r93a5,16r95d2,16r95d3,16r95d1,16r96b3,
+16r96d7,16r96da,16r5dc2,16r96df,16r96d8,16r96dd,16r9723,16r9722,
+16r9725,16r97ac,16r97ae,16r97a8,16r97ab,16r97a4,16r97aa,16r97a2,
+16r97a5,16r97d7,16r97d9,16r97d6,16r97d8,16r97fa,16r9850,16r9851,
+16r9852,16r98b8,16r9941,16r993c,16r993a,16r9a0f,16r9a0b,16r9a09,
+16r9a0d,16r9a04,16r9a11,16r9a0a,16r9a05,16r9a07,16r9a06,16r9ac0,
+16r9adc,16r9b08,16r9b04,16r9b05,16r9b29,16r9b35,16r9b4a,16r9b4c,
+16r9b4b,16r9bc7,16r9bc6,16r9bc3,16r9bbf,16r9bc1,16r9bb5,16r9bb8,
+16r9bd3,16r9bb6,16r9bc4,16r9bb9,16r9bbd,16r9d5c,16r9d53,16r9d4f,
+16r9d4a,16r9d5b,16r9d4b,16r9d59,16r9d56,16r9d4c,16r9d57,16r9d52,
+16r9d54,16r9d5f,16r9d58,16r9d5a,16r9e8e,16r9e8c,16r9edf,16r9f01,
+16r9f00,16r9f16,16r9f25,16r9f2b,16r9f2a,16r9f29,16r9f28,16r9f4c,
+16r9f55,16r5134,16r5135,16r5296,16r52f7,16r53b4,16r56ab,16r56ad,
+16r56a6,16r56a7,16r56aa,16r56ac,16r58da,16r58dd,16r58db,16r5912,
+16r5b3d,16r5b3e,16r5b3f,16r5dc3,16r5e70,16r5fbf,16r61fb,16r6507,
+16r6510,16r650d,16r6509,16r650c,16r650e,16r6584,16r65de,16r65dd,
+16r66de,16r6ae7,16r6ae0,16r6acc,16r6ad1,16r6ad9,16r6acb,16r6adf,
+16r6adc,16r6ad0,16r6aeb,16r6acf,16r6acd,16r6ade,16r6b60,16r6bb0,
+16r6c0c,16r7019,16r7027,16r7020,16r7016,16r702b,16r7021,16r7022,
+16r7023,16r7029,16r7017,16r7024,16r701c,16r702a,16r720c,16r720a,
+16r7207,16r7202,16r7205,16r72a5,16r72a6,16r72a4,16r72a3,16r72a1,
+16r74cb,16r74c5,16r74b7,16r74c3,16r7516,16r7660,16r77c9,16r77ca,
+16r77c4,16r77f1,16r791d,16r791b,16r7921,16r791c,16r7917,16r791e,
+16r79b0,16r7a67,16r7a68,16r7c33,16r7c3c,16r7c39,16r7c2c,16r7c3b,
+16r7cec,16r7cea,16r7e76,16r7e75,16r7e78,16r7e70,16r7e77,16r7e6f,
+16r7e7a,16r7e72,16r7e74,16r7e68,16r7f4b,16r7f4a,16r7f83,16r7f86,
+16r7fb7,16r7ffd,16r7ffe,16r8078,16r81d7,16r81d5,16r8264,16r8261,
+16r8263,16r85eb,16r85f1,16r85ed,16r85d9,16r85e1,16r85e8,16r85da,
+16r85d7,16r85ec,16r85f2,16r85f8,16r85d8,16r85df,16r85e3,16r85dc,
+16r85d1,16r85f0,16r85e6,16r85ef,16r85de,16r85e2,16r8800,16r87fa,
+16r8803,16r87f6,16r87f7,16r8809,16r880c,16r880b,16r8806,16r87fc,
+16r8808,16r87ff,16r880a,16r8802,16r8962,16r895a,16r895b,16r8957,
+16r8961,16r895c,16r8958,16r895d,16r8959,16r8988,16r89b7,16r89b6,
+16r89f6,16r8b50,16r8b48,16r8b4a,16r8b40,16r8b53,16r8b56,16r8b54,
+16r8b4b,16r8b55,16r8b51,16r8b42,16r8b52,16r8b57,16r8c43,16r8c77,
+16r8c76,16r8c9a,16r8d06,16r8d07,16r8d09,16r8dac,16r8daa,16r8dad,
+16r8dab,16r8e6d,16r8e78,16r8e73,16r8e6a,16r8e6f,16r8e7b,16r8ec2,
+16r8f52,16r8f51,16r8f4f,16r8f50,16r8f53,16r8fb4,16r9140,16r913f,
+16r91b0,16r91ad,16r93de,16r93c7,16r93cf,16r93c2,16r93da,16r93d0,
+16r93f9,16r93ec,16r93cc,16r93d9,16r93a9,16r93e6,16r93ca,16r93d4,
+16r93ee,16r93e3,16r93d5,16r93c4,16r93ce,16r93c0,16r93d2,16r93e7,
+16r957d,16r95da,16r95db,16r96e1,16r9729,16r972b,16r972c,16r9728,
+16r9726,16r97b3,16r97b7,16r97b6,16r97dd,16r97de,16r97df,16r985c,
+16r9859,16r985d,16r9857,16r98bf,16r98bd,16r98bb,16r98be,16r9948,
+16r9947,16r9943,16r99a6,16r99a7,16r9a1a,16r9a15,16r9a25,16r9a1d,
+16r9a24,16r9a1b,16r9a22,16r9a20,16r9a27,16r9a23,16r9a1e,16r9a1c,
+16r9a14,16r9ac2,16r9b0b,16r9b0a,16r9b0e,16r9b0c,16r9b37,16r9bea,
+16r9beb,16r9be0,16r9bde,16r9be4,16r9be6,16r9be2,16r9bf0,16r9bd4,
+16r9bd7,16r9bec,16r9bdc,16r9bd9,16r9be5,16r9bd5,16r9be1,16r9bda,
+16r9d77,16r9d81,16r9d8a,16r9d84,16r9d88,16r9d71,16r9d80,16r9d78,
+16r9d86,16r9d8b,16r9d8c,16r9d7d,16r9d6b,16r9d74,16r9d75,16r9d70,
+16r9d69,16r9d85,16r9d73,16r9d7b,16r9d82,16r9d6f,16r9d79,16r9d7f,
+16r9d87,16r9d68,16r9e94,16r9e91,16r9ec0,16r9efc,16r9f2d,16r9f40,
+16r9f41,16r9f4d,16r9f56,16r9f57,16r9f58,16r5337,16r56b2,16r56b5,
+16r56b3,16r58e3,16r5b45,16r5dc6,16r5dc7,16r5eee,16r5eef,16r5fc0,
+16r5fc1,16r61f9,16r6517,16r6516,16r6515,16r6513,16r65df,16r66e8,
+16r66e3,16r66e4,16r6af3,16r6af0,16r6aea,16r6ae8,16r6af9,16r6af1,
+16r6aee,16r6aef,16r703c,16r7035,16r702f,16r7037,16r7034,16r7031,
+16r7042,16r7038,16r703f,16r703a,16r7039,16r7040,16r703b,16r7033,
+16r7041,16r7213,16r7214,16r72a8,16r737d,16r737c,16r74ba,16r76ab,
+16r76aa,16r76be,16r76ed,16r77cc,16r77ce,16r77cf,16r77cd,16r77f2,
+16r7925,16r7923,16r7927,16r7928,16r7924,16r7929,16r79b2,16r7a6e,
+16r7a6c,16r7a6d,16r7af7,16r7c49,16r7c48,16r7c4a,16r7c47,16r7c45,
+16r7cee,16r7e7b,16r7e7e,16r7e81,16r7e80,16r7fba,16r7fff,16r8079,
+16r81db,16r81d9,16r820b,16r8268,16r8269,16r8622,16r85ff,16r8601,
+16r85fe,16r861b,16r8600,16r85f6,16r8604,16r8609,16r8605,16r860c,
+16r85fd,16r8819,16r8810,16r8811,16r8817,16r8813,16r8816,16r8963,
+16r8966,16r89b9,16r89f7,16r8b60,16r8b6a,16r8b5d,16r8b68,16r8b63,
+16r8b65,16r8b67,16r8b6d,16r8dae,16r8e86,16r8e88,16r8e84,16r8f59,
+16r8f56,16r8f57,16r8f55,16r8f58,16r8f5a,16r908d,16r9143,16r9141,
+16r91b7,16r91b5,16r91b2,16r91b3,16r940b,16r9413,16r93fb,16r9420,
+16r940f,16r9414,16r93fe,16r9415,16r9410,16r9428,16r9419,16r940d,
+16r93f5,16r9400,16r93f7,16r9407,16r940e,16r9416,16r9412,16r93fa,
+16r9409,16r93f8,16r940a,16r93ff,16r93fc,16r940c,16r93f6,16r9411,
+16r9406,16r95de,16r95e0,16r95df,16r972e,16r972f,16r97b9,16r97bb,
+16r97fd,16r97fe,16r9860,16r9862,16r9863,16r985f,16r98c1,16r98c2,
+16r9950,16r994e,16r9959,16r994c,16r994b,16r9953,16r9a32,16r9a34,
+16r9a31,16r9a2c,16r9a2a,16r9a36,16r9a29,16r9a2e,16r9a38,16r9a2d,
+16r9ac7,16r9aca,16r9ac6,16r9b10,16r9b12,16r9b11,16r9c0b,16r9c08,
+16r9bf7,16r9c05,16r9c12,16r9bf8,16r9c40,16r9c07,16r9c0e,16r9c06,
+16r9c17,16r9c14,16r9c09,16r9d9f,16r9d99,16r9da4,16r9d9d,16r9d92,
+16r9d98,16r9d90,16r9d9b,16r9da0,16r9d94,16r9d9c,16r9daa,16r9d97,
+16r9da1,16r9d9a,16r9da2,16r9da8,16r9d9e,16r9da3,16r9dbf,16r9da9,
+16r9d96,16r9da6,16r9da7,16r9e99,16r9e9b,16r9e9a,16r9ee5,16r9ee4,
+16r9ee7,16r9ee6,16r9f30,16r9f2e,16r9f5b,16r9f60,16r9f5e,16r9f5d,
+16r9f59,16r9f91,16r513a,16r5139,16r5298,16r5297,16r56c3,16r56bd,
+16r56be,16r5b48,16r5b47,16r5dcb,16r5dcf,16r5ef1,16r61fd,16r651b,
+16r6b02,16r6afc,16r6b03,16r6af8,16r6b00,16r7043,16r7044,16r704a,
+16r7048,16r7049,16r7045,16r7046,16r721d,16r721a,16r7219,16r737e,
+16r7517,16r766a,16r77d0,16r792d,16r7931,16r792f,16r7c54,16r7c53,
+16r7cf2,16r7e8a,16r7e87,16r7e88,16r7e8b,16r7e86,16r7e8d,16r7f4d,
+16r7fbb,16r8030,16r81dd,16r8618,16r862a,16r8626,16r861f,16r8623,
+16r861c,16r8619,16r8627,16r862e,16r8621,16r8620,16r8629,16r861e,
+16r8625,16r8829,16r881d,16r881b,16r8820,16r8824,16r881c,16r882b,
+16r884a,16r896d,16r8969,16r896e,16r896b,16r89fa,16r8b79,16r8b78,
+16r8b45,16r8b7a,16r8b7b,16r8d10,16r8d14,16r8daf,16r8e8e,16r8e8c,
+16r8f5e,16r8f5b,16r8f5d,16r9146,16r9144,16r9145,16r91b9,16r943f,
+16r943b,16r9436,16r9429,16r943d,16r943c,16r9430,16r9439,16r942a,
+16r9437,16r942c,16r9440,16r9431,16r95e5,16r95e4,16r95e3,16r9735,
+16r973a,16r97bf,16r97e1,16r9864,16r98c9,16r98c6,16r98c0,16r9958,
+16r9956,16r9a39,16r9a3d,16r9a46,16r9a44,16r9a42,16r9a41,16r9a3a,
+16r9a3f,16r9acd,16r9b15,16r9b17,16r9b18,16r9b16,16r9b3a,16r9b52,
+16r9c2b,16r9c1d,16r9c1c,16r9c2c,16r9c23,16r9c28,16r9c29,16r9c24,
+16r9c21,16r9db7,16r9db6,16r9dbc,16r9dc1,16r9dc7,16r9dca,16r9dcf,
+16r9dbe,16r9dc5,16r9dc3,16r9dbb,16r9db5,16r9dce,16r9db9,16r9dba,
+16r9dac,16r9dc8,16r9db1,16r9dad,16r9dcc,16r9db3,16r9dcd,16r9db2,
+16r9e7a,16r9e9c,16r9eeb,16r9eee,16r9eed,16r9f1b,16r9f18,16r9f1a,
+16r9f31,16r9f4e,16r9f65,16r9f64,16r9f92,16r4eb9,16r56c6,16r56c5,
+16r56cb,16r5971,16r5b4b,16r5b4c,16r5dd5,16r5dd1,16r5ef2,16r6521,
+16r6520,16r6526,16r6522,16r6b0b,16r6b08,16r6b09,16r6c0d,16r7055,
+16r7056,16r7057,16r7052,16r721e,16r721f,16r72a9,16r737f,16r74d8,
+16r74d5,16r74d9,16r74d7,16r766d,16r76ad,16r7935,16r79b4,16r7a70,
+16r7a71,16r7c57,16r7c5c,16r7c59,16r7c5b,16r7c5a,16r7cf4,16r7cf1,
+16r7e91,16r7f4f,16r7f87,16r81de,16r826b,16r8634,16r8635,16r8633,
+16r862c,16r8632,16r8636,16r882c,16r8828,16r8826,16r882a,16r8825,
+16r8971,16r89bf,16r89be,16r89fb,16r8b7e,16r8b84,16r8b82,16r8b86,
+16r8b85,16r8b7f,16r8d15,16r8e95,16r8e94,16r8e9a,16r8e92,16r8e90,
+16r8e96,16r8e97,16r8f60,16r8f62,16r9147,16r944c,16r9450,16r944a,
+16r944b,16r944f,16r9447,16r9445,16r9448,16r9449,16r9446,16r973f,
+16r97e3,16r986a,16r9869,16r98cb,16r9954,16r995b,16r9a4e,16r9a53,
+16r9a54,16r9a4c,16r9a4f,16r9a48,16r9a4a,16r9a49,16r9a52,16r9a50,
+16r9ad0,16r9b19,16r9b2b,16r9b3b,16r9b56,16r9b55,16r9c46,16r9c48,
+16r9c3f,16r9c44,16r9c39,16r9c33,16r9c41,16r9c3c,16r9c37,16r9c34,
+16r9c32,16r9c3d,16r9c36,16r9ddb,16r9dd2,16r9dde,16r9dda,16r9dcb,
+16r9dd0,16r9ddc,16r9dd1,16r9ddf,16r9de9,16r9dd9,16r9dd8,16r9dd6,
+16r9df5,16r9dd5,16r9ddd,16r9eb6,16r9ef0,16r9f35,16r9f33,16r9f32,
+16r9f42,16r9f6b,16r9f95,16r9fa2,16r513d,16r5299,16r58e8,16r58e7,
+16r5972,16r5b4d,16r5dd8,16r882f,16r5f4f,16r6201,16r6203,16r6204,
+16r6529,16r6525,16r6596,16r66eb,16r6b11,16r6b12,16r6b0f,16r6bca,
+16r705b,16r705a,16r7222,16r7382,16r7381,16r7383,16r7670,16r77d4,
+16r7c67,16r7c66,16r7e95,16r826c,16r863a,16r8640,16r8639,16r863c,
+16r8631,16r863b,16r863e,16r8830,16r8832,16r882e,16r8833,16r8976,
+16r8974,16r8973,16r89fe,16r8b8c,16r8b8e,16r8b8b,16r8b88,16r8c45,
+16r8d19,16r8e98,16r8f64,16r8f63,16r91bc,16r9462,16r9455,16r945d,
+16r9457,16r945e,16r97c4,16r97c5,16r9800,16r9a56,16r9a59,16r9b1e,
+16r9b1f,16r9b20,16r9c52,16r9c58,16r9c50,16r9c4a,16r9c4d,16r9c4b,
+16r9c55,16r9c59,16r9c4c,16r9c4e,16r9dfb,16r9df7,16r9def,16r9de3,
+16r9deb,16r9df8,16r9de4,16r9df6,16r9de1,16r9dee,16r9de6,16r9df2,
+16r9df0,16r9de2,16r9dec,16r9df4,16r9df3,16r9de8,16r9ded,16r9ec2,
+16r9ed0,16r9ef2,16r9ef3,16r9f06,16r9f1c,16r9f38,16r9f37,16r9f36,
+16r9f43,16r9f4f,16r9f71,16r9f70,16r9f6e,16r9f6f,16r56d3,16r56cd,
+16r5b4e,16r5c6d,16r652d,16r66ed,16r66ee,16r6b13,16r705f,16r7061,
+16r705d,16r7060,16r7223,16r74db,16r74e5,16r77d5,16r7938,16r79b7,
+16r79b6,16r7c6a,16r7e97,16r7f89,16r826d,16r8643,16r8838,16r8837,
+16r8835,16r884b,16r8b94,16r8b95,16r8e9e,16r8e9f,16r8ea0,16r8e9d,
+16r91be,16r91bd,16r91c2,16r946b,16r9468,16r9469,16r96e5,16r9746,
+16r9743,16r9747,16r97c7,16r97e5,16r9a5e,16r9ad5,16r9b59,16r9c63,
+16r9c67,16r9c66,16r9c62,16r9c5e,16r9c60,16r9e02,16r9dfe,16r9e07,
+16r9e03,16r9e06,16r9e05,16r9e00,16r9e01,16r9e09,16r9dff,16r9dfd,
+16r9e04,16r9ea0,16r9f1e,16r9f46,16r9f74,16r9f75,16r9f76,16r56d4,
+16r652e,16r65b8,16r6b18,16r6b19,16r6b17,16r6b1a,16r7062,16r7226,
+16r72aa,16r77d8,16r77d9,16r7939,16r7c69,16r7c6b,16r7cf6,16r7e9a,
+16r7e98,16r7e9b,16r7e99,16r81e0,16r81e1,16r8646,16r8647,16r8648,
+16r8979,16r897a,16r897c,16r897b,16r89ff,16r8b98,16r8b99,16r8ea5,
+16r8ea4,16r8ea3,16r946e,16r946d,16r946f,16r9471,16r9473,16r9749,
+16r9872,16r995f,16r9c68,16r9c6e,16r9c6d,16r9e0b,16r9e0d,16r9e10,
+16r9e0f,16r9e12,16r9e11,16r9ea1,16r9ef5,16r9f09,16r9f47,16r9f78,
+16r9f7b,16r9f7a,16r9f79,16r571e,16r7066,16r7c6f,16r883c,16r8db2,
+16r8ea6,16r91c3,16r9474,16r9478,16r9476,16r9475,16r9a60,16r9c74,
+16r9c73,16r9c71,16r9c75,16r9e14,16r9e13,16r9ef6,16r9f0a,16r9fa4,
+16r7068,16r7065,16r7cf7,16r866a,16r883e,16r883d,16r883f,16r8b9e,
+16r8c9c,16r8ea9,16r8ec9,16r974b,16r9873,16r9874,16r98cc,16r9961,
+16r99ab,16r9a64,16r9a66,16r9a67,16r9b24,16r9e15,16r9e17,16r9f48,
+16r6207,16r6b1e,16r7227,16r864c,16r8ea8,16r9482,16r9480,16r9481,
+16r9a69,16r9a68,16r9b2e,16r9e19,16r7229,16r864b,16r8b9f,16r9483,
+16r9c79,16r9eb7,16r7675,16r9a6b,16r9c7a,16r9e1d,16r7069,16r706a,
+16r9ea4,16r9f7e,16r9f49,16r9f98,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+};
--- /dev/null
+++ b/appl/lib/convcs/gencp.b
@@ -1,0 +1,28 @@
+NCHARS : con 256;
+ERRCHAR : con 16rFFFD;
+
+sys : Sys;
+
+GenCP : module {
+	init : fn (ctxt : ref Draw->Context, args : list of string);
+};
+
+init(nil : ref Draw->Context, nil : list of string)
+{
+	sys = load Sys Sys->PATH;
+	path := sys->sprint("/lib/convcs/%s.cp", CHARSET);
+	fd := sys->create(path, Sys->OWRITE, 8r644);
+	if (fd == nil) {
+		sys->print("cannot create %s: %r\n", path);
+		return;
+	}
+	s := "";
+	for (i := 0; i < NCHARS; i++) {
+		if (cstab[i] == -1)
+			cstab[i] = ERRCHAR;
+		s[i] = cstab[i];
+	}
+	buf := array of byte s;
+	sys->write(fd, buf, len buf);
+}
+
--- /dev/null
+++ b/appl/lib/convcs/gencp932.b
@@ -1,0 +1,7814 @@
+implement gencp932;
+
+include "sys.m";
+include "draw.m";
+
+gencp932 : module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+DATAFILE : con "/lib/convcs/cp932";
+
+CP932PAGES : con 45;		# 81..84, 87..9f, e0..ea, ed..ee, fa..fc
+CP932PAGESZ : con 189;		# 40..fc (including 7f)
+CP932CHAR0 : con 16r40;
+
+BADCHAR : con 16rFFFD;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->create(DATAFILE, Sys->OWRITE, 8r644);
+	if (fd == nil) {
+		sys->print("cannot create %s: %r\n", DATAFILE);
+		return;
+	}
+
+	# pre-calculate DBCS page numbers to mapping file page numbers
+	dbcsoff := array [256] of { * => -1 };
+	p := 0;
+	for (i := 16r81; i <= 16r84; i++)
+		dbcsoff[i] = p++;
+	for (i = 16r87; i <= 16r9f; i++)
+		dbcsoff[i] = p++;
+	for (i = 16re0; i <= 16rea; i++)
+		dbcsoff[i] = p++;
+	for (i = 16red; i <= 16ree; i++)
+		dbcsoff[i] = p++;
+	for (i = 16rfa; i <= 16rfc; i++)
+		dbcsoff[i] = p++;
+
+	pages := array [CP932PAGES * CP932PAGESZ] of { * => BADCHAR };
+
+	for (i = 0; i < len cp932; i++) {
+		(nil, toks) := sys->tokenize(cp932[i], "\t");
+		(bytes, ucode) := (hd toks, hd tl toks);
+		u := hex2int(ucode);
+		(nil, blist) := sys->tokenize(bytes, " ");
+		b1 := hex2int(hd blist);
+		b2 := hex2int(hd tl blist);
+
+		page := dbcsoff[b1];
+		if (page == -1) {
+			sys->print("conversion error\n");
+			raise "fail:bad data";
+		}
+		char := b2 - CP932CHAR0;
+		pages[(page * CP932PAGESZ) + char] = u;
+	}
+
+	s := "";
+	for (i = 0; i < len pages; i++)
+		s[i] = pages[i];
+	pages = nil;
+	buf := array of byte s;
+	sys->write(fd, buf, len buf);
+}
+
+hex2int(s: string): int
+{
+	n := 0;
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'0' to '9' =>
+			n = 16*n + s[i] - '0';
+		'A' to 'F' =>
+			n = 16*n + s[i] + 10 - 'A';
+		'a' to 'f' =>
+			n = 16*n + s[i] + 10 - 'a';
+		* =>
+			return n;
+		}
+	}
+	return n;
+}
+
+# This data derived directly from the unicode mapping file shiftjis.txt
+# available from http://www.unicode.org/Public//MAPPINGS
+
+cp932 := array [] of {
+	"81 40	3000",	#IDEOGRAPHIC SPACE
+	"81 41	3001",	#IDEOGRAPHIC COMMA
+	"81 42	3002",	#IDEOGRAPHIC FULL STOP
+	"81 43	FF0C",	#FULLWIDTH COMMA
+	"81 44	FF0E",	#FULLWIDTH FULL STOP
+	"81 45	30FB",	#KATAKANA MIDDLE DOT
+	"81 46	FF1A",	#FULLWIDTH COLON
+	"81 47	FF1B",	#FULLWIDTH SEMICOLON
+	"81 48	FF1F",	#FULLWIDTH QUESTION MARK
+	"81 49	FF01",	#FULLWIDTH EXCLAMATION MARK
+	"81 4A	309B",	#KATAKANA-HIRAGANA VOICED SOUND MARK
+	"81 4B	309C",	#KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
+	"81 4C	00B4",	#ACUTE ACCENT
+	"81 4D	FF40",	#FULLWIDTH GRAVE ACCENT
+	"81 4E	00A8",	#DIAERESIS
+	"81 4F	FF3E",	#FULLWIDTH CIRCUMFLEX ACCENT
+	"81 50	FFE3",	#FULLWIDTH MACRON
+	"81 51	FF3F",	#FULLWIDTH LOW LINE
+	"81 52	30FD",	#KATAKANA ITERATION MARK
+	"81 53	30FE",	#KATAKANA VOICED ITERATION MARK
+	"81 54	309D",	#HIRAGANA ITERATION MARK
+	"81 55	309E",	#HIRAGANA VOICED ITERATION MARK
+	"81 56	3003",	#DITTO MARK
+	"81 57	4EDD",	#CJK UNIFIED IDEOGRAPH
+	"81 58	3005",	#IDEOGRAPHIC ITERATION MARK
+	"81 59	3006",	#IDEOGRAPHIC CLOSING MARK
+	"81 5A	3007",	#IDEOGRAPHIC NUMBER ZERO
+	"81 5B	30FC",	#KATAKANA-HIRAGANA PROLONGED SOUND MARK
+	"81 5C	2015",	#HORIZONTAL BAR
+	"81 5D	2010",	#HYPHEN
+	"81 5E	FF0F",	#FULLWIDTH SOLIDUS
+	"81 5F	FF3C",	#FULLWIDTH REVERSE SOLIDUS
+	"81 60	FF5E",	#FULLWIDTH TILDE
+	"81 61	2225",	#PARALLEL TO
+	"81 62	FF5C",	#FULLWIDTH VERTICAL LINE
+	"81 63	2026",	#HORIZONTAL ELLIPSIS
+	"81 64	2025",	#TWO DOT LEADER
+	"81 65	2018",	#LEFT SINGLE QUOTATION MARK
+	"81 66	2019",	#RIGHT SINGLE QUOTATION MARK
+	"81 67	201C",	#LEFT DOUBLE QUOTATION MARK
+	"81 68	201D",	#RIGHT DOUBLE QUOTATION MARK
+	"81 69	FF08",	#FULLWIDTH LEFT PARENTHESIS
+	"81 6A	FF09",	#FULLWIDTH RIGHT PARENTHESIS
+	"81 6B	3014",	#LEFT TORTOISE SHELL BRACKET
+	"81 6C	3015",	#RIGHT TORTOISE SHELL BRACKET
+	"81 6D	FF3B",	#FULLWIDTH LEFT SQUARE BRACKET
+	"81 6E	FF3D",	#FULLWIDTH RIGHT SQUARE BRACKET
+	"81 6F	FF5B",	#FULLWIDTH LEFT CURLY BRACKET
+	"81 70	FF5D",	#FULLWIDTH RIGHT CURLY BRACKET
+	"81 71	3008",	#LEFT ANGLE BRACKET
+	"81 72	3009",	#RIGHT ANGLE BRACKET
+	"81 73	300A",	#LEFT DOUBLE ANGLE BRACKET
+	"81 74	300B",	#RIGHT DOUBLE ANGLE BRACKET
+	"81 75	300C",	#LEFT CORNER BRACKET
+	"81 76	300D",	#RIGHT CORNER BRACKET
+	"81 77	300E",	#LEFT WHITE CORNER BRACKET
+	"81 78	300F",	#RIGHT WHITE CORNER BRACKET
+	"81 79	3010",	#LEFT BLACK LENTICULAR BRACKET
+	"81 7A	3011",	#RIGHT BLACK LENTICULAR BRACKET
+	"81 7B	FF0B",	#FULLWIDTH PLUS SIGN
+	"81 7C	FF0D",	#FULLWIDTH HYPHEN-MINUS
+	"81 7D	00B1",	#PLUS-MINUS SIGN
+	"81 7E	00D7",	#MULTIPLICATION SIGN
+	"81 80	00F7",	#DIVISION SIGN
+	"81 81	FF1D",	#FULLWIDTH EQUALS SIGN
+	"81 82	2260",	#NOT EQUAL TO
+	"81 83	FF1C",	#FULLWIDTH LESS-THAN SIGN
+	"81 84	FF1E",	#FULLWIDTH GREATER-THAN SIGN
+	"81 85	2266",	#LESS-THAN OVER EQUAL TO
+	"81 86	2267",	#GREATER-THAN OVER EQUAL TO
+	"81 87	221E",	#INFINITY
+	"81 88	2234",	#THEREFORE
+	"81 89	2642",	#MALE SIGN
+	"81 8A	2640",	#FEMALE SIGN
+	"81 8B	00B0",	#DEGREE SIGN
+	"81 8C	2032",	#PRIME
+	"81 8D	2033",	#DOUBLE PRIME
+	"81 8E	2103",	#DEGREE CELSIUS
+	"81 8F	FFE5",	#FULLWIDTH YEN SIGN
+	"81 90	FF04",	#FULLWIDTH DOLLAR SIGN
+	"81 91	FFE0",	#FULLWIDTH CENT SIGN
+	"81 92	FFE1",	#FULLWIDTH POUND SIGN
+	"81 93	FF05",	#FULLWIDTH PERCENT SIGN
+	"81 94	FF03",	#FULLWIDTH NUMBER SIGN
+	"81 95	FF06",	#FULLWIDTH AMPERSAND
+	"81 96	FF0A",	#FULLWIDTH ASTERISK
+	"81 97	FF20",	#FULLWIDTH COMMERCIAL AT
+	"81 98	00A7",	#SECTION SIGN
+	"81 99	2606",	#WHITE STAR
+	"81 9A	2605",	#BLACK STAR
+	"81 9B	25CB",	#WHITE CIRCLE
+	"81 9C	25CF",	#BLACK CIRCLE
+	"81 9D	25CE",	#BULLSEYE
+	"81 9E	25C7",	#WHITE DIAMOND
+	"81 9F	25C6",	#BLACK DIAMOND
+	"81 A0	25A1",	#WHITE SQUARE
+	"81 A1	25A0",	#BLACK SQUARE
+	"81 A2	25B3",	#WHITE UP-POINTING TRIANGLE
+	"81 A3	25B2",	#BLACK UP-POINTING TRIANGLE
+	"81 A4	25BD",	#WHITE DOWN-POINTING TRIANGLE
+	"81 A5	25BC",	#BLACK DOWN-POINTING TRIANGLE
+	"81 A6	203B",	#REFERENCE MARK
+	"81 A7	3012",	#POSTAL MARK
+	"81 A8	2192",	#RIGHTWARDS ARROW
+	"81 A9	2190",	#LEFTWARDS ARROW
+	"81 AA	2191",	#UPWARDS ARROW
+	"81 AB	2193",	#DOWNWARDS ARROW
+	"81 AC	3013",	#GETA MARK
+	"81 B8	2208",	#ELEMENT OF
+	"81 B9	220B",	#CONTAINS AS MEMBER
+	"81 BA	2286",	#SUBSET OF OR EQUAL TO
+	"81 BB	2287",	#SUPERSET OF OR EQUAL TO
+	"81 BC	2282",	#SUBSET OF
+	"81 BD	2283",	#SUPERSET OF
+	"81 BE	222A",	#UNION
+	"81 BF	2229",	#INTERSECTION
+	"81 C8	2227",	#LOGICAL AND
+	"81 C9	2228",	#LOGICAL OR
+	"81 CA	FFE2",	#FULLWIDTH NOT SIGN
+	"81 CB	21D2",	#RIGHTWARDS DOUBLE ARROW
+	"81 CC	21D4",	#LEFT RIGHT DOUBLE ARROW
+	"81 CD	2200",	#FOR ALL
+	"81 CE	2203",	#THERE EXISTS
+	"81 DA	2220",	#ANGLE
+	"81 DB	22A5",	#UP TACK
+	"81 DC	2312",	#ARC
+	"81 DD	2202",	#PARTIAL DIFFERENTIAL
+	"81 DE	2207",	#NABLA
+	"81 DF	2261",	#IDENTICAL TO
+	"81 E0	2252",	#APPROXIMATELY EQUAL TO OR THE IMAGE OF
+	"81 E1	226A",	#MUCH LESS-THAN
+	"81 E2	226B",	#MUCH GREATER-THAN
+	"81 E3	221A",	#SQUARE ROOT
+	"81 E4	223D",	#REVERSED TILDE
+	"81 E5	221D",	#PROPORTIONAL TO
+	"81 E6	2235",	#BECAUSE
+	"81 E7	222B",	#INTEGRAL
+	"81 E8	222C",	#DOUBLE INTEGRAL
+	"81 F0	212B",	#ANGSTROM SIGN
+	"81 F1	2030",	#PER MILLE SIGN
+	"81 F2	266F",	#MUSIC SHARP SIGN
+	"81 F3	266D",	#MUSIC FLAT SIGN
+	"81 F4	266A",	#EIGHTH NOTE
+	"81 F5	2020",	#DAGGER
+	"81 F6	2021",	#DOUBLE DAGGER
+	"81 F7	00B6",	#PILCROW SIGN
+	"81 FC	25EF",	#LARGE CIRCLE
+	"82 4F	FF10",	#FULLWIDTH DIGIT ZERO
+	"82 50	FF11",	#FULLWIDTH DIGIT ONE
+	"82 51	FF12",	#FULLWIDTH DIGIT TWO
+	"82 52	FF13",	#FULLWIDTH DIGIT THREE
+	"82 53	FF14",	#FULLWIDTH DIGIT FOUR
+	"82 54	FF15",	#FULLWIDTH DIGIT FIVE
+	"82 55	FF16",	#FULLWIDTH DIGIT SIX
+	"82 56	FF17",	#FULLWIDTH DIGIT SEVEN
+	"82 57	FF18",	#FULLWIDTH DIGIT EIGHT
+	"82 58	FF19",	#FULLWIDTH DIGIT NINE
+	"82 60	FF21",	#FULLWIDTH LATIN CAPITAL LETTER A
+	"82 61	FF22",	#FULLWIDTH LATIN CAPITAL LETTER B
+	"82 62	FF23",	#FULLWIDTH LATIN CAPITAL LETTER C
+	"82 63	FF24",	#FULLWIDTH LATIN CAPITAL LETTER D
+	"82 64	FF25",	#FULLWIDTH LATIN CAPITAL LETTER E
+	"82 65	FF26",	#FULLWIDTH LATIN CAPITAL LETTER F
+	"82 66	FF27",	#FULLWIDTH LATIN CAPITAL LETTER G
+	"82 67	FF28",	#FULLWIDTH LATIN CAPITAL LETTER H
+	"82 68	FF29",	#FULLWIDTH LATIN CAPITAL LETTER I
+	"82 69	FF2A",	#FULLWIDTH LATIN CAPITAL LETTER J
+	"82 6A	FF2B",	#FULLWIDTH LATIN CAPITAL LETTER K
+	"82 6B	FF2C",	#FULLWIDTH LATIN CAPITAL LETTER L
+	"82 6C	FF2D",	#FULLWIDTH LATIN CAPITAL LETTER M
+	"82 6D	FF2E",	#FULLWIDTH LATIN CAPITAL LETTER N
+	"82 6E	FF2F",	#FULLWIDTH LATIN CAPITAL LETTER O
+	"82 6F	FF30",	#FULLWIDTH LATIN CAPITAL LETTER P
+	"82 70	FF31",	#FULLWIDTH LATIN CAPITAL LETTER Q
+	"82 71	FF32",	#FULLWIDTH LATIN CAPITAL LETTER R
+	"82 72	FF33",	#FULLWIDTH LATIN CAPITAL LETTER S
+	"82 73	FF34",	#FULLWIDTH LATIN CAPITAL LETTER T
+	"82 74	FF35",	#FULLWIDTH LATIN CAPITAL LETTER U
+	"82 75	FF36",	#FULLWIDTH LATIN CAPITAL LETTER V
+	"82 76	FF37",	#FULLWIDTH LATIN CAPITAL LETTER W
+	"82 77	FF38",	#FULLWIDTH LATIN CAPITAL LETTER X
+	"82 78	FF39",	#FULLWIDTH LATIN CAPITAL LETTER Y
+	"82 79	FF3A",	#FULLWIDTH LATIN CAPITAL LETTER Z
+	"82 81	FF41",	#FULLWIDTH LATIN SMALL LETTER A
+	"82 82	FF42",	#FULLWIDTH LATIN SMALL LETTER B
+	"82 83	FF43",	#FULLWIDTH LATIN SMALL LETTER C
+	"82 84	FF44",	#FULLWIDTH LATIN SMALL LETTER D
+	"82 85	FF45",	#FULLWIDTH LATIN SMALL LETTER E
+	"82 86	FF46",	#FULLWIDTH LATIN SMALL LETTER F
+	"82 87	FF47",	#FULLWIDTH LATIN SMALL LETTER G
+	"82 88	FF48",	#FULLWIDTH LATIN SMALL LETTER H
+	"82 89	FF49",	#FULLWIDTH LATIN SMALL LETTER I
+	"82 8A	FF4A",	#FULLWIDTH LATIN SMALL LETTER J
+	"82 8B	FF4B",	#FULLWIDTH LATIN SMALL LETTER K
+	"82 8C	FF4C",	#FULLWIDTH LATIN SMALL LETTER L
+	"82 8D	FF4D",	#FULLWIDTH LATIN SMALL LETTER M
+	"82 8E	FF4E",	#FULLWIDTH LATIN SMALL LETTER N
+	"82 8F	FF4F",	#FULLWIDTH LATIN SMALL LETTER O
+	"82 90	FF50",	#FULLWIDTH LATIN SMALL LETTER P
+	"82 91	FF51",	#FULLWIDTH LATIN SMALL LETTER Q
+	"82 92	FF52",	#FULLWIDTH LATIN SMALL LETTER R
+	"82 93	FF53",	#FULLWIDTH LATIN SMALL LETTER S
+	"82 94	FF54",	#FULLWIDTH LATIN SMALL LETTER T
+	"82 95	FF55",	#FULLWIDTH LATIN SMALL LETTER U
+	"82 96	FF56",	#FULLWIDTH LATIN SMALL LETTER V
+	"82 97	FF57",	#FULLWIDTH LATIN SMALL LETTER W
+	"82 98	FF58",	#FULLWIDTH LATIN SMALL LETTER X
+	"82 99	FF59",	#FULLWIDTH LATIN SMALL LETTER Y
+	"82 9A	FF5A",	#FULLWIDTH LATIN SMALL LETTER Z
+	"82 9F	3041",	#HIRAGANA LETTER SMALL A
+	"82 A0	3042",	#HIRAGANA LETTER A
+	"82 A1	3043",	#HIRAGANA LETTER SMALL I
+	"82 A2	3044",	#HIRAGANA LETTER I
+	"82 A3	3045",	#HIRAGANA LETTER SMALL U
+	"82 A4	3046",	#HIRAGANA LETTER U
+	"82 A5	3047",	#HIRAGANA LETTER SMALL E
+	"82 A6	3048",	#HIRAGANA LETTER E
+	"82 A7	3049",	#HIRAGANA LETTER SMALL O
+	"82 A8	304A",	#HIRAGANA LETTER O
+	"82 A9	304B",	#HIRAGANA LETTER KA
+	"82 AA	304C",	#HIRAGANA LETTER GA
+	"82 AB	304D",	#HIRAGANA LETTER KI
+	"82 AC	304E",	#HIRAGANA LETTER GI
+	"82 AD	304F",	#HIRAGANA LETTER KU
+	"82 AE	3050",	#HIRAGANA LETTER GU
+	"82 AF	3051",	#HIRAGANA LETTER KE
+	"82 B0	3052",	#HIRAGANA LETTER GE
+	"82 B1	3053",	#HIRAGANA LETTER KO
+	"82 B2	3054",	#HIRAGANA LETTER GO
+	"82 B3	3055",	#HIRAGANA LETTER SA
+	"82 B4	3056",	#HIRAGANA LETTER ZA
+	"82 B5	3057",	#HIRAGANA LETTER SI
+	"82 B6	3058",	#HIRAGANA LETTER ZI
+	"82 B7	3059",	#HIRAGANA LETTER SU
+	"82 B8	305A",	#HIRAGANA LETTER ZU
+	"82 B9	305B",	#HIRAGANA LETTER SE
+	"82 BA	305C",	#HIRAGANA LETTER ZE
+	"82 BB	305D",	#HIRAGANA LETTER SO
+	"82 BC	305E",	#HIRAGANA LETTER ZO
+	"82 BD	305F",	#HIRAGANA LETTER TA
+	"82 BE	3060",	#HIRAGANA LETTER DA
+	"82 BF	3061",	#HIRAGANA LETTER TI
+	"82 C0	3062",	#HIRAGANA LETTER DI
+	"82 C1	3063",	#HIRAGANA LETTER SMALL TU
+	"82 C2	3064",	#HIRAGANA LETTER TU
+	"82 C3	3065",	#HIRAGANA LETTER DU
+	"82 C4	3066",	#HIRAGANA LETTER TE
+	"82 C5	3067",	#HIRAGANA LETTER DE
+	"82 C6	3068",	#HIRAGANA LETTER TO
+	"82 C7	3069",	#HIRAGANA LETTER DO
+	"82 C8	306A",	#HIRAGANA LETTER NA
+	"82 C9	306B",	#HIRAGANA LETTER NI
+	"82 CA	306C",	#HIRAGANA LETTER NU
+	"82 CB	306D",	#HIRAGANA LETTER NE
+	"82 CC	306E",	#HIRAGANA LETTER NO
+	"82 CD	306F",	#HIRAGANA LETTER HA
+	"82 CE	3070",	#HIRAGANA LETTER BA
+	"82 CF	3071",	#HIRAGANA LETTER PA
+	"82 D0	3072",	#HIRAGANA LETTER HI
+	"82 D1	3073",	#HIRAGANA LETTER BI
+	"82 D2	3074",	#HIRAGANA LETTER PI
+	"82 D3	3075",	#HIRAGANA LETTER HU
+	"82 D4	3076",	#HIRAGANA LETTER BU
+	"82 D5	3077",	#HIRAGANA LETTER PU
+	"82 D6	3078",	#HIRAGANA LETTER HE
+	"82 D7	3079",	#HIRAGANA LETTER BE
+	"82 D8	307A",	#HIRAGANA LETTER PE
+	"82 D9	307B",	#HIRAGANA LETTER HO
+	"82 DA	307C",	#HIRAGANA LETTER BO
+	"82 DB	307D",	#HIRAGANA LETTER PO
+	"82 DC	307E",	#HIRAGANA LETTER MA
+	"82 DD	307F",	#HIRAGANA LETTER MI
+	"82 DE	3080",	#HIRAGANA LETTER MU
+	"82 DF	3081",	#HIRAGANA LETTER ME
+	"82 E0	3082",	#HIRAGANA LETTER MO
+	"82 E1	3083",	#HIRAGANA LETTER SMALL YA
+	"82 E2	3084",	#HIRAGANA LETTER YA
+	"82 E3	3085",	#HIRAGANA LETTER SMALL YU
+	"82 E4	3086",	#HIRAGANA LETTER YU
+	"82 E5	3087",	#HIRAGANA LETTER SMALL YO
+	"82 E6	3088",	#HIRAGANA LETTER YO
+	"82 E7	3089",	#HIRAGANA LETTER RA
+	"82 E8	308A",	#HIRAGANA LETTER RI
+	"82 E9	308B",	#HIRAGANA LETTER RU
+	"82 EA	308C",	#HIRAGANA LETTER RE
+	"82 EB	308D",	#HIRAGANA LETTER RO
+	"82 EC	308E",	#HIRAGANA LETTER SMALL WA
+	"82 ED	308F",	#HIRAGANA LETTER WA
+	"82 EE	3090",	#HIRAGANA LETTER WI
+	"82 EF	3091",	#HIRAGANA LETTER WE
+	"82 F0	3092",	#HIRAGANA LETTER WO
+	"82 F1	3093",	#HIRAGANA LETTER N
+	"83 40	30A1",	#KATAKANA LETTER SMALL A
+	"83 41	30A2",	#KATAKANA LETTER A
+	"83 42	30A3",	#KATAKANA LETTER SMALL I
+	"83 43	30A4",	#KATAKANA LETTER I
+	"83 44	30A5",	#KATAKANA LETTER SMALL U
+	"83 45	30A6",	#KATAKANA LETTER U
+	"83 46	30A7",	#KATAKANA LETTER SMALL E
+	"83 47	30A8",	#KATAKANA LETTER E
+	"83 48	30A9",	#KATAKANA LETTER SMALL O
+	"83 49	30AA",	#KATAKANA LETTER O
+	"83 4A	30AB",	#KATAKANA LETTER KA
+	"83 4B	30AC",	#KATAKANA LETTER GA
+	"83 4C	30AD",	#KATAKANA LETTER KI
+	"83 4D	30AE",	#KATAKANA LETTER GI
+	"83 4E	30AF",	#KATAKANA LETTER KU
+	"83 4F	30B0",	#KATAKANA LETTER GU
+	"83 50	30B1",	#KATAKANA LETTER KE
+	"83 51	30B2",	#KATAKANA LETTER GE
+	"83 52	30B3",	#KATAKANA LETTER KO
+	"83 53	30B4",	#KATAKANA LETTER GO
+	"83 54	30B5",	#KATAKANA LETTER SA
+	"83 55	30B6",	#KATAKANA LETTER ZA
+	"83 56	30B7",	#KATAKANA LETTER SI
+	"83 57	30B8",	#KATAKANA LETTER ZI
+	"83 58	30B9",	#KATAKANA LETTER SU
+	"83 59	30BA",	#KATAKANA LETTER ZU
+	"83 5A	30BB",	#KATAKANA LETTER SE
+	"83 5B	30BC",	#KATAKANA LETTER ZE
+	"83 5C	30BD",	#KATAKANA LETTER SO
+	"83 5D	30BE",	#KATAKANA LETTER ZO
+	"83 5E	30BF",	#KATAKANA LETTER TA
+	"83 5F	30C0",	#KATAKANA LETTER DA
+	"83 60	30C1",	#KATAKANA LETTER TI
+	"83 61	30C2",	#KATAKANA LETTER DI
+	"83 62	30C3",	#KATAKANA LETTER SMALL TU
+	"83 63	30C4",	#KATAKANA LETTER TU
+	"83 64	30C5",	#KATAKANA LETTER DU
+	"83 65	30C6",	#KATAKANA LETTER TE
+	"83 66	30C7",	#KATAKANA LETTER DE
+	"83 67	30C8",	#KATAKANA LETTER TO
+	"83 68	30C9",	#KATAKANA LETTER DO
+	"83 69	30CA",	#KATAKANA LETTER NA
+	"83 6A	30CB",	#KATAKANA LETTER NI
+	"83 6B	30CC",	#KATAKANA LETTER NU
+	"83 6C	30CD",	#KATAKANA LETTER NE
+	"83 6D	30CE",	#KATAKANA LETTER NO
+	"83 6E	30CF",	#KATAKANA LETTER HA
+	"83 6F	30D0",	#KATAKANA LETTER BA
+	"83 70	30D1",	#KATAKANA LETTER PA
+	"83 71	30D2",	#KATAKANA LETTER HI
+	"83 72	30D3",	#KATAKANA LETTER BI
+	"83 73	30D4",	#KATAKANA LETTER PI
+	"83 74	30D5",	#KATAKANA LETTER HU
+	"83 75	30D6",	#KATAKANA LETTER BU
+	"83 76	30D7",	#KATAKANA LETTER PU
+	"83 77	30D8",	#KATAKANA LETTER HE
+	"83 78	30D9",	#KATAKANA LETTER BE
+	"83 79	30DA",	#KATAKANA LETTER PE
+	"83 7A	30DB",	#KATAKANA LETTER HO
+	"83 7B	30DC",	#KATAKANA LETTER BO
+	"83 7C	30DD",	#KATAKANA LETTER PO
+	"83 7D	30DE",	#KATAKANA LETTER MA
+	"83 7E	30DF",	#KATAKANA LETTER MI
+	"83 80	30E0",	#KATAKANA LETTER MU
+	"83 81	30E1",	#KATAKANA LETTER ME
+	"83 82	30E2",	#KATAKANA LETTER MO
+	"83 83	30E3",	#KATAKANA LETTER SMALL YA
+	"83 84	30E4",	#KATAKANA LETTER YA
+	"83 85	30E5",	#KATAKANA LETTER SMALL YU
+	"83 86	30E6",	#KATAKANA LETTER YU
+	"83 87	30E7",	#KATAKANA LETTER SMALL YO
+	"83 88	30E8",	#KATAKANA LETTER YO
+	"83 89	30E9",	#KATAKANA LETTER RA
+	"83 8A	30EA",	#KATAKANA LETTER RI
+	"83 8B	30EB",	#KATAKANA LETTER RU
+	"83 8C	30EC",	#KATAKANA LETTER RE
+	"83 8D	30ED",	#KATAKANA LETTER RO
+	"83 8E	30EE",	#KATAKANA LETTER SMALL WA
+	"83 8F	30EF",	#KATAKANA LETTER WA
+	"83 90	30F0",	#KATAKANA LETTER WI
+	"83 91	30F1",	#KATAKANA LETTER WE
+	"83 92	30F2",	#KATAKANA LETTER WO
+	"83 93	30F3",	#KATAKANA LETTER N
+	"83 94	30F4",	#KATAKANA LETTER VU
+	"83 95	30F5",	#KATAKANA LETTER SMALL KA
+	"83 96	30F6",	#KATAKANA LETTER SMALL KE
+	"83 9F	0391",	#GREEK CAPITAL LETTER ALPHA
+	"83 A0	0392",	#GREEK CAPITAL LETTER BETA
+	"83 A1	0393",	#GREEK CAPITAL LETTER GAMMA
+	"83 A2	0394",	#GREEK CAPITAL LETTER DELTA
+	"83 A3	0395",	#GREEK CAPITAL LETTER EPSILON
+	"83 A4	0396",	#GREEK CAPITAL LETTER ZETA
+	"83 A5	0397",	#GREEK CAPITAL LETTER ETA
+	"83 A6	0398",	#GREEK CAPITAL LETTER THETA
+	"83 A7	0399",	#GREEK CAPITAL LETTER IOTA
+	"83 A8	039A",	#GREEK CAPITAL LETTER KAPPA
+	"83 A9	039B",	#GREEK CAPITAL LETTER LAMDA
+	"83 AA	039C",	#GREEK CAPITAL LETTER MU
+	"83 AB	039D",	#GREEK CAPITAL LETTER NU
+	"83 AC	039E",	#GREEK CAPITAL LETTER XI
+	"83 AD	039F",	#GREEK CAPITAL LETTER OMICRON
+	"83 AE	03A0",	#GREEK CAPITAL LETTER PI
+	"83 AF	03A1",	#GREEK CAPITAL LETTER RHO
+	"83 B0	03A3",	#GREEK CAPITAL LETTER SIGMA
+	"83 B1	03A4",	#GREEK CAPITAL LETTER TAU
+	"83 B2	03A5",	#GREEK CAPITAL LETTER UPSILON
+	"83 B3	03A6",	#GREEK CAPITAL LETTER PHI
+	"83 B4	03A7",	#GREEK CAPITAL LETTER CHI
+	"83 B5	03A8",	#GREEK CAPITAL LETTER PSI
+	"83 B6	03A9",	#GREEK CAPITAL LETTER OMEGA
+	"83 BF	03B1",	#GREEK SMALL LETTER ALPHA
+	"83 C0	03B2",	#GREEK SMALL LETTER BETA
+	"83 C1	03B3",	#GREEK SMALL LETTER GAMMA
+	"83 C2	03B4",	#GREEK SMALL LETTER DELTA
+	"83 C3	03B5",	#GREEK SMALL LETTER EPSILON
+	"83 C4	03B6",	#GREEK SMALL LETTER ZETA
+	"83 C5	03B7",	#GREEK SMALL LETTER ETA
+	"83 C6	03B8",	#GREEK SMALL LETTER THETA
+	"83 C7	03B9",	#GREEK SMALL LETTER IOTA
+	"83 C8	03BA",	#GREEK SMALL LETTER KAPPA
+	"83 C9	03BB",	#GREEK SMALL LETTER LAMDA
+	"83 CA	03BC",	#GREEK SMALL LETTER MU
+	"83 CB	03BD",	#GREEK SMALL LETTER NU
+	"83 CC	03BE",	#GREEK SMALL LETTER XI
+	"83 CD	03BF",	#GREEK SMALL LETTER OMICRON
+	"83 CE	03C0",	#GREEK SMALL LETTER PI
+	"83 CF	03C1",	#GREEK SMALL LETTER RHO
+	"83 D0	03C3",	#GREEK SMALL LETTER SIGMA
+	"83 D1	03C4",	#GREEK SMALL LETTER TAU
+	"83 D2	03C5",	#GREEK SMALL LETTER UPSILON
+	"83 D3	03C6",	#GREEK SMALL LETTER PHI
+	"83 D4	03C7",	#GREEK SMALL LETTER CHI
+	"83 D5	03C8",	#GREEK SMALL LETTER PSI
+	"83 D6	03C9",	#GREEK SMALL LETTER OMEGA
+	"84 40	0410",	#CYRILLIC CAPITAL LETTER A
+	"84 41	0411",	#CYRILLIC CAPITAL LETTER BE
+	"84 42	0412",	#CYRILLIC CAPITAL LETTER VE
+	"84 43	0413",	#CYRILLIC CAPITAL LETTER GHE
+	"84 44	0414",	#CYRILLIC CAPITAL LETTER DE
+	"84 45	0415",	#CYRILLIC CAPITAL LETTER IE
+	"84 46	0401",	#CYRILLIC CAPITAL LETTER IO
+	"84 47	0416",	#CYRILLIC CAPITAL LETTER ZHE
+	"84 48	0417",	#CYRILLIC CAPITAL LETTER ZE
+	"84 49	0418",	#CYRILLIC CAPITAL LETTER I
+	"84 4A	0419",	#CYRILLIC CAPITAL LETTER SHORT I
+	"84 4B	041A",	#CYRILLIC CAPITAL LETTER KA
+	"84 4C	041B",	#CYRILLIC CAPITAL LETTER EL
+	"84 4D	041C",	#CYRILLIC CAPITAL LETTER EM
+	"84 4E	041D",	#CYRILLIC CAPITAL LETTER EN
+	"84 4F	041E",	#CYRILLIC CAPITAL LETTER O
+	"84 50	041F",	#CYRILLIC CAPITAL LETTER PE
+	"84 51	0420",	#CYRILLIC CAPITAL LETTER ER
+	"84 52	0421",	#CYRILLIC CAPITAL LETTER ES
+	"84 53	0422",	#CYRILLIC CAPITAL LETTER TE
+	"84 54	0423",	#CYRILLIC CAPITAL LETTER U
+	"84 55	0424",	#CYRILLIC CAPITAL LETTER EF
+	"84 56	0425",	#CYRILLIC CAPITAL LETTER HA
+	"84 57	0426",	#CYRILLIC CAPITAL LETTER TSE
+	"84 58	0427",	#CYRILLIC CAPITAL LETTER CHE
+	"84 59	0428",	#CYRILLIC CAPITAL LETTER SHA
+	"84 5A	0429",	#CYRILLIC CAPITAL LETTER SHCHA
+	"84 5B	042A",	#CYRILLIC CAPITAL LETTER HARD SIGN
+	"84 5C	042B",	#CYRILLIC CAPITAL LETTER YERU
+	"84 5D	042C",	#CYRILLIC CAPITAL LETTER SOFT SIGN
+	"84 5E	042D",	#CYRILLIC CAPITAL LETTER E
+	"84 5F	042E",	#CYRILLIC CAPITAL LETTER YU
+	"84 60	042F",	#CYRILLIC CAPITAL LETTER YA
+	"84 70	0430",	#CYRILLIC SMALL LETTER A
+	"84 71	0431",	#CYRILLIC SMALL LETTER BE
+	"84 72	0432",	#CYRILLIC SMALL LETTER VE
+	"84 73	0433",	#CYRILLIC SMALL LETTER GHE
+	"84 74	0434",	#CYRILLIC SMALL LETTER DE
+	"84 75	0435",	#CYRILLIC SMALL LETTER IE
+	"84 76	0451",	#CYRILLIC SMALL LETTER IO
+	"84 77	0436",	#CYRILLIC SMALL LETTER ZHE
+	"84 78	0437",	#CYRILLIC SMALL LETTER ZE
+	"84 79	0438",	#CYRILLIC SMALL LETTER I
+	"84 7A	0439",	#CYRILLIC SMALL LETTER SHORT I
+	"84 7B	043A",	#CYRILLIC SMALL LETTER KA
+	"84 7C	043B",	#CYRILLIC SMALL LETTER EL
+	"84 7D	043C",	#CYRILLIC SMALL LETTER EM
+	"84 7E	043D",	#CYRILLIC SMALL LETTER EN
+	"84 80	043E",	#CYRILLIC SMALL LETTER O
+	"84 81	043F",	#CYRILLIC SMALL LETTER PE
+	"84 82	0440",	#CYRILLIC SMALL LETTER ER
+	"84 83	0441",	#CYRILLIC SMALL LETTER ES
+	"84 84	0442",	#CYRILLIC SMALL LETTER TE
+	"84 85	0443",	#CYRILLIC SMALL LETTER U
+	"84 86	0444",	#CYRILLIC SMALL LETTER EF
+	"84 87	0445",	#CYRILLIC SMALL LETTER HA
+	"84 88	0446",	#CYRILLIC SMALL LETTER TSE
+	"84 89	0447",	#CYRILLIC SMALL LETTER CHE
+	"84 8A	0448",	#CYRILLIC SMALL LETTER SHA
+	"84 8B	0449",	#CYRILLIC SMALL LETTER SHCHA
+	"84 8C	044A",	#CYRILLIC SMALL LETTER HARD SIGN
+	"84 8D	044B",	#CYRILLIC SMALL LETTER YERU
+	"84 8E	044C",	#CYRILLIC SMALL LETTER SOFT SIGN
+	"84 8F	044D",	#CYRILLIC SMALL LETTER E
+	"84 90	044E",	#CYRILLIC SMALL LETTER YU
+	"84 91	044F",	#CYRILLIC SMALL LETTER YA
+	"84 9F	2500",	#BOX DRAWINGS LIGHT HORIZONTAL
+	"84 A0	2502",	#BOX DRAWINGS LIGHT VERTICAL
+	"84 A1	250C",	#BOX DRAWINGS LIGHT DOWN AND RIGHT
+	"84 A2	2510",	#BOX DRAWINGS LIGHT DOWN AND LEFT
+	"84 A3	2518",	#BOX DRAWINGS LIGHT UP AND LEFT
+	"84 A4	2514",	#BOX DRAWINGS LIGHT UP AND RIGHT
+	"84 A5	251C",	#BOX DRAWINGS LIGHT VERTICAL AND RIGHT
+	"84 A6	252C",	#BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
+	"84 A7	2524",	#BOX DRAWINGS LIGHT VERTICAL AND LEFT
+	"84 A8	2534",	#BOX DRAWINGS LIGHT UP AND HORIZONTAL
+	"84 A9	253C",	#BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL
+	"84 AA	2501",	#BOX DRAWINGS HEAVY HORIZONTAL
+	"84 AB	2503",	#BOX DRAWINGS HEAVY VERTICAL
+	"84 AC	250F",	#BOX DRAWINGS HEAVY DOWN AND RIGHT
+	"84 AD	2513",	#BOX DRAWINGS HEAVY DOWN AND LEFT
+	"84 AE	251B",	#BOX DRAWINGS HEAVY UP AND LEFT
+	"84 AF	2517",	#BOX DRAWINGS HEAVY UP AND RIGHT
+	"84 B0	2523",	#BOX DRAWINGS HEAVY VERTICAL AND RIGHT
+	"84 B1	2533",	#BOX DRAWINGS HEAVY DOWN AND HORIZONTAL
+	"84 B2	252B",	#BOX DRAWINGS HEAVY VERTICAL AND LEFT
+	"84 B3	253B",	#BOX DRAWINGS HEAVY UP AND HORIZONTAL
+	"84 B4	254B",	#BOX DRAWINGS HEAVY VERTICAL AND HORIZONTAL
+	"84 B5	2520",	#BOX DRAWINGS VERTICAL HEAVY AND RIGHT LIGHT
+	"84 B6	252F",	#BOX DRAWINGS DOWN LIGHT AND HORIZONTAL HEAVY
+	"84 B7	2528",	#BOX DRAWINGS VERTICAL HEAVY AND LEFT LIGHT
+	"84 B8	2537",	#BOX DRAWINGS UP LIGHT AND HORIZONTAL HEAVY
+	"84 B9	253F",	#BOX DRAWINGS VERTICAL LIGHT AND HORIZONTAL HEAVY
+	"84 BA	251D",	#BOX DRAWINGS VERTICAL LIGHT AND RIGHT HEAVY
+	"84 BB	2530",	#BOX DRAWINGS DOWN HEAVY AND HORIZONTAL LIGHT
+	"84 BC	2525",	#BOX DRAWINGS VERTICAL LIGHT AND LEFT HEAVY
+	"84 BD	2538",	#BOX DRAWINGS UP HEAVY AND HORIZONTAL LIGHT
+	"84 BE	2542",	#BOX DRAWINGS VERTICAL HEAVY AND HORIZONTAL LIGHT
+	"87 40	2460",	#CIRCLED DIGIT ONE
+	"87 41	2461",	#CIRCLED DIGIT TWO
+	"87 42	2462",	#CIRCLED DIGIT THREE
+	"87 43	2463",	#CIRCLED DIGIT FOUR
+	"87 44	2464",	#CIRCLED DIGIT FIVE
+	"87 45	2465",	#CIRCLED DIGIT SIX
+	"87 46	2466",	#CIRCLED DIGIT SEVEN
+	"87 47	2467",	#CIRCLED DIGIT EIGHT
+	"87 48	2468",	#CIRCLED DIGIT NINE
+	"87 49	2469",	#CIRCLED NUMBER TEN
+	"87 4A	246A",	#CIRCLED NUMBER ELEVEN
+	"87 4B	246B",	#CIRCLED NUMBER TWELVE
+	"87 4C	246C",	#CIRCLED NUMBER THIRTEEN
+	"87 4D	246D",	#CIRCLED NUMBER FOURTEEN
+	"87 4E	246E",	#CIRCLED NUMBER FIFTEEN
+	"87 4F	246F",	#CIRCLED NUMBER SIXTEEN
+	"87 50	2470",	#CIRCLED NUMBER SEVENTEEN
+	"87 51	2471",	#CIRCLED NUMBER EIGHTEEN
+	"87 52	2472",	#CIRCLED NUMBER NINETEEN
+	"87 53	2473",	#CIRCLED NUMBER TWENTY
+	"87 54	2160",	#ROMAN NUMERAL ONE
+	"87 55	2161",	#ROMAN NUMERAL TWO
+	"87 56	2162",	#ROMAN NUMERAL THREE
+	"87 57	2163",	#ROMAN NUMERAL FOUR
+	"87 58	2164",	#ROMAN NUMERAL FIVE
+	"87 59	2165",	#ROMAN NUMERAL SIX
+	"87 5A	2166",	#ROMAN NUMERAL SEVEN
+	"87 5B	2167",	#ROMAN NUMERAL EIGHT
+	"87 5C	2168",	#ROMAN NUMERAL NINE
+	"87 5D	2169",	#ROMAN NUMERAL TEN
+	"87 5F	3349",	#SQUARE MIRI
+	"87 60	3314",	#SQUARE KIRO
+	"87 61	3322",	#SQUARE SENTI
+	"87 62	334D",	#SQUARE MEETORU
+	"87 63	3318",	#SQUARE GURAMU
+	"87 64	3327",	#SQUARE TON
+	"87 65	3303",	#SQUARE AARU
+	"87 66	3336",	#SQUARE HEKUTAARU
+	"87 67	3351",	#SQUARE RITTORU
+	"87 68	3357",	#SQUARE WATTO
+	"87 69	330D",	#SQUARE KARORII
+	"87 6A	3326",	#SQUARE DORU
+	"87 6B	3323",	#SQUARE SENTO
+	"87 6C	332B",	#SQUARE PAASENTO
+	"87 6D	334A",	#SQUARE MIRIBAARU
+	"87 6E	333B",	#SQUARE PEEZI
+	"87 6F	339C",	#SQUARE MM
+	"87 70	339D",	#SQUARE CM
+	"87 71	339E",	#SQUARE KM
+	"87 72	338E",	#SQUARE MG
+	"87 73	338F",	#SQUARE KG
+	"87 74	33C4",	#SQUARE CC
+	"87 75	33A1",	#SQUARE M SQUARED
+	"87 7E	337B",	#SQUARE ERA NAME HEISEI
+	"87 80	301D",	#REVERSED DOUBLE PRIME QUOTATION MARK
+	"87 81	301F",	#LOW DOUBLE PRIME QUOTATION MARK
+	"87 82	2116",	#NUMERO SIGN
+	"87 83	33CD",	#SQUARE KK
+	"87 84	2121",	#TELEPHONE SIGN
+	"87 85	32A4",	#CIRCLED IDEOGRAPH HIGH
+	"87 86	32A5",	#CIRCLED IDEOGRAPH CENTRE
+	"87 87	32A6",	#CIRCLED IDEOGRAPH LOW
+	"87 88	32A7",	#CIRCLED IDEOGRAPH LEFT
+	"87 89	32A8",	#CIRCLED IDEOGRAPH RIGHT
+	"87 8A	3231",	#PARENTHESIZED IDEOGRAPH STOCK
+	"87 8B	3232",	#PARENTHESIZED IDEOGRAPH HAVE
+	"87 8C	3239",	#PARENTHESIZED IDEOGRAPH REPRESENT
+	"87 8D	337E",	#SQUARE ERA NAME MEIZI
+	"87 8E	337D",	#SQUARE ERA NAME TAISYOU
+	"87 8F	337C",	#SQUARE ERA NAME SYOUWA
+	"87 90	2252",	#APPROXIMATELY EQUAL TO OR THE IMAGE OF
+	"87 91	2261",	#IDENTICAL TO
+	"87 92	222B",	#INTEGRAL
+	"87 93	222E",	#CONTOUR INTEGRAL
+	"87 94	2211",	#N-ARY SUMMATION
+	"87 95	221A",	#SQUARE ROOT
+	"87 96	22A5",	#UP TACK
+	"87 97	2220",	#ANGLE
+	"87 98	221F",	#RIGHT ANGLE
+	"87 99	22BF",	#RIGHT TRIANGLE
+	"87 9A	2235",	#BECAUSE
+	"87 9B	2229",	#INTERSECTION
+	"87 9C	222A",	#UNION
+	"88 9F	4E9C",	#CJK UNIFIED IDEOGRAPH
+	"88 A0	5516",	#CJK UNIFIED IDEOGRAPH
+	"88 A1	5A03",	#CJK UNIFIED IDEOGRAPH
+	"88 A2	963F",	#CJK UNIFIED IDEOGRAPH
+	"88 A3	54C0",	#CJK UNIFIED IDEOGRAPH
+	"88 A4	611B",	#CJK UNIFIED IDEOGRAPH
+	"88 A5	6328",	#CJK UNIFIED IDEOGRAPH
+	"88 A6	59F6",	#CJK UNIFIED IDEOGRAPH
+	"88 A7	9022",	#CJK UNIFIED IDEOGRAPH
+	"88 A8	8475",	#CJK UNIFIED IDEOGRAPH
+	"88 A9	831C",	#CJK UNIFIED IDEOGRAPH
+	"88 AA	7A50",	#CJK UNIFIED IDEOGRAPH
+	"88 AB	60AA",	#CJK UNIFIED IDEOGRAPH
+	"88 AC	63E1",	#CJK UNIFIED IDEOGRAPH
+	"88 AD	6E25",	#CJK UNIFIED IDEOGRAPH
+	"88 AE	65ED",	#CJK UNIFIED IDEOGRAPH
+	"88 AF	8466",	#CJK UNIFIED IDEOGRAPH
+	"88 B0	82A6",	#CJK UNIFIED IDEOGRAPH
+	"88 B1	9BF5",	#CJK UNIFIED IDEOGRAPH
+	"88 B2	6893",	#CJK UNIFIED IDEOGRAPH
+	"88 B3	5727",	#CJK UNIFIED IDEOGRAPH
+	"88 B4	65A1",	#CJK UNIFIED IDEOGRAPH
+	"88 B5	6271",	#CJK UNIFIED IDEOGRAPH
+	"88 B6	5B9B",	#CJK UNIFIED IDEOGRAPH
+	"88 B7	59D0",	#CJK UNIFIED IDEOGRAPH
+	"88 B8	867B",	#CJK UNIFIED IDEOGRAPH
+	"88 B9	98F4",	#CJK UNIFIED IDEOGRAPH
+	"88 BA	7D62",	#CJK UNIFIED IDEOGRAPH
+	"88 BB	7DBE",	#CJK UNIFIED IDEOGRAPH
+	"88 BC	9B8E",	#CJK UNIFIED IDEOGRAPH
+	"88 BD	6216",	#CJK UNIFIED IDEOGRAPH
+	"88 BE	7C9F",	#CJK UNIFIED IDEOGRAPH
+	"88 BF	88B7",	#CJK UNIFIED IDEOGRAPH
+	"88 C0	5B89",	#CJK UNIFIED IDEOGRAPH
+	"88 C1	5EB5",	#CJK UNIFIED IDEOGRAPH
+	"88 C2	6309",	#CJK UNIFIED IDEOGRAPH
+	"88 C3	6697",	#CJK UNIFIED IDEOGRAPH
+	"88 C4	6848",	#CJK UNIFIED IDEOGRAPH
+	"88 C5	95C7",	#CJK UNIFIED IDEOGRAPH
+	"88 C6	978D",	#CJK UNIFIED IDEOGRAPH
+	"88 C7	674F",	#CJK UNIFIED IDEOGRAPH
+	"88 C8	4EE5",	#CJK UNIFIED IDEOGRAPH
+	"88 C9	4F0A",	#CJK UNIFIED IDEOGRAPH
+	"88 CA	4F4D",	#CJK UNIFIED IDEOGRAPH
+	"88 CB	4F9D",	#CJK UNIFIED IDEOGRAPH
+	"88 CC	5049",	#CJK UNIFIED IDEOGRAPH
+	"88 CD	56F2",	#CJK UNIFIED IDEOGRAPH
+	"88 CE	5937",	#CJK UNIFIED IDEOGRAPH
+	"88 CF	59D4",	#CJK UNIFIED IDEOGRAPH
+	"88 D0	5A01",	#CJK UNIFIED IDEOGRAPH
+	"88 D1	5C09",	#CJK UNIFIED IDEOGRAPH
+	"88 D2	60DF",	#CJK UNIFIED IDEOGRAPH
+	"88 D3	610F",	#CJK UNIFIED IDEOGRAPH
+	"88 D4	6170",	#CJK UNIFIED IDEOGRAPH
+	"88 D5	6613",	#CJK UNIFIED IDEOGRAPH
+	"88 D6	6905",	#CJK UNIFIED IDEOGRAPH
+	"88 D7	70BA",	#CJK UNIFIED IDEOGRAPH
+	"88 D8	754F",	#CJK UNIFIED IDEOGRAPH
+	"88 D9	7570",	#CJK UNIFIED IDEOGRAPH
+	"88 DA	79FB",	#CJK UNIFIED IDEOGRAPH
+	"88 DB	7DAD",	#CJK UNIFIED IDEOGRAPH
+	"88 DC	7DEF",	#CJK UNIFIED IDEOGRAPH
+	"88 DD	80C3",	#CJK UNIFIED IDEOGRAPH
+	"88 DE	840E",	#CJK UNIFIED IDEOGRAPH
+	"88 DF	8863",	#CJK UNIFIED IDEOGRAPH
+	"88 E0	8B02",	#CJK UNIFIED IDEOGRAPH
+	"88 E1	9055",	#CJK UNIFIED IDEOGRAPH
+	"88 E2	907A",	#CJK UNIFIED IDEOGRAPH
+	"88 E3	533B",	#CJK UNIFIED IDEOGRAPH
+	"88 E4	4E95",	#CJK UNIFIED IDEOGRAPH
+	"88 E5	4EA5",	#CJK UNIFIED IDEOGRAPH
+	"88 E6	57DF",	#CJK UNIFIED IDEOGRAPH
+	"88 E7	80B2",	#CJK UNIFIED IDEOGRAPH
+	"88 E8	90C1",	#CJK UNIFIED IDEOGRAPH
+	"88 E9	78EF",	#CJK UNIFIED IDEOGRAPH
+	"88 EA	4E00",	#CJK UNIFIED IDEOGRAPH
+	"88 EB	58F1",	#CJK UNIFIED IDEOGRAPH
+	"88 EC	6EA2",	#CJK UNIFIED IDEOGRAPH
+	"88 ED	9038",	#CJK UNIFIED IDEOGRAPH
+	"88 EE	7A32",	#CJK UNIFIED IDEOGRAPH
+	"88 EF	8328",	#CJK UNIFIED IDEOGRAPH
+	"88 F0	828B",	#CJK UNIFIED IDEOGRAPH
+	"88 F1	9C2F",	#CJK UNIFIED IDEOGRAPH
+	"88 F2	5141",	#CJK UNIFIED IDEOGRAPH
+	"88 F3	5370",	#CJK UNIFIED IDEOGRAPH
+	"88 F4	54BD",	#CJK UNIFIED IDEOGRAPH
+	"88 F5	54E1",	#CJK UNIFIED IDEOGRAPH
+	"88 F6	56E0",	#CJK UNIFIED IDEOGRAPH
+	"88 F7	59FB",	#CJK UNIFIED IDEOGRAPH
+	"88 F8	5F15",	#CJK UNIFIED IDEOGRAPH
+	"88 F9	98F2",	#CJK UNIFIED IDEOGRAPH
+	"88 FA	6DEB",	#CJK UNIFIED IDEOGRAPH
+	"88 FB	80E4",	#CJK UNIFIED IDEOGRAPH
+	"88 FC	852D",	#CJK UNIFIED IDEOGRAPH
+	"89 40	9662",	#CJK UNIFIED IDEOGRAPH
+	"89 41	9670",	#CJK UNIFIED IDEOGRAPH
+	"89 42	96A0",	#CJK UNIFIED IDEOGRAPH
+	"89 43	97FB",	#CJK UNIFIED IDEOGRAPH
+	"89 44	540B",	#CJK UNIFIED IDEOGRAPH
+	"89 45	53F3",	#CJK UNIFIED IDEOGRAPH
+	"89 46	5B87",	#CJK UNIFIED IDEOGRAPH
+	"89 47	70CF",	#CJK UNIFIED IDEOGRAPH
+	"89 48	7FBD",	#CJK UNIFIED IDEOGRAPH
+	"89 49	8FC2",	#CJK UNIFIED IDEOGRAPH
+	"89 4A	96E8",	#CJK UNIFIED IDEOGRAPH
+	"89 4B	536F",	#CJK UNIFIED IDEOGRAPH
+	"89 4C	9D5C",	#CJK UNIFIED IDEOGRAPH
+	"89 4D	7ABA",	#CJK UNIFIED IDEOGRAPH
+	"89 4E	4E11",	#CJK UNIFIED IDEOGRAPH
+	"89 4F	7893",	#CJK UNIFIED IDEOGRAPH
+	"89 50	81FC",	#CJK UNIFIED IDEOGRAPH
+	"89 51	6E26",	#CJK UNIFIED IDEOGRAPH
+	"89 52	5618",	#CJK UNIFIED IDEOGRAPH
+	"89 53	5504",	#CJK UNIFIED IDEOGRAPH
+	"89 54	6B1D",	#CJK UNIFIED IDEOGRAPH
+	"89 55	851A",	#CJK UNIFIED IDEOGRAPH
+	"89 56	9C3B",	#CJK UNIFIED IDEOGRAPH
+	"89 57	59E5",	#CJK UNIFIED IDEOGRAPH
+	"89 58	53A9",	#CJK UNIFIED IDEOGRAPH
+	"89 59	6D66",	#CJK UNIFIED IDEOGRAPH
+	"89 5A	74DC",	#CJK UNIFIED IDEOGRAPH
+	"89 5B	958F",	#CJK UNIFIED IDEOGRAPH
+	"89 5C	5642",	#CJK UNIFIED IDEOGRAPH
+	"89 5D	4E91",	#CJK UNIFIED IDEOGRAPH
+	"89 5E	904B",	#CJK UNIFIED IDEOGRAPH
+	"89 5F	96F2",	#CJK UNIFIED IDEOGRAPH
+	"89 60	834F",	#CJK UNIFIED IDEOGRAPH
+	"89 61	990C",	#CJK UNIFIED IDEOGRAPH
+	"89 62	53E1",	#CJK UNIFIED IDEOGRAPH
+	"89 63	55B6",	#CJK UNIFIED IDEOGRAPH
+	"89 64	5B30",	#CJK UNIFIED IDEOGRAPH
+	"89 65	5F71",	#CJK UNIFIED IDEOGRAPH
+	"89 66	6620",	#CJK UNIFIED IDEOGRAPH
+	"89 67	66F3",	#CJK UNIFIED IDEOGRAPH
+	"89 68	6804",	#CJK UNIFIED IDEOGRAPH
+	"89 69	6C38",	#CJK UNIFIED IDEOGRAPH
+	"89 6A	6CF3",	#CJK UNIFIED IDEOGRAPH
+	"89 6B	6D29",	#CJK UNIFIED IDEOGRAPH
+	"89 6C	745B",	#CJK UNIFIED IDEOGRAPH
+	"89 6D	76C8",	#CJK UNIFIED IDEOGRAPH
+	"89 6E	7A4E",	#CJK UNIFIED IDEOGRAPH
+	"89 6F	9834",	#CJK UNIFIED IDEOGRAPH
+	"89 70	82F1",	#CJK UNIFIED IDEOGRAPH
+	"89 71	885B",	#CJK UNIFIED IDEOGRAPH
+	"89 72	8A60",	#CJK UNIFIED IDEOGRAPH
+	"89 73	92ED",	#CJK UNIFIED IDEOGRAPH
+	"89 74	6DB2",	#CJK UNIFIED IDEOGRAPH
+	"89 75	75AB",	#CJK UNIFIED IDEOGRAPH
+	"89 76	76CA",	#CJK UNIFIED IDEOGRAPH
+	"89 77	99C5",	#CJK UNIFIED IDEOGRAPH
+	"89 78	60A6",	#CJK UNIFIED IDEOGRAPH
+	"89 79	8B01",	#CJK UNIFIED IDEOGRAPH
+	"89 7A	8D8A",	#CJK UNIFIED IDEOGRAPH
+	"89 7B	95B2",	#CJK UNIFIED IDEOGRAPH
+	"89 7C	698E",	#CJK UNIFIED IDEOGRAPH
+	"89 7D	53AD",	#CJK UNIFIED IDEOGRAPH
+	"89 7E	5186",	#CJK UNIFIED IDEOGRAPH
+	"89 80	5712",	#CJK UNIFIED IDEOGRAPH
+	"89 81	5830",	#CJK UNIFIED IDEOGRAPH
+	"89 82	5944",	#CJK UNIFIED IDEOGRAPH
+	"89 83	5BB4",	#CJK UNIFIED IDEOGRAPH
+	"89 84	5EF6",	#CJK UNIFIED IDEOGRAPH
+	"89 85	6028",	#CJK UNIFIED IDEOGRAPH
+	"89 86	63A9",	#CJK UNIFIED IDEOGRAPH
+	"89 87	63F4",	#CJK UNIFIED IDEOGRAPH
+	"89 88	6CBF",	#CJK UNIFIED IDEOGRAPH
+	"89 89	6F14",	#CJK UNIFIED IDEOGRAPH
+	"89 8A	708E",	#CJK UNIFIED IDEOGRAPH
+	"89 8B	7114",	#CJK UNIFIED IDEOGRAPH
+	"89 8C	7159",	#CJK UNIFIED IDEOGRAPH
+	"89 8D	71D5",	#CJK UNIFIED IDEOGRAPH
+	"89 8E	733F",	#CJK UNIFIED IDEOGRAPH
+	"89 8F	7E01",	#CJK UNIFIED IDEOGRAPH
+	"89 90	8276",	#CJK UNIFIED IDEOGRAPH
+	"89 91	82D1",	#CJK UNIFIED IDEOGRAPH
+	"89 92	8597",	#CJK UNIFIED IDEOGRAPH
+	"89 93	9060",	#CJK UNIFIED IDEOGRAPH
+	"89 94	925B",	#CJK UNIFIED IDEOGRAPH
+	"89 95	9D1B",	#CJK UNIFIED IDEOGRAPH
+	"89 96	5869",	#CJK UNIFIED IDEOGRAPH
+	"89 97	65BC",	#CJK UNIFIED IDEOGRAPH
+	"89 98	6C5A",	#CJK UNIFIED IDEOGRAPH
+	"89 99	7525",	#CJK UNIFIED IDEOGRAPH
+	"89 9A	51F9",	#CJK UNIFIED IDEOGRAPH
+	"89 9B	592E",	#CJK UNIFIED IDEOGRAPH
+	"89 9C	5965",	#CJK UNIFIED IDEOGRAPH
+	"89 9D	5F80",	#CJK UNIFIED IDEOGRAPH
+	"89 9E	5FDC",	#CJK UNIFIED IDEOGRAPH
+	"89 9F	62BC",	#CJK UNIFIED IDEOGRAPH
+	"89 A0	65FA",	#CJK UNIFIED IDEOGRAPH
+	"89 A1	6A2A",	#CJK UNIFIED IDEOGRAPH
+	"89 A2	6B27",	#CJK UNIFIED IDEOGRAPH
+	"89 A3	6BB4",	#CJK UNIFIED IDEOGRAPH
+	"89 A4	738B",	#CJK UNIFIED IDEOGRAPH
+	"89 A5	7FC1",	#CJK UNIFIED IDEOGRAPH
+	"89 A6	8956",	#CJK UNIFIED IDEOGRAPH
+	"89 A7	9D2C",	#CJK UNIFIED IDEOGRAPH
+	"89 A8	9D0E",	#CJK UNIFIED IDEOGRAPH
+	"89 A9	9EC4",	#CJK UNIFIED IDEOGRAPH
+	"89 AA	5CA1",	#CJK UNIFIED IDEOGRAPH
+	"89 AB	6C96",	#CJK UNIFIED IDEOGRAPH
+	"89 AC	837B",	#CJK UNIFIED IDEOGRAPH
+	"89 AD	5104",	#CJK UNIFIED IDEOGRAPH
+	"89 AE	5C4B",	#CJK UNIFIED IDEOGRAPH
+	"89 AF	61B6",	#CJK UNIFIED IDEOGRAPH
+	"89 B0	81C6",	#CJK UNIFIED IDEOGRAPH
+	"89 B1	6876",	#CJK UNIFIED IDEOGRAPH
+	"89 B2	7261",	#CJK UNIFIED IDEOGRAPH
+	"89 B3	4E59",	#CJK UNIFIED IDEOGRAPH
+	"89 B4	4FFA",	#CJK UNIFIED IDEOGRAPH
+	"89 B5	5378",	#CJK UNIFIED IDEOGRAPH
+	"89 B6	6069",	#CJK UNIFIED IDEOGRAPH
+	"89 B7	6E29",	#CJK UNIFIED IDEOGRAPH
+	"89 B8	7A4F",	#CJK UNIFIED IDEOGRAPH
+	"89 B9	97F3",	#CJK UNIFIED IDEOGRAPH
+	"89 BA	4E0B",	#CJK UNIFIED IDEOGRAPH
+	"89 BB	5316",	#CJK UNIFIED IDEOGRAPH
+	"89 BC	4EEE",	#CJK UNIFIED IDEOGRAPH
+	"89 BD	4F55",	#CJK UNIFIED IDEOGRAPH
+	"89 BE	4F3D",	#CJK UNIFIED IDEOGRAPH
+	"89 BF	4FA1",	#CJK UNIFIED IDEOGRAPH
+	"89 C0	4F73",	#CJK UNIFIED IDEOGRAPH
+	"89 C1	52A0",	#CJK UNIFIED IDEOGRAPH
+	"89 C2	53EF",	#CJK UNIFIED IDEOGRAPH
+	"89 C3	5609",	#CJK UNIFIED IDEOGRAPH
+	"89 C4	590F",	#CJK UNIFIED IDEOGRAPH
+	"89 C5	5AC1",	#CJK UNIFIED IDEOGRAPH
+	"89 C6	5BB6",	#CJK UNIFIED IDEOGRAPH
+	"89 C7	5BE1",	#CJK UNIFIED IDEOGRAPH
+	"89 C8	79D1",	#CJK UNIFIED IDEOGRAPH
+	"89 C9	6687",	#CJK UNIFIED IDEOGRAPH
+	"89 CA	679C",	#CJK UNIFIED IDEOGRAPH
+	"89 CB	67B6",	#CJK UNIFIED IDEOGRAPH
+	"89 CC	6B4C",	#CJK UNIFIED IDEOGRAPH
+	"89 CD	6CB3",	#CJK UNIFIED IDEOGRAPH
+	"89 CE	706B",	#CJK UNIFIED IDEOGRAPH
+	"89 CF	73C2",	#CJK UNIFIED IDEOGRAPH
+	"89 D0	798D",	#CJK UNIFIED IDEOGRAPH
+	"89 D1	79BE",	#CJK UNIFIED IDEOGRAPH
+	"89 D2	7A3C",	#CJK UNIFIED IDEOGRAPH
+	"89 D3	7B87",	#CJK UNIFIED IDEOGRAPH
+	"89 D4	82B1",	#CJK UNIFIED IDEOGRAPH
+	"89 D5	82DB",	#CJK UNIFIED IDEOGRAPH
+	"89 D6	8304",	#CJK UNIFIED IDEOGRAPH
+	"89 D7	8377",	#CJK UNIFIED IDEOGRAPH
+	"89 D8	83EF",	#CJK UNIFIED IDEOGRAPH
+	"89 D9	83D3",	#CJK UNIFIED IDEOGRAPH
+	"89 DA	8766",	#CJK UNIFIED IDEOGRAPH
+	"89 DB	8AB2",	#CJK UNIFIED IDEOGRAPH
+	"89 DC	5629",	#CJK UNIFIED IDEOGRAPH
+	"89 DD	8CA8",	#CJK UNIFIED IDEOGRAPH
+	"89 DE	8FE6",	#CJK UNIFIED IDEOGRAPH
+	"89 DF	904E",	#CJK UNIFIED IDEOGRAPH
+	"89 E0	971E",	#CJK UNIFIED IDEOGRAPH
+	"89 E1	868A",	#CJK UNIFIED IDEOGRAPH
+	"89 E2	4FC4",	#CJK UNIFIED IDEOGRAPH
+	"89 E3	5CE8",	#CJK UNIFIED IDEOGRAPH
+	"89 E4	6211",	#CJK UNIFIED IDEOGRAPH
+	"89 E5	7259",	#CJK UNIFIED IDEOGRAPH
+	"89 E6	753B",	#CJK UNIFIED IDEOGRAPH
+	"89 E7	81E5",	#CJK UNIFIED IDEOGRAPH
+	"89 E8	82BD",	#CJK UNIFIED IDEOGRAPH
+	"89 E9	86FE",	#CJK UNIFIED IDEOGRAPH
+	"89 EA	8CC0",	#CJK UNIFIED IDEOGRAPH
+	"89 EB	96C5",	#CJK UNIFIED IDEOGRAPH
+	"89 EC	9913",	#CJK UNIFIED IDEOGRAPH
+	"89 ED	99D5",	#CJK UNIFIED IDEOGRAPH
+	"89 EE	4ECB",	#CJK UNIFIED IDEOGRAPH
+	"89 EF	4F1A",	#CJK UNIFIED IDEOGRAPH
+	"89 F0	89E3",	#CJK UNIFIED IDEOGRAPH
+	"89 F1	56DE",	#CJK UNIFIED IDEOGRAPH
+	"89 F2	584A",	#CJK UNIFIED IDEOGRAPH
+	"89 F3	58CA",	#CJK UNIFIED IDEOGRAPH
+	"89 F4	5EFB",	#CJK UNIFIED IDEOGRAPH
+	"89 F5	5FEB",	#CJK UNIFIED IDEOGRAPH
+	"89 F6	602A",	#CJK UNIFIED IDEOGRAPH
+	"89 F7	6094",	#CJK UNIFIED IDEOGRAPH
+	"89 F8	6062",	#CJK UNIFIED IDEOGRAPH
+	"89 F9	61D0",	#CJK UNIFIED IDEOGRAPH
+	"89 FA	6212",	#CJK UNIFIED IDEOGRAPH
+	"89 FB	62D0",	#CJK UNIFIED IDEOGRAPH
+	"89 FC	6539",	#CJK UNIFIED IDEOGRAPH
+	"8A 40	9B41",	#CJK UNIFIED IDEOGRAPH
+	"8A 41	6666",	#CJK UNIFIED IDEOGRAPH
+	"8A 42	68B0",	#CJK UNIFIED IDEOGRAPH
+	"8A 43	6D77",	#CJK UNIFIED IDEOGRAPH
+	"8A 44	7070",	#CJK UNIFIED IDEOGRAPH
+	"8A 45	754C",	#CJK UNIFIED IDEOGRAPH
+	"8A 46	7686",	#CJK UNIFIED IDEOGRAPH
+	"8A 47	7D75",	#CJK UNIFIED IDEOGRAPH
+	"8A 48	82A5",	#CJK UNIFIED IDEOGRAPH
+	"8A 49	87F9",	#CJK UNIFIED IDEOGRAPH
+	"8A 4A	958B",	#CJK UNIFIED IDEOGRAPH
+	"8A 4B	968E",	#CJK UNIFIED IDEOGRAPH
+	"8A 4C	8C9D",	#CJK UNIFIED IDEOGRAPH
+	"8A 4D	51F1",	#CJK UNIFIED IDEOGRAPH
+	"8A 4E	52BE",	#CJK UNIFIED IDEOGRAPH
+	"8A 4F	5916",	#CJK UNIFIED IDEOGRAPH
+	"8A 50	54B3",	#CJK UNIFIED IDEOGRAPH
+	"8A 51	5BB3",	#CJK UNIFIED IDEOGRAPH
+	"8A 52	5D16",	#CJK UNIFIED IDEOGRAPH
+	"8A 53	6168",	#CJK UNIFIED IDEOGRAPH
+	"8A 54	6982",	#CJK UNIFIED IDEOGRAPH
+	"8A 55	6DAF",	#CJK UNIFIED IDEOGRAPH
+	"8A 56	788D",	#CJK UNIFIED IDEOGRAPH
+	"8A 57	84CB",	#CJK UNIFIED IDEOGRAPH
+	"8A 58	8857",	#CJK UNIFIED IDEOGRAPH
+	"8A 59	8A72",	#CJK UNIFIED IDEOGRAPH
+	"8A 5A	93A7",	#CJK UNIFIED IDEOGRAPH
+	"8A 5B	9AB8",	#CJK UNIFIED IDEOGRAPH
+	"8A 5C	6D6C",	#CJK UNIFIED IDEOGRAPH
+	"8A 5D	99A8",	#CJK UNIFIED IDEOGRAPH
+	"8A 5E	86D9",	#CJK UNIFIED IDEOGRAPH
+	"8A 5F	57A3",	#CJK UNIFIED IDEOGRAPH
+	"8A 60	67FF",	#CJK UNIFIED IDEOGRAPH
+	"8A 61	86CE",	#CJK UNIFIED IDEOGRAPH
+	"8A 62	920E",	#CJK UNIFIED IDEOGRAPH
+	"8A 63	5283",	#CJK UNIFIED IDEOGRAPH
+	"8A 64	5687",	#CJK UNIFIED IDEOGRAPH
+	"8A 65	5404",	#CJK UNIFIED IDEOGRAPH
+	"8A 66	5ED3",	#CJK UNIFIED IDEOGRAPH
+	"8A 67	62E1",	#CJK UNIFIED IDEOGRAPH
+	"8A 68	64B9",	#CJK UNIFIED IDEOGRAPH
+	"8A 69	683C",	#CJK UNIFIED IDEOGRAPH
+	"8A 6A	6838",	#CJK UNIFIED IDEOGRAPH
+	"8A 6B	6BBB",	#CJK UNIFIED IDEOGRAPH
+	"8A 6C	7372",	#CJK UNIFIED IDEOGRAPH
+	"8A 6D	78BA",	#CJK UNIFIED IDEOGRAPH
+	"8A 6E	7A6B",	#CJK UNIFIED IDEOGRAPH
+	"8A 6F	899A",	#CJK UNIFIED IDEOGRAPH
+	"8A 70	89D2",	#CJK UNIFIED IDEOGRAPH
+	"8A 71	8D6B",	#CJK UNIFIED IDEOGRAPH
+	"8A 72	8F03",	#CJK UNIFIED IDEOGRAPH
+	"8A 73	90ED",	#CJK UNIFIED IDEOGRAPH
+	"8A 74	95A3",	#CJK UNIFIED IDEOGRAPH
+	"8A 75	9694",	#CJK UNIFIED IDEOGRAPH
+	"8A 76	9769",	#CJK UNIFIED IDEOGRAPH
+	"8A 77	5B66",	#CJK UNIFIED IDEOGRAPH
+	"8A 78	5CB3",	#CJK UNIFIED IDEOGRAPH
+	"8A 79	697D",	#CJK UNIFIED IDEOGRAPH
+	"8A 7A	984D",	#CJK UNIFIED IDEOGRAPH
+	"8A 7B	984E",	#CJK UNIFIED IDEOGRAPH
+	"8A 7C	639B",	#CJK UNIFIED IDEOGRAPH
+	"8A 7D	7B20",	#CJK UNIFIED IDEOGRAPH
+	"8A 7E	6A2B",	#CJK UNIFIED IDEOGRAPH
+	"8A 80	6A7F",	#CJK UNIFIED IDEOGRAPH
+	"8A 81	68B6",	#CJK UNIFIED IDEOGRAPH
+	"8A 82	9C0D",	#CJK UNIFIED IDEOGRAPH
+	"8A 83	6F5F",	#CJK UNIFIED IDEOGRAPH
+	"8A 84	5272",	#CJK UNIFIED IDEOGRAPH
+	"8A 85	559D",	#CJK UNIFIED IDEOGRAPH
+	"8A 86	6070",	#CJK UNIFIED IDEOGRAPH
+	"8A 87	62EC",	#CJK UNIFIED IDEOGRAPH
+	"8A 88	6D3B",	#CJK UNIFIED IDEOGRAPH
+	"8A 89	6E07",	#CJK UNIFIED IDEOGRAPH
+	"8A 8A	6ED1",	#CJK UNIFIED IDEOGRAPH
+	"8A 8B	845B",	#CJK UNIFIED IDEOGRAPH
+	"8A 8C	8910",	#CJK UNIFIED IDEOGRAPH
+	"8A 8D	8F44",	#CJK UNIFIED IDEOGRAPH
+	"8A 8E	4E14",	#CJK UNIFIED IDEOGRAPH
+	"8A 8F	9C39",	#CJK UNIFIED IDEOGRAPH
+	"8A 90	53F6",	#CJK UNIFIED IDEOGRAPH
+	"8A 91	691B",	#CJK UNIFIED IDEOGRAPH
+	"8A 92	6A3A",	#CJK UNIFIED IDEOGRAPH
+	"8A 93	9784",	#CJK UNIFIED IDEOGRAPH
+	"8A 94	682A",	#CJK UNIFIED IDEOGRAPH
+	"8A 95	515C",	#CJK UNIFIED IDEOGRAPH
+	"8A 96	7AC3",	#CJK UNIFIED IDEOGRAPH
+	"8A 97	84B2",	#CJK UNIFIED IDEOGRAPH
+	"8A 98	91DC",	#CJK UNIFIED IDEOGRAPH
+	"8A 99	938C",	#CJK UNIFIED IDEOGRAPH
+	"8A 9A	565B",	#CJK UNIFIED IDEOGRAPH
+	"8A 9B	9D28",	#CJK UNIFIED IDEOGRAPH
+	"8A 9C	6822",	#CJK UNIFIED IDEOGRAPH
+	"8A 9D	8305",	#CJK UNIFIED IDEOGRAPH
+	"8A 9E	8431",	#CJK UNIFIED IDEOGRAPH
+	"8A 9F	7CA5",	#CJK UNIFIED IDEOGRAPH
+	"8A A0	5208",	#CJK UNIFIED IDEOGRAPH
+	"8A A1	82C5",	#CJK UNIFIED IDEOGRAPH
+	"8A A2	74E6",	#CJK UNIFIED IDEOGRAPH
+	"8A A3	4E7E",	#CJK UNIFIED IDEOGRAPH
+	"8A A4	4F83",	#CJK UNIFIED IDEOGRAPH
+	"8A A5	51A0",	#CJK UNIFIED IDEOGRAPH
+	"8A A6	5BD2",	#CJK UNIFIED IDEOGRAPH
+	"8A A7	520A",	#CJK UNIFIED IDEOGRAPH
+	"8A A8	52D8",	#CJK UNIFIED IDEOGRAPH
+	"8A A9	52E7",	#CJK UNIFIED IDEOGRAPH
+	"8A AA	5DFB",	#CJK UNIFIED IDEOGRAPH
+	"8A AB	559A",	#CJK UNIFIED IDEOGRAPH
+	"8A AC	582A",	#CJK UNIFIED IDEOGRAPH
+	"8A AD	59E6",	#CJK UNIFIED IDEOGRAPH
+	"8A AE	5B8C",	#CJK UNIFIED IDEOGRAPH
+	"8A AF	5B98",	#CJK UNIFIED IDEOGRAPH
+	"8A B0	5BDB",	#CJK UNIFIED IDEOGRAPH
+	"8A B1	5E72",	#CJK UNIFIED IDEOGRAPH
+	"8A B2	5E79",	#CJK UNIFIED IDEOGRAPH
+	"8A B3	60A3",	#CJK UNIFIED IDEOGRAPH
+	"8A B4	611F",	#CJK UNIFIED IDEOGRAPH
+	"8A B5	6163",	#CJK UNIFIED IDEOGRAPH
+	"8A B6	61BE",	#CJK UNIFIED IDEOGRAPH
+	"8A B7	63DB",	#CJK UNIFIED IDEOGRAPH
+	"8A B8	6562",	#CJK UNIFIED IDEOGRAPH
+	"8A B9	67D1",	#CJK UNIFIED IDEOGRAPH
+	"8A BA	6853",	#CJK UNIFIED IDEOGRAPH
+	"8A BB	68FA",	#CJK UNIFIED IDEOGRAPH
+	"8A BC	6B3E",	#CJK UNIFIED IDEOGRAPH
+	"8A BD	6B53",	#CJK UNIFIED IDEOGRAPH
+	"8A BE	6C57",	#CJK UNIFIED IDEOGRAPH
+	"8A BF	6F22",	#CJK UNIFIED IDEOGRAPH
+	"8A C0	6F97",	#CJK UNIFIED IDEOGRAPH
+	"8A C1	6F45",	#CJK UNIFIED IDEOGRAPH
+	"8A C2	74B0",	#CJK UNIFIED IDEOGRAPH
+	"8A C3	7518",	#CJK UNIFIED IDEOGRAPH
+	"8A C4	76E3",	#CJK UNIFIED IDEOGRAPH
+	"8A C5	770B",	#CJK UNIFIED IDEOGRAPH
+	"8A C6	7AFF",	#CJK UNIFIED IDEOGRAPH
+	"8A C7	7BA1",	#CJK UNIFIED IDEOGRAPH
+	"8A C8	7C21",	#CJK UNIFIED IDEOGRAPH
+	"8A C9	7DE9",	#CJK UNIFIED IDEOGRAPH
+	"8A CA	7F36",	#CJK UNIFIED IDEOGRAPH
+	"8A CB	7FF0",	#CJK UNIFIED IDEOGRAPH
+	"8A CC	809D",	#CJK UNIFIED IDEOGRAPH
+	"8A CD	8266",	#CJK UNIFIED IDEOGRAPH
+	"8A CE	839E",	#CJK UNIFIED IDEOGRAPH
+	"8A CF	89B3",	#CJK UNIFIED IDEOGRAPH
+	"8A D0	8ACC",	#CJK UNIFIED IDEOGRAPH
+	"8A D1	8CAB",	#CJK UNIFIED IDEOGRAPH
+	"8A D2	9084",	#CJK UNIFIED IDEOGRAPH
+	"8A D3	9451",	#CJK UNIFIED IDEOGRAPH
+	"8A D4	9593",	#CJK UNIFIED IDEOGRAPH
+	"8A D5	9591",	#CJK UNIFIED IDEOGRAPH
+	"8A D6	95A2",	#CJK UNIFIED IDEOGRAPH
+	"8A D7	9665",	#CJK UNIFIED IDEOGRAPH
+	"8A D8	97D3",	#CJK UNIFIED IDEOGRAPH
+	"8A D9	9928",	#CJK UNIFIED IDEOGRAPH
+	"8A DA	8218",	#CJK UNIFIED IDEOGRAPH
+	"8A DB	4E38",	#CJK UNIFIED IDEOGRAPH
+	"8A DC	542B",	#CJK UNIFIED IDEOGRAPH
+	"8A DD	5CB8",	#CJK UNIFIED IDEOGRAPH
+	"8A DE	5DCC",	#CJK UNIFIED IDEOGRAPH
+	"8A DF	73A9",	#CJK UNIFIED IDEOGRAPH
+	"8A E0	764C",	#CJK UNIFIED IDEOGRAPH
+	"8A E1	773C",	#CJK UNIFIED IDEOGRAPH
+	"8A E2	5CA9",	#CJK UNIFIED IDEOGRAPH
+	"8A E3	7FEB",	#CJK UNIFIED IDEOGRAPH
+	"8A E4	8D0B",	#CJK UNIFIED IDEOGRAPH
+	"8A E5	96C1",	#CJK UNIFIED IDEOGRAPH
+	"8A E6	9811",	#CJK UNIFIED IDEOGRAPH
+	"8A E7	9854",	#CJK UNIFIED IDEOGRAPH
+	"8A E8	9858",	#CJK UNIFIED IDEOGRAPH
+	"8A E9	4F01",	#CJK UNIFIED IDEOGRAPH
+	"8A EA	4F0E",	#CJK UNIFIED IDEOGRAPH
+	"8A EB	5371",	#CJK UNIFIED IDEOGRAPH
+	"8A EC	559C",	#CJK UNIFIED IDEOGRAPH
+	"8A ED	5668",	#CJK UNIFIED IDEOGRAPH
+	"8A EE	57FA",	#CJK UNIFIED IDEOGRAPH
+	"8A EF	5947",	#CJK UNIFIED IDEOGRAPH
+	"8A F0	5B09",	#CJK UNIFIED IDEOGRAPH
+	"8A F1	5BC4",	#CJK UNIFIED IDEOGRAPH
+	"8A F2	5C90",	#CJK UNIFIED IDEOGRAPH
+	"8A F3	5E0C",	#CJK UNIFIED IDEOGRAPH
+	"8A F4	5E7E",	#CJK UNIFIED IDEOGRAPH
+	"8A F5	5FCC",	#CJK UNIFIED IDEOGRAPH
+	"8A F6	63EE",	#CJK UNIFIED IDEOGRAPH
+	"8A F7	673A",	#CJK UNIFIED IDEOGRAPH
+	"8A F8	65D7",	#CJK UNIFIED IDEOGRAPH
+	"8A F9	65E2",	#CJK UNIFIED IDEOGRAPH
+	"8A FA	671F",	#CJK UNIFIED IDEOGRAPH
+	"8A FB	68CB",	#CJK UNIFIED IDEOGRAPH
+	"8A FC	68C4",	#CJK UNIFIED IDEOGRAPH
+	"8B 40	6A5F",	#CJK UNIFIED IDEOGRAPH
+	"8B 41	5E30",	#CJK UNIFIED IDEOGRAPH
+	"8B 42	6BC5",	#CJK UNIFIED IDEOGRAPH
+	"8B 43	6C17",	#CJK UNIFIED IDEOGRAPH
+	"8B 44	6C7D",	#CJK UNIFIED IDEOGRAPH
+	"8B 45	757F",	#CJK UNIFIED IDEOGRAPH
+	"8B 46	7948",	#CJK UNIFIED IDEOGRAPH
+	"8B 47	5B63",	#CJK UNIFIED IDEOGRAPH
+	"8B 48	7A00",	#CJK UNIFIED IDEOGRAPH
+	"8B 49	7D00",	#CJK UNIFIED IDEOGRAPH
+	"8B 4A	5FBD",	#CJK UNIFIED IDEOGRAPH
+	"8B 4B	898F",	#CJK UNIFIED IDEOGRAPH
+	"8B 4C	8A18",	#CJK UNIFIED IDEOGRAPH
+	"8B 4D	8CB4",	#CJK UNIFIED IDEOGRAPH
+	"8B 4E	8D77",	#CJK UNIFIED IDEOGRAPH
+	"8B 4F	8ECC",	#CJK UNIFIED IDEOGRAPH
+	"8B 50	8F1D",	#CJK UNIFIED IDEOGRAPH
+	"8B 51	98E2",	#CJK UNIFIED IDEOGRAPH
+	"8B 52	9A0E",	#CJK UNIFIED IDEOGRAPH
+	"8B 53	9B3C",	#CJK UNIFIED IDEOGRAPH
+	"8B 54	4E80",	#CJK UNIFIED IDEOGRAPH
+	"8B 55	507D",	#CJK UNIFIED IDEOGRAPH
+	"8B 56	5100",	#CJK UNIFIED IDEOGRAPH
+	"8B 57	5993",	#CJK UNIFIED IDEOGRAPH
+	"8B 58	5B9C",	#CJK UNIFIED IDEOGRAPH
+	"8B 59	622F",	#CJK UNIFIED IDEOGRAPH
+	"8B 5A	6280",	#CJK UNIFIED IDEOGRAPH
+	"8B 5B	64EC",	#CJK UNIFIED IDEOGRAPH
+	"8B 5C	6B3A",	#CJK UNIFIED IDEOGRAPH
+	"8B 5D	72A0",	#CJK UNIFIED IDEOGRAPH
+	"8B 5E	7591",	#CJK UNIFIED IDEOGRAPH
+	"8B 5F	7947",	#CJK UNIFIED IDEOGRAPH
+	"8B 60	7FA9",	#CJK UNIFIED IDEOGRAPH
+	"8B 61	87FB",	#CJK UNIFIED IDEOGRAPH
+	"8B 62	8ABC",	#CJK UNIFIED IDEOGRAPH
+	"8B 63	8B70",	#CJK UNIFIED IDEOGRAPH
+	"8B 64	63AC",	#CJK UNIFIED IDEOGRAPH
+	"8B 65	83CA",	#CJK UNIFIED IDEOGRAPH
+	"8B 66	97A0",	#CJK UNIFIED IDEOGRAPH
+	"8B 67	5409",	#CJK UNIFIED IDEOGRAPH
+	"8B 68	5403",	#CJK UNIFIED IDEOGRAPH
+	"8B 69	55AB",	#CJK UNIFIED IDEOGRAPH
+	"8B 6A	6854",	#CJK UNIFIED IDEOGRAPH
+	"8B 6B	6A58",	#CJK UNIFIED IDEOGRAPH
+	"8B 6C	8A70",	#CJK UNIFIED IDEOGRAPH
+	"8B 6D	7827",	#CJK UNIFIED IDEOGRAPH
+	"8B 6E	6775",	#CJK UNIFIED IDEOGRAPH
+	"8B 6F	9ECD",	#CJK UNIFIED IDEOGRAPH
+	"8B 70	5374",	#CJK UNIFIED IDEOGRAPH
+	"8B 71	5BA2",	#CJK UNIFIED IDEOGRAPH
+	"8B 72	811A",	#CJK UNIFIED IDEOGRAPH
+	"8B 73	8650",	#CJK UNIFIED IDEOGRAPH
+	"8B 74	9006",	#CJK UNIFIED IDEOGRAPH
+	"8B 75	4E18",	#CJK UNIFIED IDEOGRAPH
+	"8B 76	4E45",	#CJK UNIFIED IDEOGRAPH
+	"8B 77	4EC7",	#CJK UNIFIED IDEOGRAPH
+	"8B 78	4F11",	#CJK UNIFIED IDEOGRAPH
+	"8B 79	53CA",	#CJK UNIFIED IDEOGRAPH
+	"8B 7A	5438",	#CJK UNIFIED IDEOGRAPH
+	"8B 7B	5BAE",	#CJK UNIFIED IDEOGRAPH
+	"8B 7C	5F13",	#CJK UNIFIED IDEOGRAPH
+	"8B 7D	6025",	#CJK UNIFIED IDEOGRAPH
+	"8B 7E	6551",	#CJK UNIFIED IDEOGRAPH
+	"8B 80	673D",	#CJK UNIFIED IDEOGRAPH
+	"8B 81	6C42",	#CJK UNIFIED IDEOGRAPH
+	"8B 82	6C72",	#CJK UNIFIED IDEOGRAPH
+	"8B 83	6CE3",	#CJK UNIFIED IDEOGRAPH
+	"8B 84	7078",	#CJK UNIFIED IDEOGRAPH
+	"8B 85	7403",	#CJK UNIFIED IDEOGRAPH
+	"8B 86	7A76",	#CJK UNIFIED IDEOGRAPH
+	"8B 87	7AAE",	#CJK UNIFIED IDEOGRAPH
+	"8B 88	7B08",	#CJK UNIFIED IDEOGRAPH
+	"8B 89	7D1A",	#CJK UNIFIED IDEOGRAPH
+	"8B 8A	7CFE",	#CJK UNIFIED IDEOGRAPH
+	"8B 8B	7D66",	#CJK UNIFIED IDEOGRAPH
+	"8B 8C	65E7",	#CJK UNIFIED IDEOGRAPH
+	"8B 8D	725B",	#CJK UNIFIED IDEOGRAPH
+	"8B 8E	53BB",	#CJK UNIFIED IDEOGRAPH
+	"8B 8F	5C45",	#CJK UNIFIED IDEOGRAPH
+	"8B 90	5DE8",	#CJK UNIFIED IDEOGRAPH
+	"8B 91	62D2",	#CJK UNIFIED IDEOGRAPH
+	"8B 92	62E0",	#CJK UNIFIED IDEOGRAPH
+	"8B 93	6319",	#CJK UNIFIED IDEOGRAPH
+	"8B 94	6E20",	#CJK UNIFIED IDEOGRAPH
+	"8B 95	865A",	#CJK UNIFIED IDEOGRAPH
+	"8B 96	8A31",	#CJK UNIFIED IDEOGRAPH
+	"8B 97	8DDD",	#CJK UNIFIED IDEOGRAPH
+	"8B 98	92F8",	#CJK UNIFIED IDEOGRAPH
+	"8B 99	6F01",	#CJK UNIFIED IDEOGRAPH
+	"8B 9A	79A6",	#CJK UNIFIED IDEOGRAPH
+	"8B 9B	9B5A",	#CJK UNIFIED IDEOGRAPH
+	"8B 9C	4EA8",	#CJK UNIFIED IDEOGRAPH
+	"8B 9D	4EAB",	#CJK UNIFIED IDEOGRAPH
+	"8B 9E	4EAC",	#CJK UNIFIED IDEOGRAPH
+	"8B 9F	4F9B",	#CJK UNIFIED IDEOGRAPH
+	"8B A0	4FA0",	#CJK UNIFIED IDEOGRAPH
+	"8B A1	50D1",	#CJK UNIFIED IDEOGRAPH
+	"8B A2	5147",	#CJK UNIFIED IDEOGRAPH
+	"8B A3	7AF6",	#CJK UNIFIED IDEOGRAPH
+	"8B A4	5171",	#CJK UNIFIED IDEOGRAPH
+	"8B A5	51F6",	#CJK UNIFIED IDEOGRAPH
+	"8B A6	5354",	#CJK UNIFIED IDEOGRAPH
+	"8B A7	5321",	#CJK UNIFIED IDEOGRAPH
+	"8B A8	537F",	#CJK UNIFIED IDEOGRAPH
+	"8B A9	53EB",	#CJK UNIFIED IDEOGRAPH
+	"8B AA	55AC",	#CJK UNIFIED IDEOGRAPH
+	"8B AB	5883",	#CJK UNIFIED IDEOGRAPH
+	"8B AC	5CE1",	#CJK UNIFIED IDEOGRAPH
+	"8B AD	5F37",	#CJK UNIFIED IDEOGRAPH
+	"8B AE	5F4A",	#CJK UNIFIED IDEOGRAPH
+	"8B AF	602F",	#CJK UNIFIED IDEOGRAPH
+	"8B B0	6050",	#CJK UNIFIED IDEOGRAPH
+	"8B B1	606D",	#CJK UNIFIED IDEOGRAPH
+	"8B B2	631F",	#CJK UNIFIED IDEOGRAPH
+	"8B B3	6559",	#CJK UNIFIED IDEOGRAPH
+	"8B B4	6A4B",	#CJK UNIFIED IDEOGRAPH
+	"8B B5	6CC1",	#CJK UNIFIED IDEOGRAPH
+	"8B B6	72C2",	#CJK UNIFIED IDEOGRAPH
+	"8B B7	72ED",	#CJK UNIFIED IDEOGRAPH
+	"8B B8	77EF",	#CJK UNIFIED IDEOGRAPH
+	"8B B9	80F8",	#CJK UNIFIED IDEOGRAPH
+	"8B BA	8105",	#CJK UNIFIED IDEOGRAPH
+	"8B BB	8208",	#CJK UNIFIED IDEOGRAPH
+	"8B BC	854E",	#CJK UNIFIED IDEOGRAPH
+	"8B BD	90F7",	#CJK UNIFIED IDEOGRAPH
+	"8B BE	93E1",	#CJK UNIFIED IDEOGRAPH
+	"8B BF	97FF",	#CJK UNIFIED IDEOGRAPH
+	"8B C0	9957",	#CJK UNIFIED IDEOGRAPH
+	"8B C1	9A5A",	#CJK UNIFIED IDEOGRAPH
+	"8B C2	4EF0",	#CJK UNIFIED IDEOGRAPH
+	"8B C3	51DD",	#CJK UNIFIED IDEOGRAPH
+	"8B C4	5C2D",	#CJK UNIFIED IDEOGRAPH
+	"8B C5	6681",	#CJK UNIFIED IDEOGRAPH
+	"8B C6	696D",	#CJK UNIFIED IDEOGRAPH
+	"8B C7	5C40",	#CJK UNIFIED IDEOGRAPH
+	"8B C8	66F2",	#CJK UNIFIED IDEOGRAPH
+	"8B C9	6975",	#CJK UNIFIED IDEOGRAPH
+	"8B CA	7389",	#CJK UNIFIED IDEOGRAPH
+	"8B CB	6850",	#CJK UNIFIED IDEOGRAPH
+	"8B CC	7C81",	#CJK UNIFIED IDEOGRAPH
+	"8B CD	50C5",	#CJK UNIFIED IDEOGRAPH
+	"8B CE	52E4",	#CJK UNIFIED IDEOGRAPH
+	"8B CF	5747",	#CJK UNIFIED IDEOGRAPH
+	"8B D0	5DFE",	#CJK UNIFIED IDEOGRAPH
+	"8B D1	9326",	#CJK UNIFIED IDEOGRAPH
+	"8B D2	65A4",	#CJK UNIFIED IDEOGRAPH
+	"8B D3	6B23",	#CJK UNIFIED IDEOGRAPH
+	"8B D4	6B3D",	#CJK UNIFIED IDEOGRAPH
+	"8B D5	7434",	#CJK UNIFIED IDEOGRAPH
+	"8B D6	7981",	#CJK UNIFIED IDEOGRAPH
+	"8B D7	79BD",	#CJK UNIFIED IDEOGRAPH
+	"8B D8	7B4B",	#CJK UNIFIED IDEOGRAPH
+	"8B D9	7DCA",	#CJK UNIFIED IDEOGRAPH
+	"8B DA	82B9",	#CJK UNIFIED IDEOGRAPH
+	"8B DB	83CC",	#CJK UNIFIED IDEOGRAPH
+	"8B DC	887F",	#CJK UNIFIED IDEOGRAPH
+	"8B DD	895F",	#CJK UNIFIED IDEOGRAPH
+	"8B DE	8B39",	#CJK UNIFIED IDEOGRAPH
+	"8B DF	8FD1",	#CJK UNIFIED IDEOGRAPH
+	"8B E0	91D1",	#CJK UNIFIED IDEOGRAPH
+	"8B E1	541F",	#CJK UNIFIED IDEOGRAPH
+	"8B E2	9280",	#CJK UNIFIED IDEOGRAPH
+	"8B E3	4E5D",	#CJK UNIFIED IDEOGRAPH
+	"8B E4	5036",	#CJK UNIFIED IDEOGRAPH
+	"8B E5	53E5",	#CJK UNIFIED IDEOGRAPH
+	"8B E6	533A",	#CJK UNIFIED IDEOGRAPH
+	"8B E7	72D7",	#CJK UNIFIED IDEOGRAPH
+	"8B E8	7396",	#CJK UNIFIED IDEOGRAPH
+	"8B E9	77E9",	#CJK UNIFIED IDEOGRAPH
+	"8B EA	82E6",	#CJK UNIFIED IDEOGRAPH
+	"8B EB	8EAF",	#CJK UNIFIED IDEOGRAPH
+	"8B EC	99C6",	#CJK UNIFIED IDEOGRAPH
+	"8B ED	99C8",	#CJK UNIFIED IDEOGRAPH
+	"8B EE	99D2",	#CJK UNIFIED IDEOGRAPH
+	"8B EF	5177",	#CJK UNIFIED IDEOGRAPH
+	"8B F0	611A",	#CJK UNIFIED IDEOGRAPH
+	"8B F1	865E",	#CJK UNIFIED IDEOGRAPH
+	"8B F2	55B0",	#CJK UNIFIED IDEOGRAPH
+	"8B F3	7A7A",	#CJK UNIFIED IDEOGRAPH
+	"8B F4	5076",	#CJK UNIFIED IDEOGRAPH
+	"8B F5	5BD3",	#CJK UNIFIED IDEOGRAPH
+	"8B F6	9047",	#CJK UNIFIED IDEOGRAPH
+	"8B F7	9685",	#CJK UNIFIED IDEOGRAPH
+	"8B F8	4E32",	#CJK UNIFIED IDEOGRAPH
+	"8B F9	6ADB",	#CJK UNIFIED IDEOGRAPH
+	"8B FA	91E7",	#CJK UNIFIED IDEOGRAPH
+	"8B FB	5C51",	#CJK UNIFIED IDEOGRAPH
+	"8B FC	5C48",	#CJK UNIFIED IDEOGRAPH
+	"8C 40	6398",	#CJK UNIFIED IDEOGRAPH
+	"8C 41	7A9F",	#CJK UNIFIED IDEOGRAPH
+	"8C 42	6C93",	#CJK UNIFIED IDEOGRAPH
+	"8C 43	9774",	#CJK UNIFIED IDEOGRAPH
+	"8C 44	8F61",	#CJK UNIFIED IDEOGRAPH
+	"8C 45	7AAA",	#CJK UNIFIED IDEOGRAPH
+	"8C 46	718A",	#CJK UNIFIED IDEOGRAPH
+	"8C 47	9688",	#CJK UNIFIED IDEOGRAPH
+	"8C 48	7C82",	#CJK UNIFIED IDEOGRAPH
+	"8C 49	6817",	#CJK UNIFIED IDEOGRAPH
+	"8C 4A	7E70",	#CJK UNIFIED IDEOGRAPH
+	"8C 4B	6851",	#CJK UNIFIED IDEOGRAPH
+	"8C 4C	936C",	#CJK UNIFIED IDEOGRAPH
+	"8C 4D	52F2",	#CJK UNIFIED IDEOGRAPH
+	"8C 4E	541B",	#CJK UNIFIED IDEOGRAPH
+	"8C 4F	85AB",	#CJK UNIFIED IDEOGRAPH
+	"8C 50	8A13",	#CJK UNIFIED IDEOGRAPH
+	"8C 51	7FA4",	#CJK UNIFIED IDEOGRAPH
+	"8C 52	8ECD",	#CJK UNIFIED IDEOGRAPH
+	"8C 53	90E1",	#CJK UNIFIED IDEOGRAPH
+	"8C 54	5366",	#CJK UNIFIED IDEOGRAPH
+	"8C 55	8888",	#CJK UNIFIED IDEOGRAPH
+	"8C 56	7941",	#CJK UNIFIED IDEOGRAPH
+	"8C 57	4FC2",	#CJK UNIFIED IDEOGRAPH
+	"8C 58	50BE",	#CJK UNIFIED IDEOGRAPH
+	"8C 59	5211",	#CJK UNIFIED IDEOGRAPH
+	"8C 5A	5144",	#CJK UNIFIED IDEOGRAPH
+	"8C 5B	5553",	#CJK UNIFIED IDEOGRAPH
+	"8C 5C	572D",	#CJK UNIFIED IDEOGRAPH
+	"8C 5D	73EA",	#CJK UNIFIED IDEOGRAPH
+	"8C 5E	578B",	#CJK UNIFIED IDEOGRAPH
+	"8C 5F	5951",	#CJK UNIFIED IDEOGRAPH
+	"8C 60	5F62",	#CJK UNIFIED IDEOGRAPH
+	"8C 61	5F84",	#CJK UNIFIED IDEOGRAPH
+	"8C 62	6075",	#CJK UNIFIED IDEOGRAPH
+	"8C 63	6176",	#CJK UNIFIED IDEOGRAPH
+	"8C 64	6167",	#CJK UNIFIED IDEOGRAPH
+	"8C 65	61A9",	#CJK UNIFIED IDEOGRAPH
+	"8C 66	63B2",	#CJK UNIFIED IDEOGRAPH
+	"8C 67	643A",	#CJK UNIFIED IDEOGRAPH
+	"8C 68	656C",	#CJK UNIFIED IDEOGRAPH
+	"8C 69	666F",	#CJK UNIFIED IDEOGRAPH
+	"8C 6A	6842",	#CJK UNIFIED IDEOGRAPH
+	"8C 6B	6E13",	#CJK UNIFIED IDEOGRAPH
+	"8C 6C	7566",	#CJK UNIFIED IDEOGRAPH
+	"8C 6D	7A3D",	#CJK UNIFIED IDEOGRAPH
+	"8C 6E	7CFB",	#CJK UNIFIED IDEOGRAPH
+	"8C 6F	7D4C",	#CJK UNIFIED IDEOGRAPH
+	"8C 70	7D99",	#CJK UNIFIED IDEOGRAPH
+	"8C 71	7E4B",	#CJK UNIFIED IDEOGRAPH
+	"8C 72	7F6B",	#CJK UNIFIED IDEOGRAPH
+	"8C 73	830E",	#CJK UNIFIED IDEOGRAPH
+	"8C 74	834A",	#CJK UNIFIED IDEOGRAPH
+	"8C 75	86CD",	#CJK UNIFIED IDEOGRAPH
+	"8C 76	8A08",	#CJK UNIFIED IDEOGRAPH
+	"8C 77	8A63",	#CJK UNIFIED IDEOGRAPH
+	"8C 78	8B66",	#CJK UNIFIED IDEOGRAPH
+	"8C 79	8EFD",	#CJK UNIFIED IDEOGRAPH
+	"8C 7A	981A",	#CJK UNIFIED IDEOGRAPH
+	"8C 7B	9D8F",	#CJK UNIFIED IDEOGRAPH
+	"8C 7C	82B8",	#CJK UNIFIED IDEOGRAPH
+	"8C 7D	8FCE",	#CJK UNIFIED IDEOGRAPH
+	"8C 7E	9BE8",	#CJK UNIFIED IDEOGRAPH
+	"8C 80	5287",	#CJK UNIFIED IDEOGRAPH
+	"8C 81	621F",	#CJK UNIFIED IDEOGRAPH
+	"8C 82	6483",	#CJK UNIFIED IDEOGRAPH
+	"8C 83	6FC0",	#CJK UNIFIED IDEOGRAPH
+	"8C 84	9699",	#CJK UNIFIED IDEOGRAPH
+	"8C 85	6841",	#CJK UNIFIED IDEOGRAPH
+	"8C 86	5091",	#CJK UNIFIED IDEOGRAPH
+	"8C 87	6B20",	#CJK UNIFIED IDEOGRAPH
+	"8C 88	6C7A",	#CJK UNIFIED IDEOGRAPH
+	"8C 89	6F54",	#CJK UNIFIED IDEOGRAPH
+	"8C 8A	7A74",	#CJK UNIFIED IDEOGRAPH
+	"8C 8B	7D50",	#CJK UNIFIED IDEOGRAPH
+	"8C 8C	8840",	#CJK UNIFIED IDEOGRAPH
+	"8C 8D	8A23",	#CJK UNIFIED IDEOGRAPH
+	"8C 8E	6708",	#CJK UNIFIED IDEOGRAPH
+	"8C 8F	4EF6",	#CJK UNIFIED IDEOGRAPH
+	"8C 90	5039",	#CJK UNIFIED IDEOGRAPH
+	"8C 91	5026",	#CJK UNIFIED IDEOGRAPH
+	"8C 92	5065",	#CJK UNIFIED IDEOGRAPH
+	"8C 93	517C",	#CJK UNIFIED IDEOGRAPH
+	"8C 94	5238",	#CJK UNIFIED IDEOGRAPH
+	"8C 95	5263",	#CJK UNIFIED IDEOGRAPH
+	"8C 96	55A7",	#CJK UNIFIED IDEOGRAPH
+	"8C 97	570F",	#CJK UNIFIED IDEOGRAPH
+	"8C 98	5805",	#CJK UNIFIED IDEOGRAPH
+	"8C 99	5ACC",	#CJK UNIFIED IDEOGRAPH
+	"8C 9A	5EFA",	#CJK UNIFIED IDEOGRAPH
+	"8C 9B	61B2",	#CJK UNIFIED IDEOGRAPH
+	"8C 9C	61F8",	#CJK UNIFIED IDEOGRAPH
+	"8C 9D	62F3",	#CJK UNIFIED IDEOGRAPH
+	"8C 9E	6372",	#CJK UNIFIED IDEOGRAPH
+	"8C 9F	691C",	#CJK UNIFIED IDEOGRAPH
+	"8C A0	6A29",	#CJK UNIFIED IDEOGRAPH
+	"8C A1	727D",	#CJK UNIFIED IDEOGRAPH
+	"8C A2	72AC",	#CJK UNIFIED IDEOGRAPH
+	"8C A3	732E",	#CJK UNIFIED IDEOGRAPH
+	"8C A4	7814",	#CJK UNIFIED IDEOGRAPH
+	"8C A5	786F",	#CJK UNIFIED IDEOGRAPH
+	"8C A6	7D79",	#CJK UNIFIED IDEOGRAPH
+	"8C A7	770C",	#CJK UNIFIED IDEOGRAPH
+	"8C A8	80A9",	#CJK UNIFIED IDEOGRAPH
+	"8C A9	898B",	#CJK UNIFIED IDEOGRAPH
+	"8C AA	8B19",	#CJK UNIFIED IDEOGRAPH
+	"8C AB	8CE2",	#CJK UNIFIED IDEOGRAPH
+	"8C AC	8ED2",	#CJK UNIFIED IDEOGRAPH
+	"8C AD	9063",	#CJK UNIFIED IDEOGRAPH
+	"8C AE	9375",	#CJK UNIFIED IDEOGRAPH
+	"8C AF	967A",	#CJK UNIFIED IDEOGRAPH
+	"8C B0	9855",	#CJK UNIFIED IDEOGRAPH
+	"8C B1	9A13",	#CJK UNIFIED IDEOGRAPH
+	"8C B2	9E78",	#CJK UNIFIED IDEOGRAPH
+	"8C B3	5143",	#CJK UNIFIED IDEOGRAPH
+	"8C B4	539F",	#CJK UNIFIED IDEOGRAPH
+	"8C B5	53B3",	#CJK UNIFIED IDEOGRAPH
+	"8C B6	5E7B",	#CJK UNIFIED IDEOGRAPH
+	"8C B7	5F26",	#CJK UNIFIED IDEOGRAPH
+	"8C B8	6E1B",	#CJK UNIFIED IDEOGRAPH
+	"8C B9	6E90",	#CJK UNIFIED IDEOGRAPH
+	"8C BA	7384",	#CJK UNIFIED IDEOGRAPH
+	"8C BB	73FE",	#CJK UNIFIED IDEOGRAPH
+	"8C BC	7D43",	#CJK UNIFIED IDEOGRAPH
+	"8C BD	8237",	#CJK UNIFIED IDEOGRAPH
+	"8C BE	8A00",	#CJK UNIFIED IDEOGRAPH
+	"8C BF	8AFA",	#CJK UNIFIED IDEOGRAPH
+	"8C C0	9650",	#CJK UNIFIED IDEOGRAPH
+	"8C C1	4E4E",	#CJK UNIFIED IDEOGRAPH
+	"8C C2	500B",	#CJK UNIFIED IDEOGRAPH
+	"8C C3	53E4",	#CJK UNIFIED IDEOGRAPH
+	"8C C4	547C",	#CJK UNIFIED IDEOGRAPH
+	"8C C5	56FA",	#CJK UNIFIED IDEOGRAPH
+	"8C C6	59D1",	#CJK UNIFIED IDEOGRAPH
+	"8C C7	5B64",	#CJK UNIFIED IDEOGRAPH
+	"8C C8	5DF1",	#CJK UNIFIED IDEOGRAPH
+	"8C C9	5EAB",	#CJK UNIFIED IDEOGRAPH
+	"8C CA	5F27",	#CJK UNIFIED IDEOGRAPH
+	"8C CB	6238",	#CJK UNIFIED IDEOGRAPH
+	"8C CC	6545",	#CJK UNIFIED IDEOGRAPH
+	"8C CD	67AF",	#CJK UNIFIED IDEOGRAPH
+	"8C CE	6E56",	#CJK UNIFIED IDEOGRAPH
+	"8C CF	72D0",	#CJK UNIFIED IDEOGRAPH
+	"8C D0	7CCA",	#CJK UNIFIED IDEOGRAPH
+	"8C D1	88B4",	#CJK UNIFIED IDEOGRAPH
+	"8C D2	80A1",	#CJK UNIFIED IDEOGRAPH
+	"8C D3	80E1",	#CJK UNIFIED IDEOGRAPH
+	"8C D4	83F0",	#CJK UNIFIED IDEOGRAPH
+	"8C D5	864E",	#CJK UNIFIED IDEOGRAPH
+	"8C D6	8A87",	#CJK UNIFIED IDEOGRAPH
+	"8C D7	8DE8",	#CJK UNIFIED IDEOGRAPH
+	"8C D8	9237",	#CJK UNIFIED IDEOGRAPH
+	"8C D9	96C7",	#CJK UNIFIED IDEOGRAPH
+	"8C DA	9867",	#CJK UNIFIED IDEOGRAPH
+	"8C DB	9F13",	#CJK UNIFIED IDEOGRAPH
+	"8C DC	4E94",	#CJK UNIFIED IDEOGRAPH
+	"8C DD	4E92",	#CJK UNIFIED IDEOGRAPH
+	"8C DE	4F0D",	#CJK UNIFIED IDEOGRAPH
+	"8C DF	5348",	#CJK UNIFIED IDEOGRAPH
+	"8C E0	5449",	#CJK UNIFIED IDEOGRAPH
+	"8C E1	543E",	#CJK UNIFIED IDEOGRAPH
+	"8C E2	5A2F",	#CJK UNIFIED IDEOGRAPH
+	"8C E3	5F8C",	#CJK UNIFIED IDEOGRAPH
+	"8C E4	5FA1",	#CJK UNIFIED IDEOGRAPH
+	"8C E5	609F",	#CJK UNIFIED IDEOGRAPH
+	"8C E6	68A7",	#CJK UNIFIED IDEOGRAPH
+	"8C E7	6A8E",	#CJK UNIFIED IDEOGRAPH
+	"8C E8	745A",	#CJK UNIFIED IDEOGRAPH
+	"8C E9	7881",	#CJK UNIFIED IDEOGRAPH
+	"8C EA	8A9E",	#CJK UNIFIED IDEOGRAPH
+	"8C EB	8AA4",	#CJK UNIFIED IDEOGRAPH
+	"8C EC	8B77",	#CJK UNIFIED IDEOGRAPH
+	"8C ED	9190",	#CJK UNIFIED IDEOGRAPH
+	"8C EE	4E5E",	#CJK UNIFIED IDEOGRAPH
+	"8C EF	9BC9",	#CJK UNIFIED IDEOGRAPH
+	"8C F0	4EA4",	#CJK UNIFIED IDEOGRAPH
+	"8C F1	4F7C",	#CJK UNIFIED IDEOGRAPH
+	"8C F2	4FAF",	#CJK UNIFIED IDEOGRAPH
+	"8C F3	5019",	#CJK UNIFIED IDEOGRAPH
+	"8C F4	5016",	#CJK UNIFIED IDEOGRAPH
+	"8C F5	5149",	#CJK UNIFIED IDEOGRAPH
+	"8C F6	516C",	#CJK UNIFIED IDEOGRAPH
+	"8C F7	529F",	#CJK UNIFIED IDEOGRAPH
+	"8C F8	52B9",	#CJK UNIFIED IDEOGRAPH
+	"8C F9	52FE",	#CJK UNIFIED IDEOGRAPH
+	"8C FA	539A",	#CJK UNIFIED IDEOGRAPH
+	"8C FB	53E3",	#CJK UNIFIED IDEOGRAPH
+	"8C FC	5411",	#CJK UNIFIED IDEOGRAPH
+	"8D 40	540E",	#CJK UNIFIED IDEOGRAPH
+	"8D 41	5589",	#CJK UNIFIED IDEOGRAPH
+	"8D 42	5751",	#CJK UNIFIED IDEOGRAPH
+	"8D 43	57A2",	#CJK UNIFIED IDEOGRAPH
+	"8D 44	597D",	#CJK UNIFIED IDEOGRAPH
+	"8D 45	5B54",	#CJK UNIFIED IDEOGRAPH
+	"8D 46	5B5D",	#CJK UNIFIED IDEOGRAPH
+	"8D 47	5B8F",	#CJK UNIFIED IDEOGRAPH
+	"8D 48	5DE5",	#CJK UNIFIED IDEOGRAPH
+	"8D 49	5DE7",	#CJK UNIFIED IDEOGRAPH
+	"8D 4A	5DF7",	#CJK UNIFIED IDEOGRAPH
+	"8D 4B	5E78",	#CJK UNIFIED IDEOGRAPH
+	"8D 4C	5E83",	#CJK UNIFIED IDEOGRAPH
+	"8D 4D	5E9A",	#CJK UNIFIED IDEOGRAPH
+	"8D 4E	5EB7",	#CJK UNIFIED IDEOGRAPH
+	"8D 4F	5F18",	#CJK UNIFIED IDEOGRAPH
+	"8D 50	6052",	#CJK UNIFIED IDEOGRAPH
+	"8D 51	614C",	#CJK UNIFIED IDEOGRAPH
+	"8D 52	6297",	#CJK UNIFIED IDEOGRAPH
+	"8D 53	62D8",	#CJK UNIFIED IDEOGRAPH
+	"8D 54	63A7",	#CJK UNIFIED IDEOGRAPH
+	"8D 55	653B",	#CJK UNIFIED IDEOGRAPH
+	"8D 56	6602",	#CJK UNIFIED IDEOGRAPH
+	"8D 57	6643",	#CJK UNIFIED IDEOGRAPH
+	"8D 58	66F4",	#CJK UNIFIED IDEOGRAPH
+	"8D 59	676D",	#CJK UNIFIED IDEOGRAPH
+	"8D 5A	6821",	#CJK UNIFIED IDEOGRAPH
+	"8D 5B	6897",	#CJK UNIFIED IDEOGRAPH
+	"8D 5C	69CB",	#CJK UNIFIED IDEOGRAPH
+	"8D 5D	6C5F",	#CJK UNIFIED IDEOGRAPH
+	"8D 5E	6D2A",	#CJK UNIFIED IDEOGRAPH
+	"8D 5F	6D69",	#CJK UNIFIED IDEOGRAPH
+	"8D 60	6E2F",	#CJK UNIFIED IDEOGRAPH
+	"8D 61	6E9D",	#CJK UNIFIED IDEOGRAPH
+	"8D 62	7532",	#CJK UNIFIED IDEOGRAPH
+	"8D 63	7687",	#CJK UNIFIED IDEOGRAPH
+	"8D 64	786C",	#CJK UNIFIED IDEOGRAPH
+	"8D 65	7A3F",	#CJK UNIFIED IDEOGRAPH
+	"8D 66	7CE0",	#CJK UNIFIED IDEOGRAPH
+	"8D 67	7D05",	#CJK UNIFIED IDEOGRAPH
+	"8D 68	7D18",	#CJK UNIFIED IDEOGRAPH
+	"8D 69	7D5E",	#CJK UNIFIED IDEOGRAPH
+	"8D 6A	7DB1",	#CJK UNIFIED IDEOGRAPH
+	"8D 6B	8015",	#CJK UNIFIED IDEOGRAPH
+	"8D 6C	8003",	#CJK UNIFIED IDEOGRAPH
+	"8D 6D	80AF",	#CJK UNIFIED IDEOGRAPH
+	"8D 6E	80B1",	#CJK UNIFIED IDEOGRAPH
+	"8D 6F	8154",	#CJK UNIFIED IDEOGRAPH
+	"8D 70	818F",	#CJK UNIFIED IDEOGRAPH
+	"8D 71	822A",	#CJK UNIFIED IDEOGRAPH
+	"8D 72	8352",	#CJK UNIFIED IDEOGRAPH
+	"8D 73	884C",	#CJK UNIFIED IDEOGRAPH
+	"8D 74	8861",	#CJK UNIFIED IDEOGRAPH
+	"8D 75	8B1B",	#CJK UNIFIED IDEOGRAPH
+	"8D 76	8CA2",	#CJK UNIFIED IDEOGRAPH
+	"8D 77	8CFC",	#CJK UNIFIED IDEOGRAPH
+	"8D 78	90CA",	#CJK UNIFIED IDEOGRAPH
+	"8D 79	9175",	#CJK UNIFIED IDEOGRAPH
+	"8D 7A	9271",	#CJK UNIFIED IDEOGRAPH
+	"8D 7B	783F",	#CJK UNIFIED IDEOGRAPH
+	"8D 7C	92FC",	#CJK UNIFIED IDEOGRAPH
+	"8D 7D	95A4",	#CJK UNIFIED IDEOGRAPH
+	"8D 7E	964D",	#CJK UNIFIED IDEOGRAPH
+	"8D 80	9805",	#CJK UNIFIED IDEOGRAPH
+	"8D 81	9999",	#CJK UNIFIED IDEOGRAPH
+	"8D 82	9AD8",	#CJK UNIFIED IDEOGRAPH
+	"8D 83	9D3B",	#CJK UNIFIED IDEOGRAPH
+	"8D 84	525B",	#CJK UNIFIED IDEOGRAPH
+	"8D 85	52AB",	#CJK UNIFIED IDEOGRAPH
+	"8D 86	53F7",	#CJK UNIFIED IDEOGRAPH
+	"8D 87	5408",	#CJK UNIFIED IDEOGRAPH
+	"8D 88	58D5",	#CJK UNIFIED IDEOGRAPH
+	"8D 89	62F7",	#CJK UNIFIED IDEOGRAPH
+	"8D 8A	6FE0",	#CJK UNIFIED IDEOGRAPH
+	"8D 8B	8C6A",	#CJK UNIFIED IDEOGRAPH
+	"8D 8C	8F5F",	#CJK UNIFIED IDEOGRAPH
+	"8D 8D	9EB9",	#CJK UNIFIED IDEOGRAPH
+	"8D 8E	514B",	#CJK UNIFIED IDEOGRAPH
+	"8D 8F	523B",	#CJK UNIFIED IDEOGRAPH
+	"8D 90	544A",	#CJK UNIFIED IDEOGRAPH
+	"8D 91	56FD",	#CJK UNIFIED IDEOGRAPH
+	"8D 92	7A40",	#CJK UNIFIED IDEOGRAPH
+	"8D 93	9177",	#CJK UNIFIED IDEOGRAPH
+	"8D 94	9D60",	#CJK UNIFIED IDEOGRAPH
+	"8D 95	9ED2",	#CJK UNIFIED IDEOGRAPH
+	"8D 96	7344",	#CJK UNIFIED IDEOGRAPH
+	"8D 97	6F09",	#CJK UNIFIED IDEOGRAPH
+	"8D 98	8170",	#CJK UNIFIED IDEOGRAPH
+	"8D 99	7511",	#CJK UNIFIED IDEOGRAPH
+	"8D 9A	5FFD",	#CJK UNIFIED IDEOGRAPH
+	"8D 9B	60DA",	#CJK UNIFIED IDEOGRAPH
+	"8D 9C	9AA8",	#CJK UNIFIED IDEOGRAPH
+	"8D 9D	72DB",	#CJK UNIFIED IDEOGRAPH
+	"8D 9E	8FBC",	#CJK UNIFIED IDEOGRAPH
+	"8D 9F	6B64",	#CJK UNIFIED IDEOGRAPH
+	"8D A0	9803",	#CJK UNIFIED IDEOGRAPH
+	"8D A1	4ECA",	#CJK UNIFIED IDEOGRAPH
+	"8D A2	56F0",	#CJK UNIFIED IDEOGRAPH
+	"8D A3	5764",	#CJK UNIFIED IDEOGRAPH
+	"8D A4	58BE",	#CJK UNIFIED IDEOGRAPH
+	"8D A5	5A5A",	#CJK UNIFIED IDEOGRAPH
+	"8D A6	6068",	#CJK UNIFIED IDEOGRAPH
+	"8D A7	61C7",	#CJK UNIFIED IDEOGRAPH
+	"8D A8	660F",	#CJK UNIFIED IDEOGRAPH
+	"8D A9	6606",	#CJK UNIFIED IDEOGRAPH
+	"8D AA	6839",	#CJK UNIFIED IDEOGRAPH
+	"8D AB	68B1",	#CJK UNIFIED IDEOGRAPH
+	"8D AC	6DF7",	#CJK UNIFIED IDEOGRAPH
+	"8D AD	75D5",	#CJK UNIFIED IDEOGRAPH
+	"8D AE	7D3A",	#CJK UNIFIED IDEOGRAPH
+	"8D AF	826E",	#CJK UNIFIED IDEOGRAPH
+	"8D B0	9B42",	#CJK UNIFIED IDEOGRAPH
+	"8D B1	4E9B",	#CJK UNIFIED IDEOGRAPH
+	"8D B2	4F50",	#CJK UNIFIED IDEOGRAPH
+	"8D B3	53C9",	#CJK UNIFIED IDEOGRAPH
+	"8D B4	5506",	#CJK UNIFIED IDEOGRAPH
+	"8D B5	5D6F",	#CJK UNIFIED IDEOGRAPH
+	"8D B6	5DE6",	#CJK UNIFIED IDEOGRAPH
+	"8D B7	5DEE",	#CJK UNIFIED IDEOGRAPH
+	"8D B8	67FB",	#CJK UNIFIED IDEOGRAPH
+	"8D B9	6C99",	#CJK UNIFIED IDEOGRAPH
+	"8D BA	7473",	#CJK UNIFIED IDEOGRAPH
+	"8D BB	7802",	#CJK UNIFIED IDEOGRAPH
+	"8D BC	8A50",	#CJK UNIFIED IDEOGRAPH
+	"8D BD	9396",	#CJK UNIFIED IDEOGRAPH
+	"8D BE	88DF",	#CJK UNIFIED IDEOGRAPH
+	"8D BF	5750",	#CJK UNIFIED IDEOGRAPH
+	"8D C0	5EA7",	#CJK UNIFIED IDEOGRAPH
+	"8D C1	632B",	#CJK UNIFIED IDEOGRAPH
+	"8D C2	50B5",	#CJK UNIFIED IDEOGRAPH
+	"8D C3	50AC",	#CJK UNIFIED IDEOGRAPH
+	"8D C4	518D",	#CJK UNIFIED IDEOGRAPH
+	"8D C5	6700",	#CJK UNIFIED IDEOGRAPH
+	"8D C6	54C9",	#CJK UNIFIED IDEOGRAPH
+	"8D C7	585E",	#CJK UNIFIED IDEOGRAPH
+	"8D C8	59BB",	#CJK UNIFIED IDEOGRAPH
+	"8D C9	5BB0",	#CJK UNIFIED IDEOGRAPH
+	"8D CA	5F69",	#CJK UNIFIED IDEOGRAPH
+	"8D CB	624D",	#CJK UNIFIED IDEOGRAPH
+	"8D CC	63A1",	#CJK UNIFIED IDEOGRAPH
+	"8D CD	683D",	#CJK UNIFIED IDEOGRAPH
+	"8D CE	6B73",	#CJK UNIFIED IDEOGRAPH
+	"8D CF	6E08",	#CJK UNIFIED IDEOGRAPH
+	"8D D0	707D",	#CJK UNIFIED IDEOGRAPH
+	"8D D1	91C7",	#CJK UNIFIED IDEOGRAPH
+	"8D D2	7280",	#CJK UNIFIED IDEOGRAPH
+	"8D D3	7815",	#CJK UNIFIED IDEOGRAPH
+	"8D D4	7826",	#CJK UNIFIED IDEOGRAPH
+	"8D D5	796D",	#CJK UNIFIED IDEOGRAPH
+	"8D D6	658E",	#CJK UNIFIED IDEOGRAPH
+	"8D D7	7D30",	#CJK UNIFIED IDEOGRAPH
+	"8D D8	83DC",	#CJK UNIFIED IDEOGRAPH
+	"8D D9	88C1",	#CJK UNIFIED IDEOGRAPH
+	"8D DA	8F09",	#CJK UNIFIED IDEOGRAPH
+	"8D DB	969B",	#CJK UNIFIED IDEOGRAPH
+	"8D DC	5264",	#CJK UNIFIED IDEOGRAPH
+	"8D DD	5728",	#CJK UNIFIED IDEOGRAPH
+	"8D DE	6750",	#CJK UNIFIED IDEOGRAPH
+	"8D DF	7F6A",	#CJK UNIFIED IDEOGRAPH
+	"8D E0	8CA1",	#CJK UNIFIED IDEOGRAPH
+	"8D E1	51B4",	#CJK UNIFIED IDEOGRAPH
+	"8D E2	5742",	#CJK UNIFIED IDEOGRAPH
+	"8D E3	962A",	#CJK UNIFIED IDEOGRAPH
+	"8D E4	583A",	#CJK UNIFIED IDEOGRAPH
+	"8D E5	698A",	#CJK UNIFIED IDEOGRAPH
+	"8D E6	80B4",	#CJK UNIFIED IDEOGRAPH
+	"8D E7	54B2",	#CJK UNIFIED IDEOGRAPH
+	"8D E8	5D0E",	#CJK UNIFIED IDEOGRAPH
+	"8D E9	57FC",	#CJK UNIFIED IDEOGRAPH
+	"8D EA	7895",	#CJK UNIFIED IDEOGRAPH
+	"8D EB	9DFA",	#CJK UNIFIED IDEOGRAPH
+	"8D EC	4F5C",	#CJK UNIFIED IDEOGRAPH
+	"8D ED	524A",	#CJK UNIFIED IDEOGRAPH
+	"8D EE	548B",	#CJK UNIFIED IDEOGRAPH
+	"8D EF	643E",	#CJK UNIFIED IDEOGRAPH
+	"8D F0	6628",	#CJK UNIFIED IDEOGRAPH
+	"8D F1	6714",	#CJK UNIFIED IDEOGRAPH
+	"8D F2	67F5",	#CJK UNIFIED IDEOGRAPH
+	"8D F3	7A84",	#CJK UNIFIED IDEOGRAPH
+	"8D F4	7B56",	#CJK UNIFIED IDEOGRAPH
+	"8D F5	7D22",	#CJK UNIFIED IDEOGRAPH
+	"8D F6	932F",	#CJK UNIFIED IDEOGRAPH
+	"8D F7	685C",	#CJK UNIFIED IDEOGRAPH
+	"8D F8	9BAD",	#CJK UNIFIED IDEOGRAPH
+	"8D F9	7B39",	#CJK UNIFIED IDEOGRAPH
+	"8D FA	5319",	#CJK UNIFIED IDEOGRAPH
+	"8D FB	518A",	#CJK UNIFIED IDEOGRAPH
+	"8D FC	5237",	#CJK UNIFIED IDEOGRAPH
+	"8E 40	5BDF",	#CJK UNIFIED IDEOGRAPH
+	"8E 41	62F6",	#CJK UNIFIED IDEOGRAPH
+	"8E 42	64AE",	#CJK UNIFIED IDEOGRAPH
+	"8E 43	64E6",	#CJK UNIFIED IDEOGRAPH
+	"8E 44	672D",	#CJK UNIFIED IDEOGRAPH
+	"8E 45	6BBA",	#CJK UNIFIED IDEOGRAPH
+	"8E 46	85A9",	#CJK UNIFIED IDEOGRAPH
+	"8E 47	96D1",	#CJK UNIFIED IDEOGRAPH
+	"8E 48	7690",	#CJK UNIFIED IDEOGRAPH
+	"8E 49	9BD6",	#CJK UNIFIED IDEOGRAPH
+	"8E 4A	634C",	#CJK UNIFIED IDEOGRAPH
+	"8E 4B	9306",	#CJK UNIFIED IDEOGRAPH
+	"8E 4C	9BAB",	#CJK UNIFIED IDEOGRAPH
+	"8E 4D	76BF",	#CJK UNIFIED IDEOGRAPH
+	"8E 4E	6652",	#CJK UNIFIED IDEOGRAPH
+	"8E 4F	4E09",	#CJK UNIFIED IDEOGRAPH
+	"8E 50	5098",	#CJK UNIFIED IDEOGRAPH
+	"8E 51	53C2",	#CJK UNIFIED IDEOGRAPH
+	"8E 52	5C71",	#CJK UNIFIED IDEOGRAPH
+	"8E 53	60E8",	#CJK UNIFIED IDEOGRAPH
+	"8E 54	6492",	#CJK UNIFIED IDEOGRAPH
+	"8E 55	6563",	#CJK UNIFIED IDEOGRAPH
+	"8E 56	685F",	#CJK UNIFIED IDEOGRAPH
+	"8E 57	71E6",	#CJK UNIFIED IDEOGRAPH
+	"8E 58	73CA",	#CJK UNIFIED IDEOGRAPH
+	"8E 59	7523",	#CJK UNIFIED IDEOGRAPH
+	"8E 5A	7B97",	#CJK UNIFIED IDEOGRAPH
+	"8E 5B	7E82",	#CJK UNIFIED IDEOGRAPH
+	"8E 5C	8695",	#CJK UNIFIED IDEOGRAPH
+	"8E 5D	8B83",	#CJK UNIFIED IDEOGRAPH
+	"8E 5E	8CDB",	#CJK UNIFIED IDEOGRAPH
+	"8E 5F	9178",	#CJK UNIFIED IDEOGRAPH
+	"8E 60	9910",	#CJK UNIFIED IDEOGRAPH
+	"8E 61	65AC",	#CJK UNIFIED IDEOGRAPH
+	"8E 62	66AB",	#CJK UNIFIED IDEOGRAPH
+	"8E 63	6B8B",	#CJK UNIFIED IDEOGRAPH
+	"8E 64	4ED5",	#CJK UNIFIED IDEOGRAPH
+	"8E 65	4ED4",	#CJK UNIFIED IDEOGRAPH
+	"8E 66	4F3A",	#CJK UNIFIED IDEOGRAPH
+	"8E 67	4F7F",	#CJK UNIFIED IDEOGRAPH
+	"8E 68	523A",	#CJK UNIFIED IDEOGRAPH
+	"8E 69	53F8",	#CJK UNIFIED IDEOGRAPH
+	"8E 6A	53F2",	#CJK UNIFIED IDEOGRAPH
+	"8E 6B	55E3",	#CJK UNIFIED IDEOGRAPH
+	"8E 6C	56DB",	#CJK UNIFIED IDEOGRAPH
+	"8E 6D	58EB",	#CJK UNIFIED IDEOGRAPH
+	"8E 6E	59CB",	#CJK UNIFIED IDEOGRAPH
+	"8E 6F	59C9",	#CJK UNIFIED IDEOGRAPH
+	"8E 70	59FF",	#CJK UNIFIED IDEOGRAPH
+	"8E 71	5B50",	#CJK UNIFIED IDEOGRAPH
+	"8E 72	5C4D",	#CJK UNIFIED IDEOGRAPH
+	"8E 73	5E02",	#CJK UNIFIED IDEOGRAPH
+	"8E 74	5E2B",	#CJK UNIFIED IDEOGRAPH
+	"8E 75	5FD7",	#CJK UNIFIED IDEOGRAPH
+	"8E 76	601D",	#CJK UNIFIED IDEOGRAPH
+	"8E 77	6307",	#CJK UNIFIED IDEOGRAPH
+	"8E 78	652F",	#CJK UNIFIED IDEOGRAPH
+	"8E 79	5B5C",	#CJK UNIFIED IDEOGRAPH
+	"8E 7A	65AF",	#CJK UNIFIED IDEOGRAPH
+	"8E 7B	65BD",	#CJK UNIFIED IDEOGRAPH
+	"8E 7C	65E8",	#CJK UNIFIED IDEOGRAPH
+	"8E 7D	679D",	#CJK UNIFIED IDEOGRAPH
+	"8E 7E	6B62",	#CJK UNIFIED IDEOGRAPH
+	"8E 80	6B7B",	#CJK UNIFIED IDEOGRAPH
+	"8E 81	6C0F",	#CJK UNIFIED IDEOGRAPH
+	"8E 82	7345",	#CJK UNIFIED IDEOGRAPH
+	"8E 83	7949",	#CJK UNIFIED IDEOGRAPH
+	"8E 84	79C1",	#CJK UNIFIED IDEOGRAPH
+	"8E 85	7CF8",	#CJK UNIFIED IDEOGRAPH
+	"8E 86	7D19",	#CJK UNIFIED IDEOGRAPH
+	"8E 87	7D2B",	#CJK UNIFIED IDEOGRAPH
+	"8E 88	80A2",	#CJK UNIFIED IDEOGRAPH
+	"8E 89	8102",	#CJK UNIFIED IDEOGRAPH
+	"8E 8A	81F3",	#CJK UNIFIED IDEOGRAPH
+	"8E 8B	8996",	#CJK UNIFIED IDEOGRAPH
+	"8E 8C	8A5E",	#CJK UNIFIED IDEOGRAPH
+	"8E 8D	8A69",	#CJK UNIFIED IDEOGRAPH
+	"8E 8E	8A66",	#CJK UNIFIED IDEOGRAPH
+	"8E 8F	8A8C",	#CJK UNIFIED IDEOGRAPH
+	"8E 90	8AEE",	#CJK UNIFIED IDEOGRAPH
+	"8E 91	8CC7",	#CJK UNIFIED IDEOGRAPH
+	"8E 92	8CDC",	#CJK UNIFIED IDEOGRAPH
+	"8E 93	96CC",	#CJK UNIFIED IDEOGRAPH
+	"8E 94	98FC",	#CJK UNIFIED IDEOGRAPH
+	"8E 95	6B6F",	#CJK UNIFIED IDEOGRAPH
+	"8E 96	4E8B",	#CJK UNIFIED IDEOGRAPH
+	"8E 97	4F3C",	#CJK UNIFIED IDEOGRAPH
+	"8E 98	4F8D",	#CJK UNIFIED IDEOGRAPH
+	"8E 99	5150",	#CJK UNIFIED IDEOGRAPH
+	"8E 9A	5B57",	#CJK UNIFIED IDEOGRAPH
+	"8E 9B	5BFA",	#CJK UNIFIED IDEOGRAPH
+	"8E 9C	6148",	#CJK UNIFIED IDEOGRAPH
+	"8E 9D	6301",	#CJK UNIFIED IDEOGRAPH
+	"8E 9E	6642",	#CJK UNIFIED IDEOGRAPH
+	"8E 9F	6B21",	#CJK UNIFIED IDEOGRAPH
+	"8E A0	6ECB",	#CJK UNIFIED IDEOGRAPH
+	"8E A1	6CBB",	#CJK UNIFIED IDEOGRAPH
+	"8E A2	723E",	#CJK UNIFIED IDEOGRAPH
+	"8E A3	74BD",	#CJK UNIFIED IDEOGRAPH
+	"8E A4	75D4",	#CJK UNIFIED IDEOGRAPH
+	"8E A5	78C1",	#CJK UNIFIED IDEOGRAPH
+	"8E A6	793A",	#CJK UNIFIED IDEOGRAPH
+	"8E A7	800C",	#CJK UNIFIED IDEOGRAPH
+	"8E A8	8033",	#CJK UNIFIED IDEOGRAPH
+	"8E A9	81EA",	#CJK UNIFIED IDEOGRAPH
+	"8E AA	8494",	#CJK UNIFIED IDEOGRAPH
+	"8E AB	8F9E",	#CJK UNIFIED IDEOGRAPH
+	"8E AC	6C50",	#CJK UNIFIED IDEOGRAPH
+	"8E AD	9E7F",	#CJK UNIFIED IDEOGRAPH
+	"8E AE	5F0F",	#CJK UNIFIED IDEOGRAPH
+	"8E AF	8B58",	#CJK UNIFIED IDEOGRAPH
+	"8E B0	9D2B",	#CJK UNIFIED IDEOGRAPH
+	"8E B1	7AFA",	#CJK UNIFIED IDEOGRAPH
+	"8E B2	8EF8",	#CJK UNIFIED IDEOGRAPH
+	"8E B3	5B8D",	#CJK UNIFIED IDEOGRAPH
+	"8E B4	96EB",	#CJK UNIFIED IDEOGRAPH
+	"8E B5	4E03",	#CJK UNIFIED IDEOGRAPH
+	"8E B6	53F1",	#CJK UNIFIED IDEOGRAPH
+	"8E B7	57F7",	#CJK UNIFIED IDEOGRAPH
+	"8E B8	5931",	#CJK UNIFIED IDEOGRAPH
+	"8E B9	5AC9",	#CJK UNIFIED IDEOGRAPH
+	"8E BA	5BA4",	#CJK UNIFIED IDEOGRAPH
+	"8E BB	6089",	#CJK UNIFIED IDEOGRAPH
+	"8E BC	6E7F",	#CJK UNIFIED IDEOGRAPH
+	"8E BD	6F06",	#CJK UNIFIED IDEOGRAPH
+	"8E BE	75BE",	#CJK UNIFIED IDEOGRAPH
+	"8E BF	8CEA",	#CJK UNIFIED IDEOGRAPH
+	"8E C0	5B9F",	#CJK UNIFIED IDEOGRAPH
+	"8E C1	8500",	#CJK UNIFIED IDEOGRAPH
+	"8E C2	7BE0",	#CJK UNIFIED IDEOGRAPH
+	"8E C3	5072",	#CJK UNIFIED IDEOGRAPH
+	"8E C4	67F4",	#CJK UNIFIED IDEOGRAPH
+	"8E C5	829D",	#CJK UNIFIED IDEOGRAPH
+	"8E C6	5C61",	#CJK UNIFIED IDEOGRAPH
+	"8E C7	854A",	#CJK UNIFIED IDEOGRAPH
+	"8E C8	7E1E",	#CJK UNIFIED IDEOGRAPH
+	"8E C9	820E",	#CJK UNIFIED IDEOGRAPH
+	"8E CA	5199",	#CJK UNIFIED IDEOGRAPH
+	"8E CB	5C04",	#CJK UNIFIED IDEOGRAPH
+	"8E CC	6368",	#CJK UNIFIED IDEOGRAPH
+	"8E CD	8D66",	#CJK UNIFIED IDEOGRAPH
+	"8E CE	659C",	#CJK UNIFIED IDEOGRAPH
+	"8E CF	716E",	#CJK UNIFIED IDEOGRAPH
+	"8E D0	793E",	#CJK UNIFIED IDEOGRAPH
+	"8E D1	7D17",	#CJK UNIFIED IDEOGRAPH
+	"8E D2	8005",	#CJK UNIFIED IDEOGRAPH
+	"8E D3	8B1D",	#CJK UNIFIED IDEOGRAPH
+	"8E D4	8ECA",	#CJK UNIFIED IDEOGRAPH
+	"8E D5	906E",	#CJK UNIFIED IDEOGRAPH
+	"8E D6	86C7",	#CJK UNIFIED IDEOGRAPH
+	"8E D7	90AA",	#CJK UNIFIED IDEOGRAPH
+	"8E D8	501F",	#CJK UNIFIED IDEOGRAPH
+	"8E D9	52FA",	#CJK UNIFIED IDEOGRAPH
+	"8E DA	5C3A",	#CJK UNIFIED IDEOGRAPH
+	"8E DB	6753",	#CJK UNIFIED IDEOGRAPH
+	"8E DC	707C",	#CJK UNIFIED IDEOGRAPH
+	"8E DD	7235",	#CJK UNIFIED IDEOGRAPH
+	"8E DE	914C",	#CJK UNIFIED IDEOGRAPH
+	"8E DF	91C8",	#CJK UNIFIED IDEOGRAPH
+	"8E E0	932B",	#CJK UNIFIED IDEOGRAPH
+	"8E E1	82E5",	#CJK UNIFIED IDEOGRAPH
+	"8E E2	5BC2",	#CJK UNIFIED IDEOGRAPH
+	"8E E3	5F31",	#CJK UNIFIED IDEOGRAPH
+	"8E E4	60F9",	#CJK UNIFIED IDEOGRAPH
+	"8E E5	4E3B",	#CJK UNIFIED IDEOGRAPH
+	"8E E6	53D6",	#CJK UNIFIED IDEOGRAPH
+	"8E E7	5B88",	#CJK UNIFIED IDEOGRAPH
+	"8E E8	624B",	#CJK UNIFIED IDEOGRAPH
+	"8E E9	6731",	#CJK UNIFIED IDEOGRAPH
+	"8E EA	6B8A",	#CJK UNIFIED IDEOGRAPH
+	"8E EB	72E9",	#CJK UNIFIED IDEOGRAPH
+	"8E EC	73E0",	#CJK UNIFIED IDEOGRAPH
+	"8E ED	7A2E",	#CJK UNIFIED IDEOGRAPH
+	"8E EE	816B",	#CJK UNIFIED IDEOGRAPH
+	"8E EF	8DA3",	#CJK UNIFIED IDEOGRAPH
+	"8E F0	9152",	#CJK UNIFIED IDEOGRAPH
+	"8E F1	9996",	#CJK UNIFIED IDEOGRAPH
+	"8E F2	5112",	#CJK UNIFIED IDEOGRAPH
+	"8E F3	53D7",	#CJK UNIFIED IDEOGRAPH
+	"8E F4	546A",	#CJK UNIFIED IDEOGRAPH
+	"8E F5	5BFF",	#CJK UNIFIED IDEOGRAPH
+	"8E F6	6388",	#CJK UNIFIED IDEOGRAPH
+	"8E F7	6A39",	#CJK UNIFIED IDEOGRAPH
+	"8E F8	7DAC",	#CJK UNIFIED IDEOGRAPH
+	"8E F9	9700",	#CJK UNIFIED IDEOGRAPH
+	"8E FA	56DA",	#CJK UNIFIED IDEOGRAPH
+	"8E FB	53CE",	#CJK UNIFIED IDEOGRAPH
+	"8E FC	5468",	#CJK UNIFIED IDEOGRAPH
+	"8F 40	5B97",	#CJK UNIFIED IDEOGRAPH
+	"8F 41	5C31",	#CJK UNIFIED IDEOGRAPH
+	"8F 42	5DDE",	#CJK UNIFIED IDEOGRAPH
+	"8F 43	4FEE",	#CJK UNIFIED IDEOGRAPH
+	"8F 44	6101",	#CJK UNIFIED IDEOGRAPH
+	"8F 45	62FE",	#CJK UNIFIED IDEOGRAPH
+	"8F 46	6D32",	#CJK UNIFIED IDEOGRAPH
+	"8F 47	79C0",	#CJK UNIFIED IDEOGRAPH
+	"8F 48	79CB",	#CJK UNIFIED IDEOGRAPH
+	"8F 49	7D42",	#CJK UNIFIED IDEOGRAPH
+	"8F 4A	7E4D",	#CJK UNIFIED IDEOGRAPH
+	"8F 4B	7FD2",	#CJK UNIFIED IDEOGRAPH
+	"8F 4C	81ED",	#CJK UNIFIED IDEOGRAPH
+	"8F 4D	821F",	#CJK UNIFIED IDEOGRAPH
+	"8F 4E	8490",	#CJK UNIFIED IDEOGRAPH
+	"8F 4F	8846",	#CJK UNIFIED IDEOGRAPH
+	"8F 50	8972",	#CJK UNIFIED IDEOGRAPH
+	"8F 51	8B90",	#CJK UNIFIED IDEOGRAPH
+	"8F 52	8E74",	#CJK UNIFIED IDEOGRAPH
+	"8F 53	8F2F",	#CJK UNIFIED IDEOGRAPH
+	"8F 54	9031",	#CJK UNIFIED IDEOGRAPH
+	"8F 55	914B",	#CJK UNIFIED IDEOGRAPH
+	"8F 56	916C",	#CJK UNIFIED IDEOGRAPH
+	"8F 57	96C6",	#CJK UNIFIED IDEOGRAPH
+	"8F 58	919C",	#CJK UNIFIED IDEOGRAPH
+	"8F 59	4EC0",	#CJK UNIFIED IDEOGRAPH
+	"8F 5A	4F4F",	#CJK UNIFIED IDEOGRAPH
+	"8F 5B	5145",	#CJK UNIFIED IDEOGRAPH
+	"8F 5C	5341",	#CJK UNIFIED IDEOGRAPH
+	"8F 5D	5F93",	#CJK UNIFIED IDEOGRAPH
+	"8F 5E	620E",	#CJK UNIFIED IDEOGRAPH
+	"8F 5F	67D4",	#CJK UNIFIED IDEOGRAPH
+	"8F 60	6C41",	#CJK UNIFIED IDEOGRAPH
+	"8F 61	6E0B",	#CJK UNIFIED IDEOGRAPH
+	"8F 62	7363",	#CJK UNIFIED IDEOGRAPH
+	"8F 63	7E26",	#CJK UNIFIED IDEOGRAPH
+	"8F 64	91CD",	#CJK UNIFIED IDEOGRAPH
+	"8F 65	9283",	#CJK UNIFIED IDEOGRAPH
+	"8F 66	53D4",	#CJK UNIFIED IDEOGRAPH
+	"8F 67	5919",	#CJK UNIFIED IDEOGRAPH
+	"8F 68	5BBF",	#CJK UNIFIED IDEOGRAPH
+	"8F 69	6DD1",	#CJK UNIFIED IDEOGRAPH
+	"8F 6A	795D",	#CJK UNIFIED IDEOGRAPH
+	"8F 6B	7E2E",	#CJK UNIFIED IDEOGRAPH
+	"8F 6C	7C9B",	#CJK UNIFIED IDEOGRAPH
+	"8F 6D	587E",	#CJK UNIFIED IDEOGRAPH
+	"8F 6E	719F",	#CJK UNIFIED IDEOGRAPH
+	"8F 6F	51FA",	#CJK UNIFIED IDEOGRAPH
+	"8F 70	8853",	#CJK UNIFIED IDEOGRAPH
+	"8F 71	8FF0",	#CJK UNIFIED IDEOGRAPH
+	"8F 72	4FCA",	#CJK UNIFIED IDEOGRAPH
+	"8F 73	5CFB",	#CJK UNIFIED IDEOGRAPH
+	"8F 74	6625",	#CJK UNIFIED IDEOGRAPH
+	"8F 75	77AC",	#CJK UNIFIED IDEOGRAPH
+	"8F 76	7AE3",	#CJK UNIFIED IDEOGRAPH
+	"8F 77	821C",	#CJK UNIFIED IDEOGRAPH
+	"8F 78	99FF",	#CJK UNIFIED IDEOGRAPH
+	"8F 79	51C6",	#CJK UNIFIED IDEOGRAPH
+	"8F 7A	5FAA",	#CJK UNIFIED IDEOGRAPH
+	"8F 7B	65EC",	#CJK UNIFIED IDEOGRAPH
+	"8F 7C	696F",	#CJK UNIFIED IDEOGRAPH
+	"8F 7D	6B89",	#CJK UNIFIED IDEOGRAPH
+	"8F 7E	6DF3",	#CJK UNIFIED IDEOGRAPH
+	"8F 80	6E96",	#CJK UNIFIED IDEOGRAPH
+	"8F 81	6F64",	#CJK UNIFIED IDEOGRAPH
+	"8F 82	76FE",	#CJK UNIFIED IDEOGRAPH
+	"8F 83	7D14",	#CJK UNIFIED IDEOGRAPH
+	"8F 84	5DE1",	#CJK UNIFIED IDEOGRAPH
+	"8F 85	9075",	#CJK UNIFIED IDEOGRAPH
+	"8F 86	9187",	#CJK UNIFIED IDEOGRAPH
+	"8F 87	9806",	#CJK UNIFIED IDEOGRAPH
+	"8F 88	51E6",	#CJK UNIFIED IDEOGRAPH
+	"8F 89	521D",	#CJK UNIFIED IDEOGRAPH
+	"8F 8A	6240",	#CJK UNIFIED IDEOGRAPH
+	"8F 8B	6691",	#CJK UNIFIED IDEOGRAPH
+	"8F 8C	66D9",	#CJK UNIFIED IDEOGRAPH
+	"8F 8D	6E1A",	#CJK UNIFIED IDEOGRAPH
+	"8F 8E	5EB6",	#CJK UNIFIED IDEOGRAPH
+	"8F 8F	7DD2",	#CJK UNIFIED IDEOGRAPH
+	"8F 90	7F72",	#CJK UNIFIED IDEOGRAPH
+	"8F 91	66F8",	#CJK UNIFIED IDEOGRAPH
+	"8F 92	85AF",	#CJK UNIFIED IDEOGRAPH
+	"8F 93	85F7",	#CJK UNIFIED IDEOGRAPH
+	"8F 94	8AF8",	#CJK UNIFIED IDEOGRAPH
+	"8F 95	52A9",	#CJK UNIFIED IDEOGRAPH
+	"8F 96	53D9",	#CJK UNIFIED IDEOGRAPH
+	"8F 97	5973",	#CJK UNIFIED IDEOGRAPH
+	"8F 98	5E8F",	#CJK UNIFIED IDEOGRAPH
+	"8F 99	5F90",	#CJK UNIFIED IDEOGRAPH
+	"8F 9A	6055",	#CJK UNIFIED IDEOGRAPH
+	"8F 9B	92E4",	#CJK UNIFIED IDEOGRAPH
+	"8F 9C	9664",	#CJK UNIFIED IDEOGRAPH
+	"8F 9D	50B7",	#CJK UNIFIED IDEOGRAPH
+	"8F 9E	511F",	#CJK UNIFIED IDEOGRAPH
+	"8F 9F	52DD",	#CJK UNIFIED IDEOGRAPH
+	"8F A0	5320",	#CJK UNIFIED IDEOGRAPH
+	"8F A1	5347",	#CJK UNIFIED IDEOGRAPH
+	"8F A2	53EC",	#CJK UNIFIED IDEOGRAPH
+	"8F A3	54E8",	#CJK UNIFIED IDEOGRAPH
+	"8F A4	5546",	#CJK UNIFIED IDEOGRAPH
+	"8F A5	5531",	#CJK UNIFIED IDEOGRAPH
+	"8F A6	5617",	#CJK UNIFIED IDEOGRAPH
+	"8F A7	5968",	#CJK UNIFIED IDEOGRAPH
+	"8F A8	59BE",	#CJK UNIFIED IDEOGRAPH
+	"8F A9	5A3C",	#CJK UNIFIED IDEOGRAPH
+	"8F AA	5BB5",	#CJK UNIFIED IDEOGRAPH
+	"8F AB	5C06",	#CJK UNIFIED IDEOGRAPH
+	"8F AC	5C0F",	#CJK UNIFIED IDEOGRAPH
+	"8F AD	5C11",	#CJK UNIFIED IDEOGRAPH
+	"8F AE	5C1A",	#CJK UNIFIED IDEOGRAPH
+	"8F AF	5E84",	#CJK UNIFIED IDEOGRAPH
+	"8F B0	5E8A",	#CJK UNIFIED IDEOGRAPH
+	"8F B1	5EE0",	#CJK UNIFIED IDEOGRAPH
+	"8F B2	5F70",	#CJK UNIFIED IDEOGRAPH
+	"8F B3	627F",	#CJK UNIFIED IDEOGRAPH
+	"8F B4	6284",	#CJK UNIFIED IDEOGRAPH
+	"8F B5	62DB",	#CJK UNIFIED IDEOGRAPH
+	"8F B6	638C",	#CJK UNIFIED IDEOGRAPH
+	"8F B7	6377",	#CJK UNIFIED IDEOGRAPH
+	"8F B8	6607",	#CJK UNIFIED IDEOGRAPH
+	"8F B9	660C",	#CJK UNIFIED IDEOGRAPH
+	"8F BA	662D",	#CJK UNIFIED IDEOGRAPH
+	"8F BB	6676",	#CJK UNIFIED IDEOGRAPH
+	"8F BC	677E",	#CJK UNIFIED IDEOGRAPH
+	"8F BD	68A2",	#CJK UNIFIED IDEOGRAPH
+	"8F BE	6A1F",	#CJK UNIFIED IDEOGRAPH
+	"8F BF	6A35",	#CJK UNIFIED IDEOGRAPH
+	"8F C0	6CBC",	#CJK UNIFIED IDEOGRAPH
+	"8F C1	6D88",	#CJK UNIFIED IDEOGRAPH
+	"8F C2	6E09",	#CJK UNIFIED IDEOGRAPH
+	"8F C3	6E58",	#CJK UNIFIED IDEOGRAPH
+	"8F C4	713C",	#CJK UNIFIED IDEOGRAPH
+	"8F C5	7126",	#CJK UNIFIED IDEOGRAPH
+	"8F C6	7167",	#CJK UNIFIED IDEOGRAPH
+	"8F C7	75C7",	#CJK UNIFIED IDEOGRAPH
+	"8F C8	7701",	#CJK UNIFIED IDEOGRAPH
+	"8F C9	785D",	#CJK UNIFIED IDEOGRAPH
+	"8F CA	7901",	#CJK UNIFIED IDEOGRAPH
+	"8F CB	7965",	#CJK UNIFIED IDEOGRAPH
+	"8F CC	79F0",	#CJK UNIFIED IDEOGRAPH
+	"8F CD	7AE0",	#CJK UNIFIED IDEOGRAPH
+	"8F CE	7B11",	#CJK UNIFIED IDEOGRAPH
+	"8F CF	7CA7",	#CJK UNIFIED IDEOGRAPH
+	"8F D0	7D39",	#CJK UNIFIED IDEOGRAPH
+	"8F D1	8096",	#CJK UNIFIED IDEOGRAPH
+	"8F D2	83D6",	#CJK UNIFIED IDEOGRAPH
+	"8F D3	848B",	#CJK UNIFIED IDEOGRAPH
+	"8F D4	8549",	#CJK UNIFIED IDEOGRAPH
+	"8F D5	885D",	#CJK UNIFIED IDEOGRAPH
+	"8F D6	88F3",	#CJK UNIFIED IDEOGRAPH
+	"8F D7	8A1F",	#CJK UNIFIED IDEOGRAPH
+	"8F D8	8A3C",	#CJK UNIFIED IDEOGRAPH
+	"8F D9	8A54",	#CJK UNIFIED IDEOGRAPH
+	"8F DA	8A73",	#CJK UNIFIED IDEOGRAPH
+	"8F DB	8C61",	#CJK UNIFIED IDEOGRAPH
+	"8F DC	8CDE",	#CJK UNIFIED IDEOGRAPH
+	"8F DD	91A4",	#CJK UNIFIED IDEOGRAPH
+	"8F DE	9266",	#CJK UNIFIED IDEOGRAPH
+	"8F DF	937E",	#CJK UNIFIED IDEOGRAPH
+	"8F E0	9418",	#CJK UNIFIED IDEOGRAPH
+	"8F E1	969C",	#CJK UNIFIED IDEOGRAPH
+	"8F E2	9798",	#CJK UNIFIED IDEOGRAPH
+	"8F E3	4E0A",	#CJK UNIFIED IDEOGRAPH
+	"8F E4	4E08",	#CJK UNIFIED IDEOGRAPH
+	"8F E5	4E1E",	#CJK UNIFIED IDEOGRAPH
+	"8F E6	4E57",	#CJK UNIFIED IDEOGRAPH
+	"8F E7	5197",	#CJK UNIFIED IDEOGRAPH
+	"8F E8	5270",	#CJK UNIFIED IDEOGRAPH
+	"8F E9	57CE",	#CJK UNIFIED IDEOGRAPH
+	"8F EA	5834",	#CJK UNIFIED IDEOGRAPH
+	"8F EB	58CC",	#CJK UNIFIED IDEOGRAPH
+	"8F EC	5B22",	#CJK UNIFIED IDEOGRAPH
+	"8F ED	5E38",	#CJK UNIFIED IDEOGRAPH
+	"8F EE	60C5",	#CJK UNIFIED IDEOGRAPH
+	"8F EF	64FE",	#CJK UNIFIED IDEOGRAPH
+	"8F F0	6761",	#CJK UNIFIED IDEOGRAPH
+	"8F F1	6756",	#CJK UNIFIED IDEOGRAPH
+	"8F F2	6D44",	#CJK UNIFIED IDEOGRAPH
+	"8F F3	72B6",	#CJK UNIFIED IDEOGRAPH
+	"8F F4	7573",	#CJK UNIFIED IDEOGRAPH
+	"8F F5	7A63",	#CJK UNIFIED IDEOGRAPH
+	"8F F6	84B8",	#CJK UNIFIED IDEOGRAPH
+	"8F F7	8B72",	#CJK UNIFIED IDEOGRAPH
+	"8F F8	91B8",	#CJK UNIFIED IDEOGRAPH
+	"8F F9	9320",	#CJK UNIFIED IDEOGRAPH
+	"8F FA	5631",	#CJK UNIFIED IDEOGRAPH
+	"8F FB	57F4",	#CJK UNIFIED IDEOGRAPH
+	"8F FC	98FE",	#CJK UNIFIED IDEOGRAPH
+	"90 40	62ED",	#CJK UNIFIED IDEOGRAPH
+	"90 41	690D",	#CJK UNIFIED IDEOGRAPH
+	"90 42	6B96",	#CJK UNIFIED IDEOGRAPH
+	"90 43	71ED",	#CJK UNIFIED IDEOGRAPH
+	"90 44	7E54",	#CJK UNIFIED IDEOGRAPH
+	"90 45	8077",	#CJK UNIFIED IDEOGRAPH
+	"90 46	8272",	#CJK UNIFIED IDEOGRAPH
+	"90 47	89E6",	#CJK UNIFIED IDEOGRAPH
+	"90 48	98DF",	#CJK UNIFIED IDEOGRAPH
+	"90 49	8755",	#CJK UNIFIED IDEOGRAPH
+	"90 4A	8FB1",	#CJK UNIFIED IDEOGRAPH
+	"90 4B	5C3B",	#CJK UNIFIED IDEOGRAPH
+	"90 4C	4F38",	#CJK UNIFIED IDEOGRAPH
+	"90 4D	4FE1",	#CJK UNIFIED IDEOGRAPH
+	"90 4E	4FB5",	#CJK UNIFIED IDEOGRAPH
+	"90 4F	5507",	#CJK UNIFIED IDEOGRAPH
+	"90 50	5A20",	#CJK UNIFIED IDEOGRAPH
+	"90 51	5BDD",	#CJK UNIFIED IDEOGRAPH
+	"90 52	5BE9",	#CJK UNIFIED IDEOGRAPH
+	"90 53	5FC3",	#CJK UNIFIED IDEOGRAPH
+	"90 54	614E",	#CJK UNIFIED IDEOGRAPH
+	"90 55	632F",	#CJK UNIFIED IDEOGRAPH
+	"90 56	65B0",	#CJK UNIFIED IDEOGRAPH
+	"90 57	664B",	#CJK UNIFIED IDEOGRAPH
+	"90 58	68EE",	#CJK UNIFIED IDEOGRAPH
+	"90 59	699B",	#CJK UNIFIED IDEOGRAPH
+	"90 5A	6D78",	#CJK UNIFIED IDEOGRAPH
+	"90 5B	6DF1",	#CJK UNIFIED IDEOGRAPH
+	"90 5C	7533",	#CJK UNIFIED IDEOGRAPH
+	"90 5D	75B9",	#CJK UNIFIED IDEOGRAPH
+	"90 5E	771F",	#CJK UNIFIED IDEOGRAPH
+	"90 5F	795E",	#CJK UNIFIED IDEOGRAPH
+	"90 60	79E6",	#CJK UNIFIED IDEOGRAPH
+	"90 61	7D33",	#CJK UNIFIED IDEOGRAPH
+	"90 62	81E3",	#CJK UNIFIED IDEOGRAPH
+	"90 63	82AF",	#CJK UNIFIED IDEOGRAPH
+	"90 64	85AA",	#CJK UNIFIED IDEOGRAPH
+	"90 65	89AA",	#CJK UNIFIED IDEOGRAPH
+	"90 66	8A3A",	#CJK UNIFIED IDEOGRAPH
+	"90 67	8EAB",	#CJK UNIFIED IDEOGRAPH
+	"90 68	8F9B",	#CJK UNIFIED IDEOGRAPH
+	"90 69	9032",	#CJK UNIFIED IDEOGRAPH
+	"90 6A	91DD",	#CJK UNIFIED IDEOGRAPH
+	"90 6B	9707",	#CJK UNIFIED IDEOGRAPH
+	"90 6C	4EBA",	#CJK UNIFIED IDEOGRAPH
+	"90 6D	4EC1",	#CJK UNIFIED IDEOGRAPH
+	"90 6E	5203",	#CJK UNIFIED IDEOGRAPH
+	"90 6F	5875",	#CJK UNIFIED IDEOGRAPH
+	"90 70	58EC",	#CJK UNIFIED IDEOGRAPH
+	"90 71	5C0B",	#CJK UNIFIED IDEOGRAPH
+	"90 72	751A",	#CJK UNIFIED IDEOGRAPH
+	"90 73	5C3D",	#CJK UNIFIED IDEOGRAPH
+	"90 74	814E",	#CJK UNIFIED IDEOGRAPH
+	"90 75	8A0A",	#CJK UNIFIED IDEOGRAPH
+	"90 76	8FC5",	#CJK UNIFIED IDEOGRAPH
+	"90 77	9663",	#CJK UNIFIED IDEOGRAPH
+	"90 78	976D",	#CJK UNIFIED IDEOGRAPH
+	"90 79	7B25",	#CJK UNIFIED IDEOGRAPH
+	"90 7A	8ACF",	#CJK UNIFIED IDEOGRAPH
+	"90 7B	9808",	#CJK UNIFIED IDEOGRAPH
+	"90 7C	9162",	#CJK UNIFIED IDEOGRAPH
+	"90 7D	56F3",	#CJK UNIFIED IDEOGRAPH
+	"90 7E	53A8",	#CJK UNIFIED IDEOGRAPH
+	"90 80	9017",	#CJK UNIFIED IDEOGRAPH
+	"90 81	5439",	#CJK UNIFIED IDEOGRAPH
+	"90 82	5782",	#CJK UNIFIED IDEOGRAPH
+	"90 83	5E25",	#CJK UNIFIED IDEOGRAPH
+	"90 84	63A8",	#CJK UNIFIED IDEOGRAPH
+	"90 85	6C34",	#CJK UNIFIED IDEOGRAPH
+	"90 86	708A",	#CJK UNIFIED IDEOGRAPH
+	"90 87	7761",	#CJK UNIFIED IDEOGRAPH
+	"90 88	7C8B",	#CJK UNIFIED IDEOGRAPH
+	"90 89	7FE0",	#CJK UNIFIED IDEOGRAPH
+	"90 8A	8870",	#CJK UNIFIED IDEOGRAPH
+	"90 8B	9042",	#CJK UNIFIED IDEOGRAPH
+	"90 8C	9154",	#CJK UNIFIED IDEOGRAPH
+	"90 8D	9310",	#CJK UNIFIED IDEOGRAPH
+	"90 8E	9318",	#CJK UNIFIED IDEOGRAPH
+	"90 8F	968F",	#CJK UNIFIED IDEOGRAPH
+	"90 90	745E",	#CJK UNIFIED IDEOGRAPH
+	"90 91	9AC4",	#CJK UNIFIED IDEOGRAPH
+	"90 92	5D07",	#CJK UNIFIED IDEOGRAPH
+	"90 93	5D69",	#CJK UNIFIED IDEOGRAPH
+	"90 94	6570",	#CJK UNIFIED IDEOGRAPH
+	"90 95	67A2",	#CJK UNIFIED IDEOGRAPH
+	"90 96	8DA8",	#CJK UNIFIED IDEOGRAPH
+	"90 97	96DB",	#CJK UNIFIED IDEOGRAPH
+	"90 98	636E",	#CJK UNIFIED IDEOGRAPH
+	"90 99	6749",	#CJK UNIFIED IDEOGRAPH
+	"90 9A	6919",	#CJK UNIFIED IDEOGRAPH
+	"90 9B	83C5",	#CJK UNIFIED IDEOGRAPH
+	"90 9C	9817",	#CJK UNIFIED IDEOGRAPH
+	"90 9D	96C0",	#CJK UNIFIED IDEOGRAPH
+	"90 9E	88FE",	#CJK UNIFIED IDEOGRAPH
+	"90 9F	6F84",	#CJK UNIFIED IDEOGRAPH
+	"90 A0	647A",	#CJK UNIFIED IDEOGRAPH
+	"90 A1	5BF8",	#CJK UNIFIED IDEOGRAPH
+	"90 A2	4E16",	#CJK UNIFIED IDEOGRAPH
+	"90 A3	702C",	#CJK UNIFIED IDEOGRAPH
+	"90 A4	755D",	#CJK UNIFIED IDEOGRAPH
+	"90 A5	662F",	#CJK UNIFIED IDEOGRAPH
+	"90 A6	51C4",	#CJK UNIFIED IDEOGRAPH
+	"90 A7	5236",	#CJK UNIFIED IDEOGRAPH
+	"90 A8	52E2",	#CJK UNIFIED IDEOGRAPH
+	"90 A9	59D3",	#CJK UNIFIED IDEOGRAPH
+	"90 AA	5F81",	#CJK UNIFIED IDEOGRAPH
+	"90 AB	6027",	#CJK UNIFIED IDEOGRAPH
+	"90 AC	6210",	#CJK UNIFIED IDEOGRAPH
+	"90 AD	653F",	#CJK UNIFIED IDEOGRAPH
+	"90 AE	6574",	#CJK UNIFIED IDEOGRAPH
+	"90 AF	661F",	#CJK UNIFIED IDEOGRAPH
+	"90 B0	6674",	#CJK UNIFIED IDEOGRAPH
+	"90 B1	68F2",	#CJK UNIFIED IDEOGRAPH
+	"90 B2	6816",	#CJK UNIFIED IDEOGRAPH
+	"90 B3	6B63",	#CJK UNIFIED IDEOGRAPH
+	"90 B4	6E05",	#CJK UNIFIED IDEOGRAPH
+	"90 B5	7272",	#CJK UNIFIED IDEOGRAPH
+	"90 B6	751F",	#CJK UNIFIED IDEOGRAPH
+	"90 B7	76DB",	#CJK UNIFIED IDEOGRAPH
+	"90 B8	7CBE",	#CJK UNIFIED IDEOGRAPH
+	"90 B9	8056",	#CJK UNIFIED IDEOGRAPH
+	"90 BA	58F0",	#CJK UNIFIED IDEOGRAPH
+	"90 BB	88FD",	#CJK UNIFIED IDEOGRAPH
+	"90 BC	897F",	#CJK UNIFIED IDEOGRAPH
+	"90 BD	8AA0",	#CJK UNIFIED IDEOGRAPH
+	"90 BE	8A93",	#CJK UNIFIED IDEOGRAPH
+	"90 BF	8ACB",	#CJK UNIFIED IDEOGRAPH
+	"90 C0	901D",	#CJK UNIFIED IDEOGRAPH
+	"90 C1	9192",	#CJK UNIFIED IDEOGRAPH
+	"90 C2	9752",	#CJK UNIFIED IDEOGRAPH
+	"90 C3	9759",	#CJK UNIFIED IDEOGRAPH
+	"90 C4	6589",	#CJK UNIFIED IDEOGRAPH
+	"90 C5	7A0E",	#CJK UNIFIED IDEOGRAPH
+	"90 C6	8106",	#CJK UNIFIED IDEOGRAPH
+	"90 C7	96BB",	#CJK UNIFIED IDEOGRAPH
+	"90 C8	5E2D",	#CJK UNIFIED IDEOGRAPH
+	"90 C9	60DC",	#CJK UNIFIED IDEOGRAPH
+	"90 CA	621A",	#CJK UNIFIED IDEOGRAPH
+	"90 CB	65A5",	#CJK UNIFIED IDEOGRAPH
+	"90 CC	6614",	#CJK UNIFIED IDEOGRAPH
+	"90 CD	6790",	#CJK UNIFIED IDEOGRAPH
+	"90 CE	77F3",	#CJK UNIFIED IDEOGRAPH
+	"90 CF	7A4D",	#CJK UNIFIED IDEOGRAPH
+	"90 D0	7C4D",	#CJK UNIFIED IDEOGRAPH
+	"90 D1	7E3E",	#CJK UNIFIED IDEOGRAPH
+	"90 D2	810A",	#CJK UNIFIED IDEOGRAPH
+	"90 D3	8CAC",	#CJK UNIFIED IDEOGRAPH
+	"90 D4	8D64",	#CJK UNIFIED IDEOGRAPH
+	"90 D5	8DE1",	#CJK UNIFIED IDEOGRAPH
+	"90 D6	8E5F",	#CJK UNIFIED IDEOGRAPH
+	"90 D7	78A9",	#CJK UNIFIED IDEOGRAPH
+	"90 D8	5207",	#CJK UNIFIED IDEOGRAPH
+	"90 D9	62D9",	#CJK UNIFIED IDEOGRAPH
+	"90 DA	63A5",	#CJK UNIFIED IDEOGRAPH
+	"90 DB	6442",	#CJK UNIFIED IDEOGRAPH
+	"90 DC	6298",	#CJK UNIFIED IDEOGRAPH
+	"90 DD	8A2D",	#CJK UNIFIED IDEOGRAPH
+	"90 DE	7A83",	#CJK UNIFIED IDEOGRAPH
+	"90 DF	7BC0",	#CJK UNIFIED IDEOGRAPH
+	"90 E0	8AAC",	#CJK UNIFIED IDEOGRAPH
+	"90 E1	96EA",	#CJK UNIFIED IDEOGRAPH
+	"90 E2	7D76",	#CJK UNIFIED IDEOGRAPH
+	"90 E3	820C",	#CJK UNIFIED IDEOGRAPH
+	"90 E4	8749",	#CJK UNIFIED IDEOGRAPH
+	"90 E5	4ED9",	#CJK UNIFIED IDEOGRAPH
+	"90 E6	5148",	#CJK UNIFIED IDEOGRAPH
+	"90 E7	5343",	#CJK UNIFIED IDEOGRAPH
+	"90 E8	5360",	#CJK UNIFIED IDEOGRAPH
+	"90 E9	5BA3",	#CJK UNIFIED IDEOGRAPH
+	"90 EA	5C02",	#CJK UNIFIED IDEOGRAPH
+	"90 EB	5C16",	#CJK UNIFIED IDEOGRAPH
+	"90 EC	5DDD",	#CJK UNIFIED IDEOGRAPH
+	"90 ED	6226",	#CJK UNIFIED IDEOGRAPH
+	"90 EE	6247",	#CJK UNIFIED IDEOGRAPH
+	"90 EF	64B0",	#CJK UNIFIED IDEOGRAPH
+	"90 F0	6813",	#CJK UNIFIED IDEOGRAPH
+	"90 F1	6834",	#CJK UNIFIED IDEOGRAPH
+	"90 F2	6CC9",	#CJK UNIFIED IDEOGRAPH
+	"90 F3	6D45",	#CJK UNIFIED IDEOGRAPH
+	"90 F4	6D17",	#CJK UNIFIED IDEOGRAPH
+	"90 F5	67D3",	#CJK UNIFIED IDEOGRAPH
+	"90 F6	6F5C",	#CJK UNIFIED IDEOGRAPH
+	"90 F7	714E",	#CJK UNIFIED IDEOGRAPH
+	"90 F8	717D",	#CJK UNIFIED IDEOGRAPH
+	"90 F9	65CB",	#CJK UNIFIED IDEOGRAPH
+	"90 FA	7A7F",	#CJK UNIFIED IDEOGRAPH
+	"90 FB	7BAD",	#CJK UNIFIED IDEOGRAPH
+	"90 FC	7DDA",	#CJK UNIFIED IDEOGRAPH
+	"91 40	7E4A",	#CJK UNIFIED IDEOGRAPH
+	"91 41	7FA8",	#CJK UNIFIED IDEOGRAPH
+	"91 42	817A",	#CJK UNIFIED IDEOGRAPH
+	"91 43	821B",	#CJK UNIFIED IDEOGRAPH
+	"91 44	8239",	#CJK UNIFIED IDEOGRAPH
+	"91 45	85A6",	#CJK UNIFIED IDEOGRAPH
+	"91 46	8A6E",	#CJK UNIFIED IDEOGRAPH
+	"91 47	8CCE",	#CJK UNIFIED IDEOGRAPH
+	"91 48	8DF5",	#CJK UNIFIED IDEOGRAPH
+	"91 49	9078",	#CJK UNIFIED IDEOGRAPH
+	"91 4A	9077",	#CJK UNIFIED IDEOGRAPH
+	"91 4B	92AD",	#CJK UNIFIED IDEOGRAPH
+	"91 4C	9291",	#CJK UNIFIED IDEOGRAPH
+	"91 4D	9583",	#CJK UNIFIED IDEOGRAPH
+	"91 4E	9BAE",	#CJK UNIFIED IDEOGRAPH
+	"91 4F	524D",	#CJK UNIFIED IDEOGRAPH
+	"91 50	5584",	#CJK UNIFIED IDEOGRAPH
+	"91 51	6F38",	#CJK UNIFIED IDEOGRAPH
+	"91 52	7136",	#CJK UNIFIED IDEOGRAPH
+	"91 53	5168",	#CJK UNIFIED IDEOGRAPH
+	"91 54	7985",	#CJK UNIFIED IDEOGRAPH
+	"91 55	7E55",	#CJK UNIFIED IDEOGRAPH
+	"91 56	81B3",	#CJK UNIFIED IDEOGRAPH
+	"91 57	7CCE",	#CJK UNIFIED IDEOGRAPH
+	"91 58	564C",	#CJK UNIFIED IDEOGRAPH
+	"91 59	5851",	#CJK UNIFIED IDEOGRAPH
+	"91 5A	5CA8",	#CJK UNIFIED IDEOGRAPH
+	"91 5B	63AA",	#CJK UNIFIED IDEOGRAPH
+	"91 5C	66FE",	#CJK UNIFIED IDEOGRAPH
+	"91 5D	66FD",	#CJK UNIFIED IDEOGRAPH
+	"91 5E	695A",	#CJK UNIFIED IDEOGRAPH
+	"91 5F	72D9",	#CJK UNIFIED IDEOGRAPH
+	"91 60	758F",	#CJK UNIFIED IDEOGRAPH
+	"91 61	758E",	#CJK UNIFIED IDEOGRAPH
+	"91 62	790E",	#CJK UNIFIED IDEOGRAPH
+	"91 63	7956",	#CJK UNIFIED IDEOGRAPH
+	"91 64	79DF",	#CJK UNIFIED IDEOGRAPH
+	"91 65	7C97",	#CJK UNIFIED IDEOGRAPH
+	"91 66	7D20",	#CJK UNIFIED IDEOGRAPH
+	"91 67	7D44",	#CJK UNIFIED IDEOGRAPH
+	"91 68	8607",	#CJK UNIFIED IDEOGRAPH
+	"91 69	8A34",	#CJK UNIFIED IDEOGRAPH
+	"91 6A	963B",	#CJK UNIFIED IDEOGRAPH
+	"91 6B	9061",	#CJK UNIFIED IDEOGRAPH
+	"91 6C	9F20",	#CJK UNIFIED IDEOGRAPH
+	"91 6D	50E7",	#CJK UNIFIED IDEOGRAPH
+	"91 6E	5275",	#CJK UNIFIED IDEOGRAPH
+	"91 6F	53CC",	#CJK UNIFIED IDEOGRAPH
+	"91 70	53E2",	#CJK UNIFIED IDEOGRAPH
+	"91 71	5009",	#CJK UNIFIED IDEOGRAPH
+	"91 72	55AA",	#CJK UNIFIED IDEOGRAPH
+	"91 73	58EE",	#CJK UNIFIED IDEOGRAPH
+	"91 74	594F",	#CJK UNIFIED IDEOGRAPH
+	"91 75	723D",	#CJK UNIFIED IDEOGRAPH
+	"91 76	5B8B",	#CJK UNIFIED IDEOGRAPH
+	"91 77	5C64",	#CJK UNIFIED IDEOGRAPH
+	"91 78	531D",	#CJK UNIFIED IDEOGRAPH
+	"91 79	60E3",	#CJK UNIFIED IDEOGRAPH
+	"91 7A	60F3",	#CJK UNIFIED IDEOGRAPH
+	"91 7B	635C",	#CJK UNIFIED IDEOGRAPH
+	"91 7C	6383",	#CJK UNIFIED IDEOGRAPH
+	"91 7D	633F",	#CJK UNIFIED IDEOGRAPH
+	"91 7E	63BB",	#CJK UNIFIED IDEOGRAPH
+	"91 80	64CD",	#CJK UNIFIED IDEOGRAPH
+	"91 81	65E9",	#CJK UNIFIED IDEOGRAPH
+	"91 82	66F9",	#CJK UNIFIED IDEOGRAPH
+	"91 83	5DE3",	#CJK UNIFIED IDEOGRAPH
+	"91 84	69CD",	#CJK UNIFIED IDEOGRAPH
+	"91 85	69FD",	#CJK UNIFIED IDEOGRAPH
+	"91 86	6F15",	#CJK UNIFIED IDEOGRAPH
+	"91 87	71E5",	#CJK UNIFIED IDEOGRAPH
+	"91 88	4E89",	#CJK UNIFIED IDEOGRAPH
+	"91 89	75E9",	#CJK UNIFIED IDEOGRAPH
+	"91 8A	76F8",	#CJK UNIFIED IDEOGRAPH
+	"91 8B	7A93",	#CJK UNIFIED IDEOGRAPH
+	"91 8C	7CDF",	#CJK UNIFIED IDEOGRAPH
+	"91 8D	7DCF",	#CJK UNIFIED IDEOGRAPH
+	"91 8E	7D9C",	#CJK UNIFIED IDEOGRAPH
+	"91 8F	8061",	#CJK UNIFIED IDEOGRAPH
+	"91 90	8349",	#CJK UNIFIED IDEOGRAPH
+	"91 91	8358",	#CJK UNIFIED IDEOGRAPH
+	"91 92	846C",	#CJK UNIFIED IDEOGRAPH
+	"91 93	84BC",	#CJK UNIFIED IDEOGRAPH
+	"91 94	85FB",	#CJK UNIFIED IDEOGRAPH
+	"91 95	88C5",	#CJK UNIFIED IDEOGRAPH
+	"91 96	8D70",	#CJK UNIFIED IDEOGRAPH
+	"91 97	9001",	#CJK UNIFIED IDEOGRAPH
+	"91 98	906D",	#CJK UNIFIED IDEOGRAPH
+	"91 99	9397",	#CJK UNIFIED IDEOGRAPH
+	"91 9A	971C",	#CJK UNIFIED IDEOGRAPH
+	"91 9B	9A12",	#CJK UNIFIED IDEOGRAPH
+	"91 9C	50CF",	#CJK UNIFIED IDEOGRAPH
+	"91 9D	5897",	#CJK UNIFIED IDEOGRAPH
+	"91 9E	618E",	#CJK UNIFIED IDEOGRAPH
+	"91 9F	81D3",	#CJK UNIFIED IDEOGRAPH
+	"91 A0	8535",	#CJK UNIFIED IDEOGRAPH
+	"91 A1	8D08",	#CJK UNIFIED IDEOGRAPH
+	"91 A2	9020",	#CJK UNIFIED IDEOGRAPH
+	"91 A3	4FC3",	#CJK UNIFIED IDEOGRAPH
+	"91 A4	5074",	#CJK UNIFIED IDEOGRAPH
+	"91 A5	5247",	#CJK UNIFIED IDEOGRAPH
+	"91 A6	5373",	#CJK UNIFIED IDEOGRAPH
+	"91 A7	606F",	#CJK UNIFIED IDEOGRAPH
+	"91 A8	6349",	#CJK UNIFIED IDEOGRAPH
+	"91 A9	675F",	#CJK UNIFIED IDEOGRAPH
+	"91 AA	6E2C",	#CJK UNIFIED IDEOGRAPH
+	"91 AB	8DB3",	#CJK UNIFIED IDEOGRAPH
+	"91 AC	901F",	#CJK UNIFIED IDEOGRAPH
+	"91 AD	4FD7",	#CJK UNIFIED IDEOGRAPH
+	"91 AE	5C5E",	#CJK UNIFIED IDEOGRAPH
+	"91 AF	8CCA",	#CJK UNIFIED IDEOGRAPH
+	"91 B0	65CF",	#CJK UNIFIED IDEOGRAPH
+	"91 B1	7D9A",	#CJK UNIFIED IDEOGRAPH
+	"91 B2	5352",	#CJK UNIFIED IDEOGRAPH
+	"91 B3	8896",	#CJK UNIFIED IDEOGRAPH
+	"91 B4	5176",	#CJK UNIFIED IDEOGRAPH
+	"91 B5	63C3",	#CJK UNIFIED IDEOGRAPH
+	"91 B6	5B58",	#CJK UNIFIED IDEOGRAPH
+	"91 B7	5B6B",	#CJK UNIFIED IDEOGRAPH
+	"91 B8	5C0A",	#CJK UNIFIED IDEOGRAPH
+	"91 B9	640D",	#CJK UNIFIED IDEOGRAPH
+	"91 BA	6751",	#CJK UNIFIED IDEOGRAPH
+	"91 BB	905C",	#CJK UNIFIED IDEOGRAPH
+	"91 BC	4ED6",	#CJK UNIFIED IDEOGRAPH
+	"91 BD	591A",	#CJK UNIFIED IDEOGRAPH
+	"91 BE	592A",	#CJK UNIFIED IDEOGRAPH
+	"91 BF	6C70",	#CJK UNIFIED IDEOGRAPH
+	"91 C0	8A51",	#CJK UNIFIED IDEOGRAPH
+	"91 C1	553E",	#CJK UNIFIED IDEOGRAPH
+	"91 C2	5815",	#CJK UNIFIED IDEOGRAPH
+	"91 C3	59A5",	#CJK UNIFIED IDEOGRAPH
+	"91 C4	60F0",	#CJK UNIFIED IDEOGRAPH
+	"91 C5	6253",	#CJK UNIFIED IDEOGRAPH
+	"91 C6	67C1",	#CJK UNIFIED IDEOGRAPH
+	"91 C7	8235",	#CJK UNIFIED IDEOGRAPH
+	"91 C8	6955",	#CJK UNIFIED IDEOGRAPH
+	"91 C9	9640",	#CJK UNIFIED IDEOGRAPH
+	"91 CA	99C4",	#CJK UNIFIED IDEOGRAPH
+	"91 CB	9A28",	#CJK UNIFIED IDEOGRAPH
+	"91 CC	4F53",	#CJK UNIFIED IDEOGRAPH
+	"91 CD	5806",	#CJK UNIFIED IDEOGRAPH
+	"91 CE	5BFE",	#CJK UNIFIED IDEOGRAPH
+	"91 CF	8010",	#CJK UNIFIED IDEOGRAPH
+	"91 D0	5CB1",	#CJK UNIFIED IDEOGRAPH
+	"91 D1	5E2F",	#CJK UNIFIED IDEOGRAPH
+	"91 D2	5F85",	#CJK UNIFIED IDEOGRAPH
+	"91 D3	6020",	#CJK UNIFIED IDEOGRAPH
+	"91 D4	614B",	#CJK UNIFIED IDEOGRAPH
+	"91 D5	6234",	#CJK UNIFIED IDEOGRAPH
+	"91 D6	66FF",	#CJK UNIFIED IDEOGRAPH
+	"91 D7	6CF0",	#CJK UNIFIED IDEOGRAPH
+	"91 D8	6EDE",	#CJK UNIFIED IDEOGRAPH
+	"91 D9	80CE",	#CJK UNIFIED IDEOGRAPH
+	"91 DA	817F",	#CJK UNIFIED IDEOGRAPH
+	"91 DB	82D4",	#CJK UNIFIED IDEOGRAPH
+	"91 DC	888B",	#CJK UNIFIED IDEOGRAPH
+	"91 DD	8CB8",	#CJK UNIFIED IDEOGRAPH
+	"91 DE	9000",	#CJK UNIFIED IDEOGRAPH
+	"91 DF	902E",	#CJK UNIFIED IDEOGRAPH
+	"91 E0	968A",	#CJK UNIFIED IDEOGRAPH
+	"91 E1	9EDB",	#CJK UNIFIED IDEOGRAPH
+	"91 E2	9BDB",	#CJK UNIFIED IDEOGRAPH
+	"91 E3	4EE3",	#CJK UNIFIED IDEOGRAPH
+	"91 E4	53F0",	#CJK UNIFIED IDEOGRAPH
+	"91 E5	5927",	#CJK UNIFIED IDEOGRAPH
+	"91 E6	7B2C",	#CJK UNIFIED IDEOGRAPH
+	"91 E7	918D",	#CJK UNIFIED IDEOGRAPH
+	"91 E8	984C",	#CJK UNIFIED IDEOGRAPH
+	"91 E9	9DF9",	#CJK UNIFIED IDEOGRAPH
+	"91 EA	6EDD",	#CJK UNIFIED IDEOGRAPH
+	"91 EB	7027",	#CJK UNIFIED IDEOGRAPH
+	"91 EC	5353",	#CJK UNIFIED IDEOGRAPH
+	"91 ED	5544",	#CJK UNIFIED IDEOGRAPH
+	"91 EE	5B85",	#CJK UNIFIED IDEOGRAPH
+	"91 EF	6258",	#CJK UNIFIED IDEOGRAPH
+	"91 F0	629E",	#CJK UNIFIED IDEOGRAPH
+	"91 F1	62D3",	#CJK UNIFIED IDEOGRAPH
+	"91 F2	6CA2",	#CJK UNIFIED IDEOGRAPH
+	"91 F3	6FEF",	#CJK UNIFIED IDEOGRAPH
+	"91 F4	7422",	#CJK UNIFIED IDEOGRAPH
+	"91 F5	8A17",	#CJK UNIFIED IDEOGRAPH
+	"91 F6	9438",	#CJK UNIFIED IDEOGRAPH
+	"91 F7	6FC1",	#CJK UNIFIED IDEOGRAPH
+	"91 F8	8AFE",	#CJK UNIFIED IDEOGRAPH
+	"91 F9	8338",	#CJK UNIFIED IDEOGRAPH
+	"91 FA	51E7",	#CJK UNIFIED IDEOGRAPH
+	"91 FB	86F8",	#CJK UNIFIED IDEOGRAPH
+	"91 FC	53EA",	#CJK UNIFIED IDEOGRAPH
+	"92 40	53E9",	#CJK UNIFIED IDEOGRAPH
+	"92 41	4F46",	#CJK UNIFIED IDEOGRAPH
+	"92 42	9054",	#CJK UNIFIED IDEOGRAPH
+	"92 43	8FB0",	#CJK UNIFIED IDEOGRAPH
+	"92 44	596A",	#CJK UNIFIED IDEOGRAPH
+	"92 45	8131",	#CJK UNIFIED IDEOGRAPH
+	"92 46	5DFD",	#CJK UNIFIED IDEOGRAPH
+	"92 47	7AEA",	#CJK UNIFIED IDEOGRAPH
+	"92 48	8FBF",	#CJK UNIFIED IDEOGRAPH
+	"92 49	68DA",	#CJK UNIFIED IDEOGRAPH
+	"92 4A	8C37",	#CJK UNIFIED IDEOGRAPH
+	"92 4B	72F8",	#CJK UNIFIED IDEOGRAPH
+	"92 4C	9C48",	#CJK UNIFIED IDEOGRAPH
+	"92 4D	6A3D",	#CJK UNIFIED IDEOGRAPH
+	"92 4E	8AB0",	#CJK UNIFIED IDEOGRAPH
+	"92 4F	4E39",	#CJK UNIFIED IDEOGRAPH
+	"92 50	5358",	#CJK UNIFIED IDEOGRAPH
+	"92 51	5606",	#CJK UNIFIED IDEOGRAPH
+	"92 52	5766",	#CJK UNIFIED IDEOGRAPH
+	"92 53	62C5",	#CJK UNIFIED IDEOGRAPH
+	"92 54	63A2",	#CJK UNIFIED IDEOGRAPH
+	"92 55	65E6",	#CJK UNIFIED IDEOGRAPH
+	"92 56	6B4E",	#CJK UNIFIED IDEOGRAPH
+	"92 57	6DE1",	#CJK UNIFIED IDEOGRAPH
+	"92 58	6E5B",	#CJK UNIFIED IDEOGRAPH
+	"92 59	70AD",	#CJK UNIFIED IDEOGRAPH
+	"92 5A	77ED",	#CJK UNIFIED IDEOGRAPH
+	"92 5B	7AEF",	#CJK UNIFIED IDEOGRAPH
+	"92 5C	7BAA",	#CJK UNIFIED IDEOGRAPH
+	"92 5D	7DBB",	#CJK UNIFIED IDEOGRAPH
+	"92 5E	803D",	#CJK UNIFIED IDEOGRAPH
+	"92 5F	80C6",	#CJK UNIFIED IDEOGRAPH
+	"92 60	86CB",	#CJK UNIFIED IDEOGRAPH
+	"92 61	8A95",	#CJK UNIFIED IDEOGRAPH
+	"92 62	935B",	#CJK UNIFIED IDEOGRAPH
+	"92 63	56E3",	#CJK UNIFIED IDEOGRAPH
+	"92 64	58C7",	#CJK UNIFIED IDEOGRAPH
+	"92 65	5F3E",	#CJK UNIFIED IDEOGRAPH
+	"92 66	65AD",	#CJK UNIFIED IDEOGRAPH
+	"92 67	6696",	#CJK UNIFIED IDEOGRAPH
+	"92 68	6A80",	#CJK UNIFIED IDEOGRAPH
+	"92 69	6BB5",	#CJK UNIFIED IDEOGRAPH
+	"92 6A	7537",	#CJK UNIFIED IDEOGRAPH
+	"92 6B	8AC7",	#CJK UNIFIED IDEOGRAPH
+	"92 6C	5024",	#CJK UNIFIED IDEOGRAPH
+	"92 6D	77E5",	#CJK UNIFIED IDEOGRAPH
+	"92 6E	5730",	#CJK UNIFIED IDEOGRAPH
+	"92 6F	5F1B",	#CJK UNIFIED IDEOGRAPH
+	"92 70	6065",	#CJK UNIFIED IDEOGRAPH
+	"92 71	667A",	#CJK UNIFIED IDEOGRAPH
+	"92 72	6C60",	#CJK UNIFIED IDEOGRAPH
+	"92 73	75F4",	#CJK UNIFIED IDEOGRAPH
+	"92 74	7A1A",	#CJK UNIFIED IDEOGRAPH
+	"92 75	7F6E",	#CJK UNIFIED IDEOGRAPH
+	"92 76	81F4",	#CJK UNIFIED IDEOGRAPH
+	"92 77	8718",	#CJK UNIFIED IDEOGRAPH
+	"92 78	9045",	#CJK UNIFIED IDEOGRAPH
+	"92 79	99B3",	#CJK UNIFIED IDEOGRAPH
+	"92 7A	7BC9",	#CJK UNIFIED IDEOGRAPH
+	"92 7B	755C",	#CJK UNIFIED IDEOGRAPH
+	"92 7C	7AF9",	#CJK UNIFIED IDEOGRAPH
+	"92 7D	7B51",	#CJK UNIFIED IDEOGRAPH
+	"92 7E	84C4",	#CJK UNIFIED IDEOGRAPH
+	"92 80	9010",	#CJK UNIFIED IDEOGRAPH
+	"92 81	79E9",	#CJK UNIFIED IDEOGRAPH
+	"92 82	7A92",	#CJK UNIFIED IDEOGRAPH
+	"92 83	8336",	#CJK UNIFIED IDEOGRAPH
+	"92 84	5AE1",	#CJK UNIFIED IDEOGRAPH
+	"92 85	7740",	#CJK UNIFIED IDEOGRAPH
+	"92 86	4E2D",	#CJK UNIFIED IDEOGRAPH
+	"92 87	4EF2",	#CJK UNIFIED IDEOGRAPH
+	"92 88	5B99",	#CJK UNIFIED IDEOGRAPH
+	"92 89	5FE0",	#CJK UNIFIED IDEOGRAPH
+	"92 8A	62BD",	#CJK UNIFIED IDEOGRAPH
+	"92 8B	663C",	#CJK UNIFIED IDEOGRAPH
+	"92 8C	67F1",	#CJK UNIFIED IDEOGRAPH
+	"92 8D	6CE8",	#CJK UNIFIED IDEOGRAPH
+	"92 8E	866B",	#CJK UNIFIED IDEOGRAPH
+	"92 8F	8877",	#CJK UNIFIED IDEOGRAPH
+	"92 90	8A3B",	#CJK UNIFIED IDEOGRAPH
+	"92 91	914E",	#CJK UNIFIED IDEOGRAPH
+	"92 92	92F3",	#CJK UNIFIED IDEOGRAPH
+	"92 93	99D0",	#CJK UNIFIED IDEOGRAPH
+	"92 94	6A17",	#CJK UNIFIED IDEOGRAPH
+	"92 95	7026",	#CJK UNIFIED IDEOGRAPH
+	"92 96	732A",	#CJK UNIFIED IDEOGRAPH
+	"92 97	82E7",	#CJK UNIFIED IDEOGRAPH
+	"92 98	8457",	#CJK UNIFIED IDEOGRAPH
+	"92 99	8CAF",	#CJK UNIFIED IDEOGRAPH
+	"92 9A	4E01",	#CJK UNIFIED IDEOGRAPH
+	"92 9B	5146",	#CJK UNIFIED IDEOGRAPH
+	"92 9C	51CB",	#CJK UNIFIED IDEOGRAPH
+	"92 9D	558B",	#CJK UNIFIED IDEOGRAPH
+	"92 9E	5BF5",	#CJK UNIFIED IDEOGRAPH
+	"92 9F	5E16",	#CJK UNIFIED IDEOGRAPH
+	"92 A0	5E33",	#CJK UNIFIED IDEOGRAPH
+	"92 A1	5E81",	#CJK UNIFIED IDEOGRAPH
+	"92 A2	5F14",	#CJK UNIFIED IDEOGRAPH
+	"92 A3	5F35",	#CJK UNIFIED IDEOGRAPH
+	"92 A4	5F6B",	#CJK UNIFIED IDEOGRAPH
+	"92 A5	5FB4",	#CJK UNIFIED IDEOGRAPH
+	"92 A6	61F2",	#CJK UNIFIED IDEOGRAPH
+	"92 A7	6311",	#CJK UNIFIED IDEOGRAPH
+	"92 A8	66A2",	#CJK UNIFIED IDEOGRAPH
+	"92 A9	671D",	#CJK UNIFIED IDEOGRAPH
+	"92 AA	6F6E",	#CJK UNIFIED IDEOGRAPH
+	"92 AB	7252",	#CJK UNIFIED IDEOGRAPH
+	"92 AC	753A",	#CJK UNIFIED IDEOGRAPH
+	"92 AD	773A",	#CJK UNIFIED IDEOGRAPH
+	"92 AE	8074",	#CJK UNIFIED IDEOGRAPH
+	"92 AF	8139",	#CJK UNIFIED IDEOGRAPH
+	"92 B0	8178",	#CJK UNIFIED IDEOGRAPH
+	"92 B1	8776",	#CJK UNIFIED IDEOGRAPH
+	"92 B2	8ABF",	#CJK UNIFIED IDEOGRAPH
+	"92 B3	8ADC",	#CJK UNIFIED IDEOGRAPH
+	"92 B4	8D85",	#CJK UNIFIED IDEOGRAPH
+	"92 B5	8DF3",	#CJK UNIFIED IDEOGRAPH
+	"92 B6	929A",	#CJK UNIFIED IDEOGRAPH
+	"92 B7	9577",	#CJK UNIFIED IDEOGRAPH
+	"92 B8	9802",	#CJK UNIFIED IDEOGRAPH
+	"92 B9	9CE5",	#CJK UNIFIED IDEOGRAPH
+	"92 BA	52C5",	#CJK UNIFIED IDEOGRAPH
+	"92 BB	6357",	#CJK UNIFIED IDEOGRAPH
+	"92 BC	76F4",	#CJK UNIFIED IDEOGRAPH
+	"92 BD	6715",	#CJK UNIFIED IDEOGRAPH
+	"92 BE	6C88",	#CJK UNIFIED IDEOGRAPH
+	"92 BF	73CD",	#CJK UNIFIED IDEOGRAPH
+	"92 C0	8CC3",	#CJK UNIFIED IDEOGRAPH
+	"92 C1	93AE",	#CJK UNIFIED IDEOGRAPH
+	"92 C2	9673",	#CJK UNIFIED IDEOGRAPH
+	"92 C3	6D25",	#CJK UNIFIED IDEOGRAPH
+	"92 C4	589C",	#CJK UNIFIED IDEOGRAPH
+	"92 C5	690E",	#CJK UNIFIED IDEOGRAPH
+	"92 C6	69CC",	#CJK UNIFIED IDEOGRAPH
+	"92 C7	8FFD",	#CJK UNIFIED IDEOGRAPH
+	"92 C8	939A",	#CJK UNIFIED IDEOGRAPH
+	"92 C9	75DB",	#CJK UNIFIED IDEOGRAPH
+	"92 CA	901A",	#CJK UNIFIED IDEOGRAPH
+	"92 CB	585A",	#CJK UNIFIED IDEOGRAPH
+	"92 CC	6802",	#CJK UNIFIED IDEOGRAPH
+	"92 CD	63B4",	#CJK UNIFIED IDEOGRAPH
+	"92 CE	69FB",	#CJK UNIFIED IDEOGRAPH
+	"92 CF	4F43",	#CJK UNIFIED IDEOGRAPH
+	"92 D0	6F2C",	#CJK UNIFIED IDEOGRAPH
+	"92 D1	67D8",	#CJK UNIFIED IDEOGRAPH
+	"92 D2	8FBB",	#CJK UNIFIED IDEOGRAPH
+	"92 D3	8526",	#CJK UNIFIED IDEOGRAPH
+	"92 D4	7DB4",	#CJK UNIFIED IDEOGRAPH
+	"92 D5	9354",	#CJK UNIFIED IDEOGRAPH
+	"92 D6	693F",	#CJK UNIFIED IDEOGRAPH
+	"92 D7	6F70",	#CJK UNIFIED IDEOGRAPH
+	"92 D8	576A",	#CJK UNIFIED IDEOGRAPH
+	"92 D9	58F7",	#CJK UNIFIED IDEOGRAPH
+	"92 DA	5B2C",	#CJK UNIFIED IDEOGRAPH
+	"92 DB	7D2C",	#CJK UNIFIED IDEOGRAPH
+	"92 DC	722A",	#CJK UNIFIED IDEOGRAPH
+	"92 DD	540A",	#CJK UNIFIED IDEOGRAPH
+	"92 DE	91E3",	#CJK UNIFIED IDEOGRAPH
+	"92 DF	9DB4",	#CJK UNIFIED IDEOGRAPH
+	"92 E0	4EAD",	#CJK UNIFIED IDEOGRAPH
+	"92 E1	4F4E",	#CJK UNIFIED IDEOGRAPH
+	"92 E2	505C",	#CJK UNIFIED IDEOGRAPH
+	"92 E3	5075",	#CJK UNIFIED IDEOGRAPH
+	"92 E4	5243",	#CJK UNIFIED IDEOGRAPH
+	"92 E5	8C9E",	#CJK UNIFIED IDEOGRAPH
+	"92 E6	5448",	#CJK UNIFIED IDEOGRAPH
+	"92 E7	5824",	#CJK UNIFIED IDEOGRAPH
+	"92 E8	5B9A",	#CJK UNIFIED IDEOGRAPH
+	"92 E9	5E1D",	#CJK UNIFIED IDEOGRAPH
+	"92 EA	5E95",	#CJK UNIFIED IDEOGRAPH
+	"92 EB	5EAD",	#CJK UNIFIED IDEOGRAPH
+	"92 EC	5EF7",	#CJK UNIFIED IDEOGRAPH
+	"92 ED	5F1F",	#CJK UNIFIED IDEOGRAPH
+	"92 EE	608C",	#CJK UNIFIED IDEOGRAPH
+	"92 EF	62B5",	#CJK UNIFIED IDEOGRAPH
+	"92 F0	633A",	#CJK UNIFIED IDEOGRAPH
+	"92 F1	63D0",	#CJK UNIFIED IDEOGRAPH
+	"92 F2	68AF",	#CJK UNIFIED IDEOGRAPH
+	"92 F3	6C40",	#CJK UNIFIED IDEOGRAPH
+	"92 F4	7887",	#CJK UNIFIED IDEOGRAPH
+	"92 F5	798E",	#CJK UNIFIED IDEOGRAPH
+	"92 F6	7A0B",	#CJK UNIFIED IDEOGRAPH
+	"92 F7	7DE0",	#CJK UNIFIED IDEOGRAPH
+	"92 F8	8247",	#CJK UNIFIED IDEOGRAPH
+	"92 F9	8A02",	#CJK UNIFIED IDEOGRAPH
+	"92 FA	8AE6",	#CJK UNIFIED IDEOGRAPH
+	"92 FB	8E44",	#CJK UNIFIED IDEOGRAPH
+	"92 FC	9013",	#CJK UNIFIED IDEOGRAPH
+	"93 40	90B8",	#CJK UNIFIED IDEOGRAPH
+	"93 41	912D",	#CJK UNIFIED IDEOGRAPH
+	"93 42	91D8",	#CJK UNIFIED IDEOGRAPH
+	"93 43	9F0E",	#CJK UNIFIED IDEOGRAPH
+	"93 44	6CE5",	#CJK UNIFIED IDEOGRAPH
+	"93 45	6458",	#CJK UNIFIED IDEOGRAPH
+	"93 46	64E2",	#CJK UNIFIED IDEOGRAPH
+	"93 47	6575",	#CJK UNIFIED IDEOGRAPH
+	"93 48	6EF4",	#CJK UNIFIED IDEOGRAPH
+	"93 49	7684",	#CJK UNIFIED IDEOGRAPH
+	"93 4A	7B1B",	#CJK UNIFIED IDEOGRAPH
+	"93 4B	9069",	#CJK UNIFIED IDEOGRAPH
+	"93 4C	93D1",	#CJK UNIFIED IDEOGRAPH
+	"93 4D	6EBA",	#CJK UNIFIED IDEOGRAPH
+	"93 4E	54F2",	#CJK UNIFIED IDEOGRAPH
+	"93 4F	5FB9",	#CJK UNIFIED IDEOGRAPH
+	"93 50	64A4",	#CJK UNIFIED IDEOGRAPH
+	"93 51	8F4D",	#CJK UNIFIED IDEOGRAPH
+	"93 52	8FED",	#CJK UNIFIED IDEOGRAPH
+	"93 53	9244",	#CJK UNIFIED IDEOGRAPH
+	"93 54	5178",	#CJK UNIFIED IDEOGRAPH
+	"93 55	586B",	#CJK UNIFIED IDEOGRAPH
+	"93 56	5929",	#CJK UNIFIED IDEOGRAPH
+	"93 57	5C55",	#CJK UNIFIED IDEOGRAPH
+	"93 58	5E97",	#CJK UNIFIED IDEOGRAPH
+	"93 59	6DFB",	#CJK UNIFIED IDEOGRAPH
+	"93 5A	7E8F",	#CJK UNIFIED IDEOGRAPH
+	"93 5B	751C",	#CJK UNIFIED IDEOGRAPH
+	"93 5C	8CBC",	#CJK UNIFIED IDEOGRAPH
+	"93 5D	8EE2",	#CJK UNIFIED IDEOGRAPH
+	"93 5E	985B",	#CJK UNIFIED IDEOGRAPH
+	"93 5F	70B9",	#CJK UNIFIED IDEOGRAPH
+	"93 60	4F1D",	#CJK UNIFIED IDEOGRAPH
+	"93 61	6BBF",	#CJK UNIFIED IDEOGRAPH
+	"93 62	6FB1",	#CJK UNIFIED IDEOGRAPH
+	"93 63	7530",	#CJK UNIFIED IDEOGRAPH
+	"93 64	96FB",	#CJK UNIFIED IDEOGRAPH
+	"93 65	514E",	#CJK UNIFIED IDEOGRAPH
+	"93 66	5410",	#CJK UNIFIED IDEOGRAPH
+	"93 67	5835",	#CJK UNIFIED IDEOGRAPH
+	"93 68	5857",	#CJK UNIFIED IDEOGRAPH
+	"93 69	59AC",	#CJK UNIFIED IDEOGRAPH
+	"93 6A	5C60",	#CJK UNIFIED IDEOGRAPH
+	"93 6B	5F92",	#CJK UNIFIED IDEOGRAPH
+	"93 6C	6597",	#CJK UNIFIED IDEOGRAPH
+	"93 6D	675C",	#CJK UNIFIED IDEOGRAPH
+	"93 6E	6E21",	#CJK UNIFIED IDEOGRAPH
+	"93 6F	767B",	#CJK UNIFIED IDEOGRAPH
+	"93 70	83DF",	#CJK UNIFIED IDEOGRAPH
+	"93 71	8CED",	#CJK UNIFIED IDEOGRAPH
+	"93 72	9014",	#CJK UNIFIED IDEOGRAPH
+	"93 73	90FD",	#CJK UNIFIED IDEOGRAPH
+	"93 74	934D",	#CJK UNIFIED IDEOGRAPH
+	"93 75	7825",	#CJK UNIFIED IDEOGRAPH
+	"93 76	783A",	#CJK UNIFIED IDEOGRAPH
+	"93 77	52AA",	#CJK UNIFIED IDEOGRAPH
+	"93 78	5EA6",	#CJK UNIFIED IDEOGRAPH
+	"93 79	571F",	#CJK UNIFIED IDEOGRAPH
+	"93 7A	5974",	#CJK UNIFIED IDEOGRAPH
+	"93 7B	6012",	#CJK UNIFIED IDEOGRAPH
+	"93 7C	5012",	#CJK UNIFIED IDEOGRAPH
+	"93 7D	515A",	#CJK UNIFIED IDEOGRAPH
+	"93 7E	51AC",	#CJK UNIFIED IDEOGRAPH
+	"93 80	51CD",	#CJK UNIFIED IDEOGRAPH
+	"93 81	5200",	#CJK UNIFIED IDEOGRAPH
+	"93 82	5510",	#CJK UNIFIED IDEOGRAPH
+	"93 83	5854",	#CJK UNIFIED IDEOGRAPH
+	"93 84	5858",	#CJK UNIFIED IDEOGRAPH
+	"93 85	5957",	#CJK UNIFIED IDEOGRAPH
+	"93 86	5B95",	#CJK UNIFIED IDEOGRAPH
+	"93 87	5CF6",	#CJK UNIFIED IDEOGRAPH
+	"93 88	5D8B",	#CJK UNIFIED IDEOGRAPH
+	"93 89	60BC",	#CJK UNIFIED IDEOGRAPH
+	"93 8A	6295",	#CJK UNIFIED IDEOGRAPH
+	"93 8B	642D",	#CJK UNIFIED IDEOGRAPH
+	"93 8C	6771",	#CJK UNIFIED IDEOGRAPH
+	"93 8D	6843",	#CJK UNIFIED IDEOGRAPH
+	"93 8E	68BC",	#CJK UNIFIED IDEOGRAPH
+	"93 8F	68DF",	#CJK UNIFIED IDEOGRAPH
+	"93 90	76D7",	#CJK UNIFIED IDEOGRAPH
+	"93 91	6DD8",	#CJK UNIFIED IDEOGRAPH
+	"93 92	6E6F",	#CJK UNIFIED IDEOGRAPH
+	"93 93	6D9B",	#CJK UNIFIED IDEOGRAPH
+	"93 94	706F",	#CJK UNIFIED IDEOGRAPH
+	"93 95	71C8",	#CJK UNIFIED IDEOGRAPH
+	"93 96	5F53",	#CJK UNIFIED IDEOGRAPH
+	"93 97	75D8",	#CJK UNIFIED IDEOGRAPH
+	"93 98	7977",	#CJK UNIFIED IDEOGRAPH
+	"93 99	7B49",	#CJK UNIFIED IDEOGRAPH
+	"93 9A	7B54",	#CJK UNIFIED IDEOGRAPH
+	"93 9B	7B52",	#CJK UNIFIED IDEOGRAPH
+	"93 9C	7CD6",	#CJK UNIFIED IDEOGRAPH
+	"93 9D	7D71",	#CJK UNIFIED IDEOGRAPH
+	"93 9E	5230",	#CJK UNIFIED IDEOGRAPH
+	"93 9F	8463",	#CJK UNIFIED IDEOGRAPH
+	"93 A0	8569",	#CJK UNIFIED IDEOGRAPH
+	"93 A1	85E4",	#CJK UNIFIED IDEOGRAPH
+	"93 A2	8A0E",	#CJK UNIFIED IDEOGRAPH
+	"93 A3	8B04",	#CJK UNIFIED IDEOGRAPH
+	"93 A4	8C46",	#CJK UNIFIED IDEOGRAPH
+	"93 A5	8E0F",	#CJK UNIFIED IDEOGRAPH
+	"93 A6	9003",	#CJK UNIFIED IDEOGRAPH
+	"93 A7	900F",	#CJK UNIFIED IDEOGRAPH
+	"93 A8	9419",	#CJK UNIFIED IDEOGRAPH
+	"93 A9	9676",	#CJK UNIFIED IDEOGRAPH
+	"93 AA	982D",	#CJK UNIFIED IDEOGRAPH
+	"93 AB	9A30",	#CJK UNIFIED IDEOGRAPH
+	"93 AC	95D8",	#CJK UNIFIED IDEOGRAPH
+	"93 AD	50CD",	#CJK UNIFIED IDEOGRAPH
+	"93 AE	52D5",	#CJK UNIFIED IDEOGRAPH
+	"93 AF	540C",	#CJK UNIFIED IDEOGRAPH
+	"93 B0	5802",	#CJK UNIFIED IDEOGRAPH
+	"93 B1	5C0E",	#CJK UNIFIED IDEOGRAPH
+	"93 B2	61A7",	#CJK UNIFIED IDEOGRAPH
+	"93 B3	649E",	#CJK UNIFIED IDEOGRAPH
+	"93 B4	6D1E",	#CJK UNIFIED IDEOGRAPH
+	"93 B5	77B3",	#CJK UNIFIED IDEOGRAPH
+	"93 B6	7AE5",	#CJK UNIFIED IDEOGRAPH
+	"93 B7	80F4",	#CJK UNIFIED IDEOGRAPH
+	"93 B8	8404",	#CJK UNIFIED IDEOGRAPH
+	"93 B9	9053",	#CJK UNIFIED IDEOGRAPH
+	"93 BA	9285",	#CJK UNIFIED IDEOGRAPH
+	"93 BB	5CE0",	#CJK UNIFIED IDEOGRAPH
+	"93 BC	9D07",	#CJK UNIFIED IDEOGRAPH
+	"93 BD	533F",	#CJK UNIFIED IDEOGRAPH
+	"93 BE	5F97",	#CJK UNIFIED IDEOGRAPH
+	"93 BF	5FB3",	#CJK UNIFIED IDEOGRAPH
+	"93 C0	6D9C",	#CJK UNIFIED IDEOGRAPH
+	"93 C1	7279",	#CJK UNIFIED IDEOGRAPH
+	"93 C2	7763",	#CJK UNIFIED IDEOGRAPH
+	"93 C3	79BF",	#CJK UNIFIED IDEOGRAPH
+	"93 C4	7BE4",	#CJK UNIFIED IDEOGRAPH
+	"93 C5	6BD2",	#CJK UNIFIED IDEOGRAPH
+	"93 C6	72EC",	#CJK UNIFIED IDEOGRAPH
+	"93 C7	8AAD",	#CJK UNIFIED IDEOGRAPH
+	"93 C8	6803",	#CJK UNIFIED IDEOGRAPH
+	"93 C9	6A61",	#CJK UNIFIED IDEOGRAPH
+	"93 CA	51F8",	#CJK UNIFIED IDEOGRAPH
+	"93 CB	7A81",	#CJK UNIFIED IDEOGRAPH
+	"93 CC	6934",	#CJK UNIFIED IDEOGRAPH
+	"93 CD	5C4A",	#CJK UNIFIED IDEOGRAPH
+	"93 CE	9CF6",	#CJK UNIFIED IDEOGRAPH
+	"93 CF	82EB",	#CJK UNIFIED IDEOGRAPH
+	"93 D0	5BC5",	#CJK UNIFIED IDEOGRAPH
+	"93 D1	9149",	#CJK UNIFIED IDEOGRAPH
+	"93 D2	701E",	#CJK UNIFIED IDEOGRAPH
+	"93 D3	5678",	#CJK UNIFIED IDEOGRAPH
+	"93 D4	5C6F",	#CJK UNIFIED IDEOGRAPH
+	"93 D5	60C7",	#CJK UNIFIED IDEOGRAPH
+	"93 D6	6566",	#CJK UNIFIED IDEOGRAPH
+	"93 D7	6C8C",	#CJK UNIFIED IDEOGRAPH
+	"93 D8	8C5A",	#CJK UNIFIED IDEOGRAPH
+	"93 D9	9041",	#CJK UNIFIED IDEOGRAPH
+	"93 DA	9813",	#CJK UNIFIED IDEOGRAPH
+	"93 DB	5451",	#CJK UNIFIED IDEOGRAPH
+	"93 DC	66C7",	#CJK UNIFIED IDEOGRAPH
+	"93 DD	920D",	#CJK UNIFIED IDEOGRAPH
+	"93 DE	5948",	#CJK UNIFIED IDEOGRAPH
+	"93 DF	90A3",	#CJK UNIFIED IDEOGRAPH
+	"93 E0	5185",	#CJK UNIFIED IDEOGRAPH
+	"93 E1	4E4D",	#CJK UNIFIED IDEOGRAPH
+	"93 E2	51EA",	#CJK UNIFIED IDEOGRAPH
+	"93 E3	8599",	#CJK UNIFIED IDEOGRAPH
+	"93 E4	8B0E",	#CJK UNIFIED IDEOGRAPH
+	"93 E5	7058",	#CJK UNIFIED IDEOGRAPH
+	"93 E6	637A",	#CJK UNIFIED IDEOGRAPH
+	"93 E7	934B",	#CJK UNIFIED IDEOGRAPH
+	"93 E8	6962",	#CJK UNIFIED IDEOGRAPH
+	"93 E9	99B4",	#CJK UNIFIED IDEOGRAPH
+	"93 EA	7E04",	#CJK UNIFIED IDEOGRAPH
+	"93 EB	7577",	#CJK UNIFIED IDEOGRAPH
+	"93 EC	5357",	#CJK UNIFIED IDEOGRAPH
+	"93 ED	6960",	#CJK UNIFIED IDEOGRAPH
+	"93 EE	8EDF",	#CJK UNIFIED IDEOGRAPH
+	"93 EF	96E3",	#CJK UNIFIED IDEOGRAPH
+	"93 F0	6C5D",	#CJK UNIFIED IDEOGRAPH
+	"93 F1	4E8C",	#CJK UNIFIED IDEOGRAPH
+	"93 F2	5C3C",	#CJK UNIFIED IDEOGRAPH
+	"93 F3	5F10",	#CJK UNIFIED IDEOGRAPH
+	"93 F4	8FE9",	#CJK UNIFIED IDEOGRAPH
+	"93 F5	5302",	#CJK UNIFIED IDEOGRAPH
+	"93 F6	8CD1",	#CJK UNIFIED IDEOGRAPH
+	"93 F7	8089",	#CJK UNIFIED IDEOGRAPH
+	"93 F8	8679",	#CJK UNIFIED IDEOGRAPH
+	"93 F9	5EFF",	#CJK UNIFIED IDEOGRAPH
+	"93 FA	65E5",	#CJK UNIFIED IDEOGRAPH
+	"93 FB	4E73",	#CJK UNIFIED IDEOGRAPH
+	"93 FC	5165",	#CJK UNIFIED IDEOGRAPH
+	"94 40	5982",	#CJK UNIFIED IDEOGRAPH
+	"94 41	5C3F",	#CJK UNIFIED IDEOGRAPH
+	"94 42	97EE",	#CJK UNIFIED IDEOGRAPH
+	"94 43	4EFB",	#CJK UNIFIED IDEOGRAPH
+	"94 44	598A",	#CJK UNIFIED IDEOGRAPH
+	"94 45	5FCD",	#CJK UNIFIED IDEOGRAPH
+	"94 46	8A8D",	#CJK UNIFIED IDEOGRAPH
+	"94 47	6FE1",	#CJK UNIFIED IDEOGRAPH
+	"94 48	79B0",	#CJK UNIFIED IDEOGRAPH
+	"94 49	7962",	#CJK UNIFIED IDEOGRAPH
+	"94 4A	5BE7",	#CJK UNIFIED IDEOGRAPH
+	"94 4B	8471",	#CJK UNIFIED IDEOGRAPH
+	"94 4C	732B",	#CJK UNIFIED IDEOGRAPH
+	"94 4D	71B1",	#CJK UNIFIED IDEOGRAPH
+	"94 4E	5E74",	#CJK UNIFIED IDEOGRAPH
+	"94 4F	5FF5",	#CJK UNIFIED IDEOGRAPH
+	"94 50	637B",	#CJK UNIFIED IDEOGRAPH
+	"94 51	649A",	#CJK UNIFIED IDEOGRAPH
+	"94 52	71C3",	#CJK UNIFIED IDEOGRAPH
+	"94 53	7C98",	#CJK UNIFIED IDEOGRAPH
+	"94 54	4E43",	#CJK UNIFIED IDEOGRAPH
+	"94 55	5EFC",	#CJK UNIFIED IDEOGRAPH
+	"94 56	4E4B",	#CJK UNIFIED IDEOGRAPH
+	"94 57	57DC",	#CJK UNIFIED IDEOGRAPH
+	"94 58	56A2",	#CJK UNIFIED IDEOGRAPH
+	"94 59	60A9",	#CJK UNIFIED IDEOGRAPH
+	"94 5A	6FC3",	#CJK UNIFIED IDEOGRAPH
+	"94 5B	7D0D",	#CJK UNIFIED IDEOGRAPH
+	"94 5C	80FD",	#CJK UNIFIED IDEOGRAPH
+	"94 5D	8133",	#CJK UNIFIED IDEOGRAPH
+	"94 5E	81BF",	#CJK UNIFIED IDEOGRAPH
+	"94 5F	8FB2",	#CJK UNIFIED IDEOGRAPH
+	"94 60	8997",	#CJK UNIFIED IDEOGRAPH
+	"94 61	86A4",	#CJK UNIFIED IDEOGRAPH
+	"94 62	5DF4",	#CJK UNIFIED IDEOGRAPH
+	"94 63	628A",	#CJK UNIFIED IDEOGRAPH
+	"94 64	64AD",	#CJK UNIFIED IDEOGRAPH
+	"94 65	8987",	#CJK UNIFIED IDEOGRAPH
+	"94 66	6777",	#CJK UNIFIED IDEOGRAPH
+	"94 67	6CE2",	#CJK UNIFIED IDEOGRAPH
+	"94 68	6D3E",	#CJK UNIFIED IDEOGRAPH
+	"94 69	7436",	#CJK UNIFIED IDEOGRAPH
+	"94 6A	7834",	#CJK UNIFIED IDEOGRAPH
+	"94 6B	5A46",	#CJK UNIFIED IDEOGRAPH
+	"94 6C	7F75",	#CJK UNIFIED IDEOGRAPH
+	"94 6D	82AD",	#CJK UNIFIED IDEOGRAPH
+	"94 6E	99AC",	#CJK UNIFIED IDEOGRAPH
+	"94 6F	4FF3",	#CJK UNIFIED IDEOGRAPH
+	"94 70	5EC3",	#CJK UNIFIED IDEOGRAPH
+	"94 71	62DD",	#CJK UNIFIED IDEOGRAPH
+	"94 72	6392",	#CJK UNIFIED IDEOGRAPH
+	"94 73	6557",	#CJK UNIFIED IDEOGRAPH
+	"94 74	676F",	#CJK UNIFIED IDEOGRAPH
+	"94 75	76C3",	#CJK UNIFIED IDEOGRAPH
+	"94 76	724C",	#CJK UNIFIED IDEOGRAPH
+	"94 77	80CC",	#CJK UNIFIED IDEOGRAPH
+	"94 78	80BA",	#CJK UNIFIED IDEOGRAPH
+	"94 79	8F29",	#CJK UNIFIED IDEOGRAPH
+	"94 7A	914D",	#CJK UNIFIED IDEOGRAPH
+	"94 7B	500D",	#CJK UNIFIED IDEOGRAPH
+	"94 7C	57F9",	#CJK UNIFIED IDEOGRAPH
+	"94 7D	5A92",	#CJK UNIFIED IDEOGRAPH
+	"94 7E	6885",	#CJK UNIFIED IDEOGRAPH
+	"94 80	6973",	#CJK UNIFIED IDEOGRAPH
+	"94 81	7164",	#CJK UNIFIED IDEOGRAPH
+	"94 82	72FD",	#CJK UNIFIED IDEOGRAPH
+	"94 83	8CB7",	#CJK UNIFIED IDEOGRAPH
+	"94 84	58F2",	#CJK UNIFIED IDEOGRAPH
+	"94 85	8CE0",	#CJK UNIFIED IDEOGRAPH
+	"94 86	966A",	#CJK UNIFIED IDEOGRAPH
+	"94 87	9019",	#CJK UNIFIED IDEOGRAPH
+	"94 88	877F",	#CJK UNIFIED IDEOGRAPH
+	"94 89	79E4",	#CJK UNIFIED IDEOGRAPH
+	"94 8A	77E7",	#CJK UNIFIED IDEOGRAPH
+	"94 8B	8429",	#CJK UNIFIED IDEOGRAPH
+	"94 8C	4F2F",	#CJK UNIFIED IDEOGRAPH
+	"94 8D	5265",	#CJK UNIFIED IDEOGRAPH
+	"94 8E	535A",	#CJK UNIFIED IDEOGRAPH
+	"94 8F	62CD",	#CJK UNIFIED IDEOGRAPH
+	"94 90	67CF",	#CJK UNIFIED IDEOGRAPH
+	"94 91	6CCA",	#CJK UNIFIED IDEOGRAPH
+	"94 92	767D",	#CJK UNIFIED IDEOGRAPH
+	"94 93	7B94",	#CJK UNIFIED IDEOGRAPH
+	"94 94	7C95",	#CJK UNIFIED IDEOGRAPH
+	"94 95	8236",	#CJK UNIFIED IDEOGRAPH
+	"94 96	8584",	#CJK UNIFIED IDEOGRAPH
+	"94 97	8FEB",	#CJK UNIFIED IDEOGRAPH
+	"94 98	66DD",	#CJK UNIFIED IDEOGRAPH
+	"94 99	6F20",	#CJK UNIFIED IDEOGRAPH
+	"94 9A	7206",	#CJK UNIFIED IDEOGRAPH
+	"94 9B	7E1B",	#CJK UNIFIED IDEOGRAPH
+	"94 9C	83AB",	#CJK UNIFIED IDEOGRAPH
+	"94 9D	99C1",	#CJK UNIFIED IDEOGRAPH
+	"94 9E	9EA6",	#CJK UNIFIED IDEOGRAPH
+	"94 9F	51FD",	#CJK UNIFIED IDEOGRAPH
+	"94 A0	7BB1",	#CJK UNIFIED IDEOGRAPH
+	"94 A1	7872",	#CJK UNIFIED IDEOGRAPH
+	"94 A2	7BB8",	#CJK UNIFIED IDEOGRAPH
+	"94 A3	8087",	#CJK UNIFIED IDEOGRAPH
+	"94 A4	7B48",	#CJK UNIFIED IDEOGRAPH
+	"94 A5	6AE8",	#CJK UNIFIED IDEOGRAPH
+	"94 A6	5E61",	#CJK UNIFIED IDEOGRAPH
+	"94 A7	808C",	#CJK UNIFIED IDEOGRAPH
+	"94 A8	7551",	#CJK UNIFIED IDEOGRAPH
+	"94 A9	7560",	#CJK UNIFIED IDEOGRAPH
+	"94 AA	516B",	#CJK UNIFIED IDEOGRAPH
+	"94 AB	9262",	#CJK UNIFIED IDEOGRAPH
+	"94 AC	6E8C",	#CJK UNIFIED IDEOGRAPH
+	"94 AD	767A",	#CJK UNIFIED IDEOGRAPH
+	"94 AE	9197",	#CJK UNIFIED IDEOGRAPH
+	"94 AF	9AEA",	#CJK UNIFIED IDEOGRAPH
+	"94 B0	4F10",	#CJK UNIFIED IDEOGRAPH
+	"94 B1	7F70",	#CJK UNIFIED IDEOGRAPH
+	"94 B2	629C",	#CJK UNIFIED IDEOGRAPH
+	"94 B3	7B4F",	#CJK UNIFIED IDEOGRAPH
+	"94 B4	95A5",	#CJK UNIFIED IDEOGRAPH
+	"94 B5	9CE9",	#CJK UNIFIED IDEOGRAPH
+	"94 B6	567A",	#CJK UNIFIED IDEOGRAPH
+	"94 B7	5859",	#CJK UNIFIED IDEOGRAPH
+	"94 B8	86E4",	#CJK UNIFIED IDEOGRAPH
+	"94 B9	96BC",	#CJK UNIFIED IDEOGRAPH
+	"94 BA	4F34",	#CJK UNIFIED IDEOGRAPH
+	"94 BB	5224",	#CJK UNIFIED IDEOGRAPH
+	"94 BC	534A",	#CJK UNIFIED IDEOGRAPH
+	"94 BD	53CD",	#CJK UNIFIED IDEOGRAPH
+	"94 BE	53DB",	#CJK UNIFIED IDEOGRAPH
+	"94 BF	5E06",	#CJK UNIFIED IDEOGRAPH
+	"94 C0	642C",	#CJK UNIFIED IDEOGRAPH
+	"94 C1	6591",	#CJK UNIFIED IDEOGRAPH
+	"94 C2	677F",	#CJK UNIFIED IDEOGRAPH
+	"94 C3	6C3E",	#CJK UNIFIED IDEOGRAPH
+	"94 C4	6C4E",	#CJK UNIFIED IDEOGRAPH
+	"94 C5	7248",	#CJK UNIFIED IDEOGRAPH
+	"94 C6	72AF",	#CJK UNIFIED IDEOGRAPH
+	"94 C7	73ED",	#CJK UNIFIED IDEOGRAPH
+	"94 C8	7554",	#CJK UNIFIED IDEOGRAPH
+	"94 C9	7E41",	#CJK UNIFIED IDEOGRAPH
+	"94 CA	822C",	#CJK UNIFIED IDEOGRAPH
+	"94 CB	85E9",	#CJK UNIFIED IDEOGRAPH
+	"94 CC	8CA9",	#CJK UNIFIED IDEOGRAPH
+	"94 CD	7BC4",	#CJK UNIFIED IDEOGRAPH
+	"94 CE	91C6",	#CJK UNIFIED IDEOGRAPH
+	"94 CF	7169",	#CJK UNIFIED IDEOGRAPH
+	"94 D0	9812",	#CJK UNIFIED IDEOGRAPH
+	"94 D1	98EF",	#CJK UNIFIED IDEOGRAPH
+	"94 D2	633D",	#CJK UNIFIED IDEOGRAPH
+	"94 D3	6669",	#CJK UNIFIED IDEOGRAPH
+	"94 D4	756A",	#CJK UNIFIED IDEOGRAPH
+	"94 D5	76E4",	#CJK UNIFIED IDEOGRAPH
+	"94 D6	78D0",	#CJK UNIFIED IDEOGRAPH
+	"94 D7	8543",	#CJK UNIFIED IDEOGRAPH
+	"94 D8	86EE",	#CJK UNIFIED IDEOGRAPH
+	"94 D9	532A",	#CJK UNIFIED IDEOGRAPH
+	"94 DA	5351",	#CJK UNIFIED IDEOGRAPH
+	"94 DB	5426",	#CJK UNIFIED IDEOGRAPH
+	"94 DC	5983",	#CJK UNIFIED IDEOGRAPH
+	"94 DD	5E87",	#CJK UNIFIED IDEOGRAPH
+	"94 DE	5F7C",	#CJK UNIFIED IDEOGRAPH
+	"94 DF	60B2",	#CJK UNIFIED IDEOGRAPH
+	"94 E0	6249",	#CJK UNIFIED IDEOGRAPH
+	"94 E1	6279",	#CJK UNIFIED IDEOGRAPH
+	"94 E2	62AB",	#CJK UNIFIED IDEOGRAPH
+	"94 E3	6590",	#CJK UNIFIED IDEOGRAPH
+	"94 E4	6BD4",	#CJK UNIFIED IDEOGRAPH
+	"94 E5	6CCC",	#CJK UNIFIED IDEOGRAPH
+	"94 E6	75B2",	#CJK UNIFIED IDEOGRAPH
+	"94 E7	76AE",	#CJK UNIFIED IDEOGRAPH
+	"94 E8	7891",	#CJK UNIFIED IDEOGRAPH
+	"94 E9	79D8",	#CJK UNIFIED IDEOGRAPH
+	"94 EA	7DCB",	#CJK UNIFIED IDEOGRAPH
+	"94 EB	7F77",	#CJK UNIFIED IDEOGRAPH
+	"94 EC	80A5",	#CJK UNIFIED IDEOGRAPH
+	"94 ED	88AB",	#CJK UNIFIED IDEOGRAPH
+	"94 EE	8AB9",	#CJK UNIFIED IDEOGRAPH
+	"94 EF	8CBB",	#CJK UNIFIED IDEOGRAPH
+	"94 F0	907F",	#CJK UNIFIED IDEOGRAPH
+	"94 F1	975E",	#CJK UNIFIED IDEOGRAPH
+	"94 F2	98DB",	#CJK UNIFIED IDEOGRAPH
+	"94 F3	6A0B",	#CJK UNIFIED IDEOGRAPH
+	"94 F4	7C38",	#CJK UNIFIED IDEOGRAPH
+	"94 F5	5099",	#CJK UNIFIED IDEOGRAPH
+	"94 F6	5C3E",	#CJK UNIFIED IDEOGRAPH
+	"94 F7	5FAE",	#CJK UNIFIED IDEOGRAPH
+	"94 F8	6787",	#CJK UNIFIED IDEOGRAPH
+	"94 F9	6BD8",	#CJK UNIFIED IDEOGRAPH
+	"94 FA	7435",	#CJK UNIFIED IDEOGRAPH
+	"94 FB	7709",	#CJK UNIFIED IDEOGRAPH
+	"94 FC	7F8E",	#CJK UNIFIED IDEOGRAPH
+	"95 40	9F3B",	#CJK UNIFIED IDEOGRAPH
+	"95 41	67CA",	#CJK UNIFIED IDEOGRAPH
+	"95 42	7A17",	#CJK UNIFIED IDEOGRAPH
+	"95 43	5339",	#CJK UNIFIED IDEOGRAPH
+	"95 44	758B",	#CJK UNIFIED IDEOGRAPH
+	"95 45	9AED",	#CJK UNIFIED IDEOGRAPH
+	"95 46	5F66",	#CJK UNIFIED IDEOGRAPH
+	"95 47	819D",	#CJK UNIFIED IDEOGRAPH
+	"95 48	83F1",	#CJK UNIFIED IDEOGRAPH
+	"95 49	8098",	#CJK UNIFIED IDEOGRAPH
+	"95 4A	5F3C",	#CJK UNIFIED IDEOGRAPH
+	"95 4B	5FC5",	#CJK UNIFIED IDEOGRAPH
+	"95 4C	7562",	#CJK UNIFIED IDEOGRAPH
+	"95 4D	7B46",	#CJK UNIFIED IDEOGRAPH
+	"95 4E	903C",	#CJK UNIFIED IDEOGRAPH
+	"95 4F	6867",	#CJK UNIFIED IDEOGRAPH
+	"95 50	59EB",	#CJK UNIFIED IDEOGRAPH
+	"95 51	5A9B",	#CJK UNIFIED IDEOGRAPH
+	"95 52	7D10",	#CJK UNIFIED IDEOGRAPH
+	"95 53	767E",	#CJK UNIFIED IDEOGRAPH
+	"95 54	8B2C",	#CJK UNIFIED IDEOGRAPH
+	"95 55	4FF5",	#CJK UNIFIED IDEOGRAPH
+	"95 56	5F6A",	#CJK UNIFIED IDEOGRAPH
+	"95 57	6A19",	#CJK UNIFIED IDEOGRAPH
+	"95 58	6C37",	#CJK UNIFIED IDEOGRAPH
+	"95 59	6F02",	#CJK UNIFIED IDEOGRAPH
+	"95 5A	74E2",	#CJK UNIFIED IDEOGRAPH
+	"95 5B	7968",	#CJK UNIFIED IDEOGRAPH
+	"95 5C	8868",	#CJK UNIFIED IDEOGRAPH
+	"95 5D	8A55",	#CJK UNIFIED IDEOGRAPH
+	"95 5E	8C79",	#CJK UNIFIED IDEOGRAPH
+	"95 5F	5EDF",	#CJK UNIFIED IDEOGRAPH
+	"95 60	63CF",	#CJK UNIFIED IDEOGRAPH
+	"95 61	75C5",	#CJK UNIFIED IDEOGRAPH
+	"95 62	79D2",	#CJK UNIFIED IDEOGRAPH
+	"95 63	82D7",	#CJK UNIFIED IDEOGRAPH
+	"95 64	9328",	#CJK UNIFIED IDEOGRAPH
+	"95 65	92F2",	#CJK UNIFIED IDEOGRAPH
+	"95 66	849C",	#CJK UNIFIED IDEOGRAPH
+	"95 67	86ED",	#CJK UNIFIED IDEOGRAPH
+	"95 68	9C2D",	#CJK UNIFIED IDEOGRAPH
+	"95 69	54C1",	#CJK UNIFIED IDEOGRAPH
+	"95 6A	5F6C",	#CJK UNIFIED IDEOGRAPH
+	"95 6B	658C",	#CJK UNIFIED IDEOGRAPH
+	"95 6C	6D5C",	#CJK UNIFIED IDEOGRAPH
+	"95 6D	7015",	#CJK UNIFIED IDEOGRAPH
+	"95 6E	8CA7",	#CJK UNIFIED IDEOGRAPH
+	"95 6F	8CD3",	#CJK UNIFIED IDEOGRAPH
+	"95 70	983B",	#CJK UNIFIED IDEOGRAPH
+	"95 71	654F",	#CJK UNIFIED IDEOGRAPH
+	"95 72	74F6",	#CJK UNIFIED IDEOGRAPH
+	"95 73	4E0D",	#CJK UNIFIED IDEOGRAPH
+	"95 74	4ED8",	#CJK UNIFIED IDEOGRAPH
+	"95 75	57E0",	#CJK UNIFIED IDEOGRAPH
+	"95 76	592B",	#CJK UNIFIED IDEOGRAPH
+	"95 77	5A66",	#CJK UNIFIED IDEOGRAPH
+	"95 78	5BCC",	#CJK UNIFIED IDEOGRAPH
+	"95 79	51A8",	#CJK UNIFIED IDEOGRAPH
+	"95 7A	5E03",	#CJK UNIFIED IDEOGRAPH
+	"95 7B	5E9C",	#CJK UNIFIED IDEOGRAPH
+	"95 7C	6016",	#CJK UNIFIED IDEOGRAPH
+	"95 7D	6276",	#CJK UNIFIED IDEOGRAPH
+	"95 7E	6577",	#CJK UNIFIED IDEOGRAPH
+	"95 80	65A7",	#CJK UNIFIED IDEOGRAPH
+	"95 81	666E",	#CJK UNIFIED IDEOGRAPH
+	"95 82	6D6E",	#CJK UNIFIED IDEOGRAPH
+	"95 83	7236",	#CJK UNIFIED IDEOGRAPH
+	"95 84	7B26",	#CJK UNIFIED IDEOGRAPH
+	"95 85	8150",	#CJK UNIFIED IDEOGRAPH
+	"95 86	819A",	#CJK UNIFIED IDEOGRAPH
+	"95 87	8299",	#CJK UNIFIED IDEOGRAPH
+	"95 88	8B5C",	#CJK UNIFIED IDEOGRAPH
+	"95 89	8CA0",	#CJK UNIFIED IDEOGRAPH
+	"95 8A	8CE6",	#CJK UNIFIED IDEOGRAPH
+	"95 8B	8D74",	#CJK UNIFIED IDEOGRAPH
+	"95 8C	961C",	#CJK UNIFIED IDEOGRAPH
+	"95 8D	9644",	#CJK UNIFIED IDEOGRAPH
+	"95 8E	4FAE",	#CJK UNIFIED IDEOGRAPH
+	"95 8F	64AB",	#CJK UNIFIED IDEOGRAPH
+	"95 90	6B66",	#CJK UNIFIED IDEOGRAPH
+	"95 91	821E",	#CJK UNIFIED IDEOGRAPH
+	"95 92	8461",	#CJK UNIFIED IDEOGRAPH
+	"95 93	856A",	#CJK UNIFIED IDEOGRAPH
+	"95 94	90E8",	#CJK UNIFIED IDEOGRAPH
+	"95 95	5C01",	#CJK UNIFIED IDEOGRAPH
+	"95 96	6953",	#CJK UNIFIED IDEOGRAPH
+	"95 97	98A8",	#CJK UNIFIED IDEOGRAPH
+	"95 98	847A",	#CJK UNIFIED IDEOGRAPH
+	"95 99	8557",	#CJK UNIFIED IDEOGRAPH
+	"95 9A	4F0F",	#CJK UNIFIED IDEOGRAPH
+	"95 9B	526F",	#CJK UNIFIED IDEOGRAPH
+	"95 9C	5FA9",	#CJK UNIFIED IDEOGRAPH
+	"95 9D	5E45",	#CJK UNIFIED IDEOGRAPH
+	"95 9E	670D",	#CJK UNIFIED IDEOGRAPH
+	"95 9F	798F",	#CJK UNIFIED IDEOGRAPH
+	"95 A0	8179",	#CJK UNIFIED IDEOGRAPH
+	"95 A1	8907",	#CJK UNIFIED IDEOGRAPH
+	"95 A2	8986",	#CJK UNIFIED IDEOGRAPH
+	"95 A3	6DF5",	#CJK UNIFIED IDEOGRAPH
+	"95 A4	5F17",	#CJK UNIFIED IDEOGRAPH
+	"95 A5	6255",	#CJK UNIFIED IDEOGRAPH
+	"95 A6	6CB8",	#CJK UNIFIED IDEOGRAPH
+	"95 A7	4ECF",	#CJK UNIFIED IDEOGRAPH
+	"95 A8	7269",	#CJK UNIFIED IDEOGRAPH
+	"95 A9	9B92",	#CJK UNIFIED IDEOGRAPH
+	"95 AA	5206",	#CJK UNIFIED IDEOGRAPH
+	"95 AB	543B",	#CJK UNIFIED IDEOGRAPH
+	"95 AC	5674",	#CJK UNIFIED IDEOGRAPH
+	"95 AD	58B3",	#CJK UNIFIED IDEOGRAPH
+	"95 AE	61A4",	#CJK UNIFIED IDEOGRAPH
+	"95 AF	626E",	#CJK UNIFIED IDEOGRAPH
+	"95 B0	711A",	#CJK UNIFIED IDEOGRAPH
+	"95 B1	596E",	#CJK UNIFIED IDEOGRAPH
+	"95 B2	7C89",	#CJK UNIFIED IDEOGRAPH
+	"95 B3	7CDE",	#CJK UNIFIED IDEOGRAPH
+	"95 B4	7D1B",	#CJK UNIFIED IDEOGRAPH
+	"95 B5	96F0",	#CJK UNIFIED IDEOGRAPH
+	"95 B6	6587",	#CJK UNIFIED IDEOGRAPH
+	"95 B7	805E",	#CJK UNIFIED IDEOGRAPH
+	"95 B8	4E19",	#CJK UNIFIED IDEOGRAPH
+	"95 B9	4F75",	#CJK UNIFIED IDEOGRAPH
+	"95 BA	5175",	#CJK UNIFIED IDEOGRAPH
+	"95 BB	5840",	#CJK UNIFIED IDEOGRAPH
+	"95 BC	5E63",	#CJK UNIFIED IDEOGRAPH
+	"95 BD	5E73",	#CJK UNIFIED IDEOGRAPH
+	"95 BE	5F0A",	#CJK UNIFIED IDEOGRAPH
+	"95 BF	67C4",	#CJK UNIFIED IDEOGRAPH
+	"95 C0	4E26",	#CJK UNIFIED IDEOGRAPH
+	"95 C1	853D",	#CJK UNIFIED IDEOGRAPH
+	"95 C2	9589",	#CJK UNIFIED IDEOGRAPH
+	"95 C3	965B",	#CJK UNIFIED IDEOGRAPH
+	"95 C4	7C73",	#CJK UNIFIED IDEOGRAPH
+	"95 C5	9801",	#CJK UNIFIED IDEOGRAPH
+	"95 C6	50FB",	#CJK UNIFIED IDEOGRAPH
+	"95 C7	58C1",	#CJK UNIFIED IDEOGRAPH
+	"95 C8	7656",	#CJK UNIFIED IDEOGRAPH
+	"95 C9	78A7",	#CJK UNIFIED IDEOGRAPH
+	"95 CA	5225",	#CJK UNIFIED IDEOGRAPH
+	"95 CB	77A5",	#CJK UNIFIED IDEOGRAPH
+	"95 CC	8511",	#CJK UNIFIED IDEOGRAPH
+	"95 CD	7B86",	#CJK UNIFIED IDEOGRAPH
+	"95 CE	504F",	#CJK UNIFIED IDEOGRAPH
+	"95 CF	5909",	#CJK UNIFIED IDEOGRAPH
+	"95 D0	7247",	#CJK UNIFIED IDEOGRAPH
+	"95 D1	7BC7",	#CJK UNIFIED IDEOGRAPH
+	"95 D2	7DE8",	#CJK UNIFIED IDEOGRAPH
+	"95 D3	8FBA",	#CJK UNIFIED IDEOGRAPH
+	"95 D4	8FD4",	#CJK UNIFIED IDEOGRAPH
+	"95 D5	904D",	#CJK UNIFIED IDEOGRAPH
+	"95 D6	4FBF",	#CJK UNIFIED IDEOGRAPH
+	"95 D7	52C9",	#CJK UNIFIED IDEOGRAPH
+	"95 D8	5A29",	#CJK UNIFIED IDEOGRAPH
+	"95 D9	5F01",	#CJK UNIFIED IDEOGRAPH
+	"95 DA	97AD",	#CJK UNIFIED IDEOGRAPH
+	"95 DB	4FDD",	#CJK UNIFIED IDEOGRAPH
+	"95 DC	8217",	#CJK UNIFIED IDEOGRAPH
+	"95 DD	92EA",	#CJK UNIFIED IDEOGRAPH
+	"95 DE	5703",	#CJK UNIFIED IDEOGRAPH
+	"95 DF	6355",	#CJK UNIFIED IDEOGRAPH
+	"95 E0	6B69",	#CJK UNIFIED IDEOGRAPH
+	"95 E1	752B",	#CJK UNIFIED IDEOGRAPH
+	"95 E2	88DC",	#CJK UNIFIED IDEOGRAPH
+	"95 E3	8F14",	#CJK UNIFIED IDEOGRAPH
+	"95 E4	7A42",	#CJK UNIFIED IDEOGRAPH
+	"95 E5	52DF",	#CJK UNIFIED IDEOGRAPH
+	"95 E6	5893",	#CJK UNIFIED IDEOGRAPH
+	"95 E7	6155",	#CJK UNIFIED IDEOGRAPH
+	"95 E8	620A",	#CJK UNIFIED IDEOGRAPH
+	"95 E9	66AE",	#CJK UNIFIED IDEOGRAPH
+	"95 EA	6BCD",	#CJK UNIFIED IDEOGRAPH
+	"95 EB	7C3F",	#CJK UNIFIED IDEOGRAPH
+	"95 EC	83E9",	#CJK UNIFIED IDEOGRAPH
+	"95 ED	5023",	#CJK UNIFIED IDEOGRAPH
+	"95 EE	4FF8",	#CJK UNIFIED IDEOGRAPH
+	"95 EF	5305",	#CJK UNIFIED IDEOGRAPH
+	"95 F0	5446",	#CJK UNIFIED IDEOGRAPH
+	"95 F1	5831",	#CJK UNIFIED IDEOGRAPH
+	"95 F2	5949",	#CJK UNIFIED IDEOGRAPH
+	"95 F3	5B9D",	#CJK UNIFIED IDEOGRAPH
+	"95 F4	5CF0",	#CJK UNIFIED IDEOGRAPH
+	"95 F5	5CEF",	#CJK UNIFIED IDEOGRAPH
+	"95 F6	5D29",	#CJK UNIFIED IDEOGRAPH
+	"95 F7	5E96",	#CJK UNIFIED IDEOGRAPH
+	"95 F8	62B1",	#CJK UNIFIED IDEOGRAPH
+	"95 F9	6367",	#CJK UNIFIED IDEOGRAPH
+	"95 FA	653E",	#CJK UNIFIED IDEOGRAPH
+	"95 FB	65B9",	#CJK UNIFIED IDEOGRAPH
+	"95 FC	670B",	#CJK UNIFIED IDEOGRAPH
+	"96 40	6CD5",	#CJK UNIFIED IDEOGRAPH
+	"96 41	6CE1",	#CJK UNIFIED IDEOGRAPH
+	"96 42	70F9",	#CJK UNIFIED IDEOGRAPH
+	"96 43	7832",	#CJK UNIFIED IDEOGRAPH
+	"96 44	7E2B",	#CJK UNIFIED IDEOGRAPH
+	"96 45	80DE",	#CJK UNIFIED IDEOGRAPH
+	"96 46	82B3",	#CJK UNIFIED IDEOGRAPH
+	"96 47	840C",	#CJK UNIFIED IDEOGRAPH
+	"96 48	84EC",	#CJK UNIFIED IDEOGRAPH
+	"96 49	8702",	#CJK UNIFIED IDEOGRAPH
+	"96 4A	8912",	#CJK UNIFIED IDEOGRAPH
+	"96 4B	8A2A",	#CJK UNIFIED IDEOGRAPH
+	"96 4C	8C4A",	#CJK UNIFIED IDEOGRAPH
+	"96 4D	90A6",	#CJK UNIFIED IDEOGRAPH
+	"96 4E	92D2",	#CJK UNIFIED IDEOGRAPH
+	"96 4F	98FD",	#CJK UNIFIED IDEOGRAPH
+	"96 50	9CF3",	#CJK UNIFIED IDEOGRAPH
+	"96 51	9D6C",	#CJK UNIFIED IDEOGRAPH
+	"96 52	4E4F",	#CJK UNIFIED IDEOGRAPH
+	"96 53	4EA1",	#CJK UNIFIED IDEOGRAPH
+	"96 54	508D",	#CJK UNIFIED IDEOGRAPH
+	"96 55	5256",	#CJK UNIFIED IDEOGRAPH
+	"96 56	574A",	#CJK UNIFIED IDEOGRAPH
+	"96 57	59A8",	#CJK UNIFIED IDEOGRAPH
+	"96 58	5E3D",	#CJK UNIFIED IDEOGRAPH
+	"96 59	5FD8",	#CJK UNIFIED IDEOGRAPH
+	"96 5A	5FD9",	#CJK UNIFIED IDEOGRAPH
+	"96 5B	623F",	#CJK UNIFIED IDEOGRAPH
+	"96 5C	66B4",	#CJK UNIFIED IDEOGRAPH
+	"96 5D	671B",	#CJK UNIFIED IDEOGRAPH
+	"96 5E	67D0",	#CJK UNIFIED IDEOGRAPH
+	"96 5F	68D2",	#CJK UNIFIED IDEOGRAPH
+	"96 60	5192",	#CJK UNIFIED IDEOGRAPH
+	"96 61	7D21",	#CJK UNIFIED IDEOGRAPH
+	"96 62	80AA",	#CJK UNIFIED IDEOGRAPH
+	"96 63	81A8",	#CJK UNIFIED IDEOGRAPH
+	"96 64	8B00",	#CJK UNIFIED IDEOGRAPH
+	"96 65	8C8C",	#CJK UNIFIED IDEOGRAPH
+	"96 66	8CBF",	#CJK UNIFIED IDEOGRAPH
+	"96 67	927E",	#CJK UNIFIED IDEOGRAPH
+	"96 68	9632",	#CJK UNIFIED IDEOGRAPH
+	"96 69	5420",	#CJK UNIFIED IDEOGRAPH
+	"96 6A	982C",	#CJK UNIFIED IDEOGRAPH
+	"96 6B	5317",	#CJK UNIFIED IDEOGRAPH
+	"96 6C	50D5",	#CJK UNIFIED IDEOGRAPH
+	"96 6D	535C",	#CJK UNIFIED IDEOGRAPH
+	"96 6E	58A8",	#CJK UNIFIED IDEOGRAPH
+	"96 6F	64B2",	#CJK UNIFIED IDEOGRAPH
+	"96 70	6734",	#CJK UNIFIED IDEOGRAPH
+	"96 71	7267",	#CJK UNIFIED IDEOGRAPH
+	"96 72	7766",	#CJK UNIFIED IDEOGRAPH
+	"96 73	7A46",	#CJK UNIFIED IDEOGRAPH
+	"96 74	91E6",	#CJK UNIFIED IDEOGRAPH
+	"96 75	52C3",	#CJK UNIFIED IDEOGRAPH
+	"96 76	6CA1",	#CJK UNIFIED IDEOGRAPH
+	"96 77	6B86",	#CJK UNIFIED IDEOGRAPH
+	"96 78	5800",	#CJK UNIFIED IDEOGRAPH
+	"96 79	5E4C",	#CJK UNIFIED IDEOGRAPH
+	"96 7A	5954",	#CJK UNIFIED IDEOGRAPH
+	"96 7B	672C",	#CJK UNIFIED IDEOGRAPH
+	"96 7C	7FFB",	#CJK UNIFIED IDEOGRAPH
+	"96 7D	51E1",	#CJK UNIFIED IDEOGRAPH
+	"96 7E	76C6",	#CJK UNIFIED IDEOGRAPH
+	"96 80	6469",	#CJK UNIFIED IDEOGRAPH
+	"96 81	78E8",	#CJK UNIFIED IDEOGRAPH
+	"96 82	9B54",	#CJK UNIFIED IDEOGRAPH
+	"96 83	9EBB",	#CJK UNIFIED IDEOGRAPH
+	"96 84	57CB",	#CJK UNIFIED IDEOGRAPH
+	"96 85	59B9",	#CJK UNIFIED IDEOGRAPH
+	"96 86	6627",	#CJK UNIFIED IDEOGRAPH
+	"96 87	679A",	#CJK UNIFIED IDEOGRAPH
+	"96 88	6BCE",	#CJK UNIFIED IDEOGRAPH
+	"96 89	54E9",	#CJK UNIFIED IDEOGRAPH
+	"96 8A	69D9",	#CJK UNIFIED IDEOGRAPH
+	"96 8B	5E55",	#CJK UNIFIED IDEOGRAPH
+	"96 8C	819C",	#CJK UNIFIED IDEOGRAPH
+	"96 8D	6795",	#CJK UNIFIED IDEOGRAPH
+	"96 8E	9BAA",	#CJK UNIFIED IDEOGRAPH
+	"96 8F	67FE",	#CJK UNIFIED IDEOGRAPH
+	"96 90	9C52",	#CJK UNIFIED IDEOGRAPH
+	"96 91	685D",	#CJK UNIFIED IDEOGRAPH
+	"96 92	4EA6",	#CJK UNIFIED IDEOGRAPH
+	"96 93	4FE3",	#CJK UNIFIED IDEOGRAPH
+	"96 94	53C8",	#CJK UNIFIED IDEOGRAPH
+	"96 95	62B9",	#CJK UNIFIED IDEOGRAPH
+	"96 96	672B",	#CJK UNIFIED IDEOGRAPH
+	"96 97	6CAB",	#CJK UNIFIED IDEOGRAPH
+	"96 98	8FC4",	#CJK UNIFIED IDEOGRAPH
+	"96 99	4FAD",	#CJK UNIFIED IDEOGRAPH
+	"96 9A	7E6D",	#CJK UNIFIED IDEOGRAPH
+	"96 9B	9EBF",	#CJK UNIFIED IDEOGRAPH
+	"96 9C	4E07",	#CJK UNIFIED IDEOGRAPH
+	"96 9D	6162",	#CJK UNIFIED IDEOGRAPH
+	"96 9E	6E80",	#CJK UNIFIED IDEOGRAPH
+	"96 9F	6F2B",	#CJK UNIFIED IDEOGRAPH
+	"96 A0	8513",	#CJK UNIFIED IDEOGRAPH
+	"96 A1	5473",	#CJK UNIFIED IDEOGRAPH
+	"96 A2	672A",	#CJK UNIFIED IDEOGRAPH
+	"96 A3	9B45",	#CJK UNIFIED IDEOGRAPH
+	"96 A4	5DF3",	#CJK UNIFIED IDEOGRAPH
+	"96 A5	7B95",	#CJK UNIFIED IDEOGRAPH
+	"96 A6	5CAC",	#CJK UNIFIED IDEOGRAPH
+	"96 A7	5BC6",	#CJK UNIFIED IDEOGRAPH
+	"96 A8	871C",	#CJK UNIFIED IDEOGRAPH
+	"96 A9	6E4A",	#CJK UNIFIED IDEOGRAPH
+	"96 AA	84D1",	#CJK UNIFIED IDEOGRAPH
+	"96 AB	7A14",	#CJK UNIFIED IDEOGRAPH
+	"96 AC	8108",	#CJK UNIFIED IDEOGRAPH
+	"96 AD	5999",	#CJK UNIFIED IDEOGRAPH
+	"96 AE	7C8D",	#CJK UNIFIED IDEOGRAPH
+	"96 AF	6C11",	#CJK UNIFIED IDEOGRAPH
+	"96 B0	7720",	#CJK UNIFIED IDEOGRAPH
+	"96 B1	52D9",	#CJK UNIFIED IDEOGRAPH
+	"96 B2	5922",	#CJK UNIFIED IDEOGRAPH
+	"96 B3	7121",	#CJK UNIFIED IDEOGRAPH
+	"96 B4	725F",	#CJK UNIFIED IDEOGRAPH
+	"96 B5	77DB",	#CJK UNIFIED IDEOGRAPH
+	"96 B6	9727",	#CJK UNIFIED IDEOGRAPH
+	"96 B7	9D61",	#CJK UNIFIED IDEOGRAPH
+	"96 B8	690B",	#CJK UNIFIED IDEOGRAPH
+	"96 B9	5A7F",	#CJK UNIFIED IDEOGRAPH
+	"96 BA	5A18",	#CJK UNIFIED IDEOGRAPH
+	"96 BB	51A5",	#CJK UNIFIED IDEOGRAPH
+	"96 BC	540D",	#CJK UNIFIED IDEOGRAPH
+	"96 BD	547D",	#CJK UNIFIED IDEOGRAPH
+	"96 BE	660E",	#CJK UNIFIED IDEOGRAPH
+	"96 BF	76DF",	#CJK UNIFIED IDEOGRAPH
+	"96 C0	8FF7",	#CJK UNIFIED IDEOGRAPH
+	"96 C1	9298",	#CJK UNIFIED IDEOGRAPH
+	"96 C2	9CF4",	#CJK UNIFIED IDEOGRAPH
+	"96 C3	59EA",	#CJK UNIFIED IDEOGRAPH
+	"96 C4	725D",	#CJK UNIFIED IDEOGRAPH
+	"96 C5	6EC5",	#CJK UNIFIED IDEOGRAPH
+	"96 C6	514D",	#CJK UNIFIED IDEOGRAPH
+	"96 C7	68C9",	#CJK UNIFIED IDEOGRAPH
+	"96 C8	7DBF",	#CJK UNIFIED IDEOGRAPH
+	"96 C9	7DEC",	#CJK UNIFIED IDEOGRAPH
+	"96 CA	9762",	#CJK UNIFIED IDEOGRAPH
+	"96 CB	9EBA",	#CJK UNIFIED IDEOGRAPH
+	"96 CC	6478",	#CJK UNIFIED IDEOGRAPH
+	"96 CD	6A21",	#CJK UNIFIED IDEOGRAPH
+	"96 CE	8302",	#CJK UNIFIED IDEOGRAPH
+	"96 CF	5984",	#CJK UNIFIED IDEOGRAPH
+	"96 D0	5B5F",	#CJK UNIFIED IDEOGRAPH
+	"96 D1	6BDB",	#CJK UNIFIED IDEOGRAPH
+	"96 D2	731B",	#CJK UNIFIED IDEOGRAPH
+	"96 D3	76F2",	#CJK UNIFIED IDEOGRAPH
+	"96 D4	7DB2",	#CJK UNIFIED IDEOGRAPH
+	"96 D5	8017",	#CJK UNIFIED IDEOGRAPH
+	"96 D6	8499",	#CJK UNIFIED IDEOGRAPH
+	"96 D7	5132",	#CJK UNIFIED IDEOGRAPH
+	"96 D8	6728",	#CJK UNIFIED IDEOGRAPH
+	"96 D9	9ED9",	#CJK UNIFIED IDEOGRAPH
+	"96 DA	76EE",	#CJK UNIFIED IDEOGRAPH
+	"96 DB	6762",	#CJK UNIFIED IDEOGRAPH
+	"96 DC	52FF",	#CJK UNIFIED IDEOGRAPH
+	"96 DD	9905",	#CJK UNIFIED IDEOGRAPH
+	"96 DE	5C24",	#CJK UNIFIED IDEOGRAPH
+	"96 DF	623B",	#CJK UNIFIED IDEOGRAPH
+	"96 E0	7C7E",	#CJK UNIFIED IDEOGRAPH
+	"96 E1	8CB0",	#CJK UNIFIED IDEOGRAPH
+	"96 E2	554F",	#CJK UNIFIED IDEOGRAPH
+	"96 E3	60B6",	#CJK UNIFIED IDEOGRAPH
+	"96 E4	7D0B",	#CJK UNIFIED IDEOGRAPH
+	"96 E5	9580",	#CJK UNIFIED IDEOGRAPH
+	"96 E6	5301",	#CJK UNIFIED IDEOGRAPH
+	"96 E7	4E5F",	#CJK UNIFIED IDEOGRAPH
+	"96 E8	51B6",	#CJK UNIFIED IDEOGRAPH
+	"96 E9	591C",	#CJK UNIFIED IDEOGRAPH
+	"96 EA	723A",	#CJK UNIFIED IDEOGRAPH
+	"96 EB	8036",	#CJK UNIFIED IDEOGRAPH
+	"96 EC	91CE",	#CJK UNIFIED IDEOGRAPH
+	"96 ED	5F25",	#CJK UNIFIED IDEOGRAPH
+	"96 EE	77E2",	#CJK UNIFIED IDEOGRAPH
+	"96 EF	5384",	#CJK UNIFIED IDEOGRAPH
+	"96 F0	5F79",	#CJK UNIFIED IDEOGRAPH
+	"96 F1	7D04",	#CJK UNIFIED IDEOGRAPH
+	"96 F2	85AC",	#CJK UNIFIED IDEOGRAPH
+	"96 F3	8A33",	#CJK UNIFIED IDEOGRAPH
+	"96 F4	8E8D",	#CJK UNIFIED IDEOGRAPH
+	"96 F5	9756",	#CJK UNIFIED IDEOGRAPH
+	"96 F6	67F3",	#CJK UNIFIED IDEOGRAPH
+	"96 F7	85AE",	#CJK UNIFIED IDEOGRAPH
+	"96 F8	9453",	#CJK UNIFIED IDEOGRAPH
+	"96 F9	6109",	#CJK UNIFIED IDEOGRAPH
+	"96 FA	6108",	#CJK UNIFIED IDEOGRAPH
+	"96 FB	6CB9",	#CJK UNIFIED IDEOGRAPH
+	"96 FC	7652",	#CJK UNIFIED IDEOGRAPH
+	"97 40	8AED",	#CJK UNIFIED IDEOGRAPH
+	"97 41	8F38",	#CJK UNIFIED IDEOGRAPH
+	"97 42	552F",	#CJK UNIFIED IDEOGRAPH
+	"97 43	4F51",	#CJK UNIFIED IDEOGRAPH
+	"97 44	512A",	#CJK UNIFIED IDEOGRAPH
+	"97 45	52C7",	#CJK UNIFIED IDEOGRAPH
+	"97 46	53CB",	#CJK UNIFIED IDEOGRAPH
+	"97 47	5BA5",	#CJK UNIFIED IDEOGRAPH
+	"97 48	5E7D",	#CJK UNIFIED IDEOGRAPH
+	"97 49	60A0",	#CJK UNIFIED IDEOGRAPH
+	"97 4A	6182",	#CJK UNIFIED IDEOGRAPH
+	"97 4B	63D6",	#CJK UNIFIED IDEOGRAPH
+	"97 4C	6709",	#CJK UNIFIED IDEOGRAPH
+	"97 4D	67DA",	#CJK UNIFIED IDEOGRAPH
+	"97 4E	6E67",	#CJK UNIFIED IDEOGRAPH
+	"97 4F	6D8C",	#CJK UNIFIED IDEOGRAPH
+	"97 50	7336",	#CJK UNIFIED IDEOGRAPH
+	"97 51	7337",	#CJK UNIFIED IDEOGRAPH
+	"97 52	7531",	#CJK UNIFIED IDEOGRAPH
+	"97 53	7950",	#CJK UNIFIED IDEOGRAPH
+	"97 54	88D5",	#CJK UNIFIED IDEOGRAPH
+	"97 55	8A98",	#CJK UNIFIED IDEOGRAPH
+	"97 56	904A",	#CJK UNIFIED IDEOGRAPH
+	"97 57	9091",	#CJK UNIFIED IDEOGRAPH
+	"97 58	90F5",	#CJK UNIFIED IDEOGRAPH
+	"97 59	96C4",	#CJK UNIFIED IDEOGRAPH
+	"97 5A	878D",	#CJK UNIFIED IDEOGRAPH
+	"97 5B	5915",	#CJK UNIFIED IDEOGRAPH
+	"97 5C	4E88",	#CJK UNIFIED IDEOGRAPH
+	"97 5D	4F59",	#CJK UNIFIED IDEOGRAPH
+	"97 5E	4E0E",	#CJK UNIFIED IDEOGRAPH
+	"97 5F	8A89",	#CJK UNIFIED IDEOGRAPH
+	"97 60	8F3F",	#CJK UNIFIED IDEOGRAPH
+	"97 61	9810",	#CJK UNIFIED IDEOGRAPH
+	"97 62	50AD",	#CJK UNIFIED IDEOGRAPH
+	"97 63	5E7C",	#CJK UNIFIED IDEOGRAPH
+	"97 64	5996",	#CJK UNIFIED IDEOGRAPH
+	"97 65	5BB9",	#CJK UNIFIED IDEOGRAPH
+	"97 66	5EB8",	#CJK UNIFIED IDEOGRAPH
+	"97 67	63DA",	#CJK UNIFIED IDEOGRAPH
+	"97 68	63FA",	#CJK UNIFIED IDEOGRAPH
+	"97 69	64C1",	#CJK UNIFIED IDEOGRAPH
+	"97 6A	66DC",	#CJK UNIFIED IDEOGRAPH
+	"97 6B	694A",	#CJK UNIFIED IDEOGRAPH
+	"97 6C	69D8",	#CJK UNIFIED IDEOGRAPH
+	"97 6D	6D0B",	#CJK UNIFIED IDEOGRAPH
+	"97 6E	6EB6",	#CJK UNIFIED IDEOGRAPH
+	"97 6F	7194",	#CJK UNIFIED IDEOGRAPH
+	"97 70	7528",	#CJK UNIFIED IDEOGRAPH
+	"97 71	7AAF",	#CJK UNIFIED IDEOGRAPH
+	"97 72	7F8A",	#CJK UNIFIED IDEOGRAPH
+	"97 73	8000",	#CJK UNIFIED IDEOGRAPH
+	"97 74	8449",	#CJK UNIFIED IDEOGRAPH
+	"97 75	84C9",	#CJK UNIFIED IDEOGRAPH
+	"97 76	8981",	#CJK UNIFIED IDEOGRAPH
+	"97 77	8B21",	#CJK UNIFIED IDEOGRAPH
+	"97 78	8E0A",	#CJK UNIFIED IDEOGRAPH
+	"97 79	9065",	#CJK UNIFIED IDEOGRAPH
+	"97 7A	967D",	#CJK UNIFIED IDEOGRAPH
+	"97 7B	990A",	#CJK UNIFIED IDEOGRAPH
+	"97 7C	617E",	#CJK UNIFIED IDEOGRAPH
+	"97 7D	6291",	#CJK UNIFIED IDEOGRAPH
+	"97 7E	6B32",	#CJK UNIFIED IDEOGRAPH
+	"97 80	6C83",	#CJK UNIFIED IDEOGRAPH
+	"97 81	6D74",	#CJK UNIFIED IDEOGRAPH
+	"97 82	7FCC",	#CJK UNIFIED IDEOGRAPH
+	"97 83	7FFC",	#CJK UNIFIED IDEOGRAPH
+	"97 84	6DC0",	#CJK UNIFIED IDEOGRAPH
+	"97 85	7F85",	#CJK UNIFIED IDEOGRAPH
+	"97 86	87BA",	#CJK UNIFIED IDEOGRAPH
+	"97 87	88F8",	#CJK UNIFIED IDEOGRAPH
+	"97 88	6765",	#CJK UNIFIED IDEOGRAPH
+	"97 89	83B1",	#CJK UNIFIED IDEOGRAPH
+	"97 8A	983C",	#CJK UNIFIED IDEOGRAPH
+	"97 8B	96F7",	#CJK UNIFIED IDEOGRAPH
+	"97 8C	6D1B",	#CJK UNIFIED IDEOGRAPH
+	"97 8D	7D61",	#CJK UNIFIED IDEOGRAPH
+	"97 8E	843D",	#CJK UNIFIED IDEOGRAPH
+	"97 8F	916A",	#CJK UNIFIED IDEOGRAPH
+	"97 90	4E71",	#CJK UNIFIED IDEOGRAPH
+	"97 91	5375",	#CJK UNIFIED IDEOGRAPH
+	"97 92	5D50",	#CJK UNIFIED IDEOGRAPH
+	"97 93	6B04",	#CJK UNIFIED IDEOGRAPH
+	"97 94	6FEB",	#CJK UNIFIED IDEOGRAPH
+	"97 95	85CD",	#CJK UNIFIED IDEOGRAPH
+	"97 96	862D",	#CJK UNIFIED IDEOGRAPH
+	"97 97	89A7",	#CJK UNIFIED IDEOGRAPH
+	"97 98	5229",	#CJK UNIFIED IDEOGRAPH
+	"97 99	540F",	#CJK UNIFIED IDEOGRAPH
+	"97 9A	5C65",	#CJK UNIFIED IDEOGRAPH
+	"97 9B	674E",	#CJK UNIFIED IDEOGRAPH
+	"97 9C	68A8",	#CJK UNIFIED IDEOGRAPH
+	"97 9D	7406",	#CJK UNIFIED IDEOGRAPH
+	"97 9E	7483",	#CJK UNIFIED IDEOGRAPH
+	"97 9F	75E2",	#CJK UNIFIED IDEOGRAPH
+	"97 A0	88CF",	#CJK UNIFIED IDEOGRAPH
+	"97 A1	88E1",	#CJK UNIFIED IDEOGRAPH
+	"97 A2	91CC",	#CJK UNIFIED IDEOGRAPH
+	"97 A3	96E2",	#CJK UNIFIED IDEOGRAPH
+	"97 A4	9678",	#CJK UNIFIED IDEOGRAPH
+	"97 A5	5F8B",	#CJK UNIFIED IDEOGRAPH
+	"97 A6	7387",	#CJK UNIFIED IDEOGRAPH
+	"97 A7	7ACB",	#CJK UNIFIED IDEOGRAPH
+	"97 A8	844E",	#CJK UNIFIED IDEOGRAPH
+	"97 A9	63A0",	#CJK UNIFIED IDEOGRAPH
+	"97 AA	7565",	#CJK UNIFIED IDEOGRAPH
+	"97 AB	5289",	#CJK UNIFIED IDEOGRAPH
+	"97 AC	6D41",	#CJK UNIFIED IDEOGRAPH
+	"97 AD	6E9C",	#CJK UNIFIED IDEOGRAPH
+	"97 AE	7409",	#CJK UNIFIED IDEOGRAPH
+	"97 AF	7559",	#CJK UNIFIED IDEOGRAPH
+	"97 B0	786B",	#CJK UNIFIED IDEOGRAPH
+	"97 B1	7C92",	#CJK UNIFIED IDEOGRAPH
+	"97 B2	9686",	#CJK UNIFIED IDEOGRAPH
+	"97 B3	7ADC",	#CJK UNIFIED IDEOGRAPH
+	"97 B4	9F8D",	#CJK UNIFIED IDEOGRAPH
+	"97 B5	4FB6",	#CJK UNIFIED IDEOGRAPH
+	"97 B6	616E",	#CJK UNIFIED IDEOGRAPH
+	"97 B7	65C5",	#CJK UNIFIED IDEOGRAPH
+	"97 B8	865C",	#CJK UNIFIED IDEOGRAPH
+	"97 B9	4E86",	#CJK UNIFIED IDEOGRAPH
+	"97 BA	4EAE",	#CJK UNIFIED IDEOGRAPH
+	"97 BB	50DA",	#CJK UNIFIED IDEOGRAPH
+	"97 BC	4E21",	#CJK UNIFIED IDEOGRAPH
+	"97 BD	51CC",	#CJK UNIFIED IDEOGRAPH
+	"97 BE	5BEE",	#CJK UNIFIED IDEOGRAPH
+	"97 BF	6599",	#CJK UNIFIED IDEOGRAPH
+	"97 C0	6881",	#CJK UNIFIED IDEOGRAPH
+	"97 C1	6DBC",	#CJK UNIFIED IDEOGRAPH
+	"97 C2	731F",	#CJK UNIFIED IDEOGRAPH
+	"97 C3	7642",	#CJK UNIFIED IDEOGRAPH
+	"97 C4	77AD",	#CJK UNIFIED IDEOGRAPH
+	"97 C5	7A1C",	#CJK UNIFIED IDEOGRAPH
+	"97 C6	7CE7",	#CJK UNIFIED IDEOGRAPH
+	"97 C7	826F",	#CJK UNIFIED IDEOGRAPH
+	"97 C8	8AD2",	#CJK UNIFIED IDEOGRAPH
+	"97 C9	907C",	#CJK UNIFIED IDEOGRAPH
+	"97 CA	91CF",	#CJK UNIFIED IDEOGRAPH
+	"97 CB	9675",	#CJK UNIFIED IDEOGRAPH
+	"97 CC	9818",	#CJK UNIFIED IDEOGRAPH
+	"97 CD	529B",	#CJK UNIFIED IDEOGRAPH
+	"97 CE	7DD1",	#CJK UNIFIED IDEOGRAPH
+	"97 CF	502B",	#CJK UNIFIED IDEOGRAPH
+	"97 D0	5398",	#CJK UNIFIED IDEOGRAPH
+	"97 D1	6797",	#CJK UNIFIED IDEOGRAPH
+	"97 D2	6DCB",	#CJK UNIFIED IDEOGRAPH
+	"97 D3	71D0",	#CJK UNIFIED IDEOGRAPH
+	"97 D4	7433",	#CJK UNIFIED IDEOGRAPH
+	"97 D5	81E8",	#CJK UNIFIED IDEOGRAPH
+	"97 D6	8F2A",	#CJK UNIFIED IDEOGRAPH
+	"97 D7	96A3",	#CJK UNIFIED IDEOGRAPH
+	"97 D8	9C57",	#CJK UNIFIED IDEOGRAPH
+	"97 D9	9E9F",	#CJK UNIFIED IDEOGRAPH
+	"97 DA	7460",	#CJK UNIFIED IDEOGRAPH
+	"97 DB	5841",	#CJK UNIFIED IDEOGRAPH
+	"97 DC	6D99",	#CJK UNIFIED IDEOGRAPH
+	"97 DD	7D2F",	#CJK UNIFIED IDEOGRAPH
+	"97 DE	985E",	#CJK UNIFIED IDEOGRAPH
+	"97 DF	4EE4",	#CJK UNIFIED IDEOGRAPH
+	"97 E0	4F36",	#CJK UNIFIED IDEOGRAPH
+	"97 E1	4F8B",	#CJK UNIFIED IDEOGRAPH
+	"97 E2	51B7",	#CJK UNIFIED IDEOGRAPH
+	"97 E3	52B1",	#CJK UNIFIED IDEOGRAPH
+	"97 E4	5DBA",	#CJK UNIFIED IDEOGRAPH
+	"97 E5	601C",	#CJK UNIFIED IDEOGRAPH
+	"97 E6	73B2",	#CJK UNIFIED IDEOGRAPH
+	"97 E7	793C",	#CJK UNIFIED IDEOGRAPH
+	"97 E8	82D3",	#CJK UNIFIED IDEOGRAPH
+	"97 E9	9234",	#CJK UNIFIED IDEOGRAPH
+	"97 EA	96B7",	#CJK UNIFIED IDEOGRAPH
+	"97 EB	96F6",	#CJK UNIFIED IDEOGRAPH
+	"97 EC	970A",	#CJK UNIFIED IDEOGRAPH
+	"97 ED	9E97",	#CJK UNIFIED IDEOGRAPH
+	"97 EE	9F62",	#CJK UNIFIED IDEOGRAPH
+	"97 EF	66A6",	#CJK UNIFIED IDEOGRAPH
+	"97 F0	6B74",	#CJK UNIFIED IDEOGRAPH
+	"97 F1	5217",	#CJK UNIFIED IDEOGRAPH
+	"97 F2	52A3",	#CJK UNIFIED IDEOGRAPH
+	"97 F3	70C8",	#CJK UNIFIED IDEOGRAPH
+	"97 F4	88C2",	#CJK UNIFIED IDEOGRAPH
+	"97 F5	5EC9",	#CJK UNIFIED IDEOGRAPH
+	"97 F6	604B",	#CJK UNIFIED IDEOGRAPH
+	"97 F7	6190",	#CJK UNIFIED IDEOGRAPH
+	"97 F8	6F23",	#CJK UNIFIED IDEOGRAPH
+	"97 F9	7149",	#CJK UNIFIED IDEOGRAPH
+	"97 FA	7C3E",	#CJK UNIFIED IDEOGRAPH
+	"97 FB	7DF4",	#CJK UNIFIED IDEOGRAPH
+	"97 FC	806F",	#CJK UNIFIED IDEOGRAPH
+	"98 40	84EE",	#CJK UNIFIED IDEOGRAPH
+	"98 41	9023",	#CJK UNIFIED IDEOGRAPH
+	"98 42	932C",	#CJK UNIFIED IDEOGRAPH
+	"98 43	5442",	#CJK UNIFIED IDEOGRAPH
+	"98 44	9B6F",	#CJK UNIFIED IDEOGRAPH
+	"98 45	6AD3",	#CJK UNIFIED IDEOGRAPH
+	"98 46	7089",	#CJK UNIFIED IDEOGRAPH
+	"98 47	8CC2",	#CJK UNIFIED IDEOGRAPH
+	"98 48	8DEF",	#CJK UNIFIED IDEOGRAPH
+	"98 49	9732",	#CJK UNIFIED IDEOGRAPH
+	"98 4A	52B4",	#CJK UNIFIED IDEOGRAPH
+	"98 4B	5A41",	#CJK UNIFIED IDEOGRAPH
+	"98 4C	5ECA",	#CJK UNIFIED IDEOGRAPH
+	"98 4D	5F04",	#CJK UNIFIED IDEOGRAPH
+	"98 4E	6717",	#CJK UNIFIED IDEOGRAPH
+	"98 4F	697C",	#CJK UNIFIED IDEOGRAPH
+	"98 50	6994",	#CJK UNIFIED IDEOGRAPH
+	"98 51	6D6A",	#CJK UNIFIED IDEOGRAPH
+	"98 52	6F0F",	#CJK UNIFIED IDEOGRAPH
+	"98 53	7262",	#CJK UNIFIED IDEOGRAPH
+	"98 54	72FC",	#CJK UNIFIED IDEOGRAPH
+	"98 55	7BED",	#CJK UNIFIED IDEOGRAPH
+	"98 56	8001",	#CJK UNIFIED IDEOGRAPH
+	"98 57	807E",	#CJK UNIFIED IDEOGRAPH
+	"98 58	874B",	#CJK UNIFIED IDEOGRAPH
+	"98 59	90CE",	#CJK UNIFIED IDEOGRAPH
+	"98 5A	516D",	#CJK UNIFIED IDEOGRAPH
+	"98 5B	9E93",	#CJK UNIFIED IDEOGRAPH
+	"98 5C	7984",	#CJK UNIFIED IDEOGRAPH
+	"98 5D	808B",	#CJK UNIFIED IDEOGRAPH
+	"98 5E	9332",	#CJK UNIFIED IDEOGRAPH
+	"98 5F	8AD6",	#CJK UNIFIED IDEOGRAPH
+	"98 60	502D",	#CJK UNIFIED IDEOGRAPH
+	"98 61	548C",	#CJK UNIFIED IDEOGRAPH
+	"98 62	8A71",	#CJK UNIFIED IDEOGRAPH
+	"98 63	6B6A",	#CJK UNIFIED IDEOGRAPH
+	"98 64	8CC4",	#CJK UNIFIED IDEOGRAPH
+	"98 65	8107",	#CJK UNIFIED IDEOGRAPH
+	"98 66	60D1",	#CJK UNIFIED IDEOGRAPH
+	"98 67	67A0",	#CJK UNIFIED IDEOGRAPH
+	"98 68	9DF2",	#CJK UNIFIED IDEOGRAPH
+	"98 69	4E99",	#CJK UNIFIED IDEOGRAPH
+	"98 6A	4E98",	#CJK UNIFIED IDEOGRAPH
+	"98 6B	9C10",	#CJK UNIFIED IDEOGRAPH
+	"98 6C	8A6B",	#CJK UNIFIED IDEOGRAPH
+	"98 6D	85C1",	#CJK UNIFIED IDEOGRAPH
+	"98 6E	8568",	#CJK UNIFIED IDEOGRAPH
+	"98 6F	6900",	#CJK UNIFIED IDEOGRAPH
+	"98 70	6E7E",	#CJK UNIFIED IDEOGRAPH
+	"98 71	7897",	#CJK UNIFIED IDEOGRAPH
+	"98 72	8155",	#CJK UNIFIED IDEOGRAPH
+	"98 9F	5F0C",	#CJK UNIFIED IDEOGRAPH
+	"98 A0	4E10",	#CJK UNIFIED IDEOGRAPH
+	"98 A1	4E15",	#CJK UNIFIED IDEOGRAPH
+	"98 A2	4E2A",	#CJK UNIFIED IDEOGRAPH
+	"98 A3	4E31",	#CJK UNIFIED IDEOGRAPH
+	"98 A4	4E36",	#CJK UNIFIED IDEOGRAPH
+	"98 A5	4E3C",	#CJK UNIFIED IDEOGRAPH
+	"98 A6	4E3F",	#CJK UNIFIED IDEOGRAPH
+	"98 A7	4E42",	#CJK UNIFIED IDEOGRAPH
+	"98 A8	4E56",	#CJK UNIFIED IDEOGRAPH
+	"98 A9	4E58",	#CJK UNIFIED IDEOGRAPH
+	"98 AA	4E82",	#CJK UNIFIED IDEOGRAPH
+	"98 AB	4E85",	#CJK UNIFIED IDEOGRAPH
+	"98 AC	8C6B",	#CJK UNIFIED IDEOGRAPH
+	"98 AD	4E8A",	#CJK UNIFIED IDEOGRAPH
+	"98 AE	8212",	#CJK UNIFIED IDEOGRAPH
+	"98 AF	5F0D",	#CJK UNIFIED IDEOGRAPH
+	"98 B0	4E8E",	#CJK UNIFIED IDEOGRAPH
+	"98 B1	4E9E",	#CJK UNIFIED IDEOGRAPH
+	"98 B2	4E9F",	#CJK UNIFIED IDEOGRAPH
+	"98 B3	4EA0",	#CJK UNIFIED IDEOGRAPH
+	"98 B4	4EA2",	#CJK UNIFIED IDEOGRAPH
+	"98 B5	4EB0",	#CJK UNIFIED IDEOGRAPH
+	"98 B6	4EB3",	#CJK UNIFIED IDEOGRAPH
+	"98 B7	4EB6",	#CJK UNIFIED IDEOGRAPH
+	"98 B8	4ECE",	#CJK UNIFIED IDEOGRAPH
+	"98 B9	4ECD",	#CJK UNIFIED IDEOGRAPH
+	"98 BA	4EC4",	#CJK UNIFIED IDEOGRAPH
+	"98 BB	4EC6",	#CJK UNIFIED IDEOGRAPH
+	"98 BC	4EC2",	#CJK UNIFIED IDEOGRAPH
+	"98 BD	4ED7",	#CJK UNIFIED IDEOGRAPH
+	"98 BE	4EDE",	#CJK UNIFIED IDEOGRAPH
+	"98 BF	4EED",	#CJK UNIFIED IDEOGRAPH
+	"98 C0	4EDF",	#CJK UNIFIED IDEOGRAPH
+	"98 C1	4EF7",	#CJK UNIFIED IDEOGRAPH
+	"98 C2	4F09",	#CJK UNIFIED IDEOGRAPH
+	"98 C3	4F5A",	#CJK UNIFIED IDEOGRAPH
+	"98 C4	4F30",	#CJK UNIFIED IDEOGRAPH
+	"98 C5	4F5B",	#CJK UNIFIED IDEOGRAPH
+	"98 C6	4F5D",	#CJK UNIFIED IDEOGRAPH
+	"98 C7	4F57",	#CJK UNIFIED IDEOGRAPH
+	"98 C8	4F47",	#CJK UNIFIED IDEOGRAPH
+	"98 C9	4F76",	#CJK UNIFIED IDEOGRAPH
+	"98 CA	4F88",	#CJK UNIFIED IDEOGRAPH
+	"98 CB	4F8F",	#CJK UNIFIED IDEOGRAPH
+	"98 CC	4F98",	#CJK UNIFIED IDEOGRAPH
+	"98 CD	4F7B",	#CJK UNIFIED IDEOGRAPH
+	"98 CE	4F69",	#CJK UNIFIED IDEOGRAPH
+	"98 CF	4F70",	#CJK UNIFIED IDEOGRAPH
+	"98 D0	4F91",	#CJK UNIFIED IDEOGRAPH
+	"98 D1	4F6F",	#CJK UNIFIED IDEOGRAPH
+	"98 D2	4F86",	#CJK UNIFIED IDEOGRAPH
+	"98 D3	4F96",	#CJK UNIFIED IDEOGRAPH
+	"98 D4	5118",	#CJK UNIFIED IDEOGRAPH
+	"98 D5	4FD4",	#CJK UNIFIED IDEOGRAPH
+	"98 D6	4FDF",	#CJK UNIFIED IDEOGRAPH
+	"98 D7	4FCE",	#CJK UNIFIED IDEOGRAPH
+	"98 D8	4FD8",	#CJK UNIFIED IDEOGRAPH
+	"98 D9	4FDB",	#CJK UNIFIED IDEOGRAPH
+	"98 DA	4FD1",	#CJK UNIFIED IDEOGRAPH
+	"98 DB	4FDA",	#CJK UNIFIED IDEOGRAPH
+	"98 DC	4FD0",	#CJK UNIFIED IDEOGRAPH
+	"98 DD	4FE4",	#CJK UNIFIED IDEOGRAPH
+	"98 DE	4FE5",	#CJK UNIFIED IDEOGRAPH
+	"98 DF	501A",	#CJK UNIFIED IDEOGRAPH
+	"98 E0	5028",	#CJK UNIFIED IDEOGRAPH
+	"98 E1	5014",	#CJK UNIFIED IDEOGRAPH
+	"98 E2	502A",	#CJK UNIFIED IDEOGRAPH
+	"98 E3	5025",	#CJK UNIFIED IDEOGRAPH
+	"98 E4	5005",	#CJK UNIFIED IDEOGRAPH
+	"98 E5	4F1C",	#CJK UNIFIED IDEOGRAPH
+	"98 E6	4FF6",	#CJK UNIFIED IDEOGRAPH
+	"98 E7	5021",	#CJK UNIFIED IDEOGRAPH
+	"98 E8	5029",	#CJK UNIFIED IDEOGRAPH
+	"98 E9	502C",	#CJK UNIFIED IDEOGRAPH
+	"98 EA	4FFE",	#CJK UNIFIED IDEOGRAPH
+	"98 EB	4FEF",	#CJK UNIFIED IDEOGRAPH
+	"98 EC	5011",	#CJK UNIFIED IDEOGRAPH
+	"98 ED	5006",	#CJK UNIFIED IDEOGRAPH
+	"98 EE	5043",	#CJK UNIFIED IDEOGRAPH
+	"98 EF	5047",	#CJK UNIFIED IDEOGRAPH
+	"98 F0	6703",	#CJK UNIFIED IDEOGRAPH
+	"98 F1	5055",	#CJK UNIFIED IDEOGRAPH
+	"98 F2	5050",	#CJK UNIFIED IDEOGRAPH
+	"98 F3	5048",	#CJK UNIFIED IDEOGRAPH
+	"98 F4	505A",	#CJK UNIFIED IDEOGRAPH
+	"98 F5	5056",	#CJK UNIFIED IDEOGRAPH
+	"98 F6	506C",	#CJK UNIFIED IDEOGRAPH
+	"98 F7	5078",	#CJK UNIFIED IDEOGRAPH
+	"98 F8	5080",	#CJK UNIFIED IDEOGRAPH
+	"98 F9	509A",	#CJK UNIFIED IDEOGRAPH
+	"98 FA	5085",	#CJK UNIFIED IDEOGRAPH
+	"98 FB	50B4",	#CJK UNIFIED IDEOGRAPH
+	"98 FC	50B2",	#CJK UNIFIED IDEOGRAPH
+	"99 40	50C9",	#CJK UNIFIED IDEOGRAPH
+	"99 41	50CA",	#CJK UNIFIED IDEOGRAPH
+	"99 42	50B3",	#CJK UNIFIED IDEOGRAPH
+	"99 43	50C2",	#CJK UNIFIED IDEOGRAPH
+	"99 44	50D6",	#CJK UNIFIED IDEOGRAPH
+	"99 45	50DE",	#CJK UNIFIED IDEOGRAPH
+	"99 46	50E5",	#CJK UNIFIED IDEOGRAPH
+	"99 47	50ED",	#CJK UNIFIED IDEOGRAPH
+	"99 48	50E3",	#CJK UNIFIED IDEOGRAPH
+	"99 49	50EE",	#CJK UNIFIED IDEOGRAPH
+	"99 4A	50F9",	#CJK UNIFIED IDEOGRAPH
+	"99 4B	50F5",	#CJK UNIFIED IDEOGRAPH
+	"99 4C	5109",	#CJK UNIFIED IDEOGRAPH
+	"99 4D	5101",	#CJK UNIFIED IDEOGRAPH
+	"99 4E	5102",	#CJK UNIFIED IDEOGRAPH
+	"99 4F	5116",	#CJK UNIFIED IDEOGRAPH
+	"99 50	5115",	#CJK UNIFIED IDEOGRAPH
+	"99 51	5114",	#CJK UNIFIED IDEOGRAPH
+	"99 52	511A",	#CJK UNIFIED IDEOGRAPH
+	"99 53	5121",	#CJK UNIFIED IDEOGRAPH
+	"99 54	513A",	#CJK UNIFIED IDEOGRAPH
+	"99 55	5137",	#CJK UNIFIED IDEOGRAPH
+	"99 56	513C",	#CJK UNIFIED IDEOGRAPH
+	"99 57	513B",	#CJK UNIFIED IDEOGRAPH
+	"99 58	513F",	#CJK UNIFIED IDEOGRAPH
+	"99 59	5140",	#CJK UNIFIED IDEOGRAPH
+	"99 5A	5152",	#CJK UNIFIED IDEOGRAPH
+	"99 5B	514C",	#CJK UNIFIED IDEOGRAPH
+	"99 5C	5154",	#CJK UNIFIED IDEOGRAPH
+	"99 5D	5162",	#CJK UNIFIED IDEOGRAPH
+	"99 5E	7AF8",	#CJK UNIFIED IDEOGRAPH
+	"99 5F	5169",	#CJK UNIFIED IDEOGRAPH
+	"99 60	516A",	#CJK UNIFIED IDEOGRAPH
+	"99 61	516E",	#CJK UNIFIED IDEOGRAPH
+	"99 62	5180",	#CJK UNIFIED IDEOGRAPH
+	"99 63	5182",	#CJK UNIFIED IDEOGRAPH
+	"99 64	56D8",	#CJK UNIFIED IDEOGRAPH
+	"99 65	518C",	#CJK UNIFIED IDEOGRAPH
+	"99 66	5189",	#CJK UNIFIED IDEOGRAPH
+	"99 67	518F",	#CJK UNIFIED IDEOGRAPH
+	"99 68	5191",	#CJK UNIFIED IDEOGRAPH
+	"99 69	5193",	#CJK UNIFIED IDEOGRAPH
+	"99 6A	5195",	#CJK UNIFIED IDEOGRAPH
+	"99 6B	5196",	#CJK UNIFIED IDEOGRAPH
+	"99 6C	51A4",	#CJK UNIFIED IDEOGRAPH
+	"99 6D	51A6",	#CJK UNIFIED IDEOGRAPH
+	"99 6E	51A2",	#CJK UNIFIED IDEOGRAPH
+	"99 6F	51A9",	#CJK UNIFIED IDEOGRAPH
+	"99 70	51AA",	#CJK UNIFIED IDEOGRAPH
+	"99 71	51AB",	#CJK UNIFIED IDEOGRAPH
+	"99 72	51B3",	#CJK UNIFIED IDEOGRAPH
+	"99 73	51B1",	#CJK UNIFIED IDEOGRAPH
+	"99 74	51B2",	#CJK UNIFIED IDEOGRAPH
+	"99 75	51B0",	#CJK UNIFIED IDEOGRAPH
+	"99 76	51B5",	#CJK UNIFIED IDEOGRAPH
+	"99 77	51BD",	#CJK UNIFIED IDEOGRAPH
+	"99 78	51C5",	#CJK UNIFIED IDEOGRAPH
+	"99 79	51C9",	#CJK UNIFIED IDEOGRAPH
+	"99 7A	51DB",	#CJK UNIFIED IDEOGRAPH
+	"99 7B	51E0",	#CJK UNIFIED IDEOGRAPH
+	"99 7C	8655",	#CJK UNIFIED IDEOGRAPH
+	"99 7D	51E9",	#CJK UNIFIED IDEOGRAPH
+	"99 7E	51ED",	#CJK UNIFIED IDEOGRAPH
+	"99 80	51F0",	#CJK UNIFIED IDEOGRAPH
+	"99 81	51F5",	#CJK UNIFIED IDEOGRAPH
+	"99 82	51FE",	#CJK UNIFIED IDEOGRAPH
+	"99 83	5204",	#CJK UNIFIED IDEOGRAPH
+	"99 84	520B",	#CJK UNIFIED IDEOGRAPH
+	"99 85	5214",	#CJK UNIFIED IDEOGRAPH
+	"99 86	520E",	#CJK UNIFIED IDEOGRAPH
+	"99 87	5227",	#CJK UNIFIED IDEOGRAPH
+	"99 88	522A",	#CJK UNIFIED IDEOGRAPH
+	"99 89	522E",	#CJK UNIFIED IDEOGRAPH
+	"99 8A	5233",	#CJK UNIFIED IDEOGRAPH
+	"99 8B	5239",	#CJK UNIFIED IDEOGRAPH
+	"99 8C	524F",	#CJK UNIFIED IDEOGRAPH
+	"99 8D	5244",	#CJK UNIFIED IDEOGRAPH
+	"99 8E	524B",	#CJK UNIFIED IDEOGRAPH
+	"99 8F	524C",	#CJK UNIFIED IDEOGRAPH
+	"99 90	525E",	#CJK UNIFIED IDEOGRAPH
+	"99 91	5254",	#CJK UNIFIED IDEOGRAPH
+	"99 92	526A",	#CJK UNIFIED IDEOGRAPH
+	"99 93	5274",	#CJK UNIFIED IDEOGRAPH
+	"99 94	5269",	#CJK UNIFIED IDEOGRAPH
+	"99 95	5273",	#CJK UNIFIED IDEOGRAPH
+	"99 96	527F",	#CJK UNIFIED IDEOGRAPH
+	"99 97	527D",	#CJK UNIFIED IDEOGRAPH
+	"99 98	528D",	#CJK UNIFIED IDEOGRAPH
+	"99 99	5294",	#CJK UNIFIED IDEOGRAPH
+	"99 9A	5292",	#CJK UNIFIED IDEOGRAPH
+	"99 9B	5271",	#CJK UNIFIED IDEOGRAPH
+	"99 9C	5288",	#CJK UNIFIED IDEOGRAPH
+	"99 9D	5291",	#CJK UNIFIED IDEOGRAPH
+	"99 9E	8FA8",	#CJK UNIFIED IDEOGRAPH
+	"99 9F	8FA7",	#CJK UNIFIED IDEOGRAPH
+	"99 A0	52AC",	#CJK UNIFIED IDEOGRAPH
+	"99 A1	52AD",	#CJK UNIFIED IDEOGRAPH
+	"99 A2	52BC",	#CJK UNIFIED IDEOGRAPH
+	"99 A3	52B5",	#CJK UNIFIED IDEOGRAPH
+	"99 A4	52C1",	#CJK UNIFIED IDEOGRAPH
+	"99 A5	52CD",	#CJK UNIFIED IDEOGRAPH
+	"99 A6	52D7",	#CJK UNIFIED IDEOGRAPH
+	"99 A7	52DE",	#CJK UNIFIED IDEOGRAPH
+	"99 A8	52E3",	#CJK UNIFIED IDEOGRAPH
+	"99 A9	52E6",	#CJK UNIFIED IDEOGRAPH
+	"99 AA	98ED",	#CJK UNIFIED IDEOGRAPH
+	"99 AB	52E0",	#CJK UNIFIED IDEOGRAPH
+	"99 AC	52F3",	#CJK UNIFIED IDEOGRAPH
+	"99 AD	52F5",	#CJK UNIFIED IDEOGRAPH
+	"99 AE	52F8",	#CJK UNIFIED IDEOGRAPH
+	"99 AF	52F9",	#CJK UNIFIED IDEOGRAPH
+	"99 B0	5306",	#CJK UNIFIED IDEOGRAPH
+	"99 B1	5308",	#CJK UNIFIED IDEOGRAPH
+	"99 B2	7538",	#CJK UNIFIED IDEOGRAPH
+	"99 B3	530D",	#CJK UNIFIED IDEOGRAPH
+	"99 B4	5310",	#CJK UNIFIED IDEOGRAPH
+	"99 B5	530F",	#CJK UNIFIED IDEOGRAPH
+	"99 B6	5315",	#CJK UNIFIED IDEOGRAPH
+	"99 B7	531A",	#CJK UNIFIED IDEOGRAPH
+	"99 B8	5323",	#CJK UNIFIED IDEOGRAPH
+	"99 B9	532F",	#CJK UNIFIED IDEOGRAPH
+	"99 BA	5331",	#CJK UNIFIED IDEOGRAPH
+	"99 BB	5333",	#CJK UNIFIED IDEOGRAPH
+	"99 BC	5338",	#CJK UNIFIED IDEOGRAPH
+	"99 BD	5340",	#CJK UNIFIED IDEOGRAPH
+	"99 BE	5346",	#CJK UNIFIED IDEOGRAPH
+	"99 BF	5345",	#CJK UNIFIED IDEOGRAPH
+	"99 C0	4E17",	#CJK UNIFIED IDEOGRAPH
+	"99 C1	5349",	#CJK UNIFIED IDEOGRAPH
+	"99 C2	534D",	#CJK UNIFIED IDEOGRAPH
+	"99 C3	51D6",	#CJK UNIFIED IDEOGRAPH
+	"99 C4	535E",	#CJK UNIFIED IDEOGRAPH
+	"99 C5	5369",	#CJK UNIFIED IDEOGRAPH
+	"99 C6	536E",	#CJK UNIFIED IDEOGRAPH
+	"99 C7	5918",	#CJK UNIFIED IDEOGRAPH
+	"99 C8	537B",	#CJK UNIFIED IDEOGRAPH
+	"99 C9	5377",	#CJK UNIFIED IDEOGRAPH
+	"99 CA	5382",	#CJK UNIFIED IDEOGRAPH
+	"99 CB	5396",	#CJK UNIFIED IDEOGRAPH
+	"99 CC	53A0",	#CJK UNIFIED IDEOGRAPH
+	"99 CD	53A6",	#CJK UNIFIED IDEOGRAPH
+	"99 CE	53A5",	#CJK UNIFIED IDEOGRAPH
+	"99 CF	53AE",	#CJK UNIFIED IDEOGRAPH
+	"99 D0	53B0",	#CJK UNIFIED IDEOGRAPH
+	"99 D1	53B6",	#CJK UNIFIED IDEOGRAPH
+	"99 D2	53C3",	#CJK UNIFIED IDEOGRAPH
+	"99 D3	7C12",	#CJK UNIFIED IDEOGRAPH
+	"99 D4	96D9",	#CJK UNIFIED IDEOGRAPH
+	"99 D5	53DF",	#CJK UNIFIED IDEOGRAPH
+	"99 D6	66FC",	#CJK UNIFIED IDEOGRAPH
+	"99 D7	71EE",	#CJK UNIFIED IDEOGRAPH
+	"99 D8	53EE",	#CJK UNIFIED IDEOGRAPH
+	"99 D9	53E8",	#CJK UNIFIED IDEOGRAPH
+	"99 DA	53ED",	#CJK UNIFIED IDEOGRAPH
+	"99 DB	53FA",	#CJK UNIFIED IDEOGRAPH
+	"99 DC	5401",	#CJK UNIFIED IDEOGRAPH
+	"99 DD	543D",	#CJK UNIFIED IDEOGRAPH
+	"99 DE	5440",	#CJK UNIFIED IDEOGRAPH
+	"99 DF	542C",	#CJK UNIFIED IDEOGRAPH
+	"99 E0	542D",	#CJK UNIFIED IDEOGRAPH
+	"99 E1	543C",	#CJK UNIFIED IDEOGRAPH
+	"99 E2	542E",	#CJK UNIFIED IDEOGRAPH
+	"99 E3	5436",	#CJK UNIFIED IDEOGRAPH
+	"99 E4	5429",	#CJK UNIFIED IDEOGRAPH
+	"99 E5	541D",	#CJK UNIFIED IDEOGRAPH
+	"99 E6	544E",	#CJK UNIFIED IDEOGRAPH
+	"99 E7	548F",	#CJK UNIFIED IDEOGRAPH
+	"99 E8	5475",	#CJK UNIFIED IDEOGRAPH
+	"99 E9	548E",	#CJK UNIFIED IDEOGRAPH
+	"99 EA	545F",	#CJK UNIFIED IDEOGRAPH
+	"99 EB	5471",	#CJK UNIFIED IDEOGRAPH
+	"99 EC	5477",	#CJK UNIFIED IDEOGRAPH
+	"99 ED	5470",	#CJK UNIFIED IDEOGRAPH
+	"99 EE	5492",	#CJK UNIFIED IDEOGRAPH
+	"99 EF	547B",	#CJK UNIFIED IDEOGRAPH
+	"99 F0	5480",	#CJK UNIFIED IDEOGRAPH
+	"99 F1	5476",	#CJK UNIFIED IDEOGRAPH
+	"99 F2	5484",	#CJK UNIFIED IDEOGRAPH
+	"99 F3	5490",	#CJK UNIFIED IDEOGRAPH
+	"99 F4	5486",	#CJK UNIFIED IDEOGRAPH
+	"99 F5	54C7",	#CJK UNIFIED IDEOGRAPH
+	"99 F6	54A2",	#CJK UNIFIED IDEOGRAPH
+	"99 F7	54B8",	#CJK UNIFIED IDEOGRAPH
+	"99 F8	54A5",	#CJK UNIFIED IDEOGRAPH
+	"99 F9	54AC",	#CJK UNIFIED IDEOGRAPH
+	"99 FA	54C4",	#CJK UNIFIED IDEOGRAPH
+	"99 FB	54C8",	#CJK UNIFIED IDEOGRAPH
+	"99 FC	54A8",	#CJK UNIFIED IDEOGRAPH
+	"9A 40	54AB",	#CJK UNIFIED IDEOGRAPH
+	"9A 41	54C2",	#CJK UNIFIED IDEOGRAPH
+	"9A 42	54A4",	#CJK UNIFIED IDEOGRAPH
+	"9A 43	54BE",	#CJK UNIFIED IDEOGRAPH
+	"9A 44	54BC",	#CJK UNIFIED IDEOGRAPH
+	"9A 45	54D8",	#CJK UNIFIED IDEOGRAPH
+	"9A 46	54E5",	#CJK UNIFIED IDEOGRAPH
+	"9A 47	54E6",	#CJK UNIFIED IDEOGRAPH
+	"9A 48	550F",	#CJK UNIFIED IDEOGRAPH
+	"9A 49	5514",	#CJK UNIFIED IDEOGRAPH
+	"9A 4A	54FD",	#CJK UNIFIED IDEOGRAPH
+	"9A 4B	54EE",	#CJK UNIFIED IDEOGRAPH
+	"9A 4C	54ED",	#CJK UNIFIED IDEOGRAPH
+	"9A 4D	54FA",	#CJK UNIFIED IDEOGRAPH
+	"9A 4E	54E2",	#CJK UNIFIED IDEOGRAPH
+	"9A 4F	5539",	#CJK UNIFIED IDEOGRAPH
+	"9A 50	5540",	#CJK UNIFIED IDEOGRAPH
+	"9A 51	5563",	#CJK UNIFIED IDEOGRAPH
+	"9A 52	554C",	#CJK UNIFIED IDEOGRAPH
+	"9A 53	552E",	#CJK UNIFIED IDEOGRAPH
+	"9A 54	555C",	#CJK UNIFIED IDEOGRAPH
+	"9A 55	5545",	#CJK UNIFIED IDEOGRAPH
+	"9A 56	5556",	#CJK UNIFIED IDEOGRAPH
+	"9A 57	5557",	#CJK UNIFIED IDEOGRAPH
+	"9A 58	5538",	#CJK UNIFIED IDEOGRAPH
+	"9A 59	5533",	#CJK UNIFIED IDEOGRAPH
+	"9A 5A	555D",	#CJK UNIFIED IDEOGRAPH
+	"9A 5B	5599",	#CJK UNIFIED IDEOGRAPH
+	"9A 5C	5580",	#CJK UNIFIED IDEOGRAPH
+	"9A 5D	54AF",	#CJK UNIFIED IDEOGRAPH
+	"9A 5E	558A",	#CJK UNIFIED IDEOGRAPH
+	"9A 5F	559F",	#CJK UNIFIED IDEOGRAPH
+	"9A 60	557B",	#CJK UNIFIED IDEOGRAPH
+	"9A 61	557E",	#CJK UNIFIED IDEOGRAPH
+	"9A 62	5598",	#CJK UNIFIED IDEOGRAPH
+	"9A 63	559E",	#CJK UNIFIED IDEOGRAPH
+	"9A 64	55AE",	#CJK UNIFIED IDEOGRAPH
+	"9A 65	557C",	#CJK UNIFIED IDEOGRAPH
+	"9A 66	5583",	#CJK UNIFIED IDEOGRAPH
+	"9A 67	55A9",	#CJK UNIFIED IDEOGRAPH
+	"9A 68	5587",	#CJK UNIFIED IDEOGRAPH
+	"9A 69	55A8",	#CJK UNIFIED IDEOGRAPH
+	"9A 6A	55DA",	#CJK UNIFIED IDEOGRAPH
+	"9A 6B	55C5",	#CJK UNIFIED IDEOGRAPH
+	"9A 6C	55DF",	#CJK UNIFIED IDEOGRAPH
+	"9A 6D	55C4",	#CJK UNIFIED IDEOGRAPH
+	"9A 6E	55DC",	#CJK UNIFIED IDEOGRAPH
+	"9A 6F	55E4",	#CJK UNIFIED IDEOGRAPH
+	"9A 70	55D4",	#CJK UNIFIED IDEOGRAPH
+	"9A 71	5614",	#CJK UNIFIED IDEOGRAPH
+	"9A 72	55F7",	#CJK UNIFIED IDEOGRAPH
+	"9A 73	5616",	#CJK UNIFIED IDEOGRAPH
+	"9A 74	55FE",	#CJK UNIFIED IDEOGRAPH
+	"9A 75	55FD",	#CJK UNIFIED IDEOGRAPH
+	"9A 76	561B",	#CJK UNIFIED IDEOGRAPH
+	"9A 77	55F9",	#CJK UNIFIED IDEOGRAPH
+	"9A 78	564E",	#CJK UNIFIED IDEOGRAPH
+	"9A 79	5650",	#CJK UNIFIED IDEOGRAPH
+	"9A 7A	71DF",	#CJK UNIFIED IDEOGRAPH
+	"9A 7B	5634",	#CJK UNIFIED IDEOGRAPH
+	"9A 7C	5636",	#CJK UNIFIED IDEOGRAPH
+	"9A 7D	5632",	#CJK UNIFIED IDEOGRAPH
+	"9A 7E	5638",	#CJK UNIFIED IDEOGRAPH
+	"9A 80	566B",	#CJK UNIFIED IDEOGRAPH
+	"9A 81	5664",	#CJK UNIFIED IDEOGRAPH
+	"9A 82	562F",	#CJK UNIFIED IDEOGRAPH
+	"9A 83	566C",	#CJK UNIFIED IDEOGRAPH
+	"9A 84	566A",	#CJK UNIFIED IDEOGRAPH
+	"9A 85	5686",	#CJK UNIFIED IDEOGRAPH
+	"9A 86	5680",	#CJK UNIFIED IDEOGRAPH
+	"9A 87	568A",	#CJK UNIFIED IDEOGRAPH
+	"9A 88	56A0",	#CJK UNIFIED IDEOGRAPH
+	"9A 89	5694",	#CJK UNIFIED IDEOGRAPH
+	"9A 8A	568F",	#CJK UNIFIED IDEOGRAPH
+	"9A 8B	56A5",	#CJK UNIFIED IDEOGRAPH
+	"9A 8C	56AE",	#CJK UNIFIED IDEOGRAPH
+	"9A 8D	56B6",	#CJK UNIFIED IDEOGRAPH
+	"9A 8E	56B4",	#CJK UNIFIED IDEOGRAPH
+	"9A 8F	56C2",	#CJK UNIFIED IDEOGRAPH
+	"9A 90	56BC",	#CJK UNIFIED IDEOGRAPH
+	"9A 91	56C1",	#CJK UNIFIED IDEOGRAPH
+	"9A 92	56C3",	#CJK UNIFIED IDEOGRAPH
+	"9A 93	56C0",	#CJK UNIFIED IDEOGRAPH
+	"9A 94	56C8",	#CJK UNIFIED IDEOGRAPH
+	"9A 95	56CE",	#CJK UNIFIED IDEOGRAPH
+	"9A 96	56D1",	#CJK UNIFIED IDEOGRAPH
+	"9A 97	56D3",	#CJK UNIFIED IDEOGRAPH
+	"9A 98	56D7",	#CJK UNIFIED IDEOGRAPH
+	"9A 99	56EE",	#CJK UNIFIED IDEOGRAPH
+	"9A 9A	56F9",	#CJK UNIFIED IDEOGRAPH
+	"9A 9B	5700",	#CJK UNIFIED IDEOGRAPH
+	"9A 9C	56FF",	#CJK UNIFIED IDEOGRAPH
+	"9A 9D	5704",	#CJK UNIFIED IDEOGRAPH
+	"9A 9E	5709",	#CJK UNIFIED IDEOGRAPH
+	"9A 9F	5708",	#CJK UNIFIED IDEOGRAPH
+	"9A A0	570B",	#CJK UNIFIED IDEOGRAPH
+	"9A A1	570D",	#CJK UNIFIED IDEOGRAPH
+	"9A A2	5713",	#CJK UNIFIED IDEOGRAPH
+	"9A A3	5718",	#CJK UNIFIED IDEOGRAPH
+	"9A A4	5716",	#CJK UNIFIED IDEOGRAPH
+	"9A A5	55C7",	#CJK UNIFIED IDEOGRAPH
+	"9A A6	571C",	#CJK UNIFIED IDEOGRAPH
+	"9A A7	5726",	#CJK UNIFIED IDEOGRAPH
+	"9A A8	5737",	#CJK UNIFIED IDEOGRAPH
+	"9A A9	5738",	#CJK UNIFIED IDEOGRAPH
+	"9A AA	574E",	#CJK UNIFIED IDEOGRAPH
+	"9A AB	573B",	#CJK UNIFIED IDEOGRAPH
+	"9A AC	5740",	#CJK UNIFIED IDEOGRAPH
+	"9A AD	574F",	#CJK UNIFIED IDEOGRAPH
+	"9A AE	5769",	#CJK UNIFIED IDEOGRAPH
+	"9A AF	57C0",	#CJK UNIFIED IDEOGRAPH
+	"9A B0	5788",	#CJK UNIFIED IDEOGRAPH
+	"9A B1	5761",	#CJK UNIFIED IDEOGRAPH
+	"9A B2	577F",	#CJK UNIFIED IDEOGRAPH
+	"9A B3	5789",	#CJK UNIFIED IDEOGRAPH
+	"9A B4	5793",	#CJK UNIFIED IDEOGRAPH
+	"9A B5	57A0",	#CJK UNIFIED IDEOGRAPH
+	"9A B6	57B3",	#CJK UNIFIED IDEOGRAPH
+	"9A B7	57A4",	#CJK UNIFIED IDEOGRAPH
+	"9A B8	57AA",	#CJK UNIFIED IDEOGRAPH
+	"9A B9	57B0",	#CJK UNIFIED IDEOGRAPH
+	"9A BA	57C3",	#CJK UNIFIED IDEOGRAPH
+	"9A BB	57C6",	#CJK UNIFIED IDEOGRAPH
+	"9A BC	57D4",	#CJK UNIFIED IDEOGRAPH
+	"9A BD	57D2",	#CJK UNIFIED IDEOGRAPH
+	"9A BE	57D3",	#CJK UNIFIED IDEOGRAPH
+	"9A BF	580A",	#CJK UNIFIED IDEOGRAPH
+	"9A C0	57D6",	#CJK UNIFIED IDEOGRAPH
+	"9A C1	57E3",	#CJK UNIFIED IDEOGRAPH
+	"9A C2	580B",	#CJK UNIFIED IDEOGRAPH
+	"9A C3	5819",	#CJK UNIFIED IDEOGRAPH
+	"9A C4	581D",	#CJK UNIFIED IDEOGRAPH
+	"9A C5	5872",	#CJK UNIFIED IDEOGRAPH
+	"9A C6	5821",	#CJK UNIFIED IDEOGRAPH
+	"9A C7	5862",	#CJK UNIFIED IDEOGRAPH
+	"9A C8	584B",	#CJK UNIFIED IDEOGRAPH
+	"9A C9	5870",	#CJK UNIFIED IDEOGRAPH
+	"9A CA	6BC0",	#CJK UNIFIED IDEOGRAPH
+	"9A CB	5852",	#CJK UNIFIED IDEOGRAPH
+	"9A CC	583D",	#CJK UNIFIED IDEOGRAPH
+	"9A CD	5879",	#CJK UNIFIED IDEOGRAPH
+	"9A CE	5885",	#CJK UNIFIED IDEOGRAPH
+	"9A CF	58B9",	#CJK UNIFIED IDEOGRAPH
+	"9A D0	589F",	#CJK UNIFIED IDEOGRAPH
+	"9A D1	58AB",	#CJK UNIFIED IDEOGRAPH
+	"9A D2	58BA",	#CJK UNIFIED IDEOGRAPH
+	"9A D3	58DE",	#CJK UNIFIED IDEOGRAPH
+	"9A D4	58BB",	#CJK UNIFIED IDEOGRAPH
+	"9A D5	58B8",	#CJK UNIFIED IDEOGRAPH
+	"9A D6	58AE",	#CJK UNIFIED IDEOGRAPH
+	"9A D7	58C5",	#CJK UNIFIED IDEOGRAPH
+	"9A D8	58D3",	#CJK UNIFIED IDEOGRAPH
+	"9A D9	58D1",	#CJK UNIFIED IDEOGRAPH
+	"9A DA	58D7",	#CJK UNIFIED IDEOGRAPH
+	"9A DB	58D9",	#CJK UNIFIED IDEOGRAPH
+	"9A DC	58D8",	#CJK UNIFIED IDEOGRAPH
+	"9A DD	58E5",	#CJK UNIFIED IDEOGRAPH
+	"9A DE	58DC",	#CJK UNIFIED IDEOGRAPH
+	"9A DF	58E4",	#CJK UNIFIED IDEOGRAPH
+	"9A E0	58DF",	#CJK UNIFIED IDEOGRAPH
+	"9A E1	58EF",	#CJK UNIFIED IDEOGRAPH
+	"9A E2	58FA",	#CJK UNIFIED IDEOGRAPH
+	"9A E3	58F9",	#CJK UNIFIED IDEOGRAPH
+	"9A E4	58FB",	#CJK UNIFIED IDEOGRAPH
+	"9A E5	58FC",	#CJK UNIFIED IDEOGRAPH
+	"9A E6	58FD",	#CJK UNIFIED IDEOGRAPH
+	"9A E7	5902",	#CJK UNIFIED IDEOGRAPH
+	"9A E8	590A",	#CJK UNIFIED IDEOGRAPH
+	"9A E9	5910",	#CJK UNIFIED IDEOGRAPH
+	"9A EA	591B",	#CJK UNIFIED IDEOGRAPH
+	"9A EB	68A6",	#CJK UNIFIED IDEOGRAPH
+	"9A EC	5925",	#CJK UNIFIED IDEOGRAPH
+	"9A ED	592C",	#CJK UNIFIED IDEOGRAPH
+	"9A EE	592D",	#CJK UNIFIED IDEOGRAPH
+	"9A EF	5932",	#CJK UNIFIED IDEOGRAPH
+	"9A F0	5938",	#CJK UNIFIED IDEOGRAPH
+	"9A F1	593E",	#CJK UNIFIED IDEOGRAPH
+	"9A F2	7AD2",	#CJK UNIFIED IDEOGRAPH
+	"9A F3	5955",	#CJK UNIFIED IDEOGRAPH
+	"9A F4	5950",	#CJK UNIFIED IDEOGRAPH
+	"9A F5	594E",	#CJK UNIFIED IDEOGRAPH
+	"9A F6	595A",	#CJK UNIFIED IDEOGRAPH
+	"9A F7	5958",	#CJK UNIFIED IDEOGRAPH
+	"9A F8	5962",	#CJK UNIFIED IDEOGRAPH
+	"9A F9	5960",	#CJK UNIFIED IDEOGRAPH
+	"9A FA	5967",	#CJK UNIFIED IDEOGRAPH
+	"9A FB	596C",	#CJK UNIFIED IDEOGRAPH
+	"9A FC	5969",	#CJK UNIFIED IDEOGRAPH
+	"9B 40	5978",	#CJK UNIFIED IDEOGRAPH
+	"9B 41	5981",	#CJK UNIFIED IDEOGRAPH
+	"9B 42	599D",	#CJK UNIFIED IDEOGRAPH
+	"9B 43	4F5E",	#CJK UNIFIED IDEOGRAPH
+	"9B 44	4FAB",	#CJK UNIFIED IDEOGRAPH
+	"9B 45	59A3",	#CJK UNIFIED IDEOGRAPH
+	"9B 46	59B2",	#CJK UNIFIED IDEOGRAPH
+	"9B 47	59C6",	#CJK UNIFIED IDEOGRAPH
+	"9B 48	59E8",	#CJK UNIFIED IDEOGRAPH
+	"9B 49	59DC",	#CJK UNIFIED IDEOGRAPH
+	"9B 4A	598D",	#CJK UNIFIED IDEOGRAPH
+	"9B 4B	59D9",	#CJK UNIFIED IDEOGRAPH
+	"9B 4C	59DA",	#CJK UNIFIED IDEOGRAPH
+	"9B 4D	5A25",	#CJK UNIFIED IDEOGRAPH
+	"9B 4E	5A1F",	#CJK UNIFIED IDEOGRAPH
+	"9B 4F	5A11",	#CJK UNIFIED IDEOGRAPH
+	"9B 50	5A1C",	#CJK UNIFIED IDEOGRAPH
+	"9B 51	5A09",	#CJK UNIFIED IDEOGRAPH
+	"9B 52	5A1A",	#CJK UNIFIED IDEOGRAPH
+	"9B 53	5A40",	#CJK UNIFIED IDEOGRAPH
+	"9B 54	5A6C",	#CJK UNIFIED IDEOGRAPH
+	"9B 55	5A49",	#CJK UNIFIED IDEOGRAPH
+	"9B 56	5A35",	#CJK UNIFIED IDEOGRAPH
+	"9B 57	5A36",	#CJK UNIFIED IDEOGRAPH
+	"9B 58	5A62",	#CJK UNIFIED IDEOGRAPH
+	"9B 59	5A6A",	#CJK UNIFIED IDEOGRAPH
+	"9B 5A	5A9A",	#CJK UNIFIED IDEOGRAPH
+	"9B 5B	5ABC",	#CJK UNIFIED IDEOGRAPH
+	"9B 5C	5ABE",	#CJK UNIFIED IDEOGRAPH
+	"9B 5D	5ACB",	#CJK UNIFIED IDEOGRAPH
+	"9B 5E	5AC2",	#CJK UNIFIED IDEOGRAPH
+	"9B 5F	5ABD",	#CJK UNIFIED IDEOGRAPH
+	"9B 60	5AE3",	#CJK UNIFIED IDEOGRAPH
+	"9B 61	5AD7",	#CJK UNIFIED IDEOGRAPH
+	"9B 62	5AE6",	#CJK UNIFIED IDEOGRAPH
+	"9B 63	5AE9",	#CJK UNIFIED IDEOGRAPH
+	"9B 64	5AD6",	#CJK UNIFIED IDEOGRAPH
+	"9B 65	5AFA",	#CJK UNIFIED IDEOGRAPH
+	"9B 66	5AFB",	#CJK UNIFIED IDEOGRAPH
+	"9B 67	5B0C",	#CJK UNIFIED IDEOGRAPH
+	"9B 68	5B0B",	#CJK UNIFIED IDEOGRAPH
+	"9B 69	5B16",	#CJK UNIFIED IDEOGRAPH
+	"9B 6A	5B32",	#CJK UNIFIED IDEOGRAPH
+	"9B 6B	5AD0",	#CJK UNIFIED IDEOGRAPH
+	"9B 6C	5B2A",	#CJK UNIFIED IDEOGRAPH
+	"9B 6D	5B36",	#CJK UNIFIED IDEOGRAPH
+	"9B 6E	5B3E",	#CJK UNIFIED IDEOGRAPH
+	"9B 6F	5B43",	#CJK UNIFIED IDEOGRAPH
+	"9B 70	5B45",	#CJK UNIFIED IDEOGRAPH
+	"9B 71	5B40",	#CJK UNIFIED IDEOGRAPH
+	"9B 72	5B51",	#CJK UNIFIED IDEOGRAPH
+	"9B 73	5B55",	#CJK UNIFIED IDEOGRAPH
+	"9B 74	5B5A",	#CJK UNIFIED IDEOGRAPH
+	"9B 75	5B5B",	#CJK UNIFIED IDEOGRAPH
+	"9B 76	5B65",	#CJK UNIFIED IDEOGRAPH
+	"9B 77	5B69",	#CJK UNIFIED IDEOGRAPH
+	"9B 78	5B70",	#CJK UNIFIED IDEOGRAPH
+	"9B 79	5B73",	#CJK UNIFIED IDEOGRAPH
+	"9B 7A	5B75",	#CJK UNIFIED IDEOGRAPH
+	"9B 7B	5B78",	#CJK UNIFIED IDEOGRAPH
+	"9B 7C	6588",	#CJK UNIFIED IDEOGRAPH
+	"9B 7D	5B7A",	#CJK UNIFIED IDEOGRAPH
+	"9B 7E	5B80",	#CJK UNIFIED IDEOGRAPH
+	"9B 80	5B83",	#CJK UNIFIED IDEOGRAPH
+	"9B 81	5BA6",	#CJK UNIFIED IDEOGRAPH
+	"9B 82	5BB8",	#CJK UNIFIED IDEOGRAPH
+	"9B 83	5BC3",	#CJK UNIFIED IDEOGRAPH
+	"9B 84	5BC7",	#CJK UNIFIED IDEOGRAPH
+	"9B 85	5BC9",	#CJK UNIFIED IDEOGRAPH
+	"9B 86	5BD4",	#CJK UNIFIED IDEOGRAPH
+	"9B 87	5BD0",	#CJK UNIFIED IDEOGRAPH
+	"9B 88	5BE4",	#CJK UNIFIED IDEOGRAPH
+	"9B 89	5BE6",	#CJK UNIFIED IDEOGRAPH
+	"9B 8A	5BE2",	#CJK UNIFIED IDEOGRAPH
+	"9B 8B	5BDE",	#CJK UNIFIED IDEOGRAPH
+	"9B 8C	5BE5",	#CJK UNIFIED IDEOGRAPH
+	"9B 8D	5BEB",	#CJK UNIFIED IDEOGRAPH
+	"9B 8E	5BF0",	#CJK UNIFIED IDEOGRAPH
+	"9B 8F	5BF6",	#CJK UNIFIED IDEOGRAPH
+	"9B 90	5BF3",	#CJK UNIFIED IDEOGRAPH
+	"9B 91	5C05",	#CJK UNIFIED IDEOGRAPH
+	"9B 92	5C07",	#CJK UNIFIED IDEOGRAPH
+	"9B 93	5C08",	#CJK UNIFIED IDEOGRAPH
+	"9B 94	5C0D",	#CJK UNIFIED IDEOGRAPH
+	"9B 95	5C13",	#CJK UNIFIED IDEOGRAPH
+	"9B 96	5C20",	#CJK UNIFIED IDEOGRAPH
+	"9B 97	5C22",	#CJK UNIFIED IDEOGRAPH
+	"9B 98	5C28",	#CJK UNIFIED IDEOGRAPH
+	"9B 99	5C38",	#CJK UNIFIED IDEOGRAPH
+	"9B 9A	5C39",	#CJK UNIFIED IDEOGRAPH
+	"9B 9B	5C41",	#CJK UNIFIED IDEOGRAPH
+	"9B 9C	5C46",	#CJK UNIFIED IDEOGRAPH
+	"9B 9D	5C4E",	#CJK UNIFIED IDEOGRAPH
+	"9B 9E	5C53",	#CJK UNIFIED IDEOGRAPH
+	"9B 9F	5C50",	#CJK UNIFIED IDEOGRAPH
+	"9B A0	5C4F",	#CJK UNIFIED IDEOGRAPH
+	"9B A1	5B71",	#CJK UNIFIED IDEOGRAPH
+	"9B A2	5C6C",	#CJK UNIFIED IDEOGRAPH
+	"9B A3	5C6E",	#CJK UNIFIED IDEOGRAPH
+	"9B A4	4E62",	#CJK UNIFIED IDEOGRAPH
+	"9B A5	5C76",	#CJK UNIFIED IDEOGRAPH
+	"9B A6	5C79",	#CJK UNIFIED IDEOGRAPH
+	"9B A7	5C8C",	#CJK UNIFIED IDEOGRAPH
+	"9B A8	5C91",	#CJK UNIFIED IDEOGRAPH
+	"9B A9	5C94",	#CJK UNIFIED IDEOGRAPH
+	"9B AA	599B",	#CJK UNIFIED IDEOGRAPH
+	"9B AB	5CAB",	#CJK UNIFIED IDEOGRAPH
+	"9B AC	5CBB",	#CJK UNIFIED IDEOGRAPH
+	"9B AD	5CB6",	#CJK UNIFIED IDEOGRAPH
+	"9B AE	5CBC",	#CJK UNIFIED IDEOGRAPH
+	"9B AF	5CB7",	#CJK UNIFIED IDEOGRAPH
+	"9B B0	5CC5",	#CJK UNIFIED IDEOGRAPH
+	"9B B1	5CBE",	#CJK UNIFIED IDEOGRAPH
+	"9B B2	5CC7",	#CJK UNIFIED IDEOGRAPH
+	"9B B3	5CD9",	#CJK UNIFIED IDEOGRAPH
+	"9B B4	5CE9",	#CJK UNIFIED IDEOGRAPH
+	"9B B5	5CFD",	#CJK UNIFIED IDEOGRAPH
+	"9B B6	5CFA",	#CJK UNIFIED IDEOGRAPH
+	"9B B7	5CED",	#CJK UNIFIED IDEOGRAPH
+	"9B B8	5D8C",	#CJK UNIFIED IDEOGRAPH
+	"9B B9	5CEA",	#CJK UNIFIED IDEOGRAPH
+	"9B BA	5D0B",	#CJK UNIFIED IDEOGRAPH
+	"9B BB	5D15",	#CJK UNIFIED IDEOGRAPH
+	"9B BC	5D17",	#CJK UNIFIED IDEOGRAPH
+	"9B BD	5D5C",	#CJK UNIFIED IDEOGRAPH
+	"9B BE	5D1F",	#CJK UNIFIED IDEOGRAPH
+	"9B BF	5D1B",	#CJK UNIFIED IDEOGRAPH
+	"9B C0	5D11",	#CJK UNIFIED IDEOGRAPH
+	"9B C1	5D14",	#CJK UNIFIED IDEOGRAPH
+	"9B C2	5D22",	#CJK UNIFIED IDEOGRAPH
+	"9B C3	5D1A",	#CJK UNIFIED IDEOGRAPH
+	"9B C4	5D19",	#CJK UNIFIED IDEOGRAPH
+	"9B C5	5D18",	#CJK UNIFIED IDEOGRAPH
+	"9B C6	5D4C",	#CJK UNIFIED IDEOGRAPH
+	"9B C7	5D52",	#CJK UNIFIED IDEOGRAPH
+	"9B C8	5D4E",	#CJK UNIFIED IDEOGRAPH
+	"9B C9	5D4B",	#CJK UNIFIED IDEOGRAPH
+	"9B CA	5D6C",	#CJK UNIFIED IDEOGRAPH
+	"9B CB	5D73",	#CJK UNIFIED IDEOGRAPH
+	"9B CC	5D76",	#CJK UNIFIED IDEOGRAPH
+	"9B CD	5D87",	#CJK UNIFIED IDEOGRAPH
+	"9B CE	5D84",	#CJK UNIFIED IDEOGRAPH
+	"9B CF	5D82",	#CJK UNIFIED IDEOGRAPH
+	"9B D0	5DA2",	#CJK UNIFIED IDEOGRAPH
+	"9B D1	5D9D",	#CJK UNIFIED IDEOGRAPH
+	"9B D2	5DAC",	#CJK UNIFIED IDEOGRAPH
+	"9B D3	5DAE",	#CJK UNIFIED IDEOGRAPH
+	"9B D4	5DBD",	#CJK UNIFIED IDEOGRAPH
+	"9B D5	5D90",	#CJK UNIFIED IDEOGRAPH
+	"9B D6	5DB7",	#CJK UNIFIED IDEOGRAPH
+	"9B D7	5DBC",	#CJK UNIFIED IDEOGRAPH
+	"9B D8	5DC9",	#CJK UNIFIED IDEOGRAPH
+	"9B D9	5DCD",	#CJK UNIFIED IDEOGRAPH
+	"9B DA	5DD3",	#CJK UNIFIED IDEOGRAPH
+	"9B DB	5DD2",	#CJK UNIFIED IDEOGRAPH
+	"9B DC	5DD6",	#CJK UNIFIED IDEOGRAPH
+	"9B DD	5DDB",	#CJK UNIFIED IDEOGRAPH
+	"9B DE	5DEB",	#CJK UNIFIED IDEOGRAPH
+	"9B DF	5DF2",	#CJK UNIFIED IDEOGRAPH
+	"9B E0	5DF5",	#CJK UNIFIED IDEOGRAPH
+	"9B E1	5E0B",	#CJK UNIFIED IDEOGRAPH
+	"9B E2	5E1A",	#CJK UNIFIED IDEOGRAPH
+	"9B E3	5E19",	#CJK UNIFIED IDEOGRAPH
+	"9B E4	5E11",	#CJK UNIFIED IDEOGRAPH
+	"9B E5	5E1B",	#CJK UNIFIED IDEOGRAPH
+	"9B E6	5E36",	#CJK UNIFIED IDEOGRAPH
+	"9B E7	5E37",	#CJK UNIFIED IDEOGRAPH
+	"9B E8	5E44",	#CJK UNIFIED IDEOGRAPH
+	"9B E9	5E43",	#CJK UNIFIED IDEOGRAPH
+	"9B EA	5E40",	#CJK UNIFIED IDEOGRAPH
+	"9B EB	5E4E",	#CJK UNIFIED IDEOGRAPH
+	"9B EC	5E57",	#CJK UNIFIED IDEOGRAPH
+	"9B ED	5E54",	#CJK UNIFIED IDEOGRAPH
+	"9B EE	5E5F",	#CJK UNIFIED IDEOGRAPH
+	"9B EF	5E62",	#CJK UNIFIED IDEOGRAPH
+	"9B F0	5E64",	#CJK UNIFIED IDEOGRAPH
+	"9B F1	5E47",	#CJK UNIFIED IDEOGRAPH
+	"9B F2	5E75",	#CJK UNIFIED IDEOGRAPH
+	"9B F3	5E76",	#CJK UNIFIED IDEOGRAPH
+	"9B F4	5E7A",	#CJK UNIFIED IDEOGRAPH
+	"9B F5	9EBC",	#CJK UNIFIED IDEOGRAPH
+	"9B F6	5E7F",	#CJK UNIFIED IDEOGRAPH
+	"9B F7	5EA0",	#CJK UNIFIED IDEOGRAPH
+	"9B F8	5EC1",	#CJK UNIFIED IDEOGRAPH
+	"9B F9	5EC2",	#CJK UNIFIED IDEOGRAPH
+	"9B FA	5EC8",	#CJK UNIFIED IDEOGRAPH
+	"9B FB	5ED0",	#CJK UNIFIED IDEOGRAPH
+	"9B FC	5ECF",	#CJK UNIFIED IDEOGRAPH
+	"9C 40	5ED6",	#CJK UNIFIED IDEOGRAPH
+	"9C 41	5EE3",	#CJK UNIFIED IDEOGRAPH
+	"9C 42	5EDD",	#CJK UNIFIED IDEOGRAPH
+	"9C 43	5EDA",	#CJK UNIFIED IDEOGRAPH
+	"9C 44	5EDB",	#CJK UNIFIED IDEOGRAPH
+	"9C 45	5EE2",	#CJK UNIFIED IDEOGRAPH
+	"9C 46	5EE1",	#CJK UNIFIED IDEOGRAPH
+	"9C 47	5EE8",	#CJK UNIFIED IDEOGRAPH
+	"9C 48	5EE9",	#CJK UNIFIED IDEOGRAPH
+	"9C 49	5EEC",	#CJK UNIFIED IDEOGRAPH
+	"9C 4A	5EF1",	#CJK UNIFIED IDEOGRAPH
+	"9C 4B	5EF3",	#CJK UNIFIED IDEOGRAPH
+	"9C 4C	5EF0",	#CJK UNIFIED IDEOGRAPH
+	"9C 4D	5EF4",	#CJK UNIFIED IDEOGRAPH
+	"9C 4E	5EF8",	#CJK UNIFIED IDEOGRAPH
+	"9C 4F	5EFE",	#CJK UNIFIED IDEOGRAPH
+	"9C 50	5F03",	#CJK UNIFIED IDEOGRAPH
+	"9C 51	5F09",	#CJK UNIFIED IDEOGRAPH
+	"9C 52	5F5D",	#CJK UNIFIED IDEOGRAPH
+	"9C 53	5F5C",	#CJK UNIFIED IDEOGRAPH
+	"9C 54	5F0B",	#CJK UNIFIED IDEOGRAPH
+	"9C 55	5F11",	#CJK UNIFIED IDEOGRAPH
+	"9C 56	5F16",	#CJK UNIFIED IDEOGRAPH
+	"9C 57	5F29",	#CJK UNIFIED IDEOGRAPH
+	"9C 58	5F2D",	#CJK UNIFIED IDEOGRAPH
+	"9C 59	5F38",	#CJK UNIFIED IDEOGRAPH
+	"9C 5A	5F41",	#CJK UNIFIED IDEOGRAPH
+	"9C 5B	5F48",	#CJK UNIFIED IDEOGRAPH
+	"9C 5C	5F4C",	#CJK UNIFIED IDEOGRAPH
+	"9C 5D	5F4E",	#CJK UNIFIED IDEOGRAPH
+	"9C 5E	5F2F",	#CJK UNIFIED IDEOGRAPH
+	"9C 5F	5F51",	#CJK UNIFIED IDEOGRAPH
+	"9C 60	5F56",	#CJK UNIFIED IDEOGRAPH
+	"9C 61	5F57",	#CJK UNIFIED IDEOGRAPH
+	"9C 62	5F59",	#CJK UNIFIED IDEOGRAPH
+	"9C 63	5F61",	#CJK UNIFIED IDEOGRAPH
+	"9C 64	5F6D",	#CJK UNIFIED IDEOGRAPH
+	"9C 65	5F73",	#CJK UNIFIED IDEOGRAPH
+	"9C 66	5F77",	#CJK UNIFIED IDEOGRAPH
+	"9C 67	5F83",	#CJK UNIFIED IDEOGRAPH
+	"9C 68	5F82",	#CJK UNIFIED IDEOGRAPH
+	"9C 69	5F7F",	#CJK UNIFIED IDEOGRAPH
+	"9C 6A	5F8A",	#CJK UNIFIED IDEOGRAPH
+	"9C 6B	5F88",	#CJK UNIFIED IDEOGRAPH
+	"9C 6C	5F91",	#CJK UNIFIED IDEOGRAPH
+	"9C 6D	5F87",	#CJK UNIFIED IDEOGRAPH
+	"9C 6E	5F9E",	#CJK UNIFIED IDEOGRAPH
+	"9C 6F	5F99",	#CJK UNIFIED IDEOGRAPH
+	"9C 70	5F98",	#CJK UNIFIED IDEOGRAPH
+	"9C 71	5FA0",	#CJK UNIFIED IDEOGRAPH
+	"9C 72	5FA8",	#CJK UNIFIED IDEOGRAPH
+	"9C 73	5FAD",	#CJK UNIFIED IDEOGRAPH
+	"9C 74	5FBC",	#CJK UNIFIED IDEOGRAPH
+	"9C 75	5FD6",	#CJK UNIFIED IDEOGRAPH
+	"9C 76	5FFB",	#CJK UNIFIED IDEOGRAPH
+	"9C 77	5FE4",	#CJK UNIFIED IDEOGRAPH
+	"9C 78	5FF8",	#CJK UNIFIED IDEOGRAPH
+	"9C 79	5FF1",	#CJK UNIFIED IDEOGRAPH
+	"9C 7A	5FDD",	#CJK UNIFIED IDEOGRAPH
+	"9C 7B	60B3",	#CJK UNIFIED IDEOGRAPH
+	"9C 7C	5FFF",	#CJK UNIFIED IDEOGRAPH
+	"9C 7D	6021",	#CJK UNIFIED IDEOGRAPH
+	"9C 7E	6060",	#CJK UNIFIED IDEOGRAPH
+	"9C 80	6019",	#CJK UNIFIED IDEOGRAPH
+	"9C 81	6010",	#CJK UNIFIED IDEOGRAPH
+	"9C 82	6029",	#CJK UNIFIED IDEOGRAPH
+	"9C 83	600E",	#CJK UNIFIED IDEOGRAPH
+	"9C 84	6031",	#CJK UNIFIED IDEOGRAPH
+	"9C 85	601B",	#CJK UNIFIED IDEOGRAPH
+	"9C 86	6015",	#CJK UNIFIED IDEOGRAPH
+	"9C 87	602B",	#CJK UNIFIED IDEOGRAPH
+	"9C 88	6026",	#CJK UNIFIED IDEOGRAPH
+	"9C 89	600F",	#CJK UNIFIED IDEOGRAPH
+	"9C 8A	603A",	#CJK UNIFIED IDEOGRAPH
+	"9C 8B	605A",	#CJK UNIFIED IDEOGRAPH
+	"9C 8C	6041",	#CJK UNIFIED IDEOGRAPH
+	"9C 8D	606A",	#CJK UNIFIED IDEOGRAPH
+	"9C 8E	6077",	#CJK UNIFIED IDEOGRAPH
+	"9C 8F	605F",	#CJK UNIFIED IDEOGRAPH
+	"9C 90	604A",	#CJK UNIFIED IDEOGRAPH
+	"9C 91	6046",	#CJK UNIFIED IDEOGRAPH
+	"9C 92	604D",	#CJK UNIFIED IDEOGRAPH
+	"9C 93	6063",	#CJK UNIFIED IDEOGRAPH
+	"9C 94	6043",	#CJK UNIFIED IDEOGRAPH
+	"9C 95	6064",	#CJK UNIFIED IDEOGRAPH
+	"9C 96	6042",	#CJK UNIFIED IDEOGRAPH
+	"9C 97	606C",	#CJK UNIFIED IDEOGRAPH
+	"9C 98	606B",	#CJK UNIFIED IDEOGRAPH
+	"9C 99	6059",	#CJK UNIFIED IDEOGRAPH
+	"9C 9A	6081",	#CJK UNIFIED IDEOGRAPH
+	"9C 9B	608D",	#CJK UNIFIED IDEOGRAPH
+	"9C 9C	60E7",	#CJK UNIFIED IDEOGRAPH
+	"9C 9D	6083",	#CJK UNIFIED IDEOGRAPH
+	"9C 9E	609A",	#CJK UNIFIED IDEOGRAPH
+	"9C 9F	6084",	#CJK UNIFIED IDEOGRAPH
+	"9C A0	609B",	#CJK UNIFIED IDEOGRAPH
+	"9C A1	6096",	#CJK UNIFIED IDEOGRAPH
+	"9C A2	6097",	#CJK UNIFIED IDEOGRAPH
+	"9C A3	6092",	#CJK UNIFIED IDEOGRAPH
+	"9C A4	60A7",	#CJK UNIFIED IDEOGRAPH
+	"9C A5	608B",	#CJK UNIFIED IDEOGRAPH
+	"9C A6	60E1",	#CJK UNIFIED IDEOGRAPH
+	"9C A7	60B8",	#CJK UNIFIED IDEOGRAPH
+	"9C A8	60E0",	#CJK UNIFIED IDEOGRAPH
+	"9C A9	60D3",	#CJK UNIFIED IDEOGRAPH
+	"9C AA	60B4",	#CJK UNIFIED IDEOGRAPH
+	"9C AB	5FF0",	#CJK UNIFIED IDEOGRAPH
+	"9C AC	60BD",	#CJK UNIFIED IDEOGRAPH
+	"9C AD	60C6",	#CJK UNIFIED IDEOGRAPH
+	"9C AE	60B5",	#CJK UNIFIED IDEOGRAPH
+	"9C AF	60D8",	#CJK UNIFIED IDEOGRAPH
+	"9C B0	614D",	#CJK UNIFIED IDEOGRAPH
+	"9C B1	6115",	#CJK UNIFIED IDEOGRAPH
+	"9C B2	6106",	#CJK UNIFIED IDEOGRAPH
+	"9C B3	60F6",	#CJK UNIFIED IDEOGRAPH
+	"9C B4	60F7",	#CJK UNIFIED IDEOGRAPH
+	"9C B5	6100",	#CJK UNIFIED IDEOGRAPH
+	"9C B6	60F4",	#CJK UNIFIED IDEOGRAPH
+	"9C B7	60FA",	#CJK UNIFIED IDEOGRAPH
+	"9C B8	6103",	#CJK UNIFIED IDEOGRAPH
+	"9C B9	6121",	#CJK UNIFIED IDEOGRAPH
+	"9C BA	60FB",	#CJK UNIFIED IDEOGRAPH
+	"9C BB	60F1",	#CJK UNIFIED IDEOGRAPH
+	"9C BC	610D",	#CJK UNIFIED IDEOGRAPH
+	"9C BD	610E",	#CJK UNIFIED IDEOGRAPH
+	"9C BE	6147",	#CJK UNIFIED IDEOGRAPH
+	"9C BF	613E",	#CJK UNIFIED IDEOGRAPH
+	"9C C0	6128",	#CJK UNIFIED IDEOGRAPH
+	"9C C1	6127",	#CJK UNIFIED IDEOGRAPH
+	"9C C2	614A",	#CJK UNIFIED IDEOGRAPH
+	"9C C3	613F",	#CJK UNIFIED IDEOGRAPH
+	"9C C4	613C",	#CJK UNIFIED IDEOGRAPH
+	"9C C5	612C",	#CJK UNIFIED IDEOGRAPH
+	"9C C6	6134",	#CJK UNIFIED IDEOGRAPH
+	"9C C7	613D",	#CJK UNIFIED IDEOGRAPH
+	"9C C8	6142",	#CJK UNIFIED IDEOGRAPH
+	"9C C9	6144",	#CJK UNIFIED IDEOGRAPH
+	"9C CA	6173",	#CJK UNIFIED IDEOGRAPH
+	"9C CB	6177",	#CJK UNIFIED IDEOGRAPH
+	"9C CC	6158",	#CJK UNIFIED IDEOGRAPH
+	"9C CD	6159",	#CJK UNIFIED IDEOGRAPH
+	"9C CE	615A",	#CJK UNIFIED IDEOGRAPH
+	"9C CF	616B",	#CJK UNIFIED IDEOGRAPH
+	"9C D0	6174",	#CJK UNIFIED IDEOGRAPH
+	"9C D1	616F",	#CJK UNIFIED IDEOGRAPH
+	"9C D2	6165",	#CJK UNIFIED IDEOGRAPH
+	"9C D3	6171",	#CJK UNIFIED IDEOGRAPH
+	"9C D4	615F",	#CJK UNIFIED IDEOGRAPH
+	"9C D5	615D",	#CJK UNIFIED IDEOGRAPH
+	"9C D6	6153",	#CJK UNIFIED IDEOGRAPH
+	"9C D7	6175",	#CJK UNIFIED IDEOGRAPH
+	"9C D8	6199",	#CJK UNIFIED IDEOGRAPH
+	"9C D9	6196",	#CJK UNIFIED IDEOGRAPH
+	"9C DA	6187",	#CJK UNIFIED IDEOGRAPH
+	"9C DB	61AC",	#CJK UNIFIED IDEOGRAPH
+	"9C DC	6194",	#CJK UNIFIED IDEOGRAPH
+	"9C DD	619A",	#CJK UNIFIED IDEOGRAPH
+	"9C DE	618A",	#CJK UNIFIED IDEOGRAPH
+	"9C DF	6191",	#CJK UNIFIED IDEOGRAPH
+	"9C E0	61AB",	#CJK UNIFIED IDEOGRAPH
+	"9C E1	61AE",	#CJK UNIFIED IDEOGRAPH
+	"9C E2	61CC",	#CJK UNIFIED IDEOGRAPH
+	"9C E3	61CA",	#CJK UNIFIED IDEOGRAPH
+	"9C E4	61C9",	#CJK UNIFIED IDEOGRAPH
+	"9C E5	61F7",	#CJK UNIFIED IDEOGRAPH
+	"9C E6	61C8",	#CJK UNIFIED IDEOGRAPH
+	"9C E7	61C3",	#CJK UNIFIED IDEOGRAPH
+	"9C E8	61C6",	#CJK UNIFIED IDEOGRAPH
+	"9C E9	61BA",	#CJK UNIFIED IDEOGRAPH
+	"9C EA	61CB",	#CJK UNIFIED IDEOGRAPH
+	"9C EB	7F79",	#CJK UNIFIED IDEOGRAPH
+	"9C EC	61CD",	#CJK UNIFIED IDEOGRAPH
+	"9C ED	61E6",	#CJK UNIFIED IDEOGRAPH
+	"9C EE	61E3",	#CJK UNIFIED IDEOGRAPH
+	"9C EF	61F6",	#CJK UNIFIED IDEOGRAPH
+	"9C F0	61FA",	#CJK UNIFIED IDEOGRAPH
+	"9C F1	61F4",	#CJK UNIFIED IDEOGRAPH
+	"9C F2	61FF",	#CJK UNIFIED IDEOGRAPH
+	"9C F3	61FD",	#CJK UNIFIED IDEOGRAPH
+	"9C F4	61FC",	#CJK UNIFIED IDEOGRAPH
+	"9C F5	61FE",	#CJK UNIFIED IDEOGRAPH
+	"9C F6	6200",	#CJK UNIFIED IDEOGRAPH
+	"9C F7	6208",	#CJK UNIFIED IDEOGRAPH
+	"9C F8	6209",	#CJK UNIFIED IDEOGRAPH
+	"9C F9	620D",	#CJK UNIFIED IDEOGRAPH
+	"9C FA	620C",	#CJK UNIFIED IDEOGRAPH
+	"9C FB	6214",	#CJK UNIFIED IDEOGRAPH
+	"9C FC	621B",	#CJK UNIFIED IDEOGRAPH
+	"9D 40	621E",	#CJK UNIFIED IDEOGRAPH
+	"9D 41	6221",	#CJK UNIFIED IDEOGRAPH
+	"9D 42	622A",	#CJK UNIFIED IDEOGRAPH
+	"9D 43	622E",	#CJK UNIFIED IDEOGRAPH
+	"9D 44	6230",	#CJK UNIFIED IDEOGRAPH
+	"9D 45	6232",	#CJK UNIFIED IDEOGRAPH
+	"9D 46	6233",	#CJK UNIFIED IDEOGRAPH
+	"9D 47	6241",	#CJK UNIFIED IDEOGRAPH
+	"9D 48	624E",	#CJK UNIFIED IDEOGRAPH
+	"9D 49	625E",	#CJK UNIFIED IDEOGRAPH
+	"9D 4A	6263",	#CJK UNIFIED IDEOGRAPH
+	"9D 4B	625B",	#CJK UNIFIED IDEOGRAPH
+	"9D 4C	6260",	#CJK UNIFIED IDEOGRAPH
+	"9D 4D	6268",	#CJK UNIFIED IDEOGRAPH
+	"9D 4E	627C",	#CJK UNIFIED IDEOGRAPH
+	"9D 4F	6282",	#CJK UNIFIED IDEOGRAPH
+	"9D 50	6289",	#CJK UNIFIED IDEOGRAPH
+	"9D 51	627E",	#CJK UNIFIED IDEOGRAPH
+	"9D 52	6292",	#CJK UNIFIED IDEOGRAPH
+	"9D 53	6293",	#CJK UNIFIED IDEOGRAPH
+	"9D 54	6296",	#CJK UNIFIED IDEOGRAPH
+	"9D 55	62D4",	#CJK UNIFIED IDEOGRAPH
+	"9D 56	6283",	#CJK UNIFIED IDEOGRAPH
+	"9D 57	6294",	#CJK UNIFIED IDEOGRAPH
+	"9D 58	62D7",	#CJK UNIFIED IDEOGRAPH
+	"9D 59	62D1",	#CJK UNIFIED IDEOGRAPH
+	"9D 5A	62BB",	#CJK UNIFIED IDEOGRAPH
+	"9D 5B	62CF",	#CJK UNIFIED IDEOGRAPH
+	"9D 5C	62FF",	#CJK UNIFIED IDEOGRAPH
+	"9D 5D	62C6",	#CJK UNIFIED IDEOGRAPH
+	"9D 5E	64D4",	#CJK UNIFIED IDEOGRAPH
+	"9D 5F	62C8",	#CJK UNIFIED IDEOGRAPH
+	"9D 60	62DC",	#CJK UNIFIED IDEOGRAPH
+	"9D 61	62CC",	#CJK UNIFIED IDEOGRAPH
+	"9D 62	62CA",	#CJK UNIFIED IDEOGRAPH
+	"9D 63	62C2",	#CJK UNIFIED IDEOGRAPH
+	"9D 64	62C7",	#CJK UNIFIED IDEOGRAPH
+	"9D 65	629B",	#CJK UNIFIED IDEOGRAPH
+	"9D 66	62C9",	#CJK UNIFIED IDEOGRAPH
+	"9D 67	630C",	#CJK UNIFIED IDEOGRAPH
+	"9D 68	62EE",	#CJK UNIFIED IDEOGRAPH
+	"9D 69	62F1",	#CJK UNIFIED IDEOGRAPH
+	"9D 6A	6327",	#CJK UNIFIED IDEOGRAPH
+	"9D 6B	6302",	#CJK UNIFIED IDEOGRAPH
+	"9D 6C	6308",	#CJK UNIFIED IDEOGRAPH
+	"9D 6D	62EF",	#CJK UNIFIED IDEOGRAPH
+	"9D 6E	62F5",	#CJK UNIFIED IDEOGRAPH
+	"9D 6F	6350",	#CJK UNIFIED IDEOGRAPH
+	"9D 70	633E",	#CJK UNIFIED IDEOGRAPH
+	"9D 71	634D",	#CJK UNIFIED IDEOGRAPH
+	"9D 72	641C",	#CJK UNIFIED IDEOGRAPH
+	"9D 73	634F",	#CJK UNIFIED IDEOGRAPH
+	"9D 74	6396",	#CJK UNIFIED IDEOGRAPH
+	"9D 75	638E",	#CJK UNIFIED IDEOGRAPH
+	"9D 76	6380",	#CJK UNIFIED IDEOGRAPH
+	"9D 77	63AB",	#CJK UNIFIED IDEOGRAPH
+	"9D 78	6376",	#CJK UNIFIED IDEOGRAPH
+	"9D 79	63A3",	#CJK UNIFIED IDEOGRAPH
+	"9D 7A	638F",	#CJK UNIFIED IDEOGRAPH
+	"9D 7B	6389",	#CJK UNIFIED IDEOGRAPH
+	"9D 7C	639F",	#CJK UNIFIED IDEOGRAPH
+	"9D 7D	63B5",	#CJK UNIFIED IDEOGRAPH
+	"9D 7E	636B",	#CJK UNIFIED IDEOGRAPH
+	"9D 80	6369",	#CJK UNIFIED IDEOGRAPH
+	"9D 81	63BE",	#CJK UNIFIED IDEOGRAPH
+	"9D 82	63E9",	#CJK UNIFIED IDEOGRAPH
+	"9D 83	63C0",	#CJK UNIFIED IDEOGRAPH
+	"9D 84	63C6",	#CJK UNIFIED IDEOGRAPH
+	"9D 85	63E3",	#CJK UNIFIED IDEOGRAPH
+	"9D 86	63C9",	#CJK UNIFIED IDEOGRAPH
+	"9D 87	63D2",	#CJK UNIFIED IDEOGRAPH
+	"9D 88	63F6",	#CJK UNIFIED IDEOGRAPH
+	"9D 89	63C4",	#CJK UNIFIED IDEOGRAPH
+	"9D 8A	6416",	#CJK UNIFIED IDEOGRAPH
+	"9D 8B	6434",	#CJK UNIFIED IDEOGRAPH
+	"9D 8C	6406",	#CJK UNIFIED IDEOGRAPH
+	"9D 8D	6413",	#CJK UNIFIED IDEOGRAPH
+	"9D 8E	6426",	#CJK UNIFIED IDEOGRAPH
+	"9D 8F	6436",	#CJK UNIFIED IDEOGRAPH
+	"9D 90	651D",	#CJK UNIFIED IDEOGRAPH
+	"9D 91	6417",	#CJK UNIFIED IDEOGRAPH
+	"9D 92	6428",	#CJK UNIFIED IDEOGRAPH
+	"9D 93	640F",	#CJK UNIFIED IDEOGRAPH
+	"9D 94	6467",	#CJK UNIFIED IDEOGRAPH
+	"9D 95	646F",	#CJK UNIFIED IDEOGRAPH
+	"9D 96	6476",	#CJK UNIFIED IDEOGRAPH
+	"9D 97	644E",	#CJK UNIFIED IDEOGRAPH
+	"9D 98	652A",	#CJK UNIFIED IDEOGRAPH
+	"9D 99	6495",	#CJK UNIFIED IDEOGRAPH
+	"9D 9A	6493",	#CJK UNIFIED IDEOGRAPH
+	"9D 9B	64A5",	#CJK UNIFIED IDEOGRAPH
+	"9D 9C	64A9",	#CJK UNIFIED IDEOGRAPH
+	"9D 9D	6488",	#CJK UNIFIED IDEOGRAPH
+	"9D 9E	64BC",	#CJK UNIFIED IDEOGRAPH
+	"9D 9F	64DA",	#CJK UNIFIED IDEOGRAPH
+	"9D A0	64D2",	#CJK UNIFIED IDEOGRAPH
+	"9D A1	64C5",	#CJK UNIFIED IDEOGRAPH
+	"9D A2	64C7",	#CJK UNIFIED IDEOGRAPH
+	"9D A3	64BB",	#CJK UNIFIED IDEOGRAPH
+	"9D A4	64D8",	#CJK UNIFIED IDEOGRAPH
+	"9D A5	64C2",	#CJK UNIFIED IDEOGRAPH
+	"9D A6	64F1",	#CJK UNIFIED IDEOGRAPH
+	"9D A7	64E7",	#CJK UNIFIED IDEOGRAPH
+	"9D A8	8209",	#CJK UNIFIED IDEOGRAPH
+	"9D A9	64E0",	#CJK UNIFIED IDEOGRAPH
+	"9D AA	64E1",	#CJK UNIFIED IDEOGRAPH
+	"9D AB	62AC",	#CJK UNIFIED IDEOGRAPH
+	"9D AC	64E3",	#CJK UNIFIED IDEOGRAPH
+	"9D AD	64EF",	#CJK UNIFIED IDEOGRAPH
+	"9D AE	652C",	#CJK UNIFIED IDEOGRAPH
+	"9D AF	64F6",	#CJK UNIFIED IDEOGRAPH
+	"9D B0	64F4",	#CJK UNIFIED IDEOGRAPH
+	"9D B1	64F2",	#CJK UNIFIED IDEOGRAPH
+	"9D B2	64FA",	#CJK UNIFIED IDEOGRAPH
+	"9D B3	6500",	#CJK UNIFIED IDEOGRAPH
+	"9D B4	64FD",	#CJK UNIFIED IDEOGRAPH
+	"9D B5	6518",	#CJK UNIFIED IDEOGRAPH
+	"9D B6	651C",	#CJK UNIFIED IDEOGRAPH
+	"9D B7	6505",	#CJK UNIFIED IDEOGRAPH
+	"9D B8	6524",	#CJK UNIFIED IDEOGRAPH
+	"9D B9	6523",	#CJK UNIFIED IDEOGRAPH
+	"9D BA	652B",	#CJK UNIFIED IDEOGRAPH
+	"9D BB	6534",	#CJK UNIFIED IDEOGRAPH
+	"9D BC	6535",	#CJK UNIFIED IDEOGRAPH
+	"9D BD	6537",	#CJK UNIFIED IDEOGRAPH
+	"9D BE	6536",	#CJK UNIFIED IDEOGRAPH
+	"9D BF	6538",	#CJK UNIFIED IDEOGRAPH
+	"9D C0	754B",	#CJK UNIFIED IDEOGRAPH
+	"9D C1	6548",	#CJK UNIFIED IDEOGRAPH
+	"9D C2	6556",	#CJK UNIFIED IDEOGRAPH
+	"9D C3	6555",	#CJK UNIFIED IDEOGRAPH
+	"9D C4	654D",	#CJK UNIFIED IDEOGRAPH
+	"9D C5	6558",	#CJK UNIFIED IDEOGRAPH
+	"9D C6	655E",	#CJK UNIFIED IDEOGRAPH
+	"9D C7	655D",	#CJK UNIFIED IDEOGRAPH
+	"9D C8	6572",	#CJK UNIFIED IDEOGRAPH
+	"9D C9	6578",	#CJK UNIFIED IDEOGRAPH
+	"9D CA	6582",	#CJK UNIFIED IDEOGRAPH
+	"9D CB	6583",	#CJK UNIFIED IDEOGRAPH
+	"9D CC	8B8A",	#CJK UNIFIED IDEOGRAPH
+	"9D CD	659B",	#CJK UNIFIED IDEOGRAPH
+	"9D CE	659F",	#CJK UNIFIED IDEOGRAPH
+	"9D CF	65AB",	#CJK UNIFIED IDEOGRAPH
+	"9D D0	65B7",	#CJK UNIFIED IDEOGRAPH
+	"9D D1	65C3",	#CJK UNIFIED IDEOGRAPH
+	"9D D2	65C6",	#CJK UNIFIED IDEOGRAPH
+	"9D D3	65C1",	#CJK UNIFIED IDEOGRAPH
+	"9D D4	65C4",	#CJK UNIFIED IDEOGRAPH
+	"9D D5	65CC",	#CJK UNIFIED IDEOGRAPH
+	"9D D6	65D2",	#CJK UNIFIED IDEOGRAPH
+	"9D D7	65DB",	#CJK UNIFIED IDEOGRAPH
+	"9D D8	65D9",	#CJK UNIFIED IDEOGRAPH
+	"9D D9	65E0",	#CJK UNIFIED IDEOGRAPH
+	"9D DA	65E1",	#CJK UNIFIED IDEOGRAPH
+	"9D DB	65F1",	#CJK UNIFIED IDEOGRAPH
+	"9D DC	6772",	#CJK UNIFIED IDEOGRAPH
+	"9D DD	660A",	#CJK UNIFIED IDEOGRAPH
+	"9D DE	6603",	#CJK UNIFIED IDEOGRAPH
+	"9D DF	65FB",	#CJK UNIFIED IDEOGRAPH
+	"9D E0	6773",	#CJK UNIFIED IDEOGRAPH
+	"9D E1	6635",	#CJK UNIFIED IDEOGRAPH
+	"9D E2	6636",	#CJK UNIFIED IDEOGRAPH
+	"9D E3	6634",	#CJK UNIFIED IDEOGRAPH
+	"9D E4	661C",	#CJK UNIFIED IDEOGRAPH
+	"9D E5	664F",	#CJK UNIFIED IDEOGRAPH
+	"9D E6	6644",	#CJK UNIFIED IDEOGRAPH
+	"9D E7	6649",	#CJK UNIFIED IDEOGRAPH
+	"9D E8	6641",	#CJK UNIFIED IDEOGRAPH
+	"9D E9	665E",	#CJK UNIFIED IDEOGRAPH
+	"9D EA	665D",	#CJK UNIFIED IDEOGRAPH
+	"9D EB	6664",	#CJK UNIFIED IDEOGRAPH
+	"9D EC	6667",	#CJK UNIFIED IDEOGRAPH
+	"9D ED	6668",	#CJK UNIFIED IDEOGRAPH
+	"9D EE	665F",	#CJK UNIFIED IDEOGRAPH
+	"9D EF	6662",	#CJK UNIFIED IDEOGRAPH
+	"9D F0	6670",	#CJK UNIFIED IDEOGRAPH
+	"9D F1	6683",	#CJK UNIFIED IDEOGRAPH
+	"9D F2	6688",	#CJK UNIFIED IDEOGRAPH
+	"9D F3	668E",	#CJK UNIFIED IDEOGRAPH
+	"9D F4	6689",	#CJK UNIFIED IDEOGRAPH
+	"9D F5	6684",	#CJK UNIFIED IDEOGRAPH
+	"9D F6	6698",	#CJK UNIFIED IDEOGRAPH
+	"9D F7	669D",	#CJK UNIFIED IDEOGRAPH
+	"9D F8	66C1",	#CJK UNIFIED IDEOGRAPH
+	"9D F9	66B9",	#CJK UNIFIED IDEOGRAPH
+	"9D FA	66C9",	#CJK UNIFIED IDEOGRAPH
+	"9D FB	66BE",	#CJK UNIFIED IDEOGRAPH
+	"9D FC	66BC",	#CJK UNIFIED IDEOGRAPH
+	"9E 40	66C4",	#CJK UNIFIED IDEOGRAPH
+	"9E 41	66B8",	#CJK UNIFIED IDEOGRAPH
+	"9E 42	66D6",	#CJK UNIFIED IDEOGRAPH
+	"9E 43	66DA",	#CJK UNIFIED IDEOGRAPH
+	"9E 44	66E0",	#CJK UNIFIED IDEOGRAPH
+	"9E 45	663F",	#CJK UNIFIED IDEOGRAPH
+	"9E 46	66E6",	#CJK UNIFIED IDEOGRAPH
+	"9E 47	66E9",	#CJK UNIFIED IDEOGRAPH
+	"9E 48	66F0",	#CJK UNIFIED IDEOGRAPH
+	"9E 49	66F5",	#CJK UNIFIED IDEOGRAPH
+	"9E 4A	66F7",	#CJK UNIFIED IDEOGRAPH
+	"9E 4B	670F",	#CJK UNIFIED IDEOGRAPH
+	"9E 4C	6716",	#CJK UNIFIED IDEOGRAPH
+	"9E 4D	671E",	#CJK UNIFIED IDEOGRAPH
+	"9E 4E	6726",	#CJK UNIFIED IDEOGRAPH
+	"9E 4F	6727",	#CJK UNIFIED IDEOGRAPH
+	"9E 50	9738",	#CJK UNIFIED IDEOGRAPH
+	"9E 51	672E",	#CJK UNIFIED IDEOGRAPH
+	"9E 52	673F",	#CJK UNIFIED IDEOGRAPH
+	"9E 53	6736",	#CJK UNIFIED IDEOGRAPH
+	"9E 54	6741",	#CJK UNIFIED IDEOGRAPH
+	"9E 55	6738",	#CJK UNIFIED IDEOGRAPH
+	"9E 56	6737",	#CJK UNIFIED IDEOGRAPH
+	"9E 57	6746",	#CJK UNIFIED IDEOGRAPH
+	"9E 58	675E",	#CJK UNIFIED IDEOGRAPH
+	"9E 59	6760",	#CJK UNIFIED IDEOGRAPH
+	"9E 5A	6759",	#CJK UNIFIED IDEOGRAPH
+	"9E 5B	6763",	#CJK UNIFIED IDEOGRAPH
+	"9E 5C	6764",	#CJK UNIFIED IDEOGRAPH
+	"9E 5D	6789",	#CJK UNIFIED IDEOGRAPH
+	"9E 5E	6770",	#CJK UNIFIED IDEOGRAPH
+	"9E 5F	67A9",	#CJK UNIFIED IDEOGRAPH
+	"9E 60	677C",	#CJK UNIFIED IDEOGRAPH
+	"9E 61	676A",	#CJK UNIFIED IDEOGRAPH
+	"9E 62	678C",	#CJK UNIFIED IDEOGRAPH
+	"9E 63	678B",	#CJK UNIFIED IDEOGRAPH
+	"9E 64	67A6",	#CJK UNIFIED IDEOGRAPH
+	"9E 65	67A1",	#CJK UNIFIED IDEOGRAPH
+	"9E 66	6785",	#CJK UNIFIED IDEOGRAPH
+	"9E 67	67B7",	#CJK UNIFIED IDEOGRAPH
+	"9E 68	67EF",	#CJK UNIFIED IDEOGRAPH
+	"9E 69	67B4",	#CJK UNIFIED IDEOGRAPH
+	"9E 6A	67EC",	#CJK UNIFIED IDEOGRAPH
+	"9E 6B	67B3",	#CJK UNIFIED IDEOGRAPH
+	"9E 6C	67E9",	#CJK UNIFIED IDEOGRAPH
+	"9E 6D	67B8",	#CJK UNIFIED IDEOGRAPH
+	"9E 6E	67E4",	#CJK UNIFIED IDEOGRAPH
+	"9E 6F	67DE",	#CJK UNIFIED IDEOGRAPH
+	"9E 70	67DD",	#CJK UNIFIED IDEOGRAPH
+	"9E 71	67E2",	#CJK UNIFIED IDEOGRAPH
+	"9E 72	67EE",	#CJK UNIFIED IDEOGRAPH
+	"9E 73	67B9",	#CJK UNIFIED IDEOGRAPH
+	"9E 74	67CE",	#CJK UNIFIED IDEOGRAPH
+	"9E 75	67C6",	#CJK UNIFIED IDEOGRAPH
+	"9E 76	67E7",	#CJK UNIFIED IDEOGRAPH
+	"9E 77	6A9C",	#CJK UNIFIED IDEOGRAPH
+	"9E 78	681E",	#CJK UNIFIED IDEOGRAPH
+	"9E 79	6846",	#CJK UNIFIED IDEOGRAPH
+	"9E 7A	6829",	#CJK UNIFIED IDEOGRAPH
+	"9E 7B	6840",	#CJK UNIFIED IDEOGRAPH
+	"9E 7C	684D",	#CJK UNIFIED IDEOGRAPH
+	"9E 7D	6832",	#CJK UNIFIED IDEOGRAPH
+	"9E 7E	684E",	#CJK UNIFIED IDEOGRAPH
+	"9E 80	68B3",	#CJK UNIFIED IDEOGRAPH
+	"9E 81	682B",	#CJK UNIFIED IDEOGRAPH
+	"9E 82	6859",	#CJK UNIFIED IDEOGRAPH
+	"9E 83	6863",	#CJK UNIFIED IDEOGRAPH
+	"9E 84	6877",	#CJK UNIFIED IDEOGRAPH
+	"9E 85	687F",	#CJK UNIFIED IDEOGRAPH
+	"9E 86	689F",	#CJK UNIFIED IDEOGRAPH
+	"9E 87	688F",	#CJK UNIFIED IDEOGRAPH
+	"9E 88	68AD",	#CJK UNIFIED IDEOGRAPH
+	"9E 89	6894",	#CJK UNIFIED IDEOGRAPH
+	"9E 8A	689D",	#CJK UNIFIED IDEOGRAPH
+	"9E 8B	689B",	#CJK UNIFIED IDEOGRAPH
+	"9E 8C	6883",	#CJK UNIFIED IDEOGRAPH
+	"9E 8D	6AAE",	#CJK UNIFIED IDEOGRAPH
+	"9E 8E	68B9",	#CJK UNIFIED IDEOGRAPH
+	"9E 8F	6874",	#CJK UNIFIED IDEOGRAPH
+	"9E 90	68B5",	#CJK UNIFIED IDEOGRAPH
+	"9E 91	68A0",	#CJK UNIFIED IDEOGRAPH
+	"9E 92	68BA",	#CJK UNIFIED IDEOGRAPH
+	"9E 93	690F",	#CJK UNIFIED IDEOGRAPH
+	"9E 94	688D",	#CJK UNIFIED IDEOGRAPH
+	"9E 95	687E",	#CJK UNIFIED IDEOGRAPH
+	"9E 96	6901",	#CJK UNIFIED IDEOGRAPH
+	"9E 97	68CA",	#CJK UNIFIED IDEOGRAPH
+	"9E 98	6908",	#CJK UNIFIED IDEOGRAPH
+	"9E 99	68D8",	#CJK UNIFIED IDEOGRAPH
+	"9E 9A	6922",	#CJK UNIFIED IDEOGRAPH
+	"9E 9B	6926",	#CJK UNIFIED IDEOGRAPH
+	"9E 9C	68E1",	#CJK UNIFIED IDEOGRAPH
+	"9E 9D	690C",	#CJK UNIFIED IDEOGRAPH
+	"9E 9E	68CD",	#CJK UNIFIED IDEOGRAPH
+	"9E 9F	68D4",	#CJK UNIFIED IDEOGRAPH
+	"9E A0	68E7",	#CJK UNIFIED IDEOGRAPH
+	"9E A1	68D5",	#CJK UNIFIED IDEOGRAPH
+	"9E A2	6936",	#CJK UNIFIED IDEOGRAPH
+	"9E A3	6912",	#CJK UNIFIED IDEOGRAPH
+	"9E A4	6904",	#CJK UNIFIED IDEOGRAPH
+	"9E A5	68D7",	#CJK UNIFIED IDEOGRAPH
+	"9E A6	68E3",	#CJK UNIFIED IDEOGRAPH
+	"9E A7	6925",	#CJK UNIFIED IDEOGRAPH
+	"9E A8	68F9",	#CJK UNIFIED IDEOGRAPH
+	"9E A9	68E0",	#CJK UNIFIED IDEOGRAPH
+	"9E AA	68EF",	#CJK UNIFIED IDEOGRAPH
+	"9E AB	6928",	#CJK UNIFIED IDEOGRAPH
+	"9E AC	692A",	#CJK UNIFIED IDEOGRAPH
+	"9E AD	691A",	#CJK UNIFIED IDEOGRAPH
+	"9E AE	6923",	#CJK UNIFIED IDEOGRAPH
+	"9E AF	6921",	#CJK UNIFIED IDEOGRAPH
+	"9E B0	68C6",	#CJK UNIFIED IDEOGRAPH
+	"9E B1	6979",	#CJK UNIFIED IDEOGRAPH
+	"9E B2	6977",	#CJK UNIFIED IDEOGRAPH
+	"9E B3	695C",	#CJK UNIFIED IDEOGRAPH
+	"9E B4	6978",	#CJK UNIFIED IDEOGRAPH
+	"9E B5	696B",	#CJK UNIFIED IDEOGRAPH
+	"9E B6	6954",	#CJK UNIFIED IDEOGRAPH
+	"9E B7	697E",	#CJK UNIFIED IDEOGRAPH
+	"9E B8	696E",	#CJK UNIFIED IDEOGRAPH
+	"9E B9	6939",	#CJK UNIFIED IDEOGRAPH
+	"9E BA	6974",	#CJK UNIFIED IDEOGRAPH
+	"9E BB	693D",	#CJK UNIFIED IDEOGRAPH
+	"9E BC	6959",	#CJK UNIFIED IDEOGRAPH
+	"9E BD	6930",	#CJK UNIFIED IDEOGRAPH
+	"9E BE	6961",	#CJK UNIFIED IDEOGRAPH
+	"9E BF	695E",	#CJK UNIFIED IDEOGRAPH
+	"9E C0	695D",	#CJK UNIFIED IDEOGRAPH
+	"9E C1	6981",	#CJK UNIFIED IDEOGRAPH
+	"9E C2	696A",	#CJK UNIFIED IDEOGRAPH
+	"9E C3	69B2",	#CJK UNIFIED IDEOGRAPH
+	"9E C4	69AE",	#CJK UNIFIED IDEOGRAPH
+	"9E C5	69D0",	#CJK UNIFIED IDEOGRAPH
+	"9E C6	69BF",	#CJK UNIFIED IDEOGRAPH
+	"9E C7	69C1",	#CJK UNIFIED IDEOGRAPH
+	"9E C8	69D3",	#CJK UNIFIED IDEOGRAPH
+	"9E C9	69BE",	#CJK UNIFIED IDEOGRAPH
+	"9E CA	69CE",	#CJK UNIFIED IDEOGRAPH
+	"9E CB	5BE8",	#CJK UNIFIED IDEOGRAPH
+	"9E CC	69CA",	#CJK UNIFIED IDEOGRAPH
+	"9E CD	69DD",	#CJK UNIFIED IDEOGRAPH
+	"9E CE	69BB",	#CJK UNIFIED IDEOGRAPH
+	"9E CF	69C3",	#CJK UNIFIED IDEOGRAPH
+	"9E D0	69A7",	#CJK UNIFIED IDEOGRAPH
+	"9E D1	6A2E",	#CJK UNIFIED IDEOGRAPH
+	"9E D2	6991",	#CJK UNIFIED IDEOGRAPH
+	"9E D3	69A0",	#CJK UNIFIED IDEOGRAPH
+	"9E D4	699C",	#CJK UNIFIED IDEOGRAPH
+	"9E D5	6995",	#CJK UNIFIED IDEOGRAPH
+	"9E D6	69B4",	#CJK UNIFIED IDEOGRAPH
+	"9E D7	69DE",	#CJK UNIFIED IDEOGRAPH
+	"9E D8	69E8",	#CJK UNIFIED IDEOGRAPH
+	"9E D9	6A02",	#CJK UNIFIED IDEOGRAPH
+	"9E DA	6A1B",	#CJK UNIFIED IDEOGRAPH
+	"9E DB	69FF",	#CJK UNIFIED IDEOGRAPH
+	"9E DC	6B0A",	#CJK UNIFIED IDEOGRAPH
+	"9E DD	69F9",	#CJK UNIFIED IDEOGRAPH
+	"9E DE	69F2",	#CJK UNIFIED IDEOGRAPH
+	"9E DF	69E7",	#CJK UNIFIED IDEOGRAPH
+	"9E E0	6A05",	#CJK UNIFIED IDEOGRAPH
+	"9E E1	69B1",	#CJK UNIFIED IDEOGRAPH
+	"9E E2	6A1E",	#CJK UNIFIED IDEOGRAPH
+	"9E E3	69ED",	#CJK UNIFIED IDEOGRAPH
+	"9E E4	6A14",	#CJK UNIFIED IDEOGRAPH
+	"9E E5	69EB",	#CJK UNIFIED IDEOGRAPH
+	"9E E6	6A0A",	#CJK UNIFIED IDEOGRAPH
+	"9E E7	6A12",	#CJK UNIFIED IDEOGRAPH
+	"9E E8	6AC1",	#CJK UNIFIED IDEOGRAPH
+	"9E E9	6A23",	#CJK UNIFIED IDEOGRAPH
+	"9E EA	6A13",	#CJK UNIFIED IDEOGRAPH
+	"9E EB	6A44",	#CJK UNIFIED IDEOGRAPH
+	"9E EC	6A0C",	#CJK UNIFIED IDEOGRAPH
+	"9E ED	6A72",	#CJK UNIFIED IDEOGRAPH
+	"9E EE	6A36",	#CJK UNIFIED IDEOGRAPH
+	"9E EF	6A78",	#CJK UNIFIED IDEOGRAPH
+	"9E F0	6A47",	#CJK UNIFIED IDEOGRAPH
+	"9E F1	6A62",	#CJK UNIFIED IDEOGRAPH
+	"9E F2	6A59",	#CJK UNIFIED IDEOGRAPH
+	"9E F3	6A66",	#CJK UNIFIED IDEOGRAPH
+	"9E F4	6A48",	#CJK UNIFIED IDEOGRAPH
+	"9E F5	6A38",	#CJK UNIFIED IDEOGRAPH
+	"9E F6	6A22",	#CJK UNIFIED IDEOGRAPH
+	"9E F7	6A90",	#CJK UNIFIED IDEOGRAPH
+	"9E F8	6A8D",	#CJK UNIFIED IDEOGRAPH
+	"9E F9	6AA0",	#CJK UNIFIED IDEOGRAPH
+	"9E FA	6A84",	#CJK UNIFIED IDEOGRAPH
+	"9E FB	6AA2",	#CJK UNIFIED IDEOGRAPH
+	"9E FC	6AA3",	#CJK UNIFIED IDEOGRAPH
+	"9F 40	6A97",	#CJK UNIFIED IDEOGRAPH
+	"9F 41	8617",	#CJK UNIFIED IDEOGRAPH
+	"9F 42	6ABB",	#CJK UNIFIED IDEOGRAPH
+	"9F 43	6AC3",	#CJK UNIFIED IDEOGRAPH
+	"9F 44	6AC2",	#CJK UNIFIED IDEOGRAPH
+	"9F 45	6AB8",	#CJK UNIFIED IDEOGRAPH
+	"9F 46	6AB3",	#CJK UNIFIED IDEOGRAPH
+	"9F 47	6AAC",	#CJK UNIFIED IDEOGRAPH
+	"9F 48	6ADE",	#CJK UNIFIED IDEOGRAPH
+	"9F 49	6AD1",	#CJK UNIFIED IDEOGRAPH
+	"9F 4A	6ADF",	#CJK UNIFIED IDEOGRAPH
+	"9F 4B	6AAA",	#CJK UNIFIED IDEOGRAPH
+	"9F 4C	6ADA",	#CJK UNIFIED IDEOGRAPH
+	"9F 4D	6AEA",	#CJK UNIFIED IDEOGRAPH
+	"9F 4E	6AFB",	#CJK UNIFIED IDEOGRAPH
+	"9F 4F	6B05",	#CJK UNIFIED IDEOGRAPH
+	"9F 50	8616",	#CJK UNIFIED IDEOGRAPH
+	"9F 51	6AFA",	#CJK UNIFIED IDEOGRAPH
+	"9F 52	6B12",	#CJK UNIFIED IDEOGRAPH
+	"9F 53	6B16",	#CJK UNIFIED IDEOGRAPH
+	"9F 54	9B31",	#CJK UNIFIED IDEOGRAPH
+	"9F 55	6B1F",	#CJK UNIFIED IDEOGRAPH
+	"9F 56	6B38",	#CJK UNIFIED IDEOGRAPH
+	"9F 57	6B37",	#CJK UNIFIED IDEOGRAPH
+	"9F 58	76DC",	#CJK UNIFIED IDEOGRAPH
+	"9F 59	6B39",	#CJK UNIFIED IDEOGRAPH
+	"9F 5A	98EE",	#CJK UNIFIED IDEOGRAPH
+	"9F 5B	6B47",	#CJK UNIFIED IDEOGRAPH
+	"9F 5C	6B43",	#CJK UNIFIED IDEOGRAPH
+	"9F 5D	6B49",	#CJK UNIFIED IDEOGRAPH
+	"9F 5E	6B50",	#CJK UNIFIED IDEOGRAPH
+	"9F 5F	6B59",	#CJK UNIFIED IDEOGRAPH
+	"9F 60	6B54",	#CJK UNIFIED IDEOGRAPH
+	"9F 61	6B5B",	#CJK UNIFIED IDEOGRAPH
+	"9F 62	6B5F",	#CJK UNIFIED IDEOGRAPH
+	"9F 63	6B61",	#CJK UNIFIED IDEOGRAPH
+	"9F 64	6B78",	#CJK UNIFIED IDEOGRAPH
+	"9F 65	6B79",	#CJK UNIFIED IDEOGRAPH
+	"9F 66	6B7F",	#CJK UNIFIED IDEOGRAPH
+	"9F 67	6B80",	#CJK UNIFIED IDEOGRAPH
+	"9F 68	6B84",	#CJK UNIFIED IDEOGRAPH
+	"9F 69	6B83",	#CJK UNIFIED IDEOGRAPH
+	"9F 6A	6B8D",	#CJK UNIFIED IDEOGRAPH
+	"9F 6B	6B98",	#CJK UNIFIED IDEOGRAPH
+	"9F 6C	6B95",	#CJK UNIFIED IDEOGRAPH
+	"9F 6D	6B9E",	#CJK UNIFIED IDEOGRAPH
+	"9F 6E	6BA4",	#CJK UNIFIED IDEOGRAPH
+	"9F 6F	6BAA",	#CJK UNIFIED IDEOGRAPH
+	"9F 70	6BAB",	#CJK UNIFIED IDEOGRAPH
+	"9F 71	6BAF",	#CJK UNIFIED IDEOGRAPH
+	"9F 72	6BB2",	#CJK UNIFIED IDEOGRAPH
+	"9F 73	6BB1",	#CJK UNIFIED IDEOGRAPH
+	"9F 74	6BB3",	#CJK UNIFIED IDEOGRAPH
+	"9F 75	6BB7",	#CJK UNIFIED IDEOGRAPH
+	"9F 76	6BBC",	#CJK UNIFIED IDEOGRAPH
+	"9F 77	6BC6",	#CJK UNIFIED IDEOGRAPH
+	"9F 78	6BCB",	#CJK UNIFIED IDEOGRAPH
+	"9F 79	6BD3",	#CJK UNIFIED IDEOGRAPH
+	"9F 7A	6BDF",	#CJK UNIFIED IDEOGRAPH
+	"9F 7B	6BEC",	#CJK UNIFIED IDEOGRAPH
+	"9F 7C	6BEB",	#CJK UNIFIED IDEOGRAPH
+	"9F 7D	6BF3",	#CJK UNIFIED IDEOGRAPH
+	"9F 7E	6BEF",	#CJK UNIFIED IDEOGRAPH
+	"9F 80	9EBE",	#CJK UNIFIED IDEOGRAPH
+	"9F 81	6C08",	#CJK UNIFIED IDEOGRAPH
+	"9F 82	6C13",	#CJK UNIFIED IDEOGRAPH
+	"9F 83	6C14",	#CJK UNIFIED IDEOGRAPH
+	"9F 84	6C1B",	#CJK UNIFIED IDEOGRAPH
+	"9F 85	6C24",	#CJK UNIFIED IDEOGRAPH
+	"9F 86	6C23",	#CJK UNIFIED IDEOGRAPH
+	"9F 87	6C5E",	#CJK UNIFIED IDEOGRAPH
+	"9F 88	6C55",	#CJK UNIFIED IDEOGRAPH
+	"9F 89	6C62",	#CJK UNIFIED IDEOGRAPH
+	"9F 8A	6C6A",	#CJK UNIFIED IDEOGRAPH
+	"9F 8B	6C82",	#CJK UNIFIED IDEOGRAPH
+	"9F 8C	6C8D",	#CJK UNIFIED IDEOGRAPH
+	"9F 8D	6C9A",	#CJK UNIFIED IDEOGRAPH
+	"9F 8E	6C81",	#CJK UNIFIED IDEOGRAPH
+	"9F 8F	6C9B",	#CJK UNIFIED IDEOGRAPH
+	"9F 90	6C7E",	#CJK UNIFIED IDEOGRAPH
+	"9F 91	6C68",	#CJK UNIFIED IDEOGRAPH
+	"9F 92	6C73",	#CJK UNIFIED IDEOGRAPH
+	"9F 93	6C92",	#CJK UNIFIED IDEOGRAPH
+	"9F 94	6C90",	#CJK UNIFIED IDEOGRAPH
+	"9F 95	6CC4",	#CJK UNIFIED IDEOGRAPH
+	"9F 96	6CF1",	#CJK UNIFIED IDEOGRAPH
+	"9F 97	6CD3",	#CJK UNIFIED IDEOGRAPH
+	"9F 98	6CBD",	#CJK UNIFIED IDEOGRAPH
+	"9F 99	6CD7",	#CJK UNIFIED IDEOGRAPH
+	"9F 9A	6CC5",	#CJK UNIFIED IDEOGRAPH
+	"9F 9B	6CDD",	#CJK UNIFIED IDEOGRAPH
+	"9F 9C	6CAE",	#CJK UNIFIED IDEOGRAPH
+	"9F 9D	6CB1",	#CJK UNIFIED IDEOGRAPH
+	"9F 9E	6CBE",	#CJK UNIFIED IDEOGRAPH
+	"9F 9F	6CBA",	#CJK UNIFIED IDEOGRAPH
+	"9F A0	6CDB",	#CJK UNIFIED IDEOGRAPH
+	"9F A1	6CEF",	#CJK UNIFIED IDEOGRAPH
+	"9F A2	6CD9",	#CJK UNIFIED IDEOGRAPH
+	"9F A3	6CEA",	#CJK UNIFIED IDEOGRAPH
+	"9F A4	6D1F",	#CJK UNIFIED IDEOGRAPH
+	"9F A5	884D",	#CJK UNIFIED IDEOGRAPH
+	"9F A6	6D36",	#CJK UNIFIED IDEOGRAPH
+	"9F A7	6D2B",	#CJK UNIFIED IDEOGRAPH
+	"9F A8	6D3D",	#CJK UNIFIED IDEOGRAPH
+	"9F A9	6D38",	#CJK UNIFIED IDEOGRAPH
+	"9F AA	6D19",	#CJK UNIFIED IDEOGRAPH
+	"9F AB	6D35",	#CJK UNIFIED IDEOGRAPH
+	"9F AC	6D33",	#CJK UNIFIED IDEOGRAPH
+	"9F AD	6D12",	#CJK UNIFIED IDEOGRAPH
+	"9F AE	6D0C",	#CJK UNIFIED IDEOGRAPH
+	"9F AF	6D63",	#CJK UNIFIED IDEOGRAPH
+	"9F B0	6D93",	#CJK UNIFIED IDEOGRAPH
+	"9F B1	6D64",	#CJK UNIFIED IDEOGRAPH
+	"9F B2	6D5A",	#CJK UNIFIED IDEOGRAPH
+	"9F B3	6D79",	#CJK UNIFIED IDEOGRAPH
+	"9F B4	6D59",	#CJK UNIFIED IDEOGRAPH
+	"9F B5	6D8E",	#CJK UNIFIED IDEOGRAPH
+	"9F B6	6D95",	#CJK UNIFIED IDEOGRAPH
+	"9F B7	6FE4",	#CJK UNIFIED IDEOGRAPH
+	"9F B8	6D85",	#CJK UNIFIED IDEOGRAPH
+	"9F B9	6DF9",	#CJK UNIFIED IDEOGRAPH
+	"9F BA	6E15",	#CJK UNIFIED IDEOGRAPH
+	"9F BB	6E0A",	#CJK UNIFIED IDEOGRAPH
+	"9F BC	6DB5",	#CJK UNIFIED IDEOGRAPH
+	"9F BD	6DC7",	#CJK UNIFIED IDEOGRAPH
+	"9F BE	6DE6",	#CJK UNIFIED IDEOGRAPH
+	"9F BF	6DB8",	#CJK UNIFIED IDEOGRAPH
+	"9F C0	6DC6",	#CJK UNIFIED IDEOGRAPH
+	"9F C1	6DEC",	#CJK UNIFIED IDEOGRAPH
+	"9F C2	6DDE",	#CJK UNIFIED IDEOGRAPH
+	"9F C3	6DCC",	#CJK UNIFIED IDEOGRAPH
+	"9F C4	6DE8",	#CJK UNIFIED IDEOGRAPH
+	"9F C5	6DD2",	#CJK UNIFIED IDEOGRAPH
+	"9F C6	6DC5",	#CJK UNIFIED IDEOGRAPH
+	"9F C7	6DFA",	#CJK UNIFIED IDEOGRAPH
+	"9F C8	6DD9",	#CJK UNIFIED IDEOGRAPH
+	"9F C9	6DE4",	#CJK UNIFIED IDEOGRAPH
+	"9F CA	6DD5",	#CJK UNIFIED IDEOGRAPH
+	"9F CB	6DEA",	#CJK UNIFIED IDEOGRAPH
+	"9F CC	6DEE",	#CJK UNIFIED IDEOGRAPH
+	"9F CD	6E2D",	#CJK UNIFIED IDEOGRAPH
+	"9F CE	6E6E",	#CJK UNIFIED IDEOGRAPH
+	"9F CF	6E2E",	#CJK UNIFIED IDEOGRAPH
+	"9F D0	6E19",	#CJK UNIFIED IDEOGRAPH
+	"9F D1	6E72",	#CJK UNIFIED IDEOGRAPH
+	"9F D2	6E5F",	#CJK UNIFIED IDEOGRAPH
+	"9F D3	6E3E",	#CJK UNIFIED IDEOGRAPH
+	"9F D4	6E23",	#CJK UNIFIED IDEOGRAPH
+	"9F D5	6E6B",	#CJK UNIFIED IDEOGRAPH
+	"9F D6	6E2B",	#CJK UNIFIED IDEOGRAPH
+	"9F D7	6E76",	#CJK UNIFIED IDEOGRAPH
+	"9F D8	6E4D",	#CJK UNIFIED IDEOGRAPH
+	"9F D9	6E1F",	#CJK UNIFIED IDEOGRAPH
+	"9F DA	6E43",	#CJK UNIFIED IDEOGRAPH
+	"9F DB	6E3A",	#CJK UNIFIED IDEOGRAPH
+	"9F DC	6E4E",	#CJK UNIFIED IDEOGRAPH
+	"9F DD	6E24",	#CJK UNIFIED IDEOGRAPH
+	"9F DE	6EFF",	#CJK UNIFIED IDEOGRAPH
+	"9F DF	6E1D",	#CJK UNIFIED IDEOGRAPH
+	"9F E0	6E38",	#CJK UNIFIED IDEOGRAPH
+	"9F E1	6E82",	#CJK UNIFIED IDEOGRAPH
+	"9F E2	6EAA",	#CJK UNIFIED IDEOGRAPH
+	"9F E3	6E98",	#CJK UNIFIED IDEOGRAPH
+	"9F E4	6EC9",	#CJK UNIFIED IDEOGRAPH
+	"9F E5	6EB7",	#CJK UNIFIED IDEOGRAPH
+	"9F E6	6ED3",	#CJK UNIFIED IDEOGRAPH
+	"9F E7	6EBD",	#CJK UNIFIED IDEOGRAPH
+	"9F E8	6EAF",	#CJK UNIFIED IDEOGRAPH
+	"9F E9	6EC4",	#CJK UNIFIED IDEOGRAPH
+	"9F EA	6EB2",	#CJK UNIFIED IDEOGRAPH
+	"9F EB	6ED4",	#CJK UNIFIED IDEOGRAPH
+	"9F EC	6ED5",	#CJK UNIFIED IDEOGRAPH
+	"9F ED	6E8F",	#CJK UNIFIED IDEOGRAPH
+	"9F EE	6EA5",	#CJK UNIFIED IDEOGRAPH
+	"9F EF	6EC2",	#CJK UNIFIED IDEOGRAPH
+	"9F F0	6E9F",	#CJK UNIFIED IDEOGRAPH
+	"9F F1	6F41",	#CJK UNIFIED IDEOGRAPH
+	"9F F2	6F11",	#CJK UNIFIED IDEOGRAPH
+	"9F F3	704C",	#CJK UNIFIED IDEOGRAPH
+	"9F F4	6EEC",	#CJK UNIFIED IDEOGRAPH
+	"9F F5	6EF8",	#CJK UNIFIED IDEOGRAPH
+	"9F F6	6EFE",	#CJK UNIFIED IDEOGRAPH
+	"9F F7	6F3F",	#CJK UNIFIED IDEOGRAPH
+	"9F F8	6EF2",	#CJK UNIFIED IDEOGRAPH
+	"9F F9	6F31",	#CJK UNIFIED IDEOGRAPH
+	"9F FA	6EEF",	#CJK UNIFIED IDEOGRAPH
+	"9F FB	6F32",	#CJK UNIFIED IDEOGRAPH
+	"9F FC	6ECC",	#CJK UNIFIED IDEOGRAPH
+	"E0 40	6F3E",	#CJK UNIFIED IDEOGRAPH
+	"E0 41	6F13",	#CJK UNIFIED IDEOGRAPH
+	"E0 42	6EF7",	#CJK UNIFIED IDEOGRAPH
+	"E0 43	6F86",	#CJK UNIFIED IDEOGRAPH
+	"E0 44	6F7A",	#CJK UNIFIED IDEOGRAPH
+	"E0 45	6F78",	#CJK UNIFIED IDEOGRAPH
+	"E0 46	6F81",	#CJK UNIFIED IDEOGRAPH
+	"E0 47	6F80",	#CJK UNIFIED IDEOGRAPH
+	"E0 48	6F6F",	#CJK UNIFIED IDEOGRAPH
+	"E0 49	6F5B",	#CJK UNIFIED IDEOGRAPH
+	"E0 4A	6FF3",	#CJK UNIFIED IDEOGRAPH
+	"E0 4B	6F6D",	#CJK UNIFIED IDEOGRAPH
+	"E0 4C	6F82",	#CJK UNIFIED IDEOGRAPH
+	"E0 4D	6F7C",	#CJK UNIFIED IDEOGRAPH
+	"E0 4E	6F58",	#CJK UNIFIED IDEOGRAPH
+	"E0 4F	6F8E",	#CJK UNIFIED IDEOGRAPH
+	"E0 50	6F91",	#CJK UNIFIED IDEOGRAPH
+	"E0 51	6FC2",	#CJK UNIFIED IDEOGRAPH
+	"E0 52	6F66",	#CJK UNIFIED IDEOGRAPH
+	"E0 53	6FB3",	#CJK UNIFIED IDEOGRAPH
+	"E0 54	6FA3",	#CJK UNIFIED IDEOGRAPH
+	"E0 55	6FA1",	#CJK UNIFIED IDEOGRAPH
+	"E0 56	6FA4",	#CJK UNIFIED IDEOGRAPH
+	"E0 57	6FB9",	#CJK UNIFIED IDEOGRAPH
+	"E0 58	6FC6",	#CJK UNIFIED IDEOGRAPH
+	"E0 59	6FAA",	#CJK UNIFIED IDEOGRAPH
+	"E0 5A	6FDF",	#CJK UNIFIED IDEOGRAPH
+	"E0 5B	6FD5",	#CJK UNIFIED IDEOGRAPH
+	"E0 5C	6FEC",	#CJK UNIFIED IDEOGRAPH
+	"E0 5D	6FD4",	#CJK UNIFIED IDEOGRAPH
+	"E0 5E	6FD8",	#CJK UNIFIED IDEOGRAPH
+	"E0 5F	6FF1",	#CJK UNIFIED IDEOGRAPH
+	"E0 60	6FEE",	#CJK UNIFIED IDEOGRAPH
+	"E0 61	6FDB",	#CJK UNIFIED IDEOGRAPH
+	"E0 62	7009",	#CJK UNIFIED IDEOGRAPH
+	"E0 63	700B",	#CJK UNIFIED IDEOGRAPH
+	"E0 64	6FFA",	#CJK UNIFIED IDEOGRAPH
+	"E0 65	7011",	#CJK UNIFIED IDEOGRAPH
+	"E0 66	7001",	#CJK UNIFIED IDEOGRAPH
+	"E0 67	700F",	#CJK UNIFIED IDEOGRAPH
+	"E0 68	6FFE",	#CJK UNIFIED IDEOGRAPH
+	"E0 69	701B",	#CJK UNIFIED IDEOGRAPH
+	"E0 6A	701A",	#CJK UNIFIED IDEOGRAPH
+	"E0 6B	6F74",	#CJK UNIFIED IDEOGRAPH
+	"E0 6C	701D",	#CJK UNIFIED IDEOGRAPH
+	"E0 6D	7018",	#CJK UNIFIED IDEOGRAPH
+	"E0 6E	701F",	#CJK UNIFIED IDEOGRAPH
+	"E0 6F	7030",	#CJK UNIFIED IDEOGRAPH
+	"E0 70	703E",	#CJK UNIFIED IDEOGRAPH
+	"E0 71	7032",	#CJK UNIFIED IDEOGRAPH
+	"E0 72	7051",	#CJK UNIFIED IDEOGRAPH
+	"E0 73	7063",	#CJK UNIFIED IDEOGRAPH
+	"E0 74	7099",	#CJK UNIFIED IDEOGRAPH
+	"E0 75	7092",	#CJK UNIFIED IDEOGRAPH
+	"E0 76	70AF",	#CJK UNIFIED IDEOGRAPH
+	"E0 77	70F1",	#CJK UNIFIED IDEOGRAPH
+	"E0 78	70AC",	#CJK UNIFIED IDEOGRAPH
+	"E0 79	70B8",	#CJK UNIFIED IDEOGRAPH
+	"E0 7A	70B3",	#CJK UNIFIED IDEOGRAPH
+	"E0 7B	70AE",	#CJK UNIFIED IDEOGRAPH
+	"E0 7C	70DF",	#CJK UNIFIED IDEOGRAPH
+	"E0 7D	70CB",	#CJK UNIFIED IDEOGRAPH
+	"E0 7E	70DD",	#CJK UNIFIED IDEOGRAPH
+	"E0 80	70D9",	#CJK UNIFIED IDEOGRAPH
+	"E0 81	7109",	#CJK UNIFIED IDEOGRAPH
+	"E0 82	70FD",	#CJK UNIFIED IDEOGRAPH
+	"E0 83	711C",	#CJK UNIFIED IDEOGRAPH
+	"E0 84	7119",	#CJK UNIFIED IDEOGRAPH
+	"E0 85	7165",	#CJK UNIFIED IDEOGRAPH
+	"E0 86	7155",	#CJK UNIFIED IDEOGRAPH
+	"E0 87	7188",	#CJK UNIFIED IDEOGRAPH
+	"E0 88	7166",	#CJK UNIFIED IDEOGRAPH
+	"E0 89	7162",	#CJK UNIFIED IDEOGRAPH
+	"E0 8A	714C",	#CJK UNIFIED IDEOGRAPH
+	"E0 8B	7156",	#CJK UNIFIED IDEOGRAPH
+	"E0 8C	716C",	#CJK UNIFIED IDEOGRAPH
+	"E0 8D	718F",	#CJK UNIFIED IDEOGRAPH
+	"E0 8E	71FB",	#CJK UNIFIED IDEOGRAPH
+	"E0 8F	7184",	#CJK UNIFIED IDEOGRAPH
+	"E0 90	7195",	#CJK UNIFIED IDEOGRAPH
+	"E0 91	71A8",	#CJK UNIFIED IDEOGRAPH
+	"E0 92	71AC",	#CJK UNIFIED IDEOGRAPH
+	"E0 93	71D7",	#CJK UNIFIED IDEOGRAPH
+	"E0 94	71B9",	#CJK UNIFIED IDEOGRAPH
+	"E0 95	71BE",	#CJK UNIFIED IDEOGRAPH
+	"E0 96	71D2",	#CJK UNIFIED IDEOGRAPH
+	"E0 97	71C9",	#CJK UNIFIED IDEOGRAPH
+	"E0 98	71D4",	#CJK UNIFIED IDEOGRAPH
+	"E0 99	71CE",	#CJK UNIFIED IDEOGRAPH
+	"E0 9A	71E0",	#CJK UNIFIED IDEOGRAPH
+	"E0 9B	71EC",	#CJK UNIFIED IDEOGRAPH
+	"E0 9C	71E7",	#CJK UNIFIED IDEOGRAPH
+	"E0 9D	71F5",	#CJK UNIFIED IDEOGRAPH
+	"E0 9E	71FC",	#CJK UNIFIED IDEOGRAPH
+	"E0 9F	71F9",	#CJK UNIFIED IDEOGRAPH
+	"E0 A0	71FF",	#CJK UNIFIED IDEOGRAPH
+	"E0 A1	720D",	#CJK UNIFIED IDEOGRAPH
+	"E0 A2	7210",	#CJK UNIFIED IDEOGRAPH
+	"E0 A3	721B",	#CJK UNIFIED IDEOGRAPH
+	"E0 A4	7228",	#CJK UNIFIED IDEOGRAPH
+	"E0 A5	722D",	#CJK UNIFIED IDEOGRAPH
+	"E0 A6	722C",	#CJK UNIFIED IDEOGRAPH
+	"E0 A7	7230",	#CJK UNIFIED IDEOGRAPH
+	"E0 A8	7232",	#CJK UNIFIED IDEOGRAPH
+	"E0 A9	723B",	#CJK UNIFIED IDEOGRAPH
+	"E0 AA	723C",	#CJK UNIFIED IDEOGRAPH
+	"E0 AB	723F",	#CJK UNIFIED IDEOGRAPH
+	"E0 AC	7240",	#CJK UNIFIED IDEOGRAPH
+	"E0 AD	7246",	#CJK UNIFIED IDEOGRAPH
+	"E0 AE	724B",	#CJK UNIFIED IDEOGRAPH
+	"E0 AF	7258",	#CJK UNIFIED IDEOGRAPH
+	"E0 B0	7274",	#CJK UNIFIED IDEOGRAPH
+	"E0 B1	727E",	#CJK UNIFIED IDEOGRAPH
+	"E0 B2	7282",	#CJK UNIFIED IDEOGRAPH
+	"E0 B3	7281",	#CJK UNIFIED IDEOGRAPH
+	"E0 B4	7287",	#CJK UNIFIED IDEOGRAPH
+	"E0 B5	7292",	#CJK UNIFIED IDEOGRAPH
+	"E0 B6	7296",	#CJK UNIFIED IDEOGRAPH
+	"E0 B7	72A2",	#CJK UNIFIED IDEOGRAPH
+	"E0 B8	72A7",	#CJK UNIFIED IDEOGRAPH
+	"E0 B9	72B9",	#CJK UNIFIED IDEOGRAPH
+	"E0 BA	72B2",	#CJK UNIFIED IDEOGRAPH
+	"E0 BB	72C3",	#CJK UNIFIED IDEOGRAPH
+	"E0 BC	72C6",	#CJK UNIFIED IDEOGRAPH
+	"E0 BD	72C4",	#CJK UNIFIED IDEOGRAPH
+	"E0 BE	72CE",	#CJK UNIFIED IDEOGRAPH
+	"E0 BF	72D2",	#CJK UNIFIED IDEOGRAPH
+	"E0 C0	72E2",	#CJK UNIFIED IDEOGRAPH
+	"E0 C1	72E0",	#CJK UNIFIED IDEOGRAPH
+	"E0 C2	72E1",	#CJK UNIFIED IDEOGRAPH
+	"E0 C3	72F9",	#CJK UNIFIED IDEOGRAPH
+	"E0 C4	72F7",	#CJK UNIFIED IDEOGRAPH
+	"E0 C5	500F",	#CJK UNIFIED IDEOGRAPH
+	"E0 C6	7317",	#CJK UNIFIED IDEOGRAPH
+	"E0 C7	730A",	#CJK UNIFIED IDEOGRAPH
+	"E0 C8	731C",	#CJK UNIFIED IDEOGRAPH
+	"E0 C9	7316",	#CJK UNIFIED IDEOGRAPH
+	"E0 CA	731D",	#CJK UNIFIED IDEOGRAPH
+	"E0 CB	7334",	#CJK UNIFIED IDEOGRAPH
+	"E0 CC	732F",	#CJK UNIFIED IDEOGRAPH
+	"E0 CD	7329",	#CJK UNIFIED IDEOGRAPH
+	"E0 CE	7325",	#CJK UNIFIED IDEOGRAPH
+	"E0 CF	733E",	#CJK UNIFIED IDEOGRAPH
+	"E0 D0	734E",	#CJK UNIFIED IDEOGRAPH
+	"E0 D1	734F",	#CJK UNIFIED IDEOGRAPH
+	"E0 D2	9ED8",	#CJK UNIFIED IDEOGRAPH
+	"E0 D3	7357",	#CJK UNIFIED IDEOGRAPH
+	"E0 D4	736A",	#CJK UNIFIED IDEOGRAPH
+	"E0 D5	7368",	#CJK UNIFIED IDEOGRAPH
+	"E0 D6	7370",	#CJK UNIFIED IDEOGRAPH
+	"E0 D7	7378",	#CJK UNIFIED IDEOGRAPH
+	"E0 D8	7375",	#CJK UNIFIED IDEOGRAPH
+	"E0 D9	737B",	#CJK UNIFIED IDEOGRAPH
+	"E0 DA	737A",	#CJK UNIFIED IDEOGRAPH
+	"E0 DB	73C8",	#CJK UNIFIED IDEOGRAPH
+	"E0 DC	73B3",	#CJK UNIFIED IDEOGRAPH
+	"E0 DD	73CE",	#CJK UNIFIED IDEOGRAPH
+	"E0 DE	73BB",	#CJK UNIFIED IDEOGRAPH
+	"E0 DF	73C0",	#CJK UNIFIED IDEOGRAPH
+	"E0 E0	73E5",	#CJK UNIFIED IDEOGRAPH
+	"E0 E1	73EE",	#CJK UNIFIED IDEOGRAPH
+	"E0 E2	73DE",	#CJK UNIFIED IDEOGRAPH
+	"E0 E3	74A2",	#CJK UNIFIED IDEOGRAPH
+	"E0 E4	7405",	#CJK UNIFIED IDEOGRAPH
+	"E0 E5	746F",	#CJK UNIFIED IDEOGRAPH
+	"E0 E6	7425",	#CJK UNIFIED IDEOGRAPH
+	"E0 E7	73F8",	#CJK UNIFIED IDEOGRAPH
+	"E0 E8	7432",	#CJK UNIFIED IDEOGRAPH
+	"E0 E9	743A",	#CJK UNIFIED IDEOGRAPH
+	"E0 EA	7455",	#CJK UNIFIED IDEOGRAPH
+	"E0 EB	743F",	#CJK UNIFIED IDEOGRAPH
+	"E0 EC	745F",	#CJK UNIFIED IDEOGRAPH
+	"E0 ED	7459",	#CJK UNIFIED IDEOGRAPH
+	"E0 EE	7441",	#CJK UNIFIED IDEOGRAPH
+	"E0 EF	745C",	#CJK UNIFIED IDEOGRAPH
+	"E0 F0	7469",	#CJK UNIFIED IDEOGRAPH
+	"E0 F1	7470",	#CJK UNIFIED IDEOGRAPH
+	"E0 F2	7463",	#CJK UNIFIED IDEOGRAPH
+	"E0 F3	746A",	#CJK UNIFIED IDEOGRAPH
+	"E0 F4	7476",	#CJK UNIFIED IDEOGRAPH
+	"E0 F5	747E",	#CJK UNIFIED IDEOGRAPH
+	"E0 F6	748B",	#CJK UNIFIED IDEOGRAPH
+	"E0 F7	749E",	#CJK UNIFIED IDEOGRAPH
+	"E0 F8	74A7",	#CJK UNIFIED IDEOGRAPH
+	"E0 F9	74CA",	#CJK UNIFIED IDEOGRAPH
+	"E0 FA	74CF",	#CJK UNIFIED IDEOGRAPH
+	"E0 FB	74D4",	#CJK UNIFIED IDEOGRAPH
+	"E0 FC	73F1",	#CJK UNIFIED IDEOGRAPH
+	"E1 40	74E0",	#CJK UNIFIED IDEOGRAPH
+	"E1 41	74E3",	#CJK UNIFIED IDEOGRAPH
+	"E1 42	74E7",	#CJK UNIFIED IDEOGRAPH
+	"E1 43	74E9",	#CJK UNIFIED IDEOGRAPH
+	"E1 44	74EE",	#CJK UNIFIED IDEOGRAPH
+	"E1 45	74F2",	#CJK UNIFIED IDEOGRAPH
+	"E1 46	74F0",	#CJK UNIFIED IDEOGRAPH
+	"E1 47	74F1",	#CJK UNIFIED IDEOGRAPH
+	"E1 48	74F8",	#CJK UNIFIED IDEOGRAPH
+	"E1 49	74F7",	#CJK UNIFIED IDEOGRAPH
+	"E1 4A	7504",	#CJK UNIFIED IDEOGRAPH
+	"E1 4B	7503",	#CJK UNIFIED IDEOGRAPH
+	"E1 4C	7505",	#CJK UNIFIED IDEOGRAPH
+	"E1 4D	750C",	#CJK UNIFIED IDEOGRAPH
+	"E1 4E	750E",	#CJK UNIFIED IDEOGRAPH
+	"E1 4F	750D",	#CJK UNIFIED IDEOGRAPH
+	"E1 50	7515",	#CJK UNIFIED IDEOGRAPH
+	"E1 51	7513",	#CJK UNIFIED IDEOGRAPH
+	"E1 52	751E",	#CJK UNIFIED IDEOGRAPH
+	"E1 53	7526",	#CJK UNIFIED IDEOGRAPH
+	"E1 54	752C",	#CJK UNIFIED IDEOGRAPH
+	"E1 55	753C",	#CJK UNIFIED IDEOGRAPH
+	"E1 56	7544",	#CJK UNIFIED IDEOGRAPH
+	"E1 57	754D",	#CJK UNIFIED IDEOGRAPH
+	"E1 58	754A",	#CJK UNIFIED IDEOGRAPH
+	"E1 59	7549",	#CJK UNIFIED IDEOGRAPH
+	"E1 5A	755B",	#CJK UNIFIED IDEOGRAPH
+	"E1 5B	7546",	#CJK UNIFIED IDEOGRAPH
+	"E1 5C	755A",	#CJK UNIFIED IDEOGRAPH
+	"E1 5D	7569",	#CJK UNIFIED IDEOGRAPH
+	"E1 5E	7564",	#CJK UNIFIED IDEOGRAPH
+	"E1 5F	7567",	#CJK UNIFIED IDEOGRAPH
+	"E1 60	756B",	#CJK UNIFIED IDEOGRAPH
+	"E1 61	756D",	#CJK UNIFIED IDEOGRAPH
+	"E1 62	7578",	#CJK UNIFIED IDEOGRAPH
+	"E1 63	7576",	#CJK UNIFIED IDEOGRAPH
+	"E1 64	7586",	#CJK UNIFIED IDEOGRAPH
+	"E1 65	7587",	#CJK UNIFIED IDEOGRAPH
+	"E1 66	7574",	#CJK UNIFIED IDEOGRAPH
+	"E1 67	758A",	#CJK UNIFIED IDEOGRAPH
+	"E1 68	7589",	#CJK UNIFIED IDEOGRAPH
+	"E1 69	7582",	#CJK UNIFIED IDEOGRAPH
+	"E1 6A	7594",	#CJK UNIFIED IDEOGRAPH
+	"E1 6B	759A",	#CJK UNIFIED IDEOGRAPH
+	"E1 6C	759D",	#CJK UNIFIED IDEOGRAPH
+	"E1 6D	75A5",	#CJK UNIFIED IDEOGRAPH
+	"E1 6E	75A3",	#CJK UNIFIED IDEOGRAPH
+	"E1 6F	75C2",	#CJK UNIFIED IDEOGRAPH
+	"E1 70	75B3",	#CJK UNIFIED IDEOGRAPH
+	"E1 71	75C3",	#CJK UNIFIED IDEOGRAPH
+	"E1 72	75B5",	#CJK UNIFIED IDEOGRAPH
+	"E1 73	75BD",	#CJK UNIFIED IDEOGRAPH
+	"E1 74	75B8",	#CJK UNIFIED IDEOGRAPH
+	"E1 75	75BC",	#CJK UNIFIED IDEOGRAPH
+	"E1 76	75B1",	#CJK UNIFIED IDEOGRAPH
+	"E1 77	75CD",	#CJK UNIFIED IDEOGRAPH
+	"E1 78	75CA",	#CJK UNIFIED IDEOGRAPH
+	"E1 79	75D2",	#CJK UNIFIED IDEOGRAPH
+	"E1 7A	75D9",	#CJK UNIFIED IDEOGRAPH
+	"E1 7B	75E3",	#CJK UNIFIED IDEOGRAPH
+	"E1 7C	75DE",	#CJK UNIFIED IDEOGRAPH
+	"E1 7D	75FE",	#CJK UNIFIED IDEOGRAPH
+	"E1 7E	75FF",	#CJK UNIFIED IDEOGRAPH
+	"E1 80	75FC",	#CJK UNIFIED IDEOGRAPH
+	"E1 81	7601",	#CJK UNIFIED IDEOGRAPH
+	"E1 82	75F0",	#CJK UNIFIED IDEOGRAPH
+	"E1 83	75FA",	#CJK UNIFIED IDEOGRAPH
+	"E1 84	75F2",	#CJK UNIFIED IDEOGRAPH
+	"E1 85	75F3",	#CJK UNIFIED IDEOGRAPH
+	"E1 86	760B",	#CJK UNIFIED IDEOGRAPH
+	"E1 87	760D",	#CJK UNIFIED IDEOGRAPH
+	"E1 88	7609",	#CJK UNIFIED IDEOGRAPH
+	"E1 89	761F",	#CJK UNIFIED IDEOGRAPH
+	"E1 8A	7627",	#CJK UNIFIED IDEOGRAPH
+	"E1 8B	7620",	#CJK UNIFIED IDEOGRAPH
+	"E1 8C	7621",	#CJK UNIFIED IDEOGRAPH
+	"E1 8D	7622",	#CJK UNIFIED IDEOGRAPH
+	"E1 8E	7624",	#CJK UNIFIED IDEOGRAPH
+	"E1 8F	7634",	#CJK UNIFIED IDEOGRAPH
+	"E1 90	7630",	#CJK UNIFIED IDEOGRAPH
+	"E1 91	763B",	#CJK UNIFIED IDEOGRAPH
+	"E1 92	7647",	#CJK UNIFIED IDEOGRAPH
+	"E1 93	7648",	#CJK UNIFIED IDEOGRAPH
+	"E1 94	7646",	#CJK UNIFIED IDEOGRAPH
+	"E1 95	765C",	#CJK UNIFIED IDEOGRAPH
+	"E1 96	7658",	#CJK UNIFIED IDEOGRAPH
+	"E1 97	7661",	#CJK UNIFIED IDEOGRAPH
+	"E1 98	7662",	#CJK UNIFIED IDEOGRAPH
+	"E1 99	7668",	#CJK UNIFIED IDEOGRAPH
+	"E1 9A	7669",	#CJK UNIFIED IDEOGRAPH
+	"E1 9B	766A",	#CJK UNIFIED IDEOGRAPH
+	"E1 9C	7667",	#CJK UNIFIED IDEOGRAPH
+	"E1 9D	766C",	#CJK UNIFIED IDEOGRAPH
+	"E1 9E	7670",	#CJK UNIFIED IDEOGRAPH
+	"E1 9F	7672",	#CJK UNIFIED IDEOGRAPH
+	"E1 A0	7676",	#CJK UNIFIED IDEOGRAPH
+	"E1 A1	7678",	#CJK UNIFIED IDEOGRAPH
+	"E1 A2	767C",	#CJK UNIFIED IDEOGRAPH
+	"E1 A3	7680",	#CJK UNIFIED IDEOGRAPH
+	"E1 A4	7683",	#CJK UNIFIED IDEOGRAPH
+	"E1 A5	7688",	#CJK UNIFIED IDEOGRAPH
+	"E1 A6	768B",	#CJK UNIFIED IDEOGRAPH
+	"E1 A7	768E",	#CJK UNIFIED IDEOGRAPH
+	"E1 A8	7696",	#CJK UNIFIED IDEOGRAPH
+	"E1 A9	7693",	#CJK UNIFIED IDEOGRAPH
+	"E1 AA	7699",	#CJK UNIFIED IDEOGRAPH
+	"E1 AB	769A",	#CJK UNIFIED IDEOGRAPH
+	"E1 AC	76B0",	#CJK UNIFIED IDEOGRAPH
+	"E1 AD	76B4",	#CJK UNIFIED IDEOGRAPH
+	"E1 AE	76B8",	#CJK UNIFIED IDEOGRAPH
+	"E1 AF	76B9",	#CJK UNIFIED IDEOGRAPH
+	"E1 B0	76BA",	#CJK UNIFIED IDEOGRAPH
+	"E1 B1	76C2",	#CJK UNIFIED IDEOGRAPH
+	"E1 B2	76CD",	#CJK UNIFIED IDEOGRAPH
+	"E1 B3	76D6",	#CJK UNIFIED IDEOGRAPH
+	"E1 B4	76D2",	#CJK UNIFIED IDEOGRAPH
+	"E1 B5	76DE",	#CJK UNIFIED IDEOGRAPH
+	"E1 B6	76E1",	#CJK UNIFIED IDEOGRAPH
+	"E1 B7	76E5",	#CJK UNIFIED IDEOGRAPH
+	"E1 B8	76E7",	#CJK UNIFIED IDEOGRAPH
+	"E1 B9	76EA",	#CJK UNIFIED IDEOGRAPH
+	"E1 BA	862F",	#CJK UNIFIED IDEOGRAPH
+	"E1 BB	76FB",	#CJK UNIFIED IDEOGRAPH
+	"E1 BC	7708",	#CJK UNIFIED IDEOGRAPH
+	"E1 BD	7707",	#CJK UNIFIED IDEOGRAPH
+	"E1 BE	7704",	#CJK UNIFIED IDEOGRAPH
+	"E1 BF	7729",	#CJK UNIFIED IDEOGRAPH
+	"E1 C0	7724",	#CJK UNIFIED IDEOGRAPH
+	"E1 C1	771E",	#CJK UNIFIED IDEOGRAPH
+	"E1 C2	7725",	#CJK UNIFIED IDEOGRAPH
+	"E1 C3	7726",	#CJK UNIFIED IDEOGRAPH
+	"E1 C4	771B",	#CJK UNIFIED IDEOGRAPH
+	"E1 C5	7737",	#CJK UNIFIED IDEOGRAPH
+	"E1 C6	7738",	#CJK UNIFIED IDEOGRAPH
+	"E1 C7	7747",	#CJK UNIFIED IDEOGRAPH
+	"E1 C8	775A",	#CJK UNIFIED IDEOGRAPH
+	"E1 C9	7768",	#CJK UNIFIED IDEOGRAPH
+	"E1 CA	776B",	#CJK UNIFIED IDEOGRAPH
+	"E1 CB	775B",	#CJK UNIFIED IDEOGRAPH
+	"E1 CC	7765",	#CJK UNIFIED IDEOGRAPH
+	"E1 CD	777F",	#CJK UNIFIED IDEOGRAPH
+	"E1 CE	777E",	#CJK UNIFIED IDEOGRAPH
+	"E1 CF	7779",	#CJK UNIFIED IDEOGRAPH
+	"E1 D0	778E",	#CJK UNIFIED IDEOGRAPH
+	"E1 D1	778B",	#CJK UNIFIED IDEOGRAPH
+	"E1 D2	7791",	#CJK UNIFIED IDEOGRAPH
+	"E1 D3	77A0",	#CJK UNIFIED IDEOGRAPH
+	"E1 D4	779E",	#CJK UNIFIED IDEOGRAPH
+	"E1 D5	77B0",	#CJK UNIFIED IDEOGRAPH
+	"E1 D6	77B6",	#CJK UNIFIED IDEOGRAPH
+	"E1 D7	77B9",	#CJK UNIFIED IDEOGRAPH
+	"E1 D8	77BF",	#CJK UNIFIED IDEOGRAPH
+	"E1 D9	77BC",	#CJK UNIFIED IDEOGRAPH
+	"E1 DA	77BD",	#CJK UNIFIED IDEOGRAPH
+	"E1 DB	77BB",	#CJK UNIFIED IDEOGRAPH
+	"E1 DC	77C7",	#CJK UNIFIED IDEOGRAPH
+	"E1 DD	77CD",	#CJK UNIFIED IDEOGRAPH
+	"E1 DE	77D7",	#CJK UNIFIED IDEOGRAPH
+	"E1 DF	77DA",	#CJK UNIFIED IDEOGRAPH
+	"E1 E0	77DC",	#CJK UNIFIED IDEOGRAPH
+	"E1 E1	77E3",	#CJK UNIFIED IDEOGRAPH
+	"E1 E2	77EE",	#CJK UNIFIED IDEOGRAPH
+	"E1 E3	77FC",	#CJK UNIFIED IDEOGRAPH
+	"E1 E4	780C",	#CJK UNIFIED IDEOGRAPH
+	"E1 E5	7812",	#CJK UNIFIED IDEOGRAPH
+	"E1 E6	7926",	#CJK UNIFIED IDEOGRAPH
+	"E1 E7	7820",	#CJK UNIFIED IDEOGRAPH
+	"E1 E8	792A",	#CJK UNIFIED IDEOGRAPH
+	"E1 E9	7845",	#CJK UNIFIED IDEOGRAPH
+	"E1 EA	788E",	#CJK UNIFIED IDEOGRAPH
+	"E1 EB	7874",	#CJK UNIFIED IDEOGRAPH
+	"E1 EC	7886",	#CJK UNIFIED IDEOGRAPH
+	"E1 ED	787C",	#CJK UNIFIED IDEOGRAPH
+	"E1 EE	789A",	#CJK UNIFIED IDEOGRAPH
+	"E1 EF	788C",	#CJK UNIFIED IDEOGRAPH
+	"E1 F0	78A3",	#CJK UNIFIED IDEOGRAPH
+	"E1 F1	78B5",	#CJK UNIFIED IDEOGRAPH
+	"E1 F2	78AA",	#CJK UNIFIED IDEOGRAPH
+	"E1 F3	78AF",	#CJK UNIFIED IDEOGRAPH
+	"E1 F4	78D1",	#CJK UNIFIED IDEOGRAPH
+	"E1 F5	78C6",	#CJK UNIFIED IDEOGRAPH
+	"E1 F6	78CB",	#CJK UNIFIED IDEOGRAPH
+	"E1 F7	78D4",	#CJK UNIFIED IDEOGRAPH
+	"E1 F8	78BE",	#CJK UNIFIED IDEOGRAPH
+	"E1 F9	78BC",	#CJK UNIFIED IDEOGRAPH
+	"E1 FA	78C5",	#CJK UNIFIED IDEOGRAPH
+	"E1 FB	78CA",	#CJK UNIFIED IDEOGRAPH
+	"E1 FC	78EC",	#CJK UNIFIED IDEOGRAPH
+	"E2 40	78E7",	#CJK UNIFIED IDEOGRAPH
+	"E2 41	78DA",	#CJK UNIFIED IDEOGRAPH
+	"E2 42	78FD",	#CJK UNIFIED IDEOGRAPH
+	"E2 43	78F4",	#CJK UNIFIED IDEOGRAPH
+	"E2 44	7907",	#CJK UNIFIED IDEOGRAPH
+	"E2 45	7912",	#CJK UNIFIED IDEOGRAPH
+	"E2 46	7911",	#CJK UNIFIED IDEOGRAPH
+	"E2 47	7919",	#CJK UNIFIED IDEOGRAPH
+	"E2 48	792C",	#CJK UNIFIED IDEOGRAPH
+	"E2 49	792B",	#CJK UNIFIED IDEOGRAPH
+	"E2 4A	7940",	#CJK UNIFIED IDEOGRAPH
+	"E2 4B	7960",	#CJK UNIFIED IDEOGRAPH
+	"E2 4C	7957",	#CJK UNIFIED IDEOGRAPH
+	"E2 4D	795F",	#CJK UNIFIED IDEOGRAPH
+	"E2 4E	795A",	#CJK UNIFIED IDEOGRAPH
+	"E2 4F	7955",	#CJK UNIFIED IDEOGRAPH
+	"E2 50	7953",	#CJK UNIFIED IDEOGRAPH
+	"E2 51	797A",	#CJK UNIFIED IDEOGRAPH
+	"E2 52	797F",	#CJK UNIFIED IDEOGRAPH
+	"E2 53	798A",	#CJK UNIFIED IDEOGRAPH
+	"E2 54	799D",	#CJK UNIFIED IDEOGRAPH
+	"E2 55	79A7",	#CJK UNIFIED IDEOGRAPH
+	"E2 56	9F4B",	#CJK UNIFIED IDEOGRAPH
+	"E2 57	79AA",	#CJK UNIFIED IDEOGRAPH
+	"E2 58	79AE",	#CJK UNIFIED IDEOGRAPH
+	"E2 59	79B3",	#CJK UNIFIED IDEOGRAPH
+	"E2 5A	79B9",	#CJK UNIFIED IDEOGRAPH
+	"E2 5B	79BA",	#CJK UNIFIED IDEOGRAPH
+	"E2 5C	79C9",	#CJK UNIFIED IDEOGRAPH
+	"E2 5D	79D5",	#CJK UNIFIED IDEOGRAPH
+	"E2 5E	79E7",	#CJK UNIFIED IDEOGRAPH
+	"E2 5F	79EC",	#CJK UNIFIED IDEOGRAPH
+	"E2 60	79E1",	#CJK UNIFIED IDEOGRAPH
+	"E2 61	79E3",	#CJK UNIFIED IDEOGRAPH
+	"E2 62	7A08",	#CJK UNIFIED IDEOGRAPH
+	"E2 63	7A0D",	#CJK UNIFIED IDEOGRAPH
+	"E2 64	7A18",	#CJK UNIFIED IDEOGRAPH
+	"E2 65	7A19",	#CJK UNIFIED IDEOGRAPH
+	"E2 66	7A20",	#CJK UNIFIED IDEOGRAPH
+	"E2 67	7A1F",	#CJK UNIFIED IDEOGRAPH
+	"E2 68	7980",	#CJK UNIFIED IDEOGRAPH
+	"E2 69	7A31",	#CJK UNIFIED IDEOGRAPH
+	"E2 6A	7A3B",	#CJK UNIFIED IDEOGRAPH
+	"E2 6B	7A3E",	#CJK UNIFIED IDEOGRAPH
+	"E2 6C	7A37",	#CJK UNIFIED IDEOGRAPH
+	"E2 6D	7A43",	#CJK UNIFIED IDEOGRAPH
+	"E2 6E	7A57",	#CJK UNIFIED IDEOGRAPH
+	"E2 6F	7A49",	#CJK UNIFIED IDEOGRAPH
+	"E2 70	7A61",	#CJK UNIFIED IDEOGRAPH
+	"E2 71	7A62",	#CJK UNIFIED IDEOGRAPH
+	"E2 72	7A69",	#CJK UNIFIED IDEOGRAPH
+	"E2 73	9F9D",	#CJK UNIFIED IDEOGRAPH
+	"E2 74	7A70",	#CJK UNIFIED IDEOGRAPH
+	"E2 75	7A79",	#CJK UNIFIED IDEOGRAPH
+	"E2 76	7A7D",	#CJK UNIFIED IDEOGRAPH
+	"E2 77	7A88",	#CJK UNIFIED IDEOGRAPH
+	"E2 78	7A97",	#CJK UNIFIED IDEOGRAPH
+	"E2 79	7A95",	#CJK UNIFIED IDEOGRAPH
+	"E2 7A	7A98",	#CJK UNIFIED IDEOGRAPH
+	"E2 7B	7A96",	#CJK UNIFIED IDEOGRAPH
+	"E2 7C	7AA9",	#CJK UNIFIED IDEOGRAPH
+	"E2 7D	7AC8",	#CJK UNIFIED IDEOGRAPH
+	"E2 7E	7AB0",	#CJK UNIFIED IDEOGRAPH
+	"E2 80	7AB6",	#CJK UNIFIED IDEOGRAPH
+	"E2 81	7AC5",	#CJK UNIFIED IDEOGRAPH
+	"E2 82	7AC4",	#CJK UNIFIED IDEOGRAPH
+	"E2 83	7ABF",	#CJK UNIFIED IDEOGRAPH
+	"E2 84	9083",	#CJK UNIFIED IDEOGRAPH
+	"E2 85	7AC7",	#CJK UNIFIED IDEOGRAPH
+	"E2 86	7ACA",	#CJK UNIFIED IDEOGRAPH
+	"E2 87	7ACD",	#CJK UNIFIED IDEOGRAPH
+	"E2 88	7ACF",	#CJK UNIFIED IDEOGRAPH
+	"E2 89	7AD5",	#CJK UNIFIED IDEOGRAPH
+	"E2 8A	7AD3",	#CJK UNIFIED IDEOGRAPH
+	"E2 8B	7AD9",	#CJK UNIFIED IDEOGRAPH
+	"E2 8C	7ADA",	#CJK UNIFIED IDEOGRAPH
+	"E2 8D	7ADD",	#CJK UNIFIED IDEOGRAPH
+	"E2 8E	7AE1",	#CJK UNIFIED IDEOGRAPH
+	"E2 8F	7AE2",	#CJK UNIFIED IDEOGRAPH
+	"E2 90	7AE6",	#CJK UNIFIED IDEOGRAPH
+	"E2 91	7AED",	#CJK UNIFIED IDEOGRAPH
+	"E2 92	7AF0",	#CJK UNIFIED IDEOGRAPH
+	"E2 93	7B02",	#CJK UNIFIED IDEOGRAPH
+	"E2 94	7B0F",	#CJK UNIFIED IDEOGRAPH
+	"E2 95	7B0A",	#CJK UNIFIED IDEOGRAPH
+	"E2 96	7B06",	#CJK UNIFIED IDEOGRAPH
+	"E2 97	7B33",	#CJK UNIFIED IDEOGRAPH
+	"E2 98	7B18",	#CJK UNIFIED IDEOGRAPH
+	"E2 99	7B19",	#CJK UNIFIED IDEOGRAPH
+	"E2 9A	7B1E",	#CJK UNIFIED IDEOGRAPH
+	"E2 9B	7B35",	#CJK UNIFIED IDEOGRAPH
+	"E2 9C	7B28",	#CJK UNIFIED IDEOGRAPH
+	"E2 9D	7B36",	#CJK UNIFIED IDEOGRAPH
+	"E2 9E	7B50",	#CJK UNIFIED IDEOGRAPH
+	"E2 9F	7B7A",	#CJK UNIFIED IDEOGRAPH
+	"E2 A0	7B04",	#CJK UNIFIED IDEOGRAPH
+	"E2 A1	7B4D",	#CJK UNIFIED IDEOGRAPH
+	"E2 A2	7B0B",	#CJK UNIFIED IDEOGRAPH
+	"E2 A3	7B4C",	#CJK UNIFIED IDEOGRAPH
+	"E2 A4	7B45",	#CJK UNIFIED IDEOGRAPH
+	"E2 A5	7B75",	#CJK UNIFIED IDEOGRAPH
+	"E2 A6	7B65",	#CJK UNIFIED IDEOGRAPH
+	"E2 A7	7B74",	#CJK UNIFIED IDEOGRAPH
+	"E2 A8	7B67",	#CJK UNIFIED IDEOGRAPH
+	"E2 A9	7B70",	#CJK UNIFIED IDEOGRAPH
+	"E2 AA	7B71",	#CJK UNIFIED IDEOGRAPH
+	"E2 AB	7B6C",	#CJK UNIFIED IDEOGRAPH
+	"E2 AC	7B6E",	#CJK UNIFIED IDEOGRAPH
+	"E2 AD	7B9D",	#CJK UNIFIED IDEOGRAPH
+	"E2 AE	7B98",	#CJK UNIFIED IDEOGRAPH
+	"E2 AF	7B9F",	#CJK UNIFIED IDEOGRAPH
+	"E2 B0	7B8D",	#CJK UNIFIED IDEOGRAPH
+	"E2 B1	7B9C",	#CJK UNIFIED IDEOGRAPH
+	"E2 B2	7B9A",	#CJK UNIFIED IDEOGRAPH
+	"E2 B3	7B8B",	#CJK UNIFIED IDEOGRAPH
+	"E2 B4	7B92",	#CJK UNIFIED IDEOGRAPH
+	"E2 B5	7B8F",	#CJK UNIFIED IDEOGRAPH
+	"E2 B6	7B5D",	#CJK UNIFIED IDEOGRAPH
+	"E2 B7	7B99",	#CJK UNIFIED IDEOGRAPH
+	"E2 B8	7BCB",	#CJK UNIFIED IDEOGRAPH
+	"E2 B9	7BC1",	#CJK UNIFIED IDEOGRAPH
+	"E2 BA	7BCC",	#CJK UNIFIED IDEOGRAPH
+	"E2 BB	7BCF",	#CJK UNIFIED IDEOGRAPH
+	"E2 BC	7BB4",	#CJK UNIFIED IDEOGRAPH
+	"E2 BD	7BC6",	#CJK UNIFIED IDEOGRAPH
+	"E2 BE	7BDD",	#CJK UNIFIED IDEOGRAPH
+	"E2 BF	7BE9",	#CJK UNIFIED IDEOGRAPH
+	"E2 C0	7C11",	#CJK UNIFIED IDEOGRAPH
+	"E2 C1	7C14",	#CJK UNIFIED IDEOGRAPH
+	"E2 C2	7BE6",	#CJK UNIFIED IDEOGRAPH
+	"E2 C3	7BE5",	#CJK UNIFIED IDEOGRAPH
+	"E2 C4	7C60",	#CJK UNIFIED IDEOGRAPH
+	"E2 C5	7C00",	#CJK UNIFIED IDEOGRAPH
+	"E2 C6	7C07",	#CJK UNIFIED IDEOGRAPH
+	"E2 C7	7C13",	#CJK UNIFIED IDEOGRAPH
+	"E2 C8	7BF3",	#CJK UNIFIED IDEOGRAPH
+	"E2 C9	7BF7",	#CJK UNIFIED IDEOGRAPH
+	"E2 CA	7C17",	#CJK UNIFIED IDEOGRAPH
+	"E2 CB	7C0D",	#CJK UNIFIED IDEOGRAPH
+	"E2 CC	7BF6",	#CJK UNIFIED IDEOGRAPH
+	"E2 CD	7C23",	#CJK UNIFIED IDEOGRAPH
+	"E2 CE	7C27",	#CJK UNIFIED IDEOGRAPH
+	"E2 CF	7C2A",	#CJK UNIFIED IDEOGRAPH
+	"E2 D0	7C1F",	#CJK UNIFIED IDEOGRAPH
+	"E2 D1	7C37",	#CJK UNIFIED IDEOGRAPH
+	"E2 D2	7C2B",	#CJK UNIFIED IDEOGRAPH
+	"E2 D3	7C3D",	#CJK UNIFIED IDEOGRAPH
+	"E2 D4	7C4C",	#CJK UNIFIED IDEOGRAPH
+	"E2 D5	7C43",	#CJK UNIFIED IDEOGRAPH
+	"E2 D6	7C54",	#CJK UNIFIED IDEOGRAPH
+	"E2 D7	7C4F",	#CJK UNIFIED IDEOGRAPH
+	"E2 D8	7C40",	#CJK UNIFIED IDEOGRAPH
+	"E2 D9	7C50",	#CJK UNIFIED IDEOGRAPH
+	"E2 DA	7C58",	#CJK UNIFIED IDEOGRAPH
+	"E2 DB	7C5F",	#CJK UNIFIED IDEOGRAPH
+	"E2 DC	7C64",	#CJK UNIFIED IDEOGRAPH
+	"E2 DD	7C56",	#CJK UNIFIED IDEOGRAPH
+	"E2 DE	7C65",	#CJK UNIFIED IDEOGRAPH
+	"E2 DF	7C6C",	#CJK UNIFIED IDEOGRAPH
+	"E2 E0	7C75",	#CJK UNIFIED IDEOGRAPH
+	"E2 E1	7C83",	#CJK UNIFIED IDEOGRAPH
+	"E2 E2	7C90",	#CJK UNIFIED IDEOGRAPH
+	"E2 E3	7CA4",	#CJK UNIFIED IDEOGRAPH
+	"E2 E4	7CAD",	#CJK UNIFIED IDEOGRAPH
+	"E2 E5	7CA2",	#CJK UNIFIED IDEOGRAPH
+	"E2 E6	7CAB",	#CJK UNIFIED IDEOGRAPH
+	"E2 E7	7CA1",	#CJK UNIFIED IDEOGRAPH
+	"E2 E8	7CA8",	#CJK UNIFIED IDEOGRAPH
+	"E2 E9	7CB3",	#CJK UNIFIED IDEOGRAPH
+	"E2 EA	7CB2",	#CJK UNIFIED IDEOGRAPH
+	"E2 EB	7CB1",	#CJK UNIFIED IDEOGRAPH
+	"E2 EC	7CAE",	#CJK UNIFIED IDEOGRAPH
+	"E2 ED	7CB9",	#CJK UNIFIED IDEOGRAPH
+	"E2 EE	7CBD",	#CJK UNIFIED IDEOGRAPH
+	"E2 EF	7CC0",	#CJK UNIFIED IDEOGRAPH
+	"E2 F0	7CC5",	#CJK UNIFIED IDEOGRAPH
+	"E2 F1	7CC2",	#CJK UNIFIED IDEOGRAPH
+	"E2 F2	7CD8",	#CJK UNIFIED IDEOGRAPH
+	"E2 F3	7CD2",	#CJK UNIFIED IDEOGRAPH
+	"E2 F4	7CDC",	#CJK UNIFIED IDEOGRAPH
+	"E2 F5	7CE2",	#CJK UNIFIED IDEOGRAPH
+	"E2 F6	9B3B",	#CJK UNIFIED IDEOGRAPH
+	"E2 F7	7CEF",	#CJK UNIFIED IDEOGRAPH
+	"E2 F8	7CF2",	#CJK UNIFIED IDEOGRAPH
+	"E2 F9	7CF4",	#CJK UNIFIED IDEOGRAPH
+	"E2 FA	7CF6",	#CJK UNIFIED IDEOGRAPH
+	"E2 FB	7CFA",	#CJK UNIFIED IDEOGRAPH
+	"E2 FC	7D06",	#CJK UNIFIED IDEOGRAPH
+	"E3 40	7D02",	#CJK UNIFIED IDEOGRAPH
+	"E3 41	7D1C",	#CJK UNIFIED IDEOGRAPH
+	"E3 42	7D15",	#CJK UNIFIED IDEOGRAPH
+	"E3 43	7D0A",	#CJK UNIFIED IDEOGRAPH
+	"E3 44	7D45",	#CJK UNIFIED IDEOGRAPH
+	"E3 45	7D4B",	#CJK UNIFIED IDEOGRAPH
+	"E3 46	7D2E",	#CJK UNIFIED IDEOGRAPH
+	"E3 47	7D32",	#CJK UNIFIED IDEOGRAPH
+	"E3 48	7D3F",	#CJK UNIFIED IDEOGRAPH
+	"E3 49	7D35",	#CJK UNIFIED IDEOGRAPH
+	"E3 4A	7D46",	#CJK UNIFIED IDEOGRAPH
+	"E3 4B	7D73",	#CJK UNIFIED IDEOGRAPH
+	"E3 4C	7D56",	#CJK UNIFIED IDEOGRAPH
+	"E3 4D	7D4E",	#CJK UNIFIED IDEOGRAPH
+	"E3 4E	7D72",	#CJK UNIFIED IDEOGRAPH
+	"E3 4F	7D68",	#CJK UNIFIED IDEOGRAPH
+	"E3 50	7D6E",	#CJK UNIFIED IDEOGRAPH
+	"E3 51	7D4F",	#CJK UNIFIED IDEOGRAPH
+	"E3 52	7D63",	#CJK UNIFIED IDEOGRAPH
+	"E3 53	7D93",	#CJK UNIFIED IDEOGRAPH
+	"E3 54	7D89",	#CJK UNIFIED IDEOGRAPH
+	"E3 55	7D5B",	#CJK UNIFIED IDEOGRAPH
+	"E3 56	7D8F",	#CJK UNIFIED IDEOGRAPH
+	"E3 57	7D7D",	#CJK UNIFIED IDEOGRAPH
+	"E3 58	7D9B",	#CJK UNIFIED IDEOGRAPH
+	"E3 59	7DBA",	#CJK UNIFIED IDEOGRAPH
+	"E3 5A	7DAE",	#CJK UNIFIED IDEOGRAPH
+	"E3 5B	7DA3",	#CJK UNIFIED IDEOGRAPH
+	"E3 5C	7DB5",	#CJK UNIFIED IDEOGRAPH
+	"E3 5D	7DC7",	#CJK UNIFIED IDEOGRAPH
+	"E3 5E	7DBD",	#CJK UNIFIED IDEOGRAPH
+	"E3 5F	7DAB",	#CJK UNIFIED IDEOGRAPH
+	"E3 60	7E3D",	#CJK UNIFIED IDEOGRAPH
+	"E3 61	7DA2",	#CJK UNIFIED IDEOGRAPH
+	"E3 62	7DAF",	#CJK UNIFIED IDEOGRAPH
+	"E3 63	7DDC",	#CJK UNIFIED IDEOGRAPH
+	"E3 64	7DB8",	#CJK UNIFIED IDEOGRAPH
+	"E3 65	7D9F",	#CJK UNIFIED IDEOGRAPH
+	"E3 66	7DB0",	#CJK UNIFIED IDEOGRAPH
+	"E3 67	7DD8",	#CJK UNIFIED IDEOGRAPH
+	"E3 68	7DDD",	#CJK UNIFIED IDEOGRAPH
+	"E3 69	7DE4",	#CJK UNIFIED IDEOGRAPH
+	"E3 6A	7DDE",	#CJK UNIFIED IDEOGRAPH
+	"E3 6B	7DFB",	#CJK UNIFIED IDEOGRAPH
+	"E3 6C	7DF2",	#CJK UNIFIED IDEOGRAPH
+	"E3 6D	7DE1",	#CJK UNIFIED IDEOGRAPH
+	"E3 6E	7E05",	#CJK UNIFIED IDEOGRAPH
+	"E3 6F	7E0A",	#CJK UNIFIED IDEOGRAPH
+	"E3 70	7E23",	#CJK UNIFIED IDEOGRAPH
+	"E3 71	7E21",	#CJK UNIFIED IDEOGRAPH
+	"E3 72	7E12",	#CJK UNIFIED IDEOGRAPH
+	"E3 73	7E31",	#CJK UNIFIED IDEOGRAPH
+	"E3 74	7E1F",	#CJK UNIFIED IDEOGRAPH
+	"E3 75	7E09",	#CJK UNIFIED IDEOGRAPH
+	"E3 76	7E0B",	#CJK UNIFIED IDEOGRAPH
+	"E3 77	7E22",	#CJK UNIFIED IDEOGRAPH
+	"E3 78	7E46",	#CJK UNIFIED IDEOGRAPH
+	"E3 79	7E66",	#CJK UNIFIED IDEOGRAPH
+	"E3 7A	7E3B",	#CJK UNIFIED IDEOGRAPH
+	"E3 7B	7E35",	#CJK UNIFIED IDEOGRAPH
+	"E3 7C	7E39",	#CJK UNIFIED IDEOGRAPH
+	"E3 7D	7E43",	#CJK UNIFIED IDEOGRAPH
+	"E3 7E	7E37",	#CJK UNIFIED IDEOGRAPH
+	"E3 80	7E32",	#CJK UNIFIED IDEOGRAPH
+	"E3 81	7E3A",	#CJK UNIFIED IDEOGRAPH
+	"E3 82	7E67",	#CJK UNIFIED IDEOGRAPH
+	"E3 83	7E5D",	#CJK UNIFIED IDEOGRAPH
+	"E3 84	7E56",	#CJK UNIFIED IDEOGRAPH
+	"E3 85	7E5E",	#CJK UNIFIED IDEOGRAPH
+	"E3 86	7E59",	#CJK UNIFIED IDEOGRAPH
+	"E3 87	7E5A",	#CJK UNIFIED IDEOGRAPH
+	"E3 88	7E79",	#CJK UNIFIED IDEOGRAPH
+	"E3 89	7E6A",	#CJK UNIFIED IDEOGRAPH
+	"E3 8A	7E69",	#CJK UNIFIED IDEOGRAPH
+	"E3 8B	7E7C",	#CJK UNIFIED IDEOGRAPH
+	"E3 8C	7E7B",	#CJK UNIFIED IDEOGRAPH
+	"E3 8D	7E83",	#CJK UNIFIED IDEOGRAPH
+	"E3 8E	7DD5",	#CJK UNIFIED IDEOGRAPH
+	"E3 8F	7E7D",	#CJK UNIFIED IDEOGRAPH
+	"E3 90	8FAE",	#CJK UNIFIED IDEOGRAPH
+	"E3 91	7E7F",	#CJK UNIFIED IDEOGRAPH
+	"E3 92	7E88",	#CJK UNIFIED IDEOGRAPH
+	"E3 93	7E89",	#CJK UNIFIED IDEOGRAPH
+	"E3 94	7E8C",	#CJK UNIFIED IDEOGRAPH
+	"E3 95	7E92",	#CJK UNIFIED IDEOGRAPH
+	"E3 96	7E90",	#CJK UNIFIED IDEOGRAPH
+	"E3 97	7E93",	#CJK UNIFIED IDEOGRAPH
+	"E3 98	7E94",	#CJK UNIFIED IDEOGRAPH
+	"E3 99	7E96",	#CJK UNIFIED IDEOGRAPH
+	"E3 9A	7E8E",	#CJK UNIFIED IDEOGRAPH
+	"E3 9B	7E9B",	#CJK UNIFIED IDEOGRAPH
+	"E3 9C	7E9C",	#CJK UNIFIED IDEOGRAPH
+	"E3 9D	7F38",	#CJK UNIFIED IDEOGRAPH
+	"E3 9E	7F3A",	#CJK UNIFIED IDEOGRAPH
+	"E3 9F	7F45",	#CJK UNIFIED IDEOGRAPH
+	"E3 A0	7F4C",	#CJK UNIFIED IDEOGRAPH
+	"E3 A1	7F4D",	#CJK UNIFIED IDEOGRAPH
+	"E3 A2	7F4E",	#CJK UNIFIED IDEOGRAPH
+	"E3 A3	7F50",	#CJK UNIFIED IDEOGRAPH
+	"E3 A4	7F51",	#CJK UNIFIED IDEOGRAPH
+	"E3 A5	7F55",	#CJK UNIFIED IDEOGRAPH
+	"E3 A6	7F54",	#CJK UNIFIED IDEOGRAPH
+	"E3 A7	7F58",	#CJK UNIFIED IDEOGRAPH
+	"E3 A8	7F5F",	#CJK UNIFIED IDEOGRAPH
+	"E3 A9	7F60",	#CJK UNIFIED IDEOGRAPH
+	"E3 AA	7F68",	#CJK UNIFIED IDEOGRAPH
+	"E3 AB	7F69",	#CJK UNIFIED IDEOGRAPH
+	"E3 AC	7F67",	#CJK UNIFIED IDEOGRAPH
+	"E3 AD	7F78",	#CJK UNIFIED IDEOGRAPH
+	"E3 AE	7F82",	#CJK UNIFIED IDEOGRAPH
+	"E3 AF	7F86",	#CJK UNIFIED IDEOGRAPH
+	"E3 B0	7F83",	#CJK UNIFIED IDEOGRAPH
+	"E3 B1	7F88",	#CJK UNIFIED IDEOGRAPH
+	"E3 B2	7F87",	#CJK UNIFIED IDEOGRAPH
+	"E3 B3	7F8C",	#CJK UNIFIED IDEOGRAPH
+	"E3 B4	7F94",	#CJK UNIFIED IDEOGRAPH
+	"E3 B5	7F9E",	#CJK UNIFIED IDEOGRAPH
+	"E3 B6	7F9D",	#CJK UNIFIED IDEOGRAPH
+	"E3 B7	7F9A",	#CJK UNIFIED IDEOGRAPH
+	"E3 B8	7FA3",	#CJK UNIFIED IDEOGRAPH
+	"E3 B9	7FAF",	#CJK UNIFIED IDEOGRAPH
+	"E3 BA	7FB2",	#CJK UNIFIED IDEOGRAPH
+	"E3 BB	7FB9",	#CJK UNIFIED IDEOGRAPH
+	"E3 BC	7FAE",	#CJK UNIFIED IDEOGRAPH
+	"E3 BD	7FB6",	#CJK UNIFIED IDEOGRAPH
+	"E3 BE	7FB8",	#CJK UNIFIED IDEOGRAPH
+	"E3 BF	8B71",	#CJK UNIFIED IDEOGRAPH
+	"E3 C0	7FC5",	#CJK UNIFIED IDEOGRAPH
+	"E3 C1	7FC6",	#CJK UNIFIED IDEOGRAPH
+	"E3 C2	7FCA",	#CJK UNIFIED IDEOGRAPH
+	"E3 C3	7FD5",	#CJK UNIFIED IDEOGRAPH
+	"E3 C4	7FD4",	#CJK UNIFIED IDEOGRAPH
+	"E3 C5	7FE1",	#CJK UNIFIED IDEOGRAPH
+	"E3 C6	7FE6",	#CJK UNIFIED IDEOGRAPH
+	"E3 C7	7FE9",	#CJK UNIFIED IDEOGRAPH
+	"E3 C8	7FF3",	#CJK UNIFIED IDEOGRAPH
+	"E3 C9	7FF9",	#CJK UNIFIED IDEOGRAPH
+	"E3 CA	98DC",	#CJK UNIFIED IDEOGRAPH
+	"E3 CB	8006",	#CJK UNIFIED IDEOGRAPH
+	"E3 CC	8004",	#CJK UNIFIED IDEOGRAPH
+	"E3 CD	800B",	#CJK UNIFIED IDEOGRAPH
+	"E3 CE	8012",	#CJK UNIFIED IDEOGRAPH
+	"E3 CF	8018",	#CJK UNIFIED IDEOGRAPH
+	"E3 D0	8019",	#CJK UNIFIED IDEOGRAPH
+	"E3 D1	801C",	#CJK UNIFIED IDEOGRAPH
+	"E3 D2	8021",	#CJK UNIFIED IDEOGRAPH
+	"E3 D3	8028",	#CJK UNIFIED IDEOGRAPH
+	"E3 D4	803F",	#CJK UNIFIED IDEOGRAPH
+	"E3 D5	803B",	#CJK UNIFIED IDEOGRAPH
+	"E3 D6	804A",	#CJK UNIFIED IDEOGRAPH
+	"E3 D7	8046",	#CJK UNIFIED IDEOGRAPH
+	"E3 D8	8052",	#CJK UNIFIED IDEOGRAPH
+	"E3 D9	8058",	#CJK UNIFIED IDEOGRAPH
+	"E3 DA	805A",	#CJK UNIFIED IDEOGRAPH
+	"E3 DB	805F",	#CJK UNIFIED IDEOGRAPH
+	"E3 DC	8062",	#CJK UNIFIED IDEOGRAPH
+	"E3 DD	8068",	#CJK UNIFIED IDEOGRAPH
+	"E3 DE	8073",	#CJK UNIFIED IDEOGRAPH
+	"E3 DF	8072",	#CJK UNIFIED IDEOGRAPH
+	"E3 E0	8070",	#CJK UNIFIED IDEOGRAPH
+	"E3 E1	8076",	#CJK UNIFIED IDEOGRAPH
+	"E3 E2	8079",	#CJK UNIFIED IDEOGRAPH
+	"E3 E3	807D",	#CJK UNIFIED IDEOGRAPH
+	"E3 E4	807F",	#CJK UNIFIED IDEOGRAPH
+	"E3 E5	8084",	#CJK UNIFIED IDEOGRAPH
+	"E3 E6	8086",	#CJK UNIFIED IDEOGRAPH
+	"E3 E7	8085",	#CJK UNIFIED IDEOGRAPH
+	"E3 E8	809B",	#CJK UNIFIED IDEOGRAPH
+	"E3 E9	8093",	#CJK UNIFIED IDEOGRAPH
+	"E3 EA	809A",	#CJK UNIFIED IDEOGRAPH
+	"E3 EB	80AD",	#CJK UNIFIED IDEOGRAPH
+	"E3 EC	5190",	#CJK UNIFIED IDEOGRAPH
+	"E3 ED	80AC",	#CJK UNIFIED IDEOGRAPH
+	"E3 EE	80DB",	#CJK UNIFIED IDEOGRAPH
+	"E3 EF	80E5",	#CJK UNIFIED IDEOGRAPH
+	"E3 F0	80D9",	#CJK UNIFIED IDEOGRAPH
+	"E3 F1	80DD",	#CJK UNIFIED IDEOGRAPH
+	"E3 F2	80C4",	#CJK UNIFIED IDEOGRAPH
+	"E3 F3	80DA",	#CJK UNIFIED IDEOGRAPH
+	"E3 F4	80D6",	#CJK UNIFIED IDEOGRAPH
+	"E3 F5	8109",	#CJK UNIFIED IDEOGRAPH
+	"E3 F6	80EF",	#CJK UNIFIED IDEOGRAPH
+	"E3 F7	80F1",	#CJK UNIFIED IDEOGRAPH
+	"E3 F8	811B",	#CJK UNIFIED IDEOGRAPH
+	"E3 F9	8129",	#CJK UNIFIED IDEOGRAPH
+	"E3 FA	8123",	#CJK UNIFIED IDEOGRAPH
+	"E3 FB	812F",	#CJK UNIFIED IDEOGRAPH
+	"E3 FC	814B",	#CJK UNIFIED IDEOGRAPH
+	"E4 40	968B",	#CJK UNIFIED IDEOGRAPH
+	"E4 41	8146",	#CJK UNIFIED IDEOGRAPH
+	"E4 42	813E",	#CJK UNIFIED IDEOGRAPH
+	"E4 43	8153",	#CJK UNIFIED IDEOGRAPH
+	"E4 44	8151",	#CJK UNIFIED IDEOGRAPH
+	"E4 45	80FC",	#CJK UNIFIED IDEOGRAPH
+	"E4 46	8171",	#CJK UNIFIED IDEOGRAPH
+	"E4 47	816E",	#CJK UNIFIED IDEOGRAPH
+	"E4 48	8165",	#CJK UNIFIED IDEOGRAPH
+	"E4 49	8166",	#CJK UNIFIED IDEOGRAPH
+	"E4 4A	8174",	#CJK UNIFIED IDEOGRAPH
+	"E4 4B	8183",	#CJK UNIFIED IDEOGRAPH
+	"E4 4C	8188",	#CJK UNIFIED IDEOGRAPH
+	"E4 4D	818A",	#CJK UNIFIED IDEOGRAPH
+	"E4 4E	8180",	#CJK UNIFIED IDEOGRAPH
+	"E4 4F	8182",	#CJK UNIFIED IDEOGRAPH
+	"E4 50	81A0",	#CJK UNIFIED IDEOGRAPH
+	"E4 51	8195",	#CJK UNIFIED IDEOGRAPH
+	"E4 52	81A4",	#CJK UNIFIED IDEOGRAPH
+	"E4 53	81A3",	#CJK UNIFIED IDEOGRAPH
+	"E4 54	815F",	#CJK UNIFIED IDEOGRAPH
+	"E4 55	8193",	#CJK UNIFIED IDEOGRAPH
+	"E4 56	81A9",	#CJK UNIFIED IDEOGRAPH
+	"E4 57	81B0",	#CJK UNIFIED IDEOGRAPH
+	"E4 58	81B5",	#CJK UNIFIED IDEOGRAPH
+	"E4 59	81BE",	#CJK UNIFIED IDEOGRAPH
+	"E4 5A	81B8",	#CJK UNIFIED IDEOGRAPH
+	"E4 5B	81BD",	#CJK UNIFIED IDEOGRAPH
+	"E4 5C	81C0",	#CJK UNIFIED IDEOGRAPH
+	"E4 5D	81C2",	#CJK UNIFIED IDEOGRAPH
+	"E4 5E	81BA",	#CJK UNIFIED IDEOGRAPH
+	"E4 5F	81C9",	#CJK UNIFIED IDEOGRAPH
+	"E4 60	81CD",	#CJK UNIFIED IDEOGRAPH
+	"E4 61	81D1",	#CJK UNIFIED IDEOGRAPH
+	"E4 62	81D9",	#CJK UNIFIED IDEOGRAPH
+	"E4 63	81D8",	#CJK UNIFIED IDEOGRAPH
+	"E4 64	81C8",	#CJK UNIFIED IDEOGRAPH
+	"E4 65	81DA",	#CJK UNIFIED IDEOGRAPH
+	"E4 66	81DF",	#CJK UNIFIED IDEOGRAPH
+	"E4 67	81E0",	#CJK UNIFIED IDEOGRAPH
+	"E4 68	81E7",	#CJK UNIFIED IDEOGRAPH
+	"E4 69	81FA",	#CJK UNIFIED IDEOGRAPH
+	"E4 6A	81FB",	#CJK UNIFIED IDEOGRAPH
+	"E4 6B	81FE",	#CJK UNIFIED IDEOGRAPH
+	"E4 6C	8201",	#CJK UNIFIED IDEOGRAPH
+	"E4 6D	8202",	#CJK UNIFIED IDEOGRAPH
+	"E4 6E	8205",	#CJK UNIFIED IDEOGRAPH
+	"E4 6F	8207",	#CJK UNIFIED IDEOGRAPH
+	"E4 70	820A",	#CJK UNIFIED IDEOGRAPH
+	"E4 71	820D",	#CJK UNIFIED IDEOGRAPH
+	"E4 72	8210",	#CJK UNIFIED IDEOGRAPH
+	"E4 73	8216",	#CJK UNIFIED IDEOGRAPH
+	"E4 74	8229",	#CJK UNIFIED IDEOGRAPH
+	"E4 75	822B",	#CJK UNIFIED IDEOGRAPH
+	"E4 76	8238",	#CJK UNIFIED IDEOGRAPH
+	"E4 77	8233",	#CJK UNIFIED IDEOGRAPH
+	"E4 78	8240",	#CJK UNIFIED IDEOGRAPH
+	"E4 79	8259",	#CJK UNIFIED IDEOGRAPH
+	"E4 7A	8258",	#CJK UNIFIED IDEOGRAPH
+	"E4 7B	825D",	#CJK UNIFIED IDEOGRAPH
+	"E4 7C	825A",	#CJK UNIFIED IDEOGRAPH
+	"E4 7D	825F",	#CJK UNIFIED IDEOGRAPH
+	"E4 7E	8264",	#CJK UNIFIED IDEOGRAPH
+	"E4 80	8262",	#CJK UNIFIED IDEOGRAPH
+	"E4 81	8268",	#CJK UNIFIED IDEOGRAPH
+	"E4 82	826A",	#CJK UNIFIED IDEOGRAPH
+	"E4 83	826B",	#CJK UNIFIED IDEOGRAPH
+	"E4 84	822E",	#CJK UNIFIED IDEOGRAPH
+	"E4 85	8271",	#CJK UNIFIED IDEOGRAPH
+	"E4 86	8277",	#CJK UNIFIED IDEOGRAPH
+	"E4 87	8278",	#CJK UNIFIED IDEOGRAPH
+	"E4 88	827E",	#CJK UNIFIED IDEOGRAPH
+	"E4 89	828D",	#CJK UNIFIED IDEOGRAPH
+	"E4 8A	8292",	#CJK UNIFIED IDEOGRAPH
+	"E4 8B	82AB",	#CJK UNIFIED IDEOGRAPH
+	"E4 8C	829F",	#CJK UNIFIED IDEOGRAPH
+	"E4 8D	82BB",	#CJK UNIFIED IDEOGRAPH
+	"E4 8E	82AC",	#CJK UNIFIED IDEOGRAPH
+	"E4 8F	82E1",	#CJK UNIFIED IDEOGRAPH
+	"E4 90	82E3",	#CJK UNIFIED IDEOGRAPH
+	"E4 91	82DF",	#CJK UNIFIED IDEOGRAPH
+	"E4 92	82D2",	#CJK UNIFIED IDEOGRAPH
+	"E4 93	82F4",	#CJK UNIFIED IDEOGRAPH
+	"E4 94	82F3",	#CJK UNIFIED IDEOGRAPH
+	"E4 95	82FA",	#CJK UNIFIED IDEOGRAPH
+	"E4 96	8393",	#CJK UNIFIED IDEOGRAPH
+	"E4 97	8303",	#CJK UNIFIED IDEOGRAPH
+	"E4 98	82FB",	#CJK UNIFIED IDEOGRAPH
+	"E4 99	82F9",	#CJK UNIFIED IDEOGRAPH
+	"E4 9A	82DE",	#CJK UNIFIED IDEOGRAPH
+	"E4 9B	8306",	#CJK UNIFIED IDEOGRAPH
+	"E4 9C	82DC",	#CJK UNIFIED IDEOGRAPH
+	"E4 9D	8309",	#CJK UNIFIED IDEOGRAPH
+	"E4 9E	82D9",	#CJK UNIFIED IDEOGRAPH
+	"E4 9F	8335",	#CJK UNIFIED IDEOGRAPH
+	"E4 A0	8334",	#CJK UNIFIED IDEOGRAPH
+	"E4 A1	8316",	#CJK UNIFIED IDEOGRAPH
+	"E4 A2	8332",	#CJK UNIFIED IDEOGRAPH
+	"E4 A3	8331",	#CJK UNIFIED IDEOGRAPH
+	"E4 A4	8340",	#CJK UNIFIED IDEOGRAPH
+	"E4 A5	8339",	#CJK UNIFIED IDEOGRAPH
+	"E4 A6	8350",	#CJK UNIFIED IDEOGRAPH
+	"E4 A7	8345",	#CJK UNIFIED IDEOGRAPH
+	"E4 A8	832F",	#CJK UNIFIED IDEOGRAPH
+	"E4 A9	832B",	#CJK UNIFIED IDEOGRAPH
+	"E4 AA	8317",	#CJK UNIFIED IDEOGRAPH
+	"E4 AB	8318",	#CJK UNIFIED IDEOGRAPH
+	"E4 AC	8385",	#CJK UNIFIED IDEOGRAPH
+	"E4 AD	839A",	#CJK UNIFIED IDEOGRAPH
+	"E4 AE	83AA",	#CJK UNIFIED IDEOGRAPH
+	"E4 AF	839F",	#CJK UNIFIED IDEOGRAPH
+	"E4 B0	83A2",	#CJK UNIFIED IDEOGRAPH
+	"E4 B1	8396",	#CJK UNIFIED IDEOGRAPH
+	"E4 B2	8323",	#CJK UNIFIED IDEOGRAPH
+	"E4 B3	838E",	#CJK UNIFIED IDEOGRAPH
+	"E4 B4	8387",	#CJK UNIFIED IDEOGRAPH
+	"E4 B5	838A",	#CJK UNIFIED IDEOGRAPH
+	"E4 B6	837C",	#CJK UNIFIED IDEOGRAPH
+	"E4 B7	83B5",	#CJK UNIFIED IDEOGRAPH
+	"E4 B8	8373",	#CJK UNIFIED IDEOGRAPH
+	"E4 B9	8375",	#CJK UNIFIED IDEOGRAPH
+	"E4 BA	83A0",	#CJK UNIFIED IDEOGRAPH
+	"E4 BB	8389",	#CJK UNIFIED IDEOGRAPH
+	"E4 BC	83A8",	#CJK UNIFIED IDEOGRAPH
+	"E4 BD	83F4",	#CJK UNIFIED IDEOGRAPH
+	"E4 BE	8413",	#CJK UNIFIED IDEOGRAPH
+	"E4 BF	83EB",	#CJK UNIFIED IDEOGRAPH
+	"E4 C0	83CE",	#CJK UNIFIED IDEOGRAPH
+	"E4 C1	83FD",	#CJK UNIFIED IDEOGRAPH
+	"E4 C2	8403",	#CJK UNIFIED IDEOGRAPH
+	"E4 C3	83D8",	#CJK UNIFIED IDEOGRAPH
+	"E4 C4	840B",	#CJK UNIFIED IDEOGRAPH
+	"E4 C5	83C1",	#CJK UNIFIED IDEOGRAPH
+	"E4 C6	83F7",	#CJK UNIFIED IDEOGRAPH
+	"E4 C7	8407",	#CJK UNIFIED IDEOGRAPH
+	"E4 C8	83E0",	#CJK UNIFIED IDEOGRAPH
+	"E4 C9	83F2",	#CJK UNIFIED IDEOGRAPH
+	"E4 CA	840D",	#CJK UNIFIED IDEOGRAPH
+	"E4 CB	8422",	#CJK UNIFIED IDEOGRAPH
+	"E4 CC	8420",	#CJK UNIFIED IDEOGRAPH
+	"E4 CD	83BD",	#CJK UNIFIED IDEOGRAPH
+	"E4 CE	8438",	#CJK UNIFIED IDEOGRAPH
+	"E4 CF	8506",	#CJK UNIFIED IDEOGRAPH
+	"E4 D0	83FB",	#CJK UNIFIED IDEOGRAPH
+	"E4 D1	846D",	#CJK UNIFIED IDEOGRAPH
+	"E4 D2	842A",	#CJK UNIFIED IDEOGRAPH
+	"E4 D3	843C",	#CJK UNIFIED IDEOGRAPH
+	"E4 D4	855A",	#CJK UNIFIED IDEOGRAPH
+	"E4 D5	8484",	#CJK UNIFIED IDEOGRAPH
+	"E4 D6	8477",	#CJK UNIFIED IDEOGRAPH
+	"E4 D7	846B",	#CJK UNIFIED IDEOGRAPH
+	"E4 D8	84AD",	#CJK UNIFIED IDEOGRAPH
+	"E4 D9	846E",	#CJK UNIFIED IDEOGRAPH
+	"E4 DA	8482",	#CJK UNIFIED IDEOGRAPH
+	"E4 DB	8469",	#CJK UNIFIED IDEOGRAPH
+	"E4 DC	8446",	#CJK UNIFIED IDEOGRAPH
+	"E4 DD	842C",	#CJK UNIFIED IDEOGRAPH
+	"E4 DE	846F",	#CJK UNIFIED IDEOGRAPH
+	"E4 DF	8479",	#CJK UNIFIED IDEOGRAPH
+	"E4 E0	8435",	#CJK UNIFIED IDEOGRAPH
+	"E4 E1	84CA",	#CJK UNIFIED IDEOGRAPH
+	"E4 E2	8462",	#CJK UNIFIED IDEOGRAPH
+	"E4 E3	84B9",	#CJK UNIFIED IDEOGRAPH
+	"E4 E4	84BF",	#CJK UNIFIED IDEOGRAPH
+	"E4 E5	849F",	#CJK UNIFIED IDEOGRAPH
+	"E4 E6	84D9",	#CJK UNIFIED IDEOGRAPH
+	"E4 E7	84CD",	#CJK UNIFIED IDEOGRAPH
+	"E4 E8	84BB",	#CJK UNIFIED IDEOGRAPH
+	"E4 E9	84DA",	#CJK UNIFIED IDEOGRAPH
+	"E4 EA	84D0",	#CJK UNIFIED IDEOGRAPH
+	"E4 EB	84C1",	#CJK UNIFIED IDEOGRAPH
+	"E4 EC	84C6",	#CJK UNIFIED IDEOGRAPH
+	"E4 ED	84D6",	#CJK UNIFIED IDEOGRAPH
+	"E4 EE	84A1",	#CJK UNIFIED IDEOGRAPH
+	"E4 EF	8521",	#CJK UNIFIED IDEOGRAPH
+	"E4 F0	84FF",	#CJK UNIFIED IDEOGRAPH
+	"E4 F1	84F4",	#CJK UNIFIED IDEOGRAPH
+	"E4 F2	8517",	#CJK UNIFIED IDEOGRAPH
+	"E4 F3	8518",	#CJK UNIFIED IDEOGRAPH
+	"E4 F4	852C",	#CJK UNIFIED IDEOGRAPH
+	"E4 F5	851F",	#CJK UNIFIED IDEOGRAPH
+	"E4 F6	8515",	#CJK UNIFIED IDEOGRAPH
+	"E4 F7	8514",	#CJK UNIFIED IDEOGRAPH
+	"E4 F8	84FC",	#CJK UNIFIED IDEOGRAPH
+	"E4 F9	8540",	#CJK UNIFIED IDEOGRAPH
+	"E4 FA	8563",	#CJK UNIFIED IDEOGRAPH
+	"E4 FB	8558",	#CJK UNIFIED IDEOGRAPH
+	"E4 FC	8548",	#CJK UNIFIED IDEOGRAPH
+	"E5 40	8541",	#CJK UNIFIED IDEOGRAPH
+	"E5 41	8602",	#CJK UNIFIED IDEOGRAPH
+	"E5 42	854B",	#CJK UNIFIED IDEOGRAPH
+	"E5 43	8555",	#CJK UNIFIED IDEOGRAPH
+	"E5 44	8580",	#CJK UNIFIED IDEOGRAPH
+	"E5 45	85A4",	#CJK UNIFIED IDEOGRAPH
+	"E5 46	8588",	#CJK UNIFIED IDEOGRAPH
+	"E5 47	8591",	#CJK UNIFIED IDEOGRAPH
+	"E5 48	858A",	#CJK UNIFIED IDEOGRAPH
+	"E5 49	85A8",	#CJK UNIFIED IDEOGRAPH
+	"E5 4A	856D",	#CJK UNIFIED IDEOGRAPH
+	"E5 4B	8594",	#CJK UNIFIED IDEOGRAPH
+	"E5 4C	859B",	#CJK UNIFIED IDEOGRAPH
+	"E5 4D	85EA",	#CJK UNIFIED IDEOGRAPH
+	"E5 4E	8587",	#CJK UNIFIED IDEOGRAPH
+	"E5 4F	859C",	#CJK UNIFIED IDEOGRAPH
+	"E5 50	8577",	#CJK UNIFIED IDEOGRAPH
+	"E5 51	857E",	#CJK UNIFIED IDEOGRAPH
+	"E5 52	8590",	#CJK UNIFIED IDEOGRAPH
+	"E5 53	85C9",	#CJK UNIFIED IDEOGRAPH
+	"E5 54	85BA",	#CJK UNIFIED IDEOGRAPH
+	"E5 55	85CF",	#CJK UNIFIED IDEOGRAPH
+	"E5 56	85B9",	#CJK UNIFIED IDEOGRAPH
+	"E5 57	85D0",	#CJK UNIFIED IDEOGRAPH
+	"E5 58	85D5",	#CJK UNIFIED IDEOGRAPH
+	"E5 59	85DD",	#CJK UNIFIED IDEOGRAPH
+	"E5 5A	85E5",	#CJK UNIFIED IDEOGRAPH
+	"E5 5B	85DC",	#CJK UNIFIED IDEOGRAPH
+	"E5 5C	85F9",	#CJK UNIFIED IDEOGRAPH
+	"E5 5D	860A",	#CJK UNIFIED IDEOGRAPH
+	"E5 5E	8613",	#CJK UNIFIED IDEOGRAPH
+	"E5 5F	860B",	#CJK UNIFIED IDEOGRAPH
+	"E5 60	85FE",	#CJK UNIFIED IDEOGRAPH
+	"E5 61	85FA",	#CJK UNIFIED IDEOGRAPH
+	"E5 62	8606",	#CJK UNIFIED IDEOGRAPH
+	"E5 63	8622",	#CJK UNIFIED IDEOGRAPH
+	"E5 64	861A",	#CJK UNIFIED IDEOGRAPH
+	"E5 65	8630",	#CJK UNIFIED IDEOGRAPH
+	"E5 66	863F",	#CJK UNIFIED IDEOGRAPH
+	"E5 67	864D",	#CJK UNIFIED IDEOGRAPH
+	"E5 68	4E55",	#CJK UNIFIED IDEOGRAPH
+	"E5 69	8654",	#CJK UNIFIED IDEOGRAPH
+	"E5 6A	865F",	#CJK UNIFIED IDEOGRAPH
+	"E5 6B	8667",	#CJK UNIFIED IDEOGRAPH
+	"E5 6C	8671",	#CJK UNIFIED IDEOGRAPH
+	"E5 6D	8693",	#CJK UNIFIED IDEOGRAPH
+	"E5 6E	86A3",	#CJK UNIFIED IDEOGRAPH
+	"E5 6F	86A9",	#CJK UNIFIED IDEOGRAPH
+	"E5 70	86AA",	#CJK UNIFIED IDEOGRAPH
+	"E5 71	868B",	#CJK UNIFIED IDEOGRAPH
+	"E5 72	868C",	#CJK UNIFIED IDEOGRAPH
+	"E5 73	86B6",	#CJK UNIFIED IDEOGRAPH
+	"E5 74	86AF",	#CJK UNIFIED IDEOGRAPH
+	"E5 75	86C4",	#CJK UNIFIED IDEOGRAPH
+	"E5 76	86C6",	#CJK UNIFIED IDEOGRAPH
+	"E5 77	86B0",	#CJK UNIFIED IDEOGRAPH
+	"E5 78	86C9",	#CJK UNIFIED IDEOGRAPH
+	"E5 79	8823",	#CJK UNIFIED IDEOGRAPH
+	"E5 7A	86AB",	#CJK UNIFIED IDEOGRAPH
+	"E5 7B	86D4",	#CJK UNIFIED IDEOGRAPH
+	"E5 7C	86DE",	#CJK UNIFIED IDEOGRAPH
+	"E5 7D	86E9",	#CJK UNIFIED IDEOGRAPH
+	"E5 7E	86EC",	#CJK UNIFIED IDEOGRAPH
+	"E5 80	86DF",	#CJK UNIFIED IDEOGRAPH
+	"E5 81	86DB",	#CJK UNIFIED IDEOGRAPH
+	"E5 82	86EF",	#CJK UNIFIED IDEOGRAPH
+	"E5 83	8712",	#CJK UNIFIED IDEOGRAPH
+	"E5 84	8706",	#CJK UNIFIED IDEOGRAPH
+	"E5 85	8708",	#CJK UNIFIED IDEOGRAPH
+	"E5 86	8700",	#CJK UNIFIED IDEOGRAPH
+	"E5 87	8703",	#CJK UNIFIED IDEOGRAPH
+	"E5 88	86FB",	#CJK UNIFIED IDEOGRAPH
+	"E5 89	8711",	#CJK UNIFIED IDEOGRAPH
+	"E5 8A	8709",	#CJK UNIFIED IDEOGRAPH
+	"E5 8B	870D",	#CJK UNIFIED IDEOGRAPH
+	"E5 8C	86F9",	#CJK UNIFIED IDEOGRAPH
+	"E5 8D	870A",	#CJK UNIFIED IDEOGRAPH
+	"E5 8E	8734",	#CJK UNIFIED IDEOGRAPH
+	"E5 8F	873F",	#CJK UNIFIED IDEOGRAPH
+	"E5 90	8737",	#CJK UNIFIED IDEOGRAPH
+	"E5 91	873B",	#CJK UNIFIED IDEOGRAPH
+	"E5 92	8725",	#CJK UNIFIED IDEOGRAPH
+	"E5 93	8729",	#CJK UNIFIED IDEOGRAPH
+	"E5 94	871A",	#CJK UNIFIED IDEOGRAPH
+	"E5 95	8760",	#CJK UNIFIED IDEOGRAPH
+	"E5 96	875F",	#CJK UNIFIED IDEOGRAPH
+	"E5 97	8778",	#CJK UNIFIED IDEOGRAPH
+	"E5 98	874C",	#CJK UNIFIED IDEOGRAPH
+	"E5 99	874E",	#CJK UNIFIED IDEOGRAPH
+	"E5 9A	8774",	#CJK UNIFIED IDEOGRAPH
+	"E5 9B	8757",	#CJK UNIFIED IDEOGRAPH
+	"E5 9C	8768",	#CJK UNIFIED IDEOGRAPH
+	"E5 9D	876E",	#CJK UNIFIED IDEOGRAPH
+	"E5 9E	8759",	#CJK UNIFIED IDEOGRAPH
+	"E5 9F	8753",	#CJK UNIFIED IDEOGRAPH
+	"E5 A0	8763",	#CJK UNIFIED IDEOGRAPH
+	"E5 A1	876A",	#CJK UNIFIED IDEOGRAPH
+	"E5 A2	8805",	#CJK UNIFIED IDEOGRAPH
+	"E5 A3	87A2",	#CJK UNIFIED IDEOGRAPH
+	"E5 A4	879F",	#CJK UNIFIED IDEOGRAPH
+	"E5 A5	8782",	#CJK UNIFIED IDEOGRAPH
+	"E5 A6	87AF",	#CJK UNIFIED IDEOGRAPH
+	"E5 A7	87CB",	#CJK UNIFIED IDEOGRAPH
+	"E5 A8	87BD",	#CJK UNIFIED IDEOGRAPH
+	"E5 A9	87C0",	#CJK UNIFIED IDEOGRAPH
+	"E5 AA	87D0",	#CJK UNIFIED IDEOGRAPH
+	"E5 AB	96D6",	#CJK UNIFIED IDEOGRAPH
+	"E5 AC	87AB",	#CJK UNIFIED IDEOGRAPH
+	"E5 AD	87C4",	#CJK UNIFIED IDEOGRAPH
+	"E5 AE	87B3",	#CJK UNIFIED IDEOGRAPH
+	"E5 AF	87C7",	#CJK UNIFIED IDEOGRAPH
+	"E5 B0	87C6",	#CJK UNIFIED IDEOGRAPH
+	"E5 B1	87BB",	#CJK UNIFIED IDEOGRAPH
+	"E5 B2	87EF",	#CJK UNIFIED IDEOGRAPH
+	"E5 B3	87F2",	#CJK UNIFIED IDEOGRAPH
+	"E5 B4	87E0",	#CJK UNIFIED IDEOGRAPH
+	"E5 B5	880F",	#CJK UNIFIED IDEOGRAPH
+	"E5 B6	880D",	#CJK UNIFIED IDEOGRAPH
+	"E5 B7	87FE",	#CJK UNIFIED IDEOGRAPH
+	"E5 B8	87F6",	#CJK UNIFIED IDEOGRAPH
+	"E5 B9	87F7",	#CJK UNIFIED IDEOGRAPH
+	"E5 BA	880E",	#CJK UNIFIED IDEOGRAPH
+	"E5 BB	87D2",	#CJK UNIFIED IDEOGRAPH
+	"E5 BC	8811",	#CJK UNIFIED IDEOGRAPH
+	"E5 BD	8816",	#CJK UNIFIED IDEOGRAPH
+	"E5 BE	8815",	#CJK UNIFIED IDEOGRAPH
+	"E5 BF	8822",	#CJK UNIFIED IDEOGRAPH
+	"E5 C0	8821",	#CJK UNIFIED IDEOGRAPH
+	"E5 C1	8831",	#CJK UNIFIED IDEOGRAPH
+	"E5 C2	8836",	#CJK UNIFIED IDEOGRAPH
+	"E5 C3	8839",	#CJK UNIFIED IDEOGRAPH
+	"E5 C4	8827",	#CJK UNIFIED IDEOGRAPH
+	"E5 C5	883B",	#CJK UNIFIED IDEOGRAPH
+	"E5 C6	8844",	#CJK UNIFIED IDEOGRAPH
+	"E5 C7	8842",	#CJK UNIFIED IDEOGRAPH
+	"E5 C8	8852",	#CJK UNIFIED IDEOGRAPH
+	"E5 C9	8859",	#CJK UNIFIED IDEOGRAPH
+	"E5 CA	885E",	#CJK UNIFIED IDEOGRAPH
+	"E5 CB	8862",	#CJK UNIFIED IDEOGRAPH
+	"E5 CC	886B",	#CJK UNIFIED IDEOGRAPH
+	"E5 CD	8881",	#CJK UNIFIED IDEOGRAPH
+	"E5 CE	887E",	#CJK UNIFIED IDEOGRAPH
+	"E5 CF	889E",	#CJK UNIFIED IDEOGRAPH
+	"E5 D0	8875",	#CJK UNIFIED IDEOGRAPH
+	"E5 D1	887D",	#CJK UNIFIED IDEOGRAPH
+	"E5 D2	88B5",	#CJK UNIFIED IDEOGRAPH
+	"E5 D3	8872",	#CJK UNIFIED IDEOGRAPH
+	"E5 D4	8882",	#CJK UNIFIED IDEOGRAPH
+	"E5 D5	8897",	#CJK UNIFIED IDEOGRAPH
+	"E5 D6	8892",	#CJK UNIFIED IDEOGRAPH
+	"E5 D7	88AE",	#CJK UNIFIED IDEOGRAPH
+	"E5 D8	8899",	#CJK UNIFIED IDEOGRAPH
+	"E5 D9	88A2",	#CJK UNIFIED IDEOGRAPH
+	"E5 DA	888D",	#CJK UNIFIED IDEOGRAPH
+	"E5 DB	88A4",	#CJK UNIFIED IDEOGRAPH
+	"E5 DC	88B0",	#CJK UNIFIED IDEOGRAPH
+	"E5 DD	88BF",	#CJK UNIFIED IDEOGRAPH
+	"E5 DE	88B1",	#CJK UNIFIED IDEOGRAPH
+	"E5 DF	88C3",	#CJK UNIFIED IDEOGRAPH
+	"E5 E0	88C4",	#CJK UNIFIED IDEOGRAPH
+	"E5 E1	88D4",	#CJK UNIFIED IDEOGRAPH
+	"E5 E2	88D8",	#CJK UNIFIED IDEOGRAPH
+	"E5 E3	88D9",	#CJK UNIFIED IDEOGRAPH
+	"E5 E4	88DD",	#CJK UNIFIED IDEOGRAPH
+	"E5 E5	88F9",	#CJK UNIFIED IDEOGRAPH
+	"E5 E6	8902",	#CJK UNIFIED IDEOGRAPH
+	"E5 E7	88FC",	#CJK UNIFIED IDEOGRAPH
+	"E5 E8	88F4",	#CJK UNIFIED IDEOGRAPH
+	"E5 E9	88E8",	#CJK UNIFIED IDEOGRAPH
+	"E5 EA	88F2",	#CJK UNIFIED IDEOGRAPH
+	"E5 EB	8904",	#CJK UNIFIED IDEOGRAPH
+	"E5 EC	890C",	#CJK UNIFIED IDEOGRAPH
+	"E5 ED	890A",	#CJK UNIFIED IDEOGRAPH
+	"E5 EE	8913",	#CJK UNIFIED IDEOGRAPH
+	"E5 EF	8943",	#CJK UNIFIED IDEOGRAPH
+	"E5 F0	891E",	#CJK UNIFIED IDEOGRAPH
+	"E5 F1	8925",	#CJK UNIFIED IDEOGRAPH
+	"E5 F2	892A",	#CJK UNIFIED IDEOGRAPH
+	"E5 F3	892B",	#CJK UNIFIED IDEOGRAPH
+	"E5 F4	8941",	#CJK UNIFIED IDEOGRAPH
+	"E5 F5	8944",	#CJK UNIFIED IDEOGRAPH
+	"E5 F6	893B",	#CJK UNIFIED IDEOGRAPH
+	"E5 F7	8936",	#CJK UNIFIED IDEOGRAPH
+	"E5 F8	8938",	#CJK UNIFIED IDEOGRAPH
+	"E5 F9	894C",	#CJK UNIFIED IDEOGRAPH
+	"E5 FA	891D",	#CJK UNIFIED IDEOGRAPH
+	"E5 FB	8960",	#CJK UNIFIED IDEOGRAPH
+	"E5 FC	895E",	#CJK UNIFIED IDEOGRAPH
+	"E6 40	8966",	#CJK UNIFIED IDEOGRAPH
+	"E6 41	8964",	#CJK UNIFIED IDEOGRAPH
+	"E6 42	896D",	#CJK UNIFIED IDEOGRAPH
+	"E6 43	896A",	#CJK UNIFIED IDEOGRAPH
+	"E6 44	896F",	#CJK UNIFIED IDEOGRAPH
+	"E6 45	8974",	#CJK UNIFIED IDEOGRAPH
+	"E6 46	8977",	#CJK UNIFIED IDEOGRAPH
+	"E6 47	897E",	#CJK UNIFIED IDEOGRAPH
+	"E6 48	8983",	#CJK UNIFIED IDEOGRAPH
+	"E6 49	8988",	#CJK UNIFIED IDEOGRAPH
+	"E6 4A	898A",	#CJK UNIFIED IDEOGRAPH
+	"E6 4B	8993",	#CJK UNIFIED IDEOGRAPH
+	"E6 4C	8998",	#CJK UNIFIED IDEOGRAPH
+	"E6 4D	89A1",	#CJK UNIFIED IDEOGRAPH
+	"E6 4E	89A9",	#CJK UNIFIED IDEOGRAPH
+	"E6 4F	89A6",	#CJK UNIFIED IDEOGRAPH
+	"E6 50	89AC",	#CJK UNIFIED IDEOGRAPH
+	"E6 51	89AF",	#CJK UNIFIED IDEOGRAPH
+	"E6 52	89B2",	#CJK UNIFIED IDEOGRAPH
+	"E6 53	89BA",	#CJK UNIFIED IDEOGRAPH
+	"E6 54	89BD",	#CJK UNIFIED IDEOGRAPH
+	"E6 55	89BF",	#CJK UNIFIED IDEOGRAPH
+	"E6 56	89C0",	#CJK UNIFIED IDEOGRAPH
+	"E6 57	89DA",	#CJK UNIFIED IDEOGRAPH
+	"E6 58	89DC",	#CJK UNIFIED IDEOGRAPH
+	"E6 59	89DD",	#CJK UNIFIED IDEOGRAPH
+	"E6 5A	89E7",	#CJK UNIFIED IDEOGRAPH
+	"E6 5B	89F4",	#CJK UNIFIED IDEOGRAPH
+	"E6 5C	89F8",	#CJK UNIFIED IDEOGRAPH
+	"E6 5D	8A03",	#CJK UNIFIED IDEOGRAPH
+	"E6 5E	8A16",	#CJK UNIFIED IDEOGRAPH
+	"E6 5F	8A10",	#CJK UNIFIED IDEOGRAPH
+	"E6 60	8A0C",	#CJK UNIFIED IDEOGRAPH
+	"E6 61	8A1B",	#CJK UNIFIED IDEOGRAPH
+	"E6 62	8A1D",	#CJK UNIFIED IDEOGRAPH
+	"E6 63	8A25",	#CJK UNIFIED IDEOGRAPH
+	"E6 64	8A36",	#CJK UNIFIED IDEOGRAPH
+	"E6 65	8A41",	#CJK UNIFIED IDEOGRAPH
+	"E6 66	8A5B",	#CJK UNIFIED IDEOGRAPH
+	"E6 67	8A52",	#CJK UNIFIED IDEOGRAPH
+	"E6 68	8A46",	#CJK UNIFIED IDEOGRAPH
+	"E6 69	8A48",	#CJK UNIFIED IDEOGRAPH
+	"E6 6A	8A7C",	#CJK UNIFIED IDEOGRAPH
+	"E6 6B	8A6D",	#CJK UNIFIED IDEOGRAPH
+	"E6 6C	8A6C",	#CJK UNIFIED IDEOGRAPH
+	"E6 6D	8A62",	#CJK UNIFIED IDEOGRAPH
+	"E6 6E	8A85",	#CJK UNIFIED IDEOGRAPH
+	"E6 6F	8A82",	#CJK UNIFIED IDEOGRAPH
+	"E6 70	8A84",	#CJK UNIFIED IDEOGRAPH
+	"E6 71	8AA8",	#CJK UNIFIED IDEOGRAPH
+	"E6 72	8AA1",	#CJK UNIFIED IDEOGRAPH
+	"E6 73	8A91",	#CJK UNIFIED IDEOGRAPH
+	"E6 74	8AA5",	#CJK UNIFIED IDEOGRAPH
+	"E6 75	8AA6",	#CJK UNIFIED IDEOGRAPH
+	"E6 76	8A9A",	#CJK UNIFIED IDEOGRAPH
+	"E6 77	8AA3",	#CJK UNIFIED IDEOGRAPH
+	"E6 78	8AC4",	#CJK UNIFIED IDEOGRAPH
+	"E6 79	8ACD",	#CJK UNIFIED IDEOGRAPH
+	"E6 7A	8AC2",	#CJK UNIFIED IDEOGRAPH
+	"E6 7B	8ADA",	#CJK UNIFIED IDEOGRAPH
+	"E6 7C	8AEB",	#CJK UNIFIED IDEOGRAPH
+	"E6 7D	8AF3",	#CJK UNIFIED IDEOGRAPH
+	"E6 7E	8AE7",	#CJK UNIFIED IDEOGRAPH
+	"E6 80	8AE4",	#CJK UNIFIED IDEOGRAPH
+	"E6 81	8AF1",	#CJK UNIFIED IDEOGRAPH
+	"E6 82	8B14",	#CJK UNIFIED IDEOGRAPH
+	"E6 83	8AE0",	#CJK UNIFIED IDEOGRAPH
+	"E6 84	8AE2",	#CJK UNIFIED IDEOGRAPH
+	"E6 85	8AF7",	#CJK UNIFIED IDEOGRAPH
+	"E6 86	8ADE",	#CJK UNIFIED IDEOGRAPH
+	"E6 87	8ADB",	#CJK UNIFIED IDEOGRAPH
+	"E6 88	8B0C",	#CJK UNIFIED IDEOGRAPH
+	"E6 89	8B07",	#CJK UNIFIED IDEOGRAPH
+	"E6 8A	8B1A",	#CJK UNIFIED IDEOGRAPH
+	"E6 8B	8AE1",	#CJK UNIFIED IDEOGRAPH
+	"E6 8C	8B16",	#CJK UNIFIED IDEOGRAPH
+	"E6 8D	8B10",	#CJK UNIFIED IDEOGRAPH
+	"E6 8E	8B17",	#CJK UNIFIED IDEOGRAPH
+	"E6 8F	8B20",	#CJK UNIFIED IDEOGRAPH
+	"E6 90	8B33",	#CJK UNIFIED IDEOGRAPH
+	"E6 91	97AB",	#CJK UNIFIED IDEOGRAPH
+	"E6 92	8B26",	#CJK UNIFIED IDEOGRAPH
+	"E6 93	8B2B",	#CJK UNIFIED IDEOGRAPH
+	"E6 94	8B3E",	#CJK UNIFIED IDEOGRAPH
+	"E6 95	8B28",	#CJK UNIFIED IDEOGRAPH
+	"E6 96	8B41",	#CJK UNIFIED IDEOGRAPH
+	"E6 97	8B4C",	#CJK UNIFIED IDEOGRAPH
+	"E6 98	8B4F",	#CJK UNIFIED IDEOGRAPH
+	"E6 99	8B4E",	#CJK UNIFIED IDEOGRAPH
+	"E6 9A	8B49",	#CJK UNIFIED IDEOGRAPH
+	"E6 9B	8B56",	#CJK UNIFIED IDEOGRAPH
+	"E6 9C	8B5B",	#CJK UNIFIED IDEOGRAPH
+	"E6 9D	8B5A",	#CJK UNIFIED IDEOGRAPH
+	"E6 9E	8B6B",	#CJK UNIFIED IDEOGRAPH
+	"E6 9F	8B5F",	#CJK UNIFIED IDEOGRAPH
+	"E6 A0	8B6C",	#CJK UNIFIED IDEOGRAPH
+	"E6 A1	8B6F",	#CJK UNIFIED IDEOGRAPH
+	"E6 A2	8B74",	#CJK UNIFIED IDEOGRAPH
+	"E6 A3	8B7D",	#CJK UNIFIED IDEOGRAPH
+	"E6 A4	8B80",	#CJK UNIFIED IDEOGRAPH
+	"E6 A5	8B8C",	#CJK UNIFIED IDEOGRAPH
+	"E6 A6	8B8E",	#CJK UNIFIED IDEOGRAPH
+	"E6 A7	8B92",	#CJK UNIFIED IDEOGRAPH
+	"E6 A8	8B93",	#CJK UNIFIED IDEOGRAPH
+	"E6 A9	8B96",	#CJK UNIFIED IDEOGRAPH
+	"E6 AA	8B99",	#CJK UNIFIED IDEOGRAPH
+	"E6 AB	8B9A",	#CJK UNIFIED IDEOGRAPH
+	"E6 AC	8C3A",	#CJK UNIFIED IDEOGRAPH
+	"E6 AD	8C41",	#CJK UNIFIED IDEOGRAPH
+	"E6 AE	8C3F",	#CJK UNIFIED IDEOGRAPH
+	"E6 AF	8C48",	#CJK UNIFIED IDEOGRAPH
+	"E6 B0	8C4C",	#CJK UNIFIED IDEOGRAPH
+	"E6 B1	8C4E",	#CJK UNIFIED IDEOGRAPH
+	"E6 B2	8C50",	#CJK UNIFIED IDEOGRAPH
+	"E6 B3	8C55",	#CJK UNIFIED IDEOGRAPH
+	"E6 B4	8C62",	#CJK UNIFIED IDEOGRAPH
+	"E6 B5	8C6C",	#CJK UNIFIED IDEOGRAPH
+	"E6 B6	8C78",	#CJK UNIFIED IDEOGRAPH
+	"E6 B7	8C7A",	#CJK UNIFIED IDEOGRAPH
+	"E6 B8	8C82",	#CJK UNIFIED IDEOGRAPH
+	"E6 B9	8C89",	#CJK UNIFIED IDEOGRAPH
+	"E6 BA	8C85",	#CJK UNIFIED IDEOGRAPH
+	"E6 BB	8C8A",	#CJK UNIFIED IDEOGRAPH
+	"E6 BC	8C8D",	#CJK UNIFIED IDEOGRAPH
+	"E6 BD	8C8E",	#CJK UNIFIED IDEOGRAPH
+	"E6 BE	8C94",	#CJK UNIFIED IDEOGRAPH
+	"E6 BF	8C7C",	#CJK UNIFIED IDEOGRAPH
+	"E6 C0	8C98",	#CJK UNIFIED IDEOGRAPH
+	"E6 C1	621D",	#CJK UNIFIED IDEOGRAPH
+	"E6 C2	8CAD",	#CJK UNIFIED IDEOGRAPH
+	"E6 C3	8CAA",	#CJK UNIFIED IDEOGRAPH
+	"E6 C4	8CBD",	#CJK UNIFIED IDEOGRAPH
+	"E6 C5	8CB2",	#CJK UNIFIED IDEOGRAPH
+	"E6 C6	8CB3",	#CJK UNIFIED IDEOGRAPH
+	"E6 C7	8CAE",	#CJK UNIFIED IDEOGRAPH
+	"E6 C8	8CB6",	#CJK UNIFIED IDEOGRAPH
+	"E6 C9	8CC8",	#CJK UNIFIED IDEOGRAPH
+	"E6 CA	8CC1",	#CJK UNIFIED IDEOGRAPH
+	"E6 CB	8CE4",	#CJK UNIFIED IDEOGRAPH
+	"E6 CC	8CE3",	#CJK UNIFIED IDEOGRAPH
+	"E6 CD	8CDA",	#CJK UNIFIED IDEOGRAPH
+	"E6 CE	8CFD",	#CJK UNIFIED IDEOGRAPH
+	"E6 CF	8CFA",	#CJK UNIFIED IDEOGRAPH
+	"E6 D0	8CFB",	#CJK UNIFIED IDEOGRAPH
+	"E6 D1	8D04",	#CJK UNIFIED IDEOGRAPH
+	"E6 D2	8D05",	#CJK UNIFIED IDEOGRAPH
+	"E6 D3	8D0A",	#CJK UNIFIED IDEOGRAPH
+	"E6 D4	8D07",	#CJK UNIFIED IDEOGRAPH
+	"E6 D5	8D0F",	#CJK UNIFIED IDEOGRAPH
+	"E6 D6	8D0D",	#CJK UNIFIED IDEOGRAPH
+	"E6 D7	8D10",	#CJK UNIFIED IDEOGRAPH
+	"E6 D8	9F4E",	#CJK UNIFIED IDEOGRAPH
+	"E6 D9	8D13",	#CJK UNIFIED IDEOGRAPH
+	"E6 DA	8CCD",	#CJK UNIFIED IDEOGRAPH
+	"E6 DB	8D14",	#CJK UNIFIED IDEOGRAPH
+	"E6 DC	8D16",	#CJK UNIFIED IDEOGRAPH
+	"E6 DD	8D67",	#CJK UNIFIED IDEOGRAPH
+	"E6 DE	8D6D",	#CJK UNIFIED IDEOGRAPH
+	"E6 DF	8D71",	#CJK UNIFIED IDEOGRAPH
+	"E6 E0	8D73",	#CJK UNIFIED IDEOGRAPH
+	"E6 E1	8D81",	#CJK UNIFIED IDEOGRAPH
+	"E6 E2	8D99",	#CJK UNIFIED IDEOGRAPH
+	"E6 E3	8DC2",	#CJK UNIFIED IDEOGRAPH
+	"E6 E4	8DBE",	#CJK UNIFIED IDEOGRAPH
+	"E6 E5	8DBA",	#CJK UNIFIED IDEOGRAPH
+	"E6 E6	8DCF",	#CJK UNIFIED IDEOGRAPH
+	"E6 E7	8DDA",	#CJK UNIFIED IDEOGRAPH
+	"E6 E8	8DD6",	#CJK UNIFIED IDEOGRAPH
+	"E6 E9	8DCC",	#CJK UNIFIED IDEOGRAPH
+	"E6 EA	8DDB",	#CJK UNIFIED IDEOGRAPH
+	"E6 EB	8DCB",	#CJK UNIFIED IDEOGRAPH
+	"E6 EC	8DEA",	#CJK UNIFIED IDEOGRAPH
+	"E6 ED	8DEB",	#CJK UNIFIED IDEOGRAPH
+	"E6 EE	8DDF",	#CJK UNIFIED IDEOGRAPH
+	"E6 EF	8DE3",	#CJK UNIFIED IDEOGRAPH
+	"E6 F0	8DFC",	#CJK UNIFIED IDEOGRAPH
+	"E6 F1	8E08",	#CJK UNIFIED IDEOGRAPH
+	"E6 F2	8E09",	#CJK UNIFIED IDEOGRAPH
+	"E6 F3	8DFF",	#CJK UNIFIED IDEOGRAPH
+	"E6 F4	8E1D",	#CJK UNIFIED IDEOGRAPH
+	"E6 F5	8E1E",	#CJK UNIFIED IDEOGRAPH
+	"E6 F6	8E10",	#CJK UNIFIED IDEOGRAPH
+	"E6 F7	8E1F",	#CJK UNIFIED IDEOGRAPH
+	"E6 F8	8E42",	#CJK UNIFIED IDEOGRAPH
+	"E6 F9	8E35",	#CJK UNIFIED IDEOGRAPH
+	"E6 FA	8E30",	#CJK UNIFIED IDEOGRAPH
+	"E6 FB	8E34",	#CJK UNIFIED IDEOGRAPH
+	"E6 FC	8E4A",	#CJK UNIFIED IDEOGRAPH
+	"E7 40	8E47",	#CJK UNIFIED IDEOGRAPH
+	"E7 41	8E49",	#CJK UNIFIED IDEOGRAPH
+	"E7 42	8E4C",	#CJK UNIFIED IDEOGRAPH
+	"E7 43	8E50",	#CJK UNIFIED IDEOGRAPH
+	"E7 44	8E48",	#CJK UNIFIED IDEOGRAPH
+	"E7 45	8E59",	#CJK UNIFIED IDEOGRAPH
+	"E7 46	8E64",	#CJK UNIFIED IDEOGRAPH
+	"E7 47	8E60",	#CJK UNIFIED IDEOGRAPH
+	"E7 48	8E2A",	#CJK UNIFIED IDEOGRAPH
+	"E7 49	8E63",	#CJK UNIFIED IDEOGRAPH
+	"E7 4A	8E55",	#CJK UNIFIED IDEOGRAPH
+	"E7 4B	8E76",	#CJK UNIFIED IDEOGRAPH
+	"E7 4C	8E72",	#CJK UNIFIED IDEOGRAPH
+	"E7 4D	8E7C",	#CJK UNIFIED IDEOGRAPH
+	"E7 4E	8E81",	#CJK UNIFIED IDEOGRAPH
+	"E7 4F	8E87",	#CJK UNIFIED IDEOGRAPH
+	"E7 50	8E85",	#CJK UNIFIED IDEOGRAPH
+	"E7 51	8E84",	#CJK UNIFIED IDEOGRAPH
+	"E7 52	8E8B",	#CJK UNIFIED IDEOGRAPH
+	"E7 53	8E8A",	#CJK UNIFIED IDEOGRAPH
+	"E7 54	8E93",	#CJK UNIFIED IDEOGRAPH
+	"E7 55	8E91",	#CJK UNIFIED IDEOGRAPH
+	"E7 56	8E94",	#CJK UNIFIED IDEOGRAPH
+	"E7 57	8E99",	#CJK UNIFIED IDEOGRAPH
+	"E7 58	8EAA",	#CJK UNIFIED IDEOGRAPH
+	"E7 59	8EA1",	#CJK UNIFIED IDEOGRAPH
+	"E7 5A	8EAC",	#CJK UNIFIED IDEOGRAPH
+	"E7 5B	8EB0",	#CJK UNIFIED IDEOGRAPH
+	"E7 5C	8EC6",	#CJK UNIFIED IDEOGRAPH
+	"E7 5D	8EB1",	#CJK UNIFIED IDEOGRAPH
+	"E7 5E	8EBE",	#CJK UNIFIED IDEOGRAPH
+	"E7 5F	8EC5",	#CJK UNIFIED IDEOGRAPH
+	"E7 60	8EC8",	#CJK UNIFIED IDEOGRAPH
+	"E7 61	8ECB",	#CJK UNIFIED IDEOGRAPH
+	"E7 62	8EDB",	#CJK UNIFIED IDEOGRAPH
+	"E7 63	8EE3",	#CJK UNIFIED IDEOGRAPH
+	"E7 64	8EFC",	#CJK UNIFIED IDEOGRAPH
+	"E7 65	8EFB",	#CJK UNIFIED IDEOGRAPH
+	"E7 66	8EEB",	#CJK UNIFIED IDEOGRAPH
+	"E7 67	8EFE",	#CJK UNIFIED IDEOGRAPH
+	"E7 68	8F0A",	#CJK UNIFIED IDEOGRAPH
+	"E7 69	8F05",	#CJK UNIFIED IDEOGRAPH
+	"E7 6A	8F15",	#CJK UNIFIED IDEOGRAPH
+	"E7 6B	8F12",	#CJK UNIFIED IDEOGRAPH
+	"E7 6C	8F19",	#CJK UNIFIED IDEOGRAPH
+	"E7 6D	8F13",	#CJK UNIFIED IDEOGRAPH
+	"E7 6E	8F1C",	#CJK UNIFIED IDEOGRAPH
+	"E7 6F	8F1F",	#CJK UNIFIED IDEOGRAPH
+	"E7 70	8F1B",	#CJK UNIFIED IDEOGRAPH
+	"E7 71	8F0C",	#CJK UNIFIED IDEOGRAPH
+	"E7 72	8F26",	#CJK UNIFIED IDEOGRAPH
+	"E7 73	8F33",	#CJK UNIFIED IDEOGRAPH
+	"E7 74	8F3B",	#CJK UNIFIED IDEOGRAPH
+	"E7 75	8F39",	#CJK UNIFIED IDEOGRAPH
+	"E7 76	8F45",	#CJK UNIFIED IDEOGRAPH
+	"E7 77	8F42",	#CJK UNIFIED IDEOGRAPH
+	"E7 78	8F3E",	#CJK UNIFIED IDEOGRAPH
+	"E7 79	8F4C",	#CJK UNIFIED IDEOGRAPH
+	"E7 7A	8F49",	#CJK UNIFIED IDEOGRAPH
+	"E7 7B	8F46",	#CJK UNIFIED IDEOGRAPH
+	"E7 7C	8F4E",	#CJK UNIFIED IDEOGRAPH
+	"E7 7D	8F57",	#CJK UNIFIED IDEOGRAPH
+	"E7 7E	8F5C",	#CJK UNIFIED IDEOGRAPH
+	"E7 80	8F62",	#CJK UNIFIED IDEOGRAPH
+	"E7 81	8F63",	#CJK UNIFIED IDEOGRAPH
+	"E7 82	8F64",	#CJK UNIFIED IDEOGRAPH
+	"E7 83	8F9C",	#CJK UNIFIED IDEOGRAPH
+	"E7 84	8F9F",	#CJK UNIFIED IDEOGRAPH
+	"E7 85	8FA3",	#CJK UNIFIED IDEOGRAPH
+	"E7 86	8FAD",	#CJK UNIFIED IDEOGRAPH
+	"E7 87	8FAF",	#CJK UNIFIED IDEOGRAPH
+	"E7 88	8FB7",	#CJK UNIFIED IDEOGRAPH
+	"E7 89	8FDA",	#CJK UNIFIED IDEOGRAPH
+	"E7 8A	8FE5",	#CJK UNIFIED IDEOGRAPH
+	"E7 8B	8FE2",	#CJK UNIFIED IDEOGRAPH
+	"E7 8C	8FEA",	#CJK UNIFIED IDEOGRAPH
+	"E7 8D	8FEF",	#CJK UNIFIED IDEOGRAPH
+	"E7 8E	9087",	#CJK UNIFIED IDEOGRAPH
+	"E7 8F	8FF4",	#CJK UNIFIED IDEOGRAPH
+	"E7 90	9005",	#CJK UNIFIED IDEOGRAPH
+	"E7 91	8FF9",	#CJK UNIFIED IDEOGRAPH
+	"E7 92	8FFA",	#CJK UNIFIED IDEOGRAPH
+	"E7 93	9011",	#CJK UNIFIED IDEOGRAPH
+	"E7 94	9015",	#CJK UNIFIED IDEOGRAPH
+	"E7 95	9021",	#CJK UNIFIED IDEOGRAPH
+	"E7 96	900D",	#CJK UNIFIED IDEOGRAPH
+	"E7 97	901E",	#CJK UNIFIED IDEOGRAPH
+	"E7 98	9016",	#CJK UNIFIED IDEOGRAPH
+	"E7 99	900B",	#CJK UNIFIED IDEOGRAPH
+	"E7 9A	9027",	#CJK UNIFIED IDEOGRAPH
+	"E7 9B	9036",	#CJK UNIFIED IDEOGRAPH
+	"E7 9C	9035",	#CJK UNIFIED IDEOGRAPH
+	"E7 9D	9039",	#CJK UNIFIED IDEOGRAPH
+	"E7 9E	8FF8",	#CJK UNIFIED IDEOGRAPH
+	"E7 9F	904F",	#CJK UNIFIED IDEOGRAPH
+	"E7 A0	9050",	#CJK UNIFIED IDEOGRAPH
+	"E7 A1	9051",	#CJK UNIFIED IDEOGRAPH
+	"E7 A2	9052",	#CJK UNIFIED IDEOGRAPH
+	"E7 A3	900E",	#CJK UNIFIED IDEOGRAPH
+	"E7 A4	9049",	#CJK UNIFIED IDEOGRAPH
+	"E7 A5	903E",	#CJK UNIFIED IDEOGRAPH
+	"E7 A6	9056",	#CJK UNIFIED IDEOGRAPH
+	"E7 A7	9058",	#CJK UNIFIED IDEOGRAPH
+	"E7 A8	905E",	#CJK UNIFIED IDEOGRAPH
+	"E7 A9	9068",	#CJK UNIFIED IDEOGRAPH
+	"E7 AA	906F",	#CJK UNIFIED IDEOGRAPH
+	"E7 AB	9076",	#CJK UNIFIED IDEOGRAPH
+	"E7 AC	96A8",	#CJK UNIFIED IDEOGRAPH
+	"E7 AD	9072",	#CJK UNIFIED IDEOGRAPH
+	"E7 AE	9082",	#CJK UNIFIED IDEOGRAPH
+	"E7 AF	907D",	#CJK UNIFIED IDEOGRAPH
+	"E7 B0	9081",	#CJK UNIFIED IDEOGRAPH
+	"E7 B1	9080",	#CJK UNIFIED IDEOGRAPH
+	"E7 B2	908A",	#CJK UNIFIED IDEOGRAPH
+	"E7 B3	9089",	#CJK UNIFIED IDEOGRAPH
+	"E7 B4	908F",	#CJK UNIFIED IDEOGRAPH
+	"E7 B5	90A8",	#CJK UNIFIED IDEOGRAPH
+	"E7 B6	90AF",	#CJK UNIFIED IDEOGRAPH
+	"E7 B7	90B1",	#CJK UNIFIED IDEOGRAPH
+	"E7 B8	90B5",	#CJK UNIFIED IDEOGRAPH
+	"E7 B9	90E2",	#CJK UNIFIED IDEOGRAPH
+	"E7 BA	90E4",	#CJK UNIFIED IDEOGRAPH
+	"E7 BB	6248",	#CJK UNIFIED IDEOGRAPH
+	"E7 BC	90DB",	#CJK UNIFIED IDEOGRAPH
+	"E7 BD	9102",	#CJK UNIFIED IDEOGRAPH
+	"E7 BE	9112",	#CJK UNIFIED IDEOGRAPH
+	"E7 BF	9119",	#CJK UNIFIED IDEOGRAPH
+	"E7 C0	9132",	#CJK UNIFIED IDEOGRAPH
+	"E7 C1	9130",	#CJK UNIFIED IDEOGRAPH
+	"E7 C2	914A",	#CJK UNIFIED IDEOGRAPH
+	"E7 C3	9156",	#CJK UNIFIED IDEOGRAPH
+	"E7 C4	9158",	#CJK UNIFIED IDEOGRAPH
+	"E7 C5	9163",	#CJK UNIFIED IDEOGRAPH
+	"E7 C6	9165",	#CJK UNIFIED IDEOGRAPH
+	"E7 C7	9169",	#CJK UNIFIED IDEOGRAPH
+	"E7 C8	9173",	#CJK UNIFIED IDEOGRAPH
+	"E7 C9	9172",	#CJK UNIFIED IDEOGRAPH
+	"E7 CA	918B",	#CJK UNIFIED IDEOGRAPH
+	"E7 CB	9189",	#CJK UNIFIED IDEOGRAPH
+	"E7 CC	9182",	#CJK UNIFIED IDEOGRAPH
+	"E7 CD	91A2",	#CJK UNIFIED IDEOGRAPH
+	"E7 CE	91AB",	#CJK UNIFIED IDEOGRAPH
+	"E7 CF	91AF",	#CJK UNIFIED IDEOGRAPH
+	"E7 D0	91AA",	#CJK UNIFIED IDEOGRAPH
+	"E7 D1	91B5",	#CJK UNIFIED IDEOGRAPH
+	"E7 D2	91B4",	#CJK UNIFIED IDEOGRAPH
+	"E7 D3	91BA",	#CJK UNIFIED IDEOGRAPH
+	"E7 D4	91C0",	#CJK UNIFIED IDEOGRAPH
+	"E7 D5	91C1",	#CJK UNIFIED IDEOGRAPH
+	"E7 D6	91C9",	#CJK UNIFIED IDEOGRAPH
+	"E7 D7	91CB",	#CJK UNIFIED IDEOGRAPH
+	"E7 D8	91D0",	#CJK UNIFIED IDEOGRAPH
+	"E7 D9	91D6",	#CJK UNIFIED IDEOGRAPH
+	"E7 DA	91DF",	#CJK UNIFIED IDEOGRAPH
+	"E7 DB	91E1",	#CJK UNIFIED IDEOGRAPH
+	"E7 DC	91DB",	#CJK UNIFIED IDEOGRAPH
+	"E7 DD	91FC",	#CJK UNIFIED IDEOGRAPH
+	"E7 DE	91F5",	#CJK UNIFIED IDEOGRAPH
+	"E7 DF	91F6",	#CJK UNIFIED IDEOGRAPH
+	"E7 E0	921E",	#CJK UNIFIED IDEOGRAPH
+	"E7 E1	91FF",	#CJK UNIFIED IDEOGRAPH
+	"E7 E2	9214",	#CJK UNIFIED IDEOGRAPH
+	"E7 E3	922C",	#CJK UNIFIED IDEOGRAPH
+	"E7 E4	9215",	#CJK UNIFIED IDEOGRAPH
+	"E7 E5	9211",	#CJK UNIFIED IDEOGRAPH
+	"E7 E6	925E",	#CJK UNIFIED IDEOGRAPH
+	"E7 E7	9257",	#CJK UNIFIED IDEOGRAPH
+	"E7 E8	9245",	#CJK UNIFIED IDEOGRAPH
+	"E7 E9	9249",	#CJK UNIFIED IDEOGRAPH
+	"E7 EA	9264",	#CJK UNIFIED IDEOGRAPH
+	"E7 EB	9248",	#CJK UNIFIED IDEOGRAPH
+	"E7 EC	9295",	#CJK UNIFIED IDEOGRAPH
+	"E7 ED	923F",	#CJK UNIFIED IDEOGRAPH
+	"E7 EE	924B",	#CJK UNIFIED IDEOGRAPH
+	"E7 EF	9250",	#CJK UNIFIED IDEOGRAPH
+	"E7 F0	929C",	#CJK UNIFIED IDEOGRAPH
+	"E7 F1	9296",	#CJK UNIFIED IDEOGRAPH
+	"E7 F2	9293",	#CJK UNIFIED IDEOGRAPH
+	"E7 F3	929B",	#CJK UNIFIED IDEOGRAPH
+	"E7 F4	925A",	#CJK UNIFIED IDEOGRAPH
+	"E7 F5	92CF",	#CJK UNIFIED IDEOGRAPH
+	"E7 F6	92B9",	#CJK UNIFIED IDEOGRAPH
+	"E7 F7	92B7",	#CJK UNIFIED IDEOGRAPH
+	"E7 F8	92E9",	#CJK UNIFIED IDEOGRAPH
+	"E7 F9	930F",	#CJK UNIFIED IDEOGRAPH
+	"E7 FA	92FA",	#CJK UNIFIED IDEOGRAPH
+	"E7 FB	9344",	#CJK UNIFIED IDEOGRAPH
+	"E7 FC	932E",	#CJK UNIFIED IDEOGRAPH
+	"E8 40	9319",	#CJK UNIFIED IDEOGRAPH
+	"E8 41	9322",	#CJK UNIFIED IDEOGRAPH
+	"E8 42	931A",	#CJK UNIFIED IDEOGRAPH
+	"E8 43	9323",	#CJK UNIFIED IDEOGRAPH
+	"E8 44	933A",	#CJK UNIFIED IDEOGRAPH
+	"E8 45	9335",	#CJK UNIFIED IDEOGRAPH
+	"E8 46	933B",	#CJK UNIFIED IDEOGRAPH
+	"E8 47	935C",	#CJK UNIFIED IDEOGRAPH
+	"E8 48	9360",	#CJK UNIFIED IDEOGRAPH
+	"E8 49	937C",	#CJK UNIFIED IDEOGRAPH
+	"E8 4A	936E",	#CJK UNIFIED IDEOGRAPH
+	"E8 4B	9356",	#CJK UNIFIED IDEOGRAPH
+	"E8 4C	93B0",	#CJK UNIFIED IDEOGRAPH
+	"E8 4D	93AC",	#CJK UNIFIED IDEOGRAPH
+	"E8 4E	93AD",	#CJK UNIFIED IDEOGRAPH
+	"E8 4F	9394",	#CJK UNIFIED IDEOGRAPH
+	"E8 50	93B9",	#CJK UNIFIED IDEOGRAPH
+	"E8 51	93D6",	#CJK UNIFIED IDEOGRAPH
+	"E8 52	93D7",	#CJK UNIFIED IDEOGRAPH
+	"E8 53	93E8",	#CJK UNIFIED IDEOGRAPH
+	"E8 54	93E5",	#CJK UNIFIED IDEOGRAPH
+	"E8 55	93D8",	#CJK UNIFIED IDEOGRAPH
+	"E8 56	93C3",	#CJK UNIFIED IDEOGRAPH
+	"E8 57	93DD",	#CJK UNIFIED IDEOGRAPH
+	"E8 58	93D0",	#CJK UNIFIED IDEOGRAPH
+	"E8 59	93C8",	#CJK UNIFIED IDEOGRAPH
+	"E8 5A	93E4",	#CJK UNIFIED IDEOGRAPH
+	"E8 5B	941A",	#CJK UNIFIED IDEOGRAPH
+	"E8 5C	9414",	#CJK UNIFIED IDEOGRAPH
+	"E8 5D	9413",	#CJK UNIFIED IDEOGRAPH
+	"E8 5E	9403",	#CJK UNIFIED IDEOGRAPH
+	"E8 5F	9407",	#CJK UNIFIED IDEOGRAPH
+	"E8 60	9410",	#CJK UNIFIED IDEOGRAPH
+	"E8 61	9436",	#CJK UNIFIED IDEOGRAPH
+	"E8 62	942B",	#CJK UNIFIED IDEOGRAPH
+	"E8 63	9435",	#CJK UNIFIED IDEOGRAPH
+	"E8 64	9421",	#CJK UNIFIED IDEOGRAPH
+	"E8 65	943A",	#CJK UNIFIED IDEOGRAPH
+	"E8 66	9441",	#CJK UNIFIED IDEOGRAPH
+	"E8 67	9452",	#CJK UNIFIED IDEOGRAPH
+	"E8 68	9444",	#CJK UNIFIED IDEOGRAPH
+	"E8 69	945B",	#CJK UNIFIED IDEOGRAPH
+	"E8 6A	9460",	#CJK UNIFIED IDEOGRAPH
+	"E8 6B	9462",	#CJK UNIFIED IDEOGRAPH
+	"E8 6C	945E",	#CJK UNIFIED IDEOGRAPH
+	"E8 6D	946A",	#CJK UNIFIED IDEOGRAPH
+	"E8 6E	9229",	#CJK UNIFIED IDEOGRAPH
+	"E8 6F	9470",	#CJK UNIFIED IDEOGRAPH
+	"E8 70	9475",	#CJK UNIFIED IDEOGRAPH
+	"E8 71	9477",	#CJK UNIFIED IDEOGRAPH
+	"E8 72	947D",	#CJK UNIFIED IDEOGRAPH
+	"E8 73	945A",	#CJK UNIFIED IDEOGRAPH
+	"E8 74	947C",	#CJK UNIFIED IDEOGRAPH
+	"E8 75	947E",	#CJK UNIFIED IDEOGRAPH
+	"E8 76	9481",	#CJK UNIFIED IDEOGRAPH
+	"E8 77	947F",	#CJK UNIFIED IDEOGRAPH
+	"E8 78	9582",	#CJK UNIFIED IDEOGRAPH
+	"E8 79	9587",	#CJK UNIFIED IDEOGRAPH
+	"E8 7A	958A",	#CJK UNIFIED IDEOGRAPH
+	"E8 7B	9594",	#CJK UNIFIED IDEOGRAPH
+	"E8 7C	9596",	#CJK UNIFIED IDEOGRAPH
+	"E8 7D	9598",	#CJK UNIFIED IDEOGRAPH
+	"E8 7E	9599",	#CJK UNIFIED IDEOGRAPH
+	"E8 80	95A0",	#CJK UNIFIED IDEOGRAPH
+	"E8 81	95A8",	#CJK UNIFIED IDEOGRAPH
+	"E8 82	95A7",	#CJK UNIFIED IDEOGRAPH
+	"E8 83	95AD",	#CJK UNIFIED IDEOGRAPH
+	"E8 84	95BC",	#CJK UNIFIED IDEOGRAPH
+	"E8 85	95BB",	#CJK UNIFIED IDEOGRAPH
+	"E8 86	95B9",	#CJK UNIFIED IDEOGRAPH
+	"E8 87	95BE",	#CJK UNIFIED IDEOGRAPH
+	"E8 88	95CA",	#CJK UNIFIED IDEOGRAPH
+	"E8 89	6FF6",	#CJK UNIFIED IDEOGRAPH
+	"E8 8A	95C3",	#CJK UNIFIED IDEOGRAPH
+	"E8 8B	95CD",	#CJK UNIFIED IDEOGRAPH
+	"E8 8C	95CC",	#CJK UNIFIED IDEOGRAPH
+	"E8 8D	95D5",	#CJK UNIFIED IDEOGRAPH
+	"E8 8E	95D4",	#CJK UNIFIED IDEOGRAPH
+	"E8 8F	95D6",	#CJK UNIFIED IDEOGRAPH
+	"E8 90	95DC",	#CJK UNIFIED IDEOGRAPH
+	"E8 91	95E1",	#CJK UNIFIED IDEOGRAPH
+	"E8 92	95E5",	#CJK UNIFIED IDEOGRAPH
+	"E8 93	95E2",	#CJK UNIFIED IDEOGRAPH
+	"E8 94	9621",	#CJK UNIFIED IDEOGRAPH
+	"E8 95	9628",	#CJK UNIFIED IDEOGRAPH
+	"E8 96	962E",	#CJK UNIFIED IDEOGRAPH
+	"E8 97	962F",	#CJK UNIFIED IDEOGRAPH
+	"E8 98	9642",	#CJK UNIFIED IDEOGRAPH
+	"E8 99	964C",	#CJK UNIFIED IDEOGRAPH
+	"E8 9A	964F",	#CJK UNIFIED IDEOGRAPH
+	"E8 9B	964B",	#CJK UNIFIED IDEOGRAPH
+	"E8 9C	9677",	#CJK UNIFIED IDEOGRAPH
+	"E8 9D	965C",	#CJK UNIFIED IDEOGRAPH
+	"E8 9E	965E",	#CJK UNIFIED IDEOGRAPH
+	"E8 9F	965D",	#CJK UNIFIED IDEOGRAPH
+	"E8 A0	965F",	#CJK UNIFIED IDEOGRAPH
+	"E8 A1	9666",	#CJK UNIFIED IDEOGRAPH
+	"E8 A2	9672",	#CJK UNIFIED IDEOGRAPH
+	"E8 A3	966C",	#CJK UNIFIED IDEOGRAPH
+	"E8 A4	968D",	#CJK UNIFIED IDEOGRAPH
+	"E8 A5	9698",	#CJK UNIFIED IDEOGRAPH
+	"E8 A6	9695",	#CJK UNIFIED IDEOGRAPH
+	"E8 A7	9697",	#CJK UNIFIED IDEOGRAPH
+	"E8 A8	96AA",	#CJK UNIFIED IDEOGRAPH
+	"E8 A9	96A7",	#CJK UNIFIED IDEOGRAPH
+	"E8 AA	96B1",	#CJK UNIFIED IDEOGRAPH
+	"E8 AB	96B2",	#CJK UNIFIED IDEOGRAPH
+	"E8 AC	96B0",	#CJK UNIFIED IDEOGRAPH
+	"E8 AD	96B4",	#CJK UNIFIED IDEOGRAPH
+	"E8 AE	96B6",	#CJK UNIFIED IDEOGRAPH
+	"E8 AF	96B8",	#CJK UNIFIED IDEOGRAPH
+	"E8 B0	96B9",	#CJK UNIFIED IDEOGRAPH
+	"E8 B1	96CE",	#CJK UNIFIED IDEOGRAPH
+	"E8 B2	96CB",	#CJK UNIFIED IDEOGRAPH
+	"E8 B3	96C9",	#CJK UNIFIED IDEOGRAPH
+	"E8 B4	96CD",	#CJK UNIFIED IDEOGRAPH
+	"E8 B5	894D",	#CJK UNIFIED IDEOGRAPH
+	"E8 B6	96DC",	#CJK UNIFIED IDEOGRAPH
+	"E8 B7	970D",	#CJK UNIFIED IDEOGRAPH
+	"E8 B8	96D5",	#CJK UNIFIED IDEOGRAPH
+	"E8 B9	96F9",	#CJK UNIFIED IDEOGRAPH
+	"E8 BA	9704",	#CJK UNIFIED IDEOGRAPH
+	"E8 BB	9706",	#CJK UNIFIED IDEOGRAPH
+	"E8 BC	9708",	#CJK UNIFIED IDEOGRAPH
+	"E8 BD	9713",	#CJK UNIFIED IDEOGRAPH
+	"E8 BE	970E",	#CJK UNIFIED IDEOGRAPH
+	"E8 BF	9711",	#CJK UNIFIED IDEOGRAPH
+	"E8 C0	970F",	#CJK UNIFIED IDEOGRAPH
+	"E8 C1	9716",	#CJK UNIFIED IDEOGRAPH
+	"E8 C2	9719",	#CJK UNIFIED IDEOGRAPH
+	"E8 C3	9724",	#CJK UNIFIED IDEOGRAPH
+	"E8 C4	972A",	#CJK UNIFIED IDEOGRAPH
+	"E8 C5	9730",	#CJK UNIFIED IDEOGRAPH
+	"E8 C6	9739",	#CJK UNIFIED IDEOGRAPH
+	"E8 C7	973D",	#CJK UNIFIED IDEOGRAPH
+	"E8 C8	973E",	#CJK UNIFIED IDEOGRAPH
+	"E8 C9	9744",	#CJK UNIFIED IDEOGRAPH
+	"E8 CA	9746",	#CJK UNIFIED IDEOGRAPH
+	"E8 CB	9748",	#CJK UNIFIED IDEOGRAPH
+	"E8 CC	9742",	#CJK UNIFIED IDEOGRAPH
+	"E8 CD	9749",	#CJK UNIFIED IDEOGRAPH
+	"E8 CE	975C",	#CJK UNIFIED IDEOGRAPH
+	"E8 CF	9760",	#CJK UNIFIED IDEOGRAPH
+	"E8 D0	9764",	#CJK UNIFIED IDEOGRAPH
+	"E8 D1	9766",	#CJK UNIFIED IDEOGRAPH
+	"E8 D2	9768",	#CJK UNIFIED IDEOGRAPH
+	"E8 D3	52D2",	#CJK UNIFIED IDEOGRAPH
+	"E8 D4	976B",	#CJK UNIFIED IDEOGRAPH
+	"E8 D5	9771",	#CJK UNIFIED IDEOGRAPH
+	"E8 D6	9779",	#CJK UNIFIED IDEOGRAPH
+	"E8 D7	9785",	#CJK UNIFIED IDEOGRAPH
+	"E8 D8	977C",	#CJK UNIFIED IDEOGRAPH
+	"E8 D9	9781",	#CJK UNIFIED IDEOGRAPH
+	"E8 DA	977A",	#CJK UNIFIED IDEOGRAPH
+	"E8 DB	9786",	#CJK UNIFIED IDEOGRAPH
+	"E8 DC	978B",	#CJK UNIFIED IDEOGRAPH
+	"E8 DD	978F",	#CJK UNIFIED IDEOGRAPH
+	"E8 DE	9790",	#CJK UNIFIED IDEOGRAPH
+	"E8 DF	979C",	#CJK UNIFIED IDEOGRAPH
+	"E8 E0	97A8",	#CJK UNIFIED IDEOGRAPH
+	"E8 E1	97A6",	#CJK UNIFIED IDEOGRAPH
+	"E8 E2	97A3",	#CJK UNIFIED IDEOGRAPH
+	"E8 E3	97B3",	#CJK UNIFIED IDEOGRAPH
+	"E8 E4	97B4",	#CJK UNIFIED IDEOGRAPH
+	"E8 E5	97C3",	#CJK UNIFIED IDEOGRAPH
+	"E8 E6	97C6",	#CJK UNIFIED IDEOGRAPH
+	"E8 E7	97C8",	#CJK UNIFIED IDEOGRAPH
+	"E8 E8	97CB",	#CJK UNIFIED IDEOGRAPH
+	"E8 E9	97DC",	#CJK UNIFIED IDEOGRAPH
+	"E8 EA	97ED",	#CJK UNIFIED IDEOGRAPH
+	"E8 EB	9F4F",	#CJK UNIFIED IDEOGRAPH
+	"E8 EC	97F2",	#CJK UNIFIED IDEOGRAPH
+	"E8 ED	7ADF",	#CJK UNIFIED IDEOGRAPH
+	"E8 EE	97F6",	#CJK UNIFIED IDEOGRAPH
+	"E8 EF	97F5",	#CJK UNIFIED IDEOGRAPH
+	"E8 F0	980F",	#CJK UNIFIED IDEOGRAPH
+	"E8 F1	980C",	#CJK UNIFIED IDEOGRAPH
+	"E8 F2	9838",	#CJK UNIFIED IDEOGRAPH
+	"E8 F3	9824",	#CJK UNIFIED IDEOGRAPH
+	"E8 F4	9821",	#CJK UNIFIED IDEOGRAPH
+	"E8 F5	9837",	#CJK UNIFIED IDEOGRAPH
+	"E8 F6	983D",	#CJK UNIFIED IDEOGRAPH
+	"E8 F7	9846",	#CJK UNIFIED IDEOGRAPH
+	"E8 F8	984F",	#CJK UNIFIED IDEOGRAPH
+	"E8 F9	984B",	#CJK UNIFIED IDEOGRAPH
+	"E8 FA	986B",	#CJK UNIFIED IDEOGRAPH
+	"E8 FB	986F",	#CJK UNIFIED IDEOGRAPH
+	"E8 FC	9870",	#CJK UNIFIED IDEOGRAPH
+	"E9 40	9871",	#CJK UNIFIED IDEOGRAPH
+	"E9 41	9874",	#CJK UNIFIED IDEOGRAPH
+	"E9 42	9873",	#CJK UNIFIED IDEOGRAPH
+	"E9 43	98AA",	#CJK UNIFIED IDEOGRAPH
+	"E9 44	98AF",	#CJK UNIFIED IDEOGRAPH
+	"E9 45	98B1",	#CJK UNIFIED IDEOGRAPH
+	"E9 46	98B6",	#CJK UNIFIED IDEOGRAPH
+	"E9 47	98C4",	#CJK UNIFIED IDEOGRAPH
+	"E9 48	98C3",	#CJK UNIFIED IDEOGRAPH
+	"E9 49	98C6",	#CJK UNIFIED IDEOGRAPH
+	"E9 4A	98E9",	#CJK UNIFIED IDEOGRAPH
+	"E9 4B	98EB",	#CJK UNIFIED IDEOGRAPH
+	"E9 4C	9903",	#CJK UNIFIED IDEOGRAPH
+	"E9 4D	9909",	#CJK UNIFIED IDEOGRAPH
+	"E9 4E	9912",	#CJK UNIFIED IDEOGRAPH
+	"E9 4F	9914",	#CJK UNIFIED IDEOGRAPH
+	"E9 50	9918",	#CJK UNIFIED IDEOGRAPH
+	"E9 51	9921",	#CJK UNIFIED IDEOGRAPH
+	"E9 52	991D",	#CJK UNIFIED IDEOGRAPH
+	"E9 53	991E",	#CJK UNIFIED IDEOGRAPH
+	"E9 54	9924",	#CJK UNIFIED IDEOGRAPH
+	"E9 55	9920",	#CJK UNIFIED IDEOGRAPH
+	"E9 56	992C",	#CJK UNIFIED IDEOGRAPH
+	"E9 57	992E",	#CJK UNIFIED IDEOGRAPH
+	"E9 58	993D",	#CJK UNIFIED IDEOGRAPH
+	"E9 59	993E",	#CJK UNIFIED IDEOGRAPH
+	"E9 5A	9942",	#CJK UNIFIED IDEOGRAPH
+	"E9 5B	9949",	#CJK UNIFIED IDEOGRAPH
+	"E9 5C	9945",	#CJK UNIFIED IDEOGRAPH
+	"E9 5D	9950",	#CJK UNIFIED IDEOGRAPH
+	"E9 5E	994B",	#CJK UNIFIED IDEOGRAPH
+	"E9 5F	9951",	#CJK UNIFIED IDEOGRAPH
+	"E9 60	9952",	#CJK UNIFIED IDEOGRAPH
+	"E9 61	994C",	#CJK UNIFIED IDEOGRAPH
+	"E9 62	9955",	#CJK UNIFIED IDEOGRAPH
+	"E9 63	9997",	#CJK UNIFIED IDEOGRAPH
+	"E9 64	9998",	#CJK UNIFIED IDEOGRAPH
+	"E9 65	99A5",	#CJK UNIFIED IDEOGRAPH
+	"E9 66	99AD",	#CJK UNIFIED IDEOGRAPH
+	"E9 67	99AE",	#CJK UNIFIED IDEOGRAPH
+	"E9 68	99BC",	#CJK UNIFIED IDEOGRAPH
+	"E9 69	99DF",	#CJK UNIFIED IDEOGRAPH
+	"E9 6A	99DB",	#CJK UNIFIED IDEOGRAPH
+	"E9 6B	99DD",	#CJK UNIFIED IDEOGRAPH
+	"E9 6C	99D8",	#CJK UNIFIED IDEOGRAPH
+	"E9 6D	99D1",	#CJK UNIFIED IDEOGRAPH
+	"E9 6E	99ED",	#CJK UNIFIED IDEOGRAPH
+	"E9 6F	99EE",	#CJK UNIFIED IDEOGRAPH
+	"E9 70	99F1",	#CJK UNIFIED IDEOGRAPH
+	"E9 71	99F2",	#CJK UNIFIED IDEOGRAPH
+	"E9 72	99FB",	#CJK UNIFIED IDEOGRAPH
+	"E9 73	99F8",	#CJK UNIFIED IDEOGRAPH
+	"E9 74	9A01",	#CJK UNIFIED IDEOGRAPH
+	"E9 75	9A0F",	#CJK UNIFIED IDEOGRAPH
+	"E9 76	9A05",	#CJK UNIFIED IDEOGRAPH
+	"E9 77	99E2",	#CJK UNIFIED IDEOGRAPH
+	"E9 78	9A19",	#CJK UNIFIED IDEOGRAPH
+	"E9 79	9A2B",	#CJK UNIFIED IDEOGRAPH
+	"E9 7A	9A37",	#CJK UNIFIED IDEOGRAPH
+	"E9 7B	9A45",	#CJK UNIFIED IDEOGRAPH
+	"E9 7C	9A42",	#CJK UNIFIED IDEOGRAPH
+	"E9 7D	9A40",	#CJK UNIFIED IDEOGRAPH
+	"E9 7E	9A43",	#CJK UNIFIED IDEOGRAPH
+	"E9 80	9A3E",	#CJK UNIFIED IDEOGRAPH
+	"E9 81	9A55",	#CJK UNIFIED IDEOGRAPH
+	"E9 82	9A4D",	#CJK UNIFIED IDEOGRAPH
+	"E9 83	9A5B",	#CJK UNIFIED IDEOGRAPH
+	"E9 84	9A57",	#CJK UNIFIED IDEOGRAPH
+	"E9 85	9A5F",	#CJK UNIFIED IDEOGRAPH
+	"E9 86	9A62",	#CJK UNIFIED IDEOGRAPH
+	"E9 87	9A65",	#CJK UNIFIED IDEOGRAPH
+	"E9 88	9A64",	#CJK UNIFIED IDEOGRAPH
+	"E9 89	9A69",	#CJK UNIFIED IDEOGRAPH
+	"E9 8A	9A6B",	#CJK UNIFIED IDEOGRAPH
+	"E9 8B	9A6A",	#CJK UNIFIED IDEOGRAPH
+	"E9 8C	9AAD",	#CJK UNIFIED IDEOGRAPH
+	"E9 8D	9AB0",	#CJK UNIFIED IDEOGRAPH
+	"E9 8E	9ABC",	#CJK UNIFIED IDEOGRAPH
+	"E9 8F	9AC0",	#CJK UNIFIED IDEOGRAPH
+	"E9 90	9ACF",	#CJK UNIFIED IDEOGRAPH
+	"E9 91	9AD1",	#CJK UNIFIED IDEOGRAPH
+	"E9 92	9AD3",	#CJK UNIFIED IDEOGRAPH
+	"E9 93	9AD4",	#CJK UNIFIED IDEOGRAPH
+	"E9 94	9ADE",	#CJK UNIFIED IDEOGRAPH
+	"E9 95	9ADF",	#CJK UNIFIED IDEOGRAPH
+	"E9 96	9AE2",	#CJK UNIFIED IDEOGRAPH
+	"E9 97	9AE3",	#CJK UNIFIED IDEOGRAPH
+	"E9 98	9AE6",	#CJK UNIFIED IDEOGRAPH
+	"E9 99	9AEF",	#CJK UNIFIED IDEOGRAPH
+	"E9 9A	9AEB",	#CJK UNIFIED IDEOGRAPH
+	"E9 9B	9AEE",	#CJK UNIFIED IDEOGRAPH
+	"E9 9C	9AF4",	#CJK UNIFIED IDEOGRAPH
+	"E9 9D	9AF1",	#CJK UNIFIED IDEOGRAPH
+	"E9 9E	9AF7",	#CJK UNIFIED IDEOGRAPH
+	"E9 9F	9AFB",	#CJK UNIFIED IDEOGRAPH
+	"E9 A0	9B06",	#CJK UNIFIED IDEOGRAPH
+	"E9 A1	9B18",	#CJK UNIFIED IDEOGRAPH
+	"E9 A2	9B1A",	#CJK UNIFIED IDEOGRAPH
+	"E9 A3	9B1F",	#CJK UNIFIED IDEOGRAPH
+	"E9 A4	9B22",	#CJK UNIFIED IDEOGRAPH
+	"E9 A5	9B23",	#CJK UNIFIED IDEOGRAPH
+	"E9 A6	9B25",	#CJK UNIFIED IDEOGRAPH
+	"E9 A7	9B27",	#CJK UNIFIED IDEOGRAPH
+	"E9 A8	9B28",	#CJK UNIFIED IDEOGRAPH
+	"E9 A9	9B29",	#CJK UNIFIED IDEOGRAPH
+	"E9 AA	9B2A",	#CJK UNIFIED IDEOGRAPH
+	"E9 AB	9B2E",	#CJK UNIFIED IDEOGRAPH
+	"E9 AC	9B2F",	#CJK UNIFIED IDEOGRAPH
+	"E9 AD	9B32",	#CJK UNIFIED IDEOGRAPH
+	"E9 AE	9B44",	#CJK UNIFIED IDEOGRAPH
+	"E9 AF	9B43",	#CJK UNIFIED IDEOGRAPH
+	"E9 B0	9B4F",	#CJK UNIFIED IDEOGRAPH
+	"E9 B1	9B4D",	#CJK UNIFIED IDEOGRAPH
+	"E9 B2	9B4E",	#CJK UNIFIED IDEOGRAPH
+	"E9 B3	9B51",	#CJK UNIFIED IDEOGRAPH
+	"E9 B4	9B58",	#CJK UNIFIED IDEOGRAPH
+	"E9 B5	9B74",	#CJK UNIFIED IDEOGRAPH
+	"E9 B6	9B93",	#CJK UNIFIED IDEOGRAPH
+	"E9 B7	9B83",	#CJK UNIFIED IDEOGRAPH
+	"E9 B8	9B91",	#CJK UNIFIED IDEOGRAPH
+	"E9 B9	9B96",	#CJK UNIFIED IDEOGRAPH
+	"E9 BA	9B97",	#CJK UNIFIED IDEOGRAPH
+	"E9 BB	9B9F",	#CJK UNIFIED IDEOGRAPH
+	"E9 BC	9BA0",	#CJK UNIFIED IDEOGRAPH
+	"E9 BD	9BA8",	#CJK UNIFIED IDEOGRAPH
+	"E9 BE	9BB4",	#CJK UNIFIED IDEOGRAPH
+	"E9 BF	9BC0",	#CJK UNIFIED IDEOGRAPH
+	"E9 C0	9BCA",	#CJK UNIFIED IDEOGRAPH
+	"E9 C1	9BB9",	#CJK UNIFIED IDEOGRAPH
+	"E9 C2	9BC6",	#CJK UNIFIED IDEOGRAPH
+	"E9 C3	9BCF",	#CJK UNIFIED IDEOGRAPH
+	"E9 C4	9BD1",	#CJK UNIFIED IDEOGRAPH
+	"E9 C5	9BD2",	#CJK UNIFIED IDEOGRAPH
+	"E9 C6	9BE3",	#CJK UNIFIED IDEOGRAPH
+	"E9 C7	9BE2",	#CJK UNIFIED IDEOGRAPH
+	"E9 C8	9BE4",	#CJK UNIFIED IDEOGRAPH
+	"E9 C9	9BD4",	#CJK UNIFIED IDEOGRAPH
+	"E9 CA	9BE1",	#CJK UNIFIED IDEOGRAPH
+	"E9 CB	9C3A",	#CJK UNIFIED IDEOGRAPH
+	"E9 CC	9BF2",	#CJK UNIFIED IDEOGRAPH
+	"E9 CD	9BF1",	#CJK UNIFIED IDEOGRAPH
+	"E9 CE	9BF0",	#CJK UNIFIED IDEOGRAPH
+	"E9 CF	9C15",	#CJK UNIFIED IDEOGRAPH
+	"E9 D0	9C14",	#CJK UNIFIED IDEOGRAPH
+	"E9 D1	9C09",	#CJK UNIFIED IDEOGRAPH
+	"E9 D2	9C13",	#CJK UNIFIED IDEOGRAPH
+	"E9 D3	9C0C",	#CJK UNIFIED IDEOGRAPH
+	"E9 D4	9C06",	#CJK UNIFIED IDEOGRAPH
+	"E9 D5	9C08",	#CJK UNIFIED IDEOGRAPH
+	"E9 D6	9C12",	#CJK UNIFIED IDEOGRAPH
+	"E9 D7	9C0A",	#CJK UNIFIED IDEOGRAPH
+	"E9 D8	9C04",	#CJK UNIFIED IDEOGRAPH
+	"E9 D9	9C2E",	#CJK UNIFIED IDEOGRAPH
+	"E9 DA	9C1B",	#CJK UNIFIED IDEOGRAPH
+	"E9 DB	9C25",	#CJK UNIFIED IDEOGRAPH
+	"E9 DC	9C24",	#CJK UNIFIED IDEOGRAPH
+	"E9 DD	9C21",	#CJK UNIFIED IDEOGRAPH
+	"E9 DE	9C30",	#CJK UNIFIED IDEOGRAPH
+	"E9 DF	9C47",	#CJK UNIFIED IDEOGRAPH
+	"E9 E0	9C32",	#CJK UNIFIED IDEOGRAPH
+	"E9 E1	9C46",	#CJK UNIFIED IDEOGRAPH
+	"E9 E2	9C3E",	#CJK UNIFIED IDEOGRAPH
+	"E9 E3	9C5A",	#CJK UNIFIED IDEOGRAPH
+	"E9 E4	9C60",	#CJK UNIFIED IDEOGRAPH
+	"E9 E5	9C67",	#CJK UNIFIED IDEOGRAPH
+	"E9 E6	9C76",	#CJK UNIFIED IDEOGRAPH
+	"E9 E7	9C78",	#CJK UNIFIED IDEOGRAPH
+	"E9 E8	9CE7",	#CJK UNIFIED IDEOGRAPH
+	"E9 E9	9CEC",	#CJK UNIFIED IDEOGRAPH
+	"E9 EA	9CF0",	#CJK UNIFIED IDEOGRAPH
+	"E9 EB	9D09",	#CJK UNIFIED IDEOGRAPH
+	"E9 EC	9D08",	#CJK UNIFIED IDEOGRAPH
+	"E9 ED	9CEB",	#CJK UNIFIED IDEOGRAPH
+	"E9 EE	9D03",	#CJK UNIFIED IDEOGRAPH
+	"E9 EF	9D06",	#CJK UNIFIED IDEOGRAPH
+	"E9 F0	9D2A",	#CJK UNIFIED IDEOGRAPH
+	"E9 F1	9D26",	#CJK UNIFIED IDEOGRAPH
+	"E9 F2	9DAF",	#CJK UNIFIED IDEOGRAPH
+	"E9 F3	9D23",	#CJK UNIFIED IDEOGRAPH
+	"E9 F4	9D1F",	#CJK UNIFIED IDEOGRAPH
+	"E9 F5	9D44",	#CJK UNIFIED IDEOGRAPH
+	"E9 F6	9D15",	#CJK UNIFIED IDEOGRAPH
+	"E9 F7	9D12",	#CJK UNIFIED IDEOGRAPH
+	"E9 F8	9D41",	#CJK UNIFIED IDEOGRAPH
+	"E9 F9	9D3F",	#CJK UNIFIED IDEOGRAPH
+	"E9 FA	9D3E",	#CJK UNIFIED IDEOGRAPH
+	"E9 FB	9D46",	#CJK UNIFIED IDEOGRAPH
+	"E9 FC	9D48",	#CJK UNIFIED IDEOGRAPH
+	"EA 40	9D5D",	#CJK UNIFIED IDEOGRAPH
+	"EA 41	9D5E",	#CJK UNIFIED IDEOGRAPH
+	"EA 42	9D64",	#CJK UNIFIED IDEOGRAPH
+	"EA 43	9D51",	#CJK UNIFIED IDEOGRAPH
+	"EA 44	9D50",	#CJK UNIFIED IDEOGRAPH
+	"EA 45	9D59",	#CJK UNIFIED IDEOGRAPH
+	"EA 46	9D72",	#CJK UNIFIED IDEOGRAPH
+	"EA 47	9D89",	#CJK UNIFIED IDEOGRAPH
+	"EA 48	9D87",	#CJK UNIFIED IDEOGRAPH
+	"EA 49	9DAB",	#CJK UNIFIED IDEOGRAPH
+	"EA 4A	9D6F",	#CJK UNIFIED IDEOGRAPH
+	"EA 4B	9D7A",	#CJK UNIFIED IDEOGRAPH
+	"EA 4C	9D9A",	#CJK UNIFIED IDEOGRAPH
+	"EA 4D	9DA4",	#CJK UNIFIED IDEOGRAPH
+	"EA 4E	9DA9",	#CJK UNIFIED IDEOGRAPH
+	"EA 4F	9DB2",	#CJK UNIFIED IDEOGRAPH
+	"EA 50	9DC4",	#CJK UNIFIED IDEOGRAPH
+	"EA 51	9DC1",	#CJK UNIFIED IDEOGRAPH
+	"EA 52	9DBB",	#CJK UNIFIED IDEOGRAPH
+	"EA 53	9DB8",	#CJK UNIFIED IDEOGRAPH
+	"EA 54	9DBA",	#CJK UNIFIED IDEOGRAPH
+	"EA 55	9DC6",	#CJK UNIFIED IDEOGRAPH
+	"EA 56	9DCF",	#CJK UNIFIED IDEOGRAPH
+	"EA 57	9DC2",	#CJK UNIFIED IDEOGRAPH
+	"EA 58	9DD9",	#CJK UNIFIED IDEOGRAPH
+	"EA 59	9DD3",	#CJK UNIFIED IDEOGRAPH
+	"EA 5A	9DF8",	#CJK UNIFIED IDEOGRAPH
+	"EA 5B	9DE6",	#CJK UNIFIED IDEOGRAPH
+	"EA 5C	9DED",	#CJK UNIFIED IDEOGRAPH
+	"EA 5D	9DEF",	#CJK UNIFIED IDEOGRAPH
+	"EA 5E	9DFD",	#CJK UNIFIED IDEOGRAPH
+	"EA 5F	9E1A",	#CJK UNIFIED IDEOGRAPH
+	"EA 60	9E1B",	#CJK UNIFIED IDEOGRAPH
+	"EA 61	9E1E",	#CJK UNIFIED IDEOGRAPH
+	"EA 62	9E75",	#CJK UNIFIED IDEOGRAPH
+	"EA 63	9E79",	#CJK UNIFIED IDEOGRAPH
+	"EA 64	9E7D",	#CJK UNIFIED IDEOGRAPH
+	"EA 65	9E81",	#CJK UNIFIED IDEOGRAPH
+	"EA 66	9E88",	#CJK UNIFIED IDEOGRAPH
+	"EA 67	9E8B",	#CJK UNIFIED IDEOGRAPH
+	"EA 68	9E8C",	#CJK UNIFIED IDEOGRAPH
+	"EA 69	9E92",	#CJK UNIFIED IDEOGRAPH
+	"EA 6A	9E95",	#CJK UNIFIED IDEOGRAPH
+	"EA 6B	9E91",	#CJK UNIFIED IDEOGRAPH
+	"EA 6C	9E9D",	#CJK UNIFIED IDEOGRAPH
+	"EA 6D	9EA5",	#CJK UNIFIED IDEOGRAPH
+	"EA 6E	9EA9",	#CJK UNIFIED IDEOGRAPH
+	"EA 6F	9EB8",	#CJK UNIFIED IDEOGRAPH
+	"EA 70	9EAA",	#CJK UNIFIED IDEOGRAPH
+	"EA 71	9EAD",	#CJK UNIFIED IDEOGRAPH
+	"EA 72	9761",	#CJK UNIFIED IDEOGRAPH
+	"EA 73	9ECC",	#CJK UNIFIED IDEOGRAPH
+	"EA 74	9ECE",	#CJK UNIFIED IDEOGRAPH
+	"EA 75	9ECF",	#CJK UNIFIED IDEOGRAPH
+	"EA 76	9ED0",	#CJK UNIFIED IDEOGRAPH
+	"EA 77	9ED4",	#CJK UNIFIED IDEOGRAPH
+	"EA 78	9EDC",	#CJK UNIFIED IDEOGRAPH
+	"EA 79	9EDE",	#CJK UNIFIED IDEOGRAPH
+	"EA 7A	9EDD",	#CJK UNIFIED IDEOGRAPH
+	"EA 7B	9EE0",	#CJK UNIFIED IDEOGRAPH
+	"EA 7C	9EE5",	#CJK UNIFIED IDEOGRAPH
+	"EA 7D	9EE8",	#CJK UNIFIED IDEOGRAPH
+	"EA 7E	9EEF",	#CJK UNIFIED IDEOGRAPH
+	"EA 80	9EF4",	#CJK UNIFIED IDEOGRAPH
+	"EA 81	9EF6",	#CJK UNIFIED IDEOGRAPH
+	"EA 82	9EF7",	#CJK UNIFIED IDEOGRAPH
+	"EA 83	9EF9",	#CJK UNIFIED IDEOGRAPH
+	"EA 84	9EFB",	#CJK UNIFIED IDEOGRAPH
+	"EA 85	9EFC",	#CJK UNIFIED IDEOGRAPH
+	"EA 86	9EFD",	#CJK UNIFIED IDEOGRAPH
+	"EA 87	9F07",	#CJK UNIFIED IDEOGRAPH
+	"EA 88	9F08",	#CJK UNIFIED IDEOGRAPH
+	"EA 89	76B7",	#CJK UNIFIED IDEOGRAPH
+	"EA 8A	9F15",	#CJK UNIFIED IDEOGRAPH
+	"EA 8B	9F21",	#CJK UNIFIED IDEOGRAPH
+	"EA 8C	9F2C",	#CJK UNIFIED IDEOGRAPH
+	"EA 8D	9F3E",	#CJK UNIFIED IDEOGRAPH
+	"EA 8E	9F4A",	#CJK UNIFIED IDEOGRAPH
+	"EA 8F	9F52",	#CJK UNIFIED IDEOGRAPH
+	"EA 90	9F54",	#CJK UNIFIED IDEOGRAPH
+	"EA 91	9F63",	#CJK UNIFIED IDEOGRAPH
+	"EA 92	9F5F",	#CJK UNIFIED IDEOGRAPH
+	"EA 93	9F60",	#CJK UNIFIED IDEOGRAPH
+	"EA 94	9F61",	#CJK UNIFIED IDEOGRAPH
+	"EA 95	9F66",	#CJK UNIFIED IDEOGRAPH
+	"EA 96	9F67",	#CJK UNIFIED IDEOGRAPH
+	"EA 97	9F6C",	#CJK UNIFIED IDEOGRAPH
+	"EA 98	9F6A",	#CJK UNIFIED IDEOGRAPH
+	"EA 99	9F77",	#CJK UNIFIED IDEOGRAPH
+	"EA 9A	9F72",	#CJK UNIFIED IDEOGRAPH
+	"EA 9B	9F76",	#CJK UNIFIED IDEOGRAPH
+	"EA 9C	9F95",	#CJK UNIFIED IDEOGRAPH
+	"EA 9D	9F9C",	#CJK UNIFIED IDEOGRAPH
+	"EA 9E	9FA0",	#CJK UNIFIED IDEOGRAPH
+	"EA 9F	582F",	#CJK UNIFIED IDEOGRAPH
+	"EA A0	69C7",	#CJK UNIFIED IDEOGRAPH
+	"EA A1	9059",	#CJK UNIFIED IDEOGRAPH
+	"EA A2	7464",	#CJK UNIFIED IDEOGRAPH
+	"EA A3	51DC",	#CJK UNIFIED IDEOGRAPH
+	"EA A4	7199",	#CJK UNIFIED IDEOGRAPH
+	"ED 40	7E8A",	#CJK UNIFIED IDEOGRAPH
+	"ED 41	891C",	#CJK UNIFIED IDEOGRAPH
+	"ED 42	9348",	#CJK UNIFIED IDEOGRAPH
+	"ED 43	9288",	#CJK UNIFIED IDEOGRAPH
+	"ED 44	84DC",	#CJK UNIFIED IDEOGRAPH
+	"ED 45	4FC9",	#CJK UNIFIED IDEOGRAPH
+	"ED 46	70BB",	#CJK UNIFIED IDEOGRAPH
+	"ED 47	6631",	#CJK UNIFIED IDEOGRAPH
+	"ED 48	68C8",	#CJK UNIFIED IDEOGRAPH
+	"ED 49	92F9",	#CJK UNIFIED IDEOGRAPH
+	"ED 4A	66FB",	#CJK UNIFIED IDEOGRAPH
+	"ED 4B	5F45",	#CJK UNIFIED IDEOGRAPH
+	"ED 4C	4E28",	#CJK UNIFIED IDEOGRAPH
+	"ED 4D	4EE1",	#CJK UNIFIED IDEOGRAPH
+	"ED 4E	4EFC",	#CJK UNIFIED IDEOGRAPH
+	"ED 4F	4F00",	#CJK UNIFIED IDEOGRAPH
+	"ED 50	4F03",	#CJK UNIFIED IDEOGRAPH
+	"ED 51	4F39",	#CJK UNIFIED IDEOGRAPH
+	"ED 52	4F56",	#CJK UNIFIED IDEOGRAPH
+	"ED 53	4F92",	#CJK UNIFIED IDEOGRAPH
+	"ED 54	4F8A",	#CJK UNIFIED IDEOGRAPH
+	"ED 55	4F9A",	#CJK UNIFIED IDEOGRAPH
+	"ED 56	4F94",	#CJK UNIFIED IDEOGRAPH
+	"ED 57	4FCD",	#CJK UNIFIED IDEOGRAPH
+	"ED 58	5040",	#CJK UNIFIED IDEOGRAPH
+	"ED 59	5022",	#CJK UNIFIED IDEOGRAPH
+	"ED 5A	4FFF",	#CJK UNIFIED IDEOGRAPH
+	"ED 5B	501E",	#CJK UNIFIED IDEOGRAPH
+	"ED 5C	5046",	#CJK UNIFIED IDEOGRAPH
+	"ED 5D	5070",	#CJK UNIFIED IDEOGRAPH
+	"ED 5E	5042",	#CJK UNIFIED IDEOGRAPH
+	"ED 5F	5094",	#CJK UNIFIED IDEOGRAPH
+	"ED 60	50F4",	#CJK UNIFIED IDEOGRAPH
+	"ED 61	50D8",	#CJK UNIFIED IDEOGRAPH
+	"ED 62	514A",	#CJK UNIFIED IDEOGRAPH
+	"ED 63	5164",	#CJK UNIFIED IDEOGRAPH
+	"ED 64	519D",	#CJK UNIFIED IDEOGRAPH
+	"ED 65	51BE",	#CJK UNIFIED IDEOGRAPH
+	"ED 66	51EC",	#CJK UNIFIED IDEOGRAPH
+	"ED 67	5215",	#CJK UNIFIED IDEOGRAPH
+	"ED 68	529C",	#CJK UNIFIED IDEOGRAPH
+	"ED 69	52A6",	#CJK UNIFIED IDEOGRAPH
+	"ED 6A	52C0",	#CJK UNIFIED IDEOGRAPH
+	"ED 6B	52DB",	#CJK UNIFIED IDEOGRAPH
+	"ED 6C	5300",	#CJK UNIFIED IDEOGRAPH
+	"ED 6D	5307",	#CJK UNIFIED IDEOGRAPH
+	"ED 6E	5324",	#CJK UNIFIED IDEOGRAPH
+	"ED 6F	5372",	#CJK UNIFIED IDEOGRAPH
+	"ED 70	5393",	#CJK UNIFIED IDEOGRAPH
+	"ED 71	53B2",	#CJK UNIFIED IDEOGRAPH
+	"ED 72	53DD",	#CJK UNIFIED IDEOGRAPH
+	"ED 73	FA0E",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED 74	549C",	#CJK UNIFIED IDEOGRAPH
+	"ED 75	548A",	#CJK UNIFIED IDEOGRAPH
+	"ED 76	54A9",	#CJK UNIFIED IDEOGRAPH
+	"ED 77	54FF",	#CJK UNIFIED IDEOGRAPH
+	"ED 78	5586",	#CJK UNIFIED IDEOGRAPH
+	"ED 79	5759",	#CJK UNIFIED IDEOGRAPH
+	"ED 7A	5765",	#CJK UNIFIED IDEOGRAPH
+	"ED 7B	57AC",	#CJK UNIFIED IDEOGRAPH
+	"ED 7C	57C8",	#CJK UNIFIED IDEOGRAPH
+	"ED 7D	57C7",	#CJK UNIFIED IDEOGRAPH
+	"ED 7E	FA0F",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED 80	FA10",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED 81	589E",	#CJK UNIFIED IDEOGRAPH
+	"ED 82	58B2",	#CJK UNIFIED IDEOGRAPH
+	"ED 83	590B",	#CJK UNIFIED IDEOGRAPH
+	"ED 84	5953",	#CJK UNIFIED IDEOGRAPH
+	"ED 85	595B",	#CJK UNIFIED IDEOGRAPH
+	"ED 86	595D",	#CJK UNIFIED IDEOGRAPH
+	"ED 87	5963",	#CJK UNIFIED IDEOGRAPH
+	"ED 88	59A4",	#CJK UNIFIED IDEOGRAPH
+	"ED 89	59BA",	#CJK UNIFIED IDEOGRAPH
+	"ED 8A	5B56",	#CJK UNIFIED IDEOGRAPH
+	"ED 8B	5BC0",	#CJK UNIFIED IDEOGRAPH
+	"ED 8C	752F",	#CJK UNIFIED IDEOGRAPH
+	"ED 8D	5BD8",	#CJK UNIFIED IDEOGRAPH
+	"ED 8E	5BEC",	#CJK UNIFIED IDEOGRAPH
+	"ED 8F	5C1E",	#CJK UNIFIED IDEOGRAPH
+	"ED 90	5CA6",	#CJK UNIFIED IDEOGRAPH
+	"ED 91	5CBA",	#CJK UNIFIED IDEOGRAPH
+	"ED 92	5CF5",	#CJK UNIFIED IDEOGRAPH
+	"ED 93	5D27",	#CJK UNIFIED IDEOGRAPH
+	"ED 94	5D53",	#CJK UNIFIED IDEOGRAPH
+	"ED 95	FA11",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED 96	5D42",	#CJK UNIFIED IDEOGRAPH
+	"ED 97	5D6D",	#CJK UNIFIED IDEOGRAPH
+	"ED 98	5DB8",	#CJK UNIFIED IDEOGRAPH
+	"ED 99	5DB9",	#CJK UNIFIED IDEOGRAPH
+	"ED 9A	5DD0",	#CJK UNIFIED IDEOGRAPH
+	"ED 9B	5F21",	#CJK UNIFIED IDEOGRAPH
+	"ED 9C	5F34",	#CJK UNIFIED IDEOGRAPH
+	"ED 9D	5F67",	#CJK UNIFIED IDEOGRAPH
+	"ED 9E	5FB7",	#CJK UNIFIED IDEOGRAPH
+	"ED 9F	5FDE",	#CJK UNIFIED IDEOGRAPH
+	"ED A0	605D",	#CJK UNIFIED IDEOGRAPH
+	"ED A1	6085",	#CJK UNIFIED IDEOGRAPH
+	"ED A2	608A",	#CJK UNIFIED IDEOGRAPH
+	"ED A3	60DE",	#CJK UNIFIED IDEOGRAPH
+	"ED A4	60D5",	#CJK UNIFIED IDEOGRAPH
+	"ED A5	6120",	#CJK UNIFIED IDEOGRAPH
+	"ED A6	60F2",	#CJK UNIFIED IDEOGRAPH
+	"ED A7	6111",	#CJK UNIFIED IDEOGRAPH
+	"ED A8	6137",	#CJK UNIFIED IDEOGRAPH
+	"ED A9	6130",	#CJK UNIFIED IDEOGRAPH
+	"ED AA	6198",	#CJK UNIFIED IDEOGRAPH
+	"ED AB	6213",	#CJK UNIFIED IDEOGRAPH
+	"ED AC	62A6",	#CJK UNIFIED IDEOGRAPH
+	"ED AD	63F5",	#CJK UNIFIED IDEOGRAPH
+	"ED AE	6460",	#CJK UNIFIED IDEOGRAPH
+	"ED AF	649D",	#CJK UNIFIED IDEOGRAPH
+	"ED B0	64CE",	#CJK UNIFIED IDEOGRAPH
+	"ED B1	654E",	#CJK UNIFIED IDEOGRAPH
+	"ED B2	6600",	#CJK UNIFIED IDEOGRAPH
+	"ED B3	6615",	#CJK UNIFIED IDEOGRAPH
+	"ED B4	663B",	#CJK UNIFIED IDEOGRAPH
+	"ED B5	6609",	#CJK UNIFIED IDEOGRAPH
+	"ED B6	662E",	#CJK UNIFIED IDEOGRAPH
+	"ED B7	661E",	#CJK UNIFIED IDEOGRAPH
+	"ED B8	6624",	#CJK UNIFIED IDEOGRAPH
+	"ED B9	6665",	#CJK UNIFIED IDEOGRAPH
+	"ED BA	6657",	#CJK UNIFIED IDEOGRAPH
+	"ED BB	6659",	#CJK UNIFIED IDEOGRAPH
+	"ED BC	FA12",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED BD	6673",	#CJK UNIFIED IDEOGRAPH
+	"ED BE	6699",	#CJK UNIFIED IDEOGRAPH
+	"ED BF	66A0",	#CJK UNIFIED IDEOGRAPH
+	"ED C0	66B2",	#CJK UNIFIED IDEOGRAPH
+	"ED C1	66BF",	#CJK UNIFIED IDEOGRAPH
+	"ED C2	66FA",	#CJK UNIFIED IDEOGRAPH
+	"ED C3	670E",	#CJK UNIFIED IDEOGRAPH
+	"ED C4	F929",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED C5	6766",	#CJK UNIFIED IDEOGRAPH
+	"ED C6	67BB",	#CJK UNIFIED IDEOGRAPH
+	"ED C7	6852",	#CJK UNIFIED IDEOGRAPH
+	"ED C8	67C0",	#CJK UNIFIED IDEOGRAPH
+	"ED C9	6801",	#CJK UNIFIED IDEOGRAPH
+	"ED CA	6844",	#CJK UNIFIED IDEOGRAPH
+	"ED CB	68CF",	#CJK UNIFIED IDEOGRAPH
+	"ED CC	FA13",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED CD	6968",	#CJK UNIFIED IDEOGRAPH
+	"ED CE	FA14",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED CF	6998",	#CJK UNIFIED IDEOGRAPH
+	"ED D0	69E2",	#CJK UNIFIED IDEOGRAPH
+	"ED D1	6A30",	#CJK UNIFIED IDEOGRAPH
+	"ED D2	6A6B",	#CJK UNIFIED IDEOGRAPH
+	"ED D3	6A46",	#CJK UNIFIED IDEOGRAPH
+	"ED D4	6A73",	#CJK UNIFIED IDEOGRAPH
+	"ED D5	6A7E",	#CJK UNIFIED IDEOGRAPH
+	"ED D6	6AE2",	#CJK UNIFIED IDEOGRAPH
+	"ED D7	6AE4",	#CJK UNIFIED IDEOGRAPH
+	"ED D8	6BD6",	#CJK UNIFIED IDEOGRAPH
+	"ED D9	6C3F",	#CJK UNIFIED IDEOGRAPH
+	"ED DA	6C5C",	#CJK UNIFIED IDEOGRAPH
+	"ED DB	6C86",	#CJK UNIFIED IDEOGRAPH
+	"ED DC	6C6F",	#CJK UNIFIED IDEOGRAPH
+	"ED DD	6CDA",	#CJK UNIFIED IDEOGRAPH
+	"ED DE	6D04",	#CJK UNIFIED IDEOGRAPH
+	"ED DF	6D87",	#CJK UNIFIED IDEOGRAPH
+	"ED E0	6D6F",	#CJK UNIFIED IDEOGRAPH
+	"ED E1	6D96",	#CJK UNIFIED IDEOGRAPH
+	"ED E2	6DAC",	#CJK UNIFIED IDEOGRAPH
+	"ED E3	6DCF",	#CJK UNIFIED IDEOGRAPH
+	"ED E4	6DF8",	#CJK UNIFIED IDEOGRAPH
+	"ED E5	6DF2",	#CJK UNIFIED IDEOGRAPH
+	"ED E6	6DFC",	#CJK UNIFIED IDEOGRAPH
+	"ED E7	6E39",	#CJK UNIFIED IDEOGRAPH
+	"ED E8	6E5C",	#CJK UNIFIED IDEOGRAPH
+	"ED E9	6E27",	#CJK UNIFIED IDEOGRAPH
+	"ED EA	6E3C",	#CJK UNIFIED IDEOGRAPH
+	"ED EB	6EBF",	#CJK UNIFIED IDEOGRAPH
+	"ED EC	6F88",	#CJK UNIFIED IDEOGRAPH
+	"ED ED	6FB5",	#CJK UNIFIED IDEOGRAPH
+	"ED EE	6FF5",	#CJK UNIFIED IDEOGRAPH
+	"ED EF	7005",	#CJK UNIFIED IDEOGRAPH
+	"ED F0	7007",	#CJK UNIFIED IDEOGRAPH
+	"ED F1	7028",	#CJK UNIFIED IDEOGRAPH
+	"ED F2	7085",	#CJK UNIFIED IDEOGRAPH
+	"ED F3	70AB",	#CJK UNIFIED IDEOGRAPH
+	"ED F4	710F",	#CJK UNIFIED IDEOGRAPH
+	"ED F5	7104",	#CJK UNIFIED IDEOGRAPH
+	"ED F6	715C",	#CJK UNIFIED IDEOGRAPH
+	"ED F7	7146",	#CJK UNIFIED IDEOGRAPH
+	"ED F8	7147",	#CJK UNIFIED IDEOGRAPH
+	"ED F9	FA15",	#CJK COMPATIBILITY IDEOGRAPH
+	"ED FA	71C1",	#CJK UNIFIED IDEOGRAPH
+	"ED FB	71FE",	#CJK UNIFIED IDEOGRAPH
+	"ED FC	72B1",	#CJK UNIFIED IDEOGRAPH
+	"EE 40	72BE",	#CJK UNIFIED IDEOGRAPH
+	"EE 41	7324",	#CJK UNIFIED IDEOGRAPH
+	"EE 42	FA16",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 43	7377",	#CJK UNIFIED IDEOGRAPH
+	"EE 44	73BD",	#CJK UNIFIED IDEOGRAPH
+	"EE 45	73C9",	#CJK UNIFIED IDEOGRAPH
+	"EE 46	73D6",	#CJK UNIFIED IDEOGRAPH
+	"EE 47	73E3",	#CJK UNIFIED IDEOGRAPH
+	"EE 48	73D2",	#CJK UNIFIED IDEOGRAPH
+	"EE 49	7407",	#CJK UNIFIED IDEOGRAPH
+	"EE 4A	73F5",	#CJK UNIFIED IDEOGRAPH
+	"EE 4B	7426",	#CJK UNIFIED IDEOGRAPH
+	"EE 4C	742A",	#CJK UNIFIED IDEOGRAPH
+	"EE 4D	7429",	#CJK UNIFIED IDEOGRAPH
+	"EE 4E	742E",	#CJK UNIFIED IDEOGRAPH
+	"EE 4F	7462",	#CJK UNIFIED IDEOGRAPH
+	"EE 50	7489",	#CJK UNIFIED IDEOGRAPH
+	"EE 51	749F",	#CJK UNIFIED IDEOGRAPH
+	"EE 52	7501",	#CJK UNIFIED IDEOGRAPH
+	"EE 53	756F",	#CJK UNIFIED IDEOGRAPH
+	"EE 54	7682",	#CJK UNIFIED IDEOGRAPH
+	"EE 55	769C",	#CJK UNIFIED IDEOGRAPH
+	"EE 56	769E",	#CJK UNIFIED IDEOGRAPH
+	"EE 57	769B",	#CJK UNIFIED IDEOGRAPH
+	"EE 58	76A6",	#CJK UNIFIED IDEOGRAPH
+	"EE 59	FA17",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 5A	7746",	#CJK UNIFIED IDEOGRAPH
+	"EE 5B	52AF",	#CJK UNIFIED IDEOGRAPH
+	"EE 5C	7821",	#CJK UNIFIED IDEOGRAPH
+	"EE 5D	784E",	#CJK UNIFIED IDEOGRAPH
+	"EE 5E	7864",	#CJK UNIFIED IDEOGRAPH
+	"EE 5F	787A",	#CJK UNIFIED IDEOGRAPH
+	"EE 60	7930",	#CJK UNIFIED IDEOGRAPH
+	"EE 61	FA18",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 62	FA19",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 63	FA1A",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 64	7994",	#CJK UNIFIED IDEOGRAPH
+	"EE 65	FA1B",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 66	799B",	#CJK UNIFIED IDEOGRAPH
+	"EE 67	7AD1",	#CJK UNIFIED IDEOGRAPH
+	"EE 68	7AE7",	#CJK UNIFIED IDEOGRAPH
+	"EE 69	FA1C",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 6A	7AEB",	#CJK UNIFIED IDEOGRAPH
+	"EE 6B	7B9E",	#CJK UNIFIED IDEOGRAPH
+	"EE 6C	FA1D",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 6D	7D48",	#CJK UNIFIED IDEOGRAPH
+	"EE 6E	7D5C",	#CJK UNIFIED IDEOGRAPH
+	"EE 6F	7DB7",	#CJK UNIFIED IDEOGRAPH
+	"EE 70	7DA0",	#CJK UNIFIED IDEOGRAPH
+	"EE 71	7DD6",	#CJK UNIFIED IDEOGRAPH
+	"EE 72	7E52",	#CJK UNIFIED IDEOGRAPH
+	"EE 73	7F47",	#CJK UNIFIED IDEOGRAPH
+	"EE 74	7FA1",	#CJK UNIFIED IDEOGRAPH
+	"EE 75	FA1E",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 76	8301",	#CJK UNIFIED IDEOGRAPH
+	"EE 77	8362",	#CJK UNIFIED IDEOGRAPH
+	"EE 78	837F",	#CJK UNIFIED IDEOGRAPH
+	"EE 79	83C7",	#CJK UNIFIED IDEOGRAPH
+	"EE 7A	83F6",	#CJK UNIFIED IDEOGRAPH
+	"EE 7B	8448",	#CJK UNIFIED IDEOGRAPH
+	"EE 7C	84B4",	#CJK UNIFIED IDEOGRAPH
+	"EE 7D	8553",	#CJK UNIFIED IDEOGRAPH
+	"EE 7E	8559",	#CJK UNIFIED IDEOGRAPH
+	"EE 80	856B",	#CJK UNIFIED IDEOGRAPH
+	"EE 81	FA1F",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 82	85B0",	#CJK UNIFIED IDEOGRAPH
+	"EE 83	FA20",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 84	FA21",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 85	8807",	#CJK UNIFIED IDEOGRAPH
+	"EE 86	88F5",	#CJK UNIFIED IDEOGRAPH
+	"EE 87	8A12",	#CJK UNIFIED IDEOGRAPH
+	"EE 88	8A37",	#CJK UNIFIED IDEOGRAPH
+	"EE 89	8A79",	#CJK UNIFIED IDEOGRAPH
+	"EE 8A	8AA7",	#CJK UNIFIED IDEOGRAPH
+	"EE 8B	8ABE",	#CJK UNIFIED IDEOGRAPH
+	"EE 8C	8ADF",	#CJK UNIFIED IDEOGRAPH
+	"EE 8D	FA22",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 8E	8AF6",	#CJK UNIFIED IDEOGRAPH
+	"EE 8F	8B53",	#CJK UNIFIED IDEOGRAPH
+	"EE 90	8B7F",	#CJK UNIFIED IDEOGRAPH
+	"EE 91	8CF0",	#CJK UNIFIED IDEOGRAPH
+	"EE 92	8CF4",	#CJK UNIFIED IDEOGRAPH
+	"EE 93	8D12",	#CJK UNIFIED IDEOGRAPH
+	"EE 94	8D76",	#CJK UNIFIED IDEOGRAPH
+	"EE 95	FA23",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 96	8ECF",	#CJK UNIFIED IDEOGRAPH
+	"EE 97	FA24",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 98	FA25",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 99	9067",	#CJK UNIFIED IDEOGRAPH
+	"EE 9A	90DE",	#CJK UNIFIED IDEOGRAPH
+	"EE 9B	FA26",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE 9C	9115",	#CJK UNIFIED IDEOGRAPH
+	"EE 9D	9127",	#CJK UNIFIED IDEOGRAPH
+	"EE 9E	91DA",	#CJK UNIFIED IDEOGRAPH
+	"EE 9F	91D7",	#CJK UNIFIED IDEOGRAPH
+	"EE A0	91DE",	#CJK UNIFIED IDEOGRAPH
+	"EE A1	91ED",	#CJK UNIFIED IDEOGRAPH
+	"EE A2	91EE",	#CJK UNIFIED IDEOGRAPH
+	"EE A3	91E4",	#CJK UNIFIED IDEOGRAPH
+	"EE A4	91E5",	#CJK UNIFIED IDEOGRAPH
+	"EE A5	9206",	#CJK UNIFIED IDEOGRAPH
+	"EE A6	9210",	#CJK UNIFIED IDEOGRAPH
+	"EE A7	920A",	#CJK UNIFIED IDEOGRAPH
+	"EE A8	923A",	#CJK UNIFIED IDEOGRAPH
+	"EE A9	9240",	#CJK UNIFIED IDEOGRAPH
+	"EE AA	923C",	#CJK UNIFIED IDEOGRAPH
+	"EE AB	924E",	#CJK UNIFIED IDEOGRAPH
+	"EE AC	9259",	#CJK UNIFIED IDEOGRAPH
+	"EE AD	9251",	#CJK UNIFIED IDEOGRAPH
+	"EE AE	9239",	#CJK UNIFIED IDEOGRAPH
+	"EE AF	9267",	#CJK UNIFIED IDEOGRAPH
+	"EE B0	92A7",	#CJK UNIFIED IDEOGRAPH
+	"EE B1	9277",	#CJK UNIFIED IDEOGRAPH
+	"EE B2	9278",	#CJK UNIFIED IDEOGRAPH
+	"EE B3	92E7",	#CJK UNIFIED IDEOGRAPH
+	"EE B4	92D7",	#CJK UNIFIED IDEOGRAPH
+	"EE B5	92D9",	#CJK UNIFIED IDEOGRAPH
+	"EE B6	92D0",	#CJK UNIFIED IDEOGRAPH
+	"EE B7	FA27",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE B8	92D5",	#CJK UNIFIED IDEOGRAPH
+	"EE B9	92E0",	#CJK UNIFIED IDEOGRAPH
+	"EE BA	92D3",	#CJK UNIFIED IDEOGRAPH
+	"EE BB	9325",	#CJK UNIFIED IDEOGRAPH
+	"EE BC	9321",	#CJK UNIFIED IDEOGRAPH
+	"EE BD	92FB",	#CJK UNIFIED IDEOGRAPH
+	"EE BE	FA28",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE BF	931E",	#CJK UNIFIED IDEOGRAPH
+	"EE C0	92FF",	#CJK UNIFIED IDEOGRAPH
+	"EE C1	931D",	#CJK UNIFIED IDEOGRAPH
+	"EE C2	9302",	#CJK UNIFIED IDEOGRAPH
+	"EE C3	9370",	#CJK UNIFIED IDEOGRAPH
+	"EE C4	9357",	#CJK UNIFIED IDEOGRAPH
+	"EE C5	93A4",	#CJK UNIFIED IDEOGRAPH
+	"EE C6	93C6",	#CJK UNIFIED IDEOGRAPH
+	"EE C7	93DE",	#CJK UNIFIED IDEOGRAPH
+	"EE C8	93F8",	#CJK UNIFIED IDEOGRAPH
+	"EE C9	9431",	#CJK UNIFIED IDEOGRAPH
+	"EE CA	9445",	#CJK UNIFIED IDEOGRAPH
+	"EE CB	9448",	#CJK UNIFIED IDEOGRAPH
+	"EE CC	9592",	#CJK UNIFIED IDEOGRAPH
+	"EE CD	F9DC",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE CE	FA29",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE CF	969D",	#CJK UNIFIED IDEOGRAPH
+	"EE D0	96AF",	#CJK UNIFIED IDEOGRAPH
+	"EE D1	9733",	#CJK UNIFIED IDEOGRAPH
+	"EE D2	973B",	#CJK UNIFIED IDEOGRAPH
+	"EE D3	9743",	#CJK UNIFIED IDEOGRAPH
+	"EE D4	974D",	#CJK UNIFIED IDEOGRAPH
+	"EE D5	974F",	#CJK UNIFIED IDEOGRAPH
+	"EE D6	9751",	#CJK UNIFIED IDEOGRAPH
+	"EE D7	9755",	#CJK UNIFIED IDEOGRAPH
+	"EE D8	9857",	#CJK UNIFIED IDEOGRAPH
+	"EE D9	9865",	#CJK UNIFIED IDEOGRAPH
+	"EE DA	FA2A",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE DB	FA2B",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE DC	9927",	#CJK UNIFIED IDEOGRAPH
+	"EE DD	FA2C",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE DE	999E",	#CJK UNIFIED IDEOGRAPH
+	"EE DF	9A4E",	#CJK UNIFIED IDEOGRAPH
+	"EE E0	9AD9",	#CJK UNIFIED IDEOGRAPH
+	"EE E1	9ADC",	#CJK UNIFIED IDEOGRAPH
+	"EE E2	9B75",	#CJK UNIFIED IDEOGRAPH
+	"EE E3	9B72",	#CJK UNIFIED IDEOGRAPH
+	"EE E4	9B8F",	#CJK UNIFIED IDEOGRAPH
+	"EE E5	9BB1",	#CJK UNIFIED IDEOGRAPH
+	"EE E6	9BBB",	#CJK UNIFIED IDEOGRAPH
+	"EE E7	9C00",	#CJK UNIFIED IDEOGRAPH
+	"EE E8	9D70",	#CJK UNIFIED IDEOGRAPH
+	"EE E9	9D6B",	#CJK UNIFIED IDEOGRAPH
+	"EE EA	FA2D",	#CJK COMPATIBILITY IDEOGRAPH
+	"EE EB	9E19",	#CJK UNIFIED IDEOGRAPH
+	"EE EC	9ED1",	#CJK UNIFIED IDEOGRAPH
+	"EE EF	2170",	#SMALL ROMAN NUMERAL ONE
+	"EE F0	2171",	#SMALL ROMAN NUMERAL TWO
+	"EE F1	2172",	#SMALL ROMAN NUMERAL THREE
+	"EE F2	2173",	#SMALL ROMAN NUMERAL FOUR
+	"EE F3	2174",	#SMALL ROMAN NUMERAL FIVE
+	"EE F4	2175",	#SMALL ROMAN NUMERAL SIX
+	"EE F5	2176",	#SMALL ROMAN NUMERAL SEVEN
+	"EE F6	2177",	#SMALL ROMAN NUMERAL EIGHT
+	"EE F7	2178",	#SMALL ROMAN NUMERAL NINE
+	"EE F8	2179",	#SMALL ROMAN NUMERAL TEN
+	"EE F9	FFE2",	#FULLWIDTH NOT SIGN
+	"EE FA	FFE4",	#FULLWIDTH BROKEN BAR
+	"EE FB	FF07",	#FULLWIDTH APOSTROPHE
+	"EE FC	FF02",	#FULLWIDTH QUOTATION MARK
+	"FA 40	2170",	#SMALL ROMAN NUMERAL ONE
+	"FA 41	2171",	#SMALL ROMAN NUMERAL TWO
+	"FA 42	2172",	#SMALL ROMAN NUMERAL THREE
+	"FA 43	2173",	#SMALL ROMAN NUMERAL FOUR
+	"FA 44	2174",	#SMALL ROMAN NUMERAL FIVE
+	"FA 45	2175",	#SMALL ROMAN NUMERAL SIX
+	"FA 46	2176",	#SMALL ROMAN NUMERAL SEVEN
+	"FA 47	2177",	#SMALL ROMAN NUMERAL EIGHT
+	"FA 48	2178",	#SMALL ROMAN NUMERAL NINE
+	"FA 49	2179",	#SMALL ROMAN NUMERAL TEN
+	"FA 4A	2160",	#ROMAN NUMERAL ONE
+	"FA 4B	2161",	#ROMAN NUMERAL TWO
+	"FA 4C	2162",	#ROMAN NUMERAL THREE
+	"FA 4D	2163",	#ROMAN NUMERAL FOUR
+	"FA 4E	2164",	#ROMAN NUMERAL FIVE
+	"FA 4F	2165",	#ROMAN NUMERAL SIX
+	"FA 50	2166",	#ROMAN NUMERAL SEVEN
+	"FA 51	2167",	#ROMAN NUMERAL EIGHT
+	"FA 52	2168",	#ROMAN NUMERAL NINE
+	"FA 53	2169",	#ROMAN NUMERAL TEN
+	"FA 54	FFE2",	#FULLWIDTH NOT SIGN
+	"FA 55	FFE4",	#FULLWIDTH BROKEN BAR
+	"FA 56	FF07",	#FULLWIDTH APOSTROPHE
+	"FA 57	FF02",	#FULLWIDTH QUOTATION MARK
+	"FA 58	3231",	#PARENTHESIZED IDEOGRAPH STOCK
+	"FA 59	2116",	#NUMERO SIGN
+	"FA 5A	2121",	#TELEPHONE SIGN
+	"FA 5B	2235",	#BECAUSE
+	"FA 5C	7E8A",	#CJK UNIFIED IDEOGRAPH
+	"FA 5D	891C",	#CJK UNIFIED IDEOGRAPH
+	"FA 5E	9348",	#CJK UNIFIED IDEOGRAPH
+	"FA 5F	9288",	#CJK UNIFIED IDEOGRAPH
+	"FA 60	84DC",	#CJK UNIFIED IDEOGRAPH
+	"FA 61	4FC9",	#CJK UNIFIED IDEOGRAPH
+	"FA 62	70BB",	#CJK UNIFIED IDEOGRAPH
+	"FA 63	6631",	#CJK UNIFIED IDEOGRAPH
+	"FA 64	68C8",	#CJK UNIFIED IDEOGRAPH
+	"FA 65	92F9",	#CJK UNIFIED IDEOGRAPH
+	"FA 66	66FB",	#CJK UNIFIED IDEOGRAPH
+	"FA 67	5F45",	#CJK UNIFIED IDEOGRAPH
+	"FA 68	4E28",	#CJK UNIFIED IDEOGRAPH
+	"FA 69	4EE1",	#CJK UNIFIED IDEOGRAPH
+	"FA 6A	4EFC",	#CJK UNIFIED IDEOGRAPH
+	"FA 6B	4F00",	#CJK UNIFIED IDEOGRAPH
+	"FA 6C	4F03",	#CJK UNIFIED IDEOGRAPH
+	"FA 6D	4F39",	#CJK UNIFIED IDEOGRAPH
+	"FA 6E	4F56",	#CJK UNIFIED IDEOGRAPH
+	"FA 6F	4F92",	#CJK UNIFIED IDEOGRAPH
+	"FA 70	4F8A",	#CJK UNIFIED IDEOGRAPH
+	"FA 71	4F9A",	#CJK UNIFIED IDEOGRAPH
+	"FA 72	4F94",	#CJK UNIFIED IDEOGRAPH
+	"FA 73	4FCD",	#CJK UNIFIED IDEOGRAPH
+	"FA 74	5040",	#CJK UNIFIED IDEOGRAPH
+	"FA 75	5022",	#CJK UNIFIED IDEOGRAPH
+	"FA 76	4FFF",	#CJK UNIFIED IDEOGRAPH
+	"FA 77	501E",	#CJK UNIFIED IDEOGRAPH
+	"FA 78	5046",	#CJK UNIFIED IDEOGRAPH
+	"FA 79	5070",	#CJK UNIFIED IDEOGRAPH
+	"FA 7A	5042",	#CJK UNIFIED IDEOGRAPH
+	"FA 7B	5094",	#CJK UNIFIED IDEOGRAPH
+	"FA 7C	50F4",	#CJK UNIFIED IDEOGRAPH
+	"FA 7D	50D8",	#CJK UNIFIED IDEOGRAPH
+	"FA 7E	514A",	#CJK UNIFIED IDEOGRAPH
+	"FA 80	5164",	#CJK UNIFIED IDEOGRAPH
+	"FA 81	519D",	#CJK UNIFIED IDEOGRAPH
+	"FA 82	51BE",	#CJK UNIFIED IDEOGRAPH
+	"FA 83	51EC",	#CJK UNIFIED IDEOGRAPH
+	"FA 84	5215",	#CJK UNIFIED IDEOGRAPH
+	"FA 85	529C",	#CJK UNIFIED IDEOGRAPH
+	"FA 86	52A6",	#CJK UNIFIED IDEOGRAPH
+	"FA 87	52C0",	#CJK UNIFIED IDEOGRAPH
+	"FA 88	52DB",	#CJK UNIFIED IDEOGRAPH
+	"FA 89	5300",	#CJK UNIFIED IDEOGRAPH
+	"FA 8A	5307",	#CJK UNIFIED IDEOGRAPH
+	"FA 8B	5324",	#CJK UNIFIED IDEOGRAPH
+	"FA 8C	5372",	#CJK UNIFIED IDEOGRAPH
+	"FA 8D	5393",	#CJK UNIFIED IDEOGRAPH
+	"FA 8E	53B2",	#CJK UNIFIED IDEOGRAPH
+	"FA 8F	53DD",	#CJK UNIFIED IDEOGRAPH
+	"FA 90	FA0E",	#CJK COMPATIBILITY IDEOGRAPH
+	"FA 91	549C",	#CJK UNIFIED IDEOGRAPH
+	"FA 92	548A",	#CJK UNIFIED IDEOGRAPH
+	"FA 93	54A9",	#CJK UNIFIED IDEOGRAPH
+	"FA 94	54FF",	#CJK UNIFIED IDEOGRAPH
+	"FA 95	5586",	#CJK UNIFIED IDEOGRAPH
+	"FA 96	5759",	#CJK UNIFIED IDEOGRAPH
+	"FA 97	5765",	#CJK UNIFIED IDEOGRAPH
+	"FA 98	57AC",	#CJK UNIFIED IDEOGRAPH
+	"FA 99	57C8",	#CJK UNIFIED IDEOGRAPH
+	"FA 9A	57C7",	#CJK UNIFIED IDEOGRAPH
+	"FA 9B	FA0F",	#CJK COMPATIBILITY IDEOGRAPH
+	"FA 9C	FA10",	#CJK COMPATIBILITY IDEOGRAPH
+	"FA 9D	589E",	#CJK UNIFIED IDEOGRAPH
+	"FA 9E	58B2",	#CJK UNIFIED IDEOGRAPH
+	"FA 9F	590B",	#CJK UNIFIED IDEOGRAPH
+	"FA A0	5953",	#CJK UNIFIED IDEOGRAPH
+	"FA A1	595B",	#CJK UNIFIED IDEOGRAPH
+	"FA A2	595D",	#CJK UNIFIED IDEOGRAPH
+	"FA A3	5963",	#CJK UNIFIED IDEOGRAPH
+	"FA A4	59A4",	#CJK UNIFIED IDEOGRAPH
+	"FA A5	59BA",	#CJK UNIFIED IDEOGRAPH
+	"FA A6	5B56",	#CJK UNIFIED IDEOGRAPH
+	"FA A7	5BC0",	#CJK UNIFIED IDEOGRAPH
+	"FA A8	752F",	#CJK UNIFIED IDEOGRAPH
+	"FA A9	5BD8",	#CJK UNIFIED IDEOGRAPH
+	"FA AA	5BEC",	#CJK UNIFIED IDEOGRAPH
+	"FA AB	5C1E",	#CJK UNIFIED IDEOGRAPH
+	"FA AC	5CA6",	#CJK UNIFIED IDEOGRAPH
+	"FA AD	5CBA",	#CJK UNIFIED IDEOGRAPH
+	"FA AE	5CF5",	#CJK UNIFIED IDEOGRAPH
+	"FA AF	5D27",	#CJK UNIFIED IDEOGRAPH
+	"FA B0	5D53",	#CJK UNIFIED IDEOGRAPH
+	"FA B1	FA11",	#CJK COMPATIBILITY IDEOGRAPH
+	"FA B2	5D42",	#CJK UNIFIED IDEOGRAPH
+	"FA B3	5D6D",	#CJK UNIFIED IDEOGRAPH
+	"FA B4	5DB8",	#CJK UNIFIED IDEOGRAPH
+	"FA B5	5DB9",	#CJK UNIFIED IDEOGRAPH
+	"FA B6	5DD0",	#CJK UNIFIED IDEOGRAPH
+	"FA B7	5F21",	#CJK UNIFIED IDEOGRAPH
+	"FA B8	5F34",	#CJK UNIFIED IDEOGRAPH
+	"FA B9	5F67",	#CJK UNIFIED IDEOGRAPH
+	"FA BA	5FB7",	#CJK UNIFIED IDEOGRAPH
+	"FA BB	5FDE",	#CJK UNIFIED IDEOGRAPH
+	"FA BC	605D",	#CJK UNIFIED IDEOGRAPH
+	"FA BD	6085",	#CJK UNIFIED IDEOGRAPH
+	"FA BE	608A",	#CJK UNIFIED IDEOGRAPH
+	"FA BF	60DE",	#CJK UNIFIED IDEOGRAPH
+	"FA C0	60D5",	#CJK UNIFIED IDEOGRAPH
+	"FA C1	6120",	#CJK UNIFIED IDEOGRAPH
+	"FA C2	60F2",	#CJK UNIFIED IDEOGRAPH
+	"FA C3	6111",	#CJK UNIFIED IDEOGRAPH
+	"FA C4	6137",	#CJK UNIFIED IDEOGRAPH
+	"FA C5	6130",	#CJK UNIFIED IDEOGRAPH
+	"FA C6	6198",	#CJK UNIFIED IDEOGRAPH
+	"FA C7	6213",	#CJK UNIFIED IDEOGRAPH
+	"FA C8	62A6",	#CJK UNIFIED IDEOGRAPH
+	"FA C9	63F5",	#CJK UNIFIED IDEOGRAPH
+	"FA CA	6460",	#CJK UNIFIED IDEOGRAPH
+	"FA CB	649D",	#CJK UNIFIED IDEOGRAPH
+	"FA CC	64CE",	#CJK UNIFIED IDEOGRAPH
+	"FA CD	654E",	#CJK UNIFIED IDEOGRAPH
+	"FA CE	6600",	#CJK UNIFIED IDEOGRAPH
+	"FA CF	6615",	#CJK UNIFIED IDEOGRAPH
+	"FA D0	663B",	#CJK UNIFIED IDEOGRAPH
+	"FA D1	6609",	#CJK UNIFIED IDEOGRAPH
+	"FA D2	662E",	#CJK UNIFIED IDEOGRAPH
+	"FA D3	661E",	#CJK UNIFIED IDEOGRAPH
+	"FA D4	6624",	#CJK UNIFIED IDEOGRAPH
+	"FA D5	6665",	#CJK UNIFIED IDEOGRAPH
+	"FA D6	6657",	#CJK UNIFIED IDEOGRAPH
+	"FA D7	6659",	#CJK UNIFIED IDEOGRAPH
+	"FA D8	FA12",	#CJK COMPATIBILITY IDEOGRAPH
+	"FA D9	6673",	#CJK UNIFIED IDEOGRAPH
+	"FA DA	6699",	#CJK UNIFIED IDEOGRAPH
+	"FA DB	66A0",	#CJK UNIFIED IDEOGRAPH
+	"FA DC	66B2",	#CJK UNIFIED IDEOGRAPH
+	"FA DD	66BF",	#CJK UNIFIED IDEOGRAPH
+	"FA DE	66FA",	#CJK UNIFIED IDEOGRAPH
+	"FA DF	670E",	#CJK UNIFIED IDEOGRAPH
+	"FA E0	F929",	#CJK COMPATIBILITY IDEOGRAPH
+	"FA E1	6766",	#CJK UNIFIED IDEOGRAPH
+	"FA E2	67BB",	#CJK UNIFIED IDEOGRAPH
+	"FA E3	6852",	#CJK UNIFIED IDEOGRAPH
+	"FA E4	67C0",	#CJK UNIFIED IDEOGRAPH
+	"FA E5	6801",	#CJK UNIFIED IDEOGRAPH
+	"FA E6	6844",	#CJK UNIFIED IDEOGRAPH
+	"FA E7	68CF",	#CJK UNIFIED IDEOGRAPH
+	"FA E8	FA13",	#CJK COMPATIBILITY IDEOGRAPH
+	"FA E9	6968",	#CJK UNIFIED IDEOGRAPH
+	"FA EA	FA14",	#CJK COMPATIBILITY IDEOGRAPH
+	"FA EB	6998",	#CJK UNIFIED IDEOGRAPH
+	"FA EC	69E2",	#CJK UNIFIED IDEOGRAPH
+	"FA ED	6A30",	#CJK UNIFIED IDEOGRAPH
+	"FA EE	6A6B",	#CJK UNIFIED IDEOGRAPH
+	"FA EF	6A46",	#CJK UNIFIED IDEOGRAPH
+	"FA F0	6A73",	#CJK UNIFIED IDEOGRAPH
+	"FA F1	6A7E",	#CJK UNIFIED IDEOGRAPH
+	"FA F2	6AE2",	#CJK UNIFIED IDEOGRAPH
+	"FA F3	6AE4",	#CJK UNIFIED IDEOGRAPH
+	"FA F4	6BD6",	#CJK UNIFIED IDEOGRAPH
+	"FA F5	6C3F",	#CJK UNIFIED IDEOGRAPH
+	"FA F6	6C5C",	#CJK UNIFIED IDEOGRAPH
+	"FA F7	6C86",	#CJK UNIFIED IDEOGRAPH
+	"FA F8	6C6F",	#CJK UNIFIED IDEOGRAPH
+	"FA F9	6CDA",	#CJK UNIFIED IDEOGRAPH
+	"FA FA	6D04",	#CJK UNIFIED IDEOGRAPH
+	"FA FB	6D87",	#CJK UNIFIED IDEOGRAPH
+	"FA FC	6D6F",	#CJK UNIFIED IDEOGRAPH
+	"FB 40	6D96",	#CJK UNIFIED IDEOGRAPH
+	"FB 41	6DAC",	#CJK UNIFIED IDEOGRAPH
+	"FB 42	6DCF",	#CJK UNIFIED IDEOGRAPH
+	"FB 43	6DF8",	#CJK UNIFIED IDEOGRAPH
+	"FB 44	6DF2",	#CJK UNIFIED IDEOGRAPH
+	"FB 45	6DFC",	#CJK UNIFIED IDEOGRAPH
+	"FB 46	6E39",	#CJK UNIFIED IDEOGRAPH
+	"FB 47	6E5C",	#CJK UNIFIED IDEOGRAPH
+	"FB 48	6E27",	#CJK UNIFIED IDEOGRAPH
+	"FB 49	6E3C",	#CJK UNIFIED IDEOGRAPH
+	"FB 4A	6EBF",	#CJK UNIFIED IDEOGRAPH
+	"FB 4B	6F88",	#CJK UNIFIED IDEOGRAPH
+	"FB 4C	6FB5",	#CJK UNIFIED IDEOGRAPH
+	"FB 4D	6FF5",	#CJK UNIFIED IDEOGRAPH
+	"FB 4E	7005",	#CJK UNIFIED IDEOGRAPH
+	"FB 4F	7007",	#CJK UNIFIED IDEOGRAPH
+	"FB 50	7028",	#CJK UNIFIED IDEOGRAPH
+	"FB 51	7085",	#CJK UNIFIED IDEOGRAPH
+	"FB 52	70AB",	#CJK UNIFIED IDEOGRAPH
+	"FB 53	710F",	#CJK UNIFIED IDEOGRAPH
+	"FB 54	7104",	#CJK UNIFIED IDEOGRAPH
+	"FB 55	715C",	#CJK UNIFIED IDEOGRAPH
+	"FB 56	7146",	#CJK UNIFIED IDEOGRAPH
+	"FB 57	7147",	#CJK UNIFIED IDEOGRAPH
+	"FB 58	FA15",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 59	71C1",	#CJK UNIFIED IDEOGRAPH
+	"FB 5A	71FE",	#CJK UNIFIED IDEOGRAPH
+	"FB 5B	72B1",	#CJK UNIFIED IDEOGRAPH
+	"FB 5C	72BE",	#CJK UNIFIED IDEOGRAPH
+	"FB 5D	7324",	#CJK UNIFIED IDEOGRAPH
+	"FB 5E	FA16",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 5F	7377",	#CJK UNIFIED IDEOGRAPH
+	"FB 60	73BD",	#CJK UNIFIED IDEOGRAPH
+	"FB 61	73C9",	#CJK UNIFIED IDEOGRAPH
+	"FB 62	73D6",	#CJK UNIFIED IDEOGRAPH
+	"FB 63	73E3",	#CJK UNIFIED IDEOGRAPH
+	"FB 64	73D2",	#CJK UNIFIED IDEOGRAPH
+	"FB 65	7407",	#CJK UNIFIED IDEOGRAPH
+	"FB 66	73F5",	#CJK UNIFIED IDEOGRAPH
+	"FB 67	7426",	#CJK UNIFIED IDEOGRAPH
+	"FB 68	742A",	#CJK UNIFIED IDEOGRAPH
+	"FB 69	7429",	#CJK UNIFIED IDEOGRAPH
+	"FB 6A	742E",	#CJK UNIFIED IDEOGRAPH
+	"FB 6B	7462",	#CJK UNIFIED IDEOGRAPH
+	"FB 6C	7489",	#CJK UNIFIED IDEOGRAPH
+	"FB 6D	749F",	#CJK UNIFIED IDEOGRAPH
+	"FB 6E	7501",	#CJK UNIFIED IDEOGRAPH
+	"FB 6F	756F",	#CJK UNIFIED IDEOGRAPH
+	"FB 70	7682",	#CJK UNIFIED IDEOGRAPH
+	"FB 71	769C",	#CJK UNIFIED IDEOGRAPH
+	"FB 72	769E",	#CJK UNIFIED IDEOGRAPH
+	"FB 73	769B",	#CJK UNIFIED IDEOGRAPH
+	"FB 74	76A6",	#CJK UNIFIED IDEOGRAPH
+	"FB 75	FA17",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 76	7746",	#CJK UNIFIED IDEOGRAPH
+	"FB 77	52AF",	#CJK UNIFIED IDEOGRAPH
+	"FB 78	7821",	#CJK UNIFIED IDEOGRAPH
+	"FB 79	784E",	#CJK UNIFIED IDEOGRAPH
+	"FB 7A	7864",	#CJK UNIFIED IDEOGRAPH
+	"FB 7B	787A",	#CJK UNIFIED IDEOGRAPH
+	"FB 7C	7930",	#CJK UNIFIED IDEOGRAPH
+	"FB 7D	FA18",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 7E	FA19",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 80	FA1A",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 81	7994",	#CJK UNIFIED IDEOGRAPH
+	"FB 82	FA1B",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 83	799B",	#CJK UNIFIED IDEOGRAPH
+	"FB 84	7AD1",	#CJK UNIFIED IDEOGRAPH
+	"FB 85	7AE7",	#CJK UNIFIED IDEOGRAPH
+	"FB 86	FA1C",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 87	7AEB",	#CJK UNIFIED IDEOGRAPH
+	"FB 88	7B9E",	#CJK UNIFIED IDEOGRAPH
+	"FB 89	FA1D",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 8A	7D48",	#CJK UNIFIED IDEOGRAPH
+	"FB 8B	7D5C",	#CJK UNIFIED IDEOGRAPH
+	"FB 8C	7DB7",	#CJK UNIFIED IDEOGRAPH
+	"FB 8D	7DA0",	#CJK UNIFIED IDEOGRAPH
+	"FB 8E	7DD6",	#CJK UNIFIED IDEOGRAPH
+	"FB 8F	7E52",	#CJK UNIFIED IDEOGRAPH
+	"FB 90	7F47",	#CJK UNIFIED IDEOGRAPH
+	"FB 91	7FA1",	#CJK UNIFIED IDEOGRAPH
+	"FB 92	FA1E",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 93	8301",	#CJK UNIFIED IDEOGRAPH
+	"FB 94	8362",	#CJK UNIFIED IDEOGRAPH
+	"FB 95	837F",	#CJK UNIFIED IDEOGRAPH
+	"FB 96	83C7",	#CJK UNIFIED IDEOGRAPH
+	"FB 97	83F6",	#CJK UNIFIED IDEOGRAPH
+	"FB 98	8448",	#CJK UNIFIED IDEOGRAPH
+	"FB 99	84B4",	#CJK UNIFIED IDEOGRAPH
+	"FB 9A	8553",	#CJK UNIFIED IDEOGRAPH
+	"FB 9B	8559",	#CJK UNIFIED IDEOGRAPH
+	"FB 9C	856B",	#CJK UNIFIED IDEOGRAPH
+	"FB 9D	FA1F",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB 9E	85B0",	#CJK UNIFIED IDEOGRAPH
+	"FB 9F	FA20",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB A0	FA21",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB A1	8807",	#CJK UNIFIED IDEOGRAPH
+	"FB A2	88F5",	#CJK UNIFIED IDEOGRAPH
+	"FB A3	8A12",	#CJK UNIFIED IDEOGRAPH
+	"FB A4	8A37",	#CJK UNIFIED IDEOGRAPH
+	"FB A5	8A79",	#CJK UNIFIED IDEOGRAPH
+	"FB A6	8AA7",	#CJK UNIFIED IDEOGRAPH
+	"FB A7	8ABE",	#CJK UNIFIED IDEOGRAPH
+	"FB A8	8ADF",	#CJK UNIFIED IDEOGRAPH
+	"FB A9	FA22",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB AA	8AF6",	#CJK UNIFIED IDEOGRAPH
+	"FB AB	8B53",	#CJK UNIFIED IDEOGRAPH
+	"FB AC	8B7F",	#CJK UNIFIED IDEOGRAPH
+	"FB AD	8CF0",	#CJK UNIFIED IDEOGRAPH
+	"FB AE	8CF4",	#CJK UNIFIED IDEOGRAPH
+	"FB AF	8D12",	#CJK UNIFIED IDEOGRAPH
+	"FB B0	8D76",	#CJK UNIFIED IDEOGRAPH
+	"FB B1	FA23",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB B2	8ECF",	#CJK UNIFIED IDEOGRAPH
+	"FB B3	FA24",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB B4	FA25",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB B5	9067",	#CJK UNIFIED IDEOGRAPH
+	"FB B6	90DE",	#CJK UNIFIED IDEOGRAPH
+	"FB B7	FA26",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB B8	9115",	#CJK UNIFIED IDEOGRAPH
+	"FB B9	9127",	#CJK UNIFIED IDEOGRAPH
+	"FB BA	91DA",	#CJK UNIFIED IDEOGRAPH
+	"FB BB	91D7",	#CJK UNIFIED IDEOGRAPH
+	"FB BC	91DE",	#CJK UNIFIED IDEOGRAPH
+	"FB BD	91ED",	#CJK UNIFIED IDEOGRAPH
+	"FB BE	91EE",	#CJK UNIFIED IDEOGRAPH
+	"FB BF	91E4",	#CJK UNIFIED IDEOGRAPH
+	"FB C0	91E5",	#CJK UNIFIED IDEOGRAPH
+	"FB C1	9206",	#CJK UNIFIED IDEOGRAPH
+	"FB C2	9210",	#CJK UNIFIED IDEOGRAPH
+	"FB C3	920A",	#CJK UNIFIED IDEOGRAPH
+	"FB C4	923A",	#CJK UNIFIED IDEOGRAPH
+	"FB C5	9240",	#CJK UNIFIED IDEOGRAPH
+	"FB C6	923C",	#CJK UNIFIED IDEOGRAPH
+	"FB C7	924E",	#CJK UNIFIED IDEOGRAPH
+	"FB C8	9259",	#CJK UNIFIED IDEOGRAPH
+	"FB C9	9251",	#CJK UNIFIED IDEOGRAPH
+	"FB CA	9239",	#CJK UNIFIED IDEOGRAPH
+	"FB CB	9267",	#CJK UNIFIED IDEOGRAPH
+	"FB CC	92A7",	#CJK UNIFIED IDEOGRAPH
+	"FB CD	9277",	#CJK UNIFIED IDEOGRAPH
+	"FB CE	9278",	#CJK UNIFIED IDEOGRAPH
+	"FB CF	92E7",	#CJK UNIFIED IDEOGRAPH
+	"FB D0	92D7",	#CJK UNIFIED IDEOGRAPH
+	"FB D1	92D9",	#CJK UNIFIED IDEOGRAPH
+	"FB D2	92D0",	#CJK UNIFIED IDEOGRAPH
+	"FB D3	FA27",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB D4	92D5",	#CJK UNIFIED IDEOGRAPH
+	"FB D5	92E0",	#CJK UNIFIED IDEOGRAPH
+	"FB D6	92D3",	#CJK UNIFIED IDEOGRAPH
+	"FB D7	9325",	#CJK UNIFIED IDEOGRAPH
+	"FB D8	9321",	#CJK UNIFIED IDEOGRAPH
+	"FB D9	92FB",	#CJK UNIFIED IDEOGRAPH
+	"FB DA	FA28",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB DB	931E",	#CJK UNIFIED IDEOGRAPH
+	"FB DC	92FF",	#CJK UNIFIED IDEOGRAPH
+	"FB DD	931D",	#CJK UNIFIED IDEOGRAPH
+	"FB DE	9302",	#CJK UNIFIED IDEOGRAPH
+	"FB DF	9370",	#CJK UNIFIED IDEOGRAPH
+	"FB E0	9357",	#CJK UNIFIED IDEOGRAPH
+	"FB E1	93A4",	#CJK UNIFIED IDEOGRAPH
+	"FB E2	93C6",	#CJK UNIFIED IDEOGRAPH
+	"FB E3	93DE",	#CJK UNIFIED IDEOGRAPH
+	"FB E4	93F8",	#CJK UNIFIED IDEOGRAPH
+	"FB E5	9431",	#CJK UNIFIED IDEOGRAPH
+	"FB E6	9445",	#CJK UNIFIED IDEOGRAPH
+	"FB E7	9448",	#CJK UNIFIED IDEOGRAPH
+	"FB E8	9592",	#CJK UNIFIED IDEOGRAPH
+	"FB E9	F9DC",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB EA	FA29",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB EB	969D",	#CJK UNIFIED IDEOGRAPH
+	"FB EC	96AF",	#CJK UNIFIED IDEOGRAPH
+	"FB ED	9733",	#CJK UNIFIED IDEOGRAPH
+	"FB EE	973B",	#CJK UNIFIED IDEOGRAPH
+	"FB EF	9743",	#CJK UNIFIED IDEOGRAPH
+	"FB F0	974D",	#CJK UNIFIED IDEOGRAPH
+	"FB F1	974F",	#CJK UNIFIED IDEOGRAPH
+	"FB F2	9751",	#CJK UNIFIED IDEOGRAPH
+	"FB F3	9755",	#CJK UNIFIED IDEOGRAPH
+	"FB F4	9857",	#CJK UNIFIED IDEOGRAPH
+	"FB F5	9865",	#CJK UNIFIED IDEOGRAPH
+	"FB F6	FA2A",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB F7	FA2B",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB F8	9927",	#CJK UNIFIED IDEOGRAPH
+	"FB F9	FA2C",	#CJK COMPATIBILITY IDEOGRAPH
+	"FB FA	999E",	#CJK UNIFIED IDEOGRAPH
+	"FB FB	9A4E",	#CJK UNIFIED IDEOGRAPH
+	"FB FC	9AD9",	#CJK UNIFIED IDEOGRAPH
+	"FC 40	9ADC",	#CJK UNIFIED IDEOGRAPH
+	"FC 41	9B75",	#CJK UNIFIED IDEOGRAPH
+	"FC 42	9B72",	#CJK UNIFIED IDEOGRAPH
+	"FC 43	9B8F",	#CJK UNIFIED IDEOGRAPH
+	"FC 44	9BB1",	#CJK UNIFIED IDEOGRAPH
+	"FC 45	9BBB",	#CJK UNIFIED IDEOGRAPH
+	"FC 46	9C00",	#CJK UNIFIED IDEOGRAPH
+	"FC 47	9D70",	#CJK UNIFIED IDEOGRAPH
+	"FC 48	9D6B",	#CJK UNIFIED IDEOGRAPH
+	"FC 49	FA2D",	#CJK COMPATIBILITY IDEOGRAPH
+	"FC 4A	9E19",	#CJK UNIFIED IDEOGRAPH
+	"FC 4B	9ED1",	#CJK UNIFIED IDEOGRAPH
+};
--- /dev/null
+++ b/appl/lib/convcs/gengb2312.b
@@ -1,0 +1,1143 @@
+# generate the GB2312 character set converter data file
+implement GenGB;
+
+include "sys.m";
+include "draw.m";
+
+GBDATA : con "/lib/convcs/gb2312";
+
+GenGB : module {
+	init : fn (ctxt : ref Draw->Context, args : list of string);
+};
+
+init(nil : ref Draw->Context, nil : list of string)
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->create(GBDATA, Sys->OWRITE, 8r644);
+	if (fd == nil) {
+		sys->print("cannot create %s: %r\n", GBDATA);
+		return;
+	}
+	s := "";
+	for (i := 0; i < len tabgb; i++)
+		s[len s] = tabgb[i];
+
+	buf := array of byte s;
+	for (i = 0; i < len buf;) {
+		towrite := Sys->ATOMICIO;
+		if (len buf - i < Sys->ATOMICIO)
+			towrite = len buf -i;
+		n := sys->write(fd, buf[i:], towrite);
+		if (n <= 0) {
+			sys->print("error writing %s: %r", GBDATA);
+			return;
+		}
+		i += n;
+	}
+}
+
+
+ERRchar : con 16rFFFD;
+
+tabgb := array [] of {
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r3000,16r3001,16r3002,
+16r30fb,16r02c9,16r02c7,16r00a8,16r3003,16r3005,16r2015,16r301c,
+16r2225,16r2026,16r2018,16r2019,16r201c,16r201d,16r3014,16r3015,
+16r3008,16r3009,16r300a,16r300b,16r300c,16r300d,16r300e,16r300f,
+16r3016,16r3017,16r3010,16r3011,16r00b1,16r00d7,16r00f7,16r2236,
+16r2227,16r2228,16r2211,16r220f,16r222a,16r2229,16r2208,16r2237,
+16r221a,16r22a5,16r2225,16r2220,16r2312,16r2299,16r222b,16r222e,
+16r2261,16r224c,16r2248,16r223d,16r221d,16r2260,16r226e,16r226f,
+16r2264,16r2265,16r221e,16r2235,16r2234,16r2642,16r2640,16r00b0,
+16r2032,16r2033,16r2103,16rff04,16r00a4,16rffe0,16rffe1,16r2030,
+16r00a7,16r2116,16r2606,16r2605,16r25cb,16r25cf,16r25ce,16r25c7,
+16r25c6,16r25a1,16r25a0,16r25b3,16r25b2,16r203b,16r2192,16r2190,
+16r2191,16r2193,16r3013,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r2488,16r2489,16r248a,16r248b,16r248c,16r248d,16r248e,
+16r248f,16r2490,16r2491,16r2492,16r2493,16r2494,16r2495,16r2496,
+16r2497,16r2498,16r2499,16r249a,16r249b,16r2474,16r2475,16r2476,
+16r2477,16r2478,16r2479,16r247a,16r247b,16r247c,16r247d,16r247e,
+16r247f,16r2480,16r2481,16r2482,16r2483,16r2484,16r2485,16r2486,
+16r2487,16r2460,16r2461,16r2462,16r2463,16r2464,16r2465,16r2466,
+16r2467,16r2468,16r2469,ERRchar,ERRchar,16r3220,16r3221,16r3222,
+16r3223,16r3224,16r3225,16r3226,16r3227,16r3228,16r3229,ERRchar,
+ERRchar,16r2160,16r2161,16r2162,16r2163,16r2164,16r2165,16r2166,
+16r2167,16r2168,16r2169,16r216a,16r216b,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16rff01,16rff02,16rff03,
+16rffe5,16rff05,16rff06,16rff07,16rff08,16rff09,16rff0a,16rff0b,
+16rff0c,16rff0d,16rff0e,16rff0f,16rff10,16rff11,16rff12,16rff13,
+16rff14,16rff15,16rff16,16rff17,16rff18,16rff19,16rff1a,16rff1b,
+16rff1c,16rff1d,16rff1e,16rff1f,16rff20,16rff21,16rff22,16rff23,
+16rff24,16rff25,16rff26,16rff27,16rff28,16rff29,16rff2a,16rff2b,
+16rff2c,16rff2d,16rff2e,16rff2f,16rff30,16rff31,16rff32,16rff33,
+16rff34,16rff35,16rff36,16rff37,16rff38,16rff39,16rff3a,16rff3b,
+16rff3c,16rff3d,16rff3e,16rff3f,16rff40,16rff41,16rff42,16rff43,
+16rff44,16rff45,16rff46,16rff47,16rff48,16rff49,16rff4a,16rff4b,
+16rff4c,16rff4d,16rff4e,16rff4f,16rff50,16rff51,16rff52,16rff53,
+16rff54,16rff55,16rff56,16rff57,16rff58,16rff59,16rff5a,16rff5b,
+16rff5c,16rff5d,16rffe3,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r3041,16r3042,16r3043,16r3044,16r3045,16r3046,16r3047,
+16r3048,16r3049,16r304a,16r304b,16r304c,16r304d,16r304e,16r304f,
+16r3050,16r3051,16r3052,16r3053,16r3054,16r3055,16r3056,16r3057,
+16r3058,16r3059,16r305a,16r305b,16r305c,16r305d,16r305e,16r305f,
+16r3060,16r3061,16r3062,16r3063,16r3064,16r3065,16r3066,16r3067,
+16r3068,16r3069,16r306a,16r306b,16r306c,16r306d,16r306e,16r306f,
+16r3070,16r3071,16r3072,16r3073,16r3074,16r3075,16r3076,16r3077,
+16r3078,16r3079,16r307a,16r307b,16r307c,16r307d,16r307e,16r307f,
+16r3080,16r3081,16r3082,16r3083,16r3084,16r3085,16r3086,16r3087,
+16r3088,16r3089,16r308a,16r308b,16r308c,16r308d,16r308e,16r308f,
+16r3090,16r3091,16r3092,16r3093,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r30a1,16r30a2,16r30a3,
+16r30a4,16r30a5,16r30a6,16r30a7,16r30a8,16r30a9,16r30aa,16r30ab,
+16r30ac,16r30ad,16r30ae,16r30af,16r30b0,16r30b1,16r30b2,16r30b3,
+16r30b4,16r30b5,16r30b6,16r30b7,16r30b8,16r30b9,16r30ba,16r30bb,
+16r30bc,16r30bd,16r30be,16r30bf,16r30c0,16r30c1,16r30c2,16r30c3,
+16r30c4,16r30c5,16r30c6,16r30c7,16r30c8,16r30c9,16r30ca,16r30cb,
+16r30cc,16r30cd,16r30ce,16r30cf,16r30d0,16r30d1,16r30d2,16r30d3,
+16r30d4,16r30d5,16r30d6,16r30d7,16r30d8,16r30d9,16r30da,16r30db,
+16r30dc,16r30dd,16r30de,16r30df,16r30e0,16r30e1,16r30e2,16r30e3,
+16r30e4,16r30e5,16r30e6,16r30e7,16r30e8,16r30e9,16r30ea,16r30eb,
+16r30ec,16r30ed,16r30ee,16r30ef,16r30f0,16r30f1,16r30f2,16r30f3,
+16r30f4,16r30f5,16r30f6,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r0391,16r0392,16r0393,16r0394,16r0395,16r0396,16r0397,
+16r0398,16r0399,16r039a,16r039b,16r039c,16r039d,16r039e,16r039f,
+16r03a0,16r03a1,16r03a3,16r03a4,16r03a5,16r03a6,16r03a7,16r03a8,
+16r03a9,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r03b1,16r03b2,16r03b3,16r03b4,16r03b5,16r03b6,16r03b7,
+16r03b8,16r03b9,16r03ba,16r03bb,16r03bc,16r03bd,16r03be,16r03bf,
+16r03c0,16r03c1,16r03c3,16r03c4,16r03c5,16r03c6,16r03c7,16r03c8,
+16r03c9,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r0410,16r0411,16r0412,
+16r0413,16r0414,16r0415,16r0401,16r0416,16r0417,16r0418,16r0419,
+16r041a,16r041b,16r041c,16r041d,16r041e,16r041f,16r0420,16r0421,
+16r0422,16r0423,16r0424,16r0425,16r0426,16r0427,16r0428,16r0429,
+16r042a,16r042b,16r042c,16r042d,16r042e,16r042f,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r0430,16r0431,16r0432,
+16r0433,16r0434,16r0435,16r0451,16r0436,16r0437,16r0438,16r0439,
+16r043a,16r043b,16r043c,16r043d,16r043e,16r043f,16r0440,16r0441,
+16r0442,16r0443,16r0444,16r0445,16r0446,16r0447,16r0448,16r0449,
+16r044a,16r044b,16r044c,16r044d,16r044e,16r044f,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r0101,16r00e1,16r01ce,16r00e0,16r0113,16r00e9,16r011b,
+16r00e8,16r012b,16r00ed,16r01d0,16r00ec,16r014d,16r00f3,16r01d2,
+16r00f2,16r016b,16r00fa,16r01d4,16r00f9,16r01d6,16r01d8,16r01da,
+16r01dc,16r00fc,16r00ea,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r3105,16r3106,16r3107,
+16r3108,16r3109,16r310a,16r310b,16r310c,16r310d,16r310e,16r310f,
+16r3110,16r3111,16r3112,16r3113,16r3114,16r3115,16r3116,16r3117,
+16r3118,16r3119,16r311a,16r311b,16r311c,16r311d,16r311e,16r311f,
+16r3120,16r3121,16r3122,16r3123,16r3124,16r3125,16r3126,16r3127,
+16r3128,16r3129,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+16r2500,16r2501,16r2502,16r2503,16r2504,16r2505,16r2506,16r2507,
+16r2508,16r2509,16r250a,16r250b,16r250c,16r250d,16r250e,16r250f,
+16r2510,16r2511,16r2512,16r2513,16r2514,16r2515,16r2516,16r2517,
+16r2518,16r2519,16r251a,16r251b,16r251c,16r251d,16r251e,16r251f,
+16r2520,16r2521,16r2522,16r2523,16r2524,16r2525,16r2526,16r2527,
+16r2528,16r2529,16r252a,16r252b,16r252c,16r252d,16r252e,16r252f,
+16r2530,16r2531,16r2532,16r2533,16r2534,16r2535,16r2536,16r2537,
+16r2538,16r2539,16r253a,16r253b,16r253c,16r253d,16r253e,16r253f,
+16r2540,16r2541,16r2542,16r2543,16r2544,16r2545,16r2546,16r2547,
+16r2548,16r2549,16r254a,16r254b,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r554a,16r963f,16r57c3,16r6328,16r54ce,16r5509,16r54c0,
+16r7691,16r764c,16r853c,16r77ee,16r827e,16r788d,16r7231,16r9698,
+16r978d,16r6c28,16r5b89,16r4ffa,16r6309,16r6697,16r5cb8,16r80fa,
+16r6848,16r80ae,16r6602,16r76ce,16r51f9,16r6556,16r71ac,16r7ff1,
+16r8884,16r50b2,16r5965,16r61ca,16r6fb3,16r82ad,16r634c,16r6252,
+16r53ed,16r5427,16r7b06,16r516b,16r75a4,16r5df4,16r62d4,16r8dcb,
+16r9776,16r628a,16r8019,16r575d,16r9738,16r7f62,16r7238,16r767d,
+16r67cf,16r767e,16r6446,16r4f70,16r8d25,16r62dc,16r7a17,16r6591,
+16r73ed,16r642c,16r6273,16r822c,16r9881,16r677f,16r7248,16r626e,
+16r62cc,16r4f34,16r74e3,16r534a,16r529e,16r7eca,16r90a6,16r5e2e,
+16r6886,16r699c,16r8180,16r7ed1,16r68d2,16r78c5,16r868c,16r9551,
+16r508d,16r8c24,16r82de,16r80de,16r5305,16r8912,16r5265,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r8584,16r96f9,16r4fdd,
+16r5821,16r9971,16r5b9d,16r62b1,16r62a5,16r66b4,16r8c79,16r9c8d,
+16r7206,16r676f,16r7891,16r60b2,16r5351,16r5317,16r8f88,16r80cc,
+16r8d1d,16r94a1,16r500d,16r72c8,16r5907,16r60eb,16r7119,16r88ab,
+16r5954,16r82ef,16r672c,16r7b28,16r5d29,16r7ef7,16r752d,16r6cf5,
+16r8e66,16r8ff8,16r903c,16r9f3b,16r6bd4,16r9119,16r7b14,16r5f7c,
+16r78a7,16r84d6,16r853d,16r6bd5,16r6bd9,16r6bd6,16r5e01,16r5e87,
+16r75f9,16r95ed,16r655d,16r5f0a,16r5fc5,16r8f9f,16r58c1,16r81c2,
+16r907f,16r965b,16r97ad,16r8fb9,16r7f16,16r8d2c,16r6241,16r4fbf,
+16r53d8,16r535e,16r8fa8,16r8fa9,16r8fab,16r904d,16r6807,16r5f6a,
+16r8198,16r8868,16r9cd6,16r618b,16r522b,16r762a,16r5f6c,16r658c,
+16r6fd2,16r6ee8,16r5bbe,16r6448,16r5175,16r51b0,16r67c4,16r4e19,
+16r79c9,16r997c,16r70b3,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r75c5,16r5e76,16r73bb,16r83e0,16r64ad,16r62e8,16r94b5,
+16r6ce2,16r535a,16r52c3,16r640f,16r94c2,16r7b94,16r4f2f,16r5e1b,
+16r8236,16r8116,16r818a,16r6e24,16r6cca,16r9a73,16r6355,16r535c,
+16r54fa,16r8865,16r57e0,16r4e0d,16r5e03,16r6b65,16r7c3f,16r90e8,
+16r6016,16r64e6,16r731c,16r88c1,16r6750,16r624d,16r8d22,16r776c,
+16r8e29,16r91c7,16r5f69,16r83dc,16r8521,16r9910,16r53c2,16r8695,
+16r6b8b,16r60ed,16r60e8,16r707f,16r82cd,16r8231,16r4ed3,16r6ca7,
+16r85cf,16r64cd,16r7cd9,16r69fd,16r66f9,16r8349,16r5395,16r7b56,
+16r4fa7,16r518c,16r6d4b,16r5c42,16r8e6d,16r63d2,16r53c9,16r832c,
+16r8336,16r67e5,16r78b4,16r643d,16r5bdf,16r5c94,16r5dee,16r8be7,
+16r62c6,16r67f4,16r8c7a,16r6400,16r63ba,16r8749,16r998b,16r8c17,
+16r7f20,16r94f2,16r4ea7,16r9610,16r98a4,16r660c,16r7316,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r573a,16r5c1d,16r5e38,
+16r957f,16r507f,16r80a0,16r5382,16r655e,16r7545,16r5531,16r5021,
+16r8d85,16r6284,16r949e,16r671d,16r5632,16r6f6e,16r5de2,16r5435,
+16r7092,16r8f66,16r626f,16r64a4,16r63a3,16r5f7b,16r6f88,16r90f4,
+16r81e3,16r8fb0,16r5c18,16r6668,16r5ff1,16r6c89,16r9648,16r8d81,
+16r886c,16r6491,16r79f0,16r57ce,16r6a59,16r6210,16r5448,16r4e58,
+16r7a0b,16r60e9,16r6f84,16r8bda,16r627f,16r901e,16r9a8b,16r79e4,
+16r5403,16r75f4,16r6301,16r5319,16r6c60,16r8fdf,16r5f1b,16r9a70,
+16r803b,16r9f7f,16r4f88,16r5c3a,16r8d64,16r7fc5,16r65a5,16r70bd,
+16r5145,16r51b2,16r866b,16r5d07,16r5ba0,16r62bd,16r916c,16r7574,
+16r8e0c,16r7a20,16r6101,16r7b79,16r4ec7,16r7ef8,16r7785,16r4e11,
+16r81ed,16r521d,16r51fa,16r6a71,16r53a8,16r8e87,16r9504,16r96cf,
+16r6ec1,16r9664,16r695a,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r7840,16r50a8,16r77d7,16r6410,16r89e6,16r5904,16r63e3,
+16r5ddd,16r7a7f,16r693d,16r4f20,16r8239,16r5598,16r4e32,16r75ae,
+16r7a97,16r5e62,16r5e8a,16r95ef,16r521b,16r5439,16r708a,16r6376,
+16r9524,16r5782,16r6625,16r693f,16r9187,16r5507,16r6df3,16r7eaf,
+16r8822,16r6233,16r7ef0,16r75b5,16r8328,16r78c1,16r96cc,16r8f9e,
+16r6148,16r74f7,16r8bcd,16r6b64,16r523a,16r8d50,16r6b21,16r806a,
+16r8471,16r56f1,16r5306,16r4ece,16r4e1b,16r51d1,16r7c97,16r918b,
+16r7c07,16r4fc3,16r8e7f,16r7be1,16r7a9c,16r6467,16r5d14,16r50ac,
+16r8106,16r7601,16r7cb9,16r6dec,16r7fe0,16r6751,16r5b58,16r5bf8,
+16r78cb,16r64ae,16r6413,16r63aa,16r632b,16r9519,16r642d,16r8fbe,
+16r7b54,16r7629,16r6253,16r5927,16r5446,16r6b79,16r50a3,16r6234,
+16r5e26,16r6b86,16r4ee3,16r8d37,16r888b,16r5f85,16r902e,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r6020,16r803d,16r62c5,
+16r4e39,16r5355,16r90f8,16r63b8,16r80c6,16r65e6,16r6c2e,16r4f46,
+16r60ee,16r6de1,16r8bde,16r5f39,16r86cb,16r5f53,16r6321,16r515a,
+16r8361,16r6863,16r5200,16r6363,16r8e48,16r5012,16r5c9b,16r7977,
+16r5bfc,16r5230,16r7a3b,16r60bc,16r9053,16r76d7,16r5fb7,16r5f97,
+16r7684,16r8e6c,16r706f,16r767b,16r7b49,16r77aa,16r51f3,16r9093,
+16r5824,16r4f4e,16r6ef4,16r8fea,16r654c,16r7b1b,16r72c4,16r6da4,
+16r7fdf,16r5ae1,16r62b5,16r5e95,16r5730,16r8482,16r7b2c,16r5e1d,
+16r5f1f,16r9012,16r7f14,16r98a0,16r6382,16r6ec7,16r7898,16r70b9,
+16r5178,16r975b,16r57ab,16r7535,16r4f43,16r7538,16r5e97,16r60e6,
+16r5960,16r6dc0,16r6bbf,16r7889,16r53fc,16r96d5,16r51cb,16r5201,
+16r6389,16r540a,16r9493,16r8c03,16r8dcc,16r7239,16r789f,16r8776,
+16r8fed,16r8c0d,16r53e0,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r4e01,16r76ef,16r53ee,16r9489,16r9876,16r9f0e,16r952d,
+16r5b9a,16r8ba2,16r4e22,16r4e1c,16r51ac,16r8463,16r61c2,16r52a8,
+16r680b,16r4f97,16r606b,16r51bb,16r6d1e,16r515c,16r6296,16r6597,
+16r9661,16r8c46,16r9017,16r75d8,16r90fd,16r7763,16r6bd2,16r728a,
+16r72ec,16r8bfb,16r5835,16r7779,16r8d4c,16r675c,16r9540,16r809a,
+16r5ea6,16r6e21,16r5992,16r7aef,16r77ed,16r953b,16r6bb5,16r65ad,
+16r7f0e,16r5806,16r5151,16r961f,16r5bf9,16r58a9,16r5428,16r8e72,
+16r6566,16r987f,16r56e4,16r949d,16r76fe,16r9041,16r6387,16r54c6,
+16r591a,16r593a,16r579b,16r8eb2,16r6735,16r8dfa,16r8235,16r5241,
+16r60f0,16r5815,16r86fe,16r5ce8,16r9e45,16r4fc4,16r989d,16r8bb9,
+16r5a25,16r6076,16r5384,16r627c,16r904f,16r9102,16r997f,16r6069,
+16r800c,16r513f,16r8033,16r5c14,16r9975,16r6d31,16r4e8c,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r8d30,16r53d1,16r7f5a,
+16r7b4f,16r4f10,16r4e4f,16r9600,16r6cd5,16r73d0,16r85e9,16r5e06,
+16r756a,16r7ffb,16r6a0a,16r77fe,16r9492,16r7e41,16r51e1,16r70e6,
+16r53cd,16r8fd4,16r8303,16r8d29,16r72af,16r996d,16r6cdb,16r574a,
+16r82b3,16r65b9,16r80aa,16r623f,16r9632,16r59a8,16r4eff,16r8bbf,
+16r7eba,16r653e,16r83f2,16r975e,16r5561,16r98de,16r80a5,16r532a,
+16r8bfd,16r5420,16r80ba,16r5e9f,16r6cb8,16r8d39,16r82ac,16r915a,
+16r5429,16r6c1b,16r5206,16r7eb7,16r575f,16r711a,16r6c7e,16r7c89,
+16r594b,16r4efd,16r5fff,16r6124,16r7caa,16r4e30,16r5c01,16r67ab,
+16r8702,16r5cf0,16r950b,16r98ce,16r75af,16r70fd,16r9022,16r51af,
+16r7f1d,16r8bbd,16r5949,16r51e4,16r4f5b,16r5426,16r592b,16r6577,
+16r80a4,16r5b75,16r6276,16r62c2,16r8f90,16r5e45,16r6c1f,16r7b26,
+16r4f0f,16r4fd8,16r670d,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r6d6e,16r6daa,16r798f,16r88b1,16r5f17,16r752b,16r629a,
+16r8f85,16r4fef,16r91dc,16r65a7,16r812f,16r8151,16r5e9c,16r8150,
+16r8d74,16r526f,16r8986,16r8d4b,16r590d,16r5085,16r4ed8,16r961c,
+16r7236,16r8179,16r8d1f,16r5bcc,16r8ba3,16r9644,16r5987,16r7f1a,
+16r5490,16r5676,16r560e,16r8be5,16r6539,16r6982,16r9499,16r76d6,
+16r6e89,16r5e72,16r7518,16r6746,16r67d1,16r7aff,16r809d,16r8d76,
+16r611f,16r79c6,16r6562,16r8d63,16r5188,16r521a,16r94a2,16r7f38,
+16r809b,16r7eb2,16r5c97,16r6e2f,16r6760,16r7bd9,16r768b,16r9ad8,
+16r818f,16r7f94,16r7cd5,16r641e,16r9550,16r7a3f,16r544a,16r54e5,
+16r6b4c,16r6401,16r6208,16r9e3d,16r80f3,16r7599,16r5272,16r9769,
+16r845b,16r683c,16r86e4,16r9601,16r9694,16r94ec,16r4e2a,16r5404,
+16r7ed9,16r6839,16r8ddf,16r8015,16r66f4,16r5e9a,16r7fb9,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r57c2,16r803f,16r6897,
+16r5de5,16r653b,16r529f,16r606d,16r9f9a,16r4f9b,16r8eac,16r516c,
+16r5bab,16r5f13,16r5de9,16r6c5e,16r62f1,16r8d21,16r5171,16r94a9,
+16r52fe,16r6c9f,16r82df,16r72d7,16r57a2,16r6784,16r8d2d,16r591f,
+16r8f9c,16r83c7,16r5495,16r7b8d,16r4f30,16r6cbd,16r5b64,16r59d1,
+16r9f13,16r53e4,16r86ca,16r9aa8,16r8c37,16r80a1,16r6545,16r987e,
+16r56fa,16r96c7,16r522e,16r74dc,16r5250,16r5be1,16r6302,16r8902,
+16r4e56,16r62d0,16r602a,16r68fa,16r5173,16r5b98,16r51a0,16r89c2,
+16r7ba1,16r9986,16r7f50,16r60ef,16r704c,16r8d2f,16r5149,16r5e7f,
+16r901b,16r7470,16r89c4,16r572d,16r7845,16r5f52,16r9f9f,16r95fa,
+16r8f68,16r9b3c,16r8be1,16r7678,16r6842,16r67dc,16r8dea,16r8d35,
+16r523d,16r8f8a,16r6eda,16r68cd,16r9505,16r90ed,16r56fd,16r679c,
+16r88f9,16r8fc7,16r54c8,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r9ab8,16r5b69,16r6d77,16r6c26,16r4ea5,16r5bb3,16r9a87,
+16r9163,16r61a8,16r90af,16r97e9,16r542b,16r6db5,16r5bd2,16r51fd,
+16r558a,16r7f55,16r7ff0,16r64bc,16r634d,16r65f1,16r61be,16r608d,
+16r710a,16r6c57,16r6c49,16r592f,16r676d,16r822a,16r58d5,16r568e,
+16r8c6a,16r6beb,16r90dd,16r597d,16r8017,16r53f7,16r6d69,16r5475,
+16r559d,16r8377,16r83cf,16r6838,16r79be,16r548c,16r4f55,16r5408,
+16r76d2,16r8c89,16r9602,16r6cb3,16r6db8,16r8d6b,16r8910,16r9e64,
+16r8d3a,16r563f,16r9ed1,16r75d5,16r5f88,16r72e0,16r6068,16r54fc,
+16r4ea8,16r6a2a,16r8861,16r6052,16r8f70,16r54c4,16r70d8,16r8679,
+16r9e3f,16r6d2a,16r5b8f,16r5f18,16r7ea2,16r5589,16r4faf,16r7334,
+16r543c,16r539a,16r5019,16r540e,16r547c,16r4e4e,16r5ffd,16r745a,
+16r58f6,16r846b,16r80e1,16r8774,16r72d0,16r7cca,16r6e56,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r5f27,16r864e,16r552c,
+16r62a4,16r4e92,16r6caa,16r6237,16r82b1,16r54d7,16r534e,16r733e,
+16r6ed1,16r753b,16r5212,16r5316,16r8bdd,16r69d0,16r5f8a,16r6000,
+16r6dee,16r574f,16r6b22,16r73af,16r6853,16r8fd8,16r7f13,16r6362,
+16r60a3,16r5524,16r75ea,16r8c62,16r7115,16r6da3,16r5ba6,16r5e7b,
+16r8352,16r614c,16r9ec4,16r78fa,16r8757,16r7c27,16r7687,16r51f0,
+16r60f6,16r714c,16r6643,16r5e4c,16r604d,16r8c0e,16r7070,16r6325,
+16r8f89,16r5fbd,16r6062,16r86d4,16r56de,16r6bc1,16r6094,16r6167,
+16r5349,16r60e0,16r6666,16r8d3f,16r79fd,16r4f1a,16r70e9,16r6c47,
+16r8bb3,16r8bf2,16r7ed8,16r8364,16r660f,16r5a5a,16r9b42,16r6d51,
+16r6df7,16r8c41,16r6d3b,16r4f19,16r706b,16r83b7,16r6216,16r60d1,
+16r970d,16r8d27,16r7978,16r51fb,16r573e,16r57fa,16r673a,16r7578,
+16r7a3d,16r79ef,16r7b95,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r808c,16r9965,16r8ff9,16r6fc0,16r8ba5,16r9e21,16r59ec,
+16r7ee9,16r7f09,16r5409,16r6781,16r68d8,16r8f91,16r7c4d,16r96c6,
+16r53ca,16r6025,16r75be,16r6c72,16r5373,16r5ac9,16r7ea7,16r6324,
+16r51e0,16r810a,16r5df1,16r84df,16r6280,16r5180,16r5b63,16r4f0e,
+16r796d,16r5242,16r60b8,16r6d4e,16r5bc4,16r5bc2,16r8ba1,16r8bb0,
+16r65e2,16r5fcc,16r9645,16r5993,16r7ee7,16r7eaa,16r5609,16r67b7,
+16r5939,16r4f73,16r5bb6,16r52a0,16r835a,16r988a,16r8d3e,16r7532,
+16r94be,16r5047,16r7a3c,16r4ef7,16r67b6,16r9a7e,16r5ac1,16r6b7c,
+16r76d1,16r575a,16r5c16,16r7b3a,16r95f4,16r714e,16r517c,16r80a9,
+16r8270,16r5978,16r7f04,16r8327,16r68c0,16r67ec,16r78b1,16r7877,
+16r62e3,16r6361,16r7b80,16r4fed,16r526a,16r51cf,16r8350,16r69db,
+16r9274,16r8df5,16r8d31,16r89c1,16r952e,16r7bad,16r4ef6,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r5065,16r8230,16r5251,
+16r996f,16r6e10,16r6e85,16r6da7,16r5efa,16r50f5,16r59dc,16r5c06,
+16r6d46,16r6c5f,16r7586,16r848b,16r6868,16r5956,16r8bb2,16r5320,
+16r9171,16r964d,16r8549,16r6912,16r7901,16r7126,16r80f6,16r4ea4,
+16r90ca,16r6d47,16r9a84,16r5a07,16r56bc,16r6405,16r94f0,16r77eb,
+16r4fa5,16r811a,16r72e1,16r89d2,16r997a,16r7f34,16r7ede,16r527f,
+16r6559,16r9175,16r8f7f,16r8f83,16r53eb,16r7a96,16r63ed,16r63a5,
+16r7686,16r79f8,16r8857,16r9636,16r622a,16r52ab,16r8282,16r6854,
+16r6770,16r6377,16r776b,16r7aed,16r6d01,16r7ed3,16r89e3,16r59d0,
+16r6212,16r85c9,16r82a5,16r754c,16r501f,16r4ecb,16r75a5,16r8beb,
+16r5c4a,16r5dfe,16r7b4b,16r65a4,16r91d1,16r4eca,16r6d25,16r895f,
+16r7d27,16r9526,16r4ec5,16r8c28,16r8fdb,16r9773,16r664b,16r7981,
+16r8fd1,16r70ec,16r6d78,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r5c3d,16r52b2,16r8346,16r5162,16r830e,16r775b,16r6676,
+16r9cb8,16r4eac,16r60ca,16r7cbe,16r7cb3,16r7ecf,16r4e95,16r8b66,
+16r666f,16r9888,16r9759,16r5883,16r656c,16r955c,16r5f84,16r75c9,
+16r9756,16r7adf,16r7ade,16r51c0,16r70af,16r7a98,16r63ea,16r7a76,
+16r7ea0,16r7396,16r97ed,16r4e45,16r7078,16r4e5d,16r9152,16r53a9,
+16r6551,16r65e7,16r81fc,16r8205,16r548e,16r5c31,16r759a,16r97a0,
+16r62d8,16r72d9,16r75bd,16r5c45,16r9a79,16r83ca,16r5c40,16r5480,
+16r77e9,16r4e3e,16r6cae,16r805a,16r62d2,16r636e,16r5de8,16r5177,
+16r8ddd,16r8e1e,16r952f,16r4ff1,16r53e5,16r60e7,16r70ac,16r5267,
+16r6350,16r9e43,16r5a1f,16r5026,16r7737,16r5377,16r7ee2,16r6485,
+16r652b,16r6289,16r6398,16r5014,16r7235,16r89c9,16r51b3,16r8bc0,
+16r7edd,16r5747,16r83cc,16r94a7,16r519b,16r541b,16r5cfb,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r4fca,16r7ae3,16r6d5a,
+16r90e1,16r9a8f,16r5580,16r5496,16r5361,16r54af,16r5f00,16r63e9,
+16r6977,16r51ef,16r6168,16r520a,16r582a,16r52d8,16r574e,16r780d,
+16r770b,16r5eb7,16r6177,16r7ce0,16r625b,16r6297,16r4ea2,16r7095,
+16r8003,16r62f7,16r70e4,16r9760,16r5777,16r82db,16r67ef,16r68f5,
+16r78d5,16r9897,16r79d1,16r58f3,16r54b3,16r53ef,16r6e34,16r514b,
+16r523b,16r5ba2,16r8bfe,16r80af,16r5543,16r57a6,16r6073,16r5751,
+16r542d,16r7a7a,16r6050,16r5b54,16r63a7,16r62a0,16r53e3,16r6263,
+16r5bc7,16r67af,16r54ed,16r7a9f,16r82e6,16r9177,16r5e93,16r88e4,
+16r5938,16r57ae,16r630e,16r8de8,16r80ef,16r5757,16r7b77,16r4fa9,
+16r5feb,16r5bbd,16r6b3e,16r5321,16r7b50,16r72c2,16r6846,16r77ff,
+16r7736,16r65f7,16r51b5,16r4e8f,16r76d4,16r5cbf,16r7aa5,16r8475,
+16r594e,16r9b41,16r5080,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r9988,16r6127,16r6e83,16r5764,16r6606,16r6346,16r56f0,
+16r62ec,16r6269,16r5ed3,16r9614,16r5783,16r62c9,16r5587,16r8721,
+16r814a,16r8fa3,16r5566,16r83b1,16r6765,16r8d56,16r84dd,16r5a6a,
+16r680f,16r62e6,16r7bee,16r9611,16r5170,16r6f9c,16r8c30,16r63fd,
+16r89c8,16r61d2,16r7f06,16r70c2,16r6ee5,16r7405,16r6994,16r72fc,
+16r5eca,16r90ce,16r6717,16r6d6a,16r635e,16r52b3,16r7262,16r8001,
+16r4f6c,16r59e5,16r916a,16r70d9,16r6d9d,16r52d2,16r4e50,16r96f7,
+16r956d,16r857e,16r78ca,16r7d2f,16r5121,16r5792,16r64c2,16r808b,
+16r7c7b,16r6cea,16r68f1,16r695e,16r51b7,16r5398,16r68a8,16r7281,
+16r9ece,16r7bf1,16r72f8,16r79bb,16r6f13,16r7406,16r674e,16r91cc,
+16r9ca4,16r793c,16r8389,16r8354,16r540f,16r6817,16r4e3d,16r5389,
+16r52b1,16r783e,16r5386,16r5229,16r5088,16r4f8b,16r4fd0,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r75e2,16r7acb,16r7c92,
+16r6ca5,16r96b6,16r529b,16r7483,16r54e9,16r4fe9,16r8054,16r83b2,
+16r8fde,16r9570,16r5ec9,16r601c,16r6d9f,16r5e18,16r655b,16r8138,
+16r94fe,16r604b,16r70bc,16r7ec3,16r7cae,16r51c9,16r6881,16r7cb1,
+16r826f,16r4e24,16r8f86,16r91cf,16r667e,16r4eae,16r8c05,16r64a9,
+16r804a,16r50da,16r7597,16r71ce,16r5be5,16r8fbd,16r6f66,16r4e86,
+16r6482,16r9563,16r5ed6,16r6599,16r5217,16r88c2,16r70c8,16r52a3,
+16r730e,16r7433,16r6797,16r78f7,16r9716,16r4e34,16r90bb,16r9cde,
+16r6dcb,16r51db,16r8d41,16r541d,16r62ce,16r73b2,16r83f1,16r96f6,
+16r9f84,16r94c3,16r4f36,16r7f9a,16r51cc,16r7075,16r9675,16r5cad,
+16r9886,16r53e6,16r4ee4,16r6e9c,16r7409,16r69b4,16r786b,16r998f,
+16r7559,16r5218,16r7624,16r6d41,16r67f3,16r516d,16r9f99,16r804b,
+16r5499,16r7b3c,16r7abf,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r9686,16r5784,16r62e2,16r9647,16r697c,16r5a04,16r6402,
+16r7bd3,16r6f0f,16r964b,16r82a6,16r5362,16r9885,16r5e90,16r7089,
+16r63b3,16r5364,16r864f,16r9c81,16r9e93,16r788c,16r9732,16r8def,
+16r8d42,16r9e7f,16r6f5e,16r7984,16r5f55,16r9646,16r622e,16r9a74,
+16r5415,16r94dd,16r4fa3,16r65c5,16r5c65,16r5c61,16r7f15,16r8651,
+16r6c2f,16r5f8b,16r7387,16r6ee4,16r7eff,16r5ce6,16r631b,16r5b6a,
+16r6ee6,16r5375,16r4e71,16r63a0,16r7565,16r62a1,16r8f6e,16r4f26,
+16r4ed1,16r6ca6,16r7eb6,16r8bba,16r841d,16r87ba,16r7f57,16r903b,
+16r9523,16r7ba9,16r9aa1,16r88f8,16r843d,16r6d1b,16r9a86,16r7edc,
+16r5988,16r9ebb,16r739b,16r7801,16r8682,16r9a6c,16r9a82,16r561b,
+16r5417,16r57cb,16r4e70,16r9ea6,16r5356,16r8fc8,16r8109,16r7792,
+16r9992,16r86ee,16r6ee1,16r8513,16r66fc,16r6162,16r6f2b,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r8c29,16r8292,16r832b,
+16r76f2,16r6c13,16r5fd9,16r83bd,16r732b,16r8305,16r951a,16r6bdb,
+16r77db,16r94c6,16r536f,16r8302,16r5192,16r5e3d,16r8c8c,16r8d38,
+16r4e48,16r73ab,16r679a,16r6885,16r9176,16r9709,16r7164,16r6ca1,
+16r7709,16r5a92,16r9541,16r6bcf,16r7f8e,16r6627,16r5bd0,16r59b9,
+16r5a9a,16r95e8,16r95f7,16r4eec,16r840c,16r8499,16r6aac,16r76df,
+16r9530,16r731b,16r68a6,16r5b5f,16r772f,16r919a,16r9761,16r7cdc,
+16r8ff7,16r8c1c,16r5f25,16r7c73,16r79d8,16r89c5,16r6ccc,16r871c,
+16r5bc6,16r5e42,16r68c9,16r7720,16r7ef5,16r5195,16r514d,16r52c9,
+16r5a29,16r7f05,16r9762,16r82d7,16r63cf,16r7784,16r85d0,16r79d2,
+16r6e3a,16r5e99,16r5999,16r8511,16r706d,16r6c11,16r62bf,16r76bf,
+16r654f,16r60af,16r95fd,16r660e,16r879f,16r9e23,16r94ed,16r540d,
+16r547d,16r8c2c,16r6478,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r6479,16r8611,16r6a21,16r819c,16r78e8,16r6469,16r9b54,
+16r62b9,16r672b,16r83ab,16r58a8,16r9ed8,16r6cab,16r6f20,16r5bde,
+16r964c,16r8c0b,16r725f,16r67d0,16r62c7,16r7261,16r4ea9,16r59c6,
+16r6bcd,16r5893,16r66ae,16r5e55,16r52df,16r6155,16r6728,16r76ee,
+16r7766,16r7267,16r7a46,16r62ff,16r54ea,16r5450,16r94a0,16r90a3,
+16r5a1c,16r7eb3,16r6c16,16r4e43,16r5976,16r8010,16r5948,16r5357,
+16r7537,16r96be,16r56ca,16r6320,16r8111,16r607c,16r95f9,16r6dd6,
+16r5462,16r9981,16r5185,16r5ae9,16r80fd,16r59ae,16r9713,16r502a,
+16r6ce5,16r5c3c,16r62df,16r4f60,16r533f,16r817b,16r9006,16r6eba,
+16r852b,16r62c8,16r5e74,16r78be,16r64b5,16r637b,16r5ff5,16r5a18,
+16r917f,16r9e1f,16r5c3f,16r634f,16r8042,16r5b7d,16r556e,16r954a,
+16r954d,16r6d85,16r60a8,16r67e0,16r72de,16r51dd,16r5b81,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r62e7,16r6cde,16r725b,
+16r626d,16r94ae,16r7ebd,16r8113,16r6d53,16r519c,16r5f04,16r5974,
+16r52aa,16r6012,16r5973,16r6696,16r8650,16r759f,16r632a,16r61e6,
+16r7cef,16r8bfa,16r54e6,16r6b27,16r9e25,16r6bb4,16r85d5,16r5455,
+16r5076,16r6ca4,16r556a,16r8db4,16r722c,16r5e15,16r6015,16r7436,
+16r62cd,16r6392,16r724c,16r5f98,16r6e43,16r6d3e,16r6500,16r6f58,
+16r76d8,16r78d0,16r76fc,16r7554,16r5224,16r53db,16r4e53,16r5e9e,
+16r65c1,16r802a,16r80d6,16r629b,16r5486,16r5228,16r70ae,16r888d,
+16r8dd1,16r6ce1,16r5478,16r80da,16r57f9,16r88f4,16r8d54,16r966a,
+16r914d,16r4f69,16r6c9b,16r55b7,16r76c6,16r7830,16r62a8,16r70f9,
+16r6f8e,16r5f6d,16r84ec,16r68da,16r787c,16r7bf7,16r81a8,16r670b,
+16r9e4f,16r6367,16r78b0,16r576f,16r7812,16r9739,16r6279,16r62ab,
+16r5288,16r7435,16r6bd7,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r5564,16r813e,16r75b2,16r76ae,16r5339,16r75de,16r50fb,
+16r5c41,16r8b6c,16r7bc7,16r504f,16r7247,16r9a97,16r98d8,16r6f02,
+16r74e2,16r7968,16r6487,16r77a5,16r62fc,16r9891,16r8d2b,16r54c1,
+16r8058,16r4e52,16r576a,16r82f9,16r840d,16r5e73,16r51ed,16r74f6,
+16r8bc4,16r5c4f,16r5761,16r6cfc,16r9887,16r5a46,16r7834,16r9b44,
+16r8feb,16r7c95,16r5256,16r6251,16r94fa,16r4ec6,16r8386,16r8461,
+16r83e9,16r84b2,16r57d4,16r6734,16r5703,16r666e,16r6d66,16r8c31,
+16r66dd,16r7011,16r671f,16r6b3a,16r6816,16r621a,16r59bb,16r4e03,
+16r51c4,16r6f06,16r67d2,16r6c8f,16r5176,16r68cb,16r5947,16r6b67,
+16r7566,16r5d0e,16r8110,16r9f50,16r65d7,16r7948,16r7941,16r9a91,
+16r8d77,16r5c82,16r4e5e,16r4f01,16r542f,16r5951,16r780c,16r5668,
+16r6c14,16r8fc4,16r5f03,16r6c7d,16r6ce3,16r8bab,16r6390,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r6070,16r6d3d,16r7275,
+16r6266,16r948e,16r94c5,16r5343,16r8fc1,16r7b7e,16r4edf,16r8c26,
+16r4e7e,16r9ed4,16r94b1,16r94b3,16r524d,16r6f5c,16r9063,16r6d45,
+16r8c34,16r5811,16r5d4c,16r6b20,16r6b49,16r67aa,16r545b,16r8154,
+16r7f8c,16r5899,16r8537,16r5f3a,16r62a2,16r6a47,16r9539,16r6572,
+16r6084,16r6865,16r77a7,16r4e54,16r4fa8,16r5de7,16r9798,16r64ac,
+16r7fd8,16r5ced,16r4fcf,16r7a8d,16r5207,16r8304,16r4e14,16r602f,
+16r7a83,16r94a6,16r4fb5,16r4eb2,16r79e6,16r7434,16r52e4,16r82b9,
+16r64d2,16r79bd,16r5bdd,16r6c81,16r9752,16r8f7b,16r6c22,16r503e,
+16r537f,16r6e05,16r64ce,16r6674,16r6c30,16r60c5,16r9877,16r8bf7,
+16r5e86,16r743c,16r7a77,16r79cb,16r4e18,16r90b1,16r7403,16r6c42,
+16r56da,16r914b,16r6cc5,16r8d8b,16r533a,16r86c6,16r66f2,16r8eaf,
+16r5c48,16r9a71,16r6e20,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r53d6,16r5a36,16r9f8b,16r8da3,16r53bb,16r5708,16r98a7,
+16r6743,16r919b,16r6cc9,16r5168,16r75ca,16r62f3,16r72ac,16r5238,
+16r529d,16r7f3a,16r7094,16r7638,16r5374,16r9e4a,16r69b7,16r786e,
+16r96c0,16r88d9,16r7fa4,16r7136,16r71c3,16r5189,16r67d3,16r74e4,
+16r58e4,16r6518,16r56b7,16r8ba9,16r9976,16r6270,16r7ed5,16r60f9,
+16r70ed,16r58ec,16r4ec1,16r4eba,16r5fcd,16r97e7,16r4efb,16r8ba4,
+16r5203,16r598a,16r7eab,16r6254,16r4ecd,16r65e5,16r620e,16r8338,
+16r84c9,16r8363,16r878d,16r7194,16r6eb6,16r5bb9,16r7ed2,16r5197,
+16r63c9,16r67d4,16r8089,16r8339,16r8815,16r5112,16r5b7a,16r5982,
+16r8fb1,16r4e73,16r6c5d,16r5165,16r8925,16r8f6f,16r962e,16r854a,
+16r745e,16r9510,16r95f0,16r6da6,16r82e5,16r5f31,16r6492,16r6d12,
+16r8428,16r816e,16r9cc3,16r585e,16r8d5b,16r4e09,16r53c1,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r4f1e,16r6563,16r6851,
+16r55d3,16r4e27,16r6414,16r9a9a,16r626b,16r5ac2,16r745f,16r8272,
+16r6da9,16r68ee,16r50e7,16r838e,16r7802,16r6740,16r5239,16r6c99,
+16r7eb1,16r50bb,16r5565,16r715e,16r7b5b,16r6652,16r73ca,16r82eb,
+16r6749,16r5c71,16r5220,16r717d,16r886b,16r95ea,16r9655,16r64c5,
+16r8d61,16r81b3,16r5584,16r6c55,16r6247,16r7f2e,16r5892,16r4f24,
+16r5546,16r8d4f,16r664c,16r4e0a,16r5c1a,16r88f3,16r68a2,16r634e,
+16r7a0d,16r70e7,16r828d,16r52fa,16r97f6,16r5c11,16r54e8,16r90b5,
+16r7ecd,16r5962,16r8d4a,16r86c7,16r820c,16r820d,16r8d66,16r6444,
+16r5c04,16r6151,16r6d89,16r793e,16r8bbe,16r7837,16r7533,16r547b,
+16r4f38,16r8eab,16r6df1,16r5a20,16r7ec5,16r795e,16r6c88,16r5ba1,
+16r5a76,16r751a,16r80be,16r614e,16r6e17,16r58f0,16r751f,16r7525,
+16r7272,16r5347,16r7ef3,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r7701,16r76db,16r5269,16r80dc,16r5723,16r5e08,16r5931,
+16r72ee,16r65bd,16r6e7f,16r8bd7,16r5c38,16r8671,16r5341,16r77f3,
+16r62fe,16r65f6,16r4ec0,16r98df,16r8680,16r5b9e,16r8bc6,16r53f2,
+16r77e2,16r4f7f,16r5c4e,16r9a76,16r59cb,16r5f0f,16r793a,16r58eb,
+16r4e16,16r67ff,16r4e8b,16r62ed,16r8a93,16r901d,16r52bf,16r662f,
+16r55dc,16r566c,16r9002,16r4ed5,16r4f8d,16r91ca,16r9970,16r6c0f,
+16r5e02,16r6043,16r5ba4,16r89c6,16r8bd5,16r6536,16r624b,16r9996,
+16r5b88,16r5bff,16r6388,16r552e,16r53d7,16r7626,16r517d,16r852c,
+16r67a2,16r68b3,16r6b8a,16r6292,16r8f93,16r53d4,16r8212,16r6dd1,
+16r758f,16r4e66,16r8d4e,16r5b70,16r719f,16r85af,16r6691,16r66d9,
+16r7f72,16r8700,16r9ecd,16r9f20,16r5c5e,16r672f,16r8ff0,16r6811,
+16r675f,16r620d,16r7ad6,16r5885,16r5eb6,16r6570,16r6f31,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r6055,16r5237,16r800d,
+16r6454,16r8870,16r7529,16r5e05,16r6813,16r62f4,16r971c,16r53cc,
+16r723d,16r8c01,16r6c34,16r7761,16r7a0e,16r542e,16r77ac,16r987a,
+16r821c,16r8bf4,16r7855,16r6714,16r70c1,16r65af,16r6495,16r5636,
+16r601d,16r79c1,16r53f8,16r4e1d,16r6b7b,16r8086,16r5bfa,16r55e3,
+16r56db,16r4f3a,16r4f3c,16r9972,16r5df3,16r677e,16r8038,16r6002,
+16r9882,16r9001,16r5b8b,16r8bbc,16r8bf5,16r641c,16r8258,16r64de,
+16r55fd,16r82cf,16r9165,16r4fd7,16r7d20,16r901f,16r7c9f,16r50f3,
+16r5851,16r6eaf,16r5bbf,16r8bc9,16r8083,16r9178,16r849c,16r7b97,
+16r867d,16r968b,16r968f,16r7ee5,16r9ad3,16r788e,16r5c81,16r7a57,
+16r9042,16r96a7,16r795f,16r5b59,16r635f,16r7b0b,16r84d1,16r68ad,
+16r5506,16r7f29,16r7410,16r7d22,16r9501,16r6240,16r584c,16r4ed6,
+16r5b83,16r5979,16r5854,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r736d,16r631e,16r8e4b,16r8e0f,16r80ce,16r82d4,16r62ac,
+16r53f0,16r6cf0,16r915e,16r592a,16r6001,16r6c70,16r574d,16r644a,
+16r8d2a,16r762b,16r6ee9,16r575b,16r6a80,16r75f0,16r6f6d,16r8c2d,
+16r8c08,16r5766,16r6bef,16r8892,16r78b3,16r63a2,16r53f9,16r70ad,
+16r6c64,16r5858,16r642a,16r5802,16r68e0,16r819b,16r5510,16r7cd6,
+16r5018,16r8eba,16r6dcc,16r8d9f,16r70eb,16r638f,16r6d9b,16r6ed4,
+16r7ee6,16r8404,16r6843,16r9003,16r6dd8,16r9676,16r8ba8,16r5957,
+16r7279,16r85e4,16r817e,16r75bc,16r8a8a,16r68af,16r5254,16r8e22,
+16r9511,16r63d0,16r9898,16r8e44,16r557c,16r4f53,16r66ff,16r568f,
+16r60d5,16r6d95,16r5243,16r5c49,16r5929,16r6dfb,16r586b,16r7530,
+16r751c,16r606c,16r8214,16r8146,16r6311,16r6761,16r8fe2,16r773a,
+16r8df3,16r8d34,16r94c1,16r5e16,16r5385,16r542c,16r70c3,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r6c40,16r5ef7,16r505c,
+16r4ead,16r5ead,16r633a,16r8247,16r901a,16r6850,16r916e,16r77b3,
+16r540c,16r94dc,16r5f64,16r7ae5,16r6876,16r6345,16r7b52,16r7edf,
+16r75db,16r5077,16r6295,16r5934,16r900f,16r51f8,16r79c3,16r7a81,
+16r56fe,16r5f92,16r9014,16r6d82,16r5c60,16r571f,16r5410,16r5154,
+16r6e4d,16r56e2,16r63a8,16r9893,16r817f,16r8715,16r892a,16r9000,
+16r541e,16r5c6f,16r81c0,16r62d6,16r6258,16r8131,16r9e35,16r9640,
+16r9a6e,16r9a7c,16r692d,16r59a5,16r62d3,16r553e,16r6316,16r54c7,
+16r86d9,16r6d3c,16r5a03,16r74e6,16r889c,16r6b6a,16r5916,16r8c4c,
+16r5f2f,16r6e7e,16r73a9,16r987d,16r4e38,16r70f7,16r5b8c,16r7897,
+16r633d,16r665a,16r7696,16r60cb,16r5b9b,16r5a49,16r4e07,16r8155,
+16r6c6a,16r738b,16r4ea1,16r6789,16r7f51,16r5f80,16r65fa,16r671b,
+16r5fd8,16r5984,16r5a01,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r5dcd,16r5fae,16r5371,16r97e6,16r8fdd,16r6845,16r56f4,
+16r552f,16r60df,16r4e3a,16r6f4d,16r7ef4,16r82c7,16r840e,16r59d4,
+16r4f1f,16r4f2a,16r5c3e,16r7eac,16r672a,16r851a,16r5473,16r754f,
+16r80c3,16r5582,16r9b4f,16r4f4d,16r6e2d,16r8c13,16r5c09,16r6170,
+16r536b,16r761f,16r6e29,16r868a,16r6587,16r95fb,16r7eb9,16r543b,
+16r7a33,16r7d0a,16r95ee,16r55e1,16r7fc1,16r74ee,16r631d,16r8717,
+16r6da1,16r7a9d,16r6211,16r65a1,16r5367,16r63e1,16r6c83,16r5deb,
+16r545c,16r94a8,16r4e4c,16r6c61,16r8bec,16r5c4b,16r65e0,16r829c,
+16r68a7,16r543e,16r5434,16r6bcb,16r6b66,16r4e94,16r6342,16r5348,
+16r821e,16r4f0d,16r4fae,16r575e,16r620a,16r96fe,16r6664,16r7269,
+16r52ff,16r52a1,16r609f,16r8bef,16r6614,16r7199,16r6790,16r897f,
+16r7852,16r77fd,16r6670,16r563b,16r5438,16r9521,16r727a,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r7a00,16r606f,16r5e0c,
+16r6089,16r819d,16r5915,16r60dc,16r7184,16r70ef,16r6eaa,16r6c50,
+16r7280,16r6a84,16r88ad,16r5e2d,16r4e60,16r5ab3,16r559c,16r94e3,
+16r6d17,16r7cfb,16r9699,16r620f,16r7ec6,16r778e,16r867e,16r5323,
+16r971e,16r8f96,16r6687,16r5ce1,16r4fa0,16r72ed,16r4e0b,16r53a6,
+16r590f,16r5413,16r6380,16r9528,16r5148,16r4ed9,16r9c9c,16r7ea4,
+16r54b8,16r8d24,16r8854,16r8237,16r95f2,16r6d8e,16r5f26,16r5acc,
+16r663e,16r9669,16r73b0,16r732e,16r53bf,16r817a,16r9985,16r7fa1,
+16r5baa,16r9677,16r9650,16r7ebf,16r76f8,16r53a2,16r9576,16r9999,
+16r7bb1,16r8944,16r6e58,16r4e61,16r7fd4,16r7965,16r8be6,16r60f3,
+16r54cd,16r4eab,16r9879,16r5df7,16r6a61,16r50cf,16r5411,16r8c61,
+16r8427,16r785d,16r9704,16r524a,16r54ee,16r56a3,16r9500,16r6d88,
+16r5bb5,16r6dc6,16r6653,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r5c0f,16r5b5d,16r6821,16r8096,16r5578,16r7b11,16r6548,
+16r6954,16r4e9b,16r6b47,16r874e,16r978b,16r534f,16r631f,16r643a,
+16r90aa,16r659c,16r80c1,16r8c10,16r5199,16r68b0,16r5378,16r87f9,
+16r61c8,16r6cc4,16r6cfb,16r8c22,16r5c51,16r85aa,16r82af,16r950c,
+16r6b23,16r8f9b,16r65b0,16r5ffb,16r5fc3,16r4fe1,16r8845,16r661f,
+16r8165,16r7329,16r60fa,16r5174,16r5211,16r578b,16r5f62,16r90a2,
+16r884c,16r9192,16r5e78,16r674f,16r6027,16r59d3,16r5144,16r51f6,
+16r80f8,16r5308,16r6c79,16r96c4,16r718a,16r4f11,16r4fee,16r7f9e,
+16r673d,16r55c5,16r9508,16r79c0,16r8896,16r7ee3,16r589f,16r620c,
+16r9700,16r865a,16r5618,16r987b,16r5f90,16r8bb8,16r84c4,16r9157,
+16r53d9,16r65ed,16r5e8f,16r755c,16r6064,16r7d6e,16r5a7f,16r7eea,
+16r7eed,16r8f69,16r55a7,16r5ba3,16r60ac,16r65cb,16r7384,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r9009,16r7663,16r7729,
+16r7eda,16r9774,16r859b,16r5b66,16r7a74,16r96ea,16r8840,16r52cb,
+16r718f,16r5faa,16r65ec,16r8be2,16r5bfb,16r9a6f,16r5de1,16r6b89,
+16r6c5b,16r8bad,16r8baf,16r900a,16r8fc5,16r538b,16r62bc,16r9e26,
+16r9e2d,16r5440,16r4e2b,16r82bd,16r7259,16r869c,16r5d16,16r8859,
+16r6daf,16r96c5,16r54d1,16r4e9a,16r8bb6,16r7109,16r54bd,16r9609,
+16r70df,16r6df9,16r76d0,16r4e25,16r7814,16r8712,16r5ca9,16r5ef6,
+16r8a00,16r989c,16r960e,16r708e,16r6cbf,16r5944,16r63a9,16r773c,
+16r884d,16r6f14,16r8273,16r5830,16r71d5,16r538c,16r781a,16r96c1,
+16r5501,16r5f66,16r7130,16r5bb4,16r8c1a,16r9a8c,16r6b83,16r592e,
+16r9e2f,16r79e7,16r6768,16r626c,16r4f6f,16r75a1,16r7f8a,16r6d0b,
+16r9633,16r6c27,16r4ef0,16r75d2,16r517b,16r6837,16r6f3e,16r9080,
+16r8170,16r5996,16r7476,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r6447,16r5c27,16r9065,16r7a91,16r8c23,16r59da,16r54ac,
+16r8200,16r836f,16r8981,16r8000,16r6930,16r564e,16r8036,16r7237,
+16r91ce,16r51b6,16r4e5f,16r9875,16r6396,16r4e1a,16r53f6,16r66f3,
+16r814b,16r591c,16r6db2,16r4e00,16r58f9,16r533b,16r63d6,16r94f1,
+16r4f9d,16r4f0a,16r8863,16r9890,16r5937,16r9057,16r79fb,16r4eea,
+16r80f0,16r7591,16r6c82,16r5b9c,16r59e8,16r5f5d,16r6905,16r8681,
+16r501a,16r5df2,16r4e59,16r77e3,16r4ee5,16r827a,16r6291,16r6613,
+16r9091,16r5c79,16r4ebf,16r5f79,16r81c6,16r9038,16r8084,16r75ab,
+16r4ea6,16r88d4,16r610f,16r6bc5,16r5fc6,16r4e49,16r76ca,16r6ea2,
+16r8be3,16r8bae,16r8c0a,16r8bd1,16r5f02,16r7ffc,16r7fcc,16r7ece,
+16r8335,16r836b,16r56e0,16r6bb7,16r97f3,16r9634,16r59fb,16r541f,
+16r94f6,16r6deb,16r5bc5,16r996e,16r5c39,16r5f15,16r9690,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r5370,16r82f1,16r6a31,
+16r5a74,16r9e70,16r5e94,16r7f28,16r83b9,16r8424,16r8425,16r8367,
+16r8747,16r8fce,16r8d62,16r76c8,16r5f71,16r9896,16r786c,16r6620,
+16r54df,16r62e5,16r4f63,16r81c3,16r75c8,16r5eb8,16r96cd,16r8e0a,
+16r86f9,16r548f,16r6cf3,16r6d8c,16r6c38,16r607f,16r52c7,16r7528,
+16r5e7d,16r4f18,16r60a0,16r5fe7,16r5c24,16r7531,16r90ae,16r94c0,
+16r72b9,16r6cb9,16r6e38,16r9149,16r6709,16r53cb,16r53f3,16r4f51,
+16r91c9,16r8bf1,16r53c8,16r5e7c,16r8fc2,16r6de4,16r4e8e,16r76c2,
+16r6986,16r865e,16r611a,16r8206,16r4f59,16r4fde,16r903e,16r9c7c,
+16r6109,16r6e1d,16r6e14,16r9685,16r4e88,16r5a31,16r96e8,16r4e0e,
+16r5c7f,16r79b9,16r5b87,16r8bed,16r7fbd,16r7389,16r57df,16r828b,
+16r90c1,16r5401,16r9047,16r55bb,16r5cea,16r5fa1,16r6108,16r6b32,
+16r72f1,16r80b2,16r8a89,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r6d74,16r5bd3,16r88d5,16r9884,16r8c6b,16r9a6d,16r9e33,
+16r6e0a,16r51a4,16r5143,16r57a3,16r8881,16r539f,16r63f4,16r8f95,
+16r56ed,16r5458,16r5706,16r733f,16r6e90,16r7f18,16r8fdc,16r82d1,
+16r613f,16r6028,16r9662,16r66f0,16r7ea6,16r8d8a,16r8dc3,16r94a5,
+16r5cb3,16r7ca4,16r6708,16r60a6,16r9605,16r8018,16r4e91,16r90e7,
+16r5300,16r9668,16r5141,16r8fd0,16r8574,16r915d,16r6655,16r97f5,
+16r5b55,16r531d,16r7838,16r6742,16r683d,16r54c9,16r707e,16r5bb0,
+16r8f7d,16r518d,16r5728,16r54b1,16r6512,16r6682,16r8d5e,16r8d43,
+16r810f,16r846c,16r906d,16r7cdf,16r51ff,16r85fb,16r67a3,16r65e9,
+16r6fa1,16r86a4,16r8e81,16r566a,16r9020,16r7682,16r7076,16r71e5,
+16r8d23,16r62e9,16r5219,16r6cfd,16r8d3c,16r600e,16r589e,16r618e,
+16r66fe,16r8d60,16r624e,16r55b3,16r6e23,16r672d,16r8f67,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r94e1,16r95f8,16r7728,
+16r6805,16r69a8,16r548b,16r4e4d,16r70b8,16r8bc8,16r6458,16r658b,
+16r5b85,16r7a84,16r503a,16r5be8,16r77bb,16r6be1,16r8a79,16r7c98,
+16r6cbe,16r76cf,16r65a9,16r8f97,16r5d2d,16r5c55,16r8638,16r6808,
+16r5360,16r6218,16r7ad9,16r6e5b,16r7efd,16r6a1f,16r7ae0,16r5f70,
+16r6f33,16r5f20,16r638c,16r6da8,16r6756,16r4e08,16r5e10,16r8d26,
+16r4ed7,16r80c0,16r7634,16r969c,16r62db,16r662d,16r627e,16r6cbc,
+16r8d75,16r7167,16r7f69,16r5146,16r8087,16r53ec,16r906e,16r6298,
+16r54f2,16r86f0,16r8f99,16r8005,16r9517,16r8517,16r8fd9,16r6d59,
+16r73cd,16r659f,16r771f,16r7504,16r7827,16r81fb,16r8d1e,16r9488,
+16r4fa6,16r6795,16r75b9,16r8bca,16r9707,16r632f,16r9547,16r9635,
+16r84b8,16r6323,16r7741,16r5f81,16r72f0,16r4e89,16r6014,16r6574,
+16r62ef,16r6b63,16r653f,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r5e27,16r75c7,16r90d1,16r8bc1,16r829d,16r679d,16r652f,
+16r5431,16r8718,16r77e5,16r80a2,16r8102,16r6c41,16r4e4b,16r7ec7,
+16r804c,16r76f4,16r690d,16r6b96,16r6267,16r503c,16r4f84,16r5740,
+16r6307,16r6b62,16r8dbe,16r53ea,16r65e8,16r7eb8,16r5fd7,16r631a,
+16r63b7,16r81f3,16r81f4,16r7f6e,16r5e1c,16r5cd9,16r5236,16r667a,
+16r79e9,16r7a1a,16r8d28,16r7099,16r75d4,16r6ede,16r6cbb,16r7a92,
+16r4e2d,16r76c5,16r5fe0,16r949f,16r8877,16r7ec8,16r79cd,16r80bf,
+16r91cd,16r4ef2,16r4f17,16r821f,16r5468,16r5dde,16r6d32,16r8bcc,
+16r7ca5,16r8f74,16r8098,16r5e1a,16r5492,16r76b1,16r5b99,16r663c,
+16r9aa4,16r73e0,16r682a,16r86db,16r6731,16r732a,16r8bf8,16r8bdb,
+16r9010,16r7af9,16r70db,16r716e,16r62c4,16r77a9,16r5631,16r4e3b,
+16r8457,16r67f1,16r52a9,16r86c0,16r8d2e,16r94f8,16r7b51,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r4f4f,16r6ce8,16r795d,
+16r9a7b,16r6293,16r722a,16r62fd,16r4e13,16r7816,16r8f6c,16r64b0,
+16r8d5a,16r7bc6,16r6869,16r5e84,16r88c5,16r5986,16r649e,16r58ee,
+16r72b6,16r690e,16r9525,16r8ffd,16r8d58,16r5760,16r7f00,16r8c06,
+16r51c6,16r6349,16r62d9,16r5353,16r684c,16r7422,16r8301,16r914c,
+16r5544,16r7740,16r707c,16r6d4a,16r5179,16r54a8,16r8d44,16r59ff,
+16r6ecb,16r6dc4,16r5b5c,16r7d2b,16r4ed4,16r7c7d,16r6ed3,16r5b50,
+16r81ea,16r6e0d,16r5b57,16r9b03,16r68d5,16r8e2a,16r5b97,16r7efc,
+16r603b,16r7eb5,16r90b9,16r8d70,16r594f,16r63cd,16r79df,16r8db3,
+16r5352,16r65cf,16r7956,16r8bc5,16r963b,16r7ec4,16r94bb,16r7e82,
+16r5634,16r9189,16r6700,16r7f6a,16r5c0a,16r9075,16r6628,16r5de6,
+16r4f50,16r67de,16r505a,16r4f5c,16r5750,16r5ea7,ERRchar,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r4e8d,16r4e0c,16r5140,16r4e10,16r5eff,16r5345,16r4e15,
+16r4e98,16r4e1e,16r9b32,16r5b6c,16r5669,16r4e28,16r79ba,16r4e3f,
+16r5315,16r4e47,16r592d,16r723b,16r536e,16r6c10,16r56df,16r80e4,
+16r9997,16r6bd3,16r777e,16r9f17,16r4e36,16r4e9f,16r9f10,16r4e5c,
+16r4e69,16r4e93,16r8288,16r5b5b,16r556c,16r560f,16r4ec4,16r538d,
+16r539d,16r53a3,16r53a5,16r53ae,16r9765,16r8d5d,16r531a,16r53f5,
+16r5326,16r532e,16r533e,16r8d5c,16r5366,16r5363,16r5202,16r5208,
+16r520e,16r522d,16r5233,16r523f,16r5240,16r524c,16r525e,16r5261,
+16r525c,16r84af,16r527d,16r5282,16r5281,16r5290,16r5293,16r5182,
+16r7f54,16r4ebb,16r4ec3,16r4ec9,16r4ec2,16r4ee8,16r4ee1,16r4eeb,
+16r4ede,16r4f1b,16r4ef3,16r4f22,16r4f64,16r4ef5,16r4f25,16r4f27,
+16r4f09,16r4f2b,16r4f5e,16r4f67,16r6538,16r4f5a,16r4f5d,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r4f5f,16r4f57,16r4f32,
+16r4f3d,16r4f76,16r4f74,16r4f91,16r4f89,16r4f83,16r4f8f,16r4f7e,
+16r4f7b,16r4faa,16r4f7c,16r4fac,16r4f94,16r4fe6,16r4fe8,16r4fea,
+16r4fc5,16r4fda,16r4fe3,16r4fdc,16r4fd1,16r4fdf,16r4ff8,16r5029,
+16r504c,16r4ff3,16r502c,16r500f,16r502e,16r502d,16r4ffe,16r501c,
+16r500c,16r5025,16r5028,16r507e,16r5043,16r5055,16r5048,16r504e,
+16r506c,16r507b,16r50a5,16r50a7,16r50a9,16r50ba,16r50d6,16r5106,
+16r50ed,16r50ec,16r50e6,16r50ee,16r5107,16r510b,16r4edd,16r6c3d,
+16r4f58,16r4f65,16r4fce,16r9fa0,16r6c46,16r7c74,16r516e,16r5dfd,
+16r9ec9,16r9998,16r5181,16r5914,16r52f9,16r530d,16r8a07,16r5310,
+16r51eb,16r5919,16r5155,16r4ea0,16r5156,16r4eb3,16r886e,16r88a4,
+16r4eb5,16r8114,16r88d2,16r7980,16r5b34,16r8803,16r7fb8,16r51ab,
+16r51b1,16r51bd,16r51bc,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r51c7,16r5196,16r51a2,16r51a5,16r8ba0,16r8ba6,16r8ba7,
+16r8baa,16r8bb4,16r8bb5,16r8bb7,16r8bc2,16r8bc3,16r8bcb,16r8bcf,
+16r8bce,16r8bd2,16r8bd3,16r8bd4,16r8bd6,16r8bd8,16r8bd9,16r8bdc,
+16r8bdf,16r8be0,16r8be4,16r8be8,16r8be9,16r8bee,16r8bf0,16r8bf3,
+16r8bf6,16r8bf9,16r8bfc,16r8bff,16r8c00,16r8c02,16r8c04,16r8c07,
+16r8c0c,16r8c0f,16r8c11,16r8c12,16r8c14,16r8c15,16r8c16,16r8c19,
+16r8c1b,16r8c18,16r8c1d,16r8c1f,16r8c20,16r8c21,16r8c25,16r8c27,
+16r8c2a,16r8c2b,16r8c2e,16r8c2f,16r8c32,16r8c33,16r8c35,16r8c36,
+16r5369,16r537a,16r961d,16r9622,16r9621,16r9631,16r962a,16r963d,
+16r963c,16r9642,16r9649,16r9654,16r965f,16r9667,16r966c,16r9672,
+16r9674,16r9688,16r968d,16r9697,16r96b0,16r9097,16r909b,16r909d,
+16r9099,16r90ac,16r90a1,16r90b4,16r90b3,16r90b6,16r90ba,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r90b8,16r90b0,16r90cf,
+16r90c5,16r90be,16r90d0,16r90c4,16r90c7,16r90d3,16r90e6,16r90e2,
+16r90dc,16r90d7,16r90db,16r90eb,16r90ef,16r90fe,16r9104,16r9122,
+16r911e,16r9123,16r9131,16r912f,16r9139,16r9143,16r9146,16r520d,
+16r5942,16r52a2,16r52ac,16r52ad,16r52be,16r54ff,16r52d0,16r52d6,
+16r52f0,16r53df,16r71ee,16r77cd,16r5ef4,16r51f5,16r51fc,16r9b2f,
+16r53b6,16r5f01,16r755a,16r5def,16r574c,16r57a9,16r57a1,16r587e,
+16r58bc,16r58c5,16r58d1,16r5729,16r572c,16r572a,16r5733,16r5739,
+16r572e,16r572f,16r575c,16r573b,16r5742,16r5769,16r5785,16r576b,
+16r5786,16r577c,16r577b,16r5768,16r576d,16r5776,16r5773,16r57ad,
+16r57a4,16r578c,16r57b2,16r57cf,16r57a7,16r57b4,16r5793,16r57a0,
+16r57d5,16r57d8,16r57da,16r57d9,16r57d2,16r57b8,16r57f4,16r57ef,
+16r57f8,16r57e4,16r57dd,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r580b,16r580d,16r57fd,16r57ed,16r5800,16r581e,16r5819,
+16r5844,16r5820,16r5865,16r586c,16r5881,16r5889,16r589a,16r5880,
+16r99a8,16r9f19,16r61ff,16r8279,16r827d,16r827f,16r828f,16r828a,
+16r82a8,16r8284,16r828e,16r8291,16r8297,16r8299,16r82ab,16r82b8,
+16r82be,16r82b0,16r82c8,16r82ca,16r82e3,16r8298,16r82b7,16r82ae,
+16r82cb,16r82cc,16r82c1,16r82a9,16r82b4,16r82a1,16r82aa,16r829f,
+16r82c4,16r82ce,16r82a4,16r82e1,16r8309,16r82f7,16r82e4,16r830f,
+16r8307,16r82dc,16r82f4,16r82d2,16r82d8,16r830c,16r82fb,16r82d3,
+16r8311,16r831a,16r8306,16r8314,16r8315,16r82e0,16r82d5,16r831c,
+16r8351,16r835b,16r835c,16r8308,16r8392,16r833c,16r8334,16r8331,
+16r839b,16r835e,16r832f,16r834f,16r8347,16r8343,16r835f,16r8340,
+16r8317,16r8360,16r832d,16r833a,16r8333,16r8366,16r8365,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r8368,16r831b,16r8369,
+16r836c,16r836a,16r836d,16r836e,16r83b0,16r8378,16r83b3,16r83b4,
+16r83a0,16r83aa,16r8393,16r839c,16r8385,16r837c,16r83b6,16r83a9,
+16r837d,16r83b8,16r837b,16r8398,16r839e,16r83a8,16r83ba,16r83bc,
+16r83c1,16r8401,16r83e5,16r83d8,16r5807,16r8418,16r840b,16r83dd,
+16r83fd,16r83d6,16r841c,16r8438,16r8411,16r8406,16r83d4,16r83df,
+16r840f,16r8403,16r83f8,16r83f9,16r83ea,16r83c5,16r83c0,16r8426,
+16r83f0,16r83e1,16r845c,16r8451,16r845a,16r8459,16r8473,16r8487,
+16r8488,16r847a,16r8489,16r8478,16r843c,16r8446,16r8469,16r8476,
+16r848c,16r848e,16r8431,16r846d,16r84c1,16r84cd,16r84d0,16r84e6,
+16r84bd,16r84d3,16r84ca,16r84bf,16r84ba,16r84e0,16r84a1,16r84b9,
+16r84b4,16r8497,16r84e5,16r84e3,16r850c,16r750d,16r8538,16r84f0,
+16r8539,16r851f,16r853a,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r8556,16r853b,16r84ff,16r84fc,16r8559,16r8548,16r8568,
+16r8564,16r855e,16r857a,16r77a2,16r8543,16r8572,16r857b,16r85a4,
+16r85a8,16r8587,16r858f,16r8579,16r85ae,16r859c,16r8585,16r85b9,
+16r85b7,16r85b0,16r85d3,16r85c1,16r85dc,16r85ff,16r8627,16r8605,
+16r8629,16r8616,16r863c,16r5efe,16r5f08,16r593c,16r5941,16r8037,
+16r5955,16r595a,16r5958,16r530f,16r5c22,16r5c25,16r5c2c,16r5c34,
+16r624c,16r626a,16r629f,16r62bb,16r62ca,16r62da,16r62d7,16r62ee,
+16r6322,16r62f6,16r6339,16r634b,16r6343,16r63ad,16r63f6,16r6371,
+16r637a,16r638e,16r63b4,16r636d,16r63ac,16r638a,16r6369,16r63ae,
+16r63bc,16r63f2,16r63f8,16r63e0,16r63ff,16r63c4,16r63de,16r63ce,
+16r6452,16r63c6,16r63be,16r6445,16r6441,16r640b,16r641b,16r6420,
+16r640c,16r6426,16r6421,16r645e,16r6484,16r646d,16r6496,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r647a,16r64b7,16r64b8,
+16r6499,16r64ba,16r64c0,16r64d0,16r64d7,16r64e4,16r64e2,16r6509,
+16r6525,16r652e,16r5f0b,16r5fd2,16r7519,16r5f11,16r535f,16r53f1,
+16r53fd,16r53e9,16r53e8,16r53fb,16r5412,16r5416,16r5406,16r544b,
+16r5452,16r5453,16r5454,16r5456,16r5443,16r5421,16r5457,16r5459,
+16r5423,16r5432,16r5482,16r5494,16r5477,16r5471,16r5464,16r549a,
+16r549b,16r5484,16r5476,16r5466,16r549d,16r54d0,16r54ad,16r54c2,
+16r54b4,16r54d2,16r54a7,16r54a6,16r54d3,16r54d4,16r5472,16r54a3,
+16r54d5,16r54bb,16r54bf,16r54cc,16r54d9,16r54da,16r54dc,16r54a9,
+16r54aa,16r54a4,16r54dd,16r54cf,16r54de,16r551b,16r54e7,16r5520,
+16r54fd,16r5514,16r54f3,16r5522,16r5523,16r550f,16r5511,16r5527,
+16r552a,16r5567,16r558f,16r55b5,16r5549,16r556d,16r5541,16r5555,
+16r553f,16r5550,16r553c,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r5537,16r5556,16r5575,16r5576,16r5577,16r5533,16r5530,
+16r555c,16r558b,16r55d2,16r5583,16r55b1,16r55b9,16r5588,16r5581,
+16r559f,16r557e,16r55d6,16r5591,16r557b,16r55df,16r55bd,16r55be,
+16r5594,16r5599,16r55ea,16r55f7,16r55c9,16r561f,16r55d1,16r55eb,
+16r55ec,16r55d4,16r55e6,16r55dd,16r55c4,16r55ef,16r55e5,16r55f2,
+16r55f3,16r55cc,16r55cd,16r55e8,16r55f5,16r55e4,16r8f94,16r561e,
+16r5608,16r560c,16r5601,16r5624,16r5623,16r55fe,16r5600,16r5627,
+16r562d,16r5658,16r5639,16r5657,16r562c,16r564d,16r5662,16r5659,
+16r565c,16r564c,16r5654,16r5686,16r5664,16r5671,16r566b,16r567b,
+16r567c,16r5685,16r5693,16r56af,16r56d4,16r56d7,16r56dd,16r56e1,
+16r56f5,16r56eb,16r56f9,16r56ff,16r5704,16r570a,16r5709,16r571c,
+16r5e0f,16r5e19,16r5e14,16r5e11,16r5e31,16r5e3b,16r5e3c,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r5e37,16r5e44,16r5e54,
+16r5e5b,16r5e5e,16r5e61,16r5c8c,16r5c7a,16r5c8d,16r5c90,16r5c96,
+16r5c88,16r5c98,16r5c99,16r5c91,16r5c9a,16r5c9c,16r5cb5,16r5ca2,
+16r5cbd,16r5cac,16r5cab,16r5cb1,16r5ca3,16r5cc1,16r5cb7,16r5cc4,
+16r5cd2,16r5ce4,16r5ccb,16r5ce5,16r5d02,16r5d03,16r5d27,16r5d26,
+16r5d2e,16r5d24,16r5d1e,16r5d06,16r5d1b,16r5d58,16r5d3e,16r5d34,
+16r5d3d,16r5d6c,16r5d5b,16r5d6f,16r5d5d,16r5d6b,16r5d4b,16r5d4a,
+16r5d69,16r5d74,16r5d82,16r5d99,16r5d9d,16r8c73,16r5db7,16r5dc5,
+16r5f73,16r5f77,16r5f82,16r5f87,16r5f89,16r5f8c,16r5f95,16r5f99,
+16r5f9c,16r5fa8,16r5fad,16r5fb5,16r5fbc,16r8862,16r5f61,16r72ad,
+16r72b0,16r72b4,16r72b7,16r72b8,16r72c3,16r72c1,16r72ce,16r72cd,
+16r72d2,16r72e8,16r72ef,16r72e9,16r72f2,16r72f4,16r72f7,16r7301,
+16r72f3,16r7303,16r72fa,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r72fb,16r7317,16r7313,16r7321,16r730a,16r731e,16r731d,
+16r7315,16r7322,16r7339,16r7325,16r732c,16r7338,16r7331,16r7350,
+16r734d,16r7357,16r7360,16r736c,16r736f,16r737e,16r821b,16r5925,
+16r98e7,16r5924,16r5902,16r9963,16r9967,16r9968,16r9969,16r996a,
+16r996b,16r996c,16r9974,16r9977,16r997d,16r9980,16r9984,16r9987,
+16r998a,16r998d,16r9990,16r9991,16r9993,16r9994,16r9995,16r5e80,
+16r5e91,16r5e8b,16r5e96,16r5ea5,16r5ea0,16r5eb9,16r5eb5,16r5ebe,
+16r5eb3,16r8d53,16r5ed2,16r5ed1,16r5edb,16r5ee8,16r5eea,16r81ba,
+16r5fc4,16r5fc9,16r5fd6,16r5fcf,16r6003,16r5fee,16r6004,16r5fe1,
+16r5fe4,16r5ffe,16r6005,16r6006,16r5fea,16r5fed,16r5ff8,16r6019,
+16r6035,16r6026,16r601b,16r600f,16r600d,16r6029,16r602b,16r600a,
+16r603f,16r6021,16r6078,16r6079,16r607b,16r607a,16r6042,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r606a,16r607d,16r6096,
+16r609a,16r60ad,16r609d,16r6083,16r6092,16r608c,16r609b,16r60ec,
+16r60bb,16r60b1,16r60dd,16r60d8,16r60c6,16r60da,16r60b4,16r6120,
+16r6126,16r6115,16r6123,16r60f4,16r6100,16r610e,16r612b,16r614a,
+16r6175,16r61ac,16r6194,16r61a7,16r61b7,16r61d4,16r61f5,16r5fdd,
+16r96b3,16r95e9,16r95eb,16r95f1,16r95f3,16r95f5,16r95f6,16r95fc,
+16r95fe,16r9603,16r9604,16r9606,16r9608,16r960a,16r960b,16r960c,
+16r960d,16r960f,16r9612,16r9615,16r9616,16r9617,16r9619,16r961a,
+16r4e2c,16r723f,16r6215,16r6c35,16r6c54,16r6c5c,16r6c4a,16r6ca3,
+16r6c85,16r6c90,16r6c94,16r6c8c,16r6c68,16r6c69,16r6c74,16r6c76,
+16r6c86,16r6ca9,16r6cd0,16r6cd4,16r6cad,16r6cf7,16r6cf8,16r6cf1,
+16r6cd7,16r6cb2,16r6ce0,16r6cd6,16r6cfa,16r6ceb,16r6cee,16r6cb1,
+16r6cd3,16r6cef,16r6cfe,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r6d39,16r6d27,16r6d0c,16r6d43,16r6d48,16r6d07,16r6d04,
+16r6d19,16r6d0e,16r6d2b,16r6d4d,16r6d2e,16r6d35,16r6d1a,16r6d4f,
+16r6d52,16r6d54,16r6d33,16r6d91,16r6d6f,16r6d9e,16r6da0,16r6d5e,
+16r6d93,16r6d94,16r6d5c,16r6d60,16r6d7c,16r6d63,16r6e1a,16r6dc7,
+16r6dc5,16r6dde,16r6e0e,16r6dbf,16r6de0,16r6e11,16r6de6,16r6ddd,
+16r6dd9,16r6e16,16r6dab,16r6e0c,16r6dae,16r6e2b,16r6e6e,16r6e4e,
+16r6e6b,16r6eb2,16r6e5f,16r6e86,16r6e53,16r6e54,16r6e32,16r6e25,
+16r6e44,16r6edf,16r6eb1,16r6e98,16r6ee0,16r6f2d,16r6ee2,16r6ea5,
+16r6ea7,16r6ebd,16r6ebb,16r6eb7,16r6ed7,16r6eb4,16r6ecf,16r6e8f,
+16r6ec2,16r6e9f,16r6f62,16r6f46,16r6f47,16r6f24,16r6f15,16r6ef9,
+16r6f2f,16r6f36,16r6f4b,16r6f74,16r6f2a,16r6f09,16r6f29,16r6f89,
+16r6f8d,16r6f8c,16r6f78,16r6f72,16r6f7c,16r6f7a,16r6fd1,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r6fc9,16r6fa7,16r6fb9,
+16r6fb6,16r6fc2,16r6fe1,16r6fee,16r6fde,16r6fe0,16r6fef,16r701a,
+16r7023,16r701b,16r7039,16r7035,16r704f,16r705e,16r5b80,16r5b84,
+16r5b95,16r5b93,16r5ba5,16r5bb8,16r752f,16r9a9e,16r6434,16r5be4,
+16r5bee,16r8930,16r5bf0,16r8e47,16r8b07,16r8fb6,16r8fd3,16r8fd5,
+16r8fe5,16r8fee,16r8fe4,16r8fe9,16r8fe6,16r8ff3,16r8fe8,16r9005,
+16r9004,16r900b,16r9026,16r9011,16r900d,16r9016,16r9021,16r9035,
+16r9036,16r902d,16r902f,16r9044,16r9051,16r9052,16r9050,16r9068,
+16r9058,16r9062,16r905b,16r66b9,16r9074,16r907d,16r9082,16r9088,
+16r9083,16r908b,16r5f50,16r5f57,16r5f56,16r5f58,16r5c3b,16r54ab,
+16r5c50,16r5c59,16r5b71,16r5c63,16r5c66,16r7fbc,16r5f2a,16r5f29,
+16r5f2d,16r8274,16r5f3c,16r9b3b,16r5c6e,16r5981,16r5983,16r598d,
+16r59a9,16r59aa,16r59a3,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r5997,16r59ca,16r59ab,16r599e,16r59a4,16r59d2,16r59b2,
+16r59af,16r59d7,16r59be,16r5a05,16r5a06,16r59dd,16r5a08,16r59e3,
+16r59d8,16r59f9,16r5a0c,16r5a09,16r5a32,16r5a34,16r5a11,16r5a23,
+16r5a13,16r5a40,16r5a67,16r5a4a,16r5a55,16r5a3c,16r5a62,16r5a75,
+16r80ec,16r5aaa,16r5a9b,16r5a77,16r5a7a,16r5abe,16r5aeb,16r5ab2,
+16r5ad2,16r5ad4,16r5ab8,16r5ae0,16r5ae3,16r5af1,16r5ad6,16r5ae6,
+16r5ad8,16r5adc,16r5b09,16r5b17,16r5b16,16r5b32,16r5b37,16r5b40,
+16r5c15,16r5c1c,16r5b5a,16r5b65,16r5b73,16r5b51,16r5b53,16r5b62,
+16r9a75,16r9a77,16r9a78,16r9a7a,16r9a7f,16r9a7d,16r9a80,16r9a81,
+16r9a85,16r9a88,16r9a8a,16r9a90,16r9a92,16r9a93,16r9a96,16r9a98,
+16r9a9b,16r9a9c,16r9a9d,16r9a9f,16r9aa0,16r9aa2,16r9aa3,16r9aa5,
+16r9aa7,16r7e9f,16r7ea1,16r7ea3,16r7ea5,16r7ea8,16r7ea9,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r7ead,16r7eb0,16r7ebe,
+16r7ec0,16r7ec1,16r7ec2,16r7ec9,16r7ecb,16r7ecc,16r7ed0,16r7ed4,
+16r7ed7,16r7edb,16r7ee0,16r7ee1,16r7ee8,16r7eeb,16r7eee,16r7eef,
+16r7ef1,16r7ef2,16r7f0d,16r7ef6,16r7efa,16r7efb,16r7efe,16r7f01,
+16r7f02,16r7f03,16r7f07,16r7f08,16r7f0b,16r7f0c,16r7f0f,16r7f11,
+16r7f12,16r7f17,16r7f19,16r7f1c,16r7f1b,16r7f1f,16r7f21,16r7f22,
+16r7f23,16r7f24,16r7f25,16r7f26,16r7f27,16r7f2a,16r7f2b,16r7f2c,
+16r7f2d,16r7f2f,16r7f30,16r7f31,16r7f32,16r7f33,16r7f35,16r5e7a,
+16r757f,16r5ddb,16r753e,16r9095,16r738e,16r7391,16r73ae,16r73a2,
+16r739f,16r73cf,16r73c2,16r73d1,16r73b7,16r73b3,16r73c0,16r73c9,
+16r73c8,16r73e5,16r73d9,16r987c,16r740a,16r73e9,16r73e7,16r73de,
+16r73ba,16r73f2,16r740f,16r742a,16r745b,16r7426,16r7425,16r7428,
+16r7430,16r742e,16r742c,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r741b,16r741a,16r7441,16r745c,16r7457,16r7455,16r7459,
+16r7477,16r746d,16r747e,16r749c,16r748e,16r7480,16r7481,16r7487,
+16r748b,16r749e,16r74a8,16r74a9,16r7490,16r74a7,16r74d2,16r74ba,
+16r97ea,16r97eb,16r97ec,16r674c,16r6753,16r675e,16r6748,16r6769,
+16r67a5,16r6787,16r676a,16r6773,16r6798,16r67a7,16r6775,16r67a8,
+16r679e,16r67ad,16r678b,16r6777,16r677c,16r67f0,16r6809,16r67d8,
+16r680a,16r67e9,16r67b0,16r680c,16r67d9,16r67b5,16r67da,16r67b3,
+16r67dd,16r6800,16r67c3,16r67b8,16r67e2,16r680e,16r67c1,16r67fd,
+16r6832,16r6833,16r6860,16r6861,16r684e,16r6862,16r6844,16r6864,
+16r6883,16r681d,16r6855,16r6866,16r6841,16r6867,16r6840,16r683e,
+16r684a,16r6849,16r6829,16r68b5,16r688f,16r6874,16r6877,16r6893,
+16r686b,16r68c2,16r696e,16r68fc,16r691f,16r6920,16r68f9,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r6924,16r68f0,16r690b,
+16r6901,16r6957,16r68e3,16r6910,16r6971,16r6939,16r6960,16r6942,
+16r695d,16r6984,16r696b,16r6980,16r6998,16r6978,16r6934,16r69cc,
+16r6987,16r6988,16r69ce,16r6989,16r6966,16r6963,16r6979,16r699b,
+16r69a7,16r69bb,16r69ab,16r69ad,16r69d4,16r69b1,16r69c1,16r69ca,
+16r69df,16r6995,16r69e0,16r698d,16r69ff,16r6a2f,16r69ed,16r6a17,
+16r6a18,16r6a65,16r69f2,16r6a44,16r6a3e,16r6aa0,16r6a50,16r6a5b,
+16r6a35,16r6a8e,16r6a79,16r6a3d,16r6a28,16r6a58,16r6a7c,16r6a91,
+16r6a90,16r6aa9,16r6a97,16r6aab,16r7337,16r7352,16r6b81,16r6b82,
+16r6b87,16r6b84,16r6b92,16r6b93,16r6b8d,16r6b9a,16r6b9b,16r6ba1,
+16r6baa,16r8f6b,16r8f6d,16r8f71,16r8f72,16r8f73,16r8f75,16r8f76,
+16r8f78,16r8f77,16r8f79,16r8f7a,16r8f7c,16r8f7e,16r8f81,16r8f82,
+16r8f84,16r8f87,16r8f8b,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r8f8d,16r8f8e,16r8f8f,16r8f98,16r8f9a,16r8ece,16r620b,
+16r6217,16r621b,16r621f,16r6222,16r6221,16r6225,16r6224,16r622c,
+16r81e7,16r74ef,16r74f4,16r74ff,16r750f,16r7511,16r7513,16r6534,
+16r65ee,16r65ef,16r65f0,16r660a,16r6619,16r6772,16r6603,16r6615,
+16r6600,16r7085,16r66f7,16r661d,16r6634,16r6631,16r6636,16r6635,
+16r8006,16r665f,16r6654,16r6641,16r664f,16r6656,16r6661,16r6657,
+16r6677,16r6684,16r668c,16r66a7,16r669d,16r66be,16r66db,16r66dc,
+16r66e6,16r66e9,16r8d32,16r8d33,16r8d36,16r8d3b,16r8d3d,16r8d40,
+16r8d45,16r8d46,16r8d48,16r8d49,16r8d47,16r8d4d,16r8d55,16r8d59,
+16r89c7,16r89ca,16r89cb,16r89cc,16r89ce,16r89cf,16r89d0,16r89d1,
+16r726e,16r729f,16r725d,16r7266,16r726f,16r727e,16r727f,16r7284,
+16r728b,16r728d,16r728f,16r7292,16r6308,16r6332,16r63b0,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r643f,16r64d8,16r8004,
+16r6bea,16r6bf3,16r6bfd,16r6bf5,16r6bf9,16r6c05,16r6c07,16r6c06,
+16r6c0d,16r6c15,16r6c18,16r6c19,16r6c1a,16r6c21,16r6c29,16r6c24,
+16r6c2a,16r6c32,16r6535,16r6555,16r656b,16r724d,16r7252,16r7256,
+16r7230,16r8662,16r5216,16r809f,16r809c,16r8093,16r80bc,16r670a,
+16r80bd,16r80b1,16r80ab,16r80ad,16r80b4,16r80b7,16r80e7,16r80e8,
+16r80e9,16r80ea,16r80db,16r80c2,16r80c4,16r80d9,16r80cd,16r80d7,
+16r6710,16r80dd,16r80eb,16r80f1,16r80f4,16r80ed,16r810d,16r810e,
+16r80f2,16r80fc,16r6715,16r8112,16r8c5a,16r8136,16r811e,16r812c,
+16r8118,16r8132,16r8148,16r814c,16r8153,16r8174,16r8159,16r815a,
+16r8171,16r8160,16r8169,16r817c,16r817d,16r816d,16r8167,16r584d,
+16r5ab5,16r8188,16r8182,16r8191,16r6ed5,16r81a3,16r81aa,16r81cc,
+16r6726,16r81ca,16r81bb,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r81c1,16r81a6,16r6b24,16r6b37,16r6b39,16r6b43,16r6b46,
+16r6b59,16r98d1,16r98d2,16r98d3,16r98d5,16r98d9,16r98da,16r6bb3,
+16r5f40,16r6bc2,16r89f3,16r6590,16r9f51,16r6593,16r65bc,16r65c6,
+16r65c4,16r65c3,16r65cc,16r65ce,16r65d2,16r65d6,16r7080,16r709c,
+16r7096,16r709d,16r70bb,16r70c0,16r70b7,16r70ab,16r70b1,16r70e8,
+16r70ca,16r7110,16r7113,16r7116,16r712f,16r7131,16r7173,16r715c,
+16r7168,16r7145,16r7172,16r714a,16r7178,16r717a,16r7198,16r71b3,
+16r71b5,16r71a8,16r71a0,16r71e0,16r71d4,16r71e7,16r71f9,16r721d,
+16r7228,16r706c,16r7118,16r7166,16r71b9,16r623e,16r623d,16r6243,
+16r6248,16r6249,16r793b,16r7940,16r7946,16r7949,16r795b,16r795c,
+16r7953,16r795a,16r7962,16r7957,16r7960,16r796f,16r7967,16r797a,
+16r7985,16r798a,16r799a,16r79a7,16r79b3,16r5fd1,16r5fd0,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r603c,16r605d,16r605a,
+16r6067,16r6041,16r6059,16r6063,16r60ab,16r6106,16r610d,16r615d,
+16r61a9,16r619d,16r61cb,16r61d1,16r6206,16r8080,16r807f,16r6c93,
+16r6cf6,16r6dfc,16r77f6,16r77f8,16r7800,16r7809,16r7817,16r7818,
+16r7811,16r65ab,16r782d,16r781c,16r781d,16r7839,16r783a,16r783b,
+16r781f,16r783c,16r7825,16r782c,16r7823,16r7829,16r784e,16r786d,
+16r7856,16r7857,16r7826,16r7850,16r7847,16r784c,16r786a,16r789b,
+16r7893,16r789a,16r7887,16r789c,16r78a1,16r78a3,16r78b2,16r78b9,
+16r78a5,16r78d4,16r78d9,16r78c9,16r78ec,16r78f2,16r7905,16r78f4,
+16r7913,16r7924,16r791e,16r7934,16r9f9b,16r9ef9,16r9efb,16r9efc,
+16r76f1,16r7704,16r770d,16r76f9,16r7707,16r7708,16r771a,16r7722,
+16r7719,16r772d,16r7726,16r7735,16r7738,16r7750,16r7751,16r7747,
+16r7743,16r775a,16r7768,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r7762,16r7765,16r777f,16r778d,16r777d,16r7780,16r778c,
+16r7791,16r779f,16r77a0,16r77b0,16r77b5,16r77bd,16r753a,16r7540,
+16r754e,16r754b,16r7548,16r755b,16r7572,16r7579,16r7583,16r7f58,
+16r7f61,16r7f5f,16r8a48,16r7f68,16r7f74,16r7f71,16r7f79,16r7f81,
+16r7f7e,16r76cd,16r76e5,16r8832,16r9485,16r9486,16r9487,16r948b,
+16r948a,16r948c,16r948d,16r948f,16r9490,16r9494,16r9497,16r9495,
+16r949a,16r949b,16r949c,16r94a3,16r94a4,16r94ab,16r94aa,16r94ad,
+16r94ac,16r94af,16r94b0,16r94b2,16r94b4,16r94b6,16r94b7,16r94b8,
+16r94b9,16r94ba,16r94bc,16r94bd,16r94bf,16r94c4,16r94c8,16r94c9,
+16r94ca,16r94cb,16r94cc,16r94cd,16r94ce,16r94d0,16r94d1,16r94d2,
+16r94d5,16r94d6,16r94d7,16r94d9,16r94d8,16r94db,16r94de,16r94df,
+16r94e0,16r94e2,16r94e4,16r94e5,16r94e7,16r94e8,16r94ea,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r94e9,16r94eb,16r94ee,
+16r94ef,16r94f3,16r94f4,16r94f5,16r94f7,16r94f9,16r94fc,16r94fd,
+16r94ff,16r9503,16r9502,16r9506,16r9507,16r9509,16r950a,16r950d,
+16r950e,16r950f,16r9512,16r9513,16r9514,16r9515,16r9516,16r9518,
+16r951b,16r951d,16r951e,16r951f,16r9522,16r952a,16r952b,16r9529,
+16r952c,16r9531,16r9532,16r9534,16r9536,16r9537,16r9538,16r953c,
+16r953e,16r953f,16r9542,16r9535,16r9544,16r9545,16r9546,16r9549,
+16r954c,16r954e,16r954f,16r9552,16r9553,16r9554,16r9556,16r9557,
+16r9558,16r9559,16r955b,16r955e,16r955f,16r955d,16r9561,16r9562,
+16r9564,16r9565,16r9566,16r9567,16r9568,16r9569,16r956a,16r956b,
+16r956c,16r956f,16r9571,16r9572,16r9573,16r953a,16r77e7,16r77ec,
+16r96c9,16r79d5,16r79ed,16r79e3,16r79eb,16r7a06,16r5d47,16r7a03,
+16r7a02,16r7a1e,16r7a14,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r7a39,16r7a37,16r7a51,16r9ecf,16r99a5,16r7a70,16r7688,
+16r768e,16r7693,16r7699,16r76a4,16r74de,16r74e0,16r752c,16r9e20,
+16r9e22,16r9e28,16r9e29,16r9e2a,16r9e2b,16r9e2c,16r9e32,16r9e31,
+16r9e36,16r9e38,16r9e37,16r9e39,16r9e3a,16r9e3e,16r9e41,16r9e42,
+16r9e44,16r9e46,16r9e47,16r9e48,16r9e49,16r9e4b,16r9e4c,16r9e4e,
+16r9e51,16r9e55,16r9e57,16r9e5a,16r9e5b,16r9e5c,16r9e5e,16r9e63,
+16r9e66,16r9e67,16r9e68,16r9e69,16r9e6a,16r9e6b,16r9e6c,16r9e71,
+16r9e6d,16r9e73,16r7592,16r7594,16r7596,16r75a0,16r759d,16r75ac,
+16r75a3,16r75b3,16r75b4,16r75b8,16r75c4,16r75b1,16r75b0,16r75c3,
+16r75c2,16r75d6,16r75cd,16r75e3,16r75e8,16r75e6,16r75e4,16r75eb,
+16r75e7,16r7603,16r75f1,16r75fc,16r75ff,16r7610,16r7600,16r7605,
+16r760c,16r7617,16r760a,16r7625,16r7618,16r7615,16r7619,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r761b,16r763c,16r7622,
+16r7620,16r7640,16r762d,16r7630,16r763f,16r7635,16r7643,16r763e,
+16r7633,16r764d,16r765e,16r7654,16r765c,16r7656,16r766b,16r766f,
+16r7fca,16r7ae6,16r7a78,16r7a79,16r7a80,16r7a86,16r7a88,16r7a95,
+16r7aa6,16r7aa0,16r7aac,16r7aa8,16r7aad,16r7ab3,16r8864,16r8869,
+16r8872,16r887d,16r887f,16r8882,16r88a2,16r88c6,16r88b7,16r88bc,
+16r88c9,16r88e2,16r88ce,16r88e3,16r88e5,16r88f1,16r891a,16r88fc,
+16r88e8,16r88fe,16r88f0,16r8921,16r8919,16r8913,16r891b,16r890a,
+16r8934,16r892b,16r8936,16r8941,16r8966,16r897b,16r758b,16r80e5,
+16r76b2,16r76b4,16r77dc,16r8012,16r8014,16r8016,16r801c,16r8020,
+16r8022,16r8025,16r8026,16r8027,16r8029,16r8028,16r8031,16r800b,
+16r8035,16r8043,16r8046,16r804d,16r8052,16r8069,16r8071,16r8983,
+16r9878,16r9880,16r9883,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r9889,16r988c,16r988d,16r988f,16r9894,16r989a,16r989b,
+16r989e,16r989f,16r98a1,16r98a2,16r98a5,16r98a6,16r864d,16r8654,
+16r866c,16r866e,16r867f,16r867a,16r867c,16r867b,16r86a8,16r868d,
+16r868b,16r86ac,16r869d,16r86a7,16r86a3,16r86aa,16r8693,16r86a9,
+16r86b6,16r86c4,16r86b5,16r86ce,16r86b0,16r86ba,16r86b1,16r86af,
+16r86c9,16r86cf,16r86b4,16r86e9,16r86f1,16r86f2,16r86ed,16r86f3,
+16r86d0,16r8713,16r86de,16r86f4,16r86df,16r86d8,16r86d1,16r8703,
+16r8707,16r86f8,16r8708,16r870a,16r870d,16r8709,16r8723,16r873b,
+16r871e,16r8725,16r872e,16r871a,16r873e,16r8748,16r8734,16r8731,
+16r8729,16r8737,16r873f,16r8782,16r8722,16r877d,16r877e,16r877b,
+16r8760,16r8770,16r874c,16r876e,16r878b,16r8753,16r8763,16r877c,
+16r8764,16r8759,16r8765,16r8793,16r87af,16r87a8,16r87d2,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r87c6,16r8788,16r8785,
+16r87ad,16r8797,16r8783,16r87ab,16r87e5,16r87ac,16r87b5,16r87b3,
+16r87cb,16r87d3,16r87bd,16r87d1,16r87c0,16r87ca,16r87db,16r87ea,
+16r87e0,16r87ee,16r8816,16r8813,16r87fe,16r880a,16r881b,16r8821,
+16r8839,16r883c,16r7f36,16r7f42,16r7f44,16r7f45,16r8210,16r7afa,
+16r7afd,16r7b08,16r7b03,16r7b04,16r7b15,16r7b0a,16r7b2b,16r7b0f,
+16r7b47,16r7b38,16r7b2a,16r7b19,16r7b2e,16r7b31,16r7b20,16r7b25,
+16r7b24,16r7b33,16r7b3e,16r7b1e,16r7b58,16r7b5a,16r7b45,16r7b75,
+16r7b4c,16r7b5d,16r7b60,16r7b6e,16r7b7b,16r7b62,16r7b72,16r7b71,
+16r7b90,16r7ba6,16r7ba7,16r7bb8,16r7bac,16r7b9d,16r7ba8,16r7b85,
+16r7baa,16r7b9c,16r7ba2,16r7bab,16r7bb4,16r7bd1,16r7bc1,16r7bcc,
+16r7bdd,16r7bda,16r7be5,16r7be6,16r7bea,16r7c0c,16r7bfe,16r7bfc,
+16r7c0f,16r7c16,16r7c0b,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r7c1f,16r7c2a,16r7c26,16r7c38,16r7c41,16r7c40,16r81fe,
+16r8201,16r8202,16r8204,16r81ec,16r8844,16r8221,16r8222,16r8223,
+16r822d,16r822f,16r8228,16r822b,16r8238,16r823b,16r8233,16r8234,
+16r823e,16r8244,16r8249,16r824b,16r824f,16r825a,16r825f,16r8268,
+16r887e,16r8885,16r8888,16r88d8,16r88df,16r895e,16r7f9d,16r7f9f,
+16r7fa7,16r7faf,16r7fb0,16r7fb2,16r7c7c,16r6549,16r7c91,16r7c9d,
+16r7c9c,16r7c9e,16r7ca2,16r7cb2,16r7cbc,16r7cbd,16r7cc1,16r7cc7,
+16r7ccc,16r7ccd,16r7cc8,16r7cc5,16r7cd7,16r7ce8,16r826e,16r66a8,
+16r7fbf,16r7fce,16r7fd5,16r7fe5,16r7fe1,16r7fe6,16r7fe9,16r7fee,
+16r7ff3,16r7cf8,16r7d77,16r7da6,16r7dae,16r7e47,16r7e9b,16r9eb8,
+16r9eb4,16r8d73,16r8d84,16r8d94,16r8d91,16r8db1,16r8d67,16r8d6d,
+16r8c47,16r8c49,16r914a,16r9150,16r914e,16r914f,16r9164,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r9162,16r9161,16r9170,
+16r9169,16r916f,16r917d,16r917e,16r9172,16r9174,16r9179,16r918c,
+16r9185,16r9190,16r918d,16r9191,16r91a2,16r91a3,16r91aa,16r91ad,
+16r91ae,16r91af,16r91b5,16r91b4,16r91ba,16r8c55,16r9e7e,16r8db8,
+16r8deb,16r8e05,16r8e59,16r8e69,16r8db5,16r8dbf,16r8dbc,16r8dba,
+16r8dc4,16r8dd6,16r8dd7,16r8dda,16r8dde,16r8dce,16r8dcf,16r8ddb,
+16r8dc6,16r8dec,16r8df7,16r8df8,16r8de3,16r8df9,16r8dfb,16r8de4,
+16r8e09,16r8dfd,16r8e14,16r8e1d,16r8e1f,16r8e2c,16r8e2e,16r8e23,
+16r8e2f,16r8e3a,16r8e40,16r8e39,16r8e35,16r8e3d,16r8e31,16r8e49,
+16r8e41,16r8e42,16r8e51,16r8e52,16r8e4a,16r8e70,16r8e76,16r8e7c,
+16r8e6f,16r8e74,16r8e85,16r8e8f,16r8e94,16r8e90,16r8e9c,16r8e9e,
+16r8c78,16r8c82,16r8c8a,16r8c85,16r8c98,16r8c94,16r659b,16r89d6,
+16r89de,16r89da,16r89dc,ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,
+ERRchar,16r89e5,16r89eb,16r89ef,16r8a3e,16r8b26,16r9753,16r96e9,
+16r96f3,16r96ef,16r9706,16r9701,16r9708,16r970f,16r970e,16r972a,
+16r972d,16r9730,16r973e,16r9f80,16r9f83,16r9f85,16r9f86,16r9f87,
+16r9f88,16r9f89,16r9f8a,16r9f8c,16r9efe,16r9f0b,16r9f0d,16r96b9,
+16r96bc,16r96bd,16r96ce,16r96d2,16r77bf,16r96e0,16r928e,16r92ae,
+16r92c8,16r933e,16r936a,16r93ca,16r938f,16r943e,16r946b,16r9c7f,
+16r9c82,16r9c85,16r9c86,16r9c87,16r9c88,16r7a23,16r9c8b,16r9c8e,
+16r9c90,16r9c91,16r9c92,16r9c94,16r9c95,16r9c9a,16r9c9b,16r9c9e,
+16r9c9f,16r9ca0,16r9ca1,16r9ca2,16r9ca3,16r9ca5,16r9ca6,16r9ca7,
+16r9ca8,16r9ca9,16r9cab,16r9cad,16r9cae,16r9cb0,16r9cb1,16r9cb2,
+16r9cb3,16r9cb4,16r9cb5,16r9cb6,16r9cb7,16r9cba,16r9cbb,16r9cbc,
+16r9cbd,16r9cc4,16r9cc5,16r9cc6,16r9cc7,16r9cca,16r9ccb,ERRchar,
+ERRchar,ERRchar,ERRchar,ERRchar,ERRchar,16r9ccc,16r9ccd,16r9cce,
+16r9ccf,16r9cd0,16r9cd3,16r9cd4,16r9cd5,16r9cd7,16r9cd8,16r9cd9,
+16r9cdc,16r9cdd,16r9cdf,16r9ce2,16r977c,16r9785,16r9791,16r9792,
+16r9794,16r97af,16r97ab,16r97a3,16r97b2,16r97b4,16r9ab1,16r9ab0,
+16r9ab7,16r9e58,16r9ab6,16r9aba,16r9abc,16r9ac1,16r9ac0,16r9ac5,
+16r9ac2,16r9acb,16r9acc,16r9ad1,16r9b45,16r9b43,16r9b47,16r9b49,
+16r9b48,16r9b4d,16r9b51,16r98e8,16r990d,16r992e,16r9955,16r9954,
+16r9adf,16r9ae1,16r9ae6,16r9aef,16r9aeb,16r9afb,16r9aed,16r9af9,
+16r9b08,16r9b0f,16r9b13,16r9b1f,16r9b23,16r9ebd,16r9ebe,16r7e3b,
+16r9e82,16r9e87,16r9e88,16r9e8b,16r9e92,16r93d6,16r9e9d,16r9e9f,
+16r9edb,16r9edc,16r9edd,16r9ee0,16r9edf,16r9ee2,16r9ee9,16r9ee7,
+16r9ee5,16r9eea,16r9eef,16r9f22,16r9f2c,16r9f2f,16r9f39,16r9f37,
+16r9f3d,16r9f3e,16r9f44,
+};
--- /dev/null
+++ b/appl/lib/convcs/genjisx0201kana.b
@@ -1,0 +1,117 @@
+implement genjisx0201kana;
+
+include "sys.m";
+include "draw.m";
+
+genjisx0201kana : module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+DATAFILE : con "/lib/convcs/jisx0201kana";
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->create(DATAFILE, Sys->OWRITE, 8r644);
+	if (fd == nil) {
+		sys->print("cannot create %s: %r\n", DATAFILE);
+		return;
+	}
+
+	s := "";
+	for (slen := 0; slen < len mapdata; slen ++) {
+		(nil, code) := sys->tokenize(mapdata[slen], " \t");
+		u := hex2int(hd tl code);
+		s[slen] = u;
+	}
+	buf := array of byte s;
+	sys->write(fd, buf, len buf);
+}
+
+hex2int(s: string): int
+{
+	n := 0;
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'0' to '9' =>
+			n = 16*n + s[i] - '0';
+		'A' to 'F' =>
+			n = 16*n + s[i] + 10 - 'A';
+		'a' to 'f' =>
+			n = 16*n + s[i] + 10 - 'a';
+		* =>
+			return n;
+		}
+	}
+	return n;
+}
+
+
+# data derived from Unicode Consortium "CharmapML" data for EUC-JP
+# (G2 charset of EUC-JP is JIS X 0201 Kana)
+# the leading code point value is not used, it just appears for convenience
+mapdata := array [] of {
+		"A1	FF61",
+		"A2	FF62",
+		"A3	FF63",
+		"A4	FF64",
+		"A5	FF65",
+		"A6	FF66",
+		"A7	FF67",
+		"A8	FF68",
+		"A9	FF69",
+		"AA	FF6A",
+		"AB	FF6B",
+		"AC	FF6C",
+		"AD	FF6D",
+		"AE	FF6E",
+		"AF	FF6F",
+		"B0	FF70",
+		"B1	FF71",
+		"B2	FF72",
+		"B3	FF73",
+		"B4	FF74",
+		"B5	FF75",
+		"B6	FF76",
+		"B7	FF77",
+		"B8	FF78",
+		"B9	FF79",
+		"BA	FF7A",
+		"BB	FF7B",
+		"BC	FF7C",
+		"BD	FF7D",
+		"BE	FF7E",
+		"BF	FF7F",
+		"C0	FF80",
+		"C1	FF81",
+		"C2	FF82",
+		"C3	FF83",
+		"C4	FF84",
+		"C5	FF85",
+		"C6	FF86",
+		"C7	FF87",
+		"C8	FF88",
+		"C9	FF89",
+		"CA	FF8A",
+		"CB	FF8B",
+		"CC	FF8C",
+		"CD	FF8D",
+		"CE	FF8E",
+		"CF	FF8F",
+		"D0	FF90",
+		"D1	FF91",
+		"D2	FF92",
+		"D3	FF93",
+		"D4	FF94",
+		"D5	FF95",
+		"D6	FF96",
+		"D7	FF97",
+		"D8	FF98",
+		"D9	FF99",
+		"DA	FF9A",
+		"DB	FF9B",
+		"DC	FF9C",
+		"DD	FF9D",
+		"DE	FF9E",
+		"DF	FF9F",
+};
--- /dev/null
+++ b/appl/lib/convcs/genjisx0208-1997.b
@@ -1,0 +1,6958 @@
+implement genjisx0208;
+
+include "sys.m";
+include "draw.m";
+
+genjisx0208 : module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+DATAFILE : con "/lib/convcs/jisx0208-1997";
+
+PAGESZ : con 94;
+NPAGES : con 84;
+PAGE0 : con 16rA1;
+CHAR0 : con 16rA1;
+
+BADCHAR : con 16rFFFD;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->create(DATAFILE, Sys->OWRITE, 8r644);
+	if (fd == nil) {
+		sys->print("cannot create %s: %r\n", DATAFILE);
+		return;
+	}
+
+	pages := array [NPAGES * PAGESZ] of { * => BADCHAR };
+
+	for (i := 0; i < len data; i++) {
+		(nil, toks) := sys->tokenize(data[i], "\t");
+		(bytes, ucode) := (hd toks, hd tl toks);
+		u := hex2int(ucode);
+		(nil, blist) := sys->tokenize(bytes, " ");
+		b1 := hex2int(hd blist);
+		b2 := hex2int(hd tl blist);
+
+		page := b1 - PAGE0;
+		char := b2 - CHAR0;
+
+		pages[(page * PAGESZ)+char] = u;
+	}
+
+	s := "";
+	for (i = 0; i < len pages; i++)
+		s[i] = pages[i];
+	pages = nil;
+	buf := array of byte s;
+	sys->write(fd, buf, len buf);
+}
+
+
+hex2int(s: string): int
+{
+	n := 0;
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'0' to '9' =>
+			n = 16*n + s[i] - '0';
+		'A' to 'F' =>
+			n = 16*n + s[i] + 10 - 'A';
+		'a' to 'f' =>
+			n = 16*n + s[i] + 10 - 'a';
+		* =>
+			return n;
+		}
+	}
+	return n;
+}
+
+
+# Data is derived from Unicode Consortium "CharmapML" mapping data
+# for EUC-JP.
+# JISX0208 appears as charset G1 of the ISO-2022 based EUC-JP encoding
+# Although this format is not convenient for building our mapping data file
+# it is easily extracted from the Unicode Consortium data.
+
+data := array [] of {
+		"A1 F1	00A2",
+		"A1 F2	00A3",
+		"A1 F8	00A7",
+		"A1 AF	00A8",
+		"A2 CC	00AC",
+		"A1 EB	00B0",
+		"A1 DE	00B1",
+		"A1 AD	00B4",
+		"A2 F9	00B6",
+		"A1 DF	00D7",
+		"A1 E0	00F7",
+		"A6 A1	0391",
+		"A6 A2	0392",
+		"A6 A3	0393",
+		"A6 A4	0394",
+		"A6 A5	0395",
+		"A6 A6	0396",
+		"A6 A7	0397",
+		"A6 A8	0398",
+		"A6 A9	0399",
+		"A6 AA	039A",
+		"A6 AB	039B",
+		"A6 AC	039C",
+		"A6 AD	039D",
+		"A6 AE	039E",
+		"A6 AF	039F",
+		"A6 B0	03A0",
+		"A6 B1	03A1",
+		"A6 B2	03A3",
+		"A6 B3	03A4",
+		"A6 B4	03A5",
+		"A6 B5	03A6",
+		"A6 B6	03A7",
+		"A6 B7	03A8",
+		"A6 B8	03A9",
+		"A6 C1	03B1",
+		"A6 C2	03B2",
+		"A6 C3	03B3",
+		"A6 C4	03B4",
+		"A6 C5	03B5",
+		"A6 C6	03B6",
+		"A6 C7	03B7",
+		"A6 C8	03B8",
+		"A6 C9	03B9",
+		"A6 CA	03BA",
+		"A6 CB	03BB",
+		"A6 CC	03BC",
+		"A6 CD	03BD",
+		"A6 CE	03BE",
+		"A6 CF	03BF",
+		"A6 D0	03C0",
+		"A6 D1	03C1",
+		"A6 D2	03C3",
+		"A6 D3	03C4",
+		"A6 D4	03C5",
+		"A6 D5	03C6",
+		"A6 D6	03C7",
+		"A6 D7	03C8",
+		"A6 D8	03C9",
+		"A7 A7	0401",
+		"A7 A1	0410",
+		"A7 A2	0411",
+		"A7 A3	0412",
+		"A7 A4	0413",
+		"A7 A5	0414",
+		"A7 A6	0415",
+		"A7 A8	0416",
+		"A7 A9	0417",
+		"A7 AA	0418",
+		"A7 AB	0419",
+		"A7 AC	041A",
+		"A7 AD	041B",
+		"A7 AE	041C",
+		"A7 AF	041D",
+		"A7 B0	041E",
+		"A7 B1	041F",
+		"A7 B2	0420",
+		"A7 B3	0421",
+		"A7 B4	0422",
+		"A7 B5	0423",
+		"A7 B6	0424",
+		"A7 B7	0425",
+		"A7 B8	0426",
+		"A7 B9	0427",
+		"A7 BA	0428",
+		"A7 BB	0429",
+		"A7 BC	042A",
+		"A7 BD	042B",
+		"A7 BE	042C",
+		"A7 BF	042D",
+		"A7 C0	042E",
+		"A7 C1	042F",
+		"A7 D1	0430",
+		"A7 D2	0431",
+		"A7 D3	0432",
+		"A7 D4	0433",
+		"A7 D5	0434",
+		"A7 D6	0435",
+		"A7 D8	0436",
+		"A7 D9	0437",
+		"A7 DA	0438",
+		"A7 DB	0439",
+		"A7 DC	043A",
+		"A7 DD	043B",
+		"A7 DE	043C",
+		"A7 DF	043D",
+		"A7 E0	043E",
+		"A7 E1	043F",
+		"A7 E2	0440",
+		"A7 E3	0441",
+		"A7 E4	0442",
+		"A7 E5	0443",
+		"A7 E6	0444",
+		"A7 E7	0445",
+		"A7 E8	0446",
+		"A7 E9	0447",
+		"A7 EA	0448",
+		"A7 EB	0449",
+		"A7 EC	044A",
+		"A7 ED	044B",
+		"A7 EE	044C",
+		"A7 EF	044D",
+		"A7 F0	044E",
+		"A7 F1	044F",
+		"A7 D7	0451",
+		"A1 BE	2010",
+		"A1 BD	2015",
+		"A1 C2	2016",
+		"A1 C6	2018",
+		"A1 C7	2019",
+		"A1 C8	201C",
+		"A1 C9	201D",
+		"A2 F7	2020",
+		"A2 F8	2021",
+		"A1 C5	2025",
+		"A1 C4	2026",
+		"A2 F3	2030",
+		"A1 EC	2032",
+		"A1 ED	2033",
+		"A2 A8	203B",
+		"A1 EE	2103",
+		"A2 F2	212B",
+		"A2 AB	2190",
+		"A2 AC	2191",
+		"A2 AA	2192",
+		"A2 AD	2193",
+		"A2 CD	21D2",
+		"A2 CE	21D4",
+		"A2 CF	2200",
+		"A2 DF	2202",
+		"A2 D0	2203",
+		"A2 E0	2207",
+		"A2 BA	2208",
+		"A2 BB	220B",
+		"A1 DD	2212",
+		"A2 E5	221A",
+		"A2 E7	221D",
+		"A1 E7	221E",
+		"A2 DC	2220",
+		"A2 CA	2227",
+		"A2 CB	2228",
+		"A2 C1	2229",
+		"A2 C0	222A",
+		"A2 E9	222B",
+		"A2 EA	222C",
+		"A1 E8	2234",
+		"A2 E8	2235",
+		"A2 E6	223D",
+		"A2 E2	2252",
+		"A1 E2	2260",
+		"A2 E1	2261",
+		"A1 E5	2266",
+		"A1 E6	2267",
+		"A2 E3	226A",
+		"A2 E4	226B",
+		"A2 BE	2282",
+		"A2 BF	2283",
+		"A2 BC	2286",
+		"A2 BD	2287",
+		"A2 DD	22A5",
+		"A2 DE	2312",
+		"A8 A1	2500",
+		"A8 AC	2501",
+		"A8 A2	2502",
+		"A8 AD	2503",
+		"A8 A3	250C",
+		"A8 AE	250F",
+		"A8 A4	2510",
+		"A8 AF	2513",
+		"A8 A6	2514",
+		"A8 B1	2517",
+		"A8 A5	2518",
+		"A8 B0	251B",
+		"A8 A7	251C",
+		"A8 BC	251D",
+		"A8 B7	2520",
+		"A8 B2	2523",
+		"A8 A9	2524",
+		"A8 BE	2525",
+		"A8 B9	2528",
+		"A8 B4	252B",
+		"A8 A8	252C",
+		"A8 B8	252F",
+		"A8 BD	2530",
+		"A8 B3	2533",
+		"A8 AA	2534",
+		"A8 BA	2537",
+		"A8 BF	2538",
+		"A8 B5	253B",
+		"A8 AB	253C",
+		"A8 BB	253F",
+		"A8 C0	2542",
+		"A8 B6	254B",
+		"A2 A3	25A0",
+		"A2 A2	25A1",
+		"A2 A5	25B2",
+		"A2 A4	25B3",
+		"A2 A7	25BC",
+		"A2 A6	25BD",
+		"A2 A1	25C6",
+		"A1 FE	25C7",
+		"A1 FB	25CB",
+		"A1 FD	25CE",
+		"A1 FC	25CF",
+		"A2 FE	25EF",
+		"A1 FA	2605",
+		"A1 F9	2606",
+		"A1 EA	2640",
+		"A1 E9	2642",
+		"A2 F6	266A",
+		"A2 F5	266D",
+		"A2 F4	266F",
+		"A1 A1	3000",
+		"A1 A2	3001",
+		"A1 A3	3002",
+		"A1 B7	3003",
+		"A1 B9	3005",
+		"A1 BA	3006",
+		"A1 BB	3007",
+		"A1 D2	3008",
+		"A1 D3	3009",
+		"A1 D4	300A",
+		"A1 D5	300B",
+		"A1 D6	300C",
+		"A1 D7	300D",
+		"A1 D8	300E",
+		"A1 D9	300F",
+		"A1 DA	3010",
+		"A1 DB	3011",
+		"A2 A9	3012",
+		"A2 AE	3013",
+		"A1 CC	3014",
+		"A1 CD	3015",
+		"A1 C1	301C",
+		"A4 A1	3041",
+		"A4 A2	3042",
+		"A4 A3	3043",
+		"A4 A4	3044",
+		"A4 A5	3045",
+		"A4 A6	3046",
+		"A4 A7	3047",
+		"A4 A8	3048",
+		"A4 A9	3049",
+		"A4 AA	304A",
+		"A4 AB	304B",
+		"A4 AC	304C",
+		"A4 AD	304D",
+		"A4 AE	304E",
+		"A4 AF	304F",
+		"A4 B0	3050",
+		"A4 B1	3051",
+		"A4 B2	3052",
+		"A4 B3	3053",
+		"A4 B4	3054",
+		"A4 B5	3055",
+		"A4 B6	3056",
+		"A4 B7	3057",
+		"A4 B8	3058",
+		"A4 B9	3059",
+		"A4 BA	305A",
+		"A4 BB	305B",
+		"A4 BC	305C",
+		"A4 BD	305D",
+		"A4 BE	305E",
+		"A4 BF	305F",
+		"A4 C0	3060",
+		"A4 C1	3061",
+		"A4 C2	3062",
+		"A4 C3	3063",
+		"A4 C4	3064",
+		"A4 C5	3065",
+		"A4 C6	3066",
+		"A4 C7	3067",
+		"A4 C8	3068",
+		"A4 C9	3069",
+		"A4 CA	306A",
+		"A4 CB	306B",
+		"A4 CC	306C",
+		"A4 CD	306D",
+		"A4 CE	306E",
+		"A4 CF	306F",
+		"A4 D0	3070",
+		"A4 D1	3071",
+		"A4 D2	3072",
+		"A4 D3	3073",
+		"A4 D4	3074",
+		"A4 D5	3075",
+		"A4 D6	3076",
+		"A4 D7	3077",
+		"A4 D8	3078",
+		"A4 D9	3079",
+		"A4 DA	307A",
+		"A4 DB	307B",
+		"A4 DC	307C",
+		"A4 DD	307D",
+		"A4 DE	307E",
+		"A4 DF	307F",
+		"A4 E0	3080",
+		"A4 E1	3081",
+		"A4 E2	3082",
+		"A4 E3	3083",
+		"A4 E4	3084",
+		"A4 E5	3085",
+		"A4 E6	3086",
+		"A4 E7	3087",
+		"A4 E8	3088",
+		"A4 E9	3089",
+		"A4 EA	308A",
+		"A4 EB	308B",
+		"A4 EC	308C",
+		"A4 ED	308D",
+		"A4 EE	308E",
+		"A4 EF	308F",
+		"A4 F0	3090",
+		"A4 F1	3091",
+		"A4 F2	3092",
+		"A4 F3	3093",
+		"A1 AB	309B",
+		"A1 AC	309C",
+		"A1 B5	309D",
+		"A1 B6	309E",
+		"A5 A1	30A1",
+		"A5 A2	30A2",
+		"A5 A3	30A3",
+		"A5 A4	30A4",
+		"A5 A5	30A5",
+		"A5 A6	30A6",
+		"A5 A7	30A7",
+		"A5 A8	30A8",
+		"A5 A9	30A9",
+		"A5 AA	30AA",
+		"A5 AB	30AB",
+		"A5 AC	30AC",
+		"A5 AD	30AD",
+		"A5 AE	30AE",
+		"A5 AF	30AF",
+		"A5 B0	30B0",
+		"A5 B1	30B1",
+		"A5 B2	30B2",
+		"A5 B3	30B3",
+		"A5 B4	30B4",
+		"A5 B5	30B5",
+		"A5 B6	30B6",
+		"A5 B7	30B7",
+		"A5 B8	30B8",
+		"A5 B9	30B9",
+		"A5 BA	30BA",
+		"A5 BB	30BB",
+		"A5 BC	30BC",
+		"A5 BD	30BD",
+		"A5 BE	30BE",
+		"A5 BF	30BF",
+		"A5 C0	30C0",
+		"A5 C1	30C1",
+		"A5 C2	30C2",
+		"A5 C3	30C3",
+		"A5 C4	30C4",
+		"A5 C5	30C5",
+		"A5 C6	30C6",
+		"A5 C7	30C7",
+		"A5 C8	30C8",
+		"A5 C9	30C9",
+		"A5 CA	30CA",
+		"A5 CB	30CB",
+		"A5 CC	30CC",
+		"A5 CD	30CD",
+		"A5 CE	30CE",
+		"A5 CF	30CF",
+		"A5 D0	30D0",
+		"A5 D1	30D1",
+		"A5 D2	30D2",
+		"A5 D3	30D3",
+		"A5 D4	30D4",
+		"A5 D5	30D5",
+		"A5 D6	30D6",
+		"A5 D7	30D7",
+		"A5 D8	30D8",
+		"A5 D9	30D9",
+		"A5 DA	30DA",
+		"A5 DB	30DB",
+		"A5 DC	30DC",
+		"A5 DD	30DD",
+		"A5 DE	30DE",
+		"A5 DF	30DF",
+		"A5 E0	30E0",
+		"A5 E1	30E1",
+		"A5 E2	30E2",
+		"A5 E3	30E3",
+		"A5 E4	30E4",
+		"A5 E5	30E5",
+		"A5 E6	30E6",
+		"A5 E7	30E7",
+		"A5 E8	30E8",
+		"A5 E9	30E9",
+		"A5 EA	30EA",
+		"A5 EB	30EB",
+		"A5 EC	30EC",
+		"A5 ED	30ED",
+		"A5 EE	30EE",
+		"A5 EF	30EF",
+		"A5 F0	30F0",
+		"A5 F1	30F1",
+		"A5 F2	30F2",
+		"A5 F3	30F3",
+		"A5 F4	30F4",
+		"A5 F5	30F5",
+		"A5 F6	30F6",
+		"A1 A6	30FB",
+		"A1 BC	30FC",
+		"A1 B3	30FD",
+		"A1 B4	30FE",
+		"B0 EC	4E00",
+		"C3 FA	4E01",
+		"BC B7	4E03",
+		"CB FC	4E07",
+		"BE E6	4E08",
+		"BB B0	4E09",
+		"BE E5	4E0A",
+		"B2 BC	4E0B",
+		"C9 D4	4E0D",
+		"CD BF	4E0E",
+		"D0 A2	4E10",
+		"B1 AF	4E11",
+		"B3 EE	4E14",
+		"D0 A3	4E15",
+		"C0 A4	4E16",
+		"D2 C2	4E17",
+		"B5 D6	4E18",
+		"CA BA	4E19",
+		"BE E7	4E1E",
+		"CE BE	4E21",
+		"CA C2	4E26",
+		"D0 A4	4E2A",
+		"C3 E6	4E2D",
+		"D0 A5	4E31",
+		"B6 FA	4E32",
+		"D0 A6	4E36",
+		"B4 DD	4E38",
+		"C3 B0	4E39",
+		"BC E7	4E3B",
+		"D0 A7	4E3C",
+		"D0 A8	4E3F",
+		"D0 A9	4E42",
+		"C7 B5	4E43",
+		"B5 D7	4E45",
+		"C7 B7	4E4B",
+		"C6 E3	4E4D",
+		"B8 C3	4E4E",
+		"CB B3	4E4F",
+		"E9 C9	4E55",
+		"D0 AA	4E56",
+		"BE E8	4E57",
+		"D0 AB	4E58",
+		"B2 B5	4E59",
+		"B6 E5	4E5D",
+		"B8 F0	4E5E",
+		"CC E9	4E5F",
+		"D6 A6	4E62",
+		"CD F0	4E71",
+		"C6 FD	4E73",
+		"B4 A5	4E7E",
+		"B5 B5	4E80",
+		"D0 AC	4E82",
+		"D0 AD	4E85",
+		"CE BB	4E86",
+		"CD BD	4E88",
+		"C1 E8	4E89",
+		"D0 AF	4E8A",
+		"BB F6	4E8B",
+		"C6 F3	4E8C",
+		"D0 B2	4E8E",
+		"B1 BE	4E91",
+		"B8 DF	4E92",
+		"B8 DE	4E94",
+		"B0 E6	4E95",
+		"CF CB	4E98",
+		"CF CA	4E99",
+		"BA B3	4E9B",
+		"B0 A1	4E9C",
+		"D0 B3	4E9E",
+		"D0 B4	4E9F",
+		"D0 B5	4EA0",
+		"CB B4	4EA1",
+		"D0 B6	4EA2",
+		"B8 F2	4EA4",
+		"B0 E7	4EA5",
+		"CB F2	4EA6",
+		"B5 FC	4EA8",
+		"B5 FD	4EAB",
+		"B5 FE	4EAC",
+		"C4 E2	4EAD",
+		"CE BC	4EAE",
+		"D0 B7	4EB0",
+		"D0 B8	4EB3",
+		"D0 B9	4EB6",
+		"BF CD	4EBA",
+		"BD BA	4EC0",
+		"BF CE	4EC1",
+		"D0 BE	4EC2",
+		"D0 BC	4EC4",
+		"D0 BD	4EC6",
+		"B5 D8	4EC7",
+		"BA A3	4ECA",
+		"B2 F0	4ECB",
+		"D0 BB	4ECD",
+		"D0 BA	4ECE",
+		"CA A9	4ECF",
+		"BB C6	4ED4",
+		"BB C5	4ED5",
+		"C2 BE	4ED6",
+		"D0 BF	4ED7",
+		"C9 D5	4ED8",
+		"C0 E7	4ED9",
+		"A1 B8	4EDD",
+		"D0 C0	4EDE",
+		"D0 C2	4EDF",
+		"C2 E5	4EE3",
+		"CE E1	4EE4",
+		"B0 CA	4EE5",
+		"D0 C1	4EED",
+		"B2 BE	4EEE",
+		"B6 C4	4EF0",
+		"C3 E7	4EF2",
+		"B7 EF	4EF6",
+		"D0 C3	4EF7",
+		"C7 A4	4EFB",
+		"B4 EB	4F01",
+		"D0 C4	4F09",
+		"B0 CB	4F0A",
+		"B8 E0	4F0D",
+		"B4 EC	4F0E",
+		"C9 FA	4F0F",
+		"C8 B2	4F10",
+		"B5 D9	4F11",
+		"B2 F1	4F1A",
+		"D0 E7	4F1C",
+		"C5 C1	4F1D",
+		"C7 EC	4F2F",
+		"D0 C6	4F30",
+		"C8 BC	4F34",
+		"CE E2	4F36",
+		"BF AD	4F38",
+		"BB C7	4F3A",
+		"BB F7	4F3C",
+		"B2 C0	4F3D",
+		"C4 D1	4F43",
+		"C3 A2	4F46",
+		"D0 CA	4F47",
+		"B0 CC	4F4D",
+		"C4 E3	4F4E",
+		"BD BB	4F4F",
+		"BA B4	4F50",
+		"CD A4	4F51",
+		"C2 CE	4F53",
+		"B2 BF	4F55",
+		"D0 C9	4F57",
+		"CD BE	4F59",
+		"D0 C5	4F5A",
+		"D0 C7	4F5B",
+		"BA EE	4F5C",
+		"D0 C8	4F5D",
+		"D5 A4	4F5E",
+		"D0 D0	4F69",
+		"D0 D3	4F6F",
+		"D0 D1	4F70",
+		"B2 C2	4F73",
+		"CA BB	4F75",
+		"D0 CB	4F76",
+		"D0 CF	4F7B",
+		"B8 F3	4F7C",
+		"BB C8	4F7F",
+		"B4 A6	4F83",
+		"D0 D4	4F86",
+		"D0 CC	4F88",
+		"CE E3	4F8B",
+		"BB F8	4F8D",
+		"D0 CD	4F8F",
+		"D0 D2	4F91",
+		"D0 D5	4F96",
+		"D0 CE	4F98",
+		"B6 A1	4F9B",
+		"B0 CD	4F9D",
+		"B6 A2	4FA0",
+		"B2 C1	4FA1",
+		"D5 A5	4FAB",
+		"CB F9	4FAD",
+		"C9 EE	4FAE",
+		"B8 F4	4FAF",
+		"BF AF	4FB5",
+		"CE B7	4FB6",
+		"CA D8	4FBF",
+		"B7 B8	4FC2",
+		"C2 A5	4FC3",
+		"B2 E4	4FC4",
+		"BD D3	4FCA",
+		"D0 D9	4FCE",
+		"D0 DE	4FD0",
+		"D0 DC	4FD1",
+		"D0 D7	4FD4",
+		"C2 AF	4FD7",
+		"D0 DA	4FD8",
+		"D0 DD	4FDA",
+		"D0 DB	4FDB",
+		"CA DD	4FDD",
+		"D0 D8	4FDF",
+		"BF AE	4FE1",
+		"CB F3	4FE3",
+		"D0 DF	4FE4",
+		"D0 E0	4FE5",
+		"BD A4	4FEE",
+		"D0 ED	4FEF",
+		"C7 D0	4FF3",
+		"C9 B6	4FF5",
+		"D0 E8	4FF6",
+		"CA F0	4FF8",
+		"B2 B6	4FFA",
+		"D0 EC	4FFE",
+		"D0 E6	5005",
+		"D0 EF	5006",
+		"C1 D2	5009",
+		"B8 C4	500B",
+		"C7 DC	500D",
+		"E0 C7	500F",
+		"D0 EE	5011",
+		"C5 DD	5012",
+		"D0 E3	5014",
+		"B8 F6	5016",
+		"B8 F5	5019",
+		"D0 E1	501A",
+		"BC DA	501F",
+		"D0 E9	5021",
+		"CA EF	5023",
+		"C3 CD	5024",
+		"D0 E5	5025",
+		"B7 F1	5026",
+		"D0 E2	5028",
+		"D0 EA	5029",
+		"D0 E4	502A",
+		"CE D1	502B",
+		"D0 EB	502C",
+		"CF C1	502D",
+		"B6 E6	5036",
+		"B7 F0	5039",
+		"D0 F0	5043",
+		"D0 F1	5047",
+		"D0 F5	5048",
+		"B0 CE	5049",
+		"CA D0	504F",
+		"D0 F4	5050",
+		"D0 F3	5055",
+		"D0 F7	5056",
+		"D0 F6	505A",
+		"C4 E4	505C",
+		"B7 F2	5065",
+		"D0 F8	506C",
+		"BC C5	5072",
+		"C2 A6	5074",
+		"C4 E5	5075",
+		"B6 F6	5076",
+		"D0 F9	5078",
+		"B5 B6	507D",
+		"D0 FA	5080",
+		"D0 FC	5085",
+		"CB B5	508D",
+		"B7 E6	5091",
+		"BB B1	5098",
+		"C8 F7	5099",
+		"D0 FB	509A",
+		"BA C5	50AC",
+		"CD C3	50AD",
+		"D0 FE	50B2",
+		"D1 A3	50B3",
+		"D0 FD	50B4",
+		"BA C4	50B5",
+		"BD FD	50B7",
+		"B7 B9	50BE",
+		"D1 A4	50C2",
+		"B6 CF	50C5",
+		"D1 A1	50C9",
+		"D1 A2	50CA",
+		"C6 AF	50CD",
+		"C1 FC	50CF",
+		"B6 A3	50D1",
+		"CB CD	50D5",
+		"D1 A5	50D6",
+		"CE BD	50DA",
+		"D1 A6	50DE",
+		"D1 A9	50E3",
+		"D1 A7	50E5",
+		"C1 CE	50E7",
+		"D1 A8	50ED",
+		"D1 AA	50EE",
+		"D1 AC	50F5",
+		"D1 AB	50F9",
+		"CA C8	50FB",
+		"B5 B7	5100",
+		"D1 AE	5101",
+		"D1 AF	5102",
+		"B2 AF	5104",
+		"D1 AD	5109",
+		"BC F4	5112",
+		"D1 B2	5114",
+		"D1 B1	5115",
+		"D1 B0	5116",
+		"D0 D6	5118",
+		"D1 B3	511A",
+		"BD FE	511F",
+		"D1 B4	5121",
+		"CD A5	512A",
+		"CC D9	5132",
+		"D1 B6	5137",
+		"D1 B5	513A",
+		"D1 B8	513B",
+		"D1 B7	513C",
+		"D1 B9	513F",
+		"D1 BA	5140",
+		"B0 F4	5141",
+		"B8 B5	5143",
+		"B7 BB	5144",
+		"BD BC	5145",
+		"C3 FB	5146",
+		"B6 A4	5147",
+		"C0 E8	5148",
+		"B8 F7	5149",
+		"B9 EE	514B",
+		"D1 BC	514C",
+		"CC C8	514D",
+		"C5 C6	514E",
+		"BB F9	5150",
+		"D1 BB	5152",
+		"D1 BD	5154",
+		"C5 DE	515A",
+		"B3 F5	515C",
+		"D1 BE	5162",
+		"C6 FE	5165",
+		"C1 B4	5168",
+		"D1 C0	5169",
+		"D1 C1	516A",
+		"C8 AC	516B",
+		"B8 F8	516C",
+		"CF BB	516D",
+		"D1 C2	516E",
+		"B6 A6	5171",
+		"CA BC	5175",
+		"C2 B6	5176",
+		"B6 F1	5177",
+		"C5 B5	5178",
+		"B7 F3	517C",
+		"D1 C3	5180",
+		"D1 C4	5182",
+		"C6 E2	5185",
+		"B1 DF	5186",
+		"D1 C7	5189",
+		"BA FD	518A",
+		"D1 C6	518C",
+		"BA C6	518D",
+		"D1 C8	518F",
+		"E6 EE	5190",
+		"D1 C9	5191",
+		"CB C1	5192",
+		"D1 CA	5193",
+		"D1 CB	5195",
+		"D1 CC	5196",
+		"BE E9	5197",
+		"BC CC	5199",
+		"B4 A7	51A0",
+		"D1 CF	51A2",
+		"D1 CD	51A4",
+		"CC BD	51A5",
+		"D1 CE	51A6",
+		"C9 DA	51A8",
+		"D1 D0	51A9",
+		"D1 D1	51AA",
+		"D1 D2	51AB",
+		"C5 DF	51AC",
+		"D1 D6	51B0",
+		"D1 D4	51B1",
+		"D1 D5	51B2",
+		"D1 D3	51B3",
+		"BA E3	51B4",
+		"D1 D7	51B5",
+		"CC EA	51B6",
+		"CE E4	51B7",
+		"D1 D8	51BD",
+		"C0 A8	51C4",
+		"D1 D9	51C5",
+		"BD DA	51C6",
+		"D1 DA	51C9",
+		"C3 FC	51CB",
+		"CE BF	51CC",
+		"C5 E0	51CD",
+		"D2 C5	51D6",
+		"D1 DB	51DB",
+		"F4 A5	51DC",
+		"B6 C5	51DD",
+		"D1 DC	51E0",
+		"CB DE	51E1",
+		"BD E8	51E6",
+		"C2 FC	51E7",
+		"D1 DE	51E9",
+		"C6 E4	51EA",
+		"D1 DF	51ED",
+		"D1 E0	51F0",
+		"B3 AE	51F1",
+		"D1 E1	51F5",
+		"B6 A7	51F6",
+		"C6 CC	51F8",
+		"B1 FA	51F9",
+		"BD D0	51FA",
+		"C8 A1	51FD",
+		"D1 E2	51FE",
+		"C5 E1	5200",
+		"BF CF	5203",
+		"D1 E3	5204",
+		"CA AC	5206",
+		"C0 DA	5207",
+		"B4 A2	5208",
+		"B4 A9	520A",
+		"D1 E4	520B",
+		"D1 E6	520E",
+		"B7 BA	5211",
+		"D1 E5	5214",
+		"CE F3	5217",
+		"BD E9	521D",
+		"C8 BD	5224",
+		"CA CC	5225",
+		"D1 E7	5227",
+		"CD F8	5229",
+		"D1 E8	522A",
+		"D1 E9	522E",
+		"C5 FE	5230",
+		"D1 EA	5233",
+		"C0 A9	5236",
+		"BA FE	5237",
+		"B7 F4	5238",
+		"D1 EB	5239",
+		"BB C9	523A",
+		"B9 EF	523B",
+		"C4 E6	5243",
+		"D1 ED	5244",
+		"C2 A7	5247",
+		"BA EF	524A",
+		"D1 EE	524B",
+		"D1 EF	524C",
+		"C1 B0	524D",
+		"D1 EC	524F",
+		"D1 F1	5254",
+		"CB B6	5256",
+		"B9 E4	525B",
+		"D1 F0	525E",
+		"B7 F5	5263",
+		"BA DE	5264",
+		"C7 ED	5265",
+		"D1 F4	5269",
+		"D1 F2	526A",
+		"C9 FB	526F",
+		"BE EA	5270",
+		"D1 FB	5271",
+		"B3 E4	5272",
+		"D1 F5	5273",
+		"D1 F3	5274",
+		"C1 CF	5275",
+		"D1 F7	527D",
+		"D1 F6	527F",
+		"B3 C4	5283",
+		"B7 E0	5287",
+		"D1 FC	5288",
+		"CE AD	5289",
+		"D1 F8	528D",
+		"D1 FD	5291",
+		"D1 FA	5292",
+		"D1 F9	5294",
+		"CE CF	529B",
+		"B8 F9	529F",
+		"B2 C3	52A0",
+		"CE F4	52A3",
+		"BD F5	52A9",
+		"C5 D8	52AA",
+		"B9 E5	52AB",
+		"D2 A2	52AC",
+		"D2 A3	52AD",
+		"CE E5	52B1",
+		"CF AB	52B4",
+		"D2 A5	52B5",
+		"B8 FA	52B9",
+		"D2 A4	52BC",
+		"B3 AF	52BE",
+		"D2 A6	52C1",
+		"CB D6	52C3",
+		"C4 BC	52C5",
+		"CD A6	52C7",
+		"CA D9	52C9",
+		"D2 A7	52CD",
+		"F0 D5	52D2",
+		"C6 B0	52D5",
+		"D2 A8	52D7",
+		"B4 AA	52D8",
+		"CC B3	52D9",
+		"BE A1	52DD",
+		"D2 A9	52DE",
+		"CA E7	52DF",
+		"D2 AD	52E0",
+		"C0 AA	52E2",
+		"D2 AA	52E3",
+		"B6 D0	52E4",
+		"D2 AB	52E6",
+		"B4 AB	52E7",
+		"B7 AE	52F2",
+		"D2 AE	52F3",
+		"D2 AF	52F5",
+		"D2 B0	52F8",
+		"D2 B1	52F9",
+		"BC DB	52FA",
+		"B8 FB	52FE",
+		"CC DE	52FF",
+		"CC E8	5301",
+		"C6 F7	5302",
+		"CA F1	5305",
+		"D2 B2	5306",
+		"D2 B3	5308",
+		"D2 B5	530D",
+		"D2 B7	530F",
+		"D2 B6	5310",
+		"D2 B8	5315",
+		"B2 BD	5316",
+		"CB CC	5317",
+		"BA FC	5319",
+		"D2 B9	531A",
+		"C1 D9	531D",
+		"BE A2	5320",
+		"B6 A9	5321",
+		"D2 BA	5323",
+		"C8 DB	532A",
+		"D2 BB	532F",
+		"D2 BC	5331",
+		"D2 BD	5333",
+		"D2 BE	5338",
+		"C9 A4	5339",
+		"B6 E8	533A",
+		"B0 E5	533B",
+		"C6 BF	533F",
+		"D2 BF	5340",
+		"BD BD	5341",
+		"C0 E9	5343",
+		"D2 C1	5345",
+		"D2 C0	5346",
+		"BE A3	5347",
+		"B8 E1	5348",
+		"D2 C3	5349",
+		"C8 BE	534A",
+		"D2 C4	534D",
+		"C8 DC	5351",
+		"C2 B4	5352",
+		"C2 EE	5353",
+		"B6 A8	5354",
+		"C6 EE	5357",
+		"C3 B1	5358",
+		"C7 EE	535A",
+		"CB CE	535C",
+		"D2 C6	535E",
+		"C0 EA	5360",
+		"B7 B5	5366",
+		"D2 C7	5369",
+		"D2 C8	536E",
+		"B1 AC	536F",
+		"B0 F5	5370",
+		"B4 ED	5371",
+		"C2 A8	5373",
+		"B5 D1	5374",
+		"CD F1	5375",
+		"D2 CB	5377",
+		"B2 B7	5378",
+		"D2 CA	537B",
+		"B6 AA	537F",
+		"D2 CC	5382",
+		"CC F1	5384",
+		"D2 CD	5396",
+		"CE D2	5398",
+		"B8 FC	539A",
+		"B8 B6	539F",
+		"D2 CE	53A0",
+		"D2 D0	53A5",
+		"D2 CF	53A6",
+		"BF DF	53A8",
+		"B1 B9	53A9",
+		"B1 DE	53AD",
+		"D2 D1	53AE",
+		"D2 D2	53B0",
+		"B8 B7	53B3",
+		"D2 D3	53B6",
+		"B5 EE	53BB",
+		"BB B2	53C2",
+		"D2 D4	53C3",
+		"CB F4	53C8",
+		"BA B5	53C9",
+		"B5 DA	53CA",
+		"CD A7	53CB",
+		"C1 D0	53CC",
+		"C8 BF	53CD",
+		"BC FD	53CE",
+		"BD C7	53D4",
+		"BC E8	53D6",
+		"BC F5	53D7",
+		"BD F6	53D9",
+		"C8 C0	53DB",
+		"D2 D7	53DF",
+		"B1 C3	53E1",
+		"C1 D1	53E2",
+		"B8 FD	53E3",
+		"B8 C5	53E4",
+		"B6 E7	53E5",
+		"D2 DB	53E8",
+		"C3 A1	53E9",
+		"C2 FE	53EA",
+		"B6 AB	53EB",
+		"BE A4	53EC",
+		"D2 DC	53ED",
+		"D2 DA	53EE",
+		"B2 C4	53EF",
+		"C2 E6	53F0",
+		"BC B8	53F1",
+		"BB CB	53F2",
+		"B1 A6	53F3",
+		"B3 F0	53F6",
+		"B9 E6	53F7",
+		"BB CA	53F8",
+		"D2 DD	53FA",
+		"D2 DE	5401",
+		"B5 C9	5403",
+		"B3 C6	5404",
+		"B9 E7	5408",
+		"B5 C8	5409",
+		"C4 DF	540A",
+		"B1 A5	540B",
+		"C6 B1	540C",
+		"CC BE	540D",
+		"B9 A1	540E",
+		"CD F9	540F",
+		"C5 C7	5410",
+		"B8 FE	5411",
+		"B7 AF	541B",
+		"D2 E7	541D",
+		"B6 E3	541F",
+		"CB CA	5420",
+		"C8 DD	5426",
+		"D2 E6	5429",
+		"B4 DE	542B",
+		"D2 E1	542C",
+		"D2 E2	542D",
+		"D2 E4	542E",
+		"D2 E5	5436",
+		"B5 DB	5438",
+		"BF E1	5439",
+		"CA AD	543B",
+		"D2 E3	543C",
+		"D2 DF	543D",
+		"B8 E3	543E",
+		"D2 E0	5440",
+		"CF A4	5442",
+		"CA F2	5446",
+		"C4 E8	5448",
+		"B8 E2	5449",
+		"B9 F0	544A",
+		"D2 E8	544E",
+		"C6 DD	5451",
+		"D2 EC	545F",
+		"BC FE	5468",
+		"BC F6	546A",
+		"D2 EF	5470",
+		"D2 ED	5471",
+		"CC A3	5473",
+		"D2 EA	5475",
+		"D2 F3	5476",
+		"D2 EE	5477",
+		"D2 F1	547B",
+		"B8 C6	547C",
+		"CC BF	547D",
+		"D2 F2	5480",
+		"D2 F4	5484",
+		"D2 F6	5486",
+		"BA F0	548B",
+		"CF C2	548C",
+		"D2 EB	548E",
+		"D2 E9	548F",
+		"D2 F5	5490",
+		"D2 F0	5492",
+		"D2 F8	54A2",
+		"D3 A3	54A4",
+		"D2 FA	54A5",
+		"D2 FE	54A8",
+		"D3 A1	54AB",
+		"D2 FB	54AC",
+		"D3 BE	54AF",
+		"BA E9	54B2",
+		"B3 B1	54B3",
+		"D2 F9	54B8",
+		"D3 A5	54BC",
+		"B0 F6	54BD",
+		"D3 A4	54BE",
+		"B0 A5	54C0",
+		"C9 CA	54C1",
+		"D3 A2	54C2",
+		"D2 FC	54C4",
+		"D2 F7	54C7",
+		"D2 FD	54C8",
+		"BA C8	54C9",
+		"D3 A6	54D8",
+		"B0 F7	54E1",
+		"D3 AF	54E2",
+		"D3 A7	54E5",
+		"D3 A8	54E6",
+		"BE A5	54E8",
+		"CB E9	54E9",
+		"D3 AD	54ED",
+		"D3 AC	54EE",
+		"C5 AF	54F2",
+		"D3 AE	54FA",
+		"D3 AB	54FD",
+		"B1 B4	5504",
+		"BA B6	5506",
+		"BF B0	5507",
+		"D3 A9	550F",
+		"C5 E2	5510",
+		"D3 AA	5514",
+		"B0 A2	5516",
+		"D3 B4	552E",
+		"CD A3	552F",
+		"BE A7	5531",
+		"D3 BA	5533",
+		"D3 B9	5538",
+		"D3 B0	5539",
+		"C2 C3	553E",
+		"D3 B1	5540",
+		"C2 EF	5544",
+		"D3 B6	5545",
+		"BE A6	5546",
+		"D3 B3	554C",
+		"CC E4	554F",
+		"B7 BC	5553",
+		"D3 B7	5556",
+		"D3 B8	5557",
+		"D3 B5	555C",
+		"D3 BB	555D",
+		"D3 B2	5563",
+		"D3 C1	557B",
+		"D3 C6	557C",
+		"D3 C2	557E",
+		"D3 BD	5580",
+		"D3 C7	5583",
+		"C1 B1	5584",
+		"D3 C9	5587",
+		"B9 A2	5589",
+		"D3 BF	558A",
+		"C3 FD	558B",
+		"D3 C3	5598",
+		"D3 BC	5599",
+		"B4 AD	559A",
+		"B4 EE	559C",
+		"B3 E5	559D",
+		"D3 C4	559E",
+		"D3 C0	559F",
+		"B7 F6	55A7",
+		"D3 CA	55A8",
+		"D3 C8	55A9",
+		"C1 D3	55AA",
+		"B5 CA	55AB",
+		"B6 AC	55AC",
+		"D3 C5	55AE",
+		"B6 F4	55B0",
+		"B1 C4	55B6",
+		"D3 CE	55C4",
+		"D3 CC	55C5",
+		"D4 A7	55C7",
+		"D3 D1	55D4",
+		"D3 CB	55DA",
+		"D3 CF	55DC",
+		"D3 CD	55DF",
+		"BB CC	55E3",
+		"D3 D0	55E4",
+		"D3 D3	55F7",
+		"D3 D8	55F9",
+		"D3 D6	55FD",
+		"D3 D5	55FE",
+		"C3 B2	5606",
+		"B2 C5	5609",
+		"D3 D2	5614",
+		"D3 D4	5616",
+		"BE A8	5617",
+		"B1 B3	5618",
+		"D3 D7	561B",
+		"B2 DE	5629",
+		"D3 E2	562F",
+		"BE FC	5631",
+		"D3 DE	5632",
+		"D3 DC	5634",
+		"D3 DD	5636",
+		"D3 DF	5638",
+		"B1 BD	5642",
+		"C1 B9	564C",
+		"D3 D9	564E",
+		"D3 DA	5650",
+		"B3 FA	565B",
+		"D3 E1	5664",
+		"B4 EF	5668",
+		"D3 E4	566A",
+		"D3 E0	566B",
+		"D3 E3	566C",
+		"CA AE	5674",
+		"C6 D5	5678",
+		"C8 B8	567A",
+		"D3 E6	5680",
+		"D3 E5	5686",
+		"B3 C5	5687",
+		"D3 E7	568A",
+		"D3 EA	568F",
+		"D3 E9	5694",
+		"D3 E8	56A0",
+		"C7 B9	56A2",
+		"D3 EB	56A5",
+		"D3 EC	56AE",
+		"D3 EE	56B4",
+		"D3 ED	56B6",
+		"D3 F0	56BC",
+		"D3 F3	56C0",
+		"D3 F1	56C1",
+		"D3 EF	56C2",
+		"D3 F2	56C3",
+		"D3 F4	56C8",
+		"D3 F5	56CE",
+		"D3 F6	56D1",
+		"D3 F7	56D3",
+		"D3 F8	56D7",
+		"D1 C5	56D8",
+		"BC FC	56DA",
+		"BB CD	56DB",
+		"B2 F3	56DE",
+		"B0 F8	56E0",
+		"C3 C4	56E3",
+		"D3 F9	56EE",
+		"BA A4	56F0",
+		"B0 CF	56F2",
+		"BF DE	56F3",
+		"D3 FA	56F9",
+		"B8 C7	56FA",
+		"B9 F1	56FD",
+		"D3 FC	56FF",
+		"D3 FB	5700",
+		"CA E0	5703",
+		"D3 FD	5704",
+		"D4 A1	5708",
+		"D3 FE	5709",
+		"D4 A2	570B",
+		"D4 A3	570D",
+		"B7 F7	570F",
+		"B1 E0	5712",
+		"D4 A4	5713",
+		"D4 A6	5716",
+		"D4 A5	5718",
+		"D4 A8	571C",
+		"C5 DA	571F",
+		"D4 A9	5726",
+		"B0 B5	5727",
+		"BA DF	5728",
+		"B7 BD	572D",
+		"C3 CF	5730",
+		"D4 AA	5737",
+		"D4 AB	5738",
+		"D4 AD	573B",
+		"D4 AE	5740",
+		"BA E4	5742",
+		"B6 D1	5747",
+		"CB B7	574A",
+		"D4 AC	574E",
+		"D4 AF	574F",
+		"BA C1	5750",
+		"B9 A3	5751",
+		"D4 B3	5761",
+		"BA A5	5764",
+		"C3 B3	5766",
+		"D4 B0	5769",
+		"C4 DA	576A",
+		"D4 B4	577F",
+		"BF E2	5782",
+		"D4 B2	5788",
+		"D4 B5	5789",
+		"B7 BF	578B",
+		"D4 B6	5793",
+		"D4 B7	57A0",
+		"B9 A4	57A2",
+		"B3 C0	57A3",
+		"D4 B9	57A4",
+		"D4 BA	57AA",
+		"D4 BB	57B0",
+		"D4 B8	57B3",
+		"D4 B1	57C0",
+		"D4 BC	57C3",
+		"D4 BD	57C6",
+		"CB E4	57CB",
+		"BE EB	57CE",
+		"D4 BF	57D2",
+		"D4 C0	57D3",
+		"D4 BE	57D4",
+		"D4 C2	57D6",
+		"C7 B8	57DC",
+		"B0 E8	57DF",
+		"C9 D6	57E0",
+		"D4 C3	57E3",
+		"BE FD	57F4",
+		"BC B9	57F7",
+		"C7 DD	57F9",
+		"B4 F0	57FA",
+		"BA EB	57FC",
+		"CB D9	5800",
+		"C6 B2	5802",
+		"B7 F8	5805",
+		"C2 CF	5806",
+		"D4 C1	580A",
+		"D4 C4	580B",
+		"C2 C4	5815",
+		"D4 C5	5819",
+		"D4 C6	581D",
+		"D4 C8	5821",
+		"C4 E9	5824",
+		"B4 AE	582A",
+		"F4 A1	582F",
+		"B1 E1	5830",
+		"CA F3	5831",
+		"BE EC	5834",
+		"C5 C8	5835",
+		"BA E6	583A",
+		"D4 CE	583D",
+		"CA BD	5840",
+		"CE DD	5841",
+		"B2 F4	584A",
+		"D4 CA	584B",
+		"C1 BA	5851",
+		"D4 CD	5852",
+		"C5 E3	5854",
+		"C5 C9	5857",
+		"C5 E4	5858",
+		"C8 B9	5859",
+		"C4 CD	585A",
+		"BA C9	585E",
+		"D4 C9	5862",
+		"B1 F6	5869",
+		"C5 B6	586B",
+		"D4 CB	5870",
+		"D4 C7	5872",
+		"BF D0	5875",
+		"D4 CF	5879",
+		"BD CE	587E",
+		"B6 AD	5883",
+		"D4 D0	5885",
+		"CA E8	5893",
+		"C1 FD	5897",
+		"C4 C6	589C",
+		"D4 D2	589F",
+		"CB CF	58A8",
+		"D4 D3	58AB",
+		"D4 D8	58AE",
+		"CA AF	58B3",
+		"D4 D7	58B8",
+		"D4 D1	58B9",
+		"D4 D4	58BA",
+		"D4 D6	58BB",
+		"BA A6	58BE",
+		"CA C9	58C1",
+		"D4 D9	58C5",
+		"C3 C5	58C7",
+		"B2 F5	58CA",
+		"BE ED	58CC",
+		"D4 DB	58D1",
+		"D4 DA	58D3",
+		"B9 E8	58D5",
+		"D4 DC	58D7",
+		"D4 DE	58D8",
+		"D4 DD	58D9",
+		"D4 E0	58DC",
+		"D4 D5	58DE",
+		"D4 E2	58DF",
+		"D4 E1	58E4",
+		"D4 DF	58E5",
+		"BB CE	58EB",
+		"BF D1	58EC",
+		"C1 D4	58EE",
+		"D4 E3	58EF",
+		"C0 BC	58F0",
+		"B0 ED	58F1",
+		"C7 E4	58F2",
+		"C4 DB	58F7",
+		"D4 E5	58F9",
+		"D4 E4	58FA",
+		"D4 E6	58FB",
+		"D4 E7	58FC",
+		"D4 E8	58FD",
+		"D4 E9	5902",
+		"CA D1	5909",
+		"D4 EA	590A",
+		"B2 C6	590F",
+		"D4 EB	5910",
+		"CD BC	5915",
+		"B3 B0	5916",
+		"D2 C9	5918",
+		"BD C8	5919",
+		"C2 BF	591A",
+		"D4 EC	591B",
+		"CC EB	591C",
+		"CC B4	5922",
+		"D4 EE	5925",
+		"C2 E7	5927",
+		"C5 B7	5929",
+		"C2 C0	592A",
+		"C9 D7	592B",
+		"D4 EF	592C",
+		"D4 F0	592D",
+		"B1 FB	592E",
+		"BC BA	5931",
+		"D4 F1	5932",
+		"B0 D0	5937",
+		"D4 F2	5938",
+		"D4 F3	593E",
+		"B1 E2	5944",
+		"B4 F1	5947",
+		"C6 E0	5948",
+		"CA F4	5949",
+		"D4 F7	594E",
+		"C1 D5	594F",
+		"D4 F6	5950",
+		"B7 C0	5951",
+		"CB DB	5954",
+		"D4 F5	5955",
+		"C5 E5	5957",
+		"D4 F9	5958",
+		"D4 F8	595A",
+		"D4 FB	5960",
+		"D4 FA	5962",
+		"B1 FC	5965",
+		"D4 FC	5967",
+		"BE A9	5968",
+		"D4 FE	5969",
+		"C3 A5	596A",
+		"D4 FD	596C",
+		"CA B3	596E",
+		"BD F7	5973",
+		"C5 DB	5974",
+		"D5 A1	5978",
+		"B9 A5	597D",
+		"D5 A2	5981",
+		"C7 A1	5982",
+		"C8 DE	5983",
+		"CC D1	5984",
+		"C7 A5	598A",
+		"D5 AB	598D",
+		"B5 B8	5993",
+		"CD C5	5996",
+		"CC AF	5999",
+		"D6 AC	599B",
+		"D5 A3	599D",
+		"D5 A6	59A3",
+		"C2 C5	59A5",
+		"CB B8	59A8",
+		"C5 CA	59AC",
+		"D5 A7	59B2",
+		"CB E5	59B9",
+		"BA CA	59BB",
+		"BE AA	59BE",
+		"D5 A8	59C6",
+		"BB D0	59C9",
+		"BB CF	59CB",
+		"B0 B9	59D0",
+		"B8 C8	59D1",
+		"C0 AB	59D3",
+		"B0 D1	59D4",
+		"D5 AC	59D9",
+		"D5 AD	59DA",
+		"D5 AA	59DC",
+		"B1 B8	59E5",
+		"B4 AF	59E6",
+		"D5 A9	59E8",
+		"CC C5	59EA",
+		"C9 B1	59EB",
+		"B0 A8	59F6",
+		"B0 F9	59FB",
+		"BB D1	59FF",
+		"B0 D2	5A01",
+		"B0 A3	5A03",
+		"D5 B2	5A09",
+		"D5 B0	5A11",
+		"CC BC	5A18",
+		"D5 B3	5A1A",
+		"D5 B1	5A1C",
+		"D5 AF	5A1F",
+		"BF B1	5A20",
+		"D5 AE	5A25",
+		"CA DA	5A29",
+		"B8 E4	5A2F",
+		"D5 B7	5A35",
+		"D5 B8	5A36",
+		"BE AB	5A3C",
+		"D5 B4	5A40",
+		"CF AC	5A41",
+		"C7 CC	5A46",
+		"D5 B6	5A49",
+		"BA A7	5A5A",
+		"D5 B9	5A62",
+		"C9 D8	5A66",
+		"D5 BA	5A6A",
+		"D5 B5	5A6C",
+		"CC BB	5A7F",
+		"C7 DE	5A92",
+		"D5 BB	5A9A",
+		"C9 B2	5A9B",
+		"D5 BC	5ABC",
+		"D5 C0	5ABD",
+		"D5 BD	5ABE",
+		"B2 C7	5AC1",
+		"D5 BF	5AC2",
+		"BC BB	5AC9",
+		"D5 BE	5ACB",
+		"B7 F9	5ACC",
+		"D5 CC	5AD0",
+		"D5 C5	5AD6",
+		"D5 C2	5AD7",
+		"C3 E4	5AE1",
+		"D5 C1	5AE3",
+		"D5 C3	5AE6",
+		"D5 C4	5AE9",
+		"D5 C6	5AFA",
+		"D5 C7	5AFB",
+		"B4 F2	5B09",
+		"D5 C9	5B0B",
+		"D5 C8	5B0C",
+		"D5 CA	5B16",
+		"BE EE	5B22",
+		"D5 CD	5B2A",
+		"C4 DC	5B2C",
+		"B1 C5	5B30",
+		"D5 CB	5B32",
+		"D5 CE	5B36",
+		"D5 CF	5B3E",
+		"D5 D2	5B40",
+		"D5 D0	5B43",
+		"D5 D1	5B45",
+		"BB D2	5B50",
+		"D5 D3	5B51",
+		"B9 A6	5B54",
+		"D5 D4	5B55",
+		"BB FA	5B57",
+		"C2 B8	5B58",
+		"D5 D5	5B5A",
+		"D5 D6	5B5B",
+		"BB DA	5B5C",
+		"B9 A7	5B5D",
+		"CC D2	5B5F",
+		"B5 A8	5B63",
+		"B8 C9	5B64",
+		"D5 D7	5B65",
+		"B3 D8	5B66",
+		"D5 D8	5B69",
+		"C2 B9	5B6B",
+		"D5 D9	5B70",
+		"D6 A3	5B71",
+		"D5 DA	5B73",
+		"D5 DB	5B75",
+		"D5 DC	5B78",
+		"D5 DE	5B7A",
+		"D5 DF	5B80",
+		"D5 E0	5B83",
+		"C2 F0	5B85",
+		"B1 A7	5B87",
+		"BC E9	5B88",
+		"B0 C2	5B89",
+		"C1 D7	5B8B",
+		"B4 B0	5B8C",
+		"BC B5	5B8D",
+		"B9 A8	5B8F",
+		"C5 E6	5B95",
+		"BD A1	5B97",
+		"B4 B1	5B98",
+		"C3 E8	5B99",
+		"C4 EA	5B9A",
+		"B0 B8	5B9B",
+		"B5 B9	5B9C",
+		"CA F5	5B9D",
+		"BC C2	5B9F",
+		"B5 D2	5BA2",
+		"C0 EB	5BA3",
+		"BC BC	5BA4",
+		"CD A8	5BA5",
+		"D5 E1	5BA6",
+		"B5 DC	5BAE",
+		"BA CB	5BB0",
+		"B3 B2	5BB3",
+		"B1 E3	5BB4",
+		"BE AC	5BB5",
+		"B2 C8	5BB6",
+		"D5 E2	5BB8",
+		"CD C6	5BB9",
+		"BD C9	5BBF",
+		"BC E4	5BC2",
+		"D5 E3	5BC3",
+		"B4 F3	5BC4",
+		"C6 D2	5BC5",
+		"CC A9	5BC6",
+		"D5 E4	5BC7",
+		"D5 E5	5BC9",
+		"C9 D9	5BCC",
+		"D5 E7	5BD0",
+		"B4 A8	5BD2",
+		"B6 F7	5BD3",
+		"D5 E6	5BD4",
+		"B4 B2	5BDB",
+		"BF B2	5BDD",
+		"D5 EB	5BDE",
+		"BB A1	5BDF",
+		"B2 C9	5BE1",
+		"D5 EA	5BE2",
+		"D5 E8	5BE4",
+		"D5 EC	5BE5",
+		"D5 E9	5BE6",
+		"C7 AB	5BE7",
+		"DC CD	5BE8",
+		"BF B3	5BE9",
+		"D5 ED	5BEB",
+		"CE C0	5BEE",
+		"D5 EE	5BF0",
+		"D5 F0	5BF3",
+		"C3 FE	5BF5",
+		"D5 EF	5BF6",
+		"C0 A3	5BF8",
+		"BB FB	5BFA",
+		"C2 D0	5BFE",
+		"BC F7	5BFF",
+		"C9 F5	5C01",
+		"C0 EC	5C02",
+		"BC CD	5C04",
+		"D5 F1	5C05",
+		"BE AD	5C06",
+		"D5 F2	5C07",
+		"D5 F3	5C08",
+		"B0 D3	5C09",
+		"C2 BA	5C0A",
+		"BF D2	5C0B",
+		"D5 F4	5C0D",
+		"C6 B3	5C0E",
+		"BE AE	5C0F",
+		"BE AF	5C11",
+		"D5 F5	5C13",
+		"C0 ED	5C16",
+		"BE B0	5C1A",
+		"D5 F6	5C20",
+		"D5 F7	5C22",
+		"CC E0	5C24",
+		"D5 F8	5C28",
+		"B6 C6	5C2D",
+		"BD A2	5C31",
+		"D5 F9	5C38",
+		"D5 FA	5C39",
+		"BC DC	5C3A",
+		"BF AC	5C3B",
+		"C6 F4	5C3C",
+		"BF D4	5C3D",
+		"C8 F8	5C3E",
+		"C7 A2	5C3F",
+		"B6 C9	5C40",
+		"D5 FB	5C41",
+		"B5 EF	5C45",
+		"D5 FC	5C46",
+		"B6 FE	5C48",
+		"C6 CF	5C4A",
+		"B2 B0	5C4B",
+		"BB D3	5C4D",
+		"D5 FD	5C4E",
+		"D6 A2	5C4F",
+		"D6 A1	5C50",
+		"B6 FD	5C51",
+		"D5 FE	5C53",
+		"C5 B8	5C55",
+		"C2 B0	5C5E",
+		"C5 CB	5C60",
+		"BC C8	5C61",
+		"C1 D8	5C64",
+		"CD FA	5C65",
+		"D6 A4	5C6C",
+		"D6 A5	5C6E",
+		"C6 D6	5C6F",
+		"BB B3	5C71",
+		"D6 A7	5C76",
+		"D6 A8	5C79",
+		"D6 A9	5C8C",
+		"B4 F4	5C90",
+		"D6 AA	5C91",
+		"D6 AB	5C94",
+		"B2 AC	5CA1",
+		"C1 BB	5CA8",
+		"B4 E4	5CA9",
+		"D6 AD	5CAB",
+		"CC A8	5CAC",
+		"C2 D2	5CB1",
+		"B3 D9	5CB3",
+		"D6 AF	5CB6",
+		"D6 B1	5CB7",
+		"B4 DF	5CB8",
+		"D6 AE	5CBB",
+		"D6 B0	5CBC",
+		"D6 B3	5CBE",
+		"D6 B2	5CC5",
+		"D6 B4	5CC7",
+		"D6 B5	5CD9",
+		"C6 BD	5CE0",
+		"B6 AE	5CE1",
+		"B2 E5	5CE8",
+		"D6 B6	5CE9",
+		"D6 BB	5CEA",
+		"D6 B9	5CED",
+		"CA F7	5CEF",
+		"CA F6	5CF0",
+		"C5 E7	5CF6",
+		"D6 B8	5CFA",
+		"BD D4	5CFB",
+		"D6 B7	5CFD",
+		"BF F2	5D07",
+		"D6 BC	5D0B",
+		"BA EA	5D0E",
+		"D6 C2	5D11",
+		"D6 C3	5D14",
+		"D6 BD	5D15",
+		"B3 B3	5D16",
+		"D6 BE	5D17",
+		"D6 C7	5D18",
+		"D6 C6	5D19",
+		"D6 C5	5D1A",
+		"D6 C1	5D1B",
+		"D6 C0	5D1F",
+		"D6 C4	5D22",
+		"CA F8	5D29",
+		"D6 CB	5D4B",
+		"D6 C8	5D4C",
+		"D6 CA	5D4E",
+		"CD F2	5D50",
+		"D6 C9	5D52",
+		"D6 BF	5D5C",
+		"BF F3	5D69",
+		"D6 CC	5D6C",
+		"BA B7	5D6F",
+		"D6 CD	5D73",
+		"D6 CE	5D76",
+		"D6 D1	5D82",
+		"D6 D0	5D84",
+		"D6 CF	5D87",
+		"C5 E8	5D8B",
+		"D6 BA	5D8C",
+		"D6 D7	5D90",
+		"D6 D3	5D9D",
+		"D6 D2	5DA2",
+		"D6 D4	5DAC",
+		"D6 D5	5DAE",
+		"D6 D8	5DB7",
+		"CE E6	5DBA",
+		"D6 D9	5DBC",
+		"D6 D6	5DBD",
+		"D6 DA	5DC9",
+		"B4 E0	5DCC",
+		"D6 DB	5DCD",
+		"D6 DD	5DD2",
+		"D6 DC	5DD3",
+		"D6 DE	5DD6",
+		"D6 DF	5DDB",
+		"C0 EE	5DDD",
+		"BD A3	5DDE",
+		"BD E4	5DE1",
+		"C1 E3	5DE3",
+		"B9 A9	5DE5",
+		"BA B8	5DE6",
+		"B9 AA	5DE7",
+		"B5 F0	5DE8",
+		"D6 E0	5DEB",
+		"BA B9	5DEE",
+		"B8 CA	5DF1",
+		"D6 E1	5DF2",
+		"CC A6	5DF3",
+		"C7 C3	5DF4",
+		"D6 E2	5DF5",
+		"B9 AB	5DF7",
+		"B4 AC	5DFB",
+		"C3 A7	5DFD",
+		"B6 D2	5DFE",
+		"BB D4	5E02",
+		"C9 DB	5E03",
+		"C8 C1	5E06",
+		"D6 E3	5E0B",
+		"B4 F5	5E0C",
+		"D6 E6	5E11",
+		"C4 A1	5E16",
+		"D6 E5	5E19",
+		"D6 E4	5E1A",
+		"D6 E7	5E1B",
+		"C4 EB	5E1D",
+		"BF E3	5E25",
+		"BB D5	5E2B",
+		"C0 CA	5E2D",
+		"C2 D3	5E2F",
+		"B5 A2	5E30",
+		"C4 A2	5E33",
+		"D6 E8	5E36",
+		"D6 E9	5E37",
+		"BE EF	5E38",
+		"CB B9	5E3D",
+		"D6 EC	5E40",
+		"D6 EB	5E43",
+		"D6 EA	5E44",
+		"C9 FD	5E45",
+		"D6 F3	5E47",
+		"CB DA	5E4C",
+		"D6 ED	5E4E",
+		"D6 EF	5E54",
+		"CB EB	5E55",
+		"D6 EE	5E57",
+		"D6 F0	5E5F",
+		"C8 A8	5E61",
+		"D6 F1	5E62",
+		"CA BE	5E63",
+		"D6 F2	5E64",
+		"B4 B3	5E72",
+		"CA BF	5E73",
+		"C7 AF	5E74",
+		"D6 F4	5E75",
+		"D6 F5	5E76",
+		"B9 AC	5E78",
+		"B4 B4	5E79",
+		"D6 F6	5E7A",
+		"B8 B8	5E7B",
+		"CD C4	5E7C",
+		"CD A9	5E7D",
+		"B4 F6	5E7E",
+		"D6 F8	5E7F",
+		"C4 A3	5E81",
+		"B9 AD	5E83",
+		"BE B1	5E84",
+		"C8 DF	5E87",
+		"BE B2	5E8A",
+		"BD F8	5E8F",
+		"C4 EC	5E95",
+		"CA F9	5E96",
+		"C5 B9	5E97",
+		"B9 AE	5E9A",
+		"C9 DC	5E9C",
+		"D6 F9	5EA0",
+		"C5 D9	5EA6",
+		"BA C2	5EA7",
+		"B8 CB	5EAB",
+		"C4 ED	5EAD",
+		"B0 C3	5EB5",
+		"BD EE	5EB6",
+		"B9 AF	5EB7",
+		"CD C7	5EB8",
+		"D6 FA	5EC1",
+		"D6 FB	5EC2",
+		"C7 D1	5EC3",
+		"D6 FC	5EC8",
+		"CE F7	5EC9",
+		"CF AD	5ECA",
+		"D6 FE	5ECF",
+		"D6 FD	5ED0",
+		"B3 C7	5ED3",
+		"D7 A1	5ED6",
+		"D7 A4	5EDA",
+		"D7 A5	5EDB",
+		"D7 A3	5EDD",
+		"C9 C0	5EDF",
+		"BE B3	5EE0",
+		"D7 A7	5EE1",
+		"D7 A6	5EE2",
+		"D7 A2	5EE3",
+		"D7 A8	5EE8",
+		"D7 A9	5EE9",
+		"D7 AA	5EEC",
+		"D7 AD	5EF0",
+		"D7 AB	5EF1",
+		"D7 AC	5EF3",
+		"D7 AE	5EF4",
+		"B1 E4	5EF6",
+		"C4 EE	5EF7",
+		"D7 AF	5EF8",
+		"B7 FA	5EFA",
+		"B2 F6	5EFB",
+		"C7 B6	5EFC",
+		"D7 B0	5EFE",
+		"C6 FB	5EFF",
+		"CA DB	5F01",
+		"D7 B1	5F03",
+		"CF AE	5F04",
+		"D7 B2	5F09",
+		"CA C0	5F0A",
+		"D7 B5	5F0B",
+		"D0 A1	5F0C",
+		"D0 B1	5F0D",
+		"BC B0	5F0F",
+		"C6 F5	5F10",
+		"D7 B6	5F11",
+		"B5 DD	5F13",
+		"C4 A4	5F14",
+		"B0 FA	5F15",
+		"D7 B7	5F16",
+		"CA A6	5F17",
+		"B9 B0	5F18",
+		"C3 D0	5F1B",
+		"C4 EF	5F1F",
+		"CC EF	5F25",
+		"B8 B9	5F26",
+		"B8 CC	5F27",
+		"D7 B8	5F29",
+		"D7 B9	5F2D",
+		"D7 BF	5F2F",
+		"BC E5	5F31",
+		"C4 A5	5F35",
+		"B6 AF	5F37",
+		"D7 BA	5F38",
+		"C9 AB	5F3C",
+		"C3 C6	5F3E",
+		"D7 BB	5F41",
+		"D7 BC	5F48",
+		"B6 B0	5F4A",
+		"D7 BD	5F4C",
+		"D7 BE	5F4E",
+		"D7 C0	5F51",
+		"C5 F6	5F53",
+		"D7 C1	5F56",
+		"D7 C2	5F57",
+		"D7 C3	5F59",
+		"D7 B4	5F5C",
+		"D7 B3	5F5D",
+		"D7 C4	5F61",
+		"B7 C1	5F62",
+		"C9 A7	5F66",
+		"BA CC	5F69",
+		"C9 B7	5F6A",
+		"C4 A6	5F6B",
+		"C9 CB	5F6C",
+		"D7 C5	5F6D",
+		"BE B4	5F70",
+		"B1 C6	5F71",
+		"D7 C6	5F73",
+		"D7 C7	5F77",
+		"CC F2	5F79",
+		"C8 E0	5F7C",
+		"D7 CA	5F7F",
+		"B1 FD	5F80",
+		"C0 AC	5F81",
+		"D7 C9	5F82",
+		"D7 C8	5F83",
+		"B7 C2	5F84",
+		"C2 D4	5F85",
+		"D7 CE	5F87",
+		"D7 CC	5F88",
+		"D7 CB	5F8A",
+		"CE A7	5F8B",
+		"B8 E5	5F8C",
+		"BD F9	5F90",
+		"D7 CD	5F91",
+		"C5 CC	5F92",
+		"BD BE	5F93",
+		"C6 C0	5F97",
+		"D7 D1	5F98",
+		"D7 D0	5F99",
+		"D7 CF	5F9E",
+		"D7 D2	5FA0",
+		"B8 E6	5FA1",
+		"D7 D3	5FA8",
+		"C9 FC	5FA9",
+		"BD DB	5FAA",
+		"D7 D4	5FAD",
+		"C8 F9	5FAE",
+		"C6 C1	5FB3",
+		"C4 A7	5FB4",
+		"C5 B0	5FB9",
+		"D7 D5	5FBC",
+		"B5 AB	5FBD",
+		"BF B4	5FC3",
+		"C9 AC	5FC5",
+		"B4 F7	5FCC",
+		"C7 A6	5FCD",
+		"D7 D6	5FD6",
+		"BB D6	5FD7",
+		"CB BA	5FD8",
+		"CB BB	5FD9",
+		"B1 FE	5FDC",
+		"D7 DB	5FDD",
+		"C3 E9	5FE0",
+		"D7 D8	5FE4",
+		"B2 F7	5FEB",
+		"D8 AD	5FF0",
+		"D7 DA	5FF1",
+		"C7 B0	5FF5",
+		"D7 D9	5FF8",
+		"D7 D7	5FFB",
+		"B9 FA	5FFD",
+		"D7 DD	5FFF",
+		"D7 E3	600E",
+		"D7 E9	600F",
+		"D7 E1	6010",
+		"C5 DC	6012",
+		"D7 E6	6015",
+		"C9 DD	6016",
+		"D7 E0	6019",
+		"D7 E5	601B",
+		"CE E7	601C",
+		"BB D7	601D",
+		"C2 D5	6020",
+		"D7 DE	6021",
+		"B5 DE	6025",
+		"D7 E8	6026",
+		"C0 AD	6027",
+		"B1 E5	6028",
+		"D7 E2	6029",
+		"B2 F8	602A",
+		"D7 E7	602B",
+		"B6 B1	602F",
+		"D7 E4	6031",
+		"D7 EA	603A",
+		"D7 EC	6041",
+		"D7 F6	6042",
+		"D7 F4	6043",
+		"D7 F1	6046",
+		"D7 F0	604A",
+		"CE F8	604B",
+		"D7 F2	604D",
+		"B6 B2	6050",
+		"B9 B1	6052",
+		"BD FA	6055",
+		"D7 F9	6059",
+		"D7 EB	605A",
+		"D7 EF	605F",
+		"D7 DF	6060",
+		"B2 FA	6062",
+		"D7 F3	6063",
+		"D7 F5	6064",
+		"C3 D1	6065",
+		"BA A8	6068",
+		"B2 B8	6069",
+		"D7 ED	606A",
+		"D7 F8	606B",
+		"D7 F7	606C",
+		"B6 B3	606D",
+		"C2 A9	606F",
+		"B3 E6	6070",
+		"B7 C3	6075",
+		"D7 EE	6077",
+		"D7 FA	6081",
+		"D7 FD	6083",
+		"D8 A1	6084",
+		"BC BD	6089",
+		"D8 A7	608B",
+		"C4 F0	608C",
+		"D7 FB	608D",
+		"D8 A5	6092",
+		"B2 F9	6094",
+		"D8 A3	6096",
+		"D8 A4	6097",
+		"D7 FE	609A",
+		"D8 A2	609B",
+		"B8 E7	609F",
+		"CD AA	60A0",
+		"B4 B5	60A3",
+		"B1 D9	60A6",
+		"D8 A6	60A7",
+		"C7 BA	60A9",
+		"B0 AD	60AA",
+		"C8 E1	60B2",
+		"D7 DC	60B3",
+		"D8 AC	60B4",
+		"D8 B0	60B5",
+		"CC E5	60B6",
+		"D8 A9	60B8",
+		"C5 E9	60BC",
+		"D8 AE	60BD",
+		"BE F0	60C5",
+		"D8 AF	60C6",
+		"C6 D7	60C7",
+		"CF C7	60D1",
+		"D8 AB	60D3",
+		"D8 B1	60D8",
+		"B9 FB	60DA",
+		"C0 CB	60DC",
+		"B0 D4	60DF",
+		"D8 AA	60E0",
+		"D8 A8	60E1",
+		"C1 DA	60E3",
+		"D7 FC	60E7",
+		"BB B4	60E8",
+		"C2 C6	60F0",
+		"D8 BD	60F1",
+		"C1 DB	60F3",
+		"D8 B8	60F4",
+		"D8 B5	60F6",
+		"D8 B6	60F7",
+		"BC E6	60F9",
+		"D8 B9	60FA",
+		"D8 BC	60FB",
+		"D8 B7	6100",
+		"BD A5	6101",
+		"D8 BA	6103",
+		"D8 B4	6106",
+		"CC FC	6108",
+		"CC FB	6109",
+		"D8 BE	610D",
+		"D8 BF	610E",
+		"B0 D5	610F",
+		"D8 B3	6115",
+		"B6 F2	611A",
+		"B0 A6	611B",
+		"B4 B6	611F",
+		"D8 BB	6121",
+		"D8 C3	6127",
+		"D8 C2	6128",
+		"D8 C7	612C",
+		"D8 C8	6134",
+		"D8 C6	613C",
+		"D8 C9	613D",
+		"D8 C1	613E",
+		"D8 C5	613F",
+		"D8 CA	6142",
+		"D8 CB	6144",
+		"D8 C0	6147",
+		"BB FC	6148",
+		"D8 C4	614A",
+		"C2 D6	614B",
+		"B9 B2	614C",
+		"D8 B2	614D",
+		"BF B5	614E",
+		"D8 D8	6153",
+		"CA E9	6155",
+		"D8 CE	6158",
+		"D8 CF	6159",
+		"D8 D0	615A",
+		"D8 D7	615D",
+		"D8 D6	615F",
+		"CB FD	6162",
+		"B4 B7	6163",
+		"D8 D4	6165",
+		"B7 C5	6167",
+		"B3 B4	6168",
+		"D8 D1	616B",
+		"CE B8	616E",
+		"D8 D3	616F",
+		"B0 D6	6170",
+		"D8 D5	6171",
+		"D8 CC	6173",
+		"D8 D2	6174",
+		"D8 D9	6175",
+		"B7 C4	6176",
+		"D8 CD	6177",
+		"CD DD	617E",
+		"CD AB	6182",
+		"D8 DC	6187",
+		"D8 E0	618A",
+		"C1 FE	618E",
+		"CE F9	6190",
+		"D8 E1	6191",
+		"D8 DE	6194",
+		"D8 DB	6196",
+		"D8 DA	6199",
+		"D8 DF	619A",
+		"CA B0	61A4",
+		"C6 B4	61A7",
+		"B7 C6	61A9",
+		"D8 E2	61AB",
+		"D8 DD	61AC",
+		"D8 E3	61AE",
+		"B7 FB	61B2",
+		"B2 B1	61B6",
+		"D8 EB	61BA",
+		"B4 B8	61BE",
+		"D8 E9	61C3",
+		"D8 EA	61C6",
+		"BA A9	61C7",
+		"D8 E8	61C8",
+		"D8 E6	61C9",
+		"D8 E5	61CA",
+		"D8 EC	61CB",
+		"D8 E4	61CC",
+		"D8 EE	61CD",
+		"B2 FB	61D0",
+		"D8 F0	61E3",
+		"D8 EF	61E6",
+		"C4 A8	61F2",
+		"D8 F3	61F4",
+		"D8 F1	61F6",
+		"D8 E7	61F7",
+		"B7 FC	61F8",
+		"D8 F2	61FA",
+		"D8 F6	61FC",
+		"D8 F5	61FD",
+		"D8 F7	61FE",
+		"D8 F4	61FF",
+		"D8 F8	6200",
+		"D8 F9	6208",
+		"D8 FA	6209",
+		"CA EA	620A",
+		"D8 FC	620C",
+		"D8 FB	620D",
+		"BD BF	620E",
+		"C0 AE	6210",
+		"B2 E6	6211",
+		"B2 FC	6212",
+		"D8 FD	6214",
+		"B0 BF	6216",
+		"C0 CC	621A",
+		"D8 FE	621B",
+		"EC C3	621D",
+		"D9 A1	621E",
+		"B7 E1	621F",
+		"D9 A2	6221",
+		"C0 EF	6226",
+		"D9 A3	622A",
+		"D9 A4	622E",
+		"B5 BA	622F",
+		"D9 A5	6230",
+		"D9 A6	6232",
+		"D9 A7	6233",
+		"C2 D7	6234",
+		"B8 CD	6238",
+		"CC E1	623B",
+		"CB BC	623F",
+		"BD EA	6240",
+		"D9 A8	6241",
+		"C0 F0	6247",
+		"EE BD	6248",
+		"C8 E2	6249",
+		"BC EA	624B",
+		"BA CD	624D",
+		"D9 A9	624E",
+		"C2 C7	6253",
+		"CA A7	6255",
+		"C2 F1	6258",
+		"D9 AC	625B",
+		"D9 AA	625E",
+		"D9 AD	6260",
+		"D9 AB	6263",
+		"D9 AE	6268",
+		"CA B1	626E",
+		"B0 B7	6271",
+		"C9 DE	6276",
+		"C8 E3	6279",
+		"D9 AF	627C",
+		"D9 B2	627E",
+		"BE B5	627F",
+		"B5 BB	6280",
+		"D9 B0	6282",
+		"D9 B7	6283",
+		"BE B6	6284",
+		"D9 B1	6289",
+		"C7 C4	628A",
+		"CD DE	6291",
+		"D9 B3	6292",
+		"D9 B4	6293",
+		"D9 B8	6294",
+		"C5 EA	6295",
+		"D9 B5	6296",
+		"B9 B3	6297",
+		"C0 DE	6298",
+		"D9 C6	629B",
+		"C8 B4	629C",
+		"C2 F2	629E",
+		"C8 E4	62AB",
+		"DA AD	62AC",
+		"CA FA	62B1",
+		"C4 F1	62B5",
+		"CB F5	62B9",
+		"D9 BB	62BB",
+		"B2 A1	62BC",
+		"C3 EA	62BD",
+		"D9 C4	62C2",
+		"C3 B4	62C5",
+		"D9 BE	62C6",
+		"D9 C5	62C7",
+		"D9 C0	62C8",
+		"D9 C7	62C9",
+		"D9 C3	62CA",
+		"D9 C2	62CC",
+		"C7 EF	62CD",
+		"D9 BC	62CF",
+		"B2 FD	62D0",
+		"D9 BA	62D1",
+		"B5 F1	62D2",
+		"C2 F3	62D3",
+		"D9 B6	62D4",
+		"D9 B9	62D7",
+		"B9 B4	62D8",
+		"C0 DB	62D9",
+		"BE B7	62DB",
+		"D9 C1	62DC",
+		"C7 D2	62DD",
+		"B5 F2	62E0",
+		"B3 C8	62E1",
+		"B3 E7	62EC",
+		"BF A1	62ED",
+		"D9 C9	62EE",
+		"D9 CE	62EF",
+		"D9 CA	62F1",
+		"B7 FD	62F3",
+		"D9 CF	62F5",
+		"BB A2	62F6",
+		"B9 E9	62F7",
+		"BD A6	62FE",
+		"D9 BD	62FF",
+		"BB FD	6301",
+		"D9 CC	6302",
+		"BB D8	6307",
+		"D9 CD	6308",
+		"B0 C4	6309",
+		"D9 C8	630C",
+		"C4 A9	6311",
+		"B5 F3	6319",
+		"B6 B4	631F",
+		"D9 CB	6327",
+		"B0 A7	6328",
+		"BA C3	632B",
+		"BF B6	632F",
+		"C4 F2	633A",
+		"C8 D4	633D",
+		"D9 D1	633E",
+		"C1 DE	633F",
+		"C2 AA	6349",
+		"BB AB	634C",
+		"D9 D2	634D",
+		"D9 D4	634F",
+		"D9 D0	6350",
+		"CA E1	6355",
+		"C4 BD	6357",
+		"C1 DC	635C",
+		"CA FB	6367",
+		"BC CE	6368",
+		"D9 E0	6369",
+		"D9 DF	636B",
+		"BF F8	636E",
+		"B7 FE	6372",
+		"D9 D9	6376",
+		"BE B9	6377",
+		"C6 E8	637A",
+		"C7 B1	637B",
+		"D9 D7	6380",
+		"C1 DD	6383",
+		"BC F8	6388",
+		"D9 DC	6389",
+		"BE B8	638C",
+		"D9 D6	638E",
+		"D9 DB	638F",
+		"C7 D3	6392",
+		"D9 D5	6396",
+		"B7 A1	6398",
+		"B3 DD	639B",
+		"D9 DD	639F",
+		"CE AB	63A0",
+		"BA CE	63A1",
+		"C3 B5	63A2",
+		"D9 DA	63A3",
+		"C0 DC	63A5",
+		"B9 B5	63A7",
+		"BF E4	63A8",
+		"B1 E6	63A9",
+		"C1 BC	63AA",
+		"D9 D8	63AB",
+		"B5 C5	63AC",
+		"B7 C7	63B2",
+		"C4 CF	63B4",
+		"D9 DE	63B5",
+		"C1 DF	63BB",
+		"D9 E1	63BE",
+		"D9 E3	63C0",
+		"C2 B7	63C3",
+		"D9 E9	63C4",
+		"D9 E4	63C6",
+		"D9 E6	63C9",
+		"C9 C1	63CF",
+		"C4 F3	63D0",
+		"D9 E7	63D2",
+		"CD AC	63D6",
+		"CD C8	63DA",
+		"B4 B9	63DB",
+		"B0 AE	63E1",
+		"D9 E5	63E3",
+		"D9 E2	63E9",
+		"B4 F8	63EE",
+		"B1 E7	63F4",
+		"D9 E8	63F6",
+		"CD C9	63FA",
+		"D9 EC	6406",
+		"C2 BB	640D",
+		"D9 F3	640F",
+		"D9 ED	6413",
+		"D9 EA	6416",
+		"D9 F1	6417",
+		"D9 D3	641C",
+		"D9 EE	6426",
+		"D9 F2	6428",
+		"C8 C2	642C",
+		"C5 EB	642D",
+		"D9 EB	6434",
+		"D9 EF	6436",
+		"B7 C8	643A",
+		"BA F1	643E",
+		"C0 DD	6442",
+		"D9 F7	644E",
+		"C5 A6	6458",
+		"D9 F4	6467",
+		"CB E0	6469",
+		"D9 F5	646F",
+		"D9 F6	6476",
+		"CC CE	6478",
+		"C0 A2	647A",
+		"B7 E2	6483",
+		"D9 FD	6488",
+		"BB B5	6492",
+		"D9 FA	6493",
+		"D9 F9	6495",
+		"C7 B2	649A",
+		"C6 B5	649E",
+		"C5 B1	64A4",
+		"D9 FB	64A5",
+		"D9 FC	64A9",
+		"C9 EF	64AB",
+		"C7 C5	64AD",
+		"BB A3	64AE",
+		"C0 F1	64B0",
+		"CB D0	64B2",
+		"B3 C9	64B9",
+		"DA A5	64BB",
+		"D9 FE	64BC",
+		"CD CA	64C1",
+		"DA A7	64C2",
+		"DA A3	64C5",
+		"DA A4	64C7",
+		"C1 E0	64CD",
+		"DA A2	64D2",
+		"D9 BF	64D4",
+		"DA A6	64D8",
+		"DA A1	64DA",
+		"DA AB	64E0",
+		"DA AC	64E1",
+		"C5 A7	64E2",
+		"DA AE	64E3",
+		"BB A4	64E6",
+		"DA A9	64E7",
+		"B5 BC	64EC",
+		"DA AF	64EF",
+		"DA A8	64F1",
+		"DA B3	64F2",
+		"DA B2	64F4",
+		"DA B1	64F6",
+		"DA B4	64FA",
+		"DA B6	64FD",
+		"BE F1	64FE",
+		"DA B5	6500",
+		"DA B9	6505",
+		"DA B7	6518",
+		"DA B8	651C",
+		"D9 F0	651D",
+		"DA BB	6523",
+		"DA BA	6524",
+		"D9 F8	652A",
+		"DA BC	652B",
+		"DA B0	652C",
+		"BB D9	652F",
+		"DA BD	6534",
+		"DA BE	6535",
+		"DA C0	6536",
+		"DA BF	6537",
+		"DA C1	6538",
+		"B2 FE	6539",
+		"B9 B6	653B",
+		"CA FC	653E",
+		"C0 AF	653F",
+		"B8 CE	6545",
+		"DA C3	6548",
+		"DA C6	654D",
+		"C9 D2	654F",
+		"B5 DF	6551",
+		"DA C5	6555",
+		"DA C4	6556",
+		"C7 D4	6557",
+		"DA C7	6558",
+		"B6 B5	6559",
+		"DA C9	655D",
+		"DA C8	655E",
+		"B4 BA	6562",
+		"BB B6	6563",
+		"C6 D8	6566",
+		"B7 C9	656C",
+		"BF F4	6570",
+		"DA CA	6572",
+		"C0 B0	6574",
+		"C5 A8	6575",
+		"C9 DF	6577",
+		"DA CB	6578",
+		"DA CC	6582",
+		"DA CD	6583",
+		"CA B8	6587",
+		"D5 DD	6588",
+		"C0 C6	6589",
+		"C9 CC	658C",
+		"BA D8	658E",
+		"C8 E5	6590",
+		"C8 C3	6591",
+		"C5 CD	6597",
+		"CE C1	6599",
+		"DA CF	659B",
+		"BC D0	659C",
+		"DA D0	659F",
+		"B0 B6	65A1",
+		"B6 D4	65A4",
+		"C0 CD	65A5",
+		"C9 E0	65A7",
+		"DA D1	65AB",
+		"BB C2	65AC",
+		"C3 C7	65AD",
+		"BB DB	65AF",
+		"BF B7	65B0",
+		"DA D2	65B7",
+		"CA FD	65B9",
+		"B1 F7	65BC",
+		"BB DC	65BD",
+		"DA D5	65C1",
+		"DA D3	65C3",
+		"DA D6	65C4",
+		"CE B9	65C5",
+		"DA D4	65C6",
+		"C0 FB	65CB",
+		"DA D7	65CC",
+		"C2 B2	65CF",
+		"DA D8	65D2",
+		"B4 FA	65D7",
+		"DA DA	65D9",
+		"DA D9	65DB",
+		"DA DB	65E0",
+		"DA DC	65E1",
+		"B4 FB	65E2",
+		"C6 FC	65E5",
+		"C3 B6	65E6",
+		"B5 EC	65E7",
+		"BB DD	65E8",
+		"C1 E1	65E9",
+		"BD DC	65EC",
+		"B0 B0	65ED",
+		"DA DD	65F1",
+		"B2 A2	65FA",
+		"DA E1	65FB",
+		"B9 B7	6602",
+		"DA E0	6603",
+		"BA AB	6606",
+		"BE BA	6607",
+		"DA DF	660A",
+		"BE BB	660C",
+		"CC C0	660E",
+		"BA AA	660F",
+		"B0 D7	6613",
+		"C0 CE	6614",
+		"DA E6	661C",
+		"C0 B1	661F",
+		"B1 C7	6620",
+		"BD D5	6625",
+		"CB E6	6627",
+		"BA F2	6628",
+		"BE BC	662D",
+		"C0 A7	662F",
+		"DA E5	6634",
+		"DA E3	6635",
+		"DA E4	6636",
+		"C3 EB	663C",
+		"DB A6	663F",
+		"DA EA	6641",
+		"BB FE	6642",
+		"B9 B8	6643",
+		"DA E8	6644",
+		"DA E9	6649",
+		"BF B8	664B",
+		"DA E7	664F",
+		"BB AF	6652",
+		"DA EC	665D",
+		"DA EB	665E",
+		"DA F0	665F",
+		"DA F1	6662",
+		"DA ED	6664",
+		"B3 A2	6666",
+		"DA EE	6667",
+		"DA EF	6668",
+		"C8 D5	6669",
+		"C9 E1	666E",
+		"B7 CA	666F",
+		"DA F2	6670",
+		"C0 B2	6674",
+		"BE BD	6676",
+		"C3 D2	667A",
+		"B6 C7	6681",
+		"DA F3	6683",
+		"DA F7	6684",
+		"B2 CB	6687",
+		"DA F4	6688",
+		"DA F6	6689",
+		"DA F5	668E",
+		"BD EB	6691",
+		"C3 C8	6696",
+		"B0 C5	6697",
+		"DA F8	6698",
+		"DA F9	669D",
+		"C4 AA	66A2",
+		"CE F1	66A6",
+		"BB C3	66AB",
+		"CA EB	66AE",
+		"CB BD	66B4",
+		"DB A2	66B8",
+		"DA FB	66B9",
+		"DA FE	66BC",
+		"DA FD	66BE",
+		"DA FA	66C1",
+		"DB A1	66C4",
+		"C6 DE	66C7",
+		"DA FC	66C9",
+		"DB A3	66D6",
+		"BD EC	66D9",
+		"DB A4	66DA",
+		"CD CB	66DC",
+		"C7 F8	66DD",
+		"DB A5	66E0",
+		"DB A7	66E6",
+		"DB A8	66E9",
+		"DB A9	66F0",
+		"B6 CA	66F2",
+		"B1 C8	66F3",
+		"B9 B9	66F4",
+		"DB AA	66F5",
+		"DB AB	66F7",
+		"BD F1	66F8",
+		"C1 E2	66F9",
+		"D2 D8	66FC",
+		"C1 BE	66FD",
+		"C1 BD	66FE",
+		"C2 D8	66FF",
+		"BA C7	6700",
+		"D0 F2	6703",
+		"B7 EE	6708",
+		"CD AD	6709",
+		"CA FE	670B",
+		"C9 FE	670D",
+		"DB AC	670F",
+		"BA F3	6714",
+		"C4 BF	6715",
+		"DB AD	6716",
+		"CF AF	6717",
+		"CB BE	671B",
+		"C4 AB	671D",
+		"DB AE	671E",
+		"B4 FC	671F",
+		"DB AF	6726",
+		"DB B0	6727",
+		"CC DA	6728",
+		"CC A4	672A",
+		"CB F6	672B",
+		"CB DC	672C",
+		"BB A5	672D",
+		"DB B2	672E",
+		"BC EB	6731",
+		"CB D1	6734",
+		"DB B4	6736",
+		"DB B7	6737",
+		"DB B6	6738",
+		"B4 F9	673A",
+		"B5 E0	673D",
+		"DB B3	673F",
+		"DB B5	6741",
+		"DB B8	6746",
+		"BF F9	6749",
+		"CD FB	674E",
+		"B0 C9	674F",
+		"BA E0	6750",
+		"C2 BC	6751",
+		"BC DD	6753",
+		"BE F3	6756",
+		"DB BB	6759",
+		"C5 CE	675C",
+		"DB B9	675E",
+		"C2 AB	675F",
+		"DB BA	6760",
+		"BE F2	6761",
+		"CC DD	6762",
+		"DB BC	6763",
+		"DB BD	6764",
+		"CD E8	6765",
+		"DB C2	676A",
+		"B9 BA	676D",
+		"C7 D5	676F",
+		"DB BF	6770",
+		"C5 EC	6771",
+		"DA DE	6772",
+		"DA E2	6773",
+		"B5 CF	6775",
+		"C7 C7	6777",
+		"DB C1	677C",
+		"BE BE	677E",
+		"C8 C4	677F",
+		"DB C7	6785",
+		"C8 FA	6787",
+		"DB BE	6789",
+		"DB C4	678B",
+		"DB C3	678C",
+		"C0 CF	6790",
+		"CB ED	6795",
+		"CE D3	6797",
+		"CB E7	679A",
+		"B2 CC	679C",
+		"BB DE	679D",
+		"CF C8	67A0",
+		"DB C6	67A1",
+		"BF F5	67A2",
+		"DB C5	67A6",
+		"DB C0	67A9",
+		"B8 CF	67AF",
+		"DB CC	67B3",
+		"DB CA	67B4",
+		"B2 CD	67B6",
+		"DB C8	67B7",
+		"DB CE	67B8",
+		"DB D4	67B9",
+		"C2 C8	67C1",
+		"CA C1	67C4",
+		"DB D6	67C6",
+		"C9 A2	67CA",
+		"DB D5	67CE",
+		"C7 F0	67CF",
+		"CB BF	67D0",
+		"B4 BB	67D1",
+		"C0 F7	67D3",
+		"BD C0	67D4",
+		"C4 D3	67D8",
+		"CD AE	67DA",
+		"DB D1	67DD",
+		"DB D0	67DE",
+		"DB D2	67E2",
+		"DB CF	67E4",
+		"DB D7	67E7",
+		"DB CD	67E9",
+		"DB CB	67EC",
+		"DB D3	67EE",
+		"DB C9	67EF",
+		"C3 EC	67F1",
+		"CC F8	67F3",
+		"BC C6	67F4",
+		"BA F4	67F5",
+		"BA BA	67FB",
+		"CB EF	67FE",
+		"B3 C1	67FF",
+		"C4 CE	6802",
+		"C6 CA	6803",
+		"B1 C9	6804",
+		"C0 F2	6813",
+		"C0 B4	6816",
+		"B7 AA	6817",
+		"DB D9	681E",
+		"B9 BB	6821",
+		"B3 FC	6822",
+		"DB DB	6829",
+		"B3 F4	682A",
+		"DB E1	682B",
+		"DB DE	6832",
+		"C0 F3	6834",
+		"B3 CB	6838",
+		"BA AC	6839",
+		"B3 CA	683C",
+		"BA CF	683D",
+		"DB DC	6840",
+		"B7 E5	6841",
+		"B7 CB	6842",
+		"C5 ED	6843",
+		"DB DA	6846",
+		"B0 C6	6848",
+		"DB DD	684D",
+		"DB DF	684E",
+		"B6 CD	6850",
+		"B7 AC	6851",
+		"B4 BC	6853",
+		"B5 CB	6854",
+		"DB E2	6859",
+		"BA F9	685C",
+		"CB F1	685D",
+		"BB B7	685F",
+		"DB E3	6863",
+		"C9 B0	6867",
+		"DB EF	6874",
+		"B2 B3	6876",
+		"DB E4	6877",
+		"DB F5	687E",
+		"DB E5	687F",
+		"CE C2	6881",
+		"DB EC	6883",
+		"C7 DF	6885",
+		"DB F4	688D",
+		"DB E7	688F",
+		"B0 B4	6893",
+		"DB E9	6894",
+		"B9 BC	6897",
+		"DB EB	689B",
+		"DB EA	689D",
+		"DB E6	689F",
+		"DB F1	68A0",
+		"BE BF	68A2",
+		"D4 ED	68A6",
+		"B8 E8	68A7",
+		"CD FC	68A8",
+		"DB E8	68AD",
+		"C4 F4	68AF",
+		"B3 A3	68B0",
+		"BA AD	68B1",
+		"DB E0	68B3",
+		"DB F0	68B5",
+		"B3 E1	68B6",
+		"DB EE	68B9",
+		"DB F2	68BA",
+		"C5 EE	68BC",
+		"B4 FE	68C4",
+		"DC B2	68C6",
+		"CC C9	68C9",
+		"DB F7	68CA",
+		"B4 FD	68CB",
+		"DB FE	68CD",
+		"CB C0	68D2",
+		"DC A1	68D4",
+		"DC A3	68D5",
+		"DC A7	68D7",
+		"DB F9	68D8",
+		"C3 AA	68DA",
+		"C5 EF	68DF",
+		"DC AB	68E0",
+		"DB FC	68E1",
+		"DC A8	68E3",
+		"DC A2	68E7",
+		"BF B9	68EE",
+		"DC AC	68EF",
+		"C0 B3	68F2",
+		"DC AA	68F9",
+		"B4 BD	68FA",
+		"CF D0	6900",
+		"DB F6	6901",
+		"DC A6	6904",
+		"B0 D8	6905",
+		"DB F8	6908",
+		"CC BA	690B",
+		"DB FD	690C",
+		"BF A2	690D",
+		"C4 C7	690E",
+		"DB F3	690F",
+		"DC A5	6912",
+		"BF FA	6919",
+		"DC AF	691A",
+		"B3 F1	691B",
+		"B8 A1	691C",
+		"DC B1	6921",
+		"DB FA	6922",
+		"DC B0	6923",
+		"DC A9	6925",
+		"DB FB	6926",
+		"DC AD	6928",
+		"DC AE	692A",
+		"DC BF	6930",
+		"C6 CE	6934",
+		"DC A4	6936",
+		"DC BB	6939",
+		"DC BD	693D",
+		"C4 D8	693F",
+		"CD CC	694A",
+		"C9 F6	6953",
+		"DC B8	6954",
+		"C2 CA	6955",
+		"DC BE	6959",
+		"C1 BF	695A",
+		"DC B5	695C",
+		"DC C2	695D",
+		"DC C1	695E",
+		"C6 EF	6960",
+		"DC C0	6961",
+		"C6 EA	6962",
+		"DC C4	696A",
+		"DC B7	696B",
+		"B6 C8	696D",
+		"DC BA	696E",
+		"BD DD	696F",
+		"C7 E0	6973",
+		"DC BC	6974",
+		"B6 CB	6975",
+		"DC B4	6977",
+		"DC B6	6978",
+		"DC B3	6979",
+		"CF B0	697C",
+		"B3 DA	697D",
+		"DC B9	697E",
+		"DC C3	6981",
+		"B3 B5	6982",
+		"BA E7	698A",
+		"B1 DD	698E",
+		"DC D4	6991",
+		"CF B1	6994",
+		"DC D7	6995",
+		"BF BA	699B",
+		"DC D6	699C",
+		"DC D5	69A0",
+		"DC D2	69A7",
+		"DC C6	69AE",
+		"DC E3	69B1",
+		"DC C5	69B2",
+		"DC D8	69B4",
+		"DC D0	69BB",
+		"DC CB	69BE",
+		"DC C8	69BF",
+		"DC C9	69C1",
+		"DC D1	69C3",
+		"F4 A2	69C7",
+		"DC CE	69CA",
+		"B9 BD	69CB",
+		"C4 C8	69CC",
+		"C1 E4	69CD",
+		"DC CC	69CE",
+		"DC C7	69D0",
+		"DC CA	69D3",
+		"CD CD	69D8",
+		"CB EA	69D9",
+		"DC CF	69DD",
+		"DC D9	69DE",
+		"DC E1	69E7",
+		"DC DA	69E8",
+		"DC E7	69EB",
+		"DC E5	69ED",
+		"DC E0	69F2",
+		"DC DF	69F9",
+		"C4 D0	69FB",
+		"C1 E5	69FD",
+		"DC DD	69FF",
+		"DC DB	6A02",
+		"DC E2	6A05",
+		"DC E8	6A0A",
+		"C8 F5	6A0B",
+		"DC EE	6A0C",
+		"DC E9	6A12",
+		"DC EC	6A13",
+		"DC E6	6A14",
+		"C3 F4	6A17",
+		"C9 B8	6A19",
+		"DC DC	6A1B",
+		"DC E4	6A1E",
+		"BE C0	6A1F",
+		"CC CF	6A21",
+		"DC F8	6A22",
+		"DC EB	6A23",
+		"B8 A2	6A29",
+		"B2 A3	6A2A",
+		"B3 DF	6A2B",
+		"DC D3	6A2E",
+		"BE C1	6A35",
+		"DC F0	6A36",
+		"DC F7	6A38",
+		"BC F9	6A39",
+		"B3 F2	6A3A",
+		"C3 AE	6A3D",
+		"DC ED	6A44",
+		"DC F2	6A47",
+		"DC F6	6A48",
+		"B6 B6	6A4B",
+		"B5 CC	6A58",
+		"DC F4	6A59",
+		"B5 A1	6A5F",
+		"C6 CB	6A61",
+		"DC F3	6A62",
+		"DC F5	6A66",
+		"DC EF	6A72",
+		"DC F1	6A78",
+		"B3 E0	6A7F",
+		"C3 C9	6A80",
+		"DC FC	6A84",
+		"DC FA	6A8D",
+		"B8 E9	6A8E",
+		"DC F9	6A90",
+		"DD A1	6A97",
+		"DB D8	6A9C",
+		"DC FB	6AA0",
+		"DC FD	6AA2",
+		"DC FE	6AA3",
+		"DD AC	6AAA",
+		"DD A8	6AAC",
+		"DB ED	6AAE",
+		"DD A7	6AB3",
+		"DD A6	6AB8",
+		"DD A3	6ABB",
+		"DC EA	6AC1",
+		"DD A5	6AC2",
+		"DD A4	6AC3",
+		"DD AA	6AD1",
+		"CF A6	6AD3",
+		"DD AD	6ADA",
+		"B6 FB	6ADB",
+		"DD A9	6ADE",
+		"DD AB	6ADF",
+		"C8 A7	6AE8",
+		"DD AE	6AEA",
+		"DD B2	6AFA",
+		"DD AF	6AFB",
+		"CD F3	6B04",
+		"DD B0	6B05",
+		"DC DE	6B0A",
+		"DD B3	6B12",
+		"DD B4	6B16",
+		"B1 B5	6B1D",
+		"DD B6	6B1F",
+		"B7 E7	6B20",
+		"BC A1	6B21",
+		"B6 D5	6B23",
+		"B2 A4	6B27",
+		"CD DF	6B32",
+		"DD B8	6B37",
+		"DD B7	6B38",
+		"DD BA	6B39",
+		"B5 BD	6B3A",
+		"B6 D6	6B3D",
+		"B4 BE	6B3E",
+		"DD BD	6B43",
+		"DD BC	6B47",
+		"DD BE	6B49",
+		"B2 CE	6B4C",
+		"C3 B7	6B4E",
+		"DD BF	6B50",
+		"B4 BF	6B53",
+		"DD C1	6B54",
+		"DD C0	6B59",
+		"DD C2	6B5B",
+		"DD C3	6B5F",
+		"DD C4	6B61",
+		"BB DF	6B62",
+		"C0 B5	6B63",
+		"BA A1	6B64",
+		"C9 F0	6B66",
+		"CA E2	6B69",
+		"CF C4	6B6A",
+		"BB F5	6B6F",
+		"BA D0	6B73",
+		"CE F2	6B74",
+		"DD C5	6B78",
+		"DD C6	6B79",
+		"BB E0	6B7B",
+		"DD C7	6B7F",
+		"DD C8	6B80",
+		"DD CA	6B83",
+		"DD C9	6B84",
+		"CB D8	6B86",
+		"BD DE	6B89",
+		"BC EC	6B8A",
+		"BB C4	6B8B",
+		"DD CB	6B8D",
+		"DD CD	6B95",
+		"BF A3	6B96",
+		"DD CC	6B98",
+		"DD CE	6B9E",
+		"DD CF	6BA4",
+		"DD D0	6BAA",
+		"DD D1	6BAB",
+		"DD D2	6BAF",
+		"DD D4	6BB1",
+		"DD D3	6BB2",
+		"DD D5	6BB3",
+		"B2 A5	6BB4",
+		"C3 CA	6BB5",
+		"DD D6	6BB7",
+		"BB A6	6BBA",
+		"B3 CC	6BBB",
+		"DD D7	6BBC",
+		"C5 C2	6BBF",
+		"D4 CC	6BC0",
+		"B5 A3	6BC5",
+		"DD D8	6BC6",
+		"DD D9	6BCB",
+		"CA EC	6BCD",
+		"CB E8	6BCE",
+		"C6 C7	6BD2",
+		"DD DA	6BD3",
+		"C8 E6	6BD4",
+		"C8 FB	6BD8",
+		"CC D3	6BDB",
+		"DD DB	6BDF",
+		"DD DD	6BEB",
+		"DD DC	6BEC",
+		"DD DF	6BEF",
+		"DD DE	6BF3",
+		"DD E1	6C08",
+		"BB E1	6C0F",
+		"CC B1	6C11",
+		"DD E2	6C13",
+		"DD E3	6C14",
+		"B5 A4	6C17",
+		"DD E4	6C1B",
+		"DD E6	6C23",
+		"DD E5	6C24",
+		"BF E5	6C34",
+		"C9 B9	6C37",
+		"B1 CA	6C38",
+		"C8 C5	6C3E",
+		"C4 F5	6C40",
+		"BD C1	6C41",
+		"B5 E1	6C42",
+		"C8 C6	6C4E",
+		"BC AE	6C50",
+		"DD E8	6C55",
+		"B4 C0	6C57",
+		"B1 F8	6C5A",
+		"C6 F2	6C5D",
+		"DD E7	6C5E",
+		"B9 BE	6C5F",
+		"C3 D3	6C60",
+		"DD E9	6C62",
+		"DD F1	6C68",
+		"DD EA	6C6A",
+		"C2 C1	6C70",
+		"B5 E2	6C72",
+		"DD F2	6C73",
+		"B7 E8	6C7A",
+		"B5 A5	6C7D",
+		"DD F0	6C7E",
+		"DD EE	6C81",
+		"DD EB	6C82",
+		"CD E0	6C83",
+		"C4 C0	6C88",
+		"C6 D9	6C8C",
+		"DD EC	6C8D",
+		"DD F4	6C90",
+		"DD F3	6C92",
+		"B7 A3	6C93",
+		"B2 AD	6C96",
+		"BA BB	6C99",
+		"DD ED	6C9A",
+		"DD EF	6C9B",
+		"CB D7	6CA1",
+		"C2 F4	6CA2",
+		"CB F7	6CAB",
+		"DD FC	6CAE",
+		"DD FD	6CB1",
+		"B2 CF	6CB3",
+		"CA A8	6CB8",
+		"CC FD	6CB9",
+		"DE A1	6CBA",
+		"BC A3	6CBB",
+		"BE C2	6CBC",
+		"DD F8	6CBD",
+		"DD FE	6CBE",
+		"B1 E8	6CBF",
+		"B6 B7	6CC1",
+		"DD F5	6CC4",
+		"DD FA	6CC5",
+		"C0 F4	6CC9",
+		"C7 F1	6CCA",
+		"C8 E7	6CCC",
+		"DD F7	6CD3",
+		"CB A1	6CD5",
+		"DD F9	6CD7",
+		"DE A4	6CD9",
+		"DE A2	6CDB",
+		"DD FB	6CDD",
+		"CB A2	6CE1",
+		"C7 C8	6CE2",
+		"B5 E3	6CE3",
+		"C5 A5	6CE5",
+		"C3 ED	6CE8",
+		"DE A5	6CEA",
+		"DE A3	6CEF",
+		"C2 D9	6CF0",
+		"DD F6	6CF1",
+		"B1 CB	6CF3",
+		"CD CE	6D0B",
+		"DE B0	6D0C",
+		"DE AF	6D12",
+		"C0 F6	6D17",
+		"DE AC	6D19",
+		"CD EC	6D1B",
+		"C6 B6	6D1E",
+		"DE A6	6D1F",
+		"C4 C5	6D25",
+		"B1 CC	6D29",
+		"B9 BF	6D2A",
+		"DE A9	6D2B",
+		"BD A7	6D32",
+		"DE AE	6D33",
+		"DE AD	6D35",
+		"DE A8	6D36",
+		"DE AB	6D38",
+		"B3 E8	6D3B",
+		"DE AA	6D3D",
+		"C7 C9	6D3E",
+		"CE AE	6D41",
+		"BE F4	6D44",
+		"C0 F5	6D45",
+		"DE B6	6D59",
+		"DE B4	6D5A",
+		"C9 CD	6D5C",
+		"DE B1	6D63",
+		"DE B3	6D64",
+		"B1 BA	6D66",
+		"B9 C0	6D69",
+		"CF B2	6D6A",
+		"B3 BD	6D6C",
+		"C9 E2	6D6E",
+		"CD E1	6D74",
+		"B3 A4	6D77",
+		"BF BB	6D78",
+		"DE B5	6D79",
+		"DE BA	6D85",
+		"BE C3	6D88",
+		"CD B0	6D8C",
+		"DE B7	6D8E",
+		"DE B2	6D93",
+		"DE B8	6D95",
+		"CE DE	6D99",
+		"C5 F3	6D9B",
+		"C6 C2	6D9C",
+		"B3 B6	6DAF",
+		"B1 D5	6DB2",
+		"DE BE	6DB5",
+		"DE C1	6DB8",
+		"CE C3	6DBC",
+		"CD E4	6DC0",
+		"DE C8	6DC5",
+		"DE C2	6DC6",
+		"DE BF	6DC7",
+		"CE D4	6DCB",
+		"DE C5	6DCC",
+		"BD CA	6DD1",
+		"DE C7	6DD2",
+		"DE CC	6DD5",
+		"C5 F1	6DD8",
+		"DE CA	6DD9",
+		"DE C4	6DDE",
+		"C3 B8	6DE1",
+		"DE CB	6DE4",
+		"DE C0	6DE6",
+		"DE C6	6DE8",
+		"DE CD	6DEA",
+		"B0 FC	6DEB",
+		"DE C3	6DEC",
+		"DE CE	6DEE",
+		"BF BC	6DF1",
+		"BD DF	6DF3",
+		"CA A5	6DF5",
+		"BA AE	6DF7",
+		"DE BB	6DF9",
+		"DE C9	6DFA",
+		"C5 BA	6DFB",
+		"C0 B6	6E05",
+		"B3 E9	6E07",
+		"BA D1	6E08",
+		"BE C4	6E09",
+		"DE BD	6E0A",
+		"BD C2	6E0B",
+		"B7 CC	6E13",
+		"DE BC	6E15",
+		"DE D2	6E19",
+		"BD ED	6E1A",
+		"B8 BA	6E1B",
+		"DE E1	6E1D",
+		"DE DB	6E1F",
+		"B5 F4	6E20",
+		"C5 CF	6E21",
+		"DE D6	6E23",
+		"DE DF	6E24",
+		"B0 AF	6E25",
+		"B1 B2	6E26",
+		"B2 B9	6E29",
+		"DE D8	6E2B",
+		"C2 AC	6E2C",
+		"DE CF	6E2D",
+		"DE D1	6E2E",
+		"B9 C1	6E2F",
+		"DE E2	6E38",
+		"DE DD	6E3A",
+		"DE D5	6E3E",
+		"DE DC	6E43",
+		"CC AB	6E4A",
+		"DE DA	6E4D",
+		"DE DE	6E4E",
+		"B8 D0	6E56",
+		"BE C5	6E58",
+		"C3 B9	6E5B",
+		"DE D4	6E5F",
+		"CD AF	6E67",
+		"DE D7	6E6B",
+		"DE D0	6E6E",
+		"C5 F2	6E6F",
+		"DE D3	6E72",
+		"DE D9	6E76",
+		"CF D1	6E7E",
+		"BC BE	6E7F",
+		"CB FE	6E80",
+		"DE E3	6E82",
+		"C8 AE	6E8C",
+		"DE EF	6E8F",
+		"B8 BB	6E90",
+		"BD E0	6E96",
+		"DE E5	6E98",
+		"CE AF	6E9C",
+		"B9 C2	6E9D",
+		"DE F2	6E9F",
+		"B0 EE	6EA2",
+		"DE F0	6EA5",
+		"DE E4	6EAA",
+		"DE EA	6EAF",
+		"DE EC	6EB2",
+		"CD CF	6EB6",
+		"DE E7	6EB7",
+		"C5 AE	6EBA",
+		"DE E9	6EBD",
+		"DE F1	6EC2",
+		"DE EB	6EC4",
+		"CC C7	6EC5",
+		"DE E6	6EC9",
+		"BC A2	6ECB",
+		"DE FE	6ECC",
+		"B3 EA	6ED1",
+		"DE E8	6ED3",
+		"DE ED	6ED4",
+		"DE EE	6ED5",
+		"C2 EC	6EDD",
+		"C2 DA	6EDE",
+		"DE F6	6EEC",
+		"DE FC	6EEF",
+		"DE FA	6EF2",
+		"C5 A9	6EF4",
+		"DF A3	6EF7",
+		"DE F7	6EF8",
+		"DE F8	6EFE",
+		"DE E0	6EFF",
+		"B5 F9	6F01",
+		"C9 BA	6F02",
+		"BC BF	6F06",
+		"B9 F7	6F09",
+		"CF B3	6F0F",
+		"DE F4	6F11",
+		"DF A2	6F13",
+		"B1 E9	6F14",
+		"C1 E6	6F15",
+		"C7 F9	6F20",
+		"B4 C1	6F22",
+		"CE FA	6F23",
+		"CC A1	6F2B",
+		"C4 D2	6F2C",
+		"DE FB	6F31",
+		"DE FD	6F32",
+		"C1 B2	6F38",
+		"DF A1	6F3E",
+		"DE F9	6F3F",
+		"DE F3	6F41",
+		"B4 C3	6F45",
+		"B7 E9	6F54",
+		"DF AF	6F58",
+		"DF AA	6F5B",
+		"C0 F8	6F5C",
+		"B3 E3	6F5F",
+		"BD E1	6F64",
+		"DF B3	6F66",
+		"DF AC	6F6D",
+		"C4 AC	6F6E",
+		"DF A9	6F6F",
+		"C4 D9	6F70",
+		"DF CC	6F74",
+		"DF A6	6F78",
+		"DF A5	6F7A",
+		"DF AE	6F7C",
+		"DF A8	6F80",
+		"DF A7	6F81",
+		"DF AD	6F82",
+		"C0 A1	6F84",
+		"DF A4	6F86",
+		"DF B0	6F8E",
+		"DF B1	6F91",
+		"B4 C2	6F97",
+		"DF B6	6FA1",
+		"DF B5	6FA3",
+		"DF B7	6FA4",
+		"DF BA	6FAA",
+		"C5 C3	6FB1",
+		"DF B4	6FB3",
+		"DF B8	6FB9",
+		"B7 E3	6FC0",
+		"C2 F9	6FC1",
+		"DF B2	6FC2",
+		"C7 BB	6FC3",
+		"DF B9	6FC6",
+		"DF BE	6FD4",
+		"DF BC	6FD5",
+		"DF BF	6FD8",
+		"DF C2	6FDB",
+		"DF BB	6FDF",
+		"B9 EA	6FE0",
+		"C7 A8	6FE1",
+		"DE B9	6FE4",
+		"CD F4	6FEB",
+		"DF BD	6FEC",
+		"DF C1	6FEE",
+		"C2 F5	6FEF",
+		"DF C0	6FF1",
+		"DF AB	6FF3",
+		"EF E9	6FF6",
+		"DF C5	6FFA",
+		"DF C9	6FFE",
+		"DF C7	7001",
+		"DF C3	7009",
+		"DF C4	700B",
+		"DF C8	700F",
+		"DF C6	7011",
+		"C9 CE	7015",
+		"DF CE	7018",
+		"DF CB	701A",
+		"DF CA	701B",
+		"DF CD	701D",
+		"C6 D4	701E",
+		"DF CF	701F",
+		"C3 F5	7026",
+		"C2 ED	7027",
+		"C0 A5	702C",
+		"DF D0	7030",
+		"DF D2	7032",
+		"DF D1	703E",
+		"DE F5	704C",
+		"DF D3	7051",
+		"C6 E7	7058",
+		"DF D4	7063",
+		"B2 D0	706B",
+		"C5 F4	706F",
+		"B3 A5	7070",
+		"B5 E4	7078",
+		"BC DE	707C",
+		"BA D2	707D",
+		"CF A7	7089",
+		"BF E6	708A",
+		"B1 EA	708E",
+		"DF D6	7092",
+		"DF D5	7099",
+		"DF D9	70AC",
+		"C3 BA	70AD",
+		"DF DC	70AE",
+		"DF D7	70AF",
+		"DF DB	70B3",
+		"DF DA	70B8",
+		"C5 C0	70B9",
+		"B0 D9	70BA",
+		"CE F5	70C8",
+		"DF DE	70CB",
+		"B1 A8	70CF",
+		"DF E0	70D9",
+		"DF DF	70DD",
+		"DF DD	70DF",
+		"DF D8	70F1",
+		"CB A3	70F9",
+		"DF E2	70FD",
+		"DF E1	7109",
+		"B1 EB	7114",
+		"DF E4	7119",
+		"CA B2	711A",
+		"DF E3	711C",
+		"CC B5	7121",
+		"BE C7	7126",
+		"C1 B3	7136",
+		"BE C6	713C",
+		"CE FB	7149",
+		"DF EA	714C",
+		"C0 F9	714E",
+		"DF E6	7155",
+		"DF EB	7156",
+		"B1 EC	7159",
+		"DF E9	7162",
+		"C7 E1	7164",
+		"DF E5	7165",
+		"DF E8	7166",
+		"BE C8	7167",
+		"C8 D1	7169",
+		"DF EC	716C",
+		"BC D1	716E",
+		"C0 FA	717D",
+		"DF EF	7184",
+		"DF E7	7188",
+		"B7 A7	718A",
+		"DF ED	718F",
+		"CD D0	7194",
+		"DF F0	7195",
+		"F4 A6	7199",
+		"BD CF	719F",
+		"DF F1	71A8",
+		"DF F2	71AC",
+		"C7 AE	71B1",
+		"DF F4	71B9",
+		"DF F5	71BE",
+		"C7 B3	71C3",
+		"C5 F5	71C8",
+		"DF F7	71C9",
+		"DF F9	71CE",
+		"CE D5	71D0",
+		"DF F6	71D2",
+		"DF F8	71D4",
+		"B1 ED	71D5",
+		"DF F3	71D7",
+		"D3 DB	71DF",
+		"DF FA	71E0",
+		"C1 E7	71E5",
+		"BB B8	71E6",
+		"DF FC	71E7",
+		"DF FB	71EC",
+		"BF A4	71ED",
+		"D2 D9	71EE",
+		"DF FD	71F5",
+		"E0 A1	71F9",
+		"DF EE	71FB",
+		"DF FE	71FC",
+		"E0 A2	71FF",
+		"C7 FA	7206",
+		"E0 A3	720D",
+		"E0 A4	7210",
+		"E0 A5	721B",
+		"E0 A6	7228",
+		"C4 DE	722A",
+		"E0 A8	722C",
+		"E0 A7	722D",
+		"E0 A9	7230",
+		"E0 AA	7232",
+		"BC DF	7235",
+		"C9 E3	7236",
+		"CC EC	723A",
+		"E0 AB	723B",
+		"E0 AC	723C",
+		"C1 D6	723D",
+		"BC A4	723E",
+		"E0 AD	723F",
+		"E0 AE	7240",
+		"E0 AF	7246",
+		"CA D2	7247",
+		"C8 C7	7248",
+		"E0 B0	724B",
+		"C7 D7	724C",
+		"C4 AD	7252",
+		"E0 B1	7258",
+		"B2 E7	7259",
+		"B5 ED	725B",
+		"CC C6	725D",
+		"CC B6	725F",
+		"B2 B4	7261",
+		"CF B4	7262",
+		"CB D2	7267",
+		"CA AA	7269",
+		"C0 B7	7272",
+		"E0 B2	7274",
+		"C6 C3	7279",
+		"B8 A3	727D",
+		"E0 B3	727E",
+		"BA D4	7280",
+		"E0 B5	7281",
+		"E0 B4	7282",
+		"E0 B6	7287",
+		"E0 B7	7292",
+		"E0 B8	7296",
+		"B5 BE	72A0",
+		"E0 B9	72A2",
+		"E0 BA	72A7",
+		"B8 A4	72AC",
+		"C8 C8	72AF",
+		"E0 BC	72B2",
+		"BE F5	72B6",
+		"E0 BB	72B9",
+		"B6 B8	72C2",
+		"E0 BD	72C3",
+		"E0 BF	72C4",
+		"E0 BE	72C6",
+		"E0 C0	72CE",
+		"B8 D1	72D0",
+		"E0 C1	72D2",
+		"B6 E9	72D7",
+		"C1 C0	72D9",
+		"B9 FD	72DB",
+		"E0 C3	72E0",
+		"E0 C4	72E1",
+		"E0 C2	72E2",
+		"BC ED	72E9",
+		"C6 C8	72EC",
+		"B6 B9	72ED",
+		"E0 C6	72F7",
+		"C3 AC	72F8",
+		"E0 C5	72F9",
+		"CF B5	72FC",
+		"C7 E2	72FD",
+		"E0 C9	730A",
+		"E0 CB	7316",
+		"E0 C8	7317",
+		"CC D4	731B",
+		"E0 CA	731C",
+		"E0 CC	731D",
+		"CE C4	731F",
+		"E0 D0	7325",
+		"E0 CF	7329",
+		"C3 F6	732A",
+		"C7 AD	732B",
+		"B8 A5	732E",
+		"E0 CE	732F",
+		"E0 CD	7334",
+		"CD B1	7336",
+		"CD B2	7337",
+		"E0 D1	733E",
+		"B1 EE	733F",
+		"B9 F6	7344",
+		"BB E2	7345",
+		"E0 D2	734E",
+		"E0 D3	734F",
+		"E0 D5	7357",
+		"BD C3	7363",
+		"E0 D7	7368",
+		"E0 D6	736A",
+		"E0 D8	7370",
+		"B3 CD	7372",
+		"E0 DA	7375",
+		"E0 D9	7378",
+		"E0 DC	737A",
+		"E0 DB	737B",
+		"B8 BC	7384",
+		"CE A8	7387",
+		"B6 CC	7389",
+		"B2 A6	738B",
+		"B6 EA	7396",
+		"B4 E1	73A9",
+		"CE E8	73B2",
+		"E0 DE	73B3",
+		"E0 E0	73BB",
+		"E0 E1	73C0",
+		"B2 D1	73C2",
+		"E0 DD	73C8",
+		"BB B9	73CA",
+		"C4 C1	73CD",
+		"E0 DF	73CE",
+		"E0 E4	73DE",
+		"BC EE	73E0",
+		"E0 E2	73E5",
+		"B7 BE	73EA",
+		"C8 C9	73ED",
+		"E0 E3	73EE",
+		"E0 FE	73F1",
+		"E0 E9	73F8",
+		"B8 BD	73FE",
+		"B5 E5	7403",
+		"E0 E6	7405",
+		"CD FD	7406",
+		"CE B0	7409",
+		"C2 F6	7422",
+		"E0 E8	7425",
+		"E0 EA	7432",
+		"CE D6	7433",
+		"B6 D7	7434",
+		"C8 FC	7435",
+		"C7 CA	7436",
+		"E0 EB	743A",
+		"E0 ED	743F",
+		"E0 F0	7441",
+		"E0 EC	7455",
+		"E0 EF	7459",
+		"B8 EA	745A",
+		"B1 CD	745B",
+		"E0 F1	745C",
+		"BF F0	745E",
+		"E0 EE	745F",
+		"CE DC	7460",
+		"E0 F4	7463",
+		"F4 A4	7464",
+		"E0 F2	7469",
+		"E0 F5	746A",
+		"E0 E7	746F",
+		"E0 F3	7470",
+		"BA BC	7473",
+		"E0 F6	7476",
+		"E0 F7	747E",
+		"CD FE	7483",
+		"E0 F8	748B",
+		"E0 F9	749E",
+		"E0 E5	74A2",
+		"E0 FA	74A7",
+		"B4 C4	74B0",
+		"BC A5	74BD",
+		"E0 FB	74CA",
+		"E0 FC	74CF",
+		"E0 FD	74D4",
+		"B1 BB	74DC",
+		"E1 A1	74E0",
+		"C9 BB	74E2",
+		"E1 A2	74E3",
+		"B4 A4	74E6",
+		"E1 A3	74E7",
+		"E1 A4	74E9",
+		"E1 A5	74EE",
+		"E1 A7	74F0",
+		"E1 A8	74F1",
+		"E1 A6	74F2",
+		"C9 D3	74F6",
+		"E1 AA	74F7",
+		"E1 A9	74F8",
+		"E1 AC	7503",
+		"E1 AB	7504",
+		"E1 AD	7505",
+		"E1 AE	750C",
+		"E1 B0	750D",
+		"E1 AF	750E",
+		"B9 F9	7511",
+		"E1 B2	7513",
+		"E1 B1	7515",
+		"B4 C5	7518",
+		"BF D3	751A",
+		"C5 BC	751C",
+		"E1 B3	751E",
+		"C0 B8	751F",
+		"BB BA	7523",
+		"B1 F9	7525",
+		"E1 B4	7526",
+		"CD D1	7528",
+		"CA E3	752B",
+		"E1 B5	752C",
+		"C5 C4	7530",
+		"CD B3	7531",
+		"B9 C3	7532",
+		"BF BD	7533",
+		"C3 CB	7537",
+		"D2 B4	7538",
+		"C4 AE	753A",
+		"B2 E8	753B",
+		"E1 B6	753C",
+		"E1 B7	7544",
+		"E1 BC	7546",
+		"E1 BA	7549",
+		"E1 B9	754A",
+		"DA C2	754B",
+		"B3 A6	754C",
+		"E1 B8	754D",
+		"B0 DA	754F",
+		"C8 AA	7551",
+		"C8 CA	7554",
+		"CE B1	7559",
+		"E1 BD	755A",
+		"E1 BB	755B",
+		"C3 DC	755C",
+		"C0 A6	755D",
+		"C8 AB	7560",
+		"C9 AD	7562",
+		"E1 BF	7564",
+		"CE AC	7565",
+		"B7 CD	7566",
+		"E1 C0	7567",
+		"E1 BE	7569",
+		"C8 D6	756A",
+		"E1 C1	756B",
+		"E1 C2	756D",
+		"B0 DB	7570",
+		"BE F6	7573",
+		"E1 C7	7574",
+		"E1 C4	7576",
+		"C6 ED	7577",
+		"E1 C3	7578",
+		"B5 A6	757F",
+		"E1 CA	7582",
+		"E1 C5	7586",
+		"E1 C6	7587",
+		"E1 C9	7589",
+		"E1 C8	758A",
+		"C9 A5	758B",
+		"C1 C2	758E",
+		"C1 C1	758F",
+		"B5 BF	7591",
+		"E1 CB	7594",
+		"E1 CC	759A",
+		"E1 CD	759D",
+		"E1 CF	75A3",
+		"E1 CE	75A5",
+		"B1 D6	75AB",
+		"E1 D7	75B1",
+		"C8 E8	75B2",
+		"E1 D1	75B3",
+		"E1 D3	75B5",
+		"E1 D5	75B8",
+		"BF BE	75B9",
+		"E1 D6	75BC",
+		"E1 D4	75BD",
+		"BC C0	75BE",
+		"E1 D0	75C2",
+		"E1 D2	75C3",
+		"C9 C2	75C5",
+		"BE C9	75C7",
+		"E1 D9	75CA",
+		"E1 D8	75CD",
+		"E1 DA	75D2",
+		"BC A6	75D4",
+		"BA AF	75D5",
+		"C5 F7	75D8",
+		"E1 DB	75D9",
+		"C4 CB	75DB",
+		"E1 DD	75DE",
+		"CE A1	75E2",
+		"E1 DC	75E3",
+		"C1 E9	75E9",
+		"E1 E2	75F0",
+		"E1 E4	75F2",
+		"E1 E5	75F3",
+		"C3 D4	75F4",
+		"E1 E3	75FA",
+		"E1 E0	75FC",
+		"E1 DE	75FE",
+		"E1 DF	75FF",
+		"E1 E1	7601",
+		"E1 E8	7609",
+		"E1 E6	760B",
+		"E1 E7	760D",
+		"E1 E9	761F",
+		"E1 EB	7620",
+		"E1 EC	7621",
+		"E1 ED	7622",
+		"E1 EE	7624",
+		"E1 EA	7627",
+		"E1 F0	7630",
+		"E1 EF	7634",
+		"E1 F1	763B",
+		"CE C5	7642",
+		"E1 F4	7646",
+		"E1 F2	7647",
+		"E1 F3	7648",
+		"B4 E2	764C",
+		"CC FE	7652",
+		"CA CA	7656",
+		"E1 F6	7658",
+		"E1 F5	765C",
+		"E1 F7	7661",
+		"E1 F8	7662",
+		"E1 FC	7667",
+		"E1 F9	7668",
+		"E1 FA	7669",
+		"E1 FB	766A",
+		"E1 FD	766C",
+		"E1 FE	7670",
+		"E2 A1	7672",
+		"E2 A2	7676",
+		"E2 A3	7678",
+		"C8 AF	767A",
+		"C5 D0	767B",
+		"E2 A4	767C",
+		"C7 F2	767D",
+		"C9 B4	767E",
+		"E2 A5	7680",
+		"E2 A6	7683",
+		"C5 AA	7684",
+		"B3 A7	7686",
+		"B9 C4	7687",
+		"E2 A7	7688",
+		"E2 A8	768B",
+		"E2 A9	768E",
+		"BB A9	7690",
+		"E2 AB	7693",
+		"E2 AA	7696",
+		"E2 AC	7699",
+		"E2 AD	769A",
+		"C8 E9	76AE",
+		"E2 AE	76B0",
+		"E2 AF	76B4",
+		"F3 E9	76B7",
+		"E2 B0	76B8",
+		"E2 B1	76B9",
+		"E2 B2	76BA",
+		"BB AE	76BF",
+		"E2 B3	76C2",
+		"C7 D6	76C3",
+		"CB DF	76C6",
+		"B1 CE	76C8",
+		"B1 D7	76CA",
+		"E2 B4	76CD",
+		"E2 B6	76D2",
+		"E2 B5	76D6",
+		"C5 F0	76D7",
+		"C0 B9	76DB",
+		"DD B9	76DC",
+		"E2 B7	76DE",
+		"CC C1	76DF",
+		"E2 B8	76E1",
+		"B4 C6	76E3",
+		"C8 D7	76E4",
+		"E2 B9	76E5",
+		"E2 BA	76E7",
+		"E2 BB	76EA",
+		"CC DC	76EE",
+		"CC D5	76F2",
+		"C4 BE	76F4",
+		"C1 EA	76F8",
+		"E2 BD	76FB",
+		"BD E2	76FE",
+		"BE CA	7701",
+		"E2 C0	7704",
+		"E2 BF	7707",
+		"E2 BE	7708",
+		"C8 FD	7709",
+		"B4 C7	770B",
+		"B8 A9	770C",
+		"E2 C6	771B",
+		"E2 C3	771E",
+		"BF BF	771F",
+		"CC B2	7720",
+		"E2 C2	7724",
+		"E2 C4	7725",
+		"E2 C5	7726",
+		"E2 C1	7729",
+		"E2 C7	7737",
+		"E2 C8	7738",
+		"C4 AF	773A",
+		"B4 E3	773C",
+		"C3 E5	7740",
+		"E2 C9	7747",
+		"E2 CA	775A",
+		"E2 CD	775B",
+		"BF E7	7761",
+		"C6 C4	7763",
+		"E2 CE	7765",
+		"CB D3	7766",
+		"E2 CB	7768",
+		"E2 CC	776B",
+		"E2 D1	7779",
+		"E2 D0	777E",
+		"E2 CF	777F",
+		"E2 D3	778B",
+		"E2 D2	778E",
+		"E2 D4	7791",
+		"E2 D6	779E",
+		"E2 D5	77A0",
+		"CA CD	77A5",
+		"BD D6	77AC",
+		"CE C6	77AD",
+		"E2 D7	77B0",
+		"C6 B7	77B3",
+		"E2 D8	77B6",
+		"E2 D9	77B9",
+		"E2 DD	77BB",
+		"E2 DB	77BC",
+		"E2 DC	77BD",
+		"E2 DA	77BF",
+		"E2 DE	77C7",
+		"E2 DF	77CD",
+		"E2 E0	77D7",
+		"E2 E1	77DA",
+		"CC B7	77DB",
+		"E2 E2	77DC",
+		"CC F0	77E2",
+		"E2 E3	77E3",
+		"C3 CE	77E5",
+		"C7 EA	77E7",
+		"B6 EB	77E9",
+		"C3 BB	77ED",
+		"E2 E4	77EE",
+		"B6 BA	77EF",
+		"C0 D0	77F3",
+		"E2 E5	77FC",
+		"BA BD	7802",
+		"E2 E6	780C",
+		"E2 E7	7812",
+		"B8 A6	7814",
+		"BA D5	7815",
+		"E2 E9	7820",
+		"C5 D6	7825",
+		"BA D6	7826",
+		"B5 CE	7827",
+		"CB A4	7832",
+		"C7 CB	7834",
+		"C5 D7	783A",
+		"B9 DC	783F",
+		"E2 EB	7845",
+		"BE CB	785D",
+		"CE B2	786B",
+		"B9 C5	786C",
+		"B8 A7	786F",
+		"C8 A3	7872",
+		"E2 ED	7874",
+		"E2 EF	787C",
+		"B8 EB	7881",
+		"E2 EE	7886",
+		"C4 F6	7887",
+		"E2 F1	788C",
+		"B3 B7	788D",
+		"E2 EC	788E",
+		"C8 EA	7891",
+		"B1 B0	7893",
+		"BA EC	7895",
+		"CF D2	7897",
+		"E2 F0	789A",
+		"E2 F2	78A3",
+		"CA CB	78A7",
+		"C0 D9	78A9",
+		"E2 F4	78AA",
+		"E2 F5	78AF",
+		"E2 F3	78B5",
+		"B3 CE	78BA",
+		"E2 FB	78BC",
+		"E2 FA	78BE",
+		"BC A7	78C1",
+		"E2 FC	78C5",
+		"E2 F7	78C6",
+		"E2 FD	78CA",
+		"E2 F8	78CB",
+		"C8 D8	78D0",
+		"E2 F6	78D1",
+		"E2 F9	78D4",
+		"E3 A2	78DA",
+		"E3 A1	78E7",
+		"CB E1	78E8",
+		"E2 FE	78EC",
+		"B0 EB	78EF",
+		"E3 A4	78F4",
+		"E3 A3	78FD",
+		"BE CC	7901",
+		"E3 A5	7907",
+		"C1 C3	790E",
+		"E3 A7	7911",
+		"E3 A6	7912",
+		"E3 A8	7919",
+		"E2 E8	7926",
+		"E2 EA	792A",
+		"E3 AA	792B",
+		"E3 A9	792C",
+		"BC A8	793A",
+		"CE E9	793C",
+		"BC D2	793E",
+		"E3 AB	7940",
+		"B7 B7	7941",
+		"B5 C0	7947",
+		"B5 A7	7948",
+		"BB E3	7949",
+		"CD B4	7950",
+		"E3 B1	7953",
+		"E3 B0	7955",
+		"C1 C4	7956",
+		"E3 AD	7957",
+		"E3 AF	795A",
+		"BD CB	795D",
+		"BF C0	795E",
+		"E3 AE	795F",
+		"E3 AC	7960",
+		"C7 AA	7962",
+		"BE CD	7965",
+		"C9 BC	7968",
+		"BA D7	796D",
+		"C5 F8	7977",
+		"E3 B2	797A",
+		"E3 B3	797F",
+		"E3 C9	7980",
+		"B6 D8	7981",
+		"CF BD	7984",
+		"C1 B5	7985",
+		"E3 B4	798A",
+		"B2 D2	798D",
+		"C4 F7	798E",
+		"CA A1	798F",
+		"E3 B5	799D",
+		"B5 FA	79A6",
+		"E3 B6	79A7",
+		"E3 B8	79AA",
+		"E3 B9	79AE",
+		"C7 A9	79B0",
+		"E3 BA	79B3",
+		"E3 BB	79B9",
+		"E3 BC	79BA",
+		"B6 D9	79BD",
+		"B2 D3	79BE",
+		"C6 C5	79BF",
+		"BD A8	79C0",
+		"BB E4	79C1",
+		"E3 BD	79C9",
+		"BD A9	79CB",
+		"B2 CA	79D1",
+		"C9 C3	79D2",
+		"E3 BE	79D5",
+		"C8 EB	79D8",
+		"C1 C5	79DF",
+		"E3 C1	79E1",
+		"E3 C2	79E3",
+		"C7 E9	79E4",
+		"BF C1	79E6",
+		"E3 BF	79E7",
+		"C3 E1	79E9",
+		"E3 C0	79EC",
+		"BE CE	79F0",
+		"B0 DC	79FB",
+		"B5 A9	7A00",
+		"E3 C3	7A08",
+		"C4 F8	7A0B",
+		"E3 C4	7A0D",
+		"C0 C7	7A0E",
+		"CC AD	7A14",
+		"C9 A3	7A17",
+		"E3 C5	7A18",
+		"E3 C6	7A19",
+		"C3 D5	7A1A",
+		"CE C7	7A1C",
+		"E3 C8	7A1F",
+		"E3 C7	7A20",
+		"BC EF	7A2E",
+		"E3 CA	7A31",
+		"B0 F0	7A32",
+		"E3 CD	7A37",
+		"E3 CB	7A3B",
+		"B2 D4	7A3C",
+		"B7 CE	7A3D",
+		"E3 CC	7A3E",
+		"B9 C6	7A3F",
+		"B9 F2	7A40",
+		"CA E6	7A42",
+		"E3 CE	7A43",
+		"CB D4	7A46",
+		"E3 D0	7A49",
+		"C0 D1	7A4D",
+		"B1 CF	7A4E",
+		"B2 BA	7A4F",
+		"B0 AC	7A50",
+		"E3 CF	7A57",
+		"E3 D1	7A61",
+		"E3 D2	7A62",
+		"BE F7	7A63",
+		"E3 D3	7A69",
+		"B3 CF	7A6B",
+		"E3 D5	7A70",
+		"B7 EA	7A74",
+		"B5 E6	7A76",
+		"E3 D6	7A79",
+		"B6 F5	7A7A",
+		"E3 D7	7A7D",
+		"C0 FC	7A7F",
+		"C6 CD	7A81",
+		"C0 E0	7A83",
+		"BA F5	7A84",
+		"E3 D8	7A88",
+		"C3 E2	7A92",
+		"C1 EB	7A93",
+		"E3 DA	7A95",
+		"E3 DC	7A96",
+		"E3 D9	7A97",
+		"E3 DB	7A98",
+		"B7 A2	7A9F",
+		"E3 DD	7AA9",
+		"B7 A6	7AAA",
+		"B5 E7	7AAE",
+		"CD D2	7AAF",
+		"E3 DF	7AB0",
+		"E3 E0	7AB6",
+		"B1 AE	7ABA",
+		"E3 E3	7ABF",
+		"B3 F6	7AC3",
+		"E3 E2	7AC4",
+		"E3 E1	7AC5",
+		"E3 E5	7AC7",
+		"E3 DE	7AC8",
+		"E3 E6	7ACA",
+		"CE A9	7ACB",
+		"E3 E7	7ACD",
+		"E3 E8	7ACF",
+		"D4 F4	7AD2",
+		"E3 EA	7AD3",
+		"E3 E9	7AD5",
+		"E3 EB	7AD9",
+		"E3 EC	7ADA",
+		"CE B5	7ADC",
+		"E3 ED	7ADD",
+		"F0 EF	7ADF",
+		"BE CF	7AE0",
+		"E3 EE	7AE1",
+		"E3 EF	7AE2",
+		"BD D7	7AE3",
+		"C6 B8	7AE5",
+		"E3 F0	7AE6",
+		"C3 A8	7AEA",
+		"E3 F1	7AED",
+		"C3 BC	7AEF",
+		"E3 F2	7AF0",
+		"B6 A5	7AF6",
+		"D1 BF	7AF8",
+		"C3 DD	7AF9",
+		"BC B3	7AFA",
+		"B4 C8	7AFF",
+		"E3 F3	7B02",
+		"E4 A2	7B04",
+		"E3 F6	7B06",
+		"B5 E8	7B08",
+		"E3 F5	7B0A",
+		"E4 A4	7B0B",
+		"E3 F4	7B0F",
+		"BE D0	7B11",
+		"E3 F8	7B18",
+		"E3 F9	7B19",
+		"C5 AB	7B1B",
+		"E3 FA	7B1E",
+		"B3 DE	7B20",
+		"BF DA	7B25",
+		"C9 E4	7B26",
+		"E3 FC	7B28",
+		"C2 E8	7B2C",
+		"E3 F7	7B33",
+		"E3 FB	7B35",
+		"E3 FD	7B36",
+		"BA FB	7B39",
+		"E4 A6	7B45",
+		"C9 AE	7B46",
+		"C8 A6	7B48",
+		"C5 F9	7B49",
+		"B6 DA	7B4B",
+		"E4 A5	7B4C",
+		"E4 A3	7B4D",
+		"C8 B5	7B4F",
+		"E3 FE	7B50",
+		"C3 DE	7B51",
+		"C5 FB	7B52",
+		"C5 FA	7B54",
+		"BA F6	7B56",
+		"E4 B8	7B5D",
+		"E4 A8	7B65",
+		"E4 AA	7B67",
+		"E4 AD	7B6C",
+		"E4 AE	7B6E",
+		"E4 AB	7B70",
+		"E4 AC	7B71",
+		"E4 A9	7B74",
+		"E4 A7	7B75",
+		"E4 A1	7B7A",
+		"CA CF	7B86",
+		"B2 D5	7B87",
+		"E4 B5	7B8B",
+		"E4 B2	7B8D",
+		"E4 B7	7B8F",
+		"E4 B6	7B92",
+		"C7 F3	7B94",
+		"CC A7	7B95",
+		"BB BB	7B97",
+		"E4 B0	7B98",
+		"E4 B9	7B99",
+		"E4 B4	7B9A",
+		"E4 B3	7B9C",
+		"E4 AF	7B9D",
+		"E4 B1	7B9F",
+		"B4 C9	7BA1",
+		"C3 BD	7BAA",
+		"C0 FD	7BAD",
+		"C8 A2	7BB1",
+		"E4 BE	7BB4",
+		"C8 A4	7BB8",
+		"C0 E1	7BC0",
+		"E4 BB	7BC1",
+		"C8 CF	7BC4",
+		"E4 BF	7BC6",
+		"CA D3	7BC7",
+		"C3 DB	7BC9",
+		"E4 BA	7BCB",
+		"E4 BC	7BCC",
+		"E4 BD	7BCF",
+		"E4 C0	7BDD",
+		"BC C4	7BE0",
+		"C6 C6	7BE4",
+		"E4 C5	7BE5",
+		"E4 C4	7BE6",
+		"E4 C1	7BE9",
+		"CF B6	7BED",
+		"E4 CA	7BF3",
+		"E4 CE	7BF6",
+		"E4 CB	7BF7",
+		"E4 C7	7C00",
+		"E4 C8	7C07",
+		"E4 CD	7C0D",
+		"E4 C2	7C11",
+		"D2 D5	7C12",
+		"E4 C9	7C13",
+		"E4 C3	7C14",
+		"E4 CC	7C17",
+		"E4 D2	7C1F",
+		"B4 CA	7C21",
+		"E4 CF	7C23",
+		"E4 D0	7C27",
+		"E4 D1	7C2A",
+		"E4 D4	7C2B",
+		"E4 D3	7C37",
+		"C8 F6	7C38",
+		"E4 D5	7C3D",
+		"CE FC	7C3E",
+		"CA ED	7C3F",
+		"E4 DA	7C40",
+		"E4 D7	7C43",
+		"E4 D6	7C4C",
+		"C0 D2	7C4D",
+		"E4 D9	7C4F",
+		"E4 DB	7C50",
+		"E4 D8	7C54",
+		"E4 DF	7C56",
+		"E4 DC	7C58",
+		"E4 DD	7C5F",
+		"E4 C6	7C60",
+		"E4 DE	7C64",
+		"E4 E0	7C65",
+		"E4 E1	7C6C",
+		"CA C6	7C73",
+		"E4 E2	7C75",
+		"CC E2	7C7E",
+		"B6 CE	7C81",
+		"B7 A9	7C82",
+		"E4 E3	7C83",
+		"CA B4	7C89",
+		"BF E8	7C8B",
+		"CC B0	7C8D",
+		"E4 E4	7C90",
+		"CE B3	7C92",
+		"C7 F4	7C95",
+		"C1 C6	7C97",
+		"C7 B4	7C98",
+		"BD CD	7C9B",
+		"B0 C0	7C9F",
+		"E4 E9	7CA1",
+		"E4 E7	7CA2",
+		"E4 E5	7CA4",
+		"B4 A1	7CA5",
+		"BE D1	7CA7",
+		"E4 EA	7CA8",
+		"E4 E8	7CAB",
+		"E4 E6	7CAD",
+		"E4 EE	7CAE",
+		"E4 ED	7CB1",
+		"E4 EC	7CB2",
+		"E4 EB	7CB3",
+		"E4 EF	7CB9",
+		"E4 F0	7CBD",
+		"C0 BA	7CBE",
+		"E4 F1	7CC0",
+		"E4 F3	7CC2",
+		"E4 F2	7CC5",
+		"B8 D2	7CCA",
+		"C1 B8	7CCE",
+		"E4 F5	7CD2",
+		"C5 FC	7CD6",
+		"E4 F4	7CD8",
+		"E4 F6	7CDC",
+		"CA B5	7CDE",
+		"C1 EC	7CDF",
+		"B9 C7	7CE0",
+		"E4 F7	7CE2",
+		"CE C8	7CE7",
+		"E4 F9	7CEF",
+		"E4 FA	7CF2",
+		"E4 FB	7CF4",
+		"E4 FC	7CF6",
+		"BB E5	7CF8",
+		"E4 FD	7CFA",
+		"B7 CF	7CFB",
+		"B5 EA	7CFE",
+		"B5 AA	7D00",
+		"E5 A1	7D02",
+		"CC F3	7D04",
+		"B9 C8	7D05",
+		"E4 FE	7D06",
+		"E5 A4	7D0A",
+		"CC E6	7D0B",
+		"C7 BC	7D0D",
+		"C9 B3	7D10",
+		"BD E3	7D14",
+		"E5 A3	7D15",
+		"BC D3	7D17",
+		"B9 C9	7D18",
+		"BB E6	7D19",
+		"B5 E9	7D1A",
+		"CA B6	7D1B",
+		"E5 A2	7D1C",
+		"C1 C7	7D20",
+		"CB C2	7D21",
+		"BA F7	7D22",
+		"BB E7	7D2B",
+		"C4 DD	7D2C",
+		"E5 A7	7D2E",
+		"CE DF	7D2F",
+		"BA D9	7D30",
+		"E5 A8	7D32",
+		"BF C2	7D33",
+		"E5 AA	7D35",
+		"BE D2	7D39",
+		"BA B0	7D3A",
+		"E5 A9	7D3F",
+		"BD AA	7D42",
+		"B8 BE	7D43",
+		"C1 C8	7D44",
+		"E5 A5	7D45",
+		"E5 AB	7D46",
+		"E5 A6	7D4B",
+		"B7 D0	7D4C",
+		"E5 AE	7D4E",
+		"E5 B2	7D4F",
+		"B7 EB	7D50",
+		"E5 AD	7D56",
+		"E5 B6	7D5B",
+		"B9 CA	7D5E",
+		"CD ED	7D61",
+		"B0 BC	7D62",
+		"E5 B3	7D63",
+		"B5 EB	7D66",
+		"E5 B0	7D68",
+		"E5 B1	7D6E",
+		"C5 FD	7D71",
+		"E5 AF	7D72",
+		"E5 AC	7D73",
+		"B3 A8	7D75",
+		"C0 E4	7D76",
+		"B8 A8	7D79",
+		"E5 B8	7D7D",
+		"E5 B5	7D89",
+		"E5 B7	7D8F",
+		"E5 B4	7D93",
+		"B7 D1	7D99",
+		"C2 B3	7D9A",
+		"E5 B9	7D9B",
+		"C1 EE	7D9C",
+		"E5 C6	7D9F",
+		"E5 C2	7DA2",
+		"E5 BC	7DA3",
+		"E5 C0	7DAB",
+		"BC FA	7DAC",
+		"B0 DD	7DAD",
+		"E5 BB	7DAE",
+		"E5 C3	7DAF",
+		"E5 C7	7DB0",
+		"B9 CB	7DB1",
+		"CC D6	7DB2",
+		"C4 D6	7DB4",
+		"E5 BD	7DB5",
+		"E5 C5	7DB8",
+		"E5 BA	7DBA",
+		"C3 BE	7DBB",
+		"E5 BF	7DBD",
+		"B0 BD	7DBE",
+		"CC CA	7DBF",
+		"E5 BE	7DC7",
+		"B6 DB	7DCA",
+		"C8 EC	7DCB",
+		"C1 ED	7DCF",
+		"CE D0	7DD1",
+		"BD EF	7DD2",
+		"E5 EE	7DD5",
+		"E5 C8	7DD8",
+		"C0 FE	7DDA",
+		"E5 C4	7DDC",
+		"E5 C9	7DDD",
+		"E5 CB	7DDE",
+		"C4 F9	7DE0",
+		"E5 CE	7DE1",
+		"E5 CA	7DE4",
+		"CA D4	7DE8",
+		"B4 CB	7DE9",
+		"CC CB	7DEC",
+		"B0 DE	7DEF",
+		"E5 CD	7DF2",
+		"CE FD	7DF4",
+		"E5 CC	7DFB",
+		"B1 EF	7E01",
+		"C6 EC	7E04",
+		"E5 CF	7E05",
+		"E5 D6	7E09",
+		"E5 D0	7E0A",
+		"E5 D7	7E0B",
+		"E5 D3	7E12",
+		"C7 FB	7E1B",
+		"BC CA	7E1E",
+		"E5 D5	7E1F",
+		"E5 D2	7E21",
+		"E5 D8	7E22",
+		"E5 D1	7E23",
+		"BD C4	7E26",
+		"CB A5	7E2B",
+		"BD CC	7E2E",
+		"E5 D4	7E31",
+		"E5 E0	7E32",
+		"E5 DC	7E35",
+		"E5 DF	7E37",
+		"E5 DD	7E39",
+		"E5 E1	7E3A",
+		"E5 DB	7E3B",
+		"E5 C1	7E3D",
+		"C0 D3	7E3E",
+		"C8 CB	7E41",
+		"E5 DE	7E43",
+		"E5 D9	7E46",
+		"C1 A1	7E4A",
+		"B7 D2	7E4B",
+		"BD AB	7E4D",
+		"BF A5	7E54",
+		"C1 B6	7E55",
+		"E5 E4	7E56",
+		"E5 E6	7E59",
+		"E5 E7	7E5A",
+		"E5 E3	7E5D",
+		"E5 E5	7E5E",
+		"E5 DA	7E66",
+		"E5 E2	7E67",
+		"E5 EA	7E69",
+		"E5 E9	7E6A",
+		"CB FA	7E6D",
+		"B7 AB	7E70",
+		"E5 E8	7E79",
+		"E5 EC	7E7B",
+		"E5 EB	7E7C",
+		"E5 EF	7E7D",
+		"E5 F1	7E7F",
+		"BB BC	7E82",
+		"E5 ED	7E83",
+		"E5 F2	7E88",
+		"E5 F3	7E89",
+		"E5 F4	7E8C",
+		"E5 FA	7E8E",
+		"C5 BB	7E8F",
+		"E5 F6	7E90",
+		"E5 F5	7E92",
+		"E5 F7	7E93",
+		"E5 F8	7E94",
+		"E5 F9	7E96",
+		"E5 FB	7E9B",
+		"E5 FC	7E9C",
+		"B4 CC	7F36",
+		"E5 FD	7F38",
+		"E5 FE	7F3A",
+		"E6 A1	7F45",
+		"E6 A2	7F4C",
+		"E6 A3	7F4D",
+		"E6 A4	7F4E",
+		"E6 A5	7F50",
+		"E6 A6	7F51",
+		"E6 A8	7F54",
+		"E6 A7	7F55",
+		"E6 A9	7F58",
+		"E6 AA	7F5F",
+		"E6 AB	7F60",
+		"E6 AE	7F67",
+		"E6 AC	7F68",
+		"E6 AD	7F69",
+		"BA E1	7F6A",
+		"B7 D3	7F6B",
+		"C3 D6	7F6E",
+		"C8 B3	7F70",
+		"BD F0	7F72",
+		"C7 CD	7F75",
+		"C8 ED	7F77",
+		"E6 AF	7F78",
+		"D8 ED	7F79",
+		"E6 B0	7F82",
+		"E6 B2	7F83",
+		"CD E5	7F85",
+		"E6 B1	7F86",
+		"E6 B4	7F87",
+		"E6 B3	7F88",
+		"CD D3	7F8A",
+		"E6 B5	7F8C",
+		"C8 FE	7F8E",
+		"E6 B6	7F94",
+		"E6 B9	7F9A",
+		"E6 B8	7F9D",
+		"E6 B7	7F9E",
+		"E6 BA	7FA3",
+		"B7 B2	7FA4",
+		"C1 A2	7FA8",
+		"B5 C1	7FA9",
+		"E6 BE	7FAE",
+		"E6 BB	7FAF",
+		"E6 BC	7FB2",
+		"E6 BF	7FB6",
+		"E6 C0	7FB8",
+		"E6 BD	7FB9",
+		"B1 A9	7FBD",
+		"B2 A7	7FC1",
+		"E6 C2	7FC5",
+		"E6 C3	7FC6",
+		"E6 C4	7FCA",
+		"CD E2	7FCC",
+		"BD AC	7FD2",
+		"E6 C6	7FD4",
+		"E6 C5	7FD5",
+		"BF E9	7FE0",
+		"E6 C7	7FE1",
+		"E6 C8	7FE6",
+		"E6 C9	7FE9",
+		"B4 E5	7FEB",
+		"B4 CD	7FF0",
+		"E6 CA	7FF3",
+		"E6 CB	7FF9",
+		"CB DD	7FFB",
+		"CD E3	7FFC",
+		"CD D4	8000",
+		"CF B7	8001",
+		"B9 CD	8003",
+		"E6 CE	8004",
+		"BC D4	8005",
+		"E6 CD	8006",
+		"E6 CF	800B",
+		"BC A9	800C",
+		"C2 D1	8010",
+		"E6 D0	8012",
+		"B9 CC	8015",
+		"CC D7	8017",
+		"E6 D1	8018",
+		"E6 D2	8019",
+		"E6 D3	801C",
+		"E6 D4	8021",
+		"E6 D5	8028",
+		"BC AA	8033",
+		"CC ED	8036",
+		"E6 D7	803B",
+		"C3 BF	803D",
+		"E6 D6	803F",
+		"E6 D9	8046",
+		"E6 D8	804A",
+		"E6 DA	8052",
+		"C0 BB	8056",
+		"E6 DB	8058",
+		"E6 DC	805A",
+		"CA B9	805E",
+		"E6 DD	805F",
+		"C1 EF	8061",
+		"E6 DE	8062",
+		"E6 DF	8068",
+		"CE FE	806F",
+		"E6 E2	8070",
+		"E6 E1	8072",
+		"E6 E0	8073",
+		"C4 B0	8074",
+		"E6 E3	8076",
+		"BF A6	8077",
+		"E6 E4	8079",
+		"E6 E5	807D",
+		"CF B8	807E",
+		"E6 E6	807F",
+		"E6 E7	8084",
+		"E6 E9	8085",
+		"E6 E8	8086",
+		"C8 A5	8087",
+		"C6 F9	8089",
+		"CF BE	808B",
+		"C8 A9	808C",
+		"E6 EB	8093",
+		"BE D3	8096",
+		"C9 AA	8098",
+		"E6 EC	809A",
+		"E6 EA	809B",
+		"B4 CE	809D",
+		"B8 D4	80A1",
+		"BB E8	80A2",
+		"C8 EE	80A5",
+		"B8 AA	80A9",
+		"CB C3	80AA",
+		"E6 EF	80AC",
+		"E6 ED	80AD",
+		"B9 CE	80AF",
+		"B9 CF	80B1",
+		"B0 E9	80B2",
+		"BA E8	80B4",
+		"C7 D9	80BA",
+		"B0 DF	80C3",
+		"E6 F4	80C4",
+		"C3 C0	80C6",
+		"C7 D8	80CC",
+		"C2 DB	80CE",
+		"E6 F6	80D6",
+		"E6 F2	80D9",
+		"E6 F5	80DA",
+		"E6 F0	80DB",
+		"E6 F3	80DD",
+		"CB A6	80DE",
+		"B8 D5	80E1",
+		"B0 FD	80E4",
+		"E6 F1	80E5",
+		"E6 F8	80EF",
+		"E6 F9	80F1",
+		"C6 B9	80F4",
+		"B6 BB	80F8",
+		"E7 A6	80FC",
+		"C7 BD	80FD",
+		"BB E9	8102",
+		"B6 BC	8105",
+		"C0 C8	8106",
+		"CF C6	8107",
+		"CC AE	8108",
+		"E6 F7	8109",
+		"C0 D4	810A",
+		"B5 D3	811A",
+		"E6 FA	811B",
+		"E6 FC	8123",
+		"E6 FB	8129",
+		"E6 FD	812F",
+		"C3 A6	8131",
+		"C7 BE	8133",
+		"C4 B1	8139",
+		"E7 A3	813E",
+		"E7 A2	8146",
+		"E6 FE	814B",
+		"BF D5	814E",
+		"C9 E5	8150",
+		"E7 A5	8151",
+		"E7 A4	8153",
+		"B9 D0	8154",
+		"CF D3	8155",
+		"E7 B5	815F",
+		"E7 A9	8165",
+		"E7 AA	8166",
+		"BC F0	816B",
+		"E7 A8	816E",
+		"B9 F8	8170",
+		"E7 A7	8171",
+		"E7 AB	8174",
+		"C4 B2	8178",
+		"CA A2	8179",
+		"C1 A3	817A",
+		"C2 DC	817F",
+		"E7 AF	8180",
+		"E7 B0	8182",
+		"E7 AC	8183",
+		"E7 AD	8188",
+		"E7 AE	818A",
+		"B9 D1	818F",
+		"E7 B6	8193",
+		"E7 B2	8195",
+		"C9 E6	819A",
+		"CB EC	819C",
+		"C9 A8	819D",
+		"E7 B1	81A0",
+		"E7 B4	81A3",
+		"E7 B3	81A4",
+		"CB C4	81A8",
+		"E7 B7	81A9",
+		"E7 B8	81B0",
+		"C1 B7	81B3",
+		"E7 B9	81B5",
+		"E7 BB	81B8",
+		"E7 BF	81BA",
+		"E7 BC	81BD",
+		"E7 BA	81BE",
+		"C7 BF	81BF",
+		"E7 BD	81C0",
+		"E7 BE	81C2",
+		"B2 B2	81C6",
+		"E7 C5	81C8",
+		"E7 C0	81C9",
+		"E7 C1	81CD",
+		"E7 C2	81D1",
+		"C2 A1	81D3",
+		"E7 C4	81D8",
+		"E7 C3	81D9",
+		"E7 C6	81DA",
+		"E7 C7	81DF",
+		"E7 C8	81E0",
+		"BF C3	81E3",
+		"B2 E9	81E5",
+		"E7 C9	81E7",
+		"CE D7	81E8",
+		"BC AB	81EA",
+		"BD AD	81ED",
+		"BB EA	81F3",
+		"C3 D7	81F4",
+		"E7 CA	81FA",
+		"E7 CB	81FB",
+		"B1 B1	81FC",
+		"E7 CC	81FE",
+		"E7 CD	8201",
+		"E7 CE	8202",
+		"E7 CF	8205",
+		"E7 D0	8207",
+		"B6 BD	8208",
+		"DA AA	8209",
+		"E7 D1	820A",
+		"C0 E5	820C",
+		"E7 D2	820D",
+		"BC CB	820E",
+		"E7 D3	8210",
+		"D0 B0	8212",
+		"E7 D4	8216",
+		"CA DE	8217",
+		"B4 DC	8218",
+		"C1 A4	821B",
+		"BD D8	821C",
+		"C9 F1	821E",
+		"BD AE	821F",
+		"E7 D5	8229",
+		"B9 D2	822A",
+		"E7 D6	822B",
+		"C8 CC	822C",
+		"E7 E4	822E",
+		"E7 D8	8233",
+		"C2 C9	8235",
+		"C7 F5	8236",
+		"B8 BF	8237",
+		"E7 D7	8238",
+		"C1 A5	8239",
+		"E7 D9	8240",
+		"C4 FA	8247",
+		"E7 DB	8258",
+		"E7 DA	8259",
+		"E7 DD	825A",
+		"E7 DC	825D",
+		"E7 DE	825F",
+		"E7 E0	8262",
+		"E7 DF	8264",
+		"B4 CF	8266",
+		"E7 E1	8268",
+		"E7 E2	826A",
+		"E7 E3	826B",
+		"BA B1	826E",
+		"CE C9	826F",
+		"E7 E5	8271",
+		"BF A7	8272",
+		"B1 F0	8276",
+		"E7 E6	8277",
+		"E7 E7	8278",
+		"E7 E8	827E",
+		"B0 F2	828B",
+		"E7 E9	828D",
+		"E7 EA	8292",
+		"C9 E7	8299",
+		"BC C7	829D",
+		"E7 EC	829F",
+		"B3 A9	82A5",
+		"B0 B2	82A6",
+		"E7 EB	82AB",
+		"E7 EE	82AC",
+		"C7 CE	82AD",
+		"BF C4	82AF",
+		"B2 D6	82B1",
+		"CB A7	82B3",
+		"B7 DD	82B8",
+		"B6 DC	82B9",
+		"E7 ED	82BB",
+		"B2 EA	82BD",
+		"B4 A3	82C5",
+		"B1 F1	82D1",
+		"E7 F2	82D2",
+		"CE EA	82D3",
+		"C2 DD	82D4",
+		"C9 C4	82D7",
+		"E7 FE	82D9",
+		"B2 D7	82DB",
+		"E7 FC	82DC",
+		"E7 FA	82DE",
+		"E7 F1	82DF",
+		"E7 EF	82E1",
+		"E7 F0	82E3",
+		"BC E3	82E5",
+		"B6 EC	82E6",
+		"C3 F7	82E7",
+		"C6 D1	82EB",
+		"B1 D1	82F1",
+		"E7 F4	82F3",
+		"E7 F3	82F4",
+		"E7 F9	82F9",
+		"E7 F5	82FA",
+		"E7 F8	82FB",
+		"CC D0	8302",
+		"E7 F7	8303",
+		"B2 D8	8304",
+		"B3 FD	8305",
+		"E7 FB	8306",
+		"E7 FD	8309",
+		"B7 D4	830E",
+		"E8 A3	8316",
+		"E8 AC	8317",
+		"E8 AD	8318",
+		"B0 AB	831C",
+		"E8 B4	8323",
+		"B0 F1	8328",
+		"E8 AB	832B",
+		"E8 AA	832F",
+		"E8 A5	8331",
+		"E8 A4	8332",
+		"E8 A2	8334",
+		"E8 A1	8335",
+		"C3 E3	8336",
+		"C2 FB	8338",
+		"E8 A7	8339",
+		"E8 A6	8340",
+		"E8 A9	8345",
+		"C1 F0	8349",
+		"B7 D5	834A",
+		"B1 C1	834F",
+		"E8 A8	8350",
+		"B9 D3	8352",
+		"C1 F1	8358",
+		"E8 BA	8373",
+		"E8 BB	8375",
+		"B2 D9	8377",
+		"B2 AE	837B",
+		"E8 B8	837C",
+		"E8 AE	8385",
+		"E8 B6	8387",
+		"E8 BD	8389",
+		"E8 B7	838A",
+		"E8 B5	838E",
+		"E7 F6	8393",
+		"E8 B3	8396",
+		"E8 AF	839A",
+		"B4 D0	839E",
+		"E8 B1	839F",
+		"E8 BC	83A0",
+		"E8 B2	83A2",
+		"E8 BE	83A8",
+		"E8 B0	83AA",
+		"C7 FC	83AB",
+		"CD E9	83B1",
+		"E8 B9	83B5",
+		"E8 CF	83BD",
+		"E8 C7	83C1",
+		"BF FB	83C5",
+		"B5 C6	83CA",
+		"B6 DD	83CC",
+		"E8 C2	83CE",
+		"B2 DB	83D3",
+		"BE D4	83D6",
+		"E8 C5	83D8",
+		"BA DA	83DC",
+		"C5 D1	83DF",
+		"E8 CA	83E0",
+		"CA EE	83E9",
+		"E8 C1	83EB",
+		"B2 DA	83EF",
+		"B8 D6	83F0",
+		"C9 A9	83F1",
+		"E8 CB	83F2",
+		"E8 BF	83F4",
+		"E8 C8	83F7",
+		"E8 D2	83FB",
+		"E8 C3	83FD",
+		"E8 C4	8403",
+		"C6 BA	8404",
+		"E8 C9	8407",
+		"E8 C6	840B",
+		"CB A8	840C",
+		"E8 CC	840D",
+		"B0 E0	840E",
+		"E8 C0	8413",
+		"E8 CE	8420",
+		"E8 CD	8422",
+		"C7 EB	8429",
+		"E8 D4	842A",
+		"E8 DF	842C",
+		"B3 FE	8431",
+		"E8 E2	8435",
+		"E8 D0	8438",
+		"E8 D5	843C",
+		"CD EE	843D",
+		"E8 DE	8446",
+		"CD D5	8449",
+		"CE AA	844E",
+		"C3 F8	8457",
+		"B3 EB	845B",
+		"C9 F2	8461",
+		"E8 E4	8462",
+		"C6 A1	8463",
+		"B0 B1	8466",
+		"E8 DD	8469",
+		"E8 D9	846B",
+		"C1 F2	846C",
+		"E8 D3	846D",
+		"E8 DB	846E",
+		"E8 E0	846F",
+		"C7 AC	8471",
+		"B0 AA	8475",
+		"E8 D8	8477",
+		"E8 E1	8479",
+		"C9 F8	847A",
+		"E8 DC	8482",
+		"E8 D7	8484",
+		"BE D5	848B",
+		"BD AF	8490",
+		"BC AC	8494",
+		"CC D8	8499",
+		"C9 C7	849C",
+		"E8 E7	849F",
+		"E8 F0	84A1",
+		"E8 DA	84AD",
+		"B3 F7	84B2",
+		"BE F8	84B8",
+		"E8 E5	84B9",
+		"E8 EA	84BB",
+		"C1 F3	84BC",
+		"E8 E6	84BF",
+		"E8 ED	84C1",
+		"C3 DF	84C4",
+		"E8 EE	84C6",
+		"CD D6	84C9",
+		"E8 E3	84CA",
+		"B3 B8	84CB",
+		"E8 E9	84CD",
+		"E8 EC	84D0",
+		"CC AC	84D1",
+		"E8 EF	84D6",
+		"E8 E8	84D9",
+		"E8 EB	84DA",
+		"CB A9	84EC",
+		"CF A1	84EE",
+		"E8 F3	84F4",
+		"E8 FA	84FC",
+		"E8 F2	84FF",
+		"BC C3	8500",
+		"E8 D1	8506",
+		"CA CE	8511",
+		"CC A2	8513",
+		"E8 F9	8514",
+		"E8 F8	8515",
+		"E8 F4	8517",
+		"E8 F5	8518",
+		"B1 B6	851A",
+		"E8 F7	851F",
+		"E8 F1	8521",
+		"C4 D5	8526",
+		"E8 F6	852C",
+		"B0 FE	852D",
+		"C2 A2	8535",
+		"CA C3	853D",
+		"E8 FB	8540",
+		"E9 A1	8541",
+		"C8 D9	8543",
+		"E8 FE	8548",
+		"BE D6	8549",
+		"BC C9	854A",
+		"E9 A3	854B",
+		"B6 BE	854E",
+		"E9 A4	8555",
+		"C9 F9	8557",
+		"E8 FD	8558",
+		"E8 D6	855A",
+		"E8 FC	8563",
+		"CF CF	8568",
+		"C6 A2	8569",
+		"C9 F3	856A",
+		"E9 AB	856D",
+		"E9 B1	8577",
+		"E9 B2	857E",
+		"E9 A5	8580",
+		"C7 F6	8584",
+		"E9 AF	8587",
+		"E9 A7	8588",
+		"E9 A9	858A",
+		"E9 B3	8590",
+		"E9 A8	8591",
+		"E9 AC	8594",
+		"B1 F2	8597",
+		"C6 E5	8599",
+		"E9 AD	859B",
+		"E9 B0	859C",
+		"E9 A6	85A4",
+		"C1 A6	85A6",
+		"E9 AA	85A8",
+		"BB A7	85A9",
+		"BF C5	85AA",
+		"B7 B0	85AB",
+		"CC F4	85AC",
+		"CC F9	85AE",
+		"BD F2	85AF",
+		"E9 B7	85B9",
+		"E9 B5	85BA",
+		"CF CE	85C1",
+		"E9 B4	85C9",
+		"CD F5	85CD",
+		"E9 B6	85CF",
+		"E9 B8	85D0",
+		"E9 B9	85D5",
+		"E9 BC	85DC",
+		"E9 BA	85DD",
+		"C6 A3	85E4",
+		"E9 BB	85E5",
+		"C8 CD	85E9",
+		"E9 AE	85EA",
+		"BD F3	85F7",
+		"E9 BD	85F9",
+		"E9 C2	85FA",
+		"C1 F4	85FB",
+		"E9 C1	85FE",
+		"E9 A2	8602",
+		"E9 C3	8606",
+		"C1 C9	8607",
+		"E9 BE	860A",
+		"E9 C0	860B",
+		"E9 BF	8613",
+		"DD B1	8616",
+		"DD A2	8617",
+		"E9 C5	861A",
+		"E9 C4	8622",
+		"CD F6	862D",
+		"E2 BC	862F",
+		"E9 C6	8630",
+		"E9 C7	863F",
+		"E9 C8	864D",
+		"B8 D7	864E",
+		"B5 D4	8650",
+		"E9 CA	8654",
+		"D1 DD	8655",
+		"B5 F5	865A",
+		"CE BA	865C",
+		"B6 F3	865E",
+		"E9 CB	865F",
+		"E9 CC	8667",
+		"C3 EE	866B",
+		"E9 CD	8671",
+		"C6 FA	8679",
+		"B0 BA	867B",
+		"B2 E3	868A",
+		"E9 D2	868B",
+		"E9 D3	868C",
+		"E9 CE	8693",
+		"BB BD	8695",
+		"E9 CF	86A3",
+		"C7 C2	86A4",
+		"E9 D0	86A9",
+		"E9 D1	86AA",
+		"E9 DB	86AB",
+		"E9 D5	86AF",
+		"E9 D8	86B0",
+		"E9 D4	86B6",
+		"E9 D6	86C4",
+		"E9 D7	86C6",
+		"BC D8	86C7",
+		"E9 D9	86C9",
+		"C3 C1	86CB",
+		"B7 D6	86CD",
+		"B3 C2	86CE",
+		"E9 DC	86D4",
+		"B3 BF	86D9",
+		"E9 E1	86DB",
+		"E9 DD	86DE",
+		"E9 E0	86DF",
+		"C8 BA	86E4",
+		"E9 DE	86E9",
+		"E9 DF	86EC",
+		"C9 C8	86ED",
+		"C8 DA	86EE",
+		"E9 E2	86EF",
+		"C2 FD	86F8",
+		"E9 EC	86F9",
+		"E9 E8	86FB",
+		"B2 EB	86FE",
+		"E9 E6	8700",
+		"CB AA	8702",
+		"E9 E7	8703",
+		"E9 E4	8706",
+		"E9 E5	8708",
+		"E9 EA	8709",
+		"E9 ED	870A",
+		"E9 EB	870D",
+		"E9 E9	8711",
+		"E9 E3	8712",
+		"C3 D8	8718",
+		"E9 F4	871A",
+		"CC AA	871C",
+		"E9 F2	8725",
+		"E9 F3	8729",
+		"E9 EE	8734",
+		"E9 F0	8737",
+		"E9 F1	873B",
+		"E9 EF	873F",
+		"C0 E6	8749",
+		"CF B9	874B",
+		"E9 F8	874C",
+		"E9 F9	874E",
+		"EA A1	8753",
+		"BF AA	8755",
+		"E9 FB	8757",
+		"E9 FE	8759",
+		"E9 F6	875F",
+		"E9 F5	8760",
+		"EA A2	8763",
+		"B2 DC	8766",
+		"E9 FC	8768",
+		"EA A3	876A",
+		"E9 FD	876E",
+		"E9 FA	8774",
+		"C4 B3	8776",
+		"E9 F7	8778",
+		"C7 E8	877F",
+		"EA A7	8782",
+		"CD BB	878D",
+		"EA A6	879F",
+		"EA A5	87A2",
+		"EA AE	87AB",
+		"EA A8	87AF",
+		"EA B0	87B3",
+		"CD E6	87BA",
+		"EA B3	87BB",
+		"EA AA	87BD",
+		"EA AB	87C0",
+		"EA AF	87C4",
+		"EA B2	87C6",
+		"EA B1	87C7",
+		"EA A9	87CB",
+		"EA AC	87D0",
+		"EA BD	87D2",
+		"EA B6	87E0",
+		"EA B4	87EF",
+		"EA B5	87F2",
+		"EA BA	87F6",
+		"EA BB	87F7",
+		"B3 AA	87F9",
+		"B5 C2	87FB",
+		"EA B9	87FE",
+		"EA A4	8805",
+		"EA B8	880D",
+		"EA BC	880E",
+		"EA B7	880F",
+		"EA BE	8811",
+		"EA C0	8815",
+		"EA BF	8816",
+		"EA C2	8821",
+		"EA C1	8822",
+		"E9 DA	8823",
+		"EA C6	8827",
+		"EA C3	8831",
+		"EA C4	8836",
+		"EA C5	8839",
+		"EA C7	883B",
+		"B7 EC	8840",
+		"EA C9	8842",
+		"EA C8	8844",
+		"BD B0	8846",
+		"B9 D4	884C",
+		"DE A7	884D",
+		"EA CA	8852",
+		"BD D1	8853",
+		"B3 B9	8857",
+		"EA CB	8859",
+		"B1 D2	885B",
+		"BE D7	885D",
+		"EA CC	885E",
+		"B9 D5	8861",
+		"EA CD	8862",
+		"B0 E1	8863",
+		"C9 BD	8868",
+		"EA CE	886B",
+		"BF EA	8870",
+		"EA D5	8872",
+		"EA D2	8875",
+		"C3 EF	8877",
+		"EA D3	887D",
+		"EA D0	887E",
+		"B6 DE	887F",
+		"EA CF	8881",
+		"EA D6	8882",
+		"B7 B6	8888",
+		"C2 DE	888B",
+		"EA DC	888D",
+		"EA D8	8892",
+		"C2 B5	8896",
+		"EA D7	8897",
+		"EA DA	8899",
+		"EA D1	889E",
+		"EA DB	88A2",
+		"EA DD	88A4",
+		"C8 EF	88AB",
+		"EA D9	88AE",
+		"EA DE	88B0",
+		"EA E0	88B1",
+		"B8 D3	88B4",
+		"EA D4	88B5",
+		"B0 C1	88B7",
+		"EA DF	88BF",
+		"BA DB	88C1",
+		"CE F6	88C2",
+		"EA E1	88C3",
+		"EA E2	88C4",
+		"C1 F5	88C5",
+		"CE A2	88CF",
+		"EA E3	88D4",
+		"CD B5	88D5",
+		"EA E4	88D8",
+		"EA E5	88D9",
+		"CA E4	88DC",
+		"EA E6	88DD",
+		"BA C0	88DF",
+		"CE A3	88E1",
+		"EA EB	88E8",
+		"EA EC	88F2",
+		"BE D8	88F3",
+		"EA EA	88F4",
+		"CD E7	88F8",
+		"EA E7	88F9",
+		"EA E9	88FC",
+		"C0 BD	88FD",
+		"BF FE	88FE",
+		"EA E8	8902",
+		"EA ED	8904",
+		"CA A3	8907",
+		"EA EF	890A",
+		"EA EE	890C",
+		"B3 EC	8910",
+		"CB AB	8912",
+		"EA F0	8913",
+		"EA FC	891D",
+		"EA F2	891E",
+		"EA F3	8925",
+		"EA F4	892A",
+		"EA F5	892B",
+		"EA F9	8936",
+		"EA FA	8938",
+		"EA F8	893B",
+		"EA F6	8941",
+		"EA F1	8943",
+		"EA F7	8944",
+		"EA FB	894C",
+		"F0 B7	894D",
+		"B2 A8	8956",
+		"EA FE	895E",
+		"B6 DF	895F",
+		"EA FD	8960",
+		"EB A2	8964",
+		"EB A1	8966",
+		"EB A4	896A",
+		"EB A3	896D",
+		"EB A5	896F",
+		"BD B1	8972",
+		"EB A6	8974",
+		"EB A7	8977",
+		"EB A8	897E",
+		"C0 BE	897F",
+		"CD D7	8981",
+		"EB A9	8983",
+		"CA A4	8986",
+		"C7 C6	8987",
+		"EB AA	8988",
+		"EB AB	898A",
+		"B8 AB	898B",
+		"B5 AC	898F",
+		"EB AC	8993",
+		"BB EB	8996",
+		"C7 C1	8997",
+		"EB AD	8998",
+		"B3 D0	899A",
+		"EB AE	89A1",
+		"EB B0	89A6",
+		"CD F7	89A7",
+		"EB AF	89A9",
+		"BF C6	89AA",
+		"EB B1	89AC",
+		"EB B2	89AF",
+		"EB B3	89B2",
+		"B4 D1	89B3",
+		"EB B4	89BA",
+		"EB B5	89BD",
+		"EB B6	89BF",
+		"EB B7	89C0",
+		"B3 D1	89D2",
+		"EB B8	89DA",
+		"EB B9	89DC",
+		"EB BA	89DD",
+		"B2 F2	89E3",
+		"BF A8	89E6",
+		"EB BB	89E7",
+		"EB BC	89F4",
+		"EB BD	89F8",
+		"B8 C0	8A00",
+		"C4 FB	8A02",
+		"EB BE	8A03",
+		"B7 D7	8A08",
+		"BF D6	8A0A",
+		"EB C1	8A0C",
+		"C6 A4	8A0E",
+		"EB C0	8A10",
+		"B7 B1	8A13",
+		"EB BF	8A16",
+		"C2 F7	8A17",
+		"B5 AD	8A18",
+		"EB C2	8A1B",
+		"EB C3	8A1D",
+		"BE D9	8A1F",
+		"B7 ED	8A23",
+		"EB C4	8A25",
+		"CB AC	8A2A",
+		"C0 DF	8A2D",
+		"B5 F6	8A31",
+		"CC F5	8A33",
+		"C1 CA	8A34",
+		"EB C5	8A36",
+		"BF C7	8A3A",
+		"C3 F0	8A3B",
+		"BE DA	8A3C",
+		"EB C6	8A41",
+		"EB C9	8A46",
+		"EB CA	8A48",
+		"BA BE	8A50",
+		"C2 C2	8A51",
+		"EB C8	8A52",
+		"BE DB	8A54",
+		"C9 BE	8A55",
+		"EB C7	8A5B",
+		"BB EC	8A5E",
+		"B1 D3	8A60",
+		"EB CE	8A62",
+		"B7 D8	8A63",
+		"BB EE	8A66",
+		"BB ED	8A69",
+		"CF CD	8A6B",
+		"EB CD	8A6C",
+		"EB CC	8A6D",
+		"C1 A7	8A6E",
+		"B5 CD	8A70",
+		"CF C3	8A71",
+		"B3 BA	8A72",
+		"BE DC	8A73",
+		"EB CB	8A7C",
+		"EB D0	8A82",
+		"EB D1	8A84",
+		"EB CF	8A85",
+		"B8 D8	8A87",
+		"CD C0	8A89",
+		"BB EF	8A8C",
+		"C7 A7	8A8D",
+		"EB D4	8A91",
+		"C0 C0	8A93",
+		"C3 C2	8A95",
+		"CD B6	8A98",
+		"EB D7	8A9A",
+		"B8 EC	8A9E",
+		"C0 BF	8AA0",
+		"EB D3	8AA1",
+		"EB D8	8AA3",
+		"B8 ED	8AA4",
+		"EB D5	8AA5",
+		"EB D6	8AA6",
+		"EB D2	8AA8",
+		"C0 E2	8AAC",
+		"C6 C9	8AAD",
+		"C3 AF	8AB0",
+		"B2 DD	8AB2",
+		"C8 F0	8AB9",
+		"B5 C3	8ABC",
+		"C4 B4	8ABF",
+		"EB DB	8AC2",
+		"EB D9	8AC4",
+		"C3 CC	8AC7",
+		"C0 C1	8ACB",
+		"B4 D2	8ACC",
+		"EB DA	8ACD",
+		"BF DB	8ACF",
+		"CE CA	8AD2",
+		"CF C0	8AD6",
+		"EB DC	8ADA",
+		"EB E7	8ADB",
+		"C4 B5	8ADC",
+		"EB E6	8ADE",
+		"EB E3	8AE0",
+		"EB EB	8AE1",
+		"EB E4	8AE2",
+		"EB E0	8AE4",
+		"C4 FC	8AE6",
+		"EB DF	8AE7",
+		"EB DD	8AEB",
+		"CD A1	8AED",
+		"BB F0	8AEE",
+		"EB E1	8AF1",
+		"EB DE	8AF3",
+		"EB E5	8AF7",
+		"BD F4	8AF8",
+		"B8 C1	8AFA",
+		"C2 FA	8AFE",
+		"CB C5	8B00",
+		"B1 DA	8B01",
+		"B0 E2	8B02",
+		"C6 A5	8B04",
+		"EB E9	8B07",
+		"EB E8	8B0C",
+		"C6 E6	8B0E",
+		"EB ED	8B10",
+		"EB E2	8B14",
+		"EB EC	8B16",
+		"EB EE	8B17",
+		"B8 AC	8B19",
+		"EB EA	8B1A",
+		"B9 D6	8B1B",
+		"BC D5	8B1D",
+		"EB EF	8B20",
+		"CD D8	8B21",
+		"EB F2	8B26",
+		"EB F5	8B28",
+		"EB F3	8B2B",
+		"C9 B5	8B2C",
+		"EB F0	8B33",
+		"B6 E0	8B39",
+		"EB F4	8B3E",
+		"EB F6	8B41",
+		"EB FA	8B49",
+		"EB F7	8B4C",
+		"EB F9	8B4E",
+		"EB F8	8B4F",
+		"EB FB	8B56",
+		"BC B1	8B58",
+		"EB FD	8B5A",
+		"EB FC	8B5B",
+		"C9 E8	8B5C",
+		"EC A1	8B5F",
+		"B7 D9	8B66",
+		"EB FE	8B6B",
+		"EC A2	8B6C",
+		"EC A3	8B6F",
+		"B5 C4	8B70",
+		"E6 C1	8B71",
+		"BE F9	8B72",
+		"EC A4	8B74",
+		"B8 EE	8B77",
+		"EC A5	8B7D",
+		"EC A6	8B80",
+		"BB BE	8B83",
+		"DA CE	8B8A",
+		"EC A7	8B8C",
+		"EC A8	8B8E",
+		"BD B2	8B90",
+		"EC A9	8B92",
+		"EC AA	8B93",
+		"EC AB	8B96",
+		"EC AC	8B99",
+		"EC AD	8B9A",
+		"C3 AB	8C37",
+		"EC AE	8C3A",
+		"EC B0	8C3F",
+		"EC AF	8C41",
+		"C6 A6	8C46",
+		"EC B1	8C48",
+		"CB AD	8C4A",
+		"EC B2	8C4C",
+		"EC B3	8C4E",
+		"EC B4	8C50",
+		"EC B5	8C55",
+		"C6 DA	8C5A",
+		"BE DD	8C61",
+		"EC B6	8C62",
+		"B9 EB	8C6A",
+		"D0 AE	8C6B",
+		"EC B7	8C6C",
+		"EC B8	8C78",
+		"C9 BF	8C79",
+		"EC B9	8C7A",
+		"EC C1	8C7C",
+		"EC BA	8C82",
+		"EC BC	8C85",
+		"EC BB	8C89",
+		"EC BD	8C8A",
+		"CB C6	8C8C",
+		"EC BE	8C8D",
+		"EC BF	8C8E",
+		"EC C0	8C94",
+		"EC C2	8C98",
+		"B3 AD	8C9D",
+		"C4 E7	8C9E",
+		"C9 E9	8CA0",
+		"BA E2	8CA1",
+		"B9 D7	8CA2",
+		"C9 CF	8CA7",
+		"B2 DF	8CA8",
+		"C8 CE	8CA9",
+		"EC C5	8CAA",
+		"B4 D3	8CAB",
+		"C0 D5	8CAC",
+		"EC C4	8CAD",
+		"EC C9	8CAE",
+		"C3 F9	8CAF",
+		"CC E3	8CB0",
+		"EC C7	8CB2",
+		"EC C8	8CB3",
+		"B5 AE	8CB4",
+		"EC CA	8CB6",
+		"C7 E3	8CB7",
+		"C2 DF	8CB8",
+		"C8 F1	8CBB",
+		"C5 BD	8CBC",
+		"EC C6	8CBD",
+		"CB C7	8CBF",
+		"B2 EC	8CC0",
+		"EC CC	8CC1",
+		"CF A8	8CC2",
+		"C4 C2	8CC3",
+		"CF C5	8CC4",
+		"BB F1	8CC7",
+		"EC CB	8CC8",
+		"C2 B1	8CCA",
+		"EC DC	8CCD",
+		"C1 A8	8CCE",
+		"C6 F8	8CD1",
+		"C9 D0	8CD3",
+		"EC CF	8CDA",
+		"BB BF	8CDB",
+		"BB F2	8CDC",
+		"BE DE	8CDE",
+		"C7 E5	8CE0",
+		"B8 AD	8CE2",
+		"EC CE	8CE3",
+		"EC CD	8CE4",
+		"C9 EA	8CE6",
+		"BC C1	8CEA",
+		"C5 D2	8CED",
+		"EC D1	8CFA",
+		"EC D2	8CFB",
+		"B9 D8	8CFC",
+		"EC D0	8CFD",
+		"EC D3	8D04",
+		"EC D4	8D05",
+		"EC D6	8D07",
+		"C2 A3	8D08",
+		"EC D5	8D0A",
+		"B4 E6	8D0B",
+		"EC D8	8D0D",
+		"EC D7	8D0F",
+		"EC D9	8D10",
+		"EC DB	8D13",
+		"EC DD	8D14",
+		"EC DE	8D16",
+		"C0 D6	8D64",
+		"BC CF	8D66",
+		"EC DF	8D67",
+		"B3 D2	8D6B",
+		"EC E0	8D6D",
+		"C1 F6	8D70",
+		"EC E1	8D71",
+		"EC E2	8D73",
+		"C9 EB	8D74",
+		"B5 AF	8D77",
+		"EC E3	8D81",
+		"C4 B6	8D85",
+		"B1 DB	8D8A",
+		"EC E4	8D99",
+		"BC F1	8DA3",
+		"BF F6	8DA8",
+		"C2 AD	8DB3",
+		"EC E7	8DBA",
+		"EC E6	8DBE",
+		"EC E5	8DC2",
+		"EC ED	8DCB",
+		"EC EB	8DCC",
+		"EC E8	8DCF",
+		"EC EA	8DD6",
+		"EC E9	8DDA",
+		"EC EC	8DDB",
+		"B5 F7	8DDD",
+		"EC F0	8DDF",
+		"C0 D7	8DE1",
+		"EC F1	8DE3",
+		"B8 D9	8DE8",
+		"EC EE	8DEA",
+		"EC EF	8DEB",
+		"CF A9	8DEF",
+		"C4 B7	8DF3",
+		"C1 A9	8DF5",
+		"EC F2	8DFC",
+		"EC F5	8DFF",
+		"EC F3	8E08",
+		"EC F4	8E09",
+		"CD D9	8E0A",
+		"C6 A7	8E0F",
+		"EC F8	8E10",
+		"EC F6	8E1D",
+		"EC F7	8E1E",
+		"EC F9	8E1F",
+		"ED A9	8E2A",
+		"EC FC	8E30",
+		"EC FD	8E34",
+		"EC FB	8E35",
+		"EC FA	8E42",
+		"C4 FD	8E44",
+		"ED A1	8E47",
+		"ED A5	8E48",
+		"ED A2	8E49",
+		"EC FE	8E4A",
+		"ED A3	8E4C",
+		"ED A4	8E50",
+		"ED AB	8E55",
+		"ED A6	8E59",
+		"C0 D8	8E5F",
+		"ED A8	8E60",
+		"ED AA	8E63",
+		"ED A7	8E64",
+		"ED AD	8E72",
+		"BD B3	8E74",
+		"ED AC	8E76",
+		"ED AE	8E7C",
+		"ED AF	8E81",
+		"ED B2	8E84",
+		"ED B1	8E85",
+		"ED B0	8E87",
+		"ED B4	8E8A",
+		"ED B3	8E8B",
+		"CC F6	8E8D",
+		"ED B6	8E91",
+		"ED B5	8E93",
+		"ED B7	8E94",
+		"ED B8	8E99",
+		"ED BA	8EA1",
+		"ED B9	8EAA",
+		"BF C8	8EAB",
+		"ED BB	8EAC",
+		"B6 ED	8EAF",
+		"ED BC	8EB0",
+		"ED BE	8EB1",
+		"ED BF	8EBE",
+		"ED C0	8EC5",
+		"ED BD	8EC6",
+		"ED C1	8EC8",
+		"BC D6	8ECA",
+		"ED C2	8ECB",
+		"B5 B0	8ECC",
+		"B7 B3	8ECD",
+		"B8 AE	8ED2",
+		"ED C3	8EDB",
+		"C6 F0	8EDF",
+		"C5 BE	8EE2",
+		"ED C4	8EE3",
+		"ED C7	8EEB",
+		"BC B4	8EF8",
+		"ED C6	8EFB",
+		"ED C5	8EFC",
+		"B7 DA	8EFD",
+		"ED C8	8EFE",
+		"B3 D3	8F03",
+		"ED CA	8F05",
+		"BA DC	8F09",
+		"ED C9	8F0A",
+		"ED D2	8F0C",
+		"ED CC	8F12",
+		"ED CE	8F13",
+		"CA E5	8F14",
+		"ED CB	8F15",
+		"ED CD	8F19",
+		"ED D1	8F1B",
+		"ED CF	8F1C",
+		"B5 B1	8F1D",
+		"ED D0	8F1F",
+		"ED D3	8F26",
+		"C7 DA	8F29",
+		"CE D8	8F2A",
+		"BD B4	8F2F",
+		"ED D4	8F33",
+		"CD A2	8F38",
+		"ED D6	8F39",
+		"ED D5	8F3B",
+		"ED D9	8F3E",
+		"CD C1	8F3F",
+		"ED D8	8F42",
+		"B3 ED	8F44",
+		"ED D7	8F45",
+		"ED DC	8F46",
+		"ED DB	8F49",
+		"ED DA	8F4C",
+		"C5 B2	8F4D",
+		"ED DD	8F4E",
+		"ED DE	8F57",
+		"ED DF	8F5C",
+		"B9 EC	8F5F",
+		"B7 A5	8F61",
+		"ED E0	8F62",
+		"ED E1	8F63",
+		"ED E2	8F64",
+		"BF C9	8F9B",
+		"ED E3	8F9C",
+		"BC AD	8F9E",
+		"ED E4	8F9F",
+		"ED E5	8FA3",
+		"D2 A1	8FA7",
+		"D1 FE	8FA8",
+		"ED E6	8FAD",
+		"E5 F0	8FAE",
+		"ED E7	8FAF",
+		"C3 A4	8FB0",
+		"BF AB	8FB1",
+		"C7 C0	8FB2",
+		"ED E8	8FB7",
+		"CA D5	8FBA",
+		"C4 D4	8FBB",
+		"B9 FE	8FBC",
+		"C3 A9	8FBF",
+		"B1 AA	8FC2",
+		"CB F8	8FC4",
+		"BF D7	8FC5",
+		"B7 DE	8FCE",
+		"B6 E1	8FD1",
+		"CA D6	8FD4",
+		"ED E9	8FDA",
+		"ED EB	8FE2",
+		"ED EA	8FE5",
+		"B2 E0	8FE6",
+		"C6 F6	8FE9",
+		"ED EC	8FEA",
+		"C7 F7	8FEB",
+		"C5 B3	8FED",
+		"ED ED	8FEF",
+		"BD D2	8FF0",
+		"ED EF	8FF4",
+		"CC C2	8FF7",
+		"ED FE	8FF8",
+		"ED F1	8FF9",
+		"ED F2	8FFA",
+		"C4 C9	8FFD",
+		"C2 E0	9000",
+		"C1 F7	9001",
+		"C6 A8	9003",
+		"ED F0	9005",
+		"B5 D5	9006",
+		"ED F9	900B",
+		"ED F6	900D",
+		"EE A5	900E",
+		"C6 A9	900F",
+		"C3 E0	9010",
+		"ED F3	9011",
+		"C4 FE	9013",
+		"C5 D3	9014",
+		"ED F4	9015",
+		"ED F8	9016",
+		"BF E0	9017",
+		"C7 E7	9019",
+		"C4 CC	901A",
+		"C0 C2	901D",
+		"ED F7	901E",
+		"C2 AE	901F",
+		"C2 A4	9020",
+		"ED F5	9021",
+		"B0 A9	9022",
+		"CF A2	9023",
+		"ED FA	9027",
+		"C2 E1	902E",
+		"BD B5	9031",
+		"BF CA	9032",
+		"ED FC	9035",
+		"ED FB	9036",
+		"B0 EF	9038",
+		"ED FD	9039",
+		"C9 AF	903C",
+		"EE A7	903E",
+		"C6 DB	9041",
+		"BF EB	9042",
+		"C3 D9	9045",
+		"B6 F8	9047",
+		"EE A6	9049",
+		"CD B7	904A",
+		"B1 BF	904B",
+		"CA D7	904D",
+		"B2 E1	904E",
+		"EE A1	904F",
+		"EE A2	9050",
+		"EE A3	9051",
+		"EE A4	9052",
+		"C6 BB	9053",
+		"C3 A3	9054",
+		"B0 E3	9055",
+		"EE A8	9056",
+		"EE A9	9058",
+		"F4 A3	9059",
+		"C2 BD	905C",
+		"EE AA	905E",
+		"B1 F3	9060",
+		"C1 CC	9061",
+		"B8 AF	9063",
+		"CD DA	9065",
+		"EE AB	9068",
+		"C5 AC	9069",
+		"C1 F8	906D",
+		"BC D7	906E",
+		"EE AC	906F",
+		"EE AF	9072",
+		"BD E5	9075",
+		"EE AD	9076",
+		"C1 AB	9077",
+		"C1 AA	9078",
+		"B0 E4	907A",
+		"CE CB	907C",
+		"EE B1	907D",
+		"C8 F2	907F",
+		"EE B3	9080",
+		"EE B2	9081",
+		"EE B0	9082",
+		"E3 E4	9083",
+		"B4 D4	9084",
+		"ED EE	9087",
+		"EE B5	9089",
+		"EE B4	908A",
+		"EE B6	908F",
+		"CD B8	9091",
+		"C6 E1	90A3",
+		"CB AE	90A6",
+		"EE B7	90A8",
+		"BC D9	90AA",
+		"EE B8	90AF",
+		"EE B9	90B1",
+		"EE BA	90B5",
+		"C5 A1	90B8",
+		"B0 EA	90C1",
+		"B9 D9	90CA",
+		"CF BA	90CE",
+		"EE BE	90DB",
+		"B7 B4	90E1",
+		"EE BB	90E2",
+		"EE BC	90E4",
+		"C9 F4	90E8",
+		"B3 D4	90ED",
+		"CD B9	90F5",
+		"B6 BF	90F7",
+		"C5 D4	90FD",
+		"EE BF	9102",
+		"EE C0	9112",
+		"EE C1	9119",
+		"C5 A2	912D",
+		"EE C3	9130",
+		"EE C2	9132",
+		"C6 D3	9149",
+		"EE C4	914A",
+		"BD B6	914B",
+		"BC E0	914C",
+		"C7 DB	914D",
+		"C3 F1	914E",
+		"BC F2	9152",
+		"BF EC	9154",
+		"EE C5	9156",
+		"EE C6	9158",
+		"BF DD	9162",
+		"EE C7	9163",
+		"EE C8	9165",
+		"EE C9	9169",
+		"CD EF	916A",
+		"BD B7	916C",
+		"EE CB	9172",
+		"EE CA	9173",
+		"B9 DA	9175",
+		"B9 F3	9177",
+		"BB C0	9178",
+		"EE CE	9182",
+		"BD E6	9187",
+		"EE CD	9189",
+		"EE CC	918B",
+		"C2 E9	918D",
+		"B8 EF	9190",
+		"C0 C3	9192",
+		"C8 B0	9197",
+		"BD B9	919C",
+		"EE CF	91A2",
+		"BE DF	91A4",
+		"EE D2	91AA",
+		"EE D0	91AB",
+		"EE D1	91AF",
+		"EE D4	91B4",
+		"EE D3	91B5",
+		"BE FA	91B8",
+		"EE D5	91BA",
+		"EE D6	91C0",
+		"EE D7	91C1",
+		"C8 D0	91C6",
+		"BA D3	91C7",
+		"BC E1	91C8",
+		"EE D8	91C9",
+		"EE D9	91CB",
+		"CE A4	91CC",
+		"BD C5	91CD",
+		"CC EE	91CE",
+		"CE CC	91CF",
+		"EE DA	91D0",
+		"B6 E2	91D1",
+		"EE DB	91D6",
+		"C5 A3	91D8",
+		"EE DE	91DB",
+		"B3 F8	91DC",
+		"BF CB	91DD",
+		"EE DC	91DF",
+		"EE DD	91E1",
+		"C4 E0	91E3",
+		"CB D5	91E6",
+		"B6 FC	91E7",
+		"EE E0	91F5",
+		"EE E1	91F6",
+		"EE DF	91FC",
+		"EE E3	91FF",
+		"C6 DF	920D",
+		"B3 C3	920E",
+		"EE E7	9211",
+		"EE E4	9214",
+		"EE E6	9215",
+		"EE E2	921E",
+		"EF CF	9229",
+		"EE E5	922C",
+		"CE EB	9234",
+		"B8 DA	9237",
+		"EE EF	923F",
+		"C5 B4	9244",
+		"EE EA	9245",
+		"EE ED	9248",
+		"EE EB	9249",
+		"EE F0	924B",
+		"EE F1	9250",
+		"EE E9	9257",
+		"EE F6	925A",
+		"B1 F4	925B",
+		"EE E8	925E",
+		"C8 AD	9262",
+		"EE EC	9264",
+		"BE E0	9266",
+		"B9 DB	9271",
+		"CB C8	927E",
+		"B6 E4	9280",
+		"BD C6	9283",
+		"C6 BC	9285",
+		"C1 AD	9291",
+		"EE F4	9293",
+		"EE EE	9295",
+		"EE F3	9296",
+		"CC C3	9298",
+		"C4 B8	929A",
+		"EE F5	929B",
+		"EE F2	929C",
+		"C1 AC	92AD",
+		"EE F9	92B7",
+		"EE F8	92B9",
+		"EE F7	92CF",
+		"CB AF	92D2",
+		"BD FB	92E4",
+		"EE FA	92E9",
+		"CA DF	92EA",
+		"B1 D4	92ED",
+		"C9 C6	92F2",
+		"C3 F2	92F3",
+		"B5 F8	92F8",
+		"EE FC	92FA",
+		"B9 DD	92FC",
+		"BB AC	9306",
+		"EE FB	930F",
+		"BF ED	9310",
+		"BF EE	9318",
+		"EF A1	9319",
+		"EF A3	931A",
+		"BE FB	9320",
+		"EF A2	9322",
+		"EF A4	9323",
+		"B6 D3	9326",
+		"C9 C5	9328",
+		"BC E2	932B",
+		"CF A3	932C",
+		"EE FE	932E",
+		"BA F8	932F",
+		"CF BF	9332",
+		"EF A6	9335",
+		"EF A5	933A",
+		"EF A7	933B",
+		"EE FD	9344",
+		"C6 E9	934B",
+		"C5 D5	934D",
+		"C4 D7	9354",
+		"EF AC	9356",
+		"C3 C3	935B",
+		"EF A8	935C",
+		"EF A9	9360",
+		"B7 AD	936C",
+		"EF AB	936E",
+		"B8 B0	9375",
+		"EF AA	937C",
+		"BE E1	937E",
+		"B3 F9	938C",
+		"EF B0	9394",
+		"BA BF	9396",
+		"C1 F9	9397",
+		"C4 CA	939A",
+		"B3 BB	93A7",
+		"EF AE	93AC",
+		"EF AF	93AD",
+		"C4 C3	93AE",
+		"EF AD	93B0",
+		"EF B1	93B9",
+		"EF B7	93C3",
+		"EF BA	93C8",
+		"EF B9	93D0",
+		"C5 AD	93D1",
+		"EF B2	93D6",
+		"EF B3	93D7",
+		"EF B6	93D8",
+		"EF B8	93DD",
+		"B6 C0	93E1",
+		"EF BB	93E4",
+		"EF B5	93E5",
+		"EF B4	93E8",
+		"EF BF	9403",
+		"EF C0	9407",
+		"EF C1	9410",
+		"EF BE	9413",
+		"EF BD	9414",
+		"BE E2	9418",
+		"C6 AA	9419",
+		"EF BC	941A",
+		"EF C5	9421",
+		"EF C3	942B",
+		"EF C4	9435",
+		"EF C2	9436",
+		"C2 F8	9438",
+		"EF C6	943A",
+		"EF C7	9441",
+		"EF C9	9444",
+		"B4 D5	9451",
+		"EF C8	9452",
+		"CC FA	9453",
+		"EF D4	945A",
+		"EF CA	945B",
+		"EF CD	945E",
+		"EF CB	9460",
+		"EF CC	9462",
+		"EF CE	946A",
+		"EF D0	9470",
+		"EF D1	9475",
+		"EF D2	9477",
+		"EF D5	947C",
+		"EF D3	947D",
+		"EF D6	947E",
+		"EF D8	947F",
+		"EF D7	9481",
+		"C4 B9	9577",
+		"CC E7	9580",
+		"EF D9	9582",
+		"C1 AE	9583",
+		"EF DA	9587",
+		"CA C4	9589",
+		"EF DB	958A",
+		"B3 AB	958B",
+		"B1 BC	958F",
+		"B4 D7	9591",
+		"B4 D6	9593",
+		"EF DC	9594",
+		"EF DD	9596",
+		"EF DE	9598",
+		"EF DF	9599",
+		"EF E0	95A0",
+		"B4 D8	95A2",
+		"B3 D5	95A3",
+		"B9 DE	95A4",
+		"C8 B6	95A5",
+		"EF E2	95A7",
+		"EF E1	95A8",
+		"EF E3	95AD",
+		"B1 DC	95B2",
+		"EF E6	95B9",
+		"EF E5	95BB",
+		"EF E4	95BC",
+		"EF E7	95BE",
+		"EF EA	95C3",
+		"B0 C7	95C7",
+		"EF E8	95CA",
+		"EF EC	95CC",
+		"EF EB	95CD",
+		"EF EE	95D4",
+		"EF ED	95D5",
+		"EF EF	95D6",
+		"C6 AE	95D8",
+		"EF F0	95DC",
+		"EF F1	95E1",
+		"EF F3	95E2",
+		"EF F2	95E5",
+		"C9 EC	961C",
+		"EF F4	9621",
+		"EF F5	9628",
+		"BA E5	962A",
+		"EF F6	962E",
+		"EF F7	962F",
+		"CB C9	9632",
+		"C1 CB	963B",
+		"B0 A4	963F",
+		"C2 CB	9640",
+		"EF F8	9642",
+		"C9 ED	9644",
+		"EF FB	964B",
+		"EF F9	964C",
+		"B9 DF	964D",
+		"EF FA	964F",
+		"B8 C2	9650",
+		"CA C5	965B",
+		"EF FD	965C",
+		"F0 A1	965D",
+		"EF FE	965E",
+		"F0 A2	965F",
+		"B1 A1	9662",
+		"BF D8	9663",
+		"BD FC	9664",
+		"B4 D9	9665",
+		"F0 A3	9666",
+		"C7 E6	966A",
+		"F0 A5	966C",
+		"B1 A2	9670",
+		"F0 A4	9672",
+		"C4 C4	9673",
+		"CE CD	9675",
+		"C6 AB	9676",
+		"EF FC	9677",
+		"CE A6	9678",
+		"B8 B1	967A",
+		"CD DB	967D",
+		"B6 F9	9685",
+		"CE B4	9686",
+		"B7 A8	9688",
+		"C2 E2	968A",
+		"E7 A1	968B",
+		"F0 A6	968D",
+		"B3 AC	968E",
+		"BF EF	968F",
+		"B3 D6	9694",
+		"F0 A8	9695",
+		"F0 A9	9697",
+		"F0 A7	9698",
+		"B7 E4	9699",
+		"BA DD	969B",
+		"BE E3	969C",
+		"B1 A3	96A0",
+		"CE D9	96A3",
+		"F0 AB	96A7",
+		"EE AE	96A8",
+		"F0 AA	96AA",
+		"F0 AE	96B0",
+		"F0 AC	96B1",
+		"F0 AD	96B2",
+		"F0 AF	96B4",
+		"F0 B0	96B6",
+		"CE EC	96B7",
+		"F0 B1	96B8",
+		"F0 B2	96B9",
+		"C0 C9	96BB",
+		"C8 BB	96BC",
+		"BF FD	96C0",
+		"B4 E7	96C1",
+		"CD BA	96C4",
+		"B2 ED	96C5",
+		"BD B8	96C6",
+		"B8 DB	96C7",
+		"F0 B5	96C9",
+		"F0 B4	96CB",
+		"BB F3	96CC",
+		"F0 B6	96CD",
+		"F0 B3	96CE",
+		"BB A8	96D1",
+		"F0 BA	96D5",
+		"EA AD	96D6",
+		"D2 D6	96D9",
+		"BF F7	96DB",
+		"F0 B8	96DC",
+		"CE A5	96E2",
+		"C6 F1	96E3",
+		"B1 AB	96E8",
+		"C0 E3	96EA",
+		"BC B6	96EB",
+		"CA B7	96F0",
+		"B1 C0	96F2",
+		"CE ED	96F6",
+		"CD EB	96F7",
+		"F0 BB	96F9",
+		"C5 C5	96FB",
+		"BC FB	9700",
+		"F0 BC	9704",
+		"F0 BD	9706",
+		"BF CC	9707",
+		"F0 BE	9708",
+		"CE EE	970A",
+		"F0 B9	970D",
+		"F0 C0	970E",
+		"F0 C2	970F",
+		"F0 C1	9711",
+		"F0 BF	9713",
+		"F0 C3	9716",
+		"F0 C4	9719",
+		"C1 FA	971C",
+		"B2 E2	971E",
+		"F0 C5	9724",
+		"CC B8	9727",
+		"F0 C6	972A",
+		"F0 C7	9730",
+		"CF AA	9732",
+		"DB B1	9738",
+		"F0 C8	9739",
+		"F0 C9	973D",
+		"F0 CA	973E",
+		"F0 CE	9742",
+		"F0 CB	9744",
+		"F0 CC	9746",
+		"F0 CD	9748",
+		"F0 CF	9749",
+		"C0 C4	9752",
+		"CC F7	9756",
+		"C0 C5	9759",
+		"F0 D0	975C",
+		"C8 F3	975E",
+		"F0 D1	9760",
+		"F3 D3	9761",
+		"CC CC	9762",
+		"F0 D2	9764",
+		"F0 D3	9766",
+		"F0 D4	9768",
+		"B3 D7	9769",
+		"F0 D6	976B",
+		"BF D9	976D",
+		"F0 D7	9771",
+		"B7 A4	9774",
+		"F0 D8	9779",
+		"F0 DC	977A",
+		"F0 DA	977C",
+		"F0 DB	9781",
+		"B3 F3	9784",
+		"F0 D9	9785",
+		"F0 DD	9786",
+		"F0 DE	978B",
+		"B0 C8	978D",
+		"F0 DF	978F",
+		"F0 E0	9790",
+		"BE E4	9798",
+		"F0 E1	979C",
+		"B5 C7	97A0",
+		"F0 E4	97A3",
+		"F0 E3	97A6",
+		"F0 E2	97A8",
+		"EB F1	97AB",
+		"CA DC	97AD",
+		"F0 E5	97B3",
+		"F0 E6	97B4",
+		"F0 E7	97C3",
+		"F0 E8	97C6",
+		"F0 E9	97C8",
+		"F0 EA	97CB",
+		"B4 DA	97D3",
+		"F0 EB	97DC",
+		"F0 EC	97ED",
+		"C7 A3	97EE",
+		"F0 EE	97F2",
+		"B2 BB	97F3",
+		"F0 F1	97F5",
+		"F0 F0	97F6",
+		"B1 A4	97FB",
+		"B6 C1	97FF",
+		"CA C7	9801",
+		"C4 BA	9802",
+		"BA A2	9803",
+		"B9 E0	9805",
+		"BD E7	9806",
+		"BF DC	9808",
+		"F0 F3	980C",
+		"F0 F2	980F",
+		"CD C2	9810",
+		"B4 E8	9811",
+		"C8 D2	9812",
+		"C6 DC	9813",
+		"BF FC	9817",
+		"CE CE	9818",
+		"B7 DB	981A",
+		"F0 F6	9821",
+		"F0 F5	9824",
+		"CB CB	982C",
+		"C6 AC	982D",
+		"B1 D0	9834",
+		"F0 F7	9837",
+		"F0 F4	9838",
+		"C9 D1	983B",
+		"CD EA	983C",
+		"F0 F8	983D",
+		"F0 F9	9846",
+		"F0 FB	984B",
+		"C2 EA	984C",
+		"B3 DB	984D",
+		"B3 DC	984E",
+		"F0 FA	984F",
+		"B4 E9	9854",
+		"B8 B2	9855",
+		"B4 EA	9858",
+		"C5 BF	985B",
+		"CE E0	985E",
+		"B8 DC	9867",
+		"F0 FC	986B",
+		"F0 FD	986F",
+		"F0 FE	9870",
+		"F1 A1	9871",
+		"F1 A3	9873",
+		"F1 A2	9874",
+		"C9 F7	98A8",
+		"F1 A4	98AA",
+		"F1 A5	98AF",
+		"F1 A6	98B1",
+		"F1 A7	98B6",
+		"F1 A9	98C3",
+		"F1 A8	98C4",
+		"F1 AA	98C6",
+		"C8 F4	98DB",
+		"E6 CC	98DC",
+		"BF A9	98DF",
+		"B5 B2	98E2",
+		"F1 AB	98E9",
+		"F1 AC	98EB",
+		"D2 AC	98ED",
+		"DD BB	98EE",
+		"C8 D3	98EF",
+		"B0 FB	98F2",
+		"B0 BB	98F4",
+		"BB F4	98FC",
+		"CB B0	98FD",
+		"BE FE	98FE",
+		"F1 AD	9903",
+		"CC DF	9905",
+		"F1 AE	9909",
+		"CD DC	990A",
+		"B1 C2	990C",
+		"BB C1	9910",
+		"F1 AF	9912",
+		"B2 EE	9913",
+		"F1 B0	9914",
+		"F1 B1	9918",
+		"F1 B3	991D",
+		"F1 B4	991E",
+		"F1 B6	9920",
+		"F1 B2	9921",
+		"F1 B5	9924",
+		"B4 DB	9928",
+		"F1 B7	992C",
+		"F1 B8	992E",
+		"F1 B9	993D",
+		"F1 BA	993E",
+		"F1 BB	9942",
+		"F1 BD	9945",
+		"F1 BC	9949",
+		"F1 BF	994B",
+		"F1 C2	994C",
+		"F1 BE	9950",
+		"F1 C0	9951",
+		"F1 C1	9952",
+		"F1 C3	9955",
+		"B6 C2	9957",
+		"BC F3	9996",
+		"F1 C4	9997",
+		"F1 C5	9998",
+		"B9 E1	9999",
+		"F1 C6	99A5",
+		"B3 BE	99A8",
+		"C7 CF	99AC",
+		"F1 C7	99AD",
+		"F1 C8	99AE",
+		"C3 DA	99B3",
+		"C6 EB	99B4",
+		"F1 C9	99BC",
+		"C7 FD	99C1",
+		"C2 CC	99C4",
+		"B1 D8	99C5",
+		"B6 EE	99C6",
+		"B6 EF	99C8",
+		"C3 F3	99D0",
+		"F1 CE	99D1",
+		"B6 F0	99D2",
+		"B2 EF	99D5",
+		"F1 CD	99D8",
+		"F1 CB	99DB",
+		"F1 CC	99DD",
+		"F1 CA	99DF",
+		"F1 D8	99E2",
+		"F1 CF	99ED",
+		"F1 D0	99EE",
+		"F1 D1	99F1",
+		"F1 D2	99F2",
+		"F1 D4	99F8",
+		"F1 D3	99FB",
+		"BD D9	99FF",
+		"F1 D5	9A01",
+		"F1 D7	9A05",
+		"B5 B3	9A0E",
+		"F1 D6	9A0F",
+		"C1 FB	9A12",
+		"B8 B3	9A13",
+		"F1 D9	9A19",
+		"C2 CD	9A28",
+		"F1 DA	9A2B",
+		"C6 AD	9A30",
+		"F1 DB	9A37",
+		"F1 E0	9A3E",
+		"F1 DE	9A40",
+		"F1 DD	9A42",
+		"F1 DF	9A43",
+		"F1 DC	9A45",
+		"F1 E2	9A4D",
+		"F1 E1	9A55",
+		"F1 E4	9A57",
+		"B6 C3	9A5A",
+		"F1 E3	9A5B",
+		"F1 E5	9A5F",
+		"F1 E6	9A62",
+		"F1 E8	9A64",
+		"F1 E7	9A65",
+		"F1 E9	9A69",
+		"F1 EB	9A6A",
+		"F1 EA	9A6B",
+		"B9 FC	9AA8",
+		"F1 EC	9AAD",
+		"F1 ED	9AB0",
+		"B3 BC	9AB8",
+		"F1 EE	9ABC",
+		"F1 EF	9AC0",
+		"BF F1	9AC4",
+		"F1 F0	9ACF",
+		"F1 F1	9AD1",
+		"F1 F2	9AD3",
+		"F1 F3	9AD4",
+		"B9 E2	9AD8",
+		"F1 F4	9ADE",
+		"F1 F5	9ADF",
+		"F1 F6	9AE2",
+		"F1 F7	9AE3",
+		"F1 F8	9AE6",
+		"C8 B1	9AEA",
+		"F1 FA	9AEB",
+		"C9 A6	9AED",
+		"F1 FB	9AEE",
+		"F1 F9	9AEF",
+		"F1 FD	9AF1",
+		"F1 FC	9AF4",
+		"F1 FE	9AF7",
+		"F2 A1	9AFB",
+		"F2 A2	9B06",
+		"F2 A3	9B18",
+		"F2 A4	9B1A",
+		"F2 A5	9B1F",
+		"F2 A6	9B22",
+		"F2 A7	9B23",
+		"F2 A8	9B25",
+		"F2 A9	9B27",
+		"F2 AA	9B28",
+		"F2 AB	9B29",
+		"F2 AC	9B2A",
+		"F2 AD	9B2E",
+		"F2 AE	9B2F",
+		"DD B5	9B31",
+		"F2 AF	9B32",
+		"E4 F8	9B3B",
+		"B5 B4	9B3C",
+		"B3 A1	9B41",
+		"BA B2	9B42",
+		"F2 B1	9B43",
+		"F2 B0	9B44",
+		"CC A5	9B45",
+		"F2 B3	9B4D",
+		"F2 B4	9B4E",
+		"F2 B2	9B4F",
+		"F2 B5	9B51",
+		"CB E2	9B54",
+		"F2 B6	9B58",
+		"B5 FB	9B5A",
+		"CF A5	9B6F",
+		"F2 B7	9B74",
+		"F2 B9	9B83",
+		"B0 BE	9B8E",
+		"F2 BA	9B91",
+		"CA AB	9B92",
+		"F2 B8	9B93",
+		"F2 BB	9B96",
+		"F2 BC	9B97",
+		"F2 BD	9B9F",
+		"F2 BE	9BA0",
+		"F2 BF	9BA8",
+		"CB EE	9BAA",
+		"BB AD	9BAB",
+		"BA FA	9BAD",
+		"C1 AF	9BAE",
+		"F2 C0	9BB4",
+		"F2 C3	9BB9",
+		"F2 C1	9BC0",
+		"F2 C4	9BC6",
+		"B8 F1	9BC9",
+		"F2 C2	9BCA",
+		"F2 C5	9BCF",
+		"F2 C6	9BD1",
+		"F2 C7	9BD2",
+		"F2 CB	9BD4",
+		"BB AA	9BD6",
+		"C2 E4	9BDB",
+		"F2 CC	9BE1",
+		"F2 C9	9BE2",
+		"F2 C8	9BE3",
+		"F2 CA	9BE4",
+		"B7 DF	9BE8",
+		"F2 D0	9BF0",
+		"F2 CF	9BF1",
+		"F2 CE	9BF2",
+		"B0 B3	9BF5",
+		"F2 DA	9C04",
+		"F2 D6	9C06",
+		"F2 D7	9C08",
+		"F2 D3	9C09",
+		"F2 D9	9C0A",
+		"F2 D5	9C0C",
+		"B3 E2	9C0D",
+		"CF CC	9C10",
+		"F2 D8	9C12",
+		"F2 D4	9C13",
+		"F2 D2	9C14",
+		"F2 D1	9C15",
+		"F2 DC	9C1B",
+		"F2 DF	9C21",
+		"F2 DE	9C24",
+		"F2 DD	9C25",
+		"C9 C9	9C2D",
+		"F2 DB	9C2E",
+		"B0 F3	9C2F",
+		"F2 E0	9C30",
+		"F2 E2	9C32",
+		"B3 EF	9C39",
+		"F2 CD	9C3A",
+		"B1 B7	9C3B",
+		"F2 E4	9C3E",
+		"F2 E3	9C46",
+		"F2 E1	9C47",
+		"C3 AD	9C48",
+		"CB F0	9C52",
+		"CE DA	9C57",
+		"F2 E5	9C5A",
+		"F2 E6	9C60",
+		"F2 E7	9C67",
+		"F2 E8	9C76",
+		"F2 E9	9C78",
+		"C4 BB	9CE5",
+		"F2 EA	9CE7",
+		"C8 B7	9CE9",
+		"F2 EF	9CEB",
+		"F2 EB	9CEC",
+		"F2 EC	9CF0",
+		"CB B1	9CF3",
+		"CC C4	9CF4",
+		"C6 D0	9CF6",
+		"F2 F0	9D03",
+		"F2 F1	9D06",
+		"C6 BE	9D07",
+		"F2 EE	9D08",
+		"F2 ED	9D09",
+		"B2 AA	9D0E",
+		"F2 F9	9D12",
+		"F2 F8	9D15",
+		"B1 F5	9D1B",
+		"F2 F6	9D1F",
+		"F2 F5	9D23",
+		"F2 F3	9D26",
+		"B3 FB	9D28",
+		"F2 F2	9D2A",
+		"BC B2	9D2B",
+		"B2 A9	9D2C",
+		"B9 E3	9D3B",
+		"F2 FC	9D3E",
+		"F2 FB	9D3F",
+		"F2 FA	9D41",
+		"F2 F7	9D44",
+		"F2 FD	9D46",
+		"F2 FE	9D48",
+		"F3 A5	9D50",
+		"F3 A4	9D51",
+		"F3 A6	9D59",
+		"B1 AD	9D5C",
+		"F3 A1	9D5D",
+		"F3 A2	9D5E",
+		"B9 F4	9D60",
+		"CC B9	9D61",
+		"F3 A3	9D64",
+		"CB B2	9D6C",
+		"F3 AB	9D6F",
+		"F3 A7	9D72",
+		"F3 AC	9D7A",
+		"F3 A9	9D87",
+		"F3 A8	9D89",
+		"B7 DC	9D8F",
+		"F3 AD	9D9A",
+		"F3 AE	9DA4",
+		"F3 AF	9DA9",
+		"F3 AA	9DAB",
+		"F2 F4	9DAF",
+		"F3 B0	9DB2",
+		"C4 E1	9DB4",
+		"F3 B4	9DB8",
+		"F3 B5	9DBA",
+		"F3 B3	9DBB",
+		"F3 B2	9DC1",
+		"F3 B8	9DC2",
+		"F3 B1	9DC4",
+		"F3 B6	9DC6",
+		"F3 B7	9DCF",
+		"F3 BA	9DD3",
+		"F3 B9	9DD9",
+		"F3 BC	9DE6",
+		"F3 BD	9DED",
+		"F3 BE	9DEF",
+		"CF C9	9DF2",
+		"F3 BB	9DF8",
+		"C2 EB	9DF9",
+		"BA ED	9DFA",
+		"F3 BF	9DFD",
+		"F3 C0	9E1A",
+		"F3 C1	9E1B",
+		"F3 C2	9E1E",
+		"F3 C3	9E75",
+		"B8 B4	9E78",
+		"F3 C4	9E79",
+		"F3 C5	9E7D",
+		"BC AF	9E7F",
+		"F3 C6	9E81",
+		"F3 C7	9E88",
+		"F3 C8	9E8B",
+		"F3 C9	9E8C",
+		"F3 CC	9E91",
+		"F3 CA	9E92",
+		"CF BC	9E93",
+		"F3 CB	9E95",
+		"CE EF	9E97",
+		"F3 CD	9E9D",
+		"CE DB	9E9F",
+		"F3 CE	9EA5",
+		"C7 FE	9EA6",
+		"F3 CF	9EA9",
+		"F3 D1	9EAA",
+		"F3 D2	9EAD",
+		"F3 D0	9EB8",
+		"B9 ED	9EB9",
+		"CC CD	9EBA",
+		"CB E3	9EBB",
+		"D6 F7	9EBC",
+		"DD E0	9EBE",
+		"CB FB	9EBF",
+		"B2 AB	9EC4",
+		"F3 D4	9ECC",
+		"B5 D0	9ECD",
+		"F3 D5	9ECE",
+		"F3 D6	9ECF",
+		"F3 D7	9ED0",
+		"B9 F5	9ED2",
+		"F3 D8	9ED4",
+		"E0 D4	9ED8",
+		"CC DB	9ED9",
+		"C2 E3	9EDB",
+		"F3 D9	9EDC",
+		"F3 DB	9EDD",
+		"F3 DA	9EDE",
+		"F3 DC	9EE0",
+		"F3 DD	9EE5",
+		"F3 DE	9EE8",
+		"F3 DF	9EEF",
+		"F3 E0	9EF4",
+		"F3 E1	9EF6",
+		"F3 E2	9EF7",
+		"F3 E3	9EF9",
+		"F3 E4	9EFB",
+		"F3 E5	9EFC",
+		"F3 E6	9EFD",
+		"F3 E7	9F07",
+		"F3 E8	9F08",
+		"C5 A4	9F0E",
+		"B8 DD	9F13",
+		"F3 EA	9F15",
+		"C1 CD	9F20",
+		"F3 EB	9F21",
+		"F3 EC	9F2C",
+		"C9 A1	9F3B",
+		"F3 ED	9F3E",
+		"F3 EE	9F4A",
+		"E3 B7	9F4B",
+		"EC DA	9F4E",
+		"F0 ED	9F4F",
+		"F3 EF	9F52",
+		"F3 F0	9F54",
+		"F3 F2	9F5F",
+		"F3 F3	9F60",
+		"F3 F4	9F61",
+		"CE F0	9F62",
+		"F3 F1	9F63",
+		"F3 F5	9F66",
+		"F3 F6	9F67",
+		"F3 F8	9F6A",
+		"F3 F7	9F6C",
+		"F3 FA	9F72",
+		"F3 FB	9F76",
+		"F3 F9	9F77",
+		"CE B6	9F8D",
+		"F3 FC	9F95",
+		"F3 FD	9F9C",
+		"E3 D4	9F9D",
+		"F3 FE	9FA0",
+		"A1 AA	FF01",
+		"A1 F4	FF03",
+		"A1 F0	FF04",
+		"A1 F3	FF05",
+		"A1 F5	FF06",
+		"A1 CA	FF08",
+		"A1 CB	FF09",
+		"A1 F6	FF0A",
+		"A1 DC	FF0B",
+		"A1 A4	FF0C",
+		"A1 A5	FF0E",
+		"A1 BF	FF0F",
+		"A3 B0	FF10",
+		"A3 B1	FF11",
+		"A3 B2	FF12",
+		"A3 B3	FF13",
+		"A3 B4	FF14",
+		"A3 B5	FF15",
+		"A3 B6	FF16",
+		"A3 B7	FF17",
+		"A3 B8	FF18",
+		"A3 B9	FF19",
+		"A1 A7	FF1A",
+		"A1 A8	FF1B",
+		"A1 E3	FF1C",
+		"A1 E1	FF1D",
+		"A1 E4	FF1E",
+		"A1 A9	FF1F",
+		"A1 F7	FF20",
+		"A3 C1	FF21",
+		"A3 C2	FF22",
+		"A3 C3	FF23",
+		"A3 C4	FF24",
+		"A3 C5	FF25",
+		"A3 C6	FF26",
+		"A3 C7	FF27",
+		"A3 C8	FF28",
+		"A3 C9	FF29",
+		"A3 CA	FF2A",
+		"A3 CB	FF2B",
+		"A3 CC	FF2C",
+		"A3 CD	FF2D",
+		"A3 CE	FF2E",
+		"A3 CF	FF2F",
+		"A3 D0	FF30",
+		"A3 D1	FF31",
+		"A3 D2	FF32",
+		"A3 D3	FF33",
+		"A3 D4	FF34",
+		"A3 D5	FF35",
+		"A3 D6	FF36",
+		"A3 D7	FF37",
+		"A3 D8	FF38",
+		"A3 D9	FF39",
+		"A3 DA	FF3A",
+		"A1 CE	FF3B",
+		"A1 C0	FF3C",
+		"A1 CF	FF3D",
+		"A1 B0	FF3E",
+		"A1 B2	FF3F",
+		"A1 AE	FF40",
+		"A3 E1	FF41",
+		"A3 E2	FF42",
+		"A3 E3	FF43",
+		"A3 E4	FF44",
+		"A3 E5	FF45",
+		"A3 E6	FF46",
+		"A3 E7	FF47",
+		"A3 E8	FF48",
+		"A3 E9	FF49",
+		"A3 EA	FF4A",
+		"A3 EB	FF4B",
+		"A3 EC	FF4C",
+		"A3 ED	FF4D",
+		"A3 EE	FF4E",
+		"A3 EF	FF4F",
+		"A3 F0	FF50",
+		"A3 F1	FF51",
+		"A3 F2	FF52",
+		"A3 F3	FF53",
+		"A3 F4	FF54",
+		"A3 F5	FF55",
+		"A3 F6	FF56",
+		"A3 F7	FF57",
+		"A3 F8	FF58",
+		"A3 F9	FF59",
+		"A3 FA	FF5A",
+		"A1 D0	FF5B",
+		"A1 C3	FF5C",
+		"A1 D1	FF5D",
+		"A1 B1	FFE3",
+		"A1 EF	FFE5",
+};
--- /dev/null
+++ b/appl/lib/convcs/genjisx0212.b
@@ -1,0 +1,6146 @@
+implement genjisx0212;
+
+include "sys.m";
+include "draw.m";
+
+genjisx0212 : module {
+	init: fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+DATAFILE : con "/lib/convcs/jisx0212";
+
+PAGESZ : con 94;
+NPAGES : con 77;
+PAGE0 : con 16rA1;
+CHAR0 : con 16rA1;
+
+BADCHAR : con 16rFFFD;
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->create(DATAFILE, Sys->OWRITE, 8r644);
+	if (fd == nil) {
+		sys->print("cannot create %s: %r\n", DATAFILE);
+		return;
+	}
+
+	pages := array [NPAGES * PAGESZ] of { * => BADCHAR };
+
+	for (i := 0; i < len data; i++) {
+		(nil, toks) := sys->tokenize(data[i], "\t");
+		(bytes, ucode) := (hd toks, hd tl toks);
+		u := hex2int(ucode);
+		(nil, blist) := sys->tokenize(bytes, " ");
+		b1 := hex2int(hd blist);
+		b2 := hex2int(hd tl blist);
+
+		page := b1 - PAGE0;
+		char := b2 - CHAR0;
+
+		pages[(page * PAGESZ)+char] = u;
+	}
+
+	s := "";
+	for (i = 0; i < len pages; i++)
+		s[i] = pages[i];
+	pages = nil;
+	buf := array of byte s;
+	sys->write(fd, buf, len buf);
+}
+
+
+hex2int(s: string): int
+{
+	n := 0;
+	for (i := 0; i < len s; i++) {
+		case s[i] {
+		'0' to '9' =>
+			n = 16*n + s[i] - '0';
+		'A' to 'F' =>
+			n = 16*n + s[i] + 10 - 'A';
+		'a' to 'f' =>
+			n = 16*n + s[i] + 10 - 'a';
+		* =>
+			return n;
+		}
+	}
+	return n;
+}
+
+
+# Data is derived from Unicode Consortium "CharmapML" mapping data
+# for EUC-JP.
+# JISX0212 appears as charset G3 of the ISO-2022 based EUC-JP encoding
+# Although this format is not convenient for building our mapping data file
+# it is easily extracted from the Unicode Consortium data.
+
+data := array [] of {
+		"A2 C2	00A1",
+		"A2 F0	00A4",
+		"A2 C3	00A6",
+		"A2 ED	00A9",
+		"A2 EC	00AA",
+		"A2 EE	00AE",
+		"A2 B4	00AF",
+		"A2 B1	00B8",
+		"A2 EB	00BA",
+		"A2 C4	00BF",
+		"AA A2	00C0",
+		"AA A1	00C1",
+		"AA A4	00C2",
+		"AA AA	00C3",
+		"AA A3	00C4",
+		"AA A9	00C5",
+		"A9 A1	00C6",
+		"AA AE	00C7",
+		"AA B2	00C8",
+		"AA B1	00C9",
+		"AA B4	00CA",
+		"AA B3	00CB",
+		"AA C0	00CC",
+		"AA BF	00CD",
+		"AA C2	00CE",
+		"AA C1	00CF",
+		"AA D0	00D1",
+		"AA D2	00D2",
+		"AA D1	00D3",
+		"AA D4	00D4",
+		"AA D8	00D5",
+		"AA D3	00D6",
+		"A9 AC	00D8",
+		"AA E3	00D9",
+		"AA E2	00DA",
+		"AA E5	00DB",
+		"AA E4	00DC",
+		"AA F2	00DD",
+		"A9 B0	00DE",
+		"A9 CE	00DF",
+		"AB A2	00E0",
+		"AB A1	00E1",
+		"AB A4	00E2",
+		"AB AA	00E3",
+		"AB A3	00E4",
+		"AB A9	00E5",
+		"A9 C1	00E6",
+		"AB AE	00E7",
+		"AB B2	00E8",
+		"AB B1	00E9",
+		"AB B4	00EA",
+		"AB B3	00EB",
+		"AB C0	00EC",
+		"AB BF	00ED",
+		"AB C2	00EE",
+		"AB C1	00EF",
+		"A9 C3	00F0",
+		"AB D0	00F1",
+		"AB D2	00F2",
+		"AB D1	00F3",
+		"AB D4	00F4",
+		"AB D8	00F5",
+		"AB D3	00F6",
+		"A9 CC	00F8",
+		"AB E3	00F9",
+		"AB E2	00FA",
+		"AB E5	00FB",
+		"AB E4	00FC",
+		"AB F2	00FD",
+		"A9 D0	00FE",
+		"AB F3	00FF",
+		"AA A7	0100",
+		"AB A7	0101",
+		"AA A5	0102",
+		"AB A5	0103",
+		"AA A8	0104",
+		"AB A8	0105",
+		"AA AB	0106",
+		"AB AB	0107",
+		"AA AC	0108",
+		"AB AC	0109",
+		"AA AF	010A",
+		"AB AF	010B",
+		"AA AD	010C",
+		"AB AD	010D",
+		"AA B0	010E",
+		"AB B0	010F",
+		"A9 A2	0110",
+		"A9 C2	0111",
+		"AA B7	0112",
+		"AB B7	0113",
+		"AA B6	0116",
+		"AB B6	0117",
+		"AA B8	0118",
+		"AB B8	0119",
+		"AA B5	011A",
+		"AB B5	011B",
+		"AA BA	011C",
+		"AB BA	011D",
+		"AA BB	011E",
+		"AB BB	011F",
+		"AA BD	0120",
+		"AB BD	0121",
+		"AA BC	0122",
+		"AA BE	0124",
+		"AB BE	0125",
+		"A9 A4	0126",
+		"A9 C4	0127",
+		"AA C7	0128",
+		"AB C7	0129",
+		"AA C5	012A",
+		"AB C5	012B",
+		"AA C6	012E",
+		"AB C6	012F",
+		"AA C4	0130",
+		"A9 C5	0131",
+		"A9 A6	0132",
+		"A9 C6	0133",
+		"AA C8	0134",
+		"AB C8	0135",
+		"AA C9	0136",
+		"AB C9	0137",
+		"A9 C7	0138",
+		"AA CA	0139",
+		"AB CA	013A",
+		"AA CC	013B",
+		"AB CC	013C",
+		"AA CB	013D",
+		"AB CB	013E",
+		"A9 A9	013F",
+		"A9 C9	0140",
+		"A9 A8	0141",
+		"A9 C8	0142",
+		"AA CD	0143",
+		"AB CD	0144",
+		"AA CF	0145",
+		"AB CF	0146",
+		"AA CE	0147",
+		"AB CE	0148",
+		"A9 CA	0149",
+		"A9 AB	014A",
+		"A9 CB	014B",
+		"AA D7	014C",
+		"AB D7	014D",
+		"AA D6	0150",
+		"AB D6	0151",
+		"A9 AD	0152",
+		"A9 CD	0153",
+		"AA D9	0154",
+		"AB D9	0155",
+		"AA DB	0156",
+		"AB DB	0157",
+		"AA DA	0158",
+		"AB DA	0159",
+		"AA DC	015A",
+		"AB DC	015B",
+		"AA DD	015C",
+		"AB DD	015D",
+		"AA DF	015E",
+		"AB DF	015F",
+		"AA DE	0160",
+		"AB DE	0161",
+		"AA E1	0162",
+		"AB E1	0163",
+		"AA E0	0164",
+		"AB E0	0165",
+		"A9 AF	0166",
+		"A9 CF	0167",
+		"AA EC	0168",
+		"AB EC	0169",
+		"AA E9	016A",
+		"AB E9	016B",
+		"AA E6	016C",
+		"AB E6	016D",
+		"AA EB	016E",
+		"AB EB	016F",
+		"AA E8	0170",
+		"AB E8	0171",
+		"AA EA	0172",
+		"AB EA	0173",
+		"AA F1	0174",
+		"AB F1	0175",
+		"AA F4	0176",
+		"AB F4	0177",
+		"AA F3	0178",
+		"AA F5	0179",
+		"AB F5	017A",
+		"AA F7	017B",
+		"AB F7	017C",
+		"AA F6	017D",
+		"AB F6	017E",
+		"AA A6	01CD",
+		"AB A6	01CE",
+		"AA C3	01CF",
+		"AB C3	01D0",
+		"AA D5	01D1",
+		"AB D5	01D2",
+		"AA E7	01D3",
+		"AB E7	01D4",
+		"AA F0	01D5",
+		"AB F0	01D6",
+		"AA ED	01D7",
+		"AB ED	01D8",
+		"AA EF	01D9",
+		"AB EF	01DA",
+		"AA EE	01DB",
+		"AB EE	01DC",
+		"AB B9	01F5",
+		"A2 B0	02C7",
+		"A2 AF	02D8",
+		"A2 B2	02D9",
+		"A2 B6	02DA",
+		"A2 B5	02DB",
+		"A2 B3	02DD",
+		"A2 B8	0384",
+		"A2 B9	0385",
+		"A6 E1	0386",
+		"A6 E2	0388",
+		"A6 E3	0389",
+		"A6 E4	038A",
+		"A6 E7	038C",
+		"A6 E9	038E",
+		"A6 EC	038F",
+		"A6 F6	0390",
+		"A6 E5	03AA",
+		"A6 EA	03AB",
+		"A6 F1	03AC",
+		"A6 F2	03AD",
+		"A6 F3	03AE",
+		"A6 F4	03AF",
+		"A6 FB	03B0",
+		"A6 F8	03C2",
+		"A6 F5	03CA",
+		"A6 FA	03CB",
+		"A6 F7	03CC",
+		"A6 F9	03CD",
+		"A6 FC	03CE",
+		"A7 C2	0402",
+		"A7 C3	0403",
+		"A7 C4	0404",
+		"A7 C5	0405",
+		"A7 C6	0406",
+		"A7 C7	0407",
+		"A7 C8	0408",
+		"A7 C9	0409",
+		"A7 CA	040A",
+		"A7 CB	040B",
+		"A7 CC	040C",
+		"A7 CD	040E",
+		"A7 CE	040F",
+		"A7 F2	0452",
+		"A7 F3	0453",
+		"A7 F4	0454",
+		"A7 F5	0455",
+		"A7 F6	0456",
+		"A7 F7	0457",
+		"A7 F8	0458",
+		"A7 F9	0459",
+		"A7 FA	045A",
+		"A7 FB	045B",
+		"A7 FC	045C",
+		"A7 FD	045E",
+		"A7 FE	045F",
+		"A2 F1	2116",
+		"A2 EF	2122",
+		"B0 A1	4E02",
+		"B0 A2	4E04",
+		"B0 A3	4E05",
+		"B0 A4	4E0C",
+		"B0 A5	4E12",
+		"B0 A6	4E1F",
+		"B0 A7	4E23",
+		"B0 A8	4E24",
+		"B0 A9	4E28",
+		"B0 AA	4E2B",
+		"B0 AB	4E2E",
+		"B0 AC	4E2F",
+		"B0 AD	4E30",
+		"B0 AE	4E35",
+		"B0 AF	4E40",
+		"B0 B0	4E41",
+		"B0 B1	4E44",
+		"B0 B2	4E47",
+		"B0 B3	4E51",
+		"B0 B4	4E5A",
+		"B0 B5	4E5C",
+		"B0 B6	4E63",
+		"B0 B7	4E68",
+		"B0 B8	4E69",
+		"B0 B9	4E74",
+		"B0 BA	4E75",
+		"B0 BB	4E79",
+		"B0 BC	4E7F",
+		"B0 BD	4E8D",
+		"B0 BE	4E96",
+		"B0 BF	4E97",
+		"B0 C0	4E9D",
+		"B0 C1	4EAF",
+		"B0 C2	4EB9",
+		"B0 C3	4EC3",
+		"B0 C4	4ED0",
+		"B0 C5	4EDA",
+		"B0 C6	4EDB",
+		"B0 C7	4EE0",
+		"B0 C8	4EE1",
+		"B0 C9	4EE2",
+		"B0 CA	4EE8",
+		"B0 CB	4EEF",
+		"B0 CC	4EF1",
+		"B0 CD	4EF3",
+		"B0 CE	4EF5",
+		"B0 CF	4EFD",
+		"B0 D0	4EFE",
+		"B0 D1	4EFF",
+		"B0 D2	4F00",
+		"B0 D3	4F02",
+		"B0 D4	4F03",
+		"B0 D5	4F08",
+		"B0 D6	4F0B",
+		"B0 D7	4F0C",
+		"B0 D8	4F12",
+		"B0 D9	4F15",
+		"B0 DA	4F16",
+		"B0 DB	4F17",
+		"B0 DC	4F19",
+		"B0 DD	4F2E",
+		"B0 DE	4F31",
+		"B0 E0	4F33",
+		"B0 E1	4F35",
+		"B0 E2	4F37",
+		"B0 E3	4F39",
+		"B0 E4	4F3B",
+		"B0 E5	4F3E",
+		"B0 E6	4F40",
+		"B0 E7	4F42",
+		"B0 E8	4F48",
+		"B0 E9	4F49",
+		"B0 EA	4F4B",
+		"B0 EB	4F4C",
+		"B0 EC	4F52",
+		"B0 ED	4F54",
+		"B0 EE	4F56",
+		"B0 EF	4F58",
+		"B0 F0	4F5F",
+		"B0 DF	4F60",
+		"B0 F1	4F63",
+		"B0 F2	4F6A",
+		"B0 F3	4F6C",
+		"B0 F4	4F6E",
+		"B0 F5	4F71",
+		"B0 F6	4F77",
+		"B0 F7	4F78",
+		"B0 F8	4F79",
+		"B0 F9	4F7A",
+		"B0 FA	4F7D",
+		"B0 FB	4F7E",
+		"B0 FC	4F81",
+		"B0 FD	4F82",
+		"B0 FE	4F84",
+		"B1 A1	4F85",
+		"B1 A2	4F89",
+		"B1 A3	4F8A",
+		"B1 A4	4F8C",
+		"B1 A5	4F8E",
+		"B1 A6	4F90",
+		"B1 A7	4F92",
+		"B1 A8	4F93",
+		"B1 A9	4F94",
+		"B1 AA	4F97",
+		"B1 AB	4F99",
+		"B1 AC	4F9A",
+		"B1 AD	4F9E",
+		"B1 AE	4F9F",
+		"B1 AF	4FB2",
+		"B1 B0	4FB7",
+		"B1 B1	4FB9",
+		"B1 B2	4FBB",
+		"B1 B3	4FBC",
+		"B1 B4	4FBD",
+		"B1 B5	4FBE",
+		"B1 B6	4FC0",
+		"B1 B7	4FC1",
+		"B1 B8	4FC5",
+		"B1 B9	4FC6",
+		"B1 BA	4FC8",
+		"B1 BB	4FC9",
+		"B1 BC	4FCB",
+		"B1 BD	4FCC",
+		"B1 BE	4FCD",
+		"B1 BF	4FCF",
+		"B1 C0	4FD2",
+		"B1 C1	4FDC",
+		"B1 C2	4FE0",
+		"B1 C3	4FE2",
+		"B1 C4	4FF0",
+		"B1 C5	4FF2",
+		"B1 C6	4FFC",
+		"B1 C7	4FFD",
+		"B1 C8	4FFF",
+		"B1 C9	5000",
+		"B1 CA	5001",
+		"B1 CB	5004",
+		"B1 CC	5007",
+		"B1 CD	500A",
+		"B1 CE	500C",
+		"B1 CF	500E",
+		"B1 D0	5010",
+		"B1 D1	5013",
+		"B1 D2	5017",
+		"B1 D3	5018",
+		"B1 D4	501B",
+		"B1 D5	501C",
+		"B1 D6	501D",
+		"B1 D7	501E",
+		"B1 D8	5022",
+		"B1 D9	5027",
+		"B1 DA	502E",
+		"B1 DB	5030",
+		"B1 DC	5032",
+		"B1 DD	5033",
+		"B1 DE	5035",
+		"B1 F6	503B",
+		"B1 DF	5040",
+		"B1 E0	5041",
+		"B1 E1	5042",
+		"B1 E2	5045",
+		"B1 E3	5046",
+		"B1 E4	504A",
+		"B1 E5	504C",
+		"B1 E6	504E",
+		"B1 E7	5051",
+		"B1 E8	5052",
+		"B1 E9	5053",
+		"B1 EA	5057",
+		"B1 EB	5059",
+		"B1 EC	505F",
+		"B1 ED	5060",
+		"B1 EE	5062",
+		"B1 EF	5063",
+		"B1 F0	5066",
+		"B1 F1	5067",
+		"B1 F2	506A",
+		"B1 F3	506D",
+		"B1 F4	5070",
+		"B1 F5	5071",
+		"B1 F7	5081",
+		"B1 F8	5083",
+		"B1 F9	5084",
+		"B1 FA	5086",
+		"B1 FB	508A",
+		"B1 FC	508E",
+		"B1 FD	508F",
+		"B1 FE	5090",
+		"B2 A1	5092",
+		"B2 A2	5093",
+		"B2 A3	5094",
+		"B2 A4	5096",
+		"B2 A5	509B",
+		"B2 A6	509C",
+		"B2 A7	509E",
+		"B2 A8	509F",
+		"B2 A9	50A0",
+		"B2 AA	50A1",
+		"B2 AB	50A2",
+		"B2 AC	50AA",
+		"B2 AD	50AF",
+		"B2 AE	50B0",
+		"B2 AF	50B9",
+		"B2 B0	50BA",
+		"B2 B1	50BD",
+		"B2 B2	50C0",
+		"B2 B3	50C3",
+		"B2 B4	50C4",
+		"B2 B5	50C7",
+		"B2 B6	50CC",
+		"B2 B7	50CE",
+		"B2 B8	50D0",
+		"B2 B9	50D3",
+		"B2 BA	50D4",
+		"B2 BB	50D8",
+		"B2 BC	50DC",
+		"B2 BD	50DD",
+		"B2 BE	50DF",
+		"B2 BF	50E2",
+		"B2 C0	50E4",
+		"B2 C1	50E6",
+		"B2 C2	50E8",
+		"B2 C3	50E9",
+		"B2 C4	50EF",
+		"B2 C5	50F1",
+		"B2 D1	50F2",
+		"B2 C6	50F6",
+		"B2 C7	50FA",
+		"B2 C8	50FE",
+		"B2 C9	5103",
+		"B2 CA	5106",
+		"B2 CB	5107",
+		"B2 CC	5108",
+		"B2 CD	510B",
+		"B2 CE	510C",
+		"B2 CF	510D",
+		"B2 D0	510E",
+		"B2 D2	5110",
+		"B2 D3	5117",
+		"B2 D4	5119",
+		"B2 D5	511B",
+		"B2 D6	511C",
+		"B2 D7	511D",
+		"B2 D8	511E",
+		"B2 D9	5123",
+		"B2 DA	5127",
+		"B2 DB	5128",
+		"B2 DC	512C",
+		"B2 DD	512D",
+		"B2 DE	512F",
+		"B2 DF	5131",
+		"B2 E0	5133",
+		"B2 E1	5134",
+		"B2 E2	5135",
+		"B2 E3	5138",
+		"B2 E4	5139",
+		"B2 E5	5142",
+		"B2 E6	514A",
+		"B2 E7	514F",
+		"B2 E8	5153",
+		"B2 E9	5155",
+		"B2 EA	5157",
+		"B2 EB	5158",
+		"B2 EC	515F",
+		"B2 ED	5164",
+		"B2 EE	5166",
+		"B2 EF	517E",
+		"B2 F0	5183",
+		"B2 F1	5184",
+		"B2 F2	518B",
+		"B2 F3	518E",
+		"B2 F4	5198",
+		"B2 F5	519D",
+		"B2 F6	51A1",
+		"B2 F7	51A3",
+		"B2 F8	51AD",
+		"B2 F9	51B8",
+		"B2 FA	51BA",
+		"B2 FB	51BC",
+		"B2 FC	51BE",
+		"B2 FD	51BF",
+		"B2 FE	51C2",
+		"B3 A1	51C8",
+		"B3 A2	51CF",
+		"B3 A3	51D1",
+		"B3 A4	51D2",
+		"B3 A5	51D3",
+		"B3 A6	51D5",
+		"B3 A7	51D8",
+		"B3 A8	51DE",
+		"B3 A9	51E2",
+		"B3 AA	51E5",
+		"B3 AB	51EE",
+		"B3 AC	51F2",
+		"B3 AD	51F3",
+		"B3 AE	51F4",
+		"B3 AF	51F7",
+		"B3 B0	5201",
+		"B3 B1	5202",
+		"B3 B2	5205",
+		"B3 B3	5212",
+		"B3 B4	5213",
+		"B3 B5	5215",
+		"B3 B6	5216",
+		"B3 B7	5218",
+		"B3 B8	5222",
+		"B3 B9	5228",
+		"B3 BA	5231",
+		"B3 BB	5232",
+		"B3 BC	5235",
+		"B3 BD	523C",
+		"B3 BE	5245",
+		"B3 BF	5249",
+		"B3 C0	5255",
+		"B3 C1	5257",
+		"B3 C2	5258",
+		"B3 C3	525A",
+		"B3 C4	525C",
+		"B3 C5	525F",
+		"B3 C6	5260",
+		"B3 C7	5261",
+		"B3 C8	5266",
+		"B3 C9	526E",
+		"B3 CA	5277",
+		"B3 CB	5278",
+		"B3 CC	5279",
+		"B3 CD	5280",
+		"B3 CE	5282",
+		"B3 CF	5285",
+		"B3 D0	528A",
+		"B3 D1	528C",
+		"B3 D2	5293",
+		"B3 D3	5295",
+		"B3 D4	5296",
+		"B3 D5	5297",
+		"B3 D6	5298",
+		"B3 D7	529A",
+		"B3 D8	529C",
+		"B3 D9	52A4",
+		"B3 DA	52A5",
+		"B3 DB	52A6",
+		"B3 DC	52A7",
+		"B3 DD	52AF",
+		"B3 DE	52B0",
+		"B3 DF	52B6",
+		"B3 E0	52B7",
+		"B3 E1	52B8",
+		"B3 E2	52BA",
+		"B3 E3	52BB",
+		"B3 E4	52BD",
+		"B3 E5	52C0",
+		"B3 E6	52C4",
+		"B3 E7	52C6",
+		"B3 E8	52C8",
+		"B3 E9	52CC",
+		"B3 EA	52CF",
+		"B3 EB	52D1",
+		"B3 EC	52D4",
+		"B3 ED	52D6",
+		"B3 EE	52DB",
+		"B3 EF	52DC",
+		"B3 F0	52E1",
+		"B3 F1	52E5",
+		"B3 F2	52E8",
+		"B3 F3	52E9",
+		"B3 F4	52EA",
+		"B3 F5	52EC",
+		"B3 F6	52F0",
+		"B3 F7	52F1",
+		"B3 F8	52F4",
+		"B3 F9	52F6",
+		"B3 FA	52F7",
+		"B3 FB	5300",
+		"B3 FC	5303",
+		"B3 FD	530A",
+		"B3 FE	530B",
+		"B4 A1	530C",
+		"B4 A2	5311",
+		"B4 A3	5313",
+		"B4 A4	5318",
+		"B4 A5	531B",
+		"B4 A6	531C",
+		"B4 A7	531E",
+		"B4 A8	531F",
+		"B4 A9	5325",
+		"B4 AA	5327",
+		"B4 AB	5328",
+		"B4 AC	5329",
+		"B4 AD	532B",
+		"B4 AE	532C",
+		"B4 AF	532D",
+		"B4 B0	5330",
+		"B4 B1	5332",
+		"B4 B2	5335",
+		"B4 B3	533C",
+		"B4 B4	533D",
+		"B4 B5	533E",
+		"B4 B6	5342",
+		"B4 B8	534B",
+		"B4 B7	534C",
+		"B4 B9	5359",
+		"B4 BA	535B",
+		"B4 BB	5361",
+		"B4 BC	5363",
+		"B4 BD	5365",
+		"B4 BE	536C",
+		"B4 BF	536D",
+		"B4 C0	5372",
+		"B4 C1	5379",
+		"B4 C2	537E",
+		"B4 C3	5383",
+		"B4 C4	5387",
+		"B4 C5	5388",
+		"B4 C6	538E",
+		"B4 C7	5393",
+		"B4 C8	5394",
+		"B4 C9	5399",
+		"B4 CA	539D",
+		"B4 CB	53A1",
+		"B4 CC	53A4",
+		"B4 CD	53AA",
+		"B4 CE	53AB",
+		"B4 CF	53AF",
+		"B4 D0	53B2",
+		"B4 D1	53B4",
+		"B4 D2	53B5",
+		"B4 D3	53B7",
+		"B4 D4	53B8",
+		"B4 D5	53BA",
+		"B4 D6	53BD",
+		"B4 D7	53C0",
+		"B4 D8	53C5",
+		"B4 D9	53CF",
+		"B4 DA	53D2",
+		"B4 DB	53D3",
+		"B4 DC	53D5",
+		"B4 DD	53DA",
+		"B4 DE	53DD",
+		"B4 DF	53DE",
+		"B4 E0	53E0",
+		"B4 E1	53E6",
+		"B4 E2	53E7",
+		"B4 E3	53F5",
+		"B4 E4	5402",
+		"B4 E5	5413",
+		"B4 E6	541A",
+		"B4 E7	5421",
+		"B4 E8	5427",
+		"B4 E9	5428",
+		"B4 EA	542A",
+		"B4 EB	542F",
+		"B4 EC	5431",
+		"B4 ED	5434",
+		"B4 EE	5435",
+		"B4 EF	5443",
+		"B4 F0	5444",
+		"B4 F1	5447",
+		"B4 F2	544D",
+		"B4 F3	544F",
+		"B4 F4	545E",
+		"B4 F5	5462",
+		"B4 F6	5464",
+		"B4 F7	5466",
+		"B4 F8	5467",
+		"B4 F9	5469",
+		"B4 FA	546B",
+		"B4 FB	546D",
+		"B4 FC	546E",
+		"B4 FD	5474",
+		"B4 FE	547F",
+		"B5 A1	5481",
+		"B5 A2	5483",
+		"B5 A3	5485",
+		"B5 A4	5488",
+		"B5 A5	5489",
+		"B5 A6	548D",
+		"B5 A7	5491",
+		"B5 A8	5495",
+		"B5 A9	5496",
+		"B5 AA	549C",
+		"B5 AB	549F",
+		"B5 AC	54A1",
+		"B5 AD	54A6",
+		"B5 AE	54A7",
+		"B5 AF	54A9",
+		"B5 B0	54AA",
+		"B5 B1	54AD",
+		"B5 B2	54AE",
+		"B5 B3	54B1",
+		"B5 B4	54B7",
+		"B5 B5	54B9",
+		"B5 B6	54BA",
+		"B5 B7	54BB",
+		"B5 B8	54BF",
+		"B5 B9	54C6",
+		"B5 BA	54CA",
+		"B5 BB	54CD",
+		"B5 BC	54CE",
+		"B5 BD	54E0",
+		"B5 BE	54EA",
+		"B5 BF	54EC",
+		"B5 C0	54EF",
+		"B5 C1	54F6",
+		"B5 C2	54FC",
+		"B5 C3	54FE",
+		"B5 C4	54FF",
+		"B5 C5	5500",
+		"B5 C6	5501",
+		"B5 C7	5505",
+		"B5 C8	5508",
+		"B5 C9	5509",
+		"B5 CA	550C",
+		"B5 CB	550D",
+		"B5 CC	550E",
+		"B5 CD	5515",
+		"B5 CE	552A",
+		"B5 CF	552B",
+		"B5 D0	5532",
+		"B5 D1	5535",
+		"B5 D2	5536",
+		"B5 D3	553B",
+		"B5 D4	553C",
+		"B5 D5	553D",
+		"B5 D6	5541",
+		"B5 D7	5547",
+		"B5 D8	5549",
+		"B5 D9	554A",
+		"B5 DA	554D",
+		"B5 DB	5550",
+		"B5 DC	5551",
+		"B5 DD	5558",
+		"B5 DE	555A",
+		"B5 DF	555B",
+		"B5 E0	555E",
+		"B5 E1	5560",
+		"B5 E2	5561",
+		"B5 E3	5564",
+		"B5 E4	5566",
+		"B5 E5	557F",
+		"B5 E6	5581",
+		"B5 E7	5582",
+		"B5 E8	5586",
+		"B5 E9	5588",
+		"B5 EA	558E",
+		"B5 EB	558F",
+		"B5 EC	5591",
+		"B5 ED	5592",
+		"B5 EE	5593",
+		"B5 EF	5594",
+		"B5 F0	5597",
+		"B5 F1	55A3",
+		"B5 F2	55A4",
+		"B5 F3	55AD",
+		"B5 F4	55B2",
+		"B5 F5	55BF",
+		"B5 F6	55C1",
+		"B5 F7	55C3",
+		"B5 F8	55C6",
+		"B5 F9	55C9",
+		"B5 FA	55CB",
+		"B5 FB	55CC",
+		"B5 FC	55CE",
+		"B5 FD	55D1",
+		"B5 FE	55D2",
+		"B6 A1	55D3",
+		"B6 A2	55D7",
+		"B6 A3	55D8",
+		"B6 A4	55DB",
+		"B6 A5	55DE",
+		"B6 A6	55E2",
+		"B6 A7	55E9",
+		"B6 A8	55F6",
+		"B6 A9	55FF",
+		"B6 AA	5605",
+		"B6 AB	5608",
+		"B6 AC	560A",
+		"B6 AD	560D",
+		"B6 AE	560E",
+		"B6 AF	560F",
+		"B6 B0	5610",
+		"B6 B1	5611",
+		"B6 B2	5612",
+		"B6 B3	5619",
+		"B6 B4	562C",
+		"B6 B5	5630",
+		"B6 B6	5633",
+		"B6 B7	5635",
+		"B6 B8	5637",
+		"B6 B9	5639",
+		"B6 BA	563B",
+		"B6 BB	563C",
+		"B6 BC	563D",
+		"B6 BD	563F",
+		"B6 BE	5640",
+		"B6 BF	5641",
+		"B6 C0	5643",
+		"B6 C1	5644",
+		"B6 C2	5646",
+		"B6 C3	5649",
+		"B6 C4	564B",
+		"B6 C5	564D",
+		"B6 C6	564F",
+		"B6 C7	5654",
+		"B6 C8	565E",
+		"B6 C9	5660",
+		"B6 CA	5661",
+		"B6 CB	5662",
+		"B6 CC	5663",
+		"B6 CD	5666",
+		"B6 CE	5669",
+		"B6 CF	566D",
+		"B6 D0	566F",
+		"B6 D1	5671",
+		"B6 D2	5672",
+		"B6 D3	5675",
+		"B6 D4	5684",
+		"B6 D5	5685",
+		"B6 D6	5688",
+		"B6 D7	568B",
+		"B6 D8	568C",
+		"B6 D9	5695",
+		"B6 DA	5699",
+		"B6 DB	569A",
+		"B6 DC	569D",
+		"B6 DD	569E",
+		"B6 DE	569F",
+		"B6 DF	56A6",
+		"B6 E0	56A7",
+		"B6 E1	56A8",
+		"B6 E2	56A9",
+		"B6 E3	56AB",
+		"B6 E4	56AC",
+		"B6 E5	56AD",
+		"B6 E6	56B1",
+		"B6 E7	56B3",
+		"B6 E8	56B7",
+		"B6 E9	56BE",
+		"B6 EA	56C5",
+		"B6 EB	56C9",
+		"B6 EC	56CA",
+		"B6 ED	56CB",
+		"B6 F0	56CC",
+		"B6 F1	56CD",
+		"B6 EE	56CF",
+		"B6 EF	56D0",
+		"B6 F2	56D9",
+		"B6 F3	56DC",
+		"B6 F4	56DD",
+		"B6 F5	56DF",
+		"B6 F6	56E1",
+		"B6 F7	56E4",
+		"B6 F8	56E5",
+		"B6 F9	56E6",
+		"B6 FA	56E7",
+		"B6 FB	56E8",
+		"B6 FD	56EB",
+		"B6 FE	56ED",
+		"B6 FC	56F1",
+		"B7 A1	56F6",
+		"B7 A2	56F7",
+		"B7 A3	5701",
+		"B7 A4	5702",
+		"B7 A5	5707",
+		"B7 A6	570A",
+		"B7 A7	570C",
+		"B7 A8	5711",
+		"B7 A9	5715",
+		"B7 AA	571A",
+		"B7 AB	571B",
+		"B7 AC	571D",
+		"B7 AD	5720",
+		"B7 AE	5722",
+		"B7 AF	5723",
+		"B7 B0	5724",
+		"B7 B1	5725",
+		"B7 B2	5729",
+		"B7 B3	572A",
+		"B7 B4	572C",
+		"B7 B5	572E",
+		"B7 B6	572F",
+		"B7 B7	5733",
+		"B7 B8	5734",
+		"B7 B9	573D",
+		"B7 BA	573E",
+		"B7 BB	573F",
+		"B7 BC	5745",
+		"B7 BD	5746",
+		"B7 BE	574C",
+		"B7 BF	574D",
+		"B7 C0	5752",
+		"B7 C1	5762",
+		"B7 C2	5765",
+		"B7 C3	5767",
+		"B7 C4	5768",
+		"B7 C5	576B",
+		"B7 C6	576D",
+		"B7 C7	576E",
+		"B7 C8	576F",
+		"B7 C9	5770",
+		"B7 CA	5771",
+		"B7 CB	5773",
+		"B7 CC	5774",
+		"B7 CD	5775",
+		"B7 CE	5777",
+		"B7 CF	5779",
+		"B7 D0	577A",
+		"B7 D1	577B",
+		"B7 D2	577C",
+		"B7 D3	577E",
+		"B7 D4	5781",
+		"B7 D5	5783",
+		"B7 D6	578C",
+		"B7 D7	5794",
+		"B7 E0	5795",
+		"B7 D8	5797",
+		"B7 D9	5799",
+		"B7 DA	579A",
+		"B7 DB	579C",
+		"B7 DC	579D",
+		"B7 DD	579E",
+		"B7 DE	579F",
+		"B7 DF	57A1",
+		"B7 E1	57A7",
+		"B7 E2	57A8",
+		"B7 E3	57A9",
+		"B7 E4	57AC",
+		"B7 E5	57B8",
+		"B7 E6	57BD",
+		"B7 E7	57C7",
+		"B7 E8	57C8",
+		"B7 E9	57CC",
+		"B7 EA	57CF",
+		"B7 EB	57D5",
+		"B7 EC	57DD",
+		"B7 ED	57DE",
+		"B7 FE	57E1",
+		"B7 EE	57E4",
+		"B7 EF	57E6",
+		"B7 F0	57E7",
+		"B7 F1	57E9",
+		"B7 F2	57ED",
+		"B7 F3	57F0",
+		"B7 F4	57F5",
+		"B7 F5	57F6",
+		"B7 F6	57F8",
+		"B7 F7	57FD",
+		"B7 F8	57FE",
+		"B7 F9	57FF",
+		"B7 FA	5803",
+		"B7 FB	5804",
+		"B7 FC	5808",
+		"B7 FD	5809",
+		"B8 A1	580C",
+		"B8 A2	580D",
+		"B8 A3	581B",
+		"B8 A4	581E",
+		"B8 A5	581F",
+		"B8 A6	5820",
+		"B8 A7	5826",
+		"B8 A8	5827",
+		"B8 A9	582D",
+		"B8 AA	5832",
+		"B8 AB	5839",
+		"B8 AC	583F",
+		"B8 AD	5849",
+		"B8 AE	584C",
+		"B8 AF	584D",
+		"B8 B0	584F",
+		"B8 B1	5850",
+		"B8 B2	5855",
+		"B8 B3	585F",
+		"B8 B4	5861",
+		"B8 B5	5864",
+		"B8 B6	5867",
+		"B8 B7	5868",
+		"B8 B8	5878",
+		"B8 B9	587C",
+		"B8 BA	587F",
+		"B8 BB	5880",
+		"B8 BC	5881",
+		"B8 BD	5887",
+		"B8 BE	5888",
+		"B8 BF	5889",
+		"B8 C0	588A",
+		"B8 C1	588C",
+		"B8 C2	588D",
+		"B8 C3	588F",
+		"B8 C4	5890",
+		"B8 C5	5894",
+		"B8 C6	5896",
+		"B8 C7	589D",
+		"B8 C8	58A0",
+		"B8 C9	58A1",
+		"B8 CA	58A2",
+		"B8 CB	58A6",
+		"B8 CC	58A9",
+		"B8 CD	58B1",
+		"B8 CE	58B2",
+		"B8 D0	58BC",
+		"B8 D1	58C2",
+		"B8 CF	58C4",
+		"B8 D2	58C8",
+		"B8 D3	58CD",
+		"B8 D4	58CE",
+		"B8 D5	58D0",
+		"B8 D6	58D2",
+		"B8 D7	58D4",
+		"B8 D8	58D6",
+		"B8 D9	58DA",
+		"B8 DA	58DD",
+		"B8 DB	58E1",
+		"B8 DC	58E2",
+		"B8 DD	58E9",
+		"B8 DE	58F3",
+		"B8 DF	5905",
+		"B8 E0	5906",
+		"B8 E1	590B",
+		"B8 E2	590C",
+		"B8 E3	5912",
+		"B8 E4	5913",
+		"B8 E5	5914",
+		"B8 E7	591D",
+		"B8 E8	5921",
+		"B8 E9	5923",
+		"B8 EA	5924",
+		"B8 EB	5928",
+		"B8 EC	592F",
+		"B8 ED	5930",
+		"B8 EE	5933",
+		"B8 EF	5935",
+		"B8 F0	5936",
+		"B8 F1	593F",
+		"B8 F2	5943",
+		"B8 F3	5946",
+		"B8 F4	5952",
+		"B8 F5	5953",
+		"B8 F6	5959",
+		"B8 F7	595B",
+		"B8 F8	595D",
+		"B8 F9	595E",
+		"B8 FA	595F",
+		"B8 FB	5961",
+		"B8 FC	5963",
+		"B8 FD	596B",
+		"B8 FE	596D",
+		"B9 A1	596F",
+		"B9 A2	5972",
+		"B9 A3	5975",
+		"B9 A4	5976",
+		"B9 A5	5979",
+		"B9 A6	597B",
+		"B9 A7	597C",
+		"B9 A8	598B",
+		"B9 A9	598C",
+		"B9 AA	598E",
+		"B9 AB	5992",
+		"B9 AC	5995",
+		"B9 AD	5997",
+		"B9 AE	599F",
+		"B9 AF	59A4",
+		"B9 B0	59A7",
+		"B9 B1	59AD",
+		"B9 B2	59AE",
+		"B9 B3	59AF",
+		"B9 B4	59B0",
+		"B9 B5	59B3",
+		"B9 B6	59B7",
+		"B9 B7	59BA",
+		"B9 B8	59BC",
+		"B9 B9	59C1",
+		"B9 BA	59C3",
+		"B9 BB	59C4",
+		"B9 BC	59C8",
+		"B9 BD	59CA",
+		"B9 BE	59CD",
+		"B9 BF	59D2",
+		"B9 C0	59DD",
+		"B9 C1	59DE",
+		"B9 C2	59DF",
+		"B9 C3	59E3",
+		"B9 C4	59E4",
+		"B9 C5	59E7",
+		"B9 C6	59EE",
+		"B9 C7	59EF",
+		"B9 C8	59F1",
+		"B9 C9	59F2",
+		"B9 CA	59F4",
+		"B9 CB	59F7",
+		"B9 CC	5A00",
+		"B9 CD	5A04",
+		"B9 CE	5A0C",
+		"B9 CF	5A0D",
+		"B9 D0	5A0E",
+		"B9 D1	5A12",
+		"B9 D2	5A13",
+		"B9 D3	5A1E",
+		"B9 D4	5A23",
+		"B9 D5	5A24",
+		"B9 D6	5A27",
+		"B9 D7	5A28",
+		"B9 D8	5A2A",
+		"B9 D9	5A2D",
+		"B9 DA	5A30",
+		"B9 DB	5A44",
+		"B9 DC	5A45",
+		"B9 DD	5A47",
+		"B9 DE	5A48",
+		"B9 DF	5A4C",
+		"B9 E0	5A50",
+		"B9 E1	5A55",
+		"B9 E2	5A5E",
+		"B9 E3	5A63",
+		"B9 E4	5A65",
+		"B9 E5	5A67",
+		"B9 E6	5A6D",
+		"B9 E7	5A77",
+		"B9 E8	5A7A",
+		"B9 E9	5A7B",
+		"B9 EA	5A7E",
+		"B9 EB	5A8B",
+		"B9 EC	5A90",
+		"B9 ED	5A93",
+		"B9 EE	5A96",
+		"B9 EF	5A99",
+		"B9 F0	5A9C",
+		"B9 F1	5A9E",
+		"B9 F2	5A9F",
+		"B9 F3	5AA0",
+		"B9 F4	5AA2",
+		"B9 F5	5AA7",
+		"B9 F6	5AAC",
+		"B9 F7	5AB1",
+		"B9 F8	5AB2",
+		"B9 F9	5AB3",
+		"B9 FA	5AB5",
+		"B9 FB	5AB8",
+		"B9 FC	5ABA",
+		"B9 FD	5ABB",
+		"B9 FE	5ABF",
+		"BA A1	5AC4",
+		"BA A2	5AC6",
+		"BA A3	5AC8",
+		"BA A4	5ACF",
+		"BA A5	5ADA",
+		"BA A6	5ADC",
+		"BA A7	5AE0",
+		"BA A8	5AE5",
+		"BA A9	5AEA",
+		"BA AA	5AEE",
+		"BA AB	5AF5",
+		"BA AC	5AF6",
+		"BA AD	5AFD",
+		"BA AE	5B00",
+		"BA AF	5B01",
+		"BA B0	5B08",
+		"BA B1	5B17",
+		"BA B3	5B19",
+		"BA B4	5B1B",
+		"BA B5	5B1D",
+		"BA B6	5B21",
+		"BA B7	5B25",
+		"BA B8	5B2D",
+		"BA B2	5B34",
+		"BA B9	5B38",
+		"BA BA	5B41",
+		"BA BB	5B4B",
+		"BA BC	5B4C",
+		"BA BD	5B52",
+		"BA BE	5B56",
+		"BA BF	5B5E",
+		"BA C0	5B68",
+		"BA C1	5B6E",
+		"BA C2	5B6F",
+		"BA C3	5B7C",
+		"BA C4	5B7D",
+		"BA C5	5B7E",
+		"BA C6	5B7F",
+		"BA C7	5B81",
+		"BA C8	5B84",
+		"BA C9	5B86",
+		"BA CA	5B8A",
+		"BA CB	5B8E",
+		"BA CC	5B90",
+		"BA CD	5B91",
+		"BA CE	5B93",
+		"BA CF	5B94",
+		"BA D0	5B96",
+		"BA D1	5BA8",
+		"BA D2	5BA9",
+		"BA D3	5BAC",
+		"BA D4	5BAD",
+		"BA D5	5BAF",
+		"BA D6	5BB1",
+		"BA D7	5BB2",
+		"BA D8	5BB7",
+		"BA D9	5BBA",
+		"BA DA	5BBC",
+		"BA DB	5BC0",
+		"BA DC	5BC1",
+		"BA DD	5BCD",
+		"BA DE	5BCF",
+		"BA DF	5BD6",
+		"BA E0	5BD7",
+		"BA E1	5BD8",
+		"BA E2	5BD9",
+		"BA E3	5BDA",
+		"BA E4	5BE0",
+		"BA E5	5BEF",
+		"BA E6	5BF1",
+		"BA E7	5BF4",
+		"BA E8	5BFD",
+		"BA E9	5C0C",
+		"BA EA	5C17",
+		"BA EB	5C1E",
+		"BA EC	5C1F",
+		"BA ED	5C23",
+		"BA EE	5C26",
+		"BA EF	5C29",
+		"BA F0	5C2B",
+		"BA F1	5C2C",
+		"BA F2	5C2E",
+		"BA F3	5C30",
+		"BA F4	5C32",
+		"BA F5	5C35",
+		"BA F6	5C36",
+		"BA F7	5C59",
+		"BA F8	5C5A",
+		"BA F9	5C5C",
+		"BA FA	5C62",
+		"BA FB	5C63",
+		"BA FC	5C67",
+		"BA FD	5C68",
+		"BA FE	5C69",
+		"BB A1	5C6D",
+		"BB A2	5C70",
+		"BB A3	5C74",
+		"BB A4	5C75",
+		"BB A5	5C7A",
+		"BB A6	5C7B",
+		"BB A7	5C7C",
+		"BB A8	5C7D",
+		"BB A9	5C87",
+		"BB AA	5C88",
+		"BB AB	5C8A",
+		"BB AC	5C8F",
+		"BB AD	5C92",
+		"BB AE	5C9D",
+		"BB AF	5C9F",
+		"BB B0	5CA0",
+		"BB B1	5CA2",
+		"BB B2	5CA3",
+		"BB B3	5CA6",
+		"BB B4	5CAA",
+		"BB B5	5CB2",
+		"BB B6	5CB4",
+		"BB B7	5CB5",
+		"BB B8	5CBA",
+		"BB B9	5CC9",
+		"BB BA	5CCB",
+		"BB BB	5CD2",
+		"BB BD	5CD7",
+		"BB BC	5CDD",
+		"BB BE	5CEE",
+		"BB BF	5CF1",
+		"BB C0	5CF2",
+		"BB C1	5CF4",
+		"BB C2	5D01",
+		"BB C3	5D06",
+		"BB C4	5D0D",
+		"BB C5	5D12",
+		"BB C7	5D23",
+		"BB C8	5D24",
+		"BB C9	5D26",
+		"BB CA	5D27",
+		"BB C6	5D2B",
+		"BB CB	5D31",
+		"BB CC	5D34",
+		"BB CD	5D39",
+		"BB CE	5D3D",
+		"BB CF	5D3F",
+		"BB D0	5D42",
+		"BB D1	5D43",
+		"BB D2	5D46",
+		"BB D3	5D48",
+		"BB D7	5D4A",
+		"BB D5	5D51",
+		"BB D4	5D55",
+		"BB D6	5D59",
+		"BB D8	5D5F",
+		"BB D9	5D60",
+		"BB DA	5D61",
+		"BB DB	5D62",
+		"BB DC	5D64",
+		"BB DD	5D6A",
+		"BB DE	5D6D",
+		"BB DF	5D70",
+		"BB E0	5D79",
+		"BB E1	5D7A",
+		"BB E2	5D7E",
+		"BB E3	5D7F",
+		"BB E4	5D81",
+		"BB E5	5D83",
+		"BB E6	5D88",
+		"BB E7	5D8A",
+		"BB E8	5D92",
+		"BB E9	5D93",
+		"BB EA	5D94",
+		"BB EB	5D95",
+		"BB EC	5D99",
+		"BB ED	5D9B",
+		"BB EE	5D9F",
+		"BB EF	5DA0",
+		"BB F0	5DA7",
+		"BB F1	5DAB",
+		"BB F2	5DB0",
+		"E6 F4	5DB2",
+		"BB F3	5DB4",
+		"BB F4	5DB8",
+		"BB F5	5DB9",
+		"BB F6	5DC3",
+		"BB F7	5DC7",
+		"BB F8	5DCB",
+		"BB FA	5DCE",
+		"BB F9	5DD0",
+		"BB FB	5DD8",
+		"BB FC	5DD9",
+		"BB FD	5DE0",
+		"BB FE	5DE4",
+		"BC A1	5DE9",
+		"BC A2	5DF8",
+		"BC A3	5DF9",
+		"BC A4	5E00",
+		"BC A5	5E07",
+		"BC A6	5E0D",
+		"BC A7	5E12",
+		"BC A8	5E14",
+		"BC A9	5E15",
+		"BC AA	5E18",
+		"BC AB	5E1F",
+		"BC AC	5E20",
+		"BC AE	5E28",
+		"BC AD	5E2E",
+		"BC AF	5E32",
+		"BC B0	5E35",
+		"BC B1	5E3E",
+		"BC B4	5E49",
+		"BC B2	5E4B",
+		"BC B3	5E50",
+		"BC B5	5E51",
+		"BC B6	5E56",
+		"BC B7	5E58",
+		"BC B8	5E5B",
+		"BC B9	5E5C",
+		"BC BA	5E5E",
+		"BC BB	5E68",
+		"BC BC	5E6A",
+		"BC BD	5E6B",
+		"BC BE	5E6C",
+		"BC BF	5E6D",
+		"BC C0	5E6E",
+		"BC C1	5E70",
+		"BC C2	5E80",
+		"BC C3	5E8B",
+		"BC C4	5E8E",
+		"BC C5	5EA2",
+		"BC C6	5EA4",
+		"BC C7	5EA5",
+		"BC C8	5EA8",
+		"BC C9	5EAA",
+		"BC CA	5EAC",
+		"BC CB	5EB1",
+		"BC CC	5EB3",
+		"BC CD	5EBD",
+		"BC CE	5EBE",
+		"BC CF	5EBF",
+		"BC D0	5EC6",
+		"BC D2	5ECB",
+		"BC D1	5ECC",
+		"BC D3	5ECE",
+		"BC D4	5ED1",
+		"BC D5	5ED2",
+		"BC D6	5ED4",
+		"BC D7	5ED5",
+		"BC D8	5EDC",
+		"BC D9	5EDE",
+		"BC DA	5EE5",
+		"BC DB	5EEB",
+		"BC DC	5F02",
+		"BC DD	5F06",
+		"BC DE	5F07",
+		"BC DF	5F08",
+		"BC E0	5F0E",
+		"BC E1	5F19",
+		"BC E2	5F1C",
+		"BC E3	5F1D",
+		"BC E4	5F21",
+		"BC E5	5F22",
+		"BC E6	5F23",
+		"BC E7	5F24",
+		"BC E8	5F28",
+		"BC E9	5F2B",
+		"BC EA	5F2C",
+		"BC EB	5F2E",
+		"BC EC	5F30",
+		"BC ED	5F34",
+		"BC EE	5F36",
+		"BC EF	5F3B",
+		"BC F0	5F3D",
+		"BC F1	5F3F",
+		"BC F2	5F40",
+		"BC F3	5F44",
+		"BC F4	5F45",
+		"BC F5	5F47",
+		"BC F6	5F4D",
+		"BC F7	5F50",
+		"BC F8	5F54",
+		"BC F9	5F58",
+		"BC FA	5F5B",
+		"BC FB	5F60",
+		"BC FC	5F63",
+		"BC FD	5F64",
+		"BC FE	5F67",
+		"BD A1	5F6F",
+		"BD A2	5F72",
+		"BD A3	5F74",
+		"BD A4	5F75",
+		"BD A5	5F78",
+		"BD A6	5F7A",
+		"BD A7	5F7D",
+		"BD A8	5F7E",
+		"BD A9	5F89",
+		"BD AA	5F8D",
+		"BD AB	5F8F",
+		"BD AC	5F96",
+		"BD AD	5F9C",
+		"BD AE	5F9D",
+		"BD AF	5FA2",
+		"BD B2	5FA4",
+		"BD B0	5FA7",
+		"BD B1	5FAB",
+		"BD B3	5FAC",
+		"BD B4	5FAF",
+		"BD B5	5FB0",
+		"BD B6	5FB1",
+		"BD B7	5FB8",
+		"BD B8	5FC4",
+		"BD B9	5FC7",
+		"BD BA	5FC8",
+		"BD BB	5FC9",
+		"BD BC	5FCB",
+		"BD BD	5FD0",
+		"BD BE	5FD1",
+		"BD BF	5FD2",
+		"BD C0	5FD3",
+		"BD C1	5FD4",
+		"BD C2	5FDE",
+		"BD C3	5FE1",
+		"BD C4	5FE2",
+		"BD C5	5FE8",
+		"BD C6	5FE9",
+		"BD C7	5FEA",
+		"BD C8	5FEC",
+		"BD C9	5FED",
+		"BD CA	5FEE",
+		"BD CB	5FEF",
+		"BD CC	5FF2",
+		"BD CD	5FF3",
+		"BD CE	5FF6",
+		"BD CF	5FFA",
+		"BD D0	5FFC",
+		"BD D1	6007",
+		"BD D2	600A",
+		"BD D3	600D",
+		"BD D4	6013",
+		"BD D5	6014",
+		"BD D6	6017",
+		"BD D7	6018",
+		"BD D8	601A",
+		"BD D9	601F",
+		"BD DA	6024",
+		"BD DB	602D",
+		"BD DC	6033",
+		"BD DD	6035",
+		"BD DE	6040",
+		"BD DF	6047",
+		"BD E0	6048",
+		"BD E1	6049",
+		"BD E2	604C",
+		"BD E3	6051",
+		"BD E4	6054",
+		"BD E5	6056",
+		"BD E6	6057",
+		"BD E7	605D",
+		"BD E8	6061",
+		"BD E9	6067",
+		"BD EA	6071",
+		"BD EB	607E",
+		"BD EC	607F",
+		"BD ED	6082",
+		"BD EE	6086",
+		"BD EF	6088",
+		"BD F0	608A",
+		"BD F1	608E",
+		"BD F2	6091",
+		"BD F3	6093",
+		"BD F4	6095",
+		"BD F5	6098",
+		"BD F6	609D",
+		"BD F7	609E",
+		"BD F8	60A2",
+		"BD F9	60A4",
+		"BD FA	60A5",
+		"BD FB	60A8",
+		"BD FC	60B0",
+		"BD FD	60B1",
+		"BD FE	60B7",
+		"BE A1	60BB",
+		"BE A2	60BE",
+		"BE A3	60C2",
+		"BE A4	60C4",
+		"BE A5	60C8",
+		"BE A6	60C9",
+		"BE A7	60CA",
+		"BE A8	60CB",
+		"BE A9	60CE",
+		"BE AA	60CF",
+		"BE AB	60D4",
+		"BE AC	60D5",
+		"BE AD	60D9",
+		"BE AE	60DB",
+		"BE AF	60DD",
+		"BE B0	60DE",
+		"BE B1	60E2",
+		"BE B2	60E5",
+		"BE B3	60F2",
+		"BE B4	60F5",
+		"BE B5	60F8",
+		"BE B6	60FC",
+		"BE B7	60FD",
+		"BE B8	6102",
+		"BE B9	6107",
+		"BE BA	610A",
+		"BE BB	610C",
+		"BE BC	6110",
+		"BE BD	6111",
+		"BE BE	6112",
+		"BE BF	6113",
+		"BE C0	6114",
+		"BE C1	6116",
+		"BE C2	6117",
+		"BE C3	6119",
+		"BE C4	611C",
+		"BE C5	611E",
+		"BE C6	6122",
+		"BE C7	612A",
+		"BE C8	612B",
+		"BE C9	6130",
+		"BE CA	6131",
+		"BE CB	6135",
+		"BE CC	6136",
+		"BE CD	6137",
+		"BE CE	6139",
+		"BE CF	6141",
+		"BE D0	6145",
+		"BE D1	6146",
+		"BE D2	6149",
+		"BE D3	615E",
+		"BE D4	6160",
+		"BE D5	616C",
+		"BE D6	6172",
+		"BE D7	6178",
+		"BE D8	617B",
+		"BE D9	617C",
+		"BE DA	617F",
+		"BE DB	6180",
+		"BE DC	6181",
+		"BE DD	6183",
+		"BE DE	6184",
+		"BE DF	618B",
+		"BE E0	618D",
+		"BE E1	6192",
+		"BE E2	6193",
+		"BE E3	6197",
+		"BE E4	6198",
+		"BE E5	619C",
+		"BE E6	619D",
+		"BE E7	619F",
+		"BE E8	61A0",
+		"BE E9	61A5",
+		"BE EA	61A8",
+		"BE EB	61AA",
+		"BE EC	61AD",
+		"BE ED	61B8",
+		"BE EE	61B9",
+		"BE EF	61BC",
+		"BE F0	61C0",
+		"BE F1	61C1",
+		"BE F2	61C2",
+		"BE F3	61CE",
+		"BE F4	61CF",
+		"BE F5	61D5",
+		"BE F6	61DC",
+		"BE F7	61DD",
+		"BE F8	61DE",
+		"BE F9	61DF",
+		"BE FA	61E1",
+		"BE FB	61E2",
+		"BE FE	61E5",
+		"BE FC	61E7",
+		"BE FD	61E9",
+		"BF A1	61EC",
+		"BF A2	61ED",
+		"BF A3	61EF",
+		"BF A4	6201",
+		"BF A5	6203",
+		"BF A6	6204",
+		"BF A7	6207",
+		"BF A8	6213",
+		"BF A9	6215",
+		"BF AA	621C",
+		"BF AB	6220",
+		"BF AC	6222",
+		"BF AD	6223",
+		"BF AE	6227",
+		"BF AF	6229",
+		"BF B0	622B",
+		"BF B1	6239",
+		"BF B2	623D",
+		"BF B3	6242",
+		"BF B4	6243",
+		"BF B5	6244",
+		"BF B6	6246",
+		"BF B7	624C",
+		"BF B8	6250",
+		"BF B9	6251",
+		"BF BA	6252",
+		"BF BB	6254",
+		"BF BC	6256",
+		"BF BD	625A",
+		"BF BE	625C",
+		"BF BF	6264",
+		"BF C0	626D",
+		"BF C1	626F",
+		"BF C2	6273",
+		"BF C3	627A",
+		"BF C4	627D",
+		"BF C5	628D",
+		"BF C6	628E",
+		"BF C7	628F",
+		"BF C8	6290",
+		"BF C9	62A6",
+		"BF CA	62A8",
+		"BF CB	62B3",
+		"BF CC	62B6",
+		"BF CD	62B7",
+		"BF CE	62BA",
+		"BF CF	62BE",
+		"BF D0	62BF",
+		"BF D1	62C4",
+		"BF D2	62CE",
+		"BF D3	62D5",
+		"BF D4	62D6",
+		"BF D5	62DA",
+		"BF D6	62EA",
+		"BF D7	62F2",
+		"BF D8	62F4",
+		"BF D9	62FC",
+		"BF DA	62FD",
+		"BF DB	6303",
+		"BF DC	6304",
+		"BF DD	630A",
+		"BF DE	630B",
+		"BF DF	630D",
+		"BF E0	6310",
+		"BF E1	6313",
+		"BF E2	6316",
+		"BF E3	6318",
+		"BF E4	6329",
+		"BF E5	632A",
+		"BF E6	632D",
+		"BF E7	6335",
+		"BF E8	6336",
+		"BF E9	6339",
+		"BF EA	633C",
+		"BF EB	6341",
+		"BF EC	6342",
+		"BF ED	6343",
+		"BF EE	6344",
+		"BF EF	6346",
+		"BF F0	634A",
+		"BF F1	634B",
+		"BF F2	634E",
+		"BF F3	6352",
+		"BF F4	6353",
+		"BF F5	6354",
+		"BF F6	6358",
+		"BF F7	635B",
+		"BF F8	6365",
+		"BF F9	6366",
+		"BF FA	636C",
+		"BF FB	636D",
+		"BF FC	6371",
+		"BF FD	6374",
+		"BF FE	6375",
+		"C0 A1	6378",
+		"C0 A2	637C",
+		"C0 A3	637D",
+		"C0 A4	637F",
+		"C0 A5	6382",
+		"C0 A6	6384",
+		"C0 A7	6387",
+		"C0 A8	638A",
+		"C0 A9	6390",
+		"C0 AA	6394",
+		"C0 AB	6395",
+		"C0 AC	6399",
+		"C0 AD	639A",
+		"C0 AE	639E",
+		"C0 AF	63A4",
+		"C0 B0	63A6",
+		"C0 B1	63AD",
+		"C0 B2	63AE",
+		"C0 B3	63AF",
+		"C0 B4	63BD",
+		"C0 B5	63C1",
+		"C0 B6	63C5",
+		"C0 B7	63C8",
+		"C0 B8	63CE",
+		"C0 B9	63D1",
+		"C0 BA	63D3",
+		"C0 BB	63D4",
+		"C0 BC	63D5",
+		"C0 BD	63DC",
+		"C0 BE	63E0",
+		"C0 BF	63E5",
+		"C0 C0	63EA",
+		"C0 C1	63EC",
+		"C0 C2	63F2",
+		"C0 C3	63F3",
+		"C0 C4	63F5",
+		"C0 C5	63F8",
+		"C0 C6	63F9",
+		"C0 C7	6409",
+		"C0 C8	640A",
+		"C0 C9	6410",
+		"C0 CA	6412",
+		"C0 CB	6414",
+		"C0 CC	6418",
+		"C0 CD	641E",
+		"C0 CE	6420",
+		"C0 CF	6422",
+		"C0 D0	6424",
+		"C0 D1	6425",
+		"C0 D2	6429",
+		"C0 D3	642A",
+		"C0 D4	642F",
+		"C0 D5	6430",
+		"C0 D6	6435",
+		"C0 D7	643D",
+		"C0 D8	643F",
+		"C0 D9	644B",
+		"C0 DA	644F",
+		"C0 DB	6451",
+		"C0 DC	6452",
+		"C0 DD	6453",
+		"C0 DE	6454",
+		"C0 DF	645A",
+		"C0 E0	645B",
+		"C0 E1	645C",
+		"C0 E2	645D",
+		"C0 E3	645F",
+		"C0 E4	6460",
+		"C0 E5	6461",
+		"C0 E6	6463",
+		"C0 E7	646D",
+		"C0 E8	6473",
+		"C0 E9	6474",
+		"C0 EA	647B",
+		"C0 EB	647D",
+		"C0 EC	6485",
+		"C0 ED	6487",
+		"C0 EE	648F",
+		"C0 EF	6490",
+		"C0 F0	6491",
+		"C0 F1	6498",
+		"C0 F2	6499",
+		"C0 F3	649B",
+		"C0 F4	649D",
+		"C0 F5	649F",
+		"C0 F6	64A1",
+		"C0 F7	64A3",
+		"C0 F8	64A6",
+		"C0 F9	64A8",
+		"C0 FA	64AC",
+		"C0 FB	64B3",
+		"C0 FC	64BD",
+		"C0 FD	64BE",
+		"C0 FE	64BF",
+		"C1 A1	64C4",
+		"C1 A2	64C9",
+		"C1 A3	64CA",
+		"C1 A4	64CB",
+		"C1 A5	64CC",
+		"C1 A6	64CE",
+		"C1 A7	64D0",
+		"C1 A8	64D1",
+		"C1 A9	64D5",
+		"C1 AA	64D7",
+		"C1 AB	64E4",
+		"C1 AC	64E5",
+		"C1 AD	64E9",
+		"C1 AE	64EA",
+		"C1 AF	64ED",
+		"C1 B0	64F0",
+		"C1 B1	64F5",
+		"C1 B2	64F7",
+		"C1 B3	64FB",
+		"C1 B4	64FF",
+		"C1 B5	6501",
+		"C1 B6	6504",
+		"C1 B7	6508",
+		"C1 B8	6509",
+		"C1 B9	650A",
+		"C1 BA	650F",
+		"C1 BB	6513",
+		"C1 BC	6514",
+		"C1 BD	6516",
+		"C1 BE	6519",
+		"C1 BF	651B",
+		"C1 C0	651E",
+		"C1 C1	651F",
+		"C1 C2	6522",
+		"C1 C3	6526",
+		"C1 C4	6529",
+		"C1 C5	652E",
+		"C1 C6	6531",
+		"C1 C7	653A",
+		"C1 C8	653C",
+		"C1 C9	653D",
+		"C1 CA	6543",
+		"C1 CB	6547",
+		"C1 CC	6549",
+		"C1 CD	6550",
+		"C1 CE	6552",
+		"C1 CF	6554",
+		"C1 D0	655F",
+		"C1 D1	6560",
+		"C1 D2	6567",
+		"C1 D3	656B",
+		"C1 D4	657A",
+		"C1 D5	657D",
+		"C1 D6	6581",
+		"C1 D7	6585",
+		"C1 D8	658A",
+		"C1 D9	6592",
+		"C1 DA	6595",
+		"C1 DB	6598",
+		"C1 DC	659D",
+		"C1 DD	65A0",
+		"C1 DE	65A3",
+		"C1 DF	65A6",
+		"C1 E0	65AE",
+		"C1 E1	65B2",
+		"C1 E2	65B3",
+		"C1 E3	65B4",
+		"C1 E4	65BF",
+		"C1 E5	65C2",
+		"C1 E6	65C8",
+		"C1 E7	65C9",
+		"C1 E8	65CE",
+		"C1 E9	65D0",
+		"C1 EA	65D4",
+		"C1 EB	65D6",
+		"C1 EC	65D8",
+		"C1 ED	65DF",
+		"C1 EE	65F0",
+		"C1 EF	65F2",
+		"C1 F0	65F4",
+		"C1 F1	65F5",
+		"C1 F2	65F9",
+		"C1 F3	65FE",
+		"C1 F4	65FF",
+		"C1 F5	6600",
+		"C1 F6	6604",
+		"C1 F7	6608",
+		"C1 F8	6609",
+		"C1 F9	660D",
+		"C1 FA	6611",
+		"C1 FB	6612",
+		"C1 FC	6615",
+		"C1 FD	6616",
+		"C1 FE	661D",
+		"C2 A1	661E",
+		"C2 A2	6621",
+		"C2 A3	6622",
+		"C2 A4	6623",
+		"C2 A5	6624",
+		"C2 A6	6626",
+		"C2 A7	6629",
+		"C2 A8	662A",
+		"C2 A9	662B",
+		"C2 AA	662C",
+		"C2 AB	662E",
+		"C2 AC	6630",
+		"C2 AD	6631",
+		"C2 AE	6633",
+		"C2 B0	6637",
+		"C2 AF	6639",
+		"C2 B1	6640",
+		"C2 B2	6645",
+		"C2 B3	6646",
+		"C2 B4	664A",
+		"C2 B5	664C",
+		"C2 B7	664E",
+		"C2 B6	6651",
+		"C2 B8	6657",
+		"C2 B9	6658",
+		"C2 BA	6659",
+		"C2 BB	665B",
+		"C2 BC	665C",
+		"C2 BD	6660",
+		"C2 BE	6661",
+		"C2 C0	666A",
+		"C2 C1	666B",
+		"C2 C2	666C",
+		"C2 C4	6673",
+		"C2 C5	6675",
+		"C2 C7	6677",
+		"C2 C8	6678",
+		"C2 C9	6679",
+		"C2 CA	667B",
+		"C2 CC	667C",
+		"C2 C3	667E",
+		"C2 C6	667F",
+		"C2 CB	6680",
+		"C2 CD	668B",
+		"C2 CE	668C",
+		"C2 CF	668D",
+		"C2 D0	6690",
+		"C2 D1	6692",
+		"C2 D2	6699",
+		"C2 D3	669A",
+		"C2 D4	669B",
+		"C2 D5	669C",
+		"C2 D6	669F",
+		"C2 D7	66A0",
+		"C2 D8	66A4",
+		"C2 D9	66AD",
+		"C2 DA	66B1",
+		"C2 DB	66B2",
+		"C2 DC	66B5",
+		"C2 DD	66BB",
+		"C2 DE	66BF",
+		"C2 DF	66C0",
+		"C2 E0	66C2",
+		"C2 E1	66C3",
+		"C2 E2	66C8",
+		"C2 E3	66CC",
+		"C2 E4	66CE",
+		"C2 E5	66CF",
+		"C2 E6	66D4",
+		"C2 E7	66DB",
+		"C2 E8	66DF",
+		"C2 E9	66E8",
+		"C2 EA	66EB",
+		"C2 EB	66EC",
+		"C2 EC	66EE",
+		"C2 ED	66FA",
+		"C2 BF	66FB",
+		"C2 EE	6705",
+		"C2 EF	6707",
+		"C2 F0	670E",
+		"C2 F1	6713",
+		"C2 F2	6719",
+		"C2 F3	671C",
+		"C2 F4	6720",
+		"C2 F5	6722",
+		"C2 F6	6733",
+		"C2 F7	673E",
+		"C2 F8	6745",
+		"C2 F9	6747",
+		"C2 FA	6748",
+		"C2 FB	674C",
+		"C2 FC	6754",
+		"C2 FD	6755",
+		"C2 FE	675D",
+		"C3 A1	6766",
+		"C3 A2	676C",
+		"C3 A3	676E",
+		"C3 A4	6774",
+		"C3 A5	6776",
+		"C3 A6	677B",
+		"C3 A7	6781",
+		"C3 A8	6784",
+		"C3 A9	678E",
+		"C3 AA	678F",
+		"C3 AB	6791",
+		"C3 AC	6793",
+		"C3 AD	6796",
+		"C3 AE	6798",
+		"C3 AF	6799",
+		"C3 B0	679B",
+		"C3 B1	67B0",
+		"C3 B2	67B1",
+		"C3 B3	67B2",
+		"C3 B4	67B5",
+		"C3 B5	67BB",
+		"C3 B6	67BC",
+		"C3 B7	67BD",
+		"C3 B9	67C0",
+		"C3 BA	67C2",
+		"C3 BB	67C3",
+		"C3 BC	67C5",
+		"C3 BD	67C8",
+		"C3 BE	67C9",
+		"C3 BF	67D2",
+		"C3 C0	67D7",
+		"C3 C1	67D9",
+		"C3 C2	67DC",
+		"C3 C3	67E1",
+		"C3 C4	67E6",
+		"C3 C5	67F0",
+		"C3 C6	67F2",
+		"C3 C7	67F6",
+		"C3 C8	67F7",
+		"C3 B8	67F9",
+		"C3 CA	6814",
+		"C3 CB	6819",
+		"C3 CC	681D",
+		"C3 CD	681F",
+		"C3 CF	6827",
+		"C3 CE	6828",
+		"C3 D0	682C",
+		"C3 D1	682D",
+		"C3 D2	682F",
+		"C3 D3	6830",
+		"C3 D4	6831",
+		"C3 D5	6833",
+		"C3 D6	683B",
+		"C3 D7	683F",
+		"C3 D8	6844",
+		"C3 D9	6845",
+		"C3 DA	684A",
+		"C3 DB	684C",
+		"C3 C9	6852",
+		"C3 DC	6855",
+		"C3 DD	6857",
+		"C3 DE	6858",
+		"C3 DF	685B",
+		"C3 E0	686B",
+		"C3 E1	686E",
+		"C3 E2	686F",
+		"C3 E3	6870",
+		"C3 E4	6871",
+		"C3 E5	6872",
+		"C3 E6	6875",
+		"C3 E7	6879",
+		"C3 E8	687A",
+		"C3 E9	687B",
+		"C3 EA	687C",
+		"C3 EB	6882",
+		"C3 EC	6884",
+		"C3 ED	6886",
+		"C3 EE	6888",
+		"C3 EF	6896",
+		"C3 F0	6898",
+		"C3 F1	689A",
+		"C3 F2	689C",
+		"C3 F3	68A1",
+		"C3 F4	68A3",
+		"C3 F5	68A5",
+		"C3 F6	68A9",
+		"C3 F7	68AA",
+		"C3 F8	68AE",
+		"C3 F9	68B2",
+		"C3 FA	68BB",
+		"C3 FB	68C5",
+		"C3 FC	68C8",
+		"C3 FD	68CC",
+		"C3 FE	68CF",
+		"C4 A1	68D0",
+		"C4 A2	68D1",
+		"C4 A3	68D3",
+		"C4 A4	68D6",
+		"C4 A5	68D9",
+		"C4 A6	68DC",
+		"C4 A7	68DD",
+		"C4 A8	68E5",
+		"C4 A9	68E8",
+		"C4 AA	68EA",
+		"C4 AB	68EB",
+		"C4 AC	68EC",
+		"C4 AD	68ED",
+		"C4 AE	68F0",
+		"C4 AF	68F1",
+		"C4 B0	68F5",
+		"C4 B1	68F6",
+		"C4 B2	68FB",
+		"C4 B3	68FC",
+		"C4 B4	68FD",
+		"C4 B5	6906",
+		"C4 B6	6909",
+		"C4 B7	690A",
+		"C4 B8	6910",
+		"C4 B9	6911",
+		"C4 BA	6913",
+		"C4 BB	6916",
+		"C4 BC	6917",
+		"C4 BD	6931",
+		"C4 BE	6933",
+		"C4 BF	6935",
+		"C4 C0	6938",
+		"C4 C1	693B",
+		"C4 C2	6942",
+		"C4 C3	6945",
+		"C4 C4	6949",
+		"C4 C5	694E",
+		"C4 C6	6957",
+		"C4 C7	695B",
+		"C4 C8	6963",
+		"C4 C9	6964",
+		"C4 CA	6965",
+		"C4 CB	6966",
+		"C4 CC	6968",
+		"C4 CD	6969",
+		"C4 CE	696C",
+		"C4 CF	6970",
+		"C4 D0	6971",
+		"C4 D1	6972",
+		"C4 D2	697A",
+		"C4 D3	697B",
+		"C4 D4	697F",
+		"C4 D5	6980",
+		"C4 D6	698D",
+		"C4 D7	6992",
+		"C4 D8	6996",
+		"C4 D9	6998",
+		"C4 DA	69A1",
+		"C4 DB	69A5",
+		"C4 DC	69A6",
+		"C4 DD	69A8",
+		"C4 DE	69AB",
+		"C4 DF	69AD",
+		"C4 E0	69AF",
+		"C4 E1	69B7",
+		"C4 E2	69B8",
+		"C4 E3	69BA",
+		"C4 E4	69BC",
+		"C4 E5	69C5",
+		"C4 E6	69C8",
+		"C4 E7	69D1",
+		"C4 E8	69D6",
+		"C4 E9	69D7",
+		"C4 EA	69E2",
+		"C4 EB	69E5",
+		"C4 EC	69EE",
+		"C4 ED	69EF",
+		"C4 EE	69F1",
+		"C4 EF	69F3",
+		"C4 F0	69F5",
+		"C4 F1	69FE",
+		"C4 F2	6A00",
+		"C4 F3	6A01",
+		"C4 F4	6A03",
+		"C4 F5	6A0F",
+		"C4 F6	6A11",
+		"C4 F7	6A15",
+		"C4 F8	6A1A",
+		"C4 F9	6A1D",
+		"C4 FA	6A20",
+		"C4 FB	6A24",
+		"C4 FC	6A28",
+		"C4 FD	6A30",
+		"C4 FE	6A32",
+		"C5 A1	6A34",
+		"C5 A2	6A37",
+		"C5 A3	6A3B",
+		"C5 A4	6A3E",
+		"C5 A5	6A3F",
+		"C5 A6	6A45",
+		"C5 A7	6A46",
+		"C5 A8	6A49",
+		"C5 A9	6A4A",
+		"C5 AA	6A4E",
+		"C5 AB	6A50",
+		"C5 AC	6A51",
+		"C5 AD	6A52",
+		"C5 AE	6A55",
+		"C5 AF	6A56",
+		"C5 B0	6A5B",
+		"C5 B1	6A64",
+		"C5 B2	6A67",
+		"C5 B3	6A6A",
+		"C5 B4	6A71",
+		"C5 B5	6A73",
+		"C5 B6	6A7E",
+		"C5 B7	6A81",
+		"C5 B8	6A83",
+		"C5 B9	6A86",
+		"C5 BA	6A87",
+		"C5 BB	6A89",
+		"C5 BC	6A8B",
+		"C5 BD	6A91",
+		"C5 BE	6A9B",
+		"C5 BF	6A9D",
+		"C5 C0	6A9E",
+		"C5 C1	6A9F",
+		"C5 C2	6AA5",
+		"C5 C3	6AAB",
+		"C5 C4	6AAF",
+		"C5 C5	6AB0",
+		"C5 C6	6AB1",
+		"C5 C7	6AB4",
+		"C5 C8	6ABD",
+		"C5 C9	6ABE",
+		"C5 CA	6ABF",
+		"C5 CB	6AC6",
+		"C5 CD	6AC8",
+		"C5 CC	6AC9",
+		"C5 CE	6ACC",
+		"C5 CF	6AD0",
+		"C5 D0	6AD4",
+		"C5 D1	6AD5",
+		"C5 D2	6AD6",
+		"C5 D3	6ADC",
+		"C5 D4	6ADD",
+		"C5 D5	6AE4",
+		"C5 D6	6AE7",
+		"C5 D7	6AEC",
+		"C5 D8	6AF0",
+		"C5 D9	6AF1",
+		"C5 DA	6AF2",
+		"C5 DB	6AFC",
+		"C5 DC	6AFD",
+		"C5 DD	6B02",
+		"C5 DE	6B03",
+		"C5 DF	6B06",
+		"C5 E0	6B07",
+		"C5 E1	6B09",
+		"C5 E2	6B0F",
+		"C5 E3	6B10",
+		"C5 E4	6B11",
+		"C5 E5	6B17",
+		"C5 E6	6B1B",
+		"C5 E7	6B1E",
+		"C5 E8	6B24",
+		"C5 E9	6B28",
+		"C5 EA	6B2B",
+		"C5 EB	6B2C",
+		"C5 EC	6B2F",
+		"C5 ED	6B35",
+		"C5 EE	6B36",
+		"C5 EF	6B3B",
+		"C5 F0	6B3F",
+		"C5 F1	6B46",
+		"C5 F2	6B4A",
+		"C5 F3	6B4D",
+		"C5 F4	6B52",
+		"C5 F5	6B56",
+		"C5 F6	6B58",
+		"C5 F7	6B5D",
+		"C5 F8	6B60",
+		"C5 F9	6B67",
+		"C5 FA	6B6B",
+		"C5 FB	6B6E",
+		"C5 FC	6B70",
+		"C5 FD	6B75",
+		"C5 FE	6B7D",
+		"C6 A1	6B7E",
+		"C6 A2	6B82",
+		"C6 A3	6B85",
+		"C6 A4	6B97",
+		"C6 A5	6B9B",
+		"C6 A6	6B9F",
+		"C6 A7	6BA0",
+		"C6 A8	6BA2",
+		"C6 A9	6BA3",
+		"C6 AA	6BA8",
+		"C6 AB	6BA9",
+		"C6 AC	6BAC",
+		"C6 AD	6BAD",
+		"C6 AE	6BAE",
+		"C6 AF	6BB0",
+		"C6 B0	6BB8",
+		"C6 B1	6BB9",
+		"C6 B2	6BBD",
+		"C6 B3	6BBE",
+		"C6 B4	6BC3",
+		"C6 B5	6BC4",
+		"C6 B6	6BC9",
+		"C6 B7	6BCC",
+		"C6 B8	6BD6",
+		"C6 B9	6BDA",
+		"C6 BA	6BE1",
+		"C6 BB	6BE3",
+		"C6 BC	6BE6",
+		"C6 BD	6BE7",
+		"C6 BE	6BEE",
+		"C6 BF	6BF1",
+		"C6 C0	6BF7",
+		"C6 C1	6BF9",
+		"C6 C2	6BFF",
+		"C6 C3	6C02",
+		"C6 C4	6C04",
+		"C6 C5	6C05",
+		"C6 C6	6C09",
+		"C6 C7	6C0D",
+		"C6 C8	6C0E",
+		"C6 C9	6C10",
+		"C6 CA	6C12",
+		"C6 CB	6C19",
+		"C6 CC	6C1F",
+		"C6 CD	6C26",
+		"C6 CE	6C27",
+		"C6 CF	6C28",
+		"C6 D0	6C2C",
+		"C6 D1	6C2E",
+		"C6 D2	6C33",
+		"C6 D3	6C35",
+		"C6 D4	6C36",
+		"C6 D5	6C3A",
+		"C6 D6	6C3B",
+		"C6 D7	6C3F",
+		"C6 D8	6C4A",
+		"C6 D9	6C4B",
+		"C6 DA	6C4D",
+		"C6 DB	6C4F",
+		"C6 DC	6C52",
+		"C6 DD	6C54",
+		"C6 DE	6C59",
+		"C6 DF	6C5B",
+		"C6 E0	6C5C",
+		"C7 B6	6C67",
+		"C6 E1	6C6B",
+		"C6 E2	6C6D",
+		"C6 E3	6C6F",
+		"C6 E4	6C74",
+		"C6 E5	6C76",
+		"C6 E6	6C78",
+		"C6 E7	6C79",
+		"C6 E8	6C7B",
+		"C6 E9	6C85",
+		"C6 EA	6C86",
+		"C6 EB	6C87",
+		"C6 EC	6C89",
+		"C6 ED	6C94",
+		"C6 EE	6C95",
+		"C6 EF	6C97",
+		"C6 F0	6C98",
+		"C6 F1	6C9C",
+		"C6 F2	6C9F",
+		"C6 F3	6CB0",
+		"C6 F4	6CB2",
+		"C6 F5	6CB4",
+		"C6 F6	6CC2",
+		"C6 F7	6CC6",
+		"C6 F8	6CCD",
+		"C6 F9	6CCF",
+		"C6 FA	6CD0",
+		"C6 FB	6CD1",
+		"C6 FC	6CD2",
+		"C6 FD	6CD4",
+		"C6 FE	6CD6",
+		"C7 A1	6CDA",
+		"C7 A2	6CDC",
+		"C7 A3	6CE0",
+		"C7 A4	6CE7",
+		"C7 A5	6CE9",
+		"C7 A6	6CEB",
+		"C7 A7	6CEC",
+		"C7 A8	6CEE",
+		"C7 A9	6CF2",
+		"C7 AA	6CF4",
+		"C7 AB	6D04",
+		"C7 AC	6D07",
+		"C7 AD	6D0A",
+		"C7 AE	6D0E",
+		"C7 AF	6D0F",
+		"C7 B0	6D11",
+		"C7 B1	6D13",
+		"C7 B2	6D1A",
+		"C7 B3	6D26",
+		"C7 B4	6D27",
+		"C7 B5	6D28",
+		"C7 B7	6D2E",
+		"C7 B8	6D2F",
+		"C7 B9	6D31",
+		"C7 BA	6D39",
+		"C7 BB	6D3C",
+		"C7 BC	6D3F",
+		"C7 BD	6D57",
+		"C7 BE	6D5E",
+		"C7 BF	6D5F",
+		"C7 C0	6D61",
+		"C7 C1	6D65",
+		"C7 C2	6D67",
+		"C7 C3	6D6F",
+		"C7 C4	6D70",
+		"C7 C5	6D7C",
+		"C7 C6	6D82",
+		"C7 C7	6D87",
+		"C7 C8	6D91",
+		"C7 C9	6D92",
+		"C7 CA	6D94",
+		"C7 CB	6D96",
+		"C7 CC	6D97",
+		"C7 CD	6D98",
+		"C7 CE	6DAA",
+		"C7 CF	6DAC",
+		"C7 D0	6DB4",
+		"C7 D1	6DB7",
+		"C7 D2	6DB9",
+		"C7 D3	6DBD",
+		"C7 D4	6DBF",
+		"C7 D5	6DC4",
+		"C7 D6	6DC8",
+		"C7 D7	6DCA",
+		"C7 D8	6DCE",
+		"C7 D9	6DCF",
+		"C7 DA	6DD6",
+		"C7 DB	6DDB",
+		"C7 DC	6DDD",
+		"C7 DD	6DDF",
+		"C7 DE	6DE0",
+		"C7 DF	6DE2",
+		"C7 E0	6DE5",
+		"C7 E1	6DE9",
+		"C7 E2	6DEF",
+		"C7 E3	6DF0",
+		"C7 E4	6DF4",
+		"C7 E5	6DF6",
+		"C7 E6	6DFC",
+		"C7 E7	6E00",
+		"C7 E8	6E04",
+		"C7 E9	6E1E",
+		"C7 EA	6E22",
+		"C7 EB	6E27",
+		"C7 EC	6E32",
+		"C7 ED	6E36",
+		"C7 EE	6E39",
+		"C7 EF	6E3B",
+		"C7 F0	6E3C",
+		"C7 F1	6E44",
+		"C7 F2	6E45",
+		"C7 F3	6E48",
+		"C7 F4	6E49",
+		"C7 F5	6E4B",
+		"C7 F6	6E4F",
+		"C7 F7	6E51",
+		"C7 F8	6E52",
+		"C7 F9	6E53",
+		"C7 FA	6E54",
+		"C7 FB	6E57",
+		"C7 FC	6E5C",
+		"C7 FD	6E5D",
+		"C7 FE	6E5E",
+		"C8 A1	6E62",
+		"C8 A2	6E63",
+		"C8 A3	6E68",
+		"C8 A4	6E73",
+		"C8 A5	6E7B",
+		"C8 A6	6E7D",
+		"C8 A7	6E8D",
+		"C8 A8	6E93",
+		"C8 A9	6E99",
+		"C8 AA	6EA0",
+		"C8 AB	6EA7",
+		"C8 AC	6EAD",
+		"C8 AD	6EAE",
+		"C8 AE	6EB1",
+		"C8 AF	6EB3",
+		"C8 B0	6EBB",
+		"C8 B1	6EBF",
+		"C8 B2	6EC0",
+		"C8 B3	6EC1",
+		"C8 B4	6EC3",
+		"C8 B5	6EC7",
+		"C8 B6	6EC8",
+		"C8 B7	6ECA",
+		"C8 B8	6ECD",
+		"C8 B9	6ECE",
+		"C8 BA	6ECF",
+		"C8 BB	6EEB",
+		"C8 BC	6EED",
+		"C8 BD	6EEE",
+		"C8 BE	6EF9",
+		"C8 BF	6EFB",
+		"C8 C0	6EFD",
+		"C8 C1	6F04",
+		"C8 C2	6F08",
+		"C8 C3	6F0A",
+		"C8 C4	6F0C",
+		"C8 C5	6F0D",
+		"C8 C6	6F16",
+		"C8 C7	6F18",
+		"C8 C8	6F1A",
+		"C8 C9	6F1B",
+		"C8 CA	6F26",
+		"C8 CB	6F29",
+		"C8 CC	6F2A",
+		"C8 D3	6F2D",
+		"C8 CD	6F2F",
+		"C8 CE	6F30",
+		"C8 CF	6F33",
+		"C8 D0	6F36",
+		"C8 D1	6F3B",
+		"C8 D2	6F3C",
+		"C8 D4	6F4F",
+		"C8 D5	6F51",
+		"C8 D6	6F52",
+		"C8 D7	6F53",
+		"C8 D8	6F57",
+		"C8 D9	6F59",
+		"C8 DA	6F5A",
+		"C8 DB	6F5D",
+		"C8 DC	6F5E",
+		"C8 DD	6F61",
+		"C8 DE	6F62",
+		"C8 DF	6F68",
+		"C8 E0	6F6C",
+		"C8 E1	6F7D",
+		"C8 E2	6F7E",
+		"C8 E3	6F83",
+		"C8 E4	6F87",
+		"C8 E5	6F88",
+		"C8 E6	6F8B",
+		"C8 E7	6F8C",
+		"C8 E8	6F8D",
+		"C8 E9	6F90",
+		"C8 EA	6F92",
+		"C8 EB	6F93",
+		"C8 EC	6F94",
+		"C8 ED	6F96",
+		"C8 EE	6F9A",
+		"C8 EF	6F9F",
+		"C8 F0	6FA0",
+		"C8 F1	6FA5",
+		"C8 F2	6FA6",
+		"C8 F3	6FA7",
+		"C8 F4	6FA8",
+		"C8 F5	6FAE",
+		"C8 F6	6FAF",
+		"C8 F7	6FB0",
+		"C8 F8	6FB5",
+		"C8 F9	6FB6",
+		"C8 FA	6FBC",
+		"C8 FB	6FC5",
+		"C8 FC	6FC7",
+		"C8 FD	6FC8",
+		"C8 FE	6FCA",
+		"C9 A1	6FDA",
+		"C9 A2	6FDE",
+		"C9 A3	6FE8",
+		"C9 A4	6FE9",
+		"C9 A5	6FF0",
+		"C9 A6	6FF5",
+		"C9 A7	6FF9",
+		"C9 A8	6FFC",
+		"C9 A9	6FFD",
+		"C9 AA	7000",
+		"C9 AB	7005",
+		"C9 AC	7006",
+		"C9 AD	7007",
+		"C9 AE	700D",
+		"C9 AF	7017",
+		"C9 B0	7020",
+		"C9 B1	7023",
+		"C9 B2	702F",
+		"C9 B3	7034",
+		"C9 B4	7037",
+		"C9 B5	7039",
+		"C9 B6	703C",
+		"C9 B7	7043",
+		"C9 B8	7044",
+		"C9 B9	7048",
+		"C9 BA	7049",
+		"C9 BB	704A",
+		"C9 BC	704B",
+		"C9 C1	704E",
+		"C9 BD	7054",
+		"C9 BE	7055",
+		"C9 BF	705D",
+		"C9 C0	705E",
+		"C9 C2	7064",
+		"C9 C3	7065",
+		"C9 C4	706C",
+		"C9 C5	706E",
+		"C9 C6	7075",
+		"C9 C7	7076",
+		"C9 C8	707E",
+		"C9 C9	7081",
+		"C9 CA	7085",
+		"C9 CB	7086",
+		"C9 CC	7094",
+		"C9 CD	7095",
+		"C9 CE	7096",
+		"C9 CF	7097",
+		"C9 D0	7098",
+		"C9 D1	709B",
+		"C9 D2	70A4",
+		"C9 D3	70AB",
+		"C9 D4	70B0",
+		"C9 D5	70B1",
+		"C9 D6	70B4",
+		"C9 D7	70B7",
+		"C9 D8	70CA",
+		"C9 D9	70D1",
+		"C9 DA	70D3",
+		"C9 DB	70D4",
+		"C9 DC	70D5",
+		"C9 DD	70D6",
+		"C9 DE	70D8",
+		"C9 DF	70DC",
+		"C9 E0	70E4",
+		"C9 E1	70FA",
+		"C9 E2	7103",
+		"C9 E3	7104",
+		"C9 E4	7105",
+		"C9 E5	7106",
+		"C9 E6	7107",
+		"C9 E7	710B",
+		"C9 E8	710C",
+		"C9 E9	710F",
+		"C9 EA	711E",
+		"C9 EB	7120",
+		"C9 EC	712B",
+		"C9 ED	712D",
+		"C9 EE	712F",
+		"C9 EF	7130",
+		"C9 F0	7131",
+		"C9 F1	7138",
+		"C9 F2	7141",
+		"C9 F3	7145",
+		"C9 F4	7146",
+		"C9 F5	7147",
+		"C9 F6	714A",
+		"C9 F7	714B",
+		"C9 F8	7150",
+		"C9 F9	7152",
+		"C9 FA	7157",
+		"C9 FB	715A",
+		"C9 FC	715C",
+		"C9 FD	715E",
+		"C9 FE	7160",
+		"CA A1	7168",
+		"CA A2	7179",
+		"CA A3	7180",
+		"CA A4	7185",
+		"CA A5	7187",
+		"CA A6	718C",
+		"CA A7	7192",
+		"CA A8	719A",
+		"CA A9	719B",
+		"CA AA	71A0",
+		"CA AB	71A2",
+		"CA AC	71AF",
+		"CA AD	71B0",
+		"CA AE	71B2",
+		"CA AF	71B3",
+		"CA B0	71BA",
+		"CA B1	71BF",
+		"CA B2	71C0",
+		"CA B3	71C1",
+		"CA B4	71C4",
+		"CA B5	71CB",
+		"CA B6	71CC",
+		"CA B7	71D3",
+		"CA B8	71D6",
+		"CA B9	71D9",
+		"CA BA	71DA",
+		"CA BB	71DC",
+		"CA BC	71F8",
+		"CA BD	71FE",
+		"CA BE	7200",
+		"CA BF	7207",
+		"CA C0	7208",
+		"CA C1	7209",
+		"CA C2	7213",
+		"CA C3	7217",
+		"CA C4	721A",
+		"CA C5	721D",
+		"CA C6	721F",
+		"CA C7	7224",
+		"CA C8	722B",
+		"CA C9	722F",
+		"CA CA	7234",
+		"CA CB	7238",
+		"CA CC	7239",
+		"CA CD	7241",
+		"CA CE	7242",
+		"CA CF	7243",
+		"CA D0	7245",
+		"CA D1	724E",
+		"CA D2	724F",
+		"CA D3	7250",
+		"CA D4	7253",
+		"CA D5	7255",
+		"CA D6	7256",
+		"CA D7	725A",
+		"CA D8	725C",
+		"CA D9	725E",
+		"CA DA	7260",
+		"CA DB	7263",
+		"CA DC	7268",
+		"CA DD	726B",
+		"CA DE	726E",
+		"CA DF	726F",
+		"CA E0	7271",
+		"CA E1	7277",
+		"CA E2	7278",
+		"CA E3	727B",
+		"CA E4	727C",
+		"CA E5	727F",
+		"CA E6	7284",
+		"CA E7	7289",
+		"CA E8	728D",
+		"CA E9	728E",
+		"CA EA	7293",
+		"CA EB	729B",
+		"CA EC	72A8",
+		"CA ED	72AD",
+		"CA EE	72AE",
+		"CA EF	72B1",
+		"CA F0	72B4",
+		"CA F1	72BE",
+		"CA F2	72C1",
+		"CA F3	72C7",
+		"CA F4	72C9",
+		"CA F5	72CC",
+		"CA F6	72D5",
+		"CA F7	72D6",
+		"CA F8	72D8",
+		"CA F9	72DF",
+		"CA FA	72E5",
+		"CA FB	72F3",
+		"CA FC	72F4",
+		"CA FD	72FA",
+		"CA FE	72FB",
+		"CB A1	72FE",
+		"CB A2	7302",
+		"CB A3	7304",
+		"CB A4	7305",
+		"CB A5	7307",
+		"CB A6	730B",
+		"CB A7	730D",
+		"CB A8	7312",
+		"CB A9	7313",
+		"CB AA	7318",
+		"CB AB	7319",
+		"CB AC	731E",
+		"CB AD	7322",
+		"CB AE	7324",
+		"CB AF	7327",
+		"CB B0	7328",
+		"CB B1	732C",
+		"CB B2	7331",
+		"CB B3	7332",
+		"CB B4	7335",
+		"CB B5	733A",
+		"CB B6	733B",
+		"CB B7	733D",
+		"CB B8	7343",
+		"CB B9	734D",
+		"CB BA	7350",
+		"CB BB	7352",
+		"CB BC	7356",
+		"CB BD	7358",
+		"CB BE	735D",
+		"CB BF	735E",
+		"CB C0	735F",
+		"CB C1	7360",
+		"CB C2	7366",
+		"CB C3	7367",
+		"CB C4	7369",
+		"CB C5	736B",
+		"CB C6	736C",
+		"CB C7	736E",
+		"CB C8	736F",
+		"CB C9	7371",
+		"CB CA	7377",
+		"CB CB	7379",
+		"CB CC	737C",
+		"CB CD	7380",
+		"CB CE	7381",
+		"CB CF	7383",
+		"CB D0	7385",
+		"CB D1	7386",
+		"CB D2	738E",
+		"CB D3	7390",
+		"CB D4	7393",
+		"CB D5	7395",
+		"CB D6	7397",
+		"CB D7	7398",
+		"CB D8	739C",
+		"CB D9	739E",
+		"CB DA	739F",
+		"CB DB	73A0",
+		"CB DC	73A2",
+		"CB DD	73A5",
+		"CB DE	73A6",
+		"CB DF	73AA",
+		"CB E0	73AB",
+		"CB E1	73AD",
+		"CB E2	73B5",
+		"CB E3	73B7",
+		"CB E4	73B9",
+		"CB E5	73BC",
+		"CB E6	73BD",
+		"CB E7	73BF",
+		"CB E8	73C5",
+		"CB E9	73C6",
+		"CB EA	73C9",
+		"CB EB	73CB",
+		"CB EC	73CC",
+		"CB ED	73CF",
+		"CB EE	73D2",
+		"CB EF	73D3",
+		"CB F0	73D6",
+		"CB F1	73D9",
+		"CB F2	73DD",
+		"CB F3	73E1",
+		"CB F4	73E3",
+		"CB F5	73E6",
+		"CB F6	73E7",
+		"CB F7	73E9",
+		"CB F8	73F4",
+		"CB F9	73F5",
+		"CB FA	73F7",
+		"CB FB	73F9",
+		"CB FC	73FA",
+		"CB FD	73FB",
+		"CB FE	73FD",
+		"CC A1	73FF",
+		"CC A2	7400",
+		"CC A3	7401",
+		"CC A4	7404",
+		"CC A5	7407",
+		"CC A6	740A",
+		"CC A7	7411",
+		"CC A8	741A",
+		"CC A9	741B",
+		"CC AA	7424",
+		"CC AB	7426",
+		"CC AC	7428",
+		"CC AD	7429",
+		"CC AE	742A",
+		"CC AF	742B",
+		"CC B0	742C",
+		"CC B1	742D",
+		"CC B2	742E",
+		"CC B3	742F",
+		"CC B4	7430",
+		"CC B5	7431",
+		"CC B6	7439",
+		"CC B7	7440",
+		"CC B8	7443",
+		"CC B9	7444",
+		"CC BA	7446",
+		"CC BB	7447",
+		"CC BC	744B",
+		"CC BD	744D",
+		"CC BE	7451",
+		"CC BF	7452",
+		"CC C0	7457",
+		"CC C1	745D",
+		"CC C2	7462",
+		"CC C3	7466",
+		"CC C4	7467",
+		"CC C5	7468",
+		"CC C6	746B",
+		"CC C7	746D",
+		"CC C8	746E",
+		"CC C9	7471",
+		"CC CA	7472",
+		"CC CB	7480",
+		"CC CC	7481",
+		"CC CD	7485",
+		"CC CE	7486",
+		"CC CF	7487",
+		"CC D0	7489",
+		"CC D1	748F",
+		"CC D2	7490",
+		"CC D3	7491",
+		"CC D4	7492",
+		"CC D5	7498",
+		"CC D6	7499",
+		"CC D7	749A",
+		"CC D8	749C",
+		"CC D9	749F",
+		"CC DA	74A0",
+		"CC DB	74A1",
+		"CC DC	74A3",
+		"CC DD	74A6",
+		"CC DE	74A8",
+		"CC DF	74A9",
+		"CC E0	74AA",
+		"CC E1	74AB",
+		"CC E2	74AE",
+		"CC E3	74AF",
+		"CC E4	74B1",
+		"CC E5	74B2",
+		"CC E6	74B5",
+		"CC E7	74B9",
+		"CC E8	74BB",
+		"CC E9	74BF",
+		"CC EA	74C8",
+		"CC EB	74C9",
+		"CC EC	74CC",
+		"CC ED	74D0",
+		"CC EE	74D3",
+		"CC EF	74D8",
+		"CC F0	74DA",
+		"CC F1	74DB",
+		"CC F2	74DE",
+		"CC F3	74DF",
+		"CC F4	74E4",
+		"CC F5	74E8",
+		"CC F6	74EA",
+		"CC F7	74EB",
+		"CC F8	74EF",
+		"CC F9	74F4",
+		"CC FA	74FA",
+		"CC FB	74FB",
+		"CC FC	74FC",
+		"CC FD	74FF",
+		"CC FE	7506",
+		"CD A1	7512",
+		"CD A2	7516",
+		"CD A3	7517",
+		"CD A4	7520",
+		"CD A5	7521",
+		"CD A6	7524",
+		"CD A7	7527",
+		"CD A8	7529",
+		"CD A9	752A",
+		"CD AA	752F",
+		"CD AB	7536",
+		"CD AC	7539",
+		"CD AD	753D",
+		"CD AE	753E",
+		"CD AF	753F",
+		"CD B0	7540",
+		"CD B1	7543",
+		"CD B2	7547",
+		"CD B3	7548",
+		"CD B4	754E",
+		"CD B5	7550",
+		"CD B6	7552",
+		"CD B7	7557",
+		"CD B8	755E",
+		"CD B9	755F",
+		"CD BA	7561",
+		"CD BB	756F",
+		"CD BC	7571",
+		"CD BD	7579",
+		"CD BE	757A",
+		"CD BF	757B",
+		"CD C0	757C",
+		"CD C1	757D",
+		"CD C2	757E",
+		"CD C3	7581",
+		"CD C4	7585",
+		"CD C5	7590",
+		"CD C6	7592",
+		"CD C7	7593",
+		"CD C8	7595",
+		"CD C9	7599",
+		"CD CA	759C",
+		"CD CB	75A2",
+		"CD CC	75A4",
+		"CD CD	75B4",
+		"CD CE	75BA",
+		"CD CF	75BF",
+		"CD D0	75C0",
+		"CD D1	75C1",
+		"CD D2	75C4",
+		"CD D3	75C6",
+		"CD D4	75CC",
+		"CD D5	75CE",
+		"CD D6	75CF",
+		"CD D7	75D7",
+		"CD D8	75DC",
+		"CD D9	75DF",
+		"CD DA	75E0",
+		"CD DB	75E1",
+		"CD DC	75E4",
+		"CD DD	75E7",
+		"CD DE	75EC",
+		"CD DF	75EE",
+		"CD E0	75EF",
+		"CD E1	75F1",
+		"CD E2	75F9",
+		"CD E3	7600",
+		"CD E4	7602",
+		"CD E5	7603",
+		"CD E6	7604",
+		"CD E7	7607",
+		"CD E8	7608",
+		"CD E9	760A",
+		"CD EA	760C",
+		"CD EB	760F",
+		"CD EC	7612",
+		"CD ED	7613",
+		"CD EE	7615",
+		"CD EF	7616",
+		"CD F0	7619",
+		"CD F1	761B",
+		"CD F2	761C",
+		"CD F3	761D",
+		"CD F4	761E",
+		"CD F5	7623",
+		"CD F6	7625",
+		"CD F7	7626",
+		"CD F8	7629",
+		"CD F9	762D",
+		"CD FA	7632",
+		"CD FB	7633",
+		"CD FC	7635",
+		"CD FD	7638",
+		"CD FE	7639",
+		"CE A1	763A",
+		"CE A2	763C",
+		"CE A4	7640",
+		"CE A5	7641",
+		"CE A6	7643",
+		"CE A7	7644",
+		"CE A8	7645",
+		"CE A9	7649",
+		"CE A3	764A",
+		"CE AA	764B",
+		"CE AB	7655",
+		"CE AC	7659",
+		"CE AD	765F",
+		"CE AE	7664",
+		"CE AF	7665",
+		"CE B0	766D",
+		"CE B1	766E",
+		"CE B2	766F",
+		"CE B3	7671",
+		"CE B4	7674",
+		"CE B5	7681",
+		"CE B6	7685",
+		"CE B7	768C",
+		"CE B8	768D",
+		"CE B9	7695",
+		"CE BA	769B",
+		"CE BB	769C",
+		"CE BC	769D",
+		"CE BD	769F",
+		"CE BE	76A0",
+		"CE BF	76A2",
+		"CE C0	76A3",
+		"CE C1	76A4",
+		"CE C2	76A5",
+		"CE C3	76A6",
+		"CE C4	76A7",
+		"CE C5	76A8",
+		"CE C6	76AA",
+		"CE C7	76AD",
+		"CE C8	76BD",
+		"CE C9	76C1",
+		"CE CA	76C5",
+		"CE CB	76C9",
+		"CE CC	76CB",
+		"CE CD	76CC",
+		"CE CE	76CE",
+		"CE CF	76D4",
+		"CE D0	76D9",
+		"CE D1	76E0",
+		"CE D2	76E6",
+		"CE D3	76E8",
+		"CE D4	76EC",
+		"CE D5	76F0",
+		"CE D6	76F1",
+		"CE D7	76F6",
+		"CE D8	76F9",
+		"CE D9	76FC",
+		"CE DA	7700",
+		"CE DB	7706",
+		"CE DC	770A",
+		"CE DD	770E",
+		"CE DE	7712",
+		"CE DF	7714",
+		"CE E0	7715",
+		"CE E1	7717",
+		"CE E2	7719",
+		"CE E3	771A",
+		"CE E4	771C",
+		"CE E5	7722",
+		"CE E6	7728",
+		"CE E7	772D",
+		"CE E8	772E",
+		"CE E9	772F",
+		"CE EA	7734",
+		"CE EB	7735",
+		"CE EC	7736",
+		"CE ED	7739",
+		"CE EE	773D",
+		"CE EF	773E",
+		"CE F0	7742",
+		"CE F1	7745",
+		"CE F2	7746",
+		"CE F3	774A",
+		"CE F4	774D",
+		"CE F5	774E",
+		"CE F6	774F",
+		"CE F7	7752",
+		"CE F8	7756",
+		"CE F9	7757",
+		"CE FA	775C",
+		"CE FB	775E",
+		"CE FC	775F",
+		"CE FD	7760",
+		"CE FE	7762",
+		"CF A1	7764",
+		"CF A2	7767",
+		"CF A3	776A",
+		"CF A4	776C",
+		"CF A5	7770",
+		"CF A6	7772",
+		"CF A7	7773",
+		"CF A8	7774",
+		"CF A9	777A",
+		"CF AA	777D",
+		"CF AB	7780",
+		"CF AC	7784",
+		"CF AD	778C",
+		"CF AE	778D",
+		"CF AF	7794",
+		"CF B0	7795",
+		"CF B1	7796",
+		"CF B2	779A",
+		"CF B3	779F",
+		"CF B4	77A2",
+		"CF B5	77A7",
+		"CF B6	77AA",
+		"CF B7	77AE",
+		"CF B8	77AF",
+		"CF B9	77B1",
+		"CF BA	77B5",
+		"CF BB	77BE",
+		"CF BC	77C3",
+		"CF BD	77C9",
+		"CF BE	77D1",
+		"CF BF	77D2",
+		"CF C0	77D5",
+		"CF C1	77D9",
+		"CF C2	77DE",
+		"CF C3	77DF",
+		"CF C4	77E0",
+		"CF C5	77E4",
+		"CF C6	77E6",
+		"CF C7	77EA",
+		"CF C8	77EC",
+		"CF C9	77F0",
+		"CF CA	77F1",
+		"CF CB	77F4",
+		"CF CC	77F8",
+		"CF CD	77FB",
+		"CF CE	7805",
+		"CF CF	7806",
+		"CF D0	7809",
+		"CF D1	780D",
+		"CF D2	780E",
+		"CF D3	7811",
+		"CF D4	781D",
+		"CF D5	7821",
+		"CF D6	7822",
+		"CF D7	7823",
+		"CF D8	782D",
+		"CF D9	782E",
+		"CF DA	7830",
+		"CF DB	7835",
+		"CF DC	7837",
+		"CF DD	7843",
+		"CF DE	7844",
+		"CF DF	7847",
+		"CF E0	7848",
+		"CF E1	784C",
+		"CF E2	784E",
+		"CF E3	7852",
+		"CF E4	785C",
+		"CF E5	785E",
+		"CF E6	7860",
+		"CF E7	7861",
+		"CF E8	7863",
+		"CF E9	7864",
+		"CF EA	7868",
+		"CF EB	786A",
+		"CF EC	786E",
+		"CF ED	787A",
+		"CF EE	787E",
+		"CF EF	788A",
+		"CF F0	788F",
+		"CF F1	7894",
+		"CF F2	7898",
+		"CF F4	789D",
+		"CF F5	789E",
+		"CF F6	789F",
+		"CF F3	78A1",
+		"CF F7	78A4",
+		"CF F8	78A8",
+		"CF F9	78AC",
+		"CF FA	78AD",
+		"CF FB	78B0",
+		"CF FC	78B1",
+		"CF FD	78B2",
+		"CF FE	78B3",
+		"D0 A1	78BB",
+		"D0 A2	78BD",
+		"D0 A3	78BF",
+		"D0 A4	78C7",
+		"D0 A5	78C8",
+		"D0 A6	78C9",
+		"D0 A7	78CC",
+		"D0 A8	78CE",
+		"D0 A9	78D2",
+		"D0 AA	78D3",
+		"D0 AB	78D5",
+		"D0 AC	78D6",
+		"D0 AE	78DB",
+		"D0 AF	78DF",
+		"D0 B0	78E0",
+		"D0 B1	78E1",
+		"D0 AD	78E4",
+		"D0 B2	78E6",
+		"D0 B3	78EA",
+		"D0 B4	78F2",
+		"D0 B5	78F3",
+		"D0 B7	78F6",
+		"D0 B8	78F7",
+		"D0 B9	78FA",
+		"D0 BA	78FB",
+		"D0 BB	78FF",
+		"D0 B6	7900",
+		"D0 BC	7906",
+		"D0 BD	790C",
+		"D0 BE	7910",
+		"D0 BF	791A",
+		"D0 C0	791C",
+		"D0 C1	791E",
+		"D0 C2	791F",
+		"D0 C3	7920",
+		"D0 C4	7925",
+		"D0 C5	7927",
+		"D0 C6	7929",
+		"D0 C7	792D",
+		"D0 C8	7931",
+		"D0 C9	7934",
+		"D0 CA	7935",
+		"D0 CB	793B",
+		"D0 CC	793D",
+		"D0 CD	793F",
+		"D0 CE	7944",
+		"D0 CF	7945",
+		"D0 D0	7946",
+		"D0 D1	794A",
+		"D0 D2	794B",
+		"D0 D3	794F",
+		"D0 D4	7951",
+		"D0 D5	7954",
+		"D0 D6	7958",
+		"D0 D7	795B",
+		"D0 D8	795C",
+		"D0 D9	7967",
+		"D0 DA	7969",
+		"D0 DB	796B",
+		"D0 DC	7972",
+		"D0 DD	7979",
+		"D0 DE	797B",
+		"D0 DF	797C",
+		"D0 E0	797E",
+		"D0 E1	798B",
+		"D0 E2	798C",
+		"D0 E3	7991",
+		"D0 E4	7993",
+		"D0 E5	7994",
+		"D0 E6	7995",
+		"D0 E7	7996",
+		"D0 E8	7998",
+		"D0 E9	799B",
+		"D0 EA	799C",
+		"D0 EB	79A1",
+		"D0 EC	79A8",
+		"D0 ED	79A9",
+		"D0 EE	79AB",
+		"D0 EF	79AF",
+		"D0 F0	79B1",
+		"D0 F1	79B4",
+		"D0 F2	79B8",
+		"D0 F3	79BB",
+		"D0 F4	79C2",
+		"D0 F5	79C4",
+		"D0 F6	79C7",
+		"D0 F7	79C8",
+		"D0 F8	79CA",
+		"D0 F9	79CF",
+		"D0 FA	79D4",
+		"D0 FB	79D6",
+		"D0 FC	79DA",
+		"D0 FD	79DD",
+		"D0 FE	79DE",
+		"D1 A1	79E0",
+		"D1 A2	79E2",
+		"D1 A3	79E5",
+		"D1 A4	79EA",
+		"D1 A5	79EB",
+		"D1 A6	79ED",
+		"D1 A7	79F1",
+		"D1 A8	79F8",
+		"D1 A9	79FC",
+		"D1 AA	7A02",
+		"D1 AB	7A03",
+		"D1 AC	7A07",
+		"D1 AD	7A09",
+		"D1 AE	7A0A",
+		"D1 AF	7A0C",
+		"D1 B0	7A11",
+		"D1 B1	7A15",
+		"D1 B2	7A1B",
+		"D1 B3	7A1E",
+		"D1 B4	7A21",
+		"D1 B5	7A27",
+		"D1 B6	7A2B",
+		"D1 B7	7A2D",
+		"D1 B8	7A2F",
+		"D1 B9	7A30",
+		"D1 BA	7A34",
+		"D1 BB	7A35",
+		"D1 BC	7A38",
+		"D1 BD	7A39",
+		"D1 BE	7A3A",
+		"D1 BF	7A44",
+		"D1 C0	7A45",
+		"D1 C1	7A47",
+		"D1 C2	7A48",
+		"D1 C3	7A4C",
+		"D1 C4	7A55",
+		"D1 C5	7A56",
+		"D1 C6	7A59",
+		"D1 C7	7A5C",
+		"D1 C8	7A5D",
+		"D1 C9	7A5F",
+		"D1 CA	7A60",
+		"D1 CB	7A65",
+		"D1 CC	7A67",
+		"D1 CD	7A6A",
+		"D1 CE	7A6D",
+		"D1 CF	7A75",
+		"D1 D0	7A78",
+		"D1 D1	7A7E",
+		"D1 D2	7A80",
+		"D1 D3	7A82",
+		"D1 D4	7A85",
+		"D1 D5	7A86",
+		"D1 D6	7A8A",
+		"D1 D7	7A8B",
+		"D1 D8	7A90",
+		"D1 D9	7A91",
+		"D1 DA	7A94",
+		"D1 DB	7A9E",
+		"D1 DC	7AA0",
+		"D1 DD	7AA3",
+		"D1 DE	7AAC",
+		"D1 DF	7AB3",
+		"D1 E0	7AB5",
+		"D1 E1	7AB9",
+		"D1 E2	7ABB",
+		"D1 E3	7ABC",
+		"D1 E4	7AC6",
+		"D1 E5	7AC9",
+		"D1 E6	7ACC",
+		"D1 E7	7ACE",
+		"D1 E8	7AD1",
+		"D1 E9	7ADB",
+		"D1 EA	7AE8",
+		"D1 EB	7AE9",
+		"D1 EC	7AEB",
+		"D1 ED	7AEC",
+		"D1 EE	7AF1",
+		"D1 EF	7AF4",
+		"D1 F0	7AFB",
+		"D1 F1	7AFD",
+		"D1 F2	7AFE",
+		"D1 F3	7B07",
+		"D1 F4	7B14",
+		"D1 F5	7B1F",
+		"D1 F6	7B23",
+		"D1 F7	7B27",
+		"D1 F8	7B29",
+		"D1 F9	7B2A",
+		"D1 FA	7B2B",
+		"D1 FB	7B2D",
+		"D1 FC	7B2E",
+		"D1 FD	7B2F",
+		"D1 FE	7B30",
+		"D2 A1	7B31",
+		"D2 A2	7B34",
+		"D2 A3	7B3D",
+		"D2 A4	7B3F",
+		"D2 A5	7B40",
+		"D2 A6	7B41",
+		"D2 A7	7B47",
+		"D2 A8	7B4E",
+		"D2 A9	7B55",
+		"D2 AA	7B60",
+		"D2 AB	7B64",
+		"D2 AC	7B66",
+		"D2 AD	7B69",
+		"D2 AE	7B6A",
+		"D2 AF	7B6D",
+		"D2 B0	7B6F",
+		"D2 B1	7B72",
+		"D2 B2	7B73",
+		"D2 B3	7B77",
+		"D2 B4	7B84",
+		"D2 B5	7B89",
+		"D2 B6	7B8E",
+		"D2 B7	7B90",
+		"D2 B8	7B91",
+		"D2 B9	7B96",
+		"D2 BA	7B9B",
+		"D2 BB	7B9E",
+		"D2 BC	7BA0",
+		"D2 BD	7BA5",
+		"D2 BE	7BAC",
+		"D2 BF	7BAF",
+		"D2 C0	7BB0",
+		"D2 C1	7BB2",
+		"D2 C2	7BB5",
+		"D2 C3	7BB6",
+		"D2 C4	7BBA",
+		"D2 C5	7BBB",
+		"D2 C6	7BBC",
+		"D2 C7	7BBD",
+		"D2 C8	7BC2",
+		"D2 C9	7BC5",
+		"D2 CA	7BC8",
+		"D2 CB	7BCA",
+		"D2 CC	7BD4",
+		"D2 CD	7BD6",
+		"D2 CE	7BD7",
+		"D2 CF	7BD9",
+		"D2 D0	7BDA",
+		"D2 D1	7BDB",
+		"D2 D2	7BE8",
+		"D2 D3	7BEA",
+		"D2 D4	7BF2",
+		"D2 D5	7BF4",
+		"D2 D6	7BF5",
+		"D2 D7	7BF8",
+		"D2 D8	7BF9",
+		"D2 D9	7BFA",
+		"D2 DA	7BFC",
+		"D2 DB	7BFE",
+		"D2 DC	7C01",
+		"D2 DD	7C02",
+		"D2 DE	7C03",
+		"D2 DF	7C04",
+		"D2 E0	7C06",
+		"D2 E1	7C09",
+		"D2 E2	7C0B",
+		"D2 E3	7C0C",
+		"D2 E4	7C0E",
+		"D2 E5	7C0F",
+		"D2 E6	7C19",
+		"D2 E7	7C1B",
+		"D2 E8	7C20",
+		"D2 E9	7C25",
+		"D2 EA	7C26",
+		"D2 EB	7C28",
+		"D2 EC	7C2C",
+		"D2 ED	7C31",
+		"D2 EE	7C33",
+		"D2 EF	7C34",
+		"D2 F0	7C36",
+		"D2 F1	7C39",
+		"D2 F2	7C3A",
+		"D2 F3	7C46",
+		"D2 F4	7C4A",
+		"D2 F6	7C51",
+		"D2 F7	7C52",
+		"D2 F8	7C53",
+		"D2 F5	7C55",
+		"D2 F9	7C59",
+		"D2 FA	7C5A",
+		"D2 FB	7C5B",
+		"D2 FC	7C5C",
+		"D2 FD	7C5D",
+		"D2 FE	7C5E",
+		"D3 A1	7C61",
+		"D3 A2	7C63",
+		"D3 A3	7C67",
+		"D3 A4	7C69",
+		"D3 A5	7C6D",
+		"D3 A6	7C6E",
+		"D3 A7	7C70",
+		"D3 A8	7C72",
+		"D3 A9	7C79",
+		"D3 AA	7C7C",
+		"D3 AB	7C7D",
+		"D3 AC	7C86",
+		"D3 AD	7C87",
+		"D3 AE	7C8F",
+		"D3 AF	7C94",
+		"D3 B0	7C9E",
+		"D3 B1	7CA0",
+		"D3 B2	7CA6",
+		"D3 B3	7CB0",
+		"D3 B4	7CB6",
+		"D3 B5	7CB7",
+		"D3 B6	7CBA",
+		"D3 B7	7CBB",
+		"D3 B8	7CBC",
+		"D3 B9	7CBF",
+		"D3 BA	7CC4",
+		"D3 BB	7CC7",
+		"D3 BC	7CC8",
+		"D3 BD	7CC9",
+		"D3 BE	7CCD",
+		"D3 BF	7CCF",
+		"D3 C0	7CD3",
+		"D3 C1	7CD4",
+		"D3 C2	7CD5",
+		"D3 C3	7CD7",
+		"D3 C4	7CD9",
+		"D3 C5	7CDA",
+		"D3 C6	7CDD",
+		"D3 C7	7CE6",
+		"D3 C8	7CE9",
+		"D3 C9	7CEB",
+		"D3 CA	7CF5",
+		"D3 CB	7D03",
+		"D3 CC	7D07",
+		"D3 CD	7D08",
+		"D3 CE	7D09",
+		"D3 CF	7D0F",
+		"D3 D0	7D11",
+		"D3 D1	7D12",
+		"D3 D2	7D13",
+		"D3 D3	7D16",
+		"D3 D4	7D1D",
+		"D3 D5	7D1E",
+		"D3 D6	7D23",
+		"D3 D7	7D26",
+		"D3 D8	7D2A",
+		"D3 D9	7D2D",
+		"D3 DA	7D31",
+		"D3 DB	7D3C",
+		"D3 DC	7D3D",
+		"D3 DD	7D3E",
+		"D3 DE	7D40",
+		"D3 DF	7D41",
+		"D3 E0	7D47",
+		"D3 E1	7D48",
+		"D3 E2	7D4D",
+		"D3 E3	7D51",
+		"D3 E4	7D53",
+		"D3 E5	7D57",
+		"D3 E6	7D59",
+		"D3 E7	7D5A",
+		"D3 E8	7D5C",
+		"D3 E9	7D5D",
+		"D3 EA	7D65",
+		"D3 EB	7D67",
+		"D3 EC	7D6A",
+		"D3 ED	7D70",
+		"D3 EE	7D78",
+		"D3 EF	7D7A",
+		"D3 F0	7D7B",
+		"D3 F1	7D7F",
+		"D3 F2	7D81",
+		"D3 F3	7D82",
+		"D3 F4	7D83",
+		"D3 F5	7D85",
+		"D3 F6	7D86",
+		"D3 F7	7D88",
+		"D3 F8	7D8B",
+		"D3 F9	7D8C",
+		"D3 FA	7D8D",
+		"D3 FB	7D91",
+		"D3 FC	7D96",
+		"D3 FD	7D97",
+		"D3 FE	7D9D",
+		"D4 A1	7D9E",
+		"D4 A2	7DA6",
+		"D4 A3	7DA7",
+		"D4 A4	7DAA",
+		"D4 A5	7DB3",
+		"D4 A6	7DB6",
+		"D4 A7	7DB7",
+		"D4 A8	7DB9",
+		"D4 A9	7DC2",
+		"D4 AA	7DC3",
+		"D4 AB	7DC4",
+		"D4 AC	7DC5",
+		"D4 AD	7DC6",
+		"D4 AE	7DCC",
+		"D4 AF	7DCD",
+		"D4 B0	7DCE",
+		"D4 B1	7DD7",
+		"D4 B2	7DD9",
+		"D4 B4	7DE2",
+		"D4 B5	7DE5",
+		"D4 B6	7DE6",
+		"D4 B7	7DEA",
+		"D4 B8	7DEB",
+		"D4 B9	7DED",
+		"D4 BA	7DF1",
+		"D4 BB	7DF5",
+		"D4 BC	7DF6",
+		"D4 BD	7DF9",
+		"D4 BE	7DFA",
+		"D4 B3	7E00",
+		"D4 BF	7E08",
+		"D4 C0	7E10",
+		"D4 C1	7E11",
+		"D4 C2	7E15",
+		"D4 C3	7E17",
+		"D4 C4	7E1C",
+		"D4 C5	7E1D",
+		"D4 C6	7E20",
+		"D4 C7	7E27",
+		"D4 C8	7E28",
+		"D4 C9	7E2C",
+		"D4 CA	7E2D",
+		"D4 CB	7E2F",
+		"D4 CC	7E33",
+		"D4 CD	7E36",
+		"D4 CE	7E3F",
+		"D4 CF	7E44",
+		"D4 D0	7E45",
+		"D4 D1	7E47",
+		"D4 D2	7E4E",
+		"D4 D3	7E50",
+		"D4 D4	7E52",
+		"D4 D5	7E58",
+		"D4 D6	7E5F",
+		"D4 D7	7E61",
+		"D4 D8	7E62",
+		"D4 D9	7E65",
+		"D4 DA	7E6B",
+		"D4 DB	7E6E",
+		"D4 DC	7E6F",
+		"D4 DD	7E73",
+		"D4 DE	7E78",
+		"D4 DF	7E7E",
+		"D4 E0	7E81",
+		"D4 E1	7E86",
+		"D4 E2	7E87",
+		"D4 E3	7E8A",
+		"D4 E4	7E8D",
+		"D4 E5	7E91",
+		"D4 E6	7E95",
+		"D4 E7	7E98",
+		"D4 E8	7E9A",
+		"D4 E9	7E9D",
+		"D4 EA	7E9E",
+		"D4 EC	7F3B",
+		"D4 EB	7F3C",
+		"D4 ED	7F3D",
+		"D4 EE	7F3E",
+		"D4 EF	7F3F",
+		"D4 F0	7F43",
+		"D4 F1	7F44",
+		"D4 F2	7F47",
+		"D4 F3	7F4F",
+		"D4 F4	7F52",
+		"D4 F5	7F53",
+		"D4 F6	7F5B",
+		"D4 F7	7F5C",
+		"D4 F8	7F5D",
+		"D4 F9	7F61",
+		"D4 FA	7F63",
+		"D4 FB	7F64",
+		"D4 FC	7F65",
+		"D4 FD	7F66",
+		"D4 FE	7F6D",
+		"D5 A1	7F71",
+		"D5 A2	7F7D",
+		"D5 A3	7F7E",
+		"D5 A4	7F7F",
+		"D5 A5	7F80",
+		"D5 A6	7F8B",
+		"D5 A7	7F8D",
+		"D5 A8	7F8F",
+		"D5 A9	7F90",
+		"D5 AA	7F91",
+		"D5 AB	7F96",
+		"D5 AC	7F97",
+		"D5 AD	7F9C",
+		"D5 AE	7FA1",
+		"D5 AF	7FA2",
+		"D5 B0	7FA6",
+		"D5 B1	7FAA",
+		"D5 B2	7FAD",
+		"D5 B3	7FB4",
+		"D5 B4	7FBC",
+		"D5 B5	7FBF",
+		"D5 B6	7FC0",
+		"D5 B7	7FC3",
+		"D5 B8	7FC8",
+		"D5 B9	7FCE",
+		"D5 BA	7FCF",
+		"D5 BB	7FDB",
+		"D5 BC	7FDF",
+		"D5 BD	7FE3",
+		"D5 BE	7FE5",
+		"D5 BF	7FE8",
+		"D5 C0	7FEC",
+		"D5 C1	7FEE",
+		"D5 C2	7FEF",
+		"D5 C3	7FF2",
+		"D5 C4	7FFA",
+		"D5 C5	7FFD",
+		"D5 C6	7FFE",
+		"D5 C7	7FFF",
+		"D5 C8	8007",
+		"D5 C9	8008",
+		"D5 CA	800A",
+		"D5 CB	800D",
+		"D5 CC	800E",
+		"D5 CD	800F",
+		"D5 CE	8011",
+		"D5 CF	8013",
+		"D5 D0	8014",
+		"D5 D1	8016",
+		"D5 D2	801D",
+		"D5 D3	801E",
+		"D5 D4	801F",
+		"D5 D5	8020",
+		"D5 D6	8024",
+		"D5 D7	8026",
+		"D5 D8	802C",
+		"D5 D9	802E",
+		"D5 DA	8030",
+		"D5 DB	8034",
+		"D5 DC	8035",
+		"D5 DD	8037",
+		"D5 DE	8039",
+		"D5 DF	803A",
+		"D5 E0	803C",
+		"D5 E1	803E",
+		"D5 E2	8040",
+		"D5 E3	8044",
+		"D5 E4	8060",
+		"D5 E5	8064",
+		"D5 E6	8066",
+		"D5 E7	806D",
+		"D5 E8	8071",
+		"D5 E9	8075",
+		"D5 EA	8081",
+		"D5 EB	8088",
+		"D5 EC	808E",
+		"D5 ED	809C",
+		"D5 EE	809E",
+		"D5 EF	80A6",
+		"D5 F0	80A7",
+		"D5 F1	80AB",
+		"D5 F2	80B8",
+		"D5 F3	80B9",
+		"D5 F4	80C8",
+		"D5 F5	80CD",
+		"D5 F6	80CF",
+		"D5 F7	80D2",
+		"D5 F8	80D4",
+		"D5 F9	80D5",
+		"D5 FA	80D7",
+		"D5 FB	80D8",
+		"D5 FC	80E0",
+		"D5 FD	80ED",
+		"D5 FE	80EE",
+		"D6 A1	80F0",
+		"D6 A2	80F2",
+		"D6 A3	80F3",
+		"D6 A4	80F6",
+		"D6 A5	80F9",
+		"D6 A6	80FA",
+		"D6 A7	80FE",
+		"D6 A8	8103",
+		"D6 A9	810B",
+		"D6 AA	8116",
+		"D6 AB	8117",
+		"D6 AC	8118",
+		"D6 AD	811C",
+		"D6 AE	811E",
+		"D6 AF	8120",
+		"D6 B0	8124",
+		"D6 B1	8127",
+		"D6 B2	812C",
+		"D6 B3	8130",
+		"D6 B4	8135",
+		"D6 B5	813A",
+		"D6 B6	813C",
+		"D6 B7	8145",
+		"D6 B8	8147",
+		"D6 B9	814A",
+		"D6 BA	814C",
+		"D6 BB	8152",
+		"D6 BC	8157",
+		"D6 BD	8160",
+		"D6 BE	8161",
+		"D6 BF	8167",
+		"D6 C0	8168",
+		"D6 C1	8169",
+		"D6 C2	816D",
+		"D6 C3	816F",
+		"D6 C4	8177",
+		"D6 C5	8181",
+		"D6 C7	8184",
+		"D6 C8	8185",
+		"D6 C9	8186",
+		"D6 CA	818B",
+		"D6 CB	818E",
+		"D6 C6	8190",
+		"D6 CC	8196",
+		"D6 CD	8198",
+		"D6 CE	819B",
+		"D6 CF	819E",
+		"D6 D0	81A2",
+		"D6 D1	81AE",
+		"D6 D2	81B2",
+		"D6 D3	81B4",
+		"D6 D4	81BB",
+		"D6 D6	81C3",
+		"D6 D7	81C5",
+		"D6 D8	81CA",
+		"D6 D5	81CB",
+		"D6 D9	81CE",
+		"D6 DA	81CF",
+		"D6 DB	81D5",
+		"D6 DC	81D7",
+		"D6 DD	81DB",
+		"D6 DE	81DD",
+		"D6 DF	81DE",
+		"D6 E0	81E1",
+		"D6 E1	81E4",
+		"D6 E2	81EB",
+		"D6 E3	81EC",
+		"D6 E4	81F0",
+		"D6 E5	81F1",
+		"D6 E6	81F2",
+		"D6 E7	81F5",
+		"D6 E8	81F6",
+		"D6 E9	81F8",
+		"D6 EA	81F9",
+		"D6 EB	81FD",
+		"D6 EC	81FF",
+		"D6 ED	8200",
+		"D6 EE	8203",
+		"D6 EF	820F",
+		"D6 F0	8213",
+		"D6 F1	8214",
+		"D6 F2	8219",
+		"D6 F3	821A",
+		"D6 F4	821D",
+		"D6 F5	8221",
+		"D6 F6	8222",
+		"D6 F7	8228",
+		"D6 F8	8232",
+		"D6 F9	8234",
+		"D6 FA	823A",
+		"D6 FB	8243",
+		"D6 FC	8244",
+		"D6 FD	8245",
+		"D6 FE	8246",
+		"D7 A1	824B",
+		"D7 A2	824E",
+		"D7 A3	824F",
+		"D7 A4	8251",
+		"D7 A5	8256",
+		"D7 A6	825C",
+		"D7 A7	8260",
+		"D7 A8	8263",
+		"D7 A9	8267",
+		"D7 AA	826D",
+		"D7 AB	8274",
+		"D7 AC	827B",
+		"D7 AD	827D",
+		"D7 AE	827F",
+		"D7 AF	8280",
+		"D7 B0	8281",
+		"D7 B1	8283",
+		"D7 B2	8284",
+		"D7 B3	8287",
+		"D7 B4	8289",
+		"D7 B5	828A",
+		"D7 B6	828E",
+		"D7 B7	8291",
+		"D7 B8	8294",
+		"D7 B9	8296",
+		"D7 BA	8298",
+		"D7 BB	829A",
+		"D7 BC	829B",
+		"D7 BD	82A0",
+		"D7 BE	82A1",
+		"D7 BF	82A3",
+		"D7 C0	82A4",
+		"D7 C1	82A7",
+		"D7 C2	82A8",
+		"D7 C3	82A9",
+		"D7 C4	82AA",
+		"D7 C5	82AE",
+		"D7 C6	82B0",
+		"D7 C7	82B2",
+		"D7 C8	82B4",
+		"D7 C9	82B7",
+		"D7 CA	82BA",
+		"D7 CB	82BC",
+		"D7 CC	82BE",
+		"D7 CD	82BF",
+		"D7 CE	82C6",
+		"D7 CF	82D0",
+		"D7 D0	82D5",
+		"D7 D1	82DA",
+		"D7 D2	82E0",
+		"D7 D3	82E2",
+		"D7 D4	82E4",
+		"D7 D5	82E8",
+		"D7 D6	82EA",
+		"D7 D7	82ED",
+		"D7 D8	82EF",
+		"D7 D9	82F6",
+		"D7 DA	82F7",
+		"D7 DB	82FD",
+		"D7 DC	82FE",
+		"D7 DD	8300",
+		"D7 DE	8301",
+		"D7 DF	8307",
+		"D7 E0	8308",
+		"D7 E1	830A",
+		"D7 E2	830B",
+		"D7 E4	831B",
+		"D7 E5	831D",
+		"D7 E6	831E",
+		"D7 E7	831F",
+		"D7 E8	8321",
+		"D7 E9	8322",
+		"D7 EA	832C",
+		"D7 EB	832D",
+		"D7 EC	832E",
+		"D7 ED	8330",
+		"D7 EE	8333",
+		"D7 EF	8337",
+		"D7 F0	833A",
+		"D7 F1	833C",
+		"D7 F2	833D",
+		"D7 F3	8342",
+		"D7 F4	8343",
+		"D7 F5	8344",
+		"D7 F6	8347",
+		"D7 F7	834D",
+		"D7 F8	834E",
+		"D7 F9	8351",
+		"D8 BE	8353",
+		"D7 E3	8354",
+		"D7 FA	8355",
+		"D7 FB	8356",
+		"D7 FC	8357",
+		"D7 FD	8370",
+		"D7 FE	8378",
+		"D8 A1	837D",
+		"D8 A2	837F",
+		"D8 A3	8380",
+		"D8 A4	8382",
+		"D8 A5	8384",
+		"D8 A6	8386",
+		"D8 A7	838D",
+		"D8 A8	8392",
+		"D8 A9	8394",
+		"D8 AA	8395",
+		"D8 AB	8398",
+		"D8 AC	8399",
+		"D8 AD	839B",
+		"D8 AE	839C",
+		"D8 AF	839D",
+		"D8 B0	83A6",
+		"D8 B1	83A7",
+		"D8 B2	83A9",
+		"D8 B3	83AC",
+		"D8 CC	83AD",
+		"D8 B4	83BE",
+		"D8 B5	83BF",
+		"D8 B6	83C0",
+		"D8 B7	83C7",
+		"D8 B8	83C9",
+		"D8 B9	83CF",
+		"D8 BA	83D0",
+		"D8 BB	83D1",
+		"D8 BC	83D4",
+		"D8 BD	83DD",
+		"D8 BF	83E8",
+		"D8 C0	83EA",
+		"D8 C1	83F6",
+		"D8 C2	83F8",
+		"D8 C3	83F9",
+		"D8 C4	83FC",
+		"D8 C5	8401",
+		"D8 C6	8406",
+		"D8 C7	840A",
+		"D8 C8	840F",
+		"D8 C9	8411",
+		"D8 CA	8415",
+		"D8 CB	8419",
+		"D8 CD	842F",
+		"D8 CE	8439",
+		"D8 CF	8445",
+		"D8 D0	8447",
+		"D8 D1	8448",
+		"D8 D2	844A",
+		"D8 D3	844D",
+		"D8 D4	844F",
+		"D8 D5	8451",
+		"D8 D6	8452",
+		"D8 D7	8456",
+		"D8 D8	8458",
+		"D8 D9	8459",
+		"D8 DA	845A",
+		"D8 DB	845C",
+		"D8 DC	8460",
+		"D8 DD	8464",
+		"D8 DE	8465",
+		"D8 DF	8467",
+		"D8 E0	846A",
+		"D8 E1	8470",
+		"D8 E2	8473",
+		"D8 E3	8474",
+		"D8 E4	8476",
+		"D8 E5	8478",
+		"D8 E6	847C",
+		"D8 E7	847D",
+		"D8 E8	8481",
+		"D8 E9	8485",
+		"D8 EA	8492",
+		"D8 EB	8493",
+		"D8 EC	8495",
+		"D8 ED	849E",
+		"D8 EE	84A6",
+		"D8 EF	84A8",
+		"D8 F0	84A9",
+		"D8 F1	84AA",
+		"D8 F2	84AF",
+		"D8 F3	84B1",
+		"D8 F4	84B4",
+		"D8 F5	84BA",
+		"D8 F6	84BD",
+		"D8 F7	84BE",
+		"D8 F8	84C0",
+		"D8 F9	84C2",
+		"D8 FA	84C7",
+		"D8 FB	84C8",
+		"D8 FC	84CC",
+		"D8 FD	84CF",
+		"D8 FE	84D3",
+		"D9 A1	84DC",
+		"D9 A2	84E7",
+		"D9 A3	84EA",
+		"D9 A4	84EF",
+		"D9 A5	84F0",
+		"D9 A6	84F1",
+		"D9 A7	84F2",
+		"D9 A8	84F7",
+		"D9 AA	84FA",
+		"D9 AB	84FB",
+		"D9 AC	84FD",
+		"D9 AD	8502",
+		"D9 AE	8503",
+		"D9 AF	8507",
+		"D9 B0	850C",
+		"D9 B1	850E",
+		"D9 B2	8510",
+		"D9 B3	851C",
+		"D9 B4	851E",
+		"D9 B5	8522",
+		"D9 B6	8523",
+		"D9 B7	8524",
+		"D9 B8	8525",
+		"D9 B9	8527",
+		"D9 BA	852A",
+		"D9 BB	852B",
+		"D9 BC	852F",
+		"D9 A9	8532",
+		"D9 BD	8533",
+		"D9 BE	8534",
+		"D9 BF	8536",
+		"D9 C0	853F",
+		"D9 C1	8546",
+		"D9 C2	854F",
+		"D9 C3	8550",
+		"D9 C4	8551",
+		"D9 C5	8552",
+		"D9 C6	8553",
+		"D9 C7	8556",
+		"D9 C8	8559",
+		"D9 C9	855C",
+		"D9 CA	855D",
+		"D9 CB	855E",
+		"D9 CC	855F",
+		"D9 CD	8560",
+		"D9 CE	8561",
+		"D9 CF	8562",
+		"D9 D0	8564",
+		"D9 D1	856B",
+		"D9 D2	856F",
+		"D9 D3	8579",
+		"D9 D4	857A",
+		"D9 D5	857B",
+		"D9 D6	857D",
+		"D9 D7	857F",
+		"D9 D8	8581",
+		"D9 D9	8585",
+		"D9 DA	8586",
+		"D9 DB	8589",
+		"D9 DC	858B",
+		"D9 DD	858C",
+		"D9 DE	858F",
+		"D9 DF	8593",
+		"D9 E0	8598",
+		"D9 E1	859D",
+		"D9 E2	859F",
+		"D9 E3	85A0",
+		"D9 E4	85A2",
+		"D9 E5	85A5",
+		"D9 E6	85A7",
+		"D9 F4	85AD",
+		"D9 E7	85B4",
+		"D9 E8	85B6",
+		"D9 E9	85B7",
+		"D9 EA	85B8",
+		"D9 EB	85BC",
+		"D9 EC	85BD",
+		"D9 ED	85BE",
+		"D9 EE	85BF",
+		"D9 EF	85C2",
+		"D9 F0	85C7",
+		"D9 F1	85CA",
+		"D9 F2	85CB",
+		"D9 F3	85CE",
+		"D9 F5	85D8",
+		"D9 F6	85DA",
+		"D9 F7	85DF",
+		"D9 F8	85E0",
+		"D9 F9	85E6",
+		"D9 FA	85E8",
+		"D9 FB	85ED",
+		"D9 FC	85F3",
+		"D9 FD	85F6",
+		"D9 FE	85FC",
+		"DA A1	85FF",
+		"DA A2	8600",
+		"DA A3	8604",
+		"DA A4	8605",
+		"DA A5	860D",
+		"DA A6	860E",
+		"DA A7	8610",
+		"DA A8	8611",
+		"DA A9	8612",
+		"DA AA	8618",
+		"DA AB	8619",
+		"DA AC	861B",
+		"DA AD	861E",
+		"DA AE	8621",
+		"DA AF	8627",
+		"DA B0	8629",
+		"DA B1	8636",
+		"DA B2	8638",
+		"DA B3	863A",
+		"DA B4	863C",
+		"DA B5	863D",
+		"DA B6	8640",
+		"B8 E6	8641",
+		"DA B7	8642",
+		"DA B8	8646",
+		"DA B9	8652",
+		"DA BA	8653",
+		"DA BB	8656",
+		"DA BC	8657",
+		"DA BD	8658",
+		"DA BE	8659",
+		"DA BF	865D",
+		"DA C0	8660",
+		"DA C1	8661",
+		"DA C2	8662",
+		"DA C3	8663",
+		"DA C4	8664",
+		"DA C5	8669",
+		"DA C6	866C",
+		"DA C7	866F",
+		"DA C8	8675",
+		"DA C9	8676",
+		"DA CA	8677",
+		"DA CB	867A",
+		"DA ED	8688",
+		"DA CC	868D",
+		"DA CD	8691",
+		"DA CE	8696",
+		"DA CF	8698",
+		"DA D0	869A",
+		"DA D1	869C",
+		"DA D2	86A1",
+		"DA D3	86A6",
+		"DA D4	86A7",
+		"DA D5	86A8",
+		"DA D6	86AD",
+		"DA D7	86B1",
+		"DA D8	86B3",
+		"DA D9	86B4",
+		"DA DA	86B5",
+		"DA DB	86B7",
+		"DA DC	86B8",
+		"DA DD	86B9",
+		"DA DE	86BF",
+		"DA DF	86C0",
+		"DA E0	86C1",
+		"DA E1	86C3",
+		"DA E2	86C5",
+		"DA E3	86D1",
+		"DA E4	86D2",
+		"DA E5	86D5",
+		"DA E6	86D7",
+		"DA E7	86DA",
+		"DA E8	86DC",
+		"DA E9	86E0",
+		"DA EA	86E3",
+		"DA EB	86E5",
+		"DA EC	86E7",
+		"DA EE	86FA",
+		"DA EF	86FC",
+		"DA F0	86FD",
+		"DA F1	8704",
+		"DA F2	8705",
+		"DA F3	8707",
+		"DA F4	870B",
+		"DA F5	870E",
+		"DA F6	870F",
+		"DA F7	8710",
+		"DA F8	8713",
+		"DA F9	8714",
+		"DA FA	8719",
+		"DA FB	871E",
+		"DA FC	871F",
+		"DA FD	8721",
+		"DA FE	8723",
+		"DB A1	8728",
+		"DB A2	872E",
+		"DB A3	872F",
+		"DB A4	8731",
+		"DB A5	8732",
+		"DB A6	8739",
+		"DB A7	873A",
+		"DB A8	873C",
+		"DB A9	873D",
+		"DB AA	873E",
+		"DB AB	8740",
+		"DB AC	8743",
+		"DB AD	8745",
+		"DB AE	874D",
+		"DB AF	8758",
+		"DB B0	875D",
+		"DB B1	8761",
+		"DB B2	8764",
+		"DB B3	8765",
+		"DB B4	876F",
+		"DB B5	8771",
+		"DB B6	8772",
+		"DB B7	877B",
+		"DB B8	8783",
+		"DB B9	8784",
+		"DB BA	8785",
+		"DB BB	8786",
+		"DB BC	8787",
+		"DB BD	8788",
+		"DB BE	8789",
+		"DB BF	878B",
+		"DB C0	878C",
+		"DB C1	8790",
+		"DB C2	8793",
+		"DB C3	8795",
+		"DB C4	8797",
+		"DB C5	8798",
+		"DB C6	8799",
+		"DB C7	879E",
+		"DB C8	87A0",
+		"DB C9	87A3",
+		"DB CA	87A7",
+		"DB CB	87AC",
+		"DB CC	87AD",
+		"DB CD	87AE",
+		"DB CE	87B1",
+		"DB CF	87B5",
+		"DB D0	87BE",
+		"DB D1	87BF",
+		"DB D2	87C1",
+		"DB D3	87C8",
+		"DB D4	87C9",
+		"DB D5	87CA",
+		"DB D6	87CE",
+		"DB D7	87D5",
+		"DB D8	87D6",
+		"DB D9	87D9",
+		"DB DA	87DA",
+		"DB DB	87DC",
+		"DB DC	87DF",
+		"DB DD	87E2",
+		"DB DE	87E3",
+		"DB DF	87E4",
+		"DB E0	87EA",
+		"DB E1	87EB",
+		"DB E2	87ED",
+		"DB E3	87F1",
+		"DB E4	87F3",
+		"DB E5	87F8",
+		"DB E6	87FA",
+		"DB E7	87FF",
+		"DB E8	8801",
+		"DB E9	8803",
+		"DB EA	8806",
+		"DB EB	8809",
+		"DB EC	880A",
+		"DB ED	880B",
+		"DB EE	8810",
+		"DB F0	8812",
+		"DB F1	8813",
+		"DB F2	8814",
+		"DB F3	8818",
+		"DB EF	8819",
+		"DB F4	881A",
+		"DB F5	881B",
+		"DB F6	881C",
+		"DB F7	881E",
+		"DB F8	881F",
+		"DB F9	8828",
+		"DB FA	882D",
+		"DB FB	882E",
+		"DB FC	8830",
+		"DB FD	8832",
+		"DB FE	8835",
+		"DC A1	883A",
+		"DC A2	883C",
+		"DC A3	8841",
+		"DC A4	8843",
+		"DC A5	8845",
+		"DC A6	8848",
+		"DC A7	8849",
+		"DC A8	884A",
+		"DC A9	884B",
+		"DC AA	884E",
+		"DC AB	8851",
+		"DC AC	8855",
+		"DC AD	8856",
+		"DC AE	8858",
+		"DC AF	885A",
+		"DC B0	885C",
+		"DC B1	885F",
+		"DC B2	8860",
+		"DC B3	8864",
+		"DC B4	8869",
+		"DC B5	8871",
+		"DC B6	8879",
+		"DC B7	887B",
+		"DC B8	8880",
+		"DC B9	8898",
+		"DC BA	889A",
+		"DC BB	889B",
+		"DC BC	889C",
+		"DC BD	889F",
+		"DC BE	88A0",
+		"DC BF	88A8",
+		"DC C0	88AA",
+		"DC C1	88BA",
+		"DC C2	88BD",
+		"DC C3	88BE",
+		"DC C4	88C0",
+		"DC C5	88CA",
+		"DC C6	88CB",
+		"DC C7	88CC",
+		"DC C8	88CD",
+		"DC C9	88CE",
+		"DC CA	88D1",
+		"DC CB	88D2",
+		"DC CC	88D3",
+		"DC CD	88DB",
+		"DC CE	88DE",
+		"DC CF	88E7",
+		"DC D0	88EF",
+		"DC D1	88F0",
+		"DC D2	88F1",
+		"DC D3	88F5",
+		"DC D4	88F7",
+		"DC D5	8901",
+		"DC D6	8906",
+		"DC D7	890D",
+		"DC D8	890E",
+		"DC D9	890F",
+		"DC DA	8915",
+		"DC DB	8916",
+		"DC DC	8918",
+		"DC DD	8919",
+		"DC DE	891A",
+		"DC DF	891C",
+		"DC E0	8920",
+		"DC E1	8926",
+		"DC E2	8927",
+		"DC E3	8928",
+		"DC E4	8930",
+		"DC E5	8931",
+		"DC E6	8932",
+		"DC E7	8935",
+		"DC E8	8939",
+		"DC E9	893A",
+		"DC EA	893E",
+		"DC EB	8940",
+		"DC EC	8942",
+		"DC ED	8945",
+		"DC EE	8946",
+		"DC EF	8949",
+		"DC F0	894F",
+		"DC F1	8952",
+		"DC F2	8957",
+		"DC F3	895A",
+		"DC F4	895B",
+		"DC F5	895C",
+		"DC F6	8961",
+		"DC F7	8962",
+		"DC F8	8963",
+		"DC F9	896B",
+		"DC FA	896E",
+		"DC FB	8970",
+		"DC FC	8973",
+		"DC FD	8975",
+		"DC FE	897A",
+		"DD A1	897B",
+		"DD A2	897C",
+		"DD A3	897D",
+		"DD A4	8989",
+		"DD A5	898D",
+		"DD A6	8990",
+		"DD A7	8994",
+		"DD A8	8995",
+		"DD A9	899B",
+		"DD AA	899C",
+		"DD AB	899F",
+		"DD AC	89A0",
+		"DD AD	89A5",
+		"DD AE	89B0",
+		"DD AF	89B4",
+		"DD B0	89B5",
+		"DD B1	89B6",
+		"DD B2	89B7",
+		"DD B3	89BC",
+		"DD B4	89D4",
+		"DD B5	89D5",
+		"DD B6	89D6",
+		"DD B7	89D7",
+		"DD B8	89D8",
+		"DD B9	89E5",
+		"DD BA	89E9",
+		"DD BB	89EB",
+		"DD BC	89ED",
+		"DD BD	89F1",
+		"DD BE	89F3",
+		"DD BF	89F6",
+		"DD C0	89F9",
+		"DD C1	89FD",
+		"DD C2	89FF",
+		"DD C3	8A04",
+		"DD C4	8A05",
+		"DD C5	8A07",
+		"DD C6	8A0F",
+		"DD C7	8A11",
+		"DD C8	8A12",
+		"DD C9	8A14",
+		"DD CA	8A15",
+		"DD CB	8A1E",
+		"DD CC	8A20",
+		"DD CD	8A22",
+		"DD CE	8A24",
+		"DD CF	8A26",
+		"DD D0	8A2B",
+		"DD D1	8A2C",
+		"DD D2	8A2F",
+		"DD D3	8A35",
+		"DD D4	8A37",
+		"DD D5	8A3D",
+		"DD D6	8A3E",
+		"DD D7	8A40",
+		"DD D8	8A43",
+		"DD D9	8A45",
+		"DD DA	8A47",
+		"DD DB	8A49",
+		"DD DC	8A4D",
+		"DD DD	8A4E",
+		"DD DE	8A53",
+		"DD DF	8A56",
+		"DD E0	8A57",
+		"DD E1	8A58",
+		"DD E2	8A5C",
+		"DD E3	8A5D",
+		"DD E4	8A61",
+		"DD E5	8A65",
+		"DD E6	8A67",
+		"DD E7	8A75",
+		"DD E8	8A76",
+		"DD E9	8A77",
+		"DD EA	8A79",
+		"DD EB	8A7A",
+		"DD EC	8A7B",
+		"DD ED	8A7E",
+		"DD EE	8A7F",
+		"DD EF	8A80",
+		"DD F0	8A83",
+		"DD F1	8A86",
+		"DD F2	8A8B",
+		"DD F3	8A8F",
+		"DD F4	8A90",
+		"DD F5	8A92",
+		"DD F6	8A96",
+		"DD F7	8A97",
+		"DD F8	8A99",
+		"DD F9	8A9F",
+		"DD FA	8AA7",
+		"DD FB	8AA9",
+		"DD FC	8AAE",
+		"DD FD	8AAF",
+		"DD FE	8AB3",
+		"DE A1	8AB6",
+		"DE A2	8AB7",
+		"DE A3	8ABB",
+		"DE A4	8ABE",
+		"DE A5	8AC3",
+		"DE A6	8AC6",
+		"DE A7	8AC8",
+		"DE A8	8AC9",
+		"DE A9	8ACA",
+		"DE AA	8AD1",
+		"DE AB	8AD3",
+		"DE AC	8AD4",
+		"DE AD	8AD5",
+		"DE AE	8AD7",
+		"DE AF	8ADD",
+		"DE B0	8ADF",
+		"DE B1	8AEC",
+		"DE B2	8AF0",
+		"DE B3	8AF4",
+		"DE B4	8AF5",
+		"DE B5	8AF6",
+		"DE B6	8AFC",
+		"DE B7	8AFF",
+		"DE B8	8B05",
+		"DE B9	8B06",
+		"DE BF	8B0A",
+		"DE BA	8B0B",
+		"DE BB	8B11",
+		"DE BC	8B1C",
+		"DE BD	8B1E",
+		"DE BE	8B1F",
+		"DE C0	8B2D",
+		"DE C1	8B30",
+		"DE C2	8B37",
+		"DE C3	8B3C",
+		"DE C4	8B42",
+		"DE C5	8B43",
+		"DE C6	8B44",
+		"DE C7	8B45",
+		"DE C8	8B46",
+		"DE C9	8B48",
+		"DE CE	8B4D",
+		"DE CA	8B52",
+		"DE CB	8B53",
+		"DE CC	8B54",
+		"DE CD	8B59",
+		"DE CF	8B5E",
+		"DE D0	8B63",
+		"DE D1	8B6D",
+		"DE D2	8B76",
+		"DE D3	8B78",
+		"DE D4	8B79",
+		"DE D5	8B7C",
+		"DE D6	8B7E",
+		"DE D7	8B81",
+		"DE D8	8B84",
+		"DE D9	8B85",
+		"DE DA	8B8B",
+		"DE DB	8B8D",
+		"DE DC	8B8F",
+		"DE DD	8B94",
+		"DE DE	8B95",
+		"DE DF	8B9C",
+		"DE E0	8B9E",
+		"DE E1	8B9F",
+		"DE E2	8C38",
+		"DE E3	8C39",
+		"DE E4	8C3D",
+		"DE E5	8C3E",
+		"DE E6	8C45",
+		"DE E7	8C47",
+		"DE E8	8C49",
+		"DE E9	8C4B",
+		"DE EA	8C4F",
+		"DE EB	8C51",
+		"DE EC	8C53",
+		"DE ED	8C54",
+		"DE EE	8C57",
+		"DE EF	8C58",
+		"DE F2	8C59",
+		"DE F0	8C5B",
+		"DE F1	8C5D",
+		"DE F3	8C63",
+		"DE F4	8C64",
+		"DE F5	8C66",
+		"DE F6	8C68",
+		"DE F7	8C69",
+		"DE F8	8C6D",
+		"DE F9	8C73",
+		"DE FA	8C75",
+		"DE FB	8C76",
+		"DE FC	8C7B",
+		"DE FD	8C7E",
+		"DE FE	8C86",
+		"DF A1	8C87",
+		"DF A2	8C8B",
+		"DF A3	8C90",
+		"DF A4	8C92",
+		"DF A5	8C93",
+		"DF A6	8C99",
+		"DF A7	8C9B",
+		"DF A8	8C9C",
+		"DF A9	8CA4",
+		"DF AA	8CB9",
+		"DF AB	8CBA",
+		"DF AC	8CC5",
+		"DF AD	8CC6",
+		"DF AE	8CC9",
+		"DF AF	8CCB",
+		"DF B0	8CCF",
+		"DF B2	8CD5",
+		"DF B1	8CD6",
+		"DF B3	8CD9",
+		"DF B4	8CDD",
+		"DF B5	8CE1",
+		"DF B6	8CE8",
+		"DF B7	8CEC",
+		"DF B8	8CEF",
+		"DF B9	8CF0",
+		"DF BA	8CF2",
+		"DF BB	8CF5",
+		"DF BC	8CF7",
+		"DF BD	8CF8",
+		"DF BE	8CFE",
+		"DF BF	8CFF",
+		"DF C0	8D01",
+		"DF C1	8D03",
+		"DF C2	8D09",
+		"DF C3	8D12",
+		"DF C4	8D17",
+		"DF C5	8D1B",
+		"DF C6	8D65",
+		"DF C7	8D69",
+		"DF C8	8D6C",
+		"DF C9	8D6E",
+		"DF CA	8D7F",
+		"DF CB	8D82",
+		"DF CC	8D84",
+		"DF CD	8D88",
+		"DF CE	8D8D",
+		"DF CF	8D90",
+		"DF D0	8D91",
+		"DF D1	8D95",
+		"DF D2	8D9E",
+		"DF D3	8D9F",
+		"DF D4	8DA0",
+		"DF D5	8DA6",
+		"DF D6	8DAB",
+		"DF D7	8DAC",
+		"DF D8	8DAF",
+		"DF D9	8DB2",
+		"DF DA	8DB5",
+		"DF DB	8DB7",
+		"DF DC	8DB9",
+		"DF DD	8DBB",
+		"DF EF	8DBC",
+		"DF DE	8DC0",
+		"DF DF	8DC5",
+		"DF E0	8DC6",
+		"DF E1	8DC7",
+		"DF E2	8DC8",
+		"DF E3	8DCA",
+		"DF E4	8DCE",
+		"DF E5	8DD1",
+		"DF E6	8DD4",
+		"DF E7	8DD5",
+		"DF E8	8DD7",
+		"DF E9	8DD9",
+		"DF EA	8DE4",
+		"DF EB	8DE5",
+		"DF EC	8DE7",
+		"DF ED	8DEC",
+		"DF EE	8DF0",
+		"DF F0	8DF1",
+		"DF F1	8DF2",
+		"DF F2	8DF4",
+		"DF F3	8DFD",
+		"DF F4	8E01",
+		"DF F5	8E04",
+		"DF F6	8E05",
+		"DF F7	8E06",
+		"DF F8	8E0B",
+		"DF F9	8E11",
+		"DF FA	8E14",
+		"DF FB	8E16",
+		"DF FC	8E20",
+		"DF FD	8E21",
+		"DF FE	8E22",
+		"E0 A1	8E23",
+		"E0 A2	8E26",
+		"E0 A3	8E27",
+		"E0 A4	8E31",
+		"E0 A5	8E33",
+		"E0 A6	8E36",
+		"E0 A7	8E37",
+		"E0 A8	8E38",
+		"E0 A9	8E39",
+		"E0 AA	8E3D",
+		"E0 AB	8E40",
+		"E0 AC	8E41",
+		"E0 AD	8E4B",
+		"E0 AE	8E4D",
+		"E0 AF	8E4E",
+		"E0 B0	8E4F",
+		"E0 B1	8E54",
+		"E0 B2	8E5B",
+		"E0 B3	8E5C",
+		"E0 B4	8E5D",
+		"E0 B5	8E5E",
+		"E0 B6	8E61",
+		"E0 B7	8E62",
+		"E0 B8	8E69",
+		"E0 B9	8E6C",
+		"E0 BA	8E6D",
+		"E0 BB	8E6F",
+		"E0 BC	8E70",
+		"E0 BD	8E71",
+		"E0 BE	8E79",
+		"E0 BF	8E7A",
+		"E0 C0	8E7B",
+		"E0 C1	8E82",
+		"E0 C2	8E83",
+		"E0 C3	8E89",
+		"E0 C4	8E90",
+		"E0 C5	8E92",
+		"E0 C6	8E95",
+		"E0 C7	8E9A",
+		"E0 C8	8E9B",
+		"E0 C9	8E9D",
+		"E0 CA	8E9E",
+		"E0 CB	8EA2",
+		"E0 CC	8EA7",
+		"E0 CD	8EA9",
+		"E0 CE	8EAD",
+		"E0 CF	8EAE",
+		"E0 D0	8EB3",
+		"E0 D1	8EB5",
+		"E0 D2	8EBA",
+		"E0 D3	8EBB",
+		"E0 D4	8EC0",
+		"E0 D5	8EC1",
+		"E0 D6	8EC3",
+		"E0 D7	8EC4",
+		"E0 D8	8EC7",
+		"E0 D9	8ECF",
+		"E0 DA	8ED1",
+		"E0 DB	8ED4",
+		"E0 DC	8EDC",
+		"E0 DD	8EE8",
+		"E0 E4	8EED",
+		"E0 DE	8EEE",
+		"E0 DF	8EF0",
+		"E0 E0	8EF1",
+		"E0 E1	8EF7",
+		"E0 E2	8EF9",
+		"E0 E3	8EFA",
+		"E0 E5	8F00",
+		"E0 E6	8F02",
+		"E0 E7	8F07",
+		"E0 E8	8F08",
+		"E0 E9	8F0F",
+		"E0 EA	8F10",
+		"E0 EB	8F16",
+		"E0 EC	8F17",
+		"E0 ED	8F18",
+		"E0 EE	8F1E",
+		"E0 EF	8F20",
+		"E0 F0	8F21",
+		"E0 F1	8F23",
+		"E0 F2	8F25",
+		"E0 F3	8F27",
+		"E0 F4	8F28",
+		"E0 F5	8F2C",
+		"E0 F6	8F2D",
+		"E0 F7	8F2E",
+		"E0 F8	8F34",
+		"E0 F9	8F35",
+		"E0 FA	8F36",
+		"E0 FB	8F37",
+		"E0 FC	8F3A",
+		"E0 FD	8F40",
+		"E0 FE	8F41",
+		"E1 A1	8F43",
+		"E1 A2	8F47",
+		"E1 A3	8F4F",
+		"E1 A4	8F51",
+		"E1 A5	8F52",
+		"E1 A6	8F53",
+		"E1 A7	8F54",
+		"E1 A8	8F55",
+		"E1 A9	8F58",
+		"E1 AA	8F5D",
+		"E1 AB	8F5E",
+		"E1 AC	8F65",
+		"E1 AD	8F9D",
+		"E1 AE	8FA0",
+		"E1 AF	8FA1",
+		"E1 B0	8FA4",
+		"E1 B1	8FA5",
+		"E1 B2	8FA6",
+		"E1 B3	8FB5",
+		"E1 B4	8FB6",
+		"E1 B5	8FB8",
+		"E1 B6	8FBE",
+		"E1 B7	8FC0",
+		"E1 B8	8FC1",
+		"E1 B9	8FC6",
+		"E1 BA	8FCA",
+		"E1 BB	8FCB",
+		"E1 BC	8FCD",
+		"E1 BD	8FD0",
+		"E1 BE	8FD2",
+		"E1 BF	8FD3",
+		"E1 C0	8FD5",
+		"E1 C1	8FE0",
+		"E1 C2	8FE3",
+		"E1 C3	8FE4",
+		"E1 C4	8FE8",
+		"E1 C5	8FEE",
+		"E1 C6	8FF1",
+		"E1 C7	8FF5",
+		"E1 C8	8FF6",
+		"E1 C9	8FFB",
+		"E1 CA	8FFE",
+		"E1 CB	9002",
+		"E1 CC	9004",
+		"E1 CD	9008",
+		"E1 CE	900C",
+		"E1 CF	9018",
+		"E1 D0	901B",
+		"E1 D1	9028",
+		"E1 D2	9029",
+		"E1 D4	902A",
+		"E1 D5	902C",
+		"E1 D6	902D",
+		"E1 D3	902F",
+		"E1 D7	9033",
+		"E1 D8	9034",
+		"E1 D9	9037",
+		"E1 DA	903F",
+		"E1 DB	9043",
+		"E1 DC	9044",
+		"E1 DD	904C",
+		"E1 DE	905B",
+		"E1 DF	905D",
+		"E1 E0	9062",
+		"E1 E1	9066",
+		"E1 E2	9067",
+		"E1 E3	906C",
+		"E1 E4	9070",
+		"E1 E5	9074",
+		"E1 E6	9079",
+		"E1 E7	9085",
+		"E1 E8	9088",
+		"E1 E9	908B",
+		"E1 EA	908C",
+		"E1 EB	908E",
+		"E1 EC	9090",
+		"E1 ED	9095",
+		"E1 EE	9097",
+		"E1 EF	9098",
+		"E1 F0	9099",
+		"E1 F1	909B",
+		"E1 F2	90A0",
+		"E1 F3	90A1",
+		"E1 F4	90A2",
+		"E1 F5	90A5",
+		"E1 F6	90B0",
+		"E1 F7	90B2",
+		"E1 F8	90B3",
+		"E1 F9	90B4",
+		"E1 FA	90B6",
+		"E1 FB	90BD",
+		"E1 FD	90BE",
+		"E1 FE	90C3",
+		"E2 A1	90C4",
+		"E2 A2	90C5",
+		"E2 A3	90C7",
+		"E2 A4	90C8",
+		"E1 FC	90CC",
+		"E2 AD	90D2",
+		"E2 A5	90D5",
+		"E2 A6	90D7",
+		"E2 A7	90D8",
+		"E2 A8	90D9",
+		"E2 A9	90DC",
+		"E2 AA	90DD",
+		"E2 AB	90DF",
+		"E2 AC	90E5",
+		"E2 AF	90EB",
+		"E2 B0	90EF",
+		"E2 B1	90F0",
+		"E2 B2	90F4",
+		"E2 AE	90F6",
+		"E2 B3	90FE",
+		"E2 B4	90FF",
+		"E2 B5	9100",
+		"E2 B6	9104",
+		"E2 B7	9105",
+		"E2 B8	9106",
+		"E2 B9	9108",
+		"E2 BA	910D",
+		"E2 BB	9110",
+		"E2 BC	9114",
+		"E2 BD	9116",
+		"E2 BE	9117",
+		"E2 BF	9118",
+		"E2 C0	911A",
+		"E2 C1	911C",
+		"E2 C2	911E",
+		"E2 C3	9120",
+		"E2 C5	9122",
+		"E2 C6	9123",
+		"E2 C4	9125",
+		"E2 C7	9127",
+		"E2 C8	9129",
+		"E2 C9	912E",
+		"E2 CA	912F",
+		"E2 CB	9131",
+		"E2 CC	9134",
+		"E2 CD	9136",
+		"E2 CE	9137",
+		"E2 CF	9139",
+		"E2 D0	913A",
+		"E2 D1	913C",
+		"E2 D2	913D",
+		"E2 D3	9143",
+		"E2 D4	9147",
+		"E2 D5	9148",
+		"E2 D6	914F",
+		"E2 D7	9153",
+		"E2 D8	9157",
+		"E2 D9	9159",
+		"E2 DA	915A",
+		"E2 DB	915B",
+		"E2 DC	9161",
+		"E2 DD	9164",
+		"E2 DE	9167",
+		"E2 DF	916D",
+		"E2 E0	9174",
+		"E2 E1	9179",
+		"E2 E2	917A",
+		"E2 E3	917B",
+		"E2 E4	9181",
+		"E2 E5	9183",
+		"E2 E6	9185",
+		"E2 E7	9186",
+		"E2 E8	918A",
+		"E2 E9	918E",
+		"E2 EA	9191",
+		"E2 EB	9193",
+		"E2 EC	9194",
+		"E2 ED	9195",
+		"E2 EE	9198",
+		"E2 EF	919E",
+		"E2 F0	91A1",
+		"E2 F1	91A6",
+		"E2 F2	91A8",
+		"E2 F3	91AC",
+		"E2 F4	91AD",
+		"E2 F5	91AE",
+		"E2 F6	91B0",
+		"E2 F7	91B1",
+		"E2 F8	91B2",
+		"E2 F9	91B3",
+		"E2 FA	91B6",
+		"E2 FB	91BB",
+		"E2 FC	91BC",
+		"E2 FD	91BD",
+		"E2 FE	91BF",
+		"E3 A1	91C2",
+		"E3 A2	91C3",
+		"E3 A3	91C5",
+		"E3 A4	91D3",
+		"E3 A5	91D4",
+		"E3 A6	91D7",
+		"E3 A7	91D9",
+		"E3 A8	91DA",
+		"E3 A9	91DE",
+		"E3 AA	91E4",
+		"E3 AB	91E5",
+		"E3 AC	91E9",
+		"E3 AD	91EA",
+		"E3 AE	91EC",
+		"E3 AF	91ED",
+		"E3 B0	91EE",
+		"E3 B1	91EF",
+		"E3 B2	91F0",
+		"E3 B3	91F1",
+		"E3 B4	91F7",
+		"E3 B5	91F9",
+		"E3 B6	91FB",
+		"E3 B7	91FD",
+		"E3 B8	9200",
+		"E3 B9	9201",
+		"E3 BA	9204",
+		"E3 BB	9205",
+		"E3 BC	9206",
+		"E3 BD	9207",
+		"E3 BE	9209",
+		"E3 BF	920A",
+		"E3 C0	920C",
+		"E3 C1	9210",
+		"E3 C2	9212",
+		"E3 C3	9213",
+		"E3 C4	9216",
+		"E3 C5	9218",
+		"E3 C6	921C",
+		"E3 C7	921D",
+		"E3 C8	9223",
+		"E3 C9	9224",
+		"E3 CA	9225",
+		"E3 CB	9226",
+		"E3 CC	9228",
+		"E3 CD	922E",
+		"E3 CE	922F",
+		"E3 CF	9230",
+		"E3 D0	9233",
+		"E3 D1	9235",
+		"E3 D2	9236",
+		"E3 D3	9238",
+		"E3 D4	9239",
+		"E3 D5	923A",
+		"E3 D6	923C",
+		"E3 D7	923E",
+		"E3 D8	9240",
+		"E3 D9	9242",
+		"E3 DA	9243",
+		"E3 DB	9246",
+		"E3 DC	9247",
+		"E3 DD	924A",
+		"E3 DE	924D",
+		"E3 DF	924E",
+		"E3 E0	924F",
+		"E3 E1	9251",
+		"E3 E2	9258",
+		"E3 E3	9259",
+		"E3 E4	925C",
+		"E3 E5	925D",
+		"E3 E6	9260",
+		"E3 E7	9261",
+		"E3 E8	9265",
+		"E3 E9	9267",
+		"E3 EA	9268",
+		"E3 EB	9269",
+		"E3 EC	926E",
+		"E3 ED	926F",
+		"E3 EE	9270",
+		"E3 EF	9275",
+		"E3 F0	9276",
+		"E3 F1	9277",
+		"E3 F2	9278",
+		"E3 F3	9279",
+		"E3 F4	927B",
+		"E3 F5	927C",
+		"E3 F6	927D",
+		"E3 F7	927F",
+		"E3 F8	9288",
+		"E3 F9	9289",
+		"E3 FA	928A",
+		"E3 FB	928D",
+		"E3 FC	928E",
+		"E3 FD	9292",
+		"E3 FE	9297",
+		"E4 A1	9299",
+		"E4 A2	929F",
+		"E4 A3	92A0",
+		"E4 A4	92A4",
+		"E4 A5	92A5",
+		"E4 A6	92A7",
+		"E4 A7	92A8",
+		"E4 A8	92AB",
+		"E4 A9	92AF",
+		"E4 AA	92B2",
+		"E4 AB	92B6",
+		"E4 AC	92B8",
+		"E4 AD	92BA",
+		"E4 AE	92BB",
+		"E4 AF	92BC",
+		"E4 B0	92BD",
+		"E4 B1	92BF",
+		"E4 B2	92C0",
+		"E4 B3	92C1",
+		"E4 B4	92C2",
+		"E4 B5	92C3",
+		"E4 B6	92C5",
+		"E4 B7	92C6",
+		"E4 B8	92C7",
+		"E4 B9	92C8",
+		"E4 BA	92CB",
+		"E4 BB	92CC",
+		"E4 BC	92CD",
+		"E4 BD	92CE",
+		"E4 BE	92D0",
+		"E4 BF	92D3",
+		"E4 C0	92D5",
+		"E4 C1	92D7",
+		"E4 C2	92D8",
+		"E4 C3	92D9",
+		"E4 C4	92DC",
+		"E4 C5	92DD",
+		"E4 C6	92DF",
+		"E4 C7	92E0",
+		"E4 C8	92E1",
+		"E4 C9	92E3",
+		"E4 CA	92E5",
+		"E4 CB	92E7",
+		"E4 CC	92E8",
+		"E4 CD	92EC",
+		"E4 CE	92EE",
+		"E4 CF	92F0",
+		"E4 D0	92F9",
+		"E4 D1	92FB",
+		"E4 D2	92FF",
+		"E4 D3	9300",
+		"E4 D4	9302",
+		"E4 D5	9308",
+		"E4 D6	930D",
+		"E4 D7	9311",
+		"E4 D8	9314",
+		"E4 D9	9315",
+		"E4 DA	931C",
+		"E4 DB	931D",
+		"E4 DC	931E",
+		"E4 DD	931F",
+		"E4 DE	9321",
+		"E4 DF	9324",
+		"E4 E0	9325",
+		"E4 E1	9327",
+		"E4 E2	9329",
+		"E4 E3	932A",
+		"E4 E4	9333",
+		"E4 E5	9334",
+		"E4 E6	9336",
+		"E4 E7	9337",
+		"E4 E8	9347",
+		"E4 E9	9348",
+		"E4 EA	9349",
+		"E4 EB	9350",
+		"E4 EC	9351",
+		"E4 ED	9352",
+		"E4 EE	9355",
+		"E4 EF	9357",
+		"E4 F0	9358",
+		"E4 F1	935A",
+		"E4 F2	935E",
+		"E4 F3	9364",
+		"E4 F4	9365",
+		"E4 F5	9367",
+		"E4 F6	9369",
+		"E4 F7	936A",
+		"E4 F8	936D",
+		"E4 F9	936F",
+		"E4 FA	9370",
+		"E4 FB	9371",
+		"E4 FC	9373",
+		"E4 FD	9374",
+		"E4 FE	9376",
+		"E5 A1	937A",
+		"E5 A2	937D",
+		"E5 A3	937F",
+		"E5 A4	9380",
+		"E5 A5	9381",
+		"E5 A6	9382",
+		"E5 A7	9388",
+		"E5 A8	938A",
+		"E5 A9	938B",
+		"E5 AA	938D",
+		"E5 AB	938F",
+		"E5 AC	9392",
+		"E5 AD	9395",
+		"E5 AE	9398",
+		"E5 AF	939B",
+		"E5 B0	939E",
+		"E5 B1	93A1",
+		"E5 B2	93A3",
+		"E5 B3	93A4",
+		"E5 B4	93A6",
+		"E5 B5	93A8",
+		"E5 BB	93A9",
+		"E5 B6	93AB",
+		"E5 B7	93B4",
+		"E5 B8	93B5",
+		"E5 B9	93B6",
+		"E5 BA	93BA",
+		"E5 BC	93C1",
+		"E5 BD	93C4",
+		"E5 BE	93C5",
+		"E5 BF	93C6",
+		"E5 C0	93C7",
+		"E5 C1	93C9",
+		"E5 C2	93CA",
+		"E5 C3	93CB",
+		"E5 C4	93CC",
+		"E5 C5	93CD",
+		"E5 C6	93D3",
+		"E5 C7	93D9",
+		"E5 C8	93DC",
+		"E5 C9	93DE",
+		"E5 CA	93DF",
+		"E5 CB	93E2",
+		"E5 CC	93E6",
+		"E5 CD	93E7",
+		"E5 CF	93F7",
+		"E5 D0	93F8",
+		"E5 CE	93F9",
+		"E5 D1	93FA",
+		"E5 D2	93FB",
+		"E5 D3	93FD",
+		"E5 D4	9401",
+		"E5 D5	9402",
+		"E5 D6	9404",
+		"E5 D7	9408",
+		"E5 D8	9409",
+		"E5 D9	940D",
+		"E5 DA	940E",
+		"E5 DB	940F",
+		"E5 DC	9415",
+		"E5 DD	9416",
+		"E5 DE	9417",
+		"E5 DF	941F",
+		"E5 E0	942E",
+		"E5 E1	942F",
+		"E5 E2	9431",
+		"E5 E3	9432",
+		"E5 E4	9433",
+		"E5 E5	9434",
+		"E5 E6	943B",
+		"E5 E8	943D",
+		"E5 E7	943F",
+		"E5 E9	9443",
+		"E5 EA	9445",
+		"E5 EB	9448",
+		"E5 EC	944A",
+		"E5 ED	944C",
+		"E5 EE	9455",
+		"E5 EF	9459",
+		"E5 F0	945C",
+		"E5 F1	945F",
+		"E5 F2	9461",
+		"E5 F3	9463",
+		"E5 F4	9468",
+		"E5 F5	946B",
+		"E5 F6	946D",
+		"E5 F7	946E",
+		"E5 F8	946F",
+		"E5 F9	9471",
+		"E5 FA	9472",
+		"E5 FC	9483",
+		"E5 FB	9484",
+		"E5 FD	9578",
+		"E5 FE	9579",
+		"E6 A1	957E",
+		"E6 A2	9584",
+		"E6 A3	9588",
+		"E6 A4	958C",
+		"E6 A5	958D",
+		"E6 A6	958E",
+		"E6 A7	959D",
+		"E6 A8	959E",
+		"E6 A9	959F",
+		"E6 AA	95A1",
+		"E6 AB	95A6",
+		"E6 AC	95A9",
+		"E6 AD	95AB",
+		"E6 AE	95AC",
+		"E6 AF	95B4",
+		"E6 B0	95B6",
+		"E6 B1	95BA",
+		"E6 B2	95BD",
+		"E6 B3	95BF",
+		"E6 B4	95C6",
+		"E6 B5	95C8",
+		"E6 B6	95C9",
+		"E6 B7	95CB",
+		"E6 B8	95D0",
+		"E6 B9	95D1",
+		"E6 BA	95D2",
+		"E6 BB	95D3",
+		"E6 BC	95D9",
+		"E6 BD	95DA",
+		"E6 BE	95DD",
+		"E6 BF	95DE",
+		"E6 C0	95DF",
+		"E6 C1	95E0",
+		"E6 C2	95E4",
+		"E6 C3	95E6",
+		"E6 C4	961D",
+		"E6 C5	961E",
+		"E6 C6	9622",
+		"E6 C7	9624",
+		"E6 C8	9625",
+		"E6 C9	9626",
+		"E6 CA	962C",
+		"E6 CB	9631",
+		"E6 CC	9633",
+		"E6 CD	9637",
+		"E6 CE	9638",
+		"E6 CF	9639",
+		"E6 D0	963A",
+		"E6 D1	963C",
+		"E6 D2	963D",
+		"E6 D3	9641",
+		"E6 D4	9652",
+		"E6 D5	9654",
+		"E6 D6	9656",
+		"E6 D7	9657",
+		"E6 D8	9658",
+		"E6 D9	9661",
+		"E6 DA	966E",
+		"E6 DB	9674",
+		"E6 DC	967B",
+		"E6 DD	967C",
+		"E6 DE	967E",
+		"E6 DF	967F",
+		"E6 E0	9681",
+		"E6 E1	9682",
+		"E6 E2	9683",
+		"E6 E3	9684",
+		"E6 E4	9689",
+		"E6 E5	9691",
+		"E6 E6	9696",
+		"E6 E7	969A",
+		"E6 E8	969D",
+		"E6 E9	969F",
+		"E6 EA	96A4",
+		"E6 EB	96A5",
+		"E6 EC	96A6",
+		"E6 ED	96A9",
+		"E6 EE	96AE",
+		"E6 EF	96AF",
+		"E6 F0	96B3",
+		"E6 F1	96BA",
+		"E6 F2	96CA",
+		"E6 F3	96D2",
+		"E6 F5	96D8",
+		"E6 F6	96DA",
+		"E6 F7	96DD",
+		"E6 F8	96DE",
+		"E6 F9	96DF",
+		"E6 FA	96E9",
+		"E6 FB	96EF",
+		"E6 FC	96F1",
+		"E6 FD	96FA",
+		"E6 FE	9702",
+		"E7 A1	9703",
+		"E7 A2	9705",
+		"E7 A3	9709",
+		"E7 A4	971A",
+		"E7 A5	971B",
+		"E7 A6	971D",
+		"E7 A7	9721",
+		"E7 A8	9722",
+		"E7 A9	9723",
+		"E7 AA	9728",
+		"E7 AB	9731",
+		"E7 AC	9733",
+		"E7 AD	9741",
+		"E7 AE	9743",
+		"E7 AF	974A",
+		"E7 B0	974E",
+		"E7 B1	974F",
+		"E7 B2	9755",
+		"E7 B3	9757",
+		"E7 B4	9758",
+		"E7 B5	975A",
+		"E7 B6	975B",
+		"E7 B7	9763",
+		"E7 B8	9767",
+		"E7 B9	976A",
+		"E7 BA	976E",
+		"E7 BB	9773",
+		"E7 BC	9776",
+		"E7 BD	9777",
+		"E7 BE	9778",
+		"E7 BF	977B",
+		"E7 C0	977D",
+		"E7 C1	977F",
+		"E7 C2	9780",
+		"E7 C3	9789",
+		"E7 C4	9795",
+		"E7 C5	9796",
+		"E7 C6	9797",
+		"E7 C7	9799",
+		"E7 C8	979A",
+		"E7 C9	979E",
+		"E7 CA	979F",
+		"E7 CB	97A2",
+		"E7 CC	97AC",
+		"E7 CD	97AE",
+		"E7 CE	97B1",
+		"E7 CF	97B2",
+		"E7 D0	97B5",
+		"E7 D1	97B6",
+		"E7 D2	97B8",
+		"E7 D3	97B9",
+		"E7 D4	97BA",
+		"E7 D5	97BC",
+		"E7 D6	97BE",
+		"E7 D7	97BF",
+		"E7 D8	97C1",
+		"E7 D9	97C4",
+		"E7 DA	97C5",
+		"E7 DB	97C7",
+		"E7 DC	97C9",
+		"E7 DD	97CA",
+		"E7 DE	97CC",
+		"E7 DF	97CD",
+		"E7 E0	97CE",
+		"E7 E1	97D0",
+		"E7 E2	97D1",
+		"E7 E3	97D4",
+		"E7 E4	97D7",
+		"E7 E5	97D8",
+		"E7 E6	97D9",
+		"E7 EA	97DB",
+		"E7 E7	97DD",
+		"E7 E8	97DE",
+		"E7 E9	97E0",
+		"E7 EB	97E1",
+		"E7 EC	97E4",
+		"E7 ED	97EF",
+		"E7 EE	97F1",
+		"E7 EF	97F4",
+		"E7 F0	97F7",
+		"E7 F1	97F8",
+		"E7 F2	97FA",
+		"E7 F3	9807",
+		"E7 F4	980A",
+		"E7 F6	980D",
+		"E7 F7	980E",
+		"E7 F8	9814",
+		"E7 F9	9816",
+		"E7 F5	9819",
+		"E7 FA	981C",
+		"E7 FB	981E",
+		"E7 FC	9820",
+		"E7 FD	9823",
+		"E8 A8	9825",
+		"E7 FE	9826",
+		"E8 A1	982B",
+		"E8 A2	982E",
+		"E8 A3	982F",
+		"E8 A4	9830",
+		"E8 A5	9832",
+		"E8 A6	9833",
+		"E8 A7	9835",
+		"E8 A9	983E",
+		"E8 AA	9844",
+		"E8 AB	9847",
+		"E8 AC	984A",
+		"E8 AD	9851",
+		"E8 AE	9852",
+		"E8 AF	9853",
+		"E8 B0	9856",
+		"E8 B1	9857",
+		"E8 B2	9859",
+		"E8 B3	985A",
+		"E8 B4	9862",
+		"E8 B5	9863",
+		"E8 B6	9865",
+		"E8 B7	9866",
+		"E8 B8	986A",
+		"E8 B9	986C",
+		"E8 BA	98AB",
+		"E8 BB	98AD",
+		"E8 BC	98AE",
+		"E8 BD	98B0",
+		"E8 BE	98B4",
+		"E8 BF	98B7",
+		"E8 C0	98B8",
+		"E8 C1	98BA",
+		"E8 C2	98BB",
+		"E8 C3	98BF",
+		"E8 C4	98C2",
+		"E8 C5	98C5",
+		"E8 C6	98C8",
+		"E8 C7	98CC",
+		"E8 C8	98E1",
+		"E8 C9	98E3",
+		"E8 CA	98E5",
+		"E8 CB	98E6",
+		"E8 CC	98E7",
+		"E8 CD	98EA",
+		"E8 CE	98F3",
+		"E8 CF	98F6",
+		"E8 D0	9902",
+		"E8 D1	9907",
+		"E8 D2	9908",
+		"E8 D3	9911",
+		"E8 D4	9915",
+		"E8 D5	9916",
+		"E8 D6	9917",
+		"E8 D7	991A",
+		"E8 D8	991B",
+		"E8 D9	991C",
+		"E8 DA	991F",
+		"E8 DB	9922",
+		"E8 DC	9926",
+		"E8 DD	9927",
+		"E8 DE	992B",
+		"E8 DF	9931",
+		"E8 E0	9932",
+		"E8 E1	9933",
+		"E8 E2	9934",
+		"E8 E3	9935",
+		"E8 E4	9939",
+		"E8 E5	993A",
+		"E8 E6	993B",
+		"E8 E7	993C",
+		"E8 E8	9940",
+		"E8 E9	9941",
+		"E8 EA	9946",
+		"E8 EB	9947",
+		"E8 EC	9948",
+		"E8 ED	994D",
+		"E8 EE	994E",
+		"E8 EF	9954",
+		"E8 F0	9958",
+		"E8 F1	9959",
+		"E8 F2	995B",
+		"E8 F3	995C",
+		"E8 F4	995E",
+		"E8 F5	995F",
+		"E8 F6	9960",
+		"E8 F7	999B",
+		"E8 F8	999D",
+		"E8 F9	999F",
+		"E8 FA	99A6",
+		"E8 FB	99B0",
+		"E8 FC	99B1",
+		"E8 FD	99B2",
+		"E8 FE	99B5",
+		"E9 A1	99B9",
+		"E9 A2	99BA",
+		"E9 A3	99BD",
+		"E9 A4	99BF",
+		"E9 A5	99C3",
+		"E9 A6	99C9",
+		"E9 A7	99D3",
+		"E9 A8	99D4",
+		"E9 A9	99D9",
+		"E9 AA	99DA",
+		"E9 AB	99DC",
+		"E9 AC	99DE",
+		"E9 AD	99E7",
+		"E9 AE	99EA",
+		"E9 AF	99EB",
+		"E9 B0	99EC",
+		"E9 B1	99F0",
+		"E9 B2	99F4",
+		"E9 B3	99F5",
+		"E9 B4	99F9",
+		"E9 B5	99FD",
+		"E9 B6	99FE",
+		"E9 B7	9A02",
+		"E9 B8	9A03",
+		"E9 B9	9A04",
+		"E9 BA	9A0B",
+		"E9 BB	9A0C",
+		"E9 BC	9A10",
+		"E9 BD	9A11",
+		"E9 BE	9A16",
+		"E9 BF	9A1E",
+		"E9 C0	9A20",
+		"E9 C1	9A22",
+		"E9 C2	9A23",
+		"E9 C3	9A24",
+		"E9 C4	9A27",
+		"E9 C5	9A2D",
+		"E9 C6	9A2E",
+		"E9 C7	9A33",
+		"E9 C8	9A35",
+		"E9 C9	9A36",
+		"E9 CA	9A38",
+		"E9 CC	9A41",
+		"E9 CD	9A44",
+		"E9 CB	9A47",
+		"E9 CE	9A4A",
+		"E9 CF	9A4B",
+		"E9 D0	9A4C",
+		"E9 D1	9A4E",
+		"E9 D2	9A51",
+		"E9 D3	9A54",
+		"E9 D4	9A56",
+		"E9 D5	9A5D",
+		"E9 D6	9AAA",
+		"E9 D7	9AAC",
+		"E9 D8	9AAE",
+		"E9 D9	9AAF",
+		"E9 DA	9AB2",
+		"E9 DB	9AB4",
+		"E9 DC	9AB5",
+		"E9 DD	9AB6",
+		"E9 DE	9AB9",
+		"E9 DF	9ABB",
+		"E9 E0	9ABE",
+		"E9 E1	9ABF",
+		"E9 E2	9AC1",
+		"E9 E3	9AC3",
+		"E9 E4	9AC6",
+		"E9 E5	9AC8",
+		"E9 E6	9ACE",
+		"E9 E7	9AD0",
+		"E9 E8	9AD2",
+		"E9 E9	9AD5",
+		"E9 EA	9AD6",
+		"E9 EB	9AD7",
+		"E9 EC	9ADB",
+		"E9 ED	9ADC",
+		"E9 EE	9AE0",
+		"E9 EF	9AE4",
+		"E9 F0	9AE5",
+		"E9 F1	9AE7",
+		"E9 F2	9AE9",
+		"E9 F3	9AEC",
+		"E9 F4	9AF2",
+		"E9 F5	9AF3",
+		"E9 F6	9AF5",
+		"E9 F7	9AF9",
+		"E9 F8	9AFA",
+		"E9 F9	9AFD",
+		"E9 FA	9AFF",
+		"E9 FB	9B00",
+		"E9 FC	9B01",
+		"E9 FD	9B02",
+		"E9 FE	9B03",
+		"EA A1	9B04",
+		"EA A2	9B05",
+		"EA A3	9B08",
+		"EA A4	9B09",
+		"EA A5	9B0B",
+		"EA A6	9B0C",
+		"EA A7	9B0D",
+		"EA A8	9B0E",
+		"EA A9	9B10",
+		"EA AA	9B12",
+		"EA AB	9B16",
+		"EA AC	9B19",
+		"EA AD	9B1B",
+		"EA AE	9B1C",
+		"EA AF	9B20",
+		"EA B0	9B26",
+		"EA B1	9B2B",
+		"EA B2	9B2D",
+		"EA B3	9B33",
+		"EA B4	9B34",
+		"EA B5	9B35",
+		"EA B6	9B37",
+		"EA B7	9B39",
+		"EA B8	9B3A",
+		"EA B9	9B3D",
+		"EA BA	9B48",
+		"EA BB	9B4B",
+		"EA BC	9B4C",
+		"EA BD	9B55",
+		"EA BE	9B56",
+		"EA BF	9B57",
+		"EA C0	9B5B",
+		"EA C1	9B5E",
+		"EA C2	9B61",
+		"EA C3	9B63",
+		"EA C4	9B65",
+		"EA C5	9B66",
+		"EA C6	9B68",
+		"EA C7	9B6A",
+		"EA C8	9B6B",
+		"EA C9	9B6C",
+		"EA CA	9B6D",
+		"EA CB	9B6E",
+		"EA CC	9B73",
+		"EA CD	9B75",
+		"EA CE	9B77",
+		"EA CF	9B78",
+		"EA D0	9B79",
+		"EA D1	9B7F",
+		"EA D2	9B80",
+		"EA D3	9B84",
+		"EA D4	9B85",
+		"EA D5	9B86",
+		"EA D6	9B87",
+		"EA D7	9B89",
+		"EA D8	9B8A",
+		"EA D9	9B8B",
+		"EA DA	9B8D",
+		"EA DB	9B8F",
+		"EA DC	9B90",
+		"EA DD	9B94",
+		"EA DE	9B9A",
+		"EA DF	9B9D",
+		"EA E0	9B9E",
+		"EA E1	9BA6",
+		"EA E2	9BA7",
+		"EA E3	9BA9",
+		"EA E4	9BAC",
+		"EA E5	9BB0",
+		"EA E6	9BB1",
+		"EA E7	9BB2",
+		"EA E8	9BB7",
+		"EA E9	9BB8",
+		"EA EA	9BBB",
+		"EA EB	9BBC",
+		"EA EC	9BBE",
+		"EA ED	9BBF",
+		"EA EE	9BC1",
+		"EA EF	9BC7",
+		"EA F0	9BC8",
+		"EA F1	9BCE",
+		"EA F2	9BD0",
+		"EA F3	9BD7",
+		"EA F4	9BD8",
+		"EA F5	9BDD",
+		"EA F6	9BDF",
+		"EA F7	9BE5",
+		"EA F8	9BE7",
+		"EA F9	9BEA",
+		"EA FA	9BEB",
+		"EA FB	9BEF",
+		"EA FC	9BF3",
+		"EA FD	9BF7",
+		"EA FE	9BF8",
+		"EB A1	9BF9",
+		"EB A2	9BFA",
+		"EB A3	9BFD",
+		"EB A4	9BFF",
+		"EB A5	9C00",
+		"EB A6	9C02",
+		"EB A7	9C0B",
+		"EB A8	9C0F",
+		"EB A9	9C11",
+		"EB AA	9C16",
+		"EB AB	9C18",
+		"EB AC	9C19",
+		"EB AD	9C1A",
+		"EB AE	9C1C",
+		"EB AF	9C1E",
+		"EB B0	9C22",
+		"EB B1	9C23",
+		"EB B2	9C26",
+		"EB B3	9C27",
+		"EB B4	9C28",
+		"EB B5	9C29",
+		"EB B6	9C2A",
+		"EB B7	9C31",
+		"EB B8	9C35",
+		"EB B9	9C36",
+		"EB BA	9C37",
+		"EB BB	9C3D",
+		"EB BC	9C41",
+		"EB BD	9C43",
+		"EB BE	9C44",
+		"EB BF	9C45",
+		"EB C0	9C49",
+		"EB C1	9C4A",
+		"EB C2	9C4E",
+		"EB C3	9C4F",
+		"EB C4	9C50",
+		"EB C5	9C53",
+		"EB C6	9C54",
+		"EB C7	9C56",
+		"EB C8	9C58",
+		"EB C9	9C5B",
+		"EB D0	9C5C",
+		"EB CA	9C5D",
+		"EB CB	9C5E",
+		"EB CC	9C5F",
+		"EB CD	9C63",
+		"EB D2	9C68",
+		"EB CE	9C69",
+		"EB CF	9C6A",
+		"EB D1	9C6B",
+		"EB D3	9C6E",
+		"EB D4	9C70",
+		"EB D5	9C72",
+		"EB D6	9C75",
+		"EB D7	9C77",
+		"EB D8	9C7B",
+		"EB D9	9CE6",
+		"EB DA	9CF2",
+		"EB DB	9CF7",
+		"EB DC	9CF9",
+		"EB DE	9D02",
+		"EB DD	9D0B",
+		"EB DF	9D11",
+		"EB E0	9D17",
+		"EB E1	9D18",
+		"EB E2	9D1C",
+		"EB E3	9D1D",
+		"EB E4	9D1E",
+		"EB E5	9D2F",
+		"EB E6	9D30",
+		"EB E7	9D32",
+		"EB E8	9D33",
+		"EB E9	9D34",
+		"EB EA	9D3A",
+		"EB EB	9D3C",
+		"EB ED	9D3D",
+		"EB EE	9D42",
+		"EB EF	9D43",
+		"EB EC	9D45",
+		"EB F0	9D47",
+		"EB F1	9D4A",
+		"EB F2	9D53",
+		"EB F3	9D54",
+		"EB F4	9D5F",
+		"EB F6	9D62",
+		"EB F5	9D63",
+		"EB F7	9D65",
+		"EB F8	9D69",
+		"EB F9	9D6A",
+		"EB FA	9D6B",
+		"EB FB	9D70",
+		"EB FC	9D76",
+		"EB FD	9D77",
+		"EB FE	9D7B",
+		"EC A1	9D7C",
+		"EC A2	9D7E",
+		"EC A3	9D83",
+		"EC A4	9D84",
+		"EC A5	9D86",
+		"EC A6	9D8A",
+		"EC A7	9D8D",
+		"EC A8	9D8E",
+		"EC A9	9D92",
+		"EC AA	9D93",
+		"EC AB	9D95",
+		"EC AC	9D96",
+		"EC AD	9D97",
+		"EC AE	9D98",
+		"EC AF	9DA1",
+		"EC B0	9DAA",
+		"EC B1	9DAC",
+		"EC B2	9DAE",
+		"EC B3	9DB1",
+		"EC B4	9DB5",
+		"EC B5	9DB9",
+		"EC B6	9DBC",
+		"EC B7	9DBF",
+		"EC B8	9DC3",
+		"EC B9	9DC7",
+		"EC BA	9DC9",
+		"EC BB	9DCA",
+		"EC BC	9DD4",
+		"EC BD	9DD5",
+		"EC BE	9DD6",
+		"EC BF	9DD7",
+		"EC C0	9DDA",
+		"EC C1	9DDE",
+		"EC C2	9DDF",
+		"EC C3	9DE0",
+		"EC C4	9DE5",
+		"EC C5	9DE7",
+		"EC C6	9DE9",
+		"EC C7	9DEB",
+		"EC C8	9DEE",
+		"EC C9	9DF0",
+		"EC CA	9DF3",
+		"EC CB	9DF4",
+		"EC CC	9DFE",
+		"EC CE	9E02",
+		"EC CF	9E07",
+		"EC CD	9E0A",
+		"EC D0	9E0E",
+		"EC D1	9E10",
+		"EC D2	9E11",
+		"EC D3	9E12",
+		"EC D4	9E15",
+		"EC D5	9E16",
+		"EC D6	9E19",
+		"EC D7	9E1C",
+		"EC D8	9E1D",
+		"EC D9	9E7A",
+		"EC DA	9E7B",
+		"EC DB	9E7C",
+		"EC DC	9E80",
+		"EC DD	9E82",
+		"EC DE	9E83",
+		"EC DF	9E84",
+		"EC E0	9E85",
+		"EC E1	9E87",
+		"EC E2	9E8E",
+		"EC E3	9E8F",
+		"EC E4	9E96",
+		"EC E5	9E98",
+		"EC E6	9E9B",
+		"EC E7	9E9E",
+		"EC E8	9EA4",
+		"EC E9	9EA8",
+		"EC EA	9EAC",
+		"EC EB	9EAE",
+		"EC EC	9EAF",
+		"EC ED	9EB0",
+		"EC EE	9EB3",
+		"EC EF	9EB4",
+		"EC F0	9EB5",
+		"EC F1	9EC6",
+		"EC F2	9EC8",
+		"EC F3	9ECB",
+		"EC F4	9ED5",
+		"EC F5	9EDF",
+		"EC F6	9EE4",
+		"EC F7	9EE7",
+		"EC F8	9EEC",
+		"EC F9	9EED",
+		"EC FA	9EEE",
+		"EC FB	9EF0",
+		"EC FC	9EF1",
+		"EC FD	9EF2",
+		"EC FE	9EF5",
+		"ED A1	9EF8",
+		"ED A2	9EFF",
+		"ED A3	9F02",
+		"ED A4	9F03",
+		"ED A5	9F09",
+		"ED A6	9F0F",
+		"ED A7	9F10",
+		"ED A8	9F11",
+		"ED A9	9F12",
+		"ED AA	9F14",
+		"ED AB	9F16",
+		"ED AC	9F17",
+		"ED AD	9F19",
+		"ED AE	9F1A",
+		"ED AF	9F1B",
+		"ED B0	9F1F",
+		"ED B1	9F22",
+		"ED B2	9F26",
+		"ED B3	9F2A",
+		"ED B4	9F2B",
+		"ED B5	9F2F",
+		"ED B6	9F31",
+		"ED B7	9F32",
+		"ED B8	9F34",
+		"ED B9	9F37",
+		"ED BA	9F39",
+		"ED BB	9F3A",
+		"ED BC	9F3C",
+		"ED BD	9F3D",
+		"ED BE	9F3F",
+		"ED BF	9F41",
+		"ED C0	9F43",
+		"ED C1	9F44",
+		"ED C2	9F45",
+		"ED C3	9F46",
+		"ED C4	9F47",
+		"ED C5	9F53",
+		"ED C6	9F55",
+		"ED C7	9F56",
+		"ED C8	9F57",
+		"ED C9	9F58",
+		"ED CA	9F5A",
+		"ED CB	9F5D",
+		"ED CC	9F5E",
+		"ED CD	9F68",
+		"ED CE	9F69",
+		"ED CF	9F6D",
+		"ED D0	9F6E",
+		"ED D1	9F6F",
+		"ED D2	9F70",
+		"ED D3	9F71",
+		"ED D4	9F73",
+		"ED D5	9F75",
+		"ED D6	9F7A",
+		"ED D7	9F7D",
+		"ED D8	9F8F",
+		"ED D9	9F90",
+		"ED DA	9F91",
+		"ED DB	9F92",
+		"ED DC	9F94",
+		"ED DD	9F96",
+		"ED DE	9F97",
+		"ED DF	9F9E",
+		"ED E0	9FA1",
+		"ED E1	9FA2",
+		"ED E2	9FA3",
+		"ED E3	9FA5",
+		"A2 B7	FF5E",
+};
--- /dev/null
+++ b/appl/lib/convcs/ibm437.b
@@ -1,0 +1,34 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "ibm437";
+
+cstab := array [] of {
+16r00,16r01,16r02,16r03,16r04,16r05,16r06,16r07,16r08,16r09,16r0a,16r0b,16r0c,16r0d,16r0e,16r0f,
+16r10,16r11,16r12,16r13,16r14,16r15,16r16,16r17,16r18,16r19,16r1a,16r1b,16r1c,16r1d,16r1e,16r1f,
+16r20,16r21,16r22,16r23,16r24,16r25,16r26,16r27,16r28,16r29,16r2a,16r2b,16r2c,16r2d,16r2e,16r2f,
+16r30,16r31,16r32,16r33,16r34,16r35,16r36,16r37,16r38,16r39,16r3a,16r3b,16r3c,16r3d,16r3e,16r3f,
+16r40,16r41,16r42,16r43,16r44,16r45,16r46,16r47,16r48,16r49,16r4a,16r4b,16r4c,16r4d,16r4e,16r4f,
+16r50,16r51,16r52,16r53,16r54,16r55,16r56,16r57,16r58,16r59,16r5a,16r5b,16r5c,16r5d,16r5e,16r5f,
+16r60,16r61,16r62,16r63,16r64,16r65,16r66,16r67,16r68,16r69,16r6a,16r6b,16r6c,16r6d,16r6e,16r6f,
+16r70,16r71,16r72,16r73,16r74,16r75,16r76,16r77,16r78,16r79,16r7a,16r7b,16r7c,16r7d,16r7e,16r7f,
+16r00c7, 16r00fc, 16r00e9, 16r00e2, 16r00e4, 16r00e0, 16r00e5, 16r00e7, # latin
+16r00ea, 16r00eb, 16r00e8, 16r00ef, 16r00ee, 16r00ec, 16r00c4, 16r00c5,
+16r00c9, 16r00e6, 16r00c6, 16r00f4, 16r00f6, 16r00f2, 16r00fb, 16r00f9,
+16r00ff, 16r00d6, 16r00dc, 16r00a2, 16r00a3, 16r00a5, 16r20a7, 16r0192,
+16r00e1, 16r00ed, 16r00f3, 16r00fa, 16r00f1, 16r00d1, 16r00aa, 16r00ba,
+16r00bf, 16r2310, 16r00ac, 16r00bd, 16r00bc, 16r00a1, 16r00ab, 16r00bb,
+16r2591, 16r2592, 16r2593, 16r2502, 16r2524, 16r2561, 16r2562, 16r2556, # forms
+16r2555, 16r2563, 16r2551, 16r2557, 16r255d, 16r255c, 16r255b, 16r2510,
+16r2514, 16r2534, 16r252c, 16r251c, 16r2500, 16r253c, 16r255e, 16r255f,
+16r255a, 16r2554, 16r2569, 16r2566, 16r2560, 16r2550, 16r256c, 16r2567,
+16r2568, 16r2564, 16r2565, 16r2559, 16r2558, 16r2552, 16r2553, 16r256b, 
+16r256a, 16r2518, 16r250c, 16r2588, 16r2584, 16r258c, 16r2590, 16r2580,
+16r03b1, 16r00df, 16r0393, 16r03c0, 16r03a3, 16r03c3, 16r00b5, 16r03c4, # greek
+16r03a6, 16r0398, 16r2126, 16r03b4, 16r221e, 16r2205, 16r2208, 16r2229,
+16r2261, 16r00b1, 16r2265, 16r2264, 16r2320, 16r2321, 16r00f7, 16r2248, # math
+16r00b0, 16r2022, 16r00b7, 16r221a, 16r207f, 16r00b2, 16r220e, 16r00a0,
+};
--- /dev/null
+++ b/appl/lib/convcs/ibm850.b
@@ -1,0 +1,34 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "ibm850";
+
+cstab := array [] of {
+16r00,16r01,16r02,16r03,16r04,16r05,16r06,16r07,16r08,16r09,16r0a,16r0b,16r0c,16r0d,16r0e,16r0f,
+16r10,16r11,16r12,16r13,16r14,16r15,16r16,16r17,16r18,16r19,16r1a,16r1b,16r1c,16r1d,16r1e,16r1f,
+16r20,16r21,16r22,16r23,16r24,16r25,16r26,16r27,16r28,16r29,16r2a,16r2b,16r2c,16r2d,16r2e,16r2f,
+16r30,16r31,16r32,16r33,16r34,16r35,16r36,16r37,16r38,16r39,16r3a,16r3b,16r3c,16r3d,16r3e,16r3f,
+16r40,16r41,16r42,16r43,16r44,16r45,16r46,16r47,16r48,16r49,16r4a,16r4b,16r4c,16r4d,16r4e,16r4f,
+16r50,16r51,16r52,16r53,16r54,16r55,16r56,16r57,16r58,16r59,16r5a,16r5b,16r5c,16r5d,16r5e,16r5f,
+16r60,16r61,16r62,16r63,16r64,16r65,16r66,16r67,16r68,16r69,16r6a,16r6b,16r6c,16r6d,16r6e,16r6f,
+16r70,16r71,16r72,16r73,16r74,16r75,16r76,16r77,16r78,16r79,16r7a,16r7b,16r7c,16r7d,16r7e,16r7f,
+16r00c7, 16r00fc, 16r00e9, 16r00e2, 16r00e4, 16r00e0, 16r00e5, 16r00e7, # latin-1 repertoire with forms 
+16r00ea, 16r00eb, 16r00e8, 16r00ef, 16r00ee, 16r00ec, 16r00c4, 16r00c5,
+16r00c9, 16r00e6, 16r00c6, 16r00f4, 16r00f6, 16r00f2, 16r00fb, 16r00f9,
+16r00ff, 16r00d6, 16r00dc, 16r00f8, 16r00a3, 16r00d8, 16r00d7, 16r0192,
+16r00e1, 16r00ed, 16r00f3, 16r00fa, 16r00f1, 16r00d1, 16r00aa, 16r00ba,
+16r00bf, 16r00ae, 16r00ac, 16r00bd, 16r00bc, 16r00a1, 16r00ab, 16r00bb,
+16r2591, 16r2592, 16r2593, 16r2502, 16r2524, 16r00c1, 16r00c2, 16r00c0,
+16r00a9, 16r2563, 16r2551, 16r2557, 16r255d, 16r00a2, 16r00a5, 16r2510,
+16r2514, 16r2534, 16r252c, 16r251c, 16r2500, 16r253c, 16r00e3, 16r00c3,
+16r255a, 16r2554, 16r2569, 16r2566, 16r2560, 16r2550, 16r256c, 16r00a4,
+16r00f0, 16r00d0, 16r00ca, 16r00cb, 16r00c8, 16r0131, 16r00cd, 16r00ce, 
+16r00cf, 16r2518, 16r250c, 16r2588, 16r2584, 16r00a6, 16r00cc, 16r2580,
+16r00d3, 16r00df, 16r00d4, 16r00d2, 16r00f5, 16r00d5, 16r00b5, 16r00fe,
+16r00de, 16r00da, 16r00db, 16r00d9, 16r00fd, 16r00dd, 16r00af, 16r00b4,
+16r00ad, 16r00b1, 16r2017, 16r00be, 16r00b6, 16r00a7, 16r00f7, 16r00b8,
+16r00b0, 16r00a8, 16r00b7, 16r00b9, 16r00b3, 16r00b2, 16r220e, 16r00a0,
+};
--- /dev/null
+++ b/appl/lib/convcs/ibm866.b
@@ -1,0 +1,34 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "ibm866";
+
+cstab := array [] of {
+16r00,16r01,16r02,16r03,16r04,16r05,16r06,16r07,16r08,16r09,16r0a,16r0b,16r0c,16r0d,16r0e,16r0f,
+16r10,16r11,16r12,16r13,16r14,16r15,16r16,16r17,16r18,16r19,16r1a,16r1b,16r1c,16r1d,16r1e,16r1f,
+16r20,16r21,16r22,16r23,16r24,16r25,16r26,16r27,16r28,16r29,16r2a,16r2b,16r2c,16r2d,16r2e,16r2f,
+16r30,16r31,16r32,16r33,16r34,16r35,16r36,16r37,16r38,16r39,16r3a,16r3b,16r3c,16r3d,16r3e,16r3f,
+16r40,16r41,16r42,16r43,16r44,16r45,16r46,16r47,16r48,16r49,16r4a,16r4b,16r4c,16r4d,16r4e,16r4f,
+16r50,16r51,16r52,16r53,16r54,16r55,16r56,16r57,16r58,16r59,16r5a,16r5b,16r5c,16r5d,16r5e,16r5f,
+16r60,16r61,16r62,16r63,16r64,16r65,16r66,16r67,16r68,16r69,16r6a,16r6b,16r6c,16r6d,16r6e,16r6f,
+16r70,16r71,16r72,16r73,16r74,16r75,16r76,16r77,16r78,16r79,16r7a,16r7b,16r7c,16r7d,16r7e,16r7f,
+16r0410,16r0411,16r0412,16r0413,16r0414,16r0415,16r0416,16r0417,
+16r0418,16r0419,16r041a,16r041b,16r041c,16r041d,16r041e,16r041f,
+16r0420,16r0421,16r0422,16r0423,16r0424,16r0425,16r0426,16r0427,
+16r0428,16r0429,16r042a,16r042b,16r042c,16r042d,16r042e,16r042f,
+16r0430,16r0431,16r0432,16r0433,16r0434,16r0435,16r0436,16r0437,
+16r0438,16r0439,16r043a,16r043b,16r043c,16r043d,16r043e,16r043f,
+    -1,    -1,    -1,    -1,    -1,    -1,    -1,    -1,
+    -1,    -1,    -1,    -1,    -1,    -1,    -1,    -1,
+    -1,    -1,    -1,    -1,    -1,    -1,    -1,    -1,
+    -1,    -1,    -1,    -1,    -1,    -1,    -1,    -1,
+    -1,    -1,    -1,    -1,    -1,    -1,    -1,    -1,
+    -1,    -1,    -1,    -1,    -1,    -1,    -1,    -1,
+16r0440,16r0441,16r0442,16r0443,16r0444,16r0445,16r0446,16r0447,
+16r0448,16r0449,16r044a,16r044b,16r044c,16r044d,16r044e,16r044f,
+16r0401,16r0451,    -1,    -1,    -1,    -1,    -1,    -1,
+    -1,    -1,    -1,    -1,    -1,    -1,    -1,    -1,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-1.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-1";
+
+cstab := array [] of {
+	16r00,16r01,16r02,16r03,16r04,16r05,16r06,16r07,
+	16r08,16r09,16r0a,16r0b,16r0c,16r0d,16r0e,16r0f,
+	16r10,16r11,16r12,16r13,16r14,16r15,16r16,16r17,
+	16r18,16r19,16r1a,16r1b,16r1c,16r1d,16r1e,16r1f,
+	16r20,16r21,16r22,16r23,16r24,16r25,16r26,16r27,
+	16r28,16r29,16r2a,16r2b,16r2c,16r2d,16r2e,16r2f,
+	16r30,16r31,16r32,16r33,16r34,16r35,16r36,16r37,
+	16r38,16r39,16r3a,16r3b,16r3c,16r3d,16r3e,16r3f,
+	16r40,16r41,16r42,16r43,16r44,16r45,16r46,16r47,
+	16r48,16r49,16r4a,16r4b,16r4c,16r4d,16r4e,16r4f,
+	16r50,16r51,16r52,16r53,16r54,16r55,16r56,16r57,
+	16r58,16r59,16r5a,16r5b,16r5c,16r5d,16r5e,16r5f,
+	16r60,16r61,16r62,16r63,16r64,16r65,16r66,16r67,
+	16r68,16r69,16r6a,16r6b,16r6c,16r6d,16r6e,16r6f,
+	16r70,16r71,16r72,16r73,16r74,16r75,16r76,16r77,
+	16r78,16r79,16r7a,16r7b,16r7c,16r7d,16r7e,16r7f,
+	16r80,16r81,16r82,16r83,16r84,16r85,16r86,16r87,
+	16r88,16r89,16r8a,16r8b,16r8c,16r8d,16r8e,16r8f,
+	16r90,16r91,16r92,16r93,16r94,16r95,16r96,16r97,
+	16r98,16r99,16r9a,16r9b,16r9c,16r9d,16r9e,16r9f,
+	16ra0,16ra1,16ra2,16ra3,16ra4,16ra5,16ra6,16ra7,
+	16ra8,16ra9,16raa,16rab,16rac,16rad,16rae,16raf,
+	16rb0,16rb1,16rb2,16rb3,16rb4,16rb5,16rb6,16rb7,
+	16rb8,16rb9,16rba,16rbb,16rbc,16rbd,16rbe,16rbf,
+	16rc0,16rc1,16rc2,16rc3,16rc4,16rc5,16rc6,16rc7,
+	16rc8,16rc9,16rca,16rcb,16rcc,16rcd,16rce,16rcf,
+	16rd0,16rd1,16rd2,16rd3,16rd4,16rd5,16rd6,16rd7,
+	16rd8,16rd9,16rda,16rdb,16rdc,16rdd,16rde,16rdf,
+	16re0,16re1,16re2,16re3,16re4,16re5,16re6,16re7,
+	16re8,16re9,16rea,16reb,16rec,16red,16ree,16ref,
+	16rf0,16rf1,16rf2,16rf3,16rf4,16rf5,16rf6,16rf7,
+	16rf8,16rf9,16rfa,16rfb,16rfc,16rfd,16rfe,16rff,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-10.b
@@ -1,0 +1,43 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-10";
+
+# from dkuug.dk:i18n/charmaps/ISO_8859-10:1993
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,16r0104,16r0112,16r0122,16r012a,16r0128,16r0136,16r00a7,
+	16r013b,16r0110,16r0160,16r0166,16r017d,16r00ad,16r016a,16r014a,
+	16r00b0,16r0105,16r0113,16r0123,16r012b,16r0129,16r0137,16r00b7,
+	16r013c,16r0110,16r0161,16r0167,16r017e,16r2014,16r016b,16r014b,
+	16r0100,16r00c1,16r00c2,16r00c3,16r00c4,16r00c5,16r00c6,16r012e,
+	16r010c,16r00c9,16r0118,16r00cb,16r0116,16r00cd,16r00ce,16r00cf,
+	16r00d0,16r0145,16r014c,16r00d3,16r00d4,16r00d5,16r00d6,16r0168,
+	16r00d8,16r0172,16r00da,16r00db,16r00dc,16r00dd,16r00de,16r00df,
+	16r0101,16r00e1,16r00e2,16r00e3,16r00e4,16r00e5,16r00e6,16r012f,
+	16r010d,16r00e9,16r0119,16r00eb,16r0117,16r00ed,16r00ee,16r00ef,
+	16r00f0,16r0146,16r014d,16r00f3,16r00f4,16r00f5,16r00f6,16r0169,
+	16r00f8,16r0173,16r00fa,16r00fb,16r00fc,16r00fd,16r00fe,16r0138,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-15.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-15";
+
+cstab := array [] of {
+	16r00,16r01,16r02,16r03,16r04,16r05,16r06,16r07,
+	16r08,16r09,16r0a,16r0b,16r0c,16r0d,16r0e,16r0f,
+	16r10,16r11,16r12,16r13,16r14,16r15,16r16,16r17,
+	16r18,16r19,16r1a,16r1b,16r1c,16r1d,16r1e,16r1f,
+	16r20,16r21,16r22,16r23,16r24,16r25,16r26,16r27,
+	16r28,16r29,16r2a,16r2b,16r2c,16r2d,16r2e,16r2f,
+	16r30,16r31,16r32,16r33,16r34,16r35,16r36,16r37,
+	16r38,16r39,16r3a,16r3b,16r3c,16r3d,16r3e,16r3f,
+	16r40,16r41,16r42,16r43,16r44,16r45,16r46,16r47,
+	16r48,16r49,16r4a,16r4b,16r4c,16r4d,16r4e,16r4f,
+	16r50,16r51,16r52,16r53,16r54,16r55,16r56,16r57,
+	16r58,16r59,16r5a,16r5b,16r5c,16r5d,16r5e,16r5f,
+	16r60,16r61,16r62,16r63,16r64,16r65,16r66,16r67,
+	16r68,16r69,16r6a,16r6b,16r6c,16r6d,16r6e,16r6f,
+	16r70,16r71,16r72,16r73,16r74,16r75,16r76,16r77,
+	16r78,16r79,16r7a,16r7b,16r7c,16r7d,16r7e,16r7f,
+	16r80,16r81,16r82,16r83,16r84,16r85,16r86,16r87,
+	16r88,16r89,16r8a,16r8b,16r8c,16r8d,16r8e,16r8f,
+	16r90,16r91,16r92,16r93,16r94,16r95,16r96,16r97,
+	16r98,16r99,16r9a,16r9b,16r9c,16r9d,16r9e,16r9f,
+	16ra0,16ra1,16ra2,16ra3,'€',16ra5,'Š',16ra7,
+	'š',16ra9,16raa,16rab,16rac,16rad,16rae,16raf,
+	16rb0,16rb1,16rb2,16rb3,'Ž',16rb5,16rb6,16rb7,
+	'ž',16rb9,16rba,16rbb,'Œ','œ','Ÿ',16rbf,
+	16rc0,16rc1,16rc2,16rc3,16rc4,16rc5,16rc6,16rc7,
+	16rc8,16rc9,16rca,16rcb,16rcc,16rcd,16rce,16rcf,
+	16rd0,16rd1,16rd2,16rd3,16rd4,16rd5,16rd6,16rd7,
+	16rd8,16rd9,16rda,16rdb,16rdc,16rdd,16rde,16rdf,
+	16re0,16re1,16re2,16re3,16re4,16re5,16re6,16re7,
+	16re8,16re9,16rea,16reb,16rec,16red,16ree,16ref,
+	16rf0,16rf1,16rf2,16rf3,16rf4,16rf5,16rf6,16rf7,
+	16rf8,16rf9,16rfa,16rfb,16rfc,16rfd,16rfe,16rff,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-2.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-2";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,16r0104,16r02d8,16r0141,16r00a4,16r013d,16r015a,16r00a7,
+	16r00a8,16r0160,16r015e,16r0164,16r0179,16r00ad,16r017d,16r017b,
+	16r00b0,16r0105,16r02db,16r0142,16r00b4,16r013e,16r015b,16r02c7,
+	16r00b8,16r0161,16r015f,16r0165,16r017a,16r02dd,16r017e,16r017c,
+	16r0154,16r00c1,16r00c2,16r0102,16r00c4,16r0139,16r0106,16r00c7,
+	16r010c,16r00c9,16r0118,16r00cb,16r011a,16r00cd,16r00ce,16r010e,
+	16r0110,16r0143,16r0147,16r00d3,16r00d4,16r0150,16r00d6,16r00d7,
+	16r0158,16r016e,16r00da,16r0170,16r00dc,16r00dd,16r0162,16r00df,
+	16r0155,16r00e1,16r00e2,16r0103,16r00e4,16r013a,16r0107,16r00e7,
+	16r010d,16r00e9,16r0119,16r00eb,16r011b,16r00ed,16r00ee,16r010f,
+	16r0111,16r0144,16r0148,16r00f3,16r00f4,16r0151,16r00f6,16r00f7,
+	16r0159,16r016f,16r00fa,16r0171,16r00fc,16r00fd,16r0163,16r02d9,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-3.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-3";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,16r0126,16r02d8,16r00a3,16r00a4,     -1,16r0124,16r00a7,
+	16r00a8,16r0130,16r015e,16r011e,16r0134,16r00ad,     -1,16r017b,
+	16r00b0,16r0127,16r00b2,16r00b3,16r00b4,16r00b5,16r0125,16r00b7,
+	16r00b8,16r0131,16r015f,16r011f,16r0135,16r00bd,     -1,16r017c,
+	16r00c0,16r00c1,16r00c2,     -1,16r00c4,16r010a,16r0108,16r00c7,
+	16r00c8,16r00c9,16r00ca,16r00cb,16r00cc,16r00cd,16r00ce,16r00cf,
+	     -1,16r00d1,16r00d2,16r00d3,16r00d4,16r0120,16r00d6,16r00d7,
+	16r011c,16r00d9,16r00da,16r00db,16r00dc,16r016c,16r015c,16r00df,
+	16r00e0,16r00e1,16r00e2,     -1,16r00e4,16r010b,16r0109,16r00e7,
+	16r00e8,16r00e9,16r00ea,16r00eb,16r00ec,16r00ed,16r00ee,16r00ef,
+	     -1,16r00f1,16r00f2,16r00f3,16r00f4,16r0121,16r00f6,16r00f7,
+	16r011d,16r00f9,16r00fa,16r00fb,16r00fc,16r016d,16r015d,16r02d9,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-4.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-4";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,16r0104,16r0138,16r0156,16r00a4,16r0128,16r013b,16r00a7,
+	16r00a8,16r0160,16r0112,16r0122,16r0166,16r00ad,16r017d,16r00af,
+	16r00b0,16r0105,16r02db,16r0157,16r00b4,16r0129,16r013c,16r02c7,
+	16r00b8,16r0161,16r0113,16r0123,16r0167,16r014a,16r017e,16r014b,
+	16r0100,16r00c1,16r00c2,16r00c3,16r00c4,16r00c5,16r00c6,16r012e,
+	16r010c,16r00c9,16r0118,16r00cb,16r0116,16r00cd,16r00ce,16r012a,
+	16r0110,16r0145,16r014c,16r0136,16r00d4,16r00d5,16r00d6,16r00d7,
+	16r00d8,16r0172,16r00da,16r00db,16r00dc,16r0168,16r016a,16r00df,
+	16r0101,16r00e1,16r00e2,16r00e3,16r00e4,16r00e5,16r00e6,16r012f,
+	16r010d,16r00e9,16r0119,16r00eb,16r0117,16r00ed,16r00ee,16r012b,
+	16r0111,16r0146,16r014d,16r0137,16r00f4,16r00f5,16r00f6,16r00f7,
+	16r00f8,16r0173,16r00fa,16r00fb,16r00fc,16r0169,16r016b,16r02d9,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-5.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-5";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,16r0401,16r0402,16r0403,16r0404,16r0405,16r0406,16r0407,
+	16r0408,16r0409,16r040a,16r040b,16r040c,16r00ad,16r040e,16r040f,
+	16r0410,16r0411,16r0412,16r0413,16r0414,16r0415,16r0416,16r0417,
+	16r0418,16r0419,16r041a,16r041b,16r041c,16r041d,16r041e,16r041f,
+	16r0420,16r0421,16r0422,16r0423,16r0424,16r0425,16r0426,16r0427,
+	16r0428,16r0429,16r042a,16r042b,16r042c,16r042d,16r042e,16r042f,
+	16r0430,16r0431,16r0432,16r0433,16r0434,16r0435,16r0436,16r0437,
+	16r0438,16r0439,16r043a,16r043b,16r043c,16r043d,16r043e,16r043f,
+	16r0440,16r0441,16r0442,16r0443,16r0444,16r0445,16r0446,16r0447,
+	16r0448,16r0449,16r044a,16r044b,16r044c,16r044d,16r044e,16r044f,
+	16r2116,16r0451,16r0452,16r0453,16r0454,16r0455,16r0456,16r0457,
+	16r0458,16r0459,16r045a,16r045b,16r045c,16r00a7,16r045e,16r045f,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-6.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-6";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,     -1,     -1,     -1,16r00a4,     -1,     -1,     -1,
+	     -1,     -1,     -1,     -1,16r060c,16r00ad,     -1,     -1,
+	     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,
+	     -1,     -1,     -1,16r061b,     -1,     -1,     -1,16r061f,
+	     -1,16r0621,16r0622,16r0623,16r0624,16r0625,16r0626,16r0627,
+	16r0628,16r0629,16r062a,16r062b,16r062c,16r062d,16r062e,16r062f,
+	16r0630,16r0631,16r0632,16r0633,16r0634,16r0635,16r0636,16r0637,
+	16r0638,16r0639,16r063a,     -1,    -1,    -1,    -1,    -1,
+	16r0640,16r0641,16r0642,16r0643,16r0644,16r0645,16r0646,16r0647,
+	16r0648,16r0649,16r064a,16r064b,16r064c,16r064d,16r064e,16r064f,
+	16r0650,16r0651,16r0652,     -1,     -1,     -1,     -1,     -1,
+	     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-7.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-7";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,16r2018,16r2019,16r00a3,     -1,     -1,16r00a6,16r00a7,
+	16r00a8,16r00a9,     -1,16r00ab,16r00ac,16r00ad,     -1,16r2015,
+	16r00b0,16r00b1,16r00b2,16r00b3,16r0384,16r0385,16r0386,16r00b7,
+	16r0388,16r0389,16r038a,16r00bb,16r038c,16r00bd,16r038e,16r038f,
+	16r0390,16r0391,16r0392,16r0393,16r0394,16r0395,16r0396,16r0397,
+	16r0398,16r0399,16r039a,16r039b,16r039c,16r039d,16r039e,16r039f,
+	16r03a0,16r03a1,     -1,16r03a3,16r03a4,16r03a5,16r03a6,16r03a7,
+	16r03a8,16r03a9,16r03aa,16r03ab,16r03ac,16r03ad,16r03ae,16r03af,
+	16r03b0,16r03b1,16r03b2,16r03b3,16r03b4,16r03b5,16r03b6,16r03b7,
+	16r03b8,16r03b9,16r03ba,16r03bb,16r03bc,16r03bd,16r03be,16r03bf,
+	16r03c0,16r03c1,16r03c2,16r03c3,16r03c4,16r03c5,16r03c6,16r03c7,
+	16r03c8,16r03c9,16r03ca,16r03cb,16r03cc,16r03cd,16r03ce,     -1,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-8.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-8";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,     -1,16r00a2,16r00a3,16r00a4,16r00a5,16r00a6,16r00a7,
+	16r00a8,16r00a9,16r00d7,16r00ab,16r00ac,16r00ad,16r00ae,16r203e,
+	16r00b0,16r00b1,16r00b2,16r00b3,16r00b4,16r00b5,16r00b6,16r00b7,
+	16r00b8,16r00b9,16r00f7,16r00bb,16r00bc,16r00bd,16r00be,     -1,
+	     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,
+	     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,
+	     -1,     -1,     -1,     -1,     -1,     -1,     -1,     -1,
+	     -1,     -1,     -1,     -1,     -1,     -1,     -1,16r2017,
+	16r05d0,16r05d1,16r05d2,16r05d3,16r05d4,16r05d5,16r05d6,16r05d7,
+	16r05d8,16r05d9,16r05da,16r05db,16r05dc,16r05dd,16r05de,16r05df,
+	16r05e0,16r05e1,16r05e2,16r05e3,16r05e4,16r05e5,16r05e6,16r05e7,
+	16r05e8,16r05e9,16r05ea,     -1,     -1,     -1,     -1,     -1,
+};
--- /dev/null
+++ b/appl/lib/convcs/iso8859-9.b
@@ -1,0 +1,42 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "iso-8859-9";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r0080,16r0081,16r0082,16r0083,16r0084,16r0085,16r0086,16r0087,
+	16r0088,16r0089,16r008a,16r008b,16r008c,16r008d,16r008e,16r008f,
+	16r0090,16r0091,16r0092,16r0093,16r0094,16r0095,16r0096,16r0097,
+	16r0098,16r0099,16r009a,16r009b,16r009c,16r009d,16r009e,16r009f,
+	16r00a0,16r00a1,16r00a2,16r00a3,16r00a4,16r00a5,16r00a6,16r00a7,
+	16r00a8,16r00a9,16r00aa,16r00ab,16r00ac,16r00ad,16r00ae,16r00af,
+	16r00b0,16r00b1,16r00b2,16r00b3,16r00b4,16r00b5,16r00b6,16r00b7,
+	16r00b8,16r00b9,16r00ba,16r00bb,16r00bc,16r00bd,16r00be,16r00bf,
+	16r00c0,16r00c1,16r00c2,16r00c3,16r00c4,16r00c5,16r00c6,16r00c7,
+	16r00c8,16r00c9,16r00ca,16r00cb,16r00cc,16r00cd,16r00ce,16r00cf,
+	16r011e,16r00d1,16r00d2,16r00d3,16r00d4,16r00d5,16r00d6,16r00d7,
+	16r00d8,16r00d9,16r00da,16r00db,16r00dc,16r0130,16r015e,16r00df,
+	16r00e0,16r00e1,16r00e2,16r00e3,16r00e4,16r00e5,16r00e6,16r00e7,
+	16r00e8,16r00e9,16r00ea,16r00eb,16r00ec,16r00ed,16r00ee,16r00ef,
+	16r011f,16r00f1,16r00f2,16r00f3,16r00f4,16r00f5,16r00f6,16r00f7,
+	16r00f8,16r00f9,16r00fa,16r00fb,16r00fc,16r0131,16r015f,16r00ff,
+};
--- /dev/null
+++ b/appl/lib/convcs/koi8-r.b
@@ -1,0 +1,43 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET: con "koi8-r";
+
+cstab := array [] of {
+	16r0000,16r0001,16r0002,16r0003,16r0004,16r0005,16r0006,16r0007,
+	16r0008,16r0009,16r000a,16r000b,16r000c,16r000d,16r000e,16r000f,
+	16r0010,16r0011,16r0012,16r0013,16r0014,16r0015,16r0016,16r0017,
+	16r0018,16r0019,16r001a,16r001b,16r001c,16r001d,16r001e,16r001f,
+	16r0020,16r0021,16r0022,16r0023,16r0024,16r0025,16r0026,16r0027,
+	16r0028,16r0029,16r002a,16r002b,16r002c,16r002d,16r002e,16r002f,
+	16r0030,16r0031,16r0032,16r0033,16r0034,16r0035,16r0036,16r0037,
+	16r0038,16r0039,16r003a,16r003b,16r003c,16r003d,16r003e,16r003f,
+	16r0040,16r0041,16r0042,16r0043,16r0044,16r0045,16r0046,16r0047,
+	16r0048,16r0049,16r004a,16r004b,16r004c,16r004d,16r004e,16r004f,
+	16r0050,16r0051,16r0052,16r0053,16r0054,16r0055,16r0056,16r0057,
+	16r0058,16r0059,16r005a,16r005b,16r005c,16r005d,16r005e,16r005f,
+	16r0060,16r0061,16r0062,16r0063,16r0064,16r0065,16r0066,16r0067,
+	16r0068,16r0069,16r006a,16r006b,16r006c,16r006d,16r006e,16r006f,
+	16r0070,16r0071,16r0072,16r0073,16r0074,16r0075,16r0076,16r0077,
+	16r0078,16r0079,16r007a,16r007b,16r007c,16r007d,16r007e,16r007f,
+	16r2500,16r2502,16r250c,16r2510,16r2514,16r2518,16r251c,16r2524,
+	16r252c,16r2534,16r253c,16r2580,16r2584,16r2588,16r258c,16r2590,
+	16r2591,16r2592,16r2593,16r2320,16r25a0,16r2219,16r221a,16r2248,
+	16r2264,16r2265,16r00a0,16r2321,16r00b0,16r00b2,16r00b7,16r00f7,
+	16r2550,16r2551,16r2552,16r0451,16r2553,16r2554,16r2555,16r2556,
+	16r2557,16r2558,16r2559,16r255a,16r255b,16r255c,16r255d,16r255e,
+	16r255f,16r2560,16r2561,16r0401,16r2562,16r2563,16r2564,16r2565,
+	16r2566,16r2567,16r2568,16r2569,16r256a,16r256b,16r256c,16r00a9,
+	16r044e,16r0430,16r0431,16r0446,16r0434,16r0435,16r0444,16r0433,
+	16r0445,16r0438,16r0439,16r043a,16r043b,16r043c,16r043d,16r043e,
+	16r043f,16r044f,16r0440,16r0441,16r0442,16r0443,16r0436,16r0432,
+	16r044c,16r044b,16r0437,16r0448,16r044d,16r0449,16r0447,16r044a,
+	16r042e,16r0410,16r0411,16r0426,16r0414,16r0415,16r0424,16r0413,
+	16r0425,16r0418,16r0419,16r041a,16r041b,16r041c,16r041d,16r041e,
+	16r041f,16r042f,16r0420,16r0421,16r0422,16r0423,16r0416,16r0412,
+	16r042c,16r042b,16r0417,16r0428,16r042d,16r0429,16r0427,16r042a,
+};
+
--- /dev/null
+++ b/appl/lib/convcs/mkdata
@@ -1,0 +1,40 @@
+#!/dis/sh
+load std
+
+# codepage generators
+GENERATORS=(
+	gencp932
+	genbig5
+	gengb2312
+	genjisx0201kana
+	genjisx0208-1997
+	genjisx0212
+	ibm437
+	ibm850
+	ibm866
+	iso8859-1
+	iso8859-10
+	iso8859-2
+	iso8859-3
+	iso8859-4
+	iso8859-5
+	iso8859-6
+	iso8859-7
+	iso8859-8
+	iso8859-9
+	iso8859-15
+	koi8-r
+	windows-1250
+	windows-1251
+	windows-1252
+)
+
+for i in $GENERATORS {
+	(1st 2nd)=`{ls -t $i.b $i.dis >[2]/dev/null}
+	if {~ $1st $i.b} {
+		echo building $i
+		limbo $i.b
+	}
+	echo running $i
+	$i
+}
--- /dev/null
+++ b/appl/lib/convcs/mkfile
@@ -1,0 +1,29 @@
+<../../../mkconfig
+
+TARG=\
+	8bit_stob.dis\
+	big5_btos.dis\
+	big5_stob.dis\
+	convcs.dis\
+	cp932_btos.dis\
+	cp_btos.dis\
+	cp_stob.dis\
+	euc-jp_btos.dis\
+	gb2312_btos.dis\
+	utf8_btos.dis\
+	utf8_stob.dis\
+	utf16_btos.dis\
+	utf16_stob.dis\
+
+MODULES=\
+
+SYSMODULES= \
+	bufio.m\
+	cfg.m\
+	convcs.m\
+	string.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/lib/convcs
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/convcs/utf16_btos.b
@@ -1,0 +1,66 @@
+implement Btos;
+
+include "sys.m";
+include "convcs.m";
+
+Littleendian, Bigendian: con iota;
+
+sys : Sys;
+default := Bigendian;
+
+init(arg : string) : string
+{
+	sys = load Sys Sys->PATH;
+	case arg {
+	"le" =>
+		default = Littleendian;
+	"be" =>
+		default = Bigendian;
+	}
+	return nil;
+}
+
+
+btos(state : Convcs->State, b : array of byte, n : int) : (Convcs->State, string, int)
+{
+	endian: int;
+	i := 0;
+	if(state != nil)
+		endian = state[0];
+	else if (len b >= 2) {
+		state = " ";
+		# XXX should probably not do this if we've been told the endianness
+		case (int b[0] << 8) | int b[1] {
+		16rfeff =>
+			endian = Bigendian;
+			i += 2;
+		16rfffe =>
+			endian = Littleendian;
+			i += 2;
+		* =>
+			endian = guessendian(b);
+		}
+		state[0] = endian;
+	}
+	nb := len b & ~1;
+	if(n > 0 && nb - i > n * 2)
+		nb = i + n * 2;
+	out := "";
+	if(endian == Bigendian){
+		for(; i < nb; i += 2)
+			out[len out] = (int b[i] << 8) | int b[i + 1];
+	}else{
+		for(; i < nb; i += 2)
+			out[len out] = int b[i] | int b[i + 1] << 8;
+	}
+	if(n == 0 && i < len b)
+		out[len out] = Sys->UTFerror;
+		
+	return (state, out, i);
+}
+
+guessendian(nil: array of byte): int
+{
+	# XXX might be able to do better than this in the absence of endian hints.
+	return default;
+}
--- /dev/null
+++ b/appl/lib/convcs/utf16_stob.b
@@ -1,0 +1,47 @@
+implement Stob;
+
+include "sys.m";
+	sys: Sys;
+include "convcs.m";
+
+bigendian := 1;
+header := 1;
+
+init(arg : string) : string
+{
+	sys = load Sys Sys->PATH;
+	case arg {
+	"le" =>
+		bigendian = 0;
+		header = 0;
+	"be" =>
+		header = 0;
+	}
+	return nil;
+}
+
+stob(state : Convcs->State, s : string) : (Convcs->State, array of byte)
+{
+	if(state == nil){
+		if(header)
+			s = sys->sprint("%c", 16rfeff) + s;
+		state = "doneheader";
+	}
+
+	b := array[len s * 2] of byte;
+	j := 0;
+	if(bigendian){
+		for(i := 0; i < len s; i++){
+			c := s[i];
+			b[j++] = byte (c >> 8);
+			b[j++] = byte c;
+		}
+	}else{
+		for(i := 0; i < len s; i++){
+			c := s[i];
+			b[j++] = byte c;
+			b[j++] = byte (c >> 8);
+		}
+	}
+	return (state, b);
+}
--- /dev/null
+++ b/appl/lib/convcs/utf8_btos.b
@@ -1,0 +1,34 @@
+implement Btos;
+
+include "sys.m";
+include "convcs.m";
+
+sys : Sys;
+
+init(nil : string) : string
+{
+	sys = load Sys Sys->PATH;
+	return nil;
+}
+
+btos(nil : Convcs->State, b : array of byte, n : int) : (Convcs->State, string, int)
+{
+	nbytes := 0;
+	str := "";
+
+	if (n == -1) {
+		# gather as much as possible
+		nbytes = sys->utfbytes(b, len b);
+		if (nbytes > 0)
+			str = string b[:nbytes];
+	} else {
+		for (; nbytes < len b && len str < n;) {
+			(ch, l, nil) := sys->byte2char(b, nbytes);
+			if (l <= 0)
+				break;
+			str[len str] = ch;
+			nbytes += l;
+		}
+	}
+	return (nil, str, nbytes);
+}
--- /dev/null
+++ b/appl/lib/convcs/utf8_stob.b
@@ -1,0 +1,17 @@
+implement Stob;
+
+include "sys.m";
+include "convcs.m";
+
+sys : Sys;
+
+init(nil : string) : string
+{
+	sys = load Sys Sys->PATH;
+	return nil;
+}
+
+stob(nil : Convcs->State, str : string) : (Convcs->State, array of byte)
+{
+	return (nil, array of byte str);
+}
--- /dev/null
+++ b/appl/lib/convcs/windows-1250.b
@@ -1,0 +1,26 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "windows-1250";
+
+cstab := array [] of {
+16r0000, 16r0001, 16r0002, 16r0003, 16r0004, 16r0005, 16r0006, 16r0007, 16r0008, 16r0009, 16r000A, 16r000B, 16r000C, 16r000D, 16r000E, 16r000F,
+16r0010, 16r0011, 16r0012, 16r0013, 16r0014, 16r0015, 16r0016, 16r0017, 16r0018, 16r0019, 16r001A, 16r001B, 16r001C, 16r001D, 16r001E, 16r001F,
+16r0020, 16r0021, 16r0022, 16r0023, 16r0024, 16r0025, 16r0026, 16r0027, 16r0028, 16r0029, 16r002A, 16r002B, 16r002C, 16r002D, 16r002E, 16r002F,
+16r0030, 16r0031, 16r0032, 16r0033, 16r0034, 16r0035, 16r0036, 16r0037, 16r0038, 16r0039, 16r003A, 16r003B, 16r003C, 16r003D, 16r003E, 16r003F,
+16r0040, 16r0041, 16r0042, 16r0043, 16r0044, 16r0045, 16r0046, 16r0047, 16r0048, 16r0049, 16r004A, 16r004B, 16r004C, 16r004D, 16r004E, 16r004F,
+16r0050, 16r0051, 16r0052, 16r0053, 16r0054, 16r0055, 16r0056, 16r0057, 16r0058, 16r0059, 16r005A, 16r005B, 16r005C, 16r005D, 16r005E, 16r005F,
+16r0060, 16r0061, 16r0062, 16r0063, 16r0064, 16r0065, 16r0066, 16r0067, 16r0068, 16r0069, 16r006A, 16r006B, 16r006C, 16r006D, 16r006E, 16r006F,
+16r0070, 16r0071, 16r0072, 16r0073, 16r0074, 16r0075, 16r0076, 16r0077, 16r0078, 16r0079, 16r007A, 16r007B, 16r007C, 16r007D, 16r007E, 16r007F,
+16r20AC, -1, 16r201A, -1, 16r201E, 16r2026, 16r2020, 16r2021, -1, 16r2030, 16r0160, 16r2039, 16r015A, 16r0164, 16r017D, 16r0179,
+-1, 16r2018, 16r2019, 16r201C, 16r201D, 16r2022, 16r2013, 16r2014, -1, 16r2122, 16r0161, 16r203A, 16r015B, 16r0165, 16r017E, 16r017A,
+16r00A0, 16r02C7, 16r02D8, 16r0141, 16r00A4, 16r0104, 16r00A6, 16r00A7, 16r00A8, 16r00A9, 16r015E, 16r00AB, 16r00AC, 16r00AD, 16r00AE, 16r017B,
+16r00B0, 16r00B1, 16r02DB, 16r0142, 16r00B4, 16r00B5, 16r00B6, 16r00B7, 16r00B8, 16r0105, 16r015F, 16r00BB, 16r013D, 16r02DD, 16r013E, 16r017C,
+16r0154, 16r00C1, 16r00C2, 16r0102, 16r00C4, 16r0139, 16r0106, 16r00C7, 16r010C, 16r00C9, 16r0118, 16r00CB, 16r011A, 16r00CD, 16r00CE, 16r010E,
+16r0110, 16r0143, 16r0147, 16r00D3, 16r00D4, 16r0150, 16r00D6, 16r00D7, 16r0158, 16r016E, 16r00DA, 16r0170, 16r00DC, 16r00DD, 16r0162, 16r00DF,
+16r0155, 16r00E1, 16r00E2, 16r0103, 16r00E4, 16r013A, 16r0107, 16r00E7, 16r010D, 16r00E9, 16r0119, 16r00EB, 16r011B, 16r00ED, 16r00EE, 16r010F,
+16r0111, 16r0144, 16r0148, 16r00F3, 16r00F4, 16r0151, 16r00F6, 16r00F7, 16r0159, 16r016F, 16r00FA, 16r0171, 16r00FC, 16r00FD, 16r0163, 16r02D9,
+};
--- /dev/null
+++ b/appl/lib/convcs/windows-1251.b
@@ -1,0 +1,34 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "windows-1251";
+
+cstab := array [] of {
+16r00,16r01,16r02,16r03,16r04,16r05,16r06,16r07,16r08,16r09,16r0a,16r0b,16r0c,16r0d,16r0e,16r0f,
+16r10,16r11,16r12,16r13,16r14,16r15,16r16,16r17,16r18,16r19,16r1a,16r1b,16r1c,16r1d,16r1e,16r1f,
+16r20,16r21,16r22,16r23,16r24,16r25,16r26,16r27,16r28,16r29,16r2a,16r2b,16r2c,16r2d,16r2e,16r2f,
+16r30,16r31,16r32,16r33,16r34,16r35,16r36,16r37,16r38,16r39,16r3a,16r3b,16r3c,16r3d,16r3e,16r3f,
+16r40,16r41,16r42,16r43,16r44,16r45,16r46,16r47,16r48,16r49,16r4a,16r4b,16r4c,16r4d,16r4e,16r4f,
+16r50,16r51,16r52,16r53,16r54,16r55,16r56,16r57,16r58,16r59,16r5a,16r5b,16r5c,16r5d,16r5e,16r5f,
+16r60,16r61,16r62,16r63,16r64,16r65,16r66,16r67,16r68,16r69,16r6a,16r6b,16r6c,16r6d,16r6e,16r6f,
+16r70,16r71,16r72,16r73,16r74,16r75,16r76,16r77,16r78,16r79,16r7a,16r7b,16r7c,16r7d,16r7e,16r7f,
+16r0402,16r0403,16r201a,16r0453,16r201e,16r2026,16r2020,16r2021,
+    -1,16r2030,16r0409,16r2039,16r040a,16r040c,16r040b,16r040f,
+16r0452,16r2018,16r2019,16r201c,16r201d,16r2022,16r2013,16r2014,
+    -1,16r2122,16r0459,16r203a,16r045a,16r045c,16r045b,16r045f,
+16r00a0,16r040e,16r045e,16r0408,16r00a4,16r0490,16r00a6,16r00a7,
+16r0401,16r00a9,16r0404,16r00ab,16r00ac,16r00ad,16r00ae,16r0407,
+16r00b0,16r00b1,16r0406,16r0456,16r0491,16r00b5,16r00b6,16r00b7,
+16r0451,16r2116,16r0454,16r00bb,16r0458,16r0405,16r0455,16r0457,
+16r0410,16r0411,16r0412,16r0413,16r0414,16r0415,16r0416,16r0417,
+16r0418,16r0419,16r041a,16r041b,16r041c,16r041d,16r041e,16r041f,
+16r0420,16r0421,16r0422,16r0423,16r0424,16r0425,16r0426,16r0427,
+16r0428,16r0429,16r042a,16r042b,16r042c,16r042d,16r042e,16r042f,
+16r0430,16r0431,16r0432,16r0433,16r0434,16r0435,16r0436,16r0437,
+16r0438,16r0439,16r043a,16r043b,16r043c,16r043d,16r043e,16r043f,
+16r0440,16r0441,16r0442,16r0443,16r0444,16r0445,16r0446,16r0447,
+16r0448,16r0449,16r044a,16r044b,16r044c,16r044d,16r044e,16r044f,
+};
--- /dev/null
+++ b/appl/lib/convcs/windows-1252.b
@@ -1,0 +1,26 @@
+implement GenCP;
+
+include "sys.m";
+include "draw.m";
+include "gencp.b";
+
+CHARSET : con "windows-1252";
+
+cstab := array [] of {
+16r0000, 16r0001, 16r0002, 16r0003, 16r0004, 16r0005, 16r0006, 16r0007, 16r0008, 16r0009, 16r000A, 16r000B, 16r000C, 16r000D, 16r000E, 16r000F,
+16r0010, 16r0011, 16r0012, 16r0013, 16r0014, 16r0015, 16r0016, 16r0017, 16r0018, 16r0019, 16r001A, 16r001B, 16r001C, 16r001D, 16r001E, 16r001F,
+16r0020, 16r0021, 16r0022, 16r0023, 16r0024, 16r0025, 16r0026, 16r0027, 16r0028, 16r0029, 16r002A, 16r002B, 16r002C, 16r002D, 16r002E, 16r002F,
+16r0030, 16r0031, 16r0032, 16r0033, 16r0034, 16r0035, 16r0036, 16r0037, 16r0038, 16r0039, 16r003A, 16r003B, 16r003C, 16r003D, 16r003E, 16r003F,
+16r0040, 16r0041, 16r0042, 16r0043, 16r0044, 16r0045, 16r0046, 16r0047, 16r0048, 16r0049, 16r004A, 16r004B, 16r004C, 16r004D, 16r004E, 16r004F,
+16r0050, 16r0051, 16r0052, 16r0053, 16r0054, 16r0055, 16r0056, 16r0057, 16r0058, 16r0059, 16r005A, 16r005B, 16r005C, 16r005D, 16r005E, 16r005F,
+16r0060, 16r0061, 16r0062, 16r0063, 16r0064, 16r0065, 16r0066, 16r0067, 16r0068, 16r0069, 16r006A, 16r006B, 16r006C, 16r006D, 16r006E, 16r006F,
+16r0070, 16r0071, 16r0072, 16r0073, 16r0074, 16r0075, 16r0076, 16r0077, 16r0078, 16r0079, 16r007A, 16r007B, 16r007C, 16r007D, 16r007E, 16r007F,
+16r20AC, -1, 16r201A, 16r0192, 16r201E, 16r2026, 16r2020, 16r2021, 16r02C6, 16r2030, 16r0160, 16r2039, 16r0152, -1, 16r017D, -1,
+-1, 16r2018, 16r2019, 16r201C, 16r201D, 16r2022, 16r2013, 16r2014, 16r02DC, 16r2122, 16r0161, 16r203A, 16r0153, -1, 16r017E, 16r0178,
+16r00A0, 16r00A1, 16r00A2, 16r00A3, 16r00A4, 16r00A5, 16r00A6, 16r00A7, 16r00A8, 16r00A9, 16r00AA, 16r00AB, 16r00AC, 16r00AD, 16r00AE, 16r00AF,
+16r00B0, 16r00B1, 16r00B2, 16r00B3, 16r00B4, 16r00B5, 16r00B6, 16r00B7, 16r00B8, 16r00B9, 16r00BA, 16r00BB, 16r00BC, 16r00BD, 16r00BE, 16r00BF,
+16r00C0, 16r00C1, 16r00C2, 16r00C3, 16r00C4, 16r00C5, 16r00C6, 16r00C7, 16r00C8, 16r00C9, 16r00CA, 16r00CB, 16r00CC, 16r00CD, 16r00CE, 16r00CF,
+16r00D0, 16r00D1, 16r00D2, 16r00D3, 16r00D4, 16r00D5, 16r00D6, 16r00D7, 16r00D8, 16r00D9, 16r00DA, 16r00DB, 16r00DC, 16r00DD, 16r00DE, 16r00DF,
+16r00E0, 16r00E1, 16r00E2, 16r00E3, 16r00E4, 16r00E5, 16r00E6, 16r00E7, 16r00E8, 16r00E9, 16r00EA, 16r00EB, 16r00EC, 16r00ED, 16r00EE, 16r00EF,
+16r00F0, 16r00F1, 16r00F2, 16r00F3, 16r00F4, 16r00F5, 16r00F6, 16r00F7, 16r00F8, 16r00F9, 16r00FA, 16r00FB, 16r00FC, 16r00FD, 16r00FE, 16r00FF,
+};
--- /dev/null
+++ b/appl/lib/crc.b
@@ -1,0 +1,45 @@
+implement Crc;
+
+include "crc.m";
+
+init(poly : int, reg : int) : ref CRCstate
+{
+	if (poly == 0)
+		poly = int 16redb88320;
+	tab := array[256] of int;
+	for(i := 0; i < 256; i++){
+		crc := i;
+		for(j := 0; j < 8; j++){
+			c := crc & 1;
+			crc = (crc >> 1) & 16r7fffffff;
+			if(c)
+				crc ^= poly;
+		}
+		tab[i] = crc;
+	}
+	crcs := ref CRCstate;
+	crcs.crc = 0;
+	crcs.crctab = tab;
+	crcs.reg = reg;
+	return crcs;
+}
+
+crc(crcs : ref CRCstate, buf : array of byte, nb : int) : int
+{
+	n := nb;
+	if (n > len buf)
+		n = len buf;
+	crc := crcs.crc;
+	tab := crcs.crctab;
+	crc ^= crcs.reg;
+	for (i := 0; i < n; i++)
+		crc = tab[int(byte crc ^ buf[i])] ^ ((crc >> 8) & 16r00ffffff);
+	crc ^= crcs.reg;
+	crcs.crc = crc;
+	return crc;
+}
+
+reset(crcs : ref CRCstate)
+{
+	crcs.crc = 0;
+}
--- /dev/null
+++ b/appl/lib/crypt/mkfile
@@ -1,0 +1,24 @@
+<../../../mkconfig
+
+TARG=\
+	pkcs.dis\
+	ssl3.dis\
+	sslsession.dis\
+	x509.dis\
+
+MODULES=
+
+SYSMODULES= \
+	asn1.m\
+	daytime.m\
+	keyring.m\
+	pkcs.m\
+	security.m\
+	ssl3.m\
+	sslsession.m\
+	sys.m\
+	x509.m\
+
+DISBIN=$ROOT/dis/lib/crypt
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/crypt/pkcs.b
@@ -1,0 +1,572 @@
+implement PKCS;
+
+include "sys.m";
+	sys				: Sys;
+
+include "keyring.m";
+	keyring				: Keyring;
+	IPint				: import keyring;
+	DESstate			: import keyring;
+
+include "security.m";
+	random				: Random;
+
+include "asn1.m";
+	asn1				: ASN1;
+	Elem, Oid			: import asn1;
+
+include "pkcs.m";
+
+# pkcs object identifiers
+
+objIdTab = array [] of {
+	id_pkcs =>			Oid(array [] of {1,2,840,113549,1}),
+	id_pkcs_1 =>			Oid(array [] of {1,2,840,113549,1,1}),
+	id_pkcs_rsaEncryption => 	Oid(array [] of {1,2,840,113549,1,1,1}),
+	id_pkcs_md2WithRSAEncryption => Oid(array [] of {1,2,840,113549,1,1,2}),
+	id_pkcs_md4WithRSAEncryption => Oid(array [] of {1,2,840,113549,1,1,3}),
+	id_pkcs_md5WithRSAEncryption =>	Oid(array [] of {1,2,840,113549,1,1,4}),
+
+	id_pkcs_3 =>			Oid(array [] of {1,2,840,113549,1,3}),
+	id_pkcs_dhKeyAgreement =>	Oid(array [] of {1,2,840,113549,1,3,1}),
+
+	id_pkcs_5 =>			Oid(array [] of {1,2,840,113549,1,5}),
+	id_pkcs_pbeWithMD2AndDESCBC => 	Oid(array [] of {1,2,840,113549,1,5,1}),
+	id_pkcs_pbeWithMD5AndDESCBC =>	Oid(array [] of {1,2,840,113549,1,5,3}),
+
+	id_pkcs_7 =>			Oid(array [] of {1,2,840,113549,1,7}),
+	id_pkcs_data =>			Oid(array [] of {1,2,840,113549,1,7,1}),
+	id_pkcs_singnedData => 		Oid(array [] of {1,2,840,113549,1,7,2}),
+	id_pkcs_envelopedData =>	Oid(array [] of {1,2,840,113549,1,7,3}),
+	id_pkcs_signedAndEnvelopedData => 	
+					Oid(array [] of {1,2,840,113549,1,7,4}),
+	id_pkcs_digestData =>		Oid(array [] of {1,2,840,113549,1,7,5}),
+	id_pkcs_encryptedData =>	Oid(array [] of {1,2,840,113549,1,7,6}),
+
+	id_pkcs_9 =>			Oid(array [] of {1,2,840,113549,1,9}),
+	id_pkcs_emailAddress => 	Oid(array [] of {1,2,840,113549,1,9,1}),
+	id_pkcs_unstructuredName =>	Oid(array [] of {1,2,840,113549,1,9,2}),
+	id_pkcs_contentType =>		Oid(array [] of {1,2,840,113549,1,9,3}),
+	id_pkcs_messageDigest =>	Oid(array [] of {1,2,840,113549,1,9,4}),
+	id_pkcs_signingTime =>		Oid(array [] of {1,2,840,113549,1,9,5}),
+	id_pkcs_countersignature =>	Oid(array [] of {1,2,840,113549,1,9,6}),
+	id_pkcs_challengePassword =>	Oid(array [] of {1,2,840,113549,1,9,7}),
+	id_pkcs_unstructuredAddress =>	Oid(array [] of {1,2,840,113549,1,9,8}),
+	id_pkcs_extCertAttrs =>		Oid(array [] of {1,2,840,113549,1,9,9}),
+	id_algorithm_shaWithDSS =>	Oid(array [] of {1,3,14,3,2,13})
+};
+
+# [public]
+# initialize PKCS module
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil)
+		return "load sys module failed";
+
+	keyring = load Keyring Keyring->PATH;
+	if(keyring == nil)
+		return "load keyring module failed";
+
+	random = load Random Random->PATH;
+	if(random == nil)
+		return "load random module failed";
+
+	asn1 = load ASN1 ASN1->PATH;
+	if(asn1 == nil)
+		return "load asn1 module failed";
+	asn1->init();
+
+	return "";
+}
+
+# [public]
+# Encrypt data according to PKCS#1, with given blocktype, using given key.
+
+rsa_encrypt(data: array of byte, key: ref RSAKey, blocktype: int)
+	: (string, array of byte)
+{
+	if(key == nil) 
+		return ("null pkcs#1 key", nil);
+	k := key.modlen;
+	dlen := len data;
+	if(k < 12 || dlen > k-11)
+		return ("bad parameters for pkcs#1", nil);
+	padlen := k-3-dlen;
+	pad := random->randombuf(Random->NotQuiteRandom, padlen);
+	for(i:=0; i < padlen; i++) {
+		if(blocktype == 0)
+			pad[i] = byte 0;
+		else if(blocktype == 1)
+			pad[i] = byte 16rff;
+		else if(pad[i] == byte 0)
+			pad[i] = byte 1;
+	}
+	eb := array[k] of byte;
+	eb[0] = byte 0;
+	eb[1] = byte blocktype;
+	eb[2:] = pad[0:];
+	eb[padlen+2] = byte 0;
+	eb[padlen+3:] = data[0:];
+	return ("", rsacomp(eb, key));
+}
+
+# [public]
+# Decrypt data according to PKCS#1, with given key.
+# If public is true, expect a block type of 0 or 1, else 2.
+
+rsa_decrypt(data: array of byte, key: ref RSAKey, public: int) 
+	: (string, array of byte)
+{
+	eb := rsacomp(data, key);
+	k := key.modlen;
+	if(len eb == k) {
+		bt := int eb[1];
+		if(int eb[0] == 0 && ((public && (bt == 0 || bt == 1)) || (!public && bt == 2))) {
+			for(i := 2; i < k; i++)
+				if(eb[i] == byte 0)
+					break;
+			if(i < k-1) {
+				ans := array[k-(i+1)] of byte;
+				ans[0:] = eb[i+1:];
+				return ("", ans);
+			}
+		}
+	}
+	return ("pkcs1 decryption error", nil);
+}
+
+# [private]
+# Do RSA computation on block according to key, and pad
+# result on left with zeros to make it key.modlen long.
+
+rsacomp(block: array of byte, key: ref RSAKey): array of byte
+{
+	x := keyring->IPint.bebytestoip(block);
+	y := x.expmod(key.exponent, key.modulus);
+	ybytes := y.iptobebytes();
+	k := key.modlen;
+	ylen := len ybytes;
+	if(ylen < k) {
+		a := array[k] of { * =>  byte 0};
+		a[k-ylen:] = ybytes[0:];
+		ybytes = a;
+	}
+	else if(ylen > k) {
+		# assume it has leading zeros (mod should make it so)
+		a := array[k] of byte;
+		a[0:] = ybytes[ylen-k:];
+		ybytes = a;
+	}
+	return ybytes;
+}
+
+# [public]
+
+rsa_sign(data: array of byte, sk: ref RSAKey, algid: int): (string, array of byte)
+{
+	# digesting and add proper padding to it
+	ph := padhash(data, algid);
+
+	return rsa_encrypt(ph, sk, 0); # blocktype <- padding with zero
+}
+
+# [public]
+
+rsa_verify(data, signature: array of byte, pk: ref RSAKey, algid: int): int
+{
+	# digesting and add proper padding to it
+	ph := padhash(data, algid);
+    
+	(err, orig) := rsa_decrypt(signature, pk, 0); # blocktype ?
+	if(err != "" || !byte_cmp(orig, ph))
+		return 0;
+
+	return 1;
+}
+
+# [private]
+# padding block A
+PA := array [] of {
+	byte 16r30, byte 16r20, byte 16r30, byte 16r0c, 
+	byte 16r06, byte 16r08, byte 16r2a, byte 16r86, 
+	byte 16r48, byte 16r86, byte 16rf7, byte 16r0d, 
+	byte 16r02
+};
+
+# [private]
+# padding block B
+PB := array [] of {byte 16r05, byte 16r00, byte 16r04, byte 16r10};
+
+# [private]
+# require either md5 or md2 of 16 bytes digest
+# length of padded digest = 13 + 1 + 4 + 16
+
+padhash(data: array of byte, algid: int): array of byte
+{
+	padded := array [34] of byte;
+	case algid {
+	MD2_WithRSAEncryption =>
+		padded[13] = byte 2;
+		# TODO: implement md2 in keyring module
+		# keyring->md2(data, len data, padded[18:], nil);
+
+	MD5_WithRSAEncryption =>	
+		padded[13] = byte 5;
+		keyring->md5(data, len data, padded[18:], nil);
+	* =>
+		return nil;
+	}
+	padded[0:] = PA;
+	padded[14:] = PB;
+
+	return padded;
+}
+
+# [private]
+# compare byte to byte of two array of byte
+
+byte_cmp(a, b: array of byte): int
+{
+	if(len a != len b)
+		return 0;
+
+	for(i := 0; i < len a; i++) {
+		if(a[i] != b[i])
+			return 0;
+	}
+
+	return 1;
+}
+
+# [public]
+
+RSAKey.bits(key: self ref RSAKey): int
+{
+	return key.modulus.bits();
+}
+
+# [public]
+# Decode an RSAPublicKey ASN1 type, defined as:
+#
+#	RSAPublickKey :: SEQUENCE {
+#		modulus INTEGER,
+#		publicExponent INTEGER
+#	}
+
+decode_rsapubkey(a: array of byte): (string, ref RSAKey)
+{
+parse:
+	for(;;) {
+		(err, e) := asn1->decode(a);
+		if(err != "")
+			break parse;
+		(ok, el) := e.is_seq();
+		if(!ok || len el != 2)
+			break parse;
+		modbytes, expbytes: array of byte;
+		(ok, modbytes) = (hd el).is_bigint();
+		if(!ok)
+			break parse;
+		modulus := IPint.bebytestoip(modbytes);
+		# get modlen this way, because sometimes it
+		# comes with leading zeros that are to be ignored!
+		mbytes := modulus.iptobebytes();
+		modlen := len mbytes;
+		(ok, expbytes) = (hd tl el).is_bigint();
+		if(!ok)
+			break parse;
+		exponent := keyring->IPint.bebytestoip(expbytes);
+		return ("", ref RSAKey(modulus, modlen, exponent));
+	}
+	return ("rsa public key: syntax error", nil);
+}
+
+
+# [public]
+# generate a pair of DSS public and private keys
+
+generateDSSKeyPair(strength: int): (ref DSSPublicKey, ref DSSPrivateKey)
+{
+	# TODO: need add getRandBetween in IPint
+	return (nil, nil);
+}
+
+# [public]
+
+dss_sign(a: array of byte, sk: ref DSSPrivateKey): (string, array of byte)
+{
+	#signature, digest: array of byte;
+
+	#case hash {
+	#Keyring->MD4 =>
+	#	digest = array [Keyring->MD4dlen] of byte;
+	#	keyring->md4(a, len a, digest, nil);
+	#Keyring->MD5 =>
+	#	digest = array [Keyring->MD5dlen] of byte;
+	#	keyring->md5(a, len a, digest, nil);
+	#Keyring->SHA =>
+	#	digest = array [Keyring->SHA1dlen] of byte;
+	#	keyring->sha1(a, len a, digest, nil);
+	#* =>
+	#	return ("unknown hash algorithm", nil);
+	#}
+
+	# TODO: add gcd or getRandBetween in Keyring->IPint
+	return ("unsupported error", nil);
+}
+
+# [public]
+
+dss_verify(a, signa: array of byte, pk: ref DSSPublicKey): int
+{
+	unsigned: array of byte;
+
+	#case hash {
+	#Keyring->MD4 =>
+	#	digest = array [Keyring->MD4dlen] of byte;
+	#	keyring->md4(a, len a, digest, nil);
+	#Keyring->MD5 =>
+	#	digest = array [Keyring->MD5dlen] of byte;
+	#	keyring->md5(a, len a, digest, nil);
+	#Keyring->SHA =>
+	#	digest = array [Keyring->SHA1dlen] of byte;
+	#	keyring->sha1(a, len a, digest, nil);
+	#* =>
+	#	return 0;
+	#}
+
+	# get unsigned from signa and compare it with digest
+
+	if(byte_cmp(unsigned, a))
+		return 1;
+
+	return 0;
+}
+
+# [public]
+decode_dsspubkey(a: array of byte): (string, ref DSSPublicKey)
+{
+	return ("unsupported error", nil);
+}
+
+
+# [public]
+# generate DH parameters with prime length at least (default) 512 bits
+
+generateDHParams(primelen: int): ref DHParams
+{
+	# prime - at least 512 bits
+	if(primelen < 512) # DHmodlen
+		primelen = 512;
+
+	# generate prime and base (generator) integers
+	(p, g) := keyring->dhparams(primelen);
+	if(p == nil || g == nil)
+		return nil;
+
+	return ref DHParams(p, g, 0);
+}
+
+# [public]
+# generate public and private key pair
+# Note: use iptobytes as integer to octet string conversion
+#	and bytestoip as octect string to integer reversion
+
+setupDHAgreement(dh: ref DHParams): (ref DHPrivateKey, ref DHPublicKey)
+{
+	if(dh == nil || dh.prime == nil || dh.base == nil)
+		return (nil, nil);
+
+	# prime length in bits
+	bits := dh.prime.bits();
+
+	# generate random private key of length between bits/4 and bits
+	x := IPint.random(bits/4, bits);
+	if(x == nil)
+		return (nil, nil);
+	dh.privateValueLength = x.bits();
+
+	# calculate public key
+	y := dh.base.expmod(x, dh.prime);
+	if(y == nil)
+		return (nil, nil);
+
+	return (ref DHPrivateKey(dh, y, x), ref DHPublicKey(dh, x));
+}
+
+# [public]
+# The second phase of Diffie-Hellman key agreement
+
+computeDHAgreedKey(dh: ref DHParams, mysk, upk: ref IPint)
+	: array of byte
+{
+	if(mysk == nil || upk == nil)
+		return nil;
+
+	# exponential - calculate agreed key (shared secret)
+	z := upk.expmod(mysk, dh.prime);
+
+	# integer to octet conversion
+	return z.iptobebytes();
+}
+
+# [public]
+# ASN1 encoding
+
+decode_dhpubkey(a: array of byte): (string, ref DHPublicKey)
+{
+	return ("unsupported error", nil);
+}
+
+
+# [public]
+# Digest the concatenation of password and salt with count iterations of
+# selected message-digest algorithm (either md2 or md5).
+# The first 8 bytes of the message digest become the DES key.
+# The last 8 bytes of the message digest become the initializing vector IV.
+
+generateDESKey(pw: array of byte, param: ref PBEParams, alg: int)
+	: (ref DESstate, array of byte, array of byte)
+{
+	if(param.iterationCount < 1)
+		return (nil, nil, nil);
+
+	# concanate password and salt
+	pwlen := len pw;
+	pslen := pwlen + len param.salt;
+	ps := array [pslen] of byte;
+	ps[0:] = pw;
+	ps[pwlen:] = param.salt;
+	key, iv: array of byte;
+
+	# digest iterations
+	case alg {
+	PBE_MD2_DESCBC =>
+		ds : ref Keyring->DigestState = nil;
+		# TODO: implement md2 in keyring module
+		#result := array [Keyring->MD2dlen] of byte;
+		#for(i := 0; i < param.iterationCount; i++)
+		#	ds = keyring->md2(ps, pslen, nil, ds);	
+		#keyring->md2(ps, pslen, result, ds);	
+		#key = result[0:8];
+		#iv = result[8:];
+
+	PBE_MD5_DESCBC =>
+		ds: ref Keyring->DigestState = nil;
+		result := array [Keyring->MD5dlen] of byte;
+		for(i := 0; i < param.iterationCount; i++) 
+			ds = keyring->md5(ps, pslen, nil, ds);
+		keyring->md5(ps, pslen, result, ds);
+		key = result[0:8];
+		iv = result[8:];
+
+	* =>
+		return (nil, nil, nil);
+	}
+
+	state := keyring->dessetup(key, iv);
+
+	return (state, key, iv);
+}
+
+# [public]
+# The message M and a padding string PS shall be formatted into
+# an octet string EB
+# 	EB = M + PS
+# where
+#	PS = 1 if M mod 8 = 7;
+#	PS = 2 + 2 if M mod 8 = 6;
+#	...
+#	PS = 8 + 8 + 8 + 8 + 8 + 8 + 8 + 8 if M mod 8 = 0;
+
+pbe_encrypt(state: ref DESstate, m: array of byte): array of byte
+{
+	mlen := len m;
+	padvalue :=  mlen % 8;
+	pdlen := 8 - padvalue;
+
+	eb := array [mlen + pdlen] of byte;
+	eb[0:] = m;
+	for(i := mlen; i < pdlen; i++)
+		eb[i] = byte padvalue;
+
+	keyring->descbc(state, eb, len eb, Keyring->Encrypt);
+
+	return eb;
+}
+
+# [public]
+
+pbe_decrypt(state: ref DESstate, eb: array of byte): array of byte
+{
+	eblen := len eb;
+	if(eblen%8 != 0) # must a multiple of 8 bytes
+		return nil;
+
+	keyring->descbc(state, eb, eblen, Keyring->Decrypt);	
+
+	# remove padding
+	for(i := eblen -8; i < 8; i++) {
+		if(int eb[i] == i) {
+			for(j := i; j < 8; j++)
+				if(int eb[j] != i)
+					break;
+			if(j == 8)
+				break;
+		}
+	}
+
+	return eb[0:i];
+}
+
+# [public]
+
+PrivateKeyInfo.encode(p: self ref PrivateKeyInfo): (string, array of byte)
+{
+
+	return ("unsupported error", nil);
+}
+
+# [public]
+
+PrivateKeyInfo.decode(a: array of byte): (string, ref PrivateKeyInfo)
+{
+	return ("unsupported error", nil);
+}
+
+# [public]
+
+EncryptedPrivateKeyInfo.encode(p: self ref EncryptedPrivateKeyInfo)
+	: (string, array of byte)
+{
+
+	return ("unsupported error", nil);
+}
+
+# [public]
+
+EncryptedPrivateKeyInfo.decode(a: array of byte)
+	: (string, ref EncryptedPrivateKeyInfo)
+{
+	return ("unsupported error", nil);
+}
+
+# [public]
+
+decode_extcertorcert(a: array of byte): (int, int, array of byte)
+{
+	(err, all) := asn1->decode(a);
+	if(err == "") {
+
+	}
+}
+
+# [public]
+
+encode_extcertorcert(a: array of byte, which: int): (int, array of byte)
+{
+
+}
+
--- /dev/null
+++ b/appl/lib/crypt/ssl3.b
@@ -1,0 +1,5557 @@
+#
+# SSL 3.0 protocol 
+#
+implement SSL3;
+
+include "sys.m";				
+	sys					: Sys;
+
+include "keyring.m";				
+	keyring					: Keyring;
+	IPint, DigestState			: import keyring;
+
+include "security.m";				
+	random					: Random;
+	ssl					: SSL;
+
+include "daytime.m";				
+	daytime					: Daytime;
+
+include "asn1.m";
+	asn1					: ASN1;
+
+include "pkcs.m";
+	pkcs					: PKCS;
+	DHParams, DHPublicKey, DHPrivateKey, 
+	RSAParams, RSAKey, 
+	DSSPrivateKey, DSSPublicKey		: import PKCS;
+
+include "x509.m";
+	x509					: X509;
+	Signed,	Certificate, SubjectPKInfo	: import x509;
+
+include "sslsession.m";
+	sslsession				: SSLsession;
+	Session					: import sslsession;
+
+include "ssl3.m";
+
+#
+# debug mode
+#
+SSL_DEBUG					: con 0;
+logfd						: ref Sys->FD;
+
+#
+# version {major, minor}
+#
+SSL_VERSION_2_0					:= array [] of {byte 0, byte 16r02};
+SSL_VERSION_3_0					:= array [] of {byte 16r03, byte 0};
+
+
+# SSL Record Protocol
+
+SSL_CHANGE_CIPHER_SPEC 				: con 20;
+	SSL_ALERT				: con 21;
+	SSL_HANDSHAKE 				: con 22;
+	SSL_APPLICATION_DATA 			: con 23;
+	SSL_V2HANDSHAKE				: con 0; # escape to sslv2
+
+# SSL Application Protocol consists of alert protocol, change cipher spec protocol
+# v2 and v3 handshake protocol and application data protocol. The v2 handshake
+# protocol is included only for backward compatibility
+
+Protocol: adt {
+	pick {
+	pAlert =>
+		alert				: ref Alert;
+	pChangeCipherSpec =>
+		change_cipher_spec		: ref ChangeCipherSpec;
+	pHandshake =>
+		handshake			: ref Handshake;
+	pV2Handshake =>
+		handshake			: ref V2Handshake;
+	pApplicationData =>
+		data				: array of byte;
+	}
+
+	decode: fn(r: ref Record, ctx: ref Context): (ref Protocol, string);
+	encode: fn(p: self ref Protocol, vers: array of byte): (ref Record, string);
+	tostring: fn(p: self ref Protocol): string;
+};
+
+#
+# ssl alert protocol
+#
+SSL_WARNING	 				: con 1; 
+	SSL_FATAL				: con 2;
+
+SSL_CLOSE_NOTIFY				: con 0;
+	SSL_UNEXPECTED_MESSAGE			: con 10;
+	SSL_BAD_RECORD_MAC			: con 20;
+	SSL_DECOMPRESSION_FAILURE		: con 30;
+	SSL_HANDSHAKE_FAILURE 			: con 40;
+	SSL_NO_CERTIFICATE			: con 41;
+	SSL_BAD_CERTIFICATE 			: con 42;
+	SSL_UNSUPPORTED_CERTIFICATE 		: con 43;
+	SSL_CERTIFICATE_REVOKED			: con 44;
+	SSL_CERTIFICATE_EXPIRED			: con 45;
+	SSL_CERTIFICATE_UNKNOWN			: con 46;
+	SSL_ILLEGAL_PARAMETER 			: con 47;
+
+Alert: adt {
+	level 					: int; 
+	description 				: int;
+	
+	tostring: fn(a: self ref Alert): string;
+};
+
+#
+# ssl change cipher spec protocol
+#
+ChangeCipherSpec: adt {
+	value					: int;
+};
+
+#
+# ssl handshake protocol
+#
+SSL_HANDSHAKE_HELLO_REQUEST	 		: con 0; 
+	SSL_HANDSHAKE_CLIENT_HELLO 		: con 1; 
+	SSL_HANDSHAKE_SERVER_HELLO 		: con 2;
+	SSL_HANDSHAKE_CERTIFICATE 		: con 11;
+	SSL_HANDSHAKE_SERVER_KEY_EXCHANGE 	: con 12;
+	SSL_HANDSHAKE_CERTIFICATE_REQUEST 	: con 13; 
+	SSL_HANDSHAKE_SERVER_HELLO_DONE 	: con 14;
+	SSL_HANDSHAKE_CERTIFICATE_VERIFY 	: con 15; 
+	SSL_HANDSHAKE_CLIENT_KEY_EXCHANGE 	: con 16;
+	SSL_HANDSHAKE_FINISHED	 		: con 20; 
+
+Handshake: adt {
+	pick {
+       	HelloRequest =>				
+        ClientHello =>
+		version 			: array of byte; # [2]
+		random 				: array of byte; # [32]
+		session_id 			: array of byte; # <0..32>
+		suites	 			: array of byte; # [2] x
+		compressions			: array of byte; # [1] x
+        ServerHello =>
+		version 			: array of byte; # [2]
+		random 				: array of byte; # [32]
+		session_id 			: array of byte; # <0..32>
+		suite	 			: array of byte; # [2]
+		compression			: byte; # [1]
+	Certificate =>
+		cert_list 			: list of array of byte; # X509 cert chain
+	ServerKeyExchange =>
+		xkey				: array of byte; # exchange_keys
+        CertificateRequest =>
+		cert_types 			: array of byte;
+		dn_list 			: list of array of byte; # DN list
+	ServerHelloDone =>
+        CertificateVerify =>
+		signature			: array of byte;
+        ClientKeyExchange =>
+		xkey				: array of byte;
+       	Finished =>
+		md5_hash			: array of byte; # [16] Keyring->MD5dlen
+		sha_hash 			: array of byte; # [20] Keyring->SHA1dlen
+	}
+
+	decode: fn(buf: array of byte): (ref Handshake, string);
+	encode: fn(hm: self ref Handshake): (array of byte, string);
+	tostring: fn(hm: self ref Handshake): string;
+};
+
+# cipher suites and cipher specs (default, not all supported)
+#	- key exchange, signature, encrypt and digest algorithms
+
+SSL3_Suites := array [] of {
+	NULL_WITH_NULL_NULL => 			array [] of {byte 0, byte 16r00},
+
+	RSA_WITH_NULL_MD5 => 			array [] of {byte 0, byte 16r01},
+	RSA_WITH_NULL_SHA => 			array [] of {byte 0, byte 16r02},
+	RSA_EXPORT_WITH_RC4_40_MD5 => 		array [] of {byte 0, byte 16r03},
+	RSA_WITH_RC4_128_MD5 => 		array [] of {byte 0, byte 16r04},
+	RSA_WITH_RC4_128_SHA => 		array [] of {byte 0, byte 16r05},
+	RSA_EXPORT_WITH_RC2_CBC_40_MD5 => 	array [] of {byte 0, byte 16r06},
+	RSA_WITH_IDEA_CBC_SHA => 		array [] of {byte 0, byte 16r07},
+	RSA_EXPORT_WITH_DES40_CBC_SHA => 	array [] of {byte 0, byte 16r08},
+	RSA_WITH_DES_CBC_SHA => 		array [] of {byte 0, byte 16r09},
+	RSA_WITH_3DES_EDE_CBC_SHA => 		array [] of {byte 0, byte 16r0A},
+
+	DH_DSS_EXPORT_WITH_DES40_CBC_SHA => 	array [] of {byte 0, byte 16r0B},
+	DH_DSS_WITH_DES_CBC_SHA => 		array [] of {byte 0, byte 16r0C},
+	DH_DSS_WITH_3DES_EDE_CBC_SHA => 	array [] of {byte 0, byte 16r0D},
+	DH_RSA_EXPORT_WITH_DES40_CBC_SHA => 	array [] of {byte 0, byte 16r0E},
+	DH_RSA_WITH_DES_CBC_SHA => 		array [] of {byte 0, byte 16r0F},
+	DH_RSA_WITH_3DES_EDE_CBC_SHA => 	array [] of {byte 0, byte 16r10},
+	DHE_DSS_EXPORT_WITH_DES40_CBC_SHA =>	array [] of {byte 0, byte 16r11},
+	DHE_DSS_WITH_DES_CBC_SHA => 		array [] of {byte 0, byte 16r12},
+	DHE_DSS_WITH_3DES_EDE_CBC_SHA => 	array [] of {byte 0, byte 16r13},
+	DHE_RSA_EXPORT_WITH_DES40_CBC_SHA =>	array [] of {byte 0, byte 16r14},
+	DHE_RSA_WITH_DES_CBC_SHA => 		array [] of {byte 0, byte 16r15},
+	DHE_RSA_WITH_3DES_EDE_CBC_SHA => 	array [] of {byte 0, byte 16r16},
+
+	DH_anon_EXPORT_WITH_RC4_40_MD5 => 	array [] of {byte 0, byte 16r17},
+	DH_anon_WITH_RC4_128_MD5 => 		array [] of {byte 0, byte 16r18},
+	DH_anon_EXPORT_WITH_DES40_CBC_SHA =>	array [] of {byte 0, byte 16r19},
+	DH_anon_WITH_DES_CBC_SHA => 		array [] of {byte 0, byte 16r1A},
+	DH_anon_WITH_3DES_EDE_CBC_SHA => 	array [] of {byte 0, byte 16r1B},
+
+	FORTEZZA_KEA_WITH_NULL_SHA => 		array [] of {byte 0, byte 16r1C},
+	FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA =>	array [] of {byte 0, byte 16r1D},
+	FORTEZZA_KEA_WITH_RC4_128_SHA => 	array [] of {byte 0, byte 16r1E},
+};
+
+#
+# key exchange algorithms
+#
+DHmodlen					: con 512; # default length
+
+
+#
+# certificate types
+#
+SSL_RSA_sign 					: con 1;
+	SSL_DSS_sign 				: con 2;
+	SSL_RSA_fixed_DH			: con 3;
+	SSL_DSS_fixed_DH			: con 4;
+	SSL_RSA_emhemeral_DH 			: con 5;
+	SSL_DSS_empemeral_DH 			: con 6;
+	SSL_FORTEZZA_MISSI			: con 20;
+
+#
+# cipher definitions
+#
+SSL_EXPORT_TRUE 				: con 0;
+	SSL_EXPORT_FALSE 			: con 1;
+
+SSL_NULL_CIPHER,
+	SSL_RC4,
+	SSL_RC2_CBC,
+	SSL_IDEA_CBC,
+	SSL_DES_CBC,
+	SSL_3DES_EDE_CBC,
+	SSL_FORTEZZA_CBC			: con iota;
+
+SSL_STREAM_CIPHER,
+	SSL_BLOCK_CIPHER			: con iota;
+
+SSL_NULL_MAC,
+	SSL_MD5,
+	SSL_SHA					: con iota;
+
+#
+# MAC paddings
+#
+SSL_MAX_MAC_PADDING 				: con 48;
+SSL_MAC_PAD1 := array [] of { 
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36, 
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36,
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36, 
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36,
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36, 
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36,
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36, 
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36,
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36, 
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36,
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36, 
+	byte 16r36, byte 16r36, byte 16r36, byte 16r36,
+};
+SSL_MAC_PAD2 := array [] of {
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+	byte 16r5c, byte 16r5c, byte 16r5c, byte 16r5c,
+};
+
+#
+# finished messages
+#
+SSL_CLIENT_SENDER := array [] of {
+	byte 16r43, byte 16r4C, byte 16r4E, byte 16r54};
+SSL_SERVER_SENDER := array [] of {
+	byte 16r53, byte 16r52, byte 16r56, byte 16r52};
+
+#
+# a default distiguished names
+#
+RSA_COMMERCIAL_CA_ROOT_SUBJECT_NAME := array [] of {   
+	byte 16r30, byte 16r5F, byte 16r31, byte 16r0B, 
+	byte 16r30, byte 16r09, byte 16r06, byte 16r03, 
+	byte 16r55, byte 16r04, byte 16r06, byte 16r13, 
+	byte 16r02, byte 16r55, byte 16r53, byte 16r31, 
+	byte 16r20, byte 16r30, byte 16r1E, byte 16r06, 
+	byte 16r03, byte 16r55, byte 16r04, byte 16r0A, 
+	byte 16r13, byte 16r17, byte 16r52, byte 16r53, 
+	byte 16r41, byte 16r20, byte 16r44, byte 16r61, 
+	byte 16r74, byte 16r61, byte 16r20, byte 16r53, 
+	byte 16r65, byte 16r63, byte 16r75, byte 16r72, 
+	byte 16r69, byte 16r74, byte 16r79, byte 16r2C, 
+	byte 16r20, byte 16r49, byte 16r6E, byte 16r63, 
+	byte 16r2E, byte 16r31, byte 16r2E, byte 16r30, 
+	byte 16r2C, byte 16r06, byte 16r03, byte 16r55, 
+	byte 16r04, byte 16r0B, byte 16r13, byte 16r25, 
+	byte 16r53, byte 16r65, byte 16r63, byte 16r75, 
+	byte 16r72, byte 16r65, byte 16r20, byte 16r53, 
+	byte 16r65, byte 16r72, byte 16r76, byte 16r65, 
+	byte 16r72, byte 16r20, byte 16r43, byte 16r65, 
+	byte 16r72, byte 16r74, byte 16r69, byte 16r66, 
+	byte 16r69, byte 16r63, byte 16r61, byte 16r74, 
+	byte 16r69, byte 16r6F, byte 16r6E, byte 16r20, 
+	byte 16r41, byte 16r75, byte 16r74, byte 16r68, 
+	byte 16r6F, byte 16r72, byte 16r69, byte 16r74, 
+	byte 16r79,
+};
+
+# SSL internal status
+USE_DEVSSL,
+	SSL3_RECORD,
+	SSL3_HANDSHAKE,
+	SSL2_HANDSHAKE,
+	CLIENT_SIDE, 				
+	SESSION_RESUMABLE,
+	CLIENT_AUTH,
+	CERT_REQUEST,
+	CERT_SENT,
+	CERT_RECEIVED,
+	OUT_READY,
+	IN_READY				: con  1 << iota;
+
+# SSL internal state
+STATE_EXIT,
+	STATE_CHANGE_CIPHER_SPEC,
+	STATE_HELLO_REQUEST,
+	STATE_CLIENT_HELLO,
+	STATE_SERVER_HELLO,
+	STATE_CLIENT_KEY_EXCHANGE,
+	STATE_SERVER_KEY_EXCHANGE,
+	STATE_SERVER_HELLO_DONE,
+	STATE_CLIENT_CERTIFICATE,
+	STATE_SERVER_CERTIFICATE,
+	STATE_CERTIFICATE_VERIFY,
+	STATE_FINISHED				: con iota;
+
+#
+# load necessary modules
+#
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil)
+		return "ssl3: load sys module failed";
+	logfd = sys->fildes(1);
+
+	keyring = load Keyring Keyring->PATH;
+	if(keyring == nil)
+		return "ssl3: load keyring module failed";
+
+	random = load Random Random->PATH;
+	if(random == nil)
+		return "ssl3: load random module failed";
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return "ssl3: load Daytime module failed";
+
+	pkcs = load PKCS PKCS->PATH;
+	if(pkcs == nil)
+		return "ssl3: load pkcs module failed";
+	pkcs->init();
+
+	x509 = load X509 X509->PATH;
+	if(x509 == nil)
+		return "ssl3: load x509 module failed";
+	x509->init();
+
+	ssl = load SSL SSL->PATH;
+	if(ssl == nil)
+		return "ssl3: load SSL module failed";
+	sslsession = load SSLsession SSLsession->PATH;
+	if(sslsession == nil)
+		return "ssl3: load sslsession module failed";
+	e := sslsession->init();
+	if(e != nil)
+		return "ssl3: sslsession init failed: "+e;
+
+	return "";
+}
+
+log(s: string)
+{
+	a := array of byte (s + "\n");
+	sys->write(logfd, a, len a);
+}
+
+#
+# protocol context
+#
+
+Context.new(): ref Context
+{
+	ctx := ref Context;
+
+	ctx.c = nil;
+	ctx.session = nil;
+	ctx.local_info = nil;
+		
+	ctx.sha_state = nil;
+	ctx.md5_state = nil;
+
+	ctx.status = 0;
+	ctx.state = 0;
+
+	ctx.client_random = array [32] of byte;
+	ctx.server_random = array [32] of byte;
+
+	ctx.cw_mac = nil;
+	ctx.sw_mac = nil;
+	ctx.cw_key = nil;
+	ctx.sw_key = nil;
+	ctx.cw_IV = nil;
+	ctx.sw_IV = nil;
+
+	ctx.in_queue = RecordQueue.new();
+	ctx.in_queue.data = ref Record(0, nil, array [1<<15] of byte) :: nil;
+	ctx.out_queue = RecordQueue.new();
+
+	# set session resumable as default
+	ctx.status |= SESSION_RESUMABLE;
+
+	return ctx;
+}
+
+Context.client(ctx: self ref Context, fd: ref Sys->FD, peername: string, ver: int, info: ref Authinfo)
+	: (string, int)
+{
+	if(SSL_DEBUG)
+		log(sys->sprint("ssl3: Context.Client peername=%s ver=%d\n", peername, ver));
+	if ((ckstr := cksuites(info.suites)) != nil)
+		return (ckstr, ver);
+	# the order is important
+	ctx.local_info = info;
+	ctx.state = STATE_HELLO_REQUEST;
+	e := ctx.connect(fd);
+	if(e != "")
+		return (e, ver);
+	ctx.session = sslsession->get_session_byname(peername);
+
+	# Request to resume an SSL 3.0 session should use an SSL 3.0 client hello
+	if(ctx.session.session_id != nil) {
+		if((ctx.session.version[0] == SSL_VERSION_3_0[0]) &&
+			(ctx.session.version[1] == SSL_VERSION_3_0[1])) {
+			ver = 3;
+			ctx.status |= SSL3_HANDSHAKE;
+			ctx.status &= ~SSL2_HANDSHAKE;
+		}
+	}
+	e = ctx.set_version(ver);
+	if(e != "")
+		return (e, ver);
+	reset_client_random(ctx);
+	ctx.status |= CLIENT_SIDE;
+	e = do_protocol(ctx);
+	if(e != nil)
+		return (e, ver);
+
+	if(ctx.status & SSL3_RECORD)
+		ver = 3;
+	else
+		ver = 2;
+	return (nil, ver);
+}
+
+Context.server(ctx: self ref Context, fd: ref Sys->FD, info: ref Authinfo, client_auth: int)
+	: string
+{
+	if ((ckstr := cksuites(info.suites)) != nil)
+		return ckstr;
+	ctx.local_info = info;
+	if(client_auth)
+		ctx.status |= CLIENT_AUTH;
+	ctx.state = STATE_CLIENT_HELLO;
+	e := ctx.connect(fd);
+	if(e != "")
+		return e;
+	reset_server_random(ctx);
+	e = ctx.set_version(3); # set ssl device to version 3
+	if(e != "")
+		return e;
+	ctx.status &= ~CLIENT_SIDE;
+	e = do_protocol(ctx);
+	if(e != nil)
+		return e;
+
+	return "";
+}
+
+
+Context.use_devssl(ctx: self ref Context)
+{
+	if(!(ctx.status & IN_READY) && !(ctx.status & OUT_READY))
+		ctx.status |= USE_DEVSSL;
+}
+
+Context.set_version(ctx: self ref Context, vers: int): string
+{
+	err := "";
+
+	if(ctx.c == nil) {
+		err = "no connection provided";
+	}
+	else {
+		if(SSL_DEBUG)
+			log("ssl3: record version = " + string vers);
+
+		if(vers == 2) {
+			ctx.status &= ~SSL3_RECORD;
+			ctx.status &= ~SSL3_HANDSHAKE;
+			ctx.status |= SSL2_HANDSHAKE;
+			if (ctx.session != nil)
+				ctx.session.version = SSL_VERSION_2_0;
+		}
+		else if(vers == 3) { # may be sslv2 handshake using ssl3 record
+			ctx.status |= SSL3_RECORD;
+			ctx.status |= SSL3_HANDSHAKE;
+			ctx.status &= ~SSL2_HANDSHAKE; # tmp test only
+			if (ctx.session != nil)
+				ctx.session.version = SSL_VERSION_3_0;
+		}
+		else if(vers == 23) { # may be sslv2 handshake using ssl3 record
+			ctx.status &= ~SSL3_RECORD;
+			ctx.status |= SSL3_HANDSHAKE;
+			ctx.status |= SSL2_HANDSHAKE;
+			vers = 2;
+		}
+		else
+			err = "unsupported ssl device version";
+
+		if((err == "") && (ctx.status & USE_DEVSSL)) {
+			if(sys->fprint(ctx.c.cfd, "ver %d", vers) < 0)
+				err = sys->sprint("ssl3: set ssl device version failed: %r");
+		}
+	}
+
+	return err;
+}
+
+Context.connect(ctx: self ref Context, fd: ref Sys->FD): string
+{
+	err := "";
+
+	if(ctx.status & USE_DEVSSL)
+		(err, ctx.c) = ssl->connect(fd);
+	else {
+		ctx.c = ref Sys->Connection(fd, nil, "");
+		ctx.in_queue.sequence_numbers[0] = 0;
+		ctx.out_queue.sequence_numbers[1] = 0;
+	}
+
+	return err;
+}
+
+Context.read(ctx: self ref Context, a: array of byte, n: int): int
+{	
+	if(ctx.state != STATE_EXIT || !(ctx.status & IN_READY)) {
+		if(SSL_DEBUG)
+			log("ssl3: read not ready\n");
+		return -1;
+	}
+
+	if(ctx.out_queue.data != nil)
+		record_write_queue(ctx);
+
+	if(ctx.status & USE_DEVSSL) {
+		fd := ctx.c.dfd;
+		if(ctx.status & SSL3_RECORD) {
+			buf := array [n+3] of byte;
+			m := sys->read(fd, buf, n+3); # header + n bytes
+			if(m < 3) {
+				if(SSL_DEBUG)
+					log(sys->sprint("ssl3: read failure: %r"));
+				return -1;
+			}
+
+			if(buf[1] != SSL_VERSION_3_0[0] || buf[2] != SSL_VERSION_3_0[1]) {
+				if(SSL_DEBUG)
+					log("ssl3: not ssl version 3 data: header = [" + bastr(buf[0:3]) + "]");
+				return -1;
+			}
+
+			a[0:] = buf[3:m];
+
+			content_type := int buf[0];
+			case content_type {
+			SSL_APPLICATION_DATA =>
+				break;
+			SSL_ALERT =>				
+				if(SSL_DEBUG)
+					log("ssl3: expect application data, got alert: [" + bastr(buf[3:m]) +"]");
+				return 0;
+			SSL_HANDSHAKE =>
+				if(SSL_DEBUG)
+					log("ssl3: expect application data, got handshake message");
+				return 0;
+			SSL_CHANGE_CIPHER_SPEC =>
+				if(SSL_DEBUG)
+					log("ssl3: dynamic change cipher spec not supported yet");
+				return 0;
+			}
+			return m-3;
+		}
+		else
+			return sys->read(fd, a, n);
+	}
+	else {
+		q := ctx.in_queue;
+		got := 0;
+		if(q.fragment) {
+			d := (hd q.data).data;
+			m := q.e - q.b;
+			i := q.e - q.fragment;
+			if(q.fragment > n) {
+				a[0:] = d[i:i+n];
+				q.fragment -= n;
+				got = n;
+			}
+			else {
+				a[0:] = d[i:q.e];
+				got = q.fragment;
+				q.fragment = 0;
+			}
+		}
+out:
+		while(got < n) {
+			err := q.read(ctx, ctx.c.dfd);
+			if(err != "") {	
+				if(SSL_DEBUG)
+					log("ssl3: read: " + err);
+				break;
+			}
+			r := hd q.data;
+			if(ctx.status & SSL3_RECORD) {
+				case r.content_type {
+				SSL_APPLICATION_DATA =>
+					break;
+				SSL_ALERT =>
+					if(SSL_DEBUG)
+						log("ssl3: read: got alert\n\t\t" + bastr(r.data[q.b:q.e]));
+					# delete session id
+					ctx.session.session_id = nil;
+					ctx.status &= ~IN_READY;
+					break out;
+				SSL_CHANGE_CIPHER_SPEC =>
+					if(SSL_DEBUG)
+						log("ssl3: read: got change cipher spec\n");
+				SSL_HANDSHAKE =>
+					if(SSL_DEBUG)
+						log("ssl3: read: got handshake data\n");
+					#do_handshake(ctx, r); # ?
+				* =>
+					if(SSL_DEBUG)
+						log("ssl3: read: unknown data\n");
+				}
+			}
+
+			if((n - got) <= (q.e - q.b)) {
+				a[got:] = r.data[q.b:q.b+n-got];
+				q.fragment = q.e - q.b - n + got;
+				got = n;
+			}
+			else {
+				a[got:] = r.data[q.b:q.e];
+				q.fragment = 0;
+				got += q.e - q.b;
+			}
+		}
+		if(SSL_DEBUG)
+			log(sys->sprint("ssl3: read: returning %d bytes\n", got));
+		return got;
+	}
+}
+
+Context.write(ctx: self ref Context, a: array of byte, n: int): int
+{	
+	if(ctx.state != STATE_EXIT || !(ctx.status & OUT_READY))
+		return-1;
+
+	if(ctx.out_queue.data != nil)
+		record_write_queue(ctx);
+
+	if(ctx.status & USE_DEVSSL) {
+		if(ctx.status & SSL3_RECORD) {
+			buf := array [n+3] of byte;
+			buf[0] = byte SSL_APPLICATION_DATA;
+			buf[1:] = SSL_VERSION_3_0;
+			buf[3:] = a[0:n];
+			n = sys->write(ctx.c.dfd, buf, n+3);
+			if(n > 0)
+				n -= 3;
+		}
+		else
+			n = sys->write(ctx.c.dfd, a, n);
+	}
+	else {
+		q := ctx.out_queue;
+		v := SSL_VERSION_2_0;
+		if(ctx.status&SSL3_RECORD)
+			v = SSL_VERSION_3_0;
+		for(i := 0; i < n;){
+			m := n-i;
+			if(m > q.length)
+				m = q.length;
+			r := ref Record(SSL_APPLICATION_DATA, v, a[i:i+m]);
+			record_write(r, ctx); # return error?		
+			i += m;
+		}
+	}
+	return n;
+}
+
+devssl_read(ctx: ref Context): (ref Record, string)
+{
+	buf := array [Sys->ATOMICIO] of byte;
+	r: ref Record;
+	c := ctx.c;
+
+	n := sys->read(c.dfd, buf, 3);
+	if(n < 0)
+		return (nil, sys->sprint("record read: read failure: %r")); 
+
+	# in case of undetermined, do auto record version detection
+	if((ctx.state == SSL2_STATE_SERVER_HELLO) &&
+		(ctx.status & SSL2_HANDSHAKE) && (ctx.status & SSL3_HANDSHAKE)) {
+
+		fstatus := sys->open(ctx.c.dir + "/status", Sys->OREAD);
+		if(fstatus == nil)
+			return (nil, "open status: " + sys->sprint("%r"));
+		status := array [64] of byte;
+		nbyte := sys->read(fstatus, status, len status);
+		if(nbyte != 1)
+			return (nil, "read status: " + sys->sprint("%r"));
+
+		ver := int status[0];
+
+		if(SSL_DEBUG)
+			log("ssl3: auto record version detect as: " + string ver); 
+
+		# assert ctx.status & SSL2_RECORD true ? before reset
+		if(ver == 2) {
+			ctx.status &= ~SSL3_RECORD;
+			ctx.status |= SSL2_HANDSHAKE;
+			ctx.status &= ~SSL3_HANDSHAKE;
+		}
+		else { 
+			ctx.status |= SSL3_RECORD;
+		}
+	}
+
+	if(ctx.status & SSL3_RECORD) {
+		if(n < 3)
+			return (nil, sys->sprint("record read: read failure: %r")); 
+
+		# assert only major version number
+		if(buf[1] != SSL_VERSION_3_0[0])
+			return (nil, "record read: version mismatch");
+
+		case int buf[0] {
+		SSL_ALERT =>
+			n = sys->read(c.dfd, buf, 5); # read in header plus rest
+			if(n != 5)
+				return (nil, sys->sprint("read alert failed: %r"));
+			r = ref Record(SSL_ALERT, SSL_VERSION_3_0, buf[3:5]);
+
+		SSL_CHANGE_CIPHER_SPEC =>
+			n = sys->read(c.dfd, buf, 4); # read in header plus rest
+			if(n != 4)
+				return (nil, sys->sprint("read change_cipher_spec failed: %r"));
+			r = ref Record(SSL_CHANGE_CIPHER_SPEC, SSL_VERSION_3_0, buf[3:4]);
+
+		SSL_HANDSHAKE =>
+			n = sys->read(c.dfd, buf, 7); # header + msg length
+			if(n != 7)
+				return (nil, sys->sprint("read handshake header + msg length failed: %r"));
+			m := int_decode(buf[4:7]);
+			if(m < 0)
+				return (nil, "read handshake failed: unexpected length");
+			data := array [m+4] of byte;
+			data[0:] = buf[3:7]; # msg type + length
+			if(m != 0) {
+				# need exact m bytes (exclude header), otherwise failure
+				remain := m;
+				now := 4;
+				while(remain > 0) {
+					n = sys->read(c.dfd, buf, remain+3); # header + msg
+					if(n < 3 || int buf[0] != SSL_HANDSHAKE)
+						return (nil, sys->sprint("read handshake msg body failed: %r"));
+					sys->print("expect %d, got %d bytes\n", m, n-3);
+					remain -= n - 3;
+					data[now:] = buf[3:n];
+					now += n - 3;
+				}
+			}
+
+			r = ref Record(SSL_HANDSHAKE, SSL_VERSION_3_0, data);
+		* =>
+			return (nil, "trying to read unknown protocol message");
+		}
+
+		if(SSL_DEBUG)
+			log("ssl3: record_read: \n\theader = \n\t\t" + bastr(buf[0:3])
+			+ "\n\tdata = \n\t\t" + bastr(r.data) + "\n");
+	}
+	# v2 record layer
+	else {
+		# assume the handshake record size less than Sys->ATOMICIO
+		# in most case, this is ok
+		if(n == 3) {
+			n = sys->read(c.dfd, buf[3:], Sys->ATOMICIO - 3);
+			if(n < 0)
+				return (nil, sys->sprint("v2 record read: read failure: %r")); 
+		}
+
+		r = ref Record(SSL_V2HANDSHAKE, SSL_VERSION_2_0, buf[0:n+3]);
+
+		if(SSL_DEBUG)
+			log("ssl3: v2 record_read: \n\tdata = \n\t\t" + bastr(r.data) + "\n");
+	}
+
+	return (r, nil);
+}
+
+record_read(ctx: ref Context): (ref Record, string) 
+{
+	q := ctx.in_queue;
+	if(q.fragment == 0) {
+		err := q.read(ctx, ctx.c.dfd);
+		if(err != "")
+			return (nil, err);
+		q.fragment = q.e - q.b;
+	}
+
+	r := hd q.data;
+	if(ctx.status & SSL3_RECORD) {
+		# confirm only major version number
+		if(r.version[0] != SSL_VERSION_3_0[0])
+			return (nil, "record read: not v3 record");
+
+		case r.content_type {
+		SSL_ALERT =>
+			a := array [2] of byte;
+			n := fetch_data(ctx, a, 2);
+			if(n != 2)
+				return (nil, "read alert failed");
+			r = ref Record(SSL_ALERT, SSL_VERSION_3_0, a);
+
+		SSL_CHANGE_CIPHER_SPEC =>
+			a := array [1] of byte;
+			n := fetch_data(ctx, a, 1);
+			if(n != 1)
+				return (nil, "read change_cipher_spec failed");
+			r = ref Record(SSL_CHANGE_CIPHER_SPEC, SSL_VERSION_3_0, a);
+
+		SSL_HANDSHAKE =>
+			a := array [4] of byte;
+			n := fetch_data(ctx, a, 4);
+			if(n != 4)
+				return (nil, "read message length failed");
+			m := int_decode(a[1:]);
+			if(m < 0)
+				return (nil, "unexpected handshake message length");
+			b := array [m+4] of byte;
+			b[0:] = a;
+			n = fetch_data(ctx, b[4:], m);
+			if(n != m)
+				return (nil, "read message body failed");
+			r = ref Record(SSL_HANDSHAKE, SSL_VERSION_3_0, b);
+		* =>
+			return (nil, "trying to read unknown protocol message");
+		}
+	}
+	# v2 record layer
+	else {
+		r = ref Record(SSL_V2HANDSHAKE, SSL_VERSION_2_0, r.data[q.b:q.e]);
+		q.fragment = 0;
+	}
+
+	return (r, nil);
+}
+
+fetch_data(ctx: ref Context, a: array of byte, n: int): int
+{
+	q := ctx.in_queue;
+	r := hd q.data;
+
+	got := 0;
+	cnt := -1;
+out:
+	while(got < n) {
+		if(q.fragment) {
+			cnt = r.content_type;
+			i := q.e - q.fragment;			
+			if(n-got <= q.fragment) {
+				a[got:] = r.data[i:i+n-got];
+				q.fragment -= n - got;
+				got = n;
+			}
+			else {
+				a[got:] = r.data[i:q.e];
+				got += q.fragment;
+				q.fragment = 0;
+			}
+		}
+		else {
+			err := q.read(ctx, ctx.c.dfd);
+			if(err != "") 
+				break out;
+			if(cnt == -1)
+				cnt = r.content_type;
+			if(ctx.status & SSL3_RECORD) {
+				case r.content_type {
+				SSL_APPLICATION_DATA =>
+					break;
+				* =>
+					if(cnt != r.content_type)
+						break out;
+				}
+			}
+			else {
+				r.content_type = SSL_V2HANDSHAKE;
+			}
+		}
+	}
+	return got;
+}
+
+record_write(r: ref Record, ctx: ref Context)
+{
+	if(ctx.status & USE_DEVSSL) {
+		buf: array of byte;
+		n: int;
+		c := ctx.c;
+
+		if(ctx.status & SSL3_RECORD) {
+			buf = array [3 + len r.data] of byte;
+			buf[0] = byte r.content_type;
+			buf[1:] = r.version; 
+			buf[3:] = r.data;
+			n = sys->write(c.dfd, buf, len buf);
+			if(n < 0 || n != len buf) {
+				if(SSL_DEBUG)
+					log(sys->sprint("ssl3: v3 record write error: %d %r", n));
+				return; # don't terminated until alerts being read
+			}
+		}
+		else {
+			buf = r.data;
+			n = sys->write(c.dfd, buf, len buf);
+			if(n < 0 || n != len buf) {
+				if(SSL_DEBUG)
+					log(sys->sprint("ssl3: v2 record write error: %d %r", n));
+				return; # don't terminated until alerts being read
+			}
+		}
+	}
+	else 
+		ctx.out_queue.write(ctx, ctx.c.dfd, r);
+	
+	# if(SSL_DEBUG) 
+	#	log("ssl3: record_write: \n\t\t" + bastr(buf) + "\n");	
+}
+
+RecordQueue.new(): ref RecordQueue
+{
+	q := ref RecordQueue(
+		ref MacState.null(0),
+		ref CipherState.null(1),
+		1 << 15,
+		array [2] of { * => 0},
+		nil,
+		0,
+		0, # b 
+		0  # e	
+	);
+	return q;
+}
+
+RecordQueue.read(q: self ref RecordQueue, ctx: ref Context, fd: ref Sys->FD): string
+{
+	r := hd q.data;
+	a := r.data;
+	if(ensure(fd, a, 2) < 0)
+		return "no more data";
+	# auto record version detection
+	m, h, pad: int = 0;
+	if(int a[0] < 20 || int a[0] > 23) {
+		ctx.status &= ~SSL3_RECORD;
+		if(int a[0] & 16r80) {
+			h = 2;
+			m = ((int a[0] & 16r7f) << 8) | int a[1];
+			pad = 0;
+		} else {
+			h = 3;
+			m = ((int a[0] & 16r3f) << 8) | int a[1];
+			if(ensure(fd, a[2:], 1) < 0)
+				return "bad v2 record";
+			pad = int a[2];
+			if(pad > m)
+				return "bad v2 pad";
+		}
+		r.content_type = SSL_V2HANDSHAKE;
+		r.version = SSL_VERSION_2_0;
+	}
+	else {
+		ctx.status |= SSL3_RECORD;
+		h = 5;
+		if(ensure(fd, a[2:], 3) < 0)
+			return "bad v3 record";
+		m = ((int a[3]) << 8) | int a[4];
+		r.content_type = int a[0];
+		r.version = a[1:3];
+	}
+	if(ensure(fd, a[h:], m) < 0)
+#		return "data too short";
+		return sys->sprint("data too short wanted %d", m);
+	if(SSL_DEBUG) {
+		log("ssl3: record read\n\tbefore decrypt\n\t\t" + bastr(a[0:m+h]));
+		log(sys->sprint("SSL3=%d\n", ctx.status & SSL3_RECORD));
+	}
+
+	# decrypt (data, pad, mac)
+	pick dec := q.cipherState {
+	null =>
+	rc4 =>
+		keyring->rc4(dec.es, a[h:], m);
+		if (SSL_DEBUG) log("rc4 1");
+	descbc =>
+		keyring->descbc(dec.es, a[h:], m, 1);
+		if (SSL_DEBUG) log("descbc 1");
+	ideacbc =>
+		keyring->ideacbc(dec.es, a[h:], m, 1);
+		if (SSL_DEBUG) log("ideacbc 1");
+	* =>
+	}
+
+	if(SSL_DEBUG)
+		log("ssl3: record read\n\tafter decrypt\n\t\t" + bastr(a[0:m]));
+
+	idata, imac, ipad: int = 0;
+	if(ctx.status & SSL3_RECORD) {
+		if(q.cipherState.block_size > 1){
+			pad = int a[h + m - 1];
+			if(pad >= q.cipherState.block_size)
+				return "bad v3 pad";
+			# pad++;
+			ipad = h+m-pad-1;
+		}
+		else
+			ipad = h+m-pad;
+		imac = ipad - q.macState.hash_size;
+		idata = h;
+	}
+	else {
+		imac = h;
+		idata = imac + q.macState.hash_size;
+		ipad = h + m - pad;
+	}
+	if(tagof q.macState != tagof MacState.null) {
+		if (ctx.status & SSL3_RECORD)
+			mac := q.calcmac(ctx, r.content_type, a, idata, imac-idata);
+		else
+			mac = q.calcmac(ctx, r.content_type, a, idata, ipad+pad-idata);
+		if(bytes_cmp(mac, a[imac:imac+len mac]) < 0)
+			return "bad mac";
+	}
+	q.b = idata;
+	if (ctx.status & SSL3_RECORD)
+		q.e = imac;
+	else
+		q.e = ipad;
+	q.fragment = q.e - q.b;
+
+	if((++q.sequence_numbers[0] == 0) && (ctx.status&SSL3_RECORD))
+		++q.sequence_numbers[1];
+
+	return "";
+}
+
+ensure(fd: ref Sys->FD, a: array of byte, n: int): int
+{
+	i, m: int = 0;
+	while(i < n) {
+		m = sys->read(fd, a[i:], n - i);
+		if(m <= 0) {
+			return -1;
+		}
+		i += m;
+	}
+	return n;
+}
+
+RecordQueue.write(q: self ref RecordQueue, ctx: ref Context, fd: ref Sys->FD, 
+	r: ref Record): string
+{
+	m := len r.data;
+	h, pad: int = 0;
+	if(ctx.status & SSL3_RECORD) {
+		h = 5;
+		if(q.cipherState.block_size > 1) {
+			pad = (m+q.macState.hash_size+1)%q.cipherState.block_size;
+			if (pad)
+				pad = q.cipherState.block_size - pad;
+		}
+	}
+	else {
+		h = 2;
+		if(q.cipherState.block_size > 1) {
+			pad = m%q.cipherState.block_size;
+			if(pad) {
+				pad = q.cipherState.block_size - pad;
+				h++;
+			}
+		}
+	}
+
+	m += pad + q.macState.hash_size;
+	if ((ctx.status & SSL3_RECORD) && q.cipherState.block_size > 1)
+		m++;
+	a := array [h+m] of byte;
+
+	idata, imac, ipad: int = 0;
+	if(ctx.status & SSL3_RECORD) {
+		a[0] = byte r.content_type;
+		a[1:] = r.version;
+		a[3] = byte (m >> 8);			#CJL - netscape ssl3 traces do not show top bit set
+#		a[3] = byte ((m >> 8) | 16r80);	#CJL
+#		a[3] = byte (m | 16r8000) >> 8;
+		a[4] = byte m;
+		idata = h;
+		imac = idata + len r.data;
+		ipad = imac + q.macState.hash_size;
+		if (q.cipherState.block_size > 1)
+			a[h+m-1] = byte pad;
+	}
+	else {
+		if(pad) {
+			a[0] = byte m >> 8;
+			a[2] = byte pad;
+		}
+		else
+			a[0] = byte ((m >> 8) | 16r80);
+		a[1] = byte m;
+		imac = h;
+		idata = imac + q.macState.hash_size;
+		ipad = idata + len r.data;
+	}
+	a[idata:] = r.data;
+	if(pad)
+		a[ipad:] = array [pad] of { * => byte (pad-1)};
+
+	if(tagof q.macState != tagof MacState.null) {
+		if (ctx.status & SSL3_RECORD)
+			a[imac:] = q.calcmac(ctx, r.content_type, a, idata, len r.data);
+		else
+			a[imac:] = q.calcmac(ctx, r.content_type, a, idata, ipad+pad-idata);
+	}
+
+	 if(SSL_DEBUG) {
+		log("ssl3: record write\n\tbefore encrypt\n\t\t" + bastr(a));	
+		log(sys->sprint("SSL3=%d\n", ctx.status & SSL3_RECORD));
+	}
+
+	# encrypt (data, pad, mac)
+	pick enc := q.cipherState {
+	null =>
+	rc4 =>
+		keyring->rc4(enc.es, a[h:], m);
+		if (SSL_DEBUG) log("rc4 0");
+	descbc =>
+		keyring->descbc(enc.es, a[h:], m, 0);
+		if (SSL_DEBUG) log(sys->sprint("descbc 0 %d", m));
+	ideacbc =>
+		keyring->ideacbc(enc.es, a[h:], m, 0);
+		if (SSL_DEBUG) log(sys->sprint("ideacbc 0 %d", m));
+	* =>
+	}
+
+	 if(SSL_DEBUG)
+		log("ssl3: record write\n\tafter encrypt\n\t\t" + bastr(a));
+
+	if(sys->write(fd, a, h+m) < 0)
+		return sys->sprint("ssl3: record write: %r");
+
+	if((++q.sequence_numbers[0] == 0) && (ctx.status&SSL3_RECORD))
+		++q.sequence_numbers[1];
+
+	return "";
+}
+
+RecordQueue.calcmac(q: self ref RecordQueue, ctx: ref Context, cntype: int, a: array of byte, 
+	ofs, n: int) : array of byte
+{
+	digest, b: array of byte;
+
+	if(ctx.status & SSL3_RECORD) {
+		b = array [11] of byte;
+		i := putn(b, 0, q.sequence_numbers[1], 4);
+		i = putn(b, i, q.sequence_numbers[0], 4);
+		b[i++] = byte cntype;
+		putn(b, i, n, 2);
+	}
+	else {
+		b = array [4] of byte;
+		putn(b, 0, q.sequence_numbers[0], 4);
+	}
+
+	# if(SSL_DEBUG)
+	#	log("ssl3: record mac\n\tother =\n\t\t" + bastr(b));
+
+	pick ms := q.macState {
+	md5 =>
+		digest = array [Keyring->MD5dlen] of byte;
+		ds0 := ms.ds[0].copy();
+		if(ctx.status & SSL3_RECORD) {
+			keyring->md5(b, len b, nil, ds0);
+			keyring->md5(a[ofs:], n, digest, ds0);
+			ds1 := ms.ds[1].copy();
+			keyring->md5(digest, len digest, digest, ds1);
+		}
+		else {
+			keyring->md5(a[ofs:], n, nil, ds0);
+			keyring->md5(b, len b, digest, ds0);
+		}
+	sha =>
+		digest = array [Keyring->SHA1dlen] of byte;
+		ds0 := ms.ds[0].copy();
+		if(ctx.status & SSL3_RECORD) {
+			keyring->sha1(b, len b, nil, ds0);
+			keyring->sha1(a[ofs:], n, digest, ds0);
+			ds1 := ms.ds[1].copy();
+			keyring->sha1(digest, len digest, digest, ds1);
+		}
+		else {
+			keyring->sha1(a[ofs:], n, nil, ds0);
+			keyring->sha1(b, len b, digest, ds0);
+		}
+	}			
+	return digest;
+}
+
+set_queues(ctx: ref Context): string
+{
+	sw: array of byte;
+	if(ctx.sw_key != nil) {
+		sw = array [len ctx.sw_key + len ctx.sw_IV] of byte;
+		sw[0:] = ctx.sw_key;
+		sw[len ctx.sw_key:] = ctx.sw_IV;
+	}
+	cw: array of byte;
+	if(ctx.cw_key != nil) {
+		cw = array [len ctx.cw_key + len ctx.cw_IV] of byte;
+		cw[0:] = ctx.cw_key;
+		cw[len ctx.cw_key:] = ctx.cw_IV;
+	}
+
+	err := "";
+	if(ctx.status & USE_DEVSSL) {
+		err = set_secrets(ctx.c, ctx.sw_mac, ctx.cw_mac, sw, cw);
+		if(err == "")
+			err = set_cipher_algs(ctx);
+	}
+	else {
+		err = set_out_queue(ctx);
+		if(err == "")
+			err = set_in_queue(ctx);
+	}
+
+	return err;
+}
+
+set_in_queue(ctx: ref Context): string
+{
+	sw: array of byte;
+	if(ctx.sw_key != nil) {
+		sw = array [len ctx.sw_key + len ctx.sw_IV] of byte;
+		sw[0:] = ctx.sw_key;
+		sw[len ctx.sw_key:] = ctx.sw_IV;
+	}
+
+	err := "";
+	if(ctx.status & USE_DEVSSL) {
+		err = set_secrets(ctx.c, ctx.sw_mac, nil, sw, nil);
+		if(err == "")
+			err = set_cipher_algs(ctx);
+	}
+	else
+		err = set_queue(ctx, ctx.in_queue, ctx.sw_mac, sw);
+
+	return err;
+}
+
+set_out_queue(ctx: ref Context): string
+{
+	cw: array of byte;
+	if(ctx.cw_key != nil) {
+		cw = array [len ctx.cw_key + len ctx.cw_IV] of byte;
+		cw[0:] = ctx.cw_key;
+		cw[len ctx.cw_key:] = ctx.cw_IV;
+	}
+
+	err := "";
+	if(ctx.status & USE_DEVSSL) {
+		err = set_secrets(ctx.c, nil, ctx.cw_mac, nil, cw);
+		if(err == "")
+			err = set_cipher_algs(ctx);
+	}
+	else
+		err = set_queue(ctx, ctx.out_queue, ctx.cw_mac, cw);
+
+	return err;
+}
+
+set_queue(ctx: ref Context, q: ref RecordQueue, mac, key: array of byte): string
+{
+	e := "";
+
+	case ctx.sel_ciph.mac_algorithm {
+	SSL_NULL_MAC =>
+		q.macState = ref MacState.null(0);
+	SSL_MD5 =>
+		ds: array of ref DigestState;
+		if(ctx.status & SSL3_RECORD) {
+			ds = array [2] of ref DigestState;
+			ds[0] = keyring->md5(mac, len mac, nil, nil);
+			ds[1] = keyring->md5(mac, len mac, nil, nil);
+			ds[0] = keyring->md5(SSL_MAC_PAD1, 48, nil, ds[0]);
+			ds[1] = keyring->md5(SSL_MAC_PAD2, 48, nil, ds[1]);
+		}
+		else {
+			ds = array [1] of ref DigestState;
+			ds[0] = keyring->md5(mac, len mac, nil, nil);
+		}
+		q.macState = ref MacState.md5(Keyring->MD5dlen, ds);
+	SSL_SHA =>
+		ds: array of ref DigestState;
+		if(ctx.status & SSL3_RECORD) {
+			ds = array [2] of ref DigestState;
+			ds[0] = keyring->sha1(mac, len mac, nil, nil);
+			ds[1] = keyring->sha1(mac, len mac, nil, nil);
+			ds[0] = keyring->sha1(SSL_MAC_PAD1, 40, nil, ds[0]);
+			ds[1] = keyring->sha1(SSL_MAC_PAD2, 40, nil, ds[1]);
+		}
+		else {
+			ds = array [1] of ref DigestState;
+			ds[0] = keyring->sha1(mac, len mac, nil, nil);
+		}
+		q.macState = ref MacState.sha(Keyring->SHA1dlen, ds);
+	* =>
+		e = "ssl3: digest method: unknown";
+	}
+
+	case ctx.sel_ciph.bulk_cipher_algorithm {
+	SSL_NULL_CIPHER =>
+		q.cipherState = ref CipherState.null(1);
+	SSL_RC4 =>
+		if (SSL_DEBUG) log("rc4setup");
+		rcs := keyring->rc4setup(key);
+		q.cipherState = ref CipherState.rc4(1, rcs);
+	SSL_DES_CBC =>
+		dcs : ref keyring->DESstate;
+
+		if (SSL_DEBUG) log(sys->sprint("dessetup %d", len key));
+		if (len key >= 16)
+			dcs = keyring->dessetup(key[0:8], key[8:16]);
+		else if (len key >= 8)
+			dcs = keyring->dessetup(key[0:8], nil);
+		else
+			e = "ssl3: bad DES key length";
+		q.cipherState = ref CipherState.descbc(8, dcs);
+	SSL_IDEA_CBC =>
+		ics : ref keyring->IDEAstate;
+
+		if (SSL_DEBUG) log(sys->sprint("ideasetup %d", len key));
+		if (len key >= 24)
+			ics = keyring->ideasetup(key[0:16], key[16:24]);
+		else if (len key >= 16)
+			ics = keyring->ideasetup(key[0:16], nil);
+		else
+			e = "ssl3: bad IDEA key length";
+		q.cipherState = ref CipherState.ideacbc(8, ics);
+	SSL_RC2_CBC or
+	SSL_3DES_EDE_CBC or
+	SSL_FORTEZZA_CBC =>
+		e = "ssl3: unsupported cipher";
+	* =>
+		e = "ssl3: unknown cipher";
+	}
+
+	if(ctx.status & SSL3_RECORD) {
+		q.length = 1 << 14;
+		if(tagof q.macState != tagof MacState.null)
+			q.length += 2048;
+	}
+	else {
+		if(q.cipherState.block_size > 1) {
+			q.length = (1<<14) - q.macState.hash_size - 1;
+			q.length -= q.length % q.cipherState.block_size;
+		}
+		else
+			q.length = (1<<15) - q.macState.hash_size - 1;
+	}
+	if(ctx.status & SSL3_RECORD)
+		q.sequence_numbers[0] = q.sequence_numbers[1] = 0;
+
+	return e;
+}
+
+set_cipher_algs(ctx: ref Context) : string
+{
+	e: string;
+
+	algspec := "alg";
+
+	case enc := ctx.sel_ciph.bulk_cipher_algorithm {
+	SSL_NULL_CIPHER =>
+		algspec += " clear";
+	SSL_RC4 => 	# stream cipher
+		algspec += " rc4_128";
+	SSL_DES_CBC => # block cipher
+		algspec += " descbc";
+	SSL_IDEA_CBC => # block cipher
+		algspec += " ideacbc";
+	SSL_RC2_CBC or
+	SSL_3DES_EDE_CBC or
+	SSL_FORTEZZA_CBC =>
+		e = "ssl3: encrypt method: unsupported";
+	* =>
+		e = "ssl3: encrypt method: unknown";
+	}
+
+	case mac := ctx.sel_ciph.mac_algorithm {
+	SSL_NULL_MAC =>
+		algspec += " clear";
+	SSL_MD5 =>
+		algspec += " md5";
+	SSL_SHA =>
+		algspec += " sha1";
+	* =>
+		e = "ssl3: digest method: unknown";
+	}
+
+	e = set_ctl(ctx.c, algspec);
+	if(e != "") {
+		if(SSL_DEBUG)
+			log("failed to set cipher algs: " + e);
+	}
+
+	return e;
+}
+
+set_ctl(c: ref Sys->Connection, s: string): string
+{
+	a := array of byte s;
+	if(sys->write(c.cfd, a, len a) < 0)
+		return sys->sprint("error writing sslctl: %r");
+
+	if(SSL_DEBUG)
+		log("ssl3: set cipher algorithm:\n\t\t" + s + "\n");
+
+	return "";
+}
+
+set_secrets(c: ref Sys->Connection, min, mout, sin, sout: array of byte) : string
+{
+	fmin := sys->open(c.dir + "/macin", Sys->OWRITE);
+	fmout := sys->open(c.dir + "/macout", Sys->OWRITE);
+	fsin := sys->open(c.dir + "/secretin", Sys->OWRITE);
+	fsout := sys->open(c.dir + "/secretout", Sys->OWRITE);
+	if(fmin == nil || fmout == nil || fsin == nil || fsout == nil)
+		return sys->sprint("can't open ssl secret files: %r\n");
+
+	if(sin != nil) {
+		if(SSL_DEBUG)
+			log("ssl3: set encryption secret and IV\n\tsecretin:\n\t\t" + bastr(sin) + "\n");
+		if(sys->write(fsin, sin, len sin) < 0)
+			return sys->sprint("error writing secretin: %r");
+	}
+	if(sout != nil) {
+		if(SSL_DEBUG)
+			log("ssl3: set encryption secret and IV\n\tsecretout:\n\t\t" + bastr(sout) + "\n");
+		if(sys->write(fsout, sout, len sout) < 0)
+			return sys->sprint("error writing secretout: %r");
+	}
+	if(min != nil) {
+		if(SSL_DEBUG)
+			log("ssl3: set digest secret\n\tmacin:\n\t\t" + bastr(min) + "\n");
+		if(sys->write(fmin, min, len min) < 0)
+			return sys->sprint("error writing macin: %r");
+	}
+	if(mout != nil) {
+		if(SSL_DEBUG)
+			log("ssl3: set digest secret\n\tmacout:\n\t\t" + bastr(mout) + "\n");
+		if(sys->write(fmout, mout, len mout) < 0)
+			return sys->sprint("error writing macout: %r");
+	}
+
+	return "";
+}
+
+#
+# description must be alert description
+#
+fatal(description: int, debug_msg: string, ctx: ref Context)
+{
+	if(SSL_DEBUG)
+		log("ssl3: " + debug_msg);
+
+	# TODO: use V2Handshake.Error for v2
+	alert_enque(ref Alert(SSL_FATAL, description), ctx);
+
+	# delete session id
+	ctx.session.session_id = nil;
+
+	ctx.state = STATE_EXIT;
+}
+
+alert_enque(a: ref Alert, ctx: ref Context)
+{
+	p := ref Protocol.pAlert(a);
+
+	protocol_write(p, ctx);
+}
+
+# clean up out queue before switch cipher. this is why
+# change cipher spec differs from handshake message by ssl spec
+
+ccs_enque(cs: ref ChangeCipherSpec, ctx: ref Context)
+{
+	p := ref Protocol.pChangeCipherSpec(cs);
+
+	protocol_write(p, ctx);
+
+	record_write_queue(ctx);
+	ctx.out_queue.data = nil;
+}
+
+handshake_enque(h: ref Handshake, ctx: ref Context)
+{
+	p := ref Protocol.pHandshake(h);
+
+	protocol_write(p, ctx);
+}
+
+protocol_write(p: ref Protocol, ctx: ref Context)
+{
+	record_version := SSL_VERSION_2_0;
+	if(ctx.status & SSL3_RECORD)
+		record_version = SSL_VERSION_3_0;
+	(r, e) := p.encode(record_version);
+	if(e != "") {
+		if(SSL_DEBUG)
+			log("ssl3: protocol_write: " + e);
+		exit;
+	}
+
+	# Note: only for sslv3
+	if((ctx.status&SSL2_HANDSHAKE) && (ctx.status&SSL3_HANDSHAKE)) {
+		if(ctx.state == STATE_HELLO_REQUEST) {
+			e = update_handshake_hash(ctx, r);
+			if(e != "") {
+				if(SSL_DEBUG)
+					log("ssl3: protocol_write: " + e);
+				exit;
+			}
+		}
+	}
+	if((ctx.status&SSL3_HANDSHAKE) && (r.content_type == SSL_HANDSHAKE)) {
+		e = update_handshake_hash(ctx, r);
+		if(e != "") {
+			if(SSL_DEBUG)
+				log("ssl3: protocol_write: " + e);
+			exit;
+		}
+	}
+
+	ctx.out_queue.data = r :: ctx.out_queue.data;
+}
+
+#feed_data(ctx: ref Context, a: array of byte, n: int): int 
+#{
+#
+#}
+
+# FIFO
+record_write_queue(ctx: ref Context)
+{
+	write_queue : list of ref Record;
+
+	wq := ctx.out_queue.data;
+	while(wq != nil) {
+		write_queue = hd wq :: write_queue;
+		wq = tl wq;
+	}
+
+	wq = write_queue;
+	while(wq != nil) {
+		record_write(hd wq, ctx);
+		wq = tl wq;
+	}
+}
+
+# Possible combinations are v2 only, v3 only and both (undetermined). The v2 only must be 
+# v2 handshake and v2 record layer. The v3 only must be v3 handshake and v3 record layer. 
+# If both v2 and v3 are supported, it may be v2 handshake and v2 record layer, or v3 
+# handshake and v3 record layer, or v2 handshake and v3 record layer. In the case of 
+# both, the client should send a v2 client hello message with handshake protocol version v3. 
+
+do_protocol(ctx: ref Context): string
+{
+	r: ref Record;
+	in: ref Protocol;
+	e: string = nil;
+
+	while(ctx.state != STATE_EXIT) {
+
+		if(SSL_DEBUG)
+			log("ssl3: state = " + state_info(ctx));
+
+		# init a new handshake
+		if(ctx.state == STATE_HELLO_REQUEST) {
+			# v2 and v3
+			if((ctx.status&SSL2_HANDSHAKE) && (ctx.status&SSL3_HANDSHAKE)) {
+				ch := ref V2Handshake.ClientHello(
+						SSL_VERSION_3_0,
+						v3tov2specs(ctx.local_info.suites),
+						ctx.session.session_id,
+						ctx.client_random
+					);
+				v2handshake_enque(ch, ctx);
+				in = ref Protocol.pV2Handshake(ch);
+			}
+			# v3 only
+			else if(ctx.status&SSL3_HANDSHAKE) {
+				in = ref Protocol.pHandshake(ref Handshake.HelloRequest());
+			}
+			# v2 only
+			else if(ctx.status&SSL2_HANDSHAKE) {		
+				ch := ref V2Handshake.ClientHello(
+						SSL_VERSION_2_0,
+						v3tov2specs(ctx.local_info.suites),
+						ctx.session.session_id,
+						ctx.client_random[32-SSL2_CHALLENGE_LENGTH:32]
+					);
+				v2handshake_enque(ch, ctx);
+				in = ref Protocol.pV2Handshake(ch);
+			}
+			# unknown version
+			else {
+				e = "unknown ssl device version";
+				fatal(SSL_CLOSE_NOTIFY, "ssl3: " + e, ctx);
+				continue;
+			}
+		}
+
+		if(in == nil) {
+			(r, in, e) = protocol_read(ctx);
+			if(e != "") {
+				fatal(SSL_CLOSE_NOTIFY, "ssl3: " + e, ctx);
+				continue;
+			}
+			if(SSL_DEBUG)
+				log("ssl3: protocol_read: ------\n" + in.tostring());
+		}
+
+		pick p := in {	
+		pAlert =>
+			do_alert(p.alert, ctx);
+
+		pChangeCipherSpec =>
+			if(ctx.state != STATE_CHANGE_CIPHER_SPEC) {
+				e += "ChangeCipherSpec";
+				break;
+			}
+			do_change_cipher_spec(ctx);
+
+		pHandshake =>
+			if(!(ctx.status & SSL3_HANDSHAKE)) {
+				e = "Wrong Handshake";
+				break;
+			}
+			if((ctx.status & SSL3_RECORD) && 
+				(ctx.state == SSL2_STATE_SERVER_HELLO)) {
+				ctx.state = STATE_SERVER_HELLO;
+				ctx.status &= ~SSL2_HANDSHAKE;
+			}
+			e = do_handshake(p.handshake, ctx);
+
+		pV2Handshake =>
+			if(ctx.state != STATE_HELLO_REQUEST) {
+				if(!(ctx.status & SSL2_HANDSHAKE)) {
+					e = "Wrong Handshake";
+					break;
+				}
+				e = do_v2handshake(p.handshake, ctx);
+			}
+			else
+				ctx.state = SSL2_STATE_SERVER_HELLO;
+
+
+		* =>
+			e = "unknown protocol message";
+		}
+
+		if(e != nil) {
+			e = "do_protocol: wrong protocol side or protocol message: " + e;
+			fatal(SSL_UNEXPECTED_MESSAGE, e, ctx);
+		}
+
+		in = nil;
+
+		record_write_queue(ctx);
+		ctx.out_queue.data = nil;
+	}
+
+	return e;
+}
+
+state_info(ctx: ref Context): string
+{
+	info: string;
+
+	if(ctx.status & SSL3_RECORD)
+		info = "\n\tRecord Version 3: ";
+	else
+		info = "\n\tRecord Version 2: ";
+
+	if(ctx.status & SSL2_HANDSHAKE) {
+
+		if(ctx.status & SSL3_HANDSHAKE) {
+			info += "\n\tHandshake Version Undetermined: Client Hello";
+		}
+		else {
+			info += "\n\tHandshake Version 2: ";
+
+			case ctx.state {
+			SSL2_STATE_CLIENT_HELLO =>
+				info += "Client Hello";
+			SSL2_STATE_SERVER_HELLO =>
+				info += "Server Hello";
+			SSL2_STATE_CLIENT_MASTER_KEY =>
+				info += "Client Master Key";
+			SSL2_STATE_SERVER_VERIFY =>
+				info += "Server Verify";
+			SSL2_STATE_REQUEST_CERTIFICATE =>
+				info += "Request Certificate";
+			SSL2_STATE_CLIENT_CERTIFICATE =>
+				info += "Client Certificate";
+			SSL2_STATE_CLIENT_FINISHED =>
+				info += "Client Finished";
+			SSL2_STATE_SERVER_FINISHED =>		
+				info += "Server Finished";
+			SSL2_STATE_ERROR =>
+				info += "Error";
+			}
+		}
+	}
+	else {
+		info = "\n\tHandshake Version 3: ";
+
+		case ctx.state {
+		STATE_EXIT =>
+			info += "Exit";
+
+		STATE_CHANGE_CIPHER_SPEC =>
+			info += "Change Cipher Spec";
+
+		STATE_HELLO_REQUEST =>
+			info += "Hello Request";
+
+		STATE_CLIENT_HELLO =>
+			info += "Client Hello";	
+
+		STATE_SERVER_HELLO =>
+			info += "Server Hello";
+
+		STATE_CLIENT_KEY_EXCHANGE =>
+			info += "Client Key Exchange";
+
+		STATE_SERVER_KEY_EXCHANGE =>
+			info += "Server Key Exchange";
+
+		STATE_SERVER_HELLO_DONE =>
+			info += "Server Hello Done";
+
+		STATE_CLIENT_CERTIFICATE =>
+			info += "Client Certificate";
+
+		STATE_SERVER_CERTIFICATE =>
+			info += "Server Certificate";
+
+		STATE_CERTIFICATE_VERIFY =>
+			info += "Certificate Verify";
+
+		STATE_FINISHED =>
+			info += "Finished";
+		}
+	}
+
+	if(ctx.status & CLIENT_AUTH)
+		info += ": Client Auth";
+	if(ctx.status & CERT_REQUEST)
+		info += ": Cert Request";
+	if(ctx.status & CERT_SENT)
+		info += ": Cert Sent";
+	if(ctx.status & CERT_RECEIVED)
+		info += ": Cert Received";
+
+	return info;
+}
+
+reset_client_random(ctx: ref Context)
+{
+	ctx.client_random[0:] = int_encode(ctx.session.connection_time, 4);
+	ctx.client_random[4:] = random->randombuf(Random->NotQuiteRandom, 28);
+}
+
+reset_server_random(ctx: ref Context)
+{
+	ctx.server_random[0:] = int_encode(ctx.session.connection_time, 4);
+	ctx.server_random[4:] = random->randombuf(Random->NotQuiteRandom, 28);
+}
+
+update_handshake_hash(ctx: ref Context, r: ref Record): string
+{
+	err := "";
+
+	ctx.sha_state = keyring->sha1(r.data, len r.data, nil, ctx.sha_state);
+	ctx.md5_state = keyring->md5(r.data, len r.data, nil, ctx.md5_state);
+	if(ctx.sha_state == nil || ctx.md5_state == nil)
+		err = "update handshake hash failed";
+
+	# if(SSL_DEBUG)
+	#	log("ssl3: update_handshake_hash\n\tmessage_data =\n\t\t" + bastr(r.data) + "\n");
+
+	return err;
+}
+
+# Note:
+#	this depends on the record protocol
+protocol_read(ctx: ref Context): (ref Record, ref Protocol, string)
+{
+	p: ref Protocol;
+	r: ref Record;
+	e: string;
+
+	vers := SSL_VERSION_2_0;
+	if(ctx.status & SSL3_RECORD)
+		vers = SSL_VERSION_3_0;
+	if(ctx.status & USE_DEVSSL)
+		(r, e) = devssl_read(ctx);
+	else
+		(r, e) = record_read(ctx);
+	if(e != "")
+		return (nil, nil, e);
+
+	(p, e) = Protocol.decode(r, ctx);
+	if(e != "")
+		return (r, nil, e);
+
+	return (r, p, nil);
+}
+
+# Alert messages with a level of fatal result in the immediate 
+# termination of the connection and zero out session.
+
+do_alert(a: ref Alert, ctx: ref Context)
+{
+	case a.level {
+	SSL_FATAL =>
+
+		case a.description {
+		SSL_UNEXPECTED_MESSAGE =>
+
+			# should never be observed in communication  
+			# between proper implementations.
+			break;
+
+		SSL_HANDSHAKE_FAILURE =>
+
+			# unable to negotiate an acceptable set of security
+			# parameters given the options available. 
+			break;
+
+		* =>
+			break;
+		}
+
+		ctx.session.session_id = nil;
+		ctx.state = STATE_EXIT;
+
+	SSL_WARNING =>
+
+		case a.description {
+		SSL_CLOSE_NOTIFY =>
+
+			if(SSL_DEBUG)
+				log("ssl3: do_alert SSL_WARNING:SSL_CLOSE_NOTIFY\n");
+			# notifies the recipient that the sender will not 
+			# send any more messages on this connection.
+
+			ctx.state = STATE_EXIT;
+			fatal(SSL_CLOSE_NOTIFY, "ssl3: response close notify", ctx);
+
+		SSL_NO_CERTIFICATE =>
+
+			# A no_certificate alert message may be sent in
+			# response to a certification request if no
+			# appropriate certificate is available.
+
+			if(ctx.state == STATE_CLIENT_CERTIFICATE) {
+				hm := ref Handshake.Certificate(ctx.local_info.certs);
+				handshake_enque(hm, ctx);
+			}
+
+		SSL_BAD_CERTIFICATE or 
+
+			# A certificate was corrupt, contained signatures
+			# that did not verify correctly, etc.
+
+		SSL_UNSUPPORTED_CERTIFICATE or 	
+
+			# A certificate was of an unsupported type.
+
+		SSL_CERTIFICATE_REVOKED or
+
+			# A certificate was revoked by its signer. 	
+
+		SSL_CERTIFICATE_EXPIRED or
+
+			# A certificate has expired or is not currently
+			# valid.	
+
+		SSL_CERTIFICATE_UNKNOWN =>
+
+			# Some other (unspecified) issue arose in
+			# processing the certificate, rendering it
+			# unacceptable.
+			break;
+
+		* =>
+			ctx.session.session_id = nil;
+			fatal(SSL_ILLEGAL_PARAMETER, "ssl3: unknown alert description", ctx);
+		}
+
+	* =>
+		ctx.session.session_id = nil;
+		fatal(SSL_ILLEGAL_PARAMETER, "ssl3: unknown alert level received", ctx);
+	}
+}
+
+# notify the receiving party that subsequent records will
+# be protected under the just-negotiated CipherSpec and keys.
+
+do_change_cipher_spec(ctx: ref Context)
+{
+	# calculate and set new keys
+	if(!(ctx.status & IN_READY)) {
+		e := set_in_queue(ctx);
+		if(e != "") {
+			fatal(SSL_CLOSE_NOTIFY, "do_change_cipher_spec: setup new cipher failed", ctx);
+			return;
+		}
+		ctx.status |= IN_READY;
+
+		if(SSL_DEBUG)
+			log("ssl3: set in cipher done\n");
+	}
+
+	ctx.state = STATE_FINISHED;
+}
+
+
+# process and advance handshake messages, update internal stack and switch to next 
+# expected state(s).
+
+do_handshake(handshake: ref Handshake, ctx: ref Context) : string
+{
+	e := "";
+	
+	pick h := handshake {
+	HelloRequest =>
+		if(!(ctx.status & CLIENT_SIDE) || ctx.state != STATE_HELLO_REQUEST) {
+			e = "HelloRequest";
+			break;
+		}
+		do_hello_request(ctx);
+
+	ClientHello =>
+		if((ctx.status & CLIENT_SIDE) || ctx.state != STATE_CLIENT_HELLO) {
+			e = "ClientHello";
+			break;
+		}
+		do_client_hello(h, ctx);
+
+	ServerHello =>
+		if(!(ctx.status & CLIENT_SIDE) || ctx.state != STATE_SERVER_HELLO) {
+			e = "ServerHello";
+			break;
+		}
+		do_server_hello(h, ctx);
+
+	ClientKeyExchange =>
+		if((ctx.status & CLIENT_SIDE) || ctx.state != STATE_CLIENT_KEY_EXCHANGE) {
+			e = "ClientKeyExchange";
+			break;
+		}
+		do_client_keyex(h, ctx);
+
+	ServerKeyExchange =>
+		if(!(ctx.status & CLIENT_SIDE) || 
+			(ctx.state != STATE_SERVER_KEY_EXCHANGE && ctx.state != STATE_SERVER_HELLO_DONE)) {
+			e = "ServerKeyExchange";
+			break;
+		}
+		do_server_keyex(h, ctx);
+
+	ServerHelloDone =>
+		# diff from SSLRef, to support variant impl
+		if(!(ctx.status & CLIENT_SIDE) || 
+			(ctx.state != STATE_SERVER_HELLO_DONE && ctx.state != STATE_SERVER_KEY_EXCHANGE)) {
+			e = "ServerHelloDone";
+			break;
+		}
+		do_server_done(ctx);
+
+	Certificate =>
+		if(ctx.status & CLIENT_SIDE) {
+			if(ctx.state != STATE_SERVER_CERTIFICATE) {
+				e = "ServerCertificate";
+				break;
+			}
+			do_server_cert(h, ctx);
+		}
+		else {
+			if(ctx.state != STATE_CLIENT_CERTIFICATE) {
+				e = "ClientCertificate";
+				break;
+			}
+			do_client_cert(h, ctx); # server_side
+		}
+
+	CertificateRequest =>
+		if(!(ctx.status & CLIENT_SIDE) || ctx.state != STATE_SERVER_HELLO_DONE
+			|| ctx.state != STATE_SERVER_KEY_EXCHANGE) {
+			e = "CertificateRequest";
+			break;
+		}
+		do_cert_request(h, ctx);
+
+	CertificateVerify =>
+		if((ctx.status & CLIENT_SIDE) || ctx.state != STATE_CERTIFICATE_VERIFY) {
+			e = "CertificateVerify";
+			break;
+		}
+		do_cert_verify(h, ctx);
+
+	Finished =>
+		if(ctx.status & CLIENT_SIDE) {
+			if(ctx.state != STATE_FINISHED) {
+				e = "ClientFinished";
+				break;
+			}
+			do_finished(SSL_CLIENT_SENDER, ctx);
+		}
+		else {
+			if(ctx.state != STATE_FINISHED) {
+				e = "ServerFinished";
+				break;
+			}
+			do_finished(SSL_SERVER_SENDER, ctx);
+		}
+
+	* =>
+		e = "unknown handshake message";
+	}
+
+	if(e != nil)
+		e = "do_handshake: " + e;
+
+	return e;
+}
+
+# [client side]
+# The hello request message may be sent by server at any time, but will be ignored by 
+# the client if the handshake protocol is already underway. It is simple notification 
+# that the client should begin the negotiation process anew by sending a client hello 
+# message.
+
+do_hello_request(ctx: ref Context)
+{
+	# start from new handshake digest state
+	ctx.sha_state = ctx.md5_state = nil;
+
+	# Note:
+	# 	sending ctx.local_info.suites instead of ctx.session.suite, 
+	#	if session is resumable by server, ctx.session.suite will be used.
+	handshake_enque(
+		ref Handshake.ClientHello(
+			ctx.session.version, 
+			ctx.client_random, 
+			ctx.session.session_id,	
+			ctx.local_info.suites, 
+			ctx.local_info.comprs
+		),
+		ctx
+	);
+
+	ctx.state = STATE_SERVER_HELLO;
+}
+
+# [client side]
+# Processes the received server hello handshake message and determines if the session
+# is resumable. (The client sends a client hello using the session id of the session
+# to be resumed. The server then checks its session cache for a match. If a match is
+# FOUND, and the server is WILLING to re-establish the connection under the specified
+# session state, it will send a server hello with the SAME session id value.) If the
+# session is resumed, at this point both client and server must send change cipher
+# spec messages. If the session is not resumable, the client and server perform
+# a full handshake. (On the server side, if a session id match is not found, the
+# server generates a new session id or if the server is not willing to resume, the
+# server uses a null session id).
+
+do_server_hello(hm: ref Handshake.ServerHello, ctx: ref Context)
+{
+	# trying to resume
+	if(bytes_cmp(ctx.session.session_id, hm.session_id) == 0) {
+
+		if(SSL_DEBUG)
+			log("ssl3: session resumed\n");
+
+		ctx.status |= SESSION_RESUMABLE;
+		# avoid version attack
+		if(ctx.session.version[0] != hm.version[0] || 
+			ctx.session.version[1] != hm.version[1]) {
+			fatal(SSL_CLOSE_NOTIFY,	"do_server_hello: version mismatch", ctx);
+			return;
+		}
+
+		ctx.server_random = hm.random;
+
+		# uses the retrieved session suite by server (should be same by client)
+		(ciph, keyx, sign, e) 
+			:= suite_to_spec(hm.suite, SSL3_Suites);
+		if(e != nil) {
+			fatal(SSL_UNEXPECTED_MESSAGE, "server hello: suite not found", ctx);
+			return;
+		}
+		ctx.sel_ciph = ciph;
+		ctx.sel_keyx = keyx;
+		ctx.sel_sign = sign;
+		ctx.sel_cmpr = int ctx.session.compression; # not supported by ssl3 yet
+
+		# calculate keys
+		(ctx.cw_mac, ctx.sw_mac, ctx.cw_key, ctx.sw_key, ctx.cw_IV, ctx.sw_IV) 
+			= calc_keys(ctx.sel_ciph, ctx.session.master_secret, 
+			ctx.client_random, ctx.server_random);
+		
+
+		ctx.state = STATE_CHANGE_CIPHER_SPEC;
+	}
+	else {
+		ctx.status &= ~SESSION_RESUMABLE;
+
+		# On the server side, if a session id match is not found, the
+		# server generates a new session id or if the server is not willing 
+		# to resume, the server uses an empty session id and cannot be
+		# cached by both client and server.
+
+		ctx.session.session_id = hm.session_id;
+		ctx.session.version = hm.version;
+		ctx.server_random = hm.random;
+
+		if(SSL_DEBUG)
+			log("ssl3: do_server_hello:\n\tselected cipher suite =\n\t\t" 
+			+ cipher_suite_info(hm.suite, SSL3_Suites) + "\n");
+
+		(ciph, keyx, sign, e) := suite_to_spec(hm.suite, SSL3_Suites);
+		if(e != nil) {
+			fatal(SSL_UNEXPECTED_MESSAGE, "server hello: suite not found", ctx);
+			return;
+		}
+		
+		ctx.sel_ciph = ciph;
+		ctx.sel_keyx = keyx;
+		ctx.sel_sign = sign;
+		ctx.sel_cmpr = int hm.compression; # not supported by ssl3 yet
+
+		# next state is determined by selected key exchange and signature methods
+		# the ctx.sel_keyx and ctx.sel_sign are completed by the following handshake
+		# Certificate and/or ServerKeyExchange
+
+		if(tagof ctx.sel_keyx == tagof KeyExAlg.DH && 
+			tagof ctx.sel_sign == tagof SigAlg.anon)
+			ctx.state = STATE_SERVER_KEY_EXCHANGE;
+		else
+			ctx.state = STATE_SERVER_CERTIFICATE;
+	}
+}
+
+# [client side]
+# Processes the received server key exchange message. The server key exchange message
+# is sent by the server if it has no certificate, has a certificate only used for
+# signing, or FORTEZZA KEA key exchange is used.
+
+do_server_keyex(hm: ref Handshake.ServerKeyExchange, ctx: ref Context)
+{
+	# install exchange keys sent by server, this may require public key
+	# retrieved from certificate sent by Handshake.Certificate message
+
+	(err, i) := install_server_xkey(hm.xkey, ctx.sel_keyx);
+	if(err == "")
+		err = verify_server_xkey(ctx.client_random, ctx.server_random, hm.xkey, i, ctx.sel_sign);
+
+	if(err == "")
+		ctx.state = STATE_SERVER_HELLO_DONE;
+	else
+		fatal(SSL_HANDSHAKE_FAILURE, "do_server_keyex: " + err, ctx);
+}
+
+# [client side]
+# Processes the received server hello done message by verifying that the server
+# provided a valid certificate if required and checking that the server hello
+# parameters are acceptable.
+
+do_server_done(ctx: ref Context)
+{
+	# On client side, optionally send client cert chain if client_auth 
+	# is required by the server. The server may drop the connection, 
+	# if it does not receive client certificate in the following 
+	# Handshake.ClientCertificate message
+	if(ctx.status & CLIENT_AUTH) {
+		if(ctx.local_info.certs != nil) {
+			handshake_enque(
+				ref Handshake.Certificate(ctx.local_info.certs),
+				ctx
+			);
+			ctx.status |= CERT_SENT;
+		}
+		else {
+			alert_enque(
+				ref Alert(SSL_WARNING, SSL_NO_CERTIFICATE), 
+				ctx
+			);
+		}
+	}
+
+	# calculate premaster secrect, client exchange keys and update ref KeyExAlg 
+	# of the client side
+	(x, pm, e) := calc_client_xkey(ctx.sel_keyx);
+	if(e != "") {
+		fatal(SSL_HANDSHAKE_FAILURE, e, ctx);
+		return;
+	}
+	handshake_enque(ref Handshake.ClientKeyExchange(x), ctx);
+
+	ms := calc_master_secret(pm, ctx.client_random, ctx.server_random);
+	if(ms == nil) {
+		fatal(SSL_HANDSHAKE_FAILURE, "server hello done: calc master secret failed", ctx);
+		return;
+	}
+	# ctx.premaster_secret = pm;
+	ctx.session.master_secret = ms;
+
+	# sending certificate verifiy message if the client auth is required 
+	# and client certificate has been sent,
+	if(ctx.status & CERT_SENT) {
+		sig : array of byte;
+		(md5_hash, sha_hash) 
+			:= calc_finished(nil, ctx.session.master_secret, ctx.sha_state, ctx.md5_state);
+		# check type of client cert being sent
+		pick sk := ctx.local_info.sk {
+		RSA =>
+			hashes := array [36] of byte;
+			hashes[0:] = md5_hash;
+			hashes[16:] = sha_hash;
+			#(e, sig) = pkcs->rsa_sign(hashes, sk, PKCS->MD5_WithRSAEncryption);
+		DSS =>
+			#(e, sig) = pkcs->dss_sign(sha_hash, sk);
+		* =>
+			e = "unknown sign";
+		}
+		if(e != "") {
+			fatal(SSL_HANDSHAKE_FAILURE, "server hello done: sign cert verify failed", ctx);
+			return;
+		}
+		handshake_enque(ref Handshake.CertificateVerify(sig), ctx);
+	}
+
+	ccs_enque(ref ChangeCipherSpec(1), ctx);
+	(ctx.cw_mac, ctx.sw_mac, ctx.cw_key, ctx.sw_key, ctx.cw_IV, ctx.sw_IV) 
+		= calc_keys(ctx.sel_ciph, ctx.session.master_secret, 
+		ctx.client_random, ctx.server_random);
+
+	# set cipher on write channel
+	e = set_out_queue(ctx);
+	if(e != nil) {
+		fatal(SSL_HANDSHAKE_FAILURE, "do_server_done: " + e, ctx);
+		return;
+	}
+	ctx.status |= OUT_READY;
+
+	if(SSL_DEBUG)
+		log("ssl3: set out cipher done\n");
+	(mh, sh) := calc_finished(SSL_CLIENT_SENDER, ctx.session.master_secret, 
+		ctx.sha_state, ctx.md5_state);
+# sending out the Finished msg causes MS https servers to hangup
+#sys->print("RETURNING FROM DO_SERVER_DONE\n");
+#return;
+	handshake_enque(ref Handshake.Finished(mh, sh), ctx);
+
+	ctx.state = STATE_CHANGE_CIPHER_SPEC;
+}
+
+# [client side]
+# Process the received certificate message. 
+# Note:
+#	according to current US export law, RSA moduli larger than 512 bits
+# 	may not be used for key exchange in software exported from US. With
+# 	this message, larger RSA keys may be used as signature only
+# 	certificates to sign temporary shorter RSA keys for key exchange.
+
+do_server_cert(hm: ref Handshake.Certificate, ctx: ref Context)
+{
+	if(hm.cert_list == nil) {
+		fatal(SSL_UNEXPECTED_MESSAGE, "nil peer certificate", ctx);
+		return;
+	}
+
+	# server's certificate is the last one in the chain (reverse required)
+	cl := hm.cert_list;
+	ctx.session.peer_certs = nil;
+	while(cl != nil) {
+		ctx.session.peer_certs = hd cl::ctx.session.peer_certs;
+		cl = tl cl;
+	}
+
+	# TODO: verify certificate chain
+	#	check if in the acceptable dnlist
+	# ctx.sel_keyx.peer_pk = x509->verify_chain(ctx.session.peer_certs);
+	if(SSL_DEBUG)
+		log("ssl3: number certificates got: " + string len ctx.session.peer_certs);
+	peer_cert := hd ctx.session.peer_certs;
+	(e, signed) := x509->Signed.decode(peer_cert);
+	if(e != "") {
+		if(SSL_DEBUG)
+			log("ss3: server certificate: " + e);
+		fatal(SSL_HANDSHAKE_FAILURE, "server certificate: " + e, ctx);
+		return;
+	}
+
+	srv_cert: ref Certificate;
+	(e, srv_cert) = x509->Certificate.decode(signed.tobe_signed);
+	if(e != "") {
+		if(SSL_DEBUG)
+			log("ss3: server certificate: " + e);
+		fatal(SSL_HANDSHAKE_FAILURE, "server certificate: " + e, ctx);
+		return;
+	}
+	if(SSL_DEBUG)
+		log("ssl3: " + srv_cert.tostring());
+
+	# extract and determine byte of user certificate
+	id: int;
+	peer_pk: ref X509->PublicKey;
+	(e, id, peer_pk) = srv_cert.subject_pkinfo.getPublicKey();
+	if(e != "") {
+		if(SSL_DEBUG)
+			log("ss3: server certificate: " + e);
+		fatal(SSL_HANDSHAKE_FAILURE, "server certificate:" + e, ctx);
+		return;
+	}
+
+	pick key := peer_pk {
+	RSA =>
+		# TODO: to allow checking X509v3 KeyUsage extension
+		if((0 && key.pk.modulus.bits() > 512 && ctx.sel_ciph.is_exportable)
+			|| id == PKCS->id_pkcs_md2WithRSAEncryption 
+			|| id == PKCS->id_pkcs_md4WithRSAEncryption 
+			|| id == PKCS->id_pkcs_md5WithRSAEncryption) {
+			pick sign := ctx.sel_sign {
+			anon =>
+				break;
+			RSA =>
+				break;
+			* =>
+				# error
+			}
+			if(ctx.local_info.sk == nil)
+				ctx.sel_sign = ref SigAlg.RSA(nil, key.pk);
+			else {
+				pick mysk := ctx.local_info.sk {
+				RSA =>
+					ctx.sel_sign = ref SigAlg.RSA(mysk.sk, key.pk);
+				* =>
+					ctx.sel_sign = ref SigAlg.RSA(nil, key.pk);
+				}
+			}
+			# key exchange may be tmp RSA, emhemeral DH depending on cipher suite
+			ctx.state = STATE_SERVER_KEY_EXCHANGE;
+		}
+		# TODO: allow id == PKCS->id_rsa
+		else if(id == PKCS->id_pkcs_rsaEncryption) {
+			pick sign := ctx.sel_sign {
+			anon =>
+				break;
+			* =>
+				# error
+			}
+			ctx.sel_sign = ref SigAlg.anon();
+			pick keyx := ctx.sel_keyx {
+			RSA =>
+				keyx.peer_pk = key.pk;
+			* =>
+				# error
+			}
+			ctx.state = STATE_SERVER_HELLO_DONE;
+		}
+		else {
+			# error
+		}
+	DSS =>
+		pick sign := ctx.sel_sign {
+		DSS =>
+			sign.peer_pk = key.pk;
+			break;
+		* =>
+			# error
+		}
+		# should be key exchagne such as emhemeral DH
+		ctx.state = STATE_SERVER_KEY_EXCHANGE;
+	DH =>
+		# fixed DH signed in certificate either by RSA or DSS???
+		pick keyx := ctx.sel_keyx {
+		DH =>
+			keyx.peer_pk = key.pk;
+		* => 
+			# error 
+		}
+		ctx.state = STATE_SERVER_KEY_EXCHANGE;
+	}
+
+	if(e != nil) {
+		fatal(SSL_HANDSHAKE_FAILURE, "do_server_cert: " + e, ctx);
+		return;
+	}
+}
+
+# [client side]
+# Processes certificate request message. A non-anonymous server can optionally
+# request a certificate from the client, if appropriate for the selected cipher
+# suite It is a fatal handshake failure alert for an anonymous server to
+# request client identification.
+
+# TODO: use another module to do x509 certs, lookup and matching rules
+
+do_cert_request(hm: ref Handshake.CertificateRequest, ctx: ref Context)
+{
+	found := 0;
+	for(i := 0; i < len hm.cert_types; i++) {
+		if(ctx.local_info.root_type == int hm.cert_types[i]) {
+			found = 1;
+			break;
+		}
+	}
+	if(!found) {
+		fatal(SSL_HANDSHAKE_FAILURE, "do_cert_request: no required type of cert", ctx);
+		return;		
+	}
+	if(dn_cmp(ctx.local_info.dns, hm.dn_list) < 0) {
+		fatal(SSL_HANDSHAKE_FAILURE, "do_cert_request: no required dn", ctx);
+		return;		
+	}
+	if(ctx.session.peer_certs == nil) {
+		fatal(SSL_NO_CERTIFICATE, "certificate request: no peer certificates", ctx);
+		return;
+	}
+
+	ctx.status |= CLIENT_AUTH;
+}
+
+dn_cmp(a, b: list of array of byte): int
+{
+	return -1;
+}
+
+# [server side]
+# Process client hello message. 
+
+do_client_hello(hm: ref Handshake.ClientHello, ctx: ref Context)
+{
+	sndm : ref Handshake;
+	e : string;
+
+	if(hm.version[0] != SSL_VERSION_3_0[0] || hm.version[1] != SSL_VERSION_3_0[1]) { 
+		fatal(SSL_UNEXPECTED_MESSAGE, "client hello: version mismatch", ctx);
+		return;
+	}
+	# else SSL_VERSION_2_0
+
+	if(hm.session_id != nil) { # trying to resume
+		if(ctx.status & SESSION_RESUMABLE) {
+			s := sslsession->get_session_byid(hm.session_id);
+			if(s == nil) {
+				fatal(SSL_UNEXPECTED_MESSAGE, "client hello: retrieve nil session", ctx);
+				return;
+			}
+
+			if(s.version[0] != hm.version[0] || s.version[1] != hm.version[1]) {
+				# avoid version attack
+				fatal(SSL_UNEXPECTED_MESSAGE, "client hello: protocol mismatch", ctx);
+				return;
+			}
+
+			reset_server_random(ctx);
+			ctx.client_random = hm.random;
+
+			sndm = ref Handshake.ServerHello(s.version, ctx.server_random, 
+				s.session_id, s.suite, s.compression);
+			handshake_enque(sndm, ctx);
+
+			ccs_enque(ref ChangeCipherSpec(1), ctx);
+			# use existing master_secret, calc keys
+			(ctx.cw_mac, ctx.sw_mac, ctx.cw_key, ctx.sw_key, ctx.cw_IV, ctx.sw_IV) 
+				= calc_keys(ctx.sel_ciph, ctx.session.master_secret, ctx.client_random, 
+				ctx.server_random);
+			e = set_out_queue(ctx);
+			if(e != nil) {
+				fatal(SSL_CLOSE_NOTIFY,	"client hello: setup new cipher failure", ctx);
+				return;
+			}
+			if(SSL_DEBUG)
+				log("do_client_hello: set out cipher done\n");
+
+			(md5_hash, sha_hash) := calc_finished(SSL_SERVER_SENDER, 
+				s.master_secret, ctx.sha_state, ctx.md5_state);
+
+			handshake_enque(ref Handshake.Finished(md5_hash, sha_hash), ctx);
+			
+			ctx.session = s;
+			ctx.state = STATE_CHANGE_CIPHER_SPEC;
+			return;
+		}
+
+		fatal(SSL_CLOSE_NOTIFY,	"client hello: resume session failed", ctx);
+		return;		
+	}
+
+	ctx.session.version = hm.version;
+	if(ctx.session.peer != nil) {		
+		ctx.session.session_id = random->randombuf(Random->NotQuiteRandom, 32);
+		if(ctx.session.session_id == nil) {
+			fatal(SSL_CLOSE_NOTIFY,	"client hello: generate session id failed", ctx);
+			return;
+		}
+	}
+
+	suite := find_cipher_suite(hm.suites, ctx.local_info.suites);
+	if(suite != nil) {
+		fatal(SSL_HANDSHAKE_FAILURE, "client hello: find cipher suite failed", ctx);
+		return;
+	}
+		
+	(ctx.sel_ciph, ctx.sel_keyx, ctx.sel_sign, e) = suite_to_spec(suite, SSL3_Suites);
+	if(e != nil) {
+		fatal(SSL_HANDSHAKE_FAILURE, "client hello: find cipher suite failed" + e, ctx);
+		return;
+	}
+
+	# not supported by ssl3 yet
+	ctx.sel_cmpr = int hm.compressions[0];
+	ctx.client_random = hm.random;
+	ctx.sha_state = nil;
+	ctx.md5_state = nil;
+
+	sndm = ref Handshake.ServerHello(ctx.session.version, ctx.server_random, 
+		ctx.session.session_id, ctx.session.suite, ctx.session.compression);
+	handshake_enque(sndm, ctx);
+
+	# set up keys based on algorithms
+
+	if(tagof ctx.sel_keyx != tagof KeyExAlg.DH) {
+		if(ctx.local_info.certs == nil || ctx.local_info.sk == nil) {
+			fatal(SSL_HANDSHAKE_FAILURE, "client hello: no local cert or key", ctx);
+			return;
+		}
+
+		sndm = ref Handshake.Certificate(ctx.local_info.certs);
+		handshake_enque(sndm, ctx);
+	}
+
+	if(tagof ctx.sel_keyx != tagof KeyExAlg.RSA || 
+		tagof ctx.sel_sign != tagof SigAlg.anon) {
+		params, signed_params, xkey: array of byte;
+		(params, e) = calc_server_xkey(ctx.sel_keyx);
+		if(e == "")
+			(signed_params, e) = sign_server_xkey(ctx.sel_sign, params, 
+				ctx.client_random, ctx.server_random); 
+		if(e != "")
+			
+		n := len params + 2 + len signed_params;
+		xkey = array [n] of byte;
+		xkey[0:] = params;
+		xkey[len params:] = int_encode(len signed_params, 2);
+		xkey[len params+2:] = signed_params;
+		handshake_enque(ref Handshake.ServerKeyExchange(xkey), ctx);
+	}
+
+	if(ctx.status & CLIENT_AUTH) {
+		sndm = ref Handshake.CertificateRequest(ctx.local_info.types, ctx.local_info.dns);
+		handshake_enque(sndm, ctx);
+
+		ctx.status |= CERT_REQUEST;
+		ctx.state = STATE_CLIENT_CERTIFICATE;
+	}
+	else
+		ctx.state = STATE_CLIENT_KEY_EXCHANGE;
+
+	handshake_enque(ref Handshake.ServerHelloDone(), ctx);
+}
+
+# [server side]
+# Process the received client key exchange message. 
+
+do_client_keyex(hm: ref Handshake.ClientKeyExchange, ctx: ref Context)
+{
+	(premaster_secret, err) := install_client_xkey(hm.xkey, ctx.sel_keyx);
+	if(err != "") {
+		fatal(SSL_HANDSHAKE_FAILURE, err, ctx);
+		return;
+	}
+		
+	ctx.session.master_secret = calc_master_secret(premaster_secret, 
+		ctx.client_random, ctx.server_random);
+
+	if(ctx.status & CERT_RECEIVED)	
+		ctx.state = STATE_CERTIFICATE_VERIFY;
+	else
+		ctx.state = STATE_CHANGE_CIPHER_SPEC;
+}
+
+# [server side]
+# Process the received certificate message from client. 
+
+do_client_cert(hm: ref Handshake.Certificate, ctx: ref Context)
+{
+	ctx.session.peer_certs = hm.cert_list;
+	
+	# verify cert chain and determine the type of cert
+	# ctx.peer_info.sk = x509->verify_chain(ctx.session.peer_certs);
+	# if(ctx.peer_info.key == nil) {
+	#	fatal(SSL_HANDSHAKE_FAILURE, "client certificate: cert verify failed", ctx);
+	#	return;
+	# }
+
+	ctx.status |= CERT_RECEIVED;
+
+	ctx.state = STATE_CLIENT_KEY_EXCHANGE;
+}
+
+# [server side]
+# Process the received certificate verify message from client.
+
+do_cert_verify(hm: ref Handshake.CertificateVerify, ctx: ref Context)
+{
+	if(ctx.status & CERT_RECEIVED) {
+		# exp : array of byte;
+		(md5_hash, sha_hash) 
+			:= calc_finished(nil, ctx.session.master_secret, ctx.sha_state, ctx.md5_state);
+		ok := 0;
+		pick upk := ctx.sel_sign {
+		RSA =>
+			hashes := array [36] of byte;
+			hashes[0:] = md5_hash;
+			hashes[16:] = sha_hash;
+			ok = pkcs->rsa_verify(hashes, hm.signature, upk.peer_pk, PKCS->MD5_WithRSAEncryption);
+		DSS =>
+			ok = pkcs->dss_verify(sha_hash, hm.signature, upk.peer_pk);
+		}
+
+		if(!ok) {
+			fatal(SSL_HANDSHAKE_FAILURE, "do_cert_verify: client auth failed", ctx);
+			return;
+		}
+	}
+	else {
+		alert_enque(ref Alert(SSL_WARNING, SSL_NO_CERTIFICATE), ctx);
+		return;
+	}
+
+	ctx.state = STATE_CHANGE_CIPHER_SPEC;
+}
+
+# [client or server side]
+# Process the received finished message either from client or server. 
+
+do_finished(sender: array of byte, ctx: ref Context)
+{
+	# setup write_cipher if not yet
+	if(!(ctx.status & OUT_READY)) {
+		ccs_enque(ref ChangeCipherSpec(1), ctx);
+		e := set_out_queue(ctx);
+		if(e != nil) {
+			fatal(SSL_CLOSE_NOTIFY, "do_finished: setup new cipher failed", ctx);
+			return;
+		}
+		ctx.status |= OUT_READY;
+
+		if(SSL_DEBUG)
+			log("ssl3: set out cipher done\n");
+
+		(md5_hash, sha_hash) := calc_finished(sender, ctx.session.master_secret, 
+			ctx.sha_state, ctx.md5_state);
+		handshake_enque(ref Handshake.Finished(md5_hash, sha_hash), ctx);
+	}
+
+	ctx.state = STATE_EXIT; # normal
+
+	# clean read queue
+	ctx.in_queue.fragment = 0;
+
+	sslsession->add_session(ctx.session);
+
+	if(SSL_DEBUG)
+		log("ssl3: add session to session database done\n");
+}
+
+install_client_xkey(a: array of byte, keyx: ref KeyExAlg): (array of byte, string)
+{
+	pmaster, x : array of byte;
+	err := "";
+	pick kx := keyx {
+	DH =>
+		i := 0;
+		(kx.peer_pk, i) = dh_params_decode(a);
+		if(kx.peer_pk != nil) 
+			pmaster = pkcs->computeDHAgreedKey(kx.sk.param, kx.sk.sk, kx.peer_pk.pk);
+		else
+			err = "decode dh params failed";
+	RSA =>
+		(err, x) = pkcs->rsa_decrypt(a, kx.sk, 2);
+		if(err != "" || len x != 48) {
+			err = "impl error";
+		}
+		else {
+			if(x[0] != SSL_VERSION_3_0[0] && x[1] != SSL_VERSION_3_0[1])
+				err = "version wrong: possible version attack";
+			else
+				pmaster = x[2:];
+		}
+	FORTEZZA_KEA =>
+		err = "Fortezza unsupported";
+	}
+	return (pmaster, err);
+}
+
+install_server_xkey(a: array of byte, keyx: ref KeyExAlg): (string, int)
+{
+	err := "";
+	i := 0;
+
+	pick kx := keyx {
+	DH =>
+		(kx.peer_pk, i) = dh_params_decode(a);
+		if(kx.peer_pk != nil) 
+			kx.peer_params = kx.peer_pk.param;
+	RSA =>
+		peer_tmp: ref RSAParams;
+		(peer_tmp, i, err) = rsa_params_decode(a);
+		if(err == "") {
+			modlen := len peer_tmp.modulus.iptobebytes();
+			kx.peer_pk = ref RSAKey(peer_tmp.modulus, modlen, peer_tmp.exponent);
+		}
+	FORTEZZA_KEA =>	
+		return ("Fortezza unsupported", i);
+	}
+
+	return (err, i);
+}
+
+verify_server_xkey(crand, srand: array of byte, a: array of byte, i : int, sign: ref SigAlg)
+	: string
+{
+	pick sg := sign {
+	anon => 
+	RSA =>
+		lb := a[0:i]::crand::srand::nil;
+		(exp, nil, nil) := md5_sha_hash(lb, nil, nil);
+		ok := pkcs->rsa_verify(exp, a[i+2:], sg.peer_pk, PKCS->MD5_WithRSAEncryption); 
+		if(!ok)
+			return "RSA sigature verification failed";
+	DSS =>
+		lb := a[0:i]::crand::srand::nil;
+		(exp, nil) := sha_hash(lb, nil);
+		ok := pkcs->dss_verify(exp, a[i+2:], sg.peer_pk); 
+		if(!ok)
+			return "DSS sigature verification failed";
+	}
+
+	return "";
+}
+
+calc_client_xkey(keyx: ref KeyExAlg): (array of byte, array of byte, string)
+{
+	pm, x : array of byte;
+	err := "";
+	pick kx := keyx {
+	DH =>	
+		# generate our own DH keys based on DH params of peer side
+		(kx.sk, kx.exch_pk) = pkcs->setupDHAgreement(kx.peer_params);
+		# TODO: need check type of client cert if(!ctx.status & CLIENT_AUTH)
+		# 	for implicit case
+		(x, err) = dh_exchpub_encode(kx.exch_pk);
+		pm = pkcs->computeDHAgreedKey(kx.sk.param, kx.sk.sk, kx.peer_pk.pk);
+	RSA =>
+		pm = array [48] of byte;
+		pm[0:] = SSL_VERSION_3_0; # against version attack
+		pm[2:] = random->randombuf(Random->NotQuiteRandom, 46);
+		(err, x) = pkcs->rsa_encrypt(pm, kx.peer_pk, 2);
+	FORTEZZA_KEA =>	
+		err = "Fortezza unsupported";
+	}
+	if(SSL_DEBUG)
+		log("ssl3: calc_client_xkey: " + bastr(x));
+	return (x, pm, err);
+}
+
+calc_server_xkey(keyx: ref KeyExAlg): (array of byte, string)
+{
+	params: array of byte;
+	err: string;
+	pick kx := keyx {
+	DH =>
+		(kx.sk, kx.exch_pk) = pkcs->setupDHAgreement(kx.params);
+		(params, err) = dh_params_encode(kx.exch_pk);
+	RSA =>
+		tmp := ref RSAParams(kx.export_key.modulus, kx.export_key.exponent);
+		(params, err) = rsa_params_encode(tmp);
+
+	FORTEZZA_KEA =>	
+		err = "Fortezza unsupported";
+	}
+	return (params, err);
+}
+
+sign_server_xkey(sign: ref SigAlg, params, cr, sr: array of byte): (array of byte, string)
+{
+	signed_params: array of byte;
+	err: string;
+	pick sg := sign {
+	anon =>
+	RSA =>
+		lb := cr::sr::params::nil;
+		(hashes, nil, nil) := md5_sha_hash(lb, nil, nil);
+		(err, signed_params) = pkcs->rsa_sign(hashes, sg.sk, PKCS->MD5_WithRSAEncryption);
+	DSS =>
+		lb := cr::sr::params::nil;
+		(hashes, nil) := sha_hash(lb, nil);
+		(err, signed_params) = pkcs->dss_sign(hashes, sg.sk);
+	}
+	return (signed_params, err);
+}
+
+# ssl encoding of DH exchange public key
+
+dh_exchpub_encode(dh: ref DHPublicKey): (array of byte, string)
+{
+	if(dh != nil) {		
+		yb := dh.pk.iptobebytes();	
+		if(yb != nil) {
+			n := 2 + len yb;
+			a := array [n] of byte;
+			i := 0;
+			a[i:] = int_encode(len yb, 2);			
+			i += 2;
+			a[i:] = yb;
+			return (a, nil);
+		}
+	}
+	return (nil, "nil dh params");
+}
+
+dh_params_encode(dh: ref DHPublicKey): (array of byte, string)
+{
+	if(dh != nil && dh.param != nil) {
+		pb := dh.param.prime.iptobebytes();		
+		gb := dh.param.base.iptobebytes();
+		yb := dh.pk.iptobebytes();	
+		if(pb != nil && gb != nil && yb != nil) {
+			n := 6 + len pb + len gb + len yb;
+			a := array [n] of byte;
+			i := 0;
+			a[i:] = int_encode(len pb, 2);			
+			i += 2;
+			a[i:] = pb;					
+			i += len pb;
+			a[i:] = int_encode(len gb, 2);			
+			i += 2;
+			a[i:] = gb;					
+			i += len gb;
+			a[i:] = int_encode(len yb, 2);			
+			i += 2;
+			a[i:] = yb;					
+			i += len yb;
+			return (a, nil);
+		}
+	}
+	return (nil, "nil dh public key");
+}
+
+dh_params_decode(a: array of byte): (ref DHPublicKey, int)
+{
+	i := 0;
+	for(;;) {
+		n := int_decode(a[i:i+2]);			
+		i += 2;
+		if(i+n > len a)
+			break;
+		p := a[i:i+n];					
+		i += n; 
+		n = int_decode(a[i:i+2]);			
+		i += 2;
+		if(i+n > len a)
+			break;
+		g := a[i:i+n];					
+		i += n;
+		n = int_decode(a[i:i+2]);			
+		i += 2;
+		if(i+n > len a)
+			break;
+		Ys := a[i:i+n];				
+		i += n;
+
+		if(SSL_DEBUG)
+			log("ssl3: dh_params_decode:" + "\n\tp =\n\t\t" + bastr(p)
+			+ "\n\tg =\n\t\t" + bastr(g) + "\n\tYs =\n\t\t" + bastr(Ys) + "\n");
+
+		# don't care privateValueLength
+		param := ref DHParams(IPint.bebytestoip(p), IPint.bebytestoip(g), 0);
+		return (ref DHPublicKey(param, IPint.bebytestoip(Ys)), i);
+	}
+	return (nil, i);
+}
+
+rsa_params_encode(rsa_params: ref RSAParams): (array of byte, string)
+{
+	if(rsa_params != nil) {
+		mod := rsa_params.modulus.iptobebytes();
+		exp := rsa_params.exponent.iptobebytes();
+		if(mod != nil || exp != nil) {
+			n := 4 + len mod + len exp;
+			a := array [n] of byte;
+			i := 0;
+			a[i:] = int_encode(len mod, 2); 		
+			i += 2;
+			a[i:] = mod;					
+			i += len mod;
+			a[i:] = int_encode(len exp, 2);			
+			i += 2;
+			a[i:] = exp;					
+			i += len exp;
+			return (a, nil);
+		}
+	}
+	return (nil, "nil rsa params");
+}
+
+rsa_params_decode(a: array of byte): (ref RSAParams, int, string)
+{
+	i := 0;
+	for(;;) {
+		if(len a < 2)
+			break;
+		n := int_decode(a[i:i+2]);
+		i += 2;
+		if(n < 0 || n + i > len a)
+			break;
+		mod := a[i:i+n];				
+		i += n;
+		n = int_decode(a[i:i+2]);			
+		i += 2;
+		if(n < 0 || n + i > len a)
+			break;
+		exp := a[i:i+n];				
+		i += n;
+		m := i;
+		modulus := IPint.bebytestoip(mod);
+		exponent := IPint.bebytestoip(exp);
+
+		if(SSL_DEBUG)
+			log("ssl3: decode RSA params\n\tmodulus = \n\t\t" + bastr(mod)
+			+ "\n\texponent = \n\t\t" + bastr(exp) + "\n");
+
+		if(len a < i+2)
+			break;
+		n = int_decode(a[i:i+2]);
+		i += 2;
+		if(len a != i + n)	
+			break;
+		return (ref RSAParams(modulus, exponent), m, nil);
+	}
+	return (nil, i, "encoding error");
+}
+
+# md5_hash       MD5(master_secret + pad2 +
+#                     MD5(handshake_messages + Sender +
+#                          master_secret + pad1));
+# sha_hash       SHA(master_secret + pad2 +
+#                     SHA(handshake_messages + Sender +
+#                          master_secret + pad1));
+#
+# handshake_messages  All of the data from all handshake messages
+#                     up to but not including this message.  This
+#                     is only data visible at the handshake layer
+#                     and does not include record layer headers.
+#
+# sender [4], master_secret [48]
+# pad1 and pad2, 48 bytes for md5, 40 bytes for sha
+
+calc_finished(sender, master_secret: array of byte, sha_state, md5_state: ref DigestState)
+	: (array of byte, array of byte)
+{
+	sha_value := array [Keyring->SHA1dlen] of byte;
+	md5_value := array [Keyring->MD5dlen] of byte;
+	sha_inner := array [Keyring->SHA1dlen] of byte;
+	md5_inner := array [Keyring->MD5dlen] of byte;
+
+	lb := master_secret::SSL_MAC_PAD1[0:48]::nil;
+	if(sender != nil)
+		lb = sender::lb;
+	(md5_inner, nil) = md5_hash(lb, md5_state);
+
+	lb = master_secret::SSL_MAC_PAD1[0:40]::nil;
+	if(sender != nil)
+		lb = sender::lb;
+	(sha_inner, nil) = sha_hash(lb, sha_state);
+
+	(md5_value, nil) = md5_hash(master_secret::SSL_MAC_PAD2[0:48]::md5_inner::nil, nil);
+	(sha_value, nil) = sha_hash(master_secret::SSL_MAC_PAD2[0:40]::sha_inner::nil, nil);
+
+	# if(SSL_DEBUG)
+	#	log("ssl3: calc_finished:" 
+	#	+ "\n\tmd5_inner = \n\t\t" + bastr(md5_inner) 
+	#	+ "\n\tsha_inner = \n\t\t" + bastr(sha_inner)
+	#	+ "\n\tmd5_value = \n\t\t" + bastr(md5_value)
+	#	+ "\n\tsha_value = \n\t\t" + bastr(sha_value) 
+	#	+ "\n");
+
+	return (md5_value, sha_value);
+}
+
+
+# master_secret =
+#	MD5(premaster_secret + SHA('A' + premaster_secret +
+#		ClientHello.random + ServerHello.random)) +
+#	MD5(premaster_secret + SHA('BB' + premaster_secret +
+#		ClientHello.random + ServerHello.random)) +
+#	MD5(premaster_secret + SHA('CCC' + premaster_secret +
+#		ClientHello.random + ServerHello.random));
+
+calc_master_secret(pm, cr, sr: array of byte): array of byte
+{
+	ms := array [48] of byte;
+	sha_value := array [Keyring->SHA1dlen] of byte;
+	leader := array [3] of byte;
+
+	j := 0;
+	lb := pm::cr::sr::nil;
+	for(i := 1; i <= 3; i++) {
+		leader[0] = leader[1] = leader[2] = byte (16r40 + i);
+		(sha_value, nil) = sha_hash(leader[0:i]::lb, nil);
+		(ms[j:], nil) = md5_hash(pm::sha_value::nil, nil); 
+		j += 16; # Keyring->MD5dlen
+	}
+
+	if(SSL_DEBUG)
+		log("ssl3: calc_master_secret:\n\tmaster_secret = \n\t\t" + bastr(ms) + "\n");
+
+	return ms;
+}
+
+
+# key_block =
+# 	MD5(master_secret + SHA(`A' + master_secret + 
+#				ServerHello.random + ClientHello.random)) +
+#       MD5(master_secret + SHA(`BB' + master_secret + 
+#				ServerHello.random + ClientHello.random)) +
+#       MD5(master_secret + SHA(`CCC' + master_secret + 
+#				ServerHello.random + ClientHello.random)) +
+#	[...];
+
+calc_key_material(n: int, ms, cr, sr: array of byte): array of byte
+{
+	key_block := array [n] of byte;
+	sha_value := array [Keyring->SHA1dlen] of byte; # [20]
+	md5_value := array [Keyring->MD5dlen] of byte; # [16]
+	leader := array [10] of byte;
+
+	if(n > 16*(len leader)) {
+		if(SSL_DEBUG)
+			log(sys->sprint("ssl3: calc key block: key size too long [%d]", n));
+		return nil;
+	}
+
+	m := n;
+	i, j, consumed, next : int = 0;
+	lb := ms::sr::cr::nil;
+	for(i = 0; m > 0; i++) {
+		for(j = 0; j <= i; j++)
+			leader[j] = byte (16r41 + i); # 'A', 'BB', 'CCC', etc.
+
+		(sha_value, nil) = sha_hash(leader[0:i+1]::lb, nil);
+		(md5_value, nil) = md5_hash(ms::sha_value::nil, nil); 
+
+		consumed = Keyring->MD5dlen;
+		if(m < Keyring->MD5dlen)
+			consumed = m;
+		m -= consumed;
+
+		key_block[next:] = md5_value[0:consumed];
+		next += consumed;		
+	}
+
+	if(SSL_DEBUG)
+		log("ssl3: calc_key_material:" + "\n\tkey_block = \n\t\t" + bastr(key_block) + "\n");
+
+	return key_block;
+}
+
+# Then the key_block is partitioned as follows.
+#
+#	client_write_MAC_secret[CipherSpec.hash_size]
+#	server_write_MAC_secret[CipherSpec.hash_size]
+#	client_write_key[CipherSpec.key_material]
+#	server_write_key[CipherSpec.key_material]
+#	client_write_IV[CipherSpec.IV_size] /* non-export ciphers */
+#	server_write_IV[CipherSpec.IV_size] /* non-export ciphers */
+#
+# Any extra key_block material is discarded.
+#
+# Exportable encryption algorithms (for which
+# CipherSpec.is_exportable is true) require additional processing as
+# follows to derive their final write keys:
+#
+#	final_client_write_key = MD5(client_write_key +
+#					ClientHello.random +
+#					ServerHello.random);
+#	final_server_write_key = MD5(server_write_key +
+#					ServerHello.random +
+#					ClientHello.random);
+#
+# Exportable encryption algorithms derive their IVs from the random
+# messages:
+#
+#	client_write_IV = MD5(ClientHello.random + ServerHello.random);
+#	server_write_IV = MD5(ServerHello.random + ClientHello.random);
+
+calc_keys(ciph: ref CipherSpec, ms, cr, sr: array of byte) 
+	: (array of byte, array of byte, array of byte, array of byte, array of byte, array of byte)
+{
+	cw_mac, sw_mac, cw_key, sw_key,	cw_IV, sw_IV: array of byte;
+
+	n := ciph.key_material + ciph.hash_size;
+	if(ciph.is_exportable == SSL_EXPORT_FALSE)
+		n += ciph.IV_size;
+	n *= 2;
+
+	key_block := calc_key_material(n, ms, cr, sr);
+
+	i := 0;
+	if(ciph.hash_size != 0) {
+		cw_mac = key_block[i:i+ciph.hash_size]; 
+		i += ciph.hash_size;
+		sw_mac = key_block[i:i+ciph.hash_size]; 
+		i += ciph.hash_size;
+	}
+
+	if(ciph.is_exportable == SSL_EXPORT_FALSE) {
+		if(ciph.key_material != 0) {
+			cw_key = key_block[i:i+ciph.key_material]; 
+			i += ciph.key_material;
+			sw_key = key_block[i:i+ciph.key_material]; 
+			i += ciph.key_material;
+		}
+		if(ciph.IV_size != 0) {
+			cw_IV = key_block[i:i+ciph.IV_size]; 
+			i += ciph.IV_size;
+			sw_IV = key_block[i:i+ciph.IV_size]; 
+			i += ciph.IV_size;
+		}
+	}
+	else {
+		if(ciph.key_material != 0) {
+			cw_key = key_block[i:i+ciph.key_material]; 
+			i += ciph.key_material;
+			sw_key = key_block[i:i+ciph.key_material]; 
+			i += ciph.key_material;
+			(cw_key, nil) = md5_hash(cw_key::cr::sr::nil, nil);
+			(sw_key, nil) = md5_hash(sw_key::sr::cr::nil, nil);
+		}
+		if(ciph.IV_size != 0) {
+			(cw_IV, nil) = md5_hash(cr::sr::nil, nil);
+			(sw_IV, nil) = md5_hash(sr::cr::nil, nil);
+		}
+	}
+
+	if(SSL_DEBUG)
+		log("ssl3: calc_keys:" 
+		+ "\n\tclient_write_mac = \n\t\t" + bastr(cw_mac)
+		+ "\n\tserver_write_mac = \n\t\t" + bastr(sw_mac)
+		+ "\n\tclient_write_key = \n\t\t" + bastr(cw_key)
+		+ "\n\tserver_write_key = \n\t\t" + bastr(sw_key)
+ 		+ "\n\tclient_write_IV  = \n\t\t" + bastr(cw_IV)
+		+ "\n\tserver_write_IV = \n\t\t" + bastr(sw_IV) + "\n");
+
+	return (cw_mac, sw_mac, cw_key, sw_key, cw_IV, sw_IV);
+}
+
+#
+# decode protocol message
+#
+Protocol.decode(r: ref Record, ctx: ref Context): (ref Protocol, string)
+{
+	p : ref Protocol;
+
+	case r.content_type {
+	SSL_ALERT =>
+		if(len r.data != 2)
+			return (nil, "alert decode failed");
+
+		p = ref Protocol.pAlert(ref Alert(int r.data[0], int r.data[1])); 
+
+	SSL_CHANGE_CIPHER_SPEC =>
+		if(len r.data != 1 || r.data[0] != byte 1)
+			return (nil, "ChangeCipherSpec decode failed");
+
+		p = ref Protocol.pChangeCipherSpec(ref ChangeCipherSpec(1));
+
+	SSL_HANDSHAKE =>
+		(hm, e) := Handshake.decode(r.data);
+		if(e != nil)
+			return (nil, e);
+
+		pick h := hm {
+		Finished =>
+			exp_sender := SSL_CLIENT_SENDER;
+			if(ctx.status & CLIENT_SIDE)
+				exp_sender = SSL_SERVER_SENDER;
+
+			(md5_hash, sha_hash) := calc_finished(exp_sender, 
+				ctx.session.master_secret, ctx.sha_state, ctx.md5_state);
+
+			if(SSL_DEBUG)
+				log("ssl3: handshake_decode: finished"
+				+ "\n\texpected_md5_hash = \n\t\t" + bastr(md5_hash)
+				+ "\n\tgot_md5_hash = \n\t\t" + bastr(h.md5_hash)
+				+ "\n\texpected_sha_hash = \n\t\t" + bastr(sha_hash)
+				+ "\n\tgot_sha_hash = \n\t\t" + bastr(h.sha_hash) + "\n");
+
+			#if(string md5_hash != string h.md5_hash || string sha_hash != string h.sha_hash)
+			if(bytes_cmp(md5_hash, h.md5_hash) < 0 || bytes_cmp(sha_hash, h.sha_hash) < 0)
+				return (nil, "finished: sender mismatch");
+
+			e = update_handshake_hash(ctx, r);
+			if(e != nil)
+				return (nil, e);
+
+		CertificateVerify =>
+
+			e = update_handshake_hash(ctx, r);
+			if(e != nil)
+				return (nil, e);
+
+		* =>
+			e = update_handshake_hash(ctx, r);
+			if(e != nil)
+				return (nil, e);
+		}
+
+		p = ref Protocol.pHandshake(hm);
+
+	SSL_V2HANDSHAKE =>
+
+		(hm, e) := V2Handshake.decode(r.data);
+		if(e != "")
+			return (nil, e);
+
+		p = ref Protocol.pV2Handshake(hm);
+
+	* =>
+		return (nil, "protocol read: unknown protocol");
+	}
+
+	return (p, nil);
+
+}
+
+
+# encode protocol message and return tuple of data record and error message,
+# may be v2 or v3 record depending on vers.
+
+Protocol.encode(protocol: self ref Protocol, vers: array of byte): (ref Record, string)
+{
+	r: ref Record;
+	e: string;
+
+	pick p := protocol {
+	pAlert =>
+		r = ref Record(
+				SSL_ALERT,
+				vers,
+				array [] of {byte p.alert.level, byte p.alert.description}
+			);
+
+	pChangeCipherSpec =>
+		r = ref Record(
+				SSL_CHANGE_CIPHER_SPEC,
+				vers,
+				array [] of {byte p.change_cipher_spec.value}
+			);
+
+	pHandshake =>
+		data: array of byte;
+		(data, e) = p.handshake.encode();
+		if(e != "")
+			break;
+		r = ref Record(
+				SSL_HANDSHAKE, 
+				vers,
+				data
+			);
+
+	pV2Handshake =>
+		data: array of byte;
+		(data, e) = p.handshake.encode();
+		if(e != "")
+			break;
+		r = ref Record(
+				SSL_V2HANDSHAKE,
+				vers,
+				data
+			);
+
+	* =>
+		e = "unknown protocol";
+	}
+
+	if(SSL_DEBUG)
+		log("ssl3: protocol encode\n" + protocol.tostring());
+
+	return (r, e);
+}
+
+#
+# protocol message description
+#
+Protocol.tostring(protocol: self ref Protocol): string
+{
+	info : string;
+
+	pick p := protocol {
+	pAlert =>
+		info = "\tAlert\n" + p.alert.tostring();
+
+	pChangeCipherSpec =>
+		info = "\tChangeCipherSpec\n";
+
+	pHandshake =>
+		info = "\tHandshake\n" + p.handshake.tostring();
+
+	pV2Handshake =>
+		info = "\tV2Handshake\n" + p.handshake.tostring();
+
+	pApplicationData =>
+		info = "\tApplicationData\n";
+
+	* =>
+		info = "\tUnknownProtocolType\n";
+	}
+
+	return "ssl3: Protocol:\n" + info;
+}
+
+Handshake.decode(buf: array of byte): (ref Handshake, string)
+{
+	m : ref Handshake;
+	e : string;
+
+	a := buf[4:]; # ignore msg length
+
+	i := 0;
+	case int buf[0] {
+	SSL_HANDSHAKE_HELLO_REQUEST =>
+		m = ref Handshake.HelloRequest();
+
+        SSL_HANDSHAKE_CLIENT_HELLO =>
+    		if(len a < 38) {
+			e = "client hello: unexpected message";
+			break;
+		}
+    		cv := a[i:i+2];
+		i += 2;
+		rd := a[i:i+32];	
+		i += 32;    
+		lsi := int a[i++];
+    		if(len a < 38 + lsi) {
+			e = "client hello: unexpected message";
+			break;
+		}
+		sid: array of byte;
+		if(lsi != 0) {
+			sid = a[i:i+lsi];			
+			i += lsi;
+		}
+		else
+			sid = nil;
+		lcs := int_decode(a[i:i+2]);    	
+		i += 2;
+		if((lcs & 1) || lcs < 2 || len a < 40 + lsi + lcs) {
+			e = "client hello: unexpected message";
+			break;
+		}
+		cs := array [lcs/2] of byte;
+		cs = a[i:i+lcs];
+		i += lcs;
+		lcm := int a[i++];
+		cr := a[i:i+lcm];			
+		i += lcm;
+		# In the interest of forward compatibility, it is
+		# permitted for a client hello message to include
+		# extra data after the compression methods. This
+		# data must be included in the handshake hashes, 
+		# but otherwise be ignored.
+		# if(i != len a) {
+		#	e = "client hello: unexpected message";
+		#	break;
+		# }
+		m = ref Handshake.ClientHello(cv, rd, sid, cs, cr);
+
+        SSL_HANDSHAKE_SERVER_HELLO =>
+		if(len a < 38) {
+			e = "server hello: unexpected message";
+			break;
+		}
+		sv := a[i:i+2];			
+		i += 2;
+		rd := a[i:i+32];			
+		i += 32;
+		lsi := int a[i++];
+		if(len a < 38 + lsi) {
+			e = "server hello: unexpected message";
+			break;
+		}
+		sid : array of byte;
+		if(lsi != 0) {
+			sid = a[i:i+lsi];			
+			i += lsi;
+		}
+		else
+			sid = nil;
+		cs := a[i:i+2];			
+		i += 2;
+		cr := a[i++];
+		if(i != len a) {
+			e = "server hello: unexpected message";
+			break;
+		}
+		m = ref Handshake.ServerHello(sv, rd, sid, cs, cr);
+
+        SSL_HANDSHAKE_CERTIFICATE =>
+		n := int_decode(a[i:i+3]);		
+		i += 3;
+		if(len a != n + 3) {
+			e = "certificate: unexpected message";
+			break;
+		}
+		cl : list of array of byte;
+		k : int;
+		while(i < n) {
+			k = int_decode(a[i:i+3]);
+			i += 3;	
+			if(k < 0 || i + k > len a) {
+				e = "certificate: unexpected message";
+				break;
+			}
+			cl = a[i:i+k] :: cl;		
+			i += k;
+		}
+		if(e != nil)
+			break;
+		m = ref Handshake.Certificate(cl);
+
+        SSL_HANDSHAKE_SERVER_KEY_EXCHANGE =>
+
+		m = ref Handshake.ServerKeyExchange(a[i:]);
+		
+        SSL_HANDSHAKE_CERTIFICATE_REQUEST =>
+		ln := int_decode(a[i:i+2]);		
+		i += 2;
+		types := a[i:i+ln];			
+		i += ln;
+		ln = int_decode(a[i:i+2]);		
+		i += 2;
+		auths : list of array of byte;
+		for(j := 0; j < ln; j++) {
+			ln = int_decode(a[i:i+2]);	
+			i += 2;
+			auths = a[i:i+ln]::auths;	
+			i += ln;
+		}
+		m = ref Handshake.CertificateRequest(types, auths);
+
+        SSL_HANDSHAKE_SERVER_HELLO_DONE =>
+		if(len a != 0) {
+			e = "server hello done: unexpected message";
+			break;
+		}
+		m = ref Handshake.ServerHelloDone();
+	
+        SSL_HANDSHAKE_CERTIFICATE_VERIFY =>
+		ln := int_decode(a[i:i+2]);		
+		i +=2;
+		sig := a[i:];				
+		i += ln;
+		if(i != len a) {
+			e = "certificate verify: unexpected message";
+			break;
+		}
+		m = ref Handshake.CertificateVerify(sig);
+
+        SSL_HANDSHAKE_CLIENT_KEY_EXCHANGE =>
+		m = ref Handshake.ClientKeyExchange(a);
+
+        SSL_HANDSHAKE_FINISHED =>
+		if(len a != Keyring->MD5dlen + Keyring->SHA1dlen) { # 16+20
+			e = "finished: unexpected message";
+			break;
+		}
+		md5_hash := a[i:i+Keyring->MD5dlen];	
+		i += Keyring->MD5dlen;
+		sha_hash := a[i:i+Keyring->SHA1dlen];	
+		i += Keyring->SHA1dlen;
+		if(i != len a) {
+			e = "finished: unexpected message";
+			break;
+		}
+		m = ref Handshake.Finished(md5_hash, sha_hash);
+
+	* =>
+		e = "unknown message";
+	}
+
+	if(e != nil)
+		return (nil, "Handshake decode: " + e);
+
+	return (m, nil);
+}
+
+Handshake.encode(hm: self ref Handshake): (array of byte, string)
+{
+	a : array of byte;
+	n : int;
+	e : string;
+
+	i := 0;
+	pick m := hm {
+	HelloRequest =>
+		a = array [4] of byte;
+		a[i++] = byte SSL_HANDSHAKE_HELLO_REQUEST;
+		a[i:] = int_encode(n, 3);
+		i += 3;
+		if(i != 4)
+			e = "hello request: wrong message length";
+
+        ClientHello =>
+		lsi := len m.session_id;
+		lcs := len m.suites;
+		if((lcs &1) || lcs < 2) {
+			e = "client hello: cipher suites is not multiple of 2 bytes";
+			break;
+		}
+		lcm := len m.compressions;
+		n = 38 + lsi + lcs + lcm; # 2+32+1+2+1
+		a = array[n+4] of byte;
+		a[i++] = byte SSL_HANDSHAKE_CLIENT_HELLO;
+		a[i:] = int_encode(n, 3);
+		i += 3;
+		a[i:] = m.version;
+		i += 2;
+		a[i:] = m.random;
+		i += 32;
+		a[i++] = byte lsi;
+		if(lsi != 0) {
+			a[i:] = m.session_id;	
+			i += lsi;
+		}
+		a[i:] = int_encode(lcs, 2);		
+		i += 2;
+		a[i:] = m.suites; # not nil
+		i += lcs;
+		a[i++] = byte lcm;
+		a[i:] = m.compressions;	# not nil	
+		i += lcm;
+		if(i != n+4)
+			e = "client hello: wrong message length";
+
+        ServerHello =>
+		lsi := len m.session_id;
+		n = 38 + lsi; # 2+32+1+2+1
+		a = array [n+4] of byte;
+		a[i++] = byte SSL_HANDSHAKE_SERVER_HELLO;
+		a[i:] = int_encode(n, 3);		
+		i += 3;
+		a[i:] = m.version;		
+		i += 2;
+		a[i:] = m.random;			
+		i += 32;
+		a[i++] = byte lsi;
+		if(lsi != 0) {
+			a[i:] = m.session_id;			
+			i += lsi;
+		}
+		a[i:] = m.suite; # should be verified, not nil
+		i += 2;
+		a[i++] = m.compression; # should be verified, not nil
+		if(i != n+4)
+			e = "server hello: wrong message length";
+
+        Certificate =>
+		cl := m.cert_list;
+		while(cl != nil) {
+			n += 3 + len hd cl;
+			cl = tl cl;
+		}
+		a = array [n+7] of byte; 
+		a[i++] = byte SSL_HANDSHAKE_CERTIFICATE;
+		a[i:] = int_encode(n+3, 3); # length of record		
+		i += 3;
+		a[i:] = int_encode(n, 3); # total length of cert chain
+		i += 3;
+		cl = m.cert_list;
+		while(cl != nil) {
+			a[i:] = int_encode(len hd cl, 3); 
+			i += 3;
+			a[i:] = hd cl;			
+			i += len hd cl;
+			cl = tl cl;
+		} 
+		if(i != n+7)
+			e = "certificate: wrong message length";
+
+        ServerKeyExchange =>
+		n = len m.xkey;
+		a = array [n+4] of byte;
+		a[i++] = byte SSL_HANDSHAKE_SERVER_KEY_EXCHANGE;
+		a[i:] = int_encode(n, 3);		
+		i += 3;
+		a[i:] = m.xkey;		
+		i += len m.xkey;
+		if(i != n+4)
+			e = "server key exchange: wrong message length";
+		
+        CertificateRequest =>
+		ntypes := len m.cert_types;
+		nauths := len m.dn_list;
+		n = 1 + ntypes;
+		dl := m.dn_list;
+		while(dl != nil) {
+			n += 2 + len hd dl;			
+			dl = tl dl;
+		}
+		n += 2;	
+		a = array [n+4] of byte;
+		a[i++] =  byte SSL_HANDSHAKE_CERTIFICATE_REQUEST;
+		a[i:] = int_encode(n, 3);		
+		i += 3;
+		a[i++] = byte ntypes;
+		a[i:] = m.cert_types;		
+		i += ntypes;
+		a[i:] = int_encode(nauths, 2);		
+		i += 2;
+		dl = m.dn_list;
+		while(dl != nil) {
+			a[i:] = int_encode(len hd dl, 2); 
+			i += 2;
+			a[i:] = hd dl;			
+			i += len hd dl;
+			dl = tl dl;
+		}
+		if(i != n+4)
+			e = "certificate request: wrong message length";
+		
+        ServerHelloDone =>
+		n = 0;
+		a = array[n+4] of byte;
+		a[i++] = byte SSL_HANDSHAKE_SERVER_HELLO_DONE;
+		a[i:] = int_encode(0, 3); # message has 0 length
+		i += 3;
+		if(i != n+4)
+			e = "server hello done: wrong message length";
+
+        CertificateVerify =>
+		n = 2 + len m.signature;
+		a = array [n+4] of byte;
+		a[i++] = byte SSL_HANDSHAKE_CERTIFICATE_VERIFY;
+		a[i:] = int_encode(n, 3);		
+		i += 3;
+		a[i:] = int_encode(n-2, 2);		
+		i += 2;
+		a[i:] = m.signature;			
+		i += n-2;
+		if(i != n+4)
+			e = "certificate verify: wrong message length";
+
+        ClientKeyExchange =>
+		n = len m.xkey;
+		a = array [n+4] of byte;
+		a[i++] = byte SSL_HANDSHAKE_CLIENT_KEY_EXCHANGE;
+		a[i:] = int_encode(n, 3);		
+		i += 3;
+		a[i:] = m.xkey;		
+		i += n;
+		if(i != n+4)
+			e = "client key exchange: wrong message length";
+
+        Finished =>
+		n = len m.md5_hash + len m.sha_hash;
+		a = array [n+4] of byte;
+		a[i++] = byte SSL_HANDSHAKE_FINISHED;    
+		a[i:] = int_encode(n, 3);
+		i += 3;
+		a[i:] = m.md5_hash;			
+		i += len m.md5_hash;
+		a[i:] = m.sha_hash;			
+		i += len m.sha_hash;
+		if(i != n+4)
+			e = "finished: wrong message length";
+
+	* =>
+		e = "unknown message";
+	}
+	
+	if(e != nil)
+		return (nil, "Handshake encode: " + e);
+
+	return (a, e);
+}
+
+Handshake.tostring(handshake: self ref Handshake): string
+{
+	info: string;
+
+	pick m := handshake {
+        HelloRequest =>
+		info = "\tHelloRequest\n"; 
+
+        ClientHello =>
+		info = "\tClientHello\n" + 
+			"\tversion = \n\t\t" + bastr(m.version) + "\n" +
+			"\trandom = \n\t\t" + bastr(m.random) + "\n" +
+			"\tsession_id = \n\t\t" + bastr(m.session_id) + "\n" +
+			"\tsuites = \n\t\t" + bastr(m.suites) + "\n" +
+			"\tcomperssion_methods = \n\t\t" + bastr(m.compressions) +"\n";
+
+        ServerHello =>
+		info = "\tServerHello\n" + 
+			"\tversion = \n\t\t" + bastr(m.version) + "\n" +
+			"\trandom = \n\t\t" + bastr(m.random) + "\n" +
+			"\tsession_id = \n\t\t" + bastr(m.session_id) + "\n" +
+			"\tsuite = \n\t\t" + bastr(m.suite) + "\n" +
+			"\tcomperssion_method = \n\t\t" + string m.compression +"\n";
+
+        Certificate =>
+		info = "\tCertificate\n" + 
+			"\tcert_list = \n\t\t" + lbastr(m.cert_list) + "\n";
+
+        ServerKeyExchange =>
+		info = "\tServerKeyExchange\n" +
+			"\txkey = \n\t\t" + bastr(m.xkey) +"\n";
+
+        CertificateRequest =>
+		info = "\tCertificateRequest\n" +
+			"\tcert_types = \n\t\t" + bastr(m.cert_types) + "\n" +
+			"\tdn_list = \n\t\t" + lbastr(m.dn_list) + "\n";
+
+        ServerHelloDone =>
+		info = "\tServerDone\n";
+
+        CertificateVerify =>
+		info = "\tCertificateVerify\n" +
+			"\tsignature = \n\t\t" + bastr(m.signature) + "\n"; 
+
+        ClientKeyExchange =>
+		info = "\tClientKeyExchange\n" +
+			"\txkey = \n\t\t" + bastr(m.xkey) +"\n";
+
+        Finished =>
+		info = "\tFinished\n" +
+			"\tmd5_hash = \n\t\t" + bastr(m.md5_hash) + "\n" +
+			"\tsha_hash = \n\t\t" + bastr(m.sha_hash) + "\n";
+	}
+
+	return info;
+}
+
+Alert.tostring(alert: self ref Alert): string
+{
+	info: string;
+
+	case alert.level {
+	SSL_WARNING =>				
+		info += "\t\twarning: ";
+
+	SSL_FATAL =>				
+		info += "\t\tfatal: ";
+
+	*  =>
+		info += sys->sprint("unknown alert level[%d]: ", alert.level);
+	}
+
+	case alert.description {
+	SSL_CLOSE_NOTIFY => 			
+		info += "close notify";
+
+	SSL_NO_CERTIFICATE => 			
+		info += "no certificate";
+
+	SSL_BAD_CERTIFICATE => 			
+		info += "bad certificate";
+
+	SSL_UNSUPPORTED_CERTIFICATE => 		
+		info += "unsupported certificate";
+
+	SSL_CERTIFICATE_REVOKED => 		
+		info += "certificate revoked";
+
+	SSL_CERTIFICATE_EXPIRED =>		
+		info += "certificate expired";
+
+	SSL_CERTIFICATE_UNKNOWN =>		
+		info += "certificate unknown";
+
+	SSL_UNEXPECTED_MESSAGE =>		
+		info += "unexpected message";
+
+	SSL_BAD_RECORD_MAC =>	 		
+		info += "bad record mac";
+
+	SSL_DECOMPRESSION_FAILURE => 		
+		info += "decompression failure";
+
+	SSL_HANDSHAKE_FAILURE => 		
+		info += "handshake failure";
+
+	SSL_ILLEGAL_PARAMETER => 		
+		info += "illegal parameter";
+
+	* =>
+		info += sys->sprint("unknown alert description[%d]", alert.description);
+	}
+
+	return info;
+}
+
+find_cipher_suite(s, suites: array of byte) : array of byte
+{
+	i, j : int;
+	a, b : array of byte;
+
+	n := len s;
+	if((n & 1) || n < 2)
+		return nil;
+
+	m := len suites;
+	if((m & 1) || m < 2)
+		return nil;
+
+	for(i = 0; i < n; ) {
+		a = s[i:i+2];
+		i += 2;
+		for(j = 0; j < m; ) {
+			b = suites[j:j+2];
+			j += 2;
+			if(a[0] == b[0] && a[1] == b[1]) 
+				return b;
+		}
+	}
+
+	return nil;
+}
+
+#
+# cipher suites and specs
+#
+suite_to_spec(cs: array of byte, cipher_suites: array of array of byte) 
+	: (ref CipherSpec, ref KeyExAlg, ref SigAlg, string)
+{
+	cip : ref CipherSpec;
+	kex : ref KeyExAlg;
+	sig : ref SigAlg;
+
+	n := len cipher_suites;
+	i : int;
+	found := array [2] of byte;
+	for(i = 0; i < n; i++) {
+		found = cipher_suites[i];
+		if(found[0]==cs[0] && found[1]==cs[1]) break;
+	}
+
+	if(i == n)
+		return (nil, nil, nil, "fail to find a matched spec");
+
+	case i {
+	NULL_WITH_NULL_NULL =>
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_NULL_CIPHER, 
+			SSL_STREAM_CIPHER, 0, 0, SSL_NULL_MAC, 0);
+		kex = ref KeyExAlg.NULL();
+		sig = ref SigAlg.anon();
+
+	RSA_WITH_NULL_MD5 => # sign only certificate
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_NULL_CIPHER, 
+			SSL_STREAM_CIPHER, 0, 0, SSL_MD5, Keyring->MD5dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	RSA_WITH_NULL_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_NULL_CIPHER, 
+			SSL_STREAM_CIPHER, 0, 0, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	RSA_EXPORT_WITH_RC4_40_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_RC4, 
+			SSL_STREAM_CIPHER, 5, 0, SSL_MD5, Keyring->MD5dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	RSA_WITH_RC4_128_MD5 => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_RC4, 
+			SSL_STREAM_CIPHER, 16, 0, SSL_MD5, Keyring->MD5dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	RSA_WITH_RC4_128_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_RC4, 
+			SSL_STREAM_CIPHER, 16, 0, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	RSA_EXPORT_WITH_RC2_CBC_40_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_RC2_CBC, 
+			SSL_BLOCK_CIPHER, 5, 8, SSL_MD5, Keyring->MD5dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.RSA(nil, nil);
+
+	RSA_WITH_IDEA_CBC_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_IDEA_CBC,
+			SSL_BLOCK_CIPHER, 16, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	RSA_EXPORT_WITH_DES40_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 5, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.RSA(nil, nil);
+
+	RSA_WITH_DES_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 8, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	RSA_WITH_3DES_EDE_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_3DES_EDE_CBC, 
+			SSL_BLOCK_CIPHER, 24, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.RSA(nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	DH_DSS_EXPORT_WITH_DES40_CBC_SHA =>
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 5, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.DSS(nil, nil);
+
+	DH_DSS_WITH_DES_CBC_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 8, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.DSS(nil, nil);
+
+	DH_DSS_WITH_3DES_EDE_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_3DES_EDE_CBC, 
+			SSL_BLOCK_CIPHER, 24, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.DSS(nil, nil);
+
+	DH_RSA_EXPORT_WITH_DES40_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 5, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.RSA(nil, nil);
+
+	DH_RSA_WITH_DES_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_DES_CBC, 
+			SSL_STREAM_CIPHER, 8, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.RSA(nil, nil);
+
+	DH_RSA_WITH_3DES_EDE_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_3DES_EDE_CBC, 
+			SSL_BLOCK_CIPHER, 24, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.RSA(nil, nil);
+
+	DHE_DSS_EXPORT_WITH_DES40_CBC_SHA =>
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 5, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.DSS(nil, nil);
+
+	DHE_DSS_WITH_DES_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 8, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.DSS(nil, nil);
+
+	DHE_DSS_WITH_3DES_EDE_CBC_SHA => 
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_3DES_EDE_CBC, 
+			SSL_BLOCK_CIPHER, 24, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.DSS(nil, nil);
+
+	DHE_RSA_EXPORT_WITH_DES40_CBC_SHA =>
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 5, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.RSA(nil, nil);
+
+	DHE_RSA_WITH_DES_CBC_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 8, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.RSA(nil, nil);
+
+	DHE_RSA_WITH_3DES_EDE_CBC_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_3DES_EDE_CBC, 
+			SSL_BLOCK_CIPHER, 24, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.RSA(nil, nil);
+
+	DH_anon_EXPORT_WITH_RC4_40_MD5 => 
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_RC4, 
+			SSL_STREAM_CIPHER, 5, 0, SSL_MD5, Keyring->MD5dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	DH_anon_WITH_RC4_128_MD5 => 
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_RC4, 
+			SSL_STREAM_CIPHER, 16, 0, SSL_MD5, Keyring->MD5dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	DH_anon_EXPORT_WITH_DES40_CBC_SHA =>
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 5, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	DH_anon_WITH_DES_CBC_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_DES_CBC, 
+			SSL_BLOCK_CIPHER, 8, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	DH_anon_WITH_3DES_EDE_CBC_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_3DES_EDE_CBC, 
+			SSL_BLOCK_CIPHER, 24, 8, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.DH(nil, nil, nil, nil, nil);
+		sig = ref SigAlg.anon();
+
+	FORTEZZA_KEA_WITH_NULL_SHA => 	
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_NULL_CIPHER, 
+			SSL_STREAM_CIPHER, 0, 0, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.FORTEZZA_KEA();
+		sig = ref SigAlg.FORTEZZA_KEA();
+
+	FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA =>
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_FORTEZZA_CBC, 
+			SSL_BLOCK_CIPHER, 0, 0, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.FORTEZZA_KEA();
+		sig = ref SigAlg.FORTEZZA_KEA();
+
+	FORTEZZA_KEA_WITH_RC4_128_SHA =>
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_RC4, 
+			SSL_STREAM_CIPHER, 16, 0, SSL_SHA, Keyring->SHA1dlen);
+		kex = ref KeyExAlg.FORTEZZA_KEA();
+		sig = ref SigAlg.FORTEZZA_KEA();
+
+	}
+
+	return (cip, kex, sig, nil);
+}
+
+#
+# use suites as default SSL3_Suites
+#
+cipher_suite_info(cs: array of byte, suites: array of array of byte) : string
+{
+	tag : string;
+
+	a := array [2] of byte;
+	n := len suites;
+	for(i := 0; i < n; i++) {
+		a = suites[i];
+		if(a[0]==cs[0] && a[1]==cs[1]) break;
+	}
+
+	if(i == n)
+		return "unknown cipher suite [" + string cs + "]";
+
+	case i {
+	NULL_WITH_NULL_NULL => 		
+		tag = "NULL_WITH_NULL_NULL";
+
+	RSA_WITH_NULL_MD5 => 		
+		tag = "RSA_WITH_NULL_MD5";
+
+	RSA_WITH_NULL_SHA => 		
+		tag = "RSA_WITH_NULL_SHA";
+
+	RSA_EXPORT_WITH_RC4_40_MD5 => 	
+		tag = "RSA_EXPORT_WITH_RC4_40_MD5";
+
+	RSA_WITH_RC4_128_MD5 => 		
+		tag = "RSA_WITH_RC4_128_MD5"; 	
+
+	RSA_WITH_RC4_128_SHA => 		
+		tag = "RSA_WITH_RC4_128_SHA";
+
+	RSA_EXPORT_WITH_RC2_CBC_40_MD5 => 	
+		tag = "RSA_EXPORT_WITH_RC2_CBC_40_MD5";
+
+	RSA_WITH_IDEA_CBC_SHA => 		
+		tag = "RSA_WITH_IDEA_CBC_SHA";
+
+	RSA_EXPORT_WITH_DES40_CBC_SHA => 	
+		tag ="RSA_EXPORT_WITH_DES40_CBC_SHA";
+
+	RSA_WITH_DES_CBC_SHA =>		
+		tag = "RSA_WITH_DES_CBC_SHA";
+
+	RSA_WITH_3DES_EDE_CBC_SHA => 	
+		tag = "RSA_WITH_3DES_EDE_CBC_SHA";
+
+	DH_DSS_EXPORT_WITH_DES40_CBC_SHA => 
+		tag = "DH_DSS_EXPORT_WITH_DES40_CBC_SHA";
+
+	DH_DSS_WITH_DES_CBC_SHA => 		
+		tag = "DH_DSS_WITH_DES_CBC_SHA";
+
+	DH_DSS_WITH_3DES_EDE_CBC_SHA => 	
+		tag = "DH_DSS_WITH_3DES_EDE_CBC_SHA";
+
+	DH_RSA_EXPORT_WITH_DES40_CBC_SHA => 
+		tag = "DH_RSA_EXPORT_WITH_DES40_CBC_SHA";
+
+	DH_RSA_WITH_DES_CBC_SHA => 		
+		tag = "DH_RSA_WITH_DES_CBC_SHA";
+
+	DH_RSA_WITH_3DES_EDE_CBC_SHA => 	
+		tag = "DH_RSA_WITH_3DES_EDE_CBC_SHA";
+
+	DHE_DSS_EXPORT_WITH_DES40_CBC_SHA => 
+		tag = "DHE_DSS_EXPORT_WITH_DES40_CBC_SHA";
+
+	DHE_DSS_WITH_DES_CBC_SHA => 	
+		tag = "DHE_DSS_WITH_DES_CBC_SHA";
+
+	DHE_DSS_WITH_3DES_EDE_CBC_SHA => 	
+		tag = "DHE_DSS_WITH_3DES_EDE_CBC_SHA";
+
+	DHE_RSA_EXPORT_WITH_DES40_CBC_SHA => 
+		tag = "DHE_RSA_EXPORT_WITH_DES40_CBC_SHA";
+
+	DHE_RSA_WITH_DES_CBC_SHA => 
+		tag = "DHE_RSA_WITH_DES_CBC_SHA";
+
+	DHE_RSA_WITH_3DES_EDE_CBC_SHA => 
+		tag = "DHE_RSA_WITH_3DES_EDE_CBC_SHA";
+
+	DH_anon_EXPORT_WITH_RC4_40_MD5 => 
+		tag = "DH_anon_EXPORT_WITH_RC4_40_MD5";
+
+	DH_anon_WITH_RC4_128_MD5 => 
+		tag = "DH_anon_WITH_RC4_128_MD5";
+
+	DH_anon_EXPORT_WITH_DES40_CBC_SHA => 
+		tag = "DH_anon_EXPORT_WITH_DES40_CBC_SHA";
+
+	DH_anon_WITH_DES_CBC_SHA => 
+		tag = "DH_anon_WITH_DES_CBC_SHA";
+
+	DH_anon_WITH_3DES_EDE_CBC_SHA => 
+		tag = "DH_anon_WITH_3DES_EDE_CBC_SHA";
+
+	FORTEZZA_KEA_WITH_NULL_SHA => 
+		tag = "FORTEZZA_KEA_WITH_NULL_SHA";
+
+	FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA => 
+		tag = "FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA";
+
+	FORTEZZA_KEA_WITH_RC4_128_SHA => 
+		tag = "FORTEZZA_KEA_WITH_RC4_128_SHA";
+	}
+
+	return "cipher suite = [" + tag + "]";
+}
+
+#################################
+## FOR SSLv2 BACKWARD COMPATIBLE
+#################################
+
+# Protocol Version Codes
+SSL2_CLIENT_VERSION				:= array [] of {byte 0, byte 16r02};
+SSL2_SERVER_VERSION				:= array [] of {byte 0, byte 16r02};
+
+# Protocol Message Codes
+
+SSL2_MT_ERROR,
+	SSL2_MT_CLIENT_HELLO,
+	SSL2_MT_CLIENT_MASTER_KEY,
+	SSL2_MT_CLIENT_FINISHED,
+	SSL2_MT_SERVER_HELLO,
+	SSL2_MT_SERVER_VERIFY,
+	SSL2_MT_SERVER_FINISHED,
+	SSL2_MT_REQUEST_CERTIFICATE,
+	SSL2_MT_CLIENT_CERTIFICATE		: con iota;
+
+# Error Message Codes
+
+SSL2_PE_NO_CIPHER				:= array [] of {byte 0, byte 16r01};
+SSL2_PE_NO_CERTIFICATE				:= array [] of {byte 0, byte 16r02};
+SSL2_PE_BAD_CERTIFICATE				:= array [] of {byte 0, byte 16r04};
+SSL2_PE_UNSUPPORTED_CERTIFICATE_TYPE		:= array [] of {byte 0, byte 16r06};
+
+# Cipher Kind Values
+
+SSL2_CK_RC4_128_WITH_MD5,
+	SSL2_CK_RC4_128_EXPORT40_WITH_MD5,
+	SSL2_CK_RC2_CBC_128_CBC_WITH_MD5,
+	SSL2_CK_RC2_CBC_128_CBC_EXPORT40_WITH_MD5,
+	SSL2_CK_IDEA_128_CBC_WITH_MD5,
+	SSL2_CK_DES_64_CBC_WITH_MD5,
+	SSL2_CK_DES_192_EDE3_CBC_WITH_MD5 	: con iota;
+
+SSL2_Cipher_Kinds := array [] of {
+	SSL2_CK_RC4_128_WITH_MD5 => 		array [] of {byte 16r01, byte 0, byte 16r80},
+	SSL2_CK_RC4_128_EXPORT40_WITH_MD5 => 	array [] of {byte 16r02, byte 0, byte 16r80},
+	SSL2_CK_RC2_CBC_128_CBC_WITH_MD5 => 	array [] of {byte 16r03, byte 0, byte 16r80},
+	SSL2_CK_RC2_CBC_128_CBC_EXPORT40_WITH_MD5 =>
+						array [] of {byte 16r04, byte 0, byte 16r80},
+	SSL2_CK_IDEA_128_CBC_WITH_MD5 => 	array [] of {byte 16r05, byte 0, byte 16r80},
+	SSL2_CK_DES_64_CBC_WITH_MD5 => 		array [] of {byte 16r06, byte 0, byte 16r40},
+	SSL2_CK_DES_192_EDE3_CBC_WITH_MD5 => 	array [] of {byte 16r07, byte 0, byte 16rC0},
+};
+
+# Certificate Type Codes
+
+SSL2_CT_X509_CERTIFICATE			: con 16r01; # encode as one byte
+
+# Authentication Type Codes
+
+SSL2_AT_MD5_WITH_RSA_ENCRYPTION			: con byte 16r01;
+
+# Upper/Lower Bounds
+
+SSL2_MAX_MASTER_KEY_LENGTH_IN_BITS		: con 256;
+SSL2_MAX_SESSION_ID_LENGTH_IN_BYTES		: con 16;
+SSL2_MIN_RSA_MODULUS_LENGTH_IN_BYTES		: con 64;
+SSL2_MAX_RECORD_LENGTH_2_BYTE_HEADER		: con 32767;
+SSL2_MAX_RECORD_LENGTH_3_BYTE_HEADER		: con 16383;
+
+# Handshake Internal State
+
+SSL2_STATE_CLIENT_HELLO,
+	SSL2_STATE_SERVER_HELLO,
+	SSL2_STATE_CLIENT_MASTER_KEY,	
+	SSL2_STATE_SERVER_VERIFY,
+	SSL2_STATE_REQUEST_CERTIFICATE,
+	SSL2_STATE_CLIENT_CERTIFICATE,
+	SSL2_STATE_CLIENT_FINISHED,
+	SSL2_STATE_SERVER_FINISHED,		
+	SSL2_STATE_ERROR			: con iota;
+
+# The client's challenge to the server for the server to identify itself is a 
+# (near) arbitary length random. The v3 server will right justify the challenge 
+# data to become the ClientHello.random data (padding with leading zeros, if 
+# necessary). If the length of the challenge is greater than 32 bytes, then only
+# the last 32 bytes are used. It is legitimate (but not necessary) for a v3 
+# server to reject a v2 ClientHello that has fewer than 16 bytes of challenge 
+# data.
+
+SSL2_CHALLENGE_LENGTH				: con 16;
+
+V2Handshake: adt {
+	pick {
+	Error =>
+		code				: array of byte; # [2];
+	ClientHello =>
+		version				: array of byte; # [2]
+		cipher_specs			: array of byte; # [3] x
+		session_id			: array of byte;
+		challenge			: array of byte;
+	ServerHello =>
+		session_id_hit			: int;
+		certificate_type		: int;
+		version				: array of byte; # [2]
+		certificate			: array of byte; # only user certificate
+		cipher_specs			: array of byte; # [3] x
+		connection_id			: array of byte;
+	ClientMasterKey =>
+		cipher_kind			: array of byte; # [3]
+		clear_key			: array of byte;
+		encrypt_key			: array of byte;
+		key_arg				: array of byte;
+	ServerVerify =>
+		challenge			: array of byte;
+	RequestCertificate =>
+		authentication_type		: int;
+		certificate_challenge		: array of byte;
+	ClientCertificate =>
+		certificate_type		: int;
+		certificate			: array of byte; # only user certificate
+		response			: array of byte;
+	ClientFinished =>
+		connection_id			: array of byte;
+	ServerFinished =>
+		session_id			: array of byte;
+	}  
+
+	encode: fn(hm: self ref V2Handshake): (array of byte, string);
+	decode: fn(a: array of byte): (ref V2Handshake, string);
+	tostring: fn(h: self ref V2Handshake): string;
+};
+
+
+V2Handshake.tostring(handshake: self ref V2Handshake): string
+{
+	info := "";
+
+	pick m := handshake {
+        ClientHello =>
+		info += "\tClientHello\n" + 
+			"\tversion = \n\t\t" + bastr(m.version) + "\n" +
+			"\tcipher_specs = \n\t\t" + bastr(m.cipher_specs) + "\n" +
+			"\tsession_id = \n\t\t" + bastr(m.session_id) + "\n" +
+			"\tchallenge = \n\t\t" + bastr(m.challenge) + "\n";
+
+        ServerHello =>
+		info += "\tServerHello\n" + 
+			"\tsession_id_hit = \n\t\t" + string m.session_id_hit + "\n" +
+			"\tcertificate_type = \n\t\t" + string m.certificate_type + "\n" +
+			"\tversion = \n\t\t" + bastr(m.version) + "\n" +
+			"\tcertificate = \n\t\t" + bastr(m.certificate) + "\n" +
+			"\tcipher_specs = \n\t\t" + bastr(m.cipher_specs) + "\n" +
+			"\tconnection_id = \n\t\t" + bastr(m.connection_id) + "\n";
+
+	ClientMasterKey =>
+		info += "\tClientMasterKey\n" +
+			"\tcipher_kind = \n\t\t" + bastr(m.cipher_kind) + "\n" +
+			"\tclear_key = \n\t\t" + bastr(m.clear_key) + "\n" +
+			"\tencrypt_key = \n\t\t" + bastr(m.encrypt_key) + "\n" +
+			"\tkey_arg = \n\t\t" + bastr(m.key_arg) + "\n";
+
+	ServerVerify =>
+		info += "\tServerVerify\n" + 
+			"\tchallenge = \n\t\t" + bastr(m.challenge) + "\n";
+
+	RequestCertificate =>
+		info += "\tRequestCertificate\n" +
+			"\tauthentication_type = \n\t\t" + string m.authentication_type + "\n" +
+			"\tcertificate_challenge = \n\t\t" + bastr(m.certificate_challenge) + "\n";
+
+	ClientCertificate =>
+		info += "ClientCertificate\n" +
+			"\tcertificate_type = \n\t\t" + string m.certificate_type + "\n" +
+			"\tcertificate = \n\t\t" + bastr(m.certificate) + "\n" +
+			"\tresponse = \n\t\t" + bastr(m.response) + "\n";
+
+	ClientFinished =>
+		info += "\tClientFinished\n" +
+			"\tconnection_id = \n\t\t" + bastr(m.connection_id) + "\n";
+
+	ServerFinished =>
+		info += "\tServerFinished\n" +
+			"\tsession_id = \n\t\t" + bastr(m.session_id) + "\n";
+	}
+
+	return info;
+}
+
+
+# v2 handshake protocol - message driven, v2 and v3 sharing the same context stack
+
+do_v2handshake(v2hs: ref V2Handshake, ctx: ref Context): string
+{
+	e: string = nil;
+
+	pick h := v2hs {
+	Error =>
+		do_v2error(h, ctx);
+
+	ClientHello =>
+		if((ctx.status & CLIENT_SIDE) || ctx.state != SSL2_STATE_CLIENT_HELLO) {
+			e = "V2ClientHello";
+			break;
+		}
+		do_v2client_hello(h, ctx);
+
+	ServerHello =>
+		if(!(ctx.status & CLIENT_SIDE) || ctx.state != SSL2_STATE_SERVER_HELLO) {
+			e = "V2ServerHello";
+			break;
+		}
+		do_v2server_hello(h, ctx);
+
+	ClientMasterKey =>
+		if((ctx.status & CLIENT_SIDE) || ctx.state != SSL2_STATE_CLIENT_MASTER_KEY) {
+			e = "V2ClientMasterKey";
+			break;
+		}
+		do_v2client_master_key(h, ctx);
+
+	ServerVerify =>
+		if(!(ctx.status & CLIENT_SIDE) || ctx.state != SSL2_STATE_SERVER_VERIFY) {
+			e = "V2ServerVerify";
+			break;
+		}
+		do_v2server_verify(h, ctx);
+		
+	RequestCertificate =>
+		if(!(ctx.status & CLIENT_SIDE) || ctx.state != SSL2_STATE_SERVER_VERIFY) {
+			e = "V2RequestCertificate";
+			break;
+		}
+		do_v2req_cert(h, ctx);
+
+	ClientCertificate =>
+		if((ctx.status & CLIENT_SIDE) || ctx.state != SSL2_STATE_CLIENT_CERTIFICATE) {
+			e = "V2ClientCertificate";
+			break;
+		}
+		do_v2client_certificate(h, ctx);
+
+	ClientFinished =>
+		if((ctx.status & CLIENT_SIDE) || ctx.state != SSL2_STATE_CLIENT_FINISHED) {
+			e = "V2ClientFinished";
+			break;
+		}
+		do_v2client_finished(h, ctx);
+
+	ServerFinished =>
+		if(!(ctx.status & CLIENT_SIDE) || ctx.state != SSL2_STATE_SERVER_FINISHED) {
+			e = "V2ServerFinished";
+			break;
+		}
+		do_v2server_finished(h, ctx);
+	}
+
+	return e;
+}
+
+do_v2error(v2hs: ref V2Handshake.Error, ctx: ref Context)
+{
+	if(SSL_DEBUG)
+		log("do_v2error: " + string v2hs.code);
+	ctx.state = STATE_EXIT;
+}
+
+# [server side]
+do_v2client_hello(v2hs: ref V2Handshake.ClientHello, ctx: ref Context)
+{
+	if(v2hs.version[0] != SSL2_CLIENT_VERSION[0] || v2hs.version[1] != SSL2_CLIENT_VERSION[1]) {
+		# promote this message to v3 handshake protocol
+		ctx.state = STATE_CLIENT_HELLO;
+		return;
+	}
+	
+	# trying to resume
+	s: ref Session;
+	if((v2hs.session_id != nil) && (ctx.status & SESSION_RESUMABLE))
+		s = sslsession->get_session_byid(v2hs.session_id);
+	if(s != nil) { # found a hit
+		# prepare and send v2 handshake hello message
+		v2handshake_enque(
+			ref V2Handshake.ServerHello(
+				1, # hit found
+				0, # no certificate required
+				SSL2_SERVER_VERSION, 
+				nil, # no authetication required
+				s.suite, # use hit session cipher kind
+				ctx.server_random # connection_id
+			),
+			ctx
+		);
+		# TODO: should in supported cipher_kinds
+		err: string;
+		(ctx.sel_ciph, ctx.sel_keyx, ctx.sel_sign, err) 
+			= v2suite_to_spec(ctx.session.suite, SSL2_Cipher_Kinds);
+		if(err != "") {
+			if(SSL_DEBUG)
+				log("do_v2client_hello: " + err);
+			ctx.state = SSL2_STATE_ERROR;
+			return;
+		}			
+		ctx.state = SSL2_STATE_SERVER_FINISHED;
+	}
+	else {
+		# find matching cipher kinds
+		n := len v2hs.cipher_specs;
+		matchs := array [n] of byte; 
+		j, k: int = 0;
+		while(j < n) {
+			# ignore those not in SSL2_Cipher_Kinds
+			matchs[k:] = v2hs.cipher_specs[j:j+3];
+			for(i := 0; i < len SSL2_Cipher_Kinds; i++) {
+				ck := SSL2_Cipher_Kinds[i];
+				if(matchs[k] == ck[0] && matchs[k+1] == ck[1] && matchs[k+2] == ck[2])
+					k +=3;
+			}
+			j += 3;
+		}
+		if(k == 0) {
+			if(SSL_DEBUG)
+				log("do_v2client_hello: No matching cipher kind");
+			ctx.state = SSL2_STATE_ERROR;
+		}
+		else {
+			matchs = matchs[0:k];
+
+			# Note: 
+			#	v2 challenge -> v3 client_random
+			#	v2 connection_id -> v3 server_random
+
+			chlen := len v2hs.challenge;
+			if(chlen > 32)
+				chlen = 32;
+			ctx.client_random = array [chlen] of byte;
+			if(chlen > 32)
+				ctx.client_random[0:] = v2hs.challenge[chlen-32:];
+			else
+				ctx.client_random[0:] = v2hs.challenge;
+			ctx.server_random = random->randombuf(Random->NotQuiteRandom, 16);
+			s.session_id = random->randombuf (
+					Random->NotQuiteRandom, 
+					SSL2_MAX_SESSION_ID_LENGTH_IN_BYTES
+				);
+			s.suite = matchs;
+			ctx.session = s;
+			v2handshake_enque(
+				ref V2Handshake.ServerHello(
+					0, # no hit - not resumable
+					SSL2_CT_X509_CERTIFICATE, 
+					SSL2_SERVER_VERSION, 
+					hd ctx.local_info.certs, # the first is user certificate
+					ctx.session.suite, # matched cipher kinds
+					ctx.server_random # connection_id
+				),
+				ctx
+			);
+			ctx.state = SSL2_STATE_CLIENT_MASTER_KEY;
+		}
+	}
+}
+
+# [client side]
+
+do_v2server_hello(v2hs: ref V2Handshake.ServerHello, ctx: ref Context)
+{
+	# must be v2 server hello otherwise it will be v3 server hello
+	# determined by auto record layer version detection
+	if(v2hs.version[0] != SSL2_SERVER_VERSION[0] 
+		|| v2hs.version[1] != SSL2_SERVER_VERSION[1]) {
+		if(SSL_DEBUG)
+			log("do_v2server_hello: not a v2 version");
+		ctx.state = SSL2_STATE_ERROR;
+		return;
+	}
+
+	ctx.session.version = SSL2_SERVER_VERSION;
+	ctx.server_random = v2hs.connection_id;
+
+	# check if a resumable session is found
+	if(v2hs.session_id_hit != 0) { # resume ok
+		err: string;
+		# TODO: should in supported cipher_kinds
+		(ctx.sel_ciph, nil, nil, err) = v2suite_to_spec(ctx.session.suite, SSL2_Cipher_Kinds);
+		if(err !=  "") {
+			if(SSL_DEBUG)
+				log("do_v2server_hello: " + err);
+			ctx.state = SSL2_STATE_ERROR;
+			return;
+		}
+	}
+	else { 	# not resumable session
+
+		# use the first matched cipher kind; install cipher spec
+		if(len v2hs.cipher_specs < 3) {
+			if(SSL_DEBUG)
+				log("do_v2server_hello: too few bytes");
+			ctx.state = SSL2_STATE_ERROR;
+			return;
+		}
+		ctx.session.suite = array [3] of byte;
+		ctx.session.suite[0:] = v2hs.cipher_specs[0:3];
+		err: string;
+		(ctx.sel_ciph, nil, nil, err) = v2suite_to_spec(ctx.session.suite, SSL2_Cipher_Kinds);
+		if(err != "") {
+			if(SSL_DEBUG)
+				log("do_v2server_hello: " + err);
+			return;
+		}
+			
+		# decode x509 certificates, authenticate server and extract 
+		# public key from server certificate
+		if(v2hs.certificate_type != int SSL2_CT_X509_CERTIFICATE) {
+			if(SSL_DEBUG)
+				log("do_v2server_hello: not x509 certificate");
+			ctx.state = SSL2_STATE_ERROR;
+			return;
+		}
+		ctx.session.peer_certs = v2hs.certificate :: nil;
+		# TODO: decode v2hs.certificate as list of certificate
+		# 	verify the list of certificate
+		(e, signed) := x509->Signed.decode(v2hs.certificate);
+		if(e != "") {
+			if(SSL_DEBUG)
+				log("do_v2server_hello: " + e);
+			ctx.state = SSL2_STATE_ERROR;
+			return;
+		}
+		certificate: ref Certificate;
+		(e, certificate) = x509->Certificate.decode(signed.tobe_signed);
+		if(e != "") {
+			if(SSL_DEBUG)
+				log("do_v2server_hello: " + e);
+			ctx.state = SSL2_STATE_ERROR;
+			return;
+		}		
+		id: int;
+		peer_pk: ref X509->PublicKey;
+		(e, id, peer_pk) = certificate.subject_pkinfo.getPublicKey();
+		if(e != nil) {
+			ctx.state = SSL2_STATE_ERROR; # protocol error
+			return;
+		}
+		pk: ref RSAKey;
+		pick key := peer_pk {
+		RSA =>
+			pk = key.pk;
+		* =>
+		}
+		# prepare and send client master key message
+		# TODO: change CipherSpec adt for more key info
+		# Temporary solution
+		# mkey (master key), ckey (clear key), skey(secret key)
+		mkey, ckey, skey, keyarg: array of byte;
+		(mkeylen, ckeylen, keyarglen) := v2suite_more(ctx.sel_ciph);
+		mkey = random->randombuf(Random->NotQuiteRandom, mkeylen);
+		if(ckeylen != 0)
+			ckey = mkey[0:ckeylen];
+		if(mkeylen > ckeylen)
+			skey = mkey[ckeylen:];
+		if(keyarglen > 0)
+			keyarg = random->randombuf(Random->NotQuiteRandom, keyarglen);
+		ekey: array of byte;
+		(e, ekey) = pkcs->rsa_encrypt(skey, pk, 2);
+		if(e != nil) {
+			if(SSL_DEBUG)
+				log("do_v2server_hello: " + e);
+			ctx.state = SSL2_STATE_ERROR;	
+			return;
+		}
+		ctx.session.master_secret = mkey;
+		v2handshake_enque(
+			ref V2Handshake.ClientMasterKey(ctx.session.suite, ckey, ekey, keyarg),
+			ctx
+		);
+	}
+
+	# clean up out_queue before switch cipher
+	record_write_queue(ctx);
+	ctx.out_queue.data = nil;
+
+	# install keys onto ctx that will be pushed on ssl record when ready
+	(ctx.cw_mac, ctx.sw_mac, ctx.cw_key, ctx.sw_key, ctx.cw_IV, ctx.sw_IV)
+		= v2calc_keys(ctx.sel_ciph, ctx.session.master_secret, 
+		ctx.client_random, ctx.server_random);
+	e := set_queues(ctx);
+	if(e != "") {
+		if(SSL_DEBUG)
+			log("do_v2server_finished: " + e);
+		ctx.state = SSL2_STATE_ERROR;
+		return;
+	}
+	ctx.status |= IN_READY;
+	ctx.status |= OUT_READY;
+
+	# prepare and send client finished message
+	v2handshake_enque(
+		ref V2Handshake.ClientFinished(ctx.server_random), # as connection_id
+		ctx
+	);
+
+	ctx.state = SSL2_STATE_SERVER_VERIFY;
+}
+
+# [server side]
+
+do_v2client_master_key(v2hs: ref V2Handshake.ClientMasterKey, ctx: ref Context)
+{
+	#if(cmk.cipher == -1 || cipher_info[cmk.cipher].cryptalg == -1) {
+	#	# return ("protocol error: bad cipher in masterkey", nullc);
+	#	ctx.state = SSL2_STATE_ERROR; # protocol error
+	#	return;
+	#}
+
+	ctx.session.suite = v2hs.cipher_kind;
+
+	# TODO:
+	#	someplace shall be able to install the key
+	# need further encapsulate encrypt and decrypt functions from KeyExAlg adt
+	master_key_length: int;
+	secret_key: array of byte;
+	pick alg := ctx.sel_keyx {
+	RSA =>
+		e: string;
+		(e, secret_key) = pkcs->rsa_decrypt(v2hs.encrypt_key, alg.sk, 0);
+		if(e != "") {
+			if(SSL_DEBUG)
+				log("do_v2client_master_key: " + e);
+			ctx.state = SSL2_STATE_ERROR;
+			return;
+		}
+		master_key_length = len v2hs.clear_key + len secret_key;
+	* =>
+		if(SSL_DEBUG)
+			log("do_v2client_master_key: unknown public key algorithm");
+		ctx.state = SSL2_STATE_ERROR;
+		return;
+	}
+	#TODO: do the following lines after modifying the CipherSpec adt
+	#if(master_key_length != ci.keylen) {
+	#	ctx.state = SSL2_STATE_ERROR; # protocol error
+	#	return;
+	#}
+
+	ctx.session.master_secret = array [master_key_length] of byte;
+	ctx.session.master_secret[0:] = v2hs.clear_key;
+	ctx.session.master_secret[len v2hs.clear_key:] = secret_key;
+
+	# install keys onto ctx that will be pushed on ssl record when ready
+	(ctx.cw_mac, ctx.sw_mac, ctx.cw_key, ctx.sw_key, ctx.cw_IV, ctx.sw_IV)
+		= v2calc_keys(ctx.sel_ciph, ctx.session.master_secret, 
+		ctx.client_random, ctx.server_random);
+	v2handshake_enque(
+		ref V2Handshake.ServerVerify(ctx.client_random[16:]),
+		ctx
+	);
+	v2handshake_enque(
+		ref V2Handshake.ServerFinished(ctx.session.session_id),
+		ctx
+	);
+	ctx.state = SSL2_STATE_CLIENT_FINISHED;
+}
+
+# used by client side
+do_v2server_verify(v2hs: ref V2Handshake.ServerVerify, ctx: ref Context)
+{
+	# TODO:
+	#	the challenge length may not be 16 bytes
+	if(bytes_cmp(v2hs.challenge, ctx.client_random[32-SSL2_CHALLENGE_LENGTH:]) < 0) {
+		if(SSL_DEBUG)
+			log("do_v2server_verify: challenge mismatch");
+		ctx.state = SSL2_STATE_ERROR;
+		return;
+	}
+
+	ctx.state = SSL2_STATE_SERVER_FINISHED;
+}
+
+# [client side]
+
+do_v2req_cert(v2hs: ref V2Handshake.RequestCertificate, ctx: ref Context)
+{
+	# not supported until v3
+	if(SSL_DEBUG)
+		log("do_v2req_cert: authenticate client not supported");
+	v2hs = nil;
+	ctx.state = SSL2_STATE_ERROR;
+}
+
+# [server side]
+
+do_v2client_certificate(v2hs: ref V2Handshake.ClientCertificate, ctx: ref Context)
+{
+	# not supported until v3
+	if(SSL_DEBUG)
+		log("do_v2client_certificate: authenticate client not supported");
+	v2handshake_enque (
+		ref V2Handshake.Error(SSL2_PE_NO_CERTIFICATE),
+		ctx
+	);
+	v2hs = nil;
+	ctx.state = SSL2_STATE_ERROR;
+}
+
+# [server side]
+
+do_v2client_finished(v2hs: ref V2Handshake.ClientFinished, ctx: ref Context)
+{
+	if(bytes_cmp(ctx.server_random, v2hs.connection_id) < 0) {
+		ctx.session.session_id = nil;
+		if(SSL_DEBUG)
+			log("do_v2client_finished: connection id mismatch");
+		ctx.state = SSL2_STATE_ERROR;
+	}
+	# TODO:
+	#	the challenge length may not be 16 bytes
+	v2handshake_enque(
+		ref V2Handshake.ServerVerify(ctx.client_random[32-SSL2_CHALLENGE_LENGTH:]),
+		ctx
+	);
+	if(ctx.session.session_id == nil)
+		ctx.session.session_id = random->randombuf(Random->NotQuiteRandom, 16);
+	v2handshake_enque(
+		ref V2Handshake.ServerFinished(ctx.session.session_id),
+		ctx
+	);
+	e := set_queues(ctx);
+	if(e != "") {
+		if(SSL_DEBUG)
+			log("do_v2client_finished: " + e);
+		ctx.state = SSL2_STATE_ERROR;
+		return;
+	}
+	ctx.status |= IN_READY;
+	ctx.status |= OUT_READY;
+	sslsession->add_session(ctx.session);
+
+	ctx.state = STATE_EXIT;
+}
+
+# [client side]
+
+do_v2server_finished(v2hs: ref V2Handshake.ServerFinished, ctx: ref Context)
+{
+	if(ctx.session.session_id == nil)
+		ctx.session.session_id = array [16] of byte;
+	ctx.session.session_id[0:] = v2hs.session_id[0:];
+
+	sslsession->add_session(ctx.session);
+
+	ctx.state = STATE_EXIT;
+}
+
+
+# Note:
+#	the key partitioning for v2 is different from v3
+
+v2calc_keys(ciph: ref CipherSpec, ms, cr, sr: array of byte)
+	: (array of byte, array of byte, array of byte, array of byte, array of byte, array of byte)
+{
+	cw_mac, sw_mac, cw_key, sw_key,	cw_IV, sw_IV: array of byte;
+
+	# TODO: check the size of key block if IV exists
+	(mkeylen, ckeylen, keyarglen) := v2suite_more(ciph);
+	kblen := 2*mkeylen;
+	if(kblen%Keyring->MD5dlen != 0) {
+		if(SSL_DEBUG)
+			log("v2calc_keys: key block length is not multiple of MD5 hash length");
+	}
+	else {
+		key_block := array [kblen] of byte;
+
+		challenge := cr[32-SSL2_CHALLENGE_LENGTH:32]; # TODO: if challenge length != 16 ?
+		connection_id := sr[0:16]; # TODO: if connection_id length != 16 ?
+		var := array [1] of byte;
+		var[0] = byte 16r30;
+		i := 0;
+		while(i < kblen) {
+			(hash, nil) := md5_hash(ms::var::challenge::connection_id::nil, nil);
+			key_block[i:] = hash;
+			i += Keyring->MD5dlen;
+			++var[0];
+		}
+
+		if(SSL_DEBUG)
+			log("ssl3: calc_keys:" 
+			+ "\n\tmaster key = \n\t\t" + bastr(ms)
+			+ "\n\tchallenge = \n\t\t" + bastr(challenge)
+			+ "\n\tconnection id = \n\t\t" + bastr(connection_id)
+			+ "\n\tkey block = \n\t\t" + bastr(key_block) + "\n");
+
+		i = 0;
+		# server write key == client read key
+		# server write mac == server write key
+		sw_key = array [mkeylen] of byte;
+		sw_key[0:] = key_block[i:mkeylen];
+		sw_mac = array [mkeylen] of byte;
+		sw_mac[0:] = key_block[i:mkeylen];
+		# client write key == server read key
+		# client write mac == client write key
+		i += mkeylen;
+		cw_key = array [mkeylen] of byte;
+		cw_key[0:] = key_block[i:i+mkeylen];
+		cw_mac = array [mkeylen] of byte;
+		cw_mac[0:] = key_block[i:i+mkeylen];
+		# client IV == server IV
+		# Note:
+		#	IV is a part of writing or reading key for ssl device
+		#	this is composed again in setctl
+		cw_IV = array [keyarglen] of byte;
+		cw_IV[0:] = ms[mkeylen:mkeylen+keyarglen];
+		sw_IV = array [keyarglen] of byte;
+		sw_IV[0:] = ms[mkeylen:mkeylen+keyarglen];
+	}
+
+	if(SSL_DEBUG)
+		log("ssl3: calc_keys:" 
+		+ "\n\tclient_write_mac = \n\t\t" + bastr(cw_mac)
+		+ "\n\tserver_write_mac = \n\t\t" + bastr(sw_mac)
+		+ "\n\tclient_write_key = \n\t\t" + bastr(cw_key)
+		+ "\n\tserver_write_key = \n\t\t" + bastr(sw_key)
+ 		+ "\n\tclient_write_IV  = \n\t\t" + bastr(cw_IV)
+		+ "\n\tserver_write_IV = \n\t\t" + bastr(sw_IV) + "\n");
+
+	return (cw_mac, sw_mac, cw_key, sw_key, cw_IV, sw_IV);
+}
+
+v3tov2specs(suites: array of byte): array of byte
+{
+	# v3 suite codes are 2 bytes each, v2 codes are 3 bytes
+	n := len suites / 2;
+	kinds := array [n*3*2] of byte;
+	k := 0;
+	for(i := 0; i < n;) {
+		a := suites[i:i+2];
+		i += 2;
+		m := len SSL3_Suites;
+		for(j := 0; j < m; j++) {
+			b := SSL3_Suites[j]; 
+			if(a[0]==b[0] && a[1]==b[1]) 
+				break;
+		}
+		if (j == m) {
+			if(SSL_DEBUG)
+				log("ssl3: unknown v3 suite");
+			continue;
+		}
+		case j {
+		RSA_EXPORT_WITH_RC4_40_MD5 => 	
+			kinds[k:] = SSL2_Cipher_Kinds[SSL2_CK_RC4_128_EXPORT40_WITH_MD5];
+			k += 3;
+		RSA_WITH_RC4_128_MD5 => 		
+			kinds[k:] = SSL2_Cipher_Kinds[SSL2_CK_RC4_128_WITH_MD5];
+			k += 3;
+		RSA_WITH_IDEA_CBC_SHA =>
+			kinds[k:] = SSL2_Cipher_Kinds[SSL2_CK_IDEA_128_CBC_WITH_MD5];
+			k += 3;
+		RSA_WITH_DES_CBC_SHA =>
+			;
+		* =>
+			if(SSL_DEBUG)
+				log("ssl3: unable to convert v3 suite to v2 kind");
+		}
+		# append v3 code in v2-safe manner
+		# (suite[0] == 0) => will be ignored by v2 server, picked up by v3 server
+		kinds[k] = byte 16r00;
+		kinds[k+1:] = SSL3_Suites[j];
+		k += 3;
+	}
+	return kinds[0:k];
+}
+
+v2suite_to_spec(cs: array of byte, cipher_kinds: array of array of byte)
+	: (ref CipherSpec, ref KeyExAlg, ref SigAlg, string)
+{
+	cip : ref CipherSpec;
+	kex : ref KeyExAlg;
+	sig : ref SigAlg;
+
+	n := len cipher_kinds;
+	for(i := 0; i < n; i++) {
+		found := cipher_kinds[i];
+		if(found[0]==cs[0] && found[1]==cs[1] && found[2]==cs[2]) break;
+	}
+
+	if(i == n)
+		return (nil, nil, nil, "fail to find a matched spec");
+
+	case i {
+	SSL2_CK_RC4_128_WITH_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_RC4, SSL_STREAM_CIPHER, 
+				16, 0, SSL_MD5, Keyring->MD4dlen);
+
+	SSL2_CK_RC4_128_EXPORT40_WITH_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_RC4, SSL_STREAM_CIPHER, 
+				5, 0, SSL_MD5, Keyring->MD4dlen);
+
+	SSL2_CK_RC2_CBC_128_CBC_WITH_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_RC2_CBC, SSL_BLOCK_CIPHER, 
+				16, 8, SSL_MD5, Keyring->MD4dlen);
+
+	SSL2_CK_RC2_CBC_128_CBC_EXPORT40_WITH_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_TRUE, SSL_RC2_CBC, SSL_BLOCK_CIPHER, 
+				5, 8, SSL_MD5, Keyring->MD4dlen);
+
+	SSL2_CK_IDEA_128_CBC_WITH_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_IDEA_CBC, SSL_BLOCK_CIPHER, 
+				16, 8, SSL_MD5, Keyring->MD4dlen);
+
+	SSL2_CK_DES_64_CBC_WITH_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_DES_CBC, SSL_BLOCK_CIPHER, 
+				8, 8, SSL_MD5, Keyring->MD4dlen);
+
+	SSL2_CK_DES_192_EDE3_CBC_WITH_MD5 =>
+		cip = ref CipherSpec(SSL_EXPORT_FALSE, SSL_3DES_EDE_CBC, SSL_BLOCK_CIPHER, 
+				24, 8, SSL_MD5, Keyring->MD4dlen);
+	}
+
+	kex = ref KeyExAlg.RSA(nil, nil, nil);
+	sig = ref SigAlg.RSA(nil, nil);
+
+	return (cip, kex, sig, nil);
+}
+
+v2suite_more(ciph: ref CipherSpec): (int, int, int)
+{
+	mkeylen, ckeylen, keyarglen: int;
+
+	case ciph.bulk_cipher_algorithm {
+	SSL_RC4 =>
+		mkeylen = 16;
+		if(ciph.key_material == 5)
+			ckeylen = 16 - 5;
+		else
+			ckeylen = 0;
+		keyarglen = 0;
+		
+	SSL_RC2_CBC =>
+		mkeylen = 16;
+		if(ciph.key_material == 5)
+			ckeylen = 16 - 5;
+		else
+			ckeylen = 0;
+		keyarglen = 8;
+
+	SSL_IDEA_CBC =>
+		mkeylen = 16;
+		ckeylen = 0;
+		keyarglen = 8;
+
+	SSL_DES_CBC =>
+		mkeylen = 8;
+		if(ciph.key_material == 5)
+			ckeylen = 8 - 5;
+		else
+			ckeylen = 0;
+		keyarglen = 8;
+
+	SSL_3DES_EDE_CBC =>
+		mkeylen = 24;
+		ckeylen = 0;
+		keyarglen = 8;
+	}
+
+	return (mkeylen, ckeylen, keyarglen);
+}
+
+v2handshake_enque(h: ref V2Handshake, ctx: ref Context)
+{
+	p := ref Protocol.pV2Handshake(h);
+
+	protocol_write(p, ctx);
+}
+
+V2Handshake.encode(hm: self ref V2Handshake): (array of byte, string)
+{
+	a : array of byte;
+	n : int;
+	e : string;
+
+	i := 0;
+	pick m := hm {
+	Error =>
+		n = 3;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_ERROR;
+		a[i:] = m.code;
+
+	ClientHello =>
+		specslen := len m.cipher_specs;
+		sidlen := len m.session_id;
+		challen := len m.challenge;
+		n = 9+specslen + sidlen + challen;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_CLIENT_HELLO;
+		a[i:] = m.version;
+		i += 2;
+		a[i:] = int_encode(specslen, 2);
+		i += 2;
+		a[i:] = int_encode(sidlen, 2);
+		i += 2;
+		a[i:] = int_encode(challen, 2);
+		i += 2;
+		a[i:] = m.cipher_specs;
+		i += specslen;
+		if(sidlen != 0) {
+			a[i:] = m.session_id;
+			i += sidlen;
+		}
+		if(challen != 0) {
+			a[i:] = m.challenge;
+			i += challen;
+		}	
+
+	ServerHello =>
+		# use only the user certificate
+		certlen := len m.certificate;
+#		specslen := 3*len m.cipher_specs;
+		specslen := len m.cipher_specs;
+		cidlen := len m.connection_id;
+		n = 11 + certlen + specslen + cidlen;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_SERVER_HELLO;
+		a[i++] = byte m.session_id_hit;
+		a[i++] = byte m.certificate_type;
+		a[i:] = m.version;
+		i += 2;
+		a[i:] = int_encode(certlen, 2);
+		i += 2;
+		a[i:] = int_encode(specslen, 2);
+		i += 2;
+		a[i:] = int_encode(cidlen, 2);
+		i += 2;
+		a[i:] = m.certificate;		
+		i += certlen;
+		a[i:] = m.cipher_specs;
+		i += specslen;
+		a[i:] = m.connection_id;
+		i += cidlen;
+
+	ClientMasterKey =>
+		ckeylen := len m.clear_key;
+		ekeylen := len m.encrypt_key;
+		karglen := len m.key_arg;
+		n = 10 + ckeylen + ekeylen + karglen;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_CLIENT_MASTER_KEY;
+		a[i:] = m.cipher_kind;
+		i += 3;
+		a[i:] = int_encode(ckeylen, 2);
+		i += 2;
+		a[i:] = int_encode(ekeylen, 2);
+		i += 2;
+		a[i:] = int_encode(karglen, 2);
+		i += 2;
+		a[i:] = m.clear_key;
+		i += ckeylen;
+		a[i:] = m.encrypt_key;
+		i += ekeylen;
+		a[i:] = m.key_arg;
+		i += karglen;
+
+	ServerVerify =>
+		challen := len m.challenge;
+		n = 1 + challen;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_SERVER_VERIFY;
+		a[i:] = m.challenge;
+
+	RequestCertificate =>
+		cclen := len m.certificate_challenge;
+		n = 2 + cclen;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_REQUEST_CERTIFICATE;
+		a[i++] = byte m.authentication_type;
+		a[i:] = m.certificate_challenge;
+		i += cclen;
+
+	ClientCertificate =>
+		# use only the user certificate
+		certlen := len m.certificate;
+		resplen := len m.response;
+		n = 6 + certlen + resplen;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_CLIENT_CERTIFICATE;
+		a[i++] = byte m.certificate_type;
+		a[i:] = int_encode(certlen, 2);
+		i += 2;
+		a[i:] = int_encode(resplen, 2);
+		i += 2;
+		a[i:] = m.certificate;
+		i += certlen;
+		a[i:] = m.response;
+		i += resplen;
+
+	ClientFinished =>
+		cidlen := len m.connection_id;
+		n = 1 + cidlen;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_CLIENT_FINISHED;
+		a[i:] = m.connection_id;
+		i += cidlen;
+
+	ServerFinished =>
+		sidlen := len m.session_id;
+		n = 1 + sidlen;
+		a = array[n] of byte;
+		a[i++] = byte SSL2_MT_SERVER_FINISHED;
+		a[i:] = m.session_id;
+		i += sidlen;
+	}
+
+	return (a, e);
+}
+
+V2Handshake.decode(a: array of byte): (ref V2Handshake, string)
+{
+	m : ref V2Handshake;
+	e : string;
+
+	n := len a;
+	i := 1; 
+	case int a[0] {
+	SSL2_MT_ERROR =>
+		if(n != 3)
+			break;
+		code := a[i:i+2];
+		i += 2;
+		m = ref V2Handshake.Error(code);
+
+	SSL2_MT_CLIENT_HELLO =>
+		if(n < 9) {
+			e = "client hello: message too short";
+			break;
+		}
+		ver := a[i:i+2];
+		i += 2;
+		specslen := int_decode(a[i:i+2]);
+		i += 2;
+		sidlen := int_decode(a[i:i+2]);
+		i += 2;
+		challen := int_decode(a[i:i+2]);
+		i += 2;
+		if(n != 9+specslen+sidlen+challen) {
+			e = "client hello: length mismatch";
+			break;
+		}
+		if(specslen%3 != 0) {
+			e = "client hello: must multiple of 3 bytes";
+			break;
+		}
+		specs: array of byte;
+		if(specslen != 0) {
+			specs = a[i:i+specslen];
+			i += specslen;
+		}
+		sid: array of byte;
+		if(sidlen != 0) {
+			sid = a[i:i+sidlen];
+			i += sidlen;
+		}
+		chal: array of byte;
+		if(challen != 0) {
+			chal = a[i:i+challen];
+			i += challen;
+		}
+		m = ref V2Handshake.ClientHello(ver, specs, sid, chal);
+
+	SSL2_MT_CLIENT_MASTER_KEY =>
+		if(n < 10) {
+			e = "client master key: message too short";
+			break;
+		}
+		kind := a[i:i+3];
+		i += 3;
+		ckeylen := int_decode(a[i:i+2]);
+		i += 2;
+		ekeylen := int_decode(a[i:i+2]);
+		i += 2;
+		karglen := int_decode(a[i:i+2]);
+		i += 2;
+		if(n != 10 + ckeylen + ekeylen + karglen) {
+			e = "client master key: length mismatch";
+			break;
+		}
+		ckey := a[i:i+ckeylen];
+		i += ckeylen;
+		ekey := a[i:i+ekeylen];
+		i += ekeylen;
+		karg := a[i:i+karglen];
+		i += karglen;
+		m = ref V2Handshake.ClientMasterKey(kind, ckey, ekey, karg);
+
+	SSL2_MT_CLIENT_FINISHED =>
+		cid := a[i:n];
+		i = n;
+		m = ref V2Handshake.ClientFinished(cid);
+
+	SSL2_MT_SERVER_HELLO =>
+		if(n < 11) {
+			e = "server hello: messsage too short";
+			break;
+		}
+		sidhit := int a[i++];
+		certtype := int a[i++];
+		ver := a[i:i+2];
+		i += 2;
+		certlen := int_decode(a[i:i+2]);
+		i += 2;
+		specslen := int_decode(a[i:i+2]);
+		i += 2;
+		cidlen := int_decode(a[i:i+2]);
+		i += 2;
+		if(n != 11+certlen+specslen+cidlen) {
+			e = "server hello: length mismatch";
+			break;
+		}
+		cert := a[i:i+certlen];
+		i += certlen;
+		if(specslen%3 != 0) {
+			e = "server hello: must be multiple of 3 bytes";
+			break;
+		}
+		specs := a[i:i+specslen];
+		i += specslen;
+		if(cidlen < 16 || cidlen > 32) {
+			e = "server hello: connection id length out of range";
+			break;
+		}
+		cid := a[i:i+cidlen];
+		i += cidlen;
+		m = ref V2Handshake.ServerHello(sidhit, certtype, ver, cert, specs, cid);
+
+	SSL2_MT_SERVER_VERIFY =>
+		chal := a[i:n];
+		i = n;
+		m = ref V2Handshake.ServerVerify(chal);
+
+	SSL2_MT_SERVER_FINISHED =>
+		sid := a[i:n];
+		m = ref V2Handshake.ServerFinished(sid);
+
+	SSL2_MT_REQUEST_CERTIFICATE =>
+		if(n < 2) {
+			e = "request certificate: message too short";
+			break;
+		}
+		authtype := int a[i++];
+		certchal := a[i:n];
+		i = n;
+		m = ref V2Handshake.RequestCertificate(authtype, certchal);
+
+	SSL2_MT_CLIENT_CERTIFICATE =>
+		if(n < 6) {
+			e = "client certificate: message too short";
+			break;
+		}
+		certtype := int a[i++];
+		certlen := int_decode(a[i:i+2]);
+		i += 2;
+		resplen := int_decode(a[i:i+2]);
+		i += 2;
+		if(n != 6+certlen+resplen) {
+			e = "client certificate: length mismatch";
+			break;
+		}
+		cert := a[i:i+certlen];
+		i += certlen;
+		resp := a[i:i+resplen];
+		m = ref V2Handshake.ClientCertificate(certtype, cert, resp);
+
+	* =>
+		e = "unknown message [" + string a[0] + "]";
+	}
+
+	return (m, e);
+}
+
+# utilities
+
+md5_hash(input: list of array of byte, md5_ds: ref DigestState): (array of byte, ref DigestState)
+{
+	hash_value := array [Keyring->MD5dlen] of byte;
+	ds : ref DigestState;
+
+	if(md5_ds != nil)
+		ds = md5_ds.copy();
+
+	lab := input;
+	for(i := 0; i < len input - 1; i++) {
+		ds = keyring->md5(hd lab, len hd lab, nil, ds);
+		lab = tl lab;
+	}
+	ds = keyring->md5(hd lab, len hd lab, hash_value, ds);
+
+	return (hash_value, ds);
+}
+
+sha_hash(input: list of array of byte, sha_ds: ref DigestState): (array of byte, ref DigestState)
+{
+	hash_value := array [Keyring->SHA1dlen] of byte;
+	ds : ref DigestState;
+
+	if(sha_ds != nil)
+		ds = sha_ds.copy();
+
+	lab := input;
+	for(i := 0; i < len input - 1; i++) {
+		ds = keyring->sha1(hd lab, len hd lab, nil, ds);
+		lab = tl lab;
+	}
+	ds = keyring->sha1(hd lab, len hd lab, hash_value, ds);
+
+	return (hash_value, ds);
+}
+
+md5_sha_hash(input: list of array of byte, md5_ds, sha_ds: ref DigestState)
+	: (array of byte, ref DigestState, ref DigestState)
+{
+	buf := array [Keyring->MD5dlen+Keyring->SHA1dlen] of byte;
+
+	(buf[0:], md5_ds) = md5_hash(input, md5_ds);
+	(buf[Keyring->MD5dlen:], sha_ds) = sha_hash(input, sha_ds);
+
+	return (buf, md5_ds, sha_ds);
+}
+
+int_decode(buf: array of byte): int
+{
+	val := 0;
+	for(i := 0; i < len buf; i++)
+		val = (val << 8) | (int buf[i]);
+
+	return val;	
+}
+
+int_encode(value, length: int): array of byte
+{
+	buf := array [length] of byte;
+
+	while(length--)	{   
+		buf[length] = byte value;
+		value >>= 8;
+	}
+
+	return buf;
+}
+
+
+bastr(a: array of byte) : string
+{
+	ans : string = "";
+
+	for(i := 0; i < len a; i++) {
+		if(i < len a - 1 && i != 0 && i%10 == 0)
+			ans += "\n\t\t";
+		if(i == len a -1)
+			ans += sys->sprint("%2x", int a[i]);
+		else
+			ans += sys->sprint("%2x ", int a[i]);
+	}
+
+	return ans;
+}
+
+bbastr(a: array of array of byte) : string
+{
+	info := "";
+
+	for(i := 0; i < len a; i++)
+		info += bastr(a[i]);
+	
+	return info;
+}
+
+lbastr(a: list of array of byte) : string
+{
+	info := "";
+
+	l := a;
+	while(l != nil) {
+		info += bastr(hd l) + "\n\t\t";
+		l = tl l;
+	}
+
+	return info;
+}
+
+# need to fix (string a == string b)
+bytes_cmp(a, b: array of byte): int
+{
+	if(len a != len b)
+		return -1;
+
+	n := len a;
+	for(i := 0; i < n; i++) {
+		if(a[i] != b[i])
+			return -1;
+	}
+
+	return 0;
+}
+
+putn(a: array of byte, i, value, n: int): int
+{
+	j := n;
+	while(j--) {   
+		a[i+j] = byte value;
+		value >>= 8;
+	}
+	return i+n;
+}
+
+INVALID_SUITE : con "invalid suite list";
+ILLEGAL_SUITE : con "illegal suite list";
+
+cksuites(suites : array of byte) : string
+{
+	m := len suites;
+	if (m == 0 || (m&1))
+		return INVALID_SUITE;
+	n := len SSL3_Suites;
+	ssl3s := array [2] of byte;
+	for (j := 0; j < m; j += 2) {
+		for( i := 0; i < n; i++) {
+			ssl3s = SSL3_Suites[i];
+			if(suites[j] == ssl3s[0] && suites[j+1] == ssl3s[1])
+				break;
+		}
+		if (i == n)
+			return ILLEGAL_SUITE;
+	}
+	return nil;
+}
--- /dev/null
+++ b/appl/lib/crypt/sslsession.b
@@ -1,0 +1,176 @@
+#
+# SSL Session Cache
+#
+implement SSLsession;
+
+include "sys.m";
+	sys					: Sys;
+
+include "daytime.m";
+	daytime					: Daytime;
+
+include "sslsession.m";
+
+
+# default session id timeout
+TIMEOUT_SECS 					: con 5*60; # sec
+
+SessionCache: adt {
+	db					: list of ref Session;
+	time_out				: int;
+};
+
+# The shared session cache by all ssl contexts is available for efficiently resumming
+# sessions for different run time contexts.
+
+Session_Cache					: ref SessionCache;
+
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil)
+		return "sslsession: load sys module failed";
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return "sslsession: load Daytime module failed";
+
+	Session_Cache = ref SessionCache(nil, TIMEOUT_SECS);
+
+	return "";
+}
+
+Session.new(peer: string, time: int, ver: array of byte): ref Session
+{
+	s := ref Session;
+
+	s.peer = peer;
+	s.connection_time = time;
+	s.version = array [2] of byte;
+	s.version[0:] = ver;
+	s.session_id = nil;
+	s.suite = nil;
+	s.master_secret = nil;
+	s.peer_certs = nil;
+
+	return s;
+}
+
+Session.duplicate(s: self ref Session): ref Session
+{
+	new := ref Session;
+
+	new.peer = s.peer;
+	new.connection_time = s.connection_time;
+	new.version = array [len s.version] of byte;
+	new.version[0:] = s.version;
+	new.session_id = array [len s.session_id] of byte;
+	new.session_id[0:] = s.session_id;
+	new.suite = array [len s.suite] of byte;
+	new.suite[0:] = s.suite;
+	new.master_secret = array [len s.master_secret] of byte;
+	new.master_secret[0:] = s.master_secret;
+	l: list of array of byte;
+	pcs := s.peer_certs;
+	while(pcs != nil) {
+		a := hd pcs;
+		b := array [len a] of byte;
+		b[0:] = a;
+		l = b :: l;
+		pcs = tl pcs;
+	}
+	while(l != nil) {
+		new.peer_certs = (hd l) :: new.peer_certs;
+		l = tl l;
+	}
+	return new;
+}
+
+# Each request process should get a copy of a session. A session will be
+# removed from database if it is expired. The garbage
+# collector will finally remove it from memory if there are no more
+# references to it.
+
+get_session_byname(peer: string): ref Session
+{
+	s: ref Session;
+	now := daytime->now(); # not accurate but more efficient
+
+	l := Session_Cache.db;
+	while(l != nil) {
+		if((hd l).peer == peer) {
+			s = hd l;
+			# TODO: remove expired session
+			if(now > s.connection_time+Session_Cache.time_out)
+				s = nil;
+			break;
+		}
+		l = tl l;
+	}
+	if(s == nil)
+		s = Session.new(peer, now, nil);
+	else
+		s = s.duplicate();
+
+	return s;
+}
+
+# replace the old by the new one
+add_session(s: ref Session)
+{
+	#old : ref Session;
+
+	#ls := Session_Cache.db;
+	#while(ls != nil) {
+	#	old = hd ls;
+	#	if(s.session_id == old.session_id) {
+	#		# old = s;
+	#		return;
+	#	}
+	#}
+
+	# always resume the most recent
+	if(s != nil)
+		Session_Cache.db = s :: Session_Cache.db;
+}
+
+get_session_byid(session_id: array of byte): ref Session
+{
+	s: ref Session;	
+	now := daytime->now(); # not accurate but more efficient
+	l := Session_Cache.db;
+	while(l != nil) {
+		if(bytes_cmp((hd l).session_id, session_id) == 0) {
+			s = hd l;
+			# replace expired session
+			if(now > s.connection_time+Session_Cache.time_out)
+				s = Session.new(s.peer, now, nil);
+			else
+				s = s.duplicate();
+			break;
+		}
+		l = tl l;
+	}
+	return s;
+}
+
+set_timeout(t: int)
+{
+	Session_Cache.time_out = t;
+}
+
+bytes_cmp(a, b: array of byte): int
+{
+	if(len a != len b)
+		return -1;
+
+	n := len a;
+	for(i := 0; i < n; i++) {
+		if(a[i] != b[i])
+			return -1;
+	}
+
+	return 0;
+}
+
--- /dev/null
+++ b/appl/lib/crypt/x509.b
@@ -1,0 +1,4125 @@
+implement X509;
+
+include "sys.m";
+	sys				: Sys;
+
+include "asn1.m";
+	asn1				: ASN1;
+	Elem, Tag, Value, Oid,
+	Universal, Context,
+	BOOLEAN,
+	INTEGER,
+	BIT_STRING,
+	OCTET_STRING,
+	OBJECT_ID,
+	SEQUENCE,
+	UTCTime,
+	IA5String,
+	GeneralString,
+	GeneralizedTime			: import asn1;
+
+include "keyring.m";
+	keyring				: Keyring;
+	IPint, DESstate	: import keyring;
+
+include "security.m";
+	random				: Random;
+
+include "daytime.m";
+	daytime				: Daytime;
+
+include "pkcs.m";
+	pkcs				: PKCS;
+
+include "x509.m";
+
+X509_DEBUG 				: con 0;
+logfd 					: ref Sys->FD;
+
+TAG_MASK 				: con 16r1F;
+CONSTR_MASK 				: con 16r20;
+CLASS_MASK 				: con 16rC0;
+
+# object identifiers
+
+objIdTab = array [] of {
+	id_at =>			Oid(array [] of {2,5,4}),
+	id_at_commonName => 		Oid(array [] of {2,5,4,3}),
+	id_at_countryName => 		Oid(array [] of {2,5,4,6}),
+	id_at_localityName => 		Oid(array [] of {2,5,4,7}), 
+	id_at_stateOrProvinceName => 	Oid(array [] of {2,5,4,8}),
+	id_at_organizationName =>	Oid(array [] of {2,5,4,10}),
+	id_at_organizationalUnitName => Oid(array [] of {2,5,4,11}), 
+	id_at_userPassword =>		Oid(array [] of {2,5,4,35}),
+	id_at_userCertificate =>	Oid(array [] of {2,5,4,36}),
+	id_at_cAcertificate =>		Oid(array [] of {2,5,4,37}),
+	id_at_authorityRevocationList =>
+					Oid(array [] of {2,5,4,38}),
+	id_at_certificateRevocationList =>
+					Oid(array [] of {2,5,4,39}),
+	id_at_crossCertificatePair =>	Oid(array [] of {2,5,4,40}),
+# 	id_at_crossCertificatePair => 	Oid(array [] of {2,5,4,58}),
+	id_at_supportedAlgorithms =>	Oid(array [] of {2,5,4,52}),
+	id_at_deltaRevocationList =>	Oid(array [] of {2,5,4,53}),
+
+	id_ce =>			Oid(array [] of {2,5,29}),
+	id_ce_subjectDirectoryAttributes =>
+					Oid(array [] of {2,5,29,9}),
+	id_ce_subjectKeyIdentifier =>	Oid(array [] of {2,5,29,14}),
+	id_ce_keyUsage =>		Oid(array [] of {2,5,29,15}),
+	id_ce_privateKeyUsage =>	Oid(array [] of {2,5,29,16}),
+	id_ce_subjectAltName =>		Oid(array [] of {2,5,29,17}),
+	id_ce_issuerAltName =>		Oid(array [] of {2,5,29,18}),
+	id_ce_basicConstraints =>	Oid(array [] of {2,5,29,19}),
+	id_ce_cRLNumber =>		Oid(array [] of {2,5,29,20}),
+	id_ce_reasonCode =>		Oid(array [] of {2,5,29,21}),
+	id_ce_instructionCode =>	Oid(array [] of {2,5,29,23}),
+	id_ce_invalidityDate =>		Oid(array [] of {2,5,29,24}),
+	id_ce_deltaCRLIndicator =>	Oid(array [] of {2,5,29,27}),
+	id_ce_issuingDistributionPoint =>
+					Oid(array [] of {2,5,29,28}),
+	id_ce_certificateIssuer =>	Oid(array [] of {2,5,29,29}),
+	id_ce_nameConstraints =>	Oid(array [] of {2,5,29,30}),
+	id_ce_cRLDistributionPoint =>	Oid(array [] of {2,5,29,31}),
+	id_ce_certificatePolicies =>	Oid(array [] of {2,5,29,32}),
+	id_ce_policyMapping =>		Oid(array [] of {2,5,29,33}),
+	id_ce_authorityKeyIdentifier =>
+					Oid(array [] of {2,5,29,35}),
+	id_ce_policyConstraints	=>	Oid(array [] of {2,5,29,36}),
+
+#	id_mr =>			Oid(array [] of {2,5,?}),
+# 	id_mr_certificateMatch =>	Oid(array [] of {2,5,?,35}),
+# 	id_mr_certificatePairExactMatch	=>
+#					Oid(array [] of {2,5,?,36}),
+# 	id_mr_certificatePairMatch =>	Oid(array [] of {2,5,?,37}),
+# 	id_mr_certificateListExactMatch	=>
+#					Oid(array [] of {2,5,?,38}),
+# 	id_mr_certificateListMatch =>	Oid(array [] of {2,5,?,39}),
+# 	id_mr_algorithmidentifierMatch =>
+#					Oid(array [] of {2,5,?,40})
+};
+
+# [public]
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+
+	if(X509_DEBUG)
+		logfd = sys->fildes(1);
+
+	keyring = load Keyring Keyring->PATH;
+	if(keyring == nil)
+		return sys->sprint("load %s: %r", Keyring->PATH);
+
+	random = load Random Random->PATH;
+	if(random == nil)
+		return sys->sprint("load %s: %r", Random->PATH);
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return sys->sprint("load %s: %r", Daytime->PATH);
+
+	asn1 = load ASN1 ASN1->PATH;
+	if(asn1 == nil)
+		return sys->sprint("load %s: %r", ASN1->PATH);
+	asn1->init();
+
+	pkcs = load PKCS PKCS->PATH;
+	if(pkcs == nil)
+		return sys->sprint("load %s: %r", PKCS->PATH);
+	if((e := pkcs->init()) != nil)
+		return sys->sprint("pkcs: %s", e);
+
+	return nil;
+}
+
+# [private]
+
+log(s: string)
+{
+	if(X509_DEBUG)
+		sys->fprint(logfd, "x509: %s\n", s);
+}
+
+## SIGNED { ToBeSigned } ::= SEQUENCE {
+##	toBeSigned	ToBeSigned,
+##	COMPONENTS OF	SIGNATURE { ToBeSigned }}
+##
+## SIGNATURE {OfSignature} ::= SEQUENCE {
+##	algorithmIdentifier	AlgorithmIdentifier,
+##	encrypted	ENCRYPTED { HASHED { OfSignature }}}
+##
+## ENCRYPTED { ToBeEnciphered }	::= BIT STRING ( CONSTRAINED BY {
+##	-- must be the result of applying an encipherment procedure --
+##	-- to the BER-encoded octets of a value of -- ToBeEnciphered } )
+
+# [public]
+
+Signed.decode(a: array of byte): (string, ref Signed)
+{
+parse:
+	for(;;) {
+		# interpret the enclosing structure
+		(ok, tag, i, n) := der_dec1(a, 0, len a);
+		if(!ok || n != len a || !tag.constr || 
+			tag.class != Universal || tag.num != SEQUENCE)
+			break parse;
+		s := ref Signed;
+		# SIGNED sequence
+		istart := i;
+		(ok, tag, i, n) = der_dec1(a, i, len a);
+		if(!ok || n == len a)
+			break parse;
+		s.tobe_signed = a[istart:n];
+		# AlgIdentifier
+		istart = n;
+		(ok, tag, i, n) = der_dec1(a, n, len a);
+		if(!ok || n == len a 
+			|| !tag.constr || tag.class != Universal || tag.num != SEQUENCE) {
+			if(X509_DEBUG)
+				log("signed: der data: " + 
+				sys->sprint("ok=%d, n=%d, constr=%d, class=%d, num=%d", 
+				ok, n, tag.constr, tag.class, tag.num));
+			break parse;
+		}
+		(ok, s.alg) = decode_algid(a[istart:n]);
+		if(!ok) {
+			if(X509_DEBUG)
+				log("signed: alg identifier: syntax error");
+			break;		
+		}
+		# signature
+		(ok, tag, i, n) = der_dec1(a, n, len a);
+		if(!ok || n != len a
+			|| tag.constr || tag.class != Universal || tag.num != BIT_STRING) {
+			if(X509_DEBUG)
+				log("signed: signature: " + 
+				sys->sprint("ok=%d, n=%d, constr=%d, class=%d, num=%d", 
+				ok, n, tag.constr, tag.class, tag.num));
+			break parse;
+		}
+		s.signature = a[i:n];
+		# to the end of no error been found
+		return ("", s);
+	}
+	return ("signed: syntax error", nil);
+}
+
+# [public]
+# der encoding of signed data
+
+Signed.encode(s: self ref Signed): (string, array of byte)
+{
+	(err, e_dat) := asn1->decode(s.tobe_signed); # why?
+	if(err != "")
+		return (err, nil);
+	e_alg := pack_alg(s.alg);
+	e_sig := ref Elem(
+			Tag(Universal, BIT_STRING, 0), 
+			ref Value.BitString(0,s.signature) # DER encode of BIT STRING
+		);
+	all := ref Elem(
+			Tag(Universal, SEQUENCE, 1), 
+			ref Value.Seq(e_dat::e_alg::e_sig::nil)
+		);
+	return asn1->encode(all);
+}
+
+# [public]
+
+Signed.sign(s: self ref Signed, sk: ref PrivateKey, hash: int): (string, array of byte)
+{
+	# we require tobe_signed has 0 bits of padding	
+	if(int s.tobe_signed[0] != 0)
+		return ("syntax error", nil);
+	pick key := sk {
+	RSA =>
+		(err, signature) := pkcs->rsa_sign(s.tobe_signed, key.sk, hash);
+		s.signature = signature;
+		# TODO: add AlgIdentifier based on public key and hash
+		return (err, signature);
+	DSS =>
+		# TODO: hash s.tobe_signed for signing
+		(err, signature) := pkcs->dss_sign(s.tobe_signed, key.sk);
+		s.signature = signature;
+		return (err, signature);
+	DH =>
+		return ("cannot sign using DH algorithm", nil);
+	}
+	return ("sign: failed", nil);
+}
+
+# [public]
+# hash algorithm should be MD2, MD4, MD5 or SHA
+
+Signed.verify(s: self ref Signed, pk: ref PublicKey, hash: int): int
+{
+	ok := 0;
+
+	pick key := pk {
+	RSA =>
+		ok = pkcs->rsa_verify(s.tobe_signed, s.signature, key.pk, hash);
+	DSS =>	
+		# TODO: hash s.tobe_signed for verifying
+		ok = pkcs->dss_verify(s.tobe_signed, s.signature, key.pk);
+	DH =>
+		# simply failure
+	}
+
+	return ok;
+}
+
+# [public]
+
+Signed.tostring(s: self ref Signed): string
+{
+	str := "Signed";
+
+	str += "\nToBeSigned: " + bastr(s.tobe_signed);
+	str += "\nAlgorithm: " + s.alg.tostring();
+	str += "\nSignature: " + bastr(s.signature);
+
+	return str + "\n";
+}
+
+# DER
+# a) the definite form of length encoding shall be used, encoded in the minimum number of 
+#    octets;
+# b) for string types, the constructed form of encoding shall not be used;
+# c) if the value of a type is its default value, it shall be absent;
+# d) the components of a Set type shall be encoded in ascending order of their tag value;
+# e) the components of a Set-of type shall be encoded in ascending order of their octet value;
+# f) if the value of a Boolean type is true, the encoding shall have its contents octet 
+#    set to "FF"16;
+# g) each unused bits in the final octet of the encoding of a Bit String value, if there are 
+#    any, shall be set to zero;
+# h) the encoding of a Real type shall be such that bases 8, 10, and 16 shall not be used, 
+#    and the binary scaling factor shall be zero.
+
+# [private]
+# decode ASN1 one record at a time and return (err, tag, start of data, 
+# end of data) for indefinite length, the end of data is same as 'n'
+
+der_dec1(a: array of byte, i, n: int): (int, Tag, int, int)
+{
+	length: int;
+	tag: Tag;
+	ok := 1;
+	(ok, i, tag) = der_dectag(a, i, n);
+	if(ok) {
+		(ok, i, length) = der_declen(a, i, n);
+		if(ok) {
+			if(length == -1) {
+				if(!tag.constr)
+					ok = 0;
+				length = n - i;
+			}
+			else {
+				if(i+length > n)
+					ok = 0;
+			}
+		}
+	}
+	if(!ok && X509_DEBUG)
+		log("der_dec1: syntax error");
+	return (ok, tag, i, i+length);
+}
+
+# [private]
+# der tag decode
+
+der_dectag(a: array of byte, i, n: int): (int, int, Tag)
+{
+	ok := 1;
+	class, num, constr: int;
+	if(n-i >= 2) {
+		v := int a[i++];
+		class = v & CLASS_MASK;
+		if(v & CONSTR_MASK)
+			constr = 1;
+		else
+			constr = 0;
+		num = v & TAG_MASK;
+		if(num == TAG_MASK)
+			# long tag number
+			(ok, i, num) = uint7_decode(a, i, n);
+	}
+	else
+		ok = 0;
+	if(!ok && X509_DEBUG)
+		log("der_declen: syntax error");
+	return (ok, i, Tag(class, num, constr));
+}
+
+# [private]
+
+int_decode(a: array of byte, i, n, count, unsigned: int): (int, int, int)
+{
+	ok := 1;
+	num := 0;
+	if(n-i >= count) {
+		if((count > 4) || (unsigned && count == 4 && (int a[i] & 16r80)))
+			ok = 1;
+		else {
+			if(!unsigned && count > 0 && count < 4 && (int a[i] & 16r80))
+				num = -1;		# all bits set
+			for(j := 0; j < count; j++) {
+				v := int a[i++];
+				num = (num << 8) | v;
+			}
+		}
+	}
+	else
+		ok = 0;
+	if(!ok && X509_DEBUG)
+		log("int_decode: syntax error");
+	return (ok, i, num);
+}
+
+
+# [private]
+
+uint7_decode(a: array of byte, i, n: int) : (int, int, int)
+{
+	ok := 1;
+	num := 0;
+	more := 1;
+	while(more && i < n) {
+		v := int a[i++];
+		if(num & 16r7F000000) {
+			ok = 0;
+			break;
+		}
+		num <<= 7;
+		more = v & 16r80;
+		num |= (v & 16r7F);
+	}
+	if(n == i)
+		ok = 0;
+	if(!ok && X509_DEBUG)
+		log("uint7_decode: syntax error");
+	return (ok, i, num);
+}
+
+
+# [private]
+# der length decode - the definite form of length encoding shall be used, encoded 
+# in the minimum number of octets
+
+der_declen(a: array of byte, i, n: int): (int, int, int)
+{
+	ok := 1;
+	num := 0;
+	if(i < n) {
+		v := int a[i++];
+		if(v & 16r80)
+			return int_decode(a, i, n, v&16r7F, 1);
+		else if(v == 16r80) # indefinite length
+			ok = 0;
+		else
+			num = v;
+	}
+	else
+		ok = 0;
+	if(!ok && X509_DEBUG)
+		log("der_declen: syntax error");
+	return (ok, i, num);
+}
+
+# [private]
+# parse der encoded algorithm identifier
+
+decode_algid(a: array of byte): (int, ref AlgIdentifier)
+{
+	(err, el) := asn1->decode(a);
+	if(err != "") {
+		if(X509_DEBUG)
+			log("decode_algid: " + err);
+		return (0, nil);
+	}
+	return parse_alg(el);
+}
+
+
+## TBS (Public Key) Certificate is signed by Certificate Authority and contains 
+## information of public key usage (as a comparison of Certificate Revocation List 
+## and Attribute Certificate).
+
+# [public]
+# constructs a certificate by parsing a der encoded certificate
+# returns error if parsing is failed or nil if parsing is ok
+
+certsyn(s: string): (string, ref Certificate)
+{
+	if(0)
+		sys->fprint(sys->fildes(2), "cert: %s\n", s);
+	return ("certificate syntax: "+s, nil);
+}
+
+#	Certificate ::= SEQUENCE {
+#		certificateInfo CertificateInfo,
+#		signatureAlgorithm AlgorithmIdentifier,
+#		signature BIT STRING }
+#
+#	CertificateInfo ::= SEQUENCE {
+#		version [0] INTEGER DEFAULT v1 (0),
+#		serialNumber INTEGER,
+#		signature AlgorithmIdentifier,
+#		issuer Name,
+#		validity Validity,
+#		subject Name,
+#		subjectPublicKeyInfo SubjectPublicKeyInfo }
+#	(version v2 has two more fields, optional unique identifiers for
+#  issuer and subject; since we ignore these anyway, we won't parse them)
+#
+#	Validity ::= SEQUENCE {
+#		notBefore UTCTime,
+#		notAfter UTCTime }
+#
+#	SubjectPublicKeyInfo ::= SEQUENCE {
+#		algorithm AlgorithmIdentifier,
+#		subjectPublicKey BIT STRING }
+#
+#	AlgorithmIdentifier ::= SEQUENCE {
+#		algorithm OBJECT IDENTIFER,
+#		parameters ANY DEFINED BY ALGORITHM OPTIONAL }
+#
+#	Name ::= SEQUENCE OF RelativeDistinguishedName
+#
+#	RelativeDistinguishedName ::= SETOF SIZE(1..MAX) OF AttributeTypeAndValue
+#
+#	AttributeTypeAndValue ::= SEQUENCE {
+#		type OBJECT IDENTIFER,
+#		value DirectoryString }
+#	(selected attributes have these Object Ids:
+#		commonName {2 5 4 3}
+#		countryName {2 5 4 6}
+#		localityName {2 5 4 7}
+#		stateOrProvinceName {2 5 4 8}
+#		organizationName {2 5 4 10}
+#		organizationalUnitName {2 5 4 11}
+#	)
+#
+#	DirectoryString ::= CHOICE {
+#		teletexString TeletexString,
+#		printableString PrintableString,
+#		universalString UniversalString }
+#
+#  See rfc1423, rfc2437 for AlgorithmIdentifier, subjectPublicKeyInfo, signature.
+
+Certificate.decode(a: array of byte): (string, ref Certificate)
+{
+parse:
+	# break on error
+	for(;;) {
+		(err, all) := asn1->decode(a);
+		if(err != "")
+			return certsyn(err);
+		c := ref Certificate;
+
+		# certificate must be a ASN1 sequence
+		(ok, el) := all.is_seq();
+		if(!ok)
+			return certsyn("invalid certificate sequence");
+
+		if(len el == 3){	# ssl3.b uses CertificateInfo; others use Certificate  (TO DO: fix this botch)
+			certinfo := hd el;
+			sigalgid := hd tl el;
+			sigbits := hd tl tl el;
+
+			# certificate info is another ASN1 sequence
+			(ok, el) = certinfo.is_seq();
+			if(!ok || len el < 6)
+				return certsyn("invalid certificate info sequence");
+		}
+
+		c.version = 0; # set to default (v1)
+		(ok, c.version) = parse_version(hd el);
+		if(!ok)
+			return certsyn("can't parse version");
+		if(c.version > 0) {
+			el = tl el;
+			if(len el < 6)
+				break parse;
+		}
+		# serial number
+		(ok, c.serial_number) = parse_sernum(hd el);
+		if(!ok)
+			return certsyn("can't parse serial number");
+		el = tl el;
+		# signature algorithm
+		(ok, c.sig) = parse_alg(hd el);
+		if(!ok)
+			return certsyn("can't parse sigalg");
+		el = tl el;
+		# issuer 
+		(ok, c.issuer) = parse_name(hd el);
+		if(!ok)
+			return certsyn("can't parse issuer");
+		el = tl el;
+		# validity	
+		evalidity := hd el;
+		(ok, c.validity) = parse_validity(evalidity);
+		if(!ok)
+			return certsyn("can't parse validity");
+		el = tl el;
+		# Subject
+		(ok, c.subject) = parse_name(hd el);
+		if(!ok)
+			return certsyn("can't parse subject");
+		el = tl el;
+		# SubjectPublicKeyInfo
+		(ok, c.subject_pkinfo) = parse_pkinfo(hd el);
+		if(!ok)
+			return certsyn("can't parse subject pk info");
+		el = tl el;
+		# OPTIONAL for v2 and v3, must be in order
+		# issuer unique identifier
+		if(c.version == 0 && el != nil)
+			return certsyn("bad unique ID");
+		if(el != nil) {
+			if(c.version < 1) # at least v2
+				return certsyn("invalid v1 cert");
+			(ok, c.issuer_uid) = parse_uid(hd el, 1);
+			if(ok)
+				el = tl el;
+		}
+		# subject unique identifier
+		if(el != nil) {
+			if(c.version < 1) # v2 or v3
+				return certsyn("invalid v1 cert");
+			(ok, c.issuer_uid) = parse_uid(hd el, 2);
+			if(ok)
+				el = tl el;
+		}
+		# extensions
+		if(el != nil) {
+			if(c.version < 2) # must be v3
+				return certsyn("invalid v1/v2 cert");
+			e : ref Elem;
+			(ok, e) = is_context(hd el, 3);
+			if (!ok)
+				break parse;
+			(ok, c.exts) = parse_extlist(e);
+			if(!ok)
+				return certsyn("can't parse extension list");
+			el = tl el;
+		}
+		# must be no more left
+		if(el != nil)
+			return certsyn("unexpected data at end");
+		return ("", c);
+	}
+
+	return ("certificate: syntax error", nil);
+}
+
+# [public]
+# a der encoding of certificate data; returns (error, nil) tuple in failure
+
+Certificate.encode(c: self ref Certificate): (string, array of byte)
+{
+pack:
+	for(;;) {
+		el: list of ref Elem;
+		# always has a version packed
+		e_version := pack_version(c.version);
+		if(e_version == nil)
+			break pack;
+		el = e_version :: el;
+		# serial number
+		e_sernum := pack_sernum(c.serial_number);
+		if(e_sernum == nil)
+			break pack;
+		el = e_sernum :: el;
+		# algorithm
+		e_sig := pack_alg(c.sig);
+		if(e_sig == nil)
+			break pack;
+		el = e_sig :: el;
+		# issuer
+		e_issuer := pack_name(c.issuer);
+		if(e_issuer == nil)
+			break pack;
+		el = e_issuer :: el;
+		# validity
+		e_validity := pack_validity(c.validity);
+		if(e_validity == nil)
+			break pack;
+		el = e_validity :: el;
+		# subject
+		e_subject := pack_name(c.subject);
+		if(e_subject == nil)
+			break pack;
+		el = e_subject :: el;
+		# public key info
+		e_pkinfo := pack_pkinfo(c.subject_pkinfo);
+		if(e_pkinfo == nil)
+			break pack;
+		el = e_pkinfo :: el;
+		# issuer unique identifier
+		if(c.issuer_uid != nil) {
+			e_issuer_uid := pack_uid(c.issuer_uid);
+			if(e_issuer_uid == nil)
+				break pack;
+			el = e_issuer_uid :: el;			
+		}
+		# subject unique identifier
+		if(c.subject_uid != nil) {
+			e_subject_uid := pack_uid(c.subject_uid);
+			if(e_subject_uid == nil)
+				break pack;
+			el = e_subject_uid :: el;
+		}
+		# extensions
+		if(c.exts != nil) {
+			e_exts := pack_extlist(c.exts);
+			if(e_exts == nil)
+				break pack;
+			el = e_exts :: el;
+		}
+		# SEQUENCE order is important
+		lseq: list of ref Elem;
+		while(el != nil) {
+			lseq = (hd el) :: lseq;
+			el = tl el;
+		}		
+		all := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(lseq));
+		return asn1->encode(all);
+	}
+	return ("incompleted certificate; unable to pack", nil);
+}
+
+# [public]
+# converts content of a certificate as visible string
+
+Certificate.tostring(c: self ref Certificate): string
+{
+	s := "\tTBS Certificate";
+	s += "\n\tVersion:\n\t\t" + string c.version;
+	s += "\n\tSerialNumber:\n\t\t" + c.serial_number.iptostr(10);
+	s += "\n\tSignature: " + c.sig.tostring();
+	s += "\n\tIssuer: " + c.issuer.tostring();
+	s += "\n\tValidity: " + c.validity.tostring("local");
+	s += "\n\tSubject: " + c.subject.tostring();
+	s += "\n\tSubjectPKInfo: " + c.subject_pkinfo.tostring();
+	s += "\n\tIssuerUID: " + bastr(c.issuer_uid);
+	s += "\n\tSubjectUID: " + bastr(c.subject_uid);
+	s += "\n\tExtensions: ";
+	exts := c.exts;
+	while(exts != nil) {
+		s += "\t\t" + (hd exts).tostring();
+		exts = tl exts;
+	}
+	return s;
+}
+
+# [public]
+
+Certificate.is_expired(c: self ref Certificate, date: int): int
+{
+	if(date > c.validity.not_after || date < c.validity.not_before)
+		return 1;
+
+	return 0;
+}
+
+
+# [private]
+# version is optional marked by explicit context tag 0; no version is 
+# required if default version (v1) is used
+
+parse_version(e: ref Elem): (int, int)
+{
+	ver := 0;
+	(ans, ec) := is_context(e, 0);
+	if(ans) {
+		ok := 0;
+		(ok, ver) = ec.is_int();
+		if(!ok || ver < 0 || ver > 2)
+			return (0, -1);
+	}
+	return (1, ver);
+}
+
+# [private]
+
+pack_version(v: int): ref Elem
+{
+	return ref Elem(Tag(Universal, INTEGER, 0), ref Value.Int(v));
+}
+
+# [private]
+
+parse_sernum(e: ref Elem): (int, ref IPint)
+{
+	(ok, a) := e.is_bigint();
+	if(ok)
+		return (1, IPint.bebytestoip(a));
+	if(X509_DEBUG)
+		log("parse_sernum: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_sernum(sn: ref IPint): ref Elem
+{
+	return ref Elem(Tag(Universal, INTEGER, 0), ref Value.BigInt(sn.iptobebytes()));
+}
+
+# [private]
+
+parse_alg(e: ref Elem): (int, ref AlgIdentifier)
+{
+parse:
+	for(;;) {	
+		(ok, el) := e.is_seq();
+		if(!ok || el == nil)
+			break parse;
+		oid: ref Oid;
+		(ok, oid) = (hd el).is_oid();
+		if(!ok)
+			break parse;
+		el = tl el;
+		params: array of byte;
+		if(el != nil) {
+			# TODO: determine the object type based on oid
+			# 	then parse params
+			#unused: int;
+			#(ok, unused, params) = (hd el).is_bitstring();
+			#if(!ok || unused || tl el != nil)
+			#	break parse;
+		}
+		return (1, ref AlgIdentifier(oid, params));
+	}
+	if(X509_DEBUG)
+		log("parse_alg: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_alg(a: ref AlgIdentifier): ref Elem
+{
+	if(a.oid != nil) {
+		el: list of ref Elem;
+		el = ref Elem(Tag(Universal, ASN1->OBJECT_ID, 0), ref Value.ObjId(a.oid)) :: nil;
+		if(a.parameter != nil)  {
+			el = ref Elem(
+				Tag(Universal, BIT_STRING, 0), 
+				ref Value.BitString(0, a.parameter)
+			) :: el;
+		}
+		return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	}
+	return nil;
+}
+
+# [private]
+
+parse_name(e: ref Elem): (int, ref Name)
+{
+parse:
+	for(;;) {
+		(ok, el) := e.is_seq();
+		if(!ok)
+			break parse;
+		lrd: list of ref RDName;
+		while(el != nil) {
+			rd: ref RDName;
+			(ok, rd) = parse_rdname(hd el);
+			if(!ok)
+				break parse;
+			lrd = rd :: lrd;
+			el = tl el;
+		}
+		# SEQUENCE
+		l: list of ref RDName;
+		while(lrd != nil) {
+			l = (hd lrd) :: l;
+			lrd = tl lrd;
+		}
+		return (1, ref Name(l));
+	}
+	if(X509_DEBUG)
+		log("parse_name: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_name(n: ref Name): ref Elem
+{
+	el: list of ref Elem;
+
+	lrd := n.rd_names;
+	while(lrd != nil) {
+		rd := pack_rdname(hd lrd);
+		if(rd == nil)
+			return nil;
+		el = rd :: el;
+		lrd = tl lrd;
+	}
+	# reverse order
+	l: list of ref Elem;
+	while(el != nil) {
+		l = (hd el) :: l;
+		el = tl el;
+	}
+
+	return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(l));
+}
+
+# [private]
+
+parse_rdname(e: ref Elem): (int, ref RDName)
+{
+parse:
+	for(;;) {
+		(ok, el) := e.is_set(); # unordered
+		if(!ok)
+			break parse;
+		lava: list of ref AVA;
+		while(el != nil) {
+			ava: ref AVA;
+			(ok, ava) = parse_ava(hd el);
+			if(!ok)
+				break parse;
+			lava = ava :: lava;
+			el = tl el;
+		}
+		return (1, ref RDName(lava));
+	}
+	if(X509_DEBUG)
+		log("parse_rdname: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_rdname(r: ref RDName): ref Elem
+{
+	el: list of ref Elem;
+	lava := r.avas;
+	while(lava != nil) {
+		ava := pack_ava(hd lava);
+		if(ava == nil)
+			return nil;
+		el = ava :: el;
+		lava = tl lava;
+	}
+	return ref Elem(Tag(Universal, ASN1->SET, 1), ref Value.Set(el));
+}
+
+# [private]
+
+parse_ava(e: ref Elem): (int, ref AVA)
+{
+parse:
+	for(;;) {
+		(ok, el) := e.is_seq();
+		if(!ok || len el != 2)
+			break parse;
+		a := ref AVA;
+		(ok, a.oid) = (hd el).is_oid();
+		if(!ok)
+			break parse;
+		el = tl el;
+		(ok, a.value) = (hd el).is_string();
+		if(!ok)
+			break parse;
+		return (1, a);
+	}
+	if(X509_DEBUG)
+		log("parse_ava: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_ava(a: ref AVA): ref Elem
+{
+	el: list of ref Elem;
+	if(a.oid == nil || a.value == "")
+		return nil;
+	# Note: order is important
+	el = ref Elem(Tag(Universal, ASN1->GeneralString, 0), ref Value.String(a.value)) :: el;
+	el = ref Elem(Tag(Universal, ASN1->OBJECT_ID, 0), ref Value.ObjId(a.oid)) :: el;	
+	return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+}
+
+# [private]
+
+parse_validity(e: ref Elem): (int, ref Validity)
+{
+parse:
+	for(;;) {
+		(ok, el) := e.is_seq();
+		if(!ok || len el != 2)
+			break parse;
+		v := ref Validity;
+		(ok, v.not_before) = parse_time(hd el, UTCTime);
+		if(!ok)
+			break parse;
+		el = tl el;
+		(ok, v.not_after) = parse_time(hd el, UTCTime);
+		if(!ok)
+			break parse;
+		return (1, v);
+	}
+	if(X509_DEBUG)
+		log("parse_validity: syntax error");
+	return (0, nil);
+}
+
+# [private]
+# standard says only UTC Time allowed for TBS Certificate, but there is exception of
+# GeneralizedTime for CRL and Attribute Certificate. Parsing is based on format of
+# UTCTime, GeneralizedTime or undetermined (any int not UTCTime or GeneralizedTime).
+
+parse_time(e: ref Elem, format: int): (int, int)
+{
+parse:
+	for(;;) {
+		(ok, date) := e.is_time();
+		if(!ok)
+			break parse;
+		if(e.tag.num != UTCTime && e.tag.num != GeneralizedTime)
+			break parse;
+		if(format == UTCTime && e.tag.num != UTCTime)
+			break parse;
+		if(format == GeneralizedTime && e.tag.num != GeneralizedTime)
+			break parse; 
+		t := decode_time(date, e.tag.num);
+		if(t < 0)
+			break parse;
+		return (1, t);
+	}
+	if(X509_DEBUG)
+		log("parse_time: syntax error");
+	return (0, -1);
+}
+
+# [private]
+# decode a BER encoded UTC or Generalized time into epoch (seconds since 1/1/1970 GMT)
+# UTC time format: YYMMDDhhmm[ss](Z|(+|-)hhmm)
+# Generalized time format: YYYYMMDDhhmm[ss.s(...)](Z|(+|-)hhmm[ss.s(...))
+
+decode_time(date: string, format: int): int
+{
+	time := ref Daytime->Tm;
+parse:
+	for(;;) {
+    		i := 0;
+		if(format == UTCTime) {
+			if(len date < 11)
+				break parse;
+			time.year = get2(date, i);
+	   		if(time.year < 0)
+        			break parse;    
+			if(time.year < 70)
+        			time.year += 100;
+			i += 2;
+		}
+		else {
+			if(len date < 13)
+				break parse;
+			time.year = get2(date, i);
+			if(time.year-19 < 0)
+				break parse;
+			time.year = (time.year - 19)*100;
+			i += 2;
+			time.year += get2(date, i);
+			i += 2;
+		}
+		time.mon = get2(date, i) - 1;
+		if(time.mon < 0 || time.mon > 11)
+			break parse;
+		i += 2;
+		time.mday = get2(date, i);
+		if(time.mday < 1 || time.mday > 31)
+			break parse;
+		i += 2;
+		time.hour = get2(date, i);
+		if(time.hour < 0 || time.hour > 23)
+			break parse;
+		i += 2;
+		time.min = get2(date, i);
+		if(time.min < 0 || time.min > 59)
+			break parse;
+		i += 2;
+		if(int date[i] >= '0' && int date[i] <= '9') {
+			if(len date < i+3)
+            			break parse;
+			time.sec = get2(date, i);
+			if(time.sec < 0 || time.sec > 59)
+				break parse;
+			i += 2;
+			if(format == GeneralizedTime) {
+				if((len date < i+3) || int date[i++] != '.')
+					break parse;
+				# ignore rest
+				ig := int date[i];
+				while(ig >= '0' && ig <= '9' && i++ < len date) {
+					ig = int date[i];
+				}
+			}
+		}
+		else {
+			time.sec = 0;
+		}    
+		zf := int date[i];
+		if(zf != 'Z' && zf != '+' && zf != '-')
+			break parse;
+		if(zf == 'Z') {
+			if(len date != i+1)
+				break parse;
+			time.tzoff = 0;
+		}
+		else {   
+			if(len date < i + 3)
+				break parse;
+			time.tzoff = get2(date, i+1);
+			if(time.tzoff < 0 || time.tzoff > 23)
+				break parse;
+			i += 2;
+			min := get2(date, i);
+			if(min < 0 || min > 59)
+				break parse;
+			i += 2;
+			sec := 0;
+			if(i != len date) {
+				if(format == UTCTime || len date < i+4)
+					break parse;
+				sec = get2(date, i);
+				i += 2;
+				# ignore the rest
+			}
+			time.tzoff = (time.tzoff*60 + min)*60 + sec;
+			if(zf == '-')
+				time.tzoff = -time.tzoff;
+		}
+		return daytime->tm2epoch(time);    
+	}
+	if(X509_DEBUG)
+		log("decode_time: syntax error: " +
+		sys->sprint("year=%d mon=%d mday=%d hour=%d min=%d, sec=%d", 
+		time.year, time.mon, time.mday, time.hour, time.min, time.sec));
+	return -1;
+}
+
+# [private]
+# pack as UTC time
+
+pack_validity(v: ref Validity): ref Elem
+{
+	el: list of ref Elem;
+	el = ref Elem(
+			Tag(Universal, UTCTime, 0), 
+			ref Value.String(pack_time(v.not_before, UTCTime))
+		) :: nil;
+	el = ref Elem(
+			Tag(Universal, UTCTime, 0), 
+			ref Value.String(pack_time(v.not_after, UTCTime))
+		) :: el;
+	return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+}
+
+# [private]
+# Format must be either UTCTime or GeneralizedTime
+# TODO: convert to coordinate time
+
+pack_time(t: int, format: int): string
+{
+	date := array [32] of byte;
+	tm := daytime->gmt(t);
+
+	i := 0;
+	if(format == UTCTime) {
+		i = put2(date, tm.year, i);
+	}
+	else { # GeneralizedTime
+		i = put2(date, 19 + tm.year/100, i);
+		i = put2(date, tm.year%100, i);
+	}
+	i = put2(date, tm.mon, i);
+	i = put2(date, tm.mday, i);
+	i = put2(date, tm.hour, i);
+	i = put2(date, tm.min, i);
+	if(tm.sec != 0) {
+		if(format == UTCTime)
+			i = put2(date, tm.sec, i);
+		else {
+			i = put2(date, tm.sec, i);
+			date[i++] = byte '.';	
+			date[i++] = byte 0;
+		}
+	}
+	if(tm.tzoff == 0) {
+		date[i++] = byte 'Z';
+	}
+	else {
+		off := tm.tzoff;
+		if(tm.tzoff < 0) {
+			off = -off;
+			date[i++] = byte '-';
+		}
+		else {
+			date[i++] = byte '+';
+		}
+		hoff := int (off/3600);
+		moff := int ((off%3600)/60);
+		soff := int ((off%3600)%60);
+		i = put2(date, hoff, i);
+		i = put2(date, moff, i);
+		if(soff) {
+			if(format == UTCTime)
+				i = put2(date, soff, i);
+			else {
+				i = put2(date, soff, i);
+				date[i++] = byte '.';	
+				date[i++] = byte 0;
+			}
+		}
+	}
+	return string date[0:i];
+}
+
+# [private]
+
+parse_pkinfo(e: ref Elem): (int, ref SubjectPKInfo)
+{
+parse:
+	for(;;) {
+		p := ref SubjectPKInfo;
+		(ok, el) := e.is_seq();
+		if(!ok || len el != 2)
+			break parse;
+		(ok, p.alg_id) = parse_alg(hd el);
+		if(!ok)
+			break parse;
+		unused: int;
+		(ok, unused, p.subject_pk) = (hd tl el).is_bitstring();
+		if(!ok || unused != 0)
+			break parse;
+		return (1, p);
+	}
+	if(X509_DEBUG)
+		log("parse_pkinfo: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_pkinfo(p: ref SubjectPKInfo): ref Elem
+{
+	el: list of ref Elem;
+	# SEQUENCE order is important
+	el = ref Elem(
+			Tag(Universal, BIT_STRING, 0), 
+			ref Value.BitString(0, p.subject_pk) # 0 bits unused ?
+		) :: nil;
+	el = pack_alg(p.alg_id) :: el;
+	return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+}
+
+# [private]
+
+parse_uid(e: ref Elem, num: int): (int, array of byte)
+{
+	ok, unused : int;
+	uid : array of byte;
+	e2 : ref Elem;
+parse:
+	for(;;) {
+		(ok, e2) = is_context(e, num);
+		if (!ok)
+			break parse;
+		e = e2;
+
+		(ok, unused, uid) = e.is_bitstring();
+#		if(!ok || unused != 0)
+		if(!ok)
+			break parse;
+		return (1, uid);
+	}
+	if(X509_DEBUG)
+		log("parse_uid: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_uid(u: array of byte): ref Elem
+{
+	return ref Elem(Tag(Universal, ASN1->BIT_STRING, 0), ref Value.BitString(0,u));
+}
+
+# [private]
+
+parse_extlist(e: ref Elem): (int, list of ref Extension)
+{
+parse:
+	# dummy loop for breaking out of
+	for(;;) {
+		l: list of ref Extension;
+		(ok, el) := e.is_seq();
+		if(!ok)
+			break parse;
+		while(el != nil) {
+			ext := ref Extension;
+			(ok, ext) = parse_extension(hd el);
+			if(!ok)
+				break parse;
+			l = ext :: l;
+			el = tl el;
+		}
+		# sort to order
+		nl: list of ref Extension;
+		while(l != nil) {
+			nl = (hd l) :: nl;
+			l = tl l;
+		}
+		return (1, nl);
+	}
+	if(X509_DEBUG)
+		log("parse_extlist: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_extlist(e: list of ref Extension): ref Elem
+{
+	el: list of ref Elem;
+	exts := e;
+	while(exts != nil) {
+		ext := pack_extension(hd exts);
+		if(ext == nil)
+			return nil;
+		el = ext :: el;
+		exts = tl exts;
+	}
+	# reverse order
+	l: list of ref Elem;
+	while(el != nil) {
+		l = (hd el) :: l;
+		el = tl el;
+	}
+	return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(l));
+}
+
+# [private]
+# Require further parse to check oid if critical is set to TRUE (see parse_exts)
+
+parse_extension(e: ref Elem): (int, ref Extension)
+{
+parse:
+	for(;;) {
+		ext := ref Extension;
+		(ok, el) := e.is_seq();
+		if(!ok)
+			break parse;
+		oid: ref Oid;
+		(ok, oid) = (hd el).is_oid(); 
+		if(!ok)
+			break parse;
+		ext.oid = oid; 
+		el = tl el;
+		# BOOLEAN DEFAULT FALSE
+		(ok, ext.critical) = (hd el).is_int();
+		if(ok)
+			el = tl el;
+		else
+			ext.critical = 0;
+		if (len el != 1) {
+			break parse;
+		}
+		(ok, ext.value) = (hd el).is_octetstring();
+		if(!ok)
+			break parse;
+		return (1, ext);
+	}
+	if(X509_DEBUG)
+		log("parse_extension: syntax error");
+	return (0, nil);
+}
+
+# [private]
+
+pack_extension(e: ref Extension): ref Elem
+{
+	el: list of ref Elem;
+
+	if(e.oid == nil || (e.critical !=0 && e.critical != 1) || e.value == nil)
+		return nil;
+	# SEQUENCE order
+	el = ref Elem(Tag(Universal, OCTET_STRING, 0), ref Value.Octets(e.value)) :: el;
+	el = ref Elem(Tag(Universal, BOOLEAN, 0), ref Value.Bool(e.critical)) :: el;
+	el = ref Elem(Tag(Universal, OBJECT_ID, 0), ref Value.ObjId(e.oid)) :: el;
+	return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+}
+
+# [public]
+
+AlgIdentifier.tostring(a: self ref AlgIdentifier): string
+{
+	return "\n\t\toid: " + a.oid.tostring() + "\n\t\twith parameter: "+ bastr(a.parameter);
+}
+
+# [public]
+
+Name.equal(a: self ref Name, b: ref Name): int
+{
+	rda := a.rd_names;
+	rdb := b.rd_names;
+	if(len rda != len rdb)
+		return 0;
+	while(rda != nil && rdb != nil) {
+		ok := (hd rda).equal(hd rdb);
+		if(!ok)
+			return 0;
+		rda = tl rda;
+		rdb = tl rdb;
+	}
+
+	return 1;
+}
+
+# [public]
+# The sequence of RelativeDistinguishedName's gives a sort of pathname, from most general to 
+# most specific.  Each element of the path can be one or more (but usually just one) 
+# attribute-value pair, such as countryName="US". We'll just form a "postal-style" address 
+# string by concatenating the elements from most specific to least specific, separated by commas.
+
+Name.tostring(a: self ref Name): string
+{
+	path: string;
+	rdn := a.rd_names;
+	while(rdn != nil) {
+		path += (hd rdn).tostring();
+		rdn = tl rdn;
+		if(rdn != nil)
+			path += ",";
+	}
+	return path;
+}
+
+# [public]
+# The allocation of distinguished names is the responsibility of the Naming Authorities. 
+# Each user shall therefore trust the Naming Authorities not to issue duplicate distinguished
+# names. The comparison shall be unique one to one match but may not in the same order.
+
+RDName.equal(a: self ref RDName, b: ref RDName): int
+{
+	if(len a.avas != len b.avas)
+		return 0;
+	aa := a.avas;
+	ba := b.avas;
+	while(aa != nil) {
+		found:= 0;
+		rest: list of ref AVA;
+		while(ba != nil) {
+			ok := (hd ba).equal(hd ba);
+			if(!ok)
+				rest = (hd aa) :: rest;
+			else {
+				if(found)
+					return 0;
+				found = 1;
+			}
+			ba = tl ba;
+		}
+		if(found == 0)
+			return 0;
+		ba = rest;
+		aa = tl aa;
+	}
+	return 1;
+}
+
+# [public]
+
+RDName.tostring(a: self ref RDName): string
+{
+	s: string;
+	avas := a.avas;
+	while(avas != nil) {
+		s += (hd avas).tostring();
+		avas = tl avas;
+		if(avas != nil)
+			s += "-";
+	}
+	return s;
+}
+
+# [public]
+# AVA are equal if they have the same type oid and value
+
+AVA.equal(a: self ref AVA, b: ref AVA): int
+{
+	# TODO: need to match different encoding (T61String vs. IA5String)
+	if(a.value != b.value)
+		return 0;
+
+	return oid_cmp(a.oid, b.oid);
+}
+
+# [public]
+
+AVA.tostring(a: self ref AVA): string
+{
+	return a.value;
+}
+
+# [public]
+
+Validity.tostring(v: self ref Validity, format: string): string
+{
+	s: string;
+	if(format == "local") {
+		s = "\n\t\tnot_before[local]: ";
+	 	s += daytime->text(daytime->local(v.not_before));
+		s += "\n\t\tnot_after[local]: ";
+		s += daytime->text(daytime->local(v.not_after));
+	}
+	else if(format == "gmt") {
+		s = "\n\t\tnot_before[gmt]: ";
+	 	s += daytime->text(daytime->gmt(v.not_before));
+		s += "\n\t\tnot_after[gmt]: ";
+		s += daytime->text(daytime->gmt(v.not_after));
+	}
+	else
+		s += "unknown format: " + format;
+	return s;	
+}
+
+# [public]
+
+SubjectPKInfo.getPublicKey(pkinfo: self ref SubjectPKInfo): (string, int, ref PublicKey)
+{
+parse:
+	for(;;) {
+		pk: ref PublicKey;
+		id := asn1->oid_lookup(pkinfo.alg_id.oid, pkcs->objIdTab);
+		case id {
+		PKCS->id_pkcs_rsaEncryption or
+		PKCS->id_pkcs_md2WithRSAEncryption or
+		PKCS->id_pkcs_md4WithRSAEncryption or
+		PKCS->id_pkcs_md5WithRSAEncryption =>
+			(err, k) := pkcs->decode_rsapubkey(pkinfo.subject_pk);
+			if(err != nil)
+				break parse;
+			pk = ref PublicKey.RSA(k);
+		PKCS->id_algorithm_shaWithDSS =>
+			(err, k) :=  pkcs->decode_dsspubkey(pkinfo.subject_pk);
+			if(err != nil)
+				break parse;
+			pk = ref PublicKey.DSS(k);
+		PKCS->id_pkcs_dhKeyAgreement =>
+			(err, k) := pkcs->decode_dhpubkey(pkinfo.subject_pk);
+			if(err != nil)
+				break parse;
+			pk = ref PublicKey.DH(k);
+		* =>
+			break parse;
+		}
+		return ("", id, pk);
+	}
+	return ("subject public key: syntax error", -1, nil);
+}
+
+# [public]
+
+SubjectPKInfo.tostring(pkinfo: self ref SubjectPKInfo): string
+{
+	s := pkinfo.alg_id.tostring();
+	s += "\n\t\tencoded key: " + bastr(pkinfo.subject_pk);
+	return s;
+}
+
+# [public]
+
+Extension.tostring(e: self ref Extension): string
+{
+	s := "oid: " + e.oid.tostring();
+	s += "critical: ";
+	if(e.critical)
+		s += "true ";
+	else
+		s += "false ";
+	s += bastr(e.value);
+	return s;
+}
+
+## Certificate PATH
+## A list of certificates needed to allow a particular user to obtain
+## the public key of another, is known as a certificate path. A
+## certificate path logically forms an unbroken chain of trusted
+## points in the DIT between two users wishing to authenticate.
+## To establish a certification path between user A and user B using
+## the Directory without any prior information, each CA may store
+## one certificate and one reverse certificate designated as
+## corresponding to its superior CA.
+
+# The ASN.1 data byte definitions for certificates and a certificate 
+# path is
+#
+# Certificates	::= SEQUENCE {
+#	userCertificate		Certificate,
+#	certificationPath	ForwardCertificationPath OPTIONAL }
+#
+# ForwardCertificationPath ::= SEQUENCE OF CrossCertificates
+# CrossCertificates ::=	SET OF Certificate
+# 
+
+# [public]
+# Verify a decoded certificate chain in order of root to user. This is useful for 
+# non_ASN.1 encoding of certificates, e.g. in SSL. Return (0, error string) if 
+# verification failure or (1, "") if verification ok
+
+verify_certchain(cs: list of array of byte): (int, string)
+{
+	lsc: list of (ref Signed, ref Certificate);
+
+	l := cs;
+	while(l != nil) {
+		(err, s) := Signed.decode(hd l); 
+		if(err != "") 
+			return (0, err);
+		c: ref Certificate;
+		(err, c) = Certificate.decode(s.tobe_signed);
+		if(err != "")
+			return (0, err);		
+		lsc = (s, c) :: lsc;
+		l = tl l;
+	}
+	# reverse order
+	a: list of (ref Signed, ref Certificate);
+	while(lsc != nil) {
+		a = (hd lsc) :: a;
+		lsc = tl lsc;
+	}
+	return verify_certpath(a);
+}
+
+# [private]
+# along certificate path; first certificate is root
+
+verify_certpath(sc: list of (ref Signed, ref Certificate)): (int, string)
+{
+	# verify self-signed root certificate
+	(s, c) := hd sc;
+	# TODO: check root RDName with known CAs and using
+	# external verification of root - Directory service
+	(err, id, pk) := c.subject_pkinfo.getPublicKey();
+	if(err != "")
+		return (0, err);
+	if(!is_validtime(c.validity)
+		|| !c.issuer.equal(c.subject)
+		|| !s.verify(pk, 0)) # TODO: prototype verify(key, ref AlgIdentifier)?
+		return (0, "verification failure");
+
+	sc = tl sc;
+	while(sc != nil) {
+		(ns, nc) := hd sc;
+		# TODO: check critical flags of extension list
+		# check alt names field
+		(err, id, pk) = c.subject_pkinfo.getPublicKey();
+		if(err != "")
+			return (0, err);
+		if(!is_validtime(nc.validity)
+			|| !nc.issuer.equal(c.subject) 
+			|| !ns.verify(pk, 0)) # TODO: move prototype as ?
+			return (0, "verification failure");
+		(s, c) = (ns, nc);
+		sc = tl sc;
+	}
+
+	return (1, "");
+}
+
+# [public]
+is_validtime(validity: ref Validity): int
+{
+	# a little more expensive but more accurate
+	now := daytime->now();
+
+	# need some conversion here
+	if(now < validity.not_before || now > validity.not_after)
+		return 0;
+
+	return 1;	
+} 
+
+is_validpair(): int
+{
+	return 0;
+}
+
+## Certificate Revocation List (CRL)
+##
+## A CRL is a time-stampted list identifying revoked certificates. It is signed by a 
+## Certificate Authority (CA) and made freely available in a public repository.
+##
+## Each revoked certificate is identified in a CRL by its certificate serial number. 
+## When a certificate-using system uses a certificate (e.g., for verifying a remote 
+## user's digital signature), that system not only checks the certificate signature 
+## and validity but also acquires a suitably-recent CRL and checks that the certificate 
+## serial number is not on that CRL. The meaning of "suitably-recent" may vary with
+## local policy, but it usually means the most recently-issued CRL. A CA issues a new 
+## CRL on a regular periodic basis (e.g., hourly, daily, or weekly). Entries are added 
+## on CRLs as revocations occur, and an entry may be removed when the certificate 
+## expiration date is reached.
+
+# [public]
+
+CRL.decode(a: array of byte): (string, ref CRL)
+{
+parse:
+	# break on error
+	for(;;) {
+		(err, all) := asn1->decode(a);
+		if(err != "")
+			break parse;
+		c := ref CRL;
+		# CRL must be a ASN1 sequence
+		(ok, el) := all.is_seq();
+		if(!ok || len el < 3)
+			break parse;
+		c.version = 1; # set to default (v2)
+		(ok, c.version) = parse_version(hd el);
+		if(!ok)
+			break parse;
+		if(c.version < 0) {
+			el = tl el;
+			if(len el < 4)
+				break parse;
+		}
+		# signature algorithm
+		(ok, c.sig) = parse_alg(hd el);
+		if(!ok)
+			break parse;
+		el = tl el;
+		# issuer: who issues the CRL
+		(ok, c.issuer) = parse_name(hd el);
+		if(!ok)
+			break parse;
+		el = tl el;
+		# this update
+		(ok, c.this_update) = parse_time(hd el, UTCTime);
+		if(!ok)
+			break parse;
+		el = tl el;
+		# OPTIONAL, must be in order
+		# next_update
+		if(el != nil) {
+			(ok, c.next_update) = parse_time(hd el, UTCTime);
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		# revoked certificates
+		if(el != nil) {
+			(ok, c.revoked_certs) = parse_revoked_certs(hd el);
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		# extensions
+		if(el != nil) {
+			(ok, c.exts) = parse_extlist(hd el);	
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		# must be no more left
+		if(el != nil)
+			break parse;
+		return ("", c);
+	}
+	return ("CRL: syntax error", nil);
+}
+
+# [public]
+
+CRL.encode(c: self ref CRL): (string, array of byte)
+{
+pack:
+	for(;;) {
+		el: list of ref Elem;
+		# always has a version packed
+		e_version := pack_version(c.version);
+		if(e_version == nil)
+			break pack;
+		el = e_version :: el;
+		# algorithm
+		e_sig := pack_alg(c.sig);
+		if(e_sig == nil)
+			break pack;
+		el = e_sig :: el;
+		# crl issuer
+		e_issuer := pack_name(c.issuer);
+		if(e_issuer == nil)
+			break pack;
+		el = e_issuer :: el;
+		# validity
+		e_this_update := pack_time(c.this_update, UTCTime);
+		if(e_this_update == nil)
+			break pack;
+		el = ref Elem(
+			Tag(Universal, ASN1->UTCTime, 0), 
+			ref Value.String(e_this_update)
+			) :: el;
+		# next crl update
+		if(c.next_update != 0) {
+			e_next_update := pack_time(c.next_update, UTCTime);
+			if(e_next_update == nil)
+				break pack;
+			el = ref Elem(
+				Tag(Universal, ASN1->UTCTime, 0),
+				ref Value.String(e_next_update)
+				) :: el;
+		}
+		# revoked certificates
+		if(c.revoked_certs != nil) {
+			e_revoked_certs := pack_revoked_certs(c.revoked_certs);
+			if(e_revoked_certs == nil)
+				break pack;
+			el = e_revoked_certs :: el;
+		}
+		# crl extensions
+		if(c.exts != nil) {
+			e_exts := pack_extlist(c.exts);
+			if(e_exts == nil)
+				break pack;
+			el = e_exts :: el;
+		}
+		# compose all elements
+		lseq: list of ref Elem;
+		while(el != nil) {
+			lseq = (hd el) :: lseq;
+			el = tl el;
+		}
+		all := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(lseq));
+		(err, ret) := asn1->encode(all);
+		if(err != "")
+			break;
+		return ("", ret);
+	}
+	return ("incompleted CRL; unable to pack", nil);
+}
+
+# [public]
+
+CRL.tostring(c: self ref CRL): string
+{
+	s := "Certificate Revocation List (CRL)";
+	s += "\nVersion: " + string c.version;
+	s += "\nSignature: " + c.sig.tostring();
+	s += "\nIssuer: " + c.issuer.tostring();
+	s += "\nThis Update: " + daytime->text(daytime->local(c.this_update));
+	s += "\nNext Update: " + daytime->text(daytime->local(c.next_update));
+	s += "\nRevoked Certificates: ";
+	rcs := c.revoked_certs;
+	while(rcs != nil) {
+		s += "\t" + (hd rcs).tostring();
+		rcs = tl rcs;
+	}
+	s += "\nExtensions: ";
+	exts := c.exts;
+	while(exts != nil) {
+		s += "\t" + (hd exts).tostring();
+		exts = tl exts;
+	}
+	return s;
+}
+
+# [public]
+
+CRL.is_revoked(c: self ref CRL, sn: ref IPint): int
+{
+	es := c.revoked_certs;
+	while(es != nil) {
+		if(sn.eq((hd es).user_cert))
+			return 1;
+		es = tl es;
+	}
+	return 0;
+}
+
+# [public]
+
+RevokedCert.tostring(rc: self ref RevokedCert): string
+{
+	s := "Revoked Certificate";
+	if(rc.user_cert == nil)
+		return s + " [Bad Format]\n";
+	s += "\nSerial Number: " + rc.user_cert.iptostr(10);
+	if(rc.revoc_date != 0)
+		s += "\nRevocation Date: " + daytime->text(daytime->local(rc.revoc_date));
+	if(rc.exts != nil) {
+		exts := rc.exts;
+		while(exts != nil) {
+			s += "\t" + (hd exts).tostring();
+			exts = tl exts;
+		}
+	}
+	return s;		
+}
+
+
+# [private]
+
+parse_revoked_certs(e: ref Elem): (int, list of ref RevokedCert)
+{
+	lc: list of ref RevokedCert;
+parse:
+	for(;;) {
+		(ok, el) := e.is_seq();
+		if(!ok)
+			break parse;
+		while(el != nil) {
+			c: ref RevokedCert;
+			(ok, c) = parse_revoked(hd el);
+			if(!ok)
+				break parse;
+			lc = c :: lc;	
+			el = tl el;
+		}
+
+		return (1, lc);
+	}
+	
+	return (0, nil);
+}
+
+# [private]
+
+pack_revoked_certs(r: list of ref RevokedCert): ref Elem
+{
+	el: list of ref Elem;
+
+	rs := r;
+	while(rs != nil) {
+		rc := pack_revoked(hd rs);
+		if(rc == nil)
+			return nil;
+		el = rc :: el;
+		rs = tl rs;
+	}
+	# reverse order
+	l: list of ref Elem;
+	while(el != nil) {
+		l = (hd el) :: l;
+		el = tl el;
+	}
+	return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(l));
+	
+}
+
+# [private]
+
+parse_revoked(e: ref Elem): (int, ref RevokedCert)
+{
+parse:
+	for(;;) {
+		c: ref RevokedCert;
+		(ok, el) := e.is_seq();
+		if(!ok || len el < 2)
+			break parse;
+		uc: array of byte;
+		(ok, uc) = (hd el).is_bigint();
+		if(!ok)
+			break parse;
+		c.user_cert = IPint.bebytestoip(uc);
+		el = tl el;
+		(ok, c.revoc_date) = parse_time(hd el, UTCTime);
+		if(!ok)
+			break parse;
+		el = tl el;
+		if(el != nil) {
+			(ok, c.exts) = parse_extlist(hd el);
+			if(!ok)
+				break parse;
+		}
+		return (1, c);
+	}
+	return (0, nil);
+}
+
+# [private]
+
+pack_revoked(r: ref RevokedCert): ref Elem
+{
+	el: list of ref Elem;
+	if(r.exts != nil) {
+		e_exts := pack_extlist(r.exts);
+		if(e_exts == nil)
+			return nil;		
+		el = e_exts :: el;
+	}
+	if(r.revoc_date != 0) {
+		e_date := pack_time(r.revoc_date, UTCTime);
+		if(e_date == nil)
+			return nil;
+		el = ref Elem(
+				Tag(Universal, ASN1->UTCTime, 0),
+				ref Value.String(e_date)
+			) :: el;
+	}
+	if(r.user_cert == nil)
+		return nil;
+	el = ref Elem(Tag(Universal, INTEGER, 0), 
+			ref Value.BigInt(r.user_cert.iptobebytes())
+		) :: el;
+	return ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+}
+
+## The extensions field allows addition of new fields to the structure 
+## without modification to the ASN.1 definition. An extension field 
+## consists of an extension identifier, a criticality flag, and a 
+## canonical encoding of a data value of an ASN.1 type associated with 
+## the identified extension. For those extensions where ordering of 
+## individual extensions within the SEQUENCE is significant, the  
+## specification of those individual extensions shall include the rules 
+## for the significance of the ordering. When an implementation 
+## processing a certificate does not recognize an extension, if the 
+## criticality flag is FALSE, it may ignore that extension. If the 
+## criticality flag is TRUE, unrecognized extensions shall cause the 
+## structure to be considered invalid, i.e. in a certificate, an 
+## unrecognized critical extension would cause validation of a signature 
+## using that certificate to fail.
+
+# [public]
+
+cr_exts(es: list of ref Extension): list of ref Extension
+{
+	cr: list of ref Extension;
+	l := es;
+	while(l != nil) {
+		e := hd l;
+		if(e.critical == 1)
+			cr = e :: cr;
+		l = tl l;		
+	}
+	return cr;
+}
+
+# [public]
+
+noncr_exts(es: list of ref Extension): list of ref Extension
+{
+	ncr: list of ref Extension;
+	l := es;
+	while(l != nil) {
+		e := hd l;
+		if(e.critical == 0)
+			ncr = e :: ncr;
+		l = tl l;		
+	}
+	return ncr;
+}
+
+# [public]
+
+parse_exts(exts: list of ref Extension): (string, list of ref ExtClass)
+{
+	ets: list of ref ExtClass;
+	l := exts;
+	while(l != nil) {
+		ext := hd l;
+		(err, et) := ExtClass.decode(ext);
+		if(err != "")
+			return (err, nil);
+		ets = et :: ets;
+		l = tl l;
+	}
+	lseq: list of ref ExtClass;
+	while(ets != nil) {
+		lseq = (hd ets) :: lseq;
+		ets = tl ets;
+	}
+	return ("", lseq);
+}
+
+# [public]
+
+ExtClass.decode(ext: ref Extension): (string, ref ExtClass)
+{
+	err: string;
+	eclass: ref ExtClass;
+
+	oid := asn1->oid_lookup(ext.oid, objIdTab);
+	case oid {
+	id_ce_authorityKeyIdentifier =>
+		(err, eclass) = decode_authorityKeyIdentifier(ext);
+		if(err == "" && ext.critical == 1) {
+			err = "authority key identifier: should be non-critical";
+			break;
+		}
+	id_ce_subjectKeyIdentifier =>
+		(err, eclass) = decode_subjectKeyIdentifier(ext);
+		if(err != "" && ext.critical != 0) {
+			err = "subject key identifier: should be non-critical";
+			break;
+		}
+	id_ce_basicConstraints =>
+		(err, eclass) = decode_basicConstraints(ext);
+		if(err == "" && ext.critical != 1) {
+			err = "basic constraints: should be critical";
+			break;
+		}
+	id_ce_keyUsage =>
+		(err, eclass) = decode_keyUsage(ext);
+		if(err == "" && ext.critical != 1) {
+			err = "key usage: should be critical";
+			break;
+		}
+	id_ce_privateKeyUsage =>
+		(err, eclass) = decode_privateKeyUsage(ext);
+		if(err == "" && ext.critical != 0) {
+			err = "private key usage: should be non-critical";
+			break;
+		}
+	id_ce_policyMapping =>
+		(err, eclass) = decode_policyMapping(ext);
+		if(err == "" && ext.critical != 0) {
+			err = "policy mapping: should be non-critical";
+			break;
+		}
+	id_ce_certificatePolicies =>
+		(err, eclass) = decode_certificatePolicies(ext);
+		# either critical or non-critical
+	id_ce_issuerAltName =>
+		n: list of ref GeneralName;
+		(err, n) = decode_alias(ext);
+		if(err == "")
+			eclass = ref ExtClass.IssuerAltName(n);
+		# either critical or non-critical
+	id_ce_subjectAltName =>
+		n: list of ref GeneralName;
+		(err, n) = decode_alias(ext);
+		if(err == "")
+			eclass = ref ExtClass.SubjectAltName(n);
+		# either critical or non-critical
+	id_ce_nameConstraints =>
+		(err, eclass) = decode_nameConstraints(ext);
+		# either critical or non-critical
+	id_ce_policyConstraints =>
+		(err, eclass) = decode_policyConstraints(ext);
+		# either critical or non-critical
+	id_ce_cRLNumber =>
+		(err, eclass) = decode_cRLNumber(ext);
+		if(err == "" && ext.critical != 0) {
+			err = "crl number: should be non-critical";
+			break;
+		}
+	id_ce_reasonCode =>
+		(err, eclass) = decode_reasonCode(ext);
+		if(err == "" && ext.critical != 0) {
+			err = "crl reason: should be non-critical";
+			break;
+		}
+	id_ce_instructionCode =>
+		(err, eclass) = decode_instructionCode(ext);
+		if(err == "" && ext.critical != 0) {
+			err = "instruction code: should be non-critical";
+			break;
+		}
+	id_ce_invalidityDate =>
+		(err, eclass) = decode_invalidityDate(ext);
+		if(err == "" && ext.critical != 0) {
+			err = "invalidity date: should be non-critical";
+			break;
+		}
+	id_ce_issuingDistributionPoint =>
+		(err, eclass) = decode_issuingDistributionPoint(ext);
+		if(err == "" && ext.critical != 1) {
+			err = "issuing distribution point: should be critical";
+			break;
+		}
+	id_ce_cRLDistributionPoint =>
+		(err, eclass) = decode_cRLDistributionPoint(ext);
+		# either critical or non-critical
+	id_ce_certificateIssuer =>
+		(err, eclass) = decode_certificateIssuer(ext);
+		if(err == "" && ext.critical != 1) {
+			err = "certificate issuer: should be critical";
+			break;
+		}
+	id_ce_deltaCRLIndicator =>
+		(err, eclass) = decode_deltaCRLIndicator(ext);
+		if(err == "" && ext.critical != 1) {
+			err = "delta crl indicator: should be critical";
+			break;
+		}
+	id_ce_subjectDirectoryAttributes =>
+		(err, eclass) = decode_subjectDirectoryAttributes(ext);
+		if(ext.critical != 0) {
+			err = "subject directory attributes should be non-critical";
+			break;
+		}
+	* =>
+		err = "unknown extension class";
+	}
+
+	return (err, eclass);
+}
+
+# [public]
+
+ExtClass.encode(ec: self ref ExtClass, critical: int): ref Extension
+{
+	ext: ref Extension;
+
+	if(critical)
+		;	# unused
+	pick c := ec {
+	AuthorityKeyIdentifier =>
+		(err, a) := encode_authorityKeyIdentifier(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_authorityKeyIdentifier], 0, a);
+	SubjectKeyIdentifier =>
+		(err, a) := encode_subjectKeyIdentifier(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_subjectKeyIdentifier], 0, a);
+	BasicConstraints =>
+		(err, a) := encode_basicConstraints(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_basicConstraints], 0, a);
+	KeyUsage =>
+		(err, a) := encode_keyUsage(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_keyUsage], 0, a);
+	PrivateKeyUsage =>
+		(err, a) := encode_privateKeyUsage(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_privateKeyUsage],	0, a);
+	PolicyMapping =>
+		(err, a) := encode_policyMapping(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_policyMapping], 0, a);
+	CertificatePolicies =>
+		(err, a) := encode_certificatePolicies(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_certificatePolicies], 0, a);
+	IssuerAltName =>
+		(err, a) := encode_alias(c.alias);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_issuerAltName], 0, a);
+	SubjectAltName =>
+		(err, a) := encode_alias(c.alias);
+		if(err == "") 
+			ext = ref Extension(ref objIdTab[id_ce_subjectAltName], 0, a);
+	NameConstraints =>
+		(err, a) := encode_nameConstraints(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_nameConstraints],	0, a);
+	PolicyConstraints =>
+		(err, a) := encode_policyConstraints(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_policyConstraints], 0, a);
+	CRLNumber =>
+		(err, a) := encode_cRLNumber(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_cRLNumber], 0, a);
+	ReasonCode =>
+		(err, a) := encode_reasonCode(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_reasonCode], 0, a);
+	InstructionCode =>
+		(err, a) := encode_instructionCode(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_instructionCode],	0, a);
+	InvalidityDate =>
+		(err, a) := encode_invalidityDate(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_invalidityDate], 0, a);
+	CRLDistributionPoint =>
+		(err, a) := encode_cRLDistributionPoint(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_cRLDistributionPoint], 0, a);
+	IssuingDistributionPoint =>
+		(err, a) := encode_issuingDistributionPoint(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_issuingDistributionPoint], 0, a);
+	CertificateIssuer =>
+		(err, a) := encode_certificateIssuer(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_certificateIssuer], 0, a);
+	DeltaCRLIndicator =>
+		(err, a) := encode_deltaCRLIndicator(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_deltaCRLIndicator], 0, a);
+	SubjectDirectoryAttributes =>
+		(err, a) := encode_subjectDirectoryAttributes(c);
+		if(err == "")
+			ext = ref Extension(ref objIdTab[id_ce_subjectDirectoryAttributes], 0, a);
+	}
+	return ext;
+}
+
+# [public]
+
+ExtClass.tostring(et: self ref ExtClass): string
+{
+	s: string;
+
+	pick t := et {
+	AuthorityKeyIdentifier =>
+		s = "Authority Key Identifier: ";
+		s += "\n\tid = " + bastr(t.id);
+		s += "\n\tissuer = " + t.issuer.tostring();
+		s += "\n\tserial_number = " + bastr(t.serial_number.iptobebytes());
+	SubjectKeyIdentifier =>
+		s = "Subject Key Identifier ";
+		s += "\n\tid = " + bastr(t.id);
+	BasicConstraints =>	
+		s = "Basic Constraints: ";
+		s += "\n\tdepth = " + string t.depth;
+	KeyUsage =>
+		s = "Key Usage: ";
+		s += "\n\tusage = ";
+	PrivateKeyUsage =>
+		s = "Private Key Usage: ";
+		s += "\n\tusage = ";
+	PolicyMapping =>
+		s = "Policy Mapping: ";
+		pl := t.pairs;
+		while(pl != nil) {
+			(issuer_oid, subject_oid) := hd pl;
+			s += "\n\t(" + issuer_oid.tostring() + ", " + subject_oid.tostring() + ")";
+			pl = tl pl;
+		}
+	CertificatePolicies =>
+		s = "Certificate Policies: ";
+		pl := t.policies;
+		while(pl != nil) {
+			s += (hd pl).tostring();
+			pl = tl pl;
+		}
+	IssuerAltName =>
+		s = "Issuer Alt Name: ";
+		al := t.alias;
+		while(al != nil) {
+			s += (hd al).tostring() + ",";
+			al = tl al;
+		}
+	SubjectAltName =>
+		s = "Subject Alt Name: ";
+		al := t.alias;
+		while(al != nil) {
+			s += (hd al).tostring() + ",";
+			al = tl al;
+		}		
+	NameConstraints =>
+		s = "Name Constraints: ";
+		s += "\n\tpermitted = ";
+		p := t.permitted;
+		while(p != nil) {
+			s += (hd p).tostring();
+			p = tl p;
+		}
+		s += "\n\texcluded = ";
+		e := t.excluded;
+		while(e != nil) {
+			s += (hd e).tostring();
+			e = tl e;
+		}
+	PolicyConstraints =>
+		s = "Policy Constraints: ";
+		s += "\n\trequire = " + string t.require;
+		s += "\n\tinhibit = " + string t.inhibit;
+	CRLNumber =>
+		s = "CRL Number: ";
+		s += "\n\tcurrent crl number = " + string t.curr;
+	ReasonCode =>
+		s = "Reason Code: ";
+		s += "\n\tcode = ";
+	InstructionCode =>
+		s = "Instruction Code: ";
+		s += "\n\thold with oid = " + t.oid.tostring();
+	InvalidityDate =>
+		s = "Invalidity Date: ";
+		s += "\n\tdate = " + daytime->text(daytime->local(t.date));
+	CRLDistributionPoint =>
+		s = "CRL Distribution Point: ";
+		ps := t.ps;
+		while(ps != nil) {
+			s += (hd ps).tostring() + ",";
+			ps = tl ps;
+		}
+	IssuingDistributionPoint =>
+		s = "Issuing Distribution Point: ";
+	CertificateIssuer =>
+		s = "Certificate Issuer: ";
+	DeltaCRLIndicator =>
+		s = "Delta CRL Indicator: ";
+	SubjectDirectoryAttributes =>
+		s = "Subject Directory Attributes: ";
+	* =>
+		s = "Unknown Extension: ";
+	}
+
+	return s;
+}
+
+# [private]
+
+decode_authorityKeyIdentifier(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok)
+			break parse;
+		ak := ref ExtClass.AuthorityKeyIdentifier;
+		e := hd el;
+		(ok, e) = is_context(e, 0);
+		if(ok) {
+			(ok, ak.id) = e.is_octetstring();
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		if(el != nil && len el != 2)
+			break parse;
+		e = hd el;
+		(ok, e) = is_context(e, 1);
+		if(!ok)
+			break parse;
+		(ok, ak.issuer) = parse_gname(e);
+		if(!ok)
+			break parse;
+		e = hd tl el;
+		(ok, e) = is_context(e, 2);
+		if(!ok)
+			break parse;
+		(ok, ak.serial_number) = parse_sernum(e);
+		if(!ok)
+			break;
+		return ("", ak);
+	}
+	return ("syntax error", nil);	
+}
+
+# [private]
+
+encode_authorityKeyIdentifier(c: ref ExtClass.AuthorityKeyIdentifier): (string, array of byte)
+{
+	el: list of ref Elem;
+	if(c.serial_number != nil) {
+		(ok, e) := pack_context(
+				ref Elem(
+					Tag(Universal, INTEGER, 0),
+					ref Value.BigInt(c.serial_number.iptobebytes())
+				),
+				2
+			);
+		if(!ok)
+			return ("syntax error", nil);
+		el = e :: nil;
+	}
+	if(c.issuer != nil) {
+		(ok, e) := pack_gname(c.issuer);
+		if(!ok)
+			return ("authority key identifier: encoding error", nil);
+		(ok, e) = pack_context(e, 1);
+		if(!ok)
+			return ("authority key identifier: encoding error", nil);
+		el = e :: el;
+	}
+	if(c.id != nil) {
+		(ok, e) := pack_context(
+				ref Elem(
+					Tag(Universal, OCTET_STRING, 0),
+					ref Value.Octets(c.id)
+				),
+				0
+			);
+		if(!ok)
+			return ("authority key identifier: encoding error", nil);
+		el = e :: el;
+	}
+	return asn1->encode(ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el)));
+}
+
+# [private]
+
+decode_subjectKeyIdentifier(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, id) := all.is_octetstring();
+		if(!ok)
+			break parse;
+		return ("", ref ExtClass.SubjectKeyIdentifier(id));
+
+	}
+	return ("subject key identifier: syntax error", nil);
+}
+
+# [private]
+
+encode_subjectKeyIdentifier(c: ref ExtClass.SubjectKeyIdentifier): (string, array of byte)
+{
+	if(c.id == nil)
+		return ("syntax error", nil);
+	e := ref Elem(Tag(Universal, OCTET_STRING, 0), ref Value.Octets(c.id));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_basicConstraints(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok || len el != 2)
+			break parse;
+		ca: int;
+		(ok, ca) = (hd el).is_int(); # boolean
+		if(!ok || ca != 1)
+			break parse;
+		path: int;
+		(ok, path) = (hd tl el).is_int(); # integer
+		if(!ok || path < 0)
+			break parse;		
+		return ("", ref ExtClass.BasicConstraints(path));
+	}
+	return ("basic constraints: syntax error", nil);
+}
+
+# [private]
+
+encode_basicConstraints(c: ref ExtClass.BasicConstraints): (string, array of byte)
+{
+	el: list of ref Elem;
+	el = ref Elem(Tag(Universal, INTEGER, 0), ref Value.Int(c.depth)) :: nil;
+	el = ref Elem(Tag(Universal, BOOLEAN, 0), ref Value.Bool(1)) :: el;
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_keyUsage(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		# assert bits can fit into a limbo int
+		if(len ext.value > 4)
+			break parse;
+		return ("", ref ExtClass.KeyUsage(b4int(ext.value)));
+	}
+	return ("key usage: syntax error", nil);
+}
+
+# [private]
+
+encode_keyUsage(c: ref ExtClass.KeyUsage): (string, array of byte)
+{
+	return ("", int4b(c.usage));
+}
+
+# [private]
+
+decode_privateKeyUsage(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok || len el < 1) # at least one exists
+			break parse;
+		v := ref Validity;
+		e := hd el;
+		(ok, e) = is_context(e, 0);
+		if(ok) {
+			(ok, v.not_before) = parse_time(e, GeneralizedTime);
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		if(el != nil) {
+			e = hd el;
+			(ok, e) = is_context(e, 1);
+			if(!ok) 
+				break parse;
+			(ok, v.not_after) = parse_time(e, GeneralizedTime);
+			if(!ok)
+				break parse;
+		}
+		return ("", ref ExtClass.PrivateKeyUsage(v));
+	}
+	return ("private key usage: syntax error", nil);
+}
+
+# [private]
+
+encode_privateKeyUsage(c: ref ExtClass.PrivateKeyUsage): (string, array of byte)
+{
+	el: list of ref Elem;
+	e: ref Elem;
+	ok := 1;
+	p := c.period;
+	if(p == nil)
+		return ("encode private key usage: imcomplete data", nil);
+	if(p.not_after > 0) {
+		t := pack_time(p.not_after, GeneralizedTime);
+		e = ref Elem(Tag(Universal, GeneralizedTime, 0), ref Value.String(t));
+		(ok, e) = pack_context(e, 1);
+		if(!ok)
+			return ("encode private key usage: illegal context", nil);
+		el = e :: nil;
+	}
+	if(p.not_before > 0) {
+		t := pack_time(p.not_before, GeneralizedTime);
+		e = ref Elem(Tag(Universal, GeneralizedTime, 0), ref Value.String(t));
+		(ok, e) = pack_context(e, 0);
+		if(!ok)
+			return ("encode private key usage: illegal context", nil);
+		el = e :: el;
+	}
+	e = ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_policyMapping(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok)
+			break parse;
+		l_pm: list of (ref Oid, ref Oid);
+		while(el != nil) {
+			e_pm: list of ref Elem;
+			(ok, e_pm) = (hd el).is_seq();
+			if(!ok || len e_pm != 2)
+				break parse;
+			idp, sdp: ref Oid;
+			(ok, idp) = (hd e_pm).is_oid();
+			if(!ok)
+				break parse;
+			(ok, sdp) = (hd tl e_pm).is_oid();
+			if(!ok)
+				break parse;
+			l_pm = (idp, sdp) :: l_pm;
+		}
+		# reverse the order
+		l: list of (ref Oid, ref Oid);
+		while(l_pm != nil) {
+			l = (hd l_pm) :: l;
+			l_pm = tl l_pm;
+		}
+		return ("", ref ExtClass.PolicyMapping(l));			
+	}
+	return ("policy mapping: syntax error", nil);
+}
+
+# [private]
+
+encode_policyMapping(c: ref ExtClass.PolicyMapping): (string, array of byte)
+{
+	el, pel: list of ref Elem;
+	if(c.pairs == nil)
+		return ("policy mapping: incomplete data", nil);
+	pl := c.pairs;
+	while(pl != nil) {
+		(a, b) := hd pl;
+		if(a == nil || b == nil)
+			return ("policy mapping: incomplete data", nil);
+		be := ref Elem(Tag(Universal, OBJECT_ID, 0), ref Value.ObjId(b));
+		ae := ref Elem(Tag(Universal, OBJECT_ID, 0), ref Value.ObjId(a));
+		pel = ref Elem(
+			Tag(Universal, SEQUENCE, 1), 
+			ref Value.Seq(ae::be::nil)
+		) :: pel;
+		pl = tl pl;
+	}
+	while(pel != nil) {
+		el = (hd pel) :: el;
+		pel = tl pel;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_certificatePolicies(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok)
+			break parse;
+		l_pi: list of ref PolicyInfo;
+		while(el != nil) {
+			e_pi: list of ref Elem;
+			(ok, e_pi) = (hd el).is_seq();
+			if(!ok || len e_pi > 2 || len e_pi < 1)
+				break parse;
+			pi: ref PolicyInfo;	
+			(ok, pi.oid) = (hd e_pi).is_oid();
+			if(!ok)
+				break parse;
+			# get optional policy qualifier info
+			e_pi = tl e_pi;
+			if(e_pi != nil) {
+				e_pq: list of ref Elem;
+				(ok, e_pq) = (hd e_pi).is_seq();
+				if(!ok || len e_pq > 2 || len e_pq < 1)
+					break parse;
+				l_pq: list of ref PolicyQualifier;
+				while(e_pq != nil) {
+					pq: ref PolicyQualifier;
+					(ok, pq.oid) = (hd e_pq).is_oid();
+					if(!ok || pq.oid == nil)
+						break parse;
+					# get optional value
+					if(tl e_pq != nil) {
+						(ok, pq.value) = (hd tl e_pq).is_octetstring();
+						if(!ok)
+							break parse;
+					}
+					l_pq = pq :: l_pq;
+					e_pq = tl e_pq;
+				}
+				# reverse the order
+				while(l_pq != nil) {
+					pi.qualifiers = (hd l_pq) :: pi.qualifiers;
+					l_pq = tl l_pq;
+				}
+			}
+			l_pi = pi :: l_pi;
+		}
+		# reverse the order
+		l: list of ref PolicyInfo;
+		while(l_pi != nil) {
+			l = (hd l_pi) :: l;
+			l_pi = tl l_pi;
+		}
+		return ("", ref ExtClass.CertificatePolicies(l));			
+	}
+	return ("certificate policies: syntax error", nil);
+}
+
+# [private]
+
+encode_certificatePolicies(c: ref ExtClass.CertificatePolicies): (string, array of byte)
+{
+	el, pel: list of ref Elem;
+	pl := c.policies;
+	while(pl != nil) {
+		p := hd pl;
+		if(p.oid == nil)
+			return ("certificate policies: incomplete data", nil);
+		plseq: list of ref Elem;
+		if(p.qualifiers != nil) {
+			ql := p.qualifiers;
+			qel, qlseq: list of ref Elem;
+			while(ql != nil) {
+				pq := hd ql;
+				pqseq: list of ref Elem;
+				if(pq.oid == nil)
+					return ("certificate policies: incomplete data", nil);
+				if(pq.value != nil) {
+					pqseq = ref Elem(
+							Tag(Universal, OCTET_STRING, 0),
+							ref Value.Octets(pq.value)
+					) :: nil;
+				}
+				pqseq = ref Elem(
+						Tag(Universal, OBJECT_ID, 0),
+						ref Value.ObjId(pq.oid)
+				) :: pqseq;
+				qlseq = ref Elem(
+						Tag(Universal, SEQUENCE, 1),
+						ref Value.Seq(pqseq)
+				) :: qlseq;
+				ql = tl ql;
+			}
+			while(qlseq != nil) {
+				qel = (hd qlseq) :: qel;
+				qlseq = tl qlseq;
+			}
+			plseq = ref Elem(
+					Tag(Universal, SEQUENCE, 1),
+					ref Value.Seq(qel)
+			) :: nil;
+		}
+		plseq = ref Elem(
+				Tag(Universal, OBJECT_ID, 0), 
+				ref Value.ObjId(p.oid)
+		) :: plseq;
+		pel = ref Elem(
+				Tag(Universal, SEQUENCE, 1), 
+				ref Value.Seq(plseq)
+		) :: pel;
+		pl = tl pl;		
+	}
+	while(pel != nil) {
+		el = (hd pel) :: el;
+		pel = tl pel;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_alias(ext: ref Extension): (string, list of ref GeneralName)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok)
+			break parse;
+		l_sa: list of ref GeneralName;
+		while(el != nil) {
+			gn: ref GeneralName;
+			(ok, gn) = parse_gname(hd el);
+			if(!ok)
+				break parse;
+			l_sa = gn :: l_sa;
+			el = tl el;
+		}
+		# reverse order
+		sa: list of ref GeneralName;
+		while(l_sa != nil) {
+			sa = (hd l_sa) :: sa;
+			l_sa = tl l_sa;
+		}
+		return ("", sa);
+	}
+	return ("alias: syntax error", nil);
+}
+
+# [private]
+
+encode_alias(gl: list of ref GeneralName): (string, array of byte)
+{
+	el, gel: list of ref Elem;
+	while(gl != nil) {
+		g := hd gl;
+		(ok, e) := pack_gname(g);
+		if(!ok)
+			return ("alias: encoding error", nil);
+		gel = e :: gel;
+		gl = tl gl;
+	}
+	while(gel != nil) {
+		el = (hd gel) :: el;
+		gel = tl gel;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_subjectDirectoryAttributes(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok)
+			break parse;
+		l_a: list of ref Attribute;
+		while(el != nil) {
+			a: ref Attribute;
+			#(ok, a) = parse_attr(hd el);
+			#if(!ok)
+			#	break parse;
+			l_a = a :: l_a;
+			el = tl el;
+		}
+		# reverse order
+		as: list of ref Attribute;
+		while(l_a != nil) {
+			as = (hd l_a) :: as;
+			l_a = tl l_a;
+		}
+		return ("", ref ExtClass.SubjectDirectoryAttributes(as));
+	}
+	return ("subject directory attributes: syntax error", nil);
+}
+
+# [private]
+
+encode_subjectDirectoryAttributes(c: ref ExtClass.SubjectDirectoryAttributes)
+	: (string, array of byte)
+{
+	el, ael: list of ref Elem;
+	al := c.attrs;
+	while(al != nil) {
+		(ok, e) := pack_attr(hd al);
+		if(!ok)
+			return ("subject directory attributes: encoding error", nil);
+		ael = e :: ael;
+		al = tl al;
+	}
+	while(ael != nil) {
+		el = (hd ael) :: el;
+		ael = tl ael;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_nameConstraints(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok || len el < 1 || len el > 2)
+			break parse;
+		nc := ref ExtClass.NameConstraints;
+		if(el != nil) {
+			(ok, nc.permitted) = parse_gsubtrees(hd el);
+			if(!ok || nc.permitted == nil)
+				break parse;
+			el = tl el; 
+		}
+		if(el!= nil) {
+			(ok, nc.excluded) = parse_gsubtrees(hd el);
+			if(!ok || nc.excluded == nil)
+				break parse;
+		}
+		return ("", nc);
+	}
+	return ("name constraints: syntax error", nil); 
+}
+
+# [private]
+
+encode_nameConstraints(c: ref ExtClass.NameConstraints): (string, array of byte)
+{
+	el: list of ref Elem;
+	if(c.permitted == nil && c.excluded == nil)
+		return ("name constraints: incomplete data", nil);
+	if(c.excluded != nil) {
+		(ok, e) := pack_gsubtrees(c.excluded);
+		if(!ok)
+			return ("name constraints: encoding error", nil);
+		el = e :: el;
+	}
+	if(c.permitted != nil) {
+		(ok, e) := pack_gsubtrees(c.permitted);
+		if(!ok)
+			return ("name constraints: encoding error", nil);
+		el = e :: el;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);	
+}
+
+# [private]
+
+parse_gsubtrees(e: ref Elem): (int, list of ref GSubtree)
+{
+parse:
+	for(;;) {
+		(ok, el) := e.is_seq();
+		if(!ok)
+			break parse;
+		l, lgs: list of ref GSubtree;
+		while(el != nil) {
+			gs: ref GSubtree;
+			(ok, gs) = parse_gsubtree(hd el);
+			if(!ok)
+				break parse;
+			lgs = gs :: lgs;
+			el = tl el;
+		}
+		while(lgs != nil) {
+			l = (hd lgs) :: l;
+			lgs = tl lgs;
+		}	 
+		return (1, l);
+	} 
+	return (0, nil);
+} 
+
+# [private]
+
+pack_gsubtrees(gs: list of ref GSubtree): (int, ref Elem)
+{
+	el, l: list of ref Elem;
+	while(gs != nil) {
+		(ok, e) := pack_gsubtree(hd gs);
+		if(!ok)
+			return (0, nil);
+		l = e :: l;
+	}
+	while(l != nil) {
+		el = (hd l) :: el;
+		l = tl l;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return (1, e);
+}
+
+# [private]
+
+parse_gsubtree(e: ref Elem): (int, ref GSubtree)
+{
+parse:
+	for(;;) {
+		(ok, el) := e.is_seq();
+		if(!ok || len el > 3 || len el < 2)
+			break parse;
+		gs := ref GSubtree; 
+		e = hd el;
+		(ok, gs.base) = parse_gname(e);
+		if(!ok)
+			break parse;
+		el = tl el;
+		e = hd el;
+		(ok, e) = is_context(e, 0);
+		if(ok) {
+			(ok, gs.min) = e.is_int();
+			if(!ok)	
+				break parse;
+			el = tl el;
+		}
+		# get optional maximum base distance
+		if(el != nil) {
+			e = hd el;
+			(ok, e) = is_context(e, 1);
+			if(!ok)
+				break parse;
+			(ok, gs.max) = e.is_int();
+			if(!ok)
+				break parse;
+		}
+		return (1, gs);
+	}
+	return (0, nil);
+}
+
+# [private]
+
+pack_gsubtree(g: ref GSubtree): (int, ref Elem)
+{
+	el: list of ref Elem;
+	ok := 1;
+	e: ref Elem;
+	if(g.base == nil)
+		return (0, nil);
+	if(g.max != 0) {
+		e = ref Elem(Tag(Universal, INTEGER, 0), ref Value.Int(g.max));
+		(ok, e) = pack_context(e, 1);
+		if(!ok)
+			return (0, nil);
+		el = e :: nil;
+	}
+	if(g.min != 0) {
+		e = ref Elem(Tag(Universal, INTEGER, 0), ref Value.Int(g.min));
+		(ok, e) = pack_context(e, 0);
+		if(!ok)
+			return (0, nil);
+		el = e :: el;
+	}
+	(ok, e) = pack_gname(g.base);
+	if(!ok)
+		return (0, nil);
+	el = e :: el;
+	e = ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return (1, e);
+}
+
+# [private]
+
+decode_policyConstraints(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok || len el < 1 || len el > 2)
+			break parse;
+		pc := ref ExtClass.PolicyConstraints;
+		e := hd el;
+		(ok, e) = is_context(e, 0);
+		if(ok) {
+			(ok, pc.require) = e.is_int();
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		if(el != nil) {
+			e = hd el;
+			(ok, e) = is_context(e, 1);
+			if(!ok)
+				break parse;
+			(ok, pc.inhibit) = e.is_int();
+			if(!ok)
+				break parse;
+		} 
+		return ("", pc);
+	}
+	return ("policy constraints: syntax error", nil);
+}
+
+# [private]
+
+encode_policyConstraints(c: ref ExtClass.PolicyConstraints): (string, array of byte)
+{
+	el: list of ref Elem;
+	ok := 1;
+	if(c.inhibit > 0) {
+		e := ref Elem(Tag(Universal, INTEGER, 0), ref Value.Int(c.inhibit));
+		(ok, e) = pack_context(e, 1);
+		if(!ok)
+			return ("policy constraints: encoding error", nil);
+		el = e :: nil;
+	}
+	if(c.require > 0) {
+		e := ref Elem(Tag(Universal, INTEGER, 0), ref Value.Int(c.require));
+		(ok, e) = pack_context(e, 0);
+		if(!ok)
+			return ("policy constraints: encoding error", nil);
+		el = e :: el;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_cRLNumber(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, n) := all.is_int(); # TODO: should be IPint
+		if(!ok)
+			break parse;
+		return ("", ref ExtClass.CRLNumber(n));
+	}
+	return ("crl number: syntax error", nil);
+}
+
+# [private]
+
+encode_cRLNumber(c: ref ExtClass.CRLNumber): (string, array of byte)
+{
+	e := ref Elem(Tag(Universal, INTEGER, 0), ref Value.Int(c.curr));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_reasonCode(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, un_used_bits, code) := all.is_bitstring();
+		if(!ok)
+			break parse;
+		# no harm to ignore unused bits
+		if(len code > 4)
+			break parse;
+		return ("", ref ExtClass.ReasonCode(b4int(code))); 
+	}
+	return ("crl reason: syntax error", nil);
+}
+
+# [private]
+
+encode_reasonCode(c: ref ExtClass.ReasonCode): (string, array of byte)
+{
+	e := ref Elem(
+			Tag(Universal, BIT_STRING, 0), 
+			ref Value.BitString(0, int4b(c.code))
+		);
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_instructionCode(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, code) := all.is_oid();
+		if(!ok)
+			break parse;
+		return ("", ref ExtClass.InstructionCode(code));
+	}
+	return ("instruction code: syntax error", nil);
+}
+
+# [private]
+
+encode_instructionCode(c: ref ExtClass.InstructionCode): (string, array of byte)
+{
+	e := ref Elem(Tag(Universal, OBJECT_ID, 0), ref Value.ObjId(c.oid));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_invalidityDate(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, date) := all.is_time();
+		if(!ok)
+			break parse;
+		t := decode_time(date, GeneralizedTime);
+		if(t < 0)
+			break parse;
+		return ("", ref ExtClass.InvalidityDate(t));
+	}
+	return ("", nil);
+}
+
+# [private]
+
+encode_invalidityDate(c: ref ExtClass.InvalidityDate): (string, array of byte)
+{
+	e := ref Elem(
+			Tag(Universal, GeneralizedTime, 0), 
+			ref Value.String(pack_time(c.date, GeneralizedTime))
+		);
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_cRLDistributionPoint(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok || len el < 1) # Note: at least one
+			break parse;
+		l, dpl: list of ref DistrPoint;
+		while(el != nil) {
+			dp: ref DistrPoint;
+			(ok, dp) = parse_distrpoint(hd el);
+			if(!ok)
+				break parse;
+			dpl = dp :: dpl;
+		} 
+		# reverse order
+		while(dpl != nil) {
+			l = (hd dpl) :: l;
+			dpl = tl dpl;
+		}
+		return ("", ref ExtClass.CRLDistributionPoint(l));
+	}
+	return ("crl distribution point: syntax error", nil);
+}
+
+# [private]
+
+encode_cRLDistributionPoint(c: ref ExtClass.CRLDistributionPoint): (string, array of byte)
+{
+	el, l: list of ref Elem;
+	dpl := c.ps;
+	if(dpl == nil) # at lease one
+		return ("crl distribution point: incomplete data error", nil);		
+	while(dpl != nil) {
+		(ok, e) := pack_distrpoint(hd dpl);
+		if(!ok)
+			return ("crl distribution point: encoding error", nil);
+		l = e :: l;
+	}
+	while(l != nil) {
+		el = (hd l) :: el;
+		l = tl l;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+parse_distrpoint(e: ref Elem): (int, ref DistrPoint)
+{
+parse:
+	for(;;) {
+		(ok, el) := e.is_seq();
+		if(!ok)
+			break parse;
+		if(!ok || len el > 3 || len el < 1)
+			break parse;
+		dp: ref DistrPoint;
+		e = hd el;
+		# get optional distribution point name
+		(ok, e) = is_context(e, 0);
+		if(ok) {
+			(ok, dp.name) = parse_dpname(e);
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		# get optional reason flags
+		if(el != nil) {
+			e = hd el;
+			(ok, e) = is_context(e, 1);
+			if(ok) {
+				unused_bits: int;
+				reasons: array of byte;
+				(ok, unused_bits, reasons) = e.is_bitstring();
+				if(!ok)
+					break parse;
+				# no harm to ignore unused bits
+				if(len reasons > 4)
+					break parse;
+				dp.reasons = b4int(reasons);
+			}
+			el = tl el;
+		}
+		# get optional crl issuer
+		if(el != nil) {
+			e = hd el;
+			(ok, e) = is_context(e, 2);
+			if(!ok)
+				break parse;
+			(ok, dp.issuer) = parse_lgname(e);
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		# must be no more left
+		if(el != nil)
+			break parse;
+		return (1, dp);	
+	}
+	return (0, nil);
+}
+
+# [private]
+
+pack_distrpoint(dp: ref DistrPoint): (int, ref Elem)
+{
+	el: list of ref Elem;
+	if(dp.issuer != nil) {
+		(ok, e) := pack_lgname(dp.issuer);
+		if(!ok)
+			return (0, nil);
+		(ok, e) = pack_context(e, 2);
+		if(!ok)
+			return (0, nil);
+		el = e :: nil;
+	}
+	if(dp.reasons != 0) {
+		e := ref Elem(
+				Tag(Universal, BIT_STRING, 0), 
+				ref Value.BitString(0, int4b(dp.reasons))
+			);
+		ok := 1;
+		(ok, e) = pack_context(e, 1);
+		if(!ok)
+			return (0, nil);
+		el = e :: el;
+	}
+	if(dp.name != nil) {
+		(ok, e) := pack_dpname(dp.name);
+		if(!ok)
+			return (0, nil);
+		(ok, e) = pack_context(e, 0);
+		if(!ok)
+			return (0, nil);
+		el = e :: el;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return (1, e);
+}
+
+# [private]
+
+parse_dpname(e: ref Elem): (int, ref DistrPointName)
+{
+parse:
+	for(;;) {
+		# parse CHOICE
+		ok := 0;
+		(ok, e) = is_context(e, 0);
+		if(ok) {
+			lg: list of ref GeneralName;
+			(ok, lg) = parse_lgname(e);
+			if(!ok)
+				break parse;
+			return (1, ref DistrPointName(lg, nil));
+		}
+		(ok, e) = is_context(e, 1);
+		if(!ok)
+			break parse;
+		n: ref Name;
+		(ok, n) = parse_name(e);
+		if(!ok)
+			break parse;
+		return (1, ref DistrPointName(nil, n.rd_names));
+	}
+	return (0, nil);
+}
+
+# [private]
+
+pack_dpname(dpn: ref DistrPointName): (int, ref Elem)
+{
+	if(dpn.full_name != nil) {
+		(ok, e) := pack_lgname(dpn.full_name);
+		if(!ok)
+			return (0, nil);
+		return pack_context(e, 0);
+	}
+	if(dpn.rdname != nil) {
+		rdn := dpn.rdname;
+		el, l: list of ref Elem;
+		while(rdn != nil) {
+			l = pack_rdname(hd rdn) :: l;
+			rdn = tl rdn;
+		}
+		while(l != nil) {
+			el = (hd l) :: el;
+			l = tl l;
+		}
+		e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+		return pack_context(e, 1);
+	}
+	return (0, nil);
+}
+
+# [private]
+
+decode_issuingDistributionPoint(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok || len el < 3 || len el > 5)
+			break parse;
+		ip := ref ExtClass.IssuingDistributionPoint;
+		ae := hd el;
+		# get optional distribution point name
+		(ok, ae) = is_context(ae, 0);
+		if(ok) {
+			#(ok, ip.name) = parse_dpname(ae);
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		# get only contains user certs field
+		if(el != nil) {
+			ae = hd el;
+			(ok, ae) = is_context(ae, 1);
+			if(ok) {
+				(ok, ip.only_usercerts) = ae.is_int(); # boolean
+				if(!ok)
+					break parse;
+			}
+			el = tl el;
+		}
+		# get only contains ca certs field
+		if(el != nil) {
+			ae = hd el;
+			(ok, ae) = is_context(ae, 2);
+			if(ok) {
+				(ok, ip.only_cacerts) = ae.is_int(); # boolean
+				if(!ok)
+					break parse;
+			}
+			el = tl el;
+		}
+		# get optioinal only some reasons
+		if(el != nil) {
+			ae = hd el;
+			(ok, ae) = is_context(ae, 3);
+			if(ok) {
+				reasons: array of byte;
+				unused_bits: int;
+				(ok, unused_bits, reasons) = ae.is_bitstring();
+				if(!ok || len reasons > 4)
+					break parse;
+				ip.only_reasons = b4int(reasons);
+			}
+			el = tl el;
+		}
+		# get indirect crl field
+		if(el != nil) {
+			ae = hd el;
+			(ok, ae) = is_context(ae, 4);
+			if(!ok)
+				break parse;
+			(ok, ip.indirect_crl) = ae.is_int(); # boolean
+			if(!ok)
+				break parse;
+			el = tl el;
+		}
+		# must be no more left
+		if(el != nil)
+			break parse;
+		return ("", ip);
+	}
+	return ("issuing distribution point: syntax error", nil);
+}
+
+# [private]
+
+encode_issuingDistributionPoint(c: ref ExtClass.IssuingDistributionPoint)
+	: (string, array of byte)
+{
+	el: list of ref Elem;
+	ok := 1;
+	if(c.indirect_crl != 0) { # no encode for DEFAULT
+		e := ref Elem(
+				Tag(Universal, BOOLEAN, 0), 
+				ref Value.Bool(c.indirect_crl)
+			);
+		(ok, e) = pack_context(e, 4);
+		if(!ok)
+			return ("issuing distribution point: encoding error", nil);
+		el = e :: el;
+	}
+	if(c.only_reasons != 0) {
+		e := ref Elem(
+				Tag(Universal, BIT_STRING, 0),
+				ref Value.BitString(0, int4b(c.only_reasons))
+			);
+		(ok, e) = pack_context(e, 3);
+		if(!ok)
+			return ("issuing distribution point: encoding error", nil);			
+		el = e :: el;
+	}
+	if(c.only_cacerts != 0) {
+		e := ref Elem(
+				Tag(Universal, BOOLEAN, 0), 
+				ref Value.Bool(c.only_cacerts)
+			);
+		(ok, e) = pack_context(e, 2);
+		if(!ok)
+			return ("issuing distribution point: encoding error", nil);
+		el = e :: el;
+	}
+	if(c.only_usercerts != 0) {
+		e := ref Elem(
+				Tag(Universal, BOOLEAN, 0), 
+				ref Value.Bool(c.only_usercerts)
+			);
+		(ok, e) = pack_context(e, 1);
+		if(!ok)
+			return ("issuing distribution point: encoding error", nil);
+		el = e :: el;
+	}
+	if(c.name != nil) {
+		e: ref Elem;
+		(ok, e) = pack_dpname(c.name);
+		if(!ok)
+			return ("issuing distribution point: encoding error", nil);
+		(ok, e) = pack_context(e, 0);
+		if(!ok)
+			return ("issuing distribution point: encoding error", nil);
+		el = e :: el;
+	}
+
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_certificateIssuer(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, el) := all.is_seq();
+		if(!ok)
+			break parse;
+		gl, gnl: list of ref GeneralName;
+		while(el != nil) {
+			g: ref GeneralName;
+			(ok, g) = parse_gname(hd el);
+			if(!ok)
+				break parse;
+			gnl = g :: gnl;
+			el = tl el;
+		}
+		while(gnl != nil) {
+			gl = (hd gnl) :: gl;
+			gnl = tl gnl;
+		}
+		return ("", ref ExtClass.CertificateIssuer(gl));
+	}
+
+	return ("certificate issuer: syntax error", nil);
+}
+
+# [private]
+
+encode_certificateIssuer(c: ref ExtClass.CertificateIssuer): (string, array of byte)
+{
+	el, nel: list of ref Elem;
+	ns := c.names;
+	while(ns != nil) {
+		(ok, e) := pack_gname(hd ns);
+		if(!ok)
+			return ("certificate issuer: encoding error", nil);
+		nel = e :: nel;
+		ns = tl ns;
+	}
+	while(nel != nil) {
+		el = (hd nel) :: el;
+		nel = tl nel;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return asn1->encode(e);
+}
+
+# [private]
+
+decode_deltaCRLIndicator(ext: ref Extension): (string, ref ExtClass)
+{
+parse:
+	for(;;) {
+		(err, all) := asn1->decode(ext.value);
+		if(err != "")
+			break parse;
+		(ok, b) := all.is_bigint();
+		if(!ok)
+			break parse;
+		return ("", ref ExtClass.DeltaCRLIndicator(IPint.bebytestoip(b)));
+	}
+	return ("delta crl number: syntax error", nil);
+}
+
+# [private]
+
+encode_deltaCRLIndicator(c: ref ExtClass.DeltaCRLIndicator): (string, array of byte)
+{
+	e := ref Elem(
+			Tag(Universal, INTEGER, 0), 
+			ref Value.BigInt(c.number.iptobebytes())
+		);
+	return asn1->encode(e);
+}
+
+# [public]
+
+GeneralName.tostring(gn: self ref GeneralName): string
+{
+	s: string;
+
+	pick g := gn {
+	otherName => 
+		s = "other name: " + g.str;
+	rfc822Name =>
+		s = "rfc822 name: " + g.str;
+	dNSName =>
+		s = "dns name: " + g.str;
+	x400Address =>
+		s = "x400 address: " + g.str;
+	uniformResourceIdentifier =>
+		s = "url: " + g.str;
+	iPAddress =>
+		s = "ip address: " + bastr(g.ip);
+	registeredID =>
+		s = "oid: " + g.oid.tostring();
+	ediPartyName =>
+		s = "edi party name: ";
+		s += "\n\tname assigner is " + g.nameAssigner.tostring();
+		s += "\n\tparty name is " + g.partyName.tostring();
+	directoryName =>
+		s = "directory name: " + g.dir.tostring();
+	}
+	return s;
+}
+
+# [public]
+
+PolicyInfo.tostring(pi: self ref PolicyInfo): string
+{
+	s := "oid: " + pi.oid.tostring();
+	s += "qualifiers: ";
+	ql := pi.qualifiers;
+	while(ql != nil) {
+		s += (hd ql).tostring();
+		ql = tl ql;
+	}
+	return s;
+}
+
+# [public]
+
+PolicyQualifier.tostring(pq: self ref PolicyQualifier): string
+{
+	s := "oid: " + pq.oid.tostring();
+	s += "value: " + bastr(pq.value);
+	return s;
+}
+
+# [public]
+
+GSubtree.tostring(gs: self ref GSubtree): string
+{
+	s := "base: " + gs.base.tostring();
+	s += "range: " + string gs.min + "-" + string gs.max;
+	return s;
+}
+
+# [public]
+
+DistrPoint.tostring(dp: self ref DistrPoint): string
+{
+	s := "Distribution Point: ";
+	s += "\n\tname = ";
+	d := dp.name;
+	if(d.full_name != nil) {
+		f := d.full_name;
+		while(f != nil) {
+			s += (hd f).tostring() + ",";
+			f = tl f;
+		}
+	}
+	else {
+		r := d.rdname;
+		while(r != nil) {
+			s += (hd r).tostring() + ",";
+			r = tl r;
+		}
+	}
+	s += "\n\treasons = " + string dp.reasons;
+	s += "\n\tissuer = ";
+	gl := dp.issuer;
+	while(gl != nil) {
+		s += (hd gl).tostring() + ",";
+		gl = tl gl;
+	}
+	return s;
+}
+
+# [private]
+
+is_context(e: ref Elem, num: int): (int, ref Elem)
+{
+	if(e.tag.class == ASN1->Context && e.tag.num == num) {
+		pick v := e.val {
+		Octets =>
+			(err, all) := asn1->decode(v.bytes);
+			if(err == "")
+				return (1, all);
+		}
+	}
+	return (0, nil);
+}
+
+# [private]
+
+pack_context(e: ref Elem, num: int): (int, ref Elem)
+{
+	(err, b) := asn1->encode(e);
+	if(err == "") 
+		return (1, ref Elem(Tag(Context, num, 0), ref Value.Octets(b)));
+	return (0, nil);
+}
+
+# [private]
+
+parse_lgname(e: ref Elem): (int, list of ref GeneralName)
+{
+parse:
+	for(;;) {
+		(ok, el) := e.is_seq();
+		if(!ok)
+			break parse;
+		l, lg: list of ref GeneralName;
+		while(el != nil) {
+			g: ref GeneralName;
+			(ok, g) = parse_gname(hd el);
+			if(!ok)
+				break parse;
+			lg = g :: lg;
+			el = tl el;
+		}
+		while(lg != nil) {
+			l = (hd lg) :: l;
+			lg = tl lg;
+		}
+		return (1, l);
+	}
+	return (0, nil);
+}
+
+# [private]
+
+pack_lgname(lg: list of ref GeneralName): (int, ref Elem)
+{
+	el, gel: list of ref Elem;
+	while(lg != nil) {
+		(ok, e) := pack_gname(hd lg);
+		if(!ok)
+			return (0, nil);
+		gel = e :: gel;
+		lg = tl lg;
+	}
+	while(gel != nil) {
+		el = (hd gel) :: el;
+		gel = tl gel;
+	}
+	e := ref Elem(Tag(Universal, SEQUENCE, 1), ref Value.Seq(el));
+	return (1, e);
+}
+
+# [private]
+
+parse_gname(e: ref Elem): (int, ref GeneralName)
+{
+parse:
+	for(;;) {
+		g: ref GeneralName;
+		ok := 1;
+		case e.tag.num {
+		0 =>
+			(ok, e) = is_context(e, 0);
+			if(!ok)
+				break parse;
+			str: string;
+			(ok, str) = e.is_string();
+			if(!ok)
+				break parse;
+			g = ref GeneralName.otherName(str);
+		1 =>
+			(ok, e) = is_context(e, 1);
+			if(!ok)
+				break parse;
+			str: string;
+			(ok, str) = e.is_string();
+			if(!ok)
+				break parse;			
+			g = ref GeneralName.rfc822Name(str);
+		2 =>
+			(ok, e) = is_context(e, 2);
+			if(!ok)
+				break parse;
+			str: string;
+			(ok, str) = e.is_string();
+			if(!ok)
+				break parse;
+			g = ref GeneralName.dNSName(str);
+		3 =>
+			(ok, e) = is_context(e, 3);
+			if(!ok)
+				break parse;
+			str: string;
+			(ok, str) = e.is_string();
+			if(!ok)
+				break parse;
+			g = ref GeneralName.x400Address(str);
+		4 =>
+			(ok, e) = is_context(e, 4);
+			if(!ok)
+				break parse;
+			dir: ref Name;
+			(ok, dir) = parse_name(e);
+			if(!ok)
+				break parse;
+			g = ref GeneralName.directoryName(dir);
+		5 =>
+			(ok, e) = is_context(e, 5);
+			if(!ok)
+				break parse;
+			el: list of ref Elem;
+			(ok, el) = e.is_seq();
+			if(!ok || len el < 1 || len el > 3)
+				break parse;
+			na, pn: ref Name;
+			(ok, e) = is_context(hd el, 0);
+			if(ok) {
+				(ok, na) = parse_name(e);
+				if(!ok)
+					break parse;
+				el = tl el;
+			}
+			if(el != nil) {
+				(ok, e) = is_context(hd el, 1);
+				if(!ok)
+					break parse;
+				(ok, pn) = parse_name(e);
+				if(!ok)
+					break parse;
+			}
+			g = ref GeneralName.ediPartyName(na, pn);
+		6 =>
+			(ok, e) = is_context(e, 6);
+			if(!ok)
+				break parse;
+			str: string;
+			(ok, str) = e.is_string();
+			if(!ok)
+				break parse;
+			g = ref GeneralName.uniformResourceIdentifier(str);
+		7 =>
+			(ok, e) = is_context(e, 7);
+			if(!ok)
+				break parse;
+			ip: array of byte;
+			(ok, ip) = e.is_octetstring();
+			if(!ok)
+				break parse;
+			g = ref GeneralName.iPAddress(ip);
+		8 =>
+			(ok, e) = is_context(e, 8);
+			if(!ok)
+				break parse;
+			oid: ref Oid;
+			(ok, oid) = e.is_oid();
+			if(!ok)
+				break parse;			
+			g = ref GeneralName.registeredID(oid);
+		* =>
+			break parse;
+		}
+		return (1, g);
+	}
+	return (0, nil);
+}
+
+# [private]
+
+pack_gname(gn: ref GeneralName): (int, ref Elem)
+{
+	e: ref Elem;
+	ok := 1;
+
+	pick g := gn {
+	otherName => 
+			e = ref Elem(
+					Tag(Universal, GeneralString, 0),
+					ref Value.String(g.str)
+				); 
+			(ok, e) = pack_context(e, 0);
+			if(!ok)
+				return (0, nil);
+	rfc822Name =>
+			e = ref Elem(
+					Tag(Universal, IA5String, 0),
+					ref Value.String(g.str)
+				); 
+			(ok, e) = pack_context(e, 1);
+			if(!ok)
+				return (0, nil);
+	dNSName =>
+			e = ref Elem(
+					Tag(Universal, IA5String, 0),
+					ref Value.String(g.str)
+				); 
+			(ok, e) = pack_context(e, 2);
+			if(!ok)
+				return (0, nil);
+	x400Address =>
+			e = ref Elem(
+					Tag(Universal, GeneralString, 0),
+					ref Value.String(g.str)
+				); 
+			(ok, e) = pack_context(e, 3);
+			if(!ok)
+				return (0, nil);
+	uniformResourceIdentifier =>
+			e = ref Elem(
+					Tag(Universal, GeneralString, 0),
+					ref Value.String(g.str)
+				); 
+			(ok, e) = pack_context(e, 6);
+			if(!ok)
+				return (0, nil);
+	iPAddress =>
+			e = ref Elem(
+					Tag(Universal, OCTET_STRING, 0),
+					ref Value.Octets(g.ip)
+				); 
+			(ok, e) = pack_context(e, 7);
+			if(!ok)
+				return (0, nil);
+
+	registeredID =>
+			e = ref Elem(
+					Tag(Universal, OBJECT_ID, 0),
+					ref Value.ObjId(g.oid)
+				); 
+			(ok, e) = pack_context(e, 8);
+			if(!ok)
+				return (0, nil);
+
+	ediPartyName =>
+			el: list of ref Elem;
+			if(g.partyName != nil) {
+				e = pack_name(g.partyName);
+				(ok, e) = pack_context(e, 1);
+				if(!ok)
+					return (0, nil);
+				el = e :: nil;
+			}
+			if(g.nameAssigner != nil) {
+				e = pack_name(g.nameAssigner);
+				(ok, e) = pack_context(e, 0);
+				if(!ok)
+					return (0, nil);
+				el = e :: el;
+			}
+			e = ref Elem(
+					Tag(Universal, SEQUENCE, 1),
+					ref Value.Seq(el)
+				); 
+			(ok, e) = pack_context(e, 5);
+			if(!ok)
+				return (0, nil);
+	directoryName =>
+			e = pack_name(g.dir);
+			(ok, e) = pack_context(e, 4);
+			if(!ok)
+				return (0, nil);			
+	}
+	return (1, e);
+}
+
+# [private]
+# convert at most 4 bytes to int, len buf must be less than 4
+
+b4int(buf: array of byte): int
+{
+	val := 0;
+	for(i := 0; i < len buf; i++)
+		val = (val << 8) | (int buf[i]);
+	return val;	
+}
+
+# [private]
+
+int4b(value: int): array of byte
+{
+	n := 4;
+	buf := array [n] of byte;
+	while(n--)	{   
+		buf[n] = byte value;
+		value >>= 8;
+	}
+	return buf;
+}
+
+# [private]
+
+oid_cmp(a, b: ref Oid): int
+{
+	na := len a.nums;
+	nb := len b.nums;
+	if(na != nb)
+		return 0;
+	for(i := 0; i < na; i++) {
+		if(a.nums[i] != b.nums[i])
+			return 0;
+	}
+	return 1;
+}
+
+# [private]
+# decode two bytes into an integer [0-99]
+# return -1 for an invalid encoding
+
+get2(a: string, i: int): int
+{
+	a0 := int a[i];
+	a1 := int a[i+1];
+	if(a0 < '0' || a0 > '9' || a1 < '0' || a1 > '9')
+        	return -1;    
+	return (a0 - '0')*10 + a1 - '0';
+}
+
+# [private]
+# encode an integer [0-99] into two bytes
+
+put2(a: array of byte, n, i: int): int
+{
+	a[i] = byte (n/10 + '0');
+	a[i+1] = byte (n%10 + '0');
+	return i+2;
+}
+
+# [private]
+
+bastr(a: array of byte) : string
+{
+	ans := "";
+	for(i := 0; i < len a; i++) {
+		if(i < len a - 1 && i%10 == 0)
+			ans += "\n\t\t";
+		ans += sys->sprint("%2x ", int a[i]);
+	}
+	return ans;
+}
+
+# [private]
+
+parse_attr(nil: ref Elem): (int, ref Attribute)
+{
+	return (0, nil);
+}
+
+# [private]
+
+pack_attr(nil: ref Attribute): (int, ref Elem)
+{
+	return (0, nil);
+}
--- /dev/null
+++ b/appl/lib/csv.b
@@ -1,0 +1,86 @@
+implement CSV;
+
+include "sys.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "csv.m";
+
+init(b: Bufio)
+{
+	bufio = b;
+}
+
+getline(fd: ref Iobuf): list of string
+{
+	rl: list of string;
+	for(;;){
+		(w, end) := getfield(fd);
+		if(rl == nil && w == nil && end < 0)
+			return nil;
+		rl = w :: rl;
+		if(end != ',')
+			break;
+	}
+	l: list of string;
+	for(; rl != nil; rl = tl rl)
+		l = hd rl :: l;
+	return l;
+}
+
+getfield(fd: ref Iobuf): (string, int)
+{
+	w := "";
+	if((c := getcr(fd)) == '"'){	# quoted field
+		while((c = getcr(fd)) >= 0){
+			if(c == '"'){
+				c = getcr(fd);
+				if(c != '"')
+					break;
+			}
+			w[len w] = c;
+		}
+	}
+	# unquoted text, possibly following quoted text above
+	for(; c >= 0 && c != ',' && c != '\n'; c = getcr(fd))
+		w[len w] = c;
+	return (w, c);
+}
+
+getcr(fd: ref Iobuf): int
+{
+	c := fd.getc();
+	if(c == '\r'){
+		nc := fd.getc();
+		if(nc >= 0 && nc != '\n')
+			fd.ungetc();
+		c = '\n';
+	}
+	return c;
+}
+
+quote(s: string): string
+{
+	sep := 0;
+	for(i := 0; i < len s; i++)
+		if((c := s[i]) == '"')
+			return innerquote(s);
+		else if(c == ',' || c == '\n')
+			sep = 1;
+	if(sep)
+		return "\""+s+"\"";
+	return s;
+}
+
+innerquote(s: string): string
+{
+	w := "\"";
+	for(i := j := 0; i < len s; i++)
+		if(s[i] == '"'){
+			w += s[j: i+1];	# including "
+			j = i;		# including " again
+		}
+	return w+s[j:i]+"\"";
+}
--- /dev/null
+++ b/appl/lib/daytime.b
@@ -1,0 +1,511 @@
+implement Daytime;
+#
+# These routines convert time as follows:
+#
+# The epoch is 0000 Jan 1 1970 GMT.
+# The argument time is in microseconds since then.
+# The local(t) entry returns a reference to an ADT
+# containing
+#
+#	seconds (0-59)
+#	minutes (0-59)
+#	hours (0-23)
+#	day of month (1-31)
+#	month (0-11)
+#	year-1900
+#	weekday (0-6, Sun is 0)
+#	day of the year
+#	daylight savings flag
+#
+# The routine gets the daylight savings time from the file /locale/timezone.
+#
+# text(tvec)
+# where tvec is produced by local
+# returns a string that has the time in the form
+#
+#	Thu Jan 01 00:00:00 GMT 1970n0
+#	012345678901234567890123456789
+#	0	  1	    2
+#
+# time() just reads the time from /dev/time
+# and then calls localtime, then asctime.
+#
+# The sign bit of second times will turn on 68 years from the epoch ->2038
+#
+include	"sys.m";
+include	"string.m";
+include "daytime.m";
+
+S: String;
+sys: Sys;
+
+dmsize := array[] of {
+	31, 28, 31, 30, 31, 30,
+	31, 31, 30, 31, 30, 31
+};
+ldmsize := array[] of {
+	31, 29, 31, 30, 31, 30,
+	31, 31, 30, 31, 30, 31
+};
+
+Timezone: adt
+{
+	stname: string;
+	dlname: string;
+	stdiff:	int;
+	dldiff: int;
+	dlpairs: array of int;
+};
+
+timezone: ref Timezone;
+
+now(): int
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	fd := sys->open("/dev/time", sys->OREAD);
+	if(fd == nil)
+		return 0;
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return 0;
+
+	t := (big string buf[0:n]) / big 1000000;
+	return int t;
+}
+
+time(): string
+{
+	t := now();
+	tm := local(t);
+	return text(tm);
+}
+
+local(tim: int): ref Tm
+{
+	ct: ref Tm;
+
+	if(timezone == nil)
+		timezone = readtimezone(nil);
+
+	t := tim + timezone.stdiff;
+	dlflag := 0;
+	for(i := 0; i+1 < len timezone.dlpairs; i += 2) {
+		if(t >= timezone.dlpairs[i] && t < timezone.dlpairs[i+1]) {
+			t = tim + timezone.dldiff;
+			dlflag++;
+			break;
+		}
+	}
+	ct = gmt(t);
+	if(dlflag) {
+		ct.zone = timezone.dlname;
+		ct.tzoff = timezone.dldiff;
+	}
+	else {
+		ct.zone = timezone.stname;
+		ct.tzoff = timezone.stdiff;
+	}
+	return ct;
+}
+
+gmt(tim: int): ref Tm
+{
+	xtime := ref Tm;
+
+	# break initial number into days
+	hms := tim % 86400;
+	day := tim / 86400;
+	if(hms < 0) {
+		hms += 86400;
+		day -= 1;
+	}
+
+	# generate hours:minutes:seconds
+	xtime.sec = hms % 60;
+	d1 := hms / 60;
+	xtime.min = d1 % 60;
+	d1 /= 60;
+	xtime.hour = d1;
+
+	# day is the day number.
+	# generate day of the week.
+	# The addend is 4 mod 7 (1/1/1970 was Thursday)
+	xtime.wday = (day + 7340036) % 7;
+
+	# year number
+	if(day >= 0)
+		for(d1 = 70; day >= dysize(d1+1900); d1++)
+			day -= dysize(d1+1900);
+	else
+		for (d1 = 70; day < 0; d1--)
+			day += dysize(d1+1900-1);
+	xtime.year = d1;
+	d0 := day;
+	xtime.yday = d0;
+
+	# generate month
+	if(dysize(d1+1900) == 366)
+		dmsz := ldmsize;
+	else
+		dmsz = dmsize;
+	for(d1 = 0; d0 >= dmsz[d1]; d1++)
+		d0 -= dmsz[d1];
+	xtime.mday = d0 + 1;
+	xtime.mon = d1;
+	xtime.zone = "GMT";
+	xtime.tzoff = 0;
+	return xtime;
+}
+
+wkday := array[] of {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+weekday := array[] of {
+	"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+};
+
+month := array[] of {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+text(t: ref Tm): string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	year := 1900+t.year;
+
+	return sys->sprint("%s %s %.2d %.2d:%.2d:%.2d %s %d",
+		wkday[t.wday],
+		month[t.mon],
+		t.mday,
+		t.hour,
+		t.min,
+		t.sec,
+		t.zone,
+		year);
+}
+
+filet(now: int, file: int): string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	t := local(file);
+	if(now - file < 6*30*24*3600)
+		return sys->sprint("%s %.2d %.2d:%.2d",
+			month[t.mon], t.mday, t.hour, t.min);
+
+	year := 1900+t.year;
+
+	return sys->sprint("%s %.2d  %d", month[t.mon], t.mday, year);
+}
+
+dysize(y: int): int
+{
+	if(y%4 == 0 && (y%100 != 0 || y%400 == 0))
+		return 366;
+	return 365;
+}
+
+readtimezone(fname: string): ref Timezone
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	tz := ref Timezone;
+	tz.stdiff = 0;
+	tz.stname = "GMT";
+
+	s: string;
+	if(fname == nil){
+		s = readfile("/env/timezone");
+		if(s == nil)
+			s = readfile("/locale/timezone");
+	}else{
+		if(fname[0] != '/' && fname[0] != '#')
+			fname = "/locale/" + fname;
+		s = readfile(fname);
+	}
+	if(s == nil)
+		return tz;
+	if(s[0] == '/' || s[0] == '#'){
+		if(s[len s-1] == '\n')
+			s = s[0: len s-1];
+		s = readfile(s);
+		if(s == nil)
+			return tz;
+	}
+	(n, val) := sys->tokenize(s, "\t \n\r");
+	if(n < 4)
+		return tz;
+
+	tz.stname = hd val;
+	val = tl val;
+	tz.stdiff = int hd val;
+	val = tl val;
+	tz.dlname = hd val;
+	val = tl val;
+	tz.dldiff = int hd val;
+	val = tl val;
+
+	tz.dlpairs = array[n-4] of {* => 0};
+	for(j := 0; val != nil; val = tl val)
+		tz.dlpairs[j++] = int hd val;
+	return tz;
+}
+
+readfile(name: string): string
+{
+	fd := sys->open(name, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[2048] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+	return string buf[0:n];
+}
+
+SEC2MIN:	con 60;
+SEC2HOUR:	con 60*SEC2MIN;
+SEC2DAY:	con 24*SEC2HOUR;
+
+tm2epoch(tm: ref Tm): int
+{
+	secs := 0;
+
+	#
+	#  seconds per year
+	#
+	yr := tm.year + 1900;
+	if(yr < 1970)
+		for(i := yr; i < 1970; i++)
+			secs -= dysize(i) * SEC2DAY;
+	else
+		for(i = 1970; i < yr; i++)
+			secs += dysize(i) * SEC2DAY;
+	#
+	#  seconds per month
+	#
+	if(dysize(yr) == 366)
+		dmsz := ldmsize;
+	else
+		dmsz = dmsize;
+	for(i = 0; i < tm.mon; i++)
+		secs += dmsz[i] * SEC2DAY;
+
+	#
+	# secs in last month
+	#
+	secs += (tm.mday-1) * SEC2DAY;
+
+	#
+	# hours, minutes, seconds
+	#
+	secs += tm.hour * SEC2HOUR;
+	secs += tm.min * SEC2MIN;
+	secs += tm.sec;
+
+	#
+	#  time zone offset includes daylight savings time
+	#
+	return secs - tm.tzoff;
+}
+
+# handle three formats (we'll be a bit more tolerant)
+#  Sun, 06 Nov 1994 08:49:37 TZ  (rfc822+rfc1123)
+#  Sunday, 06-Nov-94 08:49:37 TZ (rfc850, obsoleted by rfc1036)
+#  Sun Nov  6 08:49:37 1994	 (ANSI C's asctime() format, assume GMT)
+#
+# return nil on parsing error
+#
+string2tm(date: string): ref Tm
+{
+	buf: string;
+	ok: int;
+	tm := ref Tm;
+
+	if(S == nil)
+		S = load String String->PATH;
+
+	# Weekday|Wday
+	(date, buf) = dateword(date);
+	tm.wday = strlookup(wkday, buf);
+	if(tm.wday < 0)
+		tm.wday = strlookup(weekday, buf);
+	if(tm.wday < 0)
+		return nil;
+
+	# Try Mon
+	odate := date;
+	(date, buf) = dateword(date);
+	tm.mon = strlookup(month, buf);
+	if(tm.mon >= 0) {
+		# Mon was OK, so asctime() format
+		# DD
+		(date, tm.mday) = datenum(date);
+		if(tm.mday < 1 || tm.mday > 31)
+			return nil;
+
+		# HH:MM:SS
+		(ok, date) = hhmmss(date, tm);
+		if(!ok)
+			return nil;
+
+		# optional time zone
+		while(date != nil && date[0] == ' ')
+			date = date[1:];
+		if(date != nil && !(date[0] >= '0' && date[0] <= '9')){
+			for(i := 0; i < len date; i++)
+				if(date[i] == ' '){
+					(tm.zone, tm.tzoff) = tzinfo(date[0: i]);
+					date = date[i:];
+					break;
+				}
+		}
+
+		# YY|YYYY
+		(nil, tm.year) = datenum(date);
+		if(tm.year > 1900)
+			tm.year -= 1900;
+		if(tm.zone == ""){
+			tm.zone = "GMT";
+			tm.tzoff = 0;
+		}
+	} else {
+		# Mon was not OK
+		date = odate;
+		# DD Mon YYYY or DD-Mon-(YY|YYYY)
+		(date, tm.mday) = datenum(date);
+		if(tm.mday < 1 || tm.mday > 31)
+			return nil;
+		(date, buf) = dateword(date);
+		tm.mon = strlookup(month, buf);
+		if(tm.mon < 0 || tm.mon >= 12)
+			return nil;
+		(date, tm.year) = datenum(date);
+		if(tm.year > 1900)
+			tm.year -= 1900;
+
+		# HH:MM:SS
+		(ok, buf) = hhmmss(date, tm);
+		if(!ok)
+			return nil;
+		(tm.zone, tm.tzoff) = tzinfo(buf);
+		if(tm.zone == "")
+			return nil;
+	}
+
+	return tm;
+}
+
+dateword(date: string): (string, string)
+{
+	notalnum: con "^A-Za-z0-9";
+
+	date = S->drop(date, notalnum);
+	(w, rest) := S->splitl(date, notalnum);
+	return (rest, w);
+}
+
+datenum(date: string): (string, int)
+{
+	notdig: con "^0-9";
+
+	date = S->drop(date, notdig);
+	(num, rest) := S->splitl(date, notdig);
+	return (rest, int num);
+}
+
+strlookup(a: array of string, s: string): int
+{
+	n := len a;
+	for(i := 0; i < n; i++) {
+		if(s == a[i])
+			return i;
+	}
+	return -1;
+}
+
+hhmmss(date: string, tm: ref Tm): (int, string)
+{
+	err := (0, "");
+
+	(date, tm.hour) = datenum(date);
+	if(tm.hour < 0 || tm.hour >= 24)
+		return err;
+	(date, tm.min) = datenum(date);
+	if(tm.min < 0 || tm.min >= 60)
+		return err;
+	(date, tm.sec) = datenum(date);
+	if(tm.sec < 0 || tm.sec >= 60)
+		return err;
+
+	return (1, date);
+}
+
+tzinfo(tz: string): (string, int)
+{
+	# strip leading and trailing whitespace
+	WS: con " \t";
+	tz = S->drop(tz, WS);
+	for(n := len tz; n > 0; n--) {
+		if(S->in(tz[n-1], WS) == 0)
+			break;
+	}
+	if(n < len tz)
+		tz = tz[:n];
+
+	# if no timezone, default to GMT
+	if(tz == nil)
+		return ("GMT", 0);
+
+	# GMT aliases
+	case tz {
+	"GMT" or
+	"UT" or
+	"UTC" or
+	"Z" =>
+		return ("GMT", 0);
+	}
+
+	# [+-]hhmm (hours and minutes offset from GMT)
+	if(len tz == 5 && (tz[0] == '+' || tz[0] == '-')) {
+		h := int tz[1:3];
+		m := int tz[3:5];
+		if(h > 23 || m > 59)
+			return ("", 0);
+		tzoff := h*SEC2HOUR + m*SEC2MIN;
+		if(tz[0] == '-')
+			tzoff = -tzoff;
+		return ("GMT", tzoff);
+	}
+
+	# try continental US timezones
+	filename: string;
+	case tz {
+	"CST" or "CDT" =>
+		filename = "CST.CDT";
+	"EST" or "EDT" =>
+		filename = "EST.EDT";
+	"MST" or "MDT" =>
+		filename = "MST.MDT";
+	"PST" or "PDT" =>
+		filename = "PST.PDT";
+	* =>
+		;	# default to local timezone
+	}
+	tzdata := readtimezone(filename);
+	if(tzdata.stname == tz)
+		return (tzdata.stname, tzdata.stdiff);
+	if(tzdata.dlname == tz)
+		return (tzdata.dlname, tzdata.dldiff);
+
+	return ("", 0);
+}
--- /dev/null
+++ b/appl/lib/db.b
@@ -1,0 +1,253 @@
+implement DB;
+
+include "sys.m";
+	sys: Sys;
+
+include "dial.m";
+
+include "keyring.m";
+
+include "security.m";
+
+include "db.m";
+
+RES_HEADER_SIZE: con 22;
+
+open(addr, username, password, dbname: string): (ref DB_Handle, list of string)
+{
+	(fd, err) := connect(addr, "none");
+	if(nil == fd)
+		return (nil, err :: nil);
+	return dbopen(fd, username, password, dbname);
+}
+
+connect(addr: string, alg: string): (ref Sys->FD, string)
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+
+	dial := load Dial Dial->PATH;
+	if(dial == nil)
+		return (nil, sys->sprint("load %s: %r", Dial->PATH));
+
+	addr = dial->netmkaddr(addr, "net", "6669");	# infdb
+
+	conn := dial->dial(addr, nil);
+	if(conn == nil)
+		return (nil, sys->sprint("can't dial %s: %r", addr));
+
+	(n, addrparts) := sys->tokenize(addr, "!");
+	if(n >= 2)
+		addr = hd addrparts + "!" + hd tl addrparts;	# ignore service for key search
+
+	kr := load Keyring Keyring->PATH;
+
+	user := user();
+	kd := "/usr/" + user + "/keyring/";
+	cert := kd + addr;
+	if(sys->stat(cert).t0 < 0)
+		cert = kd + "default";
+
+	ai := kr->readauthinfo(cert);
+
+	#
+	# let auth->client handle nil ai
+	# if(ai == nil){
+	#	return (nil, sys->sprint("DB init: certificate for %s not found, use getauthinfo first", addr));
+	# }
+	#
+
+	au := load Auth Auth->PATH;
+	if(au == nil)
+		return (nil, sys->sprint("DB init: can't load module Auth %r"));
+
+	err := au->init();
+	if(err != nil)
+		return (nil, sys->sprint("DB init: can't initialize module Auth: %s", err));
+
+	fd: ref Sys->FD;
+
+	(fd, err) = au->client(alg, ai, conn.dfd);
+	if(fd == nil)
+		return (nil, sys->sprint("DB init: authentication failed: %s", err));
+
+	return (fd, nil);
+}
+
+dbopen(fd: ref Sys->FD, username, password, dbname: string): (ref DB_Handle, list of string)
+{
+	dbh := ref DB_Handle;
+	dbh.datafd = fd;
+	dbh.lock = makelock();
+	dbh.sqlstream = -1;
+	logon := array of byte (username +"/"+ password +"/"+ dbname);
+	(mtype, strm, rc, data) := sendReq(dbh, 'I', logon);
+	if(mtype == 'h')
+		return (nil, (sys->sprint("DB: couldn't initialize %s for %s", dbname, username) :: string data :: nil));
+	dbh.sqlconn = int string data;
+
+	(mtype, strm, rc, data) = sendReq(dbh, 'O', array of byte string dbh.sqlconn);
+	if(mtype == 'h')
+		return (nil, (sys->sprint("DB: couldn't open SQL connection") :: string data :: nil));
+	dbh.sqlstream = int string data;
+	return (dbh, nil);
+}
+
+DB_Handle.SQLOpen(oldh: self ref DB_Handle): (int, ref DB_Handle)
+{
+	dbh := ref *oldh;
+	(mtype, nil, nil, data) := sendReq(dbh, 'O', array of byte string dbh.sqlconn);
+	if(mtype == 'h')
+		return (-1, nil);
+	dbh.sqlstream = int string data;
+	return (0, dbh);
+}
+
+DB_Handle.SQLClose(dbh: self ref DB_Handle): int
+{
+	(mtype, nil, nil, nil) := sendReq(dbh, 'K', array[0] of byte);
+	if(mtype == 'h')
+		return -1;
+	dbh.sqlstream = -1;
+	return 0;    
+}
+
+DB_Handle.SQL(dbh: self ref DB_Handle, command: string): (int, list of string)
+{
+	(mtype, nil, nil, data) := sendReq(dbh, 'W', array of byte command);
+	if(mtype == 'h')
+		return (-1, "Probable SQL format error" :: string data :: nil);
+	return (0, nil);
+}
+
+DB_Handle.columns(dbh: self ref DB_Handle): int
+{
+	(mtype, nil, nil, data) := sendReq(dbh, 'C', array[0] of byte);
+	if(mtype == 'h')
+		return 0;
+	return int string data;
+}
+
+DB_Handle.nextRow(dbh: self ref DB_Handle): int
+{
+	(mtype, nil, nil, data) := sendReq(dbh, 'N', array[0] of byte);
+	if(mtype == 'h')
+		return 0;
+	return int string data;
+}
+
+DB_Handle.read(dbh: self ref DB_Handle, columnI: int): (int, array of byte)
+{
+	(mtype, nil, nil, data) := sendReq(dbh, 'R', array of byte string columnI);
+	if(mtype == 'h')
+		return (-1, data);
+	return (len data, data);
+}
+
+DB_Handle.write(dbh: self ref DB_Handle, paramI: int, val: array of byte)
+									: int
+{
+	outbuf := array[len val + 4] of byte;
+	param := array of byte sys->sprint("%3d ", paramI);
+
+	for(i := 0; i < 4; i++)
+		outbuf[i] = param[i];
+	outbuf[4:] = val;
+	(mtype, nil, nil, nil) := sendReq(dbh, 'P', outbuf);
+	if(mtype == 'h')
+		return -1;
+	return len val;
+}
+ 
+DB_Handle.columnTitle(handle: self ref DB_Handle, columnI: int): string
+{
+	(mtype, nil, nil, data) := sendReq(handle, 'T', array of byte string columnI);
+	if(mtype == 'h')
+		return nil;
+	return string data;
+}
+
+DB_Handle.errmsg(dbh: self ref DB_Handle): string
+{
+	(nil, nil, nil, data) := sendReq(dbh, 'H', array[0] of byte);
+	return string data;
+}
+
+sendReq(dbh: ref DB_Handle, mtype: int, data: array of byte) : (int, int, int, array of byte)
+{
+	lock(dbh);
+	header := sys->sprint("%c1%11d %3d ", mtype, len data, dbh.sqlstream);
+	if(sys->write(dbh.datafd, array of byte header, 18) != 18) {
+		unlock(dbh);
+		return ('h', dbh.sqlstream, 0, array of byte "header write failure");
+	}
+	if(sys->write(dbh.datafd, data, len data) != len data) {
+		unlock(dbh);
+		return ('h', dbh.sqlstream, 0, array of byte "data write failure");
+	}
+	if(sys->write(dbh.datafd, array of byte "\n", 1) != 1) {
+		unlock(dbh);
+		return ('h', dbh.sqlstream, 0, array of byte "header write failure");
+	}
+	hbuf := array[RES_HEADER_SIZE+3] of byte;
+	if((n := sys->readn(dbh.datafd, hbuf, RES_HEADER_SIZE)) != RES_HEADER_SIZE) {
+		unlock(dbh);
+		if(n < 0)
+			why := sys->aprint("read error: %r");
+		else if(n == 0)
+			why = sys->aprint("lost connection");
+		else
+			why = sys->aprint("read error: short read");
+		return ('h', dbh.sqlstream, 0, why);
+	}
+	rheader := string hbuf[0:22];
+	rtype := rheader[0];
+	#	Probably should check version in header[1]
+	datalen := int rheader[2:13];
+	rstrm := int rheader[14:17];
+	retcode := int rheader[18:21];
+    
+	databuf := array[datalen] of byte;
+	# read in loop until get amount of data we want.  If there is a mismatch
+	# here, we may hang with a lock on!
+
+	nbytes: int;
+
+	for(length := 0; length < datalen; length += nbytes) {
+		nbytes = sys->read(dbh.datafd, databuf[length:], datalen-length);
+		if(nbytes <= 0) {
+		    break;
+		}
+	}
+	nbytes = sys->read(dbh.datafd, hbuf, 1);	#  The final \n
+	unlock(dbh);
+	return (rtype, rstrm, retcode, databuf);
+}
+
+makelock(): chan of int
+{
+	return chan[1] of int;
+}
+
+lock(h: ref DB_Handle)
+{
+	h.lock <-= h.sqlstream;
+}
+
+unlock(h: ref DB_Handle)
+{
+	<-h.lock;
+}
+
+user(): string
+{
+	sys = load Sys Sys->PATH;
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+	return string buf[0:n];	
+}
--- /dev/null
+++ b/appl/lib/dbm.b
@@ -1,0 +1,463 @@
+implement Dbm;
+
+# Copyright © Caldera International Inc.  2001-2002.  All rights reserved.
+# Limbo transliteration (with amendment) Copyright © 2004 Vita Nuova Holdings Limited.
+
+include "sys.m";
+	sys: Sys;
+	OREAD, OWRITE, ORDWR: import Sys;
+
+include "dbm.m";
+
+BYTESIZ: con 8;	# bits
+SHORTSIZ: con 2;	# bytes
+
+PBLKSIZ: con 512;
+DBLKSIZ: con 8192;	# was 4096
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+Dbf.create(file: string, mode: int): ref Dbf
+{
+	pf := sys->create(file+".pag", ORDWR, mode);
+	if(pf == nil)
+		return nil;
+	df := sys->create(file+".dir", ORDWR, mode);
+	if(df == nil)
+		return nil;
+	return alloc(pf, df, ORDWR);
+}
+
+Dbf.open(file: string, flags: int): ref Dbf
+{
+	if((flags & 3) == OWRITE)
+		flags = (flags & ~3) | ORDWR;
+	pf := sys->open(file+".pag", flags);
+	if(pf == nil)
+		return nil;
+	df := sys->open(file+".dir", flags);
+	if(df == nil)
+		return nil;
+	return alloc(pf, df, flags);
+}
+
+alloc(pf: ref Sys->FD, df: ref Sys->FD, flags: int): ref Dbf
+{
+	db := ref Dbf;
+	db.pagf = pf;
+	db.dirf = df;
+	db.flags = flags & 3;
+	db.maxbno = 0;
+	db.bitno = 0;
+	db.hmask = 0;
+	db.blkno = 0;
+	db.pagbno = -1;
+	db.pagbuf = array[PBLKSIZ] of byte;
+	db.dirbno = -1;
+	db.dirbuf = array[DBLKSIZ] of byte;
+	(ok, d) := sys->fstat(db.dirf);
+	if(ok < 0)
+		d.length = big 0;
+	db.maxbno = int (d.length*big BYTESIZ - big 1);
+	return db;
+}
+
+Dbf.flush(db: self ref Dbf)
+{
+	db.pagbno = db.dirbno = -1;
+}
+
+Dbf.isrdonly(db: self ref Dbf): int
+{
+	return db.flags == OREAD;
+}
+
+Dbf.fetch(db: self ref Dbf, key: Datum): Datum
+{
+	access(db, calchash(key));
+	for(i:=0;; i+=2){
+		item := makdatum(db.pagbuf, i);
+		if(item == nil)
+			return item;
+		if(cmpdatum(key, item) == 0){
+			item = makdatum(db.pagbuf, i+1);
+			if(item == nil){
+				sys->fprint(sys->fildes(2), "dbm: items not in pairs\n");
+				raise "dbm: items not in pairs";
+			}
+			return item;
+		}
+	}
+}
+
+Dbf.delete(db: self ref Dbf, key: Datum): int
+{
+	if(db.isrdonly())
+		return -1;
+	access(db, calchash(key));
+	for(i:=0;; i+=2){
+		item := makdatum(db.pagbuf, i);
+		if(item == nil)
+			return -1;
+		if(cmpdatum(key, item) == 0){
+			delitem(db.pagbuf, i);
+			delitem(db.pagbuf, i);
+			break;
+		}
+	}
+	sys->seek(db.pagf, big db.blkno*big PBLKSIZ, 0);
+	write(db.pagf, db.pagbuf, PBLKSIZ);
+	db.pagbno = db.blkno;
+	return 0;
+}
+
+Dbf.store(db: self ref Dbf, key: Datum, dat: Datum, replace: int): int
+{
+	if(db.isrdonly())
+		return -1;
+	for(;;){
+		access(db, calchash(key));
+		for(i:=0;; i+=2){
+			item := makdatum(db.pagbuf, i);
+			if(item == nil)
+				break;
+			if(cmpdatum(key, item) == 0){
+				if(!replace)
+					return 1;
+				delitem(db.pagbuf, i);
+				delitem(db.pagbuf, i);
+				break;
+			}
+		}
+		i = additem(db.pagbuf, key);
+		if(i >= 0){
+			if(additem(db.pagbuf, dat) >= 0)
+				break;
+			delitem(db.pagbuf, i);
+		}
+		if(!split(db, key, dat))
+			return -1;
+	}
+	sys->seek(db.pagf, big db.blkno*big PBLKSIZ, 0);
+	write(db.pagf, db.pagbuf, PBLKSIZ);
+	db.pagbno = db.blkno;
+	return 0;
+}
+
+split(db: ref Dbf, key: Datum, dat: Datum): int
+{
+	if(len key+len dat+3*SHORTSIZ >= PBLKSIZ)
+		return 0;
+	ovfbuf := array[PBLKSIZ] of {* => byte 0};
+	for(i:=0;;){
+		item := makdatum(db.pagbuf, i);
+		if(item == nil)
+			break;
+		if(calchash(item) & (db.hmask+1)){
+			additem(ovfbuf, item);
+			delitem(db.pagbuf, i);
+			item = makdatum(db.pagbuf, i);
+			if(item == nil){
+				sys->fprint(sys->fildes(2), "dbm: split not paired\n");
+				raise "dbm: split not paired";
+				#break;
+			}
+			additem(ovfbuf, item);
+			delitem(db.pagbuf, i);
+			continue;
+		}
+		i += 2;
+	}
+	sys->seek(db.pagf, big db.blkno*big PBLKSIZ, 0);
+	write(db.pagf, db.pagbuf, PBLKSIZ);
+	db.pagbno = db.blkno;
+	sys->seek(db.pagf, (big db.blkno+big db.hmask+big 1)*big PBLKSIZ, 0);
+	write(db.pagf, ovfbuf, PBLKSIZ);
+	setbit(db);
+	return 1;
+}
+
+Dbf.firstkey(db: self ref Dbf): Datum
+{
+	return copy(firsthash(db, 0));
+}
+
+Dbf.nextkey(db: self ref Dbf, key: Datum): Datum
+{
+	hash := calchash(key);
+	access(db, hash);
+	item, bitem: Datum;
+	for(i:=0;; i+=2){
+		item = makdatum(db.pagbuf, i);
+		if(item == nil)
+			break;
+		if(cmpdatum(key, item) <= 0)
+			continue;
+		if(bitem == nil || cmpdatum(bitem, item) < 0)
+			bitem = item;
+	}
+	if(bitem != nil)
+		return copy(bitem);
+	hash = hashinc(db, hash);
+	if(hash == 0)
+		return copy(item);
+	return copy(firsthash(db, hash));
+}
+
+firsthash(db: ref Dbf, hash: int): Datum
+{
+	for(;;){
+		access(db, hash);
+		bitem := makdatum(db.pagbuf, 0);
+		item: Datum;
+		for(i:=2;; i+=2){
+			item = makdatum(db.pagbuf, i);
+			if(item == nil)
+				break;
+			if(cmpdatum(bitem, item) < 0)
+				bitem = item;
+		}
+		if(bitem != nil)
+			return bitem;
+		hash = hashinc(db, hash);
+		if(hash == 0)
+			return item;
+	}
+}
+
+access(db: ref Dbf, hash: int)
+{
+	for(db.hmask=0;; db.hmask=(db.hmask<<1)+1){
+		db.blkno = hash & db.hmask;
+		db.bitno = db.blkno + db.hmask;
+		if(getbit(db) == 0)
+			break;
+	}
+	if(db.blkno != db.pagbno){
+		sys->seek(db.pagf, big db.blkno * big PBLKSIZ, 0);
+		read(db.pagf, db.pagbuf, PBLKSIZ);
+		chkblk(db.pagbuf);
+		db.pagbno = db.blkno;
+	}
+}
+
+getbit(db: ref Dbf): int
+{
+	if(db.bitno > db.maxbno)
+		return 0;
+	n := db.bitno % BYTESIZ;
+	bn := db.bitno / BYTESIZ;
+	i := bn % DBLKSIZ;
+	b := bn / DBLKSIZ;
+	if(b != db.dirbno){
+		sys->seek(db.dirf, big b * big DBLKSIZ, 0);
+		read(db.dirf, db.dirbuf, DBLKSIZ);
+		db.dirbno = b;
+	}
+	if(int db.dirbuf[i] & (1<<n))
+		return 1;
+	return 0;
+}
+
+setbit(db: ref Dbf)
+{
+	if(db.bitno > db.maxbno){
+		db.maxbno = db.bitno;
+		getbit(db);
+	}
+	n := db.bitno % BYTESIZ;
+	bn := db.bitno / BYTESIZ;
+	i := bn % DBLKSIZ;
+	b := bn / DBLKSIZ;
+	db.dirbuf[i] |= byte (1<<n);
+	sys->seek(db.dirf, big b * big DBLKSIZ, 0);
+	write(db.dirf, db.dirbuf, DBLKSIZ);
+	db.dirbno = b;
+}
+
+makdatum(buf: array of byte, n: int): Datum
+{
+	ne := GETS(buf, 0);
+	if(n < 0 || n >= ne)
+		return nil;
+	t := PBLKSIZ;
+	if(n > 0)
+		t = GETS(buf, n+1-1);
+	v := GETS(buf, n+1);
+	return buf[v: t];	# size is t-v
+}
+
+cmpdatum(d1: Datum, d2: Datum): int
+{
+	n := len d1;
+	if(n != len d2)
+		return n - len d2;
+	if(n == 0)
+		return 0;
+	for(i := 0; i < len d1; i++)
+		if(d1[i] != d2[i])
+			return int d1[i] - int d2[i];
+	return 0;
+}
+
+copy(d: Datum): Datum
+{
+	if(d == nil)
+		return nil;
+	a := array[len d] of byte;
+	a[0:] = d;
+	return a;
+}
+
+# ken's
+#
+#	055,043,036,054,063,014,004,005,
+#	010,064,077,000,035,027,025,071,
+#
+
+hitab := array[16] of {
+         61, 57, 53, 49, 45, 41, 37, 33,
+	29, 25, 21, 17, 13,  9,  5,  1,
+};
+
+hltab := array[64] of {
+	8r6100151277,8r6106161736,8r6452611562,8r5001724107,
+	8r2614772546,8r4120731531,8r4665262210,8r7347467531,
+	8r6735253126,8r6042345173,8r3072226605,8r1464164730,
+	8r3247435524,8r7652510057,8r1546775256,8r5714532133,
+	8r6173260402,8r7517101630,8r2431460343,8r1743245566,
+	8r0261675137,8r2433103631,8r3421772437,8r4447707466,
+	8r4435620103,8r3757017115,8r3641531772,8r6767633246,
+	8r2673230344,8r0260612216,8r4133454451,8r0615531516,
+	8r6137717526,8r2574116560,8r2304023373,8r7061702261,
+	8r5153031405,8r5322056705,8r7401116734,8r6552375715,
+	8r6165233473,8r5311063631,8r1212221723,8r1052267235,
+	8r6000615237,8r1075222665,8r6330216006,8r4402355630,
+	8r1451177262,8r2000133436,8r6025467062,8r7121076461,
+	8r3123433522,8r1010635225,8r1716177066,8r5161746527,
+	8r1736635071,8r6243505026,8r3637211610,8r1756474365,
+	8r4723077174,8r3642763134,8r5750130273,8r3655541561,
+};
+
+hashinc(db: ref Dbf, hash: int): int
+{
+	hash &= db.hmask;
+	bit := db.hmask+1;
+	for(;;){
+		bit >>= 1;
+		if(bit == 0)
+			return 0;
+		if((hash&bit) == 0)
+			return hash|bit;
+		hash &= ~bit;
+	}
+}
+
+calchash(item: Datum): int
+{
+	hashl := 0;
+	hashi := 0;
+	for(i:=0; i<len item; i++){
+		f := int item[i];
+		for(j:=0; j<BYTESIZ; j+=4){
+			hashi += hitab[f&16rF];
+			hashl += hltab[hashi&16r3F];
+			f >>= 4;
+		}
+	}
+	return hashl;
+}
+
+delitem(buf: array of byte, n: int)
+{
+	ne := GETS(buf, 0);
+	if(n < 0 || n >= ne){
+		sys->fprint(sys->fildes(2), "dbm: bad delitem\n");
+		raise "dbm: bad delitem";
+	}
+	i1 := GETS(buf, n+1);
+	i2 := PBLKSIZ;
+	if(n > 0)
+		i2 = GETS(buf, n+1-1);
+	i3 := GETS(buf, ne+1-1);
+	if(i2 > i1)
+		while(i1 > i3){
+			i1--;
+			i2--;
+			buf[i2] = buf[i1];
+			buf[i1] = byte 0;
+		}
+	i2 -= i1;
+	for(i1=n+1; i1<ne; i1++)
+		PUTS(buf, i1+1-1, GETS(buf, i1+1) + i2);
+	PUTS(buf, 0, ne-1);
+	PUTS(buf, ne, 0);
+}
+
+additem(buf: array of byte, item: Datum): int
+{
+	i1 := PBLKSIZ;
+	ne := GETS(buf, 0);
+	if(ne > 0)
+		i1 = GETS(buf, ne+1-1);
+	i1 -= len item;
+	i2 := (ne+2) * SHORTSIZ;
+	if(i1 <= i2)
+		return -1;
+	PUTS(buf, ne+1, i1);
+	buf[i1:] = item;
+	PUTS(buf, 0, ne+1);
+	return ne;
+}
+
+chkblk(buf: array of byte)
+{
+	t := PBLKSIZ;
+	ne := GETS(buf, 0);
+	for(i:=0; i<ne; i++){
+		v := GETS(buf, i+1);
+		if(v > t)
+			badblk();
+		t = v;
+	}
+	if(t < (ne+1)*SHORTSIZ)
+		badblk();
+}
+
+read(fd: ref Sys->FD, buf: array of byte, n: int)
+{
+	nr := sys->read(fd, buf, n);
+	if(nr == 0){
+		for(i := 0; i < len buf; i++)
+			buf[i] = byte 0;
+	}else if(nr != n)
+		raise "dbm: read error: "+sys->sprint("%r");
+}
+
+write(fd: ref Sys->FD, buf: array of byte, n: int)
+{
+	if(sys->write(fd, buf, n) != n)
+		raise "dbm: write error: "+sys->sprint("%r");
+}
+
+badblk()
+{
+	sys->fprint(sys->fildes(2), "dbm: bad block\n");
+	raise "dbm: bad block";
+}
+
+GETS(buf: array of byte, sh: int): int
+{
+	sh *= SHORTSIZ;
+	return (int buf[sh]<<8) | int buf[sh+1];
+}
+
+PUTS(buf: array of byte, sh: int, v: int)
+{
+	sh *= SHORTSIZ;
+	buf[sh] = byte (v>>8);
+	buf[sh+1] = byte v;
+}
--- /dev/null
+++ b/appl/lib/dbsrv.b
@@ -1,0 +1,124 @@
+implement DBserver;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+
+include "security.m";
+
+include "db.m";              # For now.
+
+stderr: ref Sys->FD;
+
+DBserver : module
+{
+	init:   fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+# argv is a list of Inferno supported algorithms from Security->Auth
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stdin := sys->fildes(0);
+	stderr = sys->fildes(2);
+	if(argv != nil)
+		argv = tl argv;
+	if(argv == nil)
+		err("no algorithm list");
+
+	kr := load Keyring Keyring->PATH;
+	if(nil == kr)
+		err(sys->sprint("can't load Keyring: %r"));
+
+	auth := load Auth Auth->PATH;
+	if(auth == nil)
+		err(sys->sprint("can't load Auth: %r"));
+
+	error := auth->init();
+	if(error != nil)
+		err(sys->sprint("Auth init failed: %s", error));
+
+	ai := kr->readauthinfo("/usr/"+user()+"/keyring/default");
+
+	(client_fd, info_or_err) := auth->server(argv, ai, stdin, 1);
+	if(client_fd == nil)
+		err(sys->sprint("can't authenticate client: %s", info_or_err));
+
+	auth = nil;
+	kr = nil;
+
+	sys->pctl(Sys->FORKNS|Sys->NEWPGRP, nil);
+
+	# run the infdb database program in the host system using /cmd
+
+	cmdfd := sys->open("/cmd/clone", sys->ORDWR);
+	if (cmdfd == nil)
+		err(sys->sprint("can't open /cmd/clone: %r"));
+
+	buf := array [20] of byte;
+	n := sys->read(cmdfd, buf, len buf);
+	if(n <= 0)
+		err(sys->sprint("can't read /cmd/clone: %r"));
+	cmddir := string buf[0:n];
+
+	if (sys->fprint(cmdfd, "exec infdb") <= 0)
+		err(sys->sprint("can't start infdb via /cmd/clone: %r"));
+
+	datafile := "/cmd/" + cmddir + "/data";
+	infdb_fd := sys->open(datafile, Sys->ORDWR);
+	if (infdb_fd == nil)
+		err(sys->sprint("can't open %s: %r", datafile));
+
+	spawn dbxfer(infdb_fd, client_fd, "client");
+
+	dbxfer(client_fd, infdb_fd, "infdb");
+	sys->fprint(infdb_fd, "X1          0   0 \n");
+}
+
+dbxfer(source, sink: ref Sys->FD, tag: string)
+{
+	buf := array [Sys->ATOMICIO] of byte;
+	while((nr := sys->read(source, buf, len buf)) > 0)
+		if(sys->write(sink, buf, nr) != nr){
+			sys->fprint(stderr, "dbsrv: write to %s failed: %r\n", tag);
+			shutdown();
+		}
+	if(nr < 0){
+		sys->fprint(stderr, "dbsrv: reading data for %s: %r\n", tag);
+		shutdown();
+	}
+}
+
+shutdown()
+{
+	pid := sys->pctl(0, nil);
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "killgrp") < 0)
+		err(sys->sprint("can't kill group %d: %r", pid));
+}
+
+err(s: string)
+{
+	sys->fprint(stderr, "dbsrv: %s\n", s);
+	raise "fail:error";
+}
+
+user(): string
+{
+	sys = load Sys Sys->PATH;
+
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n]; 
+}
--- /dev/null
+++ b/appl/lib/debug.b
@@ -1,0 +1,1496 @@
+implement Debug;
+
+include "sys.m";
+sys: Sys;
+sprint, FD: import sys;
+
+include "string.m";
+str: String;
+
+include "draw.m";
+
+include "debug.m";
+
+include "dis.m";
+	dism: Dis;
+
+Command: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Spin: adt
+{
+	spin:	int;
+	pspin:	int;
+};
+
+SrcState: adt
+{
+	files:	array of string;
+	lastf:	int;
+	lastl:	int;
+	vers:	int;			# version number
+					# 11 => more source states
+};
+
+typenames := array[] of {
+	Terror => "error",
+	Tid => "id",
+	Tadt => "adt",
+	Tadtpick => "adtpick",
+	Tarray => "array",
+	Tbig => "big",
+	Tbyte => "byte",
+	Tchan => "chan",
+	Treal => "real",
+	Tfn => "fn",
+	Targ => "arg",
+	Tlocal => "local",
+	Tglobal => "global",
+	Tint => "int",
+	Tlist => "list",
+	Tmodule => "module",
+	Tnil => "nil",
+	Tnone => "none",
+	Tref => "ref",
+	Tstring => "string",
+	Ttuple => "tuple",
+	Tend => "end",
+	Targs => "args",
+	Tslice => "slice",
+	Tpoly => "poly",
+};
+
+tnone:		ref Type;
+tnil:		ref Type;
+tint:		ref Type;
+tbyte:		ref Type;
+tbig:		ref Type;
+treal:		ref Type;
+tstring:	ref Type;
+tpoly:	ref Type;
+
+IBY2WD:		con 4;
+IBY2LG:		con 8;
+H:		con int 16rffffffff;
+
+ModHash:	con 32;
+SymHash:	con 32;
+mods:=		array[ModHash] of list of ref Module;
+syms:=		array[SymHash] of list of ref Sym;
+
+sblpath :=	array[] of
+{
+	("/dis/",	"/appl/cmd/"),
+	("/dis/",	"/appl/"),
+};
+
+init(): int
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	if(sys == nil || str == nil)
+		return 0;
+	tnone = ref Type(nil, Tnone, 0, "", nil, nil, nil);
+	tnil = ref Type(nil, Tnil, IBY2WD, "nil", nil, nil, nil);
+	tint = ref Type(nil, Tint, IBY2WD, "int", nil, nil, nil);
+	tbyte = ref Type(nil, Tbyte, 1, "byte", nil, nil, nil);
+	tbig = ref Type(nil, Tbig, IBY2LG, "big", nil, nil, nil);
+	treal = ref Type(nil, Treal, IBY2LG, "real", nil, nil, nil);
+	tstring = ref Type(nil, Tstring, IBY2WD, "string", nil, nil, nil);
+	tpoly = ref Type(nil, Tpoly, IBY2WD, "polymorphic", nil, nil, nil);
+	return 1;
+}
+
+prog(pid: int): (ref Prog, string)
+{
+	spid := string pid;
+	h := sys->open("/prog/"+spid+"/heap", sys->ORDWR);
+	if(h == nil)
+		return (nil, sprint("can't open heap file: %r"));
+	c := sys->open("/prog/"+spid+"/ctl", sys->OWRITE);
+	if(c == nil)
+		return (nil, sprint("can't open ctl file: %r"));
+	d := sys->open("/prog/"+spid+"/dbgctl", sys->ORDWR);
+	if(d == nil)
+		return (nil, sprint("can't open debug ctl file: %r"));
+	s := sys->open("/prog/"+spid+"/stack", sys->OREAD);
+	if(s == nil)
+		return (nil, sprint("can't open stack file: %r"));
+	return (ref Prog(pid, h, c, d, s), "");
+}
+
+startprog(dis, dir: string, ctxt: ref Draw->Context, argv: list of string): (ref Prog, string)
+{
+	c := load Command dis;
+	if(c == nil)
+		return (nil, "module not loaded");
+
+	ack := chan of int;
+	spin := ref Spin(1, 1);
+	end := chan of int;
+	spawn execer(ack, dir, c, ctxt, argv, spin, end);
+	kid := <-ack;
+
+	fd := sys->open("/prog/"+string kid+"/dbgctl", sys->ORDWR);
+	if(fd == nil){
+		spin.spin = -1;
+		<- end;
+		return (nil, sprint("can't open debug ctl file: %r"));
+	}
+	done := chan of string;
+	spawn stepper(done, fd, spin);
+
+wait:	for(;;){
+		alt{
+		<-ack =>
+			sys->sleep(0);
+		err := <-done =>
+			if(err != ""){
+				<- end;
+				return(nil, err);
+			}
+			break wait;
+		}
+	}
+
+	b := array[20] of byte;
+	n := sys->read(fd, b, len b);
+	if(n <= 0){
+		<- end;
+		return(nil, sprint("%r"));
+	}
+	msg := string b[:n];
+	if(!str->prefix("new ", msg)){
+		<- end;
+		return (nil, msg);
+	}
+
+	kid = int msg[len "new ":];
+
+	# clean up the execer slave
+	b = array of byte "start";
+	sys->write(fd, b, len b);
+
+	<- end;
+	return prog(kid);
+}
+
+stepper(done: chan of string, ctl: ref FD, spin: ref Spin)
+{
+	b := array of byte "step1";
+	while(spin.pspin){
+		if(sys->write(ctl, b, len b) != len b)
+			done <-= sprint("can't start new thread: %r");
+		spin.spin = 0;
+	}
+	done <-= "";
+}
+
+execer(ack: chan of int, dir: string, c: Command, ctxt: ref Draw->Context, args: list of string, spin: ref Spin, end: chan of int)
+{
+	pid := sys->pctl(Sys->NEWPGRP|Sys->FORKNS|Sys->NEWFD, 0::1::2::nil);
+	sys->chdir(dir);
+	while(spin.spin == 1)
+		ack <-= pid;
+	if(spin.spin == -1){
+		end <-= 0;
+		exit;
+	}
+	spawn c->init(ctxt, args);
+	spin.pspin = 0;
+	end <-= 0;
+	exit;
+}
+
+# format of each line is
+# fp pc mp prog compiled path
+# fp, pc, mp, and prog are %.8lux
+# compile is  or 1
+# path is a string
+Prog.stack(p: self ref Prog): (array of ref Exp, string)
+{
+	buf := array[8192] of byte;
+	sys->seek(p.stk, big 0, 0);
+	n := sys->read(p.stk, buf, len buf - 1);
+	if(n < 0)
+		return (nil, sprint("can't read stack file: %r"));
+	buf[n] = byte 0;
+
+	t := 0;
+	nf := 0;
+	for(s := 0; s < n; s = t+1){
+		t = strchr(buf, s, '\n');
+		if(buf[t] != byte '\n' || t-s < 40)
+			continue;
+		nf++;
+	}
+
+	e := array[nf] of ref Exp;
+	nf = 0;
+	for(s = 0; s < n; s = t+1){
+		t = strchr(buf, s, '\n');
+		if(buf[t] != byte '\n' || t-s < 40)
+			continue;
+		e[nf] = ref Exp("unknown fn",
+				hex(buf[s+0:s+8]), 
+				hex(buf[s+9:s+17]),
+				mkmod(hex(buf[s+18:s+26]), hex(buf[s+27:s+35]), buf[36] != byte '0', string buf[s+38:t]),
+				p,
+				nil);
+		nf++;
+	}
+
+	return (e, "");
+}
+
+Prog.step(p: self ref Prog, how: int): string
+{
+	(stack, nil) := p.stack();
+	if(stack == nil)
+		return "can't find initial pc";
+	src := stack[0].srcstr();
+	stmt := ftostmt(stack[0]);
+
+	if(stack[0].m.sym == nil)
+		how = -1;
+
+	buf := array of byte("step1");
+	if(how == StepOut)
+		buf = array of byte("toret");
+	while(sys->write(p.dbgctl, buf, len buf) == len buf){
+		(stk, err) := p.stack();
+		if(err != nil)
+			return "";
+		case how{
+		StepExp =>
+			if(src != stk[0].srcstr())
+				return "";
+		StepStmt =>
+			if(stmt != ftostmt(stk[0]))
+				return "";
+			if(stk[0].offset != stack[0].offset)
+				return "";
+		StepOut =>
+			if(returned(stack, stk))
+				return "";
+		StepOver =>
+			if(stk[0].offset == stack[0].offset){
+				if(stmt != ftostmt(stk[0]))
+					return "";
+				buf = array of byte("step1");
+				break;
+			}
+			if(returned(stack, stk))
+				return "";
+			buf = array of byte("toret");
+		* =>
+			return "";
+		}
+	}
+	return sprint("%r");
+}
+
+Prog.stop(p: self ref Prog): string
+{
+	return dbgctl(p, "stop");
+}
+
+Prog.unstop(p: self ref Prog): string
+{
+	return dbgctl(p, "unstop");
+}
+
+Prog.grab(p: self ref Prog): string
+{
+	return dbgctl(p, "step0");
+}
+
+Prog.start(p: self ref Prog): string
+{
+	return dbgctl(p, "start");
+}
+
+Prog.cont(p: self ref Prog): string
+{
+	return dbgctl(p, "cont");
+}
+
+dbgctl(p: ref Prog, msg: string): string
+{
+	b := array of byte msg;
+	while(sys->write(p.dbgctl, b, len b) != len b)
+		return sprint("%r");
+	return "";
+}
+
+returned(old, new: array of ref Exp): int
+{
+	n := len old;
+	if(n > len new)
+		return 1;
+	return 0;
+}
+
+Prog.setbpt(p: self ref Prog, dis: string, pc:int): string
+{
+	b := array of byte("bpt set "+dis+" "+string pc);
+	if(sys->write(p.dbgctl, b, len b) != len b)
+		return sprint("can't set breakpoint: %r");
+	return "";
+}
+
+Prog.delbpt(p: self ref Prog, dis: string, pc:int): string
+{
+	b := array of byte("bpt del "+dis+" "+string pc);
+	if(sys->write(p.dbgctl, b, len b) != len b)
+		return sprint("can't del breakpoint: %r");
+	return "";
+}
+
+Prog.kill(p: self ref Prog): string
+{
+	b := array of byte "kill";
+	if(sys->write(p.ctl, b, len b) != len b)
+		return sprint("can't kill process: %r");
+	return "";
+}
+
+Prog.event(p: self ref Prog): string
+{
+	b := array[100] of byte;
+	n := sys->read(p.dbgctl, b, len b);
+	if(n < 0)
+		return sprint("error: %r");
+	return string b[:n];
+}
+
+ftostmt(e: ref Exp): int
+{
+	m := e.m;
+	if(!m.comp && m.sym != nil && e.pc < len m.sym.srcstmt)
+		return m.sym.srcstmt[e.pc];
+	return -1;
+}
+
+Exp.srcstr(e: self ref Exp): string
+{
+	m := e.m;
+	if(!m.comp && m.sym != nil && e.pc < len m.sym.src){
+		src := m.sym.src[e.pc];
+		ss := src.start.file+":"+string src.start.line+"."+string src.start.pos+", ";
+		if(src.stop.file != src.start.file)
+			ss += src.stop.file+":"+string src.stop.line+".";
+		else if(src.stop.line != src.start.line)
+			ss += string src.stop.line+".";
+		return ss+string src.stop.pos;
+	}
+	return sprint("Module %s PC %d", e.m.path, e.pc);
+}
+
+Exp.findsym(e: self ref Exp): string
+{
+	m := e.m;
+	if(m.comp)
+		return "compiled module";
+	if(m.sym != nil){
+		n := e.pc;
+		fns := m.sym.fns;
+		for(i := 0; i < len fns; i++){
+			if(n >= fns[i].offset && n < fns[i].stoppc){
+				e.name = fns[i].name;
+				e.id = fns[i];
+				return "";
+			}
+		}
+		return "pc out of bounds";
+	}
+	return "no symbol file";
+}
+
+Exp.src(e: self ref Exp): ref Src
+{
+	m := e.m;
+	if(e.id == nil || m.sym == nil)
+		return nil;
+	src := e.id.src;
+	if(src != nil)
+		return src;
+	if(e.id.t.kind == Tfn && !m.comp && e.pc < len m.sym.src && e.pc >= 0)
+		return m.sym.src[e.pc];
+	return nil;
+}
+
+Type.getkind(t: self ref Type, sym: ref Sym): int
+{
+	if(t == nil)
+		return -1;
+	if(t.kind == Tid)
+		return sym.adts[int t.name].getkind(sym);
+	return t.kind;
+}
+
+Type.text(t: self ref Type, sym: ref Sym): string
+{
+	if (t == nil)
+		return "no type";
+	s := typenames[t.kind];
+	case t.kind {
+	Tadt or
+	Tadtpick or
+	Tmodule =>
+		s = t.name;
+	Tid =>
+		return sym.adts[int t.name].text(sym);
+	Tarray or
+	Tlist or
+	Tchan or
+	Tslice =>
+		s += " of " + t.Of.text(sym);
+	Tref =>
+		s += " " + t.Of.text(sym);
+	Tfn =>
+		s += "(";	
+		for(i := 0; i < len t.ids; i++)
+			s += t.ids[i].name + ": " + t.ids[i].t.text(sym);
+		s += "): " + t.Of.text(sym);
+	Ttuple or
+	Tlocal or
+	Tglobal or
+	Targ =>
+		if(t.kind == Ttuple)
+			s = "";
+		s += "(";
+		for (i := 0; i < len t.ids; i++) {
+			s += t.ids[i].t.text(sym);
+			if (i < len t.ids - 1)
+				s += ", ";
+		}
+		s += ")";
+	}
+	return s;
+}
+
+Exp.typename(e: self ref Exp): string
+{
+	if (e.id == nil)
+		return "no info";
+	return e.id.t.text(e.m.sym);
+}
+
+Exp.kind(e: self ref Exp): int
+{
+	if(e.id == nil)
+		return -1;
+	return e.id.t.getkind(e.m.sym);
+}
+
+EXPLISTMAX : con	32;	# what's a good value for this ?
+
+Exp.expand(e: self ref Exp): array of ref Exp
+{
+	if(e.id == nil)
+		return nil;
+
+	t := e.id.t;
+	if(t.kind == Tid)
+		t = e.m.sym.adts[int t.name];
+
+	off := e.offset;
+	ids := t.ids;
+	case t.kind{
+	Tadt or Tfn or Targ or Tlocal or Ttuple =>
+		break;
+	Tadtpick =>
+		break;
+	Tglobal =>
+		ids = e.m.sym.vars;
+		off = e.m.data;
+	Tmodule =>
+		(s, err) := pdata(e.p, off, "M");
+		if(s == "nil" || err != "")
+			return nil;
+		off = hex(array of byte s);
+	Tref =>
+		(s, err) := pdata(e.p, off, "P");
+		if(s == "nil" || err != "")
+			return nil;
+		off = hex(array of byte s);
+		et := t.Of;
+		if(et.kind == Tid)
+			et = e.m.sym.adts[int et.name];
+		ids = et.ids;
+		if(et.kind == Tadtpick){
+			(s, err) = pdata(e.p, off, "W");
+			tg := int s;
+			if(tg < 0 || tg > len et.tags || err != "" )
+				return nil;
+			k := array[1 + len ids + len et.tags[tg].ids] of ref Exp;
+			k[0] = ref Exp(et.tags[tg].name, off+0, e.pc, e.m, e.p, ref Id(et.src, et.tags[tg].name, 0, 0, tint));
+			x := 1;
+			for(i := 0; i < len ids; i++){
+				id := ids[i];
+				k[i+x] = ref Exp(id.name, off+id.offset, e.pc, e.m, e.p, id);
+			}
+			x += len ids;
+			ids = et.tags[tg].ids;
+			for(i = 0; i < len ids; i++){
+				id := ids[i];
+				k[i+x] = ref Exp(id.name, off+id.offset, e.pc, e.m, e.p, id);
+			}
+			return k;
+		}
+	Tlist =>
+		(s, err) := pdata(e.p, off, "L");
+		if(err != "")
+			return nil;
+		(tloff, hdoff) := str->splitl(s, ".");
+		hdoff = hdoff[1:];
+		k := array[2] of ref Exp;
+		k[0] = ref Exp("hd", hex(array of byte hdoff), e.pc, e.m, e.p, ref Id(nil, "hd", H, H, t.Of));
+		k[1] = ref Exp("tl", hex(array of byte tloff), e.pc, e.m, e.p, ref Id(nil, "tl", H, H, t));
+		return k;
+	Tarray =>
+		(s, nil) := pdata(e.p, e.offset, "A");
+		if(s == "nil")
+			return nil;
+		(sn, sa) := str->splitl(s, ".");
+		n := int sn;
+		if(sa == "" || n <= 0)
+			return nil;
+		(off, nil) = str->toint(sa[1:], 16);
+		et := t.Of;
+		if(et.kind == Tid)
+			et = e.m.sym.adts[int et.name];
+		esize := et.size;
+		if (n <= EXPLISTMAX || EXPLISTMAX == 0) {
+			k := array[n] of ref Exp;
+			for(i := 0; i < n; i++){
+				name := string i;
+				k[i] = ref Exp(name, off+i*esize, e.pc, e.m, e.p, ref Id(nil, name, H, H, et));
+			}
+			return k;
+		}
+		else {
+			# slice it
+			(p, q, r) := partition(n, EXPLISTMAX);
+			lb := 0;
+			k := array[p] of ref Exp;
+			st := ref Type(et.src, Tslice, 0, nil, et, nil, nil);
+			for (i := 0; i < p; i++){
+				ub := lb+q-1;
+				if (--r >= 0)
+					ub++;
+				name := string lb + ".." + string ub;
+				k[i] = ref Exp(name, off+lb*esize, e.pc, e.m, e.p, ref Id(nil, name, H, H, st));
+				lb = ub+1;
+			}
+			return k;	
+		}
+	Tslice =>
+		(lb, ub) := bounds(e.name);
+		if (lb > ub)
+			return nil;
+		n := ub-lb+1;
+		et := t.Of;
+		if(et.kind == Tid)
+			et = e.m.sym.adts[int et.name];
+		esize := et.size;
+		if (n <= EXPLISTMAX || EXPLISTMAX == 0) {
+			k := array[n] of ref Exp;
+			for(i := 0; i < n; i++){
+				name := string (i+lb);
+				k[i] = ref Exp(name, off+i*esize, e.pc, e.m, e.p, ref Id(nil, name, H, H, et));
+			}
+			return k;
+		}
+		else {
+			# slice it again
+			(p, q, r) := partition(n, EXPLISTMAX);
+			lb0 := lb;
+			k := array[p] of ref Exp;
+			st := ref Type(et.src, Tslice, 0, nil, et, nil, nil);
+			for (i := 0; i < p; i++){
+				ub = lb+q-1;
+				if (--r >= 0)
+					ub++;
+				name := string lb + ".." + string ub;
+				k[i] = ref Exp(name, off+(lb-lb0)*esize, e.pc, e.m, e.p, ref Id(nil, name, H, H, st));
+				lb = ub+1;
+			}
+			return k;
+		}	
+	Tchan =>
+		(s, nil) := pdata(e.p, e.offset, "c");
+		if(s == "nil")
+			return nil;
+		(sn, sa) := str->splitl(s, ".");
+		n := int sn;
+		if(sa == "" || n <= 0)
+			return nil;
+		(off, nil) = str->toint(sa[1:], 16);
+		(nil, sa) = str->splitl(sa[1:], ".");
+		(sn, sa) = str->splitl(sa[1:], ".");
+		f := int sn;
+		sz := int sa[1:];
+		et := t.Of;
+		if(et.kind == Tid)
+			et = e.m.sym.adts[int et.name];
+		esize := et.size;
+		k := array[sz] of ref Exp;
+		for(i := 0; i < sz; i++){
+			name := string i;
+			j := (f+i)%n;
+			k[i] = ref Exp(name, off+j*esize, e.pc, e.m, e.p, ref Id(nil, name, H, H, et));
+		}
+		return k;
+	* =>
+		return nil;
+	}
+	k := array[len ids] of ref Exp;
+	for(i := 0; i < len k; i++){
+		id := ids[i];
+		k[i] = ref Exp(id.name, off+id.offset, e.pc, e.m, e.p, id);
+	}
+	return k;
+}
+
+Exp.val(e: self ref Exp): (string, int)
+{
+	if(e.id == nil)
+		return (e.m.path+" unknown fn", 0);
+	t := e.id.t;
+	if(t.kind == Tid)
+		t = e.m.sym.adts[int t.name];
+
+	w := 0;
+	s := "";
+	err := "";
+	p := e.p;
+	case t.kind{
+	Tfn =>
+		if(t.ids != nil)
+			w = 1;
+		src := e.m.sym.src[e.pc];
+		ss := src.start.file+":"+string src.start.line+"."+string src.start.pos+", ";
+		if(src.stop.file != src.start.file)
+			ss += src.stop.file+":"+string src.stop.line+".";
+		else if(src.stop.line != src.start.line)
+			ss += string src.stop.line+".";
+		return (ss+string src.stop.pos, w);
+	Targ or Tlocal or Tglobal or Tadtpick or Ttuple =>
+		return ("", 1);
+	Tadt =>
+		return ("#" + string e.offset, 1);
+	Tnil =>
+		s = "nil";
+	Tbyte =>
+		(s, err) = pdata(p, e.offset, "B");
+	Tint =>
+		(s, err) = pdata(p, e.offset, "W");
+	Tbig =>
+		(s, err) = pdata(p, e.offset, "V");
+	Treal =>
+		(s, err) = pdata(p, e.offset, "R");
+	Tarray =>
+		(s, err) = pdata(p, e.offset, "A");
+		if(s == "nil")
+			break;
+		(n, a) := str->splitl(s, ".");
+		if(a == "")
+			return ("", 0);
+		s = "["+n+"] @"+a[1:];
+		w = 1;
+	Tslice =>
+		(lb, ub) := bounds(e.name);
+		s = sys->sprint("[:%d] @ %x", ub-lb+1, e.offset);
+		w = 1;
+	Tstring =>
+		n : int;
+		(n, s, err) = pstring(p, e.offset);
+		if(err != "")
+			return ("", 0);
+		for(i := 0; i < len s; i++)
+			if(s[i] == '\n')
+				s[i] = '\u008a';
+		s = "["+string n+"] \""+s+"\"";
+	Tref or Tlist or Tmodule or Tpoly=>
+		(s, err) = pdata(p, e.offset, "P");
+		if(s == "nil")
+			break;
+		s = "@" + s;
+		w = 1;
+	Tchan =>
+		(s, err) = pdata(p, e.offset, "c");
+		if(s == "nil")
+			break;
+		(n, a) := str->splitl(s, ".");
+		if(a == "")
+			return ("", 0);
+		if(n == "0"){
+			s = "@" + a[1:];
+			w = 0;
+		}
+		else{
+			(a, nil) = str->splitl(a[1:], ".");
+			s = "["+n+"] @"+a;
+			w = 1;
+		}
+	}
+	if(err != "")
+		return ("", 0);
+	return (s, w);
+}
+
+Sym.srctopc(s: self ref Sym, src: ref Src): int
+{
+	srcs := s.src;
+	line := src.start.line;
+	pos := src.start.pos;
+	(nil, file) := str->splitr(src.start.file, "/");
+	backup := -1;
+	delta := 80;
+	for(i := 0; i < len srcs; i++){
+		ss := srcs[i];
+		if(ss.start.file != file)
+			continue;
+		if(ss.start.line <= line && ss.start.pos <= pos
+		&& ss.stop.line >= line && ss.stop.pos >= pos)
+			return i;
+		d := ss.start.line - line;
+		if(d >= 0 && d < delta){
+			delta = d;
+			backup = i;
+		}
+	}
+	return backup;
+}
+
+Sym.pctosrc(s: self ref Sym, pc: int): ref Src
+{
+	if(pc < 0 || pc >= len s.src)
+		return nil;
+	return s.src[pc];
+}
+
+sym(sbl: string): (ref Sym, string)
+{
+	h := 0;
+	for(i := 0; i < len sbl; i++)
+		h = (h << 1) + sbl[i];
+	h &= SymHash - 1;
+	for(sl := syms[h]; sl != nil; sl = tl sl){
+		s := hd sl;
+		if(sbl == s.path)
+			return (s, "");
+	}
+	(sy, err) := loadsyms(sbl);
+	if(err != "")
+		return (nil, err);
+	syms[h] = sy :: syms[h];
+	return (sy, "");
+}
+
+Module.addsym(m: self ref Module, sym: ref Sym)
+{
+	m.sym = sym;
+}
+
+Module.sbl(m: self ref Module): string
+{
+	if(m.sym != nil)
+		return m.sym.path;
+	return "";
+}
+
+Module.dis(m: self ref Module): string
+{
+	return m.path;
+}
+
+findsbl(dis: string): string
+{
+	n  := len dis;
+	if(n <= 4 || dis[n-4: n] != ".dis")
+		dis += ".dis";
+	if(dism == nil){
+		dism = load Dis Dis->PATH;
+		if(dism != nil)
+			dism->init();
+	}
+	if(dism != nil && (b := dism->src(dis)) != nil){
+		n = len b;
+		if(n > 2 && b[n-2: n] == ".b"){
+			sbl := b[0: n-2] + ".sbl";
+			if(sys->open(sbl, Sys->OREAD) != nil)
+				return sbl;
+		}
+	}	
+	return nil;	
+}
+
+Module.stdsym(m: self ref Module)
+{
+	if(m.sym != nil)
+		return;
+	if((sbl := findsbl(m.path)) != nil){
+		(m.sym, nil) = sym(sbl);
+		return;
+	}
+	sbl = m.path;
+	n := len sbl;
+	if(n > 4 && sbl[n-4:n] == ".dis")
+		sbl = sbl[:n-4]+".sbl";
+	else
+		sbl = sbl+".sbl";
+	path := sbl;
+	fd := sys->open(sbl, sys->OREAD);
+	for(i := 0; fd == nil && i < len sblpath; i++){
+		(dis, src) := sblpath[i];
+		nd := len dis;
+		if(len sbl > nd && sbl[:nd] == dis){
+			path = src + sbl[nd:];
+			fd = sys->open(path, sys->OREAD);
+		}
+	}
+	if(fd == nil)
+		return;
+	(m.sym, nil) = sym(path);
+}
+
+mkmod(data, code, comp: int, dis: string): ref Module
+{
+	h := 0;
+	for(i := 0; i < len dis; i++)
+		h = (h << 1) + dis[i];
+	h &= ModHash - 1;
+	sym : ref Sym;
+	for(ml := mods[h]; ml != nil; ml = tl ml){
+		m := hd ml;
+		if(m.path == dis && m.code == code && m.comp == comp){
+			sym = m.sym;
+			if(m.data == data)
+				return m;
+		}
+	}
+	m := ref Module(dis, code, data, comp, sym);
+	mods[h] = m :: mods[h];
+	return m;
+}
+
+pdata(p: ref Prog, a: int, fmt: string): (string, string)
+{
+	b := array of byte sprint("0x%ux.%s1", a, fmt);
+	if(sys->write(p.heap, b, len b) != len b)
+		return ("", sprint("can't write heap: %r"));
+
+	buf := array[64] of byte;
+	sys->seek(p.heap, big 0, 0);
+	n := sys->read(p.heap, buf, len buf);
+	if(n <= 1)
+		return ("", sprint("can't read heap: %r"));
+	return (string buf[:n-1], "");
+}
+
+pstring0(p: ref Prog, a: int, blen: int): (int, string, string)
+{
+	b := array of byte sprint("0x%ux.C1", a);
+	if(sys->write(p.heap, b, len b) != len b)
+		return (-1, "", sprint("can't write heap: %r"));
+
+	buf := array[blen] of byte;
+	sys->seek(p.heap, big 0, 0);
+	n := sys->read(p.heap, buf, len buf-1);
+	if(n <= 1)
+		return (-1, "", sprint("can't read heap: %r"));
+	buf[n] = byte 0;
+	m := strchr(buf, 0, '.');
+	if(buf[m++] != byte '.')
+		m = 0;
+	return (int string buf[0:m], string buf[m:n], "");
+}
+
+pstring(p: ref Prog, a: int): (int, string, string)
+{
+	m, n: int;
+	s, err: string;
+
+	m = 64;
+	for(;;){
+		(n, s, err) = pstring0(p, a, m);
+		if(err != "" || n <= len s)
+			break;
+		# guard against broken devprog
+		if(m >= 3 * n)
+			return (-1, nil, "bad string");
+		m *= 2;
+	}
+	return (n, s, err);
+}
+
+Prog.status(p: self ref Prog): (int, string, string, string)
+{
+	fd := sys->open(sprint("/prog/%d/status", p.id), sys->OREAD);
+	if(fd == nil)
+		return (-1, "", sprint("can't open status file: %r"), "");
+	buf := array[256] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return (-1, "", sprint("can't read status file: %r"), "");
+	(ni, info) := sys->tokenize(string buf[:n], " \t");
+	if(ni != 6 && ni != 7)
+		return (-1, "", "can't parse status file", "");
+	info = tl info;
+	if(ni == 6)
+		return (int hd info, hd tl info, hd tl tl info, hd tl tl tl tl info);
+	return (int hd info, hd tl info, hd tl tl tl info, hd tl tl tl tl tl info);
+}
+
+loadsyms(sbl: string): (ref Sym, string)
+{
+	fd := sys->open(sbl, sys->OREAD);
+	if(fd == nil)
+		return (nil, sprint("Can't open symbol file '%s': %r", sbl));
+
+	(ok, dir) := sys->fstat(fd);
+	if(ok < 0)
+		return (nil, sprint("Can't read symbol file '%s': %r", sbl));
+	n := int dir.length;
+	buf := array[n+1] of byte;
+	if(sys->read(fd, buf, n) != n)
+		return (nil, sprint("Can't read symbol file '%s': %r", sbl));
+	fd = nil;
+	buf[n] = byte 0;
+
+	s := ref Sym;
+	s.path = sbl;
+
+	n = strchr(buf, 0, '\n');
+	vers := 0;
+	if(string buf[:n] == "limbo .sbl 1.")
+		vers = 10;
+	else if(string buf[:n] == "limbo .sbl 1.1")
+		vers = 11;
+	else if(string buf[:n] == "limbo .sbl 2.0")
+		vers = 20;
+	else if(string buf[:n] == "limbo .sbl 2.1")
+		vers = 21;
+	else
+		return (nil, "Symbol file "+sbl+" out of date");
+	o := n += 1;
+	n = strchr(buf, o, '\n');
+	if(buf[n] != byte '\n')
+		return (nil, "Corrupted symbol file "+sbl);
+	s.name = string buf[o:n++];
+	ss := ref SrcState(nil, 0, 0, vers);
+	err : string;
+	if(n >= 0){
+		err = "file";
+		n = debugfiles(ss, buf, n);
+	}
+	if(n >= 0){
+		err = "pc";
+		n = debugpc(ss, s, buf, n);
+	}
+	if(n >= 0){
+		err = "types";
+		n = debugtys(ss, s, buf, n);
+	}
+	if(n >= 0){
+		err = "fn";
+		n = debugfns(ss, s, buf, n);
+	}
+	vs: array of ref Id;
+	if(n >= 0){
+		err = "global";
+		(vs, n) = debugid(ss, buf, n);
+	}
+	if(n < 0)
+		return (nil, "Corrupted "+err+" symbol table in "+sbl);
+	s.vars = vs;
+	return (s, "");
+}
+
+#
+# parse a source location
+# format[file:][line.]pos,[file:][line.]pos' '
+#
+debugsrc(ss: ref SrcState, buf: array of byte, p: int): (ref Src, int)
+{
+	n: int;
+	src: ref Src;
+
+	(n, p) = strtoi(buf, p);
+	if(buf[p] == byte ':'){
+		ss.lastf = n;
+		(n, p) = strtoi(buf, p + 1);
+	}
+	if(buf[p] == byte '.'){
+		ss.lastl = n;
+		(n, p) = strtoi(buf, p + 1);
+	}
+	if(buf[p++] != byte ',' || ss.lastf >= len ss.files || ss.lastf < 0)
+		return (nil, -1);
+	src = ref Src;
+	src.start.file = ss.files[ss.lastf];
+	src.start.line = ss.lastl;
+	src.start.pos = n;
+
+	(n, p) = strtoi(buf, p);
+	if(buf[p] == byte ':'){
+		ss.lastf = n;
+		(n, p) = strtoi(buf, p+1);
+	}
+	if(buf[p] == byte '.'){
+		ss.lastl = n;
+		(n, p) = strtoi(buf, p + 1);
+	}
+	if(buf[p++] != byte ' ' || ss.lastf >= len ss.files || ss.lastf < 0)
+		return (nil, -1);
+	src.stop.file = ss.files[ss.lastf];
+	src.stop.line = ss.lastl;
+	src.stop.pos = n;
+	return (src, p);
+}
+
+#
+# parse the file table
+# item format: file: string
+#
+debugfiles(ss: ref SrcState, buf: array of byte, p: int): int
+{
+	n, q: int;
+
+	(n, p) = strtoi(buf, p);
+	if(buf[p++] != byte '\n')
+		return -1;
+	ss.files = array[n] of string;
+	for(i := 0; i < n; i++){
+		q = strchr(buf, p, '\n');
+		ss.files[i] = string buf[p:q];
+		p = q + 1;
+	}
+	return p;
+}
+
+#
+# parse the pc to source table
+# item format: Source stmt
+#
+debugpc(ss: ref SrcState, s: ref Sym, buf: array of byte, p: int): int
+{
+	ns: int;
+
+	(ns, p) = strtoi(buf, p);
+	if(buf[p++] != byte '\n')
+		return -1;
+	s.src = array[ns] of ref Src;
+	s.srcstmt = array[ns] of int;
+	for(i := 0; i < ns; i++){
+		(s.src[i], p) = debugsrc(ss, buf, p);
+		if(p < 0)
+			return -1;
+		(s.srcstmt[i], p) = strtoi(buf, p);
+		if(buf[p++] != byte '\n')
+			return -1;
+	}
+	return p;
+}
+
+#
+# parse the type table
+# format: linear list of types
+#
+debugtys(ss: ref SrcState, s: ref Sym, buf: array of byte, p: int): int
+{
+	na: int;
+
+	(na, p) = strtoi(buf, p);
+	if(buf[p++] != byte '\n')
+		return -1;
+	s.adts = array[na] of ref Type;
+	adts := s.adts;
+	for(i := 0; i < na; i++){
+		if(ss.vers < 20)
+			(adts[i], p) = debugadt(ss, buf, p);
+		else
+			(adts[i], p) = debugtype(ss, buf, p);
+		if(p < 0)
+			return -1;
+	}
+	return p;
+}
+
+#
+# parse the function table
+# format: pc:name:argids localids rettype
+#
+debugfns(ss: ref SrcState, s: ref Sym, buf: array of byte, p: int): int
+{
+	t: ref Type;
+	args, locals: array of ref Id;
+	nf, pc, q: int;
+
+	(nf, p) = strtoi(buf, p);
+	if(buf[p++] != byte '\n')
+		return -1;
+	s.fns = array[nf] of ref Id;
+	fns := s.fns;
+	for(i := 0; i < nf; i++){
+		(pc, p) = strtoi(buf, p);
+		if(buf[p++] != byte ':')
+			return -2;
+		q = strchr(buf, p, '\n');
+		if(buf[q] != byte '\n')
+			return -3;
+		name := string buf[p:q];
+		(args, p) = debugid(ss, buf, q + 1);
+		if(p == -1)
+			return -4;
+		(locals, p) = debugid(ss, buf, p);
+		if(p == -1)
+			return -5;
+		(t, p) = debugtype(ss, buf, p);
+		if(p == -1)
+			return -6;
+		nk := 1 + (len args != 0) + (len locals != 0);
+		kids := array[nk] of ref Id;
+		nk = 0;
+		if(len locals != 0)
+			kids[nk++] = ref Id(nil, "locals", 0, 0, ref Type(nil, Tlocal, 0, nil, nil, locals, nil));
+		if(len args != 0)
+			kids[nk++] = ref Id(nil, "args", 0, 0, ref Type(nil, Targ, 0, nil, nil, args, nil));
+		kids[nk++] = ref Id(nil, "module", 0, 0, ref Type(nil, Tglobal, 0, nil, nil, nil, nil));
+		args = nil;
+		locals = nil;
+		fns[i] = ref Id(nil, name, pc, 0, ref Type(nil, Tfn, 0, name, t, kids, nil));
+	}
+	for(i = 1; i < nf; i++)
+		fns[i-1].stoppc = fns[i].offset;
+	fns[i-1].stoppc = len s.src;
+	return p;
+}
+
+#
+# parse a list of ids
+# format: offset ':' name ':' src type '\n'
+#
+debugid(ss: ref SrcState, buf: array of byte, p: int): (array of ref Id, int)
+{
+	t: ref Type;
+	off, nd, q, qq, tq: int;
+	src: ref Src;
+
+	(nd, p) = strtoi(buf, p);
+	if(buf[p++] != byte '\n')
+		return (nil, -1);
+	d := array[nd] of ref Id;
+	for(i := 0; i < nd; i++){
+		(off, q) = strtoi(buf, p);
+		if(buf[q++] != byte ':')
+			return (nil, -1);
+		qq = strchr(buf, q, ':');
+		if(buf[qq] != byte ':')
+			return (nil, -1);
+		tq = qq + 1;
+		if(ss.vers > 10){
+			(src, tq) = debugsrc(ss, buf, tq);
+			if(tq < 0)
+				return (nil, -1);
+		}
+		(t, p) = debugtype(ss, buf, tq);
+		if(p == -1 || buf[p++] != byte '\n')
+			return (nil, -1);
+		d[i] = ref Id(src, string buf[q:qq], off, 0, t);
+	}
+	return (d, p);
+}
+
+idlist(a: array of ref Id): list of ref Id
+{
+	n := len a;
+	ids : list of ref Id = nil;
+	while(n-- > 0)
+		ids = a[n] :: ids;
+	return ids;
+}
+
+#
+# parse a type description
+#
+debugtype(ss: ref SrcState, buf: array of byte, p: int): (ref Type, int)
+{
+	t: ref Type;
+	d: array of ref Id;
+	q, k: int;
+	src: ref Src;
+
+	size := 0;
+	case int buf[p++]{
+	'@' =>
+		k = Tid;
+	'A' =>
+		k = Tarray;
+		size = IBY2WD;
+	'B' =>
+		return (tbig, p);
+	'C' =>	k = Tchan;
+		size = IBY2WD;
+	'L' =>
+		k = Tlist;
+		size = IBY2WD;
+	'N' =>
+		return (tnil, p);
+	'R' =>
+		k = Tref;
+		size = IBY2WD;
+	'a' =>
+		k = Tadt;
+		if(ss.vers < 20)
+			size = -1;
+	'b' =>
+		return (tbyte, p);
+	'f' =>
+		return (treal, p);
+	'i' =>
+		return (tint, p);
+	'm' =>
+		k = Tmodule;
+		size = IBY2WD;
+	'n' =>
+		return (tnone, p);
+	'p' =>
+		k = Tadtpick;
+	's' =>
+		return (tstring, p);
+	't' =>
+		k = Ttuple;
+	 	size = -1;
+	'F' =>
+		k = Tfn;
+		size = IBY2WD;
+	'P' =>
+		return (tpoly, p);
+	* =>
+		k = Terror;
+	}
+
+	if(size == -1){
+		q = strchr(buf, p, '.');
+		if(buf[q] == byte '.'){
+			size = int string buf[p:q];
+			p = q+1;
+		}
+	}
+
+	case k{
+	Tid =>
+		q = strchr(buf, p, '\n');
+		if(buf[q] != byte '\n')
+			return (nil, -1);
+		t = ref Type(nil, Tid, -1, string buf[p:q], nil, nil, nil);
+		p = q + 1;
+	Tadt =>
+		if(ss.vers < 20){
+			q = strchr(buf, p, '\n');
+			if(buf[q] != byte '\n')
+				return (nil, -1);
+			t = ref Type(nil, Tid, size, string buf[p:q], nil, nil, nil);
+			p = q + 1;
+		}else
+			(t, p) = debugadt(ss, buf, p);
+	Tadtpick =>
+		(t, p) = debugadt(ss, buf, p);
+		t.kind = Tadtpick;
+		(t.tags, p) = debugtag(ss, buf, p);
+	Tmodule =>
+		q = strchr(buf, p, '\n');
+		if(buf[q] != byte '\n')
+			return (nil, -1);
+		t = ref Type(nil, k, size, string buf[p:q], nil, nil, nil);
+		p = q + 1;
+		if(ss.vers > 10){
+			(src, p) = debugsrc(ss, buf, p);
+			t.src = src;
+		}
+		if(ss.vers > 20)
+			(t.ids, p) = debugid(ss, buf, p);
+	Tref or Tarray or Tlist or Tchan =>		# ref, array, list, chan
+		(t, p) = debugtype(ss, buf, p);
+		t = ref Type(nil, k, size, "", t, nil, nil);
+
+	Ttuple =>						# tuple
+		(d, p) = debugid(ss, buf, p);
+		t = ref Type(nil, k, size, "", nil, d, nil);
+
+	Tfn =>						# fn
+		(d, p) = debugid(ss, buf, p);
+		(t, p) = debugtype(ss, buf, p);
+		t = ref Type(nil, k, size, "", t, d, nil);
+
+	* =>
+		p = -1;
+	}
+	return (t, p);
+}
+
+#
+# parse an adt type spec
+# format: name ' ' src size '\n' ids
+#
+debugadt(ss: ref SrcState, buf: array of byte, p: int): (ref Type, int)
+{
+	src: ref Src;
+
+	q := strchr(buf, p, ' ');
+	if(buf[q] != byte ' ')
+		return (nil, -1);
+	sq := q + 1;
+	if(ss.vers > 10){
+		(src, sq) = debugsrc(ss, buf, sq);
+		if(sq < 0)
+			return (nil, -1);
+	}
+	qq := strchr(buf, sq, '\n');
+	if(buf[qq] != byte '\n')
+		return (nil, -1);
+	(d, pp) := debugid(ss, buf, qq + 1);
+	if(pp == -1)
+		return (nil, -1);
+	t := ref Type(src, Tadt, int string buf[sq:qq], string buf[p:q], nil, d, nil);
+	return (t, pp);
+}
+
+#
+# parse a list of tags
+# format:
+#	name ':' src size '\n' ids
+# or	
+#	name ':' src '\n'
+#
+debugtag(ss: ref SrcState, buf: array of byte, p: int): (array of ref Type, int)
+{
+	d: array of ref Id;
+	ntg, q, pp, np: int;
+	src: ref Src;
+
+	(ntg, p) = strtoi(buf, p);
+	if(buf[p++] != byte '\n')
+		return (nil, -1);
+	tg := array[ntg] of ref Type;
+	for(i := 0; i < ntg; i++){
+		pp = strchr(buf, p, ':');
+		if(buf[pp] != byte ':')
+			return (nil, -1);
+		q = pp + 1;
+		(src, q) = debugsrc(ss, buf, q);
+		if(q < 0)
+			return (nil, -1);
+		if(buf[q] == byte '\n'){
+			np = q + 1;
+			if(i <= 0)
+				return (nil, -1);
+			tg[i] = ref Type(src, Tadt, tg[i-1].size, string buf[p:pp], nil, tg[i-1].ids, nil);
+		}else{
+			np = strchr(buf, q, '\n');
+			if(buf[np] != byte '\n')
+				return (nil, -1);
+			size := int string buf[q:np];
+			(d, np) = debugid(ss, buf, np+1);
+			if(np == -1)
+				return (nil, -1);
+			tg[i] = ref Type(src, Tadt, size, string buf[p:pp], nil, d, nil);
+		}
+		p = np;
+	}
+	return (tg, p);
+}
+
+strchr(a: array of byte, p, c: int): int
+{
+	bc := byte c;
+	while((b := a[p]) != byte 0 && b != bc)
+		p++;
+	return p;
+}
+
+strtoi(a: array of byte, start: int): (int, int)
+{
+	p := start;
+	for(; c := int a[p]; p++){
+		case c{
+		' ' or '\t' or '\n' or '\r' =>
+			continue;
+		}
+		break;
+	}
+
+	# sign
+	neg := c == '-';
+	if(neg || c == '+')
+		p++;
+
+	# digits
+	n := 0;
+	nn := 0;
+	ndig := 0;
+	over := 0;
+	for(; c = int a[p]; p++){
+		if(c < '0' || c > '9')
+			break;
+		ndig++;
+		nn = n * 10 + (c - '0');
+		if(nn < n)
+			over = 1;
+		n = nn;
+	}
+	if(ndig == 0)
+		return (0, start);
+	if(neg)
+		n = -n;
+	if(over)
+		if(neg)
+			n = 2147483647;
+		else
+			n = int -2147483648;
+	return (n, p);
+}
+
+hex(a: array of byte): int
+{
+	n := 0;
+	for(i := 0; i < len a; i++){
+		c := int a[i];
+		if(c >= '0' && c <= '9')
+			c -= '0';
+		else
+			c -= 'a' - 10;
+		n = (n << 4) + (c & 15);
+	}
+	return n;
+}
+
+partition(n : int, max : int) : (int, int, int)
+{
+	p := n/max; 
+	if (n%max != 0)
+		p++;
+	if (p > max)
+		p = max;
+	q := n/p;
+	r := n-p*q;
+	return (p, q, r);
+}
+
+bounds(s : string) : (int, int)
+{
+	lb := int s;
+	for (i := 0; i < len s; i++)
+		if (s[i] == '.')
+			break;
+	if (i+1 >= len s || s[i] != '.' || s[i+1] != '.')
+		return (1, 0);
+	ub := int s[i+2:];
+	return (lb, ub);
+}
--- /dev/null
+++ b/appl/lib/deflate.b
@@ -1,0 +1,1453 @@
+# gzip-compatible compression filter.
+
+implement Filter;
+
+include "sys.m";
+	sys:	Sys;
+
+include "filter.m";
+
+GZMAGIC1:	con byte 16r1f;
+GZMAGIC2:	con byte 16r8b;
+
+GZDEFLATE:	con byte 8;
+
+GZFTEXT:	con byte 1 << 0;		# file is text
+GZFHCRC:	con byte 1 << 1;		# crc of header included
+GZFEXTRA:	con byte 1 << 2;		# extra header included
+GZFNAME:	con byte 1 << 3;		# name of file included
+GZFCOMMENT:	con byte 1 << 4;		# header comment included
+GZFMASK:	con (byte 1 << 5) - byte 1;	# mask of specified bits
+
+GZXFAST:	con byte 2;			# used fast algorithm little compression
+GZXBEST:	con byte 4;			# used maximum compression algorithm
+
+GZOSFAT:	con byte 0;			# FAT file system
+GZOSAMIGA:	con byte 1;			# Amiga
+GZOSVMS:	con byte 2;			# VMS or OpenVMS
+GZOSUNIX:	con byte 3;			# Unix
+GZOSVMCMS:	con byte 4;			# VM/CMS
+GZOSATARI:	con byte 5;			# Atari TOS
+GZOSHPFS:	con byte 6;			# HPFS file system
+GZOSMAC:	con byte 7;			# Macintosh
+GZOSZSYS:	con byte 8;			# Z-System
+GZOSCPM:	con byte 9;			# CP/M
+GZOSTOPS20:	con byte 10;			# TOPS-20
+GZOSNTFS:	con byte 11;			# NTFS file system
+GZOSQDOS:	con byte 12;			# QDOS
+GZOSACORN:	con byte 13;			# Acorn RISCOS
+GZOSUNK:	con byte 255;
+
+GZCRCPOLY:	con int 16redb88320;
+GZOSINFERNO:	con GZOSUNIX;
+
+
+Hnone, Hgzip, Hzlib: con iota;  # LZstate.headers
+LZstate: adt
+{
+	hist:		array of byte;		# [HistSize];
+	epos:		int;			# end of history buffer
+	pos:		int;			# current location in history buffer
+	eof:		int;
+	hash:		array of int;		# [Nhash] hash chains
+	nexts:		array of int;		# [MaxOff]
+	me:		int;			# pos in hash chains
+	dot:		int;			# dawn of time in history
+	prevlen:	int;			# lazy matching state
+	prevoff:	int;
+	maxchars:	int;			# compressor tuning
+	maxdefer:	int;
+	level:		int;
+
+	crctab: array of int;			# for gzip trailer
+	crc:		int;
+	tot:		int;
+	sum:		big;			# for zlib trailer
+	headers:	int;			# which header to print, if any
+
+	outbuf:		array of byte;		# current output buffer;
+	out:		int;			# current position in the output buffer
+	bits:		int;			# bit shift register
+	nbits:		int;
+
+	verbose:	int;
+	debug:		int;
+
+	lzb:		ref LZblock;
+	slop:		array of byte;
+	dlitlentab:	array of Huff;		# [Nlitlen]
+	dofftab:	array of Huff;		# [Noff];
+	hlitlentab:	array of Huff;		# [Nlitlen];
+	dyncode:	ref Dyncode;
+	hdyncode:	ref Dyncode;
+	c:		chan of ref Rq;
+	rc:		chan of int;
+};
+
+#
+# lempel-ziv compressed block
+#
+LZblock: adt
+{
+	litlen:		array of byte;			# [MaxUncBlock+1];
+	off:		array of int;			# [MaxUncBlock+1];
+	litlencount:	array of int;			# [Nlitlen];
+	offcount:	array of int;			# [Noff];
+	entries:	int;				# entries in litlen & off tables
+	bytes:		int;				# consumed from the input
+	excost:		int;				# cost of encoding extra len & off bits
+};
+
+#
+# encoding of dynamic huffman trees
+#
+Dyncode: adt
+{
+	nlit:		int;
+	noff:		int;
+	nclen:		int;
+	ncode:		int;
+	codetab:	array of Huff;		# [Nclen];
+	codes:		array of byte;		# [Nlitlen+Noff];
+	codeaux:	array of byte;		# [Nlitlen+Noff];
+};
+
+#
+# huffman code table
+#
+Huff: adt
+{
+	bits:		int;				# length of the code
+	encode:		int;				# the code
+};
+
+DeflateBlock:	con 64*1024-258-1;
+DeflateOut:	con 258+10;
+
+
+DeflateUnc:	con 0;			# uncompressed block
+DeflateFix:	con 1;			# fixed huffman codes
+DeflateDyn:	con 2;			# dynamic huffman codes
+
+DeflateEob:	con 256;		# end of block code in lit/len book
+
+LenStart:	con 257;		# start of length codes in litlen
+Nlitlen:	con 288;		# number of litlen codes
+Noff:		con 30;			# number of offset codes
+Nclen:		con 19;			# number of codelen codes
+
+MaxLeaf:	con Nlitlen;
+MaxHuffBits:	con 15;			# max bits in a huffman code
+ChainMem:	con 2 * MaxHuffBits * (MaxHuffBits + 1);
+
+MaxUncBlock:	con 64*1024-1;		# maximum size of uncompressed block
+
+MaxOff:		con 32*1024;
+MinMatch:	con 3;			# shortest match possible
+MaxMatch:	con 258;		# longest match possible
+MinMatchMaxOff:	con 4096;		# max profitable offset for small match;
+					#  assumes 8 bits for len; 5+10 for offset
+HistSlop:	con 4096;		# slop for fewer calls to lzcomp
+HistSize:	con MaxOff + 2*HistSlop;
+
+Hshift:		con 4;			# nice compromise between space & time
+Nhash:		con 1<<(Hshift*MinMatch);
+Hmask:		con Nhash-1;
+
+MaxOffCode:	con 256;		# biggest offset looked up in direct table
+
+EstLitBits:	con 8;
+EstLenBits:	con 4;
+EstOffBits:	con 5;
+
+# conversion from len to code word
+lencode := array[MaxMatch] of int;
+
+#
+# conversion from off to code word
+# off <= MaxOffCode ? offcode[off] : bigoffcode[(off-1) >> 7]
+#
+offcode := array[MaxOffCode + 1] of int;
+bigoffcode := array[256] of int;
+
+# litlen code words LenStart-285 extra bits
+litlenbase := array[Nlitlen-LenStart] of int;
+litlenextra := array[Nlitlen-LenStart] of
+{
+	0, 0, 0,
+	0, 0, 0, 0, 0, 1, 1, 1, 1, 2,
+	2, 2, 2, 3, 3, 3, 3, 4, 4, 4,
+	4, 5, 5, 5, 5, 0, 0, 0
+};
+
+# offset code word extra bits
+offbase := array[Noff] of int;
+offextra := array[] of
+{
+	0,  0,  0,  0,  1,  1,  2,  2,  3,  3,
+	4,  4,  5,  5,  6,  6,  7,  7,  8,  8,
+	9,  9,  10, 10, 11, 11, 12, 12, 13, 13,
+	0,  0,
+};
+
+# order code lengths
+clenorder := array[Nclen] of
+{
+        16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
+};
+
+# static huffman tables
+litlentab : array of Huff;
+offtab : array of Huff;
+hofftab : array of Huff;
+
+# bit reversal for brain dead endian swap in huffman codes
+revtab: array of byte;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+
+	bitcount := array[MaxHuffBits] of int;
+	i, j, ci, n: int;
+
+	# byte reverse table
+	revtab = array[256] of byte;
+	for(i=0; i<256; i++){
+		revtab[i] = byte 0;
+		for(j=0; j<8; j++)
+			if(i & (1<<j))
+				revtab[i] |= byte 16r80 >> j;
+	}
+
+	litlentab = array[Nlitlen] of Huff;
+	offtab = array[Noff] of Huff;
+	hofftab = array[Noff] of { * => Huff(0, 0) };
+
+	# static Litlen bit lengths
+	for(i=0; i<144; i++)
+		litlentab[i].bits = 8;
+	for(i=144; i<256; i++)
+		litlentab[i].bits = 9;
+	for(i=256; i<280; i++)
+		litlentab[i].bits = 7;
+	for(i=280; i<Nlitlen; i++)
+		litlentab[i].bits = 8;
+
+	for(i = 0; i < 10; i++)
+		bitcount[i] = 0;
+	bitcount[8] += 144 - 0;
+	bitcount[9] += 256 - 144;
+	bitcount[7] += 280 - 256;
+	bitcount[8] += Nlitlen - 280;
+
+	hufftabinit(litlentab, Nlitlen, bitcount, 9);
+
+	# static offset bit lengths
+	for(i = 0; i < Noff; i++)
+		offtab[i].bits = 5;
+
+	for(i = 0; i < 5; i++)
+		bitcount[i] = 0;
+	bitcount[5] = Noff;
+
+	hufftabinit(offtab, Noff, bitcount, 5);
+
+	bitcount[0] = 0;
+	bitcount[1] = 0;
+	mkprecode(hofftab, bitcount, 2, MaxHuffBits);
+
+	# conversion tables for lens & offs to codes
+	ci = 0;
+	for(i = LenStart; i < 286; i++){
+		n = ci + (1 << litlenextra[i - LenStart]);
+		litlenbase[i - LenStart] = ci;
+		for(; ci < n; ci++)
+			lencode[ci] = i;
+	}
+	# patch up special case for len MaxMatch
+	lencode[MaxMatch-MinMatch] = 285;
+	litlenbase[285-LenStart] = MaxMatch-MinMatch;
+
+	ci = 1;
+	for(i = 0; i < 16; i++){
+		n = ci + (1 << offextra[i]);
+		offbase[i] = ci;
+		for(; ci < n; ci++)
+			offcode[ci] = i;
+	}
+
+	ci = (LenStart - 1) >> 7;
+	for(; i < 30; i++){
+		n = ci + (1 << (offextra[i] - 7));
+		offbase[i] = (ci << 7) + 1;
+		for(; ci < n; ci++)
+			bigoffcode[ci] = i;
+	}
+}
+
+start(param: string): chan of ref Rq
+{
+	# param contains flags:
+	# [0-9] - compression level
+	# h gzip header/trailer
+	# z zlib header/trailer
+	# v verbose
+	# d debug
+	lz := ref LZstate;
+	lz.level = 6;
+	lz.verbose = lz.debug = 0;
+	lz.headers = Hnone;
+	lz.crc = lz.tot = 0;
+	lz.sum = big 1;
+	# XXX could also put filename and modification time in param
+	for (i := 0; i < len param; i++) {
+		case param[i] {
+		'0' to '9' =>
+			lz.level = param[i] - '0';
+		'v' =>
+			lz.verbose = 1;
+		'h' =>
+			lz.headers = Hgzip;
+		'z' =>
+			lz.headers = Hzlib;
+		'd' =>
+			lz.debug = 1;
+		}
+	}
+	
+	lz.hist = array[HistSize] of byte;
+	lz.hash = array[Nhash] of int;
+	lz.nexts = array[MaxOff] of int;
+	lz.slop = array[2*MaxMatch] of byte;
+	lz.dlitlentab = array[Nlitlen] of Huff;
+	lz.dofftab = array[Noff] of Huff;
+	lz.hlitlentab = array[Nlitlen] of Huff;
+
+	lz.lzb = ref LZblock;
+	lzb := lz.lzb;
+	lzb.litlen = array[MaxUncBlock+1] of byte;
+	lzb.off = array[MaxUncBlock+1] of int;
+	lzb.litlencount = array[Nlitlen] of int;
+	lzb.offcount = array[Noff] of int;
+
+	lz.dyncode = ref Dyncode;
+	lz.dyncode.codetab =array[Nclen] of Huff;
+	lz.dyncode.codes =array[Nlitlen+Noff] of byte;
+	lz.dyncode.codeaux = array[Nlitlen+Noff] of byte;
+	lz.hdyncode = ref Dyncode;
+	lz.hdyncode.codetab =array[Nclen] of Huff;
+	lz.hdyncode.codes =array[Nlitlen+Noff] of byte;
+	lz.hdyncode.codeaux = array[Nlitlen+Noff] of byte;
+
+	for(i = 0; i < MaxOff; i++)
+		lz.nexts[i] = 0;
+	for(i = 0; i < Nhash; i++)
+		lz.hash[i] = 0;
+	lz.pos = 0;
+	lz.epos = 0;
+	lz.prevlen = MinMatch - 1;
+	lz.prevoff = 0;
+	lz.eof = 0;
+	lz.me = 4 * MaxOff;
+	lz.dot = lz.me;
+	lz.bits = 0;
+	lz.nbits = 0;
+	if(lz.level < 5) {
+		lz.maxchars = 1;
+		lz.maxdefer = 0;
+	} else if(lz.level == 9) {
+		lz.maxchars = 4000;
+		lz.maxdefer = MaxMatch;
+	} else {
+		lz.maxchars = 200;
+		lz.maxdefer = MaxMatch / 4;
+	}
+	if (lz.headers == Hgzip)
+		lz.crctab = mkcrctab(GZCRCPOLY);
+	lz.c = chan of ref Rq;
+	lz.rc = chan of int;
+	spawn deflate(lz);
+	return lz.c;
+}
+
+# return (eof, nbytes)
+fillbuf(lz: ref LZstate, buf: array of byte): (int, int)
+{
+	n := 0;
+	while (n < len buf) {
+		lz.c <-= ref Rq.Fill(buf[n:], lz.rc);
+		nr := <-lz.rc;
+		if (nr == -1)
+			exit;
+		if (nr == 0)
+			return (1, n);
+		n += nr;
+	}
+	return (0, n);
+}
+
+deflate(lz: ref LZstate)
+{
+	lz.c <-= ref Rq.Start(sys->pctl(0, nil));
+
+	header(lz);
+	buf := array[DeflateBlock] of byte;
+	out := array[DeflateBlock + DeflateOut] of byte;
+	eof := 0;
+	for (;;) {
+		nslop := lz.epos - lz.pos;
+		nbuf := 0;
+		if (!eof) {
+			(eof, nbuf) = fillbuf(lz, buf);
+			inblock(lz, buf[0:nbuf]);
+		}
+		if(eof && nbuf == 0 && nslop == 0) {
+			if(lz.nbits) {
+				out[0] = byte lz.bits;
+				lz.nbits = 0;
+				lz.c <-= ref Rq.Result(out[0:1], lz.rc);
+				if (<-lz.rc == -1)
+					exit;
+				continue;
+			}
+			footer(lz);
+			lz.c <-= ref Rq.Finished(nil);
+			exit;
+		}
+
+		lz.outbuf = out;
+
+		if(nslop > 2*MaxMatch) {
+			lz.c <-= ref Rq.Error(sys->sprint("slop too large: %d", nslop));
+			exit;
+		}
+		lz.slop[0:] = lz.hist[lz.pos:lz.epos];	# memmove(slop, lz.pos, nslop);
+	
+		lzb := lz.lzb;
+		for(i := 0; i < Nlitlen; i++)
+			lzb.litlencount[i] = 0;
+		for(i = 0; i < Noff; i++)
+			lzb.offcount[i] = 0;
+		lzb.litlencount[DeflateEob]++;
+	
+		lzb.bytes = 0;
+		lzb.entries = 0;
+		lzb.excost = 0;
+		lz.eof = 0;
+	
+		n := 0;
+		while(n < nbuf || eof && !lz.eof){
+			if(!lz.eof) {
+				if(lz.pos >= MaxOff + HistSlop) {
+					lz.pos -= MaxOff + HistSlop;
+					lz.epos -= MaxOff + HistSlop;
+					lz.hist[:] = lz.hist[MaxOff + HistSlop: MaxOff + HistSlop + lz.epos];
+				}
+				m := HistSlop - (lz.epos - lz.pos);
+				if(lz.epos + m > HistSize) {
+					lz.c <-= ref Rq.Error("read too long");
+					exit;
+				}
+				if(m >= nbuf - n) {
+					m = nbuf - n;
+					lz.eof = eof;
+				}
+				lz.hist[lz.epos:] = buf[n:n+m];
+				n += m;
+				lz.epos += m;
+			}
+			lzcomp(lz, lzb, lz.epos - lz.pos);
+		}
+	
+		lz.outbuf = out;
+		lz.out = 0;
+	
+		nunc := lzb.bytes;
+		if(nunc < nslop)
+			nslop = nunc;
+	
+		mkprecode(lz.dlitlentab, lzb.litlencount, Nlitlen, MaxHuffBits);
+		mkprecode(lz.dofftab, lzb.offcount, Noff, MaxHuffBits);
+			
+		ndyn := huffcodes(lz.dyncode, lz.dlitlentab, lz.dofftab)
+			+ bitcost(lz.dlitlentab, lzb.litlencount, Nlitlen)
+			+ bitcost(lz.dofftab, lzb.offcount, Noff)
+			+ lzb.excost;
+	
+		litcount := array[Nlitlen] of int;
+		for(i = 0; i < Nlitlen; i++)
+			litcount[i] = 0;
+		for(i = 0; i < nslop; i++)
+			litcount[int lz.slop[i]]++;
+		for(i = 0; i < nunc-nslop; i++)
+			litcount[int buf[i]]++;
+		litcount[DeflateEob]++;
+	
+		mkprecode(lz.hlitlentab, litcount, Nlitlen, MaxHuffBits);
+		nhuff := huffcodes(lz.hdyncode, lz.hlitlentab, hofftab)
+			+ bitcost(lz.hlitlentab, litcount, Nlitlen);
+	
+		nfix := bitcost(litlentab, lzb.litlencount, Nlitlen)
+			+ bitcost(offtab, lzb.offcount, Noff)
+			+ lzb.excost;
+	
+		lzput(lz, lz.eof && lz.pos == lz.epos, 1);
+	
+		if(lz.verbose) {
+			lz.c <-= ref Rq.Info(sys->sprint("block: %d bytes %d entries %d extra bits",
+						nunc, lzb.entries, lzb.excost));
+			lz.c <-= ref Rq.Info(sys->sprint("\tuncompressed %d fixed %d dynamic %d huffman %d",
+				(nunc + 4) * 8, nfix, ndyn, nhuff));
+		}
+	
+		if((nunc + 4) * 8 < ndyn && (nunc + 4) * 8 < nfix && (nunc + 4) * 8 < nhuff) {
+			lzput(lz, DeflateUnc, 2);
+			lzflushbits(lz);
+	
+			lz.outbuf[lz.out++] = byte(nunc);
+			lz.outbuf[lz.out++] = byte(nunc >> 8);
+			lz.outbuf[lz.out++] = byte(~nunc);
+			lz.outbuf[lz.out++] = byte(~nunc >> 8);
+	
+			lz.outbuf[lz.out:] = lz.slop[:nslop];
+			lz.out += nslop;
+			lz.outbuf[lz.out:] = buf[:nunc - nslop];
+			lz.out += nunc - nslop;
+		} else if(ndyn < nfix && ndyn < nhuff) {
+			lzput(lz, DeflateDyn, 2);
+	
+			wrdyncode(lz, lz.dyncode);
+			wrblock(lz, lzb.entries, lzb.litlen, lzb.off, lz.dlitlentab, lz.dofftab);
+			lzput(lz, lz.dlitlentab[DeflateEob].encode, lz.dlitlentab[DeflateEob].bits);
+		} else if(nhuff < nfix){
+			lzput(lz, DeflateDyn, 2);
+	
+			wrdyncode(lz, lz.hdyncode);
+			for(i = 0; i < len lzb.off; i++)
+				lzb.off[i] = 0;
+	
+			wrblock(lz, nslop, lz.slop, lzb.off, lz.hlitlentab, hofftab);
+			wrblock(lz, nunc-nslop, buf, lzb.off, lz.hlitlentab, hofftab);
+			lzput(lz, lz.hlitlentab[DeflateEob].encode, lz.hlitlentab[DeflateEob].bits);
+		} else {
+			lzput(lz, DeflateFix, 2);
+	
+			wrblock(lz, lzb.entries, lzb.litlen, lzb.off, litlentab, offtab);
+			lzput(lz, litlentab[DeflateEob].encode, litlentab[DeflateEob].bits);
+		}
+
+		lz.c <-= ref Rq.Result(out[0:lz.out], lz.rc);
+		if (<-lz.rc == -1)
+			exit;
+	}
+}
+
+headergzip(lz: ref LZstate)
+{
+	buf := array[20] of byte;
+	i := 0;
+	buf[i++] = byte GZMAGIC1;
+	buf[i++] = byte GZMAGIC2;
+	buf[i++] = byte GZDEFLATE;
+
+	flags := 0;
+	#if(file != nil)
+	#	flags |= GZFNAME;
+	buf[i++] = byte flags;
+
+	mtime := 0;
+	buf[i++] = byte(mtime);
+	buf[i++] = byte(mtime>>8);
+	buf[i++] = byte(mtime>>16);
+	buf[i++] = byte(mtime>>24);
+
+	buf[i++] = byte 0;
+	buf[i++] = byte GZOSINFERNO;
+
+	#if((flags & GZFNAME) == GZFNAME){
+	#	bout.puts(file);
+	#	bout.putb(byte 0);
+	#}
+	lz.c <-= ref Rq.Result(buf[0:i], lz.rc);
+	if (<-lz.rc == -1)
+		exit;
+}
+
+headerzlib(lz: ref LZstate)
+{
+	CIshift:	con 12;
+	CMdeflate:	con 8;
+	CMshift:	con 8;
+	LVshift:	con 6;
+	LVfastest, LVfast, LVnormal, LVbest: con iota;
+
+	level := LVnormal;
+	if(lz.level < 6)
+		level = LVfastest;
+	else if(lz.level >= 9)
+		level = LVbest;
+
+	h := 0;
+	h |= 7<<CIshift; # value is: (log2 of window size)-8
+	h |= CMdeflate<<CMshift;
+	h |= level<<LVshift;
+	h += 31-(h%31);
+
+	buf := array[2] of byte;
+	buf[0] = byte (h>>8);
+	buf[1] = byte (h>>0);
+
+	lz.c <-= ref Rq.Result(buf, lz.rc);
+	if (<-lz.rc == -1)
+		exit;
+}
+
+header(lz: ref LZstate)
+{
+	case lz.headers {
+	Hgzip =>	headergzip(lz);
+	Hzlib =>	headerzlib(lz);
+	}
+}
+
+footergzip(lz: ref LZstate)
+{
+	buf := array[8] of byte;
+	i := 0;
+	buf[i++] = byte(lz.crc);
+	buf[i++] = byte(lz.crc>>8);
+	buf[i++] = byte(lz.crc>>16);
+	buf[i++] = byte(lz.crc>>24);
+
+	buf[i++] = byte(lz.tot);
+	buf[i++] = byte(lz.tot>>8);
+	buf[i++] = byte(lz.tot>>16);
+	buf[i++] = byte(lz.tot>>24);
+	lz.c <-= ref Rq.Result(buf[0:i], lz.rc);
+	if (<-lz.rc == -1)
+		exit;
+}
+
+footerzlib(lz: ref LZstate)
+{
+        buf := array[4] of byte;
+	i := 0;
+        buf[i++] = byte (lz.sum>>24);
+        buf[i++] = byte (lz.sum>>16);
+        buf[i++] = byte (lz.sum>>8);
+        buf[i++] = byte (lz.sum>>0);
+
+	lz.c <-= ref Rq.Result(buf, lz.rc);
+	if(<-lz.rc == -1)
+		exit;
+}
+
+footer(lz: ref LZstate)
+{
+	case lz.headers {
+	Hgzip =>	footergzip(lz);
+	Hzlib =>	footerzlib(lz);
+	}
+}
+
+lzput(lz: ref LZstate, bits, nbits: int): int
+{
+	bits = (bits << lz.nbits) | lz.bits;
+	for(nbits += lz.nbits; nbits >= 8; nbits -= 8){
+		lz.outbuf[lz.out++] = byte bits;
+		bits >>= 8;
+	}
+	lz.bits = bits;
+	lz.nbits = nbits;
+	return 0;
+}
+
+lzflushbits(lz: ref LZstate): int
+{
+	if(lz.nbits & 7)
+		lzput(lz, 0, 8 - (lz.nbits & 7));
+	return 0;
+}
+
+#
+# write out a block of n samples,
+# given lz encoding and counts for huffman tables
+# todo: inline lzput
+#
+wrblock(lz: ref LZstate, n: int, litlen: array of byte, off: array of int, litlentab, offtab: array of Huff): int
+{
+	for(i := 0; i < n; i++) {
+		offset := off[i];
+		lit := int litlen[i];
+		if(lz.debug) {
+			if(offset == 0)
+				lz.c <-= ref Rq.Info(sys->sprint("\tlit %.2ux %c", lit, lit));
+			else
+				lz.c <-= ref Rq.Info(sys->sprint("\t<%d, %d>", offset, lit + MinMatch));
+		}
+		if(offset == 0)
+			lzput(lz, litlentab[lit].encode, litlentab[lit].bits);
+		else {
+			c := lencode[lit];
+			lzput(lz, litlentab[c].encode, litlentab[c].bits);
+			c -= LenStart;
+			if(litlenextra[c])
+				lzput(lz, lit - litlenbase[c], litlenextra[c]);
+
+			if(offset <= MaxOffCode)
+				c = offcode[offset];
+			else
+				c = bigoffcode[(offset - 1) >> 7];
+			lzput(lz, offtab[c].encode, offtab[c].bits);
+			if(offextra[c])
+				lzput(lz, offset - offbase[c], offextra[c]);
+		}
+	}
+
+	return n;
+}
+
+lzcomp(lz: ref LZstate, lzb: ref LZblock, max: int)
+{
+	q, s, es, t: int;
+	you, m: int;
+
+#	hashcheck(lz, "start");
+
+	hist := lz.hist;
+	nexts := lz.nexts;
+	hash := lz.hash;
+	me := lz.me;
+
+	p := lz.pos;
+	ep := lz.epos;
+	if(p + max < ep)
+		ep = p + max;
+	if(lz.prevlen != MinMatch - 1)
+		p++;
+
+	#
+	# hash in the links for any hanging link positions,
+	# and calculate the hash for the current position.
+	#
+	n := MinMatch;
+	if(n > ep - p)
+		n = ep - p;
+	h := 0;
+	for(i := 0; i < n - 1; i++) {
+		m = me - ((MinMatch-1) - i);
+		if(m < lz.dot)
+			continue;
+		s = p - (me - m);
+		if(s < 0)
+			s += MaxOff + HistSlop;
+		h = hashit(s, hist);
+		for(you = hash[h]; me - you < me - m; you = nexts[you & (MaxOff-1)])
+			;
+		if(you == m)
+			continue;
+		nexts[m & (MaxOff-1)] = hash[h];
+		hash[h] = m;
+	}
+	for(i = 0; i < n; i++)
+		h = ((h << Hshift) ^ int hist[p+i]) & Hmask;
+
+	#
+	# me must point to the index in the next/prev arrays
+	# corresponding to p's position in the history
+	#
+	entries := lzb.entries;
+	litlencount := lzb.litlencount;
+	offcount := lzb.offcount;
+	litlen := lzb.litlen;
+	off := lzb.off;
+	prevlen := lz.prevlen;
+	prevoff := lz.prevoff;
+	maxdefer := lz.maxdefer;
+	maxchars := lz.maxchars;
+	excost := 0;
+	for(;;) {
+		es = p + MaxMatch;
+		if(es > ep) {
+			if(!lz.eof || ep != lz.epos || p >= ep)
+				break;
+			es = ep;
+		}
+
+		#
+		# look for the longest, closest string which
+		# matches what we are going to send.  the clever
+		# part here is looking for a string 1 longer than
+		# are previous best match.
+		#
+		runlen := prevlen;
+		m = 0;
+		chars := maxchars;
+	matchloop:
+		for(you = hash[h]; me-you <= MaxOff && chars > 0; you = nexts[you & (MaxOff-1)]) {
+			s = p + runlen;
+			if(s >= es)
+				break;
+			t = s - me + you;
+			if(t - runlen < 0)
+				t += MaxOff + HistSlop;
+			for(; s >= p; s--) {
+				if(hist[s] != hist[t]) {
+					chars -= p + runlen - s + 1;
+					continue matchloop;
+				}
+				t--;
+			}
+
+			#
+			# we have a new best match.
+			# extend it to it's maximum length
+			#
+			t += runlen + 2;
+			s += runlen + 2;
+			for(; s < es; s++) {
+				if(hist[s] != hist[t])
+					break;
+				t++;
+			}
+			runlen = s - p;
+			m = you;
+			if(s == es)
+				break;
+			if(runlen > 7)
+				chars >>= 1;
+			chars -= runlen;
+		}
+
+		#
+		# back out of small matches too far in the past
+		#
+		if(runlen == MinMatch && me - m >= MinMatchMaxOff) {
+			runlen = MinMatch - 1;
+			m = 0;
+		}
+
+		#
+		# record the encoding and increment counts for huffman trees
+		# if we get a match, defer selecting it until we check for
+		# a longer match at the next position.
+		#
+		if(prevlen >= runlen && prevlen != MinMatch - 1) {
+			#
+			# old match at least as good; use that one
+			#
+			n = prevlen - MinMatch;
+			litlen[entries] = byte n;
+			n = lencode[n];
+			litlencount[n]++;
+			excost += litlenextra[n - LenStart];
+
+			off[entries++] = prevoff;
+			if(prevoff <= MaxOffCode)
+				n = offcode[prevoff];
+			else
+				n = bigoffcode[(prevoff - 1) >> 7];
+			offcount[n]++;
+			excost += offextra[n];
+
+			runlen = prevlen - 1;
+			prevlen = MinMatch - 1;
+		} else if(runlen == MinMatch - 1) {
+			#
+			# no match; just put out the literal
+			#
+			n = int hist[p];
+			litlen[entries] = byte n;
+			litlencount[n]++;
+			off[entries++] = 0;
+			runlen = 1;
+		} else {
+			if(prevlen != MinMatch - 1) {
+				#
+				# longer match now. output previous literal,
+				# update current match, and try again
+				#
+				n = int hist[p - 1];
+				litlen[entries] = byte n;
+				litlencount[n]++;
+				off[entries++] = 0;
+			}
+
+			prevoff = me - m;
+
+			if(runlen < maxdefer) {
+				prevlen = runlen;
+				runlen = 1;
+			} else {
+				n = runlen - MinMatch;
+				litlen[entries] = byte n;
+				n = lencode[n];
+				litlencount[n]++;
+				excost += litlenextra[n - LenStart];
+
+				off[entries++] = prevoff;
+				if(prevoff <= MaxOffCode)
+					n = offcode[prevoff];
+				else
+					n = bigoffcode[(prevoff - 1) >> 7];
+				offcount[n]++;
+				excost += offextra[n];
+				prevlen = MinMatch - 1;
+			}
+		}
+
+		#
+		# update the hash for the newly matched data
+		# this is constructed so the link for the old
+		# match in this position must at the end of a chain,
+		# and will expire when this match is added, ie it will
+		# never be examined for by the match loop.
+		# add to the hash chain only if we have the real hash data.
+		#
+		for(q = p + runlen; p != q; p++) {
+			if(p + MinMatch <= ep) {
+				nexts[me & (MaxOff-1)] = hash[h];
+				hash[h] = me;
+				if(p + MinMatch < ep)
+					h = ((h << Hshift) ^ int hist[p + MinMatch]) & Hmask;
+			}
+			me++;
+		}
+	}
+
+	#
+	# we can just store away the lazy state and
+	# pick it up next time.  the last block will have eof
+	# so we won't have any pending matches
+	# however, we need to correct for how much we've encoded
+	#
+	if(prevlen != MinMatch - 1)
+		p--;
+
+	lzb.excost += excost;
+	lzb.bytes += p - lz.pos;
+	lzb.entries = entries;
+
+	lz.pos = p;
+	lz.me = me;
+	lz.prevlen = prevlen;
+	lz.prevoff = prevoff;
+
+#	hashcheck(lz, "stop");
+}
+
+#
+# check all the hash list invariants are really satisfied
+#
+hashcheck(lz: ref LZstate, where: string)
+{
+	s, age, a, you: int;
+
+	nexts := lz.nexts;
+	hash := lz.hash;
+	me := lz.me;
+	start := lz.pos;
+	if(lz.prevlen != MinMatch-1)
+		start++;
+	found := array [MaxOff] of byte;
+	for(i := 0; i < MaxOff; i++)
+		found[i] = byte 0;
+	for(i = 0; i < Nhash; i++) {
+		age = 0;
+		for(you = hash[i]; me-you <= MaxOff; you = nexts[you & (MaxOff-1)]) {
+			a = me - you;
+			if(a < age)
+				fatal(lz, sys->sprint("%s: out of order links age %d a %d me %d you %d",
+					where, age, a, me, you));
+
+			age = a;
+
+			s = start - a;
+			if(s < 0)
+				s += MaxOff + HistSlop;
+
+			if(hashit(s, lz.hist) != i)
+				fatal(lz, sys->sprint("%s: bad hash chain you %d me %d s %d start %d chain %d hash %d %d %d",
+					where, you, me, s, start, i, hashit(s - 1, lz.hist), hashit(s, lz.hist), hashit(s + 1, lz.hist)));
+
+			if(found[you & (MaxOff - 1)] != byte 0)
+				fatal(lz, where + ": found link again");
+			found[you & (MaxOff - 1)] = byte 1;
+		}
+	}
+
+	for(you = me - (MaxOff-1); you != me; you++)
+		found[you & (MaxOff - 1)] = byte 1;
+
+	for(i = 0; i < MaxOff; i++){
+		if(found[i] == byte 0 && nexts[i] != 0)
+			fatal(lz, sys->sprint("%s: link not found: max %d at %d", where, me & (MaxOff-1), i));
+	}
+}
+
+hashit(p: int, hist: array of byte): int
+{
+	h := 0;
+	for(ep := p + MinMatch; p < ep; p++)
+		h = ((h << Hshift) ^ int hist[p]) & Hmask;
+	return h;
+}
+
+#
+# make up the dynamic code tables, and return the number of bits
+# needed to transmit them.
+#
+huffcodes(dc: ref Dyncode, littab, offtab: array of Huff): int
+{
+	i, n, m, c, nlit, noff, ncode, nclen: int;
+
+	codetab := dc.codetab;
+	codes := dc.codes;
+	codeaux := dc.codeaux;
+
+	#
+	# trim the sizes of the tables
+	#
+	for(nlit = Nlitlen; nlit > 257 && littab[nlit-1].bits == 0; nlit--)
+		;
+	for(noff = Noff; noff > 1 && offtab[noff-1].bits == 0; noff--)
+		;
+
+	#
+	# make the code-length code
+	#
+	for(i = 0; i < nlit; i++)
+		codes[i] = byte littab[i].bits;
+	for(i = 0; i < noff; i++)
+		codes[i + nlit] = byte offtab[i].bits;
+
+	#
+	# run-length compress the code-length code
+	#
+	excost := 0;
+	c = 0;
+	ncode = nlit+noff;
+	for(i = 0; i < ncode; ) {
+		n = i + 1;
+		v := codes[i];
+		while(n < ncode && v == codes[n])
+			n++;
+		n -= i;
+		i += n;
+		if(v == byte 0) {
+			while(n >= 11) {
+				m = n;
+				if(m > 138)
+					m = 138;
+				codes[c] = byte 18;
+				codeaux[c++] = byte(m - 11);
+				n -= m;
+				excost += 7;
+			}
+			if(n >= 3) {
+				codes[c] = byte 17;
+				codeaux[c++] = byte(n - 3);
+				n = 0;
+				excost += 3;
+			}
+		}
+		while(n--) {
+			codes[c++] = v;
+			while(n >= 3) {
+				m = n;
+				if(m > 6)
+					m = 6;
+				codes[c] = byte 16;
+				codeaux[c++] = byte(m - 3);
+				n -= m;
+				excost += 3;
+			}
+		}
+	}
+
+	codecount := array[Nclen] of {* => 0};
+	for(i = 0; i < c; i++)
+		codecount[int codes[i]]++;
+	mkprecode(codetab, codecount, Nclen, 7);
+
+	for(nclen = Nclen; nclen > 4 && codetab[clenorder[nclen-1]].bits == 0; nclen--)
+		;
+
+	dc.nlit = nlit;
+	dc.noff = noff;
+	dc.nclen = nclen;
+	dc.ncode = c;
+
+	return 5 + 5 + 4 + nclen * 3 + bitcost(codetab, codecount, Nclen) + excost;
+}
+
+wrdyncode(out: ref LZstate, dc: ref Dyncode)
+{
+	#
+	# write out header, then code length code lengths,
+	# and code lengths
+	#
+	lzput(out, dc.nlit-257, 5);
+	lzput(out, dc.noff-1, 5);
+	lzput(out, dc.nclen-4, 4);
+
+	codetab := dc.codetab;
+	for(i := 0; i < dc.nclen; i++)
+		lzput(out, codetab[clenorder[i]].bits, 3);
+
+	codes := dc.codes;
+	codeaux := dc.codeaux;
+	c := dc.ncode;
+	for(i = 0; i < c; i++){
+		v := int codes[i];
+		lzput(out, codetab[v].encode, codetab[v].bits);
+		if(v >= 16){
+			if(v == 16)
+				lzput(out, int codeaux[i], 2);
+			else if(v == 17)
+				lzput(out, int codeaux[i], 3);
+			else # v == 18
+				lzput(out, int codeaux[i], 7);
+		}
+	}
+}
+
+bitcost(tab: array of Huff, count: array of int, n: int): int
+{
+	tot := 0;
+	for(i := 0; i < n; i++)
+		tot += count[i] * tab[i].bits;
+	return tot;
+}
+
+hufftabinit(tab: array of Huff, n: int, bitcount: array of int, nbits: int)
+{
+	nc := array[MaxHuffBits + 1] of int;
+
+	code := 0;
+	for(bits := 1; bits <= nbits; bits++) {
+		code = (code + bitcount[bits-1]) << 1;
+		nc[bits] = code;
+	}
+
+	for(i := 0; i < n; i++) {
+		bits = tab[i].bits;
+		if(bits != 0) {
+			code = nc[bits]++ << (16 - bits);
+			tab[i].encode = int(revtab[code >> 8]) | (int(revtab[code & 16rff]) << 8);
+		}
+	}
+}
+
+Chain: adt
+{
+	count:		int;				# occurances of everything in the chain
+	leaf:		int;				# leaves to the left of chain, or leaf value
+	col:		byte;				# ref count for collecting unused chains
+	gen:		byte;				# need to generate chains for next lower level
+	up:		int;				# Chain up in the lists
+};
+
+Chains: adt
+{
+	lists:		array of int;			# [MaxHuffBits * 2]
+	chains:		array of Chain;			# [ChainMem]
+	nleaf:		int;				# number of leaves
+	free:		int;
+	col:		byte;
+	nlists:		int;
+};
+
+Nil:	con -1;
+
+#
+# fast, low space overhead algorithm for max depth huffman type codes
+#
+# J. Katajainen, A. Moffat and A. Turpin, "A fast and space-economical
+# algorithm for length-limited coding," Proc. Intl. Symp. on Algorithms
+# and Computation, Cairns, Australia, Dec. 1995, Lecture Notes in Computer
+# Science, Vol 1004, J. Staples, P. Eades, N. Katoh, and A. Moffat, eds.,
+# pp 12-21, Springer Verlag, New York, 1995.
+#
+mkprecode(tab: array of Huff, count: array of int, n, maxbits: int)
+{
+	cs := ref Chains(array[MaxHuffBits * 2] of int, array[MaxLeaf+ChainMem] of Chain, 0, 0, byte 0, 0);
+	bits: int;
+
+	for(i := 0; i < n; i++){
+		tab[i].bits = 0;
+		tab[i].encode = 0;
+	}
+
+	#
+	# set up the sorted list of leaves
+	#
+	m := 0;
+	for(i = 0; i < n; i++) {
+		if(count[i] != 0){
+			cs.chains[m].count = count[i];
+			cs.chains[m].leaf = i;
+			m++;
+		}
+	}
+	if(m < 2) {
+		if(m != 0) {
+			m = cs.chains[0].leaf;
+			tab[m].bits = 1;
+			tab[m].encode = 0;
+		}
+		return;
+	}
+	cs.nleaf = m;
+	csorts(cs.chains, 0, m);
+
+	cs.free = cs.nleaf + 2;
+	cs.col = byte 1;
+
+	#
+	# initialize chains for each list
+	#
+	c := cs.chains;
+	cl := cs.nleaf;
+	c[cl].count = cs.chains[0].count;
+	c[cl].leaf = 1;
+	c[cl].col = cs.col;
+	c[cl].up = Nil;
+	c[cl].gen = byte 0;
+	c[cl + 1] = c[cl];
+	c[cl + 1].leaf = 2;
+	c[cl + 1].count = cs.chains[1].count;
+	for(i = 0; i < maxbits; i++){
+		cs.lists[i * 2] = cl;
+		cs.lists[i * 2 + 1] = cl + 1;
+	}
+
+	cs.nlists = 2 * maxbits;
+	m = 2 * m - 2;
+	for(i = 2; i < m; i++)
+		nextchain(cs, cs.nlists - 2);
+
+	bitcount := array[MaxHuffBits + 1] of int;
+	bits = 0;
+	bitcount[0] = cs.nleaf;
+	for(cl = cs.lists[2 * maxbits - 1]; cl != Nil; cl = c[cl].up) {
+		m = c[cl].leaf;
+		for(i = 0; i < m; i++)
+			tab[cs.chains[i].leaf].bits++;
+		bitcount[bits++] -= m;
+		bitcount[bits] = m;
+	}
+
+	hufftabinit(tab, n, bitcount, bits);
+}
+
+#
+# calculate the next chain on the list
+# we can always toss out the old chain
+#
+nextchain(cs: ref Chains, clist: int)
+{
+	i, nleaf, sumc: int;
+
+	oc := cs.lists[clist + 1];
+	cs.lists[clist] = oc;
+	if(oc == Nil)
+		return;
+
+	#
+	# make sure we have all chains needed to make sumc
+	# note it is possible to generate only one of these,
+	# use twice that value for sumc, and then generate
+	# the second if that preliminary sumc would be chosen.
+	# however, this appears to be slower on current tests
+	#
+	chains := cs.chains;
+	if(chains[oc].gen != byte 0) {
+		nextchain(cs, clist - 2);
+		nextchain(cs, clist - 2);
+		chains[oc].gen = byte 0;
+	}
+
+	#
+	# pick up the chain we're going to add;
+	# collect unused chains no free ones are left
+	#
+	for(c := cs.free; ; c++) {
+		if(c >= ChainMem) {
+			cs.col++;
+			for(i = 0; i < cs.nlists; i++)
+				for(c = cs.lists[i]; c != Nil; c = chains[c].up)
+					chains[c].col = cs.col;
+			c = cs.nleaf;
+		}
+		if(chains[c].col != cs.col)
+			break;
+	}
+
+	#
+	# pick the cheapest of
+	# 1) the next package from the previous list
+	# 2) the next leaf
+	#
+	nleaf = chains[oc].leaf;
+	sumc = 0;
+	if(clist > 0 && cs.lists[clist-1] != Nil)
+		sumc = chains[cs.lists[clist-2]].count + chains[cs.lists[clist-1]].count;
+	if(sumc != 0 && (nleaf >= cs.nleaf || chains[nleaf].count > sumc)) {
+		chains[c].count = sumc;
+		chains[c].leaf = chains[oc].leaf;
+		chains[c].up = cs.lists[clist-1];
+		chains[c].gen = byte 1;
+	} else if(nleaf >= cs.nleaf) {
+		cs.lists[clist + 1] = Nil;
+		return;
+	} else {
+		chains[c].leaf = nleaf + 1;
+		chains[c].count = chains[nleaf].count;
+		chains[c].up = chains[oc].up;
+		chains[c].gen = byte 0;
+	}
+	cs.free = c + 1;
+
+	cs.lists[clist + 1] = c;
+	chains[c].col = cs.col;
+}
+
+chaincmp(chain: array of Chain, ai, bi: int): int
+{
+	ac := chain[ai].count;
+	bc := chain[bi].count;
+	if(ac < bc)
+		return -1;
+	if(ac > bc)
+		return 1;
+	ac = chain[ai].leaf;
+	bc = chain[bi].leaf;
+	if(ac > bc)
+		return -1;
+	return ac < bc;
+}
+
+pivot(chain: array of Chain, a, n: int): int
+{
+	j := n/6;
+	pi := a + j;	# 1/6
+	j += j;
+	pj := pi + j;	# 1/2
+	pk := pj + j;	# 5/6
+	if(chaincmp(chain, pi, pj) < 0) {
+		if(chaincmp(chain, pi, pk) < 0) {
+			if(chaincmp(chain, pj, pk) < 0)
+				return pj;
+			return pk;
+		}
+		return pi;
+	}
+	if(chaincmp(chain, pj, pk) < 0) {
+		if(chaincmp(chain, pi, pk) < 0)
+			return pi;
+		return pk;
+	}
+	return pj;
+}
+
+csorts(chain: array of Chain, a, n: int)
+{
+	j, pi, pj, pn: int;
+
+	while(n > 1) {
+		if(n > 10)
+			pi = pivot(chain, a, n);
+		else
+			pi = a + (n>>1);
+
+		t := chain[pi];
+		chain[pi] = chain[a];
+		chain[a] = t;
+		pi = a;
+		pn = a + n;
+		pj = pn;
+		for(;;) {
+			do
+				pi++;
+			while(pi < pn && chaincmp(chain, pi, a) < 0);
+			do
+				pj--;
+			while(pj > a && chaincmp(chain, pj, a) > 0);
+			if(pj < pi)
+				break;
+			t = chain[pi];
+			chain[pi] = chain[pj];
+			chain[pj] = t;
+		}
+		t = chain[a];
+		chain[a] = chain[pj];
+		chain[pj] = t;
+		j = pj - a;
+
+		n = n-j-1;
+		if(j >= n) {
+			csorts(chain, a, j);
+			a += j+1;
+		} else {
+			csorts(chain, a + (j+1), n);
+			n = j;
+		}
+	}
+}
+
+mkcrctab(poly: int): array of int
+{
+	crctab := array[256] of int;
+	for(i := 0; i < 256; i++){
+		crc := i;
+		for(j := 0; j < 8; j++){
+			c := crc & 1;
+			crc = (crc >> 1) & 16r7fffffff;
+			if(c)
+				crc ^= poly;
+		}
+		crctab[i] = crc;
+	}
+	return crctab;
+}
+
+inblockcrc(lz: ref LZstate, buf: array of byte)
+{
+	crc := lz.crc;
+	n := len buf;
+	crc ^= int 16rffffffff;
+	for(i := 0; i < n; i++)
+		crc = lz.crctab[int(byte crc ^ buf[i])] ^ ((crc >> 8) & 16r00ffffff);
+	lz.crc = crc ^ int 16rffffffff;
+	lz.tot += n;
+}
+
+inblockadler(lz: ref LZstate, buf: array of byte)
+{
+	ZLADLERBASE:	con big 65521;
+
+	s1 := lz.sum & big 16rffff;
+	s2 := (lz.sum>>16) & big 16rffff;
+
+	for(i := 0; i < len buf; i++) {
+		s1 = (s1 + big buf[i]) % ZLADLERBASE;
+		s2 = (s2 + s1) % ZLADLERBASE;
+	}
+	lz.sum = (s2<<16) + s1;
+}
+
+inblock(lz: ref LZstate, buf: array of byte)
+{
+	case lz.headers {
+	Hgzip =>	inblockcrc(lz, buf);
+	Hzlib =>	inblockadler(lz, buf);
+	}
+}
+
+fatal(lz: ref LZstate, s: string)
+{
+	lz.c <-= ref Rq.Error(s);
+	exit;
+}
--- /dev/null
+++ b/appl/lib/devpointer.b
@@ -1,0 +1,123 @@
+implement Devpointer;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	Pointer: import Draw;
+
+include "devpointer.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+reader(file: string, posn: chan of ref Pointer, pid: chan of (int, string))
+{
+	if(file == nil)
+		file = "/dev/pointer";
+	dfd := sys->open(file, sys->OREAD);
+	if(dfd == nil){
+		if(pid != nil){
+			pid <-= (-1, sys->sprint("cannot open %s: %r", file));
+			return;
+		}
+	}
+	if(pid != nil)
+		pid <-= (sys->pctl(0, nil), nil);
+	b:= array[Size] of byte;
+	while(sys->read(dfd, b, len b) == Size)
+		posn <-= bytes2ptr(b);
+}
+
+bytes2ptr(b: array of byte): ref Pointer
+{
+	if(len b < Size || int b[0] != 'm')
+		return nil;
+	x := int string b[1:13];
+	y := int string b[13:25];
+	but := int string b[25:37];
+	msec := int string b[37:49];
+	return ref Pointer (but, (x, y), msec);
+}
+
+ptr2bytes(p: ref Pointer): array of byte
+{
+	if(p == nil)
+		return nil;
+	return sys->aprint("m%11d %11d %11d %11ud ", p.xy.x, p.xy.y, p.buttons, p.msec);
+}
+
+srv(c: chan of ref Pointer, f: ref Sys->FileIO)
+{
+	ptrq := ref Ptrqueue;
+	dummy := chan of (int, int, int, Sys->Rread);
+	sys = load Sys Sys->PATH;
+
+	for(;;){
+		r := dummy;
+		if(ptrq.nonempty())
+			r = f.read;
+		alt{
+		p := <-c =>
+			if(p == nil)
+				exit;
+			ptrq.put(p);
+		(nil, nil, nil, rc) := <-r =>
+			if(rc != nil){
+				alt{
+				rc <-= (ptr2bytes(ptrq.get()), nil) =>;
+				* =>;
+				}
+			}
+		(nil, nil, nil, rc) := <-f.write =>
+			if(rc != nil)
+				rc <-= (0, "read only");
+		}
+	}
+}
+
+Ptrqueue.put(q: self ref Ptrqueue, s: ref Pointer)
+{
+	if(q.last != nil && s.buttons == q.last.buttons)
+		*q.last = *s;
+	else{
+		q.t = s :: q.t;
+		q.last = s;
+	}
+}
+
+Ptrqueue.get(q: self ref Ptrqueue): ref Pointer
+{
+	s: ref Pointer;
+	h := q.h;
+	if(h == nil){
+		for(t := q.t; t != nil; t = tl t)
+			h = hd t :: h;
+		q.t = nil;
+	}
+	if(h != nil){
+		s = hd h;
+		h = tl h;
+		if(h == nil)
+			q.last = nil;
+	}
+	q.h = h;
+	return s;
+}
+Ptrqueue.peek(q: self ref Ptrqueue): ref Pointer
+{
+	s: ref Pointer;
+	if (q.h == nil && q.t == nil)
+		return s;
+	t := q.last;
+	s = q.get();
+	q.h = s :: q.h;
+	q.last = t;
+	return s;
+}
+Ptrqueue.nonempty(q: self ref Ptrqueue): int
+{
+	return q.h != nil || q.t != nil;
+}
--- /dev/null
+++ b/appl/lib/dhcpclient.b
@@ -1,0 +1,1033 @@
+implement Dhcpclient;
+
+#
+# DHCP and BOOTP clients
+# Copyright © 2004-2006 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "ip.m";
+	ip: IP;
+	IPv4off, IPaddrlen, Udphdrlen, Udpraddr, Udpladdr, Udprport, Udplport: import IP;
+	IPaddr: import ip;
+	get2, get4, put2, put4: import ip;
+
+include "keyring.m";
+include "security.m";	# for Random
+
+include "dial.m";
+	dial: Dial;
+
+include "dhcp.m";
+
+debug := 0;
+
+xidgen: int;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	random := load Random Random->PATH;
+	if(random != nil)
+		xidgen = random->randomint(Random->NotQuiteRandom);
+	else
+		xidgen = sys->pctl(0, nil)*sys->millisec();
+	random = nil;
+	dial = load Dial Dial->PATH;
+	ip = load IP IP->PATH;
+	ip->init();
+}
+
+tracing(d: int)
+{
+	debug = d;
+}
+
+Bootconf.new(): ref Bootconf
+{
+	bc := ref Bootconf;
+	bc.lease = 0;
+	bc.options = array[256] of array of byte;
+	return bc;
+}
+
+Bootconf.get(c: self ref Bootconf, n: int): array of byte
+{
+	a := c.options;
+	if(n & Ovendor){
+		a = c.vendor;
+		n &= ~Ovendor;
+	}
+	if(n < 0 || n >= len a)
+		return nil;
+	return a[n];
+}
+
+Bootconf.getint(c: self ref Bootconf, n: int): int
+{
+	a := c.get(n);
+	v := 0;
+	for(i := 0; i < len a; i++)
+		v = (v<<8) | int a[i];
+	return v;
+}
+
+Bootconf.getip(c: self ref Bootconf, n: int): string
+{
+	l := c.getips(n);
+	if(l == nil)
+		return nil;
+	return hd l;
+}
+
+Bootconf.getips(c: self ref Bootconf, n: int): list of string
+{
+	a := c.get(n);
+	rl: list of string;
+	while(len a >= 4){
+		rl = v4text(a) :: rl;
+		a = a[4:];
+	}
+	l: list of string;
+	for(; rl != nil; rl = tl rl)
+		l = hd rl :: l;
+	return l;
+}
+
+Bootconf.gets(c: self ref Bootconf, n: int): string
+{
+	a := c.get(n);
+	if(a == nil)
+		return nil;
+	for(i:=0; i<len a; i++)
+		if(a[i] == byte 0)
+			break;
+	return string a[0:i];
+}
+
+Bootconf.put(c: self ref Bootconf, n: int, a: array of byte)
+{
+	if(n < 0 || n >= len c.options)
+		return;
+	ca := array[len a] of byte;
+	ca[0:] = a;
+	c.options[n] = ca;
+}
+
+Bootconf.putint(c: self ref Bootconf, n: int, v: int)
+{
+	if(n < 0 || n >= len c.options)
+		return;
+	a := array[4] of byte;
+	put4(a, 0, v);
+	c.options[n] = a;
+}
+
+Bootconf.putips(c: self ref Bootconf, n: int, ips: list of string)
+{
+	if(n < 0 || n >= len c.options)
+		return;
+	na := len ips;
+	a := array[na*4] of byte;
+	na = 0;
+	for(; ips != nil; ips = tl ips){
+		(nil, ipa) := IPaddr.parse(hd ips);
+		a[na++:] = ipa.v4();
+	}
+	c.options[n] = a;
+}
+
+Bootconf.puts(c: self ref Bootconf, n: int, s: string)
+{
+	if(n < 0 || n >= len c.options)
+		return;
+	c.options[n] = array of byte s;
+}
+
+#
+#
+# DHCP
+#
+#
+
+# BOOTP operations
+Bootprequest, Bootpreply: con 1+iota;
+
+# DHCP operations
+NotDHCP, Discover, Offer, Request, Decline, Ack, Nak, Release, Inform: con iota;
+
+Dhcp: adt {
+	udphdr:	array of byte;
+	op:		int;
+	htype:	int;
+	hops:	int;
+	xid:		int;
+	secs:		int;
+	flags:	int;
+	ciaddr:	IPaddr;
+	yiaddr:	IPaddr;
+	siaddr:	IPaddr;
+	giaddr:	IPaddr;
+	chaddr:	array of byte;
+	sname:	string;
+	file:		string;
+	options:	list of (int, array of byte);
+	dhcpop:	int;
+};
+
+opnames := array[] of {
+	Discover => "Discover",
+	Offer => "Offer",
+	Request => "Request",
+	Decline => "Decline",
+	Ack => "Ack",
+	Nak => "Nak",
+	Release => "Release",
+	Inform => "Inform"
+};
+
+opname(op: int): string
+{
+	if(op >= 0 && op < len opnames)
+		return opnames[op];
+	return sys->sprint("OP%d", op);
+}
+
+stringget(buf: array of byte): string
+{
+	for(x := 0; x < len buf; x++)
+		if(buf[x] == byte 0)
+			break;
+	if(x == 0)
+		return nil;
+	return string buf[0 : x];
+}
+
+eqbytes(b1: array of byte, b2: array of byte): int
+{
+	l := len b1;
+	if(l != len b2)
+		return 0;
+	for(i := 0; i < l; i++)
+		if(b1[i] != b2[i])
+			return 0;
+	return 1;
+}
+
+magic := array[] of {byte 99, byte 130, byte 83, byte 99};	# RFC2132 (replacing RFC1048)
+
+dhcpsend(fd: ref Sys->FD, xid: int, dhcp: ref Dhcp)
+{
+	dhcp.xid = xid;
+	abuf := array[576+Udphdrlen] of {* => byte 0};
+	abuf[0:] = dhcp.udphdr;
+	buf := abuf[Udphdrlen:];
+	buf[0] = byte dhcp.op;
+	buf[1] = byte dhcp.htype;
+	buf[2] = byte len dhcp.chaddr;
+	buf[3] = byte dhcp.hops;
+	put4(buf, 4, xid);
+	put2(buf, 8, dhcp.secs);
+	put2(buf, 10, dhcp.flags);
+	buf[12:] = dhcp.ciaddr.v4();
+	buf[16:] = dhcp.yiaddr.v4();
+	buf[20:] = dhcp.siaddr.v4();
+	buf[24:] = dhcp.giaddr.v4();
+	buf[28:] = dhcp.chaddr;
+	buf[44:] = array of byte dhcp.sname;	# [64]
+	buf[108:] = array of byte dhcp.file;	# [128]
+	o := 236;
+	# RFC1542 suggests including magic and Oend as a minimum, even in BOOTP
+	buf[o:] = magic;
+	o += 4;
+	if(dhcp.dhcpop != NotDHCP){
+		buf[o++] = byte Otype;
+		buf[o++] = byte 1;
+		buf[o++] = byte dhcp.dhcpop;
+	}
+	for(ol := dhcp.options; ol != nil; ol = tl ol){
+		(opt, val) := hd ol;
+		buf[o++] = byte opt;
+		buf[o++] = byte len val;
+		if(len val > 0){
+			buf[o:] = val;
+			o += len val;
+		}
+	}
+	buf[o++] = byte Oend;
+	if(debug)
+		dumpdhcp(dhcp, "->");
+	sys->write(fd, abuf, len abuf);
+}
+
+kill(pid: int, grp: string)
+{
+	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill%s", grp);
+}
+
+v4text(a: array of byte): string
+{
+	return sys->sprint("%ud.%ud.%ud.%ud", int a[0], int a[1], int a[2], int a[3]);
+}
+
+parseopt(a: array of byte, isdhcp: int): (int, list of (int, array of byte))
+{
+	opts: list of (int, array of byte);
+	xop := NotDHCP;
+	for(i := 0; i < len a;){
+		op := int a[i++];
+		if(op == Opad)
+			continue;
+		if(op == Oend || i >= len a)
+			break;
+		l := int a[i++];
+		if(i+l > len a)
+			break;
+		if(isdhcp && op == Otype)
+			xop = int a[i];
+		else
+			opts = (op, a[i:i+l]) :: opts;
+		i += l;
+	}
+	rl := opts;
+	opts = nil;
+	for(; rl != nil; rl = tl rl)
+		opts = hd rl :: opts;
+	return (xop, opts);
+}
+
+dhcpreader(pidc: chan of int, srv: ref DhcpIO)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		abuf := array [576+Udphdrlen] of byte;
+		n := sys->read(srv.fd, abuf, len abuf);
+		if(n < 0){
+			if(debug)
+				sys->print("read error: %r\n");
+			sys->sleep(1000);
+			continue;
+		}
+		if(n < Udphdrlen+236){
+			if(debug)
+				sys->print("short read: %d\n", n);
+			continue;
+		}
+		buf := abuf[Udphdrlen:n];
+		n -= Udphdrlen;
+		dhcp := ref Dhcp;
+		dhcp.op = int buf[0];
+		if(dhcp.op != Bootpreply){
+			if(debug)
+				sys->print("bootp: not reply, discarded\n");
+			continue;
+		}
+		dhcp.dhcpop = NotDHCP;
+		if(n >= 240 && eqbytes(buf[236:240], magic))	# otherwise it's something we won't understand
+			(dhcp.dhcpop, dhcp.options) = parseopt(buf[240:n], 1);
+		case dhcp.dhcpop {
+		NotDHCP or Ack or Nak or Offer =>
+			;
+		* =>
+			if(debug)
+				sys->print("dhcp: ignore dhcp op %d\n", dhcp.dhcpop);
+			continue;
+		}
+		dhcp.udphdr = abuf[0:Udphdrlen];
+		dhcp.htype = int buf[1];
+		hlen := int buf[2];
+		dhcp.hops = int buf[3];
+		dhcp.xid = get4(buf, 4);
+		dhcp.secs = get2(buf, 8);
+		dhcp.flags = get2(buf, 10);
+		dhcp.ciaddr = IPaddr.newv4(buf[12:]);
+		dhcp.yiaddr = IPaddr.newv4(buf[16:]);
+		dhcp.siaddr = IPaddr.newv4(buf[20:]);
+		dhcp.giaddr = IPaddr.newv4(buf[24:]);
+		dhcp.chaddr = buf[28 : 28 + hlen];
+		dhcp.sname = stringget(buf[44 : 108]);
+		dhcp.file = stringget(buf[108 : 236]);
+		srv.dc <-= dhcp;
+	}
+}
+
+timeoutstart(msecs: int): (int, chan of int)
+{
+	tc := chan of int;
+	spawn timeoutproc(tc, msecs);
+	return (<-tc, tc);
+}
+
+timeoutproc(c: chan of int, msecs: int)
+{
+	c <-= sys->pctl(0, nil);
+	sys->sleep(msecs);
+	c <-= 1;
+}
+
+hex(b: int): int
+{
+	if(b >= '0' && b <= '9')
+		return b-'0';
+	if(b >= 'A' && b <= 'F')
+		return b-'A' + 10;
+	if(b >= 'a' && b <= 'f')
+		return b-'a' + 10;
+	return -1;
+}
+
+gethaddr(device: string): (int, string, array of byte)
+{
+	fd := sys->open(device, Sys->OREAD);
+	if(fd == nil)
+		return (-1, sys->sprint("%r"), nil);
+	buf := array [100] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return (-1, sys->sprint("%r"), nil);
+	if(n == 0)
+		return (-1, "empty address file", nil);
+	addr := array [n/2] of byte;
+	for(i := 0; i < len addr; i++){
+		u := hex(int buf[2*i]);
+		l := hex(int buf[2*i+1]);
+		if(u < 0 || l < 0)
+			return (-1, "bad address syntax", nil);
+		addr[i] = byte ((u<<4)|l);
+	}
+	return (1, nil, addr);
+}
+
+newrequest(dest: IPaddr, bootfile: string, htype: int, haddr: array of byte, ipaddr: IPaddr, options: array of array of byte): ref Dhcp
+{
+	dhcp := ref Dhcp;
+	dhcp.op = Bootprequest;
+	hdr := array[Udphdrlen] of {* => byte 0};
+	hdr[Udpraddr:] = dest.v6();
+	put2(hdr, Udprport, 67);
+	dhcp.udphdr = hdr;
+	dhcp.htype = htype;
+	dhcp.chaddr = haddr;
+	dhcp.hops = 0;
+	dhcp.secs = 0;
+	dhcp.flags = 0;
+	dhcp.xid = 0;
+	dhcp.ciaddr = ipaddr;
+	dhcp.yiaddr = ip->v4noaddr;
+	dhcp.siaddr = ip->v4noaddr;
+	dhcp.giaddr = ip->v4noaddr;
+	dhcp.file = bootfile;
+	dhcp.dhcpop = NotDHCP;
+	if(options != nil){
+		for(i := 0; i < len options; i++)
+			if(options[i] != nil)
+				dhcp.options = (i, options[i]) :: dhcp.options;
+	}
+	clientid := array[len haddr + 1] of byte;
+	clientid[0] = byte htype;
+	clientid[1:] = haddr;
+	dhcp.options = (Oclientid, clientid) :: dhcp.options;
+	dhcp.options = (Ovendorclass, array of byte "plan9_386") :: dhcp.options;	# 386 will do because type doesn't matter
+	return dhcp;
+}
+
+udpannounce(net: string): (ref Sys->FD, string)
+{
+	if(net == nil)
+		net = "/net";
+	conn := dial->announce(net+"/udp!*!68");
+	if(conn == nil)
+		return (nil, sys->sprint("can't announce dhcp port: %r"));
+	if(sys->fprint(conn.cfd, "headers") < 0)
+		return (nil, sys->sprint("can't set headers mode on dhcp port: %r"));
+	conn.dfd = sys->open(conn.dir+"/data", Sys->ORDWR);
+	if(conn.dfd == nil)
+		return (nil, sys->sprint("can't open %s: %r", conn.dir+"/data"));
+	return (conn.dfd, nil);
+}
+
+ifcnoaddr(fd: ref Sys->FD, s: string)
+{
+	if(fd != nil && sys->fprint(fd, "%s %s %s", s, (ip->noaddr).text(), (ip->noaddr).text()) < 0){
+		if(debug)
+			sys->print("dhcp: ctl %s: %r\n", s);
+	}
+}
+
+setup(net: string, device: string, init: ref Bootconf): (ref Dhcp, ref DhcpIO, string)
+{
+	(htype, err, mac) := gethaddr(device);
+	if(htype < 0)
+		return (nil, nil, sys->sprint("can't get hardware MAC address: %s", err));
+	ciaddr := ip->v4noaddr;
+	if(init != nil && init.ip != nil){
+		valid: int;
+		(valid, ciaddr) = IPaddr.parse(init.ip);
+		if(valid < 0)
+			return (nil, nil, sys->sprint("invalid ip address: %s", init.ip));
+	}
+	(dfd, err2) := udpannounce(net);
+	if(err2 != nil)
+		return (nil, nil, err);
+	bootfile: string;
+	options: array of array of byte;
+	if(init != nil){
+		bootfile = init.bootf;
+		options = init.options;
+	}
+	return (newrequest(ip->v4bcast, bootfile, htype, mac, ciaddr, options), DhcpIO.new(dfd), nil);
+}
+
+#
+# BOOTP (RFC951) is used by Inferno only during net boots, to get initial IP address and TFTP address and parameters
+#
+bootp(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf): (ref Bootconf, string)
+{
+	(req, srv, err) := setup(net, device, init);
+	if(err != nil)
+		return (nil, err);
+	ifcnoaddr(ctlifc, "add");
+	rdhcp := exchange(srv, ++xidgen, req, 1<<NotDHCP);
+	srv.rstop();
+	ifcnoaddr(ctlifc, "remove");
+	if(rdhcp == nil)
+		return (nil, "no response to BOOTP request");
+	return (fillbootconf(init, rdhcp), nil);
+}
+
+defparams := array[] of {
+	byte Omask, byte Orouter, byte Odnsserver, byte Ohostname, byte Odomainname, byte Ontpserver,
+};
+
+#
+# DHCP (RFC2131)
+#
+dhcp(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf, needparam: array of int): (ref Bootconf, ref Lease, string)
+{
+	(req, srv, err) := setup(net, device, init);
+	if(err != nil)
+		return (nil, nil, err);
+	params := defparams;
+	if(needparam != nil){
+		n := len defparams;
+		params = array[n+len needparam] of byte;
+		params[0:] = defparams;
+		for(i := 0; i < len needparam; i++)
+			params[n+i] = byte needparam[i];
+	}
+	initopt := (Oparams, params) :: req.options;	# RFC2131 requires parameters to be repeated each time
+	lease := ref Lease(0, chan[1] of (ref Bootconf, string));
+	spawn dhcp1(srv, lease, net, ctlifc, req, init, initopt);
+	bc: ref Bootconf;
+	(bc, err) = <-lease.configs;
+	return (bc, lease, err);
+}
+
+dhcp1(srv: ref DhcpIO, lease: ref Lease, net: string, ctlifc: ref Sys->FD, req: ref Dhcp, init: ref Bootconf, initopt: list of (int, array of byte))
+{
+	cfd := -1;
+	if(ctlifc != nil)
+		cfd = ctlifc.fd;
+	lease.pid = sys->pctl(Sys->NEWPGRP|Sys->NEWFD, 1 :: srv.fd.fd :: cfd :: nil);
+	if(ctlifc != nil)
+		ctlifc = sys->fildes(ctlifc.fd);
+	srv.fd = sys->fildes(srv.fd.fd);
+	rep: ref Dhcp;
+	ifcnoaddr(ctlifc, "add");
+	if(req.ciaddr.isvalid())
+		rep = reacquire(srv, req, initopt, req.ciaddr);
+	if(rep == nil)
+		rep = askround(srv, req, initopt);
+	srv.rstop();
+	ifcnoaddr(ctlifc, "remove");
+	if(rep == nil){
+		lease.pid = 0;
+		lease.configs <-= (nil, "no response");
+		exit;
+	}
+	for(;;){
+		conf := fillbootconf(init, rep);
+		applycfg(net, ctlifc, conf);
+		if(conf.lease == 0){
+			srv.rstop();
+			lease.pid = 0;
+			flush(lease.configs);
+			lease.configs <-= (conf, nil);
+			exit;
+		}
+		flush(lease.configs);
+		lease.configs <-= (conf, nil);
+		req.ciaddr = rep.yiaddr;
+		while((rep = tenancy(srv, req, conf.lease)) != nil){
+			if(rep.dhcpop == Nak || !rep.ciaddr.eq(req.ciaddr))
+				break;
+			req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
+			conf = fillbootconf(init, rep);
+		}
+		removecfg(net, ctlifc, conf);
+		ifcnoaddr(ctlifc, "add");
+		while((rep = askround(srv, req, initopt)) == nil){
+			flush(lease.configs);
+			lease.configs <-= (nil, "no response");
+			srv.rstop();
+			sys->sleep(60*1000);
+		}
+		ifcnoaddr(ctlifc, "remove");
+	}
+}
+
+reacquire(srv: ref DhcpIO, req: ref Dhcp, initopt: list of (int, array of byte), addr: IPaddr): ref Dhcp
+{
+	# INIT-REBOOT: know an address; try requesting it (once)
+	# TO DO: could use Inform when our address is static but we need a few service parameters
+	req.ciaddr = ip->v4noaddr;
+	rep := request(srv, ++xidgen, req, (Oipaddr, addr.v4()) :: initopt);
+	if(rep != nil && rep.dhcpop == Ack && addr.eq(rep.yiaddr)){
+		if(debug)
+			sys->print("req: server accepted\n");
+		req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
+		return rep;
+	}
+	if(debug)
+		sys->print("req: cannot reclaim\n");
+	return nil;
+}
+
+askround(srv: ref DhcpIO, req: ref Dhcp, initopt: list of (int, array of byte)): ref Dhcp
+{
+	# INIT
+	req.ciaddr = ip->v4noaddr;
+	req.udphdr[Udpraddr:] = (ip->v4bcast).v6();
+	for(retries := 0; retries < 5; retries++){
+		# SELECTING
+		req.dhcpop = Discover;
+		req.options = initopt;
+		rep := exchange(srv, ++xidgen, req, 1<<Offer);
+		if(rep == nil)
+			break;
+		#
+		# could wait a little while and accumulate offers, but is it sensible?
+		# we do sometimes see arguments between DHCP servers that could
+		# only be resolved by user choice
+		#
+		if(!rep.yiaddr.isvalid())
+			continue;		# server has no idea either
+		serverid := getopt(rep.options, Oserverid, 4);
+		if(serverid == nil)
+			continue;	# broken server
+		# REQUESTING
+		options := (Oserverid, serverid) :: (Oipaddr, rep.yiaddr.v4()) :: initopt;
+		lease := getlease(rep);
+		if(lease != nil)
+			options = (Olease, lease) :: options;
+		rep = request(srv, rep.xid, req, options);
+		if(rep != nil){
+			# could probe with ARP here, and if found, Decline
+			if(debug)
+				sys->print("req: server accepted\n");
+			req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
+			return rep;
+		}
+	}
+	return nil;
+}
+
+request(srv: ref DhcpIO, xid: int, req: ref Dhcp, options: list of (int, array of byte)): ref Dhcp
+{
+	req.dhcpop = Request;	# Selecting
+	req.options = options;
+	rep := exchange(srv, xid, req, (1<<Ack)|(1<<Nak));
+	if(rep == nil || rep.dhcpop == Nak)
+		return nil;
+	return rep;
+}
+
+# renew
+#	direct to server from T1 to T2 [RENEW]
+#	Request must not include
+#		requested IP address, server identifier
+#	Request must include
+#		ciaddr set to client's address
+#	Request might include
+#		lease time
+#	similar, but broadcast, from T2 to T3 [REBIND]
+#	at T3, unbind, restart Discover
+
+tenancy(srv: ref DhcpIO, req: ref Dhcp, leasesec: int): ref Dhcp
+{
+	# configure address...
+	t3 := big leasesec * big 1000;	# lease expires; restart
+	t2 := (big 3 * t3)/big 4;	# broadcast renewal request at ¾time
+	t1 := t2/big 2;		# renew lease with original server at ½time
+	srv.rstop();
+	thebigsleep(t1);
+	# RENEW
+	rep := renewing(srv, req, t1, t2);
+	if(rep != nil)
+		return rep;
+	# REBIND
+	req.udphdr[Udpraddr:] = (ip->v4bcast).v6();	# now try broadcast
+	return renewing(srv, req, t2, t3);
+}
+
+renewing(srv: ref DhcpIO, req: ref Dhcp, a: big, b: big): ref Dhcp
+{
+	Minute: con big(60*1000);
+	while(a < b){
+		rep := exchange(srv, req.xid, req, (1<<Ack)|(1<<Nak));
+		if(rep != nil)
+			return rep;
+		delta := (b-a)/big 2;
+		if(delta < Minute)
+			delta = Minute;
+		thebigsleep(delta);
+		a += delta;
+	}
+	return nil;
+}
+
+thebigsleep(msec: big)
+{
+	Day: con big (24*3600*1000);	# 1 day in msec
+	while(msec > big 0){
+		n := msec;
+		if(n > Day)
+			n = Day;
+		sys->sleep(int n);
+		msec -= n;
+	}
+}
+
+getlease(m: ref Dhcp): array of byte
+{
+	lease := getopt(m.options, Olease, 4);
+	if(lease == nil)
+		return nil;
+	if(get4(lease, 0) == 0){
+		lease = array[4] of byte;
+		put4(lease, 0, 15*60);
+	}
+	return lease;
+}
+
+fillbootconf(init: ref Bootconf, pkt: ref Dhcp): ref Bootconf
+{
+	bc := ref Bootconf;
+	if(init != nil)
+		*bc = *init;
+	if(bc.options == nil)
+		bc.options = array[256] of array of byte;
+	for(l := pkt.options; l != nil; l = tl l){
+		(c, v) := hd l;
+		if(bc.options[c] == nil)
+			bc.options[c] = v;	# give priority to first occurring
+	}
+	if((a := bc.get(Ovendorinfo)) != nil){
+		if(bc.vendor == nil)
+			bc.vendor = array[256] of array of byte;
+		for(l = parseopt(a, 0).t1; l  != nil; l = tl l){
+			(c, v) := hd l;
+			if(bc.vendor[c] == nil)
+				bc.vendor[c] = v;
+		}
+	}
+	if(pkt.yiaddr.isvalid()){
+		bc.ip = pkt.yiaddr.text();
+		bc.ipmask = bc.getip(Omask);
+		if(bc.ipmask == nil)
+			bc.ipmask = pkt.yiaddr.classmask().masktext();
+	}
+	bc.bootf = pkt.file;
+	bc.dhcpip = IPaddr.newv6(pkt.udphdr[Udpraddr:]).text();
+	bc.siaddr = pkt.siaddr.text();
+	bc.lease = bc.getint(Olease);
+	if(bc.lease == Infinite)
+		bc.lease = 0;
+	else if(debug > 1)
+		bc.lease = 2*60;	# shorten time, for testing
+	bc.dom = bc.gets(Odomainname);
+	s := bc.gets(Ohostname);
+	for(i:=0; i<len s; i++)
+		if(s[i] == '.'){
+			if(bc.dom == nil)
+				bc.dom = s[i+1:];
+			s = s[0:i];
+			break;
+		}
+	bc.sys = s;
+	bc.ipgw = bc.getip(Orouter);
+	bc.bootip = bc.getip(Otftpserver);
+	bc.serverid = bc.getip(Oserverid);
+	return bc;
+}
+
+Lease.release(l: self ref Lease)
+{
+	# could send a Release message
+	# should unconfigure
+	if(l.pid){
+		kill(l.pid, "grp");
+		l.pid = 0;
+	}
+}
+
+flush(c: chan of (ref Bootconf, string))
+{
+	alt{
+	<-c =>	;
+	* =>	;
+	}
+}
+
+DhcpIO: adt {
+	fd:	ref Sys->FD;
+	pid:	int;
+	dc:	chan of ref Dhcp;
+	new:	fn(fd: ref Sys->FD): ref DhcpIO;
+	rstart:	fn(io: self ref DhcpIO);
+	rstop:	fn(io: self ref DhcpIO);
+};
+
+DhcpIO.new(fd: ref Sys->FD): ref DhcpIO
+{
+	return ref DhcpIO(fd, 0, chan of ref Dhcp);
+}
+
+DhcpIO.rstart(io: self ref DhcpIO)
+{
+	if(io.pid == 0){
+		pids := chan of int;
+		spawn dhcpreader(pids, io);
+		io.pid = <-pids;
+	}
+}
+
+DhcpIO.rstop(io: self ref DhcpIO)
+{
+	if(io.pid != 0){
+		kill(io.pid, "");
+		io.pid = 0;
+	}
+}
+
+getopt(options: list of (int, array of byte), op: int, minlen: int): array of byte
+{
+	for(; options != nil; options = tl options){
+		(opt, val) := hd options;
+		if(opt == op && len val >= minlen)
+			return val;
+	}
+	return nil;
+}
+
+exchange(srv: ref DhcpIO, xid: int, req: ref Dhcp, accept: int): ref Dhcp
+{
+	srv.rstart();
+	nsec := 3;
+	for(count := 0; count < 5; count++) {
+		(tpid, tc) := timeoutstart(nsec*1000);
+		dhcpsend(srv.fd, xid, req);
+	   Wait:
+		for(;;){
+			alt {
+			<-tc=>
+				break Wait;
+			rep := <-srv.dc=>
+				if(debug)
+					dumpdhcp(rep, "<-");
+				if(rep.op == Bootpreply &&
+				    rep.xid == req.xid &&
+				    rep.ciaddr.eq(req.ciaddr) &&
+				    eqbytes(rep.chaddr, req.chaddr)){
+					if((accept & (1<<rep.dhcpop)) == 0){
+						if(debug)
+							sys->print("req: unexpected reply %s to %s\n", opname(rep.dhcpop), opname(req.dhcpop));
+						continue;
+					}
+					kill(tpid, "");
+					return rep;
+				}
+				if(debug)
+					sys->print("req: mismatch\n");
+			}
+		}
+		req.secs += nsec;
+		nsec++;
+	}
+	return nil;
+}
+
+applycfg(net: string, ctlfd: ref Sys->FD, bc: ref Bootconf): string
+{
+	# write addresses to /net/...
+	# local address, mask[or default], remote address [mtu]
+	if(net == nil)
+		net = "/net";
+	if(bc.ip == nil)
+		return  "invalid address";
+	if(ctlfd != nil){
+		if(sys->fprint(ctlfd, "add %s %s", bc.ip, bc.ipmask) < 0)	# TO DO: [raddr [mtu]]
+			return sys->sprint("add interface: %r");
+		# could use "mtu n" request to set/change mtu
+	}
+	# if primary:
+	# 	add default route if gateway valid
+	# 	put ndb entries ip=, ipmask=, ipgw=; sys= dom=; fs=; auth=; dns=; ntp=; other options from bc.options
+	if(bc.ipgw != nil){
+		fd := sys->open(net+"/iproute", Sys->OWRITE);
+		if(fd != nil)
+			sys->fprint(fd, "add 0 0 %s", bc.ipgw);
+	}
+	s := sys->sprint("ip=%s ipmask=%s", bc.ip, bc.ipmask);
+	if(bc.ipgw != nil)
+		s += sys->sprint(" ipgw=%s", bc.ipgw);
+	s += "\n";
+	if(bc.sys != nil)
+		s += sys->sprint("	sys=%s\n", bc.sys);
+	if(bc.dom != nil)
+		s += sys->sprint("	dom=%s.%s\n", bc.sys, bc.dom);
+	if((addr := bc.getip(OP9auth)) != nil)
+		s += sys->sprint("	auth=%s\n", addr);	# TO DO: several addresses
+	if((addr = bc.getip(OP9fs)) != nil)
+		s += sys->sprint("	fs=%s\n", addr);
+	if((addr = bc.getip(Odnsserver)) != nil)
+		s += sys->sprint("	dns=%s\n", addr);
+	fd := sys->open(net+"/ndb", Sys->OWRITE | Sys->OTRUNC);
+	if(fd != nil){
+		a := array of byte s;
+		sys->write(fd, a, len a);
+	}
+	return nil;
+}
+
+removecfg(nil: string, ctlfd: ref Sys->FD, bc: ref Bootconf): string
+{
+	# remove localaddr, localmask[or default]
+	if(ctlfd != nil){
+		if(sys->fprint(ctlfd, "remove %s %s", bc.ip, bc.ipmask) < 0)
+			return sys->sprint("remove address: %r");
+	}
+	bc.ip = nil;
+	bc.ipgw = nil;
+	bc.ipmask = nil;
+	# remote address?
+	# clear net+"/ndb"?
+	return nil;
+}
+
+#
+# the following is just for debugging
+#
+
+dumpdhcp(m: ref Dhcp, dir: string)
+{
+	s := "";
+	sys->print("%s %s/%ud: ", dir, IPaddr.newv6(m.udphdr[Udpraddr:]).text(), get2(m.udphdr, Udprport));
+	if(m.dhcpop != NotDHCP)
+		s = " "+opname(m.dhcpop);
+	sys->print("op %d%s htype %d hops %d xid %ud\n", m.op, s, m.htype, m.hops, m.xid);
+	sys->print("\tsecs %d flags 0x%.4ux\n", m.secs, m.flags);
+	sys->print("\tciaddr %s\n", m.ciaddr.text());
+	sys->print("\tyiaddr %s\n", m.yiaddr.text());
+	sys->print("\tsiaddr %s\n", m.siaddr.text());
+	sys->print("\tgiaddr %s\n", m.giaddr.text());
+	sys->print("\tchaddr ");
+	for(x := 0; x < len m.chaddr; x++)
+		sys->print("%2.2ux", int m.chaddr[x]);
+	sys->print("\n");
+	if(m.sname != nil)
+		sys->print("\tsname %s\n", m.sname);
+	if(m.file != nil)
+		sys->print("\tfile %s\n", m.file);
+	if(m.options != nil){
+		sys->print("\t");
+		printopts(m.options, opts);
+		sys->print("\n");
+	}
+}
+
+Optbytes, Optaddr, Optmask, Optint, Optstr, Optopts, Opthex: con iota;
+
+Opt: adt
+{
+	code:	int;
+	name:	string;
+	otype:	int;
+};
+
+opts: array of Opt = array[] of {
+	(Omask, "ipmask", Optmask),
+	(Orouter, "ipgw", Optaddr),
+	(Odnsserver, "dns", Optaddr),
+	(Ohostname, "hostname", Optstr),
+	(Odomainname, "domain", Optstr),
+	(Ontpserver, "ntp", Optaddr),
+	(Oipaddr, "requestedip", Optaddr),
+	(Olease, "lease", Optint),
+	(Oserverid, "serverid", Optaddr),
+	(Otype, "dhcpop", Optint),
+	(Ovendorclass, "vendorclass", Optstr),
+	(Ovendorinfo, "vendorinfo", Optopts),
+	(Onetbiosns, "wins", Optaddr),
+	(Opop3server, "pop3", Optaddr),
+	(Osmtpserver, "smtp", Optaddr),
+	(Owwwserver, "www", Optaddr),
+	(Oparams, "params", Optbytes),
+	(Otftpserver, "tftp", Optaddr),
+	(Oclientid, "clientid", Opthex),
+};
+
+p9opts: array of Opt = array[] of {
+	(OP9fs, "fs", Optaddr),
+	(OP9auth, "auth", Optaddr),
+};
+
+lookopt(optab: array of Opt, code: int): (int, string, int)
+{
+	for(i:=0; i<len optab; i++)
+		if(opts[i].code == code)
+			return opts[i];
+	return (-1, nil, 0);
+}
+
+printopts(options: list of (int, array of byte), opts: array of Opt)
+{
+	for(; options != nil; options = tl options){
+		(code, val) := hd options;
+		sys->print("(%d %d", code, len val);
+		(nil, name, otype) := lookopt(opts, code);
+		if(name == nil){
+			for(v := 0; v < len val; v++)
+				sys->print(" %d", int val[v]);
+		}else{
+			sys->print(" %s", name);
+			case otype {
+			Optbytes =>
+				for(v := 0; v < len val; v++)
+					sys->print(" %d", int val[v]);
+			Opthex =>
+				for(v := 0; v < len val; v++)
+					sys->print(" %#.2ux", int val[v]);
+			Optaddr or Optmask =>
+				while(len val >= 4){
+					sys->print(" %s", v4text(val));
+					val = val[4:];
+				}
+			Optstr =>
+				sys->print(" \"%s\"", string val);
+			Optint =>
+				n := 0;
+				for(v := 0; v < len val; v++)
+					n = (n<<8) | int val[v];
+				sys->print(" %d", n);
+			Optopts =>
+				printopts(parseopt(val, 0).t1, p9opts);
+			}
+		}
+		sys->print(")");
+	}
+}
--- /dev/null
+++ b/appl/lib/dial.b
@@ -1,0 +1,384 @@
+implement Dial;
+
+include "sys.m";
+	sys: Sys;
+
+include "dial.m";
+
+#
+# the dialstring is of the form '[/net/]proto!dest'
+#
+dial(addr: string, local: string): ref Connection
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	(netdir, proto, rem) := dialparse(addr);
+	if(netdir != nil)
+		return csdial(netdir, proto, rem, local);
+
+	c := csdial("/net", proto, rem, local);
+	if(c != nil)
+		return c;
+	err := sys->sprint("%r");
+	if(lookstr(err, "refused") >= 0)
+		return nil;
+	c = csdial("/net.alt", proto, rem, local);
+	if(c != nil)
+		return c;
+	# ignore the least precise one
+	alterr := sys->sprint("%r");
+	if(lookstr(alterr, "translate")>=0 || lookstr(alterr, "does not exist")>=0)
+		sys->werrstr(err);
+	else
+		sys->werrstr(alterr);
+	return nil;
+}
+
+#
+# ask the connection server to translate
+#
+csdial(netdir: string, proto: string, rem: string, local: string): ref Connection
+{
+	fd := sys->open(netdir+"/cs", Sys->ORDWR);
+	if(fd == nil){
+		# no connection server, don't translate
+		return call(netdir+"/"+proto+"/clone", rem, local);
+	}
+
+	if(sys->fprint(fd, "%s!%s", proto, rem) < 0)
+		return  nil;
+
+	# try each recipe until we get one that works
+	besterr, err: string;
+	sys->seek(fd, big 0, 0);
+	for(;;){
+		(clonefile, addr) := csread(fd);
+		if(clonefile == nil)
+			break;
+		c := call(redir(clonefile, netdir), addr, local);
+		if(c != nil)
+			return c;
+		err = sys->sprint("%r");
+		if(lookstr(err, "does not exist") < 0)
+			besterr = err;
+	}
+	if(besterr != nil)
+		sys->werrstr(besterr);
+	else
+		sys->werrstr(err);
+	return nil;
+}
+
+call(clonefile: string, dest: string, local: string): ref Connection
+{
+	(cfd, convdir) := clone(clonefile);
+	if(cfd == nil)
+		return nil;
+
+	if(local != nil)
+		rv := sys->fprint(cfd, "connect %s %s", dest, local);
+	else
+		rv = sys->fprint(cfd, "connect %s", dest);
+	if(rv < 0)
+		return nil;
+
+	fd := sys->open(convdir+"/data", Sys->ORDWR);
+	if(fd == nil)
+		return nil;
+	return ref Connection(fd, cfd, convdir);
+}
+
+clone(clonefile: string): (ref Sys->FD, string)
+{
+	pdir := parent(clonefile);
+	if(pdir == nil){
+		sys->werrstr(sys->sprint("bad clone file name: %q", clonefile));
+		return (nil, nil);
+	}
+	cfd := sys->open(clonefile, Sys->ORDWR);
+	if(cfd == nil)
+		return (nil, nil);
+	lno := readchan(cfd);
+	if(lno == nil)
+		return (nil, nil);
+	return (cfd, pdir+"/"+lno);
+}
+
+readchan(cfd: ref Sys->FD): string
+{
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(cfd, buf, len buf);
+	if(n < 0)
+		return nil;
+	if(n == 0){
+		sys->werrstr("empty clone file");
+		return nil;
+	}
+	return string int string buf[0: n];
+}
+
+redir(old: string, newdir: string): string
+{
+	# because cs is in a different name space, replace the mount point
+	# assumes the mount point is directory in root (eg, /net/proto/clone)
+	if(len old > 1 && old[0] == '/'){
+		p := lookc(old[1:], '/');
+		if(p >= 0)
+			return newdir+"/"+old[1+p+1:];
+	}
+	return newdir+"/"+old;
+}
+
+lookc(s: string, c: int): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return i;
+	return -1;
+}
+
+backc(s: string, i: int, c: int): int
+{
+	if(i >= len s)
+		return -1;
+	while(i >= 0 && s[i] != c)
+		i--;
+	return i;
+}
+
+lookstr(s: string, t: string): int
+{
+	lt := len t;	# we know it's not zero
+Search:
+	for(i := 0; i <= len s - lt; i++){
+		for(j := 0; j < lt; j++)
+			if(s[i+j] != t[j])
+				continue Search;
+		return i;
+	}
+	return -1;
+}
+
+#
+# [[/netdir/]proto!]remainder
+#
+dialparse(addr: string): (string, string, string)
+{
+	p := lookc(addr, '!');
+	if(p < 0)
+		return (nil, "net", addr);
+	if(addr[0] != '/' && addr[0] != '#')
+		return (nil, addr[0: p], addr[p+1:]);
+	p2 := backc(addr, p, '/');
+	if(p2 <= 0)
+		return (addr[0: p], "net", addr[p+1:]);	# plan 9 returns proto ""
+	return (addr[0: p2], addr[p2+1: p], addr[p+1:]);
+}
+
+#
+# announce a network service
+#
+announce(addr: string): ref Connection
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	(naddr, clonefile) := nettrans(addr);
+	if(naddr == nil)
+		return nil;
+
+	(ctl, convdir) := clone(clonefile);
+	if(ctl == nil){
+		sys->werrstr(sys->sprint("announce %r"));
+		return nil;
+	}
+
+	if(sys->fprint(ctl, "announce %s", naddr) < 0){
+		sys->werrstr(sys->sprint("announce writing %s: %r", clonefile));
+		return nil;
+	}
+
+	return ref Connection(nil, ctl, convdir);
+}
+
+#
+# listen for an incoming call on announced connection
+#
+listen(ac: ref Connection): ref Connection
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	pdir := parent(ac.dir);	# ac.dir should be /netdir/N
+	if(pdir == nil){
+		sys->werrstr(sys->sprint("listen directory format: %q", ac.dir));
+		return nil;
+	}
+
+	ctl := sys->open(ac.dir+"/listen", Sys->ORDWR);
+	if(ctl == nil){
+		sys->werrstr(sys->sprint("listen opening %s: %r", ac.dir+"/listen"));
+		return nil;
+	}
+
+	lno := readchan(ctl);
+	if(lno == nil){
+		sys->werrstr(sys->sprint("listen reading %s/listen: %r", ac.dir));
+		return nil;
+	}
+	return ref Connection(nil, ctl, pdir+"/"+lno);
+
+}
+
+#
+# translate an address [[/netdir/]proto!rem] using /netdir/cs
+# returning (newaddress, clonefile)
+#
+nettrans(addr: string): (string, string)
+{
+	(netdir, proto, rem) := dialparse(addr);
+	if(proto == nil || proto == "net"){
+		sys->werrstr(sys->sprint("bad dial string: %s", addr));
+		return (nil, nil);
+	}
+	if(netdir == nil)
+		netdir = "/net";
+
+	# try to translate using connection server
+	fd := sys->open(netdir+"/cs", Sys->ORDWR);
+	if(fd == nil){
+		# use it untranslated
+		if(rem == nil){
+			sys->werrstr(sys->sprint("bad dial string: %s", addr));
+			return (nil, nil);
+		}
+		return (rem, netdir+"/"+proto+"/clone");
+	}
+	if(sys->fprint(fd, "%s!%s", proto, rem) < 0)
+		return (nil, nil);
+	sys->seek(fd, big 0, 0);
+	(clonefile, naddr) := csread(fd);
+	if(clonefile == nil)
+		return (nil, nil);
+
+	return (naddr, redir(clonefile, netdir));
+}
+
+csread(fd: ref Sys->FD): (string, string)
+{
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return (nil, nil);
+	line := string buf[0: n];
+	p := lookc(line, ' ');
+	if(p < 0)
+		return (nil, nil);
+	if(p == 0){
+		sys->werrstr("cs: no translation");
+		return (nil, nil);
+	}
+	return (line[0:p], line[p+1:]);
+}
+
+#
+# accept a call, return an fd to the open data file
+#
+accept(c: ref Connection): ref Sys->FD
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	sys->fprint(c.cfd, "accept %s", lastname(c.dir));	# ignore return value, network might not need accepts
+	return sys->open(c.dir+"/data", Sys->ORDWR);
+}
+
+#
+# reject a call, tell device the reason for the rejection
+#
+reject(c: ref Connection, why: string): int
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(sys->fprint(c.cfd, "reject %s %q", lastname(c.dir), why) < 0)
+		return -1;
+	return 0;
+}
+
+lastname(dir: string): string
+{
+	p := backc(dir, len dir-1, '/');
+	if(p < 0)
+		return dir;
+	return dir[p+1:];	# N in /net/N
+}
+
+parent(dir: string): string
+{
+	p := backc(dir, len dir-1, '/');
+	if(p < 0)
+		return nil;
+	return dir[0: p];
+}
+
+netmkaddr(addr, net, svc: string): string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(net == nil)
+		net = "net";
+	(n, nil) := sys->tokenize(addr, "!");
+	if(n <= 1){
+		if(svc== nil)
+			return sys->sprint("%s!%s", net, addr);
+		return sys->sprint("%s!%s!%s", net, addr, svc);
+	}
+	if(svc == nil || n > 2)
+		return addr;
+	return sys->sprint("%s!%s", addr, svc);
+}
+
+netinfo(c: ref Connection): ref Conninfo
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if((dir := c.dir) == nil){
+		if(c.dfd == nil)
+			return nil;
+		dir = parent(sys->fd2path(c.dfd));
+		if(dir == nil)
+			return nil;
+	}
+	ci := ref Conninfo;
+	ci.dir = dir;
+	ci.root = parent(dir);
+	while((p := parent(ci.root)) != nil && p != "/")
+		ci.root = p;
+	(ok, d) := sys->stat(ci.dir);
+	if(ok >= 0)
+		ci.spec = sys->sprint("#%c%d", d.dtype, d.dev);
+	(ci.lsys, ci.lserv) = getendpoint(ci.dir, "local");
+	(ci.rsys, ci.rserv) = getendpoint(ci.dir, "remote");
+	p = parent(ci.dir);
+	if(p == nil)
+		return nil;
+	if(len p >= 5 && p[0:5] == "/net/")
+		p = p[5:];
+	ci.laddr = sys->sprint("%s!%s!%s", p, ci.lsys, ci.lserv);
+	ci.raddr = sys->sprint("%s!%s!%s", p, ci.rsys, ci.rserv);
+	return ci;
+}
+
+getendpoint(dir: string, file: string): (string, string)
+{
+	fd := sys->open(dir+"/"+file, Sys->OREAD);
+	buf := array[128] of byte;
+	if(fd == nil || (n := sys->read(fd, buf, len buf)) <= 0)
+		return ("???", "???");	# compatible, but probably poor defaults
+	if(n > 0 && buf[n-1] == byte '\n')
+		n--;
+	s := string buf[0: n];
+	p := lookc(s, '!');
+	if(p < 0)
+		return (s, "???");
+	return (s[0:p], s[p+1:]);
+}
--- /dev/null
+++ b/appl/lib/dialog.b
@@ -1,0 +1,190 @@
+implement Dialog;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Screen, Rect, Point: import draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	tkclient->init();
+	return nil;
+}
+
+STEP: con 20;
+
+#
+# find upper left corner for subsidiary child window (always at constant
+# position relative to parent)
+#
+localgeom(im: ref Draw->Image): string
+{
+	if (im == nil)
+		return nil;
+
+	return sys->sprint("-x %d -y %d", im.r.min.x+STEP, im.r.min.y+STEP);
+}
+
+centre(t: ref Toplevel)
+{
+	org: Point;
+	org.x = t.image.screen.image.r.dx() / 2 - t.image.r.dx() / 2;
+	org.y = t.image.screen.image.r.dy() / 3 - t.image.r.dy() / 2;
+	if (org.y < 0)
+		org.y = 0;
+	cmd(t, ". configure -x " + string org.x + " -y " + string org.y);
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	n := len a;
+	for(i := 0; i < n; i++)
+		tk->cmd(top, a[i]);
+}
+
+dialog_config := array[] of {
+	"label .top.ico",
+	"label .top.msg",
+	"frame .top -relief raised -bd 1",
+	"frame .bot -relief raised -bd 1",
+	"pack .top.ico -side left -padx 10 -pady 10",
+	"pack .top.msg -side left -expand 1 -fill both -padx 10 -pady 10",
+	"pack .Wm_t .top .bot -side top -fill both",
+	"focus ."
+};
+
+prompt(ctxt: ref Draw->Context,
+	parent: ref Draw->Image,
+	ico: string,
+	title:string,
+	msg: string,
+	dflt: int,
+	labs : list of string): int
+{
+	where := localgeom(parent);
+
+	(t, tc) := tkclient->toplevel(ctxt, where, title, Tkclient->Popup);
+
+	d := chan of string;
+	tk->namechan(t, d, "d");
+
+	tkcmds(t, dialog_config);
+	cmd(t, ".top.msg configure -text '" + msg);
+	if (ico != nil)
+		cmd(t, ".top.ico configure -bitmap " + ico);
+
+	n := len labs;
+	for(i := 0; i < n; i++) {
+		cmd(t, "button .bot.button" +
+				string(i) + " -command {send d " +
+				string(i) + "} -text '" + hd labs);
+
+		if(i == dflt) {
+			cmd(t, "frame .bot.default -relief sunken -bd 1");
+			cmd(t, "pack .bot.default -side left -expand 1 -padx 10 -pady 8");
+			cmd(t, "pack .bot.button" + string i +
+				" -in .bot.default -side left -padx 10 -pady 8 -ipadx 8 -ipady 4");
+		}
+		else
+			cmd(t, "pack .bot.button" + string i +
+				" -side left -expand 1 -padx 10 -pady 10 -ipadx 8 -ipady 4");
+		labs = tl labs;
+	}
+
+	if(dflt >= 0)
+		cmd(t, "bind . <Key-\n> {send d " + string dflt + "}");
+
+	e := cmd(t, "variable lasterror");
+	if(e != "") {
+		sys->fprint(sys->fildes(2), "Dialog error: %s\n", e);
+		return dflt;
+	}
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd" :: "ptr" :: nil);
+	cmd(t, "update");
+
+	for(;;) alt {
+	c := <-t.ctxt.kbd =>
+		tk->keyboard(t, c);
+	p := <-t.ctxt.ptr =>
+		tk->pointer(t, *p);
+	c := <-t.ctxt.ctl or
+	c = <-t.wreq =>
+		tkclient->wmctl(t, c);
+	ans := <-d =>
+		return int ans;
+	tcs := <-tc =>
+		if(tcs == "exit")
+			return dflt;
+		tkclient->wmctl(t, tcs);
+	}
+
+}
+
+getstring_config := array[] of {
+	"label .lab",
+	"entry .ent -relief sunken -bd 2 -width 200",
+	"pack .lab .ent -side left",
+	"bind .ent <Key-\n> {send f 1}",
+	"focus .ent"
+};
+
+getstring(ctxt: ref Draw->Context, parent: ref Draw->Image, msg: string): string
+{
+	where := localgeom(parent);
+	(t, wmctl) := tkclient->toplevel(ctxt, where + " -borderwidth 2 -relief raised", nil, Tkclient->Popup);
+	f := chan of string;
+	tk->namechan(t, f, "f");
+
+	tkcmds(t, getstring_config);
+	cmd(t, ".lab configure -text '" + msg + ":   ");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd" :: "ptr" :: nil);
+
+	e := tk->cmd(t, "variable lasterror");
+	if(e != "") {
+		sys->print("getstring error: %s\n", e);
+		return "";
+	}
+	cmd(t, "update");
+
+	for(;;)alt{
+	c := <-t.ctxt.kbd =>
+		tk->keyboard(t, c);
+	p := <-t.ctxt.ptr =>
+		tk->pointer(t, *p);
+	c := <-t.ctxt.ctl or
+	c = <-wmctl =>
+		if(c == "exit")
+			return nil;
+		tkclient->wmctl(t, c);
+	<-f =>
+		return tk->cmd(t, ".ent get");
+	}
+}
+Showtk: con 0;
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	if (Showtk)
+		sys->print("%s\n", s);
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "Dialog: tk error %s on '%s'\n", e, s);
+	return e;
+}
--- /dev/null
+++ b/appl/lib/dict.b
@@ -1,0 +1,57 @@
+implement Dictionary;
+
+#
+# This is intended to be a simple dictionary of string tuples
+# It is not intended for large data sets or efficient deletion of keys
+#
+
+include "dict.m";
+
+Dict.add( d: self ref Dict, e: (string, string) )
+{
+	if (d.entries == nil) 
+		d.entries =  e::nil;
+	else 
+		d.entries = e::d.entries;
+}
+
+Dict.delete( d: self ref Dict, k: string )
+{
+	key : string;
+	newlist : list of (string, string);
+	temp := d.entries;
+
+	while (temp != nil) {
+		(key,nil) = hd temp;
+		if (key != k)
+			newlist = (hd temp)::newlist;
+		temp = tl temp;
+	}
+	d.entries = newlist;
+}
+
+Dict.lookup( d: self ref Dict, k: string ) :string
+{
+	key, value :string;
+	temp := d.entries;
+	while (temp != nil) {
+		(key,value) = hd temp;
+		if (key == k)
+			return value;
+		temp = tl temp;
+	}
+	return nil;
+}
+
+Dict.keys( d: self ref Dict ) :list of string
+{
+	key: string;
+	keylist : list of string;
+	temp := d.entries;
+	while (temp != nil) {
+		(key, nil) = hd temp;
+		keylist = key::keylist;
+		temp = tl temp;
+	}
+	return keylist;
+}
--- /dev/null
+++ b/appl/lib/dividers.b
@@ -1,0 +1,242 @@
+implement Dividers;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "dividers.m";
+
+Lay: adt {
+	d: int;
+	x: fn(l: self Lay, p: Point): int;
+	y: fn(l: self Lay, p: Point): int;
+	mkr: fn(l: self Lay, r: Rect): Rect;
+	mkpt: fn(l: self Lay, p: Point): Point;
+};
+
+DIVHEIGHT: con 6;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+}
+
+# dir is direction in which to stack widgets (NS or EW)
+Divider.new(win: ref Tk->Toplevel, w: string, wl: list of string, dir: int): (ref Divider, chan of string)
+{
+	lay := Lay(dir);
+	n := len wl;
+	d := ref Divider(win, w, nil, dir, array[n] of {* => ref DWidget}, (0, 0));
+	p := Point(0, 0);
+	for (i := 0; wl != nil; (wl, i) = (tl wl, i+1)) {
+		sz := lay.mkpt(wsize(win, hd wl));
+		*d.widgets[i] = (hd wl, (p, p.add(sz)), sz);
+		if (sz.x > d.canvsize.x)
+			d.canvsize.x = sz.x;
+		p.y += sz.y + DIVHEIGHT;
+	}
+	d.canvsize.y = p.y - DIVHEIGHT;
+	cmd(win, "canvas " + d.w + " -width " + string lay.x(d.canvsize) +
+			" -height " + string lay.y(d.canvsize));
+	ech := chan of string;
+	echname := "dw" + d.w;
+	tk->namechan(win, ech, echname);
+	for (i = 0; i < n; i++) {
+		dw := d.widgets[i];
+		dw.r.max.x = d.canvsize.x + dw.r.min.x;
+		sz := dxy(dw.r);
+		cmd(win, d.w + " create window " + p2s(lay.mkpt(dw.r.min)) +
+			" -window " + dw.w +
+			" -tags w" + string i + " -anchor nw" +
+			" -width " + string lay.x(sz) +
+			" -height " + string lay.y(sz));
+		cmd(win, "pack propagate " + dw.w + " 0");
+		if (i < n - 1) {
+			r := lay.mkr(((dw.r.min.x, dw.r.max.y),
+					(dw.r.max.x, dw.r.max.y + DIVHEIGHT)));
+			cmd(win, d.w + " create rectangle " + r2s(r) +
+				" -fill red" +
+				" -tags d" + string i);
+			cmd(win, d.w + " bind d" + string i + " <Button-1>" +
+				" {send " + echname + " but " + string i + " %x %y}");
+			cmd(win, d.w + " bind d" + string i + " <Motion-Button-1> {}");
+			cmd(win, d.w + " bind d" + string i + " <ButtonRelease-1>" +
+				" {send " + echname + " up x %x %y}");
+		}
+	}
+	cmd(win, d.w + " create rectangle -2 -2 -1 -1 -tags grab");
+	cmd(win, d.w + " bind grab <Button-1> {send " + echname + " drag x %x %y}");
+	cmd(win, d.w + " bind grab <ButtonRelease-1> {send " + echname + " up x %x %y}");
+	cmd(win, "bind " + d.w + " <Configure> {send " + echname + " config x x x}");
+	return (d, ech);
+}
+
+Divider.event(d: self ref Divider, e: string)
+{
+	(n, toks) := sys->tokenize(e, " ");
+	if (n != 4) {
+		sys->print("dividers: invalid event %s\n", e);
+		return;
+	}
+	lay := Lay(d.dir);
+	p := lay.mkpt((int hd tl tl toks, int hd tl tl tl toks));
+	t := hd toks;
+	if (t == "but" && d.state != nil)
+		t = "drag";
+	case t {
+	"but" =>
+		if (d.state != nil) {
+			sys->print("dividers: event '%s' received in drag mode\n", e);
+			return;
+		}
+		div := int hd tl toks;
+		d.state = ref DState;
+		d.state.dragdiv = div;
+		d.state.dy = p.y - d.widgets[div].r.max.y;
+		d.state.maxy = d.widgets[div+1].r.max.y - DIVHEIGHT;
+		d.state.miny = d.widgets[div].r.min.y;
+		cmd(d.win, d.w + " itemconfigure d" + string div + " -fill orange");
+		cmd(d.win, d.w + " raise d" + string div);
+		cmd(d.win, d.w + " coords grab -10000 -10000 10000 10000");
+		cmd(d.win, "grab set " + d.w);
+		cmd(d.win, "update");
+	"drag" =>
+		if (d.state == nil) {
+			sys->print("dividers: event '%s' received in non-drag mode\n", e);
+			return;
+		}
+		div := d.state.dragdiv;
+		ypos := p.y - d.state.dy;
+		if (ypos > d.state.maxy)
+			ypos = d.state.maxy;
+		else if (ypos < d.state.miny)
+			ypos = d.state.miny;
+		r := Rect((0, ypos), (d.canvsize.x, ypos + DIVHEIGHT));
+		cmd(d.win, d.w + " coords d" + string div + " " + r2s(lay.mkr(r)));
+		d.widgets[div].r.max.y = ypos;
+		d.widgets[div+1].r.min.y = ypos + DIVHEIGHT;
+		relayout(d);
+		cmd(d.win, "update");
+	"up" =>
+		if (d.state == nil) {
+			sys->print("dividers: event '%s' received in non-drag mode\n", e);
+			return;
+		}
+		div := d.state.dragdiv;
+		cmd(d.win, d.w + " itemconfigure d" + string div + " -fill red");
+		cmd(d.win, d.w + " coords grab -2 -2 -1 -1");
+		cmd(d.win, "grab release " + d.w);
+		cmd(d.win, "update");
+		d.state = nil;
+	"config" =>
+		resize(d);
+		cmd(d.win, "update");
+	}
+}
+
+# lay out widgets according to rectangles that have been already specified.
+relayout(d: ref Divider)
+{
+	lay := Lay(d.dir);
+	for (i := 0; i < len d.widgets; i++) {
+		dw := d.widgets[i];
+		sz := dxy(dw.r);
+		szs := " -width " + string lay.x(sz) + " -height " + string lay.y(sz);
+		cmd(d.win, d.w + " coords w" + string i + " " + p2s(lay.mkpt(dw.r.min)));
+		cmd(d.win, d.w + " itemconfigure w" + string i + szs);
+		cmd(d.win, dw.w + " configure" + szs);
+		if (i < len d.widgets - 1) {
+			r := lay.mkr(((dw.r.min.x, dw.r.max.y),
+					(dw.r.max.x, dw.r.max.y + DIVHEIGHT)));
+			cmd(d.win, d.w + " coords d" + string i + " " + r2s(r));
+		}
+	}
+}
+
+# resize based on current actual size of canvas;
+# sections resize proportionate to their previously occupied space.
+# strange things will happen if we're resizing in the middle of a drag...
+resize(d: ref Divider)
+{
+	lay := Lay(d.dir);
+	sz := lay.mkpt((int cmd(d.win, d.w + " cget -actwidth"), 
+			int cmd(d.win, d.w + " cget -actheight")));
+
+	wspace := (len d.widgets - 1) * DIVHEIGHT;
+	y := 0;
+	for (i := 0; i < len d.widgets; i++) {
+		dw := d.widgets[i];
+		prop := real dw.r.dy() / real (d.canvsize.y - wspace);
+		dw.r = ((0, y), (sz.x, y + int (prop * real (sz.y - wspace))));
+		y = dw.r.max.y + DIVHEIGHT;
+	}
+	y -= DIVHEIGHT;
+	# compensate for rounding errors
+	d.widgets[i - 1].r.max.y -= y - sz.y;
+	d.canvsize = sz;
+	relayout(d);
+}
+
+wsize(win: ref Tk->Toplevel, w: string): Point
+{
+	bw := int cmd(win, w + " cget -borderwidth");
+	return Point(int cmd(win, w + " cget -width") + bw*2,
+			int cmd(win, w + " cget -height") + bw*2);
+}
+
+dxy(r: Rect): Point
+{
+	return r.max.sub(r.min);
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+r2s(r: Rect): string
+{
+	return string r.min.x + " " + string r.min.y + " " +
+			string r.max.x + " " + string r.max.y;
+}
+
+Lay.x(l: self Lay, p: Point): int
+{
+	if (l.d == NS)
+		return p.x;
+	return p.y;
+}
+
+Lay.y(l: self Lay, p: Point): int
+{
+	if (l.d == NS)
+		return p.y;
+	return p.x;
+}
+
+Lay.mkr(l: self Lay, r: Rect): Rect
+{
+	if (l.d == NS)
+		return r;
+	return ((r.min.y, r.min.x), (r.max.y, r.max.x));
+}
+
+Lay.mkpt(l: self Lay, p: Point): Point
+{
+	if (l.d == NS)
+		return p;
+	return (p.y, p.x);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->print("dividers: tk error %s on '%s'\n", e, s);
+	return e;
+}
--- /dev/null
+++ b/appl/lib/ecmascript/builtin.b
@@ -1,0 +1,1480 @@
+#
+# utility functions
+#
+biinst(o: ref Obj, bi: Builtin, p: ref Obj, h: ESHostobj): ref Obj
+{
+	bo := mkobj(p, "Function");
+	bo.call = mkcall(nil, bi.params);
+	bo.val = strval(bi.val);
+	bo.host = h;
+	varinstant(bo, DontEnum|DontDelete|ReadOnly, "length", ref RefVal(numval(real bi.length)));
+	varinstant(o, DontEnum, bi.name, ref RefVal(objval(bo)));
+	return bo;
+}
+
+biminst(o: ref Obj, bis: array of Builtin, p: ref Obj, h: ESHostobj)
+{
+	for(i := 0; i < len bis; i++)
+		biinst(o, bis[i], p, h);
+}
+
+biarg(args: array of ref Val, i: int): ref Val
+{
+	if(i < len args)
+		return args[i];
+	return undefined;
+}
+
+#
+# interface to builtin objects
+#
+get(ex: ref Ecmascript->Exec, o: ref Ecmascript->Obj, property: string): ref Ecmascript->Val
+{
+	return esget(ex, o, property, 1);
+}
+
+put(ex: ref Ecmascript->Exec, o: ref Ecmascript->Obj, property: string, val: ref Ecmascript->Val)
+{
+	return esput(ex, o, property, val, 1);
+}
+
+canput(ex: ref Ecmascript->Exec, o: ref Ecmascript->Obj, property: string): ref Ecmascript->Val
+{
+	return escanput(ex, o, property, 1);
+}
+
+hasproperty(ex: ref Ecmascript->Exec, o: ref Ecmascript->Obj, property: string): ref Ecmascript->Val
+{
+	return eshasproperty(ex, o, property, 1);
+}
+
+delete(ex: ref Ecmascript->Exec, o: ref Ecmascript->Obj, property: string)
+{
+	return esdelete(ex, o, property, 1);
+}
+
+defaultval(ex: ref Ecmascript->Exec, o: ref Ecmascript->Obj, tyhint: int): ref Ecmascript->Val
+{
+	return esdefaultval(ex, o, tyhint, 1);
+}
+
+call(ex: ref Ecmascript->Exec, f, this: ref Ecmascript->Obj, args: array of ref Ecmascript->Val, eval: int): ref Ecmascript->Ref
+{
+	x, y: real;
+	v: ref Val;
+
+	if(this == nil)
+		this = ex.global;
+	if(f.host != me)
+		return escall(ex, f, this, args, eval);
+	case f.val.str{
+	"eval" =>
+		v = ceval(ex, f, this, args);
+	"parseInt" =>
+		v = cparseInt(ex, f, this, args);
+	"parseFloat" =>
+		v = cparseFloat(ex, f, this, args);
+	"escape" =>
+		v = cescape(ex, f, this, args);
+	"unescape" =>
+		v = cunescape(ex, f, this, args);
+	"isNaN" =>
+		v = cisNaN(ex, f, this, args);
+	"isFinite" =>
+		v = cisFinite(ex, f, this, args);
+	"decodeURI" =>
+		v = cdecodeuri(ex, f, this, args);
+	"encodeURI" =>
+		v = cencodeuri(ex, f, this, args);
+	"decodeURIComponent" =>
+		v = cdecodeuric(ex, f, this, args);
+	"encodeURIComponent" =>
+		v = cencodeuric(ex, f, this, args);
+	"Object" =>
+		v = cobj(ex, f, this, args);
+	"Object.prototype.toString" or
+	"Object.prototype.toLocaleString" =>
+		v = cobjprototoString(ex, f, this, args);
+	"Object.prototype.valueOf" =>
+		v = cobjprotovalueOf(ex, f, this, args);
+	"Object.prototype.hasOwnProperty" =>
+		v = cobjprotohasownprop(ex, f, this, args);
+	"Object.prototype.isPrototypeOf" =>
+		v = cobjprotoisprotoof(ex, f, this, args);
+	"Object.prototype.propertyisEnumerable" =>
+		v = cobjprotopropisenum(ex, f, this, args);
+	"Function" =>
+		v = objval(nfunc(ex, f, args));
+	"Function.Prototype" =>
+		v = undefined;
+	"Function.prototype.toString" =>
+		v = cfuncprototoString(ex, f, this, args);
+	"Function.prototype.apply" =>
+		v = cfuncprotoapply(ex, f, this, args);
+	"Function.prototype.call" =>
+		v = cfuncprotocall(ex, f, this, args);
+	"Error" =>
+		v = objval(nerr(ex, f, args, ex.errproto));
+	"Error.prototype.toString" =>
+		v = cerrprototoString(ex, f, this, args);
+	"EvalError" =>
+		v = objval(nerr(ex, f, args, ex.evlerrproto));
+	"EvalError.prototype.toString" =>
+		v = cerrprototoString(ex, f, this, args);
+	"RangeError" =>
+		v = objval(nerr(ex, f, args, ex.ranerrproto));
+	"RangeError.prototype.toString" =>
+		v = cerrprototoString(ex, f, this, args);
+	"ReferenceError" =>
+		v = objval(nerr(ex, f, args, ex.referrproto));
+	"ReferenceError.prototype.toString" =>
+		v = cerrprototoString(ex, f, this, args);
+	"SyntaxError" =>
+		v = objval(nerr(ex, f, args, ex.synerrproto));
+	"SyntaxError.prototype.toString" =>
+		v = cerrprototoString(ex, f, this, args);
+	"TypeError" =>
+		v = objval(nerr(ex, f, args, ex.typerrproto));
+	"TypeError.prototype.toString" =>
+		v = cerrprototoString(ex, f, this, args);
+	"URIError" =>
+		v = objval(nerr(ex, f, args, ex.urierrproto));
+	"URIError.prototype.toString" =>
+		v = cerrprototoString(ex, f, this, args);
+	"InternalError" =>
+		v = objval(nerr(ex, f, args, ex.interrproto));
+	"InternalError.prototype.toString" =>
+		v = cerrprototoString(ex, f, this, args);
+	"Array" =>
+		v = objval(narray(ex, f, args));
+	"Array.prototype.toString" or "Array.prototype.toLocaleString" =>
+		v = carrayprototoString(ex, f, this, args);
+	"Array.prototype.concat" =>
+		v = carrayprotoconcat(ex, f, this, args);
+	"Array.prototype.join" =>
+		v = carrayprotojoin(ex, f, this, args);
+	"Array.prototype.pop" =>
+		v = carrayprotopop(ex, f, this, args);
+	"Array.prototype.push" =>
+		v = carrayprotopush(ex, f, this, args);
+	"Array.prototype.reverse" =>
+		v = carrayprotoreverse(ex, f, this, args);
+	"Array.prototype.shift" =>
+		v = carrayprotoshift(ex, f, this, args);
+	"Array.prototype.slice" =>
+		v = carrayprotoslice(ex, f, this, args);
+	"Array.prototype.splice" =>
+		v = carrayprotosplice(ex, f, this, args);
+	"Array.prototype.sort" =>
+		v = carrayprotosort(ex, f, this, args);
+	"Array.prototype.unshift" =>
+		v = carrayprotounshift(ex, f, this, args);
+	"String" =>
+		v = cstr(ex, f, this, args);
+	"String.fromCharCode" =>
+		v = cstrfromCharCode(ex, f, this, args);
+	"String.prototype.toString" =>
+		v = cstrprototoString(ex, f, this, args);
+	"String.prototype.valueOf" =>
+		v = cstrprototoString(ex, f, this, args);
+	"String.prototype.charAt" =>
+		v = cstrprotocharAt(ex, f, this, args);
+	"String.prototype.charCodeAt" =>
+		v = cstrprotocharCodeAt(ex, f, this, args);
+	"String.prototype.concat" =>
+		v = cstrprotoconcat(ex, f, this, args);
+	"String.prototype.indexOf" =>
+		v = cstrprotoindexOf(ex, f, this, args);
+	"String.prototype.lastIndexOf" =>
+		v = cstrprotolastindexOf(ex, f, this, args);
+	"String.prototype.localeCompare" =>
+		v = cstrprotocmp(ex, f, this, args);
+	"String.prototype.slice" =>
+		v = cstrprotoslice(ex, f, this, args);
+	"String.prototype.split" =>
+		v = cstrprotosplit(ex, f, this, args);
+	"String.prototype.substr" =>
+		v = cstrprotosubstr(ex, f, this, args);
+	"String.prototype.substring" =>
+		v = cstrprotosubstring(ex, f, this, args);
+	"String.prototype.toLowerCase" or "String.prototype.toLocaleLowerCase" =>
+		v = cstrprototoLowerCase(ex, f, this, args);
+	"String.prototype.toUpperCase" or "String.prototype.toLocaleUpperCase" =>
+		v = cstrprototoUpperCase(ex, f, this, args);
+	"String.prototype.match" =>
+		v = cstrprotomatch(ex, f, this, args);
+	"String.prototype.replace" =>
+		v = cstrprotoreplace(ex, f, this, args);
+	"String.prototype.search" =>
+		v = cstrprotosearch(ex, f, this, args);
+# JavaScript 1.0
+	"String.prototype.anchor" or
+	"String.prototype.big" or
+	"String.prototype.blink" or
+	"String.prototype.bold" or
+	"String.prototype.fixed" or
+	"String.prototype.fontcolor" or
+	"String.prototype.fontsize" or
+	"String.prototype.italics" or
+	"String.prototype.link" or
+	"String.prototype.small" or
+	"String.prototype.strike" or
+	"String.prototype.sub" or
+	"String.prototype.sup" =>
+		s := toString(ex, objval(this));
+		arg := toString(ex, biarg(args, 0));
+		tag, endtag: string;
+		case f.val.str{
+		"String.prototype.anchor" =>
+			tag = "<A NAME=\"" + arg + "\">";
+			endtag = "</A>";
+		"String.prototype.big" =>
+			tag = "<BIG>";
+			endtag = "</BIG>";
+		"String.prototype.blink" =>
+			tag = "<BLINK>";
+			endtag = "</BLINK>";
+		"String.prototype.bold" =>
+			tag = "<B>";
+			endtag = "</B>";
+		"String.prototype.fixed" =>
+			tag = "<TT>";
+			endtag = "</TT>";
+		"String.prototype.fontcolor" =>
+			tag = "<FONT COLOR=\"" + arg + "\">";
+			endtag = "</FONT>";
+		"String.prototype.fontsize" =>
+			tag = "<FONT SIZE=\"" + arg + "\">";
+			endtag = "</FONT>";
+		"String.prototype.italics" =>
+			tag = "<I>";
+			endtag = "</I>";
+		"String.prototype.link" =>
+			tag = "<A HREF=\"" + arg + "\">";
+			endtag = "</A>";
+		"String.prototype.small" =>
+			tag = "<SMALL>";
+			endtag = "</SMALL>";
+		"String.prototype.strike" =>
+			tag = "<STRIKE>";
+			endtag = "</STRIKE>";
+		"String.prototype.sub" =>
+			tag = "<SUB>";
+			endtag = "</SUB>";
+		"String.prototype.sup" =>
+			tag = "<SUP>";
+			endtag = "</SUP>";
+		}
+		v = strval(tag + s + endtag);
+	"Boolean" =>
+		v = cbool(ex, f, this, args);
+	"Boolean.prototype.toString" =>
+		v = cboolprototoString(ex, f, this, args);
+	"Boolean.prototype.valueOf" =>
+		v = cboolprotovalueOf(ex, f, this, args);
+	"Number" =>
+		v = cnum(ex, f, this, args);
+	"Number.prototype.toString" or "Number.prototype.toLocaleString" =>
+		v = cnumprototoString(ex, f, this, args);
+	"Number.prototype.valueOf" =>
+		v = cnumprotovalueOf(ex, f, this, args);
+	"Number.prototype.toFixed" =>
+		v = cnumprotofix(ex, f, this, args);
+	"Number.prototype.toExponential" =>
+		v = cnumprotoexp(ex, f, this, args);
+	"Number.prototype.toPrecision" =>
+		v = cnumprotoprec(ex, f, this, args);
+	"RegExp" =>
+		v = cregexp(ex, f, this, args);
+	"RegExp.prototype.exec" =>
+		v = cregexpprotoexec(ex, f, this, args);
+	"RegExp.prototype.test" =>
+		v = cregexpprototest(ex, f, this, args);
+	"RegExp.prototype.toString" =>
+		v = cregexpprototoString(ex, f, this, args);
+	"Math.abs" or
+	"Math.acos" or
+	"Math.asin" or
+	"Math.atan" or
+	"Math.ceil" or
+	"Math.cos" or
+	"Math.exp" or
+	"Math.floor" or
+	"Math.log" or
+	"Math.round" or
+	"Math.sin" or
+	"Math.sqrt" or
+	"Math.tan" =>
+		x = toNumber(ex, biarg(args, 0));
+		case f.val.str{
+		"Math.abs" =>
+			if(x < 0.)
+				x = -x;
+			else if(x == 0.)
+				x = 0.;
+		"Math.acos" =>		x = math->acos(x);
+		"Math.asin" =>		x = math->asin(x);
+		"Math.atan" =>		x = math->atan(x);
+		"Math.ceil" =>		x = math->ceil(x);
+		"Math.cos" =>		x = math->cos(x);
+		"Math.exp" =>		x = math->exp(x);
+		"Math.floor" =>		x = math->floor(x);
+		"Math.log" =>		x = math->log(x);
+		"Math.round" =>		if((x == .0 && copysign(1., x) == -1.)
+					|| (x < .0 && x >= -0.5))
+						x = -0.;
+					else
+						x = math->floor(x+.5);
+		"Math.sin" =>		x = math->sin(x);
+		"Math.sqrt" =>		x = math->sqrt(x);
+		"Math.tan" =>		x = math->tan(x);
+		}
+		v = numval(x);
+	"Math.random" =>
+#		range := big 16r7fffffffffffffff;
+		range := big 1000000000;
+		v = numval(real bigrand(range)/ real range);
+	"Math.atan2" or
+	"Math.max" or
+	"Math.min" or
+	"Math.pow" =>
+		x = toNumber(ex, biarg(args, 0));
+		y = toNumber(ex, biarg(args, 1));
+		case f.val.str{
+		"Math.atan2" =>
+			x = math->atan2(x, y);
+		"Math.max" =>
+			if(x > y)
+				;
+			else if(x < y)
+				x = y;
+			else if(x == y){
+				if(x == 0. && copysign(1., x) == -1. && copysign(1., y) == 1.)
+					x = y;
+			}else
+				x = Math->NaN;
+		"Math.min" =>
+			if(x < y)
+				;
+			else if(x > y)
+				x = y;
+			else if(x == y){
+				if(x == 0. && copysign(1., x) == 1. && copysign(1., y) == -1.)
+					x = y;
+			}else
+				x = Math->NaN;
+		"Math.pow" =>
+			x = math->pow(x, y);
+		}
+		v = numval(x);
+	"Date" =>
+		v = cdate(ex, f, this, args);
+	"Date.parse" =>
+		v = cdateparse(ex, f, this, args);
+	"Date.UTC" =>
+		v = cdateUTC(ex, f, this, args);
+	"Date.prototype.toString" or
+	"Date.prototype.toLocaleString" =>
+		v = cdateprototoString(ex, f, this, args);
+	"Date.prototype.toDateString" or
+	"Date.prototype.toLocaleDateString" =>
+		v = cdateprototoDateString(ex, f, this, args);
+	"Date.prototype.toTimeString" or
+	"Date.prototype.toLocaleTimeString" =>
+		v = cdateprototoTimeString(ex, f, this, args);
+	"Date.prototype.valueOf" or
+	"Date.prototype.getTime" =>
+		v = cdateprotovalueOf(ex, f, this, args);
+	"Date.prototype.getYear" or
+	"Date.prototype.getFullYear" or
+	"Date.prototype.getMonth" or
+	"Date.prototype.getDate" or
+	"Date.prototype.getDay" or
+	"Date.prototype.getHours" or
+	"Date.prototype.getMinutes" or
+	"Date.prototype.getSeconds" =>
+		v = cdateprotoget(ex, f, this, args, !UTC);
+	"Date.prototype.getUTCFullYear" or
+	"Date.prototype.getUTCMonth" or
+	"Date.prototype.getUTCDate" or
+	"Date.prototype.getUTCDay" or
+	"Date.prototype.getUTCHours" or
+	"Date.prototype.getUTCMinutes" or
+	"Date.prototype.getUTCSeconds" =>
+		v = cdateprotoget(ex, f, this, args, UTC);
+	"Date.prototype.getMilliseconds" or
+	"Date.prototype.getUTCMilliseconds" =>
+		v = cdateprotogetMilliseconds(ex, f, this, args);
+	"Date.prototype.getTimezoneOffset" =>
+		v = cdateprotogetTimezoneOffset(ex, f, this, args);
+	"Date.prototype.setTime" =>
+		v = cdateprotosetTime(ex, f, this, args);
+	"Date.prototype.setMilliseconds" =>
+		v = cdateprotosetMilliseconds(ex, f, this, args, !UTC);
+	"Date.prototype.setUTCMilliseconds" =>
+		v = cdateprotosetMilliseconds(ex, f, this, args, UTC);
+	"Date.prototype.setSeconds" =>
+		v = cdateprotosetSeconds(ex, f, this, args, !UTC);
+	"Date.prototype.setUTCSeconds" =>
+		v = cdateprotosetSeconds(ex, f, this, args, UTC);
+	"Date.prototype.setMinutes" =>
+		v = cdateprotosetMinutes(ex, f, this, args, !UTC);
+	"Date.prototype.setUTCMinutes" =>
+		v = cdateprotosetMinutes(ex, f, this, args, UTC);
+	"Date.prototype.setHours" =>
+		v = cdateprotosetHours(ex, f, this, args, !UTC);
+	"Date.prototype.setUTCHours" =>
+		v = cdateprotosetHours(ex, f, this, args, UTC);
+	"Date.prototype.setDate" =>
+		v = cdateprotosetDate(ex, f, this, args, !UTC);
+	"Date.prototype.setUTCDate" =>
+		v = cdateprotosetDate(ex, f, this, args, UTC);
+	"Date.prototype.setMonth" =>
+		v = cdateprotosetMonth(ex, f, this, args, !UTC);
+	"Date.prototype.setUTCMonth" =>
+		v = cdateprotosetMonth(ex, f, this, args, UTC);
+	"Date.prototype.setFullYear" =>
+		v = cdateprotosetFullYear(ex, f, this, args, !UTC);
+	"Date.prototype.setUTCFullYear" =>
+		v = cdateprotosetFullYear(ex, f, this, args, UTC);
+	"Date.prototype.setYear" =>
+		v = cdateprotosetYear(ex, f, this, args);
+	"Date.prototype.toUTCString" or
+	"Date.prototype.toGMTString" =>
+		v = cdateprototoUTCString(ex, f, this, args);
+	* =>
+		v = nil;
+	}
+	if(v == nil)
+		runtime(ex, ReferenceError, "unknown function "+f.val.str+" in builtin call");
+	return valref(v);
+}
+
+rsalt := big 12345678;
+
+randinit(seed: big)
+{
+	rsalt = big seed;
+	bigrand(big 1);
+	bigrand(big 1);
+}
+
+RANDMASK: con (big 1<<63)-(big 1);
+
+bigrand(modulus: big): big
+{
+	rsalt = rsalt * big 1103515245 + big 12345;
+	if(modulus <= big 0)
+		return big 0;
+	return ((rsalt&RANDMASK)>>10) % modulus;
+}
+
+construct(ex: ref Ecmascript->Exec, f: ref Ecmascript->Obj, args: array of ref Ecmascript->Val): ref Ecmascript->Obj
+{
+	if(f.host != me)
+		runtime(ex, TypeError, "ecmascript builtin called incorrectly");
+	case f.val.str{
+	"Object" =>
+		return nobj(ex, f, args);
+	"Function" =>
+		return nfunc(ex, f, args);
+	"Array" =>
+		return narray(ex, f, args);
+	"Error" =>
+		return nerr(ex, f, args, ex.errproto);
+	"EvalError" =>
+		return nerr(ex, f, args, ex.evlerrproto);
+	"RangeError" =>
+		return nerr(ex, f, args, ex.ranerrproto);
+	"ReferenceError" =>
+		return nerr(ex, f, args, ex.referrproto);
+	"SyntaxError" =>
+		return nerr(ex, f, args, ex.synerrproto);
+	"TypeError" =>
+		return nerr(ex, f, args, ex.typerrproto);
+	"URIError" =>
+		return nerr(ex, f, args, ex.urierrproto);
+	"InternalError" =>
+		return nerr(ex, f, args, ex.interrproto);
+	"String" or
+	"Boolean" or
+	"Number" =>
+		return coerceToObj(ex, call(ex, f, nil, args, 0).val).obj;
+	"Date" =>
+		return ndate(ex, f, args);
+	"RegExp" =>
+		return nregexp(ex, f, args);
+	}
+	runtime(ex, ReferenceError, "unknown constructor "+f.val.str+" in builtin construct");
+	return nil;
+}
+
+ceval(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	if(len args < 1)
+		return undefined;
+	vs := coerceToVal(args[0]);
+	if(!isstr(vs))
+		return args[0];
+	(k, v, nil) := eval(ex, vs.str);
+	if(k != CNormal || v == nil)
+		v = undefined;
+	return v;
+}
+
+cparseInt(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	sv := biarg(args, 0);
+	s := toString(ex, sv);
+	neg := 0;
+	i := 0;
+	if(len s > i){
+		if(s[i] == '-'){
+			neg = 1;
+			i++;
+		}else if(s[i] == '+')
+			i++;
+	}
+	rv := biarg(args, 1);
+	if(rv == undefined)
+		r := big 0;
+	else
+		r = big toInt32(ex, rv);
+	if(r == big 0){
+		if(len s > i && s[i] == '0'){
+			r = big 8;
+			if(len s >= i+2 && (s[i+1] == 'x' || s[i+1] == 'X'))
+				r = big 16;
+		}else
+			r = big 10;
+	}else if(r < big 0 || r > big 36)
+		return numval(Math->NaN);
+	if(r == big 16 && len s >= i+2 && s[i] == '0' && (s[i+1] == 'x' || s[i+1] == 'X'))
+		i += 2;
+	ok := 0;
+	n := big 0;
+	for(; i < len s; i++) {
+		c := s[i];
+		v := r;
+		case c {
+		'a' to 'z' =>
+			v = big(c - 'a' + 10);
+		'A' to 'Z' =>
+			v = big(c - 'A' + 10);
+		'0' to '9' =>
+			v = big(c - '0');
+		}
+		if(v >= r)
+			break;
+		ok = 1;
+		n = n * r + v;
+	}
+	if(!ok)
+		return numval(Math->NaN);
+	if(neg)
+		n = -n;
+	return numval(real n);
+}
+
+cparseFloat(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, biarg(args, 0));
+	(nil, r) := parsenum(ex, s, 0, ParseReal|ParseTrim);
+	return numval(r);
+}
+
+cescape(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, biarg(args, 0));
+	t := "";
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		case c{
+		'A' to 'Z' or
+		'a' to 'z' or
+		'0' to '9' or
+		'@' or '*' or '_' or '+' or '-' or '.' or '/' =>
+			t[len t] = s[i];
+		* =>
+			e := "";
+			do{
+				d := c & 16rf;
+				e = "0123456789abcdef"[d:d+1] + e;
+				c >>= 4;
+			}while(c);
+			if(len e & 1)
+				e = "0" + e;
+			if(len e == 4)
+				e = "u" + e;
+			t += "%" + e;
+		}
+	}
+	return strval(t);
+}
+
+cunescape(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, biarg(args, 0));
+	t := "";
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(c == '%'){
+			if(i + 5 < len s && s[i+1] == 'u'){
+				(v, e) := str->toint(s[i+2:i+6], 16);
+				if(e == ""){
+					c = v;
+					i += 5;
+				}
+			}else if(i + 2 < len s){
+				(v, e) := str->toint(s[i+1:i+3], 16);
+				if(e == ""){
+					c = v;
+					i += 2;
+				}
+			}
+		}
+		t[len t] = c;
+	}
+	return strval(t);
+}
+
+cisNaN(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	if(math->isnan(toNumber(ex, biarg(args, 0))))
+		return true;
+	return false;
+}
+
+cisFinite(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	r := toNumber(ex, biarg(args, 0));
+	if(math->isnan(r) || r == +Infinity || r == -Infinity)
+		return false;
+	return true;
+}
+
+cobj(ex: ref Exec, f, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	o: ref Obj;
+
+	v := biarg(args, 0);
+
+	if(isnull(v) || isundefined(v))
+		o = nobj(ex, f, args);
+	else
+		o = toObject(ex, v);
+	return objval(o);
+}
+
+nobj(ex: ref Exec, nil: ref Ecmascript->Obj, args: array of ref Val): ref Ecmascript->Obj
+{
+	o: ref Obj;
+
+	v := biarg(args, 0);
+
+	case v.ty{
+	TNull or TUndef =>
+		o = mkobj(ex.objproto, "Object");
+	TBool =>
+		o = mkobj(ex.boolproto, "Boolean");
+		o.val = v;
+	TStr =>
+		o = mkobj(ex.strproto, "String");
+		o.val = v;
+		varinstant(o, DontEnum|DontDelete|ReadOnly, "length", ref RefVal(numval(real len v.str)));
+	TNum =>
+		o = mkobj(ex.numproto, "Number");
+		o.val = v;
+	TObj =>
+		o = v.obj;
+	TRegExp =>
+		o = mkobj(ex.regexpproto, "RegExp");
+		o.val = v;
+		varinstant(o, DontEnum|DontDelete|ReadOnly, "length", ref RefVal(numval(real len v.rev.p)));
+		varinstant(o, DontEnum|DontDelete|ReadOnly, "source", ref RefVal(strval(v.rev.p)));
+		varinstant(o, DontEnum|DontDelete|ReadOnly, "global", ref RefVal(strhas(v.rev.f, 'g')));
+		varinstant(o, DontEnum|DontDelete|ReadOnly, "ignoreCase", ref RefVal(strhas(v.rev.f, 'i')));
+		varinstant(o, DontEnum|DontDelete|ReadOnly, "multiline", ref RefVal(strhas(v.rev.f, 'm')));
+		varinstant(o, DontEnum|DontDelete, "lastIndex", ref RefVal(numval(real v.rev.i)));
+
+	* =>
+		runtime(ex, ReferenceError, "unknown type in Object constructor");
+	}
+	return o;
+}
+
+cobjprototoString(nil: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	return strval("[object " + this.class + "]");
+}
+
+cobjprotovalueOf(nil: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	return objval(this);
+}
+
+cobjprotohasownprop(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	o := this;
+	s := toString(ex, biarg(args, 0));
+	p := o.prototype;
+	o.prototype = nil;
+	v := eshasproperty(ex, o, s, 0);
+	o.prototype = p;
+	return v;
+}
+
+cobjprotoisprotoof(nil: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	o := this;
+	v := biarg(args, 0);
+	if(!isobj(v))
+		return false;
+	for(p := v.obj.prototype; p != nil; p = p.prototype)
+		if(p == o)
+			return true;
+	return false;
+}
+
+cobjprotopropisenum(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	return eshasenumprop(this, toString(ex, biarg(args, 0)));
+}
+
+nfunc(ex: ref Exec, nil: ref Ecmascript->Obj, args: array of ref Val): ref Ecmascript->Obj
+{
+	params := "";
+	body := "";
+	sep := "";
+	for(i := 0; i < len args - 1; i++){
+		params += sep + toString(ex, args[i]);
+		sep = ",";
+	}
+	if(i < len args)
+		body = toString(ex, args[i]);
+
+	p := mkparser(ex, "function anonymous("+params+"){"+body+"}");
+	fundecl(ex, p, 0);
+	if(p.errors)
+		runtime(ex, SyntaxError, ex.error);
+	if(p.code.vars[0].name != "anonymous")
+		runtime(ex, SyntaxError, "parse failure");
+	return p.code.vars[0].val.val.obj;
+}
+
+cfuncprototoString(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	if(this.call == nil)
+		runtime(ex, TypeError, "Function.prototype.toString called for a non-Function object");
+	return strval(funcprint(ex, this));
+}
+
+nerr(ex: ref Exec, f: ref Ecmascript->Obj, args: array of ref Val, proto: ref Obj): ref Ecmascript->Obj
+{
+	msg := biarg(args, 0);
+	if(msg == undefined)
+		s := "";
+	else
+		s = toString(ex, msg);
+	o := mkobj(proto, f.val.str);
+	varinstant(o, DontEnum|DontDelete|ReadOnly, "length", ref RefVal(numval(real 1)));
+	varinstant(o, DontEnum|DontDelete, "name", ref RefVal(strval(f.val.str)));
+	varinstant(o, DontEnum|DontDelete, "message", ref RefVal(strval(s)));
+	return o;
+}
+
+cfuncprotoapply(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	ar: ref Obj;
+
+	if(this.call == nil || !isfuncobj(this))
+		runtime(ex, TypeError, "Function.prototype.apply called for a non-Function object");
+	v := biarg(args, 0);
+	if(v == null || v == undefined)
+		th := ex.global;
+	else
+		th = coerceToObj(ex, v).obj;
+	v = biarg(args, 1);
+	if(v == null || v == undefined)
+		l := 0;
+	else{
+		if(!isobj(v))
+			runtime(ex, TypeError, "Function.prototype.apply non-array argument");
+		ar = v.obj;
+		v = esget(ex, ar, "length", 0);
+		if(v == undefined)
+			runtime(ex, TypeError, "Function.prototype.apply non-array argument");
+		l = int toUint32(ex, v);
+	}
+	args = array[l] of ref Val;
+	for(i := 0; i < l; i++)
+		args[i] = esget(ex, ar, string i, 0);
+	return  getValue(ex, escall(ex, this, th, args, 0));
+}
+
+cfuncprotocall(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	if(this.call == nil || !isfuncobj(this))
+		runtime(ex, TypeError, "Function.prototype.call called for a non-Function object");
+	v := biarg(args, 0);
+	if(v == null || v == undefined)
+		th := ex.global;
+	else
+		th = coerceToObj(ex, v).obj;
+	return  getValue(ex, escall(ex, this, th, args[1: ], 0));
+}
+
+cerrprototoString(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	return esget(ex, this, "message", 0);
+}
+
+narray(ex: ref Exec, nil: ref Ecmascript->Obj, args: array of ref Val): ref Ecmascript->Obj
+{
+	o := mkobj(ex.arrayproto, "Array");
+	length := big len args;
+	if(length == big 1 && isnum(coerceToVal(args[0]))){
+		length = toUint32(ex, args[0]);
+		varinstant(o, DontEnum|DontDelete, "length", ref RefVal(numval(real length)));
+	}else{
+		varinstant(o, DontEnum|DontDelete, "length", ref RefVal(numval(real length)));
+		for(i := 0; i < len args; i++)
+			esput(ex, o, string i, args[i], 0);
+	}
+
+	return o;
+}
+
+carrayprototoString(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	return carrayprotojoin(ex, nil, this, nil);
+}
+
+carrayprotoconcat(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	v: ref Val;
+	e: ref Obj;
+
+	a := narray(ex, nil, nil);
+	n := 0;
+	nargs := len args;
+	for(i := -1; i < nargs; i++){
+		if(i < 0){
+			e = this;
+			v = objval(e);
+		}
+		else{
+			v = biarg(args, i);
+			if(isobj(v))
+				e = v.obj;
+			else
+				e = nil;
+		}
+		if(e != nil && isarray(e)){
+			leng := int toUint32(ex, esget(ex, e, "length", 0));
+			for(k := 0; k < leng; k++){
+				av := esget(ex, e, string k, 0);
+				if(v != undefined)
+					esput(ex, a, string n, av, 0);
+				n++;
+			}
+		}
+		else{
+			esput(ex, a, string n, v, 0);
+			n++;
+		}
+	}
+	esput(ex, a, "length", numval(real n), 0);
+	return objval(a);
+}
+
+carrayprotojoin(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	length := toUint32(ex, esget(ex, this, "length", 0));
+	sepv := biarg(args, 0);
+	sep := ",";
+	if(sepv != undefined)
+		sep = toString(ex, sepv);
+	s := "";
+	ss := "";
+	for(i := big 0; i < length; i++){
+		tv := esget(ex, this, string i, 0);
+		t := "";
+		if(tv != undefined && !isnull(tv))
+			t = toString(ex, tv);
+		s += ss + t;
+		ss = sep;
+	}
+	return strval(s);
+}
+
+carrayprotoreverse(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	length := toUint32(ex, esget(ex, this, "length", 0));
+	mid := length / big 2;
+	for(i := big 0; i < mid; i++){
+		i1 := string i;
+		v1 := esget(ex, this, i1, 0);
+		i2 := string(length - i - big 1);
+		v2 := esget(ex, this, i2, 0);
+		if(v2 == undefined)
+			esdelete(ex, this, i1, 0);
+		else
+			esput(ex, this, i1, v2, 0);
+		if(v1 == undefined)
+			esdelete(ex, this, i2, 0);
+		else
+			esput(ex, this, i2, v1, 0);
+	}
+	return objval(this);
+}
+
+carrayprotopop(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	leng := toUint32(ex, esget(ex, this, "length", 0));
+	if(leng == big 0){
+		esput(ex, this, "length", numval(0.), 0);
+		return undefined;
+	}
+	ind := string (leng-big 1);
+	v := esget(ex, this, ind, 0);
+	esdelete(ex, this, ind, 0);
+	esput(ex, this, "length", numval(real (leng-big 1)), 0);
+	return v;
+}
+
+carrayprotopush(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	leng := toUint32(ex, esget(ex, this, "length", 0));
+	nargs := len args;
+	for(i := 0; i < nargs; i++)
+		esput(ex, this, string (leng+big i), biarg(args, i), 0);
+	nv := numval(real (leng+big nargs));
+	esput(ex, this, "length", nv, 0);
+	return nv;
+}
+
+carrayprotoshift(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	leng := int toUint32(ex, esget(ex, this, "length", 0));
+	if(leng == 0){
+		esput(ex, this, "length", numval(0.), 0);
+		return undefined;
+	}
+	v0 := esget(ex, this, "0", 0);
+	for(k := 1; k < leng; k++){
+		v := esget(ex, this, string k, 0);
+		if(v == undefined)
+			esdelete(ex, this, string (k-1), 0);
+		else
+			esput(ex, this, string (k-1), v, 0);
+	}
+	esdelete(ex, this, string (leng-1), 0);
+	esput(ex, this, "length", numval(real (leng-1)), 0);
+	return v0;
+}
+
+carrayprotounshift(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	leng := int toUint32(ex, esget(ex, this, "length", 0));
+	nargs := len args;
+	for(i := leng-1; i >= 0; i--){
+		v := esget(ex, this, string i, 0);
+		if(v == undefined)
+			esdelete(ex, this, string (i+nargs), 0);
+		else
+			esput(ex, this, string (i+nargs), v, 0);
+	}
+	for(i = 0; i < nargs; i++)
+		esput(ex, this, string i, biarg(args, i), 0);
+	nv := numval(real (leng+nargs));
+	esput(ex, this, "length", nv, 0);
+	return nv;
+}
+
+carrayprotoslice(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	a := narray(ex, nil, nil);
+	leng := int toUint32(ex, esget(ex, this, "length", 0));
+	start := toInt32(ex, biarg(args, 0));
+	if(start < 0) start += leng;
+	if(start < 0) start = 0;
+	if(start > leng) start = leng;
+	if(biarg(args, 1) == undefined)
+		end := leng;
+	else
+		end = toInt32(ex, biarg(args, 1));
+	if(end < 0) end += leng;
+	if(end < 0) end = 0;
+	if(end > leng) end = leng;
+	n := 0;
+	for(k := start; k < end; k++){
+		v := esget(ex, this, string k, 0);
+		if(v != undefined)
+			esput(ex, a, string n, v, 0);
+		n++;
+	}
+	esput(ex, a, "length", numval(real n), 0);
+	return objval(a);
+}
+
+carrayprotosplice(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	a := narray(ex, nil, nil);
+	leng := int toUint32(ex, esget(ex, this, "length", 0));
+	start := toInt32(ex, biarg(args, 0));
+	if(start < 0) start += leng;
+	if(start < 0) start = 0;
+	if(start > leng) start = leng;
+	delc := toInt32(ex, biarg(args, 1));
+	if(delc < 0) delc = 0;
+	if(start+delc > leng) delc = leng-start;
+	for(k := 0; k < delc; k++){
+		v := esget(ex, this, string (k+start), 0);
+		if(v != undefined)
+			esput(ex, a, string k, v, 0);
+	}
+	esput(ex, a, "length", numval(real delc), 0);
+	nargs := len args - 2;
+	if(nargs < delc){
+		for(k = start; k < leng-delc; k++){
+			v := esget(ex, this, string (k+delc), 0);
+			if(v == undefined)
+				esdelete(ex, this, string (k+nargs), 0);
+			else
+				esput(ex, this, string (k+nargs), v, 0);
+		}
+		for(k = leng; k > leng-delc+nargs; k--)
+			esdelete(ex, this, string (k-1), 0);
+	}
+	else if(nargs > delc){
+		for(k = leng-delc; k > start; k--){
+			v := esget(ex, this, string (k+delc-1), 0);
+			if(v == undefined)
+				esdelete(ex, this, string (k+nargs-1), 0);
+			else
+				esput(ex, this, string (k+nargs-1), v, 0);
+		}
+	}
+	for(k = start; k < start+nargs; k++)
+		esput(ex, this, string k, biarg(args, k-start+2), 0);
+	esput(ex, this, "length", numval(real (leng-delc+nargs)), 0);
+	return objval(a);
+}
+
+carrayprotosort(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	length := toUint32(ex, esget(ex, this, "length", 0));
+	cmp := biarg(args, 0);
+	if(cmp == undefined)
+		cmp = nil;
+	else if(!isobj(cmp) || cmp.obj.call == nil)
+		runtime(ex, TypeError, "Array.prototype.sort argument is not a function");
+
+	#
+	# shell sort
+	#
+	for(m := (length+big 3)/big 5; m > big 0; m = (m+big 1)/big 3){
+		for(i := length-m; i-- != big 0;){
+			v1, v2 : ref Val = nil;
+			ji := big -1;
+			for(j := i+m; j < length; j += m){
+				if(v1 == nil)
+					v1 = esget(ex, this, string(j-m), 0);
+				v2 = esget(ex, this, string(j), 0);
+				cr : real;
+				if(v1 == undefined && v2 == undefined)
+					cr = 0.;
+				else if(v1 == undefined)
+					cr = 1.;
+				else if(v2 == undefined)
+					cr = -1.;
+				else if(cmp == nil){
+					s1 := toString(ex, v1);
+					s2 := toString(ex, v2);
+					if(s1 < s2)
+						cr = -1.;
+					else if(s1 > s2)
+						cr = 1.;
+					else
+						cr = 0.;
+				}else{
+					#
+					# this value not specified by docs
+					#
+					cr = toNumber(ex, getValue(ex, escall(ex, cmp.obj, this, array[] of {v1, v2}, 0)));
+				}
+				if(cr <= 0.)
+					break;
+				if(v2 == undefined)
+					esdelete(ex, this, string(j-m), 0);
+				else
+					esput(ex, this, string(j-m), v2, 0);
+				ji = j;
+			}
+			if(ji != big -1){
+				if(v1 == undefined)
+					esdelete(ex, this, string(ji), 0);
+				else
+					esput(ex, this, string(ji), v1, 0);
+			}
+		}
+	}
+	return objval(this);
+}
+
+cstr(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := "";
+	if(len args > 0)
+		s = toString(ex, biarg(args, 0));
+	return strval(s);
+}
+
+cstrfromCharCode(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := "";
+	for(i := 0; i < len args; i++)
+		s[i] = toUint16(ex, args[i]);
+	return strval(s);
+}
+
+cstrprototoString(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	if(!isstrobj(this))
+		runtime(ex, TypeError, "String.prototype.toString called on non-String object");
+	return this.val;
+}
+
+cstrprotocharAt(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	rpos := toInteger(ex, biarg(args, 0));
+	if(rpos < 0. || rpos >= real len s)
+		s = "";
+	else{
+		pos := int rpos;
+		s = s[pos: pos+1];
+	}
+	return strval(s);
+}
+
+cstrprotocharCodeAt(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	rpos := toInteger(ex, biarg(args, 0));
+	if(rpos < 0. || rpos >= real len s)
+		c := Math->NaN;
+	else
+		c = real s[int rpos];
+	return numval(c);
+}
+
+cstrprotoindexOf(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	t := toString(ex, biarg(args, 0));
+	rpos := toInteger(ex, biarg(args, 1));
+	if(rpos < 0.)
+		rpos = 0.;
+	else if(rpos > real len s)
+		rpos = real len s;
+	lent := len t;
+	stop := len s - lent;
+	for(i := int rpos; i <= stop; i++)
+		if(s[i:i+lent] == t)
+			break;
+	if(i > stop)
+		i = -1;
+	return numval(real i);
+}
+
+cstrprotolastindexOf(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	t := toString(ex, biarg(args, 0));
+	v := biarg(args, 1);
+	rpos := toNumber(ex, v);
+	if(math->isnan(rpos))
+		rpos = Math->Infinity;
+	else
+		rpos = toInteger(ex, v);
+	if(rpos < 0.)
+		rpos = 0.;
+	else if(rpos > real len s)
+		rpos = real len s;
+	lent := len t;
+	i := len s - lent;
+	if(i > int rpos)
+		i = int rpos;
+	for(; i >= 0; i--)
+		if(s[i:i+lent] == t)
+			break;
+	return numval(real i);
+}
+
+cstrprotosplit(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	a := narray(ex, nil, nil);
+	tv := biarg(args, 0);
+	ai := 0;
+	if(tv == undefined)
+		esput(ex, a, string ai, strval(s), 0);
+	else{
+		t := toString(ex, tv);
+		lent := len t;
+		stop := len s - lent;
+		pos := 0;
+		if(lent == 0){
+			for(; pos < stop; pos++)
+				esput(ex, a, string ai++, strval(s[pos:pos+1]), 0);
+		}else{
+			for(k := pos; k <= stop; k++){
+				if(s[k:k+lent] == t){
+					esput(ex, a, string ai++, strval(s[pos:k]), 0);
+					pos = k + lent;
+					k = pos - 1;
+				}
+			}
+			esput(ex, a, string ai, strval(s[pos:k]), 0);
+		}
+	}
+	return objval(a);
+}
+
+cstrprotosubstring(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	rstart := toInteger(ex, biarg(args, 0));
+	lens := real len s;
+	rend := lens;
+	if(len args >= 2)
+		rend = toInteger(ex, biarg(args, 1));
+	if(rstart < 0.)
+		rstart = 0.;
+	else if(rstart > lens)
+		rstart = lens;
+	if(rend < 0.)
+		rend = 0.;
+	else if(rend > lens)
+		rend = lens;
+	if(rstart > rend){
+		lens = rstart;
+		rstart = rend;
+		rend = lens;
+	}
+	return strval(s[int rstart: int rend]);
+}
+
+cstrprotosubstr(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	ls := len s;
+	start := toInt32(ex, biarg(args, 0));
+	if(biarg(args, 1) == undefined)
+		leng := ls;
+	else
+		leng = toInt32(ex, biarg(args, 1));
+	if(start < 0)
+		start += ls;
+	if(start < 0)
+		start = 0;
+	if(leng < 0)
+		leng = 0;
+	if(start+leng > ls)
+		leng = ls-start;
+	if(leng <= 0)
+		s = "";
+	else
+		s = s[start: start+leng];
+	return strval(s);
+}
+
+cstrprotoslice(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	ls := len s;
+	start := toInt32(ex, biarg(args, 0));
+	if(biarg(args, 1) == undefined)
+		end := ls;
+	else
+		end = toInt32(ex, biarg(args, 1));
+	if(start < 0)
+		start += ls;
+	if(start < 0)
+		start = 0;
+	if(start > ls)
+		start = ls;
+	if(end < 0)
+		end += ls;
+	if(end < 0)
+		end = 0;
+	if(end > ls)
+		end = ls;
+	leng := end-start;
+	if(leng < 0)
+		leng = 0;
+	return strval(s[start: start+leng]);
+}
+
+cstrprotocmp(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	t := toString(ex, biarg(args, 0));
+	r := 0;
+	if(s < t)
+		r = -1;
+	else if(s > t)
+		r = 1;
+	return numval(real r);
+}
+
+cstrprotoconcat(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	n := len args;
+	for(i := 0; i < n; i++)
+		s += toString(ex, biarg(args, i));
+	return strval(s);
+}
+	
+# this doesn't use unicode tolower
+cstrprototoLowerCase(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	for(i := 0; i < len s; i++)
+		s[i] = tolower(s[i]);
+	return strval(s);
+}
+
+#this doesn't use unicode toupper
+cstrprototoUpperCase(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	s := toString(ex, objval(this));
+	for(i := 0; i < len s; i++)
+		s[i] = toupper(s[i]);
+	return strval(s);
+}
+
+cbool(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	return toBoolean(ex, biarg(args, 0));
+}
+
+tolower(c: int): int
+{
+	if(c >= 'A' && c <= 'Z')
+		return c - 'A' + 'a';
+	return c;
+}
+
+toupper(c: int): int
+{
+	if(c >= 'a' && c <= 'z')
+		return c - 'a' + 'A';
+	return c;
+}
+
+cboolprototoString(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	if(!isboolobj(this))
+		runtime(ex, TypeError, "Boolean.prototype.toString called on non-Boolean object");
+	return strval(toString(ex, this.val));
+}
+
+cboolprotovalueOf(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	if(!isboolobj(this))
+		runtime(ex, TypeError, "Boolean.prototype.valueOf called on non-Boolean object");
+	return this.val;
+}
+
+cnum(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	r := 0.;
+	if(len args > 0)
+		r = toNumber(ex, biarg(args, 0));
+	return numval(r);
+}
+
+cnumprototoString(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	if(!isnumobj(this))
+		runtime(ex, TypeError, "Number.prototype.toString called on non-Number object");
+	return this.val;
+}
+
+cnumprotovalueOf(ex: ref Exec, nil, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	if(!isnumobj(this))
+		runtime(ex, TypeError, "Number.prototype.valueOf called on non-Number object");
+	return strval(toString(ex, this.val));
+}
+
+cnumprotofix(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	if(!isnumobj(this))
+		runtime(ex, TypeError, "Number.prototype.toFixed called on non-Number object");
+	v := biarg(args, 0);
+	if(v == undefined)
+		f := 0;
+	else
+		f = toInt32(ex, v);
+	if(f < 0 || f > 20)
+		runtime(ex, RangeError, "fraction digits out of range");
+	x := toNumber(ex, this.val);
+	if(isnan(x) || x == Infinity || x == -Infinity)
+		s := toString(ex, this.val);
+	else
+		s = sys->sprint("%.*f", f, x);
+	return strval(s);
+}
+
+cnumprotoexp(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	if(!isnumobj(this))
+		runtime(ex, TypeError, "Number.prototype.toExponential called on non-Number object");
+	v := biarg(args, 0);
+	if(v == undefined)
+		f := 6;
+	else
+		f = toInt32(ex, v);
+	if(f < 0 || f > 20)
+		runtime(ex, RangeError, "fraction digits out of range");
+	x := toNumber(ex, this.val);
+	if(isnan(x) || x == Infinity || x == -Infinity)
+		s := toString(ex, this.val);
+	else
+		s = sys->sprint("%.*e", f, x);
+	return strval(s);
+}
+
+cnumprotoprec(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	if(!isnumobj(this))
+		runtime(ex, TypeError, "Number.prototype.toPrecision called on non-Number object");
+	v := biarg(args, 0);
+	if(v == undefined)
+		return strval(toString(ex, this.val));
+	p := toInt32(ex, v);
+	if(p < 1 || p > 21)
+		runtime(ex, RangeError, "fraction digits out of range");
+	x := toNumber(ex, this.val);
+	if(isnan(x) || x == Infinity || x == -Infinity)
+		s := toString(ex, this.val);
+	else{
+		y := x;
+		if(y < 0.0)
+			y = -y;
+		er := math->log10(y);
+		(e, ef) := math->modf(er);
+		if(ef < 0.0)
+			e--;
+		if(e < -6 || e >=p)
+			s = sys->sprint("%.*e", p-1, x);
+		else
+			s = sys->sprint("%.*f", p-1, x);
+	}
+	return strval(s);
+}
--- /dev/null
+++ b/appl/lib/ecmascript/date.b
@@ -1,0 +1,495 @@
+# the Date object is founded on the Daytime module
+
+UTC: con 1;
+msPerSec: con big 1000;
+
+# based on Daytime->Tm with big fields
+bigTm: adt {
+	ms:	big;
+	sec:	big;
+	min:	big;
+	hour:	big;
+	mday:	big;
+	mon:	big;
+	year:	big;
+	tzoff:	int;
+};
+
+isfinite(r: real): int
+{
+	if(math->isnan(r) || r == +Infinity || r == -Infinity)
+		return 0;
+	return 1;
+}
+
+time2Tm(t: real, utc: int): ref Daytime->Tm
+{
+	secs := int(big t / msPerSec);
+	if(big t % msPerSec < big 0)	# t<0?
+		secs -= 1;
+	if(utc)
+		tm := daytime->gmt(secs);
+	else
+		tm = daytime->local(secs);
+	return tm;
+}
+
+time2bigTm(t: real, utc: int): ref bigTm
+{
+	tm := time2Tm(t, utc);
+	btm := ref bigTm;
+	btm.ms = big t % msPerSec;
+	if(btm.ms < big 0)
+		btm.ms += msPerSec;
+	btm.sec = big tm.sec;
+	btm.min = big tm.min;
+	btm.hour = big tm.hour;
+	btm.mday = big tm.mday;
+	btm.mon = big tm.mon;
+	btm.year = big tm.year;
+	btm.tzoff = tm.tzoff;
+	return btm;
+}
+
+bigTm2time(btm: ref bigTm): real
+{
+	# normalize
+	if(btm.mon / big 12 != big 0){
+		btm.year += btm.mon / big 12;
+		btm.mon %= big 12;
+	}
+	if(btm.ms / msPerSec != big 0){
+		btm.sec += btm.ms / msPerSec;
+		btm.ms %= msPerSec;
+	}
+	if(btm.sec / big 60 != big 0){
+		btm.min += btm.sec / big 60;
+		btm.sec %= big 60;
+	}
+	if(btm.min / big 60 != big 0){
+		btm.hour += btm.hour / big 60;
+		btm.min %= big 60;
+	}
+	if(btm.hour / big 24 != big 0){
+		btm.mday += btm.mday / big 24;
+		btm.hour %= big 24;
+	}
+
+	tm := ref Tm;
+	tm.sec = int btm.sec;
+	tm.min = int btm.min;
+	tm.hour = int btm.hour;
+	tm.mday = int btm.mday;
+	tm.mon = int btm.mon;
+	tm.year = int btm.year;
+	tm.tzoff = btm.tzoff;
+	secs := daytime->tm2epoch(tm);
+	# check for out-of-band times
+	if(secs == daytime->tm2epoch(daytime->gmt(secs)))
+		r := real(big secs * msPerSec + btm.ms);
+	else
+		r = Math->NaN;
+	return r;
+}
+
+str2time(s: string): real
+{
+	tm := daytime->string2tm(s);
+	if(tm == nil)
+		r := Math->NaN;
+	else
+		r = real (big daytime->tm2epoch(tm) * msPerSec);
+	return r;
+}
+
+cdate(nil: ref Exec, nil, nil: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	return strval(daytime->time());
+}
+
+# process arguments of Date() [called as constructor] and Date.UTC()
+datectorargs(ex: ref Exec, args: array of ref Val): (int, ref bigTm)
+{
+	x := array[7] of { * => big 0 };
+	ok := 1;
+	for(i := 0; i < 7 && i < len args; i++){
+		if(!isfinite(toNumber(ex, biarg(args, i))))
+			ok = 0;
+		else
+			x[i] = big toInteger(ex, biarg(args, i));
+	}
+	btm := ref bigTm;
+	yr := x[0];
+	if(yr >= big 0 && yr <= big 99)
+		btm.year = yr;
+	else
+		btm.year = yr - big 1900;
+	btm.mon = x[1];
+	btm.mday = x[2];
+	btm.hour = x[3];
+	btm.min = x[4];
+	btm.sec = x[5];
+	btm.ms = x[6];
+	return (ok, btm);
+}
+
+ndate(ex: ref Exec, nil: ref Ecmascript->Obj, args: array of ref Val): ref Ecmascript->Obj
+{
+	o := mkobj(ex.dateproto, "Date");
+	r := Math->NaN;
+	case len args{
+	0 =>
+		r = real(big daytime->now() * msPerSec);
+	1 =>
+		v := toPrimitive(ex, biarg(args, 0), NoHint);
+		if(isstr(v))
+			r = str2time(v.str);
+		else if(isfinite(toNumber(ex, v))){
+			t := big toInteger(ex, v);
+			secs := t / msPerSec;
+			if(big t % msPerSec < big 0)
+				secs -= big 1;
+			if(secs == big int secs)
+				r = real t;
+		}
+	* =>
+		(ok, btm) := datectorargs(ex, args);
+		if(ok){
+			tm := daytime->local(daytime->now());
+			btm.tzoff = tm.tzoff;
+			r = bigTm2time(btm);
+		}
+	}
+	o.val = numval(r);
+	return o;
+}
+
+cdateparse(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	s := toString(ex, biarg(args, 0));
+	return numval(str2time(s));
+}
+
+cdateUTC(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	r := Math->NaN;
+	if(len args == 0)
+		r = real(big daytime->now() * msPerSec);
+	else{
+		(ok, btm) := datectorargs(ex, args);
+		if(ok){
+			btm.tzoff = 0;
+			r = bigTm2time(btm);
+		}
+	}
+	return numval(r);
+}
+
+datecheck(ex: ref Exec, o: ref Ecmascript->Obj, f: string)
+{
+	if(!isdateobj(o))
+		runtime(ex, TypeError, "Date.prototype." + f + " called on non-Date object");
+}
+
+cdateprototoString(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	secs := 0;
+	t := this.val.num;
+	if(!math->isnan(t)){
+		secs = int(big t / msPerSec);
+		if(big t % msPerSec < big 0)
+			secs -= 1;
+	}
+	return strval(daytime->text(daytime->local(secs)));
+}
+
+cdateprototoDateString(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	secs := 0;
+	t := this.val.num;
+	if(!math->isnan(t)){
+		secs = int(big t / msPerSec);
+		if(big t % msPerSec < big 0)
+			secs -= 1;
+	}
+	s := daytime->text(daytime->local(secs));
+	(n, ls) := sys->tokenize(s, " ");
+	if(n < 3)
+		return strval("");
+	return strval(hd ls + " " + hd tl ls + " " + hd tl tl ls);
+}
+
+cdateprototoTimeString(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	secs := 0;
+	t := this.val.num;
+	if(!math->isnan(t)){
+		secs = int(big t / msPerSec);
+		if(big t % msPerSec < big 0)
+			secs -= 1;
+	}
+	s := daytime->text(daytime->local(secs));
+	(n, ls) := sys->tokenize(s, " ");
+	if(n < 4)
+		return strval("");
+	return strval(hd tl tl tl ls);
+}
+
+cdateprotovalueOf(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	return this.val;
+}
+
+cdateprotoget(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val, utc: int): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	t := this.val.num;
+	if(!math->isnan(t)){
+		tm := time2Tm(t, utc);
+		case f.val.str{
+		"Date.prototype.getYear" =>
+			if (tm.year < 0 || tm.year > 99)
+				t = real(tm.year + 1900);
+			else
+				t = real tm.year;
+		"Date.prototype.getFullYear" or
+		"Date.prototype.getUTCFullYear" =>
+			t = real(tm.year + 1900);
+		"Date.prototype.getMonth" or
+		"Date.prototype.getUTCMonth" =>
+			t = real tm.mon;
+		"Date.prototype.getDate" or
+		"Date.prototype.getUTCDate" =>
+			t = real tm.mday;
+		"Date.prototype.getDay" or
+		"Date.prototype.getUTCDay" =>
+			t = real tm.wday;
+		"Date.prototype.getHours" or
+		"Date.prototype.getUTCHours" =>
+			t = real tm.hour;
+		"Date.prototype.getMinutes" or
+		"Date.prototype.getUTCMinutes" =>
+			t = real tm.min;
+		"Date.prototype.getSeconds" or
+		"Date.prototype.getUTCSeconds" =>
+			t = real tm.sec;
+		}
+	}
+	return numval(t);
+}
+
+cdateprotogetMilliseconds(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	t := this.val.num;
+	if(!math->isnan(t)){
+		ms := big t % msPerSec;
+		if(ms < big 0)
+			ms += msPerSec;
+		t = real ms;
+	}
+	return numval(t);
+}
+
+cdateprotogetTimezoneOffset(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	t := this.val.num;
+	if(!math->isnan(t)){
+		tm := time2Tm(t, !UTC);
+		t = real(tm.tzoff / 60);
+	}
+	return numval(t);
+}
+
+# process arguments of Date.prototype.set*() functions
+dateprotosetargs(ex: ref Exec, this: ref Ecmascript->Obj, args: array of ref Val, n: int): (int, big, big, big, big)
+{
+	x := array[4] of { * => big 0 };
+	ok := 1;
+	if(this != nil && !isfinite(this.val.num))
+		ok = 0;
+	for(i := 0; i < n && i < len args; i++){
+		if(!isfinite(toNumber(ex, biarg(args, i))))
+			ok = 0;
+		else
+			x[i] = big toInteger(ex, biarg(args, i));
+	}
+	return (ok, x[0], x[1], x[2], x[3]);
+}
+
+cdateprotosetTime(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, t, nil, nil, nil) := dateprotosetargs(ex, nil, args, 1);
+	if(ok){
+		secs := t / msPerSec;
+		if(big t % msPerSec < big 0)
+			secs -= big 1;
+		if(secs == big int secs)
+			r = real t;
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprotosetMilliseconds(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val, utc: int): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, ms, nil, nil, nil) := dateprotosetargs(ex, this, args, 1);
+	if(ok){
+		btm := time2bigTm(this.val.num, utc);
+		btm.ms = ms;
+		r = bigTm2time(btm);
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprotosetSeconds(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val, utc: int): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, sec, ms, nil, nil) := dateprotosetargs(ex, this, args, 2);
+	if(ok){
+		btm := time2bigTm(this.val.num, utc);
+		btm.sec = sec;
+		if(len args > 1)
+			btm.ms = ms;
+		r = bigTm2time(btm);
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprotosetMinutes(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val, utc: int): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, min, sec, ms, nil) := dateprotosetargs(ex, this, args, 3);
+	if(ok){
+		btm := time2bigTm(this.val.num, utc);
+		btm.min = min;
+		if(len args > 1){
+			btm.sec = sec;
+			if(len args > 2)
+				btm.ms = ms;
+		}
+		r = bigTm2time(btm);
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprotosetHours(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val, utc: int): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, hour, min, sec, ms) := dateprotosetargs(ex, this, args, 4);
+	if(ok){
+		btm := time2bigTm(this.val.num, utc);
+		btm.hour = hour;
+		if(len args > 1){
+			btm.min = min;
+			if(len args > 2){
+				btm.sec = sec;
+				if(len args > 3)
+					btm.ms = ms;
+			}
+		}
+		r = bigTm2time(btm);
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprotosetDate(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val, utc: int): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, day, nil, nil, nil) := dateprotosetargs(ex, this, args, 1);
+	if(ok){
+		btm := time2bigTm(this.val.num, utc);
+		btm.mday = day;
+		r = bigTm2time(btm);
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprotosetMonth(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val, utc: int): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, mon, day, nil, nil) := dateprotosetargs(ex, this, args, 2);
+	if(ok){
+		btm := time2bigTm(this.val.num, utc);
+		btm.mon = mon;
+		if(len args > 1)
+			btm.mday = day;
+		r = bigTm2time(btm);
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprotosetFullYear(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val, utc: int): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, year, mon, day, nil) := dateprotosetargs(ex, nil, args, 3);
+	if(ok){
+		t := this.val.num;
+		if(!isfinite(t))
+			t = 0.;
+		btm := time2bigTm(t, utc);
+		btm.year = (year - big 1900);
+		if(len args > 1){
+			btm.mon = mon;
+			if(len args > 2)
+				btm.mday = day;
+		}
+		r = bigTm2time(btm);
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprotosetYear(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	r := Math->NaN;
+	(ok, year, nil, nil, nil) := dateprotosetargs(ex, nil, args, 1);
+	if(ok){
+		t := this.val.num;
+		if(!isfinite(t))
+			t = 0.;
+		btm := time2bigTm(t, !UTC);
+		if(year >= big 0 && year <= big 99)
+			btm.year = year;
+		else
+			btm.year = year - big 1900;
+		r = bigTm2time(btm);
+	}
+	this.val.num = r;
+	return numval(r);
+}
+
+cdateprototoUTCString(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	datecheck(ex, this, f.val.str);
+	secs := 0;
+	t := this.val.num;
+	if(!math->isnan(t)){
+		secs = int(big t / msPerSec);
+		if(big t % msPerSec < big 0)
+			secs -= 1;
+	}
+	return strval(daytime->text(daytime->gmt(secs)));
+}
--- /dev/null
+++ b/appl/lib/ecmascript/ecmascript.b
@@ -1,0 +1,2626 @@
+implement Ecmascript;
+
+include "sys.m";
+include "math.m";
+include "string.m";
+include "daytime.m";
+include "ecmascript.m";
+
+include "pprint.b";
+include "obj.b";
+include "exec.b";
+include "date.b";
+include "builtin.b";
+include "regexp.b";
+include "uri.b";
+
+FF: con '\u000c';
+LS: con '\u2028';
+PS: con '\u2029';
+
+islt(c: int): int
+{
+	return c == '\n' || c == '\r' || c == LS || c == PS;
+}
+
+me: ESHostobj;
+
+sys: Sys;
+print, sprint: import sys;
+stdout: ref Sys->FD;
+
+math: Math;
+	isnan, floor, copysign, fabs, fmod, NaN, Infinity: import math;
+
+str: String;
+
+daytime: Daytime;
+	Tm: import daytime;
+
+labrec: adt{
+	s: string;	# name
+	k: int;	# kind
+};
+
+HashSize:	con 1024;
+
+Parser: adt
+{
+	ex:		ref Exec;
+
+	code:		ref Code;
+
+	inloop:		int;		# parser state
+	incase:		int;
+	infunc:		int;
+	lastnl:		int;		# parser state for inserting ;
+	notin:		int;		# don't allow `in' in expression
+
+	token:		int;		# lexical token
+	token1:		int;		# lexical token
+	id:		int;		# associated value
+	lineno:		int;
+
+	src:		string;		# lexical input state
+	esrc:		int;
+	srci:		int;
+
+	errors: int;
+	labs:		list of ref labrec;
+};
+
+Keywd: adt
+{
+	name:	string;
+	token:	int;
+};
+
+#
+#	lexical tokens and ops
+#
+	Lbase:	con 128;
+
+	Leos,
+
+	Landas,
+	Loras,
+	Lxoras,
+	Llshas,
+	Lrshas,
+	Lrshuas,
+	Laddas,
+	Lsubas,
+	Lmulas,
+	Ldivas,
+	Lmodas,
+	Loror,
+	Landand,
+	Leq,
+	Lneq,
+	Lleq,
+	Lgeq,
+	Llsh,
+	Lrsh,
+	Lrshu,
+	Linc,
+	Ldec,
+	Lnum,
+	Lid,
+	Lstr,
+	Lthis,
+	Ltypeof,
+	Ldelete,
+	Lvoid,
+	Lwhile,
+	Lfor,
+	Lbreak,
+	Lcontinue,
+	Lwith,
+	Lreturn,
+	Lfunction,
+	Lvar,
+	Lif,
+	Lelse,
+	Lin,
+	Lnew,
+	Lcase,
+	Ldefault,
+	Lswitch,
+	Ldo,
+	Linstanceof,
+	Lcatch,
+	Lfinally,
+	Lthrow,
+	Ltry,
+	Lregexp,
+	Lseq,
+	Lsne,
+	Lprint,
+
+	Lpostinc,		# ops that aren't lexical tokens
+	Lpostdec,
+	Lpresub,
+	Lpreadd,
+	Lcall,
+	Lnewcall,
+	Lgetval,
+	Las,
+	Lasop,
+	Lforin,
+	Lforvar,
+	Lforvarin,
+	Larrinit,
+	Lobjinit,
+	Lnoval,
+	Llabel,
+	Lbreaklab,
+	Lcontinuelab,
+
+	#
+	# reserved words
+	#
+	Labstract,
+	Lboolean,
+	Lbyte,
+	Lchar,
+	Lclass,
+	Lconst,
+	Ldebugger,
+	Ldouble,
+	Lenum,
+	Lexport,
+	Lextends,
+	Lfinal,
+	Lfloat,
+	Lgoto,
+	Limplements,
+	Limport,
+	Lint,
+	Linterface,
+	Llong,
+	Lnative,
+	Lpackage,
+	Lprivate,
+	Lprotected,
+	Lpublic,
+	Lshort,
+	Lstatic,
+	Lsuper,
+	Lsynchronized,
+	Lthrows,
+	Ltransient,
+	Lvolatile:	con Lbase + iota;
+
+
+#
+# internals
+#
+
+Mlower, Mupper, Munder, Mdigit, Msign, Mexp, Mhex, Moct: con byte 1 << iota;
+Malpha:	con Mupper|Mlower|Munder;
+map :=		array[256] of 
+{
+	'_' or '$'		=> Munder,
+	'-' or '+'		=> Msign,
+	'a' to 'd' or 'f'	=> Mlower | Mhex,
+	'e'			=> Mlower | Mhex | Mexp,
+	'g' to 'z'		=> Mlower,
+	'A' to 'D' or 'F'	=> Mupper | Mhex,
+	'E'			=> Mupper | Mhex | Mexp,
+	'G' to 'Z'		=> Mupper,
+	'0' to '7'		=> Mdigit | Mhex | Moct,
+	'8' to '9'		=> Mdigit | Mhex,
+	*			=> byte 0
+};
+
+maxerr:		int;
+toterrors:	int;
+fabort:		int;
+
+escmap :=	array[] of
+{
+	'\'' =>		byte '\'',
+	'"' =>		byte '"',
+	'\\' =>		byte '\\',
+	'b' =>		byte '\b',
+	'f' =>		byte FF,
+	'n' =>		byte '\n',
+	'r' =>		byte '\r',
+	't' =>		byte '\t',
+	'v' =>	byte FF,
+
+	* =>		byte 255
+};
+
+#
+# must be sorted
+#
+keywords := array [] of
+{
+	Keywd("abstract",	Labstract),
+	Keywd("boolean",	Lboolean),
+	Keywd("byte",		Lbyte),
+	Keywd("break",		Lbreak),
+	Keywd("case",		Lcase),
+	Keywd("catch",		Lcatch),
+	Keywd("char",		Lchar),
+	Keywd("class",		Lclass),
+	Keywd("const",		Lconst),
+	Keywd("continue",	Lcontinue),
+	Keywd("debugger",	Ldebugger),
+	Keywd("default",	Ldefault),
+	Keywd("delete",		Ldelete),
+	Keywd("do",		Ldo),
+	Keywd("double",	Ldouble),
+	Keywd("else",		Lelse),
+	Keywd("enum",		Lenum),
+	Keywd("export",		Lexport),
+	Keywd("extends	",	Lextends),
+	Keywd("final",		Lfinal),
+	Keywd("finally",	Lfinally),
+	Keywd("float",		Lfloat),
+	Keywd("for",		Lfor),
+	Keywd("function",	Lfunction),
+	Keywd("goto",		Lgoto),
+	Keywd("if",		Lif),
+	Keywd("implements",	Limplements),
+	Keywd("import",		Limport),
+	Keywd("in",		Lin),
+	Keywd("instanceof",	Linstanceof),
+	Keywd("int",		Lint),
+	Keywd("interface",	Linterface),
+	Keywd("long",		Llong),
+	Keywd("native",	Lnative),
+	Keywd("new",		Lnew),
+	Keywd("package",	Lpackage),
+	# Keywd("print",		Lprint),
+	Keywd("private",	Lprivate),
+	Keywd("protected",	Lprotected),
+	Keywd("public",		Lpublic),
+	Keywd("return",		Lreturn),
+	Keywd("short",		Lshort),
+	Keywd("static",		Lstatic),
+	Keywd("super",		Lsuper),
+	Keywd("switch",		Lswitch),
+	Keywd("synchronized",	Lsynchronized),
+	Keywd("this",		Lthis),
+	Keywd("throw",		Lthrow),
+	Keywd("throws",	Lthrows),
+	Keywd("transient",	Ltransient),
+	Keywd("try",		Ltry),
+	Keywd("typeof",		Ltypeof),
+	Keywd("var",		Lvar),
+	Keywd("void",		Lvoid),
+	Keywd("volatile",	Lvolatile),
+	Keywd("while",		Lwhile),
+	Keywd("with",		Lwith),
+};
+
+debug = array[256] of {* => 0};
+
+glbuiltins := array[] of
+{
+	Builtin("eval", "eval", array[] of {"src"}, 1),
+	Builtin("parseInt", "parseInt", array[] of {"string", "radix"}, 2),
+	Builtin("parseFloat", "parseFloat", array[] of {"string"}, 1),
+	Builtin("escape", "escape", array[] of {"string"}, 1),
+	Builtin("unescape", "unescape", array[] of {"string"}, 1),
+	Builtin("isNaN", "isNaN", array[] of {"number"}, 1),
+	Builtin("isFinite", "isFinite", array[] of {"number"}, 1),
+	Builtin("decodeURI", "decodeURI", array[] of {"string"}, 1),
+	Builtin("decodeURIComponent", "decodeURIComponent", array[] of {"string"}, 1),
+	Builtin("encodeURI", "encodeURI", array[] of {"string"}, 1),
+	Builtin("encodeURIComponent", "encodeURIComponent", array[] of {"string"}, 1),
+};
+
+biobj := Builtin("Object", "Object", array[] of {"value"}, 1);
+biobjproto := array[] of
+{
+	Builtin("toString", "Object.prototype.toString", nil, 0),
+	Builtin("toLocaleString", "Object.prototype.toLocaleString", nil, 0),
+	Builtin("valueOf", "Object.prototype.valueOf", nil, 0),
+	Builtin("hasOwnProperty", "Object.prototype.hasOwnProperty", array[] of {"V"}, 1),
+	Builtin("isPrototypeOf", "Object.prototype.isPrototypeOf", array[] of {"V"}, 1),
+	Builtin("propertyisEnumerable", "Object.prototype.propertyisEnumerable", array[] of {"V"}, 1),
+};
+
+bifunc := Builtin("Function", "Function", array[] of {"body"}, 1);
+bifuncproto := array[] of
+{
+	Builtin("toString", "Function.prototype.toString", nil, 0),
+	Builtin("apply", "Function.prototype.apply", array[] of {"this", "array"}, 2),
+	Builtin("call", "Function.prototype.call", array[] of {"this", "arg"}, 1),
+};
+
+bierr := Builtin("Error", "Error", array[] of {"message"}, 1);
+bierrproto := array[] of
+{
+	Builtin("toString", "Error.prototype.toString", nil , 0),
+};
+
+biarray := Builtin("Array", "Array", array[] of {"length"}, 1);
+biarrayproto := array[] of
+{
+	Builtin("toString", "Array.prototype.toString", nil, 0),
+	Builtin("toLocaleString", "Array.prototype.toLocaleString", nil, 0),
+	Builtin("concat", "Array.prototype.concat", array[] of {"item"}, 1),
+	Builtin("join", "Array.prototype.join", array[] of {"separator"}, 1),
+	Builtin("pop", "Array.prototype.pop", nil, 0),
+	Builtin("push", "Array.prototype.push", array[] of {"item"} , 1),
+	Builtin("reverse", "Array.prototype.reverse", nil, 0),
+	Builtin("shift", "Array.prototype.shift", nil, 0),
+	Builtin("slice", "Array.prototype.slice", array[] of {"start", "end"}, 2),
+	Builtin("splice", "Array.prototype.splice", array[] of {"start", "delcnt", "item"}, 2),
+	Builtin("sort", "Array.prototype.sort", array[] of {"comparefunc"}, 1),
+	Builtin("unshift", "Array.prototype.unshift", array[] of {"item"}, 1),
+};
+
+bistr := Builtin("String", "String", array[] of {"value"}, 1);
+bistrproto := array[] of
+{
+	Builtin("toString", "String.prototype.toString", nil, 0),
+	Builtin("valueOf", "String.prototype.valueOf", nil, 0),
+	Builtin("charAt", "String.prototype.charAt", array[] of {"pos"}, 1),
+	Builtin("charCodeAt", "String.prototype.charCodeAt", array[] of {"pos"}, 1),
+	Builtin("concat", "String.prototype.concat", array[] of {"string"}, 1),
+	Builtin("indexOf", "String.prototype.indexOf", array[] of {"string", "pos"}, 2),
+	Builtin("lastIndexOf", "String.prototype.lastIndexOf", array[] of {"string", "pos"}, 2),
+	Builtin("localeCompare", "String.prototype.localeCompare", array[] of {"that"}, 1),
+	Builtin("slice", "String.prototype.slice", array[] of {"start", "end"}, 2),
+	Builtin("split", "String.prototype.split", array[] of {"separator"}, 2),
+	Builtin("substr", "String.prototype.substr", array[] of {"start", "length"}, 2),
+	Builtin("substring", "String.prototype.substring", array[] of {"start", "end"}, 2),
+	Builtin("toLowerCase", "String.prototype.toLowerCase", nil, 0),
+	Builtin("toUpperCase", "String.prototype.toUpperCase", nil, 0),
+	Builtin("toLocaleLowerCase", "String.prototype.toLocaleLowerCase", nil, 0),
+	Builtin("toLocaleUpperCase", "String.prototype.toLocaleUpperCase", nil, 0),
+# JavaScript 1.0
+	Builtin("anchor", "String.prototype.anchor", array[] of {"name"}, 1),
+	Builtin("big", "String.prototype.big", nil, 0),
+	Builtin("blink", "String.prototype.blink", nil, 0),
+	Builtin("bold", "String.prototype.bold", nil, 0),
+	Builtin("fixed", "String.prototype.fixed", nil, 0),
+	Builtin("fontcolor", "String.prototype.fontcolor", array[] of {"color"}, 1),
+	Builtin("fontsize", "String.prototype.fontsize", array[] of {"size"}, 1),
+	Builtin("italics", "String.prototype.italics", nil, 0),
+	Builtin("link", "String.prototype.link", array[] of {"href"}, 1),
+	Builtin("small", "String.prototype.small", nil, 0),
+	Builtin("strike", "String.prototype.strike", nil, 0),
+	Builtin("sub", "String.prototype.sub", nil, 0),
+	Builtin("sup", "String.prototype.sup", nil, 0),
+	Builtin("match", "String.prototype.match", array[] of {"regexp"}, 1),
+	Builtin("replace", "String.prototype.replace", array[] of {"searchval", "replaceval"}, 2),
+	Builtin("search", "String.prototype.search", array[] of {"regexp"}, 1),
+};
+bistrctor := Builtin("fromCharCode", "String.fromCharCode", array[] of {"characters"}, 1);
+
+bibool := Builtin("Boolean", "Boolean", array[] of {"value"}, 1);
+biboolproto := array[] of
+{
+	Builtin("toString", "Boolean.prototype.toString", nil, 0),
+	Builtin("valueOf", "Boolean.prototype.valueOf", nil, 0),
+};
+
+binum := Builtin("Number", "Number", array[] of {"value"}, 1);
+binumproto := array[] of
+{
+	Builtin("toString", "Number.prototype.toString", nil, 0),
+	Builtin("toLocaleString", "Number.prototype.toLocaleString", nil, 0),
+	Builtin("valueOf", "Number.prototype.valueOf", nil, 0),
+	Builtin("toFixed", "Number.prototype.toFixed", array[] of {"digits"}, 1),
+	Builtin("toExponential", "Number.prototype.toExponential", array[] of {"digits"}, 1),
+	Builtin("toPrecision", "Number.prototype.toPrecision", array[] of {"digits"}, 1),
+};
+
+biregexp := Builtin("RegExp", "RegExp", array[] of {"pattern", "flags"}, 2);
+biregexpproto := array[] of
+{
+	Builtin("exec", "RegExp.prototype.exec", array[] of {"string"}, 1),
+	Builtin("test", "RegExp.prototype.test", array[] of {"string"}, 1),
+	Builtin("toString", "RegExp.prototype.toString", nil, 0),
+};
+
+bidate := Builtin("Date", "Date", array[] of {"value"}, 1);
+bidateproto := array[] of
+{
+	Builtin("toString", "Date.prototype.toString", nil, 0),
+	Builtin("toDateString", "Date.prototype.toDateString", nil, 0),
+	Builtin("toTimeString", "Date.prototype.toTimeString", nil, 0),
+	Builtin("toLocaleString", "Date.prototype.toLocalString", nil, 0),
+	Builtin("toLocaleDateString", "Date.prototype.toLocaleDateString", nil, 0),
+	Builtin("toLocaleTimeString", "Date.prototype.toLocaleTimeString", nil, 0),
+	Builtin("valueOf", "Date.prototype.valueOf", nil, 0),
+	Builtin("getTime", "Date.prototype.getTime", nil, 0),
+	Builtin("getYear", "Date.prototype.getYear", nil, 0),
+	Builtin("getFullYear", "Date.prototype.getFullYear", nil, 0),
+	Builtin("getUTCFullYear", "Date.prototype.getUTCFullYear", nil, 0),
+	Builtin("getMonth", "Date.prototype.getMonth", nil, 0),
+	Builtin("getUTCMonth", "Date.prototype.getUTCMonth", nil, 0),
+	Builtin("getDate", "Date.prototype.getDate", nil, 0),
+	Builtin("getUTCDate", "Date.prototype.getUTCDate", nil, 0),
+	Builtin("getDay", "Date.prototype.getDay", nil, 0),
+	Builtin("getUTCDay", "Date.prototype.getUTCDay", nil, 0),
+	Builtin("getHours", "Date.prototype.getHours", nil, 0),
+	Builtin("getUTCHours", "Date.prototype.getUTCHours", nil, 0),
+	Builtin("getMinutes", "Date.prototype.getMinutes", nil, 0),
+	Builtin("getUTCMinutes", "Date.prototype.getUTCMinutes", nil, 0),
+	Builtin("getSeconds", "Date.prototype.getSeconds", nil, 0),
+	Builtin("getUTCSeconds", "Date.prototype.getUTCSeconds", nil, 0),
+	Builtin("getMilliseconds", "Date.prototype.getMilliseconds", nil, 0),
+	Builtin("getUTCMilliseconds", "Date.prototype.getUTCMilliseconds", nil, 0),
+	Builtin("getTimezoneOffset", "Date.prototype.getTimezoneOffset", nil, 0),
+	Builtin("setTime", "Date.prototype.setTime", array[] of {"time"}, 1),
+	Builtin("setMilliseconds", "Date.prototype.setMilliseconds", array[] of {"ms"}, 1),
+	Builtin("setUTCMilliseconds", "Date.prototype.setUTCMilliseconds", array[] of {"ms"}, 1),
+	Builtin("setSeconds", "Date.prototype.setSeconds", array[] of {"sec", "ms"}, 2),
+	Builtin("setUTCSeconds", "Date.prototype.setUTCSeconds", array[] of {"sec", "ms"}, 2),
+	Builtin("setMinutes", "Date.prototype.setMinutes", array[] of {"min", "sec", "ms"}, 3),
+	Builtin("setUTCMinutes", "Date.prototype.setUTCMinutes", array[] of {"min", "sec", "ms"}, 3),
+	Builtin("setHours", "Date.prototype.setHours", array[] of {"hour", "min", "sec", "ms"}, 4),
+	Builtin("setUTCHours", "Date.prototype.setUTCHours", array[] of {"hour", "min", "sec", "ms"}, 4),
+	Builtin("setDate", "Date.prototype.setDate", array[] of {"date"}, 1),
+	Builtin("setUTCDate", "Date.prototype.setUTCDate", array[] of {"date"}, 1),
+	Builtin("setMonth", "Date.prototype.setMonth", array[] of {"mon", "date"}, 2),
+	Builtin("setUTCMonth", "Date.prototype.setUTCMonth", array[] of {"mon", "date"}, 2),
+	Builtin("setFullYear", "Date.prototype.setFullYear", array[] of {"year", "mon", "date"}, 3),
+	Builtin("setUTCFullYear", "Date.prototype.setUTCFullYear", array[] of {"year", "mon", "date"}, 3),
+	Builtin("setYear", "Date.prototype.setYear", array[] of {"year"}, 1),
+	Builtin("toLocaleString", "Date.prototype.toLocaleString", nil, 0),
+	Builtin("toUTCString", "Date.prototype.toUTCString", nil, 0),
+	Builtin("toGMTString", "Date.prototype.toGMTString", nil, 0),
+};
+bidatector := array[] of
+{
+	Builtin("parse", "Date.parse", array[] of {"string"}, 1),
+	Builtin("UTC", "Date.UTC", array[] of {"year", "month", "date", "hours", "minutes", "seconds", "ms"}, 7),
+};
+
+bimath := array[] of
+{
+	Builtin("abs", "Math.abs", array[] of {"x"}, 1),
+	Builtin("acos", "Math.acos", array[] of {"x"}, 1),
+	Builtin("asin", "Math.asin", array[] of {"x"}, 1),
+	Builtin("atan", "Math.atan", array[] of {"x"}, 1),
+	Builtin("atan2", "Math.atan2", array[] of {"y", "x"}, 2),
+	Builtin("ceil", "Math.ceil", array[] of {"x"}, 1),
+	Builtin("cos", "Math.cos", array[] of {"x"}, 1),
+	Builtin("exp", "Math.exp", array[] of {"x"}, 1),
+	Builtin("floor", "Math.floor", array[] of {"x"}, 1),
+	Builtin("log", "Math.log", array[] of {"x"}, 1),
+	Builtin("max", "Math.max", array[] of {"x", "y"}, 2),
+	Builtin("min", "Math.min", array[] of {"x", "y"}, 2),
+	Builtin("pow", "Math.pow", array[] of {"x", "y"}, 2),
+	Builtin("random", "Math.random", nil, 0),
+	Builtin("round", "Math.round", array[] of {"x"}, 1),
+	Builtin("sin", "Math.sin", array[] of {"x"}, 1),
+	Builtin("sqrt", "Math.sqrt", array[] of {"x"}, 1),
+	Builtin("tan", "Math.tan", array[] of {"x"}, 1),
+};
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	math = load Math Math->PATH;
+	if(math == nil)
+		return sys->sprint("can't load module %s: %r", Math->PATH);
+
+	str = load String String->PATH;
+	if(str == nil)
+		return sys->sprint("can't load module %s: %r", String->PATH);
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return sys->sprint("can't load module %s: %r", Daytime->PATH);
+
+	me = load ESHostobj SELF;
+	if(me == nil)
+		return "can't load builtin functions";
+
+	randinit(big sys->millisec());
+	stdout = sys->fildes(1);
+	#
+	# maximum number of syntax errors reported
+	#
+	maxerr = 1;
+
+	undefined = ref Val(TUndef, 0., nil, nil, nil);
+	null =	ref Val(TNull, 0., nil, nil, nil);
+	true = ref Val(TBool, 1., nil, nil, nil);
+	false = ref Val(TBool, 0., nil, nil, nil);
+	return "";
+}
+
+mkcall(ex : ref Exec, p: array of string): ref Call
+{
+	return ref Call(p, nil, ex);
+}
+
+mkbiobj(ex: ref Exec, meth: Builtin, proto: ref Obj): ref Obj
+{
+	o := biinst(ex.global, meth, ex.funcproto, me);
+	o.construct = o.call;
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "prototype", objval(proto));
+	valinstant(proto, DontEnum, "constructor", objval(o));
+	return o;
+}
+
+mkexec(go: ref Obj): ref Exec
+{
+	o: ref Obj;
+	if(go == nil)
+		go = mkobj(nil, "global");
+	ex := ref Exec;
+	ex.this = go;
+	ex.scopechain = go :: nil;
+	ex.stack = array[4] of ref Ref;
+	ex.sp = 0;
+	ex.global = go;
+
+	#
+	# builtin object prototypes
+	#
+	ex.objproto = mkobj(nil, "Object");
+	ex.funcproto = mkobj(ex.objproto, "Function");
+	ex.arrayproto = mkobj(ex.objproto, "Array");
+	ex.strproto = mkobj(ex.objproto, "String");
+	ex.numproto = mkobj(ex.objproto, "Number");
+	ex.boolproto = mkobj(ex.objproto, "Boolean");
+	ex.dateproto = mkobj(ex.objproto, "Date");
+	ex.regexpproto = mkobj(ex.objproto, "RegExp");
+	ex.errproto = mkobj(ex.objproto, "Error");
+
+	biminst(ex.objproto, biobjproto, ex.funcproto, me);
+
+	biminst(ex.funcproto, bifuncproto, ex.funcproto, me);
+	ex.funcproto.call = mkcall(ex, nil);
+	ex.funcproto.val = strval("Function.prototype");
+	valinstant(ex.funcproto, DontEnum|DontDelete|ReadOnly, "length", numval(real 0));
+
+	biminst(ex.arrayproto, biarrayproto, ex.funcproto, me);
+	valinstant(ex.arrayproto, DontEnum|DontDelete, "length", numval(real 0));
+
+	biminst(ex.errproto, bierrproto, ex.funcproto, me);
+	ex.errproto.val = strval("");
+	valinstant(ex.errproto, DontEnum|DontDelete, "length", numval(real 0));
+	valinstant(ex.errproto, DontEnum|DontDelete, "name", strval(""));
+	valinstant(ex.errproto, DontEnum|DontDelete, "message", strval("Error"));
+
+	biminst(ex.strproto, bistrproto, ex.funcproto, me);
+	ex.strproto.val = strval("");
+	valinstant(ex.strproto, DontEnum|DontDelete|ReadOnly, "length", numval(real 0));
+
+	biminst(ex.boolproto, biboolproto, ex.funcproto, me);
+	ex.boolproto.val = false;
+
+	biminst(ex.numproto, binumproto, ex.funcproto, me);
+	ex.numproto.val = numval(real +0);
+
+	biminst(ex.regexpproto, biregexpproto, ex.funcproto, me);
+	ex.regexpproto.val = strval("");
+	valinstant(ex.regexpproto, DontEnum|DontDelete|ReadOnly, "length", numval(real 2));
+	valinstant(ex.regexpproto, DontEnum|DontDelete|ReadOnly, "source", strval(""));
+	valinstant(ex.regexpproto, DontEnum|DontDelete|ReadOnly, "global", false);
+	valinstant(ex.regexpproto, DontEnum|DontDelete|ReadOnly, "ignoreCase", false);
+	valinstant(ex.regexpproto, DontEnum|DontDelete|ReadOnly, "multiline", false);
+	valinstant(ex.regexpproto, DontEnum|DontDelete, "lastIndex", numval(real 0));
+
+	biminst(ex.dateproto, bidateproto, ex.funcproto, me);
+	ex.dateproto.val = numval(Math->NaN);
+	valinstant(ex.dateproto, DontEnum|DontDelete|ReadOnly, "length", numval(real 7));
+
+	#
+	# simple builtin functions and values
+	#
+	valinstant(go, DontEnum, "NaN", numval(Math->NaN));
+	valinstant(go, DontEnum, "Infinity", numval(Math->Infinity));
+
+	biminst(go, glbuiltins, ex.funcproto, me);
+
+	#
+	# builtin objects, and cross-link them to their prototypes
+	#
+	mkbiobj(ex, biobj, ex.objproto);
+	mkbiobj(ex, bifunc, ex.funcproto);
+	mkbiobj(ex, biarray, ex.arrayproto);
+	o = mkbiobj(ex, bistr, ex.strproto);
+	biinst(o, bistrctor, ex.funcproto, me);
+	mkbiobj(ex, bibool, ex.boolproto);
+	o = mkbiobj(ex, binum, ex.numproto);
+	mkbiobj(ex, biregexp, ex.regexpproto);
+	mkbiobj(ex, bierr, ex.errproto);
+
+	math->FPcontrol(0, Math->INVAL|Math->ZDIV|Math->OVFL|Math->UNFL|Math->INEX);
+
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "MAX_VALUE", numval(math->nextafter(Math->Infinity, 0.)));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "MIN_VALUE", numval(math->nextafter(0., 1.)));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "NaN", numval(Math->NaN));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "NEGATIVE_INFINITY", numval(-Math->Infinity));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "POSITIVE_INFINITY", numval(+Math->Infinity));
+	o = mkbiobj(ex, bidate, ex.dateproto);
+	biminst(o, bidatector, ex.funcproto, me);
+
+	#
+	# the math object is a little different
+	#
+	o = mkobj(ex.objproto, "Object");
+	valinstant(go, DontEnum, "Math", objval(o));
+	biminst(o, bimath, ex.funcproto, me);
+
+	#
+	# these are computed so they are consistent with numbers ecma might calculate
+	#
+	mathe := math->exp(1.);
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "E", numval(mathe));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "LN10", numval(math->log(10.)));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "LN2", numval(math->log(2.)));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "LOG2E", numval(1./math->log(2.)));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "LOG10E", numval(1./math->log(10.)));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "PI", numval(Math->Pi));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "SQRT1_2", numval(math->sqrt(1./2.)));
+	valinstant(o, DontEnum|DontDelete|ReadOnly, "SQRT2", numval(math->sqrt(2.)));
+
+	(EvalError, ex.evlerrproto) = mkerr(ex, "EvalError");
+	(RangeError, ex.ranerrproto) = mkerr(ex, "RangeError");
+	(ReferenceError, ex.referrproto) = mkerr(ex, "ReferenceError");
+	(SyntaxError, ex.synerrproto) = mkerr(ex, "SyntaxError");
+	(TypeError, ex.typerrproto) = mkerr(ex, "TypeError");
+	(URIError, ex.urierrproto) = mkerr(ex, "URIError");
+	(InternalError, ex.interrproto) = mkerr(ex, "InternalError");
+
+	return ex;
+}
+
+mkerr(ex: ref Exec, e: string): (ref Obj, ref Obj)
+{
+	errproto := mkobj(ex.objproto, e);
+	biminst(errproto, array[] of { Builtin("toString", e+".prototype.toString", nil, 0) }, ex.funcproto, me);
+	errproto.val = strval("");
+	valinstant(errproto, DontEnum|DontDelete, "length", numval(real 0));
+	valinstant(errproto, DontEnum|DontDelete, "name", strval(e));
+	valinstant(errproto, DontEnum|DontDelete, "message", strval(e));
+	eo := mkbiobj(ex, Builtin(e, e, array[] of {"message"}, 1), errproto);
+	# return (eo, errproto);
+	return (nerr(ex, eo, array[] of {strval(e)}, errproto), errproto);
+}
+
+mkparser(ex: ref Exec, src: string): ref Parser
+{
+	p := ref Parser;
+	p.ex = ex;
+	p.src = src;
+	p.esrc = len src;
+	p.srci = 0;
+	p.errors = 0;
+	p.lineno = 1;
+	p.token = -1;
+	p.token1 = -1;
+	p.lastnl = 0;
+	p.inloop = 0;
+	p.incase = 0;
+	p.infunc = 0;
+	p.notin = 0;
+	p.code = mkcode();
+	return p;
+}
+
+eval(ex: ref Exec, src: string): Completion
+{
+	{
+		p := mkparser(ex, src);
+
+		if(debug['t'])
+			parset := sys->millisec();
+
+		prog(ex, p);
+
+		toterrors += p.errors;
+
+		if(p.errors)
+			runtime(ex, SyntaxError, ex.error);
+		if(debug['p']){
+			s := array of byte pprint(ex, p.code, "");
+			if(len s)
+				sys->write(stdout, s, len s);
+		}
+
+		if(debug['t'])
+			xect := sys->millisec();
+
+		globalinstant(hd ex.scopechain, p.code.vars);
+		c := exec(ex, p.code);
+
+		if(debug['t'])
+			print("parse time %d exec time %d\n", xect - parset, sys->millisec() - xect);
+
+		return c;
+	}exception{
+		"throw" =>
+			return (CThrow, ex.errval, nil);
+	}
+}
+
+#prog	: selems
+#	;
+#
+#selems	: selem
+#	| selems selem
+#	;
+#selem	: stmt
+#	| fundecl
+#	;
+prog(ex: ref Exec, p: ref Parser)
+{
+	while(look(p) != Leos)
+		if(look(p) == Lfunction)
+			fundecl(ex, p, 0);
+		else
+			stmt(p);
+}
+
+#fundecl	: Lfunction Lid '(' zplist ')' '{' stmts '}'
+#	;
+#zplist	:
+#	| plist
+#	;
+#
+#plist	: Lid
+#	| plist ',' Lid
+#	;
+fundecl(ex: ref Exec, p: ref Parser, expr: int): ref Obj
+{
+	jp: ref Prop;
+
+	c := p.code;
+	labs := p.labs;
+	p.labs = nil;
+	mustbe(p, Lfunction);
+	if(!expr || look(p) == Lid){
+		mustbe(p, Lid);
+		jp = codevar(p, expr);
+	}
+	p.code = mkcode();
+	mustbe(p, '(');
+	if(look(p) != ')'){
+		for(;;){
+			mustbe(p, Lid);
+			codevar(p, 1);
+			if(look(p) == ')')
+				break;
+			mustbe(p, ',');
+		}
+	}
+	params := p.code.vars;
+	p.code.vars = nil;
+	mustbe(p, ')');
+	mustbe(p, '{');
+	p.infunc++;
+	stmts(p);
+	p.infunc--;
+	mustbe(p, '}');
+
+	#
+	# override any existing value,
+	# as per sec. 10.1.3 Variable instantiation
+	#
+	sparams := array[len params] of string;
+	for(i := 0; i < len sparams; i++)
+		sparams[i] = params[i].name;
+
+	#
+	# construct a function object;
+	# see section 15.3.21
+	o := mkobj(ex.funcproto, "Function");
+	o.call = ref Call(sparams, p.code, ex);
+	o.construct = o.call;
+	if(jp != nil)
+		o.val = strval(jp.name);
+	else
+		o.val = strval("");
+	valinstant(o, DontDelete|DontEnum|ReadOnly, "length", numval(real len sparams));
+	po := nobj(ex, nil, nil);
+	valinstant(o, DontEnum, "prototype", objval(po));
+	valinstant(po, DontEnum, "constructor", objval(o));
+	valinstant(o, DontDelete|DontEnum|ReadOnly, "arguments", null);
+	if(jp != nil)
+		jp.val.val = objval(o);
+
+	if(debug['p']){
+		s := array of byte (funcprint(ex, o) + "\n");
+		sys->write(stdout, s, len s);
+	}
+	
+	p.code = c;
+	p.labs = labs;
+	if(expr && jp != nil)
+		popvar(p);
+	return o;
+}
+
+#
+# install a variable for the id just lexed
+#
+codevar(p: ref Parser, forcenew: int): ref Prop
+{
+	name := p.code.strs[p.id];
+	vs := p.code.vars;
+	i : int;
+	if(!forcenew){
+		for(i = 0; i < len vs; i++)
+			if(vs[i].name == name)
+				return vs[i];
+	}else{
+		i = len vs;
+	}
+	vs = array[i+1] of ref Prop;
+	vs[:] = p.code.vars;
+	p.code.vars = vs;
+	vs[i] = ref Prop(0, name, ref RefVal(undefined));
+	return vs[i];
+}
+
+popvar(p: ref Parser)
+{
+	vs := p.code.vars;
+	p.code.vars = vs[0: len vs - 1];
+}
+	
+#stmts	:
+#	| stmts stmt
+#	;
+stmts(p: ref Parser)
+{
+	while((op := look(p)) != '}' && op != Leos)
+		stmt(p);
+}
+
+#stmt	: '{' stmts '}'
+#	| Lvar varlist ';'
+#	| exp ';'
+#	| ';'
+#	| Lif '(' exp ')' stmt
+#	| Lif '(' exp ')' stmt Lelse stmt
+#	| Lwhile '(' exp ')' stmt
+#	| Ldo stmt Lwhile '(' exp ')'
+#	| Lfor '(' zexp-notin ';' zexp ';' zexp ')' stmt
+#	| Lfor '(' Lvar varlist-notin ';' zexp ';' zexp ')' stmt
+#	| Lfor '(' lhsexp Lin exp ')' stmt
+#	| Lfor '(' Lvar Lid [init] Lin exp ')' stmt
+#	| Lcontinue ';'
+#	| Lbreak ';'
+#	| Lreturn zexp ';'	# no line term after return
+#	| Lwith '(' exp ')' stmt
+#	| Lswitch '(' exp ')' '{' caseblk '}'
+#	| Lthrow exp ';'
+#	| Ltry block Lcatch '(' Lid ')' block
+#	| Ltry block finally block
+#	| Ltry block Lcatch '(' Lid ')' block finally block
+#	;
+stmt(p: ref Parser)
+{
+	pc: int;
+
+	seenlabs := 0;
+	while(look(p) == Lid && look2(p) == ':'){
+		pushlab(p, p.code.strs[p.id]);
+		emitconst(p, Llabel, p.id);
+		lex(p);
+		lex(p);
+		seenlabs++;
+	}
+
+	op := look(p);
+	if(seenlabs)
+		setkindlab(p, op, seenlabs);
+	case op{
+	';' =>
+		lexemit(p);
+	'{' =>
+		if(seenlabs == 0){
+			lex(p);
+			stmts(p);
+		}
+		else{
+			lexemit(p);
+			pc = epatch(p);
+			stmts(p);
+			patch(p, pc);
+		}
+		mustbe(p, '}');
+	Lvar =>
+		lexemit(p);
+		pc = epatch(p);
+		varlist(p);
+		semi(p);
+		patch(p, pc);
+	* =>
+		exp(p);
+		semi(p);
+		emit(p, ';');
+	Lif =>
+		lexemit(p);
+		pc = epatch(p);
+		mustbe(p, '(');
+		exp(p);
+		mustbe(p, ')');
+		patch(p, pc);
+		pc = epatch(p);
+		stmt(p);
+		patch(p, pc);
+		pc = epatch(p);
+		if(look(p) == Lelse){
+			lex(p);
+			stmt(p);
+		}
+		patch(p, pc);
+	Lwhile or
+	Lwith =>
+		lexemit(p);
+		pc = epatch(p);
+		mustbe(p, '(');
+		exp(p);
+		mustbe(p, ')');
+		patch(p, pc);
+		if(op == Lwhile)
+			p.inloop++;
+		pc = epatch(p);
+		stmt(p);
+		patch(p, pc);
+		if(op == Lwhile)
+			p.inloop--;
+	Ldo =>
+		p.inloop++;
+		lexemit(p);
+		pc = epatch(p);
+		stmt(p);
+		patch(p, pc);
+		mustbe(p, Lwhile);
+		mustbe(p, '(');
+		pc = epatch(p);
+		exp(p);
+		patch(p, pc);
+		mustbe(p, ')');
+		mustbe(p, ';');
+		p.inloop--;
+	Lfor =>
+		fpc := p.code.npc;
+		lexemit(p);
+		mustbe(p, '(');
+		p.notin++;
+		if(look(p) == Lvar){
+			lex(p);
+			pc = epatch(p);
+			varlist(p);
+			patch(p, pc);
+			p.notin--;
+			if(look(p) == Lin){
+				check1var(p);
+				p.code.ops[fpc] = byte Lforvarin;
+				lex(p);
+				pc = epatch(p);
+				exp(p);
+				patch(p, pc);
+			}else{
+				p.code.ops[fpc] = byte Lforvar;
+				mustbe(p, ';');
+				pc = epatch(p);
+				zexp(p);
+				patch(p, pc);
+				mustbe(p, ';');
+				pc = epatch(p);
+				zexp(p);
+				patch(p, pc);
+			}
+		}else{
+			pc = epatch(p);
+			lhspc := p.code.npc;
+			zexp(p);
+			patch(p, pc);
+			p.notin--;
+			if(look(p) == Lin){
+				p.code.ops[fpc] = byte Lforin;
+				checklhsexp(p, lhspc);
+				lex(p);
+				pc = epatch(p);
+				exp(p);
+				patch(p, pc);
+			}else{
+				mustbe(p, ';');
+				pc = epatch(p);
+				zexp(p);
+				patch(p, pc);
+				mustbe(p, ';');
+				pc = epatch(p);
+				zexp(p);
+				patch(p, pc);
+			}
+		}
+		mustbe(p, ')');
+		p.inloop++;
+		pc = epatch(p);
+		stmt(p);
+		patch(p, pc);
+		p.inloop--;
+	Lcontinue or
+	Lbreak =>
+		lex(p);
+		lab := 0;
+		if(look(p) == Lid){
+			if((lr := findlab(p, p.code.strs[p.id])) == nil)
+				error(p, "missing label");
+			if(op == Lcontinue && !itstmt(lr.k))
+				error(p, "continue label not on iteration statement");
+			if(op == Lbreak)
+				nop := Lbreaklab;
+			else
+				nop = Lcontinuelab;
+			if(!inlocallabs(p, lr, seenlabs))	# otherwise noop
+				emitconst(p, nop, p.id);
+			lex(p);
+			lab = 1;
+		}
+		else
+			emit(p, op);
+		semi(p);
+		if(op == Lbreak && !lab && !p.inloop && !p.incase)
+			error(p, "break not in a do or for or while or case");
+		if(op == Lcontinue && !p.inloop)
+			error(p, "continue not in a do or for or while");
+	Lreturn =>
+		lexemit(p);
+		nextop := look(p);
+		if(nextop != ';' && nextop != '}' && !p.lastnl)
+			exp(p);
+		semi(p);
+		emit(p, ';');
+		if(!p.infunc)
+			error(p, tokname(op)+" not in a function");
+	Lswitch =>
+		lexemit(p);
+		mustbe(p, '(');
+		pc = epatch(p);
+		exp(p);
+		patch(p, pc);
+		mustbe(p, ')');
+		mustbe(p, '{');
+		pc = epatch(p);
+		caseblk(p);
+		patch(p, pc);
+		mustbe(p, '}');
+	Lthrow =>
+		lexemit(p);
+		nextop := look(p);
+		if(!p.lastnl)
+			exp(p);
+		mustbe(p, ';');
+		emit(p, ';');
+	Lprint =>
+		lexemit(p);
+		nextop := look(p);
+		if(!p.lastnl)
+			exp(p);
+		mustbe(p, ';');
+		emit(p, ';');
+	Ltry =>
+		lexemit(p);
+		pc = epatch(p);
+		block(p);
+		patch(p, pc);
+		pc = epatch(p);
+		if(look(p) == Lcatch){
+			lex(p);
+			mustbe(p, '(');
+			mustbe(p, Lid);
+			emitconst(p, Lid, p.id);
+			mustbe(p, ')');
+			block(p);
+		}
+		patch(p, pc);
+		pc = epatch(p);
+		if(look(p) == Lfinally){
+			lex(p);
+			block(p);
+		}
+		patch(p, pc);
+	}
+	while(--seenlabs >= 0)
+		poplab(p);
+}
+
+block(p : ref Parser)
+{
+	mustbe(p, '{');
+	stmts(p);
+	mustbe(p, '}');
+}
+
+caseblk(p : ref Parser)
+{
+	pc, defaultpc, clausepc : int;
+	gotdef := 0;
+	p.incase++;
+
+	defaultpc = epatch(p);
+	while((op := look(p)) != '}' && op != Leos) {
+		if (op != Lcase && op != Ldefault) {
+			err := "expected " + tokname(Lcase)
+				+ " or " + tokname(Ldefault)
+				+ " found " + tokname(op);
+			error(p, err);
+		}
+		if (op == Ldefault) {
+			if (gotdef)
+				error(p, "default case already defined");
+			gotdef = 1;
+			
+			patch(p, defaultpc);
+		}
+		lex(p);
+		clausepc = epatch(p);
+		if (op == Lcase) {
+			pc = epatch(p);
+			exp(p);
+			patch(p, pc);
+		}
+		mustbe(p, ':');
+		casestmts(p);
+		patch(p, clausepc);
+	}
+	clausepc = epatch(p);
+	patch(p, clausepc);
+	if (!gotdef)
+		patch(p, defaultpc);
+	p.incase--;
+}
+
+casestmts(p : ref Parser)
+{
+	while((op := look(p)) != '}' && op != Lcase && op != Ldefault && op != Leos)
+		stmt(p);
+}
+
+semi(p: ref Parser)
+{
+	op := look(p);
+	if(op == ';'){
+		lex(p);
+		return;
+	}
+	if(op == '}' || op == Leos || p.lastnl)
+		return;
+	mustbe(p, ';');
+}
+
+#varlist	: vardecl
+#	| varlist ',' vardecl
+#	;
+#
+#vardecl	: Lid init
+#	;
+#
+#init	:
+#	| '=' asexp
+#	;
+varlist(p: ref Parser)
+{
+	#
+	# these declaration aren't supposed
+	# to override current definitions
+	#
+	mustbe(p, Lid);
+	codevar(p, 0);
+	emitconst(p, Lid, p.id);
+	if(look(p) == '='){
+		lex(p);
+		asexp(p);
+		emit(p, '=');
+	}
+	if(look(p) != ',')
+		return;
+	emit(p, Lgetval);
+	lex(p);
+	varlist(p);
+	emit(p, ',');
+}
+
+#
+# check that only 1 id is declared in the var list
+#
+check1var(p: ref Parser)
+{
+	if(p.code.ops[p.code.npc-1] == byte ',')
+		error(p, "only one identifier allowed");
+}
+
+#zexp	:
+#	| exp
+#	;
+zexp(p: ref Parser)
+{
+	op := look(p);
+	if(op == ';' || op == ')')
+		return;
+	exp(p);
+}
+
+#exp	: asexp
+#	| exp ',' asexp
+#	;
+exp(p: ref Parser)
+{
+	asexp(p);
+	while(look(p) == ','){
+		lex(p);
+		emit(p, Lgetval);
+		asexp(p);
+		emit(p, ',');
+	}
+}
+
+#asexp	: condexp
+#	| lhsexp asop asexp
+#	;
+#
+#asop	: '=' | Lmulas | Ldivas | Lmodas | Laddas | Lsubas
+#		| Llshas | Lrshas | Lrshuas | Landas | Lxoras | Loras
+#	;
+asops := array[] of { '=', Lmulas, Ldivas, Lmodas, Laddas, Lsubas,
+		Llshas, Lrshas, Lrshuas, Landas, Lxoras, Loras };
+asbaseops := array[] of { '=', '*', '/', '%', '+', '-',
+		Llsh, Lrsh, Lrshu, '&', '^', '|' };
+asexp(p: ref Parser)
+{
+	lhspc := p.code.npc;
+	condexp(p);
+	i := inops(look(p), asops);
+	if(i >= 0){
+		op := lex(p);
+		checklhsexp(p, lhspc);
+		if(op != '=')
+			emit(p, Lasop);
+		asexp(p);
+		emit(p, asbaseops[i]);
+		if(op != '=')
+			emit(p, Las);
+	}
+}
+
+#condexp	: ororexp
+#	| ororexp '?' asexp ':' asexp
+#	;
+condexp(p: ref Parser)
+{
+	ororexp(p);
+	if(look(p) == '?'){
+		lexemit(p);
+		pc := epatch(p);
+		asexp(p);
+		mustbe(p, ':');
+		patch(p, pc);
+		pc = epatch(p);
+		asexp(p);
+		patch(p, pc);
+	}
+}
+
+#ororexp	: andandexp
+#	| ororexp op andandexp
+#	;
+ororexp(p: ref Parser)
+{
+	andandexp(p);
+	while(look(p) == Loror){
+		lexemit(p);
+		pc := epatch(p);
+		andandexp(p);
+		patch(p, pc);
+	}
+}
+
+#andandexp	: laexp
+#	| andandexp op laexp
+#	;
+andandexp(p: ref Parser)
+{
+	laexp(p, 0);
+	while(look(p) == Landand){
+		lexemit(p);
+		pc := epatch(p);
+		laexp(p, 0);
+		patch(p, pc);
+	}
+}
+
+#laexp	: unexp
+#	| laexp op laexp
+#	;
+prectab := array[] of
+{
+	array[] of { '|' },
+	array[] of { '^' },
+	array[] of { '&' },
+	array[] of { Leq, Lneq, Lseq, Lsne },
+	array[] of { '<', '>', Lleq, Lgeq, Lin, Linstanceof },
+	array[] of { Llsh, Lrsh, Lrshu },
+	array[] of { '+', '-' },
+	array[] of { '*', '/', '%' },
+};
+laexp(p: ref Parser, prec: int)
+{
+	unexp(p);
+	for(pr := len prectab - 1; pr >= prec; pr--){
+		while(inops(look(p), prectab[pr]) >= 0){
+			emit(p, Lgetval);
+			op := lex(p);
+			laexp(p, pr + 1);
+			emit(p, op);
+		}
+	}
+}
+
+#unexp	: postexp
+#	| Ldelete unexp
+#	| Lvoid unexp
+#	| Ltypeof unexp
+#	| Linc unexp
+#	| Ldec unexp
+#	| '+' unexp
+#	| '-' unexp
+#	| '~' unexp
+#	| '!' unexp
+#	;
+preops := array[] of { Ldelete, Lvoid, Ltypeof, Linc, Ldec, '+', '-', '~', '!' };
+unexp(p: ref Parser)
+{
+	if(inops(look(p), preops) >= 0){
+		op := lex(p);
+		unexp(p);
+		if(op == '-')
+			op = Lpresub;
+		else if(op == '+')
+			op = Lpreadd;
+		emit(p, op);
+		return;
+	}
+	postexp(p);
+}
+
+#postexp	: lhsexp
+#	| lhsexp Linc	# no line terminators before Linc or Ldec
+#	| lhsexp Ldec
+#	;
+postexp(p: ref Parser)
+{
+	lhsexp(p, 0);
+	if(p.lastnl)
+		return;
+	op := look(p);
+	if(op == Linc || op == Ldec){
+		if(op == Linc)
+			op = Lpostinc;
+		else
+			op = Lpostdec;
+		lex(p);
+		emit(p, op);
+	}
+}
+
+#
+# verify that the last expression is actually a lhsexp
+#
+checklhsexp(p: ref Parser, pc: int)
+{
+
+	case int p.code.ops[p.code.npc-1]{
+	Lthis or
+	')' or
+	'.' or
+	'[' or
+	Lcall or
+	Lnew or
+	Lnewcall =>
+		return;
+	}
+
+	case int p.code.ops[pc]{
+	Lid or
+	Lnum or
+	Lstr or
+	Lregexp =>
+		npc := pc + 1;
+		(npc, nil) = getconst(p.code.ops, npc);
+		if(npc == p.code.npc)
+			return;
+	}
+
+	(nil, e) := pexp(mkpprint(p.ex, p.code), pc, p.code.npc);
+	error(p, "only left-hand-side expressions allowed: "+e);
+}
+
+#lhsexp	: newexp
+#	| callexp
+#	;
+#callexp: memexp args
+#	| callexp args
+#	| callexp '[' exp ']'
+#	| callexp '.' Lid
+#	;
+#newexp	: memexp
+#	| Lnew newexp
+#	;
+#memexp	: primexp
+#	| Lfunction id(opt) '(' zplist ')' '{' stmts '}'
+#	| memexp '[' exp ']'
+#	| memexp '.' Lid
+#	| Lnew memexp args
+#	;
+lhsexp(p: ref Parser, hasnew: int): int
+{
+	a: int;
+	if(look(p) == Lnew){
+		lex(p);
+		hasnew = lhsexp(p, hasnew + 1);
+		if(hasnew){
+			emit(p, Lnew);
+			hasnew--;
+		}
+		return hasnew;
+	}
+	if(look(p) == Lfunction){
+		o := fundecl(p.ex, p, 1);
+		emitconst(p, Lfunction, fexplook(p, o));
+		return 0;
+	}
+	primexp(p);
+	for(;;){
+		op := look(p);
+		if(op == '('){
+			op = Lcall;
+			if(hasnew){
+				hasnew--;
+				#
+				# stupid different order of evaluation
+				#
+				emit(p, Lgetval);
+				op = Lnewcall;
+			}
+			a = args(p);
+			emitconst(p, op, a);
+		}else if(op == '['){
+			emit(p, Lgetval);
+			lex(p);
+			exp(p);
+			mustbe(p, ']');
+			emit(p, '[');
+		}else if(op == '.'){
+			lex(p);
+			mustbe(p, Lid);
+			emitconst(p, Lid, p.id);
+			emit(p, '.');
+		}else
+			return hasnew;
+	}
+}
+
+#primexp	: Lthis
+#	| Lid
+#	| Lnum
+#	| Lstr
+#	| Lregexp
+#	| '(' exp ')'
+#	| '[' array initializer ']'
+#	| '{' propandval '}'
+#	;
+primexp(p: ref Parser)
+{
+	case t := lex(p){
+	Lthis =>
+		emit(p, t);
+	Lid or
+	Lnum or
+	Lstr =>
+		emitconst(p, t, p.id);
+	'/' =>
+		lexregexp(p);
+		emitconst(p, Lregexp, p.id);
+	'(' =>
+		emit(p, '(');
+		exp(p);
+		mustbe(p, ')');
+		emit(p, ')');
+	'[' =>
+		a := 0;
+		if(look(p) == ']')
+			lex(p);
+		else{
+			for(;;){
+				if(look(p) == ']'){
+					lex(p);
+					break;
+				}
+				if(look(p) == ',')
+					emit(p, Lnoval);
+				else
+					asexp(p);
+				emit(p, Lgetval);
+				a++;
+				if(look(p) == ']'){
+					lex(p);
+					break;
+				}
+				mustbe(p, ',');
+			}
+		}
+		emitconst(p, Larrinit, a);
+	'{' =>
+		a := 0;
+		if(look(p) == '}')
+			lex(p);
+		else{
+			for(;;){
+				case(tt := lex(p)){
+				Lid =>
+					emitconst(p, Lstr, p.id);
+				Lnum or
+				Lstr =>
+					emitconst(p, tt, p.id);
+				* =>
+					error(p, "expected identifier, number or string");
+				}
+				mustbe(p, ':');
+				asexp(p);
+				emit(p, Lgetval);
+				a++;
+				if(look(p) == '}'){
+					lex(p);
+					break;
+				}
+				mustbe(p, ',');
+			}
+		}
+		emitconst(p, Lobjinit, a);
+	* =>
+		error(p, "expected an expression");
+	}
+}
+
+#args	: '(' ')'
+#	| '(' arglist ')'
+#	;
+#
+#arglist	: asexp
+#	| arglist ',' asexp
+#	;
+args(p: ref Parser): int
+{
+	mustbe(p, '(');
+	if(look(p) == ')'){
+		lex(p);
+		return 0;
+	}
+	a := 0;
+	for(;;){
+		asexp(p);
+		emit(p, Lgetval);
+		a++;
+		if(look(p) == ')'){
+			lex(p);
+			return a;
+		}
+		mustbe(p, ',');
+	}
+}
+
+inops(tok: int, ops: array of int): int
+{
+	for(i := 0; i < len ops; i++)
+		if(tok == ops[i])
+			return i;
+	return -1;
+}
+
+mustbe(p: ref Parser, t: int)
+{
+	tt := lex(p);
+	if(tt != t)
+		error(p, "expected "+tokname(t)+" found "+tokname(tt));
+}
+
+toknames := array[] of
+{
+	Leos-Lbase =>		"end of input",
+	Landas-Lbase =>		"&=",
+	Loras-Lbase =>		"|=",
+	Lxoras-Lbase =>		"^=",
+	Llshas-Lbase =>		"<<=",
+	Lrshas-Lbase =>		">>=",
+	Lrshuas-Lbase =>	">>>=",
+	Laddas-Lbase =>		"+=",
+	Lsubas-Lbase =>		"-=",
+	Lmulas-Lbase =>		"*=",
+	Ldivas-Lbase =>		"/=",
+	Lmodas-Lbase =>		"%=",
+	Loror-Lbase =>		"||",
+	Landand-Lbase =>	"&&",
+	Leq-Lbase =>		"==",
+	Lneq-Lbase =>		"!=",
+	Lleq-Lbase =>		"<=",
+	Lgeq-Lbase =>		">=",
+	Llsh-Lbase =>		"<<",
+	Lrsh-Lbase =>		">>",
+	Lrshu-Lbase =>		">>>",
+	Linc-Lbase =>		"++",
+	Ldec-Lbase =>		"--",
+	Lnum-Lbase =>		"a number",
+	Lid-Lbase =>		"an identifier",
+	Lstr-Lbase =>		"a string",
+	Lthis-Lbase =>		"this",
+	Ltypeof-Lbase =>	"typeof",
+	Ldelete-Lbase =>	"delete",
+	Lvoid-Lbase =>		"void",
+	Lwhile-Lbase =>		"while",
+	Lfor-Lbase =>		"for",
+	Lbreak-Lbase =>		"break",
+	Lcontinue-Lbase =>	"continue",
+	Lwith-Lbase =>		"with",
+	Lreturn-Lbase =>	"return",
+	Lfunction-Lbase =>	"function",
+	Lvar-Lbase =>		"var",
+	Lif-Lbase =>		"if",
+	Lelse-Lbase =>		"else",
+	Lin-Lbase =>		"in",
+	Lnew-Lbase =>		"new",
+
+	Lpreadd-Lbase =>	"+",
+	Lpresub-Lbase =>	"-",
+	Lpostinc-Lbase =>	"++",
+	Lpostdec-Lbase =>	"--",
+	Lcall-Lbase =>		"call",
+	Lnewcall-Lbase =>	"newcall",
+	Lgetval-Lbase =>	"[[GetValue]]",
+	Las-Lbase =>		"[[as]]",
+	Lasop-Lbase =>		"[[asop]]",
+	Lforin-Lbase =>		"forin",
+	Lforvar-Lbase =>	"forvar",
+	Lforvarin-Lbase =>	"forvarin",
+	Lcase-Lbase =>		"case",
+	Labstract-Lbase =>	"abstract",
+	Lboolean-Lbase =>	"boolean",
+	Lbyte-Lbase =>	"byte",
+	Lcatch-Lbase =>		"catch",
+	Lchar-Lbase =>	"char",
+	Lclass-Lbase =>		"class",
+	Lconst-Lbase =>		"const",
+	Ldebugger-Lbase =>	"debugger",
+	Ldefault-Lbase =>	"default",
+	Ldo-Lbase =>		"do",
+	Ldouble-Lbase =>	"double",
+	Lenum-Lbase =>		"enum",
+	Lexport-Lbase =>	"export",
+	Lextends-Lbase =>	"extends",
+	Lfinal-Lbase =>	"final",
+	Lfinally-Lbase =>	"finally",
+	Lfloat-Lbase =>	"float",
+	Lgoto-Lbase =>	"goto",
+	Limplements-Lbase =>	"implements",
+	Limport-Lbase =>	"import",
+	Linstanceof-Lbase =>	"instanceof",
+	Lint-Lbase =>		"int",
+	Linterface-Lbase =>	"interface",
+	Llong-Lbase =>	"long",
+	Lnative-Lbase =>	"native",
+	Lpackage-Lbase =>	"package",
+	Lprint-Lbase =>	"print",
+	Lprivate-Lbase =>	"private",
+	Lprotected-Lbase =>	"protected",
+	Lpublic-Lbase =>	"public",
+	Lregexp-Lbase =>	"regexp",
+	Lseq-Lbase =>	"===",
+	Lsne-Lbase =>	"!==",
+	Lshort-Lbase =>	"short",
+	Lstatic-Lbase =>	"static",
+	Lsuper-Lbase =>		"super",
+	Lswitch-Lbase =>	"switch",
+	Lsynchronized-Lbase =>	"synchronized",
+	Lthrow-Lbase =>		"throw",
+	Lthrows-Lbase =>	"throws",
+	Ltransient-Lbase =>	"transient",
+	Ltry-Lbase=>		"try",
+	Lvolatile-Lbase =>	"volatile",
+	Larrinit-Lbase =>	"arrayinit",
+	Lobjinit-Lbase =>	"objinit",
+	Lnoval-Lbase =>	"novalue",
+	Llabel-Lbase =>	"label",
+	Lbreaklab-Lbase =>	"break",
+	Lcontinuelab-Lbase =>	"continue",
+};
+
+tokname(t: int): string
+{
+	if(t < Lbase){
+		s := "";
+		s[0] = t;
+		return s;
+	}
+	if(t-Lbase >= len toknames || toknames[t-Lbase] == "")
+		return sprint("<%d>", t);
+	return toknames[t-Lbase];
+}
+
+lexemit(p: ref Parser)
+{
+	emit(p, lex(p));
+	if(debug['s'])
+		sys->print("%d: %s\n", p.code.npc-1, tokname(int p.code.ops[p.code.npc-1]));
+}
+
+emit(p: ref Parser, t: int)
+{
+	if(t > 255)
+		fatal(p.ex, sprint("emit too big: %d\n", t));
+	if(p.code.npc >= len p.code.ops){
+		ops := array[2 * len p.code.ops] of byte;
+		ops[:] = p.code.ops;
+		p.code.ops = ops;
+	}
+	p.code.ops[p.code.npc++] = byte t;
+}
+
+emitconst(p: ref Parser, op, c: int)
+{
+	emit(p, op);
+	if(c < 0)
+		fatal(p.ex, "emit negative constant");
+	if(c >= 255){
+		if(c >= 65536)
+			fatal(p.ex, "constant too large");
+		emit(p, 255);
+		emit(p, c & 16rff);
+		c >>= 8;
+	}
+	emit(p, c);
+}
+
+epatch(p: ref Parser): int
+{
+	pc := p.code.npc;
+	emit(p, 0);
+	emit(p, 0);
+	return pc;
+}
+
+patch(p: ref Parser, pc: int)
+{
+	val := p.code.npc - pc;
+	if(val >= 65536)
+		fatal(p.ex, "patch constant too large");
+	p.code.ops[pc] = byte val;
+	p.code.ops[pc+1] = byte(val >> 8);
+}
+
+getconst(ops: array of byte, pc: int): (int, int)
+{
+	c := int ops[pc++];
+	if(c == 255){
+		c = int ops[pc] + (int ops[pc+1] << 8);
+		pc += 2;
+	}
+	return (pc, c);
+}
+
+getjmp(ops: array of byte, pc: int): (int, int)
+{
+	c := int ops[pc] + (int ops[pc+1] << 8) + pc;
+	pc += 2;
+	return (pc, c);
+}
+
+mkcode(): ref Code
+{
+	return ref Code(array[16] of byte, 0, nil, nil, nil, nil, nil);
+}
+
+look(p: ref Parser): int
+{
+	if(p.token == -1)
+		p.token = lex(p);
+	if(p.notin && p.token == Lin)
+		return ~Lin;
+	return p.token;
+}
+
+look2(p: ref Parser): int
+{
+	look(p);
+	if(p.token1 == -1){
+		# fool lex()
+		t := p.token;
+		p.token = -1;
+		p.token1 = lex(p);
+		p.token = t;
+	}
+	return  p.token1;
+}
+ 
+lex(p: ref Parser): int
+{
+	t := lex0(p);
+	if(0)
+		sys->print("tok=%d %s\n", t, tokname(t));
+	return t;
+}
+
+lex0(p: ref Parser): int
+{
+	t := p.token;
+	if(t != -1){
+		p.token = p.token1;
+		p.token1 = -1;
+		return t;
+	}
+
+	p.lastnl = 0;
+	while(p.srci < p.esrc){
+		c := p.src[p.srci++];
+		case c{
+		'\r' or LS or PS =>
+			p.lastnl = 1;
+		'\n' =>
+			p.lineno++;
+			p.lastnl = 1;
+		' ' or
+		'\t' or
+		'\v' or
+		FF or		# form feed
+		'\u00a0' =>	# no-break space
+			;
+		'"' or
+		'\''=>
+			return lexstring(p, c);
+		'(' or
+		')' or
+		'[' or
+		']' or
+		'{' or
+		'}' or
+		',' or
+		';' or
+		'~' or
+		'?' or
+		':' =>
+			return c;
+		'.' =>
+			if(p.srci < p.esrc && (map[p.src[p.srci]] & Mdigit) != byte 0){
+				p.srci--;
+				return lexnum(p);
+			}
+			return '.';
+		'^' =>
+			if(p.srci < p.esrc && p.src[p.srci] == '='){
+				p.srci++;
+				return Lxoras;
+			}
+			return '^';
+		'*' =>
+			if(p.srci < p.esrc && p.src[p.srci] == '='){
+				p.srci++;
+				return Lmulas;
+			}
+			return '*';
+		'%' =>
+			if(p.srci < p.esrc && p.src[p.srci] == '='){
+				p.srci++;
+				return Lmodas;
+			}
+			return '%';
+		'=' =>
+			if(p.srci < p.esrc && p.src[p.srci] == '='){
+				p.srci++;
+				if(p.srci < p.esrc && p.src[p.srci] == '='){
+					p.srci++;
+					return Lseq;
+				}
+				return Leq;
+			}
+			return '=';
+		'!' =>
+			if(p.srci < p.esrc && p.src[p.srci] == '='){
+				p.srci++;
+				if(p.srci < p.esrc && p.src[p.srci] == '='){
+					p.srci++;
+					return Lsne;
+				}
+				return Lneq;
+			}
+			return '!';
+		'+' =>
+			if(p.srci < p.esrc){
+				c = p.src[p.srci];
+				if(c == '='){
+					p.srci++;
+					return Laddas;
+				}
+				if(c == '+'){
+					p.srci++;
+					return Linc;
+				}
+			}
+			return '+';
+		'-' =>
+			if(p.srci < p.esrc){
+				c = p.src[p.srci];
+				if(c == '='){
+					p.srci++;
+					return Lsubas;
+				}
+				if(c == '-'){
+					p.srci++;
+					return Ldec;
+				}
+			}
+			return '-';
+		'|' =>
+			if(p.srci < p.esrc){
+				c = p.src[p.srci];
+				if(c == '='){
+					p.srci++;
+					return Loras;
+				}
+				if(c == '|'){
+					p.srci++;
+					return Loror;
+				}
+			}
+			return '|';
+		'&' =>
+			if(p.srci < p.esrc){
+				c = p.src[p.srci];
+				if(c == '='){
+					p.srci++;
+					return Landas;
+				}
+				if(c == '&'){
+					p.srci++;
+					return Landand;
+				}
+			}
+			return '&';
+		'/' =>
+			if(p.srci < p.esrc){
+				c = p.src[p.srci];
+				if(c == '='){
+					p.srci++;
+					return Ldivas;
+				}
+				if(c == '/'){
+					p.srci++;
+					if(lexcom(p) < 0)
+						return Leos;
+					break;
+				}
+				if(c == '*'){
+					p.srci++;
+					if(lexmcom(p) < 0)
+						return Leos;
+					break;
+				}
+			}
+			return '/';
+		'>' =>
+			if(p.srci < p.esrc){
+				c = p.src[p.srci];
+				if(c == '='){
+					p.srci++;
+					return Lgeq;
+				}
+				if(c == '>'){
+					p.srci++;
+					if (p.srci < p.esrc) {
+						c = p.src[p.srci];
+						if(c == '='){
+							p.srci++;
+							return Lrshas;
+						}
+						if(c == '>'){
+							p.srci++;
+							c = p.src[p.srci];
+							if(c == '='){
+								p.srci++;
+								return Lrshuas;
+							}
+							return Lrshu;
+						}
+					}
+					return Lrsh;
+				}
+			}
+			return '>';
+		'<' =>
+			if(p.srci < p.esrc){
+				c = p.src[p.srci];
+				case c {
+				'=' =>
+					p.srci++;
+					return Lleq;
+				'<' =>
+					p.srci++;
+					if (p.srci < p.esrc) {
+						c = p.src[p.srci];
+						if(c == '='){
+							p.srci++;
+							return Llshas;
+						}
+					}
+					return Llsh;
+				'!' =>
+					# HTML comment - consume to end of line or end of comment
+					# No way of having the HTML parser do this
+					if (p.srci+2 >= p.esrc)
+						return Leos;
+
+					if (p.src[p.srci+1] != '-' || p.src[p.srci+2] != '-')
+						# don't treat as a comment, let the parser report syntax error
+						return '<';
+					# consume "!--"
+					p.srci += 3;
+					if(lexhtmlcom(p) < 0)
+						return Leos;
+					continue;
+				}
+			}
+			return '<';
+		'0' to '9' =>
+			p.srci--;
+			return lexnum(p);
+		'\\' =>
+			return lexid(p);
+		* =>
+			if((map[c] & Malpha) != byte 0)
+				return lexid(p);
+			s := "";
+			s[0] = c;
+			error(p, "unknown character '"+s+"'");
+		}
+	}
+	return Leos;
+}
+
+#
+# single line comment
+#
+lexcom(p: ref Parser): int
+{
+	while(p.srci < p.esrc){
+		c := p.src[p.srci];
+		if(islt(c))
+			return 0;
+		p.srci++;
+	}
+	return -1;
+}
+
+#
+# multi-line comment
+#
+lexmcom(p: ref Parser): int
+{
+	star := 0;
+	while(p.srci < p.esrc){
+		c := p.src[p.srci++];
+		if(c == '/' && star)
+			return 0;
+		star = c == '*';
+	}
+	return -1;
+}
+
+# HTML comment 
+# consume to end of line or end of comment (-->), whichever we see first.
+# [not strict HTML comment semantics because of
+# the way in which HTML comments are used in JavaScript]
+#
+lexhtmlcom(p: ref Parser): int
+{
+	nmin := 0;
+	for (;p.srci < p.esrc;) {
+		c := p.src[p.srci++];
+		if (c == '-') {
+			nmin++;
+			continue;
+		}
+		if (c == '>' && nmin >= 2)
+			return 0;
+		if (islt(c))
+			return 0;
+		nmin = 0;
+	}
+	return -1;
+}
+
+lexid(p: ref Parser): int
+{
+	p.srci--;
+	id := "";
+	ch := "Z";
+	while(p.srci < p.esrc){
+		c := p.src[p.srci];
+		if(c == '\\'){
+			p.srci++;
+			c = uniescchar(p);
+			if(c == -1)
+				error(p, "malformed unicode escape sequence in identifier");
+			else
+				;
+		}
+		else{
+			if(c >= 0 && c < 256 && (map[c] & (Malpha|Mdigit)) == byte 0)
+			# if(c >= 256 || (map[c] & (Malpha|Mdigit)) == byte 0)
+				break;
+			p.srci++;
+		}
+		ch[0] = c;
+		id += ch;
+	}
+	# id := p.src[srci:p.srci];
+	t := keywdlook(id);
+	if(t != -1)
+		return t;
+	p.id = strlook(p, id);
+	return Lid;
+}
+
+ParseReal, ParseHex, ParseOct, ParseTrim, ParseEmpty: con 1 << iota;
+
+#
+# parse a numeric identifier
+# format [0-9]+(r[0-9A-Za-z]+)?
+# or ([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?
+#
+lexnum(p: ref Parser): int
+{
+	v: real;
+	(p.srci, v) = parsenum(p.ex, p.src, p.srci, ParseReal|ParseHex|ParseOct);
+	p.id = numlook(p, v);
+	return Lnum;
+}
+
+parsenum(ex: ref Exec, s: string, si, how: int): (int, real)
+{
+	Inf: con "Infinity";
+
+	osi := si;
+	lens := len s;
+	if (how & ParseTrim) {
+		while(si < lens && iswhite(s[si]))
+			si++;
+	}
+	if(si >= lens) {
+		if (how & ParseEmpty)
+			return (si, 0.);
+		return (osi, Math->NaN);
+	}
+	c := s[si];
+	neg := 0;
+	if(c == '+')
+		si++;
+	else if(c == '-'){
+		si++;
+		neg = 1;
+	}
+	v := 0.;
+	if((how & ParseReal) && si + len Inf <= lens && s[si:si+len Inf] == Inf){
+		si += len Inf;
+		v = Math->Infinity;
+	}else{
+		nsi := si;
+		(si, v) = parsenumval(ex, s, si, how);
+		if(si == nsi)
+			return (osi, Math->NaN);
+	}
+	if(neg)
+		v = -v;
+	if (how & ParseTrim) {
+		while(si < lens && iswhite(s[si]))
+			si++;
+	}
+	return (si, v);
+}
+
+#
+# parse a bunch of difference subsets of numbers
+#
+parsenumval(ex: ref Exec, s: string, si, how: int): (int, real)
+{
+	Int, Oct, Hex, FracSeen, Frac, ExpSeen, ExpSignSeen, Exp: con iota;
+
+	lens := len s;
+	if(si >= lens)
+		return (si, Math->NaN);
+	ssi := si;
+	c := s[si];
+	state := Int;
+	if(c == '.' && (how & ParseReal)){
+		state = FracSeen;
+		si++;
+	}else if(c == '0'){
+		if(si+1 >= lens)
+			return (si+1, 0.);
+		c = s[si+1];
+		if(c == '.' && (how & ParseReal)){
+			state = Frac;
+			si += 2;
+		}else if((c == 'x' || c == 'X') && (how & ParseHex)){
+			state = Hex;
+			ssi += 2;
+			si += 2;
+		}else if(how & ParseOct)
+			state = Oct;
+	}
+
+done:	while(si < lens){
+		c = s[si];
+		case state{
+		Int =>
+			if((map[c] & Mdigit) != byte 0)
+				break;
+			if((map[c] & Mexp) != byte 0 && (how & ParseReal))
+				state = ExpSeen;
+			else if(c == '.' && (how & ParseReal))
+				state = Frac;
+			else
+				break done;
+		Hex =>
+			if((map[c] & Mhex) == byte 0)
+				break done;
+		Oct =>
+			if((map[c] & Moct) == byte 0)
+				break done;
+		FracSeen or
+		Frac =>
+			if((map[c] & Mdigit) != byte 0)
+				state = Frac;
+			else if((map[c] & Mexp) != byte 0)
+				state = ExpSeen;
+			else
+				break done;
+		ExpSeen =>
+			if((map[c] & Msign) != byte 0)
+				state = ExpSignSeen;
+			else if((map[c] & Mdigit) != byte 0)
+				state = Exp;
+			else
+				break done;
+		ExpSignSeen or
+		Exp =>
+			if((map[c] & Mdigit) != byte 0)
+				state = Exp;
+			else
+				break done;
+		}
+		si++;
+	}
+
+	esi := si;
+	if(state == FracSeen)
+		return (si - 1, Math->NaN);
+	if(state == ExpSeen){
+		state = Frac;
+		esi--;
+	}else if(state == ExpSignSeen){
+		state = Frac;
+		esi -= 2;
+	}
+	buf := s[ssi:esi];
+	v: real;
+	case state{
+	* =>
+		# only if the above lexing code is wrong
+		fatal(ex, "bad parse of numerical constant '"+buf+"'");
+		v = 0.;
+	Oct =>
+		v = strtoi(ex, buf, 8);
+	Hex =>
+		v = strtoi(ex, buf, 16);
+	Int or
+	Frac or
+	Exp =>
+		v = real buf;
+	}
+	return (si, v);
+}
+
+#
+# called only from parsenumval
+# can never fatal error if that routine works correctly
+#
+strtoi(ex: ref Exec, t: string, base: int): real
+{
+	if(len t == 0)
+		return Math->NaN;
+
+	v := 0.;
+	for(i := 0; i < len t; i++){
+		c := t[i];
+		if(c >= '0' && c <= '9')
+			c -= '0';
+		else if(c >= 'a' && c <= 'z')
+			c -= 'a' - 10;
+		else
+			c -= 'A' - 10;
+		if(c >= base){
+			fatal(ex, "digit '"+t[i:i+1]+"' is not radix "+string base);
+			return Math->NaN;
+		}
+		v = v * real base + real c;
+	}
+	return v;
+}
+
+lexstring(p: ref Parser, end: int): int
+{
+	s := "";
+	i := 0;
+	for(;;){
+		if(p.srci >= p.esrc){
+			error(p, "end of file in string constant");
+			break;
+		}
+		c := p.src[p.srci];
+		if(islt(c)){
+			error(p, "newline in string constant");
+			break;
+		}
+		p.srci++;
+		if(c == end)
+			break;
+		if(c == '\\'){
+			c = escchar(p);
+			if(c == Leos)
+				continue;
+		}
+		s[i++] = c;
+	}
+	p.id = strlook(p, s);
+	return Lstr;
+}
+
+lexregexp(p: ref Parser): int
+{
+	c := esc := 0;
+	s := "";
+	i := 0;
+	s[i++] = '/';
+	for(;;){
+		if(p.srci >= p.esrc){
+			error(p, "end of file in regexp constant");
+			break;
+		}
+		c = p.src[p.srci];
+		if(islt(c)){
+			error(p, "newline in regexp constant");
+			break;
+		}
+		p.srci++;
+		s[i++] = c;
+		if(!esc && c == '/')
+			break;
+		esc = !esc && c == '\\';
+	}
+	if(esc)
+		error(p, "missing escaped character");
+	if(i == 2)
+		error(p, "missing regexp");
+	while(p.srci < p.esrc){
+		c = p.src[p.srci];
+		if(c >= 256 || (map[c] & (Malpha|Mdigit)) == byte 0)
+			break;
+		p.srci++;
+		s[i++] = c;
+	}
+	p.id = strlook(p, s);
+	return Lregexp;
+}
+
+uniescchar(p: ref Parser): int
+{
+	if(p.srci >= p.esrc)
+		return -1;
+	c := p.src[p.srci++];
+	if(c != 'u')
+		return -1;
+	v := 0;
+	for(i := 0; i < 4; i++){
+		if(p.srci >= p.esrc || (map[c = p.src[p.srci]] & (Mdigit|Mhex)) == byte 0)
+			return -1;
+		p.srci++;
+		if((map[c] & Mdigit) != byte 0)
+			c -= '0';
+		else if((map[c] & Mlower) != byte 0)
+			c = c - 'a' + 10;
+		else if((map[c] & Mupper) != byte 0)
+			c = c - 'A' + 10;
+		v = v * 16 + c;
+	}
+	return v;
+}
+
+escchar(p: ref Parser): int
+{
+	v: int;
+	if(p.srci >= p.esrc)
+		return Leos;
+	c := p.src[p.srci++];
+	if(c == 'u' || c == 'x'){
+		d := 2;
+		if(c == 'u')
+			d = 4;
+		v = 0;
+		for(i := 0; i < d; i++){
+			if(p.srci >= p.esrc || (map[c = p.src[p.srci]] & (Mdigit|Mhex)) == byte 0){
+				error(p, "malformed hex escape sequence");
+				break;
+			}
+			p.srci++;
+			if((map[c] & Mdigit) != byte 0)
+				c -= '0';
+			else if((map[c] & Mlower) != byte 0)
+				c = c - 'a' + 10;
+			else if((map[c] & Mupper) != byte 0)
+				c = c - 'A' + 10;
+			v = v * 16 + c;
+		}
+		return v;
+	}
+	if(c >= '0' && c <= '7'){
+		v = c - '0';
+		if(p.srci < p.esrc && (c = p.src[p.srci]) >= '0' && c <= '7'){
+			p.srci++;
+			v = v * 8 + c - '0';
+			if(v <= 8r37 && p.srci < p.esrc && (c = p.src[p.srci]) >= '0' && c <= '7'){
+				p.srci++;
+				v = v * 8 + c - '0';
+			}
+		}
+		return v;
+	}
+
+	if(c < len escmap && (v = int escmap[c]) < 255)
+		return v;
+	return c;
+}
+
+keywdlook(s: string): int
+{
+	m: int;
+	l := 1;
+	r := len keywords - 1;
+	while(l <= r){
+		m = (r + l) >> 1;
+		if(keywords[m].name <= s)
+			l = m + 1;
+		else
+			r = m - 1;
+	}
+	m = l - 1;
+	if(keywords[m].name == s)
+		return keywords[m].token;
+	return -1;
+}
+
+strlook(p: ref Parser, s: string): int
+{
+	for(i := 0; i < len p.code.strs; i++)
+		if(p.code.strs[i] == s)
+			return i;
+	strs := array[i + 1] of string;
+	strs[:] = p.code.strs;
+	strs[i] = s;
+	p.code.strs = strs;
+	return i;
+}
+
+numlook(p: ref Parser, r: real): int
+{
+	for(i := 0; i < len p.code.nums; i++)
+		if(p.code.nums[i] == r)
+			return i;
+	nums := array[i + 1] of real;
+	nums[:] = p.code.nums;
+	nums[i] = r;
+	p.code.nums = nums;
+	return i;
+}
+
+fexplook(p: ref Parser, o: ref Obj): int
+{
+	i := len p.code.fexps;
+	fexps := array[i+1] of ref Obj;
+	fexps[:] = p.code.fexps;
+	fexps[i] = o;
+	p.code.fexps = fexps;
+	return i;
+}
+
+iswhite(c: int): int
+{
+	if(islt(c))
+		return 1;
+	case c {
+	' ' or
+	'\t' or
+	'\v' or
+	FF or			# form feed
+	'\u00a0' =>	# no-break space
+		return 1;
+	}
+	return 0;
+}
+
+error(p: ref Parser, s: string)
+{
+	p.errors++;
+	p.ex.error += sys->sprint("%d: syntax error: %s\n", p.lineno, s);
+	if(p.errors >= maxerr)
+		runtime(p.ex, SyntaxError, p.ex.error);
+}
+
+fatal(ex: ref Exec, msg: string)
+{
+	if(debug['f']){
+		print("fatal ecmascript error: %s\n", msg);
+		if(""[5] == -1);	# abort
+	}
+	runtime(ex, InternalError, "unrecoverable internal ecmascript error: "+ msg);
+}
+
+# scanb(p: ref Parser, s: string): int
+# {
+# 	n := len s;
+# 	for(i := p.srci; i+n > p.esrc || p.src[i: i+n] != s; --i)
+# 		;
+# 	return i;
+# }
+	
+setkindlab(p: ref Parser, op: int, n: int)
+{
+	l := p.labs;
+	for(i := 0; i < n; i++){
+		(hd l).k = op;
+		l = tl l;
+	}
+}
+
+inlocallabs(p: ref Parser, lr: ref labrec, n: int): int
+{
+	l := p.labs;
+	for(i := 0; i < n; i++){
+		if(hd l == lr)
+			return 1;
+		l = tl l;
+	}
+	return 0;
+}
+
+findlab(p: ref Parser, s: string): ref labrec
+{
+	for(l := p.labs; l != nil; l = tl l)
+		if((hd l).s == s)
+			return hd l;
+	return nil;
+}
+
+pushlab(p: ref Parser, s: string)
+{
+	if(findlab(p, s) != nil)
+		error(p, "duplicate labels");
+	p.labs = ref labrec(s, 0) :: p.labs;
+}
+
+poplab(p: ref Parser)
+{
+	p.labs = tl p.labs;
+}
+
+itstmt(k: int): int
+{
+	return k == Lwhile || k == Ldo || k == Lfor;
+}
--- /dev/null
+++ b/appl/lib/ecmascript/exec.b
@@ -1,0 +1,863 @@
+exec(ex: ref Exec, code: ref Code): Completion
+{
+	ssp := ex.sp;
+
+	r := estmt(ex, code, 0, code.npc);
+
+	if(r.kind == CThrow)
+		ex.sp = ssp;
+
+	if(ssp != ex.sp)
+		runtime(ex, InternalError, "internal error: exec stack not balanced");
+
+	if(r.lab != nil)
+		runtime(ex, InternalError, "internal error: label out of stack");
+	return r;
+}
+
+estmt(ex: ref Exec, code: ref Code, pc, epc: int): Completion
+{
+	e: ref Ref;
+	ev: ref Val;
+	k, apc, pc2, apc2, pc3, apc3, c: int;
+	lab: string;
+	labs: list of string;
+
+	osp := ex.sp;
+
+{
+	v : ref Val = nil;
+	k1 := CNormal;
+	while(pc < epc){
+		v1 : ref Val = nil;
+
+		labs = nil;
+		op := int code.ops[pc++];
+		while(op == Llabel){
+			(pc, c) = getconst(code.ops, pc);
+			labs = code.strs[c] :: labs;
+			op = int code.ops[pc++];
+		}
+		if(debug['e'] > 1)
+			print("estmt(pc %d, sp %d) %s\n", pc-1, ex.sp, tokname(op));
+		case op {
+		Lbreak =>
+			return (CBreak, v, nil);
+		Lcontinue =>
+			return (CContinue, v, nil);
+		Lbreaklab =>
+			(pc, c) = getconst(code.ops, pc);
+			return (CBreak, v, code.strs[c]);
+		Lcontinuelab =>
+			(pc, c) = getconst(code.ops, pc);
+			return (CContinue, v, code.strs[c]);
+		Lreturn =>
+			(pc, v) = eexpval(ex, code, pc, code.npc);
+			return (CReturn, v, nil);
+		'{' =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(k1, v1, lab) = estmt(ex, code, pc, apc);
+			pc = apc;
+		Lif =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, ev) = eexpval(ex, code, pc, apc);
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc2, apc2) = getjmp(code.ops, apc);
+			if(toBoolean(ex, ev) != false)
+				(k1, v1, lab) = estmt(ex, code, pc, apc);
+			else if(pc2 != apc2)
+				(k1, v1, lab) = estmt(ex, code, pc2, apc2);
+			pc = apc2;
+		Lwhile =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc2, apc2) = getjmp(code.ops, apc);
+			for(;;){
+				(nil, ev) = eexpval(ex, code, pc, apc);
+				if(toBoolean(ex, ev) == false)
+					break;
+				(k, v1, lab) = estmt(ex, code, pc2, apc2);
+				if(v1 != nil)
+					v = v1;
+				if(k == CBreak || k == CContinue){
+					if(initlabs(lab, labs)){
+						if(k == CBreak)
+							break;
+						else
+							continue;
+					}
+					else
+						return (k, v1, lab);
+				}
+				if(k == CReturn || k == CThrow)
+					return (k, v1, nil);
+			}
+			pc = apc2;
+		Ldo =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc2, apc2) = getjmp(code.ops, apc);
+			for(;;){
+				(k, v1, lab) = estmt(ex, code,  pc, apc);
+				if(v1 != nil)
+					v = v1;
+				if(k == CBreak || k == CContinue){
+					if(initlabs(lab, labs)){
+						if(k == CBreak)
+							break;
+						else
+							continue;
+					}
+					else
+						return (k, v1, lab);
+				}
+				if(k == CReturn || k == CThrow)
+					return (k, v1, nil);
+				(nil, ev) = eexpval(ex, code, pc2, apc2);
+				if(toBoolean(ex, ev) == false)
+					break;
+			}
+			pc = apc2;
+		Lfor or
+		Lforvar =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, nil) = eexpval(ex, code, pc, apc);
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc2, apc2) = getjmp(code.ops, apc);
+			(pc3, apc3) = getjmp(code.ops, apc2);
+			for(;;){
+				(nil, e) = eexp(ex, code, pc, apc);
+				if(e != nil && toBoolean(ex, getValue(ex, e)) == false)
+					break;
+				(k, v1, lab) = estmt(ex, code, pc3, apc3);
+				if(v1 != nil)
+					v = v1;
+				if(k == CBreak || k == CContinue){
+					if(initlabs(lab, labs)){
+						if(k == CBreak)
+							break;
+						else
+							continue;
+					}
+					else
+						return (k, v1, lab);
+				}
+				if(k == CReturn || k == CThrow)
+					return (k, v1, nil);
+				eexpval(ex, code, pc2, apc2);
+			}
+			pc = apc3;
+		Lforin or
+		Lforvarin =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc2, apc2) = getjmp(code.ops, apc);
+			(pc3, apc3) = getjmp(code.ops, apc2);
+			if(op == Lforvarin){
+				(nil, nil) = eexp(ex, code, pc, apc);
+				# during for only evaluate the id, not the initializer
+				apc = pc + 1;
+			}
+			(nil, ev) = eexpval(ex, code, pc2, apc2);
+			bo := toObject(ex, ev);
+
+			#
+			# note this won't enumerate host properties
+			#
+			enum:
+			for(o := bo; o != nil; o = o.prototype){
+				if(o.host != nil && o.host != me)
+					continue;
+				for(i := 0; i < len o.props; i++){
+					if(o.props[i] == nil
+					|| (o.props[i].attr & DontEnum)
+					|| propshadowed(bo, o, o.props[i].name))
+						continue;
+					(nil, e) = eexp(ex, code, pc, apc);
+					putValue(ex, e, strval(o.props[i].name));
+					(k, v1, lab) = estmt(ex, code, pc3, apc3);
+					if(v1 != nil)
+						v = v1;
+					if(k == CBreak || k == CContinue){
+						if(initlabs(lab, labs)){
+							if(k == CBreak)
+								break enum;
+							else
+								continue enum;
+						}
+						else
+							return (k, v1, lab);
+					}
+					if(k == CReturn || k == CThrow)
+						return (k, v1, nil);
+				}
+			}
+			pc = apc3;
+		Lwith =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, ev) = eexpval(ex, code, pc, apc);
+			pushscope(ex, toObject(ex, ev));
+			(pc, apc) = getjmp(code.ops, pc);
+			(k1, v1, lab) = estmt(ex, code, pc, apc);
+			popscope(ex);
+			pc = apc;
+		';' =>
+			;
+		Lvar =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, nil) = eexp(ex, code, pc, apc);
+		Lswitch =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, ev) = eexpval(ex, code, pc, apc);
+			(pc, apc) = getjmp(code.ops, pc);
+			(k1, v1, lab) = ecaseblk(ex, code, ev, pc, apc, labs);
+			pc = apc;
+		Lthrow =>
+			(pc, v) = eexpval(ex, code, pc, code.npc);
+			ex.error = toString(ex, v);
+			return (CThrow, v, nil);
+		Lprint =>
+			(pc, v1) = eexpval(ex, code, pc, code.npc);
+			print("%s\n", toString(ex, v1));
+		Ltry =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(k1, v1, lab) = estmt(ex, code,  pc, apc);
+			(kc, vc) := (k1, v1);
+			(pc, apc) = getjmp(code.ops, apc);
+			if(pc != apc){
+				(pc, c) = getconst(code.ops, ++pc);
+				if(k1 == CThrow){
+					o := mkobj(ex.objproto, "Object");
+					valinstant(o, DontDelete, code.strs[c], v1);
+					pushscope(ex, o);
+					(k1, v1, lab) = estmt(ex, code, pc, apc);
+					popscope(ex);
+					if(k1 != CNormal)
+						(kc, vc) = (k1, v1);
+				}
+			}
+			(pc, apc) = getjmp(code.ops, apc);
+			if(pc != apc){
+				(k, v, lab) = estmt(ex, code, pc, apc);
+				if(k == CNormal)
+					(k1, v1) = (kc, vc);
+				else
+					(k1, v1) = (k, v);
+			}
+			pc = apc;
+		* =>
+			(pc, e) = eexp(ex, code, pc-1, code.npc);
+			if(e != nil)
+				v1 = getValue(ex, e);
+			if(debug['v'])
+				print("%s\n", toString(ex, v1));
+		}
+
+		if(v1 != nil)
+			v = v1;
+		if(k1 == CBreak && lab != nil && inlabs(lab, labs))
+			(k1, lab) = (CNormal, nil);
+		if(k1 != CNormal)
+			return (k1, v, lab);
+	}
+	return (CNormal, v, nil);
+}
+exception{
+	"throw" =>
+		ex.sp = osp;
+		return (CThrow, ex.errval, nil);
+}
+}
+
+ecaseblk(ex : ref Exec, code : ref Code, sv : ref Val, pc, epc : int, labs: list of string) : Completion
+{	defpc, nextpc, clausepc, apc : int;
+	ev : ref Val;
+	lab: string;
+
+	k := CNormal;
+	v := undefined;
+	matched := 0;
+
+	(pc, defpc) = getjmp(code.ops, pc);
+	clausepc = pc;
+	(pc, nextpc) = getjmp(code.ops, pc);
+	for (; pc <= epc; (clausepc, (pc, nextpc)) = (nextpc, getjmp(code.ops, nextpc))) {
+		if (nextpc == epc) {
+			if (matched || defpc == epc)
+				break;
+			# do the default
+			matched = 1;
+			nextpc = defpc;
+			continue;
+		}
+		if (!matched && clausepc == defpc)
+			# skip default case - still scanning guards
+			continue;
+		if (clausepc != defpc) {
+			# only case clauses have guard exprs
+			(pc, apc) = getjmp(code.ops, pc);
+			if (matched)
+				pc = apc;
+			else {
+				(pc, ev) = eexpval(ex, code, pc, apc);
+				if (identical(sv, ev))
+					matched = 1;
+				else
+					continue;
+			}
+		}
+		(k, v, lab) = estmt(ex, code, pc, nextpc);
+		if(k == CBreak && initlabs(lab, labs))
+			return (CNormal, v, nil);
+		if(k == CBreak || k == CContinue || k == CReturn || k == CThrow)
+			return (k, v, lab);
+	}
+	return (k, v, lab);
+}
+
+identical(v1, v2 : ref Val) : int
+{
+	if (v1.ty != v2.ty)
+		return 0;
+	ret := 0;
+	case v1.ty{
+	TUndef or
+	TNull =>
+		ret = 1;
+	TNum =>
+		if(v1.num == v2.num)
+			ret = 1;
+	TBool =>
+		if(v1 == v2)
+			ret = 1;
+	TStr =>
+		if(v1.str == v2.str)
+			ret = 1;
+	TObj =>
+		if(v1.obj == v2.obj)
+			ret = 1;
+	TRegExp =>
+		if(v1.rev == v2.rev)
+			ret = 1;
+	}
+	return ret;
+}
+
+eexpval(ex: ref Exec, code: ref Code, pc, epc: int): (int, ref Val)
+{
+	e: ref Ref;
+
+	(pc, e) = eexp(ex, code, pc, epc);
+	if(e == nil)
+		v := undefined;
+	else
+		v = getValue(ex, e);
+	return (pc, v);
+}
+
+eexp(ex: ref Exec, code: ref Code, pc, epc: int): (int, ref Ref)
+{
+	o, th: ref Obj;
+	a1: ref Ref;
+	v, v1, v2: ref Val;
+	s: string;
+	r1, r2: real;
+	c, apc, i1, i2: int;
+
+	savesp := ex.sp;
+out:	while(pc < epc){
+		op := int code.ops[pc++];
+		if(debug['e'] > 1){
+			case op{
+			Lid or
+			Lstr or
+			Lregexp =>
+				(nil, c) = getconst(code.ops, pc);
+				print("eexp(pc %d, sp %d) %s '%s'\n", pc-1, ex.sp, tokname(op), code.strs[c]);
+			Lnum =>
+				(nil, c) = getconst(code.ops, pc);
+				print("eexp(pc %d, sp %d) %s '%g'\n", pc-1, ex.sp, tokname(op), code.nums[c]);
+			* =>
+				print("eexp(pc %d, sp %d) %s\n", pc-1, ex.sp, tokname(op));
+			}
+		}
+		case op{
+		Lthis =>
+			v1 = objval(ex.this);
+		Lnum =>
+			(pc, c) = getconst(code.ops, pc);
+			v1 = numval(code.nums[c]);
+		Lstr =>
+			(pc, c) = getconst(code.ops, pc);
+			v1 = strval(code.strs[c]);
+		Lregexp =>
+			(pc, c) = getconst(code.ops, pc);
+			(p, f) := rsplit(code.strs[c]);
+			o = nregexp(ex, nil, array[] of { strval(p), strval(f) });
+			v1 = objval(o);
+			# v1 = regexpval(p, f, 0);
+		Lid =>
+			(pc, c) = getconst(code.ops, pc);
+			epush(ex, esprimid(ex, code.strs[c]));
+			continue;
+		Lnoval =>
+			v1 = undefined;
+		'.' =>
+			a1 = epop(ex);
+			v1 = epopval(ex);
+			epush(ex, ref Ref(1, nil, toObject(ex, v1), a1.name));
+			continue;
+		'[' =>
+			v2 = epopval(ex);
+			v1 = epopval(ex);
+			epush(ex, ref Ref(1, nil, toObject(ex, v1), toString(ex, v2)));
+			continue;
+		Lpostinc or
+		Lpostdec =>
+			a1 = epop(ex);
+			r1 = toNumber(ex, getValue(ex, a1));
+			v1 = numval(r1);
+			if(op == Lpostinc)
+				r1++;
+			else
+				r1--;
+			putValue(ex, a1, numval(r1));
+		Linc or
+		Ldec or
+		Lpreadd or
+		Lpresub =>
+			a1 = epop(ex);
+			r1 = toNumber(ex, getValue(ex, a1));
+			case op{
+			Linc =>
+				r1++;
+			Ldec =>
+				r1--;
+			Lpresub =>
+				r1 = -r1;
+			}
+			v1 = numval(r1);
+			if(op == Linc || op == Ldec)
+				putValue(ex, a1, v1);
+		'~' =>
+			v = epopval(ex);
+			i1 = toInt32(ex, v);
+			i1 = ~i1;
+			v1 = numval(real i1);
+		'!' =>
+			v = epopval(ex);
+			v1 = toBoolean(ex, v);
+			if(v1 == true)
+				v1 = false;
+			else
+				v1 = true;
+		Ltypeof =>
+			a1 = epop(ex);
+			if(a1.isref && getBase(ex, a1) == nil)
+				s = "undefined";
+			else case (v1 = getValue(ex, a1)).ty{
+			TUndef =>
+				s = "undefined";
+			TNull =>
+				s = "object";
+			TBool =>
+				s = "boolean";
+			TNum =>
+				s = "number";
+			TStr =>
+				s = "string";
+			TObj =>
+				if(v1.obj.call != nil)
+					s = "function";
+				else
+					s = "object";
+			TRegExp =>
+				s = "regexp";
+			}
+			v1 = strval(s);
+		Ldelete =>
+			a1 = epop(ex);
+			o = getBase(ex, a1);
+			s = getPropertyName(ex, a1);
+			if(o != nil)
+				esdelete(ex, o, s, 0);
+			v1 = undefined;
+		Lvoid =>
+			epopval(ex);
+			v = undefined;
+		'*' or
+		'/' or
+		'%' or
+		'-' =>
+			v2 = epopval(ex);
+			a1 = epop(ex);
+			r1 = toNumber(ex, getValue(ex, a1));
+			r2 = toNumber(ex, v2);
+			case op{
+			'*' =>
+				r1 = r1 * r2;
+			'/' =>
+				r1 = r1 / r2;
+			'%' =>
+				r1 = fmod(r1, r2);
+			'-' =>
+				r1 = r1 - r2;
+			}
+			v1 = numval(r1);
+		'+' =>
+			v2 = epopval(ex);
+			a1 = epop(ex);
+			v1 = toPrimitive(ex, getValue(ex, a1), NoHint);
+			v2 = toPrimitive(ex, v2, NoHint);
+			if(v1.ty == TStr || v2.ty == TStr)
+				v1 = strval(toString(ex, v1)+toString(ex, v2));
+			else
+				v1 = numval(toNumber(ex, v1)+toNumber(ex, v2));
+		Llsh or
+		Lrsh or
+		Lrshu or
+		'&' or
+		'^' or
+		'|' =>
+			v2 = epopval(ex);
+			a1 = epop(ex);
+			i1 = toInt32(ex, getValue(ex, a1));
+			i2 = toInt32(ex, v2);
+			case op{
+			Llsh =>
+				i1 <<= i2 & 16r1f;
+			Lrsh =>
+				i1 >>= i2 & 16r1f;
+			Lrshu =>
+				i1 = int (((big i1) & 16rffffffff) >> (i2 & 16r1f));
+			'&' =>
+				i1 &= i2;
+			'|' =>
+				i1 |= i2;
+			'^' =>
+				i1 ^= i2;
+			}
+			v1 = numval(real i1);
+		'=' or
+		Las =>
+			v1 = epopval(ex);
+			a1 = epop(ex);
+			putValue(ex, a1, v1);
+		'<' or
+		'>' or
+		Lleq or
+		Lgeq =>
+			v2 = epopval(ex);
+			v1 = epopval(ex);
+			if(op == '>' || op == Lleq){
+				v = v1;
+				v1 = v2;
+				v2 = v;
+			}
+			v1 = toPrimitive(ex, v1, TNum);
+			v2 = toPrimitive(ex, v2, TNum);
+			if(v1.ty == TStr && v2.ty == TStr){
+				if(v1.str < v2.str)
+					v1 = true;
+				else
+					v1 = false;
+			}else{
+				r1 = toNumber(ex, v1);
+				r2 = toNumber(ex, v2);
+				if(isnan(r1) || isnan(r2))
+					v1 = undefined;
+				else if(r1 < r2)
+					v1 = true;
+				else
+					v1 = false;
+			}
+			if(op == Lgeq || op == Lleq){
+				if(v1 == false)
+					v1 = true;
+				else
+					v1 = false;
+			}
+		Lin =>
+			v2 = epopval(ex);
+			v1 = epopval(ex);
+			if(v2.ty != TObj)
+				runtime(ex, TypeError, "rhs of 'in' not an object");
+			s = toString(ex, v1);
+			v1 = eshasproperty(ex, v2.obj, s, 0);
+		Linstanceof =>
+			v2 = epopval(ex);
+			v1 = epopval(ex);
+			if(v2.ty != TObj)
+				runtime(ex, TypeError, "rhs of 'instanceof' not an object");
+			if(!isfuncobj(v2.obj))
+				runtime(ex, TypeError, "rhs of 'instanceof' not a function");
+			if(v1.ty != TObj)
+				v1 = false;
+			else{
+				v2 = esget(ex, v2.obj, "prototype", 0);
+				if(v2.ty != TObj)
+					runtime(ex, TypeError, "prototype value not an object");
+				o = v2.obj;
+				for(p := v1.obj.prototype; p != nil; p = p.prototype){
+					if(p == o){
+						v1 = true;
+						break;
+					}
+				}
+				if(p == nil)
+					v1 = false;
+			}
+		Leq or
+		Lneq or
+		Lseq or
+		Lsne =>
+			strict := op == Lseq || op == Lsne;
+			v2 = epopval(ex);
+			v1 = epopval(ex);
+			v = false;
+			while(v1.ty != v2.ty){
+				if(strict)
+					break;
+				if(isnull(v1) && v2 == undefined
+				|| v1 == undefined && isnull(v2))
+					v1 = v2;
+				else if(v1.ty == TNum && v2.ty == TStr)
+					v2 = numval(toNumber(ex, v2));
+				else if(v1.ty == TStr && v2.ty == TNum)
+					v1 = numval(toNumber(ex, v1));
+				else if(v1.ty == TBool)
+					v1 = numval(toNumber(ex, v1));
+				else if(v2.ty == TBool)
+					v2 = numval(toNumber(ex, v2));
+				else if(v2.ty == TObj && (v1.ty == TStr || v1.ty == TNum))
+					v2 = toPrimitive(ex, v2, NoHint);
+				else if(v1.ty == TObj && (v2.ty == TStr || v2.ty == TNum))
+					v1 = toPrimitive(ex, v1, NoHint);
+				else{
+					v1 = true;
+					v2 = false;
+				}
+			}
+			if(v1.ty != v2.ty)
+				v = false;
+			else{
+				case v1.ty{
+				TUndef or
+				TNull =>
+					v = true;
+				TNum =>
+					if(v1.num == v2.num)
+						v = true;
+				TBool =>
+					if(v1 == v2)
+						v = true;
+				TStr =>
+					if(v1.str == v2.str)
+						v = true;
+				TObj =>
+					if(v1.obj == v2.obj)
+						v = true;
+				TRegExp =>
+					if(v1.rev.p == v2.rev.p && v1.rev.f == v2.rev.f)
+						v = true;
+				}
+			}
+			if(op == Lneq || op == Lsne){
+				if(v == false)
+					v = true;
+				else
+					v = false;
+			}
+			v1 = v;
+		Landand =>
+			v1 = epopval(ex);
+			(pc, apc) = getjmp(code.ops, pc);
+			if(toBoolean(ex, v1) != false){
+				(pc, a1) = eexp(ex, code, pc, apc);
+				v1 = getValue(ex, a1);
+			}
+			pc = apc;
+		Loror =>
+			v1 = epopval(ex);
+			(pc, apc) = getjmp(code.ops, pc);
+			if(toBoolean(ex, v1) != true){
+				(pc, a1) = eexp(ex, code, pc, apc);
+				v1 = getValue(ex, a1);
+			}
+			pc = apc;
+		'?' =>
+			v1 = epopval(ex);
+			(pc, apc) = getjmp(code.ops, pc);
+			v1 = toBoolean(ex, v1);
+			if(v1 == true)
+				(pc, a1) = eexp(ex, code, pc, apc);
+			pc = apc;
+			(pc, apc) = getjmp(code.ops, pc);
+			if(v1 != true)
+				(pc, a1) = eexp(ex, code, pc, apc);
+			pc = apc;
+			v1 = getValue(ex, a1);
+		Lasop =>
+			a1 = epop(ex);
+			epush(ex, a1);
+			v1 = getValue(ex, a1);
+		Lgetval =>
+			v1 = epopval(ex);
+		',' =>
+			v1 = epopval(ex);
+			epop(ex);
+			# a1's value already gotten by Lgetval
+		'(' or
+		')' =>
+			continue;
+		Larrinit =>
+			o = narray(ex, nil, nil);
+			(pc, c) = getconst(code.ops, pc);
+			esput(ex, o, "length", numval(real c), 0);
+			c = ex.sp-c;
+			for(sp := c; sp < ex.sp; sp++){
+				v = getValue(ex, ex.stack[sp]);
+				if(v != undefined)
+					esput(ex, o, string (sp-c), v, 0);
+			}
+			ex.sp = c;
+			v1 = objval(o);
+		Lobjinit =>
+			o = nobj(ex, nil, nil);
+			(pc, c) = getconst(code.ops, pc);
+			c = ex.sp-2*c;
+			for(sp := c; sp < ex.sp; sp += 2){
+				v = getValue(ex, ex.stack[sp]);
+				if(isnum(v) || isstr(v))
+					p := toString(ex, v);
+				else
+					p = ex.stack[sp].name;
+				v = getValue(ex, ex.stack[sp+1]);
+				esput(ex, o, p, v, 0);
+			}
+			ex.sp = c;
+			v1 = objval(o);
+		Lcall or
+		Lnewcall =>
+			(pc, c) = getconst(code.ops, pc);
+			args := array[c] of ref Val;
+			c = ex.sp - c;
+			for(sp := c; sp < ex.sp; sp++)
+				args[sp-c] = getValue(ex, ex.stack[sp]);
+			ex.sp = c;
+			a1 = epop(ex);
+			v = getValue(ex, a1);
+			o = getobj(v);
+			if(op == Lcall){
+				if(o == nil || o.call == nil)
+					runtime(ex, TypeError, "can only call function objects ("+a1.name+")");
+				th = nil;
+				if(a1.isref){
+					th = getBase(ex, a1);
+					if(th != nil && isactobj(th))
+						th = nil;
+				}
+
+				# have to execute functions in the same context as they
+				# were defined, but need to use current stack.
+				if (o.call.ex == nil)
+					a1 = escall(ex, v.obj, th, args, 0);
+				else {
+					fnex := ref *o.call.ex;
+					fnex.stack = ex.stack;
+					fnex.sp = ex.sp;
+					fnex.scopechain = fnex.global :: nil;
+					# drop ref to stack to avoid array duplication should stack grow
+					ex.stack = nil;
+					osp := ex.sp;
+					# can get an exception here that corrupts ex etc.
+#aardvark:=99;
+#test:=99;
+# zebra:=99;
+					{
+						a1 = escall(fnex, v.obj, th, args, 0);
+					}
+					exception e{
+						"throw" =>
+							# copy up error so as it gets reported properly
+							ex.error = fnex.error;
+							ex.errval = fnex.errval;
+							ex.stack = fnex.stack;
+							ex.sp = osp;
+#							raise e;
+							raise "throw";
+					}
+					# restore stack, sp is OK as escall() ensures that stack is balanced
+					ex.stack = fnex.stack;
+				}
+			}else{
+				if(o == nil || o.construct == nil)
+					runtime(ex, TypeError, "new must be given a constructor object");
+				a1 = valref(objval(esconstruct(ex, o, args)));
+			}
+			epush(ex, a1);
+			args = nil;
+			continue;
+		Lnew =>
+			v = epopval(ex);
+			o = getobj(v);
+			if(o == nil || o.construct == nil)
+				runtime(ex, TypeError, "new must be given a constructor object");
+			v1 = objval(esconstruct(ex, o, nil));
+		Lfunction =>
+			(pc, c) = getconst(code.ops, pc);
+			v1 = objval(code.fexps[c]);
+		';' =>
+			break out;
+		* =>
+			fatal(ex, sprint("eexp: unknown op %s\n", tokname(op)));
+		}
+		epushval(ex, v1);
+	}
+
+	if(savesp == ex.sp)
+		return (pc, nil);
+
+	if(savesp != ex.sp-1)
+		print("unbalanced stack in eexp: %d %d\n", savesp, ex.sp);
+	return (pc, epop(ex));
+}
+
+epushval(ex: ref Exec, v: ref Val)
+{
+	epush(ex, valref(v));
+}
+
+epush(ex: ref Exec, r: ref Ref)
+{
+	if(ex.sp >= len ex.stack){
+		st := array[2 * len ex.stack] of ref Ref;
+		st[:] = ex.stack;
+		ex.stack = st;
+	}
+	ex.stack[ex.sp++] = r;
+}
+
+epop(ex: ref Exec): ref Ref
+{
+	if(ex.sp == 0)
+		fatal(ex, "popping too far off the estack\n");
+	return ex.stack[--ex.sp];
+}
+
+epopval(ex: ref Exec): ref Val
+{
+	if(ex.sp == 0)
+		fatal(ex, "popping too far off the estack\n");
+	return getValue(ex, ex.stack[--ex.sp]);
+}
+
+inlabs(lab: string, labs: list of string): int
+{
+	for(l := labs; l != nil; l = tl l)
+		if(hd l == lab)
+			return 1;
+	return 0;
+}
+
+initlabs(lab: string, labs: list of string): int
+{
+	return lab == nil || inlabs(lab, labs);
+}
--- /dev/null
+++ b/appl/lib/ecmascript/mkfile
@@ -1,0 +1,23 @@
+<../../../mkconfig
+
+TARG=	ecmascript.dis\
+
+MODULES=\
+	builtin.b\
+	date.b\
+	exec.b\
+	obj.b\
+	pprint.b\
+	regexp.b\
+	uri.b\
+
+SYSMODULES= \
+	sys.m\
+	math.m\
+	string.m\
+	daytime.m\
+	ecmascript.m\
+
+DISBIN=$ROOT/dis/lib
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/ecmascript/obj.b
@@ -1,0 +1,836 @@
+#
+# want to use the value in a context which
+# prefers an object, so coerce schizo vals
+# to object versions
+#
+coerceToObj(ex: ref Exec, v: ref Val): ref Val
+{
+	o: ref Obj;
+
+	case v.ty{
+	TBool =>
+		o = mkobj(ex.boolproto, "Boolean");
+		o.val = v;
+	TStr =>
+		o = mkobj(ex.strproto, "String");
+		o.val = v;
+		valinstant(o, DontEnum|DontDelete|ReadOnly, "length", numval(real len v.str));
+	TNum =>
+		o = mkobj(ex.numproto, "Number");
+		o.val = v;
+	TRegExp =>
+		o = mkobj(ex.regexpproto, "RegExp");
+		o.val = v;
+		valinstant(o, DontEnum|DontDelete|ReadOnly, "length", numval(real len v.rev.p));
+		valinstant(o, DontEnum|DontDelete|ReadOnly, "source", strval(v.rev.p));
+		valinstant(o, DontEnum|DontDelete|ReadOnly, "global", strhas(v.rev.f, 'g'));
+		valinstant(o, DontEnum|DontDelete|ReadOnly, "ignoreCase", strhas(v.rev.f, 'i'));
+		valinstant(o, DontEnum|DontDelete|ReadOnly, "multiline", strhas(v.rev.f, 'm'));
+		valinstant(o, DontEnum|DontDelete, "lastIndex", numval(real v.rev.i));
+	* =>
+		return v;
+	}
+	return objval(o);
+}
+
+coerceToVal(v: ref Val): ref Val
+{
+	if(v.ty != TObj)
+		return v;
+	o := v.obj;
+	if(o.host != nil && o.host != me
+	|| o.class != "String"
+	|| o.class != "Number"
+	|| o.class != "Boolean")
+		return v;
+	return o.val;
+}
+
+isstrobj(o: ref Obj): int
+{
+	return (o.host == nil || o.host == me) && o.class == "String";
+}
+
+isnumobj(o: ref Obj): int
+{
+	return (o.host == nil || o.host == me) && o.class == "Number";
+}
+
+isboolobj(o: ref Obj): int
+{
+	return (o.host == nil || o.host == me) && o.class == "Boolean";
+}
+
+isdateobj(o: ref Obj): int
+{
+	return (o.host == nil || o.host == me) && o.class == "Date";
+}
+
+isregexpobj(o: ref Obj): int
+{
+	return (o.host == nil || o.host == me) && o.class == "RegExp";
+}
+
+isfuncobj(o: ref Obj): int
+{
+	return (o.host == nil || o.host == me) && o.class == "Function";
+}
+
+isarray(o: ref Obj): int
+{
+#	return (o.host == nil || o.host == me) && o.class == "Array";
+	# relax the host test
+	# so that hosts can intercept Array operations and defer
+	# unhandled ops to the builtin
+	return o.class == "Array";
+}
+
+iserr(o: ref Obj): int
+{
+	return (o.host == nil || o.host == me) && o.class == "Error";
+}
+
+isactobj(o: ref Obj): int
+{
+	return o.host == nil && o.class == "Activation";
+}
+
+isnull(v: ref Val): int
+{
+	return v == null || v == nil;
+}
+
+isundefined(v: ref Val): int
+{
+	return v == undefined;
+}
+
+isstr(v: ref Val): int
+{
+	return v.ty == TStr;
+}
+
+isnum(v: ref Val): int
+{
+	return v.ty == TNum;
+}
+
+isbool(v: ref Val): int
+{
+	return v.ty == TBool;
+}
+
+isobj(v: ref Val): int
+{
+	return v.ty == TObj;
+}
+
+isregexp(v: ref Val): int
+{
+	return v.ty == TRegExp || v.ty == TObj && isregexpobj(v.obj);
+}
+
+#
+# retrieve the object field if it's valid
+#
+getobj(v: ref Val): ref Obj
+{
+	if(v.ty == TObj)
+		return v.obj;
+	return nil;
+}
+
+isprimval(v: ref Val): int
+{
+	return v.ty != TObj;
+}
+
+pushscope(ex: ref Exec, o: ref Obj)
+{
+	ex.scopechain = o :: ex.scopechain;
+}
+
+popscope(ex: ref Exec)
+{
+	ex.scopechain = tl ex.scopechain;
+}
+
+runtime(ex: ref Exec, o: ref Obj, s: string)
+{
+	ex.error = s;
+	if(o == nil)
+		ex.errval = undefined;
+	else
+		ex.errval = objval(o);
+	if(debug['r']){
+		print("ecmascript runtime error: %s\n", s);
+		if(""[5] == -1);	# abort
+	}
+	raise "throw";
+	exit;	# never reached
+}
+
+mkobj(proto: ref Obj, class: string): ref Obj
+{
+	if(class == nil)
+		class = "Object";
+	return ref Obj(nil, proto, nil, nil, nil, class, nil, nil);
+}
+
+valcheck(ex: ref Exec, v: ref Val, hint: int)
+{
+	if(v == nil
+	|| v.ty < 0
+	|| v.ty >= NoHint
+	|| v.ty == TBool && v != true && v != false
+	|| v.ty == TObj && v.obj == nil
+	|| hint != NoHint && v.ty != hint)
+		runtime(ex, RangeError, "bad value generated by host object");
+}
+
+# builtin methods for properties
+esget(ex: ref Exec, o: ref Obj, prop: string, force: int): ref Val
+{
+	for( ; o != nil; o = o.prototype){
+		if(!force && o.host != nil && o.host != me){
+			v := o.host->get(ex, o, prop);
+			valcheck(ex, v, NoHint);
+			return v;
+		}
+
+		for(i := 0; i < len o.props; i++)
+			if(o.props[i] != nil && o.props[i].name == prop)
+				return o.props[i].val.val;
+		force = 0;
+	}
+	return undefined;
+}
+
+esputind(o: ref Obj, prop: string): int
+{
+	empty := -1;
+	props := o.props;
+	for(i := 0; i < len props; i++){
+		if(props[i] == nil)
+			empty = i;
+		else if(props[i].name == prop)
+			return i;
+	}
+	if(empty != -1)
+		return empty;
+	
+	props = array[i+1] of ref Prop;
+	props[:] = o.props;
+	o.props = props;
+	return i;
+}
+
+esput(ex: ref Exec, o: ref Obj, prop: string, v: ref Val, force: int)
+{
+	ai: big;
+
+	if(!force && o.host != nil && o.host != me)
+		return o.host->put(ex, o, prop, v);
+
+	if(escanput(ex, o, prop, 0) != true)
+		return;
+
+	#
+	# should this test for prototype == ex.arrayproto?
+	# hard to say, but 15.4.5 "Properties of Array Instances" implies not
+	#
+	if(isarray(o))
+		al := toUint32(ex, esget(ex, o, "length", 1));
+
+	i := esputind(o, prop);
+	props := o.props;
+	if(props[i] != nil)
+		props[i].val.val = v;
+	else
+		props[i] = ref Prop(0, prop, ref RefVal(v));
+	if(!isarray(o))
+		return;
+
+	if(prop == "length"){
+		nl := toUint32(ex, v);
+		for(ai = nl; ai < al; ai++)
+			esdelete(ex, o, string ai, 1);
+		props[i].val.val = numval(real nl);
+	}else{
+		ai = big prop;
+		if(prop != string ai || ai < big 0 || ai >= 16rffffffff)
+			return;
+		i = esputind(o, "length");
+		if(props[i] == nil)
+			fatal(ex, "bogus array esput");
+		else if(toUint32(ex, props[i].val.val) <= ai)
+			props[i].val.val = numval(real(ai+big 1));
+	}
+}
+
+escanput(ex: ref Exec, o: ref Obj, prop: string, force: int): ref Val
+{
+	for( ; o != nil; o = o.prototype){
+		if(!force && o.host != nil && o.host != me){
+			v := o.host->canput(ex, o, prop);
+			valcheck(ex, v, TBool);
+			return v;
+		}
+
+		for(i := 0; i < len o.props; i++){
+			if(o.props[i] != nil && o.props[i].name == prop){
+				if(o.props[i].attr & ReadOnly)
+					return false;
+				else
+					return true;
+			}
+		}
+
+		force = 0;
+	}
+	return true;
+}
+
+eshasproperty(ex: ref Exec, o: ref Obj, prop: string, force: int): ref Val
+{
+	for(; o != nil; o = o.prototype){
+		if(!force && o.host != nil && o.host != me){
+			v := o.host->hasproperty(ex, o, prop);
+			valcheck(ex, v, TBool);
+			return v;
+		}
+		for(i := 0; i < len o.props; i++)
+			if(o.props[i] != nil && o.props[i].name == prop)
+				return true;
+	}
+	return false;
+}
+
+eshasenumprop(o: ref Obj, prop: string): ref Val
+{
+	for(i := 0; i < len o.props; i++)
+		if(o.props[i] != nil && o.props[i].name == prop){
+			if(o.props[i].attr & DontEnum)
+				return false;
+			return true;
+		}
+	return false;
+}
+	
+propshadowed(start, end: ref Obj, prop: string): int
+{
+	for(o := start; o != end; o = o.prototype){
+		if(o.host != nil && o.host != me)
+			return 0;
+		for(i := 0; i < len o.props; i++)
+			if(o.props[i] != nil && o.props[i].name == prop)
+				return 1;
+	}
+	return 0;
+}
+
+esdelete(ex: ref Exec, o: ref Obj, prop: string, force: int)
+{
+	if(!force && o.host != nil && o.host != me)
+		return o.host->delete(ex, o, prop);
+
+	for(i := 0; i < len o.props; i++){
+		if(o.props[i] != nil && o.props[i].name == prop){
+			if(!(o.props[i].attr & DontDelete))
+				o.props[i] = nil;
+			return;
+		}
+	}
+}
+
+esdeforder := array[] of {"valueOf", "toString"};
+esdefaultval(ex: ref Exec, o: ref Obj, ty: int, force: int): ref Val
+{
+	v: ref Val;
+
+	if(!force && o.host != nil && o.host != me){
+		v = o.host->defaultval(ex, o, ty);
+		valcheck(ex, v, NoHint);
+		if(!isprimval(v))
+			runtime(ex, TypeError, "host object returned an object to [[DefaultValue]]");
+		return v;
+	}
+
+	hintstr := 0;
+	if(ty == TStr || ty == NoHint && isdateobj(o))
+		hintstr = 1;
+
+	for(i := 0; i < 2; i++){
+		v = esget(ex, o, esdeforder[hintstr ^ i], 0);
+		if(v != undefined && v.ty == TObj && v.obj.call != nil){
+			r := escall(ex, v.obj, o, nil, 0);
+			v = nil;
+			if(!r.isref)
+				v = r.val;
+			if(v != nil && isprimval(v))
+				return v;
+		}
+	}
+	runtime(ex, TypeError, "no default value");
+	return nil;
+}
+
+esprimid(ex: ref Exec, s: string): ref Ref
+{
+	for(sc := ex.scopechain; sc != nil; sc = tl sc){
+		o := hd sc;
+		if(eshasproperty(ex, o, s, 0) == true)
+			return ref Ref(1, nil, o, s);
+	}
+
+	#
+	# the right place to add literals?
+	#
+	case s{
+	"null" =>
+		return ref Ref(0, null, nil, "null");
+	"true" =>
+		return ref Ref(0, true, nil, "true");
+	"false" =>
+		return ref Ref(0, false, nil, "false");
+	}
+	return ref Ref(1, nil, nil, s);
+}
+
+bivar(ex: ref Exec, sc: list of ref Obj, s: string): ref Val
+{
+	for(; sc != nil; sc = tl sc){
+		o := hd sc;
+		if(eshasproperty(ex, o, s, 0) == true)
+			return esget(ex, o, s, 0);
+	}
+	return nil;
+}
+
+esconstruct(ex: ref Exec, func: ref Obj, args: array of ref Val): ref Obj
+{
+	o: ref Obj;
+
+	if(func.construct == nil)
+		runtime(ex, TypeError, "new must be applied to a constructor object");
+	if(func.host != nil)
+		o = func.host->construct(ex, func, args);
+	else{
+		o = getobj(esget(ex, func, "prototype", 0));
+		if(o == nil)
+			o = ex.objproto;
+		this := mkobj(o, "Object");
+		o = getobj(getValue(ex, escall(ex, func, this, args, 0)));
+
+		# Divergence from ECMA-262
+		#
+		# observed that not all script-defined constructors return an object,
+		# the value of 'this' is assumed to be the value of the constructor
+		if (o == nil)
+			o = this;
+	}
+	if(o == nil)
+		runtime(ex, TypeError, func.val.str+" failed to generate an object");
+	return o;
+}
+
+escall(ex: ref Exec, func, this: ref Obj, args: array of ref Val, eval: int): ref Ref
+{
+	if(func.call == nil)
+		runtime(ex, TypeError, "can only call function objects");
+	if(this == nil)
+		this = ex.global;
+
+	r: ref Ref = nil;
+	if(func.host != nil){
+		r = func.host->call(ex, func, this, args, 0);
+		if(r.isref && r.name == nil)
+			runtime(ex, ReferenceError, "host call returned a bad reference");
+		else if(!r.isref)
+			valcheck(ex, r.val, NoHint);
+		return r;
+	}
+
+	argobj := mkobj(ex.objproto, "Object");
+	actobj := mkobj(nil, "Activation");
+
+	oargs: ref RefVal = nil;
+	props := func.props;
+	empty := -1;
+	i := 0;
+	for(i = 0; i < len props; i++){
+		if(props[i] == nil)
+			empty = i;
+		else if(props[i].name == "arguments"){
+			oargs = props[i].val;
+			empty = i;
+			break;
+		}
+	}
+	if(i == len func.props){
+		if(empty == -1){
+			props = array[i+1] of ref Prop;
+			props[:] = func.props;
+			func.props = props;
+			empty = i;
+		}
+		props[empty] = ref Prop(DontDelete|DontEnum|ReadOnly, "arguments", nil);
+	}
+	props[empty].val = ref RefVal(objval(argobj));
+
+	#
+	#see section 10.1.3 page 33
+	# if multiple params share the same name, the last one takes effect
+	# vars don't override params of the same name, or earlier parms defs
+	#
+	actobj.props = array[] of {ref Prop(DontDelete, "arguments", ref RefVal(objval(argobj)))};
+
+	argobj.props = array[len args + 2] of {
+		ref Prop(DontEnum, "callee", ref RefVal(objval(func))),
+		ref Prop(DontEnum, "length", ref RefVal(numval(real len args))),
+	};
+
+	#
+	# instantiate the arguments by name in the activation object
+	# and by number in the arguments object, aliased to the same RefVal.
+	#
+	params := func.call.params;
+	for(i = 0; i < len args; i++){
+		rjv := ref RefVal(args[i]);
+		argobj.props[i+2] = ref Prop(DontEnum, string i, rjv);
+		if(i < len params)
+			fvarinstant(actobj, 1, DontDelete, params[i], rjv);
+	}
+	for(; i < len params; i++)
+		fvarinstant(actobj, 1, DontDelete, params[i], ref RefVal(undefined));
+
+	#
+	# instantiate the local variables defined within the function
+	#
+	vars := func.call.code.vars;
+	for(i = 0; i < len vars; i++)
+		valinstant(actobj, DontDelete, vars[i].name, undefined);
+
+	# NOTE: the treatment of scopechain here is wrong if nested functions are
+	# permitted.  ECMA-262 currently does not support nested functions (so we
+	# are ok for now) - but other flavours of Javascript do.
+	# Difficulties are introduced by multiple execution contexts.
+	# e.g. in web browsers, one frame can ref a func in
+	# another frame (each frame has a distinct execution context), but the func
+	# ids must bind as if in original lexical context
+
+	osc := ex.scopechain;
+	ex.this = this;
+	ex.scopechain = actobj :: osc;
+	(k, v, nil) := exec(ex, func.call.code);
+	ex.scopechain = osc;
+
+	#
+	# i can find nothing in the docs which defines
+	# the value of a function call
+	# this seems like a reasonable definition
+	#
+	if (k == CThrow)
+		raise "throw";
+	if(!eval && k != CReturn || v == nil)
+		v = undefined;
+	r = valref(v);
+
+	props = func.props;
+	for(i = 0; i < len props; i++){
+		if(props[i] != nil && props[i].name == "arguments"){
+			if(oargs == nil)
+				props[i] = nil;
+			else
+				props[i].val = oargs;
+			break;
+		}
+	}
+
+	return r;
+}
+
+#
+# routines for instantiating variables
+#
+fvarinstant(o: ref Obj, force, attr: int, s: string, v: ref RefVal)
+{
+	props := o.props;
+	empty := -1;
+	for(i := 0; i < len props; i++){
+		if(props[i] == nil)
+			empty = i;
+		else if(props[i].name == s){
+			if(force){
+				props[i].attr = attr;
+				props[i].val = v;
+			}
+			return;
+		}
+	}
+	if(empty == -1){
+		props = array[i+1] of ref Prop;
+		props[:] = o.props;
+		o.props = props;
+		empty = i;
+	}
+	props[empty] = ref Prop(attr, s, v);
+}
+
+varinstant(o: ref Obj, attr: int, s: string, v: ref RefVal)
+{
+	fvarinstant(o, 0, attr, s, v);
+}
+
+valinstant(o: ref Obj, attr: int, s: string, v: ref Val)
+{
+	fvarinstant(o, 0, attr, s, ref RefVal(v));
+}
+
+#
+# instantiate global or val variables
+# note that only function variables are forced to be redefined;
+# all other variables have a undefined val.val field
+#
+globalinstant(o: ref Obj, vars: array of ref Prop)
+{
+	for(i := 0; i < len vars; i++){
+		force := vars[i].val.val != undefined;
+		fvarinstant(o, force, 0, vars[i].name, vars[i].val);
+	}
+}
+
+numval(r: real): ref Val
+{
+	return ref Val(TNum, r, nil, nil, nil);
+}
+
+strval(s: string): ref Val
+{
+	return ref Val(TStr, 0., s, nil, nil);
+}
+
+objval(o: ref Obj): ref Val
+{
+	return ref Val(TObj, 0., nil, o, nil);
+}
+
+regexpval(p: string, f: string, i: int): ref Val
+{
+	return ref Val(TRegExp, 0., nil, nil, ref REval(p, f, i));
+}
+
+#
+# operations on refereneces
+# note the substitution of nil for an object
+# version of null, implied in the discussion of
+# Reference Types, since there isn't a null object
+#
+valref(v: ref Val): ref Ref
+{
+	return ref Ref(0, v, nil, nil);
+}
+
+getBase(ex: ref Exec, r: ref Ref): ref Obj
+{
+	if(!r.isref)
+		runtime(ex, ReferenceError, "not a reference");
+	return r.base;
+}
+
+getPropertyName(ex: ref Exec, r: ref Ref): string
+{
+	if(!r.isref)
+		runtime(ex, ReferenceError, "not a reference");
+	return r.name;
+}
+
+getValue(ex: ref Exec, r: ref Ref): ref Val
+{
+	if(!r.isref)
+		return r.val;
+	b := r.base;
+	if(b == nil)
+		runtime(ex, ReferenceError, "reference " + r.name + " is null");
+	return esget(ex, b, r.name, 0);
+}
+
+putValue(ex: ref Exec, r: ref Ref, v: ref Val)
+{
+	if(!r.isref)
+		runtime(ex, ReferenceError, "not a reference: " + r.name);
+	b := r.base;
+	if(b == nil)
+		b = ex.global;
+	esput(ex, b, r.name, v, 0);
+}
+
+#
+# conversion routines defined by the abstract machine
+# see section 9.
+# note that string, boolean, and number objects are
+# not automaically coerced to values, and vice versa.
+# 
+toPrimitive(ex: ref Exec, v: ref Val, ty: int): ref Val
+{
+	if(v.ty != TObj)
+		return v;
+	v = esdefaultval(ex, v.obj, ty, 0);
+	if(v.ty == TObj)
+		runtime(ex, TypeError, "toPrimitive returned an object");
+	return v;
+}
+
+toBoolean(ex: ref Exec, v: ref Val): ref Val
+{
+	case v.ty{
+	TUndef or
+	TNull =>
+		return false;
+	TBool =>
+		return v;
+	TNum =>
+		if(isnan(v.num))
+			return false;
+		if(v.num == 0.)
+			return false;
+	TStr =>
+		if(v.str == "")
+			return false;
+	TObj =>
+		break;
+	TRegExp =>
+		break;
+	* =>
+		runtime(ex, TypeError, "unknown type in toBoolean");
+	}
+	return true;
+}
+
+toNumber(ex: ref Exec, v: ref Val): real
+{
+	case v.ty{
+	TUndef =>
+		return NaN;
+	TNull =>
+		return 0.;
+	TBool =>
+		if(v == false)
+			return 0.;
+		return 1.;
+	TNum =>
+		return v.num;
+	TStr =>
+		(si, r) := parsenum(ex, v.str, 0, ParseReal|ParseHex|ParseTrim|ParseEmpty);
+		if(si != len v.str)
+			r = Math->NaN;
+		return r;
+	TObj =>
+		return toNumber(ex, toPrimitive(ex, v, TNum));
+	TRegExp =>
+		return NaN;
+	* =>
+		runtime(ex, TypeError, "unknown type in toNumber");
+		return 0.;
+	}
+}
+
+toInteger(ex: ref Exec, v: ref Val): real
+{
+	r := toNumber(ex, v);
+	if(isnan(r))
+		return 0.;
+	if(r == 0. || r == +Infinity || r == -Infinity)
+		return r;
+	return copysign(floor(fabs(r)), r);
+}
+
+#
+# toInt32 == toUint32, except for numbers > 2^31
+#
+toInt32(ex: ref Exec, v: ref Val): int
+{
+	r := toNumber(ex, v);
+	if(isnan(r) || r == 0. || r == +Infinity || r == -Infinity)
+		return 0;
+	r = copysign(floor(fabs(r)), r);
+	# need to convert to big since it might be unsigned
+	return int big fmod(r, 4294967296.);
+}
+
+toUint32(ex: ref Exec, v: ref Val): big
+{
+	r := toNumber(ex, v);
+	if(isnan(r) || r == 0. || r == +Infinity || r == -Infinity)
+		return big 0;
+	r = copysign(floor(fabs(r)), r);
+	# need to convert to big since it might be unsigned
+	b := big fmod(r, 4294967296.);
+	if(b < big 0)
+		fatal(ex, "uint32 < 0");
+	return b;
+}
+
+toUint16(ex: ref Exec, v: ref Val): int
+{
+	return toInt32(ex, v) & 16rffff;
+}
+
+toString(ex: ref Exec, v: ref Val): string
+{
+	case v.ty{
+	TUndef =>
+		return "undefined";
+	TNull =>
+		return "null";
+	TBool =>
+		if(v == false)
+			return "false";
+		return "true";
+	TNum =>
+		r := v.num;
+		if(isnan(r))
+			return "NaN";
+		if(r == 0.)
+			return "0";
+		if(r == Infinity)
+			return "Infinity";
+		if(r == -Infinity)
+			return "-Infinity";
+		# this is wrong, but right is too hard
+		if(r < 1000000000000000000000. && r >= 1./(1000000.)){
+			return string r;
+		}
+		return string r;
+	TStr =>
+		return v.str;
+	TObj =>
+		return toString(ex, toPrimitive(ex, v, TStr));
+	TRegExp =>
+		return "/" + v.rev.p + "/" + v.rev.f;
+	* =>
+		runtime(ex, TypeError, "unknown type in ToString");
+		return "";
+	}
+}
+
+toObject(ex: ref Exec, v: ref Val): ref Obj
+{
+	case v.ty{
+	TUndef =>
+		runtime(ex, TypeError, "can't convert undefined to an object");
+	TNull =>
+		runtime(ex, TypeError, "can't convert null to an object");
+	TBool or
+	TStr or
+	TNum or
+	TRegExp =>
+		return coerceToObj(ex, v).obj;
+	TObj =>
+		return v.obj;
+	* =>
+		runtime(ex, TypeError, "unknown type in toObject");
+		return nil;
+	}
+	return nil;
+}
--- /dev/null
+++ b/appl/lib/ecmascript/pprint.b
@@ -1,0 +1,378 @@
+PPrint: adt
+{
+	ex:	ref Exec;
+	code:	ref Code;
+	stack:	array of string;
+	sp:	int;
+};
+
+mkpprint(ex: ref Exec, code: ref Code): ref PPrint
+{
+	return ref PPrint(ex, code, array[4] of string, 0);
+}
+
+funcprint(ex: ref Exec, func: ref Ecmascript->Obj): string
+{
+	params := func.call.params;
+	(nil, name) := str->splitr(func.val.str, ".");
+	s := "function " + name + "(";
+	sep := "";
+	for(i := 0; i < len params; i++){
+		s += sep + params[i];
+		sep = ", ";
+	}
+	s += "){";
+	if(func.host != nil)
+		s += "[host code]";
+	else
+		s += "\n" + pprint(ex, func.call.code, "	");
+	s += "}";
+	return s;
+}
+
+pprint(ex: ref Exec, code: ref Code, indent: string): string
+{
+	pp := ref PPrint(ex, code, array[4] of string, 0);
+#for(i:=0; i < code.npc; i++) sys->print("%d: %d\n", i, int code.ops[i]);
+	s := pstmt(pp, 0, code.npc, indent);
+
+	if(pp.sp != 0)
+		fatal(ex, "pprint stack not balanced");
+
+	return s;
+}
+
+pstmt(pp: ref PPrint, pc, epc: int, indent: string): string
+{
+	e, e1, e2: string;
+	c, apc: int;
+
+	code := pp.code;
+	s := "";
+	while(pc < epc){
+		op := int code.ops[pc++];
+		while(op == Llabel){
+			(pc, c) = getconst(code.ops, pc);
+			s += code.strs[c] + ":\n";
+			op = int code.ops[pc++];
+		}
+		s += indent;
+		case op{
+		Lbreak or
+		Lcontinue or
+		Lreturn =>
+			s += tokname(op);
+			if(op == Lreturn){
+				(pc, e) = pexp(pp, pc, code.npc);
+				s += " " + e;
+			}
+			s += ";\n";
+		Lbreaklab or
+		Lcontinuelab =>
+			s += tokname(op);
+			(pc, c) = getconst(code.ops, pc);
+			s += " " + code.strs[c] + ";\n";
+		'{' =>
+			(pc, apc) = getjmp(code.ops, pc);
+			s += "{\n" + pstmt(pp, pc, apc, indent+"	") + indent + "}\n";
+			pc = apc;
+		Lif or
+		Lwith or
+		Lwhile =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, e) = pexp(pp, pc, apc);
+			(pc, apc) = getjmp(code.ops, pc);
+			s += tokname(op) + "(" + e + "){\n";
+			s += pstmt(pp, pc, apc, indent+"	");
+			if(op == Lif){
+				(pc, apc) = getjmp(code.ops, apc);
+				if(pc != apc)
+					s += indent + "}else{\n";
+				s += pstmt(pp, pc, apc, indent+"	");
+			}
+			s += indent + "}\n";
+			pc = apc;
+		Ldo =>
+			(pc, apc) = getjmp(code.ops, pc);
+			e = pstmt(pp, pc, apc, indent+"	");
+			(pc, apc) = getjmp(code.ops, apc);
+			(pc, e1) = pexp(pp, pc, apc);
+			s += "do{\n" + e + indent + "}(while(" + e1 + ");\n";
+			pc = apc;
+		Lfor or
+		Lforvar or
+		Lforin or
+		Lforvarin =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, e) = pexp(pp, pc, apc);
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, e1) = pexp(pp, pc, apc);
+			s += "for(";
+			if(op == Lforvar || op == Lforvarin)
+				s += "var ";
+			s += e;
+			if(op == Lfor || op == Lforvar){
+				(pc, apc) = getjmp(code.ops, pc);
+				(pc, e2) = pexp(pp, pc, apc);
+				s += "; " + e1 + "; " + e2;
+			}else
+				s += " in " + e1;
+			s += "){\n";
+			(pc, apc) = getjmp(code.ops, pc);
+			s += pstmt(pp, pc, apc, indent+"	");
+			s += indent + "}\n";
+			pc = apc;
+		';' =>
+			s += ";\n";
+		Lvar =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, e) = pexp(pp, pc, apc);
+			s += "var " + e + ";\n";
+		Lswitch =>
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, e) = pexp(pp, pc, apc);
+			s += "switch (" + e + ") {\n";
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, e) = pcaseblk(pp, pc, apc, indent);
+			s  += e + indent + "}\n";
+			pc = apc;
+		Lthrow =>
+			(pc, e) = pexp(pp, pc, code.npc);
+			s += "throw " + e + "\n";
+		Ltry =>
+			s += "try\n";
+			(pc, apc) = getjmp(code.ops, pc);
+			s += pstmt(pp, pc, apc, indent+"	");
+			(pc, apc) = getjmp(code.ops, apc);
+			if(pc != apc){
+				(pc, c) = getconst(code.ops, ++pc);
+				s += "catch(" + code.strs[c] + ")\n";
+				s += pstmt(pp, pc, apc, indent+"	");
+			}
+			(pc, apc) = getjmp(code.ops, apc);
+			if(pc != apc){
+				s += "finally\n";
+				s += pstmt(pp, pc, apc, indent+"	");
+			}
+			pc = apc;
+		* =>
+			(pc, e) = pexp(pp, pc-1, code.npc);
+			s += e + ";\n";
+		}
+	}
+	return s;
+}
+
+pexp(pp: ref PPrint, pc, epc: int): (int, string)
+{
+	c, apc: int;
+	s, f, a, a1, a2: string;
+
+	code := pp.code;
+	savesp := pp.sp;
+out:	while(pc < epc){
+		case op := int code.ops[pc++]{
+		Lthis =>
+			s = "this";
+		Lid or
+		Lnum or
+		Lstr or
+		Lregexp =>
+			(pc, c) = getconst(code.ops, pc);
+			if(op == Lnum)
+				s = string code.nums[c];
+			else{
+				s = code.strs[c];
+				if(op == Lstr)
+					s = "\""+escstr(code.strs[c])+"\"";
+			}
+		'*' or
+		'/' or
+		'%' or
+		'+' or
+		'-' or
+		Llsh or
+		Lrsh or
+		Lrshu or
+		'<' or
+		'>' or
+		Lleq or
+		Lgeq or
+		Lin or
+		Linstanceof or
+		Leq or
+		Lneq or
+		Lseq or
+		Lsne or
+		'&' or
+		'^' or
+		'|' or
+		'=' or
+		'.' or
+		',' or
+		'[' =>
+			a2 = ppop(pp);
+			a1 = ppop(pp);
+			s = tokname(op);
+			if(a1[0] == '='){
+				s += "=";
+				a1 = a1[1:];
+			}
+			if(op == '[')
+				s = a1 + "[" + a2 + "]";
+			else{
+				if(op != '.'){
+					if(op != ',')
+						s = " " + s;
+					s = s + " ";
+				}
+				s = a1 + s + a2;
+			}
+		Ltypeof or
+		Ldelete or
+		Lvoid or
+		Lnew or
+		Linc or
+		Ldec or
+		Lpreadd or
+		Lpresub or
+		'~' or
+		'!' or
+		Lpostinc or
+		Lpostdec =>
+			a = ppop(pp);
+			s = tokname(op);
+			if(op == Lpostinc || op == Lpostdec)
+				s = a + s;
+			else{
+				if(op == Ltypeof || op == Ldelete || op == Lvoid || op == Lnew)
+					s += " ";
+				s += a;
+			}
+		'(' =>
+			s = "(";
+		')' =>
+			s = ppop(pp);
+			if(ppop(pp) != "(")
+				fatal(pp.ex, "unbalanced () in pexp");
+			s = "(" + s + ")";
+		Lgetval or
+		Las =>
+			continue;
+		Lasop =>
+			s = "=" + ppop(pp);
+		Lcall or
+		Lnewcall =>
+			(pc, c) = getconst(code.ops, pc);
+			a = "";
+			sep := "";
+			for(sp := pp.sp-c; sp < pp.sp; sp++){
+				a += sep + pp.stack[sp];
+				sep = ", ";
+			}
+			pp.sp -= c;
+			f = ppop(pp);
+			if(op == Lnewcall)
+				f = "new " + f;
+			s = f + "(" + a + ")";
+		';' =>
+			break out;
+		Landand or
+		Loror or
+		'?' =>
+			s = ppop(pp);
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, a1) = pexp(pp, pc, apc);
+			s += " " + tokname(op) + " " + a1;
+			if(op == '?'){
+				(pc, apc) = getjmp(code.ops, pc);
+				(pc, a2) = pexp(pp, pc, apc);
+				s += " : "+ a2;
+			}
+		* =>
+			fatal(pp.ex, "pexp: unknown op " + tokname(op));
+		}
+		ppush(pp, s);
+	}
+
+	if(savesp == pp.sp)
+		return (pc, "");
+
+	if(savesp != pp.sp-1)
+		fatal(pp.ex, "unbalanced stack in pexp");
+	return (pc, ppop(pp));
+}
+
+pcaseblk(pp: ref PPrint, pc, epc: int, indent: string): (int, string)
+{
+	code := pp.code;
+	defpc, clausepc, nextpc, apc: int;
+	s, a: string;
+
+	(pc, defpc) = getjmp(code.ops, pc);
+	clausepc = pc;
+	(pc, nextpc) = getjmp(code.ops, pc);
+	for (; pc < epc; (clausepc, (pc, nextpc)) = (nextpc, getjmp(code.ops, nextpc))) {
+		if (clausepc == defpc) {
+			s += indent + "default:\n";
+		} else {
+			(pc, apc) = getjmp(code.ops, pc);
+			(pc, a) = pexp(pp, pc, apc);
+			s += indent + "case " + a + ":\n";
+		}
+		s += pstmt(pp, pc, nextpc, indent+"\t");
+	}
+	return (epc, s);
+}
+
+ppush(pp: ref PPrint, s: string)
+{
+	if(pp.sp >= len pp.stack){
+		st := array[2 * len pp.stack] of string;
+		st[:] = pp.stack;
+		pp.stack = st;
+	}
+	pp.stack[pp.sp++] = s;
+}
+
+ppop(pp: ref PPrint): string
+{
+	if(pp.sp == 0)
+		fatal(pp.ex, "popping too far off the pstack");
+	return pp.stack[--pp.sp];
+}
+
+unescmap :=	array[128] of 
+{
+	'\'' =>		byte '\'',
+	'"' =>		byte '"',
+	'\\' =>		byte '\\',
+	'\b' =>		byte 'b',
+	'\u000c' =>	byte 'f',
+	'\n' =>		byte 'n',
+	'\r' =>		byte 'r',
+	'\t' =>		byte 't',
+
+	* =>		byte 0
+};
+
+escstr(s: string): string
+{
+	n := len s;
+	sb := "";
+	for(i := 0; i < n; i++){
+		c := s[i];
+		if(c < 128 && (e := int unescmap[c])){
+			sb[len sb] = '\\';
+			sb[len sb] = e;
+		}else if(c > 128 || c < 32){
+			sb += "\\u0000";
+			for(j := 1; j <= 4; j++){
+				sb[len sb - j] = "0123456789abcdef"[c & 16rf];
+				c >>= 4;
+			}
+		}else
+			sb[len sb] = c;
+	}
+	return sb;
+}
--- /dev/null
+++ b/appl/lib/ecmascript/regexp.b
@@ -1,0 +1,1286 @@
+strhas(s: string, c: int): ref Val
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return true;
+	return false;
+}
+
+rsplit(r: string): (string, string)
+{
+	esc := 0;
+	i := 1;	# skip '/'
+	for(;;){
+		c := r[i++];
+		if(!esc && c == '/')
+			break;
+		esc = !esc && c == '\\';
+	}
+	return (r[1: i-1], r[i: ]);
+}
+		
+badflags(f: string): int
+{
+	g := i := m := 0;
+	for(j := 0; j < len f; j++){
+		case(f[j]){
+			'g' =>
+				g++;
+			'i' =>
+				i++;
+			'm' =>
+				m++;
+			* =>
+				return 1;
+		}
+	}
+	return g > 1 || i > 1 || m > 1;
+}
+
+regexpvals(ex: ref Exec, v: ref Val, o: ref Ecmascript->Obj): (string, string, int)
+{
+	if(v != nil){
+		if(v.ty == TRegExp)
+			return (v.rev.p, v.rev.f, v.rev.i);
+		o = v.obj;
+	}
+	p := toString(ex, esget(ex, o, "source", 0));
+	f := "";
+	if(toBoolean(ex, esget(ex, o, "global", 0)) == true)
+		f += "g";
+	if(toBoolean(ex, esget(ex, o, "ignoreCase", 0)) == true)
+		f += "i";
+	if(toBoolean(ex, esget(ex, o, "multiline", 0)) == true)
+		f += "m";
+	i := toInt32(ex, esget(ex, o, "lastIndex", 0));
+	return (p, f, i);
+}
+
+nregexp(ex: ref Exec, nil: ref Ecmascript->Obj, args: array of ref Val): ref Ecmascript->Obj
+{
+	pat := biarg(args, 0);
+	flags := biarg(args, 1);
+	(p, f) := ("", "");
+	if(isregexp(pat)){
+		if(flags == undefined)
+			(p, f, nil) = regexpvals(ex, pat, nil);
+		else
+			runtime(ex, TypeError, "flags defined");
+	}
+	else{
+		if(pat == undefined)
+			p = "";
+		else
+			p = toString(ex, pat);
+		if(flags == undefined)
+			f = "";
+		else
+			f = toString(ex, flags);
+	}
+	o := nobj(ex, nil, array[] of { regexpval(p, f, 0) });
+	if(badflags(f))
+		runtime(ex, SyntaxError, "bad regexp flags");
+	regex = ex;
+	(re, err) := compile(p, 1);
+	if(re == nil || err != nil)
+		runtime(ex, SyntaxError, "bad regexp pattern");
+	o.re = re;
+	return o;
+}
+
+cregexp(ex: ref Exec, f, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	pat := biarg(args, 0);
+	flags := biarg(args, 1);
+	if(isregexp(pat) && flags == undefined)
+		return pat;
+	return objval(nregexp(ex, f, args));
+}
+
+cregexpprotoexec(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	m: array of (int, int);
+
+	regexpcheck(ex, this, f);
+	s := toString(ex, biarg(args, 0));
+	l := len s;
+	i := toInt32(ex, esget(ex, this, "lastIndex", 0));
+	e := 0;
+	glob := esget(ex, this, "global", 0);
+	multiline := esget(ex, this, "multiline", 0);
+	ignorecase := esget(ex, this, "ignoreCase", 0);
+	if(glob == false)
+		i = 0;
+	for(;;){
+		if(i < 0 || i >= l){
+			esput(ex, this, "lastIndex", numval(real 0), 0);
+			return null;
+		}
+		regex = ex;
+		m = executese(this.re, s, (i, len s), i == 0, 1, multiline == true, ignorecase == true);
+		if(m != nil)
+			break;
+		i++;
+		i = -1;	# no need to loop with executese
+	}
+	(i, e) = m[0];
+	if(glob == true)
+		esput(ex, this, "lastIndex", numval(real e), 0);
+	n := len m;
+	av := array[n] of ref Val;
+	for(j := 0; j < n; j++){
+		(a, b) := m[j];
+		if(a < 0)
+			av[j] = undefined;
+		else
+			av[j] = strval(s[a: b]);
+	}
+	a := narray(ex, nil, av);
+	esput(ex, a, "index", numval(real i), 0);
+	esput(ex, a, "input", strval(s), 0);
+	return objval(a);
+}
+
+cregexpprototest(ex: ref Exec, f, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	regexpcheck(ex, this, f);
+	v := cregexpprotoexec(ex, f, this, args);
+	if(!isnull(v))
+		return true;
+	return false;
+}
+
+cregexpprototoString(ex: ref Exec, f, this: ref Ecmascript->Obj, nil: array of ref Val): ref Val
+{
+	regexpcheck(ex, this, f);
+	(p, fl, nil) := regexpvals(ex, nil, this);
+	return strval("/" + p + "/" + fl);
+}
+
+regexpcheck(ex: ref Exec, o: ref Ecmascript->Obj, f: ref Obj)
+{
+	if(f == nil)
+		s := "exec";
+	else
+		s = f.val.str;
+	if(!isregexpobj(o))
+		runtime(ex, TypeError, "RegExp.prototype." + s + " called on non-RegExp object");
+}
+
+cstrprotomatch(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	v := biarg(args, 0);
+	if(!isregexp(v))
+		re := nregexp(ex, nil, args);
+	else if(v.ty == TObj)
+		re = v.obj;
+	else
+		re = nobj(ex, nil, args);
+	s := toString(ex, this.val);
+	glob := esget(ex, re, "global", 0);
+	av := array[1] of ref Val;
+	av[0] = strval(s);
+	if(glob == false)
+		return cregexpprotoexec(ex, nil, re, av);
+	li := 0;
+	esput(ex, re, "lastIndex", numval(real li), 0);
+	ms: list of ref Val;
+	for(;;){
+		v = cregexpprotoexec(ex, nil, re, av);
+		if(isnull(v))
+			break;
+		ms = esget(ex, v.obj, "0", 0) :: ms;
+		ni := int toUint32(ex, esget(ex, re, "lastIndex", 0));
+		if(ni == li)
+			esput(ex, re, "lastIndex", numval(real ++li), 0);
+		else
+			li = ni;
+	}	
+	n := len ms;
+	av = array[n] of ref Val;
+	for(j := n-1; j >= 0; j--){
+		av[j] = hd ms;
+		ms = tl ms;
+	}
+	return objval(narray(ex, nil, av));
+}
+
+cstrprotoreplace(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	re: ref Ecmascript->Obj;
+
+	v := biarg(args, 0);
+	rege := isregexp(v);
+	if(!rege){
+		if(args == nil)
+			re = nregexp(ex, nil, args);
+		else
+			re = nregexp(ex, nil, args[0:1]);
+	}
+	else if(v.ty == TObj)
+		re = v.obj;
+	else
+		re = nobj(ex, nil, args);
+	s := toString(ex, this.val);
+	if(rege)
+		glob := esget(ex, re, "global", 0);
+	else
+		glob = false;
+	av := array[1] of ref Val;
+	av[0] = strval(s);
+	ms: list of ref Val;
+	li := 0;
+	if(glob == true)
+		esput(ex, re, "lastIndex", numval(real li), 0);
+	for(;;){
+		v = cregexpprotoexec(ex, nil, re, av);
+		if(!isnull(v))
+			ms = v :: ms;
+		if(isnull(v) || glob == false)
+			break;
+		ni := int toUint32(ex, esget(ex, re, "lastIndex", 0));
+		if(ni == li)
+			esput(ex, re, "lastIndex", numval(real ++li), 0);
+		else
+			li = ni;
+	}
+	if(ms == nil)
+		return strval(s);
+	ms = rev(ms);
+	if(rege)
+		lcp := int toUint32(ex, esget(ex, (hd ms).obj, "length", 0))-1;
+	else
+		lcp = 0;
+	v = biarg(args, 1);
+	if(isobj(v) && isfuncobj(v.obj)){
+		ns := s;
+		n := len ms;
+		args = array[lcp+3] of ref Val;
+		o := inc := 0;
+		for(i := 0; i < n; i++){
+			a := (hd ms).obj;
+			ms = tl ms;
+			for(j := 0; j <= lcp; j++)
+				args[j] = esget(ex, a, string j, 0);
+			ss := toString(ex, args[0]);
+			o = offset(ss, s, o);
+			args[lcp+1] = numval(real o);
+			args[lcp+2] = strval(s);
+			rs := toString(ex, getValue(ex, escall(ex, v.obj, nil, args, 0)));
+			ns = repl(ns, o+inc, o+inc+len ss, rs);
+			o += len ss;
+			inc += len rs - len ss;
+		}
+		return strval(ns);
+	}
+	else{
+		ps := toString(ex, v);
+		lps := len ps;
+		ns := s;
+		n := len ms;
+		o := inc := 0;
+		for(i := 0; i < n; i++){
+			a := (hd ms).obj;
+			ms = tl ms;
+			ss := toString(ex, esget(ex, a, "0", 0));
+			o = offset(ss, s, o);
+			rs := "";
+			for(j := 0; j < lps; j++){
+				if(ps[j] == '$' && j < lps-1){
+					j++;
+					case(c := ps[j]){
+						'$' =>
+							rs += "$";
+						'&' =>
+							rs += ss;
+						'`' =>
+							rs += s[0: o];
+						''' =>
+							rs += s[o+len ss: ];
+						'0' to '9' =>
+							if(j < lps-1 && isdigit(ps[j+1]))
+								c = 10*(c-'0')+ps[++j]-'0';
+							else
+								c = c-'0';
+							if(c >= 1 && c <= lcp)
+								rs += toString(ex, esget(ex, a, string c, 0));
+					}
+				}
+				else
+					rs += ps[j: j+1];
+			}
+			ns = repl(ns, o+inc, o+inc+len ss, rs);
+			o += len ss;
+			inc += len rs - len ss;
+		}
+		return strval(ns);
+	}
+}
+
+cstrprotosearch(ex: ref Exec, nil, this: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	v := biarg(args, 0);
+	if(!isregexp(v))
+		re := nregexp(ex, nil, args);
+	else if(v.ty == TObj)
+		re = v.obj;
+	else
+		re = nobj(ex, nil, args);
+	s := toString(ex, this.val);
+	glob := esget(ex, re, "global", 0);
+	esput(ex, re, "global", false, 0);
+	av := array[1] of ref Val;
+	av[0] = strval(s);
+	v = cregexpprotoexec(ex, nil, re, av);
+	if(isnull(v))
+		r := -1;
+	else{
+		ss := toString(ex, esget(ex, v.obj, "0", 0));
+		r = offset(ss, s, 0);
+	}
+	esput(ex, re, "global", glob, 0);
+	return numval(real r);
+}
+
+offset(ss: string, s: string, m: int): int
+{
+	nn := len ss;
+	n := len s;
+	for(i := m; i <= n-nn; i++){
+		if(s[i: i+nn] == ss)
+			return i;
+	}
+	return -1;
+}
+
+repl(s: string, a: int, b: int, ns: string): string
+{
+	return s[0: a] + ns + s[b: ];
+}
+
+rev(ls: list of ref Val): list of ref Val
+{
+	ns: list of ref Val;
+
+	for( ; ls != nil; ls = tl ls)
+		ns = hd ls :: ns;
+	return ns;
+}
+
+#########################################################################
+# regex.b originally
+
+# normally imported identifiers
+
+# internal identifiers, not normally imported
+
+ALT, CAT, DOT, SET, HAT, DOL, NUL, PCLO, CLO, OPT, LPN, RPN, LPN0, RPN0, LPN1, RPN1, LPN2, RPN2, BEET, BEEF, MNCLO, LCP, IDLE: con (1<<16)+iota;
+
+# syntax
+
+# RE	ALT		regular expression
+#	NUL
+# ALT	CAT		alternation
+# 	CAT | ALT
+#
+# CAT	DUP		catenation
+# 	DUP CAT
+#
+# DUP	PRIM		possibly duplicated primary
+# 	PCLO
+# 	CLO
+# 	OPT
+#
+# PCLO	PRIM +		1 or more
+# CLO	PRIM *		0 or more
+# OPT	PRIM ?		0 or 1
+#
+# PRIM	( RE )
+#	()
+# 	DOT		any character
+# 	CHAR		a single character
+#	ESC		escape sequence
+# 	[ SET ]		character set
+# 	NUL		null string
+# 	HAT		beginning of string
+# 	DOL		end of string
+#
+
+regex: ref Exec;
+
+NIL : con -1;		# a refRex constant
+NONE: con -2;		# ditto, for an un-set value
+BAD: con 1<<16;		# a non-character 
+HUGE: con (1<<31) - 1;
+
+# the data structures of re.m would like to be ref-linked, but are
+# circular (see fn walk), thus instead of pointers we use indexes
+# into an array (arena) of nodes of the syntax tree of a regular expression.
+# from a storage-allocation standpoint, this replaces many small
+# allocations of one size with one big one of variable size.
+
+ReStr: adt {
+	s : string;
+	i : int;	# cursor postion
+	n : int;	# number of chars left; -1 on error
+	peek : fn(s: self ref ReStr): int;
+	next : fn(s: self ref ReStr): int;
+	unput: fn(s: self ref ReStr);
+};
+
+ReStr.peek(s: self ref ReStr): int
+{
+	if(s.n <= 0)
+		return BAD;
+	return s.s[s.i];
+}
+
+ReStr.next(s: self ref ReStr): int
+{
+	if(s.n <= 0)
+		syntax("bad regular expression");
+	s.n--;
+	return s.s[s.i++];
+}
+
+ReStr.unput(s: self ref ReStr)
+{
+	s.n++;
+	s.i--;
+}
+
+newRe(kind: int, left, right: refRex, set: ref Set, ar: ref Arena, pno: int, greedy: int): refRex
+{
+	ar.rex[ar.ptr] = Rex(kind, left, right, set, pno, greedy, nil);
+	return ar.ptr++;
+}
+
+# parse a regex by recursive descent to get a syntax tree
+
+re(s: ref ReStr, ar: ref Arena): refRex
+{
+	left := cat(s, ar);
+	if(left==NIL || s.peek()!='|')
+		return left;
+	s.next();
+	right := re(s, ar);
+	if(right == NIL)
+		return NIL;
+	return newRe(ALT, left, right, nil, ar, 0, 0);
+}
+
+cat(s: ref ReStr, ar: ref Arena): refRex
+{
+	left := dup(s, ar);
+	if(left == NIL)
+		return left;
+	right := cat(s, ar);
+	if(right == NIL)
+		return left;
+	return newRe(CAT, left, right, nil, ar, 0, 0);
+}
+
+dup(s: ref ReStr, ar: ref Arena): refRex
+{
+	n1, n2: int;
+
+	case s.peek() {
+	BAD or ')' or ']' or '|' or '?' or '*' or '+' =>
+		return NIL;
+	}
+	prim: refRex;
+	case kind:=s.next() {
+	'(' =>	if(ar.pno < 0) {
+			if(s.peek() == ')') {
+				s.next();
+				prim = newRe(NUL, NONE, NONE, nil, ar, 0, 0);
+			} else {
+				prim = re(s, ar);
+				if(prim==NIL || s.next()!=')')
+					syntax("( with no )");
+			}
+		} else {
+			pno := ++ar.pno;
+			lp := newRe(LPN, NONE, NONE, nil, ar, pno, 0);
+			rp := newRe(RPN, NONE, NONE, nil, ar, pno, 0);
+			if(s.peek() == ')') {
+				s.next();
+				prim = newRe(CAT, lp, rp, nil, ar, 0, 0);
+			} else {
+				if(s.peek() == '?'){
+					s.next();
+					case s.next(){
+						':' => ar.rex[lp].kind = LPN0;
+							ar.rex[rp].kind = RPN0;
+						'=' => ar.rex[lp].kind = LPN1;
+							ar.rex[rp].kind = RPN1;
+						'!' => ar.rex[lp].kind = LPN2;
+							ar.rex[rp].kind = RPN2;
+						* => syntax("bad char after ?");
+					}
+				}
+				prim = re(s, ar);
+				if(prim==NIL || s.next()!=')')
+					syntax("( with no )");
+				else {
+					prim = newRe(CAT, prim, rp, nil, ar, 0, 0);
+					prim = newRe(CAT, lp, prim, nil, ar, 0, 0);
+				}
+			}
+		}
+	'[' =>	prim = newRe(SET, NONE, NONE, newSet(s), ar, 0, 0);
+	* =>	case kind {
+		'.' =>		kind = DOT;
+		'^' =>	kind = HAT;
+		'$' =>	kind = DOL;
+		}
+		(c, set, op) := esc(s, kind, 0);
+		if(set != nil)
+			prim = newRe(SET, NONE, NONE, set, ar, 0, 0);
+		else if(op == LCP){
+			if(c > ar.pno)
+				syntax("\num too big");
+			prim = newRe(LCP, NONE, NONE, nil, ar, 0, 0);
+			ar.rex[prim].ns = ref Nstate(c, c);
+		}
+		else
+			prim = newRe(c, NONE, NONE, nil, ar, 0, 0);
+	}
+	case s.peek() {
+	'*' =>	kind = CLO;
+	'+' =>	kind = PCLO;
+	'?' =>	kind = OPT;
+	'{' =>	s.next();
+		(n1, n2) = drange(s);
+		kind = MNCLO;
+		if(s.peek() != '}')
+			syntax("{ with no }");
+	* =>	return prim;
+	}
+	s.next();
+	greedy := 1;
+	if(s.peek() == '?'){
+		# non-greedy op
+		greedy = 0;
+		s.next();
+	}
+	prim = newRe(kind, prim, NONE, nil, ar, 0, greedy);
+	if(kind == MNCLO)
+		ns := ar.rex[prim].ns = ref Nstate(n1, n2);
+	return prim;
+}
+
+esc(s: ref ReStr, char: int, inset: int): (int, ref Set, int)
+{
+	set: ref Set;
+
+	op := 0;
+	if(char == '\\') {
+		char = s.next();
+		case char {
+		'b' =>
+				if(inset)
+					char = '\b';
+				else
+					char = BEET;
+		'B' =>	if(inset)
+					syntax("\\B in set");
+				else
+					char = BEEF;
+		'f' =>		char = '\u000c';
+		'n' =>	char = '\n';
+		'r' =>		char = '\r';
+		't' =>		char = '\t';
+		'v' =>	char = '\v';
+		'0' to '9' =>
+				s.unput();
+				char = digits(s);
+				if(char == 0)
+					char = '\0';
+				else if(inset)
+					syntax("\num in set");
+				else
+					op = LCP;
+		'x' =>	char = hexdigits(s, 2);
+		'u' =>	char = hexdigits(s, 4);
+		'c' =>	char = s.next()%32;
+		'd' or 'D' =>
+				set = newset('0', '9');
+				if(char == 'D')
+					set.neg = 1;
+		's' or 'S' =>
+				set = newset(' ', ' ');
+				addsets(set, "\t\v\u000c\u00a0\n\r\u2028\u2029");
+				if(char == 'S')
+					set.neg = 1;
+		'w' or 'W' =>
+				set = newset('0', '9');
+				addset(set, 'a', 'z');
+				addset(set, 'A', 'Z');
+				addset(set, '_', '_');
+				if(char == 'W')
+					set.neg = 1;
+		* =>
+				;
+		}
+	}
+	if(char == -1){
+		if(inset)
+			syntax("bad set");
+		else
+			syntax("bad character");
+	}
+	return (char, set, op);
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+islower(c: int): int
+{
+	return c >= 'a' && c <= 'z';
+}
+
+isupper(c: int): int
+{
+	return c >= 'A' && c <= 'Z';
+}
+
+isalpha(c: int): int
+{
+	return islower(c) || isupper(c);
+}
+
+hexdigit(c: int): int
+{
+	if(isdigit(c))
+		return c-'0';
+	if('a' <= c && c <= 'f')
+		return c-'a'+10;
+	if('A' <= c && c <= 'F')
+		return c-'A'+10;
+	return -1;
+}
+
+digits(s: ref ReStr): int
+{
+	n := 0;
+	while(isdigit(s.peek()))
+		n = 10*n + s.next() -'0';
+	return n;
+}
+
+hexdigits(s: ref ReStr, n: int): int
+{
+	x := 0;
+	for(i := 0; i < n; i++){
+		v := hexdigit(s.next());
+		if(v < 0)
+			return -1;
+		x = 16*x+v;
+	}
+	return x;
+}
+
+drange(s: ref ReStr): (int, int)
+{
+	n1 := n2 := -1;
+	if(isdigit(s.peek()))
+		n1 = digits(s);
+	if(s.peek() == ','){
+		s.next();
+		if(isdigit(s.peek()))
+			n2 = digits(s);
+		else
+			n2 = HUGE;
+	}
+	else
+		n2 = n1;
+	if(n1 < 0 || n1 > n2)
+		syntax("bad number range");
+	return (n1, n2);
+}
+
+# walk the tree adjusting pointers to refer to 
+# next state of the finite state machine
+
+walk(r: refRex, succ: refRex, ar: ref Arena)
+{
+	if(r==NONE)
+		return;
+	rex := ar.rex[r];
+	case rex.kind {
+	ALT =>	walk(rex.left, succ, ar);
+		walk(rex.right, succ, ar);
+		return;
+	CAT =>	walk(rex.left, rex.right, ar);
+		walk(rex.right, succ, ar);
+		ar.rex[r] = ar.rex[rex.left];	# optimization
+		return;
+	CLO or PCLO =>
+		end := newRe(OPT, r, succ, nil, ar, 0, rex.greedy); # here's the circularity
+		walk(rex.left, end, ar);
+	OPT =>	walk(rex.left, succ, ar);
+	MNCLO =>
+		ar.ptr++;
+		walk(rex.left, r, ar);
+	LCP =>
+		ar.rex[r].left = newRe(IDLE, NONE, succ, nil, ar, 0, 0);
+	}
+	ar.rex[r].right = succ;
+}
+
+prtree(r: refRex, ar: ref Arena, done: list of int, ind: string): list of int
+{
+	sys->print("%s", ind);
+	if(r==NIL){
+		sys->print("NIL\n");
+		return done;
+	}
+	if(r==NONE){
+		sys->print("NONE\n");
+		return done;
+	}
+	printed := 0;
+	for(li := done; li != nil; li = tl li){
+		if(hd li == r){
+			printed = 1;
+			break;
+		}
+	}
+	rex := ar.rex[r];
+	op := "";
+	z := "Z";
+	case rex.kind{
+		ALT => op = "|";
+		CAT => op = "and";
+		DOT => op = ".";
+		SET => op = "[]";
+		HAT => op = "^";
+		DOL => op = "$";
+		NUL => op = "NUL";
+		PCLO => op = "+";
+		CLO => op = "*";
+		OPT => op = "?";
+		LPN => op = "(";
+		RPN => op = ")";
+		LPN0 => op = "?:";
+		RPN0 => op = ":?";
+		LPN1 => op = "?=";
+		RPN1 => op = "=?";
+		LPN2 => op = "?!";
+		RPN2 => op = "!?";
+		BEET => op = "\\b";
+		BEEF => op = "\\B";
+		MNCLO => op = "{}";
+		LCP => op = "n";
+		IDLE => op = "i";
+		* => z[0] = rex.kind; op = z;
+	}
+	if(printed){
+		sys->print("node %d (%d)\n", r, r);
+		return done;
+	}
+	else{
+		if(rex.ns != nil)
+			sys->print("%s [%d-%d] (%d)\n", op, rex.ns.m, rex.ns.n, r);
+		else
+			sys->print("%s (%d)\n", op, r);
+		done = r :: done;
+		ind += "  ";
+		done = prtree(rex.left, ar, done, ind);
+		done  = prtree(rex.right, ar, done, ind);
+		return done;
+	}
+}
+
+compile(e: string, flag: int): (Re, string)
+{
+	if(e == nil)
+		return (nil, "missing expression");	
+	s := ref ReStr(e, 0, len e);
+	ar := ref Arena(array[2*s.n] of Rex, 0, 0, (flag&1)-1);
+	start := ar.start = re(s, ar);
+	if(start==NIL || s.n!=0)
+		syntax("invalid regular expression");
+	walk(start, NIL, ar);
+	# prtree(start, ar, nil, "");
+	if(ar.pno < 0)
+		ar.pno = 0;
+	return (ar, nil);
+}
+
+# todo: queue for epsilon and advancing transitions
+
+Num: adt{
+	ns: ref Nstate;
+	m: int;
+	n: int;
+};
+Gaz: adt {
+	pno: int;
+	beg: int;
+	end: int;
+};
+Trace: adt {
+	cre: refRex;		# cursor in Re
+	trans: int;		# 0 epsilon transition, 1 advancing transition
+	beg: int;		# where this trace began;
+	end: int;		# where this trace ended if success (-1 by default)
+	gaz: list of Gaz;
+	ns: list of ref Num;
+};
+Queue: adt {
+	ptr: int;
+	q: array of Trace;
+};
+
+execute(re: Re, s: string): array of (int, int)
+{
+	return executese(re, s, (-1,-1), 1, 1, 1, 0);
+}
+
+executese(re: Re, s: string, range: (int, int), bol: int, eol: int, multiline: int, ignorecase: int): array of (int,int)
+{
+	if(re==nil)
+		return nil;
+	(s0, s1) := range;
+	if(s0 < 0)
+		s0 = 0;
+	if(s1 < 0)
+		s1 = len s;
+	match := 0;
+	todo := ref Queue(0, array[2*re.ptr] of Trace);
+	for(i:=s0; i<=s1; i++) {
+		if(!match)		# no leftmost match yet
+			todo.q[todo.ptr++] = Trace(re.start, 0, i, -1, nil, nil);
+		for(k:=0; k<todo.ptr; k++) {
+			q := todo.q[k];
+			if(q.trans)
+				continue;
+			rex := re.rex[q.cre];
+			next0 := next1 := next2 := NONE;
+			case rex.kind {
+			NUL =>
+				next1 = rex.right;
+			DOT =>
+				if(i<len s && !islt(s[i]))
+					next2 = rex.right;
+			HAT =>
+				if(i == s0 && bol)
+					next1 = rex.right;
+				else if(multiline && i > 0 && islt(s[i-1]))
+					next1 = rex.right;
+			DOL =>
+				if(i == s1 && eol)
+					next1 = rex.right;
+				else if(multiline && i < s1 && islt(s[i]))
+					next1 = rex.right;
+			SET =>
+				if(i<len s && member(s[i], rex.set, ignorecase))
+					next2 = rex.right;
+			CAT or
+			PCLO =>
+				next1 = rex.left;
+			ALT or 
+			CLO or 
+			OPT =>
+				if(rex.kind == ALT || rex.greedy){
+					next0 = rex.left;
+					next1 = rex.right;
+				}
+				else{
+					next0 = rex.right;
+					next1 = rex.left;
+				}
+			LPN =>
+				next1 = rex.right;
+				q.gaz = Gaz(rex.pno,i,-1)::q.gaz;
+			RPN =>
+				next1 = rex.right;
+				for(r:=q.gaz; ; r=tl r) {
+					(pno,beg1,end1) := hd r;
+					if(rex.pno==pno && end1==-1) {
+						q.gaz = Gaz(pno,beg1,i)::q.gaz;
+						break;
+					}
+				}
+			LPN0 or RPN0 or RPN1 or RPN2 =>
+				next1 = rex.right;
+			LPN1 =>
+				(rpn, nxt, nre) := storetree(q.cre, re);
+				m := executese(nre, s, (i, -1), bol, eol, multiline, ignorecase);
+				if(m != nil && m[0].t0 == i){
+					next1 = nxt;
+					for(j := 1; j < len m; j++)
+						if(m[j].t0 >= 0)
+							q.gaz = Gaz(j, m[j].t0, m[j].t1)::q.gaz;	
+				}
+				restoretree(LPN1, rpn, nxt, nre);
+			LPN2 =>
+				(rpn, nxt, nre) := storetree(q.cre, re);
+				m := executese(nre, s, (i, -1), bol, eol, multiline, ignorecase);
+				if(m == nil || m[0].t0 != i)
+					next1 = nxt;
+				restoretree(LPN2, rpn, nxt, nre);
+			MNCLO =>
+				num: ref Num;
+
+				(q.ns, num) = nextn(q.cre, q.ns, rex.ns.m, rex.ns.n, re);
+				if(num.m > 0)
+					next1 = rex.left;
+				else if(num.n > 0){
+					if(rex.greedy){
+						next0 = rex.left;
+						next1 = rex.right;
+					}
+					else{
+						next0 = rex.right;
+						next1 = rex.left;	
+					}
+				}
+				else{
+					next1 = rex.right;
+					(num.m, num.n) = (-1, -1);
+				}
+			LCP =>
+				pno := rex.ns.m;
+				(beg1, end1) := lcpar(q.gaz, pno);
+				l := end1-beg1;
+				if(beg1 < 0)	# undefined so succeeds
+					next1 = rex.right;
+				else if(i+l <= s1 && eqstr(s[beg1: end1], s[i: i+l], ignorecase)){
+					(q.ns, nil) = nextn(rex.left, q.ns, l, l, re);
+					next1 = rex.left;	# idle
+				}
+			IDLE =>
+				num: ref Num;
+
+				(q.ns, num) = nextn(q.cre, q.ns, -1, -1, re);
+				if(num.m >= 0)
+					next2 = q.cre;
+				else{
+					next1 = rex.right;
+					(num.m, num.n) = (-1, -1);
+				}
+			BEET =>
+				if(iswordc(s, i-1) != iswordc(s, i))
+					next1 = rex.right;
+			BEEF =>
+				if(iswordc(s, i-1) == iswordc(s, i))
+					next1 = rex.right;
+			* =>
+				if(i<len s && (rex.kind==s[i] || (ignorecase && eqcase(rex.kind, s[i]))))
+					next2 = rex.right;
+			}
+			l := k;
+			if(next0 != NONE) {
+				if(next0 != NIL)
+					(k, l) = insert(next0, 0, q.beg, -1, q.gaz, q.ns, todo, k, l);
+				else{
+					match = 1;
+					(k, l) = insert(NIL, 2, q.beg, i, q.gaz, nil, todo, k, l);
+				}
+			}
+			if(next1 != NONE) {
+				if(next1 != NIL)
+					(k, l) = insert(next1, 0, q.beg, -1, q.gaz, q.ns, todo, k, l);
+				else{
+					match = 1;
+					(k, l) = insert(NIL, 2, q.beg, i, q.gaz, nil, todo, k, l);
+				}
+			}
+			if(next2 != NONE) {
+				if(next2 != NIL)
+					(k, l) = insert(next2, 1, q.beg, -1, q.gaz, q.ns, todo, k, l);
+				else{
+					match = 1;
+					(k, l) = insert(NIL, 2, q.beg, i+1, q.gaz, nil, todo, k, l);
+				}
+			}
+		}
+		if(!atoe(todo) && match)
+			break;
+	}
+	if(todo.ptr == 0)
+		return nil;
+	if(todo.ptr > 1)
+		rfatal(sys->sprint("todo.ptr = %d", todo.ptr));
+	if(todo.q[0].trans != 2)
+		rfatal(sys->sprint("trans = %d", todo.q[0].trans));
+	if(todo.q[0].cre != NIL)
+		rfatal(sys->sprint("cre = %d", todo.q[0].cre));
+	beg := todo.q[0].beg;
+	end := todo.q[0].end;
+	gaz := todo.q[0].gaz;
+	if(beg == -1)
+		return nil;
+	result := array[re.pno+1] of { 0 => (beg,end), * => (-1,-1) };
+	for( ; gaz!=nil; gaz=tl gaz) {
+		(pno, beg1, end1) := hd gaz;
+		(rbeg, nil) := result[pno];
+		if(rbeg==-1 && (beg1|end1)!=-1)
+			result[pno] = (beg1,end1);
+	}
+	return result;
+}
+
+better(newbeg, newend, oldbeg, oldend: int): int
+{
+	return oldbeg==-1 || newbeg<oldbeg ||
+	       newbeg==oldbeg && newend>oldend;
+}
+
+insert(next: refRex, trans: int, tbeg: int, tend: int, tgaz: list of Gaz, tns: list of ref Num, todo: ref Queue, k: int, l: int): (int, int)
+{
+# sys->print("insert %d eps=%d beg=%d end=%d (k, l) = (%d %d) => ", next, trans, tbeg, tend, k, l);
+	for(j:=0; j<todo.ptr; j++){
+		if(todo.q[j].trans == trans){
+			if(todo.q[j].cre == next){
+				if(better(todo.q[j].beg, todo.q[j].end, tbeg, tend))
+					return (k, l);
+				else if(better(tbeg, tend, todo.q[j].beg, todo.q[j].end))
+					break;
+				else if(j < k)
+					return (k, l);
+				else
+					break;
+			}
+		}
+	}
+	if(j < k){
+		k--;
+		l--;
+	}
+	if(j < todo.ptr){
+		todo.q[j: ] = todo.q[j+1: todo.ptr];
+		todo.ptr--;
+	}
+	todo.q[l+2: ] = todo.q[l+1: todo.ptr];
+	todo.ptr++;
+	todo.q[l+1] = Trace(next, trans, tbeg, tend, tgaz, tns);
+# for(j=0; j < todo.ptr; j++) sys->print("%d(%d) ", todo.q[j].cre, todo.q[j].trans); sys->print("\n");
+	return (k, l+1);
+}
+
+# remove epsilon transitions and move advancing transitions to epsilon ones
+atoe(todo: ref Queue): int
+{
+	n := 0;
+	for(j := 0; j < todo.ptr; j++){
+		if(todo.q[j].trans){
+			if(todo.q[j].trans == 1){
+				todo.q[j].trans = 0;
+				n++;
+			}
+		}
+		else{
+			todo.q[j: ] = todo.q[j+1: todo.ptr];
+			todo.ptr--;
+			j--;
+		}
+	}
+	return n;
+}
+
+nextn(re: int, ln: list of ref Num, m: int, n: int, ar: ref Arena): (list of ref Num, ref Num)
+{
+	num: ref Num;
+
+	ns := ar.rex[re].ns;
+	for(l := ln; l != nil; l = tl l){
+		if((hd l).ns == ns){
+			num = hd l;
+			break;
+		}
+	}
+	if(num == nil)
+		ln = (num = ref Num(ns, -1, -1)) :: ln;
+	if(num.m == -1 && num.n == -1)
+		(num.m, num.n) = (m, n);
+	else
+		(nil, nil) = (--num.m, --num.n);
+	return (ln, num);
+}
+
+ASCII : con 128;
+WORD : con 32;
+
+mem(c: int, set: ref Set): int
+{
+	return (set.ascii[c/WORD]>>c%WORD)&1;
+}
+
+member(char: int, set: ref Set, ignorecase: int): int
+{
+	if(set.subset != nil){
+		for(l := set.subset; l != nil; l = tl l)
+			if(member(char, hd l, ignorecase))
+				return !set.neg;
+	}
+	if(char < 128){
+		if(ignorecase)
+			return (mem(tolower(char), set) || mem(toupper(char), set))^set.neg;
+		else
+			return ((set.ascii[char/WORD]>>char%WORD)&1)^set.neg;
+	}
+	for(l:=set.unicode; l!=nil; l=tl l) {
+		(beg, end) := hd l;
+		if(char>=beg && char<=end)
+			return !set.neg;
+	}
+	return set.neg;
+}
+
+newSet(s: ref ReStr): ref Set
+{
+	op: int;
+	set0: ref Set;
+
+	set := ref Set(0, array[ASCII/WORD] of {* => 0}, nil, nil);
+	if(s.peek() == '^') {
+		set.neg = 1;
+		s.next();
+	}
+	while(s.n > 0) {
+		char1 := s.next();
+		if(char1 == ']')
+			return set;
+		(char1, set0, op) = esc(s, char1, 1);
+		if(set0 != nil)
+			mergeset(set, set0);
+		char2 := char1;
+		if(s.peek() == '-') {
+			if(set0 != nil)
+				syntax("set in range");
+			s.next();
+			char2 = s.next();
+			if(char2 == ']')
+				break;
+			(char2, set0, op) = esc(s, char2, 1);
+			if(set0 != nil)
+				syntax("set in range");
+			if(char2 < char1)
+				break;
+		}
+		addset(set, char1, char2);
+	}
+	syntax("bad set");
+	return nil;
+}
+
+addset(set: ref Set, c1: int, c2: int)
+{
+	for(c := c1; c <= c2; c++){
+		if(c < ASCII)
+			set.ascii[c/WORD] |= 1<<c%WORD;
+		else{
+			set.unicode = (c, c2) :: set.unicode;
+			break;
+		}
+	}
+}
+
+addsets(set: ref Set, s: string)
+{
+	for(i := 0; i < len s; i++)
+		addset(set, s[i], s[i]);
+}
+
+mergeset(set: ref Set, set0: ref Set)
+{
+	if(!set0.neg){
+		for(i := 0; i < ASCII/WORD; i++)
+			set.ascii[i] |= set0.ascii[i];
+		for(l := set0.unicode; l != nil; l = tl l)
+			set.unicode = hd l :: set.unicode;
+	}
+	else
+		set.subset = set0 :: set.subset;
+}
+		
+newset(c1: int, c2: int): ref Set
+{
+	set := ref Set(0, array[ASCII/WORD] of {* => 0}, nil, nil);
+	addset(set, c1, c2);
+	return set;
+}
+
+storetree(lpn: int, re: ref Arena): (int, int, ref Arena)
+{
+	rpn: int;
+
+	rex := re.rex[lpn];
+	k := rex.kind;
+	l := 1;
+	for(;;){
+		rpn = rex.right;
+		rex = re.rex[rpn];
+		if(rex.kind == k)
+			l++;
+		else if(rex.kind == k+1 && --l == 0)
+			break;
+	}
+	re.rex[lpn].kind = LPN;
+	re.rex[rpn].kind = RPN;
+	nxt := re.rex[rpn].right;
+	re.rex[rpn].right = NIL;
+	nre := ref *re;
+	nre.start = lpn;
+	return (rpn, nxt, nre);
+}
+
+restoretree(lop: int, rpn: int, nxt: int, re: ref Arena)
+{
+	lpn := re.start;
+	re.rex[lpn].kind = lop;
+	re.rex[rpn].kind = lop+1;
+	re.rex[rpn].right = nxt;
+}
+
+iswordc(s: string, i: int): int
+{
+	if(i < 0 || i >= len s)
+		return 0;
+	c := s[i];
+	return isdigit(c) || isalpha(c) || c == '_';
+}
+
+lcpar(gaz: list of Gaz, pno: int): (int, int)
+{
+	for(r := gaz; r != nil; r = tl r) {
+		(pno1, beg1, end1) := hd r;
+		if(pno == pno1)
+			return (beg1, end1);
+	}
+	return (-1, -1);
+}
+
+eqstr(s: string, t: string, ic: int): int
+{
+	if(!ic)
+		return s == t;
+	if(len s != len t)
+		return 0;
+	for(i := 0; i < len s; i++)
+		if(!eqcase(s[i], t[i]))
+			return 0;
+	return 1;
+}
+
+eqcase(c1: int, c2: int): int
+{
+	return toupper(c1) == toupper(c2);
+}
+	
+syntax(s: string)
+{
+	runtime(regex, SyntaxError, s);
+}
+
+rfatal(s: string)
+{
+	runtime(regex, InternalError, s);
+}
--- /dev/null
+++ b/appl/lib/ecmascript/uri.b
@@ -1,0 +1,140 @@
+tohex(c: int): int
+{
+	if(c > 9)
+		return c-10+'A';
+	return c+'0';
+}
+
+fromhex(ex: ref Exec, c1: int, c2: int): int
+{
+	c1 = hexdigit(c1);
+	c2 = hexdigit(c2);
+	if(c1 < 0 || c2 < 0)
+		runtime(ex, URIError, "bad hex digit");
+	return 16*c1+c2;
+}
+
+isres(c: int): int
+{
+	return c == ';' || c == '/' || c == '?' || c == ':' || c == '@' || c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#';		# add '#' here for convenience
+}
+
+isunesc(c: int): int
+{
+	return isalpha(c) || isdigit(c) || c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' || c == ''' || c == '(' || c == ')';
+}
+
+encode(ex: ref Exec, s: string, flag: int): string
+{
+	m := len s;
+	r := "";
+	n := len r;
+	for(k := 0; k < m; k++){
+		c := s[k];
+		if(isunesc(c) || (flag && isres(c)))
+			r[n++] = c;
+		else{
+			if(c >= 16rdc00 && c <= 16rdfff)
+				runtime(ex, URIError, "char out of range");
+			if(c < 16rd800 || c > 16rdbff)
+				;
+			else{
+				if(++k == m)
+					runtime(ex, URIError, "char missing");
+				if(s[k] < 16rdc00 || s[k] > 16rdfff)
+					runtime(ex, URIError, "char out of range");
+				c = (c-16rd800)*16r400 + (s[k]-16rdc00) + 16r10000;
+			}
+			s1 := "Z";
+			s1[0] = c;
+			o := array of byte s1;
+			for(j := 0; j < len o; j++){
+				r += sys->sprint("%%%c%c", tohex(int o[j]/16), tohex(int o[j]%16));
+				n += 3;
+			}
+		}
+	}
+	return r;
+}
+
+decode(ex: ref Exec, s: string, flag: int): string
+{
+	m := len s;
+	r := "";
+	n := len r;
+	for(k := 0; k < m; k++){
+		c := s[k];
+		if(c != '%')
+			r[n++] = c;
+		else{
+			start := k;
+			if(k+2 >= m)
+				runtime(ex, URIError, "char missing");
+			c = fromhex(ex, s[k+1], s[k+2]);
+			k += 2;
+			if((c&16r80 == 0)){
+				if(flag && isres(c)){
+					r += s[start: k+1];
+					n += k+1-start;
+				}
+				else
+					r[n++] = c;
+			}
+			else{
+				for(i := 1; ((c<<i)&16r80) == 0; i++)
+					;
+				if(i == 1 || i > 4)
+					runtime(ex, URIError, "bad hex number");
+				o := array[i] of byte;
+				o[0] = byte c;
+				if(k+3*(n-1) >= m)
+					runtime(ex, URIError, "char missing");
+				for(j := 1; j < i; j++){
+					if(s[++k] != '%')
+						runtime(ex, URIError, "% missing");
+					c = fromhex(ex, s[k+1], s[k+2]);
+					k += 2;
+					if((c&16rc0) != 2)
+						runtime(ex, URIError, "bad hex number");
+					o[j] = byte c;
+				}
+				(c, nil, nil) = sys->byte2char(o, 0);
+				if(c < 16r10000){
+					if(flag && isres(c)){
+						r += s[start: k+1];
+						n += k+1-start;
+					}
+					else
+						r[n++] = c;
+				}
+				else if(c > 16r10ffff)
+					runtime(ex, URIError, "bad byte sequence");
+				else{
+					r[n++] = ((c-16r10000)&16r3ff)+16rdc00;
+					r[n++] = (((c-16r10000)>>10)&16r3ff)+16rd800;
+				}
+			}
+		}
+	}
+	return r;
+}
+
+cdecodeuri(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	return strval(decode(ex, toString(ex, biarg(args, 0)), 1));
+}
+
+cdecodeuric(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	return strval(decode(ex, toString(ex, biarg(args, 0)), 0));
+}
+
+cencodeuri(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	return strval(encode(ex, toString(ex, biarg(args, 0)), 1));
+}
+
+cencodeuric(ex: ref Exec, nil, nil: ref Ecmascript->Obj, args: array of ref Val): ref Val
+{
+	return strval(encode(ex, toString(ex, biarg(args, 0)), 0));
+}
--- /dev/null
+++ b/appl/lib/encoding/base16.b
@@ -1,0 +1,43 @@
+implement Encoding;
+
+include "encoding.m";
+
+hex: con "0123456789ABCDEF";
+
+enc(a: array of byte): string
+{
+	o: string;
+	for(i := 0; i < len a; i++){
+		n := int a[i];
+		o[len o] = hex[n>>4];
+		o[len o] = hex[n & 16rF];
+	}
+	return o;
+}
+
+dec(s: string): array of byte
+{
+	a := array[(len s+1)/2] of byte;	# upper bound
+	o := 0;
+	j := 0;
+	n := 0;
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		n <<= 4;
+		case c {
+		'0' to '9' =>
+			n |= c-'0';
+		'A' to 'F' =>
+			n |= c-'A'+10;
+		'a' to 'f' =>
+			n |= c-'a'+10;
+		* =>
+			continue;
+		}
+		if(++j == 2){
+			a[o++] = byte n;
+			j = n = 0;
+		}
+	}
+	return a[0:o];
+}
--- /dev/null
+++ b/appl/lib/encoding/base32.b
@@ -1,0 +1,60 @@
+implement Encoding;
+
+include "encoding.m";
+
+b32: con "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+enc(a: array of byte): string
+{
+	if(len a == 0)
+		return "========";
+	out := "";
+	nbit := len a * 8;
+	for(bit := 0; bit < nbit; bit += 5){
+		b := bit >> 3;
+		r := bit & 7;
+		v := int a[b] << r;
+		if(r > 3){
+			if(b+1 < len a)
+				v |= int (a[b+1] >> (8-r));
+		}
+		out[len out] = b32[(v>>3) & 16r1F];
+	}
+	while(len out & 7)
+		out[len out] = '=';	# RFC3548 says pad: we pad.
+	return out;
+}
+
+Naughty: con 255;
+
+t32d := array[256] of {
+	'a' => byte 0, 'b' => byte 1, 'c' => byte 2, 'd' => byte 3, 'e' => byte 4, 'f' => byte 5, 'g' => byte 6, 'h' => byte 7,
+	'i' => byte 8, 'j' => byte 9, 'k' => byte 10, 'l' => byte 11, 'm' => byte 12, 'n' => byte 13, 'o' => byte 14, 'p' => byte 15,
+	'q' => byte 16, 'r' => byte 17, 's' => byte 18, 't' => byte 19, 'u' => byte 20, 'v' => byte 21, 'w' => byte 22, 'x' => byte 23,
+	'y' => byte 24, 'z' => byte 25,
+	'A' => byte 0, 'B' => byte 1, 'C' => byte 2, 'D' => byte 3, 'E' => byte 4, 'F' => byte 5, 'G' => byte 6, 'H' => byte 7,
+	'I' => byte 8, 'J' => byte 9, 'K' => byte 10, 'L' => byte 11, 'M' => byte 12, 'N' => byte 13, 'O' => byte 14, 'P' => byte 15,
+	'Q' => byte 16, 'R' => byte 17, 'S' => byte 18, 'T' => byte 19, 'U' => byte 20, 'V' => byte 21, 'W' => byte 22, 'X' => byte 23,
+	'Y' => byte 24, 'Z' => byte 25,
+	'2' => byte 26, '3' => byte 27, '4' => byte 28, '5' => byte 29, '6' => byte 30, '7' => byte 31,
+	* => byte Naughty
+};
+
+dec(s: string): array of byte
+{
+	a := array[(8*len s + 4)/5] of byte;
+	o := 0;
+	v := 0;
+	j := 0;
+	for(i := 0; i < len s; i++){
+		if((c := s[i]) > 16rFF || (c = int t32d[c]) == Naughty)
+			continue;
+		v <<= 5;
+		v |= c;
+		if((j += 5) >= 8){
+			a[o++] = byte (v>>(j-8));
+			j -= 8;
+		}
+	}
+	return a[0:o];
+}
--- /dev/null
+++ b/appl/lib/encoding/base32a.b
@@ -1,0 +1,57 @@
+implement Encoding;
+
+include "encoding.m";
+
+b32: con "23456789abcdefghijkmnpqrstuvwxyz";
+
+enc(a: array of byte): string
+{
+	if(len a == 0)
+		return "========";
+	out := "";
+	nbit := len a * 8;
+	for(bit := 0; bit < nbit; bit += 5){
+		b := bit >> 3;
+		r := bit & 7;
+		v := int a[b] << r;
+		if(r > 3){
+			if(b+1 < len a)
+				v |= int (a[b+1] >> (8-r));
+		}
+		out[len out] = b32[(v>>3) & 16r1F];
+	}
+	# RFC3548 says pad with =; this follows alternative tradition (a)
+	return out;
+}
+
+INVAL: con 255;
+
+t32d := array[256] of {
+	'2' => byte 0, '3' => byte 1, '4' => byte 2, '5' => byte 3, '6' => byte 4, '7' => byte 5, '8' => byte 6, '9' => byte 7,
+	'a' => byte 8, 'b' => byte 9, 'c' => byte 10, 'd' => byte 11, 'e' => byte 12, 'f' => byte 13, 'g' => byte 14, 'h' => byte 15,
+	'i' => byte 16, 'j' => byte 17, 'k' => byte 18, 'm' => byte 19, 'n' => byte 20, 'p' => byte 21, 'q' => byte 22, 'r' => byte 23,
+	's' => byte 24, 't' => byte 25, 'u' => byte 26, 'v' => byte 27, 'w' => byte 28, 'x' => byte 29, 'y' => byte 30, 'z' => byte 31,
+	'A' => byte 8, 'B' => byte 9, 'C' => byte 10, 'D' => byte 11, 'E' => byte 12, 'F' => byte 13, 'G' => byte 14, 'H' => byte 15,
+	'I' => byte 16, 'J' => byte 17, 'K' => byte 18, 'M' => byte 19, 'N' => byte 20, 'P' => byte 21, 'Q' => byte 22, 'R' => byte 23,
+	'S' => byte 24, 'T' => byte 25, 'U' => byte 26, 'V' => byte 27, 'W' => byte 28, 'X' => byte 29, 'Y' => byte 30, 'Z' => byte 31,
+	* => byte INVAL
+};
+
+dec(s: string): array of byte
+{
+	a := array[(8*len s + 4)/5] of byte;
+	o := 0;
+	v := 0;
+	j := 0;
+	for(i := 0; i < len s; i++){
+		if((c := s[i]) > 16rFF || (c = int t32d[c]) == INVAL)
+			continue;
+		v <<= 5;
+		v |= c;
+		if((j += 5) >= 8){
+			a[o++] = byte (v>>(j-8));
+			j -= 8;
+		}
+	}
+	return a[0:o];
+}
--- /dev/null
+++ b/appl/lib/encoding/base64.b
@@ -1,0 +1,92 @@
+implement Encoding;
+
+include "encoding.m";
+
+enc(a: array of byte) : string
+{
+	n := len a;
+	if(n == 0)
+		return "";
+	out := "";
+	j := 0;
+	i := 0;
+	while(i < n) {
+		x := int a[i++] << 16;
+		if(i < n)
+			x |= (int a[i++]&255) << 8;
+		if(i < n)
+			x |= (int a[i++]&255);
+		out[j++] = c64(x>>18);
+		out[j++] = c64(x>>12);
+		out[j++] = c64(x>> 6);
+		out[j++] = c64(x);
+	}
+	nmod3 := n % 3;
+	if(nmod3 != 0) {
+		out[j-1] = '=';
+		if(nmod3 == 1)
+			out[j-2] = '=';
+	}
+	return out;
+}
+
+c64(c: int) : int
+{
+	v: con "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+	return v[c&63];
+}
+
+INVAL: con byte 255;
+
+t64d := array[256] of {
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,   byte 62,INVAL,INVAL,INVAL,   byte 63,
+      byte 52,   byte 53,   byte 54,   byte 55,   byte 56,   byte 57,   byte 58,   byte 59,   byte 60,   byte 61,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,    byte 0,    byte 1,    byte 2,    byte 3,    byte 4,    byte 5,    byte 6,    byte 7,    byte 8,    byte 9,   byte 10,   byte 11,   byte 12,   byte 13,   byte 14,
+      byte 15,   byte 16,   byte 17,   byte 18,   byte 19,   byte 20,   byte 21,   byte 22,   byte 23,   byte 24,   byte 25,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,   byte 26,   byte 27,   byte 28,   byte 29,   byte 30,   byte 31,   byte 32,   byte 33,   byte 34,   byte 35,   byte 36,   byte 37,   byte 38,   byte 39,   byte 40,
+      byte 41,   byte 42,   byte 43,   byte 44,   byte 45,   byte 46,   byte 47,   byte 48,   byte 49,   byte 50,   byte 51,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,
+   INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL,INVAL
+};
+
+dec(s: string): array of byte
+{
+	b24 := 0;
+	i := 0;
+	out := array[(3*len s+3)/4] of byte;	# upper bound, especially if s contains white space
+	o := 0;
+	for(n := 0; n < len s; n++){
+		if((c := s[n]) > 16rFF || (c = int t64d[c]) == int INVAL)
+			continue;
+		case i++ {
+		0 =>
+			b24 = c<<18;
+		1 =>
+			b24 |= c<<12;
+		2 =>
+			b24 |= c<<6;
+		3 =>
+			b24 |= c;
+			out[o++] = byte (b24>>16);
+			out[o++] = byte (b24>>8);
+			out[o++] = byte b24;
+			i = 0;
+		}
+	}
+	case i {
+	2 =>
+		out[o++] = byte (b24>>16);
+	3 =>
+		out[o++] = byte (b24>>16);
+		out[o++] = byte (b24>>8);
+	}
+	return out[0:o];
+}
--- /dev/null
+++ b/appl/lib/encoding/mkfile
@@ -1,0 +1,16 @@
+<../../../mkconfig
+
+TARG=\
+	base16.dis\
+	base32.dis\
+	base32a.dis\
+	base64.dis\
+
+MODULES=\
+
+SYSMODULES= \
+	encoding.m\
+
+DISBIN=$ROOT/dis/lib/encoding
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/env.b
@@ -1,0 +1,91 @@
+implement Env;
+
+#
+# Copyright © 2000 Vita Nuova Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+include "readdir.m";
+include "env.m";
+
+ENVDIR : con "/env/";
+
+setenv(var: string, val: string): int
+{
+	init();
+	if (var == nil || !nameok(var)) {
+		sys->werrstr("bad variable name");
+		return -1;
+	}
+	if (val == nil) {
+		sys->remove(ENVDIR+var);
+		return 0;
+	}
+	fd := sys->create(ENVDIR+var, Sys->OWRITE, 8r600);
+	if (fd == nil)
+		return -1;
+	valb := array of byte val;
+	if (sys->write(fd, valb, len valb) != len valb)
+		return -1;
+	return 0;
+}
+
+getenv(var: string): string
+{
+	init();
+	if (var == nil || !nameok(var))
+		return nil;
+	fd := sys->open(ENVDIR+var, Sys->OREAD);
+	if (fd == nil)
+		return nil;
+	(ok, stat) := sys->fstat(fd);
+	if (ok == -1)
+		return nil;
+	buf := array[int stat.length] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n < 0)
+		return nil;
+	return string buf[0:n];
+}
+
+getall(): list of (string, string)
+{
+	readdir := load Readdir Readdir->PATH;
+	if (readdir == nil)
+		return nil;
+	(a, nil) := readdir->init(ENVDIR,
+			Readdir->NONE | Readdir->COMPACT | Readdir->DESCENDING);
+	vl: list of (string, string);
+	for (i := 0; i < len a; i++)
+		vl = (a[i].name, getenv(a[i].name)) :: vl;
+	return vl;
+}
+
+# clone the current environment
+clone(): int
+{
+	init();
+	return sys->pctl(sys->FORKENV, nil);
+}
+
+new(): int
+{
+	init();
+	return sys->pctl(sys->NEWENV, nil);
+}
+
+init()
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+}
+
+nameok(var: string): int
+{
+	for(i:=0; i<len var; i++) 
+		if (var[i] == '/') return 0;
+	return 1;
+}
--- /dev/null
+++ b/appl/lib/ether.b
@@ -1,0 +1,83 @@
+implement Ether;
+
+include "sys.m";
+	sys: Sys;
+
+include "ether.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+parse(s: string): array of byte
+{
+	a := array[Eaddrlen] of byte;
+	for(i := 0; i < len a; i++){
+		n: int;
+		(n, s) = hex(s);
+		if(n < 0){
+			sys->werrstr("invalid ether address");
+			return nil;
+		}
+		a[i] = byte n;
+		if(s != nil && s[0] == ':')
+			s = s[1:];
+	}
+	return a;
+}
+
+hex(s: string): (int, string)
+{
+	n := 0;
+	for(i := 0; i < len s && i < 2; i++){
+		if((c := s[i]) >= '0' && c <= '9')
+			c -= '0';
+		else if(c >= 'a' && c <= 'f')
+			c += 10 - 'a';
+		else if(c >= 'A' && c <= 'F')
+			c += 10 - 'A';
+		else if(c == ':')
+			break;
+		else
+			return (-1, s);
+		n = (n<<4) | c;
+	}
+	if(i == 0)
+		return (-1, s);
+	return (n, s[i:]);
+}
+
+text(a: array of byte): string
+{
+	if(len a < Eaddrlen)
+		return "<invalid>";
+	return sys->sprint("%.2ux%.2ux%.2ux%.2ux%.2ux%.2ux",
+		int a[0], int a[1], int a[2], int a[3], int a[4], int a[5]);
+}
+
+addressof(dev: string): array of byte
+{
+	if(dev != nil && dev[0] != '/')
+		dev = "/net/"+dev;
+	fd := sys->open(dev+"/addr", Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[64] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+	if(n > 0 && buf[n-1] == byte '\n')
+		n--;
+	return parse(string buf[0:n]);
+}
+
+eqaddr(a: array of byte, b: array of byte): int
+{
+	if(len a != len b)
+		return 0;
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
--- /dev/null
+++ b/appl/lib/exception.b
@@ -1,0 +1,59 @@
+implement Exception;
+
+include "sys.m";
+	sys: Sys;
+include "exception.m";
+
+getexc(pid: int): (int, string, string)
+{
+	loadsys();
+	if(pid < 0)
+		pid = sys->pctl(0, nil);
+	f := "/prog/"+string pid+"/exception";
+	if((fd := sys->open(f, Sys->OREAD)) == nil)
+		return (0, nil, nil);
+	b := array[8192] of byte;
+	if((n := sys->read(fd, b, len b)) < 0)
+		return (0, nil, nil);
+	s := string b[0: n];
+	if(s == nil)
+		return (0, nil, nil);
+	(m, l) := sys->tokenize(s, " ");
+	if(m < 3)
+		return (0, nil, nil);
+	pc := int hd l;	l = tl l;
+	mod := hd l;	l = tl l;
+	exc := hd l;	l = tl l;
+	for( ; l != nil; l = tl l)
+		exc += " " + hd l;
+	return (pc, mod, exc);
+}
+
+setexcmode(mode: int): int
+{
+	loadsys();
+	pid := sys->pctl(0, nil);
+	f := "/prog/" + string pid + "/ctl";
+	if(mode == NOTIFYLEADER)
+		return write(f, "exceptions notifyleader");
+	else if(mode == PROPAGATE)
+		return write(f, "exceptions propagate");
+	else
+		return -1;
+}
+
+loadsys()
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+}
+
+write(f: string, s: string): int
+{
+	if((fd := sys->open(f, Sys->OWRITE)) == nil)
+		return -1;
+	b := array of byte s;
+	if((n := sys->write(fd, b, len b)) != len b)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/appl/lib/factotum.b
@@ -1,0 +1,525 @@
+implement Factotum;
+
+#
+# client interface to factotum
+#
+# this is a near transliteration of Plan 9 code, subject to the Lucent Public License 1.02
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "string.m";
+
+include "factotum.m";
+
+debug := 0;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+setdebug(i: int)
+{
+	debug = i;
+}
+
+getaia(a: array of byte, n: int): (int, array of byte)
+{
+	if(len a - n < 2)
+		return (-1, nil);
+	c := (int a[n+1]<<8) | int a[n+0];
+	n += 2;
+	if(len a - n < c)
+		return  (-1, nil);
+	b := array[c] of byte;		# could avoid copy if known not to alias
+	b[0:] = a[n: n+c];
+	return (n+c, b);
+}
+
+getais(a: array of byte, n: int): (int, string)
+{
+	(n, a) = getaia(a, n);
+	return (n, string a);
+}
+
+Authinfo.unpack(a: array of byte): (int, ref Authinfo)
+{
+	ai := ref Authinfo;
+	n: int;
+	(n, ai.cuid) = getais(a, 0);
+	(n, ai.suid) = getais(a, n);
+	(n, ai.cap) = getais(a, n);
+	(n, ai.secret) = getaia(a, n);
+	if(n < 0)
+		return (-1, nil);
+	return (n, ai);
+}
+
+open(): ref Sys->FD
+{
+	return sys->open("/mnt/factotum/rpc", Sys->ORDWR);
+}
+
+mount(fd: ref Sys->FD, mnt: string, flags: int, aname: string, keyspec: string): (int, ref Authinfo)
+{
+	ai: ref Authinfo;
+	afd := sys->fauth(fd, aname);
+	if(debug && afd == nil){
+		sys->print("fauth %s: %r\n", aname);
+		return (-1, nil);
+	}
+	if(afd != nil){
+		ai = proxy(afd, open(), "proto=p9any role=client "+keyspec);
+		if(debug && ai == nil){
+			sys->print("proxy failed: %r\n");
+			return (-1, nil);
+		}
+	}
+	return (sys->mount(fd, afd, mnt, flags, aname), ai);
+}
+
+dump(a: array of byte): string
+{
+	s := sys->sprint("[%d]", len a);
+	for(i := 0; i < len a; i++){
+		c := int a[i];
+		if(c >= ' ' && c <= 16r7E)
+			s += sys->sprint("%c", c);
+		else
+			s += sys->sprint("\\x%.2ux", c);
+	}
+	return s;
+}
+
+verbof(buf: array of byte): (string, array of byte)
+{
+	n := len buf;
+	for(i:=0; i<n && buf[i] != byte ' '; i++)
+		;
+	s := string buf[0:i];
+	if(i < n)
+		i++;
+	buf = buf[i:];
+	case  s {
+	"ok" or "error" or "done" or "phase" or
+	"protocol" or "needkey" or "toosmall" or "internal" =>
+		return (s, buf);
+	* =>
+		sys->werrstr(sys->sprint("malformed rpc response: %q", s));
+		return ("rpc failure", buf);
+	}
+}
+
+dorpc(fd: ref Sys->FD, verb: string, val: array of byte): (string, array of byte)
+{
+	(o, a) := rpc(fd, verb, val);
+	if(o != "needkey" && o != "badkey")
+		return (o, a);
+	return ("no key", a);	# don't know how to get key
+}
+
+rpc(afd: ref Sys->FD, verb: string, a: array of byte): (string, array of byte)
+{
+	va := array of byte verb;
+	l := len va;
+	na := len a;
+	if(na+l+1 > AuthRpcMax){
+		sys->werrstr("rpc too big");
+		return ("toobig", nil);
+	}
+	buf := array[na+l+1] of byte;
+	buf[0:] = va;
+	buf[l] = byte ' ';
+	buf[l+1:] = a;
+	if(debug)
+		sys->print("rpc: ->%s %s\n", verb, dump(a));
+	if((n:=sys->write(afd, buf, len buf)) != len buf){
+		if(n >= 0)
+			sys->werrstr("rpc short write");
+		return ("rpc failure", nil);
+	}
+	buf = array[AuthRpcMax] of byte;
+	if((n=sys->read(afd, buf, len buf)) < 0){
+		if(debug)
+			sys->print("<- (readerr) %r\n");
+		return ("rpc failure", nil);
+	}
+	if(n < len buf)
+		buf[n] = byte 0;
+	buf = buf[0:n];
+
+	#
+	# Set error string for good default behavior.
+	#
+	s: string;
+	(t, r) := verbof(buf);
+	if(debug)
+		sys->print("<- %s %#q\n", t, dump(r));
+	case t {
+	"ok" or
+	"rpc failure" =>
+		;	# don't touch
+	"error" =>
+		if(len r == 0)
+			s = "unspecified rpc error";
+		else
+			s = sys->sprint("%s", string r);
+	"needkey" =>
+		s = sys->sprint("needkey %s", string r);
+	"badkey" =>
+		(nf, flds) := sys->tokenize(string r, "\n");
+		if(nf < 2)
+			s = sys->sprint("badkey %q", string r);
+		else
+			s = sys->sprint("badkey %q", hd tl flds);
+		break;
+	"phase" =>
+		s = sys->sprint("phase error: %q", string r);
+	* =>
+		s = sys->sprint("unknown rpc type %q (bug in rpc.c)", t);
+	}
+	if(s != nil)
+		sys->werrstr(s);
+	return (t, r);
+}
+
+Authinfo.read(fd: ref Sys->FD): ref Authinfo
+{
+	(o, a) := rpc(fd, "authinfo", nil);
+	if(o != "ok")
+		return nil;
+	(n, ai) := Authinfo.unpack(a);
+	if(n <= 0)
+		sys->werrstr("bad auth info from factotum");
+	return ai;
+}
+
+proxy(fd: ref Sys->FD, afd: ref Sys->FD, params: string): ref Authinfo
+{
+	readc := chan of (array of byte, chan of (int, string));
+	writec := chan of (array of byte, chan of (int, string));
+	donec := chan of (ref Authinfo, string);
+	spawn genproxy(readc, writec, donec, afd, params);
+	for(;;)alt{
+	(buf, reply) := <-readc =>
+		n := sys->read(fd, buf, len buf);
+		if(n == -1)
+			reply <-= (-1, sys->sprint("%r"));
+		else
+			reply <-= (n, nil);
+	(buf, reply) := <-writec =>
+		n := sys->write(fd, buf, len buf);
+		if(n == -1)
+			reply <-= (-1, sys->sprint("%r"));
+		else
+			reply <-= (n, nil);
+	(authinfo, err) := <-donec =>
+		if(authinfo == nil)
+			sys->werrstr(err);
+		return authinfo;
+	}
+}
+
+#
+# do what factotum says
+#
+genproxy(
+	readc: chan of (array of byte, chan of (int, string)),
+	writec: chan of (array of byte, chan of (int, string)),
+	donec: chan of (ref Authinfo, string),
+	afd: ref Sys->FD,
+	params: string)
+{
+	if(afd == nil){
+		donec <-= (nil, "no authentication fd");
+		return;
+	}
+
+	pa := array of byte params;
+	(o, a) := dorpc(afd, "start", pa);
+	if(o != "ok"){
+		donec <-= (nil, sys->sprint("proxy start: %r"));
+		return;
+	}
+
+	ai: ref Authinfo;
+	err: string;
+done:
+	for(;;){
+		(o, a) = dorpc(afd, "read", nil);
+		case o {
+		"done" =>
+			if(len a > 0 && a[0] == byte 'h' && string a == "haveai")
+				ai = Authinfo.read(afd);
+			else
+				ai = ref Authinfo;	# auth succeeded but empty authinfo
+			break done;
+		"ok" =>
+			writec <-= (a[0:len a], reply := chan of (int, string));
+			(n, e) := <-reply;
+			if(n != len a){
+				err = "proxy write fd: "+e;
+				break done;
+			}
+		"phase" =>
+			buf := array[AuthRpcMax] of {* => byte 0};
+			n := 0;
+			for(;;){
+				(o, a) = dorpc(afd, "write", buf[0:n]);
+				if(o != "toosmall")
+					break;
+				c := int string a;
+				if(c > AuthRpcMax)
+					break;
+				readc <-= (buf[n:c], reply := chan of (int, string));
+				(m, e) := <-reply;
+				if(m <= 0){
+					err = e;
+					if(m == 0)
+						err = sys->sprint("proxy short read");
+					break done;
+				}
+				n += m;
+			}
+			if(o != "ok"){
+				err = sys->sprint("proxy rpc write: %r");
+				break done;
+			}
+		* =>
+			err = sys->sprint("proxy rpc: %r");
+			break done;
+		}
+	}
+	donec <-= (ai, err);
+}
+
+#
+# insecure passwords, role=client
+#
+
+getuserpasswd(keyspec: string): (string, string)
+{
+	str := load String String->PATH;
+	if(str == nil)
+		return (nil, nil);
+	fd := open();
+	if(fd == nil)
+		return (nil, nil);
+	if(((o, a) := dorpc(fd, "start", array of byte keyspec)).t0 != "ok" ||
+	   ((o, a) = dorpc(fd, "read", nil)).t0 != "ok"){
+		sys->werrstr("factotum: "+o);
+		return (nil, nil);
+	}
+	flds := str->unquoted(string a);
+	if(len flds != 2){
+		sys->werrstr("odd response from factotum");
+		return (nil, nil);
+	}
+	return (hd flds, hd tl flds);
+}
+
+#
+# challenge/response, role=server
+#
+
+challenge(keyspec: string): ref Challenge
+{
+	c := ref Challenge;
+	if((c.afd = open()) == nil)
+		return nil;
+	if(rpc(c.afd, "start", array of byte keyspec).t0 != "ok")
+		return nil;
+	(w, val) := rpc(c.afd, "read", nil);
+	if(w != "ok")
+		return nil;
+	c.chal = string val;
+	return c;
+}
+
+response(c: ref Challenge, resp: string): ref Authinfo
+{
+	if(c.afd == nil){
+		sys->werrstr("auth_response: connection not open");
+		return nil;
+	}
+	if(resp == nil){
+		sys->werrstr("auth_response: nil response");
+		return nil;
+	}
+
+	if(c.user != nil){
+		if(rpc(c.afd, "write", array of byte c.user).t0 != "ok"){
+			# we're out of phase with factotum; give up
+			c.afd = nil;
+			return nil;
+		}
+	}
+
+	if(rpc(c.afd, "write", array of byte resp).t0 != "ok"){
+		# don't close the connection; we might try again
+		return nil;
+	}
+
+	(w, val) := rpc(c.afd, "read", nil);
+	if(w != "done"){
+		sys->werrstr(sys->sprint("unexpected factotum reply: %q %q", w, string val));
+		c.afd = nil;
+		return nil;
+	}
+	ai := Authinfo.read(c.afd);
+	c.afd = nil;
+	return ai;
+}
+
+#
+# challenge/response, role=client
+#
+
+respond(chal: string, keyspec: string): (string, string)
+{
+	if((afd := open()) == nil)
+		return (nil, nil);
+
+	if(dorpc(afd, "start", array of byte keyspec).t0 != "ok" ||
+	   dorpc(afd, "write", array of byte chal).t0 != "ok")
+		return (nil, nil);
+	(o, resp) := dorpc(afd, "read", nil);
+	if(o != "ok")
+		return (nil, nil);
+
+	return (string resp, findattrval(rpcattrs(afd), "user"));
+}
+
+rpcattrs(afd: ref Sys->FD): list of ref Attr
+{
+	(o, a) := rpc(afd, "attr", nil);
+	if(o != "ok")
+		return nil;
+	return parseattrs(string a);
+}
+
+#
+# attributes
+#
+
+parseattrs(s: string): list of ref Attr
+{
+	str := load String String->PATH;
+	fld := str->unquoted(s);
+	rfld := fld;
+	for(fld = nil; rfld != nil; rfld = tl rfld)
+		fld = (hd rfld) :: fld;
+	attrs: list of ref Attr;
+	for(; fld != nil; fld = tl fld){
+		n := hd fld;
+		a := "";
+		tag := Aattr;
+		for(i:=0; i<len n; i++)
+			if(n[i] == '='){
+				a = n[i+1:];
+				n = n[0:i];
+				tag = Aval;
+			}
+		if(len n == 0)
+			continue;
+		if(tag == Aattr && len n > 1 && n[len n-1] == '?'){
+			tag = Aquery;
+			n = n[0:len n-1];
+		}
+		attrs = ref Attr(tag, n, a) :: attrs;
+	}
+	# TO DO: eliminate answered queries
+	return attrs;
+}
+
+Attr.text(a: self ref Attr): string
+{
+	case a.tag {
+	Aattr =>
+		return a.name;
+	Aval =>
+		return sys->sprint("%q=%q", a.name, a.val);
+	Aquery =>
+		return sys->sprint("%q?", a.name);
+	* =>
+		return "??";
+	}
+}
+
+attrtext(attrs: list of ref Attr): string
+{
+	s := "";
+	for(; attrs != nil; attrs = tl attrs){
+		if(s != nil)
+			s[len s] = ' ';
+		s += (hd attrs).text();
+	}
+	return s;
+}
+
+findattr(attrs: list of ref Attr, n: string): ref Attr
+{
+	for(; attrs != nil; attrs = tl attrs)
+		if((a := hd attrs).tag != Aquery && a.name == n)
+			return a;
+	return nil;
+}
+
+findattrval(attrs: list of ref Attr, n: string): string
+{
+	if((a := findattr(attrs, n)) != nil)
+		return a.val;
+	return nil;
+}
+
+delattr(l: list of ref Attr, n: string): list of ref Attr
+{
+	rl: list of ref Attr;
+	for(; l != nil; l = tl l)
+		if((hd l).name != n)
+			rl = hd l :: rl;
+	return rev(rl);
+}
+
+copyattrs(l: list of ref Attr): list of ref Attr
+{
+	rl: list of ref Attr;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rev(rl);
+}
+
+takeattrs(l: list of ref Attr, names: list of string): list of ref Attr
+{
+	rl: list of ref Attr;
+	for(; l != nil; l = tl l){
+		n := (hd l).name;
+		for(nl := names; nl != nil; nl = tl nl)
+			if((hd nl) == n){
+				rl = hd l :: rl;
+				break;
+			}
+	}
+	return rev(rl);
+}
+
+publicattrs(l: list of ref Attr): list of ref Attr
+{
+	rl: list of ref Attr;
+	for(; l != nil; l = tl l){
+		a := hd l;
+		if(a.tag != Aquery || a.val == nil)
+			rl = a :: rl;
+	}
+	return rev(rl);
+}
+
+rev[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
--- /dev/null
+++ b/appl/lib/filepat.b
@@ -1,0 +1,169 @@
+implement Filepat;
+
+include "sys.m";
+	sys: Sys;
+
+include "readdir.m";
+	rdir: Readdir;
+
+include "filepat.m";
+
+expand(pat: string): list of string
+{
+	if(sys == nil){
+		sys = load Sys Sys->PATH;
+	}
+	if(rdir == nil){
+		rdir = load Readdir Readdir->PATH;
+	}
+	(nil, elem) := sys->tokenize(pat, "/");
+	if(elem == nil)
+		return filepat1(pat, nil, 0);
+
+	files: list of string;
+	if(pat[0] == '/')
+		files = "/" :: nil;
+
+	while(elem != nil){
+		files = filepat1(hd elem, files, tl elem!=nil);
+		if(files == nil)
+			break;
+		elem = tl elem;
+	}
+	return files;
+}
+
+filepat1(pat: string, files: list of string, mustbedir: int): list of string
+{
+	if(files == nil)
+		return filepatdir(pat, "", nil, mustbedir);
+
+	# reverse list; will rebuild in forward order
+	r: list of string;
+	while(files != nil){
+		r = hd files :: r;
+		files = tl files;
+	}
+	files = r;
+
+	nfiles: list of string = nil;
+	while(files != nil){
+		nfiles = filepatdir(pat, hd files, nfiles, mustbedir);
+		files = tl files;
+	}
+	return nfiles;
+}
+
+filepatdir(pat: string, dir: string, files: list of string, mustbedir: int): list of string
+{
+	if(pat=="." || pat=="..") {
+		if(dir=="/" || dir=="")
+			files = (dir + pat) :: files;
+		else
+			files = (dir + "/" + pat) :: files;
+		return files;
+	}
+	dirname := dir;
+	if(dir == "")
+		dirname = ".";
+	# sort into descending order means resulting list will ascend
+	(d, n) := rdir->init(dirname, rdir->NAME|rdir->DESCENDING|rdir->COMPACT);
+	if(d == nil)
+		return files;
+
+	# suppress duplicates
+	for(i:=1; i<n; i++)
+		if(d[i-1].name == d[i].name){
+			d[i-1:] = d[i:];
+			n--;
+			i--;
+		}
+
+	for(i=0; i<n; i++){
+		if(match(pat, d[i].name) && (mustbedir==0 || (d[i].mode&Sys->DMDIR))){
+			if(dir=="/" || dir=="")
+				files = (dir + d[i].name) :: files;
+			else
+				files = (dir + "/" + d[i].name) :: files;
+		}
+	}
+	return files;
+}
+
+match(pat, name: string): int
+{
+	n := 0;
+	p := 0;
+	while(p < len pat){
+		r := pat[p++];
+		case r{
+		'*' =>
+			pat = pat[p:];
+			if(len pat==0)
+				return 1;
+			for(; n<=len name; n++)
+				if(match(pat, name[n:]))
+					return 1;
+			return 0;
+		'[' =>
+			if(n == len name)
+				return 0;
+			s := name[n++];
+			matched := 0;
+			invert := 0;
+			first := 1;
+			esc: int;
+			while(p < len pat){
+				(p, r, esc) = char(pat, p);
+				if(first && !esc && r=='^'){
+					invert = 1;
+					first = 0;
+					continue;
+				}
+				first = 0;
+				if(!esc && r==']')
+					break;
+				lo, hi: int;
+				(p, lo, hi) = range(pat, p-1);
+				if(lo<=s && s<=hi)
+					matched = 1;
+			}
+			if(!(!esc && r==']') || invert==matched)
+				return 0;
+		'?' =>
+			if(n==len name)
+				return 0;
+			n++;
+		'\\' =>
+			if(n==len name || p==len pat || pat[p++]!=name[n++])
+				return 0;
+		* =>
+			if(n==len name || r!=name[n++])
+				return 0;
+		}
+	}
+	return n == len name;
+}
+
+# return character or range (a-z)
+range(pat: string, p: int): (int, int, int)
+{
+	(q, lo, nil) := char(pat, p);
+	(q1, hi, esc) := char(pat, q);
+	if(!esc && hi=='-'){
+		(q1, hi, nil) = char(pat, q1);
+		return (q1, lo, hi);
+	}
+	return (q, lo, lo);
+}
+
+# return possibly backslash-escaped next character
+char(pat: string, p: int): (int, int, int)
+{
+	if(p == len pat)
+		return (p, 0, -1);
+	r := pat[p++];
+	if(p==len pat || r!='\\')
+		return (p, r, 0);
+	return (p+1, pat[p], 1);
+}
--- /dev/null
+++ b/appl/lib/format.b
@@ -1,0 +1,147 @@
+implement Format;
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "sexprs.m";
+	sexprs: Sexprs;
+	Sexp: import sexprs;
+include "format.m";
+
+# possible addition?
+# se2spec(se: list of ref Sexp): (array of Fmtspec, string)
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	sexprs = load Sexprs Sexprs->PATH;
+	sexprs->init();
+	bufio = load Bufio Bufio->PATH;
+}
+
+spec2se(spec: array of Fmtspec): list of ref Sexp
+{
+	l: list of ref Sexp;
+	for(i := len spec - 1; i >= 0; i--){
+		if((sp := spec[i]).fields != nil)
+			l = ref Sexp.List(ref Sexp.String(sp.name, nil) :: spec2se(sp.fields)) :: l;
+		else if(sp.name != nil)
+			l = ref Sexp.String(sp.name, nil) :: l;
+	}
+	return l;
+}
+
+spec2fmt(specs: array of Fmtspec): array of Fmt
+{
+	if(specs == nil)
+		return nil;
+	f := array[len specs] of Fmt;
+	for(i := 0; i < len specs; i++){
+		if(specs[i].name == nil)
+			f[i].kind = -1;
+		else
+			f[i] = (i, spec2fmt(specs[i].fields));
+	}
+	return f;
+}
+
+
+se2fmt(spec: array of Fmtspec, se: ref Sexp): (array of Fmt, string)
+{
+	if(!se.islist())
+		return (nil, "format must be a list");
+	return ses2fmt(spec, se.els());
+}
+
+ses2fmt(spec: array of Fmtspec, els: list of ref Sexp): (array of Fmt, string)
+{
+	a := array[len els] of Fmt;
+	for(i := 0; els != nil; els = tl els){
+		name := (hd els).op();
+		for(j := 0; j < len spec; j++)
+			if(spec[j].name == name)
+				break;
+		if(j == len spec)
+			return (nil, sys->sprint("format name %#q not found", name));
+		sp := spec[j];
+		if((hd els).islist() == 0)
+			a[i++] = Fmt(j, spec2fmt(sp.fields));
+		else if(sp.fields == nil)
+			return (nil, sys->sprint("unexpected list %#q", name));
+		else{
+			(f, err) := ses2fmt(sp.fields, (hd els).args());
+			if(f == nil)
+				return (nil, err);
+			a[i++] = Fmt(j, f);
+		}
+	}
+	return (a, nil);
+}
+
+rec2val(spec: array of Fmtspec, se: ref Sexprs->Sexp): (array of Fmtval, string)
+{
+	if(se.islist() == 0)
+		return (nil, "expected list of fields; got "+se.text());
+	els := se.els();
+	if(len els > len spec)
+		return (nil, sys->sprint("too many fields found, expected %d, got %s", len spec, se.text()));
+	a := array[len spec] of Fmtval;
+	err: string;
+	for(i := 0; i < len spec; i++){
+		f := spec[i];
+		if(f.name == nil)
+			continue;
+		if(els == nil)
+			return (nil, sys->sprint("too few fields found, expected %d, got %s", len spec, se.text()));
+		el := hd els;
+		if(f.fields == nil)
+			a[i].val = el;
+		else{
+			if(el.islist() == 0)
+				return (nil, "expected list of elements; got "+el.text());
+			vl := el.els();
+			a[i].recs = recs := array[len vl] of array of Fmtval;
+			for(j := 0; vl != nil; vl = tl vl){
+				(recs[j++], err) = rec2val(spec[i].fields, hd vl);
+				if(err != nil)
+					return (nil, err);
+			}
+		}
+		els = tl els;
+	}
+	return (a, nil);
+}
+
+Fmtval.text(v: self Fmtval): string
+{
+	return v.val.astext();
+}			
+
+Fmtfile.new(spec: array of Fmtspec): Fmtfile
+{
+	return (spec, (ref Sexp.List(spec2se(spec))).pack());
+}
+
+Fmtfile.open(f: self Fmtfile, name: string): ref Bufio->Iobuf
+{
+	fd := sys->open(name, Sys->ORDWR);
+	if(fd == nil){
+		sys->werrstr(sys->sprint("open failed: %r"));
+		return nil;
+	}
+	if(sys->write(fd, f.descr, len f.descr) == -1){
+		sys->werrstr(sys->sprint("format write failed: %r"));
+		return nil;
+	}
+	sys->seek(fd, big 0, Sys->SEEKSTART);
+	return bufio->fopen(fd, Sys->OREAD);
+}
+
+Fmtfile.read(f: self Fmtfile, iob: ref Iobuf): (array of Fmtval, string)
+{
+	(se, err) := Sexp.read(iob);
+	if(se == nil)
+		return (nil, err);
+	return rec2val(f.spec, se);
+}
--- /dev/null
+++ b/appl/lib/fsfilter.b
@@ -1,0 +1,67 @@
+implement Fsfilter;
+
+#
+# Copyright © 2004 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+	Fschan, Next, Quit, Skip, Down: import Fslib;
+
+filter[T](t: T, src, dst: Fschan)
+	for{
+	T =>
+		query: fn(t: self T, d: ref Sys->Dir, name: string, depth: int): int;
+	}
+{
+	names: list of string;
+	name: string;
+	indent := 0;
+	myreply := chan of int;
+loop:
+	for(;;){
+		(d, reply) := <-src;
+		if(d.dir != nil){
+			p := name;
+			if(indent > 0){
+				if(p != nil && p[len p - 1] != '/')
+					p[len p] = '/';
+			}
+			if(t.query(d.dir, p + d.dir.name, indent) == 0 && indent > 0){
+				reply <-= Next;
+				continue;
+			}
+		}
+		dst <-= (d, myreply);
+		case reply <-= <-myreply {
+		Quit =>
+			break loop;
+		Next =>
+			if(d.dir == nil && d.data == nil){
+				if(--indent == 0)
+					break loop;
+				(name, names) = (hd names, tl names);
+			}
+		Skip =>
+			if(--indent == 0)
+				break loop;
+			(name, names) = (hd names, tl names);
+		Down =>
+			if(d.dir != nil){
+				names = name :: names;
+				if(d.dir.mode & Sys->DMDIR){
+					if(indent == 0)
+						name = d.dir.name;
+					else{
+						if(name[len name - 1] != '/')
+							name[len name] = '/';
+						name += d.dir.name;
+					}
+				}
+				indent++;
+			}
+		}
+	}
+}
--- /dev/null
+++ b/appl/lib/fslib.b
@@ -1,0 +1,400 @@
+implement Fslib;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "fslib.m";
+
+# Fsdata stream conventions:
+# 
+# Fsdata: adt {
+#	dir: ref Sys->Dir;
+#	data: array of byte;
+# };
+# Fschan: type chan of (Fsdata, chan of int);
+# c: Fschan;
+# 
+# a stream of values sent on c represent the contents of a directory
+# hierarchy. after each value has been received, the associated reply
+# channel must be used to prompt the sender how next to proceed.
+# 
+# the first item sent on an fsdata channel represents the root directory
+# (it must be a directory), and its name holds the full path of the
+# hierarchy that's being transferred.  the items that follow represent
+# the contents of the root directory.
+# 
+# the set of valid sequences of values can be described by a yacc-style
+# grammar, where the terminal tokens describe data values (Fsdata adts)
+# passed down the channel.  this grammar describes the case where the
+# entire fs tree is traversed in its entirety:
+# 
+# dir:	DIR dircontents NIL
+# 	|	DIR NIL
+# dircontents: entry
+# 	|	dircontents entry
+# entry: FILE filecontents NIL
+# 	| FILE NIL
+# 	| dir
+# filecontents: DATA
+# 	| filecontents DATA
+# 
+# the tests for the various terminal token types, given a token (of type
+# Fsdata) t:
+# 
+# 	FILE		t.dir != nil && (t.dir.mode & Sys->DMDIR) == 0
+# 	DIR		t.dir != nil && (t.dir.mode & Sys->DMDIR)
+# 	DATA	t.data != nil
+# 	NIL		t.data == nil && t.dir == nil
+# 
+# when a token is received, there are four possible replies:
+# 	Quit
+# 		terminate the stream immediately.  no more tokens will
+# 		be on the channel.
+# 
+# 	Down
+# 		descend one level in the hierarchy, if possible.  the next tokens
+# 		will represent the contents of the current entry.
+# 
+# 	Next
+# 		get the next entry in a directory, or the next data
+# 		block in a file, or travel one up the hierarchy if
+#		it's the last entry or data block in that directory or file.
+# 
+# 	Skip
+# 		skip to the end of a directory or file's contents.
+#		if we're already at the end, this is a no-op (same as Next)
+# 
+# grammar including replies is different.  a token is the tuple (t, reply),
+# where reply is the value that was sent over the reply channel.  Quit
+# always causes the grammar to terminate, so it is omitted for clarity.
+# thus there are 12 possible tokens (DIR_DOWN, DIR_NEXT, DIR_SKIP, FILE_DOWN, etc...)
+#
+# dir: DIR_DOWN dircontents NIL_NEXT
+# 	| DIR_DOWN dircontents NIL_SKIP
+# 	| DIR_DOWN dircontents NIL_DOWN
+# 	| DIR_NEXT
+# dircontents:
+# 	| FILE_SKIP
+# 	| DIR_SKIP
+# 	| file dircontents
+# 	| dir dircontents
+# file: FILE_DOWN filecontents NIL_NEXT
+# 	| FILE_DOWN filecontents NIL_SKIP
+# 	| FILE_DOWN filecontents NIL_DOWN
+# 	| FILE_NEXT
+# filecontents:
+# 	| data
+# 	| data DATA_SKIP
+# data: DATA_NEXT
+# 	| data DATA_NEXT
+# 
+# both the producer and consumer of fs data on the channel must between
+# them conform to the second grammar. if a stream of fs data
+# is sent with no reply channel, the stream must conform to the first grammar.
+
+valuec := array[] of {
+	tagof(Value.V) => 'v',
+	tagof(Value.X) => 'x',
+	tagof(Value.P) => 'p',
+	tagof(Value.S) => 's',
+	tagof(Value.C) => 'c',
+	tagof(Value.T) => 't',
+	tagof(Value.M) => 'm',
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+# copy the contents (not the entry itself) of a directory from src to dst.
+copy(src, dst: Fschan): int
+{
+	indent := 1;
+	myreply := chan of int;
+	for(;;){
+		(d, reply) := <-src;
+		dst <-= (d, myreply);
+		r := <-myreply;
+		case reply <-= r {
+		Quit =>
+			return Quit;
+		Next =>
+			if(d.dir == nil && d.data == nil)
+				if(--indent == 0)
+					return Next;
+		Skip =>
+			if(--indent == 0)
+				return Next;
+		Down =>
+			if(d.dir != nil || d.data != nil)
+				indent++;
+		}
+	}
+}
+
+Report.new(): ref Report
+{
+	r := ref Report(chan of string, chan of (string, chan of string), chan of int);
+	spawn reportproc(r.startc, r.enablec, r.reportc);
+	return r;
+}
+
+Report.start(r: self ref Report, name: string): chan of string
+{
+	if(r == nil)
+		return nil;
+	errorc := chan of string;
+	r.startc <-= (name, errorc);
+	return errorc;
+}
+
+Report.enable(r: self ref Report)
+{
+	r.enablec <-= 0;
+}
+
+reportproc(startc: chan of (string, chan of string), startreports: chan of int, errorc: chan of string)
+{
+	realc := array[2] of chan of string;
+	p := array[len realc] of string;
+	a := array[0] of chan of string;;
+
+	n := 0;
+	for(;;) alt{
+	(prefix, c) := <-startc =>
+		if(n == len realc){
+			realc = (array[n * 2] of chan of string)[0:] = realc;
+			p = (array[n * 2] of string)[0:] = p;
+		}
+		realc[n] = c;
+		p[n] = prefix;
+		n++;
+	<-startreports =>
+		if(n == 0){
+			errorc <-= nil;
+			exit;
+		}
+		a = realc;
+	(x, report) := <-a =>
+		if(report == nil){
+#			errorc <-= "exit " + p[x];
+			--n;
+			if(n != x){
+				a[x] = a[n];
+				a[n] = nil;
+				p[x] = p[n];
+				p[n] = nil;
+			}
+			if(n == 0){
+				errorc <-= nil;
+				exit;
+			}
+		}else if(a == realc)
+			errorc <-= p[x] + ": " + report;
+	}
+}
+
+type2s(c: int): string
+{
+	case c{
+	'a' =>
+		return "any";
+	'x' =>
+		return "fs";
+	's' =>
+		return "string";
+	'v' =>
+		return "void";
+	'p' =>
+		return "gate";
+	'c' =>
+		return "command";
+	't' =>
+		return "entries";
+	'm' =>
+		return "selector";
+	* =>
+		return sys->sprint("unknowntype('%c')", c);
+	}
+}
+
+typeerror(tc: int, v: ref Value): string
+{
+	sys->fprint(sys->fildes(2), "fs: bad type conversion, expected %s, was actually %s\n", type2s(tc), type2s(valuec[tagof v]));
+	return "type conversion error";
+}
+
+Value.t(v: self ref Value): ref Value.T
+{
+	pick xv :=v {T => return xv;}
+	raise typeerror('t', v);
+}
+Value.c(v: self ref Value): ref Value.C
+{
+	pick xv :=v {C => return xv;}
+	raise typeerror('c', v);
+}
+Value.s(v: self ref Value): ref Value.S
+{
+	pick xv :=v {S => return xv;}
+	raise typeerror('s', v);
+}
+Value.p(v: self ref Value): ref Value.P
+{
+	pick xv :=v {P => return xv;}
+	raise typeerror('p', v);
+}
+Value.x(v: self ref Value): ref Value.X
+{
+	pick xv :=v {X => return xv;}
+	raise typeerror('x', v);
+}
+Value.v(v: self ref Value): ref Value.V
+{
+	pick xv :=v {V => return xv;}
+	raise typeerror('v', v);
+}
+Value.m(v: self ref Value): ref Value.M
+{
+	pick xv :=v {M => return xv;}
+	raise typeerror('m', v);
+}
+
+Value.typec(v: self ref Value): int
+{
+	return valuec[tagof v];
+}
+
+Value.discard(v: self ref Value)
+{
+	if(v == nil)
+		return;
+	pick xv := v {
+	X =>
+		(<-xv.i).t1 <-= Quit;
+	P =>
+		xv.i <-= (Nilentry, nil);
+	M =>
+		xv.i <-= (nil, nil, nil);
+	V =>
+		xv.i <-= 0;
+	T =>
+		xv.i.sync <-= 0;
+	}
+}
+
+sendnulldir(c: Fschan): int
+{
+	reply := chan of int;
+	c <-= ((ref Sys->nulldir, nil), reply);
+	if((r := <-reply) == Down){
+		c <-= ((nil, nil), reply);
+		if(<-reply != Quit)
+			return Quit;
+		return Next;
+	}
+	return r;
+}
+
+quit(errorc: chan of string)
+{
+	if(errorc != nil)
+		errorc <-= nil;
+	exit;
+}
+
+report(errorc: chan of string, err: string)
+{
+	if(errorc != nil)
+		errorc <-= err;
+}
+
+# true if a module with type sig t1 is compatible with a caller that expects t0
+typecompat(t0, t1: string): int
+{
+	(rt0, at0, ot0) := splittype(t0);
+	(rt1, at1, ot1) := splittype(t1);
+	if((rt0 != rt1 && rt0 != 'a') || at0 != at1)		# XXX could do better for repeated args.
+		return 0;
+	for(i := 1; i < len ot0; i++){
+		for(j := i; j < len ot0; j++)
+			if(ot0[j] == '-')
+				break;
+		(ok, t) := opttypes(ot0[i], ot1);
+		if(ok == -1 || ot0[i:j] != t)
+			return 0;
+		i = j + 1;
+	}
+	return 1;
+}
+
+splittype(t: string): (int, string, string)
+{
+	if(t == nil)
+		return (-1, nil, nil);
+	for(i := 1; i < len t; i++)
+		if(t[i] == '-')
+			break;
+	return (t[0], t[1:i], t[i:]);
+}
+
+opttypes(opt: int, opts: string): (int, string)
+{
+	for(i := 1; i < len opts; i++){
+		if(opts[i] == opt && opts[i-1] == '-'){
+			for(j := i+1; j < len opts; j++)
+				if(opts[j] == '-')
+					break;
+			return (0, opts[i+1:j]);
+		}
+	}
+	return (-1, nil);
+}
+
+cmdusage(s, t: string): string
+{
+	if(s == nil)
+		return nil;
+	for(oi := 0; oi < len t; oi++)
+		if(t[oi] == '-')
+			break;
+	if(oi < len t){
+		single, multi: string;
+		for(i := oi; i < len t - 1;){
+			for(j := i + 1; j < len t; j++)
+				if(t[j] == '-')
+					break;
+
+			optargs := t[i+2:j];
+			if(optargs == nil)
+				single[len single] = t[i+1];
+			else{
+				multi += sys->sprint(" [-%c", t[i+1]);
+				for (k := 0; k < len optargs; k++)
+					multi += " " + type2s(optargs[k]);
+				multi += "]";
+			}
+			i = j;
+		}
+		if(single != nil)
+			s += " [-" + single + "]";
+		s += multi;
+	}
+	multi := 0;
+	if(oi > 2 && t[oi - 1] == '*'){
+		multi = 1;
+		oi -= 2;
+	}
+	for(k := 1; k < oi; k++)
+		s += " " + type2s(t[k]);
+	if(multi)
+		s += " [" + type2s(t[k]) + "...]";
+	s += " -> " + type2s(t[0]);
+	return s;
+}
--- /dev/null
+++ b/appl/lib/fsproto.b
@@ -1,0 +1,385 @@
+implement FSproto;
+
+include "sys.m";
+	sys: Sys;
+	Dir: import Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "readdir.m";
+	readdir: Readdir;
+include "fsproto.m";
+
+File: adt {
+	new:	string;
+	elem:	string;
+	old:	string;
+	uid:	string;
+	gid:	string;
+	mode:	int;
+};
+
+Proto: adt {
+	b:	ref Iobuf;
+	doquote:	int;
+	indent:	int;
+	lineno:	int;
+	newfile:	string;
+	oldfile:	string;
+	oldroot:	string;
+	ec:	chan of Direntry;
+	wc:	chan of (string, string);
+
+	walk:	fn(w: self ref Proto, f: ref File, level: int);
+	entry:	fn(w: self ref Proto, old: string, new: string, d: ref Sys->Dir);
+	warn:	fn(w: self ref Proto, s: string);
+	fatal:	fn(w: self ref Proto, s: string);
+};
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		return sys->sprint("%r");
+	str = load String String->PATH;
+	if(str == nil)
+		return sys->sprint("%r");
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		return sys->sprint("%r");
+	return nil;
+}
+
+readprotofile(proto: string, root: string, entries: chan of Direntry, warnings: chan of (string, string)): string
+{
+	b := bufio->open(proto, Sys->OREAD);
+	if(b == nil)
+		return sys->sprint("%r");
+	rdproto(b, root, entries, warnings);
+	return nil;
+}
+
+readprotostring(proto: string, root: string, entries: chan of Direntry, warnings: chan of (string, string))
+{
+	rdproto(bufio->sopen(proto), root, entries, warnings);
+}
+
+rdproto(b: ref Iobuf, root: string, entries: chan of Direntry, warnings: chan of (string, string)): string
+{
+	w := ref Proto;
+	w.b = b;
+	w.doquote = 1;
+	w.ec = entries;
+	w.wc = warnings;
+	w.oldroot = root;
+	w.lineno = 0;
+	w.indent = 0;
+	file := ref File;
+	file.mode = 0;
+	spawn walker(w, file);
+	return nil;
+}
+
+walker(w: ref Proto, file: ref File)
+{
+	w.walk(file, -1);
+	w.entry(nil, nil, nil);
+}
+
+Proto.entry(w: self ref Proto, old: string, new: string, d: ref Sys->Dir)
+{
+	if(w.ec != nil)
+		w.ec <-= (old, new, d);
+}
+
+Proto.warn(w: self ref Proto, s: string)
+{
+	if(w.wc != nil)
+		w.wc <-= (w.oldfile, s);
+	else
+		sys->fprint(sys->fildes(2), "warning: %s\n", s);
+}
+
+Proto.fatal(w: self ref Proto, s: string)
+{
+	if(w.wc != nil)
+		w.wc <-= (w.oldfile, s);
+	else
+		sys->fprint(sys->fildes(2), "fatal error: %s\n", s);
+	w.ec <-= (nil, nil, nil);
+	exit;
+}
+
+Proto.walk(w: self ref Proto, me: ref File, level: int)
+{
+	(child, fp) := getfile(w, me);
+	if(child == nil)
+		return;
+	if(child.elem == "+" || child.elem == "*" || child.elem == "%"){
+		rec := child.elem[0] == '+';
+		filesonly := child.elem[0] == '%';
+		child.new = me.new;
+		setnames(w, child);
+		mktree(w, child, rec, filesonly);
+		(child, fp) = getfile(w, me);
+	}
+	while(child != nil && w.indent > level){
+		if(mkfile(w, child))
+			w.walk(child, w.indent);
+		(child, fp) = getfile(w, me);
+	}
+	if(child != nil){
+		w.b.seek(big fp, 0);
+		w.lineno--;
+	}
+}
+
+mktree(w: ref Proto, me: ref File, rec: int, filesonly: int)
+{
+	fd := sys->open(w.oldfile, Sys->OREAD);
+	if(fd == nil){
+		w.warn(sys->sprint("can't open %s: %r", w.oldfile));
+		return;
+	}
+	child := ref *me;
+	(d, n) := readdir->init(w.oldfile, Readdir->NAME|Readdir->COMPACT);
+	for(i := 0; i < n; i++) {
+		if(filesonly && (d[i].mode & Sys->DMDIR))
+			continue;
+		child.new = mkpath(me.new, d[i].name);
+		if(me.old != nil)
+			child.old = mkpath(me.old, d[i].name);
+		child.elem = d[i].name;
+		setnames(w, child);
+		if(copyfile(w, child, d[i]) && rec)
+			mktree(w, child, rec, filesonly);
+	}
+}
+
+mkfile(w: ref Proto, f: ref File): int
+{
+	(i, dir) := sys->stat(w.oldfile);
+	if(i < 0){
+		w.warn(sys->sprint("can't stat file %s: %r", w.oldfile));
+		skipdir(w);
+		return 0;
+	}
+	return copyfile(w, f, ref dir);
+}
+
+copyfile(w: ref Proto, f: ref File, d: ref Dir): int
+{
+	d.name = f.elem;
+	if(f.mode != ~0){
+		if((d.mode&Sys->DMDIR) != (f.mode&Sys->DMDIR))
+			w.warn(sys->sprint("inconsistent mode for %s", f.new));
+		else
+			d.mode = f.mode;
+	}
+	w.entry(w.oldfile, w.newfile, d);
+	return (d.mode & Sys->DMDIR) != 0;
+}
+
+setnames(w: ref Proto, f: ref File)
+{
+	w.newfile = f.new;
+	if(f.old != nil){
+		if(f.old[0] == '/')
+			w.oldfile = mkpath(w.oldroot, f.old);
+		else
+			w.oldfile = f.old;
+	}else
+		w.oldfile = mkpath(w.oldroot, f.new);
+}
+
+#
+# skip all files in the proto that
+# could be in the current dir
+#
+skipdir(w: ref Proto)
+{
+	if(w.indent < 0)
+		return;
+	b := w.b;
+	level := w.indent;
+	for(;;){
+		w.indent = 0;
+		fp := b.offset();
+		p := b.gets('\n');
+		if(p != nil && p[len p - 1] != '\n')
+			p += "\n";
+		w.lineno++;
+		if(p == nil){
+			w.indent = -1;
+			return;
+		}
+		for(j := 0; (c := p[j++]) != '\n';)
+			if(c == ' ')
+				w.indent++;
+			else if(c == '\t')
+				w.indent += 8;
+			else
+				break;
+		if(w.indent <= level){
+			b.seek(fp, 0);
+			w.lineno--;
+			return;
+		}
+	}
+}
+
+getfile(w: ref Proto, old: ref File): (ref File, int)
+{
+	p, elem: string;
+	c: int;
+
+	if(w.indent < 0)
+		return (nil, 0);
+	b := w.b;
+	fp := int b.offset();
+	do {
+		w.indent = 0;
+		p = b.gets('\n');
+		if(p != nil && p[len p - 1] != '\n')
+			p += "\n";
+		w.lineno++;
+		if(p == nil){
+			w.indent = -1;
+			return (nil, 0);
+		}
+		for(; (c = p[0]) != '\n'; p = p[1:])
+			if(c == ' ')
+				w.indent++;
+			else if(c == '\t')
+				w.indent += 8;
+			else
+				break;
+	} while(c == '\n' || c == '#');
+	(elem, p) = getname(w, p);
+	if(p == nil)
+		return (nil, 0);
+	f := ref File;
+	f.new = mkpath(old.new, elem);
+	(nil, f.elem) = str->splitr(f.new, "/");
+	if(f.elem == nil)
+		w.fatal(sys->sprint("can't find file name component of %s", f.new));
+	(f.mode, p) = getmode(w, p);
+	if(p == nil)
+		return (nil, 0);
+	(f.uid, p) = getname(w, p);
+	if(p == nil)
+		return (nil, 0);
+	if(f.uid == nil)
+		f.uid = "-";
+	(f.gid, p) = getname(w, p);
+	if(p == nil)
+		return (nil, 0);
+	if(f.gid == nil)
+		f.gid = "-";
+	f.old = getpath(p);
+	if(f.old == "-")
+		f.old = nil;
+	if(f.old == nil && old.old != nil)
+		f.old = mkpath(old.old, elem);
+	setnames(w, f);
+	return (f, fp);
+}
+
+getpath(p: string): string
+{
+	for(; (c := p[0]) == ' ' || c == '\t'; p = p[1:])
+		;
+	for(n := 0; (c = p[n]) != '\n' && c != ' ' && c != '\t'; n++)
+		;
+	return p[0:n];
+}
+
+getname(w: ref Proto, p: string): (string, string)
+{
+	for(; (c := p[0]) == ' ' || c == '\t'; p = p[1:])
+		;
+	i := 0;
+	s := "";
+	quoted := 0;
+	for(; (c = p[0]) != '\n' && (c != ' ' && c != '\t' || quoted); p = p[1:]){
+		if(quoted && c == '\'' && p[1] == '\'')
+			p = p[1:];
+		else if(c == '\'' && w.doquote){
+			quoted = !quoted;
+			continue;
+		}
+		s[i++] = c;
+	}
+	if(len s > 0 && s[0] == '$'){
+		s = getenv(s[1:]);
+		if(s == nil)
+			w.warn(sys->sprint("can't read environment variable %s", s));
+	}
+	return (s, p);
+}
+
+getenv(s: string): string
+{
+	if(s == "user")
+		return readfile("/dev/user");	# more accurate?
+	return readfile("/env/"+s);
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd != nil){
+		a := array[256] of byte;
+		n := sys->read(fd, a, len a);
+		if(n > 0)
+			return string a[0:n];
+	}
+	return nil;
+}
+
+getmode(w: ref Proto, p: string): (int, string)
+{
+	s: string;
+
+	(s, p) = getname(w, p);
+	if(s == nil || s == "-")
+		return (~0, p);
+	m := 0;
+	if(s[0] == 'd'){
+		m |= Sys->DMDIR;
+		s = s[1:];
+	}
+	if(s[0] == 'a'){
+		m |= Sys->DMAPPEND;
+		s = s[1:];
+	}
+	if(s[0] == 'l'){
+		m |= Sys->DMEXCL;
+		s = s[1:];
+	}
+	for(i:=0; i<len s || i < 3; i++)
+		if(i >= len s || !(s[i]>='0' && s[i]<='7')){
+			w.warn(sys->sprint("bad mode specification %s", s));
+			return (~0, p);
+		}
+	(v, nil) := str->toint(s, 8);
+	return (m|v, p);
+}
+
+mkpath(prefix, elem: string): string
+{
+	slash1 := slash2 := 0;
+	if(len prefix > 0)
+		slash1 = prefix[len prefix - 1] == '/';
+	if(len elem > 0)
+		slash2 = elem[0] == '/';
+	if(slash1 && slash2)
+		return prefix+elem[1:];
+	if(!slash1 && !slash2)
+		return prefix+"/"+elem;
+	return prefix+elem;
+}
--- /dev/null
+++ b/appl/lib/hash.b
@@ -1,0 +1,80 @@
+# ehg@research.bell-labs.com 14Dec1996
+implement Hash;
+
+include "hash.m";
+
+# from Aho Hopcroft Ullman
+fun1(s:string, n:int):int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
+
+# from Limbo compiler
+fun2(s:string, n:int):int
+{
+	h := 0;
+	m := len s;
+	for(i := 0; i < m; i++){
+		c := s[i];
+		d := c;
+		c ^= c << 6;
+		h += (c << 11) ^ (c >> 1);
+		h ^= (d << 14) + (d << 7) + (d << 4) + d;
+	}
+	return (h & 16r7fffffff) % n;
+}
+
+new(size: int):ref HashTable
+{
+	return ref HashTable(array[size] of list of HashNode);
+}
+
+HashTable.find(h: self ref HashTable, key: string): ref HashVal
+{
+	j := fun1(key,len h.a);
+	for(q := h.a[j]; q!=nil; q = tl q){
+		if((hd q).key==key)
+			return (hd q).val;
+	}
+	return nil;
+}
+
+HashTable.insert(h: self ref HashTable, key: string, val: HashVal)
+{
+	j := fun1(key,len h.a);
+	for(q := h.a[j]; q!=nil; q = tl q){
+		if((hd q).key==key){
+			p := (hd q).val;
+			p.i = val.i;
+			p.r = val.r;
+			p.s = val.s;
+			return;
+		}
+	}
+	h.a[j] = HashNode(key,ref HashVal(val.i,val.r,val.s)) :: h.a[j];
+}
+
+HashTable.delete(h:self ref HashTable, key:string)
+{
+	j := fun1(key,len h.a);
+	dl:list of HashNode; dl = nil;
+	for(q := h.a[j]; q!=nil; q = tl q){
+		if((hd q).key!=key)
+			dl = (hd q) :: dl;
+	}
+	h.a[j] = dl;
+}
+
+HashTable.all(h:self ref HashTable): list of HashNode
+{
+	dl:list of HashNode; dl = nil;
+	for(j:=0; j<len h.a; j++)
+		for(q:=h.a[j]; q!=nil; q = tl q)
+			dl = (hd q) :: dl;
+	return dl;
+}
--- /dev/null
+++ b/appl/lib/html.b
@@ -1,0 +1,664 @@
+implement HTML;
+
+include "sys.m";
+include "html.m";
+include "strinttab.m";
+
+sys:	Sys;
+T:	StringIntTab;
+
+Stringtab: adt
+{
+	name:	string;
+	val:		int;
+};
+
+chartab:= array[] of { T->StringInt
+	("AElig", 'Æ'),
+	("Aacute", 'Á'),
+	("Acirc", 'Â'),
+	("Agrave", 'À'),
+	("Aring", 'Å'),
+	("Atilde", 'Ã'),
+	("Auml", 'Ä'),
+	("Ccedil", 'Ç'),
+	("ETH", 'Ð'),
+	("Eacute", 'É'),
+	("Ecirc", 'Ê'),
+	("Egrave", 'È'),
+	("Euml", 'Ë'),
+	("Iacute", 'Í'),
+	("Icirc", 'Î'),
+	("Igrave", 'Ì'),
+	("Iuml", 'Ï'),
+	("Ntilde", 'Ñ'),
+	("Oacute", 'Ó'),
+	("Ocirc", 'Ô'),
+	("Ograve", 'Ò'),
+	("Oslash", 'Ø'),
+	("Otilde", 'Õ'),
+	("Ouml", 'Ö'),
+	("THORN", 'Þ'),
+	("Uacute", 'Ú'),
+	("Ucirc", 'Û'),
+	("Ugrave", 'Ù'),
+	("Uuml", 'Ü'),
+	("Yacute", 'Ý'),
+	("aacute", 'á'),
+	("acirc", 'â'),
+	("acute", '´'),
+	("aelig", 'æ'),
+	("agrave", 'à'),
+	("alpha", 'α'),
+	("amp", '&'),
+	("aring", 'å'),
+	("atilde", 'ã'),
+	("auml", 'ä'),
+	("beta", 'β'),
+	("brvbar", '¦'),
+	("ccedil", 'ç'),
+	("cdots", '⋯'),
+	("cedil", '¸'),
+	("cent", '¢'),
+	("chi", 'χ'),
+	("copy", '©'),
+	("curren", '¤'),
+	("ddots", '⋱'),
+	("deg", '°'),
+	("delta", 'δ'),
+	("divide", '÷'),
+	("eacute", 'é'),
+	("ecirc", 'ê'),
+	("egrave", 'è'),
+	("emdash", '—'),
+	("emsp", ' '),
+	("endash", '–'),
+	("ensp", ' '),
+	("epsilon", 'ε'),
+	("eta", 'η'),
+	("eth", 'ð'),
+	("euml", 'ë'),
+	("frac12", '½'),
+	("frac14", '¼'),
+	("frac34", '¾'),
+	("gamma", 'γ'),
+	("gt", '>'),
+	("iacute", 'í'),
+	("icirc", 'î'),
+	("iexcl", '¡'),
+	("igrave", 'ì'),
+	("iota", 'ι'),
+	("iquest", '¿'),
+	("iuml", 'ï'),
+	("kappa", 'κ'),
+	("lambda", 'λ'),
+	("laquo", '«'),
+	("ldots", '…'),
+	("lt", '<'),
+	("macr", '¯'),
+	("micro", 'µ'),
+	("middot", '·'),
+	("mu", 'μ'),
+	("nbsp", ' '),
+	("not", '¬'),
+	("ntilde", 'ñ'),
+	("nu", 'ν'),
+	("oacute", 'ó'),
+	("ocirc", 'ô'),
+	("ograve", 'ò'),
+	("omega", 'ω'),
+	("omicron", 'ο'),
+	("ordf", 'ª'),
+	("ordm", 'º'),
+	("oslash", 'ø'),
+	("otilde", 'õ'),
+	("ouml", 'ö'),
+	("para", '¶'),
+	("phi", 'φ'),
+	("pi", 'π'),
+	("plusmn", '±'),
+	("pound", '£'),
+	("psi", 'ψ'),
+	("quad", ' '),
+	("quot", '"'),
+	("raquo", '»'),
+	("reg", '®'),
+	("rho", 'ρ'),
+	("sect", '§'),
+	("shy", '­'),
+	("sigma", 'σ'),
+	("sp", ' '),
+	("sup1", '¹'),
+	("sup2", '²'),
+	("sup3", '³'),
+	("szlig", 'ß'),
+	("tau", 'τ'),
+	("theta", 'θ'),
+	("thinsp", ' '),
+	("thorn", 'þ'),
+	("times", '×'),
+	("trade", '™'),
+	("uacute", 'ú'),
+	("ucirc", 'û'),
+	("ugrave", 'ù'),
+	("uml", '¨'),
+	("upsilon", 'υ'),
+	("uuml", 'ü'),
+	("varepsilon", '∈'),
+	("varphi", 'ϕ'),
+	("varpi", 'ϖ'),
+	("varrho", 'ϱ'),
+	("vdots", '⋮'),
+	("vsigma", 'ς'),
+	("vtheta", 'ϑ'), 
+	("xi", 'ξ'),
+	("yacute", 'ý'),
+	("yen", '¥'),
+	("yuml", 'ÿ'),
+	("zeta", 'ζ'),
+};
+
+htmlstringtab := array[] of { T->StringInt
+	("a", Ta),
+	("address", Taddress),
+	("applet", Tapplet),
+	("area", Tarea),
+	("att_footer", Tatt_footer),
+	("b", Tb),
+	("base", Tbase),
+	("basefont", Tbasefont),
+	("big", Tbig),
+	("blink", Tblink),
+	("blockquote", Tblockquote),
+	("body", Tbody),
+	("bq", Tbq),
+	("br", Tbr),
+	("caption", Tcaption),
+	("center", Tcenter),
+	("cite", Tcite),
+	("code", Tcode),
+	("col", Tcol),
+	("colgroup", Tcolgroup),
+	("dd", Tdd),
+	("dfn", Tdfn),
+	("dir", Tdir),
+	("div", Tdiv),
+	("dl", Tdl),
+	("dt", Tdt),
+	("em", Tem),
+	("font", Tfont),
+	("form", Tform),
+	("frame", Tframe),
+	("frameset", Tframeset),
+	("h1", Th1),
+	("h2", Th2),
+	("h3", Th3),
+	("h4", Th4),
+	("h5", Th5),
+	("h6", Th6),
+	("head", Thead),
+	("hr", Thr),
+	("html", Thtml),
+	("i", Ti),
+	("img", Timg),
+	("input", Tinput),
+	("isindex", Tisindex),
+	("item", Titem),
+	("kbd", Tkbd),
+	("li", Tli),
+	("link", Tlink),
+	("map", Tmap),
+	("menu", Tmenu),
+	("meta", Tmeta),
+	("nobr", Tnobr),
+	("noframes", Tnoframes),
+	("ol", Tol),
+	("option", Toption),
+	("p", Tp),
+	("param", Tparam),
+	("pre", Tpre),
+	("q", Tq),
+	("samp", Tsamp),
+	("script", Tscript),
+	("select", Tselect),
+	("small", Tsmall),
+	("strike", Tstrike),
+	("strong", Tstrong),
+	("style", Tstyle),
+	("sub", Tsub),
+	("sup", Tsup),
+	("t", Tt),
+	("table", Ttable),
+	("tbody", Ttbody),
+	("td", Ttd),
+	("textarea", Ttextarea),
+	("textflow", Ttextflow),
+	("tfoot", Ttfoot),
+	("th", Tth),
+	("thead", Tthead),
+	("title", Ttitle),
+	("tr", Ttr),
+	("tt", Ttt),
+	("u", Tu),
+	("ul", Tul),
+	("var", Tvar)
+};
+
+W, D, L, U, N: con byte (1<<iota);
+NCTYPE: con 256;
+
+ctype := array[NCTYPE] of {
+	'0'=>D, '1'=>D, '2'=>D, '3'=>D, '4'=>D,
+	'5'=>D, '6'=>D, '7'=>D, '8'=>D, '9'=>D,
+	'A'=>U, 'B'=>U, 'C'=>U, 'D'=>U, 'E'=>U, 'F'=>U,
+	'G'=>U, 'H'=>U, 'I'=>U, 'J'=>U, 'K'=>U, 'L'=>U,
+	'M'=>U, 'N'=>U, 'O'=>U, 'P'=>U, 'Q'=>U, 'R'=>U,
+	'S'=>U, 'T'=>U, 'U'=>U, 'V'=>U, 'W'=>U, 'X'=>U,
+	'Y'=>U, 'Z'=>U,
+	'a'=>L, 'b'=>L, 'c'=>L, 'd'=>L, 'e'=>L, 'f'=>L,
+	'g'=>L, 'h'=>L, 'i'=>L, 'j'=>L, 'k'=>L, 'l'=>L,
+	'm'=>L, 'n'=>L, 'o'=>L, 'p'=>L, 'q'=>L, 'r'=>L,
+	's'=>L, 't'=>L, 'u'=>L, 'v'=>L, 'w'=>L, 'x'=>L,
+	'y'=>L, 'z'=>L,
+	'.'=>N, '-'=>N,
+	' '=>W, '\n'=>W, '\t'=>W, '\r'=>W,
+	* => byte 0
+};
+
+lex(b: array of byte, charset: int, keepwh: int): array of ref Lex
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(T == nil)
+		T = load StringIntTab StringIntTab->PATH;
+	if(T == nil) {
+		sys->print("HTML->lex: couldn't %s\n", StringIntTab->PATH);
+		return nil;
+	}
+
+	a: array of ref Lex;
+	ai := 0;
+	i := 0;
+	nb := len b;
+	for(;;){
+   Whitespace:
+		for(;;){
+			# ignore nulls
+			while(i<nb && (int b[i] == 0))
+				i++;
+			# skip white space
+			if(!keepwh) {
+				while(i<nb) {
+					c := int b[i];
+					if(!(int (ctype[c]&W)) && c != ' ')
+						break;
+					i++;
+				}
+			}
+			# skip comments
+			if(i<nb-4 && int b[i]=='<' && int b[i+1]=='!'
+					&& int b[i+2]=='-' && int b[i+3]=='-') {
+				i += 4;
+				while(i<nb-3){
+					if(int b[i]=='-' && int b[i+1]=='-' && int b[i+2]=='>'){
+						i += 3;
+						continue Whitespace;
+					}
+					i++;
+				}
+				continue Whitespace;
+			}
+			break;
+		}
+		if(i == nb)
+			break;
+		if(ai == len a){
+			na := array[len a + 500] of ref Lex;
+			if(a != nil)
+				na[0:] = a;
+			a = na;
+		}
+		if(int b[i] == '<'){
+			lx : ref Lex;
+			(lx, i) = gettag(b, i, charset);
+			a[ai++] = lx;
+		}
+		else {
+			s: string;
+			(s, i) = getdata(b, i, keepwh, charset);
+			a[ai++] = ref Lex (Data, s, nil);
+		}
+	}
+	return a[0:ai];
+}
+
+getdata(b: array of byte, i: int, keepnls, charset: int): (string, int)
+{
+	s:= "";
+	j:= 0;
+	c: int;
+	nb := len b;
+
+loop:
+	while(i < nb){
+		oldi := i;
+		case charset{
+		Latin1 =>
+			c = int b[i++];
+		UTF8 =>
+			j: int;
+			(c, j, nil) = sys->byte2char(b, i);
+			i += j;
+		}
+		case c {
+		0 or 16r1a =>
+			continue loop;
+		'<' =>
+			i = oldi;
+			break loop;
+		'&' =>
+			(c, i) = ampersand(b, i);
+		'\n' =>
+			if(!keepnls)
+				c = ' ';
+		'\r' =>
+			if(oldi > 0 && int b[oldi-1] == '\n')
+				continue loop;
+			if(keepnls)
+				c = '\n';
+			else
+				c = ' ';
+		}
+		s[j++] = c;
+	}
+	return (s, i);
+}
+
+gettag(b: array of byte, i, charset: int): (ref Lex, int)
+{
+	rbra := 0;
+	nb := len b;
+	ans := ref Lex(Notfound, "", nil);
+	al: list of Attr;
+	if(++i == nb)
+		return (ans, i);
+	istart := i;
+	c := int b[i];
+	if(c == '/') {
+		rbra = RBRA;
+		if(++i == nb)
+			return (ans, i);
+		c = int b[i];
+	}
+	if(c>=NCTYPE || !int (ctype[c]&(L|U))) {
+		while(i < nb) {
+			c = int b[i++];
+			if(c == '>')
+				break;
+		}
+		ans.text = string b[istart:i];
+		return (ans, i);
+	}
+	namstart := i;
+	while(c<NCTYPE && int (ctype[c]&(L|U|D|N))) {
+		if(++i == nb) {
+			ans.text = string b[istart:i];
+			return (ans, i);
+		}
+		c = int b[i];
+	}
+	name := lowercase(b, namstart, i);
+	(fnd, tag) := T->lookup(htmlstringtab, name);
+	if(fnd)
+		ans.tag = tag+rbra;
+	else
+		ans.text = name;
+attrloop:
+	while(i < nb){
+		# look for "ws name" or "ws name ws = ws val"  (ws=whitespace)
+		# skip whitespace
+		while(c<NCTYPE && int (ctype[c]&W)) {
+			if(++i == nb)
+				break attrloop;
+			c = int b[i];
+		}
+		if(c == '>') {
+			i++;
+			break;
+		}
+		if(c == '<')
+			break;	# error: unclosed tag
+		if(c>=NCTYPE || !int (ctype[c]&(L|U))) {
+			# error, not the start of a name
+			# skip to end of tag
+			while(i < nb) {
+				c = int b[i++];
+				if(c == '>')
+					break;
+			}
+			break attrloop;
+		}
+		# gather name
+		namstart = i;
+		while(c<NCTYPE && int (ctype[c]&(L|U|D|N))) {
+			if(++i == nb)
+				break attrloop;
+			c = int b[i];
+		}
+		name = lowercase(b, namstart, i);
+		# skip whitespace
+		while(c<NCTYPE && int (ctype[c]&W)) {
+			if(++i == nb)
+				break attrloop;
+			c = int b[i];
+		}
+		if(c != '=') {
+			# no value for this attr
+			al = (name, "") :: al;
+			continue attrloop;
+		}
+		# skip whitespace
+		if(++i == nb)
+			break attrloop;
+		c = int b[i];
+		while(c<NCTYPE && int (ctype[c]&W)) {
+			if(++i == nb)
+				break attrloop;
+			c = int b[i];
+		}
+		# gather value
+		quote := 0;
+		if(c == '\'' || c == '"') {
+			quote = c;
+			i++;
+		}
+		val := "";
+		nv := 0;
+	valloop:
+		while(i < nb) {
+			case charset{
+			Latin1 =>
+				c = int b[i++];
+			UTF8 =>
+				j: int;
+				(c, j, nil) = sys->byte2char(b, i);
+				i += j;
+			}
+			if(c == '>') {
+				if(quote) {
+					# c might be part of string (though not good style)
+					# but if line ends before close quote, assume
+					# there was an unmatched quote
+					for(k := i; k < nb; k++) {
+						c = int b[k];
+						if(c == quote) {
+							val[nv++] = '>';
+							continue valloop;
+						}
+						if(c == '\n') {
+							i--;
+							break valloop;
+						}
+					}
+				}
+				i--;
+				break valloop;
+			}
+			if(quote) {
+				if(c == quote)
+					break valloop;
+				if(c == '\n')
+					continue valloop;
+				if(c == '\t' || c == '\r')
+					c = ' ';
+			}
+			else {
+				if(c<NCTYPE && int (ctype[c]&W))
+					break valloop;
+			}
+			if(c == '&')
+				(c, i) = ampersand(b, i);
+			val[nv++] = c;
+		}
+		al = (name, val) :: al;
+		if(i < nb)
+			c = int b[i];
+	}
+	ans.attr = al;
+	return (ans, i);
+}
+
+ampersand(b: array of byte, i: int): (int, int)
+{
+	starti := i;
+	c := 0;
+	nb := len b;
+	if(i >= nb)
+		return ('?', i);
+	fnd := 0;
+	ans := 0;
+	if(int b[i] == '#'){
+		i++;
+		while(i<nb){
+			d := int b[i];
+			if(!(int (ctype[d]&D)))
+				break;
+			c = c*10 + d-'0';
+			i++;
+		}
+		if(0<c && c<256) {
+			if(c==160)
+				c = ' ';   # non-breaking space
+			ans = c;
+			fnd = 1;
+		}
+	}
+	else {
+		s := "";
+		k := 0;
+		c = int b[i];
+		if(int (ctype[c]&(L|U))) {
+			while(i<nb) {
+				c = int b[i];
+				if(!(int (ctype[c]&(L|U|D|N))))
+					break;
+				s[k++] = c;
+				i++;
+			}
+		}
+		(fnd, ans) = T->lookup(chartab, s);
+	}
+	if(!fnd)
+		return ('&', starti);
+	if(i<nb && (int b[i]==';' || int b[i]=='\n'))
+		i++;
+	return (ans, i);
+}
+
+lowercase(b: array of byte, istart, iend: int): string
+{
+	l := "";
+	j := 0;
+	for(i:=istart; i<iend; i++) {
+		c := int b[i];
+		if(c < NCTYPE && int (ctype[c]&U))
+			l[j] = c-'A'+'a';
+		else
+			l[j] = c;
+		j++;
+	}
+	return l;
+}
+
+uppercase(s: string): string
+{
+	l := "";
+
+	for(i:=0; i<len s; i++) {
+		c := s[i];
+		if(c < NCTYPE && int (ctype[c]&L))
+			l[i] = c+'A'-'a';
+		else
+			l[i] = c;
+	}
+	return l;
+}
+
+attrvalue(attr: list of Attr, name: string): (int, string)
+{
+	while(attr != nil){
+		a := hd attr;
+		if(a.name == name)
+			return (1, a.value); 
+		attr = tl attr;
+	}
+	return (0, "");
+}
+
+globalattr(html: array of ref Lex, tag: int, attr: string): (int, string)
+{
+	for(i:=0; i<len html; i++)
+		if(html[i].tag == tag)
+			return attrvalue(html[i].attr, attr);
+	return (0, "");
+}
+
+isbreak(h: array of ref Lex, i: int): int
+{
+	for(; i<len h; i++){
+		case h[i].tag{
+		Th1 or Th2 or Th3 or Th4 or Th5 or Th6 or
+		Tbr or Tp or Tbody or Taddress or Tblockquote or
+		Tul or Tdl or Tdir or Tmenu or Tol or Tpre or Thr or Tform =>
+			return 1;
+		Data =>
+			return 0;
+		}
+	}
+	return 0;
+}
+
+# for debugging
+lex2string(l: ref Lex): string
+{
+	ans := "";
+	tag := l.tag;
+	if(tag == HTML->Data)
+		ans = "'" + l.text + "'";
+	else {
+		ans = "<";
+		if(tag >= RBRA) {
+			tag -= RBRA;
+			ans = ans + "/";
+		}
+		tname := T->revlookup(htmlstringtab, tag);
+		if(tname != nil)
+				ans = ans + uppercase(tname);
+		for(al := l.attr; al != nil; al = tl al) {
+			a := hd al;
+			ans = ans + " " + a.name + "='" + a.value + "'";
+		}
+		ans = ans + ">";
+	}
+	return ans;
+}
--- /dev/null
+++ b/appl/lib/ida/NOTICE
@@ -1,0 +1,33 @@
+The IDA software was originally based on a version included with MIT's sfsnet software
+which contains the copyright notice further down.
+This Limbo version and interface is
+	Copyright © 2006 C H Forsyth, Vita Nuova Holdings Limited
+(forsyth@vitanuova.com wrt bugs etc)
+This version was revised with reference to Rabin's paper, but is similar
+enough at the core that I'll include the copyright too.
+The same terms and conditions apply.
+
+Copyright (c) 2000 Frans Kaashoek, Frank Dabek, 
+    		   Massachusetts Institute of Technology
+
+Copyright (c) 2001 Frans Kaashoek, Frank Dabek, Joshua Cates,
+		   Massachusetts Institute of Technology
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+++ b/appl/lib/ida/ida.b
@@ -1,0 +1,228 @@
+implement Ida;
+
+#
+# M Rabin, ``Efficient Dispersal of Information for Security,
+#	Load Balancing, and Fault Tolerance'', JACM 36(2), April 1989, pp. 335-348
+#	the scheme used below is that suggested at the top of page 340
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "rand.m";
+	rand: Rand;
+
+include "ida.m";
+
+invtab: array of int;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	rand = load Rand Rand->PATH;
+	rand->init(sys->pctl(0, nil)^(sys->millisec()<<8));
+	# the table is in a separate module so that
+	# the copy in the module initialisation section is discarded
+	# after unloading, preventing twice the space being used
+	idatab := load Idatab Idatab->PATH;
+	invtab = idatab->init();	# the big fella
+	idatab = nil;
+}
+
+Field: con 65537;
+Fmax: con Field-1;
+
+div(a, b: int): int
+{
+	return mul(a, invtab[b]);
+}
+
+mul(a, b: int): int
+{
+	if(a == Fmax && b == Fmax)	# avoid overflow
+		return 1;
+	return int((big(a*b) & 16rFFFFFFFF) % big Field);
+}
+
+sub(a, b: int): int
+{
+	return ((a-b)+Field)%Field;
+}
+
+add(a, b: int): int
+{
+	return (a + b)%Field;
+}
+
+#
+# return a fragment representing the encoded version of data
+#
+fragment(data: array of byte, m: int): ref Frag
+{
+	nb := len data;
+	nw := (nb+1)/2;
+	a := array[m] of {* => rand->rand(Fmax)+1};	# no zero elements
+	f := array[(nw + m - 1)/m] of int;
+	o := 0;
+	i := 0;
+	for(k := 0; k < len f; k++){
+		c := 0;
+		for(j := 0; j < m && i < nb; j++){
+			b := int data[i++] << 8;
+			if(i < nb)
+				b |= int data[i++];
+			c = add(c, mul(b, a[j]));
+		}
+		f[o++] = c;
+	}
+	return ref Frag(nb, m, a, f, nil);
+}
+
+#
+# return the data encoded by the given set of fragments
+#
+reconstruct(frags: array of ref Frag): (array of byte, string)
+{
+	if(len frags < 1 || len frags < (m := frags[0].m))
+		return (nil, "too few fragments");
+	fraglen := len frags[0].enc;
+
+	a := array[m] of array of int;
+	for(j := 0; j < len a; j++){
+		a[j] = frags[j].a;
+		if(len a[j] != m)
+			return (nil, "inconsistent encoding matrix");
+		if(len frags[j].enc != fraglen)
+			return (nil, "inconsistent fragments");
+	}
+	ainv := minvert(a);
+	out := array[fraglen*2*m] of byte;
+	o := 0;
+	for(k := 0; k < fraglen; k++){
+		for(i := 0; i < m; i++){
+			row := ainv[i];
+			b := 0;
+			for(j = 0; j < m; j++)
+				b = add(b, mul(frags[j].enc[k], row[j]));
+			if((b>>16) != 0)
+				return (nil, "corrupt output");
+			out[o++] = byte (b>>8);
+			out[o++] = byte b;
+		}
+	}
+	if(frags[0].dlen < len out)
+		out = out[0: frags[0].dlen];
+	return (out, nil);
+}
+
+#
+# Rabin's paper gives a way of building an encoding matrix that can then
+# be inverted in O(m^2) operations, compared to O(m^3) for the following,
+# but m is small enough it doesn't seem worth the added complication,
+# and it's only done once per set
+#
+minvert(a: array of array of int): array of array of int
+{
+	m := len a;	# it's square
+	out := array[m] of {* => array[m*2] of {* => 0}};
+	for(r := 0; r < m; r++){
+		out[r][0:] = a[r];
+		out[r][m+r] = 1;	# identity matrix
+	}
+	for(r = 0; r < m; r++){
+		x := out[r][r];	# by construction, cannot be zero, unless later corrupted
+		for(c := 0; c < 2*m; c++)
+			out[r][c] = div(out[r][c], x);
+		for(r1 := 0; r1 < m; r1++)
+			if(r1 != r){
+				y := div(out[r1][r], out[r][r]);
+				for(c = 0; c < 2*m; c++)
+					out[r1][c] = sub(out[r1][c], mul(y, out[r][c]));
+			}
+	}
+	for(r = 0; r < m; r++)
+		out[r] = out[r][m:];
+	return out;
+}
+
+Val: adt {
+	v:	int;
+	n:	int;
+};
+
+addval(vl: list of ref Val, v: int): list of ref Val
+{
+	for(l := vl; l != nil; l = tl l)
+		if((hd l).v == v){
+			(hd l).n++;
+			return vl;
+		}
+	return ref Val(v, 1) :: vl;
+}
+
+mostly(vl: list of ref Val): ref Val
+{
+	if(len vl == 1)
+		return hd vl;
+	v: ref Val;
+	for(; vl != nil; vl = tl vl)
+		if(v == nil || (hd vl).n > v.n)
+			v = hd vl;
+	return v;
+}
+
+#
+# return a consistent set of Frags: all parameters agree with the majority,
+# and obviously bad fragments have been discarded
+#
+# in the absence of error, they  should all have the same value, so lists are fine;
+# could separately return the discarded ones, out of interest
+#
+consistent(frags: array of ref Frag): array of ref Frag
+{
+	t := array[len frags] of ref Frag;
+	t[0:] = frags;
+	frags = t;
+	ds: list of ref Val;	# data size
+	ms: list of ref Val;
+	fls: list of ref Val;
+	for(i := 0; i < len frags; i++){
+		f := frags[i];
+		if(f != nil){
+			ds = addval(ds, f.dlen);
+			ms = addval(ms, f.m);
+			fls = addval(fls, len f.enc);
+		}
+	}
+	dv := mostly(ds);
+	mv := mostly(ms);
+	flv := mostly(fls);
+	if(mv == nil || flv == nil || dv == nil)
+		return nil;
+	for(i = 0; i < len frags; i++){
+		f := frags[i];
+		if(f == nil || f.m != mv.v || f.m != len f.a || len f.enc != flv.v || f.dlen != dv.v || badfrag(f)){	# inconsistent: drop it
+			if(i+1 < len frags)
+				frags[i:] = frags[i+1:];
+			frags = frags[0:len frags-1];
+		}
+	}
+	if(len frags == 0)
+		return nil;
+	return frags;
+}
+
+badfrag(f: ref Frag): int
+{
+	for(i := 0; i < len f.a; i++){
+		v := f.a[i];
+		if(v <= 0 || v >= Field)
+			return 1;
+	}
+	for(i = 0; i < len f.a; i++){
+		v := f.enc[i];
+		if(v == 0 || v >= Field)
+			return 1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/appl/lib/ida/idatab.b
@@ -1,0 +1,65549 @@
+implement Idatab;
+
+include "ida.m";
+
+init(): array of int
+{
+	return invtab;
+}
+
+invtab := array[] of {
+0,
+1,
+32769,
+21846,
+49153,
+26215,
+10923,
+18725,
+57345,
+7282,
+45876,
+5958,
+38230,
+15124,
+42131,
+30584,
+61441,
+30841,
+3641,
+10348,
+22938,
+49933,
+2979,
+45591,
+19115,
+5243,
+7562,
+24273,
+53834,
+22599,
+15292,
+21141,
+63489,
+1986,
+48189,
+3745,
+34589,
+19484,
+5174,
+26887,
+11469,
+44757,
+57735,
+25910,
+34258,
+53886,
+55564,
+58565,
+42326,
+2675,
+35390,
+32126,
+3781,
+51935,
+44905,
+14299,
+26917,
+25295,
+44068,
+5554,
+7646,
+56942,
+43339,
+38490,
+64513,
+42347,
+993,
+5869,
+56863,
+15197,
+34641,
+48922,
+50063,
+38604,
+9742,
+45439,
+2587,
+19576,
+46212,
+9955,
+38503,
+8091,
+55147,
+61589,
+61636,
+32383,
+12955,
+7533,
+17129,
+45655,
+26943,
+11523,
+27782,
+7047,
+62051,
+15177,
+21163,
+41214,
+34106,
+662,
+17695,
+38284,
+16063,
+45176,
+34659,
+23094,
+58736,
+1225,
+55221,
+21044,
+39918,
+50186,
+46227,
+22039,
+45416,
+35333,
+22034,
+30808,
+2777,
+51218,
+3823,
+42247,
+28471,
+14919,
+54438,
+14156,
+19245,
+39219,
+65025,
+52328,
+53942,
+23013,
+33265,
+38928,
+35703,
+17962,
+61200,
+44967,
+40367,
+22160,
+50089,
+63213,
+24461,
+60954,
+57800,
+43842,
+19302,
+44583,
+4871,
+5718,
+55488,
+21701,
+34062,
+54400,
+9788,
+30443,
+23106,
+12523,
+37746,
+61003,
+52020,
+6513,
+36814,
+29753,
+60342,
+26612,
+63563,
+6279,
+30818,
+11246,
+48960,
+52123,
+39246,
+28412,
+36535,
+749,
+41333,
+23697,
+55596,
+25629,
+46240,
+4345,
+38530,
+62672,
+13891,
+43219,
+36292,
+50467,
+63794,
+12830,
+40357,
+62792,
+43350,
+33957,
+20607,
+57807,
+17053,
+52230,
+331,
+988,
+41616,
+23802,
+19142,
+59403,
+40800,
+61381,
+22588,
+48757,
+50098,
+48604,
+11547,
+31992,
+29368,
+38153,
+33381,
+5182,
+60379,
+21745,
+10522,
+12868,
+19959,
+12455,
+25093,
+34091,
+55882,
+36992,
+43788,
+24829,
+22708,
+60958,
+50435,
+28371,
+11017,
+25596,
+15404,
+11713,
+34157,
+25164,
+25609,
+44971,
+34680,
+61186,
+53892,
+2697,
+47004,
+535,
+40228,
+796,
+27219,
+64221,
+7078,
+57965,
+42391,
+45850,
+52378,
+32640,
+65281,
+32641,
+26164,
+58958,
+26971,
+2511,
+44275,
+35385,
+49401,
+10387,
+19464,
+37064,
+50620,
+30454,
+8981,
+64086,
+30600,
+3841,
+55252,
+42182,
+52952,
+11120,
+11080,
+2349,
+57813,
+41981,
+64375,
+20379,
+44999,
+5059,
+30477,
+53206,
+28900,
+28800,
+21921,
+13738,
+9651,
+57261,
+55060,
+40433,
+35204,
+43912,
+2859,
+3507,
+27744,
+59876,
+43619,
+34607,
+17031,
+63818,
+27200,
+17505,
+4894,
+58750,
+47990,
+28870,
+11553,
+52974,
+39030,
+7698,
+18873,
+11991,
+63270,
+31844,
+26010,
+22254,
+36025,
+43015,
+18407,
+60899,
+47645,
+50706,
+30171,
+64541,
+13306,
+198,
+64550,
+60420,
+35908,
+40496,
+15409,
+27615,
+5623,
+29192,
+24480,
+61501,
+58830,
+19107,
+19623,
+55469,
+14206,
+15676,
+51036,
+22722,
+33143,
+32115,
+53435,
+59039,
+44617,
+62214,
+27798,
+60764,
+45583,
+16795,
+23120,
+59183,
+34941,
+35928,
+19265,
+47043,
+31336,
+7143,
+39714,
+4973,
+54378,
+44869,
+18146,
+50778,
+58002,
+48410,
+31897,
+11821,
+6415,
+24036,
+52947,
+13073,
+31396,
+40212,
+21675,
+30130,
+49747,
+61134,
+43072,
+13815,
+61672,
+41233,
+41295,
+7671,
+26115,
+1991,
+32934,
+10235,
+494,
+12976,
+20808,
+8662,
+11901,
+46998,
+9571,
+27833,
+62470,
+19645,
+20400,
+24997,
+63459,
+14989,
+11294,
+56968,
+57147,
+51640,
+25049,
+51078,
+24302,
+24244,
+38542,
+49036,
+15996,
+21071,
+14684,
+19584,
+51845,
+17497,
+49459,
+20318,
+2591,
+55045,
+62958,
+29363,
+43641,
+14614,
+5261,
+40342,
+6434,
+16123,
+42748,
+14861,
+38996,
+53406,
+45315,
+9131,
+49814,
+1906,
+27941,
+46270,
+18496,
+57690,
+21894,
+50925,
+45183,
+15412,
+11354,
+53204,
+30479,
+39979,
+57986,
+35825,
+46954,
+20383,
+38277,
+53839,
+12798,
+16279,
+7702,
+57013,
+38625,
+26020,
+49847,
+32145,
+12582,
+55465,
+45573,
+42180,
+55254,
+5336,
+17340,
+21664,
+30593,
+2171,
+26946,
+47565,
+34117,
+12919,
+23502,
+53609,
+33036,
+48452,
+20114,
+55301,
+398,
+52562,
+46378,
+53801,
+64879,
+26136,
+3539,
+2093,
+61751,
+37003,
+53964,
+46979,
+22925,
+47440,
+26189,
+10043,
+16320,
+52327,
+65409,
+39220,
+49089,
+35250,
+13082,
+11282,
+29479,
+53162,
+46254,
+30064,
+34024,
+51377,
+54906,
+43941,
+50461,
+47505,
+57469,
+33326,
+37962,
+7899,
+9732,
+38732,
+18532,
+245,
+25310,
+8543,
+15227,
+12159,
+37259,
+14658,
+32043,
+23294,
+15300,
+43531,
+34689,
+47086,
+27626,
+64582,
+21091,
+18436,
+26476,
+20147,
+5560,
+36252,
+5540,
+6589,
+33943,
+17117,
+61675,
+38668,
+53759,
+6868,
+64956,
+43730,
+42958,
+47968,
+55268,
+53328,
+35298,
+6657,
+48007,
+64622,
+26603,
+20174,
+14450,
+28282,
+14400,
+11319,
+43729,
+64973,
+6869,
+28553,
+37594,
+19269,
+61399,
+21213,
+27530,
+4562,
+52985,
+17410,
+17602,
+40560,
+21956,
+23351,
+34198,
+22175,
+34522,
+6127,
+13872,
+30424,
+29938,
+7934,
+54578,
+60879,
+50072,
+3455,
+41284,
+19801,
+31909,
+4505,
+13600,
+20634,
+41521,
+42306,
+2447,
+62138,
+29375,
+43409,
+23995,
+38098,
+14435,
+25247,
+38545,
+29046,
+26487,
+38047,
+19515,
+62828,
+3849,
+62525,
+42205,
+10664,
+38764,
+47166,
+31635,
+20371,
+15922,
+56409,
+13005,
+40590,
+11127,
+10702,
+50781,
+23573,
+54276,
+52774,
+41972,
+60084,
+63218,
+29094,
+56591,
+39543,
+25353,
+17710,
+47854,
+26135,
+65039,
+53802,
+6653,
+31430,
+99,
+47843,
+32275,
+20893,
+30210,
+3832,
+17954,
+55055,
+20248,
+40924,
+40473,
+24832,
+46576,
+56022,
+35580,
+15392,
+14596,
+33975,
+12240,
+30122,
+63519,
+63426,
+29415,
+61423,
+42322,
+42165,
+42580,
+3995,
+60503,
+3035,
+7103,
+9457,
+7838,
+4432,
+25518,
+10343,
+11361,
+8532,
+49340,
+10097,
+48826,
+28620,
+59486,
+25750,
+62288,
+24194,
+55077,
+34571,
+31107,
+8388,
+13899,
+52209,
+30382,
+51513,
+55560,
+36836,
+41166,
+21238,
+11560,
+34541,
+62360,
+42241,
+50239,
+61198,
+17964,
+20824,
+42401,
+899,
+56290,
+63206,
+15668,
+21190,
+36340,
+22024,
+19857,
+36281,
+35255,
+60216,
+27189,
+22111,
+55203,
+62538,
+9073,
+14251,
+25389,
+21407,
+29001,
+175,
+24205,
+49480,
+48717,
+63013,
+38679,
+30555,
+35976,
+53936,
+12018,
+37129,
+59242,
+57614,
+39305,
+40456,
+15698,
+10880,
+20106,
+45799,
+43606,
+6477,
+15065,
+32726,
+57642,
+63587,
+30567,
+19196,
+21536,
+63344,
+39676,
+60994,
+30836,
+34237,
+53385,
+837,
+53416,
+15612,
+36604,
+34892,
+45826,
+11795,
+33764,
+59323,
+16467,
+34628,
+37886,
+25308,
+247,
+38319,
+6488,
+3445,
+10404,
+56046,
+4331,
+33299,
+38719,
+14410,
+23499,
+31997,
+37554,
+8182,
+46685,
+6384,
+31235,
+21362,
+42591,
+19058,
+10200,
+4813,
+45267,
+23126,
+64498,
+26582,
+40263,
+16006,
+5647,
+57752,
+28484,
+53650,
+61342,
+1423,
+25820,
+47398,
+45293,
+54129,
+25539,
+40578,
+12151,
+783,
+12122,
+61397,
+19271,
+50497,
+24518,
+57685,
+7998,
+28464,
+43304,
+43485,
+7342,
+6793,
+9792,
+6546,
+58691,
+59621,
+41517,
+23532,
+57498,
+47413,
+10159,
+54398,
+34064,
+39581,
+60291,
+16707,
+31479,
+58112,
+47450,
+9600,
+54589,
+905,
+7307,
+25658,
+35399,
+26425,
+20171,
+39472,
+3217,
+20102,
+40830,
+19087,
+21374,
+27896,
+40199,
+47724,
+19498,
+57169,
+26703,
+14925,
+55426,
+33690,
+37334,
+36483,
+24907,
+9981,
+953,
+44448,
+46739,
+1169,
+23135,
+729,
+9248,
+3055,
+28845,
+63650,
+10947,
+869,
+58231,
+57661,
+55360,
+55227,
+7706,
+46473,
+5677,
+5599,
+26602,
+64964,
+48008,
+12650,
+52758,
+45284,
+28993,
+5835,
+50681,
+49135,
+23477,
+47966,
+42960,
+41429,
+51907,
+21587,
+59688,
+24286,
+6399,
+31469,
+40908,
+62523,
+3851,
+30915,
+61275,
+17658,
+52081,
+35032,
+13010,
+21892,
+57692,
+2566,
+48841,
+19931,
+6291,
+43300,
+60501,
+3997,
+55555,
+894,
+21090,
+64988,
+27627,
+54306,
+2668,
+53236,
+8670,
+45078,
+10832,
+7418,
+48065,
+59221,
+33854,
+23043,
+13473,
+36184,
+56551,
+17886,
+49827,
+31253,
+39228,
+63991,
+11751,
+49572,
+59573,
+33940,
+16518,
+16902,
+24226,
+53603,
+10057,
+10446,
+60419,
+65205,
+199,
+23922,
+26281,
+54030,
+23189,
+66,
+59669,
+13305,
+65208,
+30172,
+13068,
+20140,
+34538,
+36795,
+33815,
+42733,
+63644,
+57190,
+51270,
+19980,
+26982,
+64173,
+56258,
+9205,
+44231,
+55962,
+23720,
+24988,
+45863,
+53422,
+37790,
+17558,
+8160,
+55010,
+58932,
+42346,
+65473,
+38491,
+19610,
+20931,
+57313,
+6369,
+17625,
+56447,
+6541,
+45933,
+5641,
+62181,
+47508,
+18770,
+26581,
+64717,
+23127,
+27071,
+15032,
+47629,
+17012,
+49043,
+58457,
+7574,
+27453,
+17993,
+54739,
+13282,
+57999,
+10705,
+56521,
+58828,
+61503,
+21825,
+16663,
+63371,
+18981,
+33170,
+36718,
+46178,
+4866,
+20738,
+19366,
+51410,
+9266,
+60142,
+32891,
+63946,
+12655,
+41350,
+37040,
+53466,
+40382,
+27444,
+38848,
+60192,
+51398,
+59535,
+7329,
+63419,
+48790,
+4349,
+11647,
+1447,
+7650,
+11976,
+54534,
+5106,
+50113,
+47249,
+23543,
+15681,
+13813,
+43074,
+32291,
+1789,
+43314,
+2381,
+9218,
+52287,
+13238,
+2491,
+42842,
+45349,
+2780,
+8037,
+18126,
+27312,
+2770,
+36802,
+36063,
+33033,
+49740,
+13788,
+41327,
+16926,
+63606,
+62380,
+19334,
+26670,
+59648,
+59828,
+3434,
+57105,
+32478,
+22639,
+21865,
+25786,
+21479,
+63686,
+23984,
+44288,
+27634,
+8012,
+26664,
+58172,
+17649,
+60425,
+36097,
+48049,
+56772,
+25299,
+32311,
+11199,
+46070,
+13404,
+10087,
+8427,
+7225,
+5343,
+14141,
+31889,
+7200,
+53925,
+38428,
+58186,
+54633,
+20378,
+65255,
+41982,
+36203,
+31334,
+47045,
+4605,
+18797,
+897,
+42403,
+63858,
+63468,
+35590,
+43375,
+15450,
+13765,
+23943,
+2281,
+2557,
+59261,
+15427,
+8705,
+10969,
+8801,
+44355,
+20280,
+27827,
+10978,
+30922,
+44444,
+47103,
+17099,
+51254,
+43856,
+35209,
+17261,
+26171,
+35832,
+7871,
+6936,
+48457,
+15212,
+24733,
+14969,
+38452,
+3967,
+10588,
+27289,
+15666,
+63208,
+50871,
+25036,
+59918,
+34496,
+52969,
+20642,
+38019,
+42669,
+47849,
+48723,
+28394,
+35021,
+46460,
+6800,
+107,
+10317,
+30178,
+53529,
+11305,
+21153,
+30985,
+33992,
+26842,
+31069,
+26374,
+47456,
+30040,
+54473,
+40835,
+44766,
+40822,
+19049,
+37751,
+39986,
+39059,
+45392,
+32532,
+52041,
+24924,
+14523,
+17026,
+46012,
+41111,
+51792,
+11593,
+42526,
+29927,
+31414,
+61737,
+34693,
+63666,
+64031,
+38191,
+53871,
+9170,
+5332,
+28708,
+19382,
+50715,
+23583,
+50068,
+48586,
+45150,
+42954,
+6528,
+7961,
+40441,
+60973,
+12554,
+39271,
+27678,
+20295,
+1890,
+38332,
+32743,
+5351,
+50464,
+58159,
+58724,
+44555,
+16600,
+27138,
+40194,
+26387,
+24899,
+20986,
+54471,
+30042,
+53479,
+31609,
+9420,
+14547,
+23539,
+61064,
+26717,
+52540,
+32593,
+45445,
+16572,
+8855,
+35293,
+23927,
+53358,
+45836,
+7077,
+65288,
+27220,
+26901,
+30309,
+36095,
+60427,
+15715,
+48645,
+32818,
+54507,
+56690,
+19508,
+48906,
+17802,
+43215,
+45546,
+15105,
+5064,
+1916,
+46735,
+8977,
+46420,
+60296,
+18599,
+10124,
+22481,
+20462,
+46847,
+53005,
+45413,
+12416,
+37269,
+23288,
+37068,
+28011,
+4851,
+17790,
+19230,
+7696,
+39032,
+7298,
+42983,
+49756,
+16975,
+6120,
+26340,
+15061,
+56257,
+64528,
+26983,
+31713,
+19081,
+47476,
+35952,
+63480,
+61426,
+21161,
+15179,
+53851,
+60866,
+21290,
+35172,
+34766,
+63636,
+63020,
+15708,
+34286,
+55633,
+36320,
+2224,
+37497,
+26177,
+3919,
+28640,
+2216,
+45466,
+12759,
+18866,
+37940,
+39792,
+38449,
+27397,
+4266,
+19722,
+24670,
+27272,
+37817,
+56615,
+24413,
+34611,
+14310,
+40850,
+29743,
+23024,
+12875,
+7478,
+31144,
+30519,
+12097,
+43398,
+60307,
+21784,
+50054,
+10715,
+48322,
+27211,
+4194,
+829,
+39718,
+40334,
+58873,
+13732,
+15191,
+43248,
+58525,
+14060,
+27780,
+11525,
+18418,
+36856,
+20583,
+45470,
+10619,
+22544,
+5780,
+8050,
+50039,
+29067,
+31180,
+5760,
+53889,
+1087,
+57888,
+44415,
+30599,
+65266,
+8982,
+40098,
+10412,
+15855,
+53969,
+63198,
+33218,
+59428,
+28145,
+26152,
+31603,
+63118,
+7834,
+37667,
+10595,
+61561,
+18170,
+62191,
+11012,
+43127,
+42697,
+59842,
+50909,
+21194,
+50396,
+42020,
+30108,
+62125,
+46363,
+59563,
+43824,
+39552,
+60370,
+61212,
+31269,
+57119,
+37305,
+31206,
+39894,
+61625,
+45463,
+28752,
+43472,
+53131,
+47269,
+8712,
+32856,
+18144,
+44871,
+55713,
+24740,
+44389,
+57127,
+38190,
+64275,
+63667,
+52108,
+34180,
+48046,
+24983,
+17988,
+48947,
+26968,
+59351,
+6009,
+10066,
+51333,
+59281,
+29621,
+37659,
+28807,
+15018,
+52421,
+25871,
+20228,
+47039,
+7849,
+37762,
+5440,
+6421,
+10053,
+39288,
+55668,
+3501,
+21803,
+42469,
+36007,
+34919,
+40301,
+51545,
+16363,
+32365,
+28821,
+11750,
+64562,
+39229,
+48052,
+38036,
+9598,
+47452,
+10768,
+32663,
+31672,
+5774,
+19838,
+61412,
+30497,
+28880,
+15418,
+23595,
+49887,
+31867,
+59461,
+49917,
+33187,
+8281,
+26708,
+60817,
+7806,
+55817,
+18302,
+23415,
+17446,
+14647,
+22913,
+41184,
+38666,
+61677,
+16882,
+15835,
+62430,
+49101,
+41002,
+28613,
+17314,
+54800,
+18943,
+3547,
+12654,
+64466,
+32892,
+2633,
+51928,
+45691,
+3244,
+14199,
+34491,
+56602,
+5202,
+44742,
+28023,
+46158,
+34934,
+43773,
+49418,
+48123,
+52128,
+25335,
+7205,
+46539,
+44518,
+25841,
+48767,
+8603,
+18777,
+4053,
+4091,
+22183,
+56111,
+45039,
+3192,
+4886,
+48386,
+51502,
+10681,
+34561,
+54064,
+51456,
+9529,
+13019,
+5100,
+47718,
+35175,
+36356,
+55402,
+36792,
+11563,
+54141,
+32249,
+37541,
+13291,
+16474,
+52900,
+39123,
+8003,
+43373,
+35592,
+56396,
+28876,
+38306,
+14242,
+27991,
+26825,
+13147,
+30671,
+11114,
+33480,
+50407,
+12910,
+55594,
+23699,
+46936,
+55415,
+12084,
+59833,
+21387,
+45538,
+23364,
+20289,
+24042,
+38844,
+62599,
+33160,
+34314,
+6061,
+49397,
+63467,
+64366,
+42404,
+50644,
+58017,
+34735,
+12259,
+5523,
+61611,
+55553,
+3999,
+24135,
+14232,
+47244,
+21652,
+43201,
+54511,
+58268,
+3671,
+19580,
+36165,
+58748,
+4896,
+37835,
+3273,
+47142,
+62114,
+38515,
+62579,
+17776,
+53527,
+30180,
+11766,
+11491,
+28749,
+2219,
+56475,
+56251,
+37848,
+2252,
+27199,
+65232,
+17032,
+9901,
+52559,
+44731,
+62914,
+50416,
+41122,
+30928,
+48508,
+28201,
+29056,
+31273,
+23725,
+62436,
+4800,
+29350,
+60063,
+3773,
+33221,
+15753,
+36422,
+5910,
+12829,
+65349,
+50468,
+56974,
+45981,
+61523,
+42854,
+53209,
+19736,
+21933,
+34377,
+59817,
+10051,
+6423,
+20415,
+36368,
+42312,
+32191,
+10687,
+7071,
+13948,
+42192,
+52868,
+51130,
+23862,
+45212,
+9749,
+13263,
+61353,
+6550,
+46120,
+27649,
+40231,
+51765,
+27713,
+38024,
+16845,
+13520,
+18667,
+3459,
+51010,
+41829,
+45222,
+51475,
+37759,
+57542,
+33245,
+1099,
+22224,
+51083,
+56138,
+56326,
+33353,
+3359,
+44336,
+23888,
+33133,
+4663,
+4624,
+29730,
+34296,
+31987,
+47191,
+24944,
+31825,
+57159,
+38242,
+46336,
+33203,
+14222,
+61884,
+17785,
+61599,
+20293,
+27680,
+14680,
+60382,
+30913,
+3853,
+44843,
+56005,
+21606,
+35607,
+22516,
+35568,
+28446,
+13301,
+40347,
+32482,
+13315,
+24004,
+45193,
+6325,
+14536,
+26379,
+54192,
+22642,
+6878,
+47265,
+5055,
+35686,
+33213,
+58109,
+14102,
+57336,
+45773,
+44507,
+32609,
+23983,
+64404,
+21480,
+5093,
+53483,
+48296,
+58722,
+58161,
+43562,
+24854,
+29844,
+42224,
+12143,
+34545,
+35968,
+23263,
+48503,
+55638,
+20454,
+52107,
+64030,
+64276,
+34694,
+61898,
+48226,
+9682,
+63406,
+10370,
+8829,
+8336,
+58809,
+34528,
+17516,
+13713,
+6505,
+28579,
+10946,
+64634,
+28846,
+45241,
+1283,
+27206,
+57189,
+64533,
+42734,
+31022,
+35914,
+27880,
+21650,
+47246,
+63019,
+64158,
+34767,
+62162,
+60546,
+15722,
+447,
+37597,
+10545,
+36871,
+32494,
+28636,
+46582,
+47928,
+27153,
+60472,
+1334,
+18803,
+26618,
+57204,
+4335,
+20572,
+22539,
+13530,
+5416,
+6026,
+3709,
+41390,
+56801,
+25413,
+62379,
+64417,
+16927,
+27632,
+44290,
+51549,
+39505,
+35729,
+18092,
+10951,
+61044,
+39437,
+8943,
+52180,
+57682,
+2763,
+48395,
+20028,
+19614,
+30566,
+64764,
+57643,
+38644,
+9698,
+24786,
+21354,
+62555,
+5827,
+16970,
+13181,
+8259,
+10260,
+8451,
+62165,
+12113,
+27749,
+59570,
+43447,
+37797,
+61909,
+5223,
+52403,
+62978,
+6278,
+65371,
+26613,
+32868,
+17934,
+11961,
+39607,
+45909,
+40361,
+27015,
+54168,
+44363,
+2047,
+33,
+17349,
+62603,
+59639,
+39421,
+395,
+32604,
+35876,
+15086,
+28810,
+6534,
+45912,
+10070,
+19966,
+17269,
+23123,
+51166,
+4548,
+49676,
+54162,
+54135,
+62043,
+31822,
+54413,
+28595,
+22454,
+25635,
+35487,
+9990,
+22507,
+13491,
+63425,
+64855,
+30123,
+28129,
+40989,
+37371,
+41564,
+54884,
+18674,
+27981,
+57357,
+11860,
+21738,
+12494,
+48822,
+55700,
+27659,
+26711,
+3929,
+18895,
+11325,
+8779,
+61455,
+4080,
+61459,
+27505,
+53732,
+29466,
+57429,
+21173,
+1985,
+65505,
+21142,
+52014,
+26905,
+9805,
+51523,
+43234,
+42320,
+61425,
+64167,
+35953,
+44625,
+41581,
+14055,
+60992,
+39678,
+36039,
+24501,
+55735,
+45023,
+35589,
+64365,
+63859,
+49398,
+23754,
+44703,
+9385,
+10328,
+46059,
+14988,
+65127,
+24998,
+44332,
+47460,
+46304,
+17336,
+7516,
+23323,
+56583,
+57969,
+8506,
+15247,
+57290,
+47139,
+61997,
+501,
+3787,
+44171,
+46495,
+2844,
+41765,
+23386,
+60138,
+29540,
+6641,
+47057,
+61768,
+36022,
+38121,
+36641,
+61029,
+9540,
+29414,
+64854,
+63520,
+13492,
+43681,
+30429,
+41100,
+48789,
+64454,
+7330,
+42259,
+51756,
+16585,
+3087,
+18359,
+43239,
+23089,
+55215,
+2433,
+13760,
+10369,
+63661,
+9683,
+2796,
+25705,
+55929,
+4633,
+46799,
+30071,
+17403,
+49214,
+5265,
+31973,
+7095,
+39096,
+17171,
+20675,
+56349,
+18520,
+30588,
+26733,
+55970,
+20191,
+57440,
+13722,
+11009,
+19424,
+28925,
+30096,
+2307,
+25699,
+60624,
+62536,
+55205,
+36433,
+18980,
+64478,
+16664,
+24395,
+35926,
+34943,
+483,
+38592,
+54016,
+33492,
+42245,
+3825,
+41574,
+5988,
+8241,
+27267,
+28787,
+2553,
+12549,
+57825,
+60498,
+56393,
+43991,
+44540,
+53711,
+40609,
+35655,
+39675,
+64760,
+21537,
+16332,
+48914,
+15989,
+33663,
+28909,
+21657,
+20605,
+33959,
+14577,
+4609,
+29187,
+58912,
+24053,
+6619,
+45867,
+34014,
+55785,
+21421,
+19131,
+55443,
+36896,
+1390,
+20072,
+36787,
+1713,
+9063,
+41193,
+13656,
+29216,
+1385,
+28041,
+18401,
+43760,
+50800,
+20846,
+49285,
+41684,
+24870,
+23274,
+6894,
+26596,
+53432,
+47080,
+8463,
+55263,
+31803,
+50827,
+31190,
+25011,
+9667,
+9254,
+13335,
+21904,
+29824,
+42516,
+29914,
+27688,
+1717,
+38339,
+61321,
+11538,
+16239,
+47592,
+44088,
+42850,
+43701,
+6145,
+12893,
+60440,
+43508,
+10185,
+31843,
+65219,
+11992,
+56352,
+22144,
+61670,
+13817,
+28112,
+4006,
+55512,
+13332,
+34222,
+29086,
+30166,
+41593,
+1178,
+62981,
+62896,
+50817,
+49963,
+56793,
+12924,
+28386,
+35331,
+45418,
+50061,
+48924,
+54819,
+38368,
+47318,
+23035,
+56949,
+6702,
+40166,
+37812,
+37112,
+36982,
+27945,
+36381,
+7165,
+35440,
+2159,
+39839,
+37693,
+48713,
+25693,
+3600,
+54600,
+59731,
+17184,
+19214,
+37478,
+29093,
+64887,
+60085,
+26740,
+10189,
+24460,
+65396,
+50090,
+20991,
+27178,
+50870,
+64327,
+15667,
+64806,
+56291,
+45845,
+35071,
+42578,
+42167,
+42177,
+33217,
+64080,
+53970,
+35750,
+31929,
+33258,
+31734,
+24510,
+17795,
+62242,
+54456,
+279,
+7725,
+56979,
+39651,
+45706,
+44740,
+5204,
+33909,
+5283,
+34047,
+3945,
+62399,
+55322,
+40482,
+16724,
+37121,
+6429,
+38253,
+47623,
+37169,
+53254,
+54946,
+52214,
+10140,
+41620,
+46682,
+11093,
+5489,
+32865,
+15461,
+55234,
+22222,
+1101,
+56320,
+58249,
+41318,
+8436,
+25627,
+55598,
+21928,
+44304,
+50373,
+12773,
+41399,
+19746,
+45854,
+27282,
+17916,
+22994,
+36704,
+34913,
+3468,
+12092,
+56997,
+18682,
+7606,
+43655,
+45135,
+40542,
+40253,
+54791,
+19226,
+25470,
+34752,
+33108,
+5294,
+26649,
+46413,
+28525,
+7833,
+64074,
+31604,
+54357,
+58204,
+60452,
+12518,
+9513,
+29959,
+24573,
+17248,
+58414,
+59253,
+19006,
+10321,
+2128,
+51778,
+41906,
+54103,
+15759,
+56693,
+50812,
+57130,
+26392,
+14197,
+3246,
+50279,
+50044,
+23230,
+616,
+3400,
+19348,
+32822,
+23450,
+37927,
+15015,
+15089,
+62120,
+59533,
+51400,
+38421,
+45095,
+43345,
+11451,
+48261,
+52552,
+16996,
+37275,
+13421,
+14000,
+48303,
+27181,
+13187,
+27159,
+23728,
+52419,
+15020,
+62942,
+60005,
+28178,
+53186,
+55068,
+22383,
+39729,
+20411,
+35078,
+42293,
+36975,
+51644,
+22320,
+19993,
+42603,
+52298,
+1105,
+22696,
+37645,
+16266,
+31442,
+58789,
+14173,
+12462,
+18043,
+40030,
+15775,
+8513,
+9138,
+23006,
+13526,
+53324,
+20861,
+25896,
+17736,
+38565,
+261,
+21263,
+39849,
+47732,
+20508,
+15707,
+64157,
+63637,
+47247,
+50115,
+58258,
+31833,
+38678,
+64784,
+48718,
+51864,
+36542,
+59704,
+41074,
+4585,
+5360,
+2666,
+54308,
+14354,
+9488,
+9691,
+50425,
+58126,
+48088,
+44560,
+14495,
+25034,
+50873,
+24293,
+15116,
+22575,
+24110,
+21477,
+25788,
+3264,
+44522,
+36749,
+2182,
+52989,
+62895,
+63255,
+1179,
+6277,
+63565,
+52404,
+9059,
+13839,
+61906,
+42916,
+7844,
+945,
+16697,
+19166,
+57195,
+49140,
+37650,
+35444,
+29470,
+25232,
+7050,
+61848,
+61824,
+29362,
+65105,
+55046,
+8354,
+8300,
+56885,
+13569,
+54686,
+20097,
+76,
+45962,
+5569,
+45218,
+430,
+10493,
+45747,
+60004,
+63062,
+15021,
+53222,
+59508,
+32302,
+48573,
+3200,
+4710,
+5816,
+40042,
+45335,
+44538,
+43993,
+30532,
+38860,
+46127,
+7254,
+26270,
+52244,
+49065,
+62705,
+55491,
+23991,
+8286,
+30654,
+37196,
+27305,
+50415,
+63813,
+44732,
+35003,
+26679,
+26120,
+22918,
+21962,
+36307,
+50392,
+32644,
+1593,
+13610,
+9501,
+46219,
+28208,
+47923,
+29776,
+50816,
+63254,
+62982,
+52990,
+40626,
+32880,
+57091,
+4828,
+16409,
+15908,
+60022,
+29394,
+28345,
+31521,
+9754,
+40902,
+24453,
+9373,
+8901,
+24524,
+54376,
+4975,
+22773,
+9155,
+40321,
+60176,
+2532,
+11230,
+958,
+12302,
+56136,
+51085,
+37257,
+12161,
+23210,
+49,
+30148,
+32438,
+42068,
+3327,
+5062,
+15107,
+44009,
+48194,
+10231,
+14816,
+56192,
+26927,
+59271,
+34438,
+55475,
+44081,
+6208,
+44681,
+51403,
+41754,
+11644,
+243,
+18534,
+59078,
+46774,
+57724,
+35194,
+22864,
+8895,
+16039,
+9615,
+30868,
+3848,
+64908,
+19516,
+14964,
+3649,
+54280,
+54260,
+43981,
+24878,
+23904,
+41256,
+41048,
+3060,
+41066,
+13170,
+11336,
+40299,
+34921,
+60897,
+18409,
+32264,
+39961,
+46260,
+56178,
+48625,
+15491,
+42309,
+35632,
+23738,
+62065,
+17976,
+23712,
+31740,
+19104,
+30713,
+52874,
+43349,
+65346,
+40358,
+6537,
+59694,
+59267,
+30433,
+47908,
+10645,
+5880,
+17586,
+29902,
+17383,
+58786,
+31818,
+28956,
+31510,
+41017,
+7854,
+1945,
+17143,
+56459,
+60585,
+25959,
+18160,
+38224,
+1112,
+28736,
+51517,
+9879,
+45857,
+59680,
+34728,
+118,
+14320,
+20187,
+1108,
+57501,
+22733,
+16084,
+39148,
+40640,
+9433,
+50887,
+18970,
+14569,
+19896,
+26088,
+51993,
+29941,
+46467,
+55853,
+2133,
+39833,
+9861,
+54181,
+12335,
+22532,
+13636,
+49030,
+51677,
+20841,
+61076,
+57412,
+44975,
+3453,
+50074,
+10305,
+7155,
+48879,
+20425,
+14481,
+47640,
+5886,
+11512,
+44846,
+39206,
+6714,
+3739,
+33523,
+15572,
+8746,
+48028,
+26799,
+38817,
+29143,
+21699,
+55490,
+62922,
+49066,
+10892,
+22701,
+25027,
+19936,
+38126,
+54341,
+24161,
+50335,
+46374,
+19041,
+2097,
+23773,
+33183,
+36279,
+19859,
+58981,
+20167,
+9103,
+62205,
+23178,
+6866,
+53761,
+40364,
+53861,
+21624,
+298,
+62031,
+54450,
+7030,
+3571,
+13890,
+65354,
+38531,
+23522,
+9209,
+32186,
+18428,
+18102,
+43060,
+16082,
+22735,
+43357,
+38078,
+39591,
+11272,
+19372,
+2890,
+19950,
+4025,
+15026,
+57788,
+58086,
+47302,
+3110,
+15590,
+46164,
+2880,
+45588,
+59713,
+56838,
+33312,
+41586,
+28944,
+17193,
+54976,
+12705,
+48068,
+7681,
+32633,
+60909,
+4491,
+26102,
+20049,
+33907,
+5206,
+18226,
+40696,
+10784,
+59753,
+5962,
+31599,
+18818,
+16609,
+7549,
+29714,
+54109,
+46841,
+52995,
+13076,
+39237,
+48570,
+43176,
+31559,
+57051,
+3917,
+26179,
+51602,
+16524,
+38066,
+59638,
+63549,
+17350,
+9085,
+33159,
+63864,
+38845,
+5506,
+24802,
+54332,
+5634,
+54117,
+53342,
+29921,
+25730,
+58223,
+61559,
+10597,
+52323,
+25198,
+50157,
+21010,
+3482,
+15054,
+17775,
+63831,
+38516,
+55950,
+21735,
+62550,
+14377,
+21912,
+8112,
+19776,
+7974,
+30185,
+44589,
+30606,
+49059,
+48403,
+18010,
+61328,
+30885,
+51421,
+19813,
+15603,
+22,
+19947,
+5826,
+63581,
+21355,
+55500,
+4435,
+14376,
+62575,
+21736,
+11862,
+59334,
+31903,
+56403,
+47297,
+4356,
+53655,
+16428,
+28559,
+9072,
+64794,
+55204,
+63375,
+60625,
+12265,
+12370,
+45407,
+54963,
+56296,
+61332,
+36090,
+19095,
+42204,
+64906,
+3850,
+64602,
+40909,
+26054,
+17226,
+17090,
+23879,
+24023,
+6660,
+45260,
+9604,
+8994,
+51498,
+57242,
+21391,
+13484,
+52382,
+62444,
+32282,
+35773,
+24914,
+5033,
+691,
+58435,
+37721,
+62409,
+18654,
+47579,
+3793,
+51598,
+47769,
+47172,
+30175,
+7509,
+60590,
+58979,
+19861,
+45704,
+39653,
+10114,
+17903,
+56288,
+901,
+36693,
+49544,
+18881,
+24488,
+2720,
+24472,
+35979,
+62028,
+37795,
+43449,
+19644,
+65131,
+27834,
+35961,
+34519,
+17606,
+43670,
+39753,
+54003,
+34676,
+50772,
+46517,
+50228,
+60088,
+52919,
+6977,
+58541,
+56375,
+40950,
+25535,
+48951,
+2123,
+47179,
+16188,
+5875,
+33606,
+32281,
+62507,
+52383,
+48004,
+24026,
+56755,
+19018,
+15311,
+4799,
+63804,
+23726,
+27161,
+5384,
+20727,
+49100,
+63955,
+15836,
+18234,
+2887,
+49948,
+9919,
+43558,
+30706,
+57479,
+48017,
+43418,
+14440,
+22420,
+7709,
+49158,
+44566,
+52715,
+57712,
+32024,
+48702,
+18653,
+62499,
+37722,
+57727,
+45631,
+49362,
+60161,
+36909,
+60039,
+13354,
+55321,
+63177,
+3946,
+3903,
+46216,
+60677,
+6610,
+9151,
+25673,
+44476,
+49689,
+8723,
+41957,
+40092,
+27600,
+44225,
+26273,
+20592,
+12505,
+19333,
+64416,
+63607,
+25414,
+8441,
+36884,
+40686,
+36467,
+31215,
+41455,
+57319,
+7905,
+20501,
+11333,
+47075,
+7275,
+8657,
+27925,
+27400,
+35648,
+42240,
+64815,
+34542,
+14534,
+6327,
+17162,
+32233,
+54748,
+16446,
+30289,
+34085,
+46536,
+25964,
+59084,
+55614,
+20497,
+1622,
+60652,
+39868,
+50604,
+50014,
+43705,
+28301,
+16963,
+2601,
+60828,
+22371,
+11581,
+46780,
+8118,
+23079,
+41893,
+17467,
+57858,
+54655,
+45433,
+24709,
+43161,
+56830,
+54570,
+26064,
+876,
+45436,
+35629,
+36371,
+51925,
+56038,
+40953,
+22259,
+17822,
+45689,
+51930,
+57152,
+28009,
+37070,
+9148,
+42157,
+56719,
+34795,
+49877,
+34814,
+20064,
+43860,
+35409,
+60824,
+49724,
+55288,
+19845,
+1596,
+51339,
+2443,
+15259,
+24193,
+64831,
+25751,
+36508,
+38109,
+8381,
+50049,
+45141,
+27032,
+57448,
+25728,
+29923,
+37533,
+22328,
+39278,
+39748,
+2550,
+21016,
+23859,
+3992,
+50356,
+11688,
+18178,
+39210,
+27701,
+1702,
+18396,
+3542,
+38550,
+43698,
+59839,
+59441,
+48893,
+52114,
+51539,
+36232,
+39414,
+5227,
+8237,
+8394,
+26450,
+53940,
+52330,
+14358,
+36770,
+8319,
+54455,
+63190,
+17796,
+22442,
+28198,
+19528,
+14438,
+43420,
+19153,
+44485,
+7121,
+6286,
+46764,
+27606,
+46181,
+17429,
+39342,
+23317,
+48104,
+58376,
+5557,
+22676,
+16740,
+10610,
+57972,
+39571,
+6455,
+36962,
+27797,
+65182,
+44618,
+17286,
+23468,
+2679,
+60476,
+35062,
+6042,
+23177,
+62685,
+9104,
+43462,
+53196,
+22769,
+35901,
+11682,
+34113,
+42913,
+37800,
+12021,
+55382,
+19422,
+11011,
+64068,
+18171,
+16580,
+57044,
+17157,
+4596,
+35799,
+38212,
+57467,
+47507,
+64502,
+5642,
+32183,
+1795,
+21202,
+39330,
+25322,
+42639,
+61777,
+57396,
+50136,
+49907,
+38898,
+8890,
+35530,
+12112,
+63574,
+8452,
+60545,
+63634,
+34768,
+25423,
+44836,
+26048,
+7116,
+19035,
+23622,
+61740,
+10826,
+55508,
+54369,
+29392,
+60024,
+41461,
+29134,
+12717,
+34604,
+30441,
+9790,
+6795,
+50851,
+45955,
+29374,
+64920,
+2448,
+46190,
+51686,
+52653,
+34405,
+58454,
+23571,
+50783,
+31057,
+11208,
+52026,
+46362,
+64058,
+30109,
+8888,
+38900,
+59532,
+63082,
+15090,
+53135,
+5883,
+38372,
+38514,
+63833,
+47143,
+25392,
+33878,
+48689,
+61006,
+37862,
+60894,
+13371,
+18924,
+5708,
+1126,
+8433,
+46368,
+35009,
+32616,
+3678,
+8516,
+3733,
+37719,
+58437,
+59048,
+799,
+55134,
+4468,
+31457,
+54611,
+25208,
+26625,
+20561,
+2809,
+15464,
+607,
+24254,
+39053,
+46869,
+1781,
+14528,
+18008,
+48405,
+53690,
+44631,
+54321,
+31218,
+30623,
+2400,
+53446,
+14675,
+17975,
+62800,
+23739,
+34655,
+53316,
+49379,
+41241,
+40645,
+54498,
+18211,
+44526,
+2955,
+50484,
+39183,
+15176,
+65443,
+7048,
+25234,
+13994,
+28487,
+50725,
+55759,
+31821,
+63530,
+54136,
+21427,
+57797,
+59373,
+47556,
+9868,
+1535,
+43735,
+23966,
+49957,
+54449,
+62677,
+299,
+37794,
+62474,
+35980,
+26226,
+42976,
+21286,
+18184,
+5724,
+21156,
+36132,
+48864,
+33709,
+38112,
+24011,
+36304,
+9971,
+6974,
+5150,
+21096,
+40006,
+26434,
+53633,
+25565,
+7981,
+11931,
+43815,
+22606,
+44161,
+37643,
+22698,
+39400,
+500,
+63445,
+47140,
+3275,
+26988,
+23060,
+33129,
+46593,
+1589,
+52884,
+25502,
+58651,
+59465,
+46625,
+33700,
+19012,
+14785,
+41191,
+9065,
+6760,
+11730,
+42102,
+52967,
+34498,
+29522,
+25505,
+49764,
+53683,
+32153,
+22611,
+10577,
+58506,
+2863,
+51648,
+15701,
+28771,
+23410,
+49391,
+44925,
+33318,
+60776,
+11112,
+30673,
+58310,
+60196,
+28069,
+33582,
+28163,
+5737,
+49445,
+54252,
+34448,
+52415,
+22168,
+26959,
+11944,
+17355,
+49335,
+46315,
+35100,
+39843,
+2312,
+10938,
+14865,
+37998,
+17148,
+33123,
+48762,
+49984,
+56364,
+30090,
+12472,
+43286,
+48681,
+54853,
+61348,
+34663,
+19121,
+57782,
+23168,
+29699,
+49370,
+25375,
+7111,
+12174,
+30942,
+25347,
+41661,
+5222,
+63568,
+37798,
+42915,
+62974,
+13840,
+16957,
+7340,
+43487,
+30191,
+36922,
+48225,
+63664,
+34695,
+18,
+55190,
+23027,
+60771,
+39502,
+10803,
+51107,
+50572,
+2712,
+11258,
+12673,
+17784,
+63725,
+14223,
+38856,
+39419,
+59641,
+52942,
+47590,
+16241,
+16702,
+39426,
+53156,
+12002,
+4238,
+55365,
+43179,
+35931,
+59178,
+7268,
+1696,
+45958,
+50317,
+27096,
+43727,
+11321,
+32920,
+3439,
+57022,
+56401,
+31905,
+35296,
+53330,
+17843,
+33471,
+49375,
+25614,
+61823,
+62961,
+7051,
+54990,
+28668,
+32174,
+55655,
+38258,
+55022,
+40985,
+49073,
+30793,
+44760,
+23038,
+32202,
+51573,
+10740,
+30637,
+35315,
+32300,
+59510,
+1926,
+24148,
+31859,
+29361,
+62960,
+61849,
+25615,
+21781,
+57303,
+12427,
+33165,
+14922,
+13826,
+21112,
+35453,
+38840,
+29065,
+50041,
+29278,
+17984,
+56275,
+44400,
+24258,
+57020,
+3441,
+27819,
+56711,
+10227,
+48859,
+58822,
+2822,
+32015,
+10844,
+32138,
+57938,
+17347,
+35,
+30949,
+8308,
+24113,
+49734,
+4841,
+5591,
+31703,
+27521,
+5185,
+9896,
+37183,
+12298,
+4168,
+57395,
+62173,
+42640,
+17264,
+25654,
+8758,
+25710,
+39625,
+31281,
+36021,
+63433,
+47058,
+53667,
+5473,
+1737,
+32317,
+6111,
+14423,
+20579,
+55389,
+10180,
+33410,
+52,
+13603,
+21222,
+61363,
+37002,
+65035,
+2094,
+21367,
+34576,
+15511,
+55977,
+17957,
+3041,
+13940,
+46748,
+10825,
+62154,
+23623,
+34692,
+64278,
+31415,
+32079,
+4808,
+50152,
+50845,
+31081,
+16905,
+30273,
+9446,
+7861,
+47274,
+32992,
+38535,
+51567,
+34306,
+38041,
+7383,
+51204,
+15050,
+16247,
+43051,
+14318,
+120,
+23291,
+2176,
+23964,
+43737,
+46345,
+16089,
+30236,
+35326,
+667,
+47584,
+42170,
+48482,
+13309,
+47876,
+28602,
+43771,
+34936,
+273,
+10286,
+9226,
+44038,
+53725,
+6765,
+34668,
+2708,
+630,
+3013,
+936,
+34623,
+1820,
+20695,
+32760,
+61169,
+30721,
+45475,
+16881,
+63958,
+38667,
+64977,
+17118,
+41232,
+65147,
+13816,
+63266,
+22145,
+56373,
+58543,
+21603,
+52521,
+27379,
+50633,
+30054,
+9046,
+46334,
+38244,
+13398,
+30522,
+5809,
+52487,
+17283,
+37240,
+51991,
+26090,
+20435,
+28841,
+49039,
+34150,
+18157,
+56966,
+11296,
+10014,
+51521,
+9807,
+39672,
+15283,
+38391,
+32382,
+65453,
+61590,
+3140,
+19322,
+59277,
+4849,
+28013,
+12393,
+29692,
+10677,
+45462,
+64046,
+39895,
+35682,
+52597,
+8485,
+2928,
+39359,
+1388,
+36898,
+32710,
+5130,
+43786,
+36994,
+55552,
+63851,
+5524,
+38825,
+2035,
+46643,
+36528,
+29785,
+33610,
+54492,
+59408,
+51667,
+20292,
+63723,
+17786,
+35380,
+19706,
+58970,
+18117,
+31489,
+2359,
+3139,
+61635,
+65454,
+55148,
+46075,
+30919,
+16434,
+24537,
+8967,
+24972,
+38749,
+10103,
+52572,
+12432,
+55723,
+13071,
+52949,
+41988,
+46276,
+20033,
+27084,
+1206,
+54950,
+16215,
+33792,
+23254,
+32785,
+58048,
+41443,
+18169,
+64070,
+10596,
+62588,
+58224,
+52479,
+50194,
+32966,
+45068,
+16302,
+46986,
+17938,
+5934,
+7543,
+30033,
+14405,
+41677,
+3267,
+15182,
+22956,
+689,
+5035,
+951,
+9983,
+1688,
+41403,
+52826,
+44330,
+25000,
+25583,
+37424,
+2274,
+10026,
+24838,
+25829,
+27081,
+37319,
+59836,
+42853,
+63790,
+45982,
+15911,
+49891,
+59975,
+29287,
+47066,
+11132,
+11227,
+51185,
+45586,
+2882,
+50512,
+25681,
+4995,
+59307,
+44022,
+25623,
+39514,
+21824,
+64481,
+58829,
+65196,
+24481,
+47830,
+41490,
+46833,
+12423,
+53263,
+22386,
+51454,
+54066,
+20782,
+12356,
+27442,
+40384,
+9337,
+37177,
+46759,
+1617,
+61447,
+40599,
+5930,
+59560,
+10869,
+6410,
+6247,
+36682,
+24411,
+56617,
+27850,
+56702,
+46598,
+56094,
+46124,
+54488,
+34733,
+58019,
+42216,
+51506,
+38431,
+16919,
+37158,
+27504,
+63496,
+4081,
+2040,
+4079,
+63498,
+8780,
+46521,
+14070,
+26866,
+19435,
+14733,
+40598,
+61483,
+1618,
+43355,
+22737,
+33761,
+30840,
+65521,
+30585,
+10571,
+19522,
+26007,
+28206,
+46221,
+16532,
+37671,
+57746,
+58530,
+11984,
+21617,
+59205,
+21160,
+64166,
+63481,
+42321,
+64852,
+29416,
+50745,
+58103,
+55081,
+48751,
+53559,
+11005,
+39796,
+28739,
+30496,
+63980,
+19839,
+54548,
+50788,
+39173,
+45019,
+11724,
+60636,
+16412,
+55280,
+10730,
+50563,
+21212,
+64951,
+19270,
+64698,
+12123,
+24699,
+5236,
+11877,
+13392,
+55120,
+42780,
+37461,
+40390,
+5164,
+43244,
+55798,
+22661,
+7494,
+22587,
+65332,
+40801,
+12499,
+57532,
+22166,
+52417,
+23730,
+40081,
+23152,
+50148,
+8668,
+53238,
+3758,
+8143,
+44430,
+60399,
+61060,
+37001,
+61753,
+21223,
+4253,
+5162,
+40392,
+49980,
+28645,
+9452,
+56338,
+6549,
+63767,
+13264,
+33019,
+57069,
+34662,
+61923,
+54854,
+30978,
+56016,
+38325,
+1422,
+64709,
+53651,
+6574,
+11693,
+38598,
+30069,
+46801,
+14770,
+52782,
+36089,
+62529,
+56297,
+41796,
+30884,
+62563,
+18011,
+44511,
+51829,
+45361,
+51089,
+11537,
+63283,
+38340,
+4770,
+39782,
+14707,
+57308,
+32427,
+60245,
+31760,
+45015,
+6746,
+51366,
+54609,
+31459,
+47983,
+36912,
+20550,
+46184,
+57163,
+8697,
+32227,
+53534,
+3665,
+10173,
+53898,
+55137,
+25878,
+59683,
+41061,
+14466,
+34312,
+33162,
+41948,
+9856,
+54388,
+29107,
+44313,
+4176,
+60376,
+27524,
+33985,
+47263,
+6880,
+48518,
+37953,
+17657,
+64599,
+30916,
+37610,
+38139,
+1398,
+45816,
+45621,
+22122,
+60733,
+14915,
+35085,
+35859,
+56168,
+57136,
+47804,
+10006,
+41470,
+60835,
+24607,
+26423,
+35401,
+51689,
+48755,
+22590,
+36316,
+14416,
+19548,
+33968,
+41354,
+31035,
+43106,
+48378,
+60943,
+23987,
+9260,
+21139,
+15294,
+47533,
+46135,
+33538,
+27985,
+45387,
+42864,
+34131,
+28720,
+35089,
+6861,
+26084,
+38273,
+58848,
+9712,
+25452,
+47231,
+55771,
+15048,
+51206,
+33922,
+55689,
+45618,
+47655,
+30312,
+24529,
+31268,
+64052,
+60371,
+22295,
+50985,
+9689,
+9490,
+802,
+32239,
+43651,
+8332,
+1920,
+44966,
+65401,
+17963,
+64812,
+50240,
+22208,
+33010,
+35945,
+19296,
+181,
+27008,
+14805,
+16746,
+1085,
+53891,
+65296,
+34681,
+43601,
+20787,
+18239,
+2994,
+11883,
+36889,
+13366,
+46402,
+31694,
+47162,
+42029,
+34045,
+5285,
+39043,
+30720,
+61681,
+32761,
+30249,
+21066,
+60965,
+5650,
+54764,
+60324,
+22270,
+41655,
+59624,
+7528,
+53073,
+47496,
+50596,
+30563,
+52606,
+56450,
+32380,
+38393,
+43537,
+42885,
+8166,
+59343,
+24457,
+28375,
+40763,
+56247,
+49600,
+36845,
+47223,
+25893,
+43597,
+42366,
+43071,
+65150,
+49748,
+31794,
+40057,
+42576,
+35073,
+59071,
+47362,
+30577,
+29456,
+58067,
+44795,
+48867,
+36078,
+27657,
+55702,
+41793,
+17007,
+5307,
+60661,
+39014,
+43479,
+50756,
+42334,
+32391,
+60490,
+19340,
+18448,
+57698,
+695,
+40020,
+10036,
+2985,
+51162,
+42554,
+33625,
+18573,
+37300,
+58424,
+53365,
+41700,
+6828,
+6738,
+14608,
+19807,
+33461,
+13184,
+46789,
+16491,
+41969,
+12451,
+21880,
+20404,
+25400,
+10293,
+10423,
+21635,
+57411,
+62731,
+20842,
+52923,
+12435,
+28211,
+11637,
+10402,
+3447,
+34081,
+13298,
+52198,
+26716,
+64233,
+23540,
+21997,
+37000,
+61365,
+60400,
+9584,
+48670,
+34487,
+58182,
+16461,
+15595,
+39556,
+45274,
+15161,
+37602,
+38236,
+4627,
+2904,
+39436,
+63597,
+10952,
+39468,
+14912,
+6048,
+21258,
+32062,
+14957,
+48341,
+13844,
+18571,
+33627,
+611,
+51938,
+9539,
+63429,
+36642,
+5769,
+36495,
+40888,
+58218,
+23796,
+12730,
+22044,
+27509,
+21425,
+54138,
+54619,
+43068,
+35841,
+7752,
+39215,
+39496,
+30220,
+33239,
+21754,
+26264,
+37861,
+62109,
+48690,
+52019,
+65378,
+37747,
+5996,
+37037,
+28176,
+60007,
+11072,
+47208,
+30835,
+64758,
+39677,
+63475,
+14056,
+14370,
+2003,
+15862,
+27756,
+25201,
+6666,
+18511,
+17111,
+22402,
+14543,
+41606,
+15083,
+35737,
+53565,
+38006,
+589,
+12553,
+64259,
+40442,
+31448,
+59553,
+58177,
+5006,
+57750,
+5649,
+61165,
+21067,
+6462,
+52315,
+14193,
+19989,
+50434,
+65308,
+22709,
+59371,
+57799,
+65394,
+24462,
+2530,
+60178,
+34433,
+19184,
+29748,
+23659,
+55159,
+44286,
+23986,
+61243,
+48379,
+3351,
+29739,
+20083,
+13096,
+18906,
+19842,
+18556,
+42614,
+18491,
+1167,
+46741,
+11480,
+50959,
+2204,
+36351,
+36002,
+17720,
+7686,
+33848,
+9827,
+52688,
+55331,
+51615,
+34010,
+57125,
+44391,
+45615,
+60873,
+1800,
+35808,
+27300,
+4490,
+62634,
+32634,
+8592,
+58079,
+9607,
+2136,
+18739,
+47608,
+47315,
+47644,
+65212,
+18408,
+62811,
+34922,
+13370,
+62107,
+37863,
+56747,
+12230,
+56370,
+32698,
+44321,
+25045,
+32691,
+43264,
+37663,
+13589,
+31072,
+25435,
+50071,
+64932,
+54579,
+40602,
+42379,
+32403,
+1799,
+60914,
+45616,
+55691,
+51705,
+50304,
+56722,
+21289,
+64162,
+53852,
+29634,
+53857,
+25612,
+49377,
+53318,
+32040,
+31711,
+26985,
+21841,
+17875,
+7865,
+48733,
+6183,
+16629,
+47737,
+15867,
+32468,
+12255,
+39705,
+41666,
+44883,
+31121,
+16639,
+27228,
+43296,
+32908,
+47113,
+36631,
+24606,
+61258,
+41471,
+52594,
+59436,
+22853,
+42118,
+22370,
+62336,
+2602,
+59722,
+49723,
+62297,
+35410,
+56808,
+49792,
+29302,
+34741,
+7805,
+63968,
+26709,
+27661,
+7618,
+20241,
+26728,
+8362,
+43964,
+51329,
+49863,
+35983,
+13728,
+51895,
+4791,
+56580,
+30201,
+51353,
+20559,
+26627,
+49011,
+27473,
+10962,
+26107,
+27124,
+5070,
+8660,
+20810,
+16153,
+23341,
+16367,
+38315,
+38638,
+35513,
+14216,
+49201,
+53229,
+40499,
+5621,
+27617,
+36590,
+11111,
+61958,
+33319,
+26501,
+28160,
+39501,
+61893,
+23028,
+20659,
+27196,
+4218,
+25756,
+45582,
+65180,
+27799,
+26105,
+10964,
+52586,
+22152,
+44569,
+57955,
+55873,
+39155,
+40380,
+53468,
+37076,
+9873,
+47438,
+22927,
+19761,
+13641,
+4733,
+8958,
+43468,
+11497,
+36862,
+18352,
+40713,
+50225,
+3100,
+1734,
+36188,
+6046,
+14914,
+61267,
+22123,
+9341,
+33457,
+3803,
+15386,
+54596,
+8909,
+55336,
+817,
+20271,
+14591,
+52895,
+46231,
+60164,
+28097,
+9613,
+16041,
+12735,
+28658,
+17376,
+38059,
+16554,
+8445,
+2647,
+49129,
+46093,
+39816,
+55975,
+15513,
+47031,
+17716,
+36685,
+9796,
+32037,
+52305,
+15802,
+3750,
+59947,
+11516,
+29102,
+46559,
+30226,
+46675,
+6259,
+3906,
+37525,
+1351,
+47748,
+21337,
+45055,
+29792,
+8624,
+29240,
+29207,
+6609,
+62395,
+46217,
+9503,
+15013,
+37929,
+19358,
+1064,
+44800,
+25889,
+45320,
+20953,
+148,
+59820,
+10813,
+40648,
+39013,
+61115,
+5308,
+25406,
+10840,
+28565,
+33366,
+13196,
+56907,
+39867,
+62344,
+1623,
+17152,
+57908,
+58553,
+25022,
+5467,
+11615,
+48031,
+308,
+6788,
+1700,
+27703,
+9674,
+15906,
+16411,
+61405,
+11725,
+28030,
+51732,
+55810,
+40276,
+57617,
+40313,
+40732,
+31060,
+12264,
+62535,
+63376,
+25700,
+23828,
+51979,
+18047,
+55316,
+32109,
+54441,
+13451,
+38494,
+56205,
+56899,
+28304,
+26276,
+52262,
+8498,
+27337,
+51406,
+22714,
+39479,
+6563,
+7000,
+13041,
+56920,
+36200,
+46359,
+28038,
+39362,
+58149,
+46348,
+59332,
+11864,
+57214,
+58978,
+62490,
+7510,
+40906,
+31471,
+25958,
+62771,
+56460,
+14089,
+10806,
+26593,
+17042,
+27534,
+31176,
+43960,
+40992,
+52633,
+33026,
+42974,
+26228,
+17539,
+18657,
+53915,
+43854,
+51256,
+47396,
+25822,
+369,
+11160,
+2661,
+42765,
+38648,
+54070,
+48465,
+26149,
+26499,
+33321,
+40377,
+11348,
+58464,
+51591,
+42237,
+8133,
+37491,
+15721,
+63633,
+62163,
+8453,
+39855,
+4028,
+6231,
+8761,
+41790,
+53502,
+20015,
+7129,
+40656,
+8554,
+37025,
+7359,
+4569,
+7788,
+11503,
+23250,
+6763,
+53727,
+26662,
+8014,
+43199,
+21654,
+12948,
+16799,
+8868,
+42712,
+52051,
+13157,
+32899,
+60305,
+43400,
+11438,
+52693,
+52500,
+23866,
+44647,
+10254,
+60157,
+40622,
+3034,
+64847,
+3996,
+64587,
+43301,
+56392,
+63352,
+57826,
+16381,
+29129,
+38727,
+48685,
+25942,
+19339,
+61109,
+32392,
+33424,
+24359,
+53574,
+25932,
+49584,
+18271,
+1841,
+29852,
+6104,
+20537,
+285,
+35061,
+62209,
+2680,
+50431,
+1333,
+63622,
+27154,
+8045,
+7177,
+38412,
+4744,
+56878,
+37614,
+15748,
+57981,
+18105,
+29063,
+38842,
+24044,
+36246,
+22280,
+38992,
+40016,
+23337,
+12517,
+63114,
+58205,
+23280,
+44915,
+49787,
+7558,
+50218,
+44056,
+1853,
+12055,
+16619,
+43507,
+63274,
+12894,
+52517,
+1632,
+17820,
+22261,
+56303,
+51143,
+11002,
+1091,
+15425,
+59263,
+15714,
+64216,
+36096,
+64396,
+17650,
+33358,
+34684,
+35907,
+65204,
+64551,
+10447,
+26202,
+27771,
+37298,
+18575,
+39688,
+33913,
+30953,
+10060,
+21458,
+32826,
+3922,
+21752,
+33241,
+25676,
+41117,
+30401,
+9583,
+61059,
+61366,
+44431,
+24570,
+10670,
+18825,
+7684,
+17722,
+40596,
+14735,
+44693,
+12616,
+58562,
+3525,
+44442,
+30924,
+51229,
+30912,
+63719,
+14681,
+21744,
+65321,
+5183,
+27523,
+61283,
+4177,
+25146,
+4150,
+22294,
+61211,
+64053,
+39553,
+36756,
+27343,
+14499,
+42817,
+46052,
+38,
+38651,
+22981,
+31914,
+35553,
+26764,
+22609,
+32155,
+215,
+5159,
+38015,
+3754,
+55642,
+31246,
+30002,
+56355,
+31531,
+39302,
+40279,
+32270,
+26611,
+65373,
+29754,
+58122,
+16151,
+20812,
+57055,
+8934,
+1600,
+20796,
+2355,
+31629,
+2908,
+47312,
+20021,
+26786,
+55436,
+44949,
+22269,
+61162,
+54765,
+48848,
+15266,
+5251,
+19430,
+43436,
+55832,
+23875,
+3627,
+1970,
+13135,
+22916,
+26122,
+3285,
+57301,
+21783,
+64121,
+43399,
+60514,
+32900,
+44764,
+40837,
+4143,
+53661,
+15327,
+48987,
+18598,
+64199,
+46421,
+25,
+57976,
+16706,
+64675,
+39582,
+22366,
+18229,
+50270,
+5217,
+46108,
+7311,
+13060,
+55086,
+11459,
+16431,
+10981,
+19939,
+50922,
+436,
+25196,
+52325,
+16322,
+2141,
+33565,
+39693,
+6805,
+6443,
+37519,
+55814,
+55878,
+57334,
+14104,
+24873,
+56730,
+32576,
+14888,
+10838,
+25408,
+34388,
+31627,
+2357,
+31491,
+4365,
+26495,
+59525,
+20313,
+14064,
+16440,
+31759,
+61314,
+32428,
+2414,
+38889,
+40973,
+55738,
+7954,
+26430,
+30011,
+58762,
+14697,
+16548,
+46941,
+6634,
+48529,
+4421,
+4877,
+40132,
+20451,
+28357,
+44995,
+24029,
+37455,
+22343,
+37219,
+37212,
+12262,
+31062,
+27188,
+64798,
+35256,
+53779,
+44155,
+17255,
+37346,
+17593,
+52929,
+22973,
+30088,
+56366,
+1266,
+36830,
+5615,
+10282,
+479,
+48198,
+6151,
+40152,
+28068,
+61954,
+58311,
+1153,
+51397,
+64458,
+38849,
+14463,
+11605,
+13943,
+32793,
+1286,
+15074,
+18034,
+16219,
+39004,
+21034,
+30964,
+34432,
+60951,
+2531,
+62872,
+40322,
+11487,
+54773,
+8270,
+24097,
+22212,
+37884,
+34630,
+7408,
+13349,
+28096,
+60719,
+46232,
+36908,
+62404,
+49363,
+17219,
+40621,
+60506,
+10255,
+54809,
+38375,
+3104,
+44811,
+55109,
+11302,
+58470,
+54677,
+20877,
+51654,
+5822,
+8701,
+32890,
+64468,
+9267,
+23400,
+29539,
+63437,
+23387,
+52068,
+28862,
+9910,
+17597,
+11907,
+11432,
+49780,
+37216,
+32508,
+40788,
+55896,
+37576,
+6768,
+15434,
+52006,
+1924,
+59512,
+32454,
+10558,
+9758,
+19053,
+7482,
+28859,
+34593,
+40192,
+27140,
+37291,
+27130,
+30831,
+54759,
+25317,
+12439,
+48432,
+11952,
+52719,
+20628,
+33775,
+20524,
+27774,
+1530,
+59117,
+20533,
+47151,
+6585,
+28610,
+5668,
+52278,
+52918,
+62458,
+50229,
+26739,
+63217,
+64888,
+41973,
+27236,
+16132,
+32150,
+52749,
+56497,
+23130,
+13105,
+28089,
+58639,
+57081,
+48772,
+40514,
+4891,
+53923,
+7202,
+17816,
+30774,
+11869,
+3772,
+63801,
+29351,
+8988,
+15771,
+11856,
+56219,
+15870,
+9482,
+9552,
+52126,
+48125,
+47937,
+26437,
+13449,
+54443,
+2376,
+32673,
+25661,
+20179,
+26284,
+36037,
+39680,
+29847,
+13353,
+62402,
+36910,
+47985,
+10210,
+23954,
+51484,
+38091,
+26691,
+2940,
+40736,
+8793,
+14347,
+14951,
+18064,
+41460,
+62149,
+29393,
+62887,
+15909,
+45984,
+14478,
+36420,
+15755,
+28323,
+53277,
+1685,
+3927,
+26713,
+33741,
+25482,
+41340,
+11071,
+60998,
+28177,
+63061,
+62943,
+45748,
+48392,
+9080,
+36562,
+19112,
+29284,
+556,
+58949,
+14368,
+14058,
+58527,
+54037,
+37708,
+54561,
+55697,
+28310,
+29840,
+51759,
+17364,
+21468,
+59,
+57892,
+7160,
+3314,
+42862,
+45389,
+554,
+29286,
+61519,
+49892,
+44135,
+59790,
+8042,
+16811,
+19574,
+2589,
+20320,
+19387,
+37485,
+26577,
+58212,
+19714,
+9485,
+51976,
+40053,
+24199,
+9948,
+49050,
+13044,
+57766,
+58765,
+15865,
+47739,
+43097,
+56002,
+11515,
+60695,
+3751,
+33835,
+19217,
+52685,
+29600,
+37699,
+21510,
+59859,
+913,
+38936,
+18546,
+11266,
+49606,
+6818,
+12476,
+24515,
+17369,
+58607,
+13063,
+43189,
+40308,
+30538,
+43271,
+28706,
+5334,
+55256,
+12286,
+34495,
+64324,
+25037,
+4757,
+37921,
+338,
+36346,
+25073,
+57208,
+11577,
+42981,
+7300,
+40009,
+47148,
+23820,
+11204,
+2943,
+11421,
+5756,
+46470,
+22423,
+43025,
+19603,
+1034,
+3357,
+33355,
+34638,
+43850,
+49530,
+824,
+7786,
+4571,
+4373,
+10774,
+24014,
+23233,
+46168,
+31372,
+52177,
+25997,
+47340,
+36735,
+43618,
+65236,
+27745,
+47981,
+31461,
+38952,
+24533,
+36926,
+5446,
+13260,
+44119,
+49485,
+45282,
+52760,
+9968,
+21965,
+19063,
+912,
+59939,
+21511,
+44849,
+48475,
+57936,
+32140,
+23187,
+54032,
+42289,
+42731,
+33817,
+18766,
+44655,
+31139,
+49360,
+45633,
+50908,
+64064,
+42698,
+59440,
+62259,
+43699,
+42852,
+61525,
+37320,
+21386,
+63871,
+12085,
+11589,
+46612,
+3433,
+64412,
+59649,
+54027,
+20182,
+24400,
+59699,
+48932,
+10812,
+60665,
+149,
+10050,
+63784,
+34378,
+27225,
+47352,
+3515,
+44382,
+34554,
+34136,
+6945,
+20900,
+32677,
+8691,
+52034,
+31391,
+11761,
+41562,
+37373,
+3587,
+16093,
+15976,
+9214,
+45788,
+9051,
+50428,
+21530,
+42003,
+8041,
+59972,
+44136,
+57352,
+54447,
+49959,
+19039,
+46376,
+52564,
+54115,
+5636,
+19068,
+9686,
+34356,
+1445,
+11649,
+9975,
+28703,
+34781,
+14176,
+7513,
+59389,
+28894,
+4510,
+29043,
+45232,
+23651,
+33864,
+1555,
+45700,
+7795,
+25128,
+23082,
+42992,
+1440,
+57488,
+22794,
+5961,
+62625,
+10785,
+28419,
+8471,
+16656,
+45114,
+20793,
+37516,
+14472,
+43318,
+41365,
+37852,
+27488,
+46024,
+39121,
+52902,
+24034,
+6417,
+36609,
+37653,
+49085,
+17183,
+63223,
+54601,
+35014,
+3881,
+13051,
+55601,
+42793,
+18625,
+49722,
+60826,
+2603,
+25496,
+9113,
+44735,
+20348,
+13882,
+5392,
+56837,
+62645,
+45589,
+2981,
+1957,
+48568,
+39239,
+9409,
+35820,
+41073,
+63009,
+36543,
+921,
+14857,
+48931,
+59823,
+24401,
+56189,
+6676,
+59266,
+62789,
+6538,
+52609,
+52387,
+53880,
+24285,
+64607,
+21588,
+45196,
+48548,
+41060,
+61294,
+25879,
+34727,
+62762,
+45858,
+46924,
+25801,
+49122,
+8262,
+9479,
+19033,
+7118,
+29819,
+13304,
+64543,
+67,
+8675,
+23788,
+37311,
+8771,
+49348,
+3090,
+31932,
+48085,
+52191,
+54891,
+2753,
+47952,
+12401,
+3420,
+27166,
+17896,
+2817,
+54026,
+59827,
+64413,
+26671,
+43721,
+47729,
+59020,
+12865,
+52941,
+61880,
+39420,
+63548,
+62604,
+38067,
+36328,
+58930,
+55012,
+12599,
+55515,
+57847,
+42482,
+10505,
+29114,
+1741,
+52709,
+7527,
+61159,
+41656,
+41516,
+64684,
+58692,
+19258,
+45784,
+27975,
+33737,
+43636,
+23723,
+31275,
+8871,
+39957,
+58405,
+10956,
+24937,
+4056,
+5978,
+9888,
+47598,
+3987,
+57995,
+47861,
+35048,
+55063,
+38755,
+15303,
+59547,
+57298,
+57145,
+56970,
+58614,
+9005,
+23484,
+30664,
+18056,
+48211,
+37080,
+58479,
+30341,
+42675,
+22528,
+40570,
+19660,
+11,
+27308,
+42742,
+5783,
+2913,
+33939,
+64559,
+49573,
+43446,
+63571,
+27750,
+51033,
+34986,
+45967,
+7188,
+43823,
+64056,
+46364,
+10868,
+61480,
+5931,
+55650,
+29667,
+51862,
+48720,
+58176,
+60970,
+31449,
+56417,
+23962,
+2178,
+57297,
+59596,
+15304,
+8214,
+22921,
+47048,
+27789,
+4536,
+28501,
+32397,
+44223,
+27602,
+7328,
+64456,
+51399,
+63081,
+62121,
+38901,
+16627,
+6185,
+1516,
+55472,
+20312,
+60250,
+26496,
+28148,
+18054,
+30666,
+40028,
+18045,
+51981,
+42316,
+20681,
+21102,
+21327,
+32453,
+60120,
+1925,
+61829,
+32301,
+62939,
+53223,
+32687,
+13027,
+51176,
+8613,
+15225,
+8545,
+14840,
+44708,
+11829,
+44780,
+30474,
+3330,
+42361,
+22630,
+29348,
+4802,
+50624,
+4497,
+44280,
+25749,
+64833,
+28621,
+53194,
+43464,
+36389,
+6742,
+10041,
+26191,
+30470,
+31222,
+1676,
+16141,
+13663,
+50655,
+25091,
+12457,
+10759,
+35285,
+57546,
+33114,
+46624,
+61986,
+58652,
+51629,
+49916,
+63973,
+31868,
+9327,
+35081,
+56558,
+19119,
+34665,
+37579,
+25799,
+46926,
+56653,
+7246,
+23586,
+26133,
+47856,
+20637,
+36523,
+16274,
+30295,
+48892,
+62258,
+59840,
+42699,
+52911,
+22852,
+60832,
+52595,
+35684,
+5057,
+45001,
+41720,
+27054,
+28144,
+64078,
+33219,
+3775,
+51115,
+32999,
+24772,
+9292,
+42209,
+20485,
+12244,
+48561,
+1360,
+39198,
+12236,
+42332,
+50758,
+52451,
+31014,
+599,
+51666,
+61602,
+54493,
+10636,
+9822,
+40799,
+65334,
+19143,
+13917,
+57453,
+50749,
+30959,
+50028,
+44353,
+8803,
+11234,
+21835,
+2261,
+52645,
+28893,
+59770,
+7514,
+17338,
+5338,
+25386,
+30814,
+56027,
+6197,
+25114,
+53477,
+30044,
+39020,
+59228,
+10297,
+36257,
+47555,
+62039,
+57798,
+60956,
+22710,
+20475,
+46542,
+45536,
+21389,
+57244,
+27561,
+33830,
+44597,
+56358,
+14875,
+8094,
+47994,
+35706,
+34905,
+16803,
+4685,
+48909,
+6008,
+64022,
+26969,
+58960,
+13226,
+24002,
+13317,
+12013,
+24456,
+61146,
+8167,
+9509,
+6155,
+40424,
+49647,
+35168,
+58699,
+31902,
+62547,
+11863,
+60594,
+46349,
+21455,
+2692,
+20857,
+43132,
+37406,
+24550,
+16466,
+64746,
+33765,
+7918,
+25226,
+9117,
+14901,
+34212,
+38747,
+24974,
+51625,
+37728,
+47134,
+21779,
+25617,
+15353,
+44021,
+61508,
+4996,
+56777,
+39829,
+21709,
+58936,
+7220,
+52024,
+11210,
+27406,
+36623,
+10081,
+24579,
+15820,
+22283,
+44307,
+59126,
+4060,
+28856,
+49470,
+16012,
+48238,
+24351,
+26873,
+42095,
+29620,
+64018,
+51334,
+18861,
+4848,
+61632,
+19323,
+55584,
+54152,
+24681,
+34437,
+62849,
+26928,
+51223,
+30432,
+62788,
+59695,
+6677,
+15713,
+60429,
+15426,
+64357,
+2558,
+1973,
+167,
+34720,
+12521,
+23108,
+19005,
+63107,
+58415,
+3305,
+18774,
+37344,
+17257,
+45605,
+948,
+22238,
+18829,
+57613,
+64777,
+37130,
+29641,
+53747,
+21048,
+20046,
+27802,
+13800,
+53538,
+54881,
+38948,
+45905,
+56611,
+10296,
+59377,
+39021,
+16418,
+42435,
+53339,
+32208,
+33853,
+64572,
+48066,
+12707,
+32950,
+36989,
+55905,
+18442,
+43947,
+20343,
+1834,
+51002,
+3180,
+48376,
+43108,
+53496,
+21159,
+61428,
+21618,
+36721,
+18849,
+43019,
+29971,
+38435,
+26343,
+56306,
+37683,
+36406,
+26812,
+37097,
+10143,
+46731,
+8529,
+13700,
+45687,
+17824,
+16263,
+21120,
+34940,
+65176,
+23121,
+17271,
+24289,
+7267,
+61868,
+35932,
+44859,
+8581,
+17252,
+48885,
+9030,
+27374,
+33453,
+8223,
+1029,
+47913,
+43225,
+49811,
+18243,
+23268,
+14413,
+12982,
+6639,
+29542,
+40140,
+27807,
+18405,
+43017,
+18851,
+811,
+34303,
+30326,
+48278,
+19934,
+25029,
+25302,
+47612,
+25007,
+43066,
+54621,
+41833,
+46919,
+48264,
+41250,
+932,
+34069,
+53906,
+30414,
+18700,
+43954,
+18643,
+38559,
+39760,
+23390,
+54667,
+4059,
+59291,
+44308,
+50259,
+53715,
+378,
+41502,
+5801,
+28929,
+20532,
+60096,
+1531,
+55485,
+1755,
+45123,
+19656,
+54349,
+40605,
+28415,
+2365,
+27285,
+25905,
+13032,
+25194,
+438,
+49415,
+22718,
+19487,
+50583,
+19084,
+50954,
+18783,
+58731,
+5269,
+28019,
+51067,
+53245,
+10196,
+43898,
+55334,
+8911,
+52838,
+55613,
+62348,
+25965,
+3320,
+28576,
+31718,
+46773,
+62838,
+18535,
+44469,
+4574,
+13223,
+53847,
+47361,
+61128,
+35074,
+50166,
+23909,
+57707,
+53333,
+17407,
+7715,
+10032,
+44302,
+21930,
+769,
+50473,
+48078,
+30412,
+53908,
+24862,
+20208,
+27644,
+37109,
+42691,
+27217,
+798,
+62093,
+58438,
+12752,
+33990,
+30987,
+40398,
+50018,
+44865,
+44616,
+65184,
+53436,
+45644,
+49246,
+18254,
+49896,
+51823,
+1884,
+36959,
+33821,
+57793,
+46477,
+55339,
+21641,
+13516,
+161,
+28724,
+44030,
+12864,
+59644,
+47730,
+39851,
+51535,
+17236,
+11164,
+21561,
+19639,
+57773,
+19874,
+22582,
+1275,
+57577,
+10508,
+13858,
+44698,
+36726,
+1996,
+19626,
+25178,
+2747,
+5844,
+12929,
+9089,
+1032,
+19605,
+53287,
+46619,
+55744,
+851,
+6847,
+9198,
+4183,
+1771,
+19418,
+19275,
+9361,
+21849,
+20166,
+62688,
+19860,
+62489,
+60591,
+57215,
+58355,
+26057,
+4933,
+58538,
+23779,
+18116,
+61595,
+19707,
+19704,
+35382,
+21972,
+36887,
+11885,
+4197,
+53845,
+13225,
+59349,
+26970,
+65278,
+26165,
+38410,
+7179,
+48314,
+18385,
+5444,
+36928,
+14367,
+59996,
+557,
+31595,
+49021,
+8898,
+11173,
+11221,
+45384,
+14099,
+31482,
+9764,
+50491,
+7219,
+59302,
+21710,
+28714,
+42345,
+64515,
+55011,
+59635,
+36329,
+4859,
+3143,
+56387,
+23382,
+12378,
+13803,
+9729,
+55859,
+41103,
+41483,
+2208,
+19671,
+51709,
+44427,
+29033,
+24052,
+63331,
+29188,
+15289,
+35547,
+40297,
+11338,
+46037,
+8370,
+18595,
+5305,
+17009,
+28986,
+16994,
+52554,
+6377,
+35996,
+2102,
+18481,
+34273,
+46667,
+55990,
+32591,
+52542,
+22309,
+8940,
+8643,
+50382,
+11734,
+660,
+34108,
+35324,
+30238,
+571,
+17531,
+41513,
+3021,
+20278,
+44357,
+13731,
+64111,
+40335,
+4552,
+47027,
+21731,
+53430,
+26598,
+28215,
+44153,
+53781,
+50719,
+9347,
+5841,
+6272,
+49825,
+17888,
+54225,
+58278,
+18900,
+8718,
+38779,
+9284,
+27691,
+50640,
+9711,
+61226,
+38274,
+20716,
+32034,
+57586,
+41854,
+43042,
+8290,
+49713,
+28522,
+7758,
+41347,
+8587,
+2298,
+25372,
+50668,
+30711,
+19106,
+65195,
+61502,
+64482,
+56522,
+37539,
+32251,
+26330,
+2821,
+61799,
+48860,
+18421,
+33666,
+17762,
+10601,
+36157,
+19665,
+38788,
+12661,
+22190,
+54088,
+34527,
+63657,
+8337,
+28698,
+14342,
+25068,
+38964,
+57722,
+46776,
+19449,
+58708,
+4445,
+50930,
+17765,
+29147,
+6056,
+55497,
+31787,
+20521,
+4226,
+14172,
+63041,
+31443,
+31817,
+62780,
+17384,
+31075,
+45480,
+27185,
+22418,
+14442,
+13024,
+56471,
+3558,
+53808,
+42286,
+5010,
+11811,
+3846,
+30870,
+27960,
+5413,
+50104,
+27754,
+15864,
+59953,
+57767,
+14696,
+60236,
+30012,
+36129,
+53499,
+55705,
+14567,
+18972,
+39127,
+23894,
+17302,
+21704,
+47989,
+65228,
+4895,
+63838,
+36166,
+22978,
+58194,
+849,
+55746,
+3395,
+14687,
+21280,
+32460,
+19076,
+1224,
+65431,
+23095,
+48765,
+25843,
+5268,
+59095,
+18784,
+49971,
+21820,
+29227,
+32552,
+44554,
+64248,
+58160,
+63681,
+48297,
+15930,
+5604,
+53062,
+26013,
+56073,
+23181,
+53588,
+32029,
+18504,
+47823,
+23836,
+4444,
+58800,
+19450,
+33253,
+29766,
+37262,
+31541,
+30031,
+7545,
+31901,
+59336,
+35169,
+35710,
+29746,
+19186,
+44084,
+19257,
+59620,
+64685,
+6547,
+56340,
+42811,
+12696,
+49026,
+16939,
+14211,
+57113,
+38500,
+30503,
+52529,
+18931,
+47956,
+30447,
+4308,
+39454,
+35237,
+9462,
+42358,
+2854,
+11777,
+563,
+582,
+36985,
+17102,
+23184,
+16687,
+50273,
+38388,
+16308,
+42894,
+1839,
+18273,
+4258,
+17020,
+34635,
+17653,
+51628,
+59464,
+61987,
+25503,
+29524,
+54435,
+33168,
+18983,
+27567,
+42262,
+2234,
+38942,
+48497,
+57080,
+60074,
+28090,
+12604,
+40779,
+46081,
+56062,
+43049,
+16249,
+34173,
+44928,
+7732,
+9315,
+33072,
+25936,
+12127,
+7442,
+52295,
+24234,
+56203,
+38496,
+33659,
+47417,
+7264,
+44411,
+9004,
+59592,
+56971,
+39767,
+26845,
+34410,
+55084,
+13062,
+59929,
+17370,
+15609,
+30410,
+48080,
+57665,
+1200,
+17081,
+26723,
+18200,
+40106,
+11642,
+41756,
+47423,
+31400,
+5728,
+44638,
+8415,
+50096,
+48759,
+26658,
+56184,
+57458,
+20746,
+53389,
+35308,
+53091,
+21629,
+27249,
+16170,
+41874,
+56301,
+22263,
+30759,
+34246,
+29988,
+25242,
+33046,
+52360,
+29999,
+7588,
+42325,
+65490,
+55565,
+3524,
+60388,
+12617,
+3081,
+6997,
+41760,
+47012,
+30905,
+58131,
+25021,
+60648,
+57909,
+48679,
+43288,
+31765,
+42788,
+27068,
+56500,
+43482,
+21602,
+61667,
+56374,
+62455,
+6978,
+23778,
+58973,
+4934,
+52497,
+33536,
+46137,
+54636,
+57884,
+11983,
+61431,
+57747,
+54036,
+59993,
+14059,
+64107,
+43249,
+32918,
+11323,
+18897,
+21360,
+31237,
+48945,
+17990,
+34192,
+13113,
+55608,
+21488,
+33137,
+10643,
+47910,
+9092,
+11086,
+2862,
+61967,
+10578,
+41458,
+18066,
+8170,
+24432,
+24056,
+49623,
+16759,
+19056,
+42593,
+44774,
+21551,
+18152,
+43927,
+37754,
+93,
+3487,
+40304,
+2575,
+3690,
+10548,
+18993,
+20003,
+14603,
+13217,
+30340,
+59585,
+37081,
+45551,
+26406,
+36759,
+18530,
+38734,
+45426,
+54676,
+60149,
+11303,
+53531,
+54849,
+1761,
+51590,
+60552,
+11349,
+21787,
+19700,
+1315,
+250,
+7573,
+64491,
+49044,
+23570,
+62132,
+34406,
+43679,
+13494,
+40615,
+11530,
+49266,
+49333,
+17357,
+56065,
+21473,
+33563,
+2143,
+26442,
+25561,
+12751,
+59047,
+62094,
+37720,
+62501,
+692,
+56081,
+31657,
+16850,
+39597,
+9506,
+18069,
+40161,
+3622,
+53364,
+61096,
+37301,
+39488,
+3380,
+46503,
+5865,
+35719,
+21051,
+3304,
+59252,
+63108,
+17249,
+47389,
+14761,
+51762,
+45521,
+5001,
+24882,
+10955,
+59610,
+39958,
+48845,
+55176,
+44074,
+40257,
+38057,
+17378,
+29253,
+22173,
+34200,
+367,
+25824,
+21757,
+40619,
+17221,
+47154,
+41262,
+11705,
+45008,
+57464,
+32599,
+55231,
+2812,
+16659,
+53001,
+30388,
+7644,
+5556,
+62224,
+48105,
+31666,
+29155,
+2305,
+30098,
+14768,
+46803,
+45992,
+16791,
+13462,
+46850,
+47949,
+35637,
+33586,
+57491,
+5068,
+27126,
+6582,
+17224,
+26056,
+58976,
+57216,
+11084,
+9094,
+46248,
+19569,
+5972,
+21715,
+41446,
+51356,
+57436,
+37438,
+55926,
+43831,
+17550,
+55329,
+52690,
+33647,
+1156,
+11613,
+5469,
+47722,
+40201,
+1610,
+18999,
+39575,
+8574,
+28073,
+49330,
+53313,
+24381,
+46193,
+24992,
+58243,
+28182,
+54876,
+15045,
+6600,
+6236,
+13514,
+21643,
+51564,
+57109,
+1152,
+60195,
+61955,
+30674,
+48602,
+50100,
+53374,
+42329,
+8490,
+28891,
+52647,
+11584,
+44935,
+47618,
+11036,
+24685,
+35796,
+45456,
+29398,
+36324,
+8883,
+6087,
+41952,
+15471,
+10849,
+45442,
+31354,
+53599,
+19409,
+2611,
+39268,
+31784,
+21358,
+18899,
+58856,
+54226,
+15687,
+31487,
+18119,
+6920,
+21127,
+41247,
+6358,
+3670,
+63842,
+54512,
+20731,
+47864,
+46695,
+18461,
+3171,
+56881,
+19363,
+31832,
+63016,
+50116,
+8191,
+9,
+19662,
+27595,
+25747,
+44282,
+41317,
+63154,
+56321,
+19751,
+37564,
+38170,
+28181,
+58322,
+24993,
+25286,
+26504,
+1356,
+22555,
+5629,
+25529,
+39105,
+35528,
+8892,
+57660,
+64631,
+870,
+39880,
+29814,
+19428,
+5253,
+52478,
+61558,
+62589,
+25731,
+26471,
+38783,
+23795,
+61024,
+40889,
+22156,
+8351,
+30643,
+19713,
+59963,
+26578,
+37934,
+6001,
+1082,
+2119,
+23279,
+60451,
+63115,
+54358,
+38527,
+50734,
+24180,
+29589,
+48579,
+3634,
+22051,
+848,
+58745,
+22979,
+38653,
+57927,
+28295,
+13548,
+32443,
+54632,
+64378,
+38429,
+51508,
+16460,
+61055,
+34488,
+58072,
+28511,
+5005,
+60969,
+59554,
+48721,
+47851,
+17648,
+64398,
+26665,
+49880,
+41690,
+21985,
+49504,
+38979,
+57456,
+56186,
+12807,
+43561,
+63680,
+58723,
+64249,
+50465,
+36294,
+27552,
+27495,
+3817,
+14334,
+8404,
+16087,
+46347,
+60596,
+39363,
+19129,
+21423,
+27511,
+48694,
+53261,
+12425,
+57305,
+34350,
+48165,
+49930,
+22380,
+48358,
+11519,
+32047,
+16101,
+25020,
+58555,
+30906,
+5370,
+52189,
+48087,
+62999,
+50426,
+9053,
+16150,
+60340,
+29755,
+54704,
+963,
+17473,
+12074,
+20886,
+48698,
+15636,
+47449,
+64672,
+31480,
+14101,
+63693,
+33214,
+45576,
+53084,
+43659,
+55080,
+61420,
+50746,
+38982,
+18356,
+49351,
+43336,
+7461,
+53409,
+6913,
+13243,
+10556,
+32456,
+50495,
+19273,
+19420,
+55384,
+47301,
+62652,
+57789,
+35024,
+14639,
+12325,
+8992,
+9606,
+60906,
+8593,
+22200,
+7440,
+12129,
+20224,
+28510,
+58180,
+34489,
+14201,
+46678,
+44794,
+61124,
+29457,
+37882,
+22214,
+57198,
+36841,
+29411,
+52661,
+1411,
+34394,
+48776,
+46483,
+5422,
+36679,
+16069,
+54172,
+28969,
+57265,
+41442,
+61564,
+32786,
+26570,
+48243,
+42875,
+4154,
+42951,
+44825,
+27860,
+24867,
+26463,
+35189,
+42506,
+35564,
+27104,
+48620,
+24250,
+46529,
+55218,
+35361,
+3046,
+4948,
+24632,
+51360,
+5766,
+6149,
+48200,
+2084,
+42215,
+61466,
+34734,
+63855,
+50645,
+21320,
+52339,
+8632,
+7638,
+12827,
+5912,
+4379,
+12465,
+12855,
+18562,
+52581,
+87,
+48409,
+65163,
+50779,
+10704,
+64485,
+13283,
+23529,
+47860,
+59602,
+3988,
+35505,
+6836,
+33637,
+34032,
+48927,
+2917,
+35824,
+65077,
+39980,
+55411,
+43058,
+18104,
+60463,
+15749,
+5090,
+15320,
+16705,
+60293,
+26,
+41265,
+39570,
+62219,
+10611,
+8505,
+63450,
+56584,
+18501,
+42390,
+65286,
+7079,
+1047,
+38085,
+43452,
+23466,
+17288,
+12279,
+40524,
+55872,
+60757,
+44570,
+41747,
+28228,
+34289,
+35537,
+6970,
+23213,
+23374,
+7986,
+38181,
+45478,
+31077,
+38071,
+44580,
+28632,
+17346,
+61794,
+32139,
+59855,
+48476,
+25920,
+48808,
+46854,
+2404,
+21883,
+25076,
+28294,
+58191,
+38654,
+48309,
+48449,
+41221,
+16111,
+47905,
+37875,
+4723,
+45297,
+36699,
+25489,
+23637,
+48523,
+16496,
+31389,
+52036,
+48678,
+58552,
+60649,
+17153,
+21797,
+51789,
+10432,
+36460,
+48730,
+25602,
+56904,
+7525,
+52711,
+40892,
+51728,
+54294,
+35148,
+7159,
+59982,
+60,
+8596,
+44414,
+64089,
+1088,
+53562,
+11982,
+58532,
+54637,
+27499,
+55941,
+50764,
+40813,
+22573,
+15118,
+52226,
+17663,
+23750,
+33102,
+20965,
+23792,
+18814,
+21085,
+24962,
+24241,
+393,
+39423,
+15323,
+23938,
+26915,
+14301,
+43034,
+54654,
+62328,
+17468,
+2901,
+32905,
+46711,
+5143,
+47816,
+4613,
+31690,
+22019,
+42481,
+59631,
+55516,
+36151,
+9618,
+17334,
+46306,
+1354,
+26506,
+315,
+46665,
+34275,
+49257,
+468,
+8525,
+50080,
+10309,
+910,
+19065,
+43116,
+3120,
+16380,
+60497,
+63353,
+12550,
+48129,
+6473,
+55506,
+10828,
+41209,
+31669,
+31979,
+16832,
+52102,
+41980,
+65257,
+2350,
+8559,
+20466,
+20616,
+17052,
+65342,
+20608,
+6908,
+56223,
+31633,
+47168,
+43841,
+65393,
+60955,
+59372,
+62040,
+21428,
+43570,
+46476,
+59029,
+33822,
+46458,
+35023,
+58085,
+62653,
+15027,
+29695,
+4523,
+26323,
+23167,
+61920,
+19122,
+37014,
+6699,
+24191,
+15261,
+21871,
+35673,
+19873,
+59012,
+19640,
+41410,
+23702,
+18620,
+14695,
+58764,
+59954,
+13045,
+21989,
+42986,
+40477,
+47189,
+31989,
+57288,
+15249,
+17075,
+27239,
+41847,
+51921,
+28483,
+64712,
+5648,
+60967,
+5007,
+54035,
+58529,
+61432,
+37672,
+39080,
+19836,
+5776,
+40410,
+32613,
+51964,
+56764,
+16191,
+25909,
+65495,
+44758,
+30795,
+4719,
+1570,
+9721,
+9661,
+45630,
+62407,
+37723,
+35193,
+62836,
+46775,
+58803,
+38965,
+17296,
+14846,
+30057,
+38107,
+36510,
+22731,
+57503,
+32023,
+62413,
+52716,
+34799,
+17841,
+53332,
+59067,
+23910,
+37011,
+2418,
+1464,
+27871,
+52448,
+56079,
+694,
+61106,
+18449,
+37913,
+16355,
+22620,
+2565,
+64593,
+21893,
+65086,
+18497,
+1528,
+27776,
+7997,
+64694,
+24519,
+2762,
+63593,
+52181,
+10218,
+33786,
+42888,
+56090,
+3810,
+18264,
+52793,
+47661,
+4683,
+16805,
+39937,
+27246,
+21271,
+29704,
+1199,
+58602,
+48081,
+10146,
+55359,
+64630,
+58232,
+8893,
+22866,
+17690,
+52398,
+9853,
+39407,
+29485,
+47559,
+41827,
+51012,
+48513,
+54970,
+33948,
+20431,
+34338,
+38643,
+63586,
+64765,
+32727,
+16183,
+27574,
+531,
+55806,
+9680,
+48228,
+21959,
+8217,
+3167,
+45037,
+56113,
+37252,
+49594,
+12486,
+53094,
+52143,
+10419,
+37820,
+36035,
+26286,
+31771,
+6216,
+40312,
+60630,
+40277,
+39304,
+64776,
+59243,
+18830,
+20994,
+44711,
+23138,
+39509,
+42785,
+9098,
+13542,
+35598,
+603,
+10960,
+27475,
+32624,
+40876,
+47219,
+16896,
+23455,
+11627,
+40677,
+49161,
+53139,
+29024,
+48994,
+53490,
+43433,
+41853,
+58844,
+32035,
+9798,
+5298,
+39108,
+31294,
+51461,
+29112,
+10507,
+59008,
+1276,
+25097,
+31678,
+16483,
+13634,
+22534,
+24308,
+8151,
+46959,
+23493,
+24970,
+8969,
+45760,
+2967,
+35353,
+36540,
+51866,
+47785,
+19565,
+39971,
+3531,
+53607,
+23504,
+34402,
+42162,
+7591,
+27357,
+11478,
+46743,
+33113,
+59468,
+35286,
+44473,
+33244,
+63750,
+37760,
+7851,
+844,
+37074,
+53470,
+25589,
+26413,
+1646,
+22165,
+61378,
+12500,
+47792,
+45560,
+26349,
+18712,
+37902,
+1137,
+38874,
+5013,
+22339,
+12419,
+43972,
+45683,
+47745,
+46309,
+40069,
+51428,
+42771,
+29918,
+21862,
+54195,
+40880,
+31895,
+48412,
+22991,
+54504,
+40724,
+32022,
+57714,
+22732,
+62756,
+1109,
+47412,
+64681,
+23533,
+5746,
+5566,
+48727,
+38382,
+5067,
+58361,
+33587,
+22793,
+59756,
+1441,
+15499,
+25256,
+33269,
+45609,
+48630,
+35266,
+48016,
+62422,
+30707,
+22011,
+52667,
+45580,
+25758,
+19757,
+31383,
+10912,
+33325,
+65009,
+47506,
+62183,
+38213,
+32598,
+58385,
+45009,
+14828,
+23915,
+18857,
+20745,
+58585,
+56185,
+58165,
+38980,
+50748,
+59400,
+13918,
+11193,
+54783,
+25727,
+62280,
+27033,
+81,
+10391,
+50661,
+6178,
+17544,
+13721,
+63384,
+20192,
+17066,
+37437,
+58345,
+51357,
+41087,
+56148,
+48330,
+33577,
+21172,
+63492,
+29467,
+53068,
+43624,
+2965,
+45762,
+29780,
+27192,
+38203,
+18756,
+3205,
+42459,
+35892,
+32135,
+18341,
+35435,
+44974,
+62730,
+61077,
+21636,
+13925,
+17390,
+28351,
+29887,
+23299,
+4988,
+28047,
+33127,
+23062,
+12186,
+27244,
+39939,
+50135,
+62172,
+61778,
+4169,
+21108,
+36506,
+25753,
+50832,
+51984,
+15007,
+41228,
+7968,
+18579,
+34967,
+13752,
+41926,
+31748,
+57374,
+34809,
+47978,
+1020,
+10528,
+34808,
+57380,
+31749,
+22651,
+4390,
+6195,
+56029,
+47470,
+7035,
+41106,
+13433,
+23559,
+42486,
+33486,
+40135,
+55246,
+20299,
+11859,
+63510,
+27982,
+809,
+18853,
+54446,
+59788,
+44137,
+35166,
+49649,
+23593,
+15420,
+7281,
+65529,
+18726,
+48061,
+37043,
+38054,
+18485,
+9761,
+48855,
+45772,
+63691,
+14103,
+60264,
+55879,
+33723,
+8266,
+16522,
+51604,
+12622,
+28873,
+42534,
+29265,
+50232,
+5992,
+42617,
+43577,
+7904,
+62371,
+41456,
+10580,
+30357,
+32083,
+6368,
+64509,
+20932,
+53929,
+18792,
+32426,
+61316,
+14708,
+34349,
+58141,
+12426,
+61820,
+21782,
+60309,
+3286,
+57144,
+59595,
+59548,
+2179,
+38271,
+26086,
+19898,
+37494,
+47138,
+63447,
+15248,
+57759,
+31990,
+11549,
+42688,
+37815,
+27274,
+33876,
+25394,
+32074,
+52355,
+1960,
+55278,
+16414,
+5862,
+56059,
+30318,
+31813,
+8206,
+49016,
+27640,
+10763,
+5365,
+41441,
+58050,
+28970,
+10606,
+55059,
+65244,
+9652,
+9635,
+35456,
+32349,
+1567,
+38830,
+35518,
+45118,
+41545,
+2618,
+34884,
+38707,
+22494,
+6696,
+15825,
+27560,
+59365,
+21390,
+62511,
+51499,
+32417,
+20195,
+57182,
+2582,
+8653,
+21622,
+53863,
+27899,
+50441,
+44099,
+34587,
+3747,
+41425,
+44062,
+31976,
+32666,
+53270,
+53169,
+35602,
+39018,
+30046,
+28766,
+3293,
+11083,
+58354,
+58977,
+60592,
+11865,
+41739,
+52809,
+22777,
+11576,
+59911,
+25074,
+21885,
+4334,
+63618,
+26619,
+56707,
+1879,
+6729,
+36840,
+58063,
+22215,
+49139,
+62968,
+19167,
+30530,
+43995,
+51269,
+64532,
+63645,
+27207,
+43380,
+7322,
+34895,
+10490,
+2581,
+57238,
+20196,
+45861,
+24990,
+46195,
+47091,
+38808,
+4726,
+21574,
+28169,
+51842,
+36043,
+26702,
+64652,
+19499,
+6632,
+46943,
+49278,
+8696,
+61303,
+46185,
+17331,
+38241,
+63730,
+31826,
+27427,
+3253,
+15489,
+48627,
+28008,
+62309,
+51931,
+34429,
+711,
+51639,
+65123,
+56969,
+59594,
+57299,
+3287,
+39088,
+38615,
+39906,
+19299,
+39639,
+47803,
+61262,
+56169,
+51202,
+7385,
+49451,
+26391,
+63097,
+50813,
+38189,
+64033,
+44390,
+60917,
+34011,
+20898,
+6947,
+15442,
+37304,
+64050,
+31270,
+41774,
+40983,
+55024,
+38499,
+58683,
+14212,
+55449,
+1151,
+58313,
+51565,
+38537,
+32477,
+64410,
+3435,
+19170,
+24218,
+2385,
+39911,
+19891,
+16293,
+40122,
+3160,
+28654,
+37585,
+48982,
+4827,
+62891,
+32881,
+15880,
+10875,
+55276,
+1962,
+3373,
+4993,
+25683,
+48771,
+60073,
+58640,
+48498,
+54210,
+56760,
+54538,
+18456,
+2238,
+10275,
+34855,
+23092,
+34661,
+61350,
+33020,
+37117,
+5787,
+48882,
+44158,
+26767,
+24761,
+34601,
+44717,
+37855,
+50859,
+26949,
+8933,
+60337,
+20813,
+12939,
+3916,
+62610,
+31560,
+53299,
+23207,
+7233,
+36647,
+17156,
+62188,
+16581,
+24857,
+20974,
+13274,
+4928,
+38201,
+27194,
+20661,
+47322,
+47486,
+54925,
+7567,
+2088,
+50291,
+30188,
+10367,
+13762,
+28491,
+49761,
+2502,
+56400,
+61858,
+3440,
+61805,
+24259,
+42881,
+51745,
+9914,
+41597,
+38624,
+65068,
+7703,
+15458,
+26616,
+18805,
+6347,
+51838,
+54175,
+699,
+16198,
+22908,
+29770,
+55579,
+35037,
+11061,
+18681,
+63135,
+12093,
+40226,
+537,
+50311,
+6036,
+50698,
+41506,
+28084,
+36866,
+28568,
+13742,
+23902,
+24880,
+5003,
+28513,
+20735,
+39650,
+63186,
+7726,
+45072,
+25362,
+45980,
+63792,
+50469,
+39766,
+58613,
+59593,
+57146,
+65124,
+11295,
+61645,
+18158,
+25961,
+7208,
+37465,
+9774,
+21945,
+16984,
+56347,
+20677,
+6362,
+48286,
+18150,
+21553,
+52880,
+24189,
+6701,
+63240,
+23036,
+44762,
+32902,
+4630,
+7459,
+43338,
+65476,
+7647,
+21124,
+56535,
+33443,
+55836,
+51532,
+16769,
+1615,
+46761,
+28195,
+55462,
+54420,
+21432,
+32346,
+49834,
+36768,
+14360,
+6034,
+50313,
+35161,
+36199,
+60601,
+13042,
+49052,
+51905,
+41431,
+29424,
+35744,
+4856,
+36298,
+12726,
+42762,
+56384,
+39866,
+60654,
+13197,
+7524,
+57900,
+25603,
+19293,
+16961,
+28303,
+60613,
+56206,
+22809,
+26098,
+56596,
+6650,
+15156,
+31855,
+45033,
+50553,
+15634,
+48700,
+32026,
+13568,
+62954,
+8301,
+43916,
+19362,
+58261,
+3172,
+37613,
+60466,
+4745,
+44728,
+401,
+53637,
+48888,
+38702,
+54594,
+15388,
+4166,
+12300,
+960,
+20460,
+22483,
+15196,
+65469,
+5870,
+41750,
+41647,
+32406,
+18946,
+25120,
+36011,
+11104,
+44192,
+16505,
+13862,
+50741,
+33919,
+9648,
+14278,
+32859,
+5731,
+13504,
+14671,
+40171,
+16258,
+8373,
+4235,
+33311,
+62644,
+59714,
+5393,
+32648,
+24406,
+50109,
+1182,
+54569,
+62323,
+43162,
+20303,
+41888,
+18267,
+1497,
+32682,
+38710,
+52392,
+51213,
+46636,
+6683,
+26759,
+23201,
+33148,
+15847,
+3148,
+23581,
+50717,
+53783,
+27921,
+49791,
+60822,
+35411,
+12025,
+52290,
+47286,
+15360,
+25412,
+63609,
+41391,
+49149,
+23833,
+47893,
+15985,
+10533,
+12923,
+63251,
+49964,
+2825,
+17510,
+27382,
+32158,
+30162,
+24362,
+11135,
+55751,
+53596,
+17494,
+29812,
+39882,
+3764,
+39828,
+59305,
+4997,
+23748,
+17665,
+25298,
+64393,
+48050,
+39231,
+26303,
+13079,
+28225,
+5873,
+16190,
+57738,
+51965,
+14392,
+54537,
+57077,
+54211,
+2038,
+4083,
+19017,
+62440,
+24027,
+44997,
+20381,
+46956,
+30572,
+53150,
+12229,
+60892,
+37864,
+24800,
+5508,
+51191,
+32653,
+56380,
+25218,
+45715,
+41725,
+54567,
+1184,
+21183,
+6142,
+54304,
+27629,
+32575,
+60261,
+24874,
+38791,
+15897,
+11053,
+52797,
+18182,
+21288,
+60868,
+50305,
+34794,
+62304,
+42158,
+23681,
+22497,
+48057,
+30113,
+14728,
+10226,
+61802,
+27820,
+55166,
+1878,
+57202,
+26620,
+18039,
+14943,
+46597,
+61472,
+27851,
+41464,
+53665,
+47060,
+41272,
+52268,
+35422,
+50811,
+63099,
+15760,
+19507,
+64211,
+54508,
+47466,
+25378,
+53290,
+21167,
+17441,
+48964,
+1310,
+30245,
+51885,
+9670,
+16719,
+9224,
+10288,
+28849,
+46622,
+33116,
+44852,
+20010,
+48737,
+5018,
+22826,
+34261,
+5925,
+25581,
+25002,
+21277,
+10353,
+49581,
+56530,
+42055,
+55292,
+18650,
+13438,
+29212,
+7245,
+59451,
+46927,
+20850,
+35427,
+3414,
+26638,
+3369,
+30008,
+7304,
+7878,
+42672,
+2704,
+49499,
+16515,
+6592,
+54365,
+56163,
+2658,
+41014,
+14163,
+53753,
+40014,
+38994,
+14863,
+10940,
+4811,
+10202,
+6450,
+12700,
+16353,
+37915,
+47688,
+37980,
+29994,
+43586,
+27849,
+61474,
+24412,
+64134,
+37818,
+10421,
+10295,
+59230,
+45906,
+38986,
+30306,
+46874,
+28450,
+38587,
+8481,
+5201,
+63938,
+34492,
+21853,
+49809,
+43227,
+6649,
+56895,
+26099,
+1942,
+13358,
+39542,
+64885,
+29095,
+11770,
+28964,
+43767,
+45307,
+18500,
+57968,
+63451,
+23324,
+30200,
+60803,
+4792,
+22070,
+24335,
+42704,
+50012,
+50606,
+29091,
+37480,
+40999,
+3954,
+40566,
+7972,
+19778,
+39968,
+22637,
+32480,
+40349,
+39994,
+18801,
+1336,
+19118,
+59457,
+35082,
+270,
+1452,
+25440,
+19718,
+17885,
+64567,
+36185,
+5476,
+49767,
+19734,
+53211,
+7456,
+55932,
+3024,
+14040,
+10629,
+21598,
+16031,
+26560,
+40247,
+33442,
+56939,
+21125,
+6922,
+5946,
+42054,
+56660,
+49582,
+25934,
+33074,
+24270,
+25969,
+34001,
+37538,
+58827,
+64483,
+10706,
+18321,
+12295,
+35653,
+40611,
+51016,
+27114,
+20444,
+44311,
+29109,
+12030,
+11898,
+16651,
+6365,
+38164,
+11022,
+14068,
+46523,
+50754,
+43481,
+58546,
+27069,
+23129,
+60078,
+52750,
+21534,
+19198,
+50689,
+35482,
+3876,
+19204,
+52376,
+45852,
+19748,
+5742,
+15110,
+7413,
+49388,
+55274,
+10877,
+51651,
+13132,
+2561,
+51699,
+56250,
+63823,
+2220,
+24345,
+3557,
+58778,
+13025,
+32689,
+25047,
+51642,
+36977,
+2998,
+744,
+51287,
+17166,
+14088,
+60584,
+62772,
+17144,
+5536,
+28976,
+23604,
+20874,
+48186,
+2936,
+32379,
+61152,
+52607,
+6540,
+64506,
+17626,
+7028,
+54452,
+7185,
+19290,
+33770,
+22751,
+7931,
+51996,
+13878,
+34774,
+45369,
+2851,
+3333,
+22076,
+42024,
+29433,
+41324,
+46068,
+11201,
+56265,
+40040,
+5818,
+20803,
+30536,
+40310,
+6218,
+50637,
+23961,
+59551,
+31450,
+19003,
+23110,
+33063,
+30718,
+39045,
+13004,
+64898,
+15923,
+20221,
+445,
+15724,
+47296,
+62545,
+31904,
+61857,
+57023,
+2503,
+42532,
+28875,
+63888,
+35593,
+43990,
+63351,
+60499,
+43302,
+28466,
+3231,
+23381,
+58926,
+3144,
+39865,
+56909,
+42763,
+2663,
+25217,
+56741,
+32654,
+49659,
+44123,
+40949,
+62454,
+58542,
+61668,
+22146,
+32697,
+60890,
+12231,
+11665,
+1265,
+60206,
+30089,
+61929,
+49985,
+29122,
+9592,
+51854,
+14874,
+59361,
+44598,
+31530,
+60348,
+30003,
+22143,
+63268,
+11993,
+18519,
+63390,
+20676,
+56958,
+16985,
+34444,
+54651,
+47638,
+14483,
+42810,
+58689,
+6548,
+61355,
+9453,
+43556,
+9921,
+45042,
+9278,
+1011,
+21307,
+11987,
+42014,
+2868,
+33352,
+63744,
+56139,
+49560,
+5740,
+19750,
+58248,
+63155,
+1102,
+13251,
+50944,
+12644,
+18001,
+48817,
+8860,
+55250,
+3843,
+21500,
+16924,
+41329,
+37682,
+59197,
+26344,
+51142,
+60434,
+22262,
+58576,
+41875,
+17005,
+41795,
+61331,
+62530,
+54964,
+46017,
+55576,
+45844,
+63205,
+64807,
+900,
+62483,
+17904,
+46296,
+13650,
+55869,
+2245,
+52203,
+31317,
+15363,
+16317,
+41549,
+4296,
+44399,
+61808,
+17985,
+37572,
+14126,
+1068,
+5396,
+42138,
+19140,
+23804,
+40039,
+56426,
+11202,
+23822,
+35874,
+32606,
+20494,
+9204,
+64527,
+64174,
+15062,
+17461,
+26757,
+6685,
+37847,
+63822,
+56476,
+51700,
+49599,
+61142,
+40764,
+6115,
+23329,
+28185,
+44293,
+16349,
+48795,
+54929,
+10480,
+45291,
+47400,
+49114,
+21268,
+21632,
+47767,
+51600,
+26181,
+39563,
+52745,
+15536,
+34771,
+45486,
+31632,
+57804,
+6909,
+32466,
+15869,
+60058,
+11857,
+20301,
+43164,
+53958,
+22558,
+48970,
+17760,
+33668,
+6078,
+30457,
+23246,
+22808,
+56898,
+60614,
+38495,
+58621,
+24235,
+25152,
+4050,
+28361,
+36365,
+43413,
+4806,
+32081,
+30359,
+26926,
+62851,
+14817,
+6675,
+59697,
+24402,
+12806,
+58164,
+57457,
+58586,
+26659,
+39070,
+16020,
+29319,
+48624,
+62806,
+46261,
+6553,
+43689,
+16386,
+41706,
+25487,
+36701,
+51201,
+57135,
+61263,
+35860,
+53522,
+41083,
+2657,
+56637,
+54366,
+40702,
+46904,
+16234,
+23164,
+38896,
+49909,
+52621,
+11378,
+20833,
+2074,
+55210,
+12574,
+48329,
+57433,
+41088,
+29164,
+13614,
+37056,
+21648,
+27882,
+16454,
+49559,
+56325,
+63745,
+51084,
+62867,
+12303,
+52845,
+30629,
+20013,
+53504,
+15334,
+26297,
+5830,
+29718,
+40286,
+44195,
+50171,
+21059,
+15850,
+11185,
+23161,
+31168,
+33927,
+1301,
+50991,
+29861,
+37251,
+57630,
+45038,
+63917,
+22184,
+17705,
+24049,
+28404,
+55843,
+24896,
+2786,
+14651,
+52249,
+50139,
+41557,
+36671,
+46096,
+31984,
+16563,
+46123,
+61470,
+46599,
+35263,
+3809,
+57677,
+42889,
+22819,
+13364,
+36891,
+4181,
+9200,
+21982,
+31656,
+58433,
+693,
+57700,
+52449,
+50760,
+30299,
+6864,
+23180,
+58716,
+26014,
+35164,
+44139,
+28290,
+46649,
+47869,
+21472,
+58445,
+17358,
+43048,
+58634,
+46082,
+30317,
+57274,
+5863,
+46505,
+49666,
+5481,
+55986,
+45822,
+5576,
+13562,
+51182,
+2535,
+55847,
+4330,
+64736,
+10405,
+55140,
+40845,
+35664,
+44439,
+25533,
+40952,
+62315,
+51926,
+2635,
+19319,
+4862,
+50525,
+25939,
+7108,
+47469,
+57369,
+6196,
+59383,
+30815,
+53018,
+2425,
+35579,
+64862,
+46577,
+51303,
+18295,
+31154,
+38324,
+61345,
+30979,
+24314,
+49428,
+55574,
+46019,
+50575,
+14080,
+1630,
+52519,
+21605,
+63715,
+44844,
+11514,
+59949,
+43098,
+48536,
+13598,
+4507,
+2109,
+36124,
+12878,
+49412,
+22791,
+33589,
+32590,
+58892,
+46668,
+40095,
+45821,
+56054,
+5482,
+13412,
+26293,
+31942,
+11076,
+39890,
+55053,
+17956,
+61746,
+15512,
+60705,
+39817,
+52346,
+22693,
+20190,
+63386,
+26734,
+49538,
+18538,
+404,
+37705,
+28678,
+23719,
+64524,
+44232,
+42106,
+42649,
+42063,
+39589,
+38080,
+35135,
+5136,
+4479,
+16868,
+21734,
+62577,
+38517,
+55661,
+18431,
+36414,
+9176,
+13684,
+53125,
+50763,
+57881,
+27500,
+1550,
+18086,
+867,
+10949,
+18094,
+20276,
+3023,
+56544,
+7457,
+4632,
+63402,
+25706,
+43830,
+58343,
+37439,
+4820,
+49497,
+2706,
+34670,
+29385,
+7693,
+48204,
+27298,
+35810,
+37223,
+52638,
+27668,
+27420,
+33177,
+19286,
+42904,
+11836,
+40064,
+18441,
+59216,
+36990,
+55884,
+8278,
+30082,
+26699,
+46817,
+14387,
+37575,
+60126,
+40789,
+19254,
+39136,
+18190,
+14329,
+31617,
+8688,
+51260,
+51798,
+292,
+8277,
+55903,
+36991,
+65313,
+34092,
+33722,
+57333,
+60265,
+55815,
+7808,
+19908,
+39154,
+60756,
+57956,
+40525,
+2244,
+56284,
+13651,
+8858,
+48819,
+51111,
+37833,
+4898,
+49632,
+48787,
+41102,
+58921,
+9730,
+7901,
+17310,
+1875,
+2132,
+62742,
+46468,
+5758,
+31182,
+14551,
+4329,
+56048,
+2536,
+15113,
+24895,
+56106,
+28405,
+35898,
+26892,
+1953,
+40752,
+51531,
+56937,
+33444,
+27078,
+23874,
+60317,
+43437,
+46653,
+55296,
+13102,
+14896,
+6688,
+4312,
+40086,
+14620,
+10436,
+47372,
+11803,
+36073,
+18301,
+63966,
+7807,
+55877,
+60266,
+37520,
+13660,
+40275,
+60632,
+51733,
+6615,
+9679,
+57637,
+532,
+26806,
+22400,
+17113,
+45713,
+25220,
+22660,
+61385,
+43245,
+26932,
+74,
+20099,
+29910,
+32328,
+38175,
+43456,
+20324,
+1768,
+52275,
+21420,
+63326,
+34015,
+2654,
+24636,
+12703,
+54978,
+5420,
+46485,
+47051,
+8198,
+16683,
+34054,
+6598,
+15047,
+61222,
+47232,
+52702,
+35670,
+31172,
+40995,
+33580,
+28071,
+8576,
+43593,
+28954,
+31820,
+62045,
+50726,
+12511,
+33529,
+35502,
+51134,
+38576,
+53595,
+56784,
+11136,
+154,
+35095,
+3394,
+58743,
+850,
+58992,
+46620,
+28851,
+4837,
+33501,
+7953,
+60240,
+40974,
+45022,
+63471,
+24502,
+38631,
+2052,
+14015,
+3896,
+25866,
+13229,
+27905,
+27321,
+20138,
+13070,
+61577,
+12433,
+52925,
+29020,
+20366,
+22413,
+15530,
+54900,
+6132,
+24739,
+64036,
+44872,
+31688,
+4615,
+12850,
+35939,
+11914,
+14566,
+58758,
+53500,
+41792,
+61119,
+27658,
+63505,
+48823,
+28309,
+59989,
+54562,
+39494,
+39217,
+19247,
+51704,
+60871,
+45617,
+61218,
+33923,
+14152,
+41181,
+13138,
+7882,
+26131,
+23588,
+4249,
+11150,
+46437,
+17968,
+25703,
+2798,
+11357,
+22014,
+52508,
+14363,
+36050,
+17980,
+3500,
+64003,
+39289,
+32566,
+28460,
+4786,
+18100,
+18430,
+55948,
+38518,
+14019,
+2773,
+19681,
+38257,
+61843,
+32175,
+23174,
+42909,
+29666,
+59558,
+5932,
+17940,
+28607,
+51172,
+29489,
+39721,
+31245,
+60351,
+3755,
+28355,
+20453,
+63670,
+48504,
+1721,
+12979,
+36319,
+64154,
+34287,
+28230,
+34333,
+39813,
+36674,
+5403,
+47941,
+46065,
+13791,
+8521,
+23941,
+13767,
+39724,
+15588,
+3112,
+21980,
+9202,
+20496,
+62347,
+59085,
+52839,
+16513,
+49501,
+21487,
+58514,
+13114,
+39668,
+41538,
+29618,
+42097,
+42792,
+59726,
+13052,
+21927,
+63150,
+25628,
+65359,
+23698,
+63876,
+12911,
+51250,
+32953,
+41337,
+5580,
+16488,
+34099,
+47228,
+54151,
+59275,
+19324,
+79,
+27035,
+35036,
+57001,
+29771,
+45843,
+56293,
+46018,
+56012,
+49429,
+33390,
+52957,
+12776,
+5674,
+43573,
+29232,
+3523,
+58564,
+65491,
+53887,
+5762,
+36835,
+64821,
+51514,
+39799,
+40629,
+893,
+64585,
+3998,
+63850,
+61612,
+36995,
+33533,
+52696,
+30049,
+2014,
+43031,
+35884,
+51417,
+37149,
+30208,
+20895,
+45870,
+26751,
+46726,
+42776,
+27549,
+36333,
+43628,
+20328,
+17732,
+4277,
+24068,
+51281,
+14929,
+36448,
+11967,
+35053,
+54240,
+3894,
+14017,
+38520,
+11371,
+11625,
+23457,
+36150,
+57846,
+59632,
+12600,
+13331,
+63262,
+4007,
+40700,
+54368,
+62152,
+10827,
+57821,
+6474,
+21236,
+41168,
+25516,
+4434,
+62553,
+21356,
+31786,
+58794,
+6057,
+39347,
+509,
+49218,
+23990,
+62921,
+62706,
+21700,
+65387,
+5719,
+1754,
+59115,
+1532,
+26250,
+43813,
+11933,
+984,
+55092,
+34583,
+5127,
+44080,
+62847,
+34439,
+20311,
+59527,
+1517,
+14205,
+65192,
+19624,
+1998,
+45572,
+65062,
+12583,
+54419,
+56931,
+28196,
+22444,
+31676,
+25099,
+28913,
+6241,
+40959,
+13110,
+47333,
+12685,
+52132,
+1150,
+57111,
+14213,
+12971,
+11424,
+42438,
+36895,
+63323,
+19132,
+16196,
+701,
+16712,
+37230,
+44948,
+60327,
+26787,
+3957,
+12966,
+47520,
+24792,
+24809,
+41904,
+51780,
+33689,
+64649,
+14926,
+25883,
+3052,
+47635,
+43037,
+24778,
+32911,
+24663,
+50299,
+12083,
+63873,
+46937,
+1340,
+43057,
+57984,
+39981,
+33435,
+37605,
+31811,
+30320,
+13577,
+33619,
+36791,
+63901,
+36357,
+52374,
+19206,
+13322,
+2372,
+23918,
+28439,
+6345,
+18807,
+17455,
+7874,
+10179,
+61759,
+20580,
+41821,
+44748,
+47300,
+58088,
+19421,
+62194,
+12022,
+37411,
+18123,
+858,
+11140,
+31643,
+19496,
+47726,
+20008,
+44854,
+44437,
+35666,
+39027,
+19025,
+31557,
+43178,
+61871,
+4239,
+11640,
+40108,
+55226,
+64629,
+57662,
+10147,
+3779,
+32128,
+25109,
+19636,
+22028,
+2265,
+33695,
+34483,
+38796,
+2322,
+41078,
+37164,
+54522,
+20369,
+31637,
+12291,
+6447,
+21640,
+59027,
+46478,
+816,
+60725,
+8910,
+59088,
+43899,
+51614,
+60920,
+52689,
+58340,
+17551,
+5501,
+41584,
+33314,
+36577,
+40481,
+63176,
+62400,
+13355,
+7857,
+31752,
+32108,
+60619,
+18048,
+30560,
+32198,
+50808,
+8825,
+3736,
+16679,
+21662,
+17342,
+2684,
+50722,
+15454,
+32602,
+397,
+65044,
+20115,
+37992,
+31882,
+13101,
+55829,
+46654,
+41051,
+18649,
+56658,
+42056,
+18554,
+19844,
+62295,
+49725,
+42873,
+48245,
+20889,
+5030,
+5381,
+10729,
+61403,
+16413,
+57277,
+1961,
+57087,
+10876,
+56482,
+49389,
+23412,
+12838,
+36774,
+53327,
+64969,
+47969,
+54913,
+37560,
+31802,
+63298,
+8464,
+30683,
+16287,
+54984,
+28366,
+12285,
+59921,
+5335,
+65059,
+42181,
+65263,
+3842,
+56312,
+8861,
+36689,
+20298,
+57360,
+40136,
+4457,
+55115,
+8925,
+6308,
+6161,
+29281,
+45594,
+34531,
+51063,
+22221,
+63158,
+15462,
+2811,
+58383,
+32600,
+15456,
+7705,
+64628,
+55361,
+40109,
+42145,
+10872,
+21043,
+65429,
+1226,
+35360,
+58030,
+46530,
+2432,
+63410,
+23090,
+34857,
+31115,
+12573,
+56151,
+2075,
+19479,
+11147,
+36432,
+63374,
+62537,
+64795,
+22112,
+52545,
+31193,
+18378,
+52815,
+46440,
+23335,
+40018,
+697,
+54177,
+12873,
+23026,
+61895,
+19,
+42600,
+52094,
+50848,
+44259,
+8875,
+15957,
+22987,
+50545,
+24767,
+13382,
+22782,
+44073,
+58402,
+48846,
+54767,
+32876,
+22048,
+35348,
+8509,
+51776,
+2130,
+1877,
+56709,
+27821,
+13906,
+15623,
+15173,
+15001,
+44285,
+60946,
+23660,
+48534,
+43100,
+19651,
+49694,
+52908,
+16105,
+16135,
+265,
+46074,
+61588,
+65455,
+8092,
+14877,
+43870,
+29061,
+18107,
+40844,
+56044,
+10406,
+25877,
+61296,
+53899,
+4467,
+62091,
+800,
+9492,
+10398,
+39661,
+33946,
+54972,
+48583,
+25438,
+1454,
+49683,
+23656,
+29682,
+42779,
+61391,
+13393,
+7913,
+27718,
+8924,
+55243,
+4458,
+43903,
+17772,
+30581,
+11301,
+60151,
+44812,
+24424,
+13747,
+7633,
+29078,
+35394,
+50916,
+9715,
+18166,
+21718,
+27768,
+27916,
+54157,
+44706,
+14842,
+34582,
+55479,
+985,
+5119,
+39336,
+16828,
+11458,
+60282,
+13061,
+58609,
+34411,
+48750,
+61419,
+58104,
+43660,
+34570,
+64829,
+24195,
+54468,
+22875,
+30257,
+26315,
+16450,
+48356,
+22382,
+63058,
+53187,
+41290,
+34840,
+38754,
+59599,
+35049,
+40432,
+65243,
+57262,
+10607,
+9299,
+20247,
+64868,
+17955,
+55979,
+39891,
+32781,
+16992,
+28988,
+30641,
+8353,
+62957,
+65106,
+2592,
+19791,
+30892,
+11183,
+15852,
+41883,
+22447,
+25135,
+33377,
+35377,
+44203,
+23054,
+5908,
+36424,
+7959,
+6530,
+51680,
+27543,
+33657,
+38498,
+57115,
+40984,
+61841,
+38259,
+10712,
+42738,
+30510,
+25461,
+43791,
+218,
+52670,
+12598,
+59634,
+58931,
+64516,
+8161,
+30730,
+33839,
+51348,
+49551,
+8742,
+52615,
+33543,
+36171,
+27970,
+35990,
+28941,
+51528,
+27319,
+27907,
+15615,
+27939,
+1908,
+28667,
+61846,
+7052,
+46545,
+45205,
+12044,
+28365,
+55259,
+16288,
+52293,
+7444,
+33082,
+5419,
+55780,
+12704,
+62639,
+17194,
+51695,
+48582,
+55128,
+33947,
+57648,
+48514,
+47015,
+34951,
+4098,
+46016,
+56295,
+62531,
+45408,
+42925,
+3569,
+7032,
+24080,
+8220,
+35181,
+48648,
+24492,
+30657,
+49080,
+16214,
+61569,
+1207,
+38249,
+52213,
+63167,
+53255,
+24506,
+27869,
+1466,
+3977,
+2950,
+13215,
+14605,
+47774,
+6720,
+29381,
+49998,
+40117,
+36566,
+8274,
+10479,
+56239,
+48796,
+3317,
+7566,
+57033,
+47487,
+34979,
+53047,
+35207,
+43858,
+20066,
+1438,
+42994,
+45742,
+46947,
+37559,
+55266,
+47970,
+44783,
+17095,
+51496,
+8996,
+43940,
+65013,
+51378,
+36556,
+18606,
+11043,
+6131,
+55716,
+15531,
+44833,
+13594,
+44221,
+32399,
+7026,
+17628,
+2752,
+59658,
+52192,
+54846,
+32230,
+41396,
+52960,
+18673,
+63513,
+41565,
+38947,
+59233,
+53539,
+44255,
+33079,
+15044,
+58320,
+28183,
+23331,
+633,
+26774,
+18415,
+44531,
+35576,
+40966,
+5141,
+46713,
+33008,
+22210,
+24099,
+50508,
+35844,
+3910,
+20076,
+14514,
+14034,
+1626,
+30977,
+61347,
+61924,
+48682,
+33345,
+1760,
+58467,
+53532,
+32229,
+54889,
+52193,
+11406,
+40000,
+16588,
+38571,
+27956,
+39740,
+46603,
+49165,
+54409,
+643,
+14757,
+7537,
+1053,
+9017,
+47217,
+40878,
+54197,
+19502,
+27277,
+10517,
+22800,
+15482,
+1419,
+17216,
+38367,
+63244,
+48925,
+34034,
+22459,
+31436,
+18311,
+20161,
+29649,
+38512,
+38374,
+60155,
+10256,
+4135,
+14975,
+44817,
+44190,
+11106,
+50592,
+18942,
+63950,
+17315,
+13963,
+3704,
+34901,
+39443,
+32070,
+14048,
+19225,
+63128,
+40254,
+23116,
+41911,
+18454,
+54540,
+31202,
+25726,
+57450,
+11194,
+41378,
+51244,
+53079,
+6067,
+30253,
+23758,
+37896,
+8269,
+60173,
+11488,
+51956,
+18084,
+1552,
+32875,
+55174,
+48847,
+60323,
+61163,
+5651,
+41524,
+29235,
+25316,
+60107,
+30832,
+43207,
+26261,
+25827,
+24840,
+2911,
+5785,
+37119,
+16726,
+16445,
+62354,
+32234,
+37284,
+37402,
+27258,
+11700,
+52891,
+47538,
+13281,
+64487,
+17994,
+44462,
+13986,
+26034,
+3646,
+14431,
+51447,
+4955,
+38945,
+41567,
+12666,
+38722,
+16604,
+5716,
+4873,
+24890,
+11041,
+18608,
+44689,
+16254,
+45129,
+20394,
+38507,
+27948,
+28775,
+18788,
+3796,
+3384,
+10030,
+7717,
+24329,
+26003,
+20458,
+962,
+58120,
+29756,
+42655,
+16227,
+50648,
+5279,
+40130,
+4879,
+36973,
+42295,
+33521,
+3741,
+33400,
+47198,
+38602,
+50065,
+7249,
+20096,
+62952,
+13570,
+51147,
+51414,
+13204,
+13565,
+53591,
+48184,
+20876,
+60148,
+58471,
+45427,
+30304,
+38988,
+37018,
+24216,
+19172,
+5976,
+4058,
+59128,
+23391,
+10314,
+44495,
+49656,
+8449,
+10262,
+9056,
+13887,
+49838,
+765,
+45432,
+62327,
+57859,
+43035,
+47637,
+56344,
+34445,
+36061,
+36804,
+14305,
+16470,
+2834,
+42837,
+26139,
+27863,
+26459,
+13953,
+31229,
+27498,
+57883,
+58533,
+46138,
+20377,
+64377,
+58187,
+32444,
+40012,
+53755,
+23688,
+13618,
+34153,
+8066,
+32213,
+16075,
+41832,
+59143,
+43067,
+61017,
+54139,
+11565,
+29789,
+39321,
+6,
+46813,
+25207,
+62088,
+31458,
+61309,
+51367,
+24386,
+16050,
+20257,
+13967,
+35214,
+35013,
+59730,
+63224,
+3601,
+50673,
+8908,
+60727,
+15387,
+56871,
+38703,
+36957,
+1886,
+904,
+64669,
+9601,
+47444,
+1939,
+4494,
+26070,
+40654,
+7131,
+5928,
+40601,
+60878,
+64933,
+7935,
+38063,
+4741,
+39431,
+4776,
+12952,
+26063,
+62322,
+56831,
+1183,
+56737,
+41726,
+45987,
+31571,
+39493,
+55696,
+59990,
+37709,
+1188,
+34616,
+49105,
+5258,
+45599,
+27413,
+42858,
+42520,
+13142,
+49706,
+50787,
+61410,
+19840,
+18908,
+47692,
+32068,
+39445,
+45104,
+31201,
+54786,
+18455,
+57076,
+56761,
+14393,
+5105,
+64447,
+11977,
+4121,
+25742,
+19726,
+51814,
+2155,
+46114,
+3345,
+1470,
+22411,
+20368,
+55345,
+37165,
+232,
+39942,
+38618,
+40244,
+27372,
+9032,
+51470,
+20730,
+58267,
+63843,
+43202,
+47465,
+56689,
+64212,
+32819,
+40723,
+57506,
+22992,
+17918,
+7239,
+40853,
+18210,
+62058,
+40646,
+10815,
+46930,
+10635,
+59407,
+61603,
+33611,
+19969,
+34732,
+61468,
+46125,
+38862,
+49639,
+8811,
+12741,
+11157,
+20670,
+20923,
+38304,
+28878,
+30499,
+8538,
+46857,
+40834,
+64299,
+30041,
+64240,
+20987,
+22874,
+55075,
+24196,
+5529,
+4540,
+18330,
+18281,
+33594,
+9556,
+25648,
+14642,
+54416,
+278,
+63189,
+62243,
+8320,
+7184,
+56444,
+7029,
+62676,
+62032,
+49958,
+59787,
+57353,
+18854,
+2375,
+60049,
+13450,
+60617,
+32110,
+14155,
+65413,
+14920,
+33167,
+58648,
+29525,
+8682,
+21346,
+10734,
+14946,
+32798,
+17191,
+28946,
+4760,
+3580,
+34865,
+1657,
+32058,
+21431,
+56930,
+55463,
+12584,
+277,
+54458,
+14643,
+28594,
+63528,
+31823,
+24946,
+642,
+54836,
+49166,
+29895,
+18470,
+4021,
+54311,
+41174,
+8752,
+9787,
+65384,
+34063,
+64678,
+10160,
+33895,
+42462,
+34877,
+51511,
+30384,
+46057,
+10330,
+29106,
+61287,
+9857,
+19101,
+37511,
+28711,
+25988,
+47659,
+52795,
+11055,
+44868,
+65167,
+4974,
+62877,
+24525,
+48300,
+6522,
+43977,
+28883,
+29391,
+62151,
+55509,
+40701,
+56162,
+56638,
+6593,
+54317,
+31534,
+28001,
+24152,
+38526,
+58203,
+63116,
+31605,
+34644,
+10496,
+49686,
+9416,
+42377,
+40604,
+59111,
+19657,
+14800,
+20936,
+51618,
+8086,
+10755,
+24160,
+62698,
+38127,
+33225,
+1147,
+19468,
+9110,
+9273,
+41716,
+5633,
+62595,
+24803,
+34479,
+3409,
+13512,
+6238,
+38132,
+45026,
+35460,
+41453,
+31217,
+62072,
+44632,
+39300,
+31533,
+54363,
+6594,
+20154,
+47306,
+15269,
+41173,
+54404,
+4022,
+14353,
+63004,
+2667,
+64580,
+27628,
+56733,
+6143,
+43703,
+50016,
+40400,
+32162,
+36206,
+45287,
+24615,
+35147,
+57895,
+51729,
+34718,
+169,
+16578,
+18173,
+34962,
+45305,
+43769,
+28604,
+45555,
+38557,
+18645,
+54259,
+62824,
+3650,
+52865,
+52773,
+64891,
+23574,
+41069,
+11910,
+46990,
+5602,
+15932,
+34240,
+24391,
+38479,
+25945,
+2878,
+46166,
+23235,
+15415,
+43980,
+62823,
+54281,
+18646,
+42570,
+52454,
+517,
+36059,
+34447,
+61948,
+49446,
+35303,
+17319,
+30397,
+21925,
+13054,
+24765,
+50547,
+412,
+8570,
+3893,
+55524,
+35054,
+23404,
+34955,
+10427,
+5387,
+7068,
+12007,
+1229,
+44385,
+42990,
+23084,
+28257,
+15686,
+58277,
+58857,
+17889,
+45767,
+12088,
+23670,
+38574,
+51136,
+579,
+21809,
+3676,
+32618,
+7015,
+46641,
+2037,
+56759,
+57078,
+48499,
+29017,
+19476,
+50551,
+45035,
+3169,
+18463,
+52366,
+2723,
+25239,
+6630,
+19501,
+54828,
+40879,
+57511,
+21863,
+22641,
+63700,
+26380,
+25159,
+4984,
+7074,
+43751,
+15485,
+42300,
+50124,
+456,
+12334,
+62738,
+9862,
+43524,
+12872,
+55193,
+698,
+57006,
+51839,
+28968,
+58052,
+16070,
+33371,
+44362,
+63554,
+27016,
+10017,
+53913,
+18659,
+54134,
+63532,
+49677,
+12915,
+9383,
+44705,
+55096,
+27917,
+48338,
+49491,
+24680,
+59274,
+55585,
+47229,
+25454,
+42387,
+32032,
+20718,
+21349,
+40284,
+29720,
+32248,
+63898,
+11564,
+54618,
+61018,
+21426,
+62042,
+63531,
+54163,
+18660,
+13343,
+10693,
+25538,
+64704,
+45294,
+38811,
+26000,
+38563,
+17738,
+23306,
+38794,
+34485,
+48672,
+32206,
+53341,
+62593,
+5635,
+59782,
+52565,
+10091,
+23100,
+12200,
+46840,
+62618,
+29715,
+24466,
+53629,
+5406,
+15758,
+63101,
+41907,
+32843,
+22136,
+5025,
+12845,
+31892,
+43867,
+17189,
+32800,
+46381,
+33281,
+23676,
+22964,
+34526,
+58811,
+22191,
+2461,
+17277,
+37435,
+17068,
+36227,
+36241,
+48708,
+10450,
+5256,
+49107,
+36980,
+37114,
+36196,
+26017,
+50286,
+48464,
+60559,
+38649,
+40,
+20781,
+61492,
+51455,
+63909,
+34562,
+22571,
+40815,
+38179,
+7988,
+18795,
+4607,
+14579,
+22894,
+39369,
+37294,
+50712,
+25214,
+5363,
+10765,
+13582,
+53770,
+1711,
+36789,
+33621,
+29986,
+34248,
+22068,
+4794,
+28676,
+37707,
+59992,
+58528,
+57748,
+5008,
+42288,
+59852,
+23188,
+64546,
+26282,
+20181,
+59826,
+59650,
+2818,
+20692,
+9534,
+5589,
+4843,
+36436,
+17178,
+7402,
+33491,
+63364,
+38593,
+91,
+37756,
+1433,
+47120,
+21008,
+50159,
+24921,
+7088,
+16272,
+36525,
+34675,
+62463,
+39754,
+14447,
+4215,
+2255,
+49299,
+47290,
+11556,
+22616,
+17419,
+44594,
+20646,
+16932,
+210,
+33546,
+8251,
+22850,
+52913,
+36666,
+312,
+12564,
+18246,
+11541,
+42922,
+21496,
+44298,
+720,
+30997,
+28744,
+1638,
+11397,
+10920,
+35749,
+63197,
+64081,
+15856,
+38161,
+32086,
+46978,
+65033,
+37004,
+42759,
+8328,
+5627,
+22557,
+56215,
+43165,
+3203,
+18758,
+12889,
+7236,
+20603,
+21659,
+19768,
+53451,
+5706,
+18926,
+23900,
+13744,
+1255,
+23012,
+65407,
+52329,
+62248,
+26451,
+29675,
+12017,
+64780,
+35977,
+24474,
+51073,
+5348,
+51595,
+18791,
+57311,
+20933,
+41360,
+38427,
+64380,
+7201,
+60069,
+4892,
+17507,
+38740,
+34709,
+30972,
+39294,
+43853,
+60569,
+18658,
+54165,
+10018,
+42081,
+7942,
+24861,
+59056,
+30413,
+59136,
+34070,
+40405,
+12748,
+18543,
+37325,
+4466,
+55136,
+61297,
+10174,
+25430,
+6941,
+23782,
+2696,
+65295,
+61187,
+1086,
+64091,
+5761,
+55563,
+65492,
+34259,
+22828,
+33747,
+39176,
+24284,
+59690,
+52388,
+34868,
+37473,
+50503,
+17910,
+22859,
+53305,
+9169,
+64273,
+38192,
+51040,
+40505,
+33229,
+32805,
+40197,
+27898,
+57234,
+21623,
+62680,
+40365,
+44969,
+25611,
+60863,
+29635,
+3338,
+31425,
+29633,
+60865,
+64163,
+15180,
+3269,
+47360,
+59073,
+13224,
+58962,
+4198,
+26940,
+12816,
+44911,
+12797,
+65072,
+38278,
+10794,
+12647,
+22598,
+65509,
+24274,
+7150,
+20530,
+28931,
+30647,
+44738,
+45708,
+37030,
+50132,
+235,
+31381,
+19759,
+22929,
+40395,
+23462,
+53368,
+45669,
+53029,
+24561,
+20517,
+4131,
+4902,
+37508,
+31743,
+42285,
+58776,
+3559,
+23436,
+47678,
+15154,
+6652,
+64878,
+65040,
+46379,
+32802,
+35144,
+37106,
+47781,
+11894,
+15169,
+51424,
+39378,
+37154,
+15599,
+24674,
+36715,
+1545,
+976,
+15966,
+27920,
+56811,
+50718,
+58864,
+44154,
+60214,
+35257,
+34145,
+5734,
+23976,
+32749,
+38969,
+35356,
+1710,
+54047,
+13583,
+36441,
+8948,
+36574,
+34177,
+41383,
+27013,
+40363,
+62682,
+6867,
+64975,
+38669,
+46104,
+23687,
+54629,
+40013,
+56633,
+14164,
+29510,
+22524,
+39201,
+21047,
+59239,
+29642,
+30940,
+12176,
+19710,
+789,
+31774,
+34699,
+31302,
+33049,
+51802,
+38658,
+18164,
+9717,
+29465,
+63494,
+27506,
+39804,
+39068,
+26661,
+60526,
+6764,
+61692,
+44039,
+21241,
+42667,
+38021,
+49620,
+14557,
+34030,
+33639,
+377,
+59123,
+50260,
+36532,
+40608,
+63348,
+44541,
+20828,
+6039,
+20758,
+47063,
+32342,
+13093,
+29346,
+22632,
+9629,
+25474,
+22892,
+14581,
+46756,
+20545,
+49637,
+38864,
+21818,
+49973,
+44630,
+62074,
+48406,
+19884,
+37204,
+15534,
+52747,
+32152,
+61971,
+49765,
+5478,
+9319,
+45237,
+8179,
+2028,
+43800,
+2989,
+6204,
+4944,
+8324,
+23799,
+37101,
+34762,
+5472,
+61766,
+47059,
+56699,
+41465,
+17524,
+15326,
+60300,
+4144,
+52146,
+21522,
+40420,
+16427,
+62542,
+4357,
+28649,
+6573,
+61341,
+64710,
+28485,
+13996,
+29307,
+23286,
+37271,
+17755,
+11742,
+50369,
+15332,
+53506,
+9028,
+48887,
+56874,
+402,
+18540,
+25564,
+62008,
+26435,
+47939,
+5405,
+54106,
+24467,
+11264,
+18548,
+20285,
+29597,
+9830,
+50972,
+32774,
+35290,
+13654,
+41195,
+21371,
+16097,
+35660,
+43968,
+34225,
+27902,
+49738,
+33035,
+65048,
+23503,
+57555,
+3532,
+21723,
+10056,
+64554,
+24227,
+13875,
+19408,
+58285,
+31355,
+17493,
+56783,
+55752,
+38577,
+3594,
+48183,
+54680,
+13566,
+32028,
+58714,
+23182,
+17104,
+5434,
+12819,
+30740,
+15662,
+35734,
+35879,
+27825,
+20282,
+47602,
+1978,
+25931,
+60486,
+24360,
+30164,
+29088,
+10011,
+30485,
+15348,
+48493,
+38005,
+60977,
+35738,
+11981,
+57886,
+1089,
+11004,
+61417,
+48752,
+29798,
+11974,
+7652,
+7006,
+4107,
+43921,
+44229,
+9207,
+23524,
+27436,
+46663,
+317,
+2268,
+9186,
+47019,
+20090,
+48967,
+44254,
+54880,
+59234,
+13801,
+12380,
+3664,
+61300,
+32228,
+54848,
+58468,
+11304,
+64309,
+30179,
+63829,
+17777,
+52219,
+6192,
+41082,
+56166,
+35861,
+11600,
+758,
+28409,
+27736,
+3342,
+10156,
+28127,
+30125,
+8731,
+13248,
+52301,
+14074,
+36427,
+9027,
+53640,
+15333,
+56131,
+20014,
+60538,
+41791,
+55704,
+58759,
+36130,
+21158,
+59207,
+43109,
+20331,
+10551,
+37173,
+43432,
+57589,
+48995,
+21115,
+30060,
+38476,
+33731,
+48295,
+63683,
+5094,
+48919,
+31608,
+64238,
+30043,
+59380,
+25115,
+49112,
+47402,
+39282,
+19729,
+25588,
+57537,
+37075,
+60753,
+40381,
+64462,
+37041,
+48063,
+7420,
+44652,
+22354,
+41991,
+38683,
+36949,
+22390,
+30406,
+15237,
+10121,
+1665,
+5705,
+53949,
+19769,
+11315,
+41868,
+14674,
+62068,
+2401,
+8541,
+25312,
+52137,
+35017,
+1414,
+22140,
+50966,
+45643,
+59038,
+65185,
+32116,
+47079,
+63301,
+26597,
+58868,
+21732,
+16870,
+50963,
+30006,
+3371,
+1964,
+37789,
+64520,
+45864,
+13830,
+15235,
+30408,
+15611,
+64753,
+838,
+4141,
+40839,
+15010,
+39600,
+6912,
+58096,
+7462,
+45314,
+65094,
+38997,
+28585,
+38148,
+18022,
+50411,
+23408,
+28773,
+27950,
+16557,
+47810,
+23312,
+1862,
+30993,
+51005,
+29326,
+35307,
+58583,
+20747,
+24958,
+836,
+64755,
+34238,
+15934,
+30016,
+37432,
+19046,
+50309,
+539,
+28279,
+2673,
+42328,
+58306,
+50101,
+13533,
+51558,
+21395,
+45668,
+53818,
+23463,
+41699,
+61095,
+58425,
+3623,
+34596,
+11793,
+45828,
+45835,
+64224,
+23928,
+17502,
+43087,
+34073,
+51030,
+42474,
+8137,
+38294,
+47916,
+40718,
+24446,
+38690,
+31129,
+21860,
+29920,
+62592,
+54118,
+32207,
+59224,
+42436,
+11426,
+18698,
+30416,
+17406,
+59066,
+57708,
+17842,
+61854,
+35297,
+64968,
+55269,
+36775,
+20860,
+63031,
+13527,
+26864,
+14072,
+52303,
+32039,
+60860,
+49378,
+62062,
+34656,
+24380,
+58326,
+49331,
+49268,
+46793,
+12386,
+8789,
+4646,
+9168,
+53873,
+22860,
+43011,
+26338,
+6122,
+23206,
+57049,
+31561,
+680,
+35416,
+19599,
+45051,
+6118,
+16977,
+21166,
+56686,
+25379,
+46618,
+58994,
+19606,
+15507,
+21186,
+33068,
+4690,
+25833,
+40583,
+30801,
+1684,
+60015,
+28324,
+5318,
+34476,
+4911,
+3003,
+53168,
+57224,
+32667,
+47605,
+42340,
+45995,
+39727,
+22385,
+61495,
+12424,
+58143,
+48695,
+48248,
+7579,
+25014,
+24505,
+54945,
+63168,
+37170,
+10280,
+5617,
+31043,
+43686,
+45373,
+33899,
+10195,
+59091,
+51068,
+47215,
+9019,
+29885,
+28353,
+3757,
+61370,
+8669,
+64578,
+2669,
+9402,
+12693,
+28505,
+15407,
+40498,
+60782,
+49202,
+35867,
+31781,
+12557,
+32686,
+59507,
+62940,
+15022,
+48904,
+19510,
+32835,
+29614,
+47686,
+37917,
+39887,
+50897,
+7455,
+56546,
+19735,
+63788,
+42855,
+28899,
+65250,
+30478,
+65080,
+11355,
+2800,
+43006,
+23496,
+23271,
+14107,
+22768,
+62202,
+43463,
+59484,
+28622,
+31651,
+46549,
+36216,
+16915,
+41289,
+55067,
+63059,
+28179,
+38172,
+40206,
+44754,
+4047,
+38096,
+23997,
+35921,
+17853,
+42585,
+50221,
+25514,
+41170,
+49806,
+35111,
+35601,
+57223,
+53271,
+3004,
+20131,
+32011,
+32358,
+46253,
+65018,
+29480,
+42154,
+6613,
+51735,
+12001,
+61874,
+39427,
+29982,
+38775,
+18743,
+12228,
+56749,
+30573,
+20234,
+36852,
+36135,
+37523,
+3908,
+35846,
+24121,
+20212,
+29023,
+57592,
+49162,
+17584,
+5882,
+62118,
+15091,
+15951,
+47268,
+64042,
+43473,
+38700,
+48890,
+30297,
+50762,
+55943,
+13685,
+43496,
+20123,
+1346,
+28269,
+43197,
+8016,
+21566,
+36454,
+18703,
+4041,
+12275,
+7395,
+8233,
+3718,
+32373,
+23591,
+49651,
+12964,
+3959,
+9815,
+12613,
+4464,
+37327,
+17616,
+40219,
+5432,
+17106,
+14260,
+52142,
+57626,
+12487,
+21628,
+58581,
+35309,
+18864,
+12761,
+23567,
+4453,
+43658,
+58106,
+45577,
+221,
+40445,
+6066,
+54779,
+51245,
+30754,
+51363,
+2498,
+47495,
+61157,
+7529,
+52683,
+19219,
+43623,
+57427,
+29468,
+35446,
+3610,
+22252,
+26012,
+58718,
+5605,
+41023,
+13703,
+24300,
+51080,
+15338,
+37809,
+39318,
+45058,
+15942,
+7910,
+12444,
+43910,
+35206,
+54922,
+34980,
+29563,
+43798,
+2030,
+16716,
+14428,
+14967,
+24735,
+4158,
+8006,
+17746,
+24119,
+35848,
+44944,
+3155,
+46205,
+24560,
+53816,
+45670,
+14810,
+9780,
+32009,
+20133,
+25667,
+14935,
+42199,
+5084,
+2424,
+56025,
+30816,
+6281,
+42430,
+157,
+27792,
+28920,
+27076,
+33446,
+45109,
+29120,
+49987,
+45412,
+64193,
+46848,
+13464,
+30387,
+58380,
+16660,
+15216,
+30278,
+31394,
+13075,
+62616,
+46842,
+36107,
+29762,
+40625,
+62894,
+62983,
+2183,
+7713,
+17409,
+64947,
+4563,
+1279,
+26267,
+33755,
+12312,
+32852,
+43046,
+17360,
+19023,
+39029,
+65224,
+11554,
+47292,
+42271,
+20641,
+64322,
+34497,
+61976,
+42103,
+34421,
+10326,
+9387,
+17209,
+18672,
+54886,
+41397,
+12775,
+55571,
+33391,
+474,
+10073,
+11119,
+65261,
+42183,
+41987,
+61575,
+13072,
+65157,
+24037,
+18565,
+19391,
+47589,
+61879,
+59642,
+12866,
+10524,
+5904,
+10023,
+52207,
+13901,
+37446,
+6900,
+24759,
+26769,
+22972,
+60209,
+17594,
+19474,
+29019,
+55721,
+12434,
+61074,
+20843,
+5148,
+6976,
+62457,
+60089,
+52279,
+13932,
+8209,
+36665,
+53986,
+22851,
+59438,
+42700,
+16104,
+55153,
+49695,
+46085,
+32286,
+14837,
+24033,
+59738,
+39122,
+63893,
+16475,
+36966,
+51263,
+46230,
+60721,
+14592,
+9221,
+47537,
+54742,
+11701,
+42940,
+17528,
+917,
+12780,
+25501,
+61989,
+1590,
+1072,
+24188,
+56952,
+21554,
+40661,
+26748,
+6724,
+43348,
+62794,
+30714,
+23969,
+10809,
+26816,
+51129,
+63773,
+42193,
+52772,
+54278,
+3651,
+47754,
+15005,
+51986,
+30119,
+45940,
+36746,
+28153,
+17122,
+51610,
+48149,
+18203,
+10085,
+13406,
+32332,
+51317,
+17700,
+37840,
+30628,
+56134,
+12304,
+37033,
+22725,
+6850,
+16512,
+55612,
+59086,
+8912,
+49185,
+40900,
+9756,
+10560,
+2899,
+17470,
+6318,
+32588,
+33591,
+44329,
+61536,
+41404,
+12795,
+44913,
+23282,
+36402,
+3391,
+30934,
+20822,
+17966,
+46439,
+55198,
+18379,
+37059,
+29238,
+8626,
+22776,
+57211,
+41740,
+4515,
+43494,
+13687,
+23141,
+49495,
+4822,
+36880,
+23674,
+33283,
+18181,
+56725,
+11054,
+54381,
+47660,
+57674,
+18265,
+41890,
+25131,
+11634,
+46995,
+39975,
+7098,
+6491,
+31548,
+36088,
+61334,
+14771,
+24726,
+20070,
+1392,
+46672,
+12449,
+41971,
+64890,
+54277,
+52866,
+42194,
+49172,
+33174,
+22814,
+49920,
+37600,
+15163,
+2391,
+24139,
+12579,
+9967,
+59864,
+45283,
+64619,
+12651,
+40037,
+23806,
+49577,
+45272,
+39558,
+21533,
+56496,
+60079,
+32151,
+53685,
+15535,
+56228,
+39564,
+24132,
+12713,
+20625,
+11697,
+466,
+49259,
+49803,
+15272,
+26953,
+52576,
+15207,
+41134,
+9350,
+7374,
+21977,
+34833,
+42090,
+43028,
+52048,
+48663,
+19880,
+38596,
+11695,
+20627,
+60102,
+11953,
+34798,
+57711,
+62414,
+44567,
+22154,
+40891,
+57898,
+7526,
+59626,
+1742,
+189,
+25181,
+20751,
+25269,
+35669,
+55769,
+47233,
+42124,
+10266,
+28764,
+30048,
+55549,
+33534,
+52499,
+60511,
+11439,
+33646,
+58339,
+55330,
+60921,
+9828,
+29599,
+59943,
+19218,
+53071,
+7530,
+46976,
+32088,
+33951,
+35829,
+46411,
+26651,
+45721,
+21506,
+6516,
+5894,
+12597,
+55014,
+219,
+45579,
+57476,
+22012,
+11359,
+10345,
+42512,
+1410,
+58060,
+29412,
+9542,
+16126,
+25477,
+23679,
+42160,
+34404,
+62134,
+51687,
+35403,
+19447,
+46778,
+11583,
+58302,
+28892,
+59391,
+2262,
+5098,
+13021,
+21949,
+37690,
+27667,
+55914,
+37224,
+32832,
+26419,
+33025,
+60575,
+40993,
+31174,
+27536,
+45751,
+15129,
+1660,
+9944,
+14288,
+13013,
+15859,
+11377,
+56155,
+49910,
+31419,
+487,
+42036,
+33542,
+55003,
+8743,
+2287,
+37152,
+39380,
+52386,
+59692,
+6539,
+56449,
+61153,
+30564,
+19616,
+17537,
+26230,
+25083,
+34230,
+44723,
+8484,
+61622,
+35683,
+59435,
+60833,
+41472,
+40436,
+36626,
+43882,
+5016,
+48739,
+22151,
+60760,
+10965,
+39475,
+33153,
+86,
+58005,
+18563,
+24039,
+51670,
+15206,
+52734,
+26954,
+15885,
+12431,
+61579,
+10104,
+18018,
+13822,
+30022,
+51323,
+10090,
+54114,
+59783,
+46377,
+65042,
+399,
+44730,
+63815,
+9902,
+29219,
+51123,
+6376,
+58899,
+16995,
+63074,
+48262,
+46921,
+20199,
+43064,
+25009,
+31192,
+55201,
+22113,
+22308,
+58890,
+32592,
+64231,
+26718,
+36396,
+22822,
+16877,
+24623,
+26491,
+9127,
+640,
+24948,
+18930,
+58680,
+30504,
+942,
+43646,
+51248,
+12913,
+49679,
+27378,
+61665,
+21604,
+56007,
+1631,
+60438,
+12895,
+43589,
+51094,
+6758,
+9067,
+32849,
+6032,
+14362,
+55673,
+22015,
+39631,
+6432,
+40344,
+29822,
+21906,
+23865,
+60510,
+52694,
+33535,
+58536,
+4935,
+8618,
+16486,
+5582,
+7772,
+43549,
+44053,
+42588,
+17282,
+61655,
+5810,
+9937,
+43611,
+11291,
+40773,
+33406,
+50193,
+61557,
+58225,
+5254,
+10452,
+6929,
+5609,
+22349,
+49771,
+18363,
+35364,
+998,
+45398,
+9813,
+3961,
+12589,
+381,
+34142,
+12541,
+2922,
+26301,
+39233,
+8769,
+37313,
+30286,
+516,
+54256,
+42571,
+31013,
+59412,
+50759,
+56078,
+57701,
+27872,
+51950,
+33194,
+11833,
+36192,
+45453,
+4599,
+46632,
+34860,
+39010,
+33654,
+10240,
+9709,
+50642,
+42406,
+5461,
+37449,
+32766,
+43693,
+24577,
+10083,
+18205,
+31344,
+7022,
+9930,
+25870,
+64013,
+15019,
+63064,
+23729,
+61376,
+22167,
+61946,
+34449,
+45797,
+20108,
+35235,
+39456,
+29269,
+13957,
+44658,
+13885,
+9058,
+62977,
+63566,
+5224,
+42622,
+24355,
+9852,
+57656,
+17691,
+23016,
+10986,
+15832,
+51212,
+56822,
+38711,
+1655,
+34867,
+53879,
+59691,
+52610,
+39381,
+48003,
+62443,
+62508,
+13485,
+5021,
+32639,
+65283,
+45851,
+56489,
+19205,
+55400,
+36358,
+13426,
+24157,
+14295,
+41961,
+24470,
+2722,
+54202,
+18464,
+19781,
+39952,
+28115,
+29998,
+58569,
+33047,
+31304,
+48566,
+1959,
+57279,
+32075,
+4449,
+18749,
+38355,
+2470,
+38379,
+36463,
+22692,
+55973,
+39818,
+50684,
+15741,
+32170,
+4882,
+8631,
+58014,
+21321,
+36378,
+38510,
+29651,
+14122,
+10855,
+51973,
+14357,
+62247,
+53941,
+65408,
+65026,
+16321,
+60274,
+25197,
+62586,
+10598,
+50933,
+7056,
+35198,
+38641,
+34340,
+14192,
+60962,
+6463,
+11691,
+6576,
+6189,
+41536,
+39670,
+9809,
+37633,
+15801,
+60698,
+32038,
+53320,
+14073,
+53510,
+13249,
+1104,
+63047,
+42604,
+24233,
+58623,
+7443,
+54982,
+16289,
+47285,
+56805,
+12026,
+13237,
+64434,
+9219,
+14594,
+15394,
+40413,
+45201,
+50542,
+13931,
+52917,
+60090,
+5669,
+21419,
+55787,
+1769,
+4185,
+32519,
+42066,
+32440,
+35421,
+56696,
+41273,
+29724,
+14493,
+44562,
+8497,
+60610,
+26277,
+36710,
+35957,
+50795,
+17998,
+10797,
+1051,
+7539,
+42009,
+14982,
+49905,
+50138,
+56102,
+14652,
+27995,
+1642,
+49064,
+62924,
+26271,
+44227,
+43923,
+31455,
+4470,
+13340,
+37090,
+1828,
+25191,
+35717,
+5867,
+995,
+330,
+65340,
+17054,
+3836,
+17662,
+57876,
+15119,
+24417,
+33054,
+1831,
+41534,
+6191,
+53525,
+17778,
+34279,
+46330,
+10139,
+63166,
+54947,
+38250,
+39634,
+30381,
+64824,
+13900,
+52936,
+10024,
+2276,
+31316,
+56282,
+2246,
+43634,
+33739,
+26715,
+61066,
+13299,
+28448,
+46876,
+11405,
+54845,
+54890,
+59659,
+48086,
+58128,
+5371,
+37442,
+16734,
+35689,
+5497,
+3136,
+10217,
+57681,
+63594,
+8944,
+25996,
+59881,
+31373,
+29139,
+42717,
+9450,
+28647,
+4359,
+19136,
+52158,
+30614,
+4642,
+3431,
+46614,
+47919,
+25320,
+39332,
+37624,
+50903,
+30613,
+52169,
+19137,
+40769,
+10358,
+42756,
+16017,
+19595,
+28793,
+21330,
+20927,
+17641,
+21521,
+53659,
+4145,
+10418,
+57625,
+53095,
+14261,
+27292,
+3879,
+35016,
+53442,
+25313,
+37062,
+19466,
+1149,
+55451,
+12686,
+33206,
+25334,
+63929,
+48124,
+60054,
+9553,
+39245,
+65366,
+48961,
+30751,
+43649,
+32241,
+21411,
+28261,
+2466,
+51538,
+62256,
+48894,
+29178,
+13165,
+41381,
+34179,
+64029,
+63668,
+20455,
+24430,
+8172,
+41979,
+57815,
+16833,
+46886,
+8881,
+36326,
+38069,
+31079,
+50847,
+55187,
+42601,
+19995,
+19394,
+49751,
+39099,
+5487,
+11095,
+4921,
+27044,
+45929,
+50032,
+35031,
+64597,
+17659,
+36937,
+14949,
+14349,
+48745,
+7171,
+18688,
+12534,
+35151,
+19482,
+34591,
+28861,
+60136,
+23388,
+39762,
+42493,
+968,
+29354,
+47673,
+34991,
+25924,
+25465,
+21199,
+41651,
+36733,
+47342,
+44145,
+3028,
+13156,
+60517,
+42713,
+48662,
+52725,
+43029,
+2016,
+2113,
+21857,
+7086,
+24923,
+64289,
+32533,
+48490,
+19313,
+48677,
+57911,
+31390,
+59805,
+8692,
+50867,
+48306,
+50350,
+22740,
+28036,
+46361,
+62127,
+11209,
+59300,
+7221,
+43895,
+6512,
+65377,
+61004,
+48691,
+1779,
+46871,
+26904,
+63487,
+21143,
+42530,
+2505,
+12214,
+38674,
+42997,
+1923,
+60122,
+15435,
+12165,
+13980,
+19281,
+35475,
+27063,
+25052,
+19406,
+13877,
+56438,
+7932,
+29940,
+62745,
+26089,
+61652,
+37241,
+7348,
+33095,
+30118,
+52861,
+15006,
+57389,
+50833,
+42315,
+59518,
+18046,
+60621,
+23829,
+40052,
+59960,
+9486,
+14356,
+52332,
+10856,
+11947,
+33510,
+8651,
+2584,
+10852,
+14391,
+56763,
+57739,
+32614,
+35011,
+35216,
+10132,
+31919,
+39161,
+18083,
+54771,
+11489,
+11768,
+29097,
+44103,
+33193,
+52446,
+27873,
+4654,
+34466,
+39165,
+40112,
+20703,
+10640,
+21317,
+16230,
+17000,
+9538,
+61031,
+612,
+44904,
+65484,
+3782,
+44316,
+34428,
+57151,
+62310,
+45690,
+63943,
+2634,
+56037,
+62316,
+36372,
+9392,
+28482,
+57754,
+41848,
+10910,
+31385,
+47382,
+30293,
+16276,
+15736,
+22277,
+33199,
+32124,
+35392,
+29080,
+21586,
+64609,
+41430,
+56917,
+49053,
+7965,
+43004,
+2802,
+16508,
+26531,
+44505,
+45775,
+4790,
+60805,
+13729,
+44359,
+34207,
+26794,
+27133,
+48783,
+19240,
+9252,
+9669,
+56680,
+30246,
+11918,
+24343,
+2222,
+36322,
+29400,
+28016,
+9725,
+25263,
+49395,
+6063,
+14883,
+29313,
+18631,
+49305,
+48539,
+28264,
+47784,
+57560,
+36541,
+63011,
+48719,
+59556,
+29668,
+33758,
+50353,
+42583,
+17855,
+46045,
+14873,
+56360,
+9593,
+12413,
+22042,
+12732,
+42397,
+26030,
+29810,
+17496,
+65111,
+19585,
+36042,
+57172,
+28170,
+28967,
+54174,
+57007,
+6348,
+19851,
+24513,
+12478,
+41238,
+43143,
+39874,
+45360,
+61325,
+44512,
+19250,
+19913,
+48020,
+1883,
+59033,
+49897,
+42234,
+32747,
+23978,
+28868,
+47992,
+8096,
+2154,
+54529,
+19727,
+39284,
+50387,
+29553,
+4731,
+13643,
+21179,
+6663,
+1427,
+50347,
+38657,
+53737,
+33050,
+43615,
+291,
+55887,
+51261,
+36968,
+8551,
+41636,
+11592,
+64283,
+41112,
+10431,
+57905,
+21798,
+19194,
+30569,
+8154,
+23612,
+21447,
+22689,
+33688,
+55428,
+41905,
+63103,
+2129,
+55169,
+8510,
+37047,
+50086,
+1176,
+41595,
+9916,
+25814,
+34294,
+29732,
+27712,
+63762,
+40232,
+45520,
+58410,
+14762,
+17363,
+59986,
+29841,
+16584,
+63416,
+42260,
+27569,
+46552,
+37972,
+21131,
+15796,
+1117,
+24211,
+19471,
+9913,
+57017,
+42882,
+28540,
+32006,
+30037,
+26211,
+14045,
+37734,
+6302,
+12000,
+53158,
+6614,
+55809,
+60633,
+28031,
+34717,
+54293,
+57896,
+40893,
+47709,
+49855,
+1096,
+22464,
+389,
+3866,
+2272,
+37426,
+33800,
+16536,
+47518,
+12968,
+35516,
+38832,
+50614,
+3721,
+44426,
+58916,
+19672,
+12117,
+50303,
+60870,
+55692,
+19248,
+44514,
+49598,
+56249,
+56477,
+2562,
+3632,
+48581,
+54974,
+17195,
+4502,
+46967,
+29796,
+48754,
+61254,
+35402,
+52652,
+62135,
+46191,
+24383,
+17205,
+31962,
+27542,
+55028,
+6531,
+20840,
+62733,
+49031,
+8685,
+14797,
+40573,
+41298,
+15205,
+52578,
+24040,
+20291,
+61601,
+59409,
+600,
+35114,
+41309,
+11936,
+46130,
+13540,
+9100,
+30764,
+20053,
+45188,
+5821,
+60146,
+20878,
+13131,
+56480,
+10878,
+15700,
+61965,
+2864,
+184,
+22319,
+63051,
+36976,
+56467,
+25048,
+65122,
+57148,
+712,
+13329,
+12602,
+28092,
+50792,
+28729,
+37715,
+10373,
+49915,
+59463,
+58653,
+17654,
+37727,
+59314,
+24975,
+43583,
+42539,
+46393,
+6136,
+8085,
+54345,
+20937,
+34009,
+60919,
+55332,
+43900,
+8128,
+48148,
+52855,
+17123,
+42548,
+14994,
+13257,
+12621,
+57329,
+16523,
+62607,
+26180,
+56231,
+47768,
+62495,
+3794,
+18790,
+53931,
+5349,
+32745,
+42236,
+60551,
+58465,
+1762,
+23346,
+30194,
+23850,
+39077,
+10897,
+34309,
+18325,
+36267,
+13129,
+20880,
+34400,
+23506,
+31494,
+48221,
+10739,
+61834,
+32203,
+45279,
+10933,
+30324,
+34305,
+61723,
+38536,
+57108,
+58314,
+21644,
+24278,
+48651,
+18193,
+21394,
+53371,
+13534,
+46257,
+28250,
+35972,
+21741,
+21074,
+10801,
+39504,
+63602,
+44291,
+28187,
+16362,
+63996,
+40302,
+3489,
+37051,
+11889,
+36231,
+62255,
+52115,
+2467,
+17235,
+59017,
+39852,
+16768,
+56936,
+55837,
+40753,
+27318,
+54997,
+28942,
+41588,
+38760,
+43233,
+63484,
+9806,
+61642,
+10015,
+27018,
+9878,
+62765,
+28737,
+39798,
+55559,
+64822,
+30383,
+54393,
+34878,
+16459,
+58184,
+38430,
+61464,
+42217,
+51022,
+10680,
+63912,
+48387,
+32416,
+57241,
+62512,
+8995,
+54909,
+17096,
+29126,
+39325,
+13798,
+27804,
+33466,
+10744,
+46313,
+49337,
+42631,
+38090,
+60034,
+23955,
+2061,
+4546,
+51168,
+5543,
+7011,
+1431,
+37758,
+63752,
+45223,
+5289,
+49098,
+20729,
+54514,
+9033,
+19015,
+4085,
+38672,
+12216,
+13235,
+12028,
+29111,
+57580,
+31295,
+41148,
+14961,
+9528,
+63908,
+54065,
+61493,
+22387,
+27392,
+43544,
+48370,
+9076,
+4954,
+54732,
+14432,
+18877,
+42490,
+32815,
+35184,
+34512,
+37550,
+20152,
+6596,
+34056,
+7427,
+1845,
+8202,
+5274,
+40665,
+42265,
+12339,
+42770,
+57515,
+40070,
+44340,
+39377,
+53793,
+15170,
+19812,
+62561,
+30886,
+51309,
+37148,
+55544,
+35885,
+13203,
+54683,
+51148,
+27964,
+9265,
+64470,
+19367,
+34167,
+22713,
+60607,
+27338,
+41753,
+62843,
+44682,
+38420,
+63080,
+59534,
+64457,
+60193,
+1154,
+33649,
+46691,
+25795,
+18936,
+30276,
+15218,
+38443,
+34568,
+43662,
+31613,
+9850,
+24357,
+33426,
+11098,
+125,
+46293,
+36555,
+54905,
+65014,
+34025,
+24522,
+8903,
+11785,
+36028,
+31066,
+39770,
+17203,
+24385,
+54608,
+61310,
+6747,
+2497,
+53076,
+30755,
+5765,
+58025,
+24633,
+41086,
+57435,
+58346,
+41447,
+20558,
+60801,
+30202,
+43505,
+16621,
+49550,
+55006,
+33840,
+31196,
+13221,
+4576,
+45549,
+37083,
+39144,
+2442,
+62292,
+1597,
+31047,
+7467,
+18860,
+59280,
+64019,
+10067,
+346,
+49862,
+60809,
+43965,
+48597,
+6853,
+8425,
+10089,
+52567,
+30023,
+4753,
+16337,
+41803,
+17699,
+52849,
+32333,
+1811,
+3654,
+26682,
+17862,
+30548,
+37147,
+51419,
+30887,
+19744,
+41401,
+1690,
+18294,
+56020,
+46578,
+35701,
+38930,
+50628,
+40089,
+43294,
+27230,
+1652,
+37547,
+29626,
+38011,
+31554,
+49147,
+41393,
+17165,
+56463,
+745,
+40149,
+34725,
+25881,
+14928,
+55529,
+24069,
+35269,
+48430,
+12441,
+13396,
+38246,
+49873,
+29805,
+17743,
+19979,
+64531,
+57191,
+43996,
+27588,
+45565,
+22037,
+46229,
+52897,
+36967,
+51797,
+55888,
+8689,
+32679,
+47395,
+60567,
+43855,
+64344,
+17100,
+36987,
+32952,
+55592,
+12912,
+52525,
+43647,
+30753,
+53078,
+54780,
+41379,
+13167,
+23577,
+22246,
+20631,
+55,
+38621,
+7676,
+22504,
+29655,
+28732,
+10890,
+49068,
+30911,
+60384,
+30925,
+1406,
+24688,
+41098,
+30431,
+59269,
+26929,
+15194,
+22485,
+3822,
+65418,
+2778,
+45351,
+31112,
+46635,
+56821,
+52393,
+15833,
+16884,
+47346,
+9646,
+33921,
+61220,
+15049,
+61719,
+7384,
+57134,
+56170,
+36702,
+22996,
+19146,
+41164,
+36838,
+6731,
+40470,
+23425,
+32652,
+56743,
+5509,
+50587,
+13460,
+16793,
+45585,
+61514,
+11228,
+2534,
+56050,
+13563,
+13206,
+3291,
+28768,
+8612,
+59504,
+13028,
+9865,
+29488,
+55646,
+28608,
+6587,
+5542,
+51480,
+4547,
+63535,
+23124,
+45269,
+42553,
+61101,
+2986,
+2963,
+43626,
+36335,
+20723,
+41115,
+25678,
+16636,
+28718,
+34133,
+18719,
+9639,
+27963,
+51413,
+54684,
+13571,
+8775,
+11001,
+60433,
+56304,
+26345,
+22879,
+49592,
+37254,
+578,
+54219,
+38575,
+55754,
+35503,
+3990,
+23861,
+63772,
+52869,
+26817,
+805,
+42039,
+42268,
+6375,
+52556,
+29220,
+4287,
+45990,
+46805,
+50297,
+24665,
+32998,
+59425,
+3776,
+44959,
+37832,
+55865,
+48820,
+12496,
+50571,
+61890,
+10804,
+14091,
+46661,
+27438,
+622,
+40291,
+46008,
+3300,
+22118,
+3118,
+43118,
+6757,
+52514,
+43590,
+48555,
+25782,
+11536,
+61323,
+45362,
+576,
+37256,
+62866,
+56137,
+63746,
+22225,
+15337,
+53057,
+24301,
+65120,
+25050,
+27065,
+26687,
+5347,
+53933,
+24475,
+4245,
+31226,
+47214,
+53244,
+59092,
+28020,
+5792,
+22220,
+55236,
+34532,
+23809,
+19552,
+5518,
+29118,
+45111,
+2815,
+17898,
+9195,
+22728,
+16546,
+14699,
+38222,
+18162,
+38660,
+37210,
+37221,
+35812,
+13272,
+20976,
+2541,
+40504,
+53869,
+38193,
+5171,
+22721,
+65189,
+15677,
+34985,
+59568,
+27751,
+42473,
+53353,
+34074,
+26587,
+19634,
+25111,
+15892,
+45460,
+10679,
+51504,
+42218,
+17435,
+29428,
+35777,
+27113,
+56515,
+40612,
+1250,
+48512,
+57650,
+41828,
+63755,
+3460,
+47530,
+43332,
+29325,
+53392,
+30994,
+3179,
+59211,
+1835,
+39159,
+31921,
+20866,
+27256,
+37404,
+43134,
+4556,
+23932,
+29860,
+56116,
+1302,
+41999,
+42870,
+34354,
+9688,
+61209,
+22296,
+42450,
+41479,
+15916,
+11818,
+31508,
+28958,
+25058,
+18350,
+36864,
+28086,
+32773,
+53622,
+9831,
+6780,
+46566,
+2789,
+45642,
+53438,
+22141,
+30005,
+53427,
+16871,
+31577,
+2203,
+60929,
+11481,
+42644,
+11839,
+18782,
+59097,
+19085,
+40832,
+46859,
+44214,
+29161,
+26850,
+45265,
+4815,
+12643,
+56317,
+13252,
+50144,
+678,
+31563,
+44046,
+42382,
+35583,
+36779,
+45533,
+7055,
+52321,
+10599,
+17764,
+58798,
+4446,
+45731,
+28830,
+45182,
+65084,
+21895,
+435,
+60277,
+19940,
+48024,
+14907,
+25450,
+9714,
+55102,
+35395,
+20903,
+26239,
+34850,
+30779,
+21193,
+64063,
+59843,
+45634,
+23615,
+46004,
+30612,
+52160,
+37625,
+44666,
+25459,
+30512,
+7454,
+53213,
+39888,
+11078,
+11122,
+36944,
+44558,
+48090,
+1575,
+42625,
+18969,
+62750,
+9434,
+13289,
+37543,
+18967,
+42627,
+35769,
+28277,
+541,
+33495,
+33828,
+27563,
+44408,
+24292,
+62994,
+25035,
+64326,
+63209,
+27179,
+48305,
+52032,
+8693,
+25367,
+23668,
+12090,
+3470,
+47563,
+26948,
+57058,
+37856,
+1817,
+5156,
+43794,
+44465,
+424,
+45954,
+62141,
+6796,
+44258,
+55186,
+52095,
+31080,
+61732,
+50153,
+46916,
+7769,
+6774,
+5302,
+48990,
+14486,
+27316,
+40755,
+32189,
+42314,
+51983,
+57390,
+25754,
+4220,
+8230,
+31189,
+63296,
+31804,
+17244,
+28401,
+29036,
+40449,
+47024,
+48428,
+35271,
+49962,
+63253,
+62897,
+29777,
+38188,
+57129,
+63098,
+56694,
+35423,
+8824,
+55312,
+32199,
+16025,
+46101,
+4088,
+24940,
+5146,
+20845,
+63309,
+43761,
+42971,
+24752,
+17997,
+52258,
+35958,
+28728,
+51634,
+28093,
+48805,
+39172,
+61409,
+54549,
+49707,
+31840,
+31056,
+62130,
+23572,
+64893,
+10703,
+58001,
+65164,
+18147,
+7126,
+13776,
+48175,
+46516,
+62461,
+34677,
+35438,
+7167,
+18735,
+4202,
+12756,
+40812,
+57880,
+55942,
+53126,
+30298,
+56077,
+52450,
+59413,
+42333,
+61112,
+43480,
+56502,
+46524,
+3555,
+24347,
+30958,
+59399,
+57454,
+38981,
+58102,
+61421,
+29417,
+17175,
+33918,
+56851,
+13863,
+24965,
+45879,
+11190,
+44602,
+24179,
+58201,
+38528,
+4347,
+48792,
+24640,
+40819,
+19866,
+12510,
+55758,
+62046,
+28488,
+15453,
+55305,
+2685,
+9346,
+58863,
+53782,
+56812,
+23582,
+64268,
+19383,
+25213,
+54052,
+37295,
+20527,
+8075,
+41623,
+30170,
+65210,
+47646,
+19772,
+27352,
+40917,
+33250,
+12633,
+41505,
+56991,
+6037,
+20830,
+10443,
+30956,
+24349,
+48240,
+7818,
+35481,
+56493,
+19199,
+32336,
+33415,
+15740,
+52344,
+39819,
+49134,
+64615,
+5836,
+16607,
+18820,
+22788,
+441,
+26542,
+8907,
+54598,
+3602,
+27540,
+31964,
+30710,
+58833,
+25373,
+49372,
+19491,
+13682,
+9178,
+6177,
+57444,
+10392,
+21668,
+48350,
+36499,
+25090,
+59473,
+13664,
+36225,
+17070,
+39390,
+32960,
+5278,
+54700,
+16228,
+21319,
+58016,
+63856,
+42405,
+52434,
+9710,
+58850,
+27692,
+23960,
+56419,
+6219,
+31326,
+30053,
+61663,
+27380,
+17512,
+14618,
+40088,
+51299,
+38931,
+26068,
+4496,
+59490,
+4803,
+4271,
+30453,
+65269,
+37065,
+123,
+11100,
+32371,
+3720,
+51712,
+38833,
+887,
+10112,
+39655,
+14255,
+10009,
+29090,
+56574,
+50013,
+62342,
+39869,
+12515,
+23339,
+16155,
+22397,
+32196,
+30562,
+61155,
+47497,
+8833,
+18941,
+54802,
+11107,
+32740,
+28599,
+13459,
+51189,
+5510,
+47474,
+19083,
+59099,
+19488,
+33474,
+4500,
+17197,
+16048,
+24388,
+14079,
+56010,
+46020,
+2711,
+61889,
+51108,
+12497,
+40803,
+1204,
+27086,
+38142,
+47253,
+21211,
+61401,
+10731,
+20721,
+36337,
+30782,
+46509,
+16393,
+23527,
+13285,
+15633,
+56890,
+45034,
+54206,
+19477,
+2077,
+411,
+54244,
+24766,
+55181,
+22988,
+13930,
+52281,
+45202,
+17834,
+46000,
+26352,
+50363,
+10376,
+21253,
+24222,
+17782,
+12675,
+13552,
+8149,
+24310,
+24697,
+12125,
+25938,
+56033,
+4863,
+27609,
+2453,
+50449,
+36729,
+1523,
+13117,
+2474,
+2596,
+12316,
+16634,
+25680,
+61511,
+2883,
+7750,
+35843,
+54862,
+24100,
+38465,
+1042,
+17909,
+53876,
+37474,
+30733,
+43440,
+17367,
+24517,
+64696,
+19272,
+58091,
+32457,
+10660,
+7218,
+58938,
+9765,
+4316,
+14332,
+3819,
+49291,
+39182,
+62054,
+2956,
+47763,
+34958,
+36083,
+39001,
+27100,
+39196,
+1362,
+9281,
+48077,
+59059,
+770,
+32812,
+39765,
+56973,
+63793,
+65350,
+36293,
+58158,
+64250,
+5352,
+47504,
+65011,
+43942,
+39410,
+35574,
+44533,
+17500,
+23930,
+4558,
+29801,
+29660,
+1994,
+36728,
+50521,
+2454,
+3418,
+12403,
+49587,
+41898,
+17016,
+44098,
+57232,
+27900,
+34227,
+22857,
+17912,
+28370,
+65307,
+60959,
+19990,
+1332,
+60474,
+2681,
+21529,
+59794,
+9052,
+58125,
+63000,
+9692,
+40643,
+41243,
+2545,
+42963,
+7660,
+13312,
+41121,
+63812,
+62915,
+27306,
+13,
+23407,
+53401,
+18023,
+19785,
+12909,
+63878,
+33481,
+38074,
+36101,
+37021,
+30493,
+31725,
+46647,
+28292,
+25078,
+42019,
+64061,
+21195,
+26162,
+32643,
+62906,
+36308,
+32987,
+33292,
+29552,
+51811,
+39285,
+21726,
+17858,
+11733,
+58886,
+8644,
+33683,
+38908,
+16528,
+20262,
+4487,
+27936,
+12772,
+63147,
+44305,
+22285,
+15331,
+53642,
+11743,
+14114,
+45726,
+49913,
+10375,
+50537,
+26353,
+3485,
+95,
+44375,
+1373,
+11687,
+62269,
+3993,
+42582,
+51859,
+33759,
+22739,
+52030,
+48307,
+38656,
+51804,
+1428,
+22290,
+38607,
+14316,
+43053,
+8673,
+69,
+30897,
+21689,
+48838,
+46373,
+62696,
+24162,
+24238,
+13866,
+12960,
+12804,
+24404,
+32650,
+23427,
+17079,
+1202,
+40805,
+43710,
+48876,
+12538,
+35260,
+14147,
+27095,
+61864,
+45959,
+19327,
+35160,
+56923,
+6035,
+56993,
+538,
+53379,
+19047,
+40824,
+34793,
+56721,
+60869,
+51706,
+12118,
+35130,
+12082,
+55417,
+24664,
+51118,
+46806,
+45513,
+34843,
+44587,
+30187,
+57030,
+2089,
+8248,
+7779,
+48463,
+54072,
+26018,
+38627,
+24339,
+21038,
+29276,
+50043,
+63093,
+3247,
+41345,
+7760,
+43667,
+38387,
+58663,
+16688,
+5216,
+60287,
+18230,
+11224,
+24365,
+15733,
+12801,
+38585,
+28452,
+26395,
+36531,
+53714,
+59124,
+44309,
+20446,
+29880,
+25864,
+3898,
+27147,
+49231,
+17574,
+25071,
+36348,
+6627,
+29991,
+42937,
+30,
+44397,
+4298,
+18005,
+22207,
+61197,
+64813,
+42242,
+544,
+22007,
+26781,
+5940,
+5991,
+57324,
+29266,
+26738,
+60087,
+62459,
+46518,
+3099,
+60739,
+40714,
+25382,
+25513,
+53175,
+42586,
+44055,
+60446,
+7559,
+48833,
+26113,
+7673,
+41600,
+48012,
+11875,
+5238,
+16551,
+32916,
+43251,
+15167,
+11896,
+12032,
+9407,
+39241,
+43311,
+14456,
+12481,
+27729,
+44889,
+48607,
+32965,
+61556,
+52480,
+33407,
+40430,
+35051,
+11969,
+17045,
+46226,
+65426,
+39919,
+6228,
+21517,
+29736,
+27327,
+25328,
+31164,
+18250,
+8734,
+40126,
+34219,
+9257,
+49221,
+21058,
+56124,
+44196,
+35340,
+49883,
+23908,
+59069,
+35075,
+45884,
+15845,
+33150,
+43778,
+24920,
+54009,
+21009,
+62584,
+25199,
+27758,
+46915,
+50844,
+61733,
+4809,
+10942,
+8667,
+61372,
+23153,
+29956,
+677,
+50942,
+13253,
+25125,
+32926,
+41556,
+56101,
+52250,
+49906,
+62171,
+57397,
+39940,
+234,
+53825,
+37031,
+12306,
+25040,
+336,
+37923,
+20353,
+455,
+54184,
+42301,
+11275,
+21558,
+36656,
+1560,
+41943,
+8190,
+58257,
+63017,
+47248,
+64445,
+5107,
+6275,
+1181,
+56833,
+24407,
+36005,
+42471,
+27753,
+58768,
+5414,
+13532,
+53373,
+58307,
+48603,
+65329,
+48758,
+58589,
+8416,
+28234,
+26051,
+16345,
+20990,
+63212,
+65397,
+22161,
+1175,
+51773,
+37048,
+14814,
+10233,
+32936,
+10308,
+57833,
+8526,
+38922,
+32671,
+2378,
+10304,
+62727,
+3454,
+64931,
+60880,
+25436,
+48585,
+64266,
+23584,
+7248,
+54689,
+38603,
+65465,
+48923,
+63246,
+45419,
+29686,
+29574,
+31020,
+42736,
+10714,
+64119,
+21785,
+11351,
+23238,
+45140,
+62283,
+8382,
+16911,
+2733,
+23229,
+63092,
+50280,
+29277,
+61811,
+29066,
+64095,
+8051,
+40282,
+21351,
+47616,
+44937,
+35030,
+52083,
+45930,
+12252,
+44352,
+59397,
+30960,
+3790,
+9561,
+4833,
+18507,
+31647,
+36118,
+48364,
+44864,
+59042,
+40399,
+54301,
+43704,
+62341,
+50605,
+56575,
+42705,
+45163,
+29506,
+43123,
+9820,
+10638,
+20705,
+28332,
+11851,
+12791,
+9310,
+30767,
+40116,
+54934,
+29382,
+31729,
+29977,
+32564,
+39291,
+42969,
+43763,
+33972,
+21493,
+45411,
+53007,
+29121,
+56363,
+61930,
+48763,
+23097,
+28644,
+61358,
+40393,
+22931,
+41306,
+32261,
+46388,
+44629,
+53692,
+21819,
+58729,
+18785,
+47010,
+41762,
+32356,
+32013,
+2824,
+56792,
+63252,
+50818,
+35272,
+19038,
+59786,
+54448,
+62033,
+23967,
+30716,
+33065,
+18836,
+21791,
+19540,
+25812,
+9918,
+62426,
+2888,
+19374,
+20205,
+30791,
+49075,
+4484,
+25982,
+28783,
+28382,
+11747,
+40864,
+21504,
+45723,
+2978,
+65516,
+22939,
+22379,
+58138,
+48166,
+6932,
+35128,
+12120,
+785,
+28934,
+37629,
+10543,
+37599,
+52767,
+22815,
+33186,
+63972,
+59462,
+51630,
+10374,
+50365,
+45727,
+31418,
+52620,
+56156,
+38897,
+62170,
+50137,
+52251,
+14983,
+8648,
+16838,
+7423,
+18089,
+47797,
+42233,
+51822,
+59034,
+18255,
+33089,
+44134,
+59974,
+61520,
+15912,
+48780,
+31866,
+63975,
+23596,
+26358,
+23907,
+50168,
+35341,
+41689,
+58170,
+26666,
+34813,
+62302,
+34796,
+11955,
+29804,
+51274,
+38247,
+1209,
+2330,
+732,
+44348,
+46704,
+38362,
+26224,
+35982,
+60808,
+51330,
+347,
+14502,
+30553,
+38681,
+41993,
+1095,
+51725,
+47710,
+40946,
+37279,
+11310,
+7261,
+34051,
+32144,
+65065,
+26021,
+43715,
+36312,
+32543,
+27004,
+42017,
+25080,
+764,
+54658,
+13888,
+3573,
+36767,
+56927,
+32347,
+35458,
+45028,
+2516,
+1381,
+31252,
+64565,
+17887,
+58859,
+6273,
+5109,
+1322,
+16893,
+16329,
+21444,
+45637,
+28045,
+4990,
+1905,
+65091,
+9132,
+18242,
+59165,
+43226,
+56599,
+21854,
+35110,
+53172,
+41171,
+15271,
+52737,
+49260,
+13623,
+43261,
+43404,
+32121,
+14852,
+13194,
+33368,
+34161,
+29301,
+60821,
+56809,
+27922,
+5073,
+7557,
+60448,
+44916,
+32315,
+1739,
+29116,
+5520,
+37215,
+60130,
+11433,
+2437,
+8845,
+46031,
+26199,
+48711,
+37695,
+18362,
+52472,
+22350,
+47511,
+19733,
+56548,
+5477,
+53682,
+61972,
+25506,
+2501,
+57025,
+28492,
+27485,
+44720,
+16974,
+64179,
+42984,
+21991,
+17169,
+39098,
+52090,
+19395,
+31793,
+61133,
+65151,
+30131,
+49132,
+39821,
+40860,
+44405,
+13787,
+64421,
+33034,
+53611,
+27903,
+13231,
+4840,
+61788,
+24114,
+46699,
+43748,
+45839,
+36877,
+48163,
+34352,
+42872,
+55287,
+62296,
+60825,
+59723,
+18626,
+36740,
+24797,
+40957,
+6243,
+43255,
+26547,
+28521,
+58840,
+8291,
+37978,
+47690,
+18910,
+31839,
+50786,
+54550,
+13143,
+14326,
+48654,
+1581,
+3108,
+47304,
+20156,
+15926,
+30315,
+46084,
+52907,
+55154,
+19652,
+15374,
+32388,
+8722,
+62390,
+44477,
+9415,
+54353,
+10497,
+23655,
+55124,
+1455,
+11569,
+27377,
+52523,
+12914,
+54161,
+63533,
+4549,
+37782,
+6771,
+5585,
+17799,
+4688,
+33070,
+9317,
+5480,
+56056,
+46506,
+32432,
+16312,
+20237,
+20438,
+44122,
+56378,
+32655,
+8448,
+54663,
+44496,
+45500,
+38582,
+12963,
+53107,
+23592,
+57349,
+35167,
+59338,
+40425,
+14512,
+20078,
+24497,
+42229,
+26745,
+8810,
+54485,
+38863,
+53695,
+20546,
+29422,
+41433,
+48786,
+55862,
+4899,
+49127,
+2649,
+5516,
+19554,
+4016,
+15647,
+16758,
+58499,
+24057,
+14556,
+53720,
+38022,
+27715,
+29504,
+45165,
+638,
+9129,
+45317,
+45380,
+15839,
+35223,
+41010,
+17239,
+6817,
+59934,
+11267,
+31298,
+12154,
+35522,
+36844,
+61141,
+56248,
+51701,
+44515,
+20478,
+12485,
+57628,
+37253,
+51139,
+22880,
+49312,
+34252,
+41897,
+50445,
+12404,
+18270,
+60484,
+25933,
+56529,
+56661,
+10354,
+42551,
+45271,
+52754,
+23807,
+34534,
+43445,
+59572,
+64560,
+11752,
+37618,
+17201,
+39772,
+21081,
+19110,
+36564,
+40119,
+46447,
+49443,
+5739,
+56324,
+56140,
+16455,
+49325,
+27710,
+29734,
+21519,
+17643,
+8741,
+55005,
+51349,
+16622,
+2197,
+31875,
+37414,
+18880,
+62480,
+36694,
+16500,
+422,
+44467,
+18537,
+55968,
+26735,
+39459,
+45563,
+27590,
+45975,
+25273,
+823,
+59891,
+43851,
+39296,
+30689,
+16066,
+6250,
+17300,
+23896,
+28531,
+22780,
+13384,
+45943,
+26466,
+9356,
+36219,
+18951,
+42497,
+33337,
+14731,
+19437,
+23619,
+35275,
+22230,
+43938,
+8998,
+38978,
+58167,
+21986,
+21486,
+55610,
+16514,
+56641,
+2705,
+55923,
+4821,
+52803,
+23142,
+25714,
+24679,
+54154,
+48339,
+14959,
+41150,
+10931,
+45281,
+59866,
+44120,
+20440,
+22194,
+48716,
+64786,
+24206,
+27857,
+44264,
+35835,
+27252,
+102,
+20362,
+34847,
+16011,
+59288,
+28857,
+7484,
+11366,
+32167,
+31378,
+40375,
+33323,
+10914,
+23706,
+20317,
+65109,
+17498,
+44535,
+22476,
+2873,
+42803,
+2783,
+26390,
+57132,
+7386,
+19191,
+3829,
+35302,
+54251,
+61949,
+5738,
+49562,
+46448,
+44165,
+11921,
+29878,
+20448,
+33489,
+7404,
+40518,
+22836,
+12628,
+10385,
+49403,
+33389,
+55573,
+56013,
+24315,
+7615,
+17633,
+48141,
+24008,
+21298,
+31211,
+21580,
+48122,
+63931,
+43774,
+22717,
+59102,
+439,
+22790,
+55994,
+12879,
+40061,
+42647,
+42108,
+48460,
+38300,
+5456,
+33388,
+49431,
+10386,
+65273,
+35386,
+23753,
+63466,
+63860,
+6062,
+51875,
+25264,
+16299,
+44924,
+61961,
+23411,
+55273,
+56483,
+7414,
+5198,
+44726,
+4747,
+42197,
+14937,
+43141,
+41240,
+62061,
+53317,
+60861,
+25613,
+61851,
+33472,
+19490,
+50666,
+25374,
+61917,
+29700,
+38287,
+6959,
+23664,
+38365,
+17218,
+60160,
+62405,
+45632,
+59845,
+31140,
+24746,
+46285,
+45626,
+32809,
+7897,
+37964,
+43335,
+58099,
+18357,
+3089,
+59663,
+8772,
+7800,
+39629,
+22017,
+31692,
+46404,
+10096,
+64837,
+8533,
+42630,
+51487,
+46314,
+61941,
+17356,
+58447,
+49267,
+53312,
+58327,
+28074,
+25149,
+24165,
+27709,
+49557,
+16456,
+10586,
+3969,
+31746,
+41928,
+47502,
+5354,
+26534,
+38439,
+21812,
+36782,
+34251,
+49590,
+22881,
+10836,
+14890,
+44219,
+13596,
+48538,
+51870,
+18632,
+9378,
+42374,
+34371,
+47289,
+53998,
+2256,
+17946,
+3660,
+48836,
+21691,
+41939,
+39181,
+50486,
+3820,
+22487,
+6905,
+31365,
+41683,
+63307,
+20847,
+10818,
+20409,
+39731,
+25365,
+8695,
+57165,
+46944,
+40944,
+47712,
+6351,
+44418,
+27891,
+2494,
+34096,
+46792,
+53311,
+49332,
+58448,
+11531,
+29013,
+6093,
+35243,
+13622,
+49802,
+52738,
+467,
+57836,
+34276,
+31086,
+19742,
+30889,
+24597,
+34853,
+10277,
+10554,
+13245,
+18253,
+59036,
+45645,
+8439,
+25416,
+19092,
+25992,
+40186,
+40272,
+16144,
+20614,
+20468,
+3984,
+18552,
+42058,
+17573,
+50252,
+27148,
+6876,
+22644,
+20963,
+33104,
+15874,
+45301,
+28687,
+21057,
+50173,
+9258,
+23989,
+55493,
+510,
+13211,
+5264,
+63397,
+17404,
+30418,
+28690,
+18316,
+48643,
+15717,
+44094,
+43999,
+2195,
+16624,
+35866,
+53228,
+60783,
+14217,
+23735,
+48157,
+36286,
+17426,
+20553,
+24626,
+39485,
+15445,
+44548,
+42665,
+21243,
+9296,
+16743,
+40899,
+52836,
+8913,
+27623,
+7842,
+42918,
+46898,
+38698,
+43475,
+31755,
+37349,
+13991,
+1542,
+33173,
+52770,
+42195,
+4749,
+27223,
+34380,
+29894,
+54408,
+54837,
+46604,
+17583,
+53138,
+57593,
+40678,
+44565,
+62416,
+7710,
+5041,
+36409,
+26214,
+65533,
+21847,
+9363,
+23832,
+56799,
+41392,
+51290,
+31555,
+19027,
+14980,
+42011,
+38103,
+37649,
+62967,
+57196,
+22216,
+22886,
+23476,
+64614,
+50682,
+39820,
+49745,
+30132,
+46092,
+60708,
+2648,
+49630,
+4900,
+4133,
+10258,
+8261,
+59676,
+25802,
+26515,
+6311,
+23103,
+47205,
+47258,
+21267,
+56235,
+47401,
+53475,
+25116,
+11881,
+2996,
+36979,
+54077,
+5257,
+54557,
+34617,
+3952,
+41001,
+63954,
+62431,
+20728,
+51472,
+5290,
+33779,
+47947,
+46852,
+48810,
+10788,
+3184,
+35249,
+65023,
+39221,
+10466,
+17182,
+59733,
+37654,
+9396,
+15979,
+16213,
+54952,
+30658,
+14028,
+7354,
+4483,
+49943,
+30792,
+61839,
+40986,
+6213,
+792,
+30910,
+51231,
+10891,
+62704,
+62923,
+52245,
+1643,
+12638,
+28572,
+48402,
+62566,
+30607,
+29774,
+47925,
+33858,
+7964,
+51904,
+56918,
+13043,
+59956,
+9949,
+31439,
+18747,
+4451,
+23569,
+58456,
+64492,
+17013,
+7624,
+34149,
+61648,
+28842,
+15995,
+65116,
+38543,
+25249,
+21344,
+8684,
+51676,
+62734,
+13637,
+39007,
+16938,
+58686,
+12697,
+9926,
+16037,
+8897,
+58946,
+31596,
+980,
+48636,
+27639,
+57270,
+8207,
+13934,
+2931,
+27472,
+60798,
+26628,
+15159,
+45276,
+48675,
+19315,
+4103,
+27867,
+24508,
+31736,
+13820,
+18020,
+38150,
+42562,
+35451,
+21114,
+53489,
+57590,
+29025,
+42808,
+14485,
+50839,
+5303,
+18597,
+60298,
+15328,
+32622,
+27477,
+4826,
+57093,
+37586,
+12140,
+17728,
+34128,
+48943,
+31239,
+33552,
+9441,
+19415,
+37890,
+17759,
+56213,
+22559,
+44253,
+53541,
+20091,
+1309,
+56683,
+17442,
+30750,
+52122,
+65367,
+11247,
+47365,
+3348,
+8494,
+40681,
+35695,
+13780,
+2122,
+62451,
+25536,
+10695,
+26967,
+64024,
+17989,
+58518,
+31238,
+48977,
+34129,
+42866,
+26184,
+28591,
+20981,
+1291,
+38400,
+37095,
+26814,
+10811,
+59822,
+59700,
+14858,
+46718,
+2916,
+57989,
+34033,
+54818,
+63245,
+50062,
+65466,
+34642,
+31607,
+53481,
+5095,
+22031,
+14187,
+15988,
+63341,
+16333,
+29672,
+26635,
+6007,
+59353,
+4686,
+17801,
+64209,
+19509,
+53220,
+15023,
+39858,
+14383,
+36820,
+34415,
+4695,
+38310,
+18187,
+29177,
+52113,
+62257,
+59442,
+30296,
+53128,
+38701,
+56873,
+53638,
+9029,
+59173,
+17253,
+44157,
+57065,
+5788,
+20424,
+62724,
+7156,
+12537,
+50322,
+43711,
+1058,
+2167,
+41143,
+31809,
+37607,
+46078,
+36077,
+61122,
+44796,
+33708,
+62019,
+36133,
+36854,
+18420,
+58821,
+61800,
+10228,
+43876,
+45771,
+57338,
+9762,
+31484,
+33395,
+42352,
+6873,
+15265,
+60322,
+54766,
+55175,
+58403,
+39959,
+32266,
+19930,
+64591,
+2567,
+46372,
+50337,
+21690,
+49295,
+3661,
+26112,
+50216,
+7560,
+5245,
+863,
+34059,
+17305,
+28619,
+64835,
+10098,
+28308,
+55699,
+63506,
+12495,
+51110,
+55866,
+8859,
+56314,
+18002,
+19404,
+25054,
+2363,
+28417,
+10787,
+49093,
+46853,
+57933,
+25921,
+39171,
+50790,
+28094,
+13351,
+29849,
+32326,
+29912,
+42518,
+42860,
+3316,
+54928,
+56240,
+16350,
+24639,
+50731,
+4348,
+64453,
+63420,
+41101,
+55861,
+49633,
+41434,
+19239,
+51889,
+27134,
+31865,
+49889,
+15913,
+7039,
+46482,
+58057,
+34395,
+40793,
+40513,
+60072,
+57082,
+25684,
+14004,
+8602,
+63923,
+25842,
+58734,
+23096,
+49983,
+61931,
+33124,
+26657,
+58588,
+50097,
+65330,
+22589,
+61253,
+51690,
+29797,
+53558,
+61418,
+55082,
+34412,
+23195,
+19544,
+7170,
+52076,
+14350,
+19953,
+359,
+42418,
+22150,
+52588,
+5017,
+56670,
+20011,
+30631,
+6182,
+60853,
+7866,
+25601,
+57902,
+36461,
+38381,
+57494,
+5567,
+45964,
+28393,
+64317,
+47850,
+58175,
+59555,
+51863,
+63012,
+64785,
+49481,
+22195,
+25692,
+63227,
+37694,
+49774,
+26200,
+10449,
+54080,
+36242,
+33557,
+7721,
+13436,
+18652,
+62411,
+32025,
+56888,
+15635,
+58115,
+20887,
+48247,
+53260,
+58144,
+27512,
+1778,
+52018,
+61005,
+62110,
+33879,
+7106,
+25941,
+60493,
+38728,
+33344,
+54852,
+61925,
+43287,
+58551,
+57910,
+52037,
+19314,
+49007,
+45277,
+32205,
+54120,
+34486,
+61057,
+9585,
+43804,
+12109,
+14575,
+33961,
+19879,
+52724,
+52049,
+42714,
+13001,
+40915,
+27354,
+20061,
+3859,
+1580,
+49703,
+14327,
+18192,
+51561,
+24279,
+24491,
+54955,
+35182,
+32817,
+64214,
+15716,
+49209,
+18317,
+7940,
+42083,
+38206,
+47426,
+27638,
+49018,
+981,
+41312,
+34455,
+3807,
+35265,
+57482,
+45610,
+28007,
+57154,
+15490,
+62805,
+56179,
+29320,
+12348,
+24249,
+58033,
+27105,
+4075,
+28380,
+28785,
+27269,
+44036,
+9228,
+24209,
+1119,
+1932,
+37906,
+32964,
+50196,
+44890,
+11546,
+65328,
+50099,
+58308,
+30675,
+26529,
+16510,
+6852,
+51327,
+43966,
+35662,
+40847,
+24441,
+34507,
+22079,
+45215,
+46152,
+40326,
+45149,
+64265,
+50069,
+25437,
+55127,
+54973,
+51696,
+3633,
+58198,
+29590,
+46243,
+8636,
+37235,
+3199,
+62937,
+32303,
+43175,
+62613,
+39238,
+59709,
+1958,
+52357,
+31305,
+20816,
+15780,
+1359,
+59418,
+12245,
+44372,
+24325,
+36385,
+25781,
+51092,
+43591,
+8578,
+9191,
+31094,
+36343,
+41059,
+59685,
+45197,
+32755,
+10487,
+36550,
+6637,
+12984,
+2464,
+28263,
+51869,
+49306,
+13597,
+56000,
+43099,
+55157,
+23661,
+9238,
+23743,
+4420,
+60231,
+6635,
+36552,
+17907,
+1044,
+16495,
+57914,
+23638,
+15094,
+21440,
+37952,
+61278,
+6881,
+30903,
+47014,
+54969,
+57649,
+51013,
+1251,
+19526,
+28200,
+63809,
+30929,
+304,
+1720,
+55637,
+63671,
+23264,
+44898,
+29016,
+54209,
+57079,
+58641,
+38943,
+4957,
+38004,
+53567,
+15349,
+19312,
+52039,
+32534,
+32559,
+36620,
+44920,
+7729,
+196,
+13308,
+61702,
+42171,
+45250,
+35942,
+39701,
+25919,
+57935,
+59856,
+44850,
+33118,
+45344,
+8099,
+28101,
+11454,
+29311,
+14885,
+26148,
+60558,
+54071,
+50287,
+7780,
+38299,
+49407,
+42109,
+15211,
+64336,
+6937,
+38815,
+26801,
+20113,
+65046,
+33037,
+41220,
+57924,
+48310,
+3018,
+41659,
+25349,
+25267,
+20753,
+10627,
+14042,
+36412,
+18433,
+1192,
+14284,
+28551,
+6871,
+42354,
+11951,
+60104,
+12440,
+51278,
+35270,
+50820,
+47025,
+4554,
+43136,
+9735,
+19825,
+25874,
+31593,
+559,
+3863,
+24306,
+22536,
+37383,
+12681,
+13928,
+22990,
+57508,
+31896,
+65162,
+58003,
+88,
+19883,
+53689,
+62075,
+18009,
+62565,
+49060,
+28573,
+37742,
+32562,
+29979,
+38416,
+20027,
+63591,
+2764,
+9079,
+60002,
+45749,
+27538,
+3604,
+32415,
+51501,
+63913,
+4887,
+7630,
+43741,
+28889,
+8492,
+3350,
+60942,
+61244,
+43107,
+59209,
+3181,
+33305,
+24143,
+14249,
+9075,
+51450,
+43545,
+15784,
+26440,
+2145,
+44863,
+50020,
+36119,
+14794,
+31620,
+29100,
+11518,
+58136,
+22381,
+55070,
+16451,
+5805,
+2315,
+46324,
+36498,
+50658,
+21669,
+11442,
+32738,
+11109,
+36592,
+2897,
+10562,
+13843,
+61036,
+14958,
+49490,
+54155,
+27918,
+15968,
+25766,
+14168,
+41153,
+13854,
+33576,
+57432,
+56149,
+12575,
+46866,
+44887,
+27731,
+25167,
+27210,
+64117,
+10716,
+27171,
+16173,
+5378,
+24917,
+7147,
+18384,
+58954,
+7180,
+39482,
+3017,
+48448,
+57925,
+38655,
+50349,
+52031,
+50868,
+27180,
+63069,
+14001,
+6521,
+54374,
+24526,
+15929,
+58721,
+63682,
+53484,
+33732,
+14712,
+37137,
+17872,
+40963,
+2428,
+7124,
+18149,
+56955,
+6363,
+16653,
+21381,
+5325,
+28192,
+6289,
+19933,
+59150,
+30327,
+1196,
+39367,
+22896,
+3762,
+39884,
+28950,
+31924,
+45570,
+2000,
+42415,
+6356,
+41249,
+59140,
+46920,
+52551,
+63075,
+11452,
+28103,
+46493,
+44173,
+22948,
+13049,
+3883,
+28298,
+20918,
+3325,
+42070,
+7578,
+53259,
+48696,
+20888,
+55285,
+42874,
+58045,
+26571,
+7817,
+50692,
+24350,
+59286,
+16013,
+41642,
+6784,
+43834,
+31477,
+16709,
+36919,
+23349,
+21958,
+57635,
+9681,
+63663,
+61899,
+36923,
+1586,
+10738,
+51575,
+31495,
+30233,
+11288,
+35141,
+33232,
+22364,
+39584,
+32969,
+37079,
+59587,
+18057,
+24444,
+40720,
+19351,
+8376,
+27297,
+55918,
+7694,
+19232,
+2083,
+58022,
+6150,
+60200,
+480,
+43874,
+10230,
+62854,
+44010,
+36904,
+7598,
+3744,
+65503,
+1987,
+2935,
+56453,
+20875,
+54679,
+53592,
+3595,
+16203,
+7091,
+9473,
+22490,
+12560,
+46515,
+50774,
+13777,
+5552,
+44070,
+22096,
+15038,
+41021,
+5607,
+6931,
+49929,
+58139,
+34351,
+49728,
+36878,
+4824,
+27479,
+7139,
+36285,
+49198,
+23736,
+35634,
+2756,
+6752,
+34463,
+40104,
+18202,
+52854,
+51611,
+8129,
+37187,
+36955,
+38705,
+34886,
+24007,
+49424,
+17634,
+31322,
+25640,
+29857,
+27765,
+35465,
+2139,
+16324,
+35120,
+12203,
+6472,
+57823,
+12551,
+591,
+47936,
+60053,
+52127,
+63930,
+49419,
+21581,
+46896,
+42920,
+11543,
+20944,
+40046,
+41902,
+24811,
+33517,
+29250,
+16341,
+44985,
+19355,
+3309,
+26196,
+31665,
+58375,
+62225,
+23318,
+14517,
+36110,
+46970,
+46148,
+28054,
+44369,
+8853,
+16574,
+34788,
+40692,
+42121,
+1574,
+50891,
+44559,
+62998,
+58127,
+52190,
+59660,
+31933,
+46729,
+10145,
+57664,
+58603,
+30411,
+59058,
+50474,
+9282,
+38781,
+26473,
+26145,
+32579,
+23643,
+3208,
+7680,
+62637,
+12706,
+59220,
+64573,
+7419,
+53464,
+37042,
+57343,
+18727,
+44685,
+30112,
+56715,
+22498,
+40761,
+28377,
+38035,
+63989,
+39230,
+56771,
+64394,
+36098,
+24982,
+64027,
+34181,
+11940,
+8755,
+35726,
+13691,
+427,
+16079,
+21003,
+15081,
+41608,
+12181,
+22451,
+38336,
+307,
+60644,
+11616,
+26798,
+62711,
+8747,
+38156,
+14906,
+50920,
+19941,
+31008,
+1882,
+51825,
+19914,
+43417,
+62421,
+57480,
+35267,
+24071,
+11874,
+50212,
+41601,
+22596,
+12649,
+64621,
+64965,
+6658,
+24025,
+62442,
+52384,
+39382,
+45920,
+12934,
+39308,
+4966,
+46881,
+17960,
+35705,
+59358,
+8095,
+51817,
+28869,
+65227,
+58751,
+21705,
+7196,
+10209,
+60037,
+36911,
+61307,
+31460,
+59874,
+27746,
+1019,
+57378,
+34810,
+37398,
+42277,
+30621,
+31220,
+30472,
+44782,
+54912,
+55267,
+64970,
+42959,
+64612,
+23478,
+16305,
+15286,
+40467,
+26575,
+37487,
+38883,
+47202,
+30446,
+58678,
+18932,
+16374,
+12400,
+59656,
+2754,
+35636,
+58364,
+46851,
+49095,
+33780,
+28190,
+5327,
+12609,
+46064,
+55626,
+5404,
+53631,
+26436,
+60052,
+48126,
+592,
+24978,
+43360,
+31017,
+3071,
+21868,
+27152,
+63624,
+46583,
+33857,
+49056,
+29775,
+62899,
+28209,
+12437,
+25319,
+52164,
+46615,
+40717,
+53349,
+38295,
+43224,
+59167,
+1030,
+9091,
+58510,
+10644,
+62786,
+30434,
+37874,
+57921,
+16112,
+17397,
+34216,
+31152,
+18297,
+21079,
+39774,
+44609,
+13389,
+44017,
+15984,
+56797,
+23834,
+47825,
+17685,
+7364,
+1140,
+5113,
+32180,
+30901,
+6883,
+13910,
+27811,
+27583,
+4261,
+939,
+13457,
+28601,
+61700,
+13310,
+7662,
+41788,
+8763,
+40240,
+21471,
+56067,
+46650,
+30736,
+47698,
+46694,
+58265,
+20732,
+35047,
+59601,
+57996,
+23530,
+41519,
+20636,
+59447,
+26134,
+64881,
+17711,
+17647,
+58174,
+48722,
+64318,
+42670,
+7880,
+13140,
+42522,
+32274,
+64874,
+100,
+27254,
+20868,
+23733,
+14219,
+12689,
+27698,
+26645,
+33955,
+43352,
+9428,
+41489,
+61499,
+24482,
+40183,
+655,
+17684,
+47891,
+23835,
+58711,
+18505,
+4835,
+28853,
+41128,
+29534,
+4612,
+57852,
+5144,
+24942,
+47193,
+45948,
+23311,
+53396,
+16558,
+31410,
+22426,
+45208,
+10005,
+61261,
+57137,
+39640,
+2509,
+26973,
+11413,
+42232,
+49899,
+18090,
+35731,
+14265,
+45559,
+57530,
+12501,
+41419,
+43407,
+29377,
+37945,
+19564,
+57559,
+51867,
+28265,
+11893,
+53796,
+37107,
+27646,
+16566,
+9325,
+31870,
+6719,
+54937,
+14606,
+6740,
+36391,
+47171,
+62494,
+51599,
+56232,
+21633,
+10425,
+34957,
+50482,
+2957,
+1707,
+12011,
+13319,
+31259,
+34453,
+41314,
+15004,
+52863,
+3652,
+1813,
+3939,
+30158,
+21336,
+60685,
+1352,
+46308,
+57518,
+45684,
+41026,
+2346,
+3296,
+43096,
+59951,
+15866,
+60850,
+16630,
+1329,
+22323,
+20507,
+63023,
+39850,
+59019,
+59645,
+43722,
+20007,
+55374,
+19497,
+64654,
+40200,
+58334,
+5470,
+34764,
+35174,
+63904,
+5101,
+43277,
+3225,
+19849,
+6350,
+49275,
+40945,
+49854,
+51726,
+40894,
+23844,
+33884,
+18990,
+20334,
+14997,
+19538,
+21793,
+25793,
+46693,
+47866,
+30737,
+7828,
+12206,
+3684,
+32067,
+54545,
+18909,
+49710,
+37979,
+56622,
+37916,
+53216,
+29615,
+12360,
+22953,
+13680,
+19493,
+43810,
+15153,
+53805,
+23437,
+38854,
+14225,
+34990,
+52062,
+29355,
+37009,
+23912,
+35369,
+23946,
+31969,
+28399,
+17246,
+24575,
+43695,
+4682,
+57673,
+52794,
+54382,
+25989,
+36093,
+30311,
+61216,
+45619,
+45818,
+8985,
+971,
+15711,
+6679,
+11313,
+19771,
+50705,
+65211,
+60900,
+47316,
+38370,
+5885,
+62721,
+14482,
+56343,
+54652,
+43036,
+55422,
+3053,
+9250,
+19242,
+28984,
+17011,
+64494,
+15033,
+11662,
+42679,
+15100,
+37168,
+63170,
+38254,
+2396,
+42544,
+11035,
+58299,
+44936,
+50035,
+21352,
+24788,
+25006,
+59146,
+25303,
+20019,
+47314,
+60902,
+18740,
+42339,
+53268,
+32668,
+1977,
+53577,
+20283,
+18550,
+3986,
+59604,
+9889,
+36931,
+19984,
+39133,
+44087,
+63280,
+16240,
+61878,
+52943,
+19392,
+19997,
+42175,
+42169,
+61704,
+668,
+10483,
+9559,
+3792,
+62497,
+18655,
+17541,
+29833,
+135,
+4338,
+726,
+44714,
+12720,
+19099,
+9859,
+39835,
+41711,
+34116,
+65052,
+26947,
+50861,
+3471,
+2738,
+41826,
+57652,
+29486,
+9867,
+62038,
+59374,
+36258,
+3728,
+9263,
+27966,
+40553,
+1512,
+16591,
+7020,
+31346,
+38083,
+1049,
+10799,
+21076,
+40784,
+14741,
+13280,
+54741,
+52892,
+9222,
+16721,
+46134,
+61238,
+15295,
+43331,
+51008,
+3461,
+17133,
+2973,
+4210,
+21027,
+45046,
+28330,
+20707,
+24791,
+55432,
+12967,
+51716,
+16537,
+27388,
+12135,
+15127,
+45753,
+19732,
+49769,
+22351,
+18769,
+64501,
+62182,
+57468,
+65010,
+50462,
+5353,
+49319,
+41929,
+28937,
+38916,
+8832,
+50595,
+61156,
+53074,
+2499,
+25508,
+6018,
+13557,
+4917,
+10222,
+34978,
+54924,
+57034,
+47323,
+37388,
+6015,
+34872,
+5949,
+17327,
+41094,
+31032,
+35951,
+64169,
+19082,
+50585,
+5511,
+24078,
+7034,
+57368,
+56030,
+7109,
+25377,
+56688,
+54509,
+43203,
+29273,
+20040,
+46303,
+63456,
+44333,
+26209,
+30039,
+64301,
+26375,
+13580,
+10767,
+63986,
+9599,
+64671,
+58113,
+15637,
+17741,
+29807,
+1938,
+54587,
+9602,
+45262,
+26188,
+65030,
+22926,
+60750,
+9874,
+47108,
+2871,
+22478,
+7555,
+5075,
+36475,
+10395,
+24694,
+46823,
+27637,
+48638,
+38207,
+31399,
+58594,
+41757,
+6566,
+3943,
+34049,
+7263,
+58618,
+33660,
+28125,
+10158,
+64680,
+57499,
+1110,
+38226,
+44941,
+35966,
+34547,
+42078,
+29389,
+28885,
+39281,
+53474,
+49113,
+56236,
+45292,
+64706,
+25821,
+60566,
+51257,
+32680,
+1499,
+20667,
+372,
+14760,
+58412,
+17250,
+8583,
+43985,
+7044,
+21611,
+30292,
+51917,
+31386,
+3891,
+8572,
+39577,
+2768,
+27314,
+14488,
+26878,
+11802,
+55821,
+10437,
+43820,
+24093,
+3975,
+1468,
+3347,
+48958,
+11248,
+30576,
+61127,
+59072,
+53848,
+3270,
+26328,
+32253,
+12739,
+8813,
+44250,
+3514,
+59814,
+27226,
+16641,
+36361,
+26400,
+9645,
+51209,
+16885,
+13977,
+44144,
+52055,
+36734,
+59879,
+25998,
+38813,
+6939,
+25432,
+17387,
+12684,
+55453,
+13111,
+34194,
+19182,
+34435,
+24683,
+11038,
+3480,
+21012,
+37387,
+47485,
+57035,
+20662,
+27579,
+23034,
+63242,
+38369,
+47643,
+60901,
+47609,
+20020,
+60330,
+2909,
+24842,
+43170,
+5249,
+15268,
+54314,
+20155,
+49700,
+3109,
+62651,
+58087,
+55385,
+44749,
+4355,
+62544,
+56404,
+15725,
+6373,
+42270,
+52972,
+11555,
+53997,
+49300,
+34372,
+15359,
+56804,
+52291,
+16290,
+6502,
+15642,
+32449,
+40743,
+40730,
+40315,
+42879,
+24261,
+32991,
+61726,
+7862,
+12745,
+23648,
+8711,
+64041,
+53132,
+15952,
+5054,
+63697,
+6879,
+61280,
+33986,
+34020,
+38486,
+21266,
+49116,
+47206,
+11074,
+31944,
+21210,
+50565,
+38143,
+21995,
+23542,
+64444,
+50114,
+63018,
+63638,
+21651,
+63846,
+14233,
+9518,
+34384,
+17637,
+44459,
+24755,
+29463,
+9719,
+1572,
+42123,
+52701,
+55770,
+61223,
+25453,
+54150,
+55586,
+34100,
+10723,
+45377,
+25892,
+61139,
+36846,
+16327,
+16895,
+57598,
+40877,
+54830,
+9018,
+53243,
+51069,
+31227,
+13955,
+29271,
+43205,
+30834,
+60996,
+11073,
+47257,
+49117,
+23104,
+30445,
+47958,
+38884,
+19818,
+38601,
+54691,
+33401,
+8122,
+30103,
+45947,
+47813,
+24943,
+63733,
+31988,
+57761,
+40478,
+14561,
+28674,
+4796,
+24825,
+25927,
+26554,
+7437,
+16187,
+62449,
+2124,
+22299,
+27841,
+15765,
+13066,
+30174,
+62493,
+47770,
+36392,
+43840,
+57802,
+31634,
+64902,
+38765,
+25358,
+42028,
+61175,
+31695,
+34343,
+10338,
+12723,
+28479,
+43891,
+41261,
+58389,
+17222,
+6584,
+60094,
+20534,
+23819,
+59906,
+40010,
+32446,
+21405,
+25391,
+62113,
+63834,
+3274,
+61996,
+63446,
+57291,
+37495,
+2226,
+21778,
+59312,
+37729,
+6381,
+22521,
+324,
+4639,
+2727,
+33274,
+19151,
+43422,
+43231,
+38762,
+10666,
+21007,
+54011,
+1434,
+28682,
+16676,
+6717,
+31872,
+36630,
+60838,
+32909,
+24780,
+33350,
+2870,
+47436,
+9875,
+9590,
+29124,
+17098,
+64346,
+44445,
+551,
+39062,
+39394,
+34750,
+25472,
+9631,
+6322,
+21591,
+41769,
+38807,
+57177,
+46196,
+4430,
+7840,
+27625,
+64990,
+34690,
+23625,
+10750,
+10998,
+8462,
+63300,
+53433,
+32117,
+18841,
+7274,
+62367,
+11334,
+13172,
+45757,
+25571,
+22003,
+30217,
+35641,
+11131,
+61517,
+29288,
+32341,
+53706,
+20759,
+41271,
+56698,
+53666,
+61767,
+63434,
+6642,
+31265,
+25278,
+27482,
+8197,
+55777,
+46486,
+27788,
+59543,
+22922,
+4604,
+64371,
+31335,
+65172,
+19266,
+450,
+7848,
+64010,
+20229,
+8952,
+7570,
+23148,
+33507,
+6825,
+17715,
+60703,
+15514,
+33891,
+21730,
+58870,
+4553,
+48427,
+50821,
+40450,
+23422,
+40927,
+20089,
+53543,
+9187,
+2148,
+34950,
+54968,
+48515,
+30904,
+58557,
+41761,
+49969,
+18786,
+28777,
+7063,
+26804,
+534,
+65293,
+2698,
+6460,
+21069,
+15998,
+9570,
+65134,
+11902,
+39974,
+52788,
+11635,
+28213,
+26600,
+5601,
+54272,
+11911,
+45253,
+17937,
+61552,
+16303,
+23480,
+10247,
+45694,
+4602,
+22924,
+65032,
+53965,
+32087,
+52681,
+7531,
+12957,
+41499,
+12592,
+46147,
+48100,
+36111,
+29795,
+51692,
+4503,
+31911,
+4439,
+28238,
+5123,
+25850,
+23492,
+57568,
+8152,
+30571,
+56751,
+20382,
+65075,
+35826,
+24568,
+44433,
+44212,
+46861,
+37558,
+54915,
+45743,
+40943,
+49277,
+57166,
+6633,
+60233,
+16549,
+5240,
+1339,
+55414,
+63874,
+23700,
+41412,
+24557,
+28980,
+10634,
+54495,
+10816,
+20849,
+56652,
+59452,
+25800,
+59678,
+45859,
+20198,
+52550,
+48263,
+59141,
+41834,
+7768,
+50843,
+50154,
+27759,
+22743,
+5813,
+15816,
+28798,
+28902,
+41217,
+36223,
+13666,
+16233,
+56160,
+40703,
+18963,
+30029,
+31543,
+38697,
+49180,
+42919,
+48120,
+21582,
+6405,
+26979,
+11255,
+11279,
+22968,
+24485,
+10244,
+8880,
+52100,
+16834,
+3127,
+3039,
+17959,
+47997,
+4967,
+11623,
+11373,
+11404,
+52195,
+28449,
+56607,
+30307,
+26903,
+52016,
+1780,
+62079,
+39054,
+44886,
+48327,
+12576,
+10652,
+2025,
+37557,
+46949,
+44213,
+50951,
+40833,
+54475,
+8539,
+2403,
+57932,
+48809,
+49094,
+47948,
+58365,
+13463,
+53004,
+64194,
+20463,
+40177,
+23511,
+36106,
+52994,
+62617,
+54110,
+12201,
+35122,
+6403,
+21584,
+29082,
+12422,
+61497,
+41491,
+29293,
+31982,
+46098,
+41920,
+19535,
+39187,
+8010,
+27636,
+47428,
+24695,
+24312,
+30981,
+31403,
+14386,
+55899,
+26700,
+36045,
+25206,
+54613,
+7,
+8193,
+17477,
+20853,
+27119,
+45512,
+50296,
+51119,
+45991,
+58369,
+14769,
+61336,
+30070,
+63400,
+4634,
+17930,
+23199,
+26761,
+12385,
+53310,
+49269,
+34097,
+16490,
+61087,
+13185,
+27183,
+45482,
+20351,
+37925,
+23452,
+27333,
+8117,
+62333,
+11582,
+52649,
+19448,
+58802,
+57723,
+62837,
+59079,
+31719,
+5689,
+20883,
+43185,
+18028,
+1037,
+38957,
+27605,
+62231,
+6287,
+28194,
+56933,
+1616,
+61485,
+37178,
+20544,
+53697,
+14582,
+6441,
+6807,
+15567,
+18528,
+36761,
+10824,
+61742,
+13941,
+11607,
+8227,
+33112,
+57548,
+11479,
+60931,
+1168,
+64641,
+44449,
+25542,
+8976,
+64202,
+1917,
+38920,
+8528,
+59191,
+10144,
+48083,
+31934,
+42775,
+55538,
+26752,
+41744,
+7667,
+44453,
+45917,
+33937,
+2915,
+48929,
+14859,
+42750,
+20143,
+33007,
+54866,
+5142,
+57854,
+32906,
+43298,
+6293,
+7925,
+44544,
+38361,
+49867,
+44349,
+32471,
+15584,
+43747,
+49732,
+24115,
+33419,
+18460,
+58264,
+47865,
+47699,
+25794,
+51394,
+33650,
+28815,
+6335,
+22519,
+6383,
+64727,
+8183,
+11092,
+63163,
+41621,
+8077,
+44793,
+58069,
+14202,
+6258,
+60690,
+30227,
+12448,
+52777,
+1393,
+27598,
+40094,
+55989,
+58893,
+34274,
+57838,
+316,
+53547,
+27437,
+51104,
+14092,
+23048,
+28122,
+15992,
+3058,
+41050,
+55295,
+55830,
+43438,
+30735,
+47868,
+56068,
+28291,
+50400,
+31726,
+34673,
+36527,
+61607,
+2036,
+54213,
+7016,
+44178,
+7258,
+6682,
+56820,
+51214,
+31113,
+34859,
+52440,
+4600,
+45696,
+10991,
+17846,
+15828,
+33699,
+61985,
+59466,
+33115,
+56674,
+28850,
+55743,
+58993,
+53288,
+25380,
+40716,
+47918,
+52165,
+3432,
+59830,
+11590,
+41638,
+29358,
+40588,
+13007,
+6858,
+17582,
+49164,
+54838,
+39741,
+14145,
+35262,
+56093,
+61471,
+56703,
+14944,
+10736,
+1588,
+61991,
+33130,
+8679,
+40418,
+21524,
+32497,
+29317,
+16022,
+23041,
+33856,
+47927,
+63625,
+28637,
+32829,
+35700,
+51302,
+56021,
+64863,
+24833,
+18635,
+35509,
+26823,
+27993,
+14654,
+22911,
+14649,
+2788,
+50969,
+6781,
+26411,
+25591,
+22457,
+34036,
+30225,
+60692,
+29103,
+2165,
+1060,
+32368,
+6891,
+37971,
+51753,
+27570,
+36215,
+53191,
+31652,
+17832,
+45204,
+54988,
+7053,
+45535,
+59368,
+20476,
+44517,
+63926,
+7206,
+25963,
+62350,
+34086,
+9122,
+42428,
+6283,
+2431,
+55217,
+58031,
+24251,
+45738,
+31836,
+3554,
+50753,
+56503,
+14069,
+61453,
+8781,
+3098,
+50227,
+62460,
+50773,
+48176,
+12561,
+26509,
+10169,
+33981,
+16392,
+50558,
+30783,
+32431,
+49665,
+56057,
+5864,
+58420,
+3381,
+41916,
+30264,
+15577,
+5752,
+19162,
+2843,
+63441,
+44172,
+48258,
+28104,
+12157,
+15229,
+24714,
+1242,
+27787,
+47050,
+55778,
+5421,
+58056,
+48777,
+7040,
+22945,
+815,
+55338,
+59028,
+57794,
+43571,
+5676,
+64626,
+7707,
+22422,
+59900,
+5757,
+55852,
+62743,
+29942,
+21549,
+44776,
+24268,
+33076,
+6799,
+64314,
+35022,
+57791,
+33823,
+1367,
+18062,
+14953,
+6439,
+14584,
+24706,
+879,
+44164,
+49442,
+49563,
+40120,
+16295,
+39546,
+29446,
+3010,
+23334,
+55197,
+52816,
+17967,
+55679,
+11151,
+28027,
+33796,
+2741,
+34825,
+6706,
+343,
+45915,
+44455,
+15971,
+28974,
+5538,
+36254,
+19945,
+24,
+60295,
+64200,
+8978,
+6081,
+30873,
+3616,
+7756,
+28524,
+63121,
+26650,
+52677,
+35830,
+26173,
+7390,
+44115,
+2213,
+10095,
+49342,
+31693,
+61177,
+13367,
+13380,
+24769,
+23398,
+9269,
+41734,
+202,
+6135,
+51621,
+42540,
+14339,
+24374,
+44628,
+49975,
+32262,
+18411,
+22116,
+3302,
+21053,
+33280,
+54093,
+32801,
+53800,
+65041,
+52563,
+59784,
+19040,
+62695,
+50336,
+48839,
+2568,
+8343,
+35008,
+62101,
+8434,
+41320,
+10867,
+59562,
+64057,
+62126,
+52027,
+28037,
+60599,
+36201,
+41984,
+31342,
+18207,
+31103,
+4588,
+35790,
+6842,
+21454,
+59331,
+60595,
+58150,
+16088,
+61709,
+43738,
+13750,
+34969,
+775,
+44002,
+9043,
+14849,
+33202,
+63728,
+38243,
+61660,
+9047,
+13162,
+10138,
+52216,
+34280,
+28079,
+28272,
+40886,
+36497,
+48352,
+2316,
+28060,
+31701,
+5593,
+12853,
+12467,
+21915,
+35099,
+61940,
+49336,
+51488,
+10745,
+2410,
+40068,
+57517,
+47746,
+1353,
+57842,
+17335,
+63455,
+47461,
+20041,
+36615,
+38463,
+24102,
+16753,
+13649,
+56286,
+17905,
+36554,
+51380,
+126,
+26319,
+9844,
+13834,
+21024,
+13710,
+45625,
+49357,
+24747,
+9643,
+26402,
+21452,
+6844,
+5918,
+19754,
+20032,
+61573,
+41989,
+22356,
+29608,
+364,
+18495,
+65088,
+27942,
+585,
+4139,
+840,
+15041,
+7447,
+46118,
+6552,
+56177,
+62807,
+39962,
+28249,
+51556,
+13535,
+30063,
+65017,
+53163,
+32359,
+9627,
+22634,
+19568,
+58351,
+9095,
+31768,
+39933,
+8635,
+48577,
+29591,
+4344,
+65357,
+25630,
+8398,
+25899,
+21694,
+146,
+20955,
+36907,
+60163,
+60720,
+52896,
+51264,
+22038,
+65425,
+50187,
+17046,
+13499,
+16861,
+16531,
+61435,
+28207,
+62901,
+9502,
+60676,
+62396,
+3904,
+6261,
+9954,
+65459,
+19577,
+15222,
+30378,
+23602,
+28978,
+24559,
+53031,
+3156,
+1122,
+38868,
+28142,
+27056,
+39594,
+5046,
+4429,
+47090,
+57178,
+24991,
+58324,
+24382,
+51685,
+62136,
+2449,
+32716,
+24816,
+17330,
+57162,
+61304,
+20551,
+17428,
+62229,
+27607,
+4865,
+64474,
+36719,
+21620,
+8655,
+7277,
+33706,
+44798,
+1066,
+14128,
+31371,
+59883,
+23234,
+54264,
+2879,
+62648,
+15591,
+45333,
+40044,
+20946,
+34933,
+63934,
+28024,
+36828,
+1268,
+14823,
+40325,
+48589,
+45216,
+5571,
+28053,
+48099,
+46971,
+12593,
+17949,
+45541,
+13446,
+15787,
+33745,
+22830,
+20376,
+54635,
+58534,
+33537,
+61237,
+47534,
+16722,
+40484,
+13539,
+51661,
+11937,
+7253,
+62927,
+38861,
+54487,
+61469,
+56095,
+16564,
+27648,
+63765,
+6551,
+46263,
+7448,
+10154,
+3344,
+54527,
+2156,
+36613,
+20043,
+35722,
+7310,
+60285,
+5218,
+22102,
+23686,
+53757,
+38670,
+4087,
+50805,
+16026,
+41919,
+46829,
+31983,
+56098,
+36672,
+39815,
+60707,
+49130,
+30133,
+12887,
+18760,
+6736,
+6830,
+32285,
+52906,
+49696,
+30316,
+56061,
+58635,
+40780,
+36076,
+48869,
+37608,
+30918,
+61587,
+55149,
+266,
+28474,
+13403,
+64389,
+11200,
+56428,
+41325,
+13790,
+55625,
+47942,
+12610,
+36519,
+11330,
+14987,
+63461,
+10329,
+54391,
+30385,
+13466,
+30947,
+37,
+60364,
+42818,
+6437,
+14955,
+32064,
+16164,
+14872,
+51856,
+17856,
+21728,
+33893,
+10162,
+17812,
+884,
+8369,
+58906,
+11339,
+10710,
+38261,
+31663,
+26198,
+49776,
+8846,
+1327,
+16632,
+12318,
+32703,
+39120,
+59740,
+27489,
+628,
+2710,
+50574,
+56011,
+55575,
+56294,
+54965,
+4099,
+39531,
+41110,
+64285,
+17027,
+37338,
+3299,
+51100,
+40292,
+36874,
+30611,
+50905,
+23616,
+18710,
+26351,
+50539,
+17835,
+43745,
+15586,
+39726,
+53266,
+42341,
+16790,
+58368,
+46804,
+51120,
+4288,
+31570,
+54565,
+41727,
+14477,
+60020,
+15910,
+61522,
+63791,
+56975,
+25363,
+39733,
+39024,
+25272,
+49533,
+27591,
+17751,
+7979,
+25567,
+42902,
+19288,
+7187,
+59566,
+34987,
+28392,
+48725,
+5568,
+62949,
+77,
+19326,
+50316,
+61865,
+1697,
+29373,
+62140,
+50852,
+425,
+13693,
+29496,
+41038,
+23310,
+47812,
+47194,
+30104,
+35187,
+26465,
+49519,
+13385,
+36745,
+52859,
+30120,
+12242,
+20487,
+23445,
+22511,
+5640,
+64504,
+6542,
+12251,
+50031,
+52084,
+27045,
+1026,
+44607,
+39776,
+45508,
+1948,
+34972,
+12933,
+48001,
+39383,
+33936,
+46721,
+44454,
+46429,
+344,
+10069,
+63540,
+6535,
+40360,
+63557,
+39608,
+38985,
+56610,
+59231,
+38949,
+14510,
+40427,
+10183,
+43510,
+43975,
+6524,
+7765,
+24128,
+27450,
+22087,
+3066,
+407,
+45138,
+23240,
+32018,
+36289,
+22436,
+10382,
+15844,
+50164,
+35076,
+20413,
+6425,
+11189,
+50738,
+24966,
+5957,
+65527,
+7283,
+37943,
+29379,
+6722,
+26750,
+55540,
+20896,
+34013,
+63328,
+6620,
+13829,
+53421,
+64521,
+24989,
+57180,
+20197,
+46923,
+59679,
+62763,
+9880,
+27281,
+63143,
+19747,
+56488,
+52377,
+65284,
+42392,
+23490,
+25852,
+35070,
+63204,
+56292,
+55577,
+29772,
+30609,
+36876,
+49730,
+43749,
+7076,
+64223,
+53359,
+45829,
+6569,
+30156,
+3941,
+6568,
+45834,
+53360,
+11794,
+64749,
+34893,
+7324,
+5575,
+56053,
+55987,
+40096,
+8984,
+47653,
+45620,
+61270,
+1399,
+40868,
+38447,
+39794,
+11007,
+13724,
+26254,
+12065,
+39950,
+19783,
+18025,
+15769,
+8990,
+12327,
+1750,
+43605,
+64770,
+20107,
+52413,
+34450,
+16283,
+34649,
+14230,
+24137,
+2393,
+19684,
+9050,
+59796,
+9215,
+7290,
+27974,
+59618,
+19259,
+45506,
+39778,
+8064,
+34155,
+11715,
+42609,
+4789,
+51897,
+44506,
+63690,
+57337,
+48856,
+43877,
+11587,
+12087,
+54223,
+17890,
+14833,
+38186,
+29779,
+57424,
+2966,
+57564,
+8970,
+25570,
+47072,
+13173,
+25586,
+19731,
+47513,
+15128,
+52629,
+27537,
+48391,
+60003,
+62944,
+10494,
+34646,
+40942,
+46946,
+54916,
+42995,
+38676,
+31835,
+46527,
+24252,
+609,
+33629,
+41613,
+39258,
+28829,
+50928,
+4447,
+32077,
+31417,
+49912,
+50366,
+14115,
+2977,
+49935,
+21505,
+52675,
+26652,
+18337,
+26937,
+35470,
+41724,
+56739,
+25219,
+55801,
+17114,
+39664,
+45311,
+37029,
+53827,
+44739,
+63184,
+39652,
+62487,
+19862,
+26456,
+7794,
+59762,
+1556,
+4126,
+10990,
+46630,
+4601,
+46982,
+10248,
+3243,
+63942,
+51929,
+62311,
+17823,
+59188,
+13701,
+41025,
+47744,
+57519,
+43973,
+43512,
+736,
+29257,
+2848,
+6557,
+3049,
+19834,
+39082,
+20769,
+24717,
+14809,
+53028,
+53817,
+53369,
+21396,
+37249,
+29863,
+7763,
+6526,
+42956,
+43732,
+23070,
+31575,
+16873,
+12814,
+26942,
+65448,
+17130,
+11849,
+28334,
+31938,
+35278,
+39224,
+39512,
+25625,
+8438,
+49245,
+59037,
+53437,
+50967,
+2790,
+39450,
+8244,
+28044,
+49818,
+21445,
+23614,
+50907,
+59844,
+49361,
+62406,
+57728,
+9662,
+26384,
+32808,
+49356,
+46286,
+13711,
+17518,
+22121,
+61269,
+45817,
+47654,
+61217,
+55690,
+60872,
+60915,
+44392,
+23009,
+35612,
+28006,
+48629,
+57483,
+33270,
+16695,
+947,
+59247,
+17258,
+6388,
+40509,
+2837,
+27412,
+54555,
+5259,
+14616,
+17514,
+34530,
+55238,
+29282,
+19114,
+65514,
+2980,
+59712,
+62646,
+2881,
+61513,
+51186,
+16794,
+65179,
+60765,
+25757,
+57475,
+52668,
+220,
+53083,
+58107,
+33215,
+42179,
+65061,
+55466,
+1999,
+48269,
+31925,
+11047,
+30806,
+22036,
+51266,
+27589,
+49535,
+39460,
+26348,
+57529,
+47793,
+14266,
+1007,
+38556,
+54284,
+28605,
+17942,
+26405,
+58477,
+37082,
+51343,
+4577,
+15104,
+64206,
+43216,
+2488,
+22935,
+13445,
+46144,
+17950,
+23363,
+63869,
+21388,
+59367,
+46543,
+7054,
+50935,
+36780,
+21814,
+17810,
+10164,
+20684,
+8866,
+16801,
+34907,
+9405,
+12034,
+5000,
+58409,
+51763,
+40233,
+17927,
+18224,
+5208,
+38752,
+34842,
+50295,
+46807,
+27120,
+17141,
+1947,
+45924,
+39777,
+45782,
+19260,
+3965,
+38454,
+28220,
+38581,
+49654,
+44497,
+36263,
+18075,
+19235,
+28923,
+19426,
+29816,
+44488,
+6300,
+37736,
+39434,
+2906,
+31631,
+56225,
+34772,
+13880,
+20350,
+46786,
+27184,
+58783,
+31076,
+57944,
+38182,
+16880,
+61679,
+30722,
+3237,
+21678,
+10618,
+64100,
+20584,
+40810,
+12758,
+64146,
+2217,
+28751,
+64045,
+61626,
+10678,
+51024,
+15893,
+41041,
+29397,
+58295,
+35797,
+4598,
+52442,
+36193,
+33023,
+26421,
+24609,
+18517,
+11995,
+16571,
+64229,
+32594,
+31353,
+58287,
+10850,
+2586,
+65462,
+9743,
+35628,
+62319,
+877,
+24708,
+62326,
+54656,
+766,
+19739,
+13125,
+30303,
+54675,
+58472,
+38735,
+17084,
+492,
+10237,
+27546,
+29685,
+50060,
+63247,
+35332,
+65423,
+22040,
+12415,
+64192,
+53006,
+49988,
+21494,
+42924,
+54962,
+62532,
+12371,
+33527,
+12513,
+39871,
+31351,
+32596,
+38215,
+9812,
+52468,
+999,
+31000,
+22786,
+18822,
+32531,
+64291,
+39060,
+553,
+59978,
+42863,
+61234,
+27986,
+14098,
+58942,
+11222,
+18232,
+15838,
+49612,
+45318,
+25891,
+47225,
+10724,
+35889,
+33898,
+53248,
+43687,
+6555,
+2850,
+56435,
+34775,
+39111,
+874,
+26066,
+38933,
+575,
+51088,
+61324,
+51830,
+39875,
+5492,
+39254,
+11509,
+5712,
+41138,
+21219,
+31111,
+51216,
+2779,
+64430,
+42843,
+9566,
+2152,
+8098,
+48472,
+33119,
+8298,
+8356,
+19677,
+18615,
+12988,
+22474,
+44537,
+62932,
+40043,
+46162,
+15592,
+34747,
+40674,
+6483,
+37894,
+23760,
+41415,
+12396,
+36515,
+45173,
+30692,
+20952,
+60668,
+25890,
+45379,
+49613,
+9130,
+65093,
+53407,
+7463,
+37028,
+45710,
+39665,
+1526,
+18499,
+56586,
+43768,
+54287,
+34963,
+12389,
+28686,
+49224,
+15875,
+45100,
+36698,
+57918,
+4724,
+38810,
+54128,
+64705,
+47399,
+56237,
+10481,
+670,
+24614,
+54297,
+36207,
+28992,
+64618,
+52759,
+59865,
+49486,
+10932,
+51571,
+32204,
+48674,
+49008,
+15160,
+61051,
+39557,
+52753,
+49578,
+42552,
+51164,
+23125,
+64719,
+4814,
+50947,
+26851,
+26187,
+47442,
+9603,
+62515,
+6661,
+21181,
+1186,
+37711,
+11959,
+17936,
+46988,
+11912,
+35941,
+48480,
+42172,
+1670,
+41496,
+13869,
+3937,
+1815,
+37858,
+1282,
+63648,
+28847,
+10290,
+8178,
+53679,
+9320,
+22374,
+8709,
+23650,
+59766,
+29044,
+38547,
+42479,
+22021,
+31097,
+10064,
+6011,
+5288,
+51474,
+63753,
+41830,
+16077,
+429,
+62947,
+5570,
+46151,
+48590,
+22080,
+9748,
+63770,
+23863,
+21908,
+10004,
+47806,
+22427,
+12043,
+54987,
+46546,
+17833,
+50541,
+52282,
+40414,
+42281,
+32754,
+48547,
+59686,
+21589,
+6324,
+63704,
+24005,
+34888,
+20801,
+5820,
+51656,
+20054,
+18752,
+27613,
+15411,
+65083,
+50926,
+28831,
+30626,
+37842,
+24378,
+34658,
+65434,
+16064,
+30691,
+45323,
+36516,
+9818,
+43125,
+11014,
+10193,
+33901,
+637,
+49616,
+29505,
+50010,
+42706,
+19398,
+10903,
+1161,
+283,
+20539,
+8785,
+18582,
+463,
+27261,
+44823,
+42953,
+64264,
+48587,
+40327,
+38914,
+28939,
+35992,
+20407,
+10820,
+27031,
+62282,
+50050,
+23239,
+45891,
+408,
+40541,
+63131,
+43656,
+4455,
+40138,
+29544,
+20393,
+54718,
+16255,
+25807,
+2482,
+30460,
+19655,
+59113,
+1756,
+29170,
+22126,
+41544,
+57253,
+35519,
+28107,
+20792,
+59748,
+16657,
+2814,
+51057,
+29119,
+53009,
+33447,
+31588,
+7891,
+31200,
+54542,
+39446,
+3887,
+36697,
+45299,
+15876,
+21416,
+16054,
+43344,
+63078,
+38422,
+9024,
+21227,
+15280,
+35658,
+16099,
+32049,
+25404,
+5310,
+37181,
+9898,
+1868,
+13431,
+41108,
+39533,
+10831,
+64576,
+8671,
+43055,
+1342,
+18691,
+25361,
+56977,
+7727,
+44922,
+16301,
+61554,
+32967,
+39586,
+32522,
+31368,
+42826,
+6168,
+18996,
+21021,
+15941,
+53053,
+39319,
+29791,
+60683,
+21338,
+23327,
+6117,
+53294,
+19600,
+42093,
+26875,
+28329,
+47524,
+21028,
+32930,
+9277,
+56334,
+9922,
+3191,
+63916,
+56112,
+57631,
+3168,
+54205,
+50552,
+56891,
+31856,
+43213,
+17804,
+2515,
+49831,
+35459,
+54325,
+38133,
+35588,
+63470,
+55736,
+40975,
+11723,
+61407,
+39174,
+33749,
+6745,
+61312,
+31761,
+5438,
+37764,
+28241,
+14827,
+57463,
+58386,
+11706,
+36607,
+6419,
+5442,
+18387,
+41719,
+59432,
+5058,
+65253,
+20380,
+56753,
+24028,
+60225,
+28358,
+18780,
+11841,
+15901,
+36116,
+31649,
+28624,
+4232,
+19354,
+48110,
+16342,
+40912,
+39048,
+27492,
+24089,
+14183,
+4737,
+38911,
+3452,
+62729,
+57413,
+35436,
+34679,
+65298,
+25610,
+53859,
+40366,
+65400,
+61201,
+1921,
+42999,
+28156,
+32098,
+37199,
+37831,
+51113,
+3777,
+10149,
+23717,
+28680,
+1436,
+20068,
+24728,
+34997,
+22268,
+60326,
+55437,
+37231,
+39263,
+3154,
+53033,
+35849,
+35965,
+47409,
+38227,
+22797,
+35029,
+50034,
+47617,
+58300,
+11585,
+43879,
+2201,
+31579,
+194,
+7731,
+58630,
+34174,
+33317,
+61960,
+49392,
+16300,
+45070,
+7728,
+48486,
+36621,
+27408,
+32314,
+49786,
+60449,
+23281,
+52823,
+12796,
+53841,
+12817,
+5436,
+31763,
+43290,
+14298,
+65483,
+51936,
+613,
+24017,
+17680,
+6091,
+29015,
+48501,
+23265,
+12567,
+1216,
+27519,
+31705,
+20942,
+11545,
+48606,
+50197,
+27730,
+48326,
+46867,
+39055,
+31120,
+60844,
+41667,
+33806,
+35562,
+42508,
+4768,
+38342,
+8501,
+18216,
+37959,
+31687,
+55712,
+64037,
+18145,
+65166,
+54379,
+11056,
+44615,
+59041,
+50019,
+48365,
+2146,
+9189,
+8580,
+59176,
+35933,
+23220,
+6021,
+44436,
+55372,
+20009,
+56672,
+33117,
+48474,
+59857,
+21512,
+39205,
+62718,
+11513,
+56004,
+63716,
+3854,
+32778,
+31209,
+21300,
+39989,
+26047,
+62159,
+25424,
+13593,
+54898,
+15532,
+37206,
+18015,
+40747,
+40533,
+44262,
+27859,
+58041,
+42952,
+45152,
+27262,
+6691,
+33504,
+11391,
+44189,
+54805,
+14976,
+29201,
+14380,
+24423,
+55108,
+60152,
+3105,
+16438,
+14066,
+11024,
+7271,
+17674,
+30491,
+37023,
+8556,
+25888,
+60670,
+1065,
+46172,
+33707,
+48866,
+61123,
+58068,
+46679,
+8078,
+6953,
+12149,
+40580,
+39621,
+40355,
+12832,
+40269,
+17094,
+54911,
+47971,
+30473,
+59497,
+11830,
+18475,
+24267,
+46464,
+21550,
+58495,
+42594,
+30747,
+24847,
+26632,
+26454,
+19864,
+40821,
+64297,
+40836,
+60303,
+32901,
+56947,
+23037,
+61837,
+30794,
+57734,
+65496,
+11470,
+4046,
+53182,
+40207,
+34375,
+21935,
+4354,
+47299,
+55386,
+41822,
+37429,
+20422,
+5790,
+28022,
+63936,
+5203,
+63183,
+45707,
+53828,
+30648,
+20347,
+59718,
+9114,
+35002,
+62913,
+63814,
+52560,
+400,
+56876,
+4746,
+49385,
+5199,
+8483,
+52599,
+34231,
+16973,
+49758,
+27486,
+37854,
+57060,
+34602,
+12719,
+47572,
+727,
+23137,
+57610,
+20995,
+11828,
+59499,
+14841,
+55095,
+54158,
+9384,
+63464,
+23755,
+39465,
+24885,
+36725,
+59005,
+13859,
+2805,
+4462,
+12615,
+60390,
+14736,
+2229,
+16253,
+54720,
+18609,
+8886,
+30111,
+48059,
+18728,
+38419,
+51402,
+62844,
+6209,
+22406,
+28761,
+12212,
+2507,
+39642,
+21942,
+36585,
+33615,
+14539,
+38282,
+17697,
+41805,
+25458,
+50901,
+37626,
+41932,
+9083,
+17352,
+10859,
+5390,
+13884,
+52407,
+13958,
+31138,
+59847,
+18767,
+22353,
+53462,
+7421,
+16840,
+17291,
+10253,
+60508,
+23867,
+33261,
+665,
+35328,
+9996,
+19668,
+31525,
+8414,
+58591,
+5729,
+32861,
+30141,
+14623,
+39299,
+54320,
+62073,
+53691,
+49974,
+46389,
+24375,
+41580,
+63478,
+35954,
+29052,
+40632,
+21830,
+37238,
+17285,
+62213,
+65183,
+59040,
+44866,
+11057,
+27234,
+41975,
+44206,
+13388,
+47897,
+39775,
+45926,
+1027,
+8225,
+11609,
+24178,
+50736,
+11191,
+13920,
+31529,
+56357,
+59362,
+33831,
+20645,
+53993,
+17420,
+25492,
+19377,
+30605,
+62568,
+30186,
+50293,
+34844,
+20216,
+4870,
+65390,
+19303,
+28631,
+57941,
+38072,
+33483,
+37418,
+36275,
+42892,
+16310,
+32434,
+7665,
+41746,
+57954,
+60758,
+22153,
+52714,
+62415,
+49159,
+40679,
+8496,
+52264,
+14494,
+62997,
+48089,
+50892,
+36945,
+16599,
+64247,
+58725,
+32553,
+40637,
+1296,
+11067,
+42664,
+49191,
+15446,
+2326,
+38360,
+46706,
+7926,
+20827,
+53710,
+63349,
+43992,
+62931,
+45336,
+22475,
+49457,
+17499,
+50457,
+35575,
+54870,
+18416,
+11527,
+15379,
+2954,
+62056,
+18212,
+28151,
+36748,
+62986,
+3265,
+41679,
+25840,
+63925,
+46540,
+20477,
+49597,
+51702,
+19249,
+51828,
+61326,
+18012,
+20492,
+32608,
+63689,
+45774,
+51898,
+26532,
+5356,
+34574,
+21369,
+41197,
+15255,
+36262,
+45499,
+49655,
+54664,
+10315,
+109,
+25620,
+26335,
+11789,
+6299,
+45492,
+29817,
+7120,
+62234,
+19154,
+32258,
+35117,
+36849,
+16315,
+15365,
+9414,
+49688,
+62391,
+25674,
+33243,
+57544,
+35287,
+4371,
+4573,
+59076,
+18536,
+49540,
+423,
+50854,
+43795,
+13985,
+54737,
+17995,
+24754,
+47239,
+17638,
+25764,
+15970,
+46428,
+45916,
+46722,
+7668,
+40576,
+25541,
+46738,
+64642,
+954,
+550,
+47102,
+64347,
+30923,
+60386,
+3526,
+25532,
+56041,
+35665,
+55371,
+44855,
+6022,
+44211,
+46951,
+24569,
+60398,
+61367,
+8144,
+29032,
+58915,
+51710,
+3722,
+30085,
+16541,
+12049,
+35478,
+27432,
+27890,
+49273,
+6352,
+30598,
+64088,
+57889,
+8597,
+9003,
+58616,
+7265,
+24291,
+50875,
+27564,
+13786,
+49742,
+40861,
+28824,
+36488,
+24257,
+61807,
+56276,
+4297,
+50244,
+31,
+2049,
+13524,
+23008,
+45614,
+60916,
+57126,
+64034,
+24741,
+22704,
+42989,
+54231,
+1230,
+34553,
+59812,
+3516,
+29406,
+12040,
+6331,
+4110,
+1372,
+50359,
+96,
+24324,
+48559,
+12246,
+8852,
+48097,
+28055,
+24540,
+31959,
+8107,
+2046,
+63553,
+54169,
+33372,
+34206,
+51893,
+13730,
+58875,
+20279,
+64352,
+8802,
+59396,
+50029,
+12253,
+32470,
+46703,
+49868,
+733,
+29198,
+34757,
+14627,
+1475,
+15142,
+39376,
+51426,
+40071,
+13479,
+23887,
+63741,
+3360,
+26208,
+47459,
+63457,
+24999,
+61535,
+52827,
+33592,
+18283,
+14973,
+4137,
+587,
+38008,
+25044,
+60888,
+32699,
+24398,
+20184,
+34427,
+51933,
+3783,
+4175,
+61285,
+29108,
+56512,
+20445,
+50258,
+59125,
+59292,
+22284,
+50372,
+63148,
+21929,
+59062,
+10033,
+24370,
+719,
+53978,
+21497,
+11814,
+22871,
+16348,
+56242,
+28186,
+51548,
+63603,
+27633,
+64402,
+23985,
+60945,
+55160,
+15002,
+41316,
+58251,
+25748,
+59488,
+4498,
+33476,
+21970,
+35384,
+65275,
+2512,
+25689,
+27050,
+18278,
+16422,
+9303,
+43906,
+38290,
+7869,
+35834,
+49477,
+27858,
+44827,
+40534,
+8874,
+55185,
+50849,
+6797,
+33078,
+54879,
+53540,
+48968,
+22560,
+3513,
+47354,
+8814,
+4670,
+1376,
+30366,
+29829,
+30634,
+26096,
+22811,
+27423,
+41528,
+16115,
+34327,
+20698,
+25549,
+26480,
+34419,
+42105,
+55961,
+64525,
+9206,
+53551,
+43922,
+52242,
+26272,
+62385,
+27601,
+59538,
+32398,
+54896,
+13595,
+49308,
+14891,
+7522,
+13199,
+29160,
+50950,
+46860,
+46950,
+44434,
+6023,
+33085,
+36743,
+13387,
+44611,
+41976,
+23053,
+55034,
+35378,
+17788,
+4853,
+20483,
+42211,
+35339,
+50170,
+56125,
+40287,
+16504,
+56854,
+11105,
+54804,
+44818,
+11392,
+25254,
+15501,
+17922,
+40750,
+1955,
+2983,
+10038,
+33752,
+7257,
+46639,
+7017,
+34301,
+813,
+22947,
+48257,
+46494,
+63442,
+3788,
+30962,
+21036,
+24341,
+11920,
+49441,
+46449,
+880,
+37642,
+62002,
+22607,
+26766,
+57064,
+48883,
+17254,
+60213,
+53780,
+58865,
+28216,
+5703,
+1667,
+20000,
+6171,
+8294,
+3027,
+52054,
+47343,
+13978,
+12167,
+19870,
+28289,
+56070,
+35165,
+57351,
+59789,
+59973,
+49893,
+33090,
+18392,
+40147,
+747,
+36537,
+38972,
+33295,
+13419,
+37277,
+40948,
+56377,
+49660,
+20439,
+49484,
+59867,
+13261,
+9751,
+2212,
+46407,
+7391,
+38027,
+4518,
+11400,
+3496,
+7741,
+21968,
+33478,
+11116,
+8608,
+33192,
+51952,
+29098,
+31622,
+34586,
+57231,
+50442,
+17017,
+27586,
+43998,
+49207,
+15718,
+19901,
+41924,
+13754,
+42849,
+63279,
+47593,
+39134,
+19256,
+58694,
+19187,
+6207,
+62846,
+55476,
+5128,
+32712,
+34836,
+23114,
+40256,
+58401,
+55177,
+22783,
+22095,
+48172,
+5553,
+65479,
+25296,
+17667,
+9471,
+7093,
+31975,
+57227,
+41426,
+2548,
+39750,
+1132,
+1852,
+60445,
+50219,
+42587,
+52490,
+43550,
+16035,
+9928,
+7024,
+32401,
+42381,
+50939,
+31564,
+15548,
+20127,
+22614,
+11558,
+21240,
+53724,
+61693,
+9227,
+48614,
+27270,
+24672,
+15601,
+19815,
+12863,
+59022,
+28725,
+27837,
+5597,
+5679,
+20689,
+26333,
+25622,
+61507,
+59308,
+15354,
+35802,
+15983,
+47895,
+13390,
+11879,
+25118,
+18948,
+33041,
+36903,
+48193,
+62855,
+15108,
+5744,
+23535,
+25978,
+12786,
+9042,
+46340,
+776,
+2194,
+49206,
+44095,
+27587,
+51268,
+57192,
+30531,
+62930,
+44539,
+63350,
+56394,
+35594,
+19072,
+20762,
+7043,
+47386,
+8584,
+12658,
+24877,
+62822,
+54261,
+15416,
+28882,
+54372,
+6523,
+45899,
+43511,
+45682,
+57520,
+12420,
+29084,
+34224,
+53614,
+35661,
+48596,
+51328,
+60810,
+8363,
+37369,
+40991,
+60577,
+31177,
+34325,
+16117,
+17416,
+18642,
+59133,
+18701,
+36456,
+13629,
+929,
+5850,
+20342,
+59214,
+18443,
+23769,
+29483,
+39409,
+50460,
+65012,
+54907,
+8997,
+49507,
+22231,
+22054,
+6993,
+3871,
+13017,
+9531,
+1823,
+29931,
+39984,
+37753,
+58492,
+18153,
+35246,
+31454,
+52241,
+44228,
+53552,
+4108,
+6333,
+28817,
+19361,
+56883,
+8302,
+11675,
+2858,
+65240,
+35205,
+53049,
+12445,
+6957,
+38289,
+44268,
+9304,
+17771,
+55113,
+4459,
+8127,
+51613,
+55333,
+59089,
+10197,
+6511,
+52022,
+7222,
+13974,
+41260,
+47156,
+28480,
+9394,
+37656,
+1898,
+18292,
+1692,
+22337,
+5015,
+52590,
+36627,
+2200,
+44933,
+11586,
+45770,
+48857,
+10229,
+48196,
+481,
+34945,
+29060,
+55144,
+14878,
+17188,
+54096,
+31893,
+40882,
+42637,
+25324,
+384,
+35408,
+62299,
+20065,
+54920,
+35208,
+64343,
+51255,
+60568,
+53916,
+39295,
+49529,
+59892,
+34639,
+15199,
+16700,
+16243,
+23599,
+39637,
+19301,
+65392,
+57801,
+47169,
+36393,
+2830,
+10048,
+151,
+31476,
+48234,
+6785,
+17549,
+58342,
+55927,
+25707,
+6234,
+6602,
+36824,
+39551,
+64055,
+59564,
+7189,
+24092,
+47370,
+10438,
+37770,
+30074,
+22605,
+62004,
+11932,
+55482,
+26251,
+15152,
+47680,
+19494,
+31645,
+18509,
+6668,
+12108,
+48668,
+9586,
+2961,
+2988,
+53676,
+2029,
+53044,
+29564,
+13984,
+44464,
+50855,
+5157,
+217,
+55016,
+25462,
+24828,
+65311,
+36993,
+61614,
+5131,
+32297,
+4528,
+39274,
+39712,
+7145,
+24919,
+50161,
+33151,
+39477,
+22716,
+49417,
+63932,
+34935,
+61698,
+28603,
+54286,
+45306,
+56587,
+28965,
+28172,
+33971,
+49991,
+42970,
+50799,
+63310,
+18402,
+6226,
+39921,
+3716,
+8235,
+5229,
+1417,
+15484,
+54187,
+7075,
+45838,
+49731,
+46700,
+15585,
+45998,
+17836,
+39745,
+28888,
+48383,
+7631,
+13749,
+46344,
+61710,
+23965,
+62035,
+1536,
+23069,
+45661,
+42957,
+64972,
+64957,
+11320,
+61862,
+27097,
+16222,
+28756,
+20006,
+47728,
+59646,
+26672,
+11844,
+15565,
+6809,
+36311,
+49845,
+26022,
+4033,
+1057,
+48875,
+50323,
+40806,
+24904,
+20916,
+28300,
+62340,
+50015,
+54302,
+6144,
+63277,
+42851,
+59838,
+62260,
+38551,
+4681,
+47663,
+24576,
+52429,
+32767,
+3,
+16385,
+56175,
+6554,
+45372,
+53249,
+31044,
+8937,
+15729,
+30428,
+63423,
+13493,
+58452,
+34407,
+12193,
+35618,
+8025,
+11343,
+42897,
+1130,
+39752,
+62465,
+17607,
+38386,
+50275,
+7761,
+29865,
+34367,
+31612,
+51387,
+34569,
+55079,
+58105,
+53085,
+4454,
+45134,
+63132,
+7607,
+40462,
+8331,
+61204,
+32240,
+52120,
+30752,
+51247,
+52526,
+943,
+7846,
+452,
+14613,
+65103,
+29364,
+37569,
+24986,
+23722,
+59615,
+33738,
+52201,
+2247,
+35714,
+13035,
+41673,
+20327,
+55534,
+36334,
+51159,
+2964,
+57426,
+53069,
+19220,
+30439,
+34606,
+65235,
+59877,
+36736,
+290,
+51800,
+33051,
+35139,
+11290,
+52484,
+9938,
+2388,
+21234,
+6476,
+64769,
+45800,
+1751,
+31161,
+20786,
+61184,
+34682,
+33360,
+42365,
+61137,
+25894,
+20863,
+28953,
+55762,
+8577,
+48554,
+51093,
+52515,
+12896,
+27848,
+56619,
+29995,
+42538,
+51623,
+24976,
+594,
+42187,
+17308,
+7903,
+57321,
+42618,
+2629,
+29231,
+55568,
+5675,
+46475,
+57795,
+21429,
+32060,
+21260,
+30154,
+6571,
+28651,
+24853,
+63679,
+58162,
+12808,
+30705,
+62424,
+9920,
+56336,
+9454,
+33882,
+23846,
+7369,
+16034,
+44052,
+52491,
+7773,
+22552,
+15783,
+48369,
+51451,
+27393,
+18286,
+41996,
+4475,
+28538,
+42884,
+61149,
+38394,
+39965,
+18467,
+35321,
+34688,
+64992,
+15301,
+38757,
+33571,
+34829,
+8060,
+12871,
+54179,
+9863,
+13030,
+25907,
+16193,
+33846,
+7688,
+23057,
+20309,
+34441,
+29196,
+735,
+45681,
+43974,
+45900,
+10184,
+63273,
+60441,
+16620,
+51351,
+30203,
+116,
+34730,
+19971,
+14272,
+19309,
+113,
+20122,
+53123,
+13686,
+52806,
+4516,
+38029,
+25735,
+32660,
+10365,
+30190,
+61902,
+7341,
+64690,
+43305,
+21601,
+58545,
+56501,
+50755,
+61113,
+39015,
+32106,
+31754,
+49178,
+38699,
+53130,
+64043,
+28753,
+31288,
+11496,
+60744,
+8959,
+41203,
+36388,
+59483,
+53195,
+62203,
+9105,
+23514,
+31029,
+16946,
+20323,
+55790,
+38176,
+41697,
+23465,
+57961,
+38086,
+19643,
+62472,
+37796,
+63570,
+59571,
+49574,
+34535,
+42753,
+21466,
+17366,
+50500,
+30734,
+46652,
+55831,
+60318,
+19431,
+41852,
+57588,
+53491,
+37174,
+29174,
+39139,
+35755,
+38347,
+741,
+10335,
+12993,
+43230,
+47125,
+19152,
+62236,
+14439,
+62420,
+48018,
+19915,
+4269,
+4805,
+56197,
+36366,
+20417,
+23994,
+64918,
+29376,
+47789,
+41420,
+32120,
+49799,
+43262,
+32693,
+11437,
+60513,
+60306,
+64122,
+12098,
+14572,
+35533,
+9184,
+2270,
+3868,
+9165,
+32841,
+41909,
+23118,
+16797,
+12950,
+4778,
+20969,
+12824,
+24647,
+7321,
+57187,
+27208,
+25169,
+139,
+15449,
+64363,
+35591,
+63890,
+8004,
+4160,
+13121,
+3592,
+38579,
+28222,
+35253,
+36283,
+7141,
+31338,
+597,
+31016,
+47933,
+24979,
+38077,
+62662,
+22736,
+61445,
+1619,
+9427,
+47833,
+33956,
+65345,
+62793,
+52875,
+6725,
+11450,
+63077,
+45096,
+16055,
+16822,
+39846,
+38489,
+65475,
+56943,
+7460,
+58098,
+49352,
+37965,
+29324,
+51007,
+47531,
+15296,
+4341,
+32528,
+10673,
+41439,
+5367,
+27654,
+7473,
+8340,
+16399,
+42652,
+41364,
+59744,
+14473,
+10302,
+2380,
+64437,
+1790,
+14455,
+50201,
+39242,
+33597,
+30261,
+16029,
+21600,
+43484,
+64691,
+28465,
+56391,
+60500,
+64588,
+6292,
+46709,
+32907,
+60840,
+27229,
+51297,
+40090,
+41959,
+14297,
+44907,
+31764,
+58550,
+48680,
+61926,
+12473,
+39526,
+321,
+29513,
+27418,
+27670,
+24583,
+3224,
+47716,
+5102,
+9235,
+6962,
+34779,
+28705,
+59924,
+30539,
+20587,
+5212,
+4376,
+23883,
+37662,
+60885,
+32692,
+43403,
+49800,
+13624,
+32339,
+29290,
+5080,
+26546,
+49716,
+6244,
+21231,
+15166,
+50207,
+32917,
+58524,
+64108,
+15192,
+26931,
+55797,
+61386,
+5165,
+4327,
+14553,
+23088,
+63412,
+18360,
+37697,
+29602,
+42319,
+63483,
+51524,
+38761,
+47124,
+43423,
+12994,
+6648,
+56598,
+49810,
+59166,
+47914,
+38296,
+23692,
+22434,
+36291,
+65352,
+13892,
+2487,
+45545,
+64207,
+17803,
+45031,
+31857,
+24150,
+28003,
+3261,
+26260,
+54757,
+30833,
+47210,
+29272,
+47464,
+54510,
+63844,
+21653,
+60523,
+8015,
+53119,
+28270,
+28081,
+5315,
+28319,
+33031,
+36065,
+40307,
+59927,
+13064,
+15767,
+18027,
+46769,
+20884,
+12076,
+23547,
+19263,
+35930,
+61870,
+55366,
+31558,
+62612,
+48571,
+32304,
+17322,
+25954,
+5248,
+47309,
+24843,
+23418,
+4708,
+3202,
+53957,
+56216,
+20302,
+56829,
+62324,
+24710,
+42597,
+15606,
+7400,
+17180,
+10468,
+2480,
+25809,
+12273,
+4043,
+14084,
+38146,
+28587,
+12080,
+35132,
+31349,
+39873,
+51832,
+41239,
+49381,
+14938,
+33342,
+38730,
+9734,
+48425,
+4555,
+50995,
+37405,
+59327,
+20858,
+36777,
+35585,
+42696,
+64066,
+11013,
+45170,
+9819,
+50008,
+29507,
+34473,
+38351,
+6756,
+51096,
+3119,
+57829,
+19066,
+5638,
+22513,
+34126,
+17730,
+20330,
+53495,
+59208,
+48377,
+61245,
+31036,
+41844,
+22316,
+29247,
+19650,
+55156,
+48535,
+56001,
+59950,
+47740,
+3297,
+37340,
+10077,
+33862,
+23653,
+10499,
+40403,
+34072,
+53355,
+17503,
+27202,
+36941,
+2011,
+39903,
+39945,
+18975,
+31502,
+10721,
+34102,
+40934,
+32290,
+64440,
+13814,
+65149,
+61135,
+42367,
+35840,
+61016,
+54620,
+59144,
+25008,
+52548,
+20200,
+21001,
+16081,
+62665,
+18103,
+57983,
+55412,
+1341,
+45076,
+8672,
+50342,
+14317,
+61716,
+16248,
+58633,
+56063,
+17359,
+52978,
+32853,
+26829,
+8289,
+58842,
+41855,
+8821,
+17481,
+24777,
+55421,
+47636,
+54653,
+57860,
+14302,
+35883,
+55546,
+2015,
+52047,
+52726,
+42091,
+19602,
+59898,
+22424,
+31412,
+29929,
+1825,
+29970,
+59201,
+18850,
+59155,
+18406,
+65214,
+36026,
+11787,
+26337,
+53303,
+22861,
+5955,
+24968,
+23495,
+53201,
+2801,
+51902,
+7966,
+41230,
+17120,
+28155,
+44964,
+1922,
+52008,
+38675,
+45741,
+54917,
+1439,
+59758,
+23083,
+54230,
+44386,
+22705,
+40476,
+57763,
+21990,
+49755,
+64180,
+7299,
+59909,
+11578,
+9323,
+16568,
+21285,
+62025,
+26227,
+60573,
+33027,
+24751,
+50798,
+43762,
+49992,
+39292,
+30974,
+11475,
+24723,
+7659,
+50420,
+2546,
+41428,
+64611,
+47967,
+64971,
+43731,
+45662,
+6527,
+64263,
+45151,
+44824,
+58042,
+4155,
+206,
+16781,
+4285,
+29222,
+34715,
+28033,
+27762,
+23935,
+17527,
+52889,
+11702,
+29,
+50246,
+29992,
+37982,
+41451,
+35462,
+21721,
+3534,
+21377,
+38772,
+5180,
+33383,
+3568,
+54961,
+45409,
+21495,
+53980,
+11542,
+48119,
+46897,
+49181,
+7843,
+62973,
+61907,
+37799,
+62197,
+34114,
+41713,
+29665,
+55652,
+23175,
+6044,
+36190,
+11835,
+55909,
+19287,
+45970,
+25568,
+8972,
+33058,
+1129,
+43673,
+11344,
+1838,
+58660,
+16309,
+44575,
+36276,
+22818,
+56089,
+57678,
+33787,
+8165,
+61148,
+43538,
+28539,
+51744,
+57018,
+24260,
+47277,
+40316,
+9738,
+4153,
+58044,
+48244,
+55286,
+49726,
+34353,
+50988,
+42000,
+39561,
+26183,
+48941,
+34130,
+61233,
+45388,
+59979,
+3315,
+48798,
+42519,
+54553,
+27414,
+28898,
+53208,
+63789,
+61524,
+59837,
+43700,
+63278,
+44089,
+13755,
+31850,
+29073,
+13190,
+9565,
+45348,
+64431,
+2492,
+27893,
+3537,
+26138,
+54644,
+2835,
+40511,
+40795,
+21150,
+22549,
+25062,
+40707,
+228,
+4580,
+6167,
+45063,
+31369,
+14130,
+4931,
+26059,
+21762,
+16121,
+6436,
+46051,
+60365,
+14500,
+349,
+32395,
+28503,
+12695,
+58688,
+56341,
+14484,
+48992,
+29026,
+7822,
+8035,
+2782,
+49454,
+2874,
+22181,
+4093,
+31777,
+15186,
+13508,
+37502,
+37777,
+18624,
+59725,
+55602,
+42098,
+26685,
+27067,
+58548,
+31766,
+9097,
+57607,
+39510,
+39226,
+31255,
+37460,
+61390,
+55121,
+29683,
+27548,
+55537,
+46727,
+31935,
+24169,
+29917,
+57514,
+51429,
+12340,
+3336,
+29637,
+38647,
+60561,
+2662,
+56383,
+56910,
+12727,
+8327,
+53962,
+37005,
+16016,
+52154,
+10359,
+21465,
+43443,
+34536,
+20142,
+46716,
+14860,
+65097,
+16124,
+9544,
+31949,
+8048,
+5782,
+59577,
+27309,
+20598,
+30509,
+55019,
+10713,
+50056,
+31021,
+63643,
+64534,
+33816,
+59850,
+42290,
+9330,
+8640,
+39440,
+21295,
+38115,
+32362,
+12769,
+15618,
+32352,
+29260,
+22647,
+9449,
+52174,
+29140,
+13000,
+48661,
+52050,
+60518,
+8869,
+31277,
+11653,
+31791,
+19397,
+45162,
+50011,
+56576,
+24336,
+25018,
+16103,
+52910,
+59439,
+59841,
+64065,
+43128,
+35586,
+38135,
+29891,
+27216,
+59051,
+37110,
+37814,
+57285,
+11550,
+12625,
+6100,
+4706,
+23420,
+40452,
+31309,
+15099,
+47626,
+11663,
+12233,
+22527,
+59583,
+30342,
+2703,
+56643,
+7879,
+47848,
+64319,
+38020,
+53722,
+21242,
+49190,
+44549,
+11068,
+10463,
+35281,
+26075,
+39191,
+14397,
+15946,
+16226,
+54702,
+29757,
+41363,
+43320,
+16400,
+42062,
+55959,
+42107,
+49409,
+40062,
+11838,
+50957,
+11482,
+26169,
+17263,
+61776,
+62174,
+25323,
+43864,
+40883,
+33999,
+25971,
+41407,
+38089,
+51486,
+49338,
+8534,
+35768,
+50882,
+18968,
+50889,
+1576,
+24354,
+52401,
+5225,
+39416,
+2628,
+43576,
+57322,
+5993,
+18490,
+60934,
+18557,
+506,
+18098,
+4788,
+45777,
+11716,
+25143,
+15559,
+24232,
+52297,
+63048,
+19994,
+52093,
+55188,
+20,
+15605,
+43159,
+24711,
+30746,
+44773,
+58496,
+19057,
+64723,
+21363,
+17281,
+52489,
+44054,
+50220,
+53176,
+17854,
+51858,
+50354,
+3994,
+64849,
+42166,
+63202,
+35072,
+61130,
+40058,
+41860,
+11447,
+31012,
+52453,
+54257,
+18647,
+41053,
+25356,
+38767,
+12607,
+5329,
+35450,
+48998,
+38151,
+29370,
+6791,
+7344,
+26885,
+5176,
+33624,
+61100,
+51163,
+45270,
+49579,
+10355,
+14993,
+51608,
+17124,
+8030,
+11034,
+47620,
+2397,
+28834,
+14338,
+46392,
+51622,
+43584,
+29996,
+28117,
+29264,
+57326,
+28874,
+56398,
+2504,
+52012,
+21144,
+19924,
+29926,
+64281,
+11594,
+131,
+32273,
+47845,
+13141,
+54552,
+42859,
+48799,
+29913,
+63288,
+29825,
+35793,
+1409,
+52663,
+10346,
+3643,
+4767,
+44879,
+35563,
+58036,
+35190,
+37957,
+18218,
+2296,
+8589,
+20776,
+3701,
+33336,
+49514,
+18952,
+31682,
+967,
+52065,
+39763,
+32814,
+51444,
+18878,
+37416,
+33485,
+57363,
+23560,
+21333,
+10504,
+59630,
+57848,
+22020,
+45229,
+38548,
+3544,
+32409,
+8136,
+53352,
+51031,
+27752,
+50106,
+36006,
+64000,
+21804,
+19877,
+33963,
+39992,
+40351,
+34876,
+54395,
+33896,
+35891,
+57418,
+3206,
+23645,
+40408,
+5778,
+22546,
+11308,
+37281,
+41478,
+50983,
+22297,
+2126,
+10323,
+30681,
+8466,
+30877,
+105,
+6802,
+16773,
+15555,
+36894,
+55445,
+11425,
+53338,
+59225,
+16419,
+18333,
+35093,
+156,
+53015,
+6282,
+46533,
+9123,
+32475,
+38539,
+30700,
+21461,
+25282,
+10748,
+23627,
+22149,
+48741,
+360,
+6355,
+48267,
+2001,
+14372,
+20269,
+819,
+1040,
+38467,
+9039,
+5460,
+52433,
+50643,
+63857,
+64367,
+898,
+64809,
+20825,
+7928,
+26029,
+51849,
+12733,
+16043,
+39824,
+23489,
+45849,
+65285,
+57966,
+18502,
+32031,
+54148,
+25455,
+4164,
+15390,
+35582,
+50938,
+44047,
+32402,
+60876,
+40603,
+54351,
+9417,
+34370,
+49302,
+9379,
+26642,
+39213,
+7754,
+3618,
+35839,
+43070,
+61136,
+43598,
+33361,
+9884,
+22629,
+59494,
+3331,
+2853,
+58672,
+9463,
+6822,
+11950,
+48434,
+6872,
+48851,
+33396,
+5684,
+11506,
+992,
+65472,
+64514,
+58933,
+28715,
+31124,
+16789,
+45994,
+53267,
+47606,
+18741,
+38777,
+8720,
+32390,
+61111,
+50757,
+59414,
+12237,
+8489,
+58305,
+53375,
+2674,
+65489,
+58566,
+7589,
+42164,
+64851,
+61424,
+63482,
+43235,
+29603,
+20680,
+59517,
+51982,
+50834,
+32190,
+63779,
+36369,
+35631,
+62803,
+15492,
+2446,
+64922,
+41522,
+5653,
+19370,
+11274,
+50123,
+54185,
+15486,
+20398,
+19647,
+33520,
+54695,
+36974,
+63053,
+35079,
+9329,
+42730,
+59851,
+54033,
+5009,
+58775,
+53809,
+31744,
+3971,
+32753,
+45199,
+40415,
+29528,
+30620,
+47975,
+37399,
+29568,
+1865,
+17035,
+20640,
+52971,
+47293,
+6374,
+51125,
+42040,
+12338,
+51431,
+40666,
+2233,
+58644,
+27568,
+51755,
+63417,
+7331,
+5087,
+20623,
+12715,
+29136,
+36239,
+36229,
+11891,
+28267,
+1348,
+28470,
+65416,
+3824,
+63362,
+33493,
+543,
+50238,
+64814,
+62361,
+35649,
+8132,
+60550,
+51592,
+32746,
+51821,
+49898,
+47798,
+11414,
+26744,
+49642,
+24498,
+19588,
+17726,
+12142,
+63676,
+29845,
+39682,
+26194,
+3311,
+17434,
+51021,
+51505,
+61465,
+58020,
+2085,
+8955,
+35338,
+44198,
+20484,
+59421,
+9293,
+37353,
+10663,
+64905,
+62526,
+19096,
+10341,
+25520,
+5083,
+53021,
+14936,
+49383,
+4748,
+49171,
+52771,
+52867,
+63774,
+13949,
+35344,
+28617,
+17307,
+43580,
+595,
+31340,
+41986,
+52951,
+65262,
+55253,
+65060,
+45574,
+33216,
+63200,
+42168,
+47586,
+19998,
+1669,
+45249,
+48481,
+61703,
+47585,
+42176,
+63201,
+42579,
+64850,
+42323,
+7590,
+57552,
+34403,
+52655,
+23680,
+56718,
+62305,
+9149,
+6612,
+53160,
+29481,
+23771,
+2099,
+5400,
+13470,
+25776,
+6408,
+10871,
+55224,
+40110,
+39167,
+32557,
+32536,
+40767,
+19139,
+56269,
+5397,
+35999,
+39092,
+30482,
+11299,
+30583,
+65523,
+15125,
+12137,
+36765,
+3575,
+16147,
+10265,
+52700,
+47234,
+1573,
+48092,
+40693,
+22369,
+60830,
+22854,
+25086,
+18515,
+24611,
+25066,
+14344,
+32886,
+15210,
+48459,
+49408,
+42648,
+55960,
+44233,
+34420,
+52966,
+61977,
+11731,
+17860,
+26684,
+42791,
+55603,
+29619,
+59283,
+26874,
+45049,
+19601,
+43027,
+52727,
+34834,
+32714,
+2451,
+27611,
+18754,
+38205,
+48640,
+7941,
+53911,
+10019,
+29388,
+47406,
+34548,
+25141,
+11718,
+12170,
+23839,
+22084,
+7577,
+48250,
+3326,
+62859,
+32439,
+52271,
+32520,
+39588,
+55958,
+42650,
+16401,
+924,
+17572,
+49233,
+18553,
+55291,
+56659,
+56531,
+5947,
+34874,
+40353,
+39623,
+25712,
+23144,
+19689,
+39686,
+18577,
+7970,
+40568,
+22530,
+12337,
+42267,
+51126,
+806,
+33541,
+52617,
+488,
+11929,
+7983,
+31136,
+13960,
+34044,
+61174,
+47163,
+25359,
+18693,
+29432,
+56431,
+22077,
+34509,
+30107,
+64060,
+50397,
+25079,
+49841,
+27005,
+2867,
+56329,
+11988,
+38102,
+49143,
+14981,
+52253,
+7540,
+17678,
+24019,
+855,
+8040,
+59792,
+21531,
+39560,
+42869,
+50989,
+1303,
+4474,
+43541,
+18287,
+1094,
+49857,
+38682,
+53460,
+22355,
+46275,
+61574,
+52950,
+42184,
+31341,
+46357,
+36202,
+64374,
+65256,
+57814,
+52103,
+8173,
+23052,
+44205,
+44612,
+27235,
+60083,
+64889,
+52775,
+12450,
+61085,
+16492,
+7082,
+3406,
+14755,
+645,
+11262,
+24469,
+52369,
+14296,
+43292,
+40091,
+62388,
+8724,
+14821,
+1270,
+15470,
+58290,
+6088,
+39405,
+9855,
+61289,
+33163,
+12429,
+15887,
+8189,
+50118,
+1561,
+15651,
+39180,
+49293,
+21692,
+25901,
+35158,
+19329,
+36560,
+9082,
+44664,
+37627,
+28936,
+47501,
+49320,
+31747,
+57382,
+13753,
+44091,
+19902,
+14631,
+19534,
+46828,
+46099,
+16027,
+30263,
+46501,
+3382,
+3798,
+30846,
+18453,
+54788,
+23117,
+43389,
+32842,
+54102,
+63102,
+51779,
+55429,
+24810,
+48115,
+40047,
+7622,
+17015,
+50444,
+49588,
+34253,
+32957,
+17466,
+62330,
+23080,
+25130,
+52791,
+18266,
+56827,
+20304,
+5772,
+31674,
+22446,
+55039,
+15853,
+10414,
+35856,
+35788,
+4590,
+10379,
+17004,
+56300,
+58577,
+16171,
+27173,
+39315,
+40169,
+14673,
+53448,
+11316,
+26964,
+37583,
+28656,
+12737,
+32255,
+11446,
+42574,
+40059,
+12881,
+23378,
+8820,
+43041,
+58843,
+57587,
+43434,
+19432,
+11781,
+10909,
+51920,
+57755,
+27240,
+22315,
+43104,
+31037,
+36017,
+24203,
+177,
+9942,
+1662,
+18602,
+24126,
+7767,
+46918,
+59142,
+54622,
+16076,
+45221,
+63754,
+51011,
+57651,
+47560,
+2739,
+33798,
+37428,
+44747,
+55387,
+20581,
+36858,
+9574,
+1014,
+40550,
+21900,
+5923,
+34263,
+1732,
+3102,
+38377,
+2472,
+13119,
+4162,
+25457,
+44668,
+17698,
+51319,
+16338,
+17381,
+29904,
+2736,
+3473,
+30883,
+61330,
+56298,
+17006,
+61118,
+55703,
+53501,
+60539,
+8762,
+47873,
+7663,
+32436,
+30150,
+16138,
+2072,
+20835,
+26073,
+35283,
+10761,
+27642,
+20210,
+24123,
+40982,
+57117,
+31271,
+29058,
+34947,
+38806,
+47093,
+21592,
+36055,
+23385,
+63439,
+2845,
+32355,
+49968,
+47011,
+58558,
+6998,
+6565,
+47422,
+58595,
+11643,
+62842,
+51404,
+27339,
+41646,
+56861,
+5871,
+28227,
+57953,
+44571,
+7666,
+46724,
+26753,
+7318,
+4514,
+52808,
+57212,
+11866,
+28437,
+23920,
+201,
+46396,
+9270,
+25499,
+12782,
+15961,
+31004,
+14476,
+45986,
+54566,
+56738,
+45716,
+35471,
+31515,
+27053,
+59431,
+45002,
+18388,
+5632,
+54334,
+9274,
+29664,
+42911,
+34115,
+47567,
+39836,
+4915,
+13559,
+25486,
+56173,
+16387,
+8739,
+17645,
+17713,
+6827,
+61094,
+53366,
+23464,
+43454,
+38177,
+40817,
+24642,
+17830,
+31654,
+21984,
+58169,
+49881,
+35342,
+13951,
+26461,
+24869,
+63306,
+49286,
+31366,
+32524,
+25839,
+44520,
+3266,
+61546,
+14406,
+1766,
+20326,
+43630,
+13036,
+5028,
+20891,
+32277,
+33805,
+44882,
+60845,
+39706,
+38458,
+9704,
+5221,
+61911,
+25348,
+48446,
+3019,
+41515,
+59623,
+61160,
+22271,
+27876,
+36732,
+52057,
+21200,
+1797,
+32405,
+56860,
+41751,
+27340,
+26409,
+6783,
+48236,
+16014,
+37007,
+29357,
+46610,
+11591,
+51794,
+8552,
+40658,
+2717,
+24282,
+39178,
+15653,
+15370,
+6469,
+7831,
+28527,
+17867,
+30169,
+50708,
+8076,
+46681,
+63164,
+10141,
+37099,
+23801,
+65337,
+989,
+39257,
+45734,
+33630,
+30243,
+1312,
+12180,
+48036,
+15082,
+60980,
+14544,
+35678,
+37774,
+22595,
+48011,
+50213,
+7674,
+38623,
+57015,
+9915,
+51771,
+1177,
+63257,
+30167,
+17869,
+33569,
+38759,
+51526,
+28943,
+62642,
+33313,
+55326,
+5502,
+14054,
+63477,
+44626,
+24376,
+37844,
+14899,
+9119,
+5987,
+63360,
+3826,
+21801,
+3503,
+15581,
+34822,
+12665,
+54729,
+38946,
+54883,
+63514,
+37372,
+59802,
+11762,
+32789,
+13718,
+36670,
+56100,
+50140,
+32927,
+1850,
+1134,
+21250,
+4593,
+4295,
+56278,
+16318,
+10045,
+2617,
+57252,
+45119,
+22127,
+620,
+27440,
+12358,
+29617,
+55605,
+39669,
+52310,
+6190,
+52221,
+1832,
+20345,
+30650,
+17395,
+16114,
+44240,
+27424,
+3521,
+29234,
+54762,
+5652,
+42305,
+64923,
+20635,
+47858,
+23531,
+64683,
+59622,
+41657,
+3020,
+58878,
+17532,
+3096,
+8783,
+20541,
+5313,
+28083,
+56990,
+50699,
+12634,
+5800,
+59121,
+379,
+12591,
+46973,
+12958,
+13868,
+45247,
+1671,
+26694,
+5078,
+29292,
+46832,
+61498,
+47831,
+9429,
+37134,
+36503,
+6624,
+2207,
+58919,
+41104,
+7037,
+15915,
+50982,
+42451,
+37282,
+32236,
+26820,
+35202,
+40435,
+52593,
+60834,
+61259,
+10007,
+14257,
+30269,
+17523,
+53664,
+56700,
+27852,
+29133,
+62148,
+60025,
+18065,
+58504,
+10579,
+57318,
+62372,
+31216,
+54323,
+35461,
+42934,
+37983,
+38044,
+20557,
+51355,
+58347,
+21716,
+18168,
+61563,
+58049,
+57266,
+5366,
+43326,
+10674,
+15030,
+27073,
+19238,
+48785,
+49634,
+29423,
+56916,
+51906,
+64610,
+42961,
+2547,
+44061,
+57228,
+3748,
+15804,
+18839,
+32119,
+43406,
+47790,
+12502,
+29690,
+12395,
+45326,
+23761,
+24556,
+46934,
+23701,
+57771,
+19641,
+38088,
+42633,
+25972,
+12794,
+52825,
+61537,
+1689,
+51306,
+19745,
+63145,
+12774,
+52959,
+54887,
+32231,
+17164,
+51289,
+49148,
+56800,
+63610,
+3710,
+33679,
+22326,
+37535,
+11177,
+27012,
+53764,
+34178,
+52110,
+13166,
+51243,
+54781,
+11195,
+2840,
+15203,
+41300,
+40387,
+16210,
+37829,
+37201,
+33601,
+22764,
+35621,
+37851,
+59743,
+43319,
+42653,
+29758,
+38426,
+53927,
+20934,
+14802,
+7337,
+35949,
+31034,
+61247,
+33969,
+28174,
+37039,
+64464,
+12656,
+8586,
+58837,
+7759,
+50277,
+3248,
+707,
+10461,
+11070,
+60009,
+25483,
+5579,
+55590,
+32954,
+29519,
+23696,
+65361,
+750,
+16058,
+37681,
+56308,
+16925,
+64419,
+13789,
+46067,
+56429,
+29434,
+28518,
+10866,
+46366,
+8435,
+63153,
+58250,
+44283,
+15003,
+47756,
+34454,
+48634,
+982,
+11935,
+51663,
+35115,
+32260,
+49977,
+22932,
+13241,
+6915,
+9335,
+40386,
+41374,
+15204,
+51672,
+40574,
+7670,
+65145,
+41234,
+419,
+26996,
+34839,
+55066,
+53188,
+16916,
+7505,
+19009,
+19800,
+64929,
+3456,
+26485,
+29048,
+21136,
+3731,
+8518,
+22657,
+18261,
+32547,
+29723,
+52267,
+56697,
+47061,
+20760,
+19074,
+32462,
+9011,
+39569,
+57974,
+27,
+11704,
+58388,
+47155,
+43892,
+13975,
+16887,
+41047,
+62819,
+23905,
+26360,
+11656,
+5848,
+931,
+59139,
+48265,
+6357,
+58271,
+21128,
+14663,
+2544,
+50422,
+40644,
+62060,
+49380,
+43142,
+51833,
+12479,
+14458,
+418,
+41294,
+65146,
+61673,
+17119,
+43002,
+7967,
+57387,
+15008,
+40841,
+18716,
+34557,
+9523,
+16110,
+57923,
+48450,
+33038,
+36222,
+46908,
+28903,
+34105,
+65440,
+21164,
+16979,
+29153,
+31668,
+57819,
+10829,
+39535,
+26976,
+25779,
+36387,
+43466,
+8960,
+22834,
+40520,
+26909,
+15254,
+44500,
+21370,
+53618,
+13655,
+63316,
+9064,
+61981,
+14786,
+34580,
+14844,
+17298,
+6252,
+38665,
+63960,
+22914,
+13137,
+55686,
+14153,
+32112,
+5050,
+11964,
+35374,
+8751,
+54403,
+54312,
+15270,
+49805,
+53173,
+25515,
+55503,
+21237,
+64819,
+36837,
+51197,
+19147,
+20910,
+23958,
+27694,
+20359,
+30880,
+12223,
+7212,
+19345,
+13853,
+48333,
+14169,
+10930,
+49488,
+14960,
+51459,
+31296,
+11269,
+27059,
+31808,
+48872,
+2168,
+29612,
+32837,
+21218,
+45354,
+5713,
+5839,
+9349,
+52732,
+15208,
+32888,
+8703,
+15429,
+29533,
+47819,
+28854,
+4062,
+8921,
+1404,
+30927,
+63811,
+50417,
+13313,
+32484,
+30400,
+60403,
+25677,
+51156,
+20724,
+10430,
+51791,
+64284,
+46013,
+39532,
+45081,
+13432,
+57366,
+7036,
+41482,
+58920,
+55860,
+48788,
+63421,
+30430,
+51225,
+24689,
+16944,
+31031,
+47479,
+17328,
+24818,
+12190,
+26848,
+29163,
+56147,
+57434,
+51358,
+24634,
+2656,
+56165,
+53523,
+6193,
+4392,
+37163,
+55347,
+2323,
+142,
+4584,
+63008,
+59705,
+35821,
+11430,
+11909,
+54274,
+23575,
+13169,
+62816,
+3061,
+29559,
+11603,
+14465,
+61293,
+59684,
+48549,
+36344,
+340,
+4037,
+17708,
+25355,
+42568,
+18648,
+55294,
+46655,
+3059,
+62818,
+41257,
+16888,
+10583,
+34881,
+28343,
+29396,
+45458,
+15894,
+23309,
+45950,
+29497,
+2065,
+9803,
+26907,
+40522,
+12281,
+10593,
+37669,
+16534,
+33802,
+2345,
+47743,
+45685,
+13702,
+53060,
+5606,
+48169,
+15039,
+842,
+7853,
+62776,
+31511,
+14162,
+56635,
+2659,
+11162,
+17238,
+49609,
+35224,
+4323,
+34270,
+40261,
+26584,
+5666,
+28612,
+63953,
+49102,
+3953,
+56571,
+37481,
+21170,
+33579,
+55766,
+31173,
+52632,
+60576,
+43961,
+37370,
+63516,
+28130,
+6212,
+49072,
+61840,
+55023,
+57116,
+41775,
+24124,
+18604,
+36558,
+19331,
+12507,
+11722,
+45021,
+55737,
+60241,
+38890,
+31584,
+29710,
+18585,
+21105,
+5140,
+54868,
+35577,
+2427,
+48290,
+17873,
+21843,
+13109,
+55455,
+6242,
+49718,
+24798,
+37866,
+22258,
+62314,
+56039,
+25534,
+62453,
+56376,
+44124,
+37278,
+49853,
+47711,
+49276,
+46945,
+45744,
+34647,
+16285,
+30685,
+34760,
+37103,
+24618,
+32289,
+43076,
+34103,
+28905,
+4701,
+4280,
+39115,
+20088,
+47021,
+23423,
+40472,
+64866,
+20249,
+11241,
+30391,
+28433,
+24601,
+33249,
+50702,
+27353,
+48659,
+13002,
+39047,
+44983,
+16343,
+26053,
+62522,
+64603,
+31470,
+60588,
+7511,
+14178,
+24452,
+62882,
+9755,
+52835,
+49186,
+16744,
+14807,
+24719,
+23843,
+47708,
+51727,
+57897,
+52712,
+22155,
+58217,
+61025,
+36496,
+46326,
+28273,
+33998,
+42636,
+43865,
+31894,
+57510,
+54196,
+54829,
+47218,
+57599,
+32625,
+10119,
+15239,
+14420,
+32540,
+30825,
+38446,
+45814,
+1400,
+38266,
+21503,
+49937,
+11748,
+28823,
+44404,
+49743,
+39822,
+16045,
+11384,
+6264,
+31101,
+18209,
+54500,
+7240,
+29742,
+64130,
+14311,
+24440,
+48594,
+35663,
+56043,
+55141,
+18108,
+18715,
+41226,
+15009,
+53413,
+4142,
+60302,
+44765,
+64298,
+54474,
+46858,
+50952,
+19086,
+64659,
+20103,
+3212,
+22377,
+22941,
+34792,
+50307,
+19048,
+64296,
+44767,
+19865,
+50729,
+24641,
+41695,
+38178,
+54061,
+22572,
+57879,
+50765,
+12757,
+45468,
+20585,
+30541,
+24903,
+43709,
+50324,
+1203,
+50569,
+12498,
+61380,
+65333,
+59404,
+9823,
+1502,
+21149,
+42834,
+40512,
+48774,
+34396,
+16179,
+19253,
+55895,
+60127,
+32509,
+22565,
+14740,
+47541,
+21077,
+18299,
+36075,
+46080,
+58636,
+12605,
+38769,
+8475,
+30937,
+33405,
+52482,
+11292,
+14991,
+10357,
+52156,
+19138,
+42140,
+32537,
+6114,
+56246,
+61143,
+28376,
+48055,
+22499,
+10117,
+32627,
+18426,
+32188,
+50836,
+27317,
+51530,
+55838,
+1954,
+44184,
+17923,
+40532,
+44829,
+18016,
+10106,
+40729,
+47280,
+32450,
+28796,
+15818,
+24581,
+27672,
+8792,
+60030,
+2941,
+11206,
+31059,
+60628,
+40314,
+47279,
+40744,
+10107,
+23634,
+17423,
+32021,
+57505,
+54505,
+32820,
+19350,
+48208,
+24445,
+53348,
+47917,
+46616,
+25381,
+50224,
+60740,
+18353,
+39611,
+40074,
+21748,
+227,
+42830,
+25063,
+673,
+18962,
+46903,
+56161,
+54367,
+55510,
+4008,
+39709,
+10783,
+62627,
+18227,
+22368,
+42120,
+48093,
+34789,
+20766,
+38906,
+33685,
+36466,
+62375,
+36885,
+21974,
+1859,
+35694,
+48955,
+8495,
+44564,
+49160,
+57594,
+11628,
+6482,
+45330,
+34748,
+39396,
+37676,
+7498,
+39075,
+23852,
+2232,
+42264,
+51432,
+5275,
+8808,
+26747,
+52878,
+21555,
+2716,
+41634,
+8553,
+60535,
+7130,
+54583,
+26071,
+20837,
+28813,
+33652,
+39012,
+60663,
+10814,
+54497,
+62059,
+41242,
+50423,
+9693,
+9432,
+62752,
+39149,
+1295,
+44552,
+32554,
+34995,
+24730,
+21829,
+44622,
+29053,
+892,
+55557,
+39800,
+32879,
+62893,
+52991,
+29763,
+3033,
+60505,
+60158,
+17220,
+58391,
+21758,
+15377,
+11529,
+58450,
+13495,
+1249,
+51015,
+56516,
+35654,
+63347,
+53712,
+36533,
+28414,
+59110,
+54350,
+42378,
+60877,
+54580,
+5929,
+61482,
+61448,
+14734,
+60392,
+17723,
+18345,
+1805,
+33713,
+11126,
+64896,
+13006,
+46608,
+29359,
+31861,
+35571,
+30800,
+53280,
+25834,
+39620,
+44789,
+12150,
+64702,
+25540,
+44451,
+7669,
+41297,
+51673,
+14798,
+19659,
+59581,
+22529,
+42043,
+7971,
+56569,
+3955,
+26789,
+6222,
+13913,
+21955,
+64944,
+17603,
+22178,
+27461,
+29438,
+17490,
+1511,
+47550,
+27967,
+21899,
+41816,
+1015,
+19675,
+8358,
+19343,
+7214,
+7295,
+40252,
+63130,
+45136,
+409,
+2079,
+21206,
+4003,
+39955,
+8873,
+44261,
+44828,
+40748,
+17924,
+6392,
+22472,
+12990,
+34346,
+2243,
+55871,
+57957,
+12280,
+41033,
+26908,
+41200,
+22835,
+49435,
+7405,
+6983,
+4890,
+60071,
+48773,
+40794,
+42835,
+2836,
+45602,
+6389,
+40236,
+33228,
+53868,
+51041,
+2542,
+14665,
+1212,
+5620,
+60781,
+53230,
+15408,
+65202,
+35909,
+27528,
+21215,
+4650,
+32847,
+9069,
+13896,
+416,
+14460,
+38473,
+13538,
+46132,
+16723,
+63175,
+55323,
+36578,
+14560,
+47188,
+57762,
+42987,
+22706,
+24831,
+64865,
+40925,
+23424,
+51194,
+6732,
+26574,
+47962,
+15287,
+29190,
+5625,
+8330,
+43653,
+7608,
+37244,
+15139,
+23519,
+15697,
+64774,
+39306,
+12936,
+31308,
+42682,
+23421,
+47023,
+50822,
+29037,
+14881,
+6065,
+53081,
+222,
+31447,
+60972,
+64260,
+7962,
+33860,
+10079,
+36625,
+52592,
+41473,
+35203,
+65242,
+55061,
+35050,
+50191,
+33408,
+10182,
+45902,
+14511,
+49646,
+59339,
+6156,
+12061,
+16426,
+53657,
+21523,
+46590,
+8680,
+29527,
+42280,
+45200,
+52283,
+15395,
+32612,
+57741,
+5777,
+42456,
+23646,
+12747,
+53904,
+34071,
+43089,
+10500,
+32161,
+54300,
+50017,
+59043,
+30988,
+23461,
+53820,
+22930,
+49979,
+61359,
+5163,
+61388,
+37462,
+16209,
+41373,
+41301,
+9336,
+61488,
+27443,
+64461,
+53467,
+60754,
+39156,
+11347,
+60554,
+33322,
+49464,
+31379,
+237,
+39929,
+37805,
+17213,
+38328,
+22159,
+65399,
+44968,
+53860,
+62681,
+53762,
+27014,
+63556,
+45910,
+6536,
+62791,
+65347,
+12831,
+44787,
+39622,
+42051,
+34875,
+42464,
+39993,
+56563,
+32481,
+63708,
+13302,
+29821,
+52504,
+6433,
+65100,
+5262,
+13213,
+2952,
+15381,
+37780,
+4551,
+58872,
+64112,
+39719,
+29491,
+18723,
+10925,
+3450,
+38913,
+45148,
+48588,
+46153,
+14824,
+11486,
+60175,
+62873,
+9156,
+8797,
+19823,
+9737,
+42878,
+47278,
+40731,
+60629,
+57618,
+6217,
+56421,
+30537,
+59926,
+43190,
+36066,
+2574,
+58488,
+3488,
+51544,
+63997,
+34920,
+62813,
+11337,
+58908,
+35548,
+6966,
+32492,
+36873,
+46007,
+51101,
+623,
+26993,
+16503,
+44194,
+56126,
+29719,
+54144,
+21350,
+50037,
+8052,
+32269,
+60345,
+39303,
+57616,
+60631,
+55811,
+13661,
+16143,
+49239,
+40187,
+17093,
+44785,
+12833,
+29869,
+26512,
+19561,
+16005,
+64715,
+26583,
+41006,
+34271,
+18483,
+38056,
+58400,
+44075,
+23115,
+54790,
+63129,
+40543,
+7296,
+39034,
+37379,
+33441,
+56537,
+26561,
+27371,
+54517,
+38619,
+57,
+21470,
+47871,
+8764,
+1145,
+33227,
+40507,
+6390,
+17926,
+45519,
+51764,
+63763,
+27650,
+795,
+65291,
+536,
+56995,
+12094,
+13401,
+28476,
+36301,
+10777,
+5431,
+53099,
+17617,
+13374,
+26206,
+3362,
+22899,
+21674,
+65154,
+31397,
+38209,
+15357,
+34374,
+44753,
+53183,
+38173,
+32330,
+13408,
+1609,
+58333,
+47723,
+64655,
+27897,
+53865,
+32806,
+26386,
+64244,
+27139,
+60112,
+34594,
+3625,
+23877,
+17092,
+40271,
+49240,
+25993,
+654,
+47828,
+24483,
+22970,
+26771,
+18373,
+23510,
+46845,
+20464,
+8561,
+19558,
+25805,
+16257,
+56843,
+14672,
+41870,
+39316,
+37811,
+63238,
+6703,
+14870,
+16166,
+3621,
+58427,
+18070,
+8850,
+12248,
+18920,
+24822,
+15314,
+40025,
+28067,
+60198,
+6152,
+34724,
+51285,
+746,
+44131,
+18393,
+3425,
+31660,
+8256,
+33464,
+27806,
+59158,
+29543,
+45132,
+4456,
+55245,
+57361,
+33487,
+20450,
+60228,
+4878,
+54698,
+5280,
+31150,
+34218,
+50176,
+8735,
+1929,
+3159,
+57097,
+16294,
+46446,
+49564,
+36565,
+54933,
+49999,
+30768,
+3377,
+20702,
+51945,
+39166,
+42144,
+55225,
+55362,
+11641,
+58597,
+18201,
+48151,
+34464,
+4656,
+15467,
+16953,
+10411,
+64084,
+8983,
+45820,
+55988,
+46669,
+27599,
+62387,
+41958,
+43293,
+51298,
+50629,
+14619,
+55824,
+4313,
+18307,
+11388,
+23151,
+61374,
+23731,
+20870,
+14637,
+35026,
+10520,
+21747,
+40710,
+39612,
+13478,
+44339,
+51427,
+57516,
+46310,
+2411,
+30786,
+18440,
+55907,
+11837,
+42646,
+49410,
+12880,
+41859,
+42575,
+61131,
+31795,
+5527,
+24198,
+59959,
+51977,
+23830,
+9365,
+28837,
+7621,
+41901,
+48116,
+20945,
+46161,
+45334,
+62933,
+5817,
+56425,
+56266,
+23805,
+52756,
+12652,
+3549,
+6887,
+36014,
+3564,
+15774,
+63037,
+18044,
+59520,
+30667,
+28066,
+40154,
+15315,
+12363,
+24368,
+10035,
+61104,
+696,
+55195,
+23336,
+60455,
+38993,
+56632,
+53754,
+54630,
+32445,
+47147,
+59907,
+7301,
+26433,
+62010,
+21097,
+9497,
+24586,
+3085,
+16587,
+54843,
+11407,
+834,
+24960,
+21087,
+18800,
+56562,
+40350,
+42465,
+33964,
+26046,
+44838,
+21301,
+39058,
+64293,
+37752,
+43929,
+29932,
+33434,
+55410,
+57985,
+65078,
+30480,
+39094,
+7097,
+52787,
+46996,
+11903,
+3530,
+57557,
+19566,
+22636,
+56566,
+19779,
+18466,
+43535,
+38395,
+28248,
+46259,
+62808,
+32265,
+48844,
+58404,
+59611,
+8872,
+40536,
+4004,
+28114,
+52363,
+19782,
+45807,
+12066,
+8001,
+39125,
+18974,
+43081,
+39904,
+38617,
+54519,
+233,
+50134,
+57398,
+27245,
+57670,
+16806,
+7636,
+8634,
+46245,
+31769,
+26288,
+37804,
+40372,
+238,
+20567,
+11679,
+4675,
+16161,
+3687,
+3715,
+43757,
+6227,
+50185,
+65427,
+21045,
+39203,
+21514,
+4031,
+26024,
+19890,
+57100,
+2386,
+9940,
+179,
+19298,
+57140,
+38616,
+39944,
+43082,
+2012,
+30051,
+31328,
+38745,
+34214,
+17399,
+35681,
+61624,
+64047,
+31207,
+32780,
+55052,
+55980,
+11077,
+50896,
+53214,
+37918,
+28949,
+48272,
+3763,
+56780,
+29813,
+58229,
+871,
+30139,
+32863,
+5491,
+45359,
+51831,
+43144,
+31350,
+45403,
+12514,
+50603,
+62343,
+60653,
+56908,
+56385,
+3145,
+21062,
+32295,
+5133,
+24421,
+14382,
+48902,
+15024,
+4027,
+60543,
+8454,
+16767,
+51534,
+59018,
+47731,
+63024,
+21264,
+38488,
+43341,
+16823,
+2311,
+61938,
+35101,
+27665,
+37692,
+63229,
+2160,
+4914,
+41710,
+47568,
+9860,
+62740,
+2134,
+9609,
+21708,
+59304,
+56778,
+3765,
+25913,
+23488,
+42394,
+16044,
+40859,
+49744,
+49133,
+50683,
+52345,
+55974,
+60706,
+46094,
+36673,
+55629,
+34334,
+10753,
+8088,
+3258,
+35615,
+2947,
+7315,
+39067,
+53730,
+27507,
+22046,
+32878,
+40628,
+55558,
+51515,
+28738,
+61415,
+11006,
+45812,
+38448,
+64142,
+37941,
+7285,
+21256,
+6050,
+705,
+3250,
+29030,
+8146,
+14706,
+61318,
+4771,
+19956,
+8063,
+45781,
+45507,
+45925,
+44608,
+47898,
+21080,
+49568,
+17202,
+51370,
+31067,
+26844,
+58612,
+56972,
+50470,
+32813,
+42492,
+52066,
+23389,
+59130,
+38560,
+24332,
+29151,
+16981,
+14446,
+54002,
+62464,
+43671,
+1131,
+44059,
+2549,
+62274,
+39279,
+28887,
+43743,
+17837,
+18845,
+14144,
+46602,
+54839,
+27957,
+6084,
+18612,
+5860,
+16416,
+39023,
+45978,
+25364,
+49281,
+20410,
+63056,
+22384,
+53265,
+45996,
+15587,
+55620,
+13768,
+31244,
+55644,
+29490,
+40333,
+64113,
+830,
+18140,
+4972,
+65169,
+7144,
+43781,
+39275,
+10782,
+40698,
+4009,
+38457,
+41665,
+60846,
+12256,
+24955,
+25918,
+48478,
+35943,
+33012,
+23857,
+21018,
+1613,
+16771,
+6804,
+60270,
+33566,
+37140,
+31147,
+33912,
+60413,
+18576,
+42046,
+19690,
+30468,
+26193,
+42222,
+29846,
+60042,
+36038,
+63474,
+60993,
+64759,
+63345,
+35656,
+15282,
+61640,
+9808,
+52309,
+41537,
+55606,
+13115,
+1525,
+45310,
+45711,
+17115,
+33945,
+55130,
+10399,
+4242,
+5855,
+30811,
+14254,
+50610,
+10113,
+62486,
+45705,
+63185,
+56980,
+20736,
+4868,
+20218,
+20159,
+18313,
+4399,
+21941,
+44675,
+2508,
+47802,
+57138,
+19300,
+43844,
+23600,
+30380,
+52211,
+38251,
+6431,
+52506,
+22016,
+49345,
+7801,
+43,
+31280,
+61771,
+25711,
+42050,
+40354,
+44788,
+40581,
+25835,
+17060,
+7603,
+16730,
+26367,
+30545,
+13477,
+40073,
+40711,
+18354,
+38984,
+45908,
+63558,
+11962,
+5052,
+15954,
+9009,
+32464,
+6911,
+53411,
+15011,
+9505,
+58430,
+16851,
+5045,
+46199,
+27057,
+11271,
+62660,
+38079,
+55957,
+42064,
+32521,
+45066,
+32968,
+48214,
+22365,
+60290,
+64676,
+34065,
+4951,
+2767,
+47378,
+8573,
+58330,
+19000,
+3188,
+6454,
+62218,
+57973,
+41266,
+9012,
+31537,
+22903,
+24131,
+52744,
+56229,
+26182,
+42868,
+42001,
+21532,
+52752,
+45273,
+61052,
+15596,
+36755,
+60369,
+64054,
+43825,
+36825,
+11154,
+17879,
+29445,
+46444,
+16296,
+25352,
+64884,
+56592,
+13359,
+5657,
+18198,
+26725,
+11411,
+26975,
+41207,
+10830,
+45080,
+41109,
+46014,
+4100,
+37332,
+33692,
+320,
+43284,
+12474,
+6820,
+9465,
+30374,
+29340,
+11464,
+15252,
+26911,
+471,
+15691,
+21823,
+61505,
+25624,
+45648,
+39225,
+42784,
+57608,
+23139,
+13689,
+35728,
+63601,
+51550,
+10802,
+61892,
+60772,
+28161,
+33584,
+35639,
+30219,
+61012,
+39216,
+55695,
+54563,
+31572,
+25547,
+20700,
+3379,
+58422,
+37302,
+15444,
+49193,
+24627,
+3016,
+48312,
+7181,
+6562,
+60605,
+22715,
+43776,
+33152,
+52584,
+10966,
+3216,
+64662,
+20172,
+26605,
+14911,
+61042,
+10953,
+24884,
+44701,
+23756,
+30255,
+22877,
+26347,
+45562,
+49536,
+26736,
+29268,
+52410,
+35236,
+58675,
+4309,
+27265,
+8243,
+45640,
+2791,
+13545,
+3886,
+45103,
+54543,
+32069,
+54795,
+34902,
+21294,
+42727,
+8641,
+8942,
+63596,
+61045,
+2905,
+45489,
+37737,
+4775,
+54574,
+4742,
+38414,
+29981,
+53155,
+61875,
+16703,
+15322,
+57865,
+394,
+63547,
+59640,
+61881,
+38857,
+2627,
+42620,
+5226,
+62253,
+36233,
+30798,
+35573,
+50459,
+43943,
+29484,
+57654,
+9854,
+41950,
+6089,
+17682,
+657,
+499,
+61999,
+22699,
+10894,
+37675,
+40672,
+34749,
+47099,
+39063,
+17464,
+32959,
+50651,
+17071,
+4980,
+39039,
+37391,
+1461,
+33935,
+45919,
+48002,
+52385,
+52611,
+37153,
+53792,
+51425,
+44341,
+15143,
+32895,
+258,
+6580,
+27128,
+37293,
+54054,
+22895,
+48275,
+1197,
+29706,
+19128,
+58148,
+60597,
+28039,
+1387,
+61619,
+2929,
+13936,
+9307,
+25975,
+22670,
+16597,
+36947,
+38685,
+20274,
+18096,
+508,
+55495,
+6058,
+35068,
+25854,
+23316,
+62227,
+17430,
+33873,
+19505,
+15762,
+16827,
+55089,
+5120,
+37767,
+37623,
+52162,
+25321,
+62176,
+21203,
+18079,
+35499,
+13797,
+51493,
+29127,
+16383,
+5,
+54615,
+29790,
+45057,
+53054,
+37810,
+40168,
+41871,
+27174,
+15672,
+29556,
+3511,
+22562,
+4965,
+47999,
+12935,
+40455,
+64775,
+57615,
+40278,
+60346,
+31532,
+54319,
+44633,
+14624,
+30688,
+49528,
+43852,
+53917,
+30973,
+42968,
+49993,
+32565,
+55667,
+64004,
+10054,
+21725,
+50386,
+51812,
+19728,
+53473,
+47403,
+28886,
+39747,
+62275,
+22329,
+10781,
+39711,
+43782,
+4529,
+27677,
+64257,
+12555,
+31783,
+58282,
+2612,
+13294,
+21311,
+3153,
+44946,
+37232,
+4926,
+13276,
+28828,
+45733,
+41614,
+990,
+11508,
+45357,
+5493,
+29501,
+7916,
+33767,
+25606,
+27734,
+28411,
+65365,
+52124,
+9554,
+33596,
+43310,
+50202,
+9408,
+59708,
+48569,
+62614,
+13077,
+26305,
+8768,
+52459,
+26302,
+56770,
+48051,
+63990,
+64563,
+31254,
+42783,
+39511,
+45649,
+35279,
+10465,
+49088,
+65024,
+65410,
+19246,
+55694,
+39495,
+61013,
+7753,
+42371,
+26643,
+27700,
+62266,
+18179,
+33285,
+6713,
+62717,
+44847,
+21513,
+39916,
+21046,
+53749,
+22525,
+12235,
+59416,
+1361,
+50477,
+27101,
+6339,
+9232,
+14396,
+42659,
+26076,
+19976,
+8009,
+46826,
+19536,
+14999,
+15175,
+62053,
+50485,
+49292,
+41940,
+15652,
+41631,
+24283,
+53882,
+33748,
+45018,
+61408,
+50789,
+48806,
+25922,
+34993,
+32556,
+42143,
+40111,
+51946,
+34467,
+1235,
+18082,
+51958,
+31920,
+51000,
+1836,
+11346,
+40379,
+60755,
+55874,
+19909,
+32730,
+25342,
+1294,
+40639,
+62753,
+16085,
+8406,
+2441,
+51341,
+37084,
+15275,
+29007,
+35754,
+43429,
+29175,
+18189,
+55893,
+19255,
+44086,
+47594,
+19985,
+7061,
+28779,
+38196,
+23893,
+58755,
+18973,
+39947,
+8002,
+63892,
+52901,
+59739,
+46025,
+32704,
+12902,
+32513,
+20087,
+40929,
+4281,
+30137,
+873,
+45367,
+34776,
+31293,
+57582,
+5299,
+35527,
+58235,
+25530,
+3528,
+11905,
+17599,
+5486,
+52089,
+49752,
+17170,
+63393,
+7096,
+39977,
+30481,
+42135,
+36000,
+36353,
+38614,
+57142,
+3288,
+11598,
+35863,
+38904,
+20768,
+45674,
+19835,
+57744,
+37673,
+10896,
+51585,
+23851,
+40669,
+7499,
+30349,
+19593,
+16019,
+56182,
+26660,
+53729,
+39805,
+7316,
+26755,
+17463,
+39393,
+47100,
+552,
+45391,
+64292,
+39987,
+21302,
+31119,
+44885,
+46868,
+62080,
+24255,
+36490,
+626,
+27491,
+44982,
+40913,
+13003,
+56411,
+30719,
+61171,
+5286,
+6013,
+37390,
+39387,
+4981,
+32217,
+4763,
+37378,
+40250,
+7297,
+64182,
+7697,
+65223,
+52975,
+19024,
+55369,
+35667,
+25271,
+45977,
+39734,
+16417,
+59227,
+59378,
+30045,
+57221,
+35603,
+32105,
+43478,
+61114,
+60662,
+40649,
+33653,
+52438,
+34861,
+16937,
+49028,
+13638,
+21033,
+60182,
+16220,
+27099,
+50479,
+36084,
+28348,
+28584,
+53405,
+65095,
+14862,
+56631,
+40015,
+60456,
+22281,
+15822,
+37017,
+54673,
+30305,
+56609,
+45907,
+39609,
+18355,
+58101,
+50747,
+57455,
+58166,
+49505,
+8999,
+25291,
+38167,
+29550,
+33294,
+44128,
+36538,
+35355,
+53773,
+32750,
+7491,
+17295,
+57721,
+58804,
+25069,
+17576,
+28051,
+5573,
+7326,
+27604,
+46766,
+1038,
+821,
+25275,
+24532,
+59872,
+31462,
+14509,
+45904,
+59232,
+54882,
+41566,
+54730,
+4956,
+48496,
+58642,
+2235,
+12106,
+6670,
+37323,
+18545,
+59937,
+914,
+574,
+45364,
+26067,
+50627,
+51300,
+35702,
+65404,
+33266,
+5194,
+165,
+1975,
+32670,
+50078,
+8527,
+46733,
+1918,
+8334,
+8831,
+47499,
+28938,
+45147,
+40328,
+3451,
+44977,
+4738,
+16527,
+50379,
+33684,
+40689,
+20767,
+39084,
+35864,
+16626,
+59531,
+62122,
+8889,
+62169,
+49908,
+56157,
+23165,
+26325,
+37838,
+17702,
+31583,
+40972,
+60242,
+2415,
+19125,
+12861,
+19817,
+47201,
+47959,
+37488,
+32412,
+16777,
+6950,
+9354,
+26468,
+11809,
+5012,
+57524,
+1138,
+7366,
+15658,
+30725,
+28141,
+46202,
+1123,
+5890,
+21817,
+53694,
+49638,
+54486,
+46126,
+62928,
+30533,
+2626,
+39418,
+61882,
+14224,
+47676,
+23438,
+22747,
+38471,
+14462,
+60191,
+64459,
+27445,
+5505,
+62598,
+63865,
+24043,
+60460,
+29064,
+61813,
+35454,
+9637,
+18721,
+29493,
+8367,
+886,
+50613,
+51713,
+35517,
+57255,
+1568,
+4721,
+37877,
+2034,
+61609,
+5525,
+31797,
+13338,
+4472,
+1305,
+12998,
+29142,
+62709,
+26800,
+48455,
+6938,
+47338,
+25999,
+54127,
+45295,
+4725,
+57176,
+47092,
+41770,
+34948,
+2150,
+9568,
+16000,
+26079,
+36270,
+15307,
+5451,
+2321,
+55349,
+34484,
+54122,
+23307,
+15896,
+56728,
+24875,
+12660,
+58814,
+19666,
+9998,
+18812,
+23794,
+58220,
+26472,
+48075,
+9283,
+58853,
+8719,
+42337,
+18742,
+53153,
+29983,
+5179,
+42929,
+21378,
+8474,
+40777,
+12606,
+42566,
+25357,
+47165,
+64903,
+10665,
+47123,
+43232,
+51525,
+41589,
+33570,
+43529,
+15302,
+59598,
+55064,
+34841,
+45515,
+5209,
+10102,
+61581,
+24973,
+59316,
+34213,
+39899,
+31329,
+13646,
+38405,
+34708,
+53920,
+17508,
+2827,
+26721,
+17083,
+45425,
+58473,
+18531,
+65004,
+9733,
+43138,
+33343,
+48684,
+60494,
+29130,
+6343,
+28441,
+16603,
+54727,
+12667,
+14409,
+64733,
+33300,
+24062,
+30336,
+30027,
+18965,
+37545,
+1654,
+52391,
+56823,
+32683,
+22493,
+57249,
+34885,
+48144,
+36956,
+54593,
+56872,
+48889,
+53129,
+43474,
+49179,
+46899,
+31544,
+1233,
+34469,
+25769,
+6926,
+31128,
+53346,
+24447,
+36375,
+14589,
+20273,
+39351,
+36948,
+53459,
+41992,
+49858,
+30554,
+64783,
+63014,
+31834,
+45740,
+42996,
+52009,
+12215,
+51466,
+4086,
+46103,
+53758,
+64976,
+61676,
+63959,
+41185,
+6253,
+23443,
+20489,
+37209,
+51048,
+18163,
+53736,
+51803,
+50348,
+48308,
+57926,
+58192,
+22980,
+60362,
+39,
+54069,
+60560,
+42766,
+29638,
+9697,
+63585,
+57644,
+34339,
+52318,
+35199,
+35512,
+60786,
+38316,
+1318,
+35229,
+18665,
+13522,
+2051,
+55733,
+24503,
+25016,
+24338,
+50284,
+26019,
+65067,
+57014,
+41598,
+7675,
+51237,
+56,
+40243,
+54518,
+39943,
+39905,
+57141,
+39089,
+36354,
+35177,
+9344,
+2687,
+6267,
+14315,
+50344,
+22291,
+9741,
+65464,
+50064,
+54690,
+47199,
+19819,
+30068,
+61338,
+11694,
+52722,
+19881,
+90,
+54015,
+63365,
+484,
+17973,
+14677,
+8480,
+56605,
+28451,
+50264,
+12802,
+12962,
+49653,
+45501,
+28221,
+43368,
+3593,
+53594,
+55753,
+51135,
+54220,
+23671,
+27955,
+54841,
+16589,
+1514,
+6187,
+6578,
+260,
+63027,
+17737,
+54125,
+26001,
+24331,
+39759,
+59131,
+18644,
+54283,
+45556,
+1008,
+1365,
+33825,
+4680,
+43697,
+62261,
+3543,
+42478,
+45230,
+29045,
+64913,
+25248,
+49035,
+65117,
+24245,
+30699,
+42425,
+32476,
+57107,
+51566,
+61724,
+32993,
+15695,
+23521,
+62671,
+65355,
+4346,
+50733,
+58202,
+54359,
+24153,
+11774,
+25175,
+1982,
+11370,
+55521,
+14018,
+55660,
+55949,
+62578,
+63832,
+62115,
+38373,
+54811,
+29650,
+52336,
+36379,
+27947,
+54716,
+20395,
+3256,
+8090,
+65457,
+9956,
+30502,
+58682,
+57114,
+55025,
+33658,
+58620,
+56204,
+60615,
+13452,
+19609,
+64512,
+65474,
+43340,
+39847,
+21265,
+47260,
+34021,
+23816,
+6107,
+37394,
+19337,
+25944,
+54267,
+24392,
+33730,
+53486,
+30061,
+13537,
+40486,
+14461,
+38851,
+22748,
+6990,
+9038,
+42409,
+1041,
+50506,
+24101,
+46300,
+36616,
+12526,
+32092,
+9703,
+41664,
+39707,
+4010,
+28219,
+45503,
+3966,
+64332,
+14970,
+27396,
+64141,
+39793,
+45813,
+40869,
+30826,
+34567,
+51389,
+15219,
+3674,
+21811,
+49316,
+26535,
+15059,
+26342,
+59199,
+29972,
+7503,
+16918,
+61463,
+51507,
+58185,
+64379,
+53926,
+41361,
+29759,
+14520,
+9023,
+45094,
+63079,
+51401,
+44683,
+18729,
+20026,
+48397,
+29980,
+39429,
+4743,
+60468,
+7178,
+58956,
+26166,
+28245,
+5428,
+34707,
+38742,
+13647,
+16755,
+33673,
+37094,
+48936,
+1292,
+25344,
+5426,
+28247,
+39964,
+43536,
+61150,
+32381,
+61638,
+15284,
+16307,
+58662,
+50274,
+43668,
+17608,
+1914,
+5066,
+57493,
+48728,
+36462,
+52349,
+2471,
+41810,
+3103,
+60154,
+54810,
+38513,
+62116,
+5884,
+47642,
+47317,
+63243,
+54820,
+17217,
+49365,
+23665,
+26223,
+49866,
+46705,
+44545,
+2327,
+14668,
+17233,
+2469,
+52351,
+18750,
+20056,
+6755,
+43120,
+34474,
+5320,
+740,
+43427,
+35756,
+8115,
+27335,
+8500,
+44877,
+4769,
+61320,
+63284,
+1718,
+306,
+48033,
+22452,
+28597,
+32742,
+64253,
+1891,
+8349,
+22158,
+40369,
+17214,
+1421,
+61344,
+56017,
+31155,
+35645,
+22845,
+6487,
+64740,
+248,
+1317,
+38637,
+60787,
+16368,
+31158,
+5722,
+18186,
+48897,
+4696,
+22242,
+14241,
+63886,
+28877,
+54479,
+20924,
+23563,
+5455,
+49406,
+48461,
+7781,
+23691,
+43223,
+47915,
+53350,
+8138,
+25599,
+7868,
+44267,
+43907,
+6958,
+49368,
+29701,
+16062,
+65436,
+17696,
+44670,
+14540,
+28134,
+10793,
+53838,
+65073,
+20384,
+20715,
+58847,
+61227,
+26085,
+57295,
+2180,
+36751,
+16922,
+21502,
+40866,
+1401,
+27721,
+8254,
+31662,
+46034,
+10711,
+55021,
+61842,
+55656,
+19682,
+2395,
+47622,
+63171,
+6430,
+39633,
+52212,
+54948,
+1208,
+49872,
+51275,
+13397,
+61659,
+46335,
+63729,
+57160,
+17332,
+9620,
+29728,
+4626,
+61048,
+37603,
+33437,
+28340,
+2621,
+15123,
+65525,
+5959,
+22796,
+44940,
+47410,
+1111,
+62768,
+18161,
+51050,
+14700,
+24783,
+14008,
+10541,
+37631,
+9811,
+45400,
+32597,
+57466,
+62184,
+35800,
+15356,
+40210,
+31398,
+47425,
+48639,
+42084,
+18755,
+57421,
+27193,
+57038,
+4929,
+14132,
+23785,
+23892,
+39129,
+28780,
+5170,
+51039,
+53870,
+64274,
+64032,
+57128,
+50814,
+29778,
+45764,
+14834,
+24621,
+16879,
+45477,
+57945,
+7987,
+54060,
+40816,
+41696,
+43455,
+55791,
+32329,
+40205,
+53184,
+28180,
+58245,
+37565,
+29549,
+38975,
+25292,
+11021,
+56506,
+6366,
+32085,
+53967,
+15857,
+13015,
+3873,
+14905,
+48026,
+8748,
+33380,
+65324,
+29369,
+42561,
+48999,
+18021,
+53403,
+28586,
+43149,
+14085,
+21994,
+47252,
+50566,
+27087,
+1397,
+61272,
+37611,
+3174,
+29890,
+42694,
+35587,
+45025,
+54326,
+6239,
+28915,
+20619,
+33224,
+54340,
+62699,
+19937,
+10983,
+22680,
+36640,
+63431,
+36023,
+22256,
+37868,
+9625,
+32361,
+42725,
+21296,
+24010,
+62017,
+33710,
+8380,
+62285,
+36509,
+57717,
+30058,
+21117,
+37648,
+49142,
+42012,
+11989,
+18875,
+14434,
+64916,
+23996,
+53180,
+4048,
+25154,
+1077,
+26690,
+60033,
+51485,
+42632,
+41408,
+19642,
+43451,
+57962,
+1048,
+47545,
+31347,
+35134,
+55956,
+39590,
+62661,
+43358,
+24980,
+36100,
+50405,
+33482,
+44579,
+57942,
+31078,
+52097,
+36327,
+59637,
+62605,
+16525,
+4740,
+54576,
+7936,
+32914,
+16553,
+60712,
+17377,
+58399,
+40258,
+18484,
+57341,
+37044,
+15778,
+20818,
+27684,
+5796,
+19514,
+64910,
+26488,
+20556,
+41449,
+37984,
+7382,
+61721,
+34307,
+10899,
+7655,
+9597,
+63988,
+48053,
+28378,
+4077,
+2042,
+11806,
+25734,
+43492,
+4517,
+44113,
+7392,
+16844,
+63760,
+27714,
+49619,
+53721,
+42668,
+64320,
+20643,
+33833,
+3753,
+60353,
+5160,
+4255,
+31553,
+51292,
+29627,
+25043,
+44323,
+588,
+60976,
+53566,
+48494,
+4958,
+34362,
+12907,
+19787,
+17147,
+61934,
+14866,
+33574,
+13856,
+10510,
+31881,
+55299,
+20116,
+35853,
+22760,
+10001,
+29205,
+29242,
+7381,
+38043,
+41450,
+42935,
+29993,
+56621,
+47689,
+49711,
+8292,
+6173,
+31708,
+14661,
+21130,
+51752,
+46553,
+6892,
+23276,
+13783,
+18986,
+29323,
+43334,
+49353,
+7898,
+65007,
+33327,
+31686,
+44874,
+18217,
+42504,
+35191,
+37725,
+17656,
+61277,
+48519,
+21441,
+21540,
+14270,
+19973,
+16003,
+19563,
+47787,
+29378,
+45874,
+7284,
+39791,
+64143,
+18867,
+25444,
+3151,
+21313,
+6000,
+58210,
+26579,
+18772,
+3307,
+19357,
+60673,
+15014,
+63085,
+23451,
+46784,
+20352,
+50127,
+337,
+59915,
+4758,
+28948,
+39886,
+53215,
+47687,
+56623,
+16354,
+57696,
+18450,
+548,
+956,
+11232,
+8805,
+32963,
+48609,
+1933,
+21248,
+1136,
+57526,
+18713,
+18110,
+16900,
+16520,
+8268,
+54775,
+23759,
+45328,
+6484,
+28429,
+17758,
+48972,
+19416,
+1773,
+25307,
+64743,
+34629,
+60169,
+22213,
+58065,
+29458,
+4417,
+9836,
+2033,
+38827,
+4722,
+57920,
+47906,
+30435,
+25696,
+27846,
+12898,
+9624,
+38118,
+22257,
+40955,
+24799,
+56746,
+60893,
+62108,
+61007,
+26265,
+1281,
+45243,
+1816,
+50858,
+57059,
+44718,
+27487,
+59742,
+41366,
+35622,
+2251,
+63821,
+56252,
+6686,
+14898,
+41578,
+24377,
+45179,
+30627,
+52847,
+17701,
+38893,
+26326,
+3272,
+63836,
+4897,
+55864,
+51112,
+44960,
+37200,
+41371,
+16211,
+15981,
+35804,
+13771,
+1776,
+27514,
+15919,
+36034,
+57623,
+10420,
+56614,
+64135,
+27273,
+57284,
+42689,
+37111,
+63237,
+40167,
+39317,
+53055,
+15339,
+20649,
+17212,
+40371,
+39930,
+26289,
+37127,
+12020,
+62196,
+42914,
+61908,
+63569,
+43448,
+62473,
+62029,
+300,
+5662,
+17557,
+64519,
+53423,
+1965,
+5968,
+14505,
+23065,
+15432,
+6770,
+49674,
+4550,
+40337,
+15382,
+18623,
+42795,
+37503,
+22594,
+41603,
+35679,
+17401,
+30073,
+43818,
+10439,
+37622,
+39334,
+5121,
+28240,
+45012,
+5439,
+64008,
+7850,
+57541,
+63751,
+51476,
+1432,
+54013,
+92,
+58491,
+43928,
+39985,
+64294,
+19050,
+18488,
+5995,
+61002,
+65379,
+12524,
+36618,
+32561,
+48400,
+28574,
+3322,
+356,
+4774,
+39433,
+45490,
+6301,
+51738,
+14046,
+32072,
+25396,
+6380,
+47133,
+59313,
+51626,
+17655,
+37955,
+35192,
+57726,
+62408,
+62500,
+58436,
+62095,
+3734,
+8827,
+10372,
+51632,
+28730,
+29657,
+11958,
+45256,
+1187,
+54560,
+59991,
+54038,
+28677,
+55965,
+405,
+3068,
+29577,
+36811,
+21509,
+59941,
+29601,
+43237,
+18361,
+49773,
+48712,
+63228,
+39840,
+27666,
+52640,
+21950,
+8919,
+4064,
+8836,
+24074,
+36405,
+59196,
+56307,
+41330,
+16059,
+21274,
+20711,
+7497,
+40671,
+39397,
+10895,
+39079,
+57745,
+61433,
+16533,
+41030,
+10594,
+64072,
+7835,
+13090,
+13588,
+60884,
+43265,
+23884,
+28806,
+64016,
+29622,
+1897,
+43888,
+9395,
+49084,
+59734,
+36610,
+35443,
+62966,
+49141,
+38104,
+21118,
+16265,
+63044,
+22697,
+62001,
+44162,
+881,
+25339,
+11673,
+8304,
+15097,
+31311,
+11925,
+15800,
+52307,
+9810,
+38217,
+10542,
+49923,
+28935,
+41931,
+44665,
+50902,
+52161,
+39333,
+37768,
+10440,
+11381,
+17200,
+49570,
+11753,
+8727,
+15747,
+60465,
+56879,
+3173,
+38138,
+61273,
+30917,
+46077,
+48870,
+31810,
+55408,
+33436,
+38235,
+61049,
+15162,
+52766,
+49921,
+10544,
+63630,
+448,
+19268,
+64953,
+28554,
+2303,
+29157,
+27029,
+10822,
+36763,
+12139,
+48981,
+57094,
+28655,
+41865,
+26965,
+10697,
+25798,
+59454,
+34666,
+6767,
+60125,
+55897,
+14388,
+14125,
+56273,
+17986,
+24985,
+43639,
+29365,
+10537,
+29548,
+38169,
+58246,
+19752,
+5920,
+31801,
+55265,
+54914,
+46948,
+46862,
+2026,
+8181,
+64729,
+31998,
+4302,
+20151,
+51440,
+34513,
+29625,
+51294,
+1653,
+38713,
+18966,
+50884,
+13290,
+63896,
+32250,
+58826,
+56523,
+34002,
+11176,
+41386,
+22327,
+62277,
+29924,
+19926,
+16909,
+8384,
+3229,
+28468,
+1350,
+60687,
+3907,
+53145,
+36136,
+13659,
+55813,
+60267,
+6444,
+14471,
+59746,
+20794,
+1602,
+19380,
+28710,
+54385,
+19102,
+31742,
+53811,
+4903,
+13806,
+30821,
+22593,
+37776,
+42796,
+13509,
+19177,
+4939,
+26176,
+64151,
+2225,
+47137,
+57292,
+19899,
+15720,
+60548,
+8134,
+32411,
+38882,
+47960,
+26576,
+59965,
+19388,
+17439,
+21169,
+40998,
+56572,
+29092,
+63220,
+19215,
+33837,
+30732,
+50502,
+53877,
+34869,
+25511,
+25384,
+5340,
+3584,
+31956,
+9773,
+56962,
+7209,
+16208,
+40389,
+61389,
+42781,
+31256,
+19209,
+37266,
+22342,
+60223,
+24030,
+8548,
+28672,
+14563,
+32765,
+52431,
+5462,
+6899,
+52934,
+13902,
+14746,
+16733,
+52187,
+5372,
+4819,
+55925,
+58344,
+57437,
+17067,
+54084,
+17278,
+19045,
+53381,
+30017,
+20421,
+44746,
+41823,
+33799,
+51719,
+2273,
+61532,
+25584,
+13175,
+35540,
+23001,
+36274,
+44577,
+33484,
+42488,
+18879,
+49546,
+31876,
+18122,
+55380,
+12023,
+35413,
+2020,
+24549,
+59326,
+43133,
+50996,
+27257,
+54745,
+37285,
+29567,
+42276,
+47976,
+34811,
+26668,
+19336,
+38482,
+6108,
+1460,
+39386,
+39040,
+6014,
+47484,
+47324,
+21013,
+28790,
+12680,
+48416,
+22537,
+20574,
+33440,
+40249,
+39035,
+4764,
+26037,
+31954,
+3586,
+59801,
+41563,
+63515,
+40990,
+43962,
+8364,
+13696,
+36571,
+21772,
+31567,
+24185,
+28499,
+4538,
+5531,
+2477,
+12352,
+27366,
+7293,
+7216,
+10662,
+42207,
+9294,
+21245,
+13990,
+49176,
+31756,
+17592,
+60211,
+17256,
+59249,
+18775,
+8605,
+10076,
+43094,
+3298,
+46010,
+17028,
+1729,
+36482,
+64647,
+33691,
+39529,
+4101,
+19317,
+2637,
+17615,
+53101,
+4465,
+53901,
+18544,
+38938,
+6671,
+21385,
+59835,
+61526,
+27082,
+20035,
+26957,
+22170,
+30285,
+52457,
+8770,
+59665,
+23789,
+7585,
+31249,
+9906,
+31205,
+64049,
+57120,
+15443,
+39487,
+58423,
+61097,
+18574,
+60415,
+27772,
+20526,
+50711,
+54053,
+39370,
+27129,
+60110,
+27141,
+25574,
+19279,
+13982,
+29566,
+37401,
+54746,
+32235,
+41477,
+42452,
+11309,
+49852,
+40947,
+44125,
+13420,
+63072,
+16997,
+13669,
+17754,
+53645,
+23287,
+64190,
+12417,
+22341,
+37457,
+19210,
+24652,
+31540,
+58704,
+29767,
+14657,
+64997,
+12160,
+62865,
+51086,
+577,
+51138,
+49593,
+57629,
+56114,
+29862,
+45666,
+21397,
+9468,
+18889,
+15138,
+40460,
+7609,
+7347,
+51990,
+61653,
+17284,
+44620,
+21831,
+3198,
+48575,
+8637,
+4925,
+39262,
+44947,
+55438,
+16713,
+9839,
+5549,
+35698,
+32831,
+52637,
+55915,
+35811,
+51046,
+37211,
+60221,
+22344,
+32507,
+60129,
+49781,
+5521,
+12261,
+60220,
+37220,
+51047,
+38661,
+20490,
+18014,
+44831,
+15533,
+53687,
+19885,
+33600,
+41370,
+37830,
+44961,
+32099,
+27304,
+62917,
+30655,
+24494,
+36142,
+2652,
+34017,
+29452,
+26538,
+36954,
+48146,
+8130,
+35651,
+12297,
+61781,
+9897,
+45085,
+5311,
+20543,
+46758,
+61486,
+9338,
+29173,
+43431,
+53492,
+10552,
+10279,
+53253,
+63169,
+47624,
+15101,
+231,
+54521,
+55346,
+41079,
+4393,
+24775,
+17483,
+27503,
+61461,
+16920,
+36753,
+15598,
+53791,
+39379,
+52612,
+2288,
+30207,
+55543,
+51418,
+51310,
+30549,
+19572,
+16813,
+1221,
+30517,
+31146,
+39691,
+33567,
+17871,
+48292,
+14713,
+36502,
+41487,
+9430,
+9695,
+29640,
+59241,
+64778,
+12019,
+37802,
+26290,
+173,
+29003,
+24931,
+6428,
+63173,
+16725,
+54751,
+5786,
+57067,
+33021,
+36195,
+54075,
+36981,
+63236,
+37813,
+42690,
+59052,
+27645,
+47780,
+53797,
+35145,
+24617,
+40937,
+34761,
+53670,
+23800,
+41618,
+10142,
+59193,
+26813,
+48935,
+38401,
+33674,
+29968,
+1827,
+52237,
+13341,
+18662,
+8931,
+26951,
+15274,
+39143,
+51342,
+45550,
+58478,
+59586,
+48212,
+32970,
+9872,
+60752,
+53469,
+57538,
+845,
+22234,
+9147,
+62307,
+28010,
+64188,
+23289,
+122,
+50619,
+65270,
+19465,
+52135,
+25314,
+29237,
+52813,
+18380,
+21647,
+56144,
+13615,
+7784,
+826,
+11888,
+51542,
+3490,
+14813,
+50085,
+51774,
+8511,
+15777,
+38053,
+57342,
+48062,
+53465,
+64463,
+41351,
+28175,
+61000,
+5997,
+33141,
+22724,
+52843,
+12305,
+50131,
+53826,
+45709,
+45312,
+7464,
+7358,
+60533,
+8555,
+44803,
+30492,
+50403,
+36102,
+24215,
+54672,
+38989,
+15823,
+6698,
+57780,
+19123,
+2417,
+57705,
+23911,
+47671,
+29356,
+41640,
+16015,
+42758,
+53963,
+65034,
+61752,
+61364,
+61061,
+21998,
+22654,
+13794,
+33532,
+55551,
+61613,
+43787,
+65312,
+55883,
+55904,
+59217,
+32951,
+51252,
+17101,
+58667,
+583,
+27944,
+63235,
+37113,
+54076,
+49108,
+2997,
+56466,
+51643,
+63052,
+42294,
+54696,
+4880,
+32172,
+28670,
+8550,
+51796,
+51262,
+52898,
+16476,
+17136,
+27796,
+62216,
+6456,
+33820,
+59031,
+1885,
+54592,
+38704,
+48145,
+37188,
+26539,
+12133,
+27390,
+22389,
+53458,
+38684,
+39352,
+16598,
+44557,
+50893,
+11123,
+2010,
+43084,
+27203,
+32796,
+14948,
+52079,
+17660,
+3838,
+21767,
+11252,
+19983,
+47596,
+9890,
+14366,
+58951,
+5445,
+59870,
+24534,
+1585,
+48224,
+61900,
+30192,
+23348,
+48231,
+16710,
+703,
+6052,
+12344,
+33887,
+20549,
+61306,
+47984,
+60038,
+62403,
+60162,
+46233,
+20956,
+7597,
+48192,
+44011,
+33042,
+1911,
+18956,
+32709,
+61617,
+1389,
+63322,
+55444,
+42439,
+15556,
+4180,
+56086,
+13365,
+61179,
+11884,
+58965,
+21973,
+40685,
+62376,
+8442,
+27953,
+23673,
+52801,
+4823,
+48162,
+49729,
+45840,
+30610,
+46006,
+40293,
+32493,
+63628,
+10546,
+3692,
+33364,
+28567,
+56988,
+28085,
+50975,
+18351,
+60742,
+11498,
+27831,
+9573,
+41819,
+20582,
+64102,
+18419,
+48862,
+36134,
+53147,
+20235,
+16314,
+44481,
+35118,
+16326,
+47222,
+61140,
+49601,
+35523,
+29410,
+58062,
+57199,
+6730,
+51196,
+41165,
+64820,
+55561,
+5763,
+30757,
+22265,
+5614,
+60204,
+1267,
+46156,
+28025,
+11153,
+39550,
+43826,
+6603,
+23193,
+34414,
+48900,
+14384,
+31405,
+4306,
+30449,
+29752,
+65375,
+6514,
+21508,
+37701,
+29578,
+14744,
+13904,
+27823,
+35881,
+14304,
+54648,
+36062,
+64424,
+2771,
+14021,
+25740,
+4123,
+35042,
+33814,
+64536,
+34539,
+11562,
+63900,
+55403,
+33620,
+54045,
+1712,
+63319,
+20073,
+1493,
+22066,
+34250,
+49314,
+21813,
+45532,
+50936,
+35584,
+43130,
+20859,
+53326,
+55270,
+12839,
+35490,
+8318,
+62245,
+14359,
+56926,
+49835,
+3574,
+42128,
+12138,
+37588,
+10823,
+46750,
+18529,
+58475,
+26407,
+27342,
+60368,
+39554,
+15597,
+37156,
+16921,
+38269,
+2181,
+62985,
+44523,
+28152,
+52858,
+45941,
+13386,
+44208,
+33086,
+24796,
+49720,
+18627,
+36636,
+289,
+43617,
+59878,
+47341,
+52056,
+41652,
+27877,
+1522,
+50520,
+50450,
+1995,
+59004,
+44699,
+24886,
+31886,
+18848,
+59203,
+21619,
+46177,
+64475,
+33171,
+1544,
+53788,
+24675,
+21134,
+29050,
+35956,
+52260,
+26278,
+19805,
+14610,
+20356,
+34912,
+63139,
+22995,
+51200,
+56171,
+25488,
+57917,
+45298,
+45101,
+3888,
+16499,
+49543,
+62481,
+902,
+1888,
+20297,
+55248,
+8862,
+18916,
+9795,
+60701,
+17717,
+24410,
+61476,
+6248,
+16068,
+58054,
+5423,
+30945,
+13468,
+5402,
+55628,
+39814,
+46095,
+56099,
+41558,
+13719,
+17546,
+311,
+53985,
+52914,
+8210,
+23004,
+9140,
+1650,
+27232,
+11059,
+35039,
+1559,
+50120,
+21559,
+11166,
+36147,
+18133,
+26257,
+25791,
+21795,
+17155,
+57046,
+7234,
+12891,
+6147,
+5768,
+61028,
+63430,
+38122,
+22681,
+12330,
+288,
+36738,
+18628,
+24321,
+31433,
+24605,
+60837,
+47114,
+31873,
+2199,
+43881,
+52591,
+40437,
+10080,
+59297,
+27407,
+44919,
+48487,
+32560,
+37744,
+12525,
+38462,
+46301,
+20042,
+46112,
+2157,
+35442,
+37652,
+59735,
+6418,
+45006,
+11707,
+34891,
+64751,
+15613,
+27909,
+23607,
+18037,
+26622,
+20391,
+29546,
+10539,
+14010,
+23950,
+2896,
+48345,
+11110,
+60778,
+27618,
+25652,
+17266,
+33614,
+44673,
+21943,
+9776,
+33718,
+2759,
+34028,
+14559,
+40480,
+55324,
+33315,
+34176,
+53766,
+8949,
+21771,
+37366,
+13697,
+11364,
+7486,
+8273,
+54932,
+40118,
+49565,
+19111,
+60000,
+9081,
+41934,
+19330,
+40979,
+18605,
+54904,
+51379,
+46294,
+17906,
+48527,
+6636,
+48544,
+10488,
+34897,
+34039,
+29329,
+20252,
+920,
+59703,
+63010,
+51865,
+57561,
+35354,
+38971,
+44129,
+748,
+65363,
+28413,
+40607,
+53713,
+50261,
+26396,
+29784,
+61606,
+46644,
+34674,
+54005,
+16273,
+59445,
+20638,
+17037,
+11329,
+46062,
+12611,
+9817,
+45172,
+45324,
+12397,
+7946,
+16544,
+22730,
+57716,
+38108,
+62286,
+25752,
+57392,
+21109,
+6623,
+41486,
+37135,
+14714,
+25089,
+50657,
+48351,
+46325,
+40887,
+61026,
+5770,
+20306,
+26991,
+625,
+39051,
+24256,
+44402,
+28825,
+29582,
+20914,
+24906,
+64646,
+37335,
+1730,
+34265,
+23765,
+30591,
+21666,
+10394,
+47431,
+5076,
+26696,
+3725,
+15497,
+1443,
+34358,
+31214,
+62374,
+40687,
+33686,
+22691,
+52348,
+38380,
+48729,
+57903,
+10433,
+30144,
+13628,
+43952,
+18702,
+53116,
+21567,
+31314,
+2278,
+35372,
+11966,
+55527,
+14930,
+8964,
+28058,
+2318,
+651,
+8947,
+53768,
+13584,
+21435,
+33916,
+17177,
+54020,
+4844,
+18979,
+63373,
+55206,
+11148,
+4251,
+21225,
+9026,
+53508,
+14075,
+7958,
+55031,
+5909,
+63797,
+15754,
+60018,
+14479,
+20427,
+12529,
+15551,
+9175,
+55946,
+18432,
+48440,
+14043,
+26213,
+49155,
+5042,
+26811,
+59195,
+37684,
+24075,
+3390,
+52821,
+23283,
+29299,
+34163,
+13362,
+22821,
+52538,
+26719,
+2829,
+43839,
+47170,
+47771,
+6741,
+59482,
+43465,
+41204,
+25780,
+48557,
+24326,
+33870,
+7164,
+63233,
+27946,
+38509,
+52337,
+21322,
+14588,
+38688,
+24448,
+9391,
+51924,
+62317,
+35630,
+42311,
+63780,
+20416,
+43412,
+56198,
+28362,
+22107,
+26399,
+47349,
+16642,
+13425,
+52373,
+55401,
+63902,
+35176,
+38613,
+39090,
+36001,
+60927,
+2205,
+6626,
+50249,
+25072,
+59913,
+339,
+41058,
+48550,
+31095,
+22023,
+64803,
+21191,
+30781,
+50560,
+20722,
+51158,
+43627,
+55535,
+27550,
+36296,
+4858,
+58929,
+59636,
+38068,
+52098,
+8882,
+58293,
+29399,
+51880,
+2223,
+64153,
+55634,
+12980,
+14415,
+61251,
+22591,
+30823,
+32542,
+49844,
+43716,
+6810,
+32986,
+50391,
+62907,
+21963,
+9970,
+62015,
+24012,
+10776,
+40222,
+28477,
+12725,
+56912,
+4857,
+36331,
+27551,
+58157,
+50466,
+65351,
+43220,
+22435,
+45888,
+32019,
+17425,
+49197,
+48158,
+7140,
+43365,
+35254,
+64800,
+19858,
+62690,
+33184,
+22817,
+42891,
+44576,
+37419,
+23002,
+8212,
+15306,
+38800,
+26080,
+13128,
+51581,
+18326,
+22333,
+18074,
+45498,
+44498,
+15256,
+15495,
+3727,
+47554,
+59375,
+10298,
+19944,
+46424,
+5539,
+64982,
+5561,
+4019,
+18472,
+33197,
+22279,
+60458,
+24045,
+18706,
+33556,
+48707,
+54081,
+36228,
+42253,
+29137,
+31375,
+15744,
+4717,
+30797,
+39413,
+62254,
+51540,
+11890,
+42252,
+36240,
+54082,
+17069,
+50653,
+13665,
+46907,
+41218,
+33039,
+18950,
+49516,
+9357,
+16914,
+53190,
+46550,
+27571,
+22204,
+14531,
+12146,
+30231,
+31497,
+28991,
+45286,
+54298,
+32163,
+31333,
+64373,
+41983,
+46358,
+60600,
+56921,
+35162,
+26016,
+54074,
+37115,
+33022,
+45452,
+52443,
+11834,
+42906,
+6045,
+60736,
+1735,
+5475,
+56550,
+64568,
+13474,
+17865,
+28529,
+23898,
+18928,
+24950,
+33677,
+3712,
+2578,
+433,
+21897,
+27969,
+55001,
+33544,
+212,
+27385,
+22977,
+58747,
+63839,
+19581,
+3398,
+618,
+22129,
+17749,
+27593,
+19664,
+58816,
+10602,
+15540,
+33809,
+30866,
+9617,
+57845,
+55517,
+23458,
+18132,
+36653,
+11167,
+3387,
+5514,
+2651,
+37193,
+24495,
+20080,
+7243,
+29214,
+13658,
+37522,
+53146,
+36853,
+48863,
+62020,
+21157,
+53498,
+58760,
+30013,
+28695,
+7476,
+12877,
+55996,
+2110,
+684,
+4115,
+14793,
+48363,
+50021,
+31648,
+44990,
+15902,
+24105,
+8622,
+29794,
+46969,
+48101,
+14518,
+29761,
+52993,
+46843,
+23512,
+9107,
+24214,
+37020,
+50404,
+38075,
+24981,
+48048,
+64395,
+60426,
+64217,
+30310,
+47657,
+25990,
+19094,
+62528,
+61333,
+52783,
+31549,
+31519,
+28347,
+39000,
+50480,
+34959,
+4412,
+7471,
+27656,
+61121,
+48868,
+46079,
+40781,
+18300,
+55819,
+11804,
+2044,
+8109,
+12470,
+30092,
+2573,
+40306,
+43191,
+33032,
+64423,
+36803,
+54649,
+34446,
+54254,
+518,
+12376,
+23384,
+41767,
+21593,
+26129,
+7884,
+17979,
+55671,
+14364,
+9892,
+25817,
+25205,
+46815,
+26701,
+57171,
+51843,
+19586,
+24500,
+63473,
+39679,
+60043,
+26285,
+57622,
+37821,
+15920,
+20373,
+15528,
+22415,
+31065,
+51372,
+11786,
+43014,
+65215,
+22255,
+38120,
+63432,
+61769,
+31282,
+32582,
+24202,
+41842,
+31038,
+3563,
+40033,
+6888,
+11103,
+56856,
+25121,
+23258,
+34918,
+63999,
+42470,
+50107,
+24408,
+17719,
+60926,
+36352,
+39091,
+42136,
+5398,
+2101,
+58897,
+6378,
+25398,
+20406,
+45145,
+28940,
+54999,
+27971,
+27369,
+26563,
+32244,
+15149,
+13727,
+60807,
+49864,
+26225,
+62027,
+62475,
+24473,
+53935,
+64781,
+30556,
+12492,
+21740,
+51554,
+28251,
+28137,
+23262,
+63673,
+34546,
+47408,
+44942,
+35850,
+15479,
+34518,
+62468,
+27835,
+28727,
+50794,
+52259,
+36711,
+29051,
+44624,
+63479,
+64168,
+47477,
+31033,
+41356,
+7338,
+16959,
+19295,
+61194,
+33011,
+39700,
+48479,
+45251,
+11913,
+55708,
+12851,
+5595,
+27839,
+22301,
+23219,
+44858,
+59177,
+61869,
+43180,
+19264,
+65174,
+34942,
+63368,
+24396,
+32701,
+12320,
+17852,
+53178,
+23998,
+9933,
+23441,
+6255,
+1520,
+27879,
+63641,
+31023,
+27989,
+14244,
+27527,
+40495,
+65203,
+60421,
+34685,
+34111,
+11684,
+4673,
+11681,
+62200,
+22770,
+26891,
+55841,
+28406,
+6297,
+11791,
+34598,
+32134,
+57417,
+42460,
+33897,
+45375,
+10725,
+27026,
+13202,
+51416,
+55545,
+43032,
+14303,
+36806,
+27824,
+53580,
+35735,
+15085,
+63544,
+32605,
+56262,
+23823,
+22627,
+9886,
+5980,
+13676,
+31780,
+53227,
+49203,
+16625,
+38903,
+39085,
+11599,
+53521,
+56167,
+61264,
+35086,
+35787,
+41880,
+10415,
+22759,
+37990,
+20117,
+15478,
+35964,
+44943,
+53034,
+24120,
+53143,
+3909,
+54861,
+50509,
+7751,
+61015,
+43069,
+42368,
+3619,
+16168,
+27251,
+49476,
+44265,
+7870,
+64339,
+26172,
+46410,
+52678,
+33952,
+24567,
+46953,
+65076,
+57987,
+2918,
+11429,
+41072,
+59706,
+9410,
+25252,
+11394,
+33290,
+32989,
+24263,
+13271,
+51045,
+37222,
+55916,
+27299,
+60912,
+1801,
+31242,
+13770,
+37826,
+15982,
+44019,
+15355,
+38211,
+62185,
+4597,
+45455,
+58296,
+24686,
+1408,
+42514,
+29826,
+6841,
+46352,
+4589,
+41879,
+35857,
+35087,
+28722,
+163,
+5196,
+7416,
+10834,
+22883,
+24175,
+27112,
+51018,
+29429,
+12545,
+24913,
+62505,
+32283,
+6832,
+28276,
+50881,
+42628,
+8535,
+9959,
+19695,
+34929,
+16480,
+17613,
+2639,
+14722,
+27350,
+19774,
+8114,
+38346,
+43428,
+39140,
+29008,
+3932,
+31928,
+63196,
+53971,
+10921,
+26217,
+20481,
+4855,
+56914,
+29425,
+18569,
+13846,
+16784,
+11980,
+53564,
+60978,
+15084,
+35878,
+53581,
+15663,
+14264,
+47795,
+18091,
+63600,
+39506,
+13690,
+48042,
+8756,
+25656,
+7309,
+46110,
+20044,
+21050,
+58418,
+5866,
+52234,
+25192,
+13034,
+43632,
+2248,
+23022,
+29745,
+58697,
+35170,
+21292,
+34904,
+59357,
+47995,
+17961,
+65403,
+38929,
+51301,
+46579,
+32830,
+37226,
+5550,
+13779,
+48954,
+40682,
+1860,
+23314,
+25856,
+5496,
+52185,
+16735,
+33212,
+63695,
+5056,
+59434,
+52596,
+61623,
+39896,
+17400,
+37773,
+41604,
+14545,
+9422,
+28287,
+19872,
+57775,
+21872,
+31171,
+55768,
+52703,
+25270,
+39026,
+55370,
+44438,
+56042,
+40846,
+48595,
+43967,
+53615,
+16098,
+45090,
+15281,
+39674,
+63346,
+40610,
+56517,
+12296,
+37185,
+8131,
+42239,
+62362,
+27401,
+22844,
+38322,
+31156,
+16370,
+11130,
+47068,
+30218,
+39498,
+33585,
+58363,
+47950,
+2755,
+48155,
+23737,
+62802,
+42310,
+36370,
+62318,
+45437,
+9744,
+33210,
+16737,
+23020,
+2250,
+37850,
+41367,
+22765,
+8024,
+43676,
+12194,
+2946,
+39808,
+3259,
+28005,
+45612,
+23010,
+1257,
+34124,
+22515,
+63713,
+21607,
+25554,
+32104,
+39017,
+57222,
+53170,
+35112,
+602,
+57604,
+13543,
+2793,
+19071,
+43989,
+56395,
+63889,
+43374,
+64364,
+63469,
+45024,
+38134,
+42695,
+43129,
+36778,
+50937,
+42383,
+15391,
+64861,
+56023,
+2426,
+40965,
+54869,
+44532,
+50458,
+39411,
+30799,
+40585,
+31862,
+28445,
+63711,
+22517,
+6337,
+27103,
+58035,
+42507,
+44880,
+33807,
+15542,
+32974,
+17139,
+27122,
+26109,
+12383,
+26763,
+60359,
+31915,
+11494,
+31290,
+6965,
+40296,
+58909,
+15290,
+22601,
+27556,
+8917,
+21952,
+23000,
+37421,
+13176,
+6969,
+57950,
+34290,
+5188,
+9183,
+43395,
+14573,
+12111,
+62167,
+8891,
+58234,
+39106,
+5300,
+6776,
+29409,
+36843,
+49602,
+12155,
+28106,
+45117,
+57254,
+38831,
+51714,
+12969,
+14215,
+60785,
+38639,
+35200,
+26822,
+46573,
+18636,
+33995,
+6835,
+57993,
+3989,
+51133,
+55755,
+33530,
+13796,
+39327,
+18080,
+1237,
+11065,
+1298,
+12059,
+6158,
+26518,
+8317,
+36772,
+12840,
+9989,
+63524,
+25636,
+34210,
+14903,
+3875,
+56492,
+50690,
+7819,
+27431,
+44421,
+12050,
+27062,
+52001,
+19282,
+521,
+31514,
+41723,
+45717,
+26938,
+4200,
+18737,
+2138,
+48135,
+27766,
+21720,
+42933,
+41452,
+54324,
+45027,
+49832,
+32348,
+57258,
+9636,
+38839,
+61814,
+21113,
+48997,
+42563,
+5330,
+9172,
+3609,
+53066,
+29469,
+62965,
+37651,
+36611,
+2158,
+63231,
+7166,
+50770,
+34678,
+44973,
+57414,
+18342,
+19591,
+30351,
+31027,
+23516,
+1478,
+3413,
+56650,
+20851,
+17479,
+8823,
+50810,
+56695,
+52269,
+32441,
+13550,
+12677,
+19598,
+53296,
+681,
+2019,
+37409,
+12024,
+56807,
+60823,
+62298,
+43861,
+385,
+15791,
+16406,
+19446,
+52651,
+51688,
+61255,
+26424,
+64665,
+25659,
+32675,
+20902,
+50915,
+55103,
+29079,
+51910,
+32125,
+65487,
+2676,
+33100,
+23752,
+49400,
+65274,
+44276,
+21971,
+58967,
+19705,
+61597,
+17787,
+44202,
+55035,
+33378,
+8750,
+41176,
+11965,
+36450,
+2279,
+23945,
+47669,
+23913,
+14830,
+328,
+997,
+52470,
+18364,
+3045,
+58029,
+55219,
+1227,
+12009,
+1709,
+53772,
+38970,
+36539,
+57562,
+2968,
+20949,
+15245,
+8508,
+55171,
+22049,
+3636,
+28616,
+42190,
+13950,
+41688,
+49882,
+50169,
+44197,
+42212,
+8956,
+4735,
+14185,
+22033,
+65422,
+45417,
+63248,
+28387,
+9995,
+44643,
+666,
+61706,
+30237,
+58882,
+34109,
+34687,
+43533,
+18468,
+29897,
+26040,
+4526,
+32299,
+61831,
+30638,
+31500,
+18977,
+4846,
+18863,
+53090,
+58582,
+53390,
+29327,
+34041,
+17318,
+54250,
+49447,
+3830,
+30212,
+6656,
+64967,
+53329,
+61855,
+31906,
+23926,
+64226,
+8856,
+13653,
+53620,
+32775,
+4370,
+44472,
+57545,
+59469,
+10760,
+41780,
+26074,
+42661,
+10464,
+39223,
+45650,
+31939,
+22229,
+49509,
+23620,
+19037,
+49961,
+50819,
+48429,
+51279,
+24070,
+48015,
+57481,
+48631,
+3808,
+56092,
+46600,
+14146,
+50320,
+12539,
+34144,
+53778,
+60215,
+64799,
+36282,
+43366,
+28223,
+13081,
+65022,
+49090,
+3185,
+31453,
+43925,
+18154,
+13621,
+49262,
+6094,
+16646,
+12410,
+14776,
+9461,
+58674,
+39455,
+52411,
+20109,
+10863,
+26550,
+8929,
+18664,
+38635,
+1319,
+29443,
+17881,
+4322,
+41009,
+49610,
+15840,
+19454,
+9477,
+8264,
+33725,
+10131,
+51961,
+35012,
+54603,
+13968,
+31233,
+6386,
+17260,
+64342,
+43857,
+54921,
+53048,
+43911,
+65241,
+40434,
+41474,
+26821,
+35511,
+38640,
+52319,
+7057,
+5953,
+22863,
+62835,
+57725,
+37724,
+37956,
+42505,
+58037,
+26464,
+45945,
+30105,
+34511,
+51442,
+32816,
+48647,
+54956,
+8221,
+33455,
+9343,
+38612,
+36355,
+63903,
+47719,
+34765,
+64160,
+21291,
+35709,
+58698,
+59337,
+49648,
+57350,
+44138,
+56071,
+26015,
+36198,
+56922,
+50314,
+19328,
+41936,
+25902,
+13327,
+714,
+14025,
+11145,
+19481,
+52072,
+12535,
+7158,
+57894,
+54295,
+24616,
+37105,
+53798,
+32803,
+33231,
+48217,
+11289,
+43613,
+33052,
+24419,
+5135,
+55955,
+38081,
+31348,
+43146,
+12081,
+50301,
+12119,
+49927,
+6933,
+17458,
+6480,
+11630,
+6402,
+46838,
+12202,
+48132,
+16325,
+36848,
+44482,
+32259,
+41308,
+51664,
+601,
+35600,
+53171,
+49807,
+21855,
+2115,
+24438,
+14313,
+6269,
+2750,
+17630,
+27664,
+39842,
+61939,
+46316,
+21916,
+30932,
+3393,
+55748,
+155,
+42432,
+18334,
+17580,
+6860,
+61230,
+28721,
+35786,
+35858,
+61265,
+14916,
+269,
+56557,
+59458,
+9328,
+42292,
+63054,
+20412,
+45883,
+50165,
+59070,
+61129,
+42577,
+63203,
+45846,
+25853,
+39345,
+6059,
+34316,
+17565,
+20756,
+6041,
+62208,
+60477,
+286,
+12332,
+458,
+25559,
+26444,
+23403,
+54239,
+55525,
+11968,
+50190,
+40431,
+55062,
+59600,
+47862,
+20733,
+28515,
+15135,
+33813,
+36797,
+4124,
+1558,
+36658,
+11060,
+57000,
+55580,
+27036,
+6856,
+13009,
+64596,
+52082,
+50033,
+44938,
+22798,
+10519,
+40077,
+14638,
+58084,
+57790,
+46459,
+64315,
+28395,
+34392,
+1413,
+53441,
+52138,
+3880,
+59729,
+54602,
+35215,
+51962,
+32615,
+62100,
+46369,
+8344,
+21543,
+2608,
+26678,
+62912,
+44733,
+9115,
+25228,
+5612,
+22267,
+44951,
+24729,
+40635,
+32555,
+39169,
+25923,
+52061,
+47674,
+14226,
+28391,
+45966,
+59567,
+51034,
+15678,
+26857,
+755,
+29562,
+53046,
+54923,
+47488,
+10223,
+33340,
+14940,
+4383,
+12932,
+45922,
+1949,
+774,
+46342,
+13751,
+57384,
+18580,
+8787,
+12388,
+45304,
+54288,
+18174,
+4411,
+36082,
+50481,
+47764,
+10426,
+54237,
+23405,
+15,
+4097,
+54967,
+47016,
+2149,
+38805,
+41771,
+29059,
+43872,
+482,
+63367,
+35927,
+65175,
+59184,
+21121,
+1450,
+272,
+61697,
+43772,
+63933,
+46159,
+20947,
+2970,
+16479,
+35764,
+19696,
+28662,
+19532,
+14633,
+13378,
+13369,
+60896,
+62812,
+40300,
+63998,
+36008,
+23259,
+17563,
+34318,
+3467,
+63138,
+36705,
+20357,
+27696,
+12691,
+9404,
+45525,
+16802,
+59356,
+35707,
+21293,
+39442,
+54796,
+3705,
+30223,
+34038,
+36548,
+10489,
+57185,
+7323,
+45825,
+64750,
+36605,
+11708,
+20800,
+45191,
+24006,
+48143,
+38706,
+57250,
+2619,
+28342,
+41044,
+10584,
+16458,
+51510,
+54394,
+42463,
+40352,
+42052,
+5948,
+47482,
+6016,
+25510,
+37472,
+53878,
+52389,
+1656,
+54424,
+3581,
+7228,
+16936,
+39009,
+52439,
+46633,
+31114,
+55213,
+23091,
+57072,
+10276,
+49251,
+24598,
+30778,
+50912,
+26240,
+16010,
+49472,
+20363,
+20215,
+44586,
+50294,
+45514,
+38753,
+55065,
+41291,
+26997,
+23113,
+44077,
+32713,
+42089,
+52728,
+21978,
+3114,
+8059,
+43527,
+33572,
+14868,
+6705,
+46432,
+2742,
+12664,
+41569,
+15582,
+32473,
+9125,
+26493,
+4367,
+3857,
+20063,
+62301,
+49878,
+26667,
+37397,
+47977,
+57379,
+57375,
+10529,
+31699,
+28062,
+15036,
+22098,
+18886,
+17670,
+17840,
+57710,
+52717,
+11954,
+49876,
+62303,
+56720,
+50306,
+40825,
+22942,
+20765,
+40691,
+48094,
+16575,
+13416,
+21889,
+14291,
+12460,
+14175,
+59773,
+28704,
+43273,
+6963,
+31292,
+39110,
+45368,
+56436,
+13879,
+45485,
+56226,
+15537,
+25422,
+62161,
+63635,
+64159,
+35173,
+47720,
+5471,
+53669,
+37102,
+40938,
+30686,
+14626,
+44345,
+29199,
+14978,
+19029,
+33107,
+63125,
+25471,
+47098,
+39395,
+40673,
+45331,
+15593,
+16463,
+3699,
+20778,
+7804,
+60819,
+29303,
+26125,
+29965,
+24953,
+12258,
+63854,
+58018,
+61467,
+54489,
+19970,
+43502,
+117,
+62761,
+59681,
+25880,
+51284,
+40150,
+6153,
+9511,
+12520,
+59257,
+168,
+54292,
+51730,
+28032,
+42945,
+29223,
+32996,
+24667,
+27092,
+30971,
+53919,
+38741,
+38406,
+5429,
+10779,
+22331,
+18328,
+4542,
+780,
+31301,
+53740,
+31775,
+4095,
+17,
+61897,
+63665,
+64277,
+61738,
+23624,
+47085,
+64991,
+43532,
+35322,
+34110,
+35906,
+60422,
+33359,
+43600,
+61185,
+65297,
+44972,
+35437,
+50771,
+62462,
+54004,
+36526,
+46645,
+31727,
+29384,
+55921,
+2707,
+61690,
+6766,
+37578,
+59455,
+19120,
+61922,
+61349,
+57070,
+23093,
+65433,
+45177,
+24379,
+53315,
+62063,
+23740,
+4208,
+2975,
+14117,
+14229,
+45794,
+16284,
+40941,
+45745,
+10495,
+54355,
+31606,
+48921,
+65467,
+15198,
+43849,
+59893,
+33356,
+17652,
+58655,
+17021,
+18524,
+6981,
+7407,
+60168,
+37885,
+64744,
+16468,
+14307,
+5154,
+1819,
+61685,
+937,
+4263,
+27928,
+19461,
+3951,
+49104,
+54558,
+1189,
+21094,
+5152,
+14309,
+64132,
+24414,
+1727,
+17030,
+65234,
+43620,
+30440,
+62145,
+12718,
+44716,
+57061,
+24762,
+32133,
+35894,
+11792,
+53362,
+3624,
+40191,
+60113,
+28860,
+52070,
+19483,
+65501,
+3746,
+57230,
+44100,
+31623,
+5126,
+55478,
+55093,
+14843,
+41189,
+14787,
+6139,
+15510,
+61748,
+21368,
+44502,
+5357,
+31106,
+64828,
+55078,
+43661,
+51388,
+38444,
+30827,
+11619,
+26244,
+22570,
+54063,
+63910,
+10682,
+4191,
+9522,
+41224,
+18717,
+34135,
+59811,
+44383,
+1231,
+31546,
+6493,
+25140,
+42077,
+47407,
+35967,
+63674,
+12144,
+14533,
+62359,
+64816,
+11561,
+36794,
+64537,
+20141,
+42752,
+43444,
+49575,
+23808,
+51062,
+55237,
+45595,
+17515,
+63656,
+58810,
+54089,
+22965,
+13085,
+6126,
+64939,
+22176,
+17605,
+62467,
+35962,
+15480,
+22802,
+1895,
+29624,
+37549,
+51441,
+35185,
+30106,
+42022,
+22078,
+48592,
+24442,
+18059,
+29587,
+24182,
+4291,
+22432,
+23694,
+29521,
+61975,
+52968,
+64323,
+59919,
+12287,
+21852,
+56601,
+63939,
+14200,
+58071,
+58181,
+61056,
+48671,
+54121,
+38795,
+55350,
+33696,
+14753,
+3408,
+54330,
+24804,
+4910,
+53274,
+5319,
+38350,
+43121,
+29508,
+14166,
+25768,
+38694,
+1234,
+39164,
+51947,
+4655,
+40103,
+48152,
+6753,
+20058,
+7594,
+27467,
+13441,
+14691,
+3806,
+48633,
+41313,
+47757,
+31260,
+16282,
+45796,
+52414,
+61947,
+54253,
+36060,
+54650,
+56345,
+16986,
+29195,
+43515,
+20310,
+55474,
+62848,
+59272,
+24682,
+47329,
+19183,
+60950,
+60179,
+30965,
+710,
+57150,
+51932,
+44317,
+20185,
+14322,
+18903,
+30679,
+10325,
+52965,
+42104,
+44234,
+26481,
+20653,
+4694,
+48899,
+36821,
+23194,
+48749,
+55083,
+58610,
+26846,
+12192,
+43678,
+58453,
+62133,
+52654,
+42161,
+57553,
+23505,
+51578,
+20881,
+5691,
+16178,
+40792,
+48775,
+58058,
+1412,
+35019,
+28396,
+25847,
+31626,
+60256,
+25409,
+31320,
+17636,
+47241,
+9519,
+27214,
+29893,
+49168,
+27224,
+59816,
+63785,
+21934,
+44752,
+40208,
+15358,
+47288,
+49301,
+42375,
+9418,
+31611,
+43664,
+29866,
+9770,
+24543,
+12906,
+38002,
+4959,
+21578,
+31213,
+36469,
+1444,
+59778,
+9687,
+50987,
+42871,
+49727,
+48164,
+58140,
+57306,
+14709,
+2242,
+40527,
+12991,
+10337,
+47160,
+31696,
+14191,
+52317,
+38642,
+57645,
+20432,
+10996,
+10752,
+39812,
+55630,
+28231,
+1489,
+25644,
+32758,
+20697,
+44238,
+16116,
+43958,
+31178,
+29069,
+3164,
+24083,
+11215,
+3466,
+34915,
+17564,
+35066,
+6060,
+63862,
+33161,
+61291,
+14467,
+18324,
+51583,
+10898,
+38040,
+61722,
+51568,
+30325,
+59152,
+812,
+44176,
+7018,
+16593,
+16561,
+31986,
+63735,
+29731,
+51768,
+25815,
+9894,
+5187,
+35536,
+57951,
+28229,
+55632,
+64155,
+15709,
+973,
+26310,
+22755,
+28078,
+46329,
+52217,
+17779,
+31085,
+49256,
+57837,
+46666,
+58894,
+18482,
+40260,
+41007,
+4324,
+1486,
+8419,
+23764,
+36480,
+1731,
+41813,
+5924,
+56667,
+22827,
+53885,
+65493,
+25911,
+3767,
+29517,
+32956,
+41896,
+49589,
+49313,
+36783,
+22067,
+54042,
+29987,
+58573,
+30760,
+26428,
+7956,
+14077,
+24390,
+54269,
+15933,
+53384,
+64756,
+30837,
+11798,
+32489,
+13179,
+16972,
+44722,
+52600,
+25084,
+22856,
+50439,
+27901,
+53613,
+43969,
+29085,
+63260,
+13333,
+9256,
+50175,
+40127,
+31151,
+47902,
+17398,
+39898,
+38746,
+59317,
+14902,
+35485,
+25637,
+26793,
+51892,
+44360,
+33373,
+29333,
+1165,
+18493,
+366,
+58395,
+22174,
+64941,
+23352,
+23552,
+19181,
+47331,
+13112,
+58516,
+17991,
+27455,
+30404,
+22392,
+25665,
+20135,
+32942,
+20094,
+7251,
+11939,
+48045,
+64028,
+52109,
+41382,
+53765,
+36575,
+33316,
+44927,
+58631,
+16250,
+23855,
+33014,
+20473,
+22712,
+51408,
+19368,
+5655,
+13361,
+36399,
+29300,
+49794,
+33369,
+16072,
+25163,
+65301,
+11714,
+45779,
+8065,
+54626,
+13619,
+18156,
+61647,
+49040,
+7625,
+13502,
+5733,
+53777,
+35258,
+12540,
+52463,
+382,
+25326,
+27329,
+18113,
+6944,
+59810,
+34555,
+18718,
+51152,
+28719,
+61232,
+42865,
+48942,
+48978,
+17729,
+43112,
+22514,
+35609,
+1258,
+3801,
+33459,
+19809,
+15626,
+12918,
+65051,
+47566,
+41712,
+42912,
+62198,
+11683,
+35905,
+34686,
+35323,
+58883,
+661,
+65439,
+41215,
+28904,
+40933,
+43077,
+10722,
+47227,
+55587,
+16489,
+46791,
+49270,
+2495,
+6749,
+33721,
+55881,
+65314,
+25094,
+4566,
+5985,
+9121,
+46535,
+62351,
+30290,
+21613,
+13297,
+61068,
+3448,
+10927,
+4229,
+17555,
+5664,
+26586,
+51029,
+53354,
+43088,
+40404,
+53905,
+59137,
+933,
+24630,
+4950,
+39580,
+64677,
+54399,
+65385,
+21702,
+17304,
+48829,
+864,
+7426,
+51437,
+6597,
+55774,
+16684,
+32143,
+49849,
+7262,
+47419,
+3944,
+63179,
+5284,
+61173,
+42030,
+13961,
+17317,
+35305,
+29328,
+36547,
+34898,
+30224,
+46561,
+22458,
+54817,
+48926,
+57990,
+33638,
+53718,
+14558,
+36580,
+2760,
+24521,
+51376,
+65015,
+30065,
+23815,
+38485,
+47261,
+33987,
+29451,
+37191,
+2653,
+55784,
+63327,
+45868,
+20897,
+57124,
+60918,
+51616,
+20938,
+9181,
+5190,
+26236,
+11219,
+11175,
+37537,
+56524,
+25970,
+42635,
+40884,
+28274,
+6834,
+35507,
+18637,
+26841,
+64305,
+30986,
+59045,
+12753,
+29450,
+34019,
+47262,
+61281,
+27525,
+14246,
+16391,
+46511,
+10170,
+22360,
+2926,
+8487,
+12239,
+64858,
+14597,
+21492,
+49990,
+43764,
+28173,
+41353,
+61248,
+19549,
+10973,
+26045,
+39991,
+42466,
+19878,
+48665,
+14576,
+63335,
+20606,
+65344,
+43351,
+47834,
+26646,
+24566,
+35828,
+52679,
+32089,
+20430,
+57647,
+54971,
+55129,
+39662,
+17116,
+64979,
+6590,
+16517,
+64558,
+59574,
+2914,
+46720,
+45918,
+39384,
+1462,
+2420,
+11181,
+30894,
+16617,
+12057,
+1300,
+56118,
+31169,
+21874,
+14151,
+55688,
+61219,
+51207,
+9647,
+56850,
+50742,
+17176,
+36438,
+21436,
+30952,
+60412,
+39689,
+31148,
+5282,
+63181,
+5205,
+62630,
+20050,
+9313,
+7734,
+18370,
+636,
+45167,
+10194,
+53247,
+45374,
+35890,
+42461,
+54396,
+10161,
+46042,
+21729,
+47029,
+15515,
+29420,
+20548,
+36914,
+12345,
+18989,
+47706,
+23845,
+43554,
+9455,
+7105,
+48688,
+62111,
+25393,
+57282,
+27275,
+19504,
+39340,
+17431,
+7163,
+36383,
+24327,
+7719,
+33559,
+32873,
+1554,
+59764,
+23652,
+43092,
+10078,
+40439,
+7963,
+49055,
+47926,
+46584,
+23042,
+64571,
+59222,
+32209,
+27577,
+20664,
+9826,
+60923,
+7687,
+43519,
+16194,
+19134,
+4361,
+18376,
+31195,
+51347,
+55007,
+30731,
+37476,
+19216,
+59945,
+3752,
+38017,
+20644,
+44596,
+59363,
+27562,
+50877,
+33496,
+4679,
+38553,
+1366,
+46457,
+57792,
+59030,
+36960,
+6457,
+18765,
+59849,
+42732,
+64535,
+36796,
+35043,
+15136,
+18891,
+30865,
+36154,
+15541,
+35561,
+44881,
+41668,
+32278,
+2344,
+41028,
+16535,
+51718,
+37427,
+41824,
+2740,
+46434,
+28028,
+11727,
+23253,
+61567,
+16216,
+23610,
+8156,
+8164,
+42887,
+57679,
+10219,
+33430,
+22061,
+16360,
+28189,
+47946,
+49096,
+5291,
+4224,
+20523,
+60100,
+20629,
+22248,
+6988,
+22750,
+56441,
+19291,
+25605,
+39250,
+7917,
+59322,
+64747,
+11796,
+30839,
+61443,
+22738,
+50352,
+51860,
+29669,
+12311,
+52981,
+26268,
+7256,
+44180,
+10039,
+6744,
+45017,
+39175,
+53883,
+22829,
+46141,
+15788,
+4405,
+25481,
+60011,
+26714,
+52200,
+43635,
+59616,
+27976,
+10273,
+2240,
+14711,
+48294,
+53485,
+38477,
+24393,
+16666,
+27931,
+10130,
+35218,
+8265,
+57332,
+55880,
+34093,
+6750,
+2758,
+36582,
+9777,
+3493,
+2008,
+11125,
+40592,
+1806,
+8379,
+38111,
+62018,
+48865,
+44797,
+46173,
+7278,
+2522,
+26860,
+19798,
+19011,
+61984,
+46626,
+15829,
+14752,
+34482,
+55351,
+2266,
+319,
+39528,
+37333,
+64648,
+55427,
+51781,
+22690,
+36465,
+40688,
+38907,
+50380,
+8645,
+20505,
+22325,
+41388,
+3711,
+36177,
+24951,
+29967,
+37093,
+38402,
+16756,
+15649,
+1563,
+6077,
+56211,
+17761,
+58819,
+18422,
+28908,
+63339,
+15990,
+28124,
+47416,
+58619,
+38497,
+55026,
+27544,
+10239,
+52437,
+39011,
+40650,
+28814,
+46690,
+51395,
+1155,
+58338,
+52691,
+11440,
+21671,
+24656,
+8028,
+17126,
+376,
+53717,
+34031,
+57991,
+6837,
+6202,
+2991,
+9135,
+3681,
+30242,
+41612,
+45735,
+610,
+61033,
+18572,
+61099,
+42555,
+5177,
+29985,
+54044,
+36790,
+55404,
+13578,
+26377,
+14538,
+44672,
+36586,
+17267,
+19968,
+54491,
+61604,
+29786,
+2342,
+32280,
+62446,
+5876,
+17453,
+18809,
+22763,
+41369,
+37202,
+19886,
+30260,
+43309,
+39243,
+9555,
+54462,
+18282,
+44328,
+52828,
+32589,
+55992,
+22792,
+57490,
+58362,
+35638,
+39499,
+28162,
+61952,
+28070,
+55765,
+40996,
+21171,
+57431,
+48331,
+13855,
+37996,
+14867,
+34828,
+43528,
+38758,
+41590,
+17870,
+37139,
+39692,
+60271,
+2142,
+58443,
+21474,
+8311,
+32872,
+33867,
+7720,
+48706,
+36243,
+18707,
+19440,
+9440,
+48975,
+31240,
+1803,
+18347,
+7777,
+8250,
+53989,
+211,
+36170,
+55002,
+52616,
+42037,
+807,
+27984,
+61236,
+46136,
+58535,
+52498,
+52695,
+55550,
+36996,
+13795,
+35501,
+55756,
+12512,
+45405,
+12372,
+33180,
+15571,
+62714,
+3740,
+54694,
+42296,
+19648,
+29249,
+48113,
+24812,
+8033,
+7824,
+3125,
+16836,
+8650,
+51970,
+11948,
+6824,
+47034,
+23149,
+11390,
+44820,
+6692,
+7952,
+55740,
+4838,
+13233,
+12218,
+4678,
+33827,
+50878,
+542,
+42244,
+63363,
+54017,
+7403,
+49437,
+20449,
+40134,
+57362,
+42487,
+37417,
+44578,
+38073,
+50406,
+63879,
+11115,
+44107,
+21969,
+44278,
+4499,
+50581,
+19489,
+49374,
+61852,
+17844,
+10993,
+26093,
+10743,
+51490,
+27805,
+40142,
+8257,
+13183,
+61089,
+19808,
+34121,
+3802,
+60730,
+9342,
+35179,
+8222,
+59170,
+27375,
+11571,
+18560,
+12857,
+31587,
+45108,
+53010,
+27077,
+55835,
+56938,
+56536,
+40248,
+37380,
+20575,
+28339,
+38234,
+37604,
+55409,
+39982,
+29933,
+26521,
+22060,
+33784,
+10220,
+4919,
+11097,
+51383,
+24358,
+60488,
+32393,
+351,
+12103,
+18459,
+46697,
+24116,
+22132,
+15739,
+50686,
+32337,
+13626,
+30146,
+51,
+61757,
+10181,
+40429,
+50192,
+52481,
+40774,
+30938,
+29644,
+8121,
+47197,
+54692,
+3742,
+7600,
+5683,
+42351,
+48852,
+31485,
+15689,
+473,
+52956,
+55572,
+49430,
+49404,
+5457,
+12789,
+11853,
+3567,
+42927,
+5181,
+65323,
+38154,
+8749,
+35376,
+55036,
+25136,
+11238,
+29332,
+34205,
+44361,
+54170,
+16071,
+34160,
+49795,
+13195,
+60656,
+28566,
+36868,
+3693,
+9883,
+42364,
+43599,
+34683,
+60423,
+17651,
+34637,
+59894,
+3358,
+63743,
+56327,
+2869,
+47110,
+24781,
+14702,
+23224,
+1759,
+54851,
+48683,
+38729,
+43139,
+14939,
+34976,
+10224,
+14730,
+49513,
+42498,
+3702,
+13965,
+20259,
+16864,
+11418,
+12197,
+6314,
+31685,
+37961,
+65008,
+57470,
+10913,
+49463,
+40376,
+60555,
+26500,
+60775,
+61959,
+44926,
+34175,
+36576,
+55325,
+41585,
+62643,
+56839,
+4236,
+12004,
+10690,
+10649,
+24142,
+48374,
+3182,
+10790,
+28254,
+24061,
+38718,
+64734,
+4332,
+21887,
+13418,
+44127,
+38973,
+29551,
+50389,
+32988,
+35816,
+11395,
+1640,
+27997,
+6712,
+39208,
+18180,
+52799,
+23675,
+54092,
+46382,
+21054,
+30421,
+24230,
+15561,
+19150,
+47127,
+2728,
+25577,
+16694,
+45608,
+57484,
+25257,
+5193,
+38927,
+65405,
+23014,
+17693,
+664,
+44645,
+23868,
+31733,
+63194,
+31930,
+3092,
+3031,
+29765,
+58706,
+19451,
+12632,
+50701,
+40918,
+24602,
+22462,
+1098,
+63749,
+57543,
+44474,
+25675,
+60405,
+21753,
+61010,
+30221,
+3707,
+6028,
+2599,
+16965,
+22363,
+48216,
+35142,
+32804,
+53867,
+40506,
+40237,
+1146,
+54339,
+38128,
+20620,
+15752,
+63799,
+3774,
+59427,
+64079,
+63199,
+42178,
+45575,
+58108,
+63694,
+35687,
+16736,
+35626,
+9745,
+27363,
+25333,
+52130,
+12687,
+14221,
+63727,
+46337,
+14850,
+32123,
+51912,
+22278,
+36248,
+18473,
+11832,
+52445,
+51951,
+44104,
+8609,
+15704,
+30080,
+8280,
+63971,
+49918,
+22816,
+36278,
+62691,
+23774,
+15570,
+33525,
+12373,
+19285,
+55911,
+27421,
+22813,
+52769,
+49173,
+1543,
+36717,
+64476,
+18982,
+58647,
+54436,
+14921,
+61818,
+12428,
+41947,
+61290,
+34313,
+63863,
+62600,
+9086,
+4386,
+27145,
+3900,
+85,
+52583,
+39476,
+43777,
+50162,
+15846,
+56816,
+23202,
+4427,
+5048,
+32114,
+65187,
+22723,
+37035,
+5998,
+21315,
+10642,
+58512,
+21489,
+23157,
+4662,
+63739,
+23889,
+8678,
+46592,
+61992,
+23061,
+57402,
+28048,
+26656,
+48761,
+61932,
+17149,
+14037,
+8297,
+45343,
+48473,
+44851,
+56673,
+46623,
+59467,
+57547,
+46744,
+8228,
+4222,
+5293,
+63124,
+34753,
+19030,
+15873,
+49226,
+20964,
+57873,
+23751,
+35388,
+2677,
+23470,
+13267,
+30117,
+51988,
+7349,
+10906,
+25526,
+18391,
+44133,
+49894,
+18256,
+24795,
+36742,
+44209,
+6024,
+5418,
+54980,
+7445,
+15043,
+54878,
+44256,
+6798,
+46462,
+24269,
+56527,
+25935,
+58627,
+9316,
+49669,
+4689,
+53283,
+21187,
+18835,
+49954,
+30717,
+56413,
+23111,
+26999,
+8431,
+1128,
+42899,
+8973,
+25189,
+1830,
+52223,
+24418,
+35138,
+43614,
+51801,
+53738,
+31303,
+52359,
+58570,
+25243,
+28665,
+1910,
+36902,
+44012,
+18949,
+36221,
+41219,
+48451,
+65047,
+53610,
+49739,
+64422,
+36064,
+43192,
+28320,
+5409,
+24750,
+42973,
+60574,
+52634,
+26420,
+45451,
+36194,
+37116,
+57068,
+61351,
+13265,
+23472,
+25950,
+20472,
+34170,
+23856,
+39699,
+35944,
+61195,
+22209,
+54865,
+46714,
+20144,
+1247,
+13497,
+17048,
+32979,
+23396,
+24771,
+59424,
+51116,
+24666,
+34713,
+29224,
+15694,
+38534,
+61725,
+47275,
+24262,
+35815,
+33291,
+50390,
+36309,
+6811,
+20984,
+24901,
+30543,
+26369,
+23395,
+33002,
+17049,
+28918,
+27794,
+17138,
+35559,
+15543,
+26247,
+9871,
+37078,
+48213,
+39585,
+45067,
+61555,
+50195,
+48608,
+37907,
+8806,
+5277,
+50650,
+39391,
+17465,
+41895,
+34254,
+29518,
+41336,
+55591,
+51251,
+36988,
+59218,
+12708,
+31947,
+9546,
+6646,
+12996,
+1307,
+20093,
+34185,
+20136,
+27323,
+8072,
+7153,
+10307,
+50082,
+10234,
+65141,
+1992,
+29662,
+9276,
+45044,
+21029,
+1849,
+41555,
+50141,
+25126,
+7797,
+13574,
+30527,
+3438,
+61860,
+11322,
+58523,
+43250,
+50208,
+16552,
+38061,
+7937,
+24662,
+55419,
+24779,
+47112,
+60839,
+43297,
+46710,
+57855,
+2902,
+4629,
+56946,
+44763,
+60304,
+60515,
+13158,
+255,
+257,
+39374,
+15144,
+2632,
+63945,
+64467,
+60143,
+8702,
+41132,
+15209,
+42111,
+14345,
+8795,
+9158,
+15879,
+57090,
+62892,
+40627,
+39801,
+22047,
+55173,
+54768,
+1553,
+33866,
+33560,
+8312,
+12268,
+17933,
+63561,
+26614,
+15460,
+63160,
+5490,
+39877,
+30140,
+44636,
+5730,
+56847,
+14279,
+18143,
+64039,
+8713,
+26828,
+43045,
+52979,
+12313,
+6031,
+52511,
+9068,
+40491,
+4651,
+22274,
+22135,
+54101,
+41908,
+43390,
+9166,
+4648,
+21217,
+41140,
+29613,
+53218,
+19511,
+26418,
+52636,
+37225,
+35699,
+46580,
+28638,
+3921,
+60408,
+21459,
+30702,
+23449,
+63087,
+19349,
+40722,
+54506,
+64213,
+48646,
+35183,
+51443,
+42491,
+39764,
+50471,
+771,
+7896,
+49355,
+45627,
+26385,
+40196,
+53866,
+33230,
+35143,
+53799,
+46380,
+54094,
+17190,
+54429,
+14947,
+36939,
+27204,
+1285,
+60187,
+13944,
+23302,
+13717,
+41560,
+11763,
+26569,
+58047,
+61565,
+23255,
+20339,
+16991,
+55051,
+39892,
+31208,
+44841,
+3855,
+4369,
+35289,
+53621,
+50973,
+28087,
+13107,
+21845,
+65535,
+2,
+43692,
+52430,
+37450,
+14564,
+11916,
+30248,
+61168,
+61682,
+20696,
+34329,
+25645,
+10486,
+48546,
+45198,
+42282,
+3972,
+7490,
+38968,
+53774,
+23977,
+51820,
+42235,
+51593,
+5350,
+64252,
+38333,
+28598,
+50590,
+11108,
+48347,
+11443,
+19157,
+11738,
+30394,
+32307,
+11671,
+25341,
+39152,
+19910,
+16182,
+57641,
+64766,
+15066,
+25773,
+23046,
+14094,
+30354,
+16891,
+1324,
+11031,
+24815,
+46188,
+2450,
+42088,
+34835,
+44078,
+5129,
+61616,
+36899,
+18957,
+29838,
+28312,
+12901,
+39119,
+46026,
+12319,
+35924,
+24397,
+44320,
+60889,
+56371,
+22147,
+23629,
+11436,
+43402,
+43263,
+60886,
+25046,
+56469,
+13026,
+59506,
+53224,
+12558,
+22492,
+38709,
+56824,
+1498,
+47394,
+51258,
+8690,
+59807,
+20901,
+35397,
+25660,
+60047,
+2377,
+50077,
+38923,
+1976,
+47604,
+53269,
+57225,
+31977,
+31671,
+63984,
+10769,
+10364,
+43490,
+25736,
+24910,
+2645,
+8447,
+49658,
+56379,
+56742,
+51192,
+23426,
+50328,
+24405,
+56835,
+5394,
+1070,
+1592,
+62905,
+50393,
+26163,
+65280,
+65282,
+52379,
+5022,
+5233,
+20774,
+8591,
+60908,
+62635,
+7682,
+18827,
+22240,
+4698,
+18425,
+40758,
+10118,
+40875,
+57600,
+27476,
+48985,
+15329,
+22287,
+7014,
+54215,
+3677,
+62099,
+35010,
+51963,
+57740,
+40411,
+15396,
+23982,
+63688,
+44508,
+20493,
+56261,
+35875,
+63545,
+396,
+55303,
+15455,
+55230,
+58384,
+57465,
+38214,
+45401,
+31352,
+45444,
+64230,
+52541,
+58891,
+55991,
+33590,
+52829,
+6319,
+28549,
+14286,
+9946,
+24201,
+36019,
+31283,
+23642,
+48072,
+26146,
+14887,
+60260,
+56731,
+27630,
+16929,
+15342,
+3982,
+20470,
+25952,
+17324,
+28459,
+55666,
+39290,
+49994,
+29978,
+48399,
+37743,
+36619,
+48488,
+32535,
+42142,
+39168,
+34994,
+40636,
+44553,
+58726,
+29228,
+15147,
+32246,
+29722,
+41275,
+18262,
+3812,
+27003,
+49843,
+36313,
+30824,
+40871,
+14421,
+6113,
+40766,
+42141,
+32558,
+48489,
+52040,
+64290,
+45393,
+18823,
+10672,
+43328,
+4342,
+29593,
+25838,
+41681,
+31367,
+45065,
+39587,
+42065,
+52272,
+4186,
+8469,
+28421,
+29343,
+20086,
+39117,
+12903,
+4963,
+22564,
+40787,
+60128,
+37217,
+22345,
+29473,
+1115,
+15798,
+11927,
+490,
+17086,
+24318,
+29316,
+46588,
+21525,
+28635,
+63627,
+36872,
+40294,
+6967,
+13178,
+34234,
+11799,
+13736,
+21923,
+30399,
+41119,
+13314,
+63707,
+40348,
+56564,
+22638,
+64409,
+57106,
+38538,
+42426,
+9124,
+34820,
+15583,
+46702,
+44350,
+12254,
+60848,
+15868,
+56221,
+6910,
+39602,
+9010,
+41268,
+19075,
+58739,
+21281,
+10659,
+50494,
+58092,
+10557,
+60119,
+59513,
+21328,
+28795,
+40742,
+47281,
+15643,
+21404,
+47146,
+40011,
+54631,
+58188,
+13549,
+35420,
+52270,
+42067,
+62860,
+30149,
+41786,
+7664,
+44573,
+16311,
+49664,
+46507,
+30784,
+2413,
+60244,
+61315,
+57309,
+18793,
+7990,
+6070,
+18914,
+8864,
+20686,
+17064,
+20194,
+57240,
+51500,
+48388,
+3605,
+16776,
+38881,
+37489,
+8135,
+42476,
+3545,
+18945,
+56859,
+41648,
+1798,
+60875,
+42380,
+44048,
+7025,
+54895,
+44222,
+59539,
+28502,
+42814,
+350,
+33423,
+60489,
+61110,
+42335,
+8721,
+49691,
+15375,
+21760,
+26061,
+12954,
+65452,
+61637,
+38392,
+61151,
+56451,
+2937,
+1674,
+31224,
+4247,
+23590,
+53109,
+3719,
+50616,
+11101,
+6890,
+46555,
+1061,
+28820,
+63994,
+16364,
+12768,
+42724,
+38116,
+9626,
+46252,
+53164,
+32012,
+49967,
+41763,
+2846,
+29259,
+42721,
+15619,
+1566,
+57257,
+35457,
+49833,
+56928,
+21433,
+13586,
+13092,
+53705,
+47064,
+29289,
+43259,
+13625,
+33414,
+50687,
+19200,
+1810,
+51316,
+52850,
+13407,
+40204,
+38174,
+55792,
+29911,
+48801,
+29850,
+1843,
+7429,
+19962,
+23359,
+2338,
+1458,
+6110,
+61763,
+1738,
+49785,
+44917,
+27409,
+11198,
+64391,
+25300,
+25031,
+11670,
+32733,
+30395,
+17321,
+43174,
+48572,
+62938,
+59509,
+61830,
+35316,
+4527,
+43784,
+5132,
+39862,
+21063,
+7994,
+1788,
+64439,
+43075,
+40935,
+24619,
+14836,
+52905,
+46086,
+6831,
+35772,
+62506,
+62445,
+33607,
+2343,
+33804,
+41669,
+20892,
+64873,
+47844,
+42523,
+132,
+26610,
+60344,
+40280,
+8053,
+19929,
+48843,
+39960,
+62809,
+18410,
+46387,
+49976,
+41307,
+35116,
+44483,
+19155,
+11445,
+41862,
+12738,
+47357,
+26329,
+58825,
+37540,
+63897,
+54142,
+29721,
+32549,
+15148,
+35986,
+26564,
+21410,
+52119,
+43650,
+61205,
+803,
+26819,
+41476,
+37283,
+54747,
+62355,
+17163,
+41395,
+54888,
+54847,
+53533,
+61301,
+8698,
+2894,
+23952,
+10212,
+28961,
+31362,
+20611,
+3578,
+4762,
+39037,
+4982,
+25161,
+16074,
+54624,
+8067,
+529,
+27576,
+33852,
+59223,
+53340,
+54119,
+48673,
+45278,
+51572,
+61835,
+23039,
+16024,
+50807,
+55313,
+30561,
+50598,
+22398,
+26808,
+16854,
+10686,
+63778,
+42313,
+50835,
+40756,
+18427,
+62668,
+9210,
+1794,
+62179,
+5643,
+30900,
+47886,
+5114,
+30854,
+21938,
+23173,
+55654,
+61844,
+28669,
+36971,
+4881,
+52342,
+15742,
+31377,
+49466,
+11367,
+21176,
+31332,
+36205,
+54299,
+40401,
+10501,
+30161,
+56788,
+27383,
+214,
+60356,
+22610,
+61970,
+53684,
+52748,
+60080,
+16133,
+16107,
+9965,
+12581,
+65064,
+49848,
+34052,
+16685,
+23186,
+59854,
+57937,
+61795,
+10845,
+18340,
+57416,
+35893,
+34599,
+24763,
+13056,
+15345,
+25108,
+55356,
+3780,
+65486,
+35391,
+51911,
+33200,
+14851,
+49798,
+43405,
+41421,
+18840,
+47078,
+53434,
+65186,
+33144,
+5049,
+41179,
+14154,
+54440,
+60618,
+55317,
+31753,
+43477,
+39016,
+35604,
+25555,
+10128,
+27933,
+27303,
+37198,
+44962,
+28157,
+25289,
+9001,
+8599,
+9702,
+38460,
+12527,
+20429,
+33950,
+52680,
+46977,
+53966,
+38162,
+6367,
+57315,
+30358,
+56195,
+4807,
+61735,
+31416,
+45729,
+4448,
+52354,
+57280,
+25395,
+37732,
+14047,
+54794,
+39444,
+54544,
+47693,
+3685,
+16163,
+46048,
+14956,
+61038,
+21259,
+43568,
+21430,
+54422,
+1658,
+15131,
+27464,
+20959,
+28120,
+23050,
+8175,
+25403,
+45088,
+16100,
+58134,
+11520,
+2174,
+23293,
+64995,
+14659,
+31710,
+60859,
+53319,
+52304,
+60699,
+9797,
+57585,
+58845,
+20717,
+54147,
+42388,
+18503,
+58713,
+53589,
+13567,
+56887,
+48701,
+62412,
+57713,
+57504,
+40725,
+17424,
+36288,
+45889,
+23241,
+10843,
+61797,
+2823,
+49966,
+32357,
+53165,
+20132,
+53025,
+9781,
+30036,
+51742,
+28541,
+9987,
+12842,
+13039,
+7002,
+19401,
+4301,
+37553,
+64730,
+23500,
+12921,
+10535,
+29367,
+65326,
+11548,
+57287,
+57760,
+47190,
+63734,
+34297,
+16562,
+56097,
+46097,
+46830,
+29294,
+16831,
+57817,
+31670,
+32665,
+57226,
+44063,
+7094,
+63395,
+5266,
+25845,
+28398,
+47667,
+23947,
+26779,
+22009,
+30709,
+50670,
+27541,
+51682,
+17206,
+8106,
+44366,
+24541,
+9772,
+37467,
+3585,
+37375,
+26038,
+29899,
+7175,
+8047,
+42745,
+9545,
+32948,
+12709,
+21209,
+47255,
+11075,
+55982,
+26294,
+22228,
+35277,
+45651,
+28335,
+24168,
+42774,
+46728,
+48084,
+59661,
+3091,
+33257,
+63195,
+35751,
+3933,
+11046,
+45569,
+48270,
+28951,
+20865,
+50999,
+39160,
+51959,
+10133,
+28747,
+11493,
+35552,
+60360,
+22982,
+4438,
+46965,
+4504,
+64927,
+19802,
+23925,
+35295,
+61856,
+56402,
+62546,
+59335,
+58700,
+7546,
+31506,
+11820,
+65161,
+48411,
+57509,
+40881,
+43866,
+54097,
+12846,
+7199,
+64382,
+14142,
+18847,
+36723,
+24887,
+26526,
+13100,
+55298,
+37993,
+10511,
+27040,
+6918,
+18121,
+37413,
+49547,
+2198,
+36629,
+47115,
+6718,
+47776,
+9326,
+59460,
+63974,
+49888,
+48781,
+27135,
+28444,
+35570,
+40586,
+29360,
+61826,
+24149,
+43212,
+45032,
+56892,
+15157,
+26630,
+24849,
+29072,
+42847,
+13756,
+10110,
+889,
+28204,
+26009,
+65218,
+63271,
+10186,
+31055,
+50785,
+49708,
+18911,
+3553,
+46526,
+45739,
+38677,
+63015,
+58259,
+19364,
+20740,
+16672,
+3519,
+27426,
+57158,
+63731,
+24945,
+54412,
+63529,
+62044,
+55760,
+28955,
+62779,
+58787,
+31444,
+9657,
+8205,
+57272,
+30319,
+55407,
+37606,
+48871,
+41144,
+27060,
+12052,
+17243,
+50826,
+63297,
+55264,
+37561,
+5921,
+21902,
+13337,
+38823,
+5526,
+40056,
+61132,
+49749,
+19396,
+42708,
+11654,
+26362,
+20520,
+58793,
+55498,
+21357,
+58281,
+39269,
+12556,
+53226,
+35868,
+13677,
+15185,
+42799,
+4094,
+34698,
+53741,
+790,
+6215,
+57620,
+26287,
+39932,
+46246,
+9096,
+42787,
+58549,
+43289,
+44908,
+5437,
+45014,
+61313,
+60246,
+16441,
+17591,
+37348,
+49177,
+43476,
+32107,
+55318,
+7858,
+22650,
+57373,
+57381,
+41927,
+49321,
+3970,
+42284,
+53810,
+37509,
+19103,
+62797,
+23713,
+28110,
+13819,
+49002,
+24509,
+63193,
+33259,
+23869,
+20656,
+29976,
+49996,
+29383,
+34672,
+46646,
+50401,
+30494,
+28741,
+1002,
+22805,
+5688,
+46772,
+59080,
+28577,
+6507,
+7745,
+19080,
+64171,
+26984,
+60858,
+32041,
+14660,
+37975,
+6174,
+20941,
+44893,
+27520,
+61785,
+5592,
+46321,
+28061,
+34806,
+10530,
+14190,
+34342,
+47161,
+61176,
+46403,
+49343,
+22018,
+57850,
+4614,
+55711,
+44873,
+37960,
+33328,
+6315,
+966,
+42495,
+18953,
+17611,
+16482,
+57574,
+25098,
+55459,
+22445,
+41885,
+5773,
+63983,
+32664,
+31978,
+57818,
+41210,
+29154,
+58374,
+48106,
+26197,
+46033,
+38262,
+8255,
+40144,
+3426,
+16849,
+58432,
+56082,
+21983,
+41692,
+17831,
+46548,
+53192,
+28623,
+44989,
+36117,
+50022,
+18508,
+43808,
+19495,
+55376,
+11141,
+23076,
+29647,
+20163,
+12290,
+55343,
+20370,
+64901,
+47167,
+57803,
+56224,
+45487,
+2907,
+60332,
+2356,
+60255,
+34389,
+25848,
+5125,
+34585,
+44101,
+29099,
+48361,
+14795,
+8687,
+55890,
+14330,
+4318,
+9849,
+51386,
+43663,
+34368,
+9419,
+64237,
+53480,
+48920,
+34643,
+54356,
+63117,
+64075,
+26153,
+19619,
+18817,
+62623,
+5963,
+979,
+49020,
+58947,
+558,
+48421,
+25875,
+10408,
+10566,
+7890,
+45107,
+33448,
+12858,
+29709,
+40971,
+38891,
+17703,
+22186,
+193,
+44931,
+2202,
+50961,
+16872,
+45659,
+23071,
+25546,
+39492,
+54564,
+45988,
+4289,
+24184,
+37364,
+21773,
+15547,
+44045,
+50940,
+679,
+53298,
+57050,
+62611,
+43177,
+55367,
+19026,
+49146,
+51291,
+38012,
+4256,
+18275,
+31518,
+36087,
+52784,
+6492,
+34551,
+1232,
+38696,
+46900,
+30030,
+58703,
+37263,
+24653,
+22902,
+39567,
+9013,
+28000,
+54362,
+54318,
+39301,
+60347,
+56356,
+44599,
+13921,
+4619,
+8413,
+44640,
+19669,
+2210,
+9753,
+62884,
+28346,
+36086,
+31550,
+18276,
+27052,
+41722,
+35472,
+522,
+14161,
+41016,
+62777,
+28957,
+50979,
+11819,
+31899,
+7547,
+16611,
+10720,
+43079,
+18976,
+35313,
+30639,
+28990,
+36209,
+30232,
+48220,
+51576,
+23507,
+4364,
+60253,
+2358,
+61593,
+18118,
+58275,
+15688,
+33394,
+48853,
+9763,
+58940,
+14100,
+58111,
+64673,
+16708,
+48233,
+43835,
+152,
+11138,
+860,
+25957,
+60587,
+40907,
+64604,
+6400,
+11632,
+25133,
+22449,
+12183,
+14508,
+38951,
+59873,
+47982,
+61308,
+54610,
+62089,
+4469,
+52240,
+43924,
+35247,
+3186,
+19002,
+56416,
+59552,
+60971,
+40443,
+223,
+9656,
+31816,
+58788,
+63042,
+16267,
+18746,
+49048,
+9950,
+18310,
+54815,
+22460,
+24604,
+36633,
+24322,
+98,
+64876,
+6654,
+30214,
+30851,
+29632,
+53854,
+3339,
+22625,
+23825,
+17971,
+486,
+52619,
+49911,
+45728,
+32078,
+61736,
+64279,
+29928,
+43023,
+22425,
+47808,
+16559,
+16595,
+22672,
+4305,
+36818,
+14385,
+46819,
+30982,
+5727,
+58593,
+47424,
+38208,
+40211,
+65155,
+13074,
+52997,
+30279,
+11760,
+59804,
+52035,
+57912,
+16497,
+3890,
+47381,
+51918,
+10911,
+57472,
+19758,
+53823,
+236,
+40374,
+49465,
+32168,
+15743,
+36237,
+29138,
+52176,
+59882,
+46169,
+14129,
+42825,
+45064,
+32523,
+41682,
+49287,
+6906,
+20610,
+32221,
+28962,
+11772,
+24155,
+13428,
+1509,
+17492,
+53598,
+58286,
+45443,
+32595,
+45402,
+39872,
+43145,
+35133,
+38082,
+47546,
+7021,
+52425,
+18206,
+46356,
+41985,
+42185,
+596,
+43363,
+7142,
+65171,
+47044,
+64372,
+36204,
+32164,
+21177,
+13645,
+38744,
+39900,
+30052,
+50635,
+6220,
+26791,
+25639,
+48139,
+17635,
+34386,
+25410,
+15362,
+56281,
+52204,
+2277,
+36452,
+21568,
+11924,
+37636,
+15098,
+42681,
+40453,
+12937,
+20815,
+48565,
+52358,
+33048,
+53739,
+34700,
+781,
+12153,
+49604,
+11268,
+41147,
+51460,
+57581,
+39109,
+34777,
+6964,
+35550,
+11495,
+43470,
+28754,
+16224,
+15948,
+23641,
+32581,
+36020,
+61770,
+39626,
+44,
+11652,
+42710,
+8870,
+59613,
+23724,
+63806,
+29057,
+41773,
+57118,
+64051,
+61213,
+24530,
+25277,
+47055,
+6643,
+18871,
+7700,
+16281,
+34452,
+47758,
+13320,
+19208,
+37459,
+42782,
+39227,
+64564,
+49828,
+1382,
+9905,
+37308,
+7586,
+30001,
+60350,
+55643,
+39722,
+13769,
+35806,
+1802,
+33551,
+48976,
+48944,
+58519,
+21361,
+64725,
+6385,
+35212,
+13969,
+3815,
+27497,
+54639,
+13954,
+47213,
+51070,
+4246,
+32376,
+1675,
+59477,
+30471,
+47973,
+30622,
+62071,
+54322,
+41454,
+62373,
+36468,
+34359,
+21579,
+49421,
+21299,
+44840,
+32779,
+39893,
+64048,
+37306,
+9907,
+25725,
+54785,
+54541,
+45105,
+7892,
+26895,
+13220,
+51346,
+33841,
+18377,
+55200,
+52546,
+25010,
+63295,
+50828,
+8231,
+7397,
+17373,
+15810,
+22666,
+14550,
+55850,
+5759,
+64093,
+29068,
+34324,
+43959,
+60578,
+27535,
+52631,
+40994,
+55767,
+35671,
+21873,
+33926,
+56119,
+23162,
+16236,
+18249,
+50179,
+25329,
+20785,
+43603,
+1752,
+5721,
+38313,
+16369,
+35644,
+38323,
+56018,
+18296,
+47901,
+34217,
+40128,
+5281,
+33911,
+39690,
+37141,
+30518,
+64125,
+7479,
+16762,
+24745,
+49359,
+59846,
+44656,
+13959,
+42032,
+7984,
+23376,
+12883,
+3404,
+7084,
+21859,
+53345,
+38691,
+6927,
+10454,
+16788,
+42343,
+28716,
+16638,
+60843,
+44884,
+39056,
+21303,
+23433,
+12572,
+55212,
+34858,
+46634,
+51215,
+45352,
+21220,
+13605,
+8387,
+64827,
+34572,
+5358,
+4587,
+46354,
+18208,
+40855,
+6265,
+2689,
+10063,
+45227,
+22022,
+36342,
+48551,
+9192,
+10887,
+29477,
+11284,
+3590,
+13123,
+19741,
+49255,
+34277,
+17780,
+24224,
+16904,
+61731,
+50846,
+52096,
+38070,
+57943,
+45479,
+58784,
+17385,
+25434,
+60882,
+13590,
+26373,
+64303,
+26843,
+39769,
+51371,
+36029,
+22416,
+27187,
+60218,
+12263,
+60627,
+40733,
+11207,
+62129,
+50784,
+31841,
+10187,
+26742,
+11416,
+16866,
+4481,
+7356,
+7466,
+51337,
+1598,
+8936,
+43685,
+53250,
+5618,
+1214,
+12569,
+3562,
+36016,
+41843,
+43105,
+61246,
+41355,
+35950,
+47478,
+41095,
+16945,
+43459,
+23515,
+35431,
+30352,
+14096,
+27988,
+35913,
+63642,
+42735,
+50057,
+29575,
+3070,
+47932,
+43361,
+598,
+59411,
+52452,
+42572,
+11448,
+6727,
+1881,
+48022,
+19942,
+10300,
+14475,
+41729,
+15962,
+22093,
+22785,
+45396,
+1000,
+28743,
+53976,
+721,
+3178,
+51004,
+53393,
+1863,
+29570,
+18130,
+23460,
+40397,
+59044,
+33991,
+64306,
+21154,
+5726,
+31402,
+46820,
+24313,
+56015,
+61346,
+54855,
+1627,
+11474,
+42967,
+39293,
+53918,
+34710,
+27093,
+14149,
+21876,
+10459,
+709,
+34431,
+60180,
+21035,
+44169,
+3789,
+50027,
+59398,
+50750,
+24348,
+50694,
+10444,
+10059,
+60411,
+33914,
+21437,
+8307,
+61791,
+36,
+46054,
+13467,
+36677,
+5424,
+25346,
+61913,
+12175,
+53745,
+29643,
+33404,
+40775,
+8476,
+20821,
+52819,
+3392,
+35097,
+21917,
+303,
+48507,
+63810,
+41123,
+1405,
+51228,
+60385,
+44443,
+64348,
+10979,
+16433,
+61586,
+46076,
+37609,
+61274,
+64600,
+3852,
+63718,
+60383,
+51230,
+49069,
+793,
+27652,
+5369,
+58130,
+58556,
+47013,
+48516,
+6882,
+47885,
+32181,
+5644,
+21688,
+50339,
+70,
+16616,
+33931,
+11182,
+55042,
+19792,
+24596,
+49253,
+19743,
+51308,
+51420,
+62562,
+61329,
+41797,
+3474,
+12222,
+41158,
+20360,
+104,
+42444,
+8467,
+4188,
+3615,
+46417,
+6082,
+27959,
+58771,
+3847,
+62830,
+9616,
+36153,
+33810,
+18892,
+29011,
+11533,
+3075,
+14766,
+30100,
+20565,
+240,
+4352,
+21937,
+32178,
+5115,
+29631,
+31427,
+30215,
+22005,
+546,
+18452,
+41913,
+3799,
+1260,
+1872,
+3640,
+65520,
+61442,
+33762,
+11797,
+34236,
+64757,
+60995,
+47209,
+43206,
+54758,
+60108,
+27131,
+26796,
+11618,
+34566,
+38445,
+40870,
+32541,
+36314,
+22592,
+37505,
+13807,
+11245,
+65369,
+6280,
+53017,
+56026,
+59384,
+25387,
+14253,
+39657,
+5856,
+2776,
+65420,
+22035,
+45567,
+11048,
+4070,
+7519,
+1683,
+53279,
+40584,
+35572,
+39412,
+36234,
+4718,
+57733,
+44759,
+61838,
+49074,
+49944,
+20206,
+24864,
+26142,
+18439,
+40066,
+2412,
+32430,
+46508,
+50559,
+36338,
+21192,
+50911,
+34851,
+24599,
+28435,
+11868,
+60066,
+17817,
+30364,
+1378,
+1902,
+3376,
+40115,
+50000,
+9311,
+20052,
+51658,
+9101,
+20169,
+26427,
+34245,
+58574,
+22264,
+36833,
+5764,
+51362,
+53077,
+51246,
+43648,
+52121,
+48962,
+17443,
+24846,
+44772,
+42595,
+24712,
+15231,
+8817,
+3234,
+15661,
+53583,
+12820,
+7827,
+47697,
+47867,
+46651,
+43439,
+50501,
+37475,
+33838,
+55008,
+8162,
+8158,
+17560,
+28140,
+38870,
+15659,
+3236,
+45474,
+61680,
+61170,
+39044,
+56412,
+33064,
+49955,
+23968,
+52873,
+62795,
+19105,
+58832,
+50669,
+31965,
+22010,
+57478,
+62423,
+43559,
+12809,
+23448,
+32824,
+21460,
+42424,
+38540,
+24246,
+10472,
+26784,
+20023,
+15243,
+20951,
+45322,
+45174,
+16065,
+49527,
+39297,
+14625,
+34759,
+40939,
+16286,
+55261,
+8465,
+42446,
+10324,
+34423,
+18904,
+13098,
+26528,
+48601,
+58309,
+61956,
+11113,
+63881,
+13148,
+11659,
+28065,
+40027,
+59521,
+18055,
+59589,
+23485,
+25185,
+23074,
+11143,
+14027,
+49079,
+54953,
+24493,
+37195,
+62918,
+8287,
+26831,
+17394,
+41531,
+20346,
+44737,
+53829,
+28932,
+787,
+19712,
+58214,
+8352,
+55048,
+28989,
+31499,
+35314,
+61832,
+10741,
+26095,
+44244,
+29830,
+6181,
+48735,
+20012,
+56133,
+52846,
+37841,
+45180,
+28832,
+2399,
+62070,
+31219,
+47974,
+42278,
+29529,
+1539,
+25237,
+2725,
+4641,
+52168,
+52159,
+50904,
+46005,
+36875,
+45841,
+29773,
+49058,
+62567,
+44590,
+19378,
+1604,
+21765,
+3840,
+65265,
+64087,
+44416,
+6353,
+362,
+29610,
+2170,
+65055,
+21665,
+36478,
+23766,
+26732,
+63388,
+18521,
+10570,
+61440,
+65522,
+42132,
+11300,
+55111,
+17773,
+15056,
+29455,
+61126,
+47363,
+11249,
+20233,
+53149,
+56750,
+46957,
+8153,
+51786,
+19195,
+64763,
+63588,
+19615,
+52605,
+61154,
+50597,
+32197,
+55314,
+18049,
+10614,
+12491,
+35975,
+64782,
+38680,
+49859,
+14503,
+5970,
+19571,
+37146,
+51311,
+17863,
+13476,
+39614,
+26368,
+32982,
+24902,
+40808,
+20586,
+43270,
+59925,
+40309,
+56422,
+20804,
+2625,
+38859,
+62929,
+43994,
+57193,
+19168,
+3437,
+32922,
+13575,
+30322,
+10935,
+5808,
+61657,
+13399,
+12096,
+64124,
+31145,
+37142,
+1222,
+19078,
+7747,
+7453,
+50899,
+25460,
+55018,
+42739,
+20599,
+15504,
+13455,
+941,
+52528,
+58681,
+38501,
+9957,
+8537,
+54477,
+28879,
+63979,
+61413,
+28740,
+31724,
+50402,
+37022,
+44804,
+17675,
+5937,
+10475,
+25106,
+15347,
+53569,
+10012,
+11298,
+42134,
+39093,
+39978,
+65079,
+53205,
+65251,
+5060,
+3329,
+59496,
+44781,
+47972,
+31221,
+59478,
+26192,
+39684,
+19691,
+2334,
+22960,
+4408,
+6467,
+15372,
+19654,
+45125,
+2483,
+23245,
+56209,
+6079,
+8980,
+65268,
+50621,
+4272,
+29679,
+29751,
+36816,
+4307,
+58677,
+47957,
+47203,
+23105,
+65382,
+9789,
+62144,
+34605,
+43621,
+19221,
+3598,
+25695,
+37873,
+47907,
+62787,
+59268,
+51224,
+41099,
+63422,
+43682,
+15730,
+12366,
+29937,
+64936,
+13873,
+24229,
+33278,
+21055,
+28689,
+49212,
+17405,
+53335,
+18699,
+59135,
+53907,
+59057,
+48079,
+58604,
+15610,
+53418,
+15236,
+53456,
+22391,
+34189,
+27456,
+9582,
+60402,
+41118,
+32485,
+21924,
+54248,
+17320,
+32306,
+32734,
+11739,
+28432,
+40921,
+11242,
+7643,
+58379,
+53002,
+13465,
+46056,
+54392,
+51512,
+64823,
+52210,
+39635,
+23601,
+46209,
+15223,
+8615,
+29339,
+39522,
+9466,
+21399,
+8187,
+15889,
+6200,
+6839,
+29828,
+44246,
+1377,
+30772,
+17818,
+1634,
+29182,
+26925,
+56194,
+32082,
+57316,
+10581,
+16890,
+32721,
+14095,
+31026,
+35432,
+19592,
+39073,
+7500,
+23032,
+27581,
+27813,
+7812,
+2702,
+42674,
+59584,
+58480,
+13218,
+26897,
+30026,
+38716,
+24063,
+25103,
+296,
+21626,
+12489,
+10616,
+21680,
+1195,
+48277,
+59151,
+34304,
+51569,
+10934,
+30525,
+13576,
+55406,
+31812,
+57273,
+56060,
+46083,
+49697,
+15927,
+24528,
+61215,
+47656,
+36094,
+64218,
+26902,
+46873,
+56608,
+38987,
+54674,
+45428,
+13126,
+26082,
+6863,
+56076,
+50761,
+53127,
+48891,
+59443,
+16275,
+51916,
+47383,
+21612,
+34084,
+62352,
+16447,
+515,
+52456,
+37314,
+22171,
+29255,
+738,
+5322,
+11759,
+31393,
+52998,
+15217,
+51391,
+18937,
+9445,
+61729,
+16906,
+8056,
+17522,
+41467,
+14258,
+17108,
+14718,
+15576,
+46500,
+41917,
+16028,
+43308,
+33598,
+19887,
+26314,
+55073,
+22876,
+39463,
+23757,
+54777,
+6068,
+7992,
+21065,
+61167,
+32762,
+11917,
+51884,
+56681,
+1311,
+41611,
+33631,
+3682,
+12208,
+570,
+58881,
+35325,
+61707,
+16090,
+11287,
+48219,
+31496,
+36210,
+12147,
+6955,
+12447,
+46674,
+60691,
+46560,
+34037,
+34899,
+3706,
+33238,
+61011,
+39497,
+35640,
+47069,
+22004,
+30850,
+31428,
+6655,
+35300,
+3831,
+64871,
+20894,
+55542,
+37150,
+2289,
+20120,
+115,
+43504,
+51352,
+60802,
+56581,
+23325,
+21340,
+15368,
+15655,
+23849,
+51587,
+23347,
+36921,
+61901,
+43488,
+10366,
+57029,
+50292,
+44588,
+62569,
+7975,
+28998,
+26567,
+11765,
+63828,
+53528,
+64310,
+10318,
+7508,
+62492,
+47173,
+13067,
+64540,
+65209,
+50707,
+41624,
+17868,
+41592,
+63258,
+29087,
+53572,
+24361,
+56787,
+32159,
+10502,
+21335,
+47750,
+3940,
+45832,
+6570,
+43566,
+21261,
+263,
+16137,
+41785,
+32437,
+62861,
+50,
+33412,
+13627,
+36458,
+10434,
+14622,
+44635,
+32862,
+39878,
+872,
+39113,
+4282,
+13849,
+12886,
+46091,
+49131,
+49746,
+65152,
+21676,
+3239,
+4714,
+8730,
+53513,
+28128,
+63518,
+64856,
+12241,
+45939,
+52860,
+51987,
+33096,
+13268,
+8842,
+14727,
+56714,
+48058,
+44686,
+8887,
+62124,
+64059,
+42021,
+34510,
+35186,
+45946,
+47195,
+8123,
+20564,
+30859,
+14767,
+58371,
+2306,
+63379,
+28926,
+27886,
+2572,
+36068,
+12471,
+61928,
+56365,
+60207,
+22974,
+16540,
+44424,
+3723,
+26698,
+55901,
+8279,
+33189,
+15705,
+20510,
+11213,
+24085,
+22604,
+43817,
+37771,
+17402,
+63399,
+46800,
+61337,
+38599,
+19820,
+23814,
+34023,
+65016,
+46255,
+13536,
+38475,
+53487,
+21116,
+38106,
+57718,
+14847,
+9045,
+61662,
+50634,
+31327,
+39901,
+2013,
+55548,
+52697,
+28765,
+57220,
+39019,
+59379,
+53478,
+64239,
+54472,
+64300,
+47457,
+26210,
+51741,
+32007,
+9782,
+14404,
+61548,
+7544,
+58702,
+31542,
+46901,
+18964,
+38715,
+30337,
+26898,
+4752,
+51322,
+52568,
+13823,
+26706,
+8283,
+20420,
+37431,
+53382,
+15935,
+28694,
+36128,
+58761,
+60237,
+26431,
+7303,
+56646,
+3370,
+53426,
+50964,
+22142,
+56354,
+60349,
+31247,
+7587,
+58568,
+52361,
+28116,
+42537,
+43585,
+56620,
+37981,
+42936,
+50247,
+6628,
+25241,
+58572,
+34247,
+54043,
+33622,
+5178,
+38774,
+53154,
+39428,
+38415,
+48398,
+32563,
+49995,
+31730,
+20657,
+23030,
+7502,
+38434,
+59200,
+43020,
+1826,
+37092,
+33675,
+24952,
+34738,
+26126,
+15079,
+21005,
+10668,
+24572,
+63111,
+9514,
+676,
+50146,
+23154,
+14600,
+28759,
+22408,
+22842,
+27403,
+20513,
+2068,
+1173,
+22163,
+1648,
+9142,
+21548,
+46466,
+62744,
+51994,
+7933,
+64935,
+30425,
+12367,
+8315,
+26520,
+33433,
+39983,
+43930,
+1824,
+43022,
+31413,
+64280,
+42527,
+19925,
+37532,
+62278,
+25729,
+62591,
+53343,
+21861,
+57513,
+42772,
+24170,
+27687,
+63287,
+42517,
+48800,
+32327,
+55793,
+20100,
+3219,
+29167,
+23227,
+2735,
+41800,
+17382,
+62782,
+17587,
+7174,
+31952,
+26039,
+35319,
+18469,
+54407,
+49167,
+34381,
+27215,
+42693,
+38136,
+3175,
+23298,
+57406,
+28352,
+53241,
+9020,
+24927,
+2191,
+25863,
+50256,
+20447,
+49439,
+11922,
+21570,
+16942,
+24691,
+9495,
+21099,
+10167,
+26511,
+40267,
+12834,
+9769,
+34366,
+43665,
+7762,
+45665,
+37250,
+56115,
+50992,
+23933,
+27764,
+48137,
+25641,
+3914,
+12941,
+6103,
+60481,
+1842,
+32325,
+48802,
+13352,
+60041,
+39681,
+42223,
+63677,
+24855,
+16583,
+51758,
+59987,
+28311,
+32707,
+18958,
+14236,
+26608,
+134,
+47576,
+17542,
+6180,
+30633,
+44245,
+30367,
+6840,
+35792,
+42515,
+63289,
+21905,
+52503,
+40345,
+13303,
+59671,
+7119,
+44487,
+45493,
+19427,
+58228,
+39881,
+56781,
+17495,
+51847,
+26031,
+1937,
+47446,
+17742,
+51273,
+49874,
+11956,
+29659,
+50453,
+4559,
+11973,
+53557,
+48753,
+51691,
+46968,
+36112,
+8623,
+60682,
+45056,
+39320,
+54616,
+11566,
+2341,
+33609,
+61605,
+36529,
+26397,
+22109,
+27191,
+57423,
+45763,
+38187,
+50815,
+62898,
+47924,
+49057,
+30608,
+45842,
+55578,
+57002,
+22909,
+14656,
+37261,
+58705,
+33254,
+3032,
+40624,
+52992,
+36108,
+14519,
+38425,
+41362,
+42654,
+54703,
+58121,
+60341,
+65374,
+36815,
+30450,
+29680,
+23658,
+60948,
+19185,
+58696,
+35711,
+23023,
+64129,
+40851,
+7241,
+20082,
+60940,
+3352,
+27326,
+50182,
+21518,
+49555,
+27711,
+51767,
+34295,
+63736,
+4625,
+38238,
+9621,
+28315,
+14492,
+52266,
+41274,
+32548,
+32247,
+54143,
+40285,
+56127,
+5831,
+24465,
+54108,
+62619,
+7550,
+461,
+18584,
+40970,
+31585,
+12859,
+19127,
+39365,
+1198,
+57667,
+21272,
+16061,
+38286,
+49369,
+61918,
+23169,
+22468,
+4522,
+57786,
+15028,
+10676,
+61628,
+12394,
+41417,
+12503,
+20594,
+29573,
+50059,
+45420,
+27547,
+42778,
+55122,
+23657,
+29750,
+30451,
+4273,
+9370,
+12016,
+53938,
+26452,
+26634,
+48912,
+16334,
+12310,
+33757,
+51861,
+59557,
+55651,
+42910,
+41714,
+9275,
+32932,
+1993,
+50452,
+29802,
+11957,
+37713,
+28731,
+51234,
+22505,
+9992,
+14121,
+52335,
+38511,
+54812,
+20162,
+31640,
+23077,
+8120,
+33403,
+30939,
+53746,
+59240,
+37131,
+9696,
+38646,
+42767,
+3337,
+53856,
+60864,
+53853,
+31426,
+30852,
+5116,
+334,
+25042,
+38010,
+51293,
+37548,
+34514,
+1896,
+37658,
+64017,
+59282,
+42096,
+55604,
+41539,
+12359,
+47685,
+53217,
+32836,
+41141,
+2169,
+30595,
+363,
+46273,
+22357,
+3668,
+6360,
+20679,
+42318,
+43236,
+37698,
+59942,
+52686,
+9829,
+53624,
+20286,
+17058,
+25837,
+32526,
+4343,
+46242,
+48578,
+58199,
+24181,
+34504,
+18060,
+1369,
+2058,
+20913,
+36486,
+28826,
+13278,
+14743,
+36810,
+37702,
+3069,
+31019,
+50058,
+29687,
+20595,
+18129,
+30991,
+1864,
+42275,
+37400,
+37286,
+13983,
+43797,
+53045,
+34981,
+756,
+11602,
+41064,
+3062,
+3510,
+39312,
+15673,
+4730,
+51810,
+50388,
+33293,
+38974,
+38168,
+37566,
+10538,
+36597,
+20392,
+45131,
+40139,
+59159,
+6640,
+63436,
+60139,
+23401,
+26446,
+29185,
+4611,
+47818,
+41129,
+15430,
+23067,
+1538,
+30619,
+42279,
+40416,
+8681,
+54434,
+58649,
+25504,
+61974,
+34499,
+23695,
+41335,
+32955,
+34255,
+3768,
+2105,
+27417,
+43282,
+322,
+22523,
+53751,
+14165,
+34472,
+43122,
+50009,
+45164,
+49617,
+27716,
+7915,
+39252,
+5494,
+25858,
+2064,
+41037,
+45951,
+13694,
+8366,
+38836,
+18722,
+40332,
+39720,
+55645,
+51173,
+9866,
+47558,
+57653,
+39408,
+43944,
+23770,
+42153,
+53161,
+65019,
+11283,
+31091,
+10888,
+28734,
+1114,
+32505,
+22346,
+25231,
+62964,
+35445,
+53067,
+57428,
+63493,
+53733,
+9718,
+47237,
+24756,
+19458,
+16669,
+4416,
+37881,
+58066,
+61125,
+30578,
+15057,
+26537,
+37190,
+34018,
+33988,
+12754,
+4204,
+3009,
+46443,
+39547,
+17880,
+35227,
+1320,
+5111,
+1142,
+17489,
+40556,
+27462,
+15133,
+28517,
+41323,
+56430,
+42025,
+18694,
+12544,
+35776,
+51019,
+17436,
+18568,
+35743,
+56915,
+41432,
+49635,
+20547,
+33889,
+15516,
+17174,
+50744,
+61422,
+64853,
+63427,
+9541,
+52660,
+58061,
+36842,
+35524,
+6777,
+12039,
+44380,
+3517,
+16674,
+28684,
+12391,
+28015,
+51879,
+36323,
+58294,
+45457,
+41042,
+28344,
+62886,
+60023,
+62150,
+54370,
+28884,
+47405,
+42079,
+10020,
+7692,
+55920,
+34671,
+31728,
+49997,
+54935,
+6721,
+45873,
+37944,
+47788,
+43408,
+64919,
+62139,
+45956,
+1698,
+6790,
+42560,
+38152,
+65325,
+31993,
+10536,
+37568,
+43640,
+65104,
+62959,
+61825,
+31860,
+40587,
+46609,
+41639,
+37008,
+47672,
+52063,
+969,
+8987,
+60062,
+63802,
+4801,
+59492,
+22631,
+53703,
+13094,
+20085,
+32515,
+28422,
+11463,
+39521,
+30375,
+8616,
+4937,
+19179,
+23554,
+1164,
+34204,
+33374,
+11239,
+20251,
+36546,
+34040,
+35306,
+53391,
+51006,
+43333,
+37966,
+18987,
+12347,
+48623,
+56180,
+16021,
+46587,
+32498,
+24319,
+18630,
+51872,
+14884,
+48468,
+11455,
+29297,
+23285,
+53647,
+13997,
+3283,
+26124,
+34740,
+60820,
+49793,
+34162,
+36400,
+23284,
+29309,
+11456,
+16830,
+31981,
+46831,
+41492,
+5079,
+43258,
+32340,
+47065,
+61518,
+59976,
+555,
+59998,
+19113,
+45593,
+55239,
+6162,
+17983,
+61810,
+50042,
+50281,
+21039,
+20039,
+47463,
+43204,
+47211,
+13956,
+52409,
+39457,
+26737,
+50231,
+57325,
+42535,
+28118,
+20961,
+22646,
+42720,
+32353,
+2847,
+45679,
+737,
+30283,
+22172,
+58397,
+17379,
+16340,
+48112,
+33518,
+19649,
+43102,
+22317,
+186,
+15071,
+7380,
+37986,
+29206,
+60680,
+8625,
+52812,
+37060,
+25315,
+54761,
+41525,
+3522,
+55567,
+43574,
+2630,
+15146,
+32551,
+58727,
+21821,
+15693,
+32995,
+34714,
+42946,
+4286,
+51122,
+52557,
+9903,
+1384,
+63314,
+13657,
+36138,
+7244,
+56655,
+13439,
+27469,
+5901,
+6608,
+60679,
+29241,
+37987,
+10002,
+21910,
+14379,
+44815,
+14977,
+34756,
+44346,
+734,
+43514,
+34442,
+16987,
+24479,
+65198,
+5624,
+40465,
+15288,
+58911,
+63332,
+4610,
+29536,
+26447,
+26924,
+30361,
+1635,
+10136,
+13164,
+52112,
+48895,
+18188,
+39138,
+43430,
+37175,
+9339,
+22125,
+45121,
+1757,
+23226,
+29907,
+3220,
+13613,
+56146,
+41089,
+26849,
+50949,
+44215,
+13200,
+27028,
+37591,
+2304,
+58373,
+31667,
+41211,
+16980,
+39757,
+24333,
+22072,
+6055,
+58796,
+17766,
+18367,
+21698,
+62708,
+38818,
+12999,
+42716,
+52175,
+31374,
+36238,
+42254,
+12716,
+62147,
+41462,
+27853,
+6342,
+38726,
+60495,
+16382,
+39324,
+51494,
+17097,
+47105,
+9591,
+56362,
+49986,
+53008,
+45110,
+51058,
+5519,
+49783,
+1740,
+59628,
+10506,
+57579,
+51462,
+12029,
+56511,
+44312,
+61286,
+54389,
+10331,
+2164,
+46558,
+60693,
+11517,
+48360,
+31621,
+44102,
+51953,
+11769,
+56590,
+64886,
+63219,
+37479,
+56573,
+50607,
+10010,
+53571,
+30165,
+63259,
+34223,
+43970,
+12421,
+46835,
+21585,
+51909,
+35393,
+55104,
+7634,
+16808,
+27157,
+13189,
+42846,
+31851,
+24850,
+3163,
+34323,
+31179,
+64094,
+50040,
+61812,
+38841,
+60461,
+18106,
+55143,
+43871,
+34946,
+41772,
+31272,
+63807,
+28202,
+891,
+40631,
+44623,
+35955,
+36712,
+21135,
+41281,
+26486,
+64912,
+38546,
+45231,
+59767,
+4511,
+24650,
+19212,
+17186,
+14880,
+40448,
+50823,
+28402,
+24051,
+58914,
+44428,
+8145,
+39785,
+3251,
+27429,
+7821,
+42807,
+48993,
+57591,
+53140,
+20213,
+20365,
+55720,
+52926,
+19475,
+54208,
+48500,
+44899,
+6092,
+49264,
+11532,
+30863,
+18893,
+3931,
+35753,
+39141,
+15276,
+11824,
+24930,
+37124,
+174,
+64789,
+21408,
+26566,
+30183,
+7976,
+13672,
+2527,
+5834,
+64617,
+45285,
+36208,
+31498,
+30640,
+55049,
+16993,
+58901,
+17010,
+47631,
+19243,
+14158,
+10633,
+46932,
+24558,
+46207,
+23603,
+56456,
+5537,
+46426,
+15972,
+25419,
+10605,
+57264,
+58051,
+54173,
+51840,
+28171,
+43766,
+56588,
+11771,
+31361,
+32222,
+10213,
+25057,
+50978,
+31509,
+62778,
+31819,
+55761,
+43594,
+20864,
+31923,
+48271,
+39885,
+37919,
+4759,
+54427,
+17192,
+62641,
+41587,
+51527,
+54998,
+35991,
+45146,
+38915,
+47500,
+41930,
+37628,
+49924,
+786,
+30646,
+53830,
+20531,
+59119,
+5802,
+27885,
+30095,
+63380,
+19425,
+45495,
+19236,
+27075,
+53012,
+27793,
+32977,
+17050,
+20618,
+38130,
+6240,
+55457,
+25100,
+12946,
+21656,
+63338,
+33664,
+18423,
+4700,
+40932,
+34104,
+41216,
+46909,
+28799,
+65249,
+53207,
+42856,
+27415,
+2107,
+4509,
+59769,
+59390,
+52646,
+58303,
+8491,
+48382,
+43742,
+39746,
+39280,
+47404,
+29390,
+54371,
+43978,
+15417,
+63978,
+30498,
+54478,
+38305,
+63887,
+56397,
+42533,
+57327,
+12623,
+11552,
+65226,
+47991,
+51818,
+23979,
+9438,
+19442,
+25723,
+9909,
+60135,
+52069,
+34592,
+60114,
+7483,
+49469,
+59289,
+4061,
+41127,
+47820,
+4836,
+55742,
+46621,
+56675,
+10289,
+45240,
+63649,
+64635,
+3056,
+15994,
+49038,
+61649,
+20436,
+20239,
+7620,
+40049,
+9366,
+14337,
+42542,
+2398,
+30625,
+45181,
+50927,
+45732,
+39259,
+13277,
+29581,
+36487,
+44403,
+40862,
+11749,
+63993,
+32366,
+1062,
+19360,
+43918,
+6334,
+46689,
+33651,
+40651,
+20838,
+6533,
+63542,
+15087,
+15017,
+64015,
+37660,
+23885,
+13481,
+18196,
+5659,
+21920,
+65248,
+28901,
+46910,
+15817,
+40741,
+32451,
+21329,
+52151,
+19596,
+12679,
+37385,
+21014,
+2552,
+63356,
+27268,
+48616,
+28381,
+49940,
+25983,
+5169,
+38195,
+39130,
+7062,
+47008,
+18787,
+54714,
+27949,
+53399,
+23409,
+61963,
+15702,
+8611,
+51178,
+3292,
+57219,
+30047,
+52698,
+10267,
+12211,
+44678,
+22407,
+29953,
+14601,
+20005,
+43724,
+16223,
+31287,
+43471,
+64044,
+45464,
+2218,
+63825,
+11492,
+31917,
+10134,
+1637,
+53975,
+30998,
+1001,
+31723,
+30495,
+61414,
+39797,
+51516,
+62766,
+1113,
+29475,
+10889,
+51233,
+29656,
+37714,
+51633,
+50793,
+35959,
+27836,
+44029,
+59023,
+162,
+35785,
+35088,
+61231,
+34132,
+51153,
+16637,
+31123,
+42344,
+58934,
+21711,
+25987,
+54384,
+37512,
+19381,
+64270,
+5333,
+59923,
+43272,
+34780,
+59774,
+9976,
+717,
+24372,
+14341,
+58807,
+8338,
+7475,
+36127,
+30014,
+15936,
+4397,
+18315,
+49211,
+30419,
+21056,
+49223,
+45302,
+12390,
+29403,
+16675,
+47118,
+1435,
+44955,
+23718,
+55964,
+37706,
+54039,
+4795,
+47186,
+14562,
+37452,
+8549,
+36970,
+32173,
+61845,
+54991,
+1909,
+33044,
+25244,
+19531,
+34927,
+19697,
+15808,
+17375,
+60714,
+12736,
+41864,
+37584,
+57095,
+3161,
+24852,
+43564,
+6572,
+53653,
+4358,
+52172,
+9451,
+61357,
+49981,
+23098,
+10093,
+2215,
+64148,
+3920,
+32828,
+46581,
+63626,
+32495,
+21526,
+17345,
+57940,
+44581,
+19304,
+5375,
+3134,
+5499,
+17553,
+4231,
+44988,
+31650,
+53193,
+59485,
+64834,
+48827,
+17306,
+42189,
+35345,
+3637,
+17313,
+63952,
+41003,
+5667,
+60092,
+6586,
+51171,
+55647,
+17941,
+45554,
+54285,
+43770,
+61699,
+47877,
+13458,
+50589,
+32741,
+38334,
+22453,
+63527,
+54414,
+14644,
+20980,
+48939,
+26185,
+26853,
+12079,
+43148,
+38147,
+53404,
+38998,
+28349,
+17392,
+26833,
+10945,
+63652,
+6506,
+31717,
+59081,
+3321,
+37741,
+48401,
+49061,
+12639,
+14275,
+13741,
+56987,
+36867,
+33365,
+60657,
+10841,
+23243,
+2485,
+13894,
+9071,
+62540,
+16429,
+11461,
+28424,
+2302,
+37593,
+64954,
+6870,
+48436,
+14285,
+32586,
+6320,
+9633,
+9654,
+225,
+21750,
+3924,
+9986,
+32005,
+51743,
+42883,
+43539,
+4476,
+4173,
+3785,
+503,
+11574,
+22779,
+49522,
+23897,
+36181,
+17866,
+41626,
+7832,
+63120,
+46414,
+7757,
+58839,
+49714,
+26548,
+10865,
+41322,
+29435,
+15134,
+35045,
+20734,
+56982,
+5004,
+58179,
+58073,
+20225,
+19828,
+11711,
+15406,
+53232,
+12694,
+42813,
+32396,
+59540,
+4537,
+37362,
+24186,
+1074,
+12072,
+17475,
+8195,
+27484,
+49760,
+57026,
+13763,
+15452,
+50724,
+62047,
+13995,
+53649,
+64711,
+57753,
+51922,
+9393,
+43890,
+47157,
+12724,
+36300,
+40223,
+13402,
+46072,
+267,
+14918,
+65415,
+42248,
+1349,
+37527,
+3230,
+56390,
+43303,
+64692,
+7999,
+12068,
+4785,
+55665,
+32567,
+17325,
+5951,
+7059,
+19987,
+14195,
+26394,
+50263,
+38586,
+56606,
+46875,
+52196,
+13300,
+63710,
+35569,
+31863,
+27136,
+16602,
+38724,
+6344,
+55395,
+23919,
+41737,
+11867,
+30776,
+24600,
+40920,
+30392,
+11740,
+17757,
+37892,
+6485,
+22847,
+27724,
+2301,
+28556,
+11462,
+29342,
+32516,
+8470,
+59751,
+10786,
+48812,
+2364,
+59109,
+40606,
+36534,
+65364,
+39247,
+27735,
+53518,
+759,
+6296,
+35897,
+55842,
+56107,
+24050,
+29035,
+50824,
+17245,
+47666,
+31970,
+25846,
+34391,
+35020,
+64316,
+48724,
+45965,
+34988,
+14227,
+14119,
+9994,
+35330,
+63249,
+12925,
+26158,
+11746,
+49939,
+28784,
+48617,
+4076,
+38034,
+48054,
+40762,
+61144,
+24458,
+10191,
+11016,
+65306,
+50436,
+17913,
+2368,
+12284,
+55258,
+54985,
+12045,
+22106,
+36364,
+56199,
+4051,
+18779,
+44994,
+60226,
+20452,
+55640,
+3756,
+53240,
+29886,
+57407,
+17391,
+28583,
+38999,
+36085,
+31520,
+62885,
+29395,
+41043,
+34882,
+2620,
+38233,
+33438,
+20576,
+27707,
+24167,
+31937,
+45652,
+11850,
+50004,
+20706,
+47523,
+45047,
+26876,
+14490,
+28317,
+5317,
+53276,
+60016,
+15756,
+5408,
+33030,
+43193,
+5316,
+28326,
+14491,
+29726,
+9622,
+12900,
+32706,
+29839,
+59988,
+55698,
+48824,
+10099,
+20590,
+26275,
+60612,
+56900,
+16962,
+62339,
+43706,
+20917,
+48253,
+3884,
+13547,
+58190,
+57928,
+25077,
+50399,
+46648,
+56069,
+44140,
+19871,
+35675,
+9423,
+7908,
+15944,
+14399,
+64960,
+14451,
+2672,
+53377,
+540,
+50880,
+35770,
+6833,
+33997,
+40885,
+46327,
+28080,
+43196,
+53120,
+1347,
+42250,
+11892,
+47783,
+51868,
+48540,
+2465,
+52117,
+21412,
+24590,
+15685,
+54228,
+23085,
+24060,
+33302,
+10791,
+28136,
+35971,
+51555,
+46258,
+39963,
+38396,
+5427,
+38408,
+26167,
+11484,
+14826,
+45011,
+37765,
+5122,
+46963,
+4440,
+7114,
+26050,
+50094,
+8417,
+1488,
+34332,
+55631,
+34288,
+57952,
+41748,
+5872,
+56767,
+13080,
+35252,
+43367,
+38580,
+45502,
+38455,
+4011,
+5702,
+44152,
+58866,
+26599,
+46993,
+11636,
+61072,
+12436,
+47922,
+62900,
+46220,
+61436,
+26008,
+31846,
+890,
+29055,
+63808,
+48509,
+19527,
+62239,
+22443,
+55461,
+56932,
+46762,
+6288,
+48281,
+5326,
+47945,
+33781,
+16361,
+51547,
+44292,
+56243,
+23330,
+54875,
+58321,
+58244,
+38171,
+53185,
+63060,
+60006,
+60999,
+37038,
+41352,
+33970,
+43765,
+28966,
+51841,
+57173,
+21575,
+24547,
+2022,
+23974,
+5736,
+61951,
+33583,
+39500,
+60773,
+26502,
+25288,
+32097,
+44963,
+43000,
+17121,
+52857,
+36747,
+44524,
+18213,
+18053,
+59523,
+26497,
+26151,
+64077,
+59429,
+27055,
+46201,
+38869,
+30726,
+17561,
+23261,
+35970,
+28252,
+10792,
+38280,
+14541,
+22404,
+6211,
+40988,
+63517,
+30124,
+53514,
+10157,
+47415,
+33661,
+15991,
+46658,
+23049,
+32053,
+20960,
+29263,
+42536,
+29997,
+52362,
+39953,
+4005,
+63264,
+13818,
+31738,
+23714,
+20791,
+45116,
+35520,
+12156,
+46492,
+48259,
+11453,
+48470,
+8100,
+7193,
+9612,
+60718,
+60165,
+13350,
+48804,
+50791,
+51635,
+12603,
+58638,
+60075,
+13106,
+32772,
+50974,
+36865,
+56989,
+41507,
+5314,
+43195,
+28271,
+46328,
+34281,
+22756,
+4148,
+25148,
+49329,
+58328,
+8575,
+55764,
+33581,
+61953,
+60197,
+40153,
+40026,
+30668,
+11660,
+15035,
+34805,
+31700,
+46322,
+2317,
+36445,
+8965,
+24539,
+44368,
+48098,
+46149,
+5572,
+38961,
+17577,
+26655,
+33126,
+57403,
+4989,
+49817,
+45638,
+8245,
+18400,
+63312,
+1386,
+39361,
+60598,
+46360,
+52028,
+22741,
+27761,
+42944,
+34716,
+51731,
+60634,
+11726,
+33795,
+46435,
+11152,
+36827,
+46157,
+63935,
+44743,
+5791,
+51066,
+59093,
+5270,
+9724,
+51878,
+29401,
+12392,
+61630,
+4850,
+64187,
+37069,
+62308,
+57153,
+48628,
+45611,
+35613,
+3260,
+43210,
+24151,
+54361,
+31535,
+9014,
+6711,
+33287,
+1641,
+52247,
+14653,
+46571,
+26824,
+63884,
+14243,
+35912,
+31024,
+14097,
+45386,
+61235,
+33539,
+808,
+57356,
+63511,
+18675,
+18589,
+10623,
+10272,
+33736,
+59617,
+45785,
+7291,
+27368,
+35989,
+55000,
+36172,
+21898,
+40552,
+47551,
+9264,
+51412,
+51149,
+9640,
+5412,
+58770,
+30871,
+6083,
+39739,
+54840,
+38572,
+23672,
+36882,
+8443,
+16556,
+53398,
+28774,
+54715,
+38508,
+36380,
+63234,
+36983,
+584,
+46269,
+65089,
+1907,
+54993,
+15616,
+12771,
+50375,
+4488,
+27302,
+32101,
+10129,
+33727,
+16667,
+19460,
+34620,
+4264,
+27399,
+62364,
+8658,
+5072,
+49790,
+56810,
+53784,
+15967,
+48337,
+54156,
+55097,
+27769,
+26204,
+13376,
+14635,
+20872,
+23606,
+36602,
+15614,
+54995,
+27320,
+55727,
+13230,
+49737,
+53612,
+34226,
+50440,
+57233,
+53864,
+40198,
+64656,
+21375,
+3536,
+42840,
+2493,
+49272,
+44419,
+27433,
+16396,
+2571,
+30094,
+28927,
+5803,
+16453,
+56142,
+21649,
+63640,
+35915,
+1521,
+36731,
+41653,
+22272,
+4653,
+51949,
+52447,
+57702,
+1465,
+54943,
+24507,
+49004,
+4104,
+7792,
+26458,
+54642,
+26140,
+24866,
+58040,
+44826,
+44263,
+49478,
+24207,
+9230,
+6341,
+29132,
+41463,
+56701,
+61473,
+56618,
+43587,
+12897,
+37871,
+25697,
+2309,
+16825,
+15764,
+47176,
+22300,
+35936,
+5596,
+44028,
+28726,
+35960,
+62469,
+65132,
+9572,
+36860,
+11499,
+5546,
+10977,
+64350,
+20281,
+53579,
+35880,
+36807,
+13905,
+55165,
+56710,
+61803,
+3442,
+7101,
+3037,
+3129,
+7811,
+30345,
+27582,
+47882,
+13911,
+6224,
+18404,
+59157,
+40141,
+33465,
+51491,
+13799,
+59236,
+20047,
+26104,
+60763,
+65181,
+62215,
+36963,
+17137,
+32976,
+28919,
+53013,
+158,
+4535,
+59542,
+47049,
+46487,
+1243,
+25552,
+21609,
+7046,
+65445,
+11524,
+64105,
+14061,
+1786,
+7996,
+57687,
+1529,
+60098,
+20525,
+37297,
+60416,
+26203,
+27915,
+55098,
+21719,
+35464,
+48136,
+29858,
+23934,
+42943,
+28034,
+22742,
+46914,
+50155,
+25200,
+60987,
+15863,
+58767,
+50105,
+42472,
+51032,
+59569,
+63572,
+12114,
+1018,
+47980,
+59875,
+65237,
+3508,
+3064,
+22089,
+1968,
+3629,
+22623,
+3341,
+53517,
+28410,
+39248,
+25607,
+25166,
+48325,
+44888,
+50198,
+12482,
+26220,
+25370,
+2300,
+28426,
+22848,
+8253,
+38264,
+1402,
+8923,
+55117,
+7914,
+29503,
+49618,
+38023,
+63761,
+51766,
+29733,
+49556,
+49326,
+24166,
+28337,
+20577,
+14425,
+9673,
+60640,
+1701,
+62265,
+39211,
+26644,
+47836,
+12690,
+34910,
+20358,
+41160,
+23959,
+50639,
+58851,
+9285,
+1716,
+63286,
+29915,
+24171,
+5795,
+38050,
+20819,
+8478,
+14679,
+63721,
+20294,
+64256,
+39272,
+4530,
+3429,
+4644,
+8791,
+40738,
+24582,
+43280,
+27419,
+55913,
+52639,
+37691,
+39841,
+35102,
+17631,
+7617,
+60815,
+26710,
+63504,
+55701,
+61120,
+36079,
+7472,
+43324,
+5368,
+30908,
+794,
+40230,
+63764,
+46121,
+16565,
+47779,
+37108,
+59053,
+20209,
+41778,
+10762,
+57269,
+49017,
+48637,
+47427,
+46824,
+8011,
+64401,
+44289,
+63604,
+16928,
+32574,
+56732,
+54305,
+64581,
+64989,
+47087,
+7841,
+49183,
+8914,
+17850,
+12322,
+25651,
+36589,
+60779,
+5622,
+65200,
+15410,
+45185,
+18753,
+42086,
+2452,
+50523,
+4864,
+46180,
+62230,
+46765,
+38958,
+7327,
+59537,
+44224,
+62386,
+40093,
+46670,
+1394,
+25746,
+58253,
+19663,
+36159,
+17750,
+45974,
+49534,
+45564,
+51267,
+43997,
+44096,
+17018,
+4260,
+47881,
+27812,
+30346,
+23033,
+47320,
+20663,
+33851,
+32210,
+530,
+57639,
+16184,
+22203,
+36214,
+46551,
+51754,
+42261,
+58645,
+18984,
+13785,
+44407,
+50876,
+33829,
+59364,
+57245,
+15826,
+17848,
+8916,
+35544,
+22602,
+24087,
+27494,
+58156,
+36295,
+36332,
+55536,
+42777,
+29684,
+45421,
+10238,
+33656,
+55027,
+51681,
+31963,
+50671,
+3603,
+48390,
+45750,
+52630,
+31175,
+60579,
+17043,
+11971,
+4561,
+64949,
+21214,
+40494,
+35910,
+14245,
+33984,
+61282,
+60377,
+5184,
+61784,
+31704,
+44894,
+1217,
+22869,
+11816,
+15918,
+37823,
+1777,
+48693,
+58145,
+21424,
+61020,
+22045,
+39803,
+53731,
+63495,
+61460,
+37159,
+17484,
+1549,
+55940,
+57882,
+54638,
+31230,
+3816,
+58155,
+27553,
+24088,
+44981,
+39049,
+627,
+46023,
+59741,
+37853,
+44719,
+49759,
+28493,
+8196,
+47053,
+25279,
+7138,
+48160,
+4825,
+48984,
+32623,
+57601,
+10961,
+60797,
+49012,
+2932,
+5900,
+29210,
+13440,
+34459,
+7595,
+20958,
+32055,
+15132,
+29437,
+40557,
+22179,
+2876,
+25947,
+9581,
+30403,
+34190,
+17992,
+64489,
+7575,
+22086,
+45895,
+24129,
+22905,
+14052,
+5504,
+38847,
+64460,
+40383,
+61489,
+12357,
+41541,
+621,
+51103,
+46662,
+53548,
+23525,
+16395,
+27889,
+44420,
+35479,
+7820,
+29028,
+3252,
+57157,
+31827,
+3520,
+41527,
+44241,
+22812,
+33176,
+55912,
+27669,
+43281,
+29514,
+2106,
+28897,
+42857,
+54554,
+45600,
+2838,
+11197,
+32313,
+44918,
+36622,
+59298,
+11211,
+20512,
+29950,
+22843,
+35647,
+62363,
+27926,
+4265,
+64140,
+38450,
+14971,
+18285,
+43543,
+51452,
+22388,
+36951,
+12134,
+47516,
+16538,
+22976,
+36168,
+213,
+32157,
+56789,
+17511,
+50632,
+61664,
+52522,
+49680,
+11570,
+33452,
+59171,
+9031,
+54516,
+40245,
+26562,
+35988,
+27972,
+7292,
+37357,
+12353,
+25332,
+33208,
+9746,
+22082,
+23841,
+24721,
+11477,
+57550,
+7592,
+20060,
+48658,
+40916,
+50703,
+19773,
+35759,
+14723,
+8409,
+1505,
+1263,
+11667,
+14498,
+60367,
+36757,
+26408,
+41645,
+41752,
+51405,
+60608,
+8499,
+38344,
+8116,
+46782,
+23453,
+16898,
+18112,
+34139,
+25327,
+50181,
+29737,
+3353,
+8071,
+32940,
+20137,
+55726,
+27906,
+54996,
+51529,
+40754,
+50837,
+14487,
+47376,
+2769,
+64426,
+18127,
+20597,
+42741,
+59578,
+12,
+50414,
+62916,
+37197,
+32100,
+27934,
+4489,
+60911,
+35809,
+55917,
+48205,
+8377,
+1808,
+19202,
+3878,
+52140,
+14262,
+15665,
+64329,
+10589,
+13325,
+25904,
+59107,
+2366,
+17915,
+63142,
+45855,
+9881,
+3695,
+10516,
+54826,
+19503,
+33875,
+57283,
+37816,
+64136,
+24671,
+44035,
+48615,
+28786,
+63357,
+8242,
+39452,
+4310,
+6690,
+44822,
+45153,
+464,
+11699,
+54744,
+37403,
+50997,
+20867,
+47841,
+101,
+49475,
+35836,
+16169,
+58579,
+21630,
+21270,
+57669,
+39938,
+57399,
+12187,
+17622,
+22314,
+41846,
+57756,
+17076,
+16131,
+60082,
+41974,
+44613,
+11058,
+36660,
+1651,
+51296,
+43295,
+60841,
+16640,
+47351,
+59815,
+34379,
+49169,
+4750,
+26900,
+64220,
+65289,
+797,
+59050,
+42692,
+29892,
+34382,
+9520,
+4193,
+64116,
+48323,
+25168,
+43379,
+57188,
+63646,
+1284,
+32795,
+36940,
+43085,
+17504,
+65231,
+63819,
+2253,
+4217,
+60768,
+20660,
+57037,
+38202,
+57422,
+29781,
+22110,
+64797,
+60217,
+31063,
+22417,
+58782,
+45481,
+46787,
+13186,
+63068,
+48304,
+50869,
+63210,
+20992,
+18832,
+15671,
+39314,
+41872,
+16172,
+48320,
+10717,
+2294,
+18220,
+17895,
+59653,
+3421,
+27024,
+10727,
+5383,
+62434,
+23727,
+63066,
+13188,
+29075,
+16809,
+8044,
+60471,
+63623,
+47929,
+21869,
+15263,
+6875,
+49230,
+50253,
+3899,
+33156,
+4387,
+22001,
+25573,
+37290,
+60111,
+40193,
+64245,
+16601,
+28443,
+31864,
+48782,
+51890,
+26795,
+30830,
+60109,
+37292,
+39371,
+6581,
+58359,
+5069,
+60794,
+26108,
+35557,
+17140,
+45511,
+46808,
+20854,
+14136,
+2458,
+20443,
+56514,
+51017,
+35778,
+24176,
+11611,
+1158,
+7352,
+14030,
+4074,
+48619,
+58034,
+35565,
+6338,
+39195,
+50478,
+39002,
+16221,
+43726,
+61863,
+50318,
+14148,
+30970,
+34711,
+24668,
+19724,
+25744,
+1396,
+38141,
+50567,
+1205,
+61571,
+20034,
+37318,
+61527,
+25830,
+23873,
+55834,
+33445,
+53011,
+28921,
+19237,
+41436,
+15031,
+64496,
+23128,
+56499,
+58547,
+42789,
+26686,
+51076,
+25051,
+52000,
+35476,
+12051,
+31807,
+41145,
+11270,
+39593,
+46200,
+28143,
+59430,
+41721,
+31516,
+18277,
+44272,
+25690,
+22197,
+63,
+1025,
+45928,
+52085,
+4922,
+9333,
+6917,
+31879,
+10512,
+8423,
+6855,
+35035,
+55581,
+80,
+57447,
+62281,
+45142,
+10821,
+37590,
+29158,
+13201,
+35887,
+10726,
+27164,
+3422,
+1705,
+2959,
+9588,
+9877,
+51519,
+10016,
+54167,
+63555,
+40362,
+53763,
+41384,
+11178,
+7335,
+14804,
+61191,
+182,
+2866,
+42016,
+49842,
+32544,
+3813,
+13971,
+8430,
+33061,
+23112,
+34838,
+41292,
+420,
+16502,
+40289,
+624,
+36492,
+20307,
+23059,
+61994,
+3276,
+21840,
+60857,
+31712,
+64172,
+64529,
+19981,
+11254,
+46893,
+6406,
+25778,
+41206,
+39536,
+11412,
+47800,
+2510,
+65277,
+58959,
+59350,
+64023,
+48948,
+10696,
+37582,
+41866,
+11317,
+14402,
+9784,
+11943,
+61944,
+22169,
+37316,
+20036,
+15884,
+52575,
+52735,
+15273,
+37086,
+8932,
+57057,
+50860,
+47564,
+65053,
+2172,
+11522,
+65447,
+45656,
+12815,
+53843,
+4199,
+35469,
+45718,
+18338,
+10847,
+15473,
+73,
+55796,
+43246,
+15193,
+51222,
+59270,
+62850,
+56193,
+30360,
+29183,
+26448,
+8396,
+25632,
+25594,
+11019,
+25294,
+65481,
+14300,
+57862,
+23939,
+8523,
+470,
+39518,
+15253,
+41199,
+40521,
+41034,
+9804,
+63486,
+52015,
+46872,
+30308,
+64219,
+27221,
+4751,
+30025,
+30338,
+13219,
+31198,
+7893,
+1952,
+55840,
+35899,
+22771,
+4977,
+11468,
+65498,
+5175,
+42557,
+7345,
+7611,
+17229,
+15189,
+13734,
+11801,
+47374,
+14489,
+28328,
+45048,
+42094,
+59284,
+24352,
+1578,
+3861,
+561,
+11779,
+19434,
+61451,
+14071,
+53322,
+13528,
+22541,
+19797,
+33703,
+2523,
+754,
+34983,
+15679,
+23545,
+12078,
+28589,
+26186,
+45264,
+50948,
+29162,
+41090,
+12191,
+34409,
+58611,
+39768,
+31068,
+64304,
+33993,
+18638,
+16358,
+22063,
+12408,
+16648,
+8665,
+10944,
+28581,
+17393,
+30652,
+8288,
+43044,
+32854,
+8714,
+13146,
+63883,
+27992,
+46572,
+35510,
+35201,
+41475,
+32237,
+804,
+51128,
+52870,
+10810,
+48934,
+37096,
+59194,
+36407,
+5043,
+16853,
+32194,
+22399,
+55804,
+533,
+47006,
+7064,
+20112,
+48454,
+38816,
+62710,
+48029,
+11617,
+30829,
+27132,
+51891,
+34208,
+25638,
+31324,
+6221,
+40564,
+3956,
+55435,
+60328,
+20022,
+30696,
+10473,
+5939,
+50235,
+22008,
+31967,
+23948,
+14012,
+22305,
+18414,
+54872,
+634,
+18372,
+40180,
+22971,
+52931,
+24760,
+57063,
+44159,
+22608,
+60358,
+35554,
+12384,
+46795,
+23200,
+56818,
+6684,
+56254,
+17462,
+39065,
+7317,
+41743,
+46725,
+55539,
+45871,
+6723,
+52877,
+40662,
+8809,
+49641,
+42230,
+11415,
+31053,
+10188,
+63216,
+60086,
+50230,
+29267,
+39458,
+49537,
+55969,
+63387,
+30589,
+23767,
+18445,
+8361,
+60812,
+20242,
+11410,
+39538,
+18199,
+58599,
+17082,
+38737,
+2828,
+36395,
+52539,
+64232,
+61065,
+52199,
+33740,
+60012,
+3928,
+63503,
+27660,
+60816,
+63969,
+8282,
+30020,
+13824,
+14924,
+64651,
+57170,
+36044,
+46816,
+55900,
+30083,
+3724,
+36473,
+5077,
+41494,
+1672,
+2939,
+60032,
+38092,
+1078,
+5346,
+51075,
+27066,
+42790,
+42099,
+17861,
+51313,
+3655,
+26119,
+62911,
+35004,
+2609,
+19411,
+11051,
+15899,
+11843,
+43720,
+59647,
+64414,
+19335,
+37396,
+34812,
+49879,
+58171,
+64399,
+8013,
+60525,
+53728,
+39069,
+56183,
+58587,
+48760,
+33125,
+28049,
+17578,
+18336,
+45720,
+52676,
+46412,
+63122,
+5295,
+24565,
+33954,
+47835,
+27699,
+39212,
+42372,
+9380,
+15629,
+3368,
+56648,
+3415,
+6006,
+48911,
+29673,
+26453,
+44770,
+24848,
+31853,
+15158,
+49010,
+60799,
+20560,
+62086,
+25209,
+20390,
+36599,
+18038,
+56706,
+57203,
+63619,
+18804,
+57010,
+15459,
+32867,
+63562,
+65372,
+60343,
+32271,
+133,
+29835,
+14237,
+14910,
+39470,
+20173,
+64963,
+64623,
+5600,
+46992,
+28214,
+58867,
+53431,
+63302,
+6895,
+17041,
+60581,
+10807,
+23971,
+10655,
+6305,
+19633,
+51028,
+34075,
+5665,
+41005,
+40262,
+64716,
+64499,
+18771,
+37933,
+58211,
+59964,
+37486,
+47961,
+40468,
+6733,
+7816,
+48242,
+58046,
+32787,
+11764,
+30182,
+28999,
+21409,
+32243,
+35987,
+27370,
+40246,
+56538,
+16032,
+7371,
+8082,
+14790,
+7436,
+47182,
+25928,
+19630,
+8928,
+35232,
+10864,
+28520,
+49715,
+43256,
+5081,
+25522,
+8906,
+50675,
+442,
+12132,
+36953,
+37189,
+29453,
+15058,
+38438,
+49317,
+5355,
+44504,
+51899,
+16509,
+48600,
+30676,
+13099,
+31884,
+24888,
+4875,
+4423,
+22059,
+33432,
+29934,
+8316,
+35492,
+6159,
+6310,
+49120,
+25803,
+19560,
+40266,
+29870,
+10168,
+46513,
+12562,
+314,
+57840,
+1355,
+58240,
+25287,
+28159,
+60774,
+33320,
+60556,
+26150,
+28147,
+59524,
+60251,
+4366,
+34818,
+9126,
+52534,
+24624,
+20555,
+38046,
+64911,
+29047,
+41282,
+3457,
+18669,
+20652,
+34418,
+44235,
+25550,
+1245,
+20146,
+64985,
+18437,
+26144,
+48074,
+38782,
+58221,
+25732,
+11808,
+38877,
+9355,
+49518,
+45944,
+35188,
+58038,
+24868,
+41686,
+13952,
+54641,
+27864,
+7793,
+45702,
+19863,
+44769,
+26633,
+29674,
+53939,
+62249,
+8395,
+26923,
+29184,
+29537,
+23402,
+35056,
+25560,
+58441,
+2144,
+48367,
+15785,
+13448,
+60051,
+47938,
+53632,
+62009,
+40007,
+7302,
+30010,
+60238,
+7955,
+34244,
+30761,
+20170,
+64664,
+35400,
+61256,
+24608,
+45450,
+33024,
+52635,
+32833,
+19512,
+5798,
+12636,
+1645,
+57535,
+25590,
+46564,
+6782,
+41644,
+27341,
+36758,
+58476,
+45552,
+17943,
+21451,
+46282,
+9644,
+47348,
+36362,
+22108,
+29783,
+36530,
+50262,
+28453,
+14196,
+63096,
+57131,
+49452,
+2784,
+24898,
+64243,
+40195,
+32807,
+45628,
+9663,
+4782,
+25158,
+54191,
+63701,
+14537,
+33617,
+13579,
+47455,
+64302,
+31070,
+13591,
+25426,
+23394,
+32981,
+30544,
+39615,
+16731,
+14748,
+4129,
+20519,
+31789,
+11655,
+41254,
+23906,
+49885,
+23597,
+16245,
+15052,
+3484,
+50362,
+50538,
+46001,
+18711,
+57528,
+45561,
+39461,
+22878,
+51141,
+56305,
+59198,
+38436,
+15060,
+64176,
+6121,
+53302,
+43012,
+11788,
+44491,
+25621,
+44024,
+20690,
+2820,
+58824,
+32252,
+47358,
+3271,
+37837,
+38894,
+23166,
+57784,
+4524,
+26042,
+9843,
+46291,
+127,
+513,
+16449,
+55072,
+30258,
+19888,
+26026,
+22754,
+34283,
+974,
+1547,
+17486,
+8767,
+39235,
+13078,
+56769,
+39232,
+52460,
+2923,
+16968,
+5829,
+56129,
+15335,
+22227,
+31941,
+55983,
+13413,
+172,
+37126,
+37803,
+39931,
+31770,
+57621,
+36036,
+60044,
+20180,
+54029,
+64547,
+23923,
+19804,
+36709,
+52261,
+60611,
+28305,
+20591,
+62384,
+44226,
+52243,
+62925,
+7255,
+33754,
+52982,
+1280,
+37860,
+61008,
+21755,
+25826,
+54756,
+43208,
+3262,
+25790,
+36651,
+18134,
+12064,
+45809,
+13725,
+15151,
+43812,
+55483,
+1533,
+9870,
+32972,
+15544,
+22569,
+34564,
+11620,
+21685,
+16009,
+34849,
+50913,
+20904,
+11218,
+34005,
+5191,
+25259,
+7922,
+762,
+25082,
+52602,
+17538,
+60572,
+42975,
+62026,
+35981,
+49865,
+38363,
+23666,
+25369,
+27727,
+12483,
+20480,
+35747,
+10922,
+65532,
+49154,
+36410,
+14044,
+51740,
+30038,
+47458,
+44334,
+3361,
+40216,
+13375,
+27914,
+27770,
+60417,
+10448,
+48710,
+49775,
+46032,
+31664,
+48107,
+3310,
+42221,
+39683,
+30469,
+59479,
+10042,
+65029,
+47441,
+45263,
+26852,
+28590,
+48940,
+42867,
+39562,
+56230,
+51601,
+62608,
+3918,
+64150,
+37498,
+4940,
+7389,
+46409,
+35831,
+64340,
+17262,
+42642,
+11483,
+28244,
+38409,
+58957,
+65279,
+32642,
+50394,
+21196,
+14112,
+11745,
+28384,
+12926,
+13152,
+17535,
+19618,
+31602,
+64076,
+28146,
+26498,
+60557,
+48466,
+14886,
+32578,
+48073,
+26474,
+18438,
+30788,
+24865,
+27862,
+54643,
+42838,
+3538,
+65038,
+64880,
+47855,
+59448,
+23587,
+55683,
+7883,
+36053,
+21594,
+15078,
+29964,
+34739,
+29304,
+3284,
+60311,
+22917,
+62910,
+26680,
+3656,
+5897,
+1990,
+65143,
+7672,
+50215,
+48834,
+3662,
+12382,
+35556,
+27123,
+60795,
+10963,
+60762,
+27800,
+20048,
+62632,
+4492,
+1941,
+56595,
+56896,
+22810,
+44243,
+30635,
+10742,
+33468,
+10994,
+20434,
+61651,
+51992,
+62746,
+19897,
+57294,
+38272,
+61228,
+6862,
+30301,
+13127,
+36269,
+38801,
+16001,
+19975,
+39190,
+42660,
+35282,
+41781,
+20836,
+40653,
+54584,
+4495,
+50626,
+38932,
+45365,
+875,
+62321,
+54571,
+12953,
+32385,
+21761,
+42822,
+4932,
+58975,
+58356,
+17225,
+62521,
+40910,
+16344,
+50093,
+28235,
+7115,
+62158,
+44837,
+39990,
+33965,
+10974,
+9842,
+26321,
+4525,
+35318,
+29898,
+31953,
+37376,
+4765,
+3645,
+54735,
+13987,
+1936,
+29809,
+51848,
+42398,
+7929,
+22753,
+26312,
+19889,
+39913,
+4032,
+43714,
+49846,
+65066,
+38626,
+50285,
+54073,
+36197,
+35163,
+56072,
+58717,
+53063,
+22253,
+65217,
+31845,
+28205,
+61437,
+19523,
+24428,
+20457,
+54707,
+24330,
+38562,
+54126,
+38812,
+47339,
+59880,
+52178,
+8945,
+653,
+40185,
+49241,
+19093,
+36092,
+47658,
+54383,
+28712,
+21712,
+1483,
+5168,
+28782,
+49941,
+4485,
+20264,
+12785,
+44005,
+23536,
+22669,
+39355,
+9308,
+12793,
+41406,
+42634,
+34000,
+56525,
+24271,
+7564,
+3319,
+59083,
+62349,
+46537,
+7207,
+56964,
+18159,
+62770,
+60586,
+31472,
+861,
+5247,
+43172,
+17323,
+32569,
+20471,
+33016,
+23473,
+9580,
+27458,
+2877,
+54266,
+38480,
+19338,
+60492,
+48686,
+7107,
+56032,
+50526,
+12126,
+58626,
+33073,
+56528,
+49583,
+60485,
+53575,
+1979,
+19629,
+26553,
+47183,
+24826,
+25464,
+52060,
+34992,
+39170,
+48807,
+57934,
+48477,
+39702,
+24956,
+20749,
+25183,
+23487,
+39826,
+3766,
+34257,
+65494,
+57736,
+16192,
+43521,
+13031,
+59106,
+27286,
+13326,
+35157,
+41937,
+21693,
+46237,
+8399,
+17735,
+63029,
+20862,
+43596,
+61138,
+47224,
+45378,
+45319,
+60669,
+44801,
+8557,
+2352,
+19832,
+3051,
+55424,
+14927,
+51283,
+34726,
+59682,
+61295,
+55138,
+10407,
+31592,
+48422,
+19826,
+20227,
+64012,
+52422,
+9931,
+24000,
+13228,
+55729,
+3897,
+50255,
+29881,
+2192,
+778,
+4544,
+2063,
+29499,
+5495,
+35691,
+23315,
+39344,
+35069,
+45847,
+23491,
+46961,
+5124,
+31625,
+34390,
+28397,
+31971,
+5267,
+58733,
+48766,
+63924,
+44519,
+41680,
+32525,
+29594,
+17059,
+39619,
+40582,
+53281,
+4691,
+23872,
+27080,
+61528,
+24839,
+54755,
+26262,
+21756,
+58393,
+368,
+60565,
+47397,
+64707,
+1424,
+25204,
+36047,
+9893,
+34293,
+51769,
+9917,
+49950,
+19541,
+12272,
+43153,
+2481,
+45127,
+16256,
+40173,
+19559,
+26514,
+49121,
+59677,
+46925,
+59453,
+37580,
+10698,
+18935,
+51393,
+46692,
+47700,
+21794,
+36650,
+26258,
+3263,
+62988,
+21478,
+64406,
+21866,
+3073,
+11535,
+51091,
+48556,
+36386,
+41205,
+26977,
+6407,
+42148,
+13471,
+23045,
+32724,
+15067,
+8565,
+6925,
+38693,
+34470,
+14167,
+48335,
+15969,
+44457,
+17639,
+20929,
+19612,
+20030,
+19756,
+57474,
+45581,
+60766,
+4219,
+50831,
+57391,
+36507,
+62287,
+64832,
+59487,
+44281,
+58252,
+27596,
+1395,
+27089,
+19725,
+54531,
+4122,
+36799,
+14022,
+9979,
+24909,
+32659,
+43491,
+38030,
+11807,
+26470,
+58222,
+62590,
+29922,
+62279,
+57449,
+54784,
+31203,
+9908,
+28864,
+19443,
+4831,
+9563,
+13192,
+14854,
+16404,
+15793,
+24678,
+49493,
+23143,
+42049,
+39624,
+61772,
+8759,
+6233,
+43829,
+55928,
+63403,
+2797,
+55677,
+17969,
+23827,
+60623,
+63377,
+2308,
+27845,
+37872,
+30436,
+3599,
+63226,
+48714,
+22196,
+27049,
+44273,
+2513,
+17806,
+6519,
+14003,
+48770,
+57083,
+4994,
+61510,
+50513,
+16635,
+51155,
+41116,
+60404,
+33242,
+44475,
+62392,
+9152,
+8629,
+4884,
+3194,
+14934,
+53023,
+20134,
+34187,
+22393,
+13706,
+20178,
+60046,
+32674,
+35398,
+64666,
+7308,
+35724,
+8757,
+61774,
+17265,
+36588,
+27619,
+12323,
+14641,
+54460,
+9557,
+10485,
+32757,
+34330,
+1490,
+3913,
+29856,
+48138,
+31323,
+26792,
+34209,
+35486,
+63525,
+22455,
+25593,
+26921,
+8397,
+46239,
+65358,
+55597,
+63151,
+8437,
+45647,
+39513,
+61506,
+44023,
+26334,
+44492,
+110,
+15352,
+59310,
+21780,
+61822,
+61850,
+49376,
+60862,
+53858,
+44970,
+65299,
+25165,
+27733,
+39249,
+33768,
+19292,
+56903,
+57901,
+48731,
+7867,
+38292,
+8139,
+15403,
+65304,
+11018,
+26920,
+25633,
+22456,
+46563,
+26412,
+57536,
+53471,
+19730,
+45755,
+13174,
+37423,
+61533,
+25001,
+56665,
+5926,
+7133,
+16693,
+33272,
+2729,
+19278,
+37289,
+27142,
+22002,
+47071,
+45758,
+8971,
+42901,
+45971,
+7980,
+62007,
+53634,
+18541,
+12750,
+58440,
+26443,
+35057,
+459,
+7552,
+10127,
+32103,
+35605,
+21608,
+27785,
+1244,
+26479,
+44236,
+20699,
+39491,
+31573,
+23072,
+25187,
+8975,
+46737,
+44450,
+40577,
+64703,
+54130,
+10694,
+48950,
+62452,
+40951,
+56040,
+44440,
+3527,
+39104,
+58236,
+5630,
+18390,
+33092,
+10907,
+11783,
+8905,
+26544,
+5082,
+42201,
+10342,
+64841,
+4433,
+55502,
+41169,
+53174,
+50222,
+25383,
+37471,
+34870,
+6017,
+47493,
+2500,
+49763,
+61973,
+29523,
+58650,
+61988,
+52885,
+12781,
+41732,
+9271,
+9112,
+59720,
+2604,
+20203,
+19376,
+44592,
+17421,
+23636,
+57916,
+36700,
+56172,
+41707,
+13560,
+5578,
+41339,
+60010,
+33742,
+4406,
+22962,
+23678,
+52657,
+16127,
+22891,
+53700,
+9630,
+47097,
+34751,
+63126,
+19227,
+8021,
+14110,
+21198,
+52059,
+25925,
+24827,
+43790,
+55017,
+30511,
+50900,
+44667,
+41806,
+4163,
+42386,
+54149,
+47230,
+61224,
+9713,
+50918,
+14908,
+14239,
+22244,
+23579,
+3150,
+37938,
+18868,
+9549,
+19717,
+56554,
+1453,
+55126,
+48584,
+50070,
+60881,
+31073,
+17386,
+47336,
+6940,
+53896,
+10175,
+10312,
+23393,
+26371,
+13592,
+44835,
+62160,
+34769,
+15538,
+10604,
+28972,
+15973,
+19091,
+49243,
+8440,
+62378,
+63608,
+56802,
+15361,
+31319,
+34387,
+60257,
+10839,
+60659,
+5309,
+45087,
+32050,
+8176,
+10292,
+61081,
+20405,
+35994,
+6379,
+37731,
+32073,
+57281,
+33877,
+62112,
+47144,
+21406,
+64791,
+14252,
+30813,
+59385,
+5339,
+37470,
+25512,
+50223,
+40715,
+46617,
+53289,
+56687,
+47467,
+7110,
+61916,
+49371,
+50667,
+58834,
+2299,
+27726,
+26221,
+23667,
+50865,
+8694,
+49280,
+39732,
+45979,
+56976,
+45073,
+18692,
+42027,
+47164,
+38766,
+42567,
+41054,
+17709,
+64883,
+39544,
+16297,
+25266,
+48445,
+41660,
+61912,
+30943,
+5425,
+38398,
+1293,
+39151,
+32731,
+11672,
+37640,
+882,
+17814,
+7204,
+63928,
+52129,
+33207,
+27364,
+12354,
+20784,
+31163,
+50180,
+27328,
+34140,
+383,
+43863,
+42638,
+62175,
+39331,
+52163,
+47920,
+12438,
+60106,
+54760,
+29236,
+37061,
+52136,
+53443,
+8542,
+65001,
+246,
+64742,
+37887,
+1774,
+13773,
+20018,
+47611,
+59147,
+25030,
+32310,
+64392,
+56773,
+17666,
+44067,
+65480,
+26918,
+11020,
+38166,
+38976,
+9000,
+32096,
+28158,
+26503,
+58241,
+24994,
+2408,
+10747,
+42422,
+21462,
+7137,
+27481,
+47054,
+31266,
+24531,
+38954,
+822,
+49532,
+45976,
+39025,
+35668,
+52704,
+20752,
+48444,
+25350,
+16298,
+49394,
+51876,
+9726,
+4906,
+7921,
+26234,
+5192,
+33268,
+57485,
+15500,
+44187,
+11393,
+35818,
+9411,
+21343,
+49034,
+38544,
+64914,
+14436,
+19530,
+28664,
+33045,
+58571,
+29989,
+6629,
+54200,
+2724,
+30617,
+1540,
+13993,
+62049,
+7049,
+62963,
+29471,
+22347,
+5611,
+35000,
+9116,
+59320,
+7919,
+4908,
+24806,
+18259,
+22659,
+55800,
+45714,
+56740,
+56381,
+2664,
+5362,
+54051,
+50713,
+19384,
+16949,
+20389,
+26624,
+62087,
+54612,
+46814,
+36046,
+25818,
+1425,
+6665,
+60986,
+27757,
+50156,
+62585,
+52324,
+60275,
+437,
+59104,
+13033,
+35716,
+52235,
+1829,
+33056,
+8974,
+25544,
+23073,
+30662,
+23486,
+25915,
+20750,
+52706,
+190,
+2746,
+59001,
+19627,
+1981,
+38523,
+11775,
+2856,
+11677,
+20569,
+138,
+43378,
+27209,
+48324,
+27732,
+25608,
+65300,
+34158,
+16073,
+32215,
+4983,
+54190,
+26381,
+4783,
+12070,
+1076,
+38094,
+4049,
+56201,
+24236,
+24164,
+49328,
+28075,
+4149,
+60374,
+4178,
+15558,
+42607,
+11717,
+42076,
+34549,
+6494,
+15520,
+11237,
+33376,
+55037,
+22448,
+31466,
+11633,
+52790,
+41891,
+23081,
+59760,
+7796,
+32925,
+50142,
+13254,
+20337,
+23257,
+36010,
+56857,
+18947,
+44014,
+11880,
+49111,
+53476,
+59381,
+6198,
+15891,
+51026,
+19635,
+55355,
+32129,
+15346,
+30487,
+10476,
+295,
+30334,
+24064,
+12945,
+28912,
+55458,
+31677,
+57575,
+1277,
+4565,
+34090,
+65315,
+12456,
+59472,
+50656,
+36500,
+14715,
+18514,
+42116,
+22855,
+34229,
+52601,
+26231,
+763,
+49840,
+42018,
+50398,
+28293,
+57929,
+21884,
+57207,
+59912,
+36347,
+50250,
+17575,
+38963,
+58805,
+14343,
+42113,
+24612,
+672,
+40706,
+42831,
+22550,
+7775,
+18349,
+50977,
+28959,
+10214,
+2362,
+48814,
+19405,
+51999,
+27064,
+51077,
+65121,
+51641,
+56468,
+32690,
+60887,
+44322,
+38009,
+29628,
+335,
+50129,
+12307,
+4756,
+59917,
+64325,
+50872,
+62995,
+14496,
+11669,
+32309,
+25301,
+59148,
+19935,
+62701,
+22702,
+24743,
+16764,
+5466,
+60647,
+58554,
+58132,
+16102,
+42702,
+24337,
+38629,
+24504,
+53257,
+7580,
+9666,
+63294,
+31191,
+52547,
+43065,
+59145,
+47613,
+24789,
+20709,
+21276,
+56664,
+25582,
+61534,
+44331,
+63458,
+65128,
+20401,
+2407,
+25285,
+58242,
+58323,
+46194,
+57179,
+45862,
+64522,
+23721,
+43638,
+37570,
+17987,
+64026,
+48047,
+36099,
+38076,
+43359,
+47934,
+593,
+43582,
+51624,
+59315,
+38748,
+61582,
+8968,
+57566,
+23494,
+43008,
+5956,
+45878,
+50739,
+13864,
+24240,
+57868,
+21086,
+39997,
+835,
+53387,
+20748,
+25917,
+39703,
+12257,
+34737,
+29966,
+33676,
+36178,
+18929,
+52531,
+641,
+54411,
+31824,
+63732,
+47192,
+47814,
+5145,
+50803,
+4089,
+4055,
+59608,
+10957,
+4660,
+23159,
+11187,
+6427,
+37123,
+29004,
+11825,
+2190,
+29883,
+9021,
+14522,
+64288,
+52042,
+7087,
+54008,
+50160,
+43779,
+7146,
+48317,
+5379,
+5032,
+62504,
+35774,
+12546,
+2644,
+32658,
+25737,
+9980,
+64645,
+36484,
+20915,
+43708,
+40807,
+30542,
+32983,
+20985,
+64242,
+26388,
+2785,
+56105,
+55844,
+15114,
+24295,
+3478,
+11040,
+54723,
+4874,
+26525,
+31885,
+36724,
+44700,
+39466,
+10954,
+58407,
+5002,
+56984,
+23903,
+62821,
+43982,
+12659,
+38790,
+56729,
+60262,
+14105,
+23273,
+63305,
+41685,
+26462,
+58039,
+27861,
+26141,
+30789,
+20207,
+59055,
+53909,
+7943,
+16377,
+20973,
+57042,
+16582,
+29843,
+63678,
+43563,
+28652,
+3162,
+29071,
+31852,
+26631,
+44771,
+30748,
+17444,
+23417,
+43169,
+47310,
+2910,
+54754,
+25828,
+61529,
+10027,
+11170,
+9376,
+18634,
+46575,
+64864,
+40474,
+22707,
+65310,
+43789,
+25463,
+25926,
+47184,
+4797,
+15313,
+40156,
+18921,
+17620,
+12189,
+41092,
+17329,
+46187,
+32717,
+11032,
+8032,
+33516,
+48114,
+41903,
+55430,
+24793,
+18258,
+25223,
+4909,
+34478,
+54331,
+62596,
+5507,
+56745,
+37865,
+40956,
+49719,
+36741,
+33087,
+18257,
+24808,
+55431,
+47521,
+20708,
+25005,
+47614,
+21353,
+63583,
+9699,
+14007,
+38220,
+14701,
+33349,
+47111,
+32910,
+55420,
+43038,
+17482,
+37161,
+4394,
+9291,
+59423,
+33000,
+23397,
+46399,
+13381,
+55180,
+50546,
+54245,
+13055,
+32132,
+34600,
+57062,
+26768,
+52932,
+6901,
+19457,
+29462,
+47238,
+44460,
+17996,
+50797,
+42972,
+33028,
+5410,
+9642,
+46284,
+49358,
+31141,
+16763,
+25025,
+22703,
+44388,
+64035,
+55714,
+6133,
+204,
+4157,
+53039,
+14968,
+64334,
+15213,
+21828,
+40634,
+34996,
+44952,
+20069,
+52780,
+14772,
+7658,
+42965,
+11476,
+27359,
+23842,
+40896,
+14808,
+45672,
+20770,
+1241,
+46489,
+15230,
+30745,
+42596,
+43160,
+62325,
+45434,
+878,
+46451,
+14585,
+18679,
+11063,
+1239,
+20772,
+5235,
+61395,
+12124,
+50528,
+24311,
+46822,
+47429,
+10396,
+9494,
+29874,
+16943,
+41097,
+51226,
+1407,
+35795,
+58297,
+11037,
+47328,
+34436,
+59273,
+54153,
+49492,
+25715,
+15794,
+21133,
+36714,
+53789,
+15600,
+44034,
+27271,
+64137,
+19723,
+27091,
+34712,
+32997,
+51117,
+50298,
+55418,
+32912,
+7938,
+18319,
+10708,
+11341,
+8027,
+33643,
+21672,
+22901,
+31539,
+37264,
+19211,
+29041,
+4512,
+7320,
+43382,
+12825,
+7640,
+13810,
+17829,
+41694,
+40818,
+50730,
+48793,
+16351,
+12702,
+55782,
+2655,
+41085,
+51359,
+58026,
+4949,
+34067,
+934,
+3015,
+39484,
+49194,
+20554,
+26490,
+52535,
+16878,
+38184,
+14835,
+32288,
+40936,
+37104,
+35146,
+54296,
+45288,
+671,
+25065,
+42114,
+18516,
+45449,
+26422,
+61257,
+60836,
+36632,
+31434,
+22461,
+33248,
+40919,
+28434,
+30777,
+34852,
+49252,
+30890,
+19793,
+18592,
+16261,
+17826,
+15684,
+28259,
+21413,
+9161,
+3084,
+40003,
+9498,
+3223,
+43279,
+27671,
+40739,
+15819,
+59295,
+10082,
+52428,
+43694,
+47664,
+17247,
+63110,
+29960,
+10669,
+60397,
+44432,
+46952,
+35827,
+33953,
+26647,
+5296,
+9800,
+20516,
+53815,
+53030,
+46206,
+28979,
+46933,
+41413,
+23762,
+8421,
+10514,
+3697,
+16465,
+59325,
+37407,
+2021,
+28167,
+21576,
+4961,
+12905,
+34364,
+9771,
+31958,
+44367,
+28056,
+8966,
+61584,
+16435,
+1584,
+36925,
+59871,
+38953,
+25276,
+31267,
+61214,
+30313,
+15928,
+48299,
+54375,
+62878,
+8902,
+51375,
+34026,
+2761,
+57684,
+64695,
+50498,
+17368,
+59931,
+12477,
+51835,
+19852,
+17794,
+63192,
+31735,
+49003,
+27868,
+54944,
+53256,
+25015,
+38630,
+55734,
+63472,
+36040,
+19587,
+42228,
+49643,
+20079,
+36141,
+37194,
+30656,
+54954,
+48649,
+24280,
+2719,
+62478,
+18882,
+10243,
+46889,
+22969,
+40182,
+47829,
+61500,
+65197,
+29193,
+16988,
+5853,
+4244,
+51072,
+53934,
+35978,
+62476,
+2721,
+52368,
+41962,
+11263,
+53628,
+54107,
+29716,
+5832,
+2529,
+60953,
+65395,
+63214,
+10190,
+28374,
+61145,
+59344,
+12014,
+9372,
+62881,
+40903,
+14179,
+8103,
+9390,
+36374,
+38689,
+53347,
+40719,
+48209,
+18058,
+34506,
+48593,
+40848,
+14312,
+35107,
+2116,
+16749,
+9677,
+6617,
+24055,
+58501,
+8171,
+52105,
+20456,
+26005,
+19524,
+1253,
+13746,
+55107,
+44813,
+14381,
+39860,
+5134,
+35137,
+33053,
+52224,
+15120,
+1726,
+34610,
+64133,
+56616,
+61475,
+36683,
+17718,
+36004,
+50108,
+56834,
+32649,
+50329,
+12805,
+56188,
+59698,
+59824,
+20183,
+44319,
+32700,
+35925,
+63369,
+16665,
+33729,
+38478,
+54268,
+34241,
+14078,
+50577,
+16049,
+54607,
+51368,
+17204,
+51684,
+46192,
+58325,
+53314,
+34657,
+45178,
+37843,
+41579,
+44627,
+46390,
+14340,
+28700,
+718,
+44300,
+10034,
+40022,
+12364,
+15732,
+50267,
+11225,
+11134,
+56786,
+30163,
+53573,
+60487,
+33425,
+51384,
+9851,
+52400,
+42623,
+1577,
+26872,
+59285,
+48239,
+50693,
+30957,
+50751,
+3556,
+56473,
+2221,
+51882,
+11919,
+44167,
+21037,
+50283,
+38628,
+25017,
+42703,
+56577,
+22071,
+29150,
+39758,
+38561,
+26002,
+54708,
+7718,
+33869,
+36384,
+48558,
+44373,
+97,
+31432,
+36634,
+18629,
+29315,
+32499,
+17087,
+7614,
+49427,
+56014,
+30980,
+46821,
+24696,
+50529,
+8150,
+57570,
+22535,
+48418,
+3864,
+391,
+24243,
+65119,
+51079,
+53058,
+13704,
+22395,
+16157,
+3477,
+24893,
+15115,
+62993,
+50874,
+44409,
+7266,
+59180,
+17272,
+6398,
+64606,
+59689,
+53881,
+39177,
+41632,
+2718,
+24490,
+48650,
+51562,
+21645,
+18382,
+7149,
+53833,
+65510,
+7563,
+25968,
+56526,
+33075,
+46463,
+44777,
+18476,
+8840,
+13270,
+35814,
+32990,
+47276,
+42880,
+57019,
+61806,
+44401,
+36489,
+39052,
+62081,
+608,
+45737,
+46528,
+58032,
+48621,
+12349,
+10471,
+30698,
+38541,
+65118,
+24303,
+392,
+57867,
+24963,
+13865,
+50333,
+24163,
+25151,
+56202,
+58622,
+52296,
+42605,
+15560,
+33277,
+30422,
+13874,
+53602,
+64555,
+16903,
+31083,
+17781,
+50534,
+21254,
+7287,
+2384,
+57102,
+19171,
+54671,
+37019,
+36103,
+9108,
+19470,
+51748,
+1118,
+48612,
+9229,
+27856,
+49479,
+64787,
+176,
+41841,
+36018,
+32583,
+9947,
+59958,
+40054,
+5528,
+54467,
+55076,
+64830,
+62289,
+15260,
+57778,
+6700,
+56951,
+52881,
+1073,
+28498,
+37363,
+31568,
+4290,
+34503,
+29588,
+58200,
+50735,
+44603,
+11610,
+27111,
+35779,
+22884,
+22218,
+5794,
+27686,
+29916,
+42773,
+31936,
+28336,
+27708,
+49327,
+25150,
+24237,
+50334,
+62697,
+54342,
+10756,
+14294,
+52371,
+13427,
+31359,
+11773,
+38525,
+54360,
+28002,
+43211,
+31858,
+61827,
+1927,
+8737,
+16389,
+14248,
+48373,
+33306,
+10650,
+12578,
+52763,
+2392,
+45792,
+14231,
+63848,
+4000,
+12712,
+52743,
+39565,
+22904,
+27449,
+45896,
+7766,
+41836,
+18603,
+40981,
+41776,
+20211,
+53142,
+35847,
+53035,
+17747,
+22131,
+33418,
+46698,
+49733,
+61789,
+8309,
+21476,
+62990,
+22576,
+927,
+13631,
+8621,
+36114,
+15903,
+16752,
+46299,
+38464,
+50507,
+54863,
+22211,
+60171,
+8271,
+7488,
+3974,
+47369,
+43821,
+7190,
+14182,
+44980,
+27493,
+27554,
+22603,
+30076,
+11214,
+34321,
+3165,
+8219,
+54958,
+7033,
+47472,
+5512,
+3389,
+36404,
+37685,
+8837,
+11873,
+48014,
+35268,
+51280,
+55530,
+4278,
+4703,
+12944,
+25102,
+30335,
+38717,
+33301,
+28255,
+23086,
+14555,
+49622,
+58500,
+24433,
+6618,
+63330,
+58913,
+29034,
+28403,
+56108,
+17706,
+4039,
+18705,
+36245,
+60459,
+38843,
+63866,
+20290,
+51669,
+52579,
+18564,
+52946,
+65158,
+6416,
+59737,
+52903,
+14838,
+8547,
+37454,
+60224,
+44996,
+56754,
+62441,
+48005,
+6659,
+62517,
+23880,
+5915,
+854,
+42006,
+17679,
+44902,
+614,
+23232,
+59885,
+10775,
+36303,
+62016,
+38113,
+21297,
+49423,
+48142,
+34887,
+45192,
+63705,
+13316,
+59347,
+13227,
+25868,
+9932,
+35920,
+53179,
+38097,
+64917,
+43410,
+20418,
+8285,
+62920,
+55492,
+49219,
+9259,
+61242,
+60944,
+44287,
+64403,
+63687,
+32610,
+15397,
+9437,
+28867,
+51819,
+32748,
+53775,
+5735,
+28165,
+2023,
+10654,
+26591,
+10808,
+52872,
+30715,
+49956,
+62034,
+43736,
+61711,
+2177,
+59550,
+56418,
+50638,
+27693,
+41161,
+20911,
+2060,
+51483,
+60035,
+10211,
+32224,
+2895,
+36594,
+14011,
+26778,
+31968,
+47668,
+35370,
+2280,
+64360,
+13766,
+55622,
+8522,
+26914,
+57863,
+15324,
+17526,
+42942,
+27763,
+29859,
+50993,
+4557,
+50455,
+17501,
+53357,
+64225,
+35294,
+31907,
+19803,
+26280,
+64548,
+200,
+41736,
+28438,
+55396,
+2373,
+18856,
+57461,
+14829,
+35368,
+47670,
+37010,
+57706,
+59068,
+50167,
+49884,
+26359,
+41255,
+62820,
+24879,
+56985,
+13743,
+53946,
+18927,
+36180,
+28530,
+49523,
+17301,
+58754,
+39128,
+38197,
+23786,
+8677,
+33132,
+63740,
+44337,
+13480,
+28805,
+37661,
+43266,
+4377,
+5914,
+24022,
+62518,
+17091,
+40189,
+3626,
+60316,
+55833,
+27079,
+25831,
+4692,
+20655,
+31732,
+33260,
+44646,
+60509,
+52501,
+21907,
+45211,
+63771,
+51131,
+3991,
+62271,
+21017,
+39698,
+33013,
+34171,
+16251,
+2231,
+40668,
+39076,
+51586,
+30195,
+15656,
+7368,
+43553,
+33883,
+47707,
+40895,
+24720,
+27360,
+22083,
+42073,
+12171,
+4443,
+58710,
+47824,
+47892,
+56798,
+49150,
+9364,
+40051,
+51978,
+60622,
+25701,
+17970,
+31422,
+22626,
+35873,
+56263,
+11203,
+59905,
+47149,
+20535,
+6106,
+38484,
+34022,
+30066,
+19821,
+8799,
+10971,
+19551,
+51061,
+34533,
+49576,
+52755,
+40038,
+56267,
+19141,
+65336,
+41617,
+37100,
+53671,
+8325,
+12729,
+61023,
+58219,
+38784,
+18813,
+57871,
+20966,
+7584,
+37310,
+59666,
+8676,
+23891,
+38198,
+14133,
+2695,
+53894,
+6942,
+18115,
+58972,
+58539,
+6979,
+18526,
+15569,
+33182,
+62692,
+2098,
+42152,
+29482,
+43945,
+18444,
+26731,
+30590,
+36479,
+34266,
+8420,
+24555,
+41414,
+45327,
+37895,
+54776,
+30254,
+39464,
+44702,
+63465,
+49399,
+35387,
+33101,
+57874,
+17664,
+56775,
+4998,
+12036,
+9834,
+4419,
+48531,
+9239,
+4207,
+34654,
+62064,
+62801,
+35633,
+48156,
+49199,
+14218,
+47839,
+20869,
+40080,
+61375,
+52418,
+63065,
+27160,
+62435,
+63805,
+31274,
+59614,
+43637,
+24987,
+64523,
+55963,
+28679,
+44956,
+10150,
+20790,
+28109,
+31739,
+62798,
+17977,
+7886,
+14526,
+1783,
+20316,
+49461,
+10915,
+6395,
+18619,
+57770,
+41411,
+46935,
+63875,
+55595,
+65360,
+41334,
+29520,
+34500,
+22433,
+43222,
+38297,
+7782,
+13617,
+54628,
+53756,
+46105,
+22103,
+7950,
+6694,
+22496,
+56717,
+42159,
+52656,
+25478,
+22963,
+54091,
+33282,
+52800,
+36881,
+27954,
+38573,
+54221,
+12089,
+50864,
+25368,
+26222,
+38364,
+49366,
+6960,
+9237,
+48533,
+55158,
+60947,
+29749,
+29681,
+55123,
+49684,
+10498,
+43091,
+33863,
+59765,
+45233,
+8710,
+47271,
+12746,
+40407,
+42457,
+3207,
+48071,
+32580,
+31284,
+15949,
+15093,
+48522,
+57915,
+25490,
+17422,
+40727,
+10108,
+13758,
+2435,
+11435,
+32695,
+22148,
+42420,
+10749,
+47084,
+34691,
+61739,
+62155,
+19036,
+35274,
+49510,
+19438,
+18709,
+46003,
+50906,
+45635,
+21446,
+51784,
+8155,
+33790,
+16217,
+18036,
+36601,
+27910,
+20873,
+56455,
+28977,
+46208,
+30379,
+39636,
+43845,
+16244,
+26357,
+49886,
+63976,
+15419,
+57348,
+49650,
+53108,
+32374,
+4248,
+55682,
+26132,
+59449,
+7247,
+50067,
+64267,
+50716,
+56813,
+3149,
+25446,
+22245,
+51241,
+13168,
+41068,
+54275,
+64892,
+50782,
+62131,
+58455,
+49045,
+4452,
+53087,
+12762,
+648,
+5454,
+38302,
+20925,
+21332,
+42485,
+57364,
+13434,
+7723,
+281,
+1163,
+29335,
+19180,
+34196,
+23353,
+12587,
+3963,
+19262,
+43182,
+12077,
+26855,
+15680,
+64443,
+47250,
+21996,
+61063,
+64234,
+14548,
+22668,
+25977,
+44006,
+5745,
+57497,
+64682,
+41518,
+47859,
+57997,
+13284,
+50556,
+16394,
+27435,
+53549,
+9208,
+62670,
+38532,
+15696,
+40458,
+15140,
+1477,
+35430,
+31028,
+43460,
+9106,
+36105,
+46844,
+40178,
+18374,
+4363,
+31493,
+51577,
+34401,
+57554,
+53608,
+65049,
+12920,
+31996,
+64731,
+14411,
+23270,
+53200,
+43007,
+24969,
+57567,
+46960,
+25851,
+45848,
+42393,
+39825,
+25914,
+25184,
+30663,
+59590,
+9006,
+8878,
+10246,
+46984,
+16304,
+47965,
+64613,
+49136,
+22887,
+9579,
+25949,
+33017,
+13266,
+33098,
+2678,
+62211,
+17287,
+57960,
+43453,
+41698,
+53367,
+53819,
+40396,
+30989,
+18131,
+36149,
+55518,
+11626,
+57596,
+16897,
+27332,
+46783,
+37926,
+63086,
+32823,
+30703,
+12810,
+22510,
+45936,
+20488,
+38663,
+6254,
+35918,
+9934,
+22746,
+38853,
+47677,
+53806,
+3560,
+12571,
+31117,
+21304,
+9577,
+22889,
+16129,
+17078,
+50327,
+32651,
+51193,
+40471,
+40926,
+47022,
+40451,
+42683,
+4707,
+43168,
+24844,
+17445,
+63964,
+18303,
+12837,
+55272,
+49390,
+61962,
+28772,
+53400,
+50412,
+14,
+34954,
+54238,
+35055,
+26445,
+29538,
+60140,
+9268,
+46398,
+24770,
+33001,
+32980,
+26370,
+25427,
+10313,
+54666,
+59129,
+39761,
+52067,
+60137,
+63438,
+41766,
+36056,
+12377,
+58925,
+56388,
+3232,
+8819,
+41857,
+12882,
+31134,
+7985,
+57947,
+23214,
+687,
+22958,
+2336,
+23361,
+17952,
+3834,
+17056,
+20288,
+63868,
+45539,
+17951,
+23369,
+2337,
+32321,
+19963,
+477,
+10284,
+275,
+12586,
+23551,
+34197,
+64942,
+21957,
+48230,
+36920,
+30193,
+51588,
+1763,
+12670,
+12766,
+16366,
+60789,
+16154,
+50601,
+12516,
+60454,
+40017,
+55196,
+46441,
+3011,
+632,
+54874,
+28184,
+56244,
+6116,
+45053,
+21339,
+30199,
+56582,
+63452,
+7517,
+4072,
+14032,
+14516,
+48103,
+62226,
+39343,
+25855,
+35692,
+1861,
+53395,
+47811,
+45949,
+41039,
+15895,
+38793,
+54123,
+17739,
+15639,
+13716,
+32791,
+13945,
+4987,
+57405,
+29888,
+3176,
+723,
+15299,
+64994,
+32044,
+2175,
+61713,
+121,
+37067,
+64189,
+37270,
+53646,
+29308,
+29298,
+36401,
+52822,
+44914,
+60450,
+58206,
+2120,
+13782,
+37969,
+6893,
+63304,
+24871,
+14106,
+53199,
+23497,
+14412,
+59163,
+18244,
+12566,
+44897,
+48502,
+63672,
+35969,
+28138,
+17562,
+34917,
+36009,
+25122,
+20338,
+32784,
+61566,
+33793,
+11728,
+6762,
+60528,
+11504,
+5686,
+22807,
+56208,
+30458,
+2484,
+28563,
+10842,
+32017,
+45890,
+45139,
+50051,
+11352,
+15414,
+54263,
+46167,
+59884,
+24015,
+615,
+63091,
+50045,
+2734,
+29906,
+29168,
+1758,
+33347,
+14703,
+13555,
+6020,
+44857,
+35934,
+22302,
+2055,
+4113,
+686,
+23373,
+57948,
+6971,
+48,
+62863,
+12162,
+7232,
+57048,
+53300,
+6123,
+14780,
+4426,
+33147,
+56817,
+26760,
+46796,
+17931,
+12270,
+19543,
+48748,
+34413,
+36822,
+6604,
+1023,
+65,
+64545,
+54031,
+59853,
+32141,
+16686,
+58665,
+17103,
+53587,
+58715,
+56074,
+6865,
+62684,
+62206,
+6043,
+42908,
+55653,
+32176,
+21939,
+4401,
+22467,
+29698,
+61919,
+57783,
+26324,
+38895,
+56158,
+16235,
+31167,
+56120,
+11186,
+24934,
+4661,
+33135,
+21490,
+14599,
+29955,
+50147,
+61373,
+40082,
+11389,
+33506,
+47035,
+7571,
+252,
+19688,
+42048,
+25713,
+49494,
+52804,
+13688,
+39508,
+57609,
+44712,
+728,
+64639,
+1170,
+1680,
+14894,
+13104,
+60077,
+56498,
+27070,
+64497,
+64718,
+45268,
+51165,
+63536,
+17270,
+59182,
+65177,
+16796,
+43388,
+41910,
+54789,
+40255,
+44076,
+34837,
+26998,
+33062,
+56414,
+19004,
+59255,
+12522,
+65381,
+30444,
+47204,
+49118,
+6312,
+12199,
+54112,
+10092,
+28643,
+49982,
+48764,
+58735,
+65432,
+34660,
+57071,
+34856,
+55214,
+63411,
+43240,
+14554,
+24059,
+28256,
+54229,
+42991,
+59759,
+25129,
+41892,
+62331,
+8119,
+29646,
+31641,
+11142,
+30661,
+25186,
+25545,
+31574,
+45660,
+43733,
+1537,
+29531,
+15431,
+37785,
+14506,
+12185,
+57401,
+33128,
+61993,
+26989,
+20308,
+43517,
+7689,
+5907,
+55033,
+44204,
+41977,
+8174,
+32052,
+28121,
+46659,
+14093,
+32723,
+25774,
+13472,
+64570,
+33855,
+46585,
+16023,
+32201,
+61836,
+44761,
+56948,
+63241,
+47319,
+27580,
+30347,
+7501,
+29974,
+20658,
+60770,
+61894,
+55191,
+12874,
+64128,
+29744,
+35712,
+2249,
+35624,
+16738,
+22678,
+10985,
+52396,
+17692,
+33264,
+65406,
+53943,
+1256,
+35611,
+45613,
+44393,
+13525,
+63033,
+9139,
+36663,
+8211,
+36273,
+37420,
+35541,
+21953,
+13915,
+19145,
+51199,
+36703,
+63140,
+17917,
+54503,
+57507,
+48413,
+13929,
+50544,
+55182,
+15958,
+20267,
+14374,
+4437,
+31913,
+60361,
+38652,
+58193,
+58746,
+36167,
+27386,
+16539,
+30087,
+60208,
+52930,
+26770,
+40181,
+24484,
+46890,
+11280,
+13084,
+34525,
+54090,
+23677,
+25479,
+4407,
+30465,
+2335,
+23371,
+688,
+61543,
+15183,
+13679,
+47683,
+12361,
+15317,
+21483,
+13048,
+48256,
+44174,
+814,
+46480,
+7041,
+20764,
+34791,
+40826,
+22378,
+49932,
+65517,
+10349,
+13444,
+45543,
+2489,
+13240,
+41305,
+49978,
+40394,
+53821,
+19760,
+60749,
+47439,
+65031,
+46980,
+4603,
+47047,
+59544,
+8215,
+21961,
+62909,
+26121,
+60312,
+13136,
+41183,
+63961,
+14648,
+46569,
+14655,
+29769,
+57003,
+16199,
+14051,
+27448,
+24130,
+39566,
+31538,
+24654,
+21673,
+40214,
+3363,
+3761,
+48274,
+39368,
+54055,
+14580,
+53699,
+25475,
+16128,
+23430,
+9578,
+23475,
+49137,
+22217,
+24174,
+35780,
+10835,
+49311,
+49591,
+51140,
+26346,
+39462,
+30256,
+55074,
+54469,
+20988,
+16347,
+44295,
+11815,
+27517,
+1218,
+17689,
+57658,
+8894,
+62834,
+35195,
+5954,
+43010,
+53304,
+53874,
+17911,
+50438,
+34228,
+25085,
+42117,
+60831,
+59437,
+52912,
+53987,
+8252,
+27723,
+28427,
+6486,
+38321,
+35646,
+27402,
+29951,
+22409,
+1472,
+5696,
+6098,
+12627,
+49434,
+40519,
+41201,
+8961,
+15526,
+20375,
+46140,
+33746,
+53884,
+34260,
+56668,
+5019,
+13487,
+16876,
+52537,
+36397,
+13363,
+56088,
+42890,
+36277,
+33185,
+49919,
+52768,
+33175,
+27422,
+44242,
+26097,
+56897,
+56207,
+23247,
+5687,
+31721,
+1003,
+1894,
+34516,
+15481,
+54824,
+10518,
+35028,
+44939,
+38228,
+5960,
+59755,
+57489,
+33588,
+55993,
+49413,
+440,
+50677,
+18821,
+45395,
+31001,
+22094,
+44072,
+55178,
+13383,
+49521,
+28532,
+11575,
+57210,
+52810,
+8627,
+9154,
+62875,
+4976,
+26890,
+35900,
+62201,
+53197,
+14108,
+8023,
+35620,
+41368,
+33602,
+18810,
+10000,
+37989,
+35854,
+10416,
+4147,
+28077,
+34282,
+26311,
+26027,
+7930,
+56440,
+33771,
+6989,
+38470,
+38852,
+23439,
+9935,
+5812,
+46913,
+27760,
+28035,
+52029,
+50351,
+33760,
+61444,
+43356,
+62663,
+16083,
+62755,
+57502,
+57715,
+36511,
+16545,
+51053,
+9196,
+6849,
+52842,
+37034,
+33142,
+65188,
+51037,
+5172,
+19486,
+59101,
+49416,
+43775,
+39478,
+60606,
+51407,
+34168,
+20474,
+59370,
+60957,
+65309,
+24830,
+40475,
+42988,
+44387,
+24742,
+25026,
+62702,
+10893,
+39399,
+62000,
+37644,
+63045,
+1106,
+20189,
+55972,
+52347,
+36464,
+33687,
+51782,
+21448,
+2259,
+21837,
+5700,
+4013,
+1748,
+12329,
+36639,
+38123,
+10984,
+23018,
+16739,
+62222,
+5558,
+20149,
+4304,
+31407,
+16596,
+39354,
+25976,
+23537,
+14549,
+31184,
+15811,
+10251,
+17293,
+7493,
+61384,
+55799,
+25221,
+18260,
+41277,
+8519,
+13793,
+36998,
+21999,
+4389,
+57372,
+31750,
+7859,
+9448,
+42719,
+29261,
+20962,
+49228,
+6877,
+63699,
+54193,
+21864,
+64408,
+32479,
+56565,
+39969,
+19567,
+46250,
+9628,
+53702,
+29347,
+59493,
+42362,
+9885,
+35872,
+23824,
+31423,
+3340,
+27738,
+3630,
+2564,
+57694,
+16356,
+18640,
+17418,
+53995,
+11557,
+44042,
+20128,
+10576,
+61969,
+32154,
+60357,
+26765,
+44160,
+62003,
+43816,
+30075,
+24086,
+27555,
+35545,
+15291,
+65508,
+53835,
+12648,
+48010,
+41602,
+37775,
+37504,
+30822,
+36315,
+61252,
+48756,
+65331,
+61382,
+7495,
+20713,
+20386,
+1274,
+59010,
+19875,
+21806,
+566,
+17570,
+926,
+24109,
+62991,
+15117,
+57878,
+40814,
+54062,
+34563,
+26245,
+15545,
+21775,
+14739,
+40786,
+32510,
+4964,
+39310,
+3512,
+44252,
+48969,
+56214,
+53959,
+5628,
+58238,
+1357,
+15782,
+43547,
+7774,
+25061,
+42832,
+21151,
+11307,
+42454,
+5779,
+64098,
+10620,
+19796,
+26862,
+13529,
+63615,
+20573,
+37382,
+48417,
+24307,
+57571,
+13635,
+62736,
+12336,
+42042,
+40569,
+59582,
+42676,
+12234,
+39200,
+53750,
+29511,
+323,
+47131,
+6382,
+46687,
+6336,
+35567,
+63712,
+35608,
+34125,
+43113,
+5639,
+45935,
+23446,
+12811,
+13490,
+63522,
+9991,
+29654,
+51235,
+7677,
+10884,
+17901,
+10116,
+40760,
+48056,
+56716,
+23682,
+6695,
+57248,
+38708,
+32684,
+12559,
+48178,
+9474,
+6904,
+49289,
+3821,
+51220,
+15195,
+56865,
+20461,
+64196,
+10125,
+7554,
+47434,
+2872,
+49456,
+44536,
+45337,
+12989,
+40529,
+6393,
+10917,
+4521,
+29697,
+23170,
+4402,
+388,
+51723,
+1097,
+33247,
+24603,
+31435,
+54816,
+34035,
+46562,
+25592,
+25634,
+63526,
+28596,
+38335,
+48034,
+12182,
+31465,
+25134,
+55038,
+41884,
+31675,
+55460,
+28197,
+62240,
+17797,
+5587,
+9536,
+17002,
+10381,
+45887,
+36290,
+43221,
+23693,
+34501,
+4292,
+17160,
+6329,
+12042,
+45207,
+47807,
+31411,
+43024,
+59899,
+46471,
+7708,
+62418,
+14441,
+58781,
+27186,
+31064,
+36030,
+15529,
+55718,
+20367,
+54524,
+1471,
+22841,
+29952,
+28760,
+44679,
+6210,
+28132,
+14542,
+60982,
+17112,
+55803,
+26807,
+32195,
+50599,
+16156,
+24298,
+13705,
+25664,
+34188,
+30405,
+53457,
+36950,
+27391,
+51453,
+61494,
+53264,
+39728,
+63057,
+55069,
+48357,
+58137,
+49931,
+22940,
+40827,
+3213,
+8708,
+45235,
+9321,
+11580,
+62335,
+60829,
+42119,
+40694,
+18228,
+60289,
+39583,
+48215,
+33233,
+16966,
+2925,
+33979,
+10171,
+3667,
+29607,
+46274,
+41990,
+53461,
+44653,
+18768,
+47510,
+49770,
+52473,
+5610,
+25230,
+29472,
+32506,
+37218,
+60222,
+37456,
+37267,
+12418,
+57522,
+5014,
+43884,
+1693,
+11027,
+18073,
+36265,
+18327,
+34704,
+10780,
+39277,
+62276,
+37534,
+41387,
+33680,
+20506,
+47734,
+1330,
+19992,
+63050,
+51645,
+185,
+29246,
+43103,
+41845,
+27241,
+17623,
+6371,
+15727,
+8939,
+58889,
+52543,
+22114,
+18413,
+26776,
+14013,
+2054,
+23218,
+35935,
+27840,
+47177,
+2125,
+42449,
+50984,
+61210,
+60372,
+4151,
+9740,
+38606,
+50345,
+1429,
+7013,
+32620,
+15330,
+50371,
+44306,
+59293,
+15821,
+38991,
+60457,
+36247,
+33198,
+51913,
+15737,
+22134,
+32845,
+4652,
+27875,
+41654,
+61161,
+60325,
+44950,
+34998,
+5613,
+36832,
+30758,
+58575,
+56302,
+60435,
+17821,
+62313,
+40954,
+37867,
+38119,
+36024,
+65216,
+26011,
+53064,
+3611,
+16857,
+6987,
+33773,
+20630,
+51240,
+23578,
+25447,
+14240,
+38308,
+4697,
+32630,
+18828,
+59245,
+949,
+5037,
+9146,
+37072,
+846,
+22053,
+43937,
+49508,
+35276,
+31940,
+26295,
+15336,
+51082,
+63747,
+1100,
+63157,
+55235,
+51064,
+5793,
+24173,
+22885,
+49138,
+57197,
+58064,
+37883,
+60170,
+24098,
+54864,
+33009,
+61196,
+50241,
+18006,
+14530,
+36213,
+27572,
+16185,
+7439,
+58077,
+8594,
+62,
+27048,
+25691,
+48715,
+49482,
+20441,
+2460,
+54087,
+58812,
+12662,
+2744,
+192,
+31581,
+17704,
+56110,
+63918,
+4092,
+42801,
+2875,
+27460,
+40558,
+17604,
+34521,
+64940,
+34199,
+58396,
+29254,
+30284,
+37315,
+26958,
+61945,
+52416,
+61377,
+57533,
+1647,
+29946,
+1174,
+50088,
+65398,
+40368,
+38329,
+8350,
+58216,
+40890,
+52713,
+44568,
+60759,
+52587,
+48740,
+42419,
+23628,
+32696,
+56372,
+61669,
+63267,
+56353,
+30004,
+50965,
+53439,
+1415,
+5231,
+5024,
+54100,
+32844,
+22275,
+15738,
+33417,
+24117,
+17748,
+36161,
+619,
+41543,
+45120,
+29171,
+9340,
+60732,
+61268,
+45622,
+17519,
+3117,
+51098,
+3301,
+46385,
+18412,
+22307,
+52544,
+55202,
+64796,
+27190,
+29782,
+26398,
+36363,
+28363,
+12046,
+7949,
+23685,
+46106,
+5219,
+9706,
+18885,
+34803,
+15037,
+48171,
+44071,
+22784,
+31002,
+15963,
+5966,
+1967,
+27741,
+3065,
+45894,
+27451,
+7576,
+42072,
+23840,
+27361,
+9747,
+45214,
+48591,
+34508,
+42023,
+56432,
+3334,
+12342,
+6054,
+29149,
+24334,
+56578,
+4793,
+54041,
+34249,
+36784,
+1494,
+12407,
+26838,
+16359,
+33783,
+33431,
+26522,
+4424,
+14782,
+9036,
+6992,
+43936,
+22232,
+847,
+58196,
+3635,
+35347,
+55172,
+32877,
+39802,
+27508,
+61021,
+12731,
+51851,
+12414,
+45415,
+65424,
+46228,
+51265,
+45566,
+30807,
+65421,
+35334,
+14186,
+48917,
+5096,
+2264,
+55353,
+19637,
+21563,
+19856,
+64802,
+36341,
+31096,
+45228,
+42480,
+57849,
+31691,
+49344,
+39630,
+52507,
+55674,
+11358,
+52666,
+57477,
+30708,
+31966,
+26780,
+50236,
+545,
+30849,
+30216,
+47070,
+25572,
+27143,
+4388,
+22653,
+36999,
+61062,
+23541,
+47251,
+38144,
+14086,
+17168,
+49754,
+42985,
+57764,
+13046,
+21485,
+49503,
+58168,
+41691,
+31655,
+56083,
+9201,
+55617,
+3113,
+34832,
+52729,
+7375,
+1858,
+40684,
+36886,
+58966,
+35383,
+44277,
+33477,
+44108,
+7742,
+19062,
+59862,
+9969,
+36306,
+62908,
+22919,
+8216,
+57634,
+48229,
+23350,
+64943,
+40561,
+13914,
+22999,
+35542,
+8918,
+37689,
+52641,
+13022,
+14444,
+16983,
+56960,
+9775,
+36584,
+44674,
+39643,
+4400,
+23172,
+32177,
+30855,
+4353,
+44751,
+34376,
+63786,
+19737,
+768,
+59061,
+44303,
+63149,
+55599,
+13053,
+54247,
+30398,
+32486,
+13737,
+65247,
+28801,
+5660,
+302,
+30931,
+35098,
+46317,
+12468,
+8111,
+62573,
+14378,
+29203,
+10003,
+45210,
+23864,
+52502,
+29823,
+63290,
+13336,
+31799,
+5922,
+41815,
+40551,
+27968,
+36173,
+434,
+50924,
+65085,
+57691,
+64594,
+13011,
+14290,
+34785,
+13417,
+33297,
+4333,
+57206,
+25075,
+57930,
+2405,
+20403,
+61083,
+12452,
+7432,
+10458,
+30968,
+14150,
+33925,
+31170,
+35672,
+57776,
+15262,
+27151,
+47930,
+3072,
+25785,
+64407,
+22640,
+54194,
+57512,
+29919,
+53344,
+31130,
+7085,
+52044,
+2114,
+35109,
+49808,
+56600,
+34493,
+12288,
+20165,
+58983,
+9362,
+49152,
+65534,
+32770,
+13108,
+40961,
+17874,
+60856,
+26986,
+3277,
+5699,
+22686,
+2260,
+59393,
+11235,
+15522,
+3197,
+37237,
+44621,
+40633,
+24731,
+15214,
+16662,
+64480,
+61504,
+39515,
+15692,
+29226,
+58728,
+49972,
+53693,
+38865,
+5891,
+17809,
+45531,
+36781,
+49315,
+38440,
+3675,
+54217,
+580,
+565,
+22580,
+19876,
+42468,
+64001,
+3502,
+41572,
+3827,
+19193,
+51788,
+57906,
+17154,
+36649,
+25792,
+47701,
+19539,
+49952,
+18837,
+15806,
+19699,
+58462,
+11350,
+50053,
+64120,
+60308,
+57302,
+61821,
+25616,
+59311,
+47135,
+2227,
+14738,
+22567,
+15546,
+31566,
+37365,
+36572,
+8950,
+20231,
+11251,
+36934,
+3839,
+30602,
+1605,
+16120,
+42821,
+26060,
+32386,
+15376,
+40618,
+58392,
+25825,
+26263,
+61009,
+33240,
+60406,
+3923,
+28544,
+226,
+40709,
+40075,
+10521,
+65320,
+60380,
+14682,
+21073,
+51553,
+35973,
+12493,
+63508,
+11861,
+62549,
+62576,
+55951,
+16869,
+53429,
+58869,
+47028,
+33892,
+46043,
+17857,
+50385,
+39286,
+10055,
+53605,
+3533,
+42932,
+35463,
+27767,
+55099,
+18167,
+41445,
+58348,
+5973,
+1482,
+25986,
+28713,
+58935,
+59303,
+39830,
+9610,
+7195,
+47988,
+58752,
+17303,
+34061,
+65386,
+55489,
+62707,
+29144,
+18368,
+7736,
+145,
+46236,
+25900,
+41938,
+49294,
+48837,
+50338,
+30898,
+5645,
+16008,
+26242,
+11621,
+4969,
+14282,
+1194,
+30329,
+10617,
+45472,
+3238,
+30129,
+65153,
+40213,
+22900,
+24655,
+33644,
+11441,
+48349,
+50659,
+10393,
+36477,
+30592,
+65056,
+17341,
+55308,
+16680,
+19767,
+53951,
+20604,
+63337,
+28910,
+12947,
+60522,
+43200,
+63845,
+47245,
+63639,
+27881,
+56143,
+37057,
+18381,
+24277,
+51563,
+58315,
+13515,
+59026,
+55340,
+6448,
+10204,
+13924,
+57410,
+61078,
+10424,
+47766,
+56233,
+21269,
+27248,
+58580,
+53092,
+12488,
+30332,
+297,
+62679,
+53862,
+57235,
+8654,
+46176,
+36720,
+59204,
+61429,
+11985,
+21309,
+13296,
+34083,
+30291,
+47384,
+7045,
+27784,
+25553,
+35606,
+63714,
+56006,
+52520,
+61666,
+58544,
+43483,
+43306,
+16030,
+56540,
+10630,
+525,
+15077,
+26128,
+36054,
+41768,
+47094,
+6323,
+45195,
+59687,
+64608,
+51908,
+29081,
+46836,
+6404,
+46895,
+48121,
+49420,
+31212,
+34360,
+4960,
+24546,
+28168,
+57174,
+4727,
+14209,
+16941,
+29876,
+11923,
+31313,
+36453,
+53117,
+8017,
+19855,
+22026,
+19638,
+59014,
+11165,
+36655,
+50121,
+11276,
+2715,
+40660,
+52879,
+56953,
+18151,
+58494,
+44775,
+46465,
+29943,
+9143,
+2187,
+20998,
+2607,
+35006,
+8345,
+14269,
+37950,
+21442,
+16331,
+63343,
+64761,
+19197,
+56495,
+52751,
+39559,
+42002,
+59793,
+50429,
+2682,
+17344,
+28634,
+32496,
+46589,
+40419,
+53658,
+52147,
+17642,
+49554,
+29735,
+50183,
+6229,
+4030,
+39915,
+39204,
+44848,
+59858,
+59940,
+37700,
+36812,
+6515,
+52674,
+45722,
+49936,
+40865,
+38267,
+16923,
+56310,
+3844,
+11813,
+44297,
+53979,
+42923,
+45410,
+49989,
+33973,
+14598,
+23156,
+33136,
+58513,
+55609,
+49502,
+21987,
+13047,
+22950,
+15318,
+5092,
+63685,
+64405,
+25787,
+62989,
+24111,
+8310,
+33562,
+58444,
+56066,
+47870,
+40241,
+58,
+59984,
+17365,
+43442,
+42754,
+10360,
+7136,
+25281,
+42423,
+30701,
+32825,
+60409,
+10061,
+2691,
+59330,
+46350,
+6843,
+46281,
+26403,
+17944,
+2258,
+22688,
+51783,
+23613,
+45636,
+49819,
+16330,
+21539,
+37951,
+48520,
+15095,
+8306,
+30951,
+33915,
+36439,
+13585,
+32345,
+56929,
+54421,
+32059,
+43569,
+57796,
+62041,
+54137,
+61019,
+27510,
+58146,
+19130,
+63325,
+55786,
+52276,
+5670,
+16053,
+45098,
+15877,
+9160,
+24589,
+28260,
+52118,
+32242,
+26565,
+29000,
+64790,
+25390,
+47145,
+32447,
+15644,
+5564,
+5748,
+8186,
+30372,
+9467,
+37248,
+45667,
+53370,
+51559,
+18194,
+13483,
+62510,
+57243,
+59366,
+45537,
+63870,
+59834,
+37321,
+6672,
+11757,
+5324,
+48283,
+16654,
+8473,
+38771,
+42930,
+3535,
+27895,
+64657,
+19088,
+16096,
+53617,
+41196,
+44501,
+34575,
+61749,
+2095,
+19043,
+17280,
+42590,
+64724,
+31236,
+58520,
+18898,
+58280,
+31785,
+55499,
+62554,
+63582,
+24787,
+47615,
+50036,
+40283,
+54145,
+20719,
+10733,
+54432,
+8683,
+49033,
+25250,
+9412,
+15367,
+30198,
+23326,
+45054,
+60684,
+47749,
+30159,
+10503,
+42484,
+23561,
+20926,
+52150,
+28794,
+32452,
+59514,
+21103,
+18587,
+18677,
+14587,
+36377,
+52338,
+58015,
+50646,
+16229,
+51942,
+10641,
+33139,
+5999,
+37936,
+3152,
+39265,
+13295,
+21615,
+11986,
+56331,
+1012,
+9576,
+23432,
+31118,
+39057,
+39988,
+44839,
+31210,
+49422,
+24009,
+38114,
+42726,
+39441,
+34903,
+35708,
+35171,
+64161,
+60867,
+56723,
+18183,
+62024,
+42977,
+16569,
+11997,
+10658,
+32459,
+58740,
+14688,
+10352,
+56663,
+25003,
+20710,
+37679,
+16060,
+29703,
+57668,
+27247,
+21631,
+56234,
+49115,
+47259,
+38487,
+39848,
+63025,
+262,
+30153,
+43567,
+32061,
+61039,
+6049,
+39789,
+7286,
+24221,
+50535,
+10377,
+4592,
+41552,
+1135,
+37904,
+1934,
+13989,
+37351,
+9295,
+49189,
+42666,
+53723,
+44040,
+11559,
+64818,
+41167,
+55504,
+6475,
+43608,
+2389,
+15165,
+43253,
+6245,
+6412,
+15279,
+45092,
+9025,
+36429,
+4252,
+61362,
+61754,
+13604,
+31110,
+45353,
+41139,
+32838,
+4649,
+40493,
+27529,
+64950,
+61400,
+50564,
+47254,
+31945,
+12710,
+4002,
+40538,
+2080,
+18078,
+39329,
+62177,
+1796,
+41650,
+52058,
+25466,
+14111,
+26161,
+50395,
+64062,
+50910,
+30780,
+36339,
+64804,
+15669,
+18834,
+33067,
+53284,
+15508,
+6141,
+56735,
+1185,
+45258,
+6662,
+51807,
+13644,
+31331,
+32165,
+11368,
+1984,
+63491,
+57430,
+33578,
+40997,
+37482,
+17440,
+56685,
+53291,
+16978,
+41213,
+65441,
+15178,
+64165,
+61427,
+59206,
+53497,
+36131,
+62021,
+5725,
+30984,
+64307,
+11306,
+22548,
+42833,
+40796,
+1503,
+8411,
+4621,
+19923,
+42529,
+52013,
+63488,
+65506,
+15293,
+61240,
+9261,
+3730,
+41280,
+29049,
+36713,
+24676,
+15795,
+51751,
+37973,
+14662,
+41246,
+58272,
+6921,
+56534,
+56940,
+7648,
+1449,
+34939,
+59185,
+16264,
+37647,
+38105,
+30059,
+53488,
+48996,
+35452,
+61815,
+13827,
+6622,
+36505,
+57393,
+4170,
+5139,
+40968,
+18586,
+21326,
+59515,
+20682,
+10166,
+29872,
+9496,
+40005,
+62011,
+5151,
+34614,
+1190,
+18435,
+64987,
+64583,
+895,
+18799,
+39996,
+24961,
+57869,
+18815,
+19621,
+19109,
+49567,
+39773,
+47899,
+18298,
+40783,
+47542,
+10800,
+51552,
+21742,
+14683,
+65114,
+15997,
+47001,
+6461,
+60964,
+61166,
+30250,
+7993,
+32294,
+39863,
+3146,
+15849,
+56123,
+50172,
+49222,
+28688,
+30420,
+33279,
+46383,
+3303,
+58417,
+35720,
+20045,
+59238,
+53748,
+39202,
+39917,
+65428,
+55222,
+10873,
+15882,
+20038,
+29275,
+50282,
+24340,
+44168,
+30963,
+60181,
+39005,
+13639,
+19763,
+1848,
+32929,
+45045,
+47525,
+4211,
+13709,
+46288,
+13835,
+15940,
+45060,
+18997,
+1612,
+39697,
+23858,
+62272,
+2551,
+28789,
+37386,
+47325,
+3481,
+62583,
+50158,
+54010,
+47121,
+10667,
+29962,
+15080,
+48038,
+16080,
+43062,
+20201,
+2606,
+21545,
+2188,
+11827,
+44710,
+57611,
+18831,
+27177,
+63211,
+50091,
+16346,
+22873,
+54470,
+64241,
+24900,
+32984,
+6812,
+1290,
+48938,
+28592,
+14645,
+17448,
+2540,
+51043,
+13273,
+57041,
+24858,
+16378,
+3122,
+12823,
+43384,
+4779,
+7583,
+23791,
+57872,
+33103,
+49227,
+22645,
+29262,
+28119,
+32054,
+27465,
+7596,
+36906,
+46234,
+147,
+60667,
+45321,
+30693,
+15244,
+35351,
+2969,
+34932,
+46160,
+40045,
+48117,
+11544,
+44892,
+31706,
+6175,
+9180,
+34008,
+51617,
+54346,
+14801,
+41359,
+53928,
+57312,
+64510,
+19611,
+25762,
+17640,
+52149,
+21331,
+23562,
+38303,
+54480,
+20671,
+6497,
+354,
+3324,
+48252,
+28299,
+43707,
+24905,
+36485,
+29583,
+2059,
+23957,
+41162,
+19148,
+15563,
+11846,
+3464,
+11217,
+26238,
+50914,
+35396,
+32676,
+59808,
+6946,
+57123,
+34012,
+45869,
+55541,
+30209,
+64872,
+32276,
+41670,
+5029,
+55284,
+48246,
+48697,
+58116,
+12075,
+43184,
+46770,
+5690,
+34399,
+51579,
+13130,
+51653,
+60147,
+54678,
+48185,
+56454,
+23605,
+27911,
+14636,
+40079,
+23732,
+47840,
+27255,
+50998,
+31922,
+28952,
+43595,
+25895,
+63030,
+53325,
+36776,
+43131,
+59328,
+2693,
+14135,
+27118,
+46809,
+17478,
+35426,
+56651,
+46928,
+10817,
+49284,
+63308,
+50801,
+5147,
+52922,
+61075,
+62732,
+51678,
+6532,
+28812,
+40652,
+26072,
+41782,
+2073,
+56153,
+11379,
+10442,
+50696,
+6038,
+53709,
+44542,
+7927,
+42400,
+64810,
+17965,
+52818,
+30935,
+8477,
+27683,
+38051,
+15779,
+48564,
+31306,
+12938,
+57054,
+60338,
+16152,
+60791,
+8661,
+65137,
+12977,
+1723,
+2624,
+30535,
+56423,
+5819,
+45190,
+34889,
+11709,
+19830,
+2354,
+60334,
+1601,
+37515,
+59747,
+45115,
+28108,
+23715,
+10151,
+18238,
+61183,
+43602,
+31162,
+25330,
+12355,
+61491,
+54067,
+41,
+7803,
+34743,
+3700,
+42500,
+8590,
+32636,
+5234,
+24701,
+1240,
+24716,
+45673,
+39083,
+38905,
+40690,
+34790,
+22943,
+7042,
+43987,
+19073,
+41270,
+47062,
+53707,
+6040,
+35064,
+17566,
+10626,
+48443,
+25268,
+52705,
+25182,
+25916,
+24957,
+53388,
+58584,
+57459,
+18858,
+7469,
+4414,
+16671,
+31830,
+19365,
+64472,
+4867,
+39649,
+56981,
+28514,
+35046,
+47863,
+58266,
+54513,
+51471,
+49099,
+62432,
+5385,
+10429,
+41114,
+51157,
+36336,
+50561,
+10732,
+21348,
+54146,
+32033,
+58846,
+38275,
+20385,
+22585,
+7496,
+37678,
+21275,
+25004,
+24790,
+47522,
+28331,
+50005,
+10639,
+51944,
+40113,
+3378,
+39490,
+25548,
+44237,
+34328,
+32759,
+61683,
+1821,
+9533,
+54024,
+2819,
+26332,
+44025,
+5680,
+17063,
+32420,
+8865,
+45528,
+10165,
+21101,
+59516,
+42317,
+29604,
+6361,
+56957,
+56348,
+63391,
+17172,
+15518,
+6496,
+20922,
+54481,
+11158,
+371,
+47392,
+1500,
+9825,
+33850,
+27578,
+47321,
+57036,
+27195,
+60769,
+23029,
+29975,
+31731,
+23870,
+4693,
+34417,
+26482,
+18670,
+17211,
+37807,
+15340,
+16931,
+53992,
+44595,
+33832,
+38018,
+64321,
+52970,
+42272,
+17036,
+36522,
+59446,
+47857,
+41520,
+64924,
+13601,
+54,
+51239,
+22247,
+33774,
+60101,
+52720,
+11696,
+52741,
+12714,
+42256,
+5088,
+15751,
+33223,
+38129,
+28916,
+17051,
+57809,
+20467,
+49237,
+16145,
+3577,
+32220,
+31363,
+6907,
+57806,
+65343,
+33958,
+63336,
+21658,
+53952,
+7237,
+17920,
+15503,
+30508,
+42740,
+27310,
+18128,
+29572,
+29688,
+12504,
+62383,
+26274,
+28306,
+10100,
+5211,
+43269,
+30540,
+40809,
+45469,
+64101,
+36857,
+41820,
+55388,
+61760,
+14424,
+27706,
+28338,
+33439,
+37381,
+22538,
+63616,
+4336,
+137,
+25171,
+11678,
+39927,
+239,
+30858,
+30101,
+8124,
+2808,
+62085,
+26626,
+60800,
+51354,
+41448,
+38045,
+26489,
+24625,
+49195,
+17427,
+46183,
+61305,
+36913,
+33888,
+29421,
+49636,
+53696,
+46757,
+37179,
+5312,
+41509,
+8784,
+45157,
+284,
+60479,
+6105,
+23818,
+47150,
+60095,
+59118,
+28930,
+53831,
+7151,
+8074,
+50710,
+37296,
+27773,
+60099,
+33776,
+4225,
+58792,
+31788,
+26363,
+4130,
+53814,
+24562,
+9801,
+2067,
+29949,
+27404,
+11212,
+30078,
+15706,
+63022,
+47733,
+22324,
+33681,
+8646,
+14985,
+11332,
+62369,
+7906,
+9425,
+1621,
+62346,
+55615,
+9203,
+56260,
+32607,
+44509,
+18013,
+37208,
+38662,
+23444,
+45937,
+12243,
+59420,
+42210,
+44199,
+4854,
+35746,
+26218,
+12484,
+49596,
+44516,
+46541,
+59369,
+22711,
+34169,
+33015,
+25951,
+32570,
+3983,
+49236,
+20615,
+57810,
+8560,
+40176,
+46846,
+64195,
+22482,
+56866,
+961,
+54706,
+26004,
+24429,
+52106,
+63669,
+55639,
+28356,
+60227,
+40133,
+33488,
+49438,
+29879,
+50257,
+44310,
+56513,
+27115,
+2459,
+22193,
+49483,
+44121,
+49661,
+20238,
+28840,
+61650,
+26091,
+10995,
+34337,
+57646,
+33949,
+32090,
+12528,
+36418,
+14480,
+62723,
+48880,
+5789,
+44745,
+37430,
+30018,
+8284,
+23993,
+43411,
+36367,
+63781,
+6424,
+45882,
+35077,
+63055,
+39730,
+49282,
+10819,
+45144,
+35993,
+25399,
+61082,
+21881,
+2406,
+24996,
+65129,
+19646,
+42298,
+15487,
+3255,
+38506,
+54717,
+45130,
+29545,
+36598,
+26623,
+25210,
+16950,
+1273,
+22584,
+20714,
+38276,
+65074,
+46955,
+56752,
+44998,
+65254,
+64376,
+54634,
+46139,
+22831,
+15527,
+36032,
+15921,
+64900,
+31636,
+55344,
+54523,
+22412,
+55719,
+29021,
+20214,
+34846,
+49473,
+103,
+30879,
+41159,
+27695,
+34911,
+36706,
+14611,
+454,
+50126,
+37924,
+46785,
+45483,
+13881,
+59717,
+44736,
+30649,
+41532,
+1833,
+59213,
+43948,
+5851,
+16990,
+32783,
+23256,
+25123,
+13255,
+14996,
+47704,
+18991,
+10550,
+53494,
+43110,
+17731,
+55533,
+43629,
+41674,
+1767,
+55789,
+43457,
+16947,
+19386,
+59967,
+2590,
+65108,
+49460,
+23707,
+1784,
+14063,
+60249,
+59526,
+55473,
+34440,
+43516,
+23058,
+26990,
+36493,
+5771,
+41887,
+56828,
+43163,
+56217,
+11858,
+57359,
+55247,
+36690,
+1889,
+64255,
+27679,
+63722,
+61600,
+51668,
+24041,
+63867,
+23365,
+17057,
+29596,
+53625,
+18549,
+47601,
+53578,
+27826,
+64351,
+44356,
+58876,
+3022,
+55934,
+18095,
+39350,
+38686,
+14590,
+60723,
+818,
+42412,
+14373,
+22985,
+15959,
+12784,
+25980,
+4486,
+50377,
+16529,
+16863,
+33333,
+13966,
+54605,
+16051,
+5672,
+12778,
+919,
+36545,
+29330,
+11240,
+40923,
+64867,
+55056,
+9300,
+18138,
+832,
+11409,
+26727,
+60813,
+7619,
+28839,
+20437,
+49662,
+16313,
+36851,
+53148,
+30574,
+11250,
+21769,
+8951,
+47038,
+64011,
+25872,
+19827,
+28509,
+58074,
+12130,
+444,
+56407,
+15924,
+20158,
+39647,
+4869,
+44585,
+34845,
+20364,
+29022,
+53141,
+24122,
+41777,
+27643,
+59054,
+24863,
+30790,
+49945,
+19375,
+25494,
+2605,
+21000,
+43063,
+52549,
+46922,
+45860,
+57181,
+57239,
+32418,
+17065,
+57439,
+63385,
+55971,
+22694,
+1107,
+62758,
+14321,
+34426,
+44318,
+24399,
+59825,
+54028,
+26283,
+60045,
+25662,
+13707,
+4213,
+14449,
+64962,
+26604,
+39471,
+64663,
+26426,
+30762,
+9102,
+62687,
+58982,
+21850,
+12289,
+31639,
+29648,
+54813,
+18312,
+39646,
+20219,
+15925,
+49699,
+47305,
+54315,
+6595,
+51439,
+37551,
+4303,
+22674,
+5559,
+64984,
+26477,
+1246,
+33006,
+46715,
+42751,
+34537,
+64538,
+13069,
+55725,
+27322,
+32941,
+34186,
+25666,
+53024,
+32010,
+53166,
+3005,
+10575,
+22613,
+44043,
+15549,
+12531,
+1345,
+53122,
+43497,
+114,
+30205,
+2290,
+15477,
+35852,
+37991,
+55300,
+65045,
+48453,
+26802,
+7065,
+10862,
+35234,
+52412,
+45798,
+64771,
+10881,
+3211,
+40829,
+64660,
+3218,
+29909,
+55794,
+75,
+62951,
+54687,
+7250,
+34184,
+32943,
+1308,
+48966,
+53542,
+47020,
+40928,
+39116,
+32514,
+29344,
+13095,
+60939,
+29740,
+7242,
+36140,
+24496,
+49644,
+14513,
+54859,
+3911,
+1492,
+36786,
+63320,
+1391,
+52779,
+24727,
+44953,
+1437,
+54919,
+43859,
+62300,
+34815,
+3858,
+48657,
+27355,
+7593,
+34461,
+6754,
+38353,
+18751,
+45187,
+51657,
+30765,
+9312,
+33906,
+62631,
+26103,
+27801,
+59237,
+21049,
+35721,
+46111,
+36614,
+46302,
+47462,
+29274,
+21040,
+15883,
+26956,
+37317,
+27083,
+61572,
+46277,
+19755,
+25760,
+19613,
+63590,
+48396,
+38417,
+18730,
+15242,
+30695,
+26785,
+60329,
+47313,
+47610,
+25304,
+13774,
+7128,
+60537,
+53503,
+56132,
+30630,
+48736,
+56671,
+44853,
+55373,
+47727,
+43723,
+28757,
+14602,
+58483,
+18994,
+6170,
+44149,
+1668,
+42174,
+47587,
+19393,
+52092,
+42602,
+63049,
+22321,
+1331,
+50433,
+60960,
+14194,
+28455,
+7060,
+39132,
+47595,
+36932,
+11253,
+26981,
+64530,
+51271,
+17744,
+8008,
+39189,
+26077,
+16002,
+37948,
+14271,
+43501,
+34731,
+54490,
+33612,
+17268,
+63538,
+10071,
+476,
+23358,
+32322,
+7430,
+12454,
+65317,
+12869,
+8062,
+39780,
+4772,
+358,
+48743,
+14351,
+4024,
+62656,
+2891,
+5825,
+62557,
+23,
+46423,
+36255,
+10299,
+31007,
+48023,
+50921,
+60278,
+10982,
+38125,
+62700,
+25028,
+59149,
+48279,
+6290,
+64590,
+48842,
+32267,
+8054,
+16908,
+37531,
+29925,
+42528,
+21145,
+4622,
+4665,
+9847,
+4320,
+17883,
+19720,
+4268,
+43416,
+48019,
+51826,
+19251,
+16181,
+32729,
+39153,
+55875,
+7809,
+3131,
+16176,
+5693,
+14630,
+41923,
+44092,
+15719,
+37493,
+57293,
+26087,
+62747,
+14570,
+12100,
+6500,
+16292,
+57099,
+39912,
+26025,
+26313,
+30259,
+33599,
+37203,
+53688,
+48407,
+89,
+38595,
+52723,
+48664,
+33962,
+42467,
+21805,
+22581,
+59011,
+57774,
+35674,
+28288,
+44141,
+12168,
+11720,
+12509,
+50728,
+40820,
+44768,
+26455,
+45703,
+62488,
+58980,
+62689,
+36280,
+64801,
+22025,
+21564,
+8018,
+17793,
+24512,
+51836,
+6349,
+47714,
+3226,
+13608,
+1595,
+62294,
+55289,
+18555,
+60936,
+18907,
+54547,
+61411,
+63981,
+5775,
+57743,
+39081,
+45675,
+3050,
+25885,
+2353,
+20798,
+11710,
+28508,
+20226,
+25873,
+48423,
+9736,
+40318,
+8798,
+23813,
+30067,
+38600,
+47200,
+38885,
+12862,
+44032,
+15602,
+62560,
+51422,
+15171,
+15625,
+34120,
+33460,
+61090,
+14609,
+36708,
+26279,
+23924,
+31908,
+64928,
+41285,
+19010,
+33702,
+26861,
+22542,
+10621,
+18591,
+24595,
+30891,
+55043,
+2593,
+5534,
+17146,
+38000,
+12908,
+50409,
+18024,
+45806,
+39951,
+52364,
+18465,
+39967,
+56567,
+7973,
+62571,
+8113,
+35758,
+27351,
+50704,
+47647,
+11314,
+53450,
+53950,
+21660,
+16681,
+8200,
+1847,
+21031,
+13640,
+60748,
+22928,
+53822,
+31382,
+57473,
+25759,
+20031,
+46278,
+5919,
+37563,
+58247,
+56322,
+5741,
+56487,
+45853,
+63144,
+41400,
+51307,
+30888,
+49254,
+31087,
+13124,
+45430,
+767,
+21932,
+63787,
+53210,
+56547,
+49768,
+47512,
+45754,
+25587,
+53472,
+39283,
+51813,
+54530,
+25743,
+27090,
+24669,
+64138,
+4267,
+19917,
+17884,
+56553,
+25441,
+9550,
+9484,
+59962,
+58213,
+30644,
+788,
+53743,
+12177,
+19703,
+58969,
+61596,
+35381,
+58968,
+19708,
+12178,
+1314,
+58461,
+21788,
+15807,
+28661,
+34928,
+35765,
+9960,
+9245,
+2333,
+30467,
+39685,
+42047,
+23145,
+253,
+13160,
+9049,
+45790,
+2394,
+38256,
+55657,
+2774,
+5858,
+18614,
+45340,
+8357,
+40548,
+1016,
+12116,
+51708,
+58917,
+2209,
+31524,
+44641,
+9997,
+38787,
+58815,
+36158,
+27594,
+58254,
+10,
+59580,
+40571,
+14799,
+54348,
+59112,
+45124,
+30461,
+15373,
+49693,
+55155,
+43101,
+29248,
+33519,
+42297,
+20399,
+65130,
+62471,
+43450,
+38087,
+41409,
+57772,
+59013,
+21562,
+22027,
+55354,
+25110,
+51027,
+26588,
+6306,
+8927,
+26552,
+25929,
+1980,
+25177,
+59002,
+1997,
+55468,
+65193,
+19108,
+21083,
+18816,
+31601,
+26154,
+17536,
+52604,
+30565,
+63589,
+20029,
+25761,
+20930,
+64511,
+38492,
+13453,
+15506,
+53286,
+58995,
+1033,
+59897,
+43026,
+42092,
+45050,
+53295,
+35417,
+12678,
+28792,
+52152,
+16018,
+39072,
+30350,
+35433,
+18343,
+17725,
+42227,
+24499,
+36041,
+51844,
+65112,
+14685,
+3397,
+36164,
+63840,
+3672,
+15221,
+46211,
+65460,
+2588,
+59969,
+16812,
+37145,
+30550,
+5971,
+58350,
+46249,
+22635,
+39970,
+57558,
+47786,
+37946,
+16004,
+40265,
+26513,
+25804,
+40174,
+8562,
+1746,
+4015,
+49627,
+5517,
+51060,
+23810,
+10972,
+33967,
+61249,
+14417,
+18733,
+7169,
+48747,
+23196,
+12271,
+25811,
+49951,
+21792,
+47702,
+14998,
+39186,
+46827,
+41921,
+14632,
+34926,
+28663,
+25245,
+14437,
+62238,
+28199,
+48510,
+1252,
+24427,
+26006,
+61438,
+10572,
+9243,
+9962,
+9526,
+14963,
+62827,
+64909,
+38048,
+5797,
+26417,
+32834,
+53219,
+48905,
+64210,
+56691,
+15761,
+39339,
+33874,
+27276,
+54827,
+54198,
+6631,
+57168,
+64653,
+47725,
+55375,
+31644,
+43809,
+47681,
+13681,
+50665,
+49373,
+33473,
+50582,
+59100,
+22719,
+5173,
+65500,
+34590,
+52071,
+35152,
+11146,
+55208,
+2076,
+50550,
+54207,
+29018,
+52927,
+17595,
+9912,
+51747,
+24212,
+9109,
+54337,
+1148,
+52134,
+37063,
+65271,
+10388,
+3950,
+34619,
+27929,
+16668,
+29461,
+24757,
+6902,
+9476,
+35221,
+15841,
+12631,
+33252,
+58707,
+58801,
+46777,
+52650,
+35404,
+16407,
+4830,
+25722,
+28865,
+9439,
+33554,
+18708,
+23618,
+49511,
+14732,
+61450,
+26867,
+11780,
+41851,
+43435,
+60319,
+5252,
+58227,
+29815,
+45494,
+28924,
+63381,
+11010,
+62193,
+55383,
+58089,
+19274,
+58986,
+1772,
+37889,
+48973,
+9442,
+4068,
+11050,
+26676,
+2610,
+58284,
+53600,
+13876,
+51998,
+25053,
+48815,
+18003,
+4300,
+32000,
+7003,
+10902,
+45161,
+42707,
+31792,
+49750,
+52091,
+19996,
+47588,
+52944,
+18566,
+17438,
+37484,
+59966,
+20321,
+16948,
+25212,
+50714,
+64269,
+28709,
+37513,
+1603,
+30604,
+44591,
+25493,
+20204,
+49946,
+2889,
+62658,
+11273,
+42303,
+5654,
+34166,
+51409,
+64471,
+20739,
+31831,
+58260,
+56882,
+43917,
+28818,
+1063,
+60672,
+37930,
+3308,
+48109,
+44986,
+4233,
+8375,
+48207,
+40721,
+32821,
+63088,
+3401,
+13852,
+41155,
+7213,
+40546,
+8359,
+18447,
+61108,
+60491,
+25943,
+38481,
+37395,
+26669,
+64415,
+62381,
+12506,
+40978,
+36559,
+41935,
+35159,
+50315,
+45960,
+78,
+55583,
+59276,
+61633,
+3141,
+4861,
+56035,
+2636,
+37330,
+4102,
+49006,
+48676,
+52038,
+48491,
+15350,
+112,
+43499,
+14273,
+12641,
+4817,
+5374,
+28630,
+44582,
+65391,
+43843,
+39638,
+57139,
+39907,
+180,
+61193,
+35946,
+16960,
+56902,
+25604,
+33769,
+56442,
+7186,
+45969,
+42903,
+55910,
+33178,
+12374,
+520,
+35474,
+52002,
+13981,
+37288,
+25575,
+2730,
+9360,
+58985,
+19419,
+58090,
+50496,
+64697,
+61398,
+64952,
+37595,
+449,
+47042,
+65173,
+35929,
+43181,
+23548,
+3964,
+45505,
+45783,
+59619,
+58693,
+44085,
+39135,
+55894,
+40790,
+16180,
+19912,
+51827,
+44513,
+51703,
+55693,
+39218,
+65411,
+14157,
+28983,
+47632,
+9251,
+51888,
+48784,
+41435,
+27074,
+28922,
+45496,
+18076,
+2082,
+48202,
+7695,
+64184,
+17791,
+8020,
+25469,
+63127,
+54792,
+14049,
+16201,
+3597,
+30438,
+43622,
+53070,
+52684,
+59944,
+33836,
+37477,
+63221,
+17185,
+29040,
+24651,
+37265,
+37458,
+31257,
+13321,
+55399,
+52375,
+56490,
+3877,
+27294,
+1809,
+32335,
+50688,
+56494,
+21535,
+64762,
+30568,
+51787,
+21799,
+3828,
+49449,
+7387,
+4942,
+6206,
+44083,
+58695,
+29747,
+60949,
+34434,
+47330,
+34195,
+23553,
+29336,
+4938,
+37500,
+13510,
+3411,
+1480,
+5975,
+54670,
+24217,
+57103,
+3436,
+30529,
+57194,
+62969,
+16698,
+15201,
+2842,
+46497,
+5753,
+12974,
+496,
+11737,
+32736,
+11444,
+32257,
+44484,
+62235,
+43421,
+47126,
+33275,
+15562,
+20909,
+41163,
+51198,
+22997,
+13916,
+59402,
+65335,
+23803,
+56268,
+42139,
+40768,
+52157,
+52170,
+4360,
+33844,
+16195,
+55442,
+63324,
+21422,
+58147,
+39364,
+29707,
+12860,
+38887,
+2416,
+37013,
+57781,
+61921,
+34664,
+59456,
+56559,
+1337,
+5242,
+65513,
+45592,
+29283,
+59999,
+36563,
+49566,
+21082,
+19622,
+65194,
+58831,
+30712,
+62796,
+31741,
+37510,
+54386,
+9858,
+47570,
+12721,
+10340,
+42203,
+62527,
+36091,
+25991,
+49242,
+25417,
+15974,
+16095,
+21373,
+64658,
+40831,
+50953,
+59098,
+50584,
+47475,
+64170,
+31714,
+7746,
+30515,
+1223,
+58738,
+32461,
+41269,
+20761,
+43988,
+35595,
+2794,
+9685,
+59780,
+5637,
+43115,
+57830,
+911,
+59861,
+21966,
+7743,
+6509,
+10199,
+64722,
+42592,
+58497,
+16760,
+7481,
+60116,
+9759,
+18487,
+37750,
+64295,
+40823,
+50308,
+53380,
+37433,
+17279,
+21365,
+2096,
+62694,
+46375,
+59785,
+49960,
+35273,
+23621,
+62156,
+7117,
+59673,
+9480,
+15872,
+33106,
+34754,
+14979,
+49145,
+31556,
+55368,
+39028,
+52976,
+17361,
+14764,
+3077,
+15310,
+62439,
+56756,
+4084,
+51468,
+9034,
+14784,
+61983,
+33701,
+19799,
+41286,
+7506,
+10320,
+63106,
+59254,
+23109,
+56415,
+31451,
+3187,
+39574,
+58331,
+1611,
+21020,
+45061,
+6169,
+20002,
+58484,
+10549,
+20333,
+47705,
+33885,
+12346,
+29322,
+37967,
+13784,
+27566,
+58646,
+33169,
+64477,
+63372,
+36434,
+4845,
+35312,
+31501,
+43080,
+39946,
+39126,
+58756,
+14568,
+62749,
+50888,
+42626,
+50883,
+37544,
+38714,
+30028,
+46902,
+40704,
+674,
+9516,
+14235,
+29837,
+32708,
+36900,
+1912,
+17610,
+31681,
+42496,
+49515,
+36220,
+33040,
+44013,
+25119,
+56858,
+32407,
+3546,
+63949,
+54801,
+50593,
+8834,
+4066,
+9444,
+30275,
+51392,
+25796,
+10699,
+16373,
+47955,
+58679,
+52530,
+24949,
+36179,
+23899,
+53947,
+5707,
+62105,
+13372,
+17619,
+24821,
+40157,
+12249,
+6544,
+9794,
+36687,
+8863,
+32422,
+6071,
+3552,
+31838,
+49709,
+47691,
+54546,
+19841,
+60937,
+13097,
+30678,
+34424,
+14323,
+8717,
+58855,
+58279,
+21359,
+58521,
+11324,
+63501,
+3930,
+29010,
+30864,
+33811,
+15137,
+37246,
+9469,
+17669,
+34802,
+22099,
+9707,
+10242,
+24487,
+62479,
+49545,
+37415,
+42489,
+51445,
+14433,
+38100,
+11990,
+65221,
+7699,
+31263,
+6644,
+9548,
+25443,
+37939,
+64144,
+12760,
+53089,
+35310,
+4847,
+59279,
+51335,
+7468,
+20744,
+57460,
+23916,
+2374,
+54445,
+57354,
+810,
+59154,
+43018,
+59202,
+36722,
+31887,
+14143,
+39743,
+17838,
+17672,
+7273,
+47077,
+32118,
+41422,
+15805,
+21790,
+49953,
+33066,
+21188,
+15670,
+27176,
+20993,
+57612,
+59244,
+22239,
+32631,
+7683,
+60395,
+10671,
+32530,
+45394,
+22787,
+50678,
+16608,
+62622,
+31600,
+19620,
+21084,
+57870,
+23793,
+38785,
+9999,
+22762,
+33603,
+17454,
+55393,
+6346,
+57009,
+26617,
+63620,
+1335,
+56561,
+39995,
+21088,
+896,
+64369,
+4606,
+54058,
+7989,
+32425,
+57310,
+53930,
+51596,
+3795,
+54713,
+28776,
+47009,
+49970,
+58730,
+59096,
+50955,
+11840,
+44993,
+28359,
+4052,
+63921,
+8604,
+37343,
+59250,
+3306,
+37932,
+26580,
+64500,
+47509,
+22352,
+44654,
+59848,
+33818,
+6458,
+2700,
+7814,
+6735,
+46089,
+12888,
+53955,
+3204,
+57420,
+38204,
+42085,
+27612,
+45186,
+20055,
+38354,
+52352,
+4450,
+49047,
+31440,
+16268,
+12227,
+53152,
+38776,
+42338,
+47607,
+60903,
+2137,
+35467,
+4201,
+50768,
+7168,
+19546,
+14418,
+15241,
+20025,
+38418,
+44684,
+48060,
+57344,
+65530,
+10924,
+40331,
+29492,
+38837,
+9638,
+51151,
+34134,
+34556,
+41225,
+40842,
+18109,
+37901,
+57527,
+26350,
+46002,
+23617,
+19439,
+33555,
+36244,
+24046,
+4040,
+53115,
+36455,
+43953,
+59134,
+30415,
+53336,
+11427,
+2920,
+12543,
+29431,
+42026,
+25360,
+45074,
+1343,
+12533,
+52074,
+7172,
+17589,
+16443,
+16728,
+7605,
+63134,
+56998,
+11062,
+24704,
+14586,
+21324,
+18588,
+27980,
+63512,
+54885,
+52961,
+17210,
+20651,
+26483,
+3458,
+63757,
+13521,
+38634,
+35230,
+8930,
+37088,
+13342,
+54133,
+54164,
+53914,
+60570,
+17540,
+47578,
+62498,
+62410,
+48703,
+13437,
+56657,
+55293,
+41052,
+42569,
+54258,
+54282,
+38558,
+59132,
+43955,
+17417,
+22618,
+16357,
+26840,
+33994,
+35508,
+46574,
+24834,
+9377,
+49304,
+51871,
+29314,
+24320,
+36635,
+36739,
+49721,
+59724,
+42794,
+37778,
+15383,
+14694,
+57769,
+23703,
+6396,
+17274,
+12987,
+45339,
+19678,
+5859,
+39737,
+6085,
+8885,
+44688,
+54721,
+11042,
+54903,
+36557,
+40980,
+24125,
+41837,
+1663,
+10123,
+64198,
+60297,
+48988,
+5304,
+58904,
+8371,
+16260,
+24594,
+19794,
+10622,
+27979,
+18676,
+21325,
+21104,
+40969,
+29711,
+462,
+45155,
+8786,
+34966,
+57385,
+7969,
+42045,
+39687,
+60414,
+37299,
+61098,
+33626,
+61034,
+13845,
+35742,
+29426,
+17437,
+19390,
+52945,
+24038,
+52580,
+58006,
+12856,
+33450,
+11572,
+505,
+42613,
+60935,
+19843,
+55290,
+42057,
+49234,
+3985,
+47600,
+20284,
+53626,
+11265,
+59936,
+38937,
+37324,
+53902,
+12749,
+25563,
+53635,
+403,
+55967,
+49539,
+44468,
+59077,
+62839,
+244,
+65003,
+38733,
+58474,
+36760,
+46751,
+15568,
+23776,
+6980,
+34633,
+17022,
+10569,
+30587,
+63389,
+56350,
+11994,
+45448,
+24610,
+42115,
+25087,
+14716,
+17110,
+60984,
+6667,
+43807,
+31646,
+50023,
+4834,
+47822,
+58712,
+32030,
+42389,
+57967,
+56585,
+45308,
+1527,
+57689,
+65087,
+46271,
+365,
+34202,
+1166,
+60933,
+42615,
+5994,
+37749,
+19051,
+9760,
+57340,
+38055,
+40259,
+34272,
+58895,
+2103,
+3770,
+11871,
+8839,
+24266,
+44778,
+11831,
+33196,
+36249,
+4020,
+54406,
+29896,
+35320,
+43534,
+39966,
+19780,
+52365,
+54203,
+3170,
+58263,
+46696,
+33420,
+12104,
+2237,
+57075,
+54539,
+54787,
+41912,
+30847,
+547,
+37912,
+57697,
+61107,
+19341,
+8360,
+26730,
+23768,
+43946,
+59215,
+55906,
+40065,
+30787,
+26143,
+26475,
+64986,
+21092,
+1191,
+48439,
+36413,
+55947,
+55662,
+18101,
+62667,
+32187,
+40757,
+32628,
+4699,
+28907,
+33665,
+58820,
+48861,
+36855,
+64103,
+11526,
+44530,
+54871,
+26775,
+22306,
+22115,
+46386,
+32263,
+62810,
+60898,
+65213,
+43016,
+59156,
+27808,
+6225,
+43759,
+63311,
+28042,
+8246,
+2091,
+3541,
+62263,
+1703,
+3424,
+40146,
+44132,
+33091,
+25527,
+5631,
+41718,
+45003,
+5443,
+58953,
+48315,
+7148,
+24276,
+21646,
+37058,
+52814,
+55199,
+31194,
+33842,
+4362,
+23509,
+40179,
+26772,
+635,
+33903,
+7735,
+21697,
+29145,
+17767,
+3044,
+35363,
+52471,
+49772,
+37696,
+43238,
+63413,
+3088,
+49350,
+58100,
+38983,
+39610,
+40712,
+60741,
+36863,
+50976,
+25059,
+7776,
+33549,
+1804,
+40594,
+17724,
+19590,
+35434,
+57415,
+32136,
+10846,
+26936,
+45719,
+26653,
+17579,
+35092,
+42433,
+16420,
+18280,
+54464,
+4541,
+34703,
+22332,
+36266,
+51582,
+34310,
+14468,
+12294,
+56519,
+10707,
+24660,
+7939,
+48642,
+49210,
+28691,
+4398,
+39645,
+20160,
+54814,
+31437,
+9951,
+11387,
+40084,
+4314,
+9767,
+12836,
+23414,
+63965,
+55818,
+36074,
+40782,
+21078,
+47900,
+31153,
+56019,
+51304,
+1691,
+43886,
+1899,
+2519,
+15423,
+1093,
+41995,
+43542,
+27394,
+14972,
+44327,
+33593,
+54463,
+18331,
+16421,
+44271,
+27051,
+31517,
+31551,
+4257,
+58658,
+1840,
+60483,
+49585,
+12405,
+1496,
+56826,
+41889,
+52792,
+57675,
+3811,
+32546,
+41276,
+22658,
+25222,
+24807,
+24794,
+33088,
+49895,
+59035,
+49247,
+13246,
+8733,
+50178,
+31165,
+16237,
+11540,
+53982,
+12565,
+23267,
+59164,
+49812,
+9133,
+2993,
+61182,
+20788,
+10152,
+7450,
+2886,
+62428,
+15837,
+45382,
+11223,
+50269,
+60288,
+22367,
+40695,
+62628,
+5207,
+45517,
+17928,
+4636,
+17894,
+27168,
+2295,
+42503,
+37958,
+44875,
+8502,
+18052,
+28150,
+44525,
+62057,
+54499,
+40854,
+31102,
+46355,
+31343,
+52426,
+10084,
+52853,
+48150,
+40105,
+58598,
+26724,
+39539,
+5658,
+28803,
+13482,
+21393,
+51560,
+48652,
+14328,
+55892,
+39137,
+29176,
+48896,
+38311,
+5723,
+62023,
+21287,
+56724,
+52798,
+33284,
+39209,
+62267,
+11689,
+6465,
+4410,
+34961,
+54289,
+16579,
+62190,
+64069,
+61562,
+41444,
+21717,
+55100,
+9716,
+53735,
+38659,
+51049,
+38223,
+62769,
+25960,
+56965,
+61646,
+34151,
+13620,
+35245,
+43926,
+58493,
+21552,
+56954,
+48287,
+7125,
+50777,
+65165,
+44870,
+64038,
+32857,
+14280,
+4971,
+39716,
+831,
+20245,
+9301,
+16424,
+12063,
+26256,
+36652,
+36148,
+23459,
+30990,
+29571,
+20596,
+27311,
+64427,
+8038,
+857,
+55379,
+37412,
+31877,
+6919,
+58274,
+31488,
+61594,
+58971,
+23780,
+6943,
+34138,
+27330,
+16899,
+37900,
+18714,
+40843,
+55142,
+29062,
+60462,
+57982,
+43059,
+62666,
+18429,
+55663,
+4787,
+42611,
+507,
+39349,
+20275,
+55935,
+10950,
+63599,
+35730,
+47796,
+49900,
+7424,
+866,
+55938,
+1551,
+54770,
+51957,
+39162,
+1236,
+35498,
+39328,
+21204,
+2081,
+19234,
+45497,
+36264,
+22334,
+11028,
+8849,
+40160,
+58428,
+9507,
+8169,
+58503,
+41459,
+60026,
+14952,
+46455,
+1368,
+29586,
+34505,
+24443,
+48210,
+59588,
+30665,
+59522,
+28149,
+18214,
+8503,
+10613,
+30559,
+55315,
+60620,
+51980,
+59519,
+40029,
+63038,
+12463,
+4381,
+14942,
+56705,
+26621,
+36600,
+23608,
+16218,
+60184,
+15075,
+527,
+8069,
+3355,
+1036,
+46768,
+43186,
+15768,
+45805,
+19784,
+50410,
+53402,
+38149,
+49000,
+13821,
+52570,
+10105,
+40746,
+44830,
+37207,
+20491,
+44510,
+61327,
+62564,
+48404,
+62076,
+14529,
+22206,
+50242,
+4299,
+19403,
+48816,
+56315,
+12645,
+10796,
+52257,
+50796,
+24753,
+44461,
+54738,
+64488,
+27454,
+34191,
+58517,
+48946,
+64025,
+24984,
+37571,
+56274,
+61809,
+29279,
+6163,
+3499,
+55670,
+36051,
+7885,
+23711,
+62799,
+62066,
+14676,
+38590,
+485,
+31421,
+23826,
+25702,
+55678,
+46438,
+52817,
+20823,
+64811,
+61199,
+65402,
+35704,
+47996,
+46882,
+3040,
+61745,
+55978,
+55054,
+64869,
+3833,
+23368,
+23362,
+45540,
+46145,
+12594,
+3659,
+49297,
+2257,
+21450,
+26404,
+45553,
+28606,
+55648,
+5933,
+61551,
+46987,
+45254,
+11960,
+63560,
+32869,
+12269,
+23198,
+46797,
+4635,
+18223,
+45518,
+40234,
+6391,
+40531,
+40749,
+44185,
+15502,
+20601,
+7238,
+54502,
+22993,
+63141,
+27283,
+2367,
+28369,
+50437,
+22858,
+53875,
+50504,
+1043,
+48526,
+36553,
+46295,
+56287,
+62484,
+10115,
+22501,
+10885,
+9194,
+51055,
+2816,
+59652,
+27167,
+18221,
+4637,
+326,
+14832,
+45766,
+54224,
+58858,
+49826,
+64566,
+56552,
+19719,
+19918,
+4321,
+35226,
+29444,
+39548,
+11155,
+12743,
+7864,
+60855,
+21842,
+40962,
+48291,
+37138,
+33568,
+41591,
+30168,
+41625,
+28528,
+36182,
+13475,
+30547,
+51312,
+26683,
+42100,
+11732,
+50384,
+21727,
+46044,
+51857,
+42584,
+53177,
+35922,
+12321,
+27621,
+8915,
+27558,
+15827,
+46628,
+10992,
+33470,
+61853,
+53331,
+57709,
+34800,
+17671,
+18844,
+39744,
+43744,
+45999,
+50540,
+45203,
+46547,
+31653,
+41693,
+24643,
+13811,
+15683,
+24592,
+16262,
+59187,
+45688,
+62312,
+22260,
+60436,
+1633,
+30363,
+30773,
+60067,
+7203,
+25337,
+883,
+46040,
+10163,
+45530,
+21815,
+5892,
+6518,
+25687,
+2514,
+45030,
+43214,
+64208,
+48907,
+4687,
+49671,
+5586,
+22441,
+62241,
+63191,
+24511,
+19853,
+8019,
+19229,
+64185,
+4852,
+44201,
+35379,
+61598,
+63724,
+61885,
+12674,
+50533,
+24223,
+31084,
+34278,
+52218,
+53526,
+63830,
+62580,
+15055,
+30580,
+55112,
+43904,
+9305,
+13938,
+3043,
+18366,
+29146,
+58797,
+50931,
+10600,
+58818,
+33667,
+56212,
+48971,
+37891,
+28430,
+11741,
+53644,
+37272,
+13670,
+7978,
+45973,
+27592,
+36160,
+22130,
+24118,
+53036,
+8007,
+19978,
+51272,
+29806,
+47447,
+15638,
+23305,
+54124,
+38564,
+63028,
+25897,
+8400,
+4276,
+55532,
+20329,
+43111,
+34127,
+48979,
+12141,
+42226,
+19589,
+18344,
+40595,
+60393,
+7685,
+60925,
+36003,
+24409,
+36684,
+60702,
+47032,
+6826,
+41702,
+17646,
+47853,
+64882,
+25354,
+41055,
+4038,
+24048,
+56109,
+22185,
+31582,
+38892,
+37839,
+52848,
+51318,
+41804,
+44669,
+38283,
+65437,
+663,
+33263,
+23015,
+52397,
+57657,
+22867,
+1219,
+16815,
+7363,
+47890,
+47826,
+656,
+39403,
+6090,
+44901,
+24018,
+42007,
+7541,
+5936,
+30490,
+44805,
+7272,
+18843,
+17839,
+34801,
+18887,
+9470,
+44066,
+25297,
+56774,
+23749,
+57875,
+52227,
+3837,
+36936,
+52080,
+64598,
+61276,
+37954,
+37726,
+51627,
+58654,
+34636,
+33357,
+60424,
+64397,
+58173,
+47852,
+17712,
+41703,
+8740,
+49553,
+21520,
+52148,
+20928,
+25763,
+44458,
+47240,
+34385,
+31321,
+48140,
+49425,
+7616,
+27663,
+35103,
+2751,
+54893,
+7027,
+56446,
+64507,
+6370,
+22313,
+27242,
+12188,
+24820,
+18922,
+13373,
+40218,
+53100,
+37328,
+2638,
+35762,
+16481,
+31680,
+18954,
+1913,
+38385,
+43669,
+62466,
+34520,
+22177,
+40559,
+64945,
+17411,
+5485,
+39101,
+11906,
+60133,
+9911,
+19473,
+52928,
+60210,
+37347,
+31757,
+16442,
+18686,
+7173,
+29901,
+62783,
+5881,
+53137,
+49163,
+46605,
+6859,
+35091,
+18335,
+26654,
+28050,
+38962,
+25070,
+50251,
+49232,
+42059,
+925,
+22578,
+567,
+10270,
+10625,
+20755,
+35065,
+34317,
+34916,
+23260,
+28139,
+30727,
+8159,
+64518,
+37791,
+5663,
+34077,
+4230,
+28626,
+5500,
+55328,
+58341,
+43832,
+6786,
+310,
+36668,
+13720,
+57442,
+6179,
+29832,
+47577,
+18656,
+60571,
+26229,
+52603,
+19617,
+26155,
+13153,
+3095,
+41512,
+58879,
+572,
+916,
+52888,
+42941,
+23936,
+15325,
+53663,
+41466,
+30270,
+8057,
+3116,
+22120,
+45623,
+13712,
+63655,
+34529,
+45596,
+14617,
+50631,
+27381,
+56790,
+2826,
+38739,
+53921,
+4893,
+65230,
+27201,
+43086,
+53356,
+23929,
+50456,
+44534,
+49458,
+65110,
+51846,
+29811,
+56782,
+53597,
+31356,
+1510,
+40555,
+29439,
+1143,
+8766,
+26307,
+1548,
+27502,
+37160,
+24776,
+43039,
+8822,
+35425,
+20852,
+46810,
+8194,
+28495,
+12073,
+58118,
+964,
+6317,
+52831,
+2900,
+57857,
+62329,
+41894,
+32958,
+39392,
+39064,
+26756,
+56255,
+15063,
+6479,
+35126,
+6934,
+7873,
+55392,
+18808,
+33604,
+5877,
+13347,
+7410,
+2539,
+20978,
+14646,
+63963,
+23416,
+24845,
+30749,
+48963,
+56684,
+21168,
+37483,
+19389,
+18567,
+29427,
+51020,
+42219,
+3312,
+7162,
+33872,
+39341,
+62228,
+46182,
+20552,
+49196,
+36287,
+32020,
+40726,
+23635,
+25491,
+44593,
+53994,
+22617,
+18641,
+43956,
+16118,
+1607,
+13410,
+5484,
+17601,
+64946,
+52986,
+7714,
+59065,
+53334,
+30417,
+49213,
+63398,
+30072,
+37772,
+35680,
+39897,
+34215,
+47903,
+16113,
+41530,
+30651,
+26832,
+28582,
+28350,
+57408,
+13926,
+12683,
+47335,
+25433,
+31074,
+58785,
+62781,
+29903,
+41801,
+16339,
+29252,
+58398,
+38058,
+60713,
+28659,
+15809,
+31186,
+7398,
+15608,
+58606,
+59930,
+24516,
+50499,
+43441,
+21467,
+59985,
+51760,
+14763,
+19022,
+52977,
+43047,
+56064,
+58446,
+49334,
+61942,
+11945,
+10858,
+44662,
+9084,
+62602,
+63550,
+34,
+61793,
+57939,
+28633,
+21527,
+2683,
+55307,
+21663,
+65057,
+5337,
+59387,
+7515,
+63454,
+46305,
+57843,
+9619,
+38240,
+57161,
+46186,
+24817,
+41093,
+47480,
+5950,
+28458,
+32568,
+25953,
+43173,
+32305,
+30396,
+54249,
+35304,
+34042,
+13962,
+54799,
+63951,
+28614,
+3638,
+1874,
+55856,
+7902,
+43579,
+42188,
+28618,
+48828,
+34060,
+21703,
+58753,
+23895,
+49524,
+6251,
+41187,
+14845,
+57720,
+38966,
+7492,
+22663,
+10252,
+44649,
+16841,
+12278,
+57959,
+23467,
+62212,
+44619,
+37239,
+61654,
+52488,
+42589,
+21364,
+19044,
+37434,
+54085,
+2462,
+12986,
+18617,
+6397,
+24288,
+59181,
+23122,
+63537,
+19967,
+33613,
+36587,
+25653,
+61775,
+42641,
+26170,
+64341,
+35210,
+6387,
+45604,
+59248,
+37345,
+60212,
+44156,
+48884,
+59174,
+8582,
+47388,
+58413,
+63109,
+24574,
+47665,
+28400,
+50825,
+31805,
+12053,
+1855,
+6816,
+49608,
+41011,
+11163,
+59016,
+51536,
+2468,
+38357,
+14669,
+13506,
+15188,
+26882,
+7612,
+17089,
+62520,
+26055,
+58357,
+6583,
+47153,
+58390,
+40620,
+60159,
+49364,
+38366,
+54821,
+1420,
+38327,
+40370,
+37806,
+20650,
+18671,
+52962,
+9388,
+8105,
+31961,
+51683,
+24384,
+51369,
+39771,
+49569,
+37619,
+11382,
+16047,
+50579,
+4501,
+51694,
+54975,
+62640,
+28945,
+54428,
+32799,
+54095,
+43868,
+14879,
+29039,
+19213,
+63222,
+59732,
+49086,
+10467,
+43156,
+7401,
+54019,
+36437,
+33917,
+50743,
+29418,
+15517,
+20674,
+63392,
+39097,
+49753,
+21992,
+14087,
+56462,
+51288,
+41394,
+32232,
+62356,
+6328,
+22430,
+4293,
+4595,
+62187,
+57045,
+36648,
+21796,
+57907,
+60650,
+1624,
+14036,
+33122,
+61933,
+37999,
+19788,
+5535,
+56458,
+62773,
+1946,
+45510,
+27121,
+35558,
+32975,
+27795,
+36964,
+16477,
+2972,
+47528,
+3462,
+11848,
+45654,
+65449,
+7534,
+375,
+33641,
+8029,
+42547,
+51609,
+52856,
+28154,
+43001,
+41231,
+61674,
+64978,
+33944,
+39663,
+45712,
+55802,
+22401,
+60983,
+18512,
+14717,
+30267,
+14259,
+53097,
+5433,
+53586,
+23183,
+58666,
+36986,
+51253,
+64345,
+47104,
+29125,
+51495,
+54910,
+44784,
+40270,
+40188,
+23878,
+62519,
+17227,
+7613,
+24317,
+32500,
+491,
+45424,
+38736,
+26722,
+58600,
+1201,
+50326,
+23428,
+16130,
+27238,
+57757,
+15250,
+11466,
+4979,
+39389,
+50652,
+36226,
+54083,
+37436,
+57438,
+20193,
+32419,
+20687,
+5681,
+7602,
+39618,
+25836,
+29595,
+20287,
+23366,
+3835,
+52229,
+65341,
+57808,
+20617,
+28917,
+32978,
+33003,
+13498,
+46225,
+50188,
+11970,
+27533,
+60580,
+26594,
+6896,
+8458,
+11328,
+36521,
+20639,
+42273,
+1866,
+9900,
+63817,
+65233,
+34608,
+1728,
+37337,
+46011,
+64286,
+14524,
+7888,
+10568,
+18523,
+34634,
+58656,
+4259,
+27585,
+44097,
+50443,
+41899,
+7623,
+49042,
+64493,
+47630,
+28985,
+58902,
+5306,
+61117,
+41794,
+56299,
+41876,
+10380,
+22438,
+9537,
+51940,
+16231,
+13668,
+37274,
+63073,
+52553,
+58900,
+28987,
+55050,
+32782,
+20340,
+5852,
+24478,
+29194,
+34443,
+56346,
+56959,
+21946,
+14445,
+39756,
+29152,
+41212,
+21165,
+53292,
+6119,
+64178,
+49757,
+44721,
+34232,
+13180,
+63579,
+5828,
+26299,
+2924,
+22362,
+33234,
+2600,
+62338,
+28302,
+56901,
+19294,
+35947,
+7339,
+61904,
+13841,
+10564,
+10410,
+40100,
+15468,
+1272,
+20388,
+25211,
+19385,
+20322,
+43458,
+31030,
+41096,
+24690,
+29875,
+21571,
+14210,
+58685,
+49027,
+39008,
+34862,
+7229,
+15438,
+209,
+53991,
+20647,
+15341,
+32573,
+27631,
+63605,
+64418,
+41328,
+56309,
+21501,
+38268,
+36752,
+37157,
+61462,
+38432,
+7504,
+41288,
+53189,
+36217,
+9358,
+2732,
+50047,
+8383,
+37530,
+19927,
+8055,
+30272,
+61730,
+31082,
+24225,
+64556,
+16519,
+37899,
+18111,
+27331,
+23454,
+57597,
+47220,
+16328,
+49821,
+1323,
+32720,
+30355,
+10582,
+41046,
+41258,
+13976,
+47345,
+51210,
+15834,
+63957,
+61678,
+45476,
+38183,
+24622,
+52536,
+22823,
+13488,
+12813,
+45658,
+31576,
+50962,
+53428,
+21733,
+55952,
+4480,
+31051,
+11417,
+33332,
+20260,
+16530,
+46223,
+13500,
+7627,
+6986,
+22250,
+3612,
+10685,
+32193,
+26809,
+5044,
+39596,
+58431,
+31658,
+3427,
+4532,
+13519,
+63759,
+38025,
+7393,
+12277,
+17290,
+44650,
+7422,
+49902,
+8649,
+33512,
+3126,
+46885,
+52101,
+57816,
+31980,
+29295,
+11457,
+55088,
+39337,
+15763,
+27843,
+2310,
+39845,
+43342,
+16056,
+752,
+2525,
+13674,
+5982,
+7362,
+17687,
+1220,
+37144,
+19573,
+59970,
+8043,
+27156,
+29076,
+7635,
+39936,
+57671,
+4684,
+59355,
+34906,
+45526,
+8867,
+60520,
+12949,
+43387,
+23119,
+65178,
+45584,
+51187,
+13461,
+58367,
+45993,
+42342,
+31125,
+10455,
+4119,
+11979,
+35740,
+13847,
+4284,
+42948,
+207,
+15440,
+6949,
+38880,
+32413,
+3606,
+15554,
+42441,
+6803,
+39695,
+1614,
+56935,
+51533,
+39853,
+8455,
+5465,
+25024,
+24744,
+31142,
+7480,
+19055,
+58498,
+49624,
+15648,
+33672,
+38403,
+13648,
+46298,
+24103,
+15904,
+9676,
+24436,
+2117,
+1084,
+61189,
+14806,
+40898,
+49187,
+9297,
+10609,
+62221,
+22677,
+23019,
+35625,
+33211,
+35688,
+52186,
+37443,
+14747,
+26366,
+39616,
+7604,
+18684,
+16444,
+54750,
+37120,
+63174,
+40483,
+46133,
+47535,
+9223,
+56678,
+9671,
+14427,
+53042,
+2031,
+9838,
+37229,
+55439,
+702,
+36918,
+48232,
+31478,
+64674,
+60292,
+57977,
+15321,
+39425,
+61876,
+16242,
+43847,
+15200,
+19165,
+62970,
+946,
+45607,
+33271,
+25578,
+7134,
+10362,
+10771,
+5215,
+50272,
+58664,
+23185,
+32142,
+34053,
+55775,
+8199,
+19766,
+21661,
+55309,
+3737,
+6716,
+47117,
+28683,
+29404,
+3518,
+31829,
+20741,
+4415,
+29460,
+19459,
+27930,
+33728,
+24394,
+63370,
+64479,
+21826,
+15215,
+53000,
+58381,
+2813,
+45113,
+59749,
+8472,
+21380,
+48284,
+6364,
+56508,
+11899,
+8664,
+26836,
+12409,
+35241,
+6095,
+3280,
+13424,
+36360,
+47350,
+27227,
+60842,
+31122,
+28717,
+51154,
+25679,
+50514,
+12317,
+46028,
+1328,
+47736,
+60851,
+6184,
+59530,
+38902,
+35865,
+49204,
+2196,
+49549,
+51350,
+43506,
+60442,
+12056,
+33930,
+30895,
+71,
+15475,
+2292,
+10719,
+31504,
+7548,
+62621,
+18819,
+50679,
+5837,
+5715,
+54726,
+38723,
+28442,
+27137,
+64246,
+44556,
+36946,
+39353,
+22671,
+31408,
+16560,
+34299,
+7019,
+47548,
+1513,
+38570,
+54842,
+40001,
+3086,
+63415,
+51757,
+29842,
+24856,
+57043,
+62189,
+18172,
+54290,
+170,
+13415,
+34787,
+48095,
+8854,
+64228,
+45446,
+11996,
+21284,
+42978,
+9324,
+47778,
+27647,
+46122,
+56096,
+31985,
+34298,
+16594,
+31409,
+47809,
+53397,
+27951,
+8444,
+60711,
+38060,
+32915,
+50209,
+5239,
+46940,
+60234,
+14698,
+51052,
+22729,
+36512,
+7947,
+12048,
+44423,
+30086,
+22975,
+27387,
+47517,
+51717,
+33801,
+41029,
+37670,
+61434,
+46222,
+16862,
+20261,
+50378,
+38909,
+4739,
+38065,
+62606,
+51603,
+57330,
+8267,
+37898,
+16901,
+64557,
+33941,
+6591,
+56640,
+49500,
+55611,
+52840,
+6851,
+48599,
+26530,
+51900,
+2803,
+13861,
+56853,
+44193,
+40288,
+26994,
+421,
+49542,
+36695,
+3889,
+31388,
+57913,
+48524,
+1045,
+7081,
+41968,
+61086,
+46790,
+34098,
+55588,
+5581,
+52494,
+8619,
+13633,
+57573,
+31679,
+17612,
+35763,
+34930,
+2971,
+17135,
+36965,
+52899,
+63894,
+13292,
+2614,
+2833,
+54646,
+14306,
+34627,
+64745,
+59324,
+24551,
+3698,
+34745,
+15594,
+61054,
+58183,
+51509,
+34879,
+10585,
+49324,
+49558,
+56141,
+27883,
+5804,
+48355,
+55071,
+26316,
+514,
+30288,
+62353,
+54749,
+16727,
+18685,
+17590,
+31758,
+60247,
+14065,
+44809,
+3106,
+1583,
+24536,
+61585,
+30920,
+10980,
+60280,
+11460,
+28558,
+62541,
+53656,
+40421,
+12062,
+18136,
+9302,
+44270,
+18279,
+18332,
+42434,
+59226,
+39022,
+39735,
+5861,
+57276,
+55279,
+61404,
+60637,
+15907,
+62889,
+4829,
+19445,
+35405,
+15792,
+25717,
+14855,
+923,
+42061,
+42651,
+43321,
+8341,
+2570,
+27888,
+27434,
+23526,
+50557,
+46510,
+33982,
+14247,
+24145,
+8738,
+41705,
+56174,
+43690,
+4,
+39323,
+29128,
+60496,
+57827,
+3121,
+20972,
+24859,
+7944,
+12399,
+47954,
+18933,
+10700,
+11129,
+35643,
+31157,
+38314,
+60788,
+23342,
+12767,
+32364,
+63995,
+51546,
+28188,
+33782,
+22062,
+26839,
+18639,
+22619,
+57695,
+37914,
+56624,
+12701,
+24638,
+48794,
+56241,
+44294,
+22872,
+20989,
+50092,
+26052,
+40911,
+44984,
+48111,
+29251,
+17380,
+41802,
+51320,
+4754,
+12309,
+29671,
+48913,
+63342,
+21538,
+21443,
+49820,
+16894,
+47221,
+36847,
+35119,
+48133,
+2140,
+60273,
+52326,
+65027,
+10044,
+41548,
+56279,
+15364,
+44480,
+36850,
+20236,
+49663,
+32433,
+44574,
+42893,
+58661,
+38389,
+15285,
+47964,
+23479,
+46985,
+61553,
+45069,
+44923,
+49393,
+25265,
+25351,
+39545,
+46445,
+40121,
+57098,
+19892,
+6501,
+47284,
+52292,
+54983,
+55260,
+30684,
+40940,
+34648,
+45795,
+34451,
+31261,
+7701,
+65070,
+12799,
+15735,
+51915,
+30294,
+59444,
+36524,
+54006,
+7089,
+16205,
+12226,
+18745,
+31441,
+63043,
+37646,
+21119,
+59186,
+17825,
+24593,
+18593,
+8372,
+56842,
+40172,
+25806,
+45128,
+54719,
+44690,
+2230,
+23854,
+34172,
+58632,
+43050,
+61717,
+15051,
+26356,
+23598,
+43846,
+16701,
+61877,
+47591,
+63281,
+11539,
+18248,
+31166,
+23163,
+56159,
+46905,
+13667,
+16999,
+51941,
+21318,
+50647,
+54701,
+42656,
+15947,
+31286,
+28755,
+43725,
+27098,
+39003,
+60183,
+18035,
+23609,
+33791,
+61568,
+54951,
+49081,
+15980,
+37828,
+41372,
+40388,
+37463,
+7210,
+12225,
+16270,
+7090,
+48181,
+3596,
+19223,
+14050,
+22907,
+57004,
+700,
+55441,
+19133,
+33845,
+43520,
+25908,
+57737,
+56765,
+5874,
+62448,
+47180,
+7438,
+22202,
+27573,
+57640,
+32728,
+19911,
+19252,
+40791,
+34397,
+5692,
+19905,
+3132,
+5377,
+48319,
+27172,
+41873,
+58578,
+27250,
+35837,
+3620,
+40163,
+14871,
+46047,
+32065,
+3686,
+39924,
+4676,
+12220,
+3476,
+24297,
+22396,
+50600,
+23340,
+60790,
+20811,
+60339,
+58123,
+9054,
+10264,
+42126,
+3576,
+20613,
+49238,
+40273,
+13662,
+59475,
+1677,
+2071,
+41784,
+30151,
+264,
+55151,
+16106,
+32149,
+60081,
+27237,
+17077,
+23429,
+22890,
+25476,
+52658,
+9543,
+42747,
+65098,
+6435,
+42820,
+21763,
+1606,
+17415,
+43957,
+34326,
+44239,
+41529,
+17396,
+47904,
+57922,
+41222,
+9524,
+9964,
+32148,
+16134,
+55152,
+52909,
+42701,
+25019,
+58133,
+32048,
+45089,
+35659,
+53616,
+21372,
+19089,
+15975,
+59799,
+3588,
+11286,
+30235,
+61708,
+46346,
+58151,
+8405,
+39147,
+62754,
+22734,
+62664,
+43061,
+21002,
+48039,
+428,
+45220,
+41831,
+54623,
+32214,
+25162,
+34159,
+33370,
+54171,
+58053,
+36680,
+6249,
+49526,
+30690,
+45175,
+65435,
+38285,
+29702,
+21273,
+37680,
+41331,
+751,
+16821,
+43343,
+45097,
+21417,
+5671,
+20256,
+54606,
+24387,
+50578,
+17198,
+11383,
+40858,
+39823,
+42395,
+12734,
+60716,
+9614,
+62832,
+8896,
+49023,
+9927,
+44051,
+43551,
+7370,
+26559,
+56539,
+21599,
+43307,
+30262,
+41918,
+46100,
+50806,
+32200,
+23040,
+46586,
+29318,
+56181,
+39071,
+19594,
+52153,
+42757,
+37006,
+41641,
+48237,
+59287,
+49471,
+34848,
+26241,
+21686,
+5646,
+64714,
+40264,
+19562,
+37947,
+19974,
+26078,
+38802,
+9569,
+47000,
+21070,
+65115,
+49037,
+28843,
+3057,
+46657,
+28123,
+33662,
+63340,
+48915,
+14188,
+10532,
+56796,
+47894,
+44018,
+35803,
+37827,
+16212,
+49082,
+9397,
+9213,
+59798,
+16094,
+19090,
+25418,
+28973,
+46427,
+44456,
+25765,
+48336,
+27919,
+53785,
+977,
+5965,
+22092,
+31003,
+41730,
+12783,
+20266,
+22986,
+55183,
+8876,
+9008,
+39604,
+5053,
+47267,
+53133,
+15092,
+23640,
+31285,
+16225,
+42657,
+14398,
+28284,
+7909,
+53052,
+45059,
+21022,
+13836,
+9289,
+4396,
+28693,
+30015,
+53383,
+34239,
+54270,
+5603,
+58720,
+48298,
+24527,
+30314,
+49698,
+20157,
+20220,
+56408,
+64899,
+20372,
+36033,
+37822,
+27515,
+11817,
+50981,
+41480,
+7038,
+48779,
+49890,
+61521,
+45983,
+60021,
+62888,
+16410,
+60638,
+9675,
+16751,
+24104,
+36115,
+44991,
+11842,
+26674,
+11052,
+56727,
+38792,
+23308,
+41040,
+45459,
+51025,
+25112,
+6199,
+30370,
+8188,
+41945,
+12430,
+52574,
+26955,
+20037,
+21041,
+10874,
+57089,
+32882,
+9159,
+21415,
+45099,
+45300,
+49225,
+33105,
+19031,
+9481,
+60057,
+56220,
+32467,
+60849,
+47738,
+59952,
+58766,
+27755,
+60988,
+2004,
+11376,
+52623,
+13014,
+38160,
+53968,
+64082,
+10413,
+41882,
+55040,
+11184,
+56122,
+21060,
+3147,
+56815,
+33149,
+50163,
+45885,
+10383,
+12630,
+19453,
+35222,
+49611,
+45381,
+18233,
+62429,
+63956,
+16883,
+51211,
+52394,
+10987,
+14751,
+33698,
+46627,
+17847,
+27559,
+57246,
+6697,
+37016,
+38990,
+22282,
+59294,
+24580,
+40740,
+28797,
+46911,
+5814,
+4712,
+3241,
+10250,
+22665,
+31185,
+17374,
+28660,
+19698,
+21789,
+18838,
+41423,
+3749,
+60697,
+52306,
+37634,
+11926,
+32503,
+1116,
+51750,
+21132,
+24677,
+25716,
+16405,
+35406,
+386,
+4404,
+33744,
+46142,
+13447,
+26439,
+48368,
+43546,
+22553,
+1358,
+48563,
+20817,
+38052,
+37045,
+8512,
+63036,
+40031,
+3565,
+11855,
+60060,
+8989,
+45804,
+18026,
+43187,
+13065,
+47175,
+27842,
+16826,
+39338,
+19506,
+56692,
+63100,
+54104,
+5407,
+28322,
+60017,
+36421,
+63798,
+33222,
+20621,
+5089,
+57980,
+60464,
+37615,
+8728,
+4716,
+36236,
+31376,
+32169,
+52343,
+50685,
+33416,
+22133,
+22276,
+51914,
+16277,
+12800,
+50266,
+24366,
+12365,
+30427,
+43683,
+8938,
+22311,
+6372,
+47295,
+56405,
+446,
+63632,
+60547,
+37492,
+19900,
+44093,
+49208,
+48644,
+64215,
+60428,
+59264,
+6678,
+47650,
+972,
+34285,
+64156,
+63021,
+20509,
+30079,
+33190,
+8610,
+28770,
+61964,
+51649,
+10879,
+64773,
+40457,
+23520,
+38533,
+32994,
+29225,
+21822,
+39516,
+472,
+33393,
+31486,
+58276,
+54227,
+28258,
+24591,
+17827,
+13812,
+64442,
+23544,
+26856,
+34984,
+51035,
+65190,
+14207,
+4729,
+29555,
+39313,
+27175,
+18833,
+21189,
+64805,
+63207,
+64328,
+27290,
+14263,
+35733,
+53582,
+30741,
+3235,
+30724,
+38871,
+7367,
+23848,
+30196,
+15369,
+41630,
+39179,
+41941,
+1562,
+33671,
+16757,
+49625,
+4017,
+5563,
+21403,
+32448,
+47282,
+6503,
+13715,
+23304,
+17740,
+47448,
+58114,
+48699,
+56889,
+50554,
+13286,
+15400,
+3367,
+26640,
+9381,
+12917,
+34119,
+19810,
+15172,
+55163,
+13907,
+6075,
+1565,
+32351,
+42722,
+12770,
+27938,
+54994,
+27908,
+36603,
+64752,
+53417,
+30409,
+58605,
+17371,
+7399,
+43158,
+42598,
+21,
+62559,
+19814,
+44033,
+24673,
+53790,
+37155,
+36754,
+39555,
+61053,
+16462,
+34746,
+45332,
+46163,
+62649,
+3111,
+55619,
+39725,
+45997,
+43746,
+46701,
+32472,
+34821,
+41570,
+3504,
+11089,
+5751,
+46499,
+30265,
+14719,
+2285,
+8745,
+62713,
+33524,
+33181,
+23775,
+18527,
+46752,
+6808,
+43718,
+11845,
+20908,
+19149,
+33276,
+24231,
+42606,
+25144,
+4179,
+36893,
+42440,
+16774,
+3607,
+9174,
+36416,
+12530,
+20126,
+44044,
+31565,
+21774,
+22568,
+26246,
+32973,
+35560,
+33808,
+36155,
+10603,
+25421,
+34770,
+56227,
+52746,
+53686,
+37205,
+44832,
+54899,
+55717,
+22414,
+36031,
+20374,
+22832,
+8962,
+14932,
+3196,
+21833,
+11236,
+25138,
+6495,
+20673,
+17173,
+29419,
+33890,
+47030,
+60704,
+55976,
+61747,
+34577,
+6140,
+21185,
+53285,
+19607,
+13454,
+30507,
+20600,
+17921,
+44186,
+25255,
+57486,
+1442,
+36471,
+3726,
+36260,
+15257,
+2445,
+42308,
+62804,
+48626,
+57155,
+3254,
+20397,
+42299,
+54186,
+43752,
+1418,
+54823,
+22801,
+34517,
+35963,
+35851,
+20118,
+2291,
+16614,
+72,
+26934,
+10848,
+58289,
+41953,
+1271,
+16952,
+40101,
+4657,
+606,
+62083,
+2810,
+55233,
+63159,
+32866,
+26615,
+57011,
+7704,
+55229,
+32601,
+55304,
+50723,
+28489,
+13764,
+64362,
+43376,
+140,
+2325,
+44547,
+49192,
+39486,
+37303,
+57121,
+6948,
+16779,
+208,
+16934,
+7230,
+12164,
+52005,
+60123,
+6769,
+37784,
+23066,
+29532,
+41130,
+8704,
+64356,
+59262,
+60430,
+1092,
+18289,
+2520,
+7280,
+57347,
+23594,
+63977,
+28881,
+43979,
+54262,
+23236,
+11353,
+65082,
+45184,
+27614,
+65201,
+40497,
+53231,
+28506,
+11712,
+65303,
+25597,
+8140,
+3366,
+15631,
+13287,
+9436,
+23981,
+32611,
+40412,
+52284,
+14595,
+64860,
+35581,
+42384,
+4165,
+56870,
+54595,
+60728,
+3804,
+14693,
+18622,
+37779,
+40338,
+2953,
+44528,
+11528,
+40617,
+21759,
+32387,
+49692,
+19653,
+30462,
+6468,
+41629,
+15654,
+30197,
+21341,
+9413,
+44479,
+16316,
+56280,
+31318,
+25411,
+56803,
+47287,
+34373,
+40209,
+38210,
+35801,
+44020,
+59309,
+25618,
+111,
+19311,
+48492,
+53568,
+30486,
+25107,
+32130,
+13057,
+3981,
+32572,
+16930,
+20648,
+37808,
+53056,
+51081,
+22226,
+26296,
+56130,
+53505,
+53641,
+50370,
+22286,
+32621,
+48986,
+60299,
+53662,
+17525,
+23937,
+57864,
+39424,
+16704,
+57978,
+5091,
+21482,
+22951,
+12362,
+40024,
+40155,
+24823,
+4798,
+62438,
+19019,
+3078,
+5450,
+38799,
+36271,
+8213,
+59546,
+59597,
+38756,
+43530,
+64993,
+23295,
+724,
+4340,
+43330,
+47532,
+61239,
+21140,
+65507,
+22600,
+35546,
+58910,
+29189,
+40466,
+47963,
+16306,
+38390,
+61639,
+39673,
+35657,
+45091,
+21228,
+6413,
+11823,
+29006,
+39142,
+37085,
+26952,
+52736,
+49804,
+41172,
+54313,
+47307,
+5250,
+60321,
+48849,
+6874,
+27150,
+21870,
+57777,
+24192,
+62290,
+2444,
+15494,
+36261,
+44499,
+41198,
+26910,
+39519,
+11465,
+17074,
+57758,
+57289,
+63448,
+8507,
+35350,
+20950,
+30694,
+20024,
+18731,
+14419,
+40873,
+10120,
+53455,
+30407,
+53419,
+13831,
+4668,
+8816,
+30744,
+24713,
+46490,
+12158,
+64999,
+8544,
+59502,
+8614,
+30377,
+46210,
+19578,
+3673,
+38442,
+51390,
+30277,
+52999,
+16661,
+21827,
+24732,
+64335,
+48458,
+42110,
+32887,
+41133,
+52733,
+52577,
+51671,
+41299,
+41375,
+2841,
+19164,
+16699,
+43848,
+34640,
+65468,
+56864,
+22484,
+51221,
+26930,
+43247,
+64109,
+13733,
+26881,
+17230,
+13507,
+42798,
+31778,
+13678,
+22955,
+61544,
+3268,
+53850,
+64164,
+21162,
+65442,
+62052,
+39184,
+15000,
+55162,
+15624,
+19811,
+51423,
+53794,
+11895,
+50206,
+43252,
+21232,
+2390,
+52765,
+37601,
+61050,
+45275,
+49009,
+26629,
+31854,
+56893,
+6651,
+53804,
+47679,
+43811,
+26252,
+13726,
+35985,
+32245,
+32550,
+29229,
+2631,
+32894,
+39375,
+44342,
+1476,
+23518,
+40459,
+37245,
+18890,
+33812,
+35044,
+28516,
+29436,
+27463,
+32056,
+1659,
+52628,
+45752,
+47514,
+12136,
+42130,
+65524,
+38231,
+2622,
+1725,
+24416,
+52225,
+57877,
+22574,
+62992,
+24294,
+24894,
+55845,
+2537,
+7412,
+56485,
+5743,
+44008,
+62856,
+5063,
+64205,
+45547,
+4578,
+230,
+37167,
+47625,
+42680,
+31310,
+37637,
+8305,
+21439,
+48521,
+23639,
+15950,
+53134,
+62119,
+63083,
+15016,
+28809,
+63543,
+35877,
+35736,
+60979,
+41607,
+48037,
+21004,
+29963,
+26127,
+21595,
+526,
+18033,
+60185,
+1287,
+7379,
+29244,
+187,
+1744,
+8564,
+25772,
+32725,
+64767,
+6478,
+17460,
+56256,
+64175,
+26341,
+38437,
+26536,
+29454,
+30579,
+17774,
+62581,
+3483,
+26355,
+16246,
+61718,
+51205,
+61221,
+55772,
+6599,
+58319,
+54877,
+33080,
+7446,
+46265,
+841,
+41020,
+48170,
+22097,
+34804,
+28063,
+11661,
+47628,
+64495,
+27072,
+41437,
+10675,
+29694,
+57787,
+62654,
+4026,
+39857,
+48903,
+53221,
+62941,
+63063,
+52420,
+64014,
+28808,
+15088,
+63084,
+37928,
+60674,
+9504,
+39599,
+53412,
+40840,
+41227,
+57388,
+51985,
+52862,
+47755,
+41315,
+44284,
+55161,
+15174,
+39185,
+19537,
+47703,
+20335,
+13256,
+51607,
+42549,
+10356,
+40771,
+11293,
+65126,
+63460,
+46060,
+11331,
+20503,
+8647,
+49904,
+52252,
+42010,
+49144,
+19028,
+34755,
+29200,
+44816,
+54806,
+4136,
+44326,
+18284,
+27395,
+38451,
+64333,
+24734,
+53040,
+14429,
+3648,
+62826,
+19517,
+9527,
+51458,
+41149,
+49489,
+48340,
+61037,
+32063,
+46049,
+6438,
+46454,
+18063,
+60027,
+14348,
+52078,
+36938,
+32797,
+54430,
+10735,
+46596,
+56704,
+18040,
+4382,
+34975,
+33341,
+43140,
+49382,
+42198,
+53022,
+25668,
+3195,
+15524,
+8963,
+36447,
+55528,
+51282,
+25882,
+55425,
+64650,
+26704,
+13825,
+61817,
+33166,
+54437,
+65414,
+28472,
+268,
+35084,
+61266,
+60734,
+6047,
+61041,
+39469,
+26606,
+14238,
+25449,
+50919,
+48025,
+38157,
+3874,
+35484,
+34211,
+59318,
+9118,
+41577,
+37845,
+6687,
+55827,
+13103,
+23132,
+1681,
+7521,
+44218,
+49309,
+10837,
+60259,
+32577,
+26147,
+48467,
+29312,
+51873,
+6064,
+40447,
+29038,
+17187,
+43869,
+55145,
+8093,
+59360,
+56359,
+51855,
+46046,
+16165,
+40164,
+6704,
+34827,
+33573,
+37997,
+61935,
+10939,
+56630,
+38995,
+65096,
+42749,
+46717,
+48930,
+59701,
+922,
+16403,
+25718,
+13193,
+49797,
+32122,
+33201,
+46338,
+9044,
+30056,
+57719,
+17297,
+41188,
+34581,
+55094,
+44707,
+59500,
+8546,
+24032,
+52904,
+32287,
+24620,
+38185,
+45765,
+17891,
+327,
+35367,
+23914,
+57462,
+45010,
+28242,
+11485,
+40324,
+46154,
+1269,
+41955,
+8725,
+11755,
+6674,
+56191,
+62852,
+10232,
+50084,
+37049,
+3491,
+9779,
+53027,
+45671,
+24718,
+40897,
+16745,
+61190,
+27009,
+7336,
+41358,
+20935,
+54347,
+19658,
+40572,
+51674,
+8686,
+31619,
+48362,
+36120,
+4116,
+7435,
+26556,
+8083,
+6138,
+34579,
+41190,
+61982,
+19013,
+9035,
+22057,
+4425,
+23204,
+6124,
+13087,
+9460,
+35239,
+12411,
+9595,
+7657,
+24725,
+52781,
+61335,
+46802,
+58370,
+30099,
+30860,
+3076,
+19021,
+17362,
+51761,
+58411,
+47390,
+373,
+7536,
+54834,
+644,
+41965,
+3407,
+34481,
+33697,
+15830,
+10988,
+4128,
+26365,
+16732,
+37444,
+13903,
+36809,
+29579,
+13279,
+47540,
+40785,
+22566,
+21776,
+2228,
+44692,
+60391,
+40597,
+61449,
+19436,
+49512,
+33338,
+10225,
+56713,
+30114,
+8843,
+2439,
+8408,
+27349,
+35760,
+2640,
+2284,
+15575,
+30266,
+17109,
+18513,
+25088,
+36501,
+37136,
+48293,
+33733,
+2241,
+34348,
+57307,
+61317,
+39783,
+8147,
+13554,
+23223,
+33348,
+24782,
+38221,
+51051,
+16547,
+60235,
+58763,
+57768,
+18621,
+15384,
+3805,
+34457,
+13442,
+10351,
+21279,
+58741,
+3396,
+19583,
+65113,
+21072,
+21743,
+60381,
+63720,
+27681,
+8479,
+38589,
+17974,
+62067,
+53447,
+41869,
+40170,
+56844,
+13505,
+17232,
+38358,
+2328,
+1211,
+40502,
+2543,
+41245,
+21129,
+37974,
+31709,
+32042,
+64996,
+37260,
+29768,
+22910,
+46570,
+27994,
+52248,
+56103,
+2787,
+46568,
+22912,
+63962,
+17447,
+20979,
+28593,
+54415,
+54459,
+25649,
+12324,
+58083,
+35025,
+40078,
+20871,
+27912,
+13377,
+34925,
+19533,
+41922,
+19903,
+5694,
+1474,
+44344,
+34758,
+30687,
+39298,
+44634,
+30142,
+10435,
+55823,
+40087,
+50630,
+17513,
+45597,
+5260,
+65102,
+43642,
+453,
+20355,
+36707,
+19806,
+61091,
+6739,
+47773,
+54938,
+13216,
+58482,
+20004,
+28758,
+29954,
+23155,
+21491,
+33974,
+64859,
+15393,
+52285,
+9220,
+52894,
+60722,
+20272,
+38687,
+36376,
+21323,
+18678,
+24705,
+46452,
+6440,
+46755,
+53698,
+22893,
+54056,
+4608,
+63334,
+33960,
+48666,
+12110,
+35532,
+43396,
+12099,
+19895,
+62748,
+18971,
+58757,
+55706,
+11915,
+32764,
+37451,
+28673,
+47187,
+40479,
+36579,
+34029,
+53719,
+49621,
+24058,
+23087,
+43241,
+4328,
+55849,
+31183,
+22667,
+23538,
+64235,
+9421,
+35677,
+41605,
+60981,
+22403,
+28133,
+38281,
+44671,
+33616,
+26378,
+63702,
+6326,
+62358,
+34543,
+12145,
+36212,
+22205,
+18007,
+62077,
+1782,
+23709,
+7887,
+17025,
+64287,
+24925,
+9022,
+38424,
+29760,
+36109,
+48102,
+23319,
+14033,
+54858,
+20077,
+49645,
+40426,
+45903,
+38950,
+31463,
+12184,
+23064,
+37786,
+5969,
+30552,
+49860,
+348,
+42816,
+60366,
+27344,
+11668,
+25033,
+62996,
+44561,
+52265,
+29725,
+28316,
+28327,
+26877,
+47375,
+27315,
+50838,
+48991,
+42809,
+56342,
+47639,
+62722,
+20426,
+36419,
+60019,
+45985,
+41728,
+31005,
+10301,
+43317,
+59745,
+37517,
+6445,
+12293,
+18323,
+34311,
+61292,
+41062,
+11604,
+60190,
+38850,
+38472,
+40487,
+417,
+41236,
+12480,
+50200,
+43312,
+1791,
+9400,
+2671,
+28281,
+64961,
+20175,
+4214,
+54001,
+39755,
+16982,
+21947,
+13023,
+58780,
+22419,
+62419,
+43419,
+62237,
+19529,
+25246,
+64915,
+38099,
+18876,
+51446,
+54733,
+3647,
+14966,
+53041,
+16717,
+9672,
+27705,
+20578,
+61761,
+6112,
+32539,
+40872,
+15240,
+18732,
+19547,
+61250,
+36317,
+12981,
+59162,
+23269,
+23498,
+64732,
+38720,
+12668,
+1765,
+41676,
+61547,
+30034,
+9783,
+26962,
+11318,
+64959,
+28283,
+15945,
+42658,
+39192,
+9233,
+5104,
+54536,
+56762,
+51966,
+10853,
+14124,
+37574,
+55898,
+46818,
+31404,
+36819,
+48901,
+39859,
+24422,
+44814,
+29202,
+21911,
+62574,
+62551,
+4436,
+22984,
+20268,
+42413,
+2002,
+60990,
+14057,
+59995,
+58950,
+36929,
+9891,
+36049,
+55672,
+52509,
+6033,
+56925,
+36769,
+62246,
+52331,
+51974,
+9487,
+63003,
+54309,
+4023,
+19952,
+48744,
+52077,
+14950,
+60028,
+8794,
+32885,
+42112,
+25067,
+58806,
+28699,
+24373,
+46391,
+42541,
+28835,
+9367,
+8403,
+58153,
+3818,
+50488,
+4317,
+31616,
+55891,
+18191,
+48653,
+49704,
+13144,
+8716,
+18902,
+34425,
+20186,
+62759,
+119,
+61715,
+43052,
+50343,
+38608,
+6268,
+35106,
+24439,
+40849,
+64131,
+34612,
+5153,
+34626,
+16469,
+54647,
+36805,
+35882,
+43033,
+57861,
+26916,
+65482,
+44906,
+43291,
+41960,
+52370,
+24158,
+10757,
+12459,
+34784,
+21890,
+13012,
+52625,
+9945,
+32585,
+28550,
+48437,
+1193,
+21682,
+4970,
+18142,
+32858,
+56848,
+9649,
+13740,
+28570,
+12640,
+19308,
+43500,
+19972,
+37949,
+21541,
+8346,
+1006,
+45558,
+47794,
+35732,
+15664,
+27291,
+52141,
+53096,
+17107,
+30268,
+41468,
+10008,
+50609,
+39656,
+30812,
+25388,
+64792,
+9074,
+48372,
+24144,
+16390,
+33983,
+27526,
+35911,
+27990,
+63885,
+38307,
+22243,
+25448,
+14909,
+26607,
+29836,
+18959,
+9517,
+47243,
+63847,
+24136,
+45793,
+34650,
+14118,
+28390,
+34989,
+47675,
+38855,
+61883,
+63726,
+33204,
+12688,
+47838,
+23734,
+49200,
+60784,
+35514,
+12970,
+55448,
+57112,
+58684,
+16940,
+21572,
+4728,
+15675,
+65191,
+55470,
+1518,
+6257,
+46677,
+58070,
+34490,
+63940,
+3245,
+63095,
+26393,
+28454,
+19988,
+60961,
+52316,
+34341,
+31697,
+10531,
+15987,
+48916,
+22032,
+35335,
+4736,
+44979,
+24090,
+7191,
+8102,
+24451,
+40904,
+7512,
+59772,
+34782,
+12461,
+63040,
+58790,
+4227,
+10929,
+41152,
+48334,
+25767,
+34471,
+29509,
+53752,
+56634,
+41015,
+31512,
+523,
+10632,
+28982,
+19244,
+65412,
+54439,
+32111,
+41180,
+55687,
+33924,
+21875,
+30969,
+27094,
+50319,
+35261,
+46601,
+39742,
+18846,
+31888,
+64383,
+5344,
+1080,
+6003,
+2457,
+27117,
+20855,
+2694,
+23784,
+38199,
+4930,
+42824,
+31370,
+46170,
+1067,
+56272,
+37573,
+14389,
+10854,
+52334,
+29652,
+9993,
+28389,
+14228,
+34651,
+2976,
+45725,
+50367,
+11744,
+26160,
+21197,
+25467,
+8022,
+22767,
+53198,
+23272,
+24872,
+60263,
+57335,
+63692,
+58110,
+31481,
+58941,
+45385,
+27987,
+31025,
+30353,
+32722,
+23047,
+46660,
+51105,
+10805,
+60583,
+56461,
+17167,
+21993,
+38145,
+43150,
+4044,
+11472,
+1629,
+56009,
+50576,
+24389,
+34242,
+7957,
+36426,
+53509,
+52302,
+53321,
+26865,
+61452,
+46522,
+56504,
+11023,
+44808,
+16439,
+60248,
+20314,
+1785,
+27779,
+64106,
+58526,
+59994,
+14369,
+60991,
+63476,
+41582,
+5503,
+27447,
+22906,
+16200,
+19224,
+54793,
+32071,
+37733,
+51739,
+26212,
+36411,
+48441,
+10628,
+56542,
+3025,
+8296,
+33121,
+17150,
+1625,
+54857,
+14515,
+23320,
+4073,
+27107,
+7353,
+49078,
+30659,
+11144,
+35154,
+715,
+9978,
+25739,
+36800,
+2772,
+55659,
+38519,
+55522,
+3895,
+55731,
+2053,
+22304,
+26777,
+23949,
+36595,
+10540,
+38219,
+24784,
+9700,
+8601,
+48769,
+25685,
+6520,
+48302,
+63070,
+13422,
+3282,
+29306,
+53648,
+28486,
+62048,
+25235,
+1541,
+49175,
+37350,
+21246,
+1935,
+26033,
+54736,
+44463,
+43796,
+29565,
+37287,
+19280,
+52003,
+12166,
+44143,
+47344,
+16886,
+41259,
+43893,
+7223,
+8429,
+27001,
+3814,
+31232,
+35213,
+54604,
+20258,
+33334,
+3703,
+54798,
+17316,
+34043,
+42031,
+31137,
+44657,
+52408,
+29270,
+47212,
+31228,
+54640,
+26460,
+41687,
+35343,
+42191,
+63775,
+7072,
+4986,
+23301,
+32792,
+60188,
+11606,
+46747,
+61743,
+3042,
+17769,
+9306,
+39357,
+2930,
+49014,
+8208,
+52916,
+52280,
+50543,
+22989,
+48414,
+12682,
+17389,
+57409,
+21637,
+10205,
+4618,
+31528,
+44600,
+11192,
+57452,
+59401,
+19144,
+22998,
+21954,
+40562,
+6223,
+27810,
+47883,
+6884,
+6074,
+15622,
+55164,
+27822,
+36808,
+14745,
+37445,
+52935,
+52208,
+64825,
+8389,
+415,
+40489,
+9070,
+28561,
+2486,
+43218,
+65353,
+62673,
+3572,
+49837,
+54659,
+9057,
+52406,
+44659,
+5391,
+59716,
+20349,
+45484,
+34773,
+56437,
+51997,
+19407,
+53601,
+24228,
+30423,
+64937,
+6128,
+3936,
+45246,
+41497,
+12959,
+50332,
+24239,
+24964,
+50740,
+56852,
+16506,
+2804,
+44697,
+59006,
+10509,
+37995,
+33575,
+48332,
+41154,
+19346,
+3402,
+12885,
+30135,
+4283,
+16783,
+35741,
+18570,
+61035,
+48342,
+10563,
+16956,
+61905,
+62975,
+9060,
+9288,
+15939,
+21023,
+46289,
+9845,
+4667,
+15234,
+53420,
+45865,
+6621,
+21111,
+61816,
+14923,
+26705,
+30021,
+52569,
+18019,
+49001,
+31737,
+28111,
+63265,
+61671,
+65148,
+43073,
+64441,
+15682,
+17828,
+24644,
+7641,
+11244,
+30820,
+37506,
+4904,
+9728,
+58923,
+12379,
+53537,
+59235,
+27803,
+51492,
+39326,
+35500,
+33531,
+36997,
+22655,
+8520,
+55624,
+46066,
+41326,
+64420,
+49741,
+44406,
+27565,
+18985,
+37968,
+23277,
+2121,
+48953,
+35696,
+5551,
+48174,
+50775,
+7127,
+20017,
+25305,
+1775,
+37825,
+35805,
+31243,
+39723,
+55621,
+23942,
+64361,
+15451,
+28490,
+57027,
+10368,
+63408,
+2434,
+23632,
+10109,
+31849,
+42848,
+44090,
+41925,
+57383,
+34968,
+46343,
+43739,
+7632,
+55106,
+24425,
+1254,
+53945,
+23901,
+56986,
+28569,
+14276,
+9650,
+65246,
+21922,
+32487,
+11800,
+26880,
+15190,
+64110,
+58874,
+44358,
+51894,
+60806,
+35984,
+15150,
+26253,
+45810,
+11008,
+63383,
+57441,
+17545,
+36669,
+41559,
+32790,
+23303,
+15640,
+6504,
+63654,
+17517,
+45624,
+46287,
+21025,
+4212,
+20177,
+25663,
+22394,
+24299,
+53059,
+41024,
+45686,
+59189,
+8530,
+11363,
+36570,
+37367,
+8365,
+29495,
+45952,
+426,
+48041,
+35727,
+39507,
+23140,
+52805,
+43495,
+53124,
+55944,
+9177,
+50664,
+19492,
+47682,
+22954,
+15184,
+31779,
+35869,
+5981,
+16818,
+2526,
+28996,
+7977,
+17753,
+37273,
+16998,
+16232,
+46906,
+36224,
+50654,
+59474,
+16142,
+40274,
+55812,
+37521,
+36137,
+29215,
+63315,
+41194,
+53619,
+35291,
+8857,
+55868,
+56285,
+46297,
+16754,
+38404,
+38743,
+31330,
+21178,
+51808,
+4732,
+60747,
+19762,
+21032,
+39006,
+49029,
+62735,
+22533,
+57572,
+16484,
+8620,
+24107,
+928,
+43951,
+36457,
+30145,
+33413,
+32338,
+43260,
+49801,
+49261,
+35244,
+18155,
+34152,
+54627,
+23689,
+7783,
+37055,
+56145,
+29165,
+3221,
+9500,
+62903,
+1594,
+19847,
+3227,
+8386,
+31109,
+21221,
+61755,
+53,
+20633,
+64925,
+4506,
+55999,
+48537,
+49307,
+44220,
+54897,
+44834,
+25425,
+26372,
+31071,
+60883,
+37664,
+13091,
+32344,
+21434,
+36440,
+53769,
+54048,
+10766,
+47454,
+26376,
+33618,
+55405,
+30321,
+30526,
+32923,
+7798,
+8774,
+51146,
+54685,
+62953,
+56886,
+32027,
+53590,
+54681,
+13205,
+51181,
+56051,
+5577,
+25485,
+41708,
+4916,
+47491,
+6019,
+23222,
+14704,
+8148,
+50531,
+12676,
+35419,
+32442,
+58189,
+28296,
+3885,
+39448,
+2792,
+35597,
+57605,
+9099,
+51660,
+46131,
+40485,
+38474,
+30062,
+46256,
+51557,
+53372,
+50102,
+5415,
+63614,
+22540,
+26863,
+53323,
+63032,
+23007,
+44394,
+2050,
+38633,
+18666,
+63758,
+16846,
+4533,
+160,
+59025,
+21642,
+58316,
+6237,
+54328,
+3410,
+19176,
+37501,
+42797,
+15187,
+17231,
+14670,
+56845,
+5732,
+34147,
+7626,
+16860,
+46224,
+17047,
+33004,
+1248,
+40614,
+58451,
+43680,
+63424,
+63521,
+22508,
+12812,
+16875,
+22824,
+5020,
+52381,
+62509,
+21392,
+18195,
+28804,
+23886,
+44338,
+40072,
+39613,
+30546,
+17864,
+36183,
+64569,
+23044,
+25775,
+42149,
+5401,
+36676,
+30946,
+46055,
+30386,
+53003,
+46849,
+58366,
+16792,
+51188,
+50588,
+28600,
+47878,
+940,
+30506,
+15505,
+19608,
+38493,
+60616,
+54442,
+60050,
+26438,
+15786,
+46143,
+45542,
+22936,
+10350,
+14690,
+34458,
+27468,
+29211,
+56656,
+18651,
+48704,
+7722,
+23558,
+57365,
+41107,
+45082,
+1869,
+1508,
+31358,
+24156,
+52372,
+36359,
+16643,
+3281,
+13999,
+63071,
+37276,
+44126,
+33296,
+21888,
+34786,
+16576,
+171,
+26292,
+55984,
+5483,
+17413,
+1608,
+40203,
+32331,
+52851,
+10086,
+64388,
+46071,
+28475,
+40224,
+12095,
+30521,
+61658,
+38245,
+51276,
+12442,
+7912,
+55119,
+61392,
+11878,
+44016,
+47896,
+44610,
+44207,
+36744,
+45942,
+49520,
+22781,
+55179,
+24768,
+46400,
+13368,
+34924,
+14634,
+27913,
+26205,
+40217,
+17618,
+18923,
+62106,
+60895,
+34923,
+13379,
+46401,
+61178,
+36890,
+56087,
+22820,
+36398,
+34164,
+5656,
+39541,
+56593,
+1943,
+7856,
+55320,
+62401,
+60040,
+29848,
+48803,
+28095,
+60166,
+7409,
+17451,
+5878,
+10647,
+10692,
+54132,
+18661,
+37089,
+52238,
+4471,
+38822,
+31798,
+21903,
+63291,
+9255,
+34221,
+63261,
+55513,
+12601,
+51637,
+713,
+35156,
+25903,
+27287,
+10590,
+2371,
+55398,
+19207,
+31258,
+47759,
+12012,
+59346,
+24003,
+63706,
+32483,
+41120,
+50418,
+7661,
+47875,
+61701,
+48483,
+197,
+65207,
+64542,
+59670,
+29820,
+40346,
+63709,
+28447,
+52197,
+61067,
+34082,
+21614,
+21310,
+39266,
+2613,
+16473,
+63895,
+37542,
+50885,
+9435,
+15399,
+15632,
+50555,
+23528,
+57998,
+64486,
+54740,
+47539,
+14742,
+29580,
+28827,
+39260,
+4927,
+57040,
+20975,
+51044,
+35813,
+24264,
+8841,
+30116,
+33097,
+23471,
+33018,
+61352,
+63768,
+9750,
+44118,
+59868,
+5447,
+12620,
+51606,
+14995,
+20336,
+25124,
+50143,
+50943,
+56318,
+1103,
+52300,
+53511,
+8732,
+18252,
+49248,
+10555,
+58094,
+6914,
+41304,
+22933,
+2490,
+64433,
+52288,
+12027,
+51464,
+12217,
+33499,
+4839,
+49736,
+27904,
+55728,
+25867,
+24001,
+59348,
+58961,
+53846,
+59074,
+4575,
+51345,
+31197,
+26896,
+30339,
+58481,
+14604,
+54939,
+2951,
+40340,
+5263,
+49216,
+511,
+129,
+11596,
+3290,
+51180,
+13564,
+54682,
+51415,
+35886,
+27027,
+29159,
+44216,
+7523,
+56906,
+60655,
+33367,
+49796,
+14853,
+25719,
+9564,
+42845,
+29074,
+27158,
+63067,
+27182,
+46788,
+61088,
+33462,
+8258,
+63578,
+16971,
+34233,
+32490,
+6968,
+35539,
+37422,
+25585,
+45756,
+47073,
+11335,
+62815,
+41067,
+23576,
+51242,
+41380,
+52111,
+29179,
+10137,
+46332,
+9048,
+19686,
+254,
+32898,
+60516,
+52052,
+3029,
+3094,
+17534,
+26156,
+12927,
+5846,
+11658,
+30670,
+63882,
+26826,
+8715,
+14325,
+49705,
+54551,
+42521,
+47846,
+7881,
+55685,
+41182,
+22915,
+60313,
+1971,
+2560,
+56479,
+51652,
+20879,
+51580,
+36268,
+26081,
+30302,
+45429,
+19740,
+31088,
+3591,
+43370,
+4161,
+41808,
+2473,
+50518,
+1524,
+39667,
+55607,
+58515,
+34193,
+47332,
+55454,
+40960,
+21844,
+32771,
+28088,
+60076,
+23131,
+14895,
+55828,
+55297,
+31883,
+26527,
+30677,
+18905,
+60938,
+20084,
+29345,
+53704,
+32343,
+13587,
+37665,
+7836,
+9459,
+14778,
+6125,
+34524,
+22966,
+11281,
+65021,
+35251,
+28224,
+56768,
+26304,
+39236,
+62615,
+52996,
+31395,
+65156,
+52948,
+61576,
+55724,
+20139,
+64539,
+30173,
+47174,
+15766,
+43188,
+59928,
+58608,
+55085,
+60283,
+7312,
+3980,
+15344,
+32131,
+24764,
+54246,
+21926,
+55600,
+59727,
+3882,
+48255,
+22949,
+21484,
+21988,
+57765,
+59955,
+49051,
+56919,
+60602,
+7001,
+32002,
+12843,
+5027,
+41672,
+43631,
+35715,
+25193,
+59105,
+25906,
+43522,
+9864,
+51175,
+59505,
+32688,
+56470,
+58779,
+14443,
+21948,
+52642,
+5099,
+63906,
+9530,
+43933,
+3872,
+38159,
+15858,
+52624,
+14289,
+21891,
+64595,
+35033,
+6857,
+46607,
+40589,
+64897,
+56410,
+39046,
+40914,
+48660,
+42715,
+29141,
+38819,
+1306,
+32945,
+6647,
+43229,
+43424,
+10336,
+34345,
+40528,
+22473,
+45338,
+18616,
+17275,
+2463,
+48542,
+6638,
+59161,
+14414,
+36318,
+55635,
+1722,
+20807,
+65138,
+495,
+19160,
+5754,
+11423,
+55447,
+14214,
+35515,
+51715,
+47519,
+55433,
+3958,
+53106,
+49652,
+38583,
+12803,
+50331,
+13867,
+41498,
+46974,
+7532,
+65451,
+32384,
+26062,
+54572,
+4777,
+43386,
+16798,
+60521,
+21655,
+28911,
+25101,
+24065,
+4704,
+6102,
+29854,
+3915,
+57053,
+20814,
+31307,
+40454,
+39307,
+48000,
+45921,
+34973,
+4384,
+9088,
+58998,
+5845,
+13151,
+26157,
+28385,
+63250,
+56794,
+10534,
+31995,
+23501,
+65050,
+34118,
+15627,
+9382,
+54160,
+49678,
+52524,
+51249,
+55593,
+63877,
+50408,
+19786,
+38001,
+34363,
+24544,
+4962,
+32512,
+39118,
+32705,
+28313,
+9623,
+37870,
+27847,
+43588,
+52516,
+60439,
+63275,
+6146,
+36645,
+7235,
+53954,
+18759,
+46090,
+30134,
+13850,
+3403,
+31133,
+23377,
+41858,
+40060,
+49411,
+55995,
+36125,
+7477,
+64127,
+23025,
+55192,
+54178,
+43525,
+8061,
+19958,
+65318,
+10523,
+52940,
+59643,
+59021,
+44031,
+19816,
+38886,
+19126,
+29708,
+31586,
+33449,
+18561,
+58007,
+12466,
+46319,
+5594,
+35938,
+55709,
+4616,
+10207,
+7198,
+31891,
+54098,
+5026,
+13038,
+32003,
+9988,
+35489,
+36773,
+55271,
+23413,
+18304,
+9768,
+29868,
+40268,
+44786,
+40356,
+65348,
+63795,
+5911,
+58011,
+7639,
+24646,
+43383,
+20970,
+3123,
+7826,
+30739,
+53584,
+5435,
+44910,
+53842,
+26941,
+45657,
+16874,
+13489,
+22509,
+23447,
+30704,
+43560,
+58163,
+56187,
+24403,
+50330,
+12961,
+38584,
+50265,
+15734,
+16278,
+65071,
+53840,
+44912,
+52824,
+41405,
+25973,
+9309,
+50002,
+11852,
+33386,
+5458,
+9041,
+44004,
+25979,
+20265,
+15960,
+41731,
+25500,
+52886,
+918,
+20254,
+5673,
+55570,
+52958,
+41398,
+63146,
+50374,
+27937,
+15617,
+42723,
+32363,
+16365,
+23343,
+12671,
+11260,
+647,
+23566,
+53088,
+18865,
+64145,
+45467,
+40811,
+50766,
+4203,
+29449,
+33989,
+59046,
+58439,
+25562,
+18542,
+53903,
+40406,
+23647,
+47272,
+7863,
+17877,
+11156,
+54483,
+8812,
+47356,
+32254,
+41863,
+28657,
+60715,
+16042,
+42396,
+51850,
+22043,
+61022,
+23797,
+8326,
+42761,
+56911,
+36299,
+28478,
+47158,
+10339,
+19098,
+47571,
+44715,
+34603,
+62146,
+29135,
+42255,
+20624,
+52742,
+24133,
+4001,
+21208,
+31946,
+32949,
+59219,
+48067,
+62638,
+54977,
+55781,
+24637,
+16352,
+56625,
+6451,
+9925,
+49025,
+58687,
+42812,
+28504,
+53233,
+9403,
+34909,
+27697,
+47837,
+14220,
+33205,
+52131,
+55452,
+47334,
+17388,
+13927,
+48415,
+37384,
+28791,
+19597,
+35418,
+13551,
+50532,
+17783,
+61886,
+11259,
+12765,
+23344,
+1764,
+14408,
+38721,
+54728,
+41568,
+34823,
+2743,
+22189,
+58813,
+38789,
+24876,
+43983,
+8585,
+41349,
+64465,
+63947,
+3548,
+40036,
+52757,
+64620,
+48009,
+22597,
+53836,
+10795,
+18000,
+56316,
+50945,
+4816,
+19307,
+14274,
+28571,
+49062,
+1644,
+26415,
+5799,
+41504,
+50700,
+33251,
+19452,
+15842,
+10384,
+49433,
+22837,
+6099,
+42686,
+11551,
+28872,
+57328,
+51605,
+13258,
+5448,
+3080,
+58561,
+60389,
+44694,
+4463,
+53103,
+9816,
+36518,
+46063,
+47943,
+5328,
+42565,
+38768,
+40778,
+58637,
+28091,
+51636,
+13330,
+55514,
+59633,
+55013,
+52671,
+5895,
+3658,
+17948,
+46146,
+46972,
+41500,
+380,
+52465,
+3962,
+23550,
+23354,
+276,
+54418,
+55464,
+65063,
+32146,
+9966,
+52762,
+24140,
+10651,
+46865,
+48328,
+56150,
+55211,
+31116,
+23434,
+3561,
+31040,
+1215,
+44896,
+23266,
+18245,
+53983,
+313,
+26508,
+46514,
+48177,
+22491,
+32685,
+53225,
+31782,
+39270,
+64258,
+60974,
+590,
+48128,
+57824,
+63354,
+2554,
+2643,
+24912,
+35775,
+29430,
+18695,
+2921,
+52462,
+34143,
+35259,
+50321,
+48877,
+7157,
+35150,
+52073,
+18689,
+1344,
+20125,
+15550,
+36417,
+20428,
+32091,
+38461,
+36617,
+37745,
+65380,
+23107,
+59256,
+34721,
+9512,
+63113,
+60453,
+23338,
+50602,
+39870,
+45404,
+33528,
+55757,
+50727,
+19867,
+11721,
+40977,
+19332,
+62382,
+20593,
+29689,
+41418,
+47791,
+57531,
+61379,
+40802,
+50570,
+51109,
+48821,
+63507,
+21739,
+35974,
+30557,
+10615,
+30331,
+21627,
+53093,
+57627,
+49595,
+20479,
+26219,
+27728,
+50199,
+14457,
+41237,
+51834,
+24514,
+59932,
+6819,
+39525,
+43285,
+61927,
+30091,
+36069,
+8110,
+21914,
+46318,
+12854,
+58008,
+4380,
+18042,
+63039,
+14174,
+34783,
+14292,
+10758,
+59471,
+25092,
+65316,
+19960,
+7431,
+21879,
+61084,
+41970,
+52776,
+46673,
+30228,
+6956,
+43909,
+53050,
+7911,
+13395,
+51277,
+48431,
+60105,
+25318,
+47921,
+28210,
+61073,
+52924,
+55722,
+61578,
+52573,
+15886,
+41946,
+33164,
+61819,
+57304,
+58142,
+53262,
+61496,
+46834,
+29083,
+43971,
+57521,
+22340,
+37268,
+64191,
+45414,
+22041,
+51852,
+9594,
+14775,
+35240,
+16647,
+26837,
+22064,
+1495,
+18269,
+49586,
+50446,
+3419,
+59655,
+47953,
+16375,
+7945,
+36514,
+45325,
+41416,
+29691,
+61629,
+28014,
+29402,
+28685,
+45303,
+34964,
+8788,
+53309,
+46794,
+26762,
+35555,
+26110,
+3663,
+53536,
+13802,
+58924,
+23383,
+36057,
+519,
+19284,
+33179,
+33526,
+45406,
+62533,
+12266,
+8314,
+29936,
+30426,
+15731,
+24367,
+40023,
+15316,
+22952,
+47684,
+29616,
+41540,
+27441,
+61490,
+20783,
+25331,
+27365,
+37358,
+2478,
+10470,
+24248,
+48622,
+29321,
+18988,
+33886,
+36915,
+6053,
+22074,
+3335,
+42769,
+51430,
+42266,
+42041,
+22531,
+62737,
+54182,
+457,
+35059,
+287,
+36638,
+22682,
+1749,
+45802,
+8991,
+58082,
+14640,
+25650,
+27620,
+17851,
+35923,
+32702,
+46027,
+16633,
+50515,
+2597,
+6030,
+32851,
+52980,
+33756,
+29670,
+16335,
+4755,
+25039,
+50130,
+37032,
+52844,
+56135,
+62868,
+959,
+56868,
+4167,
+61780,
+37184,
+35652,
+56518,
+18322,
+14469,
+6446,
+55342,
+31638,
+20164,
+21851,
+34494,
+59920,
+55257,
+28367,
+2369,
+10592,
+41032,
+40523,
+57958,
+17289,
+16842,
+7394,
+53113,
+4042,
+43152,
+25810,
+19542,
+23197,
+17932,
+32870,
+8313,
+12369,
+62534,
+60626,
+31061,
+60219,
+37213,
+5522,
+63853,
+34736,
+24954,
+39704,
+60847,
+32469,
+44351,
+50030,
+45931,
+6543,
+18919,
+40158,
+8851,
+44371,
+48560,
+59419,
+20486,
+45938,
+30121,
+64857,
+33976,
+8488,
+42331,
+59415,
+39199,
+22526,
+42677,
+11664,
+56369,
+60891,
+56748,
+53151,
+18744,
+16269,
+16206,
+7211,
+41157,
+30881,
+3475,
+16159,
+4677,
+33498,
+13234,
+51465,
+38673,
+52010,
+2506,
+44677,
+28762,
+10268,
+569,
+30240,
+3683,
+47695,
+7829,
+6471,
+48131,
+35121,
+46839,
+54111,
+23101,
+6313,
+33330,
+11419,
+2945,
+35617,
+43677,
+34408,
+26847,
+41091,
+24819,
+17621,
+27243,
+57400,
+23063,
+14507,
+31464,
+22450,
+48035,
+41609,
+1313,
+19702,
+19709,
+53744,
+30941,
+61914,
+7112,
+4442,
+23838,
+42074,
+11719,
+19869,
+44142,
+13979,
+52004,
+15436,
+7231,
+23209,
+62864,
+37258,
+64998,
+15228,
+46491,
+28105,
+35521,
+49603,
+31299,
+782,
+64701,
+40579,
+44790,
+6954,
+30230,
+36211,
+14532,
+34544,
+63675,
+42225,
+17727,
+48980,
+37587,
+36764,
+42129,
+15126,
+47515,
+27389,
+36952,
+26540,
+443,
+20223,
+58075,
+7441,
+58625,
+25937,
+50527,
+24698,
+61396,
+64699,
+784,
+49926,
+35129,
+50302,
+51707,
+19673,
+1017,
+27748,
+63573,
+62166,
+35531,
+14574,
+48667,
+43805,
+6669,
+38940,
+2236,
+18458,
+33421,
+352,
+6499,
+19894,
+14571,
+43397,
+64123,
+30520,
+13400,
+40225,
+56996,
+63136,
+3469,
+50863,
+23669,
+54222,
+45768,
+11588,
+59832,
+63872,
+55416,
+50300,
+35131,
+43147,
+28588,
+26854,
+23546,
+43183,
+20885,
+58117,
+17474,
+28496,
+1075,
+25156,
+4784,
+28462,
+8000,
+39949,
+45808,
+26255,
+18135,
+16425,
+40422,
+6157,
+35494,
+1299,
+33929,
+16618,
+60443,
+1854,
+17242,
+31806,
+27061,
+35477,
+44422,
+16542,
+7948,
+22105,
+28364,
+54986,
+45206,
+22428,
+6330,
+44379,
+29407,
+6778,
+9833,
+23746,
+4999,
+45523,
+9406,
+50204,
+11897,
+56510,
+29110,
+51463,
+13236,
+52289,
+56806,
+35412,
+37410,
+55381,
+62195,
+37801,
+37128,
+64779,
+53937,
+29676,
+9371,
+24455,
+59345,
+13318,
+47760,
+1708,
+35358,
+1228,
+54233,
+7069,
+10689,
+33309,
+4237,
+61873,
+53157,
+51736,
+6303,
+10657,
+21283,
+16570,
+45447,
+18518,
+56351,
+63269,
+65220,
+18874,
+38101,
+42013,
+56330,
+21308,
+21616,
+61430,
+58531,
+57885,
+53563,
+35739,
+16785,
+4120,
+54533,
+64448,
+7651,
+53556,
+29799,
+4560,
+27532,
+17044,
+50189,
+35052,
+55526,
+36449,
+35373,
+41177,
+5051,
+39606,
+63559,
+17935,
+45255,
+37712,
+29658,
+29803,
+49875,
+34797,
+52718,
+60103,
+48433,
+42355,
+6823,
+33509,
+51971,
+10857,
+17354,
+61943,
+26960,
+9785,
+8754,
+48044,
+34182,
+7252,
+46129,
+51662,
+41310,
+983,
+55481,
+43814,
+62005,
+7982,
+42034,
+489,
+32502,
+15799,
+37635,
+31312,
+21569,
+29877,
+49440,
+44166,
+24342,
+51883,
+30247,
+32763,
+14565,
+55707,
+35940,
+45252,
+46989,
+54273,
+41070,
+11431,
+60132,
+17598,
+39102,
+3529,
+39973,
+46997,
+65135,
+8663,
+16650,
+56509,
+12031,
+50205,
+15168,
+53795,
+47782,
+28266,
+42251,
+36230,
+51541,
+37052,
+827,
+4196,
+58964,
+36888,
+61180,
+2995,
+49110,
+25117,
+44015,
+13391,
+61393,
+5237,
+50211,
+48013,
+24072,
+8838,
+18478,
+3771,
+60065,
+30775,
+28436,
+41738,
+57213,
+60593,
+59333,
+62548,
+21737,
+63509,
+57358,
+20300,
+56218,
+60059,
+15772,
+3566,
+33385,
+12790,
+50003,
+28333,
+45653,
+17131,
+3463,
+20907,
+15564,
+43719,
+26673,
+15900,
+44992,
+18781,
+50956,
+42645,
+40063,
+55908,
+42905,
+36191,
+52444,
+33195,
+18474,
+44779,
+59498,
+44709,
+20996,
+2189,
+24929,
+29005,
+15277,
+6414,
+65160,
+31898,
+31507,
+50980,
+15917,
+27516,
+22870,
+44296,
+21498,
+3845,
+58773,
+5011,
+38876,
+26469,
+25733,
+38031,
+2043,
+36072,
+55820,
+47373,
+26879,
+13735,
+32488,
+34235,
+30838,
+33763,
+64748,
+45827,
+53361,
+34597,
+35895,
+6298,
+44490,
+26336,
+43013,
+36027,
+51373,
+8904,
+25524,
+10908,
+41850,
+19433,
+26868,
+562,
+58670,
+2855,
+25174,
+38524,
+24154,
+31360,
+28963,
+56589,
+29096,
+51954,
+11490,
+63827,
+30181,
+26568,
+32788,
+41561,
+59803,
+31392,
+30280,
+5323,
+21383,
+6673,
+14819,
+8726,
+37617,
+49571,
+64561,
+63992,
+28822,
+40863,
+49938,
+28383,
+26159,
+14113,
+50368,
+53643,
+17756,
+28431,
+30393,
+32735,
+19158,
+497,
+659,
+58885,
+50383,
+17859,
+42101,
+61978,
+6761,
+23252,
+33794,
+28029,
+60635,
+61406,
+45020,
+40976,
+12508,
+19868,
+12169,
+42075,
+25142,
+42608,
+45778,
+34156,
+65302,
+15405,
+28507,
+19829,
+20799,
+34890,
+36606,
+45007,
+58387,
+41263,
+28,
+42939,
+52890,
+54743,
+27259,
+465,
+52740,
+20626,
+52721,
+38597,
+61339,
+6575,
+52313,
+6464,
+18177,
+62268,
+50357,
+1374,
+4672,
+35904,
+34112,
+62199,
+35902,
+4674,
+39926,
+20568,
+25172,
+2857,
+43914,
+8303,
+37639,
+25340,
+32732,
+32308,
+25032,
+14497,
+27345,
+1264,
+56368,
+12232,
+42678,
+47627,
+15034,
+28064,
+30669,
+13149,
+5847,
+41253,
+26361,
+31790,
+42709,
+31278,
+45,
+9974,
+59776,
+1446,
+64451,
+4350,
+242,
+62841,
+41755,
+58596,
+40107,
+55363,
+4240,
+10401,
+61071,
+28212,
+46994,
+52789,
+25132,
+31467,
+6401,
+35124,
+6481,
+40676,
+57595,
+23456,
+55519,
+11372,
+46879,
+4968,
+21684,
+26243,
+34565,
+30828,
+26797,
+48030,
+60645,
+5468,
+58336,
+1157,
+27110,
+24177,
+44604,
+8226,
+46746,
+13942,
+60189,
+14464,
+41063,
+29560,
+757,
+53520,
+35862,
+39086,
+3289,
+13208,
+130,
+42525,
+64282,
+51793,
+41637,
+46611,
+59831,
+12086,
+45769,
+43878,
+44934,
+58301,
+52648,
+46779,
+62334,
+22372,
+9322,
+42980,
+59910,
+57209,
+22778,
+28533,
+504,
+18559,
+33451,
+27376,
+49681,
+1456,
+2340,
+29788,
+54617,
+54140,
+63899,
+36793,
+34540,
+64817,
+21239,
+44041,
+22615,
+53996,
+47291,
+52973,
+65225,
+28871,
+12624,
+42687,
+57286,
+31991,
+65327,
+48605,
+44891,
+20943,
+48118,
+42921,
+53981,
+18247,
+16238,
+63282,
+61322,
+51090,
+25783,
+3074,
+30862,
+29012,
+49265,
+58449,
+40616,
+15378,
+44529,
+18417,
+64104,
+27781,
+65446,
+26944,
+2173,
+32046,
+58135,
+48359,
+29101,
+60694,
+59948,
+56003,
+44845,
+62719,
+5887,
+5711,
+45356,
+39255,
+991,
+42349,
+5685,
+23249,
+60529,
+7789,
+7009,
+5545,
+27830,
+36861,
+60743,
+43469,
+31289,
+35551,
+31916,
+28748,
+63826,
+11767,
+51955,
+54772,
+60174,
+40323,
+14825,
+28243,
+26168,
+42643,
+50958,
+60930,
+46742,
+57549,
+27358,
+24722,
+42966,
+30975,
+1628,
+14082,
+4045,
+44756,
+65497,
+26888,
+4978,
+17073,
+15251,
+39520,
+29341,
+28423,
+28557,
+16430,
+60281,
+55087,
+16829,
+29296,
+29310,
+48469,
+28102,
+48260,
+63076,
+43346,
+6726,
+31011,
+42573,
+41861,
+32256,
+19156,
+32737,
+48348,
+21670,
+33645,
+52692,
+60512,
+43401,
+32694,
+23630,
+2436,
+49779,
+60131,
+11908,
+41071,
+35822,
+2919,
+18697,
+53337,
+42437,
+55446,
+12972,
+5755,
+59902,
+2944,
+12196,
+33331,
+16865,
+31052,
+26743,
+42231,
+47799,
+26974,
+39537,
+26726,
+20243,
+833,
+39999,
+54844,
+52194,
+46877,
+11374,
+2006,
+3495,
+44111,
+4519,
+10919,
+53973,
+1639,
+33289,
+35817,
+25253,
+44188,
+44819,
+33505,
+23150,
+40083,
+18308,
+9952,
+6263,
+40857,
+16046,
+17199,
+37620,
+10441,
+20832,
+56154,
+52622,
+15860,
+2005,
+11403,
+46878,
+11624,
+55520,
+38521,
+1983,
+21175,
+32166,
+49467,
+7485,
+36569,
+13698,
+8531,
+64839,
+10344,
+52665,
+22013,
+55675,
+2799,
+53203,
+65081,
+15413,
+23237,
+50052,
+21786,
+58463,
+60553,
+40378,
+39157,
+1837,
+42896,
+43674,
+8026,
+24658,
+10709,
+46036,
+58907,
+40298,
+62814,
+13171,
+47074,
+62368,
+20502,
+14986,
+46061,
+36520,
+17038,
+8459,
+8778,
+63500,
+18896,
+58522,
+32919,
+61861,
+43728,
+64958,
+14401,
+26963,
+41867,
+53449,
+19770,
+47648,
+6680,
+7260,
+49851,
+37280,
+42453,
+22547,
+21152,
+64308,
+53530,
+58469,
+60150,
+55110,
+30582,
+42133,
+30483,
+10013,
+61644,
+56967,
+65125,
+14990,
+40772,
+52483,
+43612,
+35140,
+48218,
+30234,
+16091,
+3589,
+31090,
+29478,
+65020,
+13083,
+22967,
+46891,
+11256,
+2714,
+21557,
+50122,
+42302,
+19371,
+62659,
+39592,
+27058,
+41146,
+31297,
+49605,
+59935,
+18547,
+53627,
+24468,
+41963,
+646,
+12764,
+12672,
+61887,
+2713,
+11278,
+46892,
+26980,
+19982,
+36933,
+21768,
+20232,
+30575,
+47364,
+48959,
+65368,
+30819,
+13808,
+7642,
+30390,
+40922,
+20250,
+29331,
+33375,
+25137,
+15521,
+21834,
+59394,
+8804,
+37909,
+957,
+62870,
+2533,
+51184,
+61515,
+11133,
+24364,
+50268,
+18231,
+45383,
+58943,
+11174,
+34004,
+26237,
+20905,
+3465,
+34320,
+24084,
+30077,
+20511,
+27405,
+59299,
+52025,
+62128,
+31058,
+40734,
+2942,
+59904,
+23821,
+56264,
+56427,
+46069,
+64390,
+32312,
+27410,
+2839,
+41377,
+54782,
+57451,
+13919,
+44601,
+50737,
+45880,
+6426,
+24933,
+23160,
+56121,
+15851,
+55041,
+30893,
+33932,
+2421,
+7334,
+27011,
+41385,
+37536,
+34003,
+11220,
+58944,
+8899,
+9375,
+24836,
+10028,
+3386,
+36146,
+36654,
+21560,
+59015,
+17237,
+41012,
+2660,
+60563,
+370,
+20669,
+54482,
+12742,
+17878,
+39549,
+36826,
+28026,
+46436,
+55680,
+4250,
+36431,
+55207,
+19480,
+35153,
+14026,
+30660,
+23075,
+31642,
+55377,
+859,
+31474,
+153,
+55750,
+56785,
+24363,
+11226,
+61516,
+47067,
+35642,
+16371,
+10701,
+64895,
+40591,
+33714,
+2009,
+36943,
+50894,
+11079,
+65260,
+52953,
+10074,
+8607,
+44106,
+33479,
+63880,
+30672,
+61957,
+60777,
+36591,
+48346,
+32739,
+50591,
+54803,
+44191,
+56855,
+36012,
+6889,
+32370,
+50617,
+124,
+51382,
+33427,
+4920,
+52087,
+5488,
+63162,
+46683,
+8184,
+5750,
+15579,
+3505,
+2861,
+58508,
+9093,
+58353,
+57217,
+3294,
+2348,
+65259,
+11121,
+50895,
+39889,
+55981,
+31943,
+47256,
+47207,
+60997,
+60008,
+41341,
+10462,
+42663,
+44550,
+1297,
+35496,
+1238,
+24703,
+18680,
+56999,
+35038,
+36659,
+27233,
+44614,
+44867,
+54380,
+52796,
+56726,
+15898,
+26675,
+19412,
+4069,
+30805,
+45568,
+31926,
+3934,
+6130,
+54902,
+18607,
+54722,
+24891,
+3479,
+47327,
+24684,
+58298,
+47619,
+42545,
+8031,
+24814,
+32718,
+1325,
+8848,
+18072,
+22335,
+1694,
+7270,
+44807,
+14067,
+56505,
+38165,
+25293,
+26919,
+25595,
+65305,
+28372,
+10192,
+45169,
+43126,
+64067,
+62192,
+19423,
+63382,
+13723,
+45811,
+39795,
+61416,
+53560,
+1090,
+60432,
+51144,
+8776,
+8461,
+47082,
+10751,
+34336,
+20433,
+26092,
+33469,
+17845,
+46629,
+45697,
+4127,
+14750,
+15831,
+52395,
+23017,
+22679,
+38124,
+19938,
+60279,
+16432,
+30921,
+64349,
+27828,
+5547,
+9841,
+26044,
+33966,
+19550,
+23811,
+8800,
+64354,
+8706,
+3215,
+39474,
+52585,
+60761,
+26106,
+60796,
+27474,
+57602,
+604,
+4659,
+24936,
+59609,
+58406,
+24883,
+39467,
+61043,
+63598,
+18093,
+55936,
+868,
+64633,
+63651,
+28580,
+26834,
+8666,
+50150,
+4810,
+56629,
+14864,
+61936,
+2313,
+5807,
+30524,
+30323,
+51570,
+45280,
+49487,
+41151,
+14170,
+4228,
+34079,
+3449,
+40330,
+18724,
+65531,
+26216,
+35748,
+53972,
+11398,
+4520,
+22470,
+6394,
+23705,
+49462,
+33324,
+57471,
+31384,
+51919,
+41849,
+11782,
+25525,
+33093,
+7350,
+1160,
+45160,
+19399,
+7004,
+7654,
+38039,
+34308,
+51584,
+39078,
+37674,
+39398,
+22700,
+62703,
+49067,
+51232,
+28733,
+29476,
+31092,
+9193,
+17900,
+22502,
+7678,
+3210,
+20105,
+64772,
+15699,
+51650,
+56481,
+55275,
+57088,
+15881,
+21042,
+55223,
+42146,
+6409,
+61479,
+59561,
+46365,
+41321,
+28519,
+26549,
+35233,
+20110,
+7066,
+5389,
+44661,
+17353,
+11946,
+51972,
+52333,
+14123,
+14390,
+51967,
+2585,
+45441,
+58288,
+15472,
+26935,
+18339,
+32137,
+61796,
+32016,
+23242,
+28564,
+60658,
+25407,
+60258,
+14889,
+49310,
+22882,
+35781,
+7417,
+64575,
+45079,
+39534,
+41208,
+57820,
+55507,
+62153,
+61741,
+46749,
+36762,
+37589,
+27030,
+45143,
+20408,
+49283,
+20848,
+46929,
+54496,
+40647,
+60664,
+59821,
+48933,
+26815,
+52871,
+23970,
+26592,
+60582,
+14090,
+51106,
+61891,
+39503,
+51551,
+21075,
+47543,
+1050,
+52256,
+17999,
+12646,
+53837,
+38279,
+28135,
+28253,
+33303,
+3183,
+49092,
+48811,
+28418,
+59752,
+62626,
+40697,
+39710,
+39276,
+22330,
+34705,
+5430,
+40221,
+36302,
+24013,
+59886,
+4374,
+5214,
+16690,
+10363,
+32662,
+63985,
+47453,
+13581,
+54049,
+5364,
+57268,
+27641,
+41779,
+35284,
+59470,
+12458,
+14293,
+24159,
+54343,
+8087,
+39811,
+34335,
+10997,
+47083,
+23626,
+42421,
+25283,
+2409,
+46312,
+51489,
+33467,
+26094,
+30636,
+61833,
+51574,
+48222,
+1587,
+46595,
+14945,
+54431,
+21347,
+20720,
+50562,
+61402,
+55281,
+5382,
+27163,
+27025,
+35888,
+45376,
+47226,
+34101,
+43078,
+31503,
+16612,
+2293,
+27170,
+48321,
+64118,
+50055,
+42737,
+55020,
+38260,
+46035,
+11340,
+24659,
+18320,
+56520,
+64484,
+58000,
+50780,
+64894,
+11128,
+16372,
+18934,
+25797,
+37581,
+26966,
+48949,
+25537,
+54131,
+13344,
+10648,
+33308,
+12005,
+7070,
+63777,
+32192,
+16855,
+3613,
+4190,
+34560,
+63911,
+51503,
+51023,
+45461,
+61627,
+29693,
+15029,
+41438,
+43327,
+32529,
+18824,
+60396,
+24571,
+29961,
+21006,
+47122,
+38763,
+64904,
+42206,
+37354,
+7217,
+50493,
+32458,
+21282,
+11998,
+6304,
+26590,
+23972,
+2024,
+46864,
+12577,
+24141,
+33307,
+10691,
+13345,
+5879,
+62785,
+47909,
+58511,
+33138,
+21316,
+51943,
+20704,
+50006,
+9821,
+59406,
+54494,
+46931,
+28981,
+14159,
+524,
+21597,
+56541,
+14041,
+48442,
+20754,
+17567,
+10271,
+27978,
+18590,
+19795,
+22543,
+64099,
+45471,
+21679,
+30330,
+12490,
+30558,
+18050,
+8504,
+57971,
+62220,
+16741,
+9298,
+55058,
+57263,
+28971,
+25420,
+15539,
+36156,
+58817,
+17763,
+50932,
+52322,
+62587,
+61560,
+64071,
+37668,
+41031,
+12282,
+2370,
+13324,
+27288,
+64330,
+3968,
+49323,
+16457,
+34880,
+41045,
+16889,
+30356,
+57317,
+41457,
+58505,
+61968,
+22612,
+20129,
+3006,
+9242,
+19521,
+61439,
+30586,
+18522,
+17023,
+7889,
+31590,
+10409,
+16955,
+13842,
+48343,
+2898,
+52833,
+9757,
+60118,
+32455,
+58093,
+13244,
+49249,
+10278,
+37172,
+53493,
+20332,
+18992,
+58485,
+3691,
+36870,
+63629,
+37598,
+49922,
+37630,
+38218,
+14009,
+36596,
+29547,
+37567,
+29366,
+31994,
+12922,
+56795,
+15986,
+14189,
+31698,
+34807,
+57376,
+1021,
+6606,
+5903,
+52939,
+12867,
+65319,
+21746,
+40076,
+35027,
+22799,
+54825,
+27278,
+3696,
+24553,
+8422,
+27039,
+31880,
+37994,
+13857,
+59007,
+57578,
+29113,
+59629,
+42483,
+21334,
+30160,
+32160,
+40402,
+43090,
+23654,
+49685,
+54354,
+34645,
+45746,
+62945,
+431,
+2580,
+57184,
+34896,
+36549,
+48545,
+32756,
+25646,
+9558,
+47582,
+669,
+45290,
+56238,
+54930,
+8275,
+294,
+25105,
+30488,
+5938,
+26783,
+30697,
+24247,
+12350,
+2479,
+43155,
+17181,
+49087,
+39222,
+35280,
+42662,
+11069,
+41342,
+708,
+30967,
+21877,
+7433,
+4118,
+16787,
+31126,
+6928,
+52476,
+5255,
+54079,
+48709,
+26201,
+60418,
+64552,
+10058,
+30955,
+50695,
+20831,
+11380,
+37621,
+37769,
+43819,
+47371,
+55822,
+14621,
+30143,
+36459,
+57904,
+51790,
+41113,
+20725,
+5386,
+54236,
+34956,
+47765,
+21634,
+61079,
+10294,
+56613,
+37819,
+57624,
+52144,
+4146,
+22758,
+35855,
+41881,
+15854,
+64083,
+40099,
+16954,
+10565,
+31591,
+25876,
+55139,
+56045,
+64737,
+3446,
+61070,
+11638,
+4241,
+39660,
+55131,
+9493,
+24693,
+47430,
+36476,
+21667,
+50660,
+57445,
+82,
+3949,
+19463,
+65272,
+49402,
+49432,
+12629,
+15843,
+45886,
+22437,
+17003,
+41877,
+4591,
+21252,
+50536,
+50364,
+49914,
+51631,
+37716,
+8828,
+63660,
+63407,
+13761,
+57028,
+30189,
+43489,
+32661,
+10770,
+16691,
+7135,
+21464,
+42755,
+52155,
+40770,
+14992,
+42550,
+49580,
+56662,
+21278,
+14689,
+13443,
+22937,
+65518,
+3642,
+42511,
+52664,
+11360,
+64840,
+25519,
+42202,
+19097,
+12722,
+47159,
+34344,
+12992,
+43425,
+742,
+3000,
+2163,
+29105,
+54390,
+46058,
+63462,
+9386,
+52964,
+34422,
+30680,
+42447,
+2127,
+63105,
+19007,
+7507,
+30177,
+64311,
+108,
+44494,
+54665,
+23392,
+25428,
+10176,
+909,
+57832,
+50081,
+32937,
+7154,
+62726,
+50075,
+2379,
+43316,
+14474,
+31006,
+19943,
+36256,
+59376,
+59229,
+56612,
+10422,
+61080,
+25401,
+8177,
+45239,
+28848,
+56676,
+9225,
+61695,
+274,
+23356,
+478,
+60202,
+5616,
+53252,
+37171,
+10553,
+49250,
+34854,
+57073,
+2239,
+33735,
+27977,
+10624,
+17568,
+568,
+12210,
+28763,
+52699,
+42125,
+16148,
+9055,
+54661,
+8450,
+63576,
+8260,
+49124,
+4134,
+54808,
+60156,
+60507,
+44648,
+17292,
+22664,
+15812,
+3242,
+45693,
+46983,
+23481,
+8879,
+46888,
+24486,
+18883,
+9708,
+52436,
+33655,
+27545,
+45422,
+493,
+65140,
+32935,
+50083,
+14815,
+62853,
+48195,
+43875,
+48858,
+61801,
+56712,
+14729,
+33339,
+34977,
+47489,
+4918,
+33429,
+33785,
+57680,
+52182,
+3137,
+2361,
+25056,
+28960,
+32223,
+23953,
+60036,
+47986,
+7197,
+12848,
+4617,
+13923,
+21638,
+6449,
+56627,
+4812,
+64721,
+19059,
+6510,
+43897,
+59090,
+53246,
+33900,
+45168,
+11015,
+28373,
+24459,
+63215,
+26741,
+31054,
+31842,
+63272,
+43509,
+45901,
+40428,
+33409,
+61758,
+55390,
+7875,
+908,
+10311,
+25429,
+53897,
+61298,
+3666,
+22359,
+33980,
+46512,
+26510,
+29871,
+21100,
+20683,
+45529,
+17811,
+46041,
+33894,
+54397,
+64679,
+47414,
+28126,
+53515,
+3343,
+46116,
+7449,
+18237,
+20789,
+23716,
+44957,
+3778,
+55358,
+57663,
+48082,
+46730,
+59192,
+37098,
+41619,
+63165,
+52215,
+46331,
+13163,
+29180,
+1636,
+28746,
+31918,
+51960,
+35217,
+33726,
+27932,
+32102,
+25556,
+7553,
+22480,
+64197,
+18600,
+1664,
+53454,
+15238,
+40874,
+32626,
+40759,
+22500,
+17902,
+62485,
+39654,
+50611,
+888,
+31848,
+13757,
+23633,
+40728,
+40745,
+18017,
+52571,
+61580,
+38750,
+5210,
+20589,
+28307,
+48825,
+64836,
+49341,
+46405,
+2214,
+28642,
+23099,
+54113,
+52566,
+51324,
+8426,
+64387,
+13405,
+52852,
+18204,
+52427,
+24578,
+59296,
+36624,
+40438,
+33861,
+43093,
+37341,
+8606,
+11118,
+52954,
+475,
+19965,
+63539,
+45913,
+345,
+51332,
+64020,
+6010,
+45226,
+31098,
+2690,
+21457,
+60410,
+30954,
+10445,
+64553,
+53604,
+21724,
+39287,
+64005,
+6422,
+63783,
+59818,
+150,
+43837,
+2831,
+2616,
+41547,
+16319,
+65028,
+26190,
+59480,
+6743,
+33751,
+44181,
+2984,
+61103,
+40021,
+24369,
+44301,
+59063,
+7716,
+54710,
+3385,
+11169,
+24837,
+61530,
+2275,
+52206,
+52937,
+5905,
+7691,
+29387,
+42080,
+53912,
+54166,
+27017,
+51520,
+61643,
+11297,
+30484,
+53570,
+29089,
+50608,
+14256,
+41469,
+61260,
+47805,
+45209,
+21909,
+29204,
+37988,
+22761,
+18811,
+38786,
+19667,
+44642,
+35329,
+28388,
+14120,
+29653,
+22506,
+63523,
+35488,
+12841,
+32004,
+28542,
+3925,
+1687,
+61539,
+952,
+64644,
+24908,
+25738,
+14023,
+716,
+28702,
+59775,
+11650,
+46,
+6973,
+62014,
+36305,
+21964,
+59863,
+52761,
+12580,
+32147,
+16108,
+9525,
+19519,
+9244,
+19694,
+35766,
+8536,
+30501,
+38502,
+65458,
+46213,
+6262,
+11386,
+18309,
+31438,
+49049,
+59957,
+24200,
+32584,
+14287,
+52626,
+1661,
+41839,
+178,
+39909,
+2387,
+43610,
+52485,
+5811,
+22745,
+23440,
+35919,
+23999,
+25869,
+52423,
+7023,
+44050,
+16036,
+49024,
+12698,
+6452,
+3190,
+45041,
+56335,
+43557,
+62425,
+49949,
+25813,
+51770,
+41596,
+57016,
+51746,
+19472,
+17596,
+60134,
+28863,
+25724,
+31204,
+37307,
+31250,
+1383,
+29218,
+52558,
+63816,
+17033,
+1867,
+45084,
+37182,
+61782,
+5186,
+34292,
+25816,
+36048,
+14365,
+36930,
+47597,
+59605,
+5979,
+35871,
+22628,
+42363,
+33362,
+3694,
+27280,
+45856,
+62764,
+51518,
+27019,
+9589,
+47107,
+47437,
+60751,
+37077,
+32971,
+26248,
+1534,
+62037,
+47557,
+29487,
+51174,
+13029,
+43523,
+54180,
+62739,
+39834,
+47569,
+19100,
+54387,
+61288,
+41949,
+39406,
+57655,
+52399,
+24356,
+51385,
+31614,
+4319,
+19920,
+4666,
+13833,
+46290,
+26320,
+26043,
+10975,
+5548,
+37228,
+16714,
+2032,
+37879,
+4418,
+23745,
+12037,
+6779,
+50971,
+53623,
+29598,
+52687,
+60922,
+33849,
+20665,
+1501,
+40798,
+59405,
+10637,
+50007,
+43124,
+45171,
+36517,
+12612,
+53104,
+3960,
+52467,
+45399,
+38216,
+37632,
+52308,
+39671,
+61641,
+51522,
+63485,
+26906,
+41035,
+2066,
+20515,
+24563,
+5297,
+57584,
+32036,
+60700,
+36686,
+18917,
+6545,
+64687,
+6794,
+62143,
+30442,
+65383,
+54401,
+8753,
+11942,
+26961,
+14403,
+30035,
+32008,
+53026,
+14811,
+3492,
+33717,
+36583,
+21944,
+56961,
+37466,
+31957,
+24542,
+34365,
+29867,
+12835,
+18305,
+4315,
+50490,
+58939,
+31483,
+48854,
+57339,
+18486,
+19052,
+60117,
+10559,
+52834,
+40901,
+62883,
+31522,
+2211,
+44117,
+13262,
+63769,
+45213,
+22081,
+27362,
+33209,
+35627,
+45438,
+65463,
+38605,
+22292,
+4152,
+42877,
+40317,
+19824,
+48424,
+43137,
+38731,
+65005,
+7900,
+55858,
+58922,
+13804,
+4905,
+25262,
+51877,
+28017,
+5271,
+9660,
+57730,
+1571,
+47236,
+29464,
+53734,
+18165,
+55101,
+50917,
+25451,
+61225,
+58849,
+50641,
+52435,
+10241,
+18884,
+22100,
+5220,
+41663,
+38459,
+32093,
+8600,
+14006,
+24785,
+63584,
+38645,
+29639,
+37132,
+9431,
+40642,
+50424,
+63001,
+9489,
+61208,
+50986,
+34355,
+59779,
+19069,
+2795,
+63405,
+63662,
+48227,
+57636,
+55807,
+6616,
+24435,
+16750,
+15905,
+60639,
+27704,
+14426,
+16718,
+56679,
+51886,
+9253,
+63293,
+25012,
+7581,
+4781,
+26383,
+45629,
+57729,
+9722,
+5272,
+8204,
+31815,
+31445,
+224,
+28546,
+9634,
+57260,
+65245,
+13739,
+14277,
+56849,
+33920,
+51208,
+47347,
+26401,
+46283,
+24748,
+5411,
+27962,
+51150,
+18720,
+38838,
+35455,
+57259,
+9653,
+28547,
+6321,
+47096,
+25473,
+53701,
+22633,
+46251,
+32360,
+38117,
+37869,
+12899,
+28314,
+29727,
+38239,
+17333,
+57844,
+36152,
+30867,
+62831,
+16040,
+60717,
+28098,
+7194,
+21707,
+39831,
+2135,
+60905,
+58080,
+8993,
+62514,
+45261,
+47443,
+54588,
+64670,
+47451,
+63987,
+38037,
+7656,
+14774,
+12412,
+51853,
+56361,
+29123,
+47106,
+9876,
+27020,
+2960,
+43803,
+48669,
+61058,
+60401,
+30402,
+27457,
+25948,
+23474,
+22888,
+23431,
+21305,
+1013,
+41818,
+36859,
+27832,
+65133,
+46999,
+15999,
+38803,
+2151,
+45347,
+42844,
+13191,
+25720,
+4832,
+50025,
+3791,
+47581,
+10484,
+25647,
+54461,
+33595,
+39244,
+52125,
+60055,
+9483,
+19716,
+25442,
+18869,
+6645,
+32947,
+31948,
+42746,
+16125,
+52659,
+29413,
+63428,
+61030,
+51939,
+17001,
+22439,
+5588,
+54023,
+20693,
+1822,
+43932,
+13018,
+63907,
+51457,
+14962,
+19518,
+9963,
+16109,
+41223,
+34558,
+4192,
+27213,
+34383,
+47242,
+14234,
+18960,
+675,
+29958,
+63112,
+12519,
+34722,
+6154,
+59341,
+8168,
+18068,
+58429,
+39598,
+15012,
+60675,
+46218,
+62902,
+13611,
+3222,
+24585,
+40004,
+21098,
+29873,
+24692,
+10397,
+55132,
+801,
+61207,
+9690,
+63002,
+14355,
+51975,
+59961,
+19715,
+9551,
+60056,
+15871,
+19032,
+59674,
+8263,
+35220,
+19455,
+6903,
+22489,
+48179,
+7092,
+44065,
+17668,
+18888,
+37247,
+21398,
+30373,
+39523,
+6821,
+42357,
+58673,
+35238,
+14777,
+13088,
+7837,
+64844,
+7104,
+33881,
+43555,
+56337,
+61356,
+28646,
+52173,
+42718,
+22648,
+7860,
+61728,
+30274,
+18938,
+4067,
+19414,
+48974,
+33553,
+19441,
+28866,
+23980,
+15398,
+13288,
+50886,
+62751,
+40641,
+9694,
+37133,
+41488,
+47832,
+43353,
+1620,
+20499,
+7907,
+28286,
+35676,
+14546,
+64236,
+31610,
+34369,
+42376,
+54352,
+49687,
+44478,
+15366,
+21342,
+25251,
+35819,
+59707,
+39240,
+50203,
+12033,
+45524,
+34908,
+12692,
+53234,
+2670,
+14453,
+1792,
+9212,
+15978,
+49083,
+37655,
+43889,
+28481,
+51923,
+36373,
+24449,
+8104,
+17208,
+52963,
+10327,
+63463,
+44704,
+54159,
+12916,
+15628,
+26641,
+42373,
+49303,
+18633,
+24835,
+11171,
+8900,
+62880,
+24454,
+12015,
+29677,
+4274,
+8402,
+14336,
+28836,
+40050,
+23831,
+49151,
+21848,
+58984,
+19276,
+2731,
+16913,
+36218,
+49517,
+26467,
+38878,
+6951,
+8080,
+7373,
+52731,
+41135,
+5840,
+58862,
+50720,
+2686,
+38611,
+35178,
+33456,
+60731,
+22124,
+29172,
+37176,
+61487,
+40385,
+41302,
+6916,
+27042,
+4923,
+8639,
+42729,
+42291,
+35080,
+59459,
+31869,
+47777,
+16567,
+42979,
+11579,
+22373,
+45236,
+53680,
+5479,
+49668,
+33071,
+58628,
+7733,
+33905,
+20051,
+30766,
+50001,
+12792,
+25974,
+39356,
+13937,
+17770,
+43905,
+44269,
+16423,
+18137,
+20246,
+55057,
+10608,
+16742,
+49188,
+21244,
+37352,
+42208,
+59422,
+24773,
+4395,
+15938,
+13837,
+9061,
+1715,
+27690,
+58852,
+38780,
+48076,
+50475,
+1363,
+1010,
+56333,
+45043,
+32931,
+29663,
+41715,
+54335,
+9111,
+25498,
+41733,
+46397,
+23399,
+60141,
+64469,
+51411,
+27965,
+47552,
+3729,
+21138,
+61241,
+23988,
+49220,
+50174,
+34220,
+13334,
+63292,
+9668,
+51887,
+19241,
+47633,
+3054,
+64637,
+730,
+2332,
+19693,
+9961,
+19520,
+10573,
+3007,
+4206,
+23742,
+48532,
+23662,
+6961,
+43275,
+5103,
+14395,
+39193,
+6340,
+27855,
+24208,
+48613,
+44037,
+61694,
+10287,
+56677,
+16720,
+47536,
+52893,
+14593,
+52286,
+64435,
+2382,
+7289,
+45787,
+59797,
+15977,
+9398,
+1793,
+32185,
+62669,
+23523,
+53550,
+44230,
+64526,
+56259,
+20495,
+55616,
+21981,
+56084,
+4182,
+58989,
+6848,
+22727,
+51054,
+17899,
+10886,
+31093,
+48552,
+8579,
+44861,
+2147,
+47018,
+53544,
+2269,
+43394,
+35534,
+5189,
+34007,
+20939,
+6176,
+50663,
+13683,
+55945,
+36415,
+15552,
+3608,
+35448,
+5331,
+64272,
+53872,
+53306,
+4647,
+32840,
+43391,
+3869,
+6995,
+3083,
+24588,
+21414,
+15878,
+32883,
+8796,
+40320,
+62874,
+22774,
+8628,
+25672,
+62393,
+6611,
+42156,
+62306,
+37071,
+22235,
+5038,
+2186,
+21547,
+29944,
+1649,
+36662,
+23005,
+63034,
+8514,
+3680,
+33633,
+2992,
+18241,
+49813,
+65092,
+45316,
+49614,
+639,
+52533,
+26492,
+34819,
+32474,
+42427,
+46534,
+34087,
+5986,
+41576,
+14900,
+59319,
+25227,
+35001,
+44734,
+59719,
+25497,
+9272,
+54336,
+19469,
+24213,
+36104,
+23513,
+43461,
+62204,
+62686,
+20168,
+30763,
+51659,
+13541,
+57606,
+42786,
+31767,
+46247,
+58352,
+11085,
+58509,
+47911,
+1031,
+58997,
+12930,
+4385,
+33158,
+62601,
+17351,
+44663,
+41933,
+36561,
+60001,
+48393,
+2765,
+4953,
+51449,
+48371,
+14250,
+64793,
+62539,
+28560,
+13895,
+40490,
+32848,
+52512,
+6759,
+61980,
+41192,
+63317,
+1714,
+9287,
+13838,
+62976,
+52405,
+13886,
+54660,
+10263,
+16149,
+58124,
+50427,
+59795,
+45789,
+19685,
+13161,
+46333,
+61661,
+30055,
+14848,
+46339,
+44003,
+12787,
+5459,
+42408,
+38468,
+6991,
+22056,
+14783,
+19014,
+51469,
+54515,
+27373,
+59172,
+48886,
+53639,
+53507,
+36428,
+21226,
+45093,
+38423,
+14521,
+24926,
+29884,
+53242,
+47216,
+54831,
+1054,
+6710,
+27999,
+31536,
+39568,
+41267,
+32463,
+39603,
+15955,
+8877,
+23483,
+59591,
+58615,
+44412,
+8598,
+32095,
+25290,
+38977,
+49506,
+43939,
+54908,
+51497,
+62513,
+9605,
+58081,
+12326,
+45803,
+15770,
+60061,
+29352,
+970,
+47652,
+45819,
+40097,
+64085,
+65267,
+30455,
+6080,
+46419,
+64201,
+46736,
+25543,
+25188,
+33057,
+42900,
+25569,
+45759,
+57565,
+24971,
+61583,
+24538,
+28057,
+36446,
+14931,
+15525,
+22833,
+41202,
+43467,
+60745,
+4734,
+35337,
+42213,
+2086,
+7569,
+47037,
+20230,
+21770,
+36573,
+53767,
+36442,
+652,
+25995,
+52179,
+63595,
+39438,
+8642,
+58888,
+22310,
+15728,
+43684,
+31045,
+1599,
+60336,
+57056,
+26950,
+37087,
+18663,
+35231,
+26551,
+19631,
+6307,
+55242,
+55116,
+27719,
+1403,
+41125,
+4063,
+37688,
+21951,
+35543,
+27557,
+17849,
+27622,
+49184,
+52837,
+59087,
+55335,
+60726,
+54597,
+50674,
+26543,
+25523,
+11784,
+51374,
+24523,
+62879,
+9374,
+11172,
+58945,
+49022,
+16038,
+62833,
+22865,
+57659,
+58233,
+35529,
+62168,
+38899,
+62123,
+30110,
+44687,
+18610,
+6086,
+58292,
+36325,
+52099,
+46887,
+10245,
+23482,
+9007,
+15956,
+55184,
+44260,
+40535,
+39956,
+59612,
+31276,
+42711,
+60519,
+16800,
+45527,
+20685,
+32421,
+18915,
+36688,
+55249,
+56313,
+48818,
+55867,
+13652,
+35292,
+64227,
+16573,
+48096,
+44370,
+12247,
+40159,
+18071,
+11029,
+1326,
+46030,
+49777,
+2438,
+14726,
+30115,
+13269,
+24265,
+18477,
+11872,
+24073,
+37686,
+4065,
+18940,
+50594,
+47498,
+38917,
+8335,
+63659,
+10371,
+37717,
+3735,
+55311,
+50809,
+35424,
+17480,
+43040,
+41856,
+23379,
+3233,
+30743,
+15232,
+4669,
+44249,
+47355,
+12740,
+54484,
+49640,
+26746,
+40663,
+5276,
+32962,
+37908,
+11233,
+59395,
+44354,
+64353,
+10970,
+23812,
+19822,
+40319,
+9157,
+32884,
+14346,
+60029,
+40737,
+27673,
+4645,
+53308,
+12387,
+34965,
+18581,
+45156,
+20540,
+41510,
+3097,
+46520,
+61454,
+63499,
+11326,
+8460,
+11000,
+51145,
+13572,
+7799,
+49347,
+59664,
+37312,
+52458,
+39234,
+26306,
+17487,
+1144,
+40239,
+47872,
+41789,
+60540,
+6232,
+25709,
+61773,
+25655,
+35725,
+48043,
+11941,
+9786,
+54402,
+41175,
+35375,
+33379,
+38155,
+48027,
+62712,
+15573,
+2286,
+52614,
+55004,
+49552,
+17644,
+41704,
+16388,
+24146,
+1928,
+40125,
+50177,
+18251,
+13247,
+53512,
+30126,
+4715,
+15746,
+37616,
+11754,
+14820,
+41956,
+62389,
+49690,
+32389,
+42336,
+38778,
+58854,
+18901,
+14324,
+13145,
+26827,
+32855,
+64040,
+47270,
+23649,
+45234,
+22375,
+3214,
+10968,
+64355,
+15428,
+41131,
+32889,
+60144,
+5823,
+2893,
+32226,
+61302,
+57164,
+49279,
+25366,
+50866,
+52033,
+59806,
+32678,
+51259,
+55889,
+31618,
+14796,
+51675,
+49032,
+21345,
+54433,
+29526,
+40417,
+46591,
+33131,
+23890,
+23787,
+59667,
+68,
+50341,
+43054,
+45077,
+64577,
+53237,
+61371,
+50149,
+10943,
+26835,
+16649,
+11900,
+65136,
+20809,
+60792,
+5071,
+27924,
+62365,
+7276,
+46175,
+21621,
+57236,
+2583,
+51969,
+33511,
+16837,
+49903,
+14984,
+20504,
+33682,
+50381,
+58887,
+8941,
+39439,
+42728,
+9331,
+4924,
+37234,
+48576,
+46244,
+39934,
+7637,
+58013,
+52340,
+4883,
+25671,
+9153,
+22775,
+52811,
+29239,
+60681,
+29793,
+36113,
+24106,
+13632,
+16485,
+52495,
+4936,
+29338,
+30376,
+15224,
+59503,
+51177,
+28769,
+15703,
+33191,
+44105,
+11117,
+10075,
+37342,
+18776,
+63922,
+48768,
+14005,
+9701,
+32094,
+9002,
+44413,
+57890,
+61,
+22199,
+58078,
+60907,
+32635,
+20775,
+42501,
+2297,
+58836,
+41348,
+12657,
+43984,
+47387,
+17251,
+59175,
+44860,
+9190,
+48553,
+43592,
+55763,
+28072,
+58329,
+39576,
+47379,
+3892,
+54242,
+413,
+8391,
+5944,
+6924,
+25771,
+15068,
+1745,
+19557,
+40175,
+20465,
+57811,
+2351,
+25887,
+44802,
+37024,
+60534,
+40657,
+41635,
+51795,
+36969,
+28671,
+37453,
+24031,
+14839,
+59501,
+15226,
+65000,
+25311,
+53444,
+2402,
+46856,
+54476,
+30500,
+9958,
+35767,
+42629,
+49339,
+64838,
+11362,
+13699,
+59190,
+46732,
+38921,
+50079,
+57834,
+469,
+26913,
+23940,
+55623,
+13792,
+22656,
+41278,
+3732,
+62097,
+3679,
+9137,
+63035,
+15776,
+37046,
+51775,
+55170,
+35349,
+15246,
+63449,
+57970,
+10612,
+18051,
+18215,
+44876,
+38343,
+27336,
+60609,
+52263,
+44563,
+40680,
+48956,
+3349,
+48381,
+28890,
+58304,
+42330,
+12238,
+33977,
+2927,
+61621,
+52598,
+44724,
+5200,
+56604,
+38588,
+14678,
+27682,
+20820,
+30936,
+40776,
+38770,
+21379,
+16655,
+59750,
+28420,
+32517,
+4187,
+30876,
+42445,
+30682,
+55262,
+63299,
+47081,
+10999,
+8777,
+11327,
+17039,
+6897,
+5464,
+16766,
+39854,
+60544,
+62164,
+63575,
+10261,
+54662,
+49657,
+32656,
+2646,
+60710,
+16555,
+27952,
+36883,
+62377,
+25415,
+49244,
+45646,
+25626,
+63152,
+41319,
+46367,
+62102,
+1127,
+33060,
+27000,
+13972,
+7224,
+64386,
+10088,
+51325,
+6854,
+27038,
+10513,
+24554,
+23763,
+34267,
+1487,
+28233,
+50095,
+58590,
+44639,
+31526,
+4620,
+21147,
+1504,
+27348,
+14724,
+2440,
+39146,
+16086,
+58152,
+14335,
+9368,
+4275,
+17734,
+25898,
+46238,
+25631,
+26922,
+26449,
+62250,
+8238,
+5943,
+8568,
+414,
+13898,
+64826,
+31108,
+13606,
+3228,
+37529,
+16910,
+50048,
+62284,
+38110,
+33711,
+1807,
+27296,
+48206,
+19352,
+4234,
+56841,
+16259,
+18594,
+58905,
+46038,
+885,
+38835,
+29494,
+13695,
+37368,
+43963,
+60811,
+26729,
+18446,
+19342,
+40547,
+19676,
+45341,
+8299,
+62956,
+55047,
+30642,
+58215,
+22157,
+38330,
+1892,
+1005,
+14268,
+21542,
+35007,
+46370,
+2569,
+16398,
+43322,
+7474,
+28697,
+58808,
+63658,
+8830,
+38918,
+1919,
+61203,
+43652,
+40463,
+5626,
+53961,
+42760,
+12728,
+23798,
+53672,
+4945,
+6560,
+7183,
+54454,
+62244,
+36771,
+35491,
+26519,
+29935,
+12368,
+12267,
+32871,
+33561,
+21475,
+24112,
+61790,
+30950,
+21438,
+15096,
+37638,
+11674,
+43915,
+56884,
+62955,
+8355,
+45342,
+33120,
+14038,
+3026,
+44147,
+6172,
+37977,
+49712,
+58841,
+43043,
+26830,
+30653,
+62919,
+23992,
+20419,
+30019,
+26707,
+63970,
+33188,
+30081,
+55902,
+55885,
+293,
+10478,
+54931,
+36567,
+7487,
+24096,
+60172,
+54774,
+37897,
+16521,
+57331,
+33724,
+35219,
+9478,
+59675,
+49123,
+10259,
+63577,
+13182,
+33463,
+40143,
+31661,
+38263,
+27722,
+22849,
+53988,
+33547,
+7778,
+50289,
+2090,
+18399,
+28043,
+45639,
+39451,
+27266,
+63358,
+5989,
+5942,
+8393,
+62251,
+5228,
+43755,
+3717,
+53111,
+7396,
+31188,
+50829,
+4221,
+33111,
+46745,
+11608,
+44605,
+1028,
+59169,
+33454,
+35180,
+54957,
+24081,
+3166,
+57633,
+21960,
+22920,
+59545,
+15305,
+36272,
+23003,
+36664,
+52915,
+13933,
+49015,
+57271,
+31814,
+9658,
+5273,
+51434,
+1846,
+19765,
+16682,
+55776,
+47052,
+27483,
+28494,
+17476,
+46811,
+8,
+58256,
+50117,
+41944,
+15888,
+30371,
+21400,
+5749,
+11091,
+46684,
+64728,
+37555,
+2027,
+53678,
+45238,
+10291,
+25402,
+32051,
+23051,
+41978,
+52104,
+24431,
+58502,
+18067,
+9508,
+59342,
+61147,
+42886,
+33788,
+8157,
+30729,
+55009,
+64517,
+17559,
+30728,
+8163,
+33789,
+23611,
+51785,
+30570,
+46958,
+57569,
+24309,
+50530,
+13553,
+14705,
+39784,
+29031,
+44429,
+61368,
+3759,
+3365,
+15402,
+25598,
+38293,
+53351,
+42475,
+32410,
+37490,
+60549,
+42238,
+35650,
+37186,
+48147,
+51612,
+43901,
+4460,
+2807,
+20563,
+30102,
+47196,
+33402,
+29645,
+23078,
+62332,
+46781,
+27334,
+38345,
+35757,
+19775,
+62572,
+21913,
+12469,
+36070,
+2045,
+44365,
+31960,
+17207,
+9389,
+24450,
+14180,
+7192,
+28100,
+48471,
+45345,
+2153,
+51816,
+47993,
+59359,
+14876,
+55146,
+65456,
+38504,
+3257,
+39810,
+10754,
+54344,
+51619,
+6137,
+14789,
+26557,
+7372,
+9352,
+6952,
+44792,
+46680,
+41622,
+50709,
+20528,
+7152,
+32939,
+27324,
+3354,
+18031,
+528,
+32212,
+54625,
+34154,
+45780,
+39779,
+19957,
+12870,
+43526,
+34830,
+3115,
+17521,
+30271,
+16907,
+19928,
+32268,
+40281,
+50038,
+64096,
+5781,
+42744,
+31950,
+7176,
+60470,
+27155,
+16810,
+59971,
+59791,
+42004,
+856,
+18125,
+64428,
+2781,
+42805,
+7823,
+33515,
+24813,
+11033,
+42546,
+17125,
+33642,
+24657,
+11342,
+43675,
+35619,
+22766,
+14109,
+25468,
+19228,
+17792,
+19854,
+21565,
+53118,
+43198,
+60524,
+26663,
+64400,
+27635,
+46825,
+39188,
+19977,
+17745,
+53037,
+4159,
+43372,
+63891,
+39124,
+39948,
+12067,
+28463,
+64693,
+57686,
+27777,
+1787,
+32293,
+21064,
+30251,
+6069,
+32424,
+18794,
+54059,
+38180,
+57946,
+23375,
+31135,
+42033,
+11930,
+62006,
+25566,
+45972,
+17752,
+13671,
+28997,
+30184,
+62570,
+19777,
+56568,
+40567,
+42044,
+18578,
+57386,
+41229,
+43003,
+51903,
+49054,
+33859,
+40440,
+64261,
+6529,
+55030,
+36425,
+14076,
+34243,
+26429,
+60239,
+55739,
+33502,
+6693,
+23684,
+22104,
+12047,
+16543,
+36513,
+12398,
+16376,
+24860,
+53910,
+42082,
+48641,
+18318,
+24661,
+32913,
+38062,
+54577,
+64934,
+29939,
+51995,
+56439,
+22752,
+26028,
+42399,
+20826,
+44543,
+46707,
+6294,
+761,
+26233,
+25260,
+4907,
+25225,
+59321,
+33766,
+39251,
+29502,
+27717,
+55118,
+13394,
+12443,
+53051,
+15943,
+28285,
+9424,
+20500,
+62370,
+57320,
+43578,
+17309,
+55857,
+9731,
+65006,
+37963,
+49354,
+32810,
+772,
+1951,
+26894,
+31199,
+45106,
+31589,
+10567,
+17024,
+14525,
+23710,
+17978,
+36052,
+26130,
+55684,
+13139,
+47847,
+42671,
+56644,
+7305,
+907,
+10178,
+55391,
+17456,
+6935,
+64338,
+35833,
+44266,
+38291,
+25600,
+48732,
+60854,
+17876,
+12744,
+47273,
+61727,
+9447,
+22649,
+31751,
+55319,
+13356,
+1944,
+62775,
+41018,
+843,
+57540,
+37761,
+64009,
+47040,
+451,
+43644,
+944,
+62972,
+42917,
+49182,
+27624,
+47088,
+4431,
+64843,
+9458,
+13089,
+37666,
+64073,
+63119,
+28526,
+41627,
+6470,
+12205,
+47696,
+30738,
+12821,
+3124,
+33514,
+8034,
+42806,
+29027,
+27430,
+35480,
+50691,
+48241,
+26572,
+6734,
+18762,
+2701,
+30344,
+27814,
+3130,
+19907,
+55876,
+55816,
+63967,
+60818,
+34742,
+20779,
+42,
+39628,
+49346,
+8773,
+13573,
+32924,
+25127,
+59761,
+45701,
+26457,
+27865,
+4105,
+7008,
+11502,
+60530,
+4570,
+59889,
+825,
+37054,
+13616,
+23690,
+38298,
+48462,
+50288,
+8249,
+33548,
+18348,
+25060,
+22551,
+43548,
+52492,
+5583,
+6773,
+50842,
+46917,
+41835,
+24127,
+45897,
+6525,
+45664,
+29864,
+43666,
+50276,
+41346,
+58838,
+28523,
+46415,
+3617,
+42370,
+39214,
+61014,
+35842,
+50510,
+2884,
+7452,
+30514,
+19079,
+31715,
+6508,
+19061,
+21967,
+44109,
+3497,
+6165,
+4582,
+144,
+21696,
+18369,
+33904,
+9314,
+58629,
+44929,
+195,
+48485,
+44921,
+45071,
+56978,
+63187,
+280,
+23557,
+13435,
+48705,
+33558,
+33868,
+24328,
+54709,
+10031,
+59064,
+17408,
+52987,
+2184,
+5040,
+49157,
+62417,
+22421,
+46472,
+64627,
+55228,
+15457,
+57012,
+65069,
+16280,
+31262,
+18872,
+65222,
+39031,
+64183,
+19231,
+48203,
+55919,
+29386,
+10021,
+5906,
+23056,
+43518,
+33847,
+60924,
+17721,
+60394,
+18826,
+32632,
+62636,
+48069,
+3209,
+10883,
+22503,
+51236,
+38622,
+41599,
+50214,
+26114,
+65144,
+41296,
+40575,
+44452,
+46723,
+41745,
+44572,
+32435,
+41787,
+47874,
+13311,
+50419,
+42964,
+24724,
+14773,
+9596,
+38038,
+10900,
+7005,
+53555,
+11975,
+64449,
+1448,
+21123,
+56941,
+65477,
+5555,
+58378,
+30389,
+11243,
+13809,
+24645,
+12826,
+58012,
+8633,
+39935,
+16807,
+29077,
+55105,
+13748,
+43740,
+48384,
+4888,
+6985,
+16859,
+13501,
+34148,
+49041,
+17014,
+41900,
+40048,
+28838,
+20240,
+60814,
+27662,
+17632,
+49426,
+24316,
+17088,
+17228,
+26883,
+7346,
+37243,
+40461,
+43654,
+63133,
+18683,
+16729,
+39617,
+17061,
+5682,
+33398,
+3743,
+48191,
+36905,
+20957,
+27466,
+34460,
+20059,
+27356,
+57551,
+42163,
+42324,
+58567,
+30000,
+31248,
+37309,
+23790,
+20967,
+4780,
+9665,
+25013,
+53258,
+48249,
+42071,
+22085,
+27452,
+64490,
+58458,
+251,
+23147,
+47036,
+8953,
+2087,
+57032,
+54926,
+3318,
+25967,
+24272,
+65511,
+5244,
+48832,
+50217,
+60447,
+49788,
+5074,
+47433,
+22479,
+10126,
+25557,
+460,
+29713,
+62620,
+16610,
+31505,
+31900,
+58701,
+30032,
+61549,
+5935,
+17677,
+42008,
+52254,
+1052,
+54833,
+14758,
+374,
+17128,
+65450,
+12956,
+46975,
+52682,
+53072,
+61158,
+59625,
+52710,
+57899,
+56905,
+13198,
+44217,
+14892,
+1682,
+30803,
+4071,
+23322,
+63453,
+17337,
+59388,
+59771,
+14177,
+40905,
+60589,
+62491,
+30176,
+10319,
+19008,
+41287,
+16917,
+38433,
+29973,
+23031,
+30348,
+39074,
+40670,
+37677,
+20712,
+22586,
+61383,
+22662,
+17294,
+38967,
+32751,
+3973,
+24095,
+8272,
+36568,
+11365,
+49468,
+28858,
+60115,
+19054,
+16761,
+31143,
+64126,
+12876,
+36126,
+28696,
+8339,
+43323,
+27655,
+36080,
+4413,
+20743,
+18859,
+51336,
+31048,
+7357,
+37027,
+45313,
+53408,
+58097,
+43337,
+56944,
+4631,
+55931,
+56545,
+53212,
+50898,
+30513,
+7748,
+2885,
+18236,
+10153,
+46117,
+46264,
+15042,
+33081,
+54981,
+52294,
+58624,
+12128,
+58076,
+22201,
+16186,
+47181,
+26555,
+14791,
+4117,
+10457,
+21878,
+12453,
+19961,
+32323,
+1844,
+51436,
+34057,
+865,
+18088,
+49901,
+16839,
+44651,
+53463,
+48064,
+64574,
+10833,
+35782,
+5197,
+49387,
+56484,
+15111,
+2538,
+17450,
+13348,
+60167,
+34631,
+6982,
+40517,
+49436,
+33490,
+54018,
+17179,
+43157,
+15607,
+17372,
+31187,
+8232,
+53112,
+12276,
+16843,
+38026,
+44114,
+46408,
+26174,
+4941,
+19190,
+49450,
+57133,
+51203,
+61720,
+38042,
+37985,
+29243,
+15072,
+1288,
+6814,
+1857,
+21976,
+52730,
+9351,
+8081,
+26558,
+16033,
+43552,
+23847,
+15657,
+38872,
+1139,
+47889,
+17686,
+16816,
+5983,
+4568,
+60532,
+37026,
+7465,
+31049,
+4482,
+49077,
+14029,
+27108,
+1159,
+10905,
+33094,
+51989,
+37242,
+7610,
+26884,
+42558,
+6792,
+64689,
+43486,
+61903,
+16958,
+35948,
+41357,
+14803,
+27010,
+11179,
+2422,
+5086,
+42258,
+63418,
+64455,
+59536,
+27603,
+38959,
+5574,
+45824,
+34894,
+57186,
+43381,
+24648,
+4513,
+41742,
+26754,
+39066,
+39806,
+2948,
+3979,
+13059,
+60284,
+46109,
+35723,
+25657,
+64667,
+906,
+7877,
+56645,
+30009,
+26432,
+40008,
+59908,
+42982,
+64181,
+39033,
+40251,
+40544,
+7215,
+37356,
+27367,
+27973,
+45786,
+9216,
+2383,
+24220,
+21255,
+39790,
+37942,
+45875,
+65528,
+57346,
+15421,
+2521,
+33705,
+46174,
+8656,
+62366,
+47076,
+18842,
+17673,
+44806,
+11025,
+1695,
+61867,
+59179,
+24290,
+44410,
+58617,
+47418,
+34050,
+49850,
+11311,
+6681,
+46638,
+44179,
+33753,
+26269,
+62926,
+46128,
+11938,
+34183,
+20095,
+54688,
+50066,
+23585,
+59450,
+56654,
+29213,
+36139,
+20081,
+29741,
+40852,
+54501,
+17919,
+20602,
+53953,
+12890,
+36646,
+57047,
+23208,
+12163,
+15437,
+16935,
+34863,
+3582,
+5342,
+64385,
+8428,
+13973,
+43894,
+52023,
+59301,
+58937,
+50492,
+10661,
+37355,
+7294,
+40545,
+19344,
+41156,
+12224,
+16207,
+37464,
+56963,
+25962,
+46538,
+63927,
+25336,
+17815,
+60068,
+53924,
+64381,
+31890,
+12847,
+10208,
+47987,
+21706,
+9611,
+28099,
+8101,
+14181,
+24091,
+43822,
+59565,
+45968,
+19289,
+56443,
+54453,
+8321,
+6561,
+39481,
+48313,
+58955,
+38411,
+60469,
+8046,
+31951,
+29900,
+17588,
+18687,
+52075,
+48746,
+19545,
+18734,
+50769,
+35439,
+63232,
+36382,
+33871,
+17432,
+3313,
+59981,
+57893,
+35149,
+12536,
+48878,
+62725,
+10306,
+32938,
+8073,
+20529,
+53832,
+24275,
+18383,
+48316,
+24918,
+43780,
+39713,
+65170,
+31337,
+43364,
+36284,
+48159,
+27480,
+25280,
+21463,
+10361,
+16692,
+25579,
+5927,
+54582,
+40655,
+60536,
+20016,
+13775,
+50776,
+18148,
+48288,
+2429,
+6285,
+62233,
+44486,
+29818,
+59672,
+19034,
+62157,
+26049,
+28236,
+4441,
+12173,
+61915,
+25376,
+47468,
+56031,
+25940,
+48687,
+33880,
+9456,
+64845,
+3036,
+27817,
+3443,
+6490,
+52786,
+39976,
+39095,
+63394,
+31974,
+44064,
+9472,
+48180,
+16204,
+16271,
+54007,
+24922,
+52043,
+21858,
+31131,
+3405,
+41967,
+16493,
+1046,
+57964,
+65287,
+64222,
+45837,
+43750,
+54188,
+4985,
+13947,
+63776,
+10688,
+12006,
+54234,
+5388,
+10861,
+20111,
+26803,
+47007,
+28778,
+39131,
+19986,
+28456,
+5952,
+35197,
+52320,
+50934,
+45534,
+46544,
+54989,
+61847,
+62962,
+25233,
+62050,
+65444,
+27783,
+21610,
+47385,
+43986,
+20763,
+22944,
+46481,
+48778,
+15914,
+41481,
+41105,
+57367,
+47471,
+24079,
+54959,
+3570,
+62675,
+54451,
+56445,
+17627,
+54894,
+32400,
+44049,
+9929,
+52424,
+31345,
+47547,
+16592,
+34300,
+44177,
+46640,
+54214,
+32619,
+22288,
+1430,
+51478,
+5544,
+11501,
+7790,
+4106,
+53554,
+7653,
+10901,
+19400,
+32001,
+13040,
+60603,
+6564,
+41759,
+58559,
+3082,
+9163,
+3870,
+43935,
+22055,
+9037,
+38469,
+22749,
+33772,
+22249,
+16858,
+7628,
+4889,
+40516,
+7406,
+34632,
+18525,
+23777,
+58540,
+62456,
+52920,
+5149,
+62013,
+9972,
+47,
+23212,
+57949,
+35538,
+13177,
+32491,
+40295,
+35549,
+31291,
+34778,
+43274,
+9236,
+23663,
+49367,
+38288,
+43908,
+12446,
+30229,
+12148,
+44791,
+8079,
+9353,
+38879,
+16778,
+15441,
+57122,
+20899,
+59809,
+34137,
+18114,
+23781,
+53895,
+25431,
+47337,
+38814,
+48456,
+64337,
+7872,
+17457,
+35127,
+49928,
+48167,
+5608,
+52475,
+10453,
+31127,
+38692,
+25770,
+8566,
+5945,
+56533,
+21126,
+58273,
+18120,
+31878,
+27041,
+9334,
+41303,
+13242,
+58095,
+53410,
+39601,
+32465,
+56222,
+57805,
+20609,
+31364,
+49288,
+22488,
+9475,
+19456,
+24758,
+52933,
+37447,
+5463,
+8457,
+17040,
+26595,
+63303,
+23275,
+37970,
+46554,
+32369,
+11102,
+36013,
+40034,
+3550,
+6073,
+13909,
+47884,
+30902,
+48517,
+61279,
+47264,
+63698,
+22643,
+49229,
+27149,
+15264,
+48850,
+42353,
+48435,
+28552,
+64955,
+64974,
+53760,
+62683,
+23179,
+56075,
+30300,
+26083,
+61229,
+35090,
+17581,
+46606,
+13008,
+35034,
+27037,
+8424,
+51326,
+48598,
+16511,
+52841,
+22726,
+9197,
+58990,
+852,
+5917,
+46280,
+21453,
+46351,
+35791,
+29827,
+30368,
+6201,
+33636,
+57992,
+35506,
+33996,
+28275,
+35771,
+32284,
+46087,
+6737,
+61093,
+41701,
+17714,
+47033,
+33508,
+11949,
+42356,
+9464,
+39524,
+12475,
+59933,
+49607,
+17240,
+1856,
+7377,
+1289,
+20983,
+32985,
+36310,
+43717,
+15566,
+46753,
+6442,
+60269,
+39694,
+16772,
+42442,
+106,
+64313,
+46461,
+33077,
+44257,
+50850,
+62142,
+9791,
+64688,
+7343,
+42559,
+29371,
+1699,
+60642,
+309,
+17548,
+43833,
+48235,
+41643,
+26410,
+46565,
+50970,
+9832,
+12038,
+29408,
+35525,
+5301,
+50841,
+7770,
+5584,
+49673,
+37783,
+15433,
+60124,
+37577,
+34667,
+61691,
+53726,
+60527,
+23251,
+11729,
+61979,
+9066,
+52513,
+51095,
+43119,
+38352,
+20057,
+34462,
+48153,
+2757,
+33720,
+34094,
+2496,
+51365,
+61311,
+45016,
+33750,
+10040,
+59481,
+36390,
+47772,
+14607,
+61092,
+6829,
+46088,
+18761,
+7815,
+26573,
+40469,
+51195,
+36839,
+57200,
+1880,
+31010,
+11449,
+43347,
+52876,
+26749,
+45872,
+29380,
+54936,
+47775,
+31871,
+47116,
+16677,
+3738,
+62716,
+39207,
+33286,
+27998,
+9015,
+1055,
+4035,
+342,
+46431,
+34826,
+14869,
+40165,
+63239,
+56950,
+24190,
+57779,
+37015,
+15824,
+57247,
+22495,
+23683,
+7951,
+33503,
+44821,
+27263,
+4311,
+55826,
+14897,
+37846,
+56253,
+26758,
+56819,
+46637,
+7259,
+11312,
+47649,
+15712,
+59265,
+59696,
+56190,
+14818,
+11756,
+21384,
+37322,
+38939,
+12107,
+43806,
+18510,
+60985,
+25202,
+1426,
+51806,
+21180,
+45259,
+62516,
+24024,
+48006,
+64966,
+35299,
+30213,
+31429,
+64877,
+53803,
+15155,
+56894,
+56597,
+43228,
+12995,
+32946,
+9547,
+18870,
+31264,
+47056,
+63435,
+29541,
+59160,
+12983,
+48543,
+36551,
+48528,
+60232,
+46942,
+57167,
+19500,
+54199,
+25240,
+29990,
+50248,
+36349,
+2206,
+41485,
+36504,
+21110,
+13828,
+45866,
+63329,
+24054,
+24434,
+9678,
+55808,
+51734,
+53159,
+42155,
+9150,
+62394,
+60678,
+29208,
+5902,
+10526,
+1022,
+23192,
+36823,
+43827,
+6235,
+58318,
+15046,
+55773,
+34055,
+51438,
+20153,
+54316,
+54364,
+56639,
+16516,
+33942,
+64980,
+5541,
+51170,
+28609,
+60093,
+47152,
+17223,
+58358,
+27127,
+39372,
+259,
+38567,
+6188,
+52312,
+11692,
+61340,
+53652,
+28650,
+43565,
+30155,
+45833,
+45830,
+3942,
+47421,
+41758,
+6999,
+60604,
+39480,
+7182,
+8322,
+4946,
+3048,
+45677,
+2849,
+45371,
+43688,
+56176,
+46262,
+46119,
+63766,
+61354,
+56339,
+58690,
+64686,
+9793,
+18918,
+12250,
+45932,
+64505,
+56448,
+52608,
+59693,
+62790,
+40359,
+45911,
+63541,
+28811,
+20839,
+51679,
+55029,
+7960,
+64262,
+42955,
+45663,
+7764,
+45898,
+43976,
+54373,
+48301,
+14002,
+25686,
+17807,
+5893,
+52673,
+21507,
+36813,
+65376,
+52021,
+43896,
+10198,
+19060,
+7744,
+31716,
+28578,
+63653,
+13714,
+15641,
+47283,
+16291,
+19893,
+12101,
+353,
+20921,
+20672,
+15519,
+25139,
+34550,
+31547,
+52785,
+7099,
+3444,
+64739,
+38320,
+22846,
+28428,
+37893,
+45329,
+40675,
+11629,
+35125,
+17459,
+15064,
+64768,
+43607,
+21235,
+55505,
+57822,
+48130,
+12204,
+7830,
+41628,
+15371,
+30463,
+4409,
+18176,
+11690,
+52314,
+60963,
+21068,
+47002,
+2699,
+18764,
+33819,
+36961,
+62217,
+39572,
+3189,
+9924,
+12699,
+56626,
+10203,
+21639,
+55341,
+12292,
+14470,
+37518,
+60268,
+6806,
+46754,
+14583,
+46453,
+14954,
+46050,
+42819,
+16122,
+65099,
+40343,
+52505,
+39632,
+38252,
+63172,
+37122,
+24932,
+11188,
+45881,
+20414,
+63782,
+10052,
+64006,
+5441,
+45005,
+36608,
+59736,
+24035,
+65159,
+11822,
+15278,
+21229,
+6246,
+61478,
+10870,
+42147,
+25777,
+26978,
+46894,
+21583,
+46837,
+35123,
+11631,
+31468,
+64605,
+24287,
+17273,
+18618,
+23704,
+10916,
+22471,
+40530,
+17925,
+40235,
+40508,
+45603,
+17259,
+35211,
+31234,
+64726,
+46686,
+22520,
+47132,
+37730,
+25397,
+35995,
+58898,
+52555,
+51124,
+42269,
+47294,
+15726,
+22312,
+17624,
+64508,
+57314,
+32084,
+38163,
+56507,
+16652,
+48285,
+56956,
+20678,
+29605,
+3669,
+58270,
+41248,
+48266,
+42416,
+361,
+30597,
+44417,
+49274,
+47713,
+19850,
+51837,
+57008,
+18806,
+55394,
+28440,
+38725,
+29131,
+27854,
+9231,
+39194,
+27102,
+35566,
+22518,
+46688,
+28816,
+43919,
+4109,
+44378,
+12041,
+22429,
+17161,
+62357,
+14535,
+63703,
+45194,
+21590,
+47095,
+9632,
+28548,
+32587,
+52830,
+17471,
+965,
+31684,
+33329,
+12198,
+23102,
+49119,
+26516,
+6160,
+55241,
+8926,
+19632,
+26589,
+10656,
+11999,
+51737,
+37735,
+45491,
+44489,
+11790,
+35896,
+28407,
+760,
+7924,
+46708,
+43299,
+64589,
+19932,
+48280,
+28193,
+46763,
+62232,
+7122,
+2430,
+46532,
+42429,
+53016,
+30817,
+65370,
+63564,
+62979,
+1180,
+50111,
+5108,
+49824,
+58860,
+5842,
+2749,
+35105,
+14314,
+38609,
+2688,
+31100,
+40856,
+11385,
+9953,
+46214,
+3905,
+60689,
+46676,
+14203,
+1519,
+35917,
+23442,
+38664,
+41186,
+17299,
+49525,
+16067,
+36681,
+61477,
+6411,
+21230,
+43254,
+49717,
+40958,
+55456,
+28914,
+38131,
+54327,
+13513,
+58317,
+6601,
+43828,
+25708,
+8760,
+60541,
+4029,
+21516,
+50184,
+39920,
+43758,
+18403,
+27809,
+13912,
+40563,
+26790,
+31325,
+50636,
+56420,
+40311,
+57619,
+31772,
+791,
+49071,
+40987,
+28131,
+22405,
+44680,
+62845,
+44082,
+19188,
+4943,
+53674,
+2990,
+33635,
+6838,
+30369,
+15890,
+25113,
+59382,
+56028,
+57370,
+4391,
+41081,
+53524,
+52220,
+41535,
+52311,
+6577,
+38568,
+1515,
+59529,
+16628,
+60852,
+48734,
+30632,
+29831,
+17543,
+57443,
+50662,
+9179,
+20940,
+31707,
+37976,
+8293,
+44148,
+20001,
+18995,
+45062,
+42827,
+4581,
+7739,
+3498,
+17982,
+29280,
+55240,
+6309,
+26517,
+35493,
+12060,
+40423,
+59340,
+9510,
+34723,
+40151,
+60199,
+48199,
+58023,
+5767,
+36644,
+12892,
+63276,
+43702,
+54303,
+56734,
+21184,
+15509,
+34578,
+14788,
+8084,
+51620,
+46394,
+203,
+24738,
+55715,
+54901,
+11044,
+3935,
+13871,
+64938,
+34523,
+13086,
+14779,
+23205,
+53301,
+26339,
+64177,
+16976,
+53293,
+45052,
+23328,
+56245,
+40765,
+32538,
+14422,
+61762,
+32318,
+1459,
+37393,
+38483,
+23817,
+20536,
+60480,
+29853,
+12942,
+4705,
+42685,
+12626,
+22838,
+5697,
+3279,
+16645,
+35242,
+49263,
+29014,
+44900,
+17681,
+39404,
+41951,
+58291,
+8884,
+18611,
+39738,
+27958,
+30872,
+46418,
+8979,
+30456,
+56210,
+33669,
+1564,
+15621,
+13908,
+6885,
+3551,
+18913,
+32423,
+7991,
+30252,
+54778,
+53080,
+40446,
+14882,
+51874,
+49396,
+63861,
+34315,
+35067,
+39346,
+55496,
+58795,
+29148,
+22073,
+12343,
+36916,
+704,
+39788,
+21257,
+61040,
+14913,
+60735,
+36189,
+42907,
+23176,
+62207,
+35063,
+20757,
+53708,
+20829,
+50697,
+56992,
+50312,
+56924,
+14361,
+52510,
+32850,
+12314,
+2598,
+33236,
+3708,
+63612,
+5417,
+33084,
+44210,
+44435,
+44856,
+23221,
+13556,
+47492,
+25509,
+34871,
+47483,
+37389,
+39041,
+5287,
+45225,
+10065,
+64021,
+59352,
+48910,
+26636,
+3416,
+2456,
+14138,
+1081,
+58209,
+37935,
+21314,
+33140,
+37036,
+61001,
+37748,
+18489,
+42616,
+57323,
+50233,
+5941,
+8240,
+63359,
+41575,
+9120,
+34088,
+4567,
+7361,
+16817,
+13675,
+35870,
+9887,
+59606,
+4057,
+54669,
+19173,
+1481,
+21714,
+58349,
+19570,
+30551,
+14504,
+37787,
+1966,
+22091,
+15964,
+978,
+31598,
+62624,
+59754,
+22795,
+38229,
+65526,
+45877,
+24967,
+43009,
+22862,
+35196,
+7058,
+28457,
+17326,
+47481,
+34873,
+42053,
+56532,
+6923,
+8567,
+8392,
+8239,
+5990,
+50234,
+26782,
+10474,
+30489,
+17676,
+7542,
+61550,
+17939,
+55649,
+59559,
+61481,
+40600,
+54581,
+7132,
+25580,
+56666,
+34262,
+41814,
+21901,
+31800,
+37562,
+19753,
+46279,
+6845,
+853,
+24021,
+23881,
+4378,
+58010,
+12828,
+63796,
+36423,
+55032,
+23055,
+7690,
+10022,
+52938,
+10525,
+6607,
+29209,
+27470,
+2933,
+1989,
+26117,
+3657,
+12596,
+52672,
+6517,
+17808,
+21816,
+38866,
+1124,
+5710,
+11511,
+62720,
+47641,
+38371,
+62117,
+53136,
+17585,
+62784,
+10646,
+13346,
+17452,
+33605,
+62447,
+16189,
+56766,
+28226,
+41749,
+56862,
+65470,
+994,
+52233,
+35718,
+58419,
+46504,
+56058,
+57275,
+16415,
+39736,
+18613,
+19679,
+2775,
+30810,
+39658,
+4243,
+24477,
+16989,
+20341,
+43949,
+930,
+41252,
+11657,
+13150,
+12928,
+58999,
+2748,
+6271,
+58861,
+9348,
+41136,
+5714,
+16606,
+50680,
+64616,
+28994,
+2528,
+24464,
+29717,
+56128,
+26298,
+16969,
+63580,
+62556,
+19948,
+2892,
+8700,
+60145,
+51655,
+45189,
+20802,
+56424,
+40041,
+62934,
+4711,
+15815,
+46912,
+22744,
+9936,
+52486,
+61656,
+30523,
+10936,
+2314,
+48354,
+16452,
+27884,
+28928,
+59120,
+41503,
+12635,
+26416,
+19513,
+38049,
+27685,
+24172,
+22219,
+51065,
+28021,
+44744,
+20423,
+48881,
+57066,
+37118,
+54752,
+2912,
+59576,
+42743,
+8049,
+64097,
+22545,
+42455,
+40409,
+57742,
+19837,
+63982,
+31673,
+41886,
+20305,
+36494,
+61027,
+36643,
+6148,
+58024,
+51361,
+30756,
+36834,
+55562,
+53888,
+64092,
+31181,
+55851,
+46469,
+59901,
+11422,
+12973,
+19161,
+46498,
+15578,
+11090,
+8185,
+21401,
+5565,
+57496,
+23534,
+44007,
+15109,
+56486,
+19749,
+56323,
+49561,
+49444,
+61950,
+28164,
+23975,
+53776,
+34146,
+13503,
+56846,
+32860,
+44637,
+58592,
+31401,
+30983,
+21155,
+62022,
+18185,
+38312,
+31159,
+1753,
+55487,
+65388,
+4872,
+54725,
+16605,
+5838,
+41137,
+45355,
+11510,
+5888,
+1125,
+62104,
+18925,
+53948,
+53452,
+1666,
+44151,
+28217,
+4012,
+22685,
+21838,
+3278,
+6097,
+22839,
+1473,
+14629,
+19904,
+16177,
+34398,
+20882,
+46771,
+31720,
+22806,
+23248,
+11505,
+42350,
+33397,
+7601,
+17062,
+20688,
+44026,
+5598,
+64625,
+46474,
+43572,
+55569,
+12777,
+20255,
+16052,
+21418,
+52277,
+60091,
+28611,
+41004,
+26585,
+34076,
+17556,
+37792,
+301,
+21919,
+28802,
+18197,
+39540,
+13360,
+34165,
+19369,
+42304,
+41523,
+54763,
+61164,
+60966,
+57751,
+64713,
+16007,
+21687,
+30899,
+32182,
+62180,
+64503,
+45934,
+22512,
+43114,
+19067,
+59781,
+54116,
+62594,
+54333,
+41717,
+18389,
+25528,
+58237,
+22556,
+53960,
+8329,
+40464,
+29191,
+65199,
+27616,
+60780,
+40500,
+1213,
+31042,
+53251,
+10281,
+60203,
+36831,
+22266,
+34999,
+25229,
+22348,
+52474,
+6930,
+48168,
+41022,
+53061,
+58719,
+15931,
+54271,
+46991,
+26601,
+64624,
+5678,
+44027,
+27838,
+35937,
+12852,
+46320,
+31702,
+61786,
+4842,
+54022,
+9535,
+22440,
+17798,
+49672,
+6772,
+7771,
+52493,
+16487,
+55589,
+41338,
+25484,
+13561,
+56052,
+45823,
+7325,
+38960,
+28052,
+46150,
+45217,
+62948,
+45963,
+48726,
+57495,
+5747,
+21402,
+15645,
+4018,
+36251,
+64983,
+20148,
+22675,
+62223,
+58377,
+7645,
+65478,
+44069,
+48173,
+13778,
+35697,
+37227,
+9840,
+10976,
+27829,
+11500,
+7010,
+51479,
+51169,
+6588,
+64981,
+36253,
+46425,
+28975,
+56457,
+17145,
+19789,
+2594,
+2476,
+37360,
+4539,
+54466,
+24197,
+40055,
+31796,
+38824,
+61610,
+63852,
+12260,
+37214,
+49782,
+29117,
+51059,
+19553,
+49628,
+2650,
+36144,
+3388,
+24077,
+47473,
+50586,
+51190,
+56744,
+24801,
+62597,
+38846,
+27446,
+14053,
+41583,
+55327,
+17552,
+28627,
+3135,
+52184,
+35690,
+25857,
+29500,
+39253,
+45358,
+39876,
+32864,
+63161,
+11094,
+52088,
+39100,
+17600,
+17412,
+13411,
+55985,
+56055,
+49667,
+9318,
+53681,
+49766,
+56549,
+36186,
+1736,
+61765,
+53668,
+34763,
+47721,
+58335,
+11614,
+60646,
+25023,
+16765,
+8456,
+6898,
+37448,
+52432,
+42407,
+9040,
+12788,
+33387,
+49405,
+38301,
+23564,
+649,
+2320,
+38798,
+15308,
+3079,
+12619,
+13259,
+59869,
+36927,
+58952,
+18386,
+45004,
+6420,
+64007,
+37763,
+45013,
+31762,
+44909,
+12818,
+53585,
+17105,
+53098,
+40220,
+10778,
+34706,
+38407,
+28246,
+38397,
+25345,
+30944,
+36678,
+58055,
+46484,
+55779,
+54979,
+33083,
+6025,
+63613,
+13531,
+50103,
+58769,
+27961,
+9641,
+24749,
+33029,
+28321,
+15757,
+54105,
+53630,
+47940,
+55627,
+36675,
+13469,
+42150,
+2100,
+35998,
+42137,
+56270,
+1069,
+32647,
+56836,
+59715,
+13883,
+44660,
+10860,
+7067,
+54235,
+10428,
+20726,
+62433,
+27162,
+10728,
+55282,
+5031,
+24916,
+48318,
+16174,
+3133,
+28629,
+19305,
+4818,
+37441,
+52188,
+58129,
+30907,
+27653,
+43325,
+41440,
+57267,
+10764,
+54050,
+25215,
+2665,
+63006,
+4586,
+31105,
+34573,
+44503,
+26533,
+49318,
+47503,
+50463,
+64251,
+32744,
+51594,
+53932,
+51074,
+26688,
+1079,
+14140,
+64384,
+7226,
+3583,
+37469,
+25385,
+59386,
+17339,
+65058,
+55255,
+59922,
+28707,
+64271,
+9171,
+35449,
+42564,
+12608,
+47944,
+28191,
+48282,
+21382,
+11758,
+30281,
+739,
+38349,
+34475,
+53275,
+28325,
+28318,
+43194,
+28082,
+41508,
+20542,
+37180,
+45086,
+25405,
+60660,
+61116,
+17008,
+58903,
+18596,
+48989,
+50840,
+6775,
+35526,
+39107,
+57583,
+9799,
+24564,
+26648,
+63123,
+33109,
+4223,
+33778,
+49097,
+51473,
+45224,
+6012,
+39042,
+61172,
+34046,
+63180,
+33910,
+31149,
+40129,
+54699,
+50649,
+32961,
+8807,
+40664,
+51433,
+8203,
+9659,
+9723,
+28018,
+59094,
+58732,
+25844,
+31972,
+63396,
+49215,
+13212,
+40341,
+65101,
+14615,
+45598,
+54556,
+49106,
+54078,
+10451,
+52477,
+58226,
+19429,
+60320,
+15267,
+47308,
+43171,
+25955,
+862,
+48831,
+7561,
+65512,
+19116,
+1338,
+46939,
+16550,
+50210,
+11876,
+61394,
+24700,
+20773,
+32637,
+5023,
+22138,
+1416,
+43754,
+8236,
+62252,
+39415,
+42621,
+52402,
+63567,
+61910,
+41662,
+9705,
+22101,
+46107,
+60286,
+50271,
+16689,
+10772,
+4375,
+43268,
+20588,
+10101,
+38751,
+45516,
+18225,
+62629,
+33908,
+63182,
+44741,
+63937,
+56603,
+8482,
+44725,
+49386,
+7415,
+35783,
+164,
+38926,
+33267,
+25258,
+26235,
+34006,
+9182,
+35535,
+34291,
+9895,
+61783,
+27522,
+60378,
+65322,
+33382,
+42928,
+38773,
+29984,
+33623,
+42556,
+26886,
+65499,
+19485,
+22720,
+51038,
+38194,
+28781,
+25984,
+1484,
+4326,
+43243,
+61387,
+40391,
+61360,
+4254,
+38014,
+60354,
+216,
+43793,
+50856,
+1818,
+34625,
+14308,
+34613,
+21095,
+62012,
+6975,
+52921,
+20844,
+50802,
+24941,
+47815,
+57853,
+46712,
+54867,
+40967,
+21106,
+4171,
+4478,
+55954,
+35136,
+24420,
+39861,
+32296,
+43785,
+61615,
+32711,
+44079,
+55477,
+34584,
+31624,
+25849,
+46962,
+28239,
+37766,
+39335,
+55090,
+986,
+333,
+29630,
+30853,
+32179,
+47887,
+1141,
+29441,
+1321,
+49823,
+6274,
+50112,
+64446,
+54535,
+14394,
+9234,
+43276,
+47717,
+63905,
+13020,
+52643,
+2263,
+22030,
+48918,
+53482,
+63684,
+21481,
+15319,
+57979,
+15750,
+20622,
+42257,
+7332,
+2423,
+53020,
+42200,
+25521,
+26545,
+43257,
+29291,
+41493,
+26695,
+36474,
+47432,
+7556,
+49789,
+27923,
+8659,
+60793,
+27125,
+58360,
+57492,
+38383,
+1915,
+64204,
+15106,
+62857,
+3328,
+30476,
+65252,
+45000,
+59433,
+35685,
+63696,
+47266,
+15953,
+39605,
+11963,
+41178,
+32113,
+33145,
+4428,
+46198,
+39595,
+16852,
+26810,
+36408,
+49156,
+7711,
+2185,
+9145,
+22236,
+950,
+61541,
+690,
+62503,
+24915,
+5380,
+55283,
+20890,
+41671,
+13037,
+12844,
+54099,
+22137,
+5232,
+32638,
+52380,
+13486,
+22825,
+56669,
+48738,
+52589,
+43883,
+22338,
+57523,
+38875,
+11810,
+58774,
+42287,
+54034,
+57749,
+60968,
+58178,
+28512,
+56983,
+24881,
+58408,
+45522,
+12035,
+23747,
+56776,
+59306,
+61509,
+25682,
+57084,
+3374,
+1904,
+49816,
+28046,
+57404,
+23300,
+13946,
+7073,
+54189,
+25160,
+32216,
+39038,
+39388,
+17072,
+11467,
+26889,
+22772,
+62876,
+54377,
+65168,
+39715,
+18141,
+14281,
+21683,
+11622,
+46880,
+47998,
+39309,
+22563,
+32511,
+12904,
+24545,
+21577,
+34361,
+38003,
+48495,
+38944,
+54731,
+51448,
+9077,
+2766,
+39579,
+34066,
+24631,
+58027,
+3047,
+6559,
+8323,
+53673,
+6205,
+19189,
+7388,
+26175,
+37499,
+19178,
+29337,
+8617,
+52496,
+58537,
+58974,
+26058,
+42823,
+14131,
+38200,
+57039,
+13275,
+39261,
+37233,
+8638,
+9332,
+27043,
+52086,
+11096,
+33428,
+10221,
+47490,
+13558,
+41709,
+39837,
+2161,
+3002,
+53273,
+34477,
+24805,
+25224,
+7920,
+25261,
+9727,
+13805,
+37507,
+53812,
+4132,
+49126,
+49631,
+55863,
+37834,
+63837,
+58749,
+65229,
+17506,
+53922,
+60070,
+40515,
+6984,
+7629,
+48385,
+63914,
+3193,
+25670,
+8630,
+52341,
+32171,
+36972,
+54697,
+40131,
+60229,
+4422,
+26524,
+24889,
+54724,
+5717,
+65389,
+44584,
+20217,
+39648,
+20737,
+64473,
+46179,
+27608,
+50524,
+56034,
+19320,
+3142,
+58928,
+36330,
+36297,
+56913,
+35745,
+20482,
+44200,
+17789,
+64186,
+28012,
+61631,
+59278,
+18862,
+35311,
+18978,
+36435,
+54021,
+5590,
+61787,
+49735,
+13232,
+33500,
+55741,
+28852,
+47821,
+18506,
+50024,
+9562,
+25721,
+19444,
+16408,
+62890,
+57092,
+48983,
+27478,
+48161,
+36879,
+52802,
+49496,
+55924,
+37440,
+5373,
+19306,
+12642,
+50946,
+45266,
+64720,
+10201,
+56628,
+10941,
+50151,
+61734,
+32080,
+56196,
+43414,
+4270,
+50623,
+59491,
+29349,
+63803,
+62437,
+15312,
+24824,
+47185,
+28675,
+54040,
+22069,
+56579,
+60804,
+51896,
+45776,
+42610,
+18099,
+55664,
+28461,
+12069,
+25157,
+26382,
+9664,
+7582,
+20968,
+43385,
+12951,
+54573,
+39432,
+37738,
+357,
+19955,
+39781,
+61319,
+38341,
+44878,
+42509,
+3644,
+26036,
+37377,
+39036,
+32218,
+3579,
+54426,
+28947,
+37920,
+59916,
+25038,
+12308,
+16336,
+51321,
+30024,
+26899,
+27222,
+49170,
+42196,
+49384,
+44727,
+56877,
+60467,
+38413,
+39430,
+54575,
+38064,
+16526,
+38910,
+44978,
+14184,
+35336,
+8957,
+60746,
+13642,
+51809,
+29554,
+15674,
+14208,
+21573,
+57175,
+38809,
+45296,
+57919,
+37876,
+38828,
+1569,
+57732,
+30796,
+36235,
+15745,
+8729,
+30127,
+3240,
+15814,
+5815,
+62935,
+3201,
+43167,
+23419,
+42684,
+6101,
+12943,
+24066,
+4279,
+40931,
+28906,
+18424,
+32629,
+22241,
+38309,
+48898,
+34416,
+20654,
+23871,
+25832,
+53282,
+33069,
+49670,
+17800,
+48908,
+59354,
+16804,
+57672,
+47662,
+43696,
+38552,
+33826,
+33497,
+12219,
+16160,
+39925,
+11680,
+35903,
+11685,
+1375,
+44248,
+8815,
+15233,
+13832,
+9846,
+19921,
+4623,
+63738,
+33134,
+23158,
+24935,
+10958,
+605,
+15466,
+40102,
+34465,
+51948,
+27874,
+22273,
+32846,
+40492,
+21216,
+32839,
+9167,
+53307,
+8790,
+27674,
+3430,
+52167,
+30615,
+2726,
+47129,
+325,
+17893,
+18222,
+17929,
+46798,
+63401,
+55930,
+7458,
+56945,
+32903,
+2903,
+61047,
+38237,
+29729,
+63737,
+4664,
+19922,
+21146,
+8412,
+31527,
+13922,
+10206,
+12849,
+55710,
+31689,
+57851,
+47817,
+29535,
+29186,
+63333,
+14578,
+54057,
+18796,
+64370,
+47046,
+22923,
+46981,
+45695,
+46631,
+52441,
+45454,
+35798,
+62186,
+17158,
+4294,
+41551,
+21251,
+10378,
+41878,
+35789,
+46353,
+31104,
+5359,
+63007,
+41075,
+143,
+7738,
+6166,
+42828,
+229,
+15103,
+45548,
+51344,
+13222,
+59075,
+44470,
+4372,
+59888,
+7787,
+60531,
+7360,
+5984,
+34089,
+25095,
+1278,
+52984,
+64948,
+27531,
+11972,
+29800,
+50454,
+23931,
+50994,
+43135,
+48426,
+47026,
+58871,
+40336,
+37781,
+49675,
+63534,
+51167,
+51481,
+2062,
+25860,
+779,
+34702,
+18329,
+54465,
+5530,
+37361,
+28500,
+59541,
+27790,
+159,
+13518,
+16847,
+3428,
+27676,
+39273,
+43783,
+32298,
+35317,
+26041,
+26322,
+57785,
+29696,
+22469,
+10918,
+11399,
+44112,
+38028,
+43493,
+52807,
+41741,
+7319,
+24649,
+29042,
+59768,
+28895,
+2108,
+55998,
+13599,
+64926,
+31910,
+46966,
+51693,
+17196,
+50580,
+33475,
+44279,
+59489,
+50625,
+26069,
+54585,
+1940,
+26101,
+62633,
+60910,
+27301,
+27935,
+50376,
+20263,
+25981,
+49942,
+49076,
+7355,
+31050,
+16867,
+55953,
+5137,
+4172,
+28537,
+43540,
+41997,
+1304,
+38821,
+13339,
+52239,
+31456,
+62090,
+55135,
+53900,
+37326,
+53102,
+12614,
+44695,
+2806,
+8126,
+43902,
+55114,
+55244,
+40137,
+45133,
+43657,
+53086,
+23568,
+49046,
+18748,
+52353,
+32076,
+45730,
+50929,
+58799,
+58709,
+23837,
+12172,
+7113,
+28237,
+46964,
+31912,
+22983,
+14375,
+62552,
+55501,
+25517,
+64842,
+7839,
+47089,
+46197,
+5047,
+33146,
+23203,
+14781,
+22058,
+26523,
+4876,
+60230,
+48530,
+23744,
+9835,
+37880,
+29459,
+16670,
+20742,
+7470,
+36081,
+34960,
+18175,
+6466,
+30464,
+22961,
+25480,
+33743,
+15789,
+387,
+22466,
+23171,
+21940,
+39644,
+18314,
+28692,
+15937,
+9290,
+24774,
+37162,
+41080,
+6194,
+57371,
+22652,
+22000,
+27144,
+33157,
+9087,
+12931,
+34974,
+14941,
+18041,
+12464,
+58009,
+5913,
+23882,
+43267,
+5213,
+10773,
+59887,
+4572,
+44471,
+35288,
+32776,
+3856,
+34817,
+26494,
+60252,
+31492,
+23508,
+18375,
+33843,
+19135,
+52171,
+28648,
+53654,
+62543,
+47298,
+44750,
+21936,
+30856,
+241,
+11646,
+64452,
+48791,
+50732,
+38529,
+65356,
+46241,
+29592,
+32527,
+43329,
+15297,
+725,
+47574,
+136,
+20571,
+63617,
+57205,
+21886,
+33298,
+64735,
+56047,
+55848,
+14552,
+43242,
+5166,
+1485,
+34269,
+41008,
+35225,
+17882,
+19919,
+9848,
+31615,
+14331,
+50489,
+9766,
+18306,
+40085,
+55825,
+6689,
+27264,
+39453,
+58676,
+30448,
+36817,
+31406,
+22673,
+20150,
+37552,
+31999,
+19402,
+18004,
+50243,
+44398,
+56277,
+41550,
+4594,
+17159,
+22431,
+34502,
+24183,
+31569,
+45989,
+51121,
+29221,
+42947,
+16782,
+13848,
+30136,
+39114,
+40930,
+4702,
+24067,
+55531,
+17733,
+8401,
+9369,
+29678,
+30452,
+50622,
+4804,
+43415,
+19916,
+19721,
+64139,
+27398,
+27927,
+34621,
+938,
+47880,
+27584,
+17019,
+58657,
+18274,
+31552,
+38013,
+5161,
+61361,
+21224,
+36430,
+11149,
+55681,
+23589,
+32375,
+31225,
+51071,
+24476,
+5854,
+39659,
+10400,
+11639,
+55364,
+61872,
+12003,
+33310,
+56840,
+8374,
+19353,
+44987,
+28625,
+17554,
+34078,
+10928,
+14171,
+58791,
+20522,
+33777,
+5292,
+33110,
+8229,
+50830,
+25755,
+60767,
+27197,
+2254,
+54000,
+14448,
+20176,
+13708,
+21026,
+47526,
+2974,
+34653,
+23741,
+9240,
+3008,
+29448,
+12755,
+50767,
+18736,
+35468,
+26939,
+53844,
+58963,
+11886,
+828,
+64115,
+27212,
+9521,
+34559,
+10683,
+3614,
+30875,
+8468,
+32518,
+52273,
+1770,
+58988,
+9199,
+56085,
+36892,
+15557,
+25145,
+60375,
+61284,
+44314,
+3784,
+28536,
+4477,
+5138,
+21107,
+57394,
+61779,
+12299,
+56869,
+15389,
+42385,
+25456,
+41807,
+13120,
+43371,
+8005,
+53038,
+24736,
+205,
+42950,
+58043,
+42876,
+9739,
+22293,
+60373,
+25147,
+28076,
+22757,
+10417,
+52145,
+53660,
+60301,
+40838,
+53414,
+839,
+46267,
+586,
+44325,
+14974,
+54807,
+10257,
+49125,
+4901,
+53813,
+20518,
+26364,
+14749,
+10989,
+45698,
+1557,
+35041,
+36798,
+25741,
+54532,
+11978,
+16786,
+10456,
+7434,
+14792,
+36121,
+685,
+23216,
+2056,
+1371,
+44377,
+6332,
+43920,
+53553,
+7007,
+7791,
+27866,
+49005,
+19316,
+37331,
+39530,
+46015,
+54966,
+34952,
+16,
+34697,
+31776,
+42800,
+22182,
+63919,
+4054,
+24939,
+50804,
+46102,
+38671,
+51467,
+19016,
+56757,
+2039,
+61458,
+63497,
+61456,
+2041,
+38033,
+28379,
+48618,
+27106,
+14031,
+23321,
+7518,
+30804,
+11049,
+19413,
+9443,
+18939,
+8835,
+37687,
+8920,
+41126,
+28855,
+59290,
+59127,
+54668,
+5977,
+59607,
+24938,
+4090,
+63920,
+18778,
+28360,
+56200,
+25153,
+38095,
+53181,
+44755,
+11471,
+14083,
+43151,
+12274,
+53114,
+18704,
+24047,
+17707,
+41056,
+341,
+6708,
+1056,
+43713,
+26023,
+39914,
+21515,
+6230,
+60542,
+39856,
+15025,
+62655,
+19951,
+14352,
+54310,
+54405,
+18471,
+36250,
+5562,
+15646,
+49626,
+19555,
+1747,
+22684,
+5701,
+28218,
+38456,
+39708,
+40699,
+55511,
+63263,
+28113,
+39954,
+40537,
+21207,
+12711,
+24134,
+63849,
+55554,
+64586,
+60502,
+64848,
+42581,
+50355,
+62270,
+23860,
+51132,
+35504,
+57994,
+59603,
+47599,
+18551,
+49235,
+20469,
+32571,
+15343,
+13058,
+7313,
+2949,
+54941,
+1467,
+47368,
+24094,
+7489,
+32752,
+42283,
+31745,
+49322,
+10587,
+64331,
+38453,
+45504,
+19261,
+23549,
+12588,
+52466,
+9814,
+53105,
+12965,
+55434,
+26788,
+40565,
+56570,
+41000,
+49103,
+34618,
+19462,
+10389,
+83,
+3902,
+62398,
+63178,
+34048,
+47420,
+6567,
+45831,
+30157,
+47751,
+1814,
+45245,
+13870,
+6129,
+11045,
+31927,
+35752,
+29009,
+18894,
+63502,
+26712,
+60013,
+1686,
+9985,
+28543,
+21751,
+60407,
+32827,
+28639,
+64149,
+26178,
+62609,
+57052,
+12940,
+29855,
+25642,
+1491,
+20075,
+54860,
+35845,
+53144,
+37524,
+60688,
+6260,
+46215,
+62397,
+3947,
+84,
+33155,
+27146,
+50254,
+25865,
+55730,
+14016,
+55523,
+54241,
+8571,
+47380,
+31387,
+16498,
+36696,
+45102,
+39447,
+13546,
+28297,
+48254,
+13050,
+59728,
+35015,
+52139,
+27293,
+19203,
+56491,
+35483,
+14904,
+38158,
+13016,
+43934,
+6994,
+9164,
+43392,
+2271,
+51721,
+390,
+24305,
+48419,
+560,
+26870,
+1579,
+48656,
+20062,
+34816,
+4368,
+32777,
+44842,
+63717,
+30914,
+64601,
+62524,
+64907,
+62829,
+30869,
+58772,
+11812,
+21499,
+56311,
+55251,
+65264,
+30601,
+21766,
+36935,
+17661,
+52228,
+17055,
+23367,
+17953,
+64870,
+30211,
+35301,
+49448,
+19192,
+21800,
+41573,
+63361,
+42246,
+65417,
+51219,
+22486,
+49290,
+50487,
+14333,
+58154,
+27496,
+31231,
+13970,
+27002,
+32545,
+18263,
+57676,
+56091,
+35264,
+48632,
+34456,
+14692,
+15385,
+60729,
+33458,
+34122,
+1259,
+30845,
+41914,
+3383,
+54712,
+18789,
+51597,
+62496,
+47580,
+9560,
+50026,
+30961,
+44170,
+63443,
+502,
+28535,
+4174,
+44315,
+51934,
+65485,
+32127,
+55357,
+10148,
+44958,
+51114,
+59426,
+33220,
+63800,
+60064,
+11870,
+18479,
+2104,
+29516,
+34256,
+25912,
+39827,
+56779,
+39883,
+48273,
+22897,
+3364,
+8142,
+61369,
+53239,
+28354,
+55641,
+60352,
+38016,
+33834,
+59946,
+60696,
+15803,
+41424,
+57229,
+34588,
+65502,
+48190,
+7599,
+33399,
+54693,
+33522,
+62715,
+6715,
+16678,
+55310,
+8826,
+37718,
+62096,
+8517,
+41279,
+21137,
+9262,
+47553,
+36259,
+15496,
+36472,
+26697,
+30084,
+44425,
+51711,
+50615,
+32372,
+53110,
+8234,
+43756,
+39922,
+3688,
+2577,
+36176,
+33678,
+41389,
+63611,
+6027,
+33237,
+30222,
+34900,
+54797,
+13964,
+33335,
+42499,
+20777,
+34744,
+16464,
+24552,
+10515,
+27279,
+9882,
+33363,
+36869,
+10547,
+58486,
+2576,
+3714,
+39923,
+16162,
+32066,
+47694,
+12207,
+30241,
+33632,
+9136,
+8515,
+62098,
+32617,
+54216,
+21810,
+38441,
+15220,
+19579,
+63841,
+58269,
+6359,
+29606,
+22358,
+10172,
+61299,
+53535,
+12381,
+26111,
+48835,
+49296,
+17947,
+12595,
+5896,
+26118,
+26681,
+51314,
+1812,
+47753,
+52864,
+54279,
+62825,
+14965,
+14430,
+54734,
+26035,
+4766,
+42510,
+10347,
+65519,
+30842,
+1873,
+17312,
+28615,
+35346,
+22050,
+58197,
+48580,
+51697,
+2563,
+22622,
+27739,
+1969,
+60315,
+23876,
+40190,
+34595,
+53363,
+58426,
+40162,
+16167,
+35838,
+42369,
+7755,
+46416,
+30874,
+4189,
+10684,
+16856,
+22251,
+53065,
+35447,
+9173,
+15553,
+16775,
+32414,
+48389,
+27539,
+50672,
+54599,
+63225,
+25694,
+30437,
+19222,
+16202,
+48182,
+53593,
+38578,
+43369,
+13122,
+31089,
+11285,
+16092,
+59800,
+37374,
+31955,
+37468,
+5341,
+7227,
+34864,
+54425,
+4761,
+32219,
+20612,
+16146,
+42127,
+36766,
+49836,
+13889,
+62674,
+7031,
+54960,
+42926,
+33384,
+11854,
+15773,
+40032,
+36015,
+31039,
+12570,
+23435,
+53807,
+58777,
+56472,
+24346,
+50752,
+46525,
+31837,
+18912,
+6072,
+6886,
+40035,
+12653,
+63948,
+18944,
+32408,
+42477,
+38549,
+62262,
+18397,
+2092,
+65037,
+26137,
+42839,
+27894,
+21376,
+42931,
+21722,
+53606,
+57556,
+39972,
+11904,
+39103,
+25531,
+44441,
+60387,
+58563,
+55566,
+29233,
+41526,
+27425,
+31828,
+16673,
+29405,
+44381,
+59813,
+47353,
+44251,
+22561,
+39311,
+29557,
+3063,
+27743,
+65238,
+2860,
+11088,
+15580,
+41571,
+21802,
+64002,
+55669,
+17981,
+6164,
+7740,
+44110,
+11401,
+2007,
+33716,
+9778,
+14812,
+37050,
+51543,
+40303,
+58489,
+94,
+50361,
+26354,
+15053,
+62582,
+21011,
+47326,
+11039,
+24892,
+24296,
+16158,
+12221,
+30882,
+41798,
+2737,
+47562,
+50862,
+12091,
+63137,
+34914,
+34319,
+11216,
+20906,
+11847,
+17132,
+47529,
+51009,
+63756,
+18668,
+26484,
+41283,
+64930,
+50073,
+62728,
+44976,
+38912,
+40329,
+10926,
+34080,
+61069,
+10403,
+64738,
+6489,
+7100,
+27818,
+61804,
+57021,
+61859,
+32921,
+30528,
+19169,
+57104,
+64411,
+59829,
+46613,
+52166,
+4643,
+27675,
+4531,
+16848,
+31659,
+40145,
+18394,
+1704,
+27023,
+27165,
+59654,
+12402,
+50447,
+2455,
+6005,
+26637,
+56649,
+35428,
+1479,
+19175,
+13511,
+54329,
+34480,
+14754,
+41966,
+7083,
+31132,
+12884,
+13851,
+19347,
+63089,
+617,
+36163,
+19582,
+14686,
+58742,
+55747,
+35096,
+30933,
+52820,
+36403,
+24076,
+5513,
+36145,
+11168,
+10029,
+54711,
+3797,
+41915,
+46502,
+58421,
+39489,
+20701,
+40114,
+30769,
+1903,
+4992,
+57085,
+1963,
+53425,
+30007,
+56647,
+26639,
+15630,
+15401,
+8141,
+3760,
+22898,
+40215,
+26207,
+44335,
+63742,
+33354,
+59895,
+1035,
+18030,
+8070,
+27325,
+29738,
+60941,
+48380,
+8493,
+48957,
+47366,
+1469,
+54526,
+46115,
+10155,
+53516,
+27737,
+22624,
+31424,
+53855,
+29636,
+42768,
+12341,
+22075,
+56433,
+2852,
+42360,
+59495,
+30475,
+5061,
+62858,
+42069,
+48251,
+20919,
+355,
+37740,
+28575,
+59082,
+25966,
+7565,
+54927,
+48797,
+42861,
+59980,
+7161,
+17433,
+42220,
+26195,
+48108,
+19356,
+37931,
+18773,
+59251,
+58416,
+21052,
+46384,
+22117,
+51099,
+46009,
+37339,
+43095,
+47741,
+2347,
+11082,
+57218,
+28767,
+51179,
+13207,
+11597,
+39087,
+57143,
+57300,
+60310,
+26123,
+29305,
+13998,
+13423,
+16644,
+6096,
+5698,
+21839,
+26987,
+61995,
+47141,
+63835,
+37836,
+26327,
+47359,
+53849,
+15181,
+61545,
+41678,
+44521,
+62987,
+25789,
+26259,
+43209,
+28004,
+35614,
+39809,
+8089,
+38505,
+20396,
+15488,
+57156,
+27428,
+29029,
+39786,
+706,
+41344,
+50278,
+63094,
+14198,
+63941,
+45692,
+10249,
+15813,
+4713,
+30128,
+21677,
+45473,
+30723,
+15660,
+30742,
+8818,
+23380,
+56389,
+28467,
+37528,
+8385,
+13607,
+19848,
+47715,
+43278,
+24584,
+9499,
+13612,
+29166,
+29908,
+20101,
+64661,
+39473,
+10967,
+8707,
+22376,
+40828,
+20104,
+10882,
+7679,
+48070,
+23644,
+42458,
+57419,
+18757,
+53956,
+43166,
+4709,
+62936,
+48574,
+37236,
+21832,
+15523,
+14933,
+25669,
+4885,
+63915,
+45040,
+9923,
+6453,
+39573,
+19001,
+31452,
+35248,
+49091,
+10789,
+33304,
+48375,
+59210,
+51003,
+30995,
+722,
+23297,
+29889,
+38137,
+37612,
+56880,
+58262,
+18462,
+54204,
+45036,
+57632,
+8218,
+24082,
+34322,
+29070,
+24851,
+28653,
+57096,
+40123,
+1930,
+1121,
+46204,
+53032,
+44945,
+39264,
+21312,
+37937,
+25445,
+23580,
+56814,
+15848,
+21061,
+39864,
+56386,
+58927,
+4860,
+19321,
+61634,
+61591,
+2360,
+10216,
+52183,
+5498,
+28628,
+5376,
+16175,
+19906,
+7810,
+27815,
+3038,
+46884,
+16835,
+33513,
+7825,
+12822,
+20971,
+16379,
+57828,
+43117,
+51097,
+22119,
+17520,
+8058,
+34831,
+21979,
+55618,
+15589,
+62650,
+47303,
+49701,
+1582,
+16437,
+44810,
+60153,
+38376,
+41811,
+1733,
+60738,
+50226,
+46519,
+8782,
+41511,
+17533,
+13154,
+3030,
+33256,
+31931,
+59662,
+49349,
+18358,
+63414,
+16586,
+40002,
+24587,
+9162,
+6996,
+58560,
+12618,
+5449,
+15309,
+19020,
+14765,
+30861,
+11534,
+25784,
+21867,
+47931,
+31018,
+29576,
+37703,
+406,
+45893,
+22088,
+27742,
+3509,
+29558,
+41065,
+62817,
+41049,
+46656,
+15993,
+28844,
+64636,
+9249,
+47634,
+55423,
+25884,
+19833,
+45676,
+6558,
+4947,
+58028,
+35362,
+18365,
+17768,
+13939,
+61744,
+17958,
+46883,
+3128,
+27816,
+7102,
+64846,
+60504,
+40623,
+29764,
+33255,
+3093,
+13155,
+52053,
+44146,
+8295,
+14039,
+56543,
+55933,
+20277,
+58877,
+41514,
+41658,
+48447,
+48311,
+39483,
+24628,
+935,
+61687,
+631,
+23333,
+46442,
+29447,
+4205,
+9241,
+10574,
+20130,
+53167,
+53272,
+4912,
+2162,
+10333,
+743,
+56465,
+36978,
+49109,
+11882,
+61181,
+18240,
+9134,
+33634,
+6203,
+53675,
+43801,
+2962,
+51161,
+61102,
+10037,
+44182,
+1956,
+59711,
+45590,
+65515,
+49934,
+45724,
+14116,
+34652,
+4209,
+47527,
+17134,
+16478,
+34931,
+20948,
+35352,
+57563,
+45761,
+57425,
+43625,
+51160,
+2987,
+43802,
+9587,
+27021,
+1706,
+47762,
+50483,
+62055,
+44527,
+15380,
+40339,
+13214,
+54940,
+3978,
+7314,
+39807,
+35616,
+12195,
+11420,
+59903,
+11205,
+40735,
+60031,
+26692,
+1673,
+32378,
+56452,
+48187,
+1988,
+5899,
+27471,
+49013,
+13935,
+39358,
+61620,
+8486,
+33978,
+22361,
+16967,
+26300,
+52461,
+12542,
+18696,
+11428,
+35823,
+57988,
+48928,
+46719,
+33938,
+59575,
+5784,
+54753,
+24841,
+47311,
+60331,
+31630,
+45488,
+39435,
+61046,
+4628,
+32904,
+57856,
+17469,
+52832,
+10561,
+48344,
+36593,
+23951,
+32225,
+8699,
+5824,
+19949,
+62657,
+19373,
+49947,
+62427,
+18235,
+7451,
+7749,
+50511,
+61512,
+45587,
+62647,
+46165,
+54265,
+25946,
+27459,
+22180,
+42802,
+49455,
+22477,
+47435,
+47109,
+33351,
+56328,
+42015,
+27006,
+183,
+51647,
+61966,
+58507,
+11087,
+3506,
+65239,
+43913,
+11676,
+25173,
+11776,
+58671,
+42359,
+3332,
+56434,
+45370,
+6556,
+45678,
+29258,
+32354,
+41764,
+63440,
+46496,
+19163,
+15202,
+41376,
+11196,
+27411,
+45601,
+40510,
+42836,
+54645,
+16471,
+2615,
+10047,
+43838,
+36394,
+26720,
+38738,
+17509,
+56791,
+49965,
+32014,
+61798,
+58823,
+26331,
+20691,
+54025,
+59651,
+17897,
+51056,
+45112,
+16658,
+58382,
+55232,
+15463,
+62084,
+20562,
+8125,
+4461,
+44696,
+13860,
+16507,
+51901,
+43005,
+53202,
+11356,
+55676,
+25704,
+63404,
+9684,
+19070,
+35596,
+13544,
+39449,
+45641,
+50968,
+46567,
+14650,
+56104,
+24897,
+26389,
+49453,
+42804,
+8036,
+64429,
+45350,
+51217,
+65419,
+30809,
+5857,
+19680,
+55658,
+14020,
+36801,
+64425,
+27313,
+47377,
+39578,
+4952,
+9078,
+48394,
+63592,
+57683,
+24520,
+34027,
+36581,
+33719,
+6751,
+48154,
+35635,
+47951,
+59657,
+54892,
+17629,
+35104,
+6270,
+5843,
+59000,
+25179,
+191,
+22188,
+12663,
+34824,
+46433,
+33797,
+41825,
+47561,
+3472,
+41799,
+29905,
+23228,
+50046,
+16912,
+9359,
+19277,
+25576,
+33273,
+47128,
+4640,
+30616,
+25238,
+54201,
+52367,
+24471,
+62477,
+24489,
+24281,
+41633,
+40659,
+21556,
+11277,
+11257,
+61888,
+50573,
+46021,
+629,
+61689,
+34669,
+55922,
+49498,
+56642,
+42673,
+30343,
+7813,
+18763,
+6459,
+47003,
+65294,
+53893,
+23783,
+14134,
+20856,
+59329,
+21456,
+10062,
+31099,
+6266,
+38610,
+9345,
+50721,
+55306,
+17343,
+21528,
+50430,
+60475,
+62210,
+23469,
+33099,
+35389,
+65488,
+42327,
+53376,
+28280,
+14452,
+9401,
+53235,
+64579,
+54307,
+63005,
+5361,
+25216,
+56382,
+42764,
+60562,
+11161,
+41013,
+56636,
+56164,
+41084,
+24635,
+55783,
+34016,
+37192,
+36143,
+5515,
+49629,
+49128,
+60709,
+8446,
+32657,
+24911,
+12547,
+2555,
+2283,
+14721,
+35761,
+17614,
+37329,
+19318,
+56036,
+51927,
+63944,
+32893,
+15145,
+29230,
+43575,
+42619,
+39417,
+38858,
+30534,
+20805,
+1724,
+15122,
+38232,
+28341,
+34883,
+57251,
+41546,
+10046,
+2832,
+16472,
+13293,
+39267,
+58283,
+19410,
+26677,
+35005,
+21544,
+20999,
+20202,
+25495,
+59721,
+60827,
+62337,
+16964,
+33235,
+6029,
+12315,
+50516,
+2475,
+5533,
+19790,
+55044,
+65107,
+20319,
+59968,
+19575,
+65461,
+45440,
+10851,
+51968,
+8652,
+57237,
+57183,
+10491,
+432,
+36175,
+3713,
+3689,
+58487,
+40305,
+36067,
+30093,
+27887,
+16397,
+8342,
+46371,
+48840,
+64592,
+57693,
+22621,
+3631,
+51698,
+56478,
+13133,
+1972,
+59260,
+64358,
+2282,
+2642,
+12548,
+63355,
+28788,
+21015,
+62273,
+39749,
+44060,
+41427,
+42962,
+50421,
+41244,
+14664,
+40503,
+51042,
+20977,
+17449,
+7411,
+15112,
+55846,
+56049,
+51183,
+11229,
+62871,
+60177,
+60952,
+24463,
+5833,
+28995,
+13673,
+16819,
+753,
+26859,
+33704,
+7279,
+15422,
+18290,
+1900,
+1380,
+49830,
+45029,
+17805,
+25688,
+44274,
+65276,
+26972,
+47801,
+39641,
+44676,
+12213,
+52011,
+42531,
+56399,
+57024,
+49762,
+25507,
+47494,
+53075,
+51364,
+6748,
+34095,
+49271,
+27892,
+42841,
+64432,
+13239,
+22934,
+45544,
+43217,
+13893,
+28562,
+23244,
+30459,
+45126,
+25808,
+43154,
+10469,
+12351,
+37359,
+5532,
+2595,
+50517,
+13118,
+41809,
+38378,
+52350,
+38356,
+17234,
+51537,
+52116,
+28262,
+48541,
+12985,
+17276,
+54086,
+22192,
+20442,
+27116,
+14137,
+6004,
+3417,
+50448,
+50522,
+27610,
+42087,
+32715,
+46189,
+62137,
+64921,
+42307,
+15493,
+15258,
+62291,
+51340,
+39145,
+8407,
+14725,
+8844,
+49778,
+11434,
+23631,
+13759,
+63409,
+55216,
+46531,
+6284,
+7123,
+48289,
+40964,
+35578,
+56024,
+53019,
+5085,
+7333,
+11180,
+33933,
+1463,
+57704,
+37012,
+19124,
+38888,
+60243,
+32429,
+30785,
+40067,
+46311,
+10746,
+25284,
+24995,
+20402,
+21882,
+57931,
+46855,
+8540,
+53445,
+62069,
+30624,
+28833,
+42543,
+47621,
+38255,
+19683,
+45791,
+24138,
+52764,
+15164,
+21233,
+43609,
+9939,
+39910,
+57101,
+24219,
+7288,
+9217,
+64436,
+43315,
+10303,
+50076,
+32672,
+60048,
+54444,
+18855,
+23917,
+55397,
+13323,
+10591,
+12283,
+28368,
+17914,
+27284,
+59108,
+28416,
+48813,
+25055,
+10215,
+3138,
+61592,
+31490,
+60254,
+31628,
+60333,
+20797,
+19831,
+25886,
+8558,
+57812,
+65258,
+11081,
+3295,
+47742,
+41027,
+33803,
+32279,
+33608,
+29787,
+11567,
+1457,
+32320,
+23360,
+23370,
+22959,
+30466,
+19692,
+9246,
+731,
+49870,
+1210,
+14667,
+38359,
+44546,
+15447,
+141,
+41077,
+55348,
+38797,
+5452,
+650,
+36444,
+28059,
+46323,
+48353,
+5806,
+10937,
+61937,
+39844,
+16824,
+27844,
+25698,
+63378,
+30097,
+58372,
+29156,
+37592,
+28555,
+28425,
+27725,
+25371,
+58835,
+8588,
+42502,
+18219,
+27169,
+10718,
+16613,
+15476,
+20119,
+30206,
+37151,
+52613,
+8744,
+15574,
+14720,
+2641,
+2556,
+64359,
+23944,
+35371,
+36451,
+31315,
+52205,
+10025,
+61531,
+37425,
+51720,
+3867,
+43393,
+9185,
+53545,
+318,
+33694,
+55352,
+22029,
+5097,
+52644,
+59392,
+21836,
+22687,
+21449,
+17945,
+49298,
+53999,
+4216,
+27198,
+63820,
+37849,
+35623,
+23021,
+35713,
+43633,
+52202,
+56283,
+55870,
+40526,
+34347,
+14710,
+33734,
+10274,
+57074,
+18457,
+12105,
+38941,
+58643,
+42263,
+40667,
+23853,
+16252,
+44691,
+14737,
+21777,
+47136,
+37496,
+64152,
+36321,
+51881,
+24344,
+56474,
+63824,
+28750,
+45465,
+64147,
+28641,
+10094,
+46406,
+44116,
+9752,
+31523,
+19670,
+58918,
+41484,
+6625,
+36350,
+60928,
+50960,
+31578,
+44932,
+43880,
+36628,
+31874,
+49548,
+16623,
+49205,
+44000,
+777,
+25862,
+29882,
+24928,
+11826,
+20997,
+21546,
+9144,
+5039,
+7712,
+52988,
+62984,
+36750,
+38270,
+57296,
+59549,
+23963,
+61712,
+23292,
+32045,
+11521,
+26945,
+65054,
+30594,
+29611,
+41142,
+48873,
+1059,
+46557,
+29104,
+10332,
+3001,
+4913,
+39838,
+63230,
+35441,
+36612,
+46113,
+54528,
+51815,
+8097,
+45346,
+9567,
+38804,
+34949,
+47017,
+9188,
+44862,
+48366,
+26441,
+58442,
+33564,
+60272,
+16323,
+48134,
+35466,
+18738,
+60904,
+9608,
+39832,
+62741,
+55854,
+1876,
+55168,
+51777,
+63104,
+10322,
+42448,
+22298,
+47178,
+62450,
+48952,
+13781,
+23278,
+58207,
+1083,
+16748,
+24437,
+35108,
+21856,
+52045,
+2017,
+683,
+36123,
+55997,
+4508,
+28896,
+27416,
+29515,
+3769,
+18480,
+58896,
+35997,
+5399,
+42151,
+23772,
+62693,
+19042,
+21366,
+61750,
+65036,
+3540,
+18398,
+8247,
+50290,
+57031,
+7568,
+8954,
+42214,
+58021,
+48201,
+19233,
+18077,
+21205,
+40539,
+410,
+50549,
+19478,
+55209,
+56152,
+20834,
+41783,
+16139,
+1678,
+1172,
+29948,
+20514,
+9802,
+41036,
+29498,
+25859,
+4545,
+51482,
+23956,
+20912,
+29584,
+1370,
+4112,
+23217,
+22303,
+14014,
+55732,
+38632,
+13523,
+44395,
+32,
+63552,
+44364,
+8108,
+36071,
+11805,
+38032,
+4078,
+61457,
+4082,
+56758,
+54212,
+46642,
+61608,
+38826,
+37878,
+9837,
+16715,
+53043,
+43799,
+53677,
+8180,
+37556,
+46863,
+10653,
+23973,
+28166,
+24548,
+37408,
+35414,
+682,
+2112,
+52046,
+43030,
+55547,
+30050,
+39902,
+43083,
+36942,
+11124,
+33715,
+3494,
+11402,
+11375,
+15861,
+60989,
+14371,
+42414,
+48268,
+45571,
+55467,
+19625,
+59003,
+36727,
+50451,
+29661,
+32933,
+65142,
+26116,
+5898,
+2934,
+48188,
+65504,
+63490,
+21174,
+11369,
+38522,
+25176,
+19628,
+25930,
+53576,
+47603,
+32669,
+38924,
+166,
+59259,
+2559,
+13134,
+60314,
+3628,
+27740,
+22090,
+5967,
+37788,
+53424,
+3372,
+57086,
+55277,
+57278,
+52356,
+48567,
+59710,
+2982,
+44183,
+40751,
+55839,
+26893,
+7894,
+773,
+34971,
+45923,
+45509,
+17142,
+62774,
+7855,
+13357,
+56594,
+26100,
+4493,
+54586,
+47445,
+29808,
+26032,
+13988,
+21247,
+37905,
+48610,
+1120,
+3158,
+40124,
+8736,
+24147,
+61828,
+59511,
+60121,
+52007,
+42998,
+44965,
+61202,
+8333,
+38919,
+46734,
+64203,
+5065,
+38384,
+17609,
+18955,
+36901,
+33043,
+28666,
+54992,
+27940,
+65090,
+49815,
+4991,
+3375,
+30770,
+1379,
+2518,
+18291,
+43887,
+37657,
+29623,
+34515,
+22803,
+1004,
+8348,
+38331,
+64254,
+20296,
+36691,
+903,
+54591,
+36958,
+59032,
+51824,
+48021,
+31009,
+6728,
+57201,
+56708,
+55167,
+2131,
+55855,
+17311,
+3639,
+30843,
+1261,
+1507,
+13430,
+45083,
+9899,
+17034,
+42274,
+29569,
+30992,
+53394,
+23313,
+35693,
+40683,
+21975,
+7376,
+6815,
+17241,
+12054,
+60444,
+44057,
+1133,
+41554,
+32928,
+21030,
+19764,
+8201,
+51435,
+7428,
+32324,
+29851,
+60482,
+18272,
+58659,
+42895,
+11345,
+39158,
+51001,
+59212,
+20344,
+41533,
+52222,
+33055,
+25190,
+52236,
+37091,
+29969,
+43021,
+29930,
+43931,
+9532,
+20694,
+61684,
+34624,
+5155,
+50857,
+37857,
+45244,
+3938,
+47752,
+3653,
+51315,
+32334,
+19201,
+27295,
+8378,
+33712,
+40593,
+18346,
+33550,
+31241,
+35807,
+60913,
+60874,
+32404,
+41649,
+21201,
+62178,
+32184,
+9211,
+9399,
+14454,
+43313,
+64438,
+32292,
+7995,
+27778,
+14062,
+20315,
+23708,
+14527,
+62078,
+46870,
+52017,
+48692,
+27513,
+37824,
+13772,
+25306,
+37888,
+19417,
+58987,
+4184,
+52274,
+55788,
+20325,
+41675,
+14407,
+12669,
+23345,
+51589,
+58466,
+54850,
+33346,
+23225,
+29169,
+45122,
+59114,
+55486,
+5720,
+31160,
+43604,
+45801,
+12328,
+22683,
+4014,
+19556,
+8563,
+15069,
+188,
+52708,
+59627,
+29115,
+49784,
+32316,
+61764,
+5474,
+36187,
+60737,
+3101,
+41812,
+34264,
+36481,
+37336,
+17029,
+34609,
+24415,
+15121,
+2623,
+20806,
+12978,
+55636,
+48505,
+305,
+38338,
+63285,
+27689,
+9286,
+9062,
+63318,
+36788,
+54046,
+53771,
+35357,
+12010,
+47761,
+2958,
+27022,
+3423,
+18395,
+62264,
+27702,
+60641,
+6789,
+29372,
+45957,
+61866,
+7269,
+11026,
+22336,
+43885,
+18293,
+51305,
+41402,
+61538,
+9984,
+3926,
+60014,
+53278,
+30802,
+7520,
+14893,
+23133,
+1171,
+2070,
+16140,
+59476,
+31223,
+32377,
+2938,
+26693,
+41495,
+45248,
+42173,
+19999,
+44150,
+5704,
+53453,
+10122,
+18601,
+41838,
+9943,
+52627,
+15130,
+32057,
+54423,
+34866,
+52390,
+38712,
+37546,
+51295,
+27231,
+36661,
+9141,
+29945,
+22164,
+57534,
+26414,
+12637,
+49063,
+52246,
+27996,
+33288,
+11396,
+53974,
+28745,
+10135,
+29181,
+30362,
+17819,
+60437,
+52518,
+56008,
+14081,
+11473,
+30976,
+54856,
+14035,
+17151,
+60651,
+62345,
+20498,
+9426,
+43354,
+61446,
+61484,
+46760,
+56934,
+16770,
+39696,
+21019,
+18998,
+58332,
+40202,
+13409,
+17414,
+16119,
+21764,
+30603,
+19379,
+37514,
+20795,
+60335,
+8935,
+31046,
+51338,
+62293,
+19846,
+13609,
+62904,
+32645,
+1071,
+52883,
+61990,
+46594,
+10737,
+48223,
+36924,
+24535,
+16436,
+3107,
+49702,
+48655,
+3860,
+26871,
+24353,
+42624,
+50890,
+48091,
+42122,
+47235,
+9720,
+57731,
+4720,
+38829,
+57256,
+32350,
+15620,
+6076,
+33670,
+15650,
+41942,
+50119,
+36657,
+35040,
+4125,
+45699,
+59763,
+33865,
+32874,
+54769,
+18085,
+55939,
+27501,
+17485,
+26308,
+975,
+53787,
+36716,
+33172,
+49174,
+13992,
+25236,
+30618,
+29530,
+23068,
+43734,
+62036,
+9869,
+26249,
+55484,
+59116,
+60097,
+27775,
+57688,
+18498,
+45309,
+39666,
+13116,
+50519,
+36730,
+27878,
+35916,
+6256,
+14204,
+55471,
+59528,
+6186,
+38569,
+16590,
+47549,
+40554,
+17491,
+31357,
+13429,
+1870,
+1262,
+27347,
+8410,
+21148,
+40797,
+9824,
+20666,
+47393,
+32681,
+56825,
+18268,
+12406,
+22065,
+36785,
+20074,
+3912,
+25643,
+34331,
+28232,
+8418,
+34268,
+4325,
+5167,
+25985,
+21713,
+5974,
+19174,
+3412,
+35429,
+23517,
+15141,
+44343,
+14628,
+5695,
+22840,
+22410,
+54525,
+3346,
+47367,
+3976,
+54942,
+27870,
+57703,
+2419,
+33934,
+39385,
+37392,
+6109,
+32319,
+2339,
+11568,
+49682,
+55125,
+25439,
+56555,
+271,
+34938,
+21122,
+7649,
+64450,
+11648,
+59777,
+34357,
+36470,
+15498,
+57487,
+59757,
+42993,
+54918,
+20067,
+44954,
+28681,
+47119,
+54012,
+37757,
+51477,
+7012,
+22289,
+50346,
+51805,
+6664,
+25203,
+25819,
+64708,
+61343,
+38326,
+17215,
+54822,
+15483,
+43753,
+5230,
+22139,
+53440,
+35018,
+34393,
+58059,
+52662,
+42513,
+35794,
+24687,
+51227,
+30926,
+41124,
+8922,
+27720,
+38265,
+40867,
+45815,
+61271,
+38140,
+27088,
+25745,
+27597,
+46671,
+52778,
+20071,
+63321,
+36897,
+61618,
+39360,
+28040,
+63313,
+29217,
+9904,
+31251,
+49829,
+2517,
+1901,
+30771,
+30365,
+44247,
+4671,
+11686,
+50358,
+44376,
+4111,
+2057,
+29585,
+18061,
+46456,
+33824,
+38554,
+1009,
+9280,
+50476,
+39197,
+59417,
+48562,
+15781,
+22554,
+58239,
+26505,
+57841,
+46307,
+47747,
+60686,
+37526,
+28469,
+42249,
+28268,
+53121,
+20124,
+12532,
+18690,
+45075,
+43056,
+55413,
+46938,
+5241,
+19117,
+56560,
+18802,
+63621,
+60473,
+50432,
+19991,
+22322,
+47735,
+16631,
+46029,
+8847,
+11030,
+32719,
+16892,
+49822,
+5110,
+29442,
+35228,
+38636,
+38317,
+249,
+58460,
+19701,
+12179,
+41610,
+30244,
+56682,
+48965,
+20092,
+32944,
+12997,
+38820,
+4473,
+41998,
+50990,
+56117,
+33928,
+12058,
+35495,
+11066,
+44551,
+40638,
+39150,
+25343,
+38399,
+48937,
+20982,
+6813,
+7378,
+15073,
+60186,
+32794,
+27205,
+63647,
+45242,
+37859,
+26266,
+52983,
+4564,
+25096,
+57576,
+59009,
+22583,
+20387,
+16951,
+15469,
+41954,
+14822,
+46155,
+36829,
+60205,
+56367,
+11666,
+27346,
+1506,
+1871,
+30844,
+3800,
+34123,
+35610,
+23011,
+53944,
+13745,
+24426,
+19525,
+48511,
+51014,
+40613,
+13496,
+33005,
+20145,
+26478,
+25551,
+27786,
+46488,
+24715,
+20771,
+24702,
+11064,
+35497,
+18081,
+39163,
+34468,
+38695,
+31545,
+34552,
+44384,
+54232,
+12008,
+35359,
+55220,
+65430,
+58737,
+19077,
+30516,
+37143,
+16814,
+17688,
+22868,
+27518,
+44895,
+12568,
+31041,
+5619,
+40501,
+14666,
+2329,
+49871,
+38248,
+54949,
+61570,
+27085,
+50568,
+40804,
+50325,
+17080,
+58601,
+57666,
+29705,
+39366,
+48276,
+30328,
+21681,
+14283,
+48438,
+18434,
+21093,
+34615,
+54559,
+37710,
+45257,
+21182,
+56736,
+54568,
+56832,
+50110,
+6276,
+62980,
+63256,
+41594,
+51772,
+50087,
+22162,
+29947,
+2069,
+1679,
+23134,
+64640,
+46740,
+60932,
+18492,
+34203,
+29334,
+23555,
+282,
+45159,
+10904,
+7351,
+27109,
+11612,
+58337,
+33648,
+51396,
+60194,
+58312,
+57110,
+55450,
+52133,
+19467,
+54338,
+33226,
+40238,
+8765,
+17488,
+29440,
+5112,
+47888,
+7365,
+38873,
+57525,
+37903,
+21249,
+41553,
+1851,
+44058,
+39751,
+43672,
+42898,
+33059,
+8432,
+62103,
+5709,
+5889,
+38867,
+46203,
+3157,
+1931,
+48611,
+24210,
+51749,
+15797,
+32504,
+29474,
+28735,
+62767,
+38225,
+47411,
+57500,
+62757,
+20188,
+22695,
+63046,
+52299,
+13250,
+56319,
+63156,
+22223,
+63748,
+33246,
+22463,
+51724,
+49856,
+41994,
+18288,
+15424,
+60431,
+11003,
+53561,
+57887,
+64090,
+53890,
+61188,
+16747,
+2118,
+58208,
+6002,
+14139,
+5345,
+26689,
+38093,
+25155,
+12071,
+28497,
+24187,
+52882,
+1591,
+32646,
+5395,
+56271,
+14127,
+46171,
+44799,
+60671,
+19359,
+28819,
+32367,
+46556,
+2166,
+48874,
+43712,
+4034,
+6709,
+9016,
+54832,
+7538,
+52255,
+10798,
+47544,
+38084,
+57963,
+7080,
+16494,
+48525,
+17908,
+50505,
+38466,
+42410,
+820,
+38956,
+46767,
+18029,
+3356,
+59896,
+19604,
+58996,
+9090,
+47912,
+59168,
+8224,
+44606,
+45927,
+27046,
+64,
+23191,
+6605,
+10527,
+57377,
+47979,
+27747,
+12115,
+19674,
+40549,
+41817,
+9575,
+21306,
+56332,
+9279,
+1364,
+38555,
+45557,
+14267,
+8347,
+1893,
+22804,
+31722,
+28742,
+30999,
+45397,
+52469,
+35365,
+329,
+52232,
+5868,
+65471,
+42348,
+11507,
+39256,
+41615,
+65338,
+332,
+5118,
+55091,
+55480,
+11934,
+41311,
+48635,
+49019,
+31597,
+5964,
+15965,
+53786,
+1546,
+26309,
+34284,
+15710,
+47651,
+8986,
+29353,
+52064,
+42494,
+31683,
+6316,
+17472,
+58119,
+54705,
+20459,
+56867,
+12301,
+62869,
+11231,
+37910,
+549,
+44447,
+64643,
+9982,
+61540,
+5036,
+22237,
+59246,
+45606,
+16696,
+62971,
+7845,
+43645,
+52527,
+30505,
+13456,
+47879,
+4262,
+34622,
+61686,
+3014,
+24629,
+34068,
+59138,
+41251,
+5849,
+43950,
+13630,
+24108,
+22577,
+17571,
+42060,
+16402,
+14856,
+59702,
+36544,
+20253,
+12779,
+52887,
+17529,
+573,
+38935,
+59938,
+59860,
+19064,
+57831,
+10310,
+10177,
+7876,
+7306,
+64668,
+54590,
+1887,
+36692,
+62482,
+56289,
+64808,
+42402,
+64368,
+18798,
+21089,
+64584,
+55556,
+40630,
+29054,
+28203,
+31847,
+10111,
+50612,
+38834,
+8368,
+46039,
+17813,
+25338,
+37641,
+44163,
+46450,
+24707,
+45435,
+62320,
+26065,
+45366,
+39112,
+30138,
+39879,
+58230,
+64632,
+10948,
+55937,
+18087,
+7425,
+34058,
+48830,
+5246,
+25956,
+31473,
+11139,
+55378,
+18124,
+8039,
+42005,
+24020,
+5916,
+6846,
+58991,
+55745,
+58744,
+58195,
+22052,
+22233,
+37073,
+57539,
+7852,
+41019,
+15040,
+46266,
+4140,
+53415,
+64754,
+53386,
+24959,
+39998,
+11408,
+20244,
+18139,
+39717,
+64114,
+4195,
+11887,
+37053,
+7785,
+59890,
+49531,
+25274,
+38955,
+1039,
+42411,
+20270,
+60724,
+55337,
+46479,
+22946,
+44175,
+34302,
+59153,
+18852,
+57355,
+27983,
+33540,
+42038,
+51127,
+26818,
+32238,
+61206,
+9491,
+55133,
+62092,
+59049,
+27218,
+65290,
+40229,
+27651,
+30909,
+49070,
+6214,
+31773,
+53742,
+19711,
+30645,
+28933,
+49925,
+12121,
+64700,
+12152,
+31300,
+34701,
+4543,
+25861,
+2193,
+44001,
+46341,
+34970,
+1950,
+7895,
+32811,
+50472,
+59060,
+21931,
+19738,
+45431,
+54657,
+49839,
+25081,
+26232,
+7923,
+6295,
+28408,
+53519,
+11601,
+29561,
+34982,
+26858,
+2524,
+16820,
+16057,
+41332,
+65362,
+36536,
+44130,
+40148,
+51286,
+56464,
+2999,
+10334,
+43426,
+38348,
+5321,
+30282,
+29256,
+45680,
+43513,
+29197,
+44347,
+49869,
+2331,
+9247,
+64638,
+23136,
+44713,
+47573,
+4339,
+15298,
+23296,
+3177,
+30996,
+53977,
+44299,
+24371,
+28701,
+9977,
+14024,
+35155,
+13328,
+51638,
+57149,
+34430,
+30966,
+10460,
+41343,
+3249,
+39787,
+6051,
+36917,
+16711,
+55440,
+16197,
+57005,
+54176,
+55194,
+40019,
+61105,
+57699,
+56080,
+58434,
+62502,
+5034,
+61542,
+22957,
+23372,
+23215,
+4114,
+36122,
+2111,
+2018,
+35415,
+53297,
+31562,
+50941,
+50145,
+29957,
+9515,
+18961,
+40705,
+25064,
+24613,
+45289,
+10482,
+47583,
+61705,
+35327,
+44644,
+33262,
+17694,
+65438,
+34107,
+58884,
+11735,
+498,
+39402,
+17683,
+47827,
+40184,
+25994,
+8946,
+36443,
+2319,
+5453,
+23565,
+12763,
+11261,
+41964,
+14756,
+54835,
+54410,
+24947,
+52532,
+9128,
+49615,
+45166,
+33902,
+18371,
+26773,
+54873,
+23332,
+3012,
+61688,
+2709,
+46022,
+27490,
+39050,
+36491,
+26992,
+40290,
+51102,
+27439,
+41542,
+22128,
+36162,
+3399,
+63090,
+23231,
+24016,
+44903,
+51937,
+61032,
+33628,
+45736,
+24253,
+62082,
+15465,
+4658,
+10959,
+57603,
+35599,
+35113,
+51665,
+59410,
+31015,
+43362,
+31339,
+42186,
+43581,
+24977,
+47935,
+48127,
+12552,
+60975,
+38007,
+44324,
+4138,
+46268,
+27943,
+36984,
+58668,
+564,
+21808,
+54218,
+51137,
+37255,
+51087,
+45363,
+38934,
+915,
+17530,
+58880,
+30239,
+12209,
+10269,
+17569,
+22579,
+21807,
+581,
+58669,
+11778,
+26869,
+3862,
+48420,
+31594,
+58948,
+59997,
+29285,
+59977,
+45390,
+39061,
+47101,
+44446,
+955,
+37911,
+18451,
+30848,
+22006,
+50237,
+42243,
+33494,
+50879,
+28278,
+53378,
+50310,
+56994,
+40227,
+65292,
+47005,
+26805,
+55805,
+57638,
+27575,
+32211,
+8068,
+18032,
+15076,
+21596,
+10631,
+14160,
+31513,
+35473,
+19283,
+12375,
+36058,
+54255,
+52455,
+30287,
+16448,
+26317,
+128,
+13210,
+49217,
+55494,
+39348,
+18097,
+42612,
+18558,
+11573,
+28534,
+3786,
+63444,
+61998,
+39401,
+658,
+11736,
+19159,
+12975,
+65139,
+10236,
+45423,
+17085,
+32501,
+11928,
+42035,
+52618,
+31420,
+17972,
+38591,
+63366,
+34944,
+43873,
+48197,
+60201,
+10283,
+23357,
+19964,
+10072,
+52955,
+33392,
+15690,
+39517,
+26912,
+8524,
+57835,
+49258,
+52739,
+11698,
+27260,
+45154,
+18583,
+29712,
+7551,
+25558,
+35058,
+12333,
+54183,
+50125,
+20354,
+14612,
+43643,
+7847,
+47041,
+19267,
+37596,
+63631,
+15723,
+56406,
+20222,
+12131,
+26541,
+50676,
+22789,
+49414,
+59103,
+25195,
+60276,
+50923,
+21896,
+36174,
+2579,
+10492,
+62946,
+45219,
+16078,
+48040,
+13692,
+45953,
+50853,
+44466,
+49541,
+16501,
+26995,
+41293,
+41235,
+14459,
+40488,
+13897,
+8390,
+8569,
+54243,
+50548,
+2078,
+40540,
+45137,
+45892,
+3067,
+37704,
+55966,
+18539,
+53636,
+56875,
+44729,
+52561,
+65043,
+55302,
+32603,
+63546,
+39422,
+57866,
+24242,
+24304,
+3865,
+51722,
+22465,
+4403,
+15790,
+35407,
+43862,
+25325,
+34141,
+52464,
+12590,
+41501,
+59122,
+53716,
+33640,
+17127,
+7535,
+14759,
+47391,
+20668,
+11159,
+60564,
+25823,
+58394,
+34201,
+18494,
+46272,
+29609,
+30596,
+6354,
+42417,
+48742,
+19954,
+4773,
+37739,
+3323,
+20920,
+6498,
+12102,
+33422,
+32394,
+42815,
+14501,
+49861,
+51331,
+10068,
+45914,
+46430,
+6707,
+4036,
+41057,
+36345,
+59914,
+37922,
+50128,
+25041,
+29629,
+5117,
+987,
+65339,
+52231,
+996,
+35366,
+14831,
+17892,
+4638,
+47130,
+22522,
+29512,
+43283,
+39527,
+33693,
+2267,
+53546,
+46664,
+57839,
+26507,
+12563,
+53984,
+36667,
+17547,
+6787,
+60643,
+48032,
+38337,
+1719,
+48506,
+30930,
+21918,
+5661,
+37793,
+62030,
+62678,
+21625,
+30333,
+25104,
+10477,
+8276,
+55886,
+51799,
+43616,
+36737,
+36637,
+12331,
+35060,
+60478,
+20538,
+45158,
+1162,
+23556,
+7724,
+63188,
+54457,
+54417,
+12585,
+23355,
+10285,
+61696,
+34937,
+1451,
+56556,
+35083,
+14917,
+28473,
+46073,
+55150,
+16136,
+30152,
+21262,
+63026,
+38566,
+6579,
+39373,
+32896,
+256,
+32897,
+13159,
+19687,
+23146,
+7572,
+58459,
+1316,
+38318,
+64741,
+25309,
+65002,
+18533,
+62840,
+11645,
+4351,
+30857,
+20566,
+39928,
+40373,
+31380,
+53824,
+50133,
+39941,
+54520,
+37166,
+15102,
+4579,
+42829,
+40708,
+21749,
+28545,
+9655,
+31446,
+40444,
+53082,
+45578,
+52669,
+55015,
+43792,
+5158,
+60355,
+32156,
+27384,
+36169,
+33545,
+53990,
+16933,
+15439,
+16780,
+42949,
+4156,
+24737,
+6134,
+46395,
+41735,
+23921,
+64549,
+65206,
+13307,
+48484,
+7730,
+44930,
+31580,
+22187,
+2745,
+25180,
+52707,
+1743,
+15070,
+29245,
+22318,
+51646,
+2865,
+27007,
+61192,
+19297,
+39908,
+9941,
+41840,
+24204,
+64788,
+29002,
+37125,
+26291,
+13414,
+16577,
+54291,
+34719,
+59258,
+1974,
+38925,
+5195,
+35784,
+28723,
+59024,
+13517,
+4534,
+27791,
+53014,
+42431,
+35094,
+55749,
+11137,
+31475,
+43836,
+10049,
+59819,
+60666,
+20954,
+46235,
+21695,
+7737,
+4583,
+41076,
+2324,
+15448,
+43377,
+25170,
+20570,
+4337,
+47575,
+29834,
+26609,
+32272,
+42524,
+11595,
+13209,
+512,
+26318,
+46292,
+51381,
+11099,
+50618,
+37066,
+23290,
+61714,
+14319,
+62760,
+34729,
+43503,
+30204,
+20121,
+43498,
+19310,
+15351,
+25619,
+44493,
+10316,
+64312,
+6801,
+42443,
+30878,
+20361,
+49474,
+27253,
+47842,
+64875,
+31431,
+24323,
+44374,
+50360,
+3486,
+58490,
+37755,
+54014,
+38594,
+19882,
+48408,
+58004,
+52582,
+33154,
+3901,
+3948,
+10390,
+57446,
+27034,
+55582,
+19325,
+45961,
+62950,
+20098,
+55795,
+26933,
+15474,
+16615,
+30896,
+50340,
+8674,
+59668,
+64544,
+23190,
+1024,
+27047,
+22198,
+8595,
+57891,
+59983,
+21469,
+40242,
+38620,
+51238,
+20632,
+13602,
+61756,
+33411,
+30147,
+62862,
+23211,
+6972,
+9973,
+11651,
+31279,
+39627,
+7802,
+20780,
+54068,
+38650,
+60363,
+46053,
+30948,
+61792,
+17348,
+63551,
+2048,
+44396,
+50245,
+42938,
+11703,
+41264,
+57975,
+60294,
+46422,
+19946,
+62558,
+15604,
+42599,
+55189,
+61896,
+34696,
+4096,
+34953,
+23406,
+50413,
+27307,
+59579,
+19661,
+58255,
+8192,
+46812,
+54614,
+39322,
+16384,
+43691,
+32768,
+65536,
+0,
+};
--- /dev/null
+++ b/appl/lib/ida/idatest.b
@@ -1,0 +1,84 @@
+implement Idatest;
+
+#
+# Copyright © 2006 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "rand.m";
+	rand: Rand;
+
+include "ida.m";
+	ida: Ida;
+	Frag: import ida;
+
+Idatest: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	rand = load Rand Rand->PATH;
+	ida = load Ida Ida->PATH;
+
+	rand->init(sys->pctl(0,nil));
+	ida->init();
+
+	stderr := sys->fildes(2);
+	args = tl args;
+	debug := 0;
+	nowrite := 0;
+	onlyenc := 0;
+	for(; args != nil; args = tl args)
+		case hd args {
+		"-d" =>	debug = 1;
+		"-w" =>	nowrite = 1;
+		"-e" =>	onlyenc = 1;
+		}
+	buf := array[1024] of byte;
+	while((n := sys->read(sys->fildes(0), buf, len buf)) > 0){
+		frags := array[14] of ref Frag;
+		for(x := 0; x < len frags; x++){
+			frags[x] = f := ida->fragment(buf[0:n], 7);
+			if(debug){
+				for(i := 0; i < len f.enc; i++)
+					sys->fprint(stderr, " %d", f.enc[i]);
+				sys->fprint(stderr, "\n");
+			}
+		}
+		if(onlyenc)
+			continue;
+		if(1){
+			# shuffle
+			for(i := 0; i < len frags; i++){
+				r := rand->rand(len frags);
+				if(r != i){
+					t := frags[i]; frags[i] = frags[r]; frags[r] = t;
+				}
+			}
+		}
+		# recover
+		(zot, err) := ida->reconstruct(frags);
+		if(err != nil){
+			sys->fprint(stderr, "reconstruction failed: %s\n", err);
+			raise "fail:reconstruct";
+		}
+		if(len zot != n){
+			sys->fprint(stderr, "bad length: expected %d got %d\n", n, len zot);
+			raise "fail:length";
+		}
+		if(debug){
+			for(i := 0; i < len zot; i++)
+				sys->fprint(stderr, " %.2ux", int zot[i]);
+			sys->fprint(stderr, "\n");
+			sys->fprint(stderr, "%q\n", string zot);
+		}else if(!nowrite)
+			sys->write(sys->fildes(1), zot, len zot);
+	}
+}
--- /dev/null
+++ b/appl/lib/ida/mkfile
@@ -1,0 +1,21 @@
+<../../../mkconfig
+
+TARG=\
+	ida.dis\
+	idatab.dis\
+
+MODULES=\
+
+SYSMODULES= \
+	ida.m\
+	rand.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/lib/ida
+
+<$ROOT/mkfiles/mkdis
+# force compilation
+LIMBOFLAGS= -c $LIMBOFLAGS
+
+idatab.dis:	idatab.dist
+	cp idatab.dist idatab.dis
--- /dev/null
+++ b/appl/lib/ida/mktab.b
@@ -1,0 +1,32 @@
+implement Genfield;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "keyring.m";
+	kr: Keyring;
+	IPint: import kr;
+
+Genfield: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+Field: con 65537;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+
+	f := IPint.inttoip(Field);
+	fm2 := f.sub(IPint.inttoip(2));
+	for(i := 1; i <= Field; i++){
+		x := IPint.inttoip(i);
+		y := x.expmod(fm2, f);
+#		sys->print("%s\n", x.mul(y).expmod(IPint.inttoip(1), f).iptostr(10));
+		sys->print("%d,\n", y.iptoint());
+	}
+}
--- /dev/null
+++ b/appl/lib/imageremap.b
@@ -1,0 +1,738 @@
+implement Imageremap;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Display, Image: import draw;
+
+include "bufio.m";
+
+include "imagefile.m";
+
+closest:= array[16*16*16] of {
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 255,byte 255,byte 250,byte 250,byte 250,byte 220,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 250,byte 250,byte 250,byte 250,byte 249,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 250,byte 250,byte 250,byte 233,byte 233,byte 249,
+	byte 249,byte 232,byte 215,byte 215,byte 248,byte 231,byte 214,byte 197,
+	byte 234,byte 234,byte 250,byte 250,byte 233,byte 233,byte 216,byte 216,
+	byte 249,byte 232,byte 215,byte 198,byte 198,byte 231,byte 214,byte 197,
+	byte 217,byte 217,byte 217,byte 246,byte 233,byte 216,byte 216,byte 199,
+	byte 199,byte 215,byte 215,byte 198,byte 198,byte 198,byte 214,byte 197,
+	byte 200,byte 200,byte 246,byte 246,byte 246,byte 216,byte 199,byte 199,
+	byte 245,byte 245,byte 198,byte 244,byte 244,byte 244,byte 227,byte 197,
+	byte 247,byte 247,byte 246,byte 246,byte 246,byte 246,byte 199,byte 245,
+	byte 245,byte 245,byte 228,byte 244,byte 244,byte 244,byte 227,byte 193,
+	byte 230,byte 230,byte 246,byte 246,byte 229,byte 229,byte 212,byte 245,
+	byte 245,byte 228,byte 228,byte 211,byte 244,byte 227,byte 210,byte 193,
+	byte 213,byte 213,byte 229,byte 229,byte 212,byte 212,byte 212,byte 195,
+	byte 228,byte 228,byte 211,byte 211,byte 194,byte 227,byte 210,byte 193,
+	byte 196,byte 196,byte 242,byte 242,byte 212,byte 195,byte 195,byte 241,
+	byte 241,byte 211,byte 211,byte 194,byte 194,byte 240,byte 210,byte 193,
+	byte 243,byte 243,byte 242,byte 242,byte 242,byte 195,byte 195,byte 241,
+	byte 241,byte 241,byte 194,byte 194,byte 240,byte 240,byte 239,byte 205,
+	byte 226,byte 226,byte 242,byte 242,byte 225,byte 225,byte 195,byte 241,
+	byte 241,byte 224,byte 224,byte 240,byte 240,byte 239,byte 239,byte 205,
+	byte 209,byte 209,byte 225,byte 225,byte 208,byte 208,byte 208,byte 224,
+	byte 224,byte 223,byte 223,byte 223,byte 239,byte 239,byte 222,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 238,byte 221,byte 221,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 221,byte 221,byte 221,byte 204,byte 250,byte 220,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 221,byte 221,byte 204,byte 250,byte 250,byte 249,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 204,byte 250,byte 250,byte 233,byte 233,byte 249,
+	byte 249,byte 232,byte 215,byte 215,byte 248,byte 231,byte 214,byte 197,
+	byte 234,byte 234,byte 250,byte 250,byte 233,byte 233,byte 216,byte 216,
+	byte 249,byte 232,byte 215,byte 198,byte 198,byte 231,byte 214,byte 197,
+	byte 217,byte 217,byte 217,byte 246,byte 233,byte 216,byte 216,byte 199,
+	byte 199,byte 215,byte 215,byte 198,byte 198,byte 198,byte 214,byte 197,
+	byte 200,byte 200,byte 246,byte 246,byte 246,byte 216,byte 199,byte 199,
+	byte 245,byte 245,byte 198,byte 244,byte 244,byte 244,byte 227,byte 197,
+	byte 247,byte 247,byte 246,byte 246,byte 246,byte 246,byte 199,byte 245,
+	byte 245,byte 245,byte 228,byte 244,byte 244,byte 244,byte 227,byte 193,
+	byte 230,byte 230,byte 246,byte 246,byte 229,byte 229,byte 212,byte 245,
+	byte 245,byte 228,byte 228,byte 211,byte 244,byte 227,byte 210,byte 193,
+	byte 213,byte 213,byte 229,byte 229,byte 212,byte 212,byte 212,byte 195,
+	byte 228,byte 228,byte 211,byte 211,byte 194,byte 227,byte 210,byte 193,
+	byte 196,byte 196,byte 242,byte 242,byte 212,byte 195,byte 195,byte 241,
+	byte 241,byte 211,byte 211,byte 194,byte 194,byte 240,byte 210,byte 193,
+	byte 243,byte 243,byte 242,byte 242,byte 242,byte 195,byte 195,byte 241,
+	byte 241,byte 241,byte 194,byte 194,byte 240,byte 240,byte 239,byte 205,
+	byte 226,byte 226,byte 242,byte 242,byte 225,byte 225,byte 195,byte 241,
+	byte 241,byte 224,byte 224,byte 240,byte 240,byte 239,byte 239,byte 205,
+	byte 209,byte 209,byte 225,byte 225,byte 208,byte 208,byte 208,byte 224,
+	byte 224,byte 223,byte 223,byte 223,byte 239,byte 239,byte 222,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 255,byte 255,byte 255,byte 191,byte 191,byte 191,byte 220,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 255,byte 221,byte 221,byte 221,byte 204,byte 191,byte 220,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 255,byte 221,byte 221,byte 204,byte 204,byte 204,byte 186,byte 186,
+	byte 186,byte 186,byte 186,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 221,byte 204,byte 204,byte 204,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 232,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 233,byte 216,byte 186,
+	byte 186,byte 186,byte 215,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 217,byte 217,byte 183,byte 183,byte 183,byte 216,byte 216,byte 199,
+	byte 182,byte 182,byte 215,byte 198,byte 198,byte 181,byte 214,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 199,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 181,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 193,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 228,byte 165,byte 181,byte 181,byte 164,byte 164,byte 193,
+	byte 167,byte 167,byte 183,byte 229,byte 166,byte 212,byte 212,byte 182,
+	byte 182,byte 165,byte 211,byte 211,byte 181,byte 164,byte 210,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 195,byte 178,
+	byte 178,byte 178,byte 211,byte 194,byte 177,byte 177,byte 177,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 195,byte 178,
+	byte 178,byte 178,byte 178,byte 177,byte 177,byte 177,byte 177,byte 205,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 178,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 177,byte 160,byte 205,
+	byte 163,byte 163,byte 162,byte 162,byte 162,byte 162,byte 208,byte 178,
+	byte 161,byte 161,byte 223,byte 177,byte 177,byte 160,byte 160,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 176,byte 176,byte 191,byte 191,byte 191,byte 191,byte 190,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 176,byte 221,byte 221,byte 204,byte 191,byte 191,byte 190,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 188,byte 221,byte 204,byte 204,byte 204,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 173,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 168,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 170,byte 170,byte 182,
+	byte 182,byte 169,byte 152,byte 152,byte 181,byte 168,byte 151,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 165,byte 181,byte 181,byte 181,byte 164,byte 193,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 164,byte 193,
+	byte 167,byte 167,byte 167,byte 166,byte 166,byte 166,byte 149,byte 182,
+	byte 165,byte 165,byte 165,byte 148,byte 181,byte 164,byte 147,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 149,byte 178,
+	byte 178,byte 178,byte 148,byte 177,byte 177,byte 177,byte 147,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 178,byte 178,
+	byte 178,byte 178,byte 178,byte 177,byte 177,byte 177,byte 160,byte 205,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 162,byte 162,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 160,byte 160,byte 205,
+	byte 163,byte 163,byte 162,byte 162,byte 162,byte 162,byte 145,byte 161,
+	byte 161,byte 161,byte 144,byte 144,byte 160,byte 160,byte 160,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 176,byte 176,byte 191,byte 191,byte 191,byte 174,byte 174,byte 190,
+	byte 190,byte 173,byte 156,byte 156,byte 189,byte 172,byte 155,byte 138,
+	byte 176,byte 176,byte 204,byte 191,byte 191,byte 174,byte 174,byte 190,
+	byte 190,byte 173,byte 156,byte 156,byte 189,byte 172,byte 155,byte 138,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 187,byte 187,byte 170,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 152,byte 152,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 183,byte 183,byte 170,byte 170,byte 170,byte 153,
+	byte 182,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 153,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 165,byte 181,byte 181,byte 181,byte 164,byte 130,
+	byte 167,byte 167,byte 183,byte 183,byte 166,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 166,byte 166,byte 166,byte 149,byte 149,byte 182,
+	byte 165,byte 165,byte 148,byte 148,byte 164,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 179,byte 179,byte 179,byte 149,byte 132,byte 178,
+	byte 178,byte 178,byte 148,byte 131,byte 177,byte 177,byte 147,byte 130,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 132,byte 178,
+	byte 178,byte 178,byte 161,byte 177,byte 177,byte 177,byte 160,byte 142,
+	byte 163,byte 163,byte 179,byte 179,byte 162,byte 162,byte 162,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 162,byte 162,byte 145,byte 145,byte 145,byte 161,
+	byte 161,byte 144,byte 144,byte 144,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 175,byte 175,byte 191,byte 191,byte 174,byte 174,byte 157,byte 157,
+	byte 190,byte 173,byte 156,byte 139,byte 139,byte 172,byte 155,byte 138,
+	byte 175,byte 175,byte 191,byte 191,byte 174,byte 174,byte 157,byte 157,
+	byte 190,byte 173,byte 156,byte 139,byte 139,byte 172,byte 155,byte 138,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 157,byte 186,
+	byte 186,byte 186,byte 156,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 168,byte 138,
+	byte 171,byte 171,byte 187,byte 187,byte 170,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 152,byte 152,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 187,byte 170,byte 170,byte 170,byte 170,byte 153,
+	byte 169,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 170,byte 153,byte 153,
+	byte 169,byte 152,byte 152,byte 135,byte 135,byte 135,byte 151,byte 134,
+	byte 154,byte 154,byte 183,byte 183,byte 183,byte 153,byte 153,byte 153,
+	byte 182,byte 182,byte 135,byte 135,byte 181,byte 181,byte 164,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 164,byte 130,
+	byte 167,byte 167,byte 183,byte 166,byte 166,byte 166,byte 149,byte 182,
+	byte 165,byte 165,byte 165,byte 148,byte 181,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 150,byte 166,byte 149,byte 149,byte 149,byte 132,
+	byte 165,byte 165,byte 148,byte 148,byte 131,byte 147,byte 147,byte 130,
+	byte 133,byte 133,byte 179,byte 179,byte 149,byte 132,byte 132,byte 132,
+	byte 178,byte 148,byte 148,byte 131,byte 131,byte 131,byte 130,byte 130,
+	byte 133,byte 133,byte 179,byte 179,byte 179,byte 132,byte 132,byte 178,
+	byte 178,byte 178,byte 131,byte 131,byte 131,byte 177,byte 160,byte 142,
+	byte 163,byte 163,byte 179,byte 162,byte 162,byte 162,byte 132,byte 178,
+	byte 161,byte 161,byte 144,byte 131,byte 177,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 162,byte 162,byte 145,byte 145,byte 145,byte 161,
+	byte 161,byte 144,byte 144,byte 143,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 158,byte 158,byte 158,byte 112,byte 174,byte 157,byte 157,byte 140,
+	byte 140,byte 156,byte 156,byte 139,byte 139,byte 139,byte 155,byte 138,
+	byte 158,byte 158,byte 158,byte 112,byte 174,byte 157,byte 157,byte 140,
+	byte 140,byte 156,byte 156,byte 139,byte 139,byte 139,byte 155,byte 138,
+	byte 158,byte 158,byte 124,byte 124,byte 124,byte 157,byte 157,byte 140,
+	byte 123,byte 123,byte 156,byte 139,byte 139,byte 122,byte 155,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 170,byte 170,byte 123,
+	byte 123,byte 169,byte 152,byte 152,byte 122,byte 168,byte 151,byte 138,
+	byte 171,byte 171,byte 124,byte 124,byte 170,byte 170,byte 170,byte 153,
+	byte 123,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 170,byte 153,byte 153,
+	byte 169,byte 152,byte 152,byte 135,byte 135,byte 135,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 153,byte 153,byte 153,
+	byte 136,byte 152,byte 135,byte 135,byte 135,byte 135,byte 134,byte 134,
+	byte 137,byte 137,byte 137,byte 120,byte 153,byte 153,byte 153,byte 136,
+	byte 136,byte 136,byte 135,byte 135,byte 135,byte 118,byte 164,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 166,byte 136,byte 136,
+	byte 136,byte 165,byte 165,byte 118,byte 118,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 120,byte 166,byte 166,byte 149,byte 149,byte 136,
+	byte 165,byte 165,byte 148,byte 148,byte 118,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 150,byte 149,byte 149,byte 149,byte 132,byte 132,
+	byte 165,byte 148,byte 148,byte 131,byte 131,byte 147,byte 147,byte 130,
+	byte 133,byte 133,byte 133,byte 149,byte 132,byte 132,byte 132,byte 132,
+	byte 115,byte 148,byte 131,byte 131,byte 131,byte 131,byte 130,byte 130,
+	byte 133,byte 133,byte 133,byte 116,byte 132,byte 132,byte 132,byte 132,
+	byte 115,byte 115,byte 131,byte 131,byte 131,byte 131,byte 160,byte 142,
+	byte 133,byte 133,byte 116,byte 162,byte 162,byte 132,byte 132,byte 115,
+	byte 161,byte 161,byte 144,byte 131,byte 131,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 146,byte 145,byte 145,byte 145,byte 128,byte 161,
+	byte 144,byte 144,byte 144,byte 143,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 141,byte 141,byte 112,byte 112,byte 112,byte 157,byte 140,byte 140,
+	byte 140,byte 127,byte 139,byte 126,byte 126,byte 126,byte 109,byte 138,
+	byte 141,byte 141,byte 112,byte 112,byte 112,byte 157,byte 140,byte 140,
+	byte 140,byte 127,byte 139,byte 126,byte 126,byte 126,byte 109,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 140,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 122,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 105,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 153,byte 123,
+	byte 123,byte 123,byte 152,byte 122,byte 122,byte 122,byte 105,byte 134,
+	byte 154,byte 154,byte 124,byte 124,byte 124,byte 153,byte 153,byte 153,
+	byte 123,byte 123,byte 135,byte 135,byte 122,byte 122,byte 105,byte 134,
+	byte 137,byte 137,byte 137,byte 120,byte 153,byte 153,byte 153,byte 136,
+	byte 136,byte 136,byte 135,byte 135,byte 135,byte 118,byte 105,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 153,byte 136,byte 136,
+	byte 136,byte 119,byte 119,byte 118,byte 118,byte 118,byte 118,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 120,byte 136,byte 136,
+	byte 119,byte 119,byte 119,byte 118,byte 118,byte 118,byte 101,byte 130,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte 130,
+	byte 133,byte 133,byte 120,byte 120,byte 149,byte 132,byte 132,byte 119,
+	byte 119,byte 102,byte 148,byte 131,byte 131,byte 101,byte 101,byte 130,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 132,byte 132,byte 115,
+	byte 115,byte 115,byte 131,byte 131,byte 114,byte 114,byte 114,byte 130,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 132,byte 115,
+	byte 115,byte 115,byte 131,byte 114,byte 114,byte 114,byte 114,byte 142,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte  98,byte 114,byte 114,byte 114,byte  97,byte 142,
+	byte 100,byte 100,byte 116,byte  99,byte  99,byte  99,byte  99,byte 115,
+	byte  98,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 113,byte 113,byte 112,byte 112,byte 112,byte 112,byte 140,byte 140,
+	byte 127,byte 127,byte 110,byte 126,byte 126,byte 126,byte 109,byte  75,
+	byte 113,byte 113,byte 112,byte 112,byte 112,byte 112,byte 140,byte 140,
+	byte 127,byte 127,byte 110,byte 126,byte 126,byte 126,byte 109,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 122,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 122,byte 105,byte  71,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte 105,byte  71,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 107,byte 136,byte 136,
+	byte 136,byte 106,byte 106,byte 118,byte 118,byte 105,byte  88,byte  71,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 120,byte 136,byte 136,
+	byte 119,byte 119,byte 119,byte 118,byte 118,byte 118,byte 101,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte  67,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 103,byte 103,byte 119,
+	byte 119,byte 102,byte 102,byte 118,byte 118,byte 101,byte 101,byte  67,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte 115,byte 114,byte 114,byte 114,byte 114,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte 115,byte 114,byte 114,byte 114,byte  97,byte  79,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte  99,byte  99,byte 115,
+	byte 115,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  99,byte  82,byte  98,
+	byte  98,byte  98,byte  81,byte 114,byte  97,byte  97,byte  97,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  96,byte  96,byte 112,byte 112,byte 111,byte 111,byte  94,byte 127,
+	byte 127,byte 110,byte 110,byte  93,byte 126,byte 109,byte  92,byte  75,
+	byte  96,byte  96,byte 112,byte 112,byte 111,byte 111,byte  94,byte 127,
+	byte 127,byte 110,byte 110,byte  93,byte 126,byte 109,byte  92,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 105,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte 105,byte  75,
+	byte 108,byte 108,byte 124,byte 124,byte 107,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte  88,byte  71,
+	byte 108,byte 108,byte 124,byte 107,byte 107,byte 107,byte  90,byte 123,
+	byte 106,byte 106,byte 106,byte  89,byte 122,byte 105,byte  88,byte  71,
+	byte  91,byte  91,byte 120,byte 107,byte 107,byte  90,byte  90,byte 136,
+	byte 106,byte 106,byte  89,byte  89,byte 118,byte 105,byte  88,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 103,byte 103,byte 119,
+	byte 119,byte 102,byte 102,byte 118,byte 118,byte 101,byte 101,byte  67,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 104,byte 104,byte 103,byte 103,byte 103,byte 103,byte  86,byte 102,
+	byte 102,byte 102,byte  85,byte  85,byte 101,byte 101,byte  84,byte  67,
+	byte  87,byte  87,byte 116,byte 116,byte 116,byte  86,byte  86,byte 115,
+	byte 115,byte 115,byte  85,byte  85,byte 114,byte 114,byte  84,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte  98,byte 114,byte 114,byte 114,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  99,byte  99,byte 115,
+	byte  98,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte  79,
+	byte  83,byte  83,byte  99,byte  99,byte  82,byte  82,byte  82,byte  98,
+	byte  98,byte  81,byte  81,byte  81,byte  97,byte  97,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  95,byte  95,byte 111,byte 111,byte  94,byte  94,byte  94,byte  77,
+	byte 110,byte 110,byte  93,byte  93,byte  76,byte 109,byte  92,byte  75,
+	byte  95,byte  95,byte 111,byte 111,byte  94,byte  94,byte  94,byte  77,
+	byte 110,byte 110,byte  93,byte  93,byte  76,byte 109,byte  92,byte  75,
+	byte 108,byte 108,byte 124,byte 111,byte 107,byte  94,byte  94,byte 123,
+	byte 123,byte 106,byte  93,byte  93,byte 122,byte 105,byte  92,byte  75,
+	byte 108,byte 108,byte 108,byte 107,byte 107,byte 107,byte  90,byte 123,
+	byte 106,byte 106,byte 106,byte  89,byte 122,byte 105,byte  88,byte  75,
+	byte  91,byte  91,byte 107,byte 107,byte 107,byte  90,byte  90,byte 123,
+	byte 106,byte 106,byte  89,byte  89,byte 105,byte 105,byte  88,byte  71,
+	byte  91,byte  91,byte  91,byte 107,byte  90,byte  90,byte  90,byte  73,
+	byte 106,byte 106,byte  89,byte  89,byte  72,byte  88,byte  88,byte  71,
+	byte  91,byte  91,byte  91,byte  90,byte  90,byte  90,byte  73,byte  73,
+	byte 106,byte  89,byte  89,byte  72,byte  72,byte  88,byte  88,byte  71,
+	byte  74,byte  74,byte 120,byte 120,byte 120,byte  73,byte  73,byte 119,
+	byte 119,byte 102,byte  89,byte  72,byte  72,byte 101,byte 101,byte  71,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 104,byte 104,byte 103,byte 103,byte 103,byte 103,byte  86,byte 102,
+	byte 102,byte 102,byte  85,byte  85,byte 101,byte 101,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte 103,byte  86,byte  86,byte  86,byte  86,
+	byte 102,byte  85,byte  85,byte  85,byte  85,byte  84,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte  86,byte  86,byte  86,byte  69,byte  69,
+	byte 115,byte  85,byte  85,byte  85,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte 116,byte 116,byte  99,byte  69,byte  69,byte  69,
+	byte 115,byte  98,byte  85,byte  68,byte  68,byte  97,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  82,byte  82,byte  98,
+	byte  98,byte  98,byte  81,byte  68,byte  97,byte  97,byte  97,byte  79,
+	byte  83,byte  83,byte  83,byte  82,byte  82,byte  82,byte  82,byte  98,
+	byte  81,byte  81,byte  81,byte  64,byte  97,byte  97,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  78,byte  78,byte  49,byte  49,byte  94,byte  77,byte  77,byte  48,
+	byte  48,byte  93,byte  93,byte  76,byte  76,byte  63,byte  92,byte  75,
+	byte  78,byte  78,byte  49,byte  49,byte  94,byte  77,byte  77,byte  48,
+	byte  48,byte  93,byte  93,byte  76,byte  76,byte  63,byte  92,byte  75,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  77,byte  60,
+	byte  60,byte  60,byte  93,byte  76,byte  59,byte  59,byte  59,byte  75,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  90,byte  60,
+	byte  60,byte  60,byte  89,byte  59,byte  59,byte  59,byte  88,byte  75,
+	byte  91,byte  91,byte  61,byte  61,byte  61,byte  90,byte  73,byte  60,
+	byte  60,byte  60,byte  89,byte  72,byte  59,byte  59,byte  88,byte  71,
+	byte  74,byte  74,byte  61,byte  61,byte  90,byte  73,byte  73,byte  73,
+	byte  60,byte  89,byte  89,byte  72,byte  72,byte  72,byte  71,byte  71,
+	byte  74,byte  74,byte  74,byte  90,byte  73,byte  73,byte  73,byte  73,
+	byte  56,byte  89,byte  72,byte  72,byte  72,byte  72,byte  71,byte  71,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  73,byte  73,byte  56,
+	byte  56,byte  56,byte  72,byte  72,byte  55,byte  55,byte  55,byte  71,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  56,byte  55,byte  55,byte  55,byte  55,byte  67,
+	byte  87,byte  87,byte  57,byte  57,byte  57,byte  86,byte  86,byte  56,
+	byte  56,byte  56,byte  85,byte  85,byte  55,byte  55,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte  86,byte  86,byte  86,byte  69,byte  69,
+	byte  56,byte  85,byte  85,byte  85,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte  70,byte  53,byte  69,byte  69,byte  69,byte  69,
+	byte  52,byte  85,byte  85,byte  68,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  68,byte  51,byte  51,byte  79,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  80,byte  79,
+	byte  83,byte  83,byte  53,byte  82,byte  82,byte  65,byte  65,byte  52,
+	byte  52,byte  81,byte  64,byte  64,byte  51,byte  80,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  50,byte  50,byte  49,byte  49,byte  49,byte  77,byte  77,byte  48,
+	byte  48,byte  48,byte  76,byte  76,byte  63,byte  63,byte  46,byte  12,
+	byte  50,byte  50,byte  49,byte  49,byte  49,byte  77,byte  77,byte  48,
+	byte  48,byte  48,byte  76,byte  76,byte  63,byte  63,byte  46,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  77,byte  60,
+	byte  60,byte  60,byte  60,byte  59,byte  59,byte  59,byte  59,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  60,byte  60,
+	byte  60,byte  60,byte  60,byte  59,byte  59,byte  59,byte  42,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  73,byte  60,
+	byte  60,byte  60,byte  43,byte  59,byte  59,byte  59,byte  42,byte   8,
+	byte  74,byte  74,byte  61,byte  61,byte  61,byte  73,byte  73,byte  60,
+	byte  60,byte  60,byte  72,byte  72,byte  72,byte  59,byte  42,byte   8,
+	byte  74,byte  74,byte  74,byte  57,byte  73,byte  73,byte  73,byte  73,
+	byte  56,byte  56,byte  72,byte  72,byte  72,byte  72,byte  42,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  73,byte  56,
+	byte  56,byte  56,byte  72,byte  55,byte  55,byte  55,byte  55,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  56,byte  55,byte  55,byte  55,byte  38,byte   4,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  39,byte  55,byte  55,byte  55,byte  38,byte   4,
+	byte  70,byte  70,byte  57,byte  57,byte  40,byte  69,byte  69,byte  69,
+	byte  56,byte  39,byte  85,byte  68,byte  68,byte  38,byte  38,byte   4,
+	byte  70,byte  70,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  68,byte  51,byte  51,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  51,byte   0,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  53,byte  69,byte  52,
+	byte  52,byte  52,byte  35,byte  51,byte  51,byte  51,byte  34,byte   0,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  33,byte  33,byte  49,byte  49,byte  32,byte  32,byte  77,byte  48,
+	byte  48,byte  47,byte  47,byte  63,byte  63,byte  46,byte  46,byte  12,
+	byte  33,byte  33,byte  49,byte  49,byte  32,byte  32,byte  77,byte  48,
+	byte  48,byte  47,byte  47,byte  63,byte  63,byte  46,byte  46,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  60,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  59,byte  42,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  44,byte  44,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  42,byte  42,byte  12,
+	byte  45,byte  45,byte  61,byte  61,byte  44,byte  44,byte  44,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  42,byte  42,byte   8,
+	byte  45,byte  45,byte  61,byte  44,byte  44,byte  44,byte  73,byte  60,
+	byte  43,byte  43,byte  26,byte  72,byte  59,byte  42,byte  42,byte   8,
+	byte  74,byte  74,byte  57,byte  44,byte  44,byte  73,byte  73,byte  56,
+	byte  43,byte  43,byte  26,byte  72,byte  72,byte  42,byte  42,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  39,byte  55,byte  55,byte  55,byte  38,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  40,byte  40,byte  56,
+	byte  56,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   4,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  40,byte  40,byte  56,
+	byte  39,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   4,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  23,byte  23,byte  39,
+	byte  39,byte  39,byte  22,byte  68,byte  38,byte  38,byte  38,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  21,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  53,byte  69,byte  52,
+	byte  52,byte  52,byte  35,byte  51,byte  51,byte  51,byte  34,byte   0,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte  37,byte  37,byte  36,byte  36,byte  36,byte  36,byte  36,byte  35,
+	byte  35,byte  35,byte  35,byte  18,byte  34,byte  34,byte  34,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  16,byte  16,byte  32,byte  32,byte  31,byte  31,byte  31,byte  47,
+	byte  47,byte  30,byte  30,byte  30,byte  46,byte  46,byte  29,byte  12,
+	byte  16,byte  16,byte  32,byte  32,byte  31,byte  31,byte  31,byte  47,
+	byte  47,byte  30,byte  30,byte  30,byte  46,byte  46,byte  29,byte  12,
+	byte  45,byte  45,byte  44,byte  44,byte  44,byte  44,byte  31,byte  60,
+	byte  43,byte  43,byte  30,byte  59,byte  59,byte  42,byte  42,byte  12,
+	byte  45,byte  45,byte  44,byte  44,byte  44,byte  44,byte  27,byte  43,
+	byte  43,byte  43,byte  26,byte  26,byte  42,byte  42,byte  42,byte  12,
+	byte  28,byte  28,byte  44,byte  44,byte  27,byte  27,byte  27,byte  43,
+	byte  43,byte  26,byte  26,byte  26,byte  42,byte  42,byte  25,byte   8,
+	byte  28,byte  28,byte  44,byte  44,byte  27,byte  27,byte  27,byte  43,
+	byte  43,byte  26,byte  26,byte   9,byte  42,byte  42,byte  25,byte   8,
+	byte  28,byte  28,byte  28,byte  27,byte  27,byte  27,byte  10,byte  43,
+	byte  26,byte  26,byte  26,byte   9,byte  42,byte  42,byte  25,byte   8,
+	byte  41,byte  41,byte  57,byte  40,byte  40,byte  40,byte  40,byte  56,
+	byte  39,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   8,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  40,byte  23,byte  39,
+	byte  39,byte  39,byte  22,byte  55,byte  38,byte  38,byte  38,byte   4,
+	byte  24,byte  24,byte  40,byte  40,byte  23,byte  23,byte  23,byte  39,
+	byte  39,byte  22,byte  22,byte  22,byte  38,byte  38,byte  21,byte   4,
+	byte  24,byte  24,byte  24,byte  23,byte  23,byte  23,byte  23,byte  39,
+	byte  22,byte  22,byte  22,byte   5,byte  38,byte  38,byte  21,byte   4,
+	byte  24,byte  24,byte  53,byte  23,byte  23,byte   6,byte   6,byte  52,
+	byte  52,byte  22,byte   5,byte   5,byte  51,byte  21,byte  21,byte   4,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte  37,byte  37,byte  36,byte  36,byte  36,byte  36,byte  36,byte  35,
+	byte  35,byte  35,byte  35,byte  18,byte  34,byte  34,byte  34,byte   0,
+	byte  20,byte  20,byte  36,byte  36,byte  19,byte  19,byte  19,byte  35,
+	byte  35,byte  18,byte  18,byte  18,byte  34,byte  34,byte  17,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+};
+
+clamp: array of int;
+rgbvmap: array of int;
+
+init(d: ref Display)
+{
+	# initialise in a way that make races slightly wasteful but benign
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(draw == nil)
+		draw = load Draw Draw->PATH;
+	if(clamp == nil){
+		m := array[64+256+64] of int;
+		for(j:=0; j<64; j++)
+			m[j] = 0;
+		for(j=0; j<256; j++)
+			m[64+j] = (j>>4);
+		for(j=0; j<64; j++)
+			m[64+256+j] = (255>>4);
+		clamp = m;
+	}
+	if(rgbvmap == nil){
+		m := array[3*256] of int;
+		for(j:=0; j<256; j++)
+			(m[3*j+0], m[3*j+1], m[3*j+2]) = d.cmap2rgb(j);
+		rgbvmap = m;
+	}
+}
+
+remap(i: ref RImagefile->Rawimage, d: ref Display, errdiff: int): (ref Image, string)
+{
+	if(sys == nil || draw == nil || clamp == nil || rgbvmap == nil)
+		init(d);	# temporarily do this here until all clients change to call init
+	j: int;
+	im := d.newimage(i.r, Draw->CMAP8, 0, Draw->Black);
+	dx := i.r.max.x-i.r.min.x;
+	dy := i.r.max.y-i.r.min.y;
+	cmap := i.cmap;
+
+	pic := i.chans[0];
+
+	case i.chandesc{
+	RImagefile->CRGB1 =>
+		if(cmap == nil)
+			return (nil, sys->sprint("image has no color map"));
+		if(i.nchans != 1)
+			return (nil, sys->sprint("can't handle nchans %d", i.nchans));
+		for(j=1; j<=8; j++)
+			if(len cmap == 3*(1<<j))
+				break;
+		if(j > 8)
+			return (nil, sys->sprint("can't understand colormap size 3*%d", len cmap/3));
+		if(len cmap != 3*256){
+			# to avoid a range check in inner loop below, make a full-size cmap
+			cmap1 := array[3*256] of byte;
+			cmap1[0:] = cmap[0:];
+			cmap = cmap1;
+			errdiff = 0;	# why not?
+		}
+		if(errdiff == 0){
+			map := array[256] of byte;
+			k := 0;
+			for(j=0; j<256; j++){
+				r := int cmap[k]>>4;
+				g := int cmap[k+1]>>4;
+				b := int cmap[k+2]>>4;
+				k += 3;
+				map[j] = byte 255 - closest[b+16*(g+16*r)];
+			}
+			for(j=0; j<len pic; j++)
+				pic[j] = map[int pic[j]];
+		}else{
+			# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+			ered := array[dx+1] of int;
+			egrn := array[dx+1] of int;
+			eblu := array[dx+1] of int;
+			for(j=0; j<=dx; j++)
+				ered[j] = 0;
+			egrn[0:] = ered[0:];
+			eblu[0:] = ered[0:];
+			p := 0;
+			for(y:=0; y<dy; y++){
+				er := 0;
+				eg := 0;
+				eb := 0;
+				for(x:=0; x<dx; x++){
+					in := 3*int pic[p];
+					r := int cmap[in+0]+ered[x];
+					g := int cmap[in+1]+egrn[x];
+					b := int cmap[in+2]+eblu[x];
+					r1 := clamp[r+64];
+					g1 := clamp[g+64];
+					b1 := clamp[b+64];
+					col := 255 - int closest[b1+16*(g1+16*r1)];
+					pic[p++] = byte col;
+
+					col *= 3;
+					r -= rgbvmap[col+0];
+					t := (3*r)>>4;
+					ered[x] = t+er;
+					ered[x+1] += t;
+					er = r-3*t;
+
+					g -= rgbvmap[col+1];
+					t = (3*g)>>4;
+					egrn[x] = t+eg;
+					egrn[x+1] += t;
+					eg = g-3*t;
+
+					b -= rgbvmap[col+2];
+					t = (3*b)>>4;
+					eblu[x] = t+eb;
+					eblu[x+1] += t;
+					eb = b-3*t;
+				}
+			}
+		}
+	RImagefile->CRGB =>
+		if(i.nchans != 3)
+			return (nil, sys->sprint("RGB image has %d channels", i.nchans));
+		rpic := i.chans[0];
+		gpic := i.chans[1];
+		bpic := i.chans[2];
+		if(errdiff == 0){
+			for(j=0; j<len rpic; j++){
+				r := int rpic[j]>>4;
+				g := int gpic[j]>>4;
+				b := int bpic[j]>>4;
+				pic[j] = byte 255 - byte closest[b+16*(g+16*r)];
+			}
+		}else{
+			# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+			ered := array[dx+1] of int;
+			egrn := array[dx+1] of int;
+			eblu := array[dx+1] of int;
+			for(j=0; j<=dx; j++)
+				ered[j] = 0;
+			egrn[0:] = ered[0:];
+			eblu[0:] = ered[0:];
+			p := 0;
+			for(y:=0; y<dy; y++){
+				er := 0;
+				eg := 0;
+				eb := 0;
+				for(x:=0; x<dx; x++){
+					r := int rpic[p]+ered[x];
+					g := int gpic[p]+egrn[x];
+					b := int bpic[p]+eblu[x];
+					r1 := clamp[r+64];
+					g1 := clamp[g+64];
+					b1 := clamp[b+64];
+					col := 255 - int closest[b1+16*(g1+16*r1)];
+					pic[p++] = byte col;
+
+					col *= 3;
+					r -= rgbvmap[col+0];
+					t := (3*r)>>4;
+					ered[x] = t+er;
+					ered[x+1] += t;
+					er = r-3*t;
+
+					g -= rgbvmap[col+1];
+					t = (3*g)>>4;
+					egrn[x] = t+eg;
+					egrn[x+1] += t;
+					eg = g-3*t;
+
+					b -= rgbvmap[col+2];
+					t = (3*b)>>4;
+					eblu[x] = t+eb;
+					eblu[x+1] += t;
+					eb = b-3*t;
+				}
+			}
+		}
+	RImagefile->CY =>
+		if(i.nchans != 1)
+			return (nil, sys->sprint("Y image has %d chans", i.nchans));
+		rpic := i.chans[0];
+		if(errdiff == 0){
+			for(j=0; j<len pic; j++){
+				r := int rpic[j]>>4;
+				pic[j] = byte 255 - byte closest[r+16*(r+16*r)];
+			}
+		}else{
+			# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+			ered := array[dx+1] of int;
+			for(j=0; j<=dx; j++)
+				ered[j] = 0;
+			p := 0;
+			for(y:=0; y<dy; y++){
+				er := 0;
+				for(x:=0; x<dx; x++){
+					r := int rpic[p]+ered[x];
+					r1 := clamp[r+64];
+					col := 255-int closest[r1+16*(r1+16*r1)];
+					pic[p++] = byte col;
+
+					col *= 3;
+					r -= rgbvmap[col+0];
+					t := (3*r)>>4;
+					ered[x] = t+er;
+					ered[x+1] += t;
+					er = r-3*t;
+				}
+			}
+		}
+	}
+	im.writepixels(im.r, pic);
+	return (im, "");
+}
--- /dev/null
+++ b/appl/lib/inflate.b
@@ -1,0 +1,820 @@
+# gzip-compatible decompression filter.
+
+implement Filter;
+
+include "sys.m";
+	sys:	Sys;
+include "filter.m";
+
+GZMAGIC1:	con byte 16r1f;
+GZMAGIC2:	con byte 16r8b;
+
+GZDEFLATE:	con byte 8;
+
+GZFTEXT:	con 1 << 0;		# file is text
+GZFHCRC:	con 1 << 1;		# crc of header included
+GZFEXTRA:	con 1 << 2;		# extra header included
+GZFNAME:	con 1 << 3;		# name of file included
+GZFCOMMENT:	con 1 << 4;		# header comment included
+GZFMASK:	con (1 << 5) - 1;	# mask of specified bits
+
+GZXBEST:	con byte 2;		# used maximum compression algorithm
+GZXFAST:	con byte 4;		# used fast algorithm little compression
+
+GZOSFAT:	con byte 0;		# FAT file system
+GZOSAMIGA:	con byte 1;		# Amiga
+GZOSVMS:	con byte 2;		# VMS or OpenVMS
+GZOSUNIX:	con byte 3;		# Unix
+GZOSVMCMS:	con byte 4;		# VM/CMS
+GZOSATARI:	con byte 5;		# Atari TOS
+GZOSHPFS:	con byte 6;		# HPFS file system
+GZOSMAC:	con byte 7;		# Macintosh
+GZOSZSYS:	con byte 8;		# Z-System
+GZOSCPM:	con byte 9;		# CP/M
+GZOSTOPS20:	con byte 10;		# TOPS-20
+GZOSNTFS:	con byte 11;		# NTFS file system
+GZOSQDOS:	con byte 12;		# QDOS
+GZOSACORN:	con byte 13;		# Acorn RISCOS
+GZOSUNK:	con byte 255;
+
+GZCRCPOLY:	con int 16redb88320;
+GZOSINFERNO:	con GZOSUNIX;
+
+# huffman code table
+Huff: adt
+{
+	bits:		int;		# length of the code
+	encode:		int;		# the code
+};
+
+# huffman decode table
+DeHuff: adt
+{
+	l1:		array of L1;	# the table
+	nb1:		int;		# no. of bits in first level
+	nb2:		int;		# no. of bits in second level
+};
+
+# first level of decode table
+L1: adt
+{
+	bits:		int;		# length of the code
+	decode:		int;		# the symbol
+	l2:		array of L2;
+};
+
+# second level
+L2: adt
+{
+	bits:		int;		# length of the code
+	decode:		int;		# the symbol
+};
+
+DeflateUnc:	con 0;			# uncompressed block
+DeflateFix:	con 1;			# fixed huffman codes
+DeflateDyn:	con 2;			# dynamic huffman codes
+DeflateErr:	con 3;			# reserved BTYPE (error)
+
+DeflateEob:	con 256;		# end of block code in lit/len book
+
+LenStart:	con 257;		# start of length codes in litlen
+LenEnd:		con 285;		# greatest valid length code
+Nlitlen:	con 288;		# number of litlen codes
+Noff:		con 30;			# number of offset codes
+Nclen:		con 19;			# number of codelen codes
+
+MaxHuffBits:	con 15;			# max bits in a huffman code
+RunlenBits:	con 7;			# max bits in a run-length huffman code
+MaxOff:		con 32*1024;		# max lempel-ziv distance
+
+Blocksize: con 32 * 1024;
+
+# tables from RFC 1951, section 3.2.5
+litlenbase := array[Noff] of
+{
+	3, 4, 5, 6, 7, 8, 9, 10, 11, 13,
+	15, 17, 19, 23, 27, 31, 35, 43, 51, 59,
+	67, 83, 99, 115, 131, 163, 195, 227, 258
+};
+
+litlenextra := array[Noff] of
+{
+	0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
+	2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4,
+	5, 5, 5, 5, 0
+};
+
+offbase := array[Noff] of
+{
+	1, 2, 3, 4, 5, 7, 9, 13, 17, 25,
+	33, 49, 65, 97, 129, 193, 257, 385, 513, 769,
+	1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577
+};
+
+offextra := array[Noff] of
+{
+	0,  0,  0,  0,  1,  1,  2,  2,  3,  3,
+	4,  4,  5,  5,  6,  6,  7,  7,  8,  8,
+	9,  9,  10, 10, 11, 11, 12, 12, 13, 13
+};
+
+# order of run-length codes
+clenorder := array[Nclen] of
+{
+	16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
+};
+
+# fixed huffman tables
+litlentab: array of Huff;
+offtab: array of Huff;
+
+# their decoding table counterparts
+litlendec: ref DeHuff;
+offdec: ref DeHuff;
+
+revtab: array of byte;	# bit reversal for endian swap of huffman codes
+mask: array of int;		# for masking low-order n bits of an int
+
+Hnone, Hgzip, Hzlib: con iota;  # State.headers
+State: adt {
+	ibuf, obuf: array of byte;
+	c: chan of ref Rq;
+	rc: chan of int;
+	in: int;		# next byte to consume from input buffer
+	ein: int;		# valid bytes in input buffer
+	out: int;		# valid bytes in output buffer
+	hist: array of byte;	# history buffer for lempel-ziv backward references
+	usehist: int;		# == 1 if 'hist' is valid
+	crctab: array of int;
+	crc, tot: int;		# for gzip trailer
+	sum:	big;		# for zlib trailer
+	
+	reg: int;		# 24-bit shift register
+	nbits: int;		# number of valid bits in reg
+	svreg: int;		# save reg for efficient ungets
+	svn: int;		# number of bits gotten in last call to getn()
+	# reg bits are consumed from right to left
+	# so low-order byte of reg came first in the input stream
+	headers: int;
+};
+
+
+init()
+{
+	sys = load Sys Sys->PATH;
+
+	# byte reverse table
+	revtab = array[256] of byte;
+	for(i := 0; i < 256; i++){
+		revtab[i] = byte 0;
+		for(j := 0; j < 8; j++) {
+			if(i & (1 << j))
+				revtab[i] |= byte 16r80 >> j;
+		}
+	}
+
+	# bit-masking table
+	mask = array[MaxHuffBits+1] of int;
+	for(i = 0; i <= MaxHuffBits; i++)
+		mask[i] = (1 << i) - 1;
+
+	litlentab = array[Nlitlen] of Huff;
+
+	# static litlen bit lengths
+	for(i = 0; i < 144; i++)
+		litlentab[i].bits = 8;
+	for(i = 144; i < 256; i++)
+		litlentab[i].bits = 9;
+	for(i = 256; i < 280; i++)
+		litlentab[i].bits = 7;
+	for(i = 280; i < Nlitlen; i++)
+		litlentab[i].bits = 8;
+
+	bitcount := array[MaxHuffBits+1] of { * => 0 };
+	bitcount[8] += 144 - 0;
+	bitcount[9] += 256 - 144;
+	bitcount[7] += 280 - 256;
+	bitcount[8] += Nlitlen - 280;
+
+	hufftabinit(litlentab, Nlitlen, bitcount, 9);
+	litlendec = decodeinit(litlentab, Nlitlen, 9, 0);
+
+	offtab = array[Noff] of Huff;
+
+	# static offset bit lengths
+	for(i = 0; i < Noff; i++)
+		offtab[i].bits = 5;
+
+	for(i = 0; i < 5; i++)
+		bitcount[i] = 0;
+	bitcount[5] = Noff;
+
+	hufftabinit(offtab, Noff, bitcount, 5);
+	offdec = decodeinit(offtab, Noff, 5, 0);
+}
+
+start(params: string): chan of ref Rq
+{
+	s := ref State;
+	s.c = chan of ref Rq;
+	s.rc = chan of int;
+	s.ibuf = array[Blocksize] of byte;
+	s.obuf = array[Blocksize] of byte;
+	s.in = 0;
+	s.ein = 0;
+	s.out = 0;
+	s.usehist = 0;
+	s.reg = 0;
+	s.nbits = 0;
+	s.crc = 0;
+	s.tot = 0;
+	s.sum = big 1;
+	s.hist = array[Blocksize] of byte;
+	s.headers = Hnone;
+	if(params != nil) {
+		if(params[0] == 'h')
+			s.headers = Hgzip;
+		if(params[0] == 'z')
+			s.headers = Hzlib;
+	}
+	if (s.headers == Hgzip)
+		s.crctab = mkcrctab(GZCRCPOLY);
+	spawn inflate(s);
+	return s.c;
+}
+
+inflate(s: ref State)
+{
+	s.c <-= ref Rq.Start(sys->pctl(0, nil));
+	header(s);
+
+	for(;;) {
+		bfinal := getn(s, 1, 0);
+		btype := getn(s, 2, 0);
+		case(btype) {
+		DeflateUnc =>
+			flushbits(s);
+			unclen := getb(s);
+			unclen |= getb(s) << 8;
+			nlen := getb(s);
+			nlen |= getb(s) << 8;
+			if(unclen != (~nlen & 16rFFFF))
+				fatal(s, "corrupted data");
+			for(; unclen > 0; unclen--) {
+				# inline putb(s, getb(s));
+				b := byte getb(s);
+				if(s.out >= MaxOff)
+					flushout(s);
+				s.obuf[s.out++] = b;
+			}
+		DeflateFix =>
+			decodeblock(s, litlendec, offdec);
+		DeflateDyn =>
+			dynhuff(s);
+		DeflateErr =>
+			fatal(s, "bad block type");
+		}
+		if(bfinal) {
+			if(s.out) {
+				outblock(s);
+				s.c <- = ref Rq.Result(s.obuf[0:s.out], s.rc);
+				flag := <- s.rc;
+				if (flag == -1)
+					exit;
+			}
+			flushbits(s);
+			footer(s);
+			s.c <-= ref Rq.Finished(s.ibuf[s.in - s.nbits/8:s.ein]);
+			exit;
+		}
+	}
+}
+
+headergzip(s: ref State)
+{
+	if(byte getb(s) != GZMAGIC1 || byte getb(s) != GZMAGIC2)
+		fatal(s, "not a gzip file");
+
+	if(byte getb(s) != GZDEFLATE)
+		fatal(s, "not compressed with deflate");
+
+	flags := getb(s);
+	if(flags & ~GZFMASK)
+		fatal(s, "reserved flag bits set");
+
+	# read modification time (ignored)
+	mtime := getb(s);
+	mtime |= (getb(s) << 8);
+	mtime |= (getb(s) << 16);
+	mtime |= (getb(s) << 24);
+	s.c <-= ref Rq.Info("mtime " + string mtime);
+	getb(s);	# xfl
+	getb(s);	# os
+
+	# skip optional "extra field"
+	if(flags & GZFEXTRA) {
+		skip := getb(s);
+		skip |= getb(s) << 8;
+		while (skip-- > 0)
+			getb(s);
+	}
+
+	# read optional filename (ignored)
+	file: string;
+	if(flags & GZFNAME){
+		n := 0;
+		while(c := getb(s))
+			file[n++] = c;
+		s.c <-= ref Rq.Info("file " + file);
+	}
+
+	# skip optional comment
+	if(flags & GZFCOMMENT) {
+		while(getb(s))
+			;
+	}
+
+	# skip optional CRC16 field
+	if(flags & GZFHCRC) {
+		getb(s);
+		getb(s);
+	}
+}
+
+headerzlib(s: ref State)
+{
+	Fdict:		con 1<<5;
+	CMshift:	con 8;
+	CMmask:		con (1<<4)-1;
+	CMdeflate:	con 8;
+
+	h := 0;
+	h |= getb(s)<<8;
+	h |= getb(s);
+	if(h % 31 != 0)
+		fatal(s, "invalid zlib header");
+	if(h&Fdict)
+		fatal(s, "preset dictionary not supported");
+	if(((h>>CMshift)&CMmask) != CMdeflate)
+		fatal(s, "zlib compression method not deflate");
+}
+
+header(s: ref State)
+{
+	case s.headers {
+	Hgzip =>	headergzip(s);
+	Hzlib =>	headerzlib(s);
+	}
+}
+
+footergzip(s: ref State)
+{
+	fcrc := getword(s);
+	if(s.crc != fcrc)
+		fatal(s, sys->sprint("crc mismatch: computed %ux, expected %ux", s.crc, fcrc));
+	ftot := getword(s);
+	if(s.tot != ftot)
+		fatal(s, sys->sprint("byte count mismatch: computed %d, expected %d", s.tot, ftot));
+}
+
+footerzlib(s: ref State)
+{
+	sum := big 0;
+	sum = (sum<<8)|big getb(s);
+	sum = (sum<<8)|big getb(s);
+	sum = (sum<<8)|big getb(s);
+	sum = (sum<<8)|big getb(s);
+	if(sum != s.sum)
+		fatal(s, sys->sprint("adler32 mismatch: computed %bux, expected %bux", s.sum, sum));
+}
+
+footer(s: ref State)
+{
+	case s.headers {
+	Hgzip =>	footergzip(s);
+	Hzlib =>	footerzlib(s);
+	}
+}
+
+getword(s: ref State): int
+{
+	n := 0;
+	for(i := 0; i < 4; i++)
+		n |= getb(s) << (8 * i);
+	return n;
+}
+
+#
+# uncompress a block using given huffman decoding tables
+#
+decodeblock(s: ref State, litlendec, offdec: ref DeHuff)
+{
+	b: byte;
+
+	for(;;) {
+		sym := decodesym(s, litlendec);
+		if(sym < DeflateEob) {		# literal byte
+			# inline putb(s, byte sym);
+			b = byte sym;
+			if(s.out >= MaxOff)
+				flushout(s);
+			s.obuf[s.out++] = b;
+		} else if(sym == DeflateEob) {	# End-of-block
+			break;
+		} else {			# lempel-ziv <length, distance>
+			if(sym > LenEnd)
+				fatal(s, "symbol too long");
+			xbits := litlenextra[sym - LenStart];
+			xtra := 0;
+			if(xbits)
+				xtra = getn(s, xbits, 0);
+			length := litlenbase[sym - LenStart] + xtra;
+
+			sym = decodesym(s, offdec);
+			if(sym >= Noff)
+				fatal(s, "symbol too long");
+			xbits = offextra[sym];
+			if(xbits)
+				xtra = getn(s, xbits, 0);
+			else
+				xtra = 0;
+			dist := offbase[sym] + xtra;
+			if(dist > s.out && s.usehist == 0)
+				fatal(s, "corrupted data");
+			for(i := 0; i < length; i++) {
+				# inline putb(lzbyte(dist));
+				ix := s.out - dist;
+				if(dist <= s.out)
+					b = s.obuf[ix];
+				else
+					b = s.hist[MaxOff + ix];
+				if(s.out >= MaxOff)
+					flushout(s);
+				s.obuf[s.out++] = b;
+			}
+		}
+	}
+}
+
+#
+# decode next symbol in input stream using given huffman decoding table
+#
+decodesym(s: ref State, dec: ref DeHuff): int
+{
+	code, bits, n: int;
+
+	l1 := dec.l1;
+	nb1 := dec.nb1;
+	nb2 := dec.nb2;
+
+	code = getn(s, nb1, 1);
+	l2 := l1[code].l2;
+	if(l2 == nil) {		# first level table has answer
+		bits = l1[code].bits;
+		if(bits == 0)
+			fatal(s, "corrupt data");
+		if(nb1 > bits) {
+			# inline ungetn(nb1 - bits);
+			n = nb1 - bits;
+			s.reg = s.svreg >> (s.svn - n);
+			s.nbits += n;
+		}
+		return l1[code].decode;
+	}
+	# must advance to second-level table
+	code = getn(s, nb2, 1);
+	bits = l2[code].bits;
+	if(bits == 0)
+		fatal(s, "corrupt data");
+	if(nb1 + nb2 > bits) {
+		# inline ungetn(nb1 + nb2 - bits);
+		n = nb1 + nb2 - bits;
+		s.reg = s.svreg >> (s.svn - n);
+		s.nbits += n;
+	}
+	return l2[code].decode;
+}
+
+#
+# uncompress a block that was encoded with dynamic huffman codes
+# RFC 1951, section 3.2.7
+#
+dynhuff(s: ref State)
+{
+	hlit := getn(s, 5, 0) + 257;
+	hdist := getn(s, 5, 0) + 1;
+	hclen := getn(s, 4, 0) + 4;
+	if(hlit > Nlitlen || hlit < 257 || hdist > Noff)
+		fatal(s, "corrupt data");
+
+	runlentab := array[Nclen] of { * => Huff(0, 0) };
+	count := array[RunlenBits+1] of { * => 0 };
+	for(i := 0; i < hclen; i++) {
+		nb := getn(s, 3, 0);
+		if(nb) {
+			runlentab[clenorder[i]].bits = nb;
+			count[nb]++;
+		}
+	}
+	hufftabinit(runlentab, Nclen, count, RunlenBits);
+	runlendec := decodeinit(runlentab, Nclen, RunlenBits, 0);
+	if(runlendec == nil)
+		fatal(s, "corrupt data");
+
+	lengths := decodelen(s, runlendec, hlit+hdist);
+	if(lengths == nil)
+		fatal(s, "corrupt length table");
+
+	dlitlendec := decodedyn(s, lengths[0:hlit], hlit, 9);
+	doffdec := decodedyn(s, lengths[hlit:], hdist, 5);
+	decodeblock(s, dlitlendec, doffdec);
+}
+
+#
+# return the decoded combined length table for literal and distance alphabets
+#
+decodelen(s: ref State, runlendec: ref DeHuff, nlen: int): array of int
+{
+	lengths := array[nlen] of int;
+	for(n := 0; n < nlen;) {
+		nb := decodesym(s, runlendec);
+		nr := 1;
+		case nb {
+		0 to 15 =>
+			;
+		16 =>
+			nr = getn(s, 2, 0) + 3;
+			if(n == 0)
+				return nil;
+			nb = lengths[n-1];
+		17 =>
+			nr = getn(s, 3, 0) + 3;
+			nb = 0;
+		18 =>
+			nr = getn(s, 7, 0) + 11;
+			nb = 0;
+		* =>
+			return nil;
+		}
+		if(n+nr > nlen)
+			return nil;
+		while(--nr >= 0)
+			lengths[n++] = nb;
+	}
+	return lengths;
+}
+
+#
+# (1) read a dynamic huffman code from the input stream
+# (2) decode it using the run-length huffman code
+# (3) return the decode table for the dynamic huffman code
+#
+decodedyn(s: ref State, lengths: array of int, nlen, nb1: int): ref DeHuff
+{
+	hufftab := array[nlen] of Huff;
+	count := array[MaxHuffBits+1] of { * => 0 };
+
+	maxnb := 0;
+	for(n := 0; n < nlen; n++) {
+		c := lengths[n];
+		if(c) {
+			hufftab[n].bits = c;
+			count[c]++;
+			if(c > maxnb)
+				maxnb = c;
+		}else
+			hufftab[n].bits = 0;
+		hufftab[n].encode = 0;
+	}
+	hufftabinit(hufftab, nlen, count, maxnb);
+	nb2 := 0;
+	if(maxnb > nb1)
+		nb2 = maxnb - nb1;
+	d := decodeinit(hufftab, nlen, nb1, nb2);
+	if (d == nil)
+		fatal(s, "decodeinit failed");
+	return d;
+}
+
+#
+# RFC 1951, section 3.2.2
+#
+hufftabinit(tab: array of Huff, n: int, bitcount: array of int, nbits: int)
+{
+	nc := array[MaxHuffBits+1] of int;
+
+	code := 0;
+	for(bits := 1; bits <= nbits; bits++) {
+		code = (code + bitcount[bits-1]) << 1;
+		nc[bits] = code;
+	}
+
+	for(i := 0; i < n; i++) {
+		bits = tab[i].bits;
+		# differences from Deflate module:
+		#  (1) leave huffman code right-justified in encode
+		#  (2) don't reverse it
+		if(bits != 0)
+			tab[i].encode = nc[bits]++;
+	}
+}
+
+#
+# convert 'array of Huff' produced by hufftabinit()
+# into 2-level lookup table for decoding
+#
+# nb1(nb2): number of bits handled by first(second)-level table
+#
+decodeinit(tab: array of Huff, n, nb1, nb2: int): ref DeHuff
+{
+	i, j, k, d: int;
+
+	dehuff := ref DeHuff(array[1<<nb1] of { * => L1(0, 0, nil) }, nb1, nb2);
+	l1 := dehuff.l1;
+	for(i = 0; i < n; i++) {
+		bits := tab[i].bits;
+		if(bits == 0)
+			continue;
+		l1x := tab[i].encode;
+		if(l1x >= (1 << bits))
+			return nil;
+		if(bits <= nb1) {
+			d = nb1 - bits;
+			l1x <<= d;
+			k = l1x + mask[d];
+			for(j = l1x; j <= k; j++) {
+				l1[j].decode = i;
+				l1[j].bits = bits;
+			}
+			continue;
+		}
+		# advance to second-level table
+		d = bits - nb1;
+		l2x := l1x & mask[d];
+		l1x >>= d;
+		if(l1[l1x].l2 == nil)
+			l1[l1x].l2 = array[1<<nb2] of { * => L2(0, 0) };
+		l2 := l1[l1x].l2;
+		d = (nb1 + nb2) - bits;
+		l2x <<= d;
+		k = l2x + mask[d];
+		for(j = l2x; j <= k; j++) {
+			l2[j].decode = i;
+			l2[j].bits = bits;
+		}
+	}
+
+	return dehuff;
+}
+
+#
+# get next byte from reg
+# assumptions:
+#  (1) flushbits() has been called
+#  (2) ungetn() won't be called after a getb()
+#
+getb(s: ref State): int
+{
+	if(s.nbits < 8)
+		need(s, 8);
+	b := byte s.reg;
+	s.reg >>= 8;
+	s.nbits -= 8;
+	return int b;
+}
+
+#
+# get next n bits from reg; if r != 0, reverse the bits
+#
+getn(s: ref State, n, r: int): int
+{
+	if(s.nbits < n)
+		need(s, n);
+	s.svreg = s.reg;
+	s.svn = n;
+	i := s.reg & mask[n];
+	s.reg >>= n;
+	s.nbits -= n;
+	if(r) {
+		if(n <= 8) {
+			i = int revtab[i];
+			i >>= 8 - n;
+		} else {
+			i = ((int revtab[i & 16rff]) << 8)
+				| (int revtab[i >> 8]);
+			i >>= 16 - n;
+		}
+	}
+	return i;
+}
+
+#
+# ensure that at least n bits are available in reg
+#
+need(s: ref State, n: int)
+{
+	while(s.nbits < n) {
+		if(s.in >= s.ein) {
+			s.c <-= ref Rq.Fill(s.ibuf, s.rc);
+			s.ein = <- s.rc;
+			if (s.ein < 0)
+				exit;
+			if (s.ein == 0)
+				fatal(s, "premature end of stream");
+			s.in = 0;
+		}
+		s.reg = ((int s.ibuf[s.in++]) << s.nbits) | s.reg;
+		s.nbits += 8;
+	}
+}
+
+#
+# if partial byte consumed from reg, dispose of remaining bits
+#
+flushbits(s: ref State)
+{
+	drek := s.nbits % 8;
+	if(drek) {
+		s.reg >>= drek;
+		s.nbits -= drek;
+	}
+}
+
+#
+# output buffer is full, so flush it
+#
+flushout(s: ref State)
+{
+	outblock(s);
+	s.c <-= ref Rq.Result(s.obuf[0:s.out], s.rc);
+	flag := <- s.rc;
+	if (flag == -1)
+		exit;
+	buf := s.hist;
+	s.hist = s.obuf;
+	s.usehist = 1;
+	s.obuf = buf;
+	s.out = 0;
+}
+
+mkcrctab(poly: int): array of int
+{
+	crctab := array[256] of int;
+	for(i := 0; i < 256; i++){
+		crc := i;
+		for(j := 0; j < 8; j++){
+			c := crc & 1;
+			crc = (crc >> 1) & 16r7fffffff;
+			if(c)
+				crc ^= poly;
+		}
+		crctab[i] = crc;
+	}
+	return crctab;
+}
+
+outblockgzip(s: ref State)
+{
+	buf := s.obuf;
+	n := s.out;
+	crc := s.crc;
+	crc ^= int 16rffffffff;
+	for(i := 0; i < n; i++)
+		crc = s.crctab[int(byte crc ^ buf[i])] ^ ((crc >> 8) & 16r00ffffff);
+	s.crc = crc ^ int 16rffffffff;
+	s.tot += n;
+}
+
+outblockzlib(s: ref State)
+{
+	ZLADLERBASE:	con big 65521;
+
+	buf := s.obuf;
+	n := s.out;
+
+	s1 := s.sum & big 16rffff;
+	s2 := (s.sum>>16) & big 16rffff;
+
+	for(i := 0; i < n; i++) {
+		s1 = (s1 + big buf[i]) % ZLADLERBASE;
+		s2 = (s2 + s1) % ZLADLERBASE;
+	}
+	s.sum = (s2<<16) + s1;
+}
+
+outblock(s: ref State)
+{
+	case s.headers {
+	Hgzip =>	outblockgzip(s);
+	Hzlib =>	outblockzlib(s);
+	}
+}
+
+#
+# irrecoverable error; invariably denotes data corruption
+#
+fatal(s: ref State, e: string)
+{
+	s.c <-= ref Rq.Error(e);
+	exit;
+}
--- /dev/null
+++ b/appl/lib/ip.b
@@ -1,0 +1,656 @@
+implement IP;
+
+#
+# Copyright © 2003,2004 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "ip.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	v4prefix = array[] of {
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 16rFF, byte 16rFF,
+	};
+
+	v4bcast = IPaddr(array[] of {
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 16rFF, byte 16rFF,
+		byte 16rFF, byte 16rFF, byte 16rFF, byte 16rFF,
+	});
+
+	v4allsys = IPaddr(array[] of {
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 16rFF, byte 16rFF,
+		byte 16rE0, byte 0, byte 0, byte 16r01,
+	});
+
+	v4allrouter = IPaddr(array[] of {
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 16rFF, byte 16rFF,
+		byte 16rE0, byte 0, byte 0, byte 16r02,
+	});
+
+	v4noaddr = IPaddr(array[] of {
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 16rFF, byte 16rFF,
+		byte 0, byte 0, byte 0, byte 0,
+	});
+
+	selfv6 = IPaddr(array[] of {
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 1,
+	});
+
+	selfv4 = IPaddr(array[] of {
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 0, byte 0,
+		byte 0, byte 0, byte 16rFF, byte 16rFF,
+		byte 127, byte 0, byte 0, byte 1,
+	});
+
+	noaddr = IPaddr(array[] of {0 to IPaddrlen-1 => byte 0});
+	allbits = IPaddr(array[] of {0 to IPaddrlen-1 => byte 16rFF});
+}
+
+IPaddr.newv6(a: array of byte): IPaddr
+{
+	b := array[IPaddrlen] of byte;
+	b[0:] = a[0:IPaddrlen];
+	return IPaddr(b);
+}
+
+IPaddr.newv4(a: array of byte): IPaddr
+{
+	b := array[IPaddrlen] of byte;
+	b[0:] = v4prefix;
+	b[IPv4off:] = a[0:IPv4addrlen];
+	return IPaddr(b);
+}
+
+IPaddr.copy(ip: self IPaddr): IPaddr
+{
+	if(ip.a == nil)
+		return noaddr.copy();
+	a := array[IPaddrlen] of byte;
+	a[0:] = ip.a;
+	return IPaddr(a);
+}
+
+IPaddr.eq(ip: self IPaddr, v: IPaddr): int
+{
+	a := ip.a;
+	if(a == nil)
+		a = noaddr.a;
+	b := v.a;
+	if(b == nil)
+		b = noaddr.a;
+	for(i := 0; i < IPaddrlen; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+IPaddr.mask(a1: self IPaddr, a2: IPaddr): IPaddr
+{
+	c := array[IPaddrlen] of byte;
+	for(i := 0; i < IPaddrlen; i++)
+		c[i] = a1.a[i] & a2.a[i];
+	return IPaddr(c);
+}
+
+IPaddr.maskn(a1: self IPaddr, a2: IPaddr): IPaddr
+{
+	c := array[IPaddrlen] of byte;
+	for(i := 0; i < IPaddrlen; i++)
+		c[i] = a1.a[i] & ~a2.a[i];
+	return IPaddr(c);
+}
+
+IPaddr.isv4(ip: self IPaddr): int
+{
+	for(i := 0; i < IPv4off; i++)
+		if(ip.a[i] != v4prefix[i])
+			return 0;
+	return 1;
+}
+
+IPaddr.ismulticast(ip: self IPaddr): int
+{
+	if(ip.isv4()){
+		v := int ip.a[IPv4off];
+		return v >= 16rE0 && v < 16rF0 || ip.eq(v4bcast);	# rfc1112
+	}
+	return ip.a[0] == byte 16rFF;
+}
+
+IPaddr.isvalid(ip: self IPaddr): int
+{
+	return !ip.eq(noaddr) && !ip.eq(v4noaddr);
+}
+
+IPaddr.v4(ip: self IPaddr): array of byte
+{
+	if(!ip.isv4() && !ip.eq(noaddr))
+		return nil;
+	a := array[4] of byte;
+	for(i := 0; i < 4; i++)
+		a[i] = ip.a[IPv4off+i];
+	return a;
+}
+
+IPaddr.v6(ip: self IPaddr): array of byte
+{
+	a := array[IPaddrlen] of byte;
+	a[0:] = ip.a;
+	return a;
+}
+
+IPaddr.class(ip: self IPaddr): int
+{
+	if(!ip.isv4())
+		return 6;
+	return int ip.a[IPv4off]>>6;
+}
+
+IPaddr.classmask(ip: self IPaddr): IPaddr
+{
+	m := allbits.copy();
+	if(!ip.isv4())
+		return m;
+	if((n := ip.class()) == 0)
+		n = 1;
+	for(i := IPaddrlen-4+n; i < IPaddrlen; i++)
+		m.a[i] = byte 0;
+	return m;
+}
+
+#
+# rfc2373
+#
+
+IPaddr.parse(s: string): (int, IPaddr)
+{
+	a := noaddr.copy();
+	col := 0;
+	gap := 0;
+	for(i:=0; i<IPaddrlen && s != ""; i+=2){
+		c := 'x';
+		v := 0;
+		for(m := 0; m < len s && (c = s[m]) != '.' && c != ':'; m++){
+			d := 0;
+			if(c >= '0' && c <= '9')
+				d = c-'0';
+			else if(c >= 'a' && c <= 'f')
+				d = c-'a'+10;
+			else if(c >= 'A' && c <= 'F')
+				d = c-'A'+10;
+			else
+				return (-1, a);
+			v = (v<<4) | d;
+		}
+		if(c == '.'){
+			if(parseipv4(a.a[i:], s) < 0)
+				return (-1, noaddr.copy());
+			i += IPv4addrlen;
+			break;
+		}
+		if(v > 16rFFFF)
+			return (-1, a);
+		a.a[i] = byte (v>>8);
+		a.a[i+1] = byte v;
+		if(c == ':'){
+			col = 1;
+			if(++m < len s && s[m] == ':'){
+				if(gap > 0)
+					return (-1, a);
+				gap = i+2;
+				m++;
+			}
+		}
+		s = s[m:];
+	}
+	if(i < IPaddrlen){	# mind the gap
+		ns := i-gap;
+		for(j := 1; j <= ns; j++){
+			a.a[IPaddrlen-j] = a.a[i-j];
+			a.a[i-j] = byte 0;
+		}
+	}
+	if(!col)
+		a.a[0:] = v4prefix;
+	return (0, IPaddr(a));
+}
+
+IPaddr.parsemask(s: string): (int, IPaddr)
+{
+	return parsemask(s, 128);
+}
+
+IPaddr.parsecidr(s: string): (int, IPaddr, IPaddr)
+{
+	for(i := 0; i < len s && s[i] != '/'; i++)
+		;
+	(ok, a) := IPaddr.parse(s[0:i]);
+	if(i < len s){
+		(ok2, m) := IPaddr.parsemask(s[i:]);
+		if(ok < 0 || ok2 < 0)
+			return (-1, a, m);
+		return (0, a, m);
+	}
+	return (ok, a, allbits.copy());
+}
+
+parseipv4(b: array of byte, s: string): int
+{
+	a := array[4] of {* => 0};
+	o := 0;
+	for(i := 0; i < 4 && o < len s; i++){
+		for(m := o; m < len s && (c := s[m]) != '.'; m++)
+			if(!(c >= '0' && c <= '9'))
+				return -1;
+		if(m == o)
+			return -1;
+		a[i] = int big s[o:m];
+		b[i] = byte a[i];
+		if(m < len s && s[m] == '.')
+			m++;
+		o = m;
+	}
+	case i {
+	1 =>		# 32 bit
+		b[0] = byte (a[0] >> 24);
+		b[1] = byte (a[0] >> 16);
+		b[2] = byte (a[0] >> 8);
+		b[3] = byte a[0];
+	2 =>
+		if(a[0] < 256){	# 8/24
+			b[0] = byte a[0];
+			b[1] = byte (a[1]>>16);
+			b[2] = byte (a[1]>>8);
+		}else if(a[0] < 65536){	# 16/16
+			b[0] = byte (a[0]>>8);
+			b[1] = byte a[0];
+			b[2] = byte (a[1]>>16);
+		}else{	# 24/8
+			b[0] = byte (a[0]>>16);
+			b[1] = byte (a[0]>>8);
+			b[2] = byte a[0];
+		}
+		b[3] = byte a[1];
+	3 =>		# 8/8/16
+		b[0] = byte a[0];
+		b[1] = byte a[1];
+		b[2] = byte (a[2]>>16);
+		b[3] = byte a[2];
+	}
+	return 0;
+}
+
+parsemask(s: string, abits: int): (int, IPaddr)
+{
+	m := allbits.copy();
+	if(s == nil)
+		return (0, m);
+	if(s[0] != '/'){
+		(ok, a) := IPaddr.parse(s);
+		if(ok < 0)
+			return (0, m);
+		if(a.isv4())
+			a.a[0:] = m.a[0:IPv4off];
+		return (0, a);
+	}
+	if(len s == 1)
+		return (0, m);
+	nbit := int s[1:];
+	if(nbit < 0)
+		return (-1, m);
+	if(nbit > abits)
+		return (0, m);
+	nbit = abits-nbit;
+	i := IPaddrlen;
+	for(; nbit >= 8; nbit -= 8)
+		m.a[--i] = byte 0;
+	if(nbit > 0)
+		m.a[i-1] &= byte (~0<<nbit);
+	return (0, m);
+}
+
+IPaddr.text(a: self IPaddr): string
+{
+	b := a.a;
+	if(b == nil)
+		return "::";
+	if(a.isv4())
+		return sys->sprint("%d.%d.%d.%d", int b[IPv4off], int b[IPv4off+1], int b[IPv4off+2], int b[IPv4off+3]);
+	cs := -1;
+	nc := 0;
+	for(i:=0; i<IPaddrlen; i+=2)
+		if(int b[i] == 0 && int b[i+1] == 0){
+			for(j:=i+2; j<IPaddrlen; j+=2)
+				if(int b[j] != 0 || int b[j+1] != 0)
+					break;
+			if(j-i > nc){
+				nc = j-i;
+				cs = i;
+			}
+		}
+	if(nc <= 2)
+		cs = -1;
+	s := "";
+	for(i=0; i<IPaddrlen; ){
+		if(i == cs){
+			s += "::";
+			i += nc;
+		}else{
+			if(s != "" && s[len s-1]!=':')
+				s[len s] = ':';
+			v := (int a.a[i] << 8) | int a.a[i+1];
+			s += sys->sprint("%ux", v);
+			i += 2;
+		}
+	}
+	return s;
+}
+
+IPaddr.masktext(a: self IPaddr): string
+{
+	b := a.a;
+	if(b == nil)
+		return "/0";
+	for(i:=0; i<IPaddrlen; i++)
+		if(i == IPv4off)
+			return sys->sprint("%d.%d.%d.%d", int b[IPv4off], int b[IPv4off+1], int b[IPv4off+2], int b[IPv4off+3]);
+		else if(b[i] != byte 16rFF)
+			break;
+	for(j:=i+1; j<IPaddrlen; j++)
+		if(b[j] != byte 0)
+			return a.text();
+	nbit := 8*i;
+	if(i < IPaddrlen){
+		v := int b[i];
+		for(m := 16r80; m != 0; m >>= 1){
+			if((v & m) == 0)
+				break;
+			v &= ~m;
+			nbit++;
+		}
+		if(v != 0)
+			return a.text();
+	}
+	return sys->sprint("/%d", nbit);
+}
+
+addressesof(ifcs: list of ref Ipifc, all: int): list of IPaddr
+{
+	ra: list of IPaddr;
+	runi: list of IPaddr;
+	for(; ifcs != nil; ifcs = tl ifcs){
+		for(ifcas :=(hd ifcs).addrs; ifcs != nil; ifcs = tl ifcs){
+			a := (hd ifcas).ip;
+			if(all || !(a.eq(noaddr) || a.eq(v4noaddr))){	# ignore unspecified and loopback
+				if(a.ismulticast() || a.eq(selfv4) || a.eq(selfv6))
+					ra = a :: ra;
+				else
+					runi = a :: runi;
+			}
+		}
+	}
+	# unicast first, then others, both sets in order as found
+	# for ipv6, might want to give priority to unicast other than link- and site-local
+	al: list of IPaddr;
+	for(; ra != nil; ra = tl ra)
+		al = hd ra :: al;
+	for(; runi != nil; runi = tl runi)
+		al = hd runi :: al;
+	return al;
+}
+
+interfaceof(l: list of ref Ipifc, ip: IPaddr): (ref Ipifc, ref Ifcaddr)
+{
+	for(; l != nil; l = tl l){
+		ifc := hd l;
+		for(addrs := ifc.addrs; addrs != nil; addrs = tl addrs){
+			a := hd addrs;
+			if(ip.mask(a.mask).eq(a.net))
+				return (ifc, a);
+		}
+	}
+	return (nil, nil);
+}
+
+ownerof(l: list of ref Ipifc, ip: IPaddr): (ref Ipifc, ref Ifcaddr)
+{
+	for(; l != nil; l = tl l){
+		ifc := hd l;
+		for(addrs := ifc.addrs; addrs != nil; addrs = tl addrs){
+			a := hd addrs;
+			if(ip.eq(a.ip))
+				return (ifc, a);
+		}
+	}
+	return (nil, nil);
+}
+
+readipifc(net: string, index: int): (list of ref Ipifc, string)
+{
+	if(net == nil)
+		net = "/net";
+	if(index < 0){
+		ifcs: list of ref Ipifc;
+		dirfd := sys->open(net+"/ipifc", Sys->OREAD);
+		if(dirfd == nil)
+			return (nil, sys->sprint("%r"));
+		err: string;
+		for(;;){
+			(nd, dirs) := sys->dirread(dirfd);
+			if(nd <= 0){
+				if(nd < 0)
+					err = sys->sprint("%r");
+				break;
+			}
+			for(i:=0; i<nd; i++)
+				if((dn := dirs[i].name) != nil && dn[0]>='0' && dn[0]<='9'){
+					index = int dn;
+					ifc := readstatus(net+"/ipifc/"+dn+"/status", index);
+					if(ifc != nil)
+						ifcs = ifc :: ifcs;
+				}
+		}
+		l := ifcs;
+		for(ifcs = nil; l != nil; l = tl l)
+			ifcs = hd l :: ifcs;
+		return (ifcs, err);
+	}
+	ifc := readstatus(net+"/ipifc/"+string index+"/status", index);
+	if(ifc == nil)
+		return (nil, sys->sprint("%r"));
+	return (ifc :: nil, nil);
+}
+
+#
+# return data structure containing values read from status file:
+#
+# device /net/ether0 maxtu 1514 sendra 0 recvra 0 mflag 0 oflag 0 maxraint 600000 minraint 200000 linkmtu 0 reachtime 0 rxmitra 0 ttl 255 routerlt 1800000 pktin 47609 pktout 42322 errin 0 errout 0
+#	144.32.112.83 /119 144.32.112.0 4294967295   4294967295
+#		...
+#
+
+readstatus(file: string, index: int): ref Ipifc
+{
+	fd := sys->open(file, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	contents := slurp(fd);
+	fd = nil;
+	(nline, lines) := sys->tokenize(contents, "\n");
+	if(nline <= 0){
+		sys->werrstr("unexpected ipifc status file format");
+		return nil;
+	}
+	(nil, details) := sys->tokenize(hd lines, " \t\n");
+	lines = tl lines;
+	ifc := ref Ipifc;
+	ifc.index = index;
+	ifc.dev = valof(details, "device");
+	ifc.mtu = int valof(details, "maxtu");
+	ifc.pktin = big valof(details, "pktin");
+	ifc.pktout = big valof(details, "pktout");
+	ifc.errin = big valof(details, "errin");
+	ifc.errout = big valof(details, "errout");
+	ifc.sendra = int valof(details, "sendra");
+	ifc.recvra = int valof(details, "recvra");
+	ifc.rp.mflag = int valof(details, "mflag");
+	ifc.rp.oflag = int valof(details, "oflag");
+	ifc.rp.maxraint = int valof(details, "maxraint");
+	ifc.rp.minraint = int valof(details, "minraint");
+	ifc.rp.linkmtu = int valof(details, "linkmtu");
+	ifc.rp.reachtime = int valof(details, "reachtime");
+	ifc.rp.rxmitra = int valof(details, "rxmitra");
+	ifc.rp.ttl = int valof(details, "ttl");
+	ifc.rp.routerlt = int valof(details, "routerlt");
+	addrs: list of ref Ifcaddr;
+	for(; lines != nil; lines = tl lines){
+		(nf, fields) := sys->tokenize(hd lines, " \t\n");
+		if(nf >= 3){
+			addr := ref Ifcaddr;
+			(nil, addr.ip) = IPaddr.parse(hd fields); fields = tl fields;
+			(nil, addr.mask) = IPaddr.parsemask(hd fields); fields = tl fields;
+			(nil, addr.net) = IPaddr.parse(hd fields); fields = tl fields;
+			if(nf >= 5){
+				addr.preflt = big hd fields; fields = tl fields;
+				addr.validlt = big hd fields; fields = tl fields;
+			}else{
+				addr.preflt = big 0;
+				addr.validlt = big 0;
+			}
+			addrs = addr :: addrs;
+		}
+	}
+	for(; addrs != nil; addrs = tl addrs)
+		ifc.addrs = hd addrs :: ifc.addrs;
+	return ifc;
+}
+
+slurp(fd: ref Sys->FD): string
+{
+	buf := array[2048] of byte;
+	s := "";
+	while((n := sys->read(fd, buf, len buf)) > 0)
+		s += string buf[0:n];
+	return s;
+}
+
+valof(l: list of string, attr: string): string
+{
+	while(l != nil){
+		label := hd l;
+		l = tl l;
+		if(label == attr){
+			if(l == nil)
+				return nil;
+			return hd l;
+		}
+		if(l != nil)
+			l = tl l;
+	}
+	return nil;
+}
+
+Udphdr.new(): ref Udphdr
+{
+	return ref Udphdr(noaddr, noaddr, noaddr, 0, 0);
+}
+
+Udphdr.unpack(a: array of byte, n: int): ref Udphdr
+{
+	case n {
+	Udp4hdrlen =>
+		u := ref Udphdr;
+		u.raddr = IPaddr.newv4(a[0:]);
+		u.laddr = IPaddr.newv4(a[IPv4addrlen:]);
+		u.rport = get2(a, 2*IPv4addrlen);
+		u.lport = get2(a, 2*IPv4addrlen+2);
+		u.ifcaddr = u.laddr.copy();
+		return u;
+	OUdphdrlen =>
+		u := ref Udphdr;
+		u.raddr = IPaddr.newv6(a[0:]);
+		u.laddr = IPaddr.newv6(a[IPaddrlen:]);
+		u.rport = get2(a, 2*IPaddrlen);
+		u.lport = get2(a, 2*IPaddrlen+2);
+		u.ifcaddr = u.laddr.copy();
+		return u;
+	Udphdrlen =>
+		u := ref Udphdr;
+		u.raddr = IPaddr.newv6(a[0:]);
+		u.laddr = IPaddr.newv6(a[IPaddrlen:]);
+		u.ifcaddr = IPaddr.newv6(a[2*IPaddrlen:]);
+		u.rport = get2(a, 3*IPaddrlen);
+		u.lport = get2(a, 3*IPaddrlen+2);
+		return u;
+	* =>
+		raise "Udphdr.unpack: bad length";
+	}
+}
+
+Udphdr.pack(u: self ref Udphdr, a: array of byte, n: int)
+{
+	case n {
+	Udp4hdrlen =>
+		a[0:] = u.raddr.v4();
+		a[IPv4addrlen:] = u.laddr.v4();
+		put2(a, 2*IPv4addrlen, u.rport);
+		put2(a, 2*IPv4addrlen+2, u.lport);
+	OUdphdrlen =>
+		a[0:] = u.raddr.v6();
+		a[IPaddrlen:] = u.laddr.v6();
+		put2(a, 2*IPaddrlen, u.rport);
+		put2(a, 2*IPaddrlen+2, u.lport);
+	Udphdrlen =>
+		a[0:] = u.raddr.v6();
+		a[IPaddrlen:] = u.laddr.v6();
+		a[2*IPaddrlen:] = u.ifcaddr.v6();
+		put2(a, 3*IPaddrlen, u.rport);
+		put2(a, 3*IPaddrlen+2, u.lport);
+	* =>
+		raise "Udphdr.pack: bad length";
+	}
+}
+
+get2(a: array of byte, o: int): int
+{
+	return (int a[o] << 8) | int a[o+1];
+}
+
+put2(a: array of byte, o: int, val: int): int
+{
+	a[o] = byte (val>>8);
+	a[o+1] = byte val;
+	return o+2;
+}
+
+get4(a: array of byte, o: int): int
+{
+	return (((((int a[o] << 8)| int a[o+1]) << 8) | int a[o+2]) << 8) | int a[o+3];
+}
+	
+put4(a: array of byte, o: int, val: int): int
+{
+	a[o] = byte (val>>24);
+	a[o+1] = byte (val>>16);
+	a[o+2] = byte (val>>8);
+	a[o+3] = byte val;
+	return o+4;
+}
--- /dev/null
+++ b/appl/lib/ipattr.b
@@ -1,0 +1,217 @@
+implement IPattr;
+
+include "sys.m";
+
+include "bufio.m";
+include "attrdb.m";
+	attrdb: Attrdb;
+	Db, Dbentry, Tuples: import attrdb;
+
+include "ip.m";
+	ip: IP;
+	IPaddr: import ip;
+
+include "ipattr.m";
+
+init(m: Attrdb, ipa: IP)
+{
+#	sys = load Sys Sys->PATH;
+	attrdb = m;
+	ip = ipa;
+}
+
+dbattr(s: string): string
+{
+	digit := 0;
+	dot := 0;
+	alpha := 0;
+	hex := 0;
+	colon := 0;
+	for(i := 0; i < len s; i++){
+		case c := s[i] {
+		'0' to '9' =>
+			digit = 1;
+		'a' to 'f' or 'A' to 'F' =>
+			hex = 1;
+		'.' =>
+			dot = 1;
+		':' =>
+			colon = 1;
+		* =>
+			if(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '-' || c == '&')
+				alpha = 1;
+		}
+	}
+	if(alpha){
+		if(dot)
+			return "dom";
+		return "sys";
+	}
+	if(colon)
+		return "ip";
+	if(dot){
+		if(!hex)
+			return "ip";
+		return "dom";
+	}
+	return "sys";
+}
+
+findnetattr(ndb: ref Db, attr: string, val: string, rattr: string): (string, string)
+{
+	(matches, err) := findnetattrs(ndb, attr, val, rattr::nil);
+	if(matches == nil)
+		return (nil, err);
+	(nil, nattr) := hd matches;
+	na := hd nattr;
+#{sys := load Sys Sys->PATH; sys->print("%q=%q->%q ::", attr, val, rattr);for(al:=na.pairs; al != nil; al = tl al)sys->print(" %q=%q", (hd al).attr, (hd al).val); sys->print("\n");}
+	if(na.name == rattr && na.pairs != nil)
+		return ((hd na.pairs).val, nil);
+	return (nil, nil);
+}
+
+reverse(l: list of string): list of string
+{
+	rl: list of string;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+valueof(l: list of ref Netattr, attr: string): list of string
+{
+	rl: list of string;
+	for(; l != nil; l = tl l){
+		na := hd l;
+		if(na.name == attr){
+			for(p := na.pairs; p != nil; p = tl p)
+				rl = (hd p).val :: rl;
+		}
+	}
+	return reverse(rl);
+}
+
+netvalueof(l: list of ref Netattr, attr: string, a: IP->IPaddr): list of string
+{
+	rl: list of string;
+	for(; l != nil; l = tl l){
+		na := hd l;
+		if(na.name == attr && a.mask(na.mask).eq(na.net)){
+			for(p := na.pairs; p != nil; p = tl p)
+				rl = (hd p).val :: rl;
+		}
+	}
+	return reverse(rl);
+}
+
+findnetattrs(ndb: ref Db, attr: string, val: string, rattrs: list of string): (list of (IPaddr, list of ref Netattr), string)
+{
+	rl: list of (IPaddr, list of ref Netattr);
+	if(ndb == nil)
+		return (nil, "no database");
+	(e, ptr) := ndb.findbyattr(nil, attr, val, "ip");
+	if(e == nil){
+		if(attr != "ip")
+			return (nil, "ip attribute not found");
+		# look for attributes associated with networks that include `a'
+		(ok, a) := IPaddr.parse(val);
+		if(ok < 0)
+			return (nil, "invalid ip address in db");
+		netattrs := mkattrlist(rattrs);
+		netattributes(ndb, a, netattrs);
+		rl = (a, netattrs) :: nil;
+	}else{
+		netattrs: list of ref Netattr;
+		for(matches := e.findbyattr(attr, val, "ip"); matches != nil; matches = tl matches){
+			for((nil, allip) := hd matches; allip != nil; allip = tl allip){
+				ipa := (hd allip).val;
+				(ok, a) := IPaddr.parse(ipa);
+				if(ok < 0)
+					return (nil, "invalid ip address in db");
+				netattrs = mkattrlist(rattrs);
+				pptr := ptr;
+				pe := e;
+				for(;;){
+					attribute(pe, a, ip->allbits, netattrs, 1);
+					(pe, pptr) = ndb.findpair(pptr, attr, val);
+					if(pe == nil)
+						break;
+				}
+				netattributes(ndb, a, netattrs);
+				rl = (a, netattrs) :: rl;
+			}
+		}
+	}
+	results: list of (IPaddr, list of ref Netattr);
+	for(; rl != nil; rl = tl rl)
+		results = hd rl :: results;
+	return (results, nil);
+}
+
+netattributes(ndb: ref Db, a: IPaddr, nas: list of ref Netattr): string
+{
+	e: ref Dbentry;
+	ptr: ref Attrdb->Dbptr;
+	for(;;){
+		(e, ptr) = ndb.find(ptr, "ipnet");
+		if(e == nil)
+			break;
+		ipaddr := e.findfirst("ip");
+		if(ipaddr == nil)
+			continue;
+		(ok, netip) := IPaddr.parse(ipaddr);
+		if(ok < 0)
+			return "bad ip address in db";
+		netmask: IPaddr;
+		mask := e.findfirst("ipmask");
+		if(mask == nil){
+			if(!netip.isv4())
+				continue;
+			netmask = netip.classmask();
+		}else{
+			(ok, netmask) = IPaddr.parsemask(mask);
+			if(ok < 0)
+				return "bad ipmask in db";
+		}
+		if(a.mask(netmask).eq(netip))
+			attribute(e, netip, netmask, nas, 0);
+	}
+	return nil;
+}
+
+attribute(e: ref Dbentry, netip: IPaddr, netmask: IPaddr, nas: list of ref Netattr, ishost: int)
+{
+	for(; nas != nil; nas = tl nas){
+		na := hd nas;
+		if(na.pairs != nil){
+			if(!na.mask.mask(netmask).eq(na.mask))
+				continue;
+			# new one is at least as specific
+		}
+		matches := e.find(na.name);
+		if(matches == nil){
+			if(na.name != "ipmask" || ishost)
+				continue;
+			matches = (nil, ref Attrdb->Attr("ipmask", netmask.masktext(), 0)::nil) :: nil;
+		}
+		na.net = netip;
+		na.mask = netmask;
+		rl: list of ref Attrdb->Attr;
+		for(; matches != nil; matches = tl matches){
+			(nil, al) := hd matches;
+			for(; al != nil; al = tl al)
+				rl = hd al :: rl;
+		}
+		na.pairs = nil;
+		for(; rl != nil; rl = tl rl)
+			na.pairs = hd rl :: na.pairs;
+	}
+}
+
+mkattrlist(rattrs: list of string): list of ref Netattr
+{
+	netattrs: list of ref Netattr;
+	for(; rattrs != nil; rattrs = tl rattrs)
+		netattrs = ref Netattr(hd rattrs, nil, ip->noaddr, ip->noaddr) :: netattrs;
+	return netattrs;
+}
--- /dev/null
+++ b/appl/lib/ir.b
@@ -1,0 +1,95 @@
+implement Ir;
+
+include "sys.m";
+FD, Dir: import Sys;
+include "ir.m";
+
+sys: Sys;
+
+init(keys, pid: chan of int): int
+{
+	sys = load Sys Sys->PATH;
+
+	cfd := sys->open("#t/eia1ctl", sys->OWRITE);
+	if(cfd == nil)
+		return -1;
+	sys->fprint(cfd, "b9600");
+
+	dfd := sys->open("#t/eia1", sys->OREAD);
+	cfd = nil;
+
+	spawn reader(keys, pid, dfd);
+	return 0;
+}
+
+reader(keys, pid: chan of int, dfd: ref FD)
+{
+	n, ta, tb: int;
+	dir: Dir;
+	b1:= array[1] of byte;
+	b2:= array[1] of byte;
+
+	pid <-= sys->pctl(0,nil);
+	(n, dir) = sys->fstat(dfd);
+	if(n >= 0 && dir.length > big 0) {
+		while(dir.length > big 0) {
+			l := int dir.length;
+			n = sys->read(dfd, array[l] of byte, l);
+			if(n < 0)
+				break;
+			dir.length -= big n;
+		}
+	}	
+
+out:	for(;;) {
+		n = sys->read(dfd, b1, len b1);
+		if(n <= 0)
+			break;
+		ta = sys->millisec();
+		for(;;) {
+			n = sys->read(dfd, b2, 1);
+			if(n <= 0)
+				break out;
+			tb = sys->millisec();
+			if(tb - ta <= 200)
+				break;
+			ta = tb;
+			b1[0] = b2[0];
+		}
+		case ((int b1[0]&16r1f)<<5) | (int b2[0]&16r1f) {
+		 71 =>	n = Ir->ChanDN;
+		 95 =>	n = Ir->Seven;
+		135 =>	n = Ir->VolDN;
+		207 =>	n = Ir->Three;
+		215 =>	n = Ir->Select;
+		263 =>	n = Ir->Dn;
+		335 =>	n = Ir->Five;
+		343 =>	n = Ir->Rew;
+		399 =>	n = Ir->Nine;
+		407 =>	n = Ir->Enter;
+		455 =>	n = Ir->Power;
+		479 =>	n = Ir->One;
+		591 =>	n = Ir->Six;
+		599 =>	n = Ir->ChanUP;
+		663 =>	n = Ir->VolUP;
+		711 =>	n = Ir->Up;
+		735 =>	n = Ir->Two;
+		791 =>	n = Ir->Mute;
+		839 =>	n = Ir->FF;
+		863 =>	n = Ir->Four;
+		903 =>	n = Ir->Record;
+		927 =>	n = Ir->Eight;
+		975 =>	n = Ir->Zero;
+		983 =>	n = Ir->Rcl;
+		* =>	n = Ir->Error;
+		}
+
+		keys <-= n;
+	}
+	keys <-= Ir->Error;
+}
+
+translate(c: int): int
+{
+	return c;
+}
--- /dev/null
+++ b/appl/lib/irmpath.b
@@ -1,0 +1,118 @@
+# Driver for Mind Path IR50.
+
+implement Ir;
+
+include "sys.m";
+FD, Dir: import Sys;
+include "ir.m";
+
+sys: Sys;
+
+init(keys, pid: chan of int): int
+{
+	sys = load Sys Sys->PATH;
+
+	cfd := sys->open("#t/eia1ctl", sys->OWRITE);
+	if(cfd == nil)
+		return -1;
+	sys->fprint(cfd, "b1200");	# baud rate
+	sys->fprint(cfd, "d1");		# DTR on
+	sys->fprint(cfd, "r1");		# RTS on
+
+	dfd := sys->open("#t/eia1", sys->OREAD);
+	if(dfd == nil)
+		return -1;
+	cfd = nil;
+
+	spawn reader(keys, pid, dfd);
+	return 0;
+}
+
+reader(keys, pid: chan of int, dfd: ref FD)
+{
+	n: int;
+	dir: Dir;
+	button: int;
+
+	pid <-= sys->pctl(0,nil);
+	(n, dir) = sys->fstat(dfd);
+	if(n >= 0 && dir.length > 0) {
+		while(dir.length) {
+			n = sys->read(dfd, array[dir.length] of byte, dir.length);
+			if(n < 0)
+				break;
+			dir.length -= n;
+		}
+	}	
+
+	for(;;) {
+		# Look for 2 consecutive characters that are the same.
+		if((button=getconsec(dfd,2)) < 0)
+			break;
+		case button {
+			'-' => n = Ir->Enter;
+			'+' => n = Ir->Rcl;
+			'1' => n = Ir->One;
+			'2' => n = Ir->Two;
+			'3' => n = Ir->Three;
+			'4' => n = Ir->ChanUP;	# page up
+			'5' => n = Ir->ChanDN;	# page down
+			'U' => continue;
+			'R' =>
+				if((button=getconsec(dfd,2)) < 0)
+					break;
+				case button {
+					'a' or 'e' or 'i' or 'p' =>
+						n = Ir->Up;
+					'b' or 'f' or 'j' or 'k' =>
+						n = Ir->FF;	# right
+					'c' or 'g' or 'l' or 'm' =>
+						n = Ir->Dn;
+					'd' or 'h' or 'n' or 'o' =>
+						n = Ir->Rew;	# left
+					'Z' => n = Ir->Select;
+					* =>	;
+				}
+			* =>	;
+		}
+		keys <-= n;	# Send translated key over channel
+		# Read through to trailer before looking for another key press
+		while((button=getconsec(dfd,2)) != 'U') {
+			if(button <= 0)
+				break;
+		}
+	}
+	keys <-= Ir->Error;
+}
+
+translate(c: int): int
+{
+	return c;
+}
+
+# Gets 'count' consecutive occurrences of a byte.
+getconsec(dfd: ref FD, count: int): int
+{
+	b1:= array[1] of byte;
+	b2:= array[1] of byte;
+
+	n := sys->read(dfd, b1, 1);
+	if(n <= 0) {
+		if(n==0)
+			n = -1;
+		return n;
+	}
+	for(sofar:=1; sofar < count; sofar++) {
+		n = sys->read(dfd, b2, 1);
+		if(n <= 0) {
+			if(n==0)
+				n = -1;
+			return n;
+		}
+		if(b1[0]!=b2[0]) {
+			sofar = 1;
+			b1[0] = b2[0];
+		}
+	}
+	return int b1[0];
+}
--- /dev/null
+++ b/appl/lib/irsage.b
@@ -1,0 +1,99 @@
+implement Ir;
+
+include "sys.m";
+FD, Dir: import Sys;
+include "ir.m";
+
+sys: Sys;
+
+init(keys, pid: chan of int): int
+{
+	sys = load Sys Sys->PATH;
+
+	cfd := sys->open("#t/eia1ctl", sys->OWRITE);
+	if(cfd == nil)
+		return -1;
+	sys->fprint(cfd, "b9600");
+
+	dfd := sys->open("#t/eia1", sys->OREAD);
+	cfd = nil;
+
+	spawn reader(keys, pid, dfd);
+	return 0;
+}
+
+reader(keys, pid: chan of int, dfd: ref FD)
+{
+	n, ta, tb: int;
+	dir: Dir;
+	b1:= array[1] of byte;
+	b2:= array[1] of byte;
+
+	pid <-= sys->pctl(0,nil);
+	(n, dir) = sys->fstat(dfd);
+	if(n >= 0 && dir.length > big 0) {
+		while(dir.length > big 0) {
+			l := int dir.length;
+			n = sys->read(dfd, array[l] of byte, l);
+			if(n < 0)
+				break;
+			dir.length -= big n;
+		}
+	}	
+
+out:	for(;;) {
+		n = sys->read(dfd, b1, len b1);
+		if(n <= 0)
+			break;
+		ta = sys->millisec();
+		for(;;) {
+			n = sys->read(dfd, b2, 1);
+			if(n <= 0)
+				break out;
+			tb = sys->millisec();
+			if(tb - ta <= 200)
+				break;
+			ta = tb;
+			b1[0] = b2[0];
+		}
+#sys->print("IR Code = %d\n", ((int b1[0]&16r1f)<<5) | (int b2[0]&16r1f));
+		case ((int b1[0]&16r1f)<<5) | (int b2[0]&16r1f) {
+		 71 =>	n = Ir->ChanDN;
+		 95 =>	n = Ir->Seven;
+#		135 =>	n = Ir->VolDN;
+		207 =>	n = Ir->Three;
+		15 =>	n = Ir->Select;
+		135 =>	n = Ir->Dn;
+		335 =>	n = Ir->Five;
+#		343 =>	n = Ir->Rew;
+		519 =>	n = Ir->Rew;
+		399 =>	n = Ir->Nine;
+		407 =>	n = Ir->Enter;
+		455 =>	n = Ir->Power;
+		479 =>	n = Ir->One;
+		591 =>	n = Ir->Six;
+		599 =>	n = Ir->ChanUP;
+#		663 =>	n = Ir->VolUP;
+		663 =>	n = Ir->Up;
+		735 =>	n = Ir->Two;
+		791 =>	n = Ir->Mute;
+#		839 =>	n = Ir->FF;
+		23 =>	n = Ir->FF;
+		863 =>	n = Ir->Four;
+		903 =>	n = Ir->Record;
+		927 =>	n = Ir->Eight;
+		975 =>	n = Ir->Zero;
+		983 =>	n = Ir->Rcl;
+		* =>	n = Ir->Error;
+		}
+
+		keys <-= n;
+
+	}
+	keys <-= Ir->Error;
+}
+
+translate(c: int): int
+{
+	return c;
+}
--- /dev/null
+++ b/appl/lib/irsim.b
@@ -1,0 +1,83 @@
+implement Ir;
+
+include "sys.m";
+sys: Sys;
+FD: import sys;
+
+include "ir.m";
+
+rawon: ref FD;
+
+init(keys, pid: chan of int): int
+{
+	dfd: ref FD;
+
+	sys = load Sys Sys->PATH;
+
+	dfd = sys->open("/dev/keyboard", sys->OREAD);
+	if(dfd == nil)
+		return -1;
+
+	spawn reader(keys, pid, dfd);
+	return 0;
+}
+
+reader(keys, pid: chan of int, dfd: ref FD)
+{
+	n: int;
+
+	nb := 0;
+	b:= array[1] of byte;
+	buf := array[10] of byte;
+	pid <-= sys->pctl(0,nil);
+	for(;;) {
+		n = sys->read(dfd, b, 1);
+		if(n != 1)
+			break;
+		if(nb>= len buf){
+			sys->print("irsim: confused by input\n");
+			break;
+		}
+
+		buf[nb++] = b[0];
+		nutf := sys->utfbytes(buf, nb);
+		if(nutf > 0){
+			s := string buf[0:nutf];
+			keys <-= s[0];
+			nb = 0;
+		}
+	}
+	keys <-= Ir->EOF;
+}
+
+translate(key: int): int
+{
+	n := Ir->Error;
+
+	case key {
+	'0' =>	n = Ir->Zero;
+	'1' =>	n = Ir->One;
+	'2' =>	n = Ir->Two;
+	'3' =>	n = Ir->Three;
+	'4' =>	n = Ir->Four;
+	'5' =>	n = Ir->Five;
+	'6' =>	n = Ir->Six;
+	'7' =>	n = Ir->Seven;
+	'8' =>	n = Ir->Eight;
+	'9' =>	n = Ir->Nine;
+	'r' =>	n = Ir->ChanUP;
+	'c' =>	n = Ir->ChanDN;
+	't' =>	n = Ir->VolUP;
+	'v' =>	n = Ir->VolDN;
+	'k' =>	n = Ir->FF;
+	'j' =>	n = Ir->Rew;
+	'i' =>	n = Ir->Up;
+	'm' =>	n = Ir->Dn;
+	'x' =>	n = Ir->Rcl;
+	'\n' =>	n = Ir->Select;
+	' ' =>	n = Ir->Enter;
+	16r7f =>	n = Ir->Power;
+	}
+
+	return n;
+}
--- /dev/null
+++ b/appl/lib/itslib.b
@@ -1,0 +1,45 @@
+implement Itslib;
+
+include "sys.m";
+	sys: Sys;
+include "itslib.m";
+include "env.m";
+	env: Env;
+
+
+init(): ref Tconfig
+{
+	sys = load Sys Sys->PATH;
+	tc := ref Tconfig(-1, sys->fildes(2));
+	env = load Env Env->PATH;
+	if (env == nil)
+		sys->fprint(sys->fildes(2), "Failed to load %s\n", Env->PATH);
+	else {
+		vstr := env->getenv(ENV_VERBOSITY);
+		mstr := env->getenv(ENV_MFD);
+		if (vstr != nil && mstr != nil) {
+			tc.verbosity = int vstr;
+			tc.mfd = sys->fildes(int mstr);
+		}
+	}
+	if (tc.verbosity >= 0)
+		tc.report(S_STIME, 0, sys->sprint("%d", sys->millisec()));
+	else 
+		sys->fprint(sys->fildes(2), "Test is running standalone\n");
+	return tc;
+}
+
+Tconfig.report(tc: self ref Tconfig, sev: int, verb: int, msg: string)
+{
+	if (sev < 0 || sev > S_ETIME) {
+		sys->fprint(sys->fildes(2), "Tconfig.report: Bad severity code: %d\n", sev);
+		sev = 0;
+	}
+	if (tc.mfd != nil && sys->fprint(tc.mfd, "%d%d%s\n", sev, verb, msg) <=0)
+		tc.mfd = nil;		# Master test process was probably killed
+}
+
+Tconfig.done(tc: self ref Tconfig)
+{
+	tc.report(S_ETIME, 0, sys->sprint("%d", sys->millisec()));
+}
--- /dev/null
+++ b/appl/lib/json.b
@@ -1,0 +1,619 @@
+implement JSON;
+
+#
+# Javascript `Object' Notation (JSON): RFC4627
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "json.m";
+
+init(b: Bufio)
+{
+	sys = load Sys Sys->PATH;
+	bufio = b;
+}
+
+jvarray(a: array of ref JValue): ref JValue.Array
+{
+	return ref JValue.Array(a);
+}
+
+jvbig(i: big): ref JValue.Int
+{
+	return ref JValue.Int(i);
+}
+
+jvfalse(): ref JValue.False
+{
+	return ref JValue.False;
+}
+
+jvint(i: int): ref JValue.Int
+{
+	return ref JValue.Int(big i);
+}
+
+jvnull(): ref JValue.Null
+{
+	return ref JValue.Null;
+}
+
+jvobject(m: list of (string, ref JValue)): ref JValue.Object
+{
+	# could `uniq' the labels
+	return ref JValue.Object(m);
+}
+
+jvreal(r: real): ref JValue.Real
+{
+	return ref JValue.Real(r);
+}
+
+jvstring(s: string): ref JValue.String
+{
+	return ref JValue.String(s);
+}
+
+jvtrue(): ref JValue.True
+{
+	return ref JValue.True;
+}
+
+Syntax: exception(string);
+Badwrite: exception;
+
+readjson(fd: ref Iobuf): (ref JValue, string)
+{
+	{
+		p := Parse.mk(fd);
+		c := p.getns();
+		if(c == Bufio->EOF)
+			return (nil, nil);
+		p.unget(c);
+		return (readval(p), nil);
+	}exception e{
+	Syntax =>
+		return (nil, sys->sprint("JSON syntax error (offset %bd): %s", fd.offset(), e));
+	}
+}
+
+writejson(fd: ref Iobuf, val: ref JValue): int
+{
+	{
+		writeval(fd, val);
+		return 0;
+	}exception{
+	Badwrite =>
+		return -1;
+	}
+}
+
+#
+# value ::= string | number | object | array | 'true' | 'false' | 'null'
+#
+readval(p: ref Parse): ref JValue raises(Syntax)
+{
+	{
+		while((c := p.getc()) == ' ' || c == '\t' || c == '\n' || c == '\r')
+			{}
+		if(c < 0){
+			if(c == Bufio->EOF)
+				raise Syntax("unexpected end-of-input");
+			raise Syntax(sys->sprint("read error: %r"));
+		}
+		case c {
+		'{' =>
+			# object ::= '{' [pair (',' pair)*] '}'
+			l:  list of (string, ref JValue);
+			if((c = p.getns()) != '}'){
+				p.unget(c);
+				rl: list of (string, ref JValue);
+				do{
+					# pair ::= string ':' value
+					c = p.getns();
+					if(c != '"')
+						raise Syntax("missing member name");
+					name := readstring(p, c);
+					if(p.getns() != ':')
+						raise Syntax("missing ':'");
+					rl = (name, readval(p)) :: rl;
+				}while((c = p.getns()) == ',');
+				for(; rl != nil; rl = tl rl)
+					l = hd rl :: l;
+			}
+			if(c != '}')
+				raise Syntax("missing '}' at end of object");
+			return ref JValue.Object(l);
+		'[' =>
+			#	array ::= '[' [value (',' value)*] ']'
+			l: list of ref JValue;
+			n := 0;
+			if((c = p.getns()) != ']'){
+				p.unget(c);
+				do{
+					l = readval(p) :: l;
+					n++;
+				}while((c = p.getns()) == ',');
+			}
+			if(c != ']')
+				raise Syntax("missing ']' at end of array");			
+			a := array[n] of ref JValue;
+			for(; --n >= 0; l = tl l)
+				a[n] = hd l;
+			return ref JValue.Array(a);
+		'"' =>
+			return ref JValue.String(readstring(p, c));
+		'-' or '0' to '9' =>
+			#	number ::=	int frac? exp?
+			#	int ::= '-'? [0-9] | [1-9][0-9]+
+			#	frac ::= '.' [0-9]+
+			#	exp ::= [eE][-+]? [0-9]+
+			if(c == '-')
+				intp := "-";
+			else
+				p.unget(c);
+			intp += readdigits(p);		# we don't enforce the absence of leading zeros
+			fracp: string;
+			c = p.getc();
+			if(c == '.'){
+				fracp = readdigits(p);
+				c = p.getc();
+			}
+			exp := "";
+			if(c == 'e' || c == 'E'){
+				exp[0] = c;
+				c = p.getc();
+				if(c == '-' || c == '+')
+					exp[1] = c;
+				else
+					p.unget(c);
+				exp += readdigits(p);
+			}else
+				p.unget(c);
+			if(fracp != nil || exp != nil)
+				return ref JValue.Real(real (intp+"."+fracp+exp));
+			return ref JValue.Int(big intp);
+		'a' to 'z' =>
+			# 'true' | 'false' | 'null'
+			s: string;
+			do{
+				s[len s] = c;
+			}while((c = p.getc()) >= 'a' && c <= 'z');
+			p.unget(c);
+			case s {
+			"true" =>	return ref JValue.True();
+			"false" =>	return ref JValue.False();
+			"null" =>	return ref JValue.Null();
+			* =>	raise Syntax("invalid literal: "+s);
+			}
+		* =>
+			raise Syntax(sys->sprint("unexpected character #%.4ux", c));
+		}
+	}exception{
+	Syntax =>
+		raise;
+	}
+}
+
+# string ::= '"' char* '"'
+# char ::= [^\x00-\x1F"\\] | '\"' | '\/' | '\b' | '\f' | '\n' | '\r' | '\t' | '\u' hex hex hex hex
+readstring(p: ref Parse, delim: int): string raises(Syntax)
+{
+	{
+		s := "";
+		while((c := p.getc()) != delim && c >= 0){
+			if(c == '\\'){
+				c = p.getc();
+				if(c < 0)
+					break;
+				case c {
+				'b' =>	c =  '\b';
+				'f' =>		c =  '\f';
+				'n' =>	c =  '\n';
+				'r' =>		c =  '\r';
+				't' =>		c =  '\t';
+				'u' =>
+					c = 0;
+					for(i := 0; i < 4; i++)
+						c = (c<<4) | hex(p.getc());
+				* =>		;	# identity, including '"', '/', and '\'
+				}
+			}
+			s[len s] = c;
+		}
+		if(c < 0){
+			if(c == Bufio->ERROR)
+				raise Syntax(sys->sprint("read error: %r"));
+			raise Syntax("unterminated string");
+		}
+		return s;
+	}exception{
+	Syntax =>
+		raise;
+	}
+}
+
+# hex ::= [0-9a-fA-F]
+hex(c: int): int raises(Syntax)
+{
+	case c {
+	'0' to '9' =>
+		return c-'0';
+	'a' to 'f' =>
+		return 10+(c-'a');
+	'A' to 'F' =>
+		return 10+(c-'A');
+	* =>
+		raise Syntax("invalid hex digit");
+	}
+}
+
+# digits ::= [0-9]+
+readdigits(p: ref Parse): string raises(Syntax)
+{
+	c := p.getc();
+	if(!(c >= '0' && c <= '9'))
+		raise Syntax("expected integer literal");
+	s := "";
+	s[0] = c;
+	while((c = p.getc()) >= '0' && c <= '9')
+		s[len s] = c;
+	p.unget(c);
+	return s;
+}
+
+writeval(out: ref Iobuf, o: ref JValue) raises(Badwrite)
+{
+	{
+		if(o == nil){
+			puts(out, "null");
+			return;
+		}
+		pick r := o {
+		String =>
+			writestring(out, r.s);
+		Int =>
+			puts(out, r.text());
+		Real =>
+			puts(out, r.text());
+		Object =>	# '{' [pair (',' pair)*] '}'
+			putc(out, '{');
+			for(l := r.mem; l != nil; l = tl l){
+				if(l != r.mem)
+					putc(out, ',');
+				(n, v) := hd l;
+				writestring(out, n);
+				putc(out, ':');
+				writeval(out, v);
+			}
+			putc(out, '}');
+		Array =>	# '[' [value (',' value)*] ']'
+			putc(out, '[');
+			for(i := 0; i < len r.a; i++){
+				if(i != 0)
+					putc(out, ',');
+				writeval(out, r.a[i]);
+			}
+			putc(out, ']');
+		True =>
+			puts(out, "true");
+		False =>
+			puts(out, "false");
+		Null =>
+			puts(out, "null");
+		* =>
+			raise "writeval: unknown value";	# can't happen
+		}
+	}exception{
+	Badwrite =>
+		raise;
+	}
+}
+
+writestring(out: ref Iobuf, s: string) raises(Badwrite)
+{
+	{
+		putc(out, '"');
+		for(i := 0; i < len s; i++){
+			c := s[i];
+			if(needesc(c))
+				puts(out, escout(c));
+			else
+				putc(out, c);
+		}
+		putc(out, '"');
+	}exception{
+	Badwrite =>
+		raise;
+	}
+}
+
+escout(c: int): string
+{
+	case c {
+	'"' =>		return "\\\"";
+	'\\' =>	return "\\\\";
+	'/' =>	return "\\/";
+	'\b' =>	return "\\b";
+	'\f' =>	return "\\f";
+	'\n' =>	return "\\n";
+	'\t' =>	return "\\t";
+	'\r' =>	return "\\r";
+	* =>		return sys->sprint("\\u%.4ux", c);
+	}
+}
+
+puts(out: ref Iobuf, s: string) raises(Badwrite)
+{
+	if(out.puts(s) == Bufio->ERROR)
+		raise Badwrite;
+}
+
+putc(out: ref Iobuf, c: int) raises(Badwrite)
+{
+	if(out.putc(c) == Bufio->ERROR)
+		raise Badwrite;
+}
+
+Parse: adt {
+	input:	ref Iobuf;
+	eof:		int;
+
+	mk:		fn(io: ref Iobuf): ref Parse;
+	getc:		fn(nil: self ref Parse): int;
+	unget:	fn(nil: self ref Parse, c: int);
+	getns:	fn(nil: self ref Parse): int;
+};
+
+Parse.mk(io: ref Iobuf): ref Parse
+{
+	return ref Parse(io, 0);
+}
+
+Parse.getc(p: self ref Parse): int
+{
+	if(p.eof)
+		return p.eof;
+	c := p.input.getc();
+	if(c < 0)
+		p.eof = c;
+	return c;
+}
+
+Parse.unget(p: self ref Parse, c: int)
+{
+	if(c >= 0)
+		p.input.ungetc();
+}
+
+# skip white space
+Parse.getns(p: self ref Parse): int
+{
+	while((c := p.getc()) == ' ' || c == '\t' || c == '\n' || c == '\r')
+		{}
+	return c;
+}
+
+JValue.isarray(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.Array;
+}
+
+JValue.isint(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.Int;
+}
+
+JValue.isnumber(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.Int || tagof v == tagof JValue.Real;
+}
+
+JValue.isobject(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.Object;
+}
+
+JValue.isreal(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.Real;
+}
+
+JValue.isstring(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.String;
+}
+
+JValue.istrue(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.True;
+}
+
+JValue.isfalse(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.False;
+}
+
+JValue.isnull(v: self ref JValue): int
+{
+	return tagof v == tagof JValue.Null;
+}
+
+JValue.copy(v: self ref JValue): ref JValue
+{
+	pick r := v {
+	True or False or Null =>
+		return ref *r;
+	Int =>
+		return ref *r;
+	Real =>
+		return ref *r;
+	String =>
+		return ref *r;
+	Array =>
+		a := array[len r.a] of ref JValue;
+		a[0:] = r.a;
+		return ref JValue.Array(a);
+	Object =>
+		return ref *r;
+	* =>
+		raise "json: bad copy";	# can't happen
+	}
+}
+
+JValue.eq(a: self ref JValue, b: ref JValue): int
+{
+	if(a == b)
+		return 1;
+	if(a == nil || b == nil || tagof a != tagof b)
+		return 0;
+	pick r := a {
+	True or False or Null =>
+		return 1;	# tags were equal above
+	Int =>
+		pick s := b {
+		Int =>
+			return r.value == s.value;
+		}
+	Real =>
+		pick s := b {
+		Real =>
+			return r.value == s.value;
+		}
+	String =>
+		pick s := b {
+		String =>
+			return r.s == s.s;
+		}
+	Array =>
+		pick s := b {
+		Array =>
+			if(len r.a != len s.a)
+				return 0;
+			for(i := 0; i < len r.a; i++)
+				if(r.a[i] == nil){
+					if(s.a[i] != nil)
+						return 0;
+				}else if(!r.a[i].eq(s.a[i]))
+					return 0;
+			return 1;
+		}
+	Object =>
+		pick s := b {
+		Object =>
+			ls := s.mem;
+			for(lr := r.mem; lr != nil; lr = tl lr){
+				if(ls == nil)
+					return 0;
+				(rn, rv) := hd lr;
+				(sn, sv) := hd ls;
+				if(rn != sn)
+					return 0;
+				if(rv == nil){
+					if(sv != nil)
+						return 0;
+				}else if(!rv.eq(sv))
+					return 0;
+			}
+			return ls == nil;
+		}
+	}
+	return 0;
+}
+
+JValue.get(v: self ref JValue, mem: string): ref JValue
+{
+	pick r := v {
+	Object =>
+		for(l := r.mem; l != nil; l = tl l)
+			if((hd l).t0 == mem)
+				return (hd l).t1;
+		return nil;
+	* =>
+		return nil;
+	}
+}
+
+# might be better if the interface were applicative?
+# this is similar to behaviour of Limbo's own ref adt, though
+JValue.set(v: self ref JValue, mem: string, val: ref JValue)
+{
+	pick j := v {
+	Object =>
+		ol: list of (string, ref JValue);
+		for(l := j.mem; l != nil; l = tl l)
+			if((hd l).t0 == mem){
+				l = tl l;
+				for(; ol != nil; ol = tl ol)
+					l = hd ol :: l;
+				j.mem = l;
+				return;
+			}else
+				ol = hd l :: ol;
+		j.mem = (mem, val) :: j.mem;
+	* =>
+		raise "json: set non-object";
+	}
+}
+
+JValue.text(v: self ref JValue): string
+{
+	if(v == nil)
+		return "null";
+	pick r := v {
+	True =>
+		return "true";
+	False =>
+		return "false";
+	Null =>
+		return "null";
+	Int =>
+		return string r.value;
+	Real =>
+		return sys->sprint("%f", r.value);
+	String =>
+		return quote(r.s);		# quoted, or not?
+	Array =>
+		s := "[";
+		for(i := 0; i < len r.a; i++){
+			if(i != 0)
+				s += ", ";
+			s += r.a[i].text();
+		}
+		return s+"]";
+	Object =>
+		s := "{";
+		for(l := r.mem; l != nil; l = tl l){
+			if(l != r.mem)
+				s += ", ";
+			s += quote((hd l).t0)+": "+(hd l).t1.text();
+		}
+		return s+"}";
+	* =>
+		return nil;
+	}
+}
+
+quote(s: string): string
+{
+	ns := "\"";
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		if(needesc(c))
+			ns += escout(c);
+		else
+			ns[len ns] = c;
+	}
+	return ns+"\"";
+}
+
+needesc(c: int): int
+{
+	return c == '"' || c == '\\' || c == '/' || c <= 16r1F;  # '/' is escaped to prevent "</xyz>" looking like an XML end tag(!)
+}
--- /dev/null
+++ b/appl/lib/keyset.b
@@ -1,0 +1,89 @@
+implement Keyset;
+
+include "sys.m";
+	sys: Sys;
+include "keyring.m";
+	keyring: Keyring;
+include "daytime.m";
+	daytime: Daytime;
+include "readdir.m";
+
+include "keyset.m";
+
+PKHASHLEN: con Keyring->SHA1dlen * 2;
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	if(keyring == nil)
+		return cant(Keyring->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return cant(Daytime->PATH);
+	return nil;
+}
+
+cant(s: string): string
+{
+	return sys->sprint("can't load %s: %r", s);
+}
+
+pkhash(pk: string): string
+{
+	d := array of byte pk;
+	digest := array[Keyring->SHA1dlen] of byte;
+	keyring->sha1(d, len d, digest, nil);
+	s := "";
+	for(i := 0; i < len digest; i++)
+		s += sys->sprint("%2.2ux", int digest[i]);
+	return s;
+}
+
+keysforsigner(signername: string, spkhash: string, user: string, dir: string): (list of (string, string, string), string)
+{
+	if(spkhash != nil && len spkhash != PKHASHLEN)
+		return (nil, "invalid hash string");
+	if(dir == nil){
+		if(user == nil)
+			user = readname("/dev/user");
+		if(user == nil)
+			dir = "/lib/keyring";
+		else
+			dir = "/usr/" + user + "/keyring";
+	}
+	readdir := load Readdir Readdir->PATH;
+	if(readdir == nil)
+		return (nil, sys->sprint("can't load Readdir: %r"));
+	now := daytime->now();
+	(a, ok) := readdir->init(dir, Readdir->COMPACT|Readdir->MTIME);
+	if(ok < 0)
+		return (nil, sys->sprint("can't open %s: %r", dir));
+	keys: list of (string, string, string);
+	for(i := 0; i < len a; i++){
+		if(a[i].mode & Sys->DMDIR)
+			continue;
+		f := dir + "/" + a[i].name;
+		info := keyring->readauthinfo(f);
+		if(info == nil || info.cert == nil || info.cert.exp != 0 && info.cert.exp < now)
+			continue;
+		if(signername != nil && info.cert.signer != signername)
+			continue;
+		if(spkhash != nil && pkhash(keyring->pktostr(info.spk)) != spkhash)
+			continue;
+		keys = (f, info.mypk.owner, info.cert.signer) :: keys;
+	}
+	return (keys, nil);
+}
+
+readname(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/lib/libc.b
@@ -1,0 +1,148 @@
+implement Libc;
+
+include "libc.m";
+
+islx(c: int): int
+{
+	return c >= 'a' && c <= 'f';
+}
+
+isux(c: int): int
+{
+	return c >= 'A' && c <= 'F';
+}
+
+isalnum(c: int): int
+{
+	return isalpha(c) || isdigit(c);
+}
+
+isalpha(c: int): int
+{
+	return islower(c) || isupper(c);
+}
+
+isascii(c: int): int
+{
+	return (c&~16r7f) == 0;
+}
+
+iscntrl(c: int): int
+{
+	return c == 16r7f || (c&~16r1f) == 0;
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isgraph(c: int): int
+{
+	return c >= '!' && c <= '~';
+}
+
+islower(c: int): int
+{
+	return c >= 'a' && c <= 'z';
+}
+
+isprint(c: int): int
+{
+	return c >= ' ' && c <= '~';
+}
+
+ispunct(c: int): int
+{
+	return isascii(c) && !iscntrl(c) && !isspace(c) && !isalnum(c);
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
+}
+
+isupper(c: int): int
+{
+	return c >= 'A' && c <= 'Z';
+}
+
+isxdigit(c: int): int
+{
+	return isdigit(c) || islx(c) || isux(c);
+}
+
+tolower(c: int): int
+{
+	if(isupper(c))
+		return c+'a'-'A';
+	return c;
+}
+
+toupper(c: int): int
+{
+	if(islower(c))
+		return c+'A'-'a';
+	return c;
+}
+
+toascii(c: int): int
+{
+	return c&16r7f;
+}
+
+strchr(s: string, n: int): int
+{
+	l := len s;
+	for(i := 0; i < l; i++)
+		if(s[i] == n)
+			return i;
+	return -1;
+}
+
+strrchr(s: string, n: int): int
+{
+	l := len s;
+	for(i := l-1; i >= 0; i--)
+		if(s[i] == n)
+			return i;
+	return -1;
+}
+
+strncmp(s1: string, s2: string, n: int): int
+{
+	l1 := len s1;
+	l2 := len s2;
+	m := n;
+	if(m > l1)
+		m = l1;
+	if(m > l2)
+		m = l2;
+	for(i := 0; i < m; i++)
+		if(s1[i] != s2[i])
+			return s1[i]-s2[i];
+	if(i == n)
+		return 0;
+	return l1-l2;
+}
+
+abs(n: int): int
+{
+	if(n < 0)
+		return -n;
+	return n;
+}
+
+min(m: int, n: int): int
+{
+	if(m < n)
+		return m;
+	return n;
+}
+
+max(m: int, n: int): int
+{
+	if(m > n)
+		return m;
+	return n;
+}
--- /dev/null
+++ b/appl/lib/libc0.b
@@ -1,0 +1,249 @@
+implement Libc0;
+
+include "libc0.m";
+
+islx(c: int): int
+{
+	return c >= 'a' && c <= 'f';
+}
+
+isux(c: int): int
+{
+	return c >= 'A' && c <= 'F';
+}
+
+isalnum(c: int): int
+{
+	return isalpha(c) || isdigit(c);
+}
+
+isalpha(c: int): int
+{
+	return islower(c) || isupper(c);
+}
+
+isascii(c: int): int
+{
+	return (c&~16r7f) == 0;
+}
+
+iscntrl(c: int): int
+{
+	return c == 16r7f || (c&~16r1f) == 0;
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isgraph(c: int): int
+{
+	return c >= '!' && c <= '~';
+}
+
+islower(c: int): int
+{
+	return c >= 'a' && c <= 'z';
+}
+
+isprint(c: int): int
+{
+	return c >= ' ' && c <= '~';
+}
+
+ispunct(c: int): int
+{
+	return isascii(c) && !iscntrl(c) && !isspace(c) && !isalnum(c);
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
+}
+
+isupper(c: int): int
+{
+	return c >= 'A' && c <= 'Z';
+}
+
+isxdigit(c: int): int
+{
+	return isdigit(c) || islx(c) || isux(c);
+}
+
+tolower(c: int): int
+{
+	if(isupper(c))
+		return c+'a'-'A';
+	return c;
+}
+
+toupper(c: int): int
+{
+	if(islower(c))
+		return c+'A'-'a';
+	return c;
+}
+
+toascii(c: int): int
+{
+	return c&16r7f;
+}
+
+strlen(s: array of byte): int
+{
+	l := len s;
+	for(i := 0; i < l; i++)
+		if(s[i] == byte 0)
+			break;
+	return i;
+}
+
+strcpy(s1: array of byte, s2: array of byte): array of byte
+{
+	l := strlen(s2)+1;
+	if(l == len s2)
+		s1[0: ] = s2;
+	else
+		s1[0: ] = s2[0: l];
+	return s1;
+}
+
+strncpy(s1: array of byte, s2: array of byte, n: int): array of byte
+{
+	l := strlen(s2);
+	if(l >= n)
+		s1[0: ] = s2[0: n];
+	else{
+		s1[0: ] = s2;
+		for(i := l; i < n; i++)
+			s1[i] = byte '\0';
+	}
+	return s1;
+}
+
+strcat(s1: array of byte, s2: array of byte): array of byte
+{
+	l := strlen(s2)+1;
+	m := strlen(s1);
+	if(l == len s2)
+		s1[m: ] = s2;
+	else
+		s1[m: ] = s2[0: l];
+	return s1;
+}
+
+strncat(s1: array of byte, s2: array of byte, n: int): array of byte
+{
+	l := strlen(s2);
+	if(l >= n){
+		m := strlen(s1);
+		s1[m: ] = s2[0: n];
+		s1[m+n] = byte '\0';
+	}
+	else
+		strcat(s1, s2);
+	return s1;
+}
+	
+strdup(s: array of byte): array of byte
+{
+	l := strlen(s)+1;
+	t := array[l] of byte;
+	if(l == len s)
+		t[0: ] = s;
+	else
+		t[0: ] = s[0: l];
+	return t;
+}
+
+strcmp(s1: array of byte, s2: array of byte): int
+{
+	l1 := strlen(s1);
+	l2 := strlen(s2);
+	for(i := 0; i < l1 && i < l2; i++)
+		if(s1[i] != s2[i])
+			return int s1[i]-int s2[i];
+	return l1-l2;
+}
+
+strncmp(s1: array of byte, s2: array of byte, n: int): int
+{
+	i1 := i2 := 0;
+	while(n > 0){
+		c1 := int s1[i1++];
+		c2 := int s2[i2++];
+		n--;
+		if(c1 != c2){
+			if(c1 > c2)
+				return 1;
+			return -1;
+		}
+		if(c1 == 0)
+			break;
+	}
+	return 0;
+}
+
+strchr(s: array of byte, n: int): array of byte
+{
+	l := strlen(s);
+	for(i := 0; i < l; i++)
+		if(s[i] == byte n)
+			return s[i: ];
+	return nil;
+}
+
+strrchr(s: array of byte, n: int): array of byte
+{
+	l := strlen(s);
+	for(i := l-1; i >= 0; i--)
+		if(s[i] == byte n)
+			return s[i: ];
+	return nil;
+}
+
+ls2aab(argl: list of string): array of array of byte
+{
+	l := len argl;
+	ls := argl;
+	a := array[l+1] of array of byte;
+	for(i := 0; i < l; i++){
+		a[i] = array of byte (hd ls + "\0");
+		ls = tl ls;
+	}
+	a[l] = nil;
+	return a;
+}
+
+s2ab(s: string): array of byte
+{
+	return array of byte (s + "\0");
+}
+
+ab2s(a: array of byte): string
+{
+	return string a[0: strlen(a)];
+}
+
+abs(n: int): int
+{
+	if(n < 0)
+		return -n;
+	return n;
+}
+
+min(m: int, n: int): int
+{
+	if(m < n)
+		return m;
+	return n;
+}
+
+max(m: int, n: int): int
+{
+	if(m > n)
+		return m;
+	return n;
+}
--- /dev/null
+++ b/appl/lib/lists.b
@@ -1,0 +1,142 @@
+implement Lists;
+
+include "lists.m";
+
+# these will be more useful when p is a closure
+allsat[T](p: ref fn(x: T): int, l: list of T): int
+{
+	for(; l != nil; l = tl l)
+		if(!p(hd l))
+			return 0;
+	return 1;
+}
+
+anysat[T](p: ref fn(x: T): int, l: list of T): int
+{
+	for(; l != nil; l = tl l)
+		if(p(hd l))
+			return 1;
+	return 0;
+}
+
+map[T](f: ref fn(x: T): T, l: list of T): list of T
+{
+	if(l == nil)
+		return nil;
+	return f(hd l) :: map(f, tl l);
+}
+
+filter[T](p: ref fn(x: T): int, l: list of T): list of T
+{
+	if(l == nil)
+		return nil;
+	if(p(hd l))
+		return hd l :: filter(p, tl l);
+	return filter(p, tl l);
+}
+
+partition[T](p: ref fn(x: T): int, l: list of T): (list of T, list of T)
+{
+	l1: list of T;
+	l2: list of T;
+	for(; l != nil; l = tl l)
+		if(p(hd l))
+			l1 = hd l :: l1;
+		else
+			l2 = hd l :: l2;
+	return (reverse(l1), reverse(l2));
+}
+
+append[T](l: list of T, x: T): list of T
+{
+	# could use the reversing loops instead if this is ever a bottleneck
+	if(l == nil)
+		return x :: nil;
+	return hd l :: append(tl l, x);
+}
+
+concat[T](l: list of T, l2: list of T): list of T
+{
+	if(l2 == nil)
+		return l;
+	for(rl1 := reverse(l); rl1 != nil; rl1 = tl rl1)
+		l2 = hd rl1 :: l2;
+	return l2;
+}
+
+combine[T](l: list of T, l2: list of T): list of T
+{
+	for(; l != nil; l = tl l)
+		l2 = hd l :: l2;
+	return l2;
+}
+
+reverse[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+last[T](l: list of T): T
+{
+	# l must not be nil
+	while(tl l != nil)
+		l = tl l;
+	return hd l;
+}
+
+# find instance of x in l, return tail of l from x
+find[T](x: T, l: list of T): list of T
+	for { T =>	eq:	fn(a, b: T): int; }
+{
+	for(; l != nil; l = tl l)
+		if(T.eq(x, hd l))
+			return l;
+	return nil;
+}
+
+# delete the first instance of x in l
+delete[T](x: T, l: list of T): list of T
+	for { T =>	eq:	fn(a, b: T): int; }
+{
+	loc := find(x, l);
+	if(loc == nil)
+		return l;
+	o: list of T;
+	for(; l != loc; l = tl l)
+		o = hd l :: o;
+	l = tl loc;
+	for(; o != nil; o = tl o)
+		l = hd o :: l;
+	return l;
+}
+
+pair[T1, T2](l1: list of T1, l2: list of T2): list of (T1, T2)
+{
+	if(l1 == nil && l2 == nil)
+		return nil;
+	return (hd l1, hd l2) :: pair(tl l1, tl l2);
+}
+
+unpair[T1, T2](l: list of (T1, T2)): (list of T1, list of T2)
+{
+	l1: list of T1;
+	l2: list of T2;
+	for(; l != nil; l = tl l){
+		(v1, v2) := hd l;
+		l1 = v1 :: l1;
+		l2 = v2 :: l2;
+	}
+	return (reverse(l1), reverse(l2));
+}
+
+ismember[T](x: T, l: list of T): int
+	for { T =>	eq:	fn(a, b: T): int; }
+{
+	for(; l != nil; l = tl l)
+		if(T.eq(x, hd l))
+			return 1;
+	return 0;
+}
--- /dev/null
+++ b/appl/lib/lock.b
@@ -1,0 +1,26 @@
+implement Lock;
+
+include "sys.m";
+	sys:	Sys;
+include "lock.m";
+
+Semaphore.obtain(l: self ref Semaphore)
+{
+	l.c <-= 0;
+}
+
+Semaphore.release(l: self ref Semaphore)
+{
+	<-l.c;
+}
+
+Semaphore.new(): ref Semaphore
+{
+	l := ref Semaphore;
+	l.c = chan[1] of int;
+	return l;
+}
+
+init()
+{
+}
--- /dev/null
+++ b/appl/lib/login.b
@@ -1,0 +1,177 @@
+# Inferno Encrypt Key Exchange Protocol
+#
+# Copyright © 1995-1999 Lucent Techologies Inc.  All rights reserved.
+#
+# This code uses methods that are subject to one or more patents
+# held by Lucent Technologies Inc.  Its use outside Inferno
+# requires a separate licence from Lucent.
+#
+implement Login;
+
+include "sys.m";
+	sys: Sys;
+
+include "keyring.m";
+	kr: Keyring;
+	IPint: import kr;
+
+include "security.m";
+
+include "dial.m";
+
+include "string.m";
+
+# see login(6)
+login(id, password, dest: string): (string, ref Keyring->Authinfo)
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	if(kr == nil)
+		return nomod(Keyring->PATH);
+
+	ssl := load SSL SSL->PATH;
+	if(ssl == nil)
+		return nomod(SSL->PATH);
+
+	rand := load Random Random->PATH;
+	if(rand == nil)
+		return nomod(Random->PATH);
+
+	dial := load Dial Dial->PATH;
+	if(dial == nil)
+		return nomod(Dial->PATH);
+
+	if(dest == nil)
+		dest = "$SIGNER";
+	dest = dial->netmkaddr(dest, "net", "inflogin");
+	lc := dial->dial(dest, nil);
+	if(lc == nil)
+		return (sys->sprint("can't contact login service: %s: %r", dest), nil);
+
+	# push ssl, leave in clear mode for now
+	(err, c) := ssl->connect(lc.dfd);
+	if(c == nil)
+		return ("can't push ssl: " + err, nil);
+	lc.dfd = nil;
+	lc.cfd = nil;
+
+	# user->CA	name
+	if(kr->putstring(c.dfd, id) < 0)
+		return (sys->sprint("can't send user name: %r"), nil);
+
+	# CA->user	ACK
+	(s, why) := kr->getstring(c.dfd);
+	if(why != nil)
+		return ("remote: " + why, nil);
+	if(s != id)
+		return ("unexpected reply from signer: " + s, nil);
+
+	# user->CA	ivec
+	ivec := rand->randombuf(rand->ReallyRandom, 8);
+	if(kr->putbytearray(c.dfd, ivec, len ivec) < 0)
+		return (sys->sprint("can't send initialization vector: %r"), nil);
+
+	# start encrypting
+	pwbuf := array of byte password;
+	digest := array[Keyring->SHA1dlen] of byte;
+	kr->sha1(pwbuf, len pwbuf, digest, nil);
+	pwbuf = array[8] of byte;
+	for(i := 0; i < 8; i++)
+		pwbuf[i] = digest[i] ^ digest[8+i];
+	for(i = 0; i < 4; i++)
+		pwbuf[i] ^= digest[16+i];
+	for(i = 0; i < 8; i++)
+		pwbuf[i] ^= ivec[i];
+	err = ssl->secret(c, pwbuf, pwbuf);
+	if(err != nil)
+		return ("can't set secret: " + err, nil);
+	if(sys->fprint(c.cfd, "alg rc4") < 0)
+		return (sys->sprint("can't push alg rc4: %r"), nil);
+	#if(sys->fprint(c.cfd, "alg desebc") < 0)
+	#	return (sys->sprint("can't push alg desecb: %r"), nil);
+
+	# CA -> user	key(alpha**r0 mod p)
+	(s, err) = kr->getstring(c.dfd);
+	if(err != nil){
+		if(err == "failure") # calculated secret is wrong
+			return ("name or secret incorrect (alpha**r0 mod p)", nil);
+		return ("remote:" + err, nil);
+	}
+
+	# stop encrypting
+	if(sys->fprint(c.cfd, "alg clear") < 0)
+		return (sys->sprint("can't push alg clear: %r"), nil);
+	alphar0 := IPint.b64toip(s);
+
+	# CA->user	alpha
+	(s, err) = kr->getstring(c.dfd);
+	if(err != nil){
+		if(err == "failure")
+			return ("name or secret incorrect (alpha)", nil);
+		return ("remote: " + err, nil);
+	}
+	info := ref Keyring->Authinfo;
+	info.alpha = IPint.b64toip(s);
+
+	# CA->user	p
+	(s, err) = kr->getstring(c.dfd);
+	if(err != nil){
+		if(err == "failure")
+			return ("name or secret incorrect (p)", nil);
+		return ("remote: " + err, nil);
+	}
+	info.p = IPint.b64toip(s);
+
+	# sanity check
+	bits := info.p.bits();
+	abits := info.alpha.bits();
+	if(abits > bits || abits < 2)
+		return ("bogus diffie hellman constants", nil);
+
+	# generate our random diffie hellman part
+	r1 := kr->IPint.random(bits/4, bits);
+	alphar1 := info.alpha.expmod(r1, info.p);
+
+	# user->CA	alpha**r1 mod p
+	if(kr->putstring(c.dfd, alphar1.iptob64()) < 0)
+		return (sys->sprint("can't send (alpha**r1 mod p): %r"), nil);
+
+	# compute alpha**(r0*r1) mod p
+	alphar0r1 := alphar0.expmod(r1, info.p);
+
+	# turn on digesting
+	secret := alphar0r1.iptobytes();
+	err = ssl->secret(c, secret, secret);
+	if(err != nil)
+		return ("can't set digesting: " + err, nil);
+	if(sys->fprint(c.cfd, "alg sha1") < 0)
+		return (sys->sprint("can't push alg sha1: %r"), nil);
+
+	# CA->user	CA's public key, SHA(CA's public key + secret)
+	(s, err) = kr->getstring(c.dfd);
+	if(err != nil)
+		return ("can't get signer's public key: " + err, nil);
+
+	info.spk = kr->strtopk(s);
+
+	# generate a key pair
+	info.mysk = kr->genSKfromPK(info.spk, id);
+	info.mypk = kr->sktopk(info.mysk);
+
+	# user->CA	user's public key, SHA(user's public key + secret)
+	if(kr->putstring(c.dfd, kr->pktostr(info.mypk)) < 0)
+		return (sys->sprint("can't send your public: %r"), nil);
+
+	# CA->user	user's public key certificate
+	(s, err) = kr->getstring(c.dfd);
+	if(err != nil)
+		return ("can't get certificate: " + err, nil);
+
+	info.cert = kr->strtocert(s);
+	return(nil, info);
+}
+
+nomod(mod: string): (string, ref Keyring->Authinfo)
+{
+	return (sys->sprint("can't load module %s: %r", mod), nil);
+}
--- /dev/null
+++ b/appl/lib/man.b
@@ -1,0 +1,139 @@
+implement Man;
+
+include "sys.m";
+	sys: Sys;
+include "filepat.m";
+include "bufio.m";
+include "man.m";
+
+MANPATH: con "/man/";
+PATHDEPTH: con 1;
+
+indices: list of (string, list of (string, string));
+
+loadsections(scanlist: list of string): string
+{
+	sys = load Sys Sys->PATH;
+	bufio := load Bufio Bufio->PATH;
+	Iobuf: import bufio;
+
+	if (bufio == nil)
+		return sys->sprint("cannot load %s: %r", Bufio->PATH);
+
+	indexpaths: list of string;
+	if (scanlist == nil) {
+		filepat := load Filepat Filepat->PATH;
+		if (filepat == nil)
+			return sys->sprint("cannot load %s: %r", Filepat->PATH);
+
+		indexpaths = filepat->expand(MANPATH + "[0-9]*/INDEX");
+		if (indexpaths == nil)
+			return "cannot find man pages";
+	} else {
+		for (; scanlist != nil; scanlist = tl scanlist)
+			indexpaths = MANPATH + trimdot(hd scanlist) + "/INDEX" :: indexpaths;
+		indexpaths = sortuniq(indexpaths);
+	}
+
+	sections: list of string;
+	for (; indexpaths != nil; indexpaths = tl indexpaths) {
+		path := hd indexpaths;
+		(nil, toks) := sys->tokenize(path, "/");
+		for (d := 0; d < PATHDEPTH; d++)
+			toks = tl toks;
+		sections = hd toks :: sections;
+	}
+
+	for (sl := sections; sl != nil; sl = tl sl) {
+		section := hd sl;
+		path := MANPATH + section + "/INDEX";
+		iob := bufio->open(path, Sys->OREAD);
+		if (iob == nil)
+			continue;
+		pairs: list of (string, string) = nil;
+		
+		while((s := iob.gets('\n')) != nil) {
+			if (s[len s - 1] == '\n')
+				s = s[0:len s - 1];
+			(n, toks) := sys->tokenize(s, " ");
+			if (n != 2)
+				continue;
+			pairs = (hd toks, hd tl toks) :: pairs;
+		}
+		iob.close();
+		indices = (section, pairs) :: indices;
+	}
+	return nil;
+}
+
+trimdot(s: string): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '.')
+			return s[0: i];
+	return s;
+}
+
+getfiles(sections: list of string, keys: list of string): list of (int, string, string)
+{
+	ixl: list of (string, list of (string, string));
+
+	if (sections == nil)
+		ixl = indices;
+	else {
+		for (; sections != nil; sections = tl sections) {
+			section := trimdot(hd sections);
+			for (il := indices; il != nil; il = tl il) {
+				(s, mapl) := hd il;
+				if (s == section) {
+					ixl = (s, mapl) :: ixl;
+					break;
+				}
+			}
+		}
+	}
+	paths: list of (int, string, string);
+	for(keyl := keys; keyl != nil; keyl = tl keyl){
+		for (; ixl != nil; ixl = tl ixl) {
+			for ((s, mapl) := hd ixl; mapl != nil; mapl = tl mapl) {
+				(kw, file) := hd mapl;
+				if (hd keyl == kw) {
+					p := MANPATH + trimdot(s) + "/" + file;
+					paths = (int s, kw, p) :: paths;
+				}
+			}
+			# allow files not in the index
+			if(paths == nil || (hd paths).t0 != int s || (hd paths).t1 != hd keyl){
+				p := MANPATH + string int s + "/" + hd keyl;
+				if(sys->stat(p).t0 != -1)
+					paths = (int s, hd keyl, p) :: paths;
+			}
+		}
+	}
+	return paths;
+}
+
+sortuniq(strlist: list of string): list of string
+{
+	strs := array [len strlist] of string;
+	for (i := 0; strlist != nil; (i, strlist) = (i+1, tl strlist))
+		strs[i] = hd strlist;
+
+	# simple sort (greatest first)
+	for (i = 0; i < len strs - 1; i++) {
+		for (j := i+1; j < len strs; j++)
+			if (strs[i] < strs[j])
+				(strs[i], strs[j]) = (strs[j], strs[i]);
+	}
+
+	# construct list (result is ascending)
+	r: list of string;
+	prev := "";
+	for (i = 0; i < len strs; i++) {
+		if (strs[i] != prev) {
+			r = strs[i] :: r;
+			prev = strs[i];
+		}
+	}
+	return r;
+}
--- /dev/null
+++ b/appl/lib/memfs.b
@@ -1,0 +1,46 @@
+# To be removed...
+# functionality has been moved to appl/cmd/memfs.b
+# some progs still refer to lib/memfs so it remains for the time being
+
+implement MemFS;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "memfs.m";
+
+Cmd: module {
+	PATH: con "/dis/memfs.dis";
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+cmd: Cmd;
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	cmd = load Cmd Cmd->PATH;
+	if (cmd == nil)
+		return sys->sprint("lib/memfs cannot load %s: %r\n", Cmd->PATH);
+	return nil;
+}
+
+newfs(maxsz: int): ref Sys->FD
+{
+	p := array [2] of ref Sys->FD;
+	if (sys->pipe(p) == -1)
+		return nil;
+	sync := chan of int;
+	spawn run(p[1].fd, maxsz, sync);
+	<- sync;
+	return p[0];
+}
+
+run(fd: int, sz: int, sync: chan of int)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	sys->dup(fd, 0);
+	sys->pctl(Sys->NEWFD, 0::1::2::nil);
+	sync <-= 1;
+	cmd->init(nil, Cmd->PATH :: "-s" :: "-m" :: string sz :: nil);
+}
--- /dev/null
+++ b/appl/lib/mkfile
@@ -1,0 +1,245 @@
+<../../mkconfig
+
+DIRS=\
+	convcs\
+	crypt\
+	ecmascript\
+	encoding\
+	ida\
+	print\
+	spki\
+	strokes\
+	styxconv\
+	usb\
+	w3c\
+
+TARG=\
+	arg.dis\
+	asn1.dis\
+	attrdb.dis\
+	attrhash.dis\
+	auth.dis\
+	auth9.dis\
+	bloomfilter.dis\
+	bufio.dis\
+	cfg.dis\
+	cfgfile.dis\
+	chanfill.dis\
+	complete.dis\
+	crc.dis\
+	csv.dis\
+	daytime.dis\
+	db.dis\
+	dbm.dis\
+	dbsrv.dis\
+	debug.dis\
+	deflate.dis\
+	devpointer.dis\
+	dhcpclient.dis\
+	dial.dis\
+	dialog.dis\
+	dict.dis\
+	dis.dis\
+	diskblocks.dis\
+	disks.dis\
+	dividers.dis\
+	env.dis\
+	ether.dis\
+	exception.dis\
+	factotum.dis\
+	filepat.dis\
+	format.dis\
+	fsfilter.dis\
+	fslib.dis\
+	fsproto.dis\
+	hash.dis\
+	html.dis\
+	imageremap.dis\
+	inflate.dis\
+	ip.dis\
+	ipattr.dis\
+	ir.dis\
+	irsage.dis\
+	irsim.dis\
+	itslib.dis\
+	json.dis\
+	keyset.dis\
+	libc.dis\
+	libc0.dis\
+	lists.dis\
+	lock.dis\
+	login.dis\
+	man.dis\
+	memfs.dis\
+	mpeg.dis\
+	msgio.dis\
+	nametree.dis\
+	names.dis\
+	newns.dis\
+	ninep.dis\
+	oldauth.dis\
+	palm.dis\
+	palmdb.dis\
+	palmfile.dis\
+	parseman.dis\
+	plumbmsg.dis\
+	plumbing.dis\
+	pop3.dis\
+	popup.dis\
+	powerman.dis\
+	profile.dis\
+	pslib.dis\
+	quicktime.dis\
+	rabin.dis\
+	rand.dis\
+	random.dis\
+	readdir.dis\
+	readgif.dis\
+	readjpg.dis\
+	readpicfile.dis\
+	readpng.dis\
+	readxbitmap.dis\
+	regex.dis\
+	registries.dis\
+	rfc822.dis\
+	riff.dis\
+	scoretable.dis\
+	scsiio.dis\
+	secstore.dis\
+	selectfile.dis\
+	sets.dis\
+	sets32.dis\
+	sexprs.dis\
+	slip.dis\
+	smtp.dis\
+	sort.dis\
+	ssl.dis\
+	string.dis\
+	strinttab.dis\
+	styx.dis\
+	styxflush.dis\
+	styxlib.dis\
+	styxpersist.dis\
+	styxservers.dis\
+	tables.dis\
+	tabs.dis\
+	tftp.dis\
+	timers.dis\
+	tcl_utils.dis\
+#	tcl_tk.dis\
+	tcl_symhash.dis\
+	tcl_string.dis\
+	tcl_strhash.dis\
+	tcl_stack.dis\
+	tcl_modhash.dis\
+	tcl_list.dis\
+	tcl_io.dis\
+	tcl_inthash.dis\
+	tcl_core.dis\
+	tcl_calc.dis\
+	tkclient.dis\
+	titlebar.dis\
+	translate.dis\
+	ubfa.dis\
+	url.dis\
+	vac.dis\
+	venti.dis\
+	virgil.dis\
+	volume.dis\
+	wait.dis\
+	watchvars.dis\
+	winplace.dis\
+	wmclient.dis\
+	wmlib.dis\
+	wmsrv.dis\
+	workdir.dis\
+	writegif.dis\
+	xml.dis\
+
+MODULES=
+
+SYSMODULES= \
+	bufio.m\
+	cci.m\
+	daytime.m\
+	db.m\
+	debug.m\
+	devpointer.m\
+	dict.m\
+	draw.m\
+	env.m\
+	exception.m\
+	factotum.m\
+	filepat.m\
+	filter.m\
+	fslib.m\
+	hash.m\
+	html.m\
+	imagefile.m\
+	inflate.m\
+	ir.m\
+	keyring.m\
+	lock.m\
+	man.m\
+	mpeg.m\
+	newns.m\
+	palmfile.m\
+	plumbmsg.m\
+	powerman.m\
+	prefab.m\
+	pslib.m\
+	quicktime.m\
+	rand.m\
+	readdir.m\
+	regex.m\
+	riff.m\
+	scoretable.m\
+	security.m\
+	sh.m\
+	smtp.m\
+	srv.m\
+	string.m\
+	styx.m\
+	styxservers.m\
+	sys.m\
+	tables.m\
+	url.m\
+	venti.m\
+	volume.m\
+	watchvars.m\
+	wmlib.m\
+
+DISBIN=$ROOT/dis/lib
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
+
+plumbing.dis:N: plumbing.m
+plumber.dis:N: plumbing.m
+
+ip.dis: $ROOT/module/ip.m
+ether.dis:	$ROOT/module/ether.m
+attrdb.dis attrhash.dis:	$ROOT/module/attrdb.m
+ipattr.dis:	$ROOT/module/attrdb.m $ROOT/module/ip.m $ROOT/module/ipattr.m
+tftp.dis:	$ROOT/module/tftp.m
+registries.dis:	$ROOT/module/registries.m
+keyset.dis:	$ROOT/module/keyset.m
+auth9.dis: $ROOT/module/auth9.m
+factotum.dis:	$ROOT/module/factotum.m
+sexprs.dis:	$ROOT/module/sexprs.m
+dbm.dis: $ROOT/module/dbm.m
+names.dis: $ROOT/module/names.m
+disks.dis: $ROOT/module/disks.m
+scsiio.dis:	$ROOT/module/scsiio.m
+dhcpclient.dis:	$ROOT/module/dhcp.m
+ubfa.dis: $ROOT/module/ubfa.m
+secstore.dis:	$ROOT/module/secstore.m
+ida.dis:	$ROOT/module/ida.m
+rfc822.dis:	$ROOT/module/rfc822.m
+csv.dis: $ROOT/module/csv.m
+json.dis: $ROOT/module/json.m
+lists.dis:	$ROOT/module/lists.m
+vac.dis:	$ROOT/module/vac.m $ROOT/module/venti.m
+dial.dis:	$ROOT/module/dial.m
+styxflush.dis:	$ROOT/module/styxflush.m
+msgio.dis:	$ROOT/module/msgio.m
--- /dev/null
+++ b/appl/lib/mpeg.b
@@ -1,0 +1,152 @@
+implement Mpeg;
+
+include "sys.m";
+sys: Sys;
+FD: import Sys;
+include "draw.m";
+draw: Draw;
+Display, Rect, Image: import draw;
+include "dial.m";
+dial: Dial;
+Connection: import dial;
+include "mpeg.m";
+
+Chroma: con 16r05;
+
+getenv()
+{
+	if(sys != nil)
+		return;
+
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	dial = load Dial Dial->PATH;
+}
+
+copy(files: list of string, notify: chan of string, mpctl, mpdata: ref FD)
+{
+	n: int;
+	c: ref Connection;
+	name: list of string;	
+
+	while(files != nil) {
+		file := hd files;
+		(n, name) = sys->tokenize(file, "@");
+		m : ref FD;
+		case n {
+		1 =>
+			m = sys->open(file, sys->OREAD);
+			if(m == nil) {
+				notify <-= "mpeg open:" + file;
+				return;
+			}
+		2 =>
+			c = dial->dial(hd tl name, nil);
+			if(c == nil) {
+				notify <-= "dial:" + hd tl name;
+				return;
+			}
+			sys->fprint(c.dfd, "%s\n", hd name);
+			c.cfd = nil;
+			m = c.dfd;
+		* =>
+			notify <-= "bad file:"+hd name;
+			return;
+		}
+		sys->stream(m, mpdata, 64*1024);
+		files = tl files;
+	}
+	sys->fprint(mpctl, "stop");
+	sys->fprint(mpctl, "window 0 0 0 0");
+	notify <-= "";
+}
+
+play(display: ref Display, w: ref Image, paint: int, r: Rect, file: string, notify: chan of string): string
+{
+ 	i, j: int;
+	line: string;
+	cfg: array of byte;
+	buf := array[1024] of byte;
+	arg, words, files: list of string;
+
+	getenv();
+
+	mpdata := sys->open("/dev/mpeg", sys->OWRITE);
+	if(mpdata == nil)
+		return sys->sprint("can't open /dev/mpeg: %r");
+
+	obj := sys->open(file, sys->OREAD);
+	if(obj == nil)
+		return "open failed:"+file;
+
+	n := sys->read(obj, buf, len buf);
+	if(n < 0)
+		return "mpeg object: read error";
+
+	mpctl := sys->open("/dev/mpegctl", sys->OWRITE);
+	if(mpctl == nil)
+		return "open mpeg ctl file";
+
+	# Parse into lines
+	(n, arg) = sys->tokenize(string buf[0:n], "\n");
+	for(i = 0; i < n; i++) {
+		# Parse into words
+		line = hd arg;
+		(j, words) = sys->tokenize(line, " \t");
+
+		# Pass device config lines through to the ctl file
+		if(hd words == "files")
+			files = tl words;
+		else {
+			cfg = array of byte line;
+			if(sys->write(mpctl, cfg, len cfg) < 0)
+				return "invalid device config:"+line;
+		}
+		arg = tl arg;
+	}
+
+	if(files == nil)
+		return "no file to play";
+
+	# now the driver is configured initialize the dsp's
+	# and set up the trident overlay
+	sys->fprint(mpctl, "init");
+	sys->fprint(mpctl, "window %d %d %d %d",
+			r.min.x, r.min.y, r.max.x, r.max.y);
+
+	# paint the window with the chroma key color
+	if(paint)
+		w.draw(r, keycolor(display), nil, r.min);
+
+	if(notify != nil) {
+		spawn copy(files, notify, mpctl, mpdata);
+		return "";
+	}
+	notify = chan of string;
+	spawn copy(files, notify, mpctl, mpdata);
+	return <-notify;
+}
+
+ctl(msg: string): int
+{
+	mpc: ref FD;
+
+	getenv();
+
+	mpc = sys->open("/dev/mpegctl", sys->OWRITE);
+	if(mpc == nil)
+		return -1;
+
+	b := array of byte msg;
+	n := sys->write(mpc, b, len b);
+	if(n != len b)
+		n = -1;
+
+	return n; 
+}
+
+keycolor(display: ref Display): ref Image
+{
+	getenv();
+	return display.color(Chroma);
+}
--- /dev/null
+++ b/appl/lib/msgio.b
@@ -1,0 +1,152 @@
+implement Msgio;
+
+# probably need Authio or Auth instead, to include Authinfo, Certificate and signing operations?
+# eliminates certificates and sigalgs from Keyring
+# might be better just to have mp.m and sec.m?
+# general signature module?
+# Keyring->dhparams is is only needed by createsignerkey (and others creating Authinfo)
+# should also improve pkcs
+
+include "sys.m";
+	sys: Sys;
+
+include "keyring.m";
+
+include "msgio.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+seterr(r: int)
+{
+	if(r > 0)
+		sys->werrstr("input or format error");
+	else if(r == 0)
+		sys->werrstr("hungup");
+}
+
+#
+# i/o on a channel that might or might not retain record boundaries
+#
+getmsg(fd: ref Sys->FD): array of byte
+{
+	num := array[5] of byte;
+	r := sys->readn(fd, num, len num);
+	if(r != len num) {
+		seterr(r);
+		return nil;
+	}
+	h := string num;
+	if(h[0] == '!')
+		m := int h[1:];
+	else
+		m = int h;
+	if(m < 0 || m > Maxmsg) {
+		seterr(1);
+		return nil;
+	}
+	buf := array[m] of byte;
+	r = sys->readn(fd, buf, m);
+	if(r != m){
+		seterr(r);
+		return nil;
+	}
+	if(h[0] == '!'){
+		sys->werrstr(string buf);
+		return nil;
+	}
+	return buf;
+}
+
+sendmsg(fd: ref Sys->FD, buf: array of byte, n: int): int
+{
+	if(sys->fprint(fd, "%4.4d\n", n) < 0)
+		return -1;
+	return sys->write(fd, buf, n);
+}
+
+senderrmsg(fd: ref Sys->FD, s: string): int
+{
+	buf := array of byte s;
+	if(sys->fprint(fd, "!%3.3d\n", len buf) < 0)
+		return -1;
+	if(sys->write(fd, buf, len buf) <= 0)
+		return -1;
+	return 0;
+}
+
+#
+# i/o on a delimited channel
+#
+getbuf(fd: ref Sys->FD, buf: array of byte, n: int): (int, string)
+{
+	n = sys->read(fd, buf, n);
+	if(n <= 0){
+		seterr(n);
+		return (-1, sys->sprint("%r"));
+	}
+	if(buf[0] == byte 0)
+		return (n, nil);
+	if(buf[0] != byte 16rFF){
+		# garbled, possibly the wrong encryption
+		return (-1, "failure");
+	}
+	# error string
+	if(--n < 1)
+		return (-1, "unknown");
+	return (-1, string buf[1:]);
+}
+
+getbytearray(fd: ref Sys->FD): (array of byte, string)
+{
+	buf := array[Maxmsg] of byte;
+	(n, err) := getbuf(fd, buf, len buf);
+	if(n < 0)
+		return (nil, err);
+	return (buf[1: n], nil);
+}
+
+getstring(fd: ref Sys->FD): (string, string)
+{
+	(a, err) := getbytearray(fd);
+	if(a != nil)
+		return (string a, err);
+	return (nil, err);
+}
+
+putbuf(fd: ref Sys->FD, data: array of byte, n: int): int
+{
+	buf := array[Maxmsg] of byte;
+	if(n < 0) {
+		buf[0] = byte 16rFF;
+		n = -n;
+	}else
+		buf[0] = byte 0;
+	if(n >= Maxmsg)
+		n = Maxmsg-1;
+	buf[1:] = data;
+	return sys->write(fd, buf, n+1);
+}
+
+putstring(fd: ref Sys->FD, s: string): int
+{
+	a := array of byte s;
+	return putbuf(fd, a, len a);
+}
+
+putbytearray(fd: ref Sys->FD, a: array of byte, n: int): int
+{
+	if(n > len a)
+		n = len a;
+	return putbuf(fd, a, n);
+}
+
+puterror(fd: ref Sys->FD, s: string): int
+{
+	if(s == nil)
+		s = "unknown";
+	a := array of byte s;
+	return putbuf(fd, a, -len a);
+}
--- /dev/null
+++ b/appl/lib/names.b
@@ -1,0 +1,154 @@
+implement Names;
+
+include "sys.m";
+
+include "names.m";
+
+# return name rewritten to compress /+, eliminate ., and interpret ..
+
+cleanname(name: string): string
+{
+	if(name == nil)
+		return ".";
+
+	p := rooted := name[0]=='/';
+	if(name[0] == '#'){	# special
+		if(len name < 2)
+			return name;
+		p += 2;	# character after # whatever it is, is the name (including /)
+		for(; p < len name; p++)
+			if(name[p] == '/')
+				break;
+		rooted = p;
+	}
+	dotdot := rooted;
+
+	#
+	# invariants:
+	#	p points at beginning of path element we're considering.
+	#	out records up to the last path element (no trailing slash unless root or #/).
+	#	dotdot points in out just past the point where .. cannot backtrack
+	#		any further (no slash).
+	#
+	out := name[0:rooted];
+	while(p < len name){
+		for(q := p; p < len name && name[p] != '/'; p++){
+			# skip
+		}
+		n := name[q:p];	# path element
+		p++;
+		case n {
+		"" or "." =>
+			;	# null effect
+		".." =>
+			if(len out > dotdot){	# can backtrack
+				for(q = len out; --q > dotdot && out[q] != '/';)
+					;
+				out = out[:q];
+			}else if(!rooted){	# /.. is / but ./../ is ..
+				if(out != nil)
+					out += "/..";
+				else
+					out += "..";
+				dotdot = len out;
+			}
+		* =>
+			if(rooted > 1 || len out > rooted)
+				out[len out] = '/';
+			out += n;
+		}
+	}
+	if(out == nil)
+		return ".";
+	return out;
+}
+
+dirname(name: string): string
+{
+	for(i := len name; --i >= 0;)
+		if(name[i] == '/')
+			break;
+	if(i < 0)
+		return nil;
+	d := name[0:i];
+	if(d != nil)
+		return d;
+	if(name[0] == '/')
+		return "/";
+	return nil;
+}
+
+basename(name: string, suffix: string): string
+{
+	for(i := len name; --i >= 0;)
+		if(name[i] == '/')
+			break;
+	if(i >= 0)
+		name = name[i+1:];
+	if(suffix != nil){
+		o := len name - len suffix;
+		if(o >= 0 && name[o:] == suffix)
+			return name[0:o];
+	}
+	return name;
+}
+
+relative(name: string, root: string): string
+{
+	if(root == nil || name == nil)
+		return name;
+	if(isprefix(root, name)){
+		name = name[len root:];
+		while(name != nil && name[0] == '/')
+			name = name[1:];
+	}
+	return name;
+}
+
+rooted(root: string, name: string): string
+{
+	if(name == nil)
+		return root;
+	if(root == nil || name[0] == '/' || name[0] == '#')
+		return name;
+	if(root[len root-1] != '/' && name[0] != '/')
+		return root+"/"+name;
+	return root+name;
+}
+
+isprefix(a: string, b: string): int
+{
+	la := len a;
+	if(la == 0)
+		return 0;	# "" isn't a pathname
+	while(la > 1 && a[la-1] == '/')
+		a = a[0:--la];
+	lb := len b;
+	if(la > lb)
+		return 0;
+	if(la == lb)
+		return a == b;
+	return a == b[0:la] && (a == "/" || b[la] == '/');
+}
+
+elements(name: string): list of string
+{
+	sys := load Sys Sys->PATH;
+	(nil, fld) := sys->tokenize(name, "/");
+	if(name != nil && name[0] == '/')
+		fld = "/" :: fld;
+	return fld;
+}
+
+pathname(els: list of string): string
+{
+	name: string;
+	sl := els != nil && hd els == "/";
+	for(; els != nil; els = tl els){
+		if(!sl)
+			name += "/";
+		name += hd els;
+		sl = 0;
+	}
+	return name;
+}
--- /dev/null
+++ b/appl/lib/nametree.b
@@ -1,0 +1,277 @@
+implement Nametree;
+include "sys.m";
+	sys: Sys;
+include "styx.m";
+include "styxservers.m";
+	Navop: import Styxservers;
+	Enotfound, Eexists: import Styxservers;
+
+Fholder: adt {
+	parentqid:	big;
+	d:		Sys->Dir;
+	child:	cyclic ref Fholder;
+	sibling:	cyclic ref Fholder;
+	hash:	cyclic ref Fholder;
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+start(): (ref Tree, chan of ref Styxservers->Navop)
+{
+	fs := ref Tree(chan of ref Treeop, chan of string);
+	c := chan of ref Styxservers->Navop;
+	spawn fsproc(c, fs.c);
+	return (fs, c);
+}
+
+Tree.quit(t: self ref Tree)
+{
+	t.c <-= nil;
+}
+
+Tree.create(t: self ref Tree, parentq: big, d: Sys->Dir): string
+{
+	t.c <-= ref Treeop.Create(t.reply, parentq, d);
+	return <-t.reply;
+}
+
+Tree.remove(t: self ref Tree, q: big): string
+{
+	t.c <-= ref Treeop.Remove(t.reply, q);
+	return <-t.reply;
+}
+
+Tree.wstat(t: self ref Tree, q: big, d: Sys->Dir): string
+{
+	t.c <-= ref Treeop.Wstat(t.reply, q, d);
+	return <-t.reply;
+}
+
+Tree.getpath(t: self ref Tree, q: big): string
+{
+	t. c <-= ref Treeop.Getpath(t.reply, q);
+	return <-t.reply;
+}
+
+fsproc(c: chan of ref Styxservers->Navop, fsc: chan of ref Treeop)
+{
+	tab := array[23] of ref Fholder;
+
+	for (;;) alt {
+	grq := <-c =>
+		if (grq == nil)
+			exit;
+		(q, reply) := (grq.path, grq.reply);
+		fh := findfile(tab, q);
+		if (fh == nil) {
+			reply <-= (nil, Enotfound);
+			continue;
+		}
+		pick rq := grq {
+		Stat =>
+			reply <-= (ref fh.d, nil);
+		Walk =>
+			d := fswalk(tab, fh, rq.name);
+			if (d == nil)
+				reply <-= (nil, Enotfound);
+			else
+				reply <-= (d, nil);
+		Readdir =>
+			(start, end) := (rq.offset, rq.offset + rq.count);
+			fh = fh.child;
+			for (i := 0; i < end && fh != nil; i++) {
+				if (i >= start)
+					reply <-= (ref fh.d, nil);
+				fh = fh.sibling;
+			}
+			reply <-= (nil, nil);
+		* =>
+			panic(sys->sprint("unknown op %d\n", tagof(grq)));
+		}
+	grq := <-fsc =>
+		if (grq == nil)
+			exit;
+		(q, reply) := (grq.q, grq.reply);
+		pick rq := grq {
+		Create =>
+			reply <-= fscreate(tab, q, rq.d);
+		Remove =>
+			reply <-= fsremove(tab, q);
+		Wstat =>
+			reply <-= fswstat(tab, q, rq.d);
+		Getpath =>
+			reply <-= fsgetpath(tab, q);
+		* =>
+			panic(sys->sprint("unknown fs op %d\n", tagof(grq)));
+		}
+	}
+}
+
+hashfn(q: big, n: int): int
+{
+	h := int (q % big n);
+	if (h < 0)
+		h += n;
+	return h;
+}
+
+findfile(tab: array of ref Fholder, q: big): ref Fholder
+{
+	for (fh := tab[hashfn(q, len tab)]; fh != nil; fh = fh.hash)
+		if (fh.d.qid.path == q)
+			return fh;
+	return nil;
+}
+
+fsgetpath(tab: array of ref Fholder, q: big): string
+{
+	fh := findfile(tab, q);
+	if (fh == nil)
+		return nil;
+	s := fh.d.name;
+	while (fh.parentqid != fh.d.qid.path) {
+		fh = findfile(tab, fh.parentqid);
+		if (fh == nil)
+			return nil;
+		s = fh.d.name + "/" + s;
+	}
+	return s;
+}
+
+fswalk(tab: array of ref Fholder, fh: ref Fholder, name: string): ref Sys->Dir
+{
+	if (name == "..")
+		return ref findfile(tab, fh.parentqid).d;
+	for (fh = fh.child; fh != nil; fh = fh.sibling)
+		if (fh.d.name == name)
+			return ref fh.d;
+	return nil;
+}
+
+fsremove(tab: array of ref Fholder, q: big): string
+{
+	prev: ref Fholder;
+
+	# remove from hash table
+	slot := hashfn(q, len tab);
+	for (fh := tab[slot]; fh != nil; fh = fh.hash) {
+		if (fh.d.qid.path == q)
+			break;
+		prev = fh;
+	}
+	if (fh == nil)
+		return Enotfound;
+	if (prev == nil)
+		tab[slot] = fh.hash;
+	else
+		prev.hash = fh.hash;
+	fh.hash = nil;
+
+	# remove from parent's children
+	parent := findfile(tab, fh.parentqid);
+	if (parent != nil) {
+		prev = nil;
+		for (sfh := parent.child; sfh != nil; sfh = sfh.sibling) {
+			if (sfh == fh)
+				break;
+			prev = sfh;
+		}
+		if (sfh == nil)
+			panic("child not found in parent");
+		if (prev == nil)
+			parent.child = fh.sibling;
+		else
+			prev.sibling = fh.sibling;
+	}
+	fh.sibling = nil;
+
+	# now remove any descendents
+	sibling: ref Fholder;
+	for (sfh := fh.child; sfh != nil; sfh = sibling) {
+		sibling = sfh.sibling;
+		sfh.parentqid = sfh.d.qid.path;		# make sure it doesn't disrupt things.
+		fsremove(tab, sfh.d.qid.path);
+	}
+	return nil;
+}
+
+fscreate(tab: array of ref Fholder, q: big, d: Sys->Dir): string
+{
+	parent := findfile(tab, q);
+	if (findfile(tab, d.qid.path) != nil)
+		return Eexists;
+	# allow creation of a root directory only if its parent is itself
+	if (parent == nil && d.qid.path != q)
+		return Enotfound;
+	fh: ref Fholder;
+	if (parent == nil)
+		fh = ref Fholder(q, d, nil, nil, nil);
+	else {
+		if (fswalk(tab, parent, d.name) != nil)
+			return Eexists;
+		fh = ref Fholder(parent.d.qid.path, d, nil, nil, nil);
+		fh.sibling = parent.child;
+		parent.child = fh;
+	}
+	slot := hashfn(d.qid.path, len tab);
+	fh.hash = tab[slot];
+	tab[slot] = fh;
+	return nil;
+}
+
+fswstat(tab: array of ref Fholder, q: big, d: Sys->Dir): string
+{
+	fh := findfile(tab, q);
+	if (fh == nil)
+		return Enotfound;
+
+	d = applydir(d, fh.d);
+
+	# if renaming a file, check for duplicates
+	if (d.name != fh.d.name) {
+		parent := findfile(tab, fh.parentqid);
+		if (parent != nil && parent != fh && fswalk(tab, parent, d.name) != nil)
+			return Eexists;
+	}
+	fh.d = d;
+	fh.d.qid.path = q;		# ensure the qid can't be changed
+	return nil;
+}
+
+applydir(d: Sys->Dir, onto: Sys->Dir): Sys->Dir
+{
+	if (d.name != nil)
+		onto.name = d.name;
+	if (d.uid != nil)
+		onto.uid = d.uid;
+	if (d.gid != nil)
+		onto.gid = d.gid;
+	if (d.muid != nil)
+		onto.muid = d.muid;
+	if (d.qid.vers != ~0)
+		onto.qid.vers = d.qid.vers;
+	if (d.qid.qtype != ~0)
+		onto.qid.qtype = d.qid.qtype;
+	if (d.mode != ~0)
+		onto.mode = d.mode;
+	if (d.atime != ~0)
+		onto.atime = d.atime;
+	if (d.mtime != ~0)
+		onto.mtime = d.mtime;
+	if (d.length != ~big 0)
+		onto.length = d.length;
+	if (d.dtype != ~0)
+		onto.dtype = d.dtype;
+	if (d.dev != ~0)
+		onto.dev = d.dev;
+	return onto;
+}
+
+panic(s: string)
+{
+	sys->fprint(sys->fildes(2), "panic: %s\n", s);
+	raise "panic";
+}
--- /dev/null
+++ b/appl/lib/newns.b
@@ -1,0 +1,463 @@
+implement Newns;
+#
+# Build a new namespace from a file
+#
+#	new	create a new namespace from current directory (use cd)
+#	fork	split the namespace before modification
+#	nodev	disallow device attaches
+#	bind	[-abrci] from to
+#	mount	[-abrci9] [net!]machine[!svc] to [spec]
+#	import [-abrci9] [net!]machine[!svc] [remotedir] dir
+#	unmount	[-i] [from] to
+#   	cd	directory
+#
+#	-i to bind/mount/unmount means continue in the face of errors
+#
+include "sys.m";
+	sys: Sys;
+	FD, FileIO: import Sys;
+	stderr: ref FD;
+
+include "draw.m";
+
+include "bufio.m";
+	bio: Bufio;
+	Iobuf: import bio;
+
+include "dial.m";
+	dial: Dial;
+	Connection: import dial;
+
+include "newns.m";
+
+#include "sh.m";
+
+include "keyring.m";
+	kr: Keyring;
+
+include "security.m";
+	au: Auth;
+
+include "factotum.m";
+
+include "arg.m";
+	arg: Arg;
+
+include "string.m";
+	str: String;
+
+newns(user: string, file: string): string
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	stderr = sys->fildes(2);
+
+	# Could do some authentication here, and bail if no good FIXME
+	if(user == nil)
+		;
+	bio = load Bufio Bufio->PATH;
+	if(bio == nil)
+		return sys->sprint("cannot load %s: %r", Bufio->PATH);
+
+	arg = load Arg Arg->PATH;
+	if (arg == nil)
+		return sys->sprint("cannot load %s: %r", Arg->PATH);
+
+	au = load Auth Auth->PATH;
+	if(au == nil)
+		return sys->sprint("cannot load %s: %r", Auth->PATH);
+	err := au->init();
+	if(err != nil)
+		return "Auth->init: "+err;
+
+	str = load String String->PATH;		# no check, because we'll live without it
+
+	if(file == nil){
+		file = "namespace";
+		if(sys->stat(file).t0 < 0)
+			file = "/lib/namespace";
+	}
+
+	mfp := bio->open(file, bio->OREAD);
+	if(mfp==nil)
+      		return sys->sprint("cannot open %q: %r", file);
+
+	if(0 && user != nil){
+		sys->pctl(Sys->FORKENV, nil);
+		setenv("user", user);
+		setenv("home", "/usr/"+user);
+	}
+
+	facfd := sys->open("/mnt/factotum/rpc", Sys->ORDWR);
+	return nsfile(mfp, facfd);
+}
+
+nsfile(b: ref Iobuf, facfd: ref Sys->FD): string
+{
+	e := "";
+	while((l := b.gets('\n')) != nil){
+		if(str != nil)
+			slist := str->unquoted(l);
+		else
+			(nil, slist) = sys->tokenize(l, " \t\n\r");	# old way, in absence of String
+		if(slist == nil)
+			continue;
+		e = nsop(expand(slist), facfd);
+		if(e != "")
+			break;
+   	}
+	return e;
+}
+
+expand(l: list of string): list of string
+{
+	nl: list of string;
+	for(; l != nil; l = tl l){
+		s := hd l;
+		for(i := 0; i < len s; i++)
+			if(s[i] == '$'){
+				for(j := i+1; j < len s; j++)
+					if((c := s[j]) == '.' || c == '/' || c == '$')
+						break;
+				if(j > i+1){
+					(ok, v) := getenv(s[i+1:j]);
+					if(!ok)
+						return nil;
+					s = s[0:i] + v + s[j:];
+					i = i + len v;
+				}
+			}
+		nl = s :: nl;
+	}
+	l = nil;
+	for(; nl != nil; nl = tl nl)
+		l = hd nl :: l;
+	return l;
+}
+
+nsop(argv: list of string, facfd: ref Sys->FD): string
+{
+	# ignore comments 
+	if(argv == nil || (hd argv)[0] == '#')
+		return nil;
+ 
+	e := "";
+	c := 0;
+	cmdstr := hd argv;
+	case cmdstr {
+	"." =>
+		if(tl argv == nil)
+			return ".: needs a filename";
+		nsf := hd tl argv;
+		mfp := bio->open(nsf, bio->OREAD);
+		if(mfp==nil)
+      			return sys->sprint("can't open %q for read %r", nsf);
+		e = nsfile(mfp, facfd);
+	"new" =>
+		c = Sys->NEWNS | Sys->FORKENV;
+	"clear" =>
+		if(sys->pctl(Sys->FORKNS, nil) < 0 ||
+		   sys->bind("#/", "/", Sys->MREPL) < 0 ||
+		   sys->chdir("/") < 0 ||
+		   sys->pctl(Sys->NEWNS, nil) < 0)
+			return sys->sprint("%r");
+		return nil;
+	"fork"  =>
+		c = Sys->FORKNS;
+	"nodev" =>
+		c = Sys->NODEVS;
+	"bind" =>
+		e = bind(argv);
+	"mount" =>
+		e = mount(argv, facfd);
+	"unmount" =>
+		e = unmount(argv);
+	"import" =>
+		e = import9(argv, facfd);
+   	"cd" =>
+   		if(len argv != 2)
+			return "cd: must have one argument";   
+		if(sys->chdir(hd tl argv) < 0)
+			return sys->sprint("%r");
+	* =>
+      		e = "invalid namespace command";
+	}
+	if(c != 0) {
+		if(sys->pctl(c, nil) < 0)
+			return sys->sprint("%r");
+	}
+	return e;
+}
+
+Moptres: adt {
+	argv: list of string;
+	flags: int;
+	alg: string;
+	keyfile: string;
+	ignore: int;
+	use9: int;
+};
+
+mopt(argv: list of string): (ref Moptres, string)
+{
+	r := ref Moptres(nil, 0, "none", nil, 0, 0);
+
+	arg->init(argv);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'i' => r.ignore = 1;
+		'a' => r.flags |= sys->MAFTER;
+		'b' => r.flags |= sys->MBEFORE;
+		'c' => r.flags |= sys->MCREATE;
+		'r' => r.flags |= sys->MREPL;
+		'k' =>
+			if((r.keyfile = arg->arg()) == nil)
+				return (nil, "mount: missing arg to -k option");
+		'C' =>
+			if((r.alg = arg->arg()) == nil)
+				return (nil, "mount: missing arg to -C option");
+		'9' =>
+			r.use9 = 1;
+		 *  =>
+			return (nil, sys->sprint("mount: bad option -%c", opt));
+		}
+	}
+	if((r.flags & (Sys->MAFTER|Sys->MBEFORE)) == 0)
+		r.flags |= Sys->MREPL;
+
+	r.argv = arg->argv();
+	return (r, nil);
+}
+
+bind(argv: list of string): string
+{
+	(r, err) := mopt(argv);
+	if(err != nil)
+		return err;
+
+	if(len r.argv < 2)
+		return "bind: too few args";
+
+	from := hd r.argv;
+	r.argv = tl r.argv;
+	todir := hd r.argv;
+	if(sys->bind(from, todir, r.flags) < 0)
+		return ig(r, sys->sprint("bind %s %s: %r", from, todir));
+
+	return nil;
+}
+
+mount(argv: list of string, facfd: ref Sys->FD): string
+{
+	fd: ref Sys->FD;
+
+	(r, err) := mopt(argv);
+	if(err != nil)
+		return err;
+
+	if(len r.argv < 2)
+		return ig(r, "mount: too few args");
+
+	if(dial == nil){
+		dial = load Dial Dial->PATH;
+		if(dial == nil)
+			return ig(r, "mount: can't load Dial");
+	}
+
+	addr := hd r.argv;
+	r.argv = tl r.argv;
+	dest := dial->netmkaddr(addr, "net", "styx");
+	dir := hd r.argv;
+	r.argv = tl r.argv;
+	if(r.argv != nil)
+		spec := hd r.argv;
+
+	c := dial->dial(dest, nil);
+	if(c == nil)
+		return ig(r, sys->sprint("dial: %s: %r", dest));
+	
+	if(r.use9){
+		factotum := load Factotum Factotum->PATH;
+		if(factotum == nil)
+			return ig(r, sys->sprint("cannot load %s: %r", Factotum->PATH));
+		factotum->init();
+		afd := sys->fauth(fd, spec);
+		if(afd != nil)
+			factotum->proxy(afd, facfd, "proto=p9any role=client");	# ignore result; if it fails, mount will fail
+		if(sys->mount(fd, afd, dir, r.flags, spec) < 0)
+			return ig(r, sys->sprint("mount %q %q: %r", addr, dir));
+		return nil;
+	}
+
+	user := user();
+	kd := "/usr/" + user + "/keyring/";
+	cert: string;
+	if (r.keyfile != nil) {
+		cert = r.keyfile;
+		if (cert[0] != '/')
+			cert = kd + cert;
+		if(sys->stat(cert).t0 < 0)
+			return ig(r, sys->sprint("cannot find certificate %q: %r", cert));
+	} else {
+		cert = kd + addr;
+		if(sys->stat(cert).t0 < 0)
+			cert = kd + "default";
+	}
+	ai := kr->readauthinfo(cert);
+	if(ai == nil)
+		return ig(r, sys->sprint("cannot read certificate from %q: %r", cert));
+
+	err = au->init();
+	if (err != nil)
+		return ig(r, sys->sprint("auth->init: %r"));
+	(fd, err) = au->client(r.alg, ai, c.dfd);
+	if(fd == nil)
+		return ig(r, sys->sprint("auth: %r"));
+
+	if(sys->mount(fd, nil, dir, r.flags, spec) < 0)
+		return ig(r, sys->sprint("mount %q %q: %r", addr, dir));
+
+	return nil;
+}
+
+import9(argv: list of string, facfd: ref Sys->FD): string
+{
+	(r, err) := mopt(argv);
+	if(err != nil)
+		return err;
+
+	if(len r.argv < 2)
+		return "import: too few args";
+	if(facfd == nil)
+		return ig(r, "import: no factotum");
+	factotum := load Factotum Factotum->PATH;
+	if(factotum == nil)
+		return ig(r, sys->sprint("cannot load %s: %r", Factotum->PATH));
+	factotum->init();
+	addr := hd r.argv;
+	r.argv = tl r.argv;
+	rdir := hd r.argv;
+	r.argv = tl r.argv;
+	dir := rdir;
+	if(r.argv != nil)
+		dir = hd r.argv;
+
+	if(dial == nil){
+		dial = load Dial Dial->PATH;
+		if(dial == nil)
+			return ig(r, "import: can't load Dial");
+	}
+
+	dest := dial->netmkaddr(addr, "net", "17007");	# exportfs; might not be in inferno's ndb yet
+	c := dial->dial(dest, nil);
+	if(c == nil)
+		return ig(r, sys->sprint("import: %s: %r", dest));
+	fd := c.dfd;
+	if(factotum->proxy(fd, facfd, "proto=p9any role=client") == nil)
+		return ig(r, sys->sprint("import: %s: %r", dest));
+	if(sys->fprint(fd, "%s", rdir) < 0)
+		return ig(r, sys->sprint("import: %s: %r", dest));
+	buf := array[256] of byte;
+	if((n := sys->read(fd, buf, len buf)) != 2 || buf[0] != byte 'O' || buf[1] != byte 'K'){
+		if(n >= 4)
+			sys->werrstr(string buf[0:n]);
+		return ig(r, sys->sprint("import: %s: %r", dest));
+	}
+	# TO DO: new style: impo aan|nofilter clear|ssl|tls\n
+	afd := sys->fauth(fd, "");
+	if(afd != nil)
+		factotum->proxy(afd, facfd, "proto=p9any role=client");
+	if(sys->mount(fd, afd, dir, r.flags, "") < 0)
+		return ig(r, sys->sprint("import %q %q: %r", addr, dir));
+	return nil;
+}
+
+unmount(argv: list of string): string
+{
+	(r, err) := mopt(argv);
+	if(err != nil)
+		return err;
+
+	from, tu: string;
+	case len r.argv {
+	* =>
+		return "unmount: takes 1 or 2 args";
+	1 =>
+		from = nil;
+		tu = hd r.argv;
+	2 =>
+		from = hd r.argv;
+		tu = hd tl r.argv;
+	}
+
+	if(sys->unmount(from, tu) < 0)
+		return ig(r, sys->sprint("unmount: %r"));
+
+	return nil;
+}
+
+ig(r: ref Moptres, e: string): string
+{
+	if(r.ignore)
+		return nil;
+	return e;
+}
+
+user(): string
+{
+	sys = load Sys Sys->PATH;
+
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n];	
+}
+
+getenv(name: string): (int, string)
+{
+	fd := sys->open("#e/"+name, Sys->OREAD);
+	if(fd == nil)
+		return (0, nil);
+	b := array[256] of byte;
+	n := sys->read(fd, b, len b);
+	if(n <= 0)
+		return (1, "");
+	for(i := 0; i < n; i++)
+		if(b[i] == byte 0 || b[i] == byte '\n')
+			break;
+	return (1, string b[0:i]);
+}
+	
+setenv(name: string, val: string)
+{
+	fd := sys->create("#e/"+name, Sys->OWRITE, 8r664);
+	if(fd != nil)
+		sys->fprint(fd, "%s", val);
+}
+
+newuser(user: string, cap: string, nsfile: string): string
+{
+	if(cap == nil)
+		return "no capability";
+
+	sys = load Sys Sys->PATH;
+	fd := sys->open("#¤/capuse", Sys->OWRITE);
+	if(fd == nil)
+		return sys->sprint("opening #¤/capuse: %r");
+
+	b := array of byte cap;
+	if(sys->write(fd, b, len b) < 0)
+		return sys->sprint("writing %s to #¤/capuse: %r", cap);
+
+	# mount factotum as new user (probably unhelpful if not factotum owner)
+	sys->unmount(nil, "/mnt/factotum");
+	sys->bind("#sfactotum", "/mnt/factotum", Sys->MREPL);
+
+	return newns(user, nsfile);
+}
--- /dev/null
+++ b/appl/lib/ninep.b
@@ -1,0 +1,929 @@
+implement Ninep;
+
+include "sys.m";
+	sys: Sys;
+
+include "9p.m";
+
+STR: con BIT16SZ;	# string length
+TAG: con BIT16SZ;
+FID: con BIT32SZ;
+QID: con BIT8SZ+BIT32SZ+BIT64SZ;
+LEN: con BIT16SZ;	# stat and qid array lengths
+COUNT: con BIT32SZ;
+OFFSET: con BIT64SZ;
+
+H: con BIT32SZ+BIT8SZ+BIT16SZ;	# minimum header length: size[4] type tag[2]
+
+#
+# the following array could be shorter if it were indexed by (type-Tversion)
+#
+hdrlen := array[Tmax] of
+{
+Tversion =>	H+COUNT+STR,	# size[4] Tversion tag[2] msize[4] version[s]
+Rversion =>	H+COUNT+STR,	# size[4] Rversion tag[2] msize[4] version[s]
+
+Tauth =>	H+FID+STR+STR,		# size[4] Tauth tag[2] afid[4] uname[s] aname[s]
+Rauth =>	H+QID,			# size[4] Rauth tag[2] aqid[13]
+
+Rerror =>	H+STR,		# size[4] Rerror tag[2] ename[s]
+
+Tflush =>	H+TAG,		# size[4] Tflush tag[2] oldtag[2]
+Rflush =>	H,			# size[4] Rflush tag[2]
+
+Tattach =>	H+FID+FID+STR+STR,	# size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s]
+Rattach =>	H+QID,		# size[4] Rattach tag[2] qid[13]
+
+Twalk =>	H+FID+FID+LEN,	# size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s])
+Rwalk =>	H+LEN,		# size[4] Rwalk tag[2] nwqid[2] nwqid*(wqid[13])
+
+Topen =>	H+FID+BIT8SZ,		# size[4] Topen tag[2] fid[4] mode[1]
+Ropen =>	H+QID+COUNT,	# size[4] Ropen tag[2] qid[13] iounit[4]
+
+Tcreate =>	H+FID+STR+BIT32SZ+BIT8SZ,	# size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1]
+Rcreate =>	H+QID+COUNT,	# size[4] Rcreate tag[2] qid[13] iounit[4]
+
+Tread =>	H+FID+OFFSET+COUNT,	# size[4] Tread tag[2] fid[4] offset[8] count[4]
+Rread =>	H+COUNT,		# size[4] Rread tag[2] count[4] data[count]
+
+Twrite =>	H+FID+OFFSET+COUNT,	# size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count]
+Rwrite =>	H+COUNT,	# size[4] Rwrite tag[2] count[4]
+
+Tclunk =>	H+FID,	# size[4] Tclunk tag[2] fid[4]
+Rclunk =>	H,		# size[4] Rclunk tag[2]
+
+Tremove =>	H+FID,	# size[4] Tremove tag[2] fid[4]
+Rremove =>	H,	# size[4] Rremove tag[2]
+
+Tstat =>	H+FID,	# size[4] Tstat tag[2] fid[4]
+Rstat =>	H+LEN,	# size[4] Rstat tag[2] stat[n]
+
+Twstat =>	H+FID+LEN,	# size[4] Twstat tag[2] fid[4] stat[n]
+Rwstat =>	H,	# size[4] Rwstat tag[2]
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+utflen(s: string): int
+{
+	# the domain is 16-bit unicode only, which is all that Inferno now implements
+	n := l := len s;
+	for(i:=0; i<l; i++)
+		if((c := s[i]) > 16r7F){
+			n++;
+			if(c > 16r7FF)
+				n++;
+		}
+	return n;
+}
+
+packdirsize(d: Sys->Dir): int
+{
+	return STATFIXLEN+utflen(d.name)+utflen(d.uid)+utflen(d.gid)+utflen(d.muid);
+}
+
+packdir(f: Sys->Dir): array of byte
+{
+	ds := packdirsize(f);
+	a := array[ds] of byte;
+	# size[2]
+	a[0] = byte (ds-LEN);
+	a[1] = byte ((ds-LEN)>>8);
+	# type[2]
+	a[2] = byte f.dtype;
+	a[3] = byte (f.dtype>>8);
+	# dev[4]
+	a[4] = byte f.dev;
+	a[5] = byte (f.dev>>8);
+	a[6] = byte (f.dev>>16);
+	a[7] = byte (f.dev>>24);
+	# qid.type[1]
+	# qid.vers[4]
+	# qid.path[8]
+	pqid(a, 8, f.qid);
+	# mode[4]
+	a[21] = byte f.mode;
+	a[22] = byte (f.mode>>8);
+	a[23] = byte (f.mode>>16);
+	a[24] = byte (f.mode>>24);
+	# atime[4]
+	a[25] = byte f.atime;
+	a[26] = byte (f.atime>>8);
+	a[27] = byte (f.atime>>16);
+	a[28] = byte (f.atime>>24);
+	# mtime[4]
+	a[29] = byte f.mtime;
+	a[30] = byte (f.mtime>>8);
+	a[31] = byte (f.mtime>>16);
+	a[32] = byte (f.mtime>>24);
+	# length[8]
+	p64(a, 33, big f.length);
+	# name[s]
+	i := pstring(a, 33+BIT64SZ, f.name);
+	i = pstring(a, i, f.uid);
+	i = pstring(a, i, f.gid);
+	i = pstring(a, i, f.muid);
+	if(i != len a)
+		raise "assertion: Ninep->packdir: bad count";	# can't happen unless packedsize is wrong
+	return a;
+}
+
+pqid(a: array of byte, o: int, q: Sys->Qid): int
+{
+	a[o] = byte q.qtype;
+	v := q.vers;
+	a[o+1] = byte v;
+	a[o+2] = byte (v>>8);
+	a[o+3] = byte (v>>16);
+	a[o+4] = byte (v>>24);
+	v = int q.path;
+	a[o+5] = byte v;
+	a[o+6] = byte (v>>8);
+	a[o+7] = byte (v>>16);
+	a[o+8] = byte (v>>24);
+	v = int (q.path >> 32);
+	a[o+9] = byte v;
+	a[o+10] = byte (v>>8);
+	a[o+11] = byte (v>>16);
+	a[o+12] = byte (v>>24);
+	return o+QID;
+}
+
+pstring(a: array of byte, o: int, s: string): int
+{
+	sa := array of byte s;	# could do conversion ourselves
+	n := len sa;
+	a[o] = byte n;
+	a[o+1] = byte (n>>8);
+	a[o+2:] = sa;
+	return o+LEN+n;
+}
+
+p32(a: array of byte, o: int, v: int): int
+{
+	a[o] = byte v;
+	a[o+1] = byte (v>>8);
+	a[o+2] = byte (v>>16);
+	a[o+3] = byte (v>>24);
+	return o+BIT32SZ;
+}
+
+p64(a: array of byte, o: int, b: big): int
+{
+	i := int b;
+	a[o] = byte i;
+	a[o+1] = byte (i>>8);
+	a[o+2] = byte (i>>16);
+	a[o+3] = byte (i>>24);
+	i = int (b>>32);
+	a[o+4] = byte i;
+	a[o+5] = byte (i>>8);
+	a[o+6] = byte (i>>16);
+	a[o+7] = byte (i>>24);
+	return o+BIT64SZ;
+}
+
+unpackdir(a: array of byte): (int, Sys->Dir)
+{
+	dir: Sys->Dir;
+
+	if(len a < STATFIXLEN)
+		return (0, dir);
+	# size[2]
+	sz := ((int a[1] << 8) | int a[0])+LEN;	# bytes this packed dir should occupy
+	if(len a < sz)
+		return (0, dir);
+	# type[2]
+	dir.dtype = (int a[3]<<8) | int a[2];
+	# dev[4]
+	dir.dev = (((((int a[7] << 8) | int a[6]) << 8) | int a[5]) << 8) | int a[4];
+	# qid.type[1]
+	# qid.vers[4]
+	# qid.path[8]
+	dir.qid = gqid(a, 8);
+	# mode[4]
+	dir.mode = (((((int a[24] << 8) | int a[23]) << 8) | int a[22]) << 8) | int a[21];
+	# atime[4]
+	dir.atime = (((((int a[28] << 8) | int a[27]) << 8) | int a[26]) << 8) | int a[25];
+	# mtime[4]
+	dir.mtime = (((((int a[32] << 8) | int a[31]) << 8) | int a[30]) << 8) | int a[29];
+	# length[8]
+	v0 := (((((int a[36] << 8) | int a[35]) << 8) | int a[34]) << 8) | int a[33];
+	v1 := (((((int a[40] << 8) | int a[39]) << 8) | int a[38]) << 8) | int a[37];
+	dir.length = (big v1 << 32) | (big v0 & 16rFFFFFFFF);
+	# name[s], uid[s], gid[s], muid[s]
+	i: int;
+	(dir.name, i) = gstring(a, 41);
+	(dir.uid, i) = gstring(a, i);
+	(dir.gid, i) = gstring(a, i);
+	(dir.muid, i) = gstring(a, i);
+	if(i != sz)
+		return (0, dir);
+	return (i, dir);
+}
+
+gqid(f: array of byte, i: int): Sys->Qid
+{
+	qtype := int f[i];
+	vers := (((((int f[i+4] << 8) | int f[i+3]) << 8) | int f[i+2]) << 8) | int f[i+1];
+	i += BIT8SZ+BIT32SZ;
+	path0 := (((((int f[i+3] << 8) | int f[i+2]) << 8) | int f[i+1]) << 8) | int f[i];
+	i += BIT32SZ;
+	path1 := (((((int f[i+3] << 8) | int f[i+2]) << 8) | int f[i+1]) << 8) | int f[i];
+	path := (big path1 << 32) | (big path0 & 16rFFFFFFFF);
+	return (path, vers, qtype);
+}
+
+g32(f: array of byte, i: int): int
+{
+	return (((((int f[i+3] << 8) | int f[i+2]) << 8) | int f[i+1]) << 8) | int f[i];
+}
+
+g64(f: array of byte, i: int): big
+{
+	b0 := (((((int f[i+3] << 8) | int f[i+2]) << 8) | int f[i+1]) << 8) | int f[i];
+	b1 := (((((int f[i+7] << 8) | int f[i+6]) << 8) | int f[i+5]) << 8) | int f[i+4];
+	return (big b1 << 32) | (big b0 & 16rFFFFFFFF);
+}
+
+gstring(a: array of byte, o: int): (string, int)
+{
+	if(o < 0 || o+STR > len a)
+		return (nil, -1);
+	l := (int a[o+1] << 8) | int a[o];
+	o += STR;
+	e := o+l;
+	if(e > len a)
+		return (nil, -1);
+	return (string a[o:e], e);
+}
+
+ttag2type := array[] of {
+tagof Tmsg.Readerror => 0,
+tagof Tmsg.Version => Tversion,
+tagof Tmsg.Auth => Tauth,
+tagof Tmsg.Attach => Tattach,
+tagof Tmsg.Flush => Tflush,
+tagof Tmsg.Walk => Twalk,
+tagof Tmsg.Open => Topen,
+tagof Tmsg.Create => Tcreate,
+tagof Tmsg.Read => Tread,
+tagof Tmsg.Write => Twrite,
+tagof Tmsg.Clunk => Tclunk,
+tagof Tmsg.Stat => Tstat,
+tagof Tmsg.Remove => Tremove,
+tagof Tmsg.Wstat => Twstat,
+};
+
+Tmsg.mtype(t: self ref Tmsg): int
+{
+	return ttag2type[tagof t];
+}
+
+Tmsg.packedsize(t: self ref Tmsg): int
+{
+	mtype := ttag2type[tagof t];
+	if(mtype <= 0)
+		return 0;
+	ml := hdrlen[mtype];
+	pick m := t {
+	Version =>
+		ml += utflen(m.version);
+	Auth =>
+		ml += utflen(m.uname)+utflen(m.aname);
+	Attach =>
+		ml += utflen(m.uname)+utflen(m.aname);
+	Walk =>
+		for(i:=0; i<len m.names; i++)
+			ml += STR+utflen(m.names[i]);
+	Create =>
+		ml += utflen(m.name);
+	Write =>
+		ml += len m.data;
+	Wstat =>
+		ml += packdirsize(m.stat);
+	}
+	return ml;
+}
+
+Tmsg.pack(t: self ref Tmsg): array of byte
+{
+	if(t == nil)
+		return nil;
+	ds := t.packedsize();
+	if(ds <= 0)
+		return nil;
+	d := array[ds] of byte;
+	d[0] = byte ds;
+	d[1] = byte (ds>>8);
+	d[2] = byte (ds>>16);
+	d[3] = byte (ds>>24);
+	d[4] = byte ttag2type[tagof t];
+	d[5] = byte t.tag;
+	d[6] = byte (t.tag >> 8);
+	pick m := t {
+	Version =>
+		p32(d, H, m.msize);
+		pstring(d, H+COUNT, m.version);
+	Auth =>
+		p32(d, H, m.afid);
+		o := pstring(d, H+FID, m.uname);
+		pstring(d, o, m.aname);
+	Flush =>
+		v := m.oldtag;
+		d[H] = byte v;
+		d[H+1] = byte (v>>8);
+	Attach =>
+		p32(d, H, m.fid);
+		p32(d, H+FID, m.afid);
+		o := pstring(d, H+2*FID, m.uname);
+		pstring(d, o, m.aname);
+	Walk =>
+		d[H] = byte m.fid;
+		d[H+1] = byte (m.fid>>8);
+		d[H+2] = byte (m.fid>>16);
+		d[H+3] = byte (m.fid>>24);
+		d[H+FID] = byte m.newfid;
+		d[H+FID+1] = byte (m.newfid>>8);
+		d[H+FID+2] = byte (m.newfid>>16);
+		d[H+FID+3] = byte (m.newfid>>24);
+		n := len m.names;
+		d[H+2*FID] = byte n;
+		d[H+2*FID+1] = byte (n>>8);
+		o := H+2*FID+LEN;
+		for(i := 0; i < n; i++)
+			o = pstring(d, o, m.names[i]);
+	Open =>
+		p32(d, H, m.fid);
+		d[H+FID] = byte m.mode;
+	Create =>
+		p32(d, H, m.fid);
+		o := pstring(d, H+FID, m.name);
+		p32(d, o, m.perm);
+		d[o+BIT32SZ] = byte m.mode;
+	Read =>
+		p32(d, H, m.fid);
+		p64(d, H+FID, m.offset);
+		p32(d, H+FID+OFFSET, m.count);
+	Write =>
+		p32(d, H, m.fid);
+		p64(d, H+FID, m.offset);
+		n := len m.data;
+		p32(d, H+FID+OFFSET, n);
+		d[H+FID+OFFSET+COUNT:] = m.data;
+	Clunk or Remove or Stat =>
+		p32(d, H, m.fid);
+	Wstat =>
+		p32(d, H, m.fid);
+		stat := packdir(m.stat);
+		n := len stat;
+		d[H+FID] = byte n;
+		d[H+FID+1] = byte (n>>8);
+		d[H+FID+LEN:] = stat;
+	* =>
+		raise sys->sprint("assertion: Ninep->Tmsg.pack: bad tag: %d", tagof t);
+	}
+	return d;
+}
+
+Tmsg.unpack(f: array of byte): (int, ref Tmsg)
+{
+	if(len f < H)
+		return (0, nil);
+	size := (int f[1] << 8) | int f[0];
+	size |= ((int f[3] << 8) | int f[2]) << 16;
+	if(len f != size){
+		if(len f < size)
+			return (0, nil);	# need more data
+		f = f[0:size];	# trim to exact length
+	}
+	mtype := int f[4];
+	if(mtype >= len hdrlen || (mtype&1) != 0 || size < hdrlen[mtype])
+		return (-1, nil);
+
+	tag := (int f[6] << 8) | int f[5];
+	fid := 0;
+	if(hdrlen[mtype] >= H+FID)
+		fid = g32(f, H);	# fid is always in same place: extract it once for all if there
+
+	# return out of each case body for a legal message;
+	# break out of the case for an illegal one
+
+Decode:
+	case mtype {
+	* =>
+		sys->print("styx: Tmsg.unpack: bad type %d\n", mtype);
+	Tversion =>
+		msize := fid;
+		(version, o) := gstring(f, H+COUNT);
+		if(o <= 0)
+			break;
+		return (o, ref Tmsg.Version(tag, msize, version));
+	Tauth =>
+		(uname, o1) := gstring(f, H+FID);
+		(aname, o2) := gstring(f, o1);
+		if(o2 <= 0)
+			break;
+		return (o2, ref Tmsg.Auth(tag, fid, uname, aname));
+	Tflush =>
+		oldtag := (int f[H+1] << 8) | int f[H];
+		return (H+TAG, ref Tmsg.Flush(tag, oldtag));
+	Tattach =>
+		afid := g32(f, H+FID);
+		(uname, o1) := gstring(f, H+2*FID);
+		(aname, o2) := gstring(f, o1);
+		if(o2 <= 0)
+			break;
+		return (o2, ref Tmsg.Attach(tag, fid, afid, uname, aname));
+	Twalk =>
+		newfid := g32(f, H+FID);
+		n := (int f[H+2*FID+1] << 8) | int f[H+2*FID];
+		if(n > MAXWELEM)
+			break;
+		o := H+2*FID+LEN;
+		names: array of string = nil;
+		if(n > 0){
+			names = array[n] of string;
+			for(i:=0; i<n; i++){
+				(names[i], o) = gstring(f, o);
+				if(o <= 0)
+					break Decode;
+			}
+		}
+		return (o, ref Tmsg.Walk(tag, fid, newfid, names));
+	Topen =>
+		return (H+FID+BIT8SZ, ref Tmsg.Open(tag, fid, int f[H+FID]));
+	Tcreate =>
+		(name, o) := gstring(f, H+FID);
+		if(o <= 0 || o+BIT32SZ+BIT8SZ > len f)
+			break;
+		perm := g32(f, o);
+		o += BIT32SZ;
+		mode := int f[o++];
+		return (o, ref Tmsg.Create(tag, fid, name, perm, mode));
+	Tread =>
+		offset := g64(f, H+FID);
+		count := g32(f, H+FID+OFFSET);
+		return (H+FID+OFFSET+COUNT, ref Tmsg.Read(tag, fid, offset, count));
+	Twrite =>
+		offset := g64(f, H+FID);
+		count := g32(f, H+FID+OFFSET);
+		O: con H+FID+OFFSET+COUNT;
+		if(count > len f-O)
+			break;
+		data := f[O:O+count];
+		return (O+count, ref Tmsg.Write(tag, fid, offset, data));
+	Tclunk =>
+		return (H+FID, ref Tmsg.Clunk(tag, fid));
+	Tremove =>
+		return (H+FID, ref Tmsg.Remove(tag, fid));
+	Tstat =>
+		return (H+FID, ref Tmsg.Stat(tag, fid));
+	Twstat =>
+		n := int (f[H+FID+1]<<8) | int f[H+FID];
+		if(len f < H+FID+LEN+n)
+			break;
+		(ds, stat) := unpackdir(f[H+FID+LEN:]);
+		if(ds != n){
+			sys->print("Ninep->Tmsg.unpack: wstat count: %d/%d\n", ds, n);	# temporary
+			break;
+		}
+		return (H+FID+LEN+n, ref Tmsg.Wstat(tag, fid, stat));
+	}
+	return (-1, nil);		# illegal
+}
+
+tmsgname := array[] of {
+tagof Tmsg.Readerror => "Readerror",
+tagof Tmsg.Version => "Version",
+tagof Tmsg.Auth => "Auth",
+tagof Tmsg.Attach => "Attach",
+tagof Tmsg.Flush => "Flush",
+tagof Tmsg.Walk => "Walk",
+tagof Tmsg.Open => "Open",
+tagof Tmsg.Create => "Create",
+tagof Tmsg.Read => "Read",
+tagof Tmsg.Write => "Write",
+tagof Tmsg.Clunk => "Clunk",
+tagof Tmsg.Stat => "Stat",
+tagof Tmsg.Remove => "Remove",
+tagof Tmsg.Wstat => "Wstat",
+};
+
+Tmsg.text(t: self ref Tmsg): string
+{
+	if(t == nil)
+		return "nil";
+	s := sys->sprint("Tmsg.%s(%ud", tmsgname[tagof t], t.tag);
+	pick m:= t {
+	* =>
+		return s + ",ILLEGAL)";
+	Readerror =>
+		return s + sys->sprint(",\"%s\")", m.error);
+	Version =>
+		return s + sys->sprint(",%d,\"%s\")", m.msize, m.version);
+	Auth =>
+		return s + sys->sprint(",%ud,\"%s\",\"%s\")", m.afid, m.uname, m.aname);
+	Flush =>
+		return s + sys->sprint(",%ud)", m.oldtag);
+	Attach =>
+		return s + sys->sprint(",%ud,%ud,\"%s\",\"%s\")", m.fid, m.afid, m.uname, m.aname);
+	Walk =>
+		s += sys->sprint(",%ud,%ud", m.fid, m.newfid);
+		if(len m.names != 0){
+			s += ",array[] of {";
+			for(i := 0; i < len m.names; i++){
+				c := ",";
+				if(i == 0)
+					c = "";
+				s += sys->sprint("%s\"%s\"", c, m.names[i]);
+			}
+			s += "}";
+		}else
+			s += ",nil";
+		return s + ")";
+	Open =>
+		return s + sys->sprint(",%ud,%d)", m.fid, m.mode);
+	Create =>
+		return s + sys->sprint(",%ud,\"%s\",8r%uo,%d)", m.fid, m.name, m.perm, m.mode);
+	Read =>
+		return s + sys->sprint(",%ud,%bd,%ud)", m.fid, m.offset, m.count);
+	Write =>
+		return s + sys->sprint(",%ud,%bd,array[%d] of byte)", m.fid, m.offset, len m.data);
+	Clunk or
+	Remove or
+	Stat =>
+		return s + sys->sprint(",%ud)", m.fid);
+	Wstat =>
+		return s + sys->sprint(",%ud,%s)", m.fid, dir2text(m.stat));
+	}
+}
+
+Tmsg.read(fd: ref Sys->FD, msglim: int): ref Tmsg
+{
+	(msg, err) := readmsg(fd, msglim);
+	if(err != nil)
+		return ref Tmsg.Readerror(0, err);
+	if(msg == nil)
+		return nil;
+	(nil, m) := Tmsg.unpack(msg);
+	if(m == nil)
+		return ref Tmsg.Readerror(0, "bad 9P T-message format");
+	return m;
+}
+
+rtag2type := array[] of {
+tagof Rmsg.Version	=> Rversion,
+tagof Rmsg.Auth	=> Rauth,
+tagof Rmsg.Error	=> Rerror,
+tagof Rmsg.Flush	=> Rflush,
+tagof Rmsg.Attach	=> Rattach,
+tagof Rmsg.Walk	=> Rwalk,
+tagof Rmsg.Open	=> Ropen,
+tagof Rmsg.Create	=> Rcreate,
+tagof Rmsg.Read	=> Rread,
+tagof Rmsg.Write	=> Rwrite,
+tagof Rmsg.Clunk	=> Rclunk,
+tagof Rmsg.Remove	=> Rremove,
+tagof Rmsg.Stat	=> Rstat,
+tagof Rmsg.Wstat	=> Rwstat,
+};
+
+Rmsg.mtype(r: self ref Rmsg): int
+{
+	return rtag2type[tagof r];
+}
+
+Rmsg.packedsize(r: self ref Rmsg): int
+{
+	mtype := rtag2type[tagof r];
+	if(mtype <= 0)
+		return 0;
+	ml := hdrlen[mtype];
+	pick m := r {
+	Version =>
+		ml += utflen(m.version);
+	Error =>
+		ml += utflen(m.ename);
+	Walk =>
+		ml += QID*len m.qids;
+	Read =>
+		ml += len m.data;
+	Stat =>
+		ml += packdirsize(m.stat);
+	}
+	return ml;
+}
+
+Rmsg.pack(r: self ref Rmsg): array of byte
+{
+	if(r == nil)
+		return nil;
+	ps := r.packedsize();
+	if(ps <= 0)
+		return nil;
+	d := array[ps] of byte;
+	d[0] = byte ps;
+	d[1] = byte (ps>>8);
+	d[2] = byte (ps>>16);
+	d[3] = byte (ps>>24);
+	d[4] = byte rtag2type[tagof r];
+	d[5] = byte r.tag;
+	d[6] = byte (r.tag >> 8);
+	pick m := r {
+	Version =>
+		p32(d, H, m.msize);
+		pstring(d, H+BIT32SZ, m.version);
+	Auth =>
+		pqid(d, H, m.aqid);
+	Flush or
+	Clunk or
+	Remove or
+	Wstat =>
+		;	# nothing more required
+	Error	=>
+		pstring(d, H, m.ename);
+	Attach =>
+		pqid(d, H, m.qid);
+	Walk =>
+		n := len m.qids;
+		d[H] = byte n;
+		d[H+1] = byte (n>>8);
+		o := H+LEN;
+		for(i:=0; i<n; i++){
+			pqid(d, o, m.qids[i]);
+			o += QID;
+		}
+	Create or
+	Open =>
+		pqid(d, H, m.qid);
+		p32(d, H+QID, m.iounit);
+	Read =>
+		v := len m.data;
+		d[H] = byte v;
+		d[H+1] = byte (v>>8);
+		d[H+2] = byte (v>>16);
+		d[H+3] = byte (v>>24);
+		d[H+4:] = m.data;
+	Write =>
+		v := m.count;
+		d[H] = byte v;
+		d[H+1] = byte (v>>8);
+		d[H+2] = byte (v>>16);
+		d[H+3] = byte (v>>24);
+	Stat =>
+		stat := packdir(m.stat);
+		v := len stat;
+		d[H] = byte v;
+		d[H+1] = byte (v>>8);
+		d[H+2:] = stat;		# should avoid copy?
+	* =>
+		raise sys->sprint("assertion: Ninep->Rmsg.pack: missed case: tag %d", tagof r);
+	}
+	return d;
+}
+
+Rmsg.unpack(f: array of byte): (int, ref Rmsg)
+{
+	if(len f < H)
+		return (0, nil);
+	size := (int f[1] << 8) | int f[0];
+	size |= ((int f[3] << 8) | int f[2]) << 16;	# size includes itself
+	if(len f != size){
+		if(len f < size)
+			return (0, nil);	# need more data
+		f = f[0:size];	# trim to exact length
+	}
+	mtype := int f[4];
+	if(mtype >= len hdrlen || (mtype&1) == 0 || size < hdrlen[mtype])
+		return (-1, nil);
+
+	tag := (int f[6] << 8) | int f[5];
+
+	# return out of each case body for a legal message;
+	# break out of the case for an illegal one
+
+	case mtype {
+	* =>
+		sys->print("Ninep->Rmsg.unpack: bad type %d\n", mtype);	# temporary
+	Rversion =>
+		msize := g32(f, H);
+		(version, o) := gstring(f, H+BIT32SZ);
+		if(o <= 0)
+			break;
+		return (o, ref Rmsg.Version(tag, msize, version));
+	Rauth =>
+		return (H+QID, ref Rmsg.Auth(tag, gqid(f, H)));
+	Rflush =>
+		return (H, ref Rmsg.Flush(tag));
+	Rerror =>
+		(ename, o) := gstring(f, H);
+		if(o <= 0)
+			break;
+		return (o, ref Rmsg.Error(tag, ename));
+	Rclunk =>
+		return (H, ref Rmsg.Clunk(tag));
+	Rremove =>
+		return (H, ref Rmsg.Remove(tag));
+	Rwstat=>
+		return (H, ref Rmsg.Wstat(tag));
+	Rattach =>
+		return (H+QID, ref Rmsg.Attach(tag, gqid(f, H)));
+	Rwalk =>
+		nqid := (int f[H+1] << 8) | int f[H];
+		if(len f < H+LEN+nqid*QID)
+			break;
+		o := H+LEN;
+		qids := array[nqid] of Sys->Qid;
+		for(i:=0; i<nqid; i++){
+			qids[i] = gqid(f, o);
+			o += QID;
+		}
+		return (o, ref Rmsg.Walk(tag, qids));
+	Ropen =>
+		return (H+QID+COUNT, ref Rmsg.Open(tag, gqid(f, H), g32(f, H+QID)));
+	Rcreate=>
+		return (H+QID+COUNT, ref Rmsg.Create(tag, gqid(f, H), g32(f, H+QID)));
+	Rread =>
+		count := g32(f, H);
+		if(len f < H+COUNT+count)
+			break;
+		data := f[H+COUNT:H+COUNT+count];
+		return (H+COUNT+count, ref Rmsg.Read(tag, data));
+	Rwrite =>
+		return (H+COUNT, ref Rmsg.Write(tag, g32(f, H)));
+	Rstat =>
+		n := (int f[H+1] << 8) | int f[H];
+		if(len f < H+LEN+n)
+			break;
+		(ds, d) := unpackdir(f[H+LEN:]);
+		if(ds <= 0)
+			break;
+		if(ds != n){
+			sys->print("Ninep->Rmsg.unpack: stat count: %d/%d\n", ds, n);		# temporary
+			break;
+		}
+		return (H+LEN+n, ref Rmsg.Stat(tag, d));
+	}
+	return (-1, nil);		# illegal
+}
+
+rmsgname := array[] of {
+tagof Rmsg.Version => "Version",
+tagof Rmsg.Auth => "Auth",
+tagof Rmsg.Attach => "Attach",
+tagof Rmsg.Error => "Error",
+tagof Rmsg.Flush => "Flush",
+tagof Rmsg.Walk => "Walk",
+tagof Rmsg.Create => "Create",
+tagof Rmsg.Open => "Open",
+tagof Rmsg.Read => "Read",
+tagof Rmsg.Write => "Write",
+tagof Rmsg.Clunk => "Clunk",
+tagof Rmsg.Remove => "Remove",
+tagof Rmsg.Stat => "Stat",
+tagof Rmsg.Wstat => "Wstat",
+};
+
+Rmsg.text(r: self ref Rmsg): string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(r == nil)
+		return "nil";
+	s := sys->sprint("Rmsg.%s(%ud", rmsgname[tagof r], r.tag);
+	pick m := r {
+	* =>
+		return s + "ERROR)";
+	Readerror =>
+		return s + sys->sprint(",\"%s\")", m.error);
+	Version =>
+		return s + sys->sprint(",%d,\"%s\")", m.msize, m.version);
+	Auth =>
+		return s+sys->sprint(",%s)", qid2text(m.aqid));
+	Error =>
+		return s+sys->sprint(",\"%s\")", m.ename);
+	Flush or
+	Clunk or
+	Remove or
+	Wstat =>
+		return s+")";
+	Attach =>
+		return s+sys->sprint(",%s)", qid2text(m.qid));
+	Walk	 =>
+		s += ",array[] of {";
+		for(i := 0; i < len m.qids; i++){
+			c := "";
+			if(i != 0)
+				c = ",";
+			s += sys->sprint("%s%s", c, qid2text(m.qids[i]));
+		}
+		return s+"})";
+	Create or
+	Open =>
+		return s+sys->sprint(",%s,%d)", qid2text(m.qid), m.iounit);
+	Read =>
+		return s+sys->sprint(",array[%d] of byte)", len m.data);
+	Write =>
+		return s+sys->sprint(",%d)", m.count);
+	Stat =>
+		return s+sys->sprint(",%s)", dir2text(m.stat));
+	}
+}
+
+Rmsg.read(fd: ref Sys->FD, msglim: int): ref Rmsg
+{
+	(msg, err) := readmsg(fd, msglim);
+	if(err != nil)
+		return ref Rmsg.Readerror(0, err);
+	if(msg == nil)
+		return nil;
+	(nil, m) := Rmsg.unpack(msg);
+	if(m == nil)
+		return ref Rmsg.Readerror(0, "bad 9P R-message format");
+	return m;
+}
+
+Rmsg.write(m: self ref Rmsg, fd: ref Sys->FD, msize: int): int
+{
+	if(msize == 0)
+		m = ref Rmsg.Error(m.tag, "Tversion not seen");
+	d := m.pack();
+	if(msize != 0 && len d > msize){
+		m = ref Rmsg.Error(m.tag, "9P reply didn't fit");
+		d = m.pack();
+	}
+	n := len d;
+	if(sys->write(fd, d, n) != n)
+		return -1;
+	return 0;
+}
+
+dir2text(d: Sys->Dir): string
+{
+	return sys->sprint("Dir(\"%s\",\"%s\",\"%s\",%s,8r%uo,%d,%d,%bd,16r%ux,%d)",
+		d.name, d.uid, d.gid, qid2text(d.qid), d.mode, d.atime, d.mtime, d.length, d.dtype, d.dev);
+}
+
+qid2text(q: Sys->Qid): string
+{
+	return sys->sprint("Qid(16r%ubx,%d,16r%.2ux)", q.path, q.vers, q.qtype);
+}
+
+readmsg(fd: ref Sys->FD, msglim: int): (array of byte, string)
+{
+	if(msglim <= 0)
+		msglim = DEFMSIZE;
+	sbuf := array[BIT32SZ] of byte;
+	if((n := sys->readn(fd, sbuf, BIT32SZ)) != BIT32SZ){
+		if(n == 0)
+			return (nil, nil);
+		return (nil, sys->sprint("%r"));
+	}
+	ml := (int sbuf[1] << 8) | int sbuf[0];
+	ml |= ((int sbuf[3] << 8) | int sbuf[2]) << 16;
+	if(ml <= BIT32SZ)
+		return (nil, "invalid 9P message size");
+	if(ml > msglim)
+		return (nil, "9P message longer than agreed");
+	buf := array[ml] of byte;
+	buf[0:] = sbuf;
+	if((n = sys->readn(fd, buf[BIT32SZ:], ml-BIT32SZ)) != ml-BIT32SZ){
+		if(n == 0)
+			return (nil, "9P message truncated");
+		return (nil, sys->sprint("%r"));
+	}
+	return (buf, nil);
+}
+
+istmsg(f: array of byte): int
+{
+	if(len f < H)
+		return -1;
+	return (int f[BIT32SZ] & 1) == 0;
+}
+
+compatible(t: ref Tmsg.Version, msize: int, version: string): (int, string)
+{
+	if(version == nil)
+		version = VERSION;
+	if(t.msize < msize)
+		msize = t.msize;
+	v := t.version;
+	if(len v < 2 || v[0:2] != "9P")
+		return (msize, "unknown");
+	for(i:=2; i<len v; i++)
+		if((c := v[i]) == '.'){
+			v = v[0:i];
+			break;
+		}else if(!(c >= '0' && c <= '9'))
+			return (msize, "unknown");	# fussier than Plan 9
+	if(v < VERSION)
+		return (msize, "unknown");
+	if(v < version)
+		version = v;
+	return (msize, version);
+}
--- /dev/null
+++ b/appl/lib/oldauth.b
@@ -1,0 +1,344 @@
+implement Oldauth;
+
+#
+# TO DO
+#	- more error checking?
+#	- details of auth error handling
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "ipints.m";
+	ipints: IPints;
+	IPint: import ipints;
+
+include "crypt.m";
+	crypt: Crypt;
+	PK, SK, PKsig: import crypt;
+
+include "msgio.m";
+	msgio: Msgio;
+
+include "oldauth.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	ipints = load IPints IPints->PATH;
+	crypt = load Crypt Crypt->PATH;
+	msgio = load Msgio Msgio->PATH;
+	msgio->init();
+}
+
+efmt()
+{
+	sys->werrstr("input or format error");
+}
+
+readauthinfo(filename: string): ref Authinfo
+{
+	fd := sys->open(filename, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	a := array[5] of string;
+	for(i := 0; i < len a; i++){
+		(s, err) := getstr(fd);
+		if(err != nil){
+			sys->werrstr(sys->sprint("%q: input or format error", filename));
+			return nil;
+		}
+		a[i] = s;
+	}
+	info := ref Authinfo;
+	(info.spk, nil) = strtopk(a[0]);
+	info.cert = strtocert(a[1]);
+	(info.mysk, info.owner) = strtosk(a[2]);
+	if(info.spk == nil || info.cert == nil || info.mysk == nil){
+		efmt();
+		return nil;
+	}
+	info.mypk = crypt->sktopk(info.mysk);
+	info.alpha = IPint.strtoip(a[3], 64);
+	info.p = IPint.strtoip(a[4], 64);
+	if(info.alpha == nil || info.p == nil){
+		efmt();
+		return nil;
+	}
+	return info;
+}
+
+writeauthinfo(filename: string, info: ref Authinfo): int
+{
+	if(info.alpha == nil || info.p == nil ||
+	   info.spk == nil || info.mysk == nil || info.cert == nil){
+		sys->werrstr("invalid authinfo");
+		return -1;
+	}
+	a := array[5] of string;
+	a[0] = pktostr(info.spk, info.cert.signer);	# signer's public key
+	a[1] = certtostr(info.cert);	# certificate for my public key
+	a[2] = sktostr(info.mysk, info.owner);	# my secret/public key
+	a[3] = b64(info.alpha);	# diffie hellman base
+	a[4] = b64(info.p);	# diffie hellman modulus
+	fd := sys->open(filename, Sys->OWRITE|Sys->OTRUNC);
+	if(fd == nil){
+		fd = sys->create(filename, Sys->OWRITE, 8r600);
+		if(fd == nil){
+			fd = sys->open(filename, Sys->OWRITE);
+			if(fd == nil)
+				return -1;
+		}
+	}
+	for(i := 0; i < len a; i++)
+		if(sendstr(fd, a[i]) <= 0)
+			return -1;
+	return 0;
+}
+
+sendstr(fd: ref Sys->FD, s: string): int
+{
+	a := array of byte s;
+	return msgio->sendmsg(fd, a, len a);
+}
+
+getstr(fd: ref Sys->FD): (string, string)
+{
+	b := msgio->getmsg(fd);
+	if(b == nil)
+		return (nil, sys->sprint("%r"));
+	return (string b, nil);
+}
+
+certtostr(c: ref Certificate): string
+{
+	s := sys->sprint("%s\n%s\n%s\n%ud\n", c.sa, c.ha, c.signer, c.exp);
+	pick r := c.sig {
+	RSA =>
+		s += b64(r.n)+"\n";
+	Elgamal =>
+		s += b64(r.r)+"\n"+b64(r.s)+"\n";
+	DSA =>
+		s += b64(r.r)+"\n"+b64(r.s)+"\n";
+	* =>
+		raise "unknown key type";
+	}
+	return s;
+}
+
+pktostr(pk: ref PK, owner: string): string
+{
+	pick k := pk {
+	RSA =>
+		s := sys->sprint("rsa\n%s\n", owner);
+		s += b64(k.n)+"\n"+b64(k.ek)+"\n";
+		return s;
+	Elgamal =>
+		s := sys->sprint("elgamal\n%s\n", owner);
+		s += b64(k.p)+"\n"+b64(k.alpha)+"\n"+b64(k.key)+"\n";
+		return s;
+	DSA =>
+		s := sys->sprint("dsa\n%s\n", owner);
+		s += b64(k.p)+"\n"+b64(k.q)+"\n"+b64(k.alpha)+"\n"+b64(k.key)+"\n";
+		return s;
+	* =>
+		raise "unknown key type";
+	}
+}
+
+sktostr(sk: ref SK, owner: string): string
+{
+	pick k := sk {
+	RSA =>
+		s := sys->sprint("rsa\n%s\n", owner);
+		s += b64(k.pk.n)+"\n"+b64(k.pk.ek)+"\n"+b64(k.dk)+"\n"+
+			b64(k.p)+"\n"+b64(k.q)+"\n"+
+			b64(k.kp)+"\n"+b64(k.kq)+"\n"+
+			k.c2.iptob64()+"\n";
+		return s;
+	Elgamal =>
+		pk := k.pk;
+		s := sys->sprint("elgamal\n%s\n", owner);
+		s += b64(pk.p)+"\n"+b64(pk.alpha)+"\n"+b64(pk.key)+"\n"+b64(k.secret)+"\n";
+		return s;
+	DSA =>
+		pk := k.pk;
+		s := sys->sprint("dsa\n%s\n", owner);
+		s += b64(pk.p)+"\n"+b64(pk.q)+"\n"+b64(pk.alpha)+"\n"+b64(k.secret)+"\n";
+		return s;
+	* =>
+		raise "unknown key type";
+	}
+}
+
+fields(s: string): array of string
+{
+	(nf, flds) := sys->tokenize(s, "\n^");
+	a := array[nf] of string;
+	for(i := 0; i < len a; i++){
+		a[i] = hd flds;
+		flds = tl flds;
+	}
+	return a;
+}
+
+bigs(a: array of string): array of ref IPint
+{
+	b := array[len a] of ref IPint;
+	for(i := 0; i < len b; i++){
+		b[i] = IPint.strtoip(a[i], 64);
+		if(b[i] == nil)
+			return nil;
+	}
+	return b;
+}
+
+need[T](a: array of T, min: int): int
+{
+	if(len a < min){
+		efmt();
+		return 1;
+	}
+	return 0;
+}
+
+strtocert(s: string): ref Certificate
+{
+	f := fields(s);
+	if(need(f, 4))
+		return nil;
+	sa := f[0];
+	ha := f[1];
+	signer := f[2];
+	exp := int big f[3];	# unsigned
+	b := bigs(f[4:]);
+	case f[0] {
+	"rsa" =>
+		if(need(b, 1))
+			return nil;
+		return ref Certificate(sa, ha, signer, exp, ref PKsig.RSA(b[0]));
+	"elgamal" =>
+		if(need(b, 2))
+			return nil;
+		return ref Certificate(sa, ha, signer, exp, ref PKsig.Elgamal(b[0], b[1]));
+	"dsa" =>
+		if(need(b, 2))
+			return nil;
+		return ref Certificate(sa, ha, signer, exp, ref PKsig.DSA(b[0], b[1]));
+	* =>
+		sys->werrstr("unknown algorithm: "+f[0]);
+		return nil;
+	}
+}
+
+strtopk(s: string): (ref PK, string)
+{
+	f := fields(s);
+	if(need(f, 3))
+		return (nil, "format error");
+	sa := f[0];
+	owner := f[1];
+	b := bigs(f[2:]);
+	case sa {
+	"rsa" =>
+		if(need(b, 2))
+			return (nil, "format error");
+		return (ref PK.RSA(b[0], b[1]), owner);
+	"elgamal" =>
+		if(need(b, 3))
+			return (nil, "format error");
+		return (ref PK.Elgamal(b[0], b[1], b[2]), owner);
+	"dsa" =>
+		if(need(b, 4))
+			return (nil, "format error");
+		return (ref PK.DSA(b[0], b[1], b[2], b[3]), owner);
+	* =>
+		return (nil, "unknown algorithm: "+f[0]);
+	}
+}
+
+strtosk(s: string): (ref SK, string)
+{
+	f := fields(s);
+	if(need(f, 3))
+		return (nil, "format error");
+	sa := f[0];
+	owner := f[1];
+	b := bigs(f[2:]);
+	case sa {
+	"rsa" =>
+		if(need(b, 8))
+			return (nil, "format error");
+		return (ref SK.RSA(ref PK.RSA(b[0], b[1]), b[2], b[3], b[4], b[5], b[6], b[7]), owner);
+	"elgamal" =>
+		if(need(b, 4))
+			return (nil, "format error");
+		return (ref SK.Elgamal(ref PK.Elgamal(b[0], b[1], b[2]), b[3]), owner);
+	"dsa" =>
+		if(need(b, 5))
+			return (nil, "format error");
+		return (ref SK.DSA(ref PK.DSA(b[0], b[1], b[2], b[3]), b[4]), owner);
+	* =>
+		return (nil, "unknown algorithm: "+f[0]);
+	}
+}
+
+skalg(sk: ref SK): string
+{
+	if(sk == nil)
+		return "nil";
+	case tagof sk {
+	tagof SK.RSA =>	return "rsa";
+	tagof SK.Elgamal =>	return "elgamal";
+	tagof SK.DSA =>	return "dsa";
+	* =>	return "gok";
+	}
+}
+
+sign(sk: ref SK, signer: string, exp: int, state: ref Crypt->DigestState, ha: string): ref Certificate
+{
+	# add signer name and expiration time to hash
+	if(state == nil)
+		return nil;
+	a := sys->aprint("%s %d", signer, exp);
+	digest := hash(ha, a, state);
+	if(digest == nil)
+		return nil;
+	b := IPint.bebytestoip(digest);
+	return ref Certificate(skalg(sk), ha, signer, exp, crypt->sign(sk, b));
+}
+
+verify(pk: ref PK, cert: ref Certificate, state: ref Crypt->DigestState): int
+{
+	if(state == nil)
+		return 0;
+	a := sys->aprint("%s %d", cert.signer, cert.exp);
+	digest := hash(cert.ha, a, state);
+	if(digest == nil)
+		return 0;
+	b := IPint.bebytestoip(digest);
+	return crypt->verify(pk, cert.sig, b);
+}
+
+hash(ha: string, a: array of byte, state: ref Crypt->DigestState): array of byte
+{
+	digest: array of byte;
+	case ha {
+	"sha" or "sha1" =>
+		digest = array[Crypt->SHA1dlen] of byte;
+		crypt->sha1(a, len a, digest, state);
+	"md5" =>
+		digest = array[Crypt->MD5dlen] of byte;
+		crypt->md5(a, len a, digest, state);
+	* =>
+		# don't bother with md4
+		sys->werrstr("unimplemented algorithm: "+ha);
+		return nil;
+	}
+	return digest;
+}
+
+b64(ip: ref IPint): string
+{
+	return ip.iptob64z();
+}
--- /dev/null
+++ b/appl/lib/palm.b
@@ -1,0 +1,504 @@
+implement Palm;
+
+#
+# Copyright © 2001-2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# Based on ``Palm® File Format Specification'', Document Number 3008-004, 1 May 2001, by Palm Inc.
+# Doc compression based on description by Paul Lucas, 18 August 1998
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "palm.m";
+
+# Exact value of "Jan 1, 1970 0:00:00 GMT" - "Jan 1, 1904 0:00:00 GMT"
+Epochdelta: con 2082844800;
+tzoff := 0;
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return "can't load required module";
+	tzoff = daytime->local(0).tzoff;
+	return nil;
+}
+
+Record.new(id: int, attr: int, cat: int, size: int): ref Record
+{
+	return ref Record(id, attr, cat, array[size] of byte);
+}
+
+Resource.new(name: int, id: int, size: int): ref Resource
+{
+	return ref Resource(name, id, array[size] of byte);
+}
+
+Doc.open(m: Palmdb, file: ref Palmdb->PDB): (ref Doc, string)
+{
+	info := m->file.db.stat();
+	if(info.dtype != "TEXt" || info.creator != "REAd")
+		return (nil, "not a Doc file: wrong type or creator");
+	r := m->file.read(0);
+	if(r == nil)
+		return (nil, sys->sprint("not a valid Doc file: %r"));
+	a := r.data;
+	if(len a < 16)
+		return (nil, sys->sprint("not a valid Doc file: bad length: %d", len a));
+	maxrec := m->file.db.nentries()-1;
+	d := ref Doc;
+	d.m = m;
+	d.file = file;
+	d.version = get2(a);
+	err := "unknown";
+	if(d.version != 1 && d.version != 2)
+		err = "unknown Docfile version";
+	# a[2:] is spare
+	d.length = get4(a[4:]);
+	d.nrec = get2(a[8:]);
+	if(maxrec >= 0 && d.nrec > maxrec){
+		d.nrec = maxrec;
+		err = "invalid record count";
+	}
+	d.recsize = get2(a[10:]);
+	d.position = get4(a[12:]);
+	return (d, sys->sprint("unexpected Doc file format: %s", err));
+}
+
+Doc.iscompressed(d: self ref Doc): int
+{
+	return (d.version&7) == 2;		# high-order bits are sometimes used, ignore them
+}
+
+Doc.read(doc: self ref Doc, index: int): (string, string)
+{
+	m := doc.m;
+	DB, PDB: import m;
+	r := doc.file.read(index+1);
+	if(r == nil)
+		return (nil, sys->sprint("%r"));
+	(s, serr) := doc.unpacktext(r.data);
+	if(s == nil)
+		return (nil, serr);
+	return (s, nil);
+}
+
+Doc.unpacktext(doc: self ref Doc, a: array of byte): (string, string)
+{
+	nb := len a;
+	s: string;
+	if(!doc.iscompressed()){
+		for(i := 0; i < nb; i++)
+			s[len s] = int a[i];	# assumes Latin-1
+		return (s, nil);
+	}
+	o := 0;
+	for(i := 0; i < nb;){
+		c := int a[i++];
+		if(c >= 9 && c <= 16r7F || c == 0)
+			s[o++] = c;
+		else if(c >= 1 && c <= 8){
+			if(i+c > nb)
+				return (nil, "missing data in record");
+			while(--c >= 0)
+				s[o++] = int a[i++];
+		}else if(c >= 16rC0 && c <= 16rFF){
+			s[o] = ' ';
+			s[o+1] = c & 16r7F;
+			o += 2;
+		}else{	# c >= 0x80 && c <= 16rBF
+			v := int a[i++];
+			m := ((c & 16r3F)<<5)|(v>>3);
+			n := (v&7) + 3;
+			if(m == 0 || m > o)
+				return (nil, sys->sprint("data is corrupt: m=%d n=%d o=%d", m, n, o));
+			for(; --n >= 0; o++)
+				s[o] = s[o-m];
+		}
+	}
+	return (s, nil);
+}
+
+Doc.textlength(doc: self ref Doc, a: array of byte): int
+{
+	nb := len a;
+	if(!doc.iscompressed())
+		return nb;
+	o := 0;
+	for(i := 0; i < nb;){
+		c := int a[i++];
+		if(c >= 9 && c <= 16r7F || c == 0)
+			o++;
+		else if(c >= 1 && c <= 8){
+			if(i+c > nb)
+				return -1;
+			o += c;
+			i += c;
+		}else if(c >= 16rC0 && c <= 16rFF){
+			o += 2;
+		}else{	# c >= 0x80 && c <= 16rBF
+			v := int a[i++];
+			m := ((c & 16r3F)<<5)|(v>>3);
+			n := (v&7) + 3;
+			if(m == 0 || m > o)
+				return -1;
+			o += n;
+		}
+	}
+	return o;
+}
+
+id2s(i: int): string
+{
+	if(i == 0)
+		return "";
+	return sys->sprint("%c%c%c%c", (i>>24)&16rFF, (i>>16)&16rFF, (i>>8)&16rFF, i&16rFF);
+}
+
+s2id(s: string): int
+{
+	n := 0;
+	for(i := 0; i < 4; i++){
+		c := 0;
+		if(i < len s)
+			c = s[i] & 16rFF;
+		n = (n<<8) | c;
+	}
+	return n;
+}
+
+DBInfo.new(name: string, attr: int, dtype: string, version: int, creator: string): ref DBInfo
+{
+	info := ref DBInfo;
+	info.name = name;
+	info.attr = attr;
+	info.version = version;
+	info.ctime = daytime->now();
+	info.mtime = daytime->now();
+	info.btime = 0;
+	info.modno = 0;
+	info.dtype = dtype;
+	info.creator = creator;
+	info.uidseed = 0;
+	info.index = 0;
+	return info;
+}
+
+Categories.new(labels: array of string): ref Categories
+{
+	c := ref Categories;
+	c.renamed = 0;
+	c.lastuid = 0;
+	c.labels = array[16] of string;
+	c.uids = array[] of {0 to 15 => 0};
+	for(i := 0; i < len labels && i < 16; i++){
+		c.labels[i] = labels[i];
+		c.lastuid = 16r80 + i;
+		c.uids[i] = c.lastuid;
+	}
+	return c;
+}
+
+Categories.unpack(a: array of byte): ref Categories
+{
+	if(len a < 16r114)
+		return nil;		# doesn't match the structure
+	c := ref Categories;
+	c.renamed = get2(a);
+	c.labels = array[16] of string;
+	c.uids = array[16] of int;
+	j := 2;
+	for(i := 0; i < 16; i++){
+		c.labels[i] = latin1(a[j:j+16], 0);
+		j += 16;
+		c.uids[i] = int a[16r102+i];
+	}
+	c.lastuid = int a[16r112];
+	# one byte of padding is shown on p. 26, but
+	# two more are invariably used in practice
+	# before application specific data.
+	if(len a > 16r116)
+		c.appdata = a[16r116:];
+	return c;
+}
+
+Categories.pack(c: self ref Categories): array of byte
+{
+	a := array[16r116 + len c.appdata] of byte;
+	put2(a, c.renamed);
+	j := 2;
+	for(i := 0; i < 16; i++){
+		puts(a[j:j+16], c.labels[i]);
+		j += 16;
+		a[16r102+i] = byte c.uids[i];
+	}
+	a[16r112] = byte c.lastuid;
+	a[16r113] = byte 0;	# pad shown on p. 26
+	a[16r114] = byte 0;	# extra two bytes of padding used in practice
+	a[16r115] = byte 0;
+	if(c.appdata != nil)
+		a[16r116:] = c.appdata;
+	return a;
+}
+
+Categories.mkidmap(c: self ref Categories): array of int
+{
+	a := array[256] of {* => 0};
+	for(i := 0; i < len c.uids; i++)
+		a[c.uids[i]] = i;
+	return a;
+}
+
+#
+# because PalmOS treats all times as local times, and doesn't associate
+# them with time zones, we'll convert using local time on Plan 9 and Inferno
+#
+
+pilot2epoch(t: int): int
+{
+	if(t == 0)
+		return 0;	# we'll assume it's not set
+	return t - Epochdelta + tzoff;
+}
+
+epoch2pilot(t: int): int
+{
+	if(t == 0)
+		return t;
+	return t - tzoff + Epochdelta;
+}
+
+#
+# map Palm name to string, assuming iso-8859-1,
+# but remap space and /
+#
+latin1(a: array of byte, remap: int): string
+{
+	s := "";
+	for(i := 0; i < len a; i++){
+		c := int a[i];
+		if(c == 0)
+			break;
+		if(remap){
+			if(c == ' ')
+				c = 16r00A0;	# unpaddable space
+			else if(c == '/')
+				c = 16r2215;	# division /
+		}
+		s[len s] = c;
+	}
+	return s;
+}
+
+#
+# map from Unicode to Palm name
+#
+filename(name: string): string
+{
+	s := "";
+	for(i := 0; i < len name; i++){
+		c := name[i];
+		if(c == ' ')
+			c = 16r00A0;	# unpaddable space
+		else if(c == '/')
+			c = 16r2215;	# division solidus
+		s[len s] = c;
+	}
+	return s;
+}
+
+dbname(name: string): string
+{
+	s := "";
+	for(i := 0; i < len name; i++){
+		c := name[i];
+		case c {
+		0 =>			c = ' ';	# unlikely, but just in case
+		16r2215 =>	c = '/';
+		16r00A0 =>	c = ' ';
+		}
+		s[len s] = c;
+	}
+	return s;
+}
+
+#
+# string conversion: can't use (string a) because
+# the bytes are Latin1, not Unicode
+#
+gets(a: array of byte): string
+{
+	s := "";
+	for(i := 0; i < len a; i++)
+		s[len s] = int a[i];
+	return s;
+}
+
+puts(a: array of byte, s: string)
+{
+	for(i := 0; i < len a-1 && i < len s; i++)
+		a[i] = byte s[i];
+	for(; i < len a; i++)
+		a[i] = byte 0;
+}
+
+#
+#  big-endian packing
+#
+
+get4(p: array of byte): int
+{
+	return (((((int p[0] << 8) | int p[1]) << 8) | int p[2]) << 8) | int p[3];
+}
+
+get3(p: array of byte): int
+{
+	return (((int p[0] << 8) | int p[1]) << 8) | int p[2];
+}
+
+get2(p: array of byte): int
+{
+	return (int p[0]<<8) | int p[1];
+}
+
+put4(p: array of byte, v: int)
+{
+	p[0] = byte (v>>24);
+	p[1] = byte (v>>16);
+	p[2] = byte (v>>8);
+	p[3] = byte (v & 16rFF);
+}
+
+put3(p: array of byte, v: int)
+{
+	p[0] = byte (v>>16);
+	p[1] = byte (v>>8);
+	p[2] = byte (v & 16rFF);
+}
+
+put2(p: array of byte, v: int)
+{
+	p[0] = byte (v>>8);
+	p[1] = byte (v & 16rFF);
+}
+
+#
+# DL protocol argument wrapping, based on conventions
+# extracted from include/Core/System/DLCommon.h in SDK 5
+#
+# tiny arguments
+#	id: byte
+#	size: byte	# excluding this header
+#	data: byte[]
+#
+# small arguments
+#	id: byte	# with 16r80 flag
+#	pad: byte
+#	size: byte[2]
+#	data: byte[]
+#
+# long arguments
+#	id: byte	# with 16r40 flag
+#	pad: byte
+#	size: byte[4]
+#	data: byte[]
+
+# wrapper format flag in request/response argument ID
+ShortWrap: con 16r80;	# 2-byte count
+LongWrap: con 16r40;	# 4-byte count
+
+Eshort: con "response shorter than expected";
+
+#
+# set the system error string
+#
+e(s: string): string
+{
+	if(s != nil)
+		sys->werrstr(s);
+	return s;
+}
+
+argsize(args: array of (int, array of byte)): int
+{
+	totnb := 0;
+	for(i := 0; i < len args; i++){
+		(nil, a) := args[i];
+		n := len a;
+		if(n > 65535)
+			totnb += 6;	# long wrap
+		else if(n > 255)
+			totnb += 4;	# short
+		else
+			totnb += 2;	# tiny
+		totnb += n;
+	}
+	return totnb;
+}
+
+packargs(out: array of byte, args: array of (int, array of byte)): array of byte
+{
+	for(i := 0; i < len args; i++){
+		(id, a) := args[i];
+		n := len a;
+		if(n > 65535){
+			out[0] = byte (LongWrap|ShortWrap|id);
+			out[1] = byte 0;
+			put4(out[2:], n);
+			out = out[6:];
+		}else if(n > 255){
+			out[0] = byte (ShortWrap|id);
+			out[1] = byte 0;
+			put2(out[2:], n);
+			out = out[4:];
+		}else{
+			out[0] = byte id;
+			out[1] = byte n;
+			out = out[2:];
+		}
+		out[0:] = a;
+		out = out[n:];
+	}
+	return out;
+}
+
+unpackargs(argc: int, reply: array of byte): (array of (int, array of byte), string)
+{
+	replies := array[argc] of (int, array of byte);
+	o := 0;
+	for(i := 0; i < len replies; i++){
+		o = (o+1)&~1;	# each argument starts at even offset
+		a := reply[o:];
+		if(len a < 2)
+			return (nil, e(Eshort));
+		rid := int a[0];
+		l: int;
+		if(rid & LongWrap){
+			if(len a < 6)
+				return (nil, e(Eshort));
+			l = get4(a[2:]);
+			a = a[6:];
+			o += 6;
+		}else if(rid & ShortWrap){
+			if(len a < 4)
+				return (nil, e(Eshort));
+			l = get2(a[2:]);
+			a = a[4:];
+			o += 4;
+		}else{
+			l = int a[1];
+			a = a[2:];
+			o += 2;
+		}
+		if(len a < l)
+			return (nil, e(Eshort));
+		replies[i] = (rid &~ 16rC0, a[0:l]);
+		o += l;
+	}
+	return (replies, nil);
+}
--- /dev/null
+++ b/appl/lib/palmdb.b
@@ -1,0 +1,576 @@
+implement Palmdb;
+
+#
+# Copyright © 2001-2002 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# Based on ``Palm® File Format Specification'', Document Number 3008-004, 1 May 2001, by Palm Inc.
+# Doc compression based on description by Paul Lucas, 18 August 1998
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "palm.m";
+	palm: Palm;
+	DBInfo, Record, Resource, get2, get3, get4, put2, put3, put4, gets, puts: import palm;
+	filename, dbname: import palm;
+
+Entry: adt {
+	id:	int;	# resource: id; record: unique ID
+	offset:	int;
+	size:	int;
+	name:	int;	# resource entry only
+	attr:	int;	# record entry only
+};
+
+Ofile: adt {
+	fname:	string;
+	f:	ref Iobuf;
+	mode:	int;
+	info:	ref DBInfo;
+	appinfo:	array of byte;
+	sortinfo:	array of int;
+	uidseed:	int;
+	entries:	array of ref Entry;
+};
+
+files:	array of ref Ofile;
+
+Dbhdrlen: con 72+6;
+Datahdrsize: con 4+1+3;
+Resourcehdrsize: con 4+2+4;
+
+# Exact value of "Jan 1, 1970 0:00:00 GMT" - "Jan 1, 1904 0:00:00 GMT"
+Epochdelta: con 2082844800;
+tzoff := 0;
+
+init(m: Palm): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	if(bufio == nil || daytime == nil)
+		return "can't load required module";
+	palm = m;
+	tzoff = daytime->local(0).tzoff;
+	return nil;
+}
+
+Eshort: con "file format error: too small";
+
+DB.open(name: string, mode: int): (ref DB, string)
+{
+	if(mode != Sys->OREAD)
+		return (nil, "invalid mode");
+	fd := sys->open(name, mode);
+	if(fd == nil)
+		return (nil, sys->sprint("%r"));
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0)
+		return (nil, sys->sprint("%r"));
+	length := int d.length;
+	if(length == 0)
+		return (nil, "empty file");
+	(pf, ofile, fx) := mkpfile(name, mode);
+
+	f := bufio->fopen(fd, mode);	# automatically closed if open fails
+
+	p := array[Dbhdrlen] of byte;
+	if(f.read(p, Dbhdrlen) != Dbhdrlen)
+		return (nil, "invalid file header: too short");
+
+	ip := ofile.info;
+	ip.name = gets(p[0:32]);
+	ip.attr = get2(p[32:]);
+	ip.version = get2(p[34:]);
+	ip.ctime = pilot2epoch(get4(p[36:]));
+	ip.mtime = pilot2epoch(get4(p[40:]));
+	ip.btime = pilot2epoch(get4(p[44:]));
+	ip.modno = get4(p[48:]);
+	appinfo := get4(p[52:]);
+	sortinfo := get4(p[56:]);
+	if(appinfo < 0 || sortinfo < 0 || (appinfo|sortinfo)&1)
+		return (nil, "invalid header: bad offset");
+	ip.dtype = xs(get4(p[60:]));
+	ip.creator = xs(get4(p[64:]));
+	ofile.uidseed = ip.uidseed = get4(p[68:]);
+
+	if(get4(p[72:]) != 0)
+		return (nil, "chained headers not supported");	# Palm says to reject such files
+	nrec := get2(p[76:]);
+	if(nrec < 0)
+		return (nil, sys->sprint("invalid header: bad record count: %d", nrec));
+
+	esize := Datahdrsize;
+	if(ip.attr & Palm->Fresource)
+		esize = Resourcehdrsize;
+	
+	dataoffset := length;
+	ofile.entries = array[nrec] of ref Entry;
+	if(nrec > 0){
+		laste: ref Entry;
+		buf := array[esize] of byte;
+		for(i := 0; i < nrec; i++){
+			if(f.read(buf, len buf) != len buf)
+				return (nil, Eshort);
+			e := ref Entry;
+			if(ip.attr & Palm->Fresource){
+				# resource entry: type[4], id[2], offset[4]
+				e.name = get4(buf);
+				e.id = get2(buf[4:]);
+				e.offset = get4(buf[6:]);
+				e.attr = 0;
+			}else{
+				# record entry: offset[4], attr[1], id[3]
+				e.offset = get4(buf);
+				e.attr = int buf[4];
+				e.id = get3(buf[5:]);
+				e.name = 0;
+			}
+			if(laste != nil)
+				laste.size = e.offset - laste.offset;
+			laste = e;
+			ofile.entries[i] = e;
+		}
+		if(laste != nil)
+			laste.size = length - laste.offset;
+		dataoffset = ofile.entries[0].offset;
+	}else{
+		if(f.read(p, 2) != 2)
+			return (nil, Eshort);	# discard placeholder bytes
+	}
+
+	n := 0;
+	if(appinfo > 0){
+		n = appinfo - int f.offset();
+		while(--n >= 0)
+			f.getb();
+		if(sortinfo)
+			n = sortinfo - appinfo;
+		else
+			n = dataoffset - appinfo;
+		ofile.appinfo = array[n] of byte;
+		if(f.read(ofile.appinfo, n) != n)
+			return (nil, Eshort);
+	}
+	if(sortinfo > 0){
+		n = sortinfo - int f.offset();
+		while(--n >= 0)
+			f.getb();
+		n = (dataoffset-sortinfo)/2;
+		ofile.sortinfo = array[n] of int;
+		tmp := array[2*n] of byte;
+		if(f.read(tmp, len tmp) != len tmp)
+			return (nil, Eshort);
+		for(i := 0; i < n; i++)
+			ofile.sortinfo[i] = get2(tmp[2*i:]);
+	}
+	ofile.f = f;	# safe to save open file reference
+	files[fx] = ofile;
+	return (pf, nil);
+}
+
+DB.close(db: self ref DB): string
+{
+	ofile := files[db.x];
+	if(ofile.f != nil){
+		ofile.f.close();
+		ofile.f = nil;
+	}
+	files[db.x] = nil;
+	return nil;
+}
+
+DB.stat(db: self ref DB): ref DBInfo
+{
+	return ref *files[db.x].info;
+}
+
+DB.create(name: string, mode: int, perm: int, info: ref DBInfo): (ref DB, string)
+{
+	return (nil, "DB.create not implemented");
+}
+
+DB.wstat(db: self ref DB, ip: ref DBInfo, flags: int)
+{
+	raise "DB.wstat not implemented";
+}
+
+#DB.wstat(db: self ref DB, ip: ref DBInfo): string
+#{
+#	ofile := files[db.x];
+#	if(ofile.mode != Sys->OWRITE)
+#		return "not open for writing";
+#	if((ip.attr & Palm->Fresource) != (ofile.info.attr & Palm->Fresource))
+#		return "cannot change file type";
+#	# copy only a subset
+#	ofile.info.name = ip.name;
+#	ofile.info.attr = ip.attr;
+#	ofile.info.version = ip.version;
+#	ofile.info.ctime = ip.ctime;
+#	ofile.info.mtime = ip.mtime;
+#	ofile.info.btime = ip.btime;
+#	ofile.info.modno = ip.modno;
+#	ofile.info.dtype = ip.dtype;
+#	ofile.info.creator = ip.creator;
+#	return nil;
+#}
+
+DB.rdappinfo(db: self ref DB): (array of byte, string)
+{
+	return (files[db.x].appinfo, nil);
+}
+
+DB.wrappinfo(db: self ref DB, data: array of byte): string
+{
+	ofile := files[db.x];
+	if(ofile.mode != Sys->OWRITE)
+		return "not open for writing";
+	ofile.appinfo = array[len data] of byte;
+	ofile.appinfo[0:] = data;
+	return nil;
+}
+
+DB.rdsortinfo(db: self ref DB): (array of int, string)
+{
+	return (files[db.x].sortinfo, nil);
+}
+
+DB.wrsortinfo(db: self ref DB, sort: array of int): string
+{
+	ofile := files[db.x];
+	if(ofile.mode != Sys->OWRITE)
+		return "not open for writing";
+	ofile.sortinfo = array[len sort] of int;
+	ofile.sortinfo[0:] = sort;
+	return nil;
+}
+
+DB.readidlist(db: self ref DB, nil: int): array of int
+{
+	ent := files[db.x].entries;
+	a := array[len ent] of int;
+	for(i := 0; i < len a; i++)
+		a[i] = ent[i].id;
+	return a;
+}
+
+DB.nentries(db: self ref DB): int
+{
+	return len files[db.x].entries;
+}
+
+DB.resetsyncflags(db: self ref DB): string
+{
+	raise "DB.resetsyncflags not implemented";
+}
+
+DB.records(db: self ref DB): ref PDB
+{
+	if(db == nil || db.attr & Palm->Fresource)
+		return nil;
+	return ref PDB(db);
+}
+
+DB.resources(db: self ref DB): ref PRC
+{
+	if(db == nil || (db.attr & Palm->Fresource) == 0)
+		return nil;
+	return ref PRC(db);
+}
+
+PDB.read(pdb: self ref PDB, i: int): ref Record
+{
+	ofile := files[pdb.db.x];
+	if(i < 0 || i >= len ofile.entries){
+		if(i == len ofile.entries)
+			return nil; # treat as end-of-file
+		#return "index out of range";
+		return nil;
+	}
+	e := ofile.entries[i];
+	nb := e.size;
+	r := ref Record(e.id, e.attr & 16rF0, e.attr & 16r0F, array[nb] of byte);
+	ofile.f.seek(big e.offset, 0);
+	if(ofile.f.read(r.data, nb) != nb)
+		return nil;
+	return r;
+}
+
+PDB.readid(pdb: self ref PDB, id: int): (ref Record, int)
+{
+	ofile := files[pdb.db.x];
+	ent := ofile.entries;
+	for(i := 0; i < len ent; i++)
+		if((e := ent[i]).id == id){
+			nb := e.size;
+			r := ref Record(e.id, e.attr & 16rF0, e.attr & 16r0F, array[e.size] of byte);
+			ofile.f.seek(big e.offset, 0);
+			if(ofile.f.read(r.data, nb) != nb)
+				return (nil, -1);
+			return (r, id);
+		}
+	sys->werrstr("ID not found");
+	return (nil, -1);
+}
+
+PDB.resetnext(db: self ref PDB): int
+{
+	raise "PDB.resetnext not implemented";
+}
+
+PDB.readnextmod(db: self ref PDB): (ref Record, int)
+{
+	raise "PDB.readnextmod not implemented";
+}
+
+PDB.write(db: self ref PDB, r: ref Record): string
+{
+	return "PDB.write not implemented";
+}
+
+PDB.truncate(db: self ref PDB): string
+{
+	return "PDB.truncate not implemented";
+}
+
+PDB.delete(db: self ref PDB, id: int): string
+{
+	return "PDB.delete not implemented";
+}
+
+PDB.deletecat(db: self ref PDB, cat: int): string
+{
+	return "PDB.deletecat not implemented";
+}
+
+PDB.purge(db: self ref PDB): string
+{
+	return "PDB.purge not implemented";
+}
+
+PDB.movecat(db: self ref PDB, old: int, new: int): string
+{
+	return "PDB.movecat not implemented";
+}
+
+PRC.read(db: self ref PRC, index: int): ref Resource
+{
+	return nil;
+}
+
+PRC.readtype(db: self ref PRC, name: int, id: int): (ref Resource, int)
+{
+	return (nil, -1);
+}
+
+PRC.write(db: self ref PRC, r: ref Resource): string
+{
+	return "PRC.write not implemented";
+}
+
+PRC.truncate(db: self ref PRC): string
+{
+	return "PRC.truncate not implemented";
+}
+
+PRC.delete(db: self ref PRC, name: int, id: int): string
+{
+	return "PRC.delete not implemented";
+}
+
+#
+# internal function to extend entry list if necessary, and return a
+# pointer to the next available slot
+#
+entryensure(db: ref DB, i: int): ref Entry
+{
+	ofile := files[db.x];
+	if(i < len ofile.entries)
+		return ofile.entries[i];
+	e := ref Entry(0, -1, 0, 0, 0);
+	n := len ofile.entries;
+	if(n == 0)
+		n = 64;
+	else
+		n = (i+63) & ~63;
+	a := array[n] of ref Entry;
+	a[0:] = ofile.entries;
+	a[i] = e;
+	ofile.entries = a;
+	return e;
+}
+
+writefilehdr(db: ref DB, mode: int, perm: int): string
+{
+	ofile := files[db.x];
+	if(len ofile.entries >= 64*1024)
+		return "too many records for Palm file";	# is there a way to extend it?
+
+	if((f := bufio->create(ofile.fname, mode, perm)) == nil)
+		return sys->sprint("%r");
+
+	ip := ofile.info;
+
+	esize := Datahdrsize;
+	if(ip.attr & Palm->Fresource)
+		esize = Resourcehdrsize;
+	offset := Dbhdrlen + esize*len ofile.entries + 2;
+	offset += 2;	# placeholder bytes or gap bytes
+	appinfo := 0;
+	if(len ofile.appinfo > 0){
+		appinfo = offset;
+		offset += len ofile.appinfo;
+	}
+	sortinfo := 0;
+	if(len ofile.sortinfo > 0){
+		sortinfo = offset;
+		offset += 2*len ofile.sortinfo;	# 2-byte entries
+	}
+	p := array[Dbhdrlen] of byte;	# bigger than any entry as well
+	puts(p[0:32], ip.name);
+	put2(p[32:], ip.attr);
+	put2(p[34:], ip.version);
+	put4(p[36:], epoch2pilot(ip.ctime));
+	put4(p[40:], epoch2pilot(ip.mtime));
+	put4(p[44:], epoch2pilot(ip.btime));
+	put4(p[48:], ip.modno);
+	put4(p[52:], appinfo);
+	put4(p[56:], sortinfo);
+	put4(p[60:], sx(ip.dtype));
+	put4(p[64:], sx(ip.creator));
+	put4(p[68:], ofile.uidseed);
+	put4(p[72:], 0);		# next record list ID
+	put2(p[76:], len ofile.entries);
+
+	if(f.write(p, Dbhdrlen) != Dbhdrlen)
+		return ewrite(f);
+	if(len ofile.entries > 0){
+		for(i := 0; i < len ofile.entries; i++) {
+			e := ofile.entries[i];
+			e.offset = offset;
+			if(ip.attr & Palm->Fresource) {
+				put4(p, e.name);
+				put2(p[4:], e.id);
+				put4(p[6:], e.offset);
+			} else {
+				put4(p, e.offset);
+				p[4] = byte e.attr;
+				put3(p[5:], e.id);
+			}
+			if(f.write(p, esize) != esize)
+				return ewrite(f);
+			offset += e.size;
+		}
+	}
+
+	f.putb(byte 0);	# placeholder bytes (figure 1.4) or gap bytes (p. 15)
+	f.putb(byte 0);
+
+	if(appinfo != 0){
+		if(f.write(ofile.appinfo, len ofile.appinfo) != len ofile.appinfo)
+			return ewrite(f);
+	}
+
+	if(sortinfo != 0){
+		tmp := array[2*len ofile.sortinfo] of byte;
+		for(i := 0; i < len ofile.sortinfo; i++)
+			put2(tmp[2*i:], ofile.sortinfo[i]);
+		if(f.write(tmp, len tmp) != len tmp)
+			return ewrite(f);
+	}
+
+	if(f.flush() != 0)
+		return ewrite(f);
+
+	return nil;
+}
+
+ewrite(f: ref Iobuf): string
+{
+	e := sys->sprint("write error: %r");
+	f.close();
+	return e;
+}
+
+xs(i: int): string
+{
+	if(i == 0)
+		return "";
+	if(i & int 16r80808080)
+		return sys->sprint("%8.8ux", i);
+	return sys->sprint("%c%c%c%c", (i>>24)&16rFF, (i>>16)&16rFF, (i>>8)&16rFF, i&16rFF);
+}
+
+sx(s: string): int
+{
+	n := 0;
+	for(i := 0; i < 4; i++){
+		c := 0;
+		if(i < len s)
+			c = s[i] & 16rFF;
+		n = (n<<8) | c;
+	}
+	return n;
+}
+
+mkpfile(name: string, mode: int): (ref DB, ref Ofile, int)
+{
+	ofile := ref Ofile(name, nil, mode, DBInfo.new(name, 0, nil, 0, nil),
+		array[0] of byte, array[0] of int, 0, nil);
+	for(x := 0; x < len files; x++)
+		if(files[x] == nil)
+			return (ref DB(x, mode, 0), ofile, x);
+	a := array[x] of ref Ofile;
+	a[0:] = files;
+	files = a;
+	return (ref DB(x, mode, 0), ofile, x);
+}
+
+#
+# because PalmOS treats all times as local times, and doesn't associate
+# them with time zones, we'll convert using local time on Plan 9 and Inferno
+#
+
+pilot2epoch(t: int): int
+{
+	if(t == 0)
+		return 0;	# we'll assume it's not set
+	return t - Epochdelta + tzoff;
+}
+
+epoch2pilot(t: int): int
+{
+	if(t == 0)
+		return t;
+	return t - tzoff + Epochdelta;
+}
+
+#
+# map Palm name to string, assuming iso-8859-1,
+# but remap space and /
+#
+latin1(a: array of byte, remap: int): string
+{
+	s := "";
+	for(i := 0; i < len a; i++){
+		c := int a[i];
+		if(c == 0)
+			break;
+		if(remap){
+			if(c == ' ')
+				c = 16r00A0;	# unpaddable space
+			else if(c == '/')
+				c = 16r2215;	# division /
+		}
+		s[len s] = c;
+	}
+	return s;
+}
--- /dev/null
+++ b/appl/lib/palmfile.b
@@ -1,0 +1,703 @@
+implement Palmfile;
+
+#
+# Copyright © 2001-2002 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# Based on ``Palm® File Format Specification'', Document Number 3008-004, 1 May 2001, by Palm Inc.
+# Doc compression based on description by Paul Lucas, 18 August 1998
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "palmfile.m";
+
+
+Dbhdrlen: con 72+6;
+Datahdrsize: con 4+1+3;
+Resourcehdrsize: con 4+2+4;
+
+# Exact value of "Jan 1, 1970 0:00:00 GMT" - "Jan 1, 1904 0:00:00 GMT"
+Epochdelta: con 2082844800;
+tzoff := 0;
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+	if(bufio == nil || daytime == nil)
+		return "can't load required module";
+	tzoff = daytime->local(0).tzoff;
+	return nil;
+}
+
+Eshort: con "file format error: too small";
+
+Pfile.open(name: string, mode: int): (ref Pfile, string)
+{
+	if(mode != Sys->OREAD)
+		return (nil, "invalid mode");
+	fd := sys->open(name, mode);
+	if(fd == nil)
+		return (nil, sys->sprint("%r"));
+	pf := mkpfile(name, mode);
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0)
+		return (nil, sys->sprint("%r"));
+	length := int d.length;
+	if(length == 0)
+		return (nil, "empty file");
+
+	f := bufio->fopen(fd, mode);	# automatically closed if open fails
+
+	p := array[Dbhdrlen] of byte;
+	if(f.read(p, Dbhdrlen) != Dbhdrlen)
+		return (nil, "invalid file header: too short");
+
+	ip := pf.info;
+	ip.name = gets(p[0:32]);
+	ip.attr = get2(p[32:]);
+	ip.version = get2(p[34:]);
+	ip.ctime = pilot2epoch(get4(p[36:]));
+	ip.mtime = pilot2epoch(get4(p[40:]));
+	ip.btime = pilot2epoch(get4(p[44:]));
+	ip.modno = get4(p[48:]);
+	ip.appinfo = get4(p[52:]);
+	ip.sortinfo = get4(p[56:]);
+	if(ip.appinfo < 0 || ip.sortinfo < 0 || (ip.appinfo|ip.sortinfo)&1)
+		return (nil, "invalid header: bad offset");
+	ip.dtype = xs(get4(p[60:]));
+	ip.creator = xs(get4(p[64:]));
+	pf.uidseed = ip.uidseed = get4(p[68:]);
+
+	if(get4(p[72:]) != 0)
+		return (nil, "chained headers not supported");	# Palm says to reject such files
+	nrec := get2(p[76:]);
+	if(nrec < 0)
+		return (nil, sys->sprint("invalid header: bad record count: %d", nrec));
+
+	esize := Datahdrsize;
+	if(ip.attr & Fresource)
+		esize = Resourcehdrsize;
+	
+	dataoffset := length;
+	pf.entries = array[nrec] of ref Entry;
+	if(nrec > 0){
+		laste: ref Entry;
+		buf := array[esize] of byte;
+		for(i := 0; i < nrec; i++){
+			if(f.read(buf, len buf) != len buf)
+				return (nil, Eshort);
+			e := ref Entry;
+			if(ip.attr & Fresource){
+				# resource entry: type[4], id[2], offset[4]
+				e.name = get4(buf);
+				e.id = get2(buf[4:]);
+				e.offset = get4(buf[6:]);
+				e.attr = 0;
+			}else{
+				# record entry: offset[4], attr[1], id[3]
+				e.offset = get4(buf);
+				e.attr = int buf[4];
+				e.id = get3(buf[5:]);
+				e.name = 0;
+			}
+			if(laste != nil)
+				laste.size = e.offset - laste.offset;
+			laste = e;
+			pf.entries[i] = e;
+		}
+		if(laste != nil)
+			laste.size = length - laste.offset;
+		dataoffset = pf.entries[0].offset;
+	}else{
+		if(f.read(p, 2) != 2)
+			return (nil, Eshort);	# discard placeholder bytes
+	}
+
+	n := 0;
+	if(ip.appinfo > 0){
+		n = ip.appinfo - int f.offset();
+		while(--n >= 0)
+			f.getb();
+		if(ip.sortinfo)
+			n = ip.sortinfo - ip.appinfo;
+		else
+			n = dataoffset - ip.appinfo;
+		pf.appinfo = array[n] of byte;
+		if(f.read(pf.appinfo, n) != n)
+			return (nil, Eshort);
+	}
+	if(ip.sortinfo > 0){
+		n = ip.sortinfo - int f.offset();
+		while(--n >= 0)
+			f.getb();
+		n = (dataoffset-ip.sortinfo)/2;
+		pf.sortinfo = array[n] of int;
+		tmp := array[2*n] of byte;
+		if(f.read(tmp, len tmp) != len tmp)
+			return (nil, Eshort);
+		for(i := 0; i < n; i++)
+			pf.sortinfo[i] = get2(tmp[2*i:]);
+	}
+	pf.f = f;	# safe to save open file reference
+	return (pf, nil);
+}
+
+Pfile.close(pf: self ref Pfile): int
+{
+	if(pf.f != nil){
+		pf.f.close();
+		pf.f = nil;
+	}
+	return 0;
+}
+
+Pfile.stat(pf: self ref Pfile): ref DBInfo
+{
+	return ref *pf.info;
+}
+
+Pfile.read(pf: self ref Pfile, i: int): (ref Record, string)
+{
+	if(i < 0 || i >= len pf.entries){
+		if(i == len pf.entries)
+			return (nil, nil);	# treat as end-of-file
+		return (nil, "index out of range");
+	}
+	e := pf.entries[i];
+	r := ref Record;
+	r.index = i;
+	nb := e.size;
+	r.data = array[nb] of byte;
+	pf.f.seek(big e.offset, 0);
+	if(pf.f.read(r.data, nb) != nb)
+		return (nil, sys->sprint("%r"));
+	r.cat = e.attr & 16r0F;
+	r.attr = e.attr & 16rF0;
+	r.id = e.id;
+	r.name = e.name;
+	return (r, nil);
+}
+
+#Pfile.create(name: string, info: ref DBInfo): ref Pfile
+#{
+#}
+
+#Pfile.wstat(pf: self ref Pfile, ip: ref DBInfo): string
+#{
+#	if(pf.mode != Sys->OWRITE)
+#		return "not open for writing";
+#	if((ip.attr & Fresource) != (pf.info.attr & Fresource))
+#		return "cannot change file type";
+#	# copy only a subset
+#	pf.info.name = ip.name;
+#	pf.info.attr = ip.attr;
+#	pf.info.version = ip.version;
+#	pf.info.ctime = ip.ctime;
+#	pf.info.mtime = ip.mtime;
+#	pf.info.btime = ip.btime;
+#	pf.info.modno = ip.modno;
+#	pf.info.dtype = ip.dtype;
+#	pf.info.creator = ip.creator;
+#	return nil;
+#}
+
+#Pfile.setappinfo(pf: self ref Pfile, data: array of byte): string
+#{
+#	if(pf.mode != Sys->OWRITE)
+#		return "not open for writing";
+#	pf.appinfo = array[len data] of byte;
+#	pf.appinfo[0:] = data;
+#}
+
+#Pfile.setsortinfo(pf: self ref Pfile, sort: array of int): string
+#{
+#	if(pf.mode != Sys->OWRITE)
+#		return "not open for writing";
+#	pf.sortinfo = array[len sort] of int;
+#	pf.sortinfo[0:] = sort;
+#}
+
+#
+# internal function to extend entry list if necessary, and return a
+# pointer to the next available slot
+#
+entryensure(pf: ref Pfile, i: int): ref Entry
+{
+	if(i < len pf.entries)
+		return pf.entries[i];
+	e := ref Entry(0, -1, 0, 0, 0);
+	n := len pf.entries;
+	if(n == 0)
+		n = 64;
+	else
+		n = (i+63) & ~63;
+	a := array[n] of ref Entry;
+	a[0:] = pf.entries;
+	a[i] = e;
+	pf.entries = a;
+	return e;
+}
+
+writefilehdr(pf: ref Pfile, mode: int, perm: int): string
+{
+	if(len pf.entries >= 64*1024)
+		return "too many records for Palm file";	# is there a way to extend it?
+
+	if((f := bufio->create(pf.fname, mode, perm)) == nil)
+		return sys->sprint("%r");
+
+	ip := pf.info;
+
+	esize := Datahdrsize;
+	if(ip.attr & Fresource)
+		esize = Resourcehdrsize;
+	offset := Dbhdrlen + esize*len pf.entries + 2;
+	offset += 2;	# placeholder bytes or gap bytes
+	ip.appinfo = 0;
+	if(len pf.appinfo > 0){
+		ip.appinfo = offset;
+		offset += len pf.appinfo;
+	}
+	ip.sortinfo = 0;
+	if(len pf.sortinfo > 0){
+		ip.sortinfo = offset;
+		offset += 2*len pf.sortinfo;	# 2-byte entries
+	}
+	p := array[Dbhdrlen] of byte;	# bigger than any entry as well
+	puts(p[0:32], ip.name);
+	put2(p[32:], ip.attr);
+	put2(p[34:], ip.version);
+	put4(p[36:], epoch2pilot(ip.ctime));
+	put4(p[40:], epoch2pilot(ip.mtime));
+	put4(p[44:], epoch2pilot(ip.btime));
+	put4(p[48:], ip.modno);
+	put4(p[52:], ip.appinfo);
+	put4(p[56:], ip.sortinfo);
+	put4(p[60:], sx(ip.dtype));
+	put4(p[64:], sx(ip.creator));
+	put4(p[68:], pf.uidseed);
+	put4(p[72:], 0);		# next record list ID
+	put2(p[76:], len pf.entries);
+
+	if(f.write(p, Dbhdrlen) != Dbhdrlen)
+		return ewrite(f);
+	if(len pf.entries > 0){
+		for(i := 0; i < len pf.entries; i++) {
+			e := pf.entries[i];
+			e.offset = offset;
+			if(ip.attr & Fresource) {
+				put4(p, e.name);
+				put2(p[4:], e.id);
+				put4(p[6:], e.offset);
+			} else {
+				put4(p, e.offset);
+				p[4] = byte e.attr;
+				put3(p[5:], e.id);
+			}
+			if(f.write(p, esize) != esize)
+				return ewrite(f);
+			offset += e.size;
+		}
+	}
+
+	f.putb(byte 0);	# placeholder bytes (figure 1.4) or gap bytes (p. 15)
+	f.putb(byte 0);
+
+	if(ip.appinfo != 0){
+		if(f.write(pf.appinfo, len pf.appinfo) != len pf.appinfo)
+			return ewrite(f);
+	}
+
+	if(ip.sortinfo != 0){
+		tmp := array[2*len pf.sortinfo] of byte;
+		for(i := 0; i < len pf.sortinfo; i++)
+			put2(tmp[2*i:], pf.sortinfo[i]);
+		if(f.write(tmp, len tmp) != len tmp)
+			return ewrite(f);
+	}
+
+	if(f.flush() != 0)
+		return ewrite(f);
+
+	return nil;
+}
+
+ewrite(f: ref Iobuf): string
+{
+	e := sys->sprint("write error: %r");
+	f.close();
+	return e;
+}
+
+Doc.open(file: ref Pfile): (ref Doc, string)
+{
+	if(file.info.dtype != "TEXt" || file.info.creator != "REAd")
+		return (nil, "not a Doc file: wrong type or creator");
+	(r, err) := file.read(0);
+	if(r == nil){
+		if(err == nil)
+			err = "no directory record";
+		return (nil, sys->sprint("not a valid Doc file: %s", err));
+	}
+	a := r.data;
+	if(len a < 16)
+		return (nil, sys->sprint("not a valid Doc file: bad length: %d", len a));
+	maxrec := len file.entries-1;
+	d := ref Doc;
+	d.file = file;
+	d.version = get2(a);
+	if(d.version != 1 && d.version != 2)
+		err = "unknown Docfile version";
+	# a[2:] is spare
+	d.length = get4(a[4:]);
+	d.nrec = get2(a[8:]);
+	if(maxrec >= 0 && d.nrec > maxrec){
+		d.nrec = maxrec;
+		err = "invalid record count";
+	}
+	d.recsize = get2(a[10:]);
+	d.position = get4(a[12:]);
+	return (d, sys->sprint("unexpected Doc file format: %s", err));
+}
+
+Doc.iscompressed(d: self ref Doc): int
+{
+	return (d.version&7) == 2;		# high-order bits are sometimes used, ignore them
+}
+
+Doc.read(doc: self ref Doc, index: int): (string, string)
+{
+	(r, err) := doc.file.read(index+1);
+	if(r == nil)
+		return (nil, err);
+	(s, serr) := doc.unpacktext(r.data);
+	if(s == nil)
+		return (nil, serr);
+	return (s, nil);
+}
+
+Doc.unpacktext(doc: self ref Doc, a: array of byte): (string, string)
+{
+	nb := len a;
+	s: string;
+	if(!doc.iscompressed()){
+		for(i := 0; i < nb; i++)
+			s[len s] = int a[i];	# assumes Latin-1
+		return (s, nil);
+	}
+	o := 0;
+	for(i := 0; i < nb;){
+		c := int a[i++];
+		if(c >= 9 && c <= 16r7F || c == 0)
+			s[o++] = c;
+		else if(c >= 1 && c <= 8){
+			if(i+c > nb)
+				return (nil, "missing data in record");
+			while(--c >= 0)
+				s[o++] = int a[i++];
+		}else if(c >= 16rC0 && c <= 16rFF){
+			s[o] = ' ';
+			s[o+1] = c & 16r7F;
+			o += 2;
+		}else{	# c >= 0x80 && c <= 16rBF
+			v := int a[i++];
+			m := ((c & 16r3F)<<5)|(v>>3);
+			n := (v&7) + 3;
+			if(m == 0 || m > o)
+				return (nil, sys->sprint("data is corrupt: m=%d n=%d o=%d", m, n, o));
+			for(; --n >= 0; o++)
+				s[o] = s[o-m];
+		}
+	}
+	return (s, nil);
+}
+
+Doc.textlength(doc: self ref Doc, a: array of byte): int
+{
+	nb := len a;
+	if(!doc.iscompressed())
+		return nb;
+	o := 0;
+	for(i := 0; i < nb;){
+		c := int a[i++];
+		if(c >= 9 && c <= 16r7F || c == 0)
+			o++;
+		else if(c >= 1 && c <= 8){
+			if(i+c > nb)
+				return -1;
+			o += c;
+			i += c;
+		}else if(c >= 16rC0 && c <= 16rFF){
+			o += 2;
+		}else{	# c >= 0x80 && c <= 16rBF
+			v := int a[i++];
+			m := ((c & 16r3F)<<5)|(v>>3);
+			n := (v&7) + 3;
+			if(m == 0 || m > o)
+				return -1;
+			o += n;
+		}
+	}
+	return o;
+}
+
+xs(i: int): string
+{
+	if(i == 0)
+		return "";
+	if(i & int 16r80808080)
+		return sys->sprint("%8.8ux", i);
+	return sys->sprint("%c%c%c%c", (i>>24)&16rFF, (i>>16)&16rFF, (i>>8)&16rFF, i&16rFF);
+}
+
+sx(s: string): int
+{
+	n := 0;
+	for(i := 0; i < 4; i++){
+		c := 0;
+		if(i < len s)
+			c = s[i] & 16rFF;
+		n = (n<<8) | c;
+	}
+	return n;
+}
+
+mkpfile(name: string, mode: int): ref Pfile
+{
+	pf := ref Pfile;
+	pf.mode = mode;
+	pf.fname = name;
+	pf.appinfo = array[0] of byte;		# making it non-nil saves having to check each access
+	pf.sortinfo = array[0] of int;
+	pf.uidseed = 0;
+	pf.info = DBInfo.new(name, 0, nil, 0, nil);
+	return pf;
+}
+
+DBInfo.new(name: string, attr: int, dtype: string, version: int, creator: string): ref DBInfo
+{
+	info := ref DBInfo;
+	info.name = name;
+	info.attr = attr;
+	info.version = version;
+	info.ctime = daytime->now();
+	info.mtime = daytime->now();
+	info.btime = 0;
+	info.modno = 0;
+	info.appinfo = 0;
+	info.sortinfo = 0;
+	info.dtype = dtype;
+	info.creator = creator;
+	info.uidseed = 0;
+	info.index = 0;
+	info.more = 0;
+	return info;
+}
+
+Categories.new(labels: array of string): ref Categories
+{
+	c := ref Categories;
+	c.renamed = 0;
+	c.lastuid = 0;
+	c.labels = array[16] of string;
+	c.uids = array[] of {0 to 15 => 0};
+	for(i := 0; i < len labels && i < 16; i++){
+		c.labels[i] = labels[i];
+		c.lastuid = 16r80 + i;
+		c.uids[i] = c.lastuid;
+	}
+	return c;
+}
+
+Categories.unpack(a: array of byte): ref Categories
+{
+	if(len a < 16r114)
+		return nil;		# doesn't match the structure
+	c := ref Categories;
+	c.renamed = get2(a);
+	c.labels = array[16] of string;
+	c.uids = array[16] of int;
+	j := 2;
+	for(i := 0; i < 16; i++){
+		c.labels[i] = latin1(a[j:j+16], 0);
+		j += 16;
+		c.uids[i] = int a[16r102+i];
+	}
+	c.lastuid = int a[16r112];
+	# one byte of padding is shown on p. 26, but
+	# two more are invariably used in practice
+	# before application specific data.
+	if(len a > 16r116)
+		c.appdata = a[16r116:];
+	return c;
+}
+
+Categories.pack(c: self ref Categories): array of byte
+{
+	a := array[16r116 + len c.appdata] of byte;
+	put2(a, c.renamed);
+	j := 2;
+	for(i := 0; i < 16; i++){
+		puts(a[j:j+16], c.labels[i]);
+		j += 16;
+		a[16r102+i] = byte c.uids[i];
+	}
+	a[16r112] = byte c.lastuid;
+	a[16r113] = byte 0;	# pad shown on p. 26
+	a[16r114] = byte 0;	# extra two bytes of padding used in practice
+	a[16r115] = byte 0;
+	if(c.appdata != nil)
+		a[16r116:] = c.appdata;
+	return a;
+}
+
+Categories.mkidmap(c: self ref Categories): array of int
+{
+	a := array[256] of {* => 0};
+	for(i := 0; i < len c.uids; i++)
+		a[c.uids[i]] = i;
+	return a;
+}
+
+#
+# because PalmOS treats all times as local times, and doesn't associate
+# them with time zones, we'll convert using local time on Plan 9 and Inferno
+#
+
+pilot2epoch(t: int): int
+{
+	if(t == 0)
+		return 0;	# we'll assume it's not set
+	return t - Epochdelta + tzoff;
+}
+
+epoch2pilot(t: int): int
+{
+	if(t == 0)
+		return t;
+	return t - tzoff + Epochdelta;
+}
+
+#
+# map Palm name to string, assuming iso-8859-1,
+# but remap space and /
+#
+latin1(a: array of byte, remap: int): string
+{
+	s := "";
+	for(i := 0; i < len a; i++){
+		c := int a[i];
+		if(c == 0)
+			break;
+		if(remap){
+			if(c == ' ')
+				c = 16r00A0;	# unpaddable space
+			else if(c == '/')
+				c = 16r2215;	# division /
+		}
+		s[len s] = c;
+	}
+	return s;
+}
+
+#
+# map from Unicode to Palm name
+#
+filename(name: string): string
+{
+	s := "";
+	for(i := 0; i < len name; i++){
+		c := name[i];
+		if(c == ' ')
+			c = 16r00A0;	# unpaddable space
+		else if(c == '/')
+			c = 16r2215;	# division solidus
+		s[len s] = c;
+	}
+	return s;
+}
+
+dbname(name: string): string
+{
+	s := "";
+	for(i := 0; i < len name; i++){
+		c := name[i];
+		case c {
+		0 =>			c = ' ';	# unlikely, but just in case
+		16r2215 =>	c = '/';
+		16r00A0 =>	c = ' ';
+		}
+		s[len s] = c;
+	}
+	return s;
+}
+
+#
+# string conversion: can't use (string a) because
+# the bytes are Latin1, not Unicode
+#
+gets(a: array of byte): string
+{
+	s := "";
+	for(i := 0; i < len a; i++)
+		s[len s] = int a[i];
+	return s;
+}
+
+puts(a: array of byte, s: string)
+{
+	for(i := 0; i < len a-1 && i < len s; i++)
+		a[i] = byte s[i];
+	for(; i < len a; i++)
+		a[i] = byte 0;
+}
+
+#
+#  big-endian packing
+#
+
+get4(p: array of byte): int
+{
+	return (((((int p[0] << 8) | int p[1]) << 8) | int p[2]) << 8) | int p[3];
+}
+
+get3(p: array of byte): int
+{
+	return (((int p[0] << 8) | int p[1]) << 8) | int p[2];
+}
+
+get2(p: array of byte): int
+{
+	return (int p[0]<<8) | int p[1];
+}
+
+put4(p: array of byte, v: int)
+{
+	p[0] = byte (v>>24);
+	p[1] = byte (v>>16);
+	p[2] = byte (v>>8);
+	p[3] = byte (v & 16rFF);
+}
+
+put3(p: array of byte, v: int)
+{
+	p[0] = byte (v>>16);
+	p[1] = byte (v>>8);
+	p[2] = byte (v & 16rFF);
+}
+
+put2(p: array of byte, v: int)
+{
+	p[0] = byte (v>>8);
+	p[1] = byte (v & 16rFF);
+}
--- /dev/null
+++ b/appl/lib/parseman.b
@@ -1,0 +1,806 @@
+implement Parseman;
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "man.m";
+
+FONT_LITERAL: con -1;
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		return sys->sprint("cannot load module: %r");
+	return nil;
+}
+
+ParseState: adt[T]
+	for{
+	T =>
+		textwidth: fn(t: self T, text: Text): int;
+	}{
+	metrics: Metrics;
+	ql: int;		# quote Literal text
+	margin: int;
+	mstack: list of int;
+	istack: list  of int;
+	indent: int;
+	ntlsetindent: int;	#copy prevailindent to indent on n.t.l
+	prevailindent: int;
+	curfont: int;
+	curattr: int;
+	verbatim: int;
+	pspace: int;
+	curline: list of (int, Text);	# most recent first
+	curwidth: int;
+	newpara: int;
+	heading: int;
+	igto: string;
+	link: string;
+	viewer: T;
+	setline: chan of list of (int, Text);
+
+	# addstring() is simply an addtext() of the current font
+	addstring: fn(s: self ref ParseState, s: string);
+	addtext: fn(s: self ref ParseState, t: list of Text);
+	brk: fn(s: self ref ParseState);
+	paragraph: fn( s: self ref ParseState);
+};
+
+parseman[T](fd: ref Sys->FD, metrics: Metrics, ql: int, viewer: T, setline: chan of list of (int, Text))
+	for{
+	T =>
+		textwidth: fn(t: self T, text: Text): int;
+	}
+{
+	iob := bufio->fopen(fd, Sys->OREAD);
+	state := ref ParseState[T](metrics, ql, 0, nil, nil, 0, 0, metrics.indent, FONT_ROMAN, 0, 0, 1, nil, 0, 1, 0, "", nil, viewer, setline);
+	while ((l := iob.gets('\n')) != nil) {
+		if (l[len l -1] == '\n')
+			l = l[0: len l - 1];
+		if (state.igto != nil && state.igto != l)
+			continue;
+		state.igto = nil;
+		parseline(state, l);
+	}
+	state.pspace = 2;
+	state.pspace = 1;
+	state.paragraph();
+	footer := Text(FONT_ROMAN, 0, "Inferno Manual", 0, nil);
+	textw := state.viewer.textwidth(footer);
+#should do 'center' in addtext (state.justify = CENTER)
+	state.indent = (state.metrics.pagew - textw) / 2;
+	state.addtext(footer::nil);
+	state.brk();
+	setline <- = nil;
+}
+
+parseline[T](state: ref ParseState[T], t: string)
+	for{
+	T =>
+		textwidth: fn(t: self T, text: Text): int;
+	}
+{
+	if (t == nil) {
+		if (state.verbatim) {
+			blank := Text(state.curfont, state.curattr, "", 0, "");
+			state.setline <- = (0, blank)::nil;
+		} else
+			state.paragraph();
+		return;
+	}
+	ntlsetindent := state.ntlsetindent;
+	state.ntlsetindent = 0;
+	if (t[0] == '.' || t[0] == '\'')
+		parsemacro(state, t[1:]);
+	else {
+		state.addtext(parsetext(state, t));
+		if (state.verbatim)
+			state.brk();
+	}
+	if (ntlsetindent) {
+		state.indent = state.prevailindent;
+		if (state.curwidth + state.metrics.en > state.indent + state.margin)
+			state.brk();
+	}
+}
+
+parsemacro[T](state: ref ParseState[T], t: string)
+	for{
+	T =>
+		textwidth: fn(t: self T, text: Text): int;
+	}
+{
+	for (n := 0; n < len t && n < 2; n++)
+		if (t[n] == ' '  || t[n] == '\t')
+			break;
+	macro := t[0:n];
+	params: list of string;
+	quote := 0;
+	param := 0;
+	esc := 0;
+	p := "";
+	for (; n < len t; n++) {
+		if (esc)
+			esc = 0;
+		else {
+			case t[n] {
+			' ' or '\t' =>
+				if (!quote) {
+					if (param) {
+						params = p :: params;
+						p = "";
+						param = 0;
+					}
+				continue;
+				}
+			'"' =>
+				param = 1;
+				quote = !quote;
+				continue;
+			'\\' =>
+				esc = 1;
+			}
+		}
+		param = 1;
+		p[len p] = t[n];
+	}
+	if (param)
+		params = p :: params;
+	plist: list of string;
+	for (; params != nil; params = tl params)
+		plist = hd params :: plist;
+	params = plist;
+
+	case macro {
+		"ig" =>
+			igto := "..";
+			if (params != nil)
+				igto = "." + hd params;
+			state.brk();
+			state.igto = igto;
+		"sp" =>
+			sp := "1";
+			if(params != nil)
+				sp = hd params;
+			d := tval(state.metrics, sp, 'v');
+			gap := d / state.metrics.V;
+			if (gap < 1)
+				gap = 1;
+			while (gap--)
+				state.paragraph();
+		"br" =>
+			state.brk();
+		"nf" =>
+			state.verbatim = 1;
+		"fi" =>
+			state.verbatim = 0;
+		"ti" =>
+			state.brk();
+			#i := 0;
+			#if(params != nil)
+			#	i = tval(state.metrics, hd params, 'n');
+			#state.ntlsetindent = 1;
+			#state.prevailindent = i;
+		"in" =>
+			state.brk();
+			#i := 0;
+			#if(params != nil)
+			#	i = tval(state.metrics, hd params, 'n');
+			#state.indent = i;
+			#state.prevailindent = state.indent;
+		"1C" =>
+			state.brk();
+			# not implemented
+		"2C" =>
+			state.brk();
+			# not implemented
+		"BI" =>
+			altattr(state, FONT_BOLD, FONT_ITALIC, params);
+		"BR" =>
+			altattr(state, FONT_BOLD, FONT_ROMAN, params);
+		"IB" =>
+			altattr(state, FONT_ITALIC, FONT_BOLD, params);
+		"IR" =>
+			# need to determine link if params of valid form
+			state.link = convlink(params);;
+			altattr(state, FONT_ITALIC, FONT_ROMAN, params);
+			state.link = nil;
+		"RB" =>
+			altattr(state, FONT_ROMAN, FONT_BOLD, params);
+		"RI" =>
+			altattr(state, FONT_ROMAN, FONT_ITALIC, params);
+		"B" =>
+			state.curfont = FONT_BOLD;
+			if (params != nil) {
+				for (; params != nil; params = tl params) {
+					textl := parsetext(state, hd params);
+					for (; textl != nil; textl = tl textl)
+						state.addtext(hd textl::nil);
+				}
+				state.curfont = FONT_ROMAN;
+			}
+		"I" =>
+			state.curfont = FONT_ITALIC;
+			if (params != nil) {
+				for (; params != nil; params = tl params) {
+					textl := parsetext(state, hd params);
+					for (; textl != nil; textl = tl textl)
+						state.addtext(hd textl::nil);
+				}
+				state.curfont = FONT_ROMAN;
+			}
+ 		"SM"=>
+			state.curattr |= ATTR_SMALL;
+			if (params != nil) {
+				for (; params != nil; params = tl params)
+					state.addstring(hd params);
+				state.curattr &= ~ATTR_SMALL;
+			}
+		"L" =>
+			state.curfont = FONT_LITERAL;
+			if (params != nil) {
+				str := "`";
+				for (pl := params; pl != nil;) {
+					str += hd pl;
+					if ((pl = tl pl) != nil)
+						str += " ";
+					else
+						break;
+				}
+				str += "'";
+				state.addstring(str);
+				state.curfont = FONT_ROMAN;
+			}
+		"LR" =>
+			if (params != nil) {
+				l := Text(FONT_LITERAL, state.curattr, hd params, 0, nil);
+				t: list of Text;
+				params = tl params;
+				if (params == nil)
+					t = l :: nil;
+				else {
+					r := Text(FONT_ROMAN, state.curattr, hd params, 0, nil);
+					t = l :: r :: nil;
+				}
+				state.addtext(t);
+			}
+		"RL" =>
+			if (params != nil) {
+				r := Text(FONT_ROMAN, state.curattr, hd params, 0, nil);
+				t: list of Text;
+				params = tl params;
+				if (params == nil)
+					t = r :: nil;
+				else {
+					l := Text(FONT_LITERAL, state.curattr, hd params, 0, nil);
+					t = r :: l :: nil;
+				}
+				state.addtext(t);
+			}
+		"DT" =>
+			# not yet supported
+			;
+		"EE" =>
+			state.brk();
+			state.verbatim = 0;
+			state.curfont = FONT_ROMAN;
+		"EX" =>
+			state.brk();
+			state.verbatim = 1;
+			state.curfont = FONT_BOLD;
+		"HP" =>
+			state.paragraph();
+			i := state.metrics.indent;
+			if (params != nil)
+				i = tval(state.metrics, hd params, 'n');
+			state.prevailindent = state.indent + i;
+		"IP" =>
+			state.paragraph();
+			i := state.metrics.indent;
+			if (params != nil) {
+				tag := hd params;
+				params = tl params;
+				state.addtext(parsetext(state, tag));
+				if (params != nil)
+					i = tval(state.metrics, hd params, 'n');
+			}
+			state.indent = state.metrics.indent + i;
+			state.prevailindent = state.indent;
+		"PD" =>
+			state.pspace = 1;
+			if (params != nil) {
+				v := tval(state.metrics, hd params, 'v') / state.metrics.V;
+				state.pspace = v;
+			}
+		"LP" or "PP" =>
+			state.paragraph();
+			state.prevailindent = state.indent;
+		"RE" =>
+			state.brk();
+			if (state.mstack == nil || state.istack == nil)
+				break;
+			
+			state.margin = hd state.mstack;
+			state.mstack = tl state.mstack;
+			state.prevailindent = hd state.istack;
+			state.indent = state.prevailindent;
+			state.istack = tl state.istack;
+		"RS" =>
+			state.brk();
+			i := state.prevailindent - state.metrics.indent;
+			if (params != nil)
+				i = tval(state.metrics, hd params, 'n');
+			state.mstack = state.margin :: state.mstack;
+			state.istack = state.prevailindent :: state.istack;
+			state.margin += i;
+			state.indent = 2 * state.metrics.indent;
+			state.prevailindent = state.indent;
+		"SH" =>
+			state.paragraph();
+			state.prevailindent = state.indent;
+			state.curfont = FONT_ROMAN;
+			state.curattr = 0;
+			state.indent = 0;
+			state.heading = 1;
+			state.verbatim = 0;
+
+			for (pl := params; pl != nil; pl = tl pl)
+				state.addstring(hd pl);
+
+			state.heading = 0;
+			state.brk();
+			state.newpara = 1;
+			state.pspace = 1;
+		"SS" =>
+			state.paragraph();
+			state.prevailindent = state.indent;
+			state.curfont = FONT_ROMAN;
+			state.curattr = 0;
+			state.indent = state.metrics.ssindent;
+			state.heading = 2;
+
+			for (pl := params; pl != nil; pl = tl pl)
+				state.addstring(hd pl);
+
+			state.heading = 0;
+			state.brk();
+			state.newpara = 1;
+			state.pspace = 1;
+
+		"TF" =>
+			state.brk();
+			state.pspace = 0;
+			i := state.metrics.indent;
+			if (params != nil) {
+				str := hd params;
+				text := Text(FONT_BOLD, 0, str, 0, nil);
+				w := state.viewer.textwidth(text) + 2*state.metrics.em;
+				if (w > i)
+					i = w;
+			}
+			state.indent = state.metrics.indent;;
+			state.prevailindent = state.indent + i;
+		"TH" =>
+			state.brk();
+			if (len params < 2)
+				break;
+			str := hd params + "(" + hd tl params + ")";
+			txt := Text(FONT_ROMAN, 0, str, 0, nil);
+			txtw := state.viewer.textwidth(txt);
+			state.indent = 0;
+			state.addtext(txt::nil);
+			state.indent = state.metrics.pagew - txtw;
+			state.addtext(txt::nil);
+			state.indent = 0;
+			state.brk();
+		"TP" =>
+			state.paragraph();
+			if (state.prevailindent == state.metrics.indent)
+				state.prevailindent += state.metrics.indent;
+			state.indent = state.metrics.indent;
+			state.ntlsetindent = 1;
+			if (params != nil) {
+				i := tval(state.metrics, hd params, 'n');
+				if (i == 0)
+					i = state.metrics.indent;
+				state.prevailindent = state.indent + i;
+			}
+		* =>
+			;
+	}
+	if (state.verbatim)
+		state.brk();
+}
+
+parsetext[T](state: ref ParseState[T], t: string): list of Text
+	for{
+	T =>
+		textwidth: fn(t: self T, text: Text): int;
+	}
+{
+	# need to do better here - spot inline font changes etc
+	# we also currently cannot support troff tab stops
+	textl: list of Text;
+	line := "";
+	curfont := state.curfont;
+	prevfont := state.curfont;	# should perhaps be in State
+	step := 1;
+	for (i := 0; i < len t; i += step) {
+		step = 1;
+		ch := t[i];
+		if (ch == '\\') {
+			i++;
+			width := len t - i;
+			if (width <= 0)
+				break;
+			case t[i] {
+			'-' or '.' or '\\' =>
+				ch = t[i];
+			' '  =>
+				ch = ' ';
+			'e' =>
+				ch = '\\';
+			'|' or '&' =>
+				continue;
+			'(' =>
+				if (width > 3)
+					width = 3;
+				step = width;
+				if (step != 3)
+					continue;
+				case t[i+1:i+3] {
+				"bu" =>
+					ch = '•';
+				"em" =>
+					ch = '—';
+				"mi" =>
+					ch = '-';
+				"mu" =>
+					ch = '×';
+				"*m" =>
+					ch = 'µ';
+				"*G" =>
+					ch = 'Γ';
+				"*p" =>
+					ch = 'π';
+				"*b" =>
+					ch = 'β';
+				"<=" =>
+					ch = '≤';
+				"->" =>
+					ch = '→';
+				* =>
+					continue;
+				}
+
+			'f' =>
+				if (width == 1)
+					continue;
+				if (t[i+1] == '(') {
+					if (width > 4)
+						width = 4;
+					step = width;
+					continue;
+				}
+				i++;
+				case t[i] {
+				'0' or 'R' =>
+					curfont = FONT_ROMAN;
+				'1' or 'I' =>
+					curfont = FONT_ITALIC;
+				'2' =>
+					# should be bold but our 'bold' font is constant width
+					curfont = FONT_ROMAN;
+				'5' or 'L' =>
+					curfont = FONT_BOLD;
+				'P' =>
+					curfont = prevfont;
+				}
+				continue;
+			'*' =>
+				if (width == 1)
+					continue;
+				case t[i+1] {
+				'R' =>
+					step = 2;
+					ch = '®';
+				'(' =>
+					if (width > 4)
+						width = 4;
+					step = width;
+					continue;
+				}
+			* =>
+				i--;
+			}
+		}
+		if (curfont != state.curfont) {
+			if (line != "") {
+				txt := Text(state.curfont, state.curattr, line, state.heading, state.link);
+				line = "";
+				textl = txt :: textl;
+			}
+			prevfont = state.curfont;
+			state.curfont = curfont;
+		}
+		line[len line] = ch;
+	}
+	if (line != "") {
+		txt := Text(state.curfont, state.curattr, line, state.heading, state.link);
+		textl = txt :: textl;
+	}
+	state.curfont = curfont;
+
+	r: list of Text;
+	for (; textl != nil; textl = tl textl)
+		r = hd textl :: r;
+	return r;
+}
+
+ParseState[T].addstring(state: self ref ParseState[T], s: string)
+{
+	t := Text(state.curfont, state.curattr, s, state.heading, state.link);
+	state.addtext(t::nil);
+}
+
+ParseState[T].addtext(state: self ref ParseState[T], t: list of Text)
+{
+#dumptextlist(t);
+	# on setting a line copy state.prevailindent to state.indent
+	#
+	# always make sure that current indent is achieved
+	#
+	# if FONT_LITERAL and state.ql then convert to FONT_BOLD and
+	# quote the text before any other processing
+
+	state.newpara = 0;
+	addspace := 1;
+	while (t != nil) {
+		# this scheme is inadequate...
+		# results in mixed formatting at end of line getting split up
+		# e.g.
+		#	.IR man (1)
+		# can get split at the '('
+
+		indent := 0;
+		spacew := 0;
+		text := hd t;
+		t = tl t;
+		if (state.indent + state.margin > state.curwidth || state.curline == nil) {
+			indent = state.indent + state.margin;
+			state.curwidth = indent;
+			addspace = 0;
+			if (!state.verbatim) {
+				text.text = trim(text.text);
+				while (text.text == "" && t != nil) {
+					text = hd t;
+					t = tl t;
+					text.text = trim(text.text);
+				}
+			}
+		}
+
+		if (text.font == FONT_LITERAL) {
+			if (state.ql)
+				text.text = "`" + text.text + "'";
+			text.font = FONT_BOLD;
+		}
+		if (addspace) {
+			(nil, previtem) := hd state.curline;
+			if (previtem.text[len previtem.text -1] == ' ')
+				addspace = 0;
+			else {
+				space := Text(previtem.font, previtem.attr, " ", 0, nil);
+				spacew = state.viewer.textwidth(space);
+			}
+		}
+		# it doesn't fit - try to word wrap...
+		t2 := text;
+		end := len text.text;
+		prevend := end;
+		nextstart := 0;
+		while (end > 0) {
+			t2.text = text.text[0:end];
+			tlen := state.viewer.textwidth(t2);
+			if (state.verbatim || state.curwidth + spacew + tlen <= state.metrics.pagew) {
+				# easy - just add it!
+				state.curwidth += spacew+tlen;
+				if (addspace) {
+					t2.text = " " + t2.text;
+					addspace = 0;
+				}
+				state.curline = (indent, t2) :: state.curline;
+				indent = 0;
+				break;
+			}
+			prevend = end;
+			for (; end > 0; end--) {
+				if (t2.text[end-1] == ' ') {
+					nextstart = end;
+					for (; end >0 && t2.text[end-1] == ' '; end--)
+						;
+					break;
+				}
+			}
+		}
+		if (end != len text.text) {
+			# couldn't fit whole item onto line
+			if (state.curline == nil) {
+				# couldn't fit (sub)item on empty line - add it anyway
+				# as there is nowhere else to put it
+				end = prevend;
+				t2.text = text.text[0:end];
+				state.curline = (indent, t2) :: state.curline;
+				if (nextstart != 0) {
+					text.text = text.text[nextstart:];
+					t = text :: t;
+				}
+			} else {
+				# already stuff on line and we have consumed upto nexstart of
+				# the current item
+				if (end != 0)
+					text.text = text.text[nextstart:];
+				t = text :: t;
+			}
+			state.brk();
+		}
+		addspace = 0;
+	}
+}
+
+trim(s: string): string
+{
+	for (spi :=0; spi < len s && s[spi] == ' '; spi++)
+			;
+	return s[spi:];
+}
+
+ParseState[T].brk(state: self ref ParseState)
+{
+	if (state.curline != nil) {
+		line: list of (int, Text);
+		for (l := state.curline; l != nil; l = tl l)
+			line = hd l :: line;
+		state.setline <- = line;
+		state.curline = nil;
+		state.curwidth = 0;
+	}
+	state.indent = state.prevailindent;
+}
+
+ParseState[T].paragraph(state: self ref ParseState)
+{
+	state.brk();
+	if (state.newpara == 0) {
+		blank := Text(state.curfont, state.curattr, "", 0, "");
+		for (i := 0; i < state.pspace; i++)
+			state.setline <- = (0, blank)::nil;
+		state.newpara = 1;
+	}
+	state.curattr = 0;
+	state.curfont = FONT_ROMAN;
+	state.indent = state.metrics.indent;
+#	state.prevailindent = state.indent;
+	state.ntlsetindent = 0;
+}
+
+# convert troff 'values' into output 'dots'
+tval(m: Metrics, v: string, defunits: int): int
+{
+	if (v == nil)
+		return 0;
+	units := v[len v -1];
+	val: real;
+
+	case units {
+	'i' or
+	'c' or
+	'P' or
+	'm' or
+	'n' or
+	'p' or
+	'u' or
+	'v' =>
+		val = real v[0:len v - 1];
+	* =>
+		val = real v;
+		units = defunits;
+	}
+	r := 0;
+	case units {
+	'i' =>
+		r = int (real m.dpi * val);
+	'c' =>
+		r =  int ((real m.dpi * val)/2.54);
+	'P' =>
+		r =  int ((real m.dpi * val)/ 6.0);
+	'm' =>
+		r =  int (real m.em * val);
+	'n' =>
+		r =  int (real m.en * val);
+	'p' =>
+		r =  int ((real m.dpi * val)/72.0);
+	'u' =>
+		r =  int val;
+	'v' =>
+		r =  int (real m.V * val);
+	}
+	return r;
+}
+
+altattr[T](state: ref ParseState[T], f1, f2: int, strs: list of string)
+	for{
+	T =>
+		textwidth: fn(t: self T, text: Text): int;
+	}
+{
+	index := 0;
+	textl: list of Text;
+
+	prevfont := state.curfont;
+	for (; strs != nil; strs = tl strs) {
+		str := hd strs;
+		f := f1;
+		if (index++ & 1)
+			f = f2;
+		state.curfont = f;
+		newtext := parsetext(state, str);
+		for (; newtext != nil; newtext = tl newtext)
+			textl = hd newtext :: textl;
+	}
+	orderedtext: list of Text;
+	for (; textl != nil; textl = tl textl)
+		orderedtext = hd textl :: orderedtext;
+	state.addtext(orderedtext);
+	state.curfont = prevfont;
+}
+
+dumptextlist(t: list of Text)
+{
+	sys->print("textlist[");
+	for (; t != nil; t = tl t) {
+		s := hd t;
+		sys->print("(%s)", s.text);
+	}
+	sys->print("]\n");
+}
+
+convlink(params: list of string): string
+{
+	# merge the texts
+	s := "";
+	for (; params != nil; params = tl params)
+		s = s + (hd params);
+
+	for (i := 0; i < len s; i ++)
+		if (s[i] == '(')
+			break;
+	if (i+1 >= len s)
+		return nil;
+	cmd := s[0:i];
+	i++;
+	s = s[i:];
+	for (i = 0; i < len s; i++)
+		if (s[i] == ')')
+			break;
+	section := s[0:i];
+	if (section == nil || !isint(section))
+		return nil;
+
+	return section + " " + cmd;
+}
+
+isint(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] != '.' && (s[i] < '0' || s[i] > '9'))
+			return 0;
+	return 1;
+}
--- /dev/null
+++ b/appl/lib/plumbing.b
@@ -1,0 +1,254 @@
+implement Plumbing;
+
+include "sys.m";
+	sys: Sys;
+
+include "regex.m";
+	regex: Regex;
+
+include "plumbing.m";
+
+init(regexmod: Regex, args: list of string): (list of ref Rule, string)
+{
+	sys = load Sys Sys->PATH;
+	regex = regexmod;
+
+	if(args == nil){
+		user := readfile("/dev/user");
+		if(user == nil)
+			return (nil, sys->sprint("can't read /dev/user: %r"));
+		filename := "/usr/"+user+"/plumbing";
+		(rc, nil) := sys->stat(filename);
+		if(rc < 0)
+			filename = "/usr/"+user+"/lib/plumbing";
+		args = filename :: nil;
+	}
+	r, rules: list of ref Rule;
+	err: string;
+	while(args != nil){
+		filename := hd args;
+		args = tl args;
+		file := readfile(filename);
+		if(file == nil)
+			return (nil, sys->sprint("can't read %s: %r", filename));
+		(r, err) = parse(filename, file);
+		if(err != nil)
+			return (nil, err);
+		while(r != nil){
+			rules = hd r :: rules;
+			r = tl r;
+		}
+	}
+	# reverse the rules
+	r = nil;
+	while(rules != nil){
+		r = hd rules :: r;
+		rules = tl rules;
+	}
+	return (r, nil);
+}
+
+readfile(filename: string): string
+{
+	fd := sys->open(filename, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	(ok, dir) := sys->fstat(fd);
+	if(ok < 0)
+		return nil;
+	size := int dir.length;
+	if(size == 0)	# devices have length 0 sometimes
+		size = 1000;
+	b := array[size] of byte;
+	n := sys->read(fd, b, len b);
+	if(n <= 0)
+		return nil;
+	return string b[0:n];
+}
+
+parse(filename, file: string): (list of ref Rule, string)
+{
+	line: string;
+	lineno := 0;
+	i := 0;
+	pats: list of ref Pattern;
+	rules: list of ref Rule;
+	while(i < len file){
+		lineno++;
+		(line, i) = nextline(file, i);
+		(pat, err) := pattern(line);
+		if(err != nil)
+			return (nil, sys->sprint("%s:%d: %s", filename, lineno, err));
+		if(pat == nil){
+			if(pats==nil || !blank(line))	# comment line
+				continue;
+			(rul, err1) := rule(pats);
+			if(err1 != nil)
+				return (nil, sys->sprint("%s:%d: %s", filename, lineno-1, err1));
+			rules = rul :: rules;
+			pats = nil;
+		}else
+			pats = pat :: pats;
+	}
+	if(pats != nil){
+		(rul, err1) := rule(pats);
+		if(err1 != nil)
+			return (nil, sys->sprint("%s:%d: %s", filename, lineno-1, err1));
+		rules = rul :: rules;
+	}
+	# reverse the rules
+	r: list of ref Rule;
+	while(rules != nil){
+		r = hd rules :: r;
+		rules = tl rules;
+	}
+	return (r, nil);
+}
+
+nextline(file: string, i: int): (string, int)
+{
+	for(j:=i; j<len file; j++)
+		if(file[j] == '\n')
+			return (file[i:j], j+1);
+	return (file[i:], len file);
+}
+
+blank(line: string): int
+{
+	for(i:=0; i<len line; i++)
+		if(line[i]!=' ' && line[i]!='\t')
+			return 0;
+	return 1;
+}
+
+pattern(line: string): (ref Pattern, string)
+{
+	expand := 0;
+	for(i:=0; i<len line; i++)
+		if(line[i] == '$'){
+			expand = 1;
+			break;
+		}
+	(w, err) := words(line);
+	if(err != nil)
+		return (nil, err);
+	if(w == nil)
+		return (nil, nil);
+	if(len w < 3)
+		return (nil, "syntax error: too few words on line");
+	pat := ref Pattern;
+	pat.field = hd w;
+	pat.pred = hd tl w;
+	pat.arg = hd tl tl w;
+	pat.extra = tl tl tl w;
+	pat.expand = expand;
+	return (pat, nil);
+}
+
+rule(pats: list of ref Pattern): (ref Rule, string)
+{
+	# pats is in reverse order on arrival
+	actionpred := list of {"alwaysstart", "start", "to"};
+	patternpred := list of {"is", "isdir", "isfile", "matches", "set"};
+	npats := 0;
+	nacts := 0;
+	haveto := 0;
+	for(l:=pats; l!=nil; l=tl l){
+		pat := hd l;
+		pred := pat.pred;
+		noextra := 1;
+		case pat.field {
+		"plumb" =>
+			nacts++;
+			if(!oneof(pred, actionpred))
+				return (nil, "illegal predicate "+pred+" in action");
+			case pred {
+			"to" or "alwaysstart" =>
+				if(len pat.arg == 0)
+					return (nil, "\"plumb "+pred+"\" must have non-empty target");
+				haveto = 1;
+			"start" =>
+				noextra = 0;
+			}
+			if(npats != 0)
+				return (nil, "actions must follow patterns in rule");
+		"src" or "dst" or "dir" or "kind" or "attr" or "data" =>
+			if(!oneof(pred, patternpred))
+				return (nil, "illegal predicate "+pred+" in pattern");
+			if(pred == "matches"){
+				(pat.regex, nil) = regex->compile(pat.arg, 1);
+				if(pat.regex == nil)
+					return (nil, sys->sprint("error in regular expression '%s'", pat.arg));
+			}
+			npats++;
+		}
+		if(noextra && pat.extra != nil)
+			return (nil, sys->sprint("too many words in '%s' pattern", pat.field));
+	}
+	if(haveto == 0)
+		return (nil, "rule must have \"plumb to\" action");
+	rule := ref Rule;
+	rule.action = array[nacts] of ref Pattern;
+	for(i:=nacts; --i>=0; ){
+		rule.action[i] = hd pats;
+		pats = tl pats;
+	}
+	rule.pattern = array[npats] of ref Pattern;
+	for(i=npats; --i>=0; ){
+		rule.pattern[i] = hd pats;
+		pats = tl pats;
+	}
+	return (rule, nil);
+}
+
+oneof(word: string, words: list of string): int
+{
+	while(words != nil){
+		if(word == hd words)
+			return 1;
+		words = tl words;
+	}
+	return 0;
+}
+
+words(line: string): (list of string, string)
+{
+	ws: list of string;
+	i := 0;
+	for(;;){
+		# not in word; find beginning of word
+		while(i<len line && (line[i]==' ' || line[i]=='\t'))
+			i++;
+		if(i==len line || line[i]=='#')
+			break;
+		# i is first character of word; is it quoted?
+		if(line[i] == '\''){
+			word := "";
+			i++;
+			while(i < len line){
+				c := line[i++];
+				if(c=='\''){
+					if(i==len line || line[i]!='\'')
+						break;
+					# else it's a literal quote
+					if(i < len line)
+						i++;
+				}
+				word[len word] = c;
+			}
+			ws = word :: ws;
+			continue;
+		}
+		# regular word; continue until white space or end
+		start := i;
+		while(i<len line && (line[i]!=' ' && line[i]!='\t'))
+			i++;
+		ws = line[start:i] :: ws;
+	}
+	r: list of string;
+	while(ws != nil){
+		r = hd ws :: r;
+		ws = tl ws;
+	}
+	return (r, nil);
+}
--- /dev/null
+++ b/appl/lib/plumbing.m
@@ -1,0 +1,23 @@
+Plumbing: module
+{
+
+	PATH:	con "/dis/lib/plumbing.dis";
+
+	Pattern: adt
+	{
+		field:		string;
+		pred:	string;
+		arg:		string;
+		extra:	list of string;
+		expand:	int;
+		regex:	Regex->Re;
+	};
+
+	Rule:	adt
+	{
+		pattern:	array of ref Pattern;
+		action:	array of ref Pattern;
+	};
+
+	init:	fn(regexmod: Regex, args: list of string): (list of ref Rule, string);
+};
--- /dev/null
+++ b/appl/lib/plumbmsg.b
@@ -1,0 +1,190 @@
+implement Plumbmsg;
+
+include "sys.m";
+	sys: Sys;
+
+include "plumbmsg.m";
+
+input: ref Sys->FD;
+port: ref Sys->FD;
+portname: string;
+maxdatasize: int;
+
+init(doinput: int, rcvport: string, maxdata: int): int
+{
+	sys = load Sys Sys->PATH;
+
+	if(!doinput && rcvport == nil)	# server, not client
+		return 1;
+	input = sys->open("/chan/plumb.input", Sys->OWRITE);
+	if(input == nil)
+		return -1;
+	if(rcvport == nil)	# sending messages but never receiving them
+		return 1;
+	port = sys->open("/chan/plumb."+rcvport, Sys->OREAD);
+	if(port == nil){
+		input = nil;
+		return -1;
+	}
+	maxdatasize = maxdata;
+	portname = rcvport;
+	msg := ref Msg;
+	msg.src = portname;
+	msg.dst = "plumb";
+	msg.kind = "text";
+	msg.data = array of byte "start";
+	if(msg.send() < 0){
+		port = nil;
+		input = nil;
+		return -1;
+	}
+	return 1;
+}
+
+shutdown()
+{
+	msg := ref Msg;
+	msg.src = portname;
+	msg.dst = "plumb";
+	msg.kind = "text";
+	msg.data = array of byte "stop";
+	msg.send();
+}
+
+Msg.send(msg: self ref Msg): int
+{
+	hdr :=
+		msg.src+"\n"+
+		msg.dst+"\n"+
+		msg.dir+"\n"+
+		msg.kind+"\n"+
+		msg.attr+"\n"+
+		string len msg.data+"\n";
+	ahdr := array of byte hdr;
+	b := array[len ahdr+len msg.data] of byte;
+	b[0:] = ahdr;
+	b[len ahdr:] = msg.data;
+	return sys->write(input, b, len b);
+}
+
+Msg.recv(): ref Msg
+{
+	b := array[maxdatasize+1000] of byte;
+	n := sys->read(port, b, len b);
+	if(n <= 0)
+		return nil;
+	return Msg.unpack(b[0:n]);
+}
+
+Msg.unpack(b: array of byte): ref Msg
+{
+	(hdr, data) := unpack(b, 6);
+	if(hdr == nil)
+		return nil;
+
+	msg := ref Msg;
+	msg.src = hdr[0];
+	msg.dst = hdr[1];
+	msg.dir = hdr[2];
+	msg.kind = hdr[3];
+	msg.attr = hdr[4];
+	msg.data = data;
+
+	return msg;
+}
+
+Msg.pack(msg: self ref Msg): array of byte
+{
+	hdr :=
+		msg.src+"\n"+
+		msg.dst+"\n"+
+		msg.dir+"\n"+
+		msg.kind+"\n"+
+		msg.attr+"\n"+
+		string len msg.data+"\n";
+	ahdr := array of byte hdr;
+	b := array[len ahdr+len msg.data] of byte;
+	b[0:] = ahdr;
+	b[len ahdr:] = msg.data;
+	return b;
+}
+
+# unpack message from array of bytes.  last string in message
+# is number of bytes in data portion of message
+unpack(b: array of byte, ns: int): (array of string, array of byte)
+{
+	i := 0;
+	a := array[ns] of string;
+	for(n:=0; n<ns; n++){
+		(i, a[n]) = unpackstring(b, i);
+		if(i < 0)
+			return (nil, nil);
+	}
+	nb := int a[ns-1];
+	if((len b)-i != nb){
+		sys->print("unpack: bad message format: wrong nbytes\n");
+		return (nil, nil);
+	}
+	# copy data so b can be reused or freed
+	data := array[nb] of byte;
+	data[0:] = b[i:];
+	return (a, data);
+}
+
+unpackstring(b: array of byte, i: int): (int, string)
+{
+	starti := i;
+	while(i < len b){
+		if(b[i] == byte '\n')
+			return (i+1, string b[starti:i]);
+		i++;
+	}
+	return (-1, nil);
+}
+
+string2attrs(s: string): list of ref Attr
+{
+	(nil, pairs) := sys->tokenize(s, "\t");
+	if(pairs == nil)
+		return nil;
+	attrs: list of ref Attr;
+	while(pairs != nil){
+		pair := hd pairs;
+		pairs = tl pairs;
+		a := ref Attr;
+		for(i:=0; i<len pair; i++)
+			if(pair[i] == '='){
+				a.name = pair[0:i];
+				if(++i < len pair)
+					a.val = pair[i:];
+				break;
+			}
+		attrs = a :: attrs;
+	}
+	return attrs;
+}
+
+attrs2string(l: list of ref Attr): string
+{
+	s := "";
+	while(l != nil){
+		a := hd l;
+		l = tl l;
+		if(s == "")
+			s = a.name + "=" + a.val;
+		else
+			s += "\t" + a.name + "=" + a.val;
+	}
+	return s;
+}
+
+lookup(attrs: list of ref Attr, name: string): (int, string)
+{
+	while(attrs != nil){
+		a := hd attrs;
+		attrs = tl attrs;
+		if(a.name == name)
+			return (1, a.val);
+	}
+	return (0, nil);
+}
--- /dev/null
+++ b/appl/lib/pop3.b
@@ -1,0 +1,297 @@
+implement Pop3;
+ 
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "bufio.m";
+	bufio : Bufio;
+include "dial.m";
+	dial: Dial;
+include "pop3.m";
+
+FD: import sys;
+Iobuf : import bufio;
+Connection: import dial;
+
+ibuf, obuf : ref Bufio->Iobuf;
+conn : int = 0;
+inited : int = 0;
+ 
+rpid : int = -1;
+cread : chan of (int, string);
+
+DEBUG : con 0;
+
+open(user, password, server : string): (int, string)
+{
+	s : string;
+ 
+	if (!inited) {
+		sys = load Sys Sys->PATH;
+		bufio = load Bufio Bufio->PATH;
+		dial = load Dial Dial->PATH;
+		inited = 1;
+	}
+	if (conn)
+		return (-1, "connection is already open");
+	if (server == nil)
+		server = "$pop3";
+	else
+		server = dial->netmkaddr(server, "net", "110");
+	c := dial->dial(server, nil);
+	if (c == nil)
+		return (-1, "dialup failed");
+	ibuf = bufio->fopen(c.dfd, Bufio->OREAD);
+	obuf = bufio->fopen(c.dfd, Bufio->OWRITE);
+	if (ibuf == nil || obuf == nil)
+		return (-1, "failed to open bufio");
+	cread = chan of (int, string);
+	spawn mreader(cread);
+	(rpid, nil) = <- cread;
+	ok: int;
+ 	(ok, s) = mread();
+	if (ok < 0)
+		return (-1, s);
+	(ok, s) = mcmd("USER " + user);
+	if (ok < 0)
+		return (-1, s);
+	(ok, s) = mcmd("PASS " + password);
+	if (ok < 0)
+		return (-1, s);
+	conn = 1;
+	return (1, nil);
+}
+
+stat() : (int, string, int, int)
+{
+	if (!conn)
+		return (-1, "not connected", 0, 0);
+	(ok, s) := mcmd("STAT");
+	if (ok < 0)
+		return (-1, s, 0, 0);
+	(n, ls) := sys->tokenize(s, " ");
+	if (n == 3)
+		return (1, nil, int hd tl ls, int hd tl tl ls);
+	return (-1, "stat failed", 0, 0);
+}
+	
+msglist() : (int, string, list of (int, int))
+{
+	ls : list of (int, int);
+
+	if (!conn)
+		return (-1, "not connected", nil);
+	(ok, s) := mcmd("LIST");
+	if (ok < 0)
+		return (-1, s, nil);
+	for (;;) {
+		(ok, s) = mread();
+		if (ok < 0)
+			return (-1, s, nil);
+		if (len s < 3) {
+			if (len s > 0 && s[0] == '.')
+				return (1, nil, rev2(ls));
+			else
+				return (-1, s, nil);
+		}
+		else {
+			(n, sl) := sys->tokenize(s, " ");
+			if (n == 2)
+				ls = (int hd sl, int hd tl sl) :: ls;
+			else
+				return (-1, "bad list format", nil);
+		}
+	}
+}
+
+msgnolist() : (int, string, list of int)
+{
+	ls : list of int;
+
+	if (!conn)
+		return (-1, "not connected", nil);
+	(ok, s) := mcmd("LIST");
+	if (ok < 0)
+		return (-1, s, nil);
+	for (;;) {
+		(ok, s) = mread();
+		if (ok < 0)
+			return (-1, s, nil);
+		if (len s < 3) {
+			if (len s > 0 && s[0] == '.')
+				return (1, nil, rev1(ls));
+			else
+				return (-1, s, nil);
+		}
+		else {
+			(n, sl) := sys->tokenize(s, " ");
+			if (n == 2)
+				ls = int hd sl :: ls;
+			else
+				return (-1, "bad list format", nil);
+		}
+	}
+}
+
+top(m : int) : (int, string, string)
+{
+	if (!conn)
+		return (-1, "not connected", nil);
+	(ok, s) := mcmd("TOP " + string m + " 1");
+	if (ok < 0)
+		return (-1, s, nil);
+	return getbdy();
+}
+
+get(m : int) : (int, string, string)
+{
+	if (!conn)
+		return (-1, "not connected", nil);
+	(ok, s) := mcmd("RETR " + string m);
+	if (ok < 0)
+		return (-1, s, nil);
+	return getbdy();
+}
+	
+getbdy() : (int, string, string)
+{
+	b : string;
+
+	for (;;) {
+		(ok, s) := mread();
+		if (ok < 0)
+			return (-1, s, nil);
+		if (s == ".")
+			break;
+		if (len s > 1 && s[0] == '.' && s[1] == '.')
+			s = s[1:];
+		b = b + s + "\n";
+	}
+	return (1, nil, b);
+}
+	
+delete(m : int) : (int, string)
+{
+	if (!conn)
+		return (-1, "not connected");
+	return mcmd("DELE " + string m);
+}
+			
+close(): (int, string)
+{
+	if (!conn)
+		return (-1, "connection not open");
+	ok := mwrite("QUIT");
+	kill(rpid);
+	ibuf.close();
+	obuf.close();
+	conn = 0;
+	if (ok < 0)
+		return (-1, "failed to close connection");
+	return (1, nil);
+}
+ 
+SLPTIME : con 100;
+MAXSLPTIME : con 10000;
+
+mread() : (int, string)
+{
+	t := 0;
+	while (t < MAXSLPTIME) {
+		alt {
+			(ok, s) := <- cread =>
+				return (ok, s);
+			* =>
+				t += SLPTIME;
+				sys->sleep(SLPTIME);
+		}
+	}
+	kill(rpid);
+	return (-1, "smtp timed out\n");	
+}
+
+mreader(c : chan of (int, string))
+{
+	c <- = (sys->pctl(0, nil), nil);
+	for (;;) {
+		line := ibuf.gets('\n');
+		if (DEBUG)
+			sys->print("mread : %s", line);
+		if (line == nil) {
+			c <- = (-1, "could not read response from server");
+			continue;
+		}
+		l := len line;
+		if (line[l-1] == '\n')
+			l--;
+		if (line[l-1] == '\r')
+			l--;
+		c <- = (1, line[0:l]);
+	}
+}
+ 
+mwrite(s : string): int
+{
+	s += "\r\n";
+	if (DEBUG)
+		sys->print("mwrite : %s", s);
+	b := array of byte s;
+	l := len b;
+	nb := obuf.write(b, l);
+	obuf.flush();
+	if (nb != l)
+		return -1;
+	return 1;
+}
+ 
+mcmd(s : string) : (int, string)
+{
+	ok : int;
+	r : string;
+
+	ok = mwrite(s);
+	if (ok < 0)
+		return (-1, err(s) + " send failed");
+	(ok, r) = mread();
+	if (ok < 0)
+		return (-1, err(s) + " receive failed (" + r + ")");
+	if (len r > 1 && r[0] == '+')
+		return (1, r);
+	return (-1, r);
+}
+
+rev1(l1 : list of int) : list of int
+{
+	l2 : list of int;
+
+	for ( ; l1 != nil; l1 = tl l1)
+		l2 = hd l1 :: l2;
+	return l2;
+}
+
+rev2(l1 : list of (int, int)) : list of (int, int)
+{
+	l2 : list of (int, int);
+
+	for ( ; l1 != nil; l1 = tl l1)
+		l2 = hd l1 :: l2;
+	return l2;
+}
+
+err(s : string) : string
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == ' ' || s[i] == ':')
+			return s[0:i];
+	return s;
+}
+
+kill(pid : int) : int
+{
+	if (pid < 0)
+		return 0;
+	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if (fd == nil || sys->fprint(fd, "kill") < 0)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/appl/lib/popup.b
@@ -1,0 +1,124 @@
+implement Popup;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	Point: import Draw;
+include "tk.m";
+	tk: Tk;
+include "popup.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	tk = load Tk Tk->PATH;
+}
+
+post(win: ref Tk->Toplevel, p: Point, a: array of string, n: int): chan of int
+{
+	rc := chan of int;
+	spawn postproc(win, p, a, n, rc);
+	return rc;
+}
+
+postproc(win: ref Tk->Toplevel, p: Point, a: array of string, n: int, rc: chan of int)
+{
+	c := chan of string;
+	tk->namechan(win, c, "c.popup");
+	mkpopupmenu(win, a);
+	cmd(win, ".popup entryconfigure " + string n + " -state active");
+	cmd(win, "bind .popup <Unmap> {send c.popup unmap}");
+
+	dy := ypos(win, n) - ypos(win, 0);
+	p.y -= dy;
+	cmd(win, ".popup post " + string p.x + " " + string p.y +
+		";grab set .popup");
+	n = -1;
+	while ((e := <-c) != "unmap")
+		n = int e;
+
+	cmd(win, "destroy .popup");
+	rc <-= n;
+}
+
+mkpopupmenu(win: ref Tk->Toplevel, a: array of string)
+{
+	cmd(win, "menu .popup");
+	for (i := 0; i < len a; i++) {
+		cmd(win, ".popup add command -command {send c.popup " + string i +
+			"} -text '" + a[i]);
+	}
+}
+
+Blank: con "-----";
+
+# XXX what should we do about popups containing no items.
+mkbutton(win: ref Tk->Toplevel, w: string, a: array of string, n: int): chan of string
+{
+	c := chan of string;
+	if (len a == 0) {
+		cmd(win, "label " + w + " -bd 2 -relief raised -text '" + Blank);
+		return c;
+	}
+	tk->namechan(win, c, "c" + w);
+	mkpopupmenu(win, a);
+	cmd(win, "label " + w + " -bd 2 -relief raised -width [.popup cget -width] -text '" + a[n]);
+	cmd(win, "bind " + w + " <Button-1> {send c" + w + " " + w + "}");
+	cmd(win, "destroy .popup");
+	return c;
+}
+
+changebutton(win: ref Tk->Toplevel, w: string, a: array of string, n: int)
+{
+	if (len a > 0) {
+		mkpopupmenu(win, a);
+		cmd(win, w + " configure -width [.popup cget -width] -text '" + a[n]);
+		cmd(win, "bind " + w + " <Button-1> {send c" + w + " " + w + "}");
+		cmd(win, "destroy .popup");
+	} else {
+		cmd(win, w + " configure -text '" + Blank);
+		cmd(win, "bind " + w + " <Button-1> {}");
+	}
+}
+
+add(a: array of string, s: string): (array of string, int)
+{
+	for (i := 0; i < len a; i++)
+		if (s == a[i])
+			return (a, i);
+	na := array[len a + 1] of string;
+	na[0:] = a;
+	na[len a] = s;
+	return (na, len a);
+}
+
+#event(win: ref Tk->Toplevel, e: string, a: array of string): int
+#{
+#	w := e;
+#	p := Point(int cmd(win, w + " cget -actx"), int cmd(win, w + " cget -acty"));
+#	s := cmd(win, w + " cget -text");
+#	for (i := 0; i < len a; i++)
+#		if (s == a[i])
+#			break;
+#	if (i == len a)
+#		i = 0;
+#		
+#	n := post(win, p, a, i);
+#	if (n != -1) {
+#		cmd(win, w + " configure -text '" + a[n]);
+#		i = n;
+#	}
+#	return i;
+#}
+
+ypos(win: ref Tk->Toplevel, n: int): int
+{
+	return int cmd(win, ".popup yposition " + string n);
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+	r := tk->cmd(win, s);
+	if (len r > 0 && r[0] == '!')
+		sys->print("error executing '%s': %s\n", s, r[1:]);
+	return r;
+}
--- /dev/null
+++ b/appl/lib/powerman.b
@@ -1,0 +1,59 @@
+implement Powerman;
+
+#
+# Copyright © 2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "powerman.m";
+
+pid := 0;
+
+init(file: string, events: chan of string): int
+{
+	if(file == nil)
+		file = "/dev/powerdata";
+	fd := sys->open(file, Sys->OREAD);
+	if(fd == nil)
+		return -1;
+	pidc := chan of int;
+	spawn reader(fd, events, pidc);
+	return pid = <-pidc;
+}
+
+reader(fd: ref Sys->FD, events: chan of string, pidc: chan of int)
+{
+	pidc <-= sys->pctl(0, nil);
+	buf := array[128] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0){
+		if(buf[n-1] == byte '\n')
+			n--;
+		events <-= string buf[0:n];
+	}
+	events <-= "error";
+}
+
+stop()
+{
+	if(pid != 0){
+		fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+		if(fd != nil)
+			sys->fprint(fd, "kill");
+		pid = 0;
+	}
+}
+
+ack(op: string)
+{
+	ctl("ack "+op);
+}
+
+ctl(op: string): string
+{
+	fd := sys->open("/dev/powerctl", Sys->OWRITE);
+	if(fd != nil && sys->fprint(fd, "%s", op) >= 0)
+		return nil;
+	return sys->sprint("%r");
+}
--- /dev/null
+++ b/appl/lib/print/hp_driver.b
@@ -1,0 +1,1536 @@
+implement Pdriver;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Font, Rect, Point, Image, Screen: import draw;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "print.m";
+	Printer: import Print;
+include "scaler.m";
+	scaler: Scaler;
+
+
+K: con 0;
+C: con 1;
+M: con 2;
+Y: con 3;
+Clight: con 4;
+Mlight: con 5;
+
+HPTRUE: con 1;
+HPFALSE: con 0;
+TRUE: con 1;
+FALSE: con 0;
+
+# RGB pixel
+
+RGB: adt {
+	r, g, b: byte;
+};
+
+
+# KCMY pixel
+
+KCMY: adt {
+	k, c, m, y: byte;
+};
+
+
+
+DitherParms: adt {
+	fNumPix: int;
+	fInput: array of byte;
+	fErr: array of int;
+	fSymmetricFlag: int;
+	fFEDRes: array of int;
+	fRasterEvenOrOdd: int;
+	fHifipe: int;
+	fOutput1, fOutput2, fOutput3: array of byte;
+};
+
+# magic and wondrous HP colour maps
+map1: array of KCMY;
+map2: array of KCMY;
+
+ABSOLUTE: con 1;
+RELATIVE: con 0;
+
+Compression := 1;
+
+DEBUG := 0;
+stderr: ref Sys->FD;
+outbuf: ref Iobuf;
+
+ESC: con 27;
+
+# Palettes for Simple_Color
+
+PALETTE_RGB: con 3;
+PALETTE_CMY: con -3;
+PALETTE_KCMY: con -4;
+PALETTE_K: con 1;
+
+
+# Initialization
+
+init(debug: int)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	bufio = load Bufio Bufio->PATH;
+	scaler = load Scaler Scaler->PATH;
+	if (scaler == nil) fatal("Failed to load Scaler module");
+	DEBUG = debug;
+}
+
+
+# Return printable area in pixels
+
+printable_pixels(p: ref Printer): (int, int)
+{
+	HMARGIN: con 0.6;
+	WMARGIN: con 0.3;
+	winches := p.popt.paper.width_inches - 2.0*WMARGIN;
+	hinches := p.popt.paper.height_inches - 2.0*HMARGIN;
+	wres := real p.popt.mode.resx;
+	hres := real p.popt.mode.resy;
+	
+	(x, y) := (int (winches*wres), int (hinches*hres));
+
+	if (p.popt.orientation == Print->PORTRAIT)
+		return (x, y);
+	return (y, x);
+}
+
+
+
+# Send image to printer
+
+MASK := array[] of {byte 1, byte 3, byte 15, byte 255, byte 255};
+SHIFT := array[] of {7, 6, 4, 0};
+GSFACTOR := array[] of {255.0, 255.0/3.0, 255.0/7.0, 1.0, 1.0};
+lastp : ref Printer;
+
+Refint: adt {
+	value: int;
+};
+
+watchdog(cancel: chan of int, cancelled: ref Refint)
+{
+	<- cancel;
+	cancelled.value = 1;
+}
+
+sendimage(p: ref Printer, pfd: ref Sys->FD, display: ref Draw->Display, im: ref Draw->Image, width: int, lmargin: int, cancel: chan of int): int
+{
+	grppid := sys->pctl(Sys->NEWPGRP, nil);
+	cancelled := ref Refint(0);
+	spawn watchdog(cancel, cancelled);
+
+	outopen(pfd);
+	dbg(sys->sprint("image depth=%d from %d,%d to %d,%d\n", im.depth, im.r.min.x, im.r.min.y, im.r.max.x, im.r.max.y));
+	if (p != lastp) {
+		(map1, map2) = readmaps(p);
+		lastp = p;
+	}
+
+	bpp := im.depth;
+	linechan := chan of array of int;
+	if (p.popt.orientation == Print->PORTRAIT)
+		InputWidth := im.r.max.x-im.r.min.x;
+	else
+		InputWidth = im.r.max.y-im.r.min.y;
+	AdjustedInputWidth := (InputWidth+7) - ((InputWidth+7) % 8);
+	dbg(sys->sprint("bpp=%d, InputWidth=%d, AdjustedInputWidth=%d\n",
+						 bpp, InputWidth, AdjustedInputWidth));
+	if (p.popt.orientation == Print->PORTRAIT)
+		spawn row_by_row(im, linechan, AdjustedInputWidth);
+	else
+		spawn rotate(im, linechan, AdjustedInputWidth);
+	DesiredOutputWidth := AdjustedInputWidth;
+	if (width > AdjustedInputWidth)
+		DesiredOutputWidth = width;
+	ScaledWidth := 8*((DesiredOutputWidth)/8);
+	mode := p.popt.mode;
+	Nplanes := 4;
+	if (map2 != nil)
+		Nplanes += 2;
+	Contone := array[Nplanes] of array of byte;
+	ColorDepth := array[Nplanes] of int;
+	ColorDepth[K] = mode.blackdepth;
+	for (col:=1; col<Nplanes; col++)
+		ColorDepth[col] = mode.coldepth;
+	OutputWidth := array[Nplanes] of int;
+	fDitherParms := array[Nplanes] of DitherParms;
+	ErrBuff := array[Nplanes] of array of int;
+	ColorPlane := array[Nplanes] of array of array of array of byte;
+	MixedRes := 0;
+	BaseResX := mode.resx;
+	BaseResY := mode.resy;
+	ResBoost := BaseResX / BaseResY;
+	ResolutionX := array[Nplanes] of int;
+	ResolutionY := array[Nplanes] of int;
+	ResolutionX[K] = mode.resx*mode.blackresmult;
+	ResolutionY[K] = mode.resy*mode.blackresmult;
+	for (col=1; col<Nplanes; col++) {
+		ResolutionX[col] = mode.resx;
+		ResolutionY[col] = mode.resy;
+	}
+	NumRows := array[Nplanes] of int;
+	for (j:=0; j<Nplanes; j++) {
+		if (ResolutionX[j] != ResolutionX[K])
+			MixedRes++;
+		if (MixedRes)
+			# means res(K) !+ res(C,M,Y)
+			NumRows[j] = ResolutionX[j] / BaseResX;
+		else
+			NumRows[j]=1;
+		OutputWidth[j]= ScaledWidth * NumRows[j] * ResBoost;
+		PlaneSize:= OutputWidth[j]/8;
+		Contone[j] = array[OutputWidth[j]] of byte;
+		ColorPlane[j] = array[NumRows[j]] of array of array of  byte;
+		for (jj:=0; jj<NumRows[j]; jj++) {
+			ColorPlane[j][jj] = array[ColorDepth[j]] of array of  byte;
+			for (jjj:=0; jjj<ColorDepth[j]; jjj++) {
+				ColorPlane[j][jj][jjj] = array[PlaneSize] of byte;
+			}
+		}
+		ErrBuff[j] = array[OutputWidth[j]+2] of {* => 0};
+	}
+
+	pcl_startjob(p);
+	if (p.popt.paper.hpcode != "")
+		PCL_Page_Size(p.popt.paper.hpcode);
+	PCL_Move_CAP_H_Units(lmargin*300/BaseResX, ABSOLUTE);
+	PCL_Configure_Raster_Data4(BaseResX, BaseResY, ColorDepth);
+	PCL_Source_Raster_Width(ScaledWidth);
+	PCL_Compression_Method(Compression);
+	PCL_Start_Raster(1);
+	cmap1 := setup_color_map(display, map1, im.depth);
+	if (map2 != nil)
+		cmap2 := setup_color_map(display, map2, im.depth);
+	numerator, denominator: int;
+	if ((ScaledWidth % AdjustedInputWidth)==0) {
+		numerator = ScaledWidth / AdjustedInputWidth;
+		denominator = 1;
+	} else {
+		numerator = ScaledWidth;
+		denominator = AdjustedInputWidth;
+	}
+	rs := scaler->init(DEBUG, AdjustedInputWidth, numerator, denominator);
+	rasterno := 0;
+	col_row: array of int;
+	eof := 0;
+
+	while (!eof) {
+		col_row = <- linechan;
+		if (col_row == nil)
+			eof++;
+		scaler->rasterin(rs, col_row);
+		while ((scaled_col_row := scaler->rasterout(rs)) != nil) {
+			rasterno++;
+			fRasterOdd := rasterno & 1;
+			kcmy_row := SimpleColorMatch(cmap1, scaled_col_row);
+			if (DEBUG) {
+				dbg("Scaled Raster line:");
+				for (q:=0; q<len scaled_col_row; q++) {
+					(r, g, b) := display.cmap2rgb(scaled_col_row[q]);
+					dbg(sys->sprint("%d rgb=(%d,%d,%d) kcmy=(%d,%d,%d,%d)\n", int scaled_col_row[q],
+						r, g, b, int kcmy_row[q].k, int kcmy_row[q].c, int kcmy_row[q].m, int kcmy_row[q].y));
+				}
+				dbg("\n");
+			}
+			Contone_K := Contone[K];
+			Contone_C := Contone[C];
+			Contone_M := Contone[M];
+			Contone_Y := Contone[Y];
+			for (ii:=0; ii<len Contone[K]; ii++) {
+				kcmy := kcmy_row[ii];
+				Contone_K[ii] = kcmy.k;
+				Contone_C[ii] = kcmy.c;
+				Contone_M[ii] = kcmy.m;
+				Contone_Y[ii] = kcmy.y;
+			}
+			if (map2 != nil) {		# For lighter inks
+				kcmy_row_light := SimpleColorMatch(cmap2, scaled_col_row);
+				Contone_Clight := Contone[Clight];
+				Contone_Mlight := Contone[Mlight];
+				for (ii=0; ii<len Contone[Clight]; ii++) {
+					kcmy := kcmy_row_light[ii];
+					Contone_Clight[ii] = kcmy.c;
+					Contone_Mlight[ii] = kcmy.m;
+				}
+			}
+
+			for (i:=0; i< Nplanes; i++) {
+# Pixel multiply here!!
+				fDitherParms[i].fNumPix = OutputWidth[i];
+				fDitherParms[i].fInput = Contone[i];
+				fDitherParms[i].fErr = ErrBuff[i];
+#				fDitherParms[i].fErr++;		// serpentine (?)
+				fDitherParms[i].fSymmetricFlag = 1;
+#				if (i == K)
+#					fDitherParms[i].fFEDResPtr = fBlackFEDResPtr;
+#				else
+#					fDitherParms[i].fFEDResPtr = fColorFEDResPtr;
+				fDitherParms[i].fFEDRes = FEDarray;
+				fDitherParms[i].fRasterEvenOrOdd = fRasterOdd;
+				fDitherParms[i].fHifipe = ColorDepth[i] > 1;
+				for (j=0; j < NumRows[i]; j++) {
+					fDitherParms[i].fOutput1 = ColorPlane[i][j][0];
+					if (fDitherParms[i].fHifipe)
+						fDitherParms[i].fOutput2 = ColorPlane[i][j][1];
+#					dbg(sys->sprint("Dither for Row %d ColorPlane[%d][%d]\n", rasterno, i, j));   
+					Dither(fDitherParms[i]);
+				}
+			}
+
+			FINALPLANE: con 3;
+#			NfinalPlanes := 4;
+			for (i=0; i<=FINALPLANE; i++) {
+				cp_i := ColorPlane[i];
+				coldepth_i := ColorDepth[i];
+				finalrow := NumRows[i]-1;
+				for (j=0; j<=finalrow; j++) {
+					cp_i_j := cp_i[j];
+					for (k:=0; k<coldepth_i; k++) {
+						if (i == FINALPLANE && j == finalrow && k == coldepth_i-1)
+							PCL_Transfer_Raster_Row(cp_i_j[k]);
+						else 
+							PCL_Transfer_Raster_Plane(cp_i_j[k]);
+						if (cancelled.value) {
+							PCL_Reset();
+							outclose();
+							killgrp(grppid);
+							return -1;
+						}
+					}
+				}
+			}
+		}
+	}
+	PCL_End_Raster();
+	PCL_Reset();
+	outclose();
+	killgrp(grppid);
+	if (cancelled.value)
+		return -1;
+#sys->print("dlen %d, clen %d overruns %d\n", dlen, clen, overruns);
+	return 0;
+}
+
+
+# Send text to printer
+
+sendtextfd(p: ref Print->Printer, pfd, tfd: ref Sys->FD, pointsize: real, proportional: int, wrap: int): int
+{
+	outopen(pfd);
+	pcl_startjob(p);
+	if (wrap) PCL_End_of_Line_Wrap(0);
+	LATIN1: con "0N";
+	PCL_Font_Symbol_Set(LATIN1);
+	if (proportional) PCL_Font_Spacing(1);
+	if (pointsize > 0.0) {
+		PCL_Font_Height(pointsize);
+		pitch := 10.0*12.0/pointsize;
+		PCL_Font_Pitch(pitch);
+		spacing := int (6.0*12.0/pointsize);
+		PCL_Line_Spacing(spacing);
+		dbg(sys->sprint("Text: pointsize %f pitch %f spacing %d\n", pointsize, pitch, spacing));
+	}
+	PCL_Line_Termination(3);
+	inbuf := bufio->fopen(tfd, Bufio->OREAD);
+	while ((line := inbuf.gets('\n')) != nil) {
+		ob := array of byte line;
+		outwrite(ob, len ob);
+	}
+	PCL_Reset();
+	outclose();
+	return 0;
+}
+
+
+
+# Common PCL start
+
+pcl_startjob(p: ref Printer)
+{
+	PCL_Reset();
+	if (p.popt.duplex) {
+		esc("%-12345X@PJL DEFAULT DUPLEX=ON\n");
+		esc("%-12345X");
+	}
+	if (p.popt.paper.hpcode != "")
+		PCL_Page_Size(p.popt.paper.hpcode);
+	PCL_Orientation(p.popt.orientation);
+	PCL_Duplex(p.popt.duplex);
+}
+
+
+# Spawned to return  sequence of rotated image rows
+
+rotate(im: ref Draw->Image, linechan: chan of array of int, adjwidth: int)
+{
+	xmin := im.r.min.x;	
+	xmax := im.r.max.x;
+	InputWidth := xmax - xmin;
+	rawchan := chan of array of int;
+	spawn row_by_row(im, rawchan, InputWidth);
+	r_image := array[InputWidth] of {* => array [adjwidth] of {* => 0}};
+	r_row := 0;
+	while ((col_row := <- rawchan) != nil) {
+		endy := len col_row - 1;
+		for (i:=0; i<len col_row; i++)
+			r_image[endy - i][r_row] = col_row[i];
+		r_row++;
+	}
+	for (i:=0; i<len r_image; i++)
+		linechan <-= r_image[i];
+	linechan <-= nil;
+}
+
+
+# Spawned to return sequence of image rows
+
+row_by_row(im: ref Draw->Image, linechan: chan of array of int, adjwidth: int)
+{
+	xmin := im.r.min.x;	
+	ymin := im.r.min.y;	
+	xmax := im.r.max.x;
+	ymax := im.r.max.y;
+	InputWidth := xmax - xmin;
+	bpp := im.depth;
+	ld := ldepth(im.depth);
+	bytesperline := (InputWidth*bpp+7)/8;	
+	rdata := array[bytesperline+10] of byte;
+	pad0 := array [7] of { * => 0};
+	for (y:=ymin; y<ymax; y++) {
+		col_row := array[adjwidth] of int;
+		rect := Rect((xmin, y), (xmax, y+1));
+		np := im.readpixels(rect, rdata);
+		if (np < 0)
+			fatal("Error reading image\n");
+		dbg(sys->sprint("Input Raster line %d: np=%d\n  ", y, np));
+		ind := 0;
+		mask := MASK[ld];
+		shift := SHIFT[ld];
+		col_row[adjwidth-7:] = pad0;	# Pad to adjusted width with white
+		data := rdata[ind];
+		for (q:=0; q<InputWidth; q++) {
+			col := int ((data  >> shift) & mask);
+			shift -= bpp;
+			if (shift < 0) {
+				shift = SHIFT[ld];
+				ind++;
+				data = rdata[ind];
+			}
+			col_row[q] = col;
+		}
+		linechan <-= col_row;
+	}
+	linechan <-= nil;
+}
+
+
+# PCL output routines
+
+
+PCL_Reset()
+{	
+	esc("E");
+}
+
+
+PCL_Orientation(value: int)
+{
+	esc(sys->sprint("&l%dO", value));
+}
+
+PCL_Duplex(value: int)
+{
+	esc(sys->sprint("&l%dS", value));
+}
+
+
+PCL_Left_Margin(value: int)
+{
+	esc(sys->sprint("&a%dL", value));
+}
+
+PCL_Page_Size(value: string)
+{
+	esc(sys->sprint("&l%sA", value));
+}
+
+
+PCL_End_of_Line_Wrap(value: int)
+{
+	esc(sys->sprint("&s%dC", value));
+}
+
+PCL_Line_Termination(value: int)
+{
+	esc(sys->sprint("&k%dG", value));
+}
+
+
+PCL_Font_Symbol_Set(value: string)
+{
+	esc(sys->sprint("(%s", value));
+}
+
+
+PCL_Font_Pitch(value: real)
+{
+	esc(sys->sprint("(s%2.2fH", value));
+}
+
+PCL_Font_Spacing(value: int)
+{
+	esc(sys->sprint("(s%dP", value));
+}
+
+PCL_Font_Height(value: real)
+{
+	esc(sys->sprint("(s%2.2fV", value));
+}
+
+PCL_Line_Spacing(value: int)
+{
+	esc(sys->sprint("&l%dD", value));
+}
+
+
+
+PCL_Start_Raster(current: int)
+{	
+	flag := 0;
+	if (current) flag = 1;
+	esc(sys->sprint("*r%dA", flag));
+}
+
+
+
+PCL_End_Raster()
+{	
+	esc("*rC");
+}
+
+
+PCL_Raster_Resolution(ppi: int)
+{	
+	esc(sys->sprint("*t%dR", ppi));
+}
+
+
+PCL_Source_Raster_Width(pixels: int)
+{	
+	esc(sys->sprint("*r%dS", pixels));
+}
+
+
+PCL_Simple_Color(palette: int)
+{	
+	esc(sys->sprint("*r%dU", palette));
+}
+
+PCL_Compression_Method(ctype: int)
+{
+	esc(sys->sprint("*b%dM", ctype));
+
+}
+
+
+PCL_Move_CAP_V_Rows(pos: int, absolute: int)
+{
+	plus := "";
+	if (!absolute && pos > 0) plus = "+";
+	esc(sys->sprint("&a%s%dR", plus, pos));
+}
+
+PCL_Move_CAP_H_Cols(pos: int, absolute: int)
+{
+	plus := "";
+	if (!absolute && pos > 0) plus = "+";
+	esc(sys->sprint("&a%s%dC", plus, pos));
+}
+
+# These Units are 1/300 of an inch.
+
+PCL_Move_CAP_H_Units(pos: int, absolute: int)
+{
+	plus := "";
+	if (!absolute && pos > 0) plus = "+";
+	esc(sys->sprint("*p%s%dX", plus, pos));
+}
+
+
+
+PCL_Move_CAP_V_Units(pos: int, absolute: int)
+{
+	plus := "";
+	if (!absolute && pos > 0) plus = "+";
+	esc(sys->sprint("*p%s%dY", plus, pos));
+}
+
+
+
+PCL_Configure_Raster_Data4(hres, vres: int, ColorDepth: array of int)
+{	
+	ncomponents := 4;
+	msg := array[ncomponents*6 + 2] of byte;
+	i := 0;
+	msg[i++] = byte 2;	# Format
+	msg[i++] = byte ncomponents;	# KCMY
+	for (c:=0; c<ncomponents; c++) {
+		msg[i++] = byte (hres/256);
+		msg[i++] = byte (hres%256);
+		msg[i++] = byte (vres/256);
+		msg[i++] = byte (vres%256);
+
+		depth := 1 << ColorDepth[c];
+		msg[i++] = byte (depth/256);
+		msg[i++] = byte (depth%256);
+	}
+	if (DEBUG) {
+		dbg("CRD: ");
+		for (ii:=0; ii<len msg; ii++) dbg(sys->sprint("%d(%x) ", int msg[ii], int msg[ii]));
+		dbg("\n");
+	}
+	esc(sys->sprint("*g%dW", len msg));
+	outwrite(msg, len msg);
+}
+
+dlen := 0;
+clen := 0;
+overruns := 0;
+PCL_Transfer_Raster_Plane(data: array of byte)
+{	
+	if (DEBUG) {
+		dbg("Transfer_Raster_Plane:");
+		for (i:=0; i<len data; i++) dbg(sys->sprint(" %x", int data[i]));
+		dbg("\n");
+	}
+	if (Compression) {
+d := len data;
+dlen += d;
+		data = compress(data);
+c := len data;
+clen += c;
+if (c > d)
+	overruns += c-d;
+		if (DEBUG) {
+			dbg("Compressed Transfer_Raster_Plane:");
+			for (i:=0; i<len data; i++) dbg(sys->sprint(" %x", int data[i]));
+			dbg("\n");
+		}
+	}
+	esc(sys->sprint("*b%dV", len data));
+	outwrite(data, len data);
+}
+
+
+PCL_Transfer_Raster_Row(data: array of byte)
+{
+	if (DEBUG) {
+		dbg("Transfer_Raster_Row:");
+		for (i:=0; i<len data; i++) dbg(sys->sprint(" %x", int data[i]));
+		dbg("\n");
+	}
+	if (Compression) {
+		data = compress(data);	
+		if (DEBUG) {
+			dbg("Compressed Transfer_Raster_Row:");
+			for (i:=0; i<len data; i++) dbg(sys->sprint(" %x", int data[i]));
+			dbg("\n");
+		}
+	}
+	esc(sys->sprint("*b%dW", len data));
+	outwrite(data, len data);
+}
+
+
+outopen(fd: ref Sys->FD)
+{
+	outbuf = bufio->fopen(fd, Bufio->OWRITE);
+	if (outbuf == nil) sys->fprint(stderr, "Failed to open output fd: %r\n");
+}
+
+outclose()
+{
+	outbuf.close();
+}
+
+
+# Write to output using buffered io
+
+outwrite(data: array of byte, length: int)
+{
+	outbuf.write(data, length);
+}
+
+
+# Send escape code to printer
+
+esc(s: string) 
+{
+	os := sys->sprint("%c%s", ESC, s);
+	ob := array of byte os;
+	outwrite(ob, len ob);
+}
+
+
+# Read all the maps
+readmaps(p: ref Printer): (array of KCMY, array of KCMY)
+{
+
+	mapfile := p.ptype.hpmapfile;
+	mapf1 := Pdriver->DATAPREFIX + mapfile + ".map";
+	m1 := read_map(mapf1);
+	if (m1 == nil) fatal("Failed to read map file");
+	mapf2 := Pdriver->DATAPREFIX + mapfile + "_2.map";
+	m2 := read_map(mapf2);
+	return (m1, m2);
+}
+
+
+# Read a map file
+
+read_map(mapfile: string) : array of KCMY
+{
+	mf := bufio->open(mapfile, bufio->OREAD);
+	if (mf == nil) return nil;
+	CUBESIZE: con 9*9*9;
+	marray := array[CUBESIZE] of KCMY;
+	i := 0;
+	while (i <CUBESIZE && (lstr := bufio->mf.gets('\n')) != nil) {
+		(n, toks) := sys->tokenize(lstr, " \t");
+		if (n >= 4) {
+			marray[i].k = byte int hd toks;
+			toks = tl toks;
+			marray[i].c = byte int hd toks;
+			toks = tl toks;
+			marray[i].m = byte int hd toks;
+			toks = tl toks;
+			marray[i].y = byte int hd toks;
+			i++;
+		}
+	}
+	return marray;
+}
+
+
+
+
+# Big interpolation routine
+
+# static data
+prev := RGB (byte 255, byte 255, byte 255);
+result: KCMY;
+offset := array[] of { 0, 1, 9, 10, 81, 82, 90, 91 };
+
+
+Interpolate(map: array of KCMY, start: int, rgb: RGB, firstpixel: int): KCMY
+{
+	cyan := array[8] of int;
+	magenta := array[8] of int;
+	yellow := array[8] of int;
+	black := array[8] of int;
+
+	if (firstpixel || prev.r != rgb.r || prev.g != rgb.g || prev.b != rgb.b) {
+		prev = rgb;
+		for (j:=0; j<8; j++) {
+			ioff := start+offset[j];
+			cyan[j] = int map[ioff].c;
+			magenta[j] = int map[ioff].m;
+			yellow[j] = int map[ioff].y;
+			black[j] = int map[ioff].k;
+		}
+
+		diff_red := int rgb.r & 16r1f;
+		diff_green := int rgb.g & 16r1f;
+		diff_blue := int rgb.b & 16r1f;
+
+
+        result.c   = byte (((cyan[0] + ( ( (cyan[4] - cyan[0] ) * diff_red) >> 5)) + ( ( ((cyan[2] + ( ( (cyan[6] - cyan[2] ) * diff_red) >> 5)) -(cyan[0] + ( ( (cyan[4] - cyan[0] ) * diff_red) >> 5)) ) * diff_green) >> 5)) + ( ( (((cyan[1] + ( ( (cyan[5] - cyan[1] ) * diff_red) >> 5)) + ( ( ((cyan[3] + ( ( (cyan[7] - cyan[3] ) * diff_red) >> 5)) -(cyan[1] + ( ( (cyan[5] - cyan[1] ) * diff_red) >> 5)) ) * diff_green) >> 5)) -((cyan[0] + ( ( (cyan[4] - cyan[0] ) * diff_red) >> 5)) + ( ( ((cyan[2] + ( ( (cyan[6] - cyan[2] ) * diff_red) >> 5)) -(cyan[0] + ( ( (cyan[4] - cyan[0] ) * diff_red) >> 5)) ) * diff_green) >> 5)) ) * diff_blue) >> 5));
+
+        result.m = byte (((magenta[0] + ( ( (magenta[4] - magenta[0] ) * diff_red) >> 5)) + ( ( ((magenta[2] + ( ( (magenta[6] - magenta[2] ) * diff_red) >> 5)) -(magenta[0] + ( ( (magenta[4] - magenta[0] ) * diff_red) >> 5)) ) * diff_green) >> 5)) + ( ( (((magenta[1] + ( ( (magenta[5] - magenta[1] ) * diff_red) >> 5)) + ( ( ((magenta[3] + ( ( (magenta[7] - magenta[3] ) * diff_red) >> 5)) -(magenta[1] + ( ( (magenta[5] - magenta[1] ) * diff_red) >> 5)) ) * diff_green) >> 5)) -((magenta[0] + ( ( (magenta[4] - magenta[0] ) * diff_red) >> 5)) + ( ( ((magenta[2] + ( ( (magenta[6] - magenta[2] ) * diff_red) >> 5)) -(magenta[0] + ( ( (magenta[4] - magenta[0] ) * diff_red) >> 5)) ) * diff_green) >> 5)) ) * diff_blue) >> 5));
+
+        result.y = byte (((yellow[0] + ( ( (yellow[4] - yellow[0] ) * diff_red) >> 5)) + ( ( ((yellow[2] + ( ( (yellow[6] - yellow[2] ) * diff_red) >> 5)) -(yellow[0] + ( ( (yellow[4] - yellow[0] ) * diff_red) >> 5)) ) * diff_green) >> 5)) + ( ( (((yellow[1] + ( ( (yellow[5] - yellow[1] ) * diff_red) >> 5)) + ( ( ((yellow[3] + ( ( (yellow[7] - yellow[3] ) * diff_red) >> 5)) -(yellow[1] + ( ( (yellow[5] - yellow[1] ) * diff_red) >> 5)) ) * diff_green) >> 5)) -((yellow[0] + ( ( (yellow[4] - yellow[0] ) * diff_red) >> 5)) + ( ( ((yellow[2] + ( ( (yellow[6] - yellow[2] ) * diff_red) >> 5)) -(yellow[0] + ( ( (yellow[4] - yellow[0] ) * diff_red) >> 5)) ) * diff_green) >> 5)) ) * diff_blue) >> 5));
+
+        result.k  = byte (((black[0] + ( ( (black[4] - black[0] ) * diff_red) >> 5)) + ( ( ((black[2] + ( ( (black[6] - black[2] ) * diff_red) >> 5)) -(black[0] + ( ( (black[4] - black[0] ) * diff_red) >> 5)) ) * diff_green) >> 5)) + ( ( (((black[1] + ( ( (black[5] - black[1] ) * diff_red) >> 5)) + ( ( ((black[3] + ( ( (black[7] - black[3] ) * diff_red) >> 5)) -(black[1] + ( ( (black[5] - black[1] ) * diff_red) >> 5)) ) * diff_green) >> 5)) -((black[0] + ( ( (black[4] - black[0] ) * diff_red) >> 5)) + ( ( ((black[2] + ( ( (black[6] - black[2] ) * diff_red) >> 5)) -(black[0] + ( ( (black[4] - black[0] ) * diff_red) >> 5)) ) * diff_green) >> 5)) ) * diff_blue) >> 5));
+
+	}
+	return result;
+}
+
+# Colour RGB to KCMY convertor
+
+ColorMatch(map: array of KCMY, row: array of RGB): array of KCMY
+{
+	kcmy := array[len row] of KCMY;
+	first := 1;
+	for (i:=0; i<len row; i++) {
+		r := int row[i].r;
+		g := int row[i].g;
+		b := int row[i].b;
+		start := ((r & 16re0) << 1) + ((r & 16re0) >> 1) + (r >> 5) +
+				((g & 16re0) >> 2) + (g >> 5) + (b >> 5);
+		kcmy[i] =  Interpolate(map, start, row[i],  first);
+#		dbg(sys->sprint("+++ for (%d,%d,%d) Interpolate returned (%d,%d,%d,%d)\n", r, g, b, int kcmy[i].k, int kcmy[i].c, int kcmy[i].m, int kcmy[i].y));
+		first = 0;
+	}
+	return kcmy;
+}
+
+
+# Simple version of above to lookup precalculated values
+
+SimpleColorMatch(cmap: array of KCMY, colrow: array of int): array of KCMY
+{
+	ncolrow := len colrow;
+	kcmy_row := array[ncolrow] of KCMY;
+	for (i:=0; i<ncolrow; i++) 
+		kcmy_row[i] = cmap[colrow[i]];
+	return kcmy_row;
+}
+
+
+ldepth(d: int): int
+{
+	if(d & (d-1) || d >= 16)
+		return 4;
+	for(i := 0; i < 3; i++)
+		if(d <= (1<<i))
+			break;
+	return i;
+}
+
+
+# Set up color map once and for all
+
+setup_color_map(display: ref Display, map: array of KCMY, depth: int): array of KCMY
+{
+	gsfactor := GSFACTOR[ldepth(depth)];
+	bpp := depth;
+	max := 1 << bpp;
+	rgb_row := array[max] of RGB;
+	for (i:=0; i<max; i++) {
+		if (depth >= 8) {
+			(r, g, b) := display.cmap2rgb(i);
+			rgb_row[i] = RGB (byte r, byte g, byte b);
+		} else {	# BW or Greyscale
+			grey := byte (255-int (real i * gsfactor));
+			rgb_row[i] = RGB (grey, grey, grey);
+		}
+	}
+	kcmy_row := ColorMatch(map, rgb_row);
+
+	return kcmy_row;
+}
+
+
+
+# Dithering
+
+tmpShortStore: int;
+diffusionErrorPtr := 1;	# for serpentine??
+errPtr: array of int;
+rasterByte1 := 0;
+rasterByte2 := 0;
+
+rand8 := array [8] of int;
+pad8 := array [8] of {* => 0};
+
+Dither(ditherParms: DitherParms)
+{
+	errPtr = ditherParms.fErr;
+	numLoop := ditherParms.fNumPix;
+	inputPtr := 0;    
+	fedResTbl := ditherParms.fFEDRes;
+	symmetricFlag := ditherParms.fSymmetricFlag;
+	doNext8Pixels : int;
+	hifipe := ditherParms.fHifipe;        
+	outputPtr1 := 0;
+	outputPtr2 := 0;
+	diffusionErrorPtr = 1;
+	fInput := ditherParms.fInput;
+
+	if(ditherParms.fRasterEvenOrOdd) {
+		tmpShortStore = errPtr[diffusionErrorPtr];
+		errPtr[diffusionErrorPtr]  = 0;
+
+		for (pixelCount := numLoop + 8; (pixelCount -= 8) > 0; ) {
+			if (pixelCount > 16) {
+				# if next 16 pixels are white, skip 8
+#				doNext8Pixels = Forward16PixelsNonWhite(fInput, inputPtr);
+				doNext8Pixels = 0;
+				lim := inputPtr + 16;
+				for (i := inputPtr; i < lim; i++) {
+					if (fInput[i] != byte 0) {
+						doNext8Pixels = 1;
+						break;
+					}
+				}
+			} else {
+				doNext8Pixels = 1;
+			}
+			if (doNext8Pixels) {
+FORWARD_FED8(fInput, inputPtr, fedResTbl);
+inputPtr += 8;
+#				HPRand8();
+#				FORWARD_FED(rand8[0], 16r80, fInput[inputPtr++], fedResTbl);
+#				FORWARD_FED(rand8[1], 16r40, fInput[inputPtr++], fedResTbl);
+#				FORWARD_FED(rand8[2], 16r20, fInput[inputPtr++], fedResTbl);
+#				FORWARD_FED(rand8[3], 16r10, fInput[inputPtr++], fedResTbl);
+#				FORWARD_FED(rand8[4], 16r08, fInput[inputPtr++], fedResTbl);
+#				FORWARD_FED(rand8[5], 16r04, fInput[inputPtr++], fedResTbl);
+#				FORWARD_FED(rand8[6], 16r02, fInput[inputPtr++], fedResTbl);
+#				FORWARD_FED(rand8[7], 16r01, fInput[inputPtr++], fedResTbl);
+
+				ditherParms.fOutput1[outputPtr1++] = byte rasterByte1;   
+				rasterByte1 = 0; 
+
+				if (hifipe) {      
+					ditherParms.fOutput2[outputPtr2++] = byte rasterByte2;
+					rasterByte2 = 0;  
+				}
+			} else {
+				 # Do white space skipping
+				inputPtr += 8;
+				ditherParms.fOutput1[outputPtr1++] = byte 0;
+				if (hifipe) {      
+	 				ditherParms.fOutput2[outputPtr2++] = byte 0;
+				}
+				errPtr[diffusionErrorPtr:] = pad8;
+				diffusionErrorPtr += 8;
+		
+				rasterByte1 = 0;
+				rasterByte2 = 0;
+				tmpShortStore = 0;
+			}
+		} # for pixelCount
+	} else {
+		rasterByte1 = 0;
+		rasterByte2 = 0;
+		inputPtr  += ( numLoop-1 );
+		outputPtr1 += ( numLoop/8 - 1 ); 
+		outputPtr2 += ( numLoop/8 - 1 );
+		diffusionErrorPtr += ( numLoop-1 ); 
+
+		tmpShortStore = errPtr[diffusionErrorPtr];  
+		errPtr[diffusionErrorPtr] = 0;
+
+        	for (pixelCount := numLoop + 8; (pixelCount -= 8) > 0; ) {
+			if (pixelCount > 16) {
+				# if next 16 pixels are white, skip 8
+#				doNext8Pixels = Backward16PixelsNonWhite(fInput, inputPtr);
+				doNext8Pixels = 0;
+				lim := inputPtr - 16;
+				for (i := inputPtr; i > lim; i--) {
+					if (fInput[i] != byte 0) {
+						doNext8Pixels = 1;
+						break;
+					}
+				}
+			} else {
+				doNext8Pixels = HPTRUE;
+			}
+
+			if (doNext8Pixels) {
+				BACKWARD_FED8(fInput, inputPtr, fedResTbl);
+				inputPtr -= 8;
+#				HPRand8();
+#				BACKWARD_FED(rand8[0], 16r01, fInput[inputPtr--], fedResTbl);
+#				BACKWARD_FED(rand8[1], 16r02, fInput[inputPtr--], fedResTbl);
+#				BACKWARD_FED(rand8[2], 16r04, fInput[inputPtr--], fedResTbl);
+#				BACKWARD_FED(rand8[3], 16r08, fInput[inputPtr--], fedResTbl);
+#				BACKWARD_FED(rand8[4], 16r10, fInput[inputPtr--], fedResTbl);
+#				BACKWARD_FED(rand8[5], 16r20, fInput[inputPtr--], fedResTbl);
+#				BACKWARD_FED(rand8[6], 16r40, fInput[inputPtr--], fedResTbl);
+#				BACKWARD_FED(rand8[7], 16r80, fInput[inputPtr--], fedResTbl);
+
+				ditherParms.fOutput1[outputPtr1-- ]= byte rasterByte1;  
+				rasterByte1 = 0; 
+
+				if (hifipe) {
+					ditherParms.fOutput2[outputPtr2--] = byte rasterByte2;
+					rasterByte2 = 0;
+				}
+			} else {
+				# Do white space skipping
+				inputPtr -= 8;
+				ditherParms.fOutput1[outputPtr1--] = byte 0;
+				if (hifipe) {
+					ditherParms.fOutput2[outputPtr2--] = byte 0;
+				}
+				diffusionErrorPtr -= 8;
+  				errPtr[diffusionErrorPtr:] = pad8;
+
+                		rasterByte1 = 0;
+				rasterByte2 = 0;
+				tmpShortStore = 0;
+			}
+		}
+	}
+}
+
+
+
+# Take a step back
+
+Backward16PixelsNonWhite(ba: array of byte, inputPtr: int): int
+{
+	lim := inputPtr - 16;
+	for (i := inputPtr; i > lim; i--) {
+		if (ba[i] != byte 0)
+			return TRUE;
+	}
+	return FALSE;
+}
+
+# Take a step forward
+
+Forward16PixelsNonWhite(ba: array of byte, inputPtr: int): int
+{
+	lim := inputPtr + 16;
+	for (i := inputPtr; i < lim; i++) {
+		if (ba[i] != byte 0)
+			return TRUE;
+	}
+	return FALSE;
+}
+
+FORWARD_FED8(input: array of byte, ix: int, fedResTbl: array of int)
+{
+	HPRand8();
+	randix := 0;
+
+	for (bitMask := 16r80; bitMask; bitMask >>= 1) {
+		tone := int input[ix++];
+		fedResPtr := tone << 2;
+		level := fedResTbl[fedResPtr];
+		if (tone != 0) {
+			tone = ( tmpShortStore + int fedResTbl[fedResPtr+1] );
+			if (tone >= rand8[randix++]) {
+				tone -= 255;
+				level++;
+			}
+			case (level) {
+			0=>
+				break;
+			1=>
+				rasterByte1 |= bitMask;
+				break;
+			2=>
+				rasterByte2 |= bitMask;
+				break;
+			3=>
+				rasterByte2 |= bitMask; rasterByte1 |= bitMask;
+				break;
+			4=>
+				break;
+			5=>
+				rasterByte1 |= bitMask;
+				break;
+			6=>
+				rasterByte2 |= bitMask;
+				break;
+			7=>
+				rasterByte2 |= bitMask; rasterByte1 |= bitMask;
+				break;
+			}
+		} else {
+			tone = tmpShortStore;
+		}
+		halftone := tone >> 1;
+		errPtr[diffusionErrorPtr++] = halftone;
+		tmpShortStore = errPtr[diffusionErrorPtr] + (tone - halftone);
+	}
+}
+
+#FORWARD_FED(thresholdValue: int, bitMask: int, toneb: byte, fedResTbl : array of int)
+#{
+#	tone := int toneb;
+#	fedResPtr := (tone << 2);
+#	level := fedResTbl[fedResPtr];
+#	if (tone != 0) {
+#		tone = ( tmpShortStore + int fedResTbl[fedResPtr+1] );
+#		if (tone >= thresholdValue) {
+#			tone -= 255;
+#			level++;
+#		}
+#		case (level) {
+#		0=>
+#			break;
+#		1=>
+#			rasterByte1 |= bitMask;
+#			break;
+#		2=>
+#			rasterByte2 |= bitMask;
+#			break;
+#		3=>
+#			rasterByte2 |= bitMask; rasterByte1 |= bitMask;
+#			break;
+#		4=>
+#			break;
+#		5=>
+#			rasterByte1 |= bitMask;
+#			break;
+#		6=>
+#			rasterByte2 |= bitMask;
+#			break;
+#		7=>
+#			rasterByte2 |= bitMask; rasterByte1 |= bitMask;
+#			break;
+#		}
+#	} else {
+#		tone = tmpShortStore;
+#	}
+#	halftone := tone >> 1;
+#	errPtr[diffusionErrorPtr++] = halftone;
+#	tmpShortStore = errPtr[diffusionErrorPtr] + (tone - halftone);
+##	dbg(sys->sprint("FORWARD_FED: thresh %d bitMask %x toneb %d => rasterbytes %d,%d,%d\n", thresholdValue, bitMask, int toneb, rasterByte1, rasterByte2));
+#}
+
+BACKWARD_FED8(input: array of byte, ix: int, fedResTbl: array of int)
+{
+	HPRand8();
+	randix := 0;
+
+	for (bitMask := 16r01; bitMask <16r100; bitMask <<= 1) {
+		tone := int input[ix--];
+		fedResPtr := (tone << 2);
+		level := fedResTbl[fedResPtr];
+		if (tone != 0) {
+			tone = ( tmpShortStore + int fedResTbl[fedResPtr+1] );
+			if (tone >= rand8[randix++]) {
+				tone -= 255;
+				level++;
+			}
+			case (level) {
+			0=>
+				break;
+			1=>
+				rasterByte1 |= bitMask;
+				break;
+			2=>
+				rasterByte2 |= bitMask;
+				break;
+			3=>
+				rasterByte2 |= bitMask; rasterByte1 |= bitMask;
+				break;
+			4=>
+				break;
+			5=>
+				rasterByte1 |= bitMask;
+				break;
+			6=>
+				rasterByte2 |= bitMask;
+				break;
+			7=>
+				rasterByte2 |= bitMask; rasterByte1 |= bitMask;
+				break;
+			}
+		} else {
+			tone = tmpShortStore;
+		 }
+		halftone := tone >> 1;
+		errPtr[diffusionErrorPtr--] = halftone;
+		tmpShortStore = errPtr[diffusionErrorPtr] + (tone - halftone);
+	}
+}
+
+
+#BACKWARD_FED(thresholdValue: int, bitMask: int, toneb: byte, fedResTbl : array of int)
+#{
+#	tone := int toneb;
+#	fedResPtr := (tone << 2);
+#	level := fedResTbl[fedResPtr];
+#	if (tone != 0) {
+#		tone = ( tmpShortStore + int fedResTbl[fedResPtr+1] );
+#		if (tone >= thresholdValue) {
+#			tone -= 255;
+#			level++;
+#		}
+#		case (level) {
+#		0=>
+#			break;
+#		1=>
+#			rasterByte1 |= bitMask;
+#			break;
+#		2=>
+#			rasterByte2 |= bitMask;
+#			break;
+#		3=>
+#			rasterByte2 |= bitMask; rasterByte1 |= bitMask;
+#			break;
+#		4=>
+#			break;
+#		5=>
+#			rasterByte1 |= bitMask;
+#			break;
+#		6=>
+#			rasterByte2 |= bitMask;
+#			break;
+#		7=>
+#			rasterByte2 |= bitMask; rasterByte1 |= bitMask;
+#			break;
+#		}
+#	} else {
+#		tone = tmpShortStore;
+#	 }
+#	halftone := tone >> 1;
+#	errPtr[diffusionErrorPtr--] = halftone;
+#	tmpShortStore = errPtr[diffusionErrorPtr] + (tone - halftone);
+##	dbg(sys->sprint("BACWARD_FED: thresh %d bitMask %x toneb %d => rasterbytes %d,%d,%d\n", thresholdValue, bitMask, int toneb, rasterByte1, rasterByte2));
+#}
+
+
+# Pixel replication
+
+pixrep(in: array of RGB): array of RGB
+{
+	out := array[2*len in] of RGB;
+	for (i:=0; i<len in; i++) {
+		out[i*2] = in[i];
+		out[i*2+1] = in[i];
+	}
+	return out;
+}
+
+
+
+
+
+
+# Random numbers
+
+IM: con 139968;
+IA: con  3877;
+IC: con 29573;
+
+last := 42;
+
+# Use a really simple and quick random number generator
+
+HPRand(): int
+{
+	return (74 * (last = (last* IA + IC) % IM) / IM ) + 5;
+}
+
+HPRand8()
+{
+	for (i:= 0; i < 8; i++)
+		rand8[i] = (74 * (last = (last* IA + IC) % IM) / IM ) + 5;
+}
+
+# Compression
+
+compress(rawdata: array of byte): array of byte
+{
+	nraw := len rawdata;
+	comp := array [2*nraw] of byte;	# worst case
+	ncomp := 0;
+	for (i:=0; i<nraw;) {
+		rpt := 0;
+		val := rawdata[i++];
+		while (i<nraw && rpt < 255 && rawdata[i] == val) {
+			rpt++;
+			i++;
+		}
+		comp[ncomp++] = byte rpt;
+		comp[ncomp++] = val;
+	}
+	return comp[0:ncomp];
+}
+
+
+
+# Print error message and exit
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "%s\n", s);
+	exit;
+}
+
+killgrp(pid: int)
+{
+	sys->fprint(sys->open("/prog/" + string pid +"/ctl", Sys->OWRITE), "killgrp");
+}
+
+
+dbg(s: string)
+{
+	if (DEBUG) sys->fprint(stderr, "%s", s);
+}
+
+
+
+# Uninteresting constants
+
+FEDarray := array[1024] of
+{
+   0 ,    0 ,    0 ,    0 ,
+   0 ,    0 ,    0 ,    0 ,
+   0 ,    2 ,    0 ,    0 ,
+   0 ,    3 ,    0 ,    0 ,
+   0 ,    4 ,    0 ,    0 ,
+   0 ,    5 ,    0 ,    0 ,
+   0 ,    6 ,    0 ,    0 ,
+   0 ,    7 ,    0 ,    0 ,
+   0 ,    8 ,    0 ,    0 ,
+   0 ,    9 ,    0 ,    0 ,
+   0 ,   10 ,    0 ,    0 ,
+   0 ,   11 ,    0 ,    0 ,
+   0 ,   12 ,    0 ,    0 ,
+   0 ,   13 ,    0 ,    0 ,
+   0 ,   14 ,    0 ,    0 ,
+   0 ,   15 ,    0 ,    0 ,
+   0 ,   16 ,    0 ,    0 ,
+   0 ,   17 ,    0 ,    0 ,
+   0 ,   18 ,    0 ,    0 ,
+   0 ,   19 ,    0 ,    0 ,
+   0 ,   20 ,    0 ,    0 ,
+   0 ,   21 ,    0 ,    0 ,
+   0 ,   22 ,    0 ,    0 ,
+   0 ,   23 ,    0 ,    0 ,
+   0 ,   24 ,    0 ,    0 ,
+   0 ,   25 ,    0 ,    0 ,
+   0 ,   26 ,    0 ,    0 ,
+   0 ,   27 ,    0 ,    0 ,
+   0 ,   28 ,    0 ,    0 ,
+   0 ,   29 ,    0 ,    0 ,
+   0 ,   30 ,    0 ,    0 ,
+   0 ,   31 ,    0 ,    0 ,
+   0 ,   32 ,    0 ,    0 ,
+   0 ,   33 ,    0 ,    0 ,
+   0 ,   34 ,    0 ,    0 ,
+   0 ,   35 ,    0 ,    0 ,
+   0 ,   36 ,    0 ,    0 ,
+   0 ,   37 ,    0 ,    0 ,
+   0 ,   38 ,    0 ,    0 ,
+   0 ,   39 ,    0 ,    0 ,
+   0 ,   40 ,    0 ,    0 ,
+   0 ,   41 ,    0 ,    0 ,
+   0 ,   42 ,    0 ,    0 ,
+   0 ,   43 ,    0 ,    0 ,
+   0 ,   44 ,    0 ,    0 ,
+   0 ,   45 ,    0 ,    0 ,
+   0 ,   46 ,    0 ,    0 ,
+   0 ,   47 ,    0 ,    0 ,
+   0 ,   48 ,    0 ,    0 ,
+   0 ,   49 ,    0 ,    0 ,
+   0 ,   50 ,    0 ,    0 ,
+   0 ,   51 ,    0 ,    0 ,
+   0 ,   52 ,    0 ,    0 ,
+   0 ,   53 ,    0 ,    0 ,
+   0 ,   54 ,    0 ,    0 ,
+   0 ,   55 ,    0 ,    0 ,
+   0 ,   56 ,    0 ,    0 ,
+   0 ,   57 ,    0 ,    0 ,
+   0 ,   58 ,    0 ,    0 ,
+   0 ,   59 ,    0 ,    0 ,
+   0 ,   60 ,    0 ,    0 ,
+   0 ,   61 ,    0 ,    0 ,
+   0 ,   62 ,    0 ,    0 ,
+   0 ,   63 ,    0 ,    0 ,
+   0 ,   64 ,    0 ,    0 ,
+   0 ,   65 ,    0 ,    0 ,
+   0 ,   66 ,    0 ,    0 ,
+   0 ,   67 ,    0 ,    0 ,
+   0 ,   68 ,    0 ,    0 ,
+   0 ,   69 ,    0 ,    0 ,
+   0 ,   70 ,    0 ,    0 ,
+   0 ,   71 ,    0 ,    0 ,
+   0 ,   72 ,    0 ,    0 ,
+   0 ,   73 ,    0 ,    0 ,
+   0 ,   74 ,    0 ,    0 ,
+   0 ,   75 ,    0 ,    0 ,
+   0 ,   76 ,    0 ,    0 ,
+   0 ,   77 ,    0 ,    0 ,
+   0 ,   78 ,    0 ,    0 ,
+   0 ,   79 ,    0 ,    0 ,
+   0 ,   80 ,    0 ,    0 ,
+   0 ,   81 ,    0 ,    0 ,
+   0 ,   82 ,    0 ,    0 ,
+   0 ,   83 ,    0 ,    0 ,
+   0 ,   84 ,    0 ,    0 ,
+   0 ,   85 ,    0 ,    0 ,
+   0 ,   86 ,    0 ,    0 ,
+   0 ,   87 ,    0 ,    0 ,
+   0 ,   88 ,    0 ,    0 ,
+   0 ,   89 ,    0 ,    0 ,
+   0 ,   90 ,    0 ,    0 ,
+   0 ,   91 ,    0 ,    0 ,
+   0 ,   92 ,    0 ,    0 ,
+   0 ,   93 ,    0 ,    0 ,
+   0 ,   94 ,    0 ,    0 ,
+   0 ,   95 ,    0 ,    0 ,
+   0 ,   96 ,    0 ,    0 ,
+   0 ,   97 ,    0 ,    0 ,
+   0 ,   98 ,    0 ,    0 ,
+   0 ,   99 ,    0 ,    0 ,
+   0 ,  100 ,    0 ,    0 ,
+   0 ,  101 ,    0 ,    0 ,
+   0 ,  102 ,    0 ,    0 ,
+   0 ,  103 ,    0 ,    0 ,
+   0 ,  104 ,    0 ,    0 ,
+   0 ,  105 ,    0 ,    0 ,
+   0 ,  106 ,    0 ,    0 ,
+   0 ,  107 ,    0 ,    0 ,
+   0 ,  108 ,    0 ,    0 ,
+   0 ,  109 ,    0 ,    0 ,
+   0 ,  110 ,    0 ,    0 ,
+   0 ,  111 ,    0 ,    0 ,
+   0 ,  112 ,    0 ,    0 ,
+   0 ,  113 ,    0 ,    0 ,
+   0 ,  114 ,    0 ,    0 ,
+   0 ,  115 ,    0 ,    0 ,
+   0 ,  116 ,    0 ,    0 ,
+   0 ,  117 ,    0 ,    0 ,
+   0 ,  118 ,    0 ,    0 ,
+   0 ,  119 ,    0 ,    0 ,
+   0 ,  120 ,    0 ,    0 ,
+   0 ,  121 ,    0 ,    0 ,
+   0 ,  122 ,    0 ,    0 ,
+   0 ,  123 ,    0 ,    0 ,
+   0 ,  124 ,    0 ,    0 ,
+   0 ,  125 ,    0 ,    0 ,
+   0 ,  126 ,    0 ,    0 ,
+   0 ,  127 ,    0 ,    0 ,
+   0 ,  128 ,    0 ,    0 ,
+   0 ,  129 ,    0 ,    0 ,
+   0 ,  130 ,    0 ,    0 ,
+   0 ,  131 ,    0 ,    0 ,
+   0 ,  132 ,    0 ,    0 ,
+   0 ,  133 ,    0 ,    0 ,
+   0 ,  134 ,    0 ,    0 ,
+   0 ,  135 ,    0 ,    0 ,
+   0 ,  136 ,    0 ,    0 ,
+   0 ,  137 ,    0 ,    0 ,
+   0 ,  138 ,    0 ,    0 ,
+   0 ,  139 ,    0 ,    0 ,
+   0 ,  140 ,    0 ,    0 ,
+   0 ,  141 ,    0 ,    0 ,
+   0 ,  142 ,    0 ,    0 ,
+   0 ,  143 ,    0 ,    0 ,
+   0 ,  144 ,    0 ,    0 ,
+   0 ,  145 ,    0 ,    0 ,
+   0 ,  146 ,    0 ,    0 ,
+   0 ,  147 ,    0 ,    0 ,
+   0 ,  148 ,    0 ,    0 ,
+   0 ,  149 ,    0 ,    0 ,
+   0 ,  150 ,    0 ,    0 ,
+   0 ,  151 ,    0 ,    0 ,
+   0 ,  152 ,    0 ,    0 ,
+   0 ,  153 ,    0 ,    0 ,
+   0 ,  154 ,    0 ,    0 ,
+   0 ,  155 ,    0 ,    0 ,
+   0 ,  156 ,    0 ,    0 ,
+   0 ,  157 ,    0 ,    0 ,
+   0 ,  158 ,    0 ,    0 ,
+   0 ,  159 ,    0 ,    0 ,
+   0 ,  160 ,    0 ,    0 ,
+   0 ,  161 ,    0 ,    0 ,
+   0 ,  162 ,    0 ,    0 ,
+   0 ,  163 ,    0 ,    0 ,
+   0 ,  164 ,    0 ,    0 ,
+   0 ,  165 ,    0 ,    0 ,
+   0 ,  166 ,    0 ,    0 ,
+   0 ,  167 ,    0 ,    0 ,
+   0 ,  168 ,    0 ,    0 ,
+   0 ,  169 ,    0 ,    0 ,
+   0 ,  170 ,    0 ,    0 ,
+   0 ,  171 ,    0 ,    0 ,
+   0 ,  172 ,    0 ,    0 ,
+   0 ,  173 ,    0 ,    0 ,
+   0 ,  174 ,    0 ,    0 ,
+   0 ,  175 ,    0 ,    0 ,
+   0 ,  176 ,    0 ,    0 ,
+   0 ,  177 ,    0 ,    0 ,
+   0 ,  178 ,    0 ,    0 ,
+   0 ,  179 ,    0 ,    0 ,
+   0 ,  180 ,    0 ,    0 ,
+   0 ,  181 ,    0 ,    0 ,
+   0 ,  182 ,    0 ,    0 ,
+   0 ,  183 ,    0 ,    0 ,
+   0 ,  184 ,    0 ,    0 ,
+   0 ,  185 ,    0 ,    0 ,
+   0 ,  186 ,    0 ,    0 ,
+   0 ,  187 ,    0 ,    0 ,
+   0 ,  188 ,    0 ,    0 ,
+   0 ,  189 ,    0 ,    0 ,
+   0 ,  190 ,    0 ,    0 ,
+   0 ,  191 ,    0 ,    0 ,
+   0 ,  192 ,    0 ,    0 ,
+   0 ,  193 ,    0 ,    0 ,
+   0 ,  194 ,    0 ,    0 ,
+   0 ,  195 ,    0 ,    0 ,
+   0 ,  196 ,    0 ,    0 ,
+   0 ,  197 ,    0 ,    0 ,
+   0 ,  198 ,    0 ,    0 ,
+   0 ,  199 ,    0 ,    0 ,
+   0 ,  200 ,    0 ,    0 ,
+   0 ,  201 ,    0 ,    0 ,
+   0 ,  202 ,    0 ,    0 ,
+   0 ,  203 ,    0 ,    0 ,
+   0 ,  204 ,    0 ,    0 ,
+   0 ,  205 ,    0 ,    0 ,
+   0 ,  206 ,    0 ,    0 ,
+   0 ,  207 ,    0 ,    0 ,
+   0 ,  208 ,    0 ,    0 ,
+   0 ,  209 ,    0 ,    0 ,
+   0 ,  210 ,    0 ,    0 ,
+   0 ,  211 ,    0 ,    0 ,
+   0 ,  212 ,    0 ,    0 ,
+   0 ,  213 ,    0 ,    0 ,
+   0 ,  214 ,    0 ,    0 ,
+   0 ,  215 ,    0 ,    0 ,
+   0 ,  216 ,    0 ,    0 ,
+   0 ,  217 ,    0 ,    0 ,
+   0 ,  218 ,    0 ,    0 ,
+   0 ,  219 ,    0 ,    0 ,
+   0 ,  220 ,    0 ,    0 ,
+   0 ,  221 ,    0 ,    0 ,
+   0 ,  222 ,    0 ,    0 ,
+   0 ,  223 ,    0 ,    0 ,
+   0 ,  224 ,    0 ,    0 ,
+   0 ,  225 ,    0 ,    0 ,
+   0 ,  226 ,    0 ,    0 ,
+   0 ,  227 ,    0 ,    0 ,
+   0 ,  228 ,    0 ,    0 ,
+   0 ,  229 ,    0 ,    0 ,
+   0 ,  230 ,    0 ,    0 ,
+   0 ,  231 ,    0 ,    0 ,
+   0 ,  232 ,    0 ,    0 ,
+   0 ,  233 ,    0 ,    0 ,
+   0 ,  234 ,    0 ,    0 ,
+   0 ,  235 ,    0 ,    0 ,
+   0 ,  236 ,    0 ,    0 ,
+   0 ,  237 ,    0 ,    0 ,
+   0 ,  238 ,    0 ,    0 ,
+   0 ,  239 ,    0 ,    0 ,
+   0 ,  240 ,    0 ,    0 ,
+   0 ,  241 ,    0 ,    0 ,
+   0 ,  242 ,    0 ,    0 ,
+   0 ,  243 ,    0 ,    0 ,
+   0 ,  244 ,    0 ,    0 ,
+   0 ,  245 ,    0 ,    0 ,
+   0 ,  246 ,    0 ,    0 ,
+   0 ,  247 ,    0 ,    0 ,
+   0 ,  248 ,    0 ,    0 ,
+   0 ,  249 ,    0 ,    0 ,
+   0 ,  250 ,    0 ,    0 ,
+   0 ,  251 ,    0 ,    0 ,
+   0 ,  252 ,    0 ,    0 ,
+   0 ,  253 ,    0 ,    0 ,
+   0 ,  254 ,    0 ,    0 ,
+   0 ,  254 ,    0 ,    0
+};
--- /dev/null
+++ b/appl/lib/print/mkfile
@@ -1,0 +1,26 @@
+
+<../../../mkconfig
+
+TARG=\
+	print.dis \
+	hp_driver.dis \
+	scaler.dis \
+
+
+MODULES=\
+	scaler.m \
+
+
+SYSMODULES= \
+	bufio.m\
+	draw.m\
+	string.m\
+	sys.m\
+	print.m \
+
+
+DISBIN=$ROOT/dis/lib/print
+
+<$ROOT/mkfiles/mkdis
+# force compilation or it's very slow
+LIMBOFLAGS= -c $LIMBOFLAGS
--- /dev/null
+++ b/appl/lib/print/print.b
@@ -1,0 +1,625 @@
+implement Print;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Font, Rect, Point, Image, Screen: import draw;
+include "bufio.m";
+	bufio: Bufio;
+include "string.m";
+	str: String;
+	
+include "print.m";
+
+MAXNAME: con 80;
+DEFMODE: con 8r664;
+
+PAPER_CONFIG: con CONFIG_PATH + "paper.cfg";
+PTYPE_CONFIG: con CONFIG_PATH + "ptype.cfg";
+PMODE_CONFIG: con CONFIG_PATH + "pmode.cfg";
+POPT_CONFIG: con CONFIG_PATH + "popt.cfg";
+PRINTER_CONFIG: con CONFIG_PATH + "printer.cfg";
+DEFPRINTER: con CONFIG_PATH + "defprinter";
+
+
+Cfg: adt {
+	name: string;
+	pairs: list of (string, string);
+};
+
+DEBUG :=0;
+
+
+all_papers: list of ref Paper;
+all_pmodes: list of ref Pmode;
+all_ptypes: list of ref Ptype;
+all_popts: list of ref Popt;
+all_printers: list of ref Printer;
+default_printer: ref Printer;
+stderr: ref Sys->FD;
+printfd: ref Sys->FD;
+
+# Initialization
+
+init(): int
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	bufio = load Bufio Bufio->PATH;
+	str = load String String->PATH;
+	all_papers = read_paper_config();
+	if (all_papers == nil) return 1;
+	all_pmodes = read_pmode_config();
+	if (all_pmodes == nil) return 1;
+	all_ptypes = read_ptype_config();
+	if (all_ptypes == nil) return 1;
+	all_printers = read_printer_config();
+	if (all_printers == nil) return 1;
+	all_popts = read_popt_config();
+	for (pl:=all_printers; pl!=nil; pl=tl pl) {
+		p := hd pl;
+		opt := find_popt(all_popts, p.name);
+		if (opt != nil) p.popt = opt;
+		else {
+			p.popt = ref Popt (p.name, hd all_pmodes, hd all_papers, 0, 0);
+			all_popts = p.popt :: all_popts;
+		}
+	}
+	return 0;
+}
+
+# Set printer FD
+
+set_printfd(fd: ref Sys->FD)
+{
+	printfd = fd;	
+}
+
+
+# Get default printer
+
+get_defprinter(): ref Printer
+{
+	if (len all_printers == 1) return hd all_printers;		# If there's only 1 printer
+	df := sys->open(DEFPRINTER, Sys->OREAD);
+	if (df == nil) {
+		if (all_printers != nil) return hd all_printers;
+		else return nil;
+	}
+	a := array[MAXNAME] of byte;
+	nb := sys->read(df, a, MAXNAME);
+	if (nb < 2) return nil;
+	name := string a[:nb-1];
+	def := find_printer(all_printers, name);
+	if (def != nil) return def;
+	else return hd all_printers;
+}
+
+# Set default printer
+
+set_defprinter(p: ref Printer)
+{
+	df := sys->create(DEFPRINTER, Sys->OWRITE, DEFMODE);
+	if (df == nil) return;
+	sys->fprint(df, "%s\n", p.name);
+}
+
+# Set paper size
+
+get_size(p: ref Printer): (int, int, int)	# dpi, xpixels, ypixels
+{
+	if (p == nil) return (0, 0, 0);
+	load_driver(p);
+	dpi := p.popt.mode.resx;
+	(xpix, ypix) := p.pdriver->printable_pixels(p);	# This takes account of orientation
+	return (dpi, xpix, ypix);
+}
+
+
+
+# Get list of all printers
+
+get_printers(): list of ref Printer
+{
+	return all_printers;
+}
+
+# Return list of printer types
+
+get_ptypes(): list of ref Ptype
+{
+	return all_ptypes;
+}
+
+# Return list of print modes
+
+get_pmodes(): list of ref Pmode
+{
+	return all_pmodes;
+}
+
+# Return list of paper types
+
+get_papers(): list of ref Paper
+{
+	return all_papers;
+}
+
+# Return list of print options
+
+get_popts(): list of ref Popt
+{
+	return all_popts;
+}
+
+# Save option settings
+
+save_settings(): int
+{
+	return write_popt_config(all_popts);
+
+}
+
+
+# Print an image
+
+print_image(p: ref Printer, display: ref Draw->Display, im: ref Draw->Image, pcwidth: int, cancel: chan of int): int
+{
+	if (p == nil || im == nil) return 1;
+	load_driver(p);
+	popen(p);
+	(xpix, ypix) := p.pdriver->printable_pixels(p);
+	imwidth := im.r.max.x - im.r.min.x;
+	imheight := im.r.max.y - im.r.min.y;
+	if (pcwidth > 0) pixwidth := int (real xpix * real pcwidth/100.0);
+	else pixwidth = imwidth;
+	lmar := (xpix - pixwidth)/2;
+	fpixwidth := pixwidth;
+	if (p.popt.orientation != PORTRAIT) {
+		lmar += pixwidth;
+		fpixwidth = pixwidth*imheight/imwidth;
+	}
+	if (lmar < 0) lmar = 0;
+	return p.pdriver->sendimage(p, printfd, display, im, fpixwidth, lmar, cancel);
+}
+
+# Print text
+
+print_textfd(p: ref Printer, fd: ref Sys->FD, ps: real, pr: int, wrap: int): int
+{
+	load_driver(p);
+	popen(p);
+	return p.pdriver->sendtextfd(p, printfd, fd, ps, pr, wrap);
+
+}
+
+
+# Open printer device if necessary
+
+popen(p: ref Printer)
+{
+	if (printfd != nil) return;
+	printfd = sys->create(p.device, Sys->OWRITE, DEFMODE);
+}
+
+# Find printer item
+
+find_printer(all: list of ref Printer, name: string): ref Printer
+{
+	for (p:=all; p!=nil; p=tl p) if ((hd p).name == name) return hd p;
+	return nil;
+}
+
+# Find popt item
+
+find_popt(all: list of ref Popt, name: string): ref Popt
+{
+	for (p:=all; p!=nil; p=tl p) if ((hd p).name == name) return hd p;
+	return nil;
+}
+
+
+# Find paper item
+
+find_paper(all: list of ref Paper, name: string): ref Paper
+{
+	for (p:=all; p!=nil; p=tl p) if ((hd p).name == name) return hd p;
+	return nil;
+}
+
+# Find pmode item
+
+find_pmode(all: list of ref Pmode, name: string): ref Pmode
+{
+	for (p:=all; p!=nil; p=tl p) if ((hd p).name == name) return hd p;
+	return nil;
+}
+
+# Find ptype item
+
+find_ptype(all: list of ref Ptype, name: string): ref Ptype
+{
+	for (p:=all; p!=nil; p=tl p) if ((hd p).name == name) return hd p;
+	return nil;
+}
+
+
+# Read paper config file
+
+read_paper_config(): list of ref Paper
+{
+	(clist, aliases) := read_config(PAPER_CONFIG);
+	rlist: list of ref Paper;
+	while (clist != nil) {
+		this := hd clist;
+		clist = tl clist;
+		item := ref Paper(this.name, "", 0.0, 0.0);
+		for (pairs:= this.pairs; pairs != nil; pairs = tl pairs) {
+			(name, value) := hd pairs;
+			case (name) {
+				"hpcode" =>
+					item.hpcode = value;
+
+				"width_inches" =>
+					item.width_inches = real value;
+
+				"height_inches" =>
+					item.height_inches = real value;
+
+				* =>
+					sys->fprint(stderr, "Unknown paper config file option: %s\n", name);
+			}
+		}
+		rlist =item :: rlist;
+	}
+	for (al:=aliases; al!=nil; al=tl al) {
+		(new, old) := hd al;
+		olda := find_paper(rlist, old);
+		if (olda == nil) sys->fprint(stderr, "Paper alias %s not found\n", old);
+		else {
+			newa := ref *olda;
+			newa.name = new;
+			rlist = newa :: rlist;
+			}
+	}
+	return rlist;
+}
+
+
+# Read pmode config file
+
+read_pmode_config(): list of ref Pmode
+{
+	(clist, aliases)  := read_config(PMODE_CONFIG);
+	rlist: list of ref Pmode;
+	while (clist != nil) {
+		this := hd clist;
+		clist = tl clist;
+		item := ref Pmode(this.name, "", 0, 0, 1, 1, 1);
+		for (pairs:= this.pairs; pairs != nil; pairs = tl pairs) {
+			(name, value) := hd pairs;
+			case (name) {
+				"desc" =>
+					item.desc = value;
+
+				"resx" =>
+					item.resx = int value;
+
+				"resy" =>
+					item.resy = int value;
+
+				"coldepth" =>
+					item.coldepth = int value;
+
+				"blackdepth" =>
+					item.blackdepth = int value;
+
+				"blackresmult" =>
+					item.blackresmult = int value;
+
+				* =>
+					sys->fprint(stderr, "Unknown pmode config file option: %s\n", name);
+
+			}
+		}
+		rlist =item :: rlist;
+	}
+	for (al:=aliases; al!=nil; al=tl al) {
+		(new, old) := hd al;
+		olda := find_pmode(rlist, old);
+		if (olda == nil) sys->fprint(stderr, "Pmode alias %s not found\n", old);
+		else {
+			newa := ref *olda;
+			newa.name = new;
+			rlist = newa :: rlist;
+			}
+	}
+	return rlist;
+}
+
+
+
+
+# Readp Ptype config file
+
+read_ptype_config(): list of ref Ptype
+{
+	(clist, aliases)  := read_config(PTYPE_CONFIG);
+	rlist: list of ref Ptype;
+	while (clist != nil) {
+		this := hd clist;
+		clist = tl clist;
+		item := ref Ptype(this.name, "", nil, "", "");
+		for (pairs:= this.pairs; pairs != nil; pairs = tl pairs) {
+			(name, value) := hd pairs;
+			case (name) {
+				"desc" =>
+					item.desc = value;
+
+				"driver" =>
+					item.driver = value;
+
+				"hpmapfile" =>
+					item.hpmapfile = value;
+
+				"modes" =>
+					item.modes = make_pmode_list(value);
+
+				* =>
+					sys->fprint(stderr, "Unknown ptype config file option: %s\n", name);
+			}
+		}
+		if (item.modes == nil) {
+			sys->fprint(stderr, "No print modes for ptype %s\n", item.name);
+			continue;
+		}			
+		rlist = item :: rlist;
+	}
+	for (al:=aliases; al!=nil; al=tl al) {
+		(new, old) := hd al;
+		olda := find_ptype(rlist, old);
+		if (olda == nil) sys->fprint(stderr, "Ptype alias %s not found\n", old);
+		else {
+			newa := ref *olda;
+			newa.name = new;
+			rlist = newa :: rlist;
+			}
+	}
+	return rlist;
+}
+
+
+# Make a list of pmodes from a string
+
+make_pmode_list(sl: string): list of ref Pmode
+{
+	pml: list of ref Pmode;
+	(n, toks) := sys->tokenize(sl, " \t");
+	if (n == 0) return nil;
+	for (i:=0; i<n; i++) {
+		pms := hd toks;
+		toks = tl toks;
+		pm := find_pmode(all_pmodes, pms);
+		if (pm == nil) {
+			sys->fprint(stderr, "unknown pmode: %s\n", pms);
+			continue;
+		}
+		pml = pm :: pml;
+	}
+	return pml;
+}
+
+
+# Read popt config file
+
+read_popt_config(): list of ref Popt
+{
+	(clist, aliases)  := read_config(POPT_CONFIG);
+	rlist: list of ref Popt;
+	while (clist != nil) {
+		this := hd clist;
+		clist = tl clist;
+		item := ref Popt(this.name, nil, nil, 0, 0);
+		for (pairs:= this.pairs; pairs != nil; pairs = tl pairs) {
+			(name, value) := hd pairs;
+			case (name) {
+
+				"mode" =>
+					item.mode = find_pmode(all_pmodes, value);
+					if (item.mode == nil) sys->fprint(stderr, "Config error: Pmode not found: %s\n", value);
+
+				"paper" =>
+					item.paper = find_paper(all_papers, value);
+					if (item.paper == nil) sys->fprint(stderr, "Config error: paper not found: %s\n", value);
+
+				"orientation" =>
+					item.orientation = int value;
+				"duplex" =>
+					item.duplex = int value;
+
+				* =>
+					sys->fprint(stderr, "Unknown popt config file option: %s\n", name);
+			}
+		}
+		if (item.mode == nil) {
+			sys->fprint(stderr, "No print mode for printer %s\n", item.name);
+			continue;
+		}			
+		if (item.paper == nil) {
+			sys->fprint(stderr, "No paper size for printer %s\n", item.name);
+			continue;
+		}			
+		rlist = item :: rlist;
+	}
+	for (al:=aliases; al!=nil; al=tl al) {
+		(new, old) := hd al;
+		olda := find_popt(rlist, old);
+		if (olda == nil) sys->fprint(stderr, "Popt alias %s not found\n", old);
+		else {
+			newa := ref *olda;
+			newa.name = new;
+			rlist = newa :: rlist;
+			}
+	}
+	return rlist;
+}
+
+
+
+
+# Read printer config file
+
+read_printer_config(): list of ref Printer
+{
+	(clist, aliases)  := read_config(PRINTER_CONFIG);
+	rlist: list of ref Printer;
+	while (clist != nil) {
+		this := hd clist;
+		clist = tl clist;
+		item := ref Printer(this.name, nil, "", nil, nil);
+		for (pairs:= this.pairs; pairs != nil; pairs = tl pairs) {
+			(name, value) := hd pairs;
+			case (name) {
+				"ptype" =>
+					item.ptype = find_ptype(all_ptypes, value);
+					if (item.ptype == nil) sys->fprint(stderr, "Config error: Ptype not found: %s\n", value);
+
+				"device" =>
+					item.device = value;
+
+				* =>
+					sys->fprint(stderr, "Unknown printer config file option: %s\n", name);
+			}
+		}
+		if (item.ptype == nil) {
+			sys->fprint(stderr, "No printer type for printer %s\n", item.name);
+			continue;
+		}			
+		rlist = item :: rlist;
+	}
+	for (al:=aliases; al!=nil; al=tl al) {
+		(new, old) := hd al;
+		olda := find_printer(rlist, old);
+		if (olda == nil) sys->fprint(stderr, "Ptype alias %s not found\n", old);
+		else {
+			newa := ref *olda;
+			newa.name = new;
+			rlist = newa :: rlist;
+			}
+	}
+	return rlist;
+}
+
+# Write opt config file
+
+write_popt_config(plist: list of ref Popt): int
+{
+	cfl: list of Cfg;
+	for (pl:=plist; pl!=nil; pl=tl pl) {
+		po := hd pl;
+		cf := Cfg(po.name, nil);
+		cf.pairs = ("mode", po.mode.name) :: cf.pairs;
+		cf.pairs = ("paper", po.paper.name) :: cf.pairs;
+		cf.pairs = ("orientation", sys->sprint("%d", po.orientation)) :: cf.pairs;
+		cf.pairs = ("duplex", sys->sprint("%d", po.duplex)) :: cf.pairs;
+		cfl = cf :: cfl;
+	}
+	return write_config(POPT_CONFIG, cfl, nil);
+}
+
+
+write_config(fspec: string, clist: list of Cfg, aliases: list of (string, string)): int
+{
+	fd := sys->create(fspec, Sys->OWRITE, DEFMODE);
+	if (fd == nil) {
+		sys->fprint(stderr, "Failed to write to config file %s: %r\n", fspec);
+		return 1;
+	}
+	for (cfl:=clist; cfl!=nil; cfl=tl cfl) {
+		cf := hd cfl;
+		sys->fprint(fd, "%s=\n", cf.name);
+		for (pl:=cf.pairs; pl!=nil; pl=tl pl) {
+			(name, value) := hd pl;
+			if (sys->fprint(fd, "\t%s=%s\n", name, value) < 0) return 2;
+		}
+	}
+	for (al:=aliases; al!=nil; al=tl al) {
+		(new, old) := hd al;
+		if (sys->fprint(fd, "%s=%s\n", new, old)) return 2;
+	}
+	return 0;	
+}
+
+
+# Read in a config file and return list of items and aliases
+
+read_config(fspec: string): (list of Cfg, list of (string, string))
+{
+	ib := bufio->open(fspec, Bufio->OREAD);
+	if (ib == nil) {
+		sys->fprint(stderr, "Failed to open config file %s: %r\n", fspec);
+		return (nil, nil);
+	}
+	clist: list of Cfg;
+	plist: list of (string, string);
+	section := "";
+	aliases : list of (string, string);
+	while ((line := bufio->ib.gets('\n')) != nil) {
+		if (line[0] == '#') continue;
+		if (line[len line-1] == '\n') line = line[:len line-1];
+		if (len line == 0) continue;
+		if (line[0] != ' ' && line[0] != '\t') {
+			if (section != "") clist = Cfg (section, plist) :: clist;
+			section = "";
+			plist = nil;
+			sspec := strip(line);
+			(n, toks) := sys->tokenize(sspec, "=");
+			if (n == 0) continue;
+			if (n > 2) {
+				sys->fprint(stderr, "Error in config file %s\n", fspec);
+				continue;
+			}
+			if (n == 2) {
+				asection := hd toks;
+				toks = tl toks;
+				alias := hd toks;
+				aliases = (asection, alias) :: aliases; 
+				continue;
+			}
+			section = hd toks;
+		} else {
+			(n, toks) := sys->tokenize(line, "=");
+			if (n == 2) {
+				name := strip(hd toks);
+				toks = tl toks;
+				value := strip(hd toks);
+				plist = (name, value) :: plist;
+			}
+		}
+	}
+	if (section != "") clist = Cfg (section, plist) :: clist;
+	return (clist, aliases);
+}
+
+
+# Load printer driver if necessary
+load_driver(p: ref Printer)
+{
+	if (p.pdriver != nil) return;
+	modpath := Pdriver->PATHPREFIX + p.ptype.driver;
+	p.pdriver = load Pdriver modpath;
+	if (p.pdriver == nil) sys->fprint(stderr, "Failed to load driver %s: %r\n", modpath);
+	p.pdriver->init(DEBUG);
+}
+
+
+# Strip leading/trailing spaces
+
+strip(s: string): string
+{
+	(dummy1, s1) := str->splitl(s, "^ \t");
+	(s2, dummy2) := str->splitr(s1, "^ \t");
+	return s2;
+}	
--- /dev/null
+++ b/appl/lib/print/scaler.b
@@ -1,0 +1,186 @@
+implement Scaler;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "print.m";
+include "scaler.m";
+
+DEBUG := 0;
+
+# Scaler initialisation
+
+init(debug: int, WidthInPixels, ScaleFactorMultiplier, ScaleFactorDivisor: int): ref RESSYNSTRUCT
+{
+	DEBUG = debug;
+	ScaleFactor := real ScaleFactorMultiplier / real ScaleFactorDivisor;
+	ScaleBound := int ScaleFactor;
+	if  (ScaleFactor > real ScaleBound) ScaleBound++;
+	ResSynStruct := ref RESSYNSTRUCT (
+					WidthInPixels+2,	# add 2 for edges
+					ScaleFactorMultiplier,
+					ScaleFactorDivisor,
+					ScaleFactor,
+					int ((real WidthInPixels / real ScaleFactorDivisor))*ScaleFactorMultiplier + 1,
+					ScaleFactorMultiplier != ScaleFactorDivisor,
+					ScaleFactor < 2.0,
+					(ScaleFactorMultiplier * 256 / ScaleFactorDivisor)
+										  -  ((ScaleFactorMultiplier/ScaleFactorDivisor) * 256),
+					0,
+					0,
+					array[NUMBER_RASTERS] of array of int,
+					array[ScaleBound] of array of int,
+					0,
+					0
+				);
+	if (ResSynStruct.ScaleFactor > real ScaleBound) ScaleBound++;
+	for (i:=0; i<len ResSynStruct.Buffer; i++) ResSynStruct.Buffer[i] = array[WidthInPixels*NUMBER_RASTERS] of int;
+	for (i=0; i<len ResSynStruct.oBuffer; i++) ResSynStruct.oBuffer[i] = array[ResSynStruct.iOutputWidth] of int;
+	return ResSynStruct;
+}
+
+
+# Input a raster line to the scaler
+
+rasterin(rs: ref RESSYNSTRUCT, inraster: array of int)
+{
+	if (!rs.scaling) {		# Just copy to output buffer
+		if (inraster == nil) return;
+		rs.oBuffer[0] = inraster;
+		rs.nready = 1;
+		rs.ndelivered = 0;
+		return;
+	}
+
+	if (rs.ReplicateOnly) {	# for scaling between 1 and 2
+#		for (i:=0; i<len inraster; i++) rs.oBuffer[0][i] = inraster[i];
+		rs.oBuffer[0][:] = inraster[0:];
+		create_out(rs, 1);
+		return;
+	}
+
+	if (rs.RastersinBuffer == 0) {	# First time through
+		if (inraster == nil) return;
+		for (i:=0; i<2; i++) {
+			rs.Buffer[i][0] = inraster[0];
+#			for (j:=1; j<rs.Width-1; j++) rs.Buffer[i][j] = inraster[j-1];
+			rs.Buffer[i][1:] = inraster[0:rs.Width-2];
+			rs.Buffer[i][rs.Width-1] = inraster[rs.Width-3];
+		}
+		rs.RastersinBuffer = 2;
+		return;
+	}
+
+	if (rs.RastersinBuffer == 2) {	# Just two buffers in so far
+		if (inraster != nil) {
+			i := 2;
+			rs.Buffer[i][0] = inraster[0];
+#			for (j:=1; j<rs.Width-1; j++) rs.Buffer[i][j] = inraster[j-1];
+			rs.Buffer[i][1:] = inraster[0:rs.Width-2];
+			rs.Buffer[i][rs.Width-1] = inraster[rs.Width-3];
+			rs.RastersinBuffer = 3;
+		} else {	# nil means end of image
+			rez_synth(rs, rs.oBuffer[0], rs.oBuffer[1]);
+			create_out(rs, 0);
+		}
+		return;
+	}
+	if (rs.RastersinBuffer == 3) {	# All three buffers are full
+		(rs.Buffer[0], rs.Buffer[1], rs.Buffer[2]) = (rs.Buffer[1], rs.Buffer[2], rs.Buffer[0]);
+		if (inraster != nil) {
+			i := 2;
+			rs.Buffer[i][0] = inraster[0];
+#			for (j:=1; j<rs.Width-1; j++) rs.Buffer[i][j] = inraster[j-1];
+			rs.Buffer[i][1:] = inraster[0:rs.Width-2];
+			rs.Buffer[i][rs.Width-1] = inraster[rs.Width-3];
+		} else {	# nil means end of image
+#			for (j:=0; j<len rs.Buffer[1]; j++) rs.Buffer[2][j] = rs.Buffer[1][j];
+			rs.Buffer[2][:] = rs.Buffer[1];
+			rs.RastersinBuffer = 0;
+
+		}
+		rez_synth(rs, rs.oBuffer[0], rs.oBuffer[1]);
+		create_out(rs, 0);
+	}
+
+}
+
+
+# Get a raster output line from the scaler
+
+rasterout(rs: ref RESSYNSTRUCT): array of int
+{
+	if (rs.nready-- > 0) {
+		return rs.oBuffer[rs.ndelivered++][:rs.iOutputWidth-1];
+	} else return nil;
+}
+
+
+
+# Create output raster
+
+create_out(rs: ref RESSYNSTRUCT, simple: int)
+{
+	factor: int;
+	if (simple) factor = 1;
+	else factor = 2;
+
+	out_width := (rs.Width-2) * rs.ScaleFactorMultiplier / rs.ScaleFactorDivisor;
+	number_out := rs.ScaleFactorMultiplier / rs.ScaleFactorDivisor; 
+	if (number_out == 2 && !(rs.ScaleFactorMultiplier % rs.ScaleFactorDivisor) ) {
+		rs.nready = 2;
+		rs.ndelivered = 0;
+		return;
+	}
+
+	if (rs.ScaleFactorMultiplier % rs.ScaleFactorDivisor)
+	{
+		rs.Remainder = rs.Remainder + rs.Repeat;  
+
+		if (rs.Remainder >= 256)	# send extra raster
+		{
+			number_out++;
+			rs.Remainder = rs.Remainder - 256; 
+		}
+	}
+	# set up pointers into the output buffer
+	output_raster := array[number_out] of array of int;
+	output_raster[:] = rs.oBuffer[0:number_out];
+
+	ScaleFactorMultiplier := rs.ScaleFactorMultiplier;
+	ScaleFactorDivisor := rs.ScaleFactorDivisor;
+	sf := factor * ScaleFactorDivisor;
+
+	# Convert the input data by starting at the bottom right hand corner and move left + up
+	for (i:=(number_out-1); i>=0; i--) {
+		y_index := i*sf/ScaleFactorMultiplier;
+		orast_i := output_raster[i];
+		orast_y := output_raster[y_index];
+		for (lx := out_width-1; lx>=0; --lx) {
+			x_index := lx*sf/ScaleFactorMultiplier;
+			orast_i[lx] = orast_y[x_index];
+		}
+	}
+
+	rs.nready = number_out;
+	rs.ndelivered = 0;
+	return;
+}
+
+
+# Synthesise raster line
+
+rez_synth(rs: ref RESSYNSTRUCT, output_raster0, output_raster1: array of int)
+{
+
+	i := 1;
+	Buffer := rs.Buffer[i];
+	h_offset := 0;
+	for (j:=1; j<rs.Width-1; j++) {
+		rgb := Buffer[j];
+		output_raster0[h_offset] = rgb;
+		output_raster1[h_offset++] = rgb;
+		output_raster0[h_offset] = rgb;
+		output_raster1[h_offset++] = rgb;
+	}
+}
--- /dev/null
+++ b/appl/lib/print/scaler.m
@@ -1,0 +1,30 @@
+Scaler: module
+{
+	PATH: con "/dis/lib/print/scaler.dis";
+
+	init: fn(debug: int, WidthInPixels, ScaleFactorMultiplier, ScaleFactorDivisor: int): ref RESSYNSTRUCT;
+	rasterin: fn(rs: ref RESSYNSTRUCT, inraster: array of int);
+	rasterout: fn(rs: ref RESSYNSTRUCT ): array of int;
+
+	RESSYNSTRUCT: adt {
+		Width: int;
+		ScaleFactorMultiplier: int;
+		ScaleFactorDivisor: int;
+		ScaleFactor: real;
+		iOutputWidth: int;
+		scaling: int;
+		ReplicateOnly: int;
+		Repeat: int;
+		RastersinBuffer: int;
+		Remainder: int;
+		Buffer: array of array of int;
+		oBuffer: array of array of int;
+		nready: int;
+		ndelivered: int;
+	};
+
+
+};
+
+
+NUMBER_RASTERS: con 3;	# no of rasters to buffer
--- /dev/null
+++ b/appl/lib/profile.b
@@ -1,0 +1,1230 @@
+implement Profile;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "workdir.m";
+	workdir: Workdir;
+include "debug.m";
+	debug: Debug;
+	Sym: import debug;
+include "dis.m";
+	dism: Dis;
+include "profile.m";
+
+# merge common code
+
+PROF: con "/prof";
+CTL: con "ctl";
+NAME: con "name";
+MPATH: con "path";
+HISTOGRAM: con "histogram";
+
+inited: int;
+modl: string;
+lasterr: string;
+
+bspath := array[] of
+{
+	("/dis/",		"/appl/cmd/"),
+	("/dis/",		"/appl/"),
+};
+
+error(s: string)
+{
+	lasterr = sys->sprint("%s: %r", s);
+}
+
+error0(s: string)
+{
+	lasterr = s;
+}
+
+cleare()
+{
+	lasterr = nil;
+}
+
+lasterror(): string
+{
+	return lasterr;
+}
+
+init(): int
+{
+	cleare();
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	debug = load Debug Debug->PATH;
+	if(debug == nil){
+		error("cannot load Debug module");
+		return -1;
+	}
+	debug->init();
+	(ok, nil) := sys->stat(PROF + "/ctl");
+	if (ok == -1) {
+		if(sys->bind("#P", PROF, Sys->MREPL|Sys->MCREATE) < 0){
+			error(sys->sprint("cannot bind prof device to /prof"));
+			return -1;
+		}
+	}
+	inited = 1;
+	return 0;
+}
+
+end(): int
+{
+	cleare();
+	inited = 0;
+	modl = nil;
+	if(write(mkpath(PROF, CTL), "end") < 0)
+		return -1;
+	return 0;
+}
+
+start(): int
+{
+	cleare();
+	if(!inited && init() < 0)
+		return -1;
+	if(write(mkpath(PROF, CTL), "module " + modl) < 0)
+		return -1;
+	if(write(mkpath(PROF, CTL), "start") < 0)
+		return -1;
+	return 0;
+}
+
+cpstart(pid: int): int
+{
+	cleare();
+	if(!inited && init() < 0)
+		return -1;
+	if(write(mkpath(PROF, CTL), "module " + modl) < 0)
+		return -1;
+	if(write(mkpath(PROF, CTL), "startcp " + string pid) < 0)
+		return -1;
+	return 0;
+}
+
+memstart(m: int): int
+{
+	cleare();
+	if(!inited && init() < 0)
+		return -1;
+	if(modl != nil && write(mkpath(PROF, CTL), "module " + modl) < 0)
+		return -1;
+	start := "startmp";
+	if(m == 0)
+		m = MAIN|HEAP|IMAGE;
+	if(m&MAIN)
+		start += "1";
+	if(m&HEAP)
+		start += "2";
+	if(m&IMAGE)
+		start += "3";
+	if(write(mkpath(PROF, CTL), start) < 0)
+		return -1;
+	return 0;
+}
+
+stop(): int
+{
+	cleare();
+	if(!inited && init() < 0)
+		return -1;
+	if(write(mkpath(PROF, CTL), "stop") < 0)
+		return -1;
+	return 0;
+}
+
+sample(i: int): int
+{
+	cleare();
+	if(i <= 0){
+		error0(sys->sprint("bad sample rate %d", i));
+		return -1;
+	}
+	if(write(mkpath(PROF, CTL), "interval " + string i) < 0)
+		return -1;
+	return 0;
+}
+
+profile(m: string): int
+{
+	cleare();
+	modl = m + " " + modl;
+	return 0;
+}
+
+stats(): Prof
+{
+	mp: Modprof;
+	p: Prof;
+	mpl: list of Modprof;
+
+	cleare();
+	fd := sys->open(PROF, Sys->OREAD);
+	if(fd == nil){
+		error(sys->sprint("cannot open %s for reading", PROF));
+		return (nil, 0, nil);
+	}
+	total := 0;
+	for(;;){
+		(nr, d) := sys->dirread(fd);
+		if(nr <= 0)
+			break;
+		for(i := 0; i < nr; i++){
+			if(d[i].name == CTL)
+				continue;
+			dn := mkpath(PROF, d[i].name);
+			mp.name = read(mkpath(dn, NAME));
+			mp.path = read(mkpath(dn, MPATH));
+			fdh := sys->open(mkpath(dn, HISTOGRAM), Sys->OREAD);
+			if(fdh == nil)
+				continue;
+			(mp.srcpath, mp.linetab, mp.funtab, mp.total) = tprofile(fdh, mp.path);
+			if((sp := getb(mp.path)) != nil)
+				mp.srcpath = sp;
+			if(mp.total != 0){
+				mpl = mp :: mpl;
+				total += mp.total;
+			}
+		}
+	}
+	p.mods = mpl;
+	p.total = total;
+	return p;
+}
+
+cpstats(rec: int, v: int): Prof
+{
+	m: string;
+	mp: Modprof;
+	p: Prof;
+	mpl: list of Modprof;
+
+	cleare();
+	fd := sys->open(PROF, Sys->OREAD);
+	if(fd == nil){
+		error(sys->sprint("cannot open %s for reading", PROF));
+		return (nil, 0, nil);
+	}
+	total := 0;
+	for(;;){
+		(nr, d) := sys->dirread(fd);
+		if(nr <= 0)
+			break;
+		for(i:=0; i<nr; i++){
+			if(d[i].name == CTL)
+				continue;
+			dn := mkpath(PROF, d[i].name);
+			mp.name = read(mkpath(dn, NAME));
+			mp.path = read(mkpath(dn, MPATH));
+			fdh := sys->open(mkpath(dn, HISTOGRAM), Sys->OREAD);
+			if(fdh == nil)
+				continue;
+			(m, mp.srcpath, mp.rawtab, mp.linetab, mp.rngtab, mp.total, mp.coverage) = cprofile(fdh, mp.path, rec, v);
+			if(mp.name == nil)
+				mp.name = m;
+			if((sp := getb(mp.path)) != nil)
+				mp.srcpath = sp;
+			if(len mp.rawtab > 0){
+				mpl = mp :: mpl;
+				total += mp.total;
+			}
+		}
+	}
+	p.mods = mpl;
+	p.total = total;
+	return p;
+}
+
+cpfstats(v: int): Prof
+{
+	mp: Modprof;
+	p: Prof;
+	mpl: list of Modprof;
+
+	cleare();
+	total := 0;
+	(nil, l) := sys->tokenize(modl, " ");
+	for( ; l != nil; l = tl l){
+		s := hd l;
+		suf := suff(s);
+		if(suf == nil)
+			s += ".dis";
+		else
+			s = repsuff(s, "."+suf, ".dis");
+		if(!exists(s) && s[0] != '/' && s[0:2] != "./")
+			s = "/dis/"+s;
+		mp.path = s;
+		(mp.name, mp.srcpath, mp.rawtab, mp.linetab, mp.rngtab, mp.total, mp.coverage) = cprofile(nil, mp.path, 1, v);
+		if((sp := getb(mp.path)) != nil)
+			mp.srcpath = sp;
+		if(len mp.rawtab > 0){
+			mpl = mp :: mpl;
+			total += mp.total;
+		}
+	}
+	p.mods = mpl;
+	p.total = total;
+	return p;
+}
+
+memstats(): Prof
+{
+	mp: Modprof;
+	p: Prof;
+	mpl: list of Modprof;
+
+	cleare();
+	fd := sys->open(PROF, Sys->OREAD);
+	if(fd == nil){
+		error(sys->sprint("cannot open %s for reading", PROF));
+		return (nil, 0, nil);
+	}
+	total := totale := 0;
+	for(;;){
+		(nr, d) := sys->dirread(fd);
+		if(nr <= 0)
+			break;
+		for(i:=0; i<nr; i++){
+			if(d[i].name == CTL)
+				continue;
+			dn := mkpath(PROF, d[i].name);
+			mp.name = read(mkpath(dn, NAME));
+			mp.path = read(mkpath(dn, MPATH));
+			fdh := sys->open(mkpath(dn, HISTOGRAM), Sys->OREAD);
+			if(fdh == nil)
+				continue;
+			mp.totals = array[1] of int;
+			(mp.srcpath, mp.linetab, mp.funtab, mp.total, mp.totals[0]) = mprofile(fdh, mp.path);
+			if((sp := getb(mp.path)) != nil)
+				mp.srcpath = sp;
+			if(mp.total != 0 || mp.totals[0] != 0){
+				mpl = mp :: mpl;
+				total += mp.total;
+				totale += mp.totals[0];
+			}
+		}
+	}
+	p.mods = mpl;
+	p.total = total;
+	p.totals = array[1] of int;
+	p.totals[0] = totale;
+	return p;
+}
+
+tprofile(fd: ref Sys->FD, dis: string): (string, array of int, array of Funprof, int)
+{
+	sbl := findsbl(dis);
+	if(sbl == nil){
+		error0(sys->sprint("cannot locate symbol table file for %s", dis));
+		return (nil, nil, nil, 0);
+	}
+	(sym, err) := debug->sym(sbl);
+	if(sym == nil){
+		error0(sys->sprint("bad symbol table file: %s", err));
+		return (nil, nil, nil, 0);
+	}
+	nlines := 0;
+	nl := len sym.src;
+	for(i := 0; i < nl; i++){
+		if((l := sym.src[i].stop.line) > nlines)
+			nlines = l;
+	}
+	name := sym.src[0].start.file;
+	line := array[nlines+1] of int;
+	for(i = 0; i <= nlines; i++)
+		line[i] = 0;
+	nf := len sym.fns;
+	fun := array[nf] of Funprof;
+	for(i = 0; i < nf; i++){
+		fun[i].name = sym.fns[i].name;
+		# src seems to be always nil
+		# fun[i].file = sym.fns[i].src.start.file;
+		# fun[i].line = (sym.fns[i].src.start.line+sym.fns[i].src.stop.line)/2;
+		src := sym.pctosrc(sym.fns[i].offset);
+		if(src != nil)
+			fun[i].line = src.start.line;
+		else
+			fun[i].line = 0;
+		fun[i].count = 0;
+	}
+	buf := array[32] of byte;
+	# pc := 0;
+	tot := 0;
+	fi := 0;
+# for(i=0; i < nl; i++) sys->print("%d -> %d\n", i, sym.pctosrc(i).start.line);
+	while((m := sys->read(fd, buf, len buf)) > 0){
+		(nw, lw) := sys->tokenize(string buf[0:m], " ");
+		if(nw != 2){
+			error0("bad histogram data");
+			return  (nil, nil, nil, 0);
+		}
+		pc := int hd lw;
+		f := int hd tl lw;
+		rpc := pc-1;
+		src := sym.pctosrc(rpc);
+		if(src == nil)
+			continue;
+		l1 := src.start.line;
+		l2 := src.stop.line;
+		if(l1 == 0 || l2 == 0)
+			continue;
+		if((nl = l2-l1+1) == 1)
+			line[l1] += f;
+		else{
+			q := f/nl;
+			r := f-q*nl;
+			for(i = l1; i <= l2; i++)
+				line[i] += q+(r-->0);
+		}
+		if(fi < nf){
+			if(rpc >= sym.fns[fi].offset && rpc < sym.fns[fi].stoppc)
+				fun[fi].count += f;
+			else{
+				while(fi < nf && rpc >= sym.fns[fi].stoppc)
+					fi++;
+				# fi++;
+				if(fi >= nf && f != 0)
+					error0(sys->sprint("bad fn index"));
+				if(fi < nf)
+					fun[fi].count += f;
+			}
+		}
+		tot += f;
+# sys->print("pc %d count %d l1 %d l2 %d\n", rpc, f, l1, l2);
+	}
+	return (name, line, fun, tot);
+}
+
+cprofile(fd: ref Sys->FD, dis: string, rec: int, v: int): (string, string, array of (int, int), array of int, array of ref Range, int, int)
+{
+	freq := v&FREQUENCY;
+	sbl := findsbl(dis);
+	if(sbl == nil){
+		error0(sys->sprint("cannot locate symbol table file for %s", dis));
+		return (nil, nil, nil, nil, nil, 0, 0);
+	}
+	(sym, err) := debug->sym(sbl);
+	if(sym == nil){
+		error0(sys->sprint("bad symbol table file: %s", err));
+		return (nil, nil, nil, nil, nil, 0, 0);
+	}
+	nlines := 0;
+	nl := len sym.src;
+	for(i := 0; i < nl; i++){
+		if((l := sym.src[i].start.line) > nlines)
+			nlines = l;
+		if((l = sym.src[i].stop.line) > nlines)
+			nlines = l;
+	}
+	name := sym.src[0].start.file;
+	line := array[nlines+1] of int;
+	for(i = 0; i <= nlines; i++){
+		if(freq)
+			line[i] = -1;
+		else
+			line[i] = 0;
+	}
+	rng := array[nlines+1] of ref Range;
+	for(i = 0; i < nl; i++)
+		cover(i, -1, sym, line, rng, freq);
+	buf := array[32] of byte;
+	nr := 0;
+	r := array[1024] of (int, int);
+	while((m := sys->read(fd, buf, len buf)) > 0){
+		(nw, lw) := sys->tokenize(string buf[0:m], " ");
+		if(nw != 2){
+			error0("bad histogram data");
+			return  (nil, nil, nil, nil, nil, 0, 0);
+		}
+		(r, nr) = add(r, nr, int hd lw, int hd tl lw);
+	}
+	r = clip(r, nr);
+	if(rec){
+		wt := nr > 0;
+		prf := repsuff(sbl, ".sbl", ".prf");
+		if(exists(prf)){
+			if(stamp(sbl) > stamp(prf)){
+				error0(sys->sprint("%s later than %s", sbl, prf));
+				return (nil, nil, nil, nil, nil, 0, 0);
+			}
+			r = mergeprof(r, readprof(prf));
+			nr = len r;
+		}
+		if(wt && writeprof(prf, r) < 0){
+			error0(sys->sprint("cannot write profile file %s", prf));
+			return (nil, nil, nil, nil, nil, 0, 0);
+		}
+	}
+	tot := 0;
+	lpc := 0;
+	dise := dist := 0;
+	for(i = 0; i < nr; i++){
+		(pc, f) := r[i];
+		for( ; lpc < pc; lpc++){
+			cover(lpc, 0, sym, line, rng, freq);
+			dist++;
+		}
+		cover(pc, f, sym, line, rng, freq);
+		dist++;
+		if(f != 0)
+			dise++;
+		tot += f;
+		lpc = pc+1;
+	}
+	for( ; lpc < nl; lpc++){
+		cover(lpc, 0, sym, line, rng, freq);
+		dist++;
+	}
+	if(dist == 0)
+		dist = 1;
+	return (sym.name, name, r, line, rng, tot, (100*dise)/dist);
+}
+
+show(p: Prof, v: int): int
+{
+	i: int;
+
+	cleare();
+	tot := p.total;
+	if(tot == 0)
+		return 0;
+	verbose := v&VERBOSE;
+	fullhdr := v&FULLHDR;
+	for(ml := p.mods; ml != nil; ml = tl ml){
+		mp := hd ml;
+		if(mp.total == 0)
+			continue;
+		if((b := getb(mp.path)) == nil)
+			continue;
+		sys->print("\nModule: %s(%s)\n\n", mp.name, mp.path);
+		line := mp.linetab;
+		if(v&FUNCTION){
+			fun := mp.funtab;
+			nf := len fun;
+			for(i = 0; i < nf; i++)
+				if(verbose || fun[i].count != 0){
+					if(fullhdr)
+						sys->print("%s:", b);
+					sys->print("%d\t%.2f\t%s()\n", fun[i].line, 100.0*(real fun[i].count)/(real tot), fun[i].name);
+			}
+			sys->print("\n**** module sampling points %d ****\n\n", mp.total);
+			if(v&LINE)
+				sys->print("\n");
+		}
+		if(v&LINE){
+			bio := bufio->open(b, Bufio->OREAD);
+			if(bio == nil){
+				error(sys->sprint("cannot open %s for reading", b));
+				continue;
+			}
+			i = 1;
+			ll := len line;
+			while((s := bio.gets('\n')) != nil){
+				f := 0;
+				if(i < ll)
+					f = line[i];
+				if(verbose || f != 0){
+					if(fullhdr)
+						sys->print("%s:", b);
+					sys->print("%d\t%.2f\t%s", i, 100.0*(real f)/(real tot), s);
+				}
+				i++;
+			}
+			sys->print("\n**** module sampling points %d ****\n\n", mp.total);
+		}
+	}
+	if(p.mods != nil && tl p.mods != nil)
+		sys->print("\n**** total sampling points %d ****\n\n", p.total);
+	return 0;
+}
+
+cpshow(p: Prof, v: int): int
+{
+	i: int;
+
+	cleare();
+	tot := p.total;
+	fullhdr := v&FULLHDR;
+	freq := v&FREQUENCY;
+	for(ml := p.mods; ml != nil; ml = tl ml){
+		mp := hd ml;
+		if((b := getb(mp.path)) == nil)
+			continue;
+		sys->print("\nModule: %s(%s)", mp.name, mp.path);
+		sys->print("\t%d%% coverage\n\n", mp.coverage);
+		if(mp.coverage == 100 && !freq)
+			continue;
+		line := mp.linetab;
+		rng := mp.rngtab;
+		bio := bufio->open(b, Bufio->OREAD);
+		if(bio == nil){
+			error(sys->sprint("cannot open %s for reading", b));
+			continue;
+		}
+		i = 1;
+		ll := len line;
+		while((s := bio.gets('\n')) != nil){
+			f := 0;
+			if(i < ll)
+				f = line[i];
+			if(fullhdr)
+				sys->print("%s:", b);
+			sys->print("%d\t", i);
+			if(rng != nil && i < ll && (r := rng[i]) != nil && multirng(r)){
+				for( ; r != nil; r = r.n){
+					sys->print("%s", trans(r.f, freq));
+					if(r.n != nil)
+						sys->print("|");
+				}
+			}
+			else
+				sys->print("%s", trans(f, freq));
+			sys->print("\t%s", s);
+			i++;
+		}
+		sys->print("\n**** module dis instructions %d ****\n\n", mp.total);
+	}
+	if(p.mods != nil && tl p.mods != nil)
+		sys->print("\n**** total number dis instructions %d ****\n\n", p.total);
+	return 0;
+}
+
+coverage(p: Prof, v: int): Coverage
+{
+	i: int;
+	clist: Coverage;
+
+	cleare();
+	freq := v&FREQUENCY;
+	for(ml := p.mods; ml != nil; ml = tl ml){
+		mp := hd ml;
+		if((b := getb(mp.path)) == nil)
+			continue;
+		line := mp.linetab;
+		rng := mp.rngtab;
+		bio := bufio->open(b, Bufio->OREAD);
+		if(bio == nil){
+			error(sys->sprint("cannot open %s for reading", b));
+			continue;
+		}
+		i = 1;
+		ll := len line;
+		llist: list of (list of (int, int, int), string);
+		while((s := bio.gets('\n')) != nil){
+			f := 0;
+			if(i < ll)
+				f = line[i];
+			rlist: list of (int, int, int);
+			if(rng != nil && i < ll && (r := rng[i]) != nil){
+				for( ; r != nil; r = r.n){
+					if(r.u == ∞)
+						r.u = len s - 1;
+					if(freq){
+						if(r.f > 0)
+							rlist = (r.l, r.u, r.f) :: rlist;
+					}
+					else{
+						if(r.f&NEX)
+							rlist = (r.l, r.u, (r.f&EXE)==EXE) :: rlist;
+					}
+				}
+			}
+			else{
+				if(freq){
+					if(f > 0)
+						rlist = (0, len s - 1, f) :: rlist;
+				}
+				else{
+					if(f&NEX)
+						rlist = (0, len s - 1, (f&EXE)==EXE) :: nil;
+				}
+			}
+			llist = (rlist, s) :: llist;
+			i++;
+		}
+		if(freq)
+			n := mp.total;
+		else
+			n = mp.coverage;
+		clist = (b, n, rev(llist)) :: clist;
+	}
+	return clist;
+}
+
+∞: con 1<<30;
+
+DIS: con 1;
+EXE: con 2;
+NEX: con 4;
+
+cover(pc: int, f: int, sym: ref Debug->Sym, line: array of int, rng: array of ref Range, freq: int)
+{
+	v: int;
+
+	src := sym.pctosrc(pc);
+	if(src == nil)
+		return;
+	l1 := src.start.line;
+	l2 := src.stop.line;
+	if(l1 == 0 || l2 == 0)
+		return;
+	c1 := src.start.pos;
+	c2 := src.stop.pos;
+	if(freq){
+		v = 0;
+		if(f > 0)
+			v = f;
+	}
+	else{
+		v = DIS;
+		if(f > 0)
+			v = EXE;
+		else if(f == 0)
+			v = NEX;
+	}
+	for(i := l1; i <= l2; i++){
+		r1 := 0;
+		r2 := ∞;
+		if(i == l1)
+			r1 = c1;
+		if(i == l2)
+			r2 = c2;
+		if(rng != nil)
+			rng[i] = mrgrng(addrng(rng[i], r1, r2, v, freq));
+		if(freq){
+			if(v > line[i])
+				line[i] = v;
+		}
+		else
+			line[i] |= v;
+		# if(i==123) sys->print("%d %d-%d %d %d\n", i, r1, r2, v, pc);
+	}
+}
+
+arng(c1: int, c2: int, f: int, tr: ref Range, lr: ref Range, r: ref Range): ref Range
+{
+	nr := ref Range(c1, c2, f, tr);
+	if(lr == nil)
+		r = nr;
+	else
+		lr.n = nr;
+	return r;
+}
+
+addrng(r: ref Range, c1: int, c2: int, f: int, freq: int): ref Range
+{
+	lr: ref Range;
+
+	if(c1 > c2)
+		return r;
+	for(tr := r; tr != nil; tr = tr.n){
+		r1 := tr.l;
+		r2 := tr.u;
+		if(c1 < r1){
+			if(c2 < r1)
+				return arng(c1, c2, f, tr, lr, r);
+			else if(c2 <= r2){
+				r = addrng(r, c1, r1-1, f, freq);
+				return addrng(r, r1, c2, f, freq);
+			}
+			else{
+				r = addrng(r, c1, r1-1, f, freq);
+				r = addrng(r, r1, r2, f, freq);
+				return addrng(r, r2+1, c2, f, freq);
+			}		
+		}
+		else if(c1 <= r2){
+			if(c2 <= r2){
+				v := tr.f;
+				tr.l = c1;
+				tr.u = c2;
+				if(freq){
+					if(f > tr.f)
+						tr.f = f;
+				}
+				else
+					tr.f |= f;
+				r = addrng(r, r1, c1-1, v, freq);
+				return addrng(r, c2+1, r2, v, freq);
+			}
+			else{
+				r = addrng(r, c1, r2, f, freq);
+				return addrng(r, r2+1, c2, f, freq);
+			}
+		}
+		lr = tr;
+	}
+	return arng(c1, c2, f, nil, lr, r);
+}
+
+mrgrng(r: ref Range): ref Range
+{
+	lr: ref Range;
+
+	for(tr := r; tr != nil; tr = tr.n){
+		if(lr != nil && lr.u >= tr.l)
+			sys->print("ERROR %d %d\n", lr.u, tr.l);
+		if(lr != nil && lr.f == tr.f && lr.u+1 == tr.l){
+			lr.u = tr.u;
+			lr.n = tr.n;
+		}
+		else
+			lr = tr;
+	}
+	return r;
+}
+
+multirng(r: ref Range): int
+{
+	f := r.f;
+	for(tr := r; tr != nil; tr = tr.n)
+		if(tr.f != f)
+			return 1;
+	return 0;
+}
+
+add(r: array of (int, int), nr: int, pc: int, f: int): (array of (int, int), int)
+{
+	l := len r;
+	if(nr == l){
+		s := array[2*l] of (int, int);
+		s[0:] = r[0: nr];
+		r = s;
+	}
+	r[nr++] = (pc, f);
+	return (r, nr);
+}
+
+clip(r: array of (int, int), nr: int): array of (int, int)
+{
+	l := len r;
+	if(nr < l){
+		s := array[nr] of (int, int);
+		s[0:] = r[0: nr];
+		r = s;
+	}
+	return r;
+}
+
+readprof(f: string): array of (int, int)
+{
+	b := bufio->open(f, Bufio->OREAD);
+	if(b == nil)
+		return nil;
+	nr := 0;
+	r := array[1024] of (int, int);
+	while((buf := b.gets('\n')) != nil){
+		(nw, lw) := sys->tokenize(buf, " ");
+		if(nw != 2){
+			error0("bad raw data");
+			return  nil;
+		}
+		(r, nr) = add(r, nr, int hd lw, int hd tl lw);
+	}
+	r = clip(r, nr);
+	return r;
+}
+
+mergeprof(r1, r2: array of (int, int)): array of (int, int)
+{
+	nr := 0;
+	r := array[1024] of (int, int);
+	l1 := len r1;
+	l2 := len r2;
+	for((i, j) := (0, 0); i < l1 || j < l2; ){
+		if(i < l1)
+			(pc1, f1) := r1[i];
+		else
+			pc1 = ∞;
+		if(j < l2)
+			(pc2, f2) := r2[j];
+		else
+			pc2 = ∞;
+		if(pc1 < pc2){
+			(r, nr) = add(r, nr, pc1, f1);
+			i++;
+		}
+		else if(pc1 > pc2){
+			(r, nr) = add(r, nr, pc2, f2);
+			j++;
+		}
+		else{
+			(r, nr) = add(r, nr, pc1, f1+f2);
+			i++;
+			j++;
+		}
+	}
+	r = clip(r, nr);
+	return r;
+}
+
+writeprof(f: string, r: array of (int, int)): int
+{
+	fd := sys->create(f, Sys->OWRITE, 8r664);
+	if(fd == nil)
+		return -1;
+	l := len r;
+	for(i := 0; i < l; i++){
+		(pc, fr) := r[i];
+		sys->fprint(fd, "%d %d\n", pc, fr);
+	}
+	return 0;
+}
+
+trans(f: int, freq: int): string
+{
+	if(freq)
+		return transf(f);
+	else
+		return transc(f);
+}
+
+transf(f: int): string
+{
+	if(f < 0)
+		return " ";
+	return string f;
+}
+
+transc(f: int): string
+{
+	c := "";
+	case(f){
+		0 => c = " ";
+		DIS|EXE => c = "+";
+		DIS|NEX => c = "-";
+		DIS|EXE|NEX => c = "?";
+		* =>
+			error(sys->sprint("bad code %d\n", f));
+	}
+	return c;
+}
+
+getb(dis: string): string
+{
+	b := findb(dis);
+	if(b == nil){
+		error0(sys->sprint("cannot locate source file for %s\n", dis));
+		return nil;
+	}
+	if(stamp(b) > stamp(dis)){
+		error0(sys->sprint("%s later than %s", b, dis));
+		return nil;
+	}
+	return b;
+}
+
+mkpath(d: string, f: string): string
+{
+	return d+"/"+f;
+}
+
+suff(s: string): string
+{
+	(n, l) := sys->tokenize(s, ".");
+	if(n > 1){
+		while(tl l != nil)
+			l = tl l;
+		return hd l;
+	}
+	return nil;
+}
+	
+repsuff(s: string, old: string, new: string): string
+{
+	lo := len old;
+	ls := len s;
+	if(lo <= ls && s[ls-lo:ls] == old)
+		return s[0:ls-lo]+new;
+	return s;
+}
+
+read(f: string): string
+{
+	if((fd := sys->open(f, Sys->OREAD)) == nil){
+		error(sys->sprint("cannot open %s for reading", f));
+		return nil;
+	}
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	return string buf[0:n];
+}
+
+write(f: string, s: string): int
+{
+	if((fd := sys->open(f, Sys->OWRITE)) == nil){
+		error(sys->sprint("cannot open %s for writing", f));
+		return -1;
+	}
+	b := array of byte s;
+	if((n := sys->write(fd, b, len b)) != len b){
+		error(sys->sprint("cannot write %s to file %s", s, f));
+		return -1;
+	}
+	return 0;
+}
+
+exists(f: string): int
+{
+	return sys->open(f, Sys->OREAD) != nil;
+}
+
+stamp(f: string): int
+{
+	(ok, d) := sys->stat(f);
+	if(ok < 0)
+		return 0;
+	return d.mtime;
+}
+
+findb(dis: string): string
+{
+	if(dism == nil){
+		dism = load Dis Dis->PATH;
+		if(dism != nil)
+			dism->init();
+	}
+	if(dism != nil && (b := dism->src(dis)) != nil && exists(b))
+		return b;
+	return findfile(repsuff(dis, ".dis", ".b"));
+}
+
+findsbl(dis: string): string
+{
+	b := findb(dis);
+	if(b != nil){
+		sbl := repsuff(b, ".b", ".sbl");
+		if(exists(sbl))
+			return sbl;
+		return findfile(sbl);
+	}
+	return findfile(repsuff(dis, ".dis", ".sbl"));
+}
+
+findfile(s: string): string
+{
+	if(exists(s))
+		return s;
+	if(s != nil && s[0] != '/'){
+		if(workdir == nil)
+			workdir = load Workdir Workdir->PATH;
+		if(workdir == nil){
+			error("cannot load Workdir module");
+			return nil;
+		}
+		s = workdir->init() + "/" + s;
+	}
+	(d, f) := split(s, '/');
+	(fp, nil) := split(f, '.');
+	if(fp != nil)
+		fp = fp[0: len fp - 1];
+	for(k := 0; k < 2; k++){
+		if(k == 0)
+			str := s;
+		else
+			str = d;
+		ls := len str;
+		for(i := 0; i < len bspath; i++){
+			(dis, src) := bspath[i];
+			ld := len dis;
+			if(ls >= ld && str[:ld] == dis){
+				if(k == 0)
+					ns := src + str[ld:];
+				else
+					ns = src + str[ld:] + fp + "/" + f;
+				if(exists(ns))
+					return ns;
+			}
+		}
+	}
+	return nil;
+}
+
+split(s: string, c: int): (string, string)
+{
+	for(i := len s - 1; i >= 0; --i)
+		if(s[i] == c)
+			break;
+	return (s[0:i+1], s[i+1:]);
+}
+
+rev(llist: list of (list of (int, int, int), string)): list of (list of (int, int, int), string)
+{
+	r: list of (list of (int, int, int), string);
+
+	for(l := llist; l != nil; l = tl l)
+		r = hd l :: r;
+	return r;
+}
+
+mprofile(fd: ref Sys->FD, dis: string): (string, array of int, array of Funprof, int, int)
+{
+	sbl := findsbl(dis);
+	if(sbl == nil){
+		error0(sys->sprint("cannot locate symbol table file for %s", dis));
+		return (nil, nil, nil, 0, 0);
+	}
+	(sym, err) := debug->sym(sbl);
+	if(sym == nil){
+		error0(sys->sprint("bad symbol table file: %s", err));
+		return (nil, nil, nil, 0, 0);
+	}
+	nlines := 0;
+	nl := len sym.src;
+	for(i := 0; i < nl; i++){
+		if((l := sym.src[i].stop.line) > nlines)
+			nlines = l;
+	}
+	name := sym.src[0].start.file;
+	nl0 := 2*(nlines+1);
+	line := array[nl0] of int;
+	for(i = 0; i < nl0; i++)
+		line[i] = 0;
+	nf := len sym.fns;
+	fun := array[nf] of Funprof;
+	for(i = 0; i < nf; i++){
+		fun[i].name = sym.fns[i].name;
+		# src seems to be always nil
+		# fun[i].file = sym.fns[i].src.start.file;
+		# fun[i].line = (sym.fns[i].src.start.line+sym.fns[i].src.stop.line)/2;
+		src := sym.pctosrc(sym.fns[i].offset);
+		if(src != nil)
+			fun[i].line = src.start.line;
+		else
+			fun[i].line = 0;
+		fun[i].count = fun[i].counte = 0;
+	}
+	buf := array[32] of byte;
+	# pc := 0;
+	ktot := ktot1 := 0;
+	fi := 0;
+# for(i=0; i < nl; i++) sys->print("%d -> %d\n", i, sym.pctosrc(i).start.line);
+	while((m := sys->read(fd, buf, len buf)) > 0){
+		(nw, lw) := sys->tokenize(string buf[0:m], " ");
+		if(nw != 2){
+			error0("bad histogram data");
+			return  (nil, nil, nil, 0, 0);
+		}
+		pc := int hd lw;
+		f := int hd tl lw;
+		if(pc == 0){
+			ktot = f;
+			continue;
+		}
+		if(pc == 1){
+			ktot1 = f;
+			continue;
+		}
+		pc -= 2;
+		t := pc&1;
+		pc /= 2;
+		rpc := pc-1;
+		src := sym.pctosrc(rpc);
+		if(src == nil)
+			continue;
+		l1 := src.start.line;
+		l2 := src.stop.line;
+		if(l1 == 0 || l2 == 0)
+			continue;
+		if((nl = l2-l1+1) == 1)
+			line[2*l1+t] += f;
+		else{
+			q := f/nl;
+			r := f-q*nl;
+			for(i = l1; i <= l2; i++)
+				line[2*i+t] += q+(r-->0);
+		}
+		if(fi < nf){
+			if(rpc >= sym.fns[fi].offset && rpc < sym.fns[fi].stoppc){
+				if(t)
+					fun[fi].counte += f;
+				else
+					fun[fi].count += f;
+			}
+			else{
+				while(fi < nf && rpc >= sym.fns[fi].stoppc)
+					fi++;
+				# fi++;
+				if(fi >= nf && f != 0)
+					error0(sys->sprint("bad fn index"));
+				if(fi < nf){
+					if(t)
+						fun[fi].counte += f;
+					else
+						fun[fi].count += f;
+				}
+			}
+		}
+# sys->print("pc %d count %d l1 %d l2 %d\n", rpc, f, l1, l2);
+	}
+	return (name, line, fun, ktot, ktot1);
+}
+
+memshow(p: Prof, v: int): int
+{
+	i: int;
+
+	cleare();
+	tot := p.total;
+	if(p.total == 0 && p.totals[0] == 0)
+		return 0;
+	verbose := v&VERBOSE;
+	fullhdr := v&FULLHDR;
+	for(ml := p.mods; ml != nil; ml = tl ml){
+		mp := hd ml;
+		if(mp.total == 0 && mp.totals[0] == 0)
+			continue;
+		if((b := getb(mp.path)) == nil)
+			continue;
+		sys->print("\nModule: %s(%s)\n\n", mp.name, mp.path);
+		line := mp.linetab;
+		if(v&LINE){
+			bio := bufio->open(b, Bufio->OREAD);
+			if(bio == nil){
+				error(sys->sprint("cannot open %s for reading", b));
+				continue;
+			}
+			i = 1;
+			ll := len line/2;
+			while((s := bio.gets('\n')) != nil){
+				f := g := 0;
+				if(i < ll){
+					f = line[2*i];
+					g = line[2*i+1];
+				}
+				if(verbose || f != 0 || g != 0){
+					if(fullhdr)
+						sys->print("%s:", b);
+					sys->print("%d\t%d\t%d\t%s", i, f, g, s);
+				}
+				i++;
+			}
+			if(v&(FUNCTION|MODULE))
+				sys->print("\n");
+		}
+		if(v&FUNCTION){
+			fun := mp.funtab;
+			nf := len fun;
+			for(i = 0; i < nf; i++)
+				if(verbose || fun[i].count != 0 || fun[i].counte != 0){
+					if(fullhdr)
+						sys->print("%s:", b);
+					sys->print("%d\t%d\t%d\t%s()\n", fun[i].line, fun[i].count, fun[i].counte, fun[i].name);
+			}
+			if(v&MODULE)
+				sys->print("\n");
+		}
+		if(v&MODULE)
+			sys->print("Module totals\t%d\t%d\n\n", mp.total, mp.totals[0]);
+	}
+	if(p.mods != nil && tl p.mods != nil)
+		sys->print("Grand totals\t%d\t%d\n\n", p.total, p.totals[0]);
+	return 0;
+}
--- /dev/null
+++ b/appl/lib/pslib.b
@@ -1,0 +1,714 @@
+implement Pslib;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw : Draw;
+Image, Display,Rect,Point : import draw;
+
+include "bufio.m";
+	bufmod : Bufio;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+Iobuf : import bufmod;
+
+include "string.m";
+	str : String;
+
+include "daytime.m";
+	time : Daytime;
+
+include "pslib.m";
+
+# old module declaration.
+# this whole thing needs a revamp, so almost all the old external
+# linkages have been removed until there's time to do it properly.
+#Pslib : module 
+#{
+#	PATH:		con "/dis/lib/pslib.dis";
+#
+#	init:	fn(env: ref Draw->Context, t: ref Tk->Toplevel, boxes: int, deb: int): string;
+#	getfonts:		fn(input: string): string;
+#	preamble:		fn(ioutb: ref Bufio->Iobuf, bbox: Draw->Rect): string;
+#	trailer:		fn(ioutb: ref Bufio->Iobuf, pages: int): string;
+#	printnewpage:	fn(pagenum: int, end: int, ioutb: ref Bufio->Iobuf);
+#	parseTkline:	fn(ioutb: ref Bufio->Iobuf, input: string): string;
+#	stats:		fn(): (int, int, int);
+#	deffont:		fn(): string;
+#	image2psfile:	fn(ioutb: ref Bufio->Iobuf, im: ref Draw->Image, dpi: int) : string;
+#};
+
+ASCII,RUNE,IMAGE : con iota;
+
+Iteminfo : adt
+{
+	itype: int;
+	offset: int;		# offset from the start of line.
+	width: int;		# width....
+	ascent: int;	# ascent of the item
+	font: int;		# font 
+	line : int;		# line its on
+	buf : string;	
+};
+
+Lineinfo : adt
+{
+	xorg: int;
+	yorg: int;
+	width: int;
+	height: int;
+	ascent: int;
+};
+
+
+font_arr := array[256] of {* => (-1,"")};
+remap := array[20] of (string,string);
+
+PXPI : con 100;
+PTPI : con 100;
+
+boxes: int;
+debug: int;
+totitems: int;
+totlines: int;
+curfont: int;
+def_font: string;
+def_font_type: int;
+curfonttype: int;
+pagestart: int;
+ctxt: ref Draw->Context;
+t: ref Toplevel;
+
+nomod(s: string)
+{
+	sys->print("pslib: cannot load %s: %r\n", s);
+	raise "fail:bad module";
+}
+
+init(bufio: Bufio)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk= load Tk Tk->PATH;
+	if (tk == nil)
+		nomod(Tk->PATH);
+	str = load String String->PATH;
+	if (str == nil)
+		nomod(String->PATH);
+	bufmod = bufio;
+}
+
+
+oldinit(env: ref Draw->Context, d: ref Toplevel, nil: int,deb: int): string
+{
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	draw = load Draw Draw->PATH;
+	tk= load Tk Tk->PATH;
+	bufmod = load Bufio Bufio->PATH;
+	ctxt=env;
+	t=d;
+	debug = deb;
+	totlines=0;
+	totitems=0;
+	pagestart=0;
+	boxes=0; #box;
+	curfont=0;
+	e := loadfonts();
+	if (e != "")
+		return e;
+	return "";
+}
+
+stats(): (int,int,int)
+{
+	return (totitems,totlines,curfont);
+}
+
+loadfonts() : string
+{
+	input : string;
+	iob:=bufmod->open("/fonts/psrename",bufmod->OREAD);
+	if (iob==nil)
+		return sys->sprint("can't open /fonts/psrename: %r");
+	i:=0;
+	while((input=iob.gets('\n'))!=nil){
+		(tkfont,psfont):=str->splitl(input," ");
+		psfont=psfont[1:len psfont -1];
+		remap[i]=(tkfont,psfont);
+		i++;
+	}
+	return "";
+}
+
+preamble(ioutb: ref Iobuf, bb: Rect)
+{
+	time = load Daytime Daytime->PATH;
+	username := "";
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd != nil) {
+		b := array[128] of byte;
+		n := sys->read(fd, b, len b);
+		b=b[0:n];
+		username = string b;
+		fd = nil;
+	}
+	if(bb.max.x == 0 && bb.max.y == 0) {
+		bb.max.x = 612;
+		bb.max.y = 792;
+	}
+	ioutb.puts("%!PS-Adobe-3.0\n");
+	ioutb.puts(sys->sprint("%%%%Creator: Pslib 1.0 (%s)\n",username));
+	ioutb.puts(sys->sprint("%%%%CreationDate: %s\n",time->time()));
+	ioutb.puts("%%Pages: (atend) \n");
+	ioutb.puts(sys->sprint("%%%%BoundingBox: %d %d %d %d\n", bb.min.x, bb.min.y, bb.max.x, bb.max.y));
+	ioutb.puts("%%EndComments\n");
+	ioutb.puts("%%BeginProlog\n");
+	ioutb.puts("/doimage {\n");
+	ioutb.puts("/bps exch def\n");
+	ioutb.puts("/width exch def\n");
+	ioutb.puts("/height exch def\n");
+	ioutb.puts("/xstart exch def\n");
+	ioutb.puts("/ystart exch def\n");
+	ioutb.puts("/iwidth exch def\n");
+	ioutb.puts("/ascent exch def\n");
+	ioutb.puts("/iheight exch def\n");
+	ioutb.puts("gsave\n");
+	if(boxes)
+		ioutb.puts("xstart ystart iwidth iheight rectstroke\n");
+	# if bps==8, use inferno colormap; else (bps < 8) it's grayscale
+	ioutb.puts("bps 8 eq\n");
+	ioutb.puts("{\n");
+	ioutb.puts("[/Indexed /DeviceRGB 255 \n");
+	ioutb.puts("<ffffff ffffaa ffff55 ffff00 ffaaff ffaaaa ffaa55 ffaa00 ff55ff ff55aa ff5555 ff5500\n");
+	ioutb.puts("ff00ff ff00aa ff0055 ff0000 ee0000 eeeeee eeee9e eeee4f eeee00 ee9eee ee9e9e ee9e4f\n");
+	ioutb.puts("ee9e00 ee4fee ee4f9e ee4f4f ee4f00 ee00ee ee009e ee004f dd0049 dd0000 dddddd dddd93\n");
+	ioutb.puts("dddd49 dddd00 dd93dd dd9393 dd9349 dd9300 dd49dd dd4993 dd4949 dd4900 dd00dd dd0093\n");
+	ioutb.puts("cc0088 cc0044 cc0000 cccccc cccc88 cccc44 cccc00 cc88cc cc8888 cc8844 cc8800 cc44cc\n");
+	ioutb.puts("cc4488 cc4444 cc4400 cc00cc aaffaa aaff55 aaff00 aaaaff bbbbbb bbbb5d bbbb00 aa55ff\n");
+	ioutb.puts("bb5dbb bb5d5d bb5d00 aa00ff bb00bb bb005d bb0000 aaffff 9eeeee 9eee9e 9eee4f 9eee00\n");
+	ioutb.puts("9e9eee aaaaaa aaaa55 aaaa00 9e4fee aa55aa aa5555 aa5500 9e00ee aa00aa aa0055 aa0000\n");
+	ioutb.puts("990000 93dddd 93dd93 93dd49 93dd00 9393dd 999999 99994c 999900 9349dd 994c99 994c4c\n");
+	ioutb.puts("994c00 9300dd 990099 99004c 880044 880000 88cccc 88cc88 88cc44 88cc00 8888cc 888888\n");
+	ioutb.puts("888844 888800 8844cc 884488 884444 884400 8800cc 880088 55ff55 55ff00 55aaff 5dbbbb\n");
+	ioutb.puts("5dbb5d 5dbb00 5555ff 5d5dbb 777777 777700 5500ff 5d00bb 770077 770000 55ffff 55ffaa\n");
+	ioutb.puts("4fee9e 4fee4f 4fee00 4f9eee 55aaaa 55aa55 55aa00 4f4fee 5555aa 666666 666600 4f00ee\n");
+	ioutb.puts("5500aa 660066 660000 4feeee 49dddd 49dd93 49dd49 49dd00 4993dd 4c9999 4c994c 4c9900\n");
+	ioutb.puts("4949dd 4c4c99 555555 555500 4900dd 4c0099 550055 550000 440000 44cccc 44cc88 44cc44\n");
+	ioutb.puts("44cc00 4488cc 448888 448844 448800 4444cc 444488 444444 444400 4400cc 440088 440044\n");
+	ioutb.puts("00ff00 00aaff 00bbbb 00bb5d 00bb00 0055ff 005dbb 007777 007700 0000ff 0000bb 000077\n");
+	ioutb.puts("333333 00ffff 00ffaa 00ff55 00ee4f 00ee00 009eee 00aaaa 00aa55 00aa00 004fee 0055aa\n");
+	ioutb.puts("006666 006600 0000ee 0000aa 000066 222222 00eeee 00ee9e 00dd93 00dd49 00dd00 0093dd\n");
+	ioutb.puts("009999 00994c 009900 0049dd 004c99 005555 005500 0000dd 000099 000055 111111 00dddd\n");
+	ioutb.puts("00cccc 00cc88 00cc44 00cc00 0088cc 008888 008844 008800 0044cc 004488 004444 004400\n");
+	ioutb.puts("0000cc 000088 000044 000000>\n");
+	ioutb.puts("] setcolorspace\n");
+	ioutb.puts("/decodemat [0 255] def\n");
+	ioutb.puts("}\n");
+	# else, bps != 8
+	ioutb.puts("{\n");
+	ioutb.puts("[/DeviceGray] setcolorspace\n");
+	ioutb.puts("/decodemat [1 0] def\n");
+	ioutb.puts("}\n");
+	ioutb.puts("ifelse\n");
+	ioutb.puts("xstart ystart translate \n");
+	ioutb.puts("iwidth iheight scale \n");
+	ioutb.puts("<<\n");
+	ioutb.puts("/ImageType 1\n");
+	ioutb.puts("/Width width \n");
+	ioutb.puts("/Height height \n");
+	ioutb.puts("/BitsPerComponent bps %bits/sample\n");
+	ioutb.puts("/Decode decodemat % Inferno cmap or DeviceGray value\n");
+	ioutb.puts("/ImageMatrix [width 0 0 height neg 0 height]\n");
+	ioutb.puts("/DataSource currentfile /ASCII85Decode filter\n");
+	ioutb.puts(">> \n");
+	ioutb.puts("image\n");
+	ioutb.puts("grestore\n");
+	ioutb.puts("} def\n");
+	ioutb.puts("%%EndProlog\n");	
+}
+
+trailer(ioutb : ref Iobuf,pages : int)
+{
+	ioutb.puts("%%Trailer\n%%Pages: "+string pages+"\n%%EOF\n");
+}
+
+
+printnewpage(pagenum : int,end : int, ioutb : ref Iobuf)
+{
+	pnum:=string pagenum;
+	if (end){			
+		# bounding box
+		if (boxes){
+			ioutb.puts("18 18 moveto 594 18 lineto 594 774 lineto 18 774 lineto"+
+								" closepath stroke\n");
+		}
+		ioutb.puts("showpage\n%%EndPage "+pnum+" "+pnum+"\n");
+	} else 
+		ioutb.puts("%%Page: "+pnum+" "+pnum+"\n");
+}
+
+printimage(ioutb: ref Iobuf, line: Lineinfo, imag: Iteminfo): (string,string)
+{
+	RM:=612-18;
+	class:=tk->cmd(t,"winfo class "+imag.buf);
+#sys->print("Looking for [%s] of type [%s]\n",imag.buf,class);
+	if (line.xorg+imag.offset+imag.width>RM)
+		imag.width=RM-line.xorg-imag.offset;
+	case class {
+		"button" or "menubutton" =>
+			# try to get the text out and print it....
+			ioutb.puts(sys->sprint("%d %d moveto\n",line.xorg+imag.offset,
+							line.yorg));
+			msg:=tk->cmd(t,sys->sprint("%s cget -text",imag.buf));
+			ft:=tk->cmd(t,sys->sprint("%s cget -font",imag.buf));
+			sys->print("font is [%s]\n",ft);
+			ioutb.puts(sys->sprint("%d %d %d %d rectstroke\n",
+						line.xorg+imag.offset,line.yorg,imag.width,
+						line.height));
+			return (class,msg);
+		"label" =>
+			(im,nil,nil) := tk->getimage(t,imag.buf);
+			if (im!=nil){
+				bps := im.depth;
+				ioutb.puts(sys->sprint("%d %d %d %d %d %d %d %d doimage\n",
+						im.r.dy(),line.ascent,im.r.dx(),line.yorg,
+						line.xorg+imag.offset,im.r.dy(), im.r.dx(), bps));
+				imagebits(ioutb,im);
+			}
+			return (class,"");
+		"entry" =>
+			ioutb.puts(sys->sprint("%d %d moveto\n",line.xorg+imag.offset,
+					line.yorg));
+			ioutb.puts(sys->sprint("%d %d %d %d rectstroke\n",
+					line.xorg+imag.offset,line.yorg,imag.width,
+					line.height));
+			return (class,"");
+		* =>
+			sys->print("Unhandled class [%s]\n",class);
+			return (class,"Error");
+		
+	}
+	return ("","");	
+}
+
+printline(ioutb: ref Iobuf,line : Lineinfo,items : array of Iteminfo)
+{
+	xstart:=line.xorg;
+	wid:=xstart;
+	# items
+	if (len items == 0) return;
+	for(j:=0;j<len items;j++){
+		msg:="";
+		class:="";
+		if (items[j].itype==IMAGE)
+			(class,msg)=printimage(ioutb,line,items[j]);
+		if (items[j].itype!=IMAGE || class=="button"|| class=="menubutton"){
+			setfont(ioutb,items[j].font);
+			if (msg!=""){ 
+				# position the text in the center of the label
+				# moveto curpoint
+				# (msg) stringwidth pop xstart sub 2 div
+				ioutb.puts(sys->sprint("%d %d moveto\n",xstart+items[j].offset,
+						line.yorg+line.height-line.ascent));
+				ioutb.puts(sys->sprint("(%s) dup stringwidth pop 2 div",
+								msg));
+				ioutb.puts(" 0 rmoveto show\n");
+			}
+			else {
+				ioutb.puts(sys->sprint("%d %d moveto\n",
+					xstart+items[j].offset,line.yorg+line.height
+					-line.ascent));
+				ioutb.puts(sys->sprint("(%s) show\n",items[j].buf));
+			}
+		}
+		wid=xstart+items[j].offset+items[j].width;
+	}
+	if (boxes)
+		ioutb.puts(sys->sprint("%d %d %d %d rectstroke\n",line.xorg,line.yorg,
+									wid,line.height));
+}
+
+setfont(ioutb: ref Iobuf, font: int)
+{
+	ftype : int;
+	fname : string;
+	if ((curfonttype & font) != curfonttype){
+		for(f:=0;f<curfont;f++){
+			(ftype,fname)=font_arr[f];
+				if ((ftype&font)==ftype)
+					break;
+		}
+		if (f==curfont){
+			fname=def_font;
+			ftype=def_font_type;
+		}
+		ioutb.puts(sys->sprint("%s setfont\n",fname));
+		curfonttype=ftype;
+	}
+}
+	
+parseTkline(ioutb: ref Iobuf, input: string): string
+{
+	thisline : Lineinfo;
+	PS:=792-18-18;	# page size in points	
+	TM:=792-18;	# top margin in points
+	LM:=18;		# left margin 1/4 in. in
+#	BM:=18;		# bottom margin 1/4 in. in
+	x : int;
+	(x,input)=str->toint(input,10);
+	thisline.xorg=(x*PTPI)/PXPI;
+	(x,input)=str->toint(input,10);
+	thisline.yorg=(x*PTPI)/PXPI;
+	(x,input)=str->toint(input,10);
+	thisline.width=(x*PTPI)/PXPI;
+	(x,input)=str->toint(input,10);
+	thisline.height=(x*PTPI)/PXPI;
+	(x,input)=str->toint(input,10);
+	thisline.ascent=(x*PTPI)/PXPI;
+	(x,input)=str->toint(input,10);
+	# thisline.numitems=x;
+	if (thisline.width==0 || thisline.height==0)
+		return "";
+	if (thisline.yorg+thisline.height-pagestart>PS){
+		pagestart=thisline.yorg;
+		return "newpage";
+		# must resend this line....
+	}
+	thisline.yorg=TM-thisline.yorg-thisline.height+pagestart;
+	thisline.xorg+=LM;
+	(items, err) :=getline(totlines,input);
+	if(err != nil)
+		return err;
+	totitems+=len items;
+	totlines++;
+	printline(ioutb,thisline,items);
+	return "";
+}
+
+getfonts(input: string) : string
+{
+	tkfont,psfont : string;
+	j : int;
+	retval := "";
+	if (input[0]=='%')
+			return "";
+	# get a line of the form 
+	# 5::/fonts/lucida/moo.16.font
+	# translate it to...
+	# 32 f32.16
+	# where 32==1<<5 and f32.16 is a postscript function that loads the 
+	# appropriate postscript font (from remap)
+	# and writes it to fonts....
+	(bits,font):=str->toint(input,10);
+	if (bits!=-1)
+		bits=1<<bits;
+	else{
+		bits=1;
+		def_font_type=bits;
+		curfonttype=def_font_type;
+	}
+	font=font[2:];
+	for(i:=0;i<len remap;i++){
+		(tkfont,psfont)=remap[i];
+		if (tkfont==font)
+			break;
+	}
+	if (i==len remap)
+		psfont="Times-Roman";
+	(font,nil)=str->splitr(font,".");
+	(nil,font)=str->splitr(font[0:len font-1],".");
+	(fsize,nil):=str->toint(font,10);
+	fsize=(PTPI*3*fsize)/(2*PXPI);
+	enc_font:="f"+string bits+"."+string fsize;
+	ps_func:="/"+enc_font+" /"+psfont+" findfont "+string fsize+
+							" scalefont def\n";
+	sy_font:="sy"+string fsize;
+	xtra_func:="/"+sy_font+" /Symbol findfont "+string fsize+
+							" scalefont def\n";
+	for(i=0;i<len font_arr;i++){
+		(j,font)=font_arr[i];
+		if (j==-1) break;
+	}
+	if (j==len font_arr)
+		return "Error";
+	font_arr[i]=(bits,enc_font);
+	if (bits==1)
+		def_font=enc_font;
+	curfont++;
+	retval+= ps_func;
+	retval+= xtra_func;	
+	return retval;
+}
+
+deffont() : string
+{
+	return def_font;
+}
+	
+getline(k : int,  input : string) : (array of Iteminfo, string)
+{
+	lineval,args : string;
+	j, nb : int;
+	lw:=0;
+	wid:=0;
+	flags:=0;
+	item_arr := array[32] of {* => Iteminfo(-1,-1,-1,-1,-1,-1,"")};
+	curitem:=0;
+	while(input!=nil){
+		(nil,input)=str->splitl(input,"[");
+		if (input==nil)
+			break;
+		com:=input[1];
+		input=input[2:];
+		case com {
+		'A' =>
+			nb=0;
+			# get the width of the item
+			(wid,input)=str->toint(input,10);
+			wid=(wid*PTPI)/PXPI;
+			if (input[0]!='{')
+				return (nil, sys->sprint(
+					"line %d item %d Bad Syntax : '{' expected",
+						k,curitem));
+			# get the args.
+			(args,input)=str->splitl(input,"}");
+			# get the flags.
+			# assume there is only one int flag..
+			(flags,args)=str->toint(args[1:],16);
+			if (args!=nil && debug){
+				sys->print("line %d item %d extra flags=%s\n",
+						k,curitem,args);
+			}
+			if (flags<1024) flags=1;
+			item_arr[curitem].font=flags;
+			item_arr[curitem].offset=lw;
+			item_arr[curitem].width=wid;
+			lw+=wid;
+			for(j=1;j<len input;j++){
+				if ((input[j]==')')||(input[j]=='('))
+						lineval[len lineval]='\\';
+				if (input[j]=='[')
+					nb++;
+				if (input[j]==']')
+					if (nb==0)
+						break;
+					else 
+						nb--;
+				lineval[len lineval]=input[j];
+			}
+			if (j<len input)
+				input=input[j:];
+			item_arr[curitem].buf=lineval;
+			item_arr[curitem].line=k;
+			item_arr[curitem].itype=ASCII;
+			curitem++;
+			lineval="";
+		'R' =>
+			nb=0;
+			# get the width of the item
+			(wid,input)=str->toint(input,10);
+			wid=(wid*PTPI)/PXPI;
+			if (input[0]!='{')
+				return (nil, "Bad Syntax : '{' expected");
+			# get the args.
+			(args,input)=str->splitl(input,"}");
+			# get the flags.
+			# assume there is only one int flag..
+			(flags,args)=str->toint(args[1:],16);
+			if (args!=nil && debug){
+				sys->print("line %d item %d Bad Syntax args=%s",
+						k,curitem,args);
+			}
+			item_arr[curitem].font=flags;
+			item_arr[curitem].offset=lw;
+			item_arr[curitem].width=wid;
+			lw+=wid;
+			for(j=1;j<len input;j++){
+				if (input[j]=='[')
+					nb++;
+				if (input[j]==']')
+					if (nb==0)
+						break;
+					else 
+						nb--;
+				case input[j] {
+					8226 => # bullet
+						lineval+="\\267 ";
+					169 =>  # copyright
+						lineval+="\\251 ";
+						curitem++;			
+					* =>
+						lineval[len lineval]=input[j];
+				}
+			}
+			if (j>len input)
+				input=input[j:];
+			item_arr[curitem].buf=lineval;
+			item_arr[curitem].line=k;
+			item_arr[curitem].itype=RUNE;
+			curitem++;
+			lineval="";
+		'N' or 'C'=>
+			# next item
+			for(j=0;j<len input;j++)
+				if (input[j]==']')
+					break;
+			if (j>len input)
+				input=input[j:];
+		'T' =>
+			(wid,input)=str->toint(input,10);
+			wid=(wid*PTPI)/PXPI;
+			item_arr[curitem].offset=lw;
+			item_arr[curitem].width=wid;
+			lw+=wid;
+			lineval[len lineval]='\t';
+			# next item
+			for(j=0;j<len input;j++)
+				if (input[j]==']')
+					break;
+			if (j>len input)
+				input=input[j:];
+			item_arr[curitem].buf=lineval;
+			item_arr[curitem].line=k;
+			item_arr[curitem].itype=ASCII;
+			curitem++;
+			lineval="";
+		'W' =>
+			(wid,input)=str->toint(input,10);
+			wid=(wid*PTPI)/PXPI;
+			item_arr[curitem].offset=lw;
+			item_arr[curitem].width=wid;
+			item_arr[curitem].itype=IMAGE;
+			lw+=wid;
+			# next item
+			for(j=1;j<len input;j++){
+				if (input[j]==']')
+					break;
+				lineval[len lineval]=input[j];
+			}
+			item_arr[curitem].buf=lineval;
+			if (j>len input)
+				input=input[j:];
+			curitem++;
+			lineval="";
+		* =>
+			# next item
+			for(j=0;j<len input;j++)
+				if (input[j]==']')
+					break;
+			if (j>len input)
+				input=input[j:];
+				
+		}
+	}
+	return (item_arr[0:curitem], "");	
+}
+
+writeimage(ioutb: ref Iobuf, im: ref Draw->Image, dpi: int)
+{
+	r := im.r;
+	width := r.dx();
+	height := r.dy();
+	iwidth := width * 72 / dpi;
+	iheight := height * 72 / dpi;
+	xstart := 72;
+	ystart := 720 - iheight;
+	bbox := Rect((xstart,ystart), (xstart+iwidth,ystart+iheight));
+	preamble(ioutb, bbox);
+	ioutb.puts("%%Page: 1\n%%BeginPageSetup\n");
+	ioutb.puts("/pgsave save def\n");
+	ioutb.puts("%%EndPageSetup\n");
+	bps := im.depth;
+	ioutb.puts(sys->sprint("%d 0 %d %d %d %d %d %d doimage\n", iheight, iwidth, ystart, xstart, height, width, bps));
+	imagebits(ioutb, im);
+	ioutb.puts("pgsave restore\nshowpage\n");
+	trailer(ioutb, 1);
+	ioutb.flush();
+}
+
+imagebits(ioutb: ref Iobuf, im: ref Draw->Image)
+{
+	if(debug)
+		sys->print("imagebits, r=%d %d %d %d, depth=%d\n",
+			im.r.min.x, im.r.min.y, im.r.max.x, im.r.max.y, im.depth);
+	width:=im.r.dx();
+	height:=im.r.dy();
+	bps:=im.depth;	# bits per sample
+	spb := 1;			# samples per byte
+	bitoff := 0;			# bit offset of beginning sample within first byte
+	linebytes := width;
+	if(bps < 8) {
+		spb=8/bps;
+		bitoff=(im.r.min.x % spb) * bps;
+		linebytes=(bitoff + (width-1)*bps) / 8 + 1;
+	}
+	arr:=array[linebytes*height] of byte;
+	n:=im.readpixels(im.r,arr);
+	if(debug)
+		sys->print("linebytes=%d, height=%d, readpixels returned %d\n",
+			linebytes, height, n);
+	if(n < 0) {
+		n = len arr;
+		for(i := 0; i < n; i++)
+			arr[i] = byte 0;
+	}
+	if(bitoff != 0) {
+		# Postscript image wants beginning of line at beginning of byte
+		pslinebytes := (width-1)*bps + 1;
+		if(debug)
+			sys->print("bitoff=%d, pslinebytes=%d\n", bitoff, pslinebytes);
+		old:=arr;
+		n = pslinebytes*height;
+		arr=array[n] of byte;
+		a0 := 0;
+		o0 := 0;
+		for(y := 0; y < height; y++) {
+			for(i:=0; i < pslinebytes; i++)
+				arr[a0+i] = (old[o0+i]<<bitoff) | (old[o0+i+1]>>(8-bitoff));
+			a0 += pslinebytes;
+			o0 += linebytes;
+		}
+	}
+	lsf:=0;
+	n4 := (n/4)*4;
+	for(i:=0;i<n4;i+=4){
+		s:=cmap2ascii85(arr[i:i+4]);
+		lsf+=len s;
+		ioutb.puts(s);
+		if (lsf>74){
+		  ioutb.puts("\n");
+		  lsf=0;
+		}
+	}
+	nrest:=n-n4;
+	if(nrest!=0){
+		foo:=array[4] of {* => byte 0};
+		foo[0:]=arr[n4:n];
+		s:=cmap2ascii85(foo);
+		if(s=="z")
+			s="!!!!!";
+		ioutb.puts(s[0:nrest+1]);
+	}
+	ioutb.puts("~>\n");
+	ioutb.flush();
+}
+
+
+cmap2ascii85(arr : array of byte) : string
+{
+	b := array[4] of {* => big 0};
+	for(i:=0;i<4;i++)
+		b[i]=big arr[i];
+	i1:=(b[0]<<24)+(b[1]<<16)+(b[2]<<8)+b[3];
+	c1:=sys->sprint("%c%c%c%c%c",'!'+int ((i1/big (85*85*85*85))%big 85),
+					'!'+int ((i1/big (85*85*85))%big 85),
+					'!'+int ((i1/big (85*85))% big 85),
+					'!'+int ((i1/big 85)% big 85),'!'+int(i1% big 85));
+	if (c1=="!!!!!") c1="z";
+	return c1;
+}
--- /dev/null
+++ b/appl/lib/quicktime.b
@@ -1,0 +1,205 @@
+implement QuickTime;
+
+include "sys.m";
+
+sys: Sys;
+
+include "quicktime.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+open(file: string): (ref QD, string)
+{
+	fd := sys->open(file, sys->OREAD);
+	if(fd == nil)
+		return (nil, "open failed");
+
+	r := ref QD;
+	r.fd = fd;
+	r.buf = array[DEFBUF] of byte;
+
+	(hdr, l) := r.atomhdr();
+	if(hdr != "mdat")
+		return (nil, "not a QuickTime movie file");
+
+	#
+	# We are expecting a unified file with .data then .rsrc
+	#
+	r.skipatom(l);
+
+	return (r, nil);
+}
+
+QD.atomhdr(r: self ref QD): (string, int)
+{
+	b := array[8] of byte;
+
+	if(r.readn(b, 8) != 8)
+		return (nil, -1);
+
+for(i := 0; i < 8; i++)
+sys->print("%.2ux ", int b[i]);
+sys->print(" %s %d\n", string b[4:8], bedword(b, 0));
+
+	return (string b[4:8], bedword(b, 0));
+}
+
+QD.skipatom(r: self ref QD, l: int): int
+{
+	return r.skip(l - AtomHDR);
+}
+
+QD.mvhd(q: self ref QD, l: int): string
+{
+	l -= AtomHDR;
+	if(l != MvhdrSIZE)
+		return "mvhd atom funny size";
+
+	b := array[l] of byte;
+	if(q.readn(b, l) != l)
+		return "short read in mvhd";
+
+	mvhdr := ref Mvhdr;
+
+	mvhdr.version = bedword(b, 0);
+	mvhdr.create = bedword(b, 4);
+	mvhdr.modtime = bedword(b, 8);
+	mvhdr.timescale = bedword(b, 12);
+	mvhdr.duration = bedword(b, 16);
+	mvhdr.rate = bedword(b, 20);
+	mvhdr.vol = beword(b, 24);
+	mvhdr.r1 = bedword(b, 26);
+	mvhdr.r2 = bedword(b, 30);
+
+	mvhdr.matrix = array[9] of int;
+	for(i :=0; i<9; i++)
+		mvhdr.matrix[i] = bedword(b, 34+i*4);
+
+	mvhdr.r3 = beword(b, 70);
+	mvhdr.r4 = bedword(b, 72);
+	mvhdr.pvtime = bedword(b, 76);
+	mvhdr.posttime = bedword(b, 80);
+	mvhdr.seltime = bedword(b, 84);
+	mvhdr.seldurat = bedword(b, 88);
+	mvhdr.curtime = bedword(b, 92);
+	mvhdr.nxttkid = bedword(b, 96);
+
+	q.mvhdr = mvhdr;
+	return nil;
+}
+
+QD.trak(q: self ref QD, l: int): string
+{
+	(tk, tkl) := q.atomhdr();
+	if(tk != "tkhd")
+		return "missing track header atom";
+
+	l -= tkl;
+	tkl -= AtomHDR;
+	b := array[tkl] of byte;
+	if(q.readn(b, tkl) != tkl)
+		return "short read in tkhd";
+
+	tkhdr := ref Tkhdr;
+
+	tkhdr.version =	bedword(b, 0);
+	tkhdr.creation = bedword(b, 4);
+	tkhdr.modtime =	bedword(b, 8);
+	tkhdr.trackid =	bedword(b, 12);
+	tkhdr.timescale = bedword(b, 16);
+	tkhdr.duration = bedword(b, 20);
+	tkhdr.timeoff = bedword(b, 24);
+	tkhdr.priority = bedword(b, 28);
+	tkhdr.layer = beword(b, 32);
+	tkhdr.altgrp = beword(b, 34);
+	tkhdr.volume = beword(b, 36);
+
+	tkhdr.matrix = array[9] of int;
+	for(i := 0; i < 9; i++)
+		tkhdr.matrix[i] = bedword(b, 38+i*4);
+
+	tkhdr.width = bedword(b, 74);
+	tkhdr.height = bedword(b, 78);
+
+	(md, mdl) := q.atomhdr();
+	if(md != "mdia")
+		return "missing media atom";
+
+	while(mdl != AtomHDR) {
+		(atom, atoml) := q.atomhdr();
+sys->print("\t%s %d\n", atom, atoml);
+		q.skipatom(atoml);
+
+		mdl -= atoml;
+	}
+
+	return nil;
+}
+
+QD.readn(r: self ref QD, b: array of byte, l: int): int
+{
+	if(r.nbyte < l) {
+		c := 0;
+		if(r.nbyte != 0) {
+			b[0:] = r.buf[r.ptr:];
+			l -= r.nbyte;
+			c += r.nbyte;
+			b = b[r.nbyte:];
+		}
+		bsize := len r.buf;
+		while(l != 0) {
+			r.nbyte = sys->read(r.fd, r.buf, bsize);
+			if(r.nbyte <= 0) {
+				r.nbyte = 0;
+				return -1;
+			}
+			n := l;
+			if(n > bsize)
+				n = bsize;
+
+			r.ptr = 0;
+			b[0:] = r.buf[0:n];
+			b = b[n:];
+			r.nbyte -= n;
+			r.ptr += n;
+			l -= n;
+			c += n;
+		}
+		return c;
+	}
+	b[0:] = r.buf[r.ptr:r.ptr+l];
+	r.nbyte -= l;
+	r.ptr += l;
+	return l;
+}
+
+QD.skip(r: self ref QD, size: int): int
+{
+	if(r.nbyte != 0) {
+		n := size;
+		if(n > r.nbyte)
+			n = r.nbyte;
+		r.ptr += n;
+		r.nbyte -= n;
+		size -= n;
+		if(size == 0)
+			return 0;
+	}
+	return int sys->seek(r.fd, big size, sys->SEEKRELA);
+}
+
+beword(b: array of byte, o: int): int
+{
+	return 	(int b[o] << 8) | int b[o+1];
+}
+
+bedword(b: array of byte, o: int): int
+{
+	return	(int b[o] << 24) |
+		(int b[o+1] << 16) |
+		(int b[o+2] << 8) |
+		int b[o+3];
+}
--- /dev/null
+++ b/appl/lib/rabin.b
@@ -1,0 +1,85 @@
+implement Rabin;
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf, EOF, ERROR: import bufio;
+include "rabin.m";
+
+sprint: import sys;
+
+init(b: Bufio)
+{
+	sys = load Sys Sys->PATH;
+	bufio = b;
+}
+
+modpower(base, n, mod: int): int
+{
+	power := 1;
+	for(i := 0; i < n; i++)
+		power = (power * base) % mod;
+	return power;
+}
+
+Rcfg.mk(prime, width, mod: int): (ref Rcfg, string)
+{
+	rcfg := ref Rcfg(prime, width, mod, array[256] of int);
+	power := modpower(prime, width, mod);
+	for(i := 0; i < 256; i++)
+		rcfg.tab[i] = (i * power) % mod;
+	return (rcfg, nil);
+}
+
+
+open(rcfg: ref Rcfg, b: ref Iobuf, min, max: int): (ref Rfile, string)
+{
+	if(min > max)
+		return (nil, sprint("bad min/max"));
+	if(min < rcfg.width)
+		return (nil, "min < width");
+	r := ref Rfile(b, rcfg, min, max, array[max+rcfg.width] of byte, 0, 0, big 0);
+
+	(prime, width, mod) := (r.rcfg.prime, r.rcfg.width, r.rcfg.mod);
+	while(r.n < width) {
+		ch := r.b.getb();
+		if(ch == ERROR)
+			return (nil, sprint("reading: %r"));
+		if(ch == EOF)
+			break;
+		r.buf[r.n] = byte ch;
+		r.state = (prime*r.state + ch) % mod;
+		r.n++;
+	}
+	return (r, nil);
+}
+
+Rfile.read(r: self ref Rfile): (array of byte, big, string)
+{
+	(prime, width, mod) := (r.rcfg.prime, r.rcfg.width, r.rcfg.mod);
+	for(;;) {
+		ch := r.b.getb();
+		if(ch == ERROR)
+			return (nil, big 0, sprint("reading: %r"));
+		if(ch == EOF) {
+			d := r.buf[:r.n];
+			off := r.off;
+			r.n = 0;
+			r.off += big len d;
+			return (d, off, nil);
+		}
+		r.buf[r.n] = byte ch;
+		r.state = (mod+prime*r.state + ch - r.rcfg.tab[int r.buf[r.n-width]]) % mod;
+		r.n++;
+		if(r.n-width >= r.max || (r.n-width >= r.min && r.state == mod-1)) {
+			d := array[r.n-width] of byte;
+			d[:] = r.buf[:len d];
+			off := r.off;
+			r.buf[:] = r.buf[r.n-width:r.n];
+			r.n = width;
+			r.off += big len d;
+			return (d, off, nil);
+		}
+	}
+}
--- /dev/null
+++ b/appl/lib/rand.b
@@ -1,0 +1,29 @@
+implement Rand;
+
+include "rand.m";
+
+rsalt: big;
+
+init(seed: int)
+{
+	rsalt = big seed;
+}
+
+MASK: con (big 1<<63)-(big 1);
+
+rand(modulus: int): int
+{
+	rsalt = rsalt * big 1103515245 + big 12345;
+	if(modulus <= 0)
+		return 0;
+	return int (((rsalt&MASK)>>10) % big modulus);
+}
+
+# 0 < modulus < 2^53
+bigrand(modulus: big): big
+{
+	rsalt = rsalt * big 1103515245 + big 12345;
+	if(modulus <= big 0)
+		return big 0;
+	return ((rsalt&MASK)>>10) % modulus;
+}
--- /dev/null
+++ b/appl/lib/random.b
@@ -1,0 +1,50 @@
+implement Random;
+
+include "sys.m";
+include "draw.m";
+include "keyring.m";
+include "security.m";
+
+sys: Sys;
+
+randfd(which: int): ref sys->FD
+{
+	file: string;
+
+	sys = load Sys Sys->PATH;
+	case(which){
+	ReallyRandom =>
+		file = "/dev/random";
+	NotQuiteRandom =>
+		file = "/dev/notquiterandom";
+	}
+	fd := sys->open(file, sys->OREAD);
+	if(fd == nil){
+		sys->print("can't open /dev/random\n");
+		return nil;
+	}
+	return fd;
+}
+
+randomint(which: int): int
+{
+	fd := randfd(which);
+	if(fd == nil)
+		return 0;
+	buf := array[4] of byte;
+	sys->read(fd, buf, 4);
+	rand := 0;
+	for(i := 0; i < 4; i++)
+		rand = (rand<<8) | int buf[i];
+	return rand;
+}
+
+randombuf(which, n: int): array of byte
+{
+	buf := array[n] of byte;
+	fd := randfd(which);
+	if(fd == nil)
+		return buf;
+	sys->read(fd, buf, n);
+	return buf;
+}
--- /dev/null
+++ b/appl/lib/readdir.b
@@ -1,0 +1,123 @@
+implement Readdir;
+
+include "sys.m";
+	sys: Sys;
+	Dir: import sys;
+include "readdir.m";
+
+init(path: string, sortkey: int): (array of ref Dir, int)
+{
+	sys = load Sys Sys->PATH;
+	fd := sys->open(path, Sys->OREAD);
+	if(fd == nil)
+		return (nil, -1);
+	return readall(fd, sortkey);
+}
+
+readall(fd: ref Sys->FD, sortkey: int): (array of ref Dir, int)
+{
+	sys = load Sys Sys->PATH;
+	dl: list of array of Dir;
+	n := 0;
+	for(;;){
+		(nr, b) := sys->dirread(fd);
+		if(nr <= 0){
+			# any error makes the whole directory unreadable
+			if(nr < 0)
+				return (nil, -1);
+			break;
+		}
+		dl = b :: dl;
+		n += nr;
+	}
+	rl := dl;
+	for(dl = nil; rl != nil; rl = tl rl)
+		dl = hd rl :: dl;
+	a := makerefs(dl, n, sortkey & COMPACT);
+	sortkey &= ~COMPACT;
+	if((sortkey & ~DESCENDING) == NONE)
+		return (a, len a);
+	return sortdir(a, sortkey);
+}
+
+makerefs(dl: list of array of Dir, n: int, compact: int): array of ref Dir
+{
+	a := array[n] of ref Dir;
+	ht: array of list of string;
+	if(compact)
+		ht = array[41] of list of string;
+	j := 0;
+	for(; dl != nil; dl = tl dl){
+		d := hd dl;
+		for(i := 0; i < len d; i++)
+			if(ht == nil || hashadd(ht, d[i].name))
+				a[j++] = ref d[i];
+	}
+	if(j != n)
+		a = a[0:j];
+	return a;
+}
+
+sortdir(a: array of ref Dir, key: int): (array of ref Dir, int)
+{
+	mergesort(a, array[len a] of ref Dir, key);
+	return (a, len a);
+}
+	
+# mergesort because it's stable.
+mergesort(a, b: array of ref Dir, key: int)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m], key);
+		mergesort(a[m:], b[m:], key);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (greater(b[i], b[j], key))
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+greater(x, y: ref Dir, sortkey: int): int
+{
+	case (sortkey) {
+	NAME => return(x.name > y.name);
+	ATIME => return(x.atime < y.atime);
+	MTIME => return(x.mtime < y.mtime);
+	SIZE => return(x.length > y.length);
+	NAME|DESCENDING => return(x.name < y.name);
+	ATIME|DESCENDING => return(x.atime > y.atime);
+	MTIME|DESCENDING => return(x.mtime > y.mtime);
+	SIZE|DESCENDING => return(x.length < y.length);
+	}
+	return 0;
+}
+
+# from tcl_strhash.b
+hashfn(key: string, n : int): int
+{
+	h := 0;
+	for(i := 0; i < len key; i++){
+		h = 10*h + key[i];
+		h = h%n;
+	}
+	return h%n;
+}
+
+hashadd(ht: array of list of string, nm: string): int
+{
+	idx := hashfn(nm, len ht);
+	for (ent := ht[idx]; ent != nil; ent = tl ent)
+		if (hd ent == nm)
+			return 0;
+	ht[idx] = nm :: ht[idx];
+	return 1;
+}
--- /dev/null
+++ b/appl/lib/readgif.b
@@ -1,0 +1,442 @@
+implement RImagefile;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "imagefile.m";
+
+Header: adt
+{
+	fd: ref Iobuf;
+	buf: array of byte;
+	vers: string;
+	screenw: int;
+	screenh: int;
+	fields: int;
+	bgrnd: int;
+	aspect: int;
+	transp: int;
+	trindex: byte;
+};
+
+Entry: adt
+{
+	prefix: int;
+	exten: int;
+};
+
+tbl: array of Entry;
+
+init(iomod: Bufio)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	bufio = iomod;
+}
+read(fd: ref Iobuf): (ref Rawimage, string)
+{
+	(a, err) := readarray(fd, 0);
+	if(a != nil)
+		return (a[0], err);
+	return (nil, err);
+}
+
+readmulti(fd: ref Iobuf): (array of ref Rawimage, string)
+{
+	return readarray(fd, 1);
+}
+
+readarray(fd: ref Iobuf, multi: int): (array of ref Rawimage, string)
+{
+	inittbl();
+
+	buf := array[3*256] of byte;
+
+	(header, err) := readheader(fd, buf);
+	if(header == nil)
+		return (nil, err);
+
+	globalcmap: array of byte;
+	if(header.fields & 16r80){
+		(globalcmap, err) = readcmap(header, (header.fields&7)+1);
+		if(globalcmap == nil)
+			return (nil, err);
+	}
+
+	images: array of ref Rawimage;
+	new: ref Rawimage;
+
+    Loop:
+	for(;;){
+		case c := fd.getb(){
+		Bufio->EOF =>
+			if(err == "")
+				err = "ReadGIF: premature EOF";
+			break Loop;
+		Bufio->ERROR =>
+			err = sys->sprint("ReadGIF: read error: %r");
+			return (nil, err);
+		16r21 =>	# Extension (ignored)
+			err = skipextension(header);
+			if(err != nil)
+				return (nil, err);
+
+		16r2C =>	# Image Descriptor
+			if(!multi && images!=nil)	# why read the rest?
+				break Loop;
+			(new, err) = readimage(header);
+			if(new == nil)
+				return (nil ,err);
+			if(new.fields & 16r80){
+				(new.cmap, err) = readcmap(header, (new.fields&7)+1);
+				if(new.cmap == nil)
+					return (nil, err);
+			}else
+				new.cmap = globalcmap;
+			(new.chans[0], err) = decode(header, new);
+			if(new.chans[0] == nil)
+				return (nil, err);
+			if(new.fields & 16r40)
+				interlace(new);
+			new.transp = header.transp;
+			new.trindex = header.trindex;
+			nimages := array[len images+1] of ref Rawimage;
+			nimages[0:] = images[0:];
+			nimages[len images] = new;
+			images = nimages;
+
+		16r3B =>	# Trailer
+			break Loop;
+
+		* =>
+			err = sys->sprint("ReadGIF: unknown block type: %x", c);
+			break Loop;
+		}
+	}
+
+	if(images==nil || images[0].chans[0] == nil){
+		if(err == nil)
+			err = "ReadGIF: no picture in file";
+		return (nil, err);
+	}
+
+	return (images, err);
+}
+
+readheader(fd: ref Iobuf, buf: array of byte): (ref Header, string)
+{
+	if(fd.read(buf, 13) != 13){
+		err := sys->sprint("ReadGIF: can't read header: %r");
+		return (nil, err);
+	}
+	h := ref Header;
+	h.vers = string buf[0:6];
+	if(h.vers!="GIF87a" && h.vers!="GIF89a"){
+		err := sys->sprint("ReadGIF: can't recognize format %s", h.vers);
+		return (nil, err);
+	}
+	h.screenw = int buf[6]+(int buf[7]<<8);
+	h.screenh = int buf[8]+(int buf[9]<<8);
+	h.fields = int buf[10];
+	h.bgrnd = int buf[11];
+	h.aspect = int buf[12];
+	h.fd = fd;
+	h.buf = buf;
+	h.transp = 0;
+	return (h, "");
+}
+
+readcmap(h: ref Header, size: int): (array of byte,string)
+{
+	size = 3*(1<<size);
+	map := array[size] of byte;
+	if(h.fd.read(map, size) != size)
+		return (nil, "ReadGIF: short read on color map");
+	return (map, "");
+}
+
+readimage(h: ref Header): (ref Rawimage, string)
+{
+	if(h.fd.read(h.buf, 9) != 9){
+		err := sys->sprint("ReadGIF: can't read image descriptor: %r");
+		return (nil, err);
+	}
+	i := ref Rawimage;
+	left := int h.buf[0]+(int h.buf[1]<<8);
+	top := int h.buf[2]+(int h.buf[3]<<8);
+	width := int h.buf[4]+(int h.buf[5]<<8);
+	height := int h.buf[6]+(int h.buf[7]<<8);
+	i.fields = int h.buf[8];
+	i.r.min.x = left;
+	i.r.min.y = top;
+	i.r.max.x = left+width;
+	i.r.max.y = top+height;
+	i.nchans = 1;
+	i.chans = array[1] of array of byte;
+	i.chandesc = CRGB1;
+	return (i, "");
+}
+
+readdata(h: ref Header, ch: chan of (array of byte, string))
+{
+	err: string;
+
+	# send nil for error, buffer of length 0 for EOF
+	for(;;){
+		nbytes := h.fd.getb();
+		if(nbytes < 0){
+			err = sys->sprint("ReadGIF: can't read data: %r");
+			ch <-= (nil, err);
+			return;
+		}
+		d := array[nbytes] of byte;
+		if(nbytes == 0){
+			ch <-= (d, "");
+			return;
+		}
+		n := h.fd.read(d, nbytes);
+		if(n != nbytes){
+			if(n > 0){
+				ch <-= (d[0:n], nil);
+				ch <-= (d[0:0], "ReadGIF: short data subblock");
+			}else
+				ch <-= (nil, sys->sprint("ReadGIF: can't read data: %r"));
+			return;
+		}
+		ch <-= (d, "");
+	}
+}
+
+readerr: con "ReadGIF: can't read extension: %r";
+
+skipextension(h: ref Header): string
+{
+	fmterr: con "ReadGIF: bad extension format";
+
+	hsize := 0;
+	hasdata := 0;
+
+	case h.fd.getb(){
+	Bufio->ERROR or Bufio->EOF =>
+		return sys->sprint(readerr);
+	16r01 =>	# Plain Text Extension
+		hsize = 13;
+		hasdata = 1;
+	16rF9 =>	# Graphic Control Extension
+		return graphiccontrol(h);
+	16rFE =>	# Comment Extension
+		hasdata = 1;
+	16rFF =>	# Application Extension
+		hsize = h.fd.getb();
+		# standard says this must be 11, but Adobe likes to put out 10-byte ones,
+		# so we pay attention to the field.
+		hasdata = 1;
+	* =>
+		return "ReadGIF: unknown extension";
+	}
+	if(hsize>0 && h.fd.read(h.buf, hsize) != hsize)
+		return sys->sprint(readerr);
+	if(!hasdata){
+		if(int h.buf[hsize-1] != 0)
+			return fmterr;
+	}else{
+		ch := chan of (array of byte, string);
+		spawn readdata(h, ch);
+		for(;;){
+			(data, err) := <-ch;
+			if(data == nil)
+				return err;
+			if(len data == 0)
+				break;
+		}
+	}
+	return "";
+}
+
+graphiccontrol(h: ref Header): string
+{
+	if(h.fd.read(h.buf, 5+1) != 5+1)
+		return sys->sprint(readerr);
+	if(int h.buf[1] & 1){
+		h.transp = 1;
+		h.trindex = h.buf[4];
+	}
+	return "";
+}
+
+inittbl()
+{
+	tbl = array[4096] of Entry;
+	for(i:=0; i<258; i++) {
+		tbl[i].prefix = -1;
+		tbl[i].exten = i;
+	}
+}
+
+decode(h: ref Header, i: ref Rawimage): (array of byte, string)
+{
+	c, incode: int;
+
+	err := "";
+	if(h.fd.read(h.buf, 1) != 1){
+		err = sys->sprint("ReadGIF: can't read data: %r");
+		return (nil, err);
+	}
+	codesize := int h.buf[0];
+	if(codesize>8 || 0>codesize){
+		err = sys->sprint("ReadGIF: can't handle codesize %d", codesize);
+		return (nil, err);
+	}
+	err1 := "";
+	if(i.cmap!=nil && len i.cmap!=3*(1<<codesize)
+	  && (codesize!=2 || len i.cmap!=3*2)) # peculiar GIF bitmap files...
+		err1 = sys->sprint("ReadGIF: codesize %d doesn't match color map 3*%d", codesize, len i.cmap/3);
+
+	ch := chan of (array of byte, string);
+
+	spawn readdata(h, ch);
+
+	CTM :=1<<codesize;
+	EOD := CTM+1;
+
+	pic := array[(i.r.max.x-i.r.min.x)*(i.r.max.y-i.r.min.y)] of byte;
+	pici := 0;
+	data := array[0] of byte;
+	datai := 0;
+
+	nbits := 0;
+	sreg := 0;
+	stack := array[4096] of byte;
+	stacki: int;
+	fc := 0;
+
+Init:
+	for(;;){
+		csize := codesize+1;
+		nentry := EOD+1;
+		maxentry := (1<<csize)-1;
+		first := 1;
+		ocode := -1;
+
+		for(;; ocode = incode) {
+			while(nbits < csize) {
+				if(datai == len data){
+					(data, err) = <-ch;
+					if(data == nil)
+						return (nil, err);
+					if(err!="" && err1=="")
+						err1 = err;
+					if(len data == 0)
+						break Init;
+					datai = 0;
+				}
+				c = int data[datai++];
+				sreg |= c<<nbits;
+				nbits += 8;
+			}
+			code := sreg & ((1<<csize) - 1);
+			sreg >>= csize;
+			nbits -= csize;
+
+			if(code == EOD){
+				(data, err) = <-ch;
+				if(len data != 0)
+					err = "ReadGIF: unexpected data past EOD";
+				if(err!="" && err1=="")
+					err1 = err;
+				break Init;
+			}
+
+			if(code == CTM)
+				continue Init;
+
+			stacki = len stack-1;
+
+			incode = code;
+
+			# special case for KwKwK 
+			if(code == nentry) {
+				stack[stacki--] = byte fc;
+				code = ocode;
+			}
+
+			if(code > nentry) {
+				err = sys->sprint("ReadGIF: bad code %x %x", code, nentry);
+				return (nil, err);
+			}
+		
+			for(c=code; c>=0; c=tbl[c].prefix)
+				stack[stacki--] = byte tbl[c].exten;
+
+			nb := len stack-(stacki+1);
+			if(pici+nb > len pic){
+				if(err1 == "")
+					err1 = "ReadGIF: data overflows picture";
+			}else{
+				pic[pici:] = stack[stacki+1:];
+				pici += nb;
+			}
+
+			fc = int stack[stacki+1];
+
+			if(first){
+				first = 0;
+				continue;
+			}
+			early:=0; # peculiar tiff feature here for reference
+			if(nentry == maxentry-early) {
+				if(csize >= 12)
+					continue;
+				csize++;
+				maxentry = (1<<csize);
+				if(csize < 12)
+					maxentry--;
+			}
+			tbl[nentry].prefix = ocode;
+			tbl[nentry].exten = fc;
+			nentry++;
+		}
+	}
+	return (pic, err1);
+}
+
+interlace(image: ref Rawimage)
+{
+	pic := image.chans[0];
+	r := image.r;
+	dx := r.max.x-r.min.x;
+	ipic := array[dx*(r.max.y-r.min.y)] of byte;
+
+	# Group 1: every 8th row, starting with row 0
+	yy := 0;
+	for(y:=r.min.y; y<r.max.y; y+=8){
+		ipic[y*dx:] = pic[yy*dx:(yy+1)*dx];
+		yy++;
+	}
+
+	# Group 2: every 8th row, starting with row 4
+	for(y=r.min.y+4; y<r.max.y; y+=8){
+		ipic[y*dx:] = pic[yy*dx:(yy+1)*dx];
+		yy++;
+	}
+
+	# Group 3: every 4th row, starting with row 2
+	for(y=r.min.y+2; y<r.max.y; y+=4){
+		ipic[y*dx:] = pic[yy*dx:(yy+1)*dx];
+		yy++;
+	}
+
+	# Group 4: every 2nd row, starting with row 1
+	for(y=r.min.y+1; y<r.max.y; y+=2){
+		ipic[y*dx:] = pic[yy*dx:(yy+1)*dx];
+		yy++;
+	}
+
+	image.chans[0] = ipic;
+}
--- /dev/null
+++ b/appl/lib/readjpg.b
@@ -1,0 +1,973 @@
+implement RImagefile;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "imagefile.m";
+
+# Constants, all preceded by byte 16rFF
+SOF:	con byte 16rC0;	# Start of Frame
+SOF2:	con byte 16rC2;	# Start of Frame; progressive Huffman
+JPG:	con byte 16rC8;	# Reserved for JPEG extensions
+DHT:	con byte 16rC4;	# Define Huffman Tables
+DAC:	con byte 16rCC;	# Arithmetic coding conditioning
+RST:	con byte 16rD0;	# Restart interval termination
+RST7:	con byte 16rD7;	# Restart interval termination (highest value)
+SOI:	con byte 16rD8;	# Start of Image
+EOI:	con byte 16rD9;	# End of Image
+SOS:	con byte 16rDA;	# Start of Scan
+DQT:	con byte 16rDB;	# Define quantization tables
+DNL:	con byte 16rDC;	# Define number of lines
+DRI:	con byte 16rDD;	# Define restart interval
+DHP:	con byte 16rDE;	# Define hierarchical progression
+EXP:	con byte 16rDF;	# Expand reference components
+APPn:	con byte 16rE0;	# Reserved for application segments
+JPGn:	con byte 16rF0;	# Reserved for JPEG extensions
+COM:	con byte 16rFE;	# Comment
+
+Header: adt
+{
+	fd:	ref Iobuf;
+	ch:	chan of (ref Rawimage, string);
+	# variables in i/o routines
+	sr:	int;	# shift register, right aligned
+	cnt:	int;	# # bits in right part of sr
+	buf:	array of byte;
+	bufi:	int;
+	nbuf:	int;
+
+	Nf:		int;
+	comp:	array of Framecomp;
+	mode:	byte;
+	X,Y:		int;
+	qt:		array of array of int;	# quantization tables
+	dcht:		array of ref Huffman;
+	acht:		array of ref Huffman;
+	sf:		array of byte;	# start of frame; do better later
+	ss:		array of byte;	# start of scan; do better later
+	ri:		int;
+};
+
+NBUF:	con 16*1024;
+
+Huffman: adt
+{
+	bits:	array of int;
+	size:	array of int;
+	code:	array of int;
+	val:	array of int;
+	mincode:	array of int;
+	maxcode:	array of int;
+	valptr:	array of int;
+	# fast lookup
+	value:	array of int;
+	shift:	array of int;
+};
+
+Framecomp: adt	# Frame component specifier from SOF marker
+{
+	C:	int;
+	H:	int;
+	V:	int;
+	Tq:	int;
+};
+
+zerobytes: array of byte;
+zeroints: array of int;
+zeroreals: array of real;
+clamp: array of byte;
+NCLAMP: con 1000;
+CLAMPOFF: con 300;
+
+init(iomod: Bufio)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	bufio = iomod;
+	zerobytes = array[8*8] of byte;
+	zeroints = array[8*8] of int;
+	zeroreals = array[8*8] of real;
+	for(k:=0; k<8*8; k++){
+		zerobytes[k] = byte 0;
+		zeroints[k] = 0;
+		zeroreals[k] = 0.0;
+	}
+	clamp = array[NCLAMP] of byte;
+	for(k=0; k<CLAMPOFF; k++)
+		clamp[k] = byte 0;
+	for(; k<CLAMPOFF+256; k++)
+		clamp[k] = byte(k-CLAMPOFF);
+	for(; k<NCLAMP; k++)
+		clamp[k] = byte 255;
+}
+
+read(fd: ref Iobuf): (ref Rawimage, string)
+{
+	# spawn a subprocess so I/O errors can clean up easily
+
+	ch := chan of (ref Rawimage, string);
+	spawn readslave(fd, ch);
+
+	return <-ch;
+}
+
+readmulti(fd: ref Iobuf): (array of ref Rawimage, string)
+{
+	(i, err) := read(fd);
+	if(i != nil){
+		a := array[1] of { i };
+		return (a, err);
+	}
+	return (nil, err);
+}
+
+readslave(fd: ref Iobuf, ch: chan of (ref Rawimage, string))
+{
+	image: ref Rawimage;
+
+	(header, err) := soiheader(fd, ch);
+	if(header == nil){
+		ch <-= (nil, err);
+		exit;
+	}
+	buf := header.buf;
+	nseg := 0;
+
+    Loop:
+	while(err == ""){
+		m: int;
+		b: array of byte;
+		nseg++;
+		(m, b, err) = readsegment(header);
+		case m{
+		-1 =>
+			break Loop;
+
+		int APPn+0 =>
+			if(nseg==1 && string b[0:4]=="JFIF"){  # JFIF header; check version
+				vers0 := int b[5];
+				vers1 := int b[6];
+				if(vers0>1 || vers1>2)
+					err = sys->sprint("ReadJPG: can't handle JFIF version %d.%2d", vers0, vers1);
+			}
+
+		int APPn+1 to int APPn+15 =>
+			;
+
+		int DQT =>
+			err = quanttables(header, b);
+
+		int SOF =>
+			header.Y = int2(b, 1);
+			header.X = int2(b, 3);
+			header.Nf = int b[5];
+			header.comp = array[header.Nf] of Framecomp;
+			for(i:=0; i<header.Nf; i++){
+				header.comp[i].C = int b[6+3*i+0];
+				(H, V) := nibbles(b[6+3*i+1]);
+				header.comp[i].H = H;
+				header.comp[i].V = V;
+				header.comp[i].Tq = int b[6+3*i+2];
+			}
+			header.mode = SOF;
+			header.sf = b;
+
+		int SOF2 =>
+			err = sys->sprint("ReadJPG: can't handle progressive Huffman mode");
+			break Loop;
+
+		int SOS =>
+			header.ss = b;
+			(image, err) = decodescan(header);
+			if(err != "")
+				break Loop;
+
+			# BUG: THIS SHOULD USE THE LOOP TO FINISH UP
+			x := nextbyte(header, 1);
+			if(x != 16rFF)
+				err = sys->sprint("ReadJPG: didn't see marker at end of scan; saw %x", x);
+		 	else{
+				x = nextbyte(header, 1);
+				if(x != int EOI)
+					err = sys->sprint("ReadJPG: expected EOI saw %x", x);
+			}
+			break Loop;
+
+		int DHT =>
+			err = huffmantables(header, b);
+
+		int DRI =>
+			header.ri = int2(b, 0);
+
+		int COM =>
+			;
+
+		int EOI =>
+			break Loop;
+
+		* =>
+			err = sys->sprint("ReadJPG: unknown marker %.2x", m);
+		}
+	}
+	ch <-= (image, err);
+}
+
+readerror(): string
+{
+	return sys->sprint("ReadJPG: read error: %r");
+}
+
+marker(buf: array of byte, n: int): byte
+{
+	if(buf[n] != byte 16rFF)
+		return byte 0;
+	return buf[n+1];
+}
+
+int2(buf: array of byte, n: int): int
+{
+	return (int buf[n]<<8)+(int buf[n+1]);
+}
+
+nibbles(b: byte): (int, int)
+{
+	i := int b;
+	return (i>>4, i&15);
+}
+
+soiheader(fd: ref Iobuf, ch: chan of (ref Rawimage, string)): (ref Header, string)
+{
+	# 1+ for restart preamble (see nextbyte), +1 for sentinel
+	buf := array[1+NBUF+1] of byte;
+	if(fd.read(buf, 2) != 2)
+		return (nil, sys->sprint("ReadJPG: can't read header: %r"));
+	if(marker(buf, 0) != SOI)
+		return (nil, sys->sprint("ReadJPG: unrecognized marker in header"));
+	h := ref Header;
+	h.buf = buf;
+	h.bufi = 0;
+	h.nbuf = 0;
+	h.fd = fd;
+	h.ri = 0;
+	h.ch = ch;
+	return (h, nil);
+}
+
+readsegment(h: ref Header): (int, array of byte, string)
+{
+	if(h.fd.read(h.buf, 2) != 2)
+		return (-1, nil, readerror());
+	m := int marker(h.buf, 0);
+	case m{
+	int EOI =>
+		return (m, nil, nil);
+	0 =>
+		err := sys->sprint("ReadJPG: expecting marker; saw %.2x%.2x)",
+			int h.buf[0], int h.buf[1]);
+		return (-1, nil, err);
+	}
+	if(h.fd.read(h.buf, 2) != 2)
+		return (-1, nil, readerror());
+	n := int2(h.buf, 0);
+	if(n < 2)
+		return (-1, nil, readerror());
+	n -= 2;
+#	if(n > len h.buf){
+#		h.buf = array[n+1] of byte;	# +1 for sentinel
+#		#h.nbuf = n;
+#	}
+	b := array[n] of byte;
+	if(h.fd.read(b, n) != n)
+		return (-1, nil, readerror());
+	return (m, b, nil);
+}
+
+huffmantables(h: ref Header, b: array of byte): string
+{
+	if(h.dcht == nil){
+		h.dcht = array[4] of ref Huffman;
+		h.acht = array[4] of ref Huffman;
+	}
+	err: string;
+	mt: int;
+	for(l:=0; l<len b; l+=17+mt){
+		(mt, err) = huffmantable(h, b[l:]);
+		if(err != nil)
+			return err;
+	}
+	return nil;
+}
+
+huffmantable(h: ref Header, b: array of byte): (int, string)
+{
+	t := ref Huffman;
+	(Tc, th) := nibbles(b[0]);
+	if(Tc > 1)
+		return (0, sys->sprint("ReadJPG: unknown Huffman table class %d", Tc));
+	if(th>3 || (h.mode==SOF && th>1))
+		return (0, sys->sprint("ReadJPG: unknown Huffman table index %d", th));
+	if(Tc == 0)
+		h.dcht[th] = t;
+	else
+		h.acht[th] = t;
+
+	# flow chart C-2
+	nsize := 0;
+	for(i:=0; i<16; i++)
+		nsize += int b[1+i];
+	t.size = array[nsize+1] of int;
+	k := 0;
+	for(i=1; i<=16; i++){
+		n := int b[i];
+		for(j:=0; j<n; j++)
+			t.size[k++] = i;
+	}
+	t.size[k] = 0;
+
+	# initialize HUFFVAL
+	t.val = array[nsize] of int;
+	for(i=0; i<nsize; i++){
+		t.val[i] = int b[17+i];
+	}
+
+	# flow chart C-3
+	t.code = array[nsize+1] of int;
+	k = 0;
+	code := 0;
+	si := t.size[0];
+	for(;;){
+		do
+			t.code[k++] = code++;
+		while(t.size[k] == si);
+		if(t.size[k] == 0)
+			break;
+		do{
+			code <<= 1;
+			si++;
+		}while(t.size[k] != si);
+	}
+
+	# flow chart F-25
+	t.mincode = array[17] of int;
+	t.maxcode = array[17] of int;
+	t.valptr = array[17] of int;
+	i = 0;
+	j := 0;
+    F25:
+	for(;;){
+		for(;;){
+			i++;
+			if(i > 16)
+				break F25;
+			if(int b[i] != 0)
+				break;
+			t.maxcode[i] = -1;
+		}
+		t.valptr[i] = j;
+		t.mincode[i] = t.code[j];
+		j += int b[i]-1;
+		t.maxcode[i] = t.code[j];
+		j++;
+	}
+
+	# create byte-indexed fast path tables
+	t.value = array[256] of int;
+	t.shift = array[256] of int;
+	maxcode := t.maxcode;
+	# stupid startup algorithm: just run machine for each byte value
+  Bytes:
+	for(v:=0; v<256; v++){
+		cnt := 7;
+		m := 1<<7;
+		code = 0;
+		sr := v;
+		i = 1;
+		for(;;i++){
+			if(sr & m)
+				code |= 1;
+			if(code <= maxcode[i])
+				break;
+			code <<= 1;
+			m >>= 1;
+			if(m == 0){
+				t.shift[v] = 0;
+				t.value[v] = -1;
+				continue Bytes;
+			}
+			cnt--;
+		}
+		t.shift[v] = 8-cnt;
+		t.value[v] = t.val[t.valptr[i]+(code-t.mincode[i])];
+	}
+
+	return (nsize, nil);
+}
+
+quanttables(h: ref Header, b: array of byte): string
+{
+	if(h.qt == nil)
+		h.qt = array[4] of array of int;
+	err: string;
+	n: int;
+	for(l:=0; l<len b; l+=1+n){
+		(n, err) = quanttable(h, b[l:]);
+		if(err != nil)
+			return err;
+	}
+	return nil;
+}
+
+quanttable(h: ref Header, b: array of byte): (int, string)
+{
+	(pq, tq) := nibbles(b[0]);
+	if(pq > 1)
+		return (0, sys->sprint("ReadJPG: unknown quantization table class %d", pq));
+	if(tq > 3)
+		return (0, sys->sprint("ReadJPG: unknown quantization table index %d", tq));
+	q := array[64] of int;
+	h.qt[tq] = q;
+	for(i:=0; i<64; i++){
+		if(pq == 0)
+			q[i] = int b[1+i];
+		else
+			q[i] = int2(b, 1+2*i);
+	}
+	return (64*(1+pq), nil);
+}
+
+zig := array[64] of {
+	0, 1, 8, 16, 9, 2, 3, 10, 17, # 0-7
+	24, 32, 25, 18, 11, 4, 5, # 8-15
+	12, 19, 26, 33, 40, 48, 41, 34, # 16-23
+	27, 20, 13, 6, 7, 14, 21, 28, # 24-31
+	35, 42, 49, 56, 57, 50, 43, 36, # 32-39
+	29, 22, 15, 23, 30, 37, 44, 51, # 40-47
+	58, 59, 52, 45, 38, 31, 39, 46, # 48-55
+	53, 60, 61, 54, 47, 55, 62, 63 # 56-63
+};
+
+decodescan(h: ref Header): (ref Rawimage, string)
+{
+	ss := h.ss;
+	Ns := int ss[0];
+	if((Ns!=3 && Ns!=1) || Ns!=h.Nf)
+		return (nil, "ReadJPG: can't handle scan not 3 components");
+
+	image := ref Rawimage;
+	image.r = ((0, 0), (h.X, h.Y));
+	image.cmap = nil;
+	image.transp = 0;
+	image.trindex = byte 0;
+	image.fields = 0;
+	image.chans = array[h.Nf] of array of byte;
+	if(Ns == 3)
+		image.chandesc = CRGB;
+	else
+		image.chandesc = CY;
+	image.nchans = h.Nf;
+	for(k:=0; k<h.Nf; k++)
+		image.chans[k] = array[h.X*h.Y] of byte;
+
+	# build per-component arrays
+	Td := array[Ns] of int;
+	Ta := array[Ns] of int;
+	data := array[Ns] of array of array of real;
+	H := array[Ns] of int;
+	V := array[Ns] of int;
+	DC := array[Ns] of int;
+
+	# compute maximum H and V
+	Hmax := 0;
+	Vmax := 0;
+	for(comp:=0; comp<Ns; comp++){
+		if(h.comp[comp].H > Hmax)
+			Hmax = h.comp[comp].H;
+		if(h.comp[comp].V > Vmax)
+			Vmax = h.comp[comp].V;
+	}
+
+	# initialize data structures
+	allHV1 := 1;
+	for(comp=0; comp<Ns; comp++){
+		# JPEG requires scan components to be in same order as in frame,
+		# so if both have 3 we know scan is Y Cb Cr and there's no need to
+		# reorder
+		cs := int ss[1+2*comp];
+		(Td[comp], Ta[comp]) = nibbles(ss[2+2*comp]);
+		H[comp] = h.comp[comp].H;
+		V[comp] = h.comp[comp].V;
+		nblock := H[comp]*V[comp];
+		if(nblock != 1)
+			allHV1 = 0;
+		data[comp] = array[nblock] of array of real;
+		DC[comp] = 0;
+		for(m:=0; m<nblock; m++)
+			data[comp][m] = array[8*8] of real;
+	}
+
+	ri := h.ri;
+
+	h.buf[0] = byte 16rFF;	# see nextbyte()
+	h.cnt = 0;
+	h.sr = 0;
+	nacross := ((h.X+(8*Hmax-1))/(8*Hmax));
+	nmcu := ((h.Y+(8*Vmax-1))/(8*Vmax))*nacross;
+	zz := array[64] of real;
+	err := "";
+	for(mcu:=0; mcu<nmcu; ){
+		for(comp=0; comp<Ns; comp++){
+			dcht := h.dcht[Td[comp]];
+			acht := h.acht[Ta[comp]];
+			qt := h.qt[h.comp[comp].Tq];
+
+			for(block:=0; block<H[comp]*V[comp]; block++){
+				# F-22
+				t := decode(h, dcht);
+				diff := receive(h, t);
+				DC[comp] += diff;
+
+				# F-23
+				zz[0:] = zeroreals;
+				zz[0] = real (qt[0]*DC[comp]);
+				k = 1;
+				for(;;){
+					rs := decode(h, acht);
+					(rrrr, ssss) := nibbles(byte rs);
+					if(ssss == 0){
+						if(rrrr != 15)
+							break;
+						k += 16;
+					}else{
+						k += rrrr;
+						z := receive(h, ssss);
+						zz[zig[k]] = real (z*qt[k]);
+						if(k == 63)
+							break;
+						k++;
+					}
+				}
+
+				idct(zz, data[comp][block]);	
+			}
+		}
+
+		# rotate colors to RGB and assign to bytes
+		if(Ns == 1) # very easy
+			colormap1(h, image, data[0][0], mcu, nacross);
+		else if(allHV1) # fairly easy
+			colormapall1(h, image, data[0][0], data[1][0], data[2][0], mcu, nacross);
+		else # miserable general case
+			colormap(h, image, data[0], data[1], data[2], mcu, nacross, Hmax, Vmax, H, V);
+
+		# process restart marker, if present
+		mcu++;
+		if(ri>0 && mcu<nmcu-1 && mcu%ri==0){
+			restart := mcu/ri-1;
+			rst, nskip: int;
+			nskip = 0;
+			do{
+				do{
+					rst = nextbyte(h, 1);
+					nskip++;
+				}while(rst>=0 && rst!=16rFF);
+				if(rst == 16rFF){
+					rst = nextbyte(h, 1);
+					nskip++;
+				}
+			}while(rst>=0 && (rst&~7)!=int RST);
+			if(nskip != 2)
+				err = sys->sprint("skipped %d bytes at restart %d\n", nskip-2, restart);
+			if(rst < 0)
+				return (nil, readerror());
+			if((rst&7) != (restart&7))
+				return (nil, sys->sprint("ReadJPG: expected RST%d got %d", restart&7, int rst&7));
+			h.cnt = 0;
+			h.sr = 0;
+			for(comp=0; comp<Ns; comp++)
+				DC[comp] = 0;
+		}
+	}
+	return (image, err);
+}
+
+colormap1(h: ref Header, image: ref Rawimage, data: array of real, mcu, nacross: int)
+{
+	pic := image.chans[0];
+	minx := 8*(mcu%nacross);
+		dx := 8;
+	if(minx+dx > h.X)
+		dx = h.X-minx;
+	miny := 8*(mcu/nacross);
+	dy := 8;
+	if(miny+dy > h.Y)
+		dy = h.Y-miny;
+	pici := miny*h.X+minx;
+	k := 0;
+	for(y:=0; y<dy; y++){
+		for(x:=0; x<dx; x++){
+			r := clamp[int (data[k+x]+128.)+CLAMPOFF];
+			pic[pici+x] = r;
+		}
+		pici += h.X;
+		k += 8;
+	}
+}
+
+colormapall1(h: ref Header, image: ref Rawimage, data0, data1, data2: array of real, mcu, nacross: int)
+{
+	rpic := image.chans[0];
+	gpic := image.chans[1];
+	bpic := image.chans[2];
+	minx := 8*(mcu%nacross);
+	dx := 8;
+	if(minx+dx > h.X)
+		dx = h.X-minx;
+	miny := 8*(mcu/nacross);
+	dy := 8;
+	if(miny+dy > h.Y)
+		dy = h.Y-miny;
+	pici := miny*h.X+minx;
+	k := 0;
+	for(y:=0; y<dy; y++){
+		for(x:=0; x<dx; x++){
+			Y := data0[k+x]+128.;
+			Cb := data1[k+x];
+			Cr := data2[k+x];
+			r := int (Y+1.402*Cr);
+			g := int (Y-0.34414*Cb-0.71414*Cr);
+			b := int (Y+1.772*Cb);
+			rpic[pici+x] = clamp[r+CLAMPOFF];
+			gpic[pici+x] = clamp[g+CLAMPOFF];
+			bpic[pici+x] = clamp[b+CLAMPOFF];
+		}
+		pici += h.X;
+		k += 8;
+	}
+}
+
+colormap(h: ref Header, image: ref Rawimage, data0, data1, data2: array of array of real, mcu, nacross, Hmax, Vmax: int,  H, V: array of int)
+{
+	rpic := image.chans[0];
+	gpic := image.chans[1];
+	bpic := image.chans[2];
+	minx := 8*Hmax*(mcu%nacross);
+	dx := 8*Hmax;
+	if(minx+dx > h.X)
+		dx = h.X-minx;
+	miny := 8*Vmax*(mcu/nacross);
+	dy := 8*Vmax;
+	if(miny+dy > h.Y)
+		dy = h.Y-miny;
+	pici := miny*h.X+minx;
+	H0 := H[0];
+	H1 := H[1];
+	H2 := H[2];
+	for(y:=0; y<dy; y++){
+		t := y*V[0];
+		b0 := H0*(t/(8*Vmax));
+		y0 := 8*((t/Vmax)&7);
+		t = y*V[1];
+		b1 := H1*(t/(8*Vmax));
+		y1 := 8*((t/Vmax)&7);
+		t = y*V[2];
+		b2 := H2*(t/(8*Vmax));
+		y2 := 8*((t/Vmax)&7);
+		x0 := 0;
+		x1 := 0;
+		x2 := 0;
+		for(x:=0; x<dx; x++){
+			Y := data0[b0][y0+x0++*H0/Hmax]+128.;
+			Cb := data1[b1][y1+x1++*H1/Hmax];
+			Cr := data2[b2][y2+x2++*H2/Hmax];
+			if(x0*H0/Hmax >= 8){
+				x0 = 0;
+				b0++;
+			}
+			if(x1*H1/Hmax >= 8){
+				x1 = 0;
+				b1++;
+			}
+			if(x2*H2/Hmax >= 8){
+				x2 = 0;
+				b2++;
+			}
+			r := int (Y+1.402*Cr);
+			g := int (Y-0.34414*Cb-0.71414*Cr);
+			b := int (Y+1.772*Cb);
+			rpic[pici+x] = clamp[r+CLAMPOFF];
+			gpic[pici+x] = clamp[g+CLAMPOFF];
+			bpic[pici+x] = clamp[b+CLAMPOFF];
+		}
+		pici += h.X;
+	}
+}
+
+# decode next 8-bit value from entropy-coded input.  chart F-26
+decode(h: ref Header, t: ref Huffman): int
+{
+	maxcode := t.maxcode;
+	if(h.cnt < 8)
+		nextbyte(h, 0);
+	# fast lookup
+	code := (h.sr>>(h.cnt-8))&16rFF;
+	v := t.value[code];
+	if(v >= 0){
+		h.cnt -= t.shift[code];
+		return v;
+	}
+
+	h.cnt -= 8;
+	if(h.cnt == 0)
+		nextbyte(h, 0);
+	h.cnt--;
+	cnt := h.cnt;
+	m := 1<<cnt;
+	sr := h.sr;
+	code <<= 1;
+	i := 9;
+	for(;;i++){
+		if(sr & m)
+			code |= 1;
+		if(code <= maxcode[i])
+			break;
+		code <<= 1;
+		m >>= 1;
+		if(m == 0){
+			sr = nextbyte(h, 0);
+			m = 16r80;
+			cnt = 8;
+		}
+		cnt--;
+	}
+	h.cnt = cnt;
+	return t.val[t.valptr[i]+(code-t.mincode[i])];
+}
+
+#
+# load next byte of input
+# we should really just call h.fd.getb(), but it's faster just to use Bufio
+# to load big chunks and manage our own byte-at-a-time input.
+#
+nextbyte(h: ref Header, marker: int): int
+{
+	b := int h.buf[h.bufi++];
+	if(b == 16rFF){
+		# check for sentinel at end of buffer
+		if(h.bufi >= h.nbuf){
+			underflow := (h.bufi > h.nbuf);
+			h.nbuf = h.fd.read(h.buf, NBUF);
+			if(h.nbuf <= 0){
+				h.ch <-= (nil, readerror());
+				exit;
+			}
+			h.buf[h.nbuf] = byte 16rFF;
+			h.bufi = 0;
+			if(underflow)	# if ran off end of buffer, just restart
+				return nextbyte(h, marker);
+		}
+		if(marker)
+			return b;
+		b2 := h.buf[h.bufi++];
+		if(b2 != byte 0){
+			if(b2 == DNL){
+				h.ch <-= (nil, "ReadJPG: DNL marker unimplemented");
+				exit;
+			}else if(b2<RST && RST7<b2){
+				h.ch <-= (nil, sys->sprint("ReadJPG: unrecognized marker %x", int b2));
+				exit;
+			}
+			# decode is reading into restart marker; satisfy it and restore state
+			if(h.bufi < 2){
+				# misery: must shift up buffer
+				h.buf[1:] = h.buf[0:h.nbuf+1];
+				h.nbuf++;
+				h.buf[0] = byte 16rFF;
+				h.bufi -= 1;
+			}else
+				h.bufi -= 2;
+			b = 16rFF;
+		}
+	}
+	h.cnt += 8;
+	h.sr = (h.sr<<8)|b;
+	return b;
+}
+
+# return next s bits of input, MSB first, and level shift it
+receive(h: ref Header, s: int): int
+{
+	while(h.cnt < s)
+		nextbyte(h, 0);
+	v := h.sr >> (h.cnt-s);
+	m := (1<<s);
+	v &= m-1;
+	h.cnt -= s;
+	# level shift
+	if(v < (m>>1))
+		v += ~(m-1)+1;
+	return v;
+}
+
+# IDCT based on Arai, Agui, and Nakajima, using flow chart Figure 4.8
+# of Pennebaker & Mitchell, JPEG: Still Image Data Compression Standard.
+# Remember IDCT is reverse of flow of DCT.
+
+a0: con 1.414;
+a1: con 0.707;
+a2: con 0.541;
+a3: con 0.707;
+a4: con 1.307;
+a5: con -0.383;
+
+# scaling factors from eqn 4-35 of P&M
+s1: con 1.0196;
+s2: con 1.0823;
+s3: con 1.2026;
+s4: con 1.4142;
+s5: con 1.8000;
+s6: con 2.6131;
+s7: con 5.1258;
+
+# overall normalization of 1/16, folded into premultiplication on vertical pass
+scale: con 0.0625;
+
+idct(zin: array of real, zout: array of real)
+{
+	x, y: int;
+
+	r := array[8*8] of real;
+
+	# transform horizontally
+	for(y=0; y<8; y++){
+		eighty := y<<3;
+		# if all non-DC components are zero, just propagate the DC term
+		if(zin[eighty+1]==0.)
+		if(zin[eighty+2]==0. && zin[eighty+3]==0.)
+		if(zin[eighty+4]==0. && zin[eighty+5]==0.)
+		if(zin[eighty+6]==0. && zin[eighty+7]==0.){
+			v := zin[eighty]*a0;
+			r[eighty+0] = v;
+			r[eighty+1] = v;
+			r[eighty+2] = v;
+			r[eighty+3] = v;
+			r[eighty+4] = v;
+			r[eighty+5] = v;
+			r[eighty+6] = v;
+			r[eighty+7] = v;
+			continue;
+		}
+
+		# step 5
+		in1 := s1*zin[eighty+1];
+		in3 := s3*zin[eighty+3];
+		in5 := s5*zin[eighty+5];
+		in7 := s7*zin[eighty+7];
+		f2 := s2*zin[eighty+2];
+		f3 := s6*zin[eighty+6];
+		f5 := (in1+in7);
+		f7 := (in5+in3);
+
+		# step 4
+		g2 := f2-f3;
+		g4 := (in5-in3);
+		g6 := (in1-in7);
+		g7 := f5+f7;
+
+		# step 3.5
+		t := (g4+g6)*a5;
+
+		# step 3
+		f0 := a0*zin[eighty+0];
+		f1 := s4*zin[eighty+4];
+		f3 += f2;
+		f2 = a1*g2;
+
+		# step 2
+		g0 := f0+f1;
+		g1 := f0-f1;
+		g3 := f2+f3;
+		g4 = t-a2*g4;
+		g5 := a3*(f5-f7);
+		g6 = a4*g6+t;
+
+		# step 1
+		f0 = g0+g3;
+		f1 = g1+f2;
+		f2 = g1-f2;
+		f3 = g0-g3;
+		f5 = g5-g4;
+		f6 := g5+g6;
+		f7 = g6+g7;
+
+		# step 6
+		r[eighty+0] = (f0+f7);
+		r[eighty+1] = (f1+f6);
+		r[eighty+2] = (f2+f5);
+		r[eighty+3] = (f3-g4);
+		r[eighty+4] = (f3+g4);
+		r[eighty+5] = (f2-f5);
+		r[eighty+6] = (f1-f6);
+		r[eighty+7] = (f0-f7);
+	}
+
+	# transform vertically
+	for(x=0; x<8; x++){
+		# step 5
+		in1 := scale*s1*r[x+8];
+		in3 := scale*s3*r[x+24];
+		in5 := scale*s5*r[x+40];
+		in7 := scale*s7*r[x+56];
+		f2 := scale*s2*r[x+16];
+		f3 := scale*s6*r[x+48];
+		f5 := (in1+in7);
+		f7 := (in5+in3);
+
+		# step 4
+		g2 := f2-f3;
+		g4 := (in5-in3);
+		g6 := (in1-in7);
+		g7 := f5+f7;
+
+		# step 3.5
+		t := (g4+g6)*a5;
+
+		# step 3
+		f0 := scale*a0*r[x];
+		f1 := scale*s4*r[x+32];
+		f3 += f2;
+		f2 = a1*g2;
+
+		# step 2
+		g0 := f0+f1;
+		g1 := f0-f1;
+		g3 := f2+f3;
+		g4 = t-a2*g4;
+		g5 := a3*(f5-f7);
+		g6 = a4*g6+t;
+
+		# step 1
+		f0 = g0+g3;
+		f1 = g1+f2;
+		f2 = g1-f2;
+		f3 = g0-g3;
+		f5 = g5-g4;
+		f6 := g5+g6;
+		f7 = g6+g7;
+
+		# step 6
+		zout[x] = (f0+f7);
+		zout[x+8] = (f1+f6);
+		zout[x+16] = (f2+f5);
+		zout[x+24] = (f3-g4);
+		zout[x+32] = (f3+g4);
+		zout[x+40] = (f2-f5);
+		zout[x+48] = (f1-f6);
+		zout[x+56] = (f0-f7);
+	}
+}
--- /dev/null
+++ b/appl/lib/readpicfile.b
@@ -1,0 +1,164 @@
+implement RImagefile;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "imagefile.m";
+
+Header: adt
+{
+	fd:	ref Iobuf;
+	ch:	chan of (ref Rawimage, string);
+	# variables in i/o routines
+	buf:	array of byte;
+	bufi:	int;
+	nbuf:	int;
+
+	TYPE:	string;
+	CHAN:	string;
+	NCHAN:	string;
+	CMAP:	int;
+
+	dx:	int;
+	dy:	int;
+};
+
+NBUF:	con 8*1024;
+
+init(iomod: Bufio)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	bufio = iomod;
+}
+
+read(fd: ref Iobuf): (ref Rawimage, string)
+{
+	# spawn a subprocess so I/O errors can clean up easily
+
+	ch := chan of (ref Rawimage, string);
+	spawn readslave(fd, ch);
+
+	return <-ch;
+}
+
+readmulti(fd: ref Iobuf): (array of ref Rawimage, string)
+{
+	(i, err) := read(fd);
+	if(i != nil){
+		a := array[1] of { i };
+		return (a, err);
+	}
+	return (nil, err);
+}
+
+readslave(fd: ref Iobuf, ch: chan of (ref Rawimage, string))
+{
+	(header, err) := header(fd, ch);
+	if(header == nil){
+		ch <-= (nil, err);
+		exit;
+	}
+
+	ch <-= image(header);
+}
+
+readerror(): string
+{
+	return sys->sprint("ReadPIC: read error: %r");
+}
+
+header(fd: ref Iobuf, ch: chan of (ref Rawimage, string)): (ref Header, string)
+{
+	h := ref Header;
+
+	h.fd = fd;
+	h.ch = ch;
+	h.CMAP = 0;
+	h.dx = 0;
+	h.dy = 0;
+	cantparse := "ReadPIC: can't parse header";
+	for(;;){
+		s := fd.gets('\n');
+		if(s==nil || s[len s-1]!='\n')
+			return (nil, cantparse);
+		if(s == "\n")
+			break;
+		addfield(h, s[0:len s-1]);
+	}
+	if(h.dx<=0 || h.dy<=0)
+		return (nil, "ReadPIC: empty picture or WINDOW not set");
+	return (h, nil);
+}
+
+addfield(h: ref Header, s: string)
+{
+	baddata := "ReadPIC: not a PIC header";
+	for(i:=0; i<len s; i++){
+		if(s[i] == '=')
+			break;
+		if(s[i]==0 || s[i]>16r7f){
+			h.ch <-= (nil, baddata);
+			exit;
+		}
+	}
+	if(i == len s){
+		h.ch <-= (nil, baddata);
+		exit;
+	}
+	case s[0:i]{
+	"TYPE" =>
+		h.TYPE = s[i+1:];
+	"CHAN" =>
+		h.CHAN = s[i+1:];
+	"NCHAN" =>
+		h.NCHAN = s[i+1:];
+	"CMAP" =>
+		h.CMAP = 1;
+	"WINDOW" =>
+		(n, l) := sys->tokenize(s[i+1:], " ");
+		if(n != 4){
+			h.ch <-= (nil, "ReadPIC: bad WINDOW specification");
+			exit;
+		}
+		x0 := int hd l;
+		l = tl l;
+		y0 := int hd l;
+		l = tl l;
+		h.dx = int hd l - x0;
+		l = tl l;
+		h.dy = int hd l - y0;
+	}
+}
+
+image(h: ref Header): (ref Rawimage, string)
+{
+	if(h.TYPE!="dump" || h.CHAN!="rgb" || h.NCHAN!="3" || h.CMAP)
+		return (nil, "ReadPIC: can't handle this type of picture");
+
+	i := ref Rawimage;
+	i.r = ((0,0), (h.dx, h.dy));
+	i.cmap = nil;
+	i.transp = 0;
+	i.trindex = byte 0;
+	i.nchans = int h.NCHAN;
+	i.chans = array[i.nchans] of array of byte;
+	for(j:=0; j<i.nchans; j++)
+		i.chans[j] = array[h.dx*h.dy] of byte;
+	i.chandesc = CRGB;
+	n := h.dx*h.dy;
+	b := array[i.nchans*n] of byte;
+	if(h.fd.read(b, len b) != len b)
+		return (nil, "ReadPIC: file too short");
+	l := 0;
+	for(j=0; j<n; j++)
+		for(k:=0; k<i.nchans; k++)
+			i.chans[k][j] = b[l++];
+	return (i, nil);
+}
--- /dev/null
+++ b/appl/lib/readpng.b
@@ -1,0 +1,823 @@
+implement RImagefile;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Point: import Draw;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "imagefile.m";
+
+include "crc.m";
+	crc: Crc;
+	CRCstate: import Crc;
+
+include "filter.m";
+	inflate: Filter;
+
+Chunk: adt {
+	size : int;
+	typ: string;
+	crc_state: ref CRCstate;
+};
+
+Png: adt {
+	depth: int;
+	filterbpp: int;
+	colortype: int;
+	compressionmethod: int;
+	filtermethod: int;
+	interlacemethod: int;
+	# tRNS
+	PLTEsize: int;
+	tRNS: array of byte;
+	# state for managing unpacking
+	alpha: int;
+	done: int;
+	error: string;
+	row, rowstep, colstart, colstep: int;
+	phase: int;
+	phasecols: int;
+	phaserows: int;
+	rowsize: int;
+	rowbytessofar: int;
+	thisrow: array of byte;
+	lastrow: array of byte;
+};
+
+init(iomod: Bufio)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(crc == nil)
+		crc =  load Crc Crc->PATH;
+	if(inflate == nil)
+		inflate = load Filter "/dis/lib/inflate.dis";
+	inflate->init();
+	bufio = iomod;
+}
+
+readmulti(fd: ref Iobuf): (array of ref Rawimage, string)
+{
+	(i, err) := read(fd);
+	if(i != nil){
+		a := array[1] of { i };
+		return (a, err);
+	}
+	return (nil, err);
+}
+
+read(fd: ref Iobuf): (ref Rawimage, string)
+{
+	chunk := ref Chunk;
+	png := ref Png;
+	raw := ref Rawimage;
+
+	chunk.crc_state = crc->init(0, int 16rffffffff);
+# Check it's a PNG
+	if (!get_signature(fd))
+		return (nil, "not a PNG");
+# Get the IHDR
+	if (!get_chunk_header(fd, chunk))
+		return (nil, "duff header");
+	if (chunk.typ != "IHDR")
+		return (nil, "IHDR must come first");
+	if (chunk.size != 13)
+		return (nil, "IHDR wrong size");
+	raw.r.max.x = get_int(fd, chunk.crc_state);
+	if (raw.r.max.x <= 0)
+		return (nil, "invalid width");
+	raw.r.max.y = get_int(fd, chunk.crc_state);
+	if (raw.r.max.y <= 0)
+		return (nil, "invalid height");
+	png.depth = get_byte(fd, chunk.crc_state);
+	case png.depth {
+	1 or 2 or 4 or 8 or 16 =>
+		;
+	* =>
+		return (nil, "invalid depth");
+	}
+	png.colortype = get_byte(fd, chunk.crc_state);
+
+	okcombo : int;
+
+	case png.colortype {
+	0 =>
+		okcombo = 1;
+		raw.nchans = 1;
+		raw.chandesc = RImagefile->CY;
+		png.alpha = 0;
+	2  =>
+		okcombo = (png.depth == 8 || png.depth == 16);
+		raw.nchans = 3;
+		raw.chandesc = RImagefile->CRGB;
+		png.alpha = 0;
+	3 =>
+		okcombo = (png.depth != 16);
+		raw.nchans = 1;
+		raw.chandesc = RImagefile->CRGB1;
+		png.alpha = 0;
+	4 =>
+		okcombo = (png.depth == 8 || png.depth == 16);
+		raw.nchans = 1;
+		raw.chandesc = RImagefile->CY;
+		png.alpha = 1;
+	6 =>
+		okcombo = (png.depth == 8 || png.depth == 16);
+		raw.nchans = 3;
+		raw.chandesc = RImagefile->CRGB;
+		png.alpha = 1;
+	* =>
+		return (nil, "invalid colortype");
+	}
+	if (!okcombo)
+		return (nil, "invalid depth/colortype combination");
+	png.compressionmethod = get_byte(fd, chunk.crc_state);
+	if (png.compressionmethod != 0)
+		return (nil, "invalid compression method " + string png.compressionmethod);
+	png.filtermethod = get_byte(fd, chunk.crc_state);
+	if (png.filtermethod != 0)
+		return (nil, "invalid filter method");
+	png.interlacemethod = get_byte(fd, chunk.crc_state);
+	if (png.interlacemethod != 0 && png.interlacemethod != 1)
+		return (nil, "invalid interlace method");
+	if(0)
+		sys->print("width %d height %d depth %d colortype %d interlace %d\n",
+			raw.r.max.x, raw.r.max.y, png.depth, png.colortype, png.interlacemethod);
+	if (!get_crc_and_check(fd, chunk))
+		return (nil, "invalid CRC");
+# Stash some detail in raw
+	raw.r.min = Point(0, 0);
+	raw.transp = 0;
+	raw.chans = array[raw.nchans] of array of byte;
+	{
+		for (r:= 0; r < raw.nchans; r++)
+			raw.chans[r] = array[raw.r.max.x * raw.r.max.y] of byte;
+	}
+# Get the next chunk
+	seenPLTE := 0;
+	seenIDAT := 0;
+	seenLastIDAT := 0;
+	inflateFinished := 0;
+	seenIEND := 0;
+	seentRNS := 0;
+	rq: chan of ref Filter->Rq;
+
+	png.error = nil;
+	rq = nil;
+	while (png.error == nil) {
+		if (!get_chunk_header(fd, chunk)) {
+			if (!seenIEND)
+				png.error = "duff header";
+			break;
+		}
+		if (seenIEND) {
+			png.error = "rubbish at eof";
+			break;
+		}
+		case (chunk.typ) {
+		"IEND" =>
+			seenIEND = 1;
+		"PLTE" =>
+			if (seenPLTE) {
+				png.error = "too many PLTEs";
+				break;
+			}
+			if (seentRNS) {
+				png.error = "tRNS before PLTE";
+				break;
+			}
+			if (seenIDAT) {
+				png.error = "PLTE too late";
+				break;
+			}
+			if (chunk.size % 3 || chunk.size < 1 * 3 || chunk.size > 256 * 3) {
+				png.error = "PLTE strange size";
+				break;
+			}
+			if (png.colortype == 0 || png.colortype == 4) {
+				png.error = "superfluous PLTE";
+				break;
+			}
+			raw.cmap = array[256 * 3] of byte;
+			png.PLTEsize = chunk.size / 3;
+			if (!get_bytes(fd, chunk.crc_state, raw.cmap, chunk.size)) {
+				png.error = "eof in PLTE";
+				break;
+			}
+#			{
+#				x: int;
+#				sys->print("Palette:\n");
+#				for (x = 0; x < chunk.size; x += 3)
+#					sys->print("%3d: (%3d, %3d, %3d)\n",
+#						x / 3, int raw.cmap[x], int raw.cmap[x + 1], int raw.cmap[x + 2]);
+#			}
+			seenPLTE = 1;
+		"tRNS" =>
+			if (seenIDAT) {
+				png.error = "tRNS too late";
+				break;
+			}
+			case png.colortype {
+			0 =>
+				if (chunk.size != 2) {
+					png.error = "tRNS wrong size";
+					break;
+				}
+				level := get_ushort(fd, chunk.crc_state);
+				if (level < 0) {
+					png.error = "eof in tRNS";
+					break;
+				}
+				if (png.depth != 16) {
+					raw.transp = 1;
+					raw.trindex = byte level;
+				}
+			2 =>
+				# a legitimate coding, but we can't use the information
+				if (!skip_bytes(fd, chunk.crc_state, chunk.size))
+					png.error = "eof in skipped tRNS chunk";
+				break;
+			3 =>
+				if (!seenPLTE) {
+					png.error = "tRNS too early";
+					break;
+				}
+				if (chunk.size > png.PLTEsize) {
+					png.error = "tRNS too big";
+					break;
+				}
+				png.tRNS = array[png.PLTEsize] of byte;
+				for (x := chunk.size; x < png.PLTEsize; x++)
+					png.tRNS[x] = byte 255;
+				if (!get_bytes(fd, chunk.crc_state, png.tRNS, chunk.size)) {
+					png.error = "eof in tRNS";
+					break;
+				}
+#				{
+#					sys->print("tRNS:\n");
+#					for (x = 0; x < chunk.size; x++)
+#						sys->print("%3d: (%3d)\n", x, int png.tRNS[x]);
+#				}
+				if (png.error == nil) {
+					# analyse the tRNS chunk to see if it contains a single transparent index
+					# translucent entries are treated as opaque
+					for (x = 0; x < chunk.size; x++)
+						if (png.tRNS[x] == byte 0) {
+							raw.trindex = byte x;
+							if (raw.transp) {
+								raw.transp = 0;
+								break;
+							}
+							raw.transp = 1;
+						}
+#					if (raw.transp)
+#						sys->print("selected index %d\n", int raw.trindex);
+				}
+			4 or 6 =>
+				png.error = "tRNS invalid when alpha present";
+			}
+			seentRNS = 1;
+		"IDAT" =>
+			if (seenLastIDAT) {
+				png.error = "non contiguous IDATs";
+				break;
+			}
+			if (inflateFinished) {
+				png.error = "too many IDATs";
+				break;
+			}
+			remaining := 0;
+			if (!seenIDAT) {
+				# open channel to inflate filter
+				if (!processdatainit(png, raw))
+					break;
+				rq = inflate->start(nil);
+				skip_bytes(fd, chunk.crc_state, 2);
+				remaining = chunk.size - 2;
+			}
+			else
+				remaining = chunk.size;
+			while (remaining && png.error == nil) {
+				pick m := <- rq {
+				Fill =>
+#					sys->print("Fill(%d) remaining %d\n", len m.buf, remaining);
+					toget := len m.buf;
+					if (toget > remaining)
+						toget = remaining;
+					if (!get_bytes(fd, chunk.crc_state, m.buf, toget)) {
+						m.reply <-= -1;
+						png.error = "eof during IDAT";
+						break;
+					}
+					m.reply <-= toget;
+					remaining -= toget;
+				Result =>
+#					sys->print("Result(%d)\n", len m.buf);
+					m.reply <-= 0;
+					processdata(png, raw, m.buf);
+				Info =>
+#					sys->print("Info(%s)\n", m.msg);
+				Finished =>
+					inflateFinished = 1;
+#					sys->print("Finished\n");
+				Error =>
+					return (nil, "inflate error\n");
+				}
+			}
+			seenIDAT = 1;
+		* =>
+			# skip the blighter
+			if (!skip_bytes(fd, chunk.crc_state, chunk.size))
+				png.error = "eof in skipped chunk";
+		}
+		if (png.error != nil)
+			break;
+		if (!get_crc_and_check(fd, chunk))
+			return (nil, "invalid CRC");
+		if (chunk.typ != "IDAT" && seenIDAT)
+			seenLastIDAT = 1;
+	}
+	# can only get here if IEND was last chunk, or png.error set
+	
+	if (png.error == nil && !seenIDAT) {
+		png.error = "no IDAT!";
+		inflateFinished = 1;
+	}
+	while (rq != nil && !inflateFinished) {
+		pick m := <-rq {
+		Fill =>
+#			sys->print("Fill(%d)\n", len m.buf);
+			png.error = "eof in zlib stream";
+			m.reply <-= -1;
+			inflateFinished = 1;
+		Result =>
+#			sys->print("Result(%d)\n", len m.buf);
+			if (png.error != nil) {
+				m.reply <-= -1;
+				inflateFinished = 1;
+			}
+			else {
+				m.reply <-= 0;
+				processdata(png, raw, m.buf);
+			}
+		Info =>
+#			sys->print("Info(%s)\n", m.msg);
+		Finished =>
+#			sys->print("Finished\n");
+			inflateFinished = 1;
+			break;
+		Error =>
+			png.error = "inflate error\n";
+			inflateFinished = 1;
+		}
+		
+	}
+	if (png.error == nil && !png.done)
+		png.error = "insufficient data";
+	return (raw, png.error);
+}
+
+phase2stepping(phase: int): (int, int, int, int)
+{
+	case phase {
+	0 =>
+		return (0, 1, 0, 1);
+	1 =>
+		return (0, 8, 0, 8);
+	2 =>
+		return (0, 8, 4, 8);
+	3 =>
+		return (4, 8, 0, 4);
+	4 =>
+		return (0, 4, 2, 4);
+	5 =>
+		return (2, 4, 0, 2);
+	6 =>
+		return (0, 2, 1, 2);
+	7 =>
+		return (1, 2, 0, 1);
+	* =>
+		return (-1, -1, -1, -1);
+	}
+}
+
+processdatainitphase(png: ref Png, raw: ref Rawimage)
+{
+	(png.row, png.rowstep, png.colstart, png.colstep) = phase2stepping(png.phase);
+	if (raw.r.max.x > png.colstart)
+		png.phasecols = (raw.r.max.x - png.colstart + png.colstep - 1) / png.colstep;
+	else
+		png.phasecols = 0;
+	if (raw.r.max.y > png.row)
+		png.phaserows = (raw.r.max.y - png.row + png.rowstep - 1) / png.rowstep;
+	else
+		png.phaserows = 0;
+	png.rowsize = png.phasecols * (raw.nchans + png.alpha) * png.depth;
+	png.rowsize = (png.rowsize + 7) / 8;
+	png.rowsize++;		# for the filter byte
+	png.rowbytessofar = 0;
+	png.thisrow = array[png.rowsize] of byte;
+	png.lastrow = array[png.rowsize] of byte;
+#	sys->print("init phase %d: r (%d, %d, %d) c (%d, %d, %d) (%d)\n",
+#		png.phase, png.row, png.rowstep, png.phaserows,
+#		png.colstart, png.colstep, png.phasecols, png.rowsize);
+}
+
+processdatainit(png: ref Png, raw: ref Rawimage): int
+{
+	if (raw.nchans != 1&& raw.nchans != 3) {
+		png.error = "only 1 or 3 channels supported";
+		return 0;
+	}
+#	if (png.interlacemethod != 0) {
+#		png.error = "only progressive supported";
+#		return 0;
+#	}
+	if (png.colortype == 3 && raw.cmap == nil) {
+		png.error = "PLTE chunk missing";
+		return 0;
+	}
+	png.done = 0;
+	png.filterbpp = (png.depth * (raw.nchans + png.alpha) + 7) / 8;
+	png.phase = png.interlacemethod;
+
+	processdatainitphase(png, raw);
+
+	return 1;
+}
+
+upconvert(out: array of byte, outstride: int, in: array of byte, pixels: int, bpp: int)
+{
+	b: byte;
+	bits := pixels * bpp;
+	lim := bits / 8;
+	mask := byte ((1 << bpp) - 1);
+	outx := 0;
+	inx := 0;
+	for (x := 0; x < lim; x++) {
+		b = in[inx];
+		for (s := 8 - bpp; s >= 0; s -= bpp) {
+			pixel := (b >> s) & mask;
+			ucp := pixel;
+			for (y := bpp; y < 8; y += bpp)
+				ucp |= pixel << y;
+			out[outx] = ucp; 
+			outx += outstride;
+		}
+		inx++;
+	}
+	residue := (bits % 8) / bpp;
+	if (residue) {
+		b = in[inx];
+		for (s := 8 - bpp; s >= 0; s -= bpp) {
+			pixel := (b >> s) & mask;
+			ucp := pixel;
+			for (y := bpp; y < 8; y += bpp)
+				ucp |= pixel << y;
+			out[outx] = ucp; 
+			outx += outstride;
+			if (--residue <= 0)
+				break;
+		}
+	}
+}
+
+# expand (1 or 2 or 4) bit to 8 bit without scaling (for palletized stuff)
+
+expand(out: array of byte, outstride: int, in: array of byte, pixels: int, bpp: int)
+{
+	b: byte;
+	bits := pixels * bpp;
+	lim := bits / 8;
+	mask := byte ((1 << bpp) - 1);
+	outx := 0;
+	inx := 0;
+	for (x := 0; x < lim; x++) {
+		b = in[inx];
+		for (s := 8 - bpp; s >= 0; s -= bpp) {
+			out[outx] = (b >> s) & mask;
+			outx += outstride;
+		}
+		inx++;
+	}
+	residue := (bits % 8) / bpp;
+	if (residue) {
+		b = in[inx];
+		for (s := 8 - bpp; s >= 0; s -= bpp) {
+			out[outx] = (b >> s) & mask;
+			outx += outstride;
+			if (--residue <= 0)
+				break;
+		}
+	}
+}
+
+copybytes(out: array of byte, outstride: int, in: array of byte, instride: int, pixels: int)
+{
+	inx := 0;
+	outx := 0;
+	for (x := 0; x < pixels; x++) {
+		out[outx] = in[inx];
+		inx += instride;
+		outx += outstride;
+	}
+}
+
+outputrow(png: ref Png, raw: ref Rawimage, row: array of byte)
+{
+	offset := png.row * raw.r.max.x;
+	case raw.nchans {
+	1 =>
+		case (png.depth) {
+		* =>
+			png.error = "depth not supported";
+			return;
+		1 or 2 or 4 =>
+			if (raw.chandesc == RImagefile->CRGB1)
+				expand(raw.chans[0][offset + png.colstart:], png.colstep, row, png.phasecols, png.depth);
+			else
+				upconvert(raw.chans[0][offset + png.colstart:], png.colstep, row, png.phasecols, png.depth);
+		8 or 16 =>
+			# might have an Alpha channel to ignore!
+			stride := (png.alpha + 1) * png.depth / 8;
+			copybytes(raw.chans[0][offset + png.colstart:], png.colstep, row, stride, png.phasecols);
+		}
+	3 =>
+		case (png.depth) {
+		* =>
+			png.error = "depth not supported (2)";
+			return;
+		8 or 16 =>
+			# split rgb into three channels
+			bytespc := png.depth / 8;
+			stride := (3  + png.alpha) * bytespc;
+			copybytes(raw.chans[0][offset + png.colstart:], png.colstep, row, stride, png.phasecols);
+			copybytes(raw.chans[1][offset + png.colstart:], png.colstep, row[bytespc:], stride, png.phasecols);
+			copybytes(raw.chans[2][offset + png.colstart:], png.colstep, row[bytespc * 2:], stride, png.phasecols);
+		}
+	}
+}
+
+filtersub(png: ref Png)
+{
+	subx := 1;
+	for (x := int png.filterbpp + 1; x < png.rowsize; x++) {
+		png.thisrow[x] += png.thisrow[subx];
+		subx++;
+	}
+}
+
+filterup(png: ref Png)
+{
+	if (png.row == 0)
+		return;
+	for (x := 1; x < png.rowsize; x++)
+		png.thisrow[x] += png.lastrow[x];
+}
+
+filteraverage(png: ref Png)
+{
+	for (x := 1; x < png.rowsize; x++) {
+		a: int;
+		if (x > png.filterbpp)
+			a = int png.thisrow[x - png.filterbpp];
+		else
+			a = 0;
+		if (png.row != 0)
+			a += int png.lastrow[x];
+		png.thisrow[x] += byte (a / 2);
+	}
+}
+
+filterpaeth(png: ref Png)
+{
+	a, b, c: byte;
+	p, pa, pb, pc: int;
+	for (x := 1; x < png.rowsize; x++) {
+		if (x > png.filterbpp)
+			a = png.thisrow[x - png.filterbpp];
+		else
+			a = byte 0;
+		if (png.row == 0) {
+			b = byte 0;
+			c = byte 0;
+		} else {
+			b = png.lastrow[x];
+			if (x > png.filterbpp)
+				c = png.lastrow[x - png.filterbpp];
+			else
+				c = byte 0;
+		}
+		p = int a + int b - int c;
+		pa = p - int a;
+		if (pa < 0)
+			pa = -pa;
+		pb  = p - int b;
+		if (pb < 0)
+			pb = -pb;
+		pc = p - int c;
+		if (pc < 0)
+			pc = -pc;
+		if (pa <= pb && pa <= pc)
+			png.thisrow[x] += a;
+		else if (pb <= pc)
+			png.thisrow[x] += b;
+		else
+			png.thisrow[x] += c;
+	}		
+}
+
+phaseendcheck(png: ref Png, raw: ref Rawimage): int
+{
+	if (png.row >= raw.r.max.y || png.rowsize <= 1) {
+		# this phase is over
+		if (png.phase == 0) {
+			png.done = 1;
+		}
+		else {
+			png.phase++;
+			if (png.phase > 7)
+				png.done = 1;
+			else
+				processdatainitphase(png, raw);
+		}
+		return 1;
+	}
+	return 0;
+}
+
+processdata(png: ref Png, raw: ref Rawimage, buf: array of byte)
+{
+#sys->print("processdata(%d)\n", len buf);
+	if (png.error != nil)
+		return;
+	i := 0;
+	while (i < len buf) {
+		if (png.done) {
+			png.error = "too much data";
+			return;
+		}
+		if (phaseendcheck(png, raw))
+			continue;
+		tocopy := (png.rowsize - png.rowbytessofar);
+		if (tocopy > (len buf - i))
+			tocopy = len buf - i;
+		png.thisrow[png.rowbytessofar :] = buf[i : i + tocopy];
+		i += tocopy;
+		png.rowbytessofar += tocopy;
+		if (png.rowbytessofar >= png.rowsize) {
+			# a new row has arrived
+			# apply filter here
+#sys->print("phase %d row %d\n", png.phase, png.row);
+			case int png.thisrow[0] {
+			0 =>
+				;
+			1 =>
+				filtersub(png);
+			2 =>
+				filterup(png);
+			3 =>
+				filteraverage(png);
+			4 =>
+				filterpaeth(png);
+			* =>
+#				sys->print("implement filter method %d\n", int png.thisrow[0]);
+				png.error = "filter method unsupported";
+				return;
+			}
+			# output row
+			if (png.row >= raw.r.max.y) {
+				png.error = "too much data";
+				return;
+			}
+			outputrow(png, raw, png.thisrow[1 :]);
+			png.row += png.rowstep;
+			save := png.lastrow;
+			png.lastrow = png.thisrow;
+			png.thisrow = save;
+			png.rowbytessofar = 0;
+		}
+	}
+	phaseendcheck(png, raw);
+}
+
+get_signature(fd: ref Iobuf): int
+{
+	sig := array[8] of { byte 137, byte 80, byte 78, byte 71, byte 13, byte 10, byte 26, byte 10 };
+	x: int;
+	for (x = 0; x < 8; x++)
+		if (fd.getb() != int sig[x])
+			return 0;
+	return 1;
+}
+
+get_bytes(fd: ref Iobuf, crc_state: ref CRCstate, buf: array of byte, n: int): int
+{
+	if (buf == nil) {
+		fd.seek(big n, bufio->SEEKRELA);
+		return 1;
+	}
+	if (fd.read(buf, n) != n)
+		return 0;
+	if (crc_state != nil)
+		crc->crc(crc_state, buf, n);
+	return 1;
+}
+
+skip_bytes(fd: ref Iobuf, crc_state: ref CRCstate, n: int): int
+{
+	buf := array[1024] of byte;
+	while (n) {
+		thistime: int = 1024;
+		if (thistime > n)
+			thistime = n;
+		if (!get_bytes(fd, crc_state, buf, thistime))
+			return 0;
+		n -= thistime;
+	}
+	return 1;
+}
+
+get_4(fd: ref Iobuf, crc_state: ref CRCstate, signed: int): (int, int)
+{
+	buf := array[4] of byte;
+	if (!get_bytes(fd, crc_state, buf, 4))
+		return (0, 0);
+	if (signed && int buf[0] & 16r80)
+		return (0, 0);
+	r:int  = (int buf[0] << 24) | (int buf[1] << 16) | (int buf[2] << 8) | (int buf[3]);
+#	sys->print("got int %d\n", r);
+	return (1, r);
+}
+
+get_int(fd: ref Iobuf, crc_state: ref CRCstate): int
+{
+	ok, r: int;
+	(ok, r) = get_4(fd, crc_state, 1);
+	if (ok)
+		return r;
+	return -1;
+}
+
+get_ushort(fd: ref Iobuf, crc_state: ref CRCstate): int
+{
+	buf := array[2] of byte;
+	if (!get_bytes(fd, crc_state, buf, 2))
+		return -1;
+	return (int buf[0] << 8) | int buf[1];
+}
+
+get_crc_and_check(fd: ref Iobuf, chunk: ref Chunk): int
+{
+	crc, ok: int;
+	(ok, crc) = get_4(fd, nil, 0);
+	if (!ok)
+		return 0;
+#	sys->print("crc: computed %.8ux expected %.8ux\n", chunk.crc_state.crc, crc);
+	if (chunk.crc_state.crc != crc)
+		return 1;
+	return 1;
+}
+
+get_byte(fd: ref Iobuf, crc_state: ref CRCstate): int
+{
+	buf := array[1] of byte;
+	if (!get_bytes(fd, crc_state, buf, 1))
+		return -1;
+#	sys->print("got byte %d\n", int buf[0]);
+	return int buf[0];
+}
+
+get_type(fd: ref Iobuf, crc_state: ref CRCstate): string
+{
+	x: int;
+	buf := array[4] of byte;
+	if (!get_bytes(fd, crc_state, buf, 4))
+		return nil;
+	for (x = 0; x < 4; x++) {
+		c: int;
+		c = int buf[x];
+		if (c == bufio->EOF || (c < 65 || c > 90 && c < 97) || c > 122)
+			return nil;
+	}
+	return string buf;
+}
+
+get_chunk_header(fd: ref Iobuf, chunk: ref Chunk): int
+{
+	chunk.size = get_int(fd, nil);
+	if (chunk.size < 0)
+		return 0;
+	crc->reset(chunk.crc_state);
+	chunk.typ = get_type(fd, chunk.crc_state);
+	if (chunk.typ == nil)
+		return 0;
+#	sys->print("%s(%d)\n", chunk.typ, chunk.size);
+	return 1;
+}
--- /dev/null
+++ b/appl/lib/readxbitmap.b
@@ -1,0 +1,131 @@
+implement RImagefile;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "imagefile.m";
+
+init(iomod: Bufio)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	bufio = iomod;
+}
+
+readmulti(fd: ref Iobuf): (array of ref Rawimage, string)
+{
+	(i, err) := read(fd);
+	if(i != nil){
+		a := array[1] of { i };
+		return (a, err);
+	}
+	return (nil, err);
+}
+
+read(fd: ref Iobuf): (ref Rawimage, string)
+{
+	width, height, fnd: int;
+	(fnd, width) = get_define(fd);
+	if(fnd)
+		(fnd, height) = get_define(fd);
+	if(!fnd)
+		return (nil, "xbitmap doesn't start with width and height");
+	if(height <= 0 || width <= 0)
+		return (nil, "xbitmap has bad width or height");
+	# now, optional x_hot, y_hot
+	(fnd, nil) = get_define(fd);
+	if(fnd)
+		(fnd, nil) = get_define(fd);
+	# now expect 'static char x...x_bits[] = {'
+	if(!get_to_char(fd, '{'))
+		return (nil, "xbitmap premature eof");
+
+	bytesperline := (width+7) / 8;
+	pixels := array[width*height] of byte;
+	pixi := 0;
+	for(i := 0; i < height; i++) {
+		for(j := 0; j < bytesperline; j++) {
+			(vfnd, v) := get_hexbyte(fd);
+			if(!vfnd)
+				return (nil,  "xbitmap premature eof");
+			kend := 7;
+			if(j == bytesperline-1)
+				kend = (width-1)%8;
+			for(k := 0; k <= kend; k++) {
+				if(v & (1<<k))
+					pixels[pixi] = byte 0;
+				else
+					pixels[pixi] = byte 1;
+				pixi++;
+			}
+		}
+	}
+	cmap := array[6] of {byte 0, byte 0, byte 0,
+			byte 255, byte 255, byte 255};
+	chans := array[1] of {pixels};
+	ans := ref Rawimage(Draw->Rect((0,0),(width,height)), cmap, 0, byte 0, 1, chans, CRGB1, 0);
+	return (ans, "");
+}
+
+# get a line, which should be of form
+#	'#define fieldname val'
+# and return (found, integer rep of val)
+get_define(fd: ref Iobuf) : (int, int)
+{
+	c := fd.getc();
+	if(c != '#') {
+		fd.ungetc();
+		return (0, 0);
+	}
+	line := fd.gets('\n');
+	for(i := len line -1; i >= 0; i--)
+		if(line[i] == ' ')
+			break;
+	val := int line[i+1:];
+	return (1, val);
+}
+
+# read fd until get char cterm; return 1 if found
+get_to_char(fd: ref Iobuf, cterm: int) : int
+{
+	for(;;) {
+		c := fd.getc();
+		if(c < 0)
+			return c;
+		if(c == cterm)
+			return 1;
+	}
+}
+
+# read fd until get xDD, were DD are hex digits.
+# return (found, value of DD as integer)
+get_hexbyte(fd: ref Iobuf) : (int, int)
+{
+	if(!get_to_char(fd, 'x'))
+		return (0, 0);
+	n1 := hexdig(fd.getc());
+	n2 := hexdig(fd.getc());
+	if(n1 < 0 || n2 < 0)
+		return (0, 0);
+	return (1, (n1<<4) | n2);
+}
+
+hexdig(c: int) : int
+{
+	if('0' <= c && c <= '9')
+		c -= '0';
+	else if('a' <= c && c <= 'f')
+		c += 10 - 'a';
+	else if('A' <= c && c <= 'F')
+		c += 10 - 'A';
+	else
+		c = -1;
+	return c;
+}
--- /dev/null
+++ b/appl/lib/regex.b
@@ -1,0 +1,389 @@
+implement Regex;
+
+include "regex.m";
+
+# syntax
+
+# RE	ALT		regular expression
+#	NUL
+# ALT	CAT		alternation
+# 	CAT | ALT
+#
+# CAT	DUP		catenation
+# 	DUP CAT
+#
+# DUP	PRIM		possibly duplicated primary
+# 	PCLO
+# 	CLO
+# 	OPT
+#
+# PCLO	PRIM +		1 or more
+# CLO	PRIM *		0 or more
+# OPT	PRIM ?		0 or 1
+#
+# PRIM	( RE )
+#	()
+# 	DOT		any character
+# 	CHAR		a single character
+#	ESC		escape sequence
+# 	[ SET ]		character set
+# 	NUL		null string
+# 	HAT		beginning of string
+# 	DOL		end of string
+#
+
+NIL : con -1;		# a refRex constant
+NONE: con -2;		# ditto, for an un-set value
+BAD: con 1<<16;		# a non-character 
+HUGE: con (1<<31) - 1;
+
+# the data structures of re.m would like to be ref-linked, but are
+# circular (see fn walk), thus instead of pointers we use indexes
+# into an array (arena) of nodes of the syntax tree of a regular expression.
+# from a storage-allocation standpoint, this replaces many small
+# allocations of one size with one big one of variable size.
+
+ReStr: adt {
+	s : string;
+	i : int;	# cursor postion
+	n : int;	# number of chars left; -1 on error
+	peek : fn(s: self ref ReStr): int;
+	next : fn(s: self ref ReStr): int;
+};
+
+ReStr.peek(s: self ref ReStr): int
+{
+	if(s.n <= 0)
+		return BAD;
+	return s.s[s.i];
+}
+
+ReStr.next(s: self ref ReStr): int
+{
+	if(s.n <= 0)
+		return BAD;
+	s.n--;
+	return s.s[s.i++];
+}
+
+newRe(kind: int, left, right: refRex, set: ref Set, ar: ref Arena, pno: int): refRex
+{
+	ar.rex[ar.ptr] = Rex(kind, left, right, set, pno);
+	return ar.ptr++;
+}
+
+# parse a regex by recursive descent to get a syntax tree
+
+re(s: ref ReStr, ar: ref Arena): refRex
+{
+	left := cat(s, ar);
+	if(left==NIL || s.peek()!='|')
+		return left;
+	s.next();
+	right := re(s, ar);
+	if(right == NIL)
+		return NIL;
+	return newRe(ALT, left, right, nil, ar, 0);
+}
+
+cat(s: ref ReStr, ar: ref Arena): refRex
+{
+	left := dup(s, ar);
+	if(left == NIL)
+		return left;
+	right := cat(s, ar);
+	if(right == NIL)
+		return left;
+	return newRe(CAT, left, right, nil, ar, 0);
+}
+
+dup(s: ref ReStr, ar: ref Arena): refRex
+{
+	case s.peek() {
+	BAD or ')' or ']' or '|' or '?' or '*' or '+' =>
+		return NIL;
+	}
+	prim: refRex;
+	case kind:=s.next() {
+	'(' =>	if(ar.pno < 0) {
+			if(s.peek() == ')') {
+				s.next();
+				prim = newRe(NUL, NONE, NONE, nil, ar, 0);
+			} else {
+				prim = re(s, ar);
+				if(prim==NIL || s.next()!=')')
+					s.n = -1;
+			}
+		} else {
+			pno := ++ar.pno;
+			lp := newRe(LPN, NONE, NONE, nil, ar, pno);
+			rp := newRe(RPN, NONE, NONE, nil, ar, pno);
+			if(s.peek() == ')') {
+				s.next();
+				prim = newRe(CAT, lp, rp, nil, ar, 0);
+				
+			} else {
+				prim = re(s, ar);
+				if(prim==NIL || s.next()!=')')
+					s.n = -1;
+				else {
+					prim = newRe(CAT, prim, rp, nil, ar, 0);
+					prim = newRe(CAT, lp, prim, nil, ar, 0);
+				}
+			}
+		}
+	'[' =>	prim = newRe(SET, NONE, NONE, newSet(s), ar, 0);
+	* =>	case kind {
+		'.' =>	kind = DOT;
+		'^' =>	kind = HAT;
+		'$' =>	kind = DOL;
+		}
+		prim = newRe(esc(s, kind), NONE, NONE, nil, ar, 0);
+	}
+	case s.peek() {
+	'*' =>	kind = CLO;
+	'+' =>	kind = PCLO;
+	'?' =>	kind = OPT;
+	* =>	return prim;
+	}
+	s.next();
+	return newRe(kind, prim, NONE, nil, ar, 0);
+}
+
+esc(s: ref ReStr, char: int): int
+{
+	if(char == '\\') {
+		char = s.next();
+		case char {
+		BAD =>	s.n = -1;
+		'n' =>	char = '\n';
+		}
+	}
+	return char;
+}
+
+# walk the tree adjusting pointers to refer to 
+# next state of the finite state machine
+
+walk(r: refRex, succ: refRex, ar: ref Arena)
+{
+	if(r==NONE)
+		return;
+	rex := ar.rex[r];
+	case rex.kind {
+	ALT =>	walk(rex.left, succ, ar);
+		walk(rex.right, succ, ar);
+		return;
+	CAT =>	walk(rex.left, rex.right, ar);
+		walk(rex.right, succ, ar);
+		ar.rex[r] = ar.rex[rex.left];	# optimization
+		return;
+	CLO or PCLO =>
+		end := newRe(OPT, r, succ, nil, ar, 0); # here's the circularity
+		walk(rex.left, end, ar);
+	OPT =>	walk(rex.left, succ, ar);
+	}
+	ar.rex[r].right = succ;
+}
+
+compile(e: string, flag: int): (Re, string)
+{
+	if(e == nil)
+		return (nil, "missing expression");	
+	s := ref ReStr(e, 0, len e);
+	ar := ref Arena(array[2*s.n] of Rex, 0, 0, (flag&1)-1);
+	start := ar.start = re(s, ar);
+	if(start==NIL || s.n!=0)
+		return (nil, "invalid regular expression");
+	walk(start, NIL, ar);
+	if(ar.pno < 0)
+		ar.pno = 0;
+	return (ar, nil);
+}
+
+# todo1, todo2: queues for epsilon and advancing transitions
+Gaz: adt {
+	pno: int;
+	beg: int;
+	end: int;
+};
+Trace: adt {
+	cre: refRex;		# cursor in Re
+	beg: int;		# where this trace began;
+	gaz: list of Gaz;
+};
+Queue: adt {
+	ptr: int;
+	q: array of Trace;
+};
+
+execute(re: Re, s: string): array of (int, int)
+{
+	return executese(re, s, (-1,-1), 1, 1);
+}
+
+executese(re: Re, s: string, range: (int, int), bol: int, eol: int): array of (int,int)
+{
+	if(re==nil)
+		return nil;
+	(s0, s1) := range;
+	if(s0 < 0)
+		s0 = 0;
+	if(s1 < 0)
+		s1 = len s;
+	gaz : list of Gaz;
+	(beg, end) := (-1, -1);
+	todo1 := ref Queue(0, array[re.ptr] of Trace);
+	todo2 := ref Queue(0, array[re.ptr] of Trace);
+	for(i:=s0; i<=s1; i++) {
+		small2 := HUGE;		# earliest possible match if advance
+		if(beg == -1)		# no leftmost match yet
+			todo1.q[todo1.ptr++] = Trace(re.start, i, nil);
+		for(k:=0; k<todo1.ptr; k++) {
+			q := todo1.q[k];
+			rex := re.rex[q.cre];
+			next1 := next2 := NONE;
+			case rex.kind {
+			NUL =>
+				next1 = rex.right;
+			DOT =>
+				if(i<len s && s[i]!='\n')
+					next2 = rex.right;
+			HAT =>
+				if(i == s0 && bol)
+					next1 = rex.right;
+			DOL =>
+				if(i == s1 && eol)
+					next1 = rex.right;
+			SET =>
+				if(i<len s && member(s[i], rex.set))
+					next2 = rex.right;
+			CAT or
+			PCLO =>
+				next1 = rex.left;
+			ALT or 
+			CLO or 
+			OPT =>
+				next1 = rex.right;
+				k = insert(rex.left, q.beg, q.gaz, todo1, k);
+			LPN =>
+				next1 = rex.right;
+				q.gaz = Gaz(rex.pno,i,-1)::q.gaz;
+			RPN =>
+				next1 = rex.right;
+				for(r:=q.gaz; ; r=tl r) {
+					(pno,beg1,end1) := hd r;
+					if(rex.pno==pno && end1==-1) {
+						q.gaz = Gaz(pno,beg1,i)::q.gaz;
+						break;
+					}
+				}
+			* =>
+				if(i<len s && rex.kind==s[i])
+					next2 = rex.right;
+			}
+			if(next1 != NONE) {
+				if(next1 != NIL)
+					k =insert(next1, q.beg, q.gaz, todo1, k);
+				else if(better(q.beg, i, beg, end))
+					(gaz, beg, end) = (q.gaz, q.beg, i);
+			}
+			if(next2 != NONE) {
+				if(next2 != NIL) {
+					if(q.beg < small2)
+						small2 = q.beg;
+					insert(next2, q.beg, q.gaz, todo2, 0);
+				 } else if(better(q.beg, i+1, beg, end))
+					(gaz, beg, end) = (q.gaz, q.beg, i+1);
+			}
+			
+		}
+		if(beg!=-1 && beg<small2)	# nothing better possible
+			break;
+		(todo1,todo2) = (todo2, todo1);
+		todo2.ptr = 0;
+	}
+	if(beg == -1)
+		return nil;
+	result := array[re.pno+1] of { 0 => (beg,end), * => (-1,-1) };
+	for( ; gaz!=nil; gaz=tl gaz) {
+		(pno, beg1, end1) := hd gaz;
+		(rbeg, nil) := result[pno];
+		if(rbeg==-1 && (beg1|end1)!=-1)
+			result[pno] = (beg1,end1);
+	}
+	return result;
+}
+
+better(newbeg, newend, oldbeg, oldend: int): int
+{
+	return oldbeg==-1 || newbeg<oldbeg ||
+	       newbeg==oldbeg && newend>oldend;
+}
+
+insert(next: refRex, tbeg: int, tgaz: list of Gaz, todo: ref Queue, k: int): int
+{
+	for(j:=0; j<todo.ptr; j++)
+		if(todo.q[j].cre == next)
+			if(todo.q[j].beg <= tbeg)
+			 	return k;
+			else
+				break;
+	if(j < k)
+		k--;
+	if(j < todo.ptr)
+		todo.ptr--;
+	for( ; j<todo.ptr; j++)
+		todo.q[j] = todo.q[j+1];
+	todo.q[todo.ptr++] = Trace(next, tbeg, tgaz);
+	return k;
+}
+
+ASCII : con 128;
+WORD : con 32;
+
+member(char: int, set: ref Set): int
+{
+	if(char < 128)
+		return ((set.ascii[char/WORD]>>char%WORD)&1)^set.neg;
+	for(l:=set.unicode; l!=nil; l=tl l) {
+		(beg, end) := hd l;
+		if(char>=beg && char<=end)
+			return !set.neg;
+	}
+	return set.neg;
+}
+
+newSet(s: ref ReStr): ref Set
+{
+	set := ref Set(0, array[ASCII/WORD] of {* => 0}, nil);
+	if(s.peek() == '^') {
+		set.neg = 1;
+		s.next();
+	}
+	while(s.n > 0) {
+		char1 := s.next();
+		if(char1 == ']')
+			return set;
+		char1 = esc(s, char1);
+		char2 := char1;
+		if(s.peek() == '-') {
+			s.next();
+			char2 = s.next();
+			if(char2 == ']')
+				break;
+			char2 = esc(s, char2);
+			if(char2 < char1)
+				break;
+		}
+		for( ; char1<=char2; char1++)
+			if(char1 < ASCII)
+				set.ascii[char1/WORD] |= 1<<char1%WORD;
+			else {
+				set.unicode = (char1,char2)::set.unicode;
+				break;
+			}
+	}
+	s.n = -1;
+	return nil;
+}
--- /dev/null
+++ b/appl/lib/registries.b
@@ -1,0 +1,291 @@
+implement Registries;
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "keyring.m";
+	keyring: Keyring;
+include "dial.m";
+	dial: Dial;
+include "security.m";
+	auth: Auth;
+include "keyset.m";
+	keyset: Keyset;
+include "registries.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	bufio = checkload(load Bufio Bufio->PATH, Bufio->PATH);
+	keyring = checkload(load Keyring Keyring->PATH, Keyring->PATH);
+	str = checkload(load String String->PATH, String->PATH);
+	keyset = checkload(load Keyset Keyset->PATH, Keyset->PATH);
+	dial = checkload(load Dial Dial->PATH, Dial->PATH);
+	auth = checkload(load Auth Auth->PATH, Auth->PATH);
+	e := keyset->init();
+	if(e != nil)
+		raise sys->sprint("can't init Keyset: %s", e);
+	e = auth->init();
+	if(e != nil)
+		raise sys->sprint("can't init Auth: %s", e);
+}
+
+checkload[T](x: T, s: string): T
+{
+	if(x == nil)
+		raise sys->sprint("can't load %s: %r", s);
+	return x;
+}
+
+Registry.new(dir: string): ref Registry
+{
+	if(dir == nil)
+		dir = "/mnt/registry";
+	r := ref Registry;
+	r.dir = dir;
+	r.indexfd = sys->open(dir + "/index", Sys->OREAD);
+	if(r.indexfd == nil)
+		return nil;
+	return r;
+}
+
+Registry.connect(svc: ref Service, user, keydir: string): ref Registry
+{
+	# XXX broadcast for local registries here.
+	if(svc == nil)
+	#	svc = ref Service("net!$registry!registry", Attributes.new(("auth", "infpk1") :: nil));
+		svc = ref Service("net!$registry!registry", Attributes.new(("auth", "none") :: nil));
+	a := svc.attach(user, keydir);
+	if(a == nil)
+		return nil;
+	if(sys->mount(a.fd, nil, "/mnt/registry", Sys->MREPL, nil) == -1){
+		sys->werrstr(sys->sprint("mount failed: %r"));
+		return nil;
+	}
+	return Registry.new("/mnt/registry");
+}
+
+Registry.services(r: self ref Registry): (list of ref Service, string)
+{
+	sys->seek(r.indexfd, big 0, Sys->SEEKSTART);
+	iob := bufio->fopen(r.indexfd, Sys->OREAD);
+	if(iob == nil)
+		return (nil, sys->sprint("%r"));
+	return (readservices(iob), nil);
+}
+
+Registry.find(r: self ref Registry, a: list of (string, string)): (list of ref Service, string)
+{
+	fd := sys->open(r.dir + "/find", Sys->ORDWR);	# could keep it open if it's a bottleneck
+	if(fd == nil)
+		return (nil, sys->sprint("%r"));
+	s := "";
+	if(a != nil){
+		for(; a != nil; a = tl a){
+			(n, v) := hd a;
+			s += sys->sprint(" %q %q", n, v);
+		}
+		s = s[1:];
+	}
+	if(sys->fprint(fd, "%s", s) == -1)
+		return (nil, sys->sprint("%r"));
+	sys->seek(fd, big 0, Sys->SEEKSTART);
+	iob := bufio->fopen(fd, Sys->OREAD);
+	return (readservices(iob), nil);
+}
+
+readservices(iob: ref Iobuf): list of ref Service
+{
+	services: list of ref Service;
+	while((s := qgets(iob, '\n')) != nil){
+		toks := str->unquoted(s);
+		if(toks == nil || len toks % 2 != 1)
+			continue;
+		svc := ref Service(hd toks, nil);
+		attrs, rattrs: list of (string, string);
+		for(toks = tl toks; toks != nil; toks = tl tl toks)
+			rattrs = (hd toks, hd tl toks) :: rattrs;
+		for(; rattrs != nil; rattrs = tl rattrs)
+			attrs = hd rattrs :: attrs;
+		svc.attrs = ref Attributes(attrs);
+		services = svc :: services;
+	}
+	return rev(services);
+}
+
+rev[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+Registry.register(r: self ref Registry, addr: string, attrs: ref Attributes, persist: int): (ref Registered, string)
+{
+	fd := sys->open(r.dir + "/new", Sys->OWRITE);
+	if(fd == nil)
+		return (nil, sys->sprint("%r"));
+	s := sys->sprint("%q", addr);
+	for(a := attrs.attrs; a != nil; a = tl a)
+		s += sys->sprint(" %q %q", (hd a).t0, (hd a).t1);
+	if(persist)
+		s += " persist 1";
+	if(sys->fprint(fd, "%s", s) == -1)
+		return (nil, sys->sprint("%r"));
+	return (ref Registered(addr, r, fd), nil);
+}
+
+Registry.unregister(r: self ref Registry, addr: string): string
+{
+	if(sys->remove(r.dir + "/" + addr) == -1)
+		return sys->sprint("%r");
+	return nil;
+}
+
+Attributes.new(attrs: list of (string, string)): ref Attributes
+{
+	return ref Attributes(attrs);
+}
+
+Attributes.set(a: self ref Attributes, attr, val: string)
+{
+	for(al := a.attrs; al != nil; al = tl al)
+		if((hd al).t0 == attr)
+			break;
+	if(al == nil){
+		a.attrs = (attr, val) :: a.attrs;
+		return;
+	}
+	attrs := (attr, val) :: tl al;
+	for(al = a.attrs; al != nil; al = tl al){
+		if((hd al).t0 == attr)
+			break;
+		attrs = hd al :: attrs;
+	}
+	a.attrs = attrs;
+}
+
+Attributes.get(a: self ref Attributes, attr: string): string
+{
+	for(al := a.attrs; al != nil; al = tl al)
+		if((hd al).t0 == attr)
+			return (hd al).t1;
+	return nil;
+}
+
+qgets(iob: ref Iobuf, eoc: int): string
+{
+	inq := 0;
+	s := "";
+	while((c := iob.getc()) >= 0){
+		s[len s] = c;
+		if(inq){
+			if(c == '\''){
+				c = iob.getc();
+				if(c == '\'')
+					s[len s] = c;
+				else{
+					iob.ungetc();
+					inq = 0;
+				}
+			}
+		}else{
+			if(c == eoc)
+				return s;
+			if(c == '\'')
+				inq = 1;
+		}
+	}
+	return s;
+}
+
+Service.attach(svc: self ref Service, localuser, keydir: string): ref Attached
+{
+	# attributes used:
+	# 	auth			type of authentication to perform (auth, none)
+	#	auth.crypt		type of encryption to push (as accepted by ssl(3)'s "alg" operation)
+	#	auth.signer	hash of service's certificate's signer's public key
+
+	c := dial->dial(svc.addr, nil);
+	if(c == nil){
+		sys->werrstr(sys->sprint("cannot dial: %r"));
+		return nil;
+	}
+	attached := ref Attached;
+	authkind := svc.attrs.get("auth");
+	case authkind {
+	"auth" or		# old
+	"infpk1" =>
+		cryptalg := svc.attrs.get("auth.crypt");
+		if(cryptalg == nil)
+			cryptalg = "none";
+		ca := svc.attrs.get("auth.signer");
+		kf: string;
+		if(ca != nil){
+			(kfl, err) := keyset->keysforsigner(nil, ca, nil, keydir);
+			if(kfl == nil){
+				s := "no matching keys found";
+				if(err != nil)
+					s += ": "+err;
+				sys->werrstr(s);
+				return nil;
+			}
+			if(localuser == nil)
+				kf = (hd kfl).t0;
+			else{
+				for(; kfl != nil; kfl = tl kfl)
+					if((hd kfl).t1 == localuser)
+						break;
+				if(kfl == nil){
+					sys->werrstr("no matching user found");
+					return nil;
+				}
+				kf = (hd kfl).t0;
+			}
+		} else {
+			user := readname("/dev/user");
+			if(user == nil)
+				kf = "/lib/keyring/default";
+			else
+				kf = "/usr/" + user + "/keyring/default";
+		}
+		info := keyring->readauthinfo(kf);
+		if(info == nil){
+			sys->werrstr(sys->sprint("cannot read key: %r"));
+			return nil;
+		}
+		(fd, ue) := auth->client(cryptalg, info, c.dfd);
+		if(fd == nil){
+			sys->werrstr(sys->sprint("cannot authenticate: %r"));
+			return nil;
+		}
+		attached.signerpkhash = keyset->pkhash(keyring->pktostr(info.spk));
+		attached.localuser = info.mypk.owner;
+		attached.remoteuser = ue;
+		attached.fd = fd;
+	"" or
+	"none" =>
+		attached.fd = c.dfd;
+	* =>
+		sys->werrstr(sys->sprint("unknown authentication type %q", authkind));
+		return nil;
+	}
+	return attached;
+}
+
+readname(s: string): string
+{
+	fd := sys->open(s, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/lib/rfc822.b
@@ -1,0 +1,561 @@
+implement RFC822;
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+	
+include "rfc822.m";
+
+include "string.m";
+	str: String;
+
+include "daytime.m";
+	daytime: Daytime;
+	Tm: import daytime;
+
+Minrequest: con 512;	# more than enough for most requests
+
+Suffix: adt {
+	suffix: string;
+	generic: string;
+	specific: string;
+	encoding: string;
+};
+
+SuffixFile: con "/lib/mimetype";
+mtime := 0;
+qid: Sys->Qid;
+
+suffixes: list of ref Suffix;
+
+nomod(s: string)
+{
+	raise sys->sprint("internal: can't load %s: %r", s);
+}
+
+init(b: Bufio)
+{
+	sys = load Sys Sys->PATH;
+	bufio = b;
+	str = load String String->PATH;
+	if(str == nil)
+		nomod(String->PATH);
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		nomod(Daytime->PATH);
+	readsuffixfile();
+}
+
+readheaders(fd: ref Iobuf, limit: int): array of (string, array of byte)
+{
+	n := 0;
+	s := 0;
+	b := array[Minrequest] of byte;
+	nline := 0;
+	lines: list of array of byte;
+	while((c := fd.getb()) >= 0){
+		if(c == '\r'){
+			c = fd.getb();
+			if(c < 0)
+				break;
+			if(c != '\n'){
+				fd.ungetb();
+				c = '\r';
+			}
+		}
+		if(n >= len b){
+			if(len b >= limit)
+				return nil;
+			ab := array[n+512] of byte;
+			ab[0:] = b;
+			b = ab;
+		}
+		b[n++] = byte c;
+		if(c == '\n'){
+			if(n == 1 || b[n-2] == byte '\n')
+				break;	# empty line
+			c = fd.getb();
+			if(c < 0)
+				break;
+			if(c != ' ' && c != '\t'){	# not continued
+				fd.ungetb();
+				lines = b[s: n] :: lines;
+				nline++;
+				s = n;
+			}else
+				b[n-1] = byte ' ';
+		}
+	}
+	if(n == 0)
+		return nil;
+	b = b[0: n];
+	if(n != s){
+		lines = b[s:n] :: lines;
+		nline++;
+	}
+	a := array[nline] of (string, array of byte);
+	for(; lines != nil; lines = tl lines){
+		b = hd lines;
+		name := "";
+		for(i := 0; i < len b; i++)
+			if(b[i] == byte ':'){
+				name = str->tolower(string b[0:i]);
+				b = b[i+1:];
+				break;
+			}
+		a[--nline] = (name, b);
+	}
+	return a;
+}
+
+#
+# *(";" parameter) used in transfer-extension, media-type and media-range
+# parameter = attribute "=" value
+# attribute = token
+# value = token | quoted-string
+#
+parseparams(ps: ref Rfclex): list of (string, string)
+{
+	l: list of (string, string);
+	do{
+		if(ps.lex() != Word)
+			break;
+		attr := ps.wordval;
+		if(ps.lex() != '=' || ps.lex() != Word && ps.tok != QString)
+			break;
+		l = (attr, ps.wordval) :: l;
+	}while(ps.lex() == ';');
+	ps.unlex();
+	return rev(l);
+}
+
+#
+# 1#transfer-coding
+#
+mimefields(ps: ref Rfclex): list of (string, list of (string, string))
+{
+	rf: list of (string, list of (string, string));
+	do{
+		if(ps.lex() == Word){
+			w := ps.wordval;
+			if(ps.lex() == ';'){
+				rf = (w, parseparams(ps)) :: rf;
+				ps.lex();
+			}else
+				rf = (w, nil) :: rf;
+		}
+	}while(ps.tok == ',');
+	ps.unlex();
+	f: list of (string, list of (string, string));
+	for(; rf != nil; rf = tl rf)
+		f = hd rf :: f;
+	return f;
+}
+
+#	#(media-type | (media-range [accept-params]))	; Content-Type and Accept
+#
+#       media-type     = type "/" subtype *( ";" parameter )
+#       type           = token
+#       subtype        = token
+#	LWS must not be used between type and subtype, nor between attribute and value (in parameter)
+#
+#	media-range = ("*/*" | type "/*" | type "/" subtype ) *(";' parameter)
+#    	accept-params  = ";" "q" "=" qvalue *( accept-extension )
+#	accept-extension = ";" token [ "=" ( token | quoted-string ) ]
+#
+#	1#( ( charset | "*" )[ ";" "q" "=" qvalue ] )		; Accept-Charset
+#	1#( codings [ ";" "q" "=" qvalue ] )			; Accept-Encoding
+#	1#( language-range [ ";" "q" "=" qvalue ] )		; Accept-Language
+#
+#	codings = ( content-coding | "*" )
+#
+parsecontent(ps: ref Rfclex, multipart: int, head: list of ref Content): list of ref Content
+{
+	do{
+		if(ps.lex() == Word){
+			generic := ps.wordval;
+			specific := "*";
+			if(ps.lex() == '/'){
+				if(ps.lex() != Word)
+					break;
+				specific = ps.wordval;
+				if(!multipart && specific != "*")
+					break;
+			}else if(multipart)
+				break;	# syntax error
+			else
+				ps.unlex();
+			params: list of (string, string) = nil;
+			if(ps.lex() == ';'){
+				params = parseparams(ps);
+				ps.lex();
+			}
+			head = Content.mk(generic, specific, params) :: head;	# order reversed, but doesn't matter
+		}
+	}while(ps.tok == ',');
+	ps.unlex();
+	return head;
+}
+
+rev(l: list of (string, string)): list of (string, string)
+{
+	rl: list of (string, string);
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+Rfclex.mk(a: array of byte): ref Rfclex
+{
+	ps := ref Rfclex;
+	ps.fd = bufio->aopen(a);
+	ps.tok = '\n';
+	ps.eof = 0;
+	return ps;
+}
+
+Rfclex.getc(ps: self ref Rfclex): int
+{
+	c := ps.fd.getb();
+	if(c < 0)
+		ps.eof = 1;
+	return c;
+}
+
+Rfclex.ungetc(ps: self ref Rfclex)
+{
+	if(!ps.eof)
+		ps.fd.ungetb();
+}
+
+Rfclex.lex(ps: self ref Rfclex): int
+{
+	if(ps.seen != nil){
+		(ps.tok, ps.wordval) = hd ps.seen;
+		ps.seen = tl ps.seen;
+	}else
+		ps.tok = lex1(ps, 0);
+	return ps.tok;
+}
+
+Rfclex.unlex(ps: self ref Rfclex)
+{
+	ps.seen = (ps.tok, ps.wordval) :: ps.seen;
+}
+
+Rfclex.skipws(ps: self ref Rfclex): int
+{
+	return lex1(ps, 1);
+}
+
+#
+# rfc 2822/rfc 1521 lexical analyzer
+#
+lex1(ps: ref Rfclex, skipwhite: int): int
+{
+	ps.wordval = nil;
+	while((c := ps.getc()) >= 0){
+		case c {
+		 '(' =>
+			level := 1;
+			while((c = ps.getc()) != Bufio->EOF && c != '\n'){
+				if(c == '\\'){
+					c = ps.getc();
+					if(c == Bufio->EOF)
+						return '\n';
+					continue;
+				}
+				if(c == '(')
+					level++;
+				else if(c == ')' && --level == 0)
+					break;
+			}
+ 		' ' or '\t' or '\r' or 0 =>
+			;
+ 		'\n' =>
+			return '\n';
+		')' or '<' or '>' or '[' or ']' or '@' or '/' or ',' or
+		';' or ':' or '?' or '=' =>
+			if(skipwhite){
+				ps.ungetc();
+				return c;
+			}
+			return c;
+
+ 		'"' =>
+			if(skipwhite){
+				ps.ungetc();
+				return c;
+			}
+			word(ps,"\"");
+			ps.getc();		# skip the closing quote 
+			return QString;
+
+ 		* =>
+			ps.ungetc();
+			if(skipwhite)
+				return c;
+			word(ps,"\"()<>@,;:/[]?={}\r\n \t");
+			return Word;
+		}
+	}
+	return '\n';
+}
+
+# return the rest of an rfc 822 line, not including \r or \n
+# do not map to lower case
+
+Rfclex.line(ps: self ref Rfclex): string
+{
+	s := "";
+	while((c := ps.getc()) != Bufio->EOF && c != '\n' && c != '\r'){
+		if(c == '\\'){
+			c = ps.getc();
+			if(c == Bufio->EOF)
+				break;
+		}
+		s[len s] = c;
+	}
+	ps.tok = '\n';
+	ps.wordval = s;
+	return s;
+}
+
+word(ps: ref Rfclex, stop: string)
+{
+	w := "";
+	while((c := ps.getc()) != Bufio->EOF){
+		if(c == '\r')
+			c = ' ';
+		if(c == '\\'){
+			c = ps.getc();
+			if(c == Bufio->EOF)
+				break;
+		}else if(str->in(c,stop)){
+			ps.ungetc();
+			break;
+		}
+		if(c >= 'A' && c <= 'Z')
+			c += 'a' - 'A';
+		w[len w] = c;
+	}
+	ps.wordval = w;
+}
+
+readsuffixfile(): string
+{
+	iob := bufio->open(SuffixFile, Bufio->OREAD);
+	if(iob == nil)
+		return sys->sprint("cannot open %s: %r", SuffixFile);
+	for(n := 1; (line := iob.gets('\n')) != nil; n++){
+		(s, nil) := parsesuffix(line);
+		if(s != nil)
+			suffixes =  s :: suffixes;
+	}
+	return nil;
+}
+
+parsesuffix(line: string): (ref Suffix, string)
+{
+	(line, nil) = str->splitstrl(line, "#");
+	if(line == nil)
+		return (nil, nil);
+	(n, slist) := sys->tokenize(line,"\n\t ");
+	if(n == 0)
+		return (nil, nil);
+	if(n < 4)
+		return (nil, "too few fields");
+	s := ref Suffix;
+	s.suffix = hd slist;
+	slist = tl slist;
+	s.generic = hd slist;
+	if (s.generic == "-")
+		s.generic = "";	
+	slist = tl slist;
+	s.specific = hd slist;
+	if (s.specific == "-")
+		s.specific = "";	
+	slist = tl slist;
+	s.encoding = hd slist;
+	if (s.encoding == "-")
+		s.encoding = "";
+	if((s.generic == nil || s.specific == nil) && s.encoding == nil)
+		return (nil, nil);
+	return (s, nil);
+}
+
+#
+# classify by file suffix
+#
+suffixclass(name: string): (ref Content, ref Content)
+{
+	typ, enc: ref Content;
+
+	p := str->splitstrr(name, "/").t1;
+	if(p != nil)
+		name = p;
+
+	for(;;){
+		(name, p) = suffix(name);	# TO DO: match below is case sensitive
+		if(p == nil)
+			break;
+		for(l := suffixes; l != nil; l = tl l){
+			s := hd l;
+			if(p == s.suffix){	
+				if(s.generic != nil && typ == nil)
+					typ = Content.mk(s.generic, s.specific, nil);
+				if(s.encoding != nil && enc == nil)
+					enc = Content.mk(s.encoding, "", nil);
+				if(typ != nil && enc != nil)
+					break;
+			}
+		}
+	}
+	return (typ, enc);
+}
+
+suffix(s: string): (string, string)
+{
+	for(n := len s; --n >= 0;)
+		if(s[n] == '.')
+			return (s[0: n], s[n:]);
+	return (s, nil);
+}
+
+#
+#  classify by initial contents of file
+#
+dataclass(a: array of byte): (ref Content, ref Content)
+{
+	utf8 := 0;
+	for(i := 0; i < len a;){
+		c := int a[i];
+		if(c < 16r80){
+			if(c < 32 && c != '\n' && c != '\r' && c != '\t' && c != '\v' && c != '\f')
+				return (nil, nil);
+			i++;
+		}else{
+			utf8 = 1;
+			(r, l, nil) := sys->byte2char(a, i);
+			if(r == Sys->UTFerror)
+				return (nil, nil);
+			i += l;
+		}
+	}
+	if(utf8)
+		params := ("charset", "utf-8") :: nil;
+	return (Content.mk("text", "plain", params), nil);
+}
+
+Content.mk(generic, specific: string, params: list of (string, string)): ref Content
+{
+	c := ref Content;	
+	c.generic = generic;
+	c.specific = specific;
+	c.params = params;
+	return c;
+}
+
+Content.check(me: self ref Content, oks: list of ref Content): int
+{
+	if(oks == nil)
+		return 1;
+	g := str->tolower(me.generic);
+	s := str->tolower(me.specific);
+	for(; oks != nil; oks = tl oks){
+		ok := hd oks;
+		if((ok.generic == g || ok.generic=="*") &&
+		   (s == nil || ok.specific == s || ok.specific=="*"))
+			return 1;
+	}
+	return 0;
+}
+
+Content.text(c: self ref Content): string
+{
+	if((s := c.specific) != nil)
+		s = c.generic+"/"+s;
+	else
+		s = c.generic;
+	for(l := c.params; l != nil; l = tl l){
+		(n, v) := hd l;
+		s += sys->sprint(";%s=%s", n, quote(v));
+	}
+	return s;
+}
+
+#
+# should probably be in a Mime or HTTP module
+#
+
+Quotable: con "()<>@,;:\\\"/[]?={} \t";
+
+quotable(s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(str->in(s[i], Quotable))
+			return 1;
+	return 0;
+}
+
+quote(s: string): string
+{
+	if(!quotable(s))
+		return s;
+	q :=  "\"";
+	for(i := 0; i < len s; i++){
+		if(str->in(s[i], Quotable))
+			q[len q] = '\\';
+		q[len q] = s[i];
+	}
+	q[len q] = '"';
+	return q;
+}
+
+weekdays := array[] of {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+months := array[] of {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+# print dates in the format
+# Wkd, DD Mon YYYY HH:MM:SS GMT
+
+sec2date(t: int): string
+{
+	tm := daytime->gmt(t);
+	return sys->sprint("%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT",
+		weekdays[tm.wday], tm.mday, months[tm.mon], tm.year+1900,
+		tm.hour, tm.min, tm.sec);	
+}
+
+# parse dates of formats
+# Wkd, DD Mon YYYY HH:MM:SS GMT
+# Weekday, DD-Mon-YY HH:MM:SS GMT
+# Wkd Mon ( D|DD) HH:MM:SS YYYY
+# plus anything similar
+
+date2sec(date: string): int
+{
+	tm := daytime->string2tm(date);
+	if(tm == nil || tm.year < 70 || tm.zone != "GMT")
+		t := 0;
+	else
+		t = daytime->tm2epoch(tm);
+	return t;
+}
+
+now(): int
+{
+	return daytime->now();
+}
+
+time(): string
+{
+	return sec2date(daytime->now());
+}
--- /dev/null
+++ b/appl/lib/riff.b
@@ -1,0 +1,225 @@
+implement Riff;
+
+include "sys.m";
+
+sys: Sys;
+
+include "riff.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+open(file: string): (ref RD, string)
+{
+	fd := sys->open(file, sys->OREAD);
+	if(fd == nil)
+		return (nil, "open failed");
+
+	r := ref RD;
+	r.fd = fd;
+	r.buf = array[DEFBUF] of byte;
+	r.ptr = 0;
+	r.nbyte = 0;
+
+	(hdr, l) := r.gethdr();
+	if(hdr != "RIFF")
+		return (nil, "not a RIFF file");
+
+	return (r, nil);
+}
+
+RD.gethdr(r: self ref RD): (string, int)
+{
+	b := array[8] of byte;
+
+	if(r.readn(b, 8) != 8)
+		return (nil, -1);
+
+	return (string b[0:4], ledword(b, 4));
+}
+
+RD.check4(r: self ref RD, code: string): string
+{
+	b := array[4] of byte;
+
+	if(r.readn(b, 4) != 4)
+		return "file i/o error";
+	if(string b != code)
+		return "bad four code header information";
+	return nil;
+}
+
+RD.avihdr(r: self ref RD): (ref AVIhdr, string)
+{
+	(s, l) := r.gethdr();
+	if(s == nil || s != "avih")
+		return (nil, "missing/malformed avih");
+
+	b := array[AVImainhdr] of byte;
+	if(r.readn(b, AVImainhdr) != AVImainhdr)
+		return (nil, "short read in avih");
+
+	h := ref AVIhdr;
+
+	h.usecperframe = ledword(b, 0);
+	h.bytesec = ledword(b, 4);
+	h.flag = ledword(b, 12);
+	h.frames = ledword(b, 16);
+	h.initframes = ledword(b, 20);
+	h.streams = ledword(b, 24);
+	h.bufsize = ledword(b, 28);
+	h.width = ledword(b, 32);
+	h.height = ledword(b, 36);
+
+	return (h, nil);
+}
+
+RD.streaminfo(r: self ref RD): (ref AVIstream, string)
+{
+	(h, l) := r.gethdr();
+	if(h != "LIST")
+		return (nil, "streaminfo expected LIST");
+
+	err := r.check4("strl");
+	if(err != nil)
+		return (nil, err);
+
+	(strh, sl) := r.gethdr();
+	if(strh != "strh")
+		return (nil, "streaminfo expected strh");
+
+	b := array[sl] of byte;
+	if(r.readn(b, sl) != sl)
+		return (nil, "streaminfo strl short read");
+
+	s := ref AVIstream;
+
+	s.stype = string b[0:4];
+	s.handler = string b[4:8];
+	s.flags = ledword(b, 8);
+	s.priority = ledword(b, 12);
+	s.initframes = ledword(b, 16);
+	s.scale = ledword(b, 20);
+	s.rate = ledword(b, 24);
+	s.start = ledword(b, 28);
+	s.length = ledword(b, 32);
+	s.bufsize = ledword(b, 36);
+	s.quality = ledword(b, 40);
+	s.samplesz = ledword(b, 44);
+
+	(strf, sf) := r.gethdr();
+	if(strf != "strf")
+		return (nil, "streaminfo expected strf");
+
+	s.fmt = array[sf] of byte;
+	if(r.readn(s.fmt, sf) != sf)
+		return (nil, "streaminfo strf short read");
+
+	return (s, nil);
+}
+
+RD.readn(r: self ref RD, b: array of byte, l: int): int
+{
+	if(r.nbyte < l) {
+		c := 0;
+		if(r.nbyte != 0) {
+			b[0:] = r.buf[r.ptr:];
+			l -= r.nbyte;
+			c += r.nbyte;
+			b = b[r.nbyte:];
+		}
+		bsize := len r.buf;
+		while(l != 0) {
+			r.nbyte = sys->read(r.fd, r.buf, bsize);
+			if(r.nbyte <= 0) {
+				r.nbyte = 0;
+				return -1;
+			}
+			n := l;
+			if(n > bsize)
+				n = bsize;
+
+			r.ptr = 0;
+			b[0:] = r.buf[0:n];
+			b = b[n:];
+			r.nbyte -= n;
+			r.ptr += n;
+			l -= n;
+			c += n;
+		}
+		return c;
+	}
+	b[0:] = r.buf[r.ptr:r.ptr+l];
+	r.nbyte -= l;
+	r.ptr += l;
+	return l;
+}
+
+RD.skip(r: self ref RD, size: int): int
+{
+	if(r.nbyte != 0) {
+		n := size;
+		if(n > r.nbyte)
+			n = r.nbyte;
+		r.ptr += n;
+		r.nbyte -= n;
+		size -= n;
+		if(size == 0)
+			return 0;
+	}
+	return int sys->seek(r.fd, big size, sys->SEEKRELA);
+}
+
+AVIstream.fmt2binfo(a: self ref AVIstream): string
+{
+	if(len a.fmt < Binfosize)
+		return "format is wrong size for BITMAPINFO";
+
+	b := ref Bitmapinfo;
+
+	# Pull out the bitmap info
+	b.width = ledword(a.fmt, 4);
+	b.height = ledword(a.fmt, 8);
+	b.planes = leword(a.fmt, 12);
+	b.bitcount = leword(a.fmt, 14);
+	b.compression = ledword(a.fmt, 16);
+	b.sizeimage = ledword(a.fmt, 20);
+	b.xpelpermeter = ledword(a.fmt, 24);
+	b.ypelpermeter = ledword(a.fmt, 28);
+	b.clrused = ledword(a.fmt, 32);
+	b.clrimportant = ledword(a.fmt, 36);
+
+	# Parse out the color map
+	ncolor := len a.fmt - Binfosize;
+	if(ncolor & 3)
+		return "wrong size color map";
+	ncolor /= 4;
+
+	b.cmap = array[ncolor] of RGB;
+	idx := 40;
+	for(i := 0; i < ncolor; i++) {
+		b.cmap[i].r = int a.fmt[idx+2];
+		b.cmap[i].g = int a.fmt[idx+1];
+		b.cmap[i].b = int a.fmt[idx+0];
+		idx += 4;
+	}
+
+	a.fmt = nil;
+	a.binfo = b;
+	return nil;
+}
+
+leword(b: array of byte, o: int): int
+{
+	return 	(int b[o+1] << 8) | int b[o];
+}
+
+ledword(b: array of byte, o: int): int
+{
+	return	(int b[o+3] << 24) |
+		(int b[o+2] << 16) |
+		(int b[o+1] << 8) |
+		int b[o];
+}
--- /dev/null
+++ b/appl/lib/scoretable.b
@@ -1,0 +1,150 @@
+# Copyright  © 1999 Roger Peppe.  All rights reserved.
+implement Scoretable;
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "scoretable.m";
+
+# this is the cut-down version - it doesn't bother
+# with score table locking at all; there is such a version,
+# but it needs a lock server, so is often more hassle than
+# it's worth. if you want a distributed score file, contact
+# rog@vitanuova.com
+# currently this module is only used by tetris - the interface
+# will probably change in the future.
+
+scorefile: string;
+username: string;
+
+MAXSCORES: con 10;
+
+init(nil: int, user, nil: string, sfile: string): (int, string)
+{
+	if (sys == nil) {
+		sys = load Sys Sys->PATH;
+		stderr = sys->fildes(2);
+		bufio = load Bufio Bufio->PATH;
+		if (bufio == nil) {
+			sys = nil;
+			return (-1, sys->sprint("cannot load %s: %r", Bufio->PATH));
+		}
+	}
+	username = user;
+	lock();
+	scorefd: ref Sys->FD;
+	if ((scorefd = sys->open(sfile, Sys->ORDWR)) == nil
+	&& (scorefd = sys->create(sfile, Sys->ORDWR, 8r666)) == nil) {
+		unlock();
+		return (-1, sys->sprint("cannot open %s: %r", sfile));
+	}
+	unlock();
+	scorefile = sfile;
+	return (0, nil);
+}
+
+lock()
+{
+}
+
+unlock()
+{
+}
+
+scores(): list of Score
+{
+	lock();
+	sl := readscores();
+	unlock();
+	return sl;
+}
+	
+readscores(): list of Score
+{
+	sl: list of Score;
+	iob := bufio->open(scorefile, Sys->OREAD);
+	if (iob == nil)
+		return nil;
+	iob.seek(big 0, Bufio->SEEKSTART);
+	while ((s := iob.gets('\n')) != nil) {
+		(n, toks) := sys->tokenize(s, " \t\n");
+		if (toks == nil)
+			continue;
+		if (n < 2) {
+			sys->fprint(stderr, "bad line in score table: %s", s);
+			continue;
+		}
+		score: Score;
+		(score.user, toks) = (hd toks, tl toks);
+		(score.score, toks) = (int hd toks, tl toks);
+		score.other = nil;
+		while (toks != nil) {
+			score.other += hd toks;
+			if (tl toks != nil)
+				score.other += " ";
+			toks = tl toks;
+		}
+		sl = score :: sl;
+	}
+	iob.close();
+	nl: list of Score;
+	while (sl != nil) {
+		nl = hd sl :: nl;
+		sl = tl sl;
+	}
+	return nl;
+}
+
+writescores(sl: list of Score)
+{
+	scoreiob := bufio->open(scorefile, Sys->OWRITE|Sys->OTRUNC);
+	if (scoreiob == nil) {
+		sys->fprint(stderr, "scoretable: cannot write score file '%s': %r\n", scorefile);
+		return;
+	}
+	scoreiob.seek(big 0, Bufio->SEEKSTART);
+	n := 0;
+	while (sl != nil && n < MAXSCORES) {
+		s := hd sl;
+		scoreiob.puts(sys->sprint("%s %d %s\n", s.user, s.score, s.other));
+		n++;
+		sl = tl sl;
+	}
+	scoreiob.close();
+}
+
+setscore(score: int, other: string): int
+{
+	lock();
+	sl := readscores();
+	nl: list of Score;
+	done := 0;
+	n := rank := 0;
+	while (sl != nil) {
+		s := hd sl;
+		if (score > s.score && !done) {
+			nl = Score(username, score, other) :: nl;
+			rank = n;
+			done = 1;
+		}
+		nl = s :: nl;
+		sl = tl sl;
+		n++;
+	}
+	if (!done) {
+		nl = Score(username, score, other) :: nl;
+		rank = n;
+	}
+	sl = nil;
+	while (nl != nil) {
+		sl = hd nl :: sl;
+		nl = tl nl;
+	}
+	writescores(sl);
+	unlock();
+	# XXX minor race condition in returning the rank, not our idea of the rank.
+	return rank;
+}
--- /dev/null
+++ b/appl/lib/scsiio.b
@@ -1,0 +1,303 @@
+implement ScsiIO;
+
+# adapted from /sys/src/libdisk on Plan 9: subject to Lucent Public License 1.02
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "scsiio.m";
+
+scsiverbose := 0;
+
+Codefile: con "/lib/scsicodes";
+
+Code: adt {
+	v:	int;	# (asc<<8) | ascq
+	s:	string;
+};
+codes: array of Code;
+
+init(verbose: int)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	scsiverbose = verbose;
+	getcodes();
+}
+
+getcodes()
+{
+	fd := bufio->open(Codefile, Sys->OREAD);
+	if(fd == nil)
+		return;
+
+	codes = array[256] of Code;
+	nc := 0;
+	while((s := fd.gets('\n')) != nil){
+		if(s[0] == '#' || s[0] == '\n')
+			continue;
+		s = s[0: len s-1];	# trim '\n'
+		m: string;
+		for(i := 0; i < len s; i++)
+			if(s[i] == ' '){
+				m = s[i+1:];
+				break;
+			}
+		c := Code(tohex(s), m);
+		if(nc >= len codes){
+			ct := array[nc + 20] of Code;
+			ct[0:] = codes;
+			codes = ct;
+		}
+		codes[nc++] = c;
+	}
+	codes = codes[0:nc];
+}
+
+tohex(s: string): int
+{
+	n := 0;
+	j := 0;
+	for(i := 0; i < len s && j < 4; i++){
+		if(s[i] == '/')
+			continue;
+		d := hex(s[i]);
+		if(d < 0)
+			return -1;
+		n = (n<<4) | d;
+		j++;
+	}
+	return n;
+}
+
+hex(c: int): int
+{
+	if(c >= '0' && c <= '9')
+		return c-'0';
+	if(c >= 'A' && c <= 'F')
+		return c-'A' + 10;
+	if(c >= 'a' && c <= 'f')
+		return c-'a' + 10;
+	return -1;
+}
+
+scsierror(asc: int, ascq: int): string
+{
+	t := -1;
+	for(i := 0; i < len codes; i++){
+		if(codes[i].v == ((asc<<8) | ascq))
+			return codes[i].s;
+		if(codes[i].v == (asc<<8))
+			t = i;
+	}
+	if(t >= 0)
+		return sys->sprint("(ascq #%.2ux) %s", ascq, codes[t].s);
+	return sys->sprint("scsi #%.2ux %.2ux", asc, ascq);
+}
+
+_scsicmd(s: ref Scsi, cmd: array of byte, data: array of byte, io: int, dolock: int): int
+{
+	if(dolock)
+		qlock(s);
+	dcount := len data;
+	if(sys->write(s.rawfd, cmd, len cmd) != len cmd) {
+		sys->werrstr("cmd write: %r");
+		if(dolock)
+			qunlock(s);
+		return -1;
+	}
+
+	n: int;
+	resp := array[16] of byte;
+	case io {
+	Sread =>
+		n = sys->read(s.rawfd, data, dcount);
+		if(n < 0 && scsiverbose)
+			sys->fprint(sys->fildes(2), "dat read: %r: cmd %#2.2uX\n", int cmd[0]);
+	Swrite =>
+		n = sys->write(s.rawfd, data, dcount);
+		if(n != dcount && scsiverbose)
+			sys->fprint(sys->fildes(2), "dat write: %r: cmd %#2.2uX\n", int cmd[0]);
+	Snone or * =>
+		n = sys->write(s.rawfd, resp, 0);
+		if(n != 0 && scsiverbose)
+			sys->fprint(sys->fildes(2), "none write: %r: cmd %#2.2uX\n", int cmd[0]);
+	}
+
+	m := sys->read(s.rawfd, resp, len resp);
+	if(dolock)
+		qunlock(s);
+	if(m < 0){
+		sys->werrstr("resp read: %r\n");
+		return -1;
+	}
+	status := int string resp[0:m];
+	if(status == 0)
+		return n;
+
+	sys->werrstr(sys->sprint("cmd %2.2uX: status %uX dcount %d n %d", int cmd[0], status, dcount, n));
+	return -1;
+}
+
+Scsi.rawcmd(s: self ref Scsi, cmd: array of byte, data: array of byte, io: int): int
+{
+	return _scsicmd(s, cmd, data, io, 1);
+}
+
+_scsiready(s: ref Scsi, dolock: int): int
+{
+	if(dolock)
+		qlock(s);
+	for(i:=0; i<3; i++) {
+		cmd := array[6] of {0 => byte 16r00, * => byte 0};	# test unit ready
+		if(sys->write(s.rawfd, cmd, len cmd) != len cmd) {
+			if(scsiverbose)
+				sys->fprint(sys->fildes(2), "ur cmd write: %r\n");
+			continue;
+		}
+		resp := array[16] of byte;
+		sys->write(s.rawfd, resp, 0);
+		m := sys->read(s.rawfd, resp, len resp);
+		if(m < 0){
+			if(scsiverbose)
+				sys->fprint(sys->fildes(2), "ur resp read: %r\n");
+			continue;	# retry
+		}
+		status := int string resp[0:m];
+		if(status == 0 || status == 16r02) {
+			if(dolock)
+				qunlock(s);
+			return 0;
+		}
+		if(scsiverbose)
+			sys->fprint(sys->fildes(2), "target: bad status: %x\n", status);
+	}
+	if(dolock)
+		qunlock(s);
+	return -1;
+}
+
+Scsi.ready(s: self ref Scsi): int
+{
+	return _scsiready(s, 1);
+}
+
+Scsi.cmd(s: self ref Scsi, cmd: array of byte, data: array of byte, io: int): int
+{
+	dcount := len data;
+	code := 0;
+	key := 0;
+	qlock(s);
+	sense: array of byte;
+	for(tries:=0; tries<2; tries++) {
+		n := _scsicmd(s, cmd, data, io, 0);
+		if(n >= 0) {
+			qunlock(s);
+			return n;
+		}
+
+		#
+		# request sense
+		#
+		sense = array[255] of {* => byte 16rFF};	# TO DO: usb mass storage devices might inist on less
+		req := array[6] of {0 => byte 16r03, 4 => byte len sense, * => byte 0};
+		if((n=_scsicmd(s, req, sense, Sread, 0)) < 14)
+			if(scsiverbose)
+				sys->fprint(sys->fildes(2), "reqsense scsicmd %d: %r\n", n);
+	
+		if(_scsiready(s, 0) < 0)
+			if(scsiverbose)
+				sys->fprint(sys->fildes(2), "unit not ready\n");
+	
+		key = int sense[2];
+		code = int sense[12];
+		if(code == 16r17 || code == 16r18) {	# recovered errors
+			qunlock(s);
+			return dcount;
+		}
+		if(code == 16r28 && int cmd[0] == 16r43) {	# get info and media changed
+			s.nchange++;
+			s.changetime = daytime->now();
+			continue;
+		}
+	}
+
+	# drive not ready, or medium not present
+	if(cmd[0] == byte 16r43 && key == 2 && (code == 16r3a || code == 16r04)) {
+		s.changetime = 0;
+		qunlock(s);
+		return -1;
+	}
+	qunlock(s);
+
+	if(cmd[0] == byte 16r43 && key == 5 && code == 16r24)	# blank media
+		return -1;
+
+	p := scsierror(code, int sense[13]);
+
+	sys->werrstr(sys->sprint("cmd #%.2ux: %s", int cmd[0], p));
+
+	if(scsiverbose)
+		sys->fprint(sys->fildes(2), "scsi cmd #%.2ux: %.2ux %.2ux %.2ux: %s\n", int cmd[0], key, code, int sense[13], p);
+
+#	if(key == 0)
+#		return dcount;
+	return -1;
+}
+
+Scsi.open(dev: string): ref Scsi
+{
+	rawfd := sys->open(dev+"/raw", Sys->ORDWR);
+	if(rawfd == nil)
+		return nil;
+	ctlfd := sys->open(dev+"/ctl", Sys->ORDWR);
+	if(ctlfd == nil)
+		return nil;
+
+	buf := array[512] of byte;
+	n := sys->readn(ctlfd, buf, len buf);
+	if(n < 8){
+		if(n >= 0)
+			sys->werrstr("error reading ctl file");
+		return nil;
+	}
+	ctlfd = nil;
+
+	for(i := 0; i < n; i++)
+		if(buf[i] == byte '\n')
+			break;
+	inq := string buf[0:i];
+	if(i >= n || inq[0:8] != "inquiry "){
+		sys->werrstr("invalid inquiry string");
+		return nil;
+	}
+	s := ref Scsi;
+	s.lock = chan[1] of int;
+	s.rawfd = rawfd;
+	s.inquire = inq[8:];
+	s.changetime = daytime->now();
+
+	if(s.ready() < 0)
+		return nil;
+
+	return s;
+}
+
+qlock(s: ref Scsi)
+{
+	s.lock <-= 1;
+}
+
+qunlock(s: ref Scsi)
+{
+	<-s.lock;
+}
--- /dev/null
+++ b/appl/lib/secstore.b
@@ -1,0 +1,520 @@
+implement Secstore;
+
+#
+# interact with the Plan 9 secstore
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "dial.m";
+	dialler: Dial;
+
+include "keyring.m";
+	kr: Keyring;
+	DigestState, IPint: import kr;
+	AESbsize, AESstate: import kr;
+
+include "security.m";
+	ssl: SSL;
+	random: Random;
+
+include "encoding.m";
+	base64: Encoding;
+
+include "secstore.m";
+
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	ssl = load SSL SSL->PATH;
+	random = load Random Random->PATH;
+	base64 = load Encoding Encoding->BASE64PATH;
+	dialler = load Dial Dial->PATH;
+	initPAKparams();
+}
+
+privacy(): int
+{
+	fd := sys->open("#p/"+string sys->pctl(0, nil)+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "private") < 0)
+		return 0;
+	return 1;
+}
+
+connect(addr: string, user: string, pwhash: array of byte): (ref Dial->Connection, string, string)
+{
+	conn := dial(addr);
+	if(conn == nil){
+		sys->werrstr(sys->sprint("can't dial %s: %r", addr));
+		return (nil, nil, sys->sprint("%r"));
+	}
+	(sname, diag) := auth(conn, user, pwhash);
+	if(sname == nil){
+		sys->werrstr(sys->sprint("can't authenticate: %s", diag));
+		return (nil, nil, sys->sprint("%r"));
+	}
+	return (conn, sname, diag);
+}
+
+dial(netaddr: string): ref Dial->Connection
+{
+	if(netaddr == nil)
+		netaddr = "net!$auth!secstore";
+	conn := dialler->dial(netaddr, nil);
+	if(conn == nil)
+		return nil;
+	(err, sslconn) := ssl->connect(conn.dfd);
+	if(err != nil)
+		sys->werrstr(err);
+	return sslconn;
+}
+
+auth(conn: ref Dial->Connection, user: string, pwhash: array of byte): (string, string)
+{
+	sname := PAKclient(conn, user, pwhash);
+	if(sname == nil)
+		return (nil, sys->sprint("%r"));
+	s := readstr(conn.dfd);
+	if(s == "STA")
+		return (sname, "need pin");
+	if(s != "OK"){
+		if(s != nil)
+			sys->werrstr(s);
+		return (nil, sys->sprint("%r"));
+	}
+	return (sname, nil);
+}
+
+cansecstore(netaddr: string, user: string): int
+{
+	conn := dial(netaddr);
+	if(conn == nil)
+		return 0;
+	if(sys->fprint(conn.dfd, "secstore\tPAK\nC=%s\nm=0\n", user) < 0)
+		return 0;
+	buf := array[128] of byte;
+	n := sys->read(conn.dfd, buf, len buf);
+	if(n <= 0)
+		return 0;
+	return string buf[0:n] == "!account exists";
+}
+
+sendpin(conn: ref Dial->Connection, pin: string): int
+{
+	if(sys->fprint(conn.dfd, "STA%s", pin) < 0)
+		return -1;
+	s := readstr(conn.dfd);
+	if(s != "OK"){
+		if(s != nil)
+			sys->werrstr(s);
+		return -1;
+	}
+	return 0;
+}
+
+files(conn: ref Dial->Connection): list of (string, int, string, string, array of byte)
+{
+	file := getfile(conn, ".", 0);
+	if(file == nil)
+		return nil;
+	rl: list of (string, int, string, string, array of byte);
+	for(linelist := lines(file); linelist != nil; linelist = tl linelist){
+		s := string hd linelist;
+		# factotum\t2552 Dec  9 13:04:49 GMT 2005 n9wSk45SPDxgljOIflGQoXjOkjs=
+		for(i := 0; i < len s && s[i] != '\t' && s[i] != ' '; i++){}	# can be trailing spaces
+		name := s[0:i];
+		for(; i < len s && (s[i] == ' ' || s[i] == '\t'); i++){}
+		for(j := i; j  < len s && s[j] != ' '; j++){}
+		size := int s[i+1:j];
+		for(i = j; i < len s && s[i] == ' '; i++){}
+		date := s[i:i+24];
+		i += 24+1;
+		for(j = i; j < len s && s[j] != '\n'; j++){}
+		sha1 := s[i:j];
+		rl = (name, int size, date, sha1, base64->dec(sha1)) :: rl;
+	}
+	l: list of (string, int, string, string, array of byte);
+	for(; rl != nil; rl = tl rl)
+		l = hd rl :: l;
+	return l;
+}
+
+getfile(conn: ref Dial->Connection, name: string, maxsize: int): array of byte
+{
+	fd := conn.dfd;
+	if(maxsize <= 0)
+		maxsize = Maxfilesize;
+	if(sys->fprint(fd, "GET %s\n", name) < 0 ||
+	   (s := readstr(fd)) == nil){
+		sys->werrstr(sys->sprint("can't get %q: %r", name));
+		return nil;
+	}
+	nb := int s;
+	if(nb == -1){
+		sys->werrstr(sys->sprint("remote file %q does not exist", name));
+		return nil;
+	}
+	if(nb < 0 || nb > maxsize){
+		sys->werrstr(sys->sprint("implausible file size %d for %q", nb, name));
+		return nil;
+	}
+	file := array[nb] of byte;
+	for(nr := 0; nr < nb;){
+		n :=  sys->read(fd, file[nr:], nb-nr);
+		if(n < 0){
+			sys->werrstr(sys->sprint("error reading %q: %r", name));
+			return nil;
+		}
+		if(n == 0){
+			sys->werrstr(sys->sprint("empty file chunk reading %q at offset %d", name, nr));
+			return nil;
+		}
+		nr += n;
+	}
+	return file;
+}
+
+remove(conn: ref Dial->Connection, name: string): int
+{
+	if(sys->fprint(conn.dfd, "RM %s\n", name) < 0)
+		return -1;
+
+	return 0;
+}
+
+putfile(conn: ref Dial->Connection, name: string, data: array of byte): int
+{
+	if(len data > Maxfilesize){
+		sys->werrstr("file too long");
+		return -1;
+	}
+	fd := conn.dfd;
+	if(sys->fprint(fd, "PUT %s\n", name) < 0)
+		return -1;
+	if(sys->fprint(fd, "%d", len data) < 0)
+		return -1;
+	for(o := 0; o < len data;){
+		n := len data-o;
+		if(n > Maxmsg)
+			n = Maxmsg;
+		if(sys->write(fd, data[o:o+n], n) != n)
+			return -1;
+		o += n;
+	}
+	return 0;
+}
+
+bye(conn: ref Dial->Connection)
+{
+	if(conn != nil){
+		if(conn.dfd != nil)
+			sys->fprint(conn.dfd, "BYE");
+		conn.dfd = nil;
+		conn.cfd = nil;
+	}
+}
+
+mkseckey(s: string): array of byte
+{
+	key := array of byte s;
+	skey := array[Keyring->SHA1dlen] of byte;
+	kr->sha1(key, len key, skey, nil);
+	erasekey(key);
+	return skey;
+}
+
+Checkpat: con "XXXXXXXXXXXXXXXX";	# it's what Plan 9's aescbc uses
+Checklen: con len Checkpat;
+
+mkfilekey(s: string): array of byte
+{
+	key := array of byte s;
+	skey := array[Keyring->SHA1dlen] of byte;
+	sha := kr->sha1(array of byte "aescbc file", 11, nil, nil);
+	kr->sha1(key, len key, skey, sha);
+	erasekey(key);
+	erasekey(skey[AESbsize:]);
+	return skey[0:AESbsize];
+}
+
+decrypt(file: array of byte, key: array of byte): array of byte
+{
+	length := len file;
+	if(length == 0)
+		return file;
+	if(length < AESbsize+Checklen)
+		return nil;
+	state := kr->aessetup(key, file[0:AESbsize]);
+	if(state == nil){
+		sys->werrstr("can't set AES state");
+		return nil;
+	}
+	kr->aescbc(state, file[AESbsize:], length-AESbsize, Keyring->Decrypt);
+	if(string file[length-Checklen:] != Checkpat){
+		sys->werrstr("file did not decrypt correctly");
+		return nil;
+	}
+	return file[AESbsize: length-Checklen];
+}
+
+encrypt(file: array of byte, key: array of byte): array of byte
+{
+	dat := array[AESbsize+len file+Checklen] of byte;
+	iv := random->randombuf(random->NotQuiteRandom, AESbsize);
+	if(len iv != AESbsize)
+		return nil;
+	dat[:] = iv;
+	dat[len iv:] = file;
+	dat[len iv+len file:] = array of byte Checkpat;
+	state := kr->aessetup(key, iv);
+	if(state == nil){
+		sys->werrstr("can't set AES state");
+		return nil;
+	}
+	kr->aescbc(state, dat[AESbsize:], len dat-AESbsize, Keyring->Encrypt);
+	return dat;
+}
+
+lines(file: array of byte): list of array of byte
+{
+	rl: list of array of byte;
+	for(i := 0; i < len file;){
+		for(j := i; j < len file; j++)
+			if(file[j] == byte '\n'){
+				j++;
+				break;
+			}
+		rl = file[i:j] :: rl;
+		i = j;
+	}
+	l: list of array of byte;
+	for(; rl != nil; rl = tl rl)
+		l = (hd rl) :: l;
+	return l;
+}
+
+readstr(fd: ref Sys->FD): string
+{
+	buf := array[500] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+	s := string buf[0:n];
+	if(s[0] == '!'){
+		sys->werrstr(s[1:]);
+		return nil;
+	}
+	return s;
+}
+
+writerr(fd: ref Sys->FD, s: string)
+{
+	sys->fprint(fd, "!%s", s);
+	sys->werrstr(s);
+}
+
+setsecret(conn: ref Dial->Connection, sigma: array of byte, direction: int): string
+{
+	secretin := array[Keyring->SHA1dlen] of byte;
+	secretout := array[Keyring->SHA1dlen] of byte;
+	if(direction != 0){
+		kr->hmac_sha1(sigma, len sigma, array of byte "one", secretout, nil);
+		kr->hmac_sha1(sigma, len sigma, array of byte "two", secretin, nil);
+	}else{
+		kr->hmac_sha1(sigma, len sigma, array of byte "two", secretout, nil);
+		kr->hmac_sha1(sigma, len sigma, array of byte "one", secretin, nil);
+	}
+	return ssl->secret(conn, secretin, secretout);
+}
+
+erasekey(a: array of byte)
+{
+	for(i := 0; i < len a; i++)
+		a[i] = byte 0;
+}
+
+#
+# the following must only be used to talk to a Plan 9 secstore
+#
+
+VERSION: con "secstore";
+
+PAKparams: adt {
+	q:	ref IPint;
+	p:	ref IPint;
+	r:	ref IPint;
+	g:	ref IPint;
+};
+
+pak: ref PAKparams;
+
+# from seed EB7B6E35F7CD37B511D96C67D6688CC4DD440E1E
+
+initPAKparams()
+{
+	if(pak != nil)
+		return;
+	lpak := ref PAKparams;
+	lpak.q = IPint.strtoip("E0F0EF284E10796C5A2A511E94748BA03C795C13", 16);
+	lpak.p = IPint.strtoip("C41CFBE4D4846F67A3DF7DE9921A49D3B42DC33728427AB159CEC8CBB"+
+		"DB12B5F0C244F1A734AEB9840804EA3C25036AD1B61AFF3ABBC247CD4B384224567A86"+
+		"3A6F020E7EE9795554BCD08ABAD7321AF27E1E92E3DB1C6E7E94FAAE590AE9C48F96D9"+
+		"3D178E809401ABE8A534A1EC44359733475A36A70C7B425125062B1142D", 16);
+	lpak.r = IPint.strtoip("DF310F4E54A5FEC5D86D3E14863921E834113E060F90052AD332B3241"+
+		"CEF2497EFA0303D6344F7C819691A0F9C4A773815AF8EAECFB7EC1D98F039F17A32A7E"+
+		"887D97251A927D093F44A55577F4D70444AEBD06B9B45695EC23962B175F266895C67D"+
+		"21C4656848614D888A4", 16);
+	lpak.g = IPint.strtoip("2F1C308DC46B9A44B52DF7DACCE1208CCEF72F69C743ADD4D23271734"+
+		"44ED6E65E074694246E07F9FD4AE26E0FDDD9F54F813C40CB9BCD4338EA6F242AB94CD"+
+		"410E676C290368A16B1A3594877437E516C53A6EEE5493A038A017E955E218E7819734"+
+		"E3E2A6E0BAE08B14258F8C03CC1B30E0DDADFCF7CEDF0727684D3D255F1", 16);
+	pak = lpak;	# atomic store
+}
+
+# H = (sha(ver,C,sha(passphrase)))^r mod p,
+# a hash function expensive to attack by brute force.
+
+longhash(ver: string, C: string, passwd: array of byte): ref IPint
+{
+	aver := array of byte ver;
+	aC := array of byte C;
+	Cp := array[len aver + len aC + len passwd] of byte;
+	Cp[0:] = aver;
+	Cp[len aver:] = aC;
+	Cp[len aver+len aC:] = passwd;
+	buf := array[7*Keyring->SHA1dlen] of byte;
+	for(i := 0; i < 7; i++){
+		key := array[] of { byte('A'+i) };
+		kr->hmac_sha1(Cp, len Cp, key, buf[i*Keyring->SHA1dlen:], nil);
+	}
+	erasekey(Cp);
+	return mod(IPint.bebytestoip(buf), pak.p).expmod(pak.r, pak.p);	# H
+}
+
+mod(a, b: ref IPint): ref IPint
+{
+	return a.div(b).t1;
+}
+
+shaz(s: string, digest: array of byte, state: ref DigestState): ref DigestState
+{
+	a := array of byte s;
+	state = kr->sha1(a, len a, digest, state);
+	erasekey(a);
+	return state;
+}
+
+# Hi = H^-1 mod p
+PAK_Hi(C: string, passhash: array of byte): (string, ref IPint, ref IPint)
+{
+	H := longhash(VERSION, C, passhash);
+	Hi := H.invert(pak.p);
+	return (Hi.iptostr(64), H, Hi);
+}
+
+# another, faster, hash function for each party to
+# confirm that the other has the right secrets.
+
+shorthash(mess: string, C: string, S: string, m: string, mu: string, sigma: string, Hi: string): array of byte
+{
+	state := shaz(mess, nil, nil);
+	state = shaz(C, nil, state);
+	state = shaz(S, nil, state);
+	state = shaz(m, nil, state);
+	state = shaz(mu, nil, state);
+	state = shaz(sigma, nil, state);
+	state = shaz(Hi, nil, state);
+	state = shaz(mess, nil, state);
+	state = shaz(C, nil, state);
+	state = shaz(S, nil, state);
+	state = shaz(m, nil, state);
+	state = shaz(mu, nil, state);
+	state = shaz(sigma, nil, state);
+	digest := array[Keyring->SHA1dlen] of byte;
+	shaz(Hi, digest, state);
+	return digest;
+}
+
+#
+# On input, conn provides an open channel to the server;
+#	C is the name this client calls itself;
+#	pass is the user's passphrase
+# On output, session secret has been set in conn
+#	(unless return code is negative, which means failure).
+#
+PAKclient(conn: ref Dial->Connection, C: string, pwhash: array of byte): string
+{
+	dfd := conn.dfd;
+
+	(hexHi, H, nil) := PAK_Hi(C, pwhash);
+
+	# random 1<=x<=q-1; send C, m=g**x H
+	x := mod(IPint.random(240, 240), pak.q);
+	if(x.eq(IPint.inttoip(0)))
+		x = IPint.inttoip(1);
+	m := mod(pak.g.expmod(x, pak.p).mul(H), pak.p);
+	hexm := m.iptostr(64);
+
+	if(sys->fprint(dfd, "%s\tPAK\nC=%s\nm=%s\n", VERSION, C, hexm) < 0)
+		return nil;
+
+	# recv g**y, S, check hash1(g**xy)
+	s := readstr(dfd);
+	if(s == nil){
+		e := sys->sprint("%r");
+		writerr(dfd, "couldn't read g**y");
+		sys->werrstr(e);
+		return nil;
+	}
+	# should be: "mu=%s\nk=%s\nS=%s\n"
+	(nf, flds) := sys->tokenize(s, "\n");
+	if(nf != 3){
+		writerr(dfd, "verifier syntax  error");
+		return nil;
+	}
+	hexmu := ex("mu=", hd flds); flds = tl flds;
+	ks := ex("k=", hd flds); flds = tl flds;
+	S := ex("S=", hd flds);
+	if(hexmu == nil || ks == nil || S == nil){
+		writerr(dfd, "verifier syntax error");
+		return nil;
+	}
+	mu := IPint.strtoip(hexmu, 64);
+	sigma := mu.expmod(x, pak.p);
+	hexsigma := sigma.iptostr(64);
+	digest := shorthash("server", C, S, hexm, hexmu, hexsigma, hexHi);
+	kc := base64->enc(digest);
+	if(ks != kc){
+		writerr(dfd, "verifier didn't match");
+		return nil;
+	}
+
+	# send hash2(g**xy)
+	digest = shorthash("client", C, S, hexm, hexmu, hexsigma, hexHi);
+	kc = base64->enc(digest);
+	if(sys->fprint(dfd, "k'=%s\n", kc) < 0)
+		return nil;
+
+	# set session key
+	digest = shorthash("session", C, S, hexm, hexmu, hexsigma, hexHi);
+	for(i := 0; i < len hexsigma; i++)
+		hexsigma[i] = 0;
+
+	err := setsecret(conn, digest, 0);
+	if(err != nil)
+		return nil;
+	erasekey(digest);
+	if(sys->fprint(conn.cfd, "alg sha1 rc4_128") < 0)
+		return nil;
+	return S;
+}
+
+ex(tag: string, s: string): string
+{
+	if(len s < len tag || s[0:len tag] != tag)
+		return nil;
+	return s[len tag:];
+}
--- /dev/null
+++ b/appl/lib/selectfile.b
@@ -1,0 +1,624 @@
+implement Selectfile;
+
+include "sys.m";
+	sys: Sys;
+	Dir: import sys;
+
+include "draw.m";
+	draw: Draw;
+	Screen, Rect, Point: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "string.m";
+	str: String;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "workdir.m";
+
+include "readdir.m";
+	readdir: Readdir;
+
+include "filepat.m";
+	filepat: Filepat;
+
+include "selectfile.m";
+
+Browser: adt {
+	top:		ref Tk->Toplevel;
+	ncols:	int;
+	colwidth:	int;
+	w:		string;
+	init:		fn(top: ref Tk->Toplevel, w: string, colwidth: string): (ref Browser, chan of string);
+
+	addcol:	fn(c: self ref Browser, t: string, d: array of string);
+	delete:	fn(c: self ref Browser, colno: int);
+	selection:	fn(c: self ref Browser, cno: int): string;
+	select:	fn(b: self ref Browser, cno: int, e: string);
+	entries:	fn(b: self ref Browser, cno: int): array of string;
+	resize:	fn(c: self ref Browser);
+};
+
+BState: adt {
+	b:			ref Browser;
+	bpath:		string;		# path currently displayed in browser
+	epath:		string;		# path entered by user
+	dirfetchpid:	int;
+	dirfetchpath:	string;
+};
+
+filename_config := array[] of {
+	"entry .e -bg white",
+	"frame .pf",
+	"entry .pf.e",
+	"label .pf.t -text {Filter:}",
+	"entry .pats",
+	"bind .e <Key> +{send ech key}",
+	"bind .e <Key-\n> {send ech enter}",
+	"bind .e {<Key-\t>} {send ech expand}",
+	"bind .pf.e <Key-\n> {send ech setpat}",
+	"bind . <Configure> {send ech config}",
+	"pack .b -side top -fill both -expand 1",
+	"pack .pf.t -side left",
+	"pack .pf.e -side top -fill x",
+	"pack .pf -side top -fill x",
+	"pack .e -side top -fill x",
+	"pack propagate . 0",
+};
+
+debugging := 0;
+STEP: con 20;
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	tkclient->init();
+	str = load String String->PATH;
+	readdir = load Readdir Readdir->PATH;
+	filepat = load Filepat Filepat->PATH;
+	return nil;
+}
+
+filename(ctxt: ref Draw->Context, parent: ref Draw->Image,
+		title: string,
+		pats: list of string,
+		dir: string): string
+{
+	patstr: string;
+
+	if (dir == nil || dir == ".") {
+		wd := load Workdir Workdir->PATH;
+		if ((dir = wd->init()) != nil) {
+			(ok, nil) := sys->stat(dir);
+			if (ok == -1)
+				dir = nil;
+		}
+		wd = nil;
+	}
+	if (dir == nil)
+		dir = "/";
+	(pats, patstr) = makepats(pats);
+	where := localgeom(parent);
+	if (title == nil)
+		title = "Open";
+	(top, wch) := tkclient->toplevel(ctxt, where+" -bd 1", # -font /fonts/misc/latin1.6x13.font", 
+			title, Tkclient->Popup|Tkclient->Resize|Tkclient->OK);
+	(b, colch) := Browser.init(top, ".b", "16w");
+	entrych := chan of string;
+	tk->namechan(top, entrych, "ech");
+	tkcmds(top, filename_config);
+	cmd(top, ". configure -width " + string (b.colwidth * 3) + " -height 20h");
+	cmd(top, ".e insert 0 '" + dir);
+	cmd(top, ".pf.e insert 0 '" + patstr);
+	s := ref BState(b, nil, dir, -1, nil);
+	s.b.resize();
+	dfch := chan of (string, array of ref Sys->Dir);
+	if (parent == nil)
+		centre(top);
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd" :: "ptr" :: nil);
+loop: for (;;) {
+		if (debugging) {
+			sys->print("filename: before sync, bpath: '%s'; epath: '%s'\n",
+				s.bpath, s.epath);
+		}
+		bsync(s, dfch, pats);
+		if (debugging) {
+			sys->print("filename: after sync, bpath: '%s'; epath: '%s'", s.bpath, s.epath);
+			if (s.dirfetchpid == -1)
+				sys->print("\n");
+			else
+				sys->print("; fetching '%s' (pid %d)\n", s.dirfetchpath, s.dirfetchpid);
+		}
+		cmd(top, "focus .e");
+		cmd(top, "update");
+		alt {
+		c := <-top.ctxt.kbd =>
+			tk->keyboard(top, c);
+		p := <-top.ctxt.ptr =>
+			tk->pointer(top, *p);
+		c := <-top.ctxt.ctl or
+		c = <-top.wreq =>
+			tkclient->wmctl(top, c);
+		c := <-colch =>
+			double := c[0] == 'd';
+			c = c[1:];
+			(bpath, nbpath, elem) := (s.bpath, "", "");
+			for (cno := 0; cno <= int c; cno++) {
+				(elem, bpath) = nextelem(bpath);
+				nbpath = pathcat(nbpath, elem);
+			}
+			nsel := s.b.selection(int c);
+			if (nsel != nil)
+				nbpath = pathcat(nbpath, nsel);
+			s.epath = nbpath;
+			cmd(top, ".e delete 0 end");
+			cmd(top, ".e insert 0 '" + s.epath);
+			if (double)
+				break loop;
+		c := <-entrych =>
+			case c {
+			"enter" =>
+				break loop;
+			"config" =>
+				s.b.resize();
+			"key" =>
+				s.epath = cmdget(top, ".e get");
+			"expand" =>
+				cmd(top, ".e delete 0 end");
+				cmd(top, ".e insert 0 '" + s.bpath);
+				s.epath = s.bpath;
+			"setpat" =>
+				patstr = cmdget(top, ".pf.e get");
+				if (patstr == "  debug  ")
+					debugging = !debugging;
+				else {
+					(nil, pats) = sys->tokenize(patstr, " ");
+					s.b.delete(0);
+					s.bpath = nil;
+				}
+			}
+		c := <-wch =>
+			if (c == "ok")
+				break loop;
+			if (c == "exit") {
+				s.epath = nil;
+				break loop;
+			}
+			tkclient->wmctl(top, c);
+		(t, d) := <-dfch =>
+			ds := array[len d] of string;
+			for (i := 0; i < len d; i++) {
+				n := d[i].name;
+				if ((d[i].mode & Sys->DMDIR) != 0)
+					n[len n] = '/';
+				ds[i] = n;
+			}
+			s.b.addcol(t, ds);
+			ds = nil;
+			d = nil;
+			s.bpath = s.dirfetchpath;
+			s.dirfetchpid = -1;
+		}
+	}
+	if (s.dirfetchpid != -1)
+		kill(s.dirfetchpid);
+	return s.epath;
+}
+
+bsync(s: ref BState, dfch: chan of (string, array of ref Sys->Dir), pats: list of string)
+{
+	(epath, bpath) := (s.epath, s.bpath);
+	cno := 0;
+	prefix, e1, e2: string = "";
+
+	# find maximal prefix of epath and bpath.
+	for (;;) {
+		p1, p2: string;
+		(e1, p1) = nextelem(epath);
+		(e2, p2) = nextelem(bpath);
+		if (e1 == nil || e1 != e2)
+			break;
+		prefix = pathcat(prefix, e1);
+		(epath, bpath) = (p1, p2);
+		cno++;
+	}
+
+	if (epath == nil) {
+		if (bpath != nil) {
+			s.b.delete(cno);
+			s.b.select(cno - 1, nil);
+			s.bpath = prefix;
+		}
+		return;
+	}
+
+	# if the paths have no prefix in common then we're starting
+	# at a different root - don't do anything until
+	# we know we have at least one full element.
+	# even then, if it's not a directory, we have to ignore it.
+	if (cno == 0 && islastelem(epath))
+		return;
+
+	if (e1 != nil && islastelem(epath)) {
+		# find first prefix-matching entry.
+		match := "";
+		for ((i, ents) := (0, s.b.entries(cno - 1)); i < len ents; i++) {
+			m := ents[i];
+			if (len m >= len e1 && m[0:len e1] == e1) {
+				match = deslash(m);
+				break;
+			}
+		}
+		if (match != nil) {
+			if (match == e2 && islastelem(bpath))
+				return;
+
+			epath = pathcat(match,  epath[len e1:]);
+			e1 = match;
+			if (e1 == e2)
+				cno++;
+		} else {
+			s.b.delete(cno);
+			s.bpath = prefix;
+			return;
+		}
+	}
+
+	s.b.delete(cno);
+	s.b.select(cno - 1, e1);
+	np := pathcat(prefix, e1);
+	if (s.dirfetchpid != -1) {
+		if (np == s.dirfetchpath)
+			return;
+		kill(s.dirfetchpid);
+		s.dirfetchpid = -1;
+	}
+	(ok, dir) := sys->stat(np);
+	if (ok != -1 && (dir.mode & Sys->DMDIR) != 0) {
+		sync := chan of int;
+		spawn dirfetch(np, e1, sync, dfch, pats);
+		s.dirfetchpid = <-sync;
+		s.dirfetchpath = np;
+	} else if (ok != -1)
+		s.bpath = np;
+	else
+		s.bpath = prefix;
+}
+
+dirfetch(p: string, t: string, sync: chan of int,
+		dfch: chan of (string, array of ref Sys->Dir),
+		pats: list of string)
+{
+	sync <-= sys->pctl(0, nil);
+	(a, e) := readdir->init(p, Readdir->NAME|Readdir->COMPACT);
+	if (e != -1) {
+		j := 0;
+		for (i := 0; i < len a; i++) {
+			pl := pats;
+			if ((a[i].mode & Sys->DMDIR) == 0) {
+				for (; pl != nil; pl = tl pl)
+					if (filepat->match(hd pl, a[i].name))
+						break;
+			}
+			if (pl != nil || pats == nil)
+				a[j++] = a[i];
+		}
+		a = a[0:j];
+	}
+	dfch <-= (t, a);
+}
+
+dist(top: ref Tk->Toplevel, s: string): int
+{
+	cmd(top, "frame .xxxx -width " + s);
+	d := int cmd(top, ".xxxx cget -width");
+	cmd(top, "destroy .xxxx");
+	return d;
+}
+	
+Browser.init(top: ref Tk->Toplevel, w: string, colwidth: string): (ref Browser, chan of string)
+{
+	b := ref Browser;
+	b.top = top;
+	b.ncols = 0;
+	b.colwidth = dist(top, colwidth);
+	b.w = w;
+	cmd(b.top, "frame " + b.w);
+	cmd(b.top, "canvas " + b.w + ".c -width 0 -height 0 -xscrollcommand {" + b.w + ".s set}");
+	cmd(b.top, "frame " + b.w + ".c.f -bd 0");
+	cmd(b.top, "pack propagate " + b.w + ".c.f 0");
+	cmd(b.top, b.w + ".c create window 0 0 -tags win -window " + b.w + ".c.f -anchor nw");
+	cmd(b.top, "scrollbar "+b.w+".s -command {"+b.w+".c xview} -orient horizontal");
+	cmd(b.top, "bind "+b.w+".c <Configure> {"+b.w+".c itemconfigure win -height ["+b.w+".c cget -actheight]}");
+	cmd(b.top, "pack "+b.w+".c -side top -fill both -expand 1");
+	cmd(b.top, "pack "+b.w+".s -side top -fill x");
+	ch := chan of string;
+	tk->namechan(b.top, ch, "colch");
+	return (b, ch);
+}
+
+xview(top: ref Tk->Toplevel, w: string): (real, real)
+{
+	s := tk->cmd(top, w + " xview");
+	if (s != nil && s[0] != '!') {
+		(n, v) := sys->tokenize(s, " ");
+		if (n == 2)
+			return (real hd v, real hd tl v);
+	}
+	return (0.0, 0.0);
+}
+
+setscrollregion(b: ref Browser)
+{
+	(w, h) := (b.colwidth * (b.ncols + 1), int cmd(b.top, b.w + ".c cget -actheight"));
+	cmd(b.top, b.w+".c.f configure -width " + string w + " -height " + string h);
+#	w := int cmd(b.top, b.w+".c.f cget -actwidth");
+#	w += int cmd(b.top, b.w+".c cget -actwidth") - b.colwidth;
+#	h := int cmd(b.top, b.w+".c.f cget -actheight");
+	if (w > 0 && h > 0)
+		cmd(b.top, b.w + ".c configure -scrollregion {0 0 " + string w + " " + string h + "}");
+	(start, end) := xview(b.top, b.w+".c");
+	if (end > 1.0)
+		cmd(b.top, b.w+".c xview scroll left 0 units");
+}
+
+Browser.addcol(b: self ref Browser, title: string, d: array of string)
+{
+	ncol := string b.ncols++;
+
+	f := b.w + ".c.f.d" + ncol;
+	cmd(b.top, "frame " + f + " -bg green -width " + string b.colwidth);
+
+	t := f + ".t";
+	cmd(b.top, "label " + t + " -text " + tk->quote(title) + " -bg black -fg white");
+
+	sb := f + ".s";
+	lb := f + ".l";
+	cmd(b.top, "scrollbar " + sb +
+		" -command {" + lb + " yview}");
+
+	cmd(b.top, "listbox " + lb +
+		" -selectmode browse" +
+		" -yscrollcommand {" + sb + " set}" +
+		" -bd 2");
+
+	cmd(b.top, "bind " + lb + " <ButtonRelease-1> +{send colch s " + ncol + "}");
+	cmd(b.top, "bind " + lb + " <Double-Button-1> +{send colch d " + ncol + "}");
+	cmd(b.top, "pack propagate " + f + " 0");
+	cmd(b.top, "pack " + t + " -side top -fill x");
+	cmd(b.top, "pack " + sb + " -side left -fill y");
+	cmd(b.top, "pack " + lb + " -side left -fill both -expand 1");
+	cmd(b.top, "pack " + f + " -side left -fill y");
+	for (i := 0; i < len d; i++)
+		cmd(b.top, lb + " insert end '" + d[i]);
+	setscrollregion(b);
+	seecol(b, b.ncols - 1);
+}
+
+Browser.resize(b: self ref Browser)
+{
+	if (b.ncols == 0)
+		return;
+	setscrollregion(b);
+}
+
+seecol(b: ref Browser, cno: int)
+{
+	w := b.w + ".c.f.d" + string cno;
+	min := int cmd(b.top, w + " cget -actx");
+	max := min + int cmd(b.top, w + " cget -actwidth") +
+			2 * int cmd(b.top, w + " cget -bd");
+	min = int cmd(b.top, b.w+".c canvasx " + string min);
+	max = int cmd(b.top, b.w +".c canvasx " + string max);
+
+	# see first the right edge; then the left edge, to ensure
+	# that the start of a column is visible, even if the window
+	# is narrower than one column.
+	cmd(b.top, b.w + ".c see " + string max + " 0");
+	cmd(b.top, b.w + ".c see " + string min + " 0");
+}
+
+Browser.delete(b: self ref Browser, colno: int)
+{
+	while (b.ncols > colno)
+		cmd(b.top, "destroy " + b.w+".c.f.d" + string --b.ncols);
+	setscrollregion(b);
+}
+
+Browser.selection(b: self ref Browser, cno: int): string
+{
+	if (cno >= b.ncols || cno < 0)
+		return nil;
+	l := b.w+".c.f.d" + string cno + ".l";
+	sel := cmd(b.top, l + " curselection");
+	if (sel == nil)
+		return nil;
+	return cmdget(b.top, l + " get " + sel);
+}
+
+Browser.select(b: self ref Browser, cno: int, e: string)
+{
+	if (cno < 0 || cno >= b.ncols)
+		return;
+	l := b.w+".c.f.d" + string cno + ".l";
+	cmd(b.top, l + " selection clear 0 end");
+	if (e == nil)
+		return;
+	ents := b.entries(cno);
+	for (i := 0; i < len ents; i++) {
+		if (deslash(ents[i]) == e) {
+			cmd(b.top, l + " selection set " + string i);
+			cmd(b.top, l + " see " + string i);
+			return;
+		}
+	}
+}
+
+Browser.entries(b: self ref Browser, cno: int): array of string
+{
+	if (cno < 0 || cno >= b.ncols)
+		return nil;
+	l := b.w+".c.f.d" + string cno + ".l";
+	nent := int cmd(b.top, l + " index end") + 1;
+	ents := array[nent] of string;
+	for (i := 0; i < len ents; i++)
+		ents[i] = cmdget(b.top, l + " get " + string i);
+	return ents;
+}
+
+# turn each pattern of the form "*.b (Limbo files)" into "*.b".
+# ignore '*' as it's a hangover from a past age.
+makepats(pats: list of string): (list of string, string)
+{
+	np: list of string;
+	s := "";
+	for (; pats != nil; pats = tl pats) {
+		p := hd pats;
+		for (i := 0; i < len p; i++)
+			if (p[i] == ' ')
+				break;
+		pat := p[0:i];
+		if (p != "*") {
+			np = p[0:i] :: np;
+			s += hd np;
+			if (tl pats != nil)
+				s[len s] = ' ';
+		}
+	}
+	return (np, s);
+}
+
+widgetwidth(top: ref Tk->Toplevel, w: string): int
+{
+	return int cmd(top, w + " cget -width") + 2 * int cmd(top, w + " cget -bd");
+}
+
+skipslash(path: string): string
+{
+	for (i := 0; i < len path; i++)
+		if (path[i] != '/')
+			return path[i:];
+	return nil;
+}
+
+nextelem(path: string): (string, string)
+{
+	if (path == nil)
+		return (nil, nil);
+	if (path[0] == '/')
+		return ("/", skipslash(path));
+	for (i := 0; i < len path; i++)
+		if (path[i] == '/')
+			break;
+	return (path[0:i], skipslash(path[i:]));
+}
+
+islastelem(path: string): int
+{
+	for (i := 0; i < len path; i++)
+		if (path[i] == '/')
+			return 0;
+	return 1;
+}
+
+pathcat(path, elem: string): string
+{
+	if (path != nil && path[len path - 1] != '/')
+		path[len path] = '/';
+	return path + elem;
+}
+
+# remove a possible trailing slash
+deslash(s: string): string
+{
+	if (len s > 0 && s[len s - 1] == '/')
+		s = s[0:len s - 1];
+	return s;
+}
+
+#
+# find upper left corner for subsidiary child window (always at constant
+# position relative to parent)
+#
+localgeom(im: ref Draw->Image): string
+{
+	if (im == nil)
+		return nil;
+
+	return sys->sprint("-x %d -y %d", im.r.min.x+STEP, im.r.min.y+STEP);
+}
+
+centre(t: ref Tk->Toplevel)
+{
+	org: Point;
+	org.x = t.screenr.dx() / 2 - int cmd(t, ". cget -width") / 2;
+	org.y = t.screenr.dy() / 3 - int cmd(t, ". cget -height") / 2;
+	if (org.y < 0)
+		org.y = 0;
+	cmd(t, ". configure -x " + string org.x + " -y " + string org.y);
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	n := len a;
+	for(i := 0; i < n; i++)
+		tk->cmd(top, a[i]);
+}
+
+topopts := array[] of {
+	"font"
+#	, "bd"			# Wait for someone to ask for these
+#	, "relief"		# Note: colors aren't inherited, it seems
+};
+
+opts(top: ref Tk->Toplevel) : string
+{
+	if (top == nil)
+		return nil;
+	opts := "";
+	for ( i := 0; i < len topopts; i++ ) {
+		cfg := tk->cmd(top, ". cget " + topopts[i]);
+		if ( cfg != "" && cfg[0] != '!' )
+			opts += " -" + topopts[i] + " " + tk->quote(cfg);
+	}
+	return opts;
+}
+ 
+kill(pid: int): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if (fd == nil)
+		return -1;
+	if (sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+Showtk: con 0;
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	if (Showtk)
+		sys->print("%s\n", s);
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "tkclient: tk error %s on '%s'\n", e, s);
+	return e;
+}
+
+cmdget(top: ref Tk->Toplevel, s: string): string
+{
+	if (Showtk)
+		sys->print("%s\n", s);
+	tk->cmd(top, "variable lasterror");
+	e := tk->cmd(top, s);
+	lerr := tk->cmd(top, "variable lasterror");
+	if (lerr != nil) sys->fprint(sys->fildes(2), "tkclient: tk error %s on '%s'\n", e, s);
+	return e;
+}
--- /dev/null
+++ b/appl/lib/sets.b
@@ -1,0 +1,329 @@
+implement Sets;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+BPW: con 32;
+SHIFT: con 5;
+MASK: con 31;
+
+# Set adt contains:
+#	a - array holding membership of set s for n (0 ≤ n < len a * BPW).
+#		∀ n: 0≤n<(len a*BPW), (s.a[n >> SHIFT] & (1 << (n & MASK)) != 0) iff s ∋ n
+#	m - "most significant bits", extrapolate membership for n >= len a * BPW.
+#		m is 0 if members are excluded by default,
+#		or ~0 if members are included by default.
+
+swapops := array[16] of {
+	byte 2r0000, byte 2r0001, byte 2r0100, byte 2r0101,
+	byte 2r0010, byte 2r0011, byte 2r0110, byte 2r0111,
+	byte 2r1000, byte 2r1001, byte 2r1100, byte 2r1101,
+	byte 2r1010, byte 2r1011, byte 2r1110, byte 2r1111,
+};
+
+Set.X(s1: self Set, o: int, s2: Set): Set
+{
+	if (len s1.a > len s2.a) {
+		(s1, s2) = (s2, s1);
+		o = int swapops[o & 2r1111];
+	}
+	r := Set(0, array[len s2.a] of int);
+	for (i := 0; i < len s1.a; i++)
+		r.a[i] = op(o, s1.a[i], s2.a[i]);
+	for (; i < len s2.a; i++)
+		r.a[i] = op(o, s1.m, s2.a[i]);
+	r.m = op(o, s1.m, s2.m);
+	return r;
+}
+
+Set.invert(s: self Set): Set
+{
+	r := Set(~s.m, array[len s.a] of int);
+	for (i := 0; i < len s.a; i++)
+		r.a[i] = ~s.a[i];
+	return r;
+}
+
+# copy s, ensuring that the copy is big enough to hold n.
+copy(s: Set, n: int): Set
+{
+	if (n >= 0) {
+		req := (n >> SHIFT) + 1;
+		if (req > len s.a) {
+			a := array[req] of int;
+			a[0:] = s.a;
+			for (i := len s.a; i < len a; i++)
+				a[i] = s.m;
+			return (s.m, a);
+		}
+	}
+	a: array of int;
+	if (len s.a > 0) {
+		a = array[len s.a] of int;
+		a[0:] = s.a;
+	}
+	return (s.m, a);
+}
+
+Set.add(s: self Set, n: int): Set
+{
+	d := n >> SHIFT;
+	if (s.m && d >= len s.a)
+		return s;
+	r := copy(s, n);
+	r.a[d] |= 1<< (n & MASK);
+	return r;
+}
+
+Set.addlist(s: self Set, ns: list of int): Set
+{
+	r: Set;
+	if (s.m == 0) {
+		max := -1;
+		for (l := ns; l != nil; l = tl l)
+			if (hd l > max)
+				max = hd l;
+		r = copy(s, max);
+	} else
+		r = copy(s, -1);
+	for (; ns != nil; ns = tl ns) {
+		n := hd ns;
+		d := n >> SHIFT;
+		if (d < len r.a)
+			r.a[d] |= 1 << (n & MASK);
+	}
+	return r;
+}
+
+
+Set.del(s: self Set, n: int): Set
+{
+	d := n >> SHIFT;
+	if (!s.m && d >= len s.a)
+		return s;
+	r := copy(s, n);
+	r.a[d] &= ~(1 << (n & MASK));
+	return r;
+}
+
+Set.holds(s: self Set, n: int): int
+{
+	d := n >> SHIFT;
+	if (d >= len s.a)
+		return s.m;
+	return s.a[d] & (1 << (n & MASK));
+}
+
+Set.limit(s: self Set): int
+{
+	for (i := len s.a - 1; i >= 0; i--)
+		if (s.a[i] != s.m)
+			return (i<<SHIFT) + topbit(s.m ^ s.a[i]);
+	return 0;
+}
+
+Set.eq(s1: self Set, s2: Set): int
+{
+	if (len s1.a > len s2.a)
+		(s1, s2) = (s2, s1);
+	for (i := 0; i < len s1.a; i++)
+		if (s1.a[i] != s2.a[i])
+			return 0;
+	for (; i < len s2.a; i++)
+		if (s1.m != s2.a[i])
+			return 0;
+	return s1.m == s2.m;
+}
+
+Set.isempty(s: self Set): int
+{
+	return Set(0, nil).eq(s);
+}
+
+Set.msb(s: self Set): int
+{
+	return s.m != 0;
+}
+
+Set.bytes(s: self Set, n: int): array of byte
+{
+	m := (s.limit() >> 3) + 1;
+	if(m > n)
+		n = m;
+	d := array[n] of byte;
+	# XXX this could proably be made substantially faster by unrolling the
+	# loop a little.
+	for(i := 0; i < len d; i++){
+		j := i >> 2;
+		if(j >= len s.a)
+			d[i] = byte s.m;
+		else
+			d[i] = byte (s.a[j] >> ((i & 3) << 3));
+	}
+	return d;
+}
+
+bytes2set(d: array of byte): Set
+{
+	if(len d == 0)
+		return (0, nil);
+	a := array[(len d + 3) >> 2] of int;		# round up
+	n := len d >> 2;
+	for(i := 0; i < n; i++){
+		j := i << 2;
+		a[i] = int d[j] + (int d[j+1] << 8) + (int d[j+2] << 16) + (int d[j+3] << 24);
+	}
+	msb := ~(int (d[len d - 1] >> 7) - 1);
+	j := i << 2;
+	case len d & 3 {
+	0 =>
+		;
+	1 =>
+		a[i] = int d[j] | (msb & int 16rffffff00);
+	2 =>
+		a[i] = int d[j] | (int d[j+1] << 8) | (msb & int 16rffff0000);
+	3 =>
+		a[i] = int d[j] | (int d[j+1] << 8) | (int d[j+2] << 16) | (msb & int 16rff000000);
+	}
+	return (msb, a);
+}
+
+Set.str(s: self Set): string
+{
+	str: string;
+
+	# discard all top bits that are the same as msb.
+	sig := 0;
+loop:
+	for (i := len s.a - 1; i >= 0; i--) {
+		t := 16rf << (BPW - 4);
+		sig = 8;
+		while (t != 0) {
+			if ((s.m & t) != (s.a[i] & t))
+				break loop;
+			sig--;
+			t = (t >> 4) & 16r0fffffff;		# logical shift right
+		}
+	}
+	if (i >= 0) {
+		top := s.a[i];
+		if (sig < 8)		# shifting left by 32 bits is undefined.
+			top &= (1 << (sig << 2)) - 1;
+		str = sys->sprint("%.*ux", sig, top);
+		for (i--; i >= 0; i--)
+			str += sys->sprint("%.8ux", s.a[i]);
+	}
+	return str + ":" + string (s.m & 1);
+}
+
+str2set(str: string): Set
+{
+	n := len str;
+	if (n < 2 || str[n - 2] != ':')
+		return (0, nil);
+	c := str[n - 1];
+	if (c != '0' && c != '1')
+		return (0, nil);
+	msb := ~(c - '1');
+
+	n -= 2;
+	if (n == 0)
+		return (msb, nil);
+	req := ((n * 4 - 1) >> SHIFT) + 1;
+	a := array[req] of int;
+	d := 0;
+	for (i := n; i > 0; ) {
+		j := i - 8;
+		if (j < 0)
+			j = 0;
+		a[d++] = hex2int(str[j:i], msb);
+		i = j;
+	}
+	return (msb, a);
+}
+
+Set.debugstr(s: self Set): string
+{
+	str: string;
+	for (i := len s.a - 1; i >= 0; i--)
+		str += sys->sprint("%ux:", s.a[i]);
+	str += sys->sprint(":%ux", s.m);
+	return str;
+}
+
+set(): Set
+{
+	return (0, nil);
+}
+
+hex2int(s: string, fill: int): int
+{
+	n := fill;
+	for (i := 0; i < len s; i++) {
+		c := s[i];
+		if (c >= '0' && c <= '9')
+			c -= '0';
+		else if (c >= 'a' && c <= 'f')
+			c -= 'a' - 10;
+		else if (c >= 'A' && c <= 'F')
+			c -= 'A' - 10;
+		else
+			c = 0;
+		n = (n << 4) | c;
+	}
+	return n;
+}
+
+op(o: int, a, b: int): int
+{
+	case o &  2r1111 {
+	2r0000 => return 0;
+	2r0001 => return ~(a | b);
+	2r0010 => return a & ~b;
+	2r0011 => return ~b;
+	2r0100 => return ~a & b;
+	2r0101 => return ~a;
+	2r0110 => return a ^ b;
+	2r0111 => return ~(a & b);
+	2r1000 => return a & b;
+	2r1001 => return ~(a ^ b);
+	2r1010 => return a;
+	2r1011 => return a | ~b;
+	2r1100 => return b;
+	2r1101 => return ~a | b;
+	2r1110 => return a | b;
+	2r1111 => return ~0;
+	}
+	return 0;
+}
+
+topbit(v: int): int
+{
+	if (v == 0)
+		return 0;
+	(b, n, mask) := (1, 16, int 16rffff0000);
+	while (n != 0) {
+		if (v & mask) {
+			b += n;
+			v >>= n;		# could return if v==0 here if we thought it worth it
+		}
+		n >>= 1;
+		mask >>= n;
+	}
+	return b;
+}
+
+nbits(n: int): int
+{
+	n = ((n >> 1) & 16r55555555) + (n & 16r55555555) ;
+	n = ((n >> 2) & 16r33333333) + (n & 16r33333333) ;
+	n = ((n >> 4) + n) & 16r0F0F0F0F ;
+	n = ((n >> 8) + n) ;
+	return ((n >> 16) + n) & 16rFF ;
+}
--- /dev/null
+++ b/appl/lib/sets32.b
@@ -1,0 +1,226 @@
+implement Sets;
+include "sys.m";
+	sys: Sys;
+include "sets32.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+set(): Set
+{
+	return Set(0);
+}
+
+BITS: con 32;
+MSB: con 1 << (BITS - 1);
+
+Set.X(s1: self Set, o: int, s2: Set): Set
+{
+	return Set(op(o, s1.s, s2.s));
+}
+
+Set.invert(s: self Set): Set
+{
+	return Set(~s.s);
+}
+
+Set.add(s: self Set, n: int): Set
+{
+	return Set(s.s | (1 << n));
+}
+
+Set.del(s: self Set, n: int): Set
+{
+	return Set(s.s & ~(1 << n));
+}
+
+Set.addlist(s: self Set, ns: list of int): Set
+{
+	for (; ns != nil; ns = tl ns)
+		s.s |= (1 << hd ns);
+	return s;
+}
+
+Set.holds(s: self Set, n: int): int
+{
+	return s.s & (1 << n);
+}
+
+Set.str(s: self Set): string
+{
+	msb := s.s >> (BITS - 1);
+
+	# discard all top bits that are the same as msb
+	t := 16rf << (BITS - 4);
+	sig := 8;
+	while (t != 0) {
+		if ((msb & t) != (s.s & t))
+			break;
+		sig--;
+		t = (t >> 4) & 16r0fffffff;		# logical shift right
+	}
+	str: string;
+	if (sig > 0) {
+		top := ~MSB & s.s;
+		if (sig < 8)		# shifting left by 32 bits is undefined.
+			top &= (1 << (sig << 2)) - 1;
+		str = sys->sprint("%.*ux", sig, top);
+	}
+	return str + ":" + string (msb & 1);
+}
+
+Set.bytes(s: self Set, n: int): array of byte
+{
+	m := (s.limit() >> 3) + 1;
+	if(m > n)
+		n = m;
+	d := array[n] of byte;
+	case len d {
+	1 =>
+		d[0] = byte s.s;
+	2 =>
+		d[0] = byte s.s;
+		d[1] = byte (s.s >> 8);
+	3 =>
+		d[0] = byte s.s;
+		d[1] = byte (s.s >> 8);
+		d[2] = byte (s.s >> 16);
+	4 =>
+		d[0] = byte s.s;
+		d[1] = byte (s.s >> 8);
+		d[2] = byte (s.s >> 16);
+		d[3] = byte (s.s >> 24);
+	* =>
+		d[0] = byte s.s;
+		d[1] = byte (s.s >> 8);
+		d[2] = byte (s.s >> 16);
+		d[3] = byte (s.s >> 24);
+		msb := byte (s.s >> (BITS - 1));		# sign extension
+		for(i := 4; i < len d; i++)
+			d[i] = msb;
+	}
+	return d;
+}
+		
+bytes2set(d: array of byte): Set
+{
+	if(len d == 0)
+		return Set(0);
+	msb := ~(int (d[len d - 1] >> 7) - 1);
+	v: int;
+	case len d {
+	1 =>
+		v = int d[0] | (msb & int 16rffffff00);
+	2 =>
+		v = int d[0] | (int d[1] << 8) | (msb & int 16rffff0000);
+	3 =>
+		v = int d[0] | (int d[1] << 8) | (int d[2] << 16) | (msb & int 16rff000000);
+	* or		# XXX could raise (or return) an error for len d > 4
+	4 =>
+		v = int d[0] | (int d[1] << 8) | (int d[2] << 16) | (int d[3] << 24);
+	}
+	return Set(v);
+}
+
+
+Set.debugstr(s: self Set): string
+{
+	return sys->sprint("%ux", s.s);
+}
+
+Set.eq(s1: self Set, s2: Set): int
+{
+	return s1.s == s2.s;
+}
+
+Set.isempty(s: self Set): int
+{
+	return s.s == 0;
+}
+
+Set.msb(s: self Set): int
+{
+	return (s.s & MSB) != 0;
+}
+
+Set.limit(s: self Set): int
+{
+	m := s.s >> (BITS - 1);	# sign extension
+	return topbit(s.s ^ m);
+}
+
+topbit(v: int): int
+{
+	if (v == 0)
+		return 0;
+	(b, n, mask) := (1, 16, int 16rffff0000);
+	while (n != 0) {
+		if (v & mask) {
+			b += n;
+			v >>= n;		# could return if v==0 here if we thought it worth it
+		}
+		n >>= 1;
+		mask >>= n;
+	}
+	return b;
+}
+
+
+str2set(str: string): Set
+{
+	n := len str;
+	if (n < 2 || str[n - 2] != ':')
+		return Set(0);
+	c := str[n - 1];
+	if (c != '0' && c != '1')
+		return Set(0);
+	n -= 2;
+	msb := ~(c - '1');
+	# XXX should we give some sort of error if there
+	# are more bits than we can hold?
+	return Set((hex2int(str[0:n], msb) & ~MSB) | (msb & MSB));
+}
+
+hex2int(s: string, fill: int): int
+{
+	n := fill;
+	for (i := 0; i < len s; i++) {
+		c := s[i];
+		if (c >= '0' && c <= '9')
+			c -= '0';
+		else if (c >= 'a' && c <= 'f')
+			c -= 'a' - 10;
+		else if (c >= 'A' && c <= 'F')
+			c -= 'A' - 10;
+		else
+			c = 0;
+		n = (n << 4) | c;
+	}
+	return n;
+}
+ 
+
+op(o: int, a, b: int): int
+{
+	case o &  2r1111 {
+	2r0000 => return 0;
+	2r0001 => return ~(a | b);
+	2r0010 => return a & ~b;
+	2r0011 => return ~b;
+	2r0100 => return ~a & b;
+	2r0101 => return ~a;
+	2r0110 => return a ^ b;
+	2r0111 => return ~(a & b);
+	2r1000 => return a & b;
+	2r1001 => return ~(a ^ b);
+	2r1010 => return a;
+	2r1011 => return a | ~b;
+	2r1100 => return b;
+	2r1101 => return ~a | b;
+	2r1110 => return a | b;
+	2r1111 => return ~0;
+	}
+	return 0;
+}
--- /dev/null
+++ b/appl/lib/sexprs.b
@@ -1,0 +1,638 @@
+implement Sexprs;
+
+#
+# full SDSI/SPKI S-expression reader
+#
+# Copyright © 2003-2004 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "encoding.m";
+	base64: Encoding;
+	base16: Encoding;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "sexprs.m";
+
+Maxtoken: con 1024*1024;	# should be more than enough
+
+Syntax: exception(string, big);
+Here: con big -1;
+
+Rd: adt[T]
+	for {
+	T =>
+		getb:	fn(nil: self T): int;
+		ungetb:	fn(nil: self T): int;
+		offset:	fn(nil: self T): big;
+	}
+{
+	t:	T;
+
+	parseitem:	fn(rd: self ref Rd[T]): ref Sexp raises (Syntax);
+	ws:	fn(rd: self ref Rd[T]): int;
+	simplestring:	fn(rd: self ref Rd[T], c: int, hint: string): ref Sexp raises (Syntax);
+	toclosing:	fn(rd: self ref Rd[T], c: int): string raises (Syntax);
+	unquote:	fn(rd: self ref Rd[T]): string raises (Syntax);
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	base64 = load Encoding Encoding->BASE64PATH;
+	base16 = load Encoding Encoding->BASE16PATH;
+	bufio = load Bufio Bufio->PATH;
+	bufio->sopen("");
+}
+
+Sexp.read[T](t: T): (ref Sexp, string)
+	for {
+	T =>
+		getb:	fn(nil: self T): int;
+		ungetb:	fn(nil: self T): int;
+		offset:	fn(nil: self T): big;
+	}
+{
+	{
+		rd := ref Rd[T](t);
+		e := rd.parseitem();
+		return (e, nil);
+	}exception e {
+	Syntax =>
+		(diag, pos) := e;
+		if(pos < big 0)
+			pos += t.offset();
+		return (nil, sys->sprint("%s at offset %bd", diag, pos));
+	}
+}
+
+Sexp.parse(s: string): (ref Sexp, string, string)
+{
+	f := bufio->sopen(s);
+	(e, diag) := Sexp.read(f);
+	pos := int f.offset();
+	return (e, s[pos:], diag);
+}
+
+Sexp.unpack(a: array of byte): (ref Sexp, array of byte, string)
+{
+	f := bufio->aopen(a);
+	(e, diag) := Sexp.read(f);
+	pos := int f.offset();
+	return (e, a[pos:], diag);
+}
+
+Rd[T].parseitem(rd: self ref Rd[T]): ref Sexp raises (Syntax)
+{
+	p0 := rd.t.offset();
+	{
+		c := rd.ws();
+		if(c < 0)
+			return nil;
+		case c {
+		'{' =>
+			a := rd.toclosing('}');
+			f := bufio->aopen(base64->dec(a));
+			ht: type Rd[ref Iobuf];
+			nr := ref ht(f);
+			return nr.parseitem();
+		'(' =>
+			lists: list of ref Sexp;
+			while((c = rd.ws()) != ')'){
+				if(c < 0)
+					raise Syntax("unclosed '('", p0);
+				rd.t.ungetb();
+				e := rd.parseitem();	# we'll catch missing ) at top of loop
+				lists = e :: lists;
+			}
+			rl := lists;
+			lists = nil;
+			for(; rl != nil; rl = tl rl)
+				lists = hd rl :: lists;
+			return ref Sexp.List(lists);
+		'[' =>
+			# display hint
+			e := rd.simplestring(rd.t.getb(), nil);
+			c = rd.ws();
+			if(c != ']'){
+				if(c >= 0)
+					rd.t.ungetb();
+				raise Syntax("missing ] in display hint", p0);
+			}
+			pick r := e {
+			String =>
+				return rd.simplestring(rd.ws(), r.s);
+			* =>
+				raise Syntax("illegal display hint", Here);
+			}
+		* =>
+			return rd.simplestring(c, nil);
+		}
+	}exception{
+	Syntax => raise;
+	}
+}
+
+# skip white space
+Rd[T].ws(rd: self ref Rd[T]): int
+{
+	while(isspace(c := rd.t.getb()))
+		{}
+	return c;
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\r' || c == '\t' || c == '\n';
+}
+
+Rd[T].simplestring(rd: self ref Rd[T], c: int, hint: string): ref Sexp raises (Syntax)
+{
+	dec := -1;
+	decs: string;
+	if(c >= '0' && c <= '9'){
+		for(dec = 0; c >= '0' && c <= '9'; c = rd.t.getb()){
+			dec = dec*10 + c-'0';
+			decs[len decs] = c;
+		}
+		if(dec < 0 || dec > Maxtoken)
+			raise Syntax("implausible token length", Here);
+	}
+	{
+		case c {
+		'"' =>
+			text := rd.unquote();
+			return ref Sexp.String(text, hint);
+		'|' =>
+			return sform(base64->dec(rd.toclosing(c)), hint);
+		'#' =>
+			return sform(base16->dec(rd.toclosing(c)), hint);
+		* =>
+			if(c == ':' && dec >= 0){	# raw bytes
+				a := array[dec] of byte;
+				for(i := 0; i < dec; i++){
+					c = rd.t.getb();
+					if(c < 0)
+						raise Syntax("missing bytes in raw token", Here);
+					a[i] = byte c;
+				}
+				return sform(a, hint);
+			}
+			#s := decs;
+			if(decs != nil)
+				raise Syntax("token can't start with a digit", Here);
+			s: string;	# <token> by definition is always printable; never utf-8
+			while(istokenc(c)){
+				s[len s] = c;
+				c = rd.t.getb();
+			}
+			if(s == nil)
+				raise Syntax("missing token", Here);	# consume c to ensure progress on error
+			if(c >= 0)
+				rd.t.ungetb();
+			return ref Sexp.String(s, hint);
+		}
+	}exception{
+	Syntax => raise;
+	}
+}
+
+sform(a: array of byte, hint: string): ref Sexp
+{
+	if(istextual(a))
+		return ref Sexp.String(string a, hint);
+	return ref Sexp.Binary(a, hint);
+}
+
+Rd[T].toclosing(rd: self ref Rd[T], end: int): string raises (Syntax)
+{
+	s: string;
+	p0 := rd.t.offset();
+	while((c := rd.t.getb()) != end){
+		if(c < 0)
+			raise Syntax(sys->sprint("missing closing '%c'", end), p0);
+		s[len s] = c;
+	}
+	return s;
+}
+
+hex(c: int): int
+{
+	if(c >= '0' && c <= '9')
+		return c-'0';
+	if(c >= 'a' && c <= 'f')
+		return 10+(c-'a');
+	if(c >= 'A' && c <= 'F')
+		return 10+(c-'A');
+	return -1;
+}
+
+Rd[T].unquote(rd: self ref Rd[T]): string raises (Syntax)
+{
+	os: string;
+
+	p0 := rd.t.offset();
+	while((c := rd.t.getb()) != '"'){
+		if(c < 0)
+			raise Syntax("unclosed quoted string", p0);
+		if(c == '\\'){
+			e0 := rd.t.offset();
+			c = rd.t.getb();
+			if(c < 0)
+				break;
+			case c {
+			'\r' =>
+				c = rd.t.getb();
+				if(c != '\n')
+					rd.t.ungetb();
+				continue;
+			'\n' =>
+				c = rd.t.getb();
+				if(c != '\r')
+					rd.t.ungetb();
+				continue;
+			'b' =>
+				c = '\b';
+			'f' =>
+				c = '\f';
+			'n' =>
+				c = '\n';
+			'r' =>
+				c = '\r';
+			't' =>
+				c = '\t';
+			'v' =>
+				c = '\v';
+			'0' to '7' =>
+				oct := 0;
+				for(i := 0;;){
+					if(!(c >= '0' && c <= '7'))
+						raise Syntax("illegal octal escape", e0);
+					oct = (oct<<3) | (c-'0');
+					if(++i == 3)
+						break;
+					c = rd.t.getb();
+				}
+				c = oct & 16rFF;
+			'x' =>
+				c0 := hex(rd.t.getb());
+				c1 := hex(rd.t.getb());
+				if(c0 < 0 || c1 < 0)
+					raise Syntax("illegal hex escape", e0);
+				c = (c0<<4) | c1;
+			* =>
+				;	# as-is
+			}
+		}
+		os[len os] = c;
+	}
+	return os;
+}
+
+hintlen(s: string): int
+{
+	if(s == nil)
+		return 0;
+	n := len array of byte s;
+	return len sys->aprint("[%d:]", n) + n;
+}
+
+Sexp.packedsize(e: self ref Sexp): int
+{
+	if(e == nil)
+		return 0;
+	pick r := e{
+	String =>
+		n := len array of byte r.s;
+		return hintlen(r.hint) + len sys->aprint("%d:", n) + n;
+	Binary =>
+		n := len r.data;
+		return hintlen(r.hint) + len sys->aprint("%d:", n) + n;
+	List =>
+		n := 1;	# '('
+		for(l := r.l; l != nil; l = tl l)
+			n += (hd l).packedsize();
+		return n+1;	# + ')'
+	}
+}
+
+packbytes(a: array of byte, b: array of byte): array of byte
+{
+	n := len b;
+	c := sys->aprint("%d:", n);
+	a[0:] = c;
+	a[len c:] = b;
+	return a[len c+n:];
+}
+
+packhint(a: array of byte, s: string): array of byte
+{
+	if(s == nil)
+		return a;
+	a[0] = byte '[';
+	a = packbytes(a[1:], array of byte s);
+	a[0] = byte ']';
+	return a[1:];
+}
+
+pack(e: ref Sexp, a: array of byte): array of byte
+{
+	if(e == nil)
+		return array[0] of byte;
+	pick r := e{
+	String =>
+		if(r.hint != nil)
+			a = packhint(a, r.hint);
+		return packbytes(a, array of byte r.s);
+	Binary =>
+		if(r.hint != nil)
+			a = packhint(a, r.hint);
+		return packbytes(a, r.data);
+	List =>
+		a[0] = byte '(';
+		a = a[1:];
+		for(l := r.l; l != nil; l = tl l)
+			a = pack(hd l, a);
+		a[0] = byte ')';
+		return a[1:];
+	}
+}
+
+Sexp.pack(e: self ref Sexp): array of byte
+{
+	a := array[e.packedsize()] of byte;
+	pack(e, a);
+	return a;
+}
+
+Sexp.b64text(e: self ref Sexp): string
+{
+	return "{" + base64->enc(e.pack()) + "}";
+}
+
+Sexp.text(e: self ref Sexp): string
+{
+	if(e == nil)
+		return "";
+	pick r := e{
+	String =>
+		s := quote(r.s);
+		if(r.hint == nil)
+			return s;
+		return "["+quote(r.hint)+"]"+s;
+	Binary =>
+		h := r.hint;
+		if(h != nil)
+			h = "["+quote(h)+"]";
+		if(len r.data <= 4)
+			return sys->sprint("%s#%s#", h, base16->enc(r.data));
+		return sys->sprint("%s|%s|", h, base64->enc(r.data));
+	List =>
+		s := "(";
+		for(l := r.l; l != nil; l = tl l){
+			s += (hd l).text();
+			if(tl l != nil)
+				s += " ";
+		}
+		return s+")";
+	}
+}
+
+#An octet string that meets the following conditions may be given
+#directly as a "token".
+#
+#	-- it does not begin with a digit
+#
+#	-- it contains only characters that are
+#		-- alphabetic (upper or lower case),
+#		-- numeric, or
+#		-- one of the eight "pseudo-alphabetic" punctuation marks:
+#			-   .   /   _   :  *  +  =  
+#	(Note: upper and lower case are not equivalent.)
+#	(Note: A token may begin with punctuation, including ":").
+
+istokenc(c: int): int
+{
+	return c >= '0' && c <= '9' ||
+		c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' ||
+		c == '-' || c == '.' || c == '/' || c == '_' || c == ':' || c == '*' || c == '+' || c == '=';
+}
+
+istoken(s: string): int
+{
+	if(s == nil)
+		return 0;
+	for(i := 0; i < len s; i++)
+		case s[i] {
+		'0' to '9' =>
+			if(i == 0)
+				return 0;
+		'a' to 'z' or 'A' to 'Z' or
+		'-' or '.' or '/' or '_' or ':' or '*' or '+' or '=' =>
+			break;
+		* =>
+			return 0;
+		}
+	return 1;
+}
+
+# should the data qualify as binary or text?
+# the if(0) version accepts valid Unicode sequences
+# could use [display] to control character set?
+istextual(a: array of byte): int
+{
+	for(i := 0; i < len a;){
+		if(0){
+			(c, n, ok) := sys->byte2char(a, i);
+			if(!ok || c < ' ' && !isspace(c) || c >= 16r7F)
+				return 0;
+			i += n;
+		}else{
+			c := int a[i++];
+			if(c < ' ' && !isspace(c) || c >= 16r7F)
+				return 0;
+		}
+	}
+	return 1;
+}
+
+esc(c: int): string
+{
+	case c {
+	'"' =>	return "\\\"";
+	'\\' =>	return "\\\\";
+	'\b' =>	return "\\b";
+	'\f' =>	return "\\f";
+	'\n' =>	return "\\n";
+	'\t' =>	return "\\t";
+	'\r' =>	return "\\r";
+	'\v' =>	return "\\v";
+	* =>
+		if(c < ' ' || c >= 16r7F)
+			return sys->sprint("\\x%.2ux", c & 16rFF);
+	}
+	return nil;
+}
+
+quote(s: string): string
+{
+	if(istoken(s))
+		return s;
+	for(i := 0; i < len s; i++)
+		if((v := esc(s[i])) != nil){
+			os := "\"" + s[0:i] + v;
+			while(++i < len s){
+				if((v = esc(s[i])) != nil)
+					os += v;
+				else
+					os[len os] = s[i];
+			}
+			os[len os] = '"';
+			return os;
+		}
+	return "\""+s+"\"";
+}
+
+#
+# other S expression operations
+#
+Sexp.islist(e: self ref Sexp): int
+{
+	return e != nil && tagof e == tagof Sexp.List;
+}
+
+Sexp.els(e: self ref Sexp): list of ref Sexp
+{
+	if(e == nil)
+		return nil;
+	pick s := e {
+	List =>
+		return s.l;
+	* =>
+		return nil;
+	}
+}
+
+Sexp.op(e: self ref Sexp): string
+{
+	if(e == nil)
+		return nil;
+	pick s := e {
+	String =>
+		return s.s;
+	Binary =>
+		return nil;
+	List =>
+		if(s.l == nil)
+			return nil;
+		pick t := hd s.l {
+		String =>
+			return t.s;
+		* =>
+			return nil;
+		}
+	}
+	return nil;
+}
+
+Sexp.args(e: self ref Sexp): list of ref Sexp
+{
+	if((l := e.els()) != nil)
+		return tl l;
+	return nil;
+}
+
+Sexp.asdata(e: self ref Sexp): array of byte
+{
+	if(e == nil)
+		return nil;
+	pick s := e {
+	List =>
+		return nil;
+	String =>
+		return array of byte s.s;
+	Binary =>
+		return s.data;
+	}
+}
+
+Sexp.astext(e: self ref Sexp): string
+{
+	if(e == nil)
+		return nil;
+	pick s := e {
+	List =>
+		return nil;
+	String =>
+		return s.s;
+	Binary =>
+		return string s.data;	# questionable; should possibly treat it as latin-1
+	}
+}
+
+Sexp.eq(e1: self ref Sexp, e2: ref Sexp): int
+{
+	if(e1 == e2)
+		return 1;
+	if(e1 == nil || e2 == nil || tagof e1 != tagof e2)
+		return 0;
+	pick s1 := e1 {
+	List =>
+		pick s2 := e2 {
+		List =>
+			l1 := s1.l;
+			l2 := s2.l;
+			for(; l1 != nil; l1 = tl l1){
+				if(l2 == nil || !(hd l1).eq(hd l2))
+					return 0;
+				l2 = tl l2;
+			}
+			return l2 == nil;
+		}
+	String =>
+		pick s2 := e2 {
+		String =>
+			return s1.s == s2.s && s1.hint == s2.hint;
+		}
+	Binary =>
+		pick s2 := e2 {
+		Binary =>
+			if(len s1.data != len s2.data || s1.hint != s2.hint)
+				return 0;
+			for(i := 0; i < len s1.data; i++)
+				if(s1.data[i] != s2.data[i])
+					return 0;
+			return 1;
+		}
+	}
+	return 0;
+}
+
+Sexp.copy(e: self ref Sexp): ref Sexp
+{
+	if(e == nil)
+		return nil;
+	pick r := e {
+	List =>
+		rl: list of ref Sexp;
+		for(l := r.l; l != nil; l = tl l)
+			rl = (hd l).copy() :: rl;
+		for(l = nil; rl != nil; rl = tl rl)
+			l = hd rl :: l;
+		return ref Sexp.List(l);
+	String =>
+		return ref *r;	# safe because .s and .hint are strings, immutable
+	Binary =>
+		b: array of byte;
+		if((a := r.data) != nil){
+			b = array[len a] of byte;
+			b[0:] = a;
+		}
+		return ref Sexp.Binary(b, r.hint);
+	}
+}
--- /dev/null
+++ b/appl/lib/slip.b
@@ -1,0 +1,113 @@
+implement Filter;
+
+include "sys.m";
+
+include "filter.m";
+
+End: con byte 8r300;
+Esc: con byte 8r333;
+Eend: con byte 8r334;	# encoded End byte
+Eesc: con byte 8r335;	# encoded Esc byte
+
+init()
+{
+}
+
+start(param: string): chan of ref Rq
+{
+	req := chan of ref Rq;
+	if(param == "encode")
+		spawn encode(req);
+	else
+		spawn decode(req);
+	return req;
+}
+
+encode(reqs: chan of ref Rq)
+{
+	sys := load Sys Sys->PATH;
+	reqs <-= ref Rq.Start(sys->pctl(0, nil));
+	buf := array[8192] of byte;
+	rc := chan of int;
+	do{
+		reqs <-= ref Rq.Fill(buf, rc);
+		if((n := <-rc) <= 0){
+			if(n == 0)
+				reqs <-= ref Rq.Finished(nil);
+			break;
+		}
+		b := array[2*n + 2] of byte;	# optimise time not space
+		o := 1;
+		b[0] = End;
+		for(i := 0; i < n; i++){
+			if((c := buf[i]) == End || c == Esc){
+				b[o++] = Esc;
+				c = byte (Eend + (c& byte 1));
+			}
+			b[o++] = c;
+		}
+		b[o++] = End;
+		if(o != len b)
+			b = b[0:o];
+		reqs <-= ref Rq.Result(b, rc);
+	}while(<-rc != -1);
+}
+
+Slipesc, Slipend: con (1<<8) + iota;
+Slipsize: con 1006;	# rfc's suggestion
+
+slipin(c: byte, esc: int): int
+{
+	if(esc == Slipesc){	# last byte was Esc
+		if(c == Eend)
+			c = End;
+		else if(c == Eesc)
+			c = Esc;
+	}else{
+		if(c == Esc)
+			return Slipesc;
+		if(c == End)
+			return Slipend;
+	}
+	return int c;
+}
+
+decode(reqs: chan of ref Rq)
+{
+	sys := load Sys Sys->PATH;
+	reqs <-= ref Rq.Start(sys->pctl(0, nil));
+	buf := array[8192] of byte;
+	b := array[Slipsize] of byte;
+	rc := chan of int;
+	c := 0;
+	o := 0;
+	for(;;){
+		reqs <-= ref Rq.Fill(buf, rc);
+		if((n := <-rc) <= 0){
+			if(n < 0)
+				exit;
+			break;
+		}
+		for(i := 0; i < n; i++){
+			c = slipin(buf[i], c);
+			if(c == Slipend){
+				if(o != 0){
+					reqs <-= ref Rq.Result(b[0:o], rc);
+					if(<-rc == -1)
+						exit;
+					b = array[Slipsize] of byte;
+					o = 0;
+				}
+			}else if(c != Slipesc){
+				if(o >= len b){
+					t := array[3*len b/2] of byte;
+					t[0:] = b;
+					b = t;
+				}
+				b[o++] = byte c;
+			}
+		}
+	}
+	# partial block discarded
+	reqs <-= ref Rq.Finished(nil);
+}
--- /dev/null
+++ b/appl/lib/smtp.b
@@ -1,0 +1,248 @@
+implement Smtp;
+ 
+include "sys.m";
+	sys : Sys;
+include "bufio.m";
+	bufio : Bufio;
+include "dial.m";
+	dial: Dial;
+include "smtp.m";
+
+FD: import sys;
+Iobuf: import bufio;
+Connection: import dial;
+
+ibuf, obuf : ref Bufio->Iobuf;
+conn : int = 0;
+init : int = 0;
+ 
+rpid : int = -1;
+cread : chan of (int, string);
+
+DEBUG : con 0;
+
+open(server : string): (int, string)
+{
+	s : string;
+ 
+	if (!init) {
+		sys = load Sys Sys->PATH;
+		bufio = load Bufio Bufio->PATH;
+		dial = load Dial Dial->PATH;
+		init = 1;
+	}
+	if (conn)
+		return (-1, "connection is already open");
+	if (server == nil)
+		server = "$smtp";
+	else
+		server = dial->netmkaddr(server, "tcp", "25");
+	c := dial->dial(server, nil);
+	if (c == nil)
+		return (-1, "dialup failed");
+	ibuf = bufio->fopen(c.dfd, Bufio->OREAD);
+	obuf = bufio->fopen(c.dfd, Bufio->OWRITE);
+	if (ibuf == nil || obuf == nil)
+		return (-1, "failed to open bufio");
+	cread = chan of (int, string);
+	spawn mreader(cread);
+	(rpid, nil) = <- cread;
+	ok: int;
+ 	(ok, s) = mread();
+	if (ok < 0)
+		return (-1, s);
+	conn = 1;
+	return (1, nil);
+}
+ 
+sendmail (fromwho : string, towho : list of string, cc : list of string, mlist: list of string): (int, string)
+{
+	ok : int;
+	s, t, line : string;
+
+	if (!conn)
+		return (-1, "connection is not open");
+	(ok, s) = mcmd("RSET");
+	if (ok < 0)
+		return (-1, s);
+	(user, dom) := split(fromwho, '@');
+	if (fromwho == nil || user == nil)
+		return (-1, "no 'from' name");
+	if (towho == nil)
+		return (-1, "no 'to' name");
+	if (dom == nil)
+		return (-1, "no domain name");
+	(ok, s) = mcmd("HELO " + dom);
+	if (ok < 0)
+		return (-1, s);
+	(ok, s) = mcmd("MAIL FROM:<" + fromwho + ">");
+	if (ok < 0)
+		return (-1, s);
+	all := concat(towho, cc);
+	t = nil;
+	for ( ; all != nil; all = tl all) {
+		(ok, s) = mcmd("RCPT TO:<" + hd all + ">");
+		if (ok < 0)
+			t += " " + s;
+	}
+	if (t != nil)
+		return (-1, t);
+	(ok, s) = mcmd("DATA");
+	if (ok < 0)
+		return (-1, s);
+	for ( ; mlist != nil; mlist = tl mlist) {
+		for (msg := hd mlist; msg != nil; ) {
+			(line, msg) = split(msg, '\n');	# BUG: too much copying for larger messages
+			if (putline(line) < 0)
+				return (-1, sys->sprint("write to server failed: %r"));
+		}
+	}
+	obuf.flush();
+	(ok, s) = mcmd(".");      
+	if (ok < 0)  
+		return (-1, s);  
+	return (1, nil);
+}
+
+putline(line: string): int
+{
+	ln := len line;
+	if (ln > 0 && line[ln-1] == '\r')
+		line = line[0:ln-1];
+	if (line != nil && line[0] == '.'){
+		if(obuf.putb(byte '.') < 0)
+			return -1;
+	}
+	if(line != nil && obuf.puts(line) < 0)
+		return -1;
+	return obuf.puts("\r\n");
+}
+
+close(): (int, string)
+{
+	ok : int;
+ 
+	if (!conn)
+		return (-1, "connection is not open");
+	ok = mwrite("QUIT");
+	kill(rpid);
+	ibuf.close();
+	obuf.close();
+	conn = 0;
+	if (ok < 0)
+		return (-1, "failed to close connection");
+	return (1, nil);
+}
+ 
+SLPTIME : con 100;
+MAXSLPTIME : con 10000;
+
+mread() : (int, string)
+{
+	t := 0;
+	while (t < MAXSLPTIME) {
+		alt {
+			(ok, s) := <- cread =>
+				return (ok, s);
+			* =>
+				t += SLPTIME;
+				sys->sleep(SLPTIME);
+		}
+	}
+	kill(rpid);
+	return (-1, "smtp timed out\n");		
+}
+
+mreader(c : chan of (int, string))
+{
+	c <- = (sys->pctl(0, nil), nil);
+	for (;;) {
+		line := ibuf.gets('\n');
+		if (DEBUG)
+			sys->print("mread : %s", line);
+		if (line == nil) {
+			c <- = (-1, "could not read response from server");
+			continue;
+		}
+		l := len line;
+		if (line[l-1] == '\n')
+			l--;
+		if (line[l-1] == '\r')
+			l--;
+		if (l < 3) {
+			c <- = (-1, "short response from server");
+			continue;
+		}
+		if (l > 0 && (line[0] == '1' || line[0] == '2' || line[0] == '3')) {
+			c <- = (1, nil);
+			continue;
+		}
+		c <- = (-1, line[3:l]);
+	}
+}
+ 
+mwrite(s : string): int
+{
+	s += "\r\n";
+	if (DEBUG)
+		sys->print("mwrite : %s", s);
+	b := array of byte s;
+	l := len b;
+	nb := obuf.write(b, l);
+	obuf.flush();
+	if (nb != l)
+		return -1;
+	return 1;
+}
+ 
+mcmd(s : string) : (int, string)
+{
+	ok : int;
+	r : string;
+
+	ok = mwrite(s);
+	if (ok < 0)
+		return (-1, err(s) + " send failed");
+	(ok, r) = mread();
+	if (ok < 0)
+		return (-1, err(s) + " receive failed (" + r + ")");
+	return (1, nil);
+}
+
+split(s : string, c : int) : (string, string)
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return (s[0:i], s[i+1:]);
+	return (s, nil);
+}
+
+concat(l1, l2 : list of string) : list of string
+{
+	ls : list of string;
+
+	ls = nil;
+	for (l := l1; l != nil; l = tl l)
+		ls = hd l :: ls;
+	for (l = l2; l != nil; l = tl l)
+		ls = hd l :: ls;
+	return ls;
+}
+
+err(s : string) : string
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == ' ' || s[i] == ':')
+			return s[0:i];
+	return s;
+}
+
+kill(pid : int) : int
+{
+	if (pid < 0)
+		return 0;
+	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if (fd == nil || sys->fprint(fd, "kill") < 0)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/appl/lib/sort.b
@@ -1,0 +1,36 @@
+implement Sort;
+include "sort.m";
+
+sort[S, T](s: S, a: array of T)
+	for{
+	S =>
+		gt: fn(s: self S, x, y: T): int;
+	}
+{
+	mergesort(s, a, array[len a] of T);
+}
+
+mergesort[S, T](s: S, a, b: array of T)
+	for{
+	S =>
+		gt: fn(s: self S, x, y: T): int;
+	}
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(s, a[0:m], b[0:m]);
+		mergesort(s, a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if(s.gt(b[i], b[j]))
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
--- /dev/null
+++ b/appl/lib/spki/mkfile
@@ -1,0 +1,21 @@
+<../../../mkconfig
+
+TARG=\
+	spki.dis\
+	verifier.dis\
+
+MODULES=
+
+SYSMODULES= \
+	sys.m\
+	daytime.m\
+	keyring.m\
+	security.m\
+	bufio.m\
+	sexprs.m\
+	spki.m\
+	encoding.m\
+
+DISBIN=$ROOT/dis/lib/spki
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/spki/spki.b
@@ -1,0 +1,2365 @@
+implement SPKI;
+
+#
+# Copyright © 2004 Vita Nuova Holdings Limited
+#
+# To do:
+#	- diagnostics
+#	- support for dsa
+#	- finish the TO DO
+
+include "sys.m";
+	sys: Sys;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "keyring.m";
+	kr: Keyring;
+	IPint, Certificate, PK, SK: import kr;
+
+include "security.m";
+
+include "bufio.m";
+
+include "sexprs.m";
+	sexprs: Sexprs;
+	Sexp: import sexprs;
+
+include "spki.m";
+
+include "encoding.m";
+	base16: Encoding;
+	base64: Encoding;
+
+debug: con 0;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	daytime = load Daytime Daytime->PATH;
+	sexprs = load Sexprs Sexprs->PATH;
+	base16 = load Encoding Encoding->BASE16PATH;
+	base64 = load Encoding Encoding->BASE64PATH;
+
+	sexprs->init();
+}
+
+#
+# parse SPKI structures
+#
+
+parse(e: ref Sexp): (ref Toplev, string)
+{
+	if(e == nil)
+		return (nil, "nil expression");
+	if(!e.islist())
+		return (nil, "list expected");
+	case e.op() {
+	"cert" =>
+		if((c := parsecert(e)) != nil)
+			return (ref Toplev.C(c), nil);
+		return (nil, "bad certificate syntax");
+	"signature" =>
+		if((s := parsesig(e)) != nil)
+			return (ref Toplev.Sig(s), nil);
+		return (nil, "bad signature syntax");
+	"public-key" or "private-key" =>
+		if((k := parsekey(e)) != nil)
+			return (ref Toplev.K(k), nil);
+		return (nil, "bad public-key syntax");
+	"sequence" =>
+		if((els := parseseq(e)) != nil)
+			return (ref Toplev.Seq(els), nil);
+		return (nil, "bad sequence syntax");
+	* =>
+		return (nil, sys->sprint("unknown operation: %#q", e.op()));
+	}
+}
+
+parseseq(e: ref Sexp): list of ref Seqel
+{
+	l := mustbe(e, "sequence");
+	if(l == nil)
+		return nil;
+	rl: list of ref Seqel;
+	for(; l != nil; l = tl l){
+		se := hd l;
+		case se.op() {
+		"cert" =>
+			cert := parsecert(se);
+			if(cert == nil)
+				return nil;
+			rl = ref Seqel.C(cert) :: rl;
+		"do" =>
+			el := se.args();
+			if(el == nil)
+				return nil;
+			op := (hd el).astext();
+			if(op == nil)
+				return nil;
+			rl = ref Seqel.O(op, tl el) :: rl;
+		"public-key" =>
+			k := parsekey(se);
+			if(k == nil)
+				return nil;
+			rl = ref Seqel.K(k) :: rl;
+		"signature" =>
+			sig := parsesig(se);
+			if(sig == nil)
+				return nil;
+			rl = ref Seqel.S(sig) :: rl;
+		* =>
+			rl = ref Seqel.E(se) :: rl;
+		}
+	}
+	return rev(rl);
+}
+
+parsecert(e: ref Sexp): ref Cert
+{
+	# "(" "cert" <version>? <cert-display>? <issuer> <issuer-loc>? <subject> <subject-loc>?
+	#	<deleg>? <tag> <valid>? <comment>? ")"
+	# elements can appear in any order in a top-level item, though the one above is conventional
+	# the original s-expression is also retained for later use by the caller, for instance in signature verification
+
+	l := mustbe(e, "cert");
+	if(l == nil)
+		return nil;
+	delegate := 0;
+	issuer: ref Name;
+	subj: ref Subject;
+	tag: ref Sexp;
+	valid: ref Valid;
+	for(; l != nil; l = tl l){
+		t := (hd l).op();
+		case t {
+		"version" or "display" or "issuer-info" or "subject-info" or "comment" =>
+			;	# skip
+		"issuer" =>
+			# <principal> | <name> [via issuer-name]
+			if(issuer != nil)
+				return nil;
+			ie := onlyarg(hd l);
+			if(ie == nil)
+				return nil;
+			issuer = parsecompound(ie);
+			if(issuer == nil)
+				return nil;
+		"subject" =>
+			#    <subject>:: "(" "subject" <subj-obj> ")" ;
+			if(subj != nil)
+				return nil;
+			se := onlyarg(hd l);
+			if(se == nil)
+				return nil;
+			subj = parsesubjobj(se);
+			if(subj == nil)
+				return nil;
+		"propagate" =>
+			if(delegate)
+				return nil;
+			delegate = 1;
+		"tag" =>
+			if(tag != nil)
+				return nil;
+			tag = maketag(hd l);	# can safely leave (tag ...) operation in place
+		"valid" =>
+			if(valid != nil)
+				return nil;
+			valid = parsevalid(hd l);
+			if(valid == nil)
+				return nil;
+		* =>
+			sys->print("cert component: %q unknown/ignored\n", t);
+		}
+	}
+	if(issuer == nil || subj == nil)
+		return nil;
+	pick s := subj {
+	KH =>
+		return ref Cert.KH(e, issuer, subj, valid, delegate, tag);
+	O =>
+		return ref Cert.O(e, issuer, subj, valid, delegate, tag);
+	* =>
+		if(issuer.isprincipal())
+			return ref Cert.A(e, issuer, subj, valid, delegate, tag);
+		return ref Cert.N(e, issuer, subj, valid);
+	}
+}
+
+parsesubjobj(e: ref Sexp): ref Subject
+{
+	#  <subj-obj>:: <principal> | <name> | <obj-hash> | <keyholder> | <subj-thresh> ;
+	case e.op() {
+	"name" or "hash" or "public-key" =>
+		name := parsecompound(e);
+		if(name == nil)
+			return nil;
+		if(name.names == nil)
+			return ref Subject.P(name.principal);
+		return ref Subject.N(name);
+
+	"object-hash" =>
+		e = onlyarg(e);
+		if(e == nil)
+			return nil;
+		hash := parsehash(e);
+		if(hash == nil)
+			return nil;
+		return ref Subject.O(hash);
+
+	"keyholder" =>
+		e = onlyarg(e);
+		if(e == nil)
+			return nil;
+		name := parsecompound(e);
+		if(name == nil)
+			return nil;
+		return ref Subject.KH(name);
+
+	"k-of-n" =>
+		el := e.args();
+		m := len el;
+		if(m < 2)
+			return nil;
+		k := intof(hd el);
+		n := intof(hd tl el);
+		if(k < 0 || n < 0 || k > n || n != m-2)
+			return nil;
+		el = tl tl el;
+		sl: list of ref Subject;
+		for(; el != nil; el = tl el){
+			o := parsesubjobj(hd el);
+			if(o == nil)
+				return nil;
+			sl = o :: sl;
+		}
+		return ref Subject.T(k, n, rev(sl));
+
+	* =>
+		return nil;
+	}
+}
+
+parsesig(e: ref Sexp): ref Signature
+{
+	# <signature>:: "("  "signature" <hash> <principal> <sig-val> ")"
+	# <sig-val>:: "(" <pub-sig-alg-id> <sig-params> ")"
+	# <pub-sig-alg-id>:: "rsa-pkcs1-md5" | "rsa-pkcs1-sha1" | "rsa-pkcs1" | "dsa-sha1" | <uri>
+	# <sig-params>:: <byte-string> | <s-expr>+
+
+	l := mustbe(e, "signature");
+	if(len l < 3)
+		return nil;
+	# signature hash key sig
+	hash := parsehash(hd l);
+	k := parseprincipal(hd tl l);
+	if(hash == nil || k == nil)
+		return nil;
+	val := hd tl tl l;
+	if(!val.islist()){	# not in grammar but examples paper uses it
+		sigalg: string;
+		if(k != nil)
+			sigalg = k.sigalg();
+		return ref Signature(hash, k, sigalg, (nil, val.asdata()) :: nil);
+	}
+	sigalg := val.op();
+	if(sigalg == nil)
+		return nil;
+	rl: list of (string, array of byte);
+	for(els := val.args(); els != nil; els = tl els){
+		g := hd els;
+		if(g.islist()){
+			arg := onlyarg(g);
+			if(arg == nil)
+				return nil;
+			rl = (g.op(), arg.asdata()) :: rl;
+		}else
+			rl = (nil, g.asdata()) :: rl;
+	}
+	return ref Signature(hash, k, sigalg, revt(rl));
+}
+
+parsecompound(e: ref Sexp): ref Name
+{
+	if(e == nil)
+		return nil;
+	case e.op() {
+	"name" =>
+		return parsename(e);
+	"public-key" or "hash" =>
+		k := parseprincipal(e);
+		if(k == nil)
+			return nil;
+		return ref Name(k, nil);
+	* =>
+		return nil;
+	}
+}
+
+parsename(e: ref Sexp): ref Name
+{
+	l := mustbe(e, "name");
+	if(l == nil)
+		return nil;
+	k: ref Key;
+	if((hd l).islist()){	# must be principal: pub key or hash of key
+		k = parseprincipal(hd l);
+		if(k == nil)
+			return nil;
+		l = tl l;
+	}
+	names: list of string;
+	for(; l != nil; l = tl l){
+		s := (hd l).astext();
+		if(s == nil)
+			return nil;
+		names = s :: names;
+	}
+	return ref Name(k, rev(names));
+}
+
+parseprincipal(e: ref Sexp): ref Key
+{
+	case e.op() {
+	"public-key" or "private-key" =>
+		return parsekey(e);
+	"hash" =>
+		hash := parsehash(e);
+		if(hash == nil)
+			return nil;
+		return ref Key(nil, nil, 0, nil, nil, hash::nil);
+	* =>
+		return nil;
+	}
+}
+
+parsekey(e: ref Sexp): ref Key
+{
+	issk := 0;
+	l := mustbe(e, "public-key");
+	if(l == nil){
+		l = mustbe(e, "private-key");
+		if(l == nil)
+			return nil;
+		issk = 1;
+	}
+	kind := (hd l).op();
+	(nf, fld) := sys->tokenize(kind, "-");
+	if(nf < 1)
+		return nil;
+	alg := hd fld;
+	if(nf > 1)
+		enc := hd tl fld;		# signature hash encoding
+	mha := "sha1";
+	if(nf > 2)
+		mha = hd tl tl fld;	# signature hash algorithm
+	kl := (hd l).args();
+	if(kl == nil)
+		return nil;
+	els: list of (string, ref IPint);
+	for(; kl != nil; kl = tl kl){
+		t := (hd kl).op();
+		a := onlyarg(hd kl).asdata();
+		if(a == nil)
+			return nil;
+		ip := IPint.bebytestoip(a);
+		if(ip == nil)
+			return nil;
+		els = (t, ip) :: els;
+	}
+	krp := ref Keyrep.PK(alg, "sdsi", els);
+	(pk, nbits) := krp.mkpk();
+	if(pk == nil){
+		sys->print("can't convert public-key\n");
+		return nil;
+	}
+	sk: ref Keyring->SK;
+	if(issk){
+		krp = ref Keyrep.SK(alg, "sdsi", els);
+		sk = krp.mksk();
+		if(sk == nil){
+			sys->print("can't convert private-key\n");
+			return nil;
+		}
+	}
+#(ref Key(pk,nil,"md5",nil,nil)).hashed("md5");		# TEST
+	return ref Key(pk, sk, nbits, mha, enc, nil);
+}
+
+parsehash(e: ref Sexp): ref Hash
+{
+	# "(" "hash" <hash-alg-name> <hash-value> <uris>? ")"
+	l := mustbe(e, "hash");
+	if(len l < 2)
+		return nil;
+	return ref Hash((hd l).astext(), (hd tl l).asdata());
+}
+
+parsevalid(e: ref Sexp): ref Valid
+{
+	l := mustbe(e, "valid");
+	if(l == nil)
+		return nil;
+	el: list of ref Sexp;
+	notbefore, notafter: string;
+	(el, l) = isita(l, "not-before");
+	if(el != nil && (notafter = ckdate((hd el).astext())) == nil)
+		return nil;
+	(el, l) = isita(l, "not-after");
+	if(el != nil && (notafter = ckdate((hd el).astext())) == nil)
+		return nil;
+	for(;;){
+		(el, l) = isita(l, "online");
+		if(el == nil)
+			break;
+	}
+	if(el != nil)
+		return nil;
+	return ref Valid(notbefore, notafter);
+}
+
+isnumeric(s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(!(s[i]>='0' && s[i]<='9'))
+			return 0;
+	return s != nil;
+}
+
+ckdate(s: string): string
+{
+	if(date2epoch(s) < 0)	# TO DO: prefix/suffix tests
+		return nil;
+	return s;
+}
+
+Toplev.sexp(top: self ref Toplev): ref Sexp
+{
+	pick t := top {
+	C =>
+		return t.v.sexp();
+	Sig =>
+		return t.v.sexp();
+	K =>
+		return t.v.sexp();
+	Seq =>
+		rels := rev(t.v);
+		els: list of ref Sexp;
+		for(; rels != nil; rels = tl rels)
+			els = (hd rels).sexp() :: els;
+		return ref Sexp.List(ref Sexp.String("sequence", nil) :: els);
+	* =>
+		raise "unexpected spki type";
+	}
+}
+
+Toplev.text(top: self ref Toplev): string
+{
+	return top.sexp().text();
+}
+
+Seqel.sexp(se: self ref Seqel): ref Sexp
+{
+	pick r := se {
+	C =>
+		return r.c.sexp();
+	K =>
+		return r.k.sexp();
+	O =>
+		return ref Sexp.List(ref Sexp.String("do",nil) :: ref Sexp.String(r.op,nil) :: r.args);
+	S =>
+		return r.sig.sexp();
+	E =>
+		return r.exp;
+	* =>
+		raise "unsupported value";
+	}
+}
+
+Seqel.text(se: self ref Seqel): string
+{
+	pick r := se {
+	C =>
+		return r.c.text();
+	K =>
+		return r.k.text();
+	O =>
+		return se.sexp().text();
+	S =>
+		return r.sig.text();
+	E =>
+		return r.exp.text();
+	* =>
+		raise "unsupported value";
+	}
+}
+
+isita(l: list of ref Sexp, s: string): (list of ref Sexp, list of ref Sexp)
+{
+	if(l == nil)
+		return (nil, nil);
+	e := hd l;
+	if(e.islist() && e.op() == s)
+		return (e.args(), tl l);
+	return (nil, l);
+}
+
+intof(e: ref Sexp): int
+{
+	# int should be plenty; don't need big
+	pick s := e {
+	List =>
+		return -1;
+	Binary =>
+		if(len s.data > 4)
+			return -1;
+		v := 0;
+		for(i := 0; i < len s.data; i++)
+			v = (v<<8) | int s.data[i];
+		return v;
+	String =>
+		if(s.s == nil || !(s.s[0]>='0' && s.s[0]<='9'))
+			return -1;
+		return int s.s;
+	}
+}
+
+onlyarg(e: ref Sexp): ref Sexp
+{
+	l := e.args();
+	if(l == nil || tl l != nil)
+		return nil;
+	return hd l;
+}
+
+mustbe(e: ref Sexp, kind: string): list of ref Sexp
+{
+	if(e != nil && e.islist() && e.op() == kind)
+		return e.args();
+	return nil;
+}
+
+checksig(c: ref Cert, sig: ref Signature): string
+{
+	if(c.e == nil)
+		return "missing S-expression for certificate";
+	if(sig.key == nil)
+		return "missing key for signature";
+	if(sig.hash == nil)
+		return "missing hash for signature";
+	if(sig.sig == nil)
+		return "missing signature value";
+	pk := sig.key.pk;
+	if(pk == nil)
+		return "missing Keyring->PK for signature";	# TO DO (need a way to tell that key was just a hash)
+#rsacomp((hd sig.sig).t1, sig.key);
+#sys->print("nbits= %d\n", sig.key.nbits);
+	(alg, enc, hashalg) := sig.algs();
+	if(alg == nil)
+		return "unspecified signature algorithm";
+	if(hashalg == nil)
+		hashalg = "md5";	# TO DO?
+	hash := hashbytes(c.e.pack(), hashalg);
+	if(hash == nil)
+		return "unknown hash algorithm "+hashalg;
+	if(enc == nil)
+		h := hash;
+	else if(enc == "pkcs" || enc == "pkcs1")
+		h = pkcs1_encode(hashalg, hash, (sig.key.nbits+7)/8);
+	else
+		return "unknown encoding algorithm "+enc;
+#dump("check/hashed", hash);
+#dump("check/h", h);
+	ip := IPint.bebytestoip(h);
+	isig := sig2icert(sig, "sdsi", 0);
+	if(isig == nil)
+		return "couldn't convert SPKI signature to Keyring form";
+	if(!kr->verifym(pk, isig, ip))
+		return "signature does not match";
+	return nil;
+}
+
+signcert(c: ref Cert, sigalg: string, key: ref Key): (ref Signature, string)
+{
+	if(c.e == nil){
+		c.e = c.sexp();
+		if(c.e == nil)
+			return (nil, "bad input certificate");
+	}
+	return signbytes(c.e.pack(), sigalg, key);
+}
+
+#
+# might be useful to have a separate `signhash' for cases where the data was hashed elsewhere
+#
+signbytes(data: array of byte, sigalg: string, key: ref Key): (ref Signature, string)
+{
+	if(key.sk == nil)
+		return (nil, "missing Keyring->SK for signature");
+	pubkey := ref *key;
+	pubkey.sk = nil;
+	sig := ref Signature(nil, pubkey, sigalg, nil);	# ref Hash, key, alg, sig: list of (string, array of byte)
+	(alg, enc, hashalg) := sigalgs(sigalg);
+	if(alg == nil)
+		return (nil, "unspecified signature algorithm");
+	if(hashalg == nil)
+		hashalg = "md5";	# TO DO?
+	hash := hashbytes(data, hashalg);
+	if(hash == nil)
+		return (nil, "unknown hash algorithm "+hashalg);
+	if(enc == nil)
+		h := hash;
+	else if(enc == "pkcs" || enc == "pkcs1")
+		h = pkcs1_encode(hashalg, hash, (sig.key.nbits+7)/8);
+	else
+		return (nil, "unknown encoding algorithm "+enc);
+#dump("sign/hashed", hash);
+#dump("sign/h", h);
+	sig.hash = ref Hash(hashalg, hash);
+	ip := IPint.bebytestoip(h);
+	icert := kr->signm(key.sk, ip, hashalg);
+	if(icert == nil)
+		return (nil, "signature failed");	# can't happen?
+	(nil, nil, nil, vals) := icert2els(icert);
+	if(vals == nil)
+		return (nil, "couldn't extract values from Keyring Certificate");
+	l: list of (string, array of byte);
+	for(; vals != nil; vals = tl vals){
+		(n, v) := hd vals;
+		l = (f2s("rsa", n), v) :: l;
+	}
+	sig.sig = revt(l);
+	return (sig, nil);
+}
+
+hashexp(e: ref Sexp, alg: string): array of byte
+{
+	return hashbytes(e.pack(), alg);
+}
+
+hashbytes(a: array of byte, alg: string): array of byte
+{
+	hash: array of byte;
+	case alg {
+	"md5" =>
+		hash = array[Keyring->MD5dlen] of byte;
+		kr->md5(a, len a, hash, nil);
+	"sha" or "sha1" =>
+		hash = array[Keyring->SHA1dlen] of byte;
+		kr->sha1(a, len a, hash, nil);
+	* =>
+		raise "Spki->hashbytes: unknown algorithm: "+alg;
+	}
+	return hash;
+}
+
+# trim mpint and add leading zero byte if needed to ensure value is unsigned
+pre0(a: array of byte): array of byte
+{
+	for(i:=0; i<len a-1; i++)
+		if(a[i] != a[i+1] && (a[i] != byte 0 || (int a[i+1] & 16r80) != 0))
+			break;
+	if(i > 0)
+		a = a[i:];
+	if(len a < 1 || (int a[0] & 16r80) == 0)
+		return a;
+	b := array[len a + 1] of byte;
+	b[0] = byte 0;
+	b[1:] = a;
+	return b;
+}
+
+dump(s: string, a: array of byte)
+{
+	s = sys->sprint("%s [%d]: ", s, len a);
+	for(i := 0; i < len a; i++)
+		s += sys->sprint(" %.2ux", int a[i]);
+	sys->print("%s\n", s);
+}
+
+Signature.algs(sg: self ref Signature): (string, string, string)
+{
+	return sigalgs(sg.sa);
+}
+
+# sig[-[enc-]hash]
+sigalgs(alg: string): (string, string, string)
+{
+	(nf, flds) := sys->tokenize(alg, "-");
+	if(nf >= 3)
+		return (hd flds, hd tl flds, hd tl tl flds);
+	if(nf >= 2)
+		return (hd flds, nil, hd tl flds);
+	if(nf >= 1)
+		return (hd flds, nil, nil);
+	return (nil, nil, nil);
+}
+
+Signature.sexp(sg: self ref Signature): ref Sexp
+{
+	sv: ref Sexp;
+	if(len sg.sig != 1){
+		l: list of ref Sexp;
+		for(els := sg.sig; els != nil; els = tl els){
+			(op, val) := hd els;
+			if(op != nil)
+				l = ref Sexp.List(ref Sexp.String(op,nil) :: ref Sexp.Binary(val,nil) :: nil) :: l;
+			else
+				l =  ref Sexp.Binary(val,nil) :: l;
+		}
+		sv = ref Sexp.List(rev(l));
+	}else
+		sv = ref Sexp.Binary((hd sg.sig).t1, nil);	# no list if signature has one component
+	if(sg.sa != nil)
+		sv = ref Sexp.List(ref Sexp.String(sg.sa,nil) :: sv :: nil);
+	return ref Sexp.List(ref Sexp.String("signature",nil) :: sg.hash.sexp() :: sg.key.sexp() ::
+		sv :: nil);
+}
+
+Signature.text(sg: self ref Signature): string
+{
+	if(sg == nil)
+		return nil;
+	return sg.sexp().text();
+}
+
+Hash.sexp(h: self ref Hash): ref Sexp
+{
+	return ref Sexp.List(ref Sexp.String("hash",nil) ::
+		ref Sexp.String(h.alg, nil) :: ref Sexp.Binary(h.hash,nil) :: nil);
+}
+
+Hash.text(h: self ref Hash): string
+{
+	return h.sexp().text();
+}
+
+Hash.eq(h1: self ref Hash, h2: ref Hash): int
+{
+	if(h1 == h2)
+		return 1;
+	if(h1 == nil || h2 == nil || h1.alg != h2.alg)
+		return 0;
+	return cmpbytes(h1.hash, h2.hash) == 0;
+}
+
+Valid.intersect(a: self Valid, b: Valid): (int, Valid)
+{
+	c: Valid;
+	if(a.notbefore < b.notbefore)
+		c.notbefore = b.notbefore;
+	else
+		c.notbefore = a.notbefore;
+	if(a.notafter == nil)
+		c.notafter = b.notafter;
+	else if(b.notafter == nil || a.notafter < b.notafter)
+		c.notafter = a.notafter;
+	else
+		c.notafter = b.notafter;
+	if(c.notbefore > c.notafter)
+		return (0, (nil, nil));
+	return (1, c);
+}
+
+Valid.text(a: self Valid): string
+{
+	na, nb: string;
+	if(a.notbefore != nil)
+		nb = " (not-before \""+a.notbefore+"\")";
+	if(a.notafter != nil)
+		na = " (not-after \""+a.notafter+"\")";
+	return sys->sprint("(valid%s%s)", nb, na);
+}
+
+Valid.sexp(a: self Valid): ref Sexp
+{
+	nb, na: ref Sexp;
+	if(a.notbefore != nil)
+		nb = ref Sexp.List(ref Sexp.String("not-before",nil) :: ref Sexp.String(a.notbefore,nil) :: nil);
+	if(a.notafter != nil)
+		na = ref Sexp.List(ref Sexp.String("not-after",nil) :: ref Sexp.String(a.notafter,nil) :: nil);
+	if(nb == nil && na == nil)
+		return nil;
+	return ref Sexp.List(ref Sexp.String("valid",nil) :: nb :: na :: nil);
+}
+
+Cert.text(c: self ref Cert): string
+{
+	if(c == nil)
+		return "nil";
+	v: string;
+	pick d := c {
+	A or KH or O =>
+		if(d.tag != nil)
+			v += " "+d.tag.text();
+	}
+	if(c.valid != nil)
+		v += " "+(*c.valid).text();
+	return sys->sprint("(cert (issuer %s) (subject %s)%s)", c.issuer.text(), c.subject.text(), v);
+}
+
+Cert.sexp(c: self ref Cert): ref Sexp
+{
+	if(c == nil)
+		return nil;
+	if(c.e != nil)
+		return c.e;
+	ds, tag: ref Sexp;
+	pick d := c {
+	N =>
+	A or KH or O =>
+		if(d.delegate)
+			ds = ref Sexp.List(ref Sexp.String("propagate",nil) :: nil);
+		tag = d.tag;
+	}
+	if(c.valid != nil)
+		vs := (*c.valid).sexp();
+	s := ref Sexp.List(ref Sexp.String("cert",nil) ::
+		ref Sexp.List(ref Sexp.String("issuer",nil) :: c.issuer.sexp() :: nil) ::
+		c.subject.sexp() ::
+		ds ::
+		tag ::
+		vs ::
+		nil);
+	return s;
+}
+
+Subject.principal(s: self ref Subject): ref Key
+{
+	pick r := s {
+	P =>
+		return r.key;
+	N =>
+		return r.name.principal;
+	KH =>
+		return r.holder.principal;
+	O =>
+		return nil;	# TO DO: need cache of hashed keys
+	* =>
+		return nil;	# TO DO? (no particular principal for threshold)
+	}
+}
+
+Subject.text(s: self ref Subject): string
+{
+	pick r := s {
+	P =>
+		return r.key.text();
+	N =>
+		return r.name.text();
+	KH =>
+		return sys->sprint("(keyholder %s)", r.holder.text());
+	O =>
+		return sys->sprint("(object-hash %s)", r.hash.text());
+	T =>
+		return s.sexp().text();	# easy way out
+	}
+}
+
+Subject.sexp(s: self ref Subject): ref Sexp
+{
+	e: ref Sexp;
+	pick r := s {
+	P =>
+		e = r.key.sexp();
+	N =>
+		e = r.name.sexp();
+	KH =>
+		e = ref Sexp.List(ref Sexp.String("keyholder",nil) :: r.holder.sexp() :: nil);
+	O =>
+		e = ref Sexp.List(ref Sexp.String("object-hash",nil) :: r.hash.sexp() :: nil);
+	T =>
+		sl: list of ref Sexp;
+		for(subs := r.subs; subs != nil; subs = tl subs)
+			sl = (hd subs).sexp() :: sl;
+		e = ref Sexp.List(ref Sexp.String("k-of-n",nil) ::
+			ref Sexp.String(string r.k,nil) :: ref Sexp.String(string r.n,nil) :: rev(sl));
+	* =>
+		return nil;
+	}
+	return ref Sexp.List(ref Sexp.String("subject",nil) :: e :: nil);
+}
+
+Subject.eq(s1: self ref Subject, s2: ref Subject): int
+{
+	if(s1 == s2)
+		return 1;
+	if(s1 == nil || s2 == nil || tagof s1 != tagof s2)
+		return 0;
+	pick r1 := s1 {
+	P =>
+		pick r2 := s2 {
+		P =>
+			return r1.key.eq(r2.key);
+		}
+	N =>
+		pick r2 := s2 {
+		N =>
+			return r1.name.eq(r2.name);
+		}
+	O =>
+		pick r2 := s2 {
+		O =>
+			return r1.hash.eq(r2.hash);
+		}
+	KH =>
+		pick r2 := s2 {
+		KH =>
+			return r1.holder.eq(r2.holder);
+		}
+	T =>
+		pick r2 := s2 {
+		T =>
+			if(r1.k != r2.k || r1.n != r2.n)
+				return 0;
+			l2 := r2.subs;
+			for(l1 := r1.subs; l1 != nil; l1 = tl l1){
+				if(l2 == nil || !(hd l1).eq(hd l2))
+					return 0;
+				l2 = tl l2;
+			}
+		}
+	}
+	return 0;
+}
+
+Name.isprincipal(n: self ref Name): int
+{
+	return n.names == nil;
+}
+
+Name.local(n: self ref Name): ref Name
+{
+	if(n.names == nil || tl n.names == nil)
+		return n;
+	return ref Name(n.principal, hd n.names :: nil);
+}
+
+Name.islocal(n: self ref Name): int
+{
+	return n.names == nil || tl n.names == nil;
+}
+
+Name.isprefix(n1: self ref Name, n2: ref Name): int
+{
+	if(n1 == nil)
+		return n2 == nil;
+	if(!n1.principal.eq(n2.principal))
+		return 0;
+	s1 := n1.names;
+	s2 := n2.names;
+	for(; s1 != nil; s1 = tl s1){
+		if(s2 == nil || hd s2 != hd s1)
+			return 0;
+		s2 = tl s2;
+	}
+	return 1;
+}
+
+Name.text(n: self ref Name): string
+{
+	if(n.principal == nil)
+		s := "$self";
+	else
+		s = n.principal.text();
+	for(nl := n.names; nl != nil; nl = tl nl)
+		s += " " + hd nl;
+	return "(name "+s+")";
+}
+
+Name.sexp(n: self ref Name): ref Sexp
+{
+	ns: list of ref Sexp;
+
+	if(n.principal != nil)
+		is := n.principal.sexp();
+	else
+		is = ref Sexp.String("$self",nil);
+	if(n.names == nil)
+		return is;
+	for(nl := n.names; nl != nil; nl = tl nl)
+		ns = ref Sexp.String(hd nl,nil) :: ns;
+	return ref Sexp.List(ref Sexp.String("name",nil) :: is :: rev(ns));
+}
+
+Name.eq(a: self ref Name, b: ref Name): int
+{
+	if(a == b)
+		return 1;
+	if(a == nil || b == nil)
+		return 0;
+	if(!a.principal.eq(b.principal))
+		return 0;
+	nb := b.names;
+	for(na := a.names; na != nil; na = tl na){
+		if(nb == nil || hd nb != hd na)
+			return 0;
+		nb = tl nb;
+	}
+	return nb == nil;
+}
+
+Key.public(key: self ref Key): ref Key
+{
+	if(key.sk != nil){
+		pk := ref *key;
+		if(pk.pk == nil)
+			pk.pk = kr->sktopk(pk.sk);
+		pk.sk = nil;
+		return pk;
+	}
+	if(key.pk == nil)
+		return nil;
+	return key;
+}
+
+Key.ishash(k: self ref Key): int
+{
+	return k.hash != nil && k.sk == nil && k.pk == nil;
+}
+
+Key.hashed(key: self ref Key, alg: string): array of byte
+{
+	e := key.sexp();
+	if(e == nil)
+		return nil;
+	return hashexp(key.sexp(), alg);
+}
+
+Key.hashexp(key: self ref Key, alg: string): ref Hash
+{
+	if(key.hash != nil){
+		for(l := key.hash; l != nil; l = tl l){
+			h := hd l;
+			if(h.alg == alg && h.hash != nil)
+				return h;
+		}
+	}
+	hash := key.hashed(alg);
+	if(hash == nil)
+		return nil;
+	h := ref Hash(alg, hash);
+	key.hash = h :: key.hash;
+	return h;
+}
+
+Key.sigalg(k: self ref Key): string
+{
+	if(k.pk != nil)
+		alg := k.pk.sa.name;
+	else if(k.sk != nil)
+		alg = k.sk.sa.name;
+	else
+		return nil;
+	if(k.halg != nil){
+		if(k.henc != nil)
+			alg += "-"+k.henc;
+		alg += "-"+k.halg;
+	}
+	return alg;
+}
+
+Key.text(k: self ref Key): string
+{
+	e := k.sexp();
+	if(e == nil)
+		return sys->sprint("(public-key unknown)");
+	return e.text();
+}
+
+Key.sexp(k: self ref Key): ref Sexp
+{
+	if(k.sk == nil && k.pk == nil){
+		if(k.hash != nil)
+			return (hd k.hash).sexp();
+		return nil;
+	}
+	sort := "public-key";
+	els: list of (string, ref IPint);
+	if(k.sk != nil){
+		krp := Keyrep.sk(k.sk);
+		if(krp == nil)
+			return nil;
+		els = krp.els;
+		sort = "private-key";
+	}else{
+		krp := Keyrep.pk(k.pk);
+		if(krp == nil)
+			return nil;
+		els = krp.els;
+	}
+	rl: list of ref Sexp;
+	for(; els != nil; els = tl els){
+		(n, v) := hd els;
+		a := pre0(v.iptobebytes());
+		rl = ref Sexp.List(ref Sexp.String(f2s("rsa", n),nil) :: ref Sexp.Binary(a,nil) :: nil) :: rl;
+	}
+	return ref Sexp.List(ref Sexp.String(sort, nil) ::
+		ref Sexp.List(ref Sexp.String(k.sigalg(),nil) :: rev(rl)) :: nil);
+}
+
+Key.eq(k1: self ref Key, k2: ref Key): int
+{
+	if(k1 == k2)
+		return 1;
+	if(k1 == nil || k2 == nil)
+		return 0;
+	for(hl1 := k1.hash; hl1 != nil; hl1 = tl hl1){
+		h1 := hd hl1;
+		for(hl2 := k2.hash; hl2 != nil; hl2 = tl hl2){
+			h2 := hd hl2;
+			if(h1.hash != nil && h1.eq(h2))
+				return 1;
+		}
+	}
+	if(k1.pk != nil && k2.pk != nil)
+		return kr->pktostr(k1.pk) == kr->pktostr(k2.pk);	# TO DO
+	return 0;
+}
+
+dec(s: string, i: int, l: int): (int, int)
+{
+	l += i;
+	n := 0;
+	for(; i < l; i++){
+		c := s[i];
+		if(!(c >= '0' && c <= '9'))
+			return (-1, 0);
+		n = n*10 + (c-'0');
+	}
+	return (n, l);
+}
+
+# accepts at least any valid prefix of a date
+date2epoch(t: string): int
+{
+	# yyyy-mm-dd_hh:mm:ss
+	if(len t >= 4 && len t < 19)
+		t += "-01-01_00:00:00"[len t-4:];	# extend non-standard short forms
+	else if(len t != 19)
+		return -1;
+	tm := ref Daytime->Tm;
+	i: int;
+	(tm.year, i) = dec(t, 0, 4);
+	if(tm.year < 0 || t[i++] != '-')
+		return -1;
+	tm.year -= 1900;
+	(tm.mon, i) = dec(t, i, 2);
+	if(tm.mon <= 0 || t[i++] != '-' || tm.mon > 12)
+		return -1;
+	tm.mon--;
+	(tm.mday, i) = dec(t, i, 2);
+	if(tm.mday <= 0 || t[i++] != '_' || tm.mday >= 31)
+		return -1;
+	(tm.hour, i) = dec(t, i, 2);
+	if(tm.hour < 0 || t[i++] != ':' || tm.hour > 23)
+		return -1;
+	(tm.min, i) = dec(t, i, 2);
+	if(tm.min < 0 || t[i++] != ':' || tm.min > 59)
+		return -1;
+	(tm.sec, i) = dec(t, i, 2);
+	if(tm.sec < 0 || tm.sec > 59)	# leap second(s)?
+		return -1;
+	tm.tzoff = 0;
+	return daytime->tm2epoch(tm);
+}
+
+epoch2date(t: int): string
+{
+	tm := daytime->gmt(t);
+	return sys->sprint("%.4d-%.2d-%.2d_%.2d:%.2d:%.2d",
+		tm.year+1900, tm.mon+1, tm.mday, tm.hour, tm.min, tm.sec);
+}
+
+# could use a delta-time function
+
+time2secs(s: string): int
+{
+	# HH:MM:SS
+	if(len s >= 2 && len s < 8)
+		s += ":00:00"[len s-2:];	# extend non-standard short forms
+	else if(len s != 8)
+		return -1;
+	hh, mm, ss, i: int;
+	(hh, i) = dec(s, 0, 2);
+	if(hh < 0 || hh > 24 || s[i++] != ':')
+		return -1;
+	(mm, i) = dec(s, i, 2);
+	if(mm < 0 || mm > 59 || s[i++] != ':')
+		return -1;
+	(ss, i) = dec(s, i, 2);
+	if(ss < 0 || ss > 59)
+		return -1;
+	return hh*3600 + mm*60 + ss;
+}
+
+secs2time(t: int): string
+{
+	hh := (t/60*60)%24;
+	mm := (t%3600)/60;
+	ss := t%60;
+	return sys->sprint("%.2d:%.2d:%.2d", hh, mm, ss);
+}
+
+#
+# auth tag intersection as defined by
+#	``A Formal Semantics for SPKI'', Jon Howell, David Kotz
+#		its proof cases are marked by the roman numerals (I) ... (X)
+# with contributions from
+#	``A Note on SPKI's Authorisation Syntax'', Olav Bandmann, Mads Dam
+#		its AIntersect cases are marked by arabic numerals
+
+maketag(e: ref Sexp): ref Sexp
+{
+	if(e == nil)
+		return e;
+	return remake(e.copy());
+}
+
+tagimplies(t1: ref Sexp, t2: ref Sexp): int
+{
+	e := tagintersect(t1, t2);
+	if(e == nil)
+		return 0;
+	return e.eq(t2);
+}
+
+Anull, Astar, Abytes, Aprefix, Asuffix, Arange, Alist, Aset: con iota;
+
+tagindex(s: ref Sexp): int
+{
+	if(s == nil)
+		return Anull;
+	pick r := s {
+	String =>
+		return Abytes;
+	Binary =>
+		return Abytes;
+	List =>
+		if(r.op() == "*"){
+			if(tl r.l == nil)
+				return Astar;
+			case (hd tl r.l).astext() {
+			"prefix" =>	return Aprefix;
+			"suffix" =>	return Asuffix;
+			"range" =>	return Arange;
+			"set" =>	return Aset;
+			* =>	return Anull;	# unknown
+			}
+		}
+		return Alist;
+	* =>
+		return Anull;	# not reached
+	}
+}
+
+#
+# 1	(*) x r = r
+# 2	r x (*) = r
+# 3	⊥ x r = ⊥
+# 4	r x ⊥ = ⊥
+# 5	a x a = a  (also a x a' = ⊥)
+# 6	a x b = a if a ∈ Val(b)
+# 7	a x b = ⊥ if a ∉ Val(b)
+# 8	a x (a' r1 ... rn)) = ⊥
+# 9	a x (* set r1 ... ri = a ... rn) = a
+# 10	a x (* set r1 ... ri = b ... rn) = a, if a ∈ Val(b)
+# 11	a x (* set r1 ... ri ... rn)) = ⊥, if neither of above two cases applies
+# 12	b x b' = b ∩ b'
+# 13	b x (a r1 ... rn) = ⊥
+# 14	b x (* set r1 ... rn) = (*set (b x r'[1]) ... (b x r'[m])), for atomic elements in r1, ..., rn
+# 15	(a r1 ... rn) x (a r'[1] ... r'[n] r'[n+1] ... r'[m]) = (a (r1 x r'[1]) ... (rn x r'[n]) r'[n+1] ... r'[m]) for m >= n
+# 16	(a r1 ... rn) x (a' r'[1] ... r'[m]) = ⊥
+# 17	(a r1 ... rn) x (* set r'[1] ... r'[i] ... r'[k]) = (a r1 ... rn) x r'[i], if r'[i] has tag a
+# 18	(a r1 ... rn) x (* set r'[1] ... r'[m]) = ⊥, if no r'[i] has tag a
+# 19	(* set r1 .. rn) x r, where r is (* set r1'[1] ... r'[m]) = (* set (r1 x r) (r2 x r) ... (rn x r))
+#
+# nil is used instead of ⊥, which works provided an incoming credential
+# with no tag has implicit tag (*)
+#
+
+# put operands in order of proof in FSS
+
+swaptag := array[] of {
+	(Abytes<<4) | Alist =>	(Alist<<4) | Abytes,	# (IV)
+
+	(Abytes<<4) | Aset =>	(Aset<<4) | Abytes,	# (VI)
+	(Aprefix<<4) | Aset =>	(Aset<<4) | Aprefix,	# (VI)
+	(Arange<<4) | Aset =>	(Aset<<4) | Arange,	# (VI)
+	(Alist<<4) | Aset =>	(Aset<<4) | Alist,	# (VI)
+	(Asuffix<<4) | Aset =>	(Aset<<4) | Asuffix,	# (VI)	extension
+
+	(Aprefix<<4) | Abytes =>	(Abytes<<4) | Aprefix,	# (VII)
+	(Arange<<4) | Abytes =>	(Abytes<<4) | Arange,	# (VII)
+	(Asuffix<<4) | Abytes =>	(Abytes<<4) | Asuffix,	# (VII) extension
+
+	* => 0,
+};
+
+tagintersect(t1, t2: ref Sexp): ref Sexp
+{
+	if(t1 == t2)
+		return t1;
+	if(t1 == nil || t2 == nil)	# 3, 4; case (I)
+		return nil;
+	x1 := tagindex(t1);
+	x2 := tagindex(t2);
+	if(debug){
+		sys->print("%#q -> %d\n", t1.text(), x1);
+		sys->print("%#q -> %d\n", t2.text(), x2);
+	}
+	if(x1 == Astar)	# 1; case (II)
+		return t2;
+	if(x2 == Astar)	# 2; case (II)
+		return t1;
+	code := (x1 << 4) | x2;	# (a[x]<<4) | a[y] in FSS
+	# reorder symmetric cases
+	if(code < len swaptag && swaptag[code]){
+		(t1, t2) = (t2, t1);
+		(x1, x2) = (x2, x1);
+		code = swaptag[code];
+	}
+	case code {
+	(Abytes<<4) | Abytes =>	# case (III); 5
+		if(t1.eq(t2))
+			return t1;
+
+	(Alist<<4) | Abytes =>	# case (IV)
+		return nil;
+
+	(Alist<<4) | Alist =>	# case (V); 15-16
+		if(t1.op() != t2.op())
+			return nil;
+		l1 := t1.els();
+		l2 := t2.els();
+		if(len l1 > len l2){
+			(t1, t2) = (t2, t1);
+			(l1, l2) = (l2, l1);
+		}
+		rl: list of ref Sexp;
+		for(; l1 != nil; l1 = tl l1){
+			x := tagintersect(hd l1, hd l2);
+			if(x == nil)
+				return nil;
+			rl = x :: rl;
+			l2 = tl l2;
+		}
+		for(; l2 != nil; l2 = tl l2)
+			rl = hd l2 :: rl;
+		return ref Sexp.List(rev(rl));
+
+	(Aset<<4) | Abytes =>	# case (VI); 9-11
+		for(el := setof(t1); el != nil; el = tl el){
+			e := hd el;
+			case tagindex(e) {
+			Abytes =>
+				if(e.eq(t2))
+					return t2;
+			Astar =>
+				return t2;
+			Arange =>
+				if(inrange(t2, e))
+					return t2;
+			Aprefix =>
+				if(isprefix(e, t2))
+					return t2;
+			Asuffix =>
+				if(issuffix(e, t2))
+					return t2;
+			}
+		}
+		# otherwise null
+
+	(Aset<<4) | Alist =>	# case (VI); 17-18
+		o := t2.op();
+		for(el := setof(t1); el != nil; el = tl el){
+			e := hd el;
+			if(e.islist() && e.op() == o || tagindex(e) == Astar)
+				return tagintersect(e, t2);
+		}
+		# otherwise null
+
+	(Aset<<4) | Aprefix or	# case (VI); 14
+	(Aset<<4) | Arange or	# case (VI); 14
+		# for Aprefix or Arange, could restrict els of t1 to atomic elements (sets A and B)
+		# here, following rule 14, but we'll let tagintersect sort it out in the general case below
+	(Aset<<4) | Aset =>	# case (VI); 19
+		rl: list of ref Sexp;
+		for(el := setof(t1); el != nil; el = tl el){
+			x := tagintersect(hd el, t2);
+			if(x != nil)
+				rl = x :: rl;
+		}
+		return mkset(rev(rl));	# null if empty
+
+	(Abytes<<4) | Aprefix =>	# case (VII)
+		if(isprefix(t2, t1))
+			return t1;
+	(Abytes<<4) | Arange =>	# case (VII)
+		if(inrange(t1, t2))
+			return t1;
+	(Abytes<<4) | Asuffix =>	# case (VII)
+		if(issuffix(t2, t1))
+			return t1;
+				
+	(Aprefix<<4) | Aprefix =>	# case (VIII)
+		p1 := prefixof(t1);
+		p2 := prefixof(t2);
+		if(p1 == nil || p2 == nil)
+			return nil;
+		if(p1.nb < p2.nb){
+			(t1, t2) = (t2, t1);
+			(p1, p2) = (p2, p1);
+		}
+		if((*p2).isprefix(*p1))
+			return t1;	# t1 is longer, thus more specific
+				
+	(Asuffix<<4) | Asuffix =>	# case (VIII)	extension
+		p1 := suffixof(t1);
+		p2 := suffixof(t2);
+		if(p1 == nil || p2 == nil)
+			return nil;
+		if(p1.nb < p2.nb){
+			(t1, t2) = (t2, t1);
+			(p1, p2) = (p2, p1);
+		}
+		if((*p2).issuffix(*p1))
+			return t1;	# t1 is longer, thus more specific
+
+	(Arange<<4) | Aprefix =>	# case (IX)
+		return nil;
+	(Arange<<4) | Asuffix =>	# case (IX)
+		return nil;
+	(Arange<<4) | Arange =>	# case (IX)
+		v1 := rangeof(t1);
+		v2 := rangeof(t2);
+		if(v1 == nil || v2 == nil)
+			return nil;	# invalid
+		(ok, v) := (*v1).intersect(*v2);
+		if(ok)
+			return mkrange(v);
+
+	(Alist<<4) | Arange or
+	(Alist<<4) | Aprefix =>	# case (X)
+		;
+	}
+	return nil;	# case (X), and default
+}
+
+isprefix(pat, subj: ref Sexp): int
+{
+	p := prefixof(pat);
+	if(p == nil)
+		return 0;
+	return (*p).isprefix(valof(subj));
+}
+
+issuffix(pat, subj: ref Sexp): int
+{
+	p := suffixof(pat);
+	if(p == nil)
+		return 0;
+	return (*p).issuffix(valof(subj));
+}
+
+inrange(t1, t2: ref Sexp): int
+{
+	v := valof(t1);
+	r := rangeof(t2);
+	if(r == nil)
+		return 0;
+	if(0)
+		sys->print("%s :: %s\n", v.text(), (*r).text());
+	pass := 0;
+	if(r.ge >= 0){
+		c := v.cmp(r.lb, r.order);
+		if(c < 0 || c == 0 && !r.ge)
+			return 0;
+		pass = 1;
+	}
+	if(r.le >= 0){
+		c := v.cmp(r.ub, r.order);
+		if(c > 0 || c == 0 && !r.le)
+			return 0;
+		pass = 1;
+	}
+	return pass;
+}
+
+addval(l: list of ref Sexp, s: string, v: Val): list of ref Sexp
+{
+	e: ref Sexp;
+	if(v.a != nil)
+		e = ref Sexp.Binary(v.a, v.hint);
+	else
+		e = ref Sexp.String(v.s, v.hint);
+	return ref Sexp.String(s, nil) :: e :: l;
+}
+
+mkrange(r: Vrange): ref Sexp
+{
+	l: list of ref Sexp;
+	if(r.le > 0)
+		l = addval(l, "le", r.ub);
+	else if(r.le == 0)
+		l = addval(l, "l", r.ub);
+	if(r.ge > 0)
+		l = addval(l, "ge", r.lb);
+	else if(r.ge == 0)
+		l = addval(l, "g", r.lb);
+	return ref Sexp.List(ref Sexp.String("*",nil) :: ref Sexp.String("range",nil) :: ref Sexp.String(r.otext(), nil) :: l);
+}
+
+valof(s: ref Sexp): Val
+{
+	pick r := s {
+	String =>
+		return Val.mk(r.s, nil, r.hint);
+	Binary =>
+		return Val.mk(nil, r.data, r.hint);
+	* =>
+		return Val.mk(nil, nil, nil);	# can't happen
+	}
+}
+
+starop(s: ref Sexp, op: string): (string, list of ref Sexp)
+{
+	if(s == nil)
+		return (nil, nil);
+	pick r := s {
+	List =>
+		if(r.op() == "*" && tl r.l != nil){
+			pick t := hd tl r.l {
+			String =>
+				if(op != nil && t.s != op)
+					return (nil, nil);
+				return (t.s, tl tl r.l);
+			}
+		}
+	}
+	return (nil, nil);
+}
+
+isset(s: ref Sexp): (int, list of ref Sexp)
+{
+	(op, l) := starop(s, "set");
+	if(op != nil)
+		return (1, l);
+	return (0, l);
+}
+
+setof(s: ref Sexp): list of ref Sexp
+{
+	return starop(s, "set").t1;
+}
+
+prefixof(s: ref Sexp): ref Val
+{
+	return substrof(s, "prefix");
+}
+
+suffixof(s: ref Sexp): ref Val
+{
+	return substrof(s, "suffix");
+}
+
+substrof(s: ref Sexp, kind: string): ref Val
+{
+	l := starop(s, kind).t1;
+	if(l == nil)
+		return nil;
+	pick x := hd l{
+	String =>
+		return ref Val.mk(x.s, nil, x.hint);
+	Binary =>
+		return ref Val.mk(nil, x.data, x.hint);
+	}
+	return nil;
+}
+
+rangeof(s: ref Sexp): ref Vrange
+{
+	l := starop(s, "range").t1;
+	if(l == nil)
+		return nil;
+	ord: int;
+	case (hd l).astext() {
+	"alpha" =>	ord = Alpha;
+	"numeric" =>	ord = Numeric;
+	"binary" =>	ord = Binary;
+	"time" =>	ord = Time;	# hh:mm:ss
+	"date" =>	ord = Date;	# full date format
+	* =>	return nil;
+	}
+	l = tl l;
+	lb, ub: Val;
+	lt := -1;
+	gt := -1;
+	while(l != nil){
+		if(tl l == nil)
+			return nil;
+		o := (hd l).astext();
+		v: Val;
+		l = tl l;
+		if(l == nil)
+			return nil;
+		pick t := hd l {
+		String =>
+			v = Val.mk(t.s, nil, t.hint);
+		Binary =>
+			v = Val.mk(nil, t.data, t.hint);
+		* =>
+			return nil;
+		}
+		l = tl l;
+		case o {
+		"g" or "ge" =>
+			if(gt >= 0 || lt >= 0)
+				return nil;
+			gt = o == "ge";
+			lb = v;
+		"l" or "le" =>
+			if(lt >= 0)
+				return nil;
+			lt = o == "le";
+			ub = v;
+		* =>
+			return nil;
+		}
+	}
+	if(gt < 0 && lt < 0)
+		return nil;
+	return ref Vrange(ord, gt, lb, lt, ub);
+}
+
+Els: adt {
+	a:	array of ref Sexp;
+	n:	int;
+
+	add:	fn(el: self ref Els, s: ref Sexp);
+	els:	fn(el: self ref Els): array of ref Sexp;
+};
+
+Els.add(el: self ref Els, s: ref Sexp)
+{
+	if(el.n >= len el.a){
+		t := array[el.n+10] of ref Sexp;
+		if(el.a != nil)
+			t[0:] = el.a;
+		el.a = t;
+	}
+	el.a[el.n++] = s;
+}
+
+Els.els(el: self ref Els): array of ref Sexp
+{
+	if(el.n == 0)
+		return nil;
+	return el.a[0:el.n];
+}
+
+remake(s: ref Sexp): ref Sexp
+{
+	if(s == nil)
+		return nil;
+	pick r := s {
+	List =>
+		(is, mem) := isset(r);
+		if(is){
+			el := ref Els(array[10] of ref Sexp, 0);
+			members(mem, el);
+			if(debug)
+				sys->print("-- %#q\n", s.text());
+			y := mkset0(tolist(el.els()));
+			if(debug){
+				if(y == nil)
+					sys->print("\t=> EMPTY\n");
+				else
+					sys->print("\t=> %#q\n", y.text());
+			}
+			return y;
+		}
+		rl: list of ref Sexp;
+		for(l := r.l; l != nil; l = tl l){
+			e := remake(hd l);
+			if(e != hd l){
+				# structure changed, remake current node's list
+				for(il := r.l; il != l; il = tl il)
+					rl = hd il :: rl;
+				rl = e :: rl;
+				while((l = tl l) != nil)
+					rl = remake(hd l) :: rl;
+				return ref Sexp.List(rev(rl));
+			}
+		}
+		# unchanged
+	}
+	return s;
+}
+
+members(l: list of ref Sexp, el: ref Els)
+{
+	for(; l != nil; l = tl l){
+		e := hd l;
+		(is, mem) := isset(e);
+		if(is)
+			members(mem, el);
+		else
+			el.add(remake(e));
+	}
+}
+
+mkset(sl: list of ref Sexp): ref Sexp
+{
+	rl: list of ref Sexp;
+	for(l := sl; l != nil; l = tl l){
+		(is, mem) := isset(hd l);
+		if(is){
+			for(; mem != nil; mem = tl mem)
+				rl = hd mem :: rl;
+		}else
+			rl = hd l :: rl;
+	}
+	return mkset0(rev(rl));
+}
+
+mkset0(mem: list of ref Sexp): ref Sexp
+{
+	if(mem == nil)
+		return nil;
+	return ref Sexp.List(ref Sexp.String("*", nil) :: ref Sexp.String("set", nil) :: mem);
+}
+
+factor(a: array of ref Sexp): ref Sexp
+{
+	mergesort(a, array[len a] of ref Sexp);
+	for(i := 0; i < len a; i++){
+		case tagindex(a[i]) {
+		Astar =>
+			return a[i];
+		Alist =>
+			k := i+1;
+			if(k >= len a)
+				break;
+			if(a[k].islist() && (op := a[i].op()) != "*" && op == a[k].op()){
+				# ensure tag uniqueness within a set by: (* set (a L1) (a L2)) => (a (* set L1 L2))
+				ml := a[i].els();
+				n0 := hd ml;
+				rl := ref Sexp.List(tl ml) :: ref Sexp.String("set", nil) :: ref Sexp.String("*", nil) :: nil;	# reversed
+				# gather tails of adjacent lists with op matching this one
+				for(; k < len a && a[k].islist() && a[k].op() == op; k++){
+					ml = tl a[k].els();
+					if(len ml == 1)
+						rl = hd ml :: rl;
+					else
+						rl = ref Sexp.List(ml) :: rl;
+				}
+				a[i] = ref Sexp.List(n0 :: remake(ref Sexp.List(rev(rl))) :: nil);
+				sys->print("common: %q [%d -> %d] -> %q\n", op, i, k-1, a[i].text());
+				if(k < len a)
+					a[i+1:] = a[k:];
+				a = a[0:i+1+(len a-k)];
+			}
+		}
+	}
+	return mkset0(tolist(a));
+}
+
+tolist(a: array of ref Sexp): list of ref Sexp
+{
+	l: list of ref Sexp;
+	for(i := len a; --i >= 0;)
+		l = a[i] :: l;
+	return l;
+}
+
+mergesort(a, b: array of ref Sexp)
+{
+	r := len a;
+	if(r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m]);
+		mergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if(b[i].islist() || !b[j].islist() && b[i].op() > b[j].op())	# a list is greater than any atom
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if(i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+Val: adt {
+	# only one of s or a is not nil
+	s:	string;
+	a:	array of byte;
+	hint:	string;
+	nb:	int;	# size in bytes
+
+	mk:	fn(s: string, a: array of byte, h: string): Val;
+	cmp:	fn(a: self Val, b: Val, order: int): int;
+	isfloat:	fn(a: self Val): int;
+	isprefix:	fn(a: self Val, b: Val): int;
+	issuffix:	fn(a: self Val, b: Val): int;
+	bytes:	fn(a: self Val): array of byte;
+	text:	fn(v: self Val): string;
+};
+
+Val.mk(s: string, a: array of byte, h: string): Val
+{
+	if(a != nil)
+		nb := len a;
+	else
+		nb = utflen(s);
+	return Val(s, a, h, nb);
+}
+
+Val.bytes(v: self Val): array of byte
+{
+	if(v.a != nil)
+		return v.a;
+	return array of byte v.s;
+}
+
+Val.isfloat(v: self Val): int
+{
+	if(v.a != nil)
+		return 0;
+	for(i := 0; i < len v.s; i++)
+		if(v.s[i] == '.')
+			return 1;
+	return 0;
+}
+
+Val.isprefix(a: self Val, b: Val): int
+{
+	if(a.hint != b.hint)
+		return 0;
+	# normalise to bytes
+	va := a.bytes();
+	vb := b.bytes();
+	for(i := 0; i < len va; i++)
+		if(i >= len vb || va[i] != vb[i])
+			return 0;
+	return 1;
+}
+
+Val.issuffix(a: self Val, b: Val): int
+{
+	if(a.hint != b.hint)
+		return 0;
+	# normalise to bytes
+	va := a.bytes();
+	vb := b.bytes();
+	for(i := 0; i < len va; i++)
+		if(i >= len vb || va[len va-i-1] != vb[len vb-i-1])
+			return 0;
+	return 1;
+}
+
+Val.cmp(a: self Val, b: Val, order: int): int
+{
+	if(a.hint != b.hint)
+		return -2;
+	case order {
+	Numeric =>	# TO DO: change this to use string comparisons
+		if(a.a != nil || b.a != nil)
+			return -2;
+		if(a.isfloat() || b.isfloat()){
+			fa := real a.s;
+			fb := real b.s;
+			if(fa < fb)
+				return -1;
+			if(fa > fb)
+				return 1;
+			return 0;
+		}
+		ia := big a.s;
+		ib := big b.s;
+		if(ia < ib)
+			return -1;
+		if(ia > ib)
+			return 1;
+		return 0;
+	Binary =>	# right-justified, unsigned binary values
+		av := a.a;
+		if(av == nil)
+			av = array of byte a.s;
+		bv := b.a;
+		if(bv == nil)
+			bv = array of byte b.s;
+		while(len av > len bv){
+			if(av[0] != byte 0)
+				return 1;
+			av = av[1:];
+		}
+		while(len bv > len av){
+			if(bv[0] != byte 0)
+				return -1;
+			bv = bv[1:];
+		}
+		return cmpbytes(av, bv);
+	}
+	# otherwise compare as strings
+	if(a.a != nil){
+		if(b.s != nil)
+			return cmpbytes(a.a, array of byte b.s);
+		return cmpbytes(a.a, b.a);
+	}
+	if(b.a != nil)
+		return cmpbytes(array of byte a.s, b.a);
+	if(a.s < b.s)
+		return -1;
+	if(a.s > b.s)
+		return 1;
+	return 0;
+}
+
+Val.text(v: self Val): string
+{
+	s: string;
+	if(v.hint != nil)
+		s = sys->sprint("[%s]", v.hint);
+	if(v.s != nil)
+		return s+v.s;
+	if(v.a != nil)
+		return sys->sprint("%s#%s#", s, base16->enc(v.a));
+	return sys->sprint("%s\"\"", s);
+}
+
+cmpbytes(a, b: array of byte): int
+{
+	n := len a;
+	if(n > len b)
+		n = len b;
+	for(i := 0; i < n; i++)
+		if(a[i] != b[i])
+			return int a[i] - int b[i];
+	return len a - len b;
+}
+
+Vrange: adt {
+	order:	int;
+	ge:	int;
+	lb:	Val;
+	le:	int;
+	ub:	Val;
+
+	text:	fn(v: self Vrange): string;
+	otext:	fn(v: self Vrange): string;
+	intersect:	fn(a: self Vrange, b: Vrange): (int, Vrange);
+};
+
+Alpha, Numeric, Time, Binary, Date: con iota;	# Vrange.order
+
+Vrange.otext(r: self Vrange): string
+{
+	case r.order {
+	Alpha =>	return "alpha";
+	Numeric =>	return "numeric";
+	Time =>	return "time";
+	Binary =>	return "binary";
+	Date => return "date";
+	* => return sys->sprint("O%d", r.order);
+	}
+}
+
+Vrange.text(v: self Vrange): string
+{
+	s := sys->sprint("(* range %s", v.otext());
+	if(v.ge >= 0){
+		s += " g";
+		if(v.ge)
+			s += "e";
+		s += " "+v.lb.text();
+	}
+	if(v.le >= 0){
+		s += " l";
+		if(v.le)
+			s += "e";
+		s += " "+v.ub.text();
+	}
+	return s+")";
+}
+
+Vrange.intersect(v1: self Vrange, v2: Vrange): (int, Vrange)
+{
+	if(v1.order != v2.order)
+		return (0, v1);	# incommensurate
+	v := v1;
+	if(v.ge < 0 || v2.ge >= 0 && v2.lb.cmp(v.lb, v.order) > 0)
+		v.lb = v2.lb;
+	if(v.le < 0 || v2.le >= 0 && v2.ub.cmp(v.ub, v.order) < 0)
+		v.ub = v2.ub;
+	if(v.lb.hint != v.ub.hint)
+		return (0, v1);	# incommensurate
+	v.ge &= v2.ge;
+	v.le &= v2.le;
+	c := v.lb.cmp(v.ub, v.order);
+	if(c > 0 || c == 0 && !(v.ge && v.le))
+		return (0, v1);	# empty range
+	return (1, v);
+}
+
+utflen(s: string): int
+{
+	return len array of byte s;
+}
+
+append[T](l1, l2: list of T): list of T
+{
+	rl1: list of T;
+	for(; l1 != nil; l1 = tl l1)
+		rl1 = hd l1 :: rl1;
+	for(; rl1 != nil; rl1 = tl rl1)
+		l2 = hd rl1 :: l2;
+	return l2;
+}
+
+rev[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+revt[S,T](l: list of (S,T)): list of (S,T)
+{
+	rl: list of (S,T);
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+#
+# the following should probably be in a separate Limbo library module,
+# or provided in some way directly by Keyring
+#
+
+Keyrep: adt {
+	alg:	string;
+	owner:	string;
+	els:	list of (string, ref IPint);
+	pick{	# keeps a type distance between public and private keys
+	PK =>
+	SK =>
+	}
+
+	pk:	fn(pk: ref Keyring->PK): ref Keyrep.PK;
+	sk:	fn(sk: ref Keyring->SK): ref Keyrep.SK;
+	mkpk:	fn(k: self ref Keyrep): (ref Keyring->PK, int);
+	mksk:	fn(k: self ref Keyrep): ref Keyring->SK;
+	get:	fn(k: self ref Keyrep, n: string): ref IPint;
+	getb:	fn(k: self ref Keyrep, n: string): array of byte;
+	eq:	fn(k1: self ref Keyrep, k2: ref Keyrep): int;
+};
+
+#
+# convert an Inferno key into a (name, IPint) representation,
+# where `names' maps between Inferno key component offsets and factotum names
+#
+keyextract(flds: list of string, names: list of (string, int)): list of (string, ref IPint)
+{
+	a := array[len flds] of ref IPint;
+	for(i := 0; i < len a; i++){
+		a[i] = IPint.b64toip(hd flds);
+		flds = tl flds;
+	}
+	rl: list of (string, ref IPint);
+	for(; names != nil; names = tl names){
+		(n, p) := hd names;
+		if(p < len a)
+			rl = (n, a[p]) :: rl;
+	}
+	return revt(rl);
+}
+
+Keyrep.pk(pk: ref Keyring->PK): ref Keyrep.PK
+{
+	s := kr->pktostr(pk);
+	(nf, flds) := sys->tokenize(s, "\n");
+	if((nf -= 2) < 0)
+		return nil;
+	case hd flds {
+	"rsa" =>
+		return ref Keyrep.PK(hd flds, hd tl flds,
+			keyextract(tl tl flds, list of {("ek",1), ("n",0)}));
+	"elgamal" =>
+		return ref Keyrep.PK(hd flds, hd tl flds,
+			keyextract(tl tl flds, list of {("p",0), ("alpha",1), ("key",2)}));
+	"dsa" =>
+		return ref Keyrep.PK(hd flds, hd tl flds,
+			keyextract(tl tl flds, list of {("p",0), ("alpha",2), ("q",1), ("key",3)}));
+	* =>
+		return nil;
+	}
+}
+
+Keyrep.sk(pk: ref Keyring->SK): ref Keyrep.SK
+{
+	s := kr->sktostr(pk);
+	(nf, flds) := sys->tokenize(s, "\n");
+	if((nf -= 2) < 0)
+		return nil;
+	# the ordering of components below should match the one defined in the spki spec
+	case hd flds {
+	"rsa" =>
+		return ref Keyrep.SK(hd flds, hd tl flds,
+			keyextract(tl tl flds,list of {("ek",1), ("n",0), ("!dk",2), ("!q",4), ("!p",3), ("!kq",6), ("!kp",5), ("!c2",7)}));	# see comment elsewhere about p, q
+	"elgamal" =>
+		return ref Keyrep.SK(hd flds, hd tl flds,
+			keyextract(tl tl flds, list of {("p",0), ("alpha",1), ("key",2), ("!secret",3)}));
+	"dsa" =>
+		return ref Keyrep.SK(hd flds, hd tl flds,
+			keyextract(tl tl flds, list of {("p",0), ("alpha",2), ("q",1), ("key",3), ("!secret",4)}));
+	* =>
+		return nil;
+	}
+}
+
+Keyrep.get(k: self ref Keyrep, n: string): ref IPint
+{
+	n1 := f2s("rsa", n);
+	for(el := k.els; el != nil; el = tl el)
+		if((hd el).t0 == n || (hd el).t0 == n1)
+			return (hd el).t1;
+	return nil;
+}
+
+Keyrep.getb(k: self ref Keyrep, n: string): array of byte
+{
+	v := k.get(n);
+	if(v == nil)
+		return nil;
+	return pre0(v.iptobebytes());
+}
+
+Keyrep.mkpk(k: self ref Keyrep): (ref Keyring->PK, int)
+{
+	case k.alg {
+	"rsa" =>
+		e := k.get("ek");
+		n := k.get("n");
+		if(e == nil || n == nil)
+			return (nil, 0);
+		return (kr->strtopk(sys->sprint("rsa\n%s\n%s\n%s\n", k.owner, n.iptob64(), e.iptob64())), n.bits());
+	* =>
+		raise "Keyrep: unknown algorithm";
+	}
+}
+
+Keyrep.mksk(k: self ref Keyrep): ref Keyring->SK
+{
+	case k.alg {
+	"rsa" =>
+		e := k.get("ek");
+		n := k.get("n");
+		dk := k.get("!dk");
+		p := k.get("!p");
+		q := k.get("!q");
+		kp := k.get("!kp");
+		kq := k.get("!kq");
+		c12 := k.get("!c2");
+		if(e == nil || n == nil || dk == nil || p == nil || q == nil || kp == nil || kq == nil || c12 == nil)
+			return nil;
+		return kr->strtosk(sys->sprint("rsa\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n",
+			k.owner, n.iptob64(), e.iptob64(), dk.iptob64(), p.iptob64(), q.iptob64(),
+			kp.iptob64(), kq.iptob64(), c12.iptob64()));
+	* =>
+		raise "Keyrep: unknown algorithm";
+	}
+}
+
+#
+# account for naming differences between keyring and factotum, and spki.
+# this might not be the best place for this.
+#
+s2f(s: string): string
+{
+	case s {
+	"e" => return "ek";
+	"d" => return "!dk";
+	"p" => return "!q";		# NB: p and q (kp and kq) roles are reversed between libsec and pkcs
+	"q" => return "!p";
+	"a" => return "!kq";
+	"b" => return "!kp";
+	"c" => return "!c2";
+	* =>	return s;
+	}
+}
+
+f2s(alg: string, s: string): string
+{
+	case alg {
+	"rsa" =>
+		case s {
+		"ek" =>	return "e";
+		"!p" =>	return "q";	# see above
+		"!q" =>	return "p";
+		"!dk" =>	return "d";
+		"!kp" =>	return "b";
+		"!kq" =>	return "a";
+		"!c2" =>	return "c";
+		}
+	"dsa" =>
+		case s {
+		"p" or "q" =>	return s;
+		"alpha" =>	return "g";
+		"key" =>	return "y";
+		}
+	* =>
+		;
+	}
+	if(s != nil && s[0] == '!')
+		return s[1:];
+	return s;
+}
+
+Keyrep.eq(k1: self ref Keyrep, k2: ref Keyrep): int
+{
+	# n⁲ but n is small
+	for(l1 := k1.els; l1 != nil; l1 = tl l1){
+		(n, v1) := hd l1;
+		v2 := k2.get(n);
+		if(v2 == nil || !v1.eq(v2))
+			return 0;
+	}
+	for(l2 := k2.els; l2 != nil; l2 = tl l2)
+		if(k1.get((hd l2).t0) == nil)
+			return 0;
+	return 1;
+}
+
+sig2icert(sig: ref Signature, signer: string, exp: int): ref Keyring->Certificate
+{
+	if(sig.sig == nil)
+		return nil;
+	s := sys->sprint("%s\n%s\n%s\n%d\n%s\n", "rsa", sig.hash.alg, signer, exp, base64->enc((hd sig.sig).t1));
+#sys->print("alg %s *** %s\n", sig.sa, base64->enc((hd sig.sig).t1));
+	return kr->strtocert(s);
+}
+
+icert2els(cert: ref Keyring->Certificate): (string, string, string, list of (string, array of byte))
+{
+	s := kr->certtoattr(cert);
+	if(s == nil)
+		return (nil, nil, nil, nil);
+	(nil, l) := sys->tokenize(s, " ");	# really need parseattr, and a better interface
+	vals: list of (string, array of byte);
+	alg, hashalg, signer: string;
+	for(; l != nil; l = tl l){
+		(nf, fld) := sys->tokenize(hd l, "=");
+		if(nf != 2)
+			continue;
+		case hd fld {
+		"sigalg" =>
+			(nf, fld) = sys->tokenize(hd tl fld, "-");
+			if(nf != 2)
+				continue;
+			alg = hd fld;
+			hashalg = hd tl fld;
+		"signer" =>
+			signer = hd tl fld;
+		"expires" =>
+			;	# don't care
+		* =>
+			vals = (hd fld, base16->dec(hd tl fld)) :: vals;
+		}
+	}
+	return (alg, hashalg, signer, revt(vals));
+}
+
+#
+# pkcs1 asn.1 DER encodings
+#
+
+pkcs1_md5_pfx := array[] of {
+	byte 16r30, byte 32,                 # SEQUENCE in 32 bytes
+		byte 16r30, byte 12,                 # SEQUENCE in 12 bytes
+			byte 6, byte 8,                     # OBJECT IDENTIFIER in 8 bytes
+				byte (40*1+2),                   # iso(1) member-body(2)
+				byte (16r80 + 6), byte 72,             # US(840)
+				byte (16r80 + 6), byte (16r80 + 119), byte 13, # rsadsi(113549)
+				byte 2,                        # digestAlgorithm(2)
+				byte 5,                        # md5(5), end of OBJECT IDENTIFIER
+			byte 16r05, byte 0,                  # NULL parameter, end of SEQUENCE
+		byte 16r04, byte 16             #OCTET STRING in 16 bytes (MD5 length)
+} ; 
+
+pkcs1_sha1_pfx := array[] of {
+	byte 16r30, byte 33,               # SEQUENCE in 33 bytes
+		byte 16r30, byte 9,                 # SEQUENCE in 9 bytes
+			byte 6, byte 5,                    # OBJECT IDENTIFIER in 5 bytes
+				byte (40*1+3),                  # iso(1) member-body(3)
+				byte 14,                      # ??(14)
+				byte 3,                       # ??(3)
+				byte 2,                       # digestAlgorithm(2)
+				byte 26,                     # sha1(26), end of OBJECT IDENTIFIER
+			byte 16r05, byte 0,          # NULL parameter, end of SEQUENCE
+		byte 16r40, byte 20	# OCTET STRING in 20 bytes (SHA1 length)
+};
+
+#
+# mlen should be key length in bytes
+#
+pkcs1_encode(ha: string, hash: array of byte, mlen: int): array of byte
+{
+	# apply hash function to message
+	prefix: array of byte;
+	case ha {
+	"md5" =>
+		prefix = pkcs1_md5_pfx;
+	"sha" or "sha1" =>
+		prefix = pkcs1_sha1_pfx;
+	* =>
+		return nil;
+	}
+	tlen := len prefix + len hash;
+	if(mlen < tlen + 11)
+		return nil;	# "intended encoded message length too short"
+	pslen := mlen - tlen - 3;
+	out := array[mlen] of byte;
+	out[0] = byte 0;
+	out[1] = byte 1;
+	for(i:=0; i<pslen; i++)
+		out[i+2] = byte 16rFF;
+	out[2+pslen] = byte 0;
+	out[2+pslen+1:] = prefix;
+	out[2+pslen+1+len prefix:] = hash;
+	return out;
+}
+
+#
+# for debugging
+#
+rsacomp(block: array of byte, akey: ref Key): array of byte
+{
+	key := Keyrep.pk(akey.pk);
+	x := kr->IPint.bebytestoip(block);
+	y := x.expmod(key.get("e"), key.get("n"));
+	ybytes := y.iptobebytes();
+#dump("rsacomp", ybytes);
+	k := 1024; # key.modlen;
+	ylen := len ybytes;
+	if(ylen < k) {
+		a := array[k] of { * =>  byte 0};
+		a[k-ylen:] = ybytes[0:];
+		ybytes = a;
+	}
+	else if(ylen > k) {
+		# assume it has leading zeros (mod should make it so)
+		a := array[k] of byte;
+		a[0:] = ybytes[ylen-k:];
+		ybytes = a;
+	}
+	return ybytes;
+}
--- /dev/null
+++ b/appl/lib/spki/verifier.b
@@ -1,0 +1,200 @@
+implement Verifier;
+
+#
+# Copyright © 2004 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "keyring.m";
+	kr: Keyring;
+	IPint: import kr;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "sexprs.m";
+	sexprs: Sexprs;
+	Sexp: import sexprs;
+
+include "spki.m";
+	spki: SPKI;
+	Hash, Key, Cert, Name, Subject, Signature, Seqel, Toplev, Valid: import spki;
+	dump: import spki;
+
+include "encoding.m";
+	base64: Encoding;
+
+debug := 0;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	kr = load Keyring Keyring->PATH;
+	bufio = load Bufio Bufio->PATH;
+	sexprs = load Sexprs Sexprs->PATH;
+	spki = load SPKI SPKI->PATH;
+	base64 = load Encoding Encoding->BASE64PATH;
+
+	sexprs->init();
+	spki->init();
+}
+
+putkey(keys: list of ref Key, k: ref Key): list of ref Key
+{
+	for(kl := keys; kl != nil; kl = tl kl)
+		if(k.eq(hd kl))
+			return keys;
+	return k :: keys;
+}
+
+keybyhash(hl: list of ref Hash, keys: list of ref Key): ref Key
+{
+	for(kl := keys; kl != nil; kl = tl kl){
+		k := hd kl;
+		if(k.hash != nil && anyhashmatch(hl, k.hash))
+			return k;
+	}
+	return nil;
+}
+
+anyhashmatch(hl1, hl2: list of ref Hash): int
+{
+	for(; hl1 != nil; hl1 = tl hl1){
+		h1 := hd hl1;
+		for(; hl2 != nil; hl2 = tl hl2)
+			if(h1.eq(hd hl2))
+				return 1;
+	}
+	return 0;
+}
+
+verify(seq: list of ref Seqel): (ref Speaksfor, list of ref Seqel, string)
+{
+	stack: list of ref Seqel;
+	keys: list of ref Key;
+	n0: ref Name;
+	cn: ref Cert;
+	delegate := 1;
+	tag: ref Sexp;
+	val: ref Valid;
+	for(; seq != nil; seq = tl seq){
+		pick s := hd seq {
+		C =>
+			diag := checkcert(s.c);
+			if(diag != nil)
+				return (nil, seq, diag);
+			if(stack != nil){
+				pick h := hd stack {
+				C =>
+					if(!delegate)
+						return(nil, seq, "previous auth certificate did not delegate");
+					if(!h.c.subject.principal().eq(s.c.issuer.principal))
+						return (nil, seq, "certificate chain has mismatched principals");
+					if(debug)
+						sys->print("issuer %s ok\n", s.c.issuer.principal.text());
+				}
+				stack = tl stack;
+			}
+			stack = s :: stack;
+			if(n0 == nil)
+				n0 = s.c.issuer;
+			cn = s.c;
+			pick t := s.c {
+			A or KH or O =>
+				delegate = t.delegate;
+				if(tag != nil){
+					tag = spki->tagintersect(tag, t.tag);
+					if(tag == nil)
+						return (nil, seq, "certificate chain has null authority");
+				}else
+					tag = t.tag;
+				if(val != nil){
+					if(t.valid != nil){
+						(ok, iv) := (*val).intersect(*t.valid);
+						if(!ok)
+							return (nil, seq, "certificate chain is not currently valid");
+						*val = iv;
+					}
+				}else
+					val = t.valid;
+			}
+		K =>
+			stack = s :: stack;
+		O =>
+			if(s.op == "debug"){
+				debug = !debug;
+				continue;
+			}
+			if(s.op != "hash" || s.args == nil || tl s.args != nil)
+				return (nil, seq, "invalid operation to `do'");
+			alg := (hd s.args).astext();
+			if(alg != "md5" && alg != "sha1")
+				return (nil, seq, "invalid hash operation");
+			if(stack == nil)
+				return (nil, seq, "verification stack empty");
+			pick h := hd stack {
+			K =>
+				a := h.k.hashed(alg);
+				if(debug)
+					dump("do hash", a);
+				keys = putkey(keys, h.k);
+				stack = tl stack;
+			C =>
+				;
+			* =>
+				return (nil, seq, "invalid type of operand for hash");
+			}
+		S =>
+			if(stack == nil)
+				return (nil, seq, "verification stack empty");
+			sig := s.sig;
+			if(sig.key == nil)
+				return (nil, seq, "neither hash nor key for signature");
+			if(sig.key.pk == nil){
+				k := keybyhash(sig.key.hash, keys);
+				if(k == nil)
+					return (nil, seq, "unknown key for signature");
+				sig.key = k;
+			}
+			pick c := hd stack {
+			C =>
+				if(c.c.e == nil)
+					return (nil, seq, "missing canonical expression for cert");
+				a := c.c.e.pack();
+				# verify signature ...
+				if(debug)
+					dump("cert a", a);
+				h := spki->hashbytes(a, "md5");
+				if(debug){
+					dump("hash cert", h);
+					sys->print("hash = %q\n", base64->enc(h));
+				}
+				failed := spki->checksig(c.c, sig);
+				if(debug)
+					sys->print("checksig: %q\n", failed);
+				if(failed != nil)
+					return (nil, seq, "signature verification failed: "+failed);
+			* =>
+				return (nil, seq, "invalid type of signature operand");
+			}
+		}
+	}
+	if(n0 != nil && cn != nil){
+		if(debug){
+			if(tag != nil)
+				auth := sys->sprint(" regarding %q", tag.text());
+			sys->print("%q speaks for %q%s\n", cn.subject.text(), n0.text(), auth);
+		}
+		return (ref Speaksfor(cn.subject, n0, tag, val), nil, nil);
+	}
+	return (nil, nil, nil);
+}
+
+checkcert(c: ref Cert): string
+{
+	# TO DO?
+	return nil;
+}
--- /dev/null
+++ b/appl/lib/ssl.b
@@ -1,0 +1,90 @@
+implement SSL;
+
+include "sys.m";
+	sys: Sys;
+
+include "keyring.m";
+include "security.m";
+
+sslclone(): (ref Sys->Connection, string)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	(rc, nil) := sys->stat("#D");	# only the local device will work, because local file descriptors are used
+	if(rc < 0)
+		return (nil, sys->sprint("cannot access SSL device #D: %r"));
+	c := ref Sys->Connection;
+	c.dir = "#D";
+	if(rc >= 0){
+		(rc, nil) = sys->stat("#D/ssl");	# another variant
+		if(rc >= 0)
+			c.dir = "#D/ssl";
+	}
+	clonef := c.dir+"/clone";
+	c.cfd = sys->open(clonef, Sys->ORDWR);
+	if(c.cfd == nil)
+		return (nil, sys->sprint("cannot open %s: %r", clonef));
+	s := readstring(c.cfd);
+	if(s == nil)
+		return (nil, sys->sprint("cannot read %s: %r", clonef));
+	c.dir += "/" + s;
+	return (c, nil);
+}
+
+connect(fd: ref Sys->FD): (string, ref Sys->Connection)
+{
+	(c, err) := sslclone();
+	if(c == nil)
+		return (err, nil);
+	c.dfd = sys->open(c.dir + "/data", Sys->ORDWR);
+	if(c.dfd == nil)
+		return (sys->sprint("cannot open data: %r"), nil);
+	if(sys->fprint(c.cfd, "fd %d", fd.fd) < 0)
+		return (sys->sprint("cannot push fd: %r"), nil);
+	return (nil, c);
+}
+
+secret(c: ref Sys->Connection, secretin, secretout: array of byte): string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	if(secretin != nil){
+		fd := sys->open(c.dir + "/secretin", Sys->ORDWR);
+		if(fd == nil)
+			return sys->sprint("cannot open %s: %r", c.dir + "/secretin");
+		if(sys->write(fd, secretin, len secretin) < 0)
+			return sys->sprint("cannot write %s: %r", c.dir + "/secretin");
+	}
+
+	if(secretout != nil){
+		fd := sys->open(c.dir + "/secretout", Sys->ORDWR);
+		if(fd == nil)
+			return sys->sprint("cannot open %s: %r", c.dir + "/secretout");
+		if(sys->write(fd, secretout, len secretout) < 0)
+			return sys->sprint("cannot open %s: %r", c.dir + "/secretout");
+	}
+	return nil;
+}
+
+algs(): (list of string, list of string)
+{
+	(c, nil) := sslclone();
+	if(c == nil)
+		return (nil, nil);
+	c.dfd = nil;
+	(nil, encalgs) := sys->tokenize(readstring(sys->open(c.dir+"/encalgs", Sys->OREAD)), " \t\n");
+	(nil, hashalgs) := sys->tokenize(readstring(sys->open(c.dir+"/hashalgs", Sys->OREAD)), " \t\n");
+	return (encalgs, hashalgs);
+}
+
+readstring(fd: ref Sys->FD): string
+{
+	if(fd == nil)
+		return nil;
+	buf := array[256] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return nil;
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/lib/string.b
@@ -1,0 +1,427 @@
+implement String;
+
+include "string.m";
+
+splitl(s: string, cl: string): (string, string)
+{
+	n := len s;
+	for(j := 0; j < n; j++) {
+		if(in(s[j], cl))
+			return (s[0:j], s[j:n]);
+	}
+	return (s,"");
+}
+
+splitr(s: string, cl: string): (string, string)
+{
+	n := len s;
+	for(j := n-1; j >= 0; j--) {
+		if(in(s[j], cl))
+			return (s[0:j+1], s[j+1:n]);
+	}
+	return ("",s);
+}
+
+drop(s: string, cl: string): string
+{
+	n := len s;
+	for(j := 0; j < n; j++) {
+		if(!in(s[j], cl))
+			return (s[j:n]);
+	}
+	return "";
+}
+
+take(s: string, cl: string): string
+{
+	n := len s;
+	for(j := 0; j < n; j++) {
+		if(!in(s[j], cl))
+			return (s[0:j]);
+	}
+	return s;
+}
+
+in(c: int, s: string): int
+{
+	n := len s;
+	if(n == 0)
+		return 0;
+	ans := 0;
+	negate := 0;
+	if(s[0] == '^') {
+		negate = 1;
+		s = s[1:];
+		n--;
+	}
+	for(i := 0; i < n; i++) {
+		if(s[i] == '-' && i > 0 && i < n-1)  {
+			if(c >= s[i-1] && c <= s[i+1]) {
+				ans = 1;
+				break;
+			}
+			i++;
+		}
+		else
+		if(c == s[i]) {
+			ans = 1;
+			break;
+		}
+	}
+	if(negate)
+		ans = !ans;
+	return ans;
+}
+
+splitstrl(s: string, t: string): (string, string)
+{
+	n := len s;
+	nt := len t;
+	if(nt == 0)
+		return ("", s);
+	c0 := t[0];
+    mainloop:
+	for(j := 0; j <= n-nt; j++) {
+		if(s[j] == c0) {
+			for(k := 1; k < nt; k++)
+				if(s[j+k] != t[k])
+					continue mainloop;
+			return(s[0:j], s[j:n]);
+		}
+	}
+	return (s,"");
+}
+
+splitstrr(s: string, t: string): (string, string)
+{
+	n := len s;
+	nt := len t;
+	if(nt == 0)
+		return (s, "");
+	c0 := t[0];
+    mainloop:
+	for(j := n-nt; j >= 0; j--) {
+		if(s[j] == c0) {
+			for(k := 1; k < nt; k++)
+				if(s[j+k] != t[k])
+					continue mainloop;
+			return(s[0:j+nt], s[j+nt:n]);
+		}
+	}
+	return ("",s);
+}
+
+prefix(pre: string, s: string): int
+{
+	if(len s < len pre)
+		return 0;
+	for(k := 0; k < len pre; k++)
+		if(pre[k] != s[k])
+			return 0;
+	return 1;
+}
+
+tolower(s: string): string
+{
+	for(i := 0; i < len s; i++) {
+		c := s[i];
+		if(c >= 'A' && c <= 'Z')
+			s[i] += 'a' - 'A';
+	}
+	return s;
+}
+
+toupper(s: string): string
+{
+	for(i := 0; i < len s; i++) {
+		c := s[i];
+		if(c >= 'a' && c <= 'z')
+			s[i] += 'A' - 'a';
+	}
+	return s;
+}
+
+startnum(s: string, base: int): (int, int, int)
+{
+	if(s == nil || base != 0 && (base < 2 || base > 36))
+		return (0, 0, 0);
+
+	# skip possible leading white space
+	c := ' ';
+	for (i := 0; i < len s; i++) {
+		c = s[i];
+		if(c != ' ' && c != '\t' && c != '\n')
+			break;
+	}
+
+	# optional sign
+	neg := 0;
+	if(c == '-' || c == '+') {
+		if(c == '-')
+			neg = 1;
+		i++;
+	}
+
+	if(base == 0) {
+		# parse possible leading base designator
+		start := i;
+		base = -1;
+		for (; i < start+3 && i < len s; i++) {
+			c = s[i];
+			if (c == 'r' && i > start) {
+				base = int s[start:i];
+				i++;
+				break;
+			} else if (c < '0' || c > '9')
+				break;
+		}
+		if (base == -1) {
+			i = start;
+			base = 10;
+		} else if (base == 0 || base > 36)
+			return (0, 0, i);
+	}
+	if(i >= len s)
+		return (0, 0, 0);
+
+	return (base, neg, i);
+}
+
+tobig(s: string, base: int): (big, string)
+{
+	neg, i: int;
+
+	(base, neg, i) = startnum(s, base);
+	if(base == 0)
+		return (big 0, s);
+
+	# parse number itself.
+	# probably this should check for overflow, and max out, as limbo op does?
+	start := i;
+	n := big 0;
+	for (; i < len s; i++) {
+		if((d := digit(s[i], base)) < 0)
+			break;
+		n = n*big base + big d;
+	}
+	if (i == start)
+		return (big 0, s);
+	if (neg)
+		return (-n, s[i:]);
+	return (n, s[i:]);
+}
+
+toint(s: string, base: int): (int, string)
+{
+	neg, i: int;
+
+	(base, neg, i) = startnum(s, base);
+	if(base == 0)
+		return (0, s);
+
+	# parse number itself.
+	# probably this should check for overflow, and max out, as limbo op does?
+	start := i;
+	n := 0;
+	for (; i < len s; i++){
+		if((d := digit(s[i], base)) < 0)
+			break;
+		n = n*base + d;
+	}
+	if (i == start)
+		return (0, s);
+	if (neg)
+		return (-n, s[i:]);
+	return (n, s[i:]);
+}
+
+digit(c: int, base: int): int
+{
+	if ('0' <= c && c <= '0' + base - 1)
+		return c-'0';
+	else if ('a' <= c && c < 'a' + base - 10)
+		return (c - 'a' + 10);
+	else if ('A' <= c && c  < 'A' + base - 10)
+		return (c - 'A' + 10);
+	else
+		return -1;	
+}
+
+rpow(x: real, n: int): real
+{
+	inv := 0;
+	if(n < 0){
+		n = -n;
+		inv = 1;
+	}
+	r := 1.0;
+	for(;;){
+		if(n&1)
+			r *= x;
+		if((n >>= 1) == 0)
+			break;
+		x *= x;
+	}
+	if(inv)
+		r = 1.0/r;
+	return r;
+}
+
+match(p: string, s: string, i: int): int
+{
+	if(i+len p > len s)
+		return 0;
+	for(j := 0; j < len p; j++){
+		c := s[i++];
+		if(c >= 'A' && c <= 'Z')
+			c += 'a'-'A';
+		if(p[j] != c)
+			return 0;
+	}
+	return 1;
+}
+
+toreal(s: string, base: int): (real, string)
+{
+	neg, i: int;
+
+	(base, neg, i) = startnum(s, base);
+	if(base == 0)
+		return (0.0, s);
+
+	c := s[i];
+	if((c == 'i' || c == 'I') && match("infinity", s, i))
+		return (real s, s[i+8:]);
+	if((c == 'n' || c == 'N') && match("nan", s, i))
+		return (real s, s[i+3:]);
+
+	if(digit(c, base) < 0)
+		return (0.0, s);
+
+	num := 0.0;
+	for(; i < len s && (d := digit(s[i], base)) >= 0; i++)
+		num = num*real base + real d;
+	dig := 0;	# digits in fraction
+	if(i < len s && s[i] == '.'){
+		i++;
+		for(; i < len s && (d = digit(s[i], base)) >= 0; i++){
+			num = num*real base + real d;
+			dig++;
+		}
+	}
+	exp := 0;
+	eneg := 0;
+	if(i < len s && ((c = s[i]) == 'e' || c == 'E')){
+		start := i;	# might still be badly formed
+		i++;
+		if(i < len s && ((c = s[i]) == '-' || c == '+')){
+			i++;
+			if(c == '-'){
+				dig = -dig;
+				eneg = 1;
+			}
+		}
+		if(i < len s && s[i] >= '0' && s[i] <= '9'){	# exponents are always decimal
+			for(; i < len s && (d = digit(s[i], 10)) >= 0; i++)
+				exp = exp*base + d;
+		}else
+			i = start;
+	}
+	if(base == 10)
+		return (real s[0: i], s[i:]);	# conversion can be more accurate
+	exp -= dig;
+	if(exp < 0){
+		exp = -exp;
+		eneg = !eneg;
+	}
+	if(exp < 0 || exp > 19999)
+		exp = 19999;	# huge but otherwise arbitrary limit
+	dem := rpow(real base, exp);
+	if(eneg)
+		num /= dem;
+	else
+		num *= dem;
+	if(neg)
+		return  (-num,s[i:]);
+	return (num, s[i:]);
+}
+
+append(s: string, l: list of string): list of string
+{
+	t:	list of string;
+
+	# Reverse l, prepend s, and reverse result.
+	while (l != nil) {
+		t = hd l :: t;
+		l = tl l;
+	}
+	t = s :: t;
+	do {
+		l = hd t :: l;
+		t = tl t;
+	} while (t != nil);
+	return l;
+}
+
+quoted(argv: list of string): string
+{
+	return quotedc(argv, nil);
+}
+
+quotedc(argv: list of string, cl: string): string
+{
+	s := "";
+	while(argv != nil){
+		arg := hd argv;
+		for(i := 0; i < len arg; i++){
+			c := arg[i];
+			if(c == ' ' || c == '\t' || c == '\n' || c == '\'' || in(c, cl))
+				break;
+		}
+		if(i < len arg || arg == nil){
+			s += "'" + arg[0:i];
+			for(; i < len arg; i++){
+				if (arg[i] == '\'')
+					s[len s] = '\'';
+				s[len s] = arg[i];
+			}
+			s[len s] = '\'';
+		}else
+			s += arg;
+		if(tl argv != nil)
+			s[len s] = ' ';
+		argv = tl argv;
+	}
+	return s;
+}
+
+unquoted(s: string): list of string
+{
+	args: list of string;
+	word: string;
+	inquote := 0;
+	for(j := len s; j > 0;){
+		c := s[j-1];
+		if(c == ' ' || c == '\t' || c == '\n'){
+			j--;
+			continue;
+		}
+		for(i := j-1; i >= 0 && ((c = s[i]) != ' ' && c != '\t' && c != '\n' || inquote); i--){	# collect word
+			if(c == '\''){
+				word = s[i+1:j] + word;
+				j = i;
+				if(!inquote || i == 0 || s[i-1] != '\'')
+					inquote = !inquote;
+				else
+					i--;
+			}
+		}
+		args = (s[i+1:j]+word) :: args;
+		word = nil;
+		j = i;
+	}
+	# if quotes were unbalanced, balance them and try again.
+	if(inquote)
+		return unquoted(s + "'");
+	return args;
+}
--- /dev/null
+++ b/appl/lib/strinttab.b
@@ -1,0 +1,28 @@
+implement StringIntTab;
+
+include "strinttab.m";
+
+lookup(t: array of StringInt, key: string) : (int, int)
+{
+	min := 0;
+	max := len t-1;
+	while(min <= max){
+		try := (min+max)/2;
+		if(t[try].key < key)
+			min = try+1;
+		else if(t[try].key > key)
+			max = try-1;
+		else
+			return (1, t[try].val);
+	}
+	return (0, 0);
+}
+
+revlookup(t: array of StringInt, val: int) : string
+{
+	n := len t;
+	for(i:=0; i < n; i++)
+		if(t[i].val == val)
+			return t[i].key;
+	return nil;
+}
--- /dev/null
+++ b/appl/lib/strokes/buildstrokes.b
@@ -1,0 +1,260 @@
+implement Buildstrokes;
+
+#
+# this Limbo code is derived from C code that had the following
+# copyright notice, which i reproduce as requested
+#
+# li_strokesnizer.c
+#
+#	Copyright 2000 Compaq Computer Corporation.
+#	Copying or modifying this code for any purpose is permitted,
+#	provided that this copyright notice is preserved in its entirety
+#	in all copies or modifications.
+#	COMPAQ COMPUTER CORPORATION MAKES NO WARRANTIES, EXPRESSED OR
+#	IMPLIED, AS TO THE USEFULNESS OR CORRECTNESS OF THIS CODE OR
+#
+#
+# Adapted from cmu_strokesnizer.c by Jay Kistler.
+#
+# Where is the CMU copyright???? Gotta track it down - Jim Gettys
+#
+# Credit to Dean Rubine, Jim Kempf, and Ari Rapkin.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "strokes.m";
+	strokes: Strokes;
+	Classifier, Penpoint, Stroke, Region: import strokes;
+	Rconvex, Rconcave, Rplain, Rpseudo: import Strokes;
+
+lidebug: con 0;
+stderr: ref Sys->FD;
+
+init(r: Strokes)
+{
+	sys = load Sys Sys->PATH;
+	if(lidebug)
+		stderr = sys->fildes(2);
+	strokes = r;
+}
+
+#
+#  Implementation of the Li/Yeung recognition algorithm
+#
+
+#  Pre-processing and canonicalization parameters
+CANONICAL_X: con 108;
+CANONICAL_Y: con 128;
+NCANONICAL: con 50;
+
+
+#
+# calculate canonical forms
+#
+
+canonical_example(nclasses: int, cnames: array of string, examples: array of list of ref Stroke): (string, array of ref Stroke, array of ref Stroke)
+{
+	canonex := array[nclasses] of ref Stroke;
+	dompts := array[nclasses] of ref Stroke;
+
+	#  make canonical examples for each class.
+	for(i := 0; i < nclasses; i++){
+		if(lidebug)
+			sys->fprint(stderr, "canonical_example: class %s\n", cnames[i]);
+
+		#  Make a copy of the examples.
+		pts: list of ref Stroke = nil;
+		nex := 0;
+		for(exl := examples[i]; exl != nil; exl = tl exl){
+			t := hd exl;
+			pts = t.copy() :: pts;
+			nex++;
+		}
+
+		#  Canonicalize each example, and derive the max x and y ranges.
+		maxxrange := 0;
+		maxyrange := 0;
+		for(exl = pts; exl != nil; exl = tl exl){
+			e := hd exl;
+			ce := canonical_stroke(e);
+			if(ce == nil){
+				if(lidebug)
+					sys->fprint(stderr, "example discarded: can't make canonical form\n");
+				continue;	# try the next one
+			}
+			*e = *ce;
+			if(e.xrange > maxxrange)
+				maxxrange = e.xrange;
+			if(e.yrange > maxyrange)
+				maxyrange = e.yrange;
+		}
+
+		#  Normalise max ranges.
+		(maxxrange, maxyrange) = normalise(maxxrange, maxyrange, CANONICAL_X, CANONICAL_Y);
+
+		#  Re-scale each example to max ranges.
+		for(exl = pts; exl != nil; exl = tl exl){
+			t := hd exl;
+			scalex, scaley: int;
+			if(t.xrange == 0)
+				scalex = 100;
+			else
+				scalex = (100*maxxrange + t.xrange/2) / t.xrange;
+			if(t.yrange == 0)
+				scaley = 100;
+			else
+				scaley = (100*maxyrange + t.yrange/2) / t.yrange;
+			t.translate(0, 0, scalex, scaley);
+		}
+
+		#  Average the examples; leave average in first example.
+		avg := hd pts;				#  careful, aliasing
+		for(k := 0; k < NCANONICAL; k++){
+			xsum := 0;
+			ysum := 0;
+			for(exl = pts; exl != nil; exl = tl exl){
+				t := hd exl;
+				xsum += t.pts[k].x;
+				ysum += t.pts[k].y;
+			}
+			avg.pts[k].x = (xsum + nex/2) / nex;
+			avg.pts[k].y = (ysum + nex/2) / nex;
+		}
+
+		#  rescale averaged stroke
+		avg.scaleup();
+
+		#  Re-compute the x and y ranges and center the stroke.
+		avg.center();
+
+		canonex[i] = avg;	# now it's the canonical representation
+
+		if(lidebug){
+			sys->fprint(stderr, "%s, avgpts = %d\n", cnames[i], avg.npts);
+			for(j := 0; j < avg.npts; j++){
+				p := avg.pts[j];
+				sys->fprint(stderr, "  (%d %d)\n", p.x, p.y);
+			}
+		}
+
+		dompts[i] = avg.interpolate().dominant();	# dominant points of canonical representation
+	}
+
+	return (nil, canonex, dompts);
+}
+
+normalise(x, y: int, xrange, yrange: int): (int, int)
+{
+	if((100*x + xrange/2)/xrange > (100*y + yrange/2)/yrange){
+		y = (y*xrange + x/2)/x;
+		x = xrange;
+	}else{
+		x = (x*yrange + y/2)/y;
+		y = yrange;
+	}
+	return (x, y);
+}
+
+canonical_stroke(points: ref Stroke): ref Stroke
+{
+	points = points.filter();
+	if(points.npts < 2)
+		return nil;
+
+	#  Scale up to avoid conversion errors.
+	points.scaleup();
+
+	#  Compute an equivalent stroke with equi-distant points
+	points = compute_equipoints(points);
+	if(points == nil)
+		return nil;
+
+	#  Re-translate the points to the origin.
+	(minx, miny, maxx, maxy) := points.bbox();
+	points.translate(minx, miny, 100, 100);
+
+	#  Store the x and y ranges in the point list.
+	points.xrange = maxx - minx;
+	points.yrange = maxy - miny;
+
+	if(lidebug){
+		sys->fprint(stderr, "Canonical stroke:   %d, %d, %d, %d\n", minx, miny, maxx, maxy);
+		for(i := 0; i < points.npts; i++){
+			p := points.pts[i];
+			sys->fprint(stderr, "      (%d %d)\n", p.x, p.y);
+		}
+	}
+
+	return points;
+}
+
+compute_equipoints(points: ref Stroke): ref Stroke
+{
+	pathlen := points.length();
+	equidist := (pathlen + (NCANONICAL-1)/2) / (NCANONICAL-1);
+	equipoints := array[NCANONICAL] of Penpoint;
+	if(lidebug)
+		sys->fprint(stderr, "compute_equipoints:  npts = %d, pathlen = %d, equidist = %d\n",
+				points.npts, pathlen, equidist);
+
+	#  First original point is an equipoint.
+	equipoints[0] = points.pts[0];
+	nequipoints := 1;
+	dist_since_last_eqpt := 0;
+
+	for(i := 1; i < points.npts; i++){
+		dx1 := points.pts[i].x - points.pts[i-1].x;
+		dy1 := points.pts[i].y - points.pts[i-1].y;
+		endx := points.pts[i-1].x*100;
+		endy := points.pts[i-1].y*100;
+		remaining_seglen := strokes->sqrt(100*100 * (dx1*dx1 + dy1*dy1));
+		dist_to_next_eqpt := equidist - dist_since_last_eqpt;
+		while(remaining_seglen >= dist_to_next_eqpt){
+			if(dx1 == 0){
+				#  x-coordinate stays the same
+				if(dy1 >= 0)
+					endy += dist_to_next_eqpt;
+				else
+					endy -= dist_to_next_eqpt;
+			}else{
+				slope := (100*dy1 + dx1/2) / dx1;
+				tmp := strokes->sqrt(100*100 + slope*slope);
+				dx := (100*dist_to_next_eqpt + tmp/2) / tmp;
+				dy := (slope*dx + 50)/100;
+				if(dy < 0)
+					dy = -dy;
+				if(dx1 >= 0)
+					endx += dx;
+				else
+					endx -= dx;
+				if(dy1 >= 0)
+					endy += dy;
+				else
+					endy -= dy;
+			}
+			equipoints[nequipoints].x = (endx + 50) / 100;
+			equipoints[nequipoints].y = (endy + 50) / 100;
+			nequipoints++;
+			#assert(nequipoints <= NCANONICAL);
+			dist_since_last_eqpt = 0;
+			remaining_seglen -= dist_to_next_eqpt;
+			dist_to_next_eqpt = equidist;
+		}
+		dist_since_last_eqpt += remaining_seglen;
+	}
+
+	#  Take care of last equipoint.
+	if(nequipoints == NCANONICAL-1){
+		#  Make last original point the last equipoint.
+		equipoints[nequipoints++] = points.pts[points.npts - 1];
+	}
+	if(nequipoints != NCANONICAL){	# fell short
+		if(lidebug)
+			sys->fprint(stderr,"compute_equipoints: nequipoints = %d\n", nequipoints);
+		# 	assert(false);
+		return nil;
+	}
+	return ref Stroke(NCANONICAL, equipoints, 0, 0);
+}
--- /dev/null
+++ b/appl/lib/strokes/mkfile
@@ -1,0 +1,18 @@
+<../../../mkconfig
+
+TARG=\
+	buildstrokes.dis\
+	strokes.dis\
+	readstrokes.dis\
+	writestrokes.dis\
+
+MODULES=
+
+SYSMODULES= \
+	bufio.m\
+	strokes.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/lib/strokes
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/strokes/readstrokes.b
@@ -1,0 +1,205 @@
+implement Readstrokes;
+
+#
+# read structures from stroke classifier files
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "strokes.m";
+	strokes: Strokes;
+	Classifier, Penpoint, Stroke, Region: import strokes;
+	buildstrokes: Buildstrokes;
+
+init(s: Strokes)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	strokes = s;
+}
+
+getint(fp: ref Iobuf): (int, int)
+{
+	while((c := fp.getc()) == ' ' || c == '\t' || c == '\n')
+		;
+	if(c < 0)
+		return (c, 0);
+	sign := 1;
+	if(c == '-')
+		sign = -1;
+	else if(c == '+')
+		;
+	else
+		fp.ungetc();
+	rc := 0;
+	n := 0;
+	while((c = fp.getc()) >= '0' && c <= '9'){
+		n = n*10 + (c-'0');
+		rc = 1;
+	}
+	return (rc, n*sign);
+}
+
+getstr(fp: ref Iobuf): (int, string)
+{
+	while((c := fp.getc()) == ' ' || c == '\t' || c == '\n')
+		;
+	if(c < 0)
+		return (c, nil);
+	fp.ungetc();
+	s := "";
+	while((c = fp.getc()) != ' ' && c != '\t' && c != '\n')
+		s[len s] = c;
+	return (0, s);
+}
+
+getpoint(fp: ref Iobuf): (int, Penpoint)
+{
+	(okx, x) := getint(fp);
+	(oky, y) := getint(fp);
+	if(okx <= 0 || oky <= 0)
+		return (-1, (0,0,0));
+	return (0, (x,y,0));
+}
+
+getpoints(fp: ref Iobuf): ref Stroke
+{
+	(ok, npts) := getint(fp);
+	if(ok <= 0 || npts < 0 || npts > 4000)
+		return nil;
+	pts := array[npts] of Penpoint;
+	for(i := 0; i < npts; i++){
+		(ok, pts[i]) = getpoint(fp);
+		if(ok < 0)
+			return nil;
+	}
+	return ref Stroke(npts, pts, 0, 0);
+}
+
+read_classifier_points(fp: ref Iobuf, nclass: int): (int, array of string, array of list of ref Stroke)
+{
+	names := array[nclass] of string;
+	examples := array[nclass] of list of ref Stroke;
+	for(k := 0; k < nclass; k++){
+		# read class name and number of examples
+		(ok, nex) := getint(fp);
+		if(ok <= 0)
+			return (-1, nil, nil);
+		(ok, names[k]) = getstr(fp);
+		if(ok < 0)
+			return (ok, nil, nil);
+
+		# read examples
+		for(i := 0; i < nex; i++){
+			pts := getpoints(fp);
+			if(pts == nil)
+				return (-1, nil, nil);
+			examples[k] = pts :: examples[k];
+		}
+	}
+	return (0, names, examples);
+}
+
+#
+# read a classifier, using its digest if that exists
+#
+read_classifier(file: string, build: int, needex: int): (string, ref Classifier)
+{
+	rc := ref Classifier;
+	l := len file;
+	digestfile: string;
+	if(l >= 4 && file[l-4:]==".clx")
+		digestfile = file;
+	else if(!needex && l >= 3 && file[l-3:]==".cl")
+		digestfile = file[0:l-3]+".clx";	# try the digest file first
+	err: string;
+	if(digestfile != nil){
+		fd := sys->open(digestfile, Sys->OREAD);
+		if(fd != nil){
+			(err, rc.cnames, rc.dompts) = read_digest(fd);
+			rc.nclasses = len rc.cnames;
+			if(rc.cnames == nil)
+				err = "empty digest file";
+			if(err == nil)
+				return (nil, rc);
+		}else
+			err = sys->sprint("%r");
+		if(!build)
+			return (sys->sprint("digest file: %s", err), nil);
+	}
+
+	if(buildstrokes == nil){
+		buildstrokes = load Buildstrokes Buildstrokes->PATH;
+		if(buildstrokes == nil)
+			return (sys->sprint("module %s: %r", Buildstrokes->PATH), nil);
+		buildstrokes->init(strokes);
+	}
+
+	fd := sys->open(file, Sys->OREAD);
+	if(fd == nil)
+		return (sys->sprint("%r"), nil);
+	(emsg, cnames, examples) := read_examples(fd);
+	if(emsg != nil)
+		return (emsg, nil);
+	rc.nclasses = len cnames;
+	(err, rc.canonex, rc.dompts) = buildstrokes->canonical_example(rc.nclasses, cnames, examples);
+	if(err != nil)
+		return ("failed to calculate canonical examples", nil);
+	rc.cnames = cnames;
+	if(needex)
+		rc.examples = examples;
+
+	return (nil, rc);
+}
+
+read_examples(fd: ref Sys->FD): (string, array of string, array of list of ref Strokes->Stroke)
+{
+	fp := bufio->fopen(fd, Bufio->OREAD);
+	(ok, nclasses) := getint(fp);
+	if(ok <= 0)
+		return ("missing number of classes", nil, nil);
+	(okc, cnames, examples) := read_classifier_points(fp, nclasses);
+	if(okc < 0)
+		return ("couldn't read examples", nil, nil);
+	return (nil, cnames, examples);
+}
+
+#
+# attempt to read the digest of a classifier,
+# and return its contents if successful;
+# return a diagnostic if not
+#
+read_digest(fd: ref Sys->FD): (string, array of string, array of ref Stroke)
+{
+	#  Read-in the name and dominant points for each class.
+	fp := bufio->fopen(fd, Bufio->OREAD);
+	cnames := array[32] of string;
+	dompts := array[32] of ref Stroke;
+	for(nclasses := 0;; nclasses++){
+		if(nclasses >= len cnames){
+			a := array[nclasses+32] of string;
+			a[0:] = cnames;
+			cnames = a;
+			b := array[nclasses+32] of ref Stroke;
+			b[0:] = dompts;
+			dompts = b;
+		}
+		(okn, class) := getstr(fp);
+		if(okn == Bufio->EOF)
+			break;
+		if(class == nil)
+			return ("expected class name", nil, nil);
+		cnames[nclasses] = class;
+		dpts := getpoints(fp);
+		if(dpts == nil)
+			return ("bad points list", nil, nil);
+		strokes->compute_chain_code(dpts);
+		dompts[nclasses] = dpts;
+	}
+	return (nil, cnames[0:nclasses], dompts[0:nclasses]);
+}
--- /dev/null
+++ b/appl/lib/strokes/strokes.b
@@ -1,0 +1,793 @@
+implement Strokes;
+
+#
+# this Limbo code is derived from C code that had the following
+# copyright notice, which i reproduce as requested
+#
+# li_recognizer.c
+#
+#	Copyright 2000 Compaq Computer Corporation.
+#	Copying or modifying this code for any purpose is permitted,
+#	provided that this copyright notice is preserved in its entirety
+#	in all copies or modifications.
+#	COMPAQ COMPUTER CORPORATION MAKES NO WARRANTIES, EXPRESSED OR
+#	IMPLIED, AS TO THE USEFULNESS OR CORRECTNESS OF THIS CODE OR
+#
+#
+# Adapted from cmu_recognizer.c by Jay Kistler.
+#
+# Where is the CMU copyright???? Gotta track it down - Jim Gettys
+#
+# Credit to Dean Rubine, Jim Kempf, and Ari Rapkin.
+#
+#
+# the copyright notice really did end in the middle of the sentence
+#
+
+#
+# Limbo version for Inferno by forsyth@vitanuova.com, Vita Nuova, September 2001
+#
+
+#
+# the code is reasonably close to the algorithms described in
+#	``On-line Handwritten Alphanumeric Character Recognition Using Dominant Stroke in Strokes'',
+#	Xiaolin Li and Dit-Yan Yueng, Department of Computer Science,
+#	Hong Kong University of Science and Technology, Hong Kong  (23 August 1996)
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "strokes.m";
+
+MAXINT: con 16r7FFFFFFF;
+
+# Dynamic programming parameters
+DP_BAND: con 3;
+SIM_THLD: con 60;	# x100
+#DIST_THLD: con 3200;	# x100
+DIST_THLD: con 3300;	# x100
+
+#  Low-pass filter parameters -- empirically derived
+LP_FILTER_WIDTH: con 6;
+LP_FILTER_ITERS: con 8;
+LP_FILTER_THLD: con 250;	# x100
+LP_FILTER_MIN: con 5;
+
+#  Pseudo-extrema parameters -- empirically derived
+PE_AL_THLD: con 1500;	# x100
+PE_ATCR_THLD: con 135;	# x100
+
+#  Pre-processing and canonicalization parameters
+CANONICAL_X: con 108;
+CANONICAL_Y: con 128;
+DIST_SQ_THRESHOLD: con 3*3;
+
+#  direction-code table; indexed by dx, dy
+dctbl := array[] of {array[] of {1, 0, 7}, array[] of {2, MAXINT, 6}, array[] of {3, 4, 5}};
+
+#  low-pass filter weights
+lpfwts := array[2 * LP_FILTER_WIDTH + 1] of int;
+lpfconst := -1;
+
+lidebug: con 0;
+stderr: ref Sys->FD;
+
+#		x := 0.04 * (i * i);
+#		wtvals[i] = floor(100.0 * exp(x));
+wtvals := array[] of {100, 104, 117, 143, 189, 271, 422};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	if(lidebug)
+		stderr = sys->fildes(2);
+	for(i := LP_FILTER_WIDTH; i >= 0; i--){
+		wt := wtvals[i];
+		lpfwts[LP_FILTER_WIDTH - i] = wt;
+		lpfwts[LP_FILTER_WIDTH + i] = wt;
+	}
+	lpfconst = 0;
+	for(i = 0; i < (2 * LP_FILTER_WIDTH + 1); i++)
+		lpfconst += lpfwts[i];
+}
+
+Stroke.new(n: int): ref Stroke
+{
+	return ref Stroke(n, array[n] of Penpoint, 0, 0);
+}
+
+Stroke.trim(ps: self ref Stroke, n: int)
+{
+	ps.npts = n;
+	ps.pts = ps.pts[0:n];
+}
+
+Stroke.copy(ps: self ref Stroke): ref Stroke
+{
+	n := ps.npts;
+	a := array[n] of Penpoint;
+	a[0:] = ps.pts[0:n];
+	return ref Stroke(n, a, ps.xrange, ps.yrange);
+}
+
+#
+# return the bounding box of a set of points
+# (note: unlike Draw->Rectangle, the region is closed)
+#
+Stroke.bbox(ps: self ref Stroke): (int, int, int, int)
+{
+	minx := maxx := ps.pts[0].x;
+	miny := maxy := ps.pts[0].y;
+	for(i := 1; i < ps.npts; i++){
+		pt := ps.pts[i];
+		if(pt.x < minx)
+			minx = pt.x;
+		if(pt.x > maxx)
+			maxx = pt.x;
+		if(pt.y < miny)
+			miny = pt.y;
+		if(pt.y > maxy)
+			maxy = pt.y;
+	}
+	return (minx, miny, maxx, maxy);	# warning: closed interval
+}
+
+Stroke.center(ps: self ref Stroke)
+{
+	(minx, miny, maxx, maxy) := ps.bbox();
+	ps.xrange = maxx-minx;
+	ps.yrange = maxy-miny;
+	avgxoff := -((CANONICAL_X - ps.xrange + 1) / 2);
+	avgyoff := -((CANONICAL_Y - ps.yrange + 1) / 2);
+	ps.translate(avgxoff, avgyoff, 100, 100);
+}
+
+Stroke.scaleup(ps: self ref Stroke): int
+{
+	(minx, miny, maxx, maxy) := ps.bbox();
+	xrange := maxx - minx;
+	yrange := maxy - miny;
+	scale: int;
+	if(((100 * xrange + CANONICAL_X / 2) / CANONICAL_X) >
+			 ((100 * yrange + CANONICAL_Y / 2) / CANONICAL_Y))
+		scale = (100 * CANONICAL_X + xrange / 2) / xrange;
+	else
+		scale = (100 * CANONICAL_Y + yrange / 2) / yrange;
+	ps.translate(minx, miny, scale, scale);
+	return scale;
+}
+
+#  scalex and scaley are x 100.
+#  Note that this does NOT update points.xrange and points.yrange!
+Stroke.translate(ps: self ref Stroke, minx: int, miny: int, scalex: int, scaley: int)
+{
+	for(i := 0; i < ps.npts; i++){
+		ps.pts[i].x = ((ps.pts[i].x - minx) * scalex + 50) / 100;
+		ps.pts[i].y = ((ps.pts[i].y - miny) * scaley + 50) / 100;
+	}
+}
+
+TAP_PATHLEN: con 10*100;		# x100
+
+Classifier.match(rec: self ref Classifier, stroke: ref Stroke): (int, string)
+{
+	if(stroke.npts < 1)
+		return (-1, nil);
+
+	#  Check for tap.
+
+	#  First thing is to filter out ``close points.''
+	stroke = stroke.filter();
+
+	#  Unfortunately, we don't have the actual time that each point
+	#  was recorded (i.e., dt is invalid).  Hence, we have to use a
+	#  heuristic based on total distance and the number of points.
+	if(stroke.npts == 1 || stroke.length() < TAP_PATHLEN)
+		return (-1, "tap");
+
+	preprocess_stroke(stroke);
+
+	#  Compute its dominant points.
+	dompts := stroke.interpolate().dominant();
+	best_dist := MAXDIST;
+	best_i := -1;
+	best_name: string;
+
+	#  Score input stroke against every class in classifier.
+	for(i := 0; i < len rec.cnames; i++){
+		name := rec.cnames[i];
+		(sim, dist) := score_stroke(dompts, rec.dompts[i]);
+		if(dist < MAXDIST)
+			sys->fprint(stderr, " (%s, %d, %d)", name, sim, dist);
+		if(dist < DIST_THLD){
+			if(lidebug)
+				sys->fprint(stderr, " (%s, %d, %d)", name, sim, dist);
+			#  Is it the best so far?
+			if(dist < best_dist){
+				best_dist = dist;
+				best_i = i;
+				best_name = name;
+			}
+		}
+	}
+
+	if(lidebug)
+		sys->fprint(stderr, "\n");
+	return (best_i, best_name);
+}
+
+preprocess_stroke(s: ref Stroke)
+{
+	#  Filter out points that are too close.
+	#  We did this earlier, when we checked for a tap.
+
+#	s = s.filter();
+
+
+#     assert(s.npts > 0);
+
+	#  Scale up to avoid conversion errors.
+	s.scaleup();
+
+	#  Center the stroke.
+	s.center();
+
+	if(lidebug){
+		(minx, miny, maxx, maxy) := s.bbox();
+		sys->fprint(stderr, "After pre-processing:  [ %d %d %d %d]\n",
+				minx, miny, maxx, maxy);
+		printpoints(stderr, s, "\n");
+	}
+}
+
+#
+# return the dominant points of Stroke s, assuming s has been through interpolation
+#
+Stroke.dominant(s: self ref Stroke): ref Stroke
+{
+	regions := s.regions();
+
+	#  Dominant points are: start, end, extrema of non plain regions, and midpoints of the preceding.
+	nonplain := 0;
+	for(r := regions; r != nil; r = r.next)
+		if(r.rtype != Rplain)
+			nonplain++;
+	dom := Stroke.new(1 + 2*nonplain + 2);
+
+	#  Pick out dominant points.
+
+	#  start point
+	dp := 0;
+	previx := 0;
+	dom.pts[dp++] = s.pts[previx];
+	currix: int;
+
+	cas := s.contourangles(regions);
+	for(r = regions; r != nil; r = r.next)
+		if(r.rtype != Rplain){
+			max_v := 0;
+			min_v := MAXINT;
+			max_ix := -1;
+			min_ix := -1;
+
+			for(i := r.start; i <= r.end; i++){
+				v := cas[i];
+				if(v > max_v){
+					max_v = v; max_ix = i;
+				}
+				if(v < min_v){
+					min_v = v; min_ix = i;
+				}
+				if(lidebug > 1)
+					sys->fprint(stderr, "  %d\n", v);
+			}
+			if(r.rtype == Rconvex)
+				currix = max_ix;
+			else
+				currix = min_ix;
+
+			dom.pts[dp++] = s.pts[(previx+currix)/2];	# midpoint
+			dom.pts[dp++] = s.pts[currix];	# extreme
+
+			previx = currix;
+		}
+
+	#  last midpoint, and end point
+	lastp := s.npts - 1;
+	dom.pts[dp++] = s.pts[(previx+lastp)/2];
+	dom.pts[dp++] = s.pts[lastp];
+	dom.trim(dp);
+
+	#  Compute chain-code.
+	compute_chain_code(dom);
+
+	return dom;
+}
+
+Stroke.contourangles(s: self ref Stroke, regions: ref Region): array of int
+{
+	V := array[s.npts] of int;
+	V[0] = 18000;
+	for(r := regions; r != nil; r = r.next){
+		for(i := r.start; i <= r.end; i++){
+			if(r.rtype == Rplain){
+				V[i] = 18000;
+			}else{
+				#  For now, simply choose the mid-point.
+				ismidpt := i == (r.start + r.end)/2;
+				if(ismidpt ^ (r.rtype!=Rconvex))
+					V[i] = 18000;
+				else
+					V[i] = 0;
+			}
+		}
+	}
+	V[s.npts - 1] = 18000;
+	return V;
+}
+
+Stroke.interpolate(s: self ref Stroke): ref Stroke
+{
+	#  Compute an upper-bound on the number of interpolated points
+	maxpts := s.npts;
+	for(i := 0; i < s.npts - 1; i++){
+		a := s.pts[i];
+		b := s.pts[i+1];
+		maxpts += abs(a.x - b.x) + abs(a.y - b.y);
+	}
+
+	#  Allocate an array of the maximum size
+	newpts := Stroke.new(maxpts);
+
+	#  Interpolate each of the segments.
+	j := 0;
+	for(i = 0; i < s.npts - 1; i++){
+		j = bresline(s.pts[i], s.pts[i+1], newpts, j);
+		j--;	#  end point gets recorded as start point of next segment!
+	}
+
+	#  Add-in last point and trim
+	newpts.pts[j++] = s.pts[s.npts - 1];
+	newpts.trim(j);
+
+	if(lidebug){
+		sys->fprint(stderr, "After interpolation:\n");
+		printpoints(stderr, newpts, "\n");
+	}
+
+	#  Compute the chain code for P (the list of points).
+	compute_unit_chain_code(newpts);
+
+	return newpts;
+}
+
+#  This implementation is due to an anonymous page on the net
+bresline(startpt: Penpoint, endpt: Penpoint, newpts: ref Stroke, j: int): int
+{
+	x0 := startpt.x;
+	x1 := endpt.x;
+	y0 := startpt.y;
+	y1 := endpt.y;
+
+	stepx := 1;
+	dx := x1-x0;
+	if(dx < 0){
+		dx = -dx;
+		stepx = -1;
+	}
+	dx <<= 1;
+	stepy := 1;
+	dy := y1-y0;
+	if(dy < 0){
+		dy = -dy;
+		stepy = -1;
+	}
+	dy <<= 1;
+	newpts.pts[j++] = (x0, y0, 0);
+	if(dx >= dy){
+		e := dy - (dx>>1);
+		while(x0 != x1){
+			if(e >= 0){
+				y0 += stepy;
+				e -= dx;
+			}
+			x0 += stepx;
+			e += dy;
+			newpts.pts[j++] = (x0, y0, 0);
+		}
+	}else{
+		e := dx - (dy>>1);
+		while(y0 != y1){
+			if(e >= 0){
+				x0 += stepx;
+				e -= dy;
+			}
+			y0 += stepy;
+			e += dx;
+			newpts.pts[j++] = (x0, y0, 0);
+		}
+	}
+	return j;
+}
+
+compute_chain_code(pts: ref Stroke)
+{
+	for(i := 0; i < pts.npts - 1; i++){
+		dx := pts.pts[i+1].x - pts.pts[i].x;
+		dy := pts.pts[i+1].y - pts.pts[i].y;
+		pts.pts[i].chaincode = (12 - quadr(likeatan(dy, dx))) % 8;
+	}
+}
+
+compute_unit_chain_code(pts: ref Stroke)
+{
+	for(i := 0; i < pts.npts - 1; i++){
+		dx := pts.pts[i+1].x - pts.pts[i].x;
+		dy := pts.pts[i+1].y - pts.pts[i].y;
+		pts.pts[i].chaincode = dctbl[dx+1][dy+1];
+	}
+}
+
+Stroke.regions(pts: self ref Stroke): ref Region
+{
+	#  Allocate a 2 x pts.npts array for use in computing the (filtered) Angle set, A_n.
+	R := array[] of {0 to LP_FILTER_ITERS+1 => array[pts.npts] of int};
+	curr := R[0];
+
+	#  Compute the Angle set, A, in the first element of array R.
+	#  Values in R are in degrees, x 100.
+	curr[0] = 18000;		#  a_0
+	for(i := 1; i < pts.npts - 1; i++){
+		d_i := pts.pts[i].chaincode;
+		d_iminusone := pts.pts[i-1].chaincode;
+		if(d_iminusone < d_i)
+			d_iminusone += 8;
+		a_i := (d_iminusone - d_i) % 8;
+		#  convert to degrees, x 100
+		curr[i] = ((12 - a_i) % 8) * 45 * 100;
+	}
+	curr[pts.npts-1] = 18000;	#  a_L-1
+
+	#  Perform a number of filtering iterations.
+	next := R[1];
+	for(j := 0; j < LP_FILTER_ITERS; ){
+		for(i = 0; i < pts.npts; i++){
+			next[i] = 0;
+			for(k := i - LP_FILTER_WIDTH; k <= i + LP_FILTER_WIDTH; k++){
+				oldval: int;
+				if(k < 0 || k >= pts.npts)
+					oldval = 18000;
+				else
+					oldval = curr[k];
+				next[i] += oldval * lpfwts[k - (i	- LP_FILTER_WIDTH)];	#  overflow?
+			}
+			next[i] /= lpfconst;
+		}
+		j++;
+		curr = R[j];
+		next = R[j+1];
+	}
+
+	#  Do final thresholding around PI.
+	#  curr and next are set-up correctly at end of previous loop!
+	for(i = 0; i < pts.npts; i++)
+		if(abs(curr[i] - 18000) < LP_FILTER_THLD)
+			next[i] = 18000;
+		else
+			next[i] = curr[i];
+	curr = next;
+
+	#  Debugging.
+	if(lidebug > 1){
+		for(i = 0; i < pts.npts; i++){
+			p := pts.pts[i];
+			sys->fprint(stderr, "%3d:  (%d %d)  %ud  ",
+				i, p.x, p.y, p.chaincode);
+			for(j = 0; j < 2 + LP_FILTER_ITERS; j++)
+				sys->fprint(stderr, "%d  ", R[j][i]);
+			sys->fprint(stderr, "\n");
+		}
+	}
+
+	#  Do the region segmentation.
+	r := regions := ref Region(regiontype(curr[0]), 0, 0, nil);
+	for(i = 1; i < pts.npts; i++){
+		t := regiontype(curr[i]);
+		if(t != r.rtype){
+			r.end = i-1;
+			if(lidebug > 1)
+				sys->fprint(stderr, "  (%d, %d) %d\n", r.start, r.end, r.rtype);
+			r.next = ref Region(t, i, 0, nil);
+			r = r.next;
+		}
+	}
+	r.end = i-1;
+	if(lidebug > 1)
+		sys->fprint(stderr, "  (%d, %d), %d\n", r.start, r.end, r.rtype);
+
+	#  Filter out convex/concave regions that are too short.
+	for(r = regions; r != nil; r = r.next)
+		if(r.rtype == Rplain){
+			while((nr := r.next) != nil && (nr.end - nr.start) < LP_FILTER_MIN){
+				#  nr must not be plain, and it must be followed by a plain
+				#  assert(nr.rtype != Rplain);
+				#  assert(nr.next != nil && (nr.next).rtype == Rplain);
+				if(nr.next == nil){
+					sys->fprint(stderr, "recog: nr.next==nil\n");	# can't happen
+					break;
+				}
+				r.next = nr.next.next;
+				r.end = nr.next.end;
+			}
+		}
+
+	#  Add-in pseudo-extremes.
+	for(r = regions; r != nil; r = r.next)
+		if(r.rtype == Rplain){
+			arclen := pts.pathlen(r.start, r.end);
+			dx := pts.pts[r.end].x - pts.pts[r.start].x;
+			dy := pts.pts[r.end].y - pts.pts[r.start].y;
+			chordlen := sqrt(100*100 * (dx*dx + dy*dy));
+			atcr := 0;
+			if(chordlen)
+				atcr = (100*arclen + chordlen/2) / chordlen;
+
+			if(lidebug)
+				sys->fprint(stderr, "%d, %d, %d\n", arclen, chordlen, atcr);
+
+			#  Split region if necessary.
+			if(arclen >= PE_AL_THLD && atcr >= PE_ATCR_THLD){
+				mid := (r.start + r.end)/2;
+				end := r.end;
+				r.end = mid - 1;
+				r = r.next = ref Region(Rpseudo, mid, mid,
+					ref Region(Rplain, mid+1, end, r.next));
+			}
+		}
+
+	return regions;
+}
+
+regiontype(val: int): int
+{
+	if(val == 18000)
+		return Rplain;
+	if(val < 18000)
+		return Rconcave;
+	return Rconvex;
+}
+
+#
+# return the similarity of two strokes and,
+# if similar, the distance between them;
+# if dissimilar, the distance is MAXDIST)
+#
+score_stroke(a: ref Stroke, b: ref Stroke): (int, int)
+{
+	sim := compute_similarity(a, b);
+	if(sim < SIM_THLD)
+		return (sim, MAXDIST);
+	return (sim, compute_distance(a, b));
+}
+
+compute_similarity(A: ref Stroke, B: ref Stroke): int
+{
+	#  A is the	longer sequence, length	N.
+	#  B is the shorter sequence, length M.
+	if(A.npts < B.npts){
+		t := A; A = B; B = t;
+	}
+	N := A.npts;
+	M := B.npts;
+
+	#  Allocate and initialize the Gain matrix, G.
+	#  The size of G is M x (N + 1).
+	#  Note that row 0 is unused.
+	#  Similarities are x 10.
+	G := array[M] of array of int;
+	for(i := 1; i < M; i++){
+		G[i] = array[N+1] of int;
+		bcode := B.pts[i-1].chaincode;
+
+		G[i][0] = 0;	# source column
+
+		for(j := 1; j < N; j++){
+			diff := abs(bcode - A.pts[j-1].chaincode);
+			if(diff > 4)
+				diff = 8 - diff;	# symmetry
+			v := 0;
+			if(diff == 0)
+				v = 10;
+			else if(diff == 1)
+				v = 6;
+			G[i][j] = v;
+		}
+
+		G[i][N] = 0;	# sink column
+	}
+
+	#  Do the DP algorithm.
+	#  Proceed in column order, from highest column to the lowest.
+	#  Within each column, proceed from the highest row to the lowest.
+	#  Skip the highest column.
+	for(j := N - 1; j >= 0; j--)
+		for(i = M - 1; i > 0; i--){
+			max := G[i][j + 1];
+			if(i < M-1){
+				t := G[i + 1][j + 1];
+				if(t > max)
+					max = t;
+			}
+			G[i][j] += max;
+		}
+
+	return (10*G[1][0] + (N-1)/2) / (N-1);
+}
+
+compute_distance(A: ref Stroke, B: ref Stroke): int
+{
+	#  A is the	longer sequence, length	N.
+	#  B is the shorter sequence, length M.
+	if(A.npts < B.npts){
+		t := A; A = B; B = t;
+	}
+	N := A.npts;
+	M := B.npts;
+
+	#  Construct the helper vectors, BE and TE, which say for each column
+	#  what are the ``bottom'' and ``top'' rows of interest.
+	BE := array[N+1] of int;
+	TE := array[N+1] of int;
+
+	for(j := 1; j <= N; j++){
+		bot := j + (M - DP_BAND);
+		if(bot > M) bot = M;
+		BE[j] = bot;
+
+		top := j - (N - DP_BAND);
+		if(top < 1) top = 1;
+		TE[j] = top;
+	}
+
+	#  Allocate and initialize the Cost matrix, C.
+	#  The size of C is (M + 1) x (N + 1).
+	#  Note that row and column 0 are unused.
+	#  Costs are x 100.
+	C := array[M+1] of array of int;
+	for(i := 1; i <= M; i++){
+		C[i] = array[N+1] of int;
+		bx := B.pts[i-1].x;
+		by := B.pts[i-1].y;
+
+		for(j = 1; j <= N; j++){
+			ax := A.pts[j-1].x;
+			ay := A.pts[j-1].y;
+			dx := bx - ax;
+			dy := by - ay;
+			dist := sqrt(10000 * (dx * dx + dy * dy));
+
+			C[i][j] = dist;
+		}
+	}
+
+	#  Do the DP algorithm.
+	#  Proceed in column order, from highest column to the lowest.
+	#  Within each column, proceed from the highest row to the lowest.
+	for(j = N; j > 0; j--)
+		for(i = M; i > 0; i--){
+			min := MAXDIST;
+			if(i > BE[j] || i < TE[j] || (j == N && i == M))
+				continue;
+			if(j < N){
+				if(i >= TE[j+1]){
+					tmp := C[i][j+1];
+					if(tmp < min)
+						min = tmp;
+				}
+				if(i < M){
+					tmp := C[i+1][j+1];
+					if(tmp < min)
+						min = tmp;
+				}
+			}
+			if(i < BE[j]){
+				tmp := C[i+1][j];
+				if(tmp < min)
+					min = tmp;
+			}
+			C[i][j] += min;
+		}
+	return (C[1][1] + N / 2) / N;	# dist
+}
+
+#  Result is x 100.
+Stroke.length(s: self ref Stroke): int
+{
+	return s.pathlen(0, s.npts-1);
+}
+
+#  Result is x 100.
+Stroke.pathlen(s: self ref Stroke, first: int, last: int): int
+{
+	l := 0;
+	for(i := first + 1; i <= last; i++){
+		dx := s.pts[i].x - s.pts[i-1].x;
+		dy := s.pts[i].y - s.pts[i-1].y;
+		l += sqrt(100*100 * (dx*dx + dy*dy));
+	}
+	return l;
+}
+
+#  Note that this does NOT update points.xrange and points.yrange!
+Stroke.filter(s: self ref Stroke): ref Stroke
+{
+	pts := array[s.npts] of Penpoint;
+	pts[0] = s.pts[0];
+	npts := 1;
+	for(i := 1; i < s.npts; i++){
+		j := npts - 1;
+		dx := s.pts[i].x - pts[j].x;
+		dy := s.pts[i].y - pts[j].y;
+		magsq := dx * dx + dy * dy;
+		if(magsq >= DIST_SQ_THRESHOLD){
+			pts[npts] = s.pts[i];
+			npts++;
+		}
+	}
+	return ref Stroke(npts, pts[0:npts], 0, 0);
+}
+
+abs(a: int): int
+{
+	if(a < 0)
+		return -a;
+	return a;
+}
+
+#  Code from Joseph Hall (jnhall@sat.mot.com).
+sqrt(n: int): int
+{
+	nn := n;
+	k0 := 2;
+	for(i := n; i > 0; i >>= 2)
+		k0 <<= 1;
+	nn <<= 2;
+	k1: int;
+	for(;;){
+		k1 = (nn / k0 + k0) >> 1;
+		if(((k0 ^ k1) & ~1) == 0)
+			break;
+		k0 = k1;
+	}
+	return (k1 + 1) >> 1;
+}
+
+#  Helper routines from Mark Hayter.
+likeatan(tantop: int, tanbot: int): int
+{
+	#  Use tan(theta)=top/bot -. order for t
+	#  t in range 0..16r40000
+
+	if(tantop == 0 && tantop == 0)
+		return 0;
+	t := (tantop << 16) / (abs(tantop) + abs(tanbot));
+	if(tanbot < 0)
+		t = 16r20000 - t;
+	else if(tantop < 0)
+		t = 16r40000 + t;
+	return t;
+}
+
+quadr(t: int): int
+{
+	return (8 - (((t + 16r4000) >> 15) & 7)) & 7;
+}
+
+printpoints(fd: ref Sys->FD, pts: ref Stroke, sep: string)
+{
+	for(j := 0; j < pts.npts; j++){
+		p := pts.pts[j];
+		sys->fprint(fd, "  (%d %d) %ud%s", p.x, p.y, pts.pts[j].chaincode, sep);
+	}
+}
--- /dev/null
+++ b/appl/lib/strokes/writestrokes.b
@@ -1,0 +1,68 @@
+implement Writestrokes;
+
+#
+# write structures to classifier files
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "strokes.m";
+	strokes: Strokes;
+	Penpoint, Stroke: import strokes;
+
+init(s: Strokes)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	strokes = s;
+}
+
+write_examples(fd: ref Sys->FD, names: array of string, examples: array of list of ref Stroke): string
+{
+	fp := bufio->fopen(fd, Bufio->OWRITE);
+	nclass := len names;
+	fp.puts(sys->sprint("%d\n", nclass));
+	for(i := 0; i < nclass; i++){
+		exl := examples[i];
+		fp.puts(sys->sprint("%d %s\n", len exl, names[i]));
+		for(; exl != nil; exl = tl exl){
+			putpoints(fp, hd exl);
+			fp.putc('\n');
+		}
+	}
+	if(fp.flush() == Bufio->ERROR)
+		return sys->sprint("write error: %r");
+	fp.close();
+	return nil;
+}
+
+write_digest(fd: ref Sys->FD, cnames: array of string, dompts: array of ref Stroke): string
+{
+	fp := bufio->fopen(fd, Bufio->OWRITE);
+	n := len cnames;
+	for(i := 0; i < n; i++){
+		d := dompts[i];
+		npts := d.npts;
+		fp.puts(cnames[i]);
+		putpoints(fp, d);
+		fp.putc('\n');
+	}
+	if(fp.flush() == Bufio->ERROR)
+		return sys->sprint("write error: %r");
+	fp.close();
+	return nil;
+}
+
+putpoints(fp: ref Iobuf, d: ref Stroke)
+{
+	fp.puts(sys->sprint(" %d", d.npts));
+	for(j := 0; j < d.npts; j++){
+		p := d.pts[j];
+		fp.puts(sys->sprint(" %d %d", p.x, p.y));
+	}
+}
--- /dev/null
+++ b/appl/lib/styx.b
@@ -1,0 +1,923 @@
+implement Styx;
+
+include "sys.m";
+	sys: Sys;
+
+include "styx.m";
+
+STR: con BIT16SZ;	# string length
+TAG: con BIT16SZ;
+FID: con BIT32SZ;
+QID: con BIT8SZ+BIT32SZ+BIT64SZ;
+LEN: con BIT16SZ;	# stat and qid array lengths
+COUNT: con BIT32SZ;
+OFFSET: con BIT64SZ;
+
+H: con BIT32SZ+BIT8SZ+BIT16SZ;	# minimum header length: size[4] type tag[2]
+
+#
+# the following array could be shorter if it were indexed by (type-Tversion)
+#
+hdrlen := array[Tmax] of
+{
+Tversion =>	H+COUNT+STR,	# size[4] Tversion tag[2] msize[4] version[s]
+Rversion =>	H+COUNT+STR,	# size[4] Rversion tag[2] msize[4] version[s]
+
+Tauth =>	H+FID+STR+STR,		# size[4] Tauth tag[2] afid[4] uname[s] aname[s]
+Rauth =>	H+QID,			# size[4] Rauth tag[2] aqid[13]
+
+Rerror =>	H+STR,		# size[4] Rerror tag[2] ename[s]
+
+Tflush =>	H+TAG,		# size[4] Tflush tag[2] oldtag[2]
+Rflush =>	H,			# size[4] Rflush tag[2]
+
+Tattach =>	H+FID+FID+STR+STR,	# size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s]
+Rattach =>	H+QID,		# size[4] Rattach tag[2] qid[13]
+
+Twalk =>	H+FID+FID+LEN,	# size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s])
+Rwalk =>	H+LEN,		# size[4] Rwalk tag[2] nwqid[2] nwqid*(wqid[13])
+
+Topen =>	H+FID+BIT8SZ,		# size[4] Topen tag[2] fid[4] mode[1]
+Ropen =>	H+QID+COUNT,	# size[4] Ropen tag[2] qid[13] iounit[4]
+
+Tcreate =>	H+FID+STR+BIT32SZ+BIT8SZ,	# size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1]
+Rcreate =>	H+QID+COUNT,	# size[4] Rcreate tag[2] qid[13] iounit[4]
+
+Tread =>	H+FID+OFFSET+COUNT,	# size[4] Tread tag[2] fid[4] offset[8] count[4]
+Rread =>	H+COUNT,		# size[4] Rread tag[2] count[4] data[count]
+
+Twrite =>	H+FID+OFFSET+COUNT,	# size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count]
+Rwrite =>	H+COUNT,	# size[4] Rwrite tag[2] count[4]
+
+Tclunk =>	H+FID,	# size[4] Tclunk tag[2] fid[4]
+Rclunk =>	H,		# size[4] Rclunk tag[2]
+
+Tremove =>	H+FID,	# size[4] Tremove tag[2] fid[4]
+Rremove =>	H,	# size[4] Rremove tag[2]
+
+Tstat =>	H+FID,	# size[4] Tstat tag[2] fid[4]
+Rstat =>	H+LEN,	# size[4] Rstat tag[2] stat[n]
+
+Twstat =>	H+FID+LEN,	# size[4] Twstat tag[2] fid[4] stat[n]
+Rwstat =>	H,	# size[4] Rwstat tag[2]
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+utflen(s: string): int
+{
+	# the domain is 21-bit unicode
+	n := l := len s;
+	for(i:=0; i<l; i++)
+		if((c := s[i]) > 16r7F){
+			n++;
+			if(c > 16r7FF){
+				n++;
+				if(c > 16rFFFF)
+					n++;
+			}
+		}
+	return n;
+}
+
+packdirsize(d: Sys->Dir): int
+{
+	return STATFIXLEN+utflen(d.name)+utflen(d.uid)+utflen(d.gid)+utflen(d.muid);
+}
+
+packdir(f: Sys->Dir): array of byte
+{
+	ds := packdirsize(f);
+	a := array[ds] of byte;
+	# size[2]
+	a[0] = byte (ds-LEN);
+	a[1] = byte ((ds-LEN)>>8);
+	# type[2]
+	a[2] = byte f.dtype;
+	a[3] = byte (f.dtype>>8);
+	# dev[4]
+	a[4] = byte f.dev;
+	a[5] = byte (f.dev>>8);
+	a[6] = byte (f.dev>>16);
+	a[7] = byte (f.dev>>24);
+	# qid.type[1]
+	# qid.vers[4]
+	# qid.path[8]
+	pqid(a, 8, f.qid);
+	# mode[4]
+	a[21] = byte f.mode;
+	a[22] = byte (f.mode>>8);
+	a[23] = byte (f.mode>>16);
+	a[24] = byte (f.mode>>24);
+	# atime[4]
+	a[25] = byte f.atime;
+	a[26] = byte (f.atime>>8);
+	a[27] = byte (f.atime>>16);
+	a[28] = byte (f.atime>>24);
+	# mtime[4]
+	a[29] = byte f.mtime;
+	a[30] = byte (f.mtime>>8);
+	a[31] = byte (f.mtime>>16);
+	a[32] = byte (f.mtime>>24);
+	# length[8]
+	p64(a, 33, big f.length);
+	# name[s]
+	i := pstring(a, 33+BIT64SZ, f.name);
+	i = pstring(a, i, f.uid);
+	i = pstring(a, i, f.gid);
+	i = pstring(a, i, f.muid);
+	if(i != len a)
+		raise "assertion: Styx->packdir: bad count";	# can't happen unless packedsize is wrong
+	return a;
+}
+
+pqid(a: array of byte, o: int, q: Sys->Qid): int
+{
+	a[o] = byte q.qtype;
+	v := q.vers;
+	a[o+1] = byte v;
+	a[o+2] = byte (v>>8);
+	a[o+3] = byte (v>>16);
+	a[o+4] = byte (v>>24);
+	v = int q.path;
+	a[o+5] = byte v;
+	a[o+6] = byte (v>>8);
+	a[o+7] = byte (v>>16);
+	a[o+8] = byte (v>>24);
+	v = int (q.path >> 32);
+	a[o+9] = byte v;
+	a[o+10] = byte (v>>8);
+	a[o+11] = byte (v>>16);
+	a[o+12] = byte (v>>24);
+	return o+QID;
+}
+
+pstring(a: array of byte, o: int, s: string): int
+{
+	sa := array of byte s;	# could do conversion ourselves
+	n := len sa;
+	a[o] = byte n;
+	a[o+1] = byte (n>>8);
+	a[o+2:] = sa;
+	return o+LEN+n;
+}
+
+p32(a: array of byte, o: int, v: int): int
+{
+	a[o] = byte v;
+	a[o+1] = byte (v>>8);
+	a[o+2] = byte (v>>16);
+	a[o+3] = byte (v>>24);
+	return o+BIT32SZ;
+}
+
+p64(a: array of byte, o: int, b: big): int
+{
+	i := int b;
+	a[o] = byte i;
+	a[o+1] = byte (i>>8);
+	a[o+2] = byte (i>>16);
+	a[o+3] = byte (i>>24);
+	i = int (b>>32);
+	a[o+4] = byte i;
+	a[o+5] = byte (i>>8);
+	a[o+6] = byte (i>>16);
+	a[o+7] = byte (i>>24);
+	return o+BIT64SZ;
+}
+
+unpackdir(a: array of byte): (int, Sys->Dir)
+{
+	dir: Sys->Dir;
+
+	if(len a < STATFIXLEN)
+		return (0, dir);
+	# size[2]
+	sz := ((int a[1] << 8) | int a[0])+LEN;	# bytes this packed dir should occupy
+	if(len a < sz)
+		return (0, dir);
+	# type[2]
+	dir.dtype = (int a[3]<<8) | int a[2];
+	# dev[4]
+	dir.dev = (((((int a[7] << 8) | int a[6]) << 8) | int a[5]) << 8) | int a[4];
+	# qid.type[1]
+	# qid.vers[4]
+	# qid.path[8]
+	dir.qid = gqid(a, 8);
+	# mode[4]
+	dir.mode = (((((int a[24] << 8) | int a[23]) << 8) | int a[22]) << 8) | int a[21];
+	# atime[4]
+	dir.atime = (((((int a[28] << 8) | int a[27]) << 8) | int a[26]) << 8) | int a[25];
+	# mtime[4]
+	dir.mtime = (((((int a[32] << 8) | int a[31]) << 8) | int a[30]) << 8) | int a[29];
+	# length[8]
+	v0 := (((((int a[36] << 8) | int a[35]) << 8) | int a[34]) << 8) | int a[33];
+	v1 := (((((int a[40] << 8) | int a[39]) << 8) | int a[38]) << 8) | int a[37];
+	dir.length = (big v1 << 32) | (big v0 & 16rFFFFFFFF);
+	# name[s], uid[s], gid[s], muid[s]
+	i: int;
+	(dir.name, i) = gstring(a, 41);
+	(dir.uid, i) = gstring(a, i);
+	(dir.gid, i) = gstring(a, i);
+	(dir.muid, i) = gstring(a, i);
+	if(i != sz)
+		return (0, dir);
+	return (i, dir);
+}
+
+gqid(f: array of byte, i: int): Sys->Qid
+{
+	qtype := int f[i];
+	vers := (((((int f[i+4] << 8) | int f[i+3]) << 8) | int f[i+2]) << 8) | int f[i+1];
+	i += BIT8SZ+BIT32SZ;
+	path0 := (((((int f[i+3] << 8) | int f[i+2]) << 8) | int f[i+1]) << 8) | int f[i];
+	i += BIT32SZ;
+	path1 := (((((int f[i+3] << 8) | int f[i+2]) << 8) | int f[i+1]) << 8) | int f[i];
+	path := (big path1 << 32) | (big path0 & 16rFFFFFFFF);
+	return (path, vers, qtype);
+}
+
+g32(f: array of byte, i: int): int
+{
+	return (((((int f[i+3] << 8) | int f[i+2]) << 8) | int f[i+1]) << 8) | int f[i];
+}
+
+g64(f: array of byte, i: int): big
+{
+	b0 := (((((int f[i+3] << 8) | int f[i+2]) << 8) | int f[i+1]) << 8) | int f[i];
+	b1 := (((((int f[i+7] << 8) | int f[i+6]) << 8) | int f[i+5]) << 8) | int f[i+4];
+	return (big b1 << 32) | (big b0 & 16rFFFFFFFF);
+}
+
+gstring(a: array of byte, o: int): (string, int)
+{
+	if(o < 0 || o+STR > len a)
+		return (nil, -1);
+	l := (int a[o+1] << 8) | int a[o];
+	o += STR;
+	e := o+l;
+	if(e > len a)
+		return (nil, -1);
+	return (string a[o:e], e);
+}
+
+ttag2type := array[] of {
+tagof Tmsg.Readerror => 0,
+tagof Tmsg.Version => Tversion,
+tagof Tmsg.Auth => Tauth,
+tagof Tmsg.Attach => Tattach,
+tagof Tmsg.Flush => Tflush,
+tagof Tmsg.Walk => Twalk,
+tagof Tmsg.Open => Topen,
+tagof Tmsg.Create => Tcreate,
+tagof Tmsg.Read => Tread,
+tagof Tmsg.Write => Twrite,
+tagof Tmsg.Clunk => Tclunk,
+tagof Tmsg.Stat => Tstat,
+tagof Tmsg.Remove => Tremove,
+tagof Tmsg.Wstat => Twstat,
+};
+
+Tmsg.mtype(t: self ref Tmsg): int
+{
+	return ttag2type[tagof t];
+}
+
+Tmsg.packedsize(t: self ref Tmsg): int
+{
+	mtype := ttag2type[tagof t];
+	if(mtype <= 0)
+		return 0;
+	ml := hdrlen[mtype];
+	pick m := t {
+	Version =>
+		ml += utflen(m.version);
+	Auth =>
+		ml += utflen(m.uname)+utflen(m.aname);
+	Attach =>
+		ml += utflen(m.uname)+utflen(m.aname);
+	Walk =>
+		for(i:=0; i<len m.names; i++)
+			ml += STR+utflen(m.names[i]);
+	Create =>
+		ml += utflen(m.name);
+	Write =>
+		ml += len m.data;
+	Wstat =>
+		ml += packdirsize(m.stat);
+	}
+	return ml;
+}
+
+Tmsg.pack(t: self ref Tmsg): array of byte
+{
+	if(t == nil)
+		return nil;
+	ds := t.packedsize();
+	if(ds <= 0)
+		return nil;
+	d := array[ds] of byte;
+	d[0] = byte ds;
+	d[1] = byte (ds>>8);
+	d[2] = byte (ds>>16);
+	d[3] = byte (ds>>24);
+	d[4] = byte ttag2type[tagof t];
+	d[5] = byte t.tag;
+	d[6] = byte (t.tag >> 8);
+	pick m := t {
+	Version =>
+		p32(d, H, m.msize);
+		pstring(d, H+COUNT, m.version);
+	Auth =>
+		p32(d, H, m.afid);
+		o := pstring(d, H+FID, m.uname);
+		pstring(d, o, m.aname);
+	Flush =>
+		v := m.oldtag;
+		d[H] = byte v;
+		d[H+1] = byte (v>>8);
+	Attach =>
+		p32(d, H, m.fid);
+		p32(d, H+FID, m.afid);
+		o := pstring(d, H+2*FID, m.uname);
+		pstring(d, o, m.aname);
+	Walk =>
+		d[H] = byte m.fid;
+		d[H+1] = byte (m.fid>>8);
+		d[H+2] = byte (m.fid>>16);
+		d[H+3] = byte (m.fid>>24);
+		d[H+FID] = byte m.newfid;
+		d[H+FID+1] = byte (m.newfid>>8);
+		d[H+FID+2] = byte (m.newfid>>16);
+		d[H+FID+3] = byte (m.newfid>>24);
+		n := len m.names;
+		d[H+2*FID] = byte n;
+		d[H+2*FID+1] = byte (n>>8);
+		o := H+2*FID+LEN;
+		for(i := 0; i < n; i++)
+			o = pstring(d, o, m.names[i]);
+	Open =>
+		p32(d, H, m.fid);
+		d[H+FID] = byte m.mode;
+	Create =>
+		p32(d, H, m.fid);
+		o := pstring(d, H+FID, m.name);
+		p32(d, o, m.perm);
+		d[o+BIT32SZ] = byte m.mode;
+	Read =>
+		p32(d, H, m.fid);
+		p64(d, H+FID, m.offset);
+		p32(d, H+FID+OFFSET, m.count);
+	Write =>
+		p32(d, H, m.fid);
+		p64(d, H+FID, m.offset);
+		n := len m.data;
+		p32(d, H+FID+OFFSET, n);
+		d[H+FID+OFFSET+COUNT:] = m.data;
+	Clunk or Remove or Stat =>
+		p32(d, H, m.fid);
+	Wstat =>
+		p32(d, H, m.fid);
+		stat := packdir(m.stat);
+		n := len stat;
+		d[H+FID] = byte n;
+		d[H+FID+1] = byte (n>>8);
+		d[H+FID+LEN:] = stat;
+	* =>
+		raise sys->sprint("assertion: Styx->Tmsg.pack: bad tag: %d", tagof t);
+	}
+	return d;
+}
+
+Tmsg.unpack(f: array of byte): (int, ref Tmsg)
+{
+	if(len f < H)
+		return (0, nil);
+	size := (int f[1] << 8) | int f[0];
+	size |= ((int f[3] << 8) | int f[2]) << 16;
+	if(len f != size){
+		if(len f < size)
+			return (0, nil);	# need more data
+		f = f[0:size];	# trim to exact length
+	}
+	mtype := int f[4];
+	if(mtype >= len hdrlen || (mtype&1) != 0 || size < hdrlen[mtype])
+		return (-1, nil);
+
+	tag := (int f[6] << 8) | int f[5];
+	fid := 0;
+	if(hdrlen[mtype] >= H+FID)
+		fid = g32(f, H);	# fid is always in same place: extract it once for all if there
+
+	# return out of each case body for a legal message;
+	# break out of the case for an illegal one
+
+Decode:
+	case mtype {
+	* =>
+		sys->print("styx: Tmsg.unpack: bad type %d\n", mtype);
+	Tversion =>
+		msize := fid;
+		(version, o) := gstring(f, H+COUNT);
+		if(o <= 0)
+			break;
+		return (o, ref Tmsg.Version(tag, msize, version));
+	Tauth =>
+		(uname, o1) := gstring(f, H+FID);
+		(aname, o2) := gstring(f, o1);
+		if(o2 <= 0)
+			break;
+		return (o2, ref Tmsg.Auth(tag, fid, uname, aname));
+	Tflush =>
+		oldtag := (int f[H+1] << 8) | int f[H];
+		return (H+TAG, ref Tmsg.Flush(tag, oldtag));
+	Tattach =>
+		afid := g32(f, H+FID);
+		(uname, o1) := gstring(f, H+2*FID);
+		(aname, o2) := gstring(f, o1);
+		if(o2 <= 0)
+			break;
+		return (o2, ref Tmsg.Attach(tag, fid, afid, uname, aname));
+	Twalk =>
+		newfid := g32(f, H+FID);
+		n := (int f[H+2*FID+1] << 8) | int f[H+2*FID];
+		if(n > MAXWELEM)
+			break;
+		o := H+2*FID+LEN;
+		names: array of string = nil;
+		if(n > 0){
+			names = array[n] of string;
+			for(i:=0; i<n; i++){
+				(names[i], o) = gstring(f, o);
+				if(o <= 0)
+					break Decode;
+			}
+		}
+		return (o, ref Tmsg.Walk(tag, fid, newfid, names));
+	Topen =>
+		return (H+FID+BIT8SZ, ref Tmsg.Open(tag, fid, int f[H+FID]));
+	Tcreate =>
+		(name, o) := gstring(f, H+FID);
+		if(o <= 0 || o+BIT32SZ+BIT8SZ > len f)
+			break;
+		perm := g32(f, o);
+		o += BIT32SZ;
+		mode := int f[o++];
+		return (o, ref Tmsg.Create(tag, fid, name, perm, mode));
+	Tread =>
+		offset := g64(f, H+FID);
+		count := g32(f, H+FID+OFFSET);
+		return (H+FID+OFFSET+COUNT, ref Tmsg.Read(tag, fid, offset, count));
+	Twrite =>
+		offset := g64(f, H+FID);
+		count := g32(f, H+FID+OFFSET);
+		O: con H+FID+OFFSET+COUNT;
+		if(count > len f-O)
+			break;
+		data := f[O:O+count];
+		return (O+count, ref Tmsg.Write(tag, fid, offset, data));
+	Tclunk =>
+		return (H+FID, ref Tmsg.Clunk(tag, fid));
+	Tremove =>
+		return (H+FID, ref Tmsg.Remove(tag, fid));
+	Tstat =>
+		return (H+FID, ref Tmsg.Stat(tag, fid));
+	Twstat =>
+		n := int (f[H+FID+1]<<8) | int f[H+FID];
+		if(len f < H+FID+LEN+n)
+			break;
+		(ds, stat) := unpackdir(f[H+FID+LEN:]);
+		if(ds != n){
+			sys->print("Styx->Tmsg.unpack: wstat count: %d/%d\n", ds, n);	# temporary
+			break;
+		}
+		return (H+FID+LEN+n, ref Tmsg.Wstat(tag, fid, stat));
+	}
+	return (-1, nil);		# illegal
+}
+
+tmsgname := array[] of {
+tagof Tmsg.Readerror => "Readerror",
+tagof Tmsg.Version => "Version",
+tagof Tmsg.Auth => "Auth",
+tagof Tmsg.Attach => "Attach",
+tagof Tmsg.Flush => "Flush",
+tagof Tmsg.Walk => "Walk",
+tagof Tmsg.Open => "Open",
+tagof Tmsg.Create => "Create",
+tagof Tmsg.Read => "Read",
+tagof Tmsg.Write => "Write",
+tagof Tmsg.Clunk => "Clunk",
+tagof Tmsg.Stat => "Stat",
+tagof Tmsg.Remove => "Remove",
+tagof Tmsg.Wstat => "Wstat",
+};
+
+Tmsg.text(t: self ref Tmsg): string
+{
+	if(t == nil)
+		return "nil";
+	s := sys->sprint("Tmsg.%s(%ud", tmsgname[tagof t], t.tag);
+	pick m:= t {
+	* =>
+		return s + ",ILLEGAL)";
+	Readerror =>
+		return s + sys->sprint(",\"%s\")", m.error);
+	Version =>
+		return s + sys->sprint(",%d,\"%s\")", m.msize, m.version);
+	Auth =>
+		return s + sys->sprint(",%ud,\"%s\",\"%s\")", m.afid, m.uname, m.aname);
+	Flush =>
+		return s + sys->sprint(",%ud)", m.oldtag);
+	Attach =>
+		return s + sys->sprint(",%ud,%ud,\"%s\",\"%s\")", m.fid, m.afid, m.uname, m.aname);
+	Walk =>
+		s += sys->sprint(",%ud,%ud", m.fid, m.newfid);
+		if(len m.names != 0){
+			s += ",array[] of {";
+			for(i := 0; i < len m.names; i++){
+				c := ",";
+				if(i == 0)
+					c = "";
+				s += sys->sprint("%s\"%s\"", c, m.names[i]);
+			}
+			s += "}";
+		}else
+			s += ",nil";
+		return s + ")";
+	Open =>
+		return s + sys->sprint(",%ud,%d)", m.fid, m.mode);
+	Create =>
+		return s + sys->sprint(",%ud,\"%s\",8r%uo,%d)", m.fid, m.name, m.perm, m.mode);
+	Read =>
+		return s + sys->sprint(",%ud,%bd,%ud)", m.fid, m.offset, m.count);
+	Write =>
+		return s + sys->sprint(",%ud,%bd,array[%d] of byte)", m.fid, m.offset, len m.data);
+	Clunk or
+	Remove or
+	Stat =>
+		return s + sys->sprint(",%ud)", m.fid);
+	Wstat =>
+		return s + sys->sprint(",%ud,%s)", m.fid, dir2text(m.stat));
+	}
+}
+
+Tmsg.read(fd: ref Sys->FD, msglim: int): ref Tmsg
+{
+	(msg, err) := readmsg(fd, msglim);
+	if(err != nil)
+		return ref Tmsg.Readerror(0, err);
+	if(msg == nil)
+		return nil;
+	(nil, m) := Tmsg.unpack(msg);
+	if(m == nil)
+		return ref Tmsg.Readerror(0, "bad 9P T-message format");
+	return m;
+}
+
+rtag2type := array[] of {
+tagof Rmsg.Version	=> Rversion,
+tagof Rmsg.Auth	=> Rauth,
+tagof Rmsg.Error	=> Rerror,
+tagof Rmsg.Flush	=> Rflush,
+tagof Rmsg.Attach	=> Rattach,
+tagof Rmsg.Walk	=> Rwalk,
+tagof Rmsg.Open	=> Ropen,
+tagof Rmsg.Create	=> Rcreate,
+tagof Rmsg.Read	=> Rread,
+tagof Rmsg.Write	=> Rwrite,
+tagof Rmsg.Clunk	=> Rclunk,
+tagof Rmsg.Remove	=> Rremove,
+tagof Rmsg.Stat	=> Rstat,
+tagof Rmsg.Wstat	=> Rwstat,
+};
+
+Rmsg.mtype(r: self ref Rmsg): int
+{
+	return rtag2type[tagof r];
+}
+
+Rmsg.packedsize(r: self ref Rmsg): int
+{
+	mtype := rtag2type[tagof r];
+	if(mtype <= 0)
+		return 0;
+	ml := hdrlen[mtype];
+	pick m := r {
+	Version =>
+		ml += utflen(m.version);
+	Error =>
+		ml += utflen(m.ename);
+	Walk =>
+		ml += QID*len m.qids;
+	Read =>
+		ml += len m.data;
+	Stat =>
+		ml += packdirsize(m.stat);
+	}
+	return ml;
+}
+
+Rmsg.pack(r: self ref Rmsg): array of byte
+{
+	if(r == nil)
+		return nil;
+	ps := r.packedsize();
+	if(ps <= 0)
+		return nil;
+	d := array[ps] of byte;
+	d[0] = byte ps;
+	d[1] = byte (ps>>8);
+	d[2] = byte (ps>>16);
+	d[3] = byte (ps>>24);
+	d[4] = byte rtag2type[tagof r];
+	d[5] = byte r.tag;
+	d[6] = byte (r.tag >> 8);
+	pick m := r {
+	Version =>
+		p32(d, H, m.msize);
+		pstring(d, H+BIT32SZ, m.version);
+	Auth =>
+		pqid(d, H, m.aqid);
+	Flush or
+	Clunk or
+	Remove or
+	Wstat =>
+		;	# nothing more required
+	Error	=>
+		pstring(d, H, m.ename);
+	Attach =>
+		pqid(d, H, m.qid);
+	Walk =>
+		n := len m.qids;
+		d[H] = byte n;
+		d[H+1] = byte (n>>8);
+		o := H+LEN;
+		for(i:=0; i<n; i++){
+			pqid(d, o, m.qids[i]);
+			o += QID;
+		}
+	Create or
+	Open =>
+		pqid(d, H, m.qid);
+		p32(d, H+QID, m.iounit);
+	Read =>
+		v := len m.data;
+		d[H] = byte v;
+		d[H+1] = byte (v>>8);
+		d[H+2] = byte (v>>16);
+		d[H+3] = byte (v>>24);
+		d[H+4:] = m.data;
+	Write =>
+		v := m.count;
+		d[H] = byte v;
+		d[H+1] = byte (v>>8);
+		d[H+2] = byte (v>>16);
+		d[H+3] = byte (v>>24);
+	Stat =>
+		stat := packdir(m.stat);
+		v := len stat;
+		d[H] = byte v;
+		d[H+1] = byte (v>>8);
+		d[H+2:] = stat;		# should avoid copy?
+	* =>
+		raise sys->sprint("assertion: Styx->Rmsg.pack: missed case: tag %d", tagof r);
+	}
+	return d;
+}
+
+Rmsg.unpack(f: array of byte): (int, ref Rmsg)
+{
+	if(len f < H)
+		return (0, nil);
+	size := (int f[1] << 8) | int f[0];
+	size |= ((int f[3] << 8) | int f[2]) << 16;	# size includes itself
+	if(len f != size){
+		if(len f < size)
+			return (0, nil);	# need more data
+		f = f[0:size];	# trim to exact length
+	}
+	mtype := int f[4];
+	if(mtype >= len hdrlen || (mtype&1) == 0 || size < hdrlen[mtype])
+		return (-1, nil);
+
+	tag := (int f[6] << 8) | int f[5];
+
+	# return out of each case body for a legal message;
+	# break out of the case for an illegal one
+
+	case mtype {
+	* =>
+		sys->print("Styx->Rmsg.unpack: bad type %d\n", mtype);	# temporary
+	Rversion =>
+		msize := g32(f, H);
+		(version, o) := gstring(f, H+BIT32SZ);
+		if(o <= 0)
+			break;
+		return (o, ref Rmsg.Version(tag, msize, version));
+	Rauth =>
+		return (H+QID, ref Rmsg.Auth(tag, gqid(f, H)));
+	Rflush =>
+		return (H, ref Rmsg.Flush(tag));
+	Rerror =>
+		(ename, o) := gstring(f, H);
+		if(o <= 0)
+			break;
+		return (o, ref Rmsg.Error(tag, ename));
+	Rclunk =>
+		return (H, ref Rmsg.Clunk(tag));
+	Rremove =>
+		return (H, ref Rmsg.Remove(tag));
+	Rwstat=>
+		return (H, ref Rmsg.Wstat(tag));
+	Rattach =>
+		return (H+QID, ref Rmsg.Attach(tag, gqid(f, H)));
+	Rwalk =>
+		nqid := (int f[H+1] << 8) | int f[H];
+		if(len f < H+LEN+nqid*QID)
+			break;
+		o := H+LEN;
+		qids := array[nqid] of Sys->Qid;
+		for(i:=0; i<nqid; i++){
+			qids[i] = gqid(f, o);
+			o += QID;
+		}
+		return (o, ref Rmsg.Walk(tag, qids));
+	Ropen =>
+		return (H+QID+COUNT, ref Rmsg.Open(tag, gqid(f, H), g32(f, H+QID)));
+	Rcreate=>
+		return (H+QID+COUNT, ref Rmsg.Create(tag, gqid(f, H), g32(f, H+QID)));
+	Rread =>
+		count := g32(f, H);
+		if(len f < H+COUNT+count)
+			break;
+		data := f[H+COUNT:H+COUNT+count];
+		return (H+COUNT+count, ref Rmsg.Read(tag, data));
+	Rwrite =>
+		return (H+COUNT, ref Rmsg.Write(tag, g32(f, H)));
+	Rstat =>
+		n := (int f[H+1] << 8) | int f[H];
+		if(len f < H+LEN+n)
+			break;
+		(ds, d) := unpackdir(f[H+LEN:]);
+		if(ds <= 0)
+			break;
+		if(ds != n){
+			sys->print("Styx->Rmsg.unpack: stat count: %d/%d\n", ds, n);		# temporary
+			break;
+		}
+		return (H+LEN+n, ref Rmsg.Stat(tag, d));
+	}
+	return (-1, nil);		# illegal
+}
+
+rmsgname := array[] of {
+tagof Rmsg.Version => "Version",
+tagof Rmsg.Auth => "Auth",
+tagof Rmsg.Attach => "Attach",
+tagof Rmsg.Error => "Error",
+tagof Rmsg.Flush => "Flush",
+tagof Rmsg.Walk => "Walk",
+tagof Rmsg.Create => "Create",
+tagof Rmsg.Open => "Open",
+tagof Rmsg.Read => "Read",
+tagof Rmsg.Write => "Write",
+tagof Rmsg.Clunk => "Clunk",
+tagof Rmsg.Remove => "Remove",
+tagof Rmsg.Stat => "Stat",
+tagof Rmsg.Wstat => "Wstat",
+};
+
+Rmsg.text(r: self ref Rmsg): string
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(r == nil)
+		return "nil";
+	s := sys->sprint("Rmsg.%s(%ud", rmsgname[tagof r], r.tag);
+	pick m := r {
+	* =>
+		return s + "ERROR)";
+	Readerror =>
+		return s + sys->sprint(",\"%s\")", m.error);
+	Version =>
+		return s + sys->sprint(",%d,\"%s\")", m.msize, m.version);
+	Auth =>
+		return s+sys->sprint(",%s)", qid2text(m.aqid));
+	Error =>
+		return s+sys->sprint(",\"%s\")", m.ename);
+	Flush or
+	Clunk or
+	Remove or
+	Wstat =>
+		return s+")";
+	Attach =>
+		return s+sys->sprint(",%s)", qid2text(m.qid));
+	Walk	 =>
+		s += ",array[] of {";
+		for(i := 0; i < len m.qids; i++){
+			c := "";
+			if(i != 0)
+				c = ",";
+			s += sys->sprint("%s%s", c, qid2text(m.qids[i]));
+		}
+		return s+"})";
+	Create or
+	Open =>
+		return s+sys->sprint(",%s,%d)", qid2text(m.qid), m.iounit);
+	Read =>
+		return s+sys->sprint(",array[%d] of byte)", len m.data);
+	Write =>
+		return s+sys->sprint(",%d)", m.count);
+	Stat =>
+		return s+sys->sprint(",%s)", dir2text(m.stat));
+	}
+}
+
+Rmsg.read(fd: ref Sys->FD, msglim: int): ref Rmsg
+{
+	(msg, err) := readmsg(fd, msglim);
+	if(err != nil)
+		return ref Rmsg.Readerror(0, err);
+	if(msg == nil)
+		return nil;
+	(nil, m) := Rmsg.unpack(msg);
+	if(m == nil)
+		return ref Rmsg.Readerror(0, "bad 9P R-message format");
+	return m;
+}
+
+dir2text(d: Sys->Dir): string
+{
+	return sys->sprint("Dir(\"%s\",\"%s\",\"%s\",%s,8r%uo,%d,%d,%bd,16r%ux,%d)",
+		d.name, d.uid, d.gid, qid2text(d.qid), d.mode, d.atime, d.mtime, d.length, d.dtype, d.dev);
+}
+
+qid2text(q: Sys->Qid): string
+{
+	return sys->sprint("Qid(16r%ubx,%d,16r%.2ux)", q.path, q.vers, q.qtype);
+}
+
+readmsg(fd: ref Sys->FD, msglim: int): (array of byte, string)
+{
+	if(msglim <= 0)
+		msglim = MAXRPC;
+	sbuf := array[BIT32SZ] of byte;
+	if((n := sys->readn(fd, sbuf, BIT32SZ)) != BIT32SZ){
+		if(n == 0)
+			return (nil, nil);
+		return (nil, sys->sprint("%r"));
+	}
+	ml := (int sbuf[1] << 8) | int sbuf[0];
+	ml |= ((int sbuf[3] << 8) | int sbuf[2]) << 16;
+	if(ml <= BIT32SZ)
+		return (nil, "invalid 9P message size");
+	if(ml > msglim)
+		return (nil, "9P message longer than agreed");
+	buf := array[ml] of byte;
+	buf[0:] = sbuf;
+	if((n = sys->readn(fd, buf[BIT32SZ:], ml-BIT32SZ)) != ml-BIT32SZ){
+		if(n == 0)
+			return (nil, "9P message truncated");
+		return (nil, sys->sprint("%r"));
+	}
+	return (buf, nil);
+}
+
+istmsg(f: array of byte): int
+{
+	if(len f < H)
+		return -1;
+	return (int f[BIT32SZ] & 1) == 0;
+}
+
+compatible(t: ref Tmsg.Version, msize: int, version: string): (int, string)
+{
+	if(version == nil)
+		version = VERSION;
+	if(t.msize < msize)
+		msize = t.msize;
+	v := t.version;
+	if(len v < 2 || v[0:2] != "9P")
+		return (msize, "unknown");
+	for(i:=2; i<len v; i++)
+		if((c := v[i]) == '.'){
+			v = v[0:i];
+			break;
+		}else if(!(c >= '0' && c <= '9'))
+			return (msize, "unknown");	# fussier than Plan 9
+	if(v < VERSION)
+		return (msize, "unknown");
+	if(v < version)
+		version = v;
+	return (msize, version);
+}
+
+# only here to support an implementation of this module that talks the previous version of Styx
+write(fd: ref Sys->FD, buf: array of byte, nb: int): int
+{
+	return sys->write(fd, buf, nb);
+}
--- /dev/null
+++ b/appl/lib/styxconv/mkfile
@@ -1,0 +1,22 @@
+<../../../mkconfig
+
+TARG=\
+	ostyx.dis\
+	new2old.dis\
+	old2new.dis\
+
+MODULES=\
+	ostyx.m\
+	osys.m\
+	nsys.m\
+
+SYSMODULES=\
+	bufio.m\
+	draw.m\
+	styx.m\
+	styxconv.m\
+	sys.m\
+
+DISBIN=$ROOT/dis/lib/styxconv
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/styxconv/new2old.b
@@ -1,0 +1,640 @@
+implement Styxconv;
+
+include "sys.m";
+	sys: Sys;
+include "osys.m";
+include "nsys.m";
+include "draw.m";
+include "styx.m";
+	nstyx: Styx;
+	Tmsg, Rmsg: import nstyx;
+include "ostyx.m";
+	ostyx: OStyx;
+	OTmsg, ORmsg: import ostyx;
+include "styxconv.m";
+
+# todo: map fids > ffff into 16 bits
+
+DEBUG: con 1;
+
+Fid: adt
+{
+	fid: int;
+	isdir: int;
+	n: int;			# size of last new client dirread request.
+	soff: int;			# dir offset on old server.
+	coff: int;			# dir offset on new client.
+	next: cyclic ref Fid;
+};
+
+Req: adt {
+	tag: int;
+	oldtag: int;					# if it's a flush.
+	rp: ref Reqproc;
+	next: cyclic ref Req;
+	flushes: list of ref Rmsg.Flush;		# flushes awaiting req finish.
+};
+
+Reqproc: adt {
+	newtmsg: chan of ref Tmsg;		# convproc -> reqproc, once per req.
+	newrmsg: chan of ref Rmsg;		# reqproc -> convproc, once per req
+
+	oldtmsg: chan of ref OTmsg;		# reqproc -> convproc
+	oldrmsg: chan of ref ORmsg;		# convproc -> reqproc
+
+	flushable: int;
+
+	new: fn(): ref Reqproc;
+	rpc: fn(rp: self ref Reqproc, otm: ref OTmsg): ref ORmsg;
+};
+
+tags: ref Req;
+avail: chan of ref Reqproc;
+fids: ref Fid;
+nprocs := 0;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil)
+		nomod("Sys", Sys->PATH);
+	nstyx = load Styx Styx->PATH;
+	if(nstyx == nil)
+		nomod("Styx", Styx->PATH);
+	ostyx = load OStyx OStyx->PATH;
+	if(ostyx == nil)
+		nomod("OStyx", OStyx->PATH);
+
+	ostyx->init();
+	nstyx->init();
+	avail = chan of ref Reqproc;
+}
+
+styxconv(newclient: ref Sys->FD, oldsrv: ref Sys->FD)
+{
+	newtmsg := chan of ref Tmsg;
+	oldrmsg := chan of ref ORmsg;
+
+	killpids := chan[2] of int;
+	spawn readnewtmsgs(killpids, newclient, newtmsg);
+	spawn readoldrmsgs(killpids, oldsrv, oldrmsg);
+
+converting:
+	for(;;)alt{
+	ntm := <-newtmsg =>
+		if(DEBUG)
+			sys->fprint(sys->fildes(2), "-> %s\n", ntm.text());
+		if(ntm == nil)
+			break converting;
+		ns2os(ntm, newclient, oldsrv);
+	orm := <-oldrmsg =>
+		if(DEBUG)
+			sys->fprint(sys->fildes(2), "	<- %s\n", ostyx->rmsg2s(orm));
+		if(orm == nil)
+			break converting;
+		t := looktag(orm.tag);
+		if(t == nil){
+			warning("reply by old-server to non-existent tag");
+			break;
+		}
+		pick rm := orm {
+		Flush =>
+			ot := looktag(t.oldtag);
+			# if it's an Rflush of a request-in-progress,
+			# we send it to the reqproc, which
+			# can then clean up as it likes.
+			if(ot != nil){
+				if(ot.rp != nil){
+					if(ot.rp.flushable){
+						ot.rp.oldrmsg <-= rm;
+						# reqproc is bound to finish after a flush
+						reqreply(ot, newclient, oldsrv);
+					}else {
+						# hold flush reply for later
+						ot.flushes = ref Rmsg.Flush(rm.tag) :: ot.flushes;
+					}
+					break;
+				}
+				deletetag(t.oldtag);
+			}
+			NRsend(newclient, ref Rmsg.Flush(rm.tag));
+			deletetag(rm.tag);
+		* =>
+			if(t.rp != nil){
+				t.rp.oldrmsg <-= orm;
+				reqreply(t, newclient, oldsrv);
+			}else{
+				os2ns(orm, newclient);
+				deletetag(orm.tag);
+			}
+		}
+	}
+	# kill off active reqprocs
+	for(; tags != nil; tags = tags.next){
+		if(tags.rp != nil){
+			tags.rp.oldrmsg <-= nil;
+			nprocs--;
+		}
+	}
+	# kill off idle reqprocs
+	while(nprocs > 0){
+		rp := <-avail;
+		rp.newtmsg <-= nil;
+		nprocs--;
+	}
+	# kill off message readers
+	kill(<-killpids);
+	kill(<-killpids);
+}
+
+# process one response from the request proc.
+# request proc can respond by sending a new tmsg to the old server
+# or by sending an rmsg to the new client, in which case
+# it implicitly signals that it has finished processing the request.
+# the actual reply might be an Rflush, signifying that
+# the request has been aborted.
+reqreply(t: ref Req, newclient: ref Sys->FD, oldsrv: ref Sys->FD)
+{
+	rp := t.rp;
+	alt{
+	nrm := <-rp.newrmsg =>
+		# request is done when process sends rmsg
+		pick rm := nrm {
+		Flush =>
+			deletetag(t.tag);
+		}
+		deletetag(nrm.tag);
+		NRsend(newclient, nrm);
+		for(; t.flushes != nil; t.flushes = tl t.flushes)
+			NRsend(newclient, hd t.flushes);
+
+	otm := <-rp.oldtmsg =>
+		OTsend(oldsrv, otm);
+	}
+}
+
+
+# T messages: forward on, reply immediately, or start processing.
+ns2os(tm0: ref Tmsg, newclient, oldsrv: ref Sys->FD)
+{
+	otm: ref OTmsg;
+
+	t := ref Req(tm0.tag, -1, nil, nil, nil);
+	pick tm := tm0{
+	Readerror =>
+		exit;
+	Version =>
+		(s, v) := nstyx->compatible(tm, nstyx->MAXRPC, nil);
+		NRsend(newclient, ref Rmsg.Version(tm.tag, s, v));
+		return;
+	Auth =>
+		NRsend(newclient, ref Rmsg.Error(tm.tag, "authorization not required"));
+		return;
+	Walk =>
+		storetag(t);
+		t.rp = Reqproc.new();
+		t.rp.newtmsg <-= tm;
+		reqreply(t, newclient, oldsrv);
+		return;
+	Attach =>
+		otm = ref OTmsg.Attach(tm.tag, tm.fid, tm.uname, tm.aname);
+	Flush =>
+		t.oldtag = tm.oldtag;
+		otm = ref OTmsg.Flush(tm.tag, tm.oldtag);
+	Open =>
+		otm = ref OTmsg.Open(tm.tag, tm.fid, tm.mode);
+	Create =>
+		otm = ref OTmsg.Create(tm.tag, tm.fid, tm.perm, tm.mode, tm.name);
+	Read =>
+		fp := findfid(tm.fid);
+		count := tm.count;
+		offset := tm.offset;
+		if(fp != nil && fp.isdir){
+			fp.n = count;
+			count = (count/OStyx->DIRLEN)*OStyx->DIRLEN;
+			if(int offset != fp.coff){
+				NRsend(newclient, ref Rmsg.Error(tm.tag, "unexpected offset in dirread"));
+				return;
+			}
+			offset = big fp.soff;
+		}
+		otm = ref OTmsg.Read(tm.tag, tm.fid, count, offset);
+	Write =>
+		otm = ref OTmsg.Write(tm.tag, tm.fid, tm.offset, tm.data);
+	Clunk =>
+		otm = ref OTmsg.Clunk(tm.tag, tm.fid);
+	Remove =>
+		otm = ref OTmsg.Remove(tm.tag, tm.fid);
+	Stat =>
+		otm = ref OTmsg.Stat(tm.tag, tm.fid);
+	Wstat =>
+		otm = ref OTmsg.Wstat(tm.tag, tm.fid, nd2od(tm.stat));
+	* =>
+		fatal("bad T message");
+	}
+	storetag(t);
+	OTsend(oldsrv, otm);
+}
+
+# R messages: old to new
+os2ns(orm0: ref ORmsg, newclient: ref Sys->FD)
+{
+	rm: ref Rmsg;
+
+	rm = nil;
+	pick orm := orm0 {
+	Error =>
+		rm = ref Rmsg.Error(orm.tag, orm.err);
+	Flush =>
+		rm = ref Rmsg.Flush(orm.tag);
+	Clone =>
+		rm = ref Rmsg.Walk(orm.tag, nil);
+	Walk =>
+		fatal("walk rmsgs should be dealt with be walkreqproc");
+	Open =>
+		setfid(orm.fid, orm.qid);
+		rm = ref Rmsg.Open(orm.tag, oq2nq(orm.qid), 0);
+	Create =>
+		setfid(orm.fid, orm.qid);
+		rm = ref Rmsg.Create(orm.tag, oq2nq(orm.qid), 0);
+	Read =>
+		fp := findfid(orm.fid);
+		data := orm.data;
+		if(fp != nil && fp.isdir)
+			data = ods2nds(data, fp.n);
+		fp.coff += len data;
+		fp.soff += len orm.data;
+		rm = ref Rmsg.Read(orm.tag, data);
+	Write =>
+		rm = ref Rmsg.Write(orm.tag, orm.count);
+	Clunk =>
+		rm = ref Rmsg.Clunk(orm.tag);
+		deletefid(orm.fid);
+	Remove =>
+		rm = ref Rmsg.Remove(orm.tag);
+		deletefid(orm.fid);
+	Stat =>
+		rm = ref Rmsg.Stat(orm.tag, od2nd(orm.stat));
+	Wstat =>
+		rm = ref Rmsg.Wstat(orm.tag);
+	Attach =>
+		newfid(orm.fid, orm.qid.path & OSys->CHDIR);
+		rm = ref Rmsg.Attach(orm.tag, oq2nq(orm.qid));
+	* =>
+		fatal("bad R message");
+	}
+	NRsend(newclient, rm);
+}
+
+Reqproc.rpc(rp: self ref Reqproc, otm: ref OTmsg): ref ORmsg
+{
+	rp.oldtmsg <-= otm;
+	m := <-rp.oldrmsg;
+	if(m == nil)
+		exit;
+	return m;
+}
+
+Reqproc.new(): ref Reqproc
+{
+	alt{
+	rp := <-avail =>
+		return rp;
+	* =>
+		rp := ref Reqproc(
+			chan of ref Tmsg,
+			chan of ref Rmsg,
+			chan of ref OTmsg,
+			chan of ref ORmsg,
+			1);
+		spawn reqproc(rp);
+		nprocs++;
+		return rp;
+	}
+}
+
+reqproc(rp: ref Reqproc)
+{
+	for(;;){
+		tm := <-rp.newtmsg;
+		if(tm == nil)
+			return;
+		rm: ref Rmsg;
+		pick m := tm {
+		Walk =>
+			rm = walkreq(m, rp);
+		* =>
+			fatal("non-walk req passed to reqproc");
+		}
+		rp.flushable = 1;
+		rp.newrmsg <-= rm;
+		avail <-= rp;
+	}
+}
+
+# note that although this is in a separate process,
+# whenever it's not in Reqproc.rpc, the styxconv
+# process is blocked, so although state is shared,
+# there are no race conditions.
+walkreq(tm: ref Tmsg.Walk, rp: ref Reqproc): ref Rmsg
+{
+	cloned := 0;
+	n := len tm.names;
+	if(tm.newfid != tm.fid){
+		cloned = 1;
+		pick rm := rp.rpc(ref OTmsg.Clone(tm.tag, tm.fid, tm.newfid)) {
+		Clone =>
+			;
+		Error =>
+			return ref Rmsg.Error(tm.tag, rm.err);
+		Flush =>
+			return ref Rmsg.Flush(rm.tag);
+		* =>
+			fatal("unexpected reply to OTmsg.Clone");
+		}
+		cloned = 1;
+	}
+	qids := array[n] of NSys->Qid;
+	finalqid: OSys->Qid;
+
+	# make sure we don't get flushed in an unwindable state.
+	rp.flushable = n == 1 || cloned;
+	for(i := 0; i < n; i++){
+		pick rm := rp.rpc(ref OTmsg.Walk(tm.tag, tm.newfid, tm.names[i])) {
+		Walk =>
+			qids[i] = oq2nq(rm.qid);
+			finalqid = rm.qid;
+		Flush =>
+			if(cloned){
+				rp.flushable = 0;
+				rp.rpc(ref OTmsg.Clunk(tm.tag, tm.newfid));
+			}
+			return ref Rmsg.Flush(rm.tag);
+		Error =>
+			if(cloned){
+				rp.flushable = 0;
+				rp.rpc(ref OTmsg.Clunk(tm.tag, tm.newfid));
+			}
+			if(i == 0)
+				return ref Rmsg.Error(tm.tag, rm.err);
+			return ref Rmsg.Walk(tm.tag, qids[0:i]);
+		}
+	}
+	if(cloned)
+		clonefid(tm.fid, tm.newfid);
+	if(n > 0)
+		setfid(tm.newfid, finalqid);
+	return ref Rmsg.Walk(tm.tag, qids);
+}
+
+storetag(t: ref Req)
+{
+	t.next = tags;
+	tags = t;
+}
+
+looktag(tag: int): ref Req
+{
+	for(t := tags; t != nil; t = t.next)
+		if(t.tag == tag)
+			return t;
+	return nil;
+}
+
+deletetag(tag: int)
+{
+	prev: ref Req;
+	t := tags;
+	while(t != nil){
+		if(t.tag == tag){
+			next := t.next;
+			t.next = nil;
+			if(prev != nil)
+				prev.next = next;
+			else
+				tags = next;
+			t = next;
+		}else{
+			prev = t;
+			t = t.next;
+		}
+	}
+}
+
+newfid(fid: int, isdir: int): ref Fid
+{
+	f := ref Fid;
+	f.fid = fid;
+	f.isdir = isdir;
+	f.n = f.soff = f.coff = 0;
+	f.next = fids;
+	fids = f;
+	return f;
+}
+
+clonefid(ofid: int, fid: int): ref Fid
+{
+	if((f := findfid(ofid)) != nil){
+		nf := newfid(fid, f.isdir);
+		return nf;
+	}
+	warning("clone of non-existent fid");
+	return newfid(fid, 0);
+}
+
+deletefid(fid: int)
+{
+	lf: ref Fid;
+
+	for(f := fids; f != nil; f = f.next){
+		if(f.fid == fid){
+			if(lf == nil)
+				fids = f.next;
+			else
+				lf.next = f.next;
+			return;
+		}
+		lf = f;
+	}
+}
+
+findfid(fid: int): ref Fid
+{
+	for(f := fids; f != nil; f = f.next)
+		if(f.fid == fid)
+			return f;
+	return nil;
+}
+
+setfid(fid: int, qid: OSys->Qid)
+{
+	f := findfid(fid);
+	if(f == nil){
+		warning(sys->sprint("cannot find fid %d", fid));
+	}else{
+		f.isdir = qid.path & OSys->CHDIR;
+	}
+}
+
+om2nm(om: int): int
+{
+	# DMDIR == CHDIR
+	return om;
+}
+
+nm2om(m: int): int
+{
+	# DMDIR == CHDIR
+	return m&~(NSys->DMAPPEND|NSys->DMEXCL|NSys->DMAUTH);
+}
+
+oq2nq(oq: OSys->Qid): NSys->Qid
+{
+	q: NSys->Qid;
+
+	isdir := oq.path&OSys->CHDIR;
+	q.path = big (oq.path&~OSys->CHDIR);
+	q.vers = oq.vers;
+	q.qtype = 0;
+	if(isdir)
+		q.qtype |= NSys->QTDIR;
+	return q;
+}
+	
+nq2oq(q: NSys->Qid): OSys->Qid
+{
+	oq: OSys->Qid;
+
+	isdir := q.qtype&NSys->QTDIR;
+	oq.path = int q.path;
+	oq.vers = q.vers;
+	if(isdir)
+		oq.path |= OSys->CHDIR;
+	return oq;
+}
+
+od2nd(od: OSys->Dir): NSys->Dir
+{
+	d: NSys->Dir;
+
+	d.name = od.name;
+	d.uid = od.uid;
+	d.gid = od.gid;
+	d.muid = od.uid;
+	d.qid = oq2nq(od.qid);
+	d.mode = om2nm(od.mode);
+	d.atime = od.atime;
+	d.mtime = od.mtime;
+	d.length = big od.length;
+	d.dtype = od.dtype;
+	d.dev = od.dev;
+	return d;
+}
+
+nd2od(d: NSys->Dir): OSys->Dir
+{
+	od: OSys->Dir;
+
+	od.name = d.name;
+	od.uid = d.uid;
+	od.gid = d.gid;
+	od.qid = nq2oq(d.qid);
+	od.mode = nm2om(d.mode);
+	od.atime = d.atime;
+	od.mtime = d.mtime;
+	od.length = int d.length;
+	od.dtype = d.dtype;
+	od.dev = d.dev;
+	return od;
+}
+
+ods2nds(ob: array of byte, max: int): array of byte
+{
+	od: OSys->Dir;
+
+	m := len ob;
+	if(m % OStyx->DIRLEN != 0)
+		fatal(sys->sprint("bad dir len %d", m));
+	m /= OStyx->DIRLEN;
+	n := 0;
+	p := ob;
+	for(i := 0; i < m; i++){
+		(p, od) = ostyx->convM2D(p);
+		d := od2nd(od);
+		nn := nstyx->packdirsize(d);
+		if(n+nn > max)	# might just happen with long file names
+			break;
+		n += nn;
+	}
+	m = i;
+	b := array[n] of byte;
+	n = 0;
+	p = ob;
+	for(i = 0; i < m; i++){
+		(p, od) = ostyx->convM2D(p);
+		d := od2nd(od);
+		q := nstyx->packdir(d);
+		nn := len q;
+		b[n: ] = q[0: nn];
+		n += nn;
+	}
+	return b;
+}
+		
+OTsend(fd: ref Sys->FD, otm: ref OTmsg): int
+{
+	if(DEBUG)
+		sys->fprint(sys->fildes(2), "	-> %s\n", ostyx->tmsg2s(otm));
+	s := array[OStyx->MAXRPC] of byte;
+	n := ostyx->tmsg2d(otm, s);
+	if(n < 0)
+		return -1;
+	return sys->write(fd, s, n);
+}
+
+NRsend(fd: ref Sys->FD, rm: ref Rmsg): int
+{
+	if(DEBUG)
+		sys->fprint(sys->fildes(2), "<- %s\n", rm.text());
+	s := rm.pack();
+	if(s == nil)
+		return -1;
+	return sys->write(fd, s, len s);
+}
+
+readnewtmsgs(pidc: chan of int, newclient: ref Sys->FD, newtmsg: chan of ref Tmsg)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		newtmsg <-= Tmsg.read(newclient, nstyx->MAXRPC);
+	}
+}
+
+readoldrmsgs(pidc: chan of int, oldsrv: ref Sys->FD, oldrmsg: chan of ref ORmsg)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		oldrmsg <-= ORmsg.read(oldsrv);
+	}
+}
+
+warning(err: string)
+{
+	sys->fprint(sys->fildes(2), "warning: %s\n", err);
+}
+
+fatal(err: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", err);
+	exit;
+}
+
+nomod(mod: string, path: string)
+{
+	fatal(sys->sprint("can't load %s(%s): %r", mod, path));
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open("#p/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
--- /dev/null
+++ b/appl/lib/styxconv/nsys.m
@@ -1,0 +1,51 @@
+NSys: module
+{
+	# Unique file identifier for file objects
+	Qid: adt
+	{
+		path:	big;
+		vers:	int;
+		qtype:	int;
+	};
+
+	QTDIR:	con 16r80;
+	QTAPPEND:	con 16r40;
+	QTEXCL:	con 16r20;
+	QTAUTH:	con 16r08;
+	QTTMP:	con 16r04;
+	QTFILE:	con 0;
+
+	# Return from stat and directory read
+	Dir: adt
+	{
+		name:	string;
+		uid:	string;
+		gid:	string;
+		muid:	string;
+		qid:	Qid;
+		mode:	int;
+		atime:	int;
+		mtime:	int;
+		length:	big;
+		dtype:	int;
+		dev:	int;
+	};
+
+	# Maximum read which will be completed atomically;
+	# also the optimum block size
+	#
+	ATOMICIO:	con 8192;
+
+	OREAD:		con 0;
+	OWRITE:		con 1;
+	ORDWR:		con 2;
+	OTRUNC:		con 16;
+	ORCLOSE:	con 64;
+	OEXCL:		con 16r1000;
+
+	DMDIR:		con int 1<<31;
+	DMAPPEND:	con int 1<<30;
+	DMEXCL:		con int 1<<29;
+	DMAUTH:		con int 1<<27;
+	DMTMP:		con int 1<<26;
+};
--- /dev/null
+++ b/appl/lib/styxconv/old2new.b
@@ -1,0 +1,480 @@
+implement Styxconv;
+
+include "sys.m";
+	sys: Sys;
+include "osys.m";
+include "nsys.m";
+include "draw.m";
+include "styx.m";
+	nstyx: Styx;
+	Tmsg, Rmsg: import nstyx;
+include "ostyx.m";
+	ostyx: OStyx;
+	OTmsg, ORmsg: import ostyx;
+include "styxconv.m";
+
+DEBUG: con 0;
+
+# convert from old styx client to new styx server.
+# more straightforward than the other way around
+# because there's an almost exactly 1-1 mapping
+# between message types. (the exception is Tversion,
+# but we do that synchronously anyway).
+
+# todo: map qids > ffffffff into 32 bits.
+
+Msize: con nstyx->IOHDRSZ + OSys->ATOMICIO;
+Fid: adt
+{
+	fid: int;
+	isdir: int;
+	n: int;			# size of last new client dirread request.
+	soff: int;			# dir offset on new server.
+	coff: int;			# dir offset on old client.
+	next: cyclic ref Fid;
+	extras: array of byte;	# packed old styx dir structures
+};
+
+Req: adt {
+	tag: int;
+	fid: int;
+	oldtag: int;			# if it's a flush.
+	newfid: int;			# if it's a clone
+	next: cyclic ref Req;
+};
+
+tags: ref Req;
+fids: ref Fid;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil)
+		nomod("Sys", Sys->PATH);
+	nstyx = load Styx Styx->PATH;
+	if(nstyx == nil)
+		nomod("Styx", Styx->PATH);
+	ostyx = load OStyx OStyx->PATH;
+	if(ostyx == nil)
+		nomod("OStyx", OStyx->PATH);
+
+	ostyx->init();
+	nstyx->init();
+}
+
+styxconv(oldclient, newsrv: ref Sys->FD)
+{
+	oldtmsg := chan of ref OTmsg;
+	newrmsg := chan of ref Rmsg;
+
+	killpids := chan[2] of int;
+	spawn readoldtmsgs(killpids, oldclient, oldtmsg);
+	spawn readnewrmsgs(killpids, newsrv, newrmsg);
+	# XXX difficulty: what happens if the server isn't responding
+	# and the client hangs up? we won't know about it.
+	# but we don't want to know about normal t-messages
+	# piling up either, so we don't want to alt on oldtmsg too.
+	NTsend(newsrv, ref Tmsg.Version(nstyx->NOTAG, Msize, "9P2000"));
+	pick nrm := <-newrmsg {
+	Version =>
+		if(DEBUG)
+			sys->fprint(sys->fildes(2), "	<- %s\n", nrm.text());
+		if(nrm.msize < Msize)
+			fatal("message size too small");
+	Error =>
+		fatal("versioning failed: " + nrm.ename);
+	* =>
+		fatal("bad response to Tversion: " + nrm.text());
+	}
+
+converting:
+	for(;;)alt{
+	otm := <-oldtmsg =>
+		if(DEBUG)
+			sys->fprint(sys->fildes(2), "-> %s\n", ostyx->tmsg2s(otm));
+		if(otm == nil || tagof(otm) == tagof(OTmsg.Readerror))
+			break converting;
+		oc2ns(otm, oldclient, newsrv);
+	nrm := <-newrmsg =>
+		if(DEBUG)
+			sys->fprint(sys->fildes(2), "	<- %s\n", nrm.text());
+		if(nrm == nil || tagof(nrm) == tagof(Rmsg.Readerror))
+			break converting;
+		t := looktag(nrm.tag);
+		if(t == nil){
+			warning("reply by new-server to non-existent tag");
+			break;
+		}
+		ns2oc(t, nrm, oldclient);
+		deletetag(nrm.tag);
+	}
+
+	kill(<-killpids);
+	kill(<-killpids);
+}
+
+# T messages: forward on or reply immediately
+oc2ns(tm0: ref OTmsg, oldclient, newsrv: ref Sys->FD)
+{
+	ntm: ref Tmsg;
+
+	t := ref Req(tm0.tag, -1, -1, -1, nil);
+	pick tm := tm0{
+	Nop =>
+		ORsend(oldclient, ref ORmsg.Nop(tm.tag));
+		return;
+	Attach =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Attach(tm.tag, tm.fid, nstyx->NOFID, tm.uname, tm.aname);
+	Clone =>
+		t.fid = tm.fid;
+		t.newfid = tm.newfid;
+		ntm = ref Tmsg.Walk(tm.tag, tm.fid, tm.newfid, nil);
+	Walk =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Walk(tm.tag, tm.fid, tm.fid, array[] of {tm.name});
+	Flush =>
+		t.oldtag = tm.oldtag;
+		ntm = ref Tmsg.Flush(tm.tag, tm.oldtag);
+	Open =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Open(tm.tag, tm.fid, tm.mode);
+	Create =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Create(tm.tag, tm.fid, tm.name, tm.perm, tm.mode);
+	Read =>
+		t.fid = tm.fid;
+		fp := findfid(tm.fid);
+		count := tm.count;
+		offset := tm.offset;
+		if(fp.isdir){
+			count = (count/OStyx->DIRLEN)*OStyx->DIRLEN;
+			# if we got some extra entries last time,
+			# then send 'em back this time.
+			extras := fp.extras;
+			if(len extras > 0){
+				if(count > len extras)
+					count = len extras;
+				ORsend(oldclient, ref ORmsg.Read(tm.tag, t.fid, fp.extras[0:count]));
+				fp.extras = extras[count:];
+				fp.coff += count;
+				return;
+			}
+			fp.n = count;
+			if(int offset != fp.coff){
+				ORsend(oldclient, ref ORmsg.Error(tm.tag, "unexpected offset in dirread"));
+				return;
+			}
+			offset = big fp.soff;
+		}
+		ntm = ref Tmsg.Read(tm.tag, tm.fid, offset, count);
+	Write =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Write(tm.tag, tm.fid, tm.offset, tm.data);
+	Clunk =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Clunk(tm.tag, tm.fid);
+	Remove =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Remove(tm.tag, tm.fid);
+	Stat =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Stat(tm.tag, tm.fid);
+	Wstat =>
+		t.fid = tm.fid;
+		ntm = ref Tmsg.Wstat(tm.tag, tm.fid, od2nd(tm.stat));
+	* =>
+		fatal("bad T message");
+	}
+	storetag(t);
+	NTsend(newsrv, ntm);
+}
+
+# R messages: new to old
+ns2oc(t: ref Req, nrm0: ref Rmsg, oldclient: ref Sys->FD)
+{
+	rm: ref ORmsg;
+	pick nrm := nrm0{
+	Error =>
+		rm = ref ORmsg.Error(nrm.tag, nrm.ename);
+	Flush =>
+		rm = ref ORmsg.Flush(nrm.tag);
+		deletetag(t.oldtag);
+	Walk =>
+		if(len nrm.qids == 0){
+			clonefid(t.fid, t.newfid);
+			rm = ref ORmsg.Clone(nrm.tag, t.fid);
+		}else{
+			q := nrm.qids[0];
+			setfid(t.fid, q);
+			rm = ref ORmsg.Walk(nrm.tag, t.fid, nq2oq(q));
+		}
+	Open =>
+		setfid(t.fid, nrm.qid);
+		rm = ref ORmsg.Open(nrm.tag, t.fid, nq2oq(nrm.qid));
+	Create =>
+		setfid(t.fid, nrm.qid);
+		rm = ref ORmsg.Create(nrm.tag, t.fid, nq2oq(nrm.qid));
+	Read =>
+		fp := findfid(t.fid);
+		data := nrm.data;
+		if(fp != nil && fp.isdir){
+			data = nds2ods(data);
+			if(len data > fp.n){
+				fp.extras = data[fp.n:];
+				data = data[0:fp.n];
+			}
+			fp.coff += len data;
+			fp.soff += len nrm.data;
+		}
+		rm = ref ORmsg.Read(nrm.tag, t.fid, data);
+	Write =>
+		rm = ref ORmsg.Write(nrm.tag, t.fid, nrm.count);
+	Clunk =>
+		deletefid(t.fid);
+		rm = ref ORmsg.Clunk(nrm.tag, t.fid);
+	Remove =>
+		deletefid(t.fid);
+		rm = ref ORmsg.Remove(nrm.tag, t.fid);
+	Stat =>
+		rm = ref ORmsg.Stat(nrm.tag, t.fid, nd2od(nrm.stat));
+	Wstat =>
+		rm = ref ORmsg.Wstat(nrm.tag, t.fid);
+	Attach =>
+		newfid(t.fid, nrm.qid.qtype & NSys->QTDIR);
+		rm = ref ORmsg.Attach(nrm.tag, t.fid, nq2oq(nrm.qid));
+	* =>
+		fatal("bad R message");
+	}
+	ORsend(oldclient, rm);
+}
+
+storetag(t: ref Req)
+{
+	t.next = tags;
+	tags = t;
+}
+
+looktag(tag: int): ref Req
+{
+	for(t := tags; t != nil; t = t.next)
+		if(t.tag == tag)
+			return t;
+	return nil;
+}
+
+deletetag(tag: int)
+{
+	prev: ref Req;
+	t := tags;
+	while(t != nil){
+		if(t.tag == tag){
+			next := t.next;
+			t.next = nil;
+			if(prev != nil)
+				prev.next = next;
+			else
+				tags = next;
+			t = next;
+		}else{
+			prev = t;
+			t = t.next;
+		}
+	}
+}
+
+newfid(fid: int, isdir: int): ref Fid
+{
+	f := ref Fid;
+	f.fid = fid;
+	f.isdir = isdir;
+	f.n = f.soff = f.coff = 0;
+	f.next = fids;
+	fids = f;
+	return f;
+}
+
+clonefid(ofid: int, fid: int): ref Fid
+{
+	if((f := findfid(ofid)) != nil)
+		return newfid(fid, f.isdir);
+	warning("clone of non-existent fid");
+	return newfid(fid, 0);
+}
+
+deletefid(fid: int)
+{
+	lf: ref Fid;
+
+	for(f := fids; f != nil; f = f.next){
+		if(f.fid == fid){
+			if(lf == nil)
+				fids = f.next;
+			else
+				lf.next = f.next;
+			return;
+		}
+		lf = f;
+	}
+}
+
+findfid(fid: int): ref Fid
+{
+	for(f := fids; f != nil && f.fid != fid; f = f.next)
+		;
+	return f;
+}
+
+setfid(fid: int, qid: NSys->Qid)
+{
+	if((f := findfid(fid)) != nil)
+		f.isdir = qid.qtype & NSys->QTDIR;
+}
+
+om2nm(om: int): int
+{
+	# DMDIR == CHDIR
+	return om;
+}
+
+nm2om(m: int): int
+{
+	# DMDIR == CHDIR
+	return m&~(NSys->DMAPPEND|NSys->DMEXCL|NSys->DMAUTH);
+}
+
+oq2nq(oq: OSys->Qid): NSys->Qid
+{
+	q: NSys->Qid;
+
+	isdir := oq.path&OSys->CHDIR;
+	q.path = big (oq.path&~OSys->CHDIR);
+	q.vers = oq.vers;
+	q.qtype = 0;
+	if(isdir)
+		q.qtype |= NSys->QTDIR;
+	return q;
+}
+	
+nq2oq(q: NSys->Qid): OSys->Qid
+{
+	oq: OSys->Qid;
+
+	isdir := q.qtype&NSys->QTDIR;
+	oq.path = int q.path;
+	oq.vers = q.vers;
+	if(isdir)
+		oq.path |= OSys->CHDIR;
+	return oq;
+}
+
+od2nd(od: OSys->Dir): NSys->Dir
+{
+	d: NSys->Dir;
+
+	d.name = od.name;
+	d.uid = od.uid;
+	d.gid = od.gid;
+	d.muid = od.uid;
+	d.qid = oq2nq(od.qid);
+	d.mode = om2nm(od.mode);
+	d.atime = od.atime;
+	d.mtime = od.mtime;
+	d.length = big od.length;
+	d.dtype = od.dtype;
+	d.dev = od.dev;
+	return d;
+}
+
+nd2od(d: NSys->Dir): OSys->Dir
+{
+	od: OSys->Dir;
+
+	od.name = d.name;
+	od.uid = d.uid;
+	od.gid = d.gid;
+	od.qid = nq2oq(d.qid);
+	od.mode = nm2om(d.mode);
+	od.atime = d.atime;
+	od.mtime = d.mtime;
+	od.length = int d.length;
+	od.dtype = d.dtype;
+	od.dev = d.dev;
+	return od;
+}
+
+nds2ods(ob: array of byte): array of byte
+{
+	i := 0;
+	n := 0;
+	ds: list of NSys->Dir;
+	while(i < len ob){
+		(size, d) := nstyx->unpackdir(ob[i:]);
+		if(size == 0)
+			break;
+		ds = d :: ds;
+		i += size;
+		n++;
+	}
+	b := array[OStyx->DIRLEN * n] of byte;
+	for(i = (n - 1) * OStyx->DIRLEN; i >= 0; i -= OStyx->DIRLEN){
+		ostyx->convD2M(b[i:], nd2od(hd ds));
+		ds = tl ds;
+	}
+	return b;
+}
+
+NTsend(fd: ref Sys->FD, ntm: ref Tmsg)
+{
+	if(DEBUG)
+		sys->fprint(sys->fildes(2), "	-> %s\n", ntm.text());
+	s := ntm.pack();
+	sys->write(fd, s, len s);
+}
+
+ORsend(fd: ref Sys->FD, orm: ref ORmsg)
+{
+	if(DEBUG)
+		sys->fprint(sys->fildes(2), "<- %s\n", ostyx->rmsg2s(orm));
+	s := array[OStyx->MAXRPC] of byte;
+	n := ostyx->rmsg2d(orm, s);
+	if(n > 0)
+		sys->write(fd, s, n);
+}
+
+readoldtmsgs(pidc: chan of int, oldclient: ref Sys->FD, oldtmsg: chan of ref OTmsg)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		oldtmsg <-= OTmsg.read(oldclient);
+	}
+}
+
+readnewrmsgs(pidc: chan of int, newsrv: ref Sys->FD, newrmsg: chan of ref Rmsg)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		newrmsg <-= Rmsg.read(newsrv, Msize);
+	}
+}
+
+warning(err: string)
+{
+	sys->fprint(sys->fildes(2), "warning: %s\n", err);
+}
+
+fatal(err: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", err);
+	exit;
+}
+
+nomod(mod: string, path: string)
+{
+	fatal(sys->sprint("can't load %s(%s): %r", mod, path));
+}
+
+kill(pid: int)
+{
+	sys->fprint(sys->open("#p/"+string pid+"/ctl", Sys->OWRITE), "kill");
+}
--- /dev/null
+++ b/appl/lib/styxconv/ostyx.b
@@ -1,0 +1,810 @@
+implement OStyx;
+
+#
+# Copyright © 1999 Vita Nuova Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "osys.m";
+include "ostyx.m";
+
+DEBUG: con 0;
+
+CHANHASHSIZE: con 32;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	gsofar = 0;
+	gdata = array[MAXRPC] of {* => byte 0};
+}
+
+# note that this implementation fails if we're reading OTmsgs and ORmsgs
+# concurrently. luckily we don't need to in styxconv.
+gsofar: int;
+gdata: array of byte;
+
+ORmsg.read(fd: ref Sys->FD): ref ORmsg
+{
+	mlen := 0;
+	m: ref ORmsg;
+	for (;;){
+		if(gsofar > 0)
+			(mlen, m) = d2rmsg(gdata[0 : gsofar]);
+		if(mlen == 0){
+			if(gsofar == len gdata){
+				ndata := array[MAXRPC] of byte;
+				ndata[0:] = gdata;
+				gdata = ndata;
+			}
+			n := sys->read(fd, gdata[gsofar:], len gdata - gsofar);
+			if(n <= 0)
+				return nil;
+			gsofar += n;
+		}else if(mlen > 0){
+			if(tagof(m) == tagof(OTmsg.Write)) {
+				ndata := array[MAXRPC] of byte;
+				ndata[0:] = gdata[mlen : gsofar];
+				gdata = ndata;
+			}else
+				gdata[0:] = gdata[mlen : gsofar];
+			gsofar -= mlen;
+			return m;
+		}else
+			gsofar = 0;
+	}
+}
+
+OTmsg.read(fd: ref Sys->FD): ref OTmsg
+{
+	mlen := 0;
+	m: ref OTmsg;
+	for (;;){
+		if(gsofar > 0)
+			(mlen, m) = d2tmsg(gdata[0 : gsofar]);
+		if(mlen == 0){
+			if(gsofar == len gdata){
+				ndata := array[MAXRPC] of byte;
+				ndata[0:] = gdata;
+				gdata = ndata;
+			}
+			n := sys->read(fd, gdata[gsofar:], len gdata - gsofar);
+			if(n <= 0)
+				return nil;
+			gsofar += n;
+		}else if(mlen > 0){
+			if(tagof(m) == tagof(OTmsg.Write)) {
+				ndata := array[MAXRPC] of byte;
+				ndata[0:] = gdata[mlen : gsofar];
+				gdata = ndata;
+			}else
+				gdata[0:] = gdata[mlen : gsofar];
+			gsofar -= mlen;
+			return m;
+		}else
+			gsofar = 0;
+	}
+}
+
+
+Styxserver.new(fd: ref Sys->FD): (chan of ref OTmsg, ref Styxserver)
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+
+	tchan := chan of ref OTmsg;
+	srv := ref Styxserver(fd, array[CHANHASHSIZE] of list of ref Chan);
+
+	sync := chan of int;
+	spawn tmsgreader(fd, tchan, sync);
+	<-sync;
+	return (tchan, srv);
+}
+
+tmsgreader(fd: ref Sys->FD, tchan: chan of ref OTmsg, sync: chan of int)
+{
+	sys->pctl(Sys->NEWFD|Sys->NEWNS, fd.fd :: nil);
+	sync <-= 1;
+	fd = sys->fildes(fd.fd);
+	data := array[MAXRPC] of byte;
+	sofar := 0;
+	for (;;) {
+		n := sys->read(fd, data[sofar:], len data - sofar);
+		if (n <= 0) {
+			m: ref OTmsg = nil;
+			if (n < 0)
+				m = ref OTmsg.Readerror(-1, sys->sprint("%r"));
+			tchan <-= m;
+			return;
+		}
+		sofar += n;
+		(cn, m) := d2tmsg(data[0:sofar]);
+		if (cn == -1) {
+			# on msg format error, flush any data and
+			# hope it'll be alright in the future.
+			sofar = 0;
+		} else if (cn > 0) {
+			# if it's a write message, then the buffer is used in
+			# the message, so allocate another one to avoid
+			# aliasing.
+			if (tagof(m) == tagof(OTmsg.Write)) {
+				ndata := array[MAXRPC] of byte;
+				ndata[0:] = data[cn:sofar];
+				data = ndata;
+			} else
+				data[0:] = data[cn:sofar];
+			sofar -= cn;
+			tchan <-= m;
+			m = nil;
+		}
+	}
+}
+
+Styxserver.reply(srv: self ref Styxserver, m: ref ORmsg): int
+{
+	d := array[MAXRPC] of byte;
+	if (DEBUG) 
+		sys->fprint(sys->fildes(2), "%s\n", rmsg2s(m));
+	n := rmsg2d(m, d);
+	return sys->write(srv.fd, d, n);
+}
+
+type2tag := array[] of {
+	Tnop	=> tagof(OTmsg.Nop),
+	Tflush	=> tagof(OTmsg.Flush),
+	Tclone	=> tagof(OTmsg.Clone),
+	Twalk	=> tagof(OTmsg.Walk),
+	Topen	=> tagof(OTmsg.Open),
+	Tcreate	=> tagof(OTmsg.Create),
+	Tread	=> tagof(OTmsg.Read),
+	Twrite	=> tagof(OTmsg.Write),
+	Tclunk	=> tagof(OTmsg.Clunk),
+	Tremove	=> tagof(OTmsg.Remove),
+	Tstat		=> tagof(OTmsg.Stat),
+	Twstat	=> tagof(OTmsg.Wstat),
+	Tattach	=> tagof(OTmsg.Attach),
+	*		=> -1
+};
+
+msglen := array[] of {
+	Tnop	=> 3,
+	Tflush	=> 5,
+	Tclone	=> 7,
+	Twalk	=> 33,
+	Topen	=> 6,
+	Tcreate	=> 38,
+	Tread	=> 15,
+	Twrite	=> 16,	# header only; excludes data
+	Tclunk	=> 5,
+	Tremove	=> 5,
+	Tstat		=> 5,
+	Twstat	=> 121,
+	Tattach	=> 5+2*OSys->NAMELEN,
+
+	Rnop	=> -3,
+	Rerror	=> -67,
+	Rflush	=> -3,
+	Rclone	=> -5,
+	Rwalk	=> -13,
+	Ropen	=> -13,
+	Rcreate	=> -13,
+	Rread	=> -8,	# header only; excludes data
+	Rwrite	=> -7,
+	Rclunk	=> -5,
+	Rremove	=> -5,
+	Rstat		=> -121,
+	Rwstat	=> -5,
+	Rsession	=> -0,
+	Rattach	=> -13,
+	*		=> 0
+};
+
+d2tmsg(d: array of byte): (int, ref OTmsg)
+{
+	tag: int;
+	gmsg: ref OTmsg;
+
+	n := len d;
+	if (n < 3)
+		return (0, nil);
+
+	t: int;
+	(d, t) = gchar(d);
+	if (t < 0 || t >= len msglen || msglen[t] <= 0)
+		return (-1, nil);
+
+	if (n < msglen[t])
+		return (0, nil);
+
+	(d, tag) = gshort(d);
+	case t {
+	Tnop	=>
+			msg := ref OTmsg.Nop;
+			gmsg = msg;
+	Tflush	=>
+			msg := ref OTmsg.Flush;
+			(d, msg.oldtag) = gshort(d);
+			gmsg = msg;
+	Tclone	=>
+			msg := ref OTmsg.Clone;
+			(d, msg.fid) = gshort(d);
+			(d, msg.newfid) = gshort(d);
+			gmsg = msg;
+	Twalk	=>
+			msg := ref OTmsg.Walk;
+			(d, msg.fid) = gshort(d);
+			(d, msg.name) = gstring(d, OSys->NAMELEN);
+			gmsg = msg;
+	Topen	=>
+			msg := ref OTmsg.Open;
+			(d, msg.fid) = gshort(d);
+			(d, msg.mode) = gchar(d);
+			gmsg = msg;
+	Tcreate	=>
+			msg := ref OTmsg.Create;
+			(d, msg.fid) = gshort(d);
+			(d, msg.name) = gstring(d, OSys->NAMELEN);
+			(d, msg.perm) = glong(d);
+			(d, msg.mode) = gchar(d);
+			gmsg = msg;
+	Tread	=>
+			msg := ref OTmsg.Read;
+			(d, msg.fid) = gshort(d);
+			(d, msg.offset) = gbig(d);
+			if (msg.offset < big 0)
+				msg.offset = big 0;
+			(d, msg.count) = gshort(d);
+			gmsg = msg;
+	Twrite	=>
+			count: int;
+			msg := ref OTmsg.Write;
+			(d, msg.fid) = gshort(d);
+			(d, msg.offset) = gbig(d);
+			if (msg.offset < big 0)
+				msg.offset = big 0;
+			(d, count) = gshort(d);
+			if (count > Sys->ATOMICIO)
+				return (-1, nil);
+			if (len d < 1 + count)
+				return (0, nil);
+			d = d[1:];
+			msg.data = d[0:count];
+			d = d[count:];
+			gmsg = msg;
+	Tclunk	=>
+			msg := ref OTmsg.Clunk;
+			(d, msg.fid) = gshort(d);
+			gmsg = msg;
+	Tremove	=>
+			msg := ref OTmsg.Remove;
+			(d, msg.fid) = gshort(d);
+			gmsg = msg;
+	Tstat		=>
+			msg := ref OTmsg.Stat;
+			(d, msg.fid) = gshort(d);
+			gmsg = msg;
+	Twstat	=>
+			msg := ref OTmsg.Wstat;
+			(d, msg.fid) = gshort(d);
+			(d, msg.stat) = convM2D(d);
+			gmsg = msg;
+	Tattach	=>
+			msg := ref OTmsg.Attach;
+			(d, msg.fid) = gshort(d);
+			(d, msg.uname) = gstring(d, OSys->NAMELEN);
+			(d, msg.aname) = gstring(d, OSys->NAMELEN);
+			gmsg = msg;
+	*  =>
+			return (-1, nil);
+	}
+	gmsg.tag = tag;
+	return (n - len d, gmsg);
+}
+
+d2rmsg(d: array of byte): (int, ref ORmsg)
+{
+	tag: int;
+	gmsg: ref ORmsg;
+
+	n := len d;
+	if (n < 3)
+		return (0, nil);
+
+	t: int;
+	(d, t) = gchar(d);
+	if (t < 0 || t >= len msglen || msglen[t] >= 0)
+		return (-1, nil);
+
+	if (n < -msglen[t])
+		return (0, nil);
+
+	(d, tag) = gshort(d);
+	case t {
+	Rerror 	=>
+			msg := ref ORmsg.Error;
+			(d, msg.err) = gstring(d, OSys->ERRLEN);
+			gmsg = msg;
+	Rnop	=>
+			msg := ref ORmsg.Nop;
+			gmsg = msg;
+	Rflush	=>
+			msg := ref ORmsg.Flush;
+			gmsg = msg;
+	Rclone	=>
+			msg := ref ORmsg.Clone;
+			(d, msg.fid) = gshort(d);
+			gmsg = msg;
+	Rwalk	=>
+			msg := ref ORmsg.Walk;
+			(d, msg.fid) = gshort(d);
+			(d, msg.qid.path) = glong(d);
+			(d, msg.qid.vers) = glong(d);
+			gmsg = msg;
+	Ropen	=>
+			msg := ref ORmsg.Open;
+			(d, msg.fid) = gshort(d);
+			(d, msg.qid.path) = glong(d);
+			(d, msg.qid.vers) = glong(d);
+			gmsg = msg;
+	Rcreate	=>
+			msg := ref ORmsg.Create;
+			(d, msg.fid) = gshort(d);
+			(d, msg.qid.path) = glong(d);
+			(d, msg.qid.vers) = glong(d);
+			gmsg = msg;
+	Rread	=>
+			count: int;
+			msg := ref ORmsg.Read;
+			(d, msg.fid) = gshort(d);
+			(d, count) = gshort(d);
+			if (count > Sys->ATOMICIO)
+				return (-1, nil);
+			if (len d < 1 + count)
+				return (0, nil);
+			d = d[1:];
+			msg.data = d[0:count];
+			d = d[count:];
+			gmsg = msg;
+	Rwrite	=>
+			msg := ref ORmsg.Write;
+			(d, msg.fid) = gshort(d);
+			(d, msg.count) = gshort(d);
+			gmsg = msg;
+	Rclunk	=>
+			msg := ref ORmsg.Clunk;
+			(d, msg.fid) = gshort(d);
+			gmsg = msg;
+	Rremove	=>
+			msg := ref ORmsg.Remove;
+			(d, msg.fid) = gshort(d);
+			gmsg = msg;
+	Rstat		=>
+			msg := ref ORmsg.Stat;
+			(d, msg.fid) = gshort(d);
+			(d, msg.stat) = convM2D(d);
+			gmsg = msg;
+	Rwstat	=>
+			msg := ref ORmsg.Wstat;
+			(d, msg.fid) = gshort(d);
+			gmsg = msg;
+	Rattach	=>
+			msg := ref ORmsg.Attach;
+			(d, msg.fid) = gshort(d);
+			(d, msg.qid.path) = glong(d);
+			(d, msg.qid.vers) = glong(d);
+			gmsg = msg;
+	*  =>
+			return (-1, nil);
+	}
+	gmsg.tag = tag;
+	return (n - len d, gmsg);
+}
+
+ttag2type := array[] of {
+tagof(OTmsg.Readerror) => Terror,
+tagof(OTmsg.Nop) => Tnop,
+tagof(OTmsg.Flush) => Tflush,
+tagof(OTmsg.Clone) => Tclone,
+tagof(OTmsg.Walk) => Twalk,
+tagof(OTmsg.Open) => Topen,
+tagof(OTmsg.Create) => Tcreate,
+tagof(OTmsg.Read) => Tread,
+tagof(OTmsg.Write) => Twrite,
+tagof(OTmsg.Clunk) => Tclunk,
+tagof(OTmsg.Stat) => Tstat,
+tagof(OTmsg.Remove) => Tremove,
+tagof(OTmsg.Wstat) => Twstat,
+tagof(OTmsg.Attach) => Tattach,
+};
+
+tag2type := array[] of {
+tagof ORmsg.Nop	=> Rnop,
+tagof ORmsg.Flush	=> Rflush,
+tagof ORmsg.Error	=> Rerror,
+tagof ORmsg.Clone	=> Rclone,
+tagof ORmsg.Walk	=> Rwalk,
+tagof ORmsg.Open	=> Ropen,
+tagof ORmsg.Create	=> Rcreate,
+tagof ORmsg.Read	=> Rread,
+tagof ORmsg.Write	=> Rwrite,
+tagof ORmsg.Clunk	=> Rclunk,
+tagof ORmsg.Remove	=> Rremove,
+tagof ORmsg.Stat	=> Rstat,
+tagof ORmsg.Wstat	=> Rwstat,
+tagof ORmsg.Attach	=> Rattach,
+};
+
+tmsg2d(gm: ref OTmsg, d: array of byte): int
+{
+	n := len d;
+	d = pchar(d, ttag2type[tagof gm]);
+	d = pshort(d, gm.tag);
+	pick m := gm {
+	Nop =>
+	Flush =>
+		d = pshort(d, m.oldtag);
+	Clone =>
+		d = pshort(d, m.fid);
+		d = pshort(d, m.newfid);
+	Walk =>
+		d = pshort(d, m.fid);
+		d = pstring(d, m.name, OSys->NAMELEN);
+	Open =>
+		d = pshort(d, m.fid);
+		d = pchar(d, m.mode);
+	Create =>
+		d = pshort(d, m.fid);
+		d = pstring(d, m.name, OSys->NAMELEN);
+		d = plong(d, m.perm);
+		d = pchar(d, m.mode);
+	Read =>
+		d = pshort(d, m.fid);
+		d = pbig(d, m.offset);
+		d = pshort(d, m.count);
+	Write =>
+		data := m.data;
+		if (len data > Sys->ATOMICIO)
+			data = data[0:Sys->ATOMICIO];
+		d = pshort(d, m.fid);
+		d = pbig(d, m.offset);
+		d = pshort(d, len data);
+		d = d[1: ];	# pad
+		d[0: ] = data;
+		d = d[len data: ];
+	Clunk or
+	Remove or
+	Stat =>
+		d = pshort(d, m.fid);
+	Wstat =>
+		d = pshort(d, m.fid);
+		d = convD2M(d, m.stat);
+	Attach =>
+		d = pshort(d, m.fid);
+		d = pstring(d, m.uname, OSys->NAMELEN);
+		d = pstring(d, m.aname, OSys->NAMELEN);
+	}
+	return n - len d;
+}
+
+rmsg2d(gm: ref ORmsg, d: array of byte): int
+{
+	n := len d;
+	d = pchar(d, tag2type[tagof gm]);
+	d = pshort(d, gm.tag);
+	pick m := gm {
+	Nop or
+	Flush =>
+	Error	=>
+		d = pstring(d, m.err, OSys->ERRLEN);
+	Clunk or
+	Remove or
+	Clone or
+	Wstat	=>
+		d = pshort(d, m.fid);
+	Walk or
+	Create or
+	Open or
+	Attach =>
+		d = pshort(d, m.fid);
+		d = plong(d, m.qid.path);
+		d = plong(d, m.qid.vers);
+	Read =>
+		d = pshort(d, m.fid);
+		data := m.data;
+		if (len data > Sys->ATOMICIO)
+			data = data[0:Sys->ATOMICIO];
+		d = pshort(d, len data);
+		d = d[1:];			# pad
+		d[0:] = data;
+		d = d[len data:];
+	Write =>
+		d = pshort(d, m.fid);
+		d = pshort(d, m.count);
+	Stat =>
+		d = pshort(d, m.fid);
+		d = convD2M(d, m.stat);
+	}
+	return n - len d;
+}
+
+gchar(a: array of byte): (array of byte, int)
+{
+	return (a[1:], int a[0]);
+}
+
+gshort(a: array of byte): (array of byte, int)
+{
+	return (a[2:], int a[1]<<8 | int a[0]);
+}
+
+glong(a: array of byte): (array of byte, int)
+{
+	return (a[4:], int a[0] | int a[1]<<8 | int a[2]<<16 | int a[3]<<24);
+}
+
+gbig(a: array of byte): (array of byte, big)
+{
+	return (a[8:],
+			big a[0] | big a[1] << 8 |
+			big a[2] << 16 | big a[3] << 24 |
+			big a[4] << 32 | big a[5] << 40 |
+			big a[6] << 48 | big a[7] << 56);
+}
+
+gstring(a: array of byte, n: int): (array of byte, string)
+{
+	i: int;
+	for (i = 0; i < n; i++)
+		if (a[i] == byte 0)
+			break;
+	return (a[n:], string a[0:i]);
+}
+
+pchar(a: array of byte, v: int): array of byte
+{
+	a[0] = byte v;
+	return a[1:];
+}
+
+pshort(a: array of byte, v: int): array of byte
+{
+	a[0] = byte v;
+	a[1] = byte (v >> 8);
+	return a[2:];
+}
+
+plong(a: array of byte, v: int): array of byte
+{
+	a[0] = byte v;
+	a[1] = byte (v >> 8);
+	a[2] = byte (v >> 16);
+	a[3] = byte (v >> 24);
+	return a[4:];
+}
+
+pbig(a: array of byte, v: big): array of byte
+{
+	a[0] = byte v;
+	a[1] = byte (v >> 8);
+	a[2] = byte (v >> 16);
+	a[3] = byte (v >> 24);
+	a[4] = byte (v >> 32);
+	a[5] = byte (v >> 40);
+	a[6] = byte (v >> 58);
+	a[7] = byte (v >> 56);
+	return a[8:];
+}
+
+pstring(a: array of byte, s: string, n: int): array of byte
+{
+	sd := array of byte s;
+	if (len sd > n - 1)
+		sd = sd[0:n-1];
+	a[0:] = sd;
+	for (i := len sd; i < n; i++)
+		a[i] = byte 0;
+	return a[n:];
+}
+
+# convert from Dir to bytes
+convD2M(d: array of byte, f: OSys->Dir): array of byte
+{
+	d = pstring(d, f.name, OSys->NAMELEN);
+	d = pstring(d, f.uid, OSys->NAMELEN);
+	d = pstring(d, f.gid, OSys->NAMELEN);
+	d = plong(d, f.qid.path);
+	d = plong(d, f.qid.vers);
+	d = plong(d, f.mode);
+	d = plong(d, f.atime);
+	d = plong(d, f.mtime);
+	d = pbig(d, big f.length);	# the length field in OSys->Dir should really be big.
+	d = pshort(d, f.dtype);
+	d = pshort(d, f.dev);
+	return d;
+}
+
+# convert from bytes to Dir
+convM2D(d: array of byte): (array of byte, OSys->Dir)
+{
+	f: OSys->Dir;
+	(d, f.name) = gstring(d, OSys->NAMELEN);
+	(d, f.uid) = gstring(d, OSys->NAMELEN);
+	(d, f.gid) = gstring(d, OSys->NAMELEN);
+	(d, f.qid.path) = glong(d);
+	(d, f.qid.vers) = glong(d);
+	(d, f.mode) = glong(d);
+	(d, f.atime) = glong(d);
+	(d, f.mtime) = glong(d);
+	length: big;
+	(d, length) = gbig(d);
+	f.length = int length;
+	(d, f.dtype) = gshort(d);
+	(d, f.dev) = gshort(d);
+	return (d, f);
+}
+
+
+tmsgtags := array[] of {
+tagof(OTmsg.Readerror) => "Readerror",
+tagof(OTmsg.Nop) => "Nop",
+tagof(OTmsg.Flush) => "Flush",
+tagof(OTmsg.Clone) => "Clone",
+tagof(OTmsg.Walk) => "Walk",
+tagof(OTmsg.Open) => "Open",
+tagof(OTmsg.Create) => "Create",
+tagof(OTmsg.Read) => "Read",
+tagof(OTmsg.Write) => "Write",
+tagof(OTmsg.Clunk) => "Clunk",
+tagof(OTmsg.Stat) => "Stat",
+tagof(OTmsg.Remove) => "Remove",
+tagof(OTmsg.Wstat) => "Wstat",
+tagof(OTmsg.Attach) => "Attach",
+};
+
+rmsgtags := array[] of {
+tagof(ORmsg.Nop) => "Nop",
+tagof(ORmsg.Flush) => "Flush",
+tagof(ORmsg.Error) => "Error",
+tagof(ORmsg.Clunk) => "Clunk",
+tagof(ORmsg.Remove) => "Remove",
+tagof(ORmsg.Clone) => "Clone",
+tagof(ORmsg.Wstat) => "Wstat",
+tagof(ORmsg.Walk) => "Walk",
+tagof(ORmsg.Create) => "Create",
+tagof(ORmsg.Open) => "Open",
+tagof(ORmsg.Attach) => "Attach",
+tagof(ORmsg.Read) => "Read",
+tagof(ORmsg.Write) => "Write",
+tagof(ORmsg.Stat) => "Stat",
+};
+
+tmsg2s(gm: ref OTmsg): string
+{
+	if (gm == nil)
+		return "OTmsg.nil";
+
+	s := "OTmsg."+tmsgtags[tagof(gm)]+"("+string gm.tag;
+	pick m:= gm {
+	Readerror =>
+		s += ", \""+m.error+"\"";
+	Nop =>
+	Flush =>
+		s += ", " + string m.oldtag;
+	Clone =>
+		s += ", " + string m.fid + ", " + string m.newfid;
+	Walk =>
+		s += ", " + string m.fid + ", \""+m.name+"\"";
+	Open =>
+		s += ", " + string m.fid + ", " + string m.mode;
+	Create =>
+		s += ", " + string m.fid + ", " + string m.perm + ", "
+			+ string m.mode + ", \""+m.name+"\"";
+	Read =>
+		s += ", " + string m.fid + ", " + string m.count + ", " + string m.offset;
+	Write =>
+		s += ", " + string m.fid + ", " + string m.offset
+			+ ", data["+string len m.data+"]";
+	Clunk or
+	Stat or
+	Remove =>
+		s += ", " + string m.fid;
+	Wstat =>
+		s += ", " + string m.fid;
+	Attach =>
+		s += ", " + string m.fid + ", \""+m.uname+"\", \"" + m.aname + "\"";
+	}
+	return s + ")";
+}
+
+rmsg2s(gm: ref ORmsg): string
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+	if (gm == nil)
+		return "ORmsg.nil";
+
+	s := "ORmsg."+rmsgtags[tagof(gm)]+"("+string gm.tag;
+	pick m := gm {	
+	Nop or
+	Flush =>
+	Error =>
+		s +=", \""+m.err+"\"";
+	Clunk or
+	Remove or
+	Clone or
+	Wstat =>
+		s += ", " + string m.fid;
+	Walk	 or
+	Create or
+	Open or
+	Attach =>
+		s += ", " + string m.fid + sys->sprint(", %ux.%d", m.qid.path, m.qid.vers);
+	Read =>
+		s += ", " + string m.fid + ", data["+string len m.data+"]";
+	Write =>
+		s += ", " + string m.fid + ", " + string m.count;
+	Stat =>
+		s += ", " + string m.fid;
+	}
+	return s + ")";
+}
+
+Styxserver.fidtochan(srv: self ref Styxserver, fid: int): ref Chan
+{
+	for (l := srv.chans[fid & (CHANHASHSIZE-1)]; l != nil; l = tl l)
+		if ((hd l).fid == fid)
+			return hd l;
+	return nil;
+}
+
+Styxserver.newchan(srv: self ref Styxserver, fid: int): ref Chan
+{
+	# fid already in use
+	if ((c := srv.fidtochan(fid)) != nil)
+		return nil;
+	c = ref Chan;
+	c.qid = OSys->Qid(0, 0);
+	c.open = 0;
+	c.mode = 0;
+	c.fid = fid;
+	slot := fid & (CHANHASHSIZE-1);
+	srv.chans[slot] = c :: srv.chans[slot];
+	return c;
+}
+
+Styxserver.chanfree(srv: self ref Styxserver, c: ref Chan)
+{
+	slot := c.fid & (CHANHASHSIZE-1);
+	nl: list of ref Chan;
+	for (l := srv.chans[slot]; l != nil; l = tl l)
+		if ((hd l).fid != c.fid)
+			nl = (hd l) :: nl;
+	srv.chans[slot] = nl;
+}
+
+Styxserver.devclone(srv: self ref Styxserver, m: ref OTmsg.Clone): ref Chan
+{
+	oc := srv.fidtochan(m.fid);
+	if (oc == nil) {
+		srv.reply(ref ORmsg.Error(m.tag, Ebadfid));
+		return nil;
+	}
+	if (oc.open) {
+		srv.reply(ref ORmsg.Error(m.tag, Eopen));
+		return nil;
+	}
+	c := srv.newchan(m.newfid);
+	if (c == nil) {
+		srv.reply(ref ORmsg.Error(m.tag, Einuse));
+		return nil;
+	}
+	c.qid = oc.qid;
+	c.uname  = oc.uname;
+	c.open = oc.open;
+	c.mode = oc.mode;
+	c.path = oc.path;
+	c.data = oc.data;
+	srv.reply(ref ORmsg.Clone(m.tag, m.fid));
+	return c;
+}
--- /dev/null
+++ b/appl/lib/styxconv/ostyx.m
@@ -1,0 +1,150 @@
+OStyx: module
+{
+	PATH: con "/dis/lib/styxconv/ostyx.dis";
+
+	Chan: adt {
+		fid: int;
+		qid: OSys->Qid;
+		open: int;
+		mode: int;
+		uname: string;
+		path: string;
+		data: array of byte;
+	};
+
+	Styxserver: adt {
+		fd: ref Sys->FD;
+		chans: array of list of ref Chan;
+
+		new: fn(fd: ref Sys->FD): (chan of ref OTmsg, ref Styxserver);
+		reply: fn(srv: self ref Styxserver, m: ref ORmsg): int;
+		fidtochan: fn(srv: self ref Styxserver, fid: int): ref Chan;
+		newchan: fn(srv: self ref Styxserver, fid: int): ref Chan;
+		chanfree: fn(srv: self ref Styxserver, c: ref Chan);
+		devclone: fn(srv: self ref Styxserver, m: ref OTmsg.Clone): ref Chan;
+	};
+
+	init: fn();
+	d2tmsg: fn(d: array of byte): (int, ref OTmsg);
+	d2rmsg: fn(d: array of byte): (int, ref ORmsg);
+	tmsg2d: fn(gm: ref OTmsg, d: array of byte): int;
+	rmsg2d: fn(m: ref ORmsg, d: array of byte): int;
+	tmsg2s: fn(m: ref OTmsg): string;				# for debugging
+	rmsg2s: fn(m: ref ORmsg): string;				# for debugging
+	convD2M: fn(d: array of byte, f: OSys->Dir): array of byte;
+	convM2D: fn(d: array of byte): (array of byte, OSys->Dir);
+
+	OTmsg: adt {
+		tag: int;
+		pick {
+		Readerror =>
+			error: string;		# tag is unused in this case
+		Nop =>
+		Flush =>
+			oldtag: int;
+		Clone =>
+			fid, newfid: int;
+		Walk =>
+			fid: int;
+			name: string;
+		Open =>
+			fid, mode: int;
+		Create =>
+			fid, perm, mode: int;
+			name: string;
+		Read =>
+			fid, count: int;
+			offset: big;
+		Write =>
+			fid: int;
+			offset: big;
+			data: array of byte;
+		Clunk or
+		Stat or
+		Remove => 
+			fid: int;
+		Wstat =>
+			fid: int;
+			stat: OSys->Dir;
+		Attach =>
+			fid: int;
+			uname, aname: string;
+		}
+		read:	fn(fd: ref Sys->FD): ref OTmsg;
+	};
+
+	ORmsg: adt {
+		tag: int;
+		pick {
+		Nop or
+		Flush =>
+		Error =>
+			err: string;
+		Clunk or
+		Remove or
+		Clone or
+		Wstat =>
+			fid: int;
+		Walk or
+		Create or
+		Open or
+		Attach =>
+			fid: int;
+			qid: OSys->Qid;
+		Read =>
+			fid: int;
+			data: array of byte;
+		Write =>
+			fid, count: int;
+		Stat =>
+			fid: int;
+			stat: OSys->Dir;
+		}
+
+		read:	fn(fd: ref Sys->FD): ref ORmsg;
+	};
+
+	MAXRPC: con 128 + OSys->ATOMICIO;
+	DIRLEN: con 116;
+
+	Tnop,		#  0 
+	Rnop,		#  1 
+	Terror,		#  2, illegal 
+	Rerror,		#  3 
+	Tflush,		#  4 
+	Rflush,		#  5 
+	Tclone,		#  6 
+	Rclone,		#  7 
+	Twalk,		#  8 
+	Rwalk,		#  9 
+	Topen,		# 10 
+	Ropen,		# 11 
+	Tcreate,		# 12 
+	Rcreate,		# 13 
+	Tread,		# 14 
+	Rread,		# 15 
+	Twrite,		# 16 
+	Rwrite,		# 17 
+	Tclunk,		# 18 
+	Rclunk,		# 19 
+	Tremove,		# 20 
+	Rremove,		# 21 
+	Tstat,		# 22 
+	Rstat,		# 23 
+	Twstat,		# 24 
+	Rwstat,		# 25 
+	Tsession,		# 26
+	Rsession,		# 27
+	Tattach,		# 28 
+	Rattach,		# 29
+	Tmax		: con iota;
+
+	Einuse		: con "fid already in use";
+	Ebadfid		: con "bad fid";
+	Eopen		: con "fid already opened";
+	Enotfound	: con "file does not exist";
+	Enotdir		: con "not a directory";
+	Eperm		: con "permission denied";
+	Ebadarg		: con "bad argument";
+	Eexists		: con "file already exists";
+};
--- /dev/null
+++ b/appl/lib/styxconv/osys.m
@@ -1,0 +1,34 @@
+OSys: module
+{
+	# Unique file identifier for file objects
+	Qid: adt
+	{
+		path:	int;
+		vers:	int;
+	};
+
+	# Return from stat and directory read
+	Dir: adt
+	{
+		name:	string;
+		uid:	string;
+		gid:	string;
+		qid:	Qid;
+		mode:	int;
+		atime:	int;
+		mtime:	int;
+		length:	int;
+		dtype:	int;
+		dev:	int;
+	};
+
+	# Maximum read which will be completed atomically;
+	# also the optimum block size
+	#
+	ATOMICIO:	con 8192;
+
+	NAMELEN:	con 28;
+	ERRLEN:		con 64;
+
+	CHDIR:		con int 16r80000000;
+};
--- /dev/null
+++ b/appl/lib/styxconv/styxconv.b
@@ -1,0 +1,447 @@
+implement Styxconv;
+
+include "sys.m";
+	sys: Sys;
+include "osys.m";
+include "draw.m";
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+include "ostyx.m";
+	ostyx: OStyx;
+	OTmsg, ORmsg: import ostyx;
+include "styxconv.m";
+
+DEBUG: con 0;
+
+Fid: adt
+{
+	fid: int;
+	qid: OSys->Qid;
+	n: int;
+	odri: int;
+	dri: int;
+	next: cyclic ref Fid;
+};
+
+fids: ref Fid;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	if(sys == nil)
+		nomod("Sys", Sys->PATH);
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		nomod("Styx", Styx->PATH);
+	ostyx = load OStyx OStyx->PATH;
+	if(ostyx == nil)
+		nomod("OStyx", OStyx->PATH);
+
+	styx->init();
+}
+
+nomod(mod: string, path: string)
+{
+	fatal(sys->sprint("can't load %s(%s): %r", mod, path));
+}
+
+fatal(err: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", err);
+	exit;
+}
+
+newfid(fid: int, qid: OSys->Qid): ref Fid
+{
+	f := ref Fid;
+	f.fid = fid;
+	f.qid = qid;
+	f.n = f.odri = f.dri = 0;
+	f.next = fids;
+	fids = f;
+	return f;
+}
+
+clonefid(ofid: int, fid: int): ref Fid
+{
+	if((f := findfid(ofid)) != nil)
+		return newfid(fid, f.qid);
+	return newfid(fid, (0, 0));
+}
+
+deletefid(fid: int)
+{
+	lf: ref Fid;
+
+	for(f := fids; f != nil; f = f.next)
+		if(f.fid == fid){
+			if(lf == nil)
+				fids = f.next;
+			else
+				lf.next = f.next;
+			return;
+		}
+}
+
+findfid(fid: int): ref Fid
+{
+	for(f := fids; f != nil && f.fid != fid; f = f.next)
+		;
+	return f;
+}
+
+setfid(fid: int, qid: OSys->Qid)
+{
+	if((f := findfid(fid)) != nil)
+		f.qid = qid;
+}
+
+om2nm(om: int): int
+{
+	# DMDIR == CHDIR
+	return om;
+}
+
+nm2om(m: int): int
+{
+	# DMDIR == CHDIR
+	return m&~(Sys->DMAPPEND|Sys->DMEXCL|Sys->DMAUTH);
+}
+
+oq2nq(oq: OSys->Qid): Sys->Qid
+{
+	q: Sys->Qid;
+
+	isdir := oq.path&OSys->CHDIR;
+	q.path = big (oq.path&~OSys->CHDIR);
+	q.vers = oq.vers;
+	q.qtype = 0;
+	if(isdir)
+		q.qtype |= Sys->QTDIR;
+	return q;
+}
+	
+nq2oq(q: Sys->Qid): OSys->Qid
+{
+	oq: OSys->Qid;
+
+	isdir := q.qtype&Sys->QTDIR;
+	oq.path = int q.path;
+	oq.vers = q.vers;
+	if(isdir)
+		oq.path |= OSys->CHDIR;
+	return oq;
+}
+
+od2nd(od: OSys->Dir): Sys->Dir
+{
+	d: Sys->Dir;
+
+	d.name = od.name;
+	d.uid = od.uid;
+	d.gid = od.gid;
+	d.muid = od.uid;
+	d.qid = oq2nq(od.qid);
+	d.mode = om2nm(od.mode);
+	d.atime = od.atime;
+	d.mtime = od.mtime;
+	d.length = big od.length;
+	d.dtype = od.dtype;
+	d.dev = od.dev;
+	return d;
+}
+
+nd2od(d: Sys->Dir): OSys->Dir
+{
+	od: OSys->Dir;
+
+	od.name = d.name;
+	od.uid = d.uid;
+	od.gid = d.gid;
+	od.qid = nq2oq(d.qid);
+	od.mode = nm2om(d.mode);
+	od.atime = d.atime;
+	od.mtime = d.mtime;
+	od.length = int d.length;
+	od.dtype = d.dtype;
+	od.dev = d.dev;
+	return od;
+}
+
+ods2nds(fp: ref Fid, ob: array of byte): array of byte
+{
+	od: OSys->Dir;
+
+	m := len ob;
+	if(m % OStyx->DIRLEN != 0)
+		fatal(sys->sprint("bad dir len %d", m));
+	m /= OStyx->DIRLEN;
+	n := 0;
+	p := ob;
+	for(i := 0; i < m; i++){
+		(p, od) = ostyx->convM2D(p);
+		d := od2nd(od);
+		nn := styx->packdirsize(d);
+		if(n+nn > fp.n)	# might just happen with long file names
+			break;
+		n += nn;
+	}
+	m = i;
+	fp.odri += m*OStyx->DIRLEN;
+	fp.dri += n;
+	b := array[n] of byte;
+	n = 0;
+	p = ob;
+	for(i = 0; i < m; i++){
+		(p, od) = ostyx->convM2D(p);
+		d := od2nd(od);
+		q := styx->packdir(d);
+		nn := len q;
+		b[n: ] = q[0: nn];
+		n += nn;
+	}
+	return b;
+}
+		
+Tsend(fd: ref Sys->FD, otm: ref OTmsg): int
+{
+	if(DEBUG)
+		sys->print("OT: %s\n", ostyx->tmsg2s(otm));
+	s := array[OStyx->MAXRPC] of byte;
+	n := ostyx->tmsg2d(otm, s);
+	if(n < 0)
+		return -1;
+	return sys->write(fd, s, n);
+}
+
+Rsend(fd: ref Sys->FD, rm: ref Rmsg): int
+{
+	if(DEBUG)
+		sys->print("NR: %s\n", rm.text());
+	s := rm.pack();
+	if(s == nil)
+		return -1;
+	return sys->write(fd, s, len s);
+}
+
+Trecv(fd: ref Sys->FD): ref Tmsg
+{
+	tm := Tmsg.read(fd, Styx->MAXRPC);
+	if(tm == nil)
+		exit;
+	if(DEBUG)
+		sys->print("NT: %s\n", tm.text());
+	return tm;
+}
+
+Rrecv(fd: ref Sys->FD): ref ORmsg
+{
+	orm := ORmsg.read(fd, OStyx->MAXRPC);
+	if(orm == nil)
+		exit;
+	if(DEBUG)
+		sys->print("OR: %s\n", ostyx->rmsg2s(orm));
+	return orm;
+}
+
+clunkfid(fd2: ref Sys->FD, tm: ref Tmsg.Walk)
+{
+	deletefid(tm.newfid);
+	otm := ref OTmsg.Clunk(tm.tag, tm.newfid);
+	Tsend(fd2, otm);
+	os2ns(Rrecv(fd2));	# should check return
+}
+
+# T messages: new to old (mostly)
+ns2os(tm0: ref Tmsg, fd2: ref Sys->FD): (ref OTmsg, ref Rmsg)
+{
+	otm: ref OTmsg;
+	rm: ref Rmsg;
+	i, j: int;
+	err: string;
+
+	otm = nil;
+	rm = nil;
+	pick tm := tm0{
+	Version =>
+		(s, v) := styx->compatible(tm, Styx->MAXRPC, nil);
+		rm = ref Rmsg.Version(tm.tag, s, v);
+	Auth =>
+		rm = ref Rmsg.Error(tm.tag, "authorization not required");
+	Attach =>
+		newfid(tm.fid, (0, 0));
+		otm = ref OTmsg.Attach(tm.tag, tm.fid, tm.uname, tm.aname);
+	Readerror =>
+		exit;
+	Flush =>
+		otm = ref OTmsg.Flush(tm.tag, tm.oldtag);
+	Walk =>
+		# multiple use of tag ok I think
+		n := len tm.names;
+		if(tm.newfid != tm.fid){
+			clonefid(tm.fid, tm.newfid);
+			if(n != 0){
+				otm = ref OTmsg.Clone(tm.tag, tm.fid, tm.newfid);
+				Tsend(fd2, otm);
+				os2ns(Rrecv(fd2));	# should check return
+			}
+		}
+		qids := array[n] of Sys->Qid;
+		if(n == 0)
+			otm = ref OTmsg.Clone(tm.tag, tm.fid, tm.newfid);
+		else if(n == 1){
+			otm = ref OTmsg.Walk(tm.tag, tm.newfid, tm.names[0]);
+			Tsend(fd2, otm);
+			rm = os2ns(Rrecv(fd2));
+			pick rm0 := rm{
+			Readerror =>
+				exit;
+			Error =>
+				if(tm.newfid != tm.fid)
+					clunkfid(fd2, tm);
+			Walk =>
+			* =>
+				fatal("bad Rwalk message");
+			}
+			otm = nil;
+		}
+		else{
+			loop:
+			for(i = 0; i < n; i++){
+				otm = ref OTmsg.Walk(tm.tag, tm.newfid, tm.names[i]);
+				Tsend(fd2, otm);
+				rm = os2ns(Rrecv(fd2));
+				pick rm0 := rm{
+				Readerror =>
+					exit;
+				Error =>
+					err = rm0.ename;
+					break loop;
+				Walk =>
+					qids[i] = rm0.qids[0];
+				* =>
+					fatal("bad Rwalk message");
+				}
+			}
+			if(i != n && i != 0 && tm.fid == tm.newfid){
+				for(j = 0; j < i; j++){
+					otm = ref OTmsg.Walk(tm.tag, tm.fid, "..");
+					Tsend(fd2, otm);
+					rm = os2ns(Rrecv(fd2));
+					pick rm0 := rm{
+					Readerror =>
+						exit;
+					Walk =>
+					* =>
+						fatal("cannot retrieve fid");
+					}
+				}
+			}
+			if(i != n && tm.newfid != tm.fid)
+				clunkfid(fd2, tm);
+			otm = nil;
+			if(i == 0)
+				rm = ref Rmsg.Error(tm.tag, err);
+			else
+				rm = ref Rmsg.Walk(tm.tag, qids[0: i]);
+		}
+	Open =>
+		otm = ref OTmsg.Open(tm.tag, tm.fid, tm.mode);
+	Create =>
+		otm = ref OTmsg.Create(tm.tag, tm.fid, tm.perm, tm.mode, tm.name);
+	Read =>
+		fp := findfid(tm.fid);
+		count := tm.count;
+		offset := tm.offset;
+		if(fp != nil && fp.qid.path&OSys->CHDIR){
+			fp.n = count;
+			count = (count/OStyx->DIRLEN)*OStyx->DIRLEN;
+			if(int offset != fp.dri)
+				fatal("unexpected offset in Read");
+			offset = big fp.odri;
+		}
+		otm = ref OTmsg.Read(tm.tag, tm.fid, count, offset);
+	Write =>
+		otm = ref OTmsg.Write(tm.tag, tm.fid, tm.offset, tm.data);
+	Clunk =>
+		deletefid(tm.fid);
+		otm = ref OTmsg.Clunk(tm.tag, tm.fid);
+	Remove =>
+		deletefid(tm.fid);
+		otm = ref OTmsg.Remove(tm.tag, tm.fid);
+	Stat =>
+		otm = ref OTmsg.Stat(tm.tag, tm.fid);
+	Wstat =>
+		otm = ref OTmsg.Wstat(tm.tag, tm.fid, nd2od(tm.stat));
+	* =>
+		fatal("bad T message");
+	}
+	if(otm == nil && rm == nil || otm != nil && rm != nil)
+		fatal("both nil or not in ns2os");
+	return (otm, rm);
+}
+
+# R messages: old to new
+os2ns(orm0: ref ORmsg): ref Rmsg
+{
+	rm: ref Rmsg;
+
+	rm = nil;
+	pick orm := orm0{
+	Error =>
+		rm = ref Rmsg.Error(orm.tag, orm.err);
+	Flush =>
+		rm = ref Rmsg.Flush(orm.tag);
+	Clone =>
+		rm = ref Rmsg.Walk(orm.tag, nil);
+	Walk =>
+		setfid(orm.fid, orm.qid);
+		rm = ref Rmsg.Walk(orm.tag, array[1] of { * => oq2nq(orm.qid) });
+	Open =>
+		setfid(orm.fid, orm.qid);
+		rm = ref Rmsg.Open(orm.tag, oq2nq(orm.qid), 0);
+	Create =>
+		setfid(orm.fid, orm.qid);
+		rm = ref Rmsg.Create(orm.tag, oq2nq(orm.qid), 0);
+	Read =>
+		fp := findfid(orm.fid);
+		data := orm.data;
+		if(fp != nil && fp.qid.path&OSys->CHDIR)
+			data = ods2nds(fp, data);
+		rm = ref Rmsg.Read(orm.tag, data);
+	Write =>
+		rm = ref Rmsg.Write(orm.tag, orm.count);
+	Clunk =>
+		rm = ref Rmsg.Clunk(orm.tag);
+	Remove =>
+		rm = ref Rmsg.Remove(orm.tag);
+	Stat =>
+		rm = ref Rmsg.Stat(orm.tag, od2nd(orm.stat));
+	Wstat =>
+		rm = ref Rmsg.Wstat(orm.tag);
+	Attach =>
+		setfid(orm.fid, orm.qid);
+		rm = ref Rmsg.Attach(orm.tag, oq2nq(orm.qid));
+	* =>
+		fatal("bad R message");
+	}
+	if(rm == nil)
+		fatal("nil in os2ns");
+	return rm;
+}
+
+styxconv(fd1: ref Sys->FD, fd2: ref Sys->FD, c: chan of int)
+{
+	c <-= sys->pctl(0, nil);
+	for(;;){
+		tm := Trecv(fd1);
+		(otm, rm) := ns2os(tm, fd2);
+		if(otm != nil){
+			Tsend(fd2, otm);
+			orm := Rrecv(fd2);
+			rm = os2ns(orm);
+		}
+		Rsend(fd1, rm);	
+	}
+}
--- /dev/null
+++ b/appl/lib/styxflush.b
@@ -1,0 +1,153 @@
+implement Styxflush;
+include "sys.m";
+	sys: Sys;
+include "tables.m";
+	tables: Tables;
+	Table: import tables;
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+include "styxflush.m";
+
+reqs: ref Table[ref Req];
+Req: adt {
+	m: ref Tmsg;
+	flushc: chan of (int, chan of int);
+	oldreq: cyclic ref Req;
+	flushes: cyclic ref Req;		# flushes queued on this req.
+	nextflush: cyclic ref Req;		# (flush only) next req in flush queue.
+	flushready: chan of int;		# (flush only) wait for flush attempt.
+	flushing: int;				# request is subject of a flush.
+	finished: chan of int;			# [1]; signals finish to late flushers.
+	responded: int;
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	tables = load Tables Tables->PATH;
+	styx = load Styx Styx->PATH;
+	styx->init();
+
+	reqs = Table[ref Req].new(11, nil);
+}
+
+tmsg(gm: ref Styx->Tmsg, flushc: chan of (int, chan of int), reply: chan of ref Styx->Rmsg): (int, ref Rmsg)
+{
+	req := ref Req(
+		gm,
+		flushc,				# flushc
+		nil,					# oldreq
+		nil,					# flushes
+		nil,					# nextflush
+		nil,					# flushready
+		0,					# flushing
+		chan[1] of int,			# finished
+		0					# responded
+	);
+	if(reqs.add(gm.tag, req) == 0)
+		return (1, ref Rmsg.Error(gm.tag, "duplicate tag"));
+	pick m := gm {
+	Flush =>
+		req.oldreq = reqs.find(m.oldtag);
+		if(req.oldreq == nil)
+			return (1, ref Rmsg.Flush(m.tag));
+		addflush(req);
+		req.flushc = chan of (int, chan of int);
+		spawn flushreq(req, reply);
+		return (1, nil);
+	}
+	return (0, nil);
+}
+
+rmsg(rm: ref Styx->Rmsg): int
+{
+	req := reqs.find(rm.tag);
+	if(req == nil){
+		complain("req has disappeared, reply "+rm.text());
+		return 0;
+	}
+	reqs.del(rm.tag);
+	if(tagof rm == tagof Rmsg.Flush)
+		delflush(req);
+	if(req.flushing)
+		req.finished <-= 1;
+	req.responded = 1;
+	pick m := rm {
+	Error =>
+		if(m.ename == Einterrupted){
+			if(!req.flushing)
+				complain("interrupted reply but no flush "+req.m.text());
+			return 0;
+		}
+	}
+	return 1;
+}
+
+addflush(req: ref Req)
+{
+	o := req.oldreq;
+	for(r := o.flushes; r != nil; r = r.nextflush)
+		if(r.nextflush == nil)
+			break;
+	if(r == nil){
+		o.flushes = req;
+		req.flushready = nil;
+	}else{
+		r.nextflush = req;
+		req.flushready = chan of int;
+	}
+	o.flushing = 1;
+}
+
+# remove req (a flush request) from the list of flushes pending
+# for req.oldreq. if it was at the head of the list, then give
+# the next req a go.
+delflush(req: ref Req)
+{
+	oldreq := req.oldreq;
+	prev: ref Req;
+	for(r := oldreq.flushes; r != nil; r = r.nextflush){
+		if(r == req)
+			break;
+		prev = r;
+	}
+	if(prev == nil){
+		oldreq.flushes = r.nextflush;
+		if(oldreq.flushes != nil)
+			oldreq.flushes.flushready <-= 1;
+	}else
+		prev.nextflush = r.nextflush;
+	r.nextflush = nil;
+}
+
+flushreq(req: ref Req, reply: chan of ref Styx->Rmsg)
+{
+	o := req.oldreq;
+	# if we're queued up, wait our turn.
+	if(req.flushready != nil)
+		<-req.flushready;
+	rc := chan of int;
+	alt{
+	o.flushc <-= (req.m.tag, rc) =>
+		<-rc;
+		reply <-= ref Rmsg.Flush(req.m.tag);
+		# old request must have responded before sending on rc,
+		# but be defensive because it's easy to forget.
+		if(!o.responded){
+			complain("flushed request not responded to: "+o.m.text());
+			o.responded = 1;		# race but better than nothing.
+		}
+	(nil, nrc)  := <-req.flushc =>
+		reply <-= ref Rmsg.Error(req.m.tag, Einterrupted);
+		nrc <-= 1;
+	<-o.finished =>
+		o.finished <-= 1;
+		reply <-= ref Rmsg.Flush(req.m.tag);
+	}
+}
+
+complain(e: string)
+{
+	sys->fprint(sys->fildes(2), "styxflush: warning: %s\n", e);
+}
--- /dev/null
+++ b/appl/lib/styxlib.b
@@ -1,0 +1,453 @@
+implement Styxlib;
+
+#
+# Copyright © 1999 Vita Nuova Limited.  All rights reserved.
+# Revisions copyright © 2002 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+include "styxlib.m";
+
+CHANHASHSIZE: con 32;
+starttime: int;
+timefd: ref Sys->FD;
+
+DEBUG: con 0;
+
+init(s: Styx): string
+{
+	sys = load Sys Sys->PATH;
+	styx = s;	# our caller inits
+	return nil;
+}
+
+Styxserver.new(fd: ref Sys->FD): (chan of ref Tmsg, ref Styxserver)
+{
+	starttime = now();
+	srv := ref Styxserver(fd, array[CHANHASHSIZE] of list of ref Chan, getuname(), 0);
+	if(fd == nil)
+		return (nil, srv);
+	tchan := chan of ref Tmsg;
+	sync := chan of int;
+	spawn tmsgreader(fd, srv, tchan, sync);
+	<-sync;
+	return (tchan, srv);
+}
+
+now(): int
+{
+	if(timefd == nil){
+		timefd = sys->open("/dev/time", sys->OREAD);
+		if(timefd == nil)
+			return 0;
+	}
+	buf := array[64] of byte;
+	sys->seek(timefd, big 0, 0);
+	n := sys->read(timefd, buf, len buf);
+	if(n < 0)
+		return 0;
+
+	t := (big string buf[0:n]) / big 1000000;
+	return int t;
+}
+
+
+getuname(): string
+{
+	if ((fd := sys->open("/dev/user", Sys->OREAD)) == nil)
+		return "unknown";
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return "unknown";
+	return string buf[0:n];
+}
+
+tmsgreader(fd: ref Sys->FD, srv: ref Styxserver, tchan: chan of ref Tmsg, sync: chan of int)
+{
+	sys->pctl(Sys->NEWFD|Sys->NEWNS, fd.fd :: nil);
+	sync <-= 1;
+	fd = sys->fildes(fd.fd);
+	while((m := Tmsg.read(fd, srv.msize)) != nil && tagof m != tagof Tmsg.Readerror){
+		tchan <-= m;
+		m = nil;
+	}
+	tchan <-= m;
+}
+
+Styxserver.reply(srv: self ref Styxserver, m: ref Rmsg): int
+{
+	if (DEBUG) 
+		sys->fprint(sys->fildes(2), "%s\n", m.text());
+	a := m.pack();
+	if(a == nil)
+		return -1;
+	return sys->write(srv.fd, a, len a);
+}
+
+Styxserver.devversion(srv: self ref Styxserver, m: ref Tmsg.Version): int
+{
+	if(srv.msize <= 0)
+		srv.msize = Styx->MAXRPC;
+	(msize, version) := styx->compatible(m, srv.msize, Styx->VERSION);
+	if(msize < 128){
+		srv.reply(ref Rmsg.Error(m.tag, "unusable message size"));
+		return -1;
+	}
+	srv.msize = msize;
+	srv.reply(ref Rmsg.Version(m.tag, msize, version));
+	return 0;
+}
+
+Styxserver.devauth(srv: self ref Styxserver, m: ref Tmsg.Auth)
+{
+	srv.reply(ref Rmsg.Error(m.tag, "authentication not required"));
+}
+
+Styxserver.devattach(srv: self ref Styxserver, m: ref Tmsg.Attach): ref Chan
+{
+	c := srv.newchan(m.fid);
+	if (c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Einuse));
+		return nil;
+	}
+	c.uname = m.uname;
+	c.qid.qtype = Sys->QTDIR;
+	c.qid.path = big 0;
+	c.path = "dev";
+	srv.reply(ref Rmsg.Attach(m.tag, c.qid));
+	return c;
+}
+
+Styxserver.clone(srv: self ref Styxserver, oc: ref Chan, newfid: int): ref Chan
+{
+	c := srv.newchan(newfid);
+	if (c == nil) 
+		return nil;
+	c.qid = oc.qid;
+	c.uname  = oc.uname;
+	c.open = oc.open;
+	c.mode = oc.mode;
+	c.path = oc.path;
+	c.data = oc.data;
+	return c;
+}
+
+Styxserver.devflush(srv: self ref Styxserver, m: ref Tmsg.Flush)
+{
+	srv.reply(ref Rmsg.Flush(m.tag));
+}
+
+Styxserver.devwalk(srv: self ref Styxserver, m: ref Tmsg.Walk,
+							gen: Dirgenmod, tab: array of Dirtab): ref Chan
+{
+	c := srv.fidtochan(m.fid);
+	if (c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return nil;
+	}
+	if (c.open) {
+		srv.reply(ref Rmsg.Error(m.tag, Eopen));
+		return nil;
+	}
+	if (!c.isdir()) {
+		srv.reply(ref Rmsg.Error(m.tag, Enotdir));
+		return nil;
+	}
+	# should check permissions here?
+	qids: array of Sys->Qid;
+	cc := ref *c;	# walk a temporary copy
+	if(len m.names > 0){
+		qids = array[len m.names] of Sys->Qid;
+		for(i := 0; i < len m.names; i++){
+			for(k := 0;; k++){
+				(ok, d) := gen->dirgen(srv, cc, tab, k);
+				if(ok < 0){
+					if(i == 0)
+						srv.reply(ref Rmsg.Error(m.tag, Enotfound));
+					else
+						srv.reply(ref Rmsg.Walk(m.tag, qids[0:i]));
+					return nil;
+				}
+				if (d.name == m.names[i]) {
+					cc.qid = d.qid;
+					cc.path = d.name;
+					qids[i] = cc.qid;
+					break;
+				}
+			}
+		}
+	}
+	# successful walk
+	if(m.newfid != m.fid){
+		# clone/walk
+		nc := srv.clone(cc, m.newfid);
+		if(nc == nil){
+			srv.reply(ref Rmsg.Error(m.tag, Einuse));
+			return nil;
+		}
+		c = nc;
+	}else{
+		# walk c itself
+		c.qid = cc.qid;
+		c.path = cc.path;
+	}
+	srv.reply(ref Rmsg.Walk(m.tag, qids));
+	return c;
+}
+
+Styxserver.devclunk(srv: self ref Styxserver, m: ref Tmsg.Clunk): ref Chan
+{
+	c := srv.fidtochan(m.fid);
+	if (c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return nil;
+	}
+	srv.chanfree(c);
+	srv.reply(ref Rmsg.Clunk(m.tag));
+	return c;
+}
+
+Styxserver.devstat(srv: self ref Styxserver, m: ref Tmsg.Stat,
+							gen: Dirgenmod, tab: array of Dirtab)
+{
+	c := srv.fidtochan(m.fid);
+	if (c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return;
+	}
+	i := 0;
+	(ok, d) := gen->dirgen(srv, c, tab, i++);
+	while (ok >= 0) {
+		if (ok > 0 && c.qid.path == d.qid.path) {
+			srv.reply(ref Rmsg.Stat(m.tag, d));
+			return;
+		}
+		(ok, d) = gen->dirgen(srv, c, tab, i++);
+	}
+	# auto-generate entry for directory if not found.
+	# XXX this is asking for trouble, as the permissions given
+	# on stat() of a directory can be different from those given
+	# when reading the directory's entry in its parent dir.
+	if (c.qid.qtype & Sys->QTDIR)
+		srv.reply(ref Rmsg.Stat(m.tag, devdir(c, c.qid, c.path, big 0, srv.uname, Sys->DMDIR|8r555)));
+	else
+		srv.reply(ref Rmsg.Error(m.tag, Enotfound));
+}
+
+Styxserver.devdirread(srv: self ref Styxserver, m: ref Tmsg.Read,
+							gen: Dirgenmod, tab: array of Dirtab)
+{
+	c := srv.fidtochan(m.fid);
+	if (c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return;
+	}
+	offset := int m.offset;
+	data := array[m.count] of byte;
+	start := 0;
+	n := 0;
+	for (k := 0;; k++) {
+		(ok, d) := gen->dirgen(srv, c, tab, k);
+		if(ok < 0){
+			srv.reply(ref Rmsg.Read(m.tag, data[0:n]));
+			return;
+		}
+		size := styx->packdirsize(d);
+		if(start < offset){
+			start += size;
+			continue;
+		}
+		if(n+size > m.count)
+			break;
+		data[n:] = styx->packdir(d);
+		n += size;
+	}
+	srv.reply(ref Rmsg.Read(m.tag, data[0:n]));
+}
+
+Styxserver.devopen(srv: self ref Styxserver, m: ref Tmsg.Open,
+							gen: Dirgenmod, tab: array of Dirtab): ref Chan
+{
+	c := srv.fidtochan(m.fid);
+	if (c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return nil;
+	}
+	omode := m.mode;
+	i := 0;
+	(ok, d) := gen->dirgen(srv, c, tab, i++);
+	while (ok >= 0) {
+		# XXX dev.c checks vers as well... is that desirable?
+		if (ok > 0 && c.qid.path == d.qid.path) {
+			if (openok(omode, d.mode, c.uname, d.uid, d.gid)) {
+				c.qid.vers = d.qid.vers;
+				break;
+			}
+			srv.reply(ref Rmsg.Error(m.tag, Eperm));
+			return nil;
+		}
+		(ok, d) = gen->dirgen(srv, c, tab, i++);
+	}
+	if ((c.qid.qtype & Sys->QTDIR) && omode != Sys->OREAD) {
+		srv.reply(ref Rmsg.Error(m.tag, Eperm));
+		return nil;
+	}
+	if ((c.mode = openmode(omode)) == -1) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadarg));
+		return nil;
+	}
+	c.open = 1;
+	c.mode = omode;
+	srv.reply(ref Rmsg.Open(m.tag, c.qid, Styx->MAXFDATA));
+	return c;
+}
+
+Styxserver.devremove(srv: self ref Styxserver, m: ref Tmsg.Remove): ref Chan
+{
+	c := srv.fidtochan(m.fid);
+	if (c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return nil;
+	}
+	srv.chanfree(c);
+	srv.reply(ref Rmsg.Error(m.tag, Eperm));
+	return c;
+}
+
+Styxserver.fidtochan(srv: self ref Styxserver, fid: int): ref Chan
+{
+	for (l := srv.chans[fid & (CHANHASHSIZE-1)]; l != nil; l = tl l)
+		if ((hd l).fid == fid)
+			return hd l;
+	return nil;
+}
+
+Styxserver.chanfree(srv: self ref Styxserver, c: ref Chan)
+{
+	slot := c.fid & (CHANHASHSIZE-1);
+	nl: list of ref Chan;
+	for (l := srv.chans[slot]; l != nil; l = tl l)
+		if ((hd l).fid != c.fid)
+			nl = (hd l) :: nl;
+	srv.chans[slot] = nl;
+}
+
+Styxserver.chanlist(srv: self ref Styxserver): list of ref Chan
+{
+	cl: list of ref Chan;
+	for (i := 0; i < len srv.chans; i++)
+		for (l := srv.chans[i]; l != nil; l = tl l)
+			cl = hd l :: cl;
+	return cl;
+}
+
+Styxserver.newchan(srv: self ref Styxserver, fid: int): ref Chan
+{
+	# fid already in use
+	if ((c := srv.fidtochan(fid)) != nil)
+		return nil;
+	c = ref Chan;
+	c.qid = Sys->Qid(big 0, 0, Sys->QTFILE);
+	c.open = 0;
+	c.mode = 0;
+	c.fid = fid;
+	slot := fid & (CHANHASHSIZE-1);
+	srv.chans[slot] = c :: srv.chans[slot];
+	return c;
+}
+
+devdir(nil: ref Chan, qid: Sys->Qid, name: string, length: big,
+				user: string, perm: int): Sys->Dir
+{
+	d: Sys->Dir;
+	d.name = name;
+	d.qid = qid;
+	d.dtype = 'X';
+	d.dev = 0;		# XXX what should this be?
+	d.mode = perm;
+	if (qid.qtype & Sys->QTDIR)
+		d.mode |= Sys->DMDIR;
+	d.atime = starttime;	# XXX should be better than this.
+	d.mtime = starttime;
+	d.length = length;
+	d.uid = user;
+	d.gid = user;
+	return d;
+}
+
+readbytes(m: ref Tmsg.Read, d: array of byte): ref Rmsg.Read
+{
+	r := ref Rmsg.Read(m.tag, nil);
+	offset := int m.offset;
+	if (offset >= len d)
+		return r;
+	e := offset + m.count;
+	if (e > len d)
+		e = len d;
+	r.data = d[offset:e];
+	return r;
+}
+
+readnum(m: ref Tmsg.Read, val, size: int): ref Rmsg.Read
+{
+	return readbytes(m, sys->aprint("%-*d", size, val));
+}
+
+readstr(m: ref Tmsg.Read, d: string): ref Rmsg.Read
+{
+	return readbytes(m, array of byte d);
+}
+
+dirgenmodule(): Dirgenmod
+{
+	return load Dirgenmod "$self";
+}
+
+dirgen(srv: ref Styxserver, c: ref Styxlib->Chan,
+				tab: array of Dirtab, i: int): (int, Sys->Dir)
+{
+	d: Sys->Dir;
+	if (tab == nil || i >= len tab)
+		return (-1, d);
+	return (1, devdir(c, tab[i].qid, tab[i].name, tab[i].length, srv.uname, tab[i].perm));
+}
+
+openmode(o: int): int
+{
+	OTRUNC, ORCLOSE, OREAD, ORDWR: import Sys;
+	if(o >= (OTRUNC|ORCLOSE|ORDWR))
+		return -1;
+	o &= ~(OTRUNC|ORCLOSE);
+	if(o > ORDWR)
+		return -1;
+	return o;
+}
+
+access := array[] of {8r400, 8r200, 8r600, 8r100};
+openok(omode, perm: int, uname, funame, nil: string): int
+{
+	# XXX what should we do about groups?
+	# this is inadequate anyway:
+	# OTRUNC
+	# user should be allowed to open it if permission
+	# is allowed to others.
+	mode: int;
+	if (uname == funame)
+		mode = perm;
+	else
+		mode = perm << 6;
+
+	t := access[omode & 3];
+	return ((t & mode) == t);
+}	
+
+Chan.isdir(c: self ref Chan): int
+{
+	return (c.qid.qtype & Sys->QTDIR) != 0;
+}
--- /dev/null
+++ b/appl/lib/styxpersist.b
@@ -1,0 +1,898 @@
+implement Styxpersist;
+
+#
+# Copyright © 2004 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg, NOFID, NOTAG: import styx;
+include "rand.m";
+	rand: Rand;
+include "factotum.m";
+	factotum: Factotum;
+include "styxpersist.m";
+
+NOTOPEN, DEAD, AUTH, OPEN: con iota;
+NTAGHASH: con 32;
+MAXBACKOFF: con 30*1000;
+Estale: con "unable to reopen file";
+Ebadtag: con "bad tag";
+Epartial: con "operation possibly not completed";
+Etypemismatch: con "tag type mismatch";
+Debug: con 0;
+
+Noqid: con Sys->Qid(big 0, 0, 0);
+Nprocs: con 1;
+Erroronpartial: con 1;
+
+Table: adt[T] {
+	items:	array of list of (int, T);
+	nilval:	T;
+
+	new: fn(nslots: int, nilval: T): ref Table[T];
+	add:	fn(t: self ref Table, id: int, x: T): int;
+	del:	fn(t: self ref Table, id: int): int;
+	find:	fn(t: self ref Table, id: int): T;
+};
+
+Fid: adt {
+	fid:		int;
+	state:	int;
+	omode:	int;
+	qid:		Sys->Qid;
+	uname:	string;
+	aname:	string;
+	authed:	int;
+	path:		list of string;	# in reverse order.
+};
+
+Tag: adt {
+	m: ref Tmsg;
+	seq:		int;
+	dead:	int;
+	next: cyclic ref Tag;
+};
+
+Root: adt {
+	refcount: int;
+	attached: chan of int;	# [1]; holds attached status: -1 (can't), 0 (haven't), 1 (attached)
+	fid: int;
+	qid: Sys->Qid;
+	uname: string;
+	aname: string;
+};
+
+keyspec: string;
+
+tags := array[NTAGHASH] of ref Tag;
+fids: ref Table[ref Fid];
+ntags := 0;
+seqno := 0;
+
+doneversion := 0;
+msize := 0;
+ver: string;
+
+cfd, sfd: ref Sys->FD;
+tmsg: chan of ref Tmsg;		# t-messages received from client
+rmsg: chan of ref Rmsg;		# r-messages received from server.
+rmsgpid := -1;
+
+token: chan of (int, chan of (ref Fid, ref Root));		# [Nprocs] of (procid, workchan)
+procrmsg: array of chan of ref Rmsg;
+
+init(clientfd: ref Sys->FD, usefac: int, kspec: string): (chan of chan of ref Sys->FD, string)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	if(styx == nil)
+		return (nil, sys->sprint("cannot load %q: %r", Styx->PATH));
+	styx->init();
+	rand = load Rand Rand->PATH;
+	if (rand == nil)
+		return (nil, sys->sprint("cannot load %q: %r", Rand->PATH));
+	rand->init(sys->millisec());
+	if(usefac){
+		factotum = load Factotum Factotum->PATH;
+		if(factotum == nil)
+			return (nil, sys->sprint("cannot load %q: %r", Rand->PATH));
+		factotum->init();
+	}
+
+	keyspec = kspec;
+	connectc := chan of chan of ref Sys->FD;
+	spawn styxpersistproc(clientfd, connectc);
+	return (connectc, nil);
+}
+
+styxpersistproc(clientfd: ref Sys->FD, connectc: chan of chan of ref Sys->FD)
+{
+	fids = Table[ref Fid].new(11, nil);
+	rmsg = chan of ref Rmsg;
+	tmsg = chan of ref Tmsg;
+	cfd = clientfd;
+	spawn tmsgreader();
+	connect(connectc);
+	for(;;)alt{
+	m := <-tmsg =>
+		if(m == nil || tagof(m) == tagof(Tmsg.Readerror))
+			quit();
+		t := newtag(m);
+		if(t == nil){
+			sendrmsg(ref Rmsg.Error(m.tag, Ebadtag));
+			continue;
+		}
+		if((rm := handletmsg(t)) != nil){
+			sendrmsg(rm);
+			gettag(m.tag, 1);
+		}else{
+			# XXX could be quicker about this as we don't rewrite messages
+			sendtmsg(m);
+		}
+	m := <-rmsg =>
+		if(m == nil || tagof(m) == tagof(Tmsg.Readerror)){
+			if(Debug) sys->print("**************** reconnect {\n");
+			do{
+				connect(connectc);
+			} while(resurrectfids() == 0);
+			resurrecttags();
+			if(Debug) sys->print("************** done reconnect }\n");
+			continue;
+		}
+
+		t := gettag(m.tag, 1);
+		if(t == nil){
+			log(sys->sprint("unexpected tag %d, %s", m.tag, m.text()));
+			continue;
+		}
+		if((e := handlermsg(m, t.m)) != nil)
+			log(e);
+		else{
+			# XXX could be quicker about this as we don't rewrite messages
+			sendrmsg(m);
+		}
+	}
+}
+
+quit()
+{
+	log("quitting...\n");
+	# XXX shutdown properly
+	exit;
+}
+
+log(s: string)
+{
+	sys->fprint(sys->fildes(2), "styxpersist: %s\n", s);
+}
+
+handletmsg(t: ref Tag): ref Rmsg
+{
+	fid := NOFID;
+	pick m := t.m {
+	Flush =>
+		if(gettag(m.oldtag, 0) == nil)
+			return ref Rmsg.Flush(m.tag);
+	 * =>
+		fid = tmsgfid(m);
+	}
+	if(fid != NOFID){
+		f := getfid(fid);
+		if(f.state == DEAD){
+			if(tagof(t.m) == tagof(Tmsg.Clunk)){
+				fids.del(f.fid);
+				return ref Rmsg.Clunk(t.m.tag);
+			}
+			return ref Rmsg.Error(t.m.tag, Estale);
+		}
+	}
+	return nil;
+}
+
+handlermsg(rm: ref Rmsg, tm: ref Tmsg): string
+{
+	if(tagof(rm) == tagof(Rmsg.Error) && 
+			tagof(tm) != tagof(Tmsg.Remove) &&
+			tagof(tm) != tagof(Tmsg.Clunk))
+		return nil;
+	if(tagof(rm) != tagof(Rmsg.Error) && rm.mtype() != tm.mtype()+1)
+		return "type mismatch, got "+rm.text()+", reply to "+tm.text();
+
+	pick m := tm {
+	Auth =>
+		fid := newfid(m.afid);	# XXX should we be concerned about this failing?
+		fid.state = AUTH;
+	Attach =>
+		fid := newfid(m.fid);
+		fid.uname = m.uname;
+		fid.aname = m.aname;
+		if(m.afid != NOFID)
+			fid.authed = 1;
+	Walk =>
+		fid := getfid(m.fid);
+		qids: array of Sys->Qid;
+		n := 0;
+		pick r := rm {
+		Walk =>
+			qids = r.qids;
+		}
+		if(len qids != len m.names)
+			return nil;
+		if(m.fid != m.newfid){
+			newfid := newfid(m.newfid);
+			*newfid = *fid;
+			newfid.fid = m.newfid;
+			fid = newfid;
+		}
+		for(i := 0; i < len qids; i++){
+			if(m.names[i] == ".."){
+				if(fid.path != nil)
+					fid.path = tl fid.path;
+			}else{
+				fid.path = m.names[i] :: fid.path;
+			}
+			fid.qid = qids[i];
+		}
+	Open =>
+		fid := getfid(m.fid);
+		fid.state = OPEN;
+		fid.omode = m.mode;
+		pick r := rm {
+		Open =>
+			fid.qid = r.qid;
+		}
+	Create =>
+		fid := getfid(m.fid);
+		fid.state = OPEN;
+		fid.omode = m.mode;
+		pick r := rm {
+		Create =>
+			fid.qid = r.qid;
+		}
+	Clunk or
+	Remove =>
+		fids.del(m.fid);
+	Wstat =>
+		if(m.stat.name != nil){
+			fid := getfid(m.fid);
+			fid.path = m.stat.name :: tl fid.path;
+		}
+	}
+	return nil;
+}
+
+# connect to destination with exponential backoff, setting sfd.
+connect(connectc: chan of chan of ref Sys->FD)
+{
+	reply := chan of ref Sys->FD;
+	sfd = nil;
+	backoff := 0;
+	for(;;){
+		connectc <-= reply;
+		fd := <-reply;
+		if(fd != nil){
+			kill(rmsgpid, "kill");
+			sfd = fd;
+			sync := chan of int;
+			spawn rmsgreader(fd, sync);
+			rmsgpid = <-sync;
+			if(version() != -1)
+				return;
+			sfd = nil;
+		}
+		if(backoff == 0)
+			backoff = 1000 + rand->rand(500) - 250;
+		else if(backoff < MAXBACKOFF)
+			backoff = backoff * 3 / 2;
+		sys->sleep(backoff);
+	}
+}
+
+# first time we use the version offered by the client,
+# and record it; subsequent times we offer the response
+# recorded initially.
+version(): int
+{
+	if(doneversion)
+		sendtmsg(ref Tmsg.Version(NOTAG, msize, ver));
+	else{
+		m := <-tmsg;
+		if(m == nil)
+			quit();
+		if(m == nil || tagof(m) != tagof(Tmsg.Version)){
+			log("invalid initial version message: "+m.text());
+			quit();
+		}
+		sendtmsg(m);
+	}
+	if((gm := <-rmsg) == nil)
+		return -1;
+	pick m := gm {
+	Readerror =>
+		return -1;
+	Version =>
+		if(doneversion && (m.msize != msize || m.version != ver)){
+			log("wrong msize/version on reconnect");
+			# XXX is there any hope here - we could quit.
+			return -1;
+		}
+		if(!doneversion){
+			msize = m.msize;
+			ver = m.version;
+			doneversion = 1;
+			sendrmsg(m);
+		}
+		return 0;
+	* =>
+		log("invalid reply to Tversion: "+m.text());
+		return -1;
+	}
+}
+
+resurrecttags()
+{
+	# make sure that we send the tmsgs in the same order that
+	# they were sent originally.
+	all := array[ntags] of ref Tag;
+	n := 0;
+	for(i := 0; i < len tags; i++){
+		for(t := tags[i]; t != nil; t = t.next){
+			fid := tmsgfid(t.m);
+			if(fid != NOFID && (f := getfid(fid)) != nil){
+				if(f.state == DEAD){
+					sendrmsg(ref Rmsg.Error(t.m.tag, Estale));
+						t.dead = 1;
+					continue;
+				}
+				if(Erroronpartial){
+					partial := 0;
+					pick m := t.m {
+					Create =>
+						partial = 1;
+					Remove =>
+						partial = 1;
+					Wstat =>
+						partial = (m.stat.name != nil && f.path != nil && hd f.path != m.stat.name);
+					Write =>
+						partial = (f.qid.qtype & Sys->QTAPPEND);
+					}
+					if(partial)
+						sendrmsg(ref Rmsg.Error(t.m.tag, Epartial));
+				}
+			}
+			all[n++] = t;
+		}
+	}
+	all = all[0:n];
+	sort(all);
+	for(i = 0; i < len all; i++){
+		t := all[i];
+		pick m := t.m {
+		Flush =>
+			ot := gettag(m.oldtag, 0);
+			if(ot == nil || ot.dead){
+				sendrmsg(ref Rmsg.Flush(t.m.tag));
+				t.dead = 1;
+				continue;
+			}
+		}
+		sendtmsg(t.m);
+	}
+	tags = array[len tags] of ref Tag;
+	ntags = 0;
+	for(i = 0; i < len all; i++)
+		if(all[i].dead == 0)
+			newtag(all[i].m);
+}
+
+# re-open all the old fids, if possible.
+# use up to Nprocs processes to keep latency down.
+resurrectfids(): int
+{
+	procrmsg = array[Nprocs] of {* => chan[1] of ref Rmsg};
+	spawn rmsgmarshal(finish := chan of int);
+	getroot := chan of (int, string, string, chan of ref Root);
+	usedroot := chan of ref Root;
+	spawn fidproc(getroot, usedroot);
+	token = chan[Nprocs] of (int, chan of (ref Fid, ref Root));
+	for(i := 0; i < Nprocs; i++)
+		token <-= (i, nil);
+
+	for(i = 0; i < len fids.items; i++){
+		for(fl := fids.items[i]; fl != nil; fl = tl fl){
+			fid := (hd fl).t1;
+			(procid, workc) := <-token;
+			getroot <-= (1, fid.uname, fid.aname, reply := chan of ref Root);
+			root := <-reply;
+			if(workc == nil){
+				workc = chan of (ref Fid, ref Root);
+				spawn workproc(procid, workc, usedroot);
+			}
+			workc <-= (fid, root);
+		}
+	}
+
+	for(i = 0; i < Nprocs; i++){
+		(nil, workc) := <-token;
+		if(workc != nil)
+			workc <-= (nil, nil);
+	}
+	for(i = 0; i < Nprocs; i++){
+		getroot <-= (0, nil, nil, reply := chan of ref Root);
+		root := <-reply;
+		if(<-root.attached > 0)
+			clunk(0, root.fid);
+	}
+	usedroot <-= nil;
+	return <-finish;
+}
+
+workproc(procid: int, workc: chan of (ref Fid, ref Root), usedroot: chan of ref Root)
+{
+	while(((fid, root) := <-workc).t0 != nil){
+		# mark fid as dead only if it's a genuine server error, not if
+		# the server has just hung up.
+		if((err := resurrectfid(procid, fid, root)) != nil && sfd != nil){
+			log(err);
+			fid.state = DEAD;
+		}
+		usedroot <-= root;
+		token <-= (procid, workc);
+	}
+}
+
+resurrectfid(procid: int, fid: ref Fid, root: ref Root): string
+{
+	if(fid.state == AUTH)
+		return "auth fid discarded";
+	attached := <-root.attached;
+	if(attached == -1){
+		root.attached <-= -1;
+		return "root attach failed";
+	}
+	if(!attached || root.uname != fid.uname || root.aname != fid.aname){
+		if(attached)
+			clunk(procid, root.fid);
+		afid := NOFID;
+		if(fid.authed){
+			afid = fid.fid - 1;		# see unusedfid()
+			if((err := auth(procid, afid, root.uname, root.aname)) != nil){
+				log(err);
+				afid = -1;
+			}
+		}
+		(err, qid) := attach(procid, root.fid, afid, fid.uname, fid.aname);
+		if(afid != NOFID)
+			clunk(procid, afid);
+		if(err != nil){
+			root.attached <-= -1;
+			return "attach failed: "+err;
+		}
+		root.uname = fid.uname;
+		root.aname = fid.aname;
+		root.qid = qid;
+	}
+	root.attached <-= 1;
+	(err, qid) := walk(procid, root.fid, fid.fid, fid.path, root.qid);
+	if(err != nil)
+		return err;
+	if(fid.state == OPEN && (err = openfid(procid, fid)) != nil){
+		clunk(procid, fid.fid);
+		return err;
+	}
+	return nil;
+}
+
+openfid(procid: int, fid: ref Fid): string
+{
+	(err, qid) := open(procid, fid.fid, fid.omode);
+	if(err != nil)
+		return err;
+	if(qid.path != fid.qid.path || qid.qtype != fid.qid.qtype)
+		return "qid mismatch on reopen";
+	return nil;
+}
+			
+# store up to Nprocs separate root fids and dole them out to those that want them.
+fidproc(getroot: chan of (int, string, string, chan of ref Root), usedroot: chan of ref Root)
+{
+	roots := array[Nprocs] of ref Root;
+	n := 0;
+	maxfid := -1;
+	for(;;)alt{
+	(match, uname, aname, reply) := <-getroot =>
+		for(i := 0; i < n; i++)
+			if(match && roots[i].uname == uname && roots[i].aname == aname)
+				break;
+		if(i == n)
+			for(i = 0; i < n; i++)
+				if(roots[i].refcount == 0)
+					break;
+		if(i == n){
+			maxfid = unusedfid(maxfid);
+			roots[n] = ref Root(0, chan[1] of int, maxfid, Noqid, uname, aname);
+			roots[n++].attached <-= 0;
+		}
+		roots[i].refcount++;
+		reply <-= roots[i];
+	r := <-usedroot =>
+		if(r == nil)
+			exit;	
+		r.refcount--;
+	}
+}
+
+clunk(procid: int, fid: int)
+{
+	pick m := fcall(ref Tmsg.Clunk(procid, fid)) {
+	Error =>
+		if(sfd != nil)
+			log("error on clunk: " + m.ename);
+	}
+}
+
+attach(procid, fid, afid: int, uname, aname: string): (string, Sys->Qid)
+{
+	pick m := fcall(ref Tmsg.Attach(procid, fid, afid, uname, aname)) {
+	Attach =>
+		return (nil, m.qid);
+	Error =>
+		return (m.ename, Noqid);
+	}
+	return (nil, Noqid);	# not reached
+}
+
+read(procid, fid: int, buf: array of byte): (int, string)
+{
+	# XXX assume that offsets are ignored of auth fid reads/writes
+	pick m := fcall(ref Tmsg.Read(procid, fid, big 0, len buf)) {
+	Error =>
+		return (-1, m.ename);
+	Read =>
+		buf[0:] = m.data;
+		return (len m.data, nil);
+	}
+	return (-1, nil);			# not reached
+}
+
+write(procid, fid: int, buf: array of byte): (int, string)
+{
+	# XXX assume that offsets are ignored of auth fid reads/writes
+	pick m := fcall(ref Tmsg.Write(procid, fid, big 0, buf)) {
+	Error =>
+		sys->werrstr(m.ename);
+		return (-1, sys->sprint("%r"));
+	Write =>
+		return (m.count, nil);
+	}
+	return (-1, nil);		# not reached
+}
+
+auth(procid, fid: int, uname, aname: string): string
+{
+	if(factotum == nil)
+		return "no factotum available";
+
+	pick m := fcall(ref Tmsg.Auth(procid, fid, uname, aname)) {
+	Error =>
+		return m.ename;
+	}
+
+	readc := chan of (array of byte, chan of (int, string));
+	writec := chan of (array of byte, chan of (int, string));
+	done := chan of (ref Factotum->Authinfo, string);
+	spawn factotum->genproxy(readc, writec, done,
+			sys->open("/mnt/factotum/rpc", Sys->ORDWR),
+			"proto=p9any role=client "+keyspec);
+	for(;;)alt{
+	(buf, reply) := <-readc =>
+		reply <-= read(procid, fid, buf);
+	(buf, reply) := <-writec =>
+		reply <-= write(procid, fid, buf);
+	(authinfo, err) := <-done =>
+		if(authinfo == nil){
+			clunk(procid, fid);
+			return err;
+		}
+		# XXX check that authinfo.cuid == uname?
+		return nil;
+	}
+}
+
+# path is in reverse order; assume fid != newfid on entry.
+walk(procid: int, fid, newfid: int, path: list of string, qid: Sys->Qid): (string, Sys->Qid)
+{
+	names := array[len path] of string;
+	for(i := len names - 1; i >= 0; i--)
+		(names[i], path) = (hd path, tl path);
+	do{
+		w := names;
+		if(len w > Styx->MAXWELEM)
+			w = w[0:Styx->MAXWELEM];
+		names = names[len w:];
+		pick m := fcall(ref Tmsg.Walk(procid, fid, newfid, w)) {
+		Error =>
+			if(newfid == fid)
+				clunk(procid, newfid);
+			return ("walk error: "+m.ename, Noqid);
+		Walk =>
+			if(len m.qids != len w){
+				if(newfid == fid)
+					clunk(procid, newfid);
+				return ("walk: file not found", Noqid);
+			}
+			if(len m.qids > 0)
+				qid = m.qids[len m.qids - 1];
+			fid = newfid;
+		}
+	}while(len names > 0);
+	return (nil, qid);
+}
+
+open(procid: int, fid: int, mode: int): (string, Sys->Qid)
+{
+	pick m := fcall(ref Tmsg.Open(procid, fid, mode)) {
+	Error =>
+		return ("open: "+m.ename, Noqid);
+	Open =>
+		return (nil, m.qid);		# XXX what if iounit doesn't match the original?
+	}
+	return (nil, Noqid);		# not reached
+}
+
+fcall(m: ref Tmsg): ref Rmsg
+{
+	sendtmsg(m);
+	pick rm := <-procrmsg[m.tag] {
+	Readerror =>
+		procrmsg[m.tag] <-= rm;
+		return ref Rmsg.Error(rm.tag, rm.error);
+	Error =>
+		return rm;
+	* =>
+		if(rm.mtype() != m.mtype()+1)
+			return ref Rmsg.Error(m.tag, Etypemismatch);
+		return rm;
+	}
+}
+
+# find an unused fid (and make sure that the one before it is unused
+# too, in case we want to use it for an auth fid);
+unusedfid(maxfid: int): int
+{
+	for(f := maxfid + 1; ; f++)
+		if(fids.find(f) == nil && fids.find(f+1) == nil)
+			return f + 1;
+	abort("no unused fids - i don't believe it");
+	return 0;
+}
+
+# XXX what about message length limitations?
+sendtmsg(m: ref Tmsg)
+{
+	if(Debug) sys->print("%s\n", m.text());
+	d := m.pack();
+	if(sys->write(sfd, d, len d) != len d)
+		log(sys->sprint("tmsg write failed: %r"));	# XXX could signal to redial
+}
+
+sendrmsg(m: ref Rmsg)
+{
+	d := m.pack();
+	if(sys->write(cfd, d, len d) != len d){
+		log(sys->sprint("rmsg write failed: %r"));
+		quit();
+	}
+}
+
+rmsgmarshal(finish: chan of int)
+{
+	for(;;)alt{
+	finish <-= 1 =>
+		exit;
+	m := <-rmsg =>
+		if(m == nil || tagof(m) == tagof(Rmsg.Readerror)){
+			sfd = nil;
+			for(i := 0; i < Nprocs; i++)
+				procrmsg[i] <-= ref Rmsg.Readerror(NOTAG, "hung up");
+			finish <-= 0;
+			exit;
+		}
+		if(m.tag >= Nprocs){
+			log("invalid reply message");
+			break;
+		}
+		# XXX if the server replies with a tag that no-one's waiting for. we'll lock up.
+		# (but is it much of a concern, given no flushes, etc?)
+		procrmsg[m.tag] <-= m;
+	}
+}
+
+rmsgreader(fd: ref Sys->FD, sync: chan of int)
+{
+	sync <-= sys->pctl(0, nil);
+	m: ref Rmsg;
+	do {
+		m = Rmsg.read(fd, msize);
+		if(Debug) sys->print("%s\n", m.text());
+		rmsg <-= m;
+	} while(m != nil && tagof(m) != tagof(Tmsg.Readerror));
+}
+
+tmsgreader()
+{
+	m: ref Tmsg;
+	do{
+		m = Tmsg.read(cfd, msize);
+		tmsg <-= m;
+	} while(m != nil && tagof(m) != tagof(Tmsg.Readerror));
+}
+
+abort(s: string)
+{
+	log(s);
+	raise "abort";
+}
+
+tmsgfid(t: ref Tmsg): int
+{
+	fid := NOFID;
+	pick m := t {
+	Attach =>
+		fid = m.afid;
+	Walk =>
+		fid = m.fid;
+	Open =>
+		fid = m.fid;
+	Create =>
+		fid = m.fid;
+	Read =>
+		fid = m.fid;
+	Write =>
+		fid = m.fid;
+	Clunk or
+	Stat or
+	Remove =>
+		fid = m.fid;
+	Wstat =>
+		fid = m.fid;
+	}
+	return fid;
+}
+
+blankfid: Fid;
+newfid(fid: int): ref Fid
+{
+	f := ref blankfid;
+	f.fid = fid;
+	if(fids.add(fid, f) == 0){
+		abort("duplicate fid "+string fid);
+	}
+	return f;
+}
+
+getfid(fid: int): ref Fid
+{
+	return fids.find(fid);
+}
+
+newtag(m: ref Tmsg): ref Tag
+{
+	# XXX what happens if the client sends a duplicate tag?
+	t := ref Tag(m, seqno++, 0, nil);
+	slot := t.m.tag & (NTAGHASH - 1);
+	t.next = tags[slot];
+	tags[slot] = t;
+	ntags++;
+	return t;
+}
+
+gettag(tag: int, destroy: int): ref Tag
+{
+	slot := tag & (NTAGHASH - 1);
+	prev: ref Tag;
+	for(t := tags[slot]; t != nil; t = t.next){
+		if(t.m.tag == tag)
+			break;
+		prev = t;
+	}
+	if(t == nil || !destroy)
+		return t;
+	if(prev == nil)
+		tags[slot] = t.next;
+	else
+		prev.next = t.next;
+	ntags--;
+	return t;
+}
+
+Table[T].new(nslots: int, nilval: T): ref Table[T]
+{
+	if(nslots == 0)
+		nslots = 13;
+	return ref Table[T](array[nslots] of list of (int, T), nilval);
+}
+
+Table[T].add(t: self ref Table[T], id: int, x: T): int
+{
+	slot := id % len t.items;
+	for(q := t.items[slot]; q != nil; q = tl q)
+		if((hd q).t0 == id)
+			return 0;
+	t.items[slot] = (id, x) :: t.items[slot];
+	return 1;
+}
+
+Table[T].del(t: self ref Table[T], id: int): int
+{
+	slot := id % len t.items;
+	
+	p: list of (int, T);
+	r := 0;
+	for(q := t.items[slot]; q != nil; q = tl q){
+		if((hd q).t0 == id){
+			p = joinip(p, tl q);
+			r = 1;
+			break;
+		}
+		p = hd q :: p;
+	}
+	t.items[slot] = p;
+	return r;
+}
+
+Table[T].find(t: self ref Table[T], id: int): T
+{
+	for(p := t.items[id % len t.items]; p != nil; p = tl p)
+		if((hd p).t0 == id)
+			return (hd p).t1;
+	return t.nilval;
+}
+
+sort(a: array of ref Tag)
+{
+	mergesort(a, array[len a] of ref Tag);
+}
+
+mergesort(a, b: array of ref Tag)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m]);
+		mergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (b[i].seq > b[j].seq)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+kill(pid: int, note: string): int
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", note) < 0)
+		return -1;
+	return 0;
+}
+
+# join x to y, leaving result in arbitrary order.
+joinip[T](x, y: list of (int, T)): list of (int, T)
+{
+	if(len x > len y)
+		(x, y) = (y, x);
+	for(; x != nil; x = tl x)
+		y = hd x :: y;
+	return y;
+}
--- /dev/null
+++ b/appl/lib/styxservers.b
@@ -1,0 +1,610 @@
+implement Styxservers;
+
+#
+# Copyright © 1999 Vita Nuova Limited.  All rights reserved.
+# Revisions copyright © 2000-2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+#	Derived from Roger Peppe's Styxlib by Martin C. Atkins, 2001/2002 by
+#	adding new helper functions, and then removing Dirgenmod and its helpers
+#
+#	Further modified by Roger Peppe to simplify the interface by
+#	adding the Navigator/Navop channel interface and making other changes,
+#	including using the Styx module
+#
+# converted to revised Styx at Vita Nuova
+# further revised August/September 2002
+#
+# TO DO:
+#	- directory reading interface revision?
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+
+include "styxservers.m";
+
+CHANHASHSIZE: con 32;
+DIRREADSIZE: con Styx->STATFIXLEN+4*20;	# ``reasonable'' chunk for reading directories
+
+debug := 0;
+
+init(styxmod: Styx)
+{
+	sys = load Sys Sys->PATH;
+	styx = styxmod;
+}
+
+traceset(d: int)
+{
+	debug = d;
+}
+
+Styxserver.new(fd: ref Sys->FD, t: ref Navigator, rootpath: big): (chan of ref Tmsg, ref Styxserver)
+{
+	tchan := chan of ref Tmsg;
+	srv := ref Styxserver(fd, array[CHANHASHSIZE] of list of ref Fid, chan[1] of int, t, rootpath, 0, nil);
+
+	sync := chan of int;
+	spawn tmsgreader(fd, srv, tchan, sync);
+	<-sync;
+	return (tchan, srv);
+}
+
+tmsgreader(fd: ref Sys->FD, srv: ref Styxserver, tchan: chan of ref Tmsg, sync: chan of int)
+{
+	if(debug)
+		sys->pctl(Sys->NEWFD|Sys->NEWNS, fd.fd :: 2 :: nil);
+	else
+		sys->pctl(Sys->NEWFD|Sys->NEWNS, fd.fd :: nil);
+	sync <-= 1;
+	fd = sys->fildes(fd.fd);
+	m: ref Tmsg;
+	do {
+		m = Tmsg.read(fd, srv.msize);
+		if(debug && m != nil)
+			sys->fprint(sys->fildes(2), "<- %s\n", m.text());
+		tchan <-= m;
+	} while(m != nil && tagof(m) != tagof(Tmsg.Readerror));
+}
+
+Fid.clone(oc: self ref Fid, c: ref Fid): ref Fid
+{
+	# c.fid not touched, other values copied from c
+	c.path = oc.path;
+	c.qtype = oc.qtype;
+	c.isopen = oc.isopen;
+	c.mode = oc.mode;
+	c.doffset = oc.doffset;
+	c.uname  = oc.uname;
+	c.param = oc.param;
+	c.data = oc.data;
+	return c;
+}
+
+Fid.walk(c: self ref Fid, qid: Sys->Qid)
+{
+	c.path = qid.path;
+	c.qtype = qid.qtype;
+}
+
+Fid.open(c: self ref Fid, mode: int, qid: Sys->Qid)
+{
+	c.isopen = 1;
+	c.mode = mode;
+	c.doffset = (0, 0);
+	c.path = qid.path;
+	c.qtype = qid.qtype;
+}
+
+Styxserver.error(srv: self ref Styxserver, m: ref Tmsg, msg: string)
+{
+	srv.reply(ref Rmsg.Error(m.tag, msg));
+}
+
+Styxserver.reply(srv: self ref Styxserver, m: ref Rmsg): int
+{
+	if(debug)
+		sys->fprint(sys->fildes(2), "-> %s\n", m.text());
+	if(srv.replychan != nil){
+		srv.replychan <-= m;
+		return 0;
+	}
+	return srv.replydirect(m);
+}
+
+Styxserver.replydirect(srv: self ref Styxserver, m: ref Rmsg): int
+{
+	if(srv.msize == 0)
+		m = ref Rmsg.Error(m.tag, "Tversion not seen");
+	d := m.pack();
+	if(srv.msize != 0 && len d > srv.msize){
+		m = ref Rmsg.Error(m.tag, "Styx reply didn't fit");
+		d = m.pack();
+	}
+	return sys->write(srv.fd, d, len d);
+}
+
+Styxserver.attach(srv: self ref Styxserver, m: ref Tmsg.Attach): ref Fid
+{
+	(d, err) := srv.t.stat(srv.rootpath);
+	if(d == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, err));
+		return nil;
+	}
+	if((d.qid.qtype & Sys->QTDIR) == 0) {
+		srv.reply(ref Rmsg.Error(m.tag, Enotdir));
+		return nil;
+	}
+	c := srv.newfid(m.fid);
+	if(c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Einuse));
+		return nil;
+	}
+	c.uname = m.uname;
+	c.param = m.aname;
+	c.path = d.qid.path;
+	c.qtype = d.qid.qtype;
+	srv.reply(ref Rmsg.Attach(m.tag, d.qid));
+	return c;
+}
+
+walk1(n: ref Navigator, c: ref Fid, name: string): (ref Sys->Dir, string)
+{
+	(d, err) := n.stat(c.path);
+	if(d == nil)
+		return (nil, err);
+	if((d.qid.qtype & Sys->QTDIR) == 0)
+		return (nil, Enotdir);
+	if(!openok(c.uname, Styx->OEXEC, d.mode, d.uid, d.gid))
+		return (nil, Eperm);
+	(d, err) = n.walk(d.qid.path, name);
+	if(d == nil)
+		return (nil, err);
+	return (d, nil);
+}
+
+Styxserver.walk(srv: self ref Styxserver, m: ref Tmsg.Walk): ref Fid
+{
+	c := srv.getfid(m.fid);
+	if(c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return nil;
+	}
+	if(c.isopen) {
+		srv.reply(ref Rmsg.Error(m.tag, Eopen));
+		return nil;
+	}
+	if(m.newfid != m.fid){
+		nc := srv.newfid(m.newfid);
+		if(nc == nil){
+			srv.reply(ref Rmsg.Error(m.tag, Einuse));
+			return nil;
+		}
+		c = c.clone(nc);
+	}
+	qids := array[len m.names] of Sys->Qid;
+	oldpath := c.path;
+	oldqtype := c.qtype;
+	for(i := 0; i < len m.names; i++){
+		(d, err) := walk1(srv.t, c, m.names[i]);
+		if(d == nil){
+			c.path = oldpath;	# restore c
+			c.qtype = oldqtype;
+			if(m.newfid != m.fid)
+				srv.delfid(c);
+			if(i == 0)
+				srv.reply(ref Rmsg.Error(m.tag, err));
+			else
+				srv.reply(ref Rmsg.Walk(m.tag, qids[0:i]));
+			return nil;
+		}
+		c.walk(d.qid);
+		qids[i] = d.qid;
+	}
+	srv.reply(ref Rmsg.Walk(m.tag, qids));
+	return c;
+}
+
+Styxserver.canopen(srv: self ref Styxserver, m: ref Tmsg.Open): (ref Fid, int, ref Sys->Dir, string)
+{
+	c := srv.getfid(m.fid);
+	if(c == nil)
+		return (nil, 0, nil, Ebadfid);
+	if(c.isopen)
+		return (nil, 0, nil, Eopen);
+	(f, err) := srv.t.stat(c.path);
+	if(f == nil)
+		return (nil, 0, nil, err);
+	mode := openmode(m.mode);
+	if(mode == -1)
+		return (nil, 0, nil, Ebadarg);
+	if(mode != Sys->OREAD && f.qid.qtype & Sys->QTDIR)
+		return (nil, 0, nil, Eperm);
+	if(!openok(c.uname, m.mode, f.mode, f.uid, f.gid))
+		return (nil, 0, nil, Eperm);
+	if(m.mode & Sys->ORCLOSE) {
+		(dir, nil) := srv.t.walk(c.path, "..");
+		if(dir == nil || dir.qid.path == f.qid.path && dir.qid.qtype == f.qid.qtype ||	# can't remove root directory
+		   !openok(c.uname, Sys->OWRITE, dir.mode, dir.uid, dir.gid))
+			return (nil, 0, nil, Eperm);
+		mode |= Sys->ORCLOSE;
+	}
+	return (c, mode, f, err);
+}
+
+Styxserver.open(srv: self ref Styxserver, m: ref Tmsg.Open): ref Fid
+{
+	(c, mode, f, err) := srv.canopen(m);
+	if(c == nil){
+		srv.reply(ref Rmsg.Error(m.tag, err));
+		return nil;
+	}
+	c.open(mode, f.qid);
+	srv.reply(ref Rmsg.Open(m.tag, f.qid, srv.iounit()));
+	return c;
+}
+
+Styxserver.cancreate(srv: self ref Styxserver, m: ref Tmsg.Create): (ref Fid, int, ref Sys->Dir, string)
+{
+	c := srv.getfid(m.fid);
+	if(c == nil)
+		return (nil, 0, nil, Ebadfid);
+	if(c.isopen)
+		return (nil, 0, nil, Eopen);
+	(d, err) := srv.t.stat(c.path);
+	if(d == nil)
+		return (nil, 0, nil, err);
+	if((d.mode & Sys->DMDIR) == 0)
+		return (nil, 0, nil, Enotdir);
+	if(m.name == "")
+		return (nil, 0, nil, Ename);
+	if(m.name == "." || m.name == "..")
+		return (nil, 0, nil, Edot);
+	if(!openok(c.uname, Sys->OWRITE, d.mode, d.uid, d.gid))
+		return (nil, 0, nil, Eperm);
+	if(srv.t.walk(d.qid.path, m.name).t0 != nil)
+		return (nil, 0, nil, Eexists);
+	if((mode := openmode(m.mode)) == -1)
+		return (nil, 0, nil, Ebadarg);
+	mode |= m.mode & Sys->ORCLOSE;		# can create, so directory known to be writable
+	f := ref Sys->zerodir;
+	if(m.perm & Sys->DMDIR){
+		f.mode = m.perm & (~8r777 | (d.mode & 8r777));
+		f.qid.qtype = Sys->QTDIR;
+	}else{
+		f.mode = m.perm & (~8r666 | (d.mode & 8r666));
+		f.qid.qtype = Sys->QTFILE;
+	}
+	f.name = m.name;
+	f.uid = c.uname;
+	f.muid = c.uname;
+	f.gid = d.gid;
+	f.dtype = d.dtype;
+	f.dev = d.dev;
+	# caller must supply atime, mtime, qid.path
+	return (c, mode, f, nil);
+}
+
+Styxserver.canread(srv: self ref Styxserver, m: ref Tmsg.Read): (ref Fid, string)
+{
+	c := srv.getfid(m.fid);
+	if(c == nil)
+		return (nil, Ebadfid);
+	if(!c.isopen)
+		return (nil, Enotopen);
+	mode := c.mode & 3;
+	if(mode != Sys->OREAD && mode != Sys->ORDWR)	# readable modes
+		return (nil, Eaccess);
+	if(m.count < 0 || m.count > srv.msize-Styx->IOHDRSZ)
+		return (nil, Ecount);
+	if(m.offset < big 0)
+		return (nil, Eoffset);
+	return (c, nil);
+}
+
+Styxserver.read(srv: self ref Styxserver, m: ref Tmsg.Read): ref Fid
+{
+	(c, err) := srv.canread(m);
+	if(c == nil){
+		srv.reply(ref Rmsg.Error(m.tag, err));
+		return nil;
+	}
+	if((c.qtype & Sys->QTDIR) == 0) {
+		srv.reply(ref Rmsg.Error(m.tag, Eperm));
+		return nil;
+	}
+	if(m.count <= 0){
+		srv.reply(ref Rmsg.Read(m.tag, nil));
+		return c;
+	}
+	a := array[m.count] of byte;
+	(offset, index) := c.doffset;
+	if(int m.offset != offset){	# rescan from the beginning
+		offset = 0;
+		index = 0;
+	}
+	p := 0;
+Dread:
+	while((d := srv.t.readdir(c.path, index, (m.count+DIRREADSIZE-1)/DIRREADSIZE)) != nil && (nd := len d) > 0){
+		for(i := 0; i < nd; i++) {
+			size := styx->packdirsize(*d[i]);
+			offset += size;
+			index++;
+			if(offset < int m.offset)
+				continue;
+			if((m.count -= size) < 0){	# won't fit, save state for next time
+				offset -= size;
+				index--;
+				break Dread;
+			}
+			de := styx->packdir(*d[i]);
+			a[p:] = de;
+			p += size;
+		}
+	}
+	c.doffset = (offset, index);
+	srv.reply(ref Rmsg.Read(m.tag, a[0:p]));
+	return c;
+}
+
+Styxserver.canwrite(srv: self ref Styxserver, m: ref Tmsg.Write): (ref Fid, string)
+{
+	c := srv.getfid(m.fid);
+	if(c == nil)
+		return (nil, Ebadfid);
+	if(!c.isopen)
+		return (nil, Enotopen);
+	if(c.qtype & Sys->QTDIR)
+		return (nil, Eperm);
+	mode := c.mode & 3;
+	if(mode != Sys->OWRITE && mode != Sys->ORDWR)	# writable modes
+		return (nil, Eaccess);
+	if(m.offset < big 0)
+		return (nil, Eoffset);
+	# could check len m.data > iounit, but since we've got it now, it doesn't matter
+	return (c, nil);
+}
+
+Styxserver.stat(srv: self ref Styxserver, m: ref Tmsg.Stat)
+{
+	c := srv.getfid(m.fid);
+	if(c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return;
+	}
+	(d, err) := srv.t.stat(c.path);
+	if(d == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, err));
+		return;
+	}
+	srv.reply(ref Rmsg.Stat(m.tag, *d));
+}
+
+Styxserver.canremove(srv: self ref Styxserver, m: ref Tmsg.Remove): (ref Fid, big, string)
+{
+	c := srv.getfid(m.fid);
+	if(c == nil)
+		return (nil, big 0, Ebadfid);
+	(dir, nil) := srv.t.walk(c.path, "..");	# this relies on .. working for non-directories
+	if(dir == nil)
+		return (nil, big 0, "can't find parent directory");
+	if(dir.qid.path == c.path && dir.qid.qtype == c.qtype ||	# can't remove root directory
+	   !openok(c.uname, Sys->OWRITE, dir.mode, dir.uid, dir.gid))
+		return (nil, big 0, Eperm);
+	return (c, dir.qid.path, nil);
+}
+
+Styxserver.remove(srv: self ref Styxserver, m: ref Tmsg.Remove): ref Fid
+{
+	c := srv.getfid(m.fid);
+	if(c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return nil;
+	}
+	srv.delfid(c);			# Remove always clunks the fid
+	srv.reply(ref Rmsg.Error(m.tag, Eperm));
+	return c;	
+}
+
+Styxserver.clunk(srv: self ref Styxserver, m: ref Tmsg.Clunk): ref Fid
+{
+	c := srv.getfid(m.fid);
+	if(c == nil) {
+		srv.reply(ref Rmsg.Error(m.tag, Ebadfid));
+		return nil;
+	}
+	srv.delfid(c);
+	srv.reply(ref Rmsg.Clunk(m.tag));
+	return c;
+}
+
+Styxserver.default(srv: self ref Styxserver, gm: ref Tmsg)
+{
+	if(gm == nil) {
+		srv.t.c <-= nil;
+		exit;
+	}
+	pick m := gm {
+	Readerror =>
+		srv.t.c <-= nil;
+		exit;
+	Version =>
+		if(srv.msize <= 0)
+			srv.msize = Styx->MAXRPC;
+		(msize, version) := styx->compatible(m, srv.msize, Styx->VERSION);
+		if(msize < 256){
+			srv.reply(ref Rmsg.Error(m.tag, "message size too small"));
+			break;
+		}
+		srv.msize = msize;
+		srv.reply(ref Rmsg.Version(m.tag, msize, version));
+	Auth =>
+		srv.reply(ref Rmsg.Error(m.tag, "authentication not required"));
+	Flush =>
+		srv.reply(ref Rmsg.Flush(m.tag));
+	Walk =>
+		srv.walk(m);
+	Open =>
+		srv.open(m);
+	Create =>
+		srv.reply(ref Rmsg.Error(m.tag, Eperm));
+	Read =>
+		srv.read(m);
+	Write =>
+		srv.reply(ref Rmsg.Error(m.tag, Eperm));
+	Clunk =>
+		srv.clunk(m);
+		# to delete on ORCLOSE:
+		# c := srv.clunk(m);
+		# if(c != nil && c.mode & Sys->ORCLOSE)
+		# 	srv.doremove(c);
+	Stat =>
+		srv.stat(m);
+	Remove =>
+		srv.remove(m);
+	Wstat =>
+		srv.reply(ref Rmsg.Error(m.tag, Eperm));
+	Attach =>
+		srv.attach(m);
+	* =>
+		sys->fprint(sys->fildes(2), "styxservers: unhandled Tmsg tag %d - should not happen\n", tagof gm);
+		raise "fail: unhandled case";
+	}
+}
+
+Styxserver.iounit(srv: self ref Styxserver): int
+{
+	n := srv.msize - Styx->IOHDRSZ;
+	if(n <= 0)
+		return 0;	# unknown
+	return n;
+}
+
+Styxserver.getfid(srv: self ref Styxserver, fid: int): ref Fid
+{
+	# the list is safe to use without locking
+	for(l := srv.fids[fid & (CHANHASHSIZE-1)]; l != nil; l = tl l)
+		if((hd l).fid == fid)
+			return hd l;
+	return nil;
+}
+
+Styxserver.delfid(srv: self ref Styxserver, c: ref Fid)
+{
+	slot := c.fid & (CHANHASHSIZE-1);
+	nl: list of ref Fid;
+	srv.fidlock <-= 1;
+	for(l := srv.fids[slot]; l != nil; l = tl l)
+		if((hd l).fid != c.fid)
+			nl = (hd l) :: nl;
+	srv.fids[slot] = nl;
+	<-srv.fidlock;
+}
+
+Styxserver.allfids(srv: self ref Styxserver): list of ref Fid
+{
+	cl: list of ref Fid;
+	srv.fidlock <-= 1;
+	for(i := 0; i < len srv.fids; i++)
+		for(l := srv.fids[i]; l != nil; l = tl l)
+			cl = hd l :: cl;
+	<-srv.fidlock;
+	return cl;
+}
+
+Styxserver.newfid(srv: self ref Styxserver, fid: int): ref Fid
+{
+	srv.fidlock <-= 1;
+	if((c := srv.getfid(fid)) != nil){
+		<-srv.fidlock;
+		return nil;		# illegal: fid in use
+	}
+	c = ref Fid;
+	c.path = big -1;
+	c.qtype = 0;
+	c.isopen = 0;
+	c.mode = 0;
+	c.fid = fid;
+	c.doffset = (0, 0);
+	slot := fid & (CHANHASHSIZE-1);
+	srv.fids[slot] = c :: srv.fids[slot];
+	<-srv.fidlock;
+	return c;
+}
+
+readstr(m: ref Tmsg.Read, d: string): ref Rmsg.Read
+{
+	return readbytes(m, array of byte d);
+}
+
+readbytes(m: ref Tmsg.Read, d: array of byte): ref Rmsg.Read
+{
+	r := ref Rmsg.Read(m.tag, nil);
+	if(m.offset >= big len d || m.offset < big 0)
+		return r;
+	offset := int m.offset;
+	e := offset + m.count;
+	if(e > len d)
+		e = len d;
+	r.data = d[offset:e];
+	return r;
+}
+
+Navigator.new(c: chan of ref Navop): ref Navigator
+{
+	return ref Navigator(c, chan of (ref Sys->Dir, string));
+}
+
+Navigator.stat(t: self ref Navigator, q: big): (ref Sys->Dir, string)
+{
+	t.c <-= ref Navop.Stat(t.reply, q);
+	return <-t.reply;
+}
+
+Navigator.walk(t: self ref Navigator, q: big, name: string): (ref Sys->Dir, string)
+{
+	t.c <-= ref Navop.Walk(t.reply, q, name);
+	return <-t.reply;
+}
+
+Navigator.readdir(t: self ref Navigator, q: big, offset, count: int): array of ref Sys->Dir
+{
+	a := array[count] of ref Sys->Dir;
+	t.c <-= ref Navop.Readdir(t.reply, q, offset, count);
+	i := 0;
+	while((d := (<-t.reply).t0) != nil)
+		if(i < count)
+			a[i++] = d;
+	if(i == 0)
+		return nil;
+	return a[0:i];
+}
+
+openmode(o: int): int
+{
+	OTRUNC, ORCLOSE, OREAD, ORDWR: import Sys;
+	o &= ~(OTRUNC|ORCLOSE);
+	if(o > ORDWR)
+		return -1;
+	return o;
+}
+
+access := array[] of {8r400, 8r200, 8r600, 8r100};
+openok(uname: string, omode: int, perm: int, fuid: string, fgid: string): int
+{
+	t := access[omode & 3];
+	if(omode & Sys->OTRUNC){
+		if(perm & Sys->DMDIR)
+			return 0;
+		t |= 8r200;
+	}
+	if(uname == fuid && (t&perm) == t)
+		return 1;
+	if(uname == fgid && (t&(perm<<3)) == t)
+		return 1;
+	return (t&(perm<<6)) == t;
+}	
--- /dev/null
+++ b/appl/lib/tables.b
@@ -1,0 +1,105 @@
+implement Tables;
+include "tables.m";
+
+Table[T].new(nslots: int, nilval: T): ref Table[T]
+{
+	if(nslots == 0)
+		nslots = 13;
+	return ref Table[T](array[nslots] of list of (int, T), nilval);
+}
+
+Table[T].add(t: self ref Table[T], id: int, x: T): int
+{
+	slot := id % len t.items;
+	for(q := t.items[slot]; q != nil; q = tl q)
+		if((hd q).t0 == id)
+			return 0;
+	t.items[slot] = (id, x) :: t.items[slot];
+	return 1;
+}
+
+Table[T].del(t: self ref Table[T], id: int): int
+{
+	slot := id % len t.items;
+	
+	p: list of (int, T);
+	r := 0;
+	for(q := t.items[slot]; q != nil; q = tl q){
+		if((hd q).t0 == id){
+			p = joinip(p, tl q);
+			r = 1;
+			break;
+		}
+		p = hd q :: p;
+	}
+	t.items[slot] = p;
+	return r;
+}
+
+Table[T].find(t: self ref Table[T], id: int): T
+{
+	for(p := t.items[id % len t.items]; p != nil; p = tl p)
+		if((hd p).t0 == id)
+			return (hd p).t1;
+	return t.nilval;
+}
+
+Strhash[T].new(nslots: int, nilval: T): ref Strhash[T]
+{
+	if(nslots == 0)
+		nslots = 13;
+	return ref Strhash[T](array[nslots] of list of (string, T), nilval);
+}
+
+Strhash[T].add(t: self ref Strhash, id: string, x: T)
+{
+	slot := hash(id, len t.items);
+	t.items[slot] = (id, x) :: t.items[slot];
+}
+
+Strhash[T].del(t: self ref Strhash, id: string)
+{
+	slot := hash(id, len t.items);
+
+	p: list of (string, T);
+	for(q := t.items[slot]; q != nil; q = tl q)
+		if((hd q).t0 != id)
+			p = hd q :: p;
+	t.items[slot] = p;
+}
+
+Strhash[T].find(t: self ref Strhash, id: string): T
+{
+	for(p := t.items[hash(id, len t.items)]; p != nil; p = tl p)
+		if((hd p).t0 == id)
+			return (hd p).t1;
+	return t.nilval;
+}
+
+hash(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i:=0; i<m; i++){
+		h = 65599*h+s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
+
+rev[T](x: list of T): list of T
+{
+	l: list of T;
+	for(; x != nil; x = tl x)
+		l = hd x :: l;
+	return l;
+}
+
+# join x to y, leaving result in arbitrary order.
+joinip[T](x, y: list of (int, T)): list of (int, T)
+{
+	if(len x > len y)
+		(x, y) = (y, x);
+	for(; x != nil; x = tl x)
+		y = hd x :: y;
+	return y;
+}
--- /dev/null
+++ b/appl/lib/tabs.b
@@ -1,0 +1,190 @@
+implement Tabs;
+
+# pseudo-widget for folder tab selections
+
+#
+# Copyright © 1996-1999 Lucent Technologies Inc.  All rights reserved.
+# Revisions Copyright © 2000-2002 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "tk.m";
+	tk: Tk;
+
+include "string.m";
+	str: String;		# could load on demand
+
+include "tabs.m";
+
+TABSXdelta : con 2;
+TABSXslant : con 5;
+TABSXoff : con 5;
+TABSYheight : con 35;
+TABSYtop : con 10;
+TABSBord : con 1;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	tk = load Tk Tk->PATH;
+	str = load String String->PATH;
+}
+
+mktabs(t: ref Tk->Toplevel, dot: string, tabs: array of (string, string), dflt: int): chan of string
+{
+	lab, widg: string;
+	cmd(t, "canvas "+dot+" -height "+string TABSYheight);
+	cmd(t, "pack propagate "+dot+" 0");
+	c := chan of string;
+	tk->namechan(t, c, dot[1:]);
+	xpos := 2*TABSXdelta;
+	ypos := TABSYheight - 3;
+	back := cmd(t, dot+" cget -background");
+	dark := "#999999";
+	light := "#ffffff";
+	w := 20;
+	h := 30;
+	last := "";
+	for(i := 0; i < len tabs; i++){
+		(lab, widg) = tabs[i];
+		tag := "tag" + string i;
+		sel := "sel" + string i;
+		xs := xpos;
+		xpos += TABSXslant + TABSXoff;
+		v := cmd(t, dot+" create text "+string xpos+" "+string ypos+" -text "+tk->quote(lab)+" -anchor sw -tags "+tag);
+		bbox := tk->cmd(t, dot+" bbox "+tag);
+		if(bbox[0] == '!')
+			break;
+		(r, nil) := parserect(bbox);
+		r.max.x += TABSXoff;
+		x1 := " "+string xs;
+		x2 := " "+string(xs + TABSXslant);
+		x3 := " "+string r.max.x;
+		x4 := " "+string(r.max.x + TABSXslant);
+		y1 := " "+string(TABSYheight - 2);
+		y2 := " "+string TABSYtop;
+		cmd(t, dot+" create polygon " + x1+y1 + x2+y2 + x3+y2 + x4+y1 +
+			" -fill "+back+" -tags "+tag);
+		cmd(t, dot+" create line " + x3+y2 + x4+y1 +
+			" -fill "+dark+" -width 1 -tags "+tag);
+		cmd(t, dot+" create line " + x1+y1 + x2+y2 + x3+y2 +
+			" -fill "+light+" -width 1 -tags "+tag);
+
+		x1 = " "+string(xs+2);
+		x4 = " "+string(r.max.x + TABSXslant - 2);
+		y1 = " "+string(TABSYheight);
+		cmd(t, dot+" create line " + x1+y1 + x4+y1 +
+			" -fill "+back+" -width 2 -tags "+sel);
+
+		cmd(t, dot+" raise "+v);
+		cmd(t, dot+" bind "+tag+" <ButtonRelease-1> 'send "+
+			dot[1:]+" "+string i);
+
+		cmd(t, dot+" lower "+tag+" "+last);
+		last = tag;
+
+		xpos = r.max.x;
+		ww := int cmd(t, widg+" cget -width");
+		wh := int cmd(t, widg+" cget -height");
+		if(wh > h)
+			h = wh;
+		if(ww > w)
+			w = ww;
+	}
+	xpos += 4*TABSXslant;
+	if(w < xpos)
+		w = xpos;
+
+	for(i = 0; i < len tabs; i++){
+		(nil, widg) = tabs[i];
+		cmd(t, "pack propagate "+widg+" 0");
+		cmd(t, widg+" configure -width "+string w+" -height "+string h);
+	}
+
+	w += 2*TABSBord;
+	h += 2*TABSBord + TABSYheight;
+
+	cmd(t, dot+" create line 0 "+string TABSYheight+
+		" "+string w+" "+string TABSYheight+" -width 2 -fill "+light);
+	cmd(t, dot+" create line 1 "+string TABSYheight+
+		" 1 "+string(h-1)+" -width 2 -fill "+light);
+	cmd(t, dot+" create line  0 "+string(h-1)+
+		" "+string w+" "+string(h-1)+" -width 2 -fill "+dark);
+	cmd(t, dot+" create line "+string(w-1)+" "+string TABSYheight+
+		" "+string(w-1)+" "+string(h-1)+" -width 2 -fill "+dark);
+
+	cmd(t, dot+" configure -width "+string w+" -height "+string h);
+	cmd(t, dot+" configure -scrollregion {0 0 "+string w+" "+string h+"}");
+	tabsctl(t, dot, tabs, -1, string dflt);
+	return c;
+}
+
+tabsctl(t: ref Tk->Toplevel,
+	dot: string,
+	tabs: array of (string, string),
+	id: int,
+	s: string): int
+{
+	lab, widg: string;
+
+	nid := int s;
+	if(id == nid)
+		return id;
+	if(id >= 0){
+		(lab, widg) = tabs[id];
+		tag := "tag" + string id;
+		cmd(t, dot+" lower sel" + string id);
+#		pos := cmd(t, dot+" coords " + tag);
+#		if(len pos >= 1 && pos[0] != '!'){
+#			(p, nil) := parsept(pos);
+#			cmd(t, dot+" coords "+tag+" "+string(p.x+1)+
+#				" "+string(p.y+1));
+#		}
+		if(id > 0)
+			cmd(t, dot+" lower "+ tag + " tag"+string (id - 1));
+		cmd(t, dot+" delete win" + string id);
+	}
+	id = nid;
+	(lab, widg) = tabs[id];
+#	pos := tk->cmd(t, dot+" coords tag" + string id);
+#	if(len pos >= 1 && pos[0] != '!'){
+#		(p, nil) := parsept(pos);
+#		cmd(t, dot+" coords tag"+string id+" "+string(p.x-1)+" "+string(p.y-1));
+#	}
+	cmd(t, dot+" raise tag"+string id);
+	cmd(t, dot+" raise sel"+string id);
+	cmd(t, dot+" create window "+string TABSBord+" "+
+		string(TABSYheight+TABSBord)+" -window "+widg+" -anchor nw -tags win"+string id);
+	cmd(t, "update");
+	return id;
+}
+
+parsept(s: string): (Draw->Point, string)
+{
+	p: Draw->Point;
+
+	(p.x, s) = str->toint(s, 10);
+	(p.y, s) = str->toint(s, 10);
+	return (p, s);
+}
+
+parserect(s: string): (Draw->Rect, string)
+{
+	r: Draw->Rect;
+
+	(r.min, s) = parsept(s);
+	(r.max, s) = parsept(s);
+	return (r, s);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "%s: tk error %s on [%s]\n", PATH, e, s);
+	return e;
+}
--- /dev/null
+++ b/appl/lib/tcl.m
@@ -1,0 +1,19 @@
+Tcl_Core: module {
+
+	PATH : con "/dis/lib/tcl_core.dis";
+	TclData : adt {
+		context : ref Draw->Context;
+		top : ref Tk->Toplevel;
+		lines : chan of string;
+		debug : int;
+	};
+
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+	grab_lines : fn(new_inp,unfin : string, lines: chan of string);
+	prepass :  fn(line : string) : string;
+	evalcmd : fn(line : string,termchar : int) : string;
+	clear_error : fn();
+	set_top : fn(win:ref Tk->Toplevel);
+	finished : fn(s : string,termchar : int) : int;
+	notify : fn(num : int, s: string) : string;
+};
--- /dev/null
+++ b/appl/lib/tcl_calc.b
@@ -1,0 +1,909 @@
+implement TclLib;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "tk.m";
+
+include "string.m";
+	str : String;
+
+include "tcl.m";
+
+include "tcllib.m";
+
+include "math.m";
+	math : Math;
+
+include "regex.m";
+	regex : Regex;
+
+include "utils.m";
+	htab: Int_Hashtab;
+
+IHash: import htab;
+
+leaf : adt {
+	which : int;
+	s_val : string;
+	i_val : int;
+	r_val : real;
+};
+
+where : int;
+text:string;
+EOS,MALFORMED,UNKNOWN,REAL,INT,STRING,FUNC,ADD,SUB,MUL,MOD,DIV,LAND,
+LOR,BAND,BOR,BEOR,EXCL,TILDE,QUEST,COLON,F_ABS,F_ACOS,F_ASIN,F_ATAN,
+F_ATAN2,F_CEIL,F_COS,F_COSH,F_EXP,F_FLOOR,F_FMOD,F_HYPOT,F_LOG,F_LOG10,
+F_POW,F_SIN,F_SINH,F_SQRT,F_TAN,F_TANH,L_BRACE,R_BRACE,COMMA,LSHIF,RSHIF,
+LT,GT,LEQ,GEQ,EQ,NEQ : con iota; 
+i_val : int;
+r_val : real;
+s_val : string;
+numbers : con "-?(([0-9]+)|([0-9]*\\.[0-9]+)([eE][-+]?[0-9]+)?)";
+re : Regex->Re;
+f_table : ref IHash;
+started : int;
+
+# does an eval on a string. The string is assumed to be 
+# mathematically correct. No Tcl parsing is done.
+
+commands := array[] of {"calc"};
+
+about() : array of string {
+	return commands;
+}
+
+init() : string {
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	math = load Math Math->PATH;
+	regex = load Regex Regex->PATH;
+	htab = load Int_Hashtab Int_Hashtab->PATH;
+	started=1;
+	if (regex==nil || math==nil || str==nil || htab==nil)
+		return "Cannot initialise calc module.";
+	f_table=htab->alloc(101);
+	f_table.insert("abs",F_ABS);
+	f_table.insert("acos",F_ACOS);
+	f_table.insert("asin",F_ASIN);
+	f_table.insert("atan",F_ATAN);
+	f_table.insert("atan2",F_ATAN2);
+	f_table.insert("ceil",F_CEIL);
+	f_table.insert("cos",F_COS);
+	f_table.insert("cosh",F_COSH);
+	f_table.insert("exp",F_EXP);
+	f_table.insert("floor",F_FLOOR);
+	f_table.insert("fmod",F_FMOD);		
+	f_table.insert("hypot",F_HYPOT);
+	f_table.insert("log",F_LOG);
+	f_table.insert("log10",F_LOG10);
+	f_table.insert("pow",F_POW);
+	f_table.insert("sin",F_SIN);
+	f_table.insert("sinh",F_SINH);
+	f_table.insert("sqrt",F_SQRT);
+	f_table.insert("tan",F_TAN);
+	f_table.insert("tanh",F_TANH);
+	(re,nil)=regex->compile(numbers, 0);
+	return nil;
+}
+
+uarray:= array[] of { EXCL, 0, 0, 0, MOD, BAND, 0, L_BRACE, R_BRACE, MUL,
+	ADD, COMMA, SUB, 0, DIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, COLON,
+	0, LT, EQ, GT, QUEST};
+
+getTok(eat : int) : int {
+	val, s : string;
+	dec:=0;
+	s=text;
+	i:=0;
+	if (s==nil) 
+		return EOS;
+	while(i<len s && (s[i]==' '||s[i]=='\t')) i++;
+	if (i==len s)
+		return EOS;
+	case s[i]{
+		'+' or '-' or '*' or '?' or '%' or '/' or '(' 
+		or ')' or ',' or ':' =>  
+			if (eat)
+				text=s[i+1:];
+			return uarray[s[i]-'!'];
+		'~'  =>
+			if (eat)
+				text=s[i+1:];
+			return TILDE;	
+		'^'  =>
+			if (eat)
+				text=s[i+1:];
+			return BEOR;
+		'&' =>
+			if (s[i+1]=='&'){
+				if (eat)
+					text=s[i+2:];
+				return LAND;
+			}
+			if (eat)
+				text=s[i+1:];
+			return BAND;
+			
+		'|' =>			
+			if (s[i+1]=='|'){
+				if (eat)
+					text=s[i+2:];
+				return LOR;
+			}
+			if (eat)
+				text=s[i+1:];
+			return BOR;
+
+		'!' =>	
+			if (s[i+1]=='='){
+				if (eat)
+					text=s[i+2:];
+				return NEQ;
+			}
+			if (eat)
+				text=s[i+1:];
+			return EXCL;
+		'=' =>
+			if (s[i+1]!='=')
+				return UNKNOWN;
+			if (eat)
+				text=s[i+2:];
+			return EQ;
+		'>' =>
+			case s[i+1]{
+				'>' =>
+					if (eat)
+						text=s[i+2:];
+					return RSHIF;
+				'=' => 
+					if (eat)
+						text=s[i+2:];
+					return GEQ;
+				* =>
+					if (eat)
+						text=s[i+1:];
+					return GT;
+			}
+		'<' =>
+			case s[i+1]{
+				'<' =>
+					if (eat)
+						text=s[i+2:];
+					return LSHIF;
+				'=' => 
+					if (eat)
+						text=s[i+2:];
+					return LEQ;
+				* =>
+					if (eat)
+						text=s[i+1:];
+					return LT;
+			}
+		'0' =>
+			return oct_hex(eat);
+		'1' to '9' 
+		or '.'=>
+			
+			match:=regex->execute(re,s[i:]);
+			if (match != nil)
+				(i1, i2) := match[0];
+			if (match==nil || i1!=0)
+				sys->print("ARRG! non-number where number should be!");
+			if (eat)
+				text=s[i+i2:];
+			val=s[i:i+i2];
+			if (str->in('.',val) || str->in('e',val)
+				|| str->in('E',val)) {
+				r_val=real val;
+				return REAL;
+			}
+			i_val=int val;
+			return INT;
+		* =>
+			return get_func(eat);	
+		}
+	return UNKNOWN;
+}
+
+oct_hex(eat : int) : int {
+	s:=text;
+	rest : string;
+	if (len s == 1){
+		i_val=0;
+		if (eat)
+			text=nil;
+		return INT;
+	}
+	if(s[1]=='x' || s[1]=='X'){
+		(i_val,rest)=str->toint(s[2:],16);
+		if (eat)
+			text = rest;
+		return INT;
+	}
+	if (s[1]=='.'){
+		match:=regex->execute(re,s);
+		if (match != nil)
+			(i1, i2) := match[0];
+		if (match==nil || i1!=0)
+			sys->print("ARRG!");
+		if (eat)
+			text=s[i2:];
+		val:=s[0:i2];
+		r_val=real val;
+		return REAL;
+	}
+	(i_val,rest)=str->toint(s[1:],8);
+	if (eat)
+		text = rest;
+	return INT;
+}
+
+get_func(eat : int) : int{
+	s:=text;
+	i:=0;
+	tok:=STRING;
+	while(i<len s && ((s[i]>='a' && s[i]<='z') || 
+			 (s[i]>='A' && s[i]<='Z') || 
+			 (s[i]>='0' && s[i]<='9') || (s[i]=='_'))) i++;
+	(found,val):=f_table.find(s[0:i]);
+	if (found)
+		tok=val;
+	else
+		s_val = s[0:i];
+	if (eat)
+		text = s[i:];
+	return tok;
+}
+
+
+exec(tcl: ref Tcl_Core->TclData,argv : array of string) : (int,string){
+	if (tcl==nil);
+	if (!started)
+		if ((msg:=init())!=nil)
+			return (1,msg);
+	retval : leaf;
+	expr:="";
+	for (i:=0;i<len argv;i++){
+		expr+=argv[i];
+		expr[len expr]=' ';
+	}
+	if (expr=="") 
+		return (1,"Error!");
+	text=expr[0:len expr-1];
+	#sys->print("Text is %s\n",text);
+	retval = expr_9();
+	if (retval.which == UNKNOWN)
+		return (1,"Error!");
+	if (retval.which == INT)
+		return (0,string retval.i_val);
+	if (retval.which == STRING)
+		return (0,retval.s_val);
+	return (0,string retval.r_val);
+}
+
+expr_9() : leaf {
+	retval : leaf;
+	r1:=expr_8();
+	tok := getTok(0);
+	if(tok==QUEST){ 
+		getTok(1);
+		r2:=expr_8();
+		if (getTok(1)!=COLON)
+			r1.which=UNKNOWN;
+		r3:=expr_8();
+		if (r1.which == INT && r1.i_val==0)
+			return r3;
+		if (r1.which == INT && r1.i_val!=0)
+			return r2;
+		if (r1.which == REAL && r1.r_val==0.0)
+			return r3;
+		if (r1.which == REAL && r1.r_val!=0.0)
+			return r2;
+		retval.which=UNKNOWN;
+		return retval;
+	}
+	return r1;
+}
+
+
+expr_8() : leaf {
+	retval : leaf;
+	r1:=expr_7();
+	retval=r1;
+	tok := getTok(0);
+	if (tok == LOR){
+		getTok(1);
+		r2:=expr_7(); # start again?
+		if (r1.which!=INT || r2.which!=INT){
+			retval.which = UNKNOWN;
+			return retval;
+		}
+		retval.i_val=r1.i_val || r2.i_val;	
+		return retval;
+	}
+	return retval;
+}
+
+expr_7() : leaf {
+	retval : leaf;
+	r1:=expr_6();
+	retval=r1;
+	tok := getTok(0);
+	if (tok == LAND){
+		getTok(1);
+		r2:=expr_6();
+		if (r1.which!=INT || r2.which!=INT){
+			retval.which = UNKNOWN;
+			return retval;
+		}
+		retval.i_val=r1.i_val && r2.i_val;	
+		return retval;
+	}
+	return retval;
+}
+
+expr_6() : leaf {
+	retval : leaf;
+	r1:=expr_5();
+	retval=r1;
+	tok := getTok(0);
+	if (tok == BOR){
+		getTok(1);
+		r2:=expr_5();
+		if (r1.which!=INT || r2.which!=INT){
+			retval.which = UNKNOWN;
+			return retval;
+		}
+		retval.i_val=r1.i_val | r2.i_val;	
+		return retval;
+	}
+	return retval;
+}
+
+expr_5() : leaf {
+	retval : leaf;
+	r1:=expr_4();
+	retval=r1;
+	tok := getTok(0);
+	if (tok == BEOR){
+		getTok(1);
+		r2:=expr_4();
+		if (r1.which!=INT || r2.which!=INT){
+			retval.which = UNKNOWN;
+			return retval;
+		}
+		retval.i_val=r1.i_val ^ r2.i_val;	
+		return retval;
+	}
+	return retval;
+}
+
+expr_4() : leaf {
+	retval : leaf;
+	r1:=expr_3();
+	retval=r1;
+	tok := getTok(0);
+	if (tok == BAND){
+		getTok(1);
+		r2:=expr_3();
+		if (r1.which!=INT || r2.which!=INT){
+			retval.which = UNKNOWN;
+			return retval;
+		}
+		retval.i_val=r1.i_val & r2.i_val;	
+		return retval;
+	}
+	return retval;
+}
+	
+expr_3() : leaf {
+	retval : leaf;
+	r1:=expr_2();
+	retval=r1;
+	tok:=getTok(0);
+	if (tok==EQ || tok==NEQ){
+		retval.which=INT;
+		getTok(1);
+		r2:=expr_2();
+		if (r1.which==UNKNOWN || r2.which==UNKNOWN){
+			r1.which=UNKNOWN;
+			return r1;
+		}
+		if (tok==EQ){
+			case r1.which {
+				STRING =>
+					if (r2.which == INT)
+					   retval.i_val = 
+					    (r1.s_val == string r2.i_val);
+					else if (r2.which == REAL)
+					   retval.i_val = 
+				 	    (r1.s_val == string r2.r_val);
+					else retval.i_val = 
+						   (r1.s_val == r2.s_val);
+				INT =>
+					if (r2.which == INT)
+					   retval.i_val = 
+						   (r1.i_val == r2.i_val);
+					else if (r2.which == REAL)
+					   retval.i_val = 
+					      (real r1.i_val == r2.r_val);
+					else retval.i_val = 
+					    (string r1.i_val == r2.s_val);
+				REAL =>
+					if (r2.which == INT)
+					   retval.i_val = 
+					      (r1.r_val == real r2.i_val);
+					else if (r2.which == REAL)
+					   retval.i_val = 
+						   (r1.r_val == r2.r_val);
+					else retval.i_val = 
+					    (string r1.r_val == r2.s_val);
+			}
+		}
+		else {
+			case r1.which {
+				STRING =>
+					if (r2.which == INT)
+					   retval.i_val = 
+					    (r1.s_val != string r2.i_val);
+					else if (r2.which == REAL)
+					   retval.i_val = 
+				 	    (r1.s_val != string r2.r_val);
+					else retval.i_val = 
+						   (r1.s_val != r2.s_val);
+				INT =>
+					if (r2.which == INT)
+					   retval.i_val = 
+						   (r1.i_val != r2.i_val);
+					else if (r2.which == REAL)
+					   retval.i_val = 
+					      (real r1.i_val != r2.r_val);
+					else retval.i_val = 
+					    (string r1.i_val != r2.s_val);
+				REAL =>
+					if (r2.which == INT)
+					   retval.i_val = 
+					      (r1.r_val != real r2.i_val);
+					else if (r2.which == REAL)
+					   retval.i_val = 
+						   (r1.r_val != r2.r_val);
+					else retval.i_val = 
+					    (string r1.r_val != r2.s_val);
+			}
+		}			
+		return retval;
+	}
+	return retval;
+}
+
+
+expr_2() : leaf {
+	retval : leaf;
+	ar1,ar2 : real;
+	s1,s2 : string;
+	r1:=expr_1();
+	retval=r1;
+	tok:=getTok(0);
+	if (tok==LT || tok==GT || tok ==LEQ || tok==GEQ){
+		retval.which=INT;
+		getTok(1);
+		r2:=expr_1();
+		if (r1.which == STRING || r2.which == STRING){
+			if (r1.which==STRING)
+				s1=r1.s_val;
+			else if (r1.which==INT)
+				s1=string r1.i_val;
+			else s1= string r1.r_val;
+			if (r2.which==STRING)
+				s2=r2.s_val;
+			else if (r2.which==INT)
+				s2=string r2.i_val;
+			else s2= string r2.r_val;
+			case tok{
+				LT =>
+					retval.i_val = (s1<s2);
+				GT =>
+					retval.i_val = (s1>s2);
+				LEQ =>
+					retval.i_val = (s1<=s2);
+				GEQ =>
+					retval.i_val = (s1>=s2);
+			}
+			return retval;
+		}
+		if (r1.which==UNKNOWN || r2.which==UNKNOWN){
+			r1.which=UNKNOWN;
+			return r1;
+		}
+		if (r1.which == INT)
+			ar1 = real r1.i_val;
+		else
+			ar1 = r1.r_val;
+		if (r2.which == INT)
+			ar2 = real r2.i_val;
+		else
+			ar2 = r2.r_val;
+		case tok{
+			LT =>
+				retval.i_val = (ar1<ar2);
+			GT =>
+				retval.i_val = (ar1>ar2);
+			LEQ =>
+				retval.i_val = (ar1<=ar2);
+			GEQ =>
+				retval.i_val = (ar1>=ar2);
+		}
+		return retval;
+	}
+	return retval;
+}
+expr_1() : leaf {
+	retval : leaf;
+	r1:=expr0();
+	retval=r1;
+	tok := getTok(0);
+	if (tok == LSHIF || tok==RSHIF){
+		getTok(1);
+		r2:=expr0();
+		if (r1.which!=INT || r2.which!=INT){
+			retval.which = UNKNOWN;
+			return retval;
+		}
+		if (tok == LSHIF)
+			retval.i_val=r1.i_val << r2.i_val;
+		if (tok == RSHIF)
+			retval.i_val=r1.i_val >> r2.i_val;
+		return retval;
+	}
+	return retval;
+}
+	
+expr0() : leaf {
+	retval : leaf;
+	r1:=expr1();
+	retval=r1;
+	tok := getTok(0);
+	while(tok==ADD || tok==SUB){
+		getTok(1);
+		r2:=expr1();
+		if (r1.which==UNKNOWN || r2.which==UNKNOWN){
+			r1.which=UNKNOWN;
+			return r1;
+		}
+		if (r2.which==r1.which){
+			case tok{
+				ADD =>
+					if (r1.which==INT)
+						r1.i_val+=r2.i_val;
+					else if (r1.which==REAL)
+						r1.r_val+=r2.r_val;
+				SUB =>
+					if (r1.which==INT)
+						r1.i_val-=r2.i_val;
+					else if (r1.which==REAL)
+						r1.r_val-=r2.r_val;
+			}
+			retval = r1;
+		}else{
+			retval.which = REAL;
+			ar1,ar2 : real;
+			if (r1.which==INT)
+				ar1= real r1.i_val;
+			else
+				ar1 = r1.r_val;
+			if (r2.which==INT)
+				ar2= real r2.i_val;
+			else
+				ar2 = r2.r_val;
+			if (tok==ADD)
+				retval.r_val = ar1+ar2;
+			if (tok==SUB)
+				retval.r_val = ar1-ar2;
+		}
+	tok=getTok(0);
+	}
+	return retval;
+}
+
+expr1() : leaf	{
+	retval : leaf;
+	r1:=expr2();
+	retval=r1;
+	tok := getTok(0);
+	while(tok==MUL || tok==DIV || tok==MOD){
+		getTok(1);
+		r2:=expr2();
+		if (tok==MOD){
+			if (r1.which!=INT && r2.which!=INT){
+				r1.which=UNKNOWN;
+				return r1;
+			}
+			r1.i_val %= r2.i_val;
+			return r1;
+		}
+		if (r1.which==UNKNOWN || r2.which==UNKNOWN){
+			r1.which=UNKNOWN;
+			return r1;
+		}
+		if (r2.which==r1.which){
+			case tok{
+				MUL =>
+					if (r1.which==INT)
+						r1.i_val*=r2.i_val;
+					else if (r1.which==REAL)
+						r1.r_val*=r2.r_val;
+				DIV =>
+					if (r1.which==INT)
+						r1.i_val/=r2.i_val;
+					else if (r1.which==REAL)
+						r1.r_val/=r2.r_val;
+			}
+			retval = r1;
+		}else{
+			retval.which = REAL;
+			ar1,ar2 : real;
+			if (r1.which==INT)
+				ar1= real r1.i_val;
+			else
+				ar1 = r1.r_val;
+			if (r2.which==INT)
+				ar2= real r2.i_val;
+			else
+				ar2 = r2.r_val;
+			if (tok==MUL)
+				retval.r_val = ar1*ar2;
+			if (tok==DIV)
+				retval.r_val = ar1/ar2;
+		}
+	tok=getTok(0);
+	}
+	return retval;
+}
+
+expr2() : leaf	{
+	tok := getTok(0);
+	if(tok==ADD || tok==SUB || tok==EXCL || tok==TILDE){
+		getTok(1);
+		r1:=expr2();
+		if (r1.which!=UNKNOWN)
+			case tok{
+				ADD =>
+					;
+				SUB =>
+					if (r1.which==INT)
+						r1.i_val=-r1.i_val;
+					else if (r1.which==REAL)
+						r1.r_val=-r1.r_val;
+				EXCL =>
+					if (r1.which != INT)
+						r1.which=UNKNOWN;
+					else
+						r1.i_val = !r1.i_val;
+				TILDE =>
+					if (r1.which != INT)
+						r1.which=UNKNOWN;
+					else
+						r1.i_val = ~r1.i_val;
+			}
+		else
+			r1.which = UNKNOWN;	
+		return r1;
+	}
+	return expr5();
+}
+
+do_func(tok : int) : leaf {
+	retval : leaf;
+	r1,r2 : real;
+	ok : int;
+	retval.which=REAL;
+	case tok{
+		F_ACOS => 
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->acos(r1);
+		F_ASIN => 
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->asin(r1);
+		F_ATAN => 
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->atan(r1);
+		F_ATAN2 => 
+			(ok,r1,r2)=pars_rfunc(2);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->atan2(r1,r2);
+		F_CEIL => 
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->ceil(r1);
+		F_COS =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->cos(r1); 
+		F_COSH =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->cosh(r1);
+		F_EXP => 
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->exp(r1);
+		F_FLOOR => 
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->floor(r1);
+		F_FMOD => 
+			(ok,r1,r2)=pars_rfunc(2);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->fmod(r1,r2);
+		F_HYPOT =>
+			(ok,r1,r2)=pars_rfunc(2);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->hypot(r1,r2);
+		F_LOG =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->log(r1);
+		F_LOG10 =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->log10(r1);
+		F_POW =>
+			(ok,r1,r2)=pars_rfunc(2);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->pow(r1,r2);
+		F_SIN =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->sin(r1);
+		F_SINH =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->sinh(r1);
+		F_SQRT =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->sqrt(r1);
+		F_TAN =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->tan(r1);
+		F_TANH =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->tanh(r1);
+		F_ABS =>
+			(ok,r1,r2)=pars_rfunc(1);
+			if (!ok){
+				retval.which=UNKNOWN;
+				return retval;
+			}
+			retval.r_val=math->fabs(r1);
+		* =>
+			sys->print("unexpected op %d\n", tok);
+			retval.which=UNKNOWN;
+	}
+	return retval;
+}
+
+pars_rfunc(args : int) : (int,real,real){
+	a1,a2 : real;
+	ok := 1;
+	if (getTok(0)!=L_BRACE)
+		ok=0;	
+	getTok(1);
+	r1:=expr_9();
+	if (r1.which == INT)
+		a1 = real r1.i_val;
+	else if (r1.which == REAL)
+		a1 = r1.r_val;
+	else ok=0;
+	if(args==2){
+		if (getTok(0)!=COMMA)
+			ok=0;
+		getTok(1);
+		r2:=expr_9();
+		if (r2.which == INT)
+			a2 = real r2.i_val;
+		else if (r2.which == REAL)
+			a2 = r2.r_val;
+		else ok=0;
+	}
+	if (getTok(0)!=R_BRACE)
+		ok=0;	
+	getTok(1);
+	return (ok,a1,a2);
+}
+
+
+expr5() : leaf {
+	retval : leaf;
+	tok:=getTok(1);
+	if (tok>=F_ABS && tok<=F_TANH)
+		return do_func(tok);
+	case tok{
+		STRING =>
+			retval.which = STRING;
+			retval.s_val = s_val;
+		INT =>
+			retval.which = INT;
+			retval.i_val = i_val;
+		REAL =>
+			retval.which = REAL;
+			retval.r_val = r_val;
+		R_BRACE or COMMA =>
+			return retval;
+		L_BRACE => 
+			r1:=expr_9();
+			if (getTok(1)!=R_BRACE)
+				r1.which=UNKNOWN;
+			return r1;
+		* =>
+			retval.which = UNKNOWN;
+	}
+	return retval;
+}
+
--- /dev/null
+++ b/appl/lib/tcl_core.b
@@ -1,0 +1,1397 @@
+implement Tcl_Core;
+
+# these are the outside modules, self explanatory..
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+
+include "bufio.m";
+	bufmod : Bufio;
+Iobuf : import bufmod;
+
+include "string.m";
+	str : String;
+
+include "tk.m";
+	tk: Tk;
+
+include	"wmlib.m";
+	wmlib: Wmlib;
+
+# these are stand alone Tcl libraries, for Tcl pieces that
+# are "big" enough to be called their own.
+
+include "tcl.m";
+
+include "tcllib.m";
+
+include "utils.m";
+	htab: Str_Hashtab;
+	mhtab : Mod_Hashtab; 
+	shtab : Sym_Hashtab;
+	stack : Tcl_Stack;
+	utils : Tcl_Utils;
+
+Hash: import htab;
+MHash : import mhtab;
+SHash : import shtab;
+
+
+
+
+# global error flag and message. One day, this will be stack based..
+errmsg : string;
+error, mypid : int;
+
+sproc : adt {
+	name : string;
+	args : string;
+	script : string;
+};
+
+TCL_UNKNOWN, TCL_SIMPLE, TCL_ARRAY : con iota;
+
+# Global vars. Simple variables, and associative arrays.
+libmods : ref MHash;
+proctab := array[100] of sproc;
+retfl : int;
+symtab : ref SHash;
+nvtab : ref Hash;
+avtab : array of (ref Hash,string);
+tclmod : TclData;
+
+core_commands:=array[] of {		
+	"append" , "array", "break" , "continue" , "catch", "dumpstack",  
+	"exit" , "expr" , "eval" ,
+	"for" , "foreach" , 
+	"global" , "if" , "incr" , "info", 
+	"lappend" , "level" , "load" ,
+	"proc" , "return" , "set" ,
+	"source" ,"switch" , "time" ,
+	"unset" , "uplevel", "upvar", "while" , "#" 
+};
+		
+
+about() : array of string {
+	return core_commands;
+}
+		
+init(ctxt: ref Draw->Context, argv: list of string) {
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	bufmod = load Bufio Bufio->PATH;
+	htab = load Str_Hashtab Str_Hashtab->PATH;
+	mhtab = load Mod_Hashtab Mod_Hashtab->PATH;
+	shtab = load Sym_Hashtab Sym_Hashtab->PATH;
+	stack = load Tcl_Stack Tcl_Stack->PATH;
+	str = load String String->PATH;
+	utils = load Tcl_Utils Tcl_Utils->PATH;
+	tk = load Tk Tk->PATH;
+	wmlib= load Wmlib Wmlib->PATH;
+	if (bufmod == nil || htab == nil || stack == nil ||
+		str == nil || utils == nil || tk == nil ||
+		wmlib==nil || mhtab == nil || shtab == nil){
+		sys->print("can't load initial modules %r\n");
+		exit;
+	}	
+
+	# get a new stack frame.
+	stack->init();
+	(nvtab,avtab,symtab)=stack->newframe();
+	
+	libmods=mhtab->alloc(101);
+
+	# grab my pid, and set a new group to make me easy to kill.
+	mypid=sys->pctl(sys->NEWPGRP, nil);
+
+	# no default top window.
+	tclmod.top=nil;
+	tclmod.context=ctxt;
+	tclmod.debug=0;
+
+	# set up library modules.
+	args:=array[] of {"do_load","io"};
+	do_load(args);
+	args=array[] of {"do_load","string"};
+	do_load(args);
+	args=array[] of {"do_load","calc"};
+	do_load(args);
+	args=array[] of {"do_load","list"};
+	do_load(args);
+	args=array[] of {"do_load","tk"};
+	do_load(args);
+	arr:=about();
+	for(i:=0;i<len arr;i++)
+		libmods.insert(arr[i],nil);
+
+	# cmd line args...
+	if (argv != nil)
+		argv = tl argv;
+	while (argv != nil) {
+		loadfile(hd argv);
+		argv = tl argv;
+	}
+	
+}
+
+set_top(win:ref Tk->Toplevel){
+	tclmod.top=win;
+}
+
+clear_error(){
+	error=0;
+	errmsg="";
+}
+
+notify(num : int,s : string) : string {
+	error=1;
+	case num{
+		1 =>
+			errmsg=sys->sprint(
+			"wrong # args: should be \"%s\"",s);
+		* =>
+			errmsg= s;
+	}
+	return errmsg;
+}
+			
+grab_lines(new_inp,unfin: string ,lines : chan of string){
+	error=0;
+	tclmod.lines=lines;
+	input,line : string;
+	if (new_inp==nil)
+		new_inp = "tcl%";
+	if (unfin==nil)
+		unfin = "tcl>";
+	sys->print("%s ", new_inp);
+	iob := bufmod->fopen(sys->fildes(0),bufmod->OREAD);
+	if (iob==nil){
+		sys->print("cannot open stdin for reading.\n");
+		return;
+	}
+	while((input=iob.gets('\n'))!=nil){
+		line+=input;
+		if (!finished(line,0))
+			sys->print("%s ", unfin);
+		else{
+			lines <- = line;
+			line=nil;
+		}
+	}
+}
+
+# this is the main function. Its input is a complete (i.e. matching 
+# brackets etc) tcl script, and its output is a message - if there 
+# is one.
+evalcmd(s: string, termchar: int) : string {
+	msg : string;
+	i:=0;
+	retfl=0;
+	if (tclmod.debug==2)
+		sys->print("Entered evalcmd, s=%s, termchar=%c\n",s,termchar);
+	# strip null statements..
+	while((i<len s) && (s[i]=='\n' || s[i]==';')) i++;
+	if (i==len s) return nil;
+
+	# parse the script statement by statement
+	for(;s!=nil;i++){
+		# wait till we have a complete statement
+		if (i==len s || ((s[i]==termchar || s[i]==';' || s[i]=='\n')
+			&& finished(s[0:i],termchar))){
+			# throw it away if its a comment...
+			if (s[0]!='#')
+				argv := parsecmd(s[0:i],termchar,0);
+			msg = nil;			
+			if (tclmod.debug==2)
+				for(k:=0;k<len argv;k++)
+				sys->print("argv[%d]: (%s)\n",k,argv[k]);
+
+			# argv is now a completely parsed array of arguments
+			# for the Tcl command..
+			
+			# find the module that the command is in and 
+			# 	execute it.
+			if (len argv != 0){
+				mod:=lookup(argv[0]);
+				if (mod!=nil){
+					(error,msg)= 
+					   mod->exec(ref tclmod,argv);
+					if (error)
+						errmsg=msg;
+				} else {
+					if (argv[0]!=nil && 
+						argv[0][0]=='.')
+						msg=do_tk(argv);
+					else
+						msg=exec(argv);
+				}
+			}
+
+			# was there an error?
+			if (error) {
+				if (len argv > 0 && argv[0]!=""){
+					stat : string;
+					stat = "In function "+argv[0];
+					if (len argv >1 && argv[1]!=""){
+						stat[len stat]=' ';
+						stat+=argv[1];
+					}
+					stat+=".....\n\t";
+					errmsg=stat+errmsg;
+				}
+				msg=errmsg;
+			}
+
+			# we stop parsing if we hit a break, continue, return,
+			# error, termchar or end of string.
+			if (msg=="break" || msg=="continue" || error || retfl==1
+				|| len s <= i || (len s > i && s[i]==termchar))
+				return msg;
+
+			# otherwise eat up the parsed statement and continue
+			s=s[i+1:];
+			i=-1;
+		}
+	}
+	return msg;
+}
+
+				
+# returns 1 if the line has matching braces, brackets and 
+# double-quotes and does not end in "\\\n"
+finished(s : string, termchar : int) : int {
+	cb:=0;
+	dq:=0;
+	sb:=0;
+	if (s==nil) return 1;
+	if (termchar=='}') cb++;
+	if (termchar==']') sb++;
+	if (len s > 1 && s[len s -2]=='\\')
+		return 0;
+	if (s[0]=='{') cb++;
+	if (s[0]=='}' && cb>0) cb--;
+	if (s[0]=='[') sb++;
+	if (s[0]==']' && sb>0) sb--;
+	if (s[0]=='"') dq=1-dq;
+	for(i:=1;i<len s;i++){
+		if (s[i]=='{' && s[i-1]!='\\') cb++;
+		if (s[i]=='}' && s[i-1]!='\\' && cb>0) cb--;
+		if (s[i]=='[' && s[i-1]!='\\') sb++;
+		if (s[i]==']' && s[i-1]!='\\' && sb>0) sb--;
+		if (s[i]=='"' && s[i-1]!='\\') dq=1-dq;
+	}
+	return (cb==0 && sb==0 && dq==0);
+}
+
+# counts the offset till the next matching ']'
+strip_to_match(s : string, ptr: int) : int {
+	j :=0;
+	nb:=0;
+	while(j<len s){
+		if (s[j]=='{')
+			while (j < len s && s[j]!='}') j++;
+		if (s[j]=='[') nb++;
+		if (s[j]==']'){
+			nb--;
+			if (nb==-1) return ptr+j;
+		}
+		j++;
+	}
+	return ptr+j;
+}
+
+# returns the type of variable represented by the string s, which is
+# a name.
+isa(s: string) : (int,int,string) {
+	found,val : int;
+	name,al : string;
+	curlev:=stack->level();
+	if (tclmod.debug==2)
+		sys->print("Called isa with %s, current stack level is %d\n",s,curlev);
+	(found,nil)=nvtab.find(s);
+	if (found) return (TCL_SIMPLE,curlev,s);
+	for (i:=0;i<len avtab;i++){
+		(nil,name)=avtab[i];
+		if (name==s) return (TCL_ARRAY,curlev,s);	
+	}
+	if (symtab==nil)
+		return (TCL_UNKNOWN,curlev,s);
+	(found,val,al)=symtab.find(s);
+	if (!found)
+		return (TCL_UNKNOWN,curlev,s);
+	(tnv,tav,nil):=stack->examine(val);
+	if (tclmod.debug==2)
+		sys->print("have a level %d for %s\n",val,al);
+	if (tnv!=nil){
+		(found,nil)=tnv.find(al);
+		if (found) return (TCL_SIMPLE,val,al);
+	}
+	if (tav!=nil){
+		for (i=0;i<len tav;i++){
+			(nil,name)=tav[i];
+			if (name==al) return (TCL_ARRAY,val,al);	
+		}
+	}	
+	if (tclmod.debug==2)
+		sys->print("%s not found, creating at stack level %d\n",al,val);
+	return (TCL_UNKNOWN,val,al);
+}
+
+# This function only works if the string is already parsed!
+# takes a var_name and returns the hash table for it and the
+# name to look up. This is one of two things:
+# for simple variables:
+# findvar(foo) ---> (nvtab,foo)
+# for associative arrays:
+# findvar(foo(bar)) -----> (avtab[i],bar)
+# where avtab[i].name==foo
+# if create is 1, then an associative array is created upon first
+# reference.
+# returns (nil,error message) if there is a problem.
+
+find_var(s : string,create : int) : (ref Hash,string) {
+	rest,name,index : string;
+	retval,tnv : ref Hash;
+	tav : array of (ref Hash,string);
+	i,tag,lev: int;
+	(name,index)=str->splitl(s,"(");
+	if (index!=nil){
+		(index,rest)=str->splitl(index[1:],")");
+		if (rest!=")")
+			return (nil,"bad variable name");
+	}
+	(tag,lev,name) = isa(name);
+	case tag {
+		TCL_SIMPLE =>
+			if (index!=nil)
+				return (nil,"variable isn't array");
+			(tnv,nil,nil)=stack->examine(lev);
+			return (tnv,name);
+		TCL_ARRAY =>
+			if (index==nil)
+				return (nil,"variable is array");
+			(nil,tav,nil)=stack->examine(lev);
+			for(i=0;i<len tav;i++){
+				(retval,rest)=tav[i];
+				if (rest==name)
+					return (retval,index);
+			}
+			return (nil,"find_var: impossible!!");
+		# if we get here, the variable needs to be
+		# created.
+		TCL_UNKNOWN =>
+			if (!create)
+				return (nil,"no such variable");
+			(tnv,tav,nil)=stack->examine(lev);
+			if (index==nil)
+				return (tnv,name);
+		
+	}
+	# if we get here, we are creating an associative variable in the
+	# tav array.
+	for(i=0;i<len tav;i++){
+		(retval,rest)=tav[i];
+		if (rest==nil){
+			retval=htab->alloc(101);
+			tav[i]=(retval,name);
+			return (retval,index);	
+		}
+	}
+	return (nil,"associative array table full!");
+}
+
+# the main parsing function, a la ousterhouts man pages. Takes a 
+# string that is meant to be a tcl statement and parses it, 
+# reevaluating and quoting upto the termchar character. If disable 
+# is true, then whitespace is not ignored.	
+parsecmd(s: string, termchar,disable: int) : array of string {
+	argv:= array[200] of string;
+	buf,nm,id: string;
+	argc := 0;
+	nc := 0;
+	c :=0;
+	tab : ref Hash;
+	
+	if (disable && (termchar=='\n' || termchar==';')) termchar=0;
+   outer:
+	for (i := 0; i<len s ;) {
+		if ((i>0 &&s[i-1]!='\\' &&s[i]==termchar)||(s[0]==termchar))
+			break;
+		case int s[i] {
+		' ' or '\t' or '\n' =>
+			if (!disable){
+				if (nc > 0) {	# end of a word?
+					argv[argc++] = buf;
+					buf = nil;
+					nc = 0;
+				}
+				i++;
+			}
+			else 
+				buf[nc++]=s[i++];
+		'$' =>
+			if (i>0 && s[i-1]=='\\') 
+				buf[nc++]=s[i++];
+			else {
+				(nm,id) = parsename(s[i+1:], termchar);
+				if (id!=nil)
+					nm=nm+"("+id+")";
+				(tab,nm)=find_var(nm,0); #don't create var!
+				if (len nm > 0 && tab!=nil) {
+					(found, val) := tab.find(nm);
+					buf += val;
+					nc += len val;
+					#sys->print("Here s[i:] is (%s)\n",s[i:]);
+					if(nm==id)
+						while(s[i]!=')') i++;
+					else
+						if (s[i+1]=='{')
+							while(s[i]!='}') i++;
+						else
+							i += len nm;
+					if (nc==0 && (i==len s-1 ||
+							s[i+1]==' ' || 
+							s[i+1]=='\t'|| 
+							s[i+1]==termchar))
+						argv[argc++]=buf;
+				} else {
+					buf[nc++] = '$';
+				}
+				i++;
+			}
+		'{' =>
+			if (i>0 && s[i-1]=='\\') 
+				buf[nc++]=s[i++];
+			else if (s[i+1]=='}'){
+				argv[argc++] = nil;
+				buf = nil;
+				nc = 0;	
+				i+=2;
+			} else {
+				nbra := 1;
+				for (i++; i < len s; i++) {
+					if (s[i] == '{')
+						nbra++;
+					else if (s[i] == '}') {
+						nbra--;
+						if (nbra == 0) {
+							i++;
+							continue outer;
+						}
+					}
+					buf[nc++] = s[i];
+				}
+			}
+		'[' =>
+			if (i>0 && s[i-1]=='\\') 
+				buf[nc++]=s[i++];
+			else{
+				a:=evalcmd(s[i+1:],']');
+				if (error)
+					return nil;
+				if (nc>0){
+					buf+=a;
+					nc += len a;
+				} else {
+					argv[argc++] = a;
+					buf = nil;
+					nc = 0;
+				}
+				i++;
+				i=strip_to_match(s[i:],i);
+				i++;
+			}
+		'"' =>
+			if (i>0 && s[i-1]!='\\' && nc==0){
+				ans:=parsecmd(s[i+1:],'"',1);
+				#sys->print("len ans is %d\n",len ans);
+				if (len ans!=0){
+					for(;;){
+						i++;
+						if(s[i]=='"' && 
+							s[i-1]!='\\')
+						break;
+					}
+					i++;
+					argv[argc++] = ans[0];
+				} else {
+					argv[argc++] = nil;
+					i+=2;
+				}
+				buf = nil;
+				nc = 0;
+			}
+			else buf[nc++] = s[i++];	
+		* =>
+			if (s[i]=='\\'){
+				c=unesc(s[i:]);
+				if (c!=0){
+					buf[nc++] = c;
+					i+=2;
+				} else {
+					if (i+1 < len s && !(s[i+1]=='"'
+						|| s[i+1]=='$' || s[i+1]=='{' 
+						|| s[i+1]=='['))
+						buf[nc++]=s[i];
+					i++;
+				}
+				c=0;
+			} else
+				buf[nc++]=s[i++];
+		}
+	}
+	if (nc > 0)	# fix up last word if present
+		argv[argc++] = buf;
+	ret := array[argc] of string;
+	ret[0:] = argv[0:argc];
+	return ret;
+}
+
+# parses a name by Tcl rules, a valid name is either $foo, $foo(bar)
+# or ${foo}.
+parsename(s: string, termchar: int) : (string,string) {
+	ret,arr,rest: string;
+	rets : array of string;
+	if (len s == 0)
+		return (nil,nil);
+	if (s[0]=='{'){
+		(ret,nil)=str->splitl(s,"}");
+		#sys->print("returning [%s]\n",ret[1:]);
+		return (ret[1:],nil);
+	}
+	loop: for (i := 0; i < len s && s[i] != termchar; i++) {
+		case (s[i]) {
+		'a' to 'z' or 'A' to 'Z' or '0' to '9' or '_' =>
+			ret[i] = s[i];
+		* =>
+			break loop;
+		'(' =>
+			arr=ret[0:i];
+			rest=s[i+1:];
+			rets=parsecmd(rest,')',0);
+			# should always be len 1?
+			if (len rets >1)
+				sys->print("len rets>1 in parsename!\n");
+			return (arr,rets[0]);
+		}
+	}
+	return (ret,nil);
+}
+
+loadfile(file :string) : string {
+	iob : ref Iobuf;
+	msg,input,line : string;
+	if (file==nil)
+		return nil;	
+	iob = bufmod->open(file,bufmod->OREAD);
+	if (iob==nil)
+		return notify(0,sys->sprint(
+			"couldn't read file \"%s\":%r",file));
+	while((input=iob.gets('\n'))!=nil){
+		line+=input;
+		if (finished(line,0)){
+			# put in a return catch here...
+			line = prepass(line);
+			msg=evalcmd(line,0);
+			if (error) return errmsg;
+			line=nil;
+		}
+	}
+	return msg;
+}
+
+
+#unescapes a string. Can do better.....
+unesc(s: string) : int {
+	c: int;
+	if (len s == 1) return 0;
+	case s[1] {
+		'a'=>   c = '\a';
+		'n'=>	c = '\n';
+		't'=>	c = '\t';
+		'r'=>	c = '\r';
+		'b'=>	c = '\b';
+		'\\'=>	c = '\\';
+		'}' =>  c = '}';
+		']' =>  c=']';
+		# do hex and octal.
+		* =>	c = 0;
+	}
+	return c;
+}
+
+# prepass a string and replace "\\n[ \t]*" with ' '
+prepass(s : string) : string {
+	for(i := 0; i < len s; i++) {
+		if(s[i] != '\\')
+			continue;
+		j:=i;
+		if (s[i+1] == '\n') {
+			s[j]=' ';  
+			i++;
+			while(i<len s && (s[i]==' ' || s[i]=='\t'))
+				i++;
+			if (i==len s)
+				s = s[0:j];
+			else
+				s=s[0:j]+s[i+1:];
+		i=j;
+		}
+	}
+	return s;
+}
+
+exec(argv : array of string) : string {
+	msg : string;
+	if (argv[0]=="")
+		return nil;
+	case (argv[0]) {		
+		"append" =>
+			msg= do_append(argv);
+		"array" =>
+			msg= do_array(argv);
+		"break" or "continue" =>
+			return argv[0];
+		"catch" =>
+			msg=do_catch(argv);
+		"debug" =>
+			msg=do_debug(argv);
+		"dumpstack" =>
+			msg=do_dumpstack(argv);
+		"exit" =>
+			do_exit();
+		"expr" =>
+			msg = do_expr(argv);
+		"eval" =>
+			msg = do_eval(argv);
+		"for" =>
+			msg = do_for(argv);
+		"foreach" =>
+			msg = do_foreach(argv);
+		"format" =>
+			msg = do_string(argv);
+		"global" =>
+			msg = do_global(argv);
+		"if" =>
+			msg = do_if(argv);
+		"incr" =>
+			msg = do_incr(argv);
+		"info" =>
+			msg = do_info(argv);
+		"lappend" =>
+			msg = do_lappend(argv);
+		"level" =>
+			msg=sys->sprint("Current Stack "+
+			    "level is %d",
+				stack->level());
+		"load" =>
+			msg=do_load(argv);
+		"proc" =>
+			msg=do_proc(argv);
+		"return" =>
+			msg=do_return(argv);
+			retfl =1;
+		"set" =>
+			msg = do_set(argv);
+		"source" =>
+			msg = do_source(argv);
+		"string" =>
+			msg = do_string(argv);
+		"switch" => 
+			msg = do_switch(argv);
+		"time" =>
+			msg=do_time(argv);
+		"unset" =>
+			msg = do_unset(argv);
+		"uplevel" =>
+			msg=do_uplevel(argv);
+		"upvar" =>
+			msg=do_upvar(argv);		
+		"while" =>
+			msg = do_while(argv);
+		"#" => 
+			msg=nil;
+		* =>	
+			msg = uproc(argv);
+	}
+	return msg;
+}
+
+# from here on is the list of commands, alpahabetised, we hope.
+
+do_append(argv :array of string) : string {
+	tab : ref Hash;
+	if (len argv==1 || len argv==2)
+		 return notify(1,
+			"append varName value ?value ...?");
+	name := argv[1];
+	(tab,name)=find_var(name,1);
+	if (tab==nil)
+		return notify(0,name);
+	(found, val) := tab.find(name);
+	for (i:=2;i<len argv;i++)
+		val+=argv[i];
+	tab.insert(name,val);	
+	return val;
+}
+
+do_array(argv : array of string) : string {
+	tab : ref Hash;
+	name : string;
+	flag : int;
+	if (len argv!=3)
+		return notify(1,"array [names, size] name");
+	case argv[1] {
+		"names" =>
+			flag=1;
+		"size" =>
+			flag=0;
+		* =>
+			return notify(0,"expexted names or size, got "+argv[1]);
+			
+	}
+	(tag,lev,al) := isa(argv[2]);
+	if (tag!=TCL_ARRAY)
+		return notify(0,argv[2]+" isn't an array");
+	(nil,tav,nil):=stack->examine(lev);
+	for (i:=0;i<len tav;i++){
+		(tab,name)=tav[i];
+		if (name==al) break;
+	}
+	if (flag==0)
+		return string tab.lsize;
+	return tab.dump();
+}
+
+do_catch(argv : array of string) : string {
+	if (len argv==1 || len argv > 3)
+		return notify(1,"catch command ?varName?");
+	msg:=evalcmd(argv[1],0);
+	if (len argv==3 && error){
+		(tab,name):=find_var(argv[2],1);
+		if (tab==nil)
+			return notify(0,name);
+		tab.insert(name, msg);
+	}
+	ret:=string error;
+	error=0;
+	return ret;
+}
+
+do_debug(argv : array of string) : string {
+	add : string;
+	if (len argv!=2)
+		return notify(1,"debug");
+	(i,rest):=str->toint(argv[1],10);
+	if (rest!=nil)
+		return notify(0,"Expected integer and got "+argv[1]);
+	tclmod.debug=i;
+	if (tclmod.debug==0)
+		add="off";
+	else
+		add="on";
+	return "debugging is now "+add+" at level"+ string i;
+} 
+
+do_dumpstack(argv : array of string) : string {
+	if (len argv!=1)
+		return notify(1,"dumpstack");
+	stack->dump();
+	return nil;
+}
+	
+do_eval(argv : array of string) : string {
+	eval_str : string;
+	for(i:=1;i<len argv;i++){
+		eval_str += argv[i];
+		eval_str[len eval_str]=' ';
+	}
+	return evalcmd(eval_str[0:len eval_str -1],0);
+}
+
+do_exit(){
+	kfd := sys->open("#p/"+string mypid+"/ctl", sys->OWRITE);
+	if(kfd == nil) 
+		sys->print("error opening pid %d (%r)\n",mypid);
+	sys->fprint(kfd, "killgrp");
+	exit;
+}
+
+
+
+do_expr(argv : array of string) : string {
+	retval : string;
+	for (i:=1;i<len argv;i++){
+		retval+=argv[i];
+		retval[len retval]=' ';
+	}
+	retval=retval[0: len retval -1];
+	argv=parsecmd(retval,0,0);
+	cal:=lookup("calc");
+	(err,ret):= cal->exec(ref tclmod,argv);
+	if (err) return notify(0,ret);
+	return ret;
+}
+
+
+do_for(argv : array of string) : string {
+	if (len argv!=5)
+		return notify(1,"for start test next command");
+	test := array[] of {"expr",argv[2]};
+	evalcmd(argv[1],0);
+	for(;;){
+		msg:=do_expr(test);
+		if (msg=="Error!")
+		return notify(0,sys->sprint(
+			"syntax error in expression \"%s\"",
+					argv[2]));
+		if (msg=="0")
+			return nil;
+		msg=evalcmd(argv[4],0);
+		if (msg=="break")
+			return nil;
+		if (msg=="continue"); #do nothing!
+		evalcmd(argv[3],0);
+		if (error)
+			return errmsg;
+	}
+}
+
+
+
+do_foreach(argv: array of string) : string{
+	tab : ref Hash;
+	if (len argv!=4)
+		return notify(1,"foreach varName list command");
+	name := argv[1];
+	(tab,name)=find_var(name,1);
+	if (tab==nil)
+		return notify(0,name);
+	arr:=utils->break_it(argv[2]);
+	for(i:=0;i<len arr;i++){
+		tab.insert(name,arr[i]);
+		evalcmd(argv[3],0);
+	}	
+	return nil;
+}
+
+
+
+do_global(argv : array of string) : string {
+	if (len argv==1)
+		return notify(1,"global varName ?varName ...?");
+	if (symtab==nil)
+		return nil;
+	for (i:=1 ; i < len argv;i++)
+		symtab.insert(argv[i],argv[i],0);
+	return nil;
+}
+
+
+	
+do_if(argv : array of string) : string {
+	if (len argv==1)
+		return notify(1,"no expression after \"if\" argument");
+	expr1 := array[] of {"expr",argv[1]};
+	msg:=do_expr(expr1);
+	if (msg=="Error!")
+		return notify(0,sys->sprint(
+			"syntax error in expression \"%s\"",
+					argv[1]));
+	if (len argv==2)
+		return notify(1,sys->sprint(
+			"no script following \""+
+					"%s\" argument",msg));
+	if (msg=="0"){
+		if (len argv>3){
+			if (argv[3]=="else"){
+				if (len argv==4)
+					return notify(1,
+					"no script"+
+				" following \"else\" argument");
+				return evalcmd(argv[4],0);
+			}
+			if (argv[3]=="elseif"){
+				argv[3]="if";
+				return do_if(argv[3:]);
+			}
+		}
+		return nil;
+	}
+	return evalcmd(argv[2],0);
+}
+
+do_incr(argv :array of string) : string {
+	num,xtra : int;
+	rest :string;
+	tab : ref Hash;
+	if (len argv==1)
+		return notify(1,"incr varName ?increment?");
+	name := argv[1];
+	(tab,name)=find_var(name,0); #doesn't create!!
+	if (tab==nil)
+		return notify(0,name);
+	(found, val) := tab.find(name);
+	if (!found)
+		return notify(0,sys->sprint("can't read \"%s\": "
+			+"no such variable",name));
+	(num,rest)=str->toint(val,10);
+	if (rest!=nil)
+		return notify(0,sys->sprint(
+			"expected integer but got \"%s\"",val));
+	if (len argv == 2){	
+		num+=1;
+		tab.insert(name,string num);
+	}
+	if (len argv == 3) {
+		val = argv[2];
+		(xtra,rest)=str->toint(val,10);
+		if (rest!=nil)
+			return notify(0,sys->sprint(
+				"expected integer but got \"%s\""
+							,val));
+		num+=xtra;
+		tab.insert(name, string num);
+	} 
+	return string num;
+}
+
+do_info(argv : array of string) : string {
+	if (len argv==1)
+		return notify(1,"info option ?arg arg ...?");
+	case argv[1] {
+		"args" =>
+			return do_info_args(argv,0);
+		"body" =>
+			return do_info_args(argv,1); 
+		"commands" =>
+			return do_info_commands(argv);
+		"exists" =>
+			return do_info_exists(argv);
+		"procs" =>
+			return do_info_procs(argv);
+
+	}
+	return sys->sprint(
+	"bad option \"%s\": should be args, body, commands, exists, procs",
+			argv[1]);
+}
+
+do_info_args(argv : array of string,body :int) : string { 
+	name: string;
+	s : sproc;
+	if (body)
+		name="body";
+	else
+		name="args";
+	if (len argv!=3)
+		return notify(1,"info "+name+" procname");
+	for(i:=0;i<len proctab;i++){
+		s=proctab[i];
+		if (s.name==argv[2])
+			break;
+	}
+	if (i==len proctab)
+		return notify(0,argv[2]+" isn't a procedure.");
+	if (body)
+		return s.script;
+	return s.args;
+}
+	
+do_info_commands(argv : array of string) : string { 
+	if (len argv==1 || len argv>3)
+		return notify(1,"info commands [pattern]");
+	return libmods.dump();
+}		
+
+do_info_exists(argv : array of string) : string { 
+	name, index : string;
+	tab : ref Hash;
+	if (len argv!=3)
+		return notify(1,"info exists varName");
+	(name,index)=parsename(argv[2],0);
+	(i,nil,nil):=isa(name);
+	if (i==TCL_UNKNOWN)
+		return "0";
+	if (index==nil)
+		return "1";
+	(tab,name)=find_var(argv[2],0);
+	if (tab==nil)
+		return "0";
+	(found, val) := tab.find(name);
+	if (!found)
+		return "0";
+	return "1";	
+	
+}
+
+do_info_procs(argv : array of string) : string { 
+	if (len argv==1 || len argv>3)
+		return notify(1,"info procs [pattern]");
+	retval : string;
+	for(i:=0;i<len proctab;i++){
+		s:=proctab[i];
+		if (s.name!=nil){
+			retval+=s.name;
+			retval[len retval]=' ';
+		}
+	}
+	return retval;			
+}
+	
+do_lappend(argv : array of string) : string{
+	tab : ref Hash;
+	retval :string;
+	retval=nil;
+	if (len argv==1 || len argv==2)
+		return notify(1,
+			"lappend varName value ?value ...?");
+	name := argv[1];
+	(tab,name)=find_var(name,1);
+	if (tab==nil)
+		return notify(0,name);
+	(found, val) := tab.find(name);
+	for(i:=2;i<len argv;i++){
+		flag:=0;
+		if (spaces(argv[i])) flag=1;
+		if (flag) retval[len retval]='{';
+		retval += argv[i];
+		if (flag) retval[len retval]='}';
+		retval[len retval]=' ';
+	}
+	if (retval!=nil)
+		retval=retval[0:len retval-1];	
+	if (val!=nil)
+		retval=val+" "+retval;
+	tab.insert(name,retval);	
+	return retval;
+}
+
+spaces(s : string) : int{
+	if (s==nil) return 1;
+	for(i:=0;i<len s;i++)
+		if (s[i]==' ' || s[i]=='\t') return 1;
+	return 0;
+}
+
+do_load(argv : array of string) : string {
+	# look for a dis library to load up, then
+	# add to library array.
+	if (len argv!=2)
+		return notify(1,"load libname");
+	fname:="/dis/lib/tcl_"+argv[1]+".dis";
+	mod:= load TclLib fname;
+	if (mod==nil)
+		return notify(0,
+			sys->sprint("Cannot load %s",fname));
+	arr:=mod->about();
+	for(i:=0;i<len arr;i++)
+		libmods.insert(arr[i],mod);
+	return nil;
+}
+	
+	
+do_proc(argv : array of string) : string {
+	if (len argv != 4)
+		return notify(1,"proc name args body");
+	for(i:=0;i<len proctab;i++)
+		if (proctab[i].name==nil || 
+			proctab[i].name==argv[1]) break;
+	if (i==len proctab)
+		return notify(0,"procedure table full!");
+	proctab[i].name=argv[1];
+	proctab[i].args=argv[2];
+	proctab[i].script=argv[3];
+	return nil;
+}
+
+do_return(argv : array of string) : string {
+	if (len argv==1)
+		return nil;
+	# put in options here.....
+	return argv[1];
+}
+	
+do_set(argv : array of string) : string {
+	tab : ref Hash;
+	if (len argv == 1 || len argv > 3)
+		return notify(1,"set varName ?newValue?");
+	name := argv[1];
+	(tab,name)=find_var(name,1);
+	if (tab==nil)
+		return notify(0,name);
+	(found, val) := tab.find(name);
+	if (len argv == 2)
+		if (!found)
+			val = notify(0,sys->sprint(
+				"can't read \"%s\": "
+				+"no such variable",name));
+	if (len argv == 3) {
+		val = argv[2];
+		tab.insert(name, val);
+	} 
+	return val;
+}
+
+do_source(argv : array of string) : string {
+	if (len argv !=2)
+		return notify(1,"source fileName");
+	return loadfile(argv[1]);
+}
+
+do_string(argv : array of string) : string {
+	stringmod := lookup("string");
+	if (stringmod==nil)
+		return notify(0,sys->sprint(
+		"String Package not loaded (%r)"));
+	(err,retval):= stringmod->exec(ref tclmod,argv);
+	if (err) return notify(0,retval);
+	return retval;
+}
+
+do_switch(argv : array of string) : string {
+	i:=0;
+	arr : array of string;
+	if (len argv < 3)
+		return notify(1,"switch "
+			+"?switches? string pattern body ... "+
+			"?default body?\"");
+	if (len argv == 3)
+		arr=utils->break_it(argv[2]);
+	else 
+		arr=argv[2:];
+	if (len arr % 2 !=0)
+		return notify(0,
+			"extra switch pattern with no body");
+	for (i=0;i<len arr;i+=2)
+		if (argv[1]==arr[i])
+			break;
+	if (i==len arr){
+		if (arr[i-2]=="default")
+			return evalcmd(arr[i-1],0);
+		else return nil;
+	}
+	while (i<len arr && arr[i+1]=="-") i+=2;
+	return evalcmd(arr[i+1],0);
+}	
+
+do_time(argv : array of string) : string {
+	rest : string;
+	end,start,times : int;
+	if (len argv==1 || len argv>3)
+		return notify(1,"time command ?count?");
+	if (len argv==2)
+		times=1;
+	else{
+		(times,rest)=str->toint(argv[2],10);
+		if (rest!=nil)
+			return notify(0,sys->sprint(
+				"expected integer but got \"%s\"",argv[2]));
+	}
+	start=sys->millisec();
+	for(i:=0;i<times;i++)
+		evalcmd(argv[1],0);
+	end=sys->millisec();
+	r:= (real end - real start) / real times;
+	return sys->sprint("%g milliseconds per iteration", r);
+}
+
+do_unset(argv : array of string) : string {
+	tab : ref Hash;
+	name: string;
+	if (len argv == 1)
+		return notify(1,"unset "+
+			"varName ?varName ...?");
+	for(i:=1;i<len argv;i++){
+		name = argv[i];
+		(tab,name)=find_var(name,0);
+		if (tab==nil)
+			return notify(0,sys->sprint("can't unset \"%s\": no such" +
+					" variable",name));
+		tab.delete(name);
+
+	}
+	return nil;
+}
+
+do_uplevel(argv : array of string) : string {
+	level: int;
+	rest,scr : string;
+	scr=nil;
+	exact:=0;
+	i:=1;
+	if (len argv==1)
+		return notify(1,"uplevel ?level? command ?arg ...?");
+	if (len argv==2)
+		level=-1;
+	else {
+		lev:=argv[1];
+		if (lev[0]=='#'){
+			exact=1;
+			lev=lev[1:];
+		}
+		(level,rest)=str->toint(lev,10);
+		if (rest!=nil){
+			i=2;	
+			level =-1;
+		}
+	}
+	oldlev:=stack->level();
+	if (!exact)
+		level+=oldlev;
+	(tnv,tav,sym):=stack->examine(level);
+	if (tnv==nil && tav==nil)
+		return notify(0,"bad level "+argv[1]);
+	if (tclmod.debug==2)
+		sys->print("In uplevel, current level is %d, moving to level %d\n",
+				oldlev,level);
+	stack->move(level);
+	oldav:=avtab;
+	oldnv:=nvtab;
+	oldsym:=symtab;
+	avtab=tav;
+	nvtab=tnv;
+	symtab=sym;
+	for(;i<len argv;i++)
+		scr=scr+argv[i]+" ";
+	msg:=evalcmd(scr[0:len scr-1],0);
+	avtab=oldav;
+	nvtab=oldnv;
+	symtab=oldsym;
+	ok:=stack->move(oldlev);
+	if (tclmod.debug==2)
+		sys->print("Leaving uplevel, current level is %d, moving back to"+
+				" level %d,move was %d\n",
+				level,oldlev,ok);
+	return msg;
+}
+				
+do_upvar(argv : array of string) : string {
+	level:int;
+	rest:string;
+	i:=1;
+	exact:=0;
+	if (len argv<3 || len argv>4)
+		return notify(1,"upvar ?level? ThisVar OtherVar");
+	if (len argv==3)
+		level=-1;
+	else {
+		lev:=argv[1];
+		if (lev[0]=='#'){
+			exact=1;
+			lev=lev[1:];
+		}
+		(level,rest)=str->toint(lev,10);
+		if (rest!=nil){
+			i=2;	
+			level =-1;
+		}
+	}
+	if (!exact)
+		level+=stack->level();
+	symtab.insert(argv[i],argv[i+1],level);
+	return nil;
+}	
+				
+do_while(argv : array of string) : string {
+	if (len argv!=3)
+		return notify(1,"while test command");
+	for(;;){
+		expr1 := array[] of {"expr",argv[1]};
+		msg:=do_expr(expr1);
+		if (msg=="Error!")
+			return notify(0,sys->sprint(
+			"syntax error in expression \"%s\"",
+					argv[1]));
+		if (msg=="0")
+			return nil;
+		evalcmd(argv[2],0);
+		if (error)
+			return errmsg;
+	}
+}
+
+uproc(argv : array of string) : string {
+	cmd,add : string;
+	for(i:=0;i< len proctab;i++)
+		if (proctab[i].name==argv[0])
+			break;
+	if (i==len proctab)
+		return notify(0,sys->sprint("invalid command name \"%s\"",
+				argv[0]));
+	# save tables
+	# push a newframe
+	# bind args to arguments
+	# do cmd
+	# pop frame
+	# return msg
+
+	# globals are supported, but upvar and uplevel are not!
+
+	arg_arr:=utils->break_it(proctab[i].args);
+	j:=len arg_arr;
+	if (len argv < j+1 && arg_arr[j-1]!="args"){
+		j=len argv-1;
+		return notify(0,sys->sprint(
+			"no value given for"+
+			" parameter \"%s\" to \"%s\"",
+			arg_arr[j],proctab[i].name));
+	}
+	if ((len argv > j+1) && arg_arr[j-1]!="args")
+		return notify(0,"called "+proctab[i].name+
+					" with too many arguments");
+	oldavtab:=avtab;
+	oldnvtab:=nvtab;
+	oldsymtab:=symtab;
+	(nvtab,avtab,symtab)=stack->newframe();
+	for (j=0;j< len arg_arr-1;j++){
+		cmd="set "+arg_arr[j]+" {"+argv[j+1]+"}";
+		evalcmd(cmd,0);
+	}
+	if (len arg_arr>j && arg_arr[j] != "args") {
+		cmd="set "+arg_arr[j]+" {"+argv[j+1]+"}";
+		evalcmd(cmd,0);
+	}
+	else {
+		if (len arg_arr > j) {
+			if (j+1==len argv)
+				add="";
+			else
+				add=argv[j+1];
+			cmd="set "+arg_arr[j]+" ";
+			arglist:="{"+add+" ";
+			j++;
+			while(j<len argv-1) {
+				arglist+=argv[j+1];
+				arglist[len arglist]=' ';
+				j++;
+			}
+			arglist[len arglist]='}';
+			cmd+=arglist;
+			evalcmd(cmd,0);
+		}
+	}
+	msg:=evalcmd(proctab[i].script,0);
+	stack->pop();
+	avtab=oldavtab;
+	nvtab=oldnvtab;
+	symtab=oldsymtab;
+	#sys->print("Error is %d, msg is %s\n",error,msg);
+	return msg;
+}
+		
+do_tk(argv : array of string) : string {
+	tkpack:=lookup("button");
+	(err,retval):= tkpack->exec(ref tclmod,argv);
+	if (err) return notify(0,retval);
+	return retval;
+}
+
+
+lookup(s : string) : TclLib {
+	(found,mod):=libmods.find(s);
+	if (!found)
+		return nil;
+	return mod;
+}
--- /dev/null
+++ b/appl/lib/tcl_inthash.b
@@ -1,0 +1,95 @@
+implement Int_Hashtab;
+
+include "sys.m";
+include "draw.m";
+include "tk.m";
+include "tcl.m";
+include "tcllib.m";
+include "utils.m";
+
+
+hashasu(key : string,n : int): int{
+        i, h : int;
+	h=0;
+	i=0;
+        while(i<len key){
+                h = 10*h + key[i];
+		h = h%n;
+		i++;
+	}
+        return h%n;
+}
+
+alloc(size : int) : ref IHash {
+	h : IHash;
+	t : list of H_link;
+	t=nil;
+	h.size= size;
+	h.tab = array[size]  of {* => t};
+	return ref h;
+}
+
+
+IHash.insert(h : self ref IHash,name: string,val:int) : int {
+	link : H_link;
+	hash,found : int;
+	nlist : list of H_link;
+	nlist=nil;
+	found=0;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link=hd tmp;
+		if (link.name==name){
+			found=1;
+			link.val = val;
+		}
+		nlist = link :: nlist;
+	}
+	if (!found){
+		link.name=name;
+		link.val=val;
+		(h.tab)[hash]= link :: (h.tab)[hash];
+	}else
+		(h.tab)[hash]=nlist;
+	return 1;
+}
+
+IHash.find(h : self ref IHash,name : string) : (int, int){
+	hash,flag : int;
+	nlist : list of H_link;
+	retval:=0;
+	flag=0;
+	nlist=nil;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if ((hd tmp).name==name){
+			flag = 1;
+			retval = (hd tmp).val;
+		}
+		nlist = link :: nlist;
+	}
+	(h.tab)[hash]=nlist;
+	return (flag,retval);
+}	
+
+IHash.delete(h : self ref IHash,name : string) : int {
+	hash,flag : int;
+	nlist : list of H_link;
+	flag=0;
+	nlist=nil;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if (link.name==name)
+			flag = 1;
+		else
+			nlist = link :: nlist;
+	}
+	(h.tab)[hash]=nlist;
+	return flag;
+}
+
--- /dev/null
+++ b/appl/lib/tcl_io.b
@@ -1,0 +1,350 @@
+implement TclLib;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufmod : Bufio;
+Iobuf : import bufmod;
+
+include "string.m";
+	str : String;
+
+include "tk.m";
+
+include "tcl.m";
+
+include "tcllib.m";
+
+error : int;
+started : int;
+tclmod : ref Tcl_Core->TclData;
+
+name2fid : array of (ref Iobuf,string,int);
+
+valid_commands := array[] of {
+		"close", 
+		"eof" , 
+		"file", 
+		"flush",  
+		"gets" , 
+		"open",  
+		"puts",  
+		"read" , 
+		"seek" , 
+		"tell"  
+};
+
+init() : string {
+	started=1;
+	str = load String String->PATH;
+	sys = load Sys Sys->PATH;
+	bufmod = load Bufio Bufio->PATH;
+	if (str==nil || bufmod==nil)
+		return "Can't initialise IO package.";
+	name2fid = array[100] of (ref Iobuf,string,int);
+	stdout := bufmod->fopen(sys->fildes(1),bufmod->OWRITE);
+	if (stdout==nil)
+		return "cannot open stdout for writing.\n";
+	name2fid[0]=(nil,"stdin",0);
+	name2fid[1]=(stdout,"stdout",0);
+	return nil;
+}
+
+about() : array of string{
+	return valid_commands;
+}
+	
+exec(tcl : ref Tcl_Core->TclData,argv : array of string) : (int,string) {
+	tclmod=tcl;
+	msg :string;
+	if (!started) init();
+	error=0;
+	case argv[0] {
+		"close" => 
+			msg = do_close(argv);
+			return (error,msg);
+		"eof" => 
+			msg = do_eof(argv);
+			return (error,msg);
+		"file" => 
+			msg = do_nothing(argv);
+			return (error,msg);				
+		"flush" => 
+			msg = do_nothing(argv);
+			return (error,msg);
+		"gets" => 
+			msg = do_gets(argv);
+			return (error,msg);
+		"open" => 
+			msg = do_open(argv);
+			return (error,msg);
+		"puts" => 
+			msg = do_puts(argv);
+			return (error,msg);
+		"read" => 
+			msg = do_read(argv);
+			return (error,msg);
+		"seek" => 
+			msg = do_seek(argv);
+			return (error,msg);
+		"tell" => 
+			msg = do_nothing(argv);
+			return (error,msg);
+	}
+	return (1,nil);
+}
+
+do_nothing(argv : array of string) : string {
+	if (len argv==0);
+	return nil;
+}
+
+do_close(argv : array of string) : string {
+	iob : ref Iobuf;
+	name : string;
+	j : int;
+	iob=nil;
+	if (len argv!=2)
+		return notify(1,"close fileId");
+	for(i:=0;i<len name2fid;i++){
+		(iob,name,j)=name2fid[i];
+		if (name==argv[1]) 
+			break;
+	}
+	if (iob==nil)
+		return notify(0,sys->sprint("bad file identifier \"%s\"",
+						argv[1]));
+	iob.flush();
+	iob.close();
+	iob=nil;
+	name2fid[i]=(nil,"",0);
+	return nil;
+}
+
+do_eof(argv : array of string) : string {
+	name : string;
+	j : int;
+	iob : ref Iobuf;
+	if (len argv!=2)
+		return notify(1,"eof fileId");
+	for(i:=0;i<len name2fid;i++){
+		(iob,name,j)=name2fid[i];
+		if (name==argv[1]) 
+			return string j;
+	}
+	return notify(0,sys->sprint("bad file identifier \"%s\"",argv[1]));
+}
+
+
+do_gets(argv : array of string) : string {
+	iob : ref Iobuf;
+	line : string;
+	if (len argv==1 || len argv > 3)
+		return notify(1,"gets fileId ?varName?");
+	if (argv[1]=="stdin")
+		line = <- tclmod.lines;
+	else{
+		iob=lookup_iob(argv[1]);
+		if (iob==nil)
+			return notify(0,sys->sprint(
+				"bad file identifier \"%s\"",argv[1]));
+		line=iob.gets('\n');
+	}
+	if (line==nil){ 			
+		set_eof(iob);
+		return nil;
+	}
+	return line[0:len line -1];
+}	
+
+do_seek(argv : array of string) : string {
+	iob : ref Iobuf;
+	if (len argv < 3 || len argv > 4)
+		return notify(1,"seek fileId offset ?origin?");
+	iob=lookup_iob(argv[1]);
+	if (iob==nil)
+		return notify(0,sys->sprint(
+				"bad file identifier \"%s\"",argv[1]));
+	flag := Sys->SEEKSTART;
+	if (len argv == 4) {
+		case argv[3] {
+			"SEEKSTART" =>
+				flag = Sys->SEEKSTART;
+			"SEEKRELA" =>
+				flag = Sys->SEEKRELA;
+			"SEEKEND" =>
+				flag = Sys->SEEKEND;
+			 * =>
+				return notify(0,sys->sprint(
+				"illegal access mode \"%s\"",
+					argv[3]));
+		}
+	}
+	iob.seek(big argv[2],flag);
+	return nil;
+}
+	
+do_open(argv : array of string) : string {
+	flag : int;
+	if (len argv==1 || len argv > 3)
+		return notify(1,
+			"open filename ?access? ?permissions?");
+	name:=argv[1];
+	if (len argv == 2)
+		flag = bufmod->OREAD;
+	else {
+		case argv[2] {
+			"OREAD" =>
+				flag = bufmod->OREAD;		
+			"OWRITE" =>		
+				flag = bufmod->OWRITE;		
+			"ORDWR"	=>	
+				flag = bufmod->ORDWR;
+			 * =>
+				return notify(0,sys->sprint(
+				"illegal access mode \"%s\"",
+					argv[2]));
+		}
+	}
+	iob := bufmod->open(name,flag);
+	if (iob==nil)
+		return notify(0,
+			sys->sprint("couldn't open \"%s\": No" +
+			      " such file or directory.",name));
+	for (i:=0;i<len name2fid;i++){
+		(iob2,name2,j):=name2fid[i];
+		if (iob2==nil){
+			name2fid[i]=(iob,"file"+string i,0);
+			return "file"+string i;
+		}
+	}
+	return notify(0,"File table full!");
+}
+	
+do_puts(argv : array of string) : string {
+	iob : ref Iobuf;
+	if (len argv==1 || len argv >4)
+		return notify(1,
+			"puts ?-nonewline? ?fileId? string");
+	if (argv[1]=="-nonewline"){
+		if (len argv==2)
+			return notify(1,
+			"puts ?-nonewline? ?fileId? string");
+		if (len argv==3)
+			sys->print("%s",argv[2]);
+		else{
+			iob=lookup_iob(argv[2]);	
+			if (iob==nil)
+				return notify(0,sys->sprint(
+				   "bad file identifier \"%s\"",
+					argv[2]));
+			iob.puts(argv[3]);
+			iob.flush();
+		}
+	} else {
+		if (len argv==2)
+			sys->print("%s\n",argv[1]);
+		if (len argv==3){
+			iob=lookup_iob(argv[1]);	
+			if (iob==nil)
+				return notify(0,sys->sprint(
+				   "bad file identifier \"%s\"",
+					argv[1]));
+			iob.puts(argv[2]+"\n");
+			iob.flush();
+		
+		}
+		if (len argv==4)
+			return notify(0,sys->sprint(
+			"bad argument \"%s\": should be"+
+			" \"nonewline\"",argv[3]));
+	}
+	return nil;
+}
+
+do_read(argv : array of string) : string {
+	iob : ref Iobuf;
+	line :string;
+	if (len argv<2 || len argv>3)
+		return notify(1,
+		  "read fileId ?numBytes?\" or \"read ?-nonewline? fileId");
+	if (argv[1]!="-nonewline"){
+		iob=lookup_iob(argv[1]);
+		if (iob==nil)
+			return notify(0,sys->sprint(
+				"bad file identifier \"%s\"", argv[1]));
+		if (len argv == 3){
+			buf := array[int argv[2]] of byte;
+			n:=iob.read(buf,len buf);
+			if (n==0){
+				set_eof(iob);
+				return nil;
+			}
+			return string buf[0:n];
+		}
+		line=iob.gets('\n');
+		if (line==nil) 
+			set_eof(iob);
+		else
+			line[len line]='\n';
+		return line;
+	}else{
+		iob=lookup_iob(argv[2]);
+		if (iob==nil)
+			return notify(0,sys->sprint(
+				"bad file identifier \"%s\"", argv[2]));
+		line=iob.gets('\n');
+		if (line==nil)
+			set_eof(iob);
+		return line;
+	}
+}
+
+
+
+		
+
+		
+
+
+notify(num : int,s : string) : string {
+	error=1;
+	case num{
+		1 =>
+			return sys->sprint(
+			"wrong # args: should be \"%s\"",s);
+		* =>
+			return s;
+	}
+}
+
+
+lookup_iob(s:string) : ref Iobuf{
+	iob : ref Iobuf;
+	name : string;
+	j : int;
+	for(i:=0;i<len name2fid;i++){
+		(iob,name,j)=name2fid[i];
+		if (name==s)
+			break;
+	}
+	if (i==len name2fid)
+		return nil;
+	return iob;
+}
+
+set_eof(iob : ref Iobuf) {
+	iob2 : ref Iobuf;
+	name : string;
+	j : int;
+	for(i:=0;i<len name2fid;i++){
+		(iob2,name,j)=name2fid[i];
+		if (iob==iob2)
+			break;
+	}
+	if (i!=len name2fid)
+		name2fid[i]=(iob,name,1);
+	return;
+}
+
--- /dev/null
+++ b/appl/lib/tcl_list.b
@@ -1,0 +1,335 @@
+implement TclLib;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "tk.m";
+include "bufio.m";
+	bufmod : Bufio;
+Iobuf : import bufmod;
+
+include "string.m";
+	str : String;
+
+include "tcl.m";
+include "tcllib.m";
+
+include "utils.m";
+	utils : Tcl_Utils;
+
+
+error : int;
+
+DEF,DEC,INT : con iota;
+valid_commands:= array[] of {
+		"concat" , 
+		"join" , 
+		"lindex" , 
+		"linsert" , 
+		"list" , 
+		"llength" ,
+		"lrange" , 
+		"lreplace" , 
+		"lsearch" , 
+		"lsort" , 
+		"split"
+};
+
+about() : array of string {
+	return valid_commands;
+}
+
+exec(tcl : ref Tcl_Core->TclData,argv : array of string) : (int,string) {
+	if (tcl.context==nil);
+	str = load String String->PATH;
+	sys = load Sys Sys->PATH;
+	utils = load Tcl_Utils Tcl_Utils->PATH;
+	if (str==nil || utils==nil)
+		return (1,"Can't load modules\n");
+	case argv[0] {
+		"concat" => 
+			return (error,do_concat(argv,0));					
+		"join" => 
+			return (error,do_join(argv));
+		"lindex" => 
+			return (error,do_lindex(argv));		 
+		"linsert" => 
+			return (error,do_linsert(argv));
+		"list" => 
+			return (error,do_concat(argv,1));
+		"llength" =>
+			return (error,do_llength(argv));					
+		"lrange" => 
+			return (error,do_lrange(argv));
+		"lreplace" => 
+			return (error,do_lreplace(argv));
+		"lsearch" => 
+			return (error,do_lsearch(argv));
+		"lsort" => 
+			return (error,do_lsort(argv));
+		"split" =>
+			return (error,do_split(argv));
+	}
+	return (1,nil);
+}
+
+spaces(s : string) : int{
+	if (s==nil) return 1;
+	for(i:=0;i<len s;i++)
+		if (s[i]==' ' || s[i]=='\t') return 1;
+	return 0;
+}
+	
+
+sort(a: array of string, key: int): array of string {
+	m: int;
+	n := len a;
+	for(m = n; m > 1; ) {
+		if(m < 5)
+			m = 1;
+		else
+			m = (5*m-1)/11;
+		for(i := n-m-1; i >= 0; i--) {
+			tmp := a[i];
+			for(j := i+m; j <= n-1 && greater(tmp, a[j], key); j += m)
+				a[j-m] = a[j];
+			a[j-m] = tmp;
+		}
+	}
+	return a;
+}
+
+greater(x, y: string, sortkey: int): int {
+	case (sortkey) {
+	DEF => return(x > y);
+	DEC => return(x < y);
+	INT => return(int x > int y);
+	}
+	return 0;
+}
+
+# from here on are the commands in alphabetical order...
+
+# turns an array into a string with spaces between the elements.
+# in braces is non-zero, the elements will be enclosed in braces.
+do_concat(argv : array of string, braces : int) : string {
+	retval :string;
+	retval=nil;
+	for(i:=1;i<len argv;i++){
+		flag:=0;
+		if (spaces(argv[i])) flag=1;
+		if (braces && flag) retval[len retval]='{';
+		retval += argv[i];
+		if (braces && flag) retval[len retval]='}';
+		retval[len retval]=' ';
+	}
+	if (retval!=nil)
+		retval=retval[0:len retval-1];
+	return retval;
+}
+
+do_join(argv : array of string) : string {
+	retval : string;
+	if (len argv ==1 || len argv >3)
+		return notify(1,"join list ?joinString?");
+	if (len argv == 2) 
+		return argv[1];
+	if (argv[1]==nil) return nil;
+	arr := utils->break_it(argv[1]);
+	for (i:=0;i<len arr;i++){
+		retval+=arr[i];
+		if (i!=len arr -1)
+			retval+=argv[2];
+	}
+	return retval;
+}
+
+do_lindex(argv : array of string) : string {
+	if (len argv != 3)
+		return notify(1,"lindex list index");
+	(num,rest):=str->toint(argv[2],10);
+	if (rest!=nil)
+		return notify(2,argv[2]);
+	arr:=utils->break_it(argv[1]);
+	if (num>=len arr)
+		return nil;
+	return arr[num];
+}
+
+do_linsert(argv : array of string) : string {
+	if (len argv < 4){
+		return notify(1,
+			"linsert list index element ?element ...?");
+	}
+	(num,rest):=str->toint(argv[2],10);
+	if (rest!=nil)
+		return notify(2,argv[2]);
+	arr:=utils->break_it(argv[1]);
+	narr := array[len arr + len argv - 2] of string;
+	narr[0]="do_concat";
+	if (num==0){
+		narr[1:]=argv[3:];
+		narr[len argv -2:]=arr[0:];
+	}else if (num>= len arr){
+		narr[1:]=arr[0:];
+		narr[len arr+1:]=argv[3:];
+	}else{
+		narr[1:]=arr[0:num];
+		narr[num+1:]=argv[3:];
+		narr[num+len argv -2:]=arr[num:];
+	}
+	return do_concat(narr,1);
+}
+
+do_llength(argv : array of string) : string {
+	if (len argv !=2){
+		return notify(1,"llength list");
+	}
+	arr:=utils->break_it(argv[1]);
+	return string len arr;
+}
+
+do_lrange(argv :array of string) : string {
+	beg,end : int;
+	rest : string;
+	if (len argv != 4)
+		return notify(1,"lrange list first last");
+	(beg,rest)=str->toint(argv[2],10);
+	if (rest!=nil)
+		return notify(2,argv[2]);
+	(end,rest)=str->toint(argv[3],10);
+	if (rest!=nil)
+		return notify(2,argv[3]);
+	if (beg <0) beg=0;
+	if (end < 0) return nil;
+	if (beg > end) return nil;
+	arr:=utils->break_it(argv[1]);
+	if (beg>len arr) return nil;
+	narr:=array[end-beg+2] of string;
+	narr[0]="do_concat";
+	narr[1:]=arr[beg:end+1];
+	return do_concat(narr,1);
+}
+
+do_lreplace(argv : array of string) : string {
+	beg,end : int;
+	rest : string;
+	if (len argv < 3)
+		return notify(1,"lreplace list "+
+			"first last ?element element ...?");
+	arr:=utils->break_it(argv[1]);
+	(beg,rest)=str->toint(argv[2],10);
+	if (rest!=nil)
+		return notify(2,argv[2]);
+	(end,rest)=str->toint(argv[3],10);
+	if (rest!=nil)
+		return notify(2,argv[3]);
+	if (beg <0) beg=0;
+	if (end < 0) return nil;
+	if (beg > end) 
+		return notify(0,
+		       "first index must not be greater than second");
+	if (beg>len arr) 
+		return notify(1,
+			"list doesn't contain element "+string beg);
+	narr:=array[len arr-(end-beg+1)+len argv - 3] of string;
+	narr[1:]=arr[0:beg];
+	narr[beg+1:]=argv[4:];
+	narr[beg+1+len argv-4:]=arr[end+1:];
+	narr[0]="do_concat";
+	return do_concat(narr,1);
+}
+
+do_lsearch(argv : array of string) : string {
+	if (len argv!=3) 
+		return notify(1,"lsearch ?mode? list pattern");
+	arr:=utils->break_it(argv[1]);
+	for(i:=0;i<len arr;i++)
+		if (arr[i]==argv[2])
+			return string i;
+	return "-1";
+}
+
+do_lsort(argv : array of string) : string {
+	lis : array of string;
+	key : int;
+	key=DEF;
+	if (len argv == 1) 
+		return notify(1,"lsort ?-ascii? ?-integer? ?-real?"+
+			" ?-increasing? ?-decreasing?"+
+			" ?-command string? list");
+	for(i:=1;i<len argv;i++)
+		if (argv[i][0]=='-')
+			case argv[i]{
+				"-decreasing" =>
+					key = DEC;
+				* =>
+					if (len argv != i+1)
+					return notify(0,sys->sprint(
+					"bad switch \"%s\": must be"+
+					" -ascii, -integer, -real, "+
+					"-increasing -decreasing, or"+
+					" -command" ,argv[i]));
+			}
+	lis=utils->break_it(argv[len argv-1]);
+	arr:=sort(lis,key);
+	narr:= array[len arr+1] of string;
+	narr[0]="list";
+	narr[1:]=arr[0:];
+	return do_concat(narr,1);
+}
+
+
+
+do_split(argv : array of string) : string {
+	arr := array[20] of string;
+	narr : array of string;
+	if (len argv ==1 || len argv>3)
+		return notify(1,"split string ?splitChars?");
+	if (len argv == 2)
+		return argv[1];
+	s:=argv[1];
+	if (s==nil) return nil;
+	if (argv[2]==nil){
+		arr=array[len s+1] of string;
+		for(i:=0;i<len s;i++)
+			arr[i+1][len arr[i+1]]=s[i];
+		arr[0]="do_concat";
+		return do_concat(arr,1);
+	}
+	i:=1;
+	while(s!=nil){
+		(piece,rest):=str->splitl(s,argv[2]);
+		arr[i]=piece;
+		if (len rest>1)
+			s=rest[1:];
+		if (len rest==1) 
+			s=nil;
+		i++;
+		if (i==len arr){
+			narr=array[i+10] of string;
+			narr[0:]=arr[0:];
+			arr=array[i+10] of string;
+			arr=narr;
+		}
+	}
+	narr = array[i] of string;
+	arr[0]="do_concat";
+	narr = arr[0:i+1];
+	return do_concat(narr,1);
+}
+
+notify(num : int,s : string) : string {
+	error=1;
+	case num{
+		1 =>
+			return sys->sprint(
+			"wrong # args: should be \"%s\"",s);		
+		2 =>
+			return sys->sprint(
+			"expected integer but got \"%s\"",s);
+		* =>
+			return s;
+	}
+}
+
--- /dev/null
+++ b/appl/lib/tcl_modhash.b
@@ -1,0 +1,114 @@
+implement Mod_Hashtab;
+
+include "sys.m";
+include "draw.m";
+include "tk.m";
+include "tcl.m";
+include "tcllib.m";
+
+include "utils.m";
+
+
+hashasu(key : string,n : int): int{
+        i, h : int;
+	h=0;
+	i=0;
+        while(i<len key){
+                h = 10*h + key[i];
+		h = h%n;
+		i++;
+	}
+        return h%n;
+}
+
+alloc(size : int) : ref MHash {
+	h : MHash;
+	t : list of H_link;
+	t=nil;
+	h.size= size;
+	h.tab = array[size]  of {* => t};
+	return ref h;
+}
+
+MHash.dump(h : self ref MHash) : string {
+	retval :string;
+	for (i:=0;i<h.size;i++){
+		tmp:=(h.tab)[i];
+		for(;tmp!=nil;tmp = tl tmp){
+			if ((hd tmp).name!=nil){
+				retval+=(hd tmp).name;
+				retval[len retval]=' ';
+			}
+		}
+	}
+	if (retval!=nil)
+		retval=retval[0:len retval-1];
+	return retval;
+}
+
+		
+
+MHash.insert(h : self ref MHash,name: string, val:TclLib) : int {
+	link : H_link;
+	hash,found : int;
+	nlist : list of H_link;
+	nlist=nil;
+	found=0;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link=hd tmp;
+		if (link.name==name){
+			found=1;
+			link.val = val;
+		}
+		nlist = link :: nlist;
+	}
+	if (!found){
+		link.name=name;
+		link.val=val;
+		(h.tab)[hash]= link :: (h.tab)[hash];
+	}else
+		(h.tab)[hash]=nlist;
+	return 1;
+}
+
+MHash.find(h : self ref MHash,name : string) : (int, TclLib){
+	hash,flag : int;
+	nlist : list of H_link;
+	retval : TclLib;
+	retval=nil;
+	flag=0;
+	nlist=nil;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if ((hd tmp).name==name){
+			flag = 1;
+			retval = (hd tmp).val;
+		}
+		nlist = link :: nlist;
+	}
+	(h.tab)[hash]=nlist;
+	return (flag,retval);
+}	
+
+MHash.delete(h : self ref MHash,name : string) : int {
+	hash,flag : int;
+	nlist : list of H_link;
+	flag=0;
+	nlist=nil;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if (link.name==name)
+			flag = 1;
+		else
+			nlist = link :: nlist;
+	}
+	(h.tab)[hash]=nlist;
+	return flag;
+}
+
--- /dev/null
+++ b/appl/lib/tcl_stack.b
@@ -1,0 +1,142 @@
+implement Tcl_Stack;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "tk.m";
+include "tcl.m";
+include "tcllib.m";
+include "utils.m";
+	htab: Str_Hashtab;
+	shtab: Sym_Hashtab;
+Hash: import htab;
+SHash: import shtab;
+
+sframe : adt {
+	simple : ref Hash;
+	assoc  : array of (ref Hash,string);
+	symtab : ref SHash;
+};
+
+stack := array[100] of sframe;
+curlevel : int;
+nlev : int;
+
+init() {
+	curlevel=-1;
+	nlev=-1;
+	htab = load Str_Hashtab Str_Hashtab->PATH;
+	shtab = load Sym_Hashtab Sym_Hashtab->PATH;
+	sys = load Sys Sys->PATH;
+	if (htab == nil){
+		sys->print("can't load Hashtab %r\n");
+		exit;
+	}
+	if (shtab == nil){
+		sys->print("can't load Sym_Hashtab %r\n");
+		exit;
+	}
+}
+
+newframe() : (ref Hash,array of (ref Hash,string),ref SHash) {
+	nv := htab->alloc(101);
+	av := array[100] of (ref Hash,string);
+	st := shtab->alloc(101);
+	#sys->print("New frame, curlevel is %d\n",curlevel);
+	push (nv,av,st);
+	return (nv,av,st);
+}
+
+level() : int {
+	return curlevel;
+}
+
+move(lev :int) : int {
+	if (lev <0 || lev>nlev)
+		return 0;
+	curlevel=lev;
+	return 1;
+}
+
+push(sv : ref Hash, av : array of (ref Hash,string), st :ref SHash){
+	curlevel++;
+	nlev++;
+	stack[curlevel].simple=sv;
+	stack[curlevel].assoc=av;
+	stack[curlevel].symtab=st;
+}
+
+pop() : (ref Hash,array of (ref Hash,string),ref SHash) {
+	s:=stack[curlevel].simple;
+	a:=stack[curlevel].assoc;
+	t:=stack[curlevel].symtab;
+	stack[curlevel].simple=nil;
+	stack[curlevel].assoc=nil;
+	stack[curlevel].symtab=nil;
+	curlevel--;
+	nlev--;
+	return (s,a,t);
+}
+
+examine(lev : int) : (ref Hash,array of (ref Hash,string),ref SHash) {
+	if (lev <0 || lev > nlev)
+		return (nil,nil,nil);
+	return (stack[lev].simple,stack[lev].assoc,stack[lev].symtab);
+}
+
+dump() {
+	for (i:=0;i<100;i++){
+		if (stack[i].simple!=nil){
+			sys->print("simple table at %d\n",i);
+			for (j:=0;j<101;j++)
+				if (stack[i].simple.tab[j]!=nil){
+					sys->print("\tH_link at %d\n",j);
+					l:=stack[i].simple.tab[j];
+					while(l!=nil){
+					    sys->print("\tname [%s], value [%s]\n",
+						(hd l).name,(hd l).val);
+					    l=tl l;
+					}
+				}
+		}
+		if (stack[i].assoc!=nil){
+			sys->print("assoc table at %d\n",i);
+			for(j:=0;j<100;j++){
+				(rh,s):=stack[i].assoc[j];
+				if (rh!=nil){
+					sys->print(
+					      "\tassoc array at %d, name %s\n",
+							j,s);
+					for (k:=0;k<101;k++)
+						if (rh.tab[k]!=nil){
+							sys->print(
+							 "\t\tH_link at %d\n",k);
+							l:=rh.tab[k];
+							while(l!=nil){
+					    			sys->print(
+						     "\t\tname [%s], value [%s]\n",
+						       (hd l).name,(hd l).val);
+					    			l=tl l;
+							}
+						}
+
+				}
+			}
+		}
+		if (stack[i].symtab!=nil){
+			sys->print("Symbol table at %d\n",i);
+			for (j:=0;j<101;j++)
+				if (stack[i].symtab.tab[j]!=nil){
+					sys->print("\tH_link at %d\n",j);
+					l:=stack[i].symtab.tab[j];
+					while(l!=nil){
+					    sys->print("\tname [%s], alias [%s], "+
+						"value [%d]\n",(hd l).name,
+							(hd l).alias,(hd l).val);
+					    l=tl l;
+					}
+				}
+		}
+	}
+}
+
--- /dev/null
+++ b/appl/lib/tcl_strhash.b
@@ -1,0 +1,116 @@
+implement Str_Hashtab;
+
+include "sys.m";
+include "draw.m";
+include "tk.m";
+include "tcl.m";
+include "tcllib.m";
+include "utils.m";
+
+
+hashasu(key : string,n : int): int{
+        i, h : int;
+	h=0;
+	i=0;
+        while(i<len key){
+                h = 10*h + key[i];
+		h = h%n;
+		i++;
+	}
+        return h%n;
+}
+
+alloc(size : int) : ref Hash {
+	h : Hash;
+	t : list of H_link;
+	t=nil;
+	h.size= size;
+	h.lsize=0;
+	h.tab = array[size]  of {* => t};
+	return ref h;
+}
+
+Hash.dump(h : self ref Hash) : string {
+	retval :string;
+	for (i:=0;i<h.size;i++){
+		tmp:=(h.tab)[i];
+		for(;tmp!=nil;tmp = tl tmp){
+			if ((hd tmp).name!=nil){
+				retval+=(hd tmp).name;
+				retval[len retval]=' ';
+			}
+		}
+	}
+	if (retval!=nil)
+		retval=retval[0:len retval-1];
+	return retval;
+}
+
+Hash.insert(h : self ref Hash,name,val: string) : int {
+	link : H_link;
+	hash,found : int;
+	nlist : list of H_link;
+	nlist=nil;
+	found=0;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link=hd tmp;
+		if (link.name==name){
+			found=1;
+			link.val = val;
+		}
+		nlist = link :: nlist;
+	}
+	if (!found){
+		h.lsize++;
+		link.name=name;
+		link.val=val;
+		(h.tab)[hash]= link :: (h.tab)[hash];
+	}else
+		(h.tab)[hash]=nlist;
+	return 1;
+}
+
+Hash.find(h : self ref Hash,name : string) : (int, string){
+	hash,flag : int;
+	nlist : list of H_link;
+	retval : string;
+	flag=0;
+	nlist=nil;
+	retval=nil;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if ((hd tmp).name==name){
+			flag = 1;
+			retval = (hd tmp).val;
+		}
+		nlist = link :: nlist;
+	}
+	(h.tab)[hash]=nlist;
+	return (flag,retval);
+}	
+
+Hash.delete(h : self ref Hash,name : string) : int {
+	hash,flag : int;
+	nlist : list of H_link;
+	retval : string;
+	flag=0;
+	nlist=nil;
+	retval=nil;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if (link.name==name){
+			flag = 1;
+			h.lsize--;
+		}else
+			nlist = link :: nlist;
+	}
+	(h.tab)[hash]=nlist;
+	return flag;
+}
+
--- /dev/null
+++ b/appl/lib/tcl_string.b
@@ -1,0 +1,246 @@
+implement TclLib;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "tk.m";
+include "bufio.m";
+	bufmod : Bufio;
+Iobuf : import bufmod;
+
+include "string.m";
+	str : String;
+include "tcl.m";
+include "tcllib.m";
+
+error : int;
+started : int;
+valid_commands:=array[] of {"format","string"};
+
+about() : array of string{
+	return valid_commands;
+}
+
+init(){
+	started=1;
+	sys=load Sys Sys->PATH;
+}
+
+exec(tcl : ref Tcl_Core->TclData,argv : array of string) : (int,string) {
+	if (tcl.context==nil);
+	if (!started) init();
+	error=0;
+	str=load String String->PATH;
+	if (str==nil)
+		return(1,"String module not loaded.");
+	if (len argv==1 && argv[0]=="string")
+		return (error,
+			notify(1,"string option arg ?arg ...?"));
+	case argv[0]{
+		"format" =>
+			return (error,do_format(argv));
+		"string" =>
+			return (error,do_string(argv));
+	}
+	return (1,nil);
+}
+
+
+do_string(argv : array of string) : string{
+	case argv[1]{
+		"compare" =>
+			if (len argv == 4){
+				i:= - (argv[2]<argv[3])+ (argv[2]>argv[3]);
+				return string i;
+			}
+			return notify(1,
+			     "string compare string1 string2");
+		"first" =>
+			return nil;
+		"last" =>
+			return nil;
+		"index" =>
+			if (len argv == 4){
+				if (len argv[2] > int argv[3])
+					return argv[2][int argv[3]:int argv[3]+1];
+				return nil;
+			}
+			return notify(1,
+			     "string index string charIndex");
+		"length" =>
+			if (len argv==3)
+				return string len argv[2];
+			return notify(1,"string length string");
+		"match" =>
+			return nil;
+		"range" =>
+			if (len argv==5){
+				end :int;
+				if (argv[4]=="end") 
+					end=len argv[2];
+				else
+					end=int argv[4];
+				if (end>len argv[2]) end=len argv[2];
+				beg:=int argv[3];
+				if (beg<0) beg=0;
+				if (beg>end)
+					return nil;
+				return argv[2][int argv[3]:end];
+			}
+			return notify(1,
+			     "string range string first last");
+		"tolower" =>
+			if (len argv==3)
+				return str->tolower(argv[2]);
+			return notify(1,"string tolower string");
+		"toupper" =>
+			if (len argv==3)
+				return str->tolower(argv[2]);
+			return notify(1,"string tolower string");
+		"trim" =>
+			return nil;
+		"trimleft" =>
+			return nil;
+		"trimright" =>
+			return nil;
+		"wordend" =>
+			return nil;
+		"wordstart" =>
+			return nil;
+	}
+	return nil;
+}
+
+do_format(argv : array of string) : string {
+	retval,num1,num2,rest,curfm : string;
+	i,j : int;
+	if (len argv==1)
+		return notify(1,
+			"format formatString ?arg arg ...?");
+	j=2;
+	i1:=-1;
+	i2:=-1;
+	(retval,rest)=str->splitl(argv[1],"%");
+	do {
+		(curfm,rest)=str->splitl(rest[1:],"%");
+		i=0;
+		num1="";
+		num2="";
+		if (curfm[i]=='-'){
+			num1[len num1]=curfm[i];
+			i++;
+		}
+		while(curfm[i]>='0' && curfm[i]<='9'){
+			num1[len num1]=curfm[i];
+			i++;
+		}
+		if (num1!="")
+			(i1,nil) = str->toint(num1,10);
+		if (curfm[i]=='.'){
+			i++;
+			while(curfm[i]>='0' && curfm[i]<='9'){
+				num2[len num2]=curfm[i];
+				i++;
+			}
+			(i2,nil) = str->toint(num2,10);
+		} else {
+			i2=i1;
+			i1=-1;
+		}
+		case curfm[i] {
+			's' =>
+				retval+=print_string(i1,i2,argv[j]);
+			'd' => 
+				retval+=print_int(i1,i2,argv[j]);
+			'f' =>
+				retval+=print_float(i1,i2,argv[j]);
+			'x' =>
+				retval+=print_hex(i1,i2,argv[j]);
+		}
+		j++;
+	} while (rest!=nil && j<len argv);
+	return retval;
+}
+
+notify(num : int,s : string) : string {
+	error=1;
+	case num{
+		1 =>
+			return sys->sprint(
+			"wrong # args: should be \"%s\"",s);
+		* =>
+			return s;
+	}
+}
+
+print_string(i1,i2 : int, s : string) : string {
+	retval : string;
+	if (i1==-1 && i2==-1)
+		retval=sys->sprint("%s",s);
+	if (i1==-1 && i2!=-1)
+		retval=sys->sprint("%*s",i1,s);
+	if (i1!=-1 && i2!=-1)
+		retval=sys->sprint("%*.*s",i1,i2,s);
+	if (i1!=-1 && i2==-1)
+		retval=sys->sprint("%.*s",i2,s);	
+	return retval;
+}
+
+print_int(i1,i2 : int, s : string) : string {
+	retval,ret2 : string;
+	n : int;
+	(num,nil):=str->toint(s,10);
+	width:=1;
+	i:=num;
+	while((i/=10)!= 0) width++;
+	if (i2 !=-1 && width<i2) width=i2;
+	for(i=0;i<width;i++)
+		retval[len retval]='0';
+	while(width!=0){
+		retval[width-1]=num%10+'0';
+		num/=10;
+		width--;
+	}
+	if (i1 !=-1 && i1>i){
+		for(n=0;n<i1-i;n++)
+			ret2[len ret2]=' ';
+		ret2+=retval;
+		retval=ret2;
+	}
+	return retval;
+}
+
+
+print_float(i1,i2 : int, s : string) : string {
+	r:= real s;
+	retval:=sys->sprint("%*.*f",i1,i2,r);
+	return retval;
+}
+
+print_hex(i1,i2 : int, s : string) : string {
+	retval,ret2 : string;
+	n : int;
+	(num,nil):=str->toint(s,10);
+	width:=1;
+	i:=num;
+	while((i/=16)!= 0) width++;
+	if (i2 !=-1 && width<i2) width=i2;
+	for(i=0;i<width;i++)
+		retval[len retval]='0';
+	while(width!=0){
+		n=num%16;
+		if (n>=0 && n<=9)
+			retval[width-1]=n+'0';
+		else
+			retval[width-1]=n+'a'-10;
+		num/=16;
+		width--;
+	}
+	if (i1 !=-1 && i1>i){
+		for(n=0;n<i1-i;n++)
+			ret2[len ret2]=' ';
+		ret2+=retval;
+		retval=ret2;
+	}
+	return retval;
+}
--- /dev/null
+++ b/appl/lib/tcl_symhash.b
@@ -1,0 +1,99 @@
+implement Sym_Hashtab;
+
+include "sys.m";
+include "draw.m";
+include "tk.m";
+include "tcl.m";
+include "tcllib.m";
+include "utils.m";
+
+
+hashasu(key : string,n : int): int{
+        i, h : int;
+	h=0;
+	i=0;
+        while(i<len key){
+                h = 10*h + key[i];
+		h = h%n;
+		i++;
+	}
+        return h%n;
+}
+
+alloc(size : int) : ref SHash {
+	h : SHash;
+	t : list of H_link;
+	t=nil;
+	h.size= size;
+	h.tab = array[size]  of {* => t};
+	return ref h;
+}
+
+
+SHash.insert(h : self ref SHash,name,alias: string,val:int) : int {
+	link : H_link;
+	hash,found : int;
+	nlist : list of H_link;
+	nlist=nil;
+	found=0;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link=hd tmp;
+		if (link.name==name){
+			found=1;
+			link.val = val;
+			link.alias = alias;
+		}
+		nlist = link :: nlist;
+	}
+	if (!found){
+		link.name=name;
+		link.val=val;
+		link.alias = alias;
+		(h.tab)[hash]= link :: (h.tab)[hash];
+	}else
+		(h.tab)[hash]=nlist;
+	return 1;
+}
+
+SHash.find(h : self ref SHash,name : string) : (int, int,string){
+	hash,flag : int;
+	nlist : list of H_link;
+	al : string;
+	retval:=0;
+	flag=0;
+	nlist=nil;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if ((hd tmp).name==name){
+			flag = 1;
+			retval = (hd tmp).val;
+			al = (hd tmp).alias;
+		}
+		nlist = link :: nlist;
+	}
+	(h.tab)[hash]=nlist;
+	return (flag,retval,al);
+}	
+
+SHash.delete(h : self ref SHash,name : string) : int {
+	hash,flag : int;
+	nlist : list of H_link;
+	flag=0;
+	nlist=nil;
+	hash = hashasu(name,h.size);
+	tmp:=(h.tab)[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if (link.name==name)
+			flag = 1;
+		else
+			nlist = link :: nlist;
+	}
+	(h.tab)[hash]=nlist;
+	return flag;
+}
+
--- /dev/null
+++ b/appl/lib/tcl_tk.b
@@ -1,0 +1,223 @@
+implement TclLib;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str : String;
+
+include "tk.m";
+	tk: Tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "tcl.m";
+
+include "tcllib.m";
+
+error,started : int;
+w_cfg := array[] of {
+	"pack .Wm_t -side top -fill x", 
+	"update",
+};
+
+tclmod : ref Tcl_Core->TclData;
+
+windows := array[100] of (string, ref Tk->Toplevel, chan of string);
+
+valid_commands:= array[] of {
+		"bind" , "bitmap" , "button" ,
+		"canvas" , "checkbutton" , "destroy" ,
+		"entry" , "focus", "frame" , "grab", "image" , "label" ,
+		"listbox" ,"lower", "menu" , "menubutton" ,
+		"pack" , "radiobutton" , "raise", "scale" ,
+		"scrollbar" , "text" , "update" ,
+		"toplevel" , "variable"
+};
+
+about() : array of string {
+	return valid_commands;
+}
+
+init() : string {
+	sys = load Sys Sys->PATH;
+	str = load String String->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient==nil || str==nil || tk==nil)
+		return "Not Initialised";
+	# set up Draw context
+	tkclient->init();
+	started=1;
+	return nil;
+}	
+
+exec(tcl : ref Tcl_Core->TclData,argv : array of string) : (int,string) {
+	retval : string;
+	retval="";
+	han,whan : ref Tk->Toplevel;
+	whan=nil;
+	msg : string;
+	c : chan of string;
+	msg=nil;
+	error=0;
+	tclmod=tcl;
+	if (!started) 
+		if (init()!=nil)
+			return (1,"Can't Initialise TK");
+	if (argv[0][0]!='.')
+		case argv[0] {
+			"destroy" =>
+				for (j:=1;j<len argv;j++){
+					(msg,han)=sweepthru(argv[j]);
+					if (msg==nil){
+						if (argv[j][0]=='.')
+							argv[j]=argv[j][1:];
+						for(i:=0;i<100;i++){
+							(retval,nil,c)=windows[i];
+							if (retval==argv[1]){
+								c <-= "exit";
+								break;
+							}
+						}
+					}
+					else
+						msg=tkcmd(whan,"destroy "+msg);
+				}
+				return (error,msg);	
+			"bind" or "bitmap" or "button" or
+			"canvas" or "checkbutton" or "entry" or 
+			"focus" or "frame" or "grab" or
+			"image" or "label" or "listbox" or "lower" or
+			"menu" or "menubutton" or "pack" or 
+			"radiobutton" or "raise" or "scale" or
+			"scrollbar" or "text" or "update" or 
+			"variable" =>
+					; # do nothing
+			"toplevel" =>
+				msg=do_toplevel(argv);
+				return (error,msg);
+			* =>
+				return (0,"Unknown");
+		}
+	# so it's a tk-command ... replace any -command with
+	# a send on the tcl channel.
+	if (argv[0]=="bind")
+		argv[3]="{send Tcl_Chan "+argv[3]+"}";
+	for (i:=0;i<len argv;i++){
+		(argv[i],han)=sweepthru(argv[i]);
+		if (han!=nil) whan=han;
+		if (argv[i]!="-tcl")
+			retval+=argv[i];
+		if (i+1<len argv &&
+			(argv[i]=="-command" || argv[i]=="-yscrollcommand"
+			|| argv[i]=="-tcl" || argv[i]=="-xscrollcommand"))
+			argv[i+1]="{send Tcl_Chan "+argv[i+1]+"}";
+		if (argv[i]!="-tcl")
+			retval[len retval]=' ';
+	}
+	retval=retval[0:len retval -1];
+	if (tclmod.debug==1)
+		sys->print("Sending [%s] to tkcmd.\n",retval);
+	msg=tkcmd(whan,retval);
+	if (msg!="" && msg[0]=='!')
+		error=1;
+	return (error,msg);
+}
+
+	
+sweepthru(s: string) : (string,ref Tk->Toplevel) {
+	han : ref Tk->Toplevel;
+	ret : string;
+	if (s=="" || s=="." || s[0]!='.')
+		return (s,nil);
+	(wname,rest):=str->splitl(s[1:],".");
+	for (i:=0;i<len windows;i++){
+		(ret,han,nil)=windows[i];
+		if (ret==wname) 
+			break;
+	}
+	if (i==len windows)
+		return (s,nil);
+	return (rest,han);
+}
+		
+do_toplevel(argv : array of string): string
+{
+	name : string;
+	whan : ref Tk->Toplevel;
+	if (len argv!=2)
+		return notify(1,"toplevel name");
+	if (argv[1][0]=='.')
+		argv[1]=argv[1][1:];
+	for(i:=0;i<len windows;i++){
+		(name,whan,nil)=windows[i];
+		if(whan==nil || name==argv[1])
+			break;
+	}
+	if (i==len windows)
+		return notify(0,"Too many top level windows");
+	if (name==argv[1])
+		return notify(0,argv[1]+" is already a window name in use.");
+
+	(top, menubut) := tkclient->toplevel(tclmod.context, "", argv[1], Tkclient->Appl);
+	whan = top;
+
+	windows[i]=(argv[1],whan,menubut);
+	if (tclmod.debug==1)
+		sys->print("creating window %d, name %s, handle %ux\n",i,argv[1],whan);
+	cmd := chan of string;
+	tk->namechan(whan, cmd, argv[1]);
+	for(i=0; i<len w_cfg; i++)
+		tk->cmd(whan, w_cfg[i]);
+	tkclient->onscreen(whan, nil);
+	tkclient->startinput(whan, "kbd"::"ptr"::nil);
+	stop := chan of int;
+	spawn tkclient->handler(whan, stop);
+	spawn menulisten(whan,menubut, stop);
+	return nil;
+}
+
+
+menulisten(t : ref Tk->Toplevel, menubut : chan of string, stop: chan of int) {
+	for(;;) alt {
+	menu := <-menubut =>
+		if(menu == "exit"){
+			for(i:=0;i<len windows;i++){
+			(name,whan,nil):=windows[i];
+				if(whan==t)
+				break;
+			}
+			if (i!=len windows)
+				windows[i]=("",nil,nil);
+			stop <-= 1;
+			exit;
+		}
+		tkclient->wmctl(t, menu);
+	}
+}
+
+tkcmd(t : ref Tk->Toplevel, cmd: string): string {
+	if (len cmd ==0 || tclmod.top==nil) return nil;
+	if (t==nil){
+		 t=tclmod.top;
+		#sys->print("Sending to WishPad\n");
+	}
+	s := tk->cmd(t, cmd);
+	tk->cmd(t,"update");
+	return s;
+}
+
+notify(num : int,s : string) : string {
+	error=1;
+	case num{
+		1 =>
+			return sys->sprint(
+			"wrong # args: should be \"%s\"",s);
+		* =>
+			return s;
+	}
+}
--- /dev/null
+++ b/appl/lib/tcl_utils.b
@@ -1,0 +1,61 @@
+implement Tcl_Utils;
+include "sys.m";
+include "draw.m";
+include "tk.m";
+include "tcl.m";
+include "tcllib.m";
+include "utils.m";
+
+break_it(s : string) : array of string {
+	argv:= array[200] of string;
+	buf : string;
+	argc := 0;
+	nc := 0;
+   outer:
+	for (i := 0; i < len s ; ) {
+		case int s[i] {
+		' ' or '\t' or '\n' =>
+			if (nc > 0) {	# end of a word?
+				argv[argc++] = buf;
+				buf = nil;
+				nc = 0;
+			}
+			i++;
+		'{' =>
+			if (s[i+1]=='}'){
+				argv[argc++] = nil;
+				buf = nil;
+				nc = 0;	
+				i+=2;
+			}else{
+				nbra := 1;
+				for (i++; i < len s; i++) {
+					if (s[i] == '{')
+						nbra++;
+					else if (s[i] == '}') {
+						nbra--;
+					if (nbra == 0) {
+							i++;
+							continue outer;
+						}
+					}
+					buf[nc++] = s[i];
+				}
+			}	
+		* =>
+			buf[nc++] = s[i++];
+		}
+	}
+	if (nc > 0)	# fix up last word if present
+		argv[argc++] = buf;
+	ret := array[argc] of string;
+	ret[0:] = argv[0:argc];
+	return ret;
+}
+
+arr_resize(argv : array of string) : array of string {
+	ret := array[len argv + 25] of string;
+	ret[0:]=argv;
+	return ret;
+}
+
--- /dev/null
+++ b/appl/lib/tftp.b
@@ -1,0 +1,174 @@
+implement Tftp;
+
+include "sys.m";
+	sys: Sys;
+
+include "dial.m";
+	dial: Dial;
+
+include "tftp.m";
+
+Maxretry: con 5;	# retries per block
+Maxblock: con 512;	# protocol's usual maximum data block size
+Tftphdrlen: con 4;
+Read, Write, Data, Ack, Error: con 1+iota;	# tftp opcode
+
+progress: int;
+
+put2(buf: array of byte, o: int, val: int)
+{
+	buf[o] = byte (val >> 8);
+	buf[o+1] = byte val;
+}
+
+get2(buf: array of byte, o: int): int
+{
+	return (int buf[o] << 8) | int buf[o+1];
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if(fd == nil)
+		return;
+
+	msg := array of byte "kill";
+	sys->write(fd, msg, len msg);
+}
+
+timeoutproc(c: chan of int, howlong: int)
+{
+	c <-= sys->pctl(0, nil);
+	sys->sleep(howlong);
+	c <-= 1;
+}
+
+tpid := -1;
+
+timeoutcancel()
+{
+	if(tpid >= 0) {
+		kill(tpid);
+		tpid = -1;
+	}
+}
+
+timeoutstart(howlong: int): chan of int
+{
+	timeoutcancel();
+	tc := chan of int;
+	spawn timeoutproc(tc, howlong);
+	tpid = <-tc;
+	return tc;
+}
+
+init(p: int)
+{
+	sys = load Sys Sys->PATH;
+	dial = load Dial Dial->PATH;
+	progress = p;
+}
+
+reader(pidc: chan of int, fd: ref Sys->FD, bc: chan of array of byte)
+{
+	pid := sys->pctl(0, nil);
+	pidc <-= pid;
+	buf := array[Tftphdrlen + Maxblock] of byte;
+	for(;;){
+		n := sys->read(fd, buf, len buf);
+		bc <-= buf[0 : n];
+	}
+}
+
+receive(host: string, filename: string, fd: ref Sys->FD): string
+{
+	rbuf: array of byte;
+	
+	conn := dial->dial(dial->netmkaddr(host, "udp", "69"), nil);
+	if(conn == nil)
+		return sys->sprint("can't dial %s: %r", host);
+	buf := array[Tftphdrlen + Maxblock] of byte;
+	i := 0;
+	put2(buf, i, Read);
+	i += 2;
+	a := array of byte filename;
+	buf[i:] = a;
+	i += len a;
+	buf[i++] = byte 0;
+	mode := array of byte "binary";
+	buf[i:] = mode;
+	i += len mode;
+	buf[i++] = byte 0;
+	pidc := chan of int;
+	bc := chan of array of byte;
+	spawn reader(pidc, conn.dfd, bc);
+	tftppid := <-pidc;
+	lastblock := 0;
+	for(;;) {
+	  Retry:
+		for(count := 0;; count++) {
+			if(count >= Maxretry){
+				kill(tftppid);
+				return sys->sprint("tftp timeout");
+			}
+
+			# (re)send request/ack
+			if(sys->write(conn.dfd, buf, i) < 0) {
+				kill(tftppid);
+				return sys->sprint( "error writing %s/data: %r", conn.dir);
+			}
+	
+			# wait for next block
+			mtc := timeoutstart(3000);
+			for(;;){
+				alt {
+				<-mtc =>
+					if(progress)
+						sys->print("T");
+					continue Retry;
+				rbuf = <-bc =>
+					if(len rbuf < Tftphdrlen)
+						break;
+					op := get2(rbuf, 0);
+					case op {
+					Data =>
+						block := get2(rbuf, 2);
+						if(block == lastblock + 1) {
+							timeoutcancel();
+							break Retry;
+						}else if(progress)
+							sys->print("S");
+					Error =>
+						timeoutcancel();
+						kill(tftppid);
+						return sys->sprint("server error %d: %s", get2(rbuf, 2), string rbuf[4:]);
+					* =>
+						timeoutcancel();
+						kill(tftppid);
+						return sys->sprint("phase error op=%d", op);
+					}
+				}
+			}
+		}
+		n := len rbuf;
+		# copy the data somewhere
+		if(sys->write(fd, rbuf[Tftphdrlen:], n - Tftphdrlen) < 0) {
+			kill(tftppid);
+			return sys->sprint("writing destination: %r");
+		}
+		lastblock++;
+		if(progress && lastblock % 25 == 0)
+			sys->print(".");
+		if(n < Maxblock + Tftphdrlen) {
+			if(progress)
+				sys->print("\n");
+			break;
+		}
+
+		# send an ack
+		put2(buf, 0, Ack);
+		put2(buf, 2, lastblock);
+	}
+	kill(tftppid);
+	return nil;
+}
--- /dev/null
+++ b/appl/lib/timers.b
@@ -1,0 +1,99 @@
+implement Timers;
+
+include "sys.m";
+	sys:	Sys;
+
+include "timers.m";
+
+timerin: chan of ref Timer;
+
+init(minms: int): int
+{
+	sys = load Sys Sys->PATH;
+	timerin = chan[20] of ref Timer;
+	if(minms <= 0)
+		minms = 1;
+	pid := chan of int;
+	spawn timeproc(timerin, minms, pid);
+	return <-pid;
+}
+
+shutdown()
+{
+	if(timerin != nil)
+		timerin <-= nil;
+}	
+
+Timer.start(dt: int): ref Timer
+{
+	t := ref Timer(dt, chan[1] of int);
+	timerin <-= t;
+	return t;
+}
+
+Timer.stop(t: self ref Timer)
+{
+	# this is safe, because only Timer.stop sets t.timeout and timeproc only fetches it
+	t.timeout = nil;
+}
+			
+timeproc(req: chan of ref Timer, msec: int, pid: chan of int)
+{
+	pending: list of ref Timer;
+
+	pid <-= sys->pctl(Sys->NEWFD|Sys->NEWNS|Sys->NEWENV, nil);	# same pgrp
+	old := sys->millisec();
+Work:
+	for(;;){
+		if(pending == nil){
+			if((t := <-req) == nil)
+				break Work;
+			pending = t :: pending;
+			old = sys->millisec();
+		}else{
+			# check quickly for new requests
+		Check:
+			for(;;) alt{
+			t := <-req =>
+				if(t == nil)
+					break Work;
+				pending = t :: pending;
+			* =>
+				break Check;
+			}
+		}
+		sys->sleep(msec);
+		new := sys->millisec();
+		dt := new-old;
+		old = new;
+		if(dt < 0)
+			continue;	# millisec counter wrapped
+		ticked := 0;
+		for(l := pending; l != nil; l = tl l)
+			if(((hd l).dt -= dt) <= 0)
+				ticked = 1;
+		if(ticked){
+			l = pending;
+			pending = nil;
+			for(; l != nil; l = tl l){
+				t := hd l;
+				if(t.dt > 0 || !notify(t))
+					pending = t :: pending;
+			}
+		}
+	}
+	# shut down: attempt to clear pending requests
+	for(; pending != nil; pending = tl pending)
+		notify(hd pending);
+}
+
+notify(t: ref Timer): int
+{
+	# copy to c to avoid race with Timer.stop
+	if((c := t.timeout) == nil)
+		return 1;	# cancelled; consider it done
+	alt{
+	c <-= 1 => return 1;
+	* => return 0;
+	}
+}
--- /dev/null
+++ b/appl/lib/titlebar.b
@@ -1,0 +1,111 @@
+implement Titlebar;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "titlebar.m";
+
+title_cfg := array[] of {
+	"frame .Wm_t -bg #aaaaaa -borderwidth 1",
+	"label .Wm_t.title -anchor w -bg #aaaaaa -fg white",
+	"button .Wm_t.e -bitmap exit.bit -command {send wm_title exit} -takefocus 0",
+	"pack .Wm_t.e -side right",
+	"bind .Wm_t <Button-1> {send wm_title move %X %Y}",
+	"bind .Wm_t <Double-Button-1> {send wm_title lower .}",
+	"bind .Wm_t <Motion-Button-1> {}",
+	"bind .Wm_t <Motion> {}",
+	"bind .Wm_t.title <Button-1> {send wm_title move %X %Y}",
+	"bind .Wm_t.title <Double-Button-1> {send wm_title lower .}",
+	"bind .Wm_t.title <Motion-Button-1> {}",
+	"bind .Wm_t.title <Motion> {}",
+	"bind . <FocusIn> {.Wm_t configure -bg blue;"+
+		".Wm_t.title configure -bg blue;update}",
+	"bind . <FocusOut> {.Wm_t configure -bg #aaaaaa;"+
+		".Wm_t.title configure -bg #aaaaaa;update}",
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+}
+
+new(top: ref Tk->Toplevel, buts: int): chan of string
+{
+	ctl := chan of string;
+	tk->namechan(top, ctl, "wm_title");
+
+	if(buts & Plain)
+		return ctl;
+
+	for(i := 0; i < len title_cfg; i++)
+		cmd(top, title_cfg[i]);
+
+	if(buts & OK)
+		cmd(top, "button .Wm_t.ok -bitmap ok.bit"+
+			" -command {send wm_title ok} -takefocus 0; pack .Wm_t.ok -side right");
+
+	if(buts & Hide)
+		cmd(top, "button .Wm_t.top -bitmap task.bit"+
+			" -command {send wm_title task} -takefocus 0; pack .Wm_t.top -side right");
+
+	if(buts & Resize)
+		cmd(top, "button .Wm_t.m -bitmap maxf.bit"+
+			" -command {send wm_title size} -takefocus 0; pack .Wm_t.m -side right");
+
+	if(buts & Help)
+		cmd(top, "button .Wm_t.h -bitmap help.bit"+
+			" -command {send wm_title help} -takefocus 0; pack .Wm_t.h -side right");
+
+	# pack the title last so it gets clipped first
+	cmd(top, "pack .Wm_t.title -side left");
+	cmd(top, "pack .Wm_t -fill x");
+
+	return ctl;
+}
+
+title(top: ref Tk->Toplevel): string
+{
+	if(tk->cmd(top, "winfo class .Wm_t.title")[0] != '!')
+		return cmd(top, ".Wm_t.title cget -text");
+	return nil;
+}
+	
+settitle(top: ref Tk->Toplevel, t: string): string
+{
+	s := title(top);
+	tk->cmd(top, ".Wm_t.title configure -text '" + t);
+	return s;
+}
+
+sendctl(top: ref Tk->Toplevel, c: string)
+{
+	cmd(top, "send wm_title " + c);
+}
+
+minsize(top: ref Tk->Toplevel): Point
+{
+	buts := array[] of {"e", "ok", "top", "m", "h"};
+	r := tk->rect(top, ".", Tk->Border);
+	r.min.x = r.max.x;
+	r.max.y = r.min.y;
+	for(i := 0; i < len  buts; i++){
+		br := tk->rect(top, ".Wm_t." + buts[i], Tk->Border);
+		if(br.dx() > 0)
+			r = r.combine(br);
+	}
+	r.max.x += tk->rect(top, ".Wm_t." + buts[0], Tk->Border).dx();
+	return r.size();
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "wmclient: tk error %s on '%s'\n", e, s);
+	return e;
+}
--- /dev/null
+++ b/appl/lib/tkclient.b
@@ -1,0 +1,249 @@
+implement Tkclient;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Image, Screen, Rect, Point, Pointer, Wmcontext, Context: import draw;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "wmlib.m";
+	wmlib: Wmlib;
+	qword, splitqword, s2r: import wmlib;
+include "titlebar.m";
+	titlebar: Titlebar;
+include "tkclient.m";
+
+Background: con int 16r777777FF;		# should be drawn over immediately, but just in case...
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	wmlib = load Wmlib Wmlib->PATH;
+	if(wmlib == nil){
+		sys->fprint(sys->fildes(2), "tkclient: cannot load %s: %r\n", Wmlib->PATH);
+		raise "fail:bad module";
+	}
+	wmlib->init();
+	titlebar = load Titlebar Titlebar->PATH;
+	if(titlebar == nil){
+		sys->fprint(sys->fildes(2), "tkclient: cannot load %s: %r\n", Titlebar->PATH);
+		raise "fail:bad module";
+	}
+	titlebar->init();
+}
+
+makedrawcontext(): ref Draw->Context
+{
+	return wmlib->makedrawcontext();
+}
+
+toplevel(ctxt: ref Draw->Context, topconfig: string, title: string, buts: int): (ref Tk->Toplevel, chan of string)
+{
+	wm := wmlib->connect(ctxt);
+	opts := "";
+	if((buts & Plain) == 0)
+		opts = "-borderwidth 1 -relief raised ";
+	top := tk->toplevel(wm.ctxt.display, opts+topconfig);
+	if (top == nil) {
+		sys->fprint(sys->fildes(2), "wmlib: window creation failed (top %ux, i %ux)\n", top, top.image);
+		raise "fail:window creation failed";
+	}
+	top.ctxt = wm;
+	readscreenrect(top);
+	c := titlebar->new(top, buts);
+	titlebar->settitle(top, title);
+	return (top, c);
+}
+
+readscreenrect(top: ref Tk->Toplevel)
+{
+	if((fd := sys->open("/chan/wmrect", Sys->OREAD)) != nil){
+		buf := array[12*4] of byte;
+		n := sys->read(fd, buf, len buf);
+		if(n > 0)
+			(top.screenr, nil) = s2r(string buf[0:n], 0);
+	}
+}
+
+onscreen(top: ref Tk->Toplevel, how: string)
+{
+	if(how == nil)
+		how = "place";
+	wmctl(top, sys->sprint("!reshape . -1 %s %q",
+			r2s(tk->rect(top, ".", Tk->Border|Tk->Required)), how));
+}
+
+startinput(top: ref Tk->Toplevel, devs: list of string)
+{
+	for(; devs != nil; devs = tl devs)
+		wmctl(top, sys->sprint("start %q", hd devs));
+}
+
+r2s(r: Rect): string
+{
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+# commands originating both from tkclient and wm (via ctl)
+wmctl(top: ref Tk->Toplevel, req: string): string
+{
+#sys->print("wmctl %s\n", req);
+	(c, next) := qword(req, 0);
+	case c {
+	"exit" =>
+		sys->fprint(sys->open("/prog/" + string sys->pctl(0, nil) + "/ctl", Sys->OWRITE), "killgrp");
+		exit;
+	# old-style requests: pass them back around in proper form.
+	"move" =>
+		# move x y
+		titlebar->sendctl(top, "!move . -1 " + req[next:]);
+	"size" =>
+		minsz := titlebar->minsize(top);
+		titlebar->sendctl(top, "!size . -1 " + string minsz.x + " " + string minsz.y);
+	"ok" or
+	"help" =>
+		;
+	"rect" =>
+		r: Rect;
+		(c, next) = qword(req, next);
+		r.min.x = int c;
+		(c, next) = qword(req, next);
+		r.min.y = int c;
+		(c, next) = qword(req, next);
+		r.max.x = int c;
+		(c, next) = qword(req, next);
+		r.max.y = int c;
+		top.screenr = r;
+	"haskbdfocus" =>
+		in := int qword(req, next).t0 != 0;
+		cmd(top, "focus -global " + string in);
+		cmd(top, "update");
+	"task" =>
+		(r, nil) := splitqword(req, next);
+		if(r.t0 == r.t1)
+			req = sys->sprint("task %q", cmd(top, ".Wm_t.title cget -text"));
+		if(wmreq(top, c, req, next) == nil)
+			cmd(top, ". unmap; update");
+	"untask" =>
+		cmd(top, ". map; update");
+		return wmreq(top, c, req, next);
+	* =>
+		return wmreq(top, c, req, next);
+	}
+	return nil;
+}
+
+wmreq(top: ref Tk->Toplevel, c, req: string, e: int): string
+{
+	err := wmreq1(top, c, req, e);
+#	if(err != nil)
+#		sys->fprint(sys->fildes(2), "tkclient: request %#q failed: %s\n", req, err);
+	return err;
+}
+
+wmreq1(top: ref Tk->Toplevel, c, req: string, e: int): string
+{
+	name, reqid: string;
+	if(req != nil && req[0] == '!'){
+		(name, e) = qword(req, e);
+		(reqid, e) = qword(req, e);
+		if(name == nil || reqid == nil)
+			return "bad arg count";
+	}
+	if(top.ctxt.connfd != nil){
+		if(sys->fprint(top.ctxt.connfd, "%s", req) == -1)
+			return sys->sprint("%r");
+		if(req[0] == '!')
+			recvimage(top, name, reqid);
+		return nil;
+	}
+	if(req[0] != '!'){
+		(nil, nil, err) := wmlib->wmctl(top.ctxt, req);
+		return err;
+	}
+	# if there's no window manager, then we create a screen on the
+	# display image. there's nowhere to find the screen again except
+	# through the toplevel's image. that means that you can't create a
+	# menu without mapping a toplevel, and if you manage to unmap
+	# the toplevel without unmapping the menu, you'll have two
+	# screens on the same display image
+	# in the image, so
+	if(c != "!reshape")
+		return "unknown request";
+	i: ref Image;
+	if(top.image == nil){
+		if(name != ".")
+			return "screen not available";
+		di := top.display.image;
+		screen := Screen.allocate(di, top.display.color(Background), 0);
+		di.draw(di.r, screen.fill, nil, screen.fill.r.min);
+		i = screen.newwindow(di.r, Draw->Refbackup, Draw->Nofill);
+	}else{
+		if(name == ".")
+			i = top.image;
+		else
+			i = top.image.screen.newwindow(s2r(req, e).t0, Draw->Refbackup, Draw->Red);
+	}
+	tk->putimage(top, name+" "+reqid, i, nil);
+	return nil;
+}
+
+recvimage(top: ref Tk->Toplevel, name, reqid: string)
+{
+	i := <-top.ctxt.images;
+	if(i == nil){
+		cmd(top, name + " suspend");
+		i = <-top.ctxt.images;
+	}
+	tk->putimage(top, name+" "+reqid, i, nil);
+}
+
+settitle(top: ref Tk->Toplevel, name: string): string
+{
+	return titlebar->settitle(top, name);
+}
+
+handler(top: ref Tk->Toplevel, stop: chan of int)
+{
+	ctxt := top.ctxt;
+	if(stop == nil)
+		stop = chan of int;
+	for(;;)alt{
+	c := <-ctxt.kbd =>
+		tk->keyboard(top, c);
+	p := <-ctxt.ptr =>
+		tk->pointer(top, *p);
+	c := <-ctxt.ctl or
+	c = <-top.wreq =>
+		wmctl(top, c);
+	<-stop =>
+		exit;
+	}
+}
+
+snarfget(): string
+{
+	return wmlib->snarfget();
+}
+
+snarfput(buf: string)
+{
+	return wmlib->snarfput(buf);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "tkclient: tk error %s on '%s'\n", e, s);
+	return e;
+}
+
--- /dev/null
+++ b/appl/lib/translate.b
@@ -1,0 +1,248 @@
+implement Translate;
+
+#
+# prototype string translation for natural language substitutions
+#
+# Copyright © 2000 Vita Nuova Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys:	Sys;
+
+include "bufio.m";
+
+include "translate.m";
+
+NTEXT: con 131;	# prime
+NNOTE: con 37;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+opendict(file: string): (ref Dict, string)
+{
+	d := Dict.new();
+	return (d, d.add(file));
+}
+
+
+opendicts(files: list of string): (ref Dict, string)
+{
+	d := Dict.new();
+	err: string;
+	for(; files != nil; files = tl files){
+		e := d.add(hd files);
+		if(e != nil){
+			if(err != nil)
+				err += "; ";
+			err += (hd files)+":"+e;
+		}
+	}
+	return (d, err);
+}
+
+Dict.new(): ref Dict
+{
+	d := ref Dict;
+	d.texts = array[NTEXT] of list of ref Phrase;
+	d.notes = array[NNOTE] of list of ref Phrase;
+	return d;
+}
+
+Dict.xlate(d: self ref Dict, text: string): string
+{
+	return d.xlaten(text, nil);
+}
+
+Dict.xlaten(d: self ref Dict, text: string, note: string): string
+{
+	nnote := 0;
+	if(note != nil){
+		pnote := look(d.notes, note);
+		if(pnote != nil)
+			nnote = pnote.n + 1;
+	}
+	(h, code) := hash(text, len d.texts);
+	for(l := d.texts[h]; l != nil; l = tl l){
+		p := hd l;
+		if(p.hash == code && p.key == text && p.note == nnote)
+			return p.text;
+	}
+	return text;
+}
+
+mkdictname(locale, app: string): string
+{
+	if(locale == nil || locale == "default")
+		return "/locale/dict/"+app;	# looks better
+	return "/locale/"+locale+"/dict/"+app;
+}
+
+#
+# eventually could load a compiled version of the tables
+# (allows some consistency checking, etc)
+#
+Dict.add(d: self ref Dict, file: string): string
+{
+	bufio := load Bufio Bufio->PATH;
+	if(bufio == nil)
+		return "can't load Bufio";
+	fd := bufio->open(file, Sys->OREAD);
+	if(fd == nil)
+		return sys->sprint("%r");
+	ntext := 0;
+	nnote := 0;
+	errs: string;
+	for(lineno := 1; (line := bufio->fd.gets('\n')) != nil; lineno++){
+		if(line[0] == '#' || line[0] == '\n')
+			continue;
+		(key, note, text, err) := parseline(line);
+		if(err != nil){
+			if(errs != nil)
+				errs += ",";
+			errs += string lineno+":"+err;
+		}
+		pkey := look(d.texts, key);
+		if(pkey != nil)
+			key = pkey.key;		# share key strings (useful with notes)
+		pkey = insert(d.texts, key);
+		if(note != nil){
+			pnote := look(d.notes, note);
+			if(pnote == nil){
+				pnote = insert(d.notes, note);
+				pnote.n = nnote++;
+			}
+			pkey.note = pnote.n+1;
+		}
+		pkey.text = text;
+		pkey.n = ntext++;
+	}
+	return errs;
+}
+
+parseline(line: string): (string, string, string, string)
+{
+	note, text: string;
+
+	(key, i) := quoted(line, 0);
+	if(i < 0)
+		return (nil, nil, nil, "bad key field");
+	i = skipwhite(line, i);
+	if(i < len line && line[i] == '('){
+		(note, i) = delimited(line, i+1, ')');
+		if(note == nil)
+			return (nil, nil, nil, "bad note syntax");
+	}
+	i = skipwhite(line, i);
+	if(i >= len line)
+		return (key, note, key, nil);	# identity
+	if(line[i] != '=')
+		return (nil, nil, nil, "missing/misplaced '='");
+	(text, i) = quoted(line, i+1);
+	if(i < 0)
+		return (nil, nil, nil, "missing translation");
+	return (key, note, text, nil);
+}
+
+quoted(s: string, i: int): (string, int)
+{
+	i = skipwhite(s, i);
+	if(i >= len s || (qc := s[i]) != '"' && qc != '\'')
+		return (nil, -1);
+	return delimited(s, i+1, qc);
+}
+
+delimited(s: string, i: int, qc: int): (string, int)
+{
+	o := "";
+	b := i;
+	for(; i < len s; i++){
+		c := s[i];
+		if(c == qc)
+			return (o, i+1);
+		if(c == '\\' && i+1 < len s){
+			i++;
+			c = s[i];
+			case c {
+			'n' =>	c = '\n';
+			'r' =>	c = '\r';
+			't' =>	c = '\t';
+			'b' => c = '\b';
+			'a' => c = '\a';
+			'v' => c = '\v';
+			'u' =>
+				(c, i)  = hex2c(s, i + 1);
+				i--;
+			'0' => c = '\0';
+			* => ;
+			}
+		}
+		o[len o] = c;
+	}
+	return (nil, -1);
+}
+
+hex2c(s: string, i: int): (int, int)
+{
+	x := 0;
+	for (j := i; j < i + 4; j++) {
+		if (j >= len s)
+			return (Sys->UTFerror, j);
+		c := s[j];
+		if (c >= '0' && c <= '9')
+			c = c - '0';
+		else if (c >= 'a' && c <= 'f')
+			c = c - 'a' + 10;
+		else if (c >= 'A' && c <= 'F')
+			c = c - 'A' + 10;
+		else
+			return (Sys->UTFerror, j);
+		x = (x * 16) + c;
+	}
+	return (x, j);
+}
+
+skipwhite(s: string, i: int): int
+{
+	for(; i<len s && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n'); i++)
+		;
+	return i;
+}
+
+look(tab: array of list of ref Phrase, key: string): ref Phrase
+{
+	(h, code) := hash(key, len tab);
+	for(l := tab[h]; l != nil; l = tl l){
+		p := hd l;
+		if(p.hash == code && p.key == key)
+			return p;
+	}
+	return nil;
+}
+
+insert(tab: array of list of ref Phrase, key: string): ref Phrase
+{
+	(h, code) := hash(key, len tab);
+	p := ref Phrase;
+	p.n = 0;
+	p.note = 0;
+	p.key = key;
+	p.hash = code;
+	#sys->print("%s = %ux [%d]\n", key, code, h);
+	tab[h] = p :: tab[h];
+	return p;
+}
+
+# hashpjw from aho & ullman
+hash(s: string, n: int): (int, int)
+{
+	h := 0;
+	for(i:=0; i<len s; i++){
+		h = (h<<4) + s[i];
+		if((g := h & int 16rF0000000) != 0)
+			h ^= ((g>>24) & 16rFF) | g;
+	}
+	return ((h&~(1<<31))%n, h);
+}
--- /dev/null
+++ b/appl/lib/ubfa.b
@@ -1,0 +1,624 @@
+implement UBFa;
+
+#
+# UBF(A) data encoding interpreter
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "ubfa.m";
+
+Syntax: exception(string);
+Badwrite: exception;
+
+dict: array of list of string;
+dictlock: chan of int;
+
+init(m: Bufio)
+{
+	sys = load Sys Sys->PATH;
+	bufio = m;
+
+	dict = array[74] of list of string;
+	dictlock = chan[1] of int;
+}
+
+uvatom(s: string): ref UValue.Atom
+{
+	return ref UValue.Atom(uniq(s));
+}
+
+uvint(i: int): ref UValue.Int
+{
+	return ref UValue.Int(i);
+}
+
+uvbig(i: big): ref UValue.Int
+{
+	return ref UValue.Int(int i);
+}
+
+uvbinary(a: array of byte): ref UValue.Binary
+{
+	return ref UValue.Binary(a);
+}
+
+uvstring(s: string): ref UValue.String
+{
+	return ref UValue.String(s);
+}
+
+uvtuple(a: array of ref UValue): ref UValue.Tuple
+{
+	return ref UValue.Tuple(a);
+}
+
+uvlist(l: list of ref UValue): ref UValue.List
+{
+	return ref UValue.List(l);
+}
+
+uvtag(s: string, o: ref UValue): ref UValue.Tag
+{
+	return ref UValue.Tag(uniq(s), o);
+}
+
+# needed only to avoid O(n) len s.s
+Stack: adt {
+	s:	list of ref UValue;
+	n:	int;
+
+	new:	fn(): ref Stack;
+	pop:	fn(s: self ref Stack): ref UValue raises(Syntax);
+	push:	fn(s: self ref Stack, o: ref UValue);
+};
+
+Stack.new(): ref Stack
+{
+	return ref Stack(nil, 0);
+}
+
+Stack.pop(s: self ref Stack): ref UValue raises(Syntax)
+{
+	if(--s.n < 0 || s.s == nil)
+		raise Syntax("parse stack underflow");
+	v := hd s.s;
+	s.s = tl s.s;
+	return v;
+}
+
+Stack.push(s: self ref Stack, o: ref UValue)
+{
+	s.s = o :: s.s;
+	s.n++;
+}
+
+Parse: adt {
+	input:	ref Iobuf;
+	stack:	ref Stack;
+	reg:		array of ref UValue;
+
+	getb:		fn(nil: self ref Parse): int raises(Syntax);
+	unget:	fn(nil: self ref Parse);
+};
+
+Parse.getb(p: self ref Parse): int raises(Syntax)
+{
+	c := p.input.getb();
+	if(c < 0){
+		if(c == Bufio->EOF)
+			raise Syntax("unexpected end-of-file");
+		raise Syntax(sys->sprint("read error: %r"));
+	}
+	return c;
+}
+
+Parse.unget(p: self ref Parse)
+{
+	p.input.ungetb();
+}
+
+uniq(s: string): string
+{
+	if(s == nil)
+		return "";
+	dictlock <-= 1;
+	h := 0;
+	for(i:=0; i<len s; i++){
+		h = (h<<4) + s[i];
+		if((g := h & int 16rF0000000) != 0)
+			h ^= ((g>>24) & 16rFF) | g;
+	}
+	h = (h & Sys->Maxint)%len dict;
+	for(l := dict[h]; l != nil; l = tl l)
+		if(hd l == s){
+			s = hd l;	# share space
+			break;
+		}
+	if(l == nil)
+		dict[h] = s :: dict[h];
+	<-dictlock;
+	return s;
+}
+
+writeubf(out: ref Iobuf, obj: ref UValue): int
+{
+	{
+		# write it out, put final '$'
+		if(out != nil)
+			writeobj(out, obj);
+		putc(out, '$');
+		return 0;
+	}exception{
+	Badwrite =>
+		return -1;
+	}
+}
+
+readubf(input: ref Iobuf): (ref UValue, string)
+{
+	{
+		return (getobj(ref Parse(input, Stack.new(), array[256] of ref UValue)), nil);
+	}exception e{
+	Syntax =>
+		return (nil, sys->sprint("ubf error: offset %bd: %s", input.offset(), e));
+	}
+}
+
+UValue.isatom(o: self ref UValue): int
+{
+	return tagof o == tagof UValue.Atom;
+}
+
+UValue.isstring(o: self ref UValue): int
+{
+	return tagof o == tagof UValue.String;
+}
+
+UValue.isint(o: self ref UValue): int
+{
+	return tagof o == tagof UValue.Int;
+}
+
+UValue.islist(o: self ref UValue): int
+{
+	return tagof o == tagof UValue.List;
+}
+
+UValue.istuple(o: self ref UValue): int
+{
+	return tagof o == tagof UValue.Tuple;
+}
+
+UValue.isbinary(o: self ref UValue): int
+{
+	return tagof o == tagof UValue.Binary;
+}
+
+UValue.istag(o: self ref UValue): int
+{
+	return tagof o == tagof UValue.Tag;
+}
+
+UValue.isop(o: self ref UValue, op: string, arity: int): int
+{
+	pick r := o {
+	Tuple =>
+		if(len r.a > 0 && (arity <= 0 || len r.a == arity))
+			pick s := r.a[0] {
+			Atom =>
+				return s.name == op;
+			String =>
+				return s.s == op;
+			}
+	}
+	return 0;
+}
+
+UValue.op(o: self ref UValue, arity: int): string
+{
+	pick r := o {
+	Tuple =>
+		if(len r.a > 0 && (arity <= 0 || len r.a == arity))
+			pick s := r.a[0] {
+			Atom =>
+				return  s.name;
+			String =>
+				return s.s;
+			}
+	}
+	return nil;
+}
+
+UValue.args(o: self ref UValue, arity: int): array of ref UValue
+{
+	pick r := o {
+	Tuple =>
+		if(len r.a > 0 && (arity <= 0 || len r.a == arity))
+			return r.a[1:];
+	}
+	return nil;
+}
+
+UValue.els(o: self ref UValue): list of ref UValue
+{
+	pick r := o {
+	List =>
+		return r.l;
+	}
+	return nil;
+}
+
+UValue.val(o: self ref UValue): int
+{
+	pick r :=  o {
+	Int =>
+		return r.value;
+	}
+	return 0;
+}
+
+UValue.objtag(o: self ref UValue): string
+{
+	pick r := o {
+	Tag =>
+		return r.name;
+	}
+	return nil;
+}
+
+UValue.obj(o: self ref UValue): ref UValue
+{
+	pick r := o {
+	Tag =>
+		return r.o;
+	}
+	return o;
+}
+
+UValue.binary(o: self ref UValue): array of byte
+{
+	pick r := o {
+	Atom =>
+		return array of byte r.name;
+	String =>
+		return array of byte r.s;
+	Binary =>
+		return r.a;
+	}
+	return nil;
+}
+
+UValue.text(o: self ref UValue): string
+{
+	pick r := o {
+	Atom =>
+		return r.name;
+	String =>
+		return r.s;
+	Int =>
+		return string r.value;
+	Tuple =>
+		s := "{";
+		for(i := 0; i < len r.a; i++)
+			s += " "+r.a[i].text();
+		return s+"}";
+	List =>
+		s := "[";
+		for(l := r.l; l != nil; l = tl l)
+			s += " "+(hd l).text();
+		return s+"]";
+	Binary =>
+		s := "<<";
+		for(i := 0; i < len r.a; i++)
+			s += sys->sprint(" %.2ux", int r.a[i]);
+		return s+">>";
+	Tag =>
+		return "{'$TYPE', "+r.name+", "+r.o.text()+"}";
+	* =>
+		return "unknown";
+	}
+}
+
+UValue.eq(o: self ref UValue, v: ref UValue): int
+{
+	if(v == nil)
+		return 0;
+	if(o == v)
+		return 1;
+	pick r := o {
+	Atom =>
+		pick s := v {
+		Atom =>
+			return r.name == s.name;
+		}
+		return 0;
+	String =>
+		pick s := v {
+		String =>
+			return r.s == s.s;
+		}
+		return 0;
+	Int =>
+		pick s := v {
+		Int =>
+			return r.value == s.value;
+		}
+		return 0;
+	Tuple =>
+		pick s := v {
+		Tuple =>
+			if(len r.a != len s.a)
+				return 0;
+			for(i := 0; i < len r.a; i++)
+				if(!r.a[i].eq(s.a[i]))
+					return 0;
+			return 1;
+		}
+		return 0;
+	List =>
+		pick s := v {
+		List =>
+			l1 := r.l;
+			l2 := s.l;
+			while(l1 != nil && l2 != nil){
+				if(!(hd l1).eq(hd l2))
+					return 0;
+				l1 = tl l1;
+				l2 = tl l2;
+			}
+			return l1 == l2;
+		}
+		return 0;
+	Binary =>
+		pick s := v {
+		Binary =>
+			if(len r.a != len s.a)
+				return 0;
+			for(i := 0; i < len r.a; i++)
+				if(r.a[i] != s.a[i])
+					return 0;
+			return 1;
+		}
+		return 0;
+	Tag =>
+		pick s := v {
+		Tag =>
+			return r.name == s.name && r.o.eq(s.o);
+		}
+		return 0;
+	* =>
+		raise "ubf: bad object";	# can't happen
+	}
+}
+
+S: con byte 1;
+
+special := array[256] of {
+	'\n' or '\r' or '\t' or ' ' or ',' => S,
+	'}' => S, '$' => S, '>' => S, '#' => S, '&' => S,
+	'"' => S, '\'' => S, '{' => S, '~' => S, '-' => S,
+	'0' to '9' => S, '%' => S, '`' => S, * => byte 0
+};
+
+getobj(p: ref Parse): ref UValue raises(Syntax)
+{
+	{
+		for(;;){
+			case p.getb() {
+			'\n' or '\r' or '\t' or ' ' or ',' =>
+				;	# white space
+			'%' =>
+				while((c := p.getb()) != '%'){
+					if(c == '\\'){	# do comments really use \?
+						c = p.getb();
+						if(c != '\\' && c != '%')
+							raise Syntax("invalid escape in comment");
+					}
+				}
+			'}' =>
+				a := array[p.stack.n] of ref UValue;
+				for(i := len a; --i >= 0;)
+					a[i] = p.stack.pop();
+				return ref UValue.Tuple(a);
+			'$' =>
+				if(p.stack.n != 1)
+					raise Syntax("unbalanced stack: size "+string p.stack.n);
+				return p.stack.pop();
+			'>' =>
+				r := p.getb();
+				if(special[r] == S)
+					raise Syntax("invalid register name");
+				p.reg[r] = p.stack.pop();
+			'`' =>
+				t := uniq(readdelimitedstring(p, '`'));
+				p.stack.push(ref UValue.Tag(t, p.stack.pop()));
+			* =>
+				p.unget();
+				p.stack.push(readobj(p));
+			}
+		}
+	}exception{
+	Syntax =>
+		raise;
+	}
+}
+
+readobj(p: ref Parse): ref UValue raises(Syntax)
+{
+	{
+	 	case c := p.getb() {
+		'#' =>
+			return ref UValue.List(nil);
+		'&' =>
+			a := p.stack.pop();
+			b := p.stack.pop();
+			pick r := b {
+			List =>
+				return ref UValue.List(a :: r.l);	# not changed in place: might be shared register value
+			* =>
+				raise Syntax("can't make cons with cdr "+b.text());
+			}
+		'"' =>
+			return ref UValue.String(readdelimitedstring(p, c));
+		'\'' =>
+			return ref UValue.Atom(uniq(readdelimitedstring(p, c)));
+		'{' =>
+			obj := getobj(ref Parse(p.input, Stack.new(), p.reg));
+			if(!obj.istuple())
+				raise Syntax("expected tuple: obj");
+			return obj;
+		'~' =>
+			o := p.stack.pop();
+			if(!o.isint())
+				raise Syntax("expected Int before ~");
+			n := o.val();
+			if(n < 0)
+				raise Syntax("negative length for binary");
+			a := array[n] of byte;
+			n = p.input.read(a, len a);
+			if(n != len a){
+				if(n != Bufio->ERROR)
+					sys->werrstr("short read");
+				raise Syntax(sys->sprint("cannot read binary data: %r"));
+			}
+			if(p.getb() != '~')
+				raise Syntax("missing closing ~");
+			return ref UValue.Binary(a);
+		'-' or '0' to '9' =>
+			p.unget();
+			return ref UValue.Int(int readinteger(p));
+		* =>
+			if(p.reg[c] != nil)
+				return p.reg[c];
+			p.unget();	# point to error
+			raise Syntax(sys->sprint("invalid start character/undefined register #%.2ux",c));
+		}
+	}exception{
+	Syntax =>
+		raise;
+	}
+}
+
+readdelimitedstring(p: ref Parse, delim: int): string raises(Syntax)
+{
+	{
+		s := "";
+		while((c := p.input.getc()) != delim){	# note: we'll use UTF-8
+			if(c < 0){
+				if(c == Bufio->ERROR)
+					raise Syntax(sys->sprint("read error: %r"));
+				raise Syntax("unexpected end of file");
+			}
+			if(c == '\\'){
+				c = p.getb();
+				if(c != '\\' && c != delim)
+					raise Syntax("invalid escape");
+			}
+			s[len s] = c;
+		}
+		return s;
+	}exception{
+	Syntax =>
+		raise;
+	}
+}
+
+readinteger(p: ref Parse): big raises(Syntax)
+{
+	sign := 1;
+	c := p.getb();
+	if(c == '-'){
+		sign = -1;
+		c = p.getb();
+		if(!(c >= '0' && c <= '9'))
+			raise Syntax("expected integer literal");
+	}
+	for(n := big 0; c >= '0' && c <= '9'; c = p.getb()){
+		n = n*big 10 + big((c-'0')*sign);
+		if(n > big Sys->Maxint || n < big(-Sys->Maxint-1))
+			raise Syntax("integer overflow");
+	}
+	p.unget();
+	return n;
+}
+
+writeobj(out: ref Iobuf, o: ref UValue) raises(Badwrite)
+{
+	{
+		pick r := o {
+		Atom =>
+			writedelimitedstring(out, r.name, '\'');
+		String =>
+			writedelimitedstring(out, r.s, '"');
+		Int =>
+			puts(out, string r.value);
+		Tuple =>	# { el * }
+			putc(out, '{');
+			for(i := 0; i < len r.a; i++){
+				if(i != 0)
+					putc(out, ' ');
+				writeobj(out, r.a[i]);
+			}
+			putc(out, '}');
+		List =>	# # eN & eN-1 & ... & e0 &
+			putc(out, '#');
+			# put them out in reverse order, each followed by '&'
+			rl: list of ref UValue;
+			for(l := r.l; l != nil; l = tl l)
+				rl = hd l :: rl;
+			for(; rl != nil; rl = tl rl){
+				writeobj(out, hd rl);
+				putc(out, '&');
+			}
+		Binary =>	# Int ~data~
+			puts(out, string len r.a);
+			putc(out, '~');
+			if(out.write(r.a, len r.a) != len r.a)
+				raise Badwrite;
+			putc(out, '~');
+		Tag =>	# obj `tag`
+			writeobj(out, r.o);
+			writedelimitedstring(out, r.name, '`');
+		* =>
+			raise "ubf: unknown object";	# can't happen
+		}
+	}exception{
+	Badwrite =>
+		raise;
+	}
+}
+
+writedelimitedstring(out: ref Iobuf, s: string, d: int) raises(Badwrite)
+{
+	{
+		putc(out, d);
+		for(i := 0; i < len s; i++){
+			c := s[i];
+			if(c == d || c == '\\')
+				putc(out, '\\');
+			putc(out, c);
+		}
+		putc(out, d);
+	}exception{
+	Badwrite =>
+		raise;
+	}
+}
+
+puts(out: ref Iobuf, s: string) raises(Badwrite)
+{
+	if(out.puts(s) == Bufio->ERROR)
+		raise Badwrite;
+}
+
+putc(out: ref Iobuf, c: int) raises(Badwrite)
+{
+	if(out.putc(c) == Bufio->ERROR)
+		raise Badwrite;
+}
--- /dev/null
+++ b/appl/lib/url.b
@@ -1,0 +1,224 @@
+implement Url;
+
+include "sys.m";
+	sys: Sys;
+
+include "string.m";
+	S: String;
+
+include "url.m";
+
+schemes = array[] of {
+	NOSCHEME => "",
+	HTTP => "http",
+	HTTPS => "https",
+	FTP => "ftp",
+	FILE => "file",
+	GOPHER => "gopher",
+	MAILTO => "mailto",
+	NEWS => "news",
+	NNTP => "nntp",
+	TELNET => "telnet",
+	WAIS => "wais",
+	PROSPERO => "prospero",
+	JAVASCRIPT => "javascript",
+	UNKNOWN => "unknown"
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	S = load String String->PATH;
+}
+
+# To allow relative urls, only fill in specified pieces (don't apply defaults)
+#  general syntax: <scheme>:<scheme-specific>
+#  for IP schemes, <scheme-specific> is
+#      //<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
+makeurl(surl: string): ref ParsedUrl
+{
+	scheme := NOSCHEME;
+	user := "";
+	passwd := "";
+	host := "";
+	port := "";
+	pstart := "";
+	path := "";
+	query := "";
+	frag := "";
+
+	(sch, url) := split(surl, ":");
+	if(url == "") {
+		url = sch;
+		sch = "";
+	}
+	else {
+		(nil, x) := S->splitl(sch, "^-a-zA-Z0-9.+");
+		if(x != nil) {
+			url = surl;
+			sch = "";
+		}
+		else {
+			scheme = UNKNOWN;
+			sch = S->tolower(sch);
+			for(i := 0; i < len schemes; i++)
+				if(schemes[i] == sch) {
+					scheme = i;
+					break;
+				}
+		}
+	}
+	if(scheme == MAILTO)
+		path = url;
+	else if (scheme == JAVASCRIPT)
+		path = url;
+	else {
+		if(S->prefix("//", url)) {
+			netloc: string;
+			(netloc, path) = S->splitl(url[2:], "/");
+			if(path != "")
+				path = path[1:];
+			pstart = "/";
+			if(scheme == FILE)
+				host = netloc;
+			else {
+				(up,hp) := split(netloc, "@");
+				if(hp == "")
+					hp = up;
+				else
+					(user, passwd) = split(up, ":");
+				(host, port) = split(hp, ":");
+			}
+		}
+		else {
+			if(S->prefix("/", url)) {
+				pstart = "/";
+				path = url[1:];
+			}
+			else
+				path = url;
+		}
+		if(scheme == FILE) {
+			if(host == "")
+				host = "localhost";
+		}
+		else {
+			(path, frag) = split(path, "#");
+			(path, query) = split(path, "?");
+		}
+	}
+
+	return ref ParsedUrl(scheme, 1, user, passwd, host, port, pstart, path, query, frag);
+}
+
+ParsedUrl.tostring(u: self ref ParsedUrl) : string
+{
+	if (u == nil)
+		return nil;
+
+	ans := "";
+	if(u.scheme > 0 && u.scheme < len schemes)
+		ans = schemes[u.scheme] + ":";
+	if(u.host != "") {
+		ans = ans + "//";
+		if(u.user != "") {
+			ans = ans + u.user;
+			if(u.passwd != "")
+				ans = ans + ":" + u.passwd;
+			ans = ans + "@";
+		}
+		ans = ans + u.host;
+		if(u.port != "")
+			ans = ans + ":" + u.port;
+	}
+	ans = ans + u.pstart + u.path;
+	if(u.query != "")
+		ans = ans + "?" + u.query;
+	if(u.frag != "")
+		ans = ans + "#" + u.frag;
+	return ans;
+}
+
+ParsedUrl.makeabsolute(u: self ref ParsedUrl, b: ref ParsedUrl)
+{
+#	The following is correct according to RFC 1808, but is violated
+#	by various extant web pages.
+
+	if(u.scheme != NOSCHEME && u.scheme != HTTP)
+		return;
+
+	if(u.host == "" && u.path == "" && u.pstart == "" && u.query == "" && u.frag == "") {
+		u.scheme = b.scheme;
+		u.user = b.user;
+		u.passwd = b.passwd;
+		u.host = b.host;
+		u.port = b.port;
+		u.path = b.path;
+		u.pstart = b.pstart;
+		u.query = b.query;
+		u.frag = b.frag;
+		return;
+	}
+	if(u.scheme == NOSCHEME)
+		u.scheme = b.scheme;
+	if(u.host != "")
+		return;
+	u.user = b.user;
+	u.passwd = b.passwd;
+	u.host = b.host;
+	u.port = b.port;
+	if(u.pstart == "/")
+		return;
+	u.pstart = "/";
+	if(u.path == "") {
+		u.path = b.path;
+		if(u.query == "")
+			u.query = b.query;
+	}
+	else {
+		(p1,nil) := S->splitr(b.path, "/");
+		u.path = canonize(p1 + u.path);
+	}
+}
+
+# Like splitl, but assume one char match, and omit that from second part.
+# If c doesn't appear in s, the return is (s, "").
+split(s, c: string) : (string, string)
+{
+	(a,b) := S->splitl(s, c);
+	if(b != "")
+		b = b[1:];
+	return (a,b);
+}
+
+# remove ./ and ../ from s
+canonize(s: string): string
+{
+	(base, file) := S->splitr(s, "/");
+	(nil, path) := sys->tokenize(base, "/");
+	revpath : list of string = nil;
+	for(p := path; p != nil; p = tl p) {
+		if(hd p == "..") {
+			if(revpath != nil)
+				revpath = tl revpath;
+		}
+		else if(hd p != ".")
+			revpath = (hd p) :: revpath;
+	}
+	while(revpath != nil && hd revpath == "..")
+		revpath = tl revpath;
+	ans := "";
+	if(revpath != nil) {
+		ans = hd revpath;
+		revpath = tl revpath;
+		while(revpath != nil) {
+			ans = (hd revpath) + "/" + ans;
+			revpath = tl revpath;
+		}
+	}
+	if (ans != nil)
+		ans += "/";
+	ans += file;
+	return ans;
+}
+
--- /dev/null
+++ b/appl/lib/usb/mkfile
@@ -1,0 +1,16 @@
+<../../../mkconfig
+
+TARG=\
+	usb.dis\
+	usbmouse.dis\
+	usbmct.dis\
+	usbmass.dis
+
+MODULES=
+
+SYSMODULES= \
+	usb.m\
+
+DISBIN=$ROOT/dis/lib/usb
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/usb/usb.b
@@ -1,0 +1,453 @@
+#
+# Copyright © 2002 Vita Nuova Holdings Limited
+#
+implement Usb;
+
+include "sys.m";
+	sys: Sys;
+
+include "usb.m";
+
+include "string.m";
+	str: String;
+
+Proto: adt {
+	proto: int;
+	name: string;
+};
+
+SubClass: adt {
+	subclass: int;
+	name: string;
+	proto: array of Proto;
+};
+
+Class: adt {
+	class: int;
+	name: string;
+	subclass: array of SubClass;
+};
+
+classes := array [] of {
+	Class(Usb->CL_AUDIO, "audio",
+		array [] of {
+			SubClass(1, "control", nil),
+			SubClass(2, "stream", nil),
+			SubClass(3, "midi", nil),
+		}
+	),
+	Class(Usb->CL_COMMS, "comms",
+		array [] of {
+			SubClass(1, "abstract",
+				array [] of {
+					Proto(1, "AT"),
+				}
+			)
+		}
+	),
+	Class(Usb->CL_HID, "hid",
+		array [] of {
+			SubClass(1, "boot",
+				array [] of {
+					Proto(1, "kbd"),
+					Proto(2, "mouse"),
+				}
+			)
+		}
+	),
+	Class(Usb->CL_PRINTER, "printer",
+		array [] of {
+			SubClass(1, "printer",
+				array [] of {
+					Proto(1, "uni"),
+					Proto(2, "bi"),
+				}
+			)
+		}
+	),
+	Class(Usb->CL_HUB, "hub",
+		array [] of {
+			SubClass(1, "hub", nil),
+		}
+	),
+	Class(Usb->CL_DATA, "data", nil),
+	Class(Usb->CL_MASS, "mass",
+		array [] of {
+			SubClass(1, "rbc",
+				array [] of {
+					Proto(0, "cbi-cc"),
+					Proto(1, "cbi-nocc"),
+					Proto(16r50, "bulkonly"),
+				}
+			),
+			SubClass(2, "sff-8020i/mmc-2",
+				array [] of {
+					Proto(0, "cbi-cc"),
+					Proto(1, "cbi-nocc"),
+					Proto(16r50, "bulkonly"),
+				}
+			),
+			SubClass(3, "qic-157",
+				array [] of {
+					Proto(0, "cbi-cc"),
+					Proto(1, "cbi-nocc"),
+					Proto(16r50, "bulkonly"),
+				}
+			),
+			SubClass(4, "ufi",
+				array [] of {
+					Proto(0, "cbi-cc"),
+					Proto(1, "cbi-nocc"),
+					Proto(16r50, "bulkonly"),
+				}
+			),
+			SubClass(5, "sff-8070i",
+				array [] of {
+					Proto(0, "cbi-cc"),
+					Proto(1, "cbi-nocc"),
+					Proto(16r50, "bulkonly"),
+				}
+			),
+			SubClass(6, "scsi",
+				array [] of {
+					Proto(0, "cbi-cc"),
+					Proto(1, "cbi-nocc"),
+					Proto(16r50, "bulkonly"),
+				}
+			),
+		}
+	),
+};
+
+get2(b: array of byte): int
+{
+	return int b[0] | (int b[1] << 8);
+}
+
+put2(buf: array of byte, v: int)
+{
+	buf[0] = byte v;
+	buf[1] = byte (v >> 8);
+}
+
+get4(b: array of byte): int
+{
+	return int b[0] | (int b[1] << 8) | (int b[2] << 16) | (int b[3] << 24);
+}
+
+put4(buf: array of byte, v: int)
+{
+	buf[0] = byte v;
+	buf[1] = byte (v >> 8);
+	buf[2] = byte (v >> 16);
+	buf[3] = byte (v >> 24);
+}
+
+bigget2(b: array of byte): int
+{
+	return int b[1] | (int b[0] << 8);
+}
+
+bigput2(buf: array of byte, v: int)
+{
+	buf[1] = byte v;
+	buf[0] = byte (v >> 8);
+}
+
+bigget4(b: array of byte): int
+{
+	return int b[3] | (int b[2] << 8) | (int b[1] << 16) | (int b[0] << 24);
+}
+
+bigput4(buf: array of byte, v: int)
+{
+	buf[3] = byte v;
+	buf[2] = byte (v >> 8);
+	buf[1] = byte (v >> 16);
+	buf[0] = byte (v >> 24);
+}
+
+strtol(s: string, base: int): (int, string)
+{
+	if (str == nil)
+		str = load String String->PATH;
+	if (base != 0)
+		return str->toint(s, base);
+	if (len s >= 2 && (s[0:2] == "0X" || s[0:2] == "0x"))
+		return str->toint(s[2:], 16);
+	if (len s > 0 && s[0:1] == "0")
+		return str->toint(s[1:], 8);
+	return str->toint(s, 10);
+}
+
+memset(buf: array of byte, v: int)
+{
+	for (x := 0; x < len buf; x++)
+		buf[x] = byte v;
+}
+
+setupreq(setupfd: ref Sys->FD, typ, req, value, index: int, outbuf: array of byte, count: int): int
+{
+	additional: int;
+	if (outbuf != nil) {
+		additional = len outbuf;
+		# if there is an outbuf, then the count sent must be length of the payload
+		# this assumes that RH2D is set
+		count = additional;
+	}
+	else
+		additional = 0;
+	buf := array[8 + additional] of byte;
+	buf[0] = byte typ;
+	buf[1] = byte req;
+	put2(buf[2:], value);
+	put2(buf[4:], index);
+	put2(buf[6:], count);
+	if (additional)
+		buf[8:] = outbuf;
+	rv := sys->write(setupfd, buf, len buf);
+	if (rv < 0)
+		return -1;
+	if (rv != len buf)
+		return -1;
+	return rv;
+}
+
+setupreply(setupfd: ref Sys->FD, buf: array of byte): int
+{
+	nb := sys->read(setupfd, buf, len buf);
+	return nb;
+}
+
+setup(setupfd: ref Sys->FD, typ, req, value, index: int, outbuf: array of byte, inbuf: array of byte): int
+{
+	count: int;
+	if (inbuf != nil)
+		count = len inbuf;
+	else
+		count = 0;
+	if (setupreq(setupfd, typ, req, value, index, outbuf, count) < 0)
+		return -1;
+	if (count == 0)
+		return 0;
+	return setupreply(setupfd, inbuf);
+}
+
+get_descriptor(fd: ref Sys->FD, rtyp: int, dtyp: int, dindex: int, langid: int, buf: array of byte): int
+{
+	nr := -1;
+	if (setupreq(fd, RD2H | rtyp | Rdevice, GET_DESCRIPTOR, (dtyp << 8) | dindex, langid, nil, len buf) < 0
+		|| (nr = setupreply(fd, buf)) < 1)
+		return -1;
+	return nr;
+}
+
+get_standard_descriptor(fd: ref Sys->FD, dtyp: int, index: int, buf: array of byte): int
+{
+	return get_descriptor(fd, Rstandard, dtyp, index, 0, buf);
+}
+
+get_class_descriptor(fd: ref Sys->FD, dtyp: int, index: int, buf: array of byte): int
+{
+	return get_descriptor(fd, Rclass, dtyp, index, 0, buf);
+}
+
+get_vendor_descriptor(fd: ref Sys->FD, dtyp: int, index: int, buf: array of byte): int
+{
+	return get_descriptor(fd, Rvendor, dtyp, index, 0, buf);
+}
+
+get_status(fd: ref Sys->FD, port: int): int
+{
+	buf := array [4] of byte;
+	if (setupreq(fd, RD2H | Rclass | Rother, GET_STATUS, 0, port, nil, len buf) < 0
+	 	|| setupreply(fd, buf) < len buf)
+		return -1;
+	return get2(buf);
+}
+
+set_address(fd: ref Sys->FD, address: int): int
+{
+	return setupreq(fd, RH2D | Rstandard | Rdevice, SET_ADDRESS, address, 0, nil, 0);
+}
+
+set_configuration(fd: ref Sys->FD, n: int): int
+{
+	return setupreq(fd, RH2D | Rstandard | Rdevice, SET_CONFIGURATION, n, 0, nil, 0);
+}
+
+setclear_feature(fd: ref Sys->FD, rtyp: int, value: int, index: int, on: int): int
+{
+	req: int;
+	if (on)
+		req = SET_FEATURE;
+	else
+		req = CLEAR_FEATURE;
+	return setupreq(fd, RH2D | rtyp, req, value, index, nil, 0);
+}
+
+parse_conf(b: array of byte): ref Configuration
+{
+	if (len b < DCONFLEN)
+		return nil;
+	conf := ref Configuration;
+	conf.id = int b[5];
+	conf.iface = array[int b[4]] of Interface;
+	conf.attr = int b[7];
+	conf.powerma = int b[8] * 2;
+	return conf;
+}
+
+parse_iface(conf: ref Configuration, b: array of byte): ref AltInterface
+{
+	if (len b < DINTERLEN || conf == nil)
+		return nil;
+	id := int b[2];
+	if (id >= len conf.iface)
+		return nil;
+	ai := ref AltInterface;
+	ai.id = int b[3];
+	if (int b[4] != 0)
+		ai.ep = array [int b[4]] of ref Endpt;
+	ai.class = int b[5];
+	ai.subclass = int b[6];
+	ai.proto = int b[7];
+	conf.iface[id].altiface = ai :: conf.iface[id].altiface;
+	return ai;
+}
+	
+parse_endpt(conf: ref Configuration, ai: ref AltInterface, b: array of byte): ref Endpt
+{
+	if (len b < DENDPLEN || conf == nil || ai == nil || ai.ep == nil)
+		return nil;
+	for (i := 0; i < len ai.ep; i++)
+		if (ai.ep[i] == nil)
+			break;
+	if (i >= len ai.ep)
+		return nil;
+	ep := ref Endpt;
+	ai.ep[i] = ep;
+	ep.addr = int b[2];
+	ep.attr = int b[3];
+	ep.d2h = ep.addr & 16r80;
+	ep.etype = int b[3] & 3;
+	ep.isotype = (int b[3] >> 2) & 3;
+	ep.maxpkt = get2(b[4:]);
+	ep.interval = int b[6];
+	return ep;
+}
+
+get_parsed_configuration_descriptor(fd: ref Sys->FD, n: int): ref Configuration
+{
+	conf: ref Configuration;
+	altiface: ref AltInterface;
+
+	b := array [256] of byte;
+	nr := get_standard_descriptor(fd, CONFIGURATION, n, b);
+	if (nr < 0)
+		return nil;
+	conf = nil;
+	altiface = nil;
+	for (i := 0; nr - i > 2 && b[i] > byte 0 && int b[i] <= nr - i; i += int b[i]) {
+		ni := i + int b[i];
+		case int b[i + 1] {
+		Usb->CONFIGURATION =>
+			conf = parse_conf(b[i: ni]);
+			if (conf == nil)
+				return nil;
+		Usb->INTERFACE =>
+			altiface = parse_iface(conf, b[i: ni]);
+			if (altiface == nil)
+				return nil;
+		Usb->ENDPOINT =>
+			if (parse_endpt(conf, altiface, b[i: ni]) == nil)
+				return nil;
+		}
+	}
+	if (i < nr)
+		sys->print("usb: residue at end of descriptors\n");
+	return conf;
+}
+
+get_parsed_device_descriptor(fd: ref Sys->FD): ref Device
+{
+	b := array [256] of byte;
+	nr := get_standard_descriptor(fd, DEVICE, 0, b);
+	if (nr < DDEVLEN) {
+		if (nr == 8 || nr == 16) {
+			memset(b[nr: DDEVLEN - 1], 0);
+			b[DDEVLEN - 1] = byte 1;
+			nr = DDEVLEN;
+		}
+		else
+			return nil;
+	}
+	dev := ref Device;
+	dev.usbmajor = int b[3];
+	dev.usbminor = int b[2];
+	dev.class = int b[4];
+	dev.subclass = int b[5];
+	dev.proto = int b[6];
+	dev.maxpkt0 = int b[7];
+	dev.vid = get2(b[8:]);
+	dev.did = get2(b[10:]);
+	dev.relmajor = int b[13];
+	dev.relminor = int b[12];
+	dev.nconf = int b[17];
+	return dev;
+}
+
+dump_configuration(fd: ref Sys->FD, conf: ref Configuration)
+{
+	sys->fprint(fd, "configuration %d attr 0x%.x powerma %d\n", conf.id, conf.attr, conf.powerma);
+	for (i := 0; i < len conf.iface; i++) {
+		sys->fprint(fd, "\tinterface %d\n", i);
+		ail := conf.iface[i].altiface;
+		while (ail != nil) {
+			ai := hd ail;
+			sys->fprint(fd, "\t\t%d class %d subclass %d proto %d [%s]\n",
+				ai.id, ai.class, ai.subclass, ai.proto,	
+				sclass(ai.class, ai.subclass, ai.proto));
+			for (e := 0; e < len ai.ep; e++) {
+				if (ai.ep[e] == nil) {
+					sys->fprint(fd, "\t\t\t missing descriptor\n");
+					continue;
+				}
+				sys->fprint(fd, "\t\t\t0x%.2ux attr 0x%.x maxpkt %d interval %d\n",
+					ai.ep[e].addr, ai.ep[e].attr, ai.ep[e].maxpkt, ai.ep[e].interval);
+			}
+			ail = tl ail;
+		}
+	}
+sys->fprint(fd, "done dumping\n");
+}
+
+sclass(class, subclass, proto: int): string
+{
+	for (c := 0; c < len classes; c++)
+		if (classes[c].class == class)
+			break;
+	if (c >= len classes)
+		return sys->sprint("%d.%d.%d", class, subclass, proto);
+	if (classes[c].subclass == nil)
+		return sys->sprint("%s.%d.%d", classes[c].name, subclass, proto);
+	for (sc := 0; sc < len classes[c].subclass; sc++)
+		if (classes[c].subclass[sc].subclass == subclass)
+			break;
+	if (sc >= len classes[c].subclass)
+		return sys->sprint("%s.%d.%d", classes[c].name, subclass, proto);
+	if (classes[c].subclass[sc].proto == nil)
+		return sys->sprint("%s.%s.%d", classes[c].name, classes[c].subclass[sc].name, proto);
+	for (p := 0; p < len classes[c].subclass[sc].proto; p++)
+		if (classes[c].subclass[sc].proto[p].proto == proto)
+			break;
+	if (p >= len classes[c].subclass[sc].proto)
+		return sys->sprint("%s.%s.%d", classes[c].name, classes[c].subclass[sc].name, proto);
+	return sys->sprint("%s.%s.%s", classes[c].name, classes[c].subclass[sc].name,
+		classes[c].subclass[sc].proto[p].name);
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
--- /dev/null
+++ b/appl/lib/usb/usbmass.b
@@ -1,0 +1,465 @@
+#
+# Copyright © 2001 Vita Nuova Holdings Limited.
+#
+implement UsbDriver;
+
+include "sys.m";
+	sys: Sys;
+include "usb.m";
+	usb: Usb;
+	Endpt, RD2H, RH2D: import Usb;
+
+ENDPOINT_STALL: con 0;	# TO DO: should be in usb.m
+
+readerpid: int;
+setupfd, ctlfd: ref Sys->FD;
+infd, outfd: ref Sys->FD;
+inep, outep: ref Endpt;
+cbwseq := 0;
+capacity: big;
+debug := 0;
+
+lun: int;
+blocksize: int;
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if (fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+reader(pidc: chan of int, fileio: ref Sys->FileIO)
+{
+	pid := sys->pctl(0, nil);
+	pidc <-= pid;
+	for(;;) alt{
+	(offset, count, nil, rc) := <-fileio.read =>
+		if (rc != nil) {
+			if (offset%blocksize || count%blocksize) {
+				rc <- = (nil, "unaligned read");
+				continue;
+			}
+			offset /= blocksize;
+			count /= blocksize;
+			buf := array [count * blocksize] of byte;
+			if (scsiread10(lun, offset, count, buf) < 0) {
+				scsirequestsense(lun);
+				rc <- = (nil, "read error");
+				continue;
+			}
+			rc <- = (buf, nil);
+		}
+	(offset, data, nil, wc) := <-fileio.write =>
+		if(wc != nil){
+			count := len data;
+			if(offset%blocksize || count%blocksize){
+				wc <-= (0, "unaligned write");
+				continue;
+			}
+			offset /= blocksize;
+			count /= blocksize;
+			if(scsiwrite10(lun, offset, count, data) < 0){
+				scsirequestsense(lun);
+				wc <-= (0, "write error");
+				continue;
+			}
+			wc <-= (len data, nil);
+		}
+	}
+	readerpid = -1;
+}
+
+massstoragereset(): int
+{
+	if (usb->setup(setupfd, Usb->RH2D | Usb->Rclass | Usb->Rinterface, 255, 0, 0, nil, nil) < 0) {
+		sys->print("usbmass: storagereset failed\n");
+		return -1;
+	}
+	return 0;
+}
+	
+getmaxlun(): int
+{
+	buf := array[1] of byte;
+	if (usb->setup(setupfd, Usb->RD2H | Usb->Rclass | Usb->Rinterface, 254, 0, 0, nil, buf) < 0) {
+		sys->print("usbmass: getmaxlun failed\n");
+		return -1;
+	}
+	return int buf[0];
+}
+
+#
+# CBW:
+#	sig[4]="USBC" tag[4] datalen[4] flags[1] lun[1] len[1] cmd[len]
+#
+sendcbw(dtl: int, outdir: int, lun: int, cmd: array of byte): int
+{
+	cbw := array [31] of byte;
+	cbw[0] = byte 'U';
+	cbw[1] = byte 'S';
+	cbw[2] = byte 'B';
+	cbw[3] = byte 'C';
+	usb->put4(cbw[4:], ++cbwseq);
+	usb->put4(cbw[8:], dtl);
+	if (outdir)
+		cbw[12] = byte RH2D;
+	else
+		cbw[12] = byte RD2H;
+	cbw[13] = byte lun;
+	cbw[14] = byte len cmd;
+	cbw[15:] = cmd;
+	rv := sys->write(outfd, cbw, len cbw);
+	if (rv < 0) {
+		sys->print("sendcbw: failed: %r\n");
+		return -1;
+	}
+	if (rv != len cbw) {
+		sys->print("sendcbw: truncated send\n");
+		return -1;
+	}
+	return 0;
+}
+
+#
+# CSW:
+#	sig[4]="USBS" tag[4] residue[4] status[1]
+#
+
+recvcsw(tag: int): (int, int)
+{
+	if(debug)
+		sys->print("recvcsw\n");
+	buf := array [13] of byte;
+	if (sys->read(infd, buf, len buf) != len buf) {
+		sys->print("recvcsw: read failed: %r\n");
+		return (-1, -1);
+	}
+	if (usb->get4(buf) != (('S'<<24)|('B'<<16)|('S'<<8)|'U')) {
+		sys->print("recvcsw: signature wrong\n");
+		return (-1, -1);
+	}
+	recvtag := usb->get4(buf[4:]);
+	if (recvtag != tag) {
+		sys->print("recvcsw: tag does not match: sent %d recved %d\n", tag, recvtag);
+		return (-1, -1);
+	}
+	residue := usb->get4(buf[8:]);
+	status := int buf[12];
+	if(debug)
+		sys->print("recvcsw: residue %d status %d\n", residue, status);
+	return (residue, status);
+}
+
+unstall(ep: ref Endpt)
+{
+	if(debug)
+		sys->print("unstalling bulk %x\n", ep.addr);
+	x := ep.addr & 16rF;
+	sys->fprint(ctlfd, "unstall %d", x);
+	sys->fprint(ctlfd, "data %d 0", x);
+	if (usb->setclear_feature(setupfd, Usb->Rendpt, ENDPOINT_STALL, ep.addr, 0) < 0) {
+		sys->print("unstall: clear_feature() failed: %r\n");
+		return;
+	}
+}
+
+warnfprint(fd: ref Sys->FD, s: string)
+{
+	if (sys->fprint(fd, "%s", s) != len s)
+		sys->print("warning: writing %s failed: %r\n", s);
+}
+
+bulkread(lun: int, cmd: array of byte, buf: array of byte, dump: int): int
+{
+	if (sendcbw(len buf, 0, lun, cmd) < 0)
+		return -1;
+	got := 0;
+	if (buf != nil) {
+		while (got < len buf) {
+			rv := sys->read(infd, buf[got:], len buf - got);
+			if (rv < 0) {
+				sys->print("bulkread: read failed: %r\n");
+				break;
+			}
+			if(debug)
+				sys->print("read %d\n", rv);
+			got += rv;
+			break;
+		}
+		if (dump) {
+			for (i := 0; i < got; i++)
+				sys->print("%.2ux", int buf[i]);
+			sys->print("\n");
+		}
+		if (got == 0)
+			unstall(inep);
+	}
+	(residue, status) := recvcsw(cbwseq);
+	if (residue < 0) {
+		unstall(inep);
+		(residue, status) = recvcsw(cbwseq);
+		if (residue < 0)
+			return -1;
+	}
+	if (status != 0)
+		return -1;
+	return got;
+}
+
+bulkwrite(lun: int, cmd: array of byte, buf: array of byte): int
+{
+	if (sendcbw(len buf, 1, lun, cmd) < 0)
+		return -1;
+	got := 0;
+	if (buf != nil) {
+		while (got < len buf) {
+			rv := sys->write(outfd, buf[got:], len buf - got);
+			if (rv < 0) {
+				sys->print("bulkwrite: write failed: %r\n");
+				break;
+			}
+			if(debug)
+				sys->print("write %d\n", rv);
+			got += rv;
+			break;
+		}
+		if (got == 0)
+			unstall(outep);
+	}
+	(residue, status) := recvcsw(cbwseq);
+	if (residue < 0) {
+		unstall(inep);
+		(residue, status) = recvcsw(cbwseq);
+		if (residue < 0)
+			return -1;
+	}
+	if (status != 0)
+		return -1;
+	return got;
+}
+
+scsiinquiry(lun: int): int
+{
+	buf := array [36] of byte;	# don't use 255, many devices can't cope
+	cmd := array [6] of byte;
+	cmd[0] = byte 16r12;
+	cmd[1] = 	byte (lun << 5);
+	cmd[2] = byte 0;
+	cmd[3] = byte 0;
+	cmd[4] = byte len buf;
+	cmd[5]  = byte 0;
+	got := bulkread(lun, cmd, buf, 0);
+	if (got < 0)
+		return -1;
+	if (got < 36) {
+		sys->print("scsiinquiry: too little data\n");
+		return -1;
+	}
+	t := int buf[0] & 16r1f;
+	if(debug)
+		sys->print("scsiinquiry: type %d/%s\n", t, string buf[8:35]);
+	if (t != 0)
+		return -1;
+	return 0;
+}
+
+scsireadcapacity(lun: int): int
+{
+#	warnfprint(ctlfd, "debug 0 1");
+#	warnfprint(ctlfd, "debug 1 1");
+#	warnfprint(ctlfd, "debug 2 1");
+	buf := array [8] of byte;
+	cmd := array [10] of byte;
+	cmd[0] = byte 16r25;
+	cmd[1] = 	byte (lun << 5);
+	cmd[2] = byte 0;
+	cmd[3] = byte 0;
+	cmd[4] = byte 0;
+	cmd[5]  = byte 0;
+	cmd[6]  = byte 0;
+	cmd[7]  = byte 0;
+	cmd[8]  = byte 0;
+	cmd[9] = byte 0;
+	got := bulkread(lun, cmd, buf, 0);
+	if (got < 0)
+		return -1;
+	if (got != len buf) {
+		sys->print("scsireadcapacity: returned data not right size\n");
+		return -1;
+	}
+	blocksize = usb->bigget4(buf[4:]);
+	lba := big usb->bigget4(buf[0:]) & 16rFFFFFFFF;
+	capacity = big blocksize * (lba+big 1);
+	if(debug)
+		sys->print("block size %d lba %bd cap %bd\n", blocksize, lba, capacity);
+	return 0;
+}
+
+scsirequestsense(lun: int): int
+{
+#	warnfprint(ctlfd, "debug 0 1");
+#	warnfprint(ctlfd, "debug 1 1");
+#	warnfprint(ctlfd, "debug 2 1");
+	buf := array [18] of byte;
+	cmd := array [6] of byte;
+	cmd[0] = byte 16r03;
+	cmd[1] = 	byte (lun << 5);
+	cmd[2] = byte 0;
+	cmd[3] = byte 0;
+	cmd[4] = byte len buf;
+	cmd[5]  = byte 0;
+	got := bulkread(lun, cmd, buf, 1);
+	if (got < 0)
+		return -1;
+	return 0;
+}
+
+scsiread10(lun: int, offset, count: int, buf: array of byte): int
+{
+	cmd := array [10] of byte;
+	cmd[0] = byte 16r28;
+	cmd[1] = byte (lun << 5);
+	usb->bigput4(cmd[2:], offset);
+	cmd[6] = byte 0;
+	usb->bigput2(cmd[7:], count);
+	cmd[9] = byte 0;
+	got := bulkread(lun, cmd, buf, 0);
+	if (got < 0)
+		return -1;
+	return 0;
+}
+
+scsiwrite10(lun: int, offset, count: int, buf: array of byte): int
+{
+	cmd := array [10] of byte;
+	cmd[0] = byte 16r2A;
+	cmd[1] = byte (lun << 5);
+	usb->bigput4(cmd[2:], offset);
+	cmd[6] = byte 0;
+	usb->bigput2(cmd[7:], count);
+	cmd[9] = byte 0;
+	got := bulkwrite(lun, cmd, buf);
+	if (got < 0)
+		return -1;
+	return 0;
+}
+
+scsistartunit(lun: int, start: int): int
+{
+#	warnfprint(ctlfd, "debug 0 1");
+#	warnfprint(ctlfd, "debug 1 1");
+#	warnfprint(ctlfd, "debug 2 1");
+	cmd := array [6] of byte;
+	cmd[0] = byte 16r1b;
+	cmd[1] = byte (lun << 5);
+	cmd[2] = byte 0;
+	cmd[3] = byte 0;
+	cmd[4] = byte (start & 1);
+	cmd[5]  = byte 0;
+	got := bulkread(lun, cmd, nil, 0);
+	if (got < 0)
+		return -1;
+	return 0;
+}
+
+init(usbmod: Usb, psetupfd, pctlfd: ref Sys->FD,
+	nil: ref Usb->Device,
+	conf: array of ref Usb->Configuration, path: string): int
+{
+	usb = usbmod;
+	setupfd = psetupfd;
+	ctlfd = pctlfd;
+
+	sys = load Sys Sys->PATH;
+	rv := usb->set_configuration(setupfd, conf[0].id);
+	if (rv < 0)
+		return rv;
+	rv = massstoragereset();
+	if (rv < 0)
+		return rv;
+	maxlun := getmaxlun();
+	if (maxlun < 0)
+		return maxlun;
+	lun = 0;
+	if(debug)
+		sys->print("maxlun %d\n", maxlun);
+	inep = outep = nil;
+	epts := (hd conf[0].iface[0].altiface).ep;
+	for(i := 0; i < len epts; i++)
+		if(epts[i].etype == Usb->Ebulk){
+			if(epts[i].d2h){
+				if(inep == nil)
+					inep = epts[i];
+			}else{
+				if(outep == nil)
+					outep = epts[i];
+			}
+		}
+	if(inep == nil || outep == nil){
+		sys->print("can't find endpoints\n");
+		return -1;
+	}
+	isrw := (inep.addr & 16rF) == (outep.addr & 16rF);
+	if(!isrw){
+		infd = openep(path, inep, Sys->OREAD);
+		if(infd == nil)
+			return -1;
+		outfd = openep(path, outep, Sys->OWRITE);
+		if(outfd == nil)
+			return -1;
+	}else{
+		infd = outfd = openep(path, inep, Sys->ORDWR);
+		if(infd == nil)
+			return -1;
+	}
+	if (scsiinquiry(0) < 0)
+		return -1;
+	scsistartunit(lun, 1);
+	if (scsireadcapacity(0) < 0) {
+		scsirequestsense(0);
+		if (scsireadcapacity(0) < 0)
+			return -1;
+	}
+	fileio := sys->file2chan("/chan", "usbdisk");
+	if (fileio == nil) {
+		sys->print("file2chan failed: %r\n");
+		return -1;
+	}
+	setlength("/chan/usbdisk", capacity);
+#	warnfprint(ctlfd, "debug 0 1");
+#	warnfprint(ctlfd, "debug 1 1");
+#	warnfprint(ctlfd, "debug 2 1");
+	pidc := chan of int;
+	spawn reader(pidc, fileio);
+	readerpid = <- pidc;
+	return 0;
+}
+
+shutdown()
+{
+	if (readerpid >= 0)
+		kill(readerpid);
+}
+
+openep(path: string, ep: ref Endpt, mode: int): ref Sys->FD
+{
+	if(debug)
+		sys->print("ep %x maxpkt %d interval %d\n", ep.addr, ep.maxpkt, ep.interval);
+	ms: string;
+	case mode {
+	Sys->OREAD => ms = "r";
+	Sys->OWRITE => ms = "w";
+	* => ms = "rw";
+	}
+	if(sys->fprint(ctlfd, "ep %d bulk %s %d 16", ep.addr&16rF, ms, ep.maxpkt) < 0)
+		return nil;
+	return sys->open(sys->sprint("%s/ep%ddata", path, ep.addr&16rF), mode);
+}
+
+setlength(f: string, size: big)
+{
+	d := sys->nulldir;
+	d.length = size;
+	sys->wstat(f, d);	# ignore errors since it fails on older kernels
+}
--- /dev/null
+++ b/appl/lib/usb/usbmct.b
@@ -1,0 +1,204 @@
+#
+# Copyright © 2002 Vita Nuova Holdings Limited
+#
+implement UsbDriver;
+
+# MCT RS232 USB driver
+# 'Documentation' mined from NetBSD
+
+include "sys.m";
+	sys: Sys;
+include "usb.m";
+	usb: Usb;
+
+UMCT_SET_REQUEST: con 1640;
+
+REQ_SET_BAUD_RATE: con 5;
+REQ_SET_LCR: con 7;
+
+LCR_SET_BREAK: con 16r40;
+LCR_PARITY_EVEN: con 16r18;
+LCR_PARITY_ODD: con 16r08;
+LCR_PARITY_NONE: con 16r00;
+LCR_DATA_BITS_5, LCR_DATA_BITS_6, LCR_DATA_BITS_7, LCR_DATA_BITS_8: con iota;
+LCR_STOP_BITS_2: con 16r04;
+LCR_STOP_BITS_1: con 16r00;
+
+setupfd: ref Sys->FD;
+debug: con 1;
+
+ioreaderpid, statusreaderpid: int;
+
+kill(pid: int): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if (fd == nil)
+		return -1;
+	if (sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+ioreader(pidc: chan of int, fd: ref Sys->FD)
+{
+	pid := sys->pctl(0, nil);
+	pidc <-= pid;
+	buf := array [256] of byte;
+	while ((n := sys->read(fd, buf, len buf)) >= 0)
+	{
+		sys->print("[%d]\n", n);
+		sys->write(sys->fildes(1), buf, n);
+	}
+	ioreaderpid = -1;
+}
+
+statusreader(pidc: chan of int, fd: ref Sys->FD)
+{
+	pid := sys->pctl(0, nil);
+	pidc <-= pid;
+	buf := array [2] of byte;
+	while ((n := sys->read(fd, buf, len buf)) >= 0)
+	{
+		sys->print("S(%d)%.2ux%.2ux\n", n, int buf[0], int buf[1]);
+	}
+	statusreaderpid = -1;
+}
+
+set_baud_rate(baud: int)
+{
+	buf := array [1] of byte;
+	val := 12;
+	case baud {
+	300 => val  = 1;
+	1200 => val  = 3;
+	2400 => val  = 4;
+	4800 => val  = 6;
+	9600 => val = 8;
+	19200 => val = 9;
+	38400 => val = 10;
+	57600 => val = 11;
+	115200 => val = 12;
+	}
+	buf[0] = byte val;
+	if (usb->setup(setupfd, UMCT_SET_REQUEST, REQ_SET_BAUD_RATE, 0, 0, buf, nil) < 0) {
+		if (debug)
+			sys->print("usbmct: set_baud_rate failed\n");
+	}
+}
+
+set_lcr(val: int)
+{
+	buf := array [1] of byte;
+	buf[0] = byte val;
+	if (usb->setup(setupfd, UMCT_SET_REQUEST, REQ_SET_LCR, 0, 0, buf, nil) < 0) {
+		if (debug)
+			sys->print("usbmct: set_lcr failed\n");
+	}
+}
+	
+init(usbmod: Usb, psetupfd, pctlfd: ref Sys->FD,
+	dev: ref Usb->Device,
+	conf: array of ref Usb->Configuration, path: string): int
+{
+	statusep, inep, outep: ref Usb->Endpt;
+	usb = usbmod;
+	sys = load Sys Sys->PATH;
+	setupfd = psetupfd;
+	# check the device descriptor to see if it really is an MCT doofer
+	if (dev.vid != 16r0711 || dev.did != 16r0230) {
+		if (debug)
+			sys->print("usbmct: wrong device!\n");
+		return -1;
+	}
+	usb->set_configuration(setupfd, conf[0].id);
+	ai := hd conf[0].iface[0].altiface;
+	statusep = nil;
+	inep = nil;
+	outep = nil;
+	for (e := 0; e < len ai.ep; e++) {
+		ep := ai.ep[e];
+		if ((ep.addr & 16r80) != 0 && (ep.attr & 3) == 3 && ep.maxpkt == 2)
+			statusep = ep;
+		else if ((ep.addr & 16r80) != 0 && (ep.attr & 3) == 3)
+			inep = ep;
+		else if ((ep.addr & 16r80) == 0 && (ep.attr & 3) == 2)
+			outep = ep;
+	}
+	if (statusep == nil || outep == nil || inep == nil) {
+		if (debug)
+			sys->print("usbmct: can't find sensible endpoints\n");
+		return -1;
+	}
+	if ((inep.addr & 15) != (outep.addr & 15)) {
+		if (debug)
+			sys->print("usbmct: in and out endpoints not same number\n");
+		return -1;
+	}
+	ioid := inep.addr & 15;
+	statusid := statusep.addr & 15;
+	if (debug)
+		sys->print("ep %d %d r %d 32\n", ioid, inep.maxpkt, inep.interval);
+	if (sys->fprint(pctlfd, "ep %d %d r %d 32", ioid, inep.maxpkt, inep.interval) < 0) {
+		if (debug)
+			sys->print("usbmct: can't create i/o endpoint (i)\n");
+		return -1;
+	}
+#	if (debug)
+#		sys->print("ep %d %d r bulk 32\n", ioid, inep.maxpkt);
+#	if (sys->fprint(pctlfd, "ep %d %d r bulk 32", ioid, inep.maxpkt) < 0) {
+#		if (debug)
+#			sys->print("usbmct: can't create i/o endpoint (i)\n");
+#		return -1;
+#	}
+	if (debug)
+		sys->print("ep %d %d w bulk 8\n", ioid, outep.maxpkt);
+	if (sys->fprint(pctlfd, "ep %d %d w bulk 8", ioid, outep.maxpkt) < 0) {
+		if (debug)
+			sys->print("usbmct: can't create i/o endpoint (o)\n");
+		return -1;
+	}
+	iofd := sys->open(path + "ep" + string ioid + "data", Sys->ORDWR);
+	if (iofd == nil) {
+		if (debug)
+			sys->print("usbmct: can't open i/o endpoint\n");
+		return -1;
+	}
+	if (debug)
+		sys->print("ep %d %d r %d 8\n", statusid, statusep.maxpkt, statusep.interval);
+	if (sys->fprint(pctlfd, "ep %d %d r %d 8", statusid, statusep.maxpkt, statusep.interval) < 0) {
+		if (debug)
+			sys->print("usbmct: can't create status endpoint\n");
+		return -1;
+	}
+	statusfd := sys->open(path + "ep" + string statusid + "data", Sys->ORDWR);
+	if (statusfd == nil) {
+		if (debug)
+			sys->print("usbmct: can't open status endpoint\n");
+		return -1;
+	}
+sys->print("setting baud rate\n");
+	set_baud_rate(9600);
+sys->print("setting lcr\n");
+	set_lcr(LCR_PARITY_NONE | LCR_DATA_BITS_8 | LCR_STOP_BITS_1);
+sys->print("launching reader\n");
+	pidc := chan of int;
+	spawn ioreader(pidc, iofd);
+	ioreaderpid = <- pidc;
+	spawn statusreader(pidc, statusfd);
+	statusreaderpid = <- pidc;
+	buf := array[512] of byte;
+	for (x := 0; x < 512; x += 16) {
+		buf[x:] = array of byte sys->sprint("%.2ux", x / 16);
+		buf[x + 2:] = array of byte "-0123456789-\r\n";
+	}
+	sys->write(iofd, buf, 512);
+	return 0;
+}
+
+shutdown()
+{
+	if (ioreaderpid >= 0)
+		kill(ioreaderpid);
+	if (statusreaderpid >= 0)
+		kill(statusreaderpid);
+}
--- /dev/null
+++ b/appl/lib/usb/usbmouse.b
@@ -1,0 +1,60 @@
+#
+# Copyright © 2002 Vita Nuova Holdings Limited.
+#
+implement UsbDriver;
+
+include "sys.m";
+	sys: Sys;
+include "usb.m";
+	usb: Usb;
+
+readerpid: int;
+
+kill(pid: int): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if (fd == nil)
+		return -1;
+	if (sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+reader(pidc: chan of int, fd: ref Sys->FD)
+{
+	pid := sys->pctl(0, nil);
+	pidc <-= pid;
+	buf := array [4] of byte;
+	while ((n := sys->read(fd, buf, len buf)) >= 0)
+		sys->print("%d: %d\n", sys->millisec(), n);
+	readerpid = -1;
+}
+	
+init(usbmod: Usb, setupfd, ctlfd: ref Sys->FD,
+	nil: ref Usb->Device,
+	conf: array of ref Usb->Configuration, path: string): int
+{
+	usb = usbmod;
+	sys = load Sys Sys->PATH;
+	rv := usb->set_configuration(setupfd, conf[0].id);
+	if (rv < 0)
+		return rv;
+	ep := (hd conf[0].iface[0].altiface).ep[0];
+	sys->print("maxpkt %d interval %d\n", ep.maxpkt, ep.interval);
+	rv = sys->fprint(ctlfd, "ep 1 %d r %d 32", ep.maxpkt, ep.interval);
+	if (rv < 0)
+		return rv;
+	datafd := sys->open(path + "ep1data", Sys->OREAD);
+	if (datafd == nil)
+		return -1;
+	pidc := chan of int;
+	spawn reader(pidc, datafd);
+	readerpid = <- pidc;
+	return 0;
+}
+
+shutdown()
+{
+	if (readerpid >= 0)
+		kill(readerpid);
+}
--- /dev/null
+++ b/appl/lib/utils.m
@@ -1,0 +1,112 @@
+Str_Hashtab : module
+{
+	PATH: con "/dis/lib/tcl_strhash.dis";
+	
+	H_link : adt{
+		name : string;
+		val : string;
+	};
+
+	Hash : adt {
+		size : int;
+		lsize : int;
+		tab : array of list of H_link;
+		insert : fn(h:self ref Hash,name,val: string) : int;
+		dump: fn(h:self ref Hash) : string;
+		find: fn(h:self ref Hash,name : string) : (int,string);
+		delete: fn(h:self ref Hash,name : string) : int;
+	};
+
+	alloc : fn(size : int) : ref Hash;
+};
+
+Int_Hashtab : module
+{
+	PATH: con "/dis/lib/tcl_inthash.dis";
+	
+	H_link : adt{
+		name : string;
+		val : int;
+	};
+
+	IHash : adt {
+		size : int;
+		tab : array of list of H_link;
+		insert : fn(h:self ref IHash,name: string,val : int) : int;
+		find: fn(h:self ref IHash,name : string) : (int,int);
+		delete: fn(h:self ref IHash,name : string) : int;
+	};
+
+	alloc : fn(size : int) : ref IHash;
+};
+
+Sym_Hashtab : module
+{
+	PATH: con "/dis/lib/tcl_symhash.dis";
+	
+	H_link : adt{
+		name : string;
+		alias : string;
+		val : int;
+	};
+
+	SHash : adt {
+		size : int;
+		tab : array of list of H_link;
+		insert : fn(h:self ref SHash,name,alias: string,val : int) : int;
+		find: fn(h:self ref SHash,name : string) : (int,int,string);
+		delete: fn(h:self ref SHash,name : string) : int;
+	};
+
+	alloc : fn(size : int) : ref SHash;
+};
+
+Mod_Hashtab : module
+{
+	PATH: con "/dis/lib/tcl_modhash.dis";
+	
+	H_link : adt{
+		name : string;
+		val : TclLib;
+	};
+
+	MHash : adt {
+		size : int;
+		tab : array of list of H_link;
+		insert : fn(h:self ref MHash,name: string,val : TclLib) 
+								: int;
+		dump: fn(h:self ref MHash) : string;
+		find: fn(h:self ref MHash,name : string) : (int,TclLib);
+		delete: fn(h:self ref MHash,name : string) : int;
+	};
+
+	alloc : fn(size : int) : ref MHash;
+};
+
+Tcl_Stack : module
+{
+	PATH: con "/dis/lib/tcl_stack.dis";
+	
+	level : fn() : int;
+	examine : fn(lev : int) : 
+	      (ref Str_Hashtab->Hash,array of (ref Str_Hashtab->Hash,string),ref Sym_Hashtab->SHash);
+	push  : fn(s:ref Str_Hashtab->Hash, 
+			a:array of (ref Str_Hashtab->Hash,string),t: ref Sym_Hashtab->SHash);
+	init : fn();
+	move : fn(lev :int) : int;
+	newframe : fn() : 
+	      (ref Str_Hashtab->Hash,array of (ref Str_Hashtab->Hash,string),ref Sym_Hashtab->SHash);
+	pop   : fn() : (ref Str_Hashtab->Hash,
+			array of (ref Str_Hashtab->Hash,string),ref Sym_Hashtab->SHash);
+	dump : fn();
+};
+
+
+
+Tcl_Utils : module
+{
+	PATH: con "/dis/lib/tcl_utils.dis";
+	break_it : fn(s : string) : array of string;
+	arr_resize : fn(argv : array of string) : array of string;
+};
+
--- /dev/null
+++ b/appl/lib/vac.b
@@ -1,0 +1,1012 @@
+implement Vac;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "venti.m";
+	venti: Venti;
+	Entrysize, Scoresize, Roottype, Dirtype, Pointertype0, Datatype: import venti;
+	Root, Entry, Score, Session: import venti;
+include "vac.m";
+
+
+dflag = 0;
+
+BIT8SZ:	con 1;
+BIT16SZ:        con 2;
+BIT32SZ:        con 4;
+BIT48SZ:        con 6;
+BIT64SZ:	con 8;
+
+Rootnamelen:	con 128;
+Rootversion:	con 2;
+Direntrymagic:	con 16r1c4d9072;
+Metablockmagic:	con 16r5656fc79;
+Maxstringsize: con 1000;
+
+blankroot: Root;
+blankentry: Entry;
+blankdirentry: Direntry;
+blankmetablock: Metablock;
+blankmetaentry: Metaentry;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	venti = load Venti Venti->PATH;
+	venti->init();
+}
+
+Direntry.new(): ref Direntry
+{
+	return ref Direntry(9, "", 0, 0, 0, 0, big 0, "", "", "", 0, 0, 0, 0, 0, 0, 0, big 0, big 0);
+}
+
+Direntry.mk(d: Sys->Dir): ref Direntry
+{
+	atime := 0; # d.atime;
+	mode := d.mode&Modeperm;
+	if(d.mode&sys->DMAPPEND)
+		mode |= Modeappend;
+	if(d.mode&sys->DMEXCL)
+		mode |= Modeexcl;
+	if(d.mode&sys->DMDIR)
+		mode |= Modedir;
+	if(d.mode&sys->DMTMP)
+		mode |= Modetemp;
+	return ref Direntry(9, d.name, 0, 0, 0, 0, d.qid.path, d.uid, d.gid, d.muid, d.mtime, 0, 0, atime, mode, d.mode, 0, big 0, big 0);
+}
+
+Direntry.mkdir(de: self ref Direntry): ref Sys->Dir
+{
+        d := ref sys->nulldir;
+        d.name = de.elem;
+        d.uid = de.uid;
+        d.gid = de.gid;
+        d.muid = de.mid;
+        d.qid.path = de.qid;
+        d.qid.vers = 0;
+        d.qid.qtype = de.emode>>24;
+        d.mode = de.emode;
+        d.atime = de.atime;
+        d.mtime = de.mtime;
+        d.length = big 0;
+        return d;
+}
+
+strlen(s: string): int
+{
+	return 2+len array of byte s;
+}
+
+Direntry.pack(de: self ref Direntry): array of byte
+{
+	if(de.version != 9) {
+		sys->werrstr("only version 9 supported");
+		return nil;
+	}
+		
+	length := 4+2+strlen(de.elem)+4+4+4+4+8+strlen(de.uid)+strlen(de.gid)+strlen(de.mid)+4+4+4+4+4;
+	if(de.qidspace)
+		length += 1+2+8+8;
+
+	d := array[length] of byte;
+	i := 0;
+	i = p32(d, i, Direntrymagic);
+	i = p16(d, i, de.version);
+	i = pstring(d, i, de.elem);
+	i = p32(d, i, de.entry);
+	if(de.version == 9) {
+		i = p32(d, i, de.gen);
+		i = p32(d, i, de.mentry);
+		i = p32(d, i, de.mgen);
+	}
+	i = p64(d, i, de.qid);
+	i = pstring(d, i, de.uid);
+	i = pstring(d, i, de.gid);
+	i = pstring(d, i, de.mid);
+	i = p32(d, i, de.mtime);
+	i = p32(d, i, de.mcount);
+	i = p32(d, i, de.ctime);
+	i = p32(d, i, de.atime);
+	i = p32(d, i, de.mode);
+	if(de.qidspace) {
+		d[i++] = byte DirQidspace;
+		i = p16(d, i, 16);
+		i = p64(d, i, de.qidoff);
+		i = p64(d, i, de.qidmax);
+	}
+	if(i != len d) {
+		sys->werrstr(sprint("bad length for direntry (expected %d, have %d)", len d, i));
+		return nil;
+	}
+	return d;
+}
+
+Direntry.unpack(d: array of byte): ref Direntry
+{
+	{
+		de := ref blankdirentry;
+		i := 0;
+		magic: int;
+		(magic, i) = eg32(d, i);
+		if(magic != Direntrymagic) {
+			sys->werrstr(sprint("bad magic (%x, want %x)", magic, Direntrymagic));
+			return nil;
+		}
+		(de.version, i) = eg16(d, i);
+		if(de.version != 8 && de.version != 9) {
+			sys->werrstr(sprint("bad version (%d)", de.version));
+			return nil;
+		}
+		(de.elem, i) = egstring(d, i);
+		(de.entry, i) = eg32(d, i);
+		case de.version {
+		8 =>
+			de.gen = 0;
+			de.mentry = de.entry+1;
+			de.mgen = 0;
+		9 =>
+			(de.gen, i) = eg32(d, i);
+			(de.mentry, i) = eg32(d, i);
+			(de.mgen, i) = eg32(d, i);
+		}
+		(de.qid, i) = eg64(d, i);
+		(de.uid, i) = egstring(d, i);
+		(de.gid, i) = egstring(d, i);
+		(de.mid, i) = egstring(d, i);
+		(de.mtime, i) = eg32(d, i);
+		(de.mcount, i) = eg32(d, i);
+		(de.ctime, i) = eg32(d, i);
+		(de.atime, i) = eg32(d, i);
+		(de.mode, i) = eg32(d, i);
+		de.emode = de.mode&Modeperm;
+		if(de.mode&Modeappend)
+			de.emode |= sys->DMAPPEND;
+		if(de.mode&Modeexcl)
+			de.emode |= sys->DMEXCL;
+		if(de.mode&Modedir)
+			de.emode |= sys->DMDIR;
+		if(de.mode&Modetemp)
+			de.emode |= sys->DMTMP;
+		while(i < len d) {
+			t := int d[i++];
+			n: int;
+			(n, i) = eg16(d, i);
+			case t {
+			DirQidspace =>
+				if(n != 16) {
+					sys->werrstr(sprint("invalid qidspace length %d", n));
+					return nil;
+				}
+				de.qidspace = 1;
+				(de.qidoff, i) = eg64(d, i);
+				(de.qidmax, i) = eg64(d, i);
+			* =>
+				# ignore other optional fields
+				i += n;
+			}
+		}
+		return de;
+	} exception e {
+	"too small:*" =>
+		sys->werrstr("direntry "+e);
+		return nil;
+	* =>
+		raise e;
+	}
+}
+
+
+Metablock.new(): ref Metablock
+{
+	return ref Metablock(0, 0, 0, 0);
+}
+
+Metablock.pack(mb: self ref Metablock, d: array of byte)
+{
+	i := 0;
+	i = p32(d, i, Metablockmagic);
+	i = p16(d, i, mb.size);
+	i = p16(d, i, mb.free);
+	i = p16(d, i, mb.maxindex);
+	i = p16(d, i, mb.nindex);
+}
+
+Metablock.unpack(d: array of byte): ref Metablock
+{
+	if(len d < Metablocksize) {
+		sys->werrstr(sprint("bad length for metablock (%d, want %d)", len d, Metablocksize));
+		return nil;
+	}
+	i := 0;
+	magic := g32(d, i);
+	if(magic != Metablockmagic && magic != Metablockmagic+1) {
+		sys->werrstr(sprint("bad magic for metablock (%x, need %x)", magic, Metablockmagic));
+		return nil;
+	}
+	i += BIT32SZ;
+
+	mb := ref blankmetablock;
+	mb.size = g16(d, i);
+	i += BIT16SZ;
+	mb.free = g16(d, i);
+	i += BIT16SZ;
+	mb.maxindex = g16(d, i);
+	i += BIT16SZ;
+	mb.nindex = g16(d, i);
+	i += BIT16SZ;
+	if(mb.nindex == 0) {
+		sys->werrstr("bad metablock, nindex=0");
+		return nil;
+	}
+	return mb;
+}
+
+Metaentry.pack(me: self ref Metaentry, d: array of byte)
+{
+	i := 0;
+	i = p16(d, i, me.offset);
+	i = p16(d, i, me.size);
+}
+
+Metaentry.unpack(d: array of byte, i: int): ref Metaentry
+{
+	o := Metablocksize+i*Metaentrysize;
+	if(o+Metaentrysize > len d) {
+		sys->werrstr(sprint("meta entry lies outside meta block, i=%d", i));
+		return nil;
+	}
+
+	me := ref blankmetaentry;
+	me.offset = g16(d, o);
+	o += BIT16SZ;
+	me.size = g16(d, o);
+	o += BIT16SZ;
+	if(me.offset+me.size > len d) {
+		sys->werrstr(sprint("meta entry points outside meta block, i=%d", i));
+		return nil;
+	}
+	return me;
+}
+
+
+Page.new(dsize: int): ref Page
+{
+	psize := (dsize/Scoresize)*Scoresize;
+	return ref Page(array[psize] of byte, 0);
+}
+
+Page.add(p: self ref Page, s: Score)
+{
+	for(i := 0; i < Scoresize; i++)
+		p.d[p.o+i] = s.a[i];
+	p.o += Scoresize;
+}
+
+Page.full(p: self ref Page): int
+{
+	return p.o+Scoresize > len p.d;
+}
+
+Page.data(p: self ref Page): array of byte
+{
+	for(i := p.o; i >= Scoresize; i -= Scoresize)
+		if(!Score(p.d[i-Scoresize:i]).eq(Score.zero()))
+			break;
+	return p.d[:i];
+}
+
+
+File.new(s: ref Session, dtype, dsize: int): ref File
+{
+	p := array[1] of ref Page;
+	p[0] = Page.new(dsize);
+	return ref File(p, dtype, dsize, big 0, s);
+}
+
+fflush(f: ref File, last: int): (int, ref Entry)
+{
+	for(i := 0; i < len f.p; i++) {
+		if(!last && !f.p[i].full())
+			return (0, nil);
+		if(last && f.p[i].o == Scoresize) {
+			flags := venti->Entryactive;
+			if(f.dtype == Dirtype)
+				flags |= venti->Entrydir;
+			flags |= i<<venti->Entrydepthshift;
+			score := Score(f.p[i].data());
+			if(len score.a == 0)
+				score = Score.zero();
+			return (0, ref Entry(0, len f.p[i].d, f.dsize, i, flags, f.size, score));
+		}
+		(ok, score) := f.s.write(Pointertype0+i, f.p[i].data());
+		if(ok < 0)
+			return (-1, nil);
+		f.p[i] = Page.new(f.dsize);
+		if(i+1 == len f.p) {
+			newp := array[len f.p+1] of ref Page;
+			newp[:] = f.p;
+			newp[len newp-1] = Page.new(f.dsize);
+			f.p = newp;
+		}
+		f.p[i+1].add(score);
+	}
+	sys->werrstr("internal error in fflush");
+	return (-1, nil);
+}
+
+File.write(f: self ref File, d: array of byte): int
+{
+	(fok, nil) := fflush(f, 0);
+	if(fok < 0)
+		return -1;
+	length := len d;
+	for(i := len d; i > 0; i--)
+		if(d[i-1] != byte 0)
+			break;
+	d = d[:i];
+	(ok, score) := f.s.write(f.dtype, d);
+	if(ok < 0)
+		return -1;
+	f.size += big length;
+	f.p[0].add(score);
+	return 0;
+}
+
+File.finish(f: self ref File): ref Entry
+{
+	(ok, e) := fflush(f, 1);
+	if(ok < 0)
+		return nil;
+	return e;
+}
+
+
+Sink.new(s: ref Venti->Session, dsize: int): ref Sink
+{
+	dirdsize := (dsize/Entrysize)*Entrysize;
+	return ref Sink(File.new(s, Dirtype, dsize), array[dirdsize] of byte, 0, 0);
+}
+
+Sink.add(m: self ref Sink, e: ref Entry): int
+{
+	ed := e.pack();
+	if(ed == nil)
+		return -1;
+	n := len m.d - m.nd;
+	if(n > len ed)
+		n = len ed;
+	m.d[m.nd:] = ed[:n];
+	m.nd += n;
+	if(n < len ed) {
+		if(m.f.write(m.d) < 0)
+			return -1;
+		m.nd = len ed - n;
+		m.d[:] = ed[n:];
+	}
+	return m.ne++;
+}
+
+Sink.finish(m: self ref Sink): ref Entry
+{
+	if(m.nd > 0)
+		if(m.f.write(m.d[:m.nd]) < 0)
+			return nil;
+	e := m.f.finish();
+	e.dsize = len m.d;
+	return e;
+}
+
+
+elemcmp(a, b: array of byte, fossil: int): int
+{
+	for(i := 0; i < len a && i < len b; i++)
+		if(a[i] != b[i])
+			return (int a[i] - int b[i]);
+	if(fossil)
+		return len a - len b;
+	return len b - len a;
+}
+
+Mentry.cmp(a, b: ref Mentry): int
+{
+	return elemcmp(array of byte a.elem, array of byte b.elem, 0);
+}
+
+MSink.new(s: ref Venti->Session, dsize: int): ref MSink
+{
+	return ref MSink(File.new(s, Datatype, dsize), array[dsize] of byte, 0, nil);
+}
+
+l2a[T](l: list of T): array of T
+{
+	a := array[len l] of T;
+	i := 0;
+	for(; l != nil; l = tl l)
+		a[i++] = hd l;
+	return a;
+}
+
+insertsort[T](a: array of T)
+	for { T =>	cmp:	fn(a, b: T): int; }
+{
+	for(i := 1; i < len a; i++) {
+		tmp := a[i];
+		for(j := i; j > 0 && T.cmp(a[j-1], tmp) > 0; j--)
+			a[j] = a[j-1];
+		a[j] = tmp;
+	}
+}
+
+mflush(m: ref MSink, last: int): int
+{
+	d := array[len m.de] of byte;
+
+	me := l2a(m.l);
+	insertsort(me);
+	o := Metablocksize;
+	deo := o+len m.l*Metaentrysize;
+	for(i := 0; i < len me; i++) {
+		me[i].me.offset += deo;
+		me[i].me.pack(d[o:]);
+		o += Metaentrysize;
+	}
+	d[o:] = m.de[:m.nde];
+	o += m.nde;
+	if(!last)
+		while(o < len d)
+			d[o++] = byte 0;
+
+	mb := Metablock.new();
+	mb.nindex = len m.l;
+	mb.maxindex = mb.nindex;
+	mb.free = 0;
+	mb.size = o;
+	mb.pack(d);
+
+	if(m.f.write(d[:o]) < 0)
+		return -1;
+	m.nde = 0;
+	m.l = nil;
+	return 0;
+}
+
+MSink.add(m: self ref MSink, de: ref Direntry): int
+{
+	d := de.pack();
+	if(d == nil)
+		return -1;
+if(dflag) say(sprint("msink: adding direntry, length %d", len d));
+	if(Metablocksize+len m.l*Metaentrysize+m.nde + Metaentrysize+len d > len m.de)
+		if(mflush(m, 0) < 0)
+			return -1;
+	m.de[m.nde:] = d;
+	m.l = ref Mentry(de.elem, ref Metaentry(m.nde, len d))::m.l;
+	m.nde += len d;
+	return 0;
+}
+
+MSink.finish(m: self ref MSink): ref Entry
+{
+	if(m.nde > 0)
+		mflush(m, 1);
+	return m.f.finish();
+}
+
+Source.new(s: ref Session, e: ref Entry): ref Source
+{
+	dsize := e.dsize;
+	if(e.flags&venti->Entrydir)
+		dsize = Entrysize*(dsize/Entrysize);
+	return ref Source(s, e, dsize);
+}
+
+power(b, e: int): big
+{
+	r := big 1;
+	while(e-- > 0)
+		r *= big b;
+	return r;
+}
+
+blocksize(e: ref Entry): int
+{
+	if(e.psize > e.dsize)
+		return e.psize;
+	return e.dsize;
+}
+
+Source.get(s: self ref Source, i: big, d: array of byte): int
+{
+	npages := (s.e.size+big (s.dsize-1))/big s.dsize;
+	if(i*big s.dsize >= s.e.size)
+		return 0;
+
+	want := s.dsize;
+	if(i == npages-big 1)
+		want = int (s.e.size - i*big s.dsize);
+	last := s.e.score;
+	bsize := blocksize(s.e);
+	buf: array of byte;
+
+	npp := s.e.psize/Scoresize;	# scores per pointer block
+	np := power(npp, s.e.depth-1);	# blocks referenced by score at this depth
+	for(depth := s.e.depth; depth >= 0; depth--) {
+		dtype := Pointertype0+depth-1;
+		if(depth == 0) {
+			dtype = Datatype;
+			if(s.e.flags & venti->Entrydir)
+				dtype = Dirtype;
+			bsize = want;
+		}
+		buf = s.session.read(last, dtype, bsize);
+		if(buf == nil)
+			return -1;
+		if(depth > 0) {
+			pi := int (i / np);
+			i %= np;
+			np /= big npp;
+			o := (pi+1)*Scoresize;
+			if(o <= len buf)
+				last = Score(buf[o-Scoresize:o]);
+			else
+				last = Score.zero();
+		}
+	}
+	for(j := len buf; j < want; j++)
+		d[j] = byte 0;
+	d[:] = buf;
+	return want;
+}
+
+
+Vacfile.mk(s: ref Source): ref Vacfile
+{
+	return ref Vacfile(s, big 0);
+}
+
+Vacfile.new(s: ref Session, e: ref Entry): ref Vacfile
+{
+	return Vacfile.mk(Source.new(s, e));
+}
+
+Vacfile.seek(v: self ref Vacfile, offset: big): big
+{
+	v.o += offset;
+	if(v.o > v.s.e.size)
+		v.o = v.s.e.size;
+	return v.o;
+}
+
+Vacfile.read(v: self ref Vacfile, d: array of byte, n: int): int
+{
+	have := v.pread(d, n, v.o);
+	if(have > 0)
+		v.o += big have;
+	return have;
+}
+
+Vacfile.pread(v: self ref Vacfile, d: array of byte, n: int, offset: big): int
+{
+	dsize := v.s.dsize;
+if(dflag) say(sprint("vf.preadn, len d %d, n %d, offset %bd", len d, n, offset));
+	have := v.s.get(big (offset/big dsize), buf := array[dsize] of byte);
+	if(have <= 0)
+		return have;
+if(dflag) say(sprint("vacfile.pread: have=%d dsize=%d", have, dsize));
+	o := int (offset % big dsize);
+	have -= o;
+	if(have > n)
+		have = n;
+	if(have <= 0)
+		return 0;
+	d[:] = buf[o:o+have];
+	return have;
+}
+
+
+Vacdir.mk(vf: ref Vacfile, ms: ref Source): ref Vacdir
+{
+	return ref Vacdir(vf, ms, big 0, 0);
+}
+
+Vacdir.new(session: ref Session, e, me: ref Entry): ref Vacdir
+{
+        vf := Vacfile.new(session, e);
+        ms := Source.new(session, me);
+        return Vacdir.mk(vf, ms);
+
+}
+
+mecmp(d: array of byte, i: int, elem: string, fromfossil: int): (int, int)
+{
+	me := Metaentry.unpack(d, i);
+	if(me == nil)
+		return (0, 1);
+	o := me.offset+6;
+	n := g16(d, o);
+	o += BIT16SZ;
+	if(o+n > len d) {
+		sys->werrstr("bad elem in direntry");
+		return (0, 1);
+	}
+	return (elemcmp(d[o:o+n], array of byte elem, fromfossil), 0);
+}
+
+finddirentry(d: array of byte, elem: string): (int, ref Direntry)
+{
+	mb := Metablock.unpack(d);
+	if(mb == nil)
+		return (-1, nil);
+	fromfossil := g32(d, 0) == Metablockmagic+1;
+
+        left := 0;
+        right := mb.nindex;
+	while(left+1 != right) {
+                mid := (left+right)/2;
+		(c, err) := mecmp(d, mid, elem, fromfossil);
+		if(err)
+			return (-1, nil);
+		if(c <= 0)
+			left = mid;
+		else
+			right = mid;
+		if(c == 0)
+			break;
+        }
+	de := readdirentry(d, left, 0);
+	if(de != nil && de.elem == elem)
+		return (1, de);
+	return (0, nil);
+}
+
+Vacdir.walk(v: self ref Vacdir, elem: string): ref Direntry
+{
+	i := big 0;
+	for(;;) {
+		n := v.ms.get(i, buf := array[v.ms.e.dsize] of byte);
+		if(n < 0)
+			return nil;
+		if(n == 0)
+			break;
+		(ok, de) := finddirentry(buf[:n], elem);
+		if(ok < 0)
+			return nil;
+		if(de != nil)
+			return de;
+		i++;
+	}
+	sys->werrstr(sprint("no such file or directory"));
+	return nil;
+}
+
+vfreadentry(vf: ref Vacfile, entry: int): ref Entry
+{
+if(dflag) say(sprint("vfreadentry: reading entry=%d", entry));
+	ebuf := array[Entrysize] of byte;
+	n := vf.pread(ebuf, len ebuf, big entry*big Entrysize);
+	if(n < 0)
+		return nil;
+	if(n != len ebuf) {
+		sys->werrstr(sprint("bad archive, entry=%d not present (read %d, wanted %d)", entry, n, len ebuf));
+		return nil;
+	}
+	e := Entry.unpack(ebuf);
+	if(e == nil)
+		return nil;
+	if(~e.flags&venti->Entryactive) {
+		sys->werrstr("entry not active");
+		return nil;
+	}
+	# p9p writes archives with Entrylocal set?
+	if(0 && e.flags&venti->Entrylocal) {
+		sys->werrstr("entry is local");
+		return nil;
+	}
+if(dflag) say(sprint("vreadentry: have entry, score=%s", e.score.text()));
+	return e;
+}
+
+Vacdir.open(vd: self ref Vacdir, de: ref Direntry): (ref Entry, ref Entry)
+{
+if(dflag) say(sprint("vacdir.open: opening entry=%d", de.entry));
+	e := vfreadentry(vd.vf, de.entry);
+	if(e == nil)
+		return (nil, nil);
+	isdir1 := de.mode & Modedir;
+	isdir2 := e.flags & venti->Entrydir;
+	if(isdir1 && !isdir2 || !isdir1 && isdir2) {
+		sys->werrstr("direntry directory bit does not match entry directory bit");
+		return (nil, nil);
+	}
+if(dflag) say(sprint("vacdir.open: have entry, score=%s size=%bd", e.score.text(), e.size));
+	me: ref Entry;
+	if(de.mode&Modedir) {
+		me = vfreadentry(vd.vf, de.mentry);
+		if(me == nil)
+			return (nil, nil);
+if(dflag) say(sprint("vacdir.open: have mentry, score=%s size=%bd", me.score.text(), e.size));
+	}
+	return (e, me);
+}
+
+readdirentry(buf: array of byte, i: int, allowroot: int): ref Direntry
+{
+	me := Metaentry.unpack(buf, i);
+	if(me == nil)
+		return nil;
+	o := me.offset;
+	de := Direntry.unpack(buf[o:o+me.size]);
+	if(de == nil)
+		return nil;
+	if(badelem(de.elem) && !(allowroot && de.elem == "/")) {
+		sys->werrstr(sprint("bad direntry: %s", de.elem));
+		return nil;
+	}
+	return de;
+}
+	
+has(c: int, s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return 1;
+	return 0;
+}
+
+badelem(elem: string): int
+{
+	return elem == "" || elem == "." || elem == ".." || has('/', elem) || has(0, elem);
+}
+
+vdreaddir(vd: ref Vacdir, allowroot: int): (int, ref Direntry)
+{
+if(dflag) say(sprint("vdreaddir: ms.e.size=%bd vd.p=%bd vd.i=%d", vd.ms.e.size, vd.p, vd.i));
+	dsize := vd.ms.dsize;
+	n := vd.ms.get(vd.p, buf := array[dsize] of byte);
+	if(n <= 0)
+		return (n, nil);
+if(dflag) say(sprint("vdreaddir: have buf, length=%d e.size=%bd", n, vd.ms.e.size));
+	mb := Metablock.unpack(buf);
+	if(mb == nil)
+		return (-1, nil);
+	de := readdirentry(buf, vd.i, allowroot);
+	if(de == nil)
+		return (-1, nil);
+	vd.i++;
+	if(vd.i >= mb.nindex) {
+		vd.p++;
+		vd.i = 0;
+	}
+if(dflag) say("vdreaddir: have entry");
+	return (1, de);
+}
+
+Vacdir.readdir(vd: self ref Vacdir): (int, ref Direntry)
+{
+	return vdreaddir(vd, 0);
+}
+
+
+Vacdir.rewind(vd: self ref Vacdir)
+{
+	vd.p = big 0;
+	vd.i = 0;
+}
+
+
+vdroot(session: ref Session, score: Venti->Score): (ref Vacdir, ref Direntry, string)
+{
+	d := session.read(score, venti->Roottype, venti->Rootsize);
+	if(d == nil)
+		return (nil, nil, sprint("reading vac score: %r"));
+	r := Root.unpack(d);
+	if(r == nil)
+		return (nil, nil, sprint("bad vac root block: %r"));
+	topscore := r.score;
+
+	d = session.read(topscore, Dirtype, 3*Entrysize);
+	if(d == nil)
+		return (nil, nil, sprint("reading rootdir score: %r"));
+	if(len d != 3*Entrysize) {
+		if(len d % Entrysize != 0 && len d == 2*Entrysize != 0)	# what's in the second 40 bytes?  looks like 2nd 20 bytes of it is zero score
+			return (nil, nil, sprint("bad fossil rootdir, have %d bytes, need %d or %d", len d, Entrysize, 2*Entrysize));
+		e := Entry.unpack(d[:Entrysize]);
+		if(e == nil)
+			return (nil, nil, sprint("unpacking fossil top-level entry: %r"));
+		topscore = e.score;
+		d = session.read(topscore, Dirtype, 3*Entrysize);
+		if(d == nil)
+			return (nil, nil, sprint("reading fossil rootdir block: %r"));
+	}
+
+	e := array[3] of ref Entry;
+	j := 0;
+	for(i := 0; i+Entrysize <= len d; i += Entrysize) {
+		e[j] = Entry.unpack(d[i:i+Entrysize]);
+		if(e[j] == nil)
+			return (nil, nil, sprint("reading root entry %d: %r", j));
+		j++;
+	}
+if(dflag) say("top entries unpacked");
+
+	mroot := Vacdir.mk(nil, Source.new(session, e[2]));
+	(ok, de) := vdreaddir(mroot, 1);
+	if(ok <= 0)
+		return (nil, nil, sprint("reading root meta entry: %r"));
+
+	return (Vacdir.new(session, e[0], e[1]), de, nil);
+}
+
+
+checksize(n: int): int
+{
+	if(n < 256 || n > Venti->Maxlumpsize) {
+		sys->werrstr("bad block size");
+		return 0;
+	}
+	return 1;
+}
+
+
+gstring(a: array of byte, o: int): (string, int)
+{
+	if(o < 0 || o+BIT16SZ > len a)
+		return (nil, -1);
+	l := (int a[o] << 8) | int a[o+1];
+	if(l > Maxstringsize)
+		return (nil, -1);
+	o += BIT16SZ;
+	e := o+l;
+	if(e > len a)
+		return (nil, -1);
+	return (string a[o:e], e);
+}
+
+gtstring(a: array of byte, o: int, n: int): string
+{
+	e := o + n;
+	if(e > len a)
+		return nil;
+	for(i := o; i < e; i++)
+		if(a[i] == byte 0)
+			break;
+	return string a[o:i];
+}
+
+gscore(f: array of byte, i: int): Score
+{
+	s := Score(array[Scoresize] of byte);
+	s.a[0:] = f[i:i+Scoresize];
+	return s;
+}
+
+g16(f: array of byte, i: int): int
+{
+	return (int f[i] << 8) | int f[i+1];
+}
+
+g32(f: array of byte, i: int): int
+{
+	return (((((int f[i+0] << 8) | int f[i+1]) << 8) | int f[i+2]) << 8) | int f[i+3];
+}
+
+g48(f: array of byte, i: int): big
+{
+	return big g16(f, i)<<32 | (big g32(f, i+2) & 16rFFFFFFFF);
+}
+
+g64(f: array of byte, i: int): big
+{
+	return big g32(f, i)<<32 | (big g32(f, i+4) & 16rFFFFFFFF);
+}
+
+p16(d: array of byte, i: int, v: int): int
+{
+	d[i+0] = byte (v>>8);
+	d[i+1] = byte v;
+	return i+BIT16SZ;
+}
+
+p32(d: array of byte, i: int, v: int): int
+{
+	p16(d, i+0, v>>16);
+	p16(d, i+2, v);
+	return i+BIT32SZ;
+}
+
+p48(d: array of byte, i: int, v: big): int
+{
+	p16(d, i+0, int (v>>32));
+	p32(d, i+2, int v);
+	return i+BIT48SZ;
+}
+
+p64(d: array of byte, i: int, v: big): int
+{
+	p32(d, i+0, int (v>>32));
+	p32(d, i+4, int v);
+	return i+BIT64SZ;
+}
+
+pstring(a: array of byte, o: int, s: string): int
+{
+	sa := array of byte s;	# could do conversion ourselves
+	n := len sa;
+	a[o] = byte (n >> 8);
+	a[o+1] = byte n;
+	a[o+2:] = sa;
+	return o+BIT16SZ+n;
+}
+
+ptstring(d: array of byte, i: int, s: string, l: int): int
+{
+	a := array of byte s;
+	if(len a > l) {
+		sys->werrstr("string too long: "+s);
+		return -1;
+	}
+	for(j := 0; j < len a; j++)
+		d[i+j] = a[j];
+	while(j < l)
+		d[i+j++] = byte 0;
+	return i+l;
+}
+
+pscore(d: array of byte, i: int, s: Score): int
+{
+	for(j := 0; j < Scoresize; j++)
+		d[i+j] = s.a[j];
+	return i+Scoresize;
+}
+
+echeck(f: array of byte, i: int, l: int)
+{
+	if(i+l > len f)
+		raise sprint("too small: buffer length is %d, requested %d bytes starting at offset %d", len f, l, i);
+}
+
+egscore(f: array of byte, i: int): (Score, int)
+{
+	echeck(f, i, Scoresize);
+	return (gscore(f, i), i+Scoresize);
+}
+
+egstring(a: array of byte, o: int): (string, int)
+{
+	(s, no) := gstring(a, o);
+	if(no == -1)
+		raise sprint("too small: string runs outside buffer (length %d)", len a);
+	return (s, no);
+}
+
+eg16(f: array of byte, i: int): (int, int)
+{
+	echeck(f, i, BIT16SZ);
+	return (g16(f, i), i+BIT16SZ);
+}
+
+eg32(f: array of byte, i: int): (int, int)
+{
+	echeck(f, i, BIT32SZ);
+	return (g32(f, i), i+BIT32SZ);
+}
+
+eg48(f: array of byte, i: int): (big, int)
+{
+	echeck(f, i, BIT48SZ);
+	return (g48(f, i), i+BIT48SZ);
+}
+
+eg64(f: array of byte, i: int): (big, int)
+{
+	echeck(f, i, BIT64SZ);
+	return (g64(f, i), i+BIT64SZ);
+}
+
+say(s: string)
+{
+	if(dflag)
+		sys->fprint(sys->fildes(2), "%s\n", s);
+}
--- /dev/null
+++ b/appl/lib/venti.b
@@ -1,0 +1,757 @@
+implement Venti;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "venti.m";
+
+BIT8SZ:	con 1;
+BIT16SZ:	con 2;
+BIT32SZ:	con 4;
+BIT48SZ:	con 6;
+SCORE:	con 20;
+STR:		con BIT16SZ;
+H: con BIT16SZ+BIT8SZ+BIT8SZ;		# minimum header length: size[2] op[1] tid[1]
+Rootnamelen: con 128;
+
+versions := array[] of {"02"};
+
+blankroot: Root;
+blankentry: Entry;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+hdrlen := array[Tmax] of {
+Rerror =>	H+STR,							# size[2] Rerror tid[1] error[s]
+Tping =>	H,								# size[2] Tping tid[1]
+Rping => 	H,								# size[2] Rping tid[1]
+Thello =>	H+STR+STR+BIT8SZ+BIT8SZ+BIT8SZ,	# size[2] Thello tid[1] version[s] uid[s] crypto[1] cryptos[n] codecs[n]
+Rhello =>	H+STR+BIT8SZ+BIT8SZ,				# size[2] Rhello tid[1] sid[s] crypto[1] codec[1]
+Tgoodbye => H,							# size[2] Tgoodbye tid[1]
+Tread =>	H+SCORE+BIT8SZ+BIT8SZ+BIT16SZ,	# size[2] Tread tid[1] score[20] type[1] pad[1] n[2]
+Rread => H,								# size[2] Rread tid[1] data
+Twrite => H+BIT8SZ+3,						# size[2] Twrite tid[1] type[1] pad[3]
+Rwrite => H+SCORE,							# size[2] Rwrite tid[1] score[20
+Tsync => H,								# size[2] Tsync tid[1]
+Rsync => H,								# size[2] Rsync tid[1]
+};
+
+tag2type := array[] of {
+tagof Vmsg.Rerror => Rerror,
+tagof Vmsg.Tping => Tping,
+tagof Vmsg.Rping => Rping,
+tagof Vmsg.Thello => Thello,
+tagof Vmsg.Rhello => Rhello,
+tagof Vmsg.Tgoodbye => Tgoodbye,
+tagof Vmsg.Tread => Tread,
+tagof Vmsg.Rread => Rread,
+tagof Vmsg.Twrite => Twrite,
+tagof Vmsg.Rwrite => Rwrite,
+tagof Vmsg.Tsync => Tsync,
+tagof Vmsg.Rsync => Rsync,
+};
+
+msgname := array[] of {
+tagof Vmsg.Rerror => "Rerror",
+tagof Vmsg.Tping => "Tping",
+tagof Vmsg.Rping => "Rping",
+tagof Vmsg.Thello => "Thello",
+tagof Vmsg.Rhello => "Rhello",
+tagof Vmsg.Tgoodbye => "Tgoodbye",
+tagof Vmsg.Tread => "Tread",
+tagof Vmsg.Rread => "Rread",
+tagof Vmsg.Twrite => "Twrite",
+tagof Vmsg.Rwrite => "Rwrite",
+tagof Vmsg.Tsync => "Tsync",
+tagof Vmsg.Rsync => "Rsync",
+};
+
+zero := array[] of {
+	byte 16rda, byte 16r39, byte 16ra3, byte 16ree, byte 16r5e,
+	byte 16r6b, byte 16r4b, byte 16r0d, byte 16r32, byte 16r55,
+	byte 16rbf, byte 16ref, byte 16r95, byte 16r60, byte 16r18,
+	byte 16r90, byte 16raf, byte 16rd8, byte 16r07, byte 16r09
+};
+	
+
+Vmsg.read(fd: ref Sys->FD): (ref Vmsg, string)
+{
+	(msg, err) := readmsg(fd);
+	if(err != nil)
+		return (nil, err);
+	if(msg == nil)
+		return (nil, "eof reading message");
+	(nil, m) := Vmsg.unpack(msg);
+	if(m == nil)
+		return (nil, sys->sprint("bad venti message format: %r"));
+	return (m, nil);
+}
+
+Vmsg.unpack(f: array of byte): (int, ref Vmsg)
+{
+	if(len f < H) {
+		sys->werrstr("message too small");
+		return (0, nil);
+	}
+	size := (int f[0] << 8) | int f[1];		# size does not include self
+	size += BIT16SZ;
+	if(len f != size){
+		if(len f < size){
+			sys->werrstr("need more data");
+			return (0, nil);		# need more data
+		}
+		f = f[0:size];			# trim to exact length
+	}
+	mtype := int f[2];
+	if(mtype >= len hdrlen || size < hdrlen[mtype]){
+		sys->werrstr("mtype out of range");
+		return (-1, nil);
+	}
+	tid := int f[3];
+	m: ref Vmsg;
+	case mtype {
+	Thello =>
+		uid: string;
+		cryptos, codecs: array of byte;
+
+		(version, o) := gstring(f, H);
+		(uid, o) = gstring(f, o);
+		if(o < 0 || o >= len f)
+			break;
+		cryptostrength := int f[o++];
+		(cryptos, o) = gbytes(f, o);
+		(codecs, o) = gbytes(f, o);
+		if(o != len f)
+			break;
+		m = ref Vmsg.Thello(1, tid, version, uid, cryptostrength, cryptos, codecs);
+	Tping =>
+		m = ref Vmsg.Tping(1, tid);
+	Tgoodbye =>
+		m = ref Vmsg.Tgoodbye(1, tid);
+	Tread =>
+		score := Score(f[H:H+SCORE]);
+		etype := int f[H+SCORE];
+		n := (int f[H+SCORE+2] << 8) | int f[H+SCORE+3];
+		m = ref Vmsg.Tread(1, tid, score, etype, n);
+	Twrite =>
+		etype := int f[H];
+		m = ref Vmsg.Twrite(1, tid, etype, f[H+4:]);
+	Tsync =>
+		m = ref Vmsg.Tsync(1, tid);
+	Rhello =>
+		(sid, o) := gstring(f, H);
+		if(o+2 != len f)
+			break;
+		crypto := int f[o++];
+		codec := int f[o++];
+		m = ref Vmsg.Rhello(0, tid, sid, crypto, codec);
+	Rping =>
+		m = ref Vmsg.Rping(0, tid);
+	Rread =>
+		m = ref Vmsg.Rread(0, tid, f[H:]);
+	Rwrite =>
+		m = ref Vmsg.Rwrite(0, tid, Score(f[H:H+SCORE]));
+	Rsync =>
+		m = ref Vmsg.Rsync(0, tid);
+	Rerror =>
+		(err, o) := gstring(f, H);
+		if(o < 0)
+			break;
+		m = ref Vmsg.Rerror(0, tid, err);
+	* =>
+		sys->werrstr("unrecognised mtype " + string mtype);
+		return (-1, nil);
+	}
+	if(m == nil) {
+		sys->werrstr("bad message size");
+		return (-1, nil);
+	}
+	return (size, m);
+}
+
+Vmsg.pack(gm: self ref Vmsg): array of byte
+{
+	if(gm == nil)
+		return nil;
+	ds := gm.packedsize();
+	if(ds <= 0)
+		return nil;
+	d := array[ds] of byte;
+	d[0] = byte ((ds - 2) >> 8);
+	d[1] = byte (ds - 2);
+	d[2] = byte tag2type[tagof gm];
+	d[3] = byte gm.tid;
+	pick m := gm {
+	Thello =>
+		o := pstring(d, H, m.version);
+		o = pstring(d, o, m.uid);
+		d[o++] = byte m.cryptostrength;
+		d[o++] = byte len m.cryptos;
+		d[o:] = m.cryptos;
+		o += len m.cryptos;
+		d[o++] = byte len m.codecs;
+		d[o:] = m.codecs;
+		o += len m.codecs;
+	Tping =>
+		;
+	Tgoodbye =>
+		;
+	Tread =>
+		d[H:] = m.score.a;
+		d[H+SCORE] = byte m.etype;
+		d[H+SCORE+2] = byte (m.n >> 8);
+		d[H+SCORE+3] = byte m.n;
+	Twrite =>
+		d[H] = byte m.etype;
+		d[H+4:] = m.data;
+	Tsync =>
+		;
+	Rhello =>
+		o := pstring(d, H, m.sid);
+		d[o++] = byte m.crypto;
+		d[o++] = byte m.codec;
+	Rping =>
+		;
+	Rread =>
+		d[H:] = m.data;
+	Rwrite =>
+		d[H:] = m.score.a;
+	Rsync =>
+		;
+	Rerror =>
+		pstring(d, H, m.e);
+	* =>
+		return nil;
+	}
+	return d;
+}
+
+Vmsg.packedsize(gm: self ref Vmsg): int
+{
+	mtype := tag2type[tagof gm];
+	if(mtype <= 0)
+		return 0;
+	ml := hdrlen[mtype];
+	pick m := gm {
+	Thello =>
+		ml += utflen(m.version) + utflen(m.uid) + len m.cryptos + len m.codecs;
+	Rhello =>
+		ml += utflen(m.sid);
+	Rread =>
+		ml += len m.data;
+	Twrite =>
+		ml += len m.data;
+	Rerror =>
+		ml += utflen(m.e);
+	}
+	return ml;
+}
+
+Vmsg.text(gm: self ref Vmsg): string
+{
+	if(gm == nil)
+		return "(nil)";
+	s := sys->sprint("%s(%d", msgname[tagof gm], gm.tid);
+	pick m := gm {
+	* =>
+		s += ",ILLEGAL";
+	Thello =>
+		s += sys->sprint(", %#q, %#q, %d, [", m.version, m.uid, m.cryptostrength);
+		if(len m.cryptos > 0){
+			s += string int m.cryptos[0];
+			for(i := 1; i < len m.cryptos; i++)
+				s += "," + string int m.cryptos[i];
+		}
+		s += "], [";
+		if(len m.codecs > 0){
+			s += string int m.codecs[0];
+			for(i := 1; i < len m.codecs; i++)
+				s += "," + string int m.codecs[i];
+		}
+		s += "]";
+	Tping =>
+		;
+	Tgoodbye =>
+		;
+	Tread =>
+		s += sys->sprint(", %s, %d, %d", m.score.text(), m.etype, m.n);
+	Twrite =>
+		s += sys->sprint(", %d, data[%d]", m.etype, len m.data);
+	Tsync =>
+		;
+	Rhello =>
+		s += sys->sprint(", %#q, %d, %d", m.sid, m.crypto, m.codec);
+	Rping =>
+	Rread =>
+		s += sys->sprint(", data[%d]", len m.data);
+	Rwrite =>
+		s += ", " + m.score.text();
+	Rsync =>
+		;
+	Rerror =>
+		s += sys->sprint(", %#q", m.e);
+	}
+	return s + ")";
+}
+
+Session.new(fd: ref Sys->FD): ref Session
+{
+	s := "venti-";
+	for(i := 0; i < len versions; i++){
+		if(i != 0)
+			s[len s] = ':';
+		s += versions[i];
+	}
+	s += "-libventi\n";
+	d := array of byte s;
+	if(sys->write(fd, d, len d) != len d)
+		return nil;
+	version := readversion(fd, "venti-", versions);
+	if(version == nil)
+		return nil;
+	session := ref Session(fd, version);
+	(r, e) := session.rpc(ref Vmsg.Thello(1, 0, version, nil, 0, nil, nil));
+	if(r == nil){
+		sys->werrstr("hello failed: " + e);
+		return nil;
+	}
+	return ref Session(fd, version);
+}
+
+Session.read(s: self ref Session, score: Score, etype: int, maxn: int): array of byte
+{
+	(gm, err) := s.rpc(ref Vmsg.Tread(1, 0, score, etype, maxn));
+	if(gm == nil){
+		sys->werrstr(err);
+		return nil;
+	}
+	pick m := gm {
+	Rread =>
+		return m.data;
+	}
+	return nil;
+}
+
+Session.write(s: self ref Session, etype: int, data: array of byte): (int, Score)
+{
+	(gm, err) := s.rpc(ref Vmsg.Twrite(1, 0, etype, data));
+	if(gm == nil){
+		sys->werrstr(err);
+		return (-1, Score(nil));
+	}
+	pick m := gm {
+	Rwrite =>
+		return (0, m.score);
+	}
+	return (-1, Score(nil));
+}
+
+Session.sync(s: self ref Session): int
+{
+	(gm, err) := s.rpc(ref Vmsg.Tsync(1, 0));
+	if(gm == nil){
+		sys->werrstr(err);
+		return -1;
+	}
+	return 0;
+}
+
+Session.rpc(s: self ref Session, m: ref Vmsg): (ref Vmsg, string)
+{
+	d := m.pack();
+	if(sys->write(s.fd, d, len d) != len d)
+		return (nil, "write failed");
+	(grm, err) := Vmsg.read(s.fd);
+	if(grm == nil)
+		return (nil, err);
+	if(grm.tid != m.tid)
+		return (nil, "message tags don't match");
+	if(grm.istmsg)
+		return (nil, "reply message is a t-message");
+	pick rm := grm {
+	Rerror =>
+		return (nil, rm.e);
+	}
+	if(tagof(grm) != tagof(m) + 1)
+		return (nil, "reply message is of wrong type");
+	return (grm, nil);
+}
+
+readversion(fd: ref Sys->FD, prefix: string, versions: array of string): string
+{
+	buf := array[Maxstringsize] of byte;
+	i := 0;
+	for(;;){
+		if(i >= len buf){
+			sys->werrstr("initial version string too long");
+			return nil;
+		}
+		if(readn(fd, buf[i:], 1) != 1){
+			sys->werrstr("eof on version string");
+			return nil;
+		}
+		c := int buf[i];
+		if(c == '\n')
+			break;
+		if(c < ' ' || c > 16r7f || i < len prefix && prefix[i] != c){
+			sys->werrstr("bad version string");
+			return nil;
+		}
+		i++;
+	}
+	if(i < len prefix){
+		sys->werrstr("bad version string");
+		return nil;
+	}
+#sys->fprint(sys->fildes(2), "read version %#q\n", string buf[0:i]);
+	v := string buf[len prefix:i];
+	i = 0;
+	for(;;){
+		for(j := i; j < len v && v[j] != ':' && v[j] != '-'; j++)
+			;
+		vv := v[i:j];
+#sys->fprint(sys->fildes(2), "checking %#q\n", vv);
+		for(k := 0; k < len versions; k++)
+			if(versions[k] == vv)
+				return vv;
+		i = j;
+		if(i >= len v || v[i] != ':'){
+			sys->werrstr("unknown version");
+			return nil;
+		}
+		i++;
+	}
+	sys->werrstr("unknown version");
+	return nil;
+}
+
+
+Score.eq(a: self Score, b: Score): int
+{
+	for(i := 0; i < SCORE; i++)
+		if(a.a[i] != b.a[i])
+			return 0;
+	return 1;
+}
+
+Score.zero(): Score
+{
+	return Score(zero);
+}
+
+Score.parse(s: string): (int, Score)
+{
+	if(len s != Scoresize * 2)
+		return (-1, Score(nil));
+	score := array[Scoresize] of {* => byte 0};
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		case s[i] {
+		'0' to '9' =>
+			c -= '0';
+		'a' to 'f' =>
+			c -= 'a' - 10;
+		'A' to 'F' =>
+			c -= 'A' - 10;
+		* =>
+			return (-1, Score(nil));
+		}
+		if((i & 1) == 0)
+			c <<= 4;
+		score[i>>1] |= byte c;
+	}
+	return (0, Score(score));
+}
+
+Score.text(a: self Score): string
+{
+	s := "";
+	for(i := 0; i < SCORE; i++)
+		s += sys->sprint("%.2ux", int a.a[i]);
+	return s;
+}
+
+readn(fd: ref Sys->FD, buf: array of byte, nb: int): int
+{
+	for(nr := 0; nr < nb;){
+		n := sys->read(fd, buf[nr:], nb-nr);
+		if(n <= 0){
+			if(nr == 0)
+				return n;
+			break;
+		}
+		nr += n;
+	}
+	return nr;
+}
+
+readmsg(fd: ref Sys->FD): (array of byte, string)
+{
+	sbuf := array[BIT16SZ] of byte;
+	if((n := readn(fd, sbuf, BIT16SZ)) != BIT16SZ){
+		if(n == 0)
+			return (nil, nil);
+		return (nil, sys->sprint("%r"));
+	}
+	ml := (int sbuf[0] << 8) | int sbuf[1];
+	if(ml < BIT16SZ)
+		return (nil, "invalid venti message size");
+	buf := array[ml + BIT16SZ] of byte;
+	buf[0:] = sbuf;
+	if((n = readn(fd, buf[BIT16SZ:], ml)) != ml){
+		if(n == 0)
+			return (nil, "venti message truncated");
+		return (nil, sys->sprint("%r"));
+	}
+	return (buf, nil);
+}
+
+pstring(a: array of byte, o: int, s: string): int
+{
+	sa := array of byte s;	# could do conversion ourselves
+	n := len sa;
+	a[o] = byte (n >> 8);
+	a[o+1] = byte n;
+	a[o+2:] = sa;
+	return o+STR+n;
+}
+
+gstring(a: array of byte, o: int): (string, int)
+{
+	if(o < 0 || o+STR > len a)
+		return (nil, -1);
+	l := (int a[o] << 8) | int a[o+1];
+	if(l > Maxstringsize)
+		return (nil, -1);
+	o += STR;
+	e := o+l;
+	if(e > len a)
+		return (nil, -1);
+	return (string a[o:e], e);
+}
+
+gbytes(a: array of byte, o: int): (array of byte, int)
+{
+	if(o < 0 || o+1 > len a)
+		return (nil, -1);
+	n := int a[o];
+	if(1+n > len a)
+		return (nil, -1);
+	no := o+1+n;
+	return (a[o+1:no], no);
+}
+
+utflen(s: string): int
+{
+	# the domain is 16-bit unicode only, which is all that Inferno now implements
+	n := l := len s;
+	for(i:=0; i<l; i++)
+		if((c := s[i]) > 16r7F){
+			n++;
+			if(c > 16r7FF)
+				n++;
+		}
+	return n;
+}
+
+gtstring(a: array of byte, o: int, n: int): string
+{
+	e := o + n;
+	if(e > len a)
+		return nil;
+	for(i := o; i < e; i++)
+		if(a[i] == byte 0)
+			break;
+	return string a[o:i];
+}
+
+Root.pack(r: self ref Root): array of byte
+{
+	d := array[Rootsize] of byte;
+	i := 0;
+	i = p16(d, i, r.version);
+	i = ptstring(d, i, r.name, Rootnamelen);
+	if(i < 0)
+		return nil;
+	i = ptstring(d, i, r.rtype, Rootnamelen);
+	if(i < 0)
+		return nil;
+	i = pscore(d, i, r.score);
+	i = p16(d, i, r.blocksize);
+	if(r.prev == nil) {
+		for(j := 0; j < Scoresize; j++)
+			d[i+j] = byte 0;
+		i += Scoresize;
+	} else 
+		i = pscore(d, i, *r.prev);
+	if(i != len d) {
+		sys->werrstr("root pack, bad length: "+string i);
+		return nil;
+	}
+	return d;
+}
+
+Root.unpack(d: array of byte): ref Root
+{
+	if(len d != Rootsize){
+		sys->werrstr("root entry is wrong length");
+		return nil;
+	}
+	r := ref blankroot;
+	r.version = g16(d, 0);
+	if(r.version != Rootversion){
+		sys->werrstr("unknown root version");
+		return nil;
+	}
+	o := BIT16SZ;
+	r.name = gtstring(d, o, Rootnamelen);
+	o += Rootnamelen;
+	r.rtype = gtstring(d, o, Rootnamelen);
+	o += Rootnamelen;
+	r.score = gscore(d, o);
+	o += Scoresize;
+	r.blocksize = g16(d, o);
+	o += BIT16SZ;
+	prev := gscore(d, o);
+	if(!prev.eq(Score(array[Scoresize] of {* => byte 0})))
+		r.prev = ref prev;
+	return r;
+}
+
+
+Entry.pack(e: self ref Entry): array of byte
+{
+	d := array[Entrysize] of byte;
+	i := 0;
+	i = p32(d, i, e.gen);
+	i = p16(d, i, e.psize);
+	i = p16(d, i, e.dsize);
+	e.flags |= e.depth<<Entrydepthshift;
+	d[i++] = byte e.flags;
+	for(j := 0; j < 5; j++)
+		d[i++] = byte 0;
+	i = p48(d, i, e.size);
+	i = pscore(d, i, e.score);
+	if(i != len d) {
+		sys->werrstr(sprint("bad length, have %d, want %d", i, len d));
+		return nil;
+	}
+	return d;
+}
+
+Entry.unpack(d: array of byte): ref Entry
+{
+	if(len d != Entrysize){
+		sys->werrstr("entry is wrong length");
+		return nil;
+	}
+	e := ref blankentry;
+	i := 0;
+	e.gen = g32(d, i);
+	i += BIT32SZ;
+	e.psize = g16(d, i);
+	i += BIT16SZ;
+	e.dsize = g16(d, i);
+	i += BIT16SZ;
+	e.flags = int d[i];
+	e.depth = (e.flags & Entrydepthmask) >> Entrydepthshift;
+	e.flags &= ~Entrydepthmask;
+	i += BIT8SZ;
+	i += 5;			# skip something...
+	e.size = g48(d, i);
+	i += BIT48SZ;
+	e.score = gscore(d, i);
+	i += Scoresize;
+	if((e.flags & Entryactive) == 0)
+		return e;
+	if(!checksize(e.psize) || !checksize(e.dsize)){
+		sys->werrstr(sys->sprint("bad blocksize (%d or %d)", e.psize, e.dsize));
+		return nil;
+	}
+	return e;
+}
+
+checksize(n: int): int
+{
+	if(n < 256 || n > Maxlumpsize) {
+		sys->werrstr("bad block size");
+		return 0;
+	}
+	return 1;
+}
+
+gscore(f: array of byte, i: int): Score
+{
+	s := Score(array[Scoresize] of byte);
+	s.a[0:] = f[i:i+Scoresize];
+	return s;
+}
+
+g16(f: array of byte, i: int): int
+{
+	return (int f[i] << 8) | int f[i+1];
+}
+
+g32(f: array of byte, i: int): int
+{
+	return (((((int f[i+0] << 8) | int f[i+1]) << 8) | int f[i+2]) << 8) | int f[i+3];
+}
+
+g48(f: array of byte, i: int): big
+{
+	b1 := (((((int f[i+0] << 8) | int f[i+1]) << 8) | int f[i+2]) << 8) | int f[i+3];
+	b0 := (int f[i+4] << 8) | int f[i+5];
+	return (big b1 << 16) | big b0;
+}
+
+g64(f: array of byte, i: int): big
+{
+	b0 := (((((int f[i+0] << 8) | int f[i+1]) << 8) | int f[i+2]) << 8) | int f[i+3];
+	b1 := (((((int f[i+4] << 8) | int f[i+5]) << 8) | int f[i+6]) << 8) | int f[i+7];
+	return (big b0 << 32) | (big b1 & 16rFFFFFFFF);
+}
+
+p16(d: array of byte, i: int, v: int): int
+{
+	d[i+0] = byte (v>>8);
+	d[i+1] = byte v;
+	return i+BIT16SZ;
+}
+
+p32(d: array of byte, i: int, v: int): int
+{
+	p16(d, i+0, v>>16);
+	p16(d, i+2, v);
+	return i+BIT32SZ;
+}
+
+p48(d: array of byte, i: int, v: big): int
+{
+	p16(d, i+0, int (v>>32));
+	p32(d, i+2, int v);
+	return i+BIT48SZ;
+}
+
+ptstring(d: array of byte, i: int, s: string, l: int): int
+{
+	a := array of byte s;
+	if(len a > l) {
+		sys->werrstr("string too long: "+s);
+		return -1;
+	}
+	for(j := 0; j < len a; j++)
+		d[i+j] = a[j];
+	while(j < l)
+		d[i+j++] = byte 0;
+	return i+l;
+}
+
+pscore(d: array of byte, i: int, s: Score): int
+{
+	for(j := 0; j < Scoresize; j++)
+		d[i+j] = s.a[j];
+	return i+Scoresize;
+}
--- /dev/null
+++ b/appl/lib/virgil.b
@@ -1,0 +1,183 @@
+implement Virgil;
+
+include "sys.m";
+	sys: Sys;
+include "string.m";
+include "keyring.m";
+include "draw.m";
+include "dial.m";
+	dial: Dial;
+include "security.m";
+include "ip.m";
+	ip: IP;
+	IPaddr, Udphdr: import ip;
+
+stderr: ref Sys->FD;
+done: int;
+Udphdrsize: con IP->Udphdrlen;
+Virgilport: con 2202;
+
+#
+#  this module is very udp dependent.  it shouldn't be. -- presotto
+#  Call with first element of argv an arbitrary string, which is
+#  discarded here.  argv must also contain at least a question.
+#
+virgil(argv: list of string): string
+{
+	s,question,reply,r : string;
+	timerpid, readerpid: int;
+
+	if (argv == nil || tl argv == nil || hd (tl argv) == nil)
+		return nil;
+	done = 0;
+	sys = load Sys Sys->PATH;
+	dial = load Dial Dial->PATH;
+	if(dial == nil){
+		cantload(Dial->PATH);
+		return nil;
+	}
+	str := load String String->PATH;
+	if(str == nil){
+		cantload(String->PATH);
+		return nil;
+	}
+	ip = load IP IP->PATH;
+	if(ip == nil){
+		cantload(IP->PATH);
+		return nil;
+	}
+	ip->init();
+	stderr = sys->fildes(2);
+
+	# We preserve the convention that the first arg is not an option.
+	# Undocumented '-v address' option allows passing in address
+	# of virgild, circumventing broadcast.  Used for development,
+	# to avoid pestering servers on network.
+	dest := ip->v4bcast;
+	argv = tl argv;
+	s = hd argv;
+	if(s[0] == '-') {
+		if(s[1] != 'v')
+			return nil;
+		argv = tl argv;
+		if (argv == nil)
+			return nil;
+		s = hd argv;
+		ok: int;
+		(ok, dest) = IPaddr.parse(s);
+		if(ok < 0){
+			sys->fprint(stderr, "virgil: invalid IP address %s\n", s);
+			return nil;
+		}
+		argv = tl argv;
+	}
+
+	# Is there a question?
+	if (argv == nil)
+		return nil;
+	question = hd argv;
+
+	c := dial->announce("udp!*!0");
+	if(c == nil)
+		return nil;
+	if(sys->fprint(c.cfd, "headers") < 0)
+		return nil;
+	c.dfd = sys->open(c.dir+"/data", sys->ORDWR);
+	if(c.dfd == nil)
+		return nil;
+
+	readerchan := chan of string;
+	timerchan := chan of int;
+	readerpidchan := chan of int;
+
+	spawn timer(timerchan);
+	timerpid = <-timerchan;
+	spawn reader(c.dfd, readerchan, readerpidchan);
+	readerpid = <-readerpidchan;
+
+	question = getid() + "?" + question;
+	qbuf := array of byte question;
+	hdr := Udphdr.new();
+	hdr.raddr = dest;
+	hdr.rport = Virgilport;
+	buf := array[Udphdrsize + len qbuf] of byte;
+	buf[Udphdrsize:] = qbuf;
+	hdr.pack(buf, Udphdrsize);
+	for(tries := 0; tries < 5; ){
+		if(sys->write(c.dfd, buf, len buf) < 0)
+			break;
+
+		alt {
+		r = <-readerchan =>
+			;
+		<-timerchan =>
+			tries++;
+			continue;
+		};
+
+		if(str->prefix(question + "=", r)){
+			reply = r[len question + 1:];
+			break;
+		}
+	}
+
+	done = 1;
+	killpid(readerpid);
+	killpid(timerpid);
+	return reply;
+}
+
+cantload(s: string)
+{
+	sys->fprint(stderr, "virgil: can't load %s: %r\n", s);
+}
+
+getid(): string
+{
+	fd := sys->open("/dev/sysname", sys->OREAD);
+	if(fd == nil)
+		return "unknown";
+	buf := array[256] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 1)
+		return "unknown";
+	return string buf[0:n];
+}
+
+reader(fd: ref sys->FD, cstring: chan of string, cpid: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	cpid <-= pid;
+
+	buf := array[2048] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= Udphdrsize)
+		return;
+
+	# dump cruft
+	for(i := Udphdrsize; i < n; i++)
+		if((int buf[i]) == 0)
+				break;
+
+	if(!done)
+		cstring <-= string buf[Udphdrsize:i];
+}
+
+timer(c: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	c <-= pid;
+	while(!done){
+		sys->sleep(1000);
+		if(done)
+			break;
+		c <-= 1;
+	}
+}
+
+killpid(pid: int)
+{
+	fd := sys->open("#p/"+(string pid)+"/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
--- /dev/null
+++ b/appl/lib/volume.b
@@ -1,0 +1,171 @@
+implement Volumectl;
+
+include "sys.m";
+sys: Sys;
+sprint: import sys;
+
+include "draw.m";
+draw: Draw;
+Context, Display, Font, Rect, Point, Image, Screen, Pointer: import draw;
+
+include "prefab.m";
+prefab: Prefab;
+Style, Element, Compound, Environ: import prefab;
+
+include "muxclient.m";
+include "volume.m";
+
+include "bufio.m";
+bufio: Bufio;
+Iobuf: import bufio;
+
+include "ir.m";
+
+screen: ref Screen;
+display: ref Display;
+windows: array of ref Image;
+env: ref Environ;
+zr := ((0,0),(0,0));
+
+el, et: ref Element;
+
+style: ref Style;
+
+c: ref Compound;
+
+tics: int;
+INTERVAL: con 500;
+
+value: int;
+
+ones, white, red: ref Image;
+
+volumectl(ctxt: ref Context, ch: chan of int, var: string)
+{
+	key: int;
+
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	prefab = load Prefab Prefab->PATH;
+	if ((bufio = load Bufio Bufio->PATH) == nil) {
+		sys->print("Audioctl: Can't load bufio\n");
+		exit;
+	}
+
+	if ((ac := bufio->open("/dev/volume", bufio->ORDWR)) == nil) {
+		sys->print("Audioctl: Can't open /dev/volume: %r\n");
+		exit;
+	}
+
+	screen = ctxt.screen;
+	display = ctxt.display;
+	windows = array[1] of ref Image;
+
+	ones = display.opaque;
+	white = display.color(draw->White);
+	red = display.color(draw->Red);
+
+	textfont := Font.open(display, "*default*");
+
+	style = ref Style(
+			textfont,			# titlefont
+			textfont,			# textfont
+			display.color(draw->White),	# elemcolor
+			display.color(draw->Black),	# edgecolor
+			display.color(draw->Yellow),	# titlecolor	
+			display.color(draw->Black),	# textcolor
+			display.color(130));		# highlightcolor
+
+	env = ref Environ (ctxt.screen, style);
+
+	slavectl := chan of int;
+	spawn timerslave(slavectl);
+
+	while ((s := ac.gets('\n')) != nil) {
+		sp := -1;
+		for (i := 0; i < len s; i++) if (s[i] == ' ') sp = i;
+		if (sp <= 1) {
+			sys->print("Volume: /dev/volume bad:\n%s\n", s);
+			exit;
+		}
+		if (var == s[0:sp]) {
+			value = int s[sp+1:];
+		}
+	}
+
+	for(;;) {
+		key = <- ch;
+		case key {
+		Ir->Enter =>
+			slavectl <-= Muxclient->AMexit;
+			return;
+		Ir->VolUP =>
+			if (value++ >= 100) value = 100;
+			ac.puts(sprint("%s %d\n", var, value));
+			displayslider();
+		Ir->VolDN =>
+			if (value-- <= 0) value = 0;
+			ac.puts(sprint("%s %d\n", var, value));
+			displayslider();
+		}
+	}
+}
+
+slider(): ref Element
+{
+	r: Rect;
+
+	r = ((0,0),(200,20));
+	chans := display.image.chans;
+	icon := display.newimage(r.inset(-2), chans, 0, draw->Black);
+	icon.draw(r, white, ones, (0,0));
+	rr := r;
+	rr.max.x = 2*value;
+	icon.draw(rr, red, ones, (0,0));
+	return Element.icon(env, zr, icon, ones);
+}
+
+displayslider()
+{
+	if (et == nil) {
+		et = Element.text(env, "Volume", zr, Prefab->EText);
+		el = slider();
+	}
+
+	img := el.image;
+	r: Rect = ((0,0),(200,20));
+	img.draw(r, white, nil, (0,0));
+	r.max.x = 2*value;
+	img.draw(r, red, nil, (0,0));
+
+	if (c == nil) {
+		c = Compound.box(env, Point(100, 100), et, el);
+		windows[0] = c.image;
+	}
+	c.draw();
+	screen.top(windows);
+	tics = 5;
+}
+
+timerslave(ctl: chan of int)
+{
+	m: int;
+
+	for(;;) {
+		sys->sleep(INTERVAL);
+		if (tics-- <= 0) {
+			tics = 0;
+			c = nil;
+			el = nil;
+			et = nil;
+			windows[0] = nil;
+		}
+
+		alt{
+		m = <-ctl =>
+			return;
+		* =>
+			continue;
+		}
+	}
+}
--- /dev/null
+++ b/appl/lib/w3c/css.b
@@ -1,0 +1,1019 @@
+implement CSS;
+
+#
+# CSS2 parsing module
+#
+# CSS2.1 style sheets 
+#
+# Copyright © 2001, 2005 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "css.m";
+
+B, NUMBER, IDENT, STRING, URL, PERCENTAGE, UNIT,
+	HASH, ATKEYWORD, IMPORTANT, IMPORT, PSEUDO, CLASS, INCLUDES,
+	DASHMATCH, FUNCTION: con 16rE000+iota;
+
+toknames := array[] of{
+	B-B => "Zero",
+	NUMBER-B => "NUMBER",
+	IDENT-B => "IDENT",
+	STRING-B => "STRING",
+	URL-B => "URL",
+	PERCENTAGE-B => "PERCENTAGE",
+	UNIT-B => "UNIT",
+	HASH-B => "HASH",
+	ATKEYWORD-B => "ATKEYWORD",
+	IMPORTANT-B => "IMPORTANT",
+	CLASS-B => "CLASS",
+	INCLUDES-B => "INCLUDES",
+	DASHMATCH-B => "DASHMATCH",
+	PSEUDO-B => "PSEUDO",
+	FUNCTION-B => "FUNCTION",
+};
+
+printdiag := 0;
+
+init(d: int)
+{
+	sys = load Sys Sys->PATH;
+	printdiag = d;
+}
+
+parse(s: string): (ref Stylesheet, string)
+{
+	return stylesheet(ref Cparse(-1, 0, nil, nil, Clex.new(s,1)));
+}
+
+parsedecl(s: string): (list of ref Decl, string)
+{
+	return (declarations(ref Cparse(-1, 0, nil, nil, Clex.new(s,0))), nil);
+}
+
+ptok(c: int): string
+{
+	if(c < 0)
+		return "eof";
+	if(c == 0)
+		return "zero?";
+	if(c >= B)
+		return sys->sprint("%s", toknames[c-B]);
+	return sys->sprint("%c", c);
+}
+
+Cparse: adt {
+	lookahead:	int;
+	eof:	int;
+	value:	string;
+	suffix:	string;
+	cs:	ref Clex;
+
+	get:	fn(nil: self ref Cparse): int;
+	look:	fn(nil: self ref Cparse): int;
+	unget:	fn(nil: self ref Cparse, tok: int);
+	skipto:	fn(nil: self ref Cparse, followset: string): int;
+	synerr:	fn(nil: self ref Cparse, s: string);
+};
+
+Cparse.get(p: self ref Cparse): int
+{
+	if((c := p.lookahead) >= 0){
+		p.lookahead = -1;
+		return c;
+	}
+	if(p.eof)
+		return -1;
+	(c, p.value, p.suffix) = csslex(p.cs);
+	if(c < 0)
+		p.eof = 1;
+	if(printdiag > 1)
+		sys->print("lex: %s v=%s s=%s\n", ptok(c), p.value, p.suffix);
+	return c;
+}
+
+Cparse.look(p: self ref Cparse): int
+{
+	c := p.get();
+	p.unget(c);
+	return c;
+}
+
+Cparse.unget(p: self ref Cparse, c: int)
+{
+	if(p.lookahead >= 0)
+		raise "css: internal error: Cparse.unget";
+	p.lookahead = c;	# note that p.value and p.suffix are assumed to be those of c
+}
+
+Cparse.skipto(p: self ref Cparse, followset: string): int
+{
+	while((c := p.get()) >= 0)
+		for(i := 0; i < len followset; i++)
+			if(followset[i] == c){
+				p.unget(c);
+				return c;
+			}
+	return -1;
+}
+
+Cparse.synerr(p: self ref Cparse, s: string)
+{
+	p.cs.synerr(s);
+}
+
+#
+# stylesheet:
+#	["@charset" STRING ';']?
+#	[CDO|CDC]* [import [CDO|CDC]*]*
+#	[[ruleset | media | page ] [CDO|CDC]*]*
+# import:
+#	"@import" [STRING|URL] [ medium [',' medium]*]? ';'
+# media:
+#	"@media" medium [',' medium]* '{' ruleset* '}'
+# medium:
+#	IDENT
+# page:
+#	"@page" pseudo_page? '{' declaration [';' declaration]* '}'
+# pseudo_page:
+#	':' IDENT
+#
+
+stylesheet(p: ref Cparse): (ref Stylesheet, string)
+{
+	charset: string;
+	if(atkeywd(p, "@charset")){
+		if(itisa(p, STRING)){
+			charset = p.value;
+			itisa(p, ';');
+		}else
+			p.synerr("bad @charset declaration");
+	}
+	imports: list of ref Import;
+	while(atkeywd(p, "@import")){
+		c := p.get();
+		if(c == STRING || c == URL){
+			name := p.value;
+			media: list of string;
+			c = p.get();
+			if(c == IDENT){	# optional medium [, ...]
+				p.unget(c);
+				media = medialist(p);
+			}
+			imports = ref Import(name, media) :: imports;
+		}else
+			p.synerr("bad @import");
+		if(c != ';'){
+			p.synerr("missing ; in @import");
+			p.unget(c);
+			if(p.skipto(";}") < 0)
+				break;
+		}
+	}
+	imports = rev(imports);
+
+	stmts: list of ref Statement;
+	do{
+		while((c := p.get()) == ATKEYWORD)
+			case p.value {
+			"@media" =>	# medium[,medium]* { ruleset*}
+				media := medialist(p);
+				if(!itisa(p, '{')){
+					p.synerr("bad @media");
+					skipatrule("@media", p);
+					continue;
+				}
+				rules: list of ref Statement.Ruleset;
+				do{
+					rule := checkrule(p);
+					if(rule != nil)
+						rules = rule :: rules;
+				}while(!itisa(p, '}') && !p.eof);
+				stmts = ref Statement.Media(media, rev(rules)) :: stmts;
+			"@page" =>	# [:ident]? { declaration [; declaration]* }
+				pseudo: string;
+				if(itisa(p, PSEUDO))
+					pseudo = p.value;
+				if(!itisa(p, '{')){
+					p.synerr("bad @page");
+					skipatrule("@page", p);
+					continue;
+				}
+				decls := declarations(p);
+				if(!itisa(p, '}')){
+					p.synerr("unclosed @page declaration block");
+					skipatrule("@page", p);
+					continue;
+				}
+				stmts = ref Statement.Page(pseudo, decls) :: stmts;
+			* =>
+				skipatrule(p.value, p);	# skip unknown or misplaced at-rule
+			}
+		p.unget(c);
+		rule := checkrule(p);
+		if(rule != nil)
+			stmts = rule :: stmts;
+	}while(!p.eof);
+	rl := stmts;
+	stmts = nil;
+	for(; rl != nil; rl = tl rl)
+		stmts = hd rl :: stmts;
+	return (ref Stylesheet(charset, imports, stmts), nil);
+}
+
+checkrule(p: ref Cparse): ref Statement.Ruleset
+{
+	(rule, err) := ruleset(p);
+	if(rule == nil){
+		if(err != nil){
+			p.synerr(sys->sprint("bad ruleset: %s", err));
+			p.get();	# make some progress
+		}
+	}
+	return rule;
+}
+
+medialist(p: ref Cparse): list of string
+{
+	media: list of string;
+	do{
+		c := p.get();
+		if(c != IDENT){
+			p.unget(c);
+			p.synerr("missing medium identifier");
+			break;
+		}
+		media = p.value :: media;
+	}while(itisa(p, ','));
+	return rev(media);
+}
+
+itisa(p: ref Cparse, expect: int): int
+{
+	if((c := p.get()) == expect)
+		return 1;
+	p.unget(c);
+	return 0;
+}
+
+atkeywd(p: ref Cparse, expect: string): int
+{
+	if((c := p.get()) == ATKEYWORD && p.value == expect)
+		return 1;
+	p.unget(c);
+	return 0;
+}
+
+skipatrule(name: string, p: ref Cparse)
+{
+	if(printdiag)
+		sys->print("skip unimplemented or misplaced %s\n", name);
+	if((c := p.get()) == '{'){	# block
+		for(nesting := '}' :: nil; nesting != nil && c >= 0; nesting = tl nesting){
+			while((c = p.cs.getc()) >= 0 && c != hd nesting)
+				case c {
+				'{' =>
+					nesting = '}' :: nesting;
+				'(' =>
+					nesting = ')' :: nesting;
+				'[' =>
+					nesting = ']' :: nesting;
+				'"' or '\'' =>
+					quotedstring(p.cs, c);
+				}
+		}
+	}else{
+		while(c >= 0 && c != ';')
+			c = p.get();
+	}
+}
+
+# ruleset:
+#	selector [','  S* selector]* '{' S* declaration [';' S* declaration]* '}' S*
+
+ruleset(p: ref Cparse): (ref Statement.Ruleset, string)
+{
+	selectors: list of list of (int, list of ref Select);
+	c := -1;
+	do{
+		s := selector(p);
+		if(s == nil){
+			if(p.eof)
+				return (nil, nil);
+			p.synerr("expected selector");
+			if(p.skipto(",{}") < 0)
+				return (nil, nil);
+			c = p.look();
+		}else
+			selectors = s :: selectors;
+	}while((c = p.get()) == ',');
+	if(c != '{')
+		return (nil, "expected declaration block");
+	sl := selectors;
+	selectors = nil;
+	for(; sl != nil; sl = tl sl)
+		selectors = hd sl :: selectors;
+	decls := declarations(p);
+	if(!itisa(p, '}')){
+		p.synerr("unclosed declaration block");
+	}
+	return (ref Statement.Ruleset(selectors, decls), nil);
+}
+
+declarations(p: ref Cparse): list of ref Decl
+{
+	decls: list of ref Decl;
+	c: int;
+	do{
+		(d, e) := declaration(p);
+		if(d != nil)
+			decls = d :: decls;
+		else if(e != nil){
+			p.synerr("ruleset declaration: "+e);
+			if((c = p.skipto(";}")) < 0)
+				break;
+		}
+	}while((c = p.get()) == ';');
+	p.unget(c);
+	l := decls;
+	for(decls = nil; l != nil; l = tl l)
+		decls = hd l :: decls;
+	return decls;
+}
+
+# selector:
+#	simple_selector [combinator simple_selector]*
+# combinator:
+#	'+' S* | '>' S* | /* empty */
+#
+
+selector(p: ref Cparse): list of (int, list of ref Select)
+{
+	sel: list of (int, list of ref Select);
+	op := ' ';
+	while((s := selector1(p)) != nil){
+		sel = (op, s) :: sel;
+		if((c := p.look()) == '+' || c == '>')
+			op = p.get();
+		else
+			op = ' ';
+	}
+	l: list of (int, list of ref Select);
+	for(; sel != nil; sel = tl sel)
+		l = hd sel :: l;
+	return l;
+}
+
+#
+# simple_selector:
+#	element_name? [HASH | class | attrib | pseudo]* S*
+# element_name:
+#	IDENT | '*'
+# class:
+#	'.' IDENT
+# attrib:
+#	'[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S* [IDENT | STRING] S* ]? ']'
+# pseudo
+#	':' [ IDENT | FUNCTION S* IDENT? S* ')' ]
+
+selector1(p: ref Cparse): list of ref Select
+{
+	sel: list of ref Select;
+	c := p.get();
+	if(c == IDENT)
+		sel = ref Select.Element(p.value) :: sel;
+	else if(c== '*')
+		sel = ref Select.Any("*") :: sel;
+	else
+		p.unget(c);
+Sel:
+	for(;;){
+		c = p.get();
+		case c {
+		HASH =>
+			sel = ref Select.ID(p.value) :: sel;
+		CLASS =>
+			sel = ref Select.Class(p.value) :: sel;
+		'[' =>
+			if(!itisa(p, IDENT))
+				break;
+			name := p.value;
+			case c = p.get() {
+			'=' =>
+				sel = ref Select.Attrib(name, "=", optaval(p)) :: sel;
+			INCLUDES =>
+				sel = ref Select.Attrib(name, "~=", optaval(p)) :: sel;
+			DASHMATCH =>
+				sel = ref Select.Attrib(name, "|=", optaval(p)) :: sel;
+			* =>
+				sel = ref Select.Attrib(name, nil, nil) :: sel;
+				p.unget(c);
+			}
+			if((c = p.get()) != ']'){
+				p.synerr("bad attribute syntax");
+				p.unget(c);
+				break Sel;
+			}
+		PSEUDO =>
+			case c = p.get() {
+			IDENT =>
+				sel = ref Select.Pseudo(p.value) :: sel;
+			FUNCTION =>
+				name := p.value;
+				case c = p.get() {
+				IDENT =>
+					sel = ref Select.Pseudofn(name, lowercase(p.value)) :: sel;
+				')' =>
+					p.unget(c);
+					sel = ref Select.Pseudofn(name, nil) :: sel;
+				* =>
+					p.synerr("bad pseudo-function syntax");
+					p.unget(c);
+					break Sel;
+				}
+				if((c = p.get()) != ')'){
+					p.synerr("missing ')' for pseudo-function");
+					p.unget(c);
+					break Sel;
+				}
+			* =>
+				p.synerr(sys->sprint("unexpected :pseudo: %s:%s", ptok(c), p.value));
+				p.unget(c);
+				break Sel;
+			}
+		* =>
+			p.unget(c);
+			break Sel;
+		}
+		# qualifiers must be adjacent to the first item, and each other
+		c = p.cs.getc();
+		p.cs.ungetc(c);
+		if(isspace(c))
+			break;
+	}
+	sl := sel;
+	for(sel = nil; sl != nil; sl = tl sl)
+		sel = hd sl :: sel;
+	return sel;
+}
+
+optaval(p: ref Cparse): ref Value
+{
+	case c := p.get() {
+	IDENT =>
+		return ref Value.Ident(' ', p.value);
+	STRING =>
+		return ref Value.String(' ', p.value);
+	* =>
+		p.unget(c);
+		return nil;
+	}
+}
+
+# declaration:
+#	property ':' S* expr prio?
+#  |	/* empty */
+# property:
+#	IDENT
+# prio:
+#	IMPORTANT S*	/* ! important */
+
+declaration(p: ref Cparse): (ref Decl, string)
+{
+	c := p.get();
+	if(c != IDENT){
+		p.unget(c);
+		return (nil, nil);
+	}
+	prop := lowercase(p.value);
+	c = p.get();
+	if(c != ':'){
+		p.unget(c);
+		return (nil, "missing :");
+	}
+	values := expr(p);
+	if(values == nil)
+		return (nil, "missing expression(s)");
+	prio := 0;
+	if(p.look() == IMPORTANT){
+		p.get();
+		prio = 1;
+	}
+	return (ref Decl(prop, values, prio), nil);
+}
+
+# expr:
+#	term [operator term]*
+# operator:
+#	'/' | ',' | /* empty */
+
+expr(p: ref Cparse): list of ref Value
+{
+	values: list of ref Value;
+	sep := ' ';
+	while((t := term(p, sep)) != nil){
+		values = t :: values;
+		if((c := p.look()) == '/' || c == ',')
+			sep = p.get();		# need something fancier here?
+		else
+			sep = ' ';
+	}
+	vl := values;
+	for(values = nil; vl != nil; vl = tl vl)
+		values = hd vl :: values;
+	return values;
+}
+
+#
+# term:
+#	unary_operator? [NUMBER | PERCENTAGE | LENGTH | EMS | EXS | ANGLE | TIME | FREQ | function]
+#	| STRING | IDENT | URI | RGB | UNICODERANGE | hexcolour
+# function:
+#	FUNCTION expr ')'
+# unary_operator:
+#	'-' | '+'
+# hexcolour:
+#	HASH S*
+#
+# LENGTH, EMS, ... FREQ have been combined into UNIT here
+#
+# TO DO: UNICODERANGE
+
+term(p: ref Cparse, sep: int): ref Value
+{
+	prefix: string;
+	case p.look(){
+	'+' or '-' =>
+		prefix[0] = p.get();
+	}
+	c := p.get();
+	case c {
+	NUMBER =>
+		return ref Value.Number(sep, prefix+p.value);
+	PERCENTAGE =>
+		return ref Value.Percentage(sep, prefix+p.value);
+	UNIT =>
+		return ref Value.Unit(sep, prefix+p.value, p.suffix);
+	}
+	if(prefix != nil)
+		p.synerr("+/- before non-numeric");
+	case c {
+	STRING =>
+		return ref Value.String(sep, p.value);
+	IDENT =>
+		return ref Value.Ident(sep, lowercase(p.value));
+	URL =>
+		return ref Value.Url(sep, p.value);
+	HASH =>
+		# could check value: 3 or 6 hex digits
+		(r, g, b) := torgb(p.value);
+		if(r < 0)
+			return nil;
+		return ref Value.Hexcolour(sep, p.value, (r,g,b));
+	FUNCTION =>
+		name := p.value;
+		args := expr(p);
+		c = p.get();
+		if(c != ')'){
+			p.synerr(sys->sprint("missing ')' for function %s", name));
+			return nil;
+		}
+		if(name == "rgb"){
+			if(len args != 3){
+				p.synerr("wrong number of arguments to rgb()");
+				return nil;
+			}
+			r := colourof(hd args);
+			g := colourof(hd tl args);
+			b := colourof(hd tl tl args);
+			if(r < 0 || g < 0 || b < 0){
+				p.synerr("invalid rgb() parameters");
+				return nil;
+			}
+			return ref Value.RGB(sep, args, (r,g,b));
+		}
+		return ref Value.Function(sep, name, args);
+	* =>
+		p.unget(c);
+		return nil;
+	}
+}
+
+torgb(s: string): (int, int, int)
+{
+	case len s {
+	3 =>
+		r := hex(s[0]);
+		g := hex(s[1]);
+		b := hex(s[2]);
+		if(r >= 0 && g >= 0 && b >= 0)
+			return ((r<<4)|r, (g<<4)|g, (b<<4)|b);
+	6 =>
+		v := 0;
+		for(i := 0; i < 6; i++){
+			n := hex(s[i]);
+			if(n < 0)
+				return (-1, 0, 0);
+			v = (v<<4) | n;
+		}
+		return (v>>16, (v>>8)&16rFF, v&16rFF);
+	}
+	return (-1, 0, 0);
+}
+
+colourof(v: ref Value): int
+{
+	pick r := v {
+	Number =>
+		return clip(int r.value, 0, 255);
+	Percentage =>
+		# just the integer part
+		return clip((int r.value*255 + 50)/100, 0, 255);
+	* =>
+		return -1;
+	}
+}
+
+clip(v: int, l: int, u: int): int
+{
+	if(v < l)
+		return l;
+	if(v > u)
+		return u;
+	return v;
+}
+
+rev[T](l: list of T): list of T
+{
+	t: list of T;
+	for(; l != nil; l = tl l)
+		t = hd l :: t;
+	return t;
+}
+
+Clex: adt {
+	context:	list of int;	# characters
+	input:	string;
+	lim:	int;
+	n:	int;
+	lineno:	int;
+
+	new:	fn(s: string, lno: int): ref Clex;
+	getc:	fn(cs: self ref Clex): int;
+	ungetc:	fn(cs: self ref Clex, c: int);
+	synerr:	fn(nil: self ref Clex, s: string);
+};
+
+Clex.new(s: string, lno: int): ref Clex
+{
+	return ref Clex(nil, s, len s, 0, lno);
+}
+
+Clex.getc(cs: self ref Clex): int
+{
+	if(cs.context != nil){
+		c := hd cs.context;
+		cs.context = tl cs.context;
+		return c;
+	}
+	if(cs.n >= cs.lim)
+		return -1;
+	c := cs.input[cs.n++];
+	if(c == '\n')
+		cs.lineno++;
+	return c;
+}
+
+Clex.ungetc(cs: self ref Clex, c: int)
+{
+	cs.context = c :: cs.context;
+}
+
+Clex.synerr(cs: self ref Clex, s: string)
+{
+	if(printdiag)
+		sys->fprint(sys->fildes(2), "%d: err: %s\n", cs.lineno, s);
+}
+
+csslex(cs: ref Clex): (int, string, string)
+{
+	for(;;){
+		c := skipws(cs);
+		if(c < 0)
+			return (-1, nil, nil);
+		case c {
+		'<' =>
+			if(seq(cs, "!--"))
+				break;		# <!-- ignore HTML comment start (CDO)
+			return (c, nil, nil);
+		'-' =>
+			if(seq(cs, "->"))
+				break;		# --> ignore HTML comment end (CDC)
+			return (c, nil, nil);
+		':' =>
+			c = cs.getc();
+			cs.ungetc(c);
+			if(isnamec(c, 0))
+				return (PSEUDO, nil, nil);
+			return (':', nil, nil);
+		'#' =>
+			c = cs.getc();
+			if(isnamec(c, 1))
+				return (HASH, name(cs, c), nil);
+			cs.ungetc(c);
+			return ('#', nil, nil);
+		'/' =>
+			if(subseq(cs, '*', 1, 0)){
+				comment(cs);
+				break;
+			}
+			return (c, nil, nil);
+		'\'' or '"' =>
+			return (STRING, quotedstring(cs, c), nil);
+		'0' to '9' or '.' =>
+			if(c == '.'){
+				d := cs.getc();
+				cs.ungetc(d);
+				if(!isdigit(d)){
+					if(isnamec(d, 1))
+						return (CLASS, name(cs, cs.getc()), nil);
+					return ('.', nil, nil);
+				}
+				# apply CSS2 treatment: .55 is a number not a class
+			}
+			val := number(cs, c);
+			c = cs.getc();
+			if(c == '%')
+				return (PERCENTAGE, val, "%");
+			if(isnamec(c, 0))	# use CSS2 interpetation
+				return (UNIT, val, lowercase(name(cs, c)));
+			cs.ungetc(c);
+			return (NUMBER, val, nil);
+		'\\' =>
+			d := cs.getc();
+			if(d >= ' ' && d <= '~' || islatin1(d)){	# probably should handle it in name
+				wd := name(cs, d);
+				return (IDENT, "\\"+wd, nil);
+			}
+			cs.ungetc(d);
+			return ('\\', nil, nil);
+		'@' =>
+			c = cs.getc();
+			if(isnamec(c, 0))	# @something
+				return (ATKEYWORD, "@"+lowercase(name(cs,c)), nil);
+			cs.ungetc(c);
+			return ('@', nil, nil);
+		'!' =>
+			c = skipws(cs);
+			if(isnamec(c, 0)){	# !something
+				wd := name(cs, c);
+				if(lowercase(wd) == "important")
+					return (IMPORTANT, nil, nil);
+				pushback(cs, wd);
+			}else
+				cs.ungetc(c);
+			return ('!', nil, nil);
+		'~' =>
+			if(subseq(cs, '=', 1, 0))
+				return (INCLUDES, "~=", nil);
+			return ('~', nil, nil);
+		'|' =>
+			if(subseq(cs, '=', 1, 0))
+				return (DASHMATCH, "|=", nil);
+			return ('|', nil, nil);
+		* =>
+			if(isnamec(c, 0)){
+				wd := name(cs, c);
+				d := cs.getc();
+				if(d != '('){
+					cs.ungetc(d);
+					return (IDENT, wd, nil);
+				}
+				val := lowercase(wd);
+				if(val == "url")
+					return (URL, url(cs), nil);	# bizarre special case
+				return (FUNCTION, val, nil);
+			}
+			return (c, nil, nil);
+		}
+
+	}
+}
+
+skipws(cs: ref Clex): int
+{
+	for(;;){
+		while((c := cs.getc()) == ' ' || c == '\t' || c == '\n'  || c == '\r' || c == '\f')
+			;
+		if(c != '/')
+			return c;
+		c = cs.getc();
+		if(c != '*'){
+			cs.ungetc(c);
+			return '/';
+		}
+		comment(cs);
+	}
+}
+
+seq(cs: ref Clex, s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if((c := cs.getc()) != s[i])
+			break;
+	if(i == len s)
+		return 1;
+	cs.ungetc(c);
+	while(i > 0)
+		cs.ungetc(s[--i]);
+	if(c < 0)
+		return -1;
+	return 0;
+}
+
+subseq(cs: ref Clex, a: int, t: int, e: int): int
+{
+	if((c := cs.getc()) != a){
+		cs.ungetc(c);
+		return e;
+	}
+	return t;
+}
+
+pushback(cs: ref Clex, wd: string)
+{
+	for(i := len wd; --i >= 0;)
+		cs.ungetc(wd[i]);
+}
+
+comment(cs: ref Clex)
+{
+	while((c := cs.getc()) != '*' || (c = cs.getc()) != '/')
+		if(c < 0) {
+			# end of file in comment
+			break;
+		}
+}
+
+number(cs: ref Clex, c: int): string
+{
+	s: string;
+	for(; isdigit(c); c = cs.getc())
+		s[len s] = c;
+	if(c != '.'){
+		cs.ungetc(c);
+		return s;
+	}
+	if(!isdigit(c = cs.getc())){
+		cs.ungetc(c);
+		cs.ungetc('.');
+		return s;
+	}
+	s[len s] = '.';
+	do{
+		s[len s] = c;
+	}while(isdigit(c = cs.getc()));
+	cs.ungetc(c);
+	return s;
+}
+
+name(cs: ref Clex, c: int): string
+{
+	s: string;
+	for(; isnamec(c, 1); c = cs.getc()){
+		s[len s] = c;
+		if(c == '\\'){
+			c = cs.getc();
+			if(isescapable(c))
+				s[len s] = c;
+		}
+	}
+	cs.ungetc(c);
+	return s;
+}
+
+isescapable(c: int): int
+{
+	return c >= ' ' && c <= '~' || isnamec(c, 1);
+}
+
+islatin1(c: int): int
+{
+	return c >= 16rA1 && c <= 16rFF;	# printable latin-1
+}
+
+isnamec(c: int, notfirst: int): int
+{
+	return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c == '\\' ||
+		notfirst && (c >= '0' && c <= '9' || c == '-') ||
+		c >= 16rA1 && c <= 16rFF;	# printable latin-1
+}
+
+isxdigit(c: int): int
+{
+	return c>='0' && c<='9' || c>='a'&&c<='f' || c>='A'&&c<='F';
+}
+
+isdigit(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
+}
+
+hex(c: int): int
+{
+	if(c >= '0' && c <= '9')
+		return c-'0';
+	if(c >= 'A' && c <= 'F')
+		return c-'A' + 10;
+	if(c >= 'a' && c <= 'f')
+		return c-'a' + 10;
+	return -1;
+}
+
+quotedstring(cs: ref Clex, delim: int): string
+{
+	s: string;
+	while((c := cs.getc()) != delim){
+		if(c < 0){
+			cs.synerr("end-of-file in string");
+			return s;
+		}
+		if(c == '\\'){
+			c = cs.getc();
+			if(c < 0){
+				cs.synerr("end-of-file in string");
+				return s;
+			}
+			if(isxdigit(c)){
+				# unicode escape
+				n := 0;
+				for(i := 0;;){
+					n = (n<<4) | hex(c);
+					c = cs.getc();
+					if(!isxdigit(c) || ++i >= 6){
+						if(!isspace(c))
+							cs.ungetc(c);	# CSS2 ignores the first white space following
+						break;
+					}
+				}
+				s[len s] = n;
+			}else if(c == '\n'){
+				;	# escaped newline
+			}else if(isescapable(c))
+				s[len s] = c;
+		}else if(c)
+			s[len s] = c;
+	}
+	return s;
+}
+
+url(cs: ref Clex): string
+{
+	s: string;
+	c := skipws(cs);
+	if(c != '"' && c != '\''){	# not a quoted string
+		while(c != ' ' && c != '\n' && c != '\'' && c != '"' && c != ')'){
+			s[len s] = c;
+			c = cs.getc();
+			if(c == '\\'){
+				c = cs.getc();
+				if(c < 0){
+					cs.synerr("end of file in url parameter");
+					break;
+				}
+				if(c == ' ' || c == '\'' || c == '"' || c == ')')
+					s[len s] = c;
+				else{
+					cs.synerr("invalid escape sequence in url");
+					s[len s] = '\\';
+					s[len s] = c;
+				}
+				c = cs.getc();
+			}
+		}
+		cs.ungetc(c);
+#		if(s == nil)
+#			p.synerr("empty parameter to url");
+	}else
+		s = quotedstring(cs, c);
+	if((c = skipws(cs)) != ')'){
+		cs.synerr("unclosed parameter to url");
+		cs.ungetc(c);
+	}
+	return s;
+}
+
+lowercase(s: string): string
+{
+	for(i := 0; i < len s; i++)
+		if((c := s[i]) >= 'A' && c <= 'Z')
+			s[i] = c-'A' + 'a';
+	return s;
+}
--- /dev/null
+++ b/appl/lib/w3c/mkfile
@@ -1,0 +1,19 @@
+<../../../mkconfig
+
+TARG=\
+	css.dis\
+	uris.dis\
+	xpointers.dis\
+
+MODULES=
+
+SYSMODULES= \
+	sys.m\
+	bufio.m\
+	css.m\
+	uris.m\
+	xpointers.m\
+
+DISBIN=$ROOT/dis/lib/w3c
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/lib/w3c/uris.b
@@ -1,0 +1,326 @@
+implement URIs;
+
+#
+# RFC3986, URI Generic Syntax
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "string.m";
+	S: String;
+
+include "uris.m";
+
+Alpha: con "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+Digit: con "0123456789";
+
+GenDelims: con ":/?#[]@";
+SubDelims: con "!$&'()*+,;=";
+Reserved: con GenDelims + SubDelims;
+HexDigit: con Digit+"abcdefABCDEF";
+
+Escape: con GenDelims+"%";	# "%" must be encoded as %25
+
+Unreserved: con Alpha+Digit+"-._~";
+
+F_Esc, F_Scheme: con byte(1<<iota);
+
+ctype: array of byte;
+
+classify(s: string, f: byte)
+{
+	for(i := 0; i < len s; i++)
+		ctype[s[i]] |= f;
+}
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	S = load String String->PATH;
+	if(S == nil)
+		raise sys->sprint("can't load %s: %r", String->PATH);
+
+	ctype = array [256] of { * => byte 0 };
+	classify(Escape, F_Esc);
+	for(i := 0; i <= ' '; i++)
+		ctype[i] |= F_Esc;
+	for(i = 16r80; i <= 16rFF; i++)
+		ctype[i] |= F_Esc;
+	classify(Alpha+Digit+"+-.", F_Scheme);
+}
+
+#      scheme://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
+#
+#      ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+#
+#	delimiters:  :/?#  /?#  ?#  #
+#
+URI.parse(url: string): ref URI
+{
+	scheme, userinfo, host, port, path, query, frag: string;
+	for(i := 0; i < len url; i++){
+		c := url[i];
+		if(c == ':'){
+			scheme = S->tolower(url[0:i]);
+			url = url[i+1:];
+			break;
+		}
+		if(c < 0 || c >= len ctype || (ctype[c] & F_Scheme) == byte 0)
+			break;
+	}
+
+	if(S->prefix("//", url)){
+		authority: string;
+		(authority, path) = S->splitstrl(url[2:], "/");
+		(up, hp) := splitl(authority, "@");
+		if(hp == "")
+			hp = authority;
+		else
+			userinfo = up;
+		if(hp != nil && hp[0] == '['){	# another rfc hack, for IPv6 addresses, which contain :
+			(host, hp) = S->splitstrr(hp, "]");
+			if(hp != nil && hp[0] == ':')
+				port = hp[1:];
+			else
+				host += hp;	# put it back
+		}else
+			(host, port) = splitl(hp, ":");
+		if(path == nil)
+			path = "/";
+	}else
+		path = url;
+	(path, frag) = S->splitstrl(path, "#");		# includes # in frag
+	(path, query) = S->splitstrl(path, "?");	#  includes ? in query
+	return ref URI(scheme, dec(userinfo), dec(host), port, dec(path), query, dec(frag));
+}
+
+URI.userpw(u: self ref URI): (string, string)
+{
+	return splitl(u.userinfo, ":");
+}
+
+URI.text(u: self ref URI): string
+{
+	s := "";
+	if(u.scheme != nil)
+		s += u.scheme + ":";
+	if(u.hasauthority())
+		s += "//" + u.authority();
+	return s + enc(u.path, "/@:") + u.query + enc1(u.fragment, "@:/?");
+}
+
+URI.copy(u: self ref URI): ref URI
+{
+	return ref *u;
+}
+
+URI.pathonly(u: self ref URI): ref URI
+{
+	v := ref *u;
+	v.userinfo = nil;
+	v.query = nil;
+	v.fragment = nil;
+	return v;
+}
+
+URI.addbase(u: self ref URI, b: ref URI): ref URI
+{
+	# RFC3986 5.2.2, rearranged
+	r := ref *u;
+	if(r.scheme == nil && b != nil){
+		r.scheme = b.scheme;
+		if(!r.hasauthority()){
+			r.userinfo = b.userinfo;
+			r.host = b.host;
+			r.port = b.port;
+			if(r.path == nil){
+				r.path = b.path;
+				if(r.query == nil)
+					r.query = b.query;
+			}else if(r.path[0] != '/'){
+				# 5.2.3: merge paths
+				if(b.path == "" && b.hasauthority())
+					p1 := "/";
+				else
+					(p1, nil) = S->splitstrr(b.path, "/");
+				r.path = p1 + r.path;
+			}
+		}
+	}
+	r.path = removedots(r.path);
+	return r;
+}
+
+URI.nodots(u: self ref URI): ref URI
+{
+	return u.addbase(nil);
+}
+
+URI.hasauthority(u: self ref URI): int
+{
+	return u.host != nil || u.userinfo != nil || u.port != nil;
+}
+
+URI.isabsolute(u: self ref URI): int
+{
+	return u.scheme != nil;
+}
+
+URI.authority(u: self ref URI): string
+{
+	s := enc(u.userinfo, ":");
+	if(s != nil)
+		s += "@";
+	if(u.host != nil){
+		s += enc(u.host, "[]:");	# assumes : appears inside []; could enforce it
+		if(u.port != nil)
+			s += ":" + enc(u.port,nil);
+	}
+	return s;
+}
+
+#
+# simplified version of procedure in RFC3986 5.2.4:
+# it extracts a complete segment from the input first, then analyses it
+#
+removedots(s: string): string
+{
+	if(s == nil)
+		return "";
+	out := "";
+	for(p := 0; p < len s;){
+		# extract the first segment and any preceding /
+		q := p;
+		if(++p < len s){
+			while(++p < len s && s[p] != '/')
+				{}
+		}
+		seg := s[q: p];
+		if((e := p) < len s)
+			e++;
+		case s[q: e] {	# includes any following /
+		"../" or "./" =>	;
+		"/./" or "/." =>
+			if(p >= len s)
+				s += "/";
+		"/../" or "/.." =>
+			if(p >= len s)
+				s += "/";
+			if(out != nil){
+				for(q = len out; --q > 0 && out[q] != '/';)
+					{}	# skip
+				out = out[0: q];
+			}
+		"." or ".." =>	;	# null effect
+		* =>		# including "/"
+			out += seg;
+		}
+	}
+	return out;
+}
+
+#
+# similar to splitstrl but trims the matched character from the result
+#
+splitl(s, c: string): (string, string)
+{
+	(a, b) := S->splitstrl(s, c);
+	if(b != "")
+		b = b[1:];
+	return (a, b);
+}
+
+hex2(s: string): int
+{
+	n := 0;
+	for(i := 0; i < 2; i++){
+		if(i >= len s)
+			return -1;
+		n <<= 4;
+		case c := s[i] {
+		'0' to '9' =>
+			n += c-'0';
+		'a' to 'f' =>
+			n += 10+(c-'a');
+		'A' to 'F' =>
+			n += 10+(c-'A');
+		* =>
+			return -1;
+		}
+	}
+	return n;
+}
+
+dec(s: string): string
+{
+	for(i := 0;; i++){
+		if(i >= len s)
+			return s;
+		if(s[i] == '%' || s[i] == 0)
+			break;
+	}
+	t := s[0:i];
+	a := array[Sys->UTFmax*len s] of byte;	# upper bound
+	o := 0;
+	while(i < len s){
+		c := s[i++];
+		if(c < 16r80){
+			case c {
+			'%' =>
+				if((v := hex2(s[i:])) > 0){
+					c = v;
+					i += 2;
+				}
+			0 =>
+				c = ' ';	# shouldn't happen
+			}
+			a[o++] = byte c;
+		}else
+			o += sys->char2byte(c, a, o);	# string contained Unicode
+	}
+	return t + string a[0:o];
+}
+
+enc1(s: string, safe: string): string
+{
+	if(len s > 1)
+		return s[0:1] + enc(s[1:], safe);
+	return s;
+}
+
+# encoding depends on context (eg, &=/: not escaped in `query' string)
+enc(s: string, safe: string): string
+{
+	for(i := 0;; i++){
+		if(i >= len s)
+			return s;	# use as-is
+		c := s[i];
+		if(c >= 16r80 || (ctype[c] & F_Esc) != byte 0 && !S->in(c, safe))
+			break;
+	}
+	t := s[0: i];
+	b := array of byte s[i:];
+	for(i = 0; i < len b; i++){
+		c := int b[i];
+		if((ctype[c] & F_Esc) != byte 0 && !S->in(c, safe))
+			t += sys->sprint("%%%.2X", c);
+		else
+			t[len t] = c;
+	}
+	return t; 
+}
+
+URI.eq(u: self ref URI, v: ref URI): int
+{
+	if(v == nil)
+		return 0;
+	return u.scheme == v.scheme && u.userinfo == v.userinfo &&
+		u.host == v.host && u.port == v.port && u.path == v.path &&	# path might need canon
+		u.query == v.query;	# not fragment
+}
+
+URI.eqf(u: self ref URI, v: ref URI): int
+{
+	return u.eq(v) && u.fragment == v.fragment;
+}
--- /dev/null
+++ b/appl/lib/w3c/xpointers.b
@@ -1,0 +1,858 @@
+implement Xpointers;
+
+#
+# Copyright © 2005 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "xpointers.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+#
+# XPointer framework syntax
+#
+# Pointer ::= Shorthand | SchemeBased
+# Shorthand ::= NCName	# from [XML-Names]
+# SchemeBased ::= PointerPart (S? PointerPart)*
+# PointerPart ::= SchemeName '(' SchemeData ')'
+# SchemeName ::= QName	# from [XML-Names]
+# SchemeData ::= EscapedData*
+# EscapedData ::= NormalChar | '^(' | '^)' | '^^' | '(' SchemeData ')'
+# NormalChar ::= UnicodeChar - [()^]
+# UnicodeChar ::= [#x0 - #x10FFFF]
+
+framework(s: string): (string, list of (string, string, string), string)
+{
+	(q, nm, i) := name(s, 0);
+	if(i >= len s){	# Shorthand
+		if(q != nil)
+			return (nil, nil, "shorthand pointer must be unqualified name");
+		if(nm == nil)
+			return (nil, nil, "missing pointer name");
+		return (nm, nil, nil);
+	}
+	# must be SchemeBased
+	l: list of (string, string, string);
+	for(;;){
+		if(nm == nil){
+			if(q != nil)
+				return (nil, nil, sys->sprint("prefix but no local part in name at %d", i));
+			return (nil, nil, sys->sprint("expected name at %d", i));
+		}
+		if(i >= len s || s[i] != '(')
+			return (nil, nil, sys->sprint("expected '(' at %d", i));
+		o := i++;
+		a := "";
+		nesting := 0;
+		for(; i < len s && ((c := s[i]) != ')' || nesting); i++){
+			case c {
+			'^' =>
+				if(i+1 >= len s)
+					return (nil, nil, "unexpected eof after ^");
+				c = s[++i];
+				if(c != '(' && c != ')' && c != '^')
+					return (nil, nil, sys->sprint("invalid escape ^%c at %d", c, i));
+			'(' =>
+				nesting++;
+			')' =>
+				if(--nesting < 0)
+					return (nil, nil, sys->sprint("unbalanced ) at %d", i));
+			}
+			a[len a] = c;
+		}
+		if(i >= len s)
+			return (nil, nil, sys->sprint("unbalanced ( at %d", o));
+		l = (q, nm, a) :: l;
+		if(++i == len s)
+			break;
+		while(i < len s && isspace(s[i]))
+			i++;
+		(q, nm, i) = name(s, i);
+	}
+	rl: list of (string, string, string);
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return (nil, rl, nil);
+}
+
+isspace(c: int): int
+{
+	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
+}
+
+#
+# QName ::= (Prefix ':')? LocalPart
+# Prefix ::= NCName
+# LocalPart ::= NCName
+#
+#NCName :: (Oetter | '_') NCNameChar*
+#NCNameChar :: Oetter | Digit | '.' | '-' | '_' | CombiningChar | Extender
+
+name(s: string, o: int): (string, string, int)
+{
+	(ns, i) := ncname(s, o);
+	if(i >= len s || s[i] != ':')
+		return (nil, ns, i);
+	(nm, j) := ncname(s, i+1);
+	if(j == i+1)
+		return (nil, ns, i);	# assume it's a LocalPart followed by ':'
+	return (ns, nm, j);
+}
+
+ncname(s: string, o: int): (string, int)
+{
+	if(o >= len s || !isalnum(c := s[o]) && c != '_' || c >= '0' && c <= '9')
+		return (nil, o);	# missing or invalid start character
+	for(i := o; i < len s && isnamec(s[i]); i++)
+		;
+	return (s[o:i], i);
+}
+
+isnamec(c: int): int
+{
+	return isalnum(c) || c == '_' || c == '-' || c == '.';
+}
+
+isalnum(c: int): int
+{
+	#
+	# Hard to get absolutely right without silly amount of character data.
+	# Use what we know about ASCII
+	# and assume anything above the Oatin control characters is
+	# potentially an alphanumeric.
+	#
+	if(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9')
+		return 1;	# usual case
+	if(c <= ' ')
+		return 0;
+	if(c > 16rA0)
+		return 1;	# non-ASCII
+	return 0;
+}
+
+# schemes: xpointer(), xmlns(), element()
+
+# xmlns()
+#	XmlnsSchemeData ::= NCName S? '=' S? EscapedNamespaceName
+#	EscapedNamespaceName ::= EscapedData*
+
+xmlns(s: string): (string, string, string)
+{
+	(nm, i) := ncname(s, 0);
+	if(nm == nil)
+		return (nil, nil, "illegal namespace name");
+	while(i < len s && isspace(s[i]))
+		i++;
+	if(i >= len s || s[i++] != '=')
+		return (nil, nil, "illegal xmlns declaration");
+	while(i < len s && isspace(s[i]))
+		i++;
+	return (nm, s[i:], nil);
+}
+
+# element()
+#	ElementSchemeData ::= (NCName ChildSequence?) | ChildSequence
+#	ChildSequence ::= ('/' [1-9] [0-9]*)+
+
+element(s: string): (string, list of int, string)
+{
+	nm: string;
+	i := 0;
+	if(s != nil && s[0] != '/'){
+		(nm, i) = ncname(s, 0);
+		if(nm == nil)
+			return (nil, nil, "illegal element name");
+	}
+	l: list of int;
+	do{
+		if(i >= len s || s[i++] != '/')
+			return (nil, nil, "illegal child sequence (expected '/')");
+		v := 0;
+		do{
+			if(i >= len s || !isdigit(s[i]))
+				return (nil, nil, "illegal child sequence (expected integer)");
+			v = v*10 + s[i]-'0';
+		}while(++i < len s && s[i] != '/');
+		l = v :: l;
+	}while(i < len s);
+	rl: list of int;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return (nm, rl, nil);
+}
+
+# xpointer()
+#	XpointerSchemeData ::= Expr	# from Xpath, with new functions and data types
+
+xpointer(s: string): (ref Xpath, string)
+{
+	p := ref Parse(ref Rd(s, 0, 0), nil);
+	{
+		e := expr(p, 0);
+		if(p.r.i < len s)
+			synerr("missing operator");
+		return (e, nil);
+	}exception e{
+	"syntax error*" =>
+		return (nil, e);
+	* =>
+		raise;
+	}
+}
+
+Lerror, Ldslash, Lint, Lreal, Llit, Lvar, Ldotdot, Lop, Laxis, Lfn: con 'a'+iota;	# internal lexical items
+
+Keywd: adt {
+	name:	string;
+	val:	int;
+};
+
+axes: array of Keywd = array[] of {
+	("ancestor", Aancestor),
+	("ancestor-or-self", Aancestor_or_self),
+	("attribute", Aattribute),
+	("child", Achild),
+	("descendant", Adescendant),
+	("descendant-or-self", Adescendant_or_self),
+	("following", Afollowing),
+	("following-sibling", Afollowing_sibling),
+	("namespace", Anamespace),
+	("parent", Aparent),
+	("preceding", Apreceding),
+	("preceding-sibling", Apreceding_sibling),
+	("self", Aself),
+};
+
+keywds: array of Keywd = array[] of {
+	("and", Oand),
+	("comment", Onodetype),
+	("div", Odiv),
+	("mod", Omod),
+	("node", Onodetype),
+	("or", Oor),
+	("processing-instruction", Onodetype),
+	("text", Onodetype),
+};
+
+iskeywd(s: string): int
+{
+	return look(keywds, s);
+}
+
+look(k: array of Keywd, s: string): int
+{
+	for(i := 0; i < len k; i++)
+		if(k[i].name == s)
+			return k[i].val;
+	return 0;
+}
+
+lookname(k: array of Keywd, v: int): string
+{
+	for(i := 0; i < len k; i++)
+		if(k[i].val == v)
+			return k[i].name;
+	return nil;
+}
+
+prectab := array[] of {
+	array[] of {Oor},
+	array[] of {Oand},
+	array[] of {'=', One},
+	array[] of {'<', Ole, '>', Oge},
+	array[] of {'+', '-'},
+	array[] of {Omul, Odiv, Omod},
+	array[] of {Oneg},	# unary '-'
+	array[] of {'|'},	# UnionExpr
+};
+
+isop(t: int, p: array of int): int
+{
+	if(t >= 0)
+		for(j := 0; j < len p; j++)
+			if(t == p[j])
+				return 1;
+	return 0;
+}
+
+# Expr ::= OrExpr
+# UnionExpr ::= PathExpr | UnionExpr '|' PathExpr
+# PathExpr ::= LocationPath | FilterExpr | FilterExpr '/' RelativeLocationPath |
+#			FilterExpr '//' RelativeLocationPath
+# OrExpr ::= AndExpr | OrExpr 'or' AndExpr
+# AndExpr ::= EqualityExpr | AndExpr 'and' EqualityExpr
+# EqualityExpr ::= RelationalExpr | EqualityExpr '=' RelationalExpr | EqualityExpr '!=' RelationalExpr
+# RelationalExpr ::= AdditiveExpr | RelationalExpr '<' AdditiveExpr | RelationalExpr '>' AdditiveExpr |
+#				RelationalExpr '<=' AdditiveExpr | RelationalExpr '>=' AdditiveExpr
+# AdditiveExpr ::= MultiplicativeExpr | AdditiveExpr '+' MultiplicativeExpr | AdditiveExpr '-' MultiplicativeExpr
+# MultiplicativeExpr ::= UnaryExpr | MultiplicativeExpr MultiplyOperator UnaryExpr |
+#				MultiplicativeExpr 'div' UnaryExpr | MultiplicativeExpr 'mod' UnaryExpr
+# UnaryExpr ::= UnionExpr | '-' UnaryExpr
+
+expr(p: ref Parse, k: int): ref Xpath
+{
+	if(k >= len prectab)
+		return pathexpr(p);
+	if(prectab[k][0] == Oneg){	# unary '-'
+		if(p.look() == '-'){
+			p.get();
+			return ref Xpath.E(Oneg, expr(p,k+1), nil);
+		}
+		# must be UnionExpr
+		k++;
+	}
+	e := expr(p, k+1);
+	while(isop(p.look(), prectab[k])){
+		o := p.get().t0;
+		e = ref Xpath.E(o, e, expr(p, k+1));	# +assoc[k]
+	}
+	return e;
+}
+
+# PathExpr ::= LocationPath | FilterExpr ( ('/' | '//') RelativeLocationPath )
+# FilterExpr ::= PrimaryExpr | FilterExpr Predicate => PrimaryExpr Predicate*
+
+pathexpr(p: ref Parse): ref Xpath
+{
+	# LocationPath?
+	case p.look() {
+	'.' or Ldotdot or Laxis or '@' or Onametest or Onodetype or '*' =>
+		return locationpath(p, 0);
+	'/' or Ldslash =>
+		return locationpath(p, 1);
+	}
+	# FilterExpr
+	e := primary(p);
+	while(p.look() == '[')
+		e = ref Xpath.E(Ofilter, e, predicate(p));
+	if((o := p.look()) == '/' || o == Ldslash)
+		e = ref Xpath.E(Opath, e, locationpath(p, 0));
+	return e;
+}
+
+# LocationPath ::= RelativeLocationPath | AbsoluteLocationPath
+# AbsoluteLocationPath ::= '/' RelativeLocationPath? | AbbreviatedAbsoluteLocationPath
+# RelativeLocationPath ::= Step | RelativeLocationPath '/' Step
+# AbbreviatedAbsoluteLocationPath ::= '//' RelativeLocationPath
+# AbbreviatedRelativeLocationPath ::= RelativeLocationPath '//' Step
+
+locationpath(p: ref Parse, abs: int): ref Xpath
+{
+	# // => /descendent-or-self::node()/
+	pl: list of ref Xstep;
+	o := p.look();
+	if(o != '/' && o != Ldslash){
+		s := step(p);
+		if(s == nil)
+			synerr("expected Step in LocationPath");
+		pl = s :: pl;
+	}
+	while((o = p.look()) == '/' || o == Ldslash){
+		p.get();
+		if(o == Ldslash)
+			pl = ref Xstep(Adescendant_or_self, Onodetype, nil, "node", nil, nil) :: pl;
+		s := step(p);
+		if(s == nil){
+			if(abs && pl == nil)
+				break;	# it's just an initial '/'
+			synerr("expected Step in LocationPath");
+		}
+		pl = s :: pl;
+	}
+	return ref Xpath.Path(abs, rev(pl));
+}
+
+# Step ::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep
+# AxisSpecifier ::= AxisName '::' | AbbreviatedAxisSpecifier
+# AxisName := ... # long list
+# NodeTest ::= NameTest | NodeType '(' ')'
+# Predicate ::= '[' PredicateExpr ']'
+# PredicateExpr ::= Expr
+# AbbreviatedStep ::= '.' | '..'
+# AbbreviatedAxisSpecifier ::= '@'?
+
+step(p: ref Parse): ref Xstep
+{
+	# AxisSpecifier ... | AbbreviatedStep
+	(o, ns, nm) := p.get();
+	axis := Achild;
+	case o {
+	'.' =>
+		return ref Xstep(Aself, Onodetype, nil, "node", nil, nil);	# self::node()
+	Ldotdot =>
+		return ref Xstep(Aparent, Onodetype, nil, "node", nil, nil);	# parent::node()
+	Laxis =>
+		axis = look(axes, ns);
+		(o, ns, nm) = p.get();
+	'@' =>
+		axis = Aattribute;
+		(o, ns, nm) = p.get();
+	* =>
+		;
+	}
+
+	if(o == '*'){
+		o = Onametest;
+		nm = "*";
+		ns = nil;
+	}
+
+	# NodeTest ::= NameTest | NodeType '(' ')'
+	if(o != Onametest && o != Onodetype){
+		p.unget((o, ns, nm));
+		return nil;
+	}
+
+	arg: string;
+	if(o == Onodetype){	# '(' ... ')'
+		expect(p, '(');
+		# grammar is wrong: processing-instruction can have optional literal
+		if(nm == "processing-instruction" && p.look() == Llit)
+			arg = p.get().t1;
+		expect(p, ')');
+	}
+
+	# Predicate*
+	pl: list of ref Xpath;
+	while((pe := predicate(p)) != nil)
+		pl = pe :: pl;
+	return ref Xstep(axis, o, ns, nm, arg, rev(pl));
+}
+
+# PrimaryExpr ::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall
+# FunctionCall ::= FunctionName '(' (Argument ( ',' Argument)*)? ')'
+# Argument ::= Expr
+
+primary(p: ref Parse): ref Xpath
+{
+	(o, ns, nm) := p.get();
+	case o {
+	Lvar =>
+		return ref Xpath.Var(ns, nm);
+	'(' =>
+		e := expr(p, 0);
+		expect(p, ')');
+		return e;
+	Llit =>
+		return ref Xpath.Str(ns);
+	Lint =>
+		return ref Xpath.Int(big ns);
+	Lreal =>
+		return ref Xpath.Real(real ns);
+	Lfn =>
+		expect(p, '(');
+		al: list of ref Xpath;
+		if(p.look() != ')'){
+			for(;;){
+				al = expr(p, 0) :: al;
+				if(p.look() != ',')
+					break;
+				p.get();
+			}
+			al = rev(al);
+		}
+		expect(p, ')');
+		return ref Xpath.Fn(ns, nm, al);
+	* =>
+		synerr("invalid PrimaryExpr");
+		return nil;
+	}
+}
+
+# Predicate ::= '[' PredicateExpr ']'
+# PredicateExpr ::= Expr
+
+predicate(p: ref Parse): ref Xpath
+{
+	l := p.get();
+	if(l.t0 != '['){
+		p.unget(l);
+		return nil;
+	}
+	e := expr(p, 0);
+	expect(p, ']');
+	return e;
+}
+
+expect(p: ref Parse, t: int)
+{
+	l := p.get();
+	if(l.t0 != t)
+		synerr(sys->sprint("expected '%c'", t));
+}
+
+Xpath.text(e: self ref Xpath): string
+{
+	if(e == nil)
+		return "nil";
+	pick r := e {
+	E =>
+		if(r.r == nil)
+			return sys->sprint("(%s%s)", opname(r.op), r.l.text());
+		if(r.op == Ofilter)
+			return sys->sprint("%s[%s]", r.l.text(), r.r.text());
+		return sys->sprint("(%s%s%s)", r.l.text(), opname(r.op), r.r.text());
+	Fn =>
+		a := "";
+		for(l := r.args; l != nil; l = tl l)
+			a += sys->sprint(",%s", (hd l).text());
+		if(a != "")
+			a = a[1:];
+		return sys->sprint("%s(%s)", qual(r.ns, r.name), a);
+	Var =>
+		return sys->sprint("$%s", qual(r.ns, r.name));
+	Path =>
+		if(r.abs)
+			t := "/";
+		else
+			t = "";
+		for(l := r.steps; l != nil; l = tl l){
+			if(t != nil && t != "/")
+				t += "/";
+			t += (hd l).text();
+		}
+		return t;
+	Int =>
+		return sys->sprint("%bd", r.val);
+	Real =>
+		return sys->sprint("%g", r.val);
+	Str =>
+		return sys->sprint("%s", str(r.s));
+	}
+}
+
+qual(ns: string, nm: string): string
+{
+	if(ns != nil)
+		return ns+":"+nm;
+	return nm;
+}
+
+str(s: string): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '\'')
+			return sys->sprint("\"%s\"", s);
+	return sys->sprint("'%s'", s);
+}
+
+opname(o: int): string
+{
+	case o {
+	One =>	return "!=";
+	Ole =>	return "<=";
+	Oge =>	return ">=";
+	Omul =>	return "*";
+	Odiv =>	return " div ";
+	Omod =>	return " mod ";
+	Oand =>	return " and ";
+	Oor =>	return " or ";
+	Oneg =>	return "-";
+	Ofilter =>	return " op_filter ";
+	Opath =>	return "/";
+	* =>	return sys->sprint(" %c ", o);
+	}
+}
+
+Xstep.text(s: self ref Xstep): string
+{
+	t := sys->sprint("%s::", Xstep.axisname(s.axis));
+	case s.op {
+	Onametest =>
+		if(s.ns == "*" && s.name == "*")
+			t += "*";
+		else
+			t += qual(s.ns, s.name);
+	Onodetype =>
+		if(s.arg != nil)
+			t += sys->sprint("%s(%s)", s.name, str(s.arg));
+		else
+			t += sys->sprint("%s()", s.name);
+	}
+	for(l := s.preds; l != nil; l = tl l)
+		t += sys->sprint("[%s]", (hd l).text());
+	return t;
+}
+
+Xstep.axisname(n: int): string
+{
+	return lookname(axes, n);
+}
+
+# ExprToken ::= '(' | ')' | '[' | ']' | '.' | '..' | '@' | ',' | '::' |
+#				NameTest | NodeType | Operator | FunctionName | AxisName |
+#				Literal | Number | VariableReference
+# Operator ::= OperatorName | MultiplyOperator | '/' | '//' | '|' | '+' | '' | '=' | '!=' | '<' | '<=' | '>' | '>='
+# MultiplyOperator ::= '*'
+# FunctionName ::= QName - NodeType
+# VariableReference ::= '$' QName
+# NameTest ::= '*' | NCName ':' '*' | QName
+# NodeType ::= 'comment' | 'text' | 'processing-instruction' | 'node'
+#
+
+Lex: type (int, string, string);
+
+Parse: adt {
+	r:	ref Rd;
+	pb:	list of Lex;	# push back
+
+	look:	fn(p: self ref Parse): int;
+	get:	fn(p: self ref Parse): Lex;
+	unget:	fn(p: self ref Parse, t: Lex);
+};
+
+Parse.get(p: self ref Parse): Lex
+{
+	if(p.pb != nil){
+		h := hd p.pb;
+		p.pb = tl p.pb;
+		return h;
+	}
+	return lex(p.r);
+}
+
+Parse.look(p: self ref Parse): int
+{
+	t := p.get();
+	p.unget(t);
+	return t.t0;
+}
+
+Parse.unget(p: self ref Parse, t: Lex)
+{
+	p.pb = t :: p.pb;
+}
+
+lex(r: ref Rd): Lex
+{
+	l := lex0(r);
+	r.prev = l.t0;
+	return l;
+}
+
+# disambiguating rules are D1 to D3
+
+# D1. preceding token p && p not in {'@', '::', '(', '[', ',', Operator} then '*' is MultiplyOperator
+#     and NCName must be OperatorName
+
+xop(t: int): int
+{
+	case t {
+	-1 or 0 or '@' or '(' or '[' or ',' or Lop or Omul or
+	'/' or Ldslash or '|' or '+' or '-' or '=' or One or '<' or Ole or '>' or Oge or
+	Oand or Oor or Omod or Odiv or Laxis =>
+		return 0;
+	}
+	return 1;
+}
+
+# UnaryExpr ::= UnionExpr | '-' UnaryExpr
+# ExprToken ::= ... |
+#				NameTest | NodeType | Operator | FunctionName | AxisName |
+#				Literal | Number | VariableReference
+# Operator ::= OperatorName | MultiplyOperator | '/' | '//' | '|' | '+' | '' | '=' | '!=' | '<' | '<=' | '>' | '>='
+# MultiplyOperator ::= '*'
+
+lex0(r: ref Rd): Lex
+{
+	while(isspace(r.look()))
+		r.get();
+	case c := r.get() {
+	-1 or
+	'(' or ')' or '[' or ']' or '@' or ',' or '+' or '-' or '|' or '=' or ':' =>
+		# singletons ('::' only valid after name, see below)
+		return (c, nil, nil);
+	'/' =>
+		return subseq(r, '/', Ldslash, '/');
+	'!' =>
+		return subseq(r, '=', One, '!');
+	'<' =>
+		return subseq(r, '=', Ole, '<');
+	'>' =>
+		return subseq(r, '=', Oge, '>');
+	'*' =>
+		if(xop(r.prev))
+			return (Omul, nil, nil);
+		return (c, nil, nil);
+	'.' =>
+		case r.look() {
+		'0' to '9' =>
+			(v, nil) := number(r, r.get());
+			return (Lreal, v, nil);
+		'.' =>
+			r.get();
+			return (Ldotdot, nil, nil);
+		* =>
+			return ('.', nil, nil);
+		}
+	'$' =>
+		# variable reference
+		(ns, nm, i) := name(r.s, r.i);
+		if(ns == nil && nm == nil)
+			return (Lerror, nil, nil);
+		r.i = i;
+		return (Lvar, ns, nm);
+	'0' to '9' =>
+		(v, f) := number(r, c);
+		if(f)
+			return (Lreal, v, nil);
+		return (Lint, v, nil);
+	'"' or '\'' =>
+		return (Llit, literal(r, c), nil);
+	* =>
+		if(isalnum(c) || c == '_'){
+			# QName/NCName
+			r.unget();
+			(ns, nm, i) := name(r.s, r.i);
+			if(ns == nil && nm == nil)
+				return (Lerror, nil, nil);
+			r.i = i;
+			if(xop(r.prev)){
+				if(ns == nil){
+					o := iskeywd(nm);
+					if(o != Laxis && o != Onodetype)
+						return (o, nil, nil);
+				}
+				return (Lop, ns, nm);
+			}
+			while(isspace(r.look()))
+				r.get();
+			case r.look() {
+			'(' =>		# D2: NCName '(' =>NodeType or FunctionName
+				if(ns == nil && iskeywd(nm) == Onodetype)
+					return (Onodetype, nil, nm);
+				return (Lfn, ns, nm);	# possibly NodeTest
+			':' =>		# D3: NCName '::' => AxisName
+				r.get();
+				case r.look() {
+				':' =>
+					if(ns == nil && look(axes, nm) != 0){
+						r.get();
+						return (Laxis, nm, nil);
+					}
+				'*' =>
+					# NameTest ::= ... | NCName ':' '*'
+					if(ns == nil){
+						r.get();
+						return (Onametest, nm, "*");
+					}
+				}
+				r.unget();	# put back the ':'
+				# NameTest ::= '*' | NCName ':' '*' | QName
+			}
+			return (Onametest, ns, nm);	# actually NameTest
+		}
+		# unexpected character
+	}
+	return (Lerror, nil, nil);
+}
+
+subseq(r: ref Rd, a: int, t: int, e: int): Lex
+{
+	if(r.look() != a)
+		return (e, nil, nil);
+	r.get();
+	return (t, nil, nil);
+}
+
+# Literal ::= '"'[^"]*'"' | "'"[^']* "'"
+
+literal(r: ref Rd, delim: int): string
+{
+	s: string;
+	while((c := r.get()) != delim){
+		if(c < 0){
+			synerr("missing string terminator");
+			return s;
+		}
+		if(c)
+			s[len s] = c;	# could slice r.s
+	}
+	return s;
+}
+
+#
+# Number ::= Digits('.' Digits?)? | '.' Digits
+# Digits ::= [0-9]+
+#
+number(r: ref Rd, c: int): (string, int)
+{
+	s: string;
+	for(; isdigit(c); c = r.get())
+		s[len s] = c;
+	if(c != '.'){
+		if(c >= 0)
+			r.unget();
+		return (s, 0);
+	}
+	if(!isdigit(c = r.get())){
+		if(c >= 0)
+			r.unget();
+		r.unget();	# the '.'
+		return (s, 0);
+	}
+	s[len s] = '.';
+	do{
+		s[len s] = c;
+	}while(isdigit(c = r.get()));
+	if(c >= 0)
+		r.unget();
+	return (s, 1);
+}
+
+isdigit(c: int): int
+{
+	return c>='0' && c<='9';
+}
+
+Rd: adt{
+	s:	string;
+	i:	int;
+	prev:	int;	# previous token
+
+	get:	fn(r: self ref Rd): int;
+	look:	fn(r: self ref Rd): int;
+	unget:	fn(r: self ref Rd);
+};
+
+Rd.get(r: self ref Rd): int
+{
+	if(r.i >= len r.s)
+		return -1;
+	return r.s[r.i++];
+}
+
+Rd.look(r: self ref Rd): int
+{
+	if(r.i >= len r.s)
+		return -1;
+	return r.s[r.i];
+}
+
+Rd.unget(r: self ref Rd)
+{
+	if(r.i > 0)
+		r.i--;
+}
+
+rev[T](l: list of T): list of T
+{
+	rl: list of T;
+	for(; l != nil; l = tl l)
+		rl = hd l :: rl;
+	return rl;
+}
+
+synerr(s: string)
+{
+	raise "syntax error: "+s;
+}
+
+# to do:
+#	dictionary?
--- /dev/null
+++ b/appl/lib/wait.b
@@ -1,0 +1,55 @@
+implement Wait;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "wait.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+read(fd: ref Sys->FD): (int, string, string)
+{
+	buf := array[2*Sys->WAITLEN] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return (n, nil, sys->sprint("%r"));
+	return parse(string buf[0:n]);
+}
+
+monitor(fd: ref Sys->FD): (int, chan of (int, string, string))
+{
+	pid := chan of int;
+	out := chan of (int, string, string);
+	spawn waitreader(fd, pid, out);
+	return (<-pid, out);
+}
+
+waitreader(fd: ref Sys->FD, pid: chan of int, out: chan of (int, string, string))
+{
+	pid <-= sys->pctl(0, nil);
+	for(;;){
+		(child, modname, status) := read(fd);
+		out <-= (child, modname, status);
+		if(child <= 0)
+			break;	# exit on error
+	}
+}
+
+parse(status: string): (int, string, string)
+{
+	for (i := 0; i < len status; i++)
+		if (status[i] == ' ')
+			break;
+	j := i+2;	# skip space and "
+	for (i = j; i < len status; i++)
+		if (status[i] == '"')
+			break;
+	return (int status, status[j:i], status[i+2:]);
+}
--- /dev/null
+++ b/appl/lib/watchvars.b
@@ -1,0 +1,44 @@
+implement Watchvars;
+include "watchvars.m";
+
+Watchvar[T].new(v: T): Watchvar
+{
+	e := Watchvar[T](chan[1] of (T, chan of T));
+	e.c <-= (v, chan[1] of T);
+	return e;
+}
+
+Watchvar[T].get(e: self Watchvar): T
+{
+	(v, ic) := <-e.c;
+	e.c <-= (v, ic);
+	return v;
+}
+
+Watchvar[T].set(e: self Watchvar, v: T)
+{
+	(nil, ic) := <-e.c;
+	ic <-= v;
+	e.c <-= (v, chan[1] of T);
+}
+
+Watchvar[T].wait(e: self Watchvar): T
+{
+	(v, ic) := <-e.c;
+	e.c <-= (v, ic);
+	v = <-ic;
+	ic <-= v;
+	return v;
+}
+
+Watchvar[T].waitc(e: self Watchvar): (T, chan of T)
+{
+	vic := <-e.c;
+	e.c <-= vic;
+	return vic;
+}
+
+Watchvar[T].waited(nil: self Watchvar, ic: chan of T, v: T)
+{
+	ic <-= v;
+}
--- /dev/null
+++ b/appl/lib/winplace.b
@@ -1,0 +1,359 @@
+implement Winplace;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Rect, Point: import draw;
+include "winplace.m";
+
+Delta: adt {
+	d:		int;	# +1 or -1
+	wid:		int;	# index into wr
+	coord:	int;	# x/y coord
+};
+
+EW, NS: con iota;
+Lay: adt {
+	d: int;
+	x: fn(l: self Lay, p: Point): int;
+	y: fn(l: self Lay, p: Point): int;
+	mkr: fn(l: self Lay, r: Rect): Rect;
+};
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+}
+
+place(wins: list of Rect, scr, lastrect: Rect, minsize: Point): Rect
+{
+	found := find(wins, scr);
+	if(found != nil){
+		# first look for any spaces big enough to hold minsize;
+		# choose top-left of those available.
+		(ok, best) := findfit(found, minsize);
+		if (ok){
+			if(minsize.x == 0)
+				return best;
+			return (best.min, best.min.add(minsize));
+		}
+		if(minsize.x == 0)
+			minsize = scr.size().div(2);
+	}
+	# no big enough space; try to avoid covering titlebars
+	tfound := find(titlebarrects(wins), scr);
+	(ok, best) := findfit(tfound, minsize);
+	if (ok)
+		return (best.min, best.min.add(minsize));
+	tfound = nil;
+
+	# no areas available - just find somewhere.
+	if(found == nil)
+		return somespace(scr, lastrect, minsize);
+
+	# no big enough space found; find the largest area available
+	# that will fit within minsize
+	best = clipsize(hd found, minsize);
+	area := best.dx() * best.dy();
+	for (fl := tl found; fl != nil; fl = tl fl) {
+		r := clipsize(hd fl, minsize);
+		rarea := r.dx() * r.dy();
+		if (rarea > area || (rarea == area && better(r, best)))
+			(area, best) = (rarea, r);
+	}
+	best.max = best.min.add(minsize);
+	return checkrect(best, scr);
+}
+
+findfit(found: list of Rect, minsize: Point): (int, Rect)
+{
+	best: Rect;
+	ok := 0;
+	for (fl := found; fl != nil; fl = tl fl) {
+		r := hd fl;
+		if (r.dx() < minsize.x || r.dy() < minsize.y)
+			continue;
+		if (!ok || better(r, best)) {
+			best = r;
+			ok++;
+		}
+	}
+	return (ok, best);
+}
+
+TBARWIDTH: con 100;
+TBARHEIGHT: con 20;
+titlebarrects(rl: list of Rect): list of Rect
+{
+	nl: list of Rect;
+	for (; rl != nil; rl = tl rl) {
+		r := hd rl;
+		tr := Rect((r.max.x - TBARWIDTH, r.min.y),
+					(r.max.x, r.min.y + TBARHEIGHT));
+		if (tr.min.x < r.min.x)
+			tr.min.x = r.min.x;
+		if (tr.max.y > r.max.y)
+			tr.max.y = r.max.y;
+		nl = tr :: nl;
+	}
+	return nl;
+}
+
+somespace(scr, lastrect: Rect, minsize: Point): Rect
+{
+	r := Rect(lastrect.min, lastrect.min.add(minsize)).addpt((20, 20));
+	if (r.max.x > scr.max.x || r.max.y > scr.max.y)
+		r = Rect(scr.min, scr.min.add(minsize));
+	return r;
+}
+
+checkrect(r, scr: Rect): Rect
+{
+	# make sure it's all on screen
+	if (r.max.x > scr.max.x) {
+		dx := r.max.x - scr.max.x;
+		r.max.x -= dx;
+		r.min.x -= dx;
+	}
+	if (r.max.y > scr.max.y) {
+		dy := r.max.y - scr.max.y;
+		r.max.y -= dy;
+		r.min.y -= dy;
+	}
+
+	# make sure origin is on screen.
+	off := r.min.sub(scr.min);
+	if (off.x > 0)
+		off.x = 0;
+	if (off.y > 0)
+		off.y = 0;
+	r = r.subpt(off);
+	return r;
+}
+
+# return true if r1 is ``better'' placed than r2, all other things
+# being equal.
+# currently we choose top-most, left-most, in that order.
+better(r1, r2: Rect): int
+{
+	return r1.min.y < r2.min.y ||
+			(r1.min.y == r2.min.y && r1.min.x < r2.min.x);
+}
+
+clipsize(r: Rect, size: Point): Rect
+{
+	if (r.dx() > size.x)
+		r.max.x = r.min.x + size.x;
+	if (r.dy() > size.y)
+		r.max.y = r.min.y + size.y;
+	return r;
+}
+
+find(wins: list of Rect, scr: Rect): list of Rect
+{
+
+	n := len wins + 4;
+	wr := array[n] of Rect;
+	for (; wins != nil; wins = tl wins)
+		wr[--n] = hd wins;
+	scr2 := scr.inset(-1);
+	# border sentinels
+	wr[3] = Rect((scr.min.x,scr2.min.y), (scr.max.x, scr.min.y));		# top
+	wr[2] = Rect((scr2.min.x, scr2.min.y), (scr.min.x, scr2.max.y));		# left
+	wr[1] = Rect((scr.min.x, scr.max.y), (scr.max.x, scr2.max.y));		# bottom
+	wr[0] = Rect((scr.max.x, scr2.min.y), (scr2.max.x, scr2.max.y));	# right
+	found := sweep(wr, Lay(EW), nil);
+	return sweep(wr, Lay(NS), found);
+}
+
+sweep(wr: array of Rect, lay: Lay, found: list of Rect): list of Rect
+{
+	# sweep through in the direction of lay,
+	# adding and removing end points of rectangles
+	# as we pass them, and maintaining list of current viable rectangles.
+	maj := sortcoords(wr, lay);
+	(cr, ncr) := (array[len wr * 2] of Delta, 0);
+	rl: list of Rect;		# ordered by lay.y(min)
+	for (i := 0; i < len maj; i++) {
+		wid := maj[i].wid;
+		if (maj[i].d > 0)
+			ncr = addwin(cr, ncr, wid, lay.y(wr[wid].min), lay.y(wr[wid].max));
+		else
+			ncr = removewin(cr, ncr, wid, lay.y(wr[wid].min), lay.y(wr[wid].max));
+		nrl: list of Rect = nil;
+		count := 0;
+		for (j := 0; j < ncr - 1; j++) {
+			count += cr[j].d;
+			(start, end) := (cr[j].coord, cr[j+1].coord);
+			if (count == 0 && end > start) {
+				nf: list of Rect;
+				(rl, nrl, nf) = select(rl, nrl, maj[i].coord, start, end);
+				for (; nf != nil; nf = tl nf)
+					found = addfound(found, lay.mkr(hd nf));
+			}
+		}
+		for (; rl != nil; rl = tl rl) {
+			r := hd rl;
+			r.max.x = maj[i].coord;
+			found = addfound(found, lay.mkr(r));
+		}
+		for (; nrl != nil; nrl = tl nrl)
+			rl = hd nrl :: rl;
+		nrl = nil;
+	}
+	return found;
+}
+
+addfound(found: list of Rect, r: Rect): list of Rect
+{
+	if (r.max.x - r.min.x < 1 ||
+			r.max.y - r.min.y < 1)
+		return found;
+	return r :: found;
+}
+
+select(rl, nrl: list of Rect, xcoord, start, end: int): (list of Rect, list of Rect, list of Rect)
+{
+	found: list of Rect;
+	made := 0;
+	while (rl != nil) {
+		r := hd rl;
+		r.max.x = xcoord;
+		(rstart, rend) := (r.min.y, r.max.y);
+		if (rstart >= end)
+			break;
+		addit := 1;
+		if (rstart == start && rend == end) {
+			made = 1;
+		} else {
+			if (!made && rstart > start) {
+				nrl = ((xcoord, start), (xcoord, end)) :: nrl;
+				made = 1;
+			}
+			if (rend > end || rstart < start) {
+				found = r :: found;
+				if (rend > end)
+					rend = end;
+				if (rstart < start)
+					rstart = start;
+				if (rstart >= rend)
+					addit = 0;
+				(r.min.y, r.max.y) = (rstart, rend);
+			}
+		}
+		if (addit)
+			nrl = r :: nrl;
+		rl = tl rl;
+	}
+	if (!made)
+		nrl = ((xcoord, start), (xcoord, end)) :: nrl;
+	return (rl, nrl, found);
+}
+
+removewin(d: array of Delta, nd: int, wid: int, min, max: int): int
+{
+	minidx := finddelta(d, nd, Delta(+1, wid, min));
+	maxidx := finddelta(d, nd, Delta(-1, wid, max));
+	if (minidx == -1 || maxidx == -1 || minidx == maxidx) {
+		sys->fprint(sys->fildes(2),
+				"bad delta find; minidx: %d; maxidx: %d; wid: %d; min: %d; max: %d\n",
+				minidx, maxidx, wid, min, max);
+		raise "panic";
+	}
+	d[minidx:] = d[minidx + 1:maxidx];
+	d[maxidx - 1:] = d[maxidx + 1:nd];
+	return nd - 2;
+}
+
+addwin(d: array of Delta, nd: int, wid: int, min, max: int): int
+{
+	(minidx, maxidx) := (findcoord(d, nd, min), findcoord(d, nd, max));
+	d[maxidx + 2:] = d[maxidx:nd];
+	d[maxidx + 1] = Delta(-1, wid, max);
+	d[minidx + 1:] = d[minidx:maxidx];
+	d[minidx] = Delta(+1, wid, min);
+	return nd + 2;
+}
+
+finddelta(d: array of Delta, nd: int, df: Delta): int
+{
+	idx := findcoord(d, nd, df.coord);
+	for (i := idx; i < nd && d[i].coord == df.coord; i++)
+		if (d[i].wid == df.wid && d[i].d == df.d)
+			return i;
+	for (i = idx - 1; i >= 0 && d[i].coord == df.coord; i--)
+		if (d[i].wid == df.wid && d[i].d == df.d)
+			return i;
+	return -1;
+}
+
+findcoord(d: array of Delta, nd: int, coord: int): int
+{
+	(lo, hi) := (0, nd - 1);
+	while (lo <= hi) {
+		mid := (lo + hi) / 2;
+		if (coord < d[mid].coord)
+			hi = mid - 1;
+		else if (coord > d[mid].coord)
+			lo = mid + 1;
+		else
+			return mid;
+	}
+	return lo;
+}
+
+sortcoords(wr: array of Rect, lay: Lay): array of Delta
+{
+	a := array[len wr * 2] of Delta;
+	j := 0;
+	for (i := 0; i < len wr; i++) {
+		a[j++] = (+1, i, lay.x(wr[i].min));
+		a[j++] = (-1, i, lay.x(wr[i].max));
+	}
+	sortdelta(a);
+	return a;
+}
+
+sortdelta(a: array of Delta)
+{
+	n := len a;
+	for(m := n; m > 1; ) {
+		if(m < 5)
+			m = 1;
+		else
+			m = (5*m-1)/11;
+		for(i := n-m-1; i >= 0; i--) {
+			tmp := a[i];
+			for(j := i+m; j <= n-1 && tmp.coord > a[j].coord; j += m)
+				a[j-m] = a[j];
+			a[j-m] = tmp;
+		}
+	}
+}
+
+Lay.x(l: self Lay, p: Point): int
+{
+	if (l.d == EW)
+		return p.x;
+	return p.y;
+}
+
+Lay.y(l: self Lay, p: Point): int
+{
+	if (l.d == EW)
+		return p.y;
+	return p.x;
+}
+
+Lay.mkr(l: self Lay, r: Rect): Rect
+{
+	if (l.d == EW)
+		return r;
+	return ((r.min.y, r.min.x), (r.max.y, r.max.x));
+}
--- /dev/null
+++ b/appl/lib/wmclient.b
@@ -1,0 +1,346 @@
+implement Wmclient;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Image, Screen, Rect, Point, Pointer, Wmcontext, Context: import draw;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "wmlib.m";
+	wmlib: Wmlib;
+	qword, splitqword, s2r: import wmlib;
+include "titlebar.m";
+	titlebar: Titlebar;
+include "wmclient.m";
+
+Focusnone, Focusimage, Focustitle: con iota;
+
+Bdup: con int 16rffffffff;
+Bddown: con int 16radadadff;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	wmlib = load Wmlib Wmlib->PATH;
+	if(wmlib == nil){
+		sys->fprint(sys->fildes(2), "wmclient: cannot load %s: %r\n", Wmlib->PATH);
+		raise "fail:bad module";
+	}
+	wmlib->init();
+	titlebar = load Titlebar Titlebar->PATH;
+	if(titlebar == nil){
+		sys->fprint(sys->fildes(2), "wmclient: cannot load %s: %r\n", Titlebar->PATH);
+		raise "fail:bad module";
+	}
+	titlebar->init();
+}
+
+makedrawcontext(): ref Draw->Context
+{
+	return wmlib->makedrawcontext();
+}
+
+cursorspec(img: ref Draw->Image): string
+{
+	Hex: con "0123456789abcdef";
+	if(img == nil || img.depth != 1)
+		return "cursor";
+	display := img.display;
+	hot := img.r.min;
+	if(img.r.min.x != 0 || img.r.min.y != 0){
+		n := display.newimage(((0, 0), img.r.size()), Draw->GREY1, 0, Draw->Nofill);
+		n.draw(n.r, img, nil, img.r.min);
+		img = n;
+	}
+	s := sys->sprint("cursor %d %d %d %d ", hot.x, hot.y, img.r.dx(), img.r.dy());
+	nb := img.r.dy() * draw->bytesperline(img.r, img.depth);
+	buf := array[nb] of byte;
+	if(img.readpixels(img.r, buf) == -1)
+		return "cursor";
+
+	for(i := 0; i < nb; i++){
+		c := int buf[i];
+		s[len s] = Hex[c >> 4];
+		s[len s] = Hex[c & 16rf];
+	}
+	return s;
+}
+		
+blankwin: Window;
+window(ctxt: ref Draw->Context, title: string, buts: int): ref Window
+{
+	w := ref blankwin;
+	w.ctxt = wmlib->connect(ctxt);
+	w.display = ctxt.display;
+	w.ctl = chan of string;
+	readscreenrect(w);
+
+	if(buts & Plain)
+		return w;
+
+	if(ctxt.wm == nil)
+		buts &= ~(Resize|Hide);
+
+	w.bd = 1;
+	w.titlebar = tk->toplevel(ctxt.display, nil);
+	top := w.titlebar;
+	top.wreq = nil;
+
+	w.ctl = titlebar->new(top, buts);
+	titlebar->settitle(top, title);
+	sizetb(w);
+	w.wmctl("fixedorigin");
+	return w;
+}
+
+Window.pointer(w: self ref Window, p: Draw->Pointer): int
+{
+	if(w.screen == nil || w.titlebar == nil)
+		return 0;
+
+	if(p.buttons && (w.ptrfocus == Focusnone || w.buttons == 0)){
+		if(p.xy.in(w.tbrect))
+			w.ptrfocus = Focustitle;
+		else
+			w.ptrfocus = Focusimage;
+	}
+	w.buttons = p.buttons;
+	if(w.ptrfocus == Focustitle){
+		tk->pointer(w.titlebar, p);
+		return 1;
+	}
+	return 0;
+}
+
+# titlebar requested size might have changed:
+# find out what size it's requesting.
+sizetb(w: ref Window)
+{
+	if(w.titlebar == nil)
+		return;
+	w.tbsize = tk->rect(w.titlebar, ".", Tk->Border|Tk->Required).size();
+}
+
+# reshape the image; the space needed for the
+# titlebar is added to r.
+Window.reshape(w: self ref Window, r: Rect)
+{
+	w.r = w.screenr(r);
+	if(w.screen == nil)
+		return;
+	w.wmctl(sys->sprint("!reshape . -1 %s", r2s(w.r)));
+}
+
+putimage(w: ref Window, i: ref Image)
+{
+	if(w.screen != nil && i == w.screen.image)
+		return;
+	w.screen = Screen.allocate(i, w.display.color(Draw->White), 0);
+	ir := i.r.inset(w.bd);
+	if(ir.dx() < 0)
+		ir.max.x = ir.min.x;
+	if(ir.dy() < 0)
+		ir.max.y = ir.min.y;
+	if(w.titlebar != nil){
+		w.tbrect = Rect(ir.min, (ir.max.x, ir.min.y + w.tbsize.y));
+		tbimage := w.screen.newwindow(w.tbrect, Draw->Refnone, Draw->Nofill);
+		tk->putimage(w.titlebar, ".", tbimage, nil);
+		ir.min.y = w.tbrect.max.y;
+	}
+	if(ir.dy() < 0)
+		ir.max.y = ir.min.y;
+	w.image = w.screen.newwindow(ir, Draw->Refnone, Draw->Nofill);
+	drawborder(w);
+	w.r = i.r;
+}
+
+# return a rectangle suitable to hold image r when the
+# titlebar and border are included.
+Window.screenr(w: self ref Window, r: Rect): Rect
+{
+	if(w.titlebar != nil){
+		if(r.dx() < w.tbsize.x)
+			r.max.x = r.min.x + w.tbsize.x;
+		r.min.y -= w.tbsize.y;
+	}
+	return r.inset(-w.bd);
+}
+
+# return the available space inside r when space for
+# border and titlebar is taken away.
+Window.imager(w: self ref Window, r: Rect): Rect
+{
+	r = r.inset(w.bd);
+	if(r.dx() < 0)
+		r.max.x = r.min.x;
+	if(r.dy() < 0)
+		r.max.y = r.min.y;
+	if(w.titlebar != nil){
+		r.min.y += w.tbsize.y;
+		if(r.dy() < 0)
+			r.max.y = r.min.y;
+	}
+	return r;
+}
+
+# draw an imitation tk border.
+drawborder(w: ref Window)
+{
+	if(w.screen == nil)
+		return;
+	col := w.display.color(Bdup);
+	i := w.screen.image;
+	r := w.screen.image.r;
+	i.draw((r.min, (r.min.x+w.bd, r.max.y)), col, nil, (0, 0));
+	i.draw(((r.min.x+w.bd, r.min.y), (r.max.x, r.min.y+w.bd)), col, nil, (0, 0));
+	col = w.display.color(Bddown);
+	i.draw(((r.max.x-w.bd, r.min.y+w.bd), r.max), col, nil, (0, 0));
+	i.draw(((r.min.x+w.bd, r.max.y-w.bd), (r.max.x-w.bd, r.max.y)), col, nil, (0, 0));
+}
+
+readscreenrect(w: ref Window)
+{
+	if((fd := sys->open("/chan/wmrect", Sys->OREAD)) != nil){
+		buf := array[12*4] of byte;
+		n := sys->read(fd, buf, len buf);
+		if(n > 0){
+			(w.displayr, nil) = s2r(string buf[0:n], 0);
+			return;
+		}
+	}
+	w.displayr = w.display.image.r;
+}
+
+Window.onscreen(w: self ref Window, how: string)
+{
+	if(how == nil)
+		how = "place";
+	w.wmctl(sys->sprint("!reshape . -1 %s %q", r2s(w.r), how));
+}
+
+Window.startinput(w: self ref Window, devs: list of string)
+{
+	for(; devs != nil; devs = tl devs)
+		w.wmctl(sys->sprint("start %q", hd devs));
+}
+
+# commands originating both from tkclient and wm (via ctl)
+Window.wmctl(w: self ref Window, req: string): string
+{
+	(c, next) := qword(req, 0);
+	case c {
+	"exit" =>
+		sys->fprint(sys->open("/prog/" + string sys->pctl(0, nil) + "/ctl", Sys->OWRITE), "killgrp");
+		exit;
+	# old-style requests: pass them back around in proper form.
+	"move" =>
+		# move x y
+		if(w.titlebar != nil)
+			titlebar->sendctl(w.titlebar, "!move . -1 " + req[next:]);
+	"size" =>
+		if(w.titlebar != nil){
+			minsz := titlebar->minsize(w.titlebar);
+			titlebar->sendctl(w.titlebar, "!size . -1 " + string minsz.x + " " + string minsz.y);
+		}
+	"ok" or
+	"help" =>
+		;
+	"rect" =>
+		(w.displayr, nil) = s2r(req, next);
+	"haskbdfocus" =>
+		w.focused = int qword(req, next).t0;
+		if(w.titlebar != nil){
+			tk->cmd(w.titlebar, "focus -global " + string w.focused);
+			tk->cmd(w.titlebar, "update");
+		}
+		drawborder(w);
+	"task" =>
+		title := "";
+		if(w.titlebar != nil)
+			title = titlebar->title(w.titlebar);
+		wmreq(w, sys->sprint("task %q", title), next);
+		w.saved = w.r.min;
+		# send window out of the way
+		# XXX oops, can't do this for plain windows...
+		titlebar->sendctl(w.titlebar, "!reshape . -1 " + r2s((w.displayr.max, w.displayr.max.add(w.r.size()))));
+	"untask" =>
+		wmreq(w, req, next);
+		# put window back where it was before.
+		# XXX what do we we do if the window manager window has been reshape in the meantime...?
+		titlebar->sendctl(w.titlebar, "!reshape . -1 " + r2s((w.saved, w.saved.add(w.r.size()))));
+	* =>
+		return wmreq(w, req, next);
+	}
+	return nil;
+}
+
+wmreq(w: ref Window, req: string, e: int): string
+{
+	name: string;
+	if(req != nil && req[0] == '!'){
+		(name, e) = qword(req, e);
+		if(name != ".")
+			return "invalid window name";
+	}
+	if(w.ctxt.connfd != nil){
+		if(sys->fprint(w.ctxt.connfd, "%s", req) == -1)
+			return sys->sprint("%r");
+		if(req[0] == '!')
+			recvimage(w);
+		return nil;
+	}
+	# if we're getting an image and there's no window manager,
+	# then there's only one image to get...
+	if(req[0] == '!')
+		putimage(w, w.ctxt.ctxt.display.image);
+	else{
+		(nil, nil, err) := wmlib->wmctl(w.ctxt, req);
+		return err;
+	}
+	return nil;
+}
+
+recvimage(w: ref Window)
+{
+	i := <-w.ctxt.images;
+	if(i == nil)
+		i = <-w.ctxt.images;
+	putimage(w, i);
+}
+
+Window.settitle(w: self ref Window, title: string): string
+{
+	if(w.titlebar == nil)
+		return nil;
+	oldr := w.imager(w.r);
+	old := titlebar->settitle(w.titlebar, title);
+	sizetb(w);
+	if(w.tbsize.x < w.r.dx())
+		tk->putimage(w.titlebar, ".", w.titlebar.image, nil);	# unsuspend the window
+	else
+		w.wmctl("!reshape . -1 " + r2s(w.screenr(oldr)));
+	return old;
+}
+
+snarfget(): string
+{
+	return wmlib->snarfget();
+}
+
+snarfput(buf: string)
+{
+	return wmlib->snarfput(buf);
+}
+
+r2s(r: Rect): string
+{
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
--- /dev/null
+++ b/appl/lib/wmlib.b
@@ -1,0 +1,590 @@
+implement Wmlib;
+
+#
+# Copyright © 2003 Vita Nuova Holdings Limited
+#
+
+# basic window manager functionality, used by
+# tkclient and wmclient to create more usable functionality.
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Image, Screen, Rect, Point, Pointer, Wmcontext, Context: import draw;
+include "wmsrv.m";
+include "wmlib.m";
+
+Client: adt{
+	ptrpid:	int;
+	kbdpid:	int;
+	ctlpid:	int;
+	req:		chan of (array of byte, Sys->Rwrite);
+	dir:		string;
+	ctlfd:		ref Sys->FD;
+	winfd:	ref Sys->FD;
+};
+
+DEVWM: con "/mnt/wm";
+Ptrsize: con 1+4*12;		# 'm' plus 4 12-byte decimal integers
+
+kbdstarted: int;
+ptrstarted: int;
+wptr: chan of Point;		# set mouse position (only if we've opened /dev/pointer directly)
+cswitch: chan of (string, int, chan of string);	# switch cursor images (as for wptr)
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+}
+
+#	(_screen, dispi) := ctxt.display.getwindow("/dev/winname", nil, nil, 1); XXX corrupts heap... fix it!
+
+makedrawcontext(): ref Draw->Context
+{
+	display := Display.allocate(nil);
+	if(display == nil){
+		sys->fprint(sys->fildes(2), "wmlib: can't allocate Display: %r\n");
+		raise "fail:no display";
+	}
+	return ref Draw->Context(display, nil, nil);
+}
+
+importdrawcontext(devdraw, mntwm: string): (ref Draw->Context, string)
+{
+	if(mntwm == nil)
+		mntwm = "/mnt/wm";
+
+	display := Display.allocate(devdraw);
+	if(display == nil)
+		return (nil, sys->sprint("cannot allocate display: %r"));
+	(ok, nil) := sys->stat(mntwm + "/clone");
+	if(ok == -1)
+		return (nil, "cannot find wm namespace");
+	wc := chan of (ref Draw->Context, string);
+	spawn wmproxy(display, mntwm, wc);
+	return <-wc;
+}
+
+# XXX we have no way of knowing when this process should go away...
+# perhaps a Draw->Context should hold a file descriptor
+# so that we do.
+wmproxy(display: ref Display, dir: string, wc: chan of (ref Draw->Context, string))
+{
+	wmsrv := load Wmsrv Wmsrv->PATH;
+	if(wmsrv == nil){
+		wc <-= (nil, sys->sprint("cannot load %s: %r", Wmsrv->PATH));
+		return;
+	}
+	sys->pctl(Sys->NEWFD, 1 :: 2 :: nil);
+
+	(wm, join, req) := wmsrv->init();
+	if(wm == nil){
+		wc <-= (nil, sys->sprint("%r"));
+		return;
+	}
+	wc <-= (ref Draw->Context(display, nil, wm), nil);
+
+	clients: array of ref Client;
+	for(;;) alt{
+	(sc, rc) := <-join =>
+		sync := chan of (ref Client, string);
+		spawn clientproc(display, sc, dir, sync);
+		(c, err) := <-sync;
+		rc <-= err;
+		if(c != nil){
+			if(sc.id >= len clients)
+				clients = (array[sc.id + 1] of ref Client)[0:] = clients;
+			clients[sc.id] = c;
+		}
+	(sc, data, rc) := <-req =>
+		clients[sc.id].req <-= (data, rc);
+		if(rc == nil)
+			clients[sc.id] = nil;
+	}
+}
+
+zclient: Client;
+clientproc(display: ref Display, sc: ref Wmsrv->Client, dir: string, rc: chan of (ref Client, string))
+{
+	ctlfd := sys->open(dir + "/clone", Sys->ORDWR);
+	if(ctlfd == nil){
+		rc <-= (nil, sys->sprint("cannot open %s/clone: %r", dir));
+		return;
+	}
+	buf := array[20] of byte;
+	n := sys->read(ctlfd, buf, len buf);
+	if(n <= 0){
+		rc <-= (nil, "cannot read ctl id");
+		return;
+	}
+	sys->fprint(ctlfd, "fixedorigin");
+	dir += "/" + string buf[0:n];
+	c := ref zclient;
+	c.req = chan of (array of byte, Sys->Rwrite);
+	c.dir = dir;
+	c.ctlfd = ctlfd;
+	if ((c.winfd = sys->open(dir + "/winname", Sys->OREAD)) == nil){
+		rc <-= (nil, sys->sprint("cannot open %s/winname: %r", dir));
+		return;
+	}
+	rc <-= (c, nil);
+
+	pidc := chan of int;
+	spawn ctlproc(pidc, ctlfd, sc.ctl);
+	c.ctlpid = <-pidc;
+	for(;;) {
+		(data, drc) := <-c.req;
+		if(drc == nil)
+			break;
+		err := handlerequest(display, c, sc, data);
+		n = len data;
+		if(err != nil)
+			n = -1;
+		alt{
+		drc <-= (n, err) =>;
+		* =>;
+		}
+	}
+	sc.stop <-= 1;
+	kill(c.kbdpid, "kill");
+	kill(c.ptrpid, "kill");
+	kill(c.ctlpid, "kill");
+	c.ctlfd = nil;
+	c.winfd = nil;
+}
+
+handlerequest(display: ref Display, c: ref Client, sc: ref Wmsrv->Client, data: array of byte): string
+{
+	req := string data;
+	if(req == nil)
+		return nil;
+	(w, e) := qword(req, 0);
+	case w {
+	"start" =>
+		(w, e) = qword(req, e);
+		case w {
+		"ptr" or
+		"mouse" =>
+			if(c.ptrpid == -1)
+				return "already started";
+			fd := sys->open(c.dir + "/pointer", Sys->OREAD);
+			if(fd == nil)
+				return sys->sprint("cannot open %s: %r", c.dir + "/pointer");
+			sync := chan of int;
+			spawn ptrproc(sync, fd, sc.ptr);
+			c.ptrpid = <-sync;
+			return nil;
+		"kbd" =>
+			if(c.kbdpid == -1)
+				return "already started";
+			sync := chan of (int, string);
+			spawn kbdproc(sync, c.dir + "/keyboard", sc.kbd);
+			(pid, err) := <-sync;
+			c.kbdpid = pid;
+			return err;
+		}
+	}
+
+	if(sys->write(c.ctlfd, data, len data) == -1)
+		return sys->sprint("%r");
+	if(req[0] == '!'){
+		buf := array[100] of byte;
+		n := sys->read(c.winfd, buf, len buf);
+		if(n <= 0)
+			return sys->sprint("read winname: %r");
+		name := string buf[0:n];
+		# XXX this is the dodgy bit...
+		i := display.namedimage(name);
+		if(i == nil)
+			return sys->sprint("cannot get image %#q: %r", name);
+		s := Screen.allocate(i, display.white, 0);
+		i = s.newwindow(i.r, Draw->Refnone, Draw->Nofill);
+		rc := chan of int;
+		sc.images <-= (nil, i, rc);
+		if(<-rc == -1)
+			return "image request already in progress";
+	}
+	return nil;
+}
+
+connect(ctxt: ref Context): ref Wmcontext
+{
+	# don't automatically make a new Draw->Context, 'cos the
+	# client should be aware that there's no wm so multiple
+	# windows won't work correctly.
+	# ... unless there's an exported wm available, of course!
+	if(ctxt == nil){
+		sys->fprint(sys->fildes(2), "wmlib: no draw context\n");
+		raise "fail:error";
+	}
+	if(ctxt.wm == nil){
+		wm := ref Wmcontext(
+			chan of int,
+			chan of ref Draw->Pointer,
+			chan of string,
+			nil,	# unused
+			chan of ref Image,
+			nil,
+			ctxt
+		);
+		return wm;
+	}
+	fd := sys->open("/chan/wmctl", Sys->ORDWR);
+	if(fd == nil){
+		sys->fprint(sys->fildes(2), "wmlib: cannot open /chan/wmctl: %r\n");
+		raise "fail:error";
+	}
+	buf := array[32] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0){
+		sys->fprint(sys->fildes(2), "wmlib: cannot get window token: %r\n");
+		raise "fail:error";
+	}
+	reply := chan of (string, ref Wmcontext);
+	ctxt.wm <-= (string buf[0:n], reply);
+	(err, wm) := <-reply;
+	if(err != nil){
+		sys->fprint(sys->fildes(2), "wmlib: cannot connect: %s\n", err);
+		raise "fail:" + err;
+	}
+	wm.connfd = fd;
+	wm.ctxt = ctxt;
+	return wm;
+}
+
+startinput(wm: ref Wmcontext, devs: list of string): string
+{
+	for(; devs != nil; devs = tl devs)
+		wmctl(wm, "start " + hd devs);
+	return nil;
+}
+
+reshape(wm: ref Wmcontext, name: string, r: Draw->Rect, i: ref Draw->Image, how: string): ref Draw->Image
+{
+	if(name == nil)
+		return nil;
+	(nil, ni, err) := wmctl(wm, sys->sprint("!reshape %s -1 %d %d %d %d %s", name, r.min.x, r.min.y, r.max.x, r.max.y, how));
+	if(err == nil)
+		return ni;
+	return i;
+}
+
+#
+# wmctl implements the default window behaviour
+#
+wmctl(wm: ref Wmcontext, request: string): (string, ref Image, string)
+{
+	(w, e) := qword(request, 0);
+	case w {
+	"exit" =>
+		kill(sys->pctl(0, nil), "killgrp");
+		exit;
+	* =>
+		if(wm.connfd != nil){
+			# standard form for requests: if request starts with '!',
+			# then the next word gives the tag of the window that the
+			# request applies to, and a new image is provided.
+			if(sys->fprint(wm.connfd, "%s", request) == -1){
+				sys->fprint(sys->fildes(2), "wmlib: wm request '%s' failed\n", request);
+				return (nil, nil, sys->sprint("%r"));
+			}
+			if(request[0] == '!'){
+				i := <-wm.images;
+				if(i == nil)
+					i = <-wm.images;
+				return (qword(request, e).t0, i, nil);
+			}
+			return (nil, nil, nil);
+		}
+		# requests we can handle ourselves, if we have to.
+		case w{
+		"start" =>
+			(w, e) = qword(request, e);
+			case w{
+			"ptr" or
+			"mouse" =>
+				if(!ptrstarted){
+					fd := sys->open("/dev/pointer", Sys->ORDWR);
+					if(fd != nil)
+						wptr = chan of Point;
+					else
+						fd = sys->open("/dev/pointer", Sys->OREAD);
+					if(fd == nil)
+						return (nil, nil, sys->sprint("cannot open /dev/pointer: %r"));
+					cfd := sys->open("/dev/cursor", Sys->OWRITE);
+					if(cfd != nil)
+						cswitch = chan of (string, int, chan of string);
+					spawn wptrproc(fd, cfd);
+					sync := chan of int;
+					spawn ptrproc(sync, fd, wm.ptr);
+					<-sync;
+					ptrstarted = 1;
+				}
+			"kbd" =>
+				if(!kbdstarted){
+					sync := chan of (int, string);
+					spawn kbdproc(sync, "/dev/keyboard", wm.kbd);
+					(nil, err) := <-sync;
+					if(err != nil)
+						return (nil, nil, err);
+					spawn sendreq(wm.ctl, "haskbdfocus 1");
+					kbdstarted = 1;
+				}
+			* =>
+				return (nil, nil, "unknown input source");
+			}
+			return (nil, nil, nil);
+		"ptr" =>
+			if(wptr == nil)
+				return (nil, nil, "cannot change mouse position");
+			p: Point;
+			(w, e) = qword(request, e);
+			p.x = int w;
+			(w, e) = qword(request, e);
+			p.y = int w;
+			wptr <-= p;
+			return (nil, nil, nil);
+		"cursor" =>
+			if(cswitch == nil)
+				return (nil, nil, "cannot switch cursor");
+			cswitch <-= (request, e, reply := chan of string);
+			return (nil, nil, <-reply);
+		* =>
+			return (nil, nil, "unknown wmctl request");
+		}
+	}
+}
+
+sendreq(c: chan of string, s: string)
+{
+	c <-= s;
+}
+
+ctlproc(sync: chan of int, fd: ref Sys->FD, ctl: chan of string)
+{
+	sync <-= sys->pctl(0, nil);
+	buf := array[4096] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0)
+		ctl <-= string buf[0:n];
+}
+
+kbdproc(sync: chan of (int, string), f: string, keys: chan of int)
+{
+	sys->pctl(Sys->NEWFD, nil);
+	fd := sys->open(f, Sys->OREAD);
+	if(fd == nil){
+		sync <-= (-1, sys->sprint("cannot open /dev/keyboard: %r"));
+		return;
+	}
+	sync <-= (sys->pctl(0, nil), nil);
+	buf := array[12] of byte;
+	while((n := sys->read(fd, buf, len buf)) > 0){
+		s := string buf[0:n];
+		for(j := 0; j < len s; j++)
+			keys <-= int s[j];
+	}
+}
+
+wptrproc(pfd, cfd: ref Sys->FD)
+{
+	if(wptr == nil && cswitch == nil)
+		return;
+	if(wptr == nil)
+		wptr = chan of Point;
+	if(cswitch == nil)
+		cswitch = chan of (string, int, chan of string);
+	for(;;)alt{
+	p := <-wptr =>
+		sys->fprint(pfd, "m%11d %11d", p.x, p.y);
+	(c, start, reply) := <-cswitch =>
+		buf: array of byte;
+		if(start == len c){
+			buf = array[0] of byte;
+		}else{
+			hot, size: Point;
+			(w, e) := qword(c, start);
+			hot.x = int w;
+			(w, e) = qword(c, e);
+			hot.y = int w;
+			(w, e) = qword(c, e);
+			size.x = int w;
+			(w, e) = qword(c, e);
+			size.y = int w;
+			((d0, d1), nil) := splitqword(c, e);
+			nb := size.x/8*size.y;
+			if(d1 - d0 != nb * 2){
+				reply <-= "inconsistent cursor image data";
+				break;
+			}
+			buf = array[4*4 + nb] of byte;
+			bplong(buf, 0*4, hot.x);
+			bplong(buf, 1*4, hot.y);
+			bplong(buf, 2*4, size.x);
+			bplong(buf, 3*4, size.y);
+			j := 4*4;
+			for(i := d0; i < d1; i += 2)
+				buf[j++] = byte ((hexc(c[i]) << 4) | hexc(c[i+1]));
+		}
+		if(sys->write(cfd, buf, len buf) != len buf)
+			reply <-= sys->sprint("%r");
+		else
+			reply <-= nil;
+	}
+}
+
+hexc(c: int): int
+{
+	if(c >= '0' && c <= '9')
+		return c - '0';
+	if(c >= 'a' && c <= 'f')
+		return c - 'a' + 10;
+	if(c >= 'A' && c <= 'F')
+		return c - 'A' + 10;
+	return 0;
+}
+
+bplong(d: array of byte, o: int, x: int)
+{
+	d[o] = byte x;
+	d[o+1] = byte (x >> 8);
+	d[o+2] = byte (x >> 16);
+	d[o+3] = byte (x >> 24);
+}
+
+ptrproc(sync: chan of int, fd: ref Sys->FD, ptr: chan of ref Draw->Pointer)
+{
+	sync <-= sys->pctl(0, nil);
+
+	b:= array[Ptrsize] of byte;
+	while(sys->read(fd, b, len b) > 0){
+		p := bytes2ptr(b);
+		if(p != nil)
+			ptr <-= p;
+	}
+}
+
+bytes2ptr(b: array of byte): ref Pointer
+{
+	if(len b < Ptrsize || int b[0] != 'm')
+		return nil;
+	x := int string b[1:13];
+	y := int string b[13:25];
+	but := int string b[25:37];
+	msec := int string b[37:49];
+	return ref Pointer (but, (x, y), msec);
+}
+
+snarfbuf: string;		# at least we get *something* when there's no wm.
+
+snarfget(): string
+{
+	fd := sys->open("/chan/snarf", sys->OREAD);
+	if(fd == nil)
+		return snarfbuf;
+
+	buf := array[8192] of byte;
+	nr := 0;
+	while ((n := sys->read(fd, buf[nr:], len buf - nr)) > 0) {
+		nr += n;
+		if (nr == len buf) {
+			nbuf := array[len buf * 2] of byte;
+			nbuf[0:] = buf;
+			buf = nbuf;
+		}
+	}
+	return string buf[0:nr];
+}
+
+snarfput(buf: string)
+{
+	fd := sys->open("/chan/snarf", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "%s", buf);
+	else
+		snarfbuf = buf;
+}
+
+# return (qslice, end).
+# the slice has a leading quote if the word is quoted; it does not include the terminating quote.
+splitqword(s: string, start: int): ((int, int), int)
+{
+	for(; start < len s; start++)
+		if(s[start] != ' ')
+			break;
+	if(start >= len s)
+		return ((start, start), start);
+	i := start;
+	end := -1;
+	if(s[i] == '\''){
+		gotq := 0;
+		for(i++; i < len s; i++){
+			if(s[i] == '\''){
+				if(i + 1 >= len s || s[i + 1] != '\''){
+					end = i+1;
+					break;
+				}
+				i++;
+				gotq = 1;
+			}
+		}
+		if(!gotq && i > start+1)
+			start++;
+		if(end == -1)
+			end = i;
+	} else {
+		for(; i < len s; i++)
+			if(s[i] == ' ')
+				break;
+		end = i;
+	}
+	return ((start, i), end);
+}
+
+# unquote a string slice as returned by sliceqword.
+qslice(s: string, r: (int, int)): string
+{
+	if(r.t0 == r.t1)
+		return nil;
+	if(s[r.t0] != '\'')
+		return s[r.t0:r.t1];
+	t := "";
+	for(i := r.t0 + 1; i < r.t1; i++){
+		t[len t] = s[i];
+		if(s[i] == '\'')
+			i++;
+	}
+	return t;
+}
+
+qword(s: string, start: int): (string, int)
+{
+	(w, next) := splitqword(s, start);
+	return (qslice(s, w), next);
+}
+
+s2r(s: string, e: int): (Rect, int)
+{
+	r: Rect;
+	w: string;
+	(w, e) = qword(s, e);
+	r.min.x = int w;
+	(w, e) = qword(s, e);
+	r.min.y = int w;
+	(w, e) = qword(s, e);
+	r.max.x = int w;
+	(w, e) = qword(s, e);
+	r.max.y = int w;
+	return (r, e);
+}
+
+kill(pid: int, note: string): int
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil)		# dodgy failover
+		fd = sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", note) < 0)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/appl/lib/wmsrv.b
@@ -1,0 +1,610 @@
+implement Wmsrv;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Image, Point, Rect, Screen, Pointer, Context, Wmcontext: import draw;
+include "wmsrv.m";
+
+zorder: ref Client;		# top of z-order list, linked by znext.
+
+ZR: con Rect((0, 0), (0, 0));
+Iqueue: adt {
+	h, t: list of int;
+	n: int;
+	put:			fn(q: self ref Iqueue, s: int);
+	get:			fn(q: self ref Iqueue): int;
+	peek:		fn(q: self ref Iqueue): int;
+	nonempty:	fn(q: self ref Iqueue): int;
+};
+Squeue: adt {
+	h, t: list of string;
+	n: int;
+	put:			fn(q: self ref Squeue, s: string);
+	get:			fn(q: self ref Squeue): string;
+	peek:		fn(q: self ref Squeue): string;
+	nonempty:	fn(q: self ref Squeue): int;
+};
+# Ptrqueue is the same as the other queues except it merges events
+# that have the same button state.
+Ptrqueue: adt {
+	last: ref Pointer;
+	h, t: list of ref Pointer;
+	put:			fn(q: self ref Ptrqueue, s: ref Pointer);
+	get:			fn(q: self ref Ptrqueue): ref Pointer;
+	peek:		fn(q: self ref Ptrqueue): ref Pointer;
+	nonempty:	fn(q: self ref Ptrqueue): int;
+	flush:		fn(q: self ref Ptrqueue);
+};
+
+init(): 	(chan of (string, chan of (string, ref Wmcontext)),
+		chan of (ref Client, chan of string),
+		chan of (ref Client, array of byte, Sys->Rwrite))
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+
+	sys->bind("#s", "/chan", Sys->MBEFORE);
+
+	ctlio := sys->file2chan("/chan", "wmctl");
+	if(ctlio == nil){
+		sys->werrstr(sys->sprint("can't create /chan/wmctl: %r"));
+		return (nil, nil, nil);
+	}
+
+	wmreq := chan of (string, chan of (string, ref Wmcontext));
+	join := chan of (ref Client, chan of string);
+	req := chan of (ref Client, array of byte, Sys->Rwrite);
+	spawn wm(ctlio, wmreq, join, req);
+	return (wmreq, join, req);
+}
+
+wm(ctlio: ref Sys->FileIO,
+			wmreq: chan of (string, chan of (string, ref Wmcontext)),
+			join: chan of (ref Client, chan of string),
+			req: chan of (ref Client, array of byte, Sys->Rwrite))
+{
+	clients: array of ref Client;
+
+	for(;;)alt{
+	(cmd, rc) := <-wmreq =>
+		token := int cmd;
+		for(i := 0; i < len clients; i++)
+			if(clients[i] != nil && clients[i].token == token)
+				break;
+
+		if(i == len clients){
+			spawn senderror(rc, "not found");
+			break;
+		}
+		c := clients[i];
+		if(c.stop != nil){
+			spawn senderror(rc, "already started");
+			break;
+		}
+		ok := chan of string;
+		join <-= (c, ok);
+		if((e := <-ok) != nil){
+			spawn senderror(rc, e);
+			break;
+		}
+		c.stop = chan of int;
+		spawn childminder(c, rc);
+
+	(nil, nbytes, fid, rc) := <-ctlio.read =>
+		if(rc == nil)
+			break;
+		c := findfid(clients, fid);
+		if(c == nil){
+			c = ref Client(
+				chan of int,
+				chan of ref Draw->Pointer,
+				chan of string,
+				nil,
+				0,
+				nil,
+				nil,
+				nil,
+
+				chan of (ref Point, ref Image, chan of int),
+				-1,
+				fid,
+				fid,			# token; XXX could be random integer + fid
+				newwmcontext()
+			);
+			clients = addclient(clients, c);
+		}
+		alt{
+		rc <-= (sys->aprint("%d", c.token), nil) => ;
+		* => ;
+		}
+	(nil, data, fid, wc) := <-ctlio.write =>
+		c := findfid(clients, fid);
+		if(wc != nil){
+			if(c == nil){
+				alt{
+				wc <-= (0, "must read first") => ;
+				* => ;
+				}
+				break;
+			}
+			req <-= (c, data, wc);
+		}else if(c != nil){
+			req <-= (c, nil, nil);
+			delclient(clients, c);
+		}
+	}
+}
+
+# buffer all events between a window manager and
+# a client, so that one recalcitrant child can't
+# clog the whole system.
+childminder(c: ref Client, rc: chan of (string, ref Wmcontext))
+{
+	wmctxt := c.wmctxt;
+
+	dummykbd := chan of int;
+	dummyptr := chan of ref Pointer;
+	dummyimg := chan of ref Image;
+	dummyctl := chan of string;
+
+	kbdq := ref Iqueue;
+	ptrq := ref Ptrqueue;
+	ctlq := ref Squeue;
+
+	Imgnone, Imgsend, Imgsendnil1, Imgsendnil2, Imgorigin: con iota;
+	img, sendimg: ref Image;
+	imgorigin: Point;
+	imgstate := Imgnone;
+
+	# send reply to client, but make sure we don't block.
+Reply:
+	for(;;) alt{
+	rc <-= (nil, ref *wmctxt) =>
+		break Reply;
+	<-c.stop =>
+		exit;
+	key := <-c.kbd =>
+		kbdq.put(key);
+	ptr := <-c.ptr =>
+		ptrq.put(ptr);
+	ctl := <-c.ctl =>
+		ctlq.put(ctl);
+	}
+
+	for(;;){
+		outkbd := dummykbd;
+		key := -1;
+		if(kbdq.nonempty()){
+			key = kbdq.peek();
+			outkbd = wmctxt.kbd;
+		}
+
+		outptr := dummyptr;
+		ptr: ref Pointer;
+		if(ptrq.nonempty()){
+			ptr = ptrq.peek();
+			outptr = wmctxt.ptr;
+		}
+
+		outctl := dummyctl;
+		ctl: string;
+		if(ctlq.nonempty()){
+			ctl = ctlq.peek();
+			outctl = wmctxt.ctl;
+		}
+
+		outimg := dummyimg;
+		case imgstate{
+		Imgsend =>
+			outimg = wmctxt.images;
+			sendimg = img;
+		Imgsendnil1 or
+		Imgsendnil2 or
+		Imgorigin =>
+			outimg = wmctxt.images;
+			sendimg = nil;
+		}
+
+		alt{
+		outkbd <-= key =>
+			kbdq.get();
+		outptr <-= ptr =>
+			ptrq.get();
+		outctl <-= ctl =>
+			ctlq.get();
+		outimg <-= sendimg =>
+			case imgstate{
+			Imgsend =>
+				imgstate = Imgnone;
+				img = sendimg = nil;
+			Imgsendnil1 =>
+				imgstate = Imgsendnil2;
+			Imgsendnil2 =>
+				imgstate = Imgnone;
+			Imgorigin =>
+				if(img.origin(imgorigin, imgorigin) == -1){
+					# XXX what can we do about this? there's no way at the moment
+					# of getting the information about the origin failure back to the wm,
+					# so we end up with an inconsistent window position.
+					# if the window manager blocks while we got the sync from
+					# the client, then a client could block the whole window manager
+					# which is what we're trying to avoid.
+					# but there's no other time we could set the origin of the window,
+					# and not risk mucking up the window contents.
+					# the short answer is that running out of image space is Bad News.
+				}
+				imgstate = Imgsend;
+			}
+
+		# XXX could mark the application as unresponding if any of these queues
+		# start growing too much.
+		ch := <-c.kbd =>
+			kbdq.put(ch);
+		p := <-c.ptr =>
+			if(p == nil)
+				ptrq.flush();
+			else
+				ptrq.put(p);
+		e := <-c.ctl =>
+			ctlq.put(e);
+		(o, i, reply) := <-c.images =>
+			# can't queue multiple image requests.
+			if(imgstate != Imgnone)
+				reply <-= -1;
+			else {
+				# if the origin is being set, then we first send a nil image
+				# to indicate that this is happening, and then the
+				# image itself (reorigined).
+				# if a nil image is being set, then we
+				# send nil twice.
+				if(o != nil){
+					imgorigin = *o;
+					imgstate = Imgorigin;
+					img = i;
+				}else if(i != nil){
+					img = i;
+					imgstate = Imgsend;
+				}else
+					imgstate = Imgsendnil1;
+				reply <-= 0;
+			}
+		<-c.stop =>
+			# XXX do we need to unblock channels, kill, etc.?
+			# we should perhaps drain the ctl output channel here
+			# if possible, exiting if it times out.
+			exit;
+		}
+	}
+}
+
+findfid(clients: array of ref Client, fid: int): ref Client
+{
+	for(i := 0; i < len clients; i++)
+		if(clients[i] != nil && clients[i].fid == fid)
+			return clients[i];
+	return nil;
+}
+
+addclient(clients: array of ref Client, c: ref Client): array of ref Client
+{
+	for(i := 0; i < len clients; i++)
+		if(clients[i] == nil){
+			clients[i] = c;
+			c.id = i;
+			return clients;
+		}
+	nc := array[len clients + 4] of ref Client;
+	nc[0:] = clients;
+	nc[len clients] = c;
+	c.id = len clients;
+	return nc;
+}
+
+delclient(clients: array of ref Client, c: ref Client)
+{
+	clients[c.id] = nil;
+}
+
+senderror(rc: chan of (string, ref Wmcontext), e: string)
+{
+	rc <-= (e, nil);
+}
+
+Client.window(c: self ref Client, tag: string): ref Window
+{
+	for (w := c.wins; w != nil; w = tl w)
+		if((hd w).tag == tag)
+			return hd w;
+	return nil;
+}
+
+Client.image(c: self ref Client, tag: string): ref Draw->Image
+{
+	w := c.window(tag);
+	if(w != nil)
+		return w.img;
+	return nil;
+}
+
+Client.setimage(c: self ref Client, tag: string, img: ref Draw->Image): int
+{
+	# if img is nil, remove window from list.
+	if(img == nil){
+		# usual case:
+		if(c.wins != nil && (hd c.wins).tag == tag){
+			c.wins = tl c.wins;
+			return -1;
+		}
+		nw: list of ref Window;
+		for (w := c.wins; w != nil; w = tl w)
+			if((hd w).tag != tag)
+				nw = hd w :: nw;
+		c.wins = nil;
+		for(; nw != nil; nw = tl nw)
+			c.wins = hd nw :: c.wins;
+		return -1;
+	}
+	for(w := c.wins; w != nil; w = tl w)
+		if((hd w).tag == tag)
+			break;
+	win: ref Window;
+	if(w != nil)
+		win = hd w;
+	else{
+		win = ref Window(tag, ZR, nil);
+		c.wins = win :: c.wins;
+	}
+	win.img = img;
+	win.r = img.r;			# save so clients can set logical origin
+	rc := chan of int;
+	c.images <-= (nil, img, rc);
+	return <-rc;
+}
+
+# tell a client about a window that's moved to screen coord o.
+Client.setorigin(c: self ref Client, tag: string, o: Draw->Point): int
+{
+	w := c.window(tag);
+	if(w == nil)
+		return -1;
+	img := w.img;
+	if(img == nil)
+		return -1;
+	rc := chan of int;
+	c.images <-= (ref o, w.img, rc);
+	if(<-rc != -1){
+		w.r = (o, o.add(img.r.size()));
+		return 0;
+	}
+	return -1;
+}
+
+clientimages(c: ref Client): array of ref Image
+{
+	a := array[len c.wins] of ref Draw->Image;
+	i := 0;
+	for(w := c.wins; w != nil; w = tl w)
+		if((hd w).img != nil)
+			a[i++] = (hd w).img;
+	return a[0:i];
+}
+
+Client.top(c: self ref Client)
+{
+	imgs := clientimages(c);
+	if(len imgs > 0)
+		imgs[0].screen.top(imgs);
+
+	if(zorder == c)
+		return;
+
+	prev: ref Client;
+	for(z := zorder; z != nil; (prev, z) = (z, z.znext))
+		if(z == c)
+			break;
+	if(prev != nil)
+		prev.znext = c.znext;
+	c.znext = zorder;
+	zorder = c;
+}
+
+Client.bottom(c: self ref Client)
+{
+	if(c.znext == nil)
+		return;
+	imgs := clientimages(c);
+	if(len imgs > 0)
+		imgs[0].screen.bottom(imgs);
+	prev: ref Client;
+	for(z := zorder; z != nil; (prev, z) = (z, z.znext))
+		if(z == c)
+			break;
+	if(prev != nil)
+		prev.znext = c.znext;
+	else
+		zorder = c.znext;
+	z = c.znext;
+	c.znext = nil;
+	for(; z != nil; (prev, z) = (z, z.znext))
+		;
+	if(prev != nil)
+		prev.znext = c;
+	else
+		zorder = c;
+}
+
+Client.hide(nil: self ref Client)
+{
+}
+
+Client.unhide(nil: self ref Client)
+{
+}
+
+Client.remove(c: self ref Client)
+{
+	prev: ref Client;
+	for(z := zorder; z != nil; (prev, z) = (z, z.znext))
+		if(z == c)
+			break;
+	if(z == nil)
+		return;
+	if(prev != nil)
+		prev.znext = z.znext;
+	else if(z != nil)
+		zorder = zorder.znext;
+}
+
+find(p: Draw->Point): ref Client
+{
+	for(z := zorder; z != nil; z = z.znext)
+		if(z.contains(p))
+			return z;
+	return nil;
+}
+
+top(): ref Client
+{
+	return zorder;
+}
+
+Client.contains(c: self ref Client, p: Point): int
+{
+	for(w := c.wins; w != nil; w = tl w)
+		if((hd w).r.contains(p))
+			return 1;
+	return 0;
+}
+
+r2s(r: Rect): string
+{
+	return string r.min.x + " " + string r.min.y + " " +
+			string r.max.x + " " + string r.max.y;
+}
+
+newwmcontext(): ref Wmcontext
+{
+	return ref Wmcontext(
+		chan of int,
+		chan of ref Pointer,
+		chan of string,
+		nil,
+		chan of ref Image,
+		nil,
+		nil
+	);
+}
+
+Iqueue.put(q: self ref Iqueue, s: int)
+{
+	q.t = s :: q.t;
+}
+Iqueue.get(q: self ref Iqueue): int
+{
+	s := -1;
+	if(q.h == nil){
+		for(t := q.t; t != nil; t = tl t)
+			q.h = hd t :: q.h;
+		q.t = nil;
+	}
+	if(q.h != nil){
+		s = hd q.h;
+		q.h = tl q.h;
+	}
+	return s;
+}
+Iqueue.peek(q: self ref Iqueue): int
+{
+	s := -1;
+	if (q.h == nil && q.t == nil)
+		return s;
+	s = q.get();
+	q.h = s :: q.h;
+	return s;
+}
+Iqueue.nonempty(q: self ref Iqueue): int
+{
+	return q.h != nil || q.t != nil;
+}
+
+
+Squeue.put(q: self ref Squeue, s: string)
+{
+	q.t = s :: q.t;
+}
+Squeue.get(q: self ref Squeue): string
+{
+	s: string;
+	if(q.h == nil){
+		for(t := q.t; t != nil; t = tl t)
+			q.h = hd t :: q.h;
+		q.t = nil;
+	}
+	if(q.h != nil){
+		s = hd q.h;
+		q.h = tl q.h;
+	}
+	return s;
+}
+Squeue.peek(q: self ref Squeue): string
+{
+	s: string;
+	if (q.h == nil && q.t == nil)
+		return s;
+	s = q.get();
+	q.h = s :: q.h;
+	return s;
+}
+Squeue.nonempty(q: self ref Squeue): int
+{
+	return q.h != nil || q.t != nil;
+}
+
+Ptrqueue.put(q: self ref Ptrqueue, s: ref Pointer)
+{
+	if(q.last != nil && s.buttons == q.last.buttons)
+		*q.last = *s;
+	else{
+		q.t = s :: q.t;
+		q.last = s;
+	}
+}
+Ptrqueue.get(q: self ref Ptrqueue): ref Pointer
+{
+	s: ref Pointer;
+	h := q.h;
+	if(h == nil){
+		for(t := q.t; t != nil; t = tl t)
+			h = hd t :: h;
+		q.t = nil;
+	}
+	if(h != nil){
+		s = hd h;
+		h = tl h;
+		if(h == nil)
+			q.last = nil;
+	}
+	q.h = h;
+	return s;
+}
+Ptrqueue.peek(q: self ref Ptrqueue): ref Pointer
+{
+	s: ref Pointer;
+	if (q.h == nil && q.t == nil)
+		return s;
+	t := q.last;
+	s = q.get();
+	q.h = s :: q.h;
+	q.last = t;
+	return s;
+}
+Ptrqueue.nonempty(q: self ref Ptrqueue): int
+{
+	return q.h != nil || q.t != nil;
+}
+Ptrqueue.flush(q: self ref Ptrqueue)
+{
+	q.h = q.t = nil;
+}
--- /dev/null
+++ b/appl/lib/workdir.b
@@ -1,0 +1,14 @@
+implement Workdir;
+
+include "sys.m";
+
+include "workdir.m";
+
+init(): string
+{
+	sys := load Sys Sys->PATH;
+	fd := sys->open(".", Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	return sys->fd2path(fd);
+}
--- /dev/null
+++ b/appl/lib/writegif.b
@@ -1,0 +1,362 @@
+implement WImagefile;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Chans, Display, Image, Rect: import draw;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "imagefile.m";
+
+Nhash: con 4001;
+
+Entry: adt
+{
+	index: int;
+	prefix: int;
+	exten: int;
+	next:	cyclic ref Entry;
+};
+
+IO: adt
+{
+	fd:	ref Iobuf;
+	buf:	array of byte;
+	i:	int;
+	nbits:	int;	 # bits in right side of shift register
+	sreg:	int;	# shift register
+};
+
+tbl: array of ref Entry;
+
+colormap: array of array of byte;
+log2 := array[] of {1 => 0, 2 => 1, 4 => 2, 8 => 3, * => -1};
+
+init(iomod: Bufio)
+{
+	if(sys == nil){
+		sys = load Sys Sys->PATH;
+		draw = load Draw Draw->PATH;
+	}
+	bufio = iomod;
+}
+
+writeimage(fd: ref Iobuf, image: ref Image): string
+{
+	case image.chans.desc {
+	(Draw->GREY1).desc or (Draw->GREY2).desc or
+	(Draw->GREY4).desc or (Draw->GREY8).desc or
+	(Draw->CMAP8).desc =>
+		if(image.depth > 8 || (image.depth&(image.depth-1)) != 0)
+			return "inconsistent depth";
+	* =>
+		return "unsupported channel type";
+	}
+
+	inittbl();
+
+	writeheader(fd, image);
+	writedescriptor(fd, image);
+
+	err := writedata(fd, image);
+	if(err != nil)
+		return err;
+
+	writetrailer(fd);
+	fd.flush();
+	return err;
+}
+
+inittbl()
+{
+	tbl = array[4096] of ref Entry;
+	for(i:=0; i<len tbl; i++)
+		tbl[i] = ref Entry(i, -1, i, nil);
+}
+
+# Write header, logical screen descriptor, and color map
+writeheader(fd: ref Iobuf, image: ref Image): string
+{
+	# Header
+	fd.puts("GIF89a");
+
+	# Logical Screen Descriptor
+	put2(fd, image.r.dx());
+	put2(fd, image.r.dy());
+	# color table present, 4 bits per color (for RGBV best case), size of color map
+	fd.putb(byte ((1<<7)|(3<<4)|(image.depth-1)));
+	fd.putb(byte 0);	# white background (doesn't matter anyway)
+	fd.putb(byte 0);	# pixel aspect ratio - unused
+
+	# Global Color Table
+	getcolormap(image);
+	ldepth := log2[image.depth];
+	if(image.chans.eq(Draw->GREY8))
+		ldepth = 4;
+	fd.write(colormap[ldepth], len colormap[ldepth]);
+	return nil;
+}
+
+# Write image descriptor
+writedescriptor(fd: ref Iobuf, image: ref Image)
+{
+	# Image Separator
+	fd.putb(byte 16r2C);
+
+	# Left, top, width, height
+	put2(fd, 0);
+	put2(fd, 0);
+	put2(fd, image.r.dx());
+	put2(fd, image.r.dy());
+	# no special processing
+	fd.putb(byte 0);
+}
+
+# Write data
+writedata(fd: ref Iobuf, image: ref Image): string
+{
+	# LZW Minimum code size
+	if(image.depth == 1)
+		fd.putb(byte 2);
+
+	else
+		fd.putb(byte image.depth);
+
+	# Encode and emit the data
+	err := encode(fd, image);
+	if(err != nil)
+		return err;
+
+	# Block Terminator
+	fd.putb(byte 0);
+	return nil;
+}
+
+# Write data
+writetrailer(fd: ref Iobuf)
+{
+	fd.putb(byte 16r3B);
+}
+
+# Write little-endian 16-bit integer
+put2(fd: ref Iobuf, i: int)
+{
+	fd.putb(byte i);
+	fd.putb(byte (i>>8));
+}
+
+# Get color map for all ldepths, in format suitable for writing out
+getcolormap(image: ref Draw->Image)
+{
+	if(colormap != nil)
+		return;
+	colormap = array[5] of array of byte;
+	display := image.display;
+	colormap[4] = array[3*256] of byte;
+	colormap[3] = array[3*256] of byte;
+	colormap[2] = array[3*16] of byte;
+	colormap[1] = array[3*4] of byte;
+	colormap[0] = array[3*2] of byte;
+	c := colormap[4];
+	for(i:=0; i<256; i++){
+		c[3*i+0] = byte i;
+		c[3*i+1] = byte i;
+		c[3*i+2] = byte i;
+	}
+	c = colormap[3];
+	for(i=0; i<256; i++){
+		(r, g, b) := display.cmap2rgb(i);
+		c[3*i+0] = byte r;
+		c[3*i+1] = byte g;
+		c[3*i+2] = byte b;
+	}
+	c = colormap[2];
+	for(i=0; i<16; i++){
+		col := (i<<4)|i;
+		(r, g, b) := display.cmap2rgb(col);
+		c[3*i+0] = byte r;
+		c[3*i+1] = byte g;
+		c[3*i+2] = byte b;
+	}
+	c = colormap[1];
+	for(i=0; i<4; i++){
+		col := (i<<6)|(i<<4)|(i<<2)|i;
+		(r, g, b) := display.cmap2rgb(col);
+		c[3*i+0] = byte r;
+		c[3*i+1] = byte g;
+		c[3*i+2] = byte b;
+	}
+	c = colormap[0];
+	for(i=0; i<2; i++){
+		if(i == 0)
+			col := 0;
+		else
+			col = 16rFF;
+		(r, g, b) := display.cmap2rgb(col);
+		c[3*i+0] = byte r;
+		c[3*i+1] = byte g;
+		c[3*i+2] = byte b;
+	}
+}
+
+# Put n bits of c into output at io.buf[i];
+output(io: ref IO, c, n: int)
+{
+	if(c < 0){
+		if(io.nbits != 0)
+			io.buf[io.i++] = byte io.sreg;
+		io.fd.putb(byte io.i);
+		io.fd.write(io.buf, io.i);
+		io.nbits = 0;
+		return;
+	}
+
+	if(io.nbits+n >= 31){
+		sys->print("panic: WriteGIF sr overflow\n");
+		exit;
+	}
+	io.sreg |= c<<io.nbits;
+	io.nbits += n;
+
+	while(io.nbits >= 8){
+		io.buf[io.i++] = byte io.sreg;
+		io.sreg >>= 8;
+		io.nbits -= 8;
+	}
+
+	if(io.i >= 255){
+		io.fd.putb(byte 255);
+		io.fd.write(io.buf, 255);
+		io.buf[0:] = io.buf[255:io.i];
+		io.i -= 255;
+	}
+}
+
+# LZW encoder
+encode(fd: ref Iobuf, image: ref Image): string
+{
+	c, h, csize, prefix: int;
+	e, oe: ref Entry;
+
+	first := 1;
+	ld := log2[image.depth];
+	# ldepth 0 must generate codesize 2 with values 0 and 1 (see the spec.)
+	ld0 := ld;
+	if(ld0 == 0)
+		ld0 = 1;
+	codesize := (1<<ld0);
+	CTM := 1<<codesize;
+	EOD := CTM+1;
+
+	io := ref IO (fd, array[300] of byte, 0, 0, 0);
+	sreg := 0;
+	nbits := 0;
+	bitsperpixel := 1<<ld;
+	pm := (1<<bitsperpixel)-1;
+
+	# Read image data into memory
+	# potentially one extra byte on each end of each scan line
+	data := array[image.r.dy()*(2+(image.r.dx()>>(3-log2[image.depth])))] of byte;
+	ndata := image.readpixels(image.r, data);
+	if(ndata < 0)
+		return sys->sprint("WriteGIF: readpixels: %r");
+	datai := 0;
+	x := image.r.min.x;
+
+Init:
+	for(;;){
+		csize = codesize+1;
+		nentry := EOD+1;
+		maxentry := (1<<csize);
+		hash := array[Nhash] of ref Entry;
+		for(i := 0; i<nentry; i++){
+			e = tbl[i];
+			h = (e.prefix<<24) | (e.exten<<8);
+			h %= Nhash;
+			if(h < 0)
+				h += Nhash;
+			e.next = hash[h];
+			hash[h] = e;
+		}
+		prefix = -1;
+		if(first)
+			output(io, CTM, csize);
+		first = 0;
+
+		# Scan over pixels.  Because of partially filled bytes on ends of scan lines,
+		# which must be ignored in the data stream passed to GIF, this is more
+		# complex than we'd like
+	Next:
+		for(;;){
+			if(ld != 3){
+				# beginning of scan line is difficult; prime the shift register
+				if(x == image.r.min.x){
+					if(datai == ndata)
+						break;
+					sreg = int data[datai++];
+					nbits = 8-((x&(7>>ld))<<ld);
+				}
+				x++;
+				if(x == image.r.max.x)
+					x = image.r.min.x;
+			}
+			if(nbits == 0){
+				if(datai == ndata)
+					break;
+				sreg = int data[datai++];
+				nbits = 8;
+			}
+			nbits -= bitsperpixel;
+			c = sreg>>nbits & pm;
+			h = prefix<<24 | c<<8;
+			h %= Nhash;
+			if(h < 0)
+				h += Nhash;
+			oe = nil;
+			for(e = hash[h]; e!=nil; e=e.next){
+				if(e.prefix == prefix && e.exten == c){
+					if(oe != nil){
+						oe.next = e.next;
+						e.next = hash[h];
+						hash[h] = e;
+					}
+					prefix = e.index;
+					continue Next;
+				}
+				oe = e;
+			}
+
+			output(io, prefix, csize);
+			early:=0; # peculiar tiff feature here for reference
+			if(nentry == maxentry-early){
+				if(csize == 12){
+					nbits += codesize;	# unget pixel
+					x--;
+					output(io, CTM, csize);
+					continue Init;
+				}
+				csize++;
+				maxentry = (1<<csize);
+			}
+
+			e = tbl[nentry];
+			e.prefix = prefix;
+			e.exten = c;
+			e.next = hash[h];
+			hash[h] = e;
+
+			prefix = c;
+			nentry++;
+		}
+		break Init;
+	}
+	output(io, prefix, csize);
+	output(io, EOD, csize);
+	output(io, -1, csize);
+	return nil;
+}
--- /dev/null
+++ b/appl/lib/xml.b
@@ -1,0 +1,717 @@
+implement Xml;
+
+#
+# Portions copyright © 2002 Vita Nuova Holdings Limited
+#
+#
+# Derived from saxparser.b Copyright © 2001-2002 by John Powers or his employer
+#
+
+# TO DO:
+# - provide a way of getting attributes out of <?...?> (process) requests,
+# so that we can process stylesheet requests given in that way.
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "hash.m";
+	hash: Hash;
+	HashTable: import hash;
+include "xml.m";
+
+Parcel: adt {
+	pick {
+	Start or
+	Empty =>
+		name: string;
+		attrs: Attributes;
+	End =>
+		name: string;
+	Text =>
+		ch: string;
+		ws1, ws2: int;
+	Process =>
+		target: string;
+		data: string;
+	Error =>
+		loc:	Locator;
+		msg:	string;
+	Doctype =>
+		name:	string;
+		public:	int;
+		params:	list of string;
+	Stylesheet =>
+		attrs: Attributes;
+	EOF =>
+	}
+};
+
+entinit := array[] of {
+	("AElig", "Æ"),
+	("OElig", "Œ"),
+	("aelig", "æ"),
+	("amp", "&"),
+	("apos", "\'"),
+	("copy", "©"),
+	("gt", ">"),
+	("ldquo", "``"),
+	("lt", "<"),
+	("mdash", "-"),		# XXX ??
+	("oelig", "œ"),
+	("quot", "\""),
+	("rdquo", "''"),
+	("rsquo", "'"),
+	("trade", "™"),
+	("nbsp", "\u00a0"),
+};
+entdict: ref HashTable;
+
+init(): string
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil)
+		return sys->sprint("cannot load %s: %r", Bufio->PATH);
+	str = load String String->PATH;
+	if (str == nil)
+		return sys->sprint("cannot load %s: %r", String->PATH);
+	hash = load Hash Hash->PATH;
+	if (hash == nil)
+		return sys->sprint("cannot load %s: %r", Hash->PATH);
+	entdict = hash->new(23);
+	for (i := 0; i < len entinit; i += 1) {
+		(key, value) := entinit[i];
+		entdict.insert(key, (0, 0.0, value));
+	}
+	return nil;
+}
+
+blankparser: Parser;
+
+open(srcfile: string, warning: chan of (Locator, string), preelem: string): (ref Parser, string)
+{
+	fd := bufio->open(srcfile, Bufio->OREAD);
+	if(fd == nil)
+		return (nil, sys->sprint("cannot open %s: %r", srcfile));
+	return fopen(fd, srcfile, warning, preelem);
+}
+
+fopen(fd: ref Bufio->Iobuf, name: string, warning: chan of (Locator, string), preelem: string): (ref Parser, string)
+{
+	x := ref blankparser;
+	x.in = fd;
+	# ignore utf16 initialisation character (yuck)
+	c := x.in.getc();
+	if (c != 16rfffe && c != 16rfeff)
+		x.in.ungetc();
+	x.estack = nil;
+	x.loc = Locator(1, name, "");
+	x.warning = warning;
+	x.preelem = preelem;
+	return (x, "");
+}
+
+Parser.next(x: self ref Parser): ref Item
+{
+	curroffset := x.fileoffset;
+	currloc := x.loc;
+	# read up until end of current item
+	while (x.actdepth > x.readdepth) {
+		pick p := getparcel(x) {
+		Start =>
+			x.actdepth++;
+		End =>
+			x.actdepth--;
+		EOF =>
+			x.actdepth = 0;			# premature EOF closes all tags
+		Error =>
+			return ref Item.Error(curroffset, x.loc, x.errormsg);
+		}
+	}
+	if (x.actdepth < x.readdepth) {
+		x.fileoffset = int x.in.offset();
+		return nil;
+	}
+	gp := getparcel(x);
+	item: ref Item;
+	pick p := gp {
+	Start =>
+		x.actdepth++;
+		item = ref Item.Tag(curroffset, p.name, p.attrs);
+	End =>
+		x.actdepth--;
+		item = nil;
+	EOF =>
+		x.actdepth = 0;
+		item = nil;
+	Error =>
+		x.actdepth = 0;			# XXX is this the right thing to do?
+		item = ref Item.Error(curroffset, currloc, x.errormsg);
+	Text =>
+		item = ref Item.Text(curroffset, p.ch, p.ws1, p.ws2);
+	Process =>
+		item = ref Item.Process(curroffset, p.target, p.data);
+	Empty =>
+		item = ref Item.Tag(curroffset, p.name, p.attrs);
+	Doctype =>
+		item = ref Item.Doctype(curroffset, p.name, p.public, p.params);
+	Stylesheet =>
+		item = ref Item.Stylesheet(curroffset, p.attrs);
+	}
+	x.fileoffset = int x.in.offset();
+	return item;
+}
+
+Parser.atmark(x: self ref Parser, m: ref Mark): int
+{
+	return  int x.in.offset() == m.offset;
+}
+
+Parser.down(x: self ref Parser)
+{
+	x.readdepth++;
+}
+
+Parser.up(x: self ref Parser)
+{
+	x.readdepth--;
+}
+
+# mark is only defined after a next(), not after up() or down().
+# this means that we don't have to record lots of state when going up or down levels.
+Parser.mark(x: self ref Parser): ref Mark
+{
+	return ref Mark(x.estack, x.loc.line, int x.in.offset(), x.readdepth);
+}
+
+Parser.goto(x: self ref Parser, m: ref Mark)
+{
+	x.in.seek(big m.offset, Sys->SEEKSTART);
+	x.fileoffset = m.offset;
+	x.eof = 0;
+	x.estack = m.estack;
+	x.loc.line = m.line;
+	x.readdepth = m.readdepth;
+	x.actdepth = len x.estack;
+}
+
+Mark.str(m: self ref Mark): string
+{
+	# assume that neither the filename nor any of the tags contain spaces.
+	# format:
+	# offset readdepth linenum [tag...]
+	# XXX would be nice if the produced string did not contain
+	# any spaces so it could be treated as a word in other contexts.
+	s := sys->sprint("%d %d %d", m.offset, m.readdepth, m.line);
+	for (t := m.estack; t != nil; t = tl t)
+		s += " " + hd t;
+	return s;
+}
+
+Parser.str2mark(p: self ref Parser, s: string): ref Mark
+{
+	(n, toks) := sys->tokenize(s, " ");
+	if (n < 3)
+		return nil;
+	m := ref Mark(nil, p.loc.line, 0, 0);
+	(m.offset, toks) = (int hd toks, tl toks);
+	(m.readdepth, toks) = (int hd toks, tl toks);
+	(m.line, toks) = (int hd toks, tl toks);
+	m.estack = toks;
+	return m;
+}
+
+getparcel(x: ref Parser): ref Parcel
+{
+	{
+		p: ref Parcel;
+		while (!x.eof && p == nil) {
+			c := getc(x);
+			if (c == '<')
+				p = element(x);
+			else {
+				ungetc(x);
+				p = characters(x);
+			}
+		}
+		if (p == nil)
+			p = ref Parcel.EOF;
+		return p;
+	}exception e{
+	"sax:*" =>
+			return ref Parcel.Error(x.loc, x.errormsg);
+	}
+}
+
+parcelstr(gi: ref Parcel): string
+{
+	if (gi == nil)
+		return "nil";
+	pick i := gi {
+	Start =>
+		return sys->sprint("Start: %s", i.name);
+	Empty =>
+		return sys->sprint("Empty: %s", i.name);
+	End =>
+		return "End";
+	Text =>
+		return "Text";
+	Doctype =>
+		return sys->sprint("Doctype: %s", i.name);
+	Stylesheet =>
+		return "Stylesheet";
+	Error =>
+		return "Error: " + i.msg;
+	EOF =>
+		return "EOF";
+	* =>
+		return "Unknown";
+	}
+}
+
+element(x: ref Parser): ref Parcel
+{
+	# <tag ...>
+	elemname := xmlname(x);
+	c: int;
+	if (elemname != "") {
+		attrs := buildattrs(x);
+		skipwhite(x);
+		c = getc(x);
+		isend := 0;
+		if (c == '/')
+			isend = 1;
+		else
+			ungetc(x);
+		expect(x, '>');
+
+		if (isend)
+			return ref Parcel.Empty(elemname, attrs);
+		else {
+			startelement(x, elemname);
+			return ref Parcel.Start(elemname, attrs);
+		}
+	# </tag>
+	} else if ((c = getc(x)) == '/') {
+		elemname = xmlname(x);
+		if (elemname != "") {
+			expect(x, '>');
+			endelement(x, elemname);
+			return ref Parcel.End(elemname);
+		}
+		else
+			error(x, sys->sprint("illegal beginning of tag: '%c'", c));
+	# <?tag ... ?>
+	} else if (c == '?') {
+		elemname = xmlname(x);
+		if (elemname != "") {
+			# this special case could be generalised if there were many
+			# processing instructions that took attributes like this.
+			if (elemname == "xml-stylesheet") {
+				attrs := buildattrs(x);
+				balancedstring(x, "?>");
+				return ref Parcel.Stylesheet(attrs);
+			} else {
+				data := balancedstring(x, "?>");
+				return ref Parcel.Process(elemname, data);
+			}
+		}
+	} else if (c == '!') {
+		c = getc(x);
+		case c {
+		'-' =>
+			# <!-- comment -->
+			if(getc(x) == '-'){
+				balancedstring(x, "-->");
+				return nil;
+			}
+		'[' =>
+			# <![CDATA[...]]
+			s := xmlname(x);
+			if(s == "CDATA" && getc(x) == '['){
+				data := balancedstring(x, "]]>");
+				return ref Parcel.Text(data, 0, 0);
+			}
+		* =>
+			# <!declaration
+			ungetc(x);
+			s := xmlname(x);
+			case s {
+			"DOCTYPE" =>
+				# <!DOCTYPE name (SYSTEM "filename" | PUBLIC "pubid" "uri"?)? ("[" decls "]")?>
+				skipwhite(x);
+				name := xmlname(x);
+				if(name == nil)
+					break;
+				id := "";
+				uri := "";
+				public := 0;
+				skipwhite(x);
+				case sort := xmlname(x) {
+				"SYSTEM" =>
+					id = xmlstring(x, 1);
+				"PUBLIC" =>
+					public = 1;
+					id = xmlstring(x, 1);
+					skipwhite(x);
+					c = getc(x);
+					ungetc(x);
+					if(c == '"' || c == '\'')
+						uri = xmlstring(x, 1);
+				* =>
+					error(x, sys->sprint("unknown DOCTYPE: %s", sort));
+					return nil;
+				}
+				skipwhite(x);
+				if(getc(x) == '['){
+					error(x, "cannot handle DOCTYPE with declarations");
+					return nil;
+				}
+				ungetc(x);
+				skipwhite(x);
+				if(getc(x) == '>')
+					return ref Parcel.Doctype(name, public, id :: uri :: nil);
+			"ELEMENT" or "ATTRLIST" or "NOTATION" or "ENTITY" =>
+				# don't interpret internal DTDs
+				# <!ENTITY name ("value" | SYSTEM "filename")>
+				s = gets(x, '>');
+				if(s == nil || s[len s-1] != '>')
+					error(x, "end of file in declaration");
+				return nil;
+			* =>
+				error(x, sys->sprint("unknown declaration: %s", s));
+			}
+		}
+		error(x, "invalid XML declaration");
+	} else
+		error(x, sys->sprint("illegal beginning of tag: %c", c));
+	return nil;
+}
+
+characters(x: ref Parser): ref Parcel
+{
+	p: ref Parcel;
+	content := gets(x, '<');
+	if (len content > 0) {
+		if (content[len content - 1] == '<') {
+			ungetc(x);
+			content = content[0:len content - 1];
+		}
+		ws1, ws2: int;
+		if (x.ispre) {
+			content = substituteentities(x, content);
+			ws1 = ws2 = 0;
+		} else
+			(content, ws1, ws2) = substituteentities_sp(x, content);
+		if (content != nil || ws1)
+			p = ref Parcel.Text(content, ws1, ws2);
+	}
+	return p;
+}
+
+startelement(x: ref Parser, name: string)
+{
+	x.estack = name :: x.estack;
+	if (name == x.preelem)
+		x.ispre++;
+}
+
+endelement(x: ref Parser, name: string)
+{
+	if (x.estack != nil && name == hd x.estack) {
+		x.estack = tl x.estack;
+		if (name == x.preelem)
+			x.ispre--;
+	} else {
+		starttag := "";
+		if (x.estack != nil)
+			starttag = hd x.estack;
+		warning(x, sys->sprint("<%s></%s> mismatch", starttag, name));
+
+		# invalid XML but try to recover anyway to reduce turnaround time on fixing errors.
+		# loop back up through the tag stack to see if there's a matching tag, in which case
+		# jump up in the stack to that, making some rude assumptions about the
+		# way Parcels are handled at the top level.
+		n := 0;
+		for (t := x.estack; t != nil; (t, n) = (tl t, n + 1))
+			if (hd t == name)
+				break;
+		if (t != nil) {
+			x.estack = tl t;
+			x.actdepth -= n;
+		}
+	}
+}
+
+buildattrs(x: ref Parser): Attributes
+{
+	attrs: list of Attribute;
+
+	attr: Attribute;
+	for (;;) {
+		skipwhite(x);
+		attr.name = xmlname(x);
+		if (attr.name == nil)
+			break;
+		skipwhite(x);
+		c := getc(x);
+		if(c != '='){
+			ungetc(x);
+			attr.value = nil;
+		}else
+			attr.value = xmlstring(x, 1);
+		attrs = attr :: attrs;
+	}
+	return Attributes(attrs);
+}
+
+xmlstring(x: ref Parser, dosub: int): string
+{
+	skipwhite(x);
+	s := "";
+	delim := getc(x);
+	if (delim == '\"' || delim == '\'') {
+		s = gets(x, delim);
+		n := len s;
+		if (n == 0 || s[n-1] != delim)
+			error(x, "unclosed string at end of file");
+		s = s[0:n-1];	# TO DO: avoid copy
+		if(dosub)
+			s = substituteentities(x, s);
+	} else
+		error(x, sys->sprint("illegal string delimiter: %c", delim));
+	return s;
+}
+
+xmlname(x: ref Parser): string
+{
+	name := "";
+	ch := getc(x);
+	case ch {
+	'_' or ':' or
+	'a' to 'z' or
+	'A' to 'Z' or
+	16r100 to 16rd7ff or
+	16re000 or 16rfffd =>
+		name[0] = ch;
+loop:
+		for (;;) {
+			case ch = getc(x) {
+			'_' or '-' or ':' or '.' or
+			'a' to 'z' or
+			'0' to '9' or
+			'A' to 'Z' or
+			16r100 to 16rd7ff or
+			16re000 to 16rfffd =>
+				name[len name] = ch;
+			* =>
+				break loop;
+			}
+		}
+	}
+	ungetc(x);
+	return name;
+}
+
+substituteentities(x: ref Parser, buff: string): string
+{
+	i := 0;
+	while (i < len buff) {
+		if (buff[i] == '&') {
+			(t, j) := translateentity(x, buff, i);
+			# XXX could be quicker
+			buff = buff[0:i] + t + buff[j:];
+			i += len t;
+		} else
+			i++;
+	}
+	return buff;
+}
+
+# subsitute entities, squashing whitespace along the way.
+substituteentities_sp(x: ref Parser, buf: string): (string, int, int)
+{
+	firstwhite := 0;
+	# skip initial white space
+	for (i := 0; i < len buf; i++) {
+		c := buf[i];
+		if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
+			break;
+		firstwhite = 1;
+	}
+
+	lastwhite := 0;
+	s := "";
+	for (; i < len buf; i++) {
+		c := buf[i];
+		if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
+			lastwhite = 1;
+		else {
+			if (lastwhite) {
+				s[len s] = ' ';
+				lastwhite = 0;
+			}
+			if (c == '&') {
+				# should &x20; count as whitespace?
+				(ent, j) := translateentity(x, buf, i);
+				i = j - 1;
+				s += ent;
+			} else
+				s[len s] = c;
+		}
+	}
+	return (s, firstwhite, lastwhite);
+}
+
+translateentity(x: ref Parser, s: string, i: int): (string, int)
+{
+	i++;
+	for (j := i; j < len s; j++)
+		if (s[j] == ';')
+			break;
+	ent := s[i:j];
+	if (j == len s) {
+		if (len ent > 10)
+			ent = ent[0:11] + "...";
+		warning(x, sys->sprint("missing ; at end of entity (&%s)", ent));
+		return (nil, i);
+	}
+	j++;
+	if (ent == nil) {
+		warning(x, "empty entity");
+		return ("", j);
+	}
+	if (ent[0] == '#') {
+		n: int;
+		rem := ent;
+		if (len ent >= 3 && ent[1] == 'x')
+			(n, rem) = str->toint(ent[2:], 16);
+		else if (len ent >= 2)
+			(n, rem) = str->toint(ent[1:], 10);
+		if (rem != nil) {
+			warning(x, sys->sprint("unrecognized entity (&%s)", ent));
+			return (nil, j);
+		}
+		ch: string = nil;
+		ch[0] = n;
+		return (ch, j);
+	}
+	hv := entdict.find(ent);
+	if (hv == nil) {
+		warning(x, sys->sprint("unrecognized entity (&%s)", ent));
+		return (nil, j);
+	}
+	return (hv.s, j);
+}
+
+balancedstring(x: ref Parser, eos: string): string
+{
+	s := "";
+	instring := 0;
+	quote: int;
+
+	for (i := 0; i < len eos; i++)
+		s[len s] = ' ';
+
+	skipwhite(x);
+	while ((c := getc(x)) != Bufio->EOF) {
+		s[len s] = c;
+		if (instring) {
+			if (c == quote)
+				instring = 0;
+		} else if (c == '\"' || c == '\'') {
+			quote = c;
+			instring = 1;
+		} else if (s[len s - len eos : len s] == eos)
+			return s[len eos : len s - len eos];
+	}
+	error(x, sys->sprint("unexpected end of file while looking for \"%s\"", eos));
+	return "";
+}
+
+skipwhite(x: ref Parser)
+{
+	while ((c := getc(x)) == ' ' || c == '\t' || c == '\n' || c == '\r')
+		;
+	ungetc(x);
+}
+
+expectwhite(x: ref Parser)
+{
+	if ((c := getc(x)) != ' ' && c != '\t' && c != '\n' && c != '\r')
+		error(x, "expecting white space");
+	skipwhite(x);
+}
+
+expect(x: ref Parser, ch: int)
+{
+	skipwhite(x);
+	c := getc(x);
+	if (c != ch)
+		error(x, sys->sprint("expecting %c", ch));
+}
+
+getc(x: ref Parser): int
+{
+	if (x.eof)
+		return Bufio->EOF;
+	ch := x.in.getc();
+	if (ch == Bufio->EOF)
+		x.eof = 1;
+	else if (ch == '\n')
+		x.loc.line++;
+	x.lastnl = ch == '\n';
+	return ch;
+}
+
+gets(x: ref Parser, delim: int): string
+{
+	if (x.eof)
+		return "";
+	s := x.in.gets(delim);
+	for (i := 0; i < len s; i++)
+		if (s[i] == '\n')
+			x.loc.line++;
+	if (s == "")
+		x.eof = 1;
+	else
+		x.lastnl = s[len s - 1] == '\n';
+	return s;
+}
+
+ungetc(x: ref Parser)
+{
+	if (x.eof)
+		return;
+	x.in.ungetc();
+	x.loc.line -= x.lastnl;
+}
+
+Attributes.all(al: self Attributes): list of Attribute
+{
+	return al.attrs;
+}
+
+Attributes.get(attrs: self Attributes, name: string): string
+{
+	for (a := attrs.attrs; a != nil; a = tl a)
+		if ((hd a).name == name)
+			return (hd a).value;
+	return nil;
+}
+
+warning(x: ref Parser, msg: string)
+{
+	if (x.warning != nil)
+		x.warning <-= (x.loc, msg);
+}
+
+error(x: ref Parser, msg: string)
+{
+	x.errormsg = msg;
+	raise "sax:error";
+}
--- /dev/null
+++ b/appl/math/ack.b
@@ -1,0 +1,43 @@
+implement Ackermann;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+
+Ackermann: module
+{
+        init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	argv = tl argv;		# remove program name
+	m := n := 0;
+	if(argv != nil){
+		m = int hd argv;
+		argv = tl argv;
+	}
+	if(m < 0)
+		m = 0;
+	if(argv != nil)
+		n = int hd argv;
+	if(n < 0)
+		n = 0;
+	t0 := sys->millisec();
+	a := ack(m, n);
+	t1 := sys->millisec();
+	sys->print("A(%d, %d) = %d (t = %d ms)\n", m, n, a, t1-t0);	
+}
+
+ack(m, n: int) : int
+{
+        if(m == 0)
+                return n+1;
+        else if(n == 0)
+                return ack(m-1, 1);
+        else
+                return ack(m-1, ack(m, n-1));
+}
--- /dev/null
+++ b/appl/math/crackerbarrel.b
@@ -1,0 +1,133 @@
+implement CBPuzzle;
+
+# Cracker Barrel Puzzle
+#
+# Holes are drilled in a triangular arrangement into which all but one
+# are seated pegs. A 6th order puzzle appears in the diagram below.
+# Note, the hole in the lower left corner of the triangle is empty.
+#
+#                 V
+#               V   V
+#             V   V   V
+#           V   V   V   V
+#         V   V   V   V   V
+#       O   V   V   V   V   V
+#
+# Pegs are moved by jumping over a neighboring peg thereby removing the
+# jumped peg. A peg can only be moved if a neighboring hole contains a
+# peg and the hole on the other side of the neighbor is empty. The last
+# peg cannot be removed.
+#
+# The object is to remove as many pegs as possible.
+
+include "sys.m";
+   sys: Sys;
+include "draw.m";
+
+CBPuzzle: module {
+   init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+ORDER: con 6;
+
+Move: adt {
+   x, y: int;
+};
+
+valid:= array[] of {Move (1,0), (0,1), (-1,1), (-1,0), (0,-1), (1,-1)};
+
+board:= array[ORDER*ORDER] of int;
+pegs, minpegs: int;
+
+puzzle(): int
+{
+   if (pegs < minpegs)
+      minpegs = pegs;
+
+   if (pegs == 1)
+      return 1;
+
+   # Check each row of puzzle
+   for (r := 0; r < ORDER; r += 1)
+      # Check each column
+      for (c := 0; c < ORDER-r; c += 1) {
+         fromx := r*ORDER + c;
+         # Is a peg in this hole?
+         if (board[fromx])
+            # Check valid moves from this hole
+            for (m := 0; m < len valid; m += 1) {
+               tor := r + 2*valid[m].y;
+               toc := c + 2*valid[m].x;
+
+               # Is new location still on the board?
+               if (tor + toc < ORDER && tor >= 0 && toc >= 0) {
+                  jumpr := r + valid[m].y;
+                  jumpc := c + valid[m].x;
+                  jumpx := jumpr*ORDER + jumpc;
+
+                  # Is neighboring hole occupied?
+                  if (board[jumpx]) {
+                     # Is new location empty?
+                     tox := tor*ORDER + toc;
+
+                     if (! board[tox]) {
+                        # Jump neighboring hole
+                        board[fromx] = 0;
+                        board[jumpx] = 0;
+                        board[tox] = 1;
+                        pegs -= 1;
+
+                        # Try solving puzzle from here
+                        if (puzzle()) {
+                           #sys->print("(%d,%d) - (%d,%d)\n", r, c, tor, toc);
+                           return 1;
+                        }
+                        # Dead end, put pegs back and try another move
+                        board[fromx] = 1;
+                        board[jumpx] = 1;
+                        board[tox] = 0;
+                        pegs += 1;
+                     } # empty location
+                  } # occupied neighbor
+               } # still on board
+            } # valid moves
+      }
+   return 0;
+}
+
+solve(): int
+{
+   minpegs = pegs = (ORDER+1)*ORDER/2 - 1;
+
+   # Put pegs on board
+   for (r := 0; r < ORDER; r += 1)
+      for (c := 0; c < ORDER - r; c += 1)
+         board[r*ORDER + c] = 1;
+
+   # Remove one peg
+   board[0] = 0;
+
+   return puzzle();
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+   sys = load Sys Sys->PATH;
+
+   TRIALS: int;
+   if (len args < 2)
+      TRIALS = 1;
+   else
+      TRIALS = int hd tl args;
+
+   start := sys->millisec();
+   for (trials := 0; trials < TRIALS; trials += 1)
+      solved := solve();
+   end := sys->millisec();
+
+   sys->print("%d ms\n", end - start);
+
+   if (! solved)
+      sys->print("No solution\n");
+   sys->print("Minimum pegs: %d\n", minpegs);
+}
--- /dev/null
+++ b/appl/math/doc.txt
@@ -1,0 +1,48 @@
+==========graph.b, gr.b================
+
+I believe scientific authors and readers are best served by a
+minimalist approach that makes all plots look boringly alike, except
+for the data content. Here is a library that does the task of
+accumulating data to find a bounding box and then draws the curves,
+points, and text surrounded by readable axes.
+
+The command   appl/math/graph.dis  is meant to be launched from wm and
+then provided a file containing (x,y) pairs.  There are no options.
+
+The library version, appl/math/gr.b, is a little more flexible.
+
+Include gr.m and call p := GR->open().  To draw a curve, call
+   p.graph(x,y)
+where x and y are of real arrays. To add the string s at a point (u,v), call
+   p.text(j,s,u,v)
+where j is LJUST, CENTER, or RJUST to indicate whether the left, middle,
+or right of the string should be at (u,v) plus one of HIGH, MED, BASE, or LOW
+to indicate where the baseline of the text should be relative to (u,v).
+To get text running in the y direction, add UP. To enforce the
+same scaling in x and y, call   p.equalxy().
+
+To change from the default solid line, call
+   p.pen(j) where j is one
+of the symbols: DASHED, DOTTED, REFERENCE, SOLID, 
+CIRCLE, CROSS, or INVIS. A CIRCLE and CROSS
+"pen" just puts markers at the points, and doesn't connect with line
+segments. A REFERENCE line is lighter than other lines. DASHED
+curve follows the curve even within one dash, and preserves arclength of
+dashes and spaces. An INVIS line is sometimes handy as "strut" for
+maintaining a consistent scale across a series of plots.
+
+Finally, call
+   p.out(p,"xlabel","xunit","ylabel","yunit")
+to put out the curves and text in PostScript on standard output.
+Axes are produced with "Guggenheim slash notation," in which the user
+variable is divided by scaled units to get dimensionless numbers of
+reasonable magnitude.
+
+The function
+	name := p.getfilename();
+opens a dialog box to get a filename from the user and update the titlebar;
+	p.bye();
+pauses until the user clicks the "X" exit button in the titlebar.
+
+
+<ehg@bell-labs.com> 16 May 1996
--- /dev/null
+++ b/appl/math/factor.b
@@ -1,0 +1,180 @@
+#
+#	initially generated by c2l
+#
+
+implement Factor;
+
+include "draw.m";
+
+Factor: module
+{
+	init: fn(nil: ref Draw->Context, argl: list of string);
+};
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf:  import bufio;
+include "math.m";
+	maths: Math;
+	modf: import maths;
+
+init(nil: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	maths = load Math Math->PATH;
+	main(len argl, argl);
+}
+
+WHLEN: con 48;
+wheel := array[WHLEN] of {
+	real 2,
+	real 10,
+	real 2,
+	real 4,
+	real 2,
+	real 4,
+	real 6,
+	real 2,
+	real 6,
+	real 4,
+	real 2,
+	real 4,
+	real 6,
+	real 6,
+	real 2,
+	real 6,
+	real 4,
+	real 2,
+	real 6,
+	real 4,
+	real 6,
+	real 8,
+	real 4,
+	real 2,
+	real 4,
+	real 2,
+	real 4,
+	real 8,
+	real 6,
+	real 4,
+	real 6,
+	real 2,
+	real 4,
+	real 6,
+	real 2,
+	real 6,
+	real 6,
+	real 4,
+	real 2,
+	real 4,
+	real 6,
+	real 2,
+	real 6,
+	real 4,
+	real 2,
+	real 4,
+	real 2,
+	real 10,
+};
+bin: ref Iobuf;
+
+main(argc: int, argv: list of string)
+{
+	n: real;
+	i: int;
+	l: string;
+
+	if(argc > 1){
+		argv = tl argv;
+		for(i = 1; i < argc; i++){
+			n = real hd argv;
+			factor(n);
+			argv = tl argv;
+		}
+		exit;
+	}
+	bin = bufio->fopen(sys->fildes(0), Sys->OREAD);
+	for(;;){
+		l = bin.gets('\n');
+		if(l == nil)
+			break;
+		n = real l;
+		if(n <= real 0)
+			break;
+		factor(n);
+	}
+	exit;
+}
+
+factor(n: real)
+{
+	quot, d, s: real;
+	i: int;
+
+	sys->print("%d\n", int n);
+	if(n == real 0)
+		return;
+	s = maths->sqrt(n)+real 1;
+	for(;;){
+		(iquot, frac) := modf(n/real 2);
+		if(frac != real 0)
+			break;
+		quot = real iquot;
+		sys->print("     2\n");
+		n = quot;
+		s = maths->sqrt(n)+real 1;
+	}
+	for(;;){
+		(iquot, frac) := modf(n/real 3);
+		if(frac != real 0)
+			break;
+		quot = real iquot;
+		sys->print("     3\n");
+		n = quot;
+		s = maths->sqrt(n)+real 1;
+	}
+	for(;;){
+		(iquot, frac) := modf(n/real 5);
+		if(frac != real 0)
+			break;
+		quot = real iquot;
+		sys->print("     5\n");
+		n = quot;
+		s = maths->sqrt(n)+real 1;
+	}
+	for(;;){
+		(iquot, frac) := modf(n/real 7);
+		if(frac != real 0)
+			break;
+		quot = real iquot;
+		sys->print("     7\n");
+		n = quot;
+		s = maths->sqrt(n)+real 1;
+	}
+	d = real 1;
+	for(i = 1;;){
+		d += wheel[i];
+		for(;;){
+			(iquot, frac) := modf(n/d);
+			if(frac != real 0)
+				break;
+			quot = real iquot;
+			sys->print("     %d\n", int d);
+			n = quot;
+			s = maths->sqrt(n)+real 1;
+		}
+		i++;
+		if(i >= WHLEN){
+			i = 0;
+			if(d > s)
+				break;
+		}
+	}
+	if(n > real 1)
+		sys->print("     %d\n", int n);
+	sys->print("\n");
+}
+
--- /dev/null
+++ b/appl/math/ffts.b
@@ -1,0 +1,639 @@
+implement FFTs;
+include "sys.m";
+	sys: Sys;
+	print: import sys;
+include "math.m";
+	math: Math;
+	cos, sin, Degree, Pi: import math;
+include "ffts.m";
+
+#  by r. c. singleton, stanford research institute, sept. 1968
+#  translated to limbo by eric grosse, jan 1997
+#  arrays at(maxf), ck(maxf), bt(maxf), sk(maxf), and np(maxp)
+#    are used for temporary storage.  if the available storage
+#    is insufficient, the program exits.
+#    maxf must be >= the maximum prime factor of n.
+#    maxp must be > the number of prime factors of n.
+#    in addition, if the square-free portion k of n has two or
+#    more prime factors, then maxp must be >= k-1.
+#  array storage in nfac for a maximum of 15 prime factors of n.
+#  if n has more than one square-free factor, the product of the
+#    square-free factors must be <= 210
+
+ffts(a,b:array of real, ntot,n,nspan,isn:int){
+	maxp: con 209;
+	i,ii,inc,j,jc,jf,jj,k,k1,k2,k3,k4,kk:int;
+	ks,kspan,kspnn,kt,m,maxf,nn,nt:int;
+	aa,aj,ajm,ajp,ak,akm,akp,bb,bj,bjm,bjp,bk,bkm,bkp:real;
+	c1,c2,c3,c72,cd,rad,radf,s1,s2,s3,s72,s120,sd:real;
+	maxf = 23;
+	if(math == nil){
+		sys = load Sys Sys->PATH;
+		math = load Math Math->PATH;
+	}
+	nfac := array[12] of int;
+	np := array[maxp] of int;
+	at := array[23] of real;
+	ck := array[23] of real;
+	bt := array[23] of real;
+	sk := array[23] of real;
+
+	if(n<2) return;
+	inc = isn;
+	c72 = cos(72.*Degree);
+	s72 = sin(72.*Degree);
+	s120 = sin(120.*Degree);
+	rad = 2.*Pi;
+	if(isn<0){
+		s72 = -s72;
+		s120 = -s120;
+		rad = -rad;
+		inc = -inc;
+	}
+	nt = inc*ntot;
+	ks = inc*nspan;
+	kspan = ks;
+	nn = nt-inc;
+	jc = ks/n;
+	radf = rad*real(jc)*0.5;
+	i = 0;
+	jf = 0;
+
+	#  determine the factors of n
+	m = 0;
+	k = n;
+	while(k==k/16*16){
+		m = m+1;
+		nfac[m] = 4;
+		k = k/16;
+	}
+	j = 3;
+	jj = 9;
+	for(;;)
+		if(k%jj==0){
+			m = m+1;
+			nfac[m] = j;
+			k = k/jj;
+		}else{
+			j = j+2;
+			jj = j*j;
+			if(jj>k)
+				break;
+		}
+	if(k<=4){
+		kt = m;
+		nfac[m+1] = k;
+		if(k!=1)
+			m = m+1;
+	}else{
+		if(k==k/4*4){
+			m = m+1;
+			nfac[m] = 2;
+			k = k/4;
+		}
+		kt = m;
+		j = 2;
+		do{
+			if(k%j==0){
+				m = m+1;
+				nfac[m] = j;
+				k = k/j;
+			}
+			j = ((j+1)/2)*2+1;
+		}while(j<=k);
+	}
+	if(kt!=0){
+		j = kt;
+		do{
+			m = m+1;
+			nfac[m] = nfac[j];
+			j = j-1;
+		}while(j!=0);
+	}
+
+	for(;;){ #  compute fourier transform
+		sd = radf/real(kspan);
+		cd = sin(sd);
+		cd = 2.0*cd*cd;
+		sd = sin(sd+sd);
+		kk = 1;
+		i = i+1;
+		if(nfac[i]==2){ #  transform for factor of 2 (including rotation factor)
+			kspan = kspan/2;
+			k1 = kspan+2;
+			for(;;){
+				k2 = kk+kspan;
+				ak = a[k2-1];
+				bk = b[k2-1];
+				a[k2-1] = a[kk-1]-ak;
+				b[k2-1] = b[kk-1]-bk;
+				a[kk-1] = a[kk-1]+ak;
+				b[kk-1] = b[kk-1]+bk;
+				kk = k2+kspan;
+				if(kk>nn){
+					kk = kk-nn;
+					if(kk>jc)
+						break;
+				}
+			}
+			if(kk>kspan)
+				break;
+			do{
+				c1 = 1.0-cd;
+				s1 = sd;
+				for(;;){
+					k2 = kk+kspan;
+					ak = a[kk-1]-a[k2-1];
+					bk = b[kk-1]-b[k2-1];
+					a[kk-1] = a[kk-1]+a[k2-1];
+					b[kk-1] = b[kk-1]+b[k2-1];
+					a[k2-1] = c1*ak-s1*bk;
+					b[k2-1] = s1*ak+c1*bk;
+					kk = k2+kspan;
+					if(kk>=nt){
+						k2 = kk-nt;
+						c1 = -c1;
+						kk = k1-k2;
+						if(kk<=k2){
+							ak = c1-(cd*c1+sd*s1);
+							s1 = (sd*c1-cd*s1)+s1;
+							c1 = 2.0-(ak*ak+s1*s1);
+							s1 = c1*s1;
+							c1 = c1*ak;
+							kk = kk+jc;
+							if(kk>=k2)
+								break;
+						}
+					}
+				}
+				k1 = k1+inc+inc;
+				kk = (k1-kspan)/2+jc;
+			}while(kk<=jc+jc);
+		}else{	#  transform for factor of 4
+			if(nfac[i]!=4){
+				#  transform for odd factors
+				k = nfac[i];
+				kspnn = kspan;
+				kspan = kspan/k;
+				if(k==3)
+					for(;;){
+						#  transform for factor of 3 (optional code)
+						k1 = kk+kspan;
+						k2 = k1+kspan;
+						ak = a[kk-1];
+						bk = b[kk-1];
+						aj = a[k1-1]+a[k2-1];
+						bj = b[k1-1]+b[k2-1];
+						a[kk-1] = ak+aj;
+						b[kk-1] = bk+bj;
+						ak = -0.5*aj+ak;
+						bk = -0.5*bj+bk;
+						aj = (a[k1-1]-a[k2-1])*s120;
+						bj = (b[k1-1]-b[k2-1])*s120;
+						a[k1-1] = ak-bj;
+						b[k1-1] = bk+aj;
+						a[k2-1] = ak+bj;
+						b[k2-1] = bk-aj;
+						kk = k2+kspan;
+						if(kk>=nn){
+							kk = kk-nn;
+							if(kk>kspan)
+								break;
+						}
+					}
+				else if(k==5){
+					#  transform for factor of 5 (optional code)
+					c2 = c72*c72-s72*s72;
+					s2 = 2.0*c72*s72;
+					for(;;){
+						k1 = kk+kspan;
+						k2 = k1+kspan;
+						k3 = k2+kspan;
+						k4 = k3+kspan;
+						akp = a[k1-1]+a[k4-1];
+						akm = a[k1-1]-a[k4-1];
+						bkp = b[k1-1]+b[k4-1];
+						bkm = b[k1-1]-b[k4-1];
+						ajp = a[k2-1]+a[k3-1];
+						ajm = a[k2-1]-a[k3-1];
+						bjp = b[k2-1]+b[k3-1];
+						bjm = b[k2-1]-b[k3-1];
+						aa = a[kk-1];
+						bb = b[kk-1];
+						a[kk-1] = aa+akp+ajp;
+						b[kk-1] = bb+bkp+bjp;
+						ak = akp*c72+ajp*c2+aa;
+						bk = bkp*c72+bjp*c2+bb;
+						aj = akm*s72+ajm*s2;
+						bj = bkm*s72+bjm*s2;
+						a[k1-1] = ak-bj;
+						a[k4-1] = ak+bj;
+						b[k1-1] = bk+aj;
+						b[k4-1] = bk-aj;
+						ak = akp*c2+ajp*c72+aa;
+						bk = bkp*c2+bjp*c72+bb;
+						aj = akm*s2-ajm*s72;
+						bj = bkm*s2-bjm*s72;
+						a[k2-1] = ak-bj;
+						a[k3-1] = ak+bj;
+						b[k2-1] = bk+aj;
+						b[k3-1] = bk-aj;
+						kk = k4+kspan;
+						if(kk>=nn){
+							kk = kk-nn;
+							if(kk>kspan)
+								break;
+						}
+					}
+				}else{
+					if(k!=jf){
+						jf = k;
+						s1 = rad/real(k);
+						c1 = cos(s1);
+						s1 = sin(s1);
+						if(jf>maxf){
+							sys->fprint(sys->fildes(2),"too many primes for fft");
+							exit;
+						}
+						ck[jf-1] = 1.0;
+						sk[jf-1] = 0.0;
+						j = 1;
+						do{
+							ck[j-1] = ck[k-1]*c1+sk[k-1]*s1;
+							sk[j-1] = ck[k-1]*s1-sk[k-1]*c1;
+							k = k-1;
+							ck[k-1] = ck[j-1];
+							sk[k-1] = -sk[j-1];
+							j = j+1;
+						}while(j<k);
+					}
+					for(;;){
+						k1 = kk;
+						k2 = kk+kspnn;
+						aa = a[kk-1];
+						bb = b[kk-1];
+						ak = aa;
+						bk = bb;
+						j = 1;
+						k1 = k1+kspan;
+						do{
+							k2 = k2-kspan;
+							j = j+1;
+							at[j-1] = a[k1-1]+a[k2-1];
+							ak = at[j-1]+ak;
+							bt[j-1] = b[k1-1]+b[k2-1];
+							bk = bt[j-1]+bk;
+							j = j+1;
+							at[j-1] = a[k1-1]-a[k2-1];
+							bt[j-1] = b[k1-1]-b[k2-1];
+							k1 = k1+kspan;
+						}while(k1<k2);
+						a[kk-1] = ak;
+						b[kk-1] = bk;
+						k1 = kk;
+						k2 = kk+kspnn;
+						j = 1;
+						do{
+							k1 = k1+kspan;
+							k2 = k2-kspan;
+							jj = j;
+							ak = aa;
+							bk = bb;
+							aj = 0.0;
+							bj = 0.0;
+							k = 1;
+							do{
+								k = k+1;
+								ak = at[k-1]*ck[jj-1]+ak;
+								bk = bt[k-1]*ck[jj-1]+bk;
+								k = k+1;
+								aj = at[k-1]*sk[jj-1]+aj;
+								bj = bt[k-1]*sk[jj-1]+bj;
+								jj = jj+j;
+								if(jj>jf)
+									jj = jj-jf;
+							}while(k<jf);
+							k = jf-j;
+							a[k1-1] = ak-bj;
+							b[k1-1] = bk+aj;
+							a[k2-1] = ak+bj;
+							b[k2-1] = bk-aj;
+							j = j+1;
+						}while(j<k);
+						kk = kk+kspnn;
+						if(kk>nn){
+							kk = kk-nn;
+							if(kk>kspan)
+								break;
+						}
+					}
+				}
+				#  multiply by rotation factor (except for factors of 2 and 4)
+				if(i==m)
+					break;
+				kk = jc+1;
+				do{
+					c2 = 1.0-cd;
+					s1 = sd;
+					do{
+						c1 = c2;
+						s2 = s1;
+						kk = kk+kspan;
+						for(;;){
+							ak = a[kk-1];
+							a[kk-1] = c2*ak-s2*b[kk-1];
+							b[kk-1] = s2*ak+c2*b[kk-1];
+							kk = kk+kspnn;
+							if(kk>nt){
+								ak = s1*s2;
+								s2 = s1*c2+c1*s2;
+								c2 = c1*c2-ak;
+								kk = kk-nt+kspan;
+								if(kk>kspnn)
+									break;
+							}
+						}
+						c2 = c1-(cd*c1+sd*s1);
+						s1 = s1+(sd*c1-cd*s1);
+						c1 = 2.0-(c2*c2+s1*s1);
+						s1 = c1*s1;
+						c2 = c1*c2;
+						kk = kk-kspnn+jc;
+					}while(kk<=kspan);
+					kk = kk-kspan+jc+inc;
+				}while(kk<=jc+jc);
+			}else{
+				kspnn = kspan;
+				kspan = kspan/4;
+				do{
+					c1 = 1.;
+					s1 = 0.;
+					for(;;){
+						k1 = kk+kspan;
+						k2 = k1+kspan;
+						k3 = k2+kspan;
+						akp = a[kk-1]+a[k2-1];
+						akm = a[kk-1]-a[k2-1];
+						ajp = a[k1-1]+a[k3-1];
+						ajm = a[k1-1]-a[k3-1];
+						a[kk-1] = akp+ajp;
+						ajp = akp-ajp;
+						bkp = b[kk-1]+b[k2-1];
+						bkm = b[kk-1]-b[k2-1];
+						bjp = b[k1-1]+b[k3-1];
+						bjm = b[k1-1]-b[k3-1];
+						b[kk-1] = bkp+bjp;
+						bjp = bkp-bjp;
+						do10 := 0;
+						if(isn<0){
+							akp = akm+bjm;
+							akm = akm-bjm;
+							bkp = bkm-ajm;
+							bkm = bkm+ajm;
+							if(s1!=0.) do10 = 1;
+						}else{
+							akp = akm-bjm;
+							akm = akm+bjm;
+							bkp = bkm+ajm;
+							bkm = bkm-ajm;
+							if(s1!=0.) do10 = 1;
+						}
+						if(do10){
+							a[k1-1] = akp*c1-bkp*s1;
+							b[k1-1] = akp*s1+bkp*c1;
+							a[k2-1] = ajp*c2-bjp*s2;
+							b[k2-1] = ajp*s2+bjp*c2;
+							a[k3-1] = akm*c3-bkm*s3;
+							b[k3-1] = akm*s3+bkm*c3;
+							kk = k3+kspan;
+							if(kk<=nt)
+								continue;
+						}else{
+							a[k1-1] = akp;
+							b[k1-1] = bkp;
+							a[k2-1] = ajp;
+							b[k2-1] = bjp;
+							a[k3-1] = akm;
+							b[k3-1] = bkm;
+							kk = k3+kspan;
+							if(kk<=nt)
+								continue;
+						}
+						c2 = c1-(cd*c1+sd*s1);
+						s1 = (sd*c1-cd*s1)+s1;
+						c1 = 2.0-(c2*c2+s1*s1);
+						s1 = c1*s1;
+						c1 = c1*c2;
+						c2 = c1*c1-s1*s1;
+						s2 = 2.0*c1*s1;
+						c3 = c2*c1-s2*s1;
+						s3 = c2*s1+s2*c1;
+						kk = kk-nt+jc;
+						if(kk>kspan)
+							break;
+					}
+					kk = kk-kspan+inc;
+				}while(kk<=jc);
+				if(kspan==jc)
+					break;
+			}
+		}
+	} # end "compute fourier transform"
+
+	#  permute the results to normal order---done in two stages
+	#  permutation for square factors of n
+	np[0] = ks;
+	if(kt!=0){
+		k = kt+kt+1;
+		if(m<k)
+			k = k-1;
+		j = 1;
+		np[k] = jc;
+		do{
+			np[j] = np[j-1]/nfac[j];
+			np[k-1] = np[k]*nfac[j];
+			j = j+1;
+			k = k-1;
+		}while(j<k);
+		k3 = np[k];
+		kspan = np[1];
+		kk = jc+1;
+		k2 = kspan+1;
+		j = 1;
+		if(n!=ntot){
+			for(;;){
+				#  permutation for multivariate transform
+				k = kk+jc;
+				do{
+					ak = a[kk-1];
+					a[kk-1] = a[k2-1];
+					a[k2-1] = ak;
+					bk = b[kk-1];
+					b[kk-1] = b[k2-1];
+					b[k2-1] = bk;
+					kk = kk+inc;
+					k2 = k2+inc;
+				}while(kk<k);
+				kk = kk+ks-jc;
+				k2 = k2+ks-jc;
+				if(kk>=nt){
+					k2 = k2-nt+kspan;
+					kk = kk-nt+jc;
+					if(k2>=ks)
+	permm:					for(;;){
+							k2 = k2-np[j-1];
+							j = j+1;
+							k2 = np[j]+k2;
+							if(k2<=np[j-1]){
+								j = 1;
+								do{
+									if(kk<k2)
+										break permm;
+									kk = kk+jc;
+									k2 = kspan+k2;
+								}while(k2<ks);
+								if(kk>=ks)
+									break permm;
+							}
+						}
+				}
+			}
+			jc = k3;
+		}else{
+			for(;;){
+				#  permutation for single-variate transform (optional code)
+				ak = a[kk-1];
+				a[kk-1] = a[k2-1];
+				a[k2-1] = ak;
+				bk = b[kk-1];
+				b[kk-1] = b[k2-1];
+				b[k2-1] = bk;
+				kk = kk+inc;
+				k2 = kspan+k2;
+				if(k2>=ks)
+	perms:				for(;;){
+						k2 = k2-np[j-1];
+						j = j+1;
+						k2 = np[j]+k2;
+						if(k2<=np[j-1]){
+							j = 1;
+							do{
+								if(kk<k2)
+									break perms;
+								kk = kk+inc;
+								k2 = kspan+k2;
+							}while(k2<ks);
+							if(kk>=ks)
+								break perms;
+						}
+					}
+			}
+			jc = k3;
+		}
+	}
+	if(2*kt+1>=m)
+		return;
+	kspnn = np[kt];
+	#  permutation for square-free factors of n
+	j = m-kt;
+	nfac[j+1] = 1;
+	do{
+		nfac[j] = nfac[j]*nfac[j+1];
+		j = j-1;
+	}while(j!=kt);
+	kt = kt+1;
+	nn = nfac[kt]-1;
+	if(nn<=maxp){
+		jj = 0;
+		j = 0;
+		for(;;){
+			k2 = nfac[kt];
+			k = kt+1;
+			kk = nfac[k];
+			j = j+1;
+			if(j>nn)
+				break;
+			for(;;){
+				jj = kk+jj;
+				if(jj<k2)
+					break;
+				jj = jj-k2;
+				k2 = kk;
+				k = k+1;
+				kk = nfac[k];
+			}
+			np[j-1] = jj;
+		}
+		#  determine the permutation cycles of length greater than 1
+		j = 0;
+		for(;;){
+			j = j+1;
+			kk = np[j-1];
+			if(kk>=0)
+				if(kk==j){
+					np[j-1] = -j;
+					if(j==nn)
+						break;
+				}else{
+					do{
+						k = kk;
+						kk = np[k-1];
+						np[k-1] = -kk;
+					}while(kk!=j);
+					k3 = kk;
+				}
+		}
+		maxf = inc*maxf;
+		for(;;){
+			j = k3+1;
+			nt = nt-kspnn;
+			ii = nt-inc+1;
+			if(nt<0)
+				break;
+			for(;;){
+				j = j-1;
+				if(np[j-1]>=0){
+					jj = jc;
+					do{
+						kspan = jj;
+						if(jj>maxf)
+							kspan = maxf;
+						jj = jj-kspan;
+						k = np[j-1];
+						kk = jc*k+ii+jj;
+						k1 = kk+kspan;
+						k2 = 0;
+						do{
+							k2 = k2+1;
+							at[k2-1] = a[k1-1];
+							bt[k2-1] = b[k1-1];
+							k1 = k1-inc;
+						}while(k1!=kk);
+						do{
+							k1 = kk+kspan;
+							k2 = k1-jc*(k+np[k-1]);
+							k = -np[k-1];
+							do{
+								a[k1-1] = a[k2-1];
+								b[k1-1] = b[k2-1];
+								k1 = k1-inc;
+								k2 = k2-inc;
+							}while(k1!=kk);
+							kk = k2;
+						}while(k!=j);
+						k1 = kk+kspan;
+						k2 = 0;
+						do{
+							k2 = k2+1;
+							a[k1-1] = at[k2-1];
+							b[k1-1] = bt[k2-1];
+							k1 = k1-inc;
+						}while(k1!=kk);
+					}while(jj!=0);
+					if(j==1)
+						break;
+				}
+			}
+		}
+	}
+}
--- /dev/null
+++ b/appl/math/fibonacci.b
@@ -1,0 +1,59 @@
+implement Fibonacci;
+
+include "sys.m";
+include "draw.m";
+
+Fibonacci: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys := load Sys Sys->PATH;
+	for(i := 0; ; i++){
+		f := fibonacci(i);
+		if(f < 0)
+			break;
+		sys->print("F(%d) = %d\n", i, f);
+	}
+}
+
+FIB: exception(int, int);
+HELP: con "help";
+
+NOVAL: con -1000000000;
+
+fibonacci(n: int): int
+{
+	{
+		fib(1, n, 1, 1);
+	}
+	exception e{
+		FIB =>
+			(x, nil) := e;
+			return x;
+		* =>
+			return NOVAL;
+	}
+	return NOVAL;
+}
+
+fib(n: int, m: int, x: int, y: int) raises (FIB)
+{
+	if(n >= m)
+		raise FIB(x, y);
+
+	{
+		fib(n+1, m, x, y);
+	}
+	exception e{
+		FIB =>
+			(x, y) = e;
+			x = x+y;
+			y = x-y;
+			raise FIB(x, y);
+		* =>
+			raise HELP;
+	}
+}
--- /dev/null
+++ b/appl/math/fit.b
@@ -1,0 +1,264 @@
+# fit a polynomial to a set of points
+#	fit -dn [-v]
+#		where n is the degree of the polynomial
+
+implement Fit;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "math.m";
+	maths: Math;
+include "bufio.m";
+	bufio: Bufio;
+include "arg.m";
+
+Fit: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+MAXPTS: con 512;
+MAXDEG: con 16;
+EPS: con 0.0000005;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	maths = load Math Math->PATH;
+	if(maths == nil)
+		fatal(sys->sprint("cannot load maths library"));
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		fatal(sys->sprint("cannot load bufio"));
+	main(argv);
+}
+
+isn(r: real, n: int): int
+{
+	s := r - real n;
+	if(s < 0.0)
+		s = -s;
+	return s < EPS;
+}
+
+fact(n: int): real
+{
+	f := 1.0;
+	for(i := 1; i <= n; i++)
+		f *= real i;
+	return f;
+}
+
+comb(n: int, r: int): real
+{
+	f := 1.0;
+	for(i := 0; i < r; i++)
+		f *= real (n-i);
+	return f/fact(r);
+}
+
+matalloc(n: int): array of array of real
+{
+	mat := array[n] of array of real;
+	for(i := 0; i < n; i++)
+		mat[i] = array[n] of real;
+	return mat;
+}
+
+matsalloc(n: int): array of array of array of real
+{
+	mats := array[n+1] of array of array of real;
+	for(i := 0; i <= n; i++)
+		mats[i] = matalloc(i);
+	return mats;
+}
+
+det(mat: array of array of real, n: int, mats: array of array of array of real): real
+{
+	# easy cases first
+	if(n == 0)
+		return 1.0;
+	if(n == 1)
+		return mat[0][0];
+	if(n == 2)
+		return mat[0][0]*mat[1][1]-mat[0][1]*mat[1][0];
+	d := 0.0;
+	s := 1;
+	m := mats[n-1];
+	for(k := 0; k < n; k++){
+		for(i := 0; i < n-1; i++){
+			for(j := 0; j < n-1; j++){
+				if(j < k)
+					m[i][j] = mat[i+1][j];
+				else
+					m[i][j] = mat[i+1][j+1];
+			}
+		}
+		d += (real s)*mat[0][k]*det(m, n-1, mats);
+		s = -s;
+	}
+	return d;
+}
+
+main(argv: list of string)
+{
+	i, j: int;
+	x, y, z: real;
+	fb: ref Bufio->Iobuf;
+
+	n := 0;
+	p := 1;
+	arg := load Arg Arg->PATH;	
+	if(arg == nil)
+		fatal(sys->sprint("cannot load %s: %r", Arg->PATH));
+	arg->init(argv);
+	verbose := 0;
+	while((o := arg->opt()) != 0)
+		case o{
+		'd' =>
+			p = int arg->arg();
+	 	'v' =>
+			verbose = 1;
+		* =>
+			fatal(sys->sprint("bad option %c", o));
+		}
+	args := arg->argv();
+	arg = nil;
+	if(args != nil){
+		s := hd args;
+		fb = bufio->open(s, bufio->OREAD);
+		if(fb == nil)
+			fatal(sys->sprint("cannot open %s", s));
+	}
+	else{
+		fb = bufio->open("/dev/cons", bufio->OREAD);
+		if(fb == nil)
+			fatal(sys->sprint("missing data file name"));
+	}
+	a := array[p+1] of real;
+	b := array[p+1] of real;
+	sx := array[2*p+1] of real;
+	sxy := array[p+1] of real;
+	xd := array[MAXPTS] of real;
+	yd := array[MAXPTS] of real;
+	while(1){
+		xs := ss(bufio->fb.gett(" \t\r\n"));
+		if(xs == nil)
+			break;
+		ys := ss(bufio->fb.gett(" \t\r\n"));
+		if(ys == nil)
+			fatal(sys->sprint("missing value"));
+		if(n >= MAXPTS)
+			fatal(sys->sprint("too many points"));
+		xd[n] = real xs;
+		yd[n] = real ys;
+		n++;
+	}
+	if(p < 0)
+		fatal(sys->sprint("negative power"));
+	if(p > MAXDEG)
+		fatal(sys->sprint("power too large"));
+	if(n < p+1)
+		fatal(sys->sprint("not enough points"));
+	# use x-xbar, y-ybar to avoid overflow
+	for(i = 0; i <= p; i++)
+		sxy[i] = 0.0;
+	for(i = 0; i <= 2*p; i++)
+		sx[i] = 0.0;
+	xbar := ybar := 0.0;
+	for(i = 0; i < n; i++){
+		xbar += xd[i];
+		ybar += yd[i];
+	}
+	xbar = xbar/(real n);
+	ybar = ybar/(real n);
+	for(i = 0; i < n; i++){
+		x = xd[i]-xbar;
+		y = yd[i]-ybar;
+		for(j = 0; j <= p; j++)
+			sxy[j] += y*x**j;
+		for(j = 0; j <= 2*p; j++)
+			sx[j] += x**j;
+	}
+	mats := matsalloc(p+1);
+	mat := mats[p+1];
+	for(i = 0; i <= p; i++)
+		for(j = 0; j <= p; j++)
+			mat[i][j] = sx[i+j];
+	d := det(mat, p+1, mats);
+	if(isn(d, 0))
+		fatal(sys->sprint("points not independent"));
+	for(j = 0; j <= p; j++){
+		for(i = 0; i <= p; i++)
+			mat[i][j] = sxy[i];
+		a[j] = det(mat, p+1, mats)/d;
+		for(i = 0; i <= p; i++)
+			mat[i][j] = sx[i+j];
+	}
+	if(verbose)
+		sys->print("\npt	actual x	actual y	predicted y\n");
+	e := 0.0;
+	for(i = 0; i < n; i++){
+		x = xd[i]-xbar;
+		y = yd[i]-ybar;
+		z = 0.0;
+		for(j = 0; j <= p; j++)
+			z += a[j]*x**j;
+		z += ybar;
+		e += (z-yd[i])*(z-yd[i]);
+		if(verbose)
+			sys->print("%d.	%f	%f	%f\n", i+1, xd[i], yd[i], z);
+	}
+	if(verbose)
+		 sys->print("root mean squared error = %f\n", maths->sqrt(e/(real n)));
+	for(i = 0; i <= p; i++)
+		b[i] = 0.0;
+	b[0] += ybar;
+	for(i = 0; i <= p; i++)
+		for(j = 0; j <= i; j++)
+			b[j] += a[i]*comb(i, j)*(-xbar)**(i-j);
+	pr := 0;
+	sys->print("y = ");
+	for(i = p; i >= 0; i--){
+		if(!isn(b[i], 0) || (i == 0 && pr == 0)){
+			if(b[i] < 0.0){
+				sys->print("-");
+				b[i] = -b[i];
+			}
+			else if(pr)
+				sys->print("+");
+			pr = 1;
+			if(i == 0)
+				sys->print("%f", b[i]);
+			else{
+				if(!isn(b[i], 1))
+				 	sys->print("%f*", b[i]);
+				 sys->print("x");
+				if(i > 1)
+		   			sys->print("^%d", i);
+ 			}
+		}
+	}
+	sys->print("\n");
+}
+
+ss(s: string): string
+{
+	l := len s;
+	while(l > 0 && (s[0] == ' ' || s[0] == '\t' || s[0] == '\r' || s[0] == '\n')){
+		s = s[1: ];
+		l--;
+	}
+	while(l > 0 && (s[l-1] == ' ' || s[l-1] == '\t' || s[l-1] == '\r' || s[l-1] == '\n')){
+		s = s[0: l-1];
+		l--;
+	}
+	return s;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "fit: %s\n", s);
+	exit;
+}
--- /dev/null
+++ b/appl/math/genprimes.b
@@ -1,0 +1,64 @@
+implement Primes;
+
+include "draw.m";
+
+Primes: module
+{
+	init: fn(nil: ref Draw->Context, argl: list of string);
+};
+
+include "sys.m";
+	sys: Sys;
+include "arg.m";
+	arg: Arg;
+
+LIM: con 1729;
+MAX: con 1000000;
+BUFSZ: con 256;
+
+init(nil: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg = load Arg Arg->PATH;
+	arg->init(argl);
+	quiet := 0;
+	lim := LIM;
+	while((ch := arg->opt()) != 0){
+		case (ch){
+			'q' =>
+				quiet = 1;
+			* =>
+				;
+		}
+	}
+	argv := arg->argv();
+	if(argv != nil)
+		lim = int hd argv;
+	if(lim < 2)
+		lim = 2;
+	if(lim > MAX)
+		lim = MAX;
+	c := chan[BUFSZ] of int;
+	spawn prime(c, !quiet);
+	for(n := 2; n <= lim; n++)
+		c <-= n;
+	c <-= 1;
+}
+
+prime(c: chan of int, pr: int)
+{
+	p := <-c;
+	if(p == 1)
+		exit;
+	if(pr)
+		sys->print("%d\n", p);
+	nc := chan[BUFSZ] of int;
+	spawn prime(nc, pr);
+	for(;;){
+		n := <-c;
+		if(n%p)
+			nc <-= n;
+		if(n == 1)
+			exit;
+	}
+}
--- /dev/null
+++ b/appl/math/geodesy.b
@@ -1,0 +1,849 @@
+implement Geodesy;
+
+include "sys.m";
+	sys: Sys;
+include "math.m";
+	maths: Math;
+	Pi: import Math;
+	sin, cos, tan, asin, acos, atan, atan2, sqrt, fabs: import maths;
+include "math/geodesy.m";
+
+Approx: con 0;
+
+Epsilon: con 0.000001;
+Mperft: con 0.3048;
+Earthrad: con 10800.0/Pi*6076.115*Mperft;	# in feet (about 4000 miles) : now metres
+Δt: con 16.0;	# now-1989
+
+# lalo0: con "53:57:45N 01:04:55W";
+# os0: con "SE6022552235";
+
+# ellipsoids
+Airy1830, Airy1830m, Int1924, GRS80: con iota;
+
+Ngrid: con 100000;	# in metres
+
+Vector: adt{
+	x, y, z: real;
+};
+
+Latlong: adt{
+	la: real;	# -Pi to Pi
+	lo: real;	# -Pi to Pi
+	x: real;
+	y: real;
+};
+
+Ellipsoid: adt{
+	name: string;
+	a: real;
+	b: real;
+};
+
+Datum: adt{
+	name: string;
+	e: int;
+	# X, Y, Z axes etc
+};
+
+Mercator: adt{
+	name: string;
+	F0: real;
+	φ0λ0: string;
+	E0: real;
+	N0: real;
+	e: int;
+};
+
+Helmert: adt{
+	tx, ty, tz: real;	# metres
+	s: real;		# ppm
+	rx, ry, rz: real;	# secs
+};
+
+Format: adt{
+	dat: int;	# datum
+	cdat: int;	# converting datum
+	prj: int;		# projection
+	tmp: ref Mercator;	# actual projection
+	orig: Lalo;	# origin of above projection
+	zone: int;	# UTM zone
+};
+
+# ellipsoids
+ells := array[] of {
+		Airy1830 => Ellipsoid("Airy1830", 6377563.396, 6356256.910),
+		Airy1830m => Ellipsoid("Airy1830 modified", 6377340.189, 6356034.447),
+		Int1924 => Ellipsoid("International 1924", 6378388.000, 6356911.946),
+		GRS80 => Ellipsoid("GRS80", 6378137.000, 6356752.3141),
+	};
+
+# datums
+dats := array[] of {
+		OSGB36 => Datum("OSGB36", Airy1830),
+		Ireland65 => Datum("Ireland65", Airy1830m),
+		ED50 => Datum("ED50", Int1924),
+		WGS84 => Datum("WGS84", GRS80),
+		ITRS2000 => Datum("ITRS2000", GRS80),
+		ETRS89 => Datum("ETRS89", GRS80),
+	};
+
+# transverse Mercator projections
+tmps := array[] of {
+		Natgrid => Mercator("National Grid", 0.9996012717, "49:00:00N 02:00:00W", real(4*Ngrid), real(-Ngrid), Airy1830),
+		IrishNatgrid => Mercator("Irish National Grid", 1.000035, "53:30:00N 08:00:00W", real(2*Ngrid), real(5*Ngrid/2), Airy1830m),
+		UTMEur => Mercator("UTM Europe", 0.9996, nil, real(5*Ngrid), real(0), Int1924),
+		UTM => Mercator("UTM", 0.9996, nil, real(5*Ngrid), real(0), GRS80),
+	};
+
+# Helmert tranformations
+HT_WGS84_OSGB36: con Helmert(-446.448, 125.157, -542.060, 20.4894, -0.1502, -0.2470, -0.8421);
+HT_ITRS2000_ETRS89: con Helmert(0.054, 0.051, -0.048, 0.0, 0.000081*Δt, 0.00049*Δt, -0.000792*Δt);
+
+# Helmert matrices
+HM_WGS84_OSGB36, HM_OSGB36_WGS84, HM_ITRS2000_ETRS89, HM_ETRS89_ITRS2000, HM_ETRS89_OSGB36, HM_OSGB36_ETRS89, HM_IDENTITY: array of array of real;
+
+fmt: ref Format;
+
+# latlong: ref Latlong;
+
+init(d: int, t: int, z: int)
+{
+	sys = load Sys Sys->PATH;
+	maths = load Math Math->PATH;
+
+	helmertinit();
+	format(d, t, z);
+	# (nil, (la, lo)) := str2lalo(lalo0);
+	# (nil, (E, N)) := os2en(os0);
+	# latlong = ref Latlong(la, lo, real E, real N);
+}
+
+format(d: int, t: int, z: int)
+{
+	if(fmt == nil)
+		fmt = ref Format(WGS84, 0, Natgrid, nil, (0.0, 0.0), 30);
+	if(d >= 0 && d <= ETRS89)
+		fmt.dat = d;
+	if(t >= 0 && t <= UTM)
+		fmt.prj = t;
+	if(z >= 1 && z <= 60)
+		fmt.zone = z;
+	fmt.cdat = fmt.dat;
+	fmt.tmp = ref Mercator(tmps[fmt.prj]);
+	if(fmt.tmp.φ0λ0 == nil)
+		fmt.orig = utmlaloz(fmt.zone);
+	else
+		(nil, fmt.orig) = str2lalo(fmt.tmp.φ0λ0);
+	e := fmt.tmp.e;
+	if(e != dats[fmt.dat].e){
+		for(i := 0; i <= ETRS89; i++)
+			if(e == dats[i].e){
+				fmt.cdat = i;
+				break;
+			}
+	}
+}
+
+str2en(s: string): (int, Eano)
+{
+	s = trim(s, " \t\n\r");
+	if(s == nil)
+		return (0, (0.0, 0.0));
+	os := s[0] >= 'A' && s[0] <= 'Z' || strchrs(s, "NSEW:") < 0;
+	en: Eano;
+	if(os){
+		(ok, p) := os2en(s);
+		if(!ok)
+			return (0, (0.0, 0.0));	
+		en = p;
+	}
+	else{
+		(ok, lalo) := str2lalo(s);
+		if(!ok)
+			return (0, (0.0, 0.0));
+		en = lalo2en(lalo);
+	}
+	return (1, en);
+}
+
+str2ll(s: string, pos: int, neg: int): (int, real)
+{
+	(n, ls) := sys->tokenize(s, ": \t");
+	if(n < 1 || n > 3)
+		return (0, 0.0);
+	t := hd ls; ls = tl ls;
+	v := real t;
+	if(ls != nil){
+		t = hd ls; ls = tl ls;
+		v += (real t)/60.0;
+	}
+	if(ls != nil){
+		t = hd ls; ls = tl ls;
+		v += (real t)/3600.0;
+	}
+	c := t[len t-1];
+	if(c == pos)
+		;
+	else if(c == neg)
+		v = -v;
+	else
+		return (0, 0.0);
+	return (1, norm(deg2rad(v)));
+}
+
+str2lalo(s: string): (int, Lalo)
+{
+	s = trim(s, " \t\n\r");
+	p := strchr(s, 'N');
+	if(p < 0)
+		p = strchr(s, 'S');
+	if(p < 0)
+		return (0, (0.0, 0.0));
+	(ok1, la) := str2ll(s[0: p+1], 'N', 'S');
+	(ok2, lo) := str2ll(s[p+1: ], 'E', 'W');
+	if(!ok1 || !ok2 || la < -Pi/2.0 || la > Pi/2.0)
+		return (0, (0.0, 0.0));
+	return (1, (la, lo));
+}
+
+ll2str(ll: int, dir: string): string
+{
+	d := ll/360000;
+	ll -= 360000*d;
+	m := ll/6000;
+	ll -= 6000*m;
+	s := ll/100;
+	ll -= 100*s;
+	return d2(d) + ":" + d2(m) + ":" + d2(s) + "." + d2(ll) + dir;
+}
+
+lalo2str(lalo: Lalo): string
+{
+	la := int(360000.0*rad2deg(lalo.la));
+	lo := int(360000.0*rad2deg(lalo.lo));
+	lad := "N";
+	lod := "E";
+	if(la < 0){
+		lad = "S";
+		la = -la;
+	}
+	if(lo < 0){
+		lod = "W";
+		lo = -lo;
+	}
+	return ll2str(la, lad) + " " + ll2str(lo, lod);
+}
+
+en2os(p: Eano): string
+{
+	E := trunc(p.e);
+	N := trunc(p.n);
+	es := E/Ngrid;
+	ns := N/Ngrid;
+	e := E-Ngrid*es;
+	n := N-Ngrid*ns;
+	d1 := 5*(4-ns/5)+es/5+'A'-3;
+	d2 := 5*(4-ns%5)+es%5+'A';
+	# now account for 'I' missing
+	if(d1 >= 'I')
+		d1++;
+	if(d2 >= 'I')
+		d2++;
+	return sys->sprint("%c%c%5.5d%5.5d", d1, d2, e, n);
+}
+
+os2en(s: string): (int, Eano)
+{
+	s = trim(s, " \t\n\r");
+	if((m := len s) != 4 && m != 6 && m != 8 && m != 10 && m != 12)
+		return (0, (0.0, 0.0));
+	m = m/2-1;
+	u := Ngrid/10**m;
+	d1 := s[0];
+	d2 := s[1];
+	if(d1 < 'A' || d2 < 'A' || d1 > 'Z' || d2 > 'Z'){
+		# error(sys->sprint("bad os reference %s", s));
+		e := u*int s[0: 1+m];
+		n := u*int s[1+m: 2+2*m];
+		return (1, (real e, real n));
+	}
+	e := u*int s[2: 2+m];
+	n := u*int s[2+m: 2+2*m];
+	if(d1 >= 'I')
+		d1--;
+	if(d2 >= 'I')
+		d2--;
+	d1 -= 'A'-3;
+	d2 -= 'A';
+	es := 5*(d1%5)+d2%5;
+	ns := 5*(4-d1/5)+4-d2/5;
+	return (1, (real(Ngrid*es+e), real(Ngrid*ns+n)));
+}
+
+utmlalo(lalo: Lalo): Lalo
+{
+	(nil, zn) := utmzone(lalo);
+	return utmlaloz(zn);
+}
+
+utmlaloz(zn: int): Lalo
+{
+	return (0.0, deg2rad(real(6*zn-183)));
+}
+
+utmzone(lalo: Lalo): (int, int)
+{
+	(la, lo) := lalo;
+	la = rad2deg(la);
+	lo = rad2deg(lo);
+	zlo := trunc(lo+180.0)/6+1;
+	if(la < -80.0)
+		zla := 'B';
+	else if(la >= 84.0)
+		zla = 'Y';
+	else if(la >= 72.0)
+		zla = 'X';
+	else{
+		zla = trunc(la+80.0)/8+'C';
+		if(zla >= 'I')
+			zla++;
+		if(zla >= 'O')
+			zla++;
+	}
+	return (zla, zlo);
+}
+
+helmertinit()
+{
+	(HM_WGS84_OSGB36, HM_OSGB36_WGS84) = helminit(HT_WGS84_OSGB36);
+	(HM_ITRS2000_ETRS89, HM_ETRS89_ITRS2000) = helminit(HT_ITRS2000_ETRS89);
+	HM_ETRS89_OSGB36 = mulmm(HM_WGS84_OSGB36, HM_ETRS89_ITRS2000);
+	HM_OSGB36_ETRS89 = mulmm(HM_ITRS2000_ETRS89, HM_OSGB36_WGS84);
+	HM_IDENTITY = m := matrix(3, 4);
+	m[0][0] = m[1][1] = m[2][2] = 1.0;
+	# mprint(HM_WGS84_OSGB36);
+	# mprint(HM_OSGB36_WGS84);
+}
+
+helminit(h: Helmert): (array of array of real, array of array of real)
+{
+	m := matrix(3, 4);
+
+	s := 1.0+h.s/1000000.0;
+	rx := sec2rad(h.rx);
+	ry := sec2rad(h.ry);
+	rz := sec2rad(h.rz);
+
+	m[0][0] = s;
+	m[0][1] = -rz;
+	m[0][2] = ry;
+	m[0][3] = h.tx;
+	m[1][0] = rz;
+	m[1][1] = s;
+	m[1][2] = -rx;
+	m[1][3] = h.ty;
+	m[2][0] = -ry;
+	m[2][1] = rx;
+	m[2][2] = s;
+	m[2][3] = h.tz;
+
+	return (m, inv(m));
+}
+
+trans(f: int, t: int): array of array of real
+{
+	case(f){
+	WGS84 =>
+		case(t){
+		WGS84 =>
+			return HM_IDENTITY;
+		OSGB36 =>
+			return HM_WGS84_OSGB36;
+		ITRS2000 =>
+			return HM_IDENTITY;
+		ETRS89 =>
+			return HM_ITRS2000_ETRS89;
+		}
+	OSGB36 =>
+		case(t){
+		WGS84 =>
+			return HM_OSGB36_WGS84;
+		OSGB36 =>
+			return HM_IDENTITY;
+		ITRS2000 =>
+			return HM_OSGB36_WGS84;
+		ETRS89 =>
+			return HM_OSGB36_ETRS89;
+		}
+	ITRS2000 =>
+		case(t){
+		WGS84 =>
+			return HM_IDENTITY;
+		OSGB36 =>
+			return HM_WGS84_OSGB36;
+		ITRS2000 =>
+			return HM_IDENTITY;
+		ETRS89 =>
+			return HM_ITRS2000_ETRS89;
+		}
+	ETRS89 =>
+		case(t){
+		WGS84 =>
+			return HM_ETRS89_ITRS2000;
+		OSGB36 =>
+			return HM_ETRS89_OSGB36;
+		ITRS2000 =>
+			return HM_ETRS89_ITRS2000;
+		ETRS89 =>
+			return HM_IDENTITY;
+		}
+	}
+	return HM_IDENTITY;	# Ireland65, ED50 not done
+}
+
+datum2datum(lalo: Lalo, f: int, t: int): Lalo
+{
+	if(f == t)
+		return lalo;
+	(la, lo) := lalo;
+	v := laloh2xyz(la, lo, 0.0, dats[f].e);
+	v = mulmv(trans(f, t), v);
+	(la, lo, nil) = xyz2laloh(v, dats[t].e);
+	return (la, lo);
+}
+
+laloh2xyz(φ: real, λ: real, H: real, e: int): Vector
+{
+	a := ells[e].a;
+	b := ells[e].b;
+	e2 := 1.0-(b/a)**2;
+
+	s := sin(φ);
+	c := cos(φ);
+
+	ν := a/sqrt(1.0-e2*s*s);
+	x := (ν+H)*c*cos(λ);
+	y := (ν+H)*c*sin(λ);
+	z := ((1.0-e2)*ν+H)*s;
+
+	return (x, y, z);
+}
+
+xyz2laloh(v: Vector, e: int): (real, real, real)
+{
+	x := v.x;
+	y := v.y;
+	z := v.z;
+
+	a := ells[e].a;
+	b := ells[e].b;
+	e2 := 1.0-(b/a)**2;
+
+	λ := atan2(y, x);
+
+	p := sqrt(x*x+y*y);
+	φ := φ1 := atan(z/(p*(1.0-e2)));
+	ν := 0.0;
+	do{
+		φ = φ1;
+		s := sin(φ);
+		ν = a/sqrt(1.0-e2*s*s);
+		φ1 = atan((z+e2*ν*s)/p);
+	}while(!small(fabs(φ-φ1)));
+
+	φ = φ1;
+	H := p/cos(φ)-ν;
+
+	return (φ, λ, H);
+}
+
+lalo2en(lalo: Lalo): Eano
+{
+	(φ, λ) := lalo;
+	if(fmt.cdat != fmt.dat)
+		(φ, λ) = datum2datum(lalo, fmt.dat, fmt.cdat);
+
+	s := sin(φ);
+	c := cos(φ);
+	t2 := tan(φ)**2;
+
+	(nil, F0, φ0λ0, E0, N0, e) := *fmt.tmp;
+	a := ells[e].a;
+	b := ells[e].b;
+	e2 := 1.0-(b/a)**2;
+
+	if(φ0λ0 == nil)	# UTM
+		(φ0, λ0) := utmlalo((φ, λ));	# don't use fmt.zone here
+	else
+		(φ0, λ0) = fmt.orig;
+
+	n := (a-b)/(a+b);
+	ν := a*F0/sqrt(1.0-e2*s*s);
+	ρ := ν*(1.0-e2)/(1.0-e2*s*s);
+	η2 := ν/ρ-1.0;
+
+	φ1 := φ-φ0;
+	φ2 := φ+φ0;
+	M := b*F0*((1.0+n*(1.0+1.25*n*(1.0+n)))*φ1 - (3.0*n*(1.0+n*(1.0+0.875*n)))*sin(φ1)*cos(φ2) + 1.875*n*n*(1.0+n)*sin(2.0*φ1)*cos(2.0*φ2) - 35.0/24.0*n**3*sin(3.0*φ1)*cos(3.0*φ2));
+
+	I := M+N0;
+	II := ν*s*c/2.0;
+	III := ν*s*c**3*(5.0-t2+9.0*η2)/24.0;
+	IIIA := ν*s*c**5*(61.0+t2*(t2-58.0))/720.0;
+	IV := ν*c;
+	V := ν*c**3*(ν/ρ-t2)/6.0;
+	VI := ν*c**5*(5.0+14.0*η2+t2*(t2-18.0-58.0*η2))/120.0;
+
+	λ -= λ0;
+	λ2 := λ*λ;
+	N := I+λ2*(II+λ2*(III+IIIA*λ2));
+	E := E0+λ*(IV+λ2*(V+VI*λ2));
+
+	# if(E < 0.0 || E >= real(7*Ngrid))
+	# 	E = 0.0;
+	# if(N < 0.0 || N >= real(13*Ngrid))
+	# 	N = 0.0;
+	return (E, N);
+}
+
+en2lalo(en: Eano): Lalo
+{
+	E := en.e;
+	N := en.n;
+
+	(nil, F0, nil, E0, N0, e) := *fmt.tmp;
+	a := ells[e].a;
+	b := ells[e].b;
+	e2 := 1.0-(b/a)**2;
+
+	(φ0, λ0) := fmt.orig;
+
+	n := (a-b)/(a+b);
+
+	M0 := 1.0+n*(1.0+1.25*n*(1.0+n));
+	M1 := 3.0*n*(1.0+n*(1.0+0.875*n));
+	M2 := 1.875*n*n*(1.0+n);
+	M3 := 35.0/24.0*n**3;
+
+	N -= N0;
+	M := 0.0;
+	φ := φold := φ0;
+	do{
+		φ = (N-M)/(a*F0)+φold;
+		φ1 := φ-φ0;
+		φ2 := φ+φ0;
+		M = b*F0*(M0*φ1 - M1*sin(φ1)*cos(φ2) + M2*sin(2.0*φ1)*cos(2.0*φ2) - M3*sin(3.0*φ1)*cos(3.0*φ2));
+		φold = φ;
+	}while(fabs(N-M) >= 0.01);
+
+	s := sin(φ);
+	c := cos(φ);
+	t := tan(φ);
+	t2 := t*t;
+
+	ν := a*F0/sqrt(1.0-e2*s*s);
+	ρ := ν*(1.0-e2)/(1.0-e2*s*s);
+	η2 := ν/ρ-1.0;
+
+	VII := t/(2.0*ρ*ν);
+	VIII := VII*(5.0+η2+3.0*t2*(1.0-3.0*η2))/(12.0*ν*ν);
+	IX := VII*(61.0+45.0*t2*(2.0+t2))/(360.0*ν**4);
+	X := 1.0/(ν*c);
+	XI := X*(ν/ρ+2.0*t2)/(6.0*ν*ν);
+	XII := X*(5.0+4.0*t2*(7.0+6.0*t2))/(120.0*ν**4);
+	XIIA := X*(61.0+2.0*t2*(331.0+60.0*t2*(11.0+6.0*t2)))/(5040.0*ν**6);
+
+	E -= E0;
+	E2 := E*E;
+	φ = φ-E2*(VII-E2*(VIII-E2*IX));
+	λ := λ0+E*(X-E2*(XI-E2*(XII-E2*XIIA)));
+
+	if(fmt.cdat != fmt.dat)
+		(φ, λ) = datum2datum((φ, λ), fmt.cdat, fmt.dat);
+	return (φ, λ);
+}
+
+mulmm(m1: array of array of real, m2: array of array of real): array of array of real
+{
+	m := matrix(3, 4);
+	mul3x3(m, m1, m2);
+	for(i := 0; i < 3; i++){
+		sum := 0.0;
+		for(k := 0; k < 3; k++)
+			sum += m1[i][k]*m2[k][3];
+		m[i][3] = sum+m1[i][3];
+	}
+	return m;
+}
+
+mulmv(m: array of array of real, v: Vector): Vector
+{
+	x := v.x;
+	y := v.y;
+	z := v.z;
+	v.x = m[0][0]*x + m[0][1]*y + m[0][2]*z + m[0][3];
+	v.y = m[1][0]*x + m[1][1]*y + m[1][2]*z + m[1][3];
+	v.z = m[2][0]*x + m[2][1]*y + m[2][2]*z + m[2][3];
+	return v;
+}
+
+inv(m: array of array of real): array of array of real
+{
+	n := matrix(3, 4);
+	inv3x3(m, n);
+	(n[0][3], n[1][3], n[2][3]) = mulmv(n, (-m[0][3], -m[1][3], -m[2][3]));
+	return n;
+}
+
+mul3x3(m: array of array of real, m1: array of array of real, m2: array of array of real)
+{
+	for(i := 0; i < 3; i++){
+		for(j := 0; j < 3; j++){
+			sum := 0.0;
+			for(k := 0; k < 3; k++)
+				sum += m1[i][k]*m2[k][j];
+			m[i][j] = sum;
+		}
+	}
+}
+
+inv3x3(m: array of array of real, n: array of array of real)
+{
+	t00 := m[0][0];
+	t01 := m[0][1];
+	t02 := m[0][2];
+	t10 := m[1][0];
+	t11 := m[1][1];
+	t12 := m[1][2];
+	t20 := m[2][0];
+	t21 := m[2][1];
+	t22 := m[2][2];
+
+	n[0][0] = t11*t22-t12*t21;
+	n[1][0] = t12*t20-t10*t22;
+	n[2][0] = t10*t21-t11*t20;
+	n[0][1] = t02*t21-t01*t22;
+	n[1][1] = t00*t22-t02*t20;
+	n[2][1] = t01*t20-t00*t21;
+	n[0][2] = t01*t12-t02*t11;
+	n[1][2] = t02*t10-t00*t12;
+	n[2][2] = t00*t11-t01*t10;
+
+	d := t00*n[0][0]+t01*n[1][0]+t02*n[2][0];
+	for(i := 0; i < 3; i++)
+		for(j := 0; j < 3; j++)
+			n[i][j] /= d;
+}
+
+matrix(rows: int, cols: int): array of array of real
+{
+	m := array[rows] of array of real;
+	for(i := 0; i < rows; i++)
+		m[i] = array[cols] of { * => 0.0 };
+	return m;
+}
+
+vprint(v: Vector)
+{
+	sys->print("	%f	%f	%f\n", v.x, v.y, v.z);
+}
+
+mprint(m: array of array of real)
+{
+	for(i := 0; i < len m; i++){
+		for(j := 0; j < len m[i]; j++)
+			sys->print("	%f", m[i][j]);
+		sys->print("\n");
+	}
+}
+
+# lalo2xy(la: real, lo: real, lalo: ref Latlong): Eano
+# {
+# 	x, y: real;
+# 
+# 	la0 := lalo.la;
+# 	lo0 := lalo.lo;
+# 	if(Approx){
+# 		x = Earthrad*cos(la0)*(lo-lo0)+lalo.x;
+# 		y = Earthrad*(la-la0)+lalo.y;
+# 	}
+# 	else{
+# 		x = Earthrad*cos(la)*sin(lo-lo0)+lalo.x;
+# 		y = Earthrad*(sin(la)*cos(la0)-sin(la0)*cos(la)*cos(lo-lo0))+lalo.y;
+# 	}
+# 	return (x, y);
+# }
+
+# lalo2xyz(la: real, lo: real, lalo: ref Latlong): (int, int, int)
+# {
+# 	z: real;
+# 
+# 	la0 := lalo.la;
+#     	lo0 := lalo.lo;
+# 	(x, y) := lalo2xy(la, lo, lalo);
+# 	if(Approx)
+# 		z = Earthrad;
+# 	else
+# 		z = Earthrad*(sin(la)*sin(la0)+cos(la)*cos(la0)*cos(lo-lo0));
+# 	return (x, y, int z);
+# }
+
+# xy2lalo(p: Eano, lalo: ref Latlong): (real, real)
+# {
+# 	la, lo: real;
+# 
+# 	x := p.e;
+# 	y := p.n;
+# 	la0 := lalo.la;
+# 	lo0 := lalo.lo;
+# 	if(Approx){
+# 		la = la0 + (y-lalo.y)/Earthrad;
+# 		lo = lo0 + (x-lalo.x)/(Earthrad*cos(la0));
+# 	}
+# 	else{
+# 		a, b, c, d, bestd, r, r1, r2, lat, lon, tmp: real;
+# 		i, n: int;
+# 
+# 		bestd = -1.0;
+# 		la = lo = 0.0;
+# 		a = (x-lalo.x)/Earthrad;
+# 		b = (y-lalo.y)/Earthrad;
+# 		(n, r1, r2) = quad(1.0, -2.0*b*cos(la0), (a*a-1.0)*sin(la0)*sin(la0)+b*b);
+# 		if(n == 0)
+# 			return (la, lo);
+# 		while(--n >= 0){
+# 			if(n == 1)
+# 				r = r2;
+# 			else
+# 				r = r1;
+# 			if(fabs(r) <= 1.0){
+# 				lat = asin(r);
+# 				c = cos(lat);
+# 				if(small(c))
+# 					tmp = 0.0;	# lat = +90, -90, lon = lo0
+# 				else
+# 					tmp = a/c;
+# 				if(fabs(tmp) <= 1.0){
+# 					for(i = 0; i < 2; i++){
+# 						if(i == 0)
+# 							lon = norm(asin(tmp)+lo0);
+# 						else
+# 							lon = norm(Pi-asin(tmp)+lo0);
+# 						(X, Y, Z) := lalo2xyz(lat, lon, lalo);
+# 						# eliminate non-roots by d, root on other side of earth by Z
+# 						d = (real X-x)**2+(real Y-y)**2;
+# 						if(Z >= 0 && (bestd < 0.0 || d < bestd)){
+# 							bestd = d;
+# 							la = lat;
+# 							lo = lon;
+# 						}
+# 					}
+# 				}
+# 			}
+# 		}
+# 	}
+# 	return (la, lo);
+# }
+
+# quad(a: real, b: real, c: real): (int, real, real)
+# {
+# 	r1, r2: real;
+# 
+# 	D := b*b-4.0*a*c;
+# 	if(small(a)){
+# 		if(small(b))
+# 			return (0, r1, r2);
+# 		r1 = r2 = -c/b;
+# 		return (1, r1, r2);
+# 	}
+# 	if(D < 0.0)
+# 		return (0, r1, r2);
+# 	D = sqrt(D);
+# 	r1 = (-b+D)/(2.0*a);
+# 	r2 = (-b-D)/(2.0*a);
+# 	if(small(D))
+# 		return (1, r1, r2);
+# 	else
+# 		return (2, r1, r2);
+# }
+
+d2(v: int): string
+{
+	s := string v;
+	if(v < 10)
+		s = "0" + s;
+	return s;
+}
+
+trim(s: string, t: string): string
+{
+	while(s != nil && strchr(t, s[0]) >= 0)
+		s = s[1: ];
+	while(s != nil && strchr(t, s[len s-1]) >= 0)
+		s = s[0: len s-1];
+	return s;
+}
+
+strchrs(s: string, t: string): int
+{
+	for(i := 0; i < len t; i++){
+		p := strchr(s, t[i]);
+		if(p >= 0)
+			return p;
+	}
+	return -1;
+}
+
+strchr(s: string, c: int): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return i;
+	return -1;
+}
+
+deg2rad(d: real): real
+{
+	return d*Pi/180.0;
+}
+
+rad2deg(r: real): real
+{
+	return r*180.0/Pi;
+}
+
+sec2rad(s: real): real
+{
+	return deg2rad(s/3600.0);
+}
+
+norm(r: real): real
+{
+	while(r > Pi)
+		r -= 2.0*Pi;
+	while(r < -Pi)
+		r += 2.0*Pi;
+	return r;
+}
+
+small(r: real): int
+{
+	return r > -Epsilon && r < Epsilon;
+}
+
+trunc(r: real): int
+{
+	# down : assumes r >= 0
+	i := int r;
+	if(real i > r)
+		i--;
+	return i;
+}
+
+abs(x: int): int
+{
+	if(x < 0)
+		return -x;
+	return x;
+}
--- /dev/null
+++ b/appl/math/gr.b
@@ -1,0 +1,557 @@
+implement GR;
+
+include "sys.m";
+	sys: Sys;
+	print, sprint: import sys;
+include "math.m";
+	math: Math;
+	ceil, fabs, floor, Infinity, log10, pow10, sqrt: import math;
+include "draw.m";
+	screen: ref Draw->Screen;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "gr.m";
+
+gr_cfg := array[] of {
+	"frame .fc",
+	"frame .fc.b",
+	"label .fc.b.xy -text {0 0} -anchor e",
+	"pack .fc.b.xy -fill x",
+	"pack .fc.b -fill both -expand 1",
+	"canvas .fc.c -relief sunken -bd 2 -width 600 -height 480 -bg white"+
+		" -font /fonts/lucidasans/unicode.8.font",
+	"pack .fc.c -fill both -expand 1",
+	"pack .Wm_t -fill x",
+	"pack .fc -fill both -expand 1",
+	"pack propagate . 0",
+	"bind .fc.c <ButtonPress-1> {send grcmd down1,%x,%y}",
+};
+
+TkCmd(t: ref Toplevel, arg: string): string
+{
+	rv := tk->cmd(t,arg);
+	if(rv!=nil && rv[0]=='!')
+		print("tk->cmd(%s): %s\n",arg,rv);
+	return rv;
+}
+
+
+open(ctxt: ref Draw->Context, title: string): ref Plot
+{
+	if(sys==nil){
+		sys = load Sys Sys->PATH;
+		math = load Math Math->PATH;
+		tk = load Tk Tk->PATH;
+		tkclient = load Tkclient Tkclient->PATH;
+		tkclient->init();
+	}
+	textsize := 8.;	# textsize is in points, if no user transform
+	(t, tb) := tkclient->toplevel(ctxt, "", title, Tkclient->Appl);
+	cc := chan of string;
+	tk->namechan(t, cc, "grcmd");
+	p := ref Plot(nil, Infinity,-Infinity,Infinity,-Infinity, textsize, t, tb, cc);
+	for (i:=0; i<len gr_cfg; i++)
+		tk->cmd(p.t,gr_cfg[i]);
+	tkclient->onscreen(p.t, nil);
+	tkclient->startinput(p.t, "kbd"::"ptr"::nil);
+	return p;
+}
+
+Plot.bye(p: self ref Plot)
+{
+	cmdloop: for(;;) alt {
+	s := <-p.t.ctxt.kbd =>
+		tk->keyboard(p.t, s);
+	s := <-p.t.ctxt.ptr =>
+		tk->pointer(p.t, *s);
+	s := <-p.t.ctxt.ctl or
+	s = <-p.t.wreq or
+	s = <-p.titlechan =>
+		if(s == "exit")
+			break cmdloop;
+		tkclient->wmctl(p.t, s);
+		case s{
+		"size" =>
+			canvw := int TkCmd(p.t, ".fc.c cget -width");
+			canvh := int TkCmd(p.t, ".fc.c cget -height");
+			TkCmd(p.t,".fc.b.xy configure -text {"+sprint("%d %d",canvw,canvh)+"}");
+		}
+	press := <-p.canvaschan =>
+		(nil,cmds) := sys->tokenize(press,",");
+		if(cmds==nil) continue;
+		case hd cmds {
+		"down1" =>
+			xpos := real(hd tl cmds);
+			ypos := real(hd tl tl cmds);
+			x := (xpos-bx)/ax;
+			y := -(ypos-tky+by)/ay;
+			TkCmd(p.t,".fc.b.xy configure -text {"+sprint("%.3g %.3g",x,y)+"}");
+		}
+	}
+	TkCmd(p.t,"destroy .;update");
+	p.t = nil;
+}
+
+Plot.equalxy(p: self ref Plot)
+{
+	r := 0.;
+	if( r < p.xmax - p.xmin ) r = p.xmax - p.xmin;
+	if( r < p.ymax - p.ymin ) r = p.ymax - p.ymin;
+	m := (p.xmax + p.xmin)/2.;
+	p.xmax = m + r/2.;
+	p.xmin = m - r/2.;
+	m = (p.ymax + p.ymin)/2.;
+	p.ymax = m + r/2.;
+	p.ymin = m - r/2.;
+}
+
+Plot.graph(p: self ref Plot, x, y: array of real)
+{
+	n := len x;
+	op := OP(GR->GRAPH, n, array[n] of real, array[n] of real, nil);
+	while(n--){
+		t := x[n];
+		op.x[n] = t;
+		if(t < p.xmin) 
+			p.xmin = t;
+		if(t > p.xmax) 
+			p.xmax = t;
+		t = y[n];
+		op.y[n] = t;
+		if(t < p.ymin) 
+			p.ymin = t;
+		if(t > p.ymax) 
+			p.ymax = t;
+	}
+	p.op = op :: p.op;
+}
+
+Plot.text(p: self ref Plot, justify: int, s: string, x, y: real)
+{
+	op := OP(GR->TEXT, justify, array[1] of real, array[1] of real, s);
+	op.x[0] = x;
+	op.y[0] = y;
+	p.op = op :: p.op;
+}
+
+Plot.pen(p: self ref Plot, nib: int)
+{
+	p.op = OP(GR->PEN, nib, nil, nil, nil) :: p.op;
+}
+
+
+#---------------------------------------------------------
+# The rest of this file is concerned with sending the "display list"
+# to Tk.  The only interesting parts of the problem are picking axes
+# and drawing dashed lines properly.
+
+ax, bx, ay, by: real;			# transform user to pixels
+tky: con 630.;				# Tk_y = tky - y
+nseg: int;				# how many segments in current stroke path
+pendown: int;				# is pen currently drawing?
+xoff := array[] of{"w","","e"};	# LJUST, CENTER, RJUST
+yoff := array[] of{"n","","s","s"};	# HIGH, MED, BASE, LOW
+linewidth: real;
+toplevel: ref Toplevel;			# p.t
+tkcmd: string;
+
+mv(x, y: real)
+{
+	tkcmd = sprint(".fc.c create line %.1f %.1f", ax*x+bx, tky-(ay*y+by));
+}
+
+stroke()
+{
+	if(pendown){
+		tkcmd += " -width 3";   # -capstyle round -joinstyle round
+		TkCmd(toplevel,tkcmd);
+		tkcmd = nil;
+		pendown = 0;
+		nseg = 0;
+	}
+}
+
+vec(x, y: real)
+{
+	tkcmd += sprint(" %.1f %.1f", ax*x+bx, tky-(ay*y+by));
+	pendown = 1;
+	nseg++;
+	if(nseg>1000){
+		stroke();
+		mv(x,y);
+	}
+}
+
+circle(u, v, radius: real)
+{
+	x := ax*u+bx;
+	y := tky-(ay*v+by);
+	r := radius*(ax+ay)/2.;
+	tkcmd = sprint(".fc.c create oval %.1f %.1f %.1f %.1f -width 3",
+		x-r, y-r, x+r, y+r);
+	TkCmd(toplevel,tkcmd);
+	tkcmd = nil;
+}
+
+text(s: string, x, y: real, xoff, yoff: string)
+{
+	# rot = rotation in degrees.  90 is used for y-axis
+	# x,y are in PostScript coordinate system, not user
+	anchor := yoff + xoff;
+	if(anchor!="")
+		anchor = "-anchor " + anchor + " ";
+	tkcmd = sprint(".fc.c create text %.1f %.1f %s-text '%s",
+		ax*x+bx,
+		tky-(ay*y+by), anchor, s);
+	TkCmd(toplevel,tkcmd);
+	tkcmd = nil;
+}
+
+datarange(xmin, xmax, margin: real): (real,real)
+{
+	r := 1.e-30;
+	if( r < 0.001*fabs(xmin) ) 
+		r = 0.001*fabs(xmin);
+	if( r < 0.001*fabs(xmax) ) 
+		r = 0.001*fabs(xmax);
+	if( r < xmax-xmin ) 
+		r = xmax-xmin;
+	r *= 1.+2.*margin;
+	x0 :=(xmin+xmax)/2. - r/2.;
+	return ( x0, x0 + r);
+}
+
+dashed(ndash: int, x, y: array of real)
+{
+	cx, cy: real;	# current position
+	d: real;	# length undone in p[i],p[i+1]
+	t: real;	# length undone in current dash
+	n := len x;
+	if(n!=len y || n<=0)
+		return;
+
+	# choose precise dashlen
+	s := 0.;
+	for(i := 0; i < n - 1; i += 1){
+		u := x[i+1] - x[i];
+		v := y[i+1] - y[i];
+		s += sqrt(u*u + v*v);
+	}
+	i = int floor(real ndash * s);
+	if(i < 2) 
+		i = 2;
+	dashlen := s / real(2 * i - 1);
+
+	t = dashlen;
+	ink := 1;
+	mv(x[0], y[0]);
+	cx = x[0];
+	cy = y[0];
+	for(i = 0; i < n - 1; i += 1){
+		u := x[i+1] - x[i];
+		v := y[i+1] - y[i];
+		d = sqrt(u * u + v * v);
+		if(d > 0.){
+			u /= d;
+			v /= d;
+			while(t <= d){
+				cx += t * u;
+				cy += t * v;
+				if(ink){
+					vec(cx, cy);
+					stroke();
+				}else{
+					mv(cx, cy);
+				}
+				d -= t;
+				t = dashlen;
+				ink = 1 - ink;
+			}
+			cx = x[i+1];
+			cy = y[i+1];
+			if(ink){
+				vec(cx, cy);
+			}else{
+				mv(cx, cy);
+			}
+			t -= d;
+		}
+	}
+	stroke();
+}
+
+labfmt(x:real): string
+{
+	lab := sprint("%.6g",x);
+	if(len lab>2){
+		if(lab[0]=='0' && lab[1]=='.')
+			lab = lab[1:];
+		else if(lab[0]=='-' && len lab>3 && lab[1]=='0' && lab[2]=='.')
+			lab = "-"+lab[2:];
+	}
+	return lab;
+}
+
+Plot.paint(p: self ref Plot, xlabel, xunit, ylabel, yunit: string)
+{
+	oplist: list of OP;
+
+	# tunable parameters for dimensions of graph (fraction of box side)
+	margin: con 0.075;		# separation of data from box boundary
+	ticksize := 0.02;
+	sep := ticksize;		# separation of text from box boundary
+
+	# derived coordinates of various feature points...
+	x0, x1, y0, y1: real;		# box corners, in original coord
+	# radius := 0.2*p.textsize;	# radius for circle marker
+	radius := 0.8*p.textsize;	# radius for circle marker
+
+	Pen := SOLID;
+	width := SOLID;
+	linewidth = 2.;
+	nseg = 0;
+	pendown = 0;
+
+	if(xunit=="") xunit = nil;
+	if(yunit=="") yunit = nil;
+
+	(x0,x1) = datarange(p.xmin,p.xmax,margin);
+	ax = (400.-2.*p.textsize)/((x1-x0)*(1.+2.*sep));
+	bx = 506.-ax*x1;
+	(y0,y1) = datarange(p.ymin,p.ymax,margin);
+	ay = (400.-2.*p.textsize)/((y1-y0)*(1.+2.*sep));
+	by = 596.-ay*y1;
+	# PostScript version
+	# magic numbers here come from BoundingBox: 106 196 506 596
+	# (x0,x1) = datarange(p.xmin,p.xmax,margin);
+	# ax = (400.-2.*p.textsize)/((x1-x0)*(1.+2.*sep));
+	# bx = 506.-ax*x1;
+	# (y0,y1) = datarange(p.ymin,p.ymax,margin);
+	# ay = (400.-2.*p.textsize)/((y1-y0)*(1.+2.*sep));
+	# by = 596.-ay*y1;
+
+	# convert from fraction of box to PostScript units
+	ticksize *= ax*(x1-x0);
+	sep *= ax*(x1-x0);
+
+	# revert to original drawing order
+	log := p.op;
+	oplist = nil;
+	while(log!=nil){
+		oplist = hd log :: oplist;
+		log = tl log;
+	}
+	p.op = oplist;
+
+	toplevel = p.t;
+	#------------send display list to Tk-----------------
+	while(oplist!=nil){
+		op := hd oplist;
+		n := op.n;
+		case op.code{
+		GRAPH =>
+			if(Pen == DASHED){
+				dashed(17, op.x, op.y);
+			}else if(Pen == DOTTED){
+				dashed(85, op.x, op.y);
+			}else{
+				for(i:=0; i<n; i++){
+					xx := op.x[i];
+					yy := op.y[i];
+					if(Pen == CIRCLE){
+						circle(xx, yy, radius/(ax+ay));
+					}else if(Pen == CROSS){
+						mv(xx-radius/ax, yy);
+						vec(xx+radius/ax, yy);
+						stroke();
+						mv(xx, yy-radius/ay);
+						vec(xx, yy+radius/ay);
+						stroke();
+					}else if(Pen == INVIS){
+					}else{
+						if(i==0){
+							mv(xx, yy);
+						}else{
+							vec(xx, yy);
+						}
+					}
+				}
+				stroke();
+			}
+		TEXT =>
+			angle := 0.;
+			if(op.n&UP) angle = 90.;
+			text(op.t,op.x[0],op.y[0],xoff[n&7],yoff[(n>>3)&7]);
+		PEN =>
+			Pen = n;
+			if( Pen==SOLID && width!=SOLID ){
+				linewidth = 2.;
+				width=SOLID;
+			}else if( Pen==REFERENCE && width!=REFERENCE ){
+				linewidth = 0.8;
+				width=REFERENCE;
+			}
+		}
+		oplist = tl oplist;
+	}
+
+	#--------------------now add axes-----------------------
+	mv(x0,y0);
+	vec(x1,y0);
+	vec(x1,y1);
+	vec(x0,y1);
+	vec(x0,y0);
+	stroke();
+
+	# x ticks
+	(lab1,labn,labinc,k,u,s) := mytic(x0,x1);
+	for (i := lab1; i <= labn; i += labinc){
+		r := real i*s*u;
+		mv(r,y0);
+		vec(r,y0+ticksize/ay);
+		stroke();
+		mv(r,y1);
+		vec(r,y1-ticksize/ay);
+		stroke();
+		text(labfmt(real i*s),r,y0-sep/ay,"","n");
+	}
+	yy := y0-(2.*sep+p.textsize)/ay;
+	labelstr := "";
+	if(xlabel!=nil)
+		labelstr = xlabel;
+	if(k!=0||xunit!=nil)
+		labelstr += " /";
+	if(k!=0)
+		labelstr += " ₁₀"+ string k;
+	if(xunit!=nil)
+		labelstr += " " + xunit;
+	text(labelstr,(x0+x1)/2.,yy,"","n");
+
+	# y ticks
+	(lab1,labn,labinc,k,u,s) = mytic(y0,y1);
+	for (i = lab1; i <= labn; i += labinc){
+		r := real i*s*u;
+		mv(x0,r);
+		vec(x0+ticksize/ax,r);
+		stroke();
+		mv(x1,r);
+		vec(x1-ticksize/ax,r);
+		stroke();
+		text(labfmt(real i*s),x0-sep/ax,r,"e","");
+	}
+	xx := x0-(4.*sep+p.textsize)/ax;
+	labelstr = "";
+	if(ylabel!=nil)
+		labelstr = ylabel;
+	if(k!=0||yunit!=nil)
+		labelstr += " /";
+	if(k!=0)
+		labelstr += " ₁₀"+ string k;
+	if(yunit!=nil)
+		labelstr += " " + yunit;
+	text(labelstr,xx,(y0+y1)/2.,"e","");
+
+	TkCmd(p.t, "update");
+}
+
+
+
+# automatic tic choice                      Eric Grosse  9 Dec 84
+# Input: low and high endpoints of expanded data range
+# Output: lab1, labn, labinc, k, u, s   where the tics are
+#   (lab1*s, (lab1+labinc)*s, ..., labn*s) * 10^k
+# and u = 10^k.  k is metric, i.e. k=0 mod 3.
+
+max3(a, b, c: real): real
+{
+	if(a<b) a=b;
+	if(a<c) a=c;
+	return(a);
+}
+
+my_mod(i, n: int): int
+{
+	while(i< 0) i+=n;
+	while(i>=n) i-=n;
+	return(i);
+}
+
+mytic(l, h: real): (int,int,int,int,real,real)
+{
+	lab1, labn, labinc, k, nlab, j, ndig, t1, tn: int;
+	u, s: real;
+	eps := .0001;
+	k = int floor( log10((h-l)/(3.+eps)) );
+	u = pow10(k);
+	t1 = int ceil(l/u-eps);
+	tn = int floor(h/u+eps);
+	lab1 = t1;
+	labn = tn;
+	labinc = 1;
+	nlab = labn - lab1 + 1;
+	if( nlab>5 ){
+		lab1 = t1 + my_mod(-t1,2);
+		labn = tn - my_mod( tn,2);
+		labinc = 2;
+		nlab = (labn-lab1)/labinc + 1;
+		if( nlab>5 ){
+			lab1 = t1 + my_mod(-t1,5);
+			labn = tn - my_mod( tn,5);
+			labinc = 5;
+			nlab = (labn-lab1)/labinc + 1;
+			if( nlab>5 ){
+				u *= 10.; 
+				k++;
+				lab1 = int ceil(l/u-eps);
+				labn = int floor(h/u+eps);
+				nlab = labn - lab1 + 1;
+				labinc = 1;
+			} else if( nlab<3 ){
+				lab1 = t1 + my_mod(-t1,4);
+				labn = tn - my_mod( tn,4);
+				labinc = 4;
+				nlab = (labn-lab1)/labinc + 1;
+			}
+		}
+	}
+	ndig = int(1.+floor(log10(max3(fabs(real lab1),fabs(real labn),1.e-30))));
+	if( ((k<=0)&&(k>=-ndig))   # no zeros have to be added
+	    || ((k<0)&&(k>=-3))
+	    || ((k>0)&&(ndig+k<=4)) ){   # even with zeros, label is small
+		s = u;
+		k = 0;
+		u = 1.;
+	}else if(k>0){
+		s = 1.;
+		j = ndig;
+		while(k%3!=0){ 
+			k--; 
+			u/=10.; 
+			s*=10.; 
+			j++; 
+		}
+		if(j-3>0){ 
+			k+=3; 
+			u*=1000.; 
+			s/=1000.; 
+		}
+	}else{ # k<0
+		s = 1.;
+		j = ndig;
+		while(k%3!=0){ 
+			k++; 
+			u*=10.; 
+			s/=10.; 
+			j--; 
+		}
+		if(j<0){ 
+			k-=3; 
+			u/=1000.; 
+			s*=1000.; 
+		}
+	}
+	return (lab1, labn, labinc, k, u, s);
+}
--- /dev/null
+++ b/appl/math/graph0.b
@@ -1,0 +1,84 @@
+implement Graph0;
+
+include "sys.m";
+	sys: Sys;
+	print: import sys;
+
+include "draw.m";
+include "tk.m";
+	tk: Tk;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "gr.m";
+	gr: GR;
+	Plot: import gr;
+
+Graph0: module{
+	init:	fn(nil: ref Draw->Context, argv: list of string);
+};
+
+
+plotfile(ctxt: ref Draw->Context, nil: list of string, filename: string){
+	p := gr->open(ctxt,filename);
+	input := bufio->open(filename,bufio->OREAD);
+	if(input==nil){
+		print("can't read %s",filename);
+		exit;
+	}
+
+	n := 0;
+	maxn := 100;
+	x := array[maxn] of real;
+	y := array[maxn] of real;
+	while(1){
+		xn := input.gett(" \t\n\r");
+		if(xn==nil)
+			break;
+		yn := input.gett(" \t\n\r");
+		if(yn==nil){
+			print("after reading %d pairs, saw singleton\n",n);
+			exit;
+		}
+		if(n>=maxn){
+			maxn *= 2;
+			newx := array[maxn] of real;
+			newy := array[maxn] of real;
+			for(i:=0; i<n; i++){
+				newx[i] = x[i];
+				newy[i] = y[i];
+			}
+			x = newx;
+			y = newy;
+		}
+		x[n] = real xn;
+		y[n] = real yn;
+		n++;
+	}
+	if(n==0){
+		print("empty input\n");
+		exit;
+	}
+
+	p.graph(x[0:n],y[0:n]);
+	p.pen(GR->CIRCLE);
+	p.graph(x[0:n],y[0:n]);
+	p.paint("",nil,"",nil);
+	p.bye();
+}
+
+init(ctxt: ref Draw->Context, argv: list of string){
+	sys = load Sys Sys->PATH;
+	tk = load Tk Tk->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if((gr = load GR GR->PATH) == nil){
+		sys->print("%s: Can't load gr\n",hd argv);
+		exit;
+	}
+
+	argv = tl argv;
+	if(argv!=nil)
+		plotfile(ctxt,argv,hd argv);
+}
--- /dev/null
+++ b/appl/math/hist0.b
@@ -1,0 +1,99 @@
+implement Hist0;
+
+include "sys.m";
+	sys: Sys;
+	print: import sys;
+include "math.m";
+	math: Math;
+	fmin: import math;
+
+include "draw.m";
+include "tk.m";
+	tk: Tk;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "gr.m";
+	gr: GR;
+	Plot: import gr;
+
+Hist0: module{
+	init:	fn(nil: ref Draw->Context, argv: list of string);
+};
+
+
+plotfile(ctxt: ref Draw->Context, nil: list of string, filename: string){
+	p := gr->open(ctxt,filename);
+	input := bufio->open(filename,bufio->OREAD);
+	if(input==nil){
+		print("can't read %s",filename);
+		exit;
+	}
+
+	n := 0;
+	maxn := 100;
+	x := array[maxn] of real;
+	y := array[maxn] of real;
+	while(1){
+		xn := input.gett(" \t\n\r");
+		if(xn==nil)
+			break;
+		yn := input.gett(" \t\n\r");
+		if(yn==nil){
+			print("after reading %d pairs, saw singleton\n",n);
+			exit;
+		}
+		if(n>=maxn){
+			maxn *= 2;
+			newx := array[maxn] of real;
+			newy := array[maxn] of real;
+			for(i:=0; i<n; i++){
+				newx[i] = x[i];
+				newy[i] = y[i];
+			}
+			x = newx;
+			y = newy;
+		}
+		x[n] = real xn;
+		y[n] = real yn;
+		n++;
+	}
+	if(n==0){
+		print("empty input\n");
+		exit;
+	}
+
+	for(i:=0; i<n; i++){
+		h := 0.2;
+		if(i==0) h *= x[i+1]-x[i];
+		else if(i==n-1) h *= x[i]-x[i-1];
+		else h *= fmin(x[i+1]-x[i],x[i]-x[i-1]);
+		barx := array[] of{x[i]-h,x[i]-h,x[i]+h,x[i]+h,x[i]-h};
+		bary := array[] of{0.,y[i],y[i],0.,0.};
+		p.graph(barx,bary);
+	}
+	p.paint("",nil,"",nil);
+	p.bye();
+}
+
+init(ctxt: ref Draw->Context, argv: list of string){
+	sys = load Sys Sys->PATH;
+	math = load Math Math->PATH;
+	tk = load Tk Tk->PATH;
+	bufio = load Bufio Bufio->PATH;
+	if((gr = load GR GR->PATH) == nil){
+		sys->print("%s: Can't load gr\n",hd argv);
+		exit;
+	}
+
+	tkargs := "";
+	argv = tl argv;
+	if(argv != nil) {
+		tkargs = hd argv;
+		argv = tl argv;
+	}
+	if(argv!=nil)
+		plotfile(ctxt,argv,hd argv);
+}
--- /dev/null
+++ b/appl/math/linalg.b
@@ -1,0 +1,197 @@
+implement LinAlg;
+
+include "sys.m";
+sys: Sys;
+print: import sys;
+
+include "math.m";
+math: Math;
+ceil, fabs, floor, Infinity, log10, pow10, sqrt: import math;
+dot, gemm, iamax: import math;
+
+include "linalg.m";
+
+# print a matrix in MATLAB-compatible format
+printmat(label:string, a:array of real, lda, m, n:int)
+{
+	if(m>30 || n>10)
+		return;
+	if(sys==nil){
+		sys = load Sys Sys->PATH;
+		math = load Math Math->PATH;
+	}
+	print("%% %d by %d matrix\n",m,n);
+	print("%s = [",label);
+	for(i:=0; i<m; i++){
+		print("%.4g",a[i]);
+		for(j:=1; j<n; j++)
+			print(", %.4g",a[i+lda*j]);
+		if(i==m-1)
+			print("]\n");
+		else
+			print(";\n");
+	}
+}
+
+
+# Constant times a vector plus a vector.
+daxpy(da:real, dx:array of real, dy:array of real)
+{
+	n := len dx;
+	gemm('N','N',n,1,n,da,nil,0,dx,n,1.,dy,n);
+}
+
+# Scales a vector by a constant.
+dscal(da:real, dx:array of real)
+{
+	n := len dx;
+	gemm('N','N',n,1,n,0.,nil,0,nil,0,da,dx,n);
+}
+
+# gaussian elimination with partial pivoting
+#   dgefa factors a double precision matrix by gaussian elimination.
+#   dgefa is usually called by dgeco, but it can be called
+#   directly with a saving in time if  rcond  is not needed.
+#   (time for dgeco) = (1 + 9/n)*(time for dgefa) .
+#   on entry
+#      a       REAL precision[n][lda]
+#	      the matrix to be factored.
+#      lda     integer
+#	      the leading dimension of the array  a .
+#      n       integer
+#	      the order of the matrix  a .
+#   on return
+#      a       an upper triangular matrix and the multipliers
+#	      which were used to obtain it.
+#	      the factorization can be written  a = l*u  where
+#	      l  is a product of permutation and unit lower
+#	      triangular matrices and  u  is upper triangular.
+#      ipvt    integer[n]
+#	      an integer vector of pivot indices.
+#      info    integer
+#	      = 0  normal value.
+#	      = k  if  u[k][k] .eq. 0.0 .  this is not an error
+#		   condition for this subroutine, but it does
+#		   indicate that dgesl or dgedi will divide by zero
+#		   if called.  use  rcond  in dgeco for a reliable
+#		   indication of singularity.
+dgefa(a:array of real, lda, n:int, ipvt:array of int): int
+{
+	if(sys==nil){
+		sys = load Sys Sys->PATH;
+		math = load Math Math->PATH;
+	}
+	info := 0;
+	nm1 := n - 1;
+	if(nm1 >= 0)
+	    for(k := 0; k < nm1; k++){
+		kp1 := k + 1;
+		ldak := lda*k;
+	
+		# find l = pivot index
+		l := iamax(a[ldak+k:ldak+n]) + k;
+		ipvt[k] = l;
+	
+		# zero pivot implies this column already triangularized
+		if(a[ldak+l]!=0.){
+	
+		    # interchange if necessary
+		    if(l!=k){
+			t := a[ldak+l];
+			a[ldak+l] = a[ldak+k];
+			a[ldak+k] = t;
+		    }
+	
+		    # compute multipliers
+		    t := -1./a[ldak+k];
+		    dscal(t,a[ldak+k+1:ldak+n]);
+	
+		    # row elimination with column indexing
+		    for(j := kp1; j < n; j++){
+			ldaj := lda*j;
+			t = a[ldaj+l];
+			if(l!=k){
+			    a[ldaj+l] = a[ldaj+k];
+			    a[ldaj+k] = t;
+			}
+			daxpy(t,a[ldak+k+1:ldak+n],a[ldaj+k+1:ldaj+n]);
+		    }
+		}else
+		    info = k;
+	    }
+	ipvt[n-1] = n-1;
+	if(a[lda*(n-1)+(n-1)] == 0.)
+	    info = n-1;
+	return info;
+}
+
+
+#   dgesl solves the double precision system
+#   a * x = b  or  trans(a) * x = b
+#   using the factors computed by dgeco or dgefa.
+#   on entry
+#      a       double precision[n][lda]
+#	      the output from dgeco or dgefa.
+#      lda     integer
+#	      the leading dimension of the array  a .
+#      n       integer
+#	      the order of the matrix  a .
+#      ipvt    integer[n]
+#	      the pivot vector from dgeco or dgefa.
+#      b       double precision[n]
+#	      the right hand side vector.
+#      job     integer
+#	      = 0	 to solve  a*x = b ,
+#	      = nonzero   to solve  trans(a)*x = b  where
+#			  trans(a)  is the transpose.
+#  on return
+#      b       the solution vector  x .
+#   error condition
+#      a division by zero will occur if the input factor contains a
+#      zero on the diagonal.  technically this indicates singularity
+#      but it is often caused by improper arguments or improper
+#      setting of lda.
+dgesl(a:array of real, lda, n:int, ipvt:array of int, b:array of real, job:int)
+{
+	nm1 := n - 1;
+	if(job == 0){	# job = 0 , solve  a * x = b
+	    # first solve  l*y = b	
+	    if(nm1 >= 1)
+		for(k := 0; k < nm1; k++){
+		    l := ipvt[k];
+		    t := b[l];
+		    if(l!=k){
+			b[l] = b[k];
+			b[k] = t;
+		    }
+		    daxpy(t,a[lda*k+k+1:lda*k+n],b[k+1:n]);
+		}
+
+	    # now solve  u*x = y
+	    for(kb := 0; kb < n; kb++){
+		k = n - (kb + 1);
+		b[k] = b[k]/a[lda*k+k];
+		t := -b[k];
+		daxpy(t,a[lda*k:lda*k+k],b[0:k]);
+	    }
+	}else{	# job = nonzero, solve  trans(a) * x = b
+	    # first solve  trans(u)*y = b	
+	    for(k := 0; k < n; k++){
+		t := dot(a[lda*k:lda*k+k],b[0:k]);
+		b[k] = (b[k] - t)/a[lda*k+k];
+	    }
+
+	    # now solve trans(l)*x = y
+	    if(nm1 >= 1)
+		for(kb := 1; kb < nm1; kb++){
+		    k = n - (kb+1);
+		    b[k] += dot(a[lda*k+k+1:lda*k+n],b[k+1:n]);
+		    l := ipvt[k];
+		    if(l!=k){
+			t := b[l];
+			b[l] = b[k];
+			b[k] = t;
+		    }
+		}
+	 }
+}
--- /dev/null
+++ b/appl/math/linbench.b
@@ -1,0 +1,197 @@
+# Translated to Limbo by Eric Grosse <ehg@netlib.bell-labs.com> 3/96
+# Translated to Java by Reed Wade  (wade@cs.utk.edu) 2/96
+# Translated to C by Bonnie Toy 5/88
+# Will Menninger, 10/93
+# Jack Dongarra, linpack, 3/11/78.
+# Cleve Moler, linpack, 08/14/78
+
+implement linbench;
+
+include "sys.m";
+	sys: Sys;
+	print: import sys;
+include "math.m";
+	math: Math;
+	dot, fabs, gemm, iamax: import math;
+include "draw.m";
+	draw: Draw;
+	Rect, Screen, Display, Image: import draw;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "linalg.m";
+	linalg: LinAlg;
+	dgefa, dgesl, printmat: import linalg;
+
+ctxt: ref Draw->Context;
+top: ref Tk->Toplevel;
+buttonlist: string;
+
+
+linbench: module
+{
+	init:   fn(nil: ref Draw->Context, argv: list of string);
+};
+
+tkcmd(arg: string): string{
+	rv := tk->cmd(top,arg);
+	if(rv!=nil && rv[0]=='!')
+		print("tk->cmd(%s): %s\n",arg,rv);
+	return rv;
+}
+
+init(xctxt: ref Draw->Context, nil: list of string)
+{
+	sys    = load Sys  Sys->PATH;
+	math   = load Math Math->PATH;
+	draw   = load Draw Draw->PATH;
+	tk     = load Tk   Tk->PATH;
+	tkclient  = load Tkclient Tkclient->PATH;
+	linalg = load LinAlg LinAlg->PATH;
+		if(linalg==nil) print("couldn't load LinAlg\n");
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+	ctxt = xctxt;
+	menubut: chan of string;
+	(top, menubut) = tkclient->toplevel(ctxt, "",
+				"Linpack in Limbo", Tkclient->Appl);
+	cmd := chan of string;
+	tk->namechan(top, cmd, "cmd");
+
+	tkcmd("pack .Wm_t -fill x");
+	tkcmd("frame .b");
+	tkcmd("button .b.Run -text Run -command {send cmd run}");
+	tkcmd("entry .b.N -width 10w");  tkcmd(".b.N insert 0 200");
+	tkcmd("pack .b.Run .b.N -side left");
+	tkcmd("pack .b -anchor w");
+	tkcmd("frame .d");
+	tkcmd("listbox .d.f -width 35w -height 150 -selectmode single -yscrollcommand {.d.fscr set}");
+	tkcmd("scrollbar .d.fscr -command {.d.f yview}");
+	tkcmd("pack .d.f .d.fscr -expand 1 -fill y -side right");
+	tkcmd("pack .d -side top");
+	tkcmd("focus .b.N");
+	tkcmd("pack propagate . 0;update");
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+	s := <-top.ctxt.kbd =>
+		tk->keyboard(top, s);
+	s := <-top.ctxt.ptr =>
+		tk->pointer(top, *s);
+	s := <-top.ctxt.ctl or
+	s = <-top.wreq or
+	s = <-menubut =>
+		tkclient->wmctl(top, s);
+	press := <-cmd =>
+		case press {
+		"run" =>
+			tkcmd("cursor -bitmap cursor.wait; update");
+			nstr := tkcmd(".b.N get");
+			n := int nstr;
+			(mflops,secs) := benchmark(n);
+			result := sys->sprint("%8.2f Mflops %8.1f secs",mflops,secs);
+			tkcmd("cursor -default");
+			tkcmd(".d.f insert end {" + result + "}");
+			tkcmd(".d.f yview moveto 1; update");
+		}
+	}
+}
+
+benchmark(n: int): (real,real)
+{
+	math = load Math Math->PATH;
+
+	time := array [2] of real;
+	lda := 201;
+	if(n>lda) lda = n;
+	a := array [lda*n] of real;
+	b := array [n] of real;
+	x := array [n] of real;
+	ipvt := array [n] of int;
+	ops := (2*n*n*n)/3 + 2*n*n;
+
+	norma := matgen(a,lda,n,b);
+	printmat("a",a,lda,n,n);
+	printmat("b",b,lda,n,1);
+	t1 := second();
+	dgefa(a,lda,n,ipvt);
+	time[0] = second() - t1;
+	printmat("a",a,lda,n,n);
+	t1 = second();
+	dgesl(a,lda,n,ipvt,b,0);
+	time[1] = second() - t1;
+	total := time[0] + time[1];
+
+	for(i := 0; i < n; i++) {
+		x[i] = b[i];
+	}
+	printmat("x",x,lda,n,1);
+	norma = matgen(a,lda,n,b);
+	for(i = 0; i < n; i++) {
+		b[i] = -b[i];
+	}
+	dmxpy(b,x,a,lda);
+	resid := 0.;
+	normx := 0.;
+	for(i = 0; i < n; i++){
+		if(resid<fabs(b[i])) resid = fabs(b[i]);
+		if(normx<fabs(x[i])) normx = fabs(x[i]);
+	}
+
+	eps_result := math->MachEps;
+	residn_result := (real n)*norma*normx*eps_result;
+	if(residn_result!=0.)
+		residn_result = resid/residn_result;
+	else
+		print("can't scale residual.");
+	if(residn_result>math->sqrt(real n))
+		print("resid/MachEps=%.3g\n",residn_result);
+	time_result := total;
+	mflops_result := 0.;
+	if(total!=0.)
+		mflops_result = real ops/(1e6*total);
+	else
+		print("can't measure time\n");
+	return (mflops_result,time_result);
+}
+
+
+# multiply matrix m times vector x and add the r_result to vector y.
+dmxpy(y, x, m:array of real, ldm: int)
+{
+	n1 := len y;
+	n2 := len x;
+	gemm('N','N',n1,1,n2,1.,m,ldm,x,n2,1.,y,n1);
+}
+
+second(): real
+{
+	return(real sys->millisec()/1000.);
+}
+
+
+# generate a (fixed) random matrix and right hand side
+# a[i][j] => a[lda*i+j]
+matgen(a: array of real, lda, n: int, b: array of real): real
+{
+	seed := 1325;
+	norma := 0.;
+	for(j := 0; j < n; j++)
+		for(i := 0; i < n; i++){
+			seed = 3125*seed % 65536;
+			a[lda*j+i] = (real seed - 32768.0)/16384.0;
+			if(norma < a[lda*j+i]) norma = a[lda*j+i];
+		}
+	for (i = 0; i < n; i++)
+		b[i] = 0.;
+	for (j = 0; j < n; j++)
+		for (i = 0; i < n; i++)
+			b[i] += a[lda*j+i];
+	return norma;
+}
--- /dev/null
+++ b/appl/math/mersenne.b
@@ -1,0 +1,99 @@
+implement Mersenne;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "keyring.m";
+	keyring: Keyring;
+	IPint: import keyring;
+
+# Test primality of Mersenne numbers
+
+Mersenne: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	p := 3;
+	if(tl argv != nil)
+		p = int hd tl argv;
+	if(isprime(p) && (p == 2 || lucas(p)))
+		s := "";
+	else
+		s = "not ";
+	sys->print("2^%d-1 is %sprime\n", p, s);
+}
+
+# s such that s^2 <= n
+sqrt(n: int): int
+{
+	v := n;
+	r := 0;
+	for(t := 1<<30; t; t >>= 2){
+		if(t+r <= v){
+			v -= t+r;
+			r = (r>>1)|t;
+		}
+		else
+			r = r>>1;
+	}
+	return r;
+}
+
+isprime(n: int): int
+{
+	if(n < 2)
+		return 0;
+	if(n == 2)
+		return 1;
+	if((n&1) == 0)
+		return 0;
+	s := sqrt(n);
+	for(i := 3; i <= s; i += 2)
+		if(n%i == 0)
+			return 0;
+	return 1;
+}
+
+pow(b : ref IPint, n : int): ref IPint
+{
+	zero := IPint.inttoip(0);
+	one := IPint.inttoip(1);
+	if((b.cmp(zero) == 0 && n != 0) || b.cmp(one) == 0 || n == 1)
+		return b;
+	if(n == 0)
+		return one;
+	c := b;
+	b = one;
+	while(n){
+		while(!(n & 1)){
+			n >>= 1;
+			c = c.mul(c);
+		}
+		n--;
+		b = c.mul(b);
+	}
+	return b;
+}
+
+lucas(p: int): int
+{
+	zero := IPint.inttoip(0);
+	one := IPint.inttoip(1);
+	two := IPint.inttoip(2);
+	bigp := pow(two, p).sub(one);
+	u := IPint.inttoip(4);
+	for(i := 2; i < p; i++){
+		u = u.mul(u);
+		if(u.cmp(two) <= 0)
+			u = two.sub(u);
+		else
+			u = u.sub(two).expmod(one, bigp);
+	}
+	return u.cmp(zero) == 0;
+}
+
--- /dev/null
+++ b/appl/math/mkfile
@@ -1,0 +1,43 @@
+<../../mkconfig
+
+TARG=\
+	ack.dis\
+	crackerbarrel.dis\
+	factor.dis\
+	ffts.dis\
+	fibonacci.dis\
+	fit.dis\
+	genprimes.dis\
+	geodesy.dis\
+	graph0.dis\
+	gr.dis\
+	hist0.dis\
+	linalg.dis\
+	linbench.dis\
+	mersenne.dis\
+	parts.dis\
+	perms.dis\
+	pi.dis\
+	polyfill.dis\
+	polyhedra.dis\
+	powers.dis\
+	primes.dis\
+	sieve.dis\
+
+MODULES=
+
+SYSMODULES=\
+	math.m\
+	gr.m\
+	linalg.m\
+	draw.m\
+	sys.m\
+	tk.m\
+	wmlib.m\
+	bufio.m\
+	math/polyfill.m\
+	math/polyhedra.m\
+
+DISBIN=$ROOT/dis/math
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/math/parts.b
@@ -1,0 +1,125 @@
+implement Partitions;
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+include "keyring.m";
+	keyring: Keyring;
+	IPint: import keyring;
+
+#
+# the number p(n) of partitions of n 
+# based upon the formula :-
+# p(n) = p(n-1)+p(n-2)-p(n-5)-p(n-7)+p(n-12)+p(n-15)-p(n-22)-p(n-26)+.....
+# where p[0] = 1 and p[m] = 0 for m < 0
+#
+
+aflag := 0;
+cflag := 0;
+
+Partitions: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	keyring = load Keyring Keyring->PATH;
+	argv = tl argv;
+	while(argv != nil){
+		s := hd argv;
+		if(s != nil && s[0] == '-'){
+			for(i := 1; i < len s; i++){
+				case s[i]{
+					'a' => aflag = 1;
+					'c' => cflag = 1;
+				}
+			}
+		}
+		else
+			parts(int s);
+		argv = tl argv;
+	}
+}
+
+parts(m : int)
+{
+	if (aflag)
+		sys->print("n	p(n)\n");
+	if (m <= 0) {
+		p := 0;
+		if (m == 0)
+			p = 1;
+		if (aflag)
+			sys->print("%d	%d\n", m, p);
+		else
+			sys->print("p[%d] = %d\n", m, p);
+		return;
+	}
+	p := array[m+1] of ref IPint;
+	if (p == nil)
+		return;
+	p[0] = IPint.inttoip(1);
+	for (i := 1; i <= m; i++) {
+		k := i;
+		s := 1;
+		n := IPint.inttoip(0);
+		for (j := 1; ; j++) {
+			k -= 2*j-1;
+			if (k < 0)
+				break;
+			if (s == 1)
+				n = n.add(p[k]);
+			else
+				n = n.sub(p[k]);
+			k -= j;
+			if (k < 0)
+				break;
+			if (s == 1)
+				n = n.add(p[k]);
+			else
+				n = n.sub(p[k]);
+			s = -s;
+		}
+		if (aflag)
+			sys->print("%d	%s\n", i, n.iptostr(10));
+		p[i] = n;
+	}
+	if (!aflag)
+		sys->print("p[%d] = %s\n", m, p[m].iptostr(10));
+	if (cflag)
+		check(m, p);
+}
+
+#
+# given p[0]..p[m], search for congruences of the form
+# p[ni+j] = r mod i
+#
+check(m : int, p : array of ref IPint)
+{
+	one := IPint.inttoip(1);
+	for (i := 2; i < m/3; i++) {
+		ip := IPint.inttoip(i);
+		for (j := 0; j < i; j++) {
+			k := j;
+			r := p[k].expmod(one, ip).iptoint();
+			s := 1;
+			for (;;) {
+				k += i;
+				if (k > m)
+					break;
+				if (p[k].expmod(one, ip).iptoint() != r) {
+					r = -1;
+					break;
+				}
+				s++;
+			}
+			if (r >= 0)
+				if (j == 0)
+					sys->print("p(%dm) = %d mod %d ?\n", i, r, i);
+				else
+					sys->print("p(%dm+%d) = %d mod %d ?\n", i, j, r, i);
+		}
+	}
+}
--- /dev/null
+++ b/appl/math/perms.b
@@ -1,0 +1,131 @@
+#
+#	initially generated by c2l
+#
+
+implement Perms;
+
+include "draw.m";
+
+Perms: module
+{
+	init: fn(nil: ref Draw->Context, argl: list of string);
+};
+
+include "sys.m";
+	sys: Sys;
+
+init(nil: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	main(len argl, argl);
+}
+
+# 
+#  * generate permutations of N elements
+#  *	from ``On Programming, an interim report on the SETL project'',
+#  *	Jacob T Schwartz (ed), New York University
+#  
+Seq: adt{
+	nel: int;
+	el: array of int;
+};
+
+origin: int = 1;
+
+main(argc: int, argv: list of string): int
+{
+	n: int;
+
+	if(argc > 1 && (as := hd tl argv)[0] == '-'){
+		origin = int (as[1: ]);
+		argc--;
+		argv = tl argv;
+	}
+	if(argc != 2){
+		sys->fprint(sys->fildes(2), "Usage: perms #elements\n");
+		exit;
+	}
+	n = int hd tl argv;
+	if(n > 0)
+		perms(n);
+	exit;
+}
+
+
+perms(n: int)
+{
+	seq: ref Seq;
+
+	seq = newseq(n);
+	do
+		putseq(seq);
+	while(eperm(seq) != nil);
+}
+
+putseq(seq: ref Seq)
+{
+	k: int;
+
+	for(k = 0; k < seq.nel; k++)
+		sys->print(" %d", seq.el[k]+origin);
+	sys->print("\n");
+}
+
+eperm(seq: ref Seq): ref Seq
+{
+	j, k, n: int;
+
+	n = seq.nel;
+	#  if sequence is monotone decreasing, there are no more
+	# 		permutations.  Otherwise, find last point of increase 
+	hit := 0;
+	for(j = n-2; j >= 0; j--)
+		if(seq.el[j] < seq.el[j+1]){
+			hit = 1;
+			break;
+		}
+	if(!hit)
+		return nil;
+	#  then find the last seq[k] which exceeds seq[j] and swop 
+	for(k = seq.nel-1; k > j; k--)
+		if(seq.el[k] > seq.el[j]){
+			{
+				t: int;
+
+				t = seq.el[k];
+				seq.el[k] = seq.el[j];
+				seq.el[j] = t;
+			}
+			;
+			break;
+		}
+	#  then re-arrange all the elements from seq[j+1] into
+	# 		increasing order 
+	for(k = j+1; k < (n+j+1)/2; k++){
+		kk: int;
+
+		kk = n-k+j;
+		{
+			t: int;
+
+			t = seq.el[k];
+			seq.el[k] = seq.el[kk];
+			seq.el[kk] = t;
+		}
+		;
+	}
+	return seq;
+}
+
+newseq(n: int): ref Seq
+{
+	seq: ref Seq;
+	k: int;
+
+	seq = ref Seq;
+	seq.nel = n;
+	seq.el = array[n] of int;
+	for(k = 0; k < n; k++)
+		seq.el[k] = k;
+	return seq;
+}
--- /dev/null
+++ b/appl/math/pi.b
@@ -1,0 +1,405 @@
+implement Pi;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "math.m";
+	math: Math;
+	log: import math;
+include "daytime.m";
+	daytime: Daytime;
+
+LBASE: con 3;	# 4
+BASE: con 1000;	# 10000
+
+stderr: ref Sys->FD;
+
+# spawn process for each series ?
+
+Pi: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	math = load Math Math->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	stderr = sys->fildes(2);
+	dp := 1000;
+	argv = tl argv;
+	if(argv != nil){
+		if(tl argv != nil){
+			picmp(hd argv, hd tl argv);
+			exit;
+		}
+		dp = int hd argv;
+	}
+	if(dp <= 0)
+		exit;
+	# t1 := daytime->now();
+	p2 := pi2(dp+1);
+	# t2 := daytime->now();
+	prpi(p2);
+	p1 := pi1(dp+1);
+	# t3 := daytime->now();
+	# sys->print("%d %d\n", t2-t1, t3-t2);
+	if(p1 == nil && p2 == nil)
+		fatal("too many dp: reduce dp or source base");
+	else if(p1 == nil)
+		p1 = p2;
+	else if(p2 == nil)
+		p2 = p1;
+	n1 := len p1;
+	n2 := len p2;
+	if(n1 != n2)
+		fatal(sys->sprint("lens differ %d %d", n1, n2));
+	f := array[10] of { * => 0 };
+	for(i := 0; i < n1; i++){
+		if(p1[i] != p2[i])
+			fatal(sys->sprint("arrays differ %d/%d: %d %d", i, n1, p1[i], p2[i]));
+		if(p1[i] < 0 || p1[i] >= BASE)
+			fatal(sys->sprint("bad array element %d: %d", i, p1[i]));
+		if(0){
+			p := p1[i];
+			for(j := 0; j < LBASE; j++){
+				f[p%10]++;
+				p /= 10;
+			}
+		}
+	}
+	# prpi(p1);
+	if(0){
+		t := 0;
+		for(i = 0; i < 10; i++){
+			sys->print("%d	%d\n", i, f[i]);
+			t += f[i];
+		}
+		sys->print("T	%d\n", t);
+	}
+}
+
+terms(dp: int, f: int, v: int): (int, int)
+{
+	p := dp;
+	t := 0;
+	for(;;){
+		t = 2 + int ((real p*log(real 10)+log(real v))/log(real f));
+		if(!(t&1))
+			t++;
+		e := int (log(real (v*(t+1)/2))/log(real 10))+1;
+		if(dp <= p-e)
+			break;
+		p += e;
+	}
+	# sys->fprint(stderr, "dp=%d p=%d f=%d v=%d terms=%d\n", dp, p, f, v, t);
+	if(t < f*f)
+		k := f*f;
+	else
+		k = t;
+	m := BASE*k;
+	if(m < 0 || m < BASE || m < k || m/BASE != k || m/k != BASE)
+		return (-1, -1);
+	return (t, p);
+}
+
+prpi(p: array of int)
+{
+	n := len p;
+	sys->print("π ≅ ");
+	m := BASE/10;
+	sys->print("%d.%.*d", p[0]/m, LBASE-1, p[0]%m);
+	for(i := 1; i < n; i++)
+		sys->print("%.*d", LBASE, p[i]);
+	sys->print("\n");
+}
+
+memcmp(b1: array of byte, b2: array of byte, n: int): (int, int, int)
+{
+	for(i := 0; i < n; i++)
+		if(b1[i] != b2[i])
+			return (i, int b1[i], int b2[i]);
+	return (-1, 0, 0);
+}
+
+picmp(f1: string, f2: string)
+{
+	fd1 := sys->open(f1, Sys->OREAD);
+	fd2 := sys->open(f2, Sys->OREAD);
+	if(fd1 == nil || fd2 == nil)
+		fatal(sys->sprint("cannot open %s or %s", f1, f2));
+	b1 := array[Sys->ATOMICIO] of byte;
+	b2 := array[Sys->ATOMICIO] of byte;
+	t := 0;
+	shouldexit := 0;
+	for(;;){
+		n1 := sys->read(fd1, b1, len b1);
+		n2 := sys->read(fd2, b2, len b2);
+		if(n1 <= 0 || n2 <= 0)
+			return;
+		if(shouldexit)
+			fatal("bad picmp");
+		if(n1 < n2)
+			(d, v1, v2) := memcmp(b1, b2, n1);
+		else
+			(d, v1, v2) = memcmp(b1, b2, n2);
+		if(d >= 0){
+			if(v1 == '\n' || v2 == '\n')
+				shouldexit = 1;
+			else
+				fatal(sys->sprint("%s %s differ at byte %d(%c %c)", f1, f2, t+d, v1, v2));
+		}
+		t += n1;
+		if(n1 != n2)
+			shouldexit = 1;
+	}
+}
+
+roundup(n: int, m: int): (int, int)
+{
+	r := m*((n+m-1)/m);
+	return (r, r/m);
+}
+
+pi1(dp: int): array of int
+{
+	fs := array[2] of { 5, 239 };
+	vs := array[2] of { 16, 4 };
+	ss := array[2] of { 1, -1 };
+	# sys->fprint(stderr, "π1\n");
+	return pi(dp, fs, vs, ss);
+}
+
+pi2(dp: int): array of int
+{
+	fs := array[3] of { 18, 57, 239 };
+	vs := array[3] of { 48, 32, 20 };
+	ss := array[3] of { 1, 1, -1 };
+	# sys->fprint(stderr, "π2\n");
+	return pi(dp, fs, vs, ss);
+}
+
+pi3(dp: int): array of int
+{
+	fs := array[4] of { 57, 239, 682, 12943 };
+	vs := array[4] of { 176, 28, 48, 96 };
+	ss := array[4] of { 1, 1, -1, 1 };
+	# sys->fprint(stderr, "π3\n");
+	return pi(dp, fs, vs, ss);
+}
+
+pi(dp: int, fs: array of int, vs: array of int, ss: array of int): array of int
+{
+	k := len fs;
+	n := cn := adp := 0;
+	(dp, n) = roundup(dp, LBASE);
+	cdp := dp;
+	m := array[k] of int;
+	for(i := 0; i < k; i++){
+		(m[i], adp) = terms(dp+1, fs[i], vs[i]);
+		if(m[i] < 0)
+			return nil;
+		if(adp > cdp)
+			cdp = adp;
+	}
+	(cdp, cn) = roundup(cdp, LBASE);
+	a := array[cn] of int;
+	p := array[cn] of int;
+	for(i = 0; i < cn; i++)
+		p[i] = 0;
+	for(i = 0; i < k; i++){
+		series(m[i], cn, fs[i], (vs[i]*BASE)/10, ss[i], a, p);
+		# sys->fprint(stderr, "term %d done\n", i+1);
+	}
+	return p[0: n];
+}
+
+series(m: int, n: int, f: int, v: int, s: int, a: array of int, p: array of int)
+{
+	i, j, k, q, r, r1, r2, n0: int;
+
+	v *= f;
+	f *= f;
+	for(j = 0; j < n; j++)
+		a[j] = 0;
+	a[0] = v;
+
+	if(s == 1)
+		series1(m, n, f, v, a, p);
+	else
+		series2(m, n, f, v, a, p);
+	return;
+
+	# following code now split
+	n0 = 0;	# reaches n when very close to m so no check needed
+	for(i = 1; i <= m; i += 2){
+		r1 = r2 = 0;
+		for(j = n0; j < n; j++){
+			v = a[j]+r1;
+			q = v/f;
+			r1 = (v-q*f)*BASE;
+			a[j] = q;
+			v = q+r2;
+			q = v/i;
+			r2 = (v-q*i)*BASE;
+			for(k = j; q > 0; k--){
+				r = p[k]+s*q;
+				if(r >= BASE){
+					p[k] = r-BASE;
+					q = 1;
+				}
+				else if(r < 0){
+					p[k] = r+BASE;
+					q = 1;
+				}
+				else{
+					p[k] = r;
+					q = 0;
+				}
+			}
+		}
+		for(j = n0; j < n; j++){
+			if(a[j] == 0)
+				n0++;
+			else
+				break;
+		}
+		s = -s;
+	}
+}
+
+series1(m: int, n: int, f: int, v: int, a: array of int, p: array of int)
+{
+	i, j, k, q, r, r1, r2, n0: int;
+
+	n0 = 0;
+	for(i = 1; i <= m; i += 2){
+		r1 = r2 = 0;
+		for(j = n0; j < n; j++){
+			v = a[j]+r1;
+			q = v/f;
+			r1 = (v-q*f)*BASE;
+			a[j] = q;
+			v = q+r2;
+			q = v/i;
+			r2 = (v-q*i)*BASE;
+			for(k = j; q > 0; k--){
+				r = p[k]+q;
+				if(r >= BASE){
+					p[k] = r-BASE;
+					q = 1;
+				}
+				else{
+					p[k] = r;
+					q = 0;
+				}
+			}
+		}
+		for(j = n0; j < n; j++){
+			if(a[j] == 0)
+				n0++;
+			else
+				break;
+		}
+		i += 2;
+		r1 = r2 = 0;
+		for(j = n0; j < n; j++){
+			v = a[j]+r1;
+			q = v/f;
+			r1 = (v-q*f)*BASE;
+			a[j] = q;
+			v = q+r2;
+			q = v/i;
+			r2 = (v-q*i)*BASE;
+			for(k = j; q > 0; k--){
+				r = p[k]-q;
+				if(r < 0){
+					p[k] = r+BASE;
+					q = 1;
+				}
+				else{
+					p[k] = r;
+					q = 0;
+				}
+			}
+		}
+		for(j = n0; j < n; j++){
+			if(a[j] == 0)
+				n0++;
+			else
+				break;
+		}
+	}
+}
+
+series2(m: int, n: int, f: int, v: int, a: array of int, p: array of int)
+{
+	i, j, k, q, r, r1, r2, n0: int;
+
+	n0 = 0;
+	for(i = 1; i <= m; i += 2){
+		r1 = r2 = 0;
+		for(j = n0; j < n; j++){
+			v = a[j]+r1;
+			q = v/f;
+			r1 = (v-q*f)*BASE;
+			a[j] = q;
+			v = q+r2;
+			q = v/i;
+			r2 = (v-q*i)*BASE;
+			for(k = j; q > 0; k--){
+				r = p[k]-q;
+				if(r < 0){
+					p[k] = r+BASE;
+					q = 1;
+				}
+				else{
+					p[k] = r;
+					q = 0;
+				}
+			}
+		}
+		for(j = n0; j < n; j++){
+			if(a[j] == 0)
+				n0++;
+			else
+				break;
+		}
+		i += 2;
+		r1 = r2 = 0;
+		for(j = n0; j < n; j++){
+			v = a[j]+r1;
+			q = v/f;
+			r1 = (v-q*f)*BASE;
+			a[j] = q;
+			v = q+r2;
+			q = v/i;
+			r2 = (v-q*i)*BASE;
+			for(k = j; q > 0; k--){
+				r = p[k]+q;
+				if(r >= BASE){
+					p[k] = r-BASE;
+					q = 1;
+				}
+				else{
+					p[k] = r;
+					q = 0;
+				}
+			}
+		}
+		for(j = n0; j < n; j++){
+			if(a[j] == 0)
+				n0++;
+			else
+				break;
+		}
+	}
+}
+
+fatal(e: string)
+{
+	sys->print("%s\n", e);
+	exit;
+}
--- /dev/null
+++ b/appl/math/polyfill.b
@@ -1,0 +1,440 @@
+implement Polyfill;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Image, Endsquare: import draw;
+include "math/polyfill.m";
+
+∞: con 16r7fffffff;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+}
+
+initzbuf(r: Rect): ref Zstate
+{
+	if(sys == nil)
+		init();
+	s := ref Zstate;
+	s.r = r;
+	s.xlen = r.dx();
+	s.ylen = r.dy();
+	s.xylen = s.xlen*s.ylen;
+	s.zbuf0 = array[s.xylen] of int;
+	s.zbuf1 = array[s.xylen] of int;
+	return s;
+}
+
+clearzbuf(s: ref Zstate)
+{
+	b0 := s.zbuf0;
+	b1 := s.zbuf1;
+	n := s.xylen;
+	for(i := 0; i < n; i++)
+		b0[i] = b1[i] = ∞;
+}
+
+setzbuf(s: ref Zstate, zd: int)
+{
+	b0 := s.zbuf0;
+	b1 := s.zbuf1;
+	n := s.xylen;
+	for(i := 0; i < n; i++)
+		b0[i] = b1[i] = zd;
+}
+
+Seg: adt
+{
+	p0: Point;
+	p1: Point;
+	num: int;
+	den: int;
+	dz: int;
+	dzrem: int;
+	z: int;
+	zerr: int;
+	d: int;
+};
+
+fillline(dst: ref Image, left: int, right: int, y: int, src: ref Image, p: Point)
+{
+	p.x += left;
+	p.y += y;
+	dst.line((left, y), (right, y), Endsquare, Endsquare, 0, src, p);
+}
+
+filllinez(dst: ref Image, left: int, right: int, y: int, z: int, e: int, dx: int, k: int, zbuf0: array of int, zbuf1: array of int, src: ref Image, p: Point)
+{
+	prevx := ∞;
+	for(x := left; x <= right; x++){
+		if(z+e < zbuf0[k] || (z-e <= zbuf1[k] && x != right && prevx != ∞)){
+			zbuf0[k] = z-e;
+			zbuf1[k] = z+e;
+			if(prevx == ∞)
+				prevx = x;
+		}
+		else if(prevx != ∞){
+			fillline(dst, prevx, x-1, y, src, p);
+			prevx = ∞;
+		}
+		z += dx;
+		k++;
+	}
+	if(prevx != ∞)
+		fillline(dst, prevx, right, y, src, p);
+}
+
+fillpoly(dst: ref Image, vert: array of Point, w: int, src: ref Image, sp: Point, zstate: ref Zstate, dc: int, dx: int, dy: int)
+{
+	p0: Point;
+	i: int;
+
+	nvert := len vert;
+	if(nvert == 0)
+		return;
+	fixshift := 0;
+	seg := array[nvert+2] of ref Seg;
+	if(seg == nil)
+		return;
+	segtab := array[nvert+1] of ref Seg;
+	if(segtab == nil)
+		return;
+
+	sp.x = (sp.x - vert[0].x) >> fixshift;
+	sp.y = (sp.y - vert[0].y) >> fixshift;
+	p0 = vert[nvert-1];
+	if(!fixshift) {
+		p0.x <<= 1;
+		p0.y <<= 1;
+	}
+	for(i = 0; i < nvert; i++) {
+		segtab[i] = ref Seg;
+		segtab[i].p0 = p0;
+		p0 = vert[i];
+		if(!fixshift) {
+			p0.x <<= 1;
+			p0.y <<= 1;
+		}
+		segtab[i].p1 = p0;
+		segtab[i].d = 1;
+	}
+	if(!fixshift)
+		fixshift = 1;
+
+	xscan(dst, seg, segtab, nvert, w, src, sp, zstate, dc, dx, dy, fixshift);
+}
+
+mod(x: int, y: int): int
+{
+	z: int;
+
+	z = x%y;
+	if((z^y) > 0 || z == 0)
+		return z;
+	return z + y;
+}
+
+sdiv(x: int, y: int): int
+{
+	if((x^y) >= 0 || x == 0)
+		return x/y;
+	return (x+((y>>30)|1))/y-1;
+}
+
+smuldivmod(x: int, y: int, z: int): (int, int)
+{
+	mod: int;
+	vx: int;
+
+	if(x == 0 || y == 0)
+		return (0, 0);
+	vx = x;
+	vx *= y;
+	mod = vx % z;
+	if(mod < 0)
+		mod += z;
+	if((vx < 0) == (z < 0))
+		return (vx/z, mod);
+	return (-((-vx)/z), mod);
+}
+
+xscan(dst: ref Image, seg: array of ref Seg, segtab: array of ref Seg, nseg: int, wind: int, src: ref Image, spt: Point, zstate: ref Zstate, dc: int, dx: int, dy: int, fixshift: int)
+{
+	y, maxy, x, x2, onehalf: int;
+	ep, next, p, q, s: int;
+	n, i, iy, cnt, ix, ix2, minx, maxx, zinc, k, zv: int;
+	pt: Point;
+	sp: ref Seg;
+
+	er := (abs(dx)+abs(dy)+1)/2;
+	zr := zstate.r;
+	xlen := zstate.xlen;
+	zbuf0 := zstate.zbuf0;
+	zbuf1 := zstate.zbuf1;
+	s = 0;
+	p = 0;
+	for(i=0; i<nseg; i++) {
+		sp = seg[p] = segtab[s];
+		if(sp.p0.y == sp.p1.y){
+			s++;
+			continue;
+		}
+		if(sp.p0.y > sp.p1.y) {
+			pt = sp.p0;
+			sp.p0 = sp.p1;
+			sp.p1 = pt;
+			sp.d = -sp.d;
+		}
+		sp.num = sp.p1.x - sp.p0.x;
+		sp.den = sp.p1.y - sp.p0.y;
+		sp.dz = sdiv(sp.num, sp.den) << fixshift;
+		sp.dzrem = mod(sp.num, sp.den) << fixshift;
+		sp.dz += sdiv(sp.dzrem, sp.den);
+		sp.dzrem = mod(sp.dzrem, sp.den);
+		p++;
+		s++;
+	}
+	n = p;
+	if(n == 0)
+		return;
+	seg[p] = nil;
+	qsortycompare(seg, p);
+
+	onehalf = 0;
+	if(fixshift)
+		onehalf = 1 << (fixshift-1);
+
+	minx = dst.clipr.min.x;
+	maxx = dst.clipr.max.x;
+
+	y = seg[0].p0.y;
+	if(y < (dst.clipr.min.y << fixshift))
+		y = dst.clipr.min.y << fixshift;
+	iy = (y + onehalf) >> fixshift;
+	y = (iy << fixshift) + onehalf;
+	maxy = dst.clipr.max.y << fixshift;
+	k = (iy-zr.min.y)*xlen;
+	zv = dc+iy*dy;
+
+	ep = next = 0;
+
+	while(y<maxy) {
+		for(q = p = 0; p < ep; p++) {
+			sp = seg[p];
+			if(sp.p1.y < y)
+				continue;
+			sp.z += sp.dz;
+			sp.zerr += sp.dzrem;
+			if(sp.zerr >= sp.den) {
+				sp.z++;
+				sp.zerr -= sp.den;
+				if(sp.zerr < 0 || sp.zerr >= sp.den)
+					sys->print("bad ratzerr1: %d den %d dzrem %d\n", sp.zerr, sp.den, sp.dzrem);
+			}
+			seg[q] = sp;
+			q++;
+		}
+
+		for(p = next; seg[p] != nil; p++) {
+			sp = seg[p];
+			if(sp.p0.y >= y)
+				break;
+			if(sp.p1.y < y)
+				continue;
+			sp.z = sp.p0.x;
+			(zinc, sp.zerr) = smuldivmod(y - sp.p0.y, sp.num, sp.den);
+			sp.z += zinc;
+			if(sp.zerr < 0 || sp.zerr >= sp.den)
+				sys->print("bad ratzerr2: %d den %d ratdzrem %d\n", sp.zerr, sp.den, sp.dzrem);
+			seg[q] = sp;
+			q++;
+		}
+		ep = q;
+		next = p;
+
+		if(ep == 0) {
+			if(seg[next] == nil)
+				break;
+			iy = (seg[next].p0.y + onehalf) >> fixshift;
+			y = (iy << fixshift) + onehalf;
+			k = (iy-zr.min.y)*xlen;
+			zv = dc+iy*dy;
+			continue;
+		}
+
+		zsort(seg, ep);
+
+		for(p = 0; p < ep; p++) {
+			sp = seg[p];
+			cnt = 0;
+			x = sp.z;
+			ix = (x + onehalf) >> fixshift;
+			if(ix >= maxx)
+				break;
+			if(ix < minx)
+				ix = minx;
+			cnt += sp.d;
+			p++;
+			sp = seg[p];
+			for(;;) {
+				if(p == ep) {
+					sys->print("xscan: fill to infinity");
+					return;
+				}
+				cnt += sp.d;
+				if((cnt&wind) == 0)
+					break;
+				p++;
+				sp = seg[p];
+			}
+			x2 = sp.z;
+			ix2 = (x2 + onehalf) >> fixshift;
+			if(ix2 <= minx)
+				continue;
+			if(ix2 > maxx)
+				ix2 = maxx;
+			filllinez(dst, ix, ix2, iy, zv+ix*dx, er, dx, k+ix-zr.min.x, zbuf0, zbuf1, src, spt);
+		}
+		y += (1<<fixshift);
+		iy++;
+		k += xlen;
+		zv += dy;
+	}
+}
+
+zsort(seg: array of ref Seg, ep: int)
+{
+	done: int;
+	s: ref Seg;
+	q, p: int;
+
+	if(ep < 20) {
+		# bubble sort by z - they should be almost sorted already
+		q = ep;
+		do {
+			done = 1;
+			q--;
+			for(p = 0; p < q; p++) {
+				if(seg[p].z > seg[p+1].z) {
+					s = seg[p];
+					seg[p] = seg[p+1];
+					seg[p+1] = s;
+					done = 0;
+				}
+			}
+		} while(!done);
+	} else {
+		q = ep-1;
+		for(p = 0; p < q; p++) {
+			if(seg[p].z > seg[p+1].z) {
+				qsortzcompare(seg, ep);
+				break;
+			}
+		}
+	}
+}
+
+ycompare(s0: ref Seg, s1: ref Seg): int
+{
+	y0, y1: int;
+
+	y0 = s0.p0.y;
+	y1 = s1.p0.y;
+
+	if(y0 < y1)
+		return -1;
+	if(y0 == y1)
+		return 0;
+	return 1;
+}
+
+zcompare(s0: ref Seg, s1: ref Seg): int
+{
+	z0, z1: int;
+
+	z0 = s0.z;
+	z1 = s1.z;
+
+	if(z0 < z1)
+		return -1;
+	if(z0 == z1)
+		return 0;
+	return 1;
+}
+
+qsortycompare(a : array of ref Seg, n : int)
+{
+	i, j : int;
+	t : ref Seg;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && ycompare(a[i], a[0]) < 0);
+			do
+				j--;
+			while(j > 0 && ycompare(a[j], a[0]) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsortycompare(a, j);
+			a = a[j+1:];
+		} else {
+			qsortycompare(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+qsortzcompare(a : array of ref Seg, n : int)
+{
+	i, j : int;
+	t : ref Seg;
+
+	while(n > 1) {
+		i = n>>1;
+		t = a[0]; a[0] = a[i]; a[i] = t;
+		i = 0;
+		j = n;
+		for(;;) {
+			do
+				i++;
+			while(i < n && zcompare(a[i], a[0]) < 0);
+			do
+				j--;
+			while(j > 0 && zcompare(a[j], a[0]) > 0);
+			if(j < i)
+				break;
+			t = a[i]; a[i] = a[j]; a[j] = t;
+		}
+		t = a[0]; a[0] = a[j]; a[j] = t;
+		n = n-j-1;
+		if(j >= n) {
+			qsortzcompare(a, j);
+			a = a[j+1:];
+		} else {
+			qsortzcompare(a[j+1:], n);
+			n = j;
+		}
+	}
+}
+
+abs(n: int): int
+{
+	if(n < 0)
+		return -n;
+	return n;
+}
--- /dev/null
+++ b/appl/math/polyhedra.b
@@ -1,0 +1,195 @@
+implement Polyhedra;
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "math/polyhedra.m";
+
+scanpolyhedra(f: string): (int, ref Polyhedron, ref Iobuf)
+{
+	first, last: ref Polyhedron;
+	D: int;
+
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(bufio == nil)
+		bufio = load Bufio Bufio->PATH;
+	b := bufio->open(f, Sys->OREAD);
+	if(b == nil)
+		return (0, nil, nil);
+	n := 0;
+	for(;;){
+		s := getstring(b);
+		if(s == nil)
+			break;
+		n++;
+		p := ref Polyhedron;
+		if(first == nil)
+			first = p;
+		else{
+			last.nxt = p;
+			p.prv = last;
+		}
+		last = p;
+		p.name = s;
+		p.dname = getstring(b);
+		b.gets('\n');
+		(p.allf, p.adj) = scanvc(getstring(b));
+		b.gets('\n');
+		b.gets('\n');
+		b.gets('\n');
+		l := getstring(b);
+		(p.indx, l) = getint(l);
+		(p.V, l) = getint(l);
+		(p.E, l) = getint(l);
+		(p.F, l) = getint(l);
+		(nil, l) = getint(l);
+		(D, l) = getint(l);
+		(p.anti, l) = getint(l);
+		p.concave = D != 1 || p.allf;
+		p.offset = b.offset();
+		tot := 2*p.V+2*p.F;
+		for(i := 0; i < tot; i++)
+			b.gets('\n');
+		if(p.indx < 58 || p.indx == 59 || p.indx == 66 || p.indx == 67)
+			p.inc = 0.1;
+		else
+			p.inc = 0.0;
+		# sys->print("%d:	%d %d %d %d %s\n", p.indx, p.allf, D != 1, p.anti, p.concave, vc);
+	}
+	first.prv = last;
+	last.nxt = first;
+	return (n, first, b);
+}
+
+getpolyhedra(p: ref Polyhedron, b: ref Iobuf)
+{
+	q := p;
+	do{
+		getpolyhedron(q, b);
+		q = q.nxt;
+	}while(q != p);	
+}
+
+getpolyhedron(p: ref Polyhedron, b: ref Iobuf)
+{
+	if(p.v != nil)
+		return;
+	b.seek(p.offset, Bufio->SEEKSTART);
+	p.v = array[p.V] of Vector;
+	for(i := 0; i < p.V; i++)
+		p.v[i] = getvector(b);
+	p.f = array[p.F] of Vector;
+	for(i = 0; i < p.F; i++)
+		p.f[i] = getvector(b);
+	p.fv = array[p.F] of array of int;
+	for(i = 0; i < p.F; i++)
+		p.fv[i] = getarray(b, p.adj);
+	p.vf = array[p.V] of array of int;
+	for(i = 0; i < p.V; i++)
+		p.vf[i] = getarray(b, p.adj);
+}
+
+getstring(b: ref Iobuf): string
+{
+	s := b.gets('\n');
+	if(s == nil)
+		return nil;
+	if(s[0] == '#')
+		return getstring(b);
+	if(s[len s - 1] == '\n')
+		return s[0: len s - 1];
+	return s;
+}
+
+getvector(b: ref Iobuf): Vector
+{
+	v: Vector;
+
+	s := getstring(b);
+	(v.x, s) = getreal(s);
+	(v.y, s) = getreal(s);
+	(v.z, s) = getreal(s);
+	return v;
+}
+
+getarray(b: ref Iobuf, adj: int): array of int
+{
+	n, d: int;
+
+	s := getstring(b);
+	(n, s) = getint(s);
+	a := array[n+2] of int;
+	a[0] = n;
+	for(i := 1; i <= n; i++)
+		(a[i], s) = getint(s);
+	(d, s) = getint(s);
+	if(d == 0 || d == n-1 || adj)
+		d = 1;
+	a[n+1] = d;
+	return a;
+}
+
+getint(s: string): (int, string)
+{
+	n := int s;
+	for(i := 0; i < len s && s[i] == ' '; i++)
+		;
+	for( ; i < len s; i++)
+		if(s[i] == ' ')
+			return (n, s[i+1:]);
+	return (n, nil);
+}
+
+getreal(s: string): (real, string)
+{
+	r := real s;
+	for(i := 0; i < len s && s[i] == ' '; i++)
+		;
+	for( ; i < len s; i++)
+		if(s[i] == ' ')
+			return (r, s[i+1:]);
+	return (r, nil);
+}
+
+vftab := array[] of { 0, 0, 0, 2, 3, 3, 5, 0, 3, 0, 3 };
+
+scanvc(s: string): (int, int)
+{
+	af := 0;
+	ad := 0;
+	fd := ld := 1;
+	ln := len s;
+	if(ln > 0 && s[0] == '('){
+		s = s[1:];
+		ln--;
+	}
+	while(ln > 0 && s[ln-1] != ')'){
+		s = s[0: ln-1];
+		ln--;
+	}
+	(m, lst) := sys->tokenize(s, ".");
+	for(l := lst ; l != nil; l = tl l){
+		(m, lst) = sys->tokenize(hd l, "/");
+		if(m == 1)
+			(n, d) := (int hd lst, 1);
+		else if(m == 2)
+			(n, d) = (int hd lst, int hd tl lst);
+		else
+			sys->print("vc error\n");
+		if(d != 1 && d == vftab[n])
+			af = 1;
+		if(d == n-1)
+			d = 1;
+		if(l == lst)
+			fd = d;
+		else if(ld != 1 && d != 1)
+			ad = 1;
+		ld = d;
+	}
+	if(ld != 1 && fd != 1)
+		ad = 1;
+	return (af, ad);
+}
--- /dev/null
+++ b/appl/math/powers.b
@@ -1,0 +1,672 @@
+implement Powers;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+include "lock.m";
+	lockm: Lock;
+	Semaphore: import lockm;
+
+Powers: module
+{
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+MAXNODES: con (1<<20)/4;
+
+verbose: int;
+
+# Doing
+# 	powers -p 3
+# gives
+# 	[2] 1729 = 1**3 + 12**3 = 9**3 + 10**3
+# 	[2] 4104 = 2**3 + 16**3 = 9**3 + 15**3
+
+# ie 1729 can be written in two ways as the sum of 2 cubes as can 4104.
+
+# The options are
+
+# -p	the power to use - default 2
+# -n	the number of powers summed - default 2
+# -f	the minimum number of ways found before reporting it - default 2
+# -l	the least number to consider - default 0
+# -m	the greatest number to consider - default 8192
+
+# Thus
+# 	pow -p 4 -n 3 -f 3 -l 0 -m 1000000
+# gives
+# 	[3] 811538 = 12**4 + 17**4 + 29**4 = 7**4 + 21**4 + 28**4 = 4**4 + 23**4 + 27**4
+
+# ie fourth powers, 3 in each sum, minimum of 3 representations, numbers from 0-1000000.
+
+# [2] 25
+# [3] 325
+# [4] 1105
+# [5] 4225
+# [6] 5525
+# [7] 203125
+# [8] 27625
+# [9] 71825
+# [10] 138125
+# [11] 2640625
+# [12] 160225
+# [13] 17850625
+# [14] 1221025
+# [15] 1795625
+# [16] 801125
+# [18] 2082925
+# [20] 4005625
+# [23] 30525625
+# [24] 5928325
+# [32] 29641625
+
+# [24] 5928325 = 63**2 + 2434**2 = 94**2 + 2433**2 = 207**2 + 2426**2 = 294**2 + 2417**2 = 310**2 + 2415**2 = 465**2 + 2390**2 = 490**2 + 2385**2 = 591**2 + 2362**2 = 690**2 + 2335**2 = 742**2 + 2319**2 = 849**2 + 2282**2 = 878**2 + 2271**2 = 959**2 + 2238**2 = 1039**2 + 2202**2 = 1062**2 + 2191**2 = 1201**2 + 2118**2 = 1215**2 + 2110**2 = 1290**2 + 2065**2 = 1410**2 + 1985**2 = 1454**2 + 1953**2 = 1535**2 + 1890**2 = 1614**2 + 1823**2 = 1633**2 + 1806**2 = 1697**2 + 1746**2
+
+# [32] 29641625 = 67**2 + 5444**2 = 124**2 + 5443**2 = 284**2 + 5437**2 = 320**2 + 5435**2 = 515**2 + 5420**2 = 584**2 + 5413**2 = 835**2 + 5380**2 = 955**2 + 5360**2 = 1180**2 + 5315**2 = 1405**2 + 5260**2 = 1460**2 + 5245**2 = 1648**2 + 5189**2 = 1795**2 + 5140**2 = 1829**2 + 5128**2 = 1979**2 + 5072**2 = 2012**2 + 5059**2 = 2032**2 + 5051**2 = 2245**2 + 4960**2 = 2308**2 + 4931**2 = 2452**2 + 4861**2 = 2560**2 + 4805**2 = 2621**2 + 4772**2 = 2840**2 + 4645**2 = 3005**2 + 4540**2 = 3035**2 + 4520**2 = 3320**2 + 4315**2 = 3365**2 + 4280**2 = 3517**2 + 4156**2 = 3544**2 + 4133**2 = 3664**2 + 4027**2 = 3715**2 + 3980**2 = 3803**2 + 3896**2
+
+# [2] 1729 = 1**3 + 12**3 = 9**3 + 10**3
+# [2] 4104 = 2**3 + 16**3 = 9**3 + 15**3
+# [3] 87539319 = 167**3 + 436**3 = 228**3 + 423**3 = 255**3 + 414**3
+
+# [2] 635318657 = 59**4 + 158**4 = 133**4 + 134**4
+# [2] 3262811042 = 7**4 + 239**4 = 157**4 + 227**4
+# [2] 8657437697 = 193**4 + 292**4 = 256**4 + 257**4
+# [2] 68899596497 = 271**4 + 502**4 = 298**4 + 497**4
+# [2] 86409838577 = 103**4 + 542**4 = 359**4 + 514**4
+# [2] 160961094577 = 222**4 + 631**4 = 503**4 + 558**4
+# [2] 2094447251857 = 76**4 + 1203**4 = 653**4 + 1176**4
+# [2] 4231525221377 = 878**4 + 1381**4 = 997**4 + 1342**4
+# [2] 26033514998417 = 1324**4 + 2189**4 = 1784**4 + 1997**4
+# [2] 37860330087137 = 1042**4 + 2461**4 = 2026**4 + 2141**4
+# [2] 61206381799697 = 248**4 + 2797**4 = 2131**4 + 2524**4
+# [2] 76773963505537 = 1034**4 + 2949**4 = 1797**4 + 2854**4
+# [2] 109737827061041 = 1577**4 + 3190**4 = 2345**4 + 2986**4
+# [2] 155974778565937 = 1623**4 + 3494**4 = 2338**4 + 3351**4
+# [2] 156700232476402 = 661**4 + 3537**4 = 2767**4 + 3147**4
+# [2] 621194785437217 = 2694**4 + 4883**4 = 3966**4 + 4397**4
+# [2] 652057426144337 = 604**4 + 5053**4 = 1283**4 + 5048**4
+# [2] 680914892583617 = 3364**4 + 4849**4 = 4288**4 + 4303**4
+# [2] 1438141494155441 = 2027**4 + 6140**4 = 4840**4 + 5461**4
+# [2] 1919423464573697 = 274**4 + 6619**4 = 5093**4 + 5942**4
+# [2] 2089568089060657 = 498**4 + 6761**4 = 5222**4 + 6057**4
+# [2] 2105144161376801 = 2707**4 + 6730**4 = 3070**4 + 6701**4
+# [2] 3263864585622562 = 1259**4 + 7557**4 = 4661**4 + 7269**4
+# [2] 4063780581008977 = 5181**4 + 7604**4 = 6336**4 + 7037**4
+# [2] 6315669699408737 = 1657**4 + 8912**4 = 7432**4 + 7559**4
+# [2] 6884827518602786 = 635**4 + 9109**4 = 3391**4 + 9065**4
+# [2] 7191538859126257 = 4903**4 + 9018**4 = 6842**4 + 8409**4
+# [2] 7331928977565937 = 1104**4 + 9253**4 = 5403**4 + 8972**4
+# [2] 7362748995747617 = 5098**4 + 9043**4 = 6742**4 + 8531**4
+# [2] 7446891977980337 = 1142**4 + 9289**4 = 4946**4 + 9097**4
+# [2] 7532132844821777 = 173**4 + 9316**4 = 4408**4 + 9197**4
+# [2] 7985644522300177 = 6262**4 + 8961**4 = 7234**4 + 8511**4
+
+# 5, 6, 7, 8, 9, 10, 11 none
+
+Btree: adt{
+	sum: big;
+	left: cyclic ref Btree;
+	right: cyclic ref Btree;
+};
+
+Dtree: adt{
+	sum: big;
+	freq: int;
+	lst: list of array of int;
+	left: cyclic ref Dtree;
+	right: cyclic ref Dtree;
+};
+
+nCr(n: int, r: int): int
+{
+	if(r > n-r)
+		r = n-r;
+
+	# f := g := 1;
+	# for(i := 0; i < r; i++){
+	# 	f *= n-i;
+	# 	g *= i+1;
+	# }
+	# return f/g;
+
+	num := array[r] of int;
+	den := array[r] of int;
+	for(i := 0; i < r; i++){
+		num[i] = n-i;
+		den[i] = i+1;
+	}
+	for(i = 0; i < r; i++){
+		for(j := 0; den[i] != 1; j++){
+			if(num[j] == 1)
+				continue;
+			k := hcf(num[j], den[i]);
+			if(k != 1){
+				num[j] /= k;
+				den[i] /= k;
+			}
+		}
+	}
+	f := 1;
+	for(i = 0; i < r; i++)
+		f *= num[i];
+	return f;
+}
+
+nHr(n: int, r: int): int
+{
+	if(n == 0)
+		return 0;
+	return nCr(n+r-1, r);
+}
+
+nSr(n: int, i: int, j: int): int
+{
+	return nHr(j, n)-nHr(i, n);
+	# s := 0;
+	# for(k := i; k < j; k++)
+	# 	s += nHr(k+1, n-1);
+	# return s;
+}
+
+nSrmax(n: int, i: int, m: int): int
+{
+	s := 0;
+	for(k := i; ; k++){
+		s += nHr(k+1, n-1);
+		if(s > m)
+			break;
+	}
+	if(k == i)
+		return i+1;
+	return k;
+}
+
+kth(c: array of int, n: int, i: int, j: int, k: int)
+{
+	l, u: int;
+
+	m := nSr(n, i, j);
+	if(k < 0)
+		k = 0;
+	if(k >= m)
+		k = m-1;
+	p := 0;
+	for(q := 0; q < n; q++){
+		if(q == 0){
+			l = i;
+			u = j-1;
+		}
+		else{
+			l = 0;
+			u = c[q-1];
+		}
+		for(x := l; x <= u; x++){
+			m = nHr(x+1, n-q-1);
+			p += m;
+			if(p > k){
+				p -= m;
+				break;
+			}
+		}
+		c[q] = x;
+	}	
+}
+
+pos(c: array of int, n: int): int
+{
+	p := 0;
+	for(q := 0; q < n; q++)
+		p += nSr(n-q, 0, c[q]);
+	return p;
+}
+
+min(c: array of int, n: int, p: int): big
+{
+	s := big(0);
+	for(i := 0; i < n; i++)
+		s += big(c[i])**p;
+	m := s;
+	for(i = n-1; i > 0; i--){
+		s -= big(c[i])**p;
+		s -= big(c[i-1])**p;
+		c[i]--;
+		c[i-1]++;
+		s += big(c[i-1])**p;
+		if(s < m)
+			m = s;
+	}
+	c[0]--;
+	c[n-1]++;
+	# m--;
+	return m;
+}
+
+hcf(a, b: int): int
+{
+	if(b == 0)
+		return a;
+	for(;;){
+		if(a == 0)
+			break;
+		if(a < b)
+			(a, b) = (b, a);
+		a %= b;
+		# a -= (a/b)*b;
+	}
+	return b;
+}
+
+gcd(l: list of array of int): int
+{
+	g := (hd l)[0];
+	for(; l != nil; l = tl l){
+		d := hd l;
+		n := len d;
+		for(i := 0; i < n; i++)
+			g = hcf(d[i], g);
+	}
+	return g;
+}
+
+adddup(s: big, root: ref Dtree): int
+{
+	n, p, lp: ref Dtree;
+	
+	p = root;
+	while(p != nil){
+		if(s == p.sum)
+			return ++p.freq;
+		lp = p;
+		if(s < p.sum)
+			p = p.left;
+		else
+			p = p.right;
+	}
+	n = ref Dtree(s, 2, nil, nil, nil);
+	if(s < lp.sum)
+		lp.left = n;
+	else
+		lp.right = n;
+	return n.freq;
+}
+
+cp(c: array of int): array of int
+{
+	n := len c;
+	m := 0;
+	for(i := 0; i < n; i++)
+		if(c[i] != 0)
+			m++;
+	nc := array[m] of int;
+	nc[0: ] = c[0: m];
+	return nc;
+}
+
+finddup(s: big, c: array of int, root: ref Dtree, f: int)
+{
+	p: ref Dtree;
+	
+	p = root;
+	while(p != nil){
+		if(s == p.sum){
+			if(p.freq >= f)
+				p.lst = cp(c) :: p.lst;
+			return;
+		}
+		if(s < p.sum)
+			p = p.left;
+		else
+			p = p.right;
+	}
+}
+
+printdup(p: ref Dtree, pow: int, ix: int)
+{
+	if(p == nil)
+		return;
+	printdup(p.left, pow, ix);
+	if((l := p.lst) != nil){
+		if(gcd(l) == 1){
+			min1 := min2 := 16r7fffffff;
+			for(; l != nil; l = tl l){
+				n := len hd l;
+				if(n < min1){
+					min2 = min1;
+					min1 = n;
+				}
+				else if(n < min2)
+					min2 = n;
+			}
+			i := min1+min2-pow;
+			if(i <= ix){
+				sys->print("[%d, %d] %bd", i, p.freq, p.sum);
+				for(l = p.lst; l != nil; l = tl l){
+					d := hd l;
+					n := len d;
+					sys->print(" = ");
+					for(j := n-1; j >= 0; j--){
+						sys->print("%d**%d", d[j], pow);
+						if(j > 0)
+							sys->print(" + ");
+					}
+				}
+				sys->print("\n");
+				if(i < 0){
+					sys->print("****************\n");
+					exit;
+				}
+			}
+		}
+	}
+	printdup(p.right, pow, ix);
+}
+
+addsum(s: big, root: ref Btree, root1: ref Dtree): int
+{
+	n, p, lp: ref Btree;
+	
+	p = root;
+	while(p != nil){
+		if(s == p.sum)
+			return adddup(s, root1);
+		lp = p;
+		if(s < p.sum)
+			p = p.left;
+		else
+			p = p.right;
+	}
+	n = ref Btree(s, nil, nil);
+	if(s < lp.sum)
+		lp.left = n;
+	else
+		lp.right = n;
+	return 1;
+}
+
+oiroot(x: big, p: int): int
+{
+	for(i := 0; ; i++){
+		n := big(i)**p;
+		if(n > x)
+			break;
+	}
+	return i-1;
+}
+
+iroot(x: big, p: int): int
+{
+	m: big;
+
+	if(x == big(0) || x == big(1))
+		return int x;
+	v := x;
+	n := 0;
+	for(i := 32; i > 0; i >>= 1){
+		m = ((big(1)<<i)-big(1))<<i;
+		if((v&m) != big(0)){
+			n += i;
+			v >>= i;
+		}
+	}
+	a := big(1) << (n/p);
+	b := a<<1;
+	while(a < b){
+		m = (a+b+big(1))/big(2);
+		y := m**p;
+		if(y > x)
+			b = m-big(1);
+		else if(y < x)
+			a = m;
+		else
+			a = b = m;
+	}
+	if(a**p <= x && (a+big(1))**p > x)
+		;
+	else{
+		sys->print("fatal: %bd %d -> %bd\n", x, p, a);
+		exit;
+	}
+	return int a;
+}
+
+initval(c: array of int, n: int, p: int, v: int): big
+{
+	for(i := 0; i < n; i++)
+		c[i] = 0;
+	c[0] = v;
+	return big(v)**p;
+}
+
+nxtval(c: array of int, n: int, p: int, s: big): big
+{
+	for(k := n-1; k >= 0; k--){
+		s -= big(c[k])**p;
+		c[k]++;
+		if(k == 0){
+			s += big(c[k])**p;
+			break;
+		}
+		else{
+			if(c[k] <= c[k-1]){
+				s += big(c[k])**p;
+				break;
+			}
+			c[k] = 0;
+		}
+	}
+	return s;
+}
+
+powers(p: int, n: int, f: int, ix: int, lim0: big, lim: big, ch: chan of int, lock: ref Semaphore)
+{
+	root := ref Btree(big(-1), nil, nil);
+	root1 := ref Dtree(big(-1), 0, nil, nil, nil);
+
+	min := max := lim0;
+
+	c := array[n] of int;
+
+	for(;;){
+		imin := iroot((min+big(n-1))/big(n), p);
+		imax := nSrmax(n, imin, MAXNODES);
+		max = big(imax)**p - big(1);
+		while(max <= min){	# could do better
+			imax++;
+			max = big(imax)**p - big(1);
+		}
+		if(max > lim){
+			max = lim;
+			imax = iroot(max, p)+1;
+		}
+
+		if(verbose)
+			sys->print("searching in %d-%d(%bd-%bd)\n", imin, imax, min, max);
+
+		m := mm := 0;
+		maxf := 0;
+		s := initval(c, n, p, imin);
+		for(;;){
+			mm++;
+			if(s >= min && s < max){
+				fr := addsum(s, root, root1);
+				if(fr > maxf)
+					maxf = fr;
+				m++;
+			}
+			s = nxtval(c, n, p, s);
+			if(c[0] == imax)
+				break;
+		}
+
+		root.left = root.right = nil;
+
+		if(maxf >= f){
+			if(verbose)
+				sys->print("finding duplicates\n");
+
+			s = initval(c, n, p, imin);
+			for(;;){
+				if(s >= min && s < max)
+					finddup(s, c, root1, f);
+				s = nxtval(c, n, p, s);
+				if(c[0] == imax)
+					break;
+			}
+
+			if(lock != nil)
+				lock.obtain();
+			printdup(root1, p, ix);
+			if(lock != nil)
+				lock.release();
+
+			root1.left = root1.right = nil;
+		}
+
+		if(verbose)
+			sys->print("%d(%d) nodes searched\n", m, mm);
+
+		if(mm != nSr(n, imin, imax)){
+			sys->print("**fatal**\n");
+			exit;
+		}
+
+		min = max;
+		if(min >= lim)
+			break;
+	}
+	if(ch != nil)
+		ch <-= 0;
+}
+
+usage()
+{
+	sys->print("usage: powers -p power -n number -f frequency -i index -l minimum -m maximum -s procs -v\n");
+	exit;
+}
+
+partition(p: int, n: int, l: big, m: big, s: int): array of big
+{
+	a := array[s+1] of big;
+	a[0] = big(iroot(l, p))**n;
+	a[s] = (big(iroot(m, p))+big(1))**n;
+	nn := a[s]-a[0];
+	q := nn/big(s);
+	r := nn-q*big(s);
+	t := big(0);
+	lb := a[0];
+	for(i := 0; i < s; i++){
+		ub := lb+q;
+		t += r;
+		if(t >= big(s)){
+			ub++;
+			t -= big(s);
+		}
+		a[i+1] = ub;
+		lb = ub;
+	}
+	if(a[s] != a[0]+nn){
+		sys->print("fatal: a[s]\n");
+		exit;
+	}
+	for(i = 0; i < s; i++){
+		# sys->print("%bd %bd\n", a[i], a[i]**p);
+		a[i] = big(iroot(a[i], n))**p;
+	}
+	a[0] = l;
+	a[s] = m;
+	while(a[0] >= a[1]){
+		a[1] = a[0];
+		a = a[1: ];
+		--s;
+	}
+	while(a[s] <= a[s-1]){
+		a[s-1] = a[s];
+		a = a[0: s];
+		--s;
+	}
+	return a;
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	lockm = load Lock Lock->PATH;
+
+	lockm->init();
+	lock := Semaphore.new();
+
+	p := n := f := 2;
+	ix := 1<<30;
+	l := m := big(0);
+	s := 1;
+
+	arg->init(args);
+	while((c := arg->opt()) != 0){
+		case c {
+			'p' =>
+				p = int arg->arg();
+			'n' =>
+				n = int arg->arg();
+			'f' =>
+				f = int arg->arg();
+			'i' =>
+				ix = int arg->arg();
+			'l' =>
+				l = big(arg->arg());
+			'm' =>
+				m = big(arg->arg())+big(1);
+			's' =>
+				s = int arg->arg();
+			'v' =>
+				verbose = 1;
+			* =>
+				usage();
+		}
+	}
+	if(arg->argv() != nil)
+		usage();
+
+	if(p < 2){
+		p = 2;
+		sys->print("setting p = %d\n", p);
+	}
+	if(n < 2){
+		n = 2;
+		sys->print("setting n = %d\n", n);
+	}
+	if(f < 2){
+		f = 2;
+		sys->print("setting f = %d\n", f);
+	}
+	if(l < big(0)){
+		l = big(0);
+		sys->print("setting l = %bd\n", l);
+	}
+	if(m <= big(0)){
+		m = big((1<<13)+1);
+		sys->print("setting m = %bd\n", m-big(1));
+	}
+	if(l >= m)
+		exit;
+
+	if(s <= 1)
+		powers(p, n, f, ix, l, m, nil, nil);
+	else{
+		nproc := 0;
+		ch := chan of int;
+		a := partition(p, n, l, m, s);
+		lb := a[0];
+		for(i := 0; i < s; i++){
+			ub := a[i+1];
+			if(lb < ub){
+				nproc++;
+				spawn powers(p, n, f, ix, lb, ub, ch, lock);
+			}
+			lb = ub;
+		}
+		for( ; nproc != 0; nproc--)
+			<- ch;
+	}
+}
--- /dev/null
+++ b/appl/math/primes.b
@@ -1,0 +1,228 @@
+implement Primes;
+
+#
+# primes starting [ending]
+#
+# Subject to the Lucent Public License 1.02
+#
+
+include "draw.m";
+
+Primes: module
+{
+	init: fn(nil: ref Draw->Context, argl: list of string);
+};
+
+include "sys.m";
+	sys: Sys;
+include "math.m";
+	maths: Math;
+
+bigx: con 9.007199254740992e15;
+pt := array[] of {
+	2,
+	3,
+	5,
+	7,
+	11,
+	13,
+	17,
+	19,
+	23,
+	29,
+	31,
+	37,
+	41,
+	43,
+	47,
+	53,
+	59,
+	61,
+	67,
+	71,
+	73,
+	79,
+	83,
+	89,
+	97,
+	101,
+	103,
+	107,
+	109,
+	113,
+	127,
+	131,
+	137,
+	139,
+	149,
+	151,
+	157,
+	163,
+	167,
+	173,
+	179,
+	181,
+	191,
+	193,
+	197,
+	199,
+	211,
+	223,
+	227,
+	229,
+};
+wheel := array[] of {
+	10.0,
+	2.0,
+	4.0,
+	2.0,
+	4.0,
+	6.0,
+	2.0,
+	6.0,
+	4.0,
+	2.0,
+	4.0,
+	6.0,
+	6.0,
+	2.0,
+	6.0,
+	4.0,
+	2.0,
+	6.0,
+	4.0,
+	6.0,
+	8.0,
+	4.0,
+	2.0,
+	4.0,
+	2.0,
+	4.0,
+	8.0,
+	6.0,
+	4.0,
+	6.0,
+	2.0,
+	4.0,
+	6.0,
+	2.0,
+	6.0,
+	6.0,
+	4.0,
+	2.0,
+	4.0,
+	6.0,
+	2.0,
+	6.0,
+	4.0,
+	2.0,
+	4.0,
+	2.0,
+	10.0,
+	2.0,
+};
+BITS: con 8;
+TABLEN: con 1000;
+table := array[TABLEN] of byte;
+bittab := array[8] of {
+	byte 1,
+	byte 2,
+	byte 4,
+	byte 8,
+	byte 16,
+	byte 32,
+	byte 64,
+	byte 128,
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	maths = load Math Math->PATH;
+
+	if(len args <= 1){
+		sys->fprint(sys->fildes(2), "usage: primes starting [ending]\n");
+		raise "fail:usage";
+	}
+	args = tl args;
+	nn := real hd args;
+	limit := bigx;
+	if(tl args != nil){
+		limit = real hd tl args;
+		if(limit < nn)
+			exit;
+		if(limit > bigx)
+			ouch();
+	}
+	if(nn < 0.0 || nn > bigx)
+		ouch();
+	if(nn == 0.0)
+		nn = 1.0;
+	if(nn < 230.0){
+		for(i := 0; i < len pt; i++){
+			r := real pt[i];
+			if(r < nn)
+				continue;
+			if(r > limit)
+				exit;
+			sys->print("%d\n", pt[i]);
+			if(limit >= bigx)
+				exit;
+		}
+		nn = 230.0;
+	}
+	(t, nil) := maths->modf(nn/2.0);
+	nn = 2.0*real t+1.0;
+	for(;;){
+		# 
+		# clear the sieve table.
+		#  
+		for(i := 0; i < len table; i++)
+			table[i] = byte 0;
+		# 
+		# run the sieve
+		#  
+		v := maths->sqrt(nn+real (TABLEN*BITS));
+		mark(nn, 3);
+		mark(nn, 5);
+		mark(nn, 7);
+		i = 0;
+		for(k := 11.0; k <= v; k += wheel[i]){
+			mark(nn, int k);
+			i++;
+			if(i >= len wheel)
+				i = 0;
+		}
+		# 
+		# now get the primes from the table and print them
+		#  
+		for(i = 0; i < TABLEN*BITS; i += 2){
+			if(int table[i>>3]&int bittab[i&8r7])
+				continue;
+			temp := nn+real i;
+			if(temp > limit)
+				exit;
+			sys->print("%d\n", int temp);
+			if(limit >= bigx)
+				exit;
+		}
+		nn += real (TABLEN*BITS);
+	}
+}
+
+mark(nn: real, k: int)
+{
+	(it1, nil) := maths->modf(nn/real k);
+	j := int (real k*real it1-nn);
+	if(j < 0)
+		j += k;
+	for(; j < len table*BITS; j += k)
+		table[j>>3] |= bittab[j&8r7];
+}
+
+ouch()
+{
+	sys->fprint(sys->fildes(2), "primes: limits exceeded\n");
+	raise "fail:ouch";
+}
+
--- /dev/null
+++ b/appl/math/sieve.b
@@ -1,0 +1,220 @@
+implement Sieve;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+	arg: Arg;
+
+M: con 16*1024*1024;
+N: con 8*M;
+T: con 2*1024*1024;
+
+limit := array[5] of { M, N, 2*N, 3*N, 15*(N/4) };
+
+Sieve: module
+{
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg = load Arg Arg->PATH;
+
+	np := 0;
+	alg := 3;
+	arg->init(argv);
+	while((c := arg->opt()) != 0){
+		case (c){
+			'a' =>
+				alg = int arg->arg();
+		}
+	}
+	if(alg < 0 || alg > 4)
+		alg = 3;
+	lim := limit[alg];
+	argv = arg->argv();
+	if(argv != nil)
+		lim = int hd argv;
+	if(lim < 0 || lim > limit[alg])
+		lim = limit[alg];
+	if(lim < 6){
+		if(lim > 2){
+			sys->print("2\n");
+			np++;
+		}
+		if(lim > 3){
+			sys->print("3\n");
+			np++;
+		}
+	}
+	else{
+		case (alg){
+			0 => np = init0(lim);
+			1 => np = init1(lim);
+			2 => np = init2(lim);
+			3 => np = init3(lim);
+			4 => np = init4(lim);
+		}
+	}
+	sys->print("%d primes < %d\n", np, lim);
+}
+
+init0(lim: int): int
+{
+	p := array[lim] of byte;
+	for(i := 0; i < lim; i++)
+		p[i] = byte 1;
+	p[0] = p[1] = byte 0;
+	np := 0;
+	for(i = 0; i < lim; i++){
+		if(p[i] == byte 1){
+			np++;
+			sys->print("%d\n", i);
+			for(j := i+i; j < lim; j += i)
+				p[j] = byte 0;
+		}
+	}
+	return np;
+}
+
+init1(lim: int): int
+{
+	n := (lim+31)/32;
+	p := array[n] of int;
+	for(i := 0; i < n; i++)
+		p[i] = int 16rffffffff;
+	p[0] = int 16rfffffffc;
+	np := 0;
+	for(i = 0; i < lim; i++){
+		if(p[i>>5] & (1<<(i&31))){
+			np++;
+			sys->print("%d\n", i);
+			for(j := i+i; j < lim; j += i)
+				p[j>>5] &= ~(1<<(j&31));
+		}
+	}
+	return np;
+}
+
+init2(lim: int): int
+{
+	n := ((lim+1)/2+31)/32;
+	p := array[n] of int;
+	for(i := 0; i < n; i++)
+		p[i] = int 16rffffffff;
+	p[0] = int 16rfffffffe;
+	np := 1;
+	sys->print("%d\n", 2);
+	for(i = 1; i < lim; i += 2){
+		k := (i-1)>>1;
+		if(p[k>>5] & (1<<(k&31))){
+			np++;
+			sys->print("%d\n", i);
+			inc := i+i;
+			for(j := i+i+i; j < lim; j += inc){
+				k = (j-1)>>1;
+				p[k>>5] &= ~(1<<(k&31));
+			}
+		}
+	}
+	return np;
+}
+
+init3(lim: int): int
+{
+	n := ((lim+2)/3+31)/32;
+	p := array[n] of int;
+	for(i := 0; i < n; i++)
+		p[i] = int 16rffffffff;
+	p[0] = int 16rfffffffe;
+	np := 2;
+	sys->print("%d\n", 2);
+	sys->print("%d\n", 3);
+	d := 2;
+	for(i = 1; i < lim; i += d){
+		k := (i-1)/3;
+		if(p[k>>5] & (1<<(k&31))){
+			np++;
+			sys->print("%d\n", i);
+			inc := 6*i;
+			for(j := 5*i; j > 0 && j < lim; j += inc){
+				k = (j-1)/3;
+				p[k>>5] &= ~(1<<(k&31));
+			}
+			for(j = 7*i; j > 0 && j < lim; j += inc){
+				k = (j-1)/3;
+				p[k>>5] &= ~(1<<(k&31));
+			}
+		}
+		d = 6-d;
+	}
+	return np;
+}
+
+init4(lim: int): int
+{
+	n := (4*((lim+14)/15)+31)/32;
+	p := array[n] of int;
+	for(i := 0; i < n; i++)
+		p[i] = int 16rffffffff;
+	p[0] = int 16rfffffffe;
+	np := 3;
+	sys->print("%d\n", 2);
+	sys->print("%d\n", 3);
+	sys->print("%d\n", 5);
+	m := -1;
+	d := array[8] of { 6, 4, 2, 4, 2, 4, 6, 2 };
+	for(i = 1; i < lim; i += d[m]){
+		k := (17*(i%30-1))/60+8*(i/30);
+		if(p[k>>5] & (1<<(k&31))){
+			np++;
+			sys->print("%d\n", i);
+			inc := 30*i;
+			for(j := 7*i; j > 0 && j < lim; j += inc){
+				k = (17*(j%30-1))/60+8*(j/30);
+				p[k>>5] &= ~(1<<(k&31));
+			}
+			for(j = 11*i; j > 0 && j < lim; j += inc){
+				k = (17*(j%30-1))/60+8*(j/30);
+				p[k>>5] &= ~(1<<(k&31));
+			}
+			for(j = 13*i; j > 0 && j < lim; j += inc){
+				k = (17*(j%30-1))/60+8*(j/30);
+				p[k>>5] &= ~(1<<(k&31));
+			}
+			for(j = 17*i; j > 0 &&  j < lim; j += inc){
+				k = (17*(j%30-1))/60+8*(j/30);
+				p[k>>5] &= ~(1<<(k&31));
+			}
+			for(j = 19*i; j > 0 && j < lim; j += inc){
+				k = (17*(j%30-1))/60+8*(j/30);
+				p[k>>5] &= ~(1<<(k&31));
+			}
+			for(j = 23*i; j > 0 && j < lim; j += inc){
+				k = (17*(j%30-1))/60+8*(j/30);
+				p[k>>5] &= ~(1<<(k&31));
+			}
+			for(j = 29*i; j > 0 && j < lim; j += inc){
+				k = (17*(j%30-1))/60+8*(j/30);
+				p[k>>5] &= ~(1<<(k&31));
+			}
+			for(j = 31*i; j > 0 && j < lim; j += inc){
+				k = (17*(j%30-1))/60+8*(j/30);
+				p[k>>5] &= ~(1<<(k&31));
+			}
+		}
+		m++;
+		if(m == 8)
+			m = 0;
+	}
+	return np;
+}
+
+init5(lim: int): int
+{
+	# you must be joking
+	lim = 0;
+	return 0;
+}
--- /dev/null
+++ b/appl/mkfile
@@ -1,0 +1,20 @@
+<../mkconfig
+
+DIRS=\
+	acme\
+#	alphabet\
+	charon\
+	cmd\
+	collab\
+	demo\
+	ebook\
+	grid\
+	lib\
+	math\
+#	mux\
+	spree\
+	svc\
+	tiny\
+	wm\
+
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/spree/archives.b
@@ -1,0 +1,515 @@
+implement Archives;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "sets.m";
+	sets: Sets;
+	Set, set, A, B, All, None: import sets;
+include "string.m";
+	str: String;
+include "spree.m";
+	spree: Spree;
+	Clique, Member, Attributes, Attribute, Object: import spree;
+	MAXPLAYERS: import Spree;
+
+stderr: ref Sys->FD;
+
+Qc: con " \t{}=\n";
+Saveinfo: adt {
+	clique: ref Clique;
+	idmap: array of int;		# map clique id to archive id
+	memberids:	Set;			# set of member ids to archive
+};
+
+Error: exception(string);
+
+Cliqueparse: adt {
+	iob:		ref Iobuf;
+	line:		int;
+	filename:	string;
+	lasttok:	int;
+	errstr:	string;
+
+	gettok:	fn(gp: self ref Cliqueparse): (int, string) raises (Error);
+	lgettok:	fn(gp: self ref Cliqueparse, t: int): string raises (Error);
+	getline:	fn(gp: self ref Cliqueparse): list of string raises (Error);
+	error:	fn(gp: self ref Cliqueparse, e: string) raises (Error);
+};
+
+WORD: con 16rff;
+
+init(cliquemod: Spree)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->fprint(stderr, "cliquearchive: cannot load %s: %r\n", Bufio->PATH);
+		raise "fail:bad module";
+	}
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->fprint(stderr, "cliquearchive: cannot load %s: %r\n", Sets->PATH);
+		raise "fail:bad module";
+	}
+	str = load String String->PATH;
+	if (str == nil) {
+		sys->fprint(stderr, "cliquearchive: cannot load %s: %r\n", String->PATH);
+		raise "fail:bad module";
+	}
+	sets->init();
+	spree = cliquemod;
+}
+
+write(clique: ref Clique, info: list of (string, string), name: string, memberids: Sets->Set): string
+{
+	sys->print("saveclique, saving %d objects\n", objcount(clique.objects[0]));
+	iob := bufio->create(name, Sys->OWRITE, 8r666);
+	if (iob == nil)
+		return sys->sprint("cannot open %s: %r", name);
+
+	# integrate suspended members with current members
+	# for the archive.
+
+	si := ref Saveinfo(clique, array[memberids.limit()] of int, memberids);
+	members := clique.members();
+	pa := array[len members] of (string, int);
+	for (i := 0; members != nil; members = tl members) {
+		p := hd members;
+		if (memberids.holds(p.id))
+			pa[i++] = (p.name, p.id);
+	}
+	pa = pa[0:i];
+	sortmembers(pa);		# ensure members stay in the same order when rearchived.
+	pl: list of string;
+	for (i = len pa - 1; i >= 0; i--) {
+		si.idmap[pa[i].t1] = i;
+		pl = pa[i].t0 :: pl;
+	}
+	iob.puts(quotedc("session" :: clique.archive.argv, Qc));
+	iob.putc('\n');
+	iob.puts(quotedc("members" :: pl, Qc));
+	iob.putc('\n');
+	il: list of string;
+	for (; info != nil; info = tl info)
+		il = (hd info).t0 :: (hd info).t1 :: il;
+	iob.puts(quotedc("info" :: il, Qc));
+	iob.putc('\n');
+	writeobject(iob, 0, si, clique.objects[0]);
+	iob.close();
+	return nil;
+}
+
+writeobject(iob: ref Iobuf, depth: int, si: ref Saveinfo, obj: ref Object)
+{
+	indent(iob, depth);
+	iob.puts(quotedc(obj.objtype :: nil, Qc));
+	iob.putc(' ');
+	iob.puts(mapset(si, obj.visibility).str());
+	writeattrs(iob, si, obj);
+	if (len obj.children > 0) {
+		iob.puts(" {\n");
+		for (i := 0; i < len obj.children; i++)
+			writeobject(iob, depth + 1, si, obj.children[i]);
+		indent(iob, depth);
+		iob.puts("}\n");
+	} else
+		iob.putc('\n');
+}
+
+writeattrs(iob: ref Iobuf, si: ref Saveinfo, obj: ref Object)
+{
+	a := obj.attrs.a;
+	n := 0;
+	for (i := 0; i < len a; i++)
+		n += len a[i];
+	attrs := array[n] of ref Attribute;
+	j := 0;
+	for (i = 0; i < len a; i++)
+		for (l := a[i]; l != nil; l = tl l)
+			attrs[j++] = hd l;
+	sortattrs(attrs);
+	for (i = 0; i < len attrs; i++) {
+		attr := attrs[i];
+		if (attr.val == nil)
+			continue;
+		iob.putc(' ');
+		iob.puts(quotedc(attr.name :: nil, Qc));
+		vis := mapset(si, attr.visibility);
+		if (!vis.eq(All))
+			iob.puts("{" + vis.str() + "}");
+		iob.putc('=');
+		iob.puts(quotedc(attr.val :: nil, Qc));
+	}
+}
+
+mapset(si: ref Saveinfo, s: Set): Set
+{
+	idmap := si.idmap;
+	m := s.msb() != 0;
+	limit := si.memberids.limit();
+	r := None;
+	for (i := 0; i < limit; i++)
+		if (m == !s.holds(i))
+			r = r.add(idmap[i]);
+	if (m)
+		r = All.X(A&~B, r);
+	return r;
+}
+
+readheader(filename: string): (ref Archive, string)
+{
+	iob := bufio->open(filename, Sys->OREAD);
+	if (iob == nil)
+		return (nil, sys->sprint("cannot open '%s': %r", filename));
+	gp := ref Cliqueparse(iob, 1, filename, Bufio->EOF, nil);
+
+	{
+		line := gp.getline();
+		if (len line < 2 || hd line != "session")
+			gp.error("expected 'session' line, got " + str->quoted(line));
+		argv := tl line;
+		line = gp.getline();
+		if (line == nil || tl line == nil || hd line != "members")
+			gp.error("expected 'members' line");
+		members := l2a(tl line);
+		line = gp.getline();
+		if (line == nil || hd line != "info")
+			gp.error("expected 'info' line");
+		if (len tl line % 2 != 0)
+			gp.error("'info' line must have an even number of fields");
+		info: list of (string, string);
+		for (line = tl line; line != nil; line = tl tl line)
+			info = (hd line, hd tl line) :: info;
+		arch := ref Archive(argv, members, info, nil);
+		iob.close();
+		return (arch, nil);
+	} exception e {
+	Error =>
+		return (nil, x := e);
+	}
+}
+
+read(filename: string): (ref Archive, string)
+{
+	iob := bufio->open(filename, Sys->OREAD);
+	if (iob == nil)
+		return (nil, sys->sprint("cannot open '%s': %r", filename));
+	gp := ref Cliqueparse(iob, 1, filename, Bufio->EOF, nil);
+
+	{
+		line := gp.getline();
+		if (len line < 2 || hd line != "session")
+			gp.error("expected 'session' line, got " + str->quoted(line));
+		argv := tl line;
+		line = gp.getline();
+		if (line == nil || tl line == nil || hd line != "members")
+			gp.error("expected 'members' line");
+		members := l2a(tl line);
+		line = gp.getline();
+		if (line == nil || hd line != "info")
+			gp.error("expected 'info' line");
+		if (len tl line % 2 != 0)
+			gp.error("'info' line must have an even number of fields");
+		info: list of (string, string);
+		for (line = tl line; line != nil; line = tl tl line)
+			info = (hd line, hd tl line) :: info;
+		root := readobject(gp);
+		if (root == nil)
+			return (nil, filename + ": no root object found");
+		n := objcount(root);
+		arch := ref Archive(argv, members, info, array[n] of ref Object);
+		arch.objects[0] = root;
+		root.parentid = -1;
+		root.id = 0;
+		allocobjects(root, arch.objects, 1);
+		iob.close();
+		return (arch, nil);
+	} exception e {
+	Error =>
+		return (nil, x := e);
+	}
+}
+
+allocobjects(parent: ref Object, objects: array of ref Object, n: int): int
+{
+	base := n;
+	children := parent.children;
+	objects[n:] = children;
+	n += len children;
+	for (i := 0; i < len children; i++) {
+		child := children[i];
+		(child.id, child.parentid) = (base + i, parent.id);
+		n = allocobjects(child, objects, n);
+	}
+	return n;
+}
+
+objcount(o: ref Object): int
+{
+	n := 1;
+	a := o.children;
+	for (i := 0; i < len a; i++)
+		n += objcount(a[i]);
+	return n;
+}
+
+readobject(gp: ref Cliqueparse): ref Object raises (Error)
+{
+	{
+		# object format:
+		# objtype visibility [attr[{vis}]=val]... [{\nchildren\n}]\n
+		(t, s) := gp.gettok();			#{
+		if (t == Bufio->EOF || t == '}')
+			return nil;
+		if (t != WORD)
+			gp.error("expected WORD");
+		objtype := s;
+		vis := sets->str2set(gp.lgettok(WORD));
+		attrs := Attributes.new();
+		objs: array of ref Object;
+	loop:	for (;;) {
+			(t, s) = gp.gettok();
+			case t {
+			WORD =>
+				attr := s;
+				attrvis := All;
+				(t, s) = gp.gettok();
+				if (t == '{') {		#}
+					attrvis = sets->str2set(gp.lgettok(WORD));	#{
+					gp.lgettok('}');
+					gp.lgettok('=');
+				} else if (t != '=')
+					gp.error("expected '='");
+				val := gp.lgettok(WORD);
+				attrs.set(attr, val, attrvis);
+			'{' =>		#}
+				gp.lgettok('\n');
+				objl: list of ref Object;
+				while ((obj := readobject(gp)) != nil)
+					objl = obj :: objl;
+				n := len objl;
+				objs = array[n] of ref Object;
+				for (n--; n >= 0; n--)
+					(objs[n], objl) = (hd objl, tl objl);
+				gp.lgettok('\n');
+				break loop;
+			'\n' =>
+				break loop;
+			* =>
+				gp.error("expected WORD or '{'");	#}
+			}
+		}
+		return ref Object(-1, attrs, vis, -1, objs, -1, objtype);
+	} exception e {Error => raise e;}
+}
+
+Cliqueparse.error(gp: self ref Cliqueparse, e: string) raises (Error)
+{
+	raise Error(sys->sprint("%s:%d: parse error after %s: %s", gp.filename, gp.line,
+			tok2str(gp.lasttok), e));
+}
+
+Cliqueparse.getline(gp: self ref Cliqueparse): list of string raises (Error)
+{
+	{
+		line, nline: list of string;
+		for (;;) {
+			(t, s) := gp.gettok();
+			if (t == '\n')
+				break;
+			if (t != WORD)
+				gp.error("expected a WORD");
+			line = s :: line;
+		}
+		for (; line != nil; line = tl line)
+			nline = hd line :: nline;
+		return nline;
+	} exception e {Error => raise e;}
+}
+
+# get a token, which must be of type t.
+Cliqueparse.lgettok(gp: self ref Cliqueparse, mustbe: int): string raises (Error)
+{
+	{
+		(t, s) := gp.gettok();
+		if (t != mustbe)
+			gp.error("lgettok expected " + tok2str(mustbe));
+		return s;
+	} exception e {Error => raise e;}
+
+}
+
+Cliqueparse.gettok(gp: self ref Cliqueparse): (int, string) raises (Error)
+{
+	{
+		iob := gp.iob;
+		while ((c := iob.getc()) == ' ' || c == '\t')
+			;
+		t: int;
+		s: string;
+		case c {
+		Bufio->EOF or
+		Bufio->ERROR =>
+			t = Bufio->EOF;
+		'\n' =>
+			gp.line++;
+			t = '\n';
+		'{' =>
+			t = '{';
+		'}' =>
+			t = '}';
+		'=' =>
+			t = '=';
+		'\'' =>
+			for(;;) {
+				while ((nc := iob.getc()) != '\'' && nc >= 0) {
+					s[len s] = nc;
+					if (nc == '\n')
+						gp.line++;
+				}
+				if (nc == Bufio->EOF || nc == Bufio->ERROR)
+					gp.error("unterminated quote");
+				if (iob.getc() != '\'') {
+					iob.ungetc();
+					break;
+				}
+				s[len s] = '\'';	# 'xxx''yyy' becomes WORD(xxx'yyy)
+			}
+			t = WORD;
+		* =>
+			do {
+				s[len s] = c;
+				c = iob.getc();
+				if (in(c, Qc)) {
+					iob.ungetc();
+					break;
+				}
+			} while (c >= 0);
+			t = WORD;
+		}
+		gp.lasttok = t;
+		return (t, s);
+	} exception e {Error => raise e;}
+}
+
+tok2str(t: int): string
+{
+	case t {
+	Bufio->EOF =>
+		return "EOF";
+	WORD =>
+		return "WORD";
+	'\n' =>
+		return "'\\n'";
+	* =>
+		return sys->sprint("'%c'", t);
+	}
+}
+
+# stolen from lib/string.b - should be part of interface in string.m
+quotedc(argv: list of string, cl: string): string
+{
+	s := "";
+	while (argv != nil) {
+		arg := hd argv;
+		for (i := 0; i < len arg; i++) {
+			c := arg[i];
+			if (c == ' ' || c == '\t' || c == '\n' || c == '\'' || in(c, cl))
+				break;
+		}
+		if (i < len arg || arg == nil) {
+			s += "'" + arg[0:i];
+			for (; i < len arg; i++) {
+				if (arg[i] == '\'')
+					s[len s] = '\'';
+				s[len s] = arg[i];
+			}
+			s[len s] = '\'';
+		} else
+			s += arg;
+		if (tl argv != nil)
+			s[len s] = ' ';
+		argv = tl argv;
+	}
+	return s;
+}
+
+in(c: int, cl: string): int
+{
+	n := len cl;
+	for (i := 0; i < n; i++)
+		if (cl[i] == c)
+			return 1;
+	return 0;
+}
+
+indent(iob: ref Iobuf, depth: int)
+{
+	for (i := 0; i < depth; i++)
+		iob.putc('\t');
+}
+
+sortmembers(p: array of (string, int))
+{
+	membermergesort(p, array[len p] of (string, int));
+}
+
+membermergesort(a, b: array of (string, int))
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		membermergesort(a[0:m], b[0:m]);
+		membermergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (b[i].t1 > b[j].t1)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+sortattrs(a: array of ref Attribute)
+{
+	attrmergesort(a, array[len a] of ref Attribute);
+}
+
+attrmergesort(a, b: array of ref Attribute)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		attrmergesort(a[0:m], b[0:m]);
+		attrmergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (b[i].name > b[j].name)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+l2a(l: list of string): array of string
+{
+	n := len l;
+	a := array[n] of string;
+	for (i := 0; i < n; i++)
+		(a[i], l) = (hd l, tl l);
+	return a;
+}
--- /dev/null
+++ b/appl/spree/clients/bounce.b
@@ -1,0 +1,958 @@
+implement Clientmod;
+
+# bouncing balls demo.  it uses tk and multiple processes to animate a
+# number of balls bouncing around the screen.  each ball has its own
+# process; CPU time is doled out fairly to each process by using
+# a central monitor loop.
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Point, Rect, Image: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "math.m";
+	math: Math;
+include "rand.m";
+include "../client.m";
+
+BALLSIZE: con 5;
+ZERO: con 1e-6;
+π: con Math->Pi;
+Maxδ: con π / 4.0;			# max bat angle deflection
+
+Line: adt {
+	p, v:		Realpoint;
+	s:		real;
+	new:			fn(p1, p2: Point): ref Line;
+	hittest:		fn(l: self ref Line, p: Point): (Realpoint, real, real);
+	intersection:	fn(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real);
+	point:		fn(b: self ref Line, s: real): Point;
+};
+
+Realpoint: adt {
+	x, y: real;
+};
+
+cliquecmds := array[] of {
+"canvas .c -bg black",
+"bind .c <ButtonRelease-1> {send mouse 0 1 %x %y}",
+"bind .c <ButtonRelease-2> {send mouse 0 2 %x %y}",
+"bind .c <Button-1> {send mouse 1 1 %x %y}",
+"bind .c <Button-2> {send mouse 1 2 %x %y}",
+"bind . <Key-b> {send ucmd newball}",
+"bind . <ButtonRelease-1> {focus .}",
+"bind .Wm_t <ButtonRelease-1> +{focus .}",
+"focus .",
+"bind .c <Key-b> {send ucmd newball}",
+"bind .c <Key-u> {grab release .c}",
+"frame .f",
+"button .f.b -text {Start} -command {send ucmd start}",
+"button .f.n -text {New ball} -command {send ucmd newball}",
+"pack .f.b .f.n -side left",
+"pack .f -fill x",
+"pack .c -fill both -expand 1",
+"update",
+};
+
+Ballstate: adt {
+	owner: int;		# index into member array
+	hitobs: ref Obstacle;
+	t0: int;
+	p, v: Realpoint;
+	speed: real;
+};
+
+Queue: adt {
+	h, t: list of T; 
+	put: fn(q: self ref Queue, s: T);
+	get: fn(q: self ref Queue): T;
+};
+
+
+Obstacle: adt {
+	line: 		ref Line;
+	id: 		int;
+	isbat: 	int;
+	s1, s2: 	real;
+	srvid:	int;
+	owner:	int;
+	new: 	fn(id: int): ref Obstacle;
+	config: 	fn(b: self ref Obstacle);
+};
+
+Object: adt {
+	obstacle: ref Obstacle;
+	ballctl: chan of ref Ballstate;
+};
+
+
+Member: adt {
+	id: int;
+	colour: string;
+};
+
+win: ref Tk->Toplevel;
+
+lines: list of ref Obstacle;
+lineversion := 0;
+memberid: int;
+myturn: int;
+stderr: ref Sys->FD;
+timeoffset := 0;
+
+objects: array of ref Object;
+srvobjects: array of ref Obstacle;	# all for lasthit...
+members: array of ref Member;
+
+CORNER: con 60;
+INSET: con 20;
+WIDTH: con 500;
+HEIGHT: con 500;
+
+bats: list of ref Obstacle;
+mkball: chan of (int, chan of chan of ref Ballstate);
+cliquefd: ref Sys->FD;
+currentlydragging := -1;
+Ballexit: ref Ballstate;
+Noobs: ref Obstacle;
+
+nomod(s: string)
+{
+	sys->fprint(stderr, "bounce: cannot load %s: %r\n", s);
+	sys->raise("fail:bad module");
+}
+
+client(ctxt: ref Draw->Context, argv: list of string, nil: int)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	math = load Math Math->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		nomod(Tkclient->PATH);
+	tkclient->init();
+	cliquefd = sys->fildes(0);
+	Ballexit = ref Ballstate;
+	Noobs = Obstacle.new(-1);
+	lines = tl lines;		# XXX ahem.
+
+	if (len argv >= 3)		# argv: modname mnt dir ...
+		membername = readfile(hd tl argv + "/name");
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	wmctl: chan of string;
+	(win, wmctl) = tkclient->toplevel(ctxt.screen, nil, "Bounce", 0);
+	ucmd := chan of string;
+	tk->namechan(win, ucmd, "ucmd");
+	mouse := chan of string;
+	tk->namechan(win, mouse, "mouse");
+	for (i := 0; i < len cliquecmds; i++)
+		cmd(win, cliquecmds[i]);
+	cmd(win, ".c configure -width 500 -height 500");
+	cmd(win, ".c configure -width [.c cget -actwidth] -height [.c cget -actheight]");
+	imageinit();
+
+	mch := chan of (int, Point);
+
+	spawn mouseproc(mch);
+	mkball = chan of (int, chan of chan of ref Ballstate);
+	spawn monitor(mkball);
+	balls: list of chan of ref Ballstate;
+
+	spawn updateproc();
+	sys->sleep(500);		# wait for things to calm down a little
+	cliquecmd("time " + string sys->millisec());
+
+	buts := 0;
+	for (;;) alt {
+	c := <-wmctl =>
+		if (c == "exit")
+			sys->write(cliquefd, array[0] of byte, 0);
+		tkclient->wmctl(win, c);
+	c := <-mouse =>
+		(nil, toks) := sys->tokenize(c, " ");
+		if ((hd toks)[0] == '1')
+			buts |= int hd tl toks;
+		else
+			buts &= ~int hd tl toks;
+		mch <-= (buts, Point(int hd tl tl toks, int hd tl tl tl toks));
+	c := <-ucmd =>
+		cliquecmd(c);
+	}
+}
+
+cliquecmd(s: string): int
+{
+	if (sys->fprint(cliquefd, "%s\n", s) == -1) {
+		err := sys->sprint("%r");
+		notify(err);
+		sys->print("bounce: cmd error on '%s': %s\n", s, err);
+		return 0;
+	}
+	return 1;
+}
+
+updateproc()
+{
+	wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+	spawn updateproc1();
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(wfd, buf, len buf);
+	sys->print("updateproc process exited: %s\n", string buf[0:n]);
+}
+
+updateproc1()
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(cliquefd, buf, len buf)) > 0) {
+		(nil, lines) := sys->tokenize(string buf[0:n], "\n");
+		for (; lines != nil; lines = tl lines)
+			applyupdate(hd lines);
+		cmd(win, "update");
+	}
+	if (n < 0)
+		sys->fprint(stderr, "bounce: error reading updates: %r\n");
+	sys->fprint(stderr, "bounce: updateproc exiting\n");
+}
+
+UNKNOWN, BALL, OBSTACLE: con iota;
+
+applyupdate(s: string)
+{
+#	sys->print("bounce: got update %s\n", s);
+	(nt, toks) := sys->tokenize(s, " ");
+	case hd toks {
+	"create" =>
+		# create id parentid vis type
+		id := int hd tl toks;
+		if (id >= len objects) {
+			newobjects := array[id + 10] of ref Object;
+			newobjects[0:] = objects;
+			objects = newobjects;
+		}
+		objects[id] = ref Object;
+	"del" =>
+		# del parent start end objid...
+		for (toks = tl tl tl tl toks; toks != nil; toks = tl toks) {
+			id := int hd toks;
+			if (objects[id].obstacle != nil)
+				sys->fprint(stderr, "bounce: cannot delete obstructions yet\n");
+			else
+				objects[id].ballctl <-= Ballexit;
+			objects[id] = nil;
+		}
+	"set" =>
+		# set obj attr val
+		id := int hd tl toks;
+		attr := hd tl tl toks;
+		val := tl tl tl toks;
+		case attr {
+		"state" =>
+			# state lasthit owner p.x p.y v.x v.y s time
+			state := ref Ballstate;
+			(state.hitobs, val) = (srvobj(int hd val), tl val);
+			(state.owner, val) = (int hd val, tl val);
+			(state.p.x, val) = (real hd val, tl val);
+			(state.p.y, val) = (real hd val, tl val);
+			(state.v.x, val) = (real hd val, tl val);
+			(state.v.y, val) = (real hd val, tl val);
+			(state.speed, val) = (real hd val, tl val);
+			(state.t0, val) = (int hd val, tl val);
+			if (objects[id].ballctl == nil)
+				objects[id].ballctl = makeball(id, state);
+			else
+				objects[id].ballctl <-= state;
+		"pos" or "coords" or "owner" or "id" =>
+			if (objects[id].obstacle == nil)
+				objects[id].obstacle = Obstacle.new(id);
+			o := objects[id].obstacle;
+			case attr {
+			"pos" =>
+				(o.s1, val) = (real hd val, tl val);
+				(o.s2, val) = (real hd val, tl val);
+				o.isbat = 1;
+			"coords" =>
+				p1, p2: Point;
+				(p1.x, val) = (int hd val, tl val);
+				(p1.y, val) = (int hd val, tl val);
+				(p2.x, val) = (int hd val, tl val);
+				(p2.y, val) = (int hd val, tl val);
+				o.line = Line.new(p1, p2);
+			"owner" =>
+				o.owner = hd val;
+				if (o.owner == membername)
+					bats = o :: bats;
+			"id" =>
+				o.srvid = int hd val;
+				if (o.srvid >= len srvobjects) {
+					newobjects := array[id + 10] of ref Obstacle;
+					newobjects[0:] = srvobjects;
+					srvobjects = newobjects;
+				}
+				srvobjects[o.srvid] = o;
+			}
+			if (currentlydragging != id)
+				o.config();
+		"arenasize" =>
+			# arenasize w h
+			cmd(win, ".c configure -width " + hd val + " -height " + hd tl val);
+		* =>
+			if (len attr > 5 && attr[0:5] == "score") {
+				# scoreN val
+				n := int attr[5:];
+				w := ".f." + string n;
+				if (!tkexists(w)) {
+					cmd(win, "label " + w + "l -text '" + attr);
+					cmd(win, "label " + w + " -relief sunken -bd 5 -width 5w");
+					cmd(win, "pack " +w + "l " + w + " -side left");
+				}
+				cmd(win, w + " configure -text {" + hd val + "}");
+			} else if (len attr > 6 && attr[0:6] == "member") {
+				# memberN id colour
+				n := int attr[6:];
+				if (n >= len members) {
+					newmembers := array[n + 1] of ref Member;
+					newmembers[0:] = members;
+					members = newmembers;
+				}
+				p := members[n] = ref Member(int hd val, hd tl val);
+				cmd(win, ".c itemconfigure o" + string p.id + " -fill " + p.colour);
+				if (p.id == memberid)
+					myturn = n;
+			}
+			else
+				sys->fprint(stderr, "bounce: unknown attr '%s'\n", attr);
+		}
+	"time" =>
+		# time offset orig
+		now := sys->millisec();
+		time := int hd tl tl toks;
+		transit := now - time;
+		timeoffset = int hd tl toks - transit / 2;
+		sys->print("transit time %d, timeoffset: %d\n", transit, timeoffset);
+	* =>
+		sys->fprint(stderr, "chat: unknown update message '%s'\n", s);
+	}
+}
+
+tkexists(w: string): int
+{
+	return tk->cmd(win, w + " cget -bd")[0] != '!';
+}
+
+srvobj(id: int): ref Obstacle
+{
+	if (id < 0 || id >= len srvobjects || srvobjects[id] == nil)
+		return Noobs;
+	return srvobjects[id];
+}
+
+mouseproc(mch: chan of (int, Point))
+{
+	procname("mouse");
+	for (;;) {
+		hitbat: ref Obstacle = nil;
+		minperp, hitdist: real;
+		(buts, p) := <-mch;
+		for (bl := bats; bl != nil; bl = tl bl) {
+			b := hd bl;
+			(normal, perp, dist) := b.line.hittest(p);
+			perp = abs(perp);
+			
+			if ((hitbat == nil || perp < minperp) && (dist >= b.s1 && dist <= b.s2))
+				(hitbat, minperp, hitdist) = (b, perp, dist);
+		}
+		if (hitbat == nil || minperp > 30.0) {
+			while ((<-mch).t0)
+				;
+			continue;
+		}
+		offset := hitdist - hitbat.s1;
+		if (buts & 2)
+			(buts, p) = aim(mch, hitbat, p);
+		if (buts & 1)
+			drag(mch, hitbat, offset);
+	}
+}
+
+
+drag(mch: chan of (int, Point), hitbat: ref Obstacle, offset: real)
+{
+	realtosrv := chan of string;
+	dummytosrv := chan of string;
+	tosrv := dummytosrv;
+	currevent := "";
+
+	currentlydragging = hitbat.id;
+
+	line := hitbat.line;
+	batlen := hitbat.s2 - hitbat.s1;
+
+	cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+	spawn sendproc(realtosrv);
+
+	cmd(win, "grab set .c");
+	cmd(win, "focus .");
+loop:	for (;;) alt {
+	tosrv <-= currevent =>
+		tosrv = dummytosrv;
+
+	(buts, p) := <-mch =>
+		if (buts & 2)
+			(buts, p) = aim(mch, hitbat, p);
+		(v, perp, dist) := line.hittest(p);
+		dist -= offset;
+		# constrain bat and mouse positions
+		if (dist < 0.0 || dist + batlen > line.s) {
+			if (dist < 0.0) {
+				p = line.point(offset);
+				dist = 1.0;
+			} else {
+				p = line.point(line.s - batlen + offset);
+				dist = line.s - batlen;
+			}
+			p.x -= int (v.x * perp);
+			p.y -= int (v.y * perp);
+			win.image.display.cursorset(p.add(cvsorigin));
+		}
+		(hitbat.s1, hitbat.s2) = (dist, dist + batlen);
+		hitbat.config();
+		cmd(win, "update");
+		currevent = "bat " + string hitbat.s1;
+		tosrv = realtosrv;
+		if (!buts)
+			break loop;
+	}
+	cmd(win, "grab release .c");
+	realtosrv <-= nil;
+	currentlydragging = -1;
+}
+
+CHARGETIME: con 1000.0;
+MAXCHARGE: con 50.0;
+
+α: con 0.999;		# decay in one millisecond
+D: con 5;
+aim(mch: chan of (int, Point), hitbat: ref Obstacle, p: Point): (int, Point)
+{
+	cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+	startms := ms := sys->millisec();
+	δ := Realpoint(0.0, 0.0);
+	line := hitbat.line;
+	charge := 0.0;
+	pivot := line.point((hitbat.s1 + hitbat.s2) / 2.0);
+	s1 := p2s(line.point(hitbat.s1));
+	s2 := p2s(line.point(hitbat.s2));
+	cmd(win, ".c create line 0 0 0 0 -tags wire -fill yellow");
+	ballid := makeballitem(-1, myturn);
+	bp, p2: Point;
+	buts := 2;
+	for (;;) {
+		v := makeunit(δ);
+		bp = pivot.add((int (v.x * charge), int (v.y * charge)));
+		cmd(win, ".c coords wire "+s1+" "+p2s(bp)+" "+s2);
+		ballmove(ballid, bp);
+		cmd(win, "update");
+		if ((buts & 2) == 0)
+			break;
+		(buts, p2) = <-mch;
+		now := sys->millisec();
+		fade := math->pow(α, real (now - ms));
+		charge = real (now - startms) * (MAXCHARGE / CHARGETIME);
+		if (charge > MAXCHARGE)
+			charge = MAXCHARGE;
+		ms = now;
+		dp := p2.sub(p);
+		δ.x = δ.x * fade + real dp.x;
+		δ.y = δ.y * fade + real dp.y;
+		mag := δ.x * δ.x + δ.y * δ.y;
+		if (dp.x != 0 || dp.y != 0)
+			win.image.display.cursorset(p.add(cvsorigin));
+	}
+	cmd(win, ".c delete wire " + ballid);
+	cmd(win, "update");
+	(δ.x, δ.y) = (-δ.x, -δ.y);
+	cliquecmd("newball " + string hitbat.id + " " +
+		p2s(bp) + " " + rp2s(makeunit(δ)) + " " + string (charge / 100.0));
+	return (buts, p2);
+}
+
+makeunit(v: Realpoint): Realpoint
+{
+	mag := math->sqrt(v.x * v.x + v.y * v.y);
+	if (mag < ZERO)
+		return (1.0, 0.0);
+	return (v.x / mag, v.y / mag);
+}
+
+sendproc(tosrv: chan of string)
+{
+	procname("send");
+	while ((ev := <-tosrv) != nil)
+		cliquecmd(ev);
+}
+
+makeball(id: int, state: ref Ballstate): chan of ref Ballstate
+{
+	mkballreply := chan of chan of ref Ballstate;
+	mkball <-= (id, mkballreply);
+	ballctl := <-mkballreply;
+	ballctl <-= state;
+	return ballctl;
+}
+
+blankobstacle: Obstacle;
+Obstacle.new(id: int): ref Obstacle
+{
+	cmd(win, ".c create line 0 0 0 0 -width 3 -fill #aaaaaa" + " -tags l" + string id);
+	o := ref blankobstacle;
+	o.line = Line.new((0, 0), (0, 0));
+	o.id = id;
+	o.owner = -1;
+	o.srvid = -1;
+	lineversion++;
+	lines = o :: lines;
+	return o;
+}
+
+Obstacle.config(o: self ref Obstacle)
+{
+	if (o.isbat) {
+		cmd(win, ".c coords l" + string o.id + " " +
+			p2s(o.line.point(o.s1)) + " " + p2s(o.line.point(o.s2)));
+		if (o.owner == memberid)
+			cmd(win, ".c itemconfigure l" + string o.id + " -fill red");
+		else
+			cmd(win, ".c itemconfigure l" + string o.id + " -fill white");
+	} else {
+		cmd(win, ".c coords l" + string o.id + " " +
+			p2s(o.line.point(0.0)) + " " + p2s(o.line.point(o.line.s)));
+	}
+}
+	
+# make sure cpu time is handed to all ball processes fairly
+# by passing a "token" around to each process in turn.
+# each process does its work when it *hasn't* got its
+# token but it can't go through two iterations without
+# waiting its turn.
+#
+# new processes are created by sending on mkball.
+# the channel sent back can be used to control the position
+# and velocity of the ball and to destroy it.
+monitor(mkball: chan of (int, chan of chan of ref Ballstate))
+{
+	procname("mon");
+	procl, proc: list of (chan of ref Ballstate, chan of int);
+	rc := dummyrc := chan of int;
+	for (;;) {
+		alt {
+		(id, ch) := <-mkball =>
+			(newc, newrc) := (chan of ref Ballstate, chan of int);
+			procl = (newc, newrc) :: procl;
+			spawn animproc(id, newc, newrc);
+			ch <-= newc;
+			if (tl procl == nil) {		# first ball
+				newc <-= nil;
+				rc = newrc;
+				proc = procl;
+			}
+		alive := <-rc =>					# got token.
+			if (!alive) {
+				# ball has exited: remove from list
+				newprocl: list of (chan of ref Ballstate, chan of int);
+				for (; procl != nil; procl = tl procl)
+					if ((hd procl).t1 != rc)
+						newprocl = hd procl :: newprocl;
+				procl = newprocl;
+			}
+			if ((proc = tl proc) == nil)
+				proc = procl;
+			if (proc == nil) {
+				rc = dummyrc;
+			} else {
+				c: chan of ref Ballstate;
+				(c, rc) = hd proc;
+				c <-= nil;				# hand token to next process.
+			}
+		}
+	}
+}
+
+# buffer ball state commands, so at least balls we handle
+# locally appear glitch free.
+bufferproc(cmdch: chan of string)
+{
+	procname("buffer");
+	buffer := ref Queue;
+	bufhd: string;
+	dummytosrv := chan of string;
+	realtosrv := chan of string;
+	spawn sendproc(realtosrv);
+	tosrv := dummytosrv;
+	for (;;) alt {
+	tosrv <-= bufhd =>
+		if ((bufhd = buffer.get()) == nil)
+			tosrv = dummytosrv;
+	s := <-cmdch =>
+		if (s == nil) {
+			# ignore other queued requests, as they're
+			# only state changes for a ball that's now been deleted.
+			realtosrv <-= nil;
+			exit;
+		}
+		buffer.put(s);
+		if (tosrv == dummytosrv) {
+			tosrv = realtosrv;
+			bufhd = buffer.get();
+		}
+	}
+}
+start: int;
+# animate one ball. initial position and unit-velocity are
+# given by p and v.
+animproc(id: int, c: chan of ref Ballstate, rc: chan of int)
+{
+	procname("anim");
+	while ((newstate := <-c) == nil)
+		rc <-= 1;
+	state := *newstate;
+	totaldist := 0.0;		# distance ball has travelled from reference point to last intersection
+	ballid := makeballitem(id, state.owner);
+	smallcount := 0;
+	version := lineversion;
+	tosrv := chan of string;
+	start := sys->millisec();
+	spawn bufferproc(tosrv);
+loop:	for (;;) {
+		hitp: Realpoint;
+
+		dist := 1000000.0;
+		oldobs := state.hitobs;
+		hitt: real;
+		for (l := lines; l != nil; l = tl l) {
+			obs := hd l;
+			(ok, hp, hdist, t) := obs.line.intersection(state.p, state.v);
+			if (ok && hdist < dist && obs != oldobs && (smallcount < 10 || hdist > 1.5)) {
+				(hitp, state.hitobs, dist, hitt) = (hp, obs, hdist, t);
+			}
+		}
+		if (dist > 10000.0) {
+			sys->print("no intersection!\n");
+			state = ballexit(1, ballid, tosrv, c, rc);
+			totaldist = 0.0;
+			continue loop;
+		}
+		if (dist < 0.0001)
+			smallcount++;
+		else
+			smallcount = 0;
+		t0 := int (totaldist / state.speed) + state.t0 - timeoffset;
+		et := t0 + int (dist / state.speed);
+		t := sys->millisec() - t0;
+		dt := et - t0;
+		do {
+			s := real t * state.speed;
+			currp := Realpoint(state.p.x + s * state.v.x,  state.p.y + s * state.v.y);
+			ballmove(ballid, (int currp.x, int currp.y));
+			cmd(win, "update");
+			if (lineversion > version) {
+				(state.p, state.hitobs, version) = (currp, oldobs, lineversion);
+				totaldist += s;
+				continue loop;
+			}
+			if ((newstate := <-c) != nil) {
+				if (newstate == Ballexit)
+					ballexit(0, ballid, tosrv, c, rc);
+				state = *newstate;
+				totaldist = 0.0;
+				continue loop;
+			}
+			rc <-= 1;
+			t = sys->millisec() - t0;
+		} while (t < dt);
+		totaldist += dist;
+		state.p = hitp;
+		hitobs := state.hitobs;
+		if (hitobs.isbat) {
+			if (hitobs.owner == memberid) {
+				if (hitt >= hitobs.s1 && hitt <= hitobs.s2)
+					state.v = batboing(hitobs, hitt, state.v);
+				tosrv <-= "state " + 
+					string id + 
+					" " + string hitobs.srvid +
+					" " + string state.owner +
+					" " + rp2s(state.p) + " " + rp2s(state.v) +
+					" " + string state.speed +
+					" " + string (sys->millisec() + timeoffset);
+			} else {
+				# wait for enlightenment
+				while ((newstate := <-c) == nil)
+					rc <-= 1;
+				if (newstate == Ballexit)
+					ballexit(0, ballid, tosrv, c, rc);
+				state = *newstate;
+				totaldist = 0.0;
+			}
+		} else if (hitobs.owner == memberid) {
+			# if line has an owner but isn't a bat, then it's
+			# a terminating line, so we inform server.
+			cliquecmd("lost " + string id);
+			state = ballexit(1, ballid, tosrv, c, rc);
+			totaldist = 0.0;
+		} else
+			state.v = boing(state.v, hitobs.line);
+	}
+}
+
+#ballmask: ref Image;
+imageinit()
+{
+#	displ := win.image.display;
+#	ballmask = displ.newimage(((0, 0), (BALLSIZE+1, BALLSIZE+1)), 0, 0, Draw->White);
+#	ballmask.draw(ballmask.r, displ.zeros, displ.ones, (0, 0));
+#	ballmask.fillellipse((BALLSIZE/2, BALLSIZE/2), BALLSIZE/2, BALLSIZE/2, displ.ones,  (0, 0));
+#	End: con Draw->Endsquare;
+#	n := 5;
+#	θ := 0.0;
+#	δ := (2.0 * π) / real n;
+#	c := Point(BALLSIZE / 2, BALLSIZE / 2).sub((1, 1));
+#	r := real (BALLSIZE / 2);
+#	for (i := 0; i < n; i++) {
+#		p2 := Point(int (r * math->cos(θ)), int (r * math->sin(θ)));
+#		sys->print("drawing from %s to %s\n", p2s(c), p2s(p2.add(c)));
+#		ballmask.line(c, c.add(p2), End, End, 1, displ.ones, (0, 0));
+#		θ += δ;
+#	}
+}
+
+makeballitem(id, owner: int): string
+{
+	displ := win.image.display;
+	return cmd(win, ".c create oval 0 0 1 1 -fill " + members[owner].colour +
+			" -tags o" + string owner);
+}
+
+ballmove(ballid: string, p: Point)
+{
+	cmd(win, ".c coords " + ballid +
+		" " + string (p.x - BALLSIZE) +
+		" " + string (p.y - BALLSIZE) +
+		" " + string (p.x + BALLSIZE) +
+		" " + string (p.y + BALLSIZE));
+}
+
+ballexit(wait: int, ballid: string, tosrv: chan of string, c: chan of ref Ballstate, rc: chan of int): Ballstate
+{
+	if (wait) {
+		while ((s := <-c) != Ballexit)
+			if (s == nil)
+				rc <-= 1;
+			else
+				return *s;			# maybe we're not exiting, after all...
+	}
+	cmd(win, ".c delete " + ballid + ";update");
+#	cmd(win, "image delete " + ballid);
+	tosrv <-= nil;
+	<-c;
+	rc <-= 0;		# inform monitor that we've gone
+	exit;
+}
+
+# thread-safe access to the Rand module
+randgenproc(ch: chan of int)
+{
+	procname("rand");
+	rand := load Rand Rand->PATH;
+	for (;;)
+		ch <-= rand->rand(16r7fffffff);
+}
+
+abs(x: real): real
+{
+	if (x < 0.0)
+		return -x;
+	return x;
+}
+
+# bounce ball travelling in direction av off line b.
+# return the new unit vector.
+boing(av: Realpoint, b: ref Line): Realpoint
+{
+	d := math->atan2(b.v.y, b.v.x) * 2.0 - math->atan2(av.y, av.x);
+	return (math->cos(d), math->sin(d));
+}
+
+# calculate how a bounce vector should be modified when
+# hitting a bat. t gives the intersection point on the bat;
+# ballv is the ball's vector.
+batboing(bat: ref Obstacle, t: real, ballv: Realpoint): Realpoint
+{
+	ballθ := math->atan2(ballv.y, ballv.x);
+	batθ := math->atan2(bat.line.v.y, bat.line.v.x);
+	φ := ballθ - batθ;
+	δ: real;
+	t -= bat.s1;
+	batlen := bat.s2 - bat.s1;
+	if (math->sin(φ) > 0.0)
+		δ = (t / batlen) * Maxδ * 2.0 - Maxδ;
+	else
+		δ = (t / batlen) * -Maxδ * 2.0 + Maxδ;
+	θ := math->atan2(bat.line.v.y, bat.line.v.x) * 2.0 - ballθ;	# boing
+	θ += δ;
+	return (math->cos(θ), math->sin(θ));
+}
+
+Line.new(p1, p2: Point): ref Line
+{
+	ln := ref Line;
+	ln.p = (real p1.x, real p1.y);
+	v := Realpoint(real (p2.x - p1.x), real (p2.y - p1.y));
+	ln.s =  math->sqrt(v.x * v.x + v.y * v.y);
+	if (ln.s > ZERO)
+		ln.v = (v.x / ln.s, v.y / ln.s);
+	else
+		ln.v = (1.0, 0.0);
+	return ln;
+}
+
+# return normal from line, perpendicular distance from line and distance down line
+Line.hittest(l: self ref Line, ip: Point): (Realpoint, real, real)
+{
+	p := Realpoint(real ip.x, real ip.y);
+	v := Realpoint(-l.v.y, l.v.x);
+	(nil, nil, perp, ldist) := l.intersection(p, v);
+	return (v, perp, ldist);
+}
+
+Line.point(l: self ref Line, s: real): Point
+{
+	return (int (l.p.x + s * l.v.x), int (l.p.y + s * l.v.y));
+}
+
+# compute the intersection of lines a and b.
+# b is assumed to be fixed, and a is indefinitely long
+# but doesn't extend backwards from its starting point.
+# a is defined by the starting point p and the unit vector v.
+# return whether it hit, the point at which it hit if so,
+# the distance of the intersection point from p,
+# and the distance of the intersection point from b.p.
+Line.intersection(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real)
+{
+	det := b.v.x * v.y - v.x * b.v.y;
+	if (det > -ZERO && det < ZERO)
+		return (0, (0.0, 0.0), 0.0, 0.0);
+
+	y21 := b.p.y - p.y;
+	x21 := b.p.x - p.x;
+	s := (b.v.x * y21 - b.v.y * x21) / det;
+	t := (v.x * y21 - v.y * x21) / det;
+	if (s < 0.0)
+		return (0, (0.0, 0.0), s, t);
+	hit := t >= 0.0 && t <= b.s;
+	hp: Realpoint;
+	if (hit)
+		hp = (p.x+v.x*s, p.y+v.y*s);
+	return (hit, hp, s, t);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->print("tk error %s on '%s'\n", e, s);
+	return e;
+}
+
+state2s(s: ref Ballstate): string
+{
+	return sys->sprint("[hitobs:%d(id %d), t0: %d, p: %g %g; v: %g %g; s: %g",
+		s.hitobs.srvid, s.hitobs.id, s.t0, s.p.x, s.p.y, s.v.x, s.v.y, s.speed);
+}
+
+l2s(l: ref Line): string
+{
+	return p2s(l.point(0.0)) + " " + p2s(l.point(l.s));
+}
+
+rp2s(rp: Realpoint): string
+{
+	return string rp.x + " " + string rp.y;
+}
+
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+notifypid := -1;
+notify(s: string)
+{
+	kill(notifypid);
+	sync := chan of int;
+	spawn notifyproc(s, sync);
+	notifypid = <-sync;
+}
+
+notifyproc(s: string, sync: chan of int)
+{
+	procname("notify");
+	sync <-= sys->pctl(0, nil);
+	cmd(win, ".c delete notify");
+	id := cmd(win, ".c create text 0 0 -anchor nw -fill red -tags notify -text '" + s);
+	bbox := cmd(win, ".c bbox " + id);
+	cmd(win, ".c create rectangle " + bbox + " -fill #ffffaa -tags notify");
+	cmd(win, ".c raise " + id);
+	cmd(win, "update");
+	sys->sleep(750);
+	cmd(win, ".c delete notify");
+	cmd(win, "update");
+	notifypid = -1;
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+		sys->write(fd, array of byte "kill", 4);
+}
+
+T: type string;
+
+Queue.put(q: self ref Queue, s: T)
+{
+	q.t = s :: q.t;
+}
+
+Queue.get(q: self ref Queue): T
+{
+	s: T;
+	if(q.h == nil){
+		q.h = revlist(q.t);
+		q.t = nil;
+	}
+	if(q.h != nil){
+		s = hd q.h;
+		q.h = tl q.h;
+	}
+	return s;
+}
+
+revlist(ls: list of T) : list of T
+{
+	rs: list of T;
+	for (; ls != nil; ls = tl ls)
+		rs = hd ls :: rs;
+	return rs;
+}
+
+procname(s: string)
+{
+#	sys->procname(sys->procname(nil) + " " + s);
+}
+
--- /dev/null
+++ b/appl/spree/clients/cards.b
@@ -1,0 +1,2220 @@
+implement Cards;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "math.m";
+	math: Math;
+
+Cards: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+# fairly general card clique client.
+# inherent restrictions:
+#	no dragging of cards visible over the net; it's unclear how
+#		to handle the coordinate spaces involved
+
+Object: adt {
+	id:				int;
+	pick {
+	Card =>
+		parentid:		int;
+		face:			int;			# 1 is face up
+		number:		int;
+		rear:			int;
+	Member =>
+		cid:			int;
+		name:		string;
+	Stack =>
+		o:			ref Layobject.Stack;
+	Widget =>
+		o:			ref Layobject.Widget;
+	Menuentry =>
+		parentid:		int;
+		text:			string;
+	Layoutframe =>
+		lay:			ref Layout.Frame;
+	Layoutobj =>
+		lay:			ref Layout.Obj;
+	Scoretable =>
+		scores:		array of ref Object.Score;
+	Score =>
+		row:			array of (int, string);
+		height:		int;
+	Button =>
+	Other =>
+	}
+};
+
+# specify how an object is laid out.
+Layout: adt {
+	id:			int;
+	parentid:		int;
+	opts:			string;		# pack options
+	orientation:	int;
+	pick {
+	Frame =>
+		lays:		cyclic array of ref Layout;
+	Obj =>
+		layid:	int;			# reference to layid of laid-out object
+	}
+};
+
+# an object which can be laid out on the canvas
+Layobject: adt {
+	id:			int;
+	parentid:		int;
+	w:			string;
+	size:			Point;
+	needrepack:	int;
+	orientation:	int;
+	layid:		int;
+	pick {
+	Stack =>
+		style:		int;
+		cards:		array of ref Object.Card;	# fake objects when invisible
+		pos:			Point;		# top-left origin of first card in stack
+		delta:		Point;		# card offset delta.
+		animq:		ref Queue;	# queue of pending animations.
+		actions:		int;
+		maxcards:	int;
+		title:			string;
+		visible:		int;
+		n:			int;			# for concealed stacks, n cards in stack.
+		ownerid:		int;			# owner of selection
+		sel:			ref Selection;
+		showsize,
+		hassize:		int;
+	Widget =>
+		wtype:		string;
+		entries:		array of ref Object.Menuentry;
+		cmd:			string;		# only used for entry widgets
+		width:		int;
+	}
+};
+	
+Animation: adt {
+	tag:		string;					# canvas tag common to cards being moved.
+	srcpt:	Point;					# where cards are coming from.
+	cards:	array of ref Object.Card;		# objects being transferred.
+	dstid:	int;
+	index:	int;
+	waitch:	chan of ref Animation;		# notification comes on this chan when finished.
+};
+
+Selection: adt {
+	pick {
+	XRange =>
+		r: Range;
+	Indexes =>
+		idxl: list of int;
+	Empty =>
+	}
+};
+
+MAXPLAYERS: con 4;
+
+# layout actions
+lFRAME, lOBJECT: con iota;
+
+# possible actions on a card on a stack.
+aCLICK: con 1<<iota;
+
+# styles of stack display
+styDISPLAY, styPILE: con iota;
+
+# orientations
+oLEFT, oRIGHT, oUP, oDOWN: con iota;
+
+Range: adt {
+	start, end: int;
+};
+
+T: type ref Animation;
+Queue: adt {
+	h, t: list of T; 
+	put: fn(q: self ref Queue, s: T);
+	get: fn(q: self ref Queue): T;
+	isempty: fn(q: self ref Queue): int;
+	peek: fn(q: self ref Queue): T;
+};
+
+configcmds := array[] of {
+"frame .buts",
+"frame .cf",
+"canvas .c -width 400 -height 450 -bg green",
+"label .status -text 0",
+"checkbutton .buts.scores -text {Show scores} -command {send cmd scores}",
+"button .buts.sizetofit -text {Fit} -command {send cmd sizetofit}",
+"checkbutton .buts.debug -text {Debug} -variable debug -command {send cmd debug}",
+"pack .buts.sizetofit .buts.debug .status -in .buts -side left",
+"pack .buts -side top -fill x",
+"pack  .c -in .cf -side top -fill both -expand 1",
+"pack .cf -side top -fill both -expand 1",
+"bind .c <Button-1> {send cmd b1 %X %Y}",
+"bind .c <ButtonRelease-1} {send cmd b1r %X %Y}",
+"bind .c <Button-2> {send cmd b2 %X %Y}",
+"bind .c <ButtonRelease-2> {send cmd b2r %X %Y}",
+"bind .c <ButtonPress-3> {send cmd b3 %X %Y}",
+"bind .c <ButtonRelease-3> {send cmd b3r %X %Y}",
+"bind . <Configure> {send cmd config}",
+"pack propagate .buts 0",
+".status configure -text {}",
+"pack propagate . 0",
+};
+
+objects: 		array of ref Object;
+layobjects := array[20] of list of ref Layobject;
+members := array[8] of list of ref Object.Member;
+win: 			ref Tk->Toplevel;
+drawctxt:		ref Draw->Context;
+me:			ref Object.Member;
+layout:		ref Layout;
+scoretable:	ref Object.Scoretable;
+showingscores := 0;
+debugging := 0;
+
+stderr:		ref Sys->FD;
+animfinishedch: chan of (ref Animation, chan of chan of ref Animation);
+yieldch:		chan of int;
+cardlockch: 	chan of int;
+notifych:		chan of string;
+tickregisterch, tickunregisterch: chan of chan of int;
+starttime :=	0;
+cvsfont: 		ref Font;
+
+packwin:		ref Tk->Toplevel;	# invisible; used to steal tk's packing algorithms...
+packobjs:		list of ref Layobject;
+repackobjs:	list of ref Layobject;
+needresize := 0;
+needrepack := 0;
+
+animid := 0;
+fakeid := -2;		# ids allocated to "fake" cards in private hands; descending
+nimages := 0;
+Hiddenpos := Point(5000, 5000);
+
+cliquefd: ref Sys->FD;
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	math = load Math Math->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil) {
+		sys->fprint(stderr, "cards: cannot load %s: %r\n", Tkclient->PATH);
+		raise "fail:bad module";
+	}
+	tkclient->init();
+	drawctxt = ctxt;
+	client1();
+}
+
+# maximum number of rears (overridden by actual rear images)
+rearcolours := array[] of {
+	int 16r0000ccff,
+	int 16rff0000ff,
+	int 16rffff00ff,
+	int 16r008000ff,
+	int 16rffffffff,
+	int 16rffaa00ff,
+	int 16r00ffffff,
+	int 16r808080ff,
+	int 16r00ff00ff,
+	int 16r800000ff,
+	int 16r800080ff,
+};
+Rearborder := 3;
+Border := 6;
+Selectborder := 3;
+cardsize: Point;
+carddelta := Point(12, 15);		# offset in order to see card number/suit
+Selectcolour := "red";
+Textfont := "/fonts/pelm/unicode.8.font";
+
+client1()
+{
+	cliquefd = sys->fildes(0);
+	if (readconfig() == -1)
+		raise "fail:error";
+
+	winctl: chan of string;
+	(win, winctl) = tkclient->toplevel(drawctxt, "-font " + Textfont,
+		"Cards", Tkclient->Appl);
+	cmd(win, ". unmap");
+	bcmd := chan of string;
+	tk->namechan(win, bcmd, "cmd");
+	srvcmd := chan of string;
+	tk->namechan(win, srvcmd, "srv");
+
+	if (readcardimages() == -1)
+		raise "fail:error";
+	for (i := 0; i < len configcmds; i++)
+		cmd(win, configcmds[i]);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+	fontname := cmd(win, ".c cget -font");
+	cvsfont = Font.open(drawctxt.display, fontname);
+	if (cvsfont == nil) {
+		sys->fprint(stderr, "cards: cannot open font %s: %r\n", fontname);
+		raise "fail:error";
+	}
+	fontname = nil;
+
+	cardlockch = chan of int;
+	spawn lockproc();
+
+	yieldch = chan of int;
+	spawn yieldproc();
+
+	notifych = chan of string;
+	spawn notifierproc();
+
+	updatech := chan of array of byte;
+	spawn readproc(cliquefd, updatech);
+
+	spawn updateproc(updatech);
+	b1down := 0;
+
+	tickregisterch = chan of chan of int;
+	tickunregisterch = chan of chan of int;
+	spawn timeproc();
+	spawn eventproc(win);
+
+	for (;;) alt {
+	c := <-bcmd =>
+		(n, toks) := sys->tokenize(c, " ");
+		case hd toks {
+		"b3" =>
+			curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+				int cmd(win, ".c canvasy " + hd tl tl toks));
+			b3raise(bcmd, curp);
+		"b2" =>
+			curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+				int cmd(win, ".c canvasy " + hd tl tl toks));
+			dopan(bcmd, "b2", curp);
+		"b1" =>
+			if (!b1down) {
+				# b1 x y
+				# x and y in screen coords
+				curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+					int cmd(win, ".c canvasy " + hd tl tl toks));
+				b1down = b1action(bcmd, curp);
+			}
+		"b1r" =>
+			b1down = 0;
+		"entry" =>
+			id := int hd tl toks;
+			lock();
+			cc := "";
+			pick o := objects[id] {
+			Widget =>
+				cc = o.o.cmd;
+			* =>
+				sys->print("entry message from unknown obj: id %d\n", id);
+			}
+			unlock();
+			if (cc != nil) {
+				w := ".buts." + string id + ".b";
+				s := cmd(win, w + " get");
+				cardscmd(cc + " " + s);
+				cmd(win, w + " selection range 0 end");
+				cmd(win, "update");
+			}
+		"config" =>
+			lock();
+			needresize = 1;
+			updatearena();
+			unlock();
+			cmd(win, "update");
+		"scores" =>
+			if (scoretable == nil)
+				break;
+			if (!showingscores) {
+				cmd(win, ".c move score " + string -Hiddenpos.x + " " + string -Hiddenpos.y);
+				cmd(win, ".c raise score");
+			} else
+				cmd(win, ".c move score " + p2s(Hiddenpos));
+			cmd(win, "update");
+			showingscores = !showingscores;
+		"sizetofit" =>
+			lock();
+			sizetofit();
+			unlock();
+			cmd(win, "update");
+		"debug" =>
+			debugging = int cmd(win, "variable debug");
+		}
+	c := <-srvcmd =>		# from button or menu entry
+		cardscmd(c);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-winctl =>
+		if (s == "exit")
+			sys->write(cliquefd, array[0] of byte, 0);
+		tkclient->wmctl(win, s);
+	}
+}
+
+eventproc(win: ref Tk->Toplevel)
+{
+	for(;;)alt{
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	}
+}
+
+readproc(fd: ref Sys->FD, updatech: chan of array of byte)
+{
+	buf := rest := array[Sys->ATOMICIO * 2] of byte;
+	while ((n := sys->read(fd, rest, Sys->ATOMICIO)) > 0) {
+		updatech <-= rest[0:n];
+		rest = rest[n:];
+		if (len rest < Sys->ATOMICIO)
+			buf = rest = array[Sys->ATOMICIO * 2] of byte;
+	}
+	updatech <-= nil;
+}
+
+
+b1action(bcmd: chan of string, p: Point): int
+{
+	(hitsomething, id) := hitcard(p);
+	if (!hitsomething) {
+		dopan(bcmd, "b1", p);
+		return 0;
+	}
+	if (id < 0) {		# either error, or someone else's private card
+		sys->print("no card hit (%d)\n", id);
+		return 1;
+	}
+	lock();
+	if (objects[id] == nil) {
+		notify("it's gone");
+		unlock();
+		return 1;
+	}
+	stack: ref Layobject.Stack;
+	index := -1;
+	pick o := objects[id] {
+	Card =>
+		card := o;
+		parentid := card.parentid;
+		stack = stackobj(parentid);
+		for (index = 0; index < len stack.cards; index++)
+			if (stack.cards[index] == card)
+				break;
+		if (index == len stack.cards)
+			index = -1;
+	Stack =>
+		stack = o.o;
+	* =>
+		unlock();
+		return 1;
+	}
+	actions := stack.actions;
+	stackid := stack.id;
+	unlock();
+	# XXX potential problems when object ids get reused.
+	# the object id that we saw before the unlock()
+	# might now refer to a different object, so the user
+	# might be performing a different action to the one intended.
+	# this should be changed throughout... hmm.
+	if (actions == 0) {
+		notify("no way josé");
+		sys->print("no way: stack %d, actions %d\n", stackid, actions);
+		return 1;
+	}
+	cardscmd("click " + string stackid + " " + string index);
+	return 1;
+}
+
+dopan(bcmd: chan of string, b: string, p: Point)
+{
+	r := b + "r";
+	for (;;) {
+		(n, toks) := sys->tokenize(<-bcmd, " ");
+		if (hd toks == b) {
+			pan(p, (int hd tl toks, int hd tl tl toks));
+			p = Point(int cmd(win, ".c canvasx " + hd tl toks),
+				int cmd(win, ".c canvasy " + hd tl tl toks));
+			cmd(win, "update");
+		} else if (hd toks == r)
+			return;
+	}
+}
+
+b3raise(bcmd: chan of string, p: Point)
+{
+	currcard := -1;
+	above := "";
+loop:	for (;;) {
+		(nil, id) := hitcard(p);
+		if (id != currcard) {
+			if (currcard != -1 && above != nil)
+				cmd(win, ".c lower i" + string currcard + " " + above);
+			if (id == -1 || tagof(objects[id]) != tagof(Object.Card)) {
+				above = nil;
+				currcard = -1;
+			} else {
+				above = cmd(win, ".c find above i" + string id);
+				cmd(win, ".c raise i" + string id);
+				cmd(win, "update");
+				currcard = id;
+			}
+		}
+		(nil, toks) := sys->tokenize(<-bcmd, " ");
+		case hd toks {
+		"b3" =>
+			p = Point(int cmd(win, ".c canvasx " + hd tl toks),
+				int cmd(win, ".c canvasy " + hd tl tl toks));
+		"b3r" =>
+			break loop;
+		}
+	}
+	if (currcard != -1 && above != nil) {
+		cmd(win, ".c lower i" + string currcard + " " + above);
+		cmd(win, "update");
+	}
+}
+
+hitcard(p: Point): (int, int)
+{
+	(nil, hitids) := sys->tokenize(cmd(win, ".c find overlapping " + r2s((p, p))), " ");
+	if (hitids == nil)
+		return (0, -1);
+	ids: list of string;
+	for (; hitids != nil; hitids = tl hitids)
+		ids = hd hitids :: ids;
+	for (; ids != nil; ids = tl ids) {
+		(nil, tags) := sys->tokenize(cmd(win, ".c gettags " + hd ids), " ");
+		for (; tags != nil; tags = tl tags) {
+			tag := hd tags;
+			if (tag[0] == 'i' || tag[0] == 'r' || tag[0] == 'n' || tag[0] == 'N')
+				return (1, int (hd tags)[1:]);
+			if (tag[0] == 's')		# ignore selection
+				break;
+		}
+		if (tags == nil)
+			break;
+	}
+	return (1, -1);
+}
+
+cardscmd(s: string): int
+{
+	if (debugging)
+		sys->print("cmd: %s\n", s);
+	if (sys->fprint(cliquefd, "%s", s) == -1) {
+		err := sys->sprint("%r");
+		notify(err);
+		sys->print("cmd error on '%s': %s\n", s, err);
+		return 0;
+	}
+	return 1;
+}
+
+updateproc(updatech: chan of array of byte)
+{
+	wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+	spawn updateproc1(updatech);
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(wfd, buf, len buf);
+	sys->print("updateproc process exited: %s\n", string buf[0:n]);
+}
+
+updateproc1(updatech: chan of array of byte)
+{
+	animfinishedch = chan of (ref Animation, chan of chan of ref Animation);
+	first := 1;
+	for (;;) {
+		alt {
+		v := <-animfinishedch =>
+			lock();
+			animterminated(v);
+			updatearena();
+			cmd(win, "update");
+			unlock();
+		u := <-updatech =>
+			if (u == nil) {
+				# XXX notify user that clique has been hung up somehow
+				exit;
+			}
+			moretocome := 0;
+			if (len u > 2 && u[len u-1] == byte '*' && u[len u-2] == byte '\n') {
+				u = u[0:len u - 2];
+				moretocome = 1;
+			}
+			(nil, lines) := sys->tokenize(string u, "\n");
+			lock();
+			starttime = sys->millisec();
+			for (; lines != nil; lines = tl lines)
+				applyupdate(hd lines);
+			updatearena();
+			if (!moretocome) {
+				if (first) {
+					sizetofit();
+					first = 0;
+				}
+				cmd(win, "update");
+			}
+			unlock();
+		}
+	}
+}
+
+updatearena()
+{
+	if (needrepack)
+		repackall();
+	if (needresize)
+		resizeall();
+	for (pstk := repackobjs; pstk != nil; pstk = tl pstk)
+		repackobj(hd pstk);
+	repackobjs = nil;
+}
+
+applyupdate(s: string)
+{
+	if (debugging) {
+		sys->print("update: %s\n", s);
+#		showtk = 1;
+	}
+	(nt, toks) := sys->tokenize(s, " ");
+	case hd toks {
+	"create" =>
+		# create id parentid vis type
+		id := int hd tl toks;
+		if (id >= len objects)
+			objects = (array[len objects + 10] of ref Object)[0:] = objects;
+		if (objects[id] != nil)
+			panic(sys->sprint("object %d already exists!", id));
+		parentid := int hd tl tl toks;
+		vis := int hd tl tl tl toks;
+		objtype := tl tl tl tl toks;
+		case hd objtype {
+		"stack" =>
+			objects[id] = makestack(id, parentid, vis);
+			needrepack = 1;
+		"card" =>
+			stk := stackobj(parentid);
+			completeanim(stk);
+			if (!stk.visible) {
+				# if creating in a private stack, we assume
+				# that the cards were there already, and
+				# just make them real again.
+
+				# first find a fake card.
+				for (i := 0; i < len stk.cards; i++)
+					if (stk.cards[i].id < 0)
+						break;
+				c: ref Object.Card;
+				if (i == len stk.cards) {
+					# no fake cards - we'll create one instead.
+					# this can happen if we've entered halfway through
+					# a clique, so don't know how many cards people
+					# are holding.
+					c = makecard(id, stk);
+					insertcards(stk, array[] of {c}, len stk.cards);
+				} else {
+					c = stk.cards[i];
+					changecardid(c, id);
+				}
+				objects[id] = c;
+			} else {
+				objects[id] = c := makecard(id, stk);
+				insertcards(stk, array[] of {c}, len stk.cards);
+			}
+		"widget" =>
+			objects[id] = makewidget(id, parentid, hd tl objtype);
+		"menuentry" =>
+			objects[id] = makemenuentry(id, parentid, tl objtype);
+		"member" =>
+			objects[id] = ref Object.Member(id, -1, "");
+		"layframe" =>
+			lay := ref Layout.Frame(id, parentid, "", -1, nil);
+			objects[id] = ref Object.Layoutframe(id, lay);
+			addlayout(lay);
+		"layobj" =>
+			lay := ref Layout.Obj(id, parentid, "", -1, -1);
+			objects[id] = ref Object.Layoutobj(id, lay);
+			addlayout(lay);
+		"scoretable" =>
+			if (scoretable != nil)
+				panic("cannot make two scoretables");
+			scoretable = objects[id] = ref Object.Scoretable(id, nil);
+		"score" =>
+			pick l := objects[parentid] {
+			Scoretable =>
+				nl := array[len l.scores + 1] of ref Object.Score;
+				nl[0:] = l.scores;
+				nl[len nl - 1] = objects[id] = ref Object.Score(id, nil, 0);
+				l.scores = nl;
+				cmd(win, "pack .buts.scores -side left");
+			* =>
+				panic("score created outside scoretable object");
+			}
+		"button" =>
+			objects[id] = ref Object.Button(id);
+			cmd(win, "button .buts." + string id);
+			cmd(win, "pack .buts." + string id + " -side left");
+		* =>
+			if (parentid != -1)
+				sys->print("cards: unknown objtype: '%s'\n", hd objtype);
+			objects[id] = ref Object.Other(id);
+		}
+
+	"tx" =>
+		# tx src dst start end dstindex
+		src, dst: ref Layobject.Stack;
+		index: int;
+		r: Range;
+		(src, toks) = (stackobj(int hd tl toks), tl tl toks);
+		(dst, toks) = (stackobj(int hd toks), tl toks);
+		(r.start, toks) =  (int hd toks, tl toks);
+		(r.end, toks) =  (int hd toks, tl toks);
+		(index, toks) = (int hd toks, tl toks);
+		transfer(src, r, dst, index);
+
+	"del" =>
+		# del parent start end objs...
+		oo := objects[int hd tl toks];	# parent
+		r := Range(int hd tl tl toks, int hd tl tl tl toks);
+		pick o := oo {
+		Stack =>			# deleting cards from a stack.
+			stk := o.o;
+			completeanim(stk);
+			if (!stk.visible) {
+				# if deleting from a private area, we assume the cards aren't
+				# actually being deleted at all, but merely becoming
+				# invisible, so turn them into fakes.
+				for (i := r.start; i < r.end; i++) {
+					card := stk.cards[i];
+					objects[card.id] = nil;
+					changecardid(card, --fakeid);
+					cardsetattr(card, "face", "0" :: nil);
+				}
+			} else {
+				cards := extractcards(stk, r);
+				for (i := 0; i < len cards; i++)
+					destroy(cards[i]);
+			}
+		Layoutframe =>		# deleting the layout specification.
+			lay := o.lay;
+			if (r.start != 0 || r.end != len lay.lays)
+				panic("cannot partially delete layouts");
+			for (i := r.start; i < r.end; i++)
+				destroy(objects[lay.lays[i].id]);
+			lay.lays = nil;
+			needrepack = 1;
+		Widget =>
+			# must be a menu widget
+			cmd(win, ".buts." + string o.id + ".m delete " +
+				string r.start + " " + string r.end);
+		* =>
+			for (objs := tl tl tl tl toks; objs != nil; objs = tl objs)
+				destroy(objects[int hd objs]);
+		}
+
+	"set" =>
+		# set obj attr val
+		id := int hd tl toks;
+		(attr, val) := (hd tl tl toks, tl tl tl toks);
+		pick o := objects[id] {
+		Card =>
+			cardsetattr(o, attr, val);
+		Widget =>
+			widgetsetattr(o.o, attr, val);
+		Stack =>
+			stacksetattr(o.o, attr, val);
+		Member =>
+			membersetattr(o, attr, val);
+		Layoutframe =>
+			laysetattr(o.lay, attr, val);
+		Layoutobj =>
+			laysetattr(o.lay, attr, val);
+		Score =>
+			scoresetattr(o, attr, val);
+		Button =>
+			buttonsetattr(o, attr, val);
+		Menuentry =>
+			menuentrysetattr(o, attr, val);
+		* =>
+			sys->fprint(stderr, "unknown attr set on object(tag %d), %s\n", tagof(objects[id]), s);
+		}
+
+	"say" or
+	"remark" =>
+		notify(join(tl toks));
+	* =>
+		sys->fprint(stderr, "cards: unknown update message '%s'\n", s);
+	}
+}
+
+addlayout(lay: ref Layout)
+{
+	pick lo := objects[lay.parentid] {
+	Layoutframe =>
+		l := lo.lay;
+		nl := array[len l.lays + 1] of ref Layout;
+		nl[0:] = l.lays;
+		nl[len nl - 1] = lay;
+		l.lays = nl;
+	* =>
+		if (layout == nil)
+			layout = lay;
+		else
+			panic("cannot make two layout objects");
+	}
+}
+
+makestack(id, parentid: int, vis: int): ref Object.Stack
+{
+	o := ref Object.Stack(
+		id,
+		ref Layobject.Stack(
+			id,
+			parentid,
+			"",			# pack widget name
+			(0, 0),		# size
+			0,			# needrepack
+			-1,			# orientation
+			-1,			# layid
+			-1,			# style
+			nil,			# cards
+			Hiddenpos,	# pos
+			(0, 0),		# delta
+			ref Queue,
+			0,			# actions
+			0,			# maxcards
+			"",			# title
+			vis,			# visible
+			0,			# n
+			-1,			# ownerid
+			ref Selection.Empty,		# sel
+			1,			# showsize
+			0			# hassize
+		)
+	);
+	cmd(win, ".c create rectangle -10 -10 -10 -10 -width 3 -tags r" + string id);
+	return o;
+}
+
+makewidget(id, parentid: int, wtype: string): ref Object.Widget
+{
+	wctype := wtype;
+	if (wtype == "menu")
+		wctype = "menubutton";
+	# XXX the widget is put in a frame 'cos of bugs in the canvas
+	# to do with size propagation.
+	w := cmd(win, "frame .buts." + string id + " -bg transparent");
+	cmd(win, wctype + " " + w + ".b");
+	cmd(win, "pack " + w + ".b -fill both -expand 1");
+	case wtype {
+	"menu" =>
+		cmd(win, "menu " + w + ".m");
+		cmd(win, w + ".b configure -menu " + w + ".m" +
+			" -relief raised");
+	"entry" =>
+		cmd(win, "bind " + w + ".b <Key-\n> {send cmd entry " + string id + "}");
+	}
+	cmd(win, ".c create window -1000 -1000 -tags r" + string id +
+		" -window " + w + " -anchor nw");
+	o := ref Object.Widget(
+		id,
+		ref Layobject.Widget(
+			id,
+			parentid,
+			nil,		# w
+			(0, 0),	# size
+			0,		# needrepack
+			-1,		# orientation
+			-1,		# style
+
+			wtype,
+			nil,		# entries
+			"",		# cmd
+			0		# width
+		)
+	);
+	return o;
+}
+
+menutitleid := 0;		# hack to identify menu entries
+makemenuentry(id, parentid: int, nil: list of string): ref Object.Menuentry
+{
+	m := ".buts." + string parentid + ".m";
+	t := "@" + string menutitleid++;
+	cmd(win, m + " add command -text " + t);
+	return ref Object.Menuentry(id, parentid, t);
+}
+
+makecard(id: int, stack: ref Layobject.Stack): ref Object.Card
+{
+	cmd(win, ".c create image 5000 5000 -anchor nw -tags i" + string id);
+	return ref Object.Card(id, stack.id, -1, -1, 0);
+}
+
+buttonsetattr(b: ref Object.Button, attr: string, val: list of string)
+{
+	w := ".buts." + string b.id;
+	case attr {
+	"text" =>
+		cmd(win, w + " configure -text '" + join(val));
+	"command" =>
+		cmd(win, w + " configure -command 'send srv " + join(val));
+	* =>
+		sys->print("unknown attribute on button: %s\n", attr);
+	}
+}
+
+widgetsetattr(b: ref Layobject.Widget, attr: string, val: list of string)
+{
+	w := ".buts." + string b.id + ".b";
+	case attr {
+	"text" =>
+		t := join(val);
+		if (b.wtype == "entry") {
+			cmd(win, w + " delete 0 end");
+			cmd(win, w + " insert 0 '" + t);
+			cmd(win, w + " select 0 end");		# XXX ??
+		} else {
+			cmd(win, w + " configure -text '" + t);
+			needresize = 1;
+		}
+	"command" =>
+		case b.wtype {
+		"button" =>
+			cmd(win, w + " configure -command 'send srv " + join(val));
+		"entry" =>
+			b.cmd = join(val);
+		}
+	"width" =>		# width in characters
+		b.width = int hd val;
+		sys->print("configuring %s for width %s\n", w, hd val);
+		cmd(win, w + " configure -width " + hd val + "w");
+		needresize = 1;
+	"layid" =>
+		setlayid(b, int hd val);
+	* =>
+		sys->print("unknown attribute on button: %s\n", attr);
+	}
+}
+
+findmenuentry(m: string, title: string): int
+{
+	end := int cmd(win, m + " index end");
+	for (i := 0; i <= end; i++) {
+		t := cmd(win, m + " entrycget " + string i + " -text");
+		if (t == title)
+			return i;
+	}
+	return -1;
+}
+
+menuentrysetattr(e: ref Object.Menuentry, attr: string, val: list of string)
+{
+	m := ".buts." + string e.parentid + ".m";
+	idx := findmenuentry(m, e.text);
+	if (idx == -1) {
+		sys->print("couldn't find menu entry '%s'\n", e.text);
+		return;
+	}
+	case attr {
+	"text" =>
+		t := join(val);
+		cmd(win, m + " entryconfigure " + string idx +" -text '" + t);
+		e.text = t;
+	"command" =>
+		cmd(win, m + " entryconfigure " + string idx +
+				" -command 'send srv " + join(val));
+	* =>
+		sys->print("unknown attribute on menu entry: %s\n", attr);
+	}
+}
+
+stacksetattr(stack: ref Layobject.Stack, attr: string, val: list of string)
+{
+	id := string stack.id;
+	case attr {
+	"maxcards" =>
+		stack.maxcards = int hd val;
+		needresize = 1;
+	"layid" =>
+		setlayid(stack, int hd val);
+	"showsize" =>
+		stack.showsize = int hd val;
+		showsize(stack);
+	"title" =>
+		title := join(val);
+		if (title != stack.title) {
+			if (stack.title == nil) {
+				cmd(win, ".c create text 5000 6000 -anchor n -tags t" + string id +
+					" -fill #ffffaa");
+				needresize = 1;
+			} else if (title == nil) {
+				cmd(win, ".c delete t" + string id);
+				needresize = 1;
+			}
+			if (title != nil)
+				cmd(win, ".c itemconfigure t" + string id + " -text '" + title);
+			stack.title = title;
+		}
+	"n" =>
+		# there are "n" cards in this stack, honest guv.
+		n := int hd val;
+		if (!stack.visible) {
+			if (n > len stack.cards) {
+				a := array[n - len stack.cards] of ref Object.Card;
+				for (i := 0; i < len a; i++) {
+					a[i] = makecard(--fakeid, stack);
+					cardsetattr(a[i], "face", "0" :: nil);
+				}
+				insertcards(stack, a, len stack.cards);
+			} else if (n < len stack.cards) {
+				for (i := len stack.cards - 1; i >= n; i--)
+					if (stack.cards[i].id >= 0)
+						break;
+				cards := extractcards(stack, (i + 1, len stack.cards));
+				for (i = 0; i < len cards; i++)
+					destroy(cards[i]);
+			}
+		}
+		stack.n = n;
+	"style" =>
+		case hd val {
+		"pile" =>
+			stack.style = styPILE;
+		"display" =>
+			stack.style = styDISPLAY;
+		* =>
+			sys->print("unknown stack style '%s'\n", hd val);
+		}
+		needresize = 1;
+	"owner" =>
+		if (val != nil)
+			stack.ownerid = int hd val;
+		else
+			stack.ownerid = -1;
+		changesel(stack, stack.sel);
+	"sel" =>
+		sel: ref Selection;
+		if (val == nil)
+			sel = ref Selection.Empty;
+		else if (tl val != nil && hd tl val == "-")
+			sel = ref Selection.XRange((int hd val, int hd tl tl val));
+		else {
+			idxl: list of int;
+			for (; val != nil; val = tl val)
+				idxl = int hd val :: idxl;
+			sel = ref Selection.Indexes(idxl);
+		}
+		changesel(stack, sel);
+	* =>
+		if (len attr >= len "actions" && attr[0:len "actions"] == "actions") {
+			oldactions := stack.actions;
+			act := 0;
+			for (; val != nil; val = tl val) {
+				case hd val {
+				"click" =>
+					act |= aCLICK;
+				* =>
+					sys->print("unknown action '%s'\n", hd val);
+				}
+			}
+			stack.actions = act;
+		} else
+			sys->fprint(stderr, "bad stack attr '%s'\n", attr);
+	}
+}
+
+showsize(stack: ref Layobject.Stack)
+{
+	id := string stack.id;
+	needsize := stack.showsize && len stack.cards > 0 && stack.style == styPILE;
+	if (needsize != stack.hassize) {
+		if (stack.hassize)
+			cmd(win, ".c delete n" + id + " N" + id);
+		else {
+			cmd(win, ".c create rectangle -5000 0 0 0  -fill #ffffaa -tags n" + id);
+			cmd(win, ".c create text -5000 0 -anchor sw -fill red -tags N" + id);
+		}
+		stack.hassize = needsize;
+	}
+	if (needsize) {
+		cmd(win, ".c itemconfigure N" + id + " -text " + string len stack.cards);
+		sr := cardrect(stack, (len stack.cards - 1, len stack.cards));
+		cmd(win, ".c coords N" + id + " " + p2s((sr.min.x, sr.max.y)));
+		bbox := cmd(win, ".c bbox N" + id);
+		cmd(win, ".c coords n" + id + " " + bbox);
+		cmd(win, ".c raise n" + id + "; .c raise N" + id);
+	}
+}		
+
+changesel(stack: ref Layobject.Stack, newsel: ref Selection)
+{
+	sid := "s" + string stack.id;
+	cmd(win, ".c delete " + sid);
+
+	if (me != nil && stack.ownerid == me.cid) {
+		pick sel := newsel {
+		Indexes =>
+			for (l := sel.idxl; l != nil; l = tl l) {
+				s := cmd(win, ".c create rectangle " +
+					r2s(cardrect(stack, (hd l, hd l + 1)).inset(-1)) +
+					" -width " + string Selectborder +
+					" -outline " + Selectcolour +
+					" -tags {" + sid + " " + sid + "." + string hd l + "}");
+				cmd(win, ".c lower " + s + " i" + string stack.cards[hd l].id);
+			}
+		XRange =>
+			cmd(win, ".c create rectangle " +
+					r2s(cardrect(stack, sel.r).inset(-1)) +
+					" -outline " + Selectcolour +
+					" -width " + string Selectborder +
+					" -tags " + sid);
+		}
+	}
+	stack.sel = newsel;
+}
+
+cardsetattr(card: ref Object.Card, attr: string, val: list of string)
+{
+	id := string card.id;
+	case attr {
+	"face" =>
+		card.face = int hd val;
+		if (card.face) {
+			if (card.number != -1)
+				cmd(win, ".c itemconfigure i" + id + " -image c" + string card.number );
+		} else
+			cmd(win, ".c itemconfigure i" + id + " -image rear" + string card.rear);
+	"number" =>
+		card.number = int hd val;
+		if (card.face)
+			cmd(win, ".c itemconfigure i" + id + " -image c" + string card.number );
+	"rear" =>
+		card.rear = int hd val;
+		if (card.face == 0)
+			cmd(win, ".c itemconfigure i" + id + " -image rear" + string card.rear);
+	* =>
+		sys->print("unknown attribute on card: %s\n", attr);
+	}
+}
+
+setlayid(layobj: ref Layobject, layid: int)
+{
+	if (layobj.layid != -1)
+		panic("obj already has a layout id (" + string layobj.layid + ")");
+	layobj.layid = layid;
+	x := layobj.layid % len layobjects;
+	layobjects[x] = layobj :: layobjects[x];
+	needrepack = 1;
+}
+
+membersetattr(p: ref Object.Member, attr: string, val: list of string)
+{
+	case attr {
+	"you" =>
+		me = p;
+		p.cid = int hd val;
+		for (i := 0; i < len objects; i++) {
+			if (objects[i] != nil) {
+				pick o := objects[i] {
+				Stack =>
+					if (o.o.ownerid == p.cid)
+						objneedsrepack(o.o);
+				}
+			}
+		}
+	"name" =>
+		p.name = hd val;
+	"id" =>
+		p.cid = int hd val;
+	"status" =>
+		if (p == me)
+			cmd(win, ".status configure -text '" + join(val));
+	"cliquetitle" =>
+		if (p == me)
+			tkclient->settitle(win, join(val));
+	* =>
+		sys->print("unknown attribute on member: %s\n", attr);
+	}
+}
+
+laysetattr(lay: ref Layout, attr: string, val: list of string)
+{
+	case attr {
+	"opts" =>
+		# orientation opts
+		case hd val {
+		"up" =>
+			lay.orientation = oUP;
+		"down" =>
+			lay.orientation = oDOWN;
+		"left" =>
+			lay.orientation = oLEFT;
+		"right" =>
+			lay.orientation = oRIGHT;
+		* =>
+			sys->print("unknown orientation '%s'\n", hd val);
+		}
+		lay.opts = join(tl val);
+	"layid" =>
+#		sys->print("layout obj %d => layid %s\n", lay.id, hd val);
+		pick l := lay {
+		Obj =>
+			l.layid = int hd val;
+			needrepack = 1;
+		* =>
+			sys->print("cannot set layid on Layout.Frame!\n");
+		}
+	* =>
+		sys->print("unknown attribute on lay: %s\n", attr);
+	}
+	needrepack = 1;
+}
+
+scoresetattr(score: ref Object.Score, attr: string, val: list of string)
+{
+	if (attr != "score")
+		return;
+	cmd(win, ".c delete score");
+
+	Padx: con 10;		# padding to the right of each item
+	Pady: con 6;		# padding below each item.
+
+	n := len val;
+	row := score.row = array[n] of (int, string);
+	height := 0;
+
+	# calculate values for this row
+	for ((col, vl) := (0, val); vl != nil; (col, vl) = (col + 1, tl vl)) {
+		v := hd vl;
+		size := textsize(v);
+		size.y += Pady;
+		if (size.y > height)
+			height = size.y;
+		row[col] = (size.x + Padx, v);
+	}
+	score.height = height;
+	totheight := 0;
+	scores := scoretable.scores;
+
+	# calculate number of columns
+	ncols := 0;
+	for (i := 0; i < len scores; i++)
+		if (len scores[i].row > ncols)
+			ncols = len scores[i].row;
+
+	# calculate column widths
+	colwidths := array[ncols] of {* => 0};
+	for (i = 0; i < len scores; i++) {
+		r := scores[i].row;
+		for (j := 0; j < len r; j++) {
+			(w, nil) := r[j];
+			if (w > colwidths[j])
+				colwidths[j] = w;
+		}
+		totheight += scores[i].height;
+	}
+	# create all table items
+	p := Hiddenpos;
+	for (i = 0; i < len scores; i++) {
+		p.x = Hiddenpos.x;
+		r := scores[i].row;
+		for (j := 0; j < len r; j++) {
+			(w, text) := r[j];
+			cmd(win, ".c create text " + p2s(p) + " -anchor nw -tags {score scoreent}-text '" + text);
+			p.x += colwidths[j];
+		}
+		p.y += scores[i].height;
+	}
+	r := Rect(Hiddenpos, p);
+	r.min.x -= Padx;
+	r.max.y -= Pady / 2;
+
+	cmd(win, ".c create rectangle " + r2s(r) + " -fill #ffffaa -tags score");
+
+	# horizontal lines
+	y := 0;
+	for (i = 0; i < len scores - 1; i++) {
+		ly := y + scores[i].height - Pady / 2;
+		cmd(win, ".c create line " + r2s(((r.min.x, ly), (r.max.x, ly))) + " -fill gray -tags score");
+		y += scores[i].height;
+	}
+
+	cmd(win, ".c raise scoreent");
+	cmd(win, ".c move score " + p2s(Hiddenpos.sub(r.min)));
+}
+
+textsize(s: string): Point
+{
+	return (cvsfont.width(s), cvsfont.height);
+}
+
+changecardid(c: ref Object.Card, newid: int)
+{
+	(nil, tags) := sys->tokenize(cmd(win, ".c gettags i" + string c.id), " ");
+	for (; tags != nil; tags = tl tags) {
+		tag := hd tags;
+		if (tag[0] >= '0' && tag[0] <= '9')
+			break;
+	}
+	cvsid := hd tags;
+	cmd(win, ".c dtag " + cvsid + " i" + string c.id);
+	c.id = newid;
+	cmd(win, ".c addtag i" + string c.id + " withtag " + cvsid);
+}
+
+stackobj(id: int): ref Layobject.Stack
+{
+	obj := objects[id];
+	if (obj == nil)
+		panic("nil stack object");
+	pick o := obj {
+	Stack =>
+		return o.o;
+	* =>
+		panic("expected obj " + string id + " to be a stack");
+	}
+	return nil;
+}
+
+# if there are updates pending on the stack,
+# then wait for them all to finish before we can do
+# any operations on the stack (e.g. insert, delete, create, etc)
+completeanim(stk: ref Layobject.Stack)
+{
+	while (!stk.animq.isempty())
+		animterminated(<-animfinishedch);
+}
+
+transfer(src: ref Layobject.Stack, r: Range, dst: ref Layobject.Stack, index: int)
+{
+	# we don't bother animating movement within a stack; maybe later?
+	if (src == dst) {
+		transfercards(src, r, dst, index);
+		return;
+	}
+	completeanim(src);
+
+	if (!src.visible) {
+		# cards being transferred out of private area should
+		# have already been created, but check anyway.
+		if (r.start != 0)
+			panic("bad transfer out of private");
+		for (i := 0; i < r.end; i++)
+			if (src.cards[i].id < 0)
+				panic("cannot transfer fake card");
+	}
+
+	startanimating(newanimation(src, r), dst, index);
+}
+
+objneedsrepack(obj: ref Layobject)
+{
+	if (!obj.needrepack) {
+		obj.needrepack = 1;
+		repackobjs = obj :: repackobjs;
+	}
+}
+
+repackobj(obj: ref Layobject)
+{
+	pick o := obj {
+	Stack =>
+		cards := o.cards;
+		pos := o.pos;
+		delta := o.delta;
+		for (i := 0; i < len cards; i++) {
+			p := pos.add(delta.mul(i));
+			id := string cards[i].id;
+			cmd(win, ".c coords i" + id + " " + p2s(p));
+			cmd(win, ".c raise i" + id);		# XXX could be more efficient.
+			cmd(win, ".c lower s" + string o.id + "." + string i + " i" + id);
+		}
+		changesel(o, o.sel);
+		showsize(o);
+	}
+	obj.needrepack = 0;
+}
+
+cardrect(stack: ref Layobject.Stack, r: Range): Rect
+{
+	if (r.start == r.end)
+		return ((-10, -10), (-10, -10));
+	cr := Rect((0, 0), cardsize).addpt(stack.pos);
+	delta := stack.delta;
+	return union(cr.addpt(delta.mul(r.start)), cr.addpt(delta.mul(r.end - 1)));
+}
+
+repackall()
+{
+	sys->print("repackall()\n");
+	needrepack = 0;
+	if (layout == nil) {
+		sys->print("no layout\n");
+		return;
+	}
+	if (packwin == nil) {
+		# use an unmapped tk window to do our packing arrangements
+		packwin = tk->toplevel(drawctxt.display, "-bd 0");
+		packwin.wreq = nil;			# stop window requests piling up.
+	}
+	cmd(packwin, "destroy " + cmd(packwin, "pack slaves ."));
+	packobjs = nil;
+	packit(layout, ".0");
+	sys->print("%d packobjs\n", len packobjs);
+	needresize = 1;
+}
+
+# make the frames for the objects to be laid out, in the
+# offscreen window.
+packit(lay: ref Layout, f: string)
+{
+	cmd(packwin, "frame " + f);
+	cmd(packwin, "pack " + f + " " + lay.opts);
+	pick l := lay {
+	Frame =>
+		for (i := 0; i < len l.lays; i++)
+			packit(l.lays[i], f + "." + string i);
+	Obj =>
+		if ((obj := findlayobject(l.layid)) != nil) {
+			obj.w = f;
+			obj.orientation = l.orientation;
+			packobjs = obj :: packobjs;
+		} else
+			sys->print("cannot find layobject %d\n", l.layid);
+	}
+}
+
+sizetofit()
+{
+	if (packobjs == nil)
+		return;
+	cmd(packwin, "pack propagate . 1");
+	cmd(packwin, ". configure -width 0 -height 0");	# make sure propagation works.
+	csz := actsize(packwin, ".");
+	cmd(win, "bind . <Configure> {}");
+	cmd(win, "pack propagate . 1");
+	cmd(win, ". configure -width 0 -height 0");
+
+	cmd(win, ".c configure -width " + string csz.x + " -height " + string csz.y
+			+ " -scrollregion {0 0 " + p2s(csz) + "}");
+	winr := actrect(win, ".");
+	screenr := win.image.screen.image.r;
+	if (!winr.inrect(screenr)) {
+		if (winr.dx() > screenr.dx())
+			(winr.min.x, winr.max.x) = (screenr.min.x, screenr.max.x);
+		if (winr.dy() > screenr.dy())
+			(winr.min.y, winr.max.y) = (screenr.min.y, screenr.max.y);
+		if (winr.max.x > screenr.max.x)
+			(winr.min.x, winr.max.x) = (screenr.max.x - winr.dx(), screenr.max.x);
+		if (winr.max.y > screenr.max.y)
+			(winr.min.y, winr.max.y) = (screenr.max.y - winr.dy(), screenr.max.y);
+	}
+	cmd(win, "pack propagate . 0");
+	cmd(win, ". configure " +
+			" -x " + string winr.min.x +
+			" -y " + string winr.min.y +
+			" -width " + string winr.dx() +
+			" -height " + string winr.dy());
+	needresize = 1;
+	updatearena();
+	cmd(win, "bind . <Configure> {send cmd config}");
+}
+
+setorigin(r: Rect, p: Point): Rect
+{
+	sz := Point(r.max.x - r.min.x, r.max.y - r.min.y);
+	return (p, p.add(sz));
+}
+
+resizeall()
+{
+	needresize = 0;
+	if (packobjs == nil)
+		return;
+	cmd(packwin, "pack propagate . 1");
+	cmd(packwin, ". configure -width 0 -height 0");	# make sure propagation works.
+	for (sl := packobjs; sl != nil; sl = tl sl) {
+		obj := hd sl;
+		sizeobj(obj);
+		cmd(packwin, obj.w + " configure -width " + string obj.size.x +
+			" -height " + string obj.size.y);
+	}
+	csz := actsize(packwin, ".");
+	sz := actsize(win, ".cf");
+	if (sz.x > csz.x || sz.y > csz.y) {
+		cmd(packwin, "pack propagate . 0");
+		if (sz.x > csz.x) {
+			cmd(packwin, ". configure -width " + string sz.x);
+			cmd(win, ".c xview moveto 0");
+			csz.x = sz.x;
+		}
+		if (sz.y > csz.y) {
+			cmd(packwin, ". configure -height " + string sz.y);
+			cmd(win, ".c yview moveto 0");
+			csz.y = sz.y;
+		}
+	}
+	cmd(win, ".c configure -width " + string csz.x + " -height " + string csz.y
+			+ " -scrollregion {0 0 " + p2s(csz) + "}");
+	onscreen();
+	for (sl = packobjs; sl != nil; sl = tl sl) {
+		obj := hd sl;
+		r := actrect(packwin, obj.w);
+		positionobj(obj, r);
+	}
+}
+
+# make sure that there aren't any unnecessary blank
+# bits in the scroll area.
+onscreen()
+{
+	(n, toks) := sys->tokenize(cmd(win, ".c xview"), " ");
+	cmd(win, ".c xview moveto " + hd toks);
+	(n, toks) = sys->tokenize(cmd(win, ".c yview"), " ");
+	cmd(win, ".c yview moveto " + hd toks);
+}
+
+# work out the size of an object to be laid out.
+sizeobj(obj: ref Layobject)
+{
+	pick o := obj {
+	Stack =>
+		delta := Point(0, 0);
+		case o.style {
+		styDISPLAY =>
+			case o.orientation {
+			oRIGHT =>	delta.x = carddelta.x;
+			oLEFT =>		delta.x = -carddelta.x;
+			oDOWN =>	delta.y = carddelta.y;
+			oUP =>		delta.y = -carddelta.y;
+			}
+		styPILE =>
+			;	# no offset
+		}
+		o.delta = delta;
+		r := Rect((0, 0), size(cardrect(o, (0, max(len o.cards, o.maxcards)))));
+		if (o.title != nil) {
+			p := Point(r.min.x + r.dx() / 2, r.min.y);
+			tr := s2r(cmd(win, ".c bbox t" + string o.id));
+			tbox := Rect((p.x - tr.dx() / 2, p.y - tr.dy()), (p.x + tr.dx() / 2, p.y));
+			r = union(r, tbox);
+		}
+		o.size = r.max.sub(r.min).add((Border * 2, Border * 2));
+#		sys->print("sized stack %d => %s\n", o.id, p2s(o.size));
+	Widget =>
+		w := ".buts." + string o.id;
+		o.size.x = int cmd(win, w + " cget -width");
+		o.size.y = int cmd(win, w + " cget -height");
+#		sys->print("sized widget %d (%s) => %s\n", o.id,
+#			cmd(win, "winfo class " + w + ".b"), p2s(o.size));
+	}
+}
+
+# set a laid-out object's position on the canvas, given
+# its allocated rectangle, r.
+positionobj(obj: ref Layobject, r: Rect)
+{
+	pick o := obj {
+	Stack =>
+#		sys->print("positioning stack %d, r %s\n", o.id, r2s(r));
+		delta := o.delta;
+		sz := o.size.sub((Border * 2, Border * 2));
+		r.min.x += (r.dx() - sz.x) / 2;
+		r.min.y += (r.dy() - sz.y) / 2;
+		r.max = r.min.add(sz);
+		if (o.title != nil) {
+			cmd(win, ".c coords t" +string o.id + " " +
+				string (r.min.x + r.dx() / 2) + " " + string r.min.y);
+			tr := s2r(cmd(win, ".c bbox t" + string o.id));
+			r.min.y = tr.max.y;
+			sz = size(cardrect(o, (0, max(len o.cards, o.maxcards))));
+			r.min.x += (r.dx() - sz.x) / 2;
+			r.min.y += (r.dy() - sz.y) / 2;
+			r.max = r.min.add(sz);
+		}
+		o.pos = r.min;
+		if (delta.x < 0)
+			o.pos.x = r.max.x - cardsize.x;
+		if (delta.y < 0)
+			o.pos.y = r.max.y - cardsize.y;
+		cmd(win, ".c coords r" + string o.id + " " + r2s(r.inset(-(Border / 2))));
+		objneedsrepack(o);
+	Widget =>
+#		sys->print("positioning widget %d, r %s\n", o.id, r2s(r));
+		cmd(win, ".c coords r" + string o.id + " " + p2s(r.min));
+		bd := int cmd(win, ".buts." + string o.id + " cget -bd");
+		cmd(win, ".c itemconfigure r" + string o.id +
+			" -width " + string (r.dx() - bd * 2) +
+			" -height " + string (r.dy() - bd * 2));
+	}
+}
+
+size(r: Rect): Point
+{
+	return r.max.sub(r.min);
+}
+
+transfercards(src: ref Layobject.Stack, r: Range, dst: ref Layobject.Stack, index: int)
+{
+	cards := extractcards(src, r);
+	n := r.end - r.start;
+	# if we've just removed some cards from the destination,
+	# then adjust the destination index accordingly.
+	if (src == dst && index > r.start) {
+		if (index < r.end)
+			index = r.start;
+		else
+			index -= n;
+	}
+	insertcards(dst, cards, index);
+}
+
+extractcards(src: ref Layobject.Stack, r: Range): array of ref Object.Card
+{
+	if (len src.cards > src.maxcards)
+		needresize = 1;
+	deltag(src.cards[r.start:r.end], "c" + string src.id);
+	n := r.end - r.start;
+	cards := src.cards[r.start:r.end];
+	newcards := array[len src.cards - n] of ref Object.Card;
+	newcards[0:] = src.cards[0:r.start];
+	newcards[r.start:] = src.cards[r.end:];
+	src.cards = newcards;
+	objneedsrepack(src);		# XXX not necessary if moving from top?
+	return cards;
+}
+
+insertcards(dst: ref Layobject.Stack, cards: array of ref Object.Card, index: int)
+{
+	n := len cards;
+	newcards := array[len dst.cards + n] of ref Object.Card;
+	newcards[0:] = dst.cards[0:index];
+	newcards[index + n:] = dst.cards[index:];
+	newcards[index:] = cards;
+	dst.cards = newcards;
+
+	for (i := 0; i < len cards; i++)
+		cards[i].parentid = dst.id;
+	addtag(dst.cards[index:index + n], "c" + string dst.id);
+	objneedsrepack(dst);		# XXX not necessary if adding to top?
+	if (len dst.cards > dst.maxcards)
+		needresize = 1;
+}
+
+destroy(obj: ref Object)
+{
+	if (obj.id >= 0)
+		objects[obj.id] = nil;
+	id := string obj.id;
+	pick o := obj {
+	Card =>
+		cmd(win, ".c delete i" + id);	# XXX crashed here once...
+	Widget =>
+		cmd(win, ".c delete r" + id);
+		w := ".buts." + id;
+		cmd(win, "destroy " + w);
+		dellayobject(o.o);
+	Stack =>
+		completeanim(o.o);
+		cmd(win, ".c delete r" + id + " s" + id + " n" + id + " N" + id);
+		if (o.o.title != nil)
+			cmd(win, ".c delete t" + id);
+		cmd(win, ".c delete c" + id);		# any remaining "fake" cards
+		needrepack = 1;
+		dellayobject(o.o);
+	Button =>
+		cmd(win, "destroy .buts." + string o.id);
+	Member =>
+		if (o.cid != -1) {
+			# XXX remove member from members hash.
+		}
+	Layoutobj =>
+		if ((l := findlayobject(o.lay.layid)) != nil) {
+			# XXX are we sure they're not off-screen anyway?
+			cmd(win, ".c move r" + string l.id + " 5000 5000");
+			cmd(win, ".c move c" + string l.id + " 5000 5000");
+			cmd(win, ".c move N" + string l.id + " 5000 5000");
+			cmd(win, ".c move n" + string l.id + " 5000 5000");
+			cmd(win, ".c move s" + string l.id + " 5000 5000");
+		}
+		if (layout == o.lay)
+			layout = nil;
+	Layoutframe =>
+		if (layout == o.lay)
+			layout = nil;
+	}
+}
+
+dellayobject(lay: ref Layobject)
+{
+	if (lay.layid == -1)
+		return;
+	x := lay.layid % len layobjects;
+	nl: list of ref Layobject;
+	for (ll := layobjects[x]; ll != nil; ll = tl ll)
+		if ((hd ll).layid != lay.layid)
+			nl = hd ll :: nl;
+	layobjects[x] = nl;
+}
+
+findlayobject(layid: int): ref Layobject
+{
+	if (layid == -1)
+		return nil;
+	for (ll := layobjects[layid % len layobjects]; ll != nil; ll = tl ll)
+		if ((hd ll).layid == layid)
+			return hd ll;
+	return nil;
+}
+
+deltag(cards: array of ref Object.Card, tag: string)
+{
+	for (i := 0; i < len cards; i++)
+		cmd(win, ".c dtag i" + string cards[i].id + " " + tag);
+}
+
+addtag(cards: array of ref Object.Card, tag: string)
+{
+	for (i := 0; i < len cards; i++)
+		cmd(win, ".c addtag " + tag + " withtag i" + string cards[i].id);
+}
+
+join(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
+
+notify(s: string)
+{
+	notifych <-= s;
+}
+
+notifierproc()
+{
+	notifypid := -1;
+	sync := chan of int;
+	for (;;) {
+		s := <-notifych;
+		kill(notifypid);
+		spawn notifyproc(s, sync);
+		notifypid = <-sync;
+	}
+}
+
+notifyproc(s: string, sync: chan of int)
+{
+	sync <-= sys->pctl(0, nil);
+	cmd(win, ".c delete notify");
+	id := cmd(win, ".c create text " + p2s(visibleorigin()) + " -anchor nw -fill red -tags notify -text '" + s);
+	bbox := cmd(win, ".c bbox " + id);
+	cmd(win, ".c create rectangle " + bbox + " -fill #ffffaa -tags notify");
+	cmd(win, ".c raise " + id);
+	cmd(win, "update");
+	sys->sleep(1500);
+	cmd(win, ".c delete notify");
+	cmd(win, "update");
+}
+
+# move canvas so that canvas point canvp lies under
+# screen point scrp.
+pan(canvp, scrp: Point)
+{
+	o := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+	co := canvp.sub(scrp.sub(o));
+	sz := Point(int cmd(win, ".c cget -width"), int cmd(win, ".c cget -height"));
+
+	cmd(win, ".c xview moveto " + string (real co.x / real sz.x));
+	cmd(win, ".c yview moveto " + string (real co.y / real sz.y));
+}
+
+# return the top left point that's currently visible
+# in the canvas, taking into account scrolling.
+visibleorigin(): Point
+{
+	(scrx, scry) := (cmd(win, ".c cget -actx"), cmd(win, ".c cget -acty"));
+	return Point (int cmd(win, ".c canvasx " + scrx),
+		int cmd(win, ".c canvasy " + scry));
+}
+
+s2r(s: string): Rect
+{
+	r: Rect;
+	(n, toks) := sys->tokenize(s, " ");
+	if (n < 4)
+		panic("malformed rectangle " + s);
+	(r.min.x, toks) = (int hd toks, tl toks);
+	(r.min.y, toks) = (int hd toks, tl toks);
+	(r.max.x, toks) = (int hd toks, tl toks);
+	(r.max.y, toks) = (int hd toks, tl toks);
+	return r;
+}
+
+r2s(r: Rect): string
+{
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+union(r1, r2: Rect): Rect
+{
+	if (r1.min.x > r2.min.x)
+		r1.min.x = r2.min.x;
+	if (r1.min.y > r2.min.y)
+		r1.min.y = r2.min.y;
+
+	if (r1.max.x < r2.max.x)
+		r1.max.x = r2.max.x;
+	if (r1.max.y < r2.max.y)
+		r1.max.y = r2.max.y;
+	return r1;
+}
+ 
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+		sys->write(fd, array of byte "kill", 4);
+}
+
+lockproc()
+{
+	for (;;) {
+		<-cardlockch;
+		cardlockch <-=1;
+	}
+}
+
+lock()
+{
+	cardlockch <-= 1;
+}
+
+unlock()
+{
+	<-cardlockch;
+}
+
+openimage(file: string, id: string): Point
+{
+	if (tk->cmd(win, "image create bitmap " + id + " -file " + file)[0] == '!')
+		return (0, 0);
+	return (int tk->cmd(win, "image width " + id),
+				int tk->cmd(win, "image height " + id));
+}
+
+# read images into tk.
+readimages(dir: string, prefix: string): (int, Point)
+{
+	displ := drawctxt.display;
+	if (cardsize.x > 0 && cardsize.y > 0 &&
+			(img := displ.open(dir + "/" + prefix + ".all.bit")) != nil) {
+		if (img.r.dx() % cardsize.x != 0 || img.r.dy() != cardsize.y)
+			sys->fprint(stderr, "cards: inconsistent complete image, ignoring\n");
+		else {
+			n := img.r.dx() / cardsize.x;
+			x := img.r.min.x;
+			sys->print("found %d cards in complete image\n", n);
+			for (i := 0; i < n; i++) {
+				c := displ.newimage(((0, 0), cardsize), img.chans, 0, 0);
+				c.draw(c.r, img, nil, (x, 0));
+				id := prefix + string i;
+				cmd(win, "image create bitmap " + id);
+				tk->putimage(win, id, c, nil);
+				x += cardsize.x;
+			}
+			return (n, cardsize);
+		}
+	}
+				
+	size := openimage("@" + dir + "/" + prefix + "0.bit", prefix + "0");
+	if (size.x == 0) {
+		sys->print("no first image (filename: '%s')\n", dir + "/" + prefix + "0.bit");
+		return (0, (0, 0));
+	}
+	i := 1;
+	for (;;) {
+		nsize := openimage("@" + dir + "/" + prefix + string i + ".bit", prefix + string i);
+		if (nsize.x == 0)
+			break;
+		if (!nsize.eq(size))
+			sys->fprint(stderr, "warning: inconsistent image size in %s/%s%d.bit, " +
+				"[%d %d] vs [%d %d]\n", dir, prefix, i, size.x, size.y, nsize.x, nsize.y);
+		i++;
+	}
+	return (i, size);
+}
+
+newanimation(src: ref Layobject.Stack, r: Range): ref Animation
+{
+	a := ref Animation;
+	a.srcpt = src.pos.add(src.delta.mul(r.start));
+	cards := extractcards(src, r);
+	a.cards = cards;
+	a.waitch = chan of ref Animation;
+	return a;
+}
+
+startanimating(a: ref Animation, dst: ref Layobject.Stack, index: int)
+{
+	q := dst.animq;
+	if (q.isempty())
+		spawn animqueueproc(a.waitch);
+
+	a.tag = "a" + string animid++;
+	addtag(a.cards, a.tag);
+	q.put(a);
+	a.dstid = dst.id;
+	a.index = index;
+	spawn animproc(a);
+}
+
+SPEED: con 1.5;			# animation speed in pixels/millisec
+
+animproc(a: ref Animation)
+{
+	tick := chan of int;
+	dst := stackobj(a.dstid);
+	if (dst == nil)
+		panic("animation destination has gone!");
+	dstpt := dst.pos.add(dst.delta.mul(a.index));
+	srcpt := a.srcpt;
+	d := dstpt.sub(srcpt);
+	# don't bother animating if moving to or from a hidden stack.
+	if (!srcpt.eq(Hiddenpos) && !dst.pos.eq(Hiddenpos) && !d.eq((0, 0))) {
+		mag := math->sqrt(real(d.x * d.x + d.y * d.y));
+		(vx, vy) := (real d.x / mag, real d.y / mag);
+		currpt := a.srcpt;		# current position of cards
+		t0 := starttime;
+		dt := int (mag / SPEED);
+		t := 0;
+		tickregister(tick);
+		cmd(win, ".c raise " + a.tag);
+		while (t < dt) {
+			s := real t * SPEED;
+			p := Point(srcpt.x + int (s * vx), srcpt.y + int (s * vy));
+			dp := p.sub(currpt);
+			cmd(win, ".c move " + a.tag + " " + string dp.x + " " + string dp.y);
+			currpt = p;
+			t = <-tick - t0;
+		}
+		tickunregister(tick);
+		cmd(win, "update");
+	}
+	a.waitch <-= a;
+}
+
+tickregister(tick: chan of int)
+{
+	tickregisterch <-= tick;
+}
+
+tickunregister(tick: chan of int)
+{
+	tickunregisterch <-= tick;
+}
+
+tickproc(tick: chan of int)
+{
+	for (;;)
+		tick <-= 1;
+}
+
+timeproc()
+{
+	reg: list of chan of int;
+	dummytick := chan of int;
+	realtick := chan of int;
+	tick := dummytick;
+	spawn tickproc(realtick);
+	for (;;) {
+		alt {
+		c := <-tickregisterch =>
+			if (reg == nil)
+				tick = realtick;
+			reg = c :: reg;
+		c := <-tickunregisterch =>
+			r: list of chan of int;
+			for (; reg != nil; reg = tl reg)
+				if (hd reg != c)
+					r = hd reg :: r;
+			reg = r;
+			if (reg == nil)
+				tick = dummytick;
+		<-tick =>
+			t := sys->millisec();
+			for (r := reg; r != nil; r = tl r) {
+				alt {
+				hd r <-= t =>
+					;
+				* =>
+					;
+				}
+			}
+			cmd(win, "update");
+		}
+	}
+}
+
+yield()
+{
+	yieldch <-= 1;
+}
+
+yieldproc()
+{
+	for (;;)
+		<-yieldch;
+}
+
+
+# send completed animations down animfinishedch;
+# wait for a reply, which is either a new animation to wait
+# for (the next in the queue) or nil, telling us to exit
+animqueueproc(waitch: chan of ref Animation)
+{
+	rc := chan of chan of ref Animation;
+	while (waitch != nil) {
+		animfinishedch <-= (<-waitch, rc);
+		waitch = <-rc;
+	}
+}
+
+# an animation has finished.
+# move the cards into their final place in the stack,
+# remove the animation from the queue it's on,
+# and inform the mediating process of the next animation process in the queue.
+animterminated(v: (ref Animation, chan of chan of ref Animation))
+{
+	(a, rc) := v;
+	deltag(a.cards, a.tag);
+	dst := stackobj(a.dstid);
+	insertcards(dst, a.cards, a.index);
+	repackobj(dst);
+	cmd(win, "update");
+	q := dst.animq;
+	q.get();
+	if (q.isempty())
+		rc <-= nil;
+	else {
+		a = q.peek();
+		rc <-= a.waitch;
+	}
+}
+
+actrect(win: ref Tk->Toplevel, w: string): Rect
+{
+	r: Rect;
+	r.min.x = int cmd(win, w + " cget -actx") + int cmd(win, w + " cget -bd");
+	r.min.y = int cmd(win, w + " cget -acty") + int cmd(win, w + " cget -bd");
+	r.max.x = r.min.x + int cmd(win, w + " cget -actwidth");
+	r.max.y = r.min.y + int cmd(win, w + " cget -actheight");
+	return r;
+}
+
+actsize(win: ref Tk->Toplevel, w: string): Point
+{
+	return (int cmd(win, w + " cget -actwidth"), int cmd(win, w + " cget -actheight"));
+}
+
+Queue.put(q: self ref Queue, s: T)
+{
+	q.t = s :: q.t;
+}
+
+Queue.get(q: self ref Queue): T
+{
+	s: T;
+	if(q.h == nil){
+		q.h = revlist(q.t);
+		q.t = nil;
+	}
+	if(q.h != nil){
+		s = hd q.h;
+		q.h = tl q.h;
+	}
+	return s;
+}
+
+Queue.peek(q: self ref Queue): T
+{
+	s: T;
+	if (q.isempty())
+		return s;
+	s = q.get();
+	q.h = s :: q.h;
+	return s;
+}
+
+Queue.isempty(q: self ref Queue): int
+{
+	return q.h == nil && q.t == nil;
+}
+
+revlist(ls: list of T) : list of T
+{
+	rs: list of T;
+	for (; ls != nil; ls = tl ls)
+		rs = hd ls :: rs;
+	return rs;
+}
+
+readconfig(): int
+{
+	for (lines := readconfigfile("/icons/cards/config"); lines != nil; lines = tl lines) {
+		t := hd lines;
+		case hd t {
+		"rearborder" =>
+			Rearborder = int hd tl t;
+		"border" =>
+			Border = int hd tl t;
+		"selectborder" =>
+			Selectborder = int hd tl t;
+		"xdelta" =>
+			carddelta.x = int hd tl t;
+		"ydelta" =>
+			carddelta.y = int hd tl t;
+		"font" =>
+			Textfont = hd tl t;
+		"selectcolour" =>
+			Selectcolour = hd tl t;
+		"cardsize" =>
+			if (len t != 3)
+				sys->fprint(stderr, "cards: invalid value for cardsize attribute\n");
+			else
+				cardsize = (int hd tl t, int hd tl tl t);
+		* =>
+			sys->fprint(stderr, "cards: unknown config attribute: %s\n", hd t);
+		}
+	}
+	return 0;
+}
+
+readcardimages(): int
+{
+	(nimages, cardsize) = readimages("/icons/cards", "c");
+ 	if (nimages == 0) {
+		sys->fprint(stderr, "cards: no card images found\n");
+		return -1;
+	}
+	sys->print("%d card images found\n", nimages);
+
+	(nrears, rearsize) := readimages("/icons/cardrears", "rear");
+	if (nrears > 0 && !rearsize.eq(cardsize)) {
+		sys->fprint(stderr, "cards: card rear sizes don't match card sizes (%s vs %s)\n", p2s(rearsize), p2s(cardsize));
+		return -1;
+	}
+	sys->print("%d card rear images found\n", nrears);
+	cr := Rect((0, 0), cardsize);
+	for (i := nrears; i < len rearcolours; i++) {
+		cmd(win, "image create bitmap rear" + string i);
+		img := drawctxt.display.newimage(cr, Draw->XRGB32, 0, Draw->Black);
+		img.draw(cr.inset(Rearborder),
+			drawctxt.display.color(rearcolours[i] - nrears), nil, (0, 0));
+		tk->putimage(win, "rear" + string i, img, nil);
+	}
+	return 0;
+}
+
+readconfigfile(f: string): list of list of string
+{
+	sys->print("opening config file '%s'\n", f);
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil)
+		return nil;
+	buf := array[Sys->ATOMICIO] of byte;
+	nb := sys->read(fd, buf, len buf);
+	if (nb <= 0)
+		return nil;
+	(nil, lines) := sys->tokenize(string buf[0:nb], "\r\n");
+	r: list of list of string;
+	for (; lines != nil; lines = tl lines) {
+		(n, toks) := sys->tokenize(hd lines, " \t");
+		if (n == 0)
+			continue;
+		if (n < 2)
+			sys->fprint(stderr, "cards: invalid config line: %s\n", hd lines);
+		else
+			r = toks :: r;
+	}
+	return r;
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	Point: import draw;
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+	bd := int cmd(win, ". cget -bd");
+	winsize := Point(int cmd(win, ". cget -actwidth") + bd * 2, int cmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		cmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		cmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int cmd(win, ". cget -actx"), int cmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int cmd(win, ". cget -actwidth") + bd*2,
+				int cmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	cmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
+
+panic(s: string)
+{
+	sys->fprint(stderr, "cards: panic: %s\n", s);
+	raise "panic";
+}
+
+showtk := 0;
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	if (showtk)
+		sys->print("tk: %s\n", s);
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!') {
+		sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+		raise "panic";
+	}
+	return e;
+}
+
+max(a, b: int): int
+{
+	if (a > b)
+		return a;
+	return b;
+}
--- /dev/null
+++ b/appl/spree/clients/chat.b
@@ -1,0 +1,194 @@
+implement Clientmod;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Display, Image: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "../client.m";
+include "commandline.m";
+	commandline: Commandline;
+	Cmdline: import commandline;
+
+stderr: ref Sys->FD;
+
+memberid := -1;
+win: ref Tk->Toplevel;
+
+client(ctxt: ref Draw->Context, argv: list of string, nil: int)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil) {
+		sys->fprint(stderr, "chat: cannot load %s: %r\n", Tkclient->PATH);
+		sys->raise("fail:bad module");
+	}
+	commandline = load Commandline Commandline->PATH;
+	if (commandline == nil) {
+		sys->fprint(stderr, "chat: cannot load %s: %r\n", Commandline->PATH);
+		sys->raise("fail:bad module");
+	}
+	commandline->init();
+
+	tkclient->init();
+	client1(ctxt);
+}
+cmdlinech: chan of string;
+cmdline: ref Cmdline;
+
+client1(ctxt: ref Draw->Context)
+{
+	cliquefd := sys->fildes(0);
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	winctl: chan of string;
+	(win, winctl) = tkclient->toplevel(ctxt.screen, nil,
+		"Cards", Tkclient->Appl);
+	cmdlinech = chan of string;
+
+	srvcmd := chan of string;
+	spawn updateproc(cliquefd, srvcmd);
+
+	for (;;) alt {
+	c := <-cmdlinech =>
+		for (cmds := cmdline.event(c); cmds != nil; cmds = tl cmds)
+			cliquecmd(cliquefd, "say " + quote(hd cmds));
+	c := <-srvcmd =>
+		applyupdate(c);
+		cmd(win, "update");
+	c := <-winctl =>
+		if (c == "exit")
+			sys->write(cliquefd, array[0] of byte, 0);
+		tkclient->wmctl(win, c);
+	}
+}
+
+quote(s: string): string
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == ' ')
+			s[i] = '_';
+	return s;
+}
+
+unquote(s: string): string
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == '_')
+			s[i] = ' ';
+	return s;
+}
+
+cliquecmd(fd: ref Sys->FD, s: string): int
+{
+	if (sys->fprint(fd, "%s\n", s) == -1) {
+		sys->print("chat: cmd error on '%s': %r\n", s);
+		return 0;
+	}
+	return 1;
+}
+
+
+updateproc(fd: ref Sys->FD, srvcmd: chan of string)
+{
+	wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+	spawn updateproc1(fd, srvcmd);
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(wfd, buf, len buf);
+	sys->print("updateproc process exited: %s\n", string buf[0:n]);
+}
+
+updateproc1(fd: ref Sys->FD, srvcmd: chan of string)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(fd, buf, len buf)) > 0) {
+		(nil, lines) := sys->tokenize(string buf[0:n], "\n");
+		for (; lines != nil; lines = tl lines)
+			srvcmd <-= hd lines;
+	}
+	if (n < 0)
+		sys->fprint(stderr, "chat: error reading updates: %r\n");
+	sys->fprint(stderr, "chat: updateproc exiting\n");
+}
+
+
+applyupdate(s: string)
+{
+	(nt, toks) := sys->tokenize(s, " ");
+	case hd toks {
+	"memberid" =>
+		# memberid clientid memberid name
+		memberid = int hd tl tl toks;
+		cmd(win, "frame .me");
+		cmd(win, "label .me.l -text {Type here}");
+		(cmdline, cmdlinech) = Cmdline.new(win, ".me.f", nil);
+		cmd(win, "pack .me -side top -fill x");
+		cmd(win, "pack .me.l -side top");
+		cmd(win, "pack .me.f -side top -fill both -expand 1 -anchor w");
+
+	"joinclique" =>
+		# joinclique cliqueid clientid memberid name
+		id := int hd tl tl tl toks;
+		name := hd tl tl tl tl toks;
+		if (id == memberid)
+			break;
+		f := "." + string id;
+		cmd(win, "frame " + f);
+		cmd(win, "label " + f + ".l -text '" + name);
+		tf := f + ".tf";
+		cmd(win, "frame " + tf);
+		cmd(win, "scrollbar " + tf + ".s -orient vertical -command {" + tf + ".t yview}");
+		cmd(win, "text " + tf + ".t -height 5h");
+		cmd(win, "pack " + f + ".l -side top");
+		cmd(win, "pack " + tf + ".s -side left -fill y");
+		cmd(win, "pack " + tf + ".t -side top -fill both -expand 1");
+		cmd(win, "pack " + tf + " -side top -fill both -expand 1");
+		cmd(win, "pack " + f + " -side top -fill both -expand 1");
+
+	"say" =>
+		# say memberid text
+		id := int hd tl toks;
+		if (id == memberid)
+			break;
+		t := "." + string id + ".tf.t";
+		cmd(win, t + " insert end '" + unquote(hd tl tl toks) + "\n");
+		cmd(win, t + " see end");
+	* =>
+		sys->fprint(stderr, "chat: unknown update message '%s'\n", s);
+	}
+}
+
+concat(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+		sys->write(fd, array of byte "kill", 4);
+}
+
+showtk := 0;
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	if (showtk)
+		sys->print("tk: %s\n", s);
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+	return e;
+}
+
--- /dev/null
+++ b/appl/spree/clients/gather.b
@@ -1,0 +1,178 @@
+implement Gather;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "commandline.m";
+	commandline: Commandline;
+	Cmdline: import commandline;
+include "sh.m";
+
+Gather: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+CLIENTDIR: con "/dis/spree/clients";
+
+drawctxt: ref Draw->Context;
+cliquefd: ref Sys->FD;
+stderr: ref Sys->FD;
+
+mnt, dir: string;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil) {
+		sys->fprint(stderr, "gather: cannot load %s: %r\n", Tkclient->PATH);
+		raise "fail:bad module";
+	}
+	tkclient->init();
+	commandline = load Commandline Commandline->PATH;
+	if(commandline == nil) {
+		sys->fprint(stderr, "gather: cannot load %s: %r\n", Commandline->PATH);
+		raise "fail:bad module";
+	}
+	commandline->init();
+	drawctxt = ctxt;
+	cliquefd = sys->fildes(0);
+
+	if (len argv >= 3) {
+		mnt = hd tl argv;
+		dir = hd tl tl argv;
+	} else
+		sys->fprint(stderr, "gather: expected mnt, dir args\n");
+	client1();
+}
+
+client1()
+{
+	(win, winctl) := tkclient->toplevel(drawctxt, nil, "Gathering", Tkclient->Appl);
+	ech := chan of string;
+	tk->namechan(win, ech, "e");
+	(chat, chatevent) := Cmdline.new(win, ".chat", nil);
+	updatech := chan of string;
+	spawn readproc(updatech);
+
+	cmd(win, "button .b -text Start -command {send e start}");
+	cmd(win, "pack .b -side top -anchor w");
+	cmd(win, "pack .chat -fill both -expand 1");
+	cmd(win, "pack propagate . 0");
+	cmd(win, "update");
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	for (;;) alt {
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-winctl =>
+		tkclient->wmctl(win, s);
+	line := <-updatech =>
+		(n, toks) := sys->tokenize(line, " ");
+		if (toks == nil)
+			continue;
+		case hd toks {
+		"clienttype" =>
+			chat.addtext("starting " + hd tl toks + " session...\n");
+			cmd(win, "update");
+			path := CLIENTDIR + "/" + hd tl toks + ".dis";
+			mod := load Command path;
+			if (mod == nil) {
+				chat.addtext(sys->sprint("could not load %s: %r\n", path));
+				chat.addtext("bye bye\n");
+				cliquefd = nil;
+			} else {
+				win = nil;
+				chat = nil;
+				startclient(mod, hd tl toks :: mnt :: dir :: tl tl toks);
+				exit;
+			}
+		"chat" =>
+			chat.addtext(hd tl toks + ": " + concat(tl tl toks) + "\n");
+		"title" =>
+			tkclient->settitle(win, "Gather " + concat(tl toks));
+		"join" or
+		"leave" or
+		"watch" or
+		"unwatch" =>
+			chat.addtext(line + "\n");
+		* =>
+			chat.addtext("unknown update: " + line + "\n");
+		}
+		cmd(win, "update");
+	c := <-chatevent =>
+		lines := chat.event(c);
+		for (; lines != nil; lines = tl lines)
+			cliquecmd("chat " + hd lines, chat);
+	c := <-ech =>
+		cliquecmd(c, chat);
+	}
+}
+
+cliquecmd(s: string, chat: ref Cmdline)
+{
+	if (sys->fprint(cliquefd, "%s", s) == -1) {
+		chat.addtext(sys->sprint("command failed: %r\n"));
+		cmd(chat.top, "update");
+	}
+}
+
+prefixed(s: string, prefix: string): int
+{
+	return len s >= len prefix && s[0:len prefix] == prefix;
+}
+
+readproc(updatech: chan of string)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(cliquefd, buf, Sys->ATOMICIO)) > 0) {
+		(nil, lines) := sys->tokenize(string buf[0:n], "\n");
+		for (; lines != nil; lines = tl lines) {
+			updatech <-= hd lines;
+			if (prefixed(hd lines, "clienttype"))
+				exit;
+		}
+	}
+	updatech <-= nil;
+}
+
+startclient(mod: Command, argv: list of string)
+{
+	{
+		mod->init(drawctxt, argv);
+	} exception e {
+	"*" =>
+		sys->print("client %s broken: %s\n", hd argv, e);
+	}
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+	r := tk->cmd(win, s);
+	if(len r > 0 && r[0] == '!')
+		sys->print("error executing '%s': %s\n", s, r[1:]);
+	return r;
+}
+
+concat(l: list of string): string
+{
+	if (l == nil)
+		return nil;
+	s := hd l;
+	for (l = tl l; l != nil; l = tl l)
+		s += " " + hd l;
+	return s;
+}
--- /dev/null
+++ b/appl/spree/clients/lobby.b
@@ -1,0 +1,562 @@
+implement Lobby;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "../join.m";
+	join: Join;
+include "dividers.m";
+	dividers: Dividers;
+	Divider: import dividers;
+include "commandline.m";
+	commandline: Commandline;
+	Cmdline: import commandline;
+include "sh.m";
+
+Lobby: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+CLIENTDIR: con "/dis/spree/clients";
+NAMEFONT: con "/fonts/charon/plain.small.font";
+TITLEFONT: con "/fonts/charon/bold.normal.font";
+HEADERFONT: con "/fonts/charon/italic.normal.font";
+
+Object: adt {
+	id:	int;
+	pick {
+	Session =>
+		filename:		string;
+		owner:		string;
+		invitations: 	list of string;
+		members:		list of string;
+		invited:		int;
+	Sessiontype =>
+		start:			string;
+		name:		string;
+		title:			string;
+		clienttype:	string;
+	Invite =>
+		session:		ref Object.Session;
+		name:		string;
+	Member =>
+		parentid:		int;
+		name:		string;
+	Archive =>
+	Other =>
+	}
+};
+
+drawctxt: ref Draw->Context;
+cliquefd: ref Sys->FD;
+objects: array of ref Object;
+myname: string;
+maxid := 0;
+
+badmodule(m: string)
+{
+	sys->fprint(sys->fildes(2), "lobby: cannot load %s: %r\n", m);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmodule(Tkclient->PATH);
+	tkclient->init();
+
+	commandline = load Commandline Commandline->PATH;
+	if(commandline == nil)
+		badmodule(Commandline->PATH);
+	commandline->init();
+
+	dividers = load Dividers Dividers->PATH;
+	if (dividers == nil)
+		badmodule(Dividers->PATH);
+	dividers->init();
+
+	join = load Join Join->PATH;
+	if (join == nil)
+		badmodule(Join->PATH);
+
+	drawctxt = ctxt;
+	cliquefd = sys->fildes(0);
+	sys->pctl(Sys->NEWPGRP, nil);
+	client1();
+}
+
+columns := array[] of {("name", ""), ("members", ""), ("watch", "Watch"), ("join", "Join"), ("invite", "Invite")};
+
+reqwidth(win: ref Tk->Toplevel, w: string): int
+{
+	return 2 * int cmd(win, w + " cget -bd") + int cmd(win, w + " cget -width");
+}
+
+client1()
+{
+	(win, winctl) := tkclient->toplevel(drawctxt, nil, "Lobby", Tkclient->Appl);
+	ech := chan of string;
+	tk->namechan(win, ech, "e");
+	(chat, chatevent) := Cmdline.new(win, ".d2", nil);
+	updatech := chan of list of string;
+	spawn readproc(updatech);
+
+	cmd(win, "frame .buts");
+	cmd(win, "menubutton .buts.start -text New -menu .buts.start.m");
+	cmd(win, "menu .buts.start.m");
+	cmd(win, "pack .buts.start -side left");
+	cmd(win, "button .buts.kick -text Kick -command {send e kick}");
+	cmd(win, "pack .buts.kick -side left");
+	cmd(win, "pack .buts -side top -fill x");
+
+	cmd(win, "frame .d1");
+
+	cmd(win, "scrollbar .d1.s -orient vertical -command {.d1.c yview}");
+	cmd(win, "canvas .d1.c -yscrollcommand {.d1.s set}");
+	cmd(win, "pack .d1.s -side left -fill y");
+	cmd(win, "pack .d1.c -side top -fill both -expand 1");
+	cmd(win, "frame .t");
+	cmd(win, ".d1.c create window 0 0 -anchor nw -window .t");
+	cmd(win, "frame .t.f1 -bd 2 -relief sunken");
+	cmd(win, "pack .t.f1 -side top -fill both -expand 1");
+
+	cmd(win, "label .t.f1.sessionlabel -text Sessions -font " + TITLEFONT);
+	cmd(win, "pack .t.f1.sessionlabel");
+	cmd(win, "frame .t.s");
+	cmd(win, "pack .t.s -in .t.f1 -side top -fill both -expand 1");
+
+	cmd(win, "frame .t.f2 -bd 2 -relief sunken");
+	cmd(win, "label .t.archiveslabel -text Archives -font " + TITLEFONT);
+	cmd(win, "pack .t.archiveslabel");
+	cmd(win, "frame .t.a");
+	cmd(win, "pack .t.a -in .t.f2 -side top -fill both -expand 1 -anchor w");
+	cmd(win, "pack .t.f2 -side top -fill both -expand 1");
+
+	cmd(win, "label .t.a.title0 -text Title -font " + HEADERFONT);
+	cmd(win, "label .t.a.title1 -text Members -font " + HEADERFONT);
+	cmd(win, "grid .t.a.title0 .t.a.title1 -sticky w");
+	cmd(win, "grid columnconfigure .t.a 1 -weight 1");
+
+	cmd(win, "bind .t <Configure> {.d1.c configure -scrollregion {0 0 [.t cget -width] [.t cget -height]}}");
+
+	cmd(win, "button .tmp");
+	for (i := 0; i < len columns; i++) {
+		(name, mintext) := columns[i];
+		cmd(win, ".tmp configure -text '" + mintext);
+		cmd(win, "grid columnconfigure .t.s " + string i +
+			" -name " + name +
+			" -minsize " + string reqwidth(win, ".tmp"));
+	}
+	cmd(win, "grid columnconfigure .t.s members -weight 1");
+	cmd(win, "destroy .tmp");
+	cmd(win, "menu .invite");
+
+	(divider, dividerevent) := Divider.new(win, ".d", ".d1" :: ".d2" :: nil, Dividers->NS);
+	cmd(win, "pack .d -side top -fill both");
+	cmd(win, "pack propagate . 0");
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	for (;;) {
+		alt {
+		s := <-win.ctxt.kbd =>
+			tk->keyboard(win, s);
+		s := <-win.ctxt.ptr =>
+			tk->pointer(win, *s);
+		s := <-win.ctxt.ctl or
+		s = <-win.wreq or
+		s = <-winctl =>
+			tkclient->wmctl(win, s);
+		c := <-dividerevent =>
+			divider.event(c);
+		c := <-chatevent =>
+			lines := chat.event(c);
+			for (; lines != nil; lines = tl lines) {
+				line := hd lines;
+				if (len line > 0 && line[len line-1]=='\n')
+					line = line[0:len line-1];
+				cliquecmd("chat " + line);
+			}
+		lines := <-updatech =>
+#sys->print("++\n");
+			for (; lines != nil; lines = tl lines) {
+#sys->print("+%s\n", hd lines);
+				doupdate(win, chat, hd lines);
+			}
+			cmd(win, "update");
+		c := <-ech =>
+			(n, toks) := sys->tokenize(c, " ");
+			case hd toks {
+			"watch" =>
+				joinclique(win, chat, int hd tl toks, "watch");
+			"join" =>
+				joinclique(win, chat, int hd tl toks, "join");
+			"start" =>
+				start(win, chat, int hd tl toks);
+			"postinvite" =>
+				postinvite(win, int hd tl toks, hd tl tl toks);
+			"unarchive" =>
+				e := cliquecmd("unarchive " + hd tl toks);
+				if (e != nil)
+					chat.addtext("failed to unarchive: " + e + "\n");
+			"invite" =>
+				# invite sessionid name
+				(id, name) := (hd tl toks, hd tl tl toks);
+				vname := "inv." + name;
+				v := int cmd(win, "variable " + vname);
+				s := "invite";
+				if (!v)
+					s = "uninvite";
+				e := cliquecmd(s + " " + string id + " " + name);
+				if (e != nil) {
+					chat.addtext("invite failed: " + e + "\n");
+					cmd(win, "variable " + vname + " " + string !v);
+				}
+			"kick" =>
+				e := cliquecmd("kick");
+				if (e != nil)
+					chat.addtext("kick failed: " + e + "\n");
+			* =>
+				sys->print("unknown msg %s\n", c);
+			}
+			cmd(win, "update");
+		}
+	}
+}
+
+joinclique(nil: ref Tk->Toplevel, chat: ref Cmdline, id: int, how: string)
+{
+	pick o := objects[id] {
+	Session =>
+		e := join->join(drawctxt, "/n/remote", o.filename, how);
+		if (e != nil)
+			chat.addtext("couldn't join clique: " + e + "\n");
+		else
+			chat.addtext("joined clique ok\n");
+	* =>
+		sys->print("join bad id %d (type %d)\n", id, tagof objects[id]);
+	}
+}
+
+start(nil: ref Tk->Toplevel, chat: ref Cmdline, id: int)
+{
+	pick o := objects[id] {
+	Sessiontype =>
+		e := cliquecmd("start " + o.start);
+		if (e != nil)
+			chat.addtext("failed to start clique: " + e + "\n");
+	* =>
+		sys->print("start bad id %d (type %d)\n", id, tagof objects[id]);
+	}
+}
+
+postinvite(win: ref Tk->Toplevel, id: int, widget: string)
+{
+	pick o := objects[id] {
+	Session =>
+		cmd(win, ".invite delete 0 end");
+		cmd(win, ".invite add checkbutton -text All -variable inv.all -command {send e invite " + string id + " all}");
+		for (invites := o.invitations; invites != nil; invites = tl invites)
+			if (hd invites == "all")
+				break;
+		cmd(win, "variable inv.all " + string (invites != nil));
+
+		for (i := 0; i < len objects; i++) {
+			if (objects[i] == nil)
+				continue;
+			pick p := objects[i] {
+			Member =>
+				if (tagof(objects[p.parentid]) != tagof(Object.Session) && p.name != o.owner) {
+					for (invites = o.invitations; invites != nil; invites = tl invites)
+						if (hd invites == p.name)
+							break;
+					invited := invites != nil;
+					cmd(win, "variable inv." + p.name + " " + string invited);
+					cmd(win, ".invite add checkbutton -variable inv." + p.name +
+						" -command {send e invite " + string id + " " + p.name + "}" +
+						" -text '" + p.name);
+				}
+			}
+		}
+		x := int cmd(win, widget + " cget -actx");
+		y := int cmd(win, widget + " cget -acty");
+		h := 2 * int cmd(win, widget + " cget -bd") + int cmd(win, widget + " cget -actheight");
+		cmd(win, ".invite post " + string x + " " + string (y + h));
+	* =>
+		sys->print("bad invited id %d (type %d)\n", id, tagof objects[id]);
+	}
+}
+
+panic(s: string)
+{
+	sys->print("lobby panic: %s\n", s);
+	raise "panic";
+}
+
+doupdate(win: ref Tk->Toplevel, chat: ref Cmdline, line: string)
+{
+	(n, toks) := sys->tokenize(line, " ");
+	if (n == 0)
+		return;
+	case hd toks {
+	"chat" =>
+		chat.addtext(sys->sprint("%s: %s\n", hd tl toks, concat(tl tl toks)));
+	"create" =>
+		# create id parentid vis type
+		id := int hd tl toks;
+		if (id >= len objects)
+			objects = (array[len objects + 10] of ref Object)[0:] = objects;
+		if (objects[id] != nil)
+			panic(sys->sprint("object %d already exists!", id));
+		parentid := int hd tl tl toks;
+		objtype := tl tl tl tl toks;
+		o: ref Object;
+		case hd objtype {
+		"sessiontype" =>
+			o = ref Object.Sessiontype(id, nil, nil, nil, nil);
+		"session" =>
+			cmd(win, "grid rowinsert .t.s 0");
+			cmd(win, "grid rowconfigure .t.s 0 -name id" + string id);
+			f := ".t.s.f" + string id;
+			cmd(win, "frame " + f);			# dummy, so we can destroy row easily
+			cmd(win, "label "+f+".name");
+			cmd(win, "grid "+f+".name -row id" + string id + " -column name -in .t.s");
+			cmd(win, "button "+f+".watch -text Watch -command {send e watch " + string id + "}");
+			cmd(win, "grid "+f+".watch -row id" + string id + " -column watch -in .t.s");
+			cmd(win, "label "+f+".members -font " + NAMEFONT);
+			cmd(win, "grid "+f+".members -row id" + string id + " -column members -in .t.s");
+			o = ref Object.Session(id, nil, nil, nil, nil, 0);
+		"member" =>
+			o = ref Object.Member(id, parentid, nil);
+		"invite" =>
+			pick parent := objects[parentid] {
+			Session =>
+				o = ref Object.Invite(id, parent, nil);
+			* =>
+				panic("invite not under session");
+			}
+		"archive" =>
+			cmd(win, "grid rowinsert .t.a 1");
+			cmd(win, "grid rowconfigure .t.a 1 -name id" + string id);
+			f := ".t.a.f" + string id;
+			cmd(win, "frame " + f);
+			cmd(win, "label "+f+".name");
+			cmd(win, "grid "+f+".name -row id" + string id + " -column 0 -in .t.a -sticky w");
+			cmd(win, "label "+f+".members -anchor w -font " + NAMEFONT);
+			cmd(win, "grid "+f+".members -row id" + string id + " -column 1 -in .t.a -sticky ew");
+			cmd(win, "button "+f+".unarchive -text Unarchive -command {send e unarchive " + string id + "}");
+			cmd(win, "grid "+f+".unarchive -row id" + string id + " -column 2 -in .t.a");
+			o = ref Object.Archive(id);
+		* =>
+			o = ref Object.Other(id);
+		}
+		objects[id] = o;
+
+	"del" =>
+		# del parent start end objs...
+		for (objs := tl tl tl tl toks; objs != nil; objs = tl objs) {
+			id := int hd objs;
+			pick o := objects[id] {
+			Session =>
+				cmd(win, "grid rowdelete .t.s id" + string id);
+				cmd(win, "destroy .t.s.f" + string id);
+			Archive =>
+				cmd(win, "grid rowdelete .t.a id" + string id);
+				cmd(win, "destroy .t.a.f" + string id);
+			Sessiontype =>
+				sys->print("cannot destroy sessiontypes yet\n");
+			Member =>
+				pick parent := objects[o.parentid] {
+				Session =>
+					parent.members = removeitem(parent.members, o.name);
+					cmd(win, sys->sprint(".t.s.f%d.members configure -text '%s", o.parentid, concat(parent.members)));
+				* =>
+					chat.addtext(o.name + " has left\n");
+				}
+			Invite =>
+				s := o.session;
+				invites := s.invitations;
+				invited := 0;
+				for (s.invitations = nil; invites != nil; invites = tl invites) {
+					inv := hd invites;
+					if (inv != o.name) {
+						s.invitations = inv :: s.invitations;
+						if (inv == "all" || inv == myname)
+							invited = 1;
+					}
+				}
+				if (!invited && s.invited) {
+					cmd(win, "destroy .t.s.f" + hd tl toks + ".join");
+					s.invited = 0;
+				}
+			}
+			objects[id] = nil;
+		}
+
+	"name" =>
+		myname = hd tl toks;
+		tkclient->settitle(win, "Lobby (" + myname + ")");
+
+	"set" =>
+		# set obj attr val
+		id := int hd tl toks;
+		(attr, val) := (hd tl tl toks, tl tl tl toks);
+		pick o := objects[id] {
+		Session =>
+			f := ".t.s.f" + string id;
+			case attr {
+			"filename" =>
+				o.filename = hd val;
+			"owner" =>
+				if (hd val == myname) {
+					cmd(win, "label "+f+".invite -text Invite -bd 2 -relief raised");
+					cmd(win, "bind "+f+".invite <Button-1> {send e postinvite " + string id + " %W}");
+					cmd(win, "grid "+f+".invite -row id" + string id + " -column invite -in .t.s");
+				}
+				o.owner = hd val;
+			"title" =>
+				cmd(win, f + ".name configure -text '" + concat(val));
+			}
+		Archive =>
+			f := ".t.a.f" + string id;
+			case attr {
+			"name" =>
+				cmd(win, f + ".name configure -text '" + concat(val));
+			"members" =>
+				cmd(win, f + ".members configure -text '" + concat(val));
+			}
+		Sessiontype =>
+			case attr {
+			"start" =>
+				o.start = concat(val);
+			"clienttype" =>
+				o.clienttype = hd val;
+			"title" =>
+				if (o.title != nil)
+					panic("can't change sessiontype name!");
+				else {
+					o.title = concat(val);
+					cmd(win, ".buts.start.m add command" +
+							" -command {send e start " + string id + "}" +
+							" -text '" + o.title);
+				}
+			"name" =>
+				o.name = hd val;
+			}
+		Member =>
+			case attr {
+			"name" =>
+				if (o.name != nil)
+					panic("cannot change member name!");
+				o.name = hd val;
+				pick parent := objects[o.parentid] {
+				Session =>
+					parent.members = o.name :: parent.members;
+					cmd(win, sys->sprint(".t.s.f%d.members configure -text '%s", o.parentid, concat(parent.members)));
+				* =>
+					chat.addtext(o.name + " has arrived\n");
+				}
+			}
+		Invite  =>
+			case attr {
+			"name" =>
+				o.name = hd val;
+				s := o.session;
+				sid := string s.id;
+				f := ".t.s.f" + sid;
+				invited := o.name == myname || o.name == "all";
+				s.invitations = o.name :: s.invitations;
+				if (invited && !s.invited) {
+					cmd(win, "button "+f+".join -text Join -command {send e join " + sid + "}");
+					cmd(win, "grid "+f+".join -row id" + sid + " -column join -in .t.s");
+					s.invited = 1;
+				}
+			}
+		}
+	}
+}
+
+removeitem(l: list of string, i: string): list of string
+{
+	rl: list of string;
+	for (; l != nil; l = tl l)
+		if (hd l != i)
+			rl = hd l :: rl;
+	return rl;
+}
+
+numsplit(s: string): (string, int)
+{
+	for (i := len s - 1; i >= 0; i--)
+		if (s[i] < '0' || s[i] > '9')
+			break;
+	if (i == len s -1)
+		return (s, 0);
+	return (s[0:i+1], int s[i+1:]);
+}
+
+cliquecmd(s: string): string
+{
+	if (sys->fprint(cliquefd, "%s", s) == -1) {
+		e := sys->sprint("%r");
+		sys->print("error on '%s': %s\n", s, e);
+		return e;
+	}
+	return nil;
+}
+
+prefixed(s: string, prefix: string): int
+{
+	return len s >= len prefix && s[0:len prefix] == prefix;
+}
+
+readproc(updatech: chan of list of string)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(cliquefd, buf, Sys->ATOMICIO)) > 0) {
+		(nil, lines) := sys->tokenize(string buf[0:n], "\n");
+		if (lines != nil)
+			updatech <-= lines;
+	}
+	updatech <-= nil;
+}
+
+startclient(mod: Command, argv: list of string)
+{
+	{
+		mod->init(drawctxt, argv);
+	} exception e {
+	"*" =>
+		sys->print("client %s broken: %s\n", hd argv, e);
+		exit;
+	}
+	mod->init(drawctxt, argv);
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+	r := tk->cmd(win, s);
+	if(len r > 0 && r[0] == '!')
+		sys->print("error executing '%s': %s\n", s, r[1:]);
+	return r;
+}
+
+concat(l: list of string): string
+{
+	if (l == nil)
+		return nil;
+	s := hd l;
+	for (l = tl l; l != nil; l = tl l)
+		s += " " + hd l;
+	return s;
+}
--- /dev/null
+++ b/appl/spree/clients/othello.b
@@ -1,0 +1,270 @@
+implement Othello;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+
+SQ: con 30;		# Square size in pixels
+N: con 8;
+
+stderr: ref Sys->FD;
+
+Othello: module {
+	init:   fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Black, White, Nocolour: con iota;
+colours := array[] of {White => "white", Black => "black"};
+
+win: ref Tk->Toplevel;
+board: array of array of int;
+notifypid := -1;
+membername: string;
+membernames := array[2] of string;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil) {
+		sys->fprint(stderr, "othello: cannot load %s: %r\n", Tkclient->PATH);
+		raise "fail:bad module";
+	}
+	tkclient->init();
+
+	if (len argv >= 3) {		# argv: modname mnt dir ...
+		membername = readfile(hd tl argv + "/name");
+		sys->print("name is %s\n", membername);
+	}
+	client1(ctxt);
+}
+
+configcmds := array[] of {
+"canvas .c -height " + string (SQ * N) + " -width " + string (SQ * N) + " -bg green",
+"label .status -text {No clique in progress}",
+"frame .f",
+"label .f.l -text {watching} -bg white",
+"label .f.turn -text {}",
+"pack .f.l -side left -expand 1  -fill x",
+"pack .f.turn -side left -fill x -expand 1",
+"pack .c -side top",
+"pack .status .f -side top -fill x",
+"bind .c <ButtonRelease-1> {send cmd b1up %x %y}",
+};
+
+client1(ctxt: ref Draw->Context)
+{
+	cliquefd := sys->fildes(0);
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	winctl: chan of string;
+	(win, winctl) = tkclient->toplevel(ctxt, nil,
+		"Othello", Tkclient->Appl);
+	bcmd := chan of string;
+	tk->namechan(win, bcmd, "cmd");
+	for (i := 0; i < len configcmds; i++)
+		cmd(win, configcmds[i]);
+
+	for (i = 0; i < N; i++)
+		for (j := 0; j < N; j++)
+			cmd(win, ".c create rectangle " + r2s(square(i, j)));
+	board = array[N] of {* => array[N] of {* => Nocolour}};
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "ptr"::"kbd"::nil);
+	spawn updateproc(cliquefd);
+
+	for (;;) alt {
+	c := <-bcmd =>
+		(n, toks) := sys->tokenize(c, " ");
+		case hd toks {
+		"b1up" =>
+			(inboard, x, y) := boardpos((int hd tl toks, int hd tl tl toks));
+			if (!inboard)
+				break;
+			othellocmd(cliquefd, "move " + string x + " " + string y);
+			cmd(win, "update");
+		}
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-winctl =>
+		if (s == "exit")
+			sys->write(cliquefd, array[0] of byte, 0);
+		tkclient->wmctl(win, s);
+	}
+}
+
+othellocmd(fd: ref Sys->FD, s: string): int
+{
+	if (sys->fprint(fd, "%s\n", s) == -1) {
+		notify(sys->sprint("%r"));
+		return 0;
+	}
+	return 1;
+}
+
+updateproc(cliquefd: ref Sys->FD)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(cliquefd, buf, len buf)) > 0) {
+		(nil, lines) := sys->tokenize(string buf[0:n], "\n");
+		for (; lines != nil; lines = tl lines)
+			applyupdate(hd lines);
+		cmd(win, "update");
+	}
+	if (n < 0)
+		sys->fprint(stderr, "othello: error reading updates: %r\n");
+	sys->fprint(stderr, "othello: updateproc exiting\n");
+}
+
+applyupdate(s: string)
+{
+	(nt, toks) := sys->tokenize(s, " ");
+	case hd toks {
+	"create" =>
+		; # ignore - there's only one object (the board)
+	"set" =>
+		# set objid attr val
+		toks = tl tl toks;
+		(attr, val) := (hd toks, hd tl toks);
+		case attr {
+		"members" =>
+			membernames[Black] = hd tl toks;
+			membernames[White] = hd tl tl toks;
+			status(membernames[Black]+ "(Black) vs. " + string membernames[White] + "(White)");
+			if (membername == membernames[Black])
+				cmd(win, ".f.l configure -text Black");
+			else if (membername == membernames[White])
+				cmd(win, ".f.l configure -text White");
+		"turn" =>
+			turn := int val;
+			if (turn != Nocolour) {
+				if (membername == membernames[turn])
+					cmd(win, ".f.turn configure -text {(Your turn)}");
+				else if (membername == membernames[!turn])
+					cmd(win, ".f.turn configure -text {}");
+			}
+		"winner" =>
+			text := "it was a draw";
+			winner := int val;
+			if (winner != Nocolour)
+				text = colours[int val] + " won.";
+			status("clique over. " + text);
+			cmd(win, ".f.l configure -text {watching}");
+		* =>
+			(x, y) := (attr[0] - 'a', attr[1] - 'a');
+			set(x, y, int val);
+		}
+	* =>
+		sys->fprint(stderr, "othello: unknown update message '%s'\n", s);
+	}
+}
+
+status(s: string)
+{
+	cmd(win, ".status configure -text '" + s);
+}
+
+itemopts(colour: int): string
+{
+	return "-fill " + colours[colour] +
+		" -outline " + colours[!colour];
+}
+
+set(x, y, colour: int)
+{
+	id := piece(x, y);
+	if (colour == Nocolour)
+		cmd(win, ".c delete " + id);
+	else if (board[x][y] != Nocolour)
+		cmd(win, ".c itemconfigure " + id + " " + itemopts(colour));
+	else
+		cmd(win, ".c create oval " + r2s(square(x, y)) + " " +
+			itemopts(colour) +
+			" -tags {piece " + id + "}");
+	board[x][y] = colour;
+}
+
+notify(s: string)
+{
+	kill(notifypid);
+	sync := chan of int;
+	spawn notifyproc(s, sync);
+	notifypid = <-sync;
+}
+
+notifyproc(s: string, sync: chan of int)
+{
+	sync <-= sys->pctl(0, nil);
+	cmd(win, ".c delete notify");
+	id := cmd(win, ".c create text 0 0 -anchor nw -fill red -tags notify -text '" + s);
+	bbox := cmd(win, ".c bbox " + id);
+	cmd(win, ".c create rectangle " + bbox + " -fill #ffffaa -tags notify");
+	cmd(win, ".c raise " + id);
+	cmd(win, "update");
+	sys->sleep(750);
+	cmd(win, ".c delete notify");
+	cmd(win, "update");
+	notifypid = -1;
+}
+
+boardpos(p: Point): (int, int, int)
+{
+	(x, y) := (p.x / SQ, p.y / SQ);
+	if (x < 0 || x > N - 1 || y < 0 || y > N - 1)
+		return (0, 0, 0);
+	return (1, x, y);
+}
+
+square(x, y: int): Rect
+{
+	return ((SQ*x, SQ*y), (SQ*(x + 1), SQ*(y + 1)));
+}
+
+piece(x, y: int): string
+{
+	return "p" + string x + "." + string y;
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+	return e;
+}
+
+r2s(r: Rect): string
+{
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+		sys->write(fd, array of byte "kill", 4);
+}
+
+readfile(f: string): string
+{
+	if ((fd := sys->open(f, Sys->OREAD)) == nil)
+		return nil;
+	a := array[8192] of byte;
+	n := sys->read(fd, a, len a);
+	if (n <= 0)
+		return nil;
+	return string a[0:n];
+}
+
--- /dev/null
+++ b/appl/spree/engines/afghan.b
@@ -1,0 +1,302 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	All, None: import Sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	Selection, Cmember: import cardlib;
+	dTOP, dLEFT, oLEFT, oRIGHT, EXPAND, FILLX, FILLY, aUPPERCENTRE,
+	Stackspec: import Cardlib;
+include "../gather.m";
+
+CLICK, REDEAL: con iota;
+
+clique: ref Clique;
+rows: array of ref Object;		# [10]
+central: array of ref Object;	# [4]
+chokey, deck: ref Object;
+direction := 0;
+nredeals := 0;
+
+Rowpilespec := Stackspec(
+	"display",		# style
+	10,			# maxcards
+	0,			# conceal
+	nil			# title
+);
+
+Centralpilespec := Stackspec(
+	"pile",
+	13,
+	0,
+	nil
+);
+
+clienttype(): string
+{
+	return "cards";
+}
+
+maxmembers(): int
+{
+	return 1;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+	cardlib->init(spree, clique);
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members != 1)
+		return "one member only";
+	return nil;
+}
+
+archive()
+{
+	archiveobj := cardlib->archive();
+	allow->archive(archiveobj);
+	cardlib->archivearray(rows, "rows");
+	cardlib->archivearray(central, "central");
+	cardlib->setarchivename(chokey, "chokey");
+	cardlib->setarchivename(deck, "deck");
+	archiveobj.setattr("direction", string direction, None);
+	archiveobj.setattr("nredeals", string nredeals, None);
+}
+
+start(members: array of ref Member, archived: int)
+{
+	if (archived) {
+		archiveobj := cardlib->unarchive();
+		allow->unarchive(archiveobj);
+		rows = cardlib->getarchivearray("rows");
+		central = cardlib->getarchivearray("central");
+		chokey = cardlib->getarchiveobj("chokey");
+		deck = cardlib->getarchiveobj("deck");
+		direction = int archiveobj.getattr("direction");
+		nredeals = int archiveobj.getattr("nredeals");
+	} else {
+		p := members[0];
+		Cmember.join(p, -1).layout.lay.setvisibility(All);
+		startclique();
+		allow->add(CLICK, p, "click %o %d");
+	}
+}
+
+command(p: ref Member, cmd: string): string
+{
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil)
+		return err;
+	cp := Cmember.find(p);
+	if (cp == nil)
+		return "you are not playing";
+
+	case tag {
+	REDEAL =>
+		if (nredeals >= 3)
+			return "no more redeals";
+		redeal();
+		nredeals++;
+	CLICK =>
+		# click stack index
+		stack := clique.objects[int hd tl toks];
+		nc := len stack.children;
+		idx := int hd tl tl toks;
+		sel := cp.sel;
+		stype := stack.getattr("type");
+
+		if (sel.isempty() || sel.stack == stack) {
+			# selecting a card to move
+			if (idx < 0 || idx >= len stack.children)
+				return "invalid index";
+			case stype {
+			"row" or
+			"chokey" =>
+				select(cp, stack, (nc - 1, nc));
+			* =>
+				return "you can't move cards from there";
+			}
+		} else {
+			# selecting a stack to move to.
+			card := cardlib->getcard(sel.stack.children[sel.r.start]);
+			case stype {
+			"central" =>
+				top := cardlib->getcard(stack.children[nc - 1]);
+				if (direction == 0) {
+					if (card.number != (top.number + 1) % 13 &&
+							card.number != (top.number + 12) % 13)
+						return "out of sequence";
+					if (card.suit != top.suit)
+						return "wrong suit";
+					direction = card.number - top.number;
+				} else {
+					if (card.number != (top.number + direction + 13) % 13)
+						return "out of sequence";
+					if (card.suit != top.suit)
+						return "wrong suit";
+				}
+			"row" =>
+				if (nc == 0 || sel.stack.getattr("type") == "chokey")
+					return "you wish!";
+				top := cardlib->getcard(stack.children[nc - 1]);
+				if (card.suit != top.suit)
+					return "wrong suit";
+				if (card.number != (top.number + 1) % 13 &&
+						card.number != (top.number + 12) % 13)
+					return "out of sequence";
+			"chokey" =>
+				if (nc != 0)
+					return "only one card allowed there";
+			* =>
+				return "can't move there";
+			}
+			sel.transfer(stack, -1);
+		}
+	}
+	return nil;
+}
+
+startclique()
+{
+	addlayobj, addlayframe: import cardlib;
+
+	entry := clique.newobject(nil, All, "widget entry");
+	entry.setattr("command", "say", All);
+
+	but := clique.newobject(nil, All, "widget button");
+	but.setattr("text", "Redeal", All);
+	but.setattr("command", "redeal", All);
+	allow->add(REDEAL, Cmember.index(0).p, "redeal");
+
+	addlayframe("topf", nil, nil, dTOP|EXPAND|FILLX|aUPPERCENTRE, dTOP);
+	addlayobj(nil, "topf", nil, dLEFT, but);
+	addlayobj(nil, "topf", nil, dLEFT|EXPAND|FILLX, entry);
+
+	addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+
+	addlayframe("left", "arena", nil, dLEFT|EXPAND, dTOP);
+	addlayframe("central", "arena", nil, dLEFT|EXPAND, dTOP);
+	addlayframe("right", "arena", nil, dLEFT|EXPAND, dTOP);
+
+	rows = array[10] of {* => newstack(nil, Rowpilespec, "row")};
+	central = array[4] of {* => newstack(nil, Centralpilespec, "central")};
+	chokey = newstack(nil, Centralpilespec, "chokey");
+
+	deck = clique.newobject(nil, All, "stack");
+	cardlib->makecards(deck, (0, 13), nil);
+	cardlib->shuffle(deck);
+
+	for (i := 0; i < 5; i++)
+		addlayobj(nil, "left", nil, dTOP|oRIGHT, rows[i]);
+	for (i = 5; i < 10; i++)
+		addlayobj(nil, "right", nil, dTOP|oRIGHT, rows[i]);
+	for (i = 0; i < 4; i++)
+		addlayobj(nil, "central", nil, dTOP, central[i]);
+	addlayobj(nil, "central", nil, dTOP, chokey);
+
+	for (i = 0; i < 52; i++)
+		cardlib->setface(deck.children[i], 1);
+	# get top card from deck for central piles.
+	c := deck.children[len deck.children - 1];
+	v := cardlib->getcard(c);
+	j := 0;
+	for (i = len deck.children - 1; i >= 0; i--) {
+		w := cardlib->getcard(deck.children[i]);
+		if (w.number == v.number)
+			deck.transfer((i, i + 1), central[j++], -1);
+	}
+	for (i = 0; i < 10; i += 5) {
+		for (j = i; j < i + 4; j++)
+			deck.transfer((0, 5), rows[j], -1);
+		deck.transfer((0, 4), rows[j], -1);
+	}
+}
+
+redeal()
+{
+	for (i := 0; i < len rows; i++)
+		cardlib->discard(rows[i], deck, 0);
+	cardlib->shuffle(deck);
+
+	i = 0;
+	while ((n := len deck.children) > 0) {
+		l, r: int;
+		if (n >= 10)
+			l = r = 5;
+		else {
+			l = n / 2;
+			r = n - l;
+		}
+		deck.transfer((0, l), rows[i], 0);
+		deck.transfer((0, r), rows[i + 5], 0);
+		i++;
+	}
+
+	n = cardlib->nmembers();
+	for (i = 0; i < n; i++)
+		Cmember.index(i).sel.set(nil);
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype: string): ref Object
+{
+	stack := cardlib->newstack(parent, nil, spec);
+	stack.setattr("type", stype, None);
+	stack.setattr("actions", "click", All);
+	return stack;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+	if (cp.sel.isempty()) {
+		cp.sel.set(stack);
+		cp.sel.setrange(r);
+	} else {
+		if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+			cp.sel.set(nil);
+		else
+			cp.sel.setrange(r);
+	}
+}
+
+archivearray(a: array of ref Object, name: string)
+{
+	for (i := 0; i < len a; i++)
+		cardlib->setarchivename(a[i], name + string i);
+}
+
+unarchivearray(a: array of ref Object, name: string)
+{
+	for (i := 0; i < len a; i++)
+		a[i] = cardlib->getarchiveobj(name + string i);
+}
--- /dev/null
+++ b/appl/spree/engines/bounce.b
@@ -1,0 +1,258 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "sets.m";
+	sets: Sets;
+	Set, All, None, A, B: import sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member: import spree;
+include "../gather.m";
+
+clique: ref Clique;
+
+W, H: con 500;
+INSET: con 20;
+D: con 30;
+BATLEN: con 100.0;
+GOALSIZE: con 0.1;
+
+MAXPLAYERS: con 32;
+nmembers := 0;
+
+Line: adt {
+	p1, p2: Point;
+	seg: fn(l: self Line, s1, s2: real): Line;
+};
+
+Dmember: adt {
+	p:		ref Member;
+	score:	int;
+	bat:		ref Object;
+};
+
+Eusage: con "bad command usage";
+colours := array[4] of {"blue", "orange", "yellow", "white"};
+batpos: array of Line;
+borderpos: array of Line;
+
+members: array of Dmember;
+arena: ref Object;
+clienttype(): string
+{
+	return "bounce";
+}
+
+maxmembers(): int
+{
+	return 4;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	clique = g;
+	spree = srvmod;
+
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->print("spit: cannot load %s: %r\n", Sets->PATH);
+		return "bad module";
+	}
+	sets->init();
+
+	r := Rect((0, 0), (W, H));
+	walls := sides(r.inset(INSET));
+	addlines(segs(walls, 0.0, 0.5 - GOALSIZE), nil);
+	addlines(segs(walls, 0.5 + GOALSIZE, 1.0), nil);
+
+	batpos = l2a(segs(sides(r.inset(INSET + 50)), 0.1, 0.9));
+	borderpos = l2a(sides(r.inset(-1)));
+
+	arena = clique.newobject(nil, All, "arena");
+	arena.setattr("arenasize", string W + " " + string H, All);
+
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members < 2)
+		return "need at least two members";
+	if (len members > 4)
+		return "too many members";
+	return nil;
+}
+
+archive()
+{
+}		
+
+start(pl: array of ref Member, archived: int)
+{
+	if (archived) {
+	} else {
+		members = array[len pl] of Dmember;
+		for (i := 0; i < len pl; i++) {
+			p := pl[i];
+			bat := addline(batpos[i], nil);
+			bat.setattr("pos", "10 " + string (10.0 + BATLEN), All);
+			bat.setattr("owner", p.name, All);
+			addline(borderpos[i], ("owner", p.name) :: nil);
+			arena.setattr("member" + string i, p.name + " " + colours[i], All);
+			members[i] = (p, 0, bat);
+		}
+		r := Rect((0, 0), (W, H)).inset(INSET + 1);
+		goals := l2a(sides(r));
+		for (i = len members; i < len batpos; i++) {
+			addline(goals[i], nil);
+			addline(borderpos[i], ("owner", pl[0].name) :: nil);
+		}
+	}
+}
+
+addline(lp: (Point, Point), attrs: list of (string, string)): ref Object
+{
+	(p1, p2) := lp;
+	l := clique.newobject(nil, All, "line");
+	l.setattr("coords", p2s(p1) + " " + p2s(p2), All);
+	l.setattr("id", string l.id, All);
+	for (; attrs != nil; attrs = tl attrs) {
+		(attr, val) := hd attrs;
+		l.setattr(attr, val, All);
+	}
+	return l;
+}
+
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+command(member: ref Member, cmd: string): string
+{
+	ord := order(member);
+	sys->print("cmd: %s", cmd);
+	{
+		(n, toks) := sys->tokenize(cmd, " \n");
+		assert(n > 0, "unknown command");
+		case hd toks {
+		"newball" =>
+			# newball batid p.x p.y v.x v.y speed
+			assert(n == 7, Eusage);
+			bat := member.obj(int hd tl toks);
+			assert(bat != nil, "no such bat");
+			ball := clique.newobject(nil, All, "ball");
+			ball.setattr("state", string bat.id +  " " + string ord +
+				" " + concat(tl tl toks) + " " + string sys->millisec(), All);
+		"lost" =>
+			# lost ballid
+			assert(n == 2, Eusage);
+			o := member.obj(int hd tl toks);
+			assert(o != nil, "bad object");
+			assert(o.getattr("state") != nil, "can only lose balls");
+			o.delete();
+		"state" =>
+			# state ballid lasthit owner p.x p.y v.x v.y s time
+			assert(n == 10, Eusage);
+			assert(ord >= 0, "you are not playing");
+			o := member.obj(int hd tl toks);
+			assert(o != nil, "object does not exist");
+			o.setattr("state", concat(tl tl toks), All);
+			members[ord].score++;
+			arena.setattr("score" + string ord, string members[ord].score, All);
+		"bat" =>
+			# bat pos
+			assert(n == 2, Eusage);
+			s1 := real hd tl toks;
+			members[ord].bat.setattr("pos", hd tl toks + " " + string (s1 + BATLEN), All);
+		"time" =>
+			# time millisec
+			assert(n == 2, Eusage);
+			tm := int hd tl toks;
+			offset := sys->millisec() - tm;
+			clique.action("time " + string offset + " " + string tm, nil, nil, None.add(member.id));
+		* =>
+			assert(0, "bad command");
+		}
+	} exception e {
+	"parse:*" =>
+		return e[6:];
+	}
+	return nil;
+}
+
+order(p: ref Member): int
+{
+	for (i := 0; i < len members; i++)
+		if (members[i].p == p)
+			return i;
+	return -1;
+}
+
+assert(b: int, err: string)
+{
+	if (b == 0)
+		raise "parse:" + err;
+}
+
+concat(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
+
+Line.seg(l: self Line, s1, s2: real): Line
+{
+	(dx, dy) := (l.p2.x - l.p1.x, l.p2.y - l.p1.y);
+	return (((l.p1.x + int (s1 * real dx)), l.p1.y + int (s1 * real dy)),
+			((l.p1.x + int (s2 * real dx)), l.p1.y + int (s2 * real dy)));
+}
+
+sides(r: Rect): list of Line
+{
+	return ((r.min.x, r.min.y), (r.min.x, r.max.y)) ::
+		((r.max.x, r.min.y), (r.max.x, r.max.y)) ::
+		((r.min.x, r.min.y), (r.max.x, r.min.y)) ::
+		((r.min.x, r.max.y), (r.max.x, r.max.y)) :: nil;
+}
+
+addlines(ll: list of Line, attrs: list of (string, string))
+{
+	for (; ll != nil; ll = tl ll)
+		addline(hd ll, attrs);
+}
+
+segs(ll: list of Line, s1, s2: real): list of Line
+{
+	nll: list of Line;
+	for (; ll != nil; ll = tl ll)
+		nll = (hd ll).seg(s1, s2) :: nll;
+	ll = nil;
+	for (; nll != nil; nll = tl nll)
+		ll = hd nll :: ll;
+	return ll;
+}
+
+l2a(ll: list of Line): array of Line
+{
+	a := array[len ll] of Line;
+	for (i := 0; ll != nil; ll = tl ll)
+		a[i++] = hd ll;
+	return a;
+}
--- /dev/null
+++ b/appl/spree/engines/canfield.b
@@ -1,0 +1,340 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	All, None: import Sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	Selection, Cmember, Card: import cardlib;
+	dTOP, dRIGHT, dLEFT, oDOWN,
+	aCENTRELEFT, aUPPERRIGHT,
+	EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+
+sevens: array of ref Object;	# [7]
+spare1, spare2: ref Object;
+acepiles: array of ref Object;	# [4]
+top2botcount := 3;
+top2bot: ref Object;
+
+CLICK, TOP2BOT, REDEAL, SHOW: con iota;
+
+Openspec := Stackspec(
+	"display",		# style
+	19,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+Pilespec := Stackspec(
+	"pile",		# style
+	19,			# maxcards
+	0,			# conceal
+	"pile"		# title
+);
+
+Untitledpilespec := Stackspec(
+	"pile",		# style
+	13,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+clienttype(): string
+{
+	return "cards";
+}
+
+rank := array[] of {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+
+maxmembers(): int
+{
+	return 1;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+	cardlib->init(spree, clique);
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members != 1)
+		return "one member only";
+	return nil;
+}
+
+start(members: array of ref Member, archived: int)
+{
+	allow->add(SHOW, nil, "show");
+	if (archived) {
+		archiveobj := cardlib->unarchive();
+		sevens = cardlib->getarchivearray("sevens");
+		acepiles = cardlib->getarchivearray("acepiles");
+		spare1 = cardlib->getarchiveobj("spare1");
+		spare2 = cardlib->getarchiveobj("spare2");
+		top2bot = cardlib->getarchiveobj("top2bot");
+		top2botcount = int archiveobj.getattr("top2botcount");
+
+		allow->unarchive(archiveobj);
+		archiveobj.delete();
+	} else {
+		p := members[0];
+		Cmember.join(p, -1).layout.lay.setvisibility(All);
+		startclique();
+		allow->add(CLICK, p, "click %o %d");
+		allow->add(TOP2BOT, p, "top2bot");
+		allow->add(REDEAL, p, "redeal");
+	}
+}
+
+archive()
+{
+	archiveobj := cardlib->archive();
+	cardlib->archivearray(sevens, "sevens");
+	cardlib->archivearray(acepiles, "acepiles");
+	cardlib->setarchivename(spare1, "spare1");
+	cardlib->setarchivename(spare2, "spare2");
+	cardlib->setarchivename(top2bot, "top2bot");
+	archiveobj.setattr("top2botcount", string top2botcount, None);
+	allow->archive(archiveobj);
+}
+
+command(p: ref Member, cmd: string): string
+{
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil)
+		return err;
+	cp := Cmember.find(p);
+	if (cp == nil)
+		return "you are not playing";
+
+	case tag {
+	CLICK =>
+		# click stack index
+		stack := clique.objects[int hd tl toks];
+		nc := len stack.children;
+		idx := int hd tl tl toks;
+		sel := cp.sel;
+		stype := stack.getattr("type");
+		if (sel.isempty() || sel.stack == stack) {
+			if (nc == 0 && stype == "spare1") {
+				cardlib->flip(spare2);
+				spare2.transfer((0, len spare2.children), spare1, 0);
+				return nil;
+			}
+			if (idx < 0 || idx >= len stack.children)
+				return "invalid index";
+			case stype {
+			"spare2" or
+			"open" =>
+				select(cp, stack, (idx, nc));
+			"spare1" =>
+				if ((n := nc) > 3)
+					n = 3;
+				for (i := 0; i < n; i++) {
+					cardlib->setface(stack.children[nc - 1], 1);
+					stack.transfer((nc - 1, nc), spare2, -1);
+					nc--;
+				}
+			* =>
+				return "you can't move cards from there";
+			}
+		} else {
+			from := sel.stack;
+			case stype {
+			"acepile" =>
+				if (sel.r.end != sel.r.start + 1)
+					return "only one card at a time!";
+				card := getcard(sel.stack.children[sel.r.start]);
+				if (nc == 0) {
+					if (card.number != 0)
+						return "aces only";
+				} else {
+					top := getcard(stack.children[nc - 1]);
+					if (card.number != top.number + 1)
+						return "out of sequence";
+					if (card.suit != top.suit)
+						return "wrong suit";
+				}
+				sel.transfer(stack, -1);
+			"open" =>
+				c := getcard(sel.stack.children[sel.r.start]);
+				col := !isred(c);
+				n := c.number + 1;
+				for (i := sel.r.start; i < sel.r.end; i++) {
+					c2 := getcard(sel.stack.children[i]);
+					if (c2.face == 0)
+						return "cannot move face-down cards";
+					if (isred(c2) == col)
+						return "bad colour sequence";
+					if (c2.number != n - 1)
+						return "bad number sequence";
+					n = c2.number;
+					col = isred(c2);
+				}
+				if (nc != 0) {
+					c2 := getcard(stack.children[nc - 1]);
+					if (isred(c2) == isred(c) || c2.number != c.number + 1)
+						return "invalid move";
+				} else if (c.number != 12)
+					return "only kings allowed there";
+				sel.transfer(stack, -1);
+			* =>
+				return "can't move there";
+			}
+			if (from.getattr("type") == "open" && len from.children > 0)
+				cardlib->setface(from.children[len from.children - 1], 1);
+		}
+	TOP2BOT =>
+		if (len spare2.children != 0)
+			return "can only top-to-bottom on the whole pile";
+		if (top2botcount <= 0)
+			return "too late";
+		nc := len spare1.children;
+		if (nc > 0) {
+			spare1.transfer((nc - 1, nc), spare1, 0);
+			top2botcount--;
+			settop2bottext();
+		}
+	REDEAL =>
+		clearup();
+		cardlib->shuffle(spare1);
+		deal();
+		top2botcount = 3;
+		settop2bottext();
+	SHOW =>
+		clique.show(nil);
+	}
+	return nil;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+settop2bottext()
+{
+	top2bot.setattr("text",
+		sys->sprint("top to bottom (%d left)", top2botcount), All);
+}
+
+startclique()
+{
+	addlayobj, addlayframe: import cardlib;
+
+	entry := clique.newobject(nil, All, "widget entry");
+	entry.setattr("command", "say", All);
+	addlayobj("entry", nil, nil, dTOP|FILLX, entry);
+	addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+
+	addlayframe("top", "arena", nil, dTOP|EXPAND, dTOP);
+	addlayframe("mid", "arena", nil, dTOP|EXPAND, dTOP);
+	addlayframe("bot", "arena", nil, dTOP|EXPAND, dTOP);
+
+	sevens = array[7] of {* => newstack(nil, Openspec, "open")};
+	acepiles = array[4] of {* => newstack(nil, Untitledpilespec, "acepile")};
+	spare1 = newstack(nil, Untitledpilespec, "spare1");
+	spare2 = newstack(nil, Untitledpilespec, "spare2");
+
+	cardlib->makecards(spare1, (0, 13), nil);
+
+	for (i := 0; i < 4; i++)
+		addlayobj(nil, "top", nil, dRIGHT, acepiles[i]);
+	for (i = 0; i < len sevens; i++)
+		addlayobj(nil, "mid", nil, dLEFT|oDOWN|EXPAND, sevens[i]);
+	addlayframe("buts", "bot", nil, dLEFT|EXPAND|aUPPERRIGHT, dTOP);
+	top2bot = newbutton("top2bot", "top to bottom");
+	addlayobj(nil, "buts", nil, dTOP, top2bot);
+	addlayobj(nil, "buts", nil, dTOP, newbutton("redeal", "redeal"));
+	addlayobj(nil, "bot", nil, dLEFT, spare1);
+	addlayobj(nil, "bot", nil, dLEFT|EXPAND|aCENTRELEFT, spare2);
+	deal();
+	settop2bottext();
+}
+
+clearup()
+{
+	for (i := 0; i < len sevens; i++)
+		cardlib->discard(sevens[i], spare1, 1);
+	for (i = 0; i < len acepiles; i++)
+		cardlib->discard(acepiles[i], spare1, 1);
+	cardlib->discard(spare2, spare1, 1);
+}
+
+deal()
+{
+	cardlib->shuffle(spare1);
+
+	for (i := 0; i < 7; i++) {
+		spare1.transfer((0, i + 1), sevens[i], 0);
+		cardlib->setface(sevens[i].children[i], 1);
+	}
+
+}
+
+newbutton(cmd, text: string): ref Object
+{
+	but := clique.newobject(nil, All, "widget button");
+	but.setattr("command", cmd, All);
+	but.setattr("text", text, All);
+	return but;
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype: string): ref Object
+{
+	stack := cardlib->newstack(parent, nil, spec);
+	stack.setattr("type", stype, None);
+	stack.setattr("actions", "click", All);
+	return stack;
+}
+
+getcard(card: ref Object): Card
+{
+	c := cardlib->getcard(card);
+	c.number = rank[c.number];
+	return c;
+}
+
+isred(c: Card): int
+{
+	return c.suit == Cardlib->DIAMONDS || c.suit == Cardlib->HEARTS;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+	if (cp.sel.isempty()) {
+		cp.sel.set(stack);
+		cp.sel.setrange(r);
+	} else {
+		if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+			cp.sel.set(nil);
+		else
+			cp.sel.setrange(r);
+	}
+}
--- /dev/null
+++ b/appl/spree/engines/chat.b
@@ -1,0 +1,60 @@
+implement Engine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member: import spree;
+
+clique: ref Clique;
+
+clienttype(): string
+{
+	return "chat";
+}
+
+init(g: ref Clique, srvmod: Spree): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+	return nil;
+}
+
+join(nil: ref Member): string
+{
+	return nil;
+}
+
+leave(nil: ref Member)
+{
+}
+
+Eusage: con "bad command usage";
+
+command(member: ref Member, cmd: string): string
+{
+	e := ref Sys->Exception;
+	if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+		sys->rescued(Sys->ONCE, nil);
+		return e.name[6:];
+	}
+	(n, toks) := sys->tokenize(cmd, " \n");
+	assert(n > 0, "unknown command");
+	case hd toks {
+	"say" =>
+		# say something
+		assert(n == 2, Eusage);
+		clique.action("say " + string member.id + " " + hd tl toks, nil, nil, ~0);
+	* =>
+		assert(0, "bad command");
+	}
+	return nil;
+}
+
+assert(b: int, err: string)
+{
+	if (b == 0)
+		sys->raise("parse:" + err);
+}
--- /dev/null
+++ b/appl/spree/engines/debug.b
@@ -1,0 +1,163 @@
+implement Engine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member: import spree;
+
+clique: ref Clique;
+
+init(g: ref Clique, srvmod: Spree): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+	return nil;
+}
+
+join(nil: ref Member): string
+{
+	return nil;
+}
+
+leave(nil: ref Member)
+{
+}
+
+number := 0;
+currmember: ref Member;
+
+obj(ext: int): ref Object
+{
+	o := currmember.obj(ext);
+	if (o == nil)
+		sys->raise("parse:bad object");
+	return o;
+}
+
+Eusage: con "bad command usage";
+
+assert(b: int, err: string)
+{
+	if (b == 0)
+		sys->raise("parse:" + err);
+}
+
+command(member: ref Member, cmd: string): string
+{
+	e := ref Sys->Exception;
+	if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+		sys->rescued(Sys->ONCE, nil);
+		currmember = nil;
+		return e.name[6:];
+	}
+	currmember = member;
+	(nlines, lines) := sys->tokenize(cmd, "\n");
+	assert(nlines > 0, "unknown command");
+	(n, toks) := sys->tokenize(hd lines, " ");
+	assert(n > 0, "unknown command");
+	case hd toks {
+	"new" =>			# new parent visibility\nvisibility attr val\nvisibility attr val...
+		assert(n == 3, Eusage);
+		setattrs(clique.newobject(obj(int hd tl toks), int hd tl tl toks), tl lines);
+	"deck" =>
+		stack := clique.newobject(nil, ~0);
+		stack.setattr("type", "stack", ~0);
+		for (i := 0; i < 6; i++) {
+			o := clique.newobject(stack, ~0);
+			o.setattr("face", "down", ~0);
+			o.setattr("number", string number++, 0);
+		}
+	"flip" =>
+		# flip objid start [end]
+		assert(n == 2 || n == 3 || n == 4, Eusage);
+		o := obj(int hd tl toks);
+		if (n > 2) {
+			start := int hd tl tl toks;
+			end := start + 1;
+			if (n == 4)
+				end = int hd tl tl tl toks;
+			assert(start >= 0 && start < len o.children &&
+					end >= start && end >= 0 && end <= len o.children, "index out of range");
+			for (; start < end; start++)
+				flip(o.children[start]);
+		} else
+			flip(o);
+		
+	"set" =>			# set objid attr val
+		assert(n == 4, Eusage);
+		obj(int hd tl toks).setattr(hd tl tl toks, hd tl tl tl toks, ~0);
+	"vis" =>			# vis objid flags
+		assert(n == 3, Eusage);
+		obj(int hd tl toks).setvisibility(int hd tl tl toks);
+	"attrvis" =>		# attrvis objid attr flags
+		assert(n == 4, Eusage);
+		o := obj(int hd tl toks);
+		name := hd tl tl toks;
+		attr := o.attrs.get(name);
+		assert(attr != nil, "attribute not found");
+		o.setattrvisibility(name, int hd tl tl tl toks);
+	"show" =>			# show [memberid]
+		p: ref Member = nil;
+		if (n == 2) {
+			memberid := int hd tl toks;
+			p = clique.member(memberid);
+			assert(p != nil, "bad memberid");
+		}
+		clique.show(p);
+	"del" or "delete" =>			# del obj
+		assert(n == 2, Eusage);
+		obj(int hd tl toks).delete();
+	"tx" =>					# tx src from to dest [index]
+		assert(n == 5 || n == 6, Eusage);
+		src, dest: ref Object;
+		r: Range;
+		(src, toks) = (obj(int hd tl toks), tl tl toks);
+		(r.start, toks) = (int hd toks, tl toks);
+		(r.end, toks) = (int hd toks, tl toks);
+		(dest, toks) = (obj(int hd toks), tl toks);
+		index := len dest.children;
+		if (n == 6)
+			index = int hd toks;
+		assert(r.start >= 0 && r.start < len src.children &&
+				r.end >= 0 && r.end <= len src.children && r.end >= r.start,
+				"bad range");
+		src.transfer(r, dest, index);
+	* =>
+		assert(0, "bad command");
+	}
+	currmember = nil;
+	return nil;
+}
+
+
+flip(o: ref Object)
+{
+	face := o.getattr("face");
+	if (face == "down") {
+		face = "up";
+		o.setattrvisibility("number", ~0);
+	} else {
+		face = "down";
+		o.setattrvisibility("number", 0);
+	}
+	o.setattr("face", face, ~0);
+}
+
+setattrs(o: ref Object, lines: list of string): string
+{
+	for (; lines != nil; lines = tl lines) {
+		# attr val [visibility]
+		(n, toks) := sys->tokenize(hd lines, " ");
+		if (n != 2 && n != 3)
+			return "bad attribute line";
+		vis := 0;
+		if (n == 3)
+			vis = int hd tl tl toks;
+		o.setattr(hd toks, hd tl toks, vis);
+	}
+	return nil;
+}
+
--- /dev/null
+++ b/appl/spree/engines/freecell.b
@@ -1,0 +1,428 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, set, A, B, All, None: import sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	Selection, Cmember, Card: import cardlib;
+	getcard: import cardlib;
+	dTOP, dRIGHT, dLEFT, oRIGHT, oDOWN,
+	aCENTRERIGHT, aCENTRELEFT, aUPPERRIGHT,
+	EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+
+open: array of ref Object;		# [8]
+cells: array of ref Object;		# [4]
+acepiles: array of ref Object;	# [4]
+txpiles: array of ref Object;	# [len open + len cells]
+deck: ref Object;
+
+fnames := array[] of {
+"qua",
+"quack",
+"quackery",
+"quad",
+"quadrangle",
+"quadrangular",
+"quadrant",
+"quadratic",
+"quadrature",
+"quadrennial",
+};
+dir(name: string, perm: int, owner: string): Sys->Dir
+{
+	d := Sys->zerodir;
+	d.name = name;
+	d.uid = owner;
+	d.gid = owner;
+	d.qid.qtype = (perm >> 24) & 16rff;
+	d.mode = perm;
+	# d.atime = now;
+	# d.mtime = now;
+	return d;
+}
+
+
+suitsout := array[4] of {* => -1};
+
+mainmember: ref Cmember;
+
+CLICK: con iota;
+
+Openspec := Stackspec(
+	"display",		# style
+	19,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+Pilespec := Stackspec(
+	"pile",		# style
+	19,			# maxcards
+	0,			# conceal
+	"pile"		# title
+);
+
+Untitledpilespec := Stackspec(
+	"pile",		# style
+	13,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+clienttype(): string
+{
+	return "cards";
+}
+
+maxmembers(): int
+{
+	return 1;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->print("whist: cannot load %s: %r\n", Sets->PATH);
+		return "bad module";
+	}
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+	cardlib->init(spree, clique);
+	g.fcreate(0, -1, dir("data", 8r555|Sys->DMDIR, "spree"));
+	for(i := 0; i < len fnames; i++)
+		g.fcreate(i + 1, 0, dir(fnames[i], 8r444, "arble"));
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members != 1)
+		return "one member only";
+	return nil;
+}
+
+start(members: array of ref Member, archived: int)
+{
+sys->print("freecell: starting\n");
+	if (archived) {
+		archiveobj := cardlib->unarchive();
+		open = cardlib->getarchivearray("open");
+		cells = cardlib->getarchivearray("cells");
+		acepiles = cardlib->getarchivearray("acepiles");
+		txpiles = cardlib->getarchivearray("txpiles");
+		deck = cardlib->getarchiveobj("deck");
+		for (i := 0; i < len suitsout; i++)
+			suitsout[i] = int archiveobj.getattr("suitsout" + string i);
+		mainmember = Cmember.findid(int archiveobj.getattr("mainmember"));
+		allow->unarchive(archiveobj);
+		archiveobj.delete();
+	} else {
+		sys->print("freecell: starting afresh\n");
+		mainmember = Cmember.join(members[0], -1);
+		mainmember.layout.lay.setvisibility(All);
+		startclique();
+		movefree();
+		allow->add(CLICK, members[0], "click %o %d");
+	}
+}
+
+readfile(f: int, boffset: big, n: int): array of byte
+{
+	offset := int boffset;
+	f--;
+	if (f < 0 || f >= len fnames)
+		return nil;
+	data := array of byte fnames[f];
+	if (offset >= len data)
+		return nil;
+	if (offset + n > len data)
+		n = len data - offset;
+	return data[offset:offset + n];
+}
+
+archive()
+{
+	sys->print("freecell: archiving\n");
+	archiveobj := cardlib->archive();
+	cardlib->archivearray(open, "open");
+	cardlib->archivearray(cells, "cells");
+	cardlib->archivearray(acepiles, "acepiles");
+	cardlib->archivearray(txpiles, "txpiles");
+	cardlib->setarchivename(deck, "deck");
+	for (i := 0; i < len suitsout; i++)
+		archiveobj.setattr("suitsout" + string i, string suitsout[i], None);
+	archiveobj.setattr("mainmember", string mainmember.id, None);
+	allow->archive(archiveobj);
+}
+
+command(p: ref Member, cmd: string): string
+{
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil)
+		return err;
+	cp := Cmember.find(p);
+	if (cp == nil)
+		return "you are not playing";
+	case tag {
+	CLICK =>
+		# click stack index
+		stack := clique.objects[int hd tl toks];
+		nc := len stack.children;
+		idx := int hd tl tl toks;
+		sel := cp.sel;
+		stype := stack.getattr("type");
+		if (sel.isempty() || sel.stack == stack) {
+			if (idx < 0 || idx >= len stack.children)
+				return "invalid index";
+			case stype {
+			"cell" or
+			"open" =>
+				select(cp, stack, (idx, nc));
+			* =>
+				return "you can't move cards from there";
+			}
+		} else {
+			from := sel.stack;
+			case stype {
+			"acepile" =>
+				if (sel.r.end != sel.r.start + 1)
+					return "only one card at a time!";
+				addtoacepile(sel.stack);
+				sel.set(nil);
+				movefree();
+			"open" =>
+				c := getcard(sel.stack.children[sel.r.start]);
+				col := !isred(c.suit);
+				n := c.number + 1;
+				for (i := sel.r.start; i < sel.r.end; i++) {
+					c2 := getcard(sel.stack.children[i]);
+					if (isred(c2.suit) == col)
+						return "bad colour sequence";
+					if (c2.number != n - 1)
+						return "bad number sequence";
+					n = c2.number;
+					col = isred(c2.suit);
+				}
+				if (nc != 0) {
+					c2 := getcard(stack.children[nc - 1]);
+					if (isred(c2.suit) == isred(c.suit) || c2.number != c.number + 1)
+						return "opposite colours, descending, only";
+				}
+				r := sel.r;
+				selstack := sel.stack;
+				sel.set(nil);
+				fc := freecells(stack);
+				if (r.end - r.start - 1 > len fc)
+					return "not enough free cells";
+				n = 0;
+				for (i = r.end - 1; i >= r.start + 1; i--)
+					selstack.transfer((i, i + 1), fc[n++], -1);
+				selstack.transfer((i, i + 1), stack, -1);
+				while (--n >= 0)
+					fc[n].transfer((0, 1), stack, -1);
+				movefree();
+			"cell" =>
+				if (sel.r.end - sel.r.start > 1 || nc > 0)
+					return "only one card allowed there";
+				sel.transfer(stack, -1);
+				movefree();
+			* =>
+				return "can't move there";
+			}
+		}
+	}
+	return nil;
+}
+
+freecells(dest: ref Object): array of ref Object
+{
+	fc := array[len txpiles] of ref Object;
+	n := 0;
+	for (i := 0; i < len txpiles; i++)
+		if (len txpiles[i].children == 0 && txpiles[i] != dest)
+			fc[n++] = txpiles[i];
+	return fc[0:n];
+}
+
+# move any cards that can be moved.
+movefree()
+{
+	nmoved := 1;
+	while (nmoved > 0) {
+		nmoved = 0;
+		for (i := 0; i < len txpiles; i++) {
+			pile := txpiles[i];
+			nc := len pile.children;
+			if (nc == 0)
+				continue;
+			card := getcard(pile.children[nc - 1]);
+			if (suitsout[card.suit] != card.number - 1)
+				continue;
+			# card can be moved; now make sure there's no card out
+			# that might be moved onto this card
+			for (j := 0; j < len suitsout; j++)
+				if (isred(j) != isred(card.suit) && card.number > 1 && suitsout[j] < card.number - 1)
+					break;
+			if (j == len suitsout) {
+				addtoacepile(pile);
+				nmoved++;
+			}
+		}
+	}
+}
+
+addtoacepile(pile: ref Object)
+{
+	nc := len pile.children;
+	if (nc == 0)
+		return;
+	card := getcard(pile.children[nc - 1]);
+	for (i := 0; i < len acepiles; i++) {
+		anc := len acepiles[i].children;
+		if (anc == 0) {
+			if (card.number == 0)
+				break;
+			continue;
+		}
+		acard := getcard(acepiles[i].children[anc - 1]);
+		if (acard.suit == card.suit && acard.number == card.number - 1)
+			break;
+	}
+	if (i < len acepiles) {
+		pile.transfer((nc - 1, nc), acepiles[i], -1);
+		suitsout[card.suit] = card.number;
+	}
+}
+
+startclique()
+{
+	addlayobj, addlayframe: import cardlib;
+
+	open = array[8] of {* => newstack(nil, Openspec, "open", nil)};
+	acepiles = array[4] of {* => newstack(nil, Untitledpilespec, "acepile", nil)};
+	cells = array[4] of {* => newstack(nil, Untitledpilespec, "cell", "cell")};
+	for (i := 0; i < len cells; i++)
+		cells[i].setattr("showsize", "0", All);
+
+	txpiles = array[12] of ref Object;
+	txpiles[0:] = open;
+	txpiles[len open:] = cells;
+	deck = clique.newobject(nil, All, "stack");
+
+	cardlib->makecards(deck, (0, 13), nil);
+
+	addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+	addlayframe("top", "arena", nil, dTOP|EXPAND, dTOP);
+	addlayframe("bot", "arena", nil, dTOP|EXPAND, dTOP);
+	for (i = 0; i < 4; i++)
+		addlayobj(nil, "top", nil, dRIGHT, acepiles[i]);
+	for (i = 0; i < 4; i++)
+		addlayobj(nil, "top", nil, dLEFT, cells[i]);
+	for (i = 0; i < len open; i++)
+		addlayobj(nil, "bot", nil, dLEFT|oDOWN|EXPAND, open[i]);
+	deal();
+}
+
+deal()
+{
+	cardlib->shuffle(deck);
+	cardlib->deal(deck, 7, open, 0);
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype, title: string): ref Object
+{
+	stack := cardlib->newstack(parent, nil, spec);
+	stack.setattr("type", stype, None);
+	stack.setattr("actions", "click", All);
+	stack.setattr("title", title, All);
+	return stack;
+}
+
+isred(suit: int): int
+{
+	return suit == Cardlib->DIAMONDS || suit == Cardlib->HEARTS;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+	if (cp.sel.isempty()) {
+		cp.sel.set(stack);
+		cp.sel.setrange(r);
+	} else {
+		if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+			cp.sel.set(nil);
+		else
+			cp.sel.setrange(r);
+	}
+}
+
+#randstate := 1;
+#srand(seed: int)
+#{
+#        randstate = seed;
+#}
+#
+#rand(): int
+#{
+#	randstate = randstate * 214013 + 2531011;
+#	return (randstate >> 16) & 0x7fff;
+#}
+##From: jimh@MICROSOFT.com (Jim Horne)
+##
+##I'm happy to share the card shuffle algorithm, but I warn you,
+##it does depend on the rand() and srand() function built into MS
+##compilers.  The good news is that I believe these work the same
+##for all our compilers.
+##
+##I use cards.dll which has it's own mapping of numbers (0-51) to
+##cards.  The following will give you the idea.  Play around with
+##this and you'll be able to generate all the cliques.
+##
+##Go ahead and post the code.  People might as well have fun with it.
+##Please keep me posted on anything interesting that comes of it.  
+##Thanks.
+#
+#msdeal(cliquenumber: int): array of array of Card
+#{
+#	deck := array[52] of Card;
+#	for (i := 0; i < len deck; i++)	# put unique card in each deck loc.
+#		deck[i] = Card(i % 4, i / 4, 0);
+#	wleft := 52;				# cards left to be chosen in shuffle
+#	cards := array[8] of {* => array[7] of Card};
+#	max := array[8] of {* => 0};
+#	srand(cliquenumber);
+#	for (i = 0; i < 52; i++)	{
+#		j := rand() % wleft;
+#		card[i % 8][i / 8] = deck[j];
+#		max[i % 8] = i / 8;
+#		deck[j] = deck[--wleft];
+#	}
+#	for (i = 0; i < len cards; i++)
+#		cards[i] = cards[i][0:max[i]];
+#	return cards;
+#}
--- /dev/null
+++ b/appl/spree/engines/gather.b
@@ -1,0 +1,267 @@
+implement Engine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, set, A, B, All, None: import sets;
+include "../spree.m";
+	spree: Spree;
+	archives: Archives;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "daytime.m";
+	daytime: Daytime;
+include "../gather.m";
+
+clique: ref Clique;
+
+started := 0;
+halted := 0;
+suspended: Set;		# set of members currently suspended from the clique.
+count := 0;
+nmembers := 0;
+title := "unknown";
+cliquemod: Gatherengine;
+
+members: Set;
+watchers: Set;
+
+invited: list of string;
+
+# options:
+# <n> cliquemodule opts
+init(srvmod: Spree, g: ref Clique, argv: list of string): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->print("gather: cannot load %s: %r\n", Sets->PATH);
+		return "bad module";
+	}
+	sets->init();
+	daytime = load Daytime Daytime->PATH;
+	if (daytime == nil) {
+		sys->print("gather: cannot load %s: %r\n", Daytime->PATH);
+		return "bad module";
+	}
+	archives = load Archives Archives->PATH;
+	if (archives == nil) {
+		sys->print("gather: cannot load %s: %r\n", Archives->PATH);
+		return "bad module";
+	}
+	archives->init(srvmod);
+	argv = tl argv;
+	n := len argv;
+	if (n < 2)
+		return "bad init options";
+	count = int hd argv;
+	if (count != -1 && count <= 0)
+		return "bad gather count";
+	argv = tl argv;
+	if (count < len clique.archive.members)
+		count = len clique.archive.members;
+	cliquemod = load Gatherengine "/dis/spree/engines/" + hd argv + ".dis";
+	if (cliquemod == nil)
+		return sys->sprint("bad module: %r");
+	title = concat(argv);
+	e := cliquemod->init(srvmod, clique, tl argv, len clique.archive.members > 0);
+	if (e != nil)
+		return e;
+	if (len clique.archive.members > 0) {
+		for (i := 0; i < len clique.archive.members; i++)
+			invited = clique.archive.members[i] :: invited;
+	} else
+		invited = clique.owner() :: nil;
+	for (inv := invited; inv != nil; inv = tl inv)
+		clique.notify(clique.parentid, "invite " + hd inv);
+	clique.notify(clique.parentid, "title (" + title + ")");
+	return nil;
+}
+
+join(p: ref Member, cmd: string, susp: int): string
+{
+sys->print("gather: %s[%d] joining '%s' (suspended: %d)\n", p.name, p.id, cmd, susp);
+	case cmd {
+	"join" =>
+		if (started) {
+			if (!susp || !halted)
+				return "clique has already started";
+			suspended = suspended.del(p.id);
+			if (suspended.eq(None)) {
+				halted = 0;
+				# XXX inform participants that clique is starting again
+			}
+			pset := None.add(p.id);
+			clique.action("clienttype " + cliquemod->clienttype(), nil, nil, pset);
+			clique.breakmsg(pset);
+			return nil;
+		}
+		for (inv := invited;  inv != nil; inv = tl inv)
+			if (hd inv == p.name || hd inv == "all")
+				break;
+		if (inv == nil)
+			return "you have not been invited";
+		if (nmembers >= cliquemod->maxmembers() || (count != -1 && nmembers >= count))
+			return "too many members already";
+		if (len clique.archive.members > 0) {
+			for (i := 0; i < len clique.archive.members; i++)
+				if (p.name == clique.archive.members[i])
+					break;
+			if (i == len clique.archive.members)
+				return "you are not part of that clique";
+		}
+		nmembers++;
+		members = members.add(p.id);
+		clique.notify(clique.parentid, "join " + p.name);
+		s := None.add(p.id);
+		# special case for single member cliques: don't need a gather client as we can start right now.
+		if (cliquemod->maxmembers() == 1)
+			return startclique();
+		clique.action("clienttype gather", nil, nil, s);
+		clique.breakmsg(s);
+		clique.action("title " + title, nil, nil, s);
+		clique.action("join " + p.name, nil, nil, All);
+	"watch" =>
+		if (susp)
+			return "you cannot watch if you are playing";
+		watchers = watchers.add(p.id);
+		s := None.add(p.id);
+		if (started)
+			clique.action("clienttype " + cliquemod->clienttype(), nil, nil, s);
+		else
+			clique.action("clienttype gather", nil, nil, s);
+		clique.breakmsg(s);
+		if (!started)
+			clique.action("watch " + p.name, nil, nil, All);
+	* =>
+		return "unknown join request";
+	}
+	return nil;
+}
+
+leave(p: ref Member): int
+{
+	if (members.holds(p.id)) {
+		if (started) {
+			suspended = suspended.add(p.id);
+			if (suspended.eq(members)) {
+				cliquemod->archive();
+				name := spree->newarchivename();
+				e := archives->write(clique,
+					("title", concat(tl tl clique.archive.argv)) :: 
+					("date", string daytime->now()) :: nil,
+					name, members);
+				if (e != nil)
+					sys->print("warning: cannot archive clique: %s\n", e);
+				else
+					clique.notify(clique.parentid, "archived " + name);
+				clique.hangup();
+				return 1;
+			} else {
+				halted = 1;
+				return 0;
+			}
+		}
+
+		members = members.del(p.id);
+		nmembers--;
+		clique.notify(clique.parentid, "leave " + p.name);
+		if (nmembers == 0)
+			clique.hangup();
+	} else {
+		watchers = watchers.del(p.id);
+		clique.action("unwatch " + p.name, nil, nil, All);
+	}
+	return 1;
+}
+
+notify(nil: int, note: string)
+{
+	(n, toks) := sys->tokenize(note, " ");
+	case hd toks {
+	"invite" =>
+		invited = hd tl toks :: invited;
+	"uninvite" =>
+		inv := invited;
+		for (invited = nil; inv != nil; inv = tl inv)
+			if (hd inv != hd tl toks)
+				invited = hd inv :: invited;
+	* =>
+		sys->print("gather: unknown notification '%s'\n", note);
+	}
+}
+
+command(p: ref Member, cmd: string): string
+{
+	if (halted)
+		return "clique is halted for the time being";
+	if (started) {
+		if (!members.holds(p.id)) {
+sys->print("members (%s) doesn't hold %s[%d]\n", members.str(), p.name, p.id);
+			return "you are only watching";
+		}
+		return cliquemod->command(p, cmd);
+	}
+
+	(n, toks) := sys->tokenize(cmd, " \n");
+	if (n == 0)
+		return "bad command";
+	case hd toks {
+	"start" =>
+		if (len clique.archive.members == 0 && p.name != clique.owner())
+			return "only the owner can start a clique";
+		if (count != -1 && nmembers != count)
+			return "need " + string count + " members";
+		return startclique();
+	"chat" =>
+		clique.action("chat " + p.name + " " + concat(tl toks), nil, nil, All);
+	* =>
+		return "unknown command";
+	}
+	return nil;
+}
+
+startclique(): string
+{
+	# XXX could randomly shuffle members here
+
+	pa := array[nmembers] of ref Member;
+	names := array[nmembers] of string;
+	j := nmembers;
+	for (i := members.limit(); i >= 0; i--)
+		if (members.holds(i)) {
+			pa[--j] = clique.member(i);
+			names[j] = pa[j].name;
+		}
+	e := cliquemod->propose(names);
+	if (e != nil)
+		return e;
+	clique.action("clienttype " + cliquemod->clienttype(), nil, nil, All);
+	clique.breakmsg(All);
+	cliquemod->start(pa, len clique.archive.members > 0);
+	clique.start();
+	started = 1;
+	clique.notify(clique.parentid, "started");
+	clique.notify(clique.parentid, "title " + concat(tl tl clique.archive.argv));
+	return nil;
+}
+
+readfile(f: int, offset: big, n: int): array of byte
+{
+	if (!started)
+		return nil;
+	return cliquemod->readfile(f, offset, n);
+}
+
+concat(l: list of string): string
+{
+	if (l == nil)
+		return nil;
+	s := hd l;
+	for (l = tl l; l != nil; l = tl l)
+		s += " " + hd l;
+	return s;
+}
--- /dev/null
+++ b/appl/spree/engines/hearts.b
@@ -1,0 +1,300 @@
+implement Engine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	dTOP, dLEFT, oLEFT, oRIGHT, EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "tricks.m";
+	tricks: Tricks;
+	Trick: import tricks;
+clique: ref Clique;
+CLICK, START, SAY: con iota;
+
+started := 0;
+
+buttons: ref Object;
+scores: ref Object;
+deck, pile: ref Object;
+hands, taken, passon: array of ref Object;
+
+MINPLAYERS: con 2;
+MAXPLAYERS: con 4;
+
+leader, turn: int;
+trick: ref Trick;
+
+Trickpilespec := Stackspec(
+	"display",		# style
+	4,			# maxcards
+	0,			# conceal
+	"trick pile"	# title
+);
+
+Handspec := Stackspec(
+	"display",
+	13,
+	1,
+	""
+);
+
+Passonspec := Stackspec(
+	"display",
+	3,
+	0,
+	"pass on"
+);
+
+Takenspec := Stackspec(
+	"pile",
+	52,
+	0,
+	"tricks"
+);
+
+clienttype(): string
+{
+	return "cards";
+}
+
+init(g: ref Clique, srvmod: Spree): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("hearts: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+	allow->add(SAY, nil, "say &");
+
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("hearts: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+	cardlib->init(spree, clique);
+
+	tricks = load Tricks Tricks->PATH;
+	if (tricks == nil) {
+		sys->print("hearts: cannot load %s: %r\n", Tricks->PATH);
+		return "bad module";
+	}
+	tricks->init(spree, clique, cardlib);
+
+	deck = clique.newobject(nil, ~0, "stack");
+	cardlib->makecards(deck, (0, 13), 1);
+	cardlib->shuffle(deck);
+	buttons = clique.newobject(nil, ~0, "buttons");
+	scores = clique.newobject(nil, ~0, "scoretable");
+
+	return nil;
+}
+
+join(p: ref Member): string
+{
+	sys->print("%s(%d) joining\n", p.name(), p.id);
+	if (!started && cardlib->nmembers() < MAXPLAYERS) {
+		(nil, err) := cardlib->join(p, -1);
+		if (err == nil) {
+			if (cardlib->nmembers() == MINPLAYERS) {
+				mkbutton("Start", "start");
+				allow->add(START, nil, "start");
+			}
+		} else
+			sys->print("error on join: %s\n", err);
+	}
+	return nil;
+}
+		
+leave(p: ref Member)
+{
+	cardlib->leave(p);
+	started == 0;
+	if (cardlib->nmembers() < MINPLAYERS) {
+		buttons.deletechildren((0, len buttons.children));
+		allow->del(START, nil);
+	}
+}
+
+command(p: ref Member, cmd: string): string
+{
+	e := ref Sys->Exception;
+	if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+		sys->rescued(Sys->ONCE, nil);
+		return e.name[6:];
+	}
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil)
+		return err;
+	ord := cardlib->order(p);
+	case tag {
+	START =>
+		buttons.deletechildren((0, len buttons.children));
+		allow->del(START, nil);
+		startclique();
+		n := cardlib->nmembers();
+		leader = rand(n);
+		starthand();
+		titles := "";
+		for (i := 0; i < n; i++)
+			titles += cardlib->info(i).p.name() + " ";
+		clique.newobject(scores, ~0, "score").setattr("score", titles, ~0);
+
+	CLICK =>
+		# click stackid index
+		hand := hands[ord];
+		if (int hd tl toks != hand.id)
+			return "can't click there";
+		index := int hd tl tl toks;
+		if (index < 0 || index >= len hand.children)
+			return "index out of range";
+		cardlib->setsel(hands[ord], (index, len hands[ord].children), p);
+		break;
+		err := trick.play(cardlib->order(p), int hd tl toks);
+		if (err != nil)
+			return err;
+
+		turn = next(turn);		# clockwise
+		if (turn == leader) {			# come full circle
+			winner := trick.winner;
+			inf := cardlib->info(winner);
+			remark(sys->sprint("%s won the trick", inf.p.name()));
+			cardlib->discard(pile, taken[winner], 0);
+			taken[winner].setattr("title",
+				string (len taken[winner].children / cardlib->nmembers()) +
+				" " + "tricks", ~0);
+			o := cardlib->info(winner).obj;
+			trick = nil;
+			s := "";
+			for (i := 0; i < cardlib->nmembers(); i++) {
+				if (i == winner)
+					s += "1 ";
+				else
+					s += "0 ";
+			}
+			clique.newobject(scores, ~0, "score").setattr("score", s, ~0);
+			if (len hands[winner].children > 0) {
+				leader = turn = winner;
+				trick = Trick.new(pile, -1, hands);
+			} else {
+				remark("one round down, some to go");
+				leader = turn  = -1;		# XXX this round over
+			}
+		}
+		canplay(turn);
+	SAY =>
+		clique.action("say member " + string p.id + ": '" + joinwords(tl toks) + "'", nil, nil, ~0);
+	}
+	return nil;
+}
+
+startclique()
+{
+	cardlib->startclique();
+	entry := clique.newobject(nil, ~0, "widget entry");
+	entry.setattr("command", "say", ~0);
+	cardlib->addlayobj("entry", nil, nil, dTOP|FILLX, entry);
+	cardlib->addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+	cardlib->maketable("arena");
+
+	pile = cardlib->newstack(nil, nil, Trickpilespec);
+	cardlib->addlayobj(nil, "public", nil, dTOP|oLEFT, pile);
+	n := cardlib->nmembers();
+	hands = array[n] of ref Object;
+	taken = array[n] of ref Object;
+	passon = array[n] of ref Object;
+	tt := clique.newobject(nil, ~0, "widget menu");
+	tt.setattr("text", "hello", ~0);
+	for (ml := "one" :: "two" :: "three" :: nil; ml != nil; ml = tl ml) {
+		o := clique.newobject(tt, ~0, "menuentry");
+		o.setattr("text", hd ml, ~0);
+		o.setattr("command", hd ml, ~0);
+	}
+	for (i := 0; i < n; i++) {
+		inf := cardlib->info(i);
+		hands[i] = cardlib->newstack(inf.obj, inf.p, Handspec);
+		taken[i] = cardlib->newstack(inf.obj, inf.p, Takenspec);
+		passon[i] = cardlib->newstack(inf.obj, inf.p, Passonspec);
+		p := "p" + string i;
+		cardlib->addlayframe(p + ".f", p, nil, dLEFT|oLEFT, dTOP);
+		cardlib->addlayobj(nil, p + ".f", inf.layout, dTOP, tt);
+		cardlib->addlayobj(nil, p + ".f", nil, dTOP|oLEFT, hands[i]);
+		cardlib->addlayobj(nil, p, nil, dLEFT|oLEFT, taken[i]);
+		cardlib->addlayobj(nil, p, nil, dLEFT|oLEFT, passon[i]);
+	}
+}
+
+joinwords(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
+
+starthand()
+{
+	cardlib->deal(deck, 13, hands, 0);
+	trick = Trick.new(pile, -1, hands);
+	turn = leader;
+	canplay(turn);
+}
+
+canplay(ord: int)
+{
+	allow->del(CLICK, nil);
+	for (i := 0; i < cardlib->nmembers(); i++) {
+		inf := cardlib->info(i);
+		inf.obj.setattr("status", nil, 1<<inf.p.id);
+		hands[i].setattr("actions", nil, 1<<inf.p.id);
+	}
+	if (ord != -1) {
+		allow->add(CLICK, member(ord), "click %o %d");
+		inf := cardlib->info(ord);
+		inf.obj.setattr("status", "It's your turn to play", 1<<inf.p.id);
+		hands[ord].setattr("actions", "click", 1<<inf.p.id);
+	}
+}
+
+memberobj(p: ref Member): ref Object
+{
+	return cardlib->info(cardlib->order(p)).obj;
+}
+
+member(ord: int): ref Member
+{
+	return cardlib->info(ord).p;
+}
+
+next(i: int): int
+{
+	i++;
+	if (i >= cardlib->nmembers())
+		i = 0;
+	return i;
+}
+
+remark(s: string)
+{
+	clique.action("remark " + s, nil, nil, ~0);
+}
+
+mkbutton(text, cmd: string): ref Object
+{
+	but := clique.newobject(buttons, ~0, "button");
+	but.setattr("text", text, ~0);
+	but.setattr("command", cmd, ~0);
+	return but;
+}
--- /dev/null
+++ b/appl/spree/engines/liars.b
@@ -1,0 +1,490 @@
+implement Engine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+
+MAXPLAYERS: con 32;
+
+clique: ref Clique;
+
+# each member is described by a state machine.
+# a member progresses through the following states:
+#
+# Notplaying
+# 	istart			-> Havedice
+# 	otherstarts	-> Waiting
+# Havedice
+# 	declare		-> Waiting
+# 	look			-> Looking
+# Looking
+# 	expose		-> Looking
+# 	unexpose		-> Looking
+# 	declare		-> Waiting
+# 	roll			-> Rolled
+# Rolled
+# 	expose		-> Rolled
+# 	unexpose		-> Rolled
+# 	declare		-> Waiting
+# Waiting
+# 	queried		-> Queried
+# 	lost			-> Havedice
+# Queried
+# 	reject,win		-> Waiting
+# 	reject,lose	-> Havedice
+# 	accept		-> Havedice
+
+
+plate, cup, space, members: ref Object;
+dice := array[5] of ref Object;
+
+declared: int;
+
+# member states
+Notplaying, Havedice, Looking, Rolled, Waiting, Queried: con iota;
+
+# info on a particular member
+Info: adt {
+	state:	int;
+	id:		int;
+	member:	ref Object;
+	action:	ref Object;
+};
+
+info := array[MAXPLAYERS] of ref Info;
+plorder := array[MAXPLAYERS] of int;	# map member id to their place around the table
+nplaying := 0;
+nmembers := 0;
+turn := 0;
+
+clienttype(): string
+{
+	return "none";
+}
+
+init(g: ref Clique, srvmod: Spree): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+
+	plate = clique.newobject(nil, ~0, "plate");
+	cup = clique.newobject(plate, 0, "cup");
+	space = clique.newobject(plate, ~0, "space");
+	members = clique.newobject(nil, ~0, "members");
+
+	for (i := 0; i < len dice; i++) {
+		dice[i] = clique.newobject(cup, ~0, "die");
+		dice[i].setattr("number", string rand(6), ~0);
+	}
+
+	return nil;
+}
+
+join(member: ref Member): string
+{
+	check();
+	pmask := 1 << member.id;
+
+	ord := nmembers++;
+	inf := info[ord] = ref Info;
+	inf.state = -1;
+	inf.id = member.id;
+	inf.action = clique.newobject(nil, pmask, "actions" + string member.id);
+	plorder[member.id] = ord;
+	setstate(ord, Notplaying);
+	check();
+	return nil;
+}
+	
+leave(member: ref Member)
+{
+	check();
+
+	ord := plorder[member.id];
+	state := info[ord].state;
+	info[ord] = nil;
+	for (i := 0; i < nmembers; i++)
+		if (i != ord)
+			setstate(i, Notplaying);
+	nmembers--;
+	nplaying = 0;
+	clique.action("say member " + string ord + " has left. the clique stops.", nil, nil, ~0);
+	check();
+}
+
+currmember: ref Member;
+currcmd: string;
+command(member: ref Member, cmd: string): string
+{
+	check();
+	e := ref Sys->Exception;
+	if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+		sys->rescued(Sys->ONCE, nil);
+		check();
+		currmember = nil;
+		currcmd = nil;
+		return e.name[6:];
+	}
+	currmember = member;
+	currcmd = cmd;
+	(nlines, lines) := sys->tokenize(cmd, "\n");
+	assert(nlines > 0, "unknown command");
+	(n, toks) := sys->tokenize(hd lines, " ");
+	assert(n > 0, "unknown command");
+	pmask := 1 << member.id;
+	ord := plorder[member.id];
+	state := info[ord].state;
+	case hd toks {
+	"say" or
+	"show" or
+	"showme" =>
+		case hd toks {
+		"say" =>
+			clique.action("say member " + string member.id + ": '" + (hd lines)[4:] + "'", nil, nil, ~0);
+		"show" =>			# show [memberid]
+			p: ref Member = nil;
+			if (n == 2) {
+				memberid := int hd tl toks;
+				p = clique.member(memberid);
+				assert(p != nil, "bad memberid");
+			}
+			clique.show(p);
+		"showme" =>
+			clique.show(member);
+		}
+		currmember = nil;
+		currcmd = nil;
+		return nil;
+	}
+	case state {
+	Notplaying =>
+		case hd toks {
+		"start" =>
+			assert(nplaying == 0, "clique is in progress");
+			assert(nmembers > 1, "need at least two members");
+			newinfo := array[len info] of ref Info;
+			members.deletechildren((0, len members.children));
+			j := 0;
+			for (i := 0; i < len info; i++)
+				if (info[i] != nil)
+					newinfo[j++] = info[i];
+			info = newinfo;
+			nplaying = nmembers;
+			for (i = 0; i < nplaying; i++) {
+				info[i].member = clique.newobject(members, ~0, nil);
+				info[i].member.setattr("id", string info[i].id, ~0);
+			}
+			turn = rand(nplaying);
+			start();
+		* =>
+			assert(0, "you are not playing");
+		}
+	Havedice =>
+		case hd toks {
+		"declare" =>
+			# declare hand
+			declare(ord, tl toks);
+		"look" =>
+			cup.setattr("raised", "1", ~0);
+			cup.setvisibility(pmask);
+			setstate(ord, Looking);
+		* =>
+			assert(0, "bad command");
+		}
+	Looking =>
+		case hd toks {
+		"expose" or
+		"unexpose" =>
+			expose(n, toks);
+		"declare" =>
+			declare(ord, tl toks);
+		"roll" =>
+			# roll index...
+			# XXX should be able to roll in the open too
+			for (toks = tl toks; toks != nil; toks = tl toks) {
+				index := int hd toks;
+				checkrange((index, index), cup);
+				cup.children[index].setattr("number", string rand(6), ~0);
+			}
+			setstate(ord, Rolled);
+		* =>
+			assert(0, "bad command");
+		}
+	Rolled =>
+		case hd toks {
+		"expose" or
+		"unexpose" =>
+			expose(n, toks);
+		"declare" =>
+			declare(ord, tl toks);
+		* =>
+			assert(0, "bad command");
+		}
+	Waiting =>
+		assert(0, "not your turn");
+	Queried =>
+		case hd toks {
+		"reject" =>
+			# lift the cup!
+			cup.transfer((0, len cup.children), space, len space.children);
+			assert(len space.children == 5, "lost a die somewhere!");
+			dvals := array[5] of int;
+			for (i := 0; i < 5; i++)
+				dvals[i] = int space.children[i].getattr("number");
+			actval := value(dvals);
+			if (actval >= declared) {
+				# declaration was correct; rejector loses
+				clique.action("say member " + string ord + " loses.", nil, nil, ~0);
+				turn = ord;
+				start();
+			} else {
+				# liar caught out. rejector wins.
+				clique.action("say member " + string turn + " was lying...", nil, nil, ~0);
+				start();
+			}
+		"accept" =>
+			# dice accepted, turn moves on
+			# XXX should allow for anticlockwise play
+			newturn := (turn + 1) % nplaying;
+			plate.setattr("owner", string newturn, ~0);
+			setstate(ord, Havedice);
+			setstate(turn, Waiting);
+		}
+	}
+	check();
+	currmember = nil;
+	currcmd = nil;
+	return nil;
+}
+
+expose(n: int, toks: list of string)
+{
+	# (un)expose index
+	assert(n == 2, Eusage);
+	(src, dest) := (cup, space);
+	if (hd toks == "unexpose")
+		(src, dest) = (space, cup);
+	index := int hd tl toks;
+	checkrange((index, index+1), cup);
+	src.transfer((index, index+1), dest, len dest.children);
+}
+
+start()
+{
+	clique.action("start", nil, nil, ~0);
+	space.transfer((0, len space.children), cup, len cup.children);
+	cup.setvisibility(0);
+	for (i := 0; i < len dice; i++)
+		dice[i].setattr("number", string rand(6), ~0);
+
+	plate.setattr("owner", string turn, ~0);
+	for (i = 0; i < nplaying; i++) {
+		if (i == turn)
+			setstate(i, Havedice);
+		else
+			setstate(i, Waiting);
+	}
+	declared = 0;
+}
+
+declare(ord: int, toks: list of string)
+{
+	cup.setvisibility(0);
+	assert(len toks == 1 && len hd toks == 5, "bad declaration");
+	d := hd toks;
+	v := array[5] of {* => 0};
+	for (i := 0; i < 5; i++) {
+		v[i] = (hd toks)[i] - '0';
+		assert(v[i] >= 0 && v[i] <= 5, "bad declaration");
+	}
+	newval := value(v);
+	assert(newval > declared, "declaration not high enough");
+	declared = newval;
+
+	setstate(turn, Waiting);
+	setstate((turn + 1) % nplaying, Queried);
+}
+
+# check that range is valid for object's children
+checkrange(r: Range, o: ref Object)
+{
+	assert(r.start >= 0 && r.start < len o.children &&
+			r.end >= r.start && r.end >= 0 &&
+			r.end <= len o.children,
+			"index out of range");
+}
+
+setstate(ord: int, state: int)
+{
+	poss: string;
+	case state {
+	Notplaying =>
+		poss = "start";
+	Havedice =>
+		poss = "declare look";
+	Looking =>
+		poss = "expose unexpose declare roll";
+	Rolled =>
+		poss = "expose unexpose declare";
+	Waiting =>
+		poss = "";
+	Queried =>
+		poss = "accept reject";
+	* =>
+		sys->print("liarclique: unknown state %d, member %d\n", state, ord);
+		sys->raise("panic");
+	}
+	info[ord].action.setattr("actions", poss, 1<<info[ord].id);
+	info[ord].state = state;
+}
+
+obj(ext: int): ref Object
+{
+	assert((o := currmember.obj(ext)) != nil, "bad object");
+	return o;
+}
+
+Eusage: con "bad command usage";
+
+assert(b: int, err: string)
+{
+	if (b == 0) {
+		sys->print("cardclique: error '%s' on %s", err, currcmd);
+		sys->raise("parse:" + err);
+	}
+}
+
+checkobj(o: ref Object, what: string)
+{
+	if (o != nil && o.id == -1) {
+		clique.show(currmember);
+		sys->print("object %d has been deleted unexpectedly (%s)\n", o.id, what);
+		sys->raise("panic");
+	}
+}
+
+check()
+{
+}
+
+NOTHING, PAIR, TWOPAIRS, THREES, LOWSTRAIGHT,
+FULLHOUSE, HIGHSTRAIGHT, FOURS, FIVES: con iota;
+
+what := array[] of {
+NOTHING => "nothing",
+PAIR => "pair",
+TWOPAIRS => "twopairs",
+THREES => "threes",
+LOWSTRAIGHT => "lowstraight",
+FULLHOUSE => "fullhouse",
+HIGHSTRAIGHT => "highstraight",
+FOURS => "fours",
+FIVES => "fives"
+};
+	
+same(dice: array of int): int
+{
+	x := dice[0];
+	for (i := 0; i < len dice; i++)
+		if (dice[i] != x)
+			return 0;
+	return 1;
+}
+
+val(hi, lo: int): int
+{
+	return hi * 100000 + lo;
+}
+
+D: con 10;
+
+value(dice: array of int): int
+{
+	mergesort(dice, array[5] of int);
+
+	for (i := 0; i < 5; i++)
+		sys->print("%d ", dice[i]);
+	sys->print("\n");
+
+	# five of a kind
+	x := dice[0];
+	if (same(dice))
+		return val(FIVES, dice[0]);
+
+	# four of a kind
+	if (same(dice[1:]))
+		return val(FOURS, dice[0] + dice[1]*D);
+	if (same(dice[0:4]))
+		return val(FOURS, dice[4] + dice[0]*D);
+
+	# high straight
+	if (dice[0] == 1 && dice[1] == 2 && dice[2] == 3 &&
+			dice[3] == 4 && dice[4] == 5)
+		return val(HIGHSTRAIGHT, 0);
+
+	# full house
+	if (same(dice[0:3]) && same(dice[3:5]))
+		return val(FULLHOUSE, dice[0]*D + dice[4]);
+	if (same(dice[0:2]) && same(dice[2:5]))
+		return val(FULLHOUSE, dice[4]*D + dice[0]);
+
+	# low straight
+	if (dice[0] == 0 && dice[1] == 1 && dice[2] == 2 &&
+			dice[3] == 3 && dice[4] == 4)
+		return val(LOWSTRAIGHT, 0);
+	# three of a kind
+	if (same(dice[0:3]))
+		return val(THREES, dice[3] + dice[4]*D + dice[0]*D*D);
+	if (same(dice[1:4]))
+		return val(THREES, dice[0] + dice[4]*D + dice[1]*D*D);
+	if (same(dice[2:5]))
+		return val(THREES, dice[0] + dice[1]*D + dice[2]*D*D);
+
+	for (i = 0; i < 4; i++)
+		if (same(dice[i:i+2]))
+			break;
+	case i {
+	4 =>
+		return val(NOTHING, dice[0] + dice[1]*D + dice[2]*D*D +
+				dice[3]*D*D*D + dice[4]*D*D*D*D);
+	3 =>
+		return val(PAIR, dice[0] + dice[1]*D + dice[2]*D*D + dice[3]*D*D*D);
+	2 =>
+		return val(PAIR, dice[0] + dice[1]*D + dice[4]*D*D + dice[2]*D*D*D);
+	}
+	h := array[5] of int;
+	h[0:] = dice;
+	if (i == 1)
+		(h[0], h[2]) = (h[2], h[0]);
+	# pair is in first two dice
+	if (same(h[2:4]))
+		return val(TWOPAIRS, h[4] + h[2]*D + h[0]*D*D);
+	if (same(h[3:5]))
+		return val(TWOPAIRS, h[2] + h[0]*D + h[4]*D*D);
+	return val(PAIR, dice[2] + dice[3]*D + dice[4]*D*D + dice[0]*D*D*D);
+}
+
+mergesort(a, b: array of int)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		mergesort(a[0:m], b[0:m]);
+		mergesort(a[m:], b[m:]);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (b[i] > b[j])
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
--- /dev/null
+++ b/appl/spree/engines/liars.y
@@ -1,0 +1,132 @@
+%{
+
+
+YYSTYPE: adt {
+};
+
+YYLEX: adt {
+	lval: YYSTYPE;
+	lex: fn(l: self ref YYLEX): int;
+	error: fn(l: self ref YYLEX, err: string);
+	toks: list of string;
+};
+%}
+
+%module Sh {
+	# module definition is in shell.m
+}
+%token A ALL AND BROKEN FIVE FOUR FUCK FULL HIGH
+%token HOUSE KIND LOW NOTHING OF ON PAIR PAIRS STRAIGHT THREE TWO VALUE
+
+%start phrase
+%%
+phrase: nothing
+	| pair
+	| twopairs
+	| threes
+	| lowstraight
+	| fullhouse
+	| highstraight
+	| fours
+	| fives
+
+pair:	PAIR
+	| PAIR ofsomething ',' extras
+
+nothing: NOTHING
+	| BROKEN STRAIGHT
+	| FUCK ALL
+
+twopairs: TWO PAIRS moretuppers
+	| TWO VALUE optcomma TWO VALUE and_a VALUE
+	| PAIR OF VALUE ',' PAIR OF VALUE and_a VALUE
+
+moretuppers:
+	|	',' VALUE ',' VALUE and_a VALUE
+
+threes:	THREE OF A KIND extras
+	| THREE VALUE extras
+
+lowstraight: LOW STRAIGHT
+
+fullhouse:	FULL HOUSE
+	| FULL HOUSE optcomma VALUE
+	| FULL HOUSE optcomma VALUE ON VALUE
+	| FULL HOUSE optcomma VALUE HIGH
+
+highstraight:	HIGH STRAIGHT
+
+fours:	FOUR OF A KIND extras
+	| FOUR VALUE extras
+
+fives:	FIVE OF A KIND
+	|	FIVE VALUE
+and_a:	# null
+	|	AND A
+optcomma:
+	|	','
+extras: VALUE
+	| extras VALUE
+%%
+
+Tok: adt {
+	s: string;
+	tok: int;
+	val: int;
+};
+
+known := array of {
+Tok("an", A, -1),
+Tok("a",  A, -1),
+Tok("all",  ALL, -1),
+Tok("and",  AND, -1),
+Tok("broken",  BROKEN, -1),
+Tok(",",  ',', -1),
+Tok("five",  FIVE, -1),
+Tok("5",	FIVE, -1),
+Tok("four",  FOUR, -1),
+Tok("4", FOUR, -1),
+Tok("fuck",  FUCK, -1),
+Tok("full",  FULL, -1),
+Tok("high",  HIGH, -1),
+Tok("house",  HOUSE, -1),
+Tok("kind",  KIND, -1),
+Tok("low",  LOW, -1),
+Tok("nothing",  NOTHING, -1),
+Tok("of",  OF, -1),
+Tok("on",  ON, -1),
+Tok("pair",  PAIR, -1),
+Tok("pairs",  PAIRS, -1),
+Tok("straight",  STRAIGHT, -1),
+Tok("three",  THREE, -1),
+Tok("3", THREE, -1),
+Tok("two",  TWO, -1),
+Tok("2", TWO, -1),
+
+Tok("A", VALUE, 5),
+Tok("K", VALUE, 4),
+Tok("Q", VALUE, 3),
+Tok("J", VALUE, 2),
+Tok("10", VALUE, 1),
+Tok("9", VALUE, 0),
+
+Tok("ace"
+};
+
+YYLEX.lex(l: self ref YYLEX): int
+{
+	if (l.toks == nil)
+		return -1;
+	t := hd l.toks;
+	for (i := 0; i < len known; i++) {
+		if (known[i].t0 == t)
+			return known[i].t1;
+		
+	case hd l.toks {
+
+
+%token A ALL AND BROKEN FIVE FOUR FUCK FULL HIGH
+%token HOUSE KIND LOW NOTHING OF ON PAIR PAIRS STRAIGHT THREE TWO VALUE
+%token END
+
+}
--- /dev/null
+++ b/appl/spree/engines/lobby.b
@@ -1,0 +1,389 @@
+implement Engine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, set, A, B, All, None: import sets;
+include "../spree.m";
+	spree: Spree;
+	archives: Archives;
+	Archive: import Archives;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "readdir.m";
+	readdir: Readdir;
+
+# what the lobby provides:
+#	a list of cliques it's started
+#		name of clique
+#		current members
+#	list of members inside the lobby.
+#		name
+#		invites
+#			how does a gather engine know who's been invited?
+#			as the lobby's the only place with the knowledge of who's around to invite.
+#			could allow lobby to communicate with the cliques it's started...
+#			but clique also needs to communicate with the lobby
+#			(e.g. to say clique has started, no more invites necessary or allowed)
+#
+#	list of available engines
+#		title
+#		clienttype(s?)
+#
+#	understands commands:
+#		chat message
+#		invite
+#		new 	name params
+#
+#	question: how do we know about archives?
+#	answer: maybe we don't... could have another module
+#		that does, or maybe an option to gather ("gather unarchive"?)
+#
+#	the one that's started the clique is always invited.
+#	start clique.
+#		clique says to parent "invite x, y and z" (perhaps they were in the archive)
+#		how should we deal with recursive invocation?
+#		could queue up requests to other clique engines,
+#			and deliver them after the current request has been processed.
+#			no return available (one way channel) but maybe that's good,
+#			as if sometime in the future engines do run in parallel, we will
+#			need to avoid deadlock.
+#		Clique.notify(clique: self ref Clique, cliqueid: int, note: string);
+#			when a request has been completed, we run notify requests
+#			for all the cliques that have been notified, and repeat
+#			until no more. (could keep a count to check for infinite loop).
+#			don't allow communication between unrelated cliques.
+
+clique: ref Clique;
+
+members: ref Object;
+sessions: ref Object;
+available: ref Object;
+archiveobj: ref Object;
+
+ARCHIVEDIR: con "./archive";
+
+init(srvmod: Spree, g: ref Clique, nil: list of string): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->print("lobby: cannot load %s: %r\n", Sets->PATH);
+		return "bad module";
+	}
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil) {
+		sys->print("lobby: cannot load %s: %r\n", Readdir->PATH);
+		return "bad module";
+	}
+	archives = load Archives Archives->PATH;
+	if (archives == nil) {
+		sys->print("lobby: cannot load %s: %r\n", Archives->PATH);
+		return "bad module";
+	}
+	archives->init(srvmod);
+	members = clique.newobject(nil, All, "members");
+	sessions = clique.newobject(nil, All, "sessions");
+	available = clique.newobject(nil, All, "available");
+	o := clique.newobject(available, All, "sessiontype");
+	o.setattr("name", "freecell", All);
+	o.setattr("title", "Freecell", All);
+	o.setattr("clienttype", "cards", All);
+	o.setattr("start", "gather 1 freecell", All);
+
+	o = clique.newobject(available, All, "sessiontype");
+	o.setattr("title", "Lobby", All);
+	o.setattr("name", "lobby", All);
+	o.setattr("clienttype", "lobby", All);
+	o.setattr("start", "lobby", All);
+
+	o = clique.newobject(available, All, "sessiontype");
+	o.setattr("title", "Spit", All);
+	o.setattr("name", "spit", All);
+	o.setattr("clienttype", "cards", All);
+	o.setattr("start", "gather 2 spit", All);
+
+	o = clique.newobject(available, All, "sessiontype");
+	o.setattr("title", "Canfield", All);
+	o.setattr("name", "canfield", All);
+	o.setattr("clienttype", "cards", All);
+	o.setattr("start", "gather 1 canfield", All);
+
+	o = clique.newobject(available, All, "sessiontype");
+	o.setattr("title", "Afghan", All);
+	o.setattr("name", "afghan", All);
+	o.setattr("clienttype", "cards", All);
+	o.setattr("start", "gather 1 afghan", All);
+
+	o = clique.newobject(available, All, "sessiontype");
+	o.setattr("title", "Spider", All);
+	o.setattr("name", "spider", All);
+	o.setattr("clienttype", "cards", All);
+	o.setattr("start", "gather 1 spider", All);
+
+	o = clique.newobject(available, All, "sessiontype");
+	o.setattr("title", "Racing Demon", All);
+	o.setattr("name", "racingdemon", All);
+	o.setattr("clienttype", "cards", All);
+	o.setattr("start", "gather 3 racingdemon", All);
+
+	o = clique.newobject(available, All, "sessiontype");
+	o.setattr("title", "Othello", All);
+	o.setattr("name", "othello", All);
+	o.setattr("clienttype", "othello", All);
+	o.setattr("start", "gather 2 othello", All);
+
+	o = clique.newobject(available, All, "sessiontype");
+	o.setattr("title", "Whist", All);
+	o.setattr("name", "whist", All);
+	o.setattr("clienttype", "whist", All);
+	o.setattr("start", "gather 4 whist", All);
+
+	getarchives();
+
+	clique.start();
+
+	return nil;
+}
+
+join(p: ref Member, cmd: string, nil: int): string
+{
+	sys->print("%s joins '%s'\n", p.name, cmd);
+	clique.notify(clique.parentid, "join " + p.name);
+	s := None.add(p.id);
+	clique.action("clienttype lobby", nil, nil, s);
+	clique.breakmsg(s);
+	clique.action("name " + p.name, nil, nil, s);
+	o := clique.newobject(members, All, "member");
+	o.setattr("name", p.name, All);
+	return nil;
+}
+
+leave(p: ref Member): int
+{
+	clique.notify(clique.parentid, "leave " + p.name);
+	deletename(members, p.name, "member");
+	sys->print("%s leaves\n", p.name);
+	return 1;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+command(p: ref Member, cmd: string): string
+{
+	sys->print("%s: '%s'\n", p.name, cmd);
+	(n, toks) := sys->tokenize(cmd, " \n");
+	if (n == 0)
+		return "bad command";
+	case hd toks {
+	"kick" =>
+		getarchives();
+		return nil;
+	"chat" =>
+		clique.action("chat " + p.name + " " + concat(tl toks), nil, nil, All);
+		return nil;
+	"start" =>
+		# start engine [params]
+		if (n >= 2) {
+			(gid, fname, err) := clique.new(
+				ref Archive(tl toks, nil, nil, nil),
+				p.name);
+			if (gid == -1)
+				return err;
+			s := addname(sessions, string gid, "session");
+			s.setattr("title", concat(tl toks), All);
+			s.setattr("filename", fname, All);
+			s.setattr("cliqueid", string gid, None);
+			s.setattr("owner", p.name, All);
+			return nil;
+		}
+		return "bad start params";
+	"invite" or
+	"uninvite"=>
+		# invite sessionid name
+		if (n == 3) {
+			(what, sessionid, name) := (hd toks, int hd tl toks, hd tl tl toks);
+			if ((s := p.obj(sessionid)) == nil)
+				return "bad object id";
+			if (s.objtype != "session")
+				return "bad session type " + s.objtype;
+			if (s.getattr("owner") != p.name)
+				return "permission denied";
+			clique.notify(int s.getattr("cliqueid"), what + " " + name);
+			if (hd toks == "invite")
+				addname(s, name, "invite");
+			else
+				deletename(s, name, "invite");
+			return nil;
+		}
+		return "bad invite params";
+	"unarchive" =>
+		# unarchive object
+		if (n == 2) {
+			o := p.obj(int hd tl toks);
+			if (o == nil || o.objtype != "archive")
+				return "bad archive object";
+			# archive object contains:
+			# name		name of clique
+			# members		members of the clique
+			# file			filename of archive
+
+			aname := o.getattr("file");
+			(archive, err) := archives->read(aname);
+			if (archive == nil)
+				return sys->sprint("cannot load archive: %s", err);
+			for (i := 0; i < len archive.members; i++)
+				if (p.name == archive.members[i])
+					break;
+			if (i == len archive.members)
+				return "you did not participate in that session";
+			(gid, fname, err2) := clique.new(archive, p.name);
+			if (gid == -1)
+				return err2;
+			s := addname(sessions, string gid, "session");
+			s.setattr("title", concat(archive.argv), All);
+			s.setattr("filename", fname, All);
+			s.setattr("cliqueid", string gid, None);
+			s.setattr("owner", p.name, All);
+
+			o.delete();
+			(ok, d) := sys->stat(aname);
+			if (ok != -1) {
+				d.name += ".old";
+				sys->wstat(aname, d);
+			}
+			# XXX delete old archive file?
+			return nil;
+		}
+		return "bad unarchive params";
+	* =>
+		return "bad command";
+	}
+}
+
+notify(srcid: int, note: string)
+{
+	sys->print("lobby: note from %d: %s\n", srcid, note);
+	s := findname(sessions, string srcid);
+	if (s == nil) {
+		sys->print("cannot find srcid %d\n", srcid);
+		return;
+	}
+	if (note == nil) {
+		s.delete();
+		return;
+	}
+	if (srcid == clique.parentid)
+		return;
+	(n, toks) := sys->tokenize(note, " ");
+	case hd toks {
+	"join" =>
+		p := addname(s, hd tl toks, "member");
+	"leave" =>
+		deletename(s, hd tl toks, "member");
+	"invite" =>
+		addname(s, hd tl toks, "invite");
+	"uninvite" =>
+		deletename(s, hd tl toks, "invite");
+	"title" =>
+		s.setattr("title", concat(tl toks), All);
+	"archived" =>
+		# archived filename
+		arch := clique.newobject(archiveobj, All, "archive");
+		arch.setattr("name", s.getattr("title"), All);
+		pnames := "";
+		for (i := 0; i < len s.children; i++)
+			if (s.children[i].objtype == "member")
+				pnames += " " + s.children[i].getattr("name");
+		if (pnames != nil)
+			pnames = pnames[1:];
+		arch.setattr("members", pnames, All);
+		arch.setattr("file", hd tl toks, None);
+	* =>
+		sys->print("unknown note from %d: %s\n", srcid, note);
+	}
+}
+
+addname(o: ref Object, name: string, otype: string): ref Object
+{
+	x := clique.newobject(o, All, otype);
+	x.setattr("name", name, All);
+	return x;
+}
+
+findname(o: ref Object, name: string): ref Object
+{
+	c := o.children;
+	for (i := 0; i < len c; i++)
+		if (c[i].getattr("name") == name)
+			return c[i];
+	return nil;
+}
+
+deletename(o: ref Object, name: string, objtype: string)
+{
+	c := o.children;
+	for (i := 0; i < len c; i++)
+		if (c[i].objtype == objtype && c[i].getattr("name") == name) {
+			o.deletechildren((i, i+1));
+			break;
+		}
+}
+
+getarchives()
+{
+	if (archiveobj == nil)
+		archiveobj = clique.newobject(nil, All, "archives");
+	else
+		archiveobj.deletechildren((0, len archiveobj.children));
+	for (names := spree->archivenames(); names != nil; names = tl names) {
+		fname := hd names;
+		(a, err) := archives->readheader(fname);
+		if (a == nil) {
+			sys->print("lobby: cannot read archive header on %s: %s\n", fname, err);
+			continue;
+		}
+		title := "";
+		for (inf := a.info; inf != nil; inf = tl inf) {
+			if ((hd inf).t0 == "title") {
+				title = (hd inf).t1;
+				break;
+			}
+		}
+		if (title == nil)
+			title = concat(a.argv);
+		arch := clique.newobject(archiveobj, All, "archive");
+		arch.setattr("name", title, All);
+		arch.setattr("members", concatarray(a.members), All);
+		arch.setattr("file", fname, None);
+		j := 0;
+		for (info := a.info; info != nil; info = tl info)
+			arch.setattr("info" + string j++, (hd info).t0 + " " + (hd info).t1, All);
+	}
+}
+
+concat(l: list of string): string
+{
+	if (l == nil)
+		return nil;
+	s := hd l;
+	for (l = tl l; l != nil; l = tl l)
+		s += " " + hd l;
+	return s;
+}
+
+concatarray(a: array of string): string
+{
+	if (len a == 0)
+		return nil;
+	s := a[0];
+	for (i := 1; i < len a; i++)
+		s += " " + a[i];
+	return s;
+}
--- /dev/null
+++ b/appl/spree/engines/othello.b
@@ -1,0 +1,242 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	All, None: import Sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member: import spree;
+include "objstore.m";
+	objstore: Objstore;
+include "../gather.m";
+
+clique: ref Clique;
+
+Black, White, Nocolour: con iota;		# first two must be 0 and 1.
+N: con 8;
+
+boardobj: ref Object;
+board:	array of array of int;
+pieces:	array of int;
+turn		:= Nocolour;
+members	:= array[2] of ref Member;			# member ids of those playing
+
+Point: adt {
+	x, y: int;
+	add: fn(p: self Point, p1: Point): Point;
+	inboard: fn(p: self Point): int;
+};
+
+clienttype(): string
+{
+	return "othello";
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+
+	objstore = load Objstore Objstore->PATH;
+	if (objstore == nil) {
+		sys->print("othello: cannot load %s: %r", Objstore->PATH);
+		return "bad module";
+	}
+	objstore->init(srvmod, g);
+
+	return nil;
+}
+
+maxmembers(): int
+{
+	return 2;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members != 2)
+		return "need exactly two members";
+	return nil;
+}
+
+archive()
+{
+	objstore->setname(boardobj, "board");
+}		
+
+start(pl: array of ref Member, archived: int)
+{
+	members = pl;
+	board = array[N] of {* => array[N] of {* => Nocolour}};
+	pieces = array[2] of {* => 0};
+	if (archived) {
+		objstore->unarchive();
+		boardobj = objstore->get("board");
+		for (i := 0; i < N; i++) {
+			for (j := 0; j < N; j++) {
+				a := boardobj.getattr(pt2attr((j, i)));
+				if (a != nil) {
+					piece := int a;
+					board[j][i] = piece;
+					if (piece != Nocolour)
+						pieces[piece]++;
+				}
+			}
+		}
+		turn = int boardobj.getattr("turn");
+	} else {
+		boardobj = clique.newobject(nil, All, nil);
+		boardobj.setattr("members", string members[Black].name + " " + string members[White].name, All);
+		for (ps := (Black, (3, 3)) :: (Black, (4, 4)) :: (White, (3, 4)) :: (White, Point(4, 3)) :: nil;
+				ps != nil;
+				ps = tl ps) {
+			(colour, p) := hd ps;
+			setpiece(colour, p);
+		}
+		turn = Black;
+		boardobj.setattr("turn", string Black, All);
+	}
+}
+
+cliqueover()
+{
+	turn = Nocolour;
+	boardobj.setattr("winner", string winner(), All);
+	boardobj.setattr("turn", string turn, All);
+}
+
+command(member: ref Member, cmd: string): string
+{
+	{
+		(n, toks) := sys->tokenize(cmd, " \n");
+		assert(n > 0, "unknown command");
+	
+		case hd toks {
+		"move" =>
+			assert(n == 3, "bad command usage");
+			assert(turn != Nocolour, "clique has finished");
+			assert(member == members[White] || member == members[Black], "you are not playing");
+			assert(member == members[turn], "it is not your turn");
+			p := Point(int hd tl toks, int hd tl tl toks);
+			assert(p.x >= 0 && p.x < N && p.y >= 0 && p.y < N, "invalid move position");
+			assert(board[p.x][p.y] == Nocolour, "position is already occupied");
+			assert(newmove(turn, p, 1), "cannot move there");
+	
+			turn = reverse(turn);
+			if (!canplay()) {
+				turn = reverse(turn);
+				if (!canplay())
+					cliqueover();
+			}
+			boardobj.setattr("turn", string turn, All);
+			return nil;
+		}
+		sys->print("othello: unknown client command '%s'\n", hd toks);
+		return "who knows";
+	} exception e {
+	"parse:*" =>
+		return e[6:];
+	}
+}
+
+Directions := array[] of {Point(0, 1), (1, 1), (1, 0), (1, -1), (0, -1), (-1, -1), (-1, 0), (-1, 1)};
+
+setpiece(colour: int, p: Point)
+{
+	v := board[p.x][p.y];
+	if (v != Nocolour)
+		pieces[v]--;
+	board[p.x][p.y] = colour;
+	pieces[colour]++;
+	boardobj.setattr(pt2attr(p), string colour, All);
+}
+
+pt2attr(pt: Point): string
+{
+	s := "  ";
+	s[0] = pt.x + 'a';
+	s[1] = pt.y + 'a';
+	return  s;
+}
+
+# member colour has tried to place a piece at mp.
+# return -1 if it's an illegal move, 0 otherwise.
+# (in which case appropriate updates are sent out all round).
+# if update is 0, just check for the move's validity
+# (no change to the board, no updates sent)
+newmove(colour: int, mp: Point, update: int): int
+{
+	totchanged := 0;
+	for (i := 0; i < len Directions; i++) {
+		d := Directions[i];
+		n := 0;
+		for (p := mp.add(d); p.inboard(); p = p.add(d)) {
+			n++;
+			if (board[p.x][p.y] == colour || board[p.x][p.y] == Nocolour)
+				break;
+		}
+		if (p.inboard() && board[p.x][p.y] == colour && n > 1) {
+			if (!update)
+				return 1;
+			totchanged += n - 1;
+			for (p = mp.add(d); --n > 0; p = p.add(d))
+				setpiece(reverse(board[p.x][p.y]), p);
+		}
+	}
+	if (totchanged > 0) {
+		setpiece(colour, mp);
+		return 1;
+	}
+	return 0;
+}
+
+# who has most pieces?
+winner(): int
+{
+	if (pieces[White] > pieces[Black])
+		return White;
+	else if (pieces[Black] > pieces[White])
+		return Black;
+	return Nocolour;
+}
+
+# is there any possible legal move?
+canplay(): int
+{
+	for (y := 0; y < N; y++)
+		for (x := 0; x < N; x++)
+			if (board[x][y] == Nocolour && newmove(turn, (x, y), 0))
+				return 1;
+	return 0;
+}
+
+reverse(colour: int): int
+{
+	if (colour == Nocolour)
+		return Nocolour;
+	return !colour;
+}
+
+Point.add(p: self Point, p1: Point): Point
+{
+	return (p.x + p1.x, p.y + p1.y);
+}
+
+Point.inboard(p: self Point): int
+{
+	return p.x >= 0 && p.x < N && p.y >= 0 && p.y < N;
+}
+
+assert(b: int, err: string)
+{
+	if (b == 0)
+		raise "parse:" + err;
+}
--- /dev/null
+++ b/appl/spree/engines/racingdemon.b
@@ -1,0 +1,464 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, set, A, B, All, None: import sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	Selection, Cmember, Card: import cardlib;
+	dTOP, dLEFT, oDOWN, EXPAND, FILLX, FILLY, aCENTRELEFT, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+
+CLICK, SAY, SHOW: con iota;
+KING: con 12;
+NACES: con 7;		# number of ace piles to fit across the board.
+
+Dmember: adt {
+	pile,
+	spare1,
+	spare2: ref Object;
+	open: array of ref Object;		# [4]
+	acepiles: array of ref Object;
+};
+scores: array of int;
+scorelabel: ref Object;
+
+dmembers: array of ref Dmember;
+
+Openspec := Stackspec(
+	"display",		# style
+	4,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+Pilespec := Stackspec(
+	"pile",		# style
+	13,			# maxcards
+	0,			# conceal
+	"pile"		# title
+);
+
+Untitledpilespec := Stackspec(
+	"pile",		# style
+	13,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+clienttype(): string
+{
+	return "cards";
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->print("whist: cannot load %s: %r\n", Sets->PATH);
+		return "bad module";
+	}
+	sets->init();
+
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+	cardlib->init(spree, clique);
+
+	return nil;
+}
+
+maxmembers(): int
+{
+	return 100;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members < 3)
+		return "need at least 3 members";
+	return nil;
+}
+
+archive()
+{
+	archiveobj := cardlib->archive();
+	allow->archive(archiveobj);
+	for (i := 0; i < len dmembers; i++) {
+		dp := dmembers[i];
+		s := "d" + string i + "_";
+		cardlib->setarchivename(dp.spare1, s + "spare1");
+		cardlib->setarchivename(dp.spare2, s + "spare2");
+		cardlib->setarchivename(dp.pile, s + "pile");
+		cardlib->archivearray(dp.open, s + "open");
+		cardlib->archivearray(dp.acepiles, s + "acepiles");
+	}
+	cardlib->setarchivename(scorelabel, "scorelabel");
+	s := "";
+	for (i = 0; i < len scores; i++)
+		s += " " + string scores[i];
+	archiveobj.setattr("scores", s, None);
+
+}
+
+start(members: array of ref Member, archived: int)
+{
+	if (archived) {
+		archiveobj := cardlib->unarchive();
+		allow->unarchive(archiveobj);
+		dmembers = array[len members] of ref Dmember;
+		for (i := 0; i < len dmembers; i++) {
+			dp := dmembers[i] = ref Dmember;
+			s := "d" + string i + "_";
+			dp.spare1 = cardlib->getarchiveobj(s + "spare1");
+			dp.spare2 = cardlib->getarchiveobj(s + "spare2");
+			dp.pile = cardlib->getarchiveobj(s + "pile");
+			dp.open = cardlib->getarchivearray(s + "open");
+			dp.acepiles = cardlib->getarchivearray(s + "acepiles");
+		}
+		scorelabel = cardlib->getarchiveobj("scorelabel");
+		s := archiveobj.getattr("scores");
+		(n, toks) := sys->tokenize(s, " ");
+		scores = array[len members] of int;
+		for (i = 0; toks != nil; toks = tl toks)
+			scores[i++] = int hd toks;
+	} else {
+		pset := None;
+		for (i := 0; i < len members; i++) {
+			p := members[i];
+			Cmember.join(p, i);
+			pset = pset.add(p.id);
+			allow->add(CLICK, p, "click %o %d");
+		}
+		Cmember.index(0).layout.lay.setvisibility(All.X(A&~B, pset).add(members[0].id));
+
+		layout();
+		deal();
+		allow->add(SAY, nil, "say &");
+	}
+}
+
+command(p: ref Member, cmd: string): string
+{
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil)
+		return err;
+	cp := Cmember.find(p);
+	if (cp == nil)
+		return "bad member";
+	case tag {
+	CLICK =>
+		# click stack index
+		stack := clique.objects[int hd tl toks];
+		nc := len stack.children;
+		idx := int hd tl tl toks;
+		sel := cp.sel;
+		stype := stack.getattr("type");
+		d := dmembers[cp.ord];
+		if (sel.isempty() || sel.stack == stack) {
+			# selecting a card to move
+			if (nc == 0 && stype == "spare1") {
+				cardlib->flip(d.spare2);
+				d.spare2.transfer((0, len d.spare2.children), d.spare1, 0);
+				return nil;
+			}
+			if (idx < 0 || idx >= len stack.children)
+				return "invalid index";
+			if (owner(stack) != cp)
+				return "not yours, don't touch!";
+			case stype {
+			"spare2" or
+			"pile" =>
+				select(cp, stack, (nc - 1, nc));
+			"open" =>
+				select(cp, stack, (idx, nc));
+			"spare1" =>
+				if ((n := nc) > 3)
+					n = 3;
+				for (i := 0; i < n; i++) {
+					cardlib->setface(stack.children[nc - 1], 1);
+					stack.transfer((nc - 1, nc), d.spare2, -1);
+					nc--;
+				}
+			* =>
+				return "you can't move cards from there";
+			}
+		} else {
+			# selecting a stack to move to.
+			frompile := sel.stack.getattr("type") == "pile";
+			case stype {
+			"acepile" =>
+				if (sel.r.end != sel.r.start + 1)
+					return "only one card at a time!";
+				card := getcard(sel.stack.children[sel.r.start]);
+				if (nc == 0) {
+					if (card.number != 0)
+						return "aces only";
+				} else {
+					top := getcard(stack.children[nc - 1]);
+					if (card.number != top.number + 1)
+						return "out of sequence";
+					if (card.suit != top.suit)
+						return "wrong suit";
+				}
+				sel.transfer(stack, -1);
+				if (card.number == KING)	# kings get flipped
+					cardlib->setface(stack.children[len stack.children - 1], 0);
+			"open" =>
+				if (owner(stack) != cp)
+					return "not yours, don't touch!";
+				c := getcard(sel.stack.children[sel.r.start]);
+				col := !isred(c);
+				n := c.number + 1;
+				for (i := sel.r.start; i < sel.r.end; i++) {
+					c2 := getcard(sel.stack.children[i]);
+					if (isred(c2) == col)
+						return "bad colour sequence";
+					if (c2.number != n - 1)
+						return "bad number sequence";
+					n = c2.number;
+					col = isred(c2);
+				}
+				if (nc != 0) {
+					c2 := getcard(stack.children[nc - 1]);
+					if (isred(c2) == isred(c) || c2.number != c.number + 1)
+						return "invalid move";
+				}
+				sel.transfer(stack, -1);
+			* =>
+				return "can't move there";
+			}
+			if (frompile) {
+				nc = len d.pile.children;
+				if (nc == 0) {
+					endround();
+					deal();
+				} else {
+					cardlib->setface(d.pile.children[nc - 1], 1);
+					d.pile.setattr("title", "pile [" + string nc + "]", All);
+				}
+			}
+		}
+	SAY =>
+		clique.action("say member " + string p.id + ": '" + joinwords(tl toks) + "'", nil, nil, All);
+
+	SHOW =>
+		clique.show(nil);
+	}
+	return nil;
+}
+
+getcard(card: ref Object): Card
+{
+	return cardlib->getcard(card);
+}
+
+isred(c: Card): int
+{
+	return c.suit == Cardlib->DIAMONDS || c.suit == Cardlib->HEARTS;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+	if (cp.sel.isempty()) {
+		cp.sel.set(stack);
+		cp.sel.setrange(r);
+	} else {
+		if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+			cp.sel.set(nil);
+		else
+			cp.sel.setrange(r);
+	}
+}
+
+owner(stack: ref Object): ref Cmember
+{
+	parent := clique.objects[stack.parentid];
+	n := cardlib->nmembers();
+	for (i := 0; i < n; i++) {
+		cp := Cmember.index(i);
+		if (cp.obj == parent)
+			return cp;
+	}
+	return nil;
+}
+
+layout()
+{
+	n := cardlib->nmembers();
+	dmembers = array[n] of ref Dmember;
+	for (i := 0; i < n; i++) {
+		cp := Cmember.index(i);
+		d := dmembers[i] = ref Dmember;
+		d.spare1 = newstack(cp.obj, Untitledpilespec, "spare1");
+		d.spare2 = newstack(cp.obj, Untitledpilespec, "spare2");
+		d.pile = newstack(cp.obj, Pilespec, "pile");
+		d.open = array[4] of {* => newstack(cp.obj, Openspec, "open")};
+		d.acepiles = array[4] of {* => newstack(cp.obj, Untitledpilespec, "acepile")};
+		cardlib->makecards(d.spare1, (0, 13), string i);
+	}
+
+	entry := clique.newobject(nil, All, "widget entry");
+	entry.setattr("command", "say", All);
+	cardlib->addlayobj(nil, nil, nil, dTOP|FILLX, entry);
+
+	scores = array[n] of {* => 0};
+	scorelabel = clique.newobject(nil, All, "widget label");
+	setscores();
+	cardlib->addlayobj(nil, nil, nil, dTOP|FILLX, scorelabel);
+
+	cardlib->addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+	row := 0;
+	col := 0;
+	maketable("arena");
+	for (i = 0; i < n; i++) {
+		d := dmembers[i];
+		f := "p" + string i;
+		cardlib->addlayobj(nil, f, nil, dLEFT, d.spare1);
+		cardlib->addlayobj(nil, f, nil, dLEFT, d.spare2);
+		cardlib->addlayobj(nil, f, nil, dLEFT, d.pile);
+		for (j := 0; j < len d.open; j++)
+			cardlib->addlayobj(nil, f, nil, dLEFT|EXPAND|oDOWN, d.open[j]);
+		for (j = 0; j < len d.acepiles; j++) {
+			cardlib->addlayobj(nil, "a" + string row, nil, dLEFT|EXPAND, d.acepiles[j]);
+			if (++col >= NACES) {
+				col = 0;
+				row++;
+			}
+		}
+	}
+}
+
+setscores()
+{
+	s := "Scores: ";
+	n := cardlib->nmembers();
+	for (i := 0; i < n; i++) {
+		s += Cmember.index(i).p.name + ": " + string scores[i];
+		if (i < n - 1)
+			s[len s] = ' ';
+	}
+	scorelabel.setattr("text", s, All);
+}
+
+deal()
+{
+	n := cardlib->nmembers();
+	for (i := 0; i < n; i++) {
+		cp := Cmember.index(i);
+		d := dmembers[i];
+		deck := d.spare1;
+		cardlib->shuffle(deck);
+		deck.transfer((0, 13), d.pile, 0);
+		cardlib->setface(d.pile.children[12], 1);
+		d.pile.setattr("title", "pile [13]", All);
+		for (j := 0; j < len d.open; j++) {
+			deck.transfer((0, 1), d.open[j], 0);
+			cardlib->setface(d.open[j].children[0], 1);
+		}
+	}
+}
+
+endround()
+{
+	# go through all the ace piles, moving cards back to the appropriate deck
+	# and counting appropriately.
+	# move all other cards back too.
+	n := cardlib->nmembers();
+	for (i := 0; i < n; i++) {
+		d := dmembers[i];
+		Cmember.index(i).sel.set(nil);
+		for (j := 0; j < len d.acepiles; j++) {
+			acepile := d.acepiles[j];
+			nc := len acepile.children;
+			for (k := nc - 1; k >= 0; k--) {
+				card := acepile.children[k];
+				back := int card.getattr("rear");
+				scores[back]++;
+				if (getcard(card).number == KING)
+					scores[back] += 5;
+				cardlib->setface(card, 0);
+				acepile.transfer((k, k + 1), dmembers[back].spare1, -1);
+			}
+		}
+		if (len d.pile.children == 0)
+			scores[i] += 10;			# bonus for going out
+		else
+			scores[i] -= len d.pile.children;
+		cardlib->discard(d.pile, d.spare1, 1);
+		cardlib->discard(d.spare2, d.spare1, 1);
+		for (j = 0; j < len d.open; j++)
+			cardlib->discard(d.open[j], d.spare1, 1);
+	}
+	setscores();
+}
+
+maketable(parent: string)
+{
+	addlayframe: import cardlib;
+
+	n := cardlib->nmembers();
+	na := ((n * 4) + (NACES - 1)) / NACES;
+	for (i := 0; i < n; i++) {
+		layout := Cmember.index(i).layout;
+		# one frame for each member other than self;
+		# then all the ace piles; then self.
+		for (j := 0; j < n; j++)
+			if (j != i)
+				addlayframe("p" + string j, parent, layout, dTOP|EXPAND, dTOP);
+		for (j = 0; j < na; j++)
+			addlayframe("a" + string j, parent, layout, dTOP|EXPAND|aCENTRELEFT, dTOP);
+		addlayframe("p" + string i, parent, layout, dTOP|EXPAND, dTOP);
+	}
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype: string): ref Object
+{
+	stack := cardlib->newstack(parent, nil, spec);
+	stack.setattr("type", stype, None);
+	stack.setattr("actions", "click", All);
+	return stack;
+}
+
+joinwords(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
+
+remark(s: string)
+{
+	clique.action("remark " + s, nil, nil, All);
+}
--- /dev/null
+++ b/appl/spree/engines/snap.b
@@ -1,0 +1,241 @@
+implement Engine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	publicstack: import cardlib;
+	VERT, HORIZ, TOP, BOTTOM, LEFT, RIGHT, Stackspec: import Cardlib;
+
+clique: ref Clique;
+PLAY, START, SAY, SNAP: con iota;
+
+started := 0;
+
+buttons: ref Object;
+scores: ref Object;
+deck: ref Object;
+
+HAND, PILE: con iota;
+
+hands := array[2] of ref Object;
+piles := array[2] of ref Object;
+
+publicspec: array of Stackspec;
+
+privatespec := array[] of {
+	HAND => Stackspec(Cardlib->sPILE,
+			52,
+			0,
+			"hand",
+			HORIZ,
+			BOTTOM),
+	PILE => Stackspec(Cardlib->sPILE,
+			52,
+			0,
+			"pile",
+			HORIZ,
+			TOP),
+};
+
+oneplayed := 0;			# true if only one member's put down a card so far
+
+MINPLAYERS: con 2;
+MAXPLAYERS: con 2;
+
+clienttype(): string
+{
+	return "cards";
+}
+
+init(g: ref Clique, srvmod: Spree): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+	allow->add(SAY, nil, "say &");
+
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+
+	cardlib->init(clique, spree);
+	deck = clique.newobject(nil, ~0, "stack");
+	cardlib->makepack(deck, (0, 52), 1);
+	cardlib->shuffle(deck);
+	buttons = clique.newobject(nil, ~0, "buttons");
+	scores = clique.newobject(nil, ~0, "scoretable");
+
+	return nil;
+}
+
+join(p: ref Member): string
+{
+	sys->print("%s(%d) joining\n", p.name(), p.id);
+	if (!started && cardlib->nmembers() < MAXPLAYERS) {
+		(nil, err) := cardlib->join(p, -1);
+		if (err == nil) {
+			if (cardlib->nmembers() == MINPLAYERS) {
+				mkbutton("Start", "start");
+				allow->add(START, nil, "start");
+			}
+		} else
+			sys->print("error on join: %s\n", err);
+	}
+	return nil;
+}
+		
+leave(p: ref Member)
+{
+	cardlib->leave(p);
+	started == 0;
+	if (cardlib->nmembers() < MINPLAYERS) {
+		buttons.deletechildren((0, len buttons.children));
+		allow->del(START, nil);
+	}
+}
+
+command(p: ref Member, cmd: string): string
+{
+	e := ref Sys->Exception;
+	if (sys->rescue("parse:*", e) == Sys->EXCEPTION) {
+		sys->rescued(Sys->ONCE, nil);
+		return e.name[6:];
+	}
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil)
+		return err;
+	case tag {
+	START =>
+		buttons.deletechildren((0, len buttons.children));
+		allow->del(START, nil);
+		allow->add(SNAP, nil, "snap");
+		mkbutton("Snap!", "snap");
+		cardlib->startclique(publicspec, privatespec);
+		for (i := 0; i < 2; i++) {
+			hands[i] = cardlib->info(i).stacks[HAND];
+			piles[i] = cardlib->info(i).stacks[PILE];
+		}
+		deck.transfer((0, 26), hands[0], 0);
+		deck.transfer((0, 26), hands[1], 0);
+		canplay(0);
+		canplay(1);
+
+	PLAY =>
+		# click index
+		ord := cardlib->order(p);
+		inf := cardlib->info(ord);
+
+		hand := hands[ord];
+		pile := piles[ord];
+		hand.transfer((len hand.children - 1, len hand.children), pile, len pile.children);
+		cardlib->setface(pile.children[len pile.children - 1], 1);
+		cantplay(ord);
+		oneplayed = !oneplayed;
+		if (!oneplayed || len hands[!ord].children == 0) {
+			for (i := 0; i < 2; i++)
+				if (len hands[i].children > 0)
+					canplay(i);
+		}
+	SNAP =>
+		# snap
+		ord := cardlib->order(p);
+		inf := cardlib->info(ord);
+		if (oneplayed)		# XXX allow for case where one person has no cards.
+			return "must wait for two cards to be put down";
+		if (len piles[0].children == 0 || len piles[1].children == 0)
+			return "no cards";
+		c0 := cardlib->getcard(piles[0].children[len piles[0].children - 1]);
+		c1 := cardlib->getcard(piles[1].children[len piles[0].children - 1]);
+		if (c0.number != c1.number) {
+			remark(p.name() + " said snap wrongly!");
+			return "cards must be the same";
+		} else {
+			transferall(piles[!ord], piles[ord], len piles[ord].children);
+			flipstack(piles[ord]);
+			transferall(piles[ord], hands[ord], 0);
+			if (len hands[!ord].children == 0)
+				remark(p.name() + " has won!");
+			oneplayed = 0;
+			for (i := 0; i < 2; i++)
+				if (len hands[i].children > 0)
+					canplay(i);
+				else
+					cantplay(i);
+		}
+	SAY =>
+		clique.action("say member " + string p.id + ": '" + joinwords(tl toks) + "'", nil, nil, ~0);
+	}
+	return nil;
+}
+
+transferall(stack, into: ref Object, idx: int)
+{
+	stack.transfer((0, len stack.children), into, idx);
+}
+
+flipstack(stack: ref Object)
+{
+	for (i := 0; i < len stack.children; i++) {
+		card := stack.children[i];
+		cardlib->setface(card, ! int card.getattr("face"));
+	}
+}
+
+joinwords(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
+
+canplay(ord: int)
+{
+	inf := cardlib->info(ord);
+	allow->del(PLAY, inf.p);
+	allow->add(PLAY, inf.p, "click %d");
+	inf.stacks[HAND].setattr("actions", "click", 1<<inf.p.id);
+}
+
+cantplay(ord: int)
+{
+	inf := cardlib->info(ord);
+	allow->del(PLAY, inf.p);
+	inf.stacks[HAND].setattr("actions", nil, 1<<inf.p.id);
+}
+
+member(ord: int): ref Member
+{
+	return cardlib->info(ord).p;
+}
+
+remark(s: string)
+{
+	clique.action("remark " + s, nil, nil, ~0);
+}
+
+mkbutton(text, cmd: string): ref Object
+{
+	but := clique.newobject(buttons, ~0, "button");
+	but.setattr("text", text, ~0);
+	but.setattr("command", cmd, ~0);
+	return but;
+}
--- /dev/null
+++ b/appl/spree/engines/spider.b
@@ -1,0 +1,259 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	All, None: import Sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	Selection, Cmember, Card: import cardlib;
+	getcard: import cardlib;
+	dTOP, dRIGHT, dLEFT, oRIGHT, oDOWN,
+	aCENTRERIGHT, aCENTRELEFT, aUPPERRIGHT, aUPPERCENTRE,
+	EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+
+open: array of ref Object;		# [10]
+deck: ref Object;
+discard: ref Object;
+dealbutton: ref Object;
+
+CLICK, MORECARDS: con iota;
+
+Openspec := Stackspec(
+	"display",		# style
+	19,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+clienttype(): string
+{
+	return "cards";
+}
+
+maxmembers(): int
+{
+	return 1;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members != 1)
+		return "one member only";
+	return nil;
+}
+
+archive()
+{
+	archiveobj := cardlib->archive();
+	allow->archive(archiveobj);
+	cardlib->archivearray(open, "open");
+	cardlib->setarchivename(deck, "deck");
+	cardlib->setarchivename(discard, "discard");
+	cardlib->setarchivename(dealbutton, "dealbutton");
+}
+
+start(members: array of ref Member, archived: int)
+{
+	cardlib->init(spree, clique);
+	if (archived) {
+		archiveobj := cardlib->unarchive();
+		allow->unarchive(archiveobj);
+		open = cardlib->getarchivearray("open");
+		discard = cardlib->getarchiveobj("discard");
+		deck = cardlib->getarchiveobj("deck");
+		dealbutton = cardlib->getarchiveobj("dealbutton");
+	} else {
+		p := members[0];
+		Cmember.join(p, -1).layout.lay.setvisibility(All);
+		startclique();
+		allow->add(CLICK, p, "click %o %d");
+		allow->add(MORECARDS, p, "morecards");
+	}
+}
+
+command(p: ref Member, cmd: string): string
+{
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil)
+		return err;
+	cp := Cmember.find(p);
+	if (cp == nil)
+		return "you are not playing";
+	case tag {
+	CLICK =>
+		# click stack index
+		stack := clique.objects[int hd tl toks];
+		nc := len stack.children;
+		idx := int hd tl tl toks;
+		sel := cp.sel;
+		stype := stack.getattr("type");
+		if (sel.isempty() || sel.stack == stack) {
+			if (idx < 0 || idx >= len stack.children)
+				return "invalid index";
+			case stype {
+			"open" =>
+				select(cp, stack, (idx, nc));
+			* =>
+				return "you can't move cards from there";
+			}
+		} else {
+			from := sel.stack;
+			case stype {
+			"open" =>
+				c := getcard(sel.stack.children[sel.r.start]);
+				n := c.number + 1;
+				for (i := sel.r.start; i < sel.r.end; i++) {
+					c2 := getcard(sel.stack.children[i]);
+					if (c2.face == 0)
+						return "cannot move face down cards";
+					if (c2.number != n - 1)
+						return "bad number sequence";
+					n = c2.number;
+				}
+				if (nc != 0) {
+					c2 := getcard(stack.children[nc - 1]);
+					if (c2.number != c.number + 1)
+						return "descending, only";
+				}
+				srcstack := sel.stack;
+				sel.transfer(stack, -1);
+				turntop(srcstack);
+
+				nc = len stack.children;
+				if (nc >= 13) {
+					c = getcard(stack.children[nc - 1]);
+					suit := c.suit;
+					for (i = 0; i < 13; i++) {
+						c = getcard(stack.children[nc - i - 1]);
+						if (c.suit != suit || c.number != i)
+							break;
+					}
+					if (i == 13) {
+						stack.transfer((nc - 13, nc), discard, -1);
+						turntop(stack);
+					}
+				}
+			* =>
+				return "can't move there";
+			}
+		}
+	MORECARDS =>
+		for (i := 0; i < 10; i++)
+			if (len open[i].children == 0)
+				return "spaces must be filled before redeal";
+		for (i = 0; i < 10; i++) {
+			if (len deck.children == 0)
+				break;
+			cp.sel.set(nil);
+			cardlib->setface(deck.children[0], 1);
+			deck.transfer((0, 1), open[i], -1);
+		}
+		setdealbuttontext();
+	}
+	return nil;
+}
+
+setdealbuttontext()
+{
+	dealbutton.setattr("text", sys->sprint("deal more (%d left)", len deck.children), All);
+}
+
+turntop(stack: ref Object)
+{
+	if (len stack.children > 0)
+		cardlib->setface(stack.children[len stack.children - 1], 1);
+}
+
+startclique()
+{
+	addlayobj, addlayframe: import cardlib;
+	open = array[10] of {* => newstack(nil, Openspec, "open", nil)};
+	deck = clique.newobject(nil, All, "stack");
+	discard = clique.newobject(nil, All, "stack");
+	cardlib->makecards(deck, (0, 13), "0");
+	cardlib->makecards(deck, (0, 13), "1");
+	addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+	addlayframe("top", "arena", nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+
+	for (i := 0; i < 10; i++)
+		addlayobj(nil, "top", nil, dLEFT|oDOWN|EXPAND|aUPPERCENTRE, open[i]);
+	addlayframe("bot", "arena", nil, dTOP, dTOP);
+	dealbutton = newbutton("morecards", "deal more");
+	addlayobj(nil, "bot", nil, dLEFT, dealbutton);
+	deal();
+	setdealbuttontext();
+}
+
+deal()
+{
+	cardlib->shuffle(deck);
+	for (i := 0; i < 10; i++) {
+		deck.transfer((0, 4), open[i], 0);
+		turntop(open[i]);
+	}
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype, title: string): ref Object
+{
+	stack := cardlib->newstack(parent, nil, spec);
+	stack.setattr("type", stype, None);
+	stack.setattr("actions", "click", All);
+	stack.setattr("title", title, All);
+	return stack;
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+	if (cp.sel.isempty()) {
+		cp.sel.set(stack);
+		cp.sel.setrange(r);
+	} else {
+		if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+			cp.sel.set(nil);
+		else
+			cp.sel.setrange(r);
+	}
+}
+
+newbutton(cmd, text: string): ref Object
+{
+	but := clique.newobject(nil, All, "widget button");
+	but.setattr("command", cmd, All);
+	but.setattr("text", text, All);
+	return but;
+}
+
--- /dev/null
+++ b/appl/spree/engines/spit.b
@@ -1,0 +1,483 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, All, None, A, B: import sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	Selection, Cmember, Card: import cardlib;
+	dTOP, dLEFT, dBOTTOM, oDOWN, EXPAND, FILLX, FILLY, aCENTRELEFT, Stackspec: import Cardlib;
+include "../gather.m";
+
+clique: ref Clique;
+CLICK, SPIT, SAY, SHOW: con iota;
+playing := 0;
+dealt := 0;
+deck: ref Object;
+buttons: ref Object;
+winner: ref Member;
+
+Dmember: adt {
+	spare:	ref Object;
+	row:		array of ref Object;
+	centre:	ref Object;
+};
+
+dmembers := array[2] of ref Dmember;
+
+Openspec := Stackspec(
+	"display",		# style
+	4,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+Pilespec := Stackspec(
+	"pile",		# style
+	13,			# maxcards
+	0,			# conceal
+	"pile"		# title
+);
+
+Untitledpilespec := Stackspec(
+	"pile",		# style
+	13,			# maxcards
+	0,			# conceal
+	""			# title
+);
+
+clienttype(): string
+{
+	return "cards";
+}
+
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("spit: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->print("spit: cannot load %s: %r\n", Sets->PATH);
+		return "bad module";
+	}
+	sets->init();
+
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("spit: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+	cardlib->init(spree, clique);
+
+	return nil;
+}
+
+maxmembers(): int
+{
+	return 2;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members != 2)
+		return "need exactly two members";
+	return nil;
+}
+
+archive()
+{
+	archiveobj := cardlib->archive();
+	allow->archive(archiveobj);
+	for (i := 0; i < len dmembers; i++) {
+		dp := dmembers[i];
+		s := "d" + string i + "_";
+		cardlib->setarchivename(dp.spare, s + "spare");
+		cardlib->setarchivename(dp.centre, s + "centre");
+		for (j := 0; j < len dp.row; j++)
+			cardlib->setarchivename(dp.row[j], s + "row" + string j);
+	}
+	archiveobj.setattr("playing", string playing, None);
+	archiveobj.setattr("dealt", string dealt, None);
+	cardlib->setarchivename(deck, "deck");
+}
+
+start(members: array of ref Member, archived: int)
+{
+	cardlib->init(spree, clique);
+	if (archived) {
+		archiveobj := cardlib->unarchive();
+		allow->unarchive(archiveobj);
+		playing = int archiveobj.getattr("playing");
+		dealt = int archiveobj.getattr("dealt");
+		deck = cardlib->getarchiveobj("deck");
+		for (i := 0; i < len dmembers; i++) {
+			dp := dmembers[i] = ref Dmember;
+			s := "d" + string i + "_";
+			dp.spare = cardlib->getarchiveobj(s + "spare");
+			dp.centre = cardlib->getarchiveobj(s + "centre");
+			dp.row = array[4] of ref Object;
+			for (j := 0; j < len dp.row; j++)
+				dp.row[j] = cardlib->getarchiveobj(s + "row" + string j);
+		}
+	} else {
+		buttons = clique.newobject(nil, All, "buttons");
+		pset := None;
+		for (i := 0; i < len members; i++) {
+			Cmember.join(members[i], i);
+			pset = pset.add(members[i].id);
+		}
+		# member 0 layout visible to member 0 and everyone else but other member.
+		# could be All.del(members[1].id) but doing it this way extends to many-member cliques.
+		Cmember.index(0).layout.lay.setvisibility(All.X(A&~B, pset).add(members[0].id));
+		layout();
+		deal();
+		dealt = 1;
+		playing = 0;
+		allow->add(SPIT, nil, "spit");
+		allow->add(SAY, nil, "say &");
+		allow->add(SHOW, nil, "show");
+	}
+}
+
+command(p: ref Member, cmd: string): string
+{
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil){
+		if(winner != nil){
+			if(winner == p)
+				return "game has finished: you have won";
+			return "game has finished: you have lost";
+		}
+		return err;
+	}
+	cp := Cmember.find(p);
+	if (cp == nil)
+		return "you're only watching";
+	case tag {
+	SPIT =>
+		if (!dealt) {
+			deal();
+			dealt = 1;
+		} else if (!playing) {
+			go();
+			allow->add(CLICK, nil, "click %o %d");
+			playing = 1;
+		} else if (!canplay(!cp.ord)) {
+			go();
+		} else
+			return "it is possible to play";
+		
+	CLICK =>
+		stack := clique.objects[int hd tl toks];
+		nc := len stack.children;
+		idx := int hd tl tl toks;
+		sel := cp.sel;
+		stype := stack.getattr("type");
+		d := dmembers[cp.ord];
+		if (sel.isempty() || sel.stack == stack) {
+			# selecting a card to move
+			if (idx < 0 || idx >= len stack.children)
+				return "invalid index";
+			if (owner(stack) != cp)
+				return "not yours, don't touch!";
+			case stype {
+			"row" =>
+				card := getcard(stack.children[nc - 1]);
+				if (card.face == 0)
+					cardlib->setface(stack.children[nc - 1], 1);
+				else
+					select(cp, stack, (nc - 1, nc));
+			* =>
+				return "you can't move cards from there";
+			}
+		} else {
+			# selecting a stack to move to.
+			case stype {
+			"centre" =>
+				card := getcard(sel.stack.children[sel.r.start]);
+				onto := getcard(stack.children[nc - 1]);
+				if ((card.number + 1) % 13 != onto.number &&
+						(card.number + 12) % 13 != onto.number) {
+					sel.set(nil);
+					return "out of sequence";
+				}
+				sel.transfer(stack, -1);
+				for (i := 0; i < len d.row; i++)
+					if (len d.row[i].children > 0)
+						break;
+				if (i == len d.row) {
+					if (len d.spare.children == 0) {
+						remark(p.name + " has won");
+						winner = p;
+						allow->del(CLICK, nil);
+						allow->del(SPIT, nil);
+						clearsel();
+					} else
+						finish(cp);
+				}
+			"row" =>
+				if (owner(stack) != cp) {
+					sel.set(nil);
+					return "not yours, don't touch!";
+				}
+				if (nc != 0) {
+					sel.set(nil);
+					return "cannot stack cards";
+				}
+				sel.transfer(stack, -1);
+			* =>
+				sel.set(nil);
+				return "can't move there";
+			}
+		}
+		
+	SAY =>
+		clique.action("say member " + string p.id + ": '" + concat(tl toks) + "'", nil, nil, All);
+
+	SHOW =>
+		clique.show(nil);
+	}
+	return nil;
+}
+
+canplay(ord: int): int
+{
+	d := dmembers[ord];
+	nmulti := nfree := 0;
+	for (j := 0; j < len d.row; j++) {
+		s1 := d.row[j];
+		if (len s1.children > 0) {
+			nmulti += len s1.children > 1;
+			card1 := getcard(s1.children[len s1.children - 1]);
+			for (k := 0; k < 2; k++) {
+				s2 := dmembers[k].centre;
+				if (len s2.children > 0) {
+					card2 := getcard(s2.children[len s2.children - 1]);
+					if ((card1.number + 1) % 13 == card2.number ||
+							(card1.number + 12) % 13 == card2.number)
+						return 1;
+				}
+			}
+		} else
+			nfree++;
+	}
+	return nmulti > 0 && nfree > 0;
+}
+
+bottomdiscard(src, dst: ref Object)
+{
+	cardlib->flip(src);
+	for (i := 0; i < len src.children; i++)
+		cardlib->setface(src.children[i], 0);
+	src.transfer((0, len src.children), dst, 0);
+}
+
+finish(winner: ref Cmember)
+{
+	loser := dmembers[!winner.ord];
+	for (i := 0; i < 2; i++) {
+		d := dmembers[i];
+		bottomdiscard(d.centre, loser.spare);
+		for (j := 0; j < len d.row; j++)
+			bottomdiscard(d.row[j], loser.spare);
+	}
+	playing = 0;
+	dealt = 0;
+	allow->del(CLICK, nil);
+	allow->add(SPIT, nil, "spit");
+	clearsel();
+}
+
+go()
+{
+	for (i := 0; i < 2; i++) {
+		d := dmembers[i];
+		n := len d.spare.children;
+		if (n > 0)
+			d.spare.transfer((n - 1, n), d.centre, -1);
+		else if ((m := len dmembers[!i].spare.children) > 0)
+			dmembers[!i].spare.transfer((m - 1, m), d.centre, -1);
+		else {
+			# both members' spare piles are used up; use central piles instead
+			for (j := 0; j < 2; j++) {
+				cardlib->discard(dmembers[j].centre, dmembers[j].spare, 0);
+				cardlib->flip(dmembers[j].spare);
+			}
+			go();
+			return;
+		}
+		cardlib->setface(d.centre.children[len d.centre.children - 1], 1);
+	}
+}
+
+getcard(card: ref Object): Card
+{
+	return cardlib->getcard(card);
+}
+
+select(cp: ref Cmember, stack: ref Object, r: Range)
+{
+	if (cp.sel.isempty()) {
+		cp.sel.set(stack);
+		cp.sel.setrange(r);
+	} else {
+		if (cp.sel.r.start == r.start && cp.sel.r.end == r.end)
+			cp.sel.set(nil);
+		else
+			cp.sel.setrange(r);
+	}
+}
+
+owner(stack: ref Object): ref Cmember
+{
+	parent := clique.objects[stack.parentid];
+	n := cardlib->nmembers();
+	for (i := 0; i < n; i++) {
+		cp := Cmember.index(i);
+		if (cp.obj == parent)
+			return cp;
+	}
+	return nil;
+}
+
+layout()
+{
+	for (i := 0; i < 2; i++) {
+		cp := Cmember.index(i);
+		d := dmembers[i] = ref Dmember;
+		d.spare = newstack(cp.obj, Untitledpilespec, "spare");
+		d.row = array[4] of {* => newstack(cp.obj, Openspec, "row")};
+		d.centre = newstack(cp.obj, Untitledpilespec, "centre");
+	}
+	deck = clique.newobject(nil, All, "stack");
+	cardlib->makecards(deck, (0, 13), "0");
+	cardlib->shuffle(deck);
+
+	entry := clique.newobject(nil, All, "widget entry");
+	entry.setattr("command", "say", All);
+	cardlib->addlayobj(nil, nil, nil, dTOP|FILLX, entry);
+
+	cardlib->addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+	maketable("arena");
+	spitbutton := newbutton("spit", "Spit!");
+	for (i = 0; i < 2; i++) {
+		d := dmembers[i];
+		f := "p" + string i;
+
+		subf := "f" + string i;
+		cardlib->addlayframe(subf, f, nil, dLEFT, dTOP);
+		cardlib->addlayobj(nil, subf, Cmember.index(i).layout, dTOP, spitbutton);
+		cardlib->addlayobj(nil, subf, nil, dTOP, d.spare);
+		for (j := 0; j < len d.row; j++)
+			cardlib->addlayobj(nil, f, nil, dLEFT|EXPAND|oDOWN, d.row[j]);
+		cardlib->addlayobj(nil, "centre", nil, dLEFT|EXPAND, d.centre);
+	}
+}
+
+newbutton(cmd, text: string): ref Object
+{
+	but := clique.newobject(nil, All, "widget button");
+	but.setattr("command", cmd, All);
+	but.setattr("text", text, All);
+	return but;
+}
+
+settopface(stack: ref Object, face: int)
+{
+	n := len stack.children;
+	if (n > 0)
+		cardlib->setface(stack.children[n - 1], face);
+}
+
+transfertop(src, dst: ref Object, index: int)
+{
+	n := len src.children;
+	src.transfer((n - 1, n), dst, index);
+}
+
+deal()
+{
+	clearsel();
+	n := len deck.children;
+	if (n > 0) {
+		deck.transfer((0, n / 2), dmembers[0].spare, 0);
+		deck.transfer((0, len deck.children), dmembers[1].spare, 0);
+	}
+
+	for (i := 0; i < 2; i++) {
+		d := dmembers[i];
+loop:		for (j := 0; j < len d.row; j++) {
+			for (k := j; k < len d.row; k++) {
+				if (len d.spare.children == 0)
+					break loop;
+				transfertop(d.spare, d.row[k], -1);
+			}
+		}
+		for (j = 0; j < len d.row; j++)
+			settopface(d.row[j], 1);
+	}
+}
+
+maketable(parent: string)
+{
+	addlayframe: import cardlib;
+
+	for (i := 0; i < 2; i++) {
+		layout := Cmember.index(i).layout;
+		addlayframe("p" + string !i, parent, layout, dTOP|EXPAND, dBOTTOM);
+		addlayframe("p" + string i, parent, layout, dBOTTOM|EXPAND, dTOP);
+		addlayframe("centre", parent, layout, dTOP|EXPAND, dTOP);
+	}
+}
+
+newstack(parent: ref Object, spec: Stackspec, stype: string): ref Object
+{
+	stack := cardlib->newstack(parent, nil, spec);
+	stack.setattr("type", stype, None);
+	stack.setattr("actions", "click", All);
+	return stack;
+}
+
+concat(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
+
+remark(s: string)
+{
+	clique.action("remark " + s, nil, nil, All);
+}
+
+clearsel()
+{
+	n := cardlib->nmembers();
+	for (i := 0; i < n; i++)
+		Cmember.index(i).sel.set(nil);
+}
--- /dev/null
+++ b/appl/spree/engines/whist.b
@@ -1,0 +1,305 @@
+implement Gatherengine;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, All, None, A, B: import sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+	allow: Allow;
+include "cardlib.m";
+	cardlib: Cardlib;
+	Selection, Cmember: import cardlib;
+	dTOP, dLEFT, oRIGHT, EXPAND, FILLX, FILLY, Stackspec: import Cardlib;
+include "tricks.m";
+	tricks: Tricks;
+	Trick: import tricks;
+include "../gather.m";
+
+clique: ref Clique;
+CLICK, SAY: con iota;
+
+scores: ref Object;
+deck, pile: ref Object;
+hands, taken: array of ref Object;
+leader, turn: ref Cmember;
+trick: ref Trick;
+
+Trickpilespec := Stackspec(
+	"display",		# style
+	4,			# maxcards
+	0,			# conceal
+	"trick pile"	# title
+);
+
+Handspec := Stackspec(
+	"display",
+	13,
+	1,
+	""
+);
+
+Takenspec := Stackspec(
+	"pile",
+	52,
+	0,
+	"tricks"
+);
+
+clienttype(): string
+{
+	return "cards";
+}
+
+init(srvmod: Spree, g: ref Clique, nil: list of string, nil: int): string
+{
+	sys = load Sys Sys->PATH;
+	clique = g;
+	spree = srvmod;
+
+	allow = load Allow Allow->PATH;
+	if (allow == nil) {
+		sys->print("whist: cannot load %s: %r\n", Allow->PATH);
+		return "bad module";
+	}
+	allow->init(spree, clique);
+
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->print("spit: cannot load %s: %r\n", Sets->PATH);
+		return "bad module";
+	}
+	sets->init();
+
+	cardlib = load Cardlib Cardlib->PATH;
+	if (cardlib == nil) {
+		sys->print("whist: cannot load %s: %r\n", Cardlib->PATH);
+		return "bad module";
+	}
+
+	tricks = load Tricks Tricks->PATH;
+	if (tricks == nil) {
+		sys->print("hearts: cannot load %s: %r\n", Tricks->PATH);
+		return "bad module";
+	}
+
+	return nil;
+}
+
+maxmembers(): int
+{
+	return 4;
+}
+
+readfile(nil: int, nil: big, nil: int): array of byte
+{
+	return nil;
+}
+
+propose(members: array of string): string
+{
+	if (len members < 2)
+		return "need at least two members";
+	if (len members > 4)
+		return "too many members";
+	return nil;
+}
+
+archive()
+{
+	archiveobj := cardlib->archive();
+	allow->archive(archiveobj);
+
+	cardlib->setarchivename(scores, "scores");
+	cardlib->setarchivename(deck, "deck");
+	cardlib->setarchivename(pile, "pile");
+	cardlib->archivearray(hands, "hands");
+	cardlib->archivearray(taken, "taken");
+	if (leader != nil)
+		archiveobj.setattr("leader", string leader.ord, None);
+	if (turn != nil)
+		archiveobj.setattr("turn", string turn.ord, None);
+	trick.archive(archiveobj, "trick");
+}
+
+start(members: array of ref Member, archived: int)
+{
+	cardlib->init(spree, clique);
+	tricks->init(spree, clique, cardlib);
+	if (archived) {
+		archiveobj := cardlib->unarchive();
+		allow->unarchive(archiveobj);
+
+		scores = cardlib->getarchiveobj("scores");
+		deck = cardlib->getarchiveobj("deck");
+		pile = cardlib->getarchiveobj("pile");
+		hands = cardlib->getarchivearray("hands");
+		taken = cardlib->getarchivearray("taken");
+
+		o := archiveobj.getattr("leader");
+		if (o != nil)
+			leader = Cmember.index(int o);
+		o = archiveobj.getattr("turn");
+		if (o != nil)
+			turn = Cmember.index(int o);
+		trick = Trick.unarchive(archiveobj, "trick");
+	} else {
+		pset := None;
+		for (i := 0; i < len members; i++) {
+			Cmember.join(members[i], i);
+			pset = pset.add(members[i].id);
+		}
+		# member 0 layout visible to member 0 and everyone else but other member.
+		# could be All.del(members[1].id) but doing it this way extends to many-member cliques.
+		Cmember.index(0).layout.lay.setvisibility(All.X(A&~B, pset).add(members[0].id));
+		deck = clique.newobject(nil, All, "stack");
+		cardlib->makecards(deck, (0, 13), nil);
+		cardlib->shuffle(deck);
+		scores = clique.newobject(nil, All, "scoretable");
+		startclique();
+		n := cardlib->nmembers();
+		leader = Cmember.index(rand(n));
+		starthand();
+		titles := "";
+		for (i = 0; i < n; i++)
+			titles += members[i].name + " ";
+		clique.newobject(scores, All, "score").setattr("score", titles, All);
+	}
+}
+
+command(p: ref Member, cmd: string): string
+{
+	(err, tag, toks) := allow->action(p, cmd);
+	if (err != nil)
+		return err;
+	cp := Cmember.find(p);
+	if (cp == nil)
+		return "you're only watching";
+	case tag {
+	CLICK =>
+		# click stackid index
+		stack := p.obj(int hd tl toks);
+		if (stack != trick.hands[cp.ord])
+			return "not yours";
+		err = trick.play(cp.ord, int hd tl tl toks);
+		if (err != nil)
+			return err;
+
+		turn = turn.next(1);
+		if (turn == leader) {			# come full circle
+			winner := Cmember.index(trick.winner);
+			remark(sys->sprint("%s won the trick", winner.p.name));
+			cardlib->discard(pile, taken[winner.ord], 0);
+			nmembers := cardlib->nmembers();
+			taken[winner.ord].setattr("title",
+				string (len taken[winner.ord].children / nmembers) +
+				" tricks", All);
+			o := winner.obj;
+			trick = nil;
+			s := "";
+			for (i := 0; i < nmembers; i++) {
+				if (i == winner.ord)
+					s += "1 ";
+				else
+					s += "0 ";
+			}
+			clique.newobject(scores, All, "score").setattr("score", s, All);
+			if (len hands[winner.ord].children > 0) {
+				leader = turn = winner;
+				trick = Trick.new(pile, -1, hands, nil);
+			} else {
+				remark("one round down, some to go");
+				leader = turn  = nil;		# XXX this round over
+			}
+		}
+		canplay(turn);
+	SAY =>
+		clique.action("say member " + string p.id + ": '" + joinwords(tl toks) + "'", nil, nil, All);
+	}
+	return nil;
+}
+
+startclique()
+{
+	entry := clique.newobject(nil, All, "widget entry");
+	entry.setattr("command", "say", All);
+	cardlib->addlayobj("entry", nil, nil, dTOP|FILLX, entry);
+	cardlib->addlayframe("arena", nil, nil, dTOP|EXPAND|FILLX|FILLY, dTOP);
+	cardlib->maketable("arena");
+
+	pile = cardlib->newstack(nil, nil, Trickpilespec);
+	cardlib->addlayobj(nil, "public", nil, dTOP|oRIGHT, pile);
+	n := cardlib->nmembers();
+	hands = array[n] of ref Object;
+	taken = array[n] of ref Object;
+	tt := clique.newobject(nil, All, "widget menu");
+	tt.setattr("text", "hello", All);
+	for (ml := "one" :: "two" :: "three" :: nil; ml != nil; ml = tl ml) {
+		o := clique.newobject(tt, All, "menuentry");
+		o.setattr("text", hd ml, All);
+		o.setattr("command", hd ml, All);
+	}
+	for (i := 0; i < n; i++) {
+		cp := Cmember.index(i);
+		hands[i] = cardlib->newstack(cp.obj, cp.p, Handspec);
+		taken[i] = cardlib->newstack(cp.obj, cp.p, Takenspec);
+		p := "p" + string i;
+		cardlib->addlayframe(p + ".f", p, nil, dLEFT|oRIGHT, dTOP);
+		cardlib->addlayobj(nil, p + ".f", cp.layout, dTOP, tt);
+		cardlib->addlayobj(nil, p + ".f", nil, dTOP, hands[i]);
+		cardlib->addlayobj(nil, "p" + string i, nil, dLEFT|oRIGHT, taken[i]);
+	}
+}
+
+joinwords(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
+
+suitrank := array[] of {
+	Cardlib->CLUBS => 0,
+	Cardlib->DIAMONDS => 1,
+	Cardlib->SPADES => 2,
+	Cardlib->HEARTS => 3
+};
+
+starthand()
+{
+	cardlib->deal(deck, 13, hands, 0);
+	for (i := 0; i < len hands; i++)
+		cardlib->sort(hands[i], nil, suitrank);
+	trick = Trick.new(pile, -1, hands, nil);
+	turn = leader;
+	canplay(turn);
+}
+
+canplay(cp: ref Cmember)
+{
+	allow->del(CLICK, nil);
+	for (i := 0; i < cardlib->nmembers(); i++) {
+		ccp := Cmember.index(i);
+		v := None.add(ccp.p.id);
+		ccp.obj.setattr("status", nil, v);
+		hands[i].setattr("actions", nil, v);
+	}
+	if (cp != nil && cp.ord != -1) {
+		allow->add(CLICK, cp.p, "click %d %d");
+		v := None.add(cp.p.id);
+		cp.obj.setattr("status", "Your turn", v);
+		hands[cp.ord].setattr("actions", "click", v);
+	}
+}
+
+remark(s: string)
+{
+	clique.action("remark " + s, nil, nil, All);
+}
--- /dev/null
+++ b/appl/spree/gather.m
@@ -1,0 +1,10 @@
+Gatherengine: module {
+	init:			fn(srvmod: Spree, clique: ref Spree->Clique, argv: list of string, archived: int): string;
+	propose:		fn(members: array of string): string;
+	start:			fn(members: array of ref Spree->Member, archived: int);
+	command:	fn(member: ref Spree->Member, e: string): string;
+	readfile:		fn(f: int, offset: big, n: int): array of byte;
+	archive:		fn();
+	clienttype:	fn(): string;
+	maxmembers:	fn(): int;
+};
--- /dev/null
+++ b/appl/spree/join.b
@@ -1,0 +1,115 @@
+implement Join;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "arg.m";
+include "join.m";
+
+usage()
+{
+	sys->fprint(stderr(), "usage: joinsession [-d mntdir] [-j joinrequest] name\n");
+	raise "fail:usage";
+}
+
+CLIENTDIR: con "/dis/spree/clients";
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	arg->init(argv);
+	mnt := "/n/remote";
+	joinmsg := "join";
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'd' =>
+			if ((mnt = arg->arg()) == nil)
+				usage();
+		'j' =>
+			joinmsg = arg->arg();
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	if (len argv != 1)
+		usage();
+	arg = nil;
+	e := join(ctxt, mnt, hd argv, joinmsg);
+	if (e != nil) {
+		sys->fprint(stderr(), "startclient: %s\n", e);
+		raise "fail:error";
+	}
+}
+
+join(ctxt: ref Draw->Context, mnt: string, dir: string, joinmsg: string): string
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+
+	fd := sys->open(mnt + "/" + dir + "/ctl", Sys->ORDWR);
+	if (fd == nil)
+		return sys->sprint("cannot open %s: %r", mnt + "/" + dir + "/ctl");
+	if (joinmsg != nil)
+		if (sys->fprint(fd, "%s", joinmsg) == -1)
+			return sys->sprint("cannot join: %r");
+
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(fd, buf, len buf)) > 0) {
+		(nil, lines) := sys->tokenize(string buf[0:n], "\n");
+		for (; lines != nil; lines = tl lines) {
+			(nil, toks) := sys->tokenize(hd lines, " ");
+			if (len toks > 1 && hd toks == "clienttype") {
+				sync := chan of string;
+				spawn startclient(ctxt, hd tl toks :: mnt :: dir :: tl tl toks, fd, sync);
+				fd = nil;
+				return <-sync;
+			}
+			sys->fprint(stderr(), "startclient: unknown lobby message %s\n", hd lines);
+		}
+	}
+	return "premature EOF";
+}
+
+startclient(ctxt: ref Draw->Context, argv: list of string, fd: ref Sys->FD, sync: chan of string)
+{
+	sys->pctl(Sys->FORKNS|Sys->FORKFD|Sys->NEWPGRP, nil);
+	sys->dup(fd.fd, 0);
+	fd = nil;
+	sys->pctl(Sys->NEWFD, 0 :: 1 :: 2 :: nil);
+
+	# XXX security: weed out slashes
+	path := CLIENTDIR + "/" + hd argv + ".dis";
+	mod := load Command path;
+	if (mod == nil) {
+		sync <-= sys->sprint("cannot load %s: %r\n", path);
+		return;
+	}
+	spawn clientmod(mod, ctxt, argv);
+	sync <-= nil;
+}
+
+clientmod(mod: Command, ctxt: ref Draw->Context, argv: list of string)
+{
+	wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+	spawn mod->init(ctxt, argv);
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(wfd, buf, len buf);
+	sys->print("client process (%s) exited: %s\n", concat(argv), string buf[0:n]);
+}
+
+concat(l: list of string): string
+{
+	if (l == nil)
+		return nil;
+	s := hd l;
+	for (l = tl l; l != nil; l = tl l)
+		s += " " + hd l;
+	return s;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/spree/join.m
@@ -1,0 +1,5 @@
+Join: module {
+	PATH: con "/dis/spree/join.dis";
+	join: fn(ctxt: ref Draw->Context, mnt: string, dir: string, joinstr: string): string;
+	init:   fn(ctxt: ref Draw->Context, argv: list of string);
+};
--- /dev/null
+++ b/appl/spree/joinsession.b
@@ -1,0 +1,115 @@
+implement Joinsession;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sh.m";
+include "arg.m";
+include "joinsession.m";
+
+usage()
+{
+	sys->fprint(stderr(), "usage: joinsession [-d mntdir] [-j joinrequest] name\n");
+	raise "fail:usage";
+}
+
+CLIENTDIR: con "/dis/spree/clients";
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	arg := load Arg Arg->PATH;
+	arg->init(argv);
+	mnt := "/n/remote";
+	joinmsg := "join";
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'd' =>
+			if ((mnt = arg->arg()) == nil)
+				usage();
+		'j' =>
+			joinmsg = arg->arg();
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	if (len argv != 1)
+		usage();
+	arg = nil;
+	e := join(ctxt, mnt, hd argv, joinmsg);
+	if (e != nil) {
+		sys->fprint(stderr(), "startclient: %s\n", e);
+		raise "fail:error";
+	}
+}
+
+join(ctxt: ref Draw->Context, mnt: string, dir: string, joinmsg: string): string
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+
+	fd := sys->open(mnt + "/" + dir + "/ctl", Sys->ORDWR);
+	if (fd == nil)
+		return sys->sprint("cannot open %s: %r", mnt + "/" + dir + "/ctl");
+	if (joinmsg != nil)
+		if (sys->fprint(fd, "%s", joinmsg) == -1)
+			return sys->sprint("cannot join: %r");
+
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(fd, buf, len buf)) > 0) {
+		(nil, lines) := sys->tokenize(string buf[0:n], "\n");
+		for (; lines != nil; lines = tl lines) {
+			(nil, toks) := sys->tokenize(hd lines, " ");
+			if (len toks > 1 && hd toks == "clienttype") {
+				sync := chan of string;
+				spawn startclient(ctxt, hd tl toks :: mnt :: dir :: tl tl toks, fd, sync);
+				fd = nil;
+				return <-sync;
+			}
+			sys->fprint(stderr(), "startclient: unknown lobby message %s\n", hd lines);
+		}
+	}
+	return "premature EOF";
+}
+
+startclient(ctxt: ref Draw->Context, argv: list of string, fd: ref Sys->FD, sync: chan of string)
+{
+	sys->pctl(Sys->FORKNS|Sys->FORKFD|Sys->NEWPGRP, nil);
+	sys->dup(fd.fd, 0);
+	fd = nil;
+	sys->pctl(Sys->NEWFD, 0 :: 1 :: 2 :: nil);
+
+	# XXX security: weed out slashes
+	path := CLIENTDIR + "/" + hd argv + ".dis";
+	mod := load Command path;
+	if (mod == nil) {
+		sync <-= sys->sprint("cannot load %s: %r\n", path);
+		return;
+	}
+	spawn clientmod(mod, ctxt, argv);
+	sync <-= nil;
+}
+
+clientmod(mod: Command, ctxt: ref Draw->Context, argv: list of string)
+{
+	wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+	spawn mod->init(ctxt, argv);
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(wfd, buf, len buf);
+	sys->print("client process (%s) exited: %s\n", concat(argv), string buf[0:n]);
+}
+
+concat(l: list of string): string
+{
+	if (l == nil)
+		return nil;
+	s := hd l;
+	for (l = tl l; l != nil; l = tl l)
+		s += " " + hd l;
+	return s;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/spree/joinsession.m
@@ -1,0 +1,7 @@
+
+Joinsession: module {
+	PATH: con "/dis/spree/joinsession.dis";
+	join: fn(ctxt: ref Draw->Context, mnt: string, dir: string, join: string): string;
+	init:   fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
--- /dev/null
+++ b/appl/spree/lib/allow.b
@@ -1,0 +1,194 @@
+implement Allow;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, set, None: import sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "allow.m";
+
+Action: adt {
+	tag:		int;
+	member:	ref Member;
+	action:	string;
+};
+
+actions: list of Action;
+clique: ref Clique;
+
+init(srvmod: Spree, g: ref Clique)
+{
+	sys = load Sys Sys->PATH;
+	sets = load Sets Sets->PATH;
+	(clique, spree) = (g, srvmod);
+}
+
+ILLEGALNAME: con "/";	# illegal char in member names, ahem.
+archive(archiveobj: ref Object)
+{
+	i := 0;
+	for (al := actions; al != nil; al = tl al) {
+		a := hd al;
+		pname: string;
+		if (a.member != nil)
+			pname = a.member.name;
+		else
+			pname = ILLEGALNAME;
+		archiveobj.setattr(
+			"allow" + string i++,
+			sys->sprint("%d %s %s", a.tag, pname, a.action),
+			None
+		);
+	}
+}
+
+unarchive(archiveobj: ref Object)
+{
+	for (i := 0; (s := archiveobj.getattr("allow" + string i)) != nil; i++) {
+		(n, toks) := sys->tokenize(s, " ");
+		p: ref Member = nil;
+		if (hd tl toks != ILLEGALNAME) {
+			# if the member is no longer around, ignore the action. XXX do we still need to do this?
+			if ((p = clique.membernamed(hd tl toks)) == nil)
+				continue;
+		}
+		sys->print("allow: adding action %d, %ux, %s\n", int hd toks, p, concat(tl tl toks));
+		actions = Action(int hd toks, p, concat(tl tl toks)) :: actions;
+	}
+}
+
+add(tag: int, member: ref Member, action: string)
+{
+#	sys->print("allow: add %d, member %ux, action: %s\n", tag, member, action);
+	actions = (tag, member, action) :: actions;
+}
+
+del(tag: int, member: ref Member)
+{
+#	sys->print("allow: del %d\n", tag);
+	na: list of Action;
+	for (a := actions; a != nil; a = tl a) {
+		action := hd a;
+		if (action.tag == tag && (member == nil || action.member == member))
+			continue;
+		na = action :: na;
+	}
+	actions = na;
+}
+
+action(member: ref Member, cmd: string): (string, int, list of string)
+{
+	for (al := actions; al != nil; al = tl al) {
+		a := hd al;
+		if (a.member == nil || a.member == member) {
+			(e, v) := match(member, a.action, cmd);
+			if (e != nil || v != nil)
+				return (e, a.tag, v);
+		}
+	}
+	return ("you can't do that", -1, nil);
+}
+
+match(member: ref Member, pat, action: string): (string, list of string)
+{
+#	sys->print("allow: matching pat: '%s' against action '%s'\n", pat, action);
+	toks: list of string;
+	na := len action;
+	if (na > 0 && action[na - 1] == '\n')
+		na--;
+
+	(i, j) := (0, 0);
+	for (;;) {
+		for (; i < len pat; i++)
+			if (pat[i] != ' ')
+				break;
+		for (; j < na; j++)
+			if (action[j] != ' ')
+				break;
+		for (i1 := i; i1 < len pat; i1++)
+			if (pat[i1] == ' ')
+				break;
+		for (j1 := j; j1 < na; j1++)
+			if (action[j1] == ' ')
+				break;
+		if (i == i1) {
+			if (j == j1)
+				break;
+			return (nil, nil);
+		}
+		if (j == j1) {
+			if (pat == "&")
+				break;
+			return (nil, nil);
+		}
+		pw := pat[i : i1];
+		w := action[j : j1];
+		case pw[0] {
+		'*' =>
+			toks = w :: toks;
+		'&' =>
+			toks = w :: toks;
+			pat = "&";
+			i1 = 0;
+		'%' =>
+			(ok, nw) := checkformat(member, pw[1], w);
+			if (!ok)
+				return ("invalid field value", nil);
+			toks = nw :: toks;
+		* =>
+			if (w != pw)
+				return (nil, nil);
+			toks = w :: toks;
+		}
+		(i, j) = (i1, j1);
+	}
+	return (nil, revs(toks));
+}
+
+revs(l: list of string): list of string
+{
+	m: list of string;
+	for (; l != nil; l = tl l)
+		m = hd l :: m;
+	return m;
+}
+
+checkformat(p: ref Member, fmt: int, w: string): (int, string)
+{
+	case fmt {
+	'o' =>
+		# object id
+		if (isnum(w) && (o := p.obj(int w)) != nil)
+			return (1, string o.id);
+	'd' =>
+		# integer
+		if (isnum(w))
+			return (1, w);
+	'p' =>
+		# member id
+		if (isnum(w) && (member := clique.member(int w)) != nil)
+			return (1, w);
+	}
+	return (0, nil);
+}
+
+isnum(w: string): int
+{
+	# XXX lazy for the time being...
+	if (w != nil && ((w[0] >= '0' && w[0] <= '9') || w[0] == '-'))
+		return 1;
+	return 0;
+}
+
+concat(v: list of string): string
+{
+	if (v == nil)
+		return nil;
+	s := hd v;
+	for (v = tl v; v != nil; v = tl v)
+		s += " " + hd v;
+	return s;
+}
--- /dev/null
+++ b/appl/spree/lib/allow.m
@@ -1,0 +1,9 @@
+Allow: module {
+	PATH:	con "/dis/spree/lib/allow.dis";
+	init:		fn(srvmod: Spree, g: ref Spree->Clique);
+	add:		fn(tag: int, member: ref Spree->Member, action: string);
+	del:		fn(tag: int, member: ref Spree->Member);
+	action:	fn(member: ref Spree->Member, cmd: string): (string, int, list of string);
+	archive:	fn(archiveobj: ref Object);
+	unarchive: fn(archiveobj: ref Object);
+};
--- /dev/null
+++ b/appl/spree/lib/base64.b
@@ -1,0 +1,72 @@
+implement Base64;
+include "base64.m";
+
+PADCH: con '=';
+encode(b: array of byte): string
+{
+	chmap := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
+			"abcdefghijklmnopqrstuvwxyz0123456789+/";
+	r := "";
+	blen := len b;
+	full := (blen + 2)/ 3;
+	rplen := (4*blen + 2) / 3;
+	ip := 0;
+	rp := 0;
+	for (i:=0; i<full; i++) {
+		word := 0;
+		for (j:=2; j>=0; j--)
+			if (ip < blen)
+				word = word | int b[ip++] << 8*j;
+		for (l:=3; l>=0; l--)
+			if (rp < rplen)
+				r[rp++] = chmap[(word >> (6*l)) & 16r3f];
+			else
+				r[rp++] = PADCH;
+	}
+	return r;
+}
+
+# Decode a base 64 string to a byte stream
+# Must be a multiple of 4 characters in length
+decode(s: string): array of byte
+{
+
+	tch: int;
+	slen := len s;
+	rlen := (3*slen+3)/4;
+	if (slen >= 4 && s[slen-1] == PADCH)
+		rlen--;
+	if (slen >= 4 && s[slen-2] == PADCH)
+		rlen--;
+	r := array[rlen] of byte;
+	full := slen / 4;
+	sp := 0;
+	rp := 0;
+	for (i:=0; i<full; i++) {
+		word := 0; 
+		for (j:=0; j<4; j++) {
+			ch := s[sp++];
+			case ch {
+			'A' to 'Z' =>
+				tch = ch - 'A';
+			'a' to 'z' =>
+				tch = ch - 'a' + 26;
+			'0' to '9' =>
+				tch = ch - '0' + 52;
+			'+' =>
+				tch = 62;
+			'/' =>
+				tch = 63;
+			* =>
+				tch = 0;
+			}
+			word = (word << 6) | tch;
+		}
+		for (l:=2; l>=0; l--)
+			if (rp < rlen)
+				r[rp++] = byte( (word >> 8*l) & 16rff);
+
+	}
+	return r;
+}
+
--- /dev/null
+++ b/appl/spree/lib/base64.m
@@ -1,0 +1,5 @@
+Base64: module {
+	PATH : con "/dis/spree/lib/base64.dis";
+	encode : fn(b : array of byte) : string;
+	decode : fn(s : string) : array of byte;
+};
--- /dev/null
+++ b/appl/spree/lib/cardlib.b
@@ -1,0 +1,917 @@
+implement Cardlib;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, set, A, B, All, None: import sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member, rand: import spree;
+include "objstore.m";
+	objstore: Objstore;
+include "cardlib.m";
+
+MAXPLAYERS: con 4;
+
+Layobject: adt {
+	lay:		ref Object;
+	name:	string;
+	packopts:		int;
+	pick {
+	Obj =>
+		obj:		ref Object;		# nil if it's a frame
+	Frame =>
+		facing:	int;				# only valid if for frames
+	}
+};
+
+clique:	ref Clique;
+cmembers: array of ref Cmember;
+cpids := array[8] of list of ref Cmember;
+
+# XXX first string is unnecessary as it's held in the Layobject anyway?
+layouts := array[17] of list of (string, ref Layout, ref Layobject);
+maxlayid := 1;
+cmemberid := 1;
+
+archiveobjs: array of list of (string, ref Object);
+
+defaultrank := array[13] of {12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+defaultsuitrank := array[] of {CLUBS => 0, DIAMONDS => 1, HEARTS => 2, SPADES => 3};
+
+table := array[] of {
+	0 =>	array[] of {
+		(-1, dTOP|EXPAND, dBOTTOM, dTOP),
+	},
+	1 => array [] of {
+		(0, dBOTTOM|FILLX, dBOTTOM, dTOP),
+		(-1, dTOP|EXPAND, dBOTTOM, dTOP),
+	},
+	2 => array[] of {
+		(0, dBOTTOM|FILLX, dBOTTOM, dTOP),
+		(1, dTOP|FILLX, dTOP, dBOTTOM),
+		(-1, dTOP|EXPAND, dBOTTOM, dTOP)
+	},
+	3 => array[] of {
+		(2, dRIGHT|FILLY, dRIGHT, dLEFT),
+		(0, dBOTTOM|FILLX, dBOTTOM, dTOP),
+		(1, dTOP|FILLX, dTOP, dBOTTOM),
+		(-1, dRIGHT|EXPAND, dBOTTOM, dTOP)
+	},
+	4 => array[] of {
+		(1, dLEFT|FILLY, dLEFT, dRIGHT),
+		(3, dRIGHT|FILLY, dRIGHT, dLEFT),
+		(0, dBOTTOM|FILLX, dBOTTOM, dTOP),
+		(2, dTOP|FILLX, dTOP, dBOTTOM),
+		(-1, dRIGHT|EXPAND, dBOTTOM, dTOP)
+	},
+};
+
+
+init(mod: Spree, g: ref Clique)
+{
+	sys = load Sys Sys->PATH;
+	sets = load Sets Sets->PATH;
+	if (sets == nil)
+		panic(sys->sprint("cannot load %s: %r", Sets->PATH));
+	objstore = load Objstore Objstore->PATH;
+	if (objstore == nil)
+		panic(sys->sprint("cannot load %s: %r", Objstore->PATH));
+	objstore->init(mod, g);
+	clique = g;
+	spree = mod;
+}
+
+archive(): ref Object
+{
+	for (i := 0; i < len cmembers; i++) {
+		cp := cmembers[i];
+		setarchivename(cp.obj, "member" + string i);
+		setarchivename(cp.layout.lay, "layout" + string i);
+		sel := cp.sel;
+		if (sel.stack != nil)
+			setarchivename(sel.stack, "sel" + string i);
+	}
+	for (i = 0; i < len layouts; i++) {
+		for (ll := layouts[i]; ll != nil; ll = tl ll) {
+			(name, lay, layobj) := hd ll;
+			if (name != nil)
+				layobj.lay.setattr("layname", name, None);
+			pick l := layobj {
+			Frame =>
+				l.lay.setattr("facing", sides[l.facing], None);
+			Obj =>
+				setarchivename(l.obj, "layid" + l.obj.getattr("layid"));
+			}
+		}
+	}
+	# XXX should archive layouts that aren't particular to a member.
+	archiveobj := clique.newobject(nil, None, "archive");
+	setarchivename(archiveobj, "archive");
+	archiveobj.setattr("maxlayid", string maxlayid, None);
+	archiveobj.setattr("cmemberid", string cmemberid, None);
+	return archiveobj;
+}
+
+setarchivename(o: ref Object, name: string)
+{
+	objstore->setname(o, name);
+}
+
+getarchiveobj(name: string): ref Object
+{
+	return objstore->get(name);
+}
+
+archivearray(a: array of ref Object, name: string)
+{
+	for (i := 0; i < len a; i++)
+		objstore->setname(a[i], name + string i);
+}
+
+getarchivearray(name: string): array of ref Object
+{
+	l: list of ref Object;
+	for (i := 0; ; i++) {
+		o := objstore->get(name + string i);
+		if (o == nil)
+			break;
+		l = o :: l;
+	}
+	a := array[i] of ref Object;
+	for (; l != nil; l = tl l)
+		a[--i] = hd l;
+	return a;
+}
+
+unarchive(): ref Object
+{
+	objstore->unarchive();
+	archiveobj := getarchiveobj("archive");
+	cpl: list of ref Cmember;
+	for (i := 0; (o := getarchiveobj("member" + string i)) != nil; i++) {
+		cp := ref Cmember(
+			i,
+			int o.getattr("id"),
+			clique.membernamed(o.getattr("name")),
+			o,
+			ref Layout(getarchiveobj("layout" + string i)),
+			ref Selection(getarchiveobj("sel" + string i), -1, 1, (0, 0), nil)
+		);
+		cp.sel.ownerid = cp.id;
+		sel := cp.sel;
+		if (sel.stack != nil && (selstr := sel.stack.getattr("sel")) != nil) {
+			(n, val) := sys->tokenize(selstr, " ");
+			if (tl val != nil && hd tl val == "-")
+				(sel.r.start, sel.r.end) = (int hd val, int hd tl tl val);
+			else {
+				idxl: list of int;
+				sel.isrange = 0;
+				for (; val != nil; val = tl val)
+					idxl = int hd val :: idxl;
+				sel.idxl = idxl;
+			}
+		}
+		lay := cp.layout.lay;
+		# there should be exactly one child, of type "layframe"
+		if (len lay.children != 1 || lay.children[0].objtype != "layframe")
+			panic("invalid layout");
+		x := strhash(nil, len layouts);
+		layouts[x] = (nil, cp.layout, obj2layobj(lay.children[0])) :: layouts[x];
+		unarchivelayoutobj(cp.layout, lay.children[0]);
+		cpl = cp :: cpl;
+	}
+	cmembers = array[len cpl] of ref Cmember;
+	for (; cpl != nil; cpl = tl cpl) {
+		cp := hd cpl;
+		cmembers[cp.ord] = cp;
+		idx := cp.id % len cpids;
+		cpids[idx] = cp :: cpids[idx];
+	}
+		
+	maxlayid = int archiveobj.getattr("maxlayid");
+	cmemberid = int archiveobj.getattr("cmemberid");
+	return archiveobj;
+}
+
+unarchivelayoutobj(layout: ref Layout, o: ref Object)
+{
+	for (i := 0; i < len o.children; i++) {
+		child := o.children[i];
+		layobj := obj2layobj(child);
+		if (layobj.name != nil) {
+			x := strhash(layobj.name, len layouts);
+			layouts[x] = (layobj.name, layout, layobj) :: layouts[x];
+		}
+		if (tagof(layobj) == tagof(Layobject.Frame))
+			unarchivelayoutobj(layout, child);
+	}
+}
+
+obj2layobj(o: ref Object): ref Layobject
+{
+	case o.objtype {
+	"layframe" =>
+		return ref Layobject.Frame(
+			o,
+			o.getattr("layname"),
+			s2packopts(o.getattr("opts")),
+			searchopt(sides, o.getattr("facing"))
+		);
+	"layobj" =>
+		return ref Layobject.Obj(
+			o,
+			o.getattr("layname"),
+			s2packopts(o.getattr("opts")),
+			getarchiveobj("layid" + o.getattr("layid"))
+		);
+	* =>
+		panic("invalid layobject found, of type '" + o.objtype + "'");
+		return nil;
+	}
+}
+
+Cmember.join(member: ref Member, ord: int): ref Cmember
+{
+	cmembers = (array[len cmembers + 1] of ref Cmember)[0:] = cmembers;
+	if (ord == -1)
+		ord = len cmembers - 1;
+	else {
+		cmembers[ord + 1:] = cmembers[ord:len cmembers - 1];
+		for (i := ord + 1; i < len cmembers; i++)
+			cmembers[i].ord = i;
+	}
+	cp := cmembers[ord] = ref Cmember(ord, cmemberid++, member, nil, nil, nil);
+	cp.obj = clique.newobject(nil, All, "member");
+	cp.obj.setattr("id", string cp.id, All);
+	cp.obj.setattr("name", member.name, All);
+	cp.obj.setattr("you", string cp.id, None.add(member.id));
+	cp.obj.setattr("cliquetitle", clique.fname, All);
+	cp.layout = newlayout(cp.obj, None.add(member.id));
+	cp.sel = ref Selection(nil, cp.id, 1, (0, 0), nil);
+
+	idx := cp.id % len cpids;
+	cpids[idx] = cp :: cpids[idx];
+	return cp;
+}
+
+Cmember.find(p: ref Member): ref Cmember
+{
+	id := p.id;
+	for (i := 0; i < len cmembers; i++)
+		if (cmembers[i].p.id == id)
+			return cmembers[i];
+	return nil;
+}
+
+Cmember.index(ord: int): ref Cmember
+{
+	if (ord < 0 || ord >= len cmembers)
+		return nil;
+	return cmembers[ord];
+}
+
+Cmember.next(cp: self ref Cmember, fwd: int): ref Cmember
+{
+	if (!fwd)
+		return cp.prev(1);
+	x := cp.ord + 1;
+	if (x >= len cmembers)
+		x = 0;
+	return cmembers[x];
+}
+
+Cmember.prev(cp: self ref Cmember, fwd: int): ref Cmember
+{
+	if (!fwd)
+		return cp.next(1);
+	x := cp.ord - 1;
+	if (x < 0)
+		x = len cmembers - 1;
+	return cmembers[x];
+}
+	
+Cmember.leave(cp: self ref Cmember)
+{
+	ord := cp.ord;
+	cmembers[ord] = nil;
+	cmembers[ord:] = cmembers[ord + 1:];
+	cmembers[len cmembers - 1] = nil;
+	cmembers = cmembers[0:len cmembers - 1];
+	for (i := ord; i < len cmembers; i++)
+		cmembers[i].ord = i;
+	cp.obj.delete();
+	dellayout(cp.layout);
+	cp.layout = nil;
+	idx := cp.id % len cpids;
+	l: list of ref Cmember;
+	ll := cpids[idx];
+	for (; ll != nil; ll = tl ll)
+		if (hd ll != cp)
+			l = hd ll :: l;
+	cpids[idx] = l;
+	cp.ord = -1;
+}
+
+Cmember.findid(id: int): ref Cmember
+{
+	for (l := cpids[id % len cpids]; l != nil; l = tl l)
+		if ((hd l).id == id)
+			return hd l;
+	return nil;
+}
+
+newstack(parent: ref Object, owner: ref Member, spec: Stackspec): ref Object
+{
+	vis := All;
+	if (spec.conceal) {
+		vis = None;
+		if (owner != nil)
+			vis = vis.add(owner.id);
+	}
+	o := clique.newobject(parent, vis, "stack");
+	o.setattr("maxcards", string spec.maxcards, All);
+	o.setattr("style", spec.style, All);
+
+	# XXX provide some means for this to contain the member's name?
+	o.setattr("title", spec.title, All);
+	return o;
+}
+
+makecard(deck: ref Object, c: Card, rear: string): ref Object
+{
+	card := clique.newobject(deck, None, "card");
+	card.setattr("face", string c.face, All);
+	vis := None;
+	if(c.face)
+		vis = All;
+	card.setattr("number", string (c.number * 4 + c.suit), vis);
+	if (rear != nil)
+		card.setattr("rear", rear, All);
+	return card;
+}
+
+makecards(deck: ref Object, r: Range, rear: string)
+{
+	for (i := r.start; i < r.end; i++)
+		for(suit := 0; suit < 4; suit++)
+			makecard(deck, (suit, i, 0), rear);
+}
+
+# deal n cards to each member, if possible.
+# deal in chunks for efficiency.
+# if accuracy is required (e.g. dealing from an unshuffled
+# deck containing known cards) then this'll have to change.
+deal(deck: ref Object, n: int, stacks: array of ref Object, first: int)
+{
+	ncards := len deck.children;
+	ord := 0;
+	permember := n;
+	leftover := 0;
+	if (n * len stacks > ncards) {
+		# if trying to deal more cards than we've got,
+		# deal all that we've got, distributing the remainder fairly.
+		permember = ncards / len stacks;
+		leftover = ncards % len stacks;
+	}
+	for (i := 0; i < len stacks; i++) {
+		n = permember;
+		if (leftover > 0) {
+			n++;
+			leftover--;
+		}
+		priv := stacks[(first + i) % len stacks];
+		deck.transfer((ncards - n, ncards), priv, len priv.children);
+		priv.setattr("n", string (int priv.getattr("n") + n), All);
+		# make cards visible to member
+		for (j := len priv.children - n; j < len priv.children; j++)
+			setface(priv.children[j], 1);
+
+		ncards -= n;
+	}
+}
+
+setface(card: ref Object, face: int)
+{
+	# XXX check parent stack style and if it's a pile,
+	# only expose a face up card at the top.
+
+	card.setattr("face", string face, All);
+	if (face)
+		card.setattrvisibility("number", All);
+	else
+		card.setattrvisibility("number", None);
+}
+
+nmembers(): int
+{
+	return len cmembers;
+}
+
+getcard(card: ref Object): Card
+{
+	n := int card.getattr("number");
+	(suit, num) := (n % 4, n / 4);
+	return Card(suit, num, int card.getattr("face"));
+}
+
+getcards(stack: ref Object): array of Card
+{
+	a := array[len stack.children] of Card;
+	for (i := 0; i < len a; i++)
+		a[i] = getcard(stack.children[i]);
+	return a;
+}
+
+discard(stk, pile: ref Object, facedown: int)
+{
+	n := len stk.children;
+	if (facedown)
+		for (i := 0; i < n; i++)
+			setface(stk.children[i], 0);
+	stk.transfer((0, n), pile, len pile.children);
+}
+
+# shuffle children into a random order.  first we make all the children
+# invisible (which will cause them to be deleted in the clients) then
+# shuffle to our heart's content, and make visible again...
+shuffle(o: ref Object)
+{
+	ovis := o.visibility;
+	o.setvisibility(None);
+	a := o.children;
+	n := len a;
+	for (i := 0; i < n; i++) {
+		j := i + rand(n - i);
+		(a[i], a[j]) = (a[j], a[i]);
+	}
+	o.setvisibility(ovis);
+}
+
+sort(o: ref Object, rank, suitrank: array of int)
+{
+	if (rank == nil)
+		rank = defaultrank;
+	if (suitrank == nil)
+		suitrank = defaultsuitrank;
+	ovis := o.visibility;
+	o.setvisibility(None);
+	cardmergesort(o.children, array[len o.children] of ref Object, rank, suitrank);
+	o.setvisibility(ovis);
+}
+
+cardcmp(a, b: ref Object, rank, suitrank: array of int): int
+{
+	c1 := getcard(a);
+	c2 := getcard(b);
+	if (suitrank[c1.suit] != suitrank[c2.suit])
+		return suitrank[c1.suit] - suitrank[c2.suit];
+	return rank[c1.number] - rank[c2.number];
+}
+
+cardmergesort(a, b: array of ref Object, rank, suitrank: array of int)
+{
+	r := len a;
+	if (r > 1) {
+		m := (r-1)/2 + 1;
+		cardmergesort(a[0:m], b[0:m], rank, suitrank);
+		cardmergesort(a[m:], b[m:], rank, suitrank);
+		b[0:] = a;
+		for ((i, j, k) := (0, m, 0); i < m && j < r; k++) {
+			if (cardcmp(b[i], b[j], rank, suitrank) > 0)
+				a[k] = b[j++];
+			else
+				a[k] = b[i++];
+		}
+		if (i < m)
+			a[k:] = b[i:m];
+		else if (j < r)
+			a[k:] = b[j:r];
+	}
+}
+
+# reverse and flip all cards in stack.
+flip(stack: ref Object)
+{
+	ovis := stack.visibility;
+	stack.setvisibility(None);
+	a := stack.children;
+	(n, m) := (len a, len a / 2);
+	for (i := 0; i < m; i++) {
+		j := n - i - 1;
+		(a[i], a[j]) = (a[j], a[i]);
+	}
+	for (i = 0; i < n; i++)
+		setface(a[i], !int a[i].getattr("face"));
+	stack.setvisibility(ovis);
+}
+
+selection(stack: ref Object): ref Selection
+{
+	if ((owner := stack.getattr("owner")) != nil &&
+			(cp := Cmember.findid(int owner)) != nil)
+		return cp.sel;
+	return nil;
+}
+
+Selection.set(sel: self ref Selection, stack: ref Object)
+{
+	if (stack == sel.stack)
+		return;
+	if (stack != nil) {
+		oldowner := stack.getattr("owner");
+		if (oldowner != nil) {
+			oldcp := Cmember.findid(int oldowner);
+			if (oldcp != nil)
+				oldcp.sel.set(nil);
+		}
+	}
+	if (sel.stack != nil)
+		sel.stack.setattr("owner", nil, All);
+	sel.stack = stack;
+	sel.isrange = 1;
+	sel.r = (0, 0);
+	sel.idxl = nil;
+	setsel(sel);
+}
+
+Selection.setexcl(sel: self ref Selection, stack: ref Object): int
+{
+	if (stack != nil && (oldowner := stack.getattr("owner")) != nil)
+		if ((cp := Cmember.findid(int oldowner)) != nil && !cp.sel.isempty())
+			return 0;
+	sel.set(stack);
+	return 1;
+}
+
+Selection.owner(sel: self ref Selection): ref Cmember
+{
+	return Cmember.findid(sel.ownerid);
+}
+
+Selection.setrange(sel: self ref Selection, r: Range)
+{
+	if (!sel.isrange) {
+		sel.idxl = nil;
+		sel.isrange = 1;
+	}
+	sel.r = r;
+	setsel(sel);
+}
+
+Selection.addindex(sel: self ref Selection, i: int)
+{
+	if (sel.isrange) {
+		sel.r = (0, 0);
+		sel.isrange = 0;
+	}
+	ll: list of int;
+	for (l := sel.idxl; l != nil; l = tl l) {
+		if (hd l >= i)
+			break;
+		ll = hd l :: ll;
+	}
+	if (l != nil && hd l == i)
+		return;
+	l = i :: l;
+	for (; ll != nil; ll = tl ll)
+		l = hd ll :: l;
+	sel.idxl = l;
+	setsel(sel);
+}
+
+Selection.delindex(sel: self ref Selection, i: int)
+{
+	if (sel.isrange) {
+		sys->print("cardlib: delindex from range-type selection\n");
+		return;
+	}
+	ll: list of int;
+	for (l := sel.idxl; l != nil; l = tl l) {
+		if (hd l == i) {
+			l = tl l;
+			break;
+		}
+		ll = hd l :: ll;
+	}
+	for (; ll != nil; ll = tl ll)
+		l = hd ll :: l;
+	sel.idxl = l;
+	setsel(sel);
+}
+
+Selection.isempty(sel: self ref Selection): int
+{
+	if (sel.stack == nil)
+		return 1;
+	if (sel.isrange)
+		return sel.r.start == sel.r.end;
+	return sel.idxl == nil;
+}
+
+Selection.isset(sel: self ref Selection, index: int): int
+{
+	if (sel.isrange)
+		return index >= sel.r.start && index < sel.r.end;
+	for (l := sel.idxl; l != nil; l = tl l)
+		if (hd l == index)
+			return 1;
+	return 0;
+}
+
+Selection.transfer(sel: self ref Selection, dst: ref Object, index: int)
+{
+	if (sel.isempty())
+		return;
+	src := sel.stack;
+	if (sel.isrange) {
+		r := sel.r;
+		sel.set(nil);
+		src.transfer(r, dst, index);
+	} else {
+		if (sel.stack == dst) {
+			sys->print("cardlib: cannot move multisel to same stack\n");
+			return;
+		}
+		xl := l := sel.idxl;
+		sel.set(nil);
+		rl: list of Range;
+		for (; l != nil; l = tl l) {
+			r := Range(hd l, hd l);
+			last := l;
+			# concatenate adjacent items, for efficiency.
+			for (l = tl l; l != nil; (last, l) = (l, tl l)) {
+				if (hd l != r.end + 1)
+					break;
+				r.end = hd l;
+			}
+			rl = (r.start, r.end + 1) :: rl;
+			l = last;
+		}
+		# do ranges in reverse, so that later ranges
+		# aren't affected by earlier ones.
+		if (index == -1)
+			index = len dst.children;
+		for (; rl != nil; rl = tl rl)
+			src.transfer(hd rl, dst, index);
+	}
+}
+
+setsel(sel: ref Selection)
+{
+	if (sel.stack == nil)
+		return;
+	s := "";
+	if (sel.isrange) {
+		if (sel.r.end > sel.r.start)
+			s = string sel.r.start + " - " + string sel.r.end;
+	} else {
+		if (sel.idxl != nil) {
+			s = string hd sel.idxl;
+			for (l := tl sel.idxl; l != nil; l = tl l)
+				s += " " + string hd l;
+		}
+	}
+	if (s != nil)
+		sel.stack.setattr("owner", string sel.owner().id, All);
+	else
+		sel.stack.setattr("owner", nil, All);
+	vis := None.add(sel.owner().p.id);
+	sel.stack.setattr("sel", s, vis);
+	sel.stack.setattrvisibility("sel", vis);
+}
+
+newlayout(parent: ref Object, vis: Set): ref Layout
+{
+	l := ref Layout(clique.newobject(parent, vis, "layout"));
+	x := strhash(nil, len layouts);
+	layobj := ref Layobject.Frame(nil, "", dTOP|EXPAND|FILLX|FILLY, dTOP);
+	layobj.lay = clique.newobject(l.lay, All, "layframe");
+	layobj.lay.setattr("opts", packopts2s(layobj.packopts), All);
+	layouts[x] = (nil, l, layobj) :: layouts[x];
+#	sys->print("[%d] => ('%s', %ux, %ux) (new layout)\n", x, "", l, layobj);
+	return l;
+}
+
+addlayframe(name, parent: string, layout: ref Layout, packopts: int, facing: int)
+{
+#	sys->print("addlayframe('%s', %ux, name: %s\n", parent, layout, name);
+	addlay(parent, layout, ref Layobject.Frame(nil, name, packopts, facing));
+}
+
+addlayobj(name, parent: string, layout: ref Layout, packopts: int, obj: ref Object)
+{
+#	sys->print("addlayobj('%s', %ux, name: %s, obj %d\n", parent, layout, name, obj.id);
+	addlay(parent, layout, ref Layobject.Obj(nil, name, packopts, obj));
+}
+
+addlay(parent: string, layout: ref Layout, layobj: ref Layobject)
+{
+	a := layouts;
+	name := layobj.name;
+	x := strhash(name, len a);
+	added := 0;
+	for (nl := a[strhash(parent, len a)]; nl != nil; nl = tl nl) {
+		(s, lay, parentlay) := hd nl;
+		if (s == parent && (layout == nil || layout == lay)) {
+			pick p := parentlay {
+			Obj =>
+				sys->fprint(sys->fildes(2),
+					"cardlib: cannot add layout to non-frame: %d\n", p.obj.id);
+			Frame =>
+				nlayobj := copylayobj(layobj);
+				nlayobj.packopts = packoptsfacing(nlayobj.packopts, p.facing);
+				o: ref Object;
+				pick lo := nlayobj {
+				Obj =>
+					o = clique.newobject(p.lay, All, "layobj");
+					id := lo.obj.getattr("layid");
+					if (id == nil) {
+						id = string maxlayid++;
+						lo.obj.setattr("layid", id, All);
+					}
+					o.setattr("layid", id, All);
+				Frame =>
+					o = clique.newobject(p.lay, All, "layframe");
+					lo.facing = (lo.facing + p.facing) % 4;
+				}
+				o.setattr("opts", packopts2s(nlayobj.packopts), All);
+				nlayobj.lay = o;
+				if (name != nil)
+					a[x] = (name, lay, nlayobj) :: a[x];
+				added++;
+			}
+		}
+	}
+	if (added == 0)
+		sys->print("no parent found, adding '%s', parent '%s', layout %ux\n",
+			layobj.name, parent, layout);
+#	sys->print("%d new entries\n", added);
+}
+
+maketable(parent: string)
+{
+	# make a table for all current members.
+	plcount := len cmembers;
+	packopts := table[plcount];
+	for (i := 0; i < plcount; i++) {
+		layout := cmembers[i].layout;
+		for (j := 0; j < len packopts; j++) {
+			(ord, outer, inner, facing) := packopts[j];
+			name := "public";
+			if (ord != -1)
+				name = "p" + string ((ord + i) % plcount);
+			addlayframe("@" + name, parent, layout, outer, dTOP);
+			addlayframe(name, "@" + name, layout, inner, facing);
+		}
+	}
+}
+
+dellay(name: string, layout: ref Layout)
+{
+	a := layouts;
+	x := strhash(name, len a);
+	rl: list of (string, ref Layout, ref Layobject);
+	for (nl := a[x]; nl != nil; nl = tl nl) {
+		(s, lay, layobj) := hd nl;
+		if (s != name || (layout != nil && layout != lay))
+			rl = hd nl :: rl;
+	}
+	a[x] = rl;
+}
+
+dellayout(layout: ref Layout)
+{
+	for (i := 0; i < len layouts; i++) {
+		ll: list of (string, ref Layout, ref Layobject);
+		for (nl := layouts[i]; nl != nil; nl = tl nl) {
+			(s, lay, layobj) := hd nl;
+			if (lay != layout)
+				ll = hd nl :: ll;
+		}
+		layouts[i] = ll;
+	}
+}
+
+copylayobj(obj: ref Layobject): ref Layobject
+{
+	pick o := obj {
+	Frame =>
+		return ref *o;
+	Obj =>
+		return ref *o;
+	}
+	return nil;
+}
+
+packoptsfacing(opts, facing: int): int
+{
+	if (facing == dTOP)
+		return opts;
+	nopts := 0;
+
+	# 4 directions
+	nopts |= (facing + (opts & dMASK)) % 4;
+
+	# 2 orientations
+	nopts |= ((facing + ((opts & oMASK) >> oSHIFT)) % 4) << oSHIFT;
+
+	# 8 anchorpoints (+ centre)
+	a := (opts & aMASK);
+	if (a != aCENTRE)
+		a = ((((a >> aSHIFT) - 1 + facing * 2) % 8) + 1) << aSHIFT;
+	nopts |= a;
+
+	# two fill options
+	if (facing % 2) {
+		if (opts & FILLX)
+			nopts |= FILLY;
+		if (opts & FILLY)
+			nopts |= FILLX;
+	} else
+		nopts |= (opts & (FILLX | FILLY));
+
+	nopts |= (opts & EXPAND);
+	return nopts;
+}
+
+# these arrays are dependent on the ordering of
+# the relevant constants defined in cardlib.m
+
+sides := array[] of {"top", "left", "bottom", "right"};
+anchors := array[] of {"centre", "n", "nw", "w", "sw", "s", "se", "e", "ne"};
+orientations := array[] of {"right", "up", "left", "down"};
+fills := array[] of {"none", "x", "y", "both"};
+
+packopts2s(opts: int): string
+{
+	s := orientations[(opts & oMASK) >> oSHIFT] +
+			" -side " + sides[opts & dMASK];
+	if ((opts & aMASK) != aCENTRE)
+		s += " -anchor " + anchors[(opts & aMASK) >> aSHIFT];
+	if (opts & EXPAND)
+		s += " -expand 1";
+	if (opts & (FILLX | FILLY))
+		s += " -fill " + fills[(opts & FILLMASK) >> FILLSHIFT];
+	return s;
+}
+
+searchopt(a: array of string, s: string): int
+{
+	for (i := 0; i < len a; i++)
+		if (a[i] == s)
+			return i;
+	panic("unknown pack option '" + s + "'");
+	return 0;
+}
+
+s2packopts(s: string): int
+{
+	(nil, toks) := sys->tokenize(s, " ");
+	if (toks == nil)
+		panic("invalid packopts: " + s);
+	p := searchopt(orientations, hd toks) << oSHIFT;
+	for (toks = tl toks; toks != nil; toks = tl tl toks) {
+		if (tl toks == nil)
+			panic("invalid packopts: " + s);
+		arg := hd tl toks;
+		case hd toks {
+		"-anchor" =>
+			p |= searchopt(anchors, arg) << aSHIFT;
+		"-fill" =>
+			p |= searchopt(fills, arg) << FILLSHIFT;
+		"-side" =>
+			p |= searchopt(sides, arg) << dSHIFT;
+		"-expand" =>
+			if (int hd tl toks)
+				p |= EXPAND;
+		* =>
+			panic("unknown pack option: " + hd toks);
+		}
+	}
+	return p;
+}
+
+panic(e: string)
+{
+	sys->fprint(sys->fildes(2), "cardlib panic: %s\n", e);
+	raise "panic";
+}
+
+assert(b: int, err: string)
+{
+	if (b == 0)
+		raise "parse:" + err;
+}
+
+# from Aho Hopcroft Ullman
+strhash(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i := 0; i<m; i++){
+		h = 65599 * h + s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
--- /dev/null
+++ b/appl/spree/lib/cardlib.m
@@ -1,0 +1,114 @@
+Cardlib: module {
+	PATH:		con "/dis/spree/lib/cardlib.dis";
+
+	Layout: adt {
+		lay:			ref Spree->Object;		# the actual layout object
+	};
+
+	Stackspec: adt {
+		style:	string;
+		maxcards:	int;
+		conceal:	int;
+		title:		string;
+	};
+
+	Card: adt {
+		suit:		int;
+		number:	int;
+		face:		int;
+	};
+
+	# a member currently playing
+	Cmember: adt {
+		ord:		int;
+		id:		int;
+		p:		ref Spree->Member;
+		obj:		ref Spree->Object;
+		layout:	ref Layout;
+		sel:		ref Selection;
+
+		join:		fn(p: ref Spree->Member, ord: int): ref Cmember;
+		index:	fn(ord: int): ref Cmember;
+		find:		fn(p: ref Spree->Member): ref Cmember;
+		findid:	fn(id: int): ref Cmember;
+		leave:	fn(cp: self ref Cmember);
+		next:		fn(cp: self ref Cmember, fwd: int): ref Cmember;
+		prev:		fn(cp: self ref Cmember, fwd: int): ref Cmember;
+	};
+
+	Selection: adt {
+		stack:	ref Spree->Object;
+		ownerid:	int;
+		isrange:	int;
+		r:		Range;
+		idxl:		list of int;
+
+		set:		fn(sel: self ref Selection, stack: ref Spree->Object);
+		setexcl:	fn(sel: self ref Selection, stack: ref Spree->Object): int;
+		setrange:	fn(sel: self ref Selection, r: Range);
+		addindex:	fn(sel: self ref Selection, i: int);
+		delindex:	fn(sel: self ref Selection, i: int);
+		isempty:	fn(sel: self ref Selection): int;
+		isset:		fn(sel: self ref Selection, index: int): int;
+		transfer:	fn(sel: self ref Selection, dst: ref Spree->Object, index: int);
+		owner:	fn(sel: self ref Selection): ref Cmember;
+	};
+
+	selection:	fn(stack: ref Spree->Object): ref Selection;
+
+	# pack and facing directions (clockwise by face direction)
+	dTOP, dLEFT, dBOTTOM, dRIGHT: con iota;
+	dMASK: con 7;
+	dSHIFT: con 0;
+	
+	# anchor positions
+	aSHIFT: con 4;
+	aMASK: con 16rf0;
+	aCENTRE, aUPPERCENTRE, aUPPERLEFT, aCENTRELEFT,
+		aLOWERLEFT, aLOWERCENTRE, aLOWERRIGHT,
+		aCENTRERIGHT, aUPPERRIGHT: con iota << aSHIFT;
+	
+	# orientations
+	oMASK: con 16rf00;
+	oSHIFT: con 8;
+	oRIGHT, oUP, oLEFT, oDOWN: con iota << oSHIFT;
+
+	EXPAND: con 16r1000;
+
+	FILLSHIFT: con 13;
+	FILLX, FILLY: con 1 << (FILLSHIFT + iota);
+	FILLMASK: con FILLX|FILLY;
+
+	CLUBS, DIAMONDS, HEARTS, SPADES: con iota;
+
+	init:			fn(spree: Spree, clique: ref Spree->Clique);
+
+	addlayframe:	fn(name: string, parent: string, layout: ref Layout, packopts: int, facing: int);
+	addlayobj:	fn(name: string, parent: string, layout: ref Layout, packopts: int, obj: ref Spree->Object);
+	dellay:		fn(name: string, layout: ref Layout);
+
+	newstack:		fn(parent: ref Spree->Object, p: ref Spree->Member, spec: Stackspec): ref Spree->Object;
+
+	archive:		fn(): ref Spree->Object;
+	unarchive:	fn(): ref Spree->Object;
+	setarchivename: fn(o: ref Spree->Object, name: string);
+	getarchiveobj:	fn(name: string): ref Spree->Object;
+	archivearray:	fn(a: array of ref Spree->Object, name: string);
+	getarchivearray: fn(name: string): array of ref Spree->Object;
+
+	newlayout:	fn(parent: ref Spree->Object, vis: Sets->Set): ref Layout;
+	makecards:	fn(stack: ref Spree->Object, r: Range, rear: string);
+	maketable:	fn(parent: string);
+	deal:			fn(stack: ref Spree->Object, n: int, stacks: array of ref Spree->Object, first: int);
+	shuffle:		fn(stack: ref Spree->Object);
+	sort:			fn(stack: ref Spree->Object, rank, suitrank: array of int);
+
+	getcard:		fn(card: ref Spree->Object): Card;
+	getcards:		fn(stack: ref Spree->Object): array of Card;
+	discard:		fn(stk, pile: ref Spree->Object, facedown: int);
+	setface:		fn(card: ref Spree->Object, face: int);
+
+	flip:			fn(stack: ref Spree->Object);
+
+	nmembers:		fn(): int;
+};
--- /dev/null
+++ b/appl/spree/lib/commandline.b
@@ -1,0 +1,191 @@
+implement Commandline;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "commandline.m";
+
+Debug: con 0;
+
+nomodule(modpath: string)
+{
+	sys->fprint(stderr(), "fibs: couldn't load %s: %r\n", modpath);
+	raise "fail:bad module";
+}
+
+init()
+{	sys = load Sys Sys->PATH;
+
+	tk = load Tk Tk->PATH;
+	if (tk == nil) nomodule(Tk->PATH);
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil) nomodule(Tkclient->PATH);
+	tkclient->init();
+}
+
+Cmdline.new(top: ref Tk->Toplevel, w, textopts: string): (ref Cmdline, chan of string)
+{
+	window_cfg := array[] of {
+		"frame " + w,
+		"scrollbar " + w + ".scroll -command {" + w + ".t yview}",
+		"text " + w + ".t -yscrollcommand {" + w + ".scroll set} " + textopts,
+		"pack " + w + ".scroll -side left -fill y",
+		"pack " + w + ".t -fill both -expand 1",
+	
+		"bind " + w + ".t <Key> {send evch k {%A}}",
+		"bind " + w + ".t <Control-d> {send evch k {%A}}",
+		"bind " + w + ".t <Control-u> {send evch k {%A}}",
+		"bind " + w + ".t <Control-w> {send evch k {%A}}",
+		"bind " + w + ".t <Control-h> {send evch k {%A}}",
+		# treat button 2 and button 3 the same so we're alright with a 2-button mouse
+		"bind " + w + ".t <ButtonPress-2> {send evch b %x %y}",
+		"bind " + w + ".t <ButtonPress-3> {send evch b %x %y}",
+		w + ".t mark set outpoint end",
+		w + ".t mark gravity outpoint left",
+		w + ".t mark set inpoint end",
+		w + ".t mark gravity inpoint left",
+	};
+	evch := chan of string;
+	tk->namechan(top, evch, "evch");
+
+	for (i := 0; i < len window_cfg; i++) {
+		e := cmd(top, window_cfg[i]);
+		if (e != nil && e[0] == '!')
+			break;
+	}
+
+	err := tk->cmd(top, "variable lasterror");
+	if (err != nil) {
+		sys->fprint(stderr(), "error in commandline config: %s\n", err);
+		raise "fail:commandline config error";
+	}
+	cmd(top, w + ".t mark set insert end;" + w + ".t see insert");
+	return (ref Cmdline(w, top), evch);
+}
+
+Cmdline.focus(cmdl: self ref Cmdline)
+{
+	cmd(cmdl.top, "focus " + cmdl.w + ".t");
+}
+
+Cmdline.event(cmdl: self ref Cmdline, e: string): list of string
+{
+	case e[0] {
+	'k' =>
+		return handle_key(cmdl, e[2:]);
+	'b' =>
+		;
+	}
+	return nil;
+}
+
+BS:		con 8;		# ^h backspace character
+BSW:		con 23;		# ^w bacspace word
+BSL:		con 21;		# ^u backspace line
+
+handle_key(cmdl: ref Cmdline, c: string): list of string
+{
+	(w, top) := (cmdl.w, cmdl.top);
+	# don't allow editing of the text before the inpoint.
+	if (int cmd(top, w + ".t compare insert < inpoint"))
+		return nil;
+	lines: list of string;
+	char := c[1];
+	if (char == '\\')
+		char = c[2];
+	case char {
+	* =>
+		cmd(top, w + ".t insert insert "+c+" {}");
+	'\n' =>
+		cmd(top, w + ".t insert insert "+c+" {}");
+		lines = sendinput(cmdl);
+	BSL or BSW or BS =>
+		delpoint: string;
+		case char {
+		BSL =>	delpoint = "{insert linestart}";
+		BSW =>	delpoint = "{insert -1char wordstart}";	# wordstart isn't ideal
+		BS  =>	delpoint = "{insert-1char}";
+		}
+		if (int cmd(top, w + ".t compare inpoint < " + delpoint))
+			cmd(top, w + ".t delete "+delpoint+" insert");
+		else
+			cmd(top, w + ".t delete inpoint insert");
+	}
+	cmd(top, w + ".t see insert;update");
+	return lines;
+}
+
+sendinput(cmdl: ref Cmdline): list of string
+{
+	(w, top) := (cmdl.w, cmdl.top);
+	# loop through all the lines that have been entered,
+	# processing each one in turn.
+	nl, lines: list of string;
+	for (;;) {
+		input: string;
+		input = cmd(top, w + ".t get inpoint end");
+		if (len input == 0)
+			break;
+		for (i := 0; i < len input; i++)
+			if (input[i] == '\n')
+				break;
+		if (i >= len input)
+			break;
+		cmd(top, w + ".t mark set outpoint inpoint+"+string (i+1)+"chars");
+		cmd(top, w + ".t mark set inpoint outpoint");
+		lines = input[0:i+1] :: lines;
+	}
+	for (; lines != nil; lines = tl lines)
+		nl = hd lines :: nl;
+	return nl;
+}
+
+add(cmdl: ref Cmdline, t: string, n: int)
+{
+	(w, top) := (cmdl.w, cmdl.top);
+	cmd(top, w + ".t insert outpoint " + t);
+	cmd(top, w + ".t mark set outpoint outpoint+"+string n+"chars");
+	cmd(top, w + ".t mark set inpoint outpoint");
+	cmd(top, w + ".t see insert");
+}
+
+Cmdline.tagaddtext(cmdl: self ref Cmdline, t: list of (string, string))
+{
+	txt := "";
+	n := 0;
+	for (; t != nil; t = tl t) {
+		(tags, s) := hd t;
+		txt += " " + tk->quote(s) + " {" + tags + "}";
+		n += len s;
+	}
+	add(cmdl, txt, n);
+}
+
+Cmdline.addtext(cmdl: self ref Cmdline, txt: string)
+{
+	if (Debug) sys->print("%s", txt);
+	add(cmdl, tk->quote(txt) + " {}" , len txt);
+}
+
+Cmdline.maketag(cmdl: self ref Cmdline, name, options: string)
+{
+	cmd(cmdl.top, cmdl.w + ".t tag configure " + name + " " + options);
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr(), "cmd error on '%s': %s\n", s, e);
+	return e;
+}
--- /dev/null
+++ b/appl/spree/lib/commandline.m
@@ -1,0 +1,16 @@
+Commandline: module {
+	init:			fn();
+
+	PATH:		con "/dis/spree/lib/commandline.dis";
+	Cmdline: adt {
+		new:			fn(win: ref Tk->Toplevel, w, textopts: string): (ref Cmdline, chan of string);
+		event:		fn(cmdl: self ref Cmdline, e: string): list of string;
+		tagaddtext:	fn(cmdl: self ref Cmdline, t: list of (string, string));
+		addtext:		fn(cmdl: self ref Cmdline, txt: string);
+		focus:		fn(cmdl: self ref Cmdline);
+		maketag:		fn(cmdl: self ref Cmdline, name, options: string);
+
+		w: string;
+		top: ref Tk->Toplevel;
+	};
+};
--- /dev/null
+++ b/appl/spree/lib/objstore.b
@@ -1,0 +1,65 @@
+implement Objstore;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	None: import Sets;
+include "../spree.m";
+	spree: Spree;
+	Object, Clique: import spree;
+include "objstore.m";
+
+clique: ref Clique;
+archiveobjs: array of list of (string, ref Object);
+
+init(mod: Spree, g: ref Clique)
+{
+	sys = load Sys Sys->PATH;
+	spree = mod;
+	clique = g;
+}
+
+unarchive()
+{
+	archiveobjs = array[27] of list of (string, ref Object);
+	for (i := 0; i < len clique.objects; i++) {
+		obj := clique.objects[i];
+		if (obj != nil && (nm := obj.getattr("§")) != nil) {
+			(n, toks) := sys->tokenize(nm, " ");
+			for (; toks != nil; toks = tl toks) {
+				x := strhash(hd toks, len archiveobjs);
+				archiveobjs[x] = (hd toks, obj) :: archiveobjs[x];
+			}
+			obj.setattr("§", nil, None);
+		}
+	}
+}
+
+setname(obj: ref Object, name: string)
+{
+	nm := obj.getattr("§");
+	if (nm != nil)
+		nm += " " + name;
+	else
+		nm = name;
+	obj.setattr("§", nm, None);
+}
+
+get(name: string): ref Object
+{
+	for (al := archiveobjs[strhash(name, len archiveobjs)]; al != nil; al = tl al)
+		if ((hd al).t0 == name)
+			return (hd al).t1;
+	return nil;
+}
+
+# from Aho Hopcroft Ullman
+strhash(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i := 0; i<m; i++){
+		h = 65599 * h + s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
--- /dev/null
+++ b/appl/spree/lib/objstore.m
@@ -1,0 +1,8 @@
+Objstore: module {
+	PATH: con "/dis/spree/lib/objstore.dis";
+
+	init:			fn(mod: Spree, g: ref Clique);
+	unarchive:	fn();
+	setname:		fn(o: ref Object, name: string);
+	get:			fn(name: string): ref Object;
+};
--- /dev/null
+++ b/appl/spree/lib/testsets.b
@@ -1,0 +1,152 @@
+implement Testsets;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "rand.m";
+include "sets.m";		# "sets.m" or "sets32.m"
+	sets: Sets;
+	Set, set, A, B: import sets;
+
+BPW: con 32;
+SHIFT: con 5;
+MASK: con 31;
+
+Testsets: module {
+	init: fn(nil: ref Draw->Context, nil: list of string);
+};
+
+∅: Set;
+
+Testbig: con 1;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	sets = load Sets Sets->PATH;
+	if (sets == nil) {
+		sys->print("cannot load %s: %r\n", Sets->PATH);
+		exit;
+	}
+	rand := load Rand Rand->PATH;
+	sets->init();
+
+	∅ = set();
+	s := set().addlist(1::2::3::4::nil);
+	addit(s);
+	sys->print("s %s\n", s.str());
+	r := s.invert();
+	sys->print("r %s\n", r.str());
+	r = r.del(20);
+	addit(r);
+	sys->print("r del20: %s\n", r.str());
+	z := r.X(~A&~B, s);
+	addit(z);
+	sys->print("z: %s\n", z.str());
+
+	x := set();
+	for (i := 0; i < 31; i++)
+		if (rand->rand(2))
+			x = x.add(i);
+	addit(x);
+	for(i = 0; i < 31; i++)
+		addit(set().add(i));
+	if (Testbig) {
+		r = r.del(100);
+		addit(r);
+		sys->print("rz: %s\n", r.str());
+		r = r.add(100);
+		addit(r);
+		sys->print("rz2: %s\n", r.str());
+		x = set();
+		for (i = 0; i < 200; i++)
+			x = x.add(rand->rand(300));
+		addit(x);
+		for(i = 31; i < 70; i++)
+			addit(set().add(i));
+	}
+	sys->print("empty: %s\n", set().str());
+	addit(set());
+	sys->print("full: %s\n", set().invert().str());
+	test();
+	sys->print("done tests\n");
+}
+
+ds(d: array of byte): string
+{
+	s := "";
+	for(i := len d - 1; i >= 0; i--)
+		s += sys->sprint("%.2x", int d[i]);
+	return s;
+}
+
+testsets: list of Set;
+addit(s: Set)
+{
+	testsets = s :: testsets;
+}
+
+test()
+{
+	for (t := testsets; t != nil; t = tl t)
+		testsets = (hd t).invert() :: testsets;
+
+	for (t = testsets; t != nil; t = tl t)
+		testa(hd t);
+	for (t = testsets; t != nil; t = tl t) {
+		a := hd t;
+		for (s := testsets; s != nil; s = tl s) {
+			b := hd s;
+			testab(a, b);
+		}
+	}
+}
+
+testab(a, b: Set)
+{
+	{
+		check(!a.eq(b) == !b.eq(a), "equality");
+		if (superset(a, b) && !a.eq(b))
+			check(!superset(b, a), "superset");
+	} exception {
+	"test failed" =>
+		sys->print("%s, %s [%s, %s]\n", a.str(), b.str(), a.debugstr(), b.debugstr());
+	}
+}
+
+testa(a: Set)
+{
+	{
+		check(sets->str2set(a.str()).eq(a), "string conversion");
+		check(a.eq(a), "self equality");
+		check(a.eq(a.invert().invert()), "double inversion");
+		check(a.X(A&~B, a).eq(∅), "self not intersect");
+		check(a.limit() == a.invert().limit(), "invert limit");
+		check(a.X(A&~B, set().invert()).limit() == 0, "zero limit");
+		check(sets->bytes2set(a.bytes(0)).eq(a), "bytes conversion");
+		check(sets->bytes2set(a.bytes(3)).eq(a), "bytes conversion(2)");
+
+		if (a.limit() > 0) {
+			if (a.msb())
+				check(!a.holds(a.limit() - 1), "hold limit 1");
+			else
+				check(a.holds(a.limit() - 1), "hold limit 2");
+		}
+	} exception {
+	"test failed" =>
+		sys->print("%s [%s]\n", a.str(), a.debugstr());
+	}
+}
+
+check(ok: int, s: string)
+{
+	if (!ok) {
+		sys->print("test failed: %s; ", s);
+		raise "test failed";
+	}
+}
+
+# return true if a is a superset of b
+superset(a, b: Set): int
+{
+	return a.X(~A&B, b).eq(∅);
+}
--- /dev/null
+++ b/appl/spree/lib/tricks.b
@@ -1,0 +1,140 @@
+implement Tricks;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "sets.m";
+	sets: Sets;
+	Set, All, None: import sets;
+include "../spree.m";
+	spree: Spree;
+	Attributes, Range, Object, Clique, Member: import spree;
+include "cardlib.m";
+	cardlib: Cardlib;
+	Card, getcard: import cardlib;
+include "tricks.m";
+
+clique: ref Clique;
+
+init(mod: Spree, g: ref Clique, cardlibmod: Cardlib)
+{
+	sys = load Sys Sys->PATH;
+	sets = load Sets Sets->PATH;
+	if (sets == nil)
+		panic(sys->sprint("cannot load %s: %r", Sets->PATH));
+	clique = g;
+	spree = mod;
+	cardlib = cardlibmod;
+}
+
+defaultrank := array[13] of {12, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+# XXX should take a "rank" array so that we can cope with custom
+# card ranking
+Trick.new(pile: ref Object, trumps: int, hands: array of ref Object, rank: array of int): ref Trick
+{
+	t := ref Trick;
+	t.highcard = t.startcard = Card(-1, -1, -1);
+	t.winner = -1;
+	t.trumps = trumps;
+	t.pile = pile;
+	t.hands = hands;
+	if (rank == nil)
+		rank = defaultrank;
+	t.rank = rank;
+	return t;
+}
+
+Trick.archive(t: self ref Trick, archiveobj: ref Object, name: string)
+{
+	a := clique.newobject(archiveobj, None, "trick");
+	cardlib->setarchivename(a, name);
+	a.setattr("trumps", string t.trumps, None);
+	a.setattr("winner", string t.winner, None);
+	a.setattr("startcard.n", string t.startcard.number, None);
+	a.setattr("startcard.suit", string t.startcard.suit, None);
+	a.setattr("highcard.n", string t.highcard.number, None);
+	a.setattr("highcard.suit", string t.highcard.suit, None);
+	cardlib->setarchivename(t.pile, name + ".pile");
+	cardlib->archivearray(t.hands, name);
+	for (i := 0; i < len t.rank; i++)
+		if (t.rank[i] != defaultrank[i])
+			break;
+	if (i < len t.rank) {
+		r := "";
+		for (i = 0; i < len t.rank; i++)
+			r += " " + string t.rank[i];
+		a.setattr("rank", r, None);
+	}
+}
+
+Trick.unarchive(nil: ref Object, name: string): ref Trick
+{
+	t := ref Trick;
+	a := cardlib->getarchiveobj(name);
+	t.trumps = int a.getattr("trumps");
+	t.winner = int a.getattr("winner");
+	t.startcard.number = int a.getattr("startcard.n");
+	t.startcard.suit = int a.getattr("startcard.suit");
+	t.highcard.number = int a.getattr("highcard.n");
+	t.highcard.suit = int a.getattr("highcard.suit");
+	t.pile = cardlib->getarchiveobj(name + ".pile");
+	t.hands = cardlib->getarchivearray(name);
+	r := a.getattr("rank");
+	if (r != nil) {
+		(nil, toks) := sys->tokenize(r, " ");
+		t.rank = array[len toks] of int;
+		i := 0;
+		for (; toks != nil; toks = tl toks)
+			t.rank[i++] = int hd toks;
+	} else
+		t.rank = defaultrank;
+	return t;
+}
+
+Trick.play(t: self ref Trick, ord, idx: int): string
+{
+	stack := t.hands[ord];
+	if (idx < 0 || idx >= len stack.children)
+		return "invalid card to play";
+
+	c := getcard(stack.children[idx]);
+	c.number = t.rank[c.number];
+	if (len t.pile.children == 0) {
+		t.winner = ord;
+		t.startcard = t.highcard = c;
+	} else {
+		if (c.suit != t.startcard.suit) {
+			if (containssuit(stack, t.startcard.suit))
+				return "you must play the suit that was led";
+			if (c.suit == t.trumps &&
+					(t.highcard.suit != t.trumps ||
+					c.number > t.highcard.number)) {
+				t.highcard = c;
+				t.winner = ord;
+			}
+		} else if (c.suit == t.highcard.suit && c.number > t.highcard.number) {
+			t.highcard = c;
+			t.winner = ord;
+		}
+	}
+
+	stack.transfer((idx, idx + 1), t.pile, len t.pile.children);
+	stack.setattr("n", string (int stack.getattr("n") - 1), All);
+	return nil;
+}
+
+containssuit(stack: ref Object, suit: int): int
+{
+	ch := stack.children;
+	n := len ch;
+	for (i := 0; i < n; i++)
+		if (getcard(ch[i]).suit == suit)
+			return 1;
+	return 0;
+}
+
+panic(e: string)
+{
+	sys->fprint(sys->fildes(2), "tricks panic: %s\n", e);
+	raise "panic";
+}
--- /dev/null
+++ b/appl/spree/lib/tricks.m
@@ -1,0 +1,21 @@
+Tricks: module {
+	PATH:		con "/dis/spree/lib/tricks.dis";
+	init:			fn(mod: Spree, g: ref Clique, cardlibmod: Cardlib);
+
+	Trick: adt {
+		trumps:	int;
+		startcard:	Cardlib->Card;
+		highcard:	Cardlib->Card;
+		winner:	int;
+		pile:		ref Object;
+		hands:	array of ref Object;
+		rank:		array of int;
+
+		new:		fn(pile: ref Object, trumps: int,
+					hands: array of ref Object, rank: array of int): ref Trick;
+		play:		fn(t: self ref Trick, ord, idx: int): string;
+		archive:	fn(t: self ref Trick, archiveobj: ref Object, name: string);
+		unarchive:	fn(archiveobj: ref Object, name: string): ref Trick;
+	};
+
+};
--- /dev/null
+++ b/appl/spree/mkfile
@@ -1,0 +1,66 @@
+<../../mkconfig
+
+ENGINES=\
+	engines/afghan.dis \
+	engines/bounce.dis \
+	engines/canfield.dis \
+	engines/freecell.dis \
+	engines/gather.dis \
+	engines/lobby.dis \
+	engines/othello.dis \
+	engines/racingdemon.dis \
+	engines/spit.dis \
+	engines/spider.dis \
+	engines/whist.dis \
+
+CLIENTS=\
+	clients/cards.dis \
+	clients/gather.dis \
+	clients/lobby.dis \
+	clients/othello.dis \
+
+LIB=\
+	lib/allow.dis \
+	lib/cardlib.dis \
+	lib/commandline.dis \
+	lib/objstore.dis \
+	lib/tricks.dis \
+
+MAIN=\
+	archives.dis \
+	join.dis \
+	spree.dis \
+
+MODULES=\
+	sys.m\
+	draw.m\
+	tk.m\
+	tkclient.m\
+	styx.m\
+	styxservers.m\
+
+DEST=$ROOT/dis/spree
+
+ALL= ${ENGINES:%=$DEST/%} \
+	${CLIENTS:%=$DEST/%} \
+	${LIB:%=$DEST/%} \
+	${MAIN:%=$DEST/%}
+
+all:V: $ENGINES $CLIENTS $LIB $MAIN
+
+install:V:	$ALL
+
+$ROOT/dis/spree/%.dis:	%.dis
+	cp $prereq $target
+
+%.dis:	%.b
+	limbo -gw -I$ROOT/module -Ilib -o $stem.dis $stem.b
+
+$ENGINES $MAIN $LIB: spree.m gather.m lib/cardlib.m lib/allow.m lib/objstore.m
+$ENGINES $MAIN $CLIENTS $LIB: ${MODULES:%=$ROOT/module/%}
+
+clean:V:
+	rm -f *.dis *.sbl */*.dis */*.sbl
+
+nuke:V: clean
+	rm -f $DEST/*.dis $DEST/*/*.dis
--- /dev/null
+++ b/appl/spree/other/tst.b
@@ -1,0 +1,151 @@
+implement Tst;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+Tst: module
+{
+	init:   fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+File: adt {
+	name: string;
+	fd: ref Sys->FD;
+	pid: int;
+};
+
+files: list of ref File;
+
+stderr: ref Sys->FD;
+outputch: chan of chan of string;
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;
+	sys->print(":cardtst\n");
+	stdin := bufio->fopen(sys->fildes(0), Sys->OREAD);
+	line := "";
+	currfd: ref Sys->FD;
+	outputch = chan of chan of string;
+	spawn outputproc();
+	while ((s := stdin.gets('\n')) != nil) {
+		if (len s > 1 && s[len s - 2] == '\\')
+			line += s[0:len s - 2] + "\n";
+		else {
+			s = line + s;
+			line = nil;
+			if (s[0] == ':') {
+				(nil, toks) := sys->tokenize(s, " \n");
+				case hd toks {
+				":open" =>
+					if (tl toks == nil) {
+						sys->fprint(stderr, "usage: open file\n");
+						continue;
+					}
+					f := open(hd tl toks);
+					if (f != nil) {
+						currfd = f.fd;
+						sys->print("current file is now %s\n", f.name);
+					}
+				":close" =>
+					if (tl toks == nil) {
+						sys->fprint(stderr, "usage: close file\n");
+						continue;
+					}
+					fl := files;
+					f: ref File;
+					for (files = nil; fl != nil; fl = tl fl) {
+						if ((hd fl).name == hd tl toks)
+							f = hd fl;
+						else
+							files = hd fl :: files;
+					}
+					if (f == nil) {
+						sys->fprint(stderr, "unknown file '%s'\n", hd tl toks);
+						continue;
+					}
+					sys->fprint(f.fd, "");
+					f = nil;
+				":files" =>
+					for (fl := files; fl != nil; fl = tl fl) {
+						if ((hd fl).fd == currfd)
+							sys->print(":%s <--- current\n", (hd fl).name);
+						else
+							sys->print(":%s\n", (hd fl).name);
+					}
+				* =>
+					for (fl := files; fl != nil; fl = tl fl)
+						if ((hd fl).name == (hd toks)[1:])
+							break;
+					if (fl == nil) {
+						sys->fprint(stderr, "unknown file '%s'\n", (hd toks)[1:]);
+						continue;
+					}
+					currfd = (hd fl).fd;
+				}
+			} else if (currfd == nil)
+				sys->fprint(stderr, "no current file\n");
+			else if (len s > 1 && sys->fprint(currfd, "%s", s[0:len s - 1]) == -1)
+				sys->fprint(stderr, "command failed: %r\n");
+		}
+	}
+	for (fl := files; fl != nil; fl = tl fl)
+		kill((hd fl).pid);
+	outputch <-= nil;
+}
+
+open(f: string): ref File
+{
+	fd := sys->open("/n/remote/" + f, Sys->ORDWR);
+	if (fd == nil) {
+		sys->fprint(stderr, "cannot open %s: %r\n", f);
+		return nil;
+	}
+	sync := chan of int;
+	spawn updateproc(f, fd, sync);
+	files = ref File(f, fd, <-sync) :: files;
+	sys->print("opened %s\n", f);
+	return hd files;
+}
+
+updateproc(name: string, fd: ref Sys->FD,  sync: chan of int)
+{
+	sync <-= sys->pctl(0, nil);
+	c := chan of string;
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(fd, buf, len buf)) > 0) {
+		(nt, toks) := sys->tokenize(string buf[0:n], "\n");
+		outputch <-= c;
+		c <-= "++ " + name + ":\n";
+		for (; toks != nil; toks = tl toks)
+			c <-= sys->sprint("+%s\n", hd toks);
+		c <-= nil;
+	}
+	if (n < 0)
+		sys->fprint(stderr, "cards: error reading %s: %r\n", name);
+	sys->fprint(stderr, "cards: updateproc (%s) exiting\n", name);
+}
+
+outputproc()
+{
+	for (;;) {
+		c := <-outputch;
+		if (c == nil)
+			exit;
+		while ((s := <-c) != nil)
+			sys->print("%s", s);
+	}
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+		sys->write(fd, array of byte "kill", 4);
+}
+
--- /dev/null
+++ b/appl/spree/other/tstboing.b
@@ -1,0 +1,158 @@
+implement Tst;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "sh.m";
+	sh: Sh;
+	Context: import Sh;
+include "math.m";
+	math: Math;
+ZERO: con 1e-6;
+
+stderr: ref Sys->FD;
+
+Tst: module {
+	init: fn(nil: ref Draw->Context, argv: list of string);
+};
+π: con Math->Pi;
+Maxδ: con π / 4.0;
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	math = load Math Math->PATH;
+	if (len argv != 9) {
+		sys->fprint(stderr, "args?\n");
+		exit;
+	}
+	ar := argv2r(tl argv);
+	br := argv2r(tl tl tl tl tl argv);
+
+	a := Line.new(ar.min, ar.max);			# ball
+	b := Line.new(br.min, br.max);			# bat
+	(hit, hitp, s, t) := b.intersection(a.p, a.v);
+	if (hit) {
+		nv := boing(a.v, b);
+		rl := ref Line(hitp, nv, 50.0);
+		ballθ := a.θ();
+		batθ := b.θ();
+		φ := ballθ - batθ;
+		δ: real;
+		if (math->sin(φ) > 0.0)
+			δ = (t / b.s) * Maxδ * 2.0 - Maxδ;
+		else
+			δ = (t / b.s) * -Maxδ * 2.0 + Maxδ;
+		nl := Line.newpolar(rl.p, rl.θ() + δ, rl.s);
+		sys->print("%s %s %s\n", p2s(rl.point(0.0)), p2s(rl.point(rl.s)), p2s(nl.point(nl.s)));
+	} else
+		sys->fprint(stderr, "no hit\n");
+}
+
+argv2r(v: list of string): Rect
+{
+	r: Rect;
+	(r.min.x, v) = (int hd v, tl v);
+	(r.min.y, v) = (int hd v, tl v);
+	(r.max.x, v) = (int hd v, tl v);
+	(r.max.y, v) = (int hd v, tl v);
+	return r;
+}
+Line: adt {
+	p, v:		Realpoint;
+	s:		real;
+	new:			fn(p1, p2: Point): ref Line;
+	hittest:		fn(l: self ref Line, p: Point): (Realpoint, real, real);
+	intersection:	fn(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real);
+	point:		fn(b: self ref Line, s: real): Point;
+	θ:			fn(b: self ref Line): real;
+	newpolar:		fn(p: Realpoint, θ: real, s: real): ref Line;
+};
+
+Realpoint: adt {
+	x, y: real;
+};
+
+Line.new(p1, p2: Point): ref Line
+{
+	ln := ref Line;
+	ln.p = (real p1.x, real p1.y);
+	v := Realpoint(real (p2.x - p1.x), real (p2.y - p1.y));
+	ln.s =  math->sqrt(v.x * v.x + v.y * v.y);
+	if (ln.s > ZERO)
+		ln.v = (v.x / ln.s, v.y / ln.s);
+	else
+		ln.v = (1.0, 0.0);
+	return ln;
+}
+
+Line.newpolar(p: Realpoint, θ: real, s: real): ref Line
+{
+	l := ref Line;
+	l.p = p;
+	l.s = s;
+	l.v = (math->cos(θ), math->sin(θ));
+	return l;
+}
+
+Line.θ(l: self ref Line): real
+{
+	return math->atan2(l.v.y, l.v.x);
+}
+
+# return normal from line, perpendicular distance from line and distance down line
+Line.hittest(l: self ref Line, ip: Point): (Realpoint, real, real)
+{
+	p := Realpoint(real ip.x, real ip.y);
+	v := Realpoint(-l.v.y, l.v.x);
+	(nil, nil, perp, ldist) := l.intersection(p, v);
+	return (v, perp, ldist);
+}
+
+Line.point(l: self ref Line, s: real): Point
+{
+	return (int (l.p.x + s * l.v.x), int (l.p.y + s * l.v.y));
+}
+
+# compute the intersection of lines a and b.
+# b is assumed to be fixed, and a is indefinitely long
+# but doesn't extend backwards from its starting point.
+# a is defined by the starting point p and the unit vector v.
+# return whether it hit, the point at which it hit if so,
+# the distance of the intersection point from p,
+# and the distance of the intersection point from b.p.
+Line.intersection(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real)
+{
+	det := b.v.x * v.y - v.x * b.v.y;
+	if (det > -ZERO && det < ZERO)
+		return (0, (0.0, 0.0), 0.0, 0.0);
+
+	y21 := b.p.y - p.y;
+	x21 := b.p.x - p.x;
+	s := (b.v.x * y21 - b.v.y * x21) / det;
+	t := (v.x * y21 - v.y * x21) / det;
+	if (s < 0.0)
+		return (0, (0.0, 0.0), s, t);
+	hit := t >= 0.0 && t <= b.s;
+	hp: Realpoint;
+	if (hit)
+		hp = (p.x+v.x*s, p.y+v.y*s);
+	return (hit, hp, s, t);
+}
+
+# bounce ball travelling in direction av off line b.
+# return the new unit vector.
+boing(av: Realpoint, b: ref Line): Realpoint
+{
+	d := math->atan2(real b.v.y, real b.v.x) * 2.0 - math->atan2(av.y, av.x);
+	return (math->cos(d), math->sin(d));
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
--- /dev/null
+++ b/appl/spree/other/tstlines.sh
@@ -1,0 +1,53 @@
+#!/dis/sh
+load tk std
+pctl newpgrp
+wid=${tk window 'Test lines'}
+fn x {tk $wid $*}
+x canvas .c
+x pack .c
+x 'bind .c <ButtonRelease-1> {send b1 %x %y}'
+x 'bind .c <ButtonRelease-2> {send b2 %x %y}'
+x update
+chan b1 b2
+tk namechan $wid b1
+tk namechan $wid b2
+while {} {tk winctl $wid ${recv $wid}} &
+chan show
+ifs=' 	
+'
+v1 := 0 0 1 1
+v2 := 1 1 2 2
+while {} {
+	args:=${split ${recv show}}
+	(t args) = $args
+	$t = $args
+
+	tk 0 .c delete lines
+	echo $v1 $v2
+	r := `{tstboing $v1 $v2}
+	(ap1x ap1y ap2x ap2y bp1x bp1y bp2x bp2y) := $v1 $v2
+	tk 0 .c create line $ap1x $ap1y $ap2x $ap2y -tags lines -fill black -width 3 -arrow last
+	tk 0 .c create line $bp1x $bp1y $bp2x $bp2y -tags lines -fill red
+	and {~ $#r 6} {
+		(rp1x rp1y rp2x rp2y sp2x sp2y) := $r
+		tk 0 .c create line $ap2x $ap2y $rp1x $rp1y -tags lines -fill black
+		tk 0 .c create line $rp1x $rp1y $rp2x $rp2y -tags lines -fill green -arrow last
+		tk 0 .c create line $rp1x $rp1y $sp2x $sp2y -tags lines -fill blue -arrow last
+	}
+	tk 0 update
+} &
+
+fn show {
+	a:=$*
+	if {~ $#a 8} {echo usage} {
+		send show ${join ' ' $a}
+	}
+}
+
+for i in 1 2 {
+	while {} {
+		p1:=${recv b^$i}
+		p2:=${recv b^$i}
+		send show ${join ' ' v^$i $p1 $p2}
+	} &
+}
--- /dev/null
+++ b/appl/spree/other/tstwin.b
@@ -1,0 +1,351 @@
+implement Tstwin;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context, Display, Point, Rect, Image, Screen: import draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "math.m";
+	math: Math;
+
+Tstwin: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+screen: ref Screen;
+display: ref Display;
+win: ref Toplevel;
+
+NC: con 6;
+
+task_cfg := array[] of {
+"label .xy -text {0 0}",
+"canvas .c -height 500 -width 500",
+"pack .xy -side top -fill x",
+"pack .c -side bottom -fill both -expand 1",
+"bind .c <ButtonRelease-1> {send cmd 0 1 %x %y}",
+"bind .c <ButtonRelease-2> {send cmd 0 2 %x %y}",
+"bind .c <Button-1> {send cmd 1 1 %x %y}",
+"bind .c <Button-2> {send cmd 1 2 %x %y}",
+};
+
+Obstacle: adt {
+	line: 		ref Line;
+	s1, s2: 	real;
+	id:		int;
+	config: 	fn(b: self ref Obstacle);
+	new:		fn(id: int): ref Obstacle;
+};
+
+Line: adt {
+	p, v:		Realpoint;
+	s:		real;
+	new:			fn(p1, p2: Point): ref Line;
+	hittest:		fn(l: self ref Line, p: Point): (Realpoint, real, real);
+	intersection:	fn(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real);
+	point:		fn(b: self ref Line, s: real): Point;
+};
+bats: list of ref Obstacle;
+init(ctxt: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	math = load Math Math->PATH;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	display = ctxt.display;
+	screen = ctxt.screen;
+
+	tkclient->init();
+
+	menubut: chan of string;
+	(win, menubut) = tkclient->toplevel(screen, nil, "Window testing", 0);
+
+	cmd := chan of string;
+	tk->namechan(win, cmd, "cmd");
+
+	tkclient->tkcmds(win, task_cfg);
+
+	mch := chan of (int, Point);
+	spawn mouseproc(mch);
+
+	bat := Obstacle.new(0);
+	bats = bat :: nil;
+	bat.line = Line.new((100, 0), (150, 500));
+	bat.s1 = 10.0;
+	bat.s2 = 110.0;
+	bat.config();
+
+	tk->cmd(win, "update");
+	buts := 0;
+	for(;;) alt {
+	menu := <-menubut =>
+		tkclient->wmctl(win, menu);
+
+	c := <-cmd =>
+		(nil, toks) := sys->tokenize(c, " ");
+		if ((hd toks)[0] == '1')
+			buts |= int hd tl toks;
+		else
+			buts &= ~int hd tl toks;
+		mch <-= (buts, Point(int hd tl tl toks, int hd tl tl tl toks));
+	}
+}
+
+Realpoint: adt {
+	x, y: real;
+};
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->print("tk error %s on '%s'\n", e, s);
+	return e;
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+mouseproc(mch: chan of (int, Point))
+{
+	for (;;) {
+		hitbat: ref Obstacle = nil;
+		minperp, hitdist: real;
+		(buts, p) := <-mch;
+		for (bl := bats; bl != nil; bl = tl bl) {
+			b := hd bl;
+			(normal, perp, dist) := b.line.hittest(p);
+			perp = abs(perp);
+			
+			if ((hitbat == nil || perp < minperp) && (dist >= b.s1 && dist <= b.s2))
+				(hitbat, minperp, hitdist) = (b, perp, dist);
+		}
+		if (hitbat == nil || minperp > 30.0) {
+			while ((<-mch).t0)
+				;
+			continue;
+		}
+		offset := hitdist - hitbat.s1;
+		if (buts & 2)
+			(buts, p) = aim(mch, hitbat, p);
+		if (buts & 1)
+			drag(mch, hitbat, offset);
+	}
+}
+
+
+drag(mch: chan of (int, Point), hitbat: ref Obstacle, offset: real)
+{
+	line := hitbat.line;
+	batlen := hitbat.s2 - hitbat.s1;
+
+	cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+
+#	cmd(win, "grab set .c");
+#	cmd(win, "focus .");
+loop:	for (;;) alt {
+	(buts, p) := <-mch =>
+		if (buts & 2)
+			(buts, p) = aim(mch, hitbat, p);
+		(v, perp, dist) := line.hittest(p);
+		dist -= offset;
+		# constrain bat and mouse positions
+		if (dist < 0.0 || dist + batlen > line.s) {
+			if (dist < 0.0) {
+				p = line.point(offset);
+				dist = 1.0;
+			} else {
+				p = line.point(line.s - batlen + offset);
+				dist = line.s - batlen;
+			}
+			p.x -= int (v.x * perp);
+			p.y -= int (v.y * perp);
+			win.image.display.cursorset(p.add(cvsorigin));
+		}
+		(hitbat.s1, hitbat.s2) = (dist, dist + batlen);
+		hitbat.config();
+		cmd(win, "update");
+		if (!buts)
+			break loop;
+	}
+#	cmd(win, "grab release .c");
+}
+
+CHARGETIME: con 1000.0;
+MAXCHARGE: con 50.0;
+
+α: con 0.999;		# decay in one millisecond
+Max: con 60.0;
+D: con 5;
+ZERO: con 1e-6;
+aim(mch: chan of (int, Point), hitbat: ref Obstacle, p: Point): (int, Point)
+{
+	cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+	startms := ms := sys->millisec();
+	delta := Realpoint(0.0, 0.0);
+	line := hitbat.line;
+	charge := 0.0;
+	pivot := line.point((hitbat.s1 + hitbat.s2) / 2.0);
+	s1 := p2s(line.point(hitbat.s1));
+	s2 := p2s(line.point(hitbat.s2));
+	cmd(win, ".c create line 0 0 0 0 -tags wire");
+	cmd(win, ".c create oval 0 0 1 1 -fill green -tags ball");
+	p2: Point;
+	buts := 2;
+	for (;;) {
+		v := makeunit(delta);
+		bp := pivot.add((int (v.x * charge), int (v.y * charge)));
+		cmd(win, ".c coords wire "+s1+" "+p2s(bp)+" "+s2);
+		cmd(win, ".c coords ball "+string (bp.x - D) + " " + string (bp.y - D) + " " +
+					string (bp.x + D) + " " + string (bp.y + D));
+		cmd(win, "update");
+		if ((buts & 2) == 0)
+			break;
+		(buts, p2) = <-mch;
+		now := sys->millisec();
+		fade := math->pow(α, real (now - ms));
+		charge = real (now - startms) * (MAXCHARGE / CHARGETIME);
+		if (charge > MAXCHARGE)
+			charge = MAXCHARGE;
+		ms = now;
+		delta.x = delta.x * fade + real (p2.x - p.x);
+		delta.y = delta.y * fade + real (p2.y - p.y);
+		mag := delta.x * delta.x + delta.y * delta.y;
+		win.image.display.cursorset(p.add(cvsorigin));
+	}
+	sys->print("pow\n");
+	cmd(win, ".c delete wire ball");
+	cmd(win, "update");
+	return (buts, p2);
+}
+
+makeunit(v: Realpoint): Realpoint
+{
+	mag := math->sqrt(v.x * v.x + v.y * v.y);
+	if (mag < ZERO)
+		return (1.0, 0.0);
+	return (v.x / mag, v.y / mag);
+}
+
+#drag(mch: chan of (int, Point), p: Point)
+#{
+#	down := 1;
+#	cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+#	ms := sys->millisec();
+#	delta := Realpoint(0.0, 0.0);
+#	id := cmd(win, ".c create line " + p2s(p) + " " + p2s(p));
+#	coords := ".c coords " + id + " " + p2s(p) + " ";
+#	do {
+#		p2: Point;
+#		(down, p2) = <-mch;
+#		now := sys->millisec();
+#		fade := math->pow(α, real (now - ms));
+#		ms = now;
+#		delta.x = delta.x * fade + real (p2.x - p.x);
+#		delta.y = delta.y * fade + real (p2.y - p.y);
+#		mag := delta.x * delta.x + delta.y * delta.y;
+#		d: Realpoint;
+#		if (mag > Max * Max) {
+#			fade = Max / math->sqrt(mag);
+#			d  = (delta.x * fade, delta.y * fade);
+#		} else
+#			d = delta;
+#		
+#		cmd(win, coords + p2s(p.add((int d.x, int d.y))));
+#		win.image.display.cursorset(p.add(cvsorigin));
+#		cmd(win, "update");
+#	} while (down);
+#}
+#
+Line.new(p1, p2: Point): ref Line
+{
+	ln := ref Line;
+	ln.p = (real p1.x, real p1.y);
+	v := Realpoint(real (p2.x - p1.x), real (p2.y - p1.y));
+	ln.s =  math->sqrt(v.x * v.x + v.y * v.y);
+	if (ln.s > ZERO)
+		ln.v = (v.x / ln.s, v.y / ln.s);
+	else
+		ln.v = (1.0, 0.0);
+	return ln;
+}
+
+# return normal from line, perpendicular distance from line and distance down line
+Line.hittest(l: self ref Line, ip: Point): (Realpoint, real, real)
+{
+	p := Realpoint(real ip.x, real ip.y);
+	v := Realpoint(-l.v.y, l.v.x);
+	(nil, nil, perp, ldist) := l.intersection(p, v);
+	return (v, perp, ldist);
+}
+
+Line.point(l: self ref Line, s: real): Point
+{
+	return (int (l.p.x + s * l.v.x), int (l.p.y + s * l.v.y));
+}
+
+# compute the intersection of lines a and b.
+# b is assumed to be fixed, and a is indefinitely long
+# but doesn't extend backwards from its starting point.
+# a is defined by the starting point p and the unit vector v.
+# return whether it hit, the point at which it hit if so,
+# the distance of the intersection point from p,
+# and the distance of the intersection point from b.p.
+Line.intersection(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real)
+{
+	det := b.v.x * v.y - v.x * b.v.y;
+	if (det > -ZERO && det < ZERO)
+		return (0, (0.0, 0.0), 0.0, 0.0);
+
+	y21 := b.p.y - p.y;
+	x21 := b.p.x - p.x;
+	s := (b.v.x * y21 - b.v.y * x21) / det;
+	t := (v.x * y21 - v.y * x21) / det;
+	if (s < 0.0)
+		return (0, (0.0, 0.0), s, t);
+	hit := t >= 0.0 && t <= b.s;
+	hp: Realpoint;
+	if (hit)
+		hp = (p.x+v.x*s, p.y+v.y*s);
+	return (hit, hp, s, t);
+}
+
+blankobstacle: Obstacle;
+Obstacle.new(id: int): ref Obstacle
+{
+	cmd(win, ".c create line 0 0 0 0 -width 3 -fill #aaaaaa" + " -tags l" + string id);
+	o := ref blankobstacle;
+	o.line = Line.new((0, 0), (0, 0));
+	o.id = id;
+	return o;
+}
+
+Obstacle.config(o: self ref Obstacle)
+{
+	cmd(win, ".c coords l" + string o.id + " " +
+		p2s(o.line.point(o.s1)) + " " + p2s(o.line.point(o.s2)));
+	cmd(win, ".c itemconfigure l" + string o.id + " -fill red");
+}
+
+abs(x: real): real
+{
+	if (x < 0.0)
+		return -x;
+	return x;
+}
--- /dev/null
+++ b/appl/spree/spree.b
@@ -1,0 +1,1554 @@
+implement Spree;
+
+include "sys.m";
+	sys: Sys;
+include "readdir.m";
+	readdir: Readdir;
+include "styx.m";
+	Rmsg, Tmsg: import Styx;
+include "styxservers.m";
+	styxservers: Styxservers;
+	Styxserver, Fid, Eperm, Navigator: import styxservers;
+	nametree: Nametree;
+include "draw.m";
+include "arg.m";
+include "sets.m";
+	sets: Sets;
+	Set, set, A, B, All, None: import sets;
+include "spree.m";
+	archives: Archives;
+	Archive: import archives;
+
+stderr: ref Sys->FD;
+myself: Spree;
+
+Debug: con 0;
+Update: adt {
+	pick {
+	Set =>
+		o:		ref Object;
+		objid:	int;			# member-specific id
+		attr:		ref Attribute;
+	Transfer =>
+		srcid:	int;			# parent object
+		from:	Range;		# range within src to transfer
+		dstid:	int;			# destination object
+		index:	int;			# insertion point
+	Create =>
+		objid:	int;
+		parentid:	int;
+		visibility:	Sets->Set;
+		objtype:	string;
+	Delete =>
+		parentid:	int;
+		r:		Range;
+		objs:		array of int;
+	Setvisibility =>
+		objid:	int;
+		visibility:	Sets->Set;		# set of members that can see it
+	Action =>
+		s:		string;
+		objs:		list of int;
+		rest:		string;
+	Break =>
+		# break in transmission
+	}
+};
+
+T: type ref Update;
+Queue: adt {
+	h, t: list of T; 
+	put: fn(q: self ref Queue, s: T);
+	get: fn(q: self ref Queue): T;
+	isempty: fn(q: self ref Queue): int;
+	peek: fn(q: self ref Queue): T;
+};
+
+Openfid: adt {
+	fid:		int;
+	uname:	string;
+	fileid:	int;
+	member:	ref Member;		# nil for non-clique files.
+	updateq:	ref Queue;
+	readreq:	ref Tmsg.Read;
+	hungup:	int;
+	# alias:	string;		# could use this to allow a member to play themselves
+
+	new:		fn(fid: ref Fid, file: ref Qfile): ref Openfid;
+	find:		fn(fid: int): ref Openfid;
+	close:	fn(fid: self ref Openfid);
+#	cmd:		fn(fid: self ref Openfid, cmd: string): string;
+};
+
+Qfile: adt {
+	id:		int;				# index into files array
+	owner:	string;
+	qid:		Sys->Qid;
+	ofids:	list of ref Openfid;		# list of all fids that are holding this open
+	needsupdate:	int;			# updates have been added since last updateall
+
+	create:	fn(parent: big, d: Sys->Dir): ref Qfile;
+	delete:	fn(f: self ref Qfile);
+};
+
+# which updates do we send even though the clique isn't yet started?
+alwayssend := array[] of {
+	tagof(Update.Set) => 0,
+	tagof(Update.Transfer) => 0,
+	tagof(Update.Create) => 0,
+	tagof(Update.Delete) => 0,
+	tagof(Update.Setvisibility) => 0,
+	tagof(Update.Action) => 1,
+	tagof(Update.Break) => 1,
+};
+
+srv:		ref Styxserver;
+tree:		ref Nametree->Tree;
+cliques:	array of ref Clique;
+qfiles:	array of ref Qfile;
+fids :=	array[47] of list of ref Openfid;	# hash table
+lobby:	ref Clique;
+Qroot:	big;
+sequence := 0;
+
+fROOT,
+fGAME,
+fNAME,
+fGAMEDIR,
+fGAMEDATA: con iota;
+
+GAMEDIR: con "/n/remote";
+ENGINES: con "/dis/spree/engines";
+ARCHIVEDIR: con "/lib/spreearchive";
+
+badmod(p: string)
+{
+	sys->fprint(stderr, "spree: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	myself = load Spree "$self";
+
+	styx := load Styx Styx->PATH;
+	if (styx == nil)
+		badmod(Styx->PATH);
+	styx->init();
+
+	styxservers = load Styxservers Styxservers->PATH;
+	if (styxservers == nil)
+		badmod(Styxservers->PATH);
+	styxservers->init(styx);
+ 
+	nametree = load Nametree Nametree->PATH;
+	if (nametree == nil)
+		badmod(Nametree->PATH);
+	nametree->init();
+
+	sets = load Sets Sets->PATH;
+	if (sets == nil)
+		badmod(Sets->PATH);
+	sets->init();
+
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil)
+		badmod(Readdir->PATH);
+
+	archives = load Archives Archives->PATH;
+	if (archives == nil)
+		badmod(Archives->PATH);
+	archives->init(myself);
+
+	initrand();
+
+	navop: chan of ref Styxservers->Navop;
+	(tree, navop) = nametree->start();
+	tchan: chan of ref Tmsg;
+	Qroot = mkqid(fROOT, 0);
+	(tchan, srv) = Styxserver.new(sys->fildes(0), Navigator.new(navop), Qroot);
+	nametree->tree.create(Qroot, dir(Qroot, ".", 8r555|Sys->DMDIR, "spree"));
+	nametree->tree.create(Qroot, dir(mkqid(fNAME, 0), "name", 8r444, "spree"));
+	(lobbyid, nil, err) := lobby.new(ref Archive("lobby" :: nil, nil, nil, nil), "spree");
+	if (lobbyid == -1) {
+		sys->fprint(stderr, "spree: couldn't start lobby: %s\n", err);
+		raise "fail:no lobby";
+	}
+	sys->pctl(Sys->FORKNS, nil);
+	for (;;) {
+		gm := <-tchan;
+		if (gm == nil || tagof(gm) == tagof(Tmsg.Readerror)) {
+			if (gm != nil) {
+				pick m := gm {
+				Readerror =>
+					sys->print("spree: read error: %s\n", m.error);
+				}
+			}
+			sys->print("spree: exiting\n");
+			exit;
+		} else {
+			e := handletmsg(gm);
+			if (e != nil)
+				srv.reply(ref Rmsg.Error(gm.tag, e));
+		}
+	}
+}
+
+
+dir(qidpath: big, name: string, perm: int, owner: string): Sys->Dir
+{
+	DM2QT: con 24;
+	d := Sys->zerodir;
+	d.name = name;
+	d.uid = owner;
+	d.gid = owner;
+	d.qid.path = qidpath;
+	d.qid.qtype = (perm >> DM2QT) & 16rff;
+	d.mode = perm;
+	# d.atime = now;
+	# d.mtime = now;
+	return d;
+}
+
+handletmsg(tmsg: ref Tmsg): string
+{
+	pick m := tmsg {
+	Open =>
+		(fid, omode, d, err) := srv.canopen(m);
+		if (fid == nil)
+			return err;
+		if (d.qid.qtype & Sys->QTDIR) {
+			srv.default(m);
+			return nil;
+		}
+		case qidkind(d.qid.path) {
+		fGAMEDATA =>
+			fid.open(m.mode, Sys->Qid(fid.path, fid.qtype, 0));
+			srv.reply(ref Rmsg.Open(m.tag, Sys->Qid(fid.path, fid.qtype, 0), 0));
+		fGAME =>
+			f := qid2file(d.qid.path);
+			if (f == nil)
+				return "cannot find qid";
+			ofid := Openfid.new(fid, f);
+			err = openfile(ofid);
+			if (err != nil) {
+				ofid.close();
+				return err;
+			}
+			fid.open(m.mode, f.qid);
+			srv.reply(ref Rmsg.Open(m.tag, Sys->Qid(fid.path, fid.qtype, 0), 0));
+		* =>
+			srv.default(m);
+		}
+		updateall();
+	Read =>
+		(fid, err) := srv.canread(m);
+		if (fid == nil)
+			return err;
+		if (fid.qtype & Sys->QTDIR) {
+			srv.default(m);
+			return nil;
+		}
+		case qidkind(fid.path) {
+		fGAMEDATA =>
+			f := qidindex(fid.path);
+			id := f & 16rffff;
+			f = (f >> 16) & 16rffff;
+			data := cliques[id].mod->readfile(f, m.offset, m.count);
+			srv.reply(ref Rmsg.Read(m.tag, data));
+		fGAME =>
+			ff := Openfid.find(m.fid);
+			if (ff.readreq != nil)
+				return "duplicate read";
+			ff.readreq = m;
+			sendupdate(ff);
+		fNAME =>
+			srv.reply(styxservers->readstr(m, fid.uname));
+		* =>
+			return "darn rats!";
+		}
+	Write =>
+		(fid, err) := srv.canwrite(m);
+		if (fid == nil)
+			return err;
+		ff := Openfid.find(m.fid);
+		err = command(ff, string m.data);
+		if (err != nil) {
+			updateall();
+			return err;
+		}
+		srv.reply(ref Rmsg.Write(m.tag, len m.data));
+		updateall();		# XXX might we need to do this on error too?
+	Clunk =>
+		fid := srv.clunk(m);
+		if (fid != nil) {
+			clunked(fid);
+			updateall();
+		}
+	Flush =>
+		for (i := 0; i < len qfiles; i++) {
+			if (qfiles[i] == nil)
+				continue;
+			for (ol := qfiles[i].ofids; ol != nil; ol = tl ol) {
+				ofid := hd ol;
+				if (ofid.readreq != nil && ofid.readreq.tag == m.oldtag)
+					ofid.readreq = nil;
+			}
+		}
+		srv.reply(ref Rmsg.Flush(m.tag));
+# Removed => clunked too.
+	* =>
+		srv.default(tmsg);
+	}
+	return nil;
+}
+
+clunked(fid: ref Fid)
+{
+	if (!fid.isopen || (fid.qtype & Sys->QTDIR))
+		return;
+	ofid := Openfid.find(fid.fid);
+	if (ofid == nil)
+		return;
+	if (ofid.member != nil)
+		memberleaves(ofid.member);
+	ofid.close();
+	f := qfiles[ofid.fileid];
+	# if it's the last close, and clique is hung up, then remove clique from
+	# directory hierarchy.
+	if (f.ofids == nil && qidkind(f.qid.path) == fGAME) {
+		g := cliques[qidindex(f.qid.path)];
+		if (g.hungup) {
+			stopclique(g);
+			nametree->tree.remove(mkqid(fGAMEDIR, g.id));
+			f.delete();
+			cliques[g.id] = nil;
+		}
+	}
+}
+
+mkqid(kind, i: int): big
+{
+	return big kind | (big i << 4);
+}
+
+qidkind(qid: big): int
+{
+	return int (qid & big 16rf);
+}
+
+qidindex(qid: big): int
+{
+	return int (qid >> 4);
+}
+
+qid2file(qid: big): ref Qfile
+{
+	for (i := 0; i < len qfiles; i++) {
+		f := qfiles[i];
+		if (f != nil && f.qid.path == qid)
+			return f;
+	}
+	return nil;
+}
+
+Qfile.create(parent: big, d: Sys->Dir): ref Qfile
+{
+	nametree->tree.create(parent, d);
+	for (i := 0; i < len qfiles; i++)
+		if (qfiles[i] == nil)
+			break;
+	if (i == len qfiles)
+		qfiles = (array[len qfiles + 1] of ref Qfile)[0:] = qfiles;
+	f := qfiles[i] = ref Qfile(i, d.uid, d.qid, nil, 0);
+	return f;
+}
+
+Qfile.delete(f: self ref Qfile)
+{
+	nametree->tree.remove(f.qid.path);
+	qfiles[f.id] = nil;
+}
+
+Openfid.new(fid: ref Fid, file: ref Qfile): ref Openfid
+{
+	i := fid.fid % len fids;
+	ofid := ref Openfid(fid.fid, fid.uname, file.id, nil, ref Queue, nil, 0);
+	fids[i] = ofid :: fids[i];
+	file.ofids = ofid :: file.ofids;
+	return ofid;
+}
+
+Openfid.find(fid: int): ref Openfid
+{
+	for (ol := fids[fid % len fids]; ol != nil; ol = tl ol)
+		if ((hd ol).fid == fid)
+			return hd ol;
+	return nil;
+}
+	
+Openfid.close(ofid: self ref Openfid)
+{
+	i := ofid.fid % len fids;
+	newol: list of ref Openfid;
+	for (ol := fids[i]; ol != nil; ol = tl ol)
+		if (hd ol != ofid)
+			newol = hd ol :: newol;
+	fids[i] = newol;
+	newol = nil;
+	for (ol = qfiles[ofid.fileid].ofids; ol != nil; ol = tl ol)
+		if (hd ol != ofid)
+			newol = hd ol :: newol;
+	qfiles[ofid.fileid].ofids = newol;
+}
+
+openfile(ofid: ref Openfid): string
+{
+	name := ofid.uname;
+	f := qfiles[ofid.fileid];
+	if (qidkind(f.qid.path) == fGAME) {
+		if (cliques[qidindex(f.qid.path)].hungup)
+			return "hungup";
+		i := 0;
+		for (o := f.ofids; o != nil; o = tl o) {
+			if ((hd o) != ofid && (hd o).uname == name)
+				return "you cannot join a clique twice";
+			i++;
+		}
+		if (i > MAXPLAYERS)
+			return "too many members";
+	}
+	return nil;
+}
+
+# process a client's command; return a non-nil string on error.
+command(ofid: ref Openfid, cmd: string): string
+{
+	err: string;
+	f := qfiles[ofid.fileid];
+	qid := f.qid.path;
+	if (ofid.hungup)
+		return "hung up";
+	if (cmd == nil) {
+		ofid.hungup = 1;
+		sys->print("hanging up file %s for user %s, fid %d\n", nametree->tree.getpath(f.qid.path), ofid.uname, ofid.fid);
+		return nil;
+	}
+	case qidkind(qid) {
+	fGAME =>
+		clique := cliques[qidindex(qid)];
+		if (ofid.member == nil)
+			err = newmember(clique, ofid, cmd);
+		else
+			err = cliquerequest(clique, ref Rq.Command(ofid.member, cmd));
+	* =>
+		err = "invalid command " + string qid;		# XXX dud error message
+	}
+	return err;
+}
+
+Clique.notify(src: self ref Clique, dstid: int, cmd: string)
+{
+	if (cmd == nil)
+		return;		# don't allow faking of clique exit.
+	if (dstid < 0 || dstid >= len cliques) {
+		if (dstid != -1)
+			sys->fprint(stderr, "%d cannot notify invalid %d: '%s'\n", src.id, dstid, cmd);
+		return;
+	}
+	dst := cliques[dstid];
+	if (dst.parentid != src.id && dstid != src.parentid) {
+		sys->fprint(stderr, "%d cannot notify %d: '%s'\n", src.id, dstid, cmd);
+		return;
+	}
+	src.notes = (src.id, dstid, cmd) :: src.notes;
+}
+
+# add a new member to a clique.
+# it should already have been checked that the member's name
+# isn't a duplicate of another in the same clique.
+newmember(clique: ref Clique, ofid: ref Openfid, cmd: string): string
+{
+	name := ofid.uname;
+
+	# check if member was suspended, and give them their old id back
+	# if so, otherwise find first free id.
+	for (s := clique.suspended; s != nil; s = tl s)
+		if ((hd s).name == name)
+			break;
+	id: int;
+	suspended := 0;
+	member: ref Member;
+	if (s != nil) {
+		member = hd s;
+		# remove from suspended list
+		q := tl s;
+		for (t := clique.suspended; t != s; t = tl t)
+			q = hd t :: q;
+		clique.suspended = q;
+		suspended = 1;
+		member.suspended = 0;
+	} else {
+		for (id = 0; clique.memberids.holds(id); id++)
+			;
+		member = ref Member(id, clique.id, nil, nil, nil, name, 0, 0);
+		clique.memberids = clique.memberids.add(member.id);
+	}
+
+	q := ofid.updateq;
+	ofid.member = member;
+
+	started := clique.started;
+	err := cliquerequest(clique, ref Rq.Join(member, cmd, suspended));
+	if (err != nil) {
+		member.del(0);
+		if (suspended) {
+			member.suspended = 1;
+			clique.suspended = member :: clique.suspended;
+		}
+		return err;
+	}
+	if (started) {
+		qrecreateobject(q, member, clique.objects[0], nil);
+		qfiles[ofid.fileid].needsupdate = 1;
+	}
+	member.updating = 1;
+	return nil;
+}
+
+Clique.start(clique: self ref Clique)
+{
+	if (clique.started)
+		return;
+
+	for (ol := qfiles[clique.fileid].ofids; ol != nil; ol = tl ol)
+		if ((hd ol).member != nil)
+			qrecreateobject((hd ol).updateq, (hd ol).member, clique.objects[0], nil);
+	clique.started = 1;
+}
+
+Blankclique: Clique;
+maxcliqueid := 0;
+Clique.new(parent: self ref Clique, archive: ref Archive, owner: string): (int, string, string)
+{
+	for (id := 0; id < len cliques; id++)
+		if (cliques[id] == nil)
+			break;
+	if (id == len cliques)
+		cliques = (array[len cliques + 1] of ref Clique)[0:] = cliques;
+
+	mod := load Engine ENGINES +"/" + hd archive.argv + ".dis";
+	if (mod == nil)
+		return (-1, nil, sys->sprint("cannot load engine: %r"));
+
+	dirq := mkqid(fGAMEDIR, id);
+	fname := string maxcliqueid++;
+	e := nametree->tree.create(Qroot, dir(dirq, fname, 8r555|Sys->DMDIR, owner));
+	if (e != nil)
+		return (-1, nil, e);
+	f := Qfile.create(dirq, dir(mkqid(fGAME, id), "ctl", 8r666, owner));
+	objs: array of ref Object;
+	if (archive.objects != nil) {
+		objs = archive.objects;
+		for (i := 0; i < len objs; i++)
+			objs[i].cliqueid = id;
+	} else
+		objs = array[] of {ref Object(0, Attributes.new(), All, -1, nil, id, nil)};
+
+	memberids := None;
+	suspended: list of ref Member;
+	for (i := 0; i < len archive.members; i++) {
+		suspended = ref Member(i, id, nil, nil, nil, archive.members[i], 0, 1) :: suspended;
+		memberids = memberids.add(i);
+	}
+
+	archive = ref *archive;
+	archive.objects = nil;
+
+	g := cliques[id] = ref Clique(
+		id,			# id
+		f.id,			# fileid
+		fname,		# fname
+		objs,			# objects
+		archive,		# archive
+		nil,			# freelist
+		mod,		# mod
+		memberids,		# memberids
+		suspended,
+		chan of ref Rq,	# request
+		chan of string,	# reply
+		0,			# hungup
+		0,			# started
+		-1,			# parentid
+		nil			# notes
+	);
+	if (parent != nil) {
+		g.parentid = parent.id;
+		g.notes = parent.notes;
+	}
+	spawn cliqueproc(g);
+	e = cliquerequest1(g, ref Rq.Init);
+	if (e != nil) {
+		stopclique(g);
+		nametree->tree.remove(dirq);
+		f.delete();
+		cliques[id] = nil;
+		return (-1, nil, e);
+	}
+	# only send notifications if the clique was successfully created, otherwise
+	# pretend it never existed.
+	if (parent != nil) {
+		parent.notes = g.notes;
+		g.notes = nil;
+	}
+	return (g.id, fname, nil);
+}
+
+# as a special case, if parent is nil, we use the root object.
+Clique.newobject(clique: self ref Clique, parent: ref Object, visibility: Set, objtype: string): ref Object
+{
+	if (clique.freelist == nil)
+		(clique.objects, clique.freelist) =
+			makespace(clique.objects, clique.freelist);
+	id := hd clique.freelist;
+	clique.freelist = tl clique.freelist;
+
+	if (parent == nil)
+		parent = clique.objects[0];
+	obj := ref Object(id, Attributes.new(), visibility, parent.id, nil, clique.id, objtype);
+
+	n := len parent.children;
+	newchildren := array[n + 1] of ref Object;
+	newchildren[0:] = parent.children;
+	newchildren[n] = obj;
+	parent.children = newchildren;
+	clique.objects[id] = obj;
+	applycliqueupdate(clique, ref Update.Create(id, parent.id, visibility, objtype), All);
+	if (Debug)
+		sys->print("new %d, parent %d, visibility %s\n", obj.id, parent.id, visibility.str());
+	return obj;
+}
+
+Clique.hangup(clique: self ref Clique)
+{
+	if (clique.hungup)
+		return;
+sys->print("clique.hangup(%s)\n", clique.fname);
+	f := qfiles[clique.fileid];
+	for (ofids := f.ofids; ofids != nil; ofids = tl ofids)
+		(hd ofids).hungup = 1;
+	f.needsupdate = 1;
+	clique.hungup = 1;
+	if (clique.parentid != -1) {
+		clique.notes = (clique.id, clique.parentid, nil) :: clique.notes;
+		clique.parentid = -1;
+	}
+	# orphan children
+	# XXX could be more efficient for childless cliques by keeping child count
+	for(i := 0; i < len cliques; i++)
+		if (cliques[i] != nil && cliques[i].parentid == clique.id)
+			cliques[i].parentid = -1;
+}
+
+stopclique(clique: ref Clique)
+{
+	clique.hangup();
+	if (clique.request != nil)
+		clique.request <-= nil;
+}
+
+Clique.breakmsg(clique: self ref Clique, whoto: Set)
+{	
+	applycliqueupdate(clique, ref Update.Break, whoto);
+}
+
+Clique.action(clique: self ref Clique, cmd: string,
+			objs: list of int, rest: string, whoto: Set)
+{
+	applycliqueupdate(clique, ref Update.Action(cmd, objs, rest), whoto);
+}
+
+Clique.member(clique: self ref Clique, id: int): ref Member
+{
+	for (ol := qfiles[clique.fileid].ofids; ol != nil; ol = tl ol)
+		if ((hd ol).member != nil && (hd ol).member.id == id)
+			return (hd ol).member;
+	for (s := clique.suspended; s != nil; s = tl s)
+		if ((hd s).id == id)
+			return hd s;
+	return nil;
+}
+
+Clique.membernamed(clique: self ref Clique, name: string): ref Member
+{
+	for (ol := qfiles[clique.fileid].ofids; ol != nil; ol = tl ol)
+		if ((hd ol).uname == name)
+			return (hd ol).member;
+	for (s := clique.suspended; s != nil; s = tl s)
+		if ((hd s).name == name)
+			return hd s;
+	return nil;
+}
+
+Clique.owner(clique: self ref Clique): string
+{
+	return qfiles[clique.fileid].owner;
+}
+
+Clique.fcreate(clique: self ref Clique, f: int, parent: int, d: Sys->Dir): string
+{
+	pq: big;
+	if (parent == -1)
+		pq = mkqid(fGAMEDIR, clique.id);
+	else
+		pq = mkqid(fGAMEDATA, clique.id | (parent<<16));
+	d.qid.path = mkqid(fGAMEDATA, clique.id | (f<<16));
+	d.mode &= ~8r222;
+	return nametree->tree.create(pq, d);
+}
+
+Clique.fremove(clique: self ref Clique, f: int): string
+{
+	return nametree->tree.remove(mkqid(fGAMEDATA, clique.id | (f<<16)));
+}
+
+# debugging...
+Clique.show(nil: self ref Clique, nil: ref Member)
+{
+#	sys->print("**************** all objects:\n");
+#	showobject(clique, clique.objects[0], p, 0, ~0);
+#	if (p == nil) {
+#		f := qfiles[clique.fileid];
+#		for (ol := f.ofids; ol != nil; ol = tl ol) {
+#			p = (hd ol).member;
+#			if (p == nil) {
+#				sys->print("lurker (name '%s')\n",
+#					(hd ol).uname);
+#				continue;
+#			}
+#			sys->print("member %d, '%s': ext->obj ", p.id, p.name);
+#			for (j := 0; j < len p.ext2obj; j++)
+#				if (p.ext2obj[j] != nil)
+#					sys->print("%d->%d[%d] ", j, p.ext2obj[j].id, p.ext(p.ext2obj[j].id));
+#			sys->print("\n");
+#		}
+#	}
+}
+
+cliquerequest(clique: ref Clique, rq: ref Rq): string
+{
+	e := cliquerequest1(clique, rq);
+	sendnotifications(clique);
+	return e;
+}
+
+cliquerequest1(clique: ref Clique, rq: ref Rq): string
+{
+	if (clique.request == nil)
+		return "clique has exited";
+	clique.request <-= rq;
+	err := <-clique.reply;
+	if (clique.hungup && clique.request != nil) {
+		clique.request <-= nil;
+		clique.request = nil;
+	}
+	return err;
+}
+
+sendnotifications(clique: ref Clique)
+{
+	notes, pending: list of (int, int, string);
+	(pending, clique.notes) = (clique.notes, nil);
+	n := 0;
+	while (pending != nil) {
+		for (notes = nil; pending != nil; pending = tl pending)
+			notes = hd pending :: notes;
+		for (; notes != nil; notes = tl notes) {
+			(srcid, dstid, cmd) := hd notes;
+			dst := cliques[dstid];
+			if (!dst.hungup) {
+				dst.notes = pending;
+				cliquerequest1(dst, ref Rq.Notify(srcid, cmd));
+				(pending, dst.notes) = (dst.notes, nil);
+			}
+		}
+		if (n++ > 50)
+			panic("probable loop in clique notification");	# XXX probably shouldn't panic, but useful for debugging
+	}
+}
+
+cliqueproc(clique: ref Clique)
+{
+	wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+	spawn cliqueproc1(clique);
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(wfd, buf, len buf);
+	sys->print("spree: clique '%s' exited: %s\n", clique.fname, string buf[0:n]);
+	clique.hangup();
+	clique.request = nil;
+	clique.reply <-= "clique exited";
+}
+
+cliqueproc1(clique: ref Clique)
+{
+	for (;;) {
+		rq := <-clique.request;
+		if (rq == nil)
+			break;
+		reply := "";
+		pick r := rq {
+		Init =>
+			reply = clique.mod->init(myself, clique, clique.archive.argv);
+		Join =>
+			reply = clique.mod->join(r.member, r.cmd, r.suspended);
+		Command =>
+			reply = clique.mod->command(r.member, r.cmd);
+		Leave =>
+			if (clique.mod->leave(r.member) == 0)
+				reply = "suspended";
+		Notify =>
+			clique.mod->notify(r.srcid, r.cmd);
+		* =>
+			panic("unknown engine request, tag " + string tagof(rq));
+		}
+		clique.reply <-= reply;
+	}
+	sys->print("spree: clique '%s' exiting\n", clique.fname);
+}
+
+Member.ext(member: self ref Member, id: int): int
+{
+	obj2ext := member.obj2ext;
+	if (id >= len obj2ext || id < 0)
+		return -1;
+	return obj2ext[id];
+}
+
+Member.obj(member: self ref Member, ext: int): ref Object
+{
+	if (ext < 0 || ext >= len member.ext2obj)
+		return nil;
+	return member.ext2obj[ext];
+}
+
+# allocate an object in a member's map.
+memberaddobject(p: ref Member, o: ref Object)
+{
+	if (p.freelist == nil)
+		(p.ext2obj, p.freelist) = makespace(p.ext2obj, p.freelist);
+	ext := hd p.freelist;
+	p.freelist = tl p.freelist;
+
+	if (o.id >= len p.obj2ext) {
+		oldmap := p.obj2ext;
+		newmap := array[o.id + 10] of int;
+		newmap[0:] = oldmap;
+		for (i := len oldmap; i < len newmap; i++)
+			newmap[i] = -1;
+		p.obj2ext = newmap;
+	}
+	p.obj2ext[o.id] = ext;
+	p.ext2obj[ext] = o;
+	if (Debug)
+		sys->print("addobject member %d, internal %d, external %d\n", p.id, o.id, ext);
+}
+
+# delete an object from a member's map.
+memberdelobject(member: ref Member, id: int)
+{
+	if (id >= len member.obj2ext) {
+		sys->fprint(stderr, "spree: bad delobject (member %d, id %d, len obj2ext %d)\n",
+				member.id, id, len member.obj2ext);
+		return;
+	}
+	ext := member.obj2ext[id];
+	member.ext2obj[ext] = nil;
+	member.obj2ext[id] = -1;
+	member.freelist = ext :: member.freelist;
+	if (Debug)
+		sys->print("delobject member %d, internal %d, external %d\n", member.id, id, ext);
+}
+
+memberleaves(member: ref Member)
+{
+	clique := cliques[member.cliqueid];
+	sys->print("member %d leaving clique %d\n", member.id, member.cliqueid);
+
+	suspend := 0;
+	if (!clique.hungup)
+		suspend = cliquerequest(clique, ref Rq.Leave(member)) != nil;
+	member.del(suspend);
+}
+
+resetvisibilities(o: ref Object, id: int)
+{
+	o.visibility = setreset(o.visibility, id);
+	a := o.attrs.a;
+	for (i := 0; i < len a; i++) {
+		for (al := a[i]; al != nil; al = tl al) {
+			(hd al).visibility = setreset((hd al).visibility, id);
+			(hd al).needupdate = setreset((hd al).needupdate, id);
+		}
+	}
+	for (i = 0; i < len o.children; i++)
+		resetvisibilities(o.children[i], id);
+}
+
+# remove a member from their clique.
+# the client is still there, but won't get any clique updates.
+Member.del(member: self ref Member, suspend: int)
+{
+	clique := cliques[member.cliqueid];
+	if (!member.suspended) {
+		for (ofids := qfiles[clique.fileid].ofids; ofids != nil; ofids = tl ofids)
+			if ((hd ofids).member == member) {
+				(hd ofids).member = nil;
+				(hd ofids).hungup = 1;
+				# XXX purge update queue?
+			}
+		# go through all clique objects and attributes, resetting
+		# permissions for member id to their default values.
+		if (suspend) {
+			member.obj2ext = nil;
+			member.ext2obj = nil;
+			member.freelist = nil;
+			member.updating = 0;
+			member.suspended = 1;
+			clique.suspended = member :: clique.suspended;
+		}
+	} else if (!suspend) {
+		ns: list of ref Member;
+		for (s := clique.suspended; s != nil; s = tl s)
+			if (hd s != member)
+				ns = hd s :: ns;
+		clique.suspended = ns;
+	}
+	if (!suspend) {
+		resetvisibilities(clique.objects[0], member.id);
+		clique.memberids = clique.memberids.del(member.id);
+	}
+}
+
+Clique.members(clique: self ref Clique): list of ref Member
+{
+	pl := clique.suspended;
+	for (ofids := qfiles[clique.fileid].ofids; ofids != nil; ofids = tl ofids)
+		if ((hd ofids).member != nil)
+			pl = (hd ofids).member :: pl;
+	return pl;
+}
+
+Object.delete(o: self ref Object)
+{
+	clique := cliques[o.cliqueid];
+	if (o.parentid != -1) {
+		parent := clique.objects[o.parentid];
+		siblings := parent.children;
+		for (i := 0; i < len siblings; i++)
+			if (siblings[i] == o)
+				break;
+		if (i == len siblings)
+			panic("object " + string o.id + " not found in parent");
+		parent.deletechildren((i, i+1));
+	} else
+		sys->fprint(stderr, "spree: cannot delete root object\n");
+}
+
+Object.deletechildren(parent: self ref Object, r: Range)
+{
+	if (len parent.children == 0)
+		return;
+	clique := cliques[parent.cliqueid];
+	n := r.end - r.start;
+	objs := array[r.end - r.start] of int;
+	children := parent.children;
+	for (i := r.start; i < r.end; i++) {
+		o := children[i];
+		objs[i - r.start] = o.id;
+		o.deletechildren((0, len o.children));
+		clique.objects[o.id] = nil;
+		clique.freelist = o.id :: clique.freelist;
+		o.id = -1;
+		o.parentid = -1;
+	}
+	children[r.start:] = children[r.end:];
+	for (i = len children - n; i < len children; i++)
+		children[i] = nil;
+	if (n < len children)
+		parent.children = children[0:len children - n];
+	else
+		parent.children = nil;
+
+	if (Debug) {
+		sys->print("+del from %d, range [%d %d], objs: ", parent.id, r.start, r.end);
+		for (i = 0; i < len objs; i++)
+			sys->print("%d ", objs[i]);
+		sys->print("\n");
+	}
+	applycliqueupdate(clique, ref Update.Delete(parent.id, r, objs), All);
+}
+
+# move a range of objects from src and insert them at index in dst.
+Object.transfer(src: self ref Object, r: Range, dst: ref Object, index: int)
+{
+	if (index == -1)
+		index = len dst.children;
+	if (src == dst && index >= r.start && index <= r.end)
+		return;
+	n := r.end - r.start;
+	objs := src.children[r.start:r.end];
+	newchildren := array[len src.children - n] of ref Object;
+	newchildren[0:] = src.children[0:r.start];
+	newchildren[r.start:] = src.children[r.end:];
+	src.children = newchildren;
+
+	if (Debug) {
+		sys->print("+transfer from %d[%d,%d] to %d[%d], objs: ",
+			src.id, r.start, r.end, dst.id, index);
+		for (x := 0; x < len objs; x++)
+			sys->print("%d ", objs[x].id);
+		sys->print("\n");
+	}
+
+	nindex := index;
+
+	# if we've just removed some cards from the destination,
+	# then adjust the destination index accordingly.
+	if (src == dst && nindex > r.start) {
+		if (nindex < r.end)
+			nindex = r.start;
+		else
+			nindex -= n;
+	}
+	newchildren = array[len dst.children + n] of ref Object;
+	newchildren[0:] = dst.children[0:index];
+	newchildren[nindex + n:] = dst.children[nindex:];
+	newchildren[nindex:] = objs;
+	dst.children = newchildren;
+
+	for (i := 0; i < len objs; i++)
+		objs[i].parentid = dst.id;
+
+	clique := cliques[src.cliqueid];
+	applycliqueupdate(clique,
+		ref Update.Transfer(src.id, r, dst.id, index),
+		All);
+}
+
+# visibility is only set when the attribute is newly created.
+Object.setattr(o: self ref Object, name, val: string, visibility: Set)
+{
+	(changed, attr) := o.attrs.set(name, val, visibility);
+	if (changed) {
+		attr.needupdate = All;
+		applycliqueupdate(cliques[o.cliqueid], ref Update.Set(o, o.id, attr), objvisibility(o));
+	}
+}
+
+Object.getattr(o: self ref Object, name: string): string
+{
+	attr := o.attrs.get(name);
+	if (attr == nil)
+		return nil;
+	return attr.val;
+}
+
+# set visibility of an object - reveal any uncovered descendents
+# if necessary.
+Object.setvisibility(o: self ref Object, visibility: Set)
+{
+	if (o.visibility.eq(visibility))
+		return;
+	o.visibility = visibility;
+	applycliqueupdate(cliques[o.cliqueid], ref Update.Setvisibility(o.id, visibility), objvisibility(o));
+}
+
+Object.setattrvisibility(o: self ref Object, name: string, visibility: Set)
+{
+	attr := o.attrs.get(name);
+	if (attr == nil) {
+		sys->fprint(stderr, "spree: setattrvisibility, no attribute '%s', id %d\n", name, o.id);
+		return;
+	}
+	if (attr.visibility.eq(visibility))
+		return;
+	# send updates to anyone that has needs updating,
+	# is in the new visibility list, but not in the old one.
+	ovisibility := objvisibility(o);
+	before := ovisibility.X(A&B, attr.visibility);
+	after := ovisibility.X(A&B, visibility);
+	attr.visibility = visibility;
+	applycliqueupdate(cliques[o.cliqueid], ref Update.Set(o, o.id, attr), before.X(~A&B, after));
+}
+
+# an object's visibility is the intersection
+# of the visibility of all its parents.
+objvisibility(o: ref Object): Set
+{
+	clique := cliques[o.cliqueid];
+	visibility := All;
+	for (id := o.parentid; id != -1; id = o.parentid) {
+		o = clique.objects[id];
+		visibility = visibility.X(A&B, o.visibility);
+	}
+	return visibility;
+}
+
+makespace(objects: array of ref Object,
+		freelist: list of int): (array of ref Object, list of int)
+{
+	if (freelist == nil) {
+		na := array[len objects + 10] of ref Object;
+		na[0:] = objects;
+		for (j := len na - 1; j >= len objects; j--)
+			freelist = j :: freelist;
+		objects = na;
+	}
+	return (objects, freelist);
+}
+
+updateall()
+{
+	for (i := 0; i < len qfiles; i++) {
+		f := qfiles[i];
+		if (f != nil && f.needsupdate) {
+			for (ol := f.ofids; ol != nil; ol = tl ol)
+				sendupdate(hd ol);
+			f.needsupdate = 0;
+		}
+	}
+}
+
+applyupdate(f: ref Qfile, upd: ref Update)
+{
+	for (ol := f.ofids; ol != nil; ol = tl ol)
+		(hd ol).updateq.put(upd);
+	f.needsupdate = 1;
+}
+
+# send update to members in the clique in the needupdate set.
+applycliqueupdate(clique: ref Clique, upd: ref Update, needupdate: Set)
+{
+	always := alwayssend[tagof(upd)];
+	if (needupdate.isempty() || (!clique.started && !always))
+		return;
+	f := qfiles[clique.fileid];
+	for (ol := f.ofids; ol != nil; ol = tl ol) {
+		ofid := hd ol;
+		member := ofid.member;
+		if (member != nil && needupdate.holds(member.id) && (member.updating || always))
+			queueupdate(ofid.updateq, member, upd);
+	}
+	f.needsupdate = 1;
+}
+
+# transform an outgoing update according to the visibility
+# of the object(s) concerned.
+# the update concerned has already occurred.
+queueupdate(q: ref Queue, p: ref Member, upd: ref Update)
+{
+	clique := cliques[p.cliqueid];
+	pick u := upd {
+	Set =>
+		if (p.ext(u.o.id) != -1 && u.attr.needupdate.holds(p.id)) {
+			q.put(ref Update.Set(u.o, p.ext(u.o.id), u.attr));
+			u.attr.needupdate = u.attr.needupdate.del(p.id);
+		} else
+			u.attr.needupdate = u.attr.needupdate.add(p.id);
+
+	Transfer =>
+		# if moving from an invisible object, create the objects
+		# temporarily in the source object, and then transfer from that.
+		# if moving to an invisible object, delete the objects.
+		# if moving from invisible to invisible, do nothing.
+		src := clique.objects[u.srcid];
+		dst := clique.objects[u.dstid];
+		fromvisible := objvisibility(src).X(A&B, src.visibility).holds(p.id);
+		tovisible := objvisibility(dst).X(A&B, dst.visibility).holds(p.id);
+		if (fromvisible || tovisible) {
+			# N.B. objects are already in destination object at this point.
+			(r, index, srcid) := (u.from, u.index, u.srcid);
+
+			# XXX this scheme is all very well when the parent of src
+			# or dst is visible, but not when it's not... in that case
+			# we should revert to the old scheme of deleting objects in src
+			# or recreating them in dst as appropriate.
+			if (!tovisible) {
+				# transfer objects to destination, then delete them,
+				# so client knows where they've gone.
+				q.put(ref Update.Transfer(p.ext(srcid), r, p.ext(u.dstid), 0));
+				qdelobjects(q, p, dst, (u.index, u.index + r.end - r.start), 0);
+				break;
+			}
+			if (!fromvisible) {
+				# create at the end of source object,
+				# then transfer into correct place in destination.
+				n := r.end - r.start;
+				for (i := 0; i < n; i++) {
+					o := dst.children[index + i];
+					qrecreateobject(q, p, o, src);
+				}
+				r = (0, n);
+			}
+			if (p.ext(srcid) == -1 || p.ext(u.dstid) == -1)
+				panic("external objects do not exist");
+			q.put(ref Update.Transfer(p.ext(srcid), r, p.ext(u.dstid), index));
+		}
+	Create =>
+		dst := clique.objects[u.parentid];
+		if (objvisibility(dst).X(A&B, dst.visibility).holds(p.id)) {
+			memberaddobject(p, clique.objects[u.objid]);
+			q.put(ref Update.Create(p.ext(u.objid), p.ext(u.parentid), u.visibility, u.objtype));
+		}
+	Delete =>
+		# we can only get this update when all the children are
+		# leaf nodes.
+		o := clique.objects[u.parentid];
+		if (objvisibility(o).X(A&B, o.visibility).holds(p.id)) {
+			r := u.r;
+			extobjs := array[len u.objs] of int;
+			for (i := 0; i < len u.objs; i++) {
+				extobjs[i] = p.ext(u.objs[i]);
+				memberdelobject(p, u.objs[i]);
+			}
+			q.put(ref Update.Delete(p.ext(o.id), u.r, extobjs));
+		}
+	Setvisibility =>
+		# if the object doesn't exist for this member, don't do anything.
+		# else if there are children, check whether they exist, and
+		# create or delete them as necessary.
+		if (p.ext(u.objid) != -1) {
+			o := clique.objects[u.objid];
+			if (len o.children > 0) {
+				visible := u.visibility.holds(p.id);
+				made := p.ext(o.children[0].id) != -1;
+				if (!visible && made)
+					qdelobjects(q, p, o, (0, len o.children), 0);
+				else if (visible && !made)
+					for (i := 0; i < len o.children; i++)
+						qrecreateobject(q, p, o.children[i], nil);
+			}
+			q.put(ref Update.Setvisibility(p.ext(u.objid), u.visibility));
+		}
+	Action =>
+		s := u.s;
+		for (ol := u.objs; ol != nil; ol = tl ol)
+			s += " " + string p.ext(hd ol);
+		s += " " + u.rest;
+		q.put(ref Update.Action(s, nil, nil));
+	* =>
+		q.put(upd);
+	}
+}
+
+# queue deletions for o; we pretend to the client that
+# the deletions are at index.
+qdelobjects(q: ref Queue, p: ref Member, o: ref Object, r: Range, index: int)
+{
+	if (r.start >= r.end)
+		return;
+	children := o.children;
+	extobjs := array[r.end - r.start] of int;
+	for (i := r.start; i < r.end; i++) {
+		c := children[i];
+		qdelobjects(q, p, c, (0, len c.children), 0);
+		extobjs[i - r.start] = p.ext(c.id);
+		memberdelobject(p, c.id);
+	}
+	q.put(ref Update.Delete(p.ext(o.id), (index, index + (r.end - r.start)), extobjs));
+}
+
+# parent visibility now allows o to be seen, so recreate
+# it for the member. (if parent is non-nil, pretend we're creating it there)
+qrecreateobject(q: ref Queue, p: ref Member, o: ref Object, parent: ref Object)
+{
+	memberaddobject(p, o);
+	parentid := o.parentid;
+	if (parent != nil)
+		parentid = parent.id;
+	q.put(ref Update.Create(p.ext(o.id), p.ext(parentid), o.visibility, o.objtype));
+	recreateattrs(q, p, o);
+	if (o.visibility.holds(p.id)) {
+		a := o.children;
+		for (i := 0; i < len a; i++)
+			qrecreateobject(q, p, a[i], nil);
+	}
+}
+
+recreateattrs(q: ref Queue, p: ref Member, o: ref Object)
+{
+	a := o.attrs.a;
+	for (i := 0; i < len a; i++) {
+		for (al := a[i]; al != nil; al = tl al) {
+			attr := hd al;
+			q.put(ref Update.Set(o, p.ext(o.id), attr));
+		}
+	}
+}
+
+CONTINUATION := array[] of {byte '\n', byte '*'};
+
+# send the client as many updates as we can fit in their read request
+# (if there are some updates to send and there's an outstanding read request)
+sendupdate(ofid: ref Openfid)
+{
+	clique: ref Clique;
+	if (ofid.readreq == nil || (ofid.updateq.isempty() && !ofid.hungup))
+		return;
+	m := ofid.readreq;
+	q := ofid.updateq;
+	if (ofid.hungup) {
+		srv.reply(ref Rmsg.Read(m.tag, nil));
+		q.h = q.t = nil;
+		return;
+	}
+	data := array[m.count] of byte;
+	nb := 0;
+	plid := -1;
+	if (ofid.member != nil) {
+		plid = ofid.member.id;
+		clique = cliques[ofid.member.cliqueid];
+	}
+	avail := len data - len CONTINUATION;
+Putdata:
+	for (; !q.isempty(); q.get()) {
+		upd := q.peek();
+		pick u := upd {
+		Set =>
+			if (plid != -1 && !objvisibility(u.o).X(A&B, u.attr.visibility).holds(plid)) {
+				u.attr.needupdate = u.attr.needupdate.add(plid);
+				continue Putdata;
+			}
+		Break =>
+			if (nb > 0) {
+				q.get();
+				break Putdata;
+			}
+			continue Putdata;
+		}
+		d := array of byte update2s(upd, plid);
+		if (len d + nb > avail)
+			break;
+		data[nb:] = d;
+		nb += len d;
+	}
+	err := "";
+	if (nb == 0) {
+		if (q.isempty())
+			return;
+		err = "short read";
+	} else if (!q.isempty()) {
+		data[nb:] = CONTINUATION;
+		nb += len CONTINUATION;
+	}
+	data = data[0:nb];
+			
+	if (err != nil)
+		srv.reply(ref Rmsg.Error(m.tag, err));
+	else
+		srv.reply(ref Rmsg.Read(m.tag, data));
+	ofid.readreq = nil;
+}
+
+# convert an Update adt to a string.
+update2s(upd: ref Update, plid: int): string
+{
+	s: string;
+	pick u := upd {
+	Create =>
+		objtype := u.objtype;
+		if (objtype == nil)
+			objtype = "nil";
+		s = sys->sprint("create %d %d %d %s\n", u.objid, u.parentid, u.visibility.holds(plid) != 0, objtype);
+	Transfer =>
+		# tx src dst dstindex start end
+		if (u.srcid == -1 || u.dstid == -1)
+			panic("src or dst object is -1");
+		s = sys->sprint("tx %d %d %d %d %d\n",
+			u.srcid, u.dstid, u.from.start, u.from.end, u.index);
+	Delete =>
+		s = sys->sprint("del %d %d %d", u.parentid, u.r.start, u.r.end);
+		for (i := 0; i < len u.objs; i++)
+			s += " " + string u.objs[i];
+		s[len s] = '\n';
+	Set =>
+		s = sys->sprint("set %d %s %s\n", u.objid, u.attr.name, u.attr.val);
+	Setvisibility =>
+		s = sys->sprint("vis %d %d\n", u.objid, u.visibility.holds(plid) != 0);
+	Action =>
+		s = u.s + "\n";
+	* =>
+		sys->fprint(stderr, "unknown update tag %d\n", tagof(upd));
+	}
+	return s;
+}
+
+Queue.put(q: self ref Queue, s: T)
+{
+	q.t = s :: q.t;
+}
+
+Queue.get(q: self ref Queue): T
+{
+	s: T;
+	if(q.h == nil){
+		q.h = revlist(q.t);
+		q.t = nil;
+	}
+	if(q.h != nil){
+		s = hd q.h;
+		q.h = tl q.h;
+	}
+	return s;
+}
+
+Queue.peek(q: self ref Queue): T
+{
+	s: T;
+	if (q.isempty())
+		return s;
+	s = q.get();
+	q.h = s :: q.h;
+	return s;
+}
+
+Queue.isempty(q: self ref Queue): int
+{
+	return q.h == nil && q.t == nil;
+}
+
+revlist(ls: list of T) : list of T
+{
+	rs: list of T;
+	for (; ls != nil; ls = tl ls)
+		rs = hd ls :: rs;
+	return rs;
+}
+
+Attributes.new(): ref Attributes
+{
+	return ref Attributes(array[7] of list of ref Attribute);
+}
+
+Attributes.get(attrs: self ref Attributes, name: string): ref Attribute
+{
+	for (al := attrs.a[strhash(name, len attrs.a)]; al != nil; al = tl al)
+		if ((hd al).name == name)
+			return hd al;
+	return nil;
+}
+
+# return (haschanged, attr)
+Attributes.set(attrs: self ref Attributes, name, val: string, visibility: Set): (int, ref Attribute)
+{
+	h := strhash(name, len attrs.a);
+	for (al := attrs.a[h]; al != nil; al = tl al) {
+		attr := hd al;
+		if (attr.name == name) {
+			if (attr.val == val)
+				return (0, attr);
+			attr.val = val;
+			return (1, attr);
+		}
+	}
+	attr := ref Attribute(name, val, visibility, All);
+	attrs.a[h] = attr :: attrs.a[h];
+	return (1, attr);
+}
+
+setreset(set: Set, i: int): Set
+{
+	if (set.msb())
+		return set.add(i);
+	return set.del(i);
+}
+
+# from Aho Hopcroft Ullman
+strhash(s: string, n: int): int
+{
+	h := 0;
+	m := len s;
+	for(i := 0; i<m; i++){
+		h = 65599 * h + s[i];
+	}
+	return (h & 16r7fffffff) % n;
+}
+
+panic(s: string)
+{
+	cliques[0].show(nil);
+	sys->fprint(stderr, "panic: %s\n", s);
+	raise "panic";
+}
+
+randbits: chan of int;
+
+initrand()
+{
+	randbits = chan of int;
+	spawn randproc();
+}
+
+randproc()
+{
+	fd := sys->open("/dev/notquiterandom", Sys->OREAD);
+	if (fd == nil) {
+		sys->print("cannot open /dev/random: %r\n");
+		exit;
+	}
+	randbits <-= sys->pctl(0, nil);
+	buf := array[1] of byte;
+	while ((n := sys->read(fd, buf, len buf)) > 0) {
+		b := buf[0];
+		for (i := byte 1; i != byte 0; i <<= 1)
+			randbits <-= (b & i) != byte 0;
+	}
+}
+
+rand(n: int): int
+{
+	x: int;
+	for (nbits := 0; (1 << nbits) < n; nbits++)
+		x ^= <-randbits << nbits;
+	x ^= <-randbits << nbits;
+	x &= (1 << nbits) - 1;
+	i := 0;
+	while (x >= n) {
+		x ^= <-randbits << i;
+		i = (i + 1) % nbits;
+	}
+	return x;
+}
+
+archivenum := -1;
+
+newarchivename(): string
+{
+	if (archivenum == -1) {
+		(d, nil) := readdir->init(ARCHIVEDIR, Readdir->MTIME|Readdir->COMPACT);
+		for (i := 0; i < len d; i++) {
+			name := d[i].name;
+			if (name != nil && name[0] == 'a') {
+				for (j := 1; j < len name; j++)
+					if (name[j] < '0' || name[j] > '9')
+						break;
+				if (j == len name && int name[1:] > archivenum)
+					archivenum = int name[1:];
+			}
+		}
+		archivenum++;
+	}
+	return ARCHIVEDIR + "/a" + string archivenum++;
+}
+
+archivenames(): list of string
+{
+	names: list of string;
+	(d, nil) := readdir->init(ARCHIVEDIR, Readdir->MTIME|Readdir->COMPACT);
+	for (i := 0; i < len d; i++)
+		if (len d[i].name < 4 || d[i].name[len d[i].name - 4:] != ".old")
+			names = ARCHIVEDIR + "/" + d[i].name ::  names;
+	return names;
+}
--- /dev/null
+++ b/appl/spree/spree.m
@@ -1,0 +1,140 @@
+Spree: module
+{
+	MAXPLAYERS: con 100;
+	Attribute: adt {
+		name:	string;
+		val:		string;
+		visibility:	Sets->Set;			# set of members that can see attr
+		needupdate:	Sets->Set;		# set of members that have not got an update queued
+	};
+	
+	Attributes: adt {
+		a:		array of list of ref Attribute;
+		set:		fn(attr: self ref Attributes, name, val: string, vis: Sets->Set): (int, ref Attribute);
+		get:		fn(attr: self ref Attributes, name: string): ref Attribute;
+		new:		fn(): ref Attributes;
+	};
+	
+	Range: adt {
+		start:		int;
+		end:		int;
+	};
+	
+	Object: adt {
+		id:		int;
+		attrs:		ref Attributes;
+		visibility:	Sets->Set;
+		parentid:	int;
+		children:	cyclic array of ref Object;		# not actually cyclic
+		cliqueid:	int;
+		objtype:	string;
+	
+		transfer:		fn(o: self ref Object, r: Range, dst: ref Object, i: int);
+		setvisibility:	fn(o: self ref Object, visibility: Sets->Set);
+		setattrvisibility:	fn(o: self ref Object, name: string, visibility: Sets->Set);
+		setattr:		fn(o: self ref Object, name: string, val: string, vis: Sets->Set);
+		getattr:		fn(o: self ref Object, name: string): string;
+		delete:		fn(o: self ref Object);
+		deletechildren:	fn(o: self ref Object, r: Range);
+	};
+
+	Rq: adt {
+		pick {
+		Init =>
+			opts: string;
+		Command =>
+			member: ref Member;
+			cmd: string;
+		Join =>
+			member: ref Member;
+			cmd:	string;
+			suspended: int;
+		Leave =>
+			member: ref Member;
+		Notify =>
+			srcid: int;
+			cmd:	string;
+		}
+	};
+	
+	# this might also be known as a "group", as there's nothing
+	# inherently clique-like about it; it's just a group of members
+	# mutually creating and manipulating objects.
+	Clique: adt {
+		id:		int;
+		fileid:	int;
+		fname:	string;
+		objects:	array of ref Object;
+		archive:	ref Archives->Archive;
+		freelist:	list of int;
+		mod:	Engine;
+		memberids:	Sets->Set;				# set of allocated member ids
+		suspended: list of ref Member;
+		request:	chan of ref Rq;
+		reply:	chan of string;
+		hungup:	int;
+		started:	int;
+		parentid:	int;
+		notes:	list of (int, int, string);	# (src, dest, note)
+
+		new:			fn(parent: self ref Clique, archive: ref Archives->Archive, owner: string): (int, string, string);	# returns (cliqueid, filename, error)
+		newobject:	fn(clique: self ref Clique, parent: ref Object, visibility: Sets->Set, objtype: string): ref Object;
+		start:			fn(clique: self ref Clique);
+		action:		fn(clique: self ref Clique, cmd: string,
+						objs: list of int, rest: string, whoto: Sets->Set);
+		breakmsg:	fn(clique: self ref Clique, whoto: Sets->Set);
+		show:		fn(clique: self ref Clique, member: ref Member);
+		member:	fn(clique: self ref Clique, id: int): ref Member;
+		membernamed:	fn(clique: self ref Clique, name: string): ref Member;
+		members:	fn(clique: self ref Clique): list of ref Member;
+		owner:	fn(clique: self ref Clique): string;
+		hangup:	fn(clique: self ref Clique);
+		fcreate:	fn(clique: self ref Clique, i: int, pq: int, d: Sys->Dir): string;
+		fremove:	fn(clique: self ref Clique, i: int): string;
+		notify:	fn(clique: self ref Clique, cliqueid: int, msg: string);
+	};
+
+	# a Member is involved in one clique only
+	Member: adt {
+		id:		int;
+		cliqueid:	int;
+		obj2ext:	array of int;
+		ext2obj:	array of ref Object;
+		freelist:	list of int;
+		name:	string;
+		updating:	int;
+		suspended:	int;
+
+		ext:		fn(member: self ref Member, id: int): int;
+		obj:		fn(member: self ref Member, id: int): ref Object;
+		del:		fn(member: self ref Member, suspend: int);
+	};
+
+	init:   fn(ctxt: ref Draw->Context, argv: list of string);
+	archivenames: fn(): list of string;
+	newarchivename: fn(): string;
+	rand:	fn(n: int): int;
+};
+
+Engine: module {
+	init:			fn(srvmod: Spree, clique: ref Spree->Clique, argv: list of string): string;
+	command:	fn(member: ref Spree->Member, e: string): string;
+	join:			fn(member: ref Spree->Member , e: string, suspended: int): string;
+	leave:		fn(member: ref Spree->Member): int;
+	notify:		fn(fromid: int, s: string);
+	readfile:		fn(f: int, offset: big, count: int): array of byte;
+};
+
+Archives: module {
+	PATH:	con "/dis/spree/archives.dis";
+	Archive: adt {
+		argv:		list of string;			# how to restart the session.
+		members:	array of string;			# members involved.
+		info:		list of (string, string);	# any other information.
+		objects:	array of ref Spree->Object;
+	};
+	init:			fn(mod: Spree);
+	write:		fn(clique: ref Spree->Clique, info: list of (string, string), file: string, members: Sets->Set): string;
+	read:			fn(file: string): (ref Archive, string);
+	readheader:	fn(file: string): (ref Archive, string);
+};
--- /dev/null
+++ b/appl/svc/auth.sh
@@ -1,0 +1,13 @@
+#!/dis/sh.dis -n
+load std
+or {ftest -e /net/dns} {ftest -e /env/emuhost} {ndb/dns}
+or {ftest -e /net/cs} {ndb/cs}
+or {ftest -f /keydb/signerkey} {echo 'auth: need to use createsignerkey(8)' >[1=2]; raise nosignerkey}
+or {ftest -f /keydb/keys} {echo 'auth: need to create /keydb/keys' >[1=2]; raise nokeys}
+and {auth/keyfs} {
+	listen -v -t -A 'tcp!*!inflogin' {auth/logind&}
+	listen -v -t -A 'tcp!*!infkey' {auth/keysrv&}
+	listen -v -t -A 'tcp!*!infsigner' {auth/signer&}
+	listen -v -t -A 'tcp!*!infcsigner' {auth/countersigner&}
+}
+# run svc/registry separately if desired
--- /dev/null
+++ b/appl/svc/httpd/alarms.b
@@ -1,0 +1,45 @@
+implement Alarms;
+include "sys.m";
+	sys: Sys;
+
+include "alarms.m";
+
+Alarm.stop(a: self Alarm) 
+{
+	a.alchan <-= -1;
+	fd:=sys->open("#p/"+string a.pid+"/ctl",sys->OWRITE);
+	sys->fprint(fd, "killgrp");
+}
+
+Alarm.alarm(time: int): Alarm
+{
+	if (sys == nil)
+		sys = load Sys Sys->PATH;
+
+	pid := sys->pctl(sys->NEWPGRP|sys->FORKNS,nil);
+	a:=Alarm(chan of int,pid);
+	spawn listener(a.alchan);
+	spawn sleeper(a.alchan,time,pid);
+	return a;
+}
+	
+sleeper(ch: chan of int, time, pid: int)
+{
+	sys->sleep(time);
+	alt{
+		ch <-= pid =>
+			;
+		* =>
+			exit;
+	}
+}
+
+listener(ch: chan of int)
+{
+	a := <-ch;
+	if (a==-1)
+		exit;
+	fd := sys->open("#p/"+string a+"/ctl",sys->OWRITE);
+	if (fd != nil)
+		sys->fprint(fd, "killgrp");
+}
--- /dev/null
+++ b/appl/svc/httpd/alarms.m
@@ -1,0 +1,11 @@
+Alarms: module{
+	PATH:  		con	"/dis/svc/httpd/alarms.dis";	
+
+	Alarm: adt{
+		alchan: chan of int;
+		pid: int;
+		stop: fn(a: self Alarm); 
+		alarm: fn(time: int): Alarm;
+	};
+	
+};
--- /dev/null
+++ b/appl/svc/httpd/cache.b
@@ -1,0 +1,188 @@
+implement Cache;
+
+include "sys.m";
+	sys : Sys;
+
+include "bufio.m";
+	bufio : Bufio;
+Iobuf : import bufio;
+
+include "lock.m";
+	locks: Lock;
+	Semaphore: import locks;
+
+dbg_log : ref Sys->FD;
+
+include "cache.m";
+
+HASHSIZE : con 1019;
+
+lru ,cache_size : int; # lru link, and maximum size of cache.
+cur_size, cur_tag : int; # current size of cache and current number.
+
+lock: ref Semaphore;
+
+Cache_link : adt{
+	name : string; 			# name of file
+	contents : array of byte; 	# contents
+	length : int; 			# length of file
+	qid:Sys->Qid;			
+	tag : int;
+};
+
+tab := array[HASHSIZE] of list of Cache_link;
+
+hashasu(key : string,n : int): int
+{
+	i, h : int;
+	h=0;
+	i=0;
+        while(i<len key){
+		h = 10*h + key[i];
+		h = h%n;
+		i++;
+	}
+	return h%n;
+}
+
+
+insert(name: string, ctents: array of byte , length : int, qid:Sys->Qid) : int
+{
+	tmp : Cache_link;
+	hash : int;
+	lock.obtain();
+	hash = hashasu(name,HASHSIZE);
+	if (dbg_log!=nil){
+		sys->fprint(dbg_log,"current size is %d, adding %s\n", cur_size,name);
+	}
+	while (cur_size+length > cache_size)
+		throw_out();
+	tmp.name =name;
+	tmp.contents = ctents;
+	tmp.length = length;
+	tmp.qid = qid;
+	tmp.tag = cur_tag;
+	cur_size+=length;
+	cur_tag++;
+	if (cur_tag<0) cur_tag=0;
+	tab[hash]= tmp :: tab[hash];
+	lock.release();
+	return 1;
+}
+
+find(name : string, qid:Sys->Qid) : (int, array of byte)
+{
+	hash,flag,stale : int;
+	nlist : list of Cache_link;
+	retval : array of byte;
+	flag=0;
+	nlist=nil;
+	retval=nil;
+	stale=0;
+	lock.obtain();
+	hash = hashasu(name,HASHSIZE);
+	tmp:=tab[hash];
+	for(;tmp!=nil;tmp = tl tmp){
+		link:=hd tmp;
+		if (link.name==name){
+			if(link.qid.path==qid.path && link.qid.vers==qid.vers){
+				link.tag=cur_tag;
+				cur_tag++;
+				flag = 1;
+				retval = (hd tmp).contents;
+			} else { # cache is stale
+				lru--;  if(lru<0) lru = 0;
+				link.tag = lru;
+				stale = 1;
+			}
+		}
+		nlist = link :: nlist;
+	}
+	tab[hash]=nlist;
+	if (flag && (dbg_log!=nil))
+		sys->fprint(dbg_log,"Found %s in cache, cur_tag is %d\n",name,cur_tag);
+	if (stale){
+		if (dbg_log!=nil)
+			sys->fprint(dbg_log,"Stale %s in cache\n",name);
+		throw_out();
+	}
+	lock.release();
+	return (flag,retval);
+}	
+
+throw_out()
+{
+	nlist : list of Cache_link;
+	for(i:=0;i<HASHSIZE;i++){
+		tmp:=tab[i];
+		for(;tmp!=nil;tmp = tl tmp)
+			if ((hd tmp).tag==lru)
+				break;
+		if (tmp!=nil)
+			break;
+	}
+	# now, the lru is in tab[i]...
+	nlist=nil;
+	if(i < len tab){
+		for(;tab[i]!=nil;tab[i]=tl tab[i]){
+			if ((hd tab[i]).tag==lru){
+				if (dbg_log!=nil)
+					sys->fprint(dbg_log,"Throwing out %s\n",(hd tab[i]).name);
+				cur_size-=(hd tab[i]).length;	
+				tab[i] = tl tab[i];
+			}
+			if (tab[i]!=nil)
+				nlist = (hd tab[i]) :: nlist;
+			if (tab[i]==nil) break;
+		}
+	}
+	lru=find_lru();
+	if (dbg_log!=nil)
+		sys->fprint(dbg_log,"New lru is %d",lru);
+	tab[i] = nlist;
+}
+
+find_lru() : int
+{
+	min := cur_tag;
+	for(i:=0;i<HASHSIZE;i++){
+		tmp:=tab[i];
+		for(;tmp!=nil;tmp = tl tmp)
+			if ((hd tmp).tag<min)
+				min=(hd tmp).tag;
+	}
+	return min;
+}
+
+cache_init(log : ref Sys->FD, csize : int)
+{
+	n : int;
+	for(n=0;n<HASHSIZE;n++)
+		tab[n]= nil;
+	lru=0;
+	cur_size=0;
+	cache_size = csize*1024;
+	sys = load Sys Sys->PATH;
+	locks = load Lock Lock->PATH;
+	locks->init();
+	lock = Semaphore.new();
+	dbg_log = log;
+	if (dbg_log!=nil)
+		sys->fprint(dbg_log,"Cache initialised, max size is %d K\n",cache_size);
+}
+
+dump() : list of (string,int,int)
+{
+	retval: list of (string,int,int);
+	lock.obtain();
+	for(i:=0;i<HASHSIZE;i++){
+		tmp:=tab[i];
+		while(tmp!=nil){
+			retval = ((hd tmp).name, (hd tmp).length,
+					(hd tmp).tag) :: retval;
+			tmp = tl tmp;
+		}
+	}
+	lock.release();
+	return retval;
+}
--- /dev/null
+++ b/appl/svc/httpd/cache.m
@@ -1,0 +1,9 @@
+Cache : module
+{
+	PATH: con "/dis/svc/httpd/cache.dis";
+
+	cache_init: fn(log : ref Sys->FD,  size : int);
+	insert : fn(name: string, ctents: array of byte, length : int, qid:Sys->Qid) : int;
+	find: fn(name : string, qid:Sys->Qid) : (int,array of byte);
+	dump : fn() : list of (string,int,int);
+};
--- /dev/null
+++ b/appl/svc/httpd/cgiparse.b
@@ -1,0 +1,258 @@
+implement CgiParse;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "string.m";
+	str: String;
+include "bufio.m";
+include "daytime.m";
+	daytime : Daytime;
+include "parser.m";
+	parser : Parser;
+include "contents.m";
+include "cache.m";
+include "httpd.m";
+	Private_info: import Httpd;
+include "cgiparse.m";
+
+stderr : ref Sys->FD;
+
+cgiparse(g: ref Private_info, req: Httpd->Request): ref CgiData
+{
+	ret: ref CgiData;
+	(ok, err) := loadmodules();
+	if(ok == -1) {
+		sys->fprint(stderr, "CgiParse: %s\n", err );
+		return nil;
+	}
+
+	(ok, err, ret) = parse(g, req);
+
+	if(ok < 0){
+		sys->fprint( stderr, "CgiParse: %s\n", err );
+		return nil;
+	}
+	return ret;
+}
+
+badmod(p: string): (int, string)
+{
+	return (-1, sys->sprint("cannot load %s: %r", p));
+}
+
+loadmodules(): (int, string)
+{
+	if( sys == nil )
+		sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	if(daytime == nil)
+		daytime = load Daytime Daytime->PATH;
+	if(daytime == nil)
+		return badmod(Daytime->PATH);
+	if(str == nil)
+		str = load String String->PATH;
+	if(str == nil)
+		return badmod(String->PATH);
+	if( parser == nil )
+		parser = load Parser Parser->PATH;
+	if( parser == nil )
+		return badmod(Parser->PATH);
+	return (0, nil);
+}
+
+parse(g: ref Private_info, req: Httpd->Request) : (int, string, ref CgiData)
+{
+	bufio := g.bufio;
+	Iobuf: import bufio;
+	
+	host, remote, referer, httphd : string;
+	form: list of (string, string);
+	
+	tmstamp := daytime->time();
+
+	(method, version, uri, search) := (req.method, req.version, req.uri, req.search);
+	
+	if(version != ""){
+		if( g.version == nil )
+			return (-1, "version unknown.", nil);
+		if( g.bout == nil )
+			return (-1, "internal error, g.bout is nil.", nil);
+		if( g.bin == nil )
+			return (-1, "internal error, g.bin is nil.", nil);
+		httphd = g.version + " 200 OK\r\n" +
+			"Server: Inferno-Httpd\r\n" +
+			"MIME-version: 1.0\r\n" +
+			"Date: " + tmstamp + "\r\n" +
+			"Content-type: text/html\r\n" +
+			"\r\n";
+	}
+	
+	hstr := "";
+	lastnl := 1;
+	eof := 0;
+	while((c := g.bin.getc()) != bufio->EOF ) {	
+		if (c == '\r' ) {	
+			hstr[len hstr] = c;
+			c = g.bin.getb();
+			if( c == bufio->EOF ){
+				eof = 1;
+				break;
+			}
+		}
+		hstr[len hstr] = c;
+		if(c == '\n' ){	
+			if( lastnl )
+				break;
+			lastnl = 1;
+		}
+		else
+			lastnl = 0;
+	}
+	host = g.host;
+	remote = g.remotesys;
+	referer = g.referer;
+	(cnt, header) := parseheader( hstr );
+	method = str->toupper( method);
+	if (method  == "POST") {	
+		s := "";
+		while(!eof && cnt && (c = g.bin.getc()) != '\n' ) {	
+			s[len s] = c;
+			cnt--;
+			if( c == '\r' )
+				eof = 1;
+		}
+		form = parsequery(s);
+	}
+	for (ql := parsequery(req.search); ql != nil; ql = tl ql)
+		form = hd ql :: form;
+	return (0, nil, 
+		ref CgiData(method, version, uri, search, tmstamp, host, remote, referer,
+		httphd, header, form));
+}
+
+parseheader(hstr: string): (int, list of (string, string))
+{
+	header : list of (string, string);
+	cnt := 0;
+	if( hstr == nil || len hstr == 0 )
+		return (0, nil);
+	(n, sl) := sys->tokenize( hstr, "\r\n" );
+	if( n <= 0 )
+		return (0, nil);
+	while( sl != nil ){
+		s := hd sl;
+		sl = tl sl;
+		for( i := 0; i < len s; i++ ){	
+				if( s[i] == ':' ){
+				tag := s[0:i+1];
+				val := s[i+1:];
+				if( val[len val - 1] == '\r' )
+					val[len val - 1] = ' ';
+				if( val[len val - 1] == '\n' )
+					val[len val - 1] = ' ';
+				header = (tag, val) :: header;
+				if(str->tolower( tag ) == "content-length:" ){
+					if( val != nil && len val > 0 )
+						cnt = int val;
+					else
+						cnt = 0;
+				}
+				break;
+			}
+		}
+	}
+	return (cnt, listrev( header ));
+}
+
+listrev(s: list of (string, string)): list of (string, string)
+{
+	    tmp : list of (string, string);
+	    while( s != nil ) {
+		tmp = hd s :: tmp;
+		s = tl s;
+	    }
+	    return tmp;
+}
+
+getbaseip() : string
+{
+	buf : array of byte;
+	fd := sys->open( "/net/bootp", Sys->OREAD );
+	if( fd != nil ){
+		(n, d) := sys->fstat( fd );
+		if( n >= 0 ){
+			if(int d.length > 0 )
+				buf = array [int d.length] of byte;
+			else
+				buf = array [128] of byte;
+			n = sys->read( fd, buf, len buf );
+			if( n > 0 ){
+				(nil, sl) := sys->tokenize( string buf[0:n], " \t\n" );
+				while( sl != nil ){
+					if( hd sl == "ipaddr" ){
+						sl = tl sl;
+						break;
+					}
+					sl = tl sl;
+				}
+				if( sl != nil )
+					return "http://" + (hd sl);
+			}
+		}
+	}
+	return "http://beast2";
+}
+
+getbase() : string
+{
+	fd := sys->open( "/dev/sysname", Sys->OREAD );
+	if( fd != nil ){
+		buf := array [128] of byte;
+		n := sys->read( fd, buf, len buf );
+		if( n > 0 )
+			return "http://" + string buf[0:n];
+	}
+	return "http://beast2";
+}
+
+gethost() : string
+{
+	fd := sys->open( "/dev/sysname", Sys->OREAD );
+	if(fd != nil) {
+		buf := array [128] of byte;
+		n := sys->read( fd, buf, len buf );
+		if( n > 0 )
+			return string buf[0:n];
+	}
+	return "none";
+}
+
+# parse a search string of the form
+# tag=val&tag1=val1...
+parsequery(search : string): list of (string, string)
+{
+	q: list of (string, string);
+	tag, val : string;
+	if (contains(search, '?'))
+		(nil,search) = str->splitr(search,"?");
+	while(search!=nil){
+		(tag,search) = str->splitl(search,"=");
+		if (search != nil) {
+			search=search[1:];
+			(val,search) = str->splitl(search,"&");
+			if (search!=nil)
+				search=search[1:];
+			q = (parser->urlunesc(tag), parser->urlunesc(val)) :: q;
+		}
+	}
+	return q;
+}
+
+contains(s: string, c: int): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == c)
+			return 1;
+	return 0;
+}
--- /dev/null
+++ b/appl/svc/httpd/cgiparse.m
@@ -1,0 +1,22 @@
+CgiData : adt {
+    method : string;
+    version : string;
+    uri : string;
+    search : string;
+    tmstamp : string;
+    host : string;
+    remote : string;
+    referer : string;
+    httphd : string;
+    header : list of (string, string);
+    form : list of (string, string);
+};
+
+CgiParse : module
+{
+    PATH : con "/dis/svc/httpd/cgiparse.dis";
+    cgiparse : fn( g : ref Httpd->Private_info, req: Httpd->Request): ref CgiData;
+    getbase : fn() : string;
+    gethost : fn() : string;
+};
+
--- /dev/null
+++ b/appl/svc/httpd/contents.b
@@ -1,0 +1,192 @@
+implement Contents;
+
+include "sys.m";
+	sys: Sys;
+	dbg_log : ref Sys->FD;
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+Iobuf : import bufio;
+	
+include "contents.m";
+
+include "cache.m";
+
+include "httpd.m";
+
+include "string.m";
+	str : String;
+
+Suffix: adt{
+	suffix : string;
+	generic : string;
+	specific : string;
+	encoding : string;
+};
+
+suffixes: list of Suffix;
+
+#internal functions...
+parsesuffix : fn(nil:string): (int,Suffix);
+
+mkcontent(generic,specific : string): ref Content
+{
+	c:= ref Content; 	
+	c.generic = generic;
+	c.specific = specific;
+	c.q = real 1;
+	return c;
+}
+
+badmod(m: string)
+{
+	sys->fprint(stderr(), "contents: cannot load %s: %r\n", m);
+	raise "fail:bad module";
+}
+
+contentinit(log: ref Sys->FD)
+{
+	if(suffixes != nil)
+		return;
+
+	sys = load Sys Sys->PATH;
+
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) badmod(Bufio->PATH);
+
+	str = load String String->PATH;
+	if (str == nil) badmod(String->PATH);
+
+	iob := bufio->open(Httpd->HTTP_SUFF, bufio->OREAD);
+	if (iob==nil) {
+		sys->fprint(stderr(), "contents: cannot open %s: %r\n", Httpd->HTTP_SUFF);
+		raise "fail:no suffix file";;
+	}
+	while((s := iob.gets('\n'))!=nil) {
+		(i, su) := parsesuffix(s);
+		if (i != 0)
+			suffixes =  su :: suffixes;
+	}
+	dbg_log = log;
+}
+
+# classify by file name extensions
+
+uriclass(name : string): (ref Content, ref Content)
+{
+	s : Suffix;
+	typ, enc: ref Content;
+	p : string;
+	lis := suffixes;
+	typ=nil;
+	enc=nil;
+	uri:=name;
+	(nil,p) = str->splitr(name,"/");
+	if (p!=nil) name=p;
+
+	if(str->in('.',name)){
+		(nil,p) = str->splitl(name,".");
+		for(s = hd lis; lis!=nil; lis = tl lis){
+			if(p == s.suffix){	
+				if(s.generic != nil && typ==nil)
+					typ = mkcontent(s.generic, s.specific);
+				if(s.encoding != nil && enc==nil)
+					enc = mkcontent(s.encoding, "");
+			}
+		s = hd lis;
+		}
+	}
+	if(typ == nil && enc == nil){
+		buff := array[64] of byte;
+		fd := sys->open(uri, sys->OREAD);
+		n := sys->read(fd, buff, len buff);
+		if(n > 0){
+			tmp := string buff[0:n];
+			(typ, enc) = dataclass(tmp);
+		}
+	}
+	return (typ, enc);
+}
+
+
+parsesuffix(line: string): (int, Suffix)
+{
+	s : Suffix;	
+	if (str->in('#',line))
+		(line,nil) = str->splitl(line, "#");
+	if (line!=nil){
+		(n,slist):=sys->tokenize(line,"\n\t ");
+		if (n!=4 && n!=0){
+			if (dbg_log!=nil)
+				sys->fprint(dbg_log,"Error in suffixes file!, n=%d\n",n);
+			sys->print("Error in suffixes file!, n=%d\n",n);
+			exit;
+		}
+		s.suffix = hd slist;
+		slist = tl slist;
+		s.generic = hd slist;
+		if (s.generic == "-") s.generic="";	
+		slist = tl slist;
+		s.specific = hd slist;
+		if (s.specific == "-") s.specific="";	
+		slist = tl slist;
+		s.encoding = hd slist;
+		if (s.encoding == "-") s.encoding="";
+		
+	}
+	if (((s.generic ==  "")||(s.specific ==  "")) && s.encoding=="")
+		return (0,s);
+	return (1,s);
+}
+
+#classify by initial contents of file
+dataclass(buf : string): (ref Content,ref Content)
+{
+	c,n : int;
+	c=0;
+	n = len buf;
+	for(; n > 0; n --){		
+		if(buf[c] < 16r80)
+			if(buf[c] < 32 && buf[c] != '\n' && buf[c] != '\r' 
+					&& buf[c] != '\t' && buf[c] != '\v')
+				return (nil,nil);
+		c++;		
+	}
+	return (mkcontent("text", "plain"),nil);
+}
+
+checkcontent(me: ref Content,oks :list of ref Content, clist : string): int
+{
+	ok:=oks;
+	try : ref Content;
+	if(oks == nil || me == nil)
+		return 1;
+	for(; ok != nil; ok = tl ok){
+		try = hd ok;
+		if((try.generic==me.generic || try.generic=="*")
+		&& (try.specific==me.specific || try.specific=="*")){
+			return 1;
+		}
+	}
+
+	sys->fprint(dbg_log,"%s/%s not found", 
+				me.generic, me.specific);
+	logcontent(clist, oks);
+	return 1;
+}
+
+logcontent(name : string, c : list of ref Content)
+{
+	buf : string;
+	if (dbg_log!=nil){
+		for(; c!=nil; c = tl c)
+			buf+=sys->sprint("%s/%s ", (hd c).generic,(hd c).specific);
+		sys->fprint(dbg_log,"%s: %s: %s", "client", name, buf);
+	}
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/svc/httpd/contents.m
@@ -1,0 +1,16 @@
+Contents: module
+{
+	PATH:		con	"/dis/svc/httpd/contents.dis";
+
+	Content: adt{
+		generic: string;
+		specific: string;
+		q: real;
+	};
+	
+	contentinit: fn(log : ref Sys->FD);
+	mkcontent: fn(specific,generic : string): ref Content;
+	uriclass:  fn(name : string): (ref Content, ref Content);
+	checkcontent: fn(me: ref Content,oks :list of ref Content, 
+			clist : string): int;
+};
--- /dev/null
+++ b/appl/svc/httpd/date.b
@@ -1,0 +1,264 @@
+implement Date;
+
+include "sys.m";
+	sys: Sys;
+
+include "daytime.m";
+	daytime : Daytime;
+
+Tm: import daytime;
+
+include "date.m";
+
+ # print dates in the format
+ # Wkd, DD Mon YYYY HH:MM:SS GMT
+ # parse dates of formats
+ # Wkd, DD Mon YYYY HH:MM:SS GMT
+ # Weekday, DD-Mon-YY HH:MM:SS GMT
+ # Wkd Mon ( D|DD) HH:MM:SS YYYY
+ # plus anything similar
+
+SEC2MIN: con 60;
+SEC2HOUR: con (60*SEC2MIN);
+SEC2DAY: con (24*SEC2HOUR);
+
+#  days per month plus days/year
+
+dmsize := array[] of {
+	365, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+};
+
+ldmsize := array[] of {
+	366, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+};
+
+#  return the days/month for the given year
+
+
+weekdayname := array[] of {
+	"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+};
+
+wdayname := array[] of {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+
+monname := array[] of {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	daytime = load Daytime Daytime->PATH;
+	if (daytime==nil){
+		sys->print("daytime load: %r\n");
+		exit;
+	}
+}
+
+yrsize(yr : int): array of int
+{
+	if(yr % 4 == 0 && (yr % 100 != 0 || yr % 400 == 0))
+		return ldmsize;
+	else
+		return dmsize;
+}
+
+tolower(c: int): int
+{
+	if(c >= 'A' && c <= 'Z')
+		return c - 'A' + 'a';
+	return c;
+}
+
+
+isalpha(c: int): int
+{
+	return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
+}
+
+
+isdig(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+
+dateconv(t: int): string
+{
+	tm : ref Tm;
+	tm = daytime->gmt(t);
+	return sys->sprint("%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT",
+		wdayname[tm.wday], tm.mday, monname[tm.mon], tm.year+1900,
+		tm.hour, tm.min, tm.sec);	
+}
+
+
+dateword(date : string): (string,string)
+{
+	p : string;
+	i:=0;
+	p = "";
+	while((i<len date) && !isalpha(date[i]) && !isdig(date[i]))
+		i++;
+	while((i<len date) && isalpha(date[i])){		
+		p[len p] = tolower(date[i]);
+		i++;
+	}
+	return (date[i:],p);
+}
+
+
+datenum(date : string): (string, int)
+{
+	n, i : int;
+	i=0;
+	while((i<len date) && !isdig(date[i]))
+		i++;
+	if(i == len date)
+		return (nil, -1);
+	n = 0;
+	while((i<len date) && isdig(date[i])){
+		n = n * 10 + date[i] - '0';
+		i++;
+	}
+	return (date[i:], n);
+}
+
+
+ # parse a date and return the seconds since the epoch
+ # return 0 for a failure
+ 
+# could be big?
+date2sec(date : string): int
+{
+	tm : Tm;
+	buf : string;
+	
+	 # Weekday|Wday
+	 
+	(date,buf) = dateword(date);
+	tm.wday = dateindex(buf, wdayname);
+	if(tm.wday < 0)
+		tm.wday = dateindex(buf, weekdayname);
+
+	if(tm.wday < 0)
+		return 0;
+
+	 # check for the two major formats
+	
+	(date,buf) = dateword(date);
+	tm.mon = dateindex(buf, monname);
+	if(tm.mon >= 0){
+		 # MM
+		(date, tm.mday) = datenum(date);
+		if(tm.mday < 1 || tm.mday > 31)
+			return 0;
+
+		 # HH:MM:SS
+		(date, tm.hour) = datenum(date);
+		if(tm.hour < 0 || tm.hour >= 24)
+			return 0;
+		(date, tm.min) = datenum(date);
+		if(tm.min < 0 || tm.min >= 60)
+			return 0;
+		(date, tm.sec) = datenum(date);
+		if(tm.sec < 0 || tm.sec >= 60)
+			return 0;
+
+		
+		 # YYYY 
+		(nil, tm.year) = datenum(date);
+		if(tm.year < 70 || tm.year > 99 && tm.year < 1970)
+			return 0;
+		if(tm.year >= 1970)
+			tm.year -= 1900;
+	}else{
+		# MM-Mon-(YY|YYYY)
+		(date, tm.mday) = datenum(date);
+		if(tm.mday < 1 || tm.mday > 31)
+			return 0;
+		(date,buf) = dateword(date);
+		tm.mon = dateindex(buf, monname);
+		if(tm.mon < 0 || tm.mon >= 12)
+			return 0;
+		(date, tm.year) = datenum(date);
+		if(tm.year < 70 || tm.year > 99 && tm.year < 1970)
+			return 0;
+		if(tm.year >= 1970)
+			tm.year -= 1900;
+		
+		 # HH:MM:SS
+		(date, tm.hour) = datenum(date);
+		if(tm.hour < 0 || tm.hour >= 24)
+			return 0;
+		(date, tm.min) = datenum(date);
+		if(tm.min < 0 || tm.min >= 60)
+			return 0;
+		(date, tm.sec) = datenum(date);
+		if(tm.sec < 0 || tm.sec >= 60)
+			return 0;
+
+		 # timezone
+		(date,buf)=dateword(date);
+		if(buf[0:3]!="gmt")
+			return 0;
+	}
+
+	tm.zone="GMT";
+	return gmtm2sec(tm);
+}
+
+lowercase(name:string): string
+{
+	p: string;
+	for(i:=0;i<len name;i++)
+		p[i]=tolower(name[i]);
+	return p;
+}
+
+dateindex(d : string, tab : array of string): int
+{
+	for(i := 0; i < len tab; i++)
+		if (lowercase(tab[i]) == d)
+			return i;
+	return -1;
+}
+
+
+# compute seconds since Jan 1 1970 GMT
+
+gmtm2sec(tm:Tm): int 
+{
+	secs,i : int;
+	d2m: array of int;
+	secs=0;
+
+	#seconds per year
+	tm.year += 1900;
+	if(tm.year < 1970)
+		return 0;
+	for(i = 1970; i < tm.year; i++){
+		d2m = yrsize(i);
+		secs += d2m[0] * SEC2DAY;
+	}
+
+
+	#seconds per month
+	d2m = yrsize(tm.year);
+	for(i = 0; i < tm.mon; i++)
+		secs += d2m[i+1] * SEC2DAY;
+
+	#secs in last month
+	secs += (tm.mday-1) * SEC2DAY;
+
+	#hours, minutes, seconds	
+	secs += tm.hour * SEC2HOUR;
+	secs += tm.min * SEC2MIN;
+	secs += tm.sec;
+
+	return secs;
+}
--- /dev/null
+++ b/appl/svc/httpd/date.m
@@ -1,0 +1,11 @@
+
+Date: module{
+	PATH : con "/dis/svc/httpd/date.dis";
+
+	init: fn();
+	dateconv: fn(secs :int): string; # returns an http formatted
+					 # date representing secs.
+	date2sec: fn(foo:string): int;   # parses a date and returns
+					 # number of secs since the 
+					 # epoch that it represents. 
+};
--- /dev/null
+++ b/appl/svc/httpd/echo.b
@@ -1,0 +1,89 @@
+implement echo;
+
+include "sys.m";
+	sys: Sys;
+stderr: ref Sys->FD;
+include "bufio.m";
+
+include "draw.m";
+draw : Draw;
+
+include "cache.m";
+include "contents.m";
+include "httpd.m";
+	Private_info: import Httpd;
+
+include "cgiparse.m";
+cgiparse: CgiParse;
+
+echo: module
+{
+    init: fn(g: ref Private_info, req: Httpd->Request);
+};
+
+init(g: ref Private_info, req: Httpd->Request) 
+{	
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);	
+	cgiparse = load CgiParse CgiParse->PATH;
+	if( cgiparse == nil ) {
+		sys->fprint( stderr, "echo: cannot load %s: %r\n", CgiParse->PATH);
+		return;
+	}
+
+	send(g, cgiparse->cgiparse(g, req));
+}
+
+send(g: ref Private_info, cgidata: ref CgiData ) 
+{	
+	bufio := g.bufio;
+	Iobuf: import bufio;
+	if( cgidata == nil ){
+		g.bout.flush();
+		return;
+	}
+	
+	g.bout.puts( cgidata.httphd );
+	
+	g.bout.puts("<head><title>Echo</title></head>\r\n");
+	g.bout.puts("<body><h1>Echo</h1>\r\n");
+	g.bout.puts(sys->sprint("You requested a %s on %s", 
+	cgidata.method, cgidata.uri));
+	if(cgidata.search!=nil)
+		g.bout.puts(sys->sprint(" with search string %s", cgidata.search));
+	g.bout.puts(".\n");
+	
+	g.bout.puts("Your client sent the following headers:<p><pre>");
+	g.bout.puts( "Client: " + cgidata.remote + "\n" );
+	g.bout.puts( "Date: " + cgidata.tmstamp + "\n" );
+	g.bout.puts( "Version: " + cgidata.version + "\n" );
+	while( cgidata.header != nil ){
+		(tag, val) := hd cgidata.header;
+		g.bout.puts( tag + " " + val + "\n" );
+		cgidata.header = tl cgidata.header;
+	}
+	
+	g.bout.puts("</pre>\n");	
+	if (cgidata.form != nil){	
+		i := 0;
+		g.bout.puts("</pre>");
+		g.bout.puts("Your client sent the following form data:<p>");
+		g.bout.puts("<table>\n");
+		while(cgidata.form!=nil){	
+			(tag, val) := hd cgidata.form;
+			g.bout.puts(sys->sprint("<tr><td>%d</td><td><I> ",i));
+			g.bout.puts(tag);
+			g.bout.puts("</I></td> ");
+			g.bout.puts("<td><B> ");
+			g.bout.puts(val);
+			g.bout.puts("</B></td></tr>\n");
+			g.bout.puts("\n");
+			cgidata.form = tl cgidata.form;
+			i++;
+		}
+		g.bout.puts("</table>\n");
+	}	
+	g.bout.puts("</body>\n");
+	g.bout.flush();
+}
+
--- /dev/null
+++ b/appl/svc/httpd/httpd.b
@@ -1,0 +1,720 @@
+implement Httpd;
+
+include "sys.m";
+	sys: Sys;
+
+Dir: import sys;
+FD : import sys;
+
+include "draw.m";
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "string.m";
+	str: String;
+
+include "readdir.m";
+	readdir: Readdir;
+
+include "daytime.m";
+	daytime : Daytime;
+
+include "cache.m";
+	cache : Cache;
+
+include "contents.m";
+	contents: Contents;
+	Content: import contents;
+
+include "httpd.m";
+
+include "parser.m";
+	parser : Parser;
+
+include "date.m";
+	date: Date;
+
+include "redirect.m";
+	redir: Redirect;
+
+include "alarms.m";
+	alarms: Alarms;
+	Alarm: import alarms;
+
+# globals 
+
+cache_size: int;
+port := "80";
+addr: string;
+stderr : ref FD;
+dbg_log, logfile: ref FD;
+debug: int;
+my_domain: string;
+
+usage()
+{
+	sys->fprint(stderr, "usage: httpd [-c num] [-D] [-a servaddr]\n");
+	raise "fail:usage";
+}
+
+atexit(g: ref Private_info)
+{
+	debug_print(g,"At exit from httpd, closing fds. \n");
+	g.bin.close();	
+	g.bout.close();
+	g.bin=nil;
+	g.bout=nil;
+	exit;
+}
+
+debug_print(g : ref Private_info,message : string)
+{
+	if (g.dbg_log!=nil)
+		sys->fprint(g.dbg_log,"%s",message);
+}
+
+parse_args(args : list of string)
+{
+	while(args!=nil){
+		case (hd args){
+			"-c" =>
+				args = tl args;
+				cache_size = int hd args;
+			"-D" =>
+				debug=1;
+			"-p" =>
+				args = tl args;
+				port = hd args;
+			"-a" =>
+				args = tl args;
+				addr = hd args;
+		}
+		args = tl args;
+	}
+}
+
+badmod(m: string)
+{
+	sys->fprint(stderr, "httpd: cannot load %s: %r\n", m);
+	raise "fail:bad module";
+}
+
+init(nil: ref Draw->Context, argv: list of string)
+{	
+	# Load global modules.
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	bufio = load Bufio Bufio->PATH;	
+	if (bufio==nil) badmod(Bufio->PATH);
+
+	str = load String String->PATH;
+	if (str == nil) badmod(String->PATH);
+
+	date = load Date Date->PATH;
+	if(date == nil) badmod(Date->PATH);
+
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil) badmod(Readdir->PATH);
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil) badmod(Daytime->PATH);
+
+	contents = load Contents Contents->PATH;
+	if(contents == nil) badmod(Contents->PATH);
+
+	cache = load Cache Cache->PATH;
+	if(cache == nil) badmod(Cache->PATH);
+
+	alarms = load Alarms Alarms->PATH;
+	if(alarms == nil) badmod(Alarms->PATH);
+
+	redir = load Redirect Redirect->PATH;
+	if(redir == nil) badmod(Redirect->PATH);
+
+	parser = load Parser Parser->PATH;
+	if(parser == nil) badmod(Parser->PATH);
+
+	logfile=sys->create(HTTPLOG,Sys->ORDWR,8r666);
+	if (logfile==nil) {
+		sys->fprint(stderr, "httpd: cannot open %s: %r\n", HTTPLOG);
+		raise "cannot open http log";
+	}
+
+	# parse arguments to httpd.
+
+	cache_size=5000;
+	debug = 0;
+	parse_args(argv);
+	if (debug==1){
+		dbg_log=sys->create(DEBUGLOG,Sys->ORDWR,8r666);
+		if (dbg_log==nil){
+			sys->print("debug log open: %r\n");
+			exit;
+		}
+	}else 
+		dbg_log=nil;
+	sys->fprint(dbg_log,"started at %s \n",daytime->time());
+
+	# initialisation routines
+	contents->contentinit(dbg_log);
+	cache->cache_init(dbg_log,cache_size);
+	redir->redirect_init(REWRITE);
+	date->init();
+	parser->init();
+	my_domain=sysname();
+	if(addr == nil){
+		if(port != nil)
+			addr = "tcp!*!"+port;
+		else
+			addr = "tcp!*!80";
+	}
+	(ok, c) := sys->announce(addr);
+	if(ok < 0) {
+		sys->fprint(stderr, "can't announce %s: %r\n", addr);
+		exit;
+	}
+	sys->fprint(logfile,"************ Charon Awakened at %s\n",
+			daytime->time());
+	for(;;)
+		doit(c);
+	exit;
+}
+
+
+doit(c: Sys->Connection)
+{
+	(ok, nc) := sys->listen(c);
+	if(ok < 0) {
+		sys->fprint(stderr, "listen: %r\n");
+		exit;
+	}
+	if (dbg_log!=nil)
+		sys->fprint(dbg_log,"spawning connection.\n");
+	spawn service_req(nc);
+}
+
+service_req(nc : Sys->Connection)
+{
+	buf := array[64] of byte;
+	l := sys->open(nc.dir+"/remote", sys->OREAD);
+	n := sys->read(l, buf, len buf);
+	if(n >= 0)
+		if (dbg_log!=nil)
+			sys->fprint(dbg_log,"New client http: %s %s", nc.dir, 
+							string buf[0:n]);
+	#  wait for a call (or an error)
+	#  start a process for the service
+	g:= ref Private_info;
+	g.bufio = bufio;
+	g.dbg_log=dbg_log;
+	g.logfile = logfile;
+	g.modtime=0;
+	g.mydomain = my_domain;
+	g.version = "HTTP/1.0";
+	g.cache = cache;
+	g.okencode=nil;
+	g.oktype=nil;
+	g.getcerr="";
+	g.parse_eof=0;
+	g.eof=0;
+	g.remotesys=getendpoints(nc.dir);
+	debug_print(g,"opening in for "+string buf[0:n]+"\n");
+	g.bin= bufio->open(nc.dir+"/data",bufio->OREAD);
+	if (g.bin==nil){
+		sys->print("bin open: %r\n");
+		exit;
+	}
+	debug_print(g,"opening out for "+string buf[0:n]+"\n");
+	g.bout= bufio->open(nc.dir+"/data",bufio->OWRITE);
+	if (g.bout==nil){
+		sys->print("bout open: %r\n");
+		exit;
+	}
+	debug_print(g,"calling parsereq for "+string buf[0:n]+"\n");
+	parsereq(g);
+	atexit(g);
+}
+
+parsereq(g: ref Private_info)
+{
+	meth, v,magic,search,uri,origuri,extra : string;
+	# 15 minutes to get request line
+	a := Alarm.alarm(15*1000*60);
+	meth = getword(g);
+	if(meth == nil){
+		parser->logit(g,sys->sprint("no method%s", g.getcerr));
+		a.stop();
+		parser->fail(g,Syntax,"");
+	}
+	uri = getword(g);
+	if(uri == nil || len uri == 0){
+		parser->logit(g,sys->sprint("no uri: %s%s", meth, g.getcerr));
+		a.stop();
+		parser->fail(g,Syntax,"");
+	}
+	v = getword(g);
+	extra = getword(g);
+	a.stop();
+	if(extra != nil){
+			parser->logit(g,sys->sprint(
+				"extra header word '%s'%s", 
+					extra, g.getcerr));
+			parser->fail(g,Syntax,"");
+	}
+	case v {
+		"" =>
+			if(meth!="GET"){
+				parser->logit(g,sys->sprint("unimplemented method %s%s", meth, g.getcerr));
+				parser->fail(g,Unimp, meth);
+			}
+	
+		"HTTP/V1.0" or "HTTP/1.0" or "HTTP/1.1" =>
+			if((meth != "GET")  && (meth!= "HEAD") && (meth!="POST")){
+				parser->logit(g,sys->sprint("unimplemented method %s", meth));
+				parser->fail(g,Unimp, meth);
+			}	
+		* =>
+			parser->logit(g,sys->sprint("method %s uri %s%s", meth, uri, g.getcerr));
+			parser->fail(g,UnkVers, v);
+	}
+
+	# the fragment is not supposed to be sent
+	# strip it because some clients send it
+
+	(uri,extra) = str->splitl(uri, "#");
+	if(extra != nil)
+		parser->logit(g,sys->sprint("fragment %s", extra));
+	
+	 # munge uri for search, protection, and magic	 
+	(uri, search) = stripsearch(uri);
+	uri = compact_path(parser->urlunesc(uri));
+#	if(uri == SVR_ROOT)
+#		parser->fail(g,NotFound, "no object specified");
+	(uri, magic) = stripmagic(uri);
+	debug_print(g,"stripmagic=("+uri+","+magic+")\n");
+
+	 # normal case is just file transfer
+	if(magic == nil || (magic == "httpd")){
+		if (meth=="POST")
+			parser->fail(g,Unimp,meth);	# /magic does handles POST
+		g.host = g.mydomain;
+		origuri = uri;
+		parser->httpheaders(g,v);
+		uri = redir->redirect(origuri);
+		# must change this to implement proxies
+		if(uri==nil){
+			send(g,meth, v, origuri, search);
+		}else{
+			g.bout.puts(sys->sprint("%s 301 Moved Permanently\r\n", g.version));
+			g.bout.puts(sys->sprint("Date: %s\r\n", daytime->time()));
+			g.bout.puts("Server: Charon\r\n");
+			g.bout.puts("MIME-version: 1.0\r\n");
+			g.bout.puts("Content-type: text/html\r\n");
+			g.bout.puts(sys->sprint("URI: <%s>\r\n",parser->urlconv(uri)));
+			g.bout.puts(sys->sprint("Location: %s\r\n",parser->urlconv(uri)));
+			g.bout.puts("\r\n");
+			g.bout.puts("<head><title>Object Moved</title></head>\r\n");
+			g.bout.puts("<body><h1>Object Moved</h1>\r\n");
+			g.bout.puts(sys->sprint(
+				"Your selection moved to <a href=\"%s\"> here</a>.<p></body>\r\n",
+							 parser->urlconv(uri)));
+			g.bout.flush();
+		}
+		atexit(g);
+	}
+
+	# for magic we init a new program
+	do_magic(g,magic,uri,origuri,Request(meth, v, uri, search));
+}
+
+do_magic(g: ref Private_info,file, uri, origuri: string, req: Request)
+{
+	buf := sys->sprint("%s%s.dis", MAGICPATH, file);
+	debug_print(g,"looking for "+buf+"\n");
+	c:= load Cgi buf;
+	if (c==nil){
+		parser->logit(g,sys->sprint("no magic %s uri %s", file, uri));
+		parser->fail(g,NotFound, origuri);
+	}
+	{
+		c->init(g, req);
+	}
+	exception{
+		"fail:*" =>
+			return;
+	}
+}
+
+send(g: ref Private_info,name, vers, uri, search : string)
+{
+	typ,enc : ref Content;
+	w : string;
+	n, bad, force301: int;
+	if(search!=nil)
+		parser->fail(g,NoSearch, uri);
+
+	# figure out the type of file and send headers
+	debug_print( g, "httpd->send->open(" + uri + ")\n" );
+	fd := sys->open(uri, sys->OREAD);
+	if(fd == nil){
+		dbm := sys->sprint( "open failed: %r\n" );
+		debug_print( g, dbm );
+		notfound(g,uri);
+	}
+	(i,dir):=sys->fstat(fd);
+	if(i< 0)
+		parser->fail(g,Internal,"");
+	if(dir.mode & Sys->DMDIR){
+		(nil,p) := str->splitr(uri, "/");
+		if(p == nil){
+			w=sys->sprint("%sindex.html", uri);
+			force301 = 0;
+		}else{
+			w=sys->sprint("%s/index.html", uri);
+			force301 = 1; 
+		}
+		fd1 := sys->open(w, sys->OREAD);
+		if(fd1 == nil){
+			parser->logit(g,sys->sprint("%s directory %s", name, uri));
+			if(g.modtime >= dir.mtime)
+				parser->notmodified(g);
+			senddir(g,vers, uri, fd, ref dir);
+		} else if(force301 != 0 && vers != ""){
+			g.bout.puts(sys->sprint("%s 301 Moved Permanently\r\n", g.version));
+			g.bout.puts(sys->sprint("Date: %s\r\n", daytime->time()));
+			g.bout.puts("Server: Charon\r\n");
+			g.bout.puts("MIME-version: 1.0\r\n");
+			g.bout.puts("Content-type: text/html\r\n");
+			(nil, reluri) := str->splitstrr(parser->urlconv(w), SVR_ROOT);
+			g.bout.puts(sys->sprint("URI: </%s>\r\n", reluri));
+			g.bout.puts(sys->sprint("Location: http://%s/%s\r\n", 
+				parser->urlconv(g.host), reluri));
+			g.bout.puts("\r\n");
+			g.bout.puts("<head><title>Object Moved</title></head>\r\n");
+			g.bout.puts("<body><h1>Object Moved</h1>\r\n");
+			g.bout.puts(sys->sprint(
+				"Your selection moved to <a href=\"/%s\"> here</a>.<p></body>\r\n",
+					reluri));
+			atexit(g);
+		}
+		fd = fd1;
+		uri = w;
+		(i,dir)=sys->fstat(fd);
+		if(i < 0)
+			parser->fail(g,Internal,"");
+	}
+	parser->logit(g,sys->sprint("%s %s %d", name, uri, int dir.length));
+	if(g.modtime >= dir.mtime)
+		parser->notmodified(g);
+	n = -1;
+	if(vers != ""){
+		(typ, enc) = contents->uriclass(uri);
+		if(typ == nil)
+			typ = contents->mkcontent("application", "octet-stream");
+		bad = 0;
+		if(!contents->checkcontent(typ, g.oktype, "Content-Type")){
+			bad = 1;
+			g.bout.puts(sys->sprint("%s 406 None Acceptable\r\n", g.version));
+			parser->logit(g,"no content-type ok");
+		}else if(!contents->checkcontent(enc, g.okencode, "Content-Encoding")){
+			bad = 1;
+			g.bout.puts(sys->sprint("%s 406 None Acceptable\r\n", g.version));
+			parser->logit(g,"no content-encoding ok");
+		}else
+			g.bout.puts(sys->sprint("%s 200 OK\r\n", g.version));
+		g.bout.puts("Server: Charon\r\n");
+		g.bout.puts(sys->sprint("Last-Modified: %s\r\n", date->dateconv(dir.mtime)));
+		g.bout.puts(sys->sprint("Version: %uxv%ux\r\n", int dir.qid.path, dir.qid.vers));
+		g.bout.puts(sys->sprint("Message-Id: <%uxv%ux@%s>\r\n",
+			int dir.qid.path, dir.qid.vers, g.mydomain));
+		g.bout.puts(sys->sprint("Content-Type: %s/%s", typ.generic, typ.specific));
+
+#		if(typ.generic== "text")
+#			g.bout.puts(";charset=unicode-1-1-utf-8");
+
+		g.bout.puts("\r\n");
+		if(enc != nil){
+			g.bout.puts(sys->sprint("Content-Encoding: %s", enc.generic));
+			g.bout.puts("\r\n");
+		}
+		g.bout.puts(sys->sprint("Content-Length: %d\r\n", int dir.length));
+		g.bout.puts(sys->sprint("Date: %s\r\n", daytime->time()));
+		g.bout.puts("MIME-version: 1.0\r\n");
+		g.bout.puts("\r\n");
+		if(bad)
+			atexit(g);
+	}
+	if(name == "HEAD")
+		atexit(g);
+	# send the file if it's a normal file
+	g.bout.flush();
+	# find if its in hash....
+	# if so, retrieve, if not add..
+	conts : array of byte;
+	(i,conts) = cache->find(uri, dir.qid);
+	if (i==0){
+		# add to cache...
+		conts = array[int dir.length] of byte;
+		sys->seek(fd,big 0,0);
+		n = sys->read(fd, conts, len conts);
+		cache->insert(uri,conts, len conts, dir.qid);
+	}
+	sys->write(g.bout.fd, conts, len conts);
+}
+
+
+
+# classify a file
+classify(d: ref Dir): (ref Content, ref Content)
+{
+	typ, enc: ref Content;
+	
+	if(d.qid.qtype&sys->QTDIR)
+		return (contents->mkcontent("directory", nil),nil);
+	(typ, enc) = contents->uriclass(d.name);
+	if(typ == nil)
+		typ = contents->mkcontent("unknown ", nil);
+	return (typ, enc);
+}
+
+# read in a directory, format it in html, and send it back
+senddir(g: ref Private_info,vers,uri: string, fd: ref FD, mydir: ref Dir)
+{
+	myname: string;
+	myname = uri;
+	if (myname[len myname-1]!='/')
+		myname[len myname]='/';
+	(a, n) := readdir->readall(fd, Readdir->NAME);
+	if(vers != ""){
+		parser->okheaders(g);
+		g.bout.puts("Content-Type: text/html\r\n");
+		g.bout.puts(sys->sprint("Date: %s\r\n", daytime->time()));
+		g.bout.puts(sys->sprint("Last-Modified: %d\r\n", 
+				mydir.mtime));
+		g.bout.puts(sys->sprint("Message-Id: <%d%d@%s>\r\n",
+			int mydir.qid.path, mydir.qid.vers, g.mydomain));
+		g.bout.puts(sys->sprint("Version: %d\r\n", mydir.qid.vers));
+		g.bout.puts("\r\n");
+	}
+	g.bout.puts(sys->sprint("<head><title>Contents of directory %s.</title></head>\n",
+		uri));
+	g.bout.puts(sys->sprint("<body><h1>Contents of directory %s.</h1>\n",
+		uri));
+	g.bout.puts("<table>\n");
+	for(i := 0; i < n; i++){
+		(typ, enc) := classify(a[i]);
+		g.bout.puts(sys->sprint("<tr><td><a href=\"%s%s\">%s</A></td>",
+			myname, a[i].name, a[i].name));
+		if(typ != nil){
+			if(typ.generic!=nil)
+				g.bout.puts(sys->sprint("<td>%s", typ.generic));
+			if(typ.specific!=nil)
+				g.bout.puts(sys->sprint("/%s", 
+						typ.specific));
+			typ=nil;
+		}
+		if(enc != nil){
+			g.bout.puts(sys->sprint(" %s", enc.generic));
+			enc=nil;
+		}
+		g.bout.puts("</td></tr>\n");
+	}
+	if(n == 0)
+		g.bout.puts("<td>This directory is empty</td>\n");
+	g.bout.puts("</table></body>\n");
+	g.bout.flush();
+	atexit(g);
+}
+
+stripmagic(uri : string): (string, string)
+{
+	prog,newuri : string;
+	prefix := SVR_ROOT+"magic/";
+	if (!str->prefix(prefix,uri) || len newuri == len prefix)
+		return(uri,nil);
+	uri=uri[len prefix:];
+	(prog,newuri)=str->splitl(uri,"/");
+	return (newuri,prog);
+}
+
+stripsearch(uri : string): (string,string)
+{
+	search : string;
+	(uri,search) = str->splitl(uri, "?");
+	if (search!=nil)
+		search=search[1:];
+	return (uri, search);
+}
+
+# get rid of "." and ".." path components; make absolute
+compact_path(origpath:string): string
+{
+	if(origpath == nil)
+		origpath = "";
+	(origpath,nil) = str->splitl(origpath, "`;| "); # remove specials
+	(nil,olpath) := sys->tokenize(origpath, "/");
+	rlpath : list of string;
+	for(p := olpath; p != nil; p = tl p) {
+		if(hd p == "..") {
+			if(rlpath != nil)
+				rlpath = tl rlpath;
+		} else if(hd p != ".")
+			rlpath = (hd p) :: rlpath;
+	}
+	cpath := "";
+	if(rlpath!=nil){		
+		cpath = hd rlpath;
+		rlpath = tl rlpath;
+		while( rlpath != nil ) {
+			cpath = (hd rlpath) + "/" +  cpath;
+			rlpath = tl rlpath;
+		}
+	}
+	return SVR_ROOT + cpath;
+}
+
+getword(g: ref Private_info): string
+{
+	c: int;
+	while((c = getc(g)) == ' ' || c == '\t' || c == '\r')
+		;
+	if(c == '\n')
+		return nil;
+	buf := "";
+	for(;;){
+		case c{
+		' ' or '\t' or '\r' or '\n' =>
+			return buf;
+		}
+		buf[len buf] = c;
+		c = getc(g);
+	}
+}
+
+getc(g : ref Private_info): int
+{
+	# do we read buffered or unbuffered?
+	# buf : array of byte;
+	n : int;
+	if(g.eof){
+		debug_print(g,"eof is set in httpd\n");
+		return '\n';
+	}
+	n = g.bin.getc();
+	if (n<=0) { 
+		if(n == 0)
+			g.getcerr=": eof";
+		else
+			g.getcerr=sys->sprint(": n == -1: %r");
+		g.eof = 1;
+		return '\n';
+	}
+	n &= 16r7f;
+	if(n == '\n')
+		g.eof = 1;
+	return n;
+}
+
+# couldn't open a file
+# figure out why and return and error message
+notfound(g : ref Private_info,url : string)
+{
+	buf := sys->sprint("%r!");
+	(nil,chk):=str->splitstrl(buf, "file does not exist");
+	if (chk!=nil) 
+		parser->fail(g,NotFound, url);
+	(nil,chk)=str->splitstrl(buf,"permission denied");
+	if(chk != nil)
+		parser->fail(g,Unauth, url);
+	parser->fail(g,NotFound, url);
+}
+
+sysname(): string
+{
+	n : int;
+	fd : ref FD;
+	buf := array[128] of byte;
+	
+	fd = sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return "";
+	n = sys->read(fd, buf , len buf);
+	if(n <= 0)
+		return "";
+	
+	return string buf[0:n];
+}
+
+sysdom(): string
+{
+	dn : string;
+	dn = csquery("sys" , sysname(), "dom");
+	if(dn == nil)
+		dn = "who cares";
+	return dn; 
+}
+
+#  query the connection server
+csquery(attr, val, rattr : string): string
+{
+	token : string;
+	buf := array[4096] of byte;
+	fd : ref FD;
+	n: int;
+	if(val == "" ){
+		return nil;
+	}
+	fd = sys->open("/net/cs", sys->ORDWR);
+	if(fd == nil)
+		return nil;
+	sys->fprint(fd, "!%s=%s", attr, val);
+	sys->seek(fd, big 0, 0);
+	token = sys->sprint("%s=", rattr);
+	for(;;){
+		n = sys->read(fd, buf, len buf);
+		if(n <= 0)
+			break;
+		name:=string buf[0:n];
+		(nil,p) := str->splitstrl(name, token);
+		if(p != nil){	
+			(p,nil) = str->splitl(p, " \n");
+			if(p == nil)
+				return nil;
+			return p[4:];
+		}
+	}
+	return nil;
+}
+
+getendpoint(dir, file: string): (string, string)
+{
+	sysf := serv := "";
+	fto := sys->sprint("%s/%s", dir, file);
+	fd := sys->open(fto, sys->OREAD);
+
+	if(fd !=nil) {
+		buf := array[128] of byte;
+		n := sys->read(fd, buf, len buf);
+		if(n>0) {
+			buf = buf[0:n-1];
+			(sysf, serv) = str->splitl(string buf, "!");
+			if (serv != nil)
+				serv = serv[1:];
+		}
+	}
+	if(serv == nil)
+		serv = "unknown";
+	if(sysf == nil)
+		sysf = "unknown";
+	return (sysf, serv);
+}
+
+getendpoints(dir: string): string
+{
+#	(lsys, lserv) := getendpoint(dir, "local");
+	(rsys, nil) := getendpoint(dir, "remote");
+	return rsys;
+}
--- /dev/null
+++ b/appl/svc/httpd/httpd.m
@@ -1,0 +1,44 @@
+Httpd: module {
+	
+	Internal, TempFail, Unimp, UnkVers, BadCont, BadReq, Syntax, 
+	BadSearch, NotFound, NoSearch , OnlySearch, Unauth, OK : con iota;	
+	
+	SVR_ROOT : con "/services/httpd/root/";
+	HTTPLOG : con "/services/httpd/httpd.log";
+	DEBUGLOG : con "/services/httpd/httpd.debug";
+	HTTP_SUFF : con "/services/httpd/httpd.suff";
+	REWRITE   : con "/services/httpd/httpd.rewrite";
+	MAGICPATH : con "/dis/svc/httpd/"; # must end in /
+	
+	Private_info : adt{
+		# used in parse and httpd
+		bufio: Bufio;
+		bin,bout : ref Bufio->Iobuf;
+		logfile,dbg_log : ref Sys->FD;
+		cache : Cache;
+		eof : int;
+		getcerr : string;
+		version : string;
+		okencode, oktype : list of ref Contents->Content;
+		host : string; # initialized to mydomain just 	
+			       # before parsing header
+		remotesys, referer : string;
+		modtime : int;
+		# used by /magic for reading body
+		clength : int;
+		ctype : string;
+		#only used in parse
+		wordval : string;
+		tok,parse_eof : int;
+		mydomain,client : string;
+		oklang : list of ref Contents->Content;
+	};
+	Request: adt {
+		method, version, uri, search: string;
+	};
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Cgi: module{
+	init: fn(g: ref Httpd->Private_info, req: Httpd->Request);
+};
--- /dev/null
+++ b/appl/svc/httpd/httpd.rewrite
@@ -1,0 +1,10 @@
+# syntax: pattern replacement
+# The most specific pattern wins, and is applied once (no rescanning).
+# Leave off trailing slash if pattern is a directory.
+# Replacements starting with http:// return "Permanently moved" 
+# message.
+# e.g the following line aliases requests for / to requests for
+# /usr/mig/html
+
+# /   /usr/mig/html
+
--- /dev/null
+++ b/appl/svc/httpd/httpd.suff
@@ -1,0 +1,110 @@
+#suffix		generic type	specific type	encoding
+.C		text		plain		-		# C++ program
+.Z		-		-		x-compress
+.a		application	octet-stream	-		# [Mosaic]
+.ada		text		plain		-		# ada program
+.ai		application	postscript	-		# [Mosaic]
+.aif		audio		x-aiff		-
+.aifc		audio		x-aiff		-
+.aiff		audio		x-aiff		-
+.au		audio		basic		-		# sun audio
+.avi		video		x-msvideo	-		# [Mosaic]
+.awk		text		plain		-		# awk program
+.bas		text		plain		-		# basic program
+.bbl		text		plain		-		# BibTex output
+.bcpio		application	x-bcpio		-		# [Mosaic]
+.bib		text		plain		-		# BibTex input
+.c		text		plain		-		# C program
+.c++		text		plain		-		# C++ program
+.cc		text		plain		-		# [Mosaic]
+.cdf		application	x-netcdf	-
+.cpio		application	x-cpio		-
+.cpp		text		plain		-		# DOS C++ program
+.dat		text		plain		-		# AMPL et al.
+.diff		text		plain		-
+.dvi		application	x-dvi		-		# TeX output
+.enc		application	octet-stream	-		# encrypted file
+.eps		application	postscript	-
+.etx		text		x-setext	-		# [Mosaic]
+.exe		application	octet-stream	-		# DOS executable
+.executable	application	octet-stream	-		# DOS executable
+.exz		application	octet-stream	x-gzip		# gziped DOS executable
+.f		text		plain		-		# fortran-77 program
+.flc		video		x-flc		-
+.fli		video		x-fli		-
+.gif		image		gif		-
+.gtar		application	x-gtar		-		# [Mosaic]
+.gz		-		-		x-gzip		# gziped file
+.h		text		plain		-		# C header file
+.hdf		application	x-hdf		-
+.hqx		application	octet-stream	-		# Mac BinHex
+.htm		text		html		-
+.html		text		html		-
+.ief		image		ief		-		# [Mosaic]
+.jfif		image		jpeg		-		# [Mosaic]
+.jfif-tbnl	image		jpeg		-		# [Mosaic]
+.jpe		image		jpeg		-		# [Mosaic]
+.jpeg		image		jpeg		-
+.jpg		image		jpeg		-
+.latex		application	x-latex		-		# [Mosaic]
+.ltx		application	x-latex		-
+.man		application	x-troff-man	-		# [Mosaic]
+.me		application	x-troff-me	-		# [Mosaic]
+.mime		message		rfc822		-		# [Mosaic]
+.mod		text		plain		-		# AMPL et al.
+.mov		video		quicktime	-		# [Mosaic]
+.movie		video		x-sgi-movie	-		# [Mosaic]
+.mpe		video		mpeg		-		# [Mosaic]
+.mpeg		video		mpeg		-
+.mpg		video		mpeg		-
+.ms		application	x-troff-ms	-		# [Mosaic]
+.mv		video		x-sgi-movie	-		# [Mosaic]
+.nc		application	x-netcdf	-		# [Mosaic]
+.o		application	octet-stream	-		# [Mosaic]
+.oda		application	oda		-		# [Mosaic]
+.pbm		image		x-portable-bitmap	-	# [Mosaic]
+.pdf		application	pdf		-		# Adobe Portable Document Format
+.pgm		image		x-portable-graymap	-	# [Mosaic]
+.pl		text		plain		-		# [Mosaic]
+.pnm		image		x-portable-anymap	-	# [Mosaic]
+.ppm		image		x-portable-pixmap	-	# [Mosaic]
+.ps		application	postscript	-
+.qt		video		quicktime	-		# [Mosaic]
+.r		text		plain		-		# ratfor program
+.ras		image		x-cmu-rast	-		# [Mosaic]
+.rc		text		plain		-		# rc
+.rfr		text		plain		-		# refer
+.rgb		image		x-rgb		-		# [Mosaic]
+.roff		application	x-troff		-		# [Mosaic]
+.rtf		application	rtf		-		# [Mosaic]
+.rtx		text		richtext 	-	# MIME richtext	  [Mosaic]
+.sh		application	x-shar		-
+.shar		application	x-shar		-
+.snd		audio		basic		-
+.sv4cpio	application	x-sv4cpio	-		# [Mosaic]
+.sv4crc		application	x-sv4crc	-		# [Mosaic]
+.t		application	x-troff		-		# [Mosaic]
+.tar		application	x-tar		-		# [Mosaic]
+.taz		application	x-tar		x-compress
+.tcl		application	x-tcl		-
+.tex		application	x-tex		-		# Tex input
+.texi		application	x-texinfo	-		# [Mosaic]
+.texinfo	application	x-texinfo	-		# [Mosaic]
+.text		text		plain		-		# [Mosaic]
+.tgz		application	x-tar		x-gzip
+.tif		image		tiff		-
+.tiff		image		tiff		-
+.toc		text		plain		-		# table of contents
+.tr		application	x-troff		-		# [Mosaic]
+.trz		application	x-tar		x-compress
+.tsv		text		tab-separated-values	-	# [Mosaic]
+.txt		text		plain		-
+.ustar		application	x-ustar		-		# [Mosaic]
+.wav		audio		x-wav		-
+.wsrc		application	x-wais-source	-		# [Mosaic]
+.xbm		image		x-xbitmap	-		# X bitmap
+.xpm		image		x-xpixmap	-		# [Mosaic]
+.xwd		image		x-xwindowdump	-		# [Mosaic]
+.z		-		-		x-compress
+.Z		-		-		x-compress
+.zip		application	zip		-
--- /dev/null
+++ b/appl/svc/httpd/imagemap.b
@@ -1,0 +1,251 @@
+implement imagemap;
+
+include "sys.m";
+	sys : Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: 	import bufio;
+
+include "draw.m";
+	draw: Draw;
+
+include "cache.m";
+include "contents.m";
+
+include "httpd.m";
+	Private_info: import Httpd;
+	OnlySearch, BadSearch, NotFound: import Httpd;
+	g : ref Private_info;
+
+include "parser.m";
+	parser : Parser;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "string.m";
+	str : String;
+
+imagemap : module
+{
+	init: fn(g : ref Private_info, req: Httpd->Request);
+};
+
+Point : adt {
+	x,y : int;
+};
+
+me : string;
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "imagemap: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(k : ref Private_info, req: Httpd->Request)
+{
+	dest, s : string;
+	sys = load Sys "$Sys";
+	draw = load Draw "$Draw";
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil) badmod(Daytime->PATH);
+
+	parser = load Parser Parser->PATH;
+	if(parser == nil) badmod(Parser->PATH);
+
+	str = load String String->PATH;
+	if (str == nil) badmod(String->PATH);
+
+	me = "imagemap";
+
+	g=k;
+	bufio=g.bufio;
+	parser->init();
+	parser->httpheaders(g,req.version);
+	dest = translate(req.uri, req.search);
+	if(dest == nil){
+		if(req.version!= ""){
+			parser->okheaders(g);
+			g.bout.puts(sys->sprint("Date: %s\r\n", daytime->time()));
+			g.bout.puts("Content-type: text/html\r\n");
+			g.bout.puts("\r\n");
+		}
+		g.bout.puts("<head><title>Nothing Found</title></head><body>\n");
+		g.bout.puts("Nothing satisfying your search request could be found.\n</body>\n");
+		return;
+	}
+
+	g.bout.puts(sys->sprint("%s 301 Moved Permanently\r\n", g.version));
+	g.bout.puts(sys->sprint("Date: %s\r\n", daytime->time()));
+	g.bout.puts("Server: Charon\r\n");
+	g.bout.puts("MIME-version: 1.0\r\n");
+	g.bout.puts("Content-type: text/html\r\n");
+	(s,nil)=str->splitl(dest, ":");
+	if(s!=nil){
+		g.bout.puts(sys->sprint("URI: <%s>\r\n", parser->urlconv(dest)));
+		g.bout.puts(sys->sprint("Location: %s\r\n", parser->urlconv(dest)));
+	}else if(dest[0] == '/'){
+		g.bout.puts(sys->sprint("URI: <%s>\r\n",parser->urlconv(dest)));
+		g.bout.puts(sys->sprint("Location: http://%s%s\r\n", parser->urlconv(g.mydomain), parser->urlconv(dest)));
+	} else {
+		(req.uri,s) = str->splitr(req.uri, "/");
+		g.bout.puts(sys->sprint("URI: <%s/%s>\r\n", parser->urlconv(req.uri), parser->urlconv(dest)));
+		g.bout.puts(sys->sprint("Location: http://%s%s/%s\r\n", parser->urlconv(g.mydomain), parser->urlconv(req.uri), parser->urlconv(dest)));
+	}
+	g.bout.puts("\r\n");
+	g.bout.puts("<head><title>Object Moved</title></head>\r\n");
+	g.bout.puts("<body><h1>Object Moved</h1></body>\r\n");
+	if(dest[0] == '/')
+		g.bout.puts(sys->sprint("Your selection mapped to <a href=\"%s\"> here</a>.<p>\r\n", parser->urlconv(dest)));
+	else
+		g.bout.puts(sys->sprint("Your selection mapped to <a href=\"%s/%s\"> here</a>.<p>\r\n", parser->urlconv(req.uri), parser->urlconv(dest)));
+}
+
+
+translate(uri, search : string) : string
+{
+	b : ref Iobuf;
+	p, c, q, start : Point;
+	close, d : real;
+	line, To, def, s : string;
+	ok, n, inside, r : int;
+	(pth,nil):=str->splitr(uri,"/");
+	# sys->print("pth is %s",pth);
+	if(search == nil)
+		parser->fail(g,OnlySearch, me);
+	(p, ok) = pt(search);
+	if(!ok)
+		parser->fail(g,BadSearch, me);
+
+	b = bufio->open(uri, bufio->OREAD);
+	if(b == nil){
+		sys->fprint(sys->fildes(2), "imagemap: cannot open %s: %r\n", uri);
+		parser->fail(g, NotFound, uri);
+	}
+	To = "";
+	def = "";
+	close = 0.;
+	while((line = b.gets('\n'))!=nil){
+		line=line[0:len line-1];
+
+		(s, line) = getfield(line);
+		if(s== "rect"){
+			(s, line) = getfield(line);
+			(q, ok) = pt(s);
+			if(!ok || q.x > p.x || q.y > p.y)
+				continue;
+			(s, line) = getfield(line);
+			(q, ok) = pt(s);
+			if(!ok || q.x < p.x || q.y < p.y)
+				continue;
+			(s, nil) = getfield(line);
+			return pth+s;
+		}else if(s== "circle"){
+			(s, line) = getfield(line);
+			(c, ok) = pt(s);
+			if(!ok)
+				continue;
+			(s, line) = getfield(line);
+			(r,nil) = str->toint(s,10);
+			(s, line) = getfield(line);
+			d = real (r * r);
+			if(d >= dist(p, c))
+				return pth+s;
+		}else if(s=="poly"){
+			(s, line) = getfield(line);
+			(start, ok) = pt(s);
+			if(!ok)
+				continue;
+			inside = 0;
+			c = start;
+			for(n = 1; ; n++){
+				(s, line) = getfield(line);
+				(q, ok) = pt(s);
+				if(!ok)
+					break;
+				inside = polytest(inside, p, c, q);
+				c = q;
+			}
+			inside = polytest(inside, p, c, start);
+			if(n >= 3 && inside)
+				return pth+s;
+		}else if(s== "point"){
+			(s, line) = getfield(line);
+			(q, ok) = pt(s);
+			if(!ok)
+				continue;
+			d = dist(p, q);
+			(s, line) = getfield(line);
+			if(d == 0.)
+				return pth+s;
+			if(close == 0. || d < close){
+				close = d;
+				To = s;
+			}
+		}else if(s ==  "default"){
+			(def, line) = getfield(line);
+		}
+	}
+	if(To == nil)
+		To = def;
+	return pth+To;
+}
+
+
+polytest(inside : int,p, b, a : Point) : int{
+	pa, ba : Point;
+
+	if(b.y>a.y){
+		pa=sub(p, a);
+		ba=sub(b, a);
+	}else{
+		pa=sub(p, b);
+		ba=sub(a, b);
+	}
+	if(0<=pa.y && pa.y<ba.y && pa.y*ba.x<=pa.x*ba.y)
+		inside = !inside;
+	return inside;
+}
+
+
+sub(p, q : Point) : Point {
+	return (Point)(p.x-q.x, p.y-q.y);
+}
+
+
+dist(p, q : Point) : real {
+	p.x -= q.x;
+	p.y -= q.y;
+	return real (p.x * p.x + p.y *p.y);
+}
+
+
+pt(s : string) : (Point, int) {
+	p : Point;
+	x, y : string;
+
+	if(s[0] == '(')
+		s=s[1:];
+	(s,nil)=str->splitl(s, ")");
+	p = Point(0, 0);
+	(x,y) = str->splitl(s, ",");
+	if(x == s)
+		return (p, 0);
+	(p.x,nil) = str->toint(x,10);
+	if(y==nil)
+		return (p, 0);
+	y=y[1:];
+	(p.y,nil) = str->toint(y, 10);
+	return (p, 1);
+}
+
+
+getfield(s : string) : (string,string) {
+	i:=0;
+	while(s[i] == '\t' || s[i] == ' ')
+		i++;
+	return str->splitl(s[i:],"\t ");
+}
--- /dev/null
+++ b/appl/svc/httpd/mkfile
@@ -1,0 +1,50 @@
+<../../../mkconfig
+
+TARG=	cache.dis\
+	contents.dis\
+	date.dis\
+	echo.dis\
+	httpd.dis\
+	imagemap.dis\
+	parser.dis\
+	redirect.dis\
+	stats.dis\
+	alarms.dis\
+	cgiparse.dis\
+
+
+MODULES=\
+	cache.m\
+	contents.m\
+	date.m\
+	httpd.m\
+	parser.m\
+	redirect.m\
+	alarms.m\
+	cgiparse.m\
+
+SYSMODULES=
+
+LOGS=	httpd.debug\
+	httpd.log\
+	httpd.rewrite\
+	httpd.suff\
+
+DISBIN=$ROOT/dis/svc/httpd
+
+<$ROOT/mkfiles/mkdis
+
+install:V: 	install-logs-$SHELLTYPE
+
+install-logs-rc install-logs-nt:V:
+	for (i in $LOGS){
+		rm -f $ROOT/services/httpd/$i && cp $i $ROOT/services/httpd/$i
+	}
+	# chmod 644 $ROOT/services/httpd/httpd.log
+
+install-logs-sh:V:
+	for i in $LOGS
+	do
+		rm -f $ROOT/services/httpd/$i && cp $i $ROOT/services/httpd/$i
+	done
+	# chmod 644 $ROOT/services/httpd/httpd.log
--- /dev/null
+++ b/appl/svc/httpd/parser.b
@@ -1,0 +1,593 @@
+implement Parser;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+include "bufio.m";
+include "string.m";
+	str: String;
+include "daytime.m";
+	daytime: Daytime;
+include "contents.m";
+	contents : Contents;
+	Content: import contents;
+include "cache.m";
+include "httpd.m";
+	Private_info: import Httpd;
+	Internal, TempFail, Unimp, UnkVers, BadCont, BadReq, Syntax, 
+	BadSearch, NotFound, NoSearch , OnlySearch, Unauth, OK : import Httpd;	
+include "parser.m";
+include "date.m";
+	date : Date;
+include "alarms.m";
+	alarms: Alarms;
+	Alarm: import alarms;
+include "lock.m";
+	locks: Lock;
+	Semaphore: import locks;
+
+Error: adt {
+	num : string;
+	concise: string;
+	verbose: string;
+};
+
+errormsg := array[] of {
+	Internal => Error("500 Internal Error", "Internal Error",
+		"This server could not process your request due to an interal error."),
+	TempFail =>	Error("500 Internal Error", "Temporary Failure",
+		"The object %s is currently inaccessible.<p>Please try again later."),
+	Unimp =>	Error("501 Not implemented", "Command not implemented",
+		"This server does not implement the %s command."),
+	UnkVers =>	Error("501 Not Implemented", "Unknown http version",
+		"This server does not know how to respond to http version %s."),
+	BadCont =>	Error("501 Not Implemented", "Impossible format",
+		"This server cannot produce %s in any of the formats your client accepts."),
+	BadReq =>	Error("400 Bad Request", "Strange Request",
+		"Your client sent a query that this server could not understand."),
+	Syntax =>	Error("400 Bad Request", "Garbled Syntax",
+		"Your client sent a query with incoherent syntax."),
+	BadSearch =>Error("400 Bad Request", "Inapplicable Search",
+		"Your client sent a search that cannot be applied to %s."),
+	NotFound =>Error("404 Not Found", "Object not found",
+		"The object %s does not exist on this server."),
+	NoSearch =>	Error("403 Forbidden", "Search not supported",
+		"The object %s does not support the search command."),
+	OnlySearch =>Error("403 Forbidden", "Searching Only",
+		"The object %s only supports the searching methods."),
+	Unauth =>	Error("401 Unauthorized", "Unauthorized",
+		"You are not authorized to see the object %s."),
+	OK =>	Error("200 OK", "everything is fine","Groovy man"),
+};	
+
+badmodule(p: string)
+{
+	sys->fprint(sys->fildes(2), "parse: cannot load %s: %r", p);
+	raise "fail:bad module";
+}
+
+lock: ref Semaphore;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+
+	date = load Date Date->PATH;
+	if (date==nil) badmodule(Date->PATH);
+
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil) badmodule(Daytime->PATH);
+
+	contents = load Contents Contents->PATH;
+	if(contents == nil) badmodule(Contents->PATH);
+
+	str = load String String->PATH;
+	if(str == nil) badmodule(String->PATH);
+
+	alarms = load Alarms Alarms->PATH;
+	if(alarms == nil) badmodule(Alarms->PATH);
+
+	locks = load Lock Lock->PATH;
+	if(locks == nil) badmodule(Lock->PATH);
+	locks->init();
+	lock = Semaphore.new();
+	date->init();
+}
+
+atexit(g: ref Private_info)
+{
+	if (g.dbg_log!=nil){
+		sys->fprint(g.dbg_log,"At exit from parse, closing fds. \n");
+	}
+	if (g.bin!=nil)
+		g.bufio->g.bin.close();
+	if (g.bout!=nil)
+		g.bufio->g.bout.close();
+	g.bin=nil;
+	g.bout=nil;
+	exit;
+}
+
+
+httpheaders(g: ref Private_info,vers : string)
+{
+	if(vers == "")
+		return;
+	g.tok = '\n';
+	# 15 minutes to get request line
+	a := Alarm.alarm(15*1000*60); 
+	while(lex(g) != '\n'){
+		if(g.tok == Word && lex(g) == ':'){
+			if (g.dbg_log!=nil)
+				sys->fprint(g.dbg_log,"hitting parsejump. wordval is %s\n",
+										g.wordval);
+			parsejump(g,g.wordval);
+		}
+		while(g.tok != '\n')
+			lex(g);
+	}
+	a.stop();
+}
+
+
+mimeok(g: ref Private_info,name : string,multipart : int,head : list of ref Content): list of ref Content
+{
+
+	generic, specific, s : string;
+	v : real;
+
+	while(lex(g) != Word)
+		if(g.tok != ',')
+			return head;
+
+	generic = g.wordval;
+	lex(g);
+	if(g.tok == '/' || multipart){
+		if(g.tok != '/')
+			return head;
+		if(lex(g) != Word)
+			return head;
+		specific = g.wordval;
+		lex(g);
+	}else
+		specific = "*";
+	tmp := contents->mkcontent(generic, specific);
+	head = tmp::head;
+	for(;;){
+		case g.tok {
+		';' =>
+			if(lex(g) == Word){
+				s = g.wordval;
+				if(lex(g) != '=' || lex(g) != Word)
+					return head;
+				v = 3.14; # should be strtof(g.wordval, nil);
+				if(s=="q")
+					tmp.q = v;
+				else
+					logit(g,sys->sprint(
+						"unknown %s param: %s %s",
+						name, s, g.wordval));
+			}
+			break;
+		',' =>
+			return  mimeok(g,name, multipart,head);
+		* =>
+			return head;
+		}
+		lex(g);
+	}
+	return head;
+}
+
+mimeaccept(g: ref Private_info,name : string)
+{
+	g.oktype = mimeok(g,name, 1, g.oktype);
+}
+
+mimeacceptenc(g: ref Private_info,name : string)
+{
+	g.okencode = mimeok(g,name, 0, g.okencode);
+}
+
+mimeacceptlang(g: ref Private_info,name : string)
+{
+	g.oklang = mimeok(g,name, 0, g.oklang);
+}
+
+mimemodified(g: ref Private_info,name : string)
+{
+	lexhead(g);
+	g.modtime = date->date2sec(g.wordval);
+	if (g.dbg_log!=nil){
+		sys->fprint(g.dbg_log,"modtime %d\n",g.modtime);
+	}
+	if(g.modtime == 0)
+		logit(g,sys->sprint("%s: %s", name, g.wordval));
+}
+
+
+mimeagent(g: ref Private_info,nil : string)
+{
+	lexhead(g);
+	g.client = g.wordval;
+}
+
+mimefrom(g: ref Private_info,nil : string)
+{
+	lexhead(g);
+}
+
+
+mimehost(g: ref Private_info,nil : string)
+{
+	h : string;
+	lexhead(g);
+	(nil,h)=str->splitr(g.wordval," \t");
+	g.host = h;
+}
+
+mimereferer(g: ref Private_info,nil : string)
+{
+	h : string;
+	lexhead(g);
+	(nil,h)=str->splitr(g.wordval," \t");
+	g.referer = h;
+}
+
+mimeclength(g: ref Private_info,nil : string)
+{
+	h : string;
+	lexhead(g);
+	(nil,h)=str->splitr(g.wordval," \t");
+	g.clength = int h;
+}
+
+mimectype(g: ref Private_info,nil : string)
+{
+	h : string;
+	lexhead(g);
+	(nil,h)=str->splitr(g.wordval," \t");
+	g.ctype = h;
+}
+
+
+mimeignore(g: ref Private_info,nil : string)
+{
+	lexhead(g);
+}
+
+
+mimeunknown(g: ref Private_info,name : string)
+{
+	lexhead(g);
+	if(g.client!="")
+		logit(g,sys->sprint("agent %s: ignoring header %s: %s ", 
+			g.client, name, g.wordval));
+	else
+		logit(g,sys->sprint("ignoring header %s: %s", name, g.wordval));
+}
+
+
+parsejump(g: ref Private_info,k : string)
+{
+	case k { 
+
+	"from" =>		
+		mimefrom(g,k);
+	"if-modified-since" =>	
+		mimemodified(g,k);
+	"accept" =>		
+		mimeaccept(g,k);
+	"accept-encoding" =>	
+		mimeacceptenc(g,k);
+	"accept-language" =>	
+		mimeacceptlang(g,k);
+	"user-agent" =>		
+		mimeagent(g,k);
+	"host" =>		
+		mimehost(g,k);
+	"referer" =>		
+		mimereferer(g,k);
+	"content-length" =>
+		mimeclength(g,k);
+	"content-type" =>
+		mimectype(g,k);
+	"authorization" or "chargeto" or "connection" or "forwarded" or
+	"pragma" or "proxy-agent" or "proxy-connection" or
+	"x-afs-tokens" or "x-serial-number" =>	
+		mimeignore(g,k);
+	* =>				
+		mimeunknown(g,k);
+	};	
+}
+
+lex(g: ref Private_info): int
+{
+	g.tok = lex1(g);
+	return g.tok;
+}
+
+
+# rfc 822/rfc 1521 lexical analyzer
+lex1(g: ref Private_info): int
+{
+	level, c : int;
+	if(g.parse_eof)
+		return '\n';
+
+# top:
+	for(;;){
+		c = getc(g);
+		case c {
+			 '(' =>
+				level = 1;
+				while((c = getc(g)) != Bufio->EOF){
+					if(c == '\\'){
+						c = getc(g);
+						if(c == Bufio->EOF)
+							return '\n';
+						continue;
+					}
+					if(c == '(')
+						level++;
+					else if(c == ')' && level == 1){
+						level--;
+						break;
+					}
+					else if(c == '\n'){
+						c = getc(g);
+						if(c == Bufio->EOF)
+							return '\n';
+							break;
+						if(c != ' ' && c != '\t'){
+							ungetc(g);
+							return '\n';
+						}
+					}
+				}
+	 		' ' or '\t' or '\r' =>
+				break;
+	 		'\n' =>
+				if(g.tok == '\n'){
+					g.parse_eof = 1;
+					return '\n';
+				}
+				c = getc(g);
+				if(c == Bufio->EOF)
+					return '\n';
+				if(c != ' ' && c != '\t'){
+					ungetc(g);
+					return '\n';
+				}
+			')' or '<' or '>' or '[' or ']' or '@' or '/' or ',' 
+			or ';' or ':' or '?' or '=' =>
+				return c;
+
+	 		'"' =>
+				word(g,"\"");
+				getc(g);		# skip the closing quote 
+				return Word;
+
+	 		* =>
+				ungetc(g);
+				word(g,"\"()<>@,;:/[]?=\r\n \t");
+				return Word;
+			}
+	}
+	return 0;	
+}
+
+# return the rest of an rfc 822, not including \r or \n
+# do not map to lower case
+
+lexhead(g: ref Private_info)
+{
+	c, n: int;
+	n = 0;
+	while((c = getc(g)) != Bufio->EOF){
+		if(c == '\r')
+			c = wordcr(g);
+		else if(c == '\n')
+			c = wordnl(g);
+		if(c == '\n')
+			break;
+		if(c == '\\'){
+			c = getc(g);
+			if(c == Bufio->EOF)
+				break;
+		}
+		g.wordval[n++] = c;
+	}
+	g.tok = '\n';
+	g.wordval= g.wordval[0:n];
+}
+
+word(g: ref Private_info,stop : string)
+{
+	c : int;
+	n := 0;
+	while((c = getc(g)) != Bufio->EOF){
+		if(c == '\r')
+			c = wordcr(g);
+		else if(c == '\n')
+			c = wordnl(g);
+		if(c == '\\'){
+			c = getc(g);
+			if(c == Bufio->EOF)
+				break;
+		}else if(str->in(c,stop)){
+				ungetc(g);
+				g.wordval = g.wordval[0:n];	
+				return;
+			}
+		if(c >= 'A' && c <= 'Z')
+			c += 'a' - 'A';
+		g.wordval[n++] = c;
+	}
+	g.wordval = g.wordval[0:n];
+	# sys->print("returning from word");
+}
+
+
+wordcr(g: ref Private_info): int
+{
+	c := getc(g);
+	if(c == '\n')
+		return wordnl(g);
+	ungetc(g);
+	return ' ';
+}
+
+
+wordnl(g: ref Private_info): int
+{
+	c := getc(g);
+	if(c == ' ' || c == '\t')
+		return c;
+	ungetc(g);
+	return '\n';
+}
+
+
+getc(g: ref Private_info): int
+{
+	c := g.bufio->g.bin.getc();
+	if(c == Bufio->EOF){
+		g.parse_eof = 1;
+		return c;
+	}
+	return c & 16r7f;
+}
+
+ungetc(g: ref Private_info)
+{
+	# this is a dirty hack, I am tacitly assuming that characters read
+	# from stdin will be ASCII.....
+	g.bufio->g.bin.ungetc();
+}
+
+# go from url with ascii and %xx escapes to unicode, allowing for existing unencoded utf-8
+
+urlunesc(s : string): string
+{
+	a := array[Sys->UTFmax*len s] of byte;
+	o := 0;
+	for(i := 0; i < len s; i++){
+		c := int s[i];
+		if(c < Runeself){
+			if(c == '%' && i+2 < len s){
+				d0 := hex(int s[i+1]);
+				if(d0 >= 0){
+					d1 := hex(int s[i+2]);
+					if(d1 >= 0){
+						i += 2;
+						c = d0*16 + d1;
+					}
+				}
+			} else if(c == '+'  || c == 0)
+				c = ' ';
+			a[o++] = byte c;
+		}else
+			o += sys->char2byte(c, a, o);
+	}
+	return string a[0: o];
+}
+
+hex(c: int): int
+{
+	if(c >= '0' && c <= '9')
+		return c-'0';
+	if(c >= 'a' && c <= 'f')
+		return c-'a' + 10;
+	if(c >= 'A' && c <= 'F')
+		return c-'A' + 10;
+	return -1;
+}
+
+# write a failure message to the net and exit
+fail(g: ref Private_info,reason : int, message : string)
+{
+	verb : string;
+	title:=sys->sprint("<head><title>%s</title></head>\n<body bgcolor=#ffffff>\n",
+					errormsg[reason].concise);
+	body1:=	"<h1> Error </h1>\n<P>" +
+		"Sorry, Charon is unable to process your request. The webserver reports"+
+		" the following error <P><b>";
+	#concise error
+	body2:="</b><p>for the URL\n<P><b>";
+	#message
+	body3:="</b><P>with the following reason:\n<P><b>";
+	#reason
+	if (str->in('%',errormsg[reason].verbose)){
+		(v1,v2):=str->splitl(errormsg[reason].verbose,"%");
+		verb=v1+message+v2[2:];
+	}else
+		verb=errormsg[reason].verbose;
+	body4:="</b><hr> This Webserver powered by <img src=\"/inferno.gif\">. <P>"+
+		"For more information click <a href=\"http://inferno.lucent.com\"> here </a>\n"+
+		"<hr><address>\n";
+	dtime:=sys->sprint("This information processed at %s.\n",daytime->time());
+	body5:="</address>\n</body>\n";
+	strbuf:=title+body1+errormsg[reason].concise+body2+message+body3+
+		verb+body4+dtime+body5;
+	if (g.bout!=nil && reason!=2){
+		g.bufio->g.bout.puts(sys->sprint("%s %s\r\n", g.version, errormsg[reason].num));
+		g.bufio->g.bout.puts(sys->sprint("Date: %s\r\n", daytime->time()));
+		g.bufio->g.bout.puts(sys->sprint("Server: Charon\r\n"));
+		g.bufio->g.bout.puts(sys->sprint("MIME-version: 1.0\r\n"));
+		g.bufio->g.bout.puts(sys->sprint("Content-Type: text/html\r\n"));
+		g.bufio->g.bout.puts(sys->sprint("Content-Length: %d\r\n", len strbuf));
+		g.bufio->g.bout.puts(sys->sprint("\r\n"));
+		g.bufio->g.bout.puts(strbuf);
+		g.bufio->g.bout.flush();
+	}
+	logit(g,sys->sprint("failing: %s", errormsg[reason].num));
+	atexit(g);
+}
+
+
+# write successful header
+ 
+okheaders(g: ref Private_info)
+{
+	g.bufio->g.bout.puts(sys->sprint("%s 200 OK\r\n", g.version));
+	g.bufio->g.bout.puts("Server: Charon\r\n");
+	g.bufio->g.bout.puts("MIME-version: 1.0\r\n");
+}
+
+notmodified(g: ref Private_info)
+{
+	g.bufio->g.bout.puts(sys->sprint("%s 304 Not Modified\r\n", g.version));
+	g.bufio->g.bout.puts("Server: Charon\r\n");
+	g.bufio->g.bout.puts("MIME-version: 1.0\r\n\r\n");
+	atexit(g);
+}
+
+logit(g: ref Private_info,message : string )
+{
+	lock.obtain();
+	sys->fprint(g.logfile,"%s %s\n", g.remotesys, message);
+	lock.release();
+}
+
+urlconv(p : string): string
+{
+	a := array[Sys->UTFmax] of byte;
+	t := "";
+	for(i := 0; i < len p; i++){
+		c := p[i];
+		if(c == 0)
+			continue;	# ignore nul bytes
+		if(c >= Runeself){	# convert to UTF-8
+			n := sys->char2byte(c, a, 0);
+			for(j := 0; j < n; j++)
+				t += sys->sprint("%%%.2X", int a[j]);
+		}else if(c <= ' ' || c == '%'){
+			t += sys->sprint("%%%2.2X", c);
+		} else {
+			t[len t] = c;
+		}
+	}
+	return t; 
+}
--- /dev/null
+++ b/appl/svc/httpd/parser.m
@@ -1,0 +1,15 @@
+Parser: module {
+	Runeself : con 	16r80;
+	Word : con 1;
+
+	PATH:  		con	"/dis/svc/httpd/parser.dis";
+
+	init: fn();
+	urlunesc: fn(s: string): string;
+	fail: fn(g: ref Httpd->Private_info,reason: int, message: string);
+	logit: fn(g: ref Httpd->Private_info, message: string );
+	notmodified: fn(g: ref Httpd->Private_info);
+	httpheaders: fn(g: ref Httpd->Private_info, vers: string);
+	urlconv: fn(url : string): string;
+	okheaders: fn(g: ref Httpd->Private_info);
+};
--- /dev/null
+++ b/appl/svc/httpd/redirect.b
@@ -1,0 +1,130 @@
+implement Redirect;
+
+include "sys.m";
+	sys : Sys;
+
+include "bufio.m";
+	bufio : Bufio;
+Iobuf : import bufio; 
+
+include "string.m";	
+	str : String;
+
+include "redirect.m";
+
+HASHSIZE : con 1019;
+
+
+Redir: adt{
+	pat, repl : string;
+};
+
+tab := array[HASHSIZE] of list of Redir;
+
+
+hashasu(key : string,n : int): int
+{
+        i,h : int;
+	i=0;
+	h=0;
+        while(i<len key){
+                h = 10*h + key[i];
+		h%= n;
+		i++;
+	}
+        return h;
+}
+
+insert(pat, repl : string)
+{
+	hash := hashasu(pat,HASHSIZE);
+	tab[hash]= Redir(pat, repl) :: tab[hash];
+}
+
+redirect_init(file : string)
+{
+	sys = load Sys Sys->PATH;
+	line : string;
+	flist : list of string;
+	n : int;
+	bb : ref Iobuf;
+	for(n=0;n<HASHSIZE;n++)
+		tab[n]= nil; 
+	stderr := sys->fildes(2);
+	bufio = load Bufio Bufio->PATH;	
+	if (bufio==nil){
+		sys->fprint(stderr,"redirect: cannot load %s: %r\n", Bufio->PATH);
+		raise "fail:bad module";
+	}
+	str = load String String->PATH;	
+	if (str==nil){
+		sys->fprint(stderr,"redirect: cannot load %s: %r\n", String->PATH);
+		raise "fail:bad module";
+	}
+	bb = bufio->open(file,bufio->OREAD);
+	if (bb==nil)
+		return;
+	while((line = bb.gets('\n'))!=nil){
+		line = line[0:len line -1]; #chop newline 
+		if (str->in('#',line)){
+			(line,nil) = str->splitl(line, "#");
+			if (line!=nil){
+				n = len line;
+				while(line[n]==' '||line[n]=='\t') n--; 
+					 # and preceeding blanks 
+				line = line[0:n];
+			}
+		}
+		if (line!=nil){
+			(n,flist)=sys->tokenize(line,"\t ");
+			if (n==2)
+				insert(hd flist,hd tl flist);
+		}
+	}
+	
+}
+
+lookup(pat : string):  ref Redir
+{
+	srch : list of Redir;
+	tmp :  Redir;
+	hash : int;
+	hash = hashasu(pat,HASHSIZE);
+	for(srch = tab[hash]; srch!=nil; srch = tl srch){
+		tmp =  hd srch;
+		if(tmp.pat==nil)
+			return nil;
+		if(pat==tmp.pat)
+			return ref tmp;
+	}
+	return nil;
+}
+
+
+redirect(path : string): string {
+	redir :  ref Redir;
+	newpath, oldp : string;
+	s : int;
+	if((redir = lookup(path))!=nil)
+		if(redir.repl==nil)
+			return nil;
+		else
+			return redir.repl;
+	for(s = len path - 1; s>0; s--){
+		if(path[s]=='/'){
+			oldp = path[s+1:];
+			path = path[0:s];	
+			if((redir = lookup(path))!=nil){
+				if(redir.repl!=nil)
+					newpath=sys->sprint("%s/%s",
+						redir.repl,oldp);
+				else
+					newpath = nil;
+				path = path+"/"+oldp;
+				return newpath;
+			}
+			path = path+"/"+oldp;
+		}
+	}
+	return nil;
+}
--- /dev/null
+++ b/appl/svc/httpd/redirect.m
@@ -1,0 +1,7 @@
+Redirect: module
+{
+	PATH: con "/dis/svc/httpd/redirect.dis";
+
+	redirect_init: fn(file : string);
+	redirect: fn(path : string): string;
+};
--- /dev/null
+++ b/appl/svc/httpd/stats.b
@@ -1,0 +1,85 @@
+implement Stats;
+
+include "sys.m";
+	sys : Sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: 	import bufio;
+
+include "draw.m";
+	draw: Draw;
+
+include "contents.m";
+include "cache.m";
+	cache : Cache;
+
+include "httpd.m";
+	Private_info: import Httpd;
+
+include "date.m";
+	date : Date;
+
+include "parser.m";
+	pars : Parser;
+
+include "daytime.m";
+	daytime: Daytime;
+
+Stats: module
+{
+	init: fn(g : ref Private_info, req: Httpd->Request);
+};
+
+badmod(p: string)
+{
+	sys->fprint(sys->fildes(2), "stats: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(k : ref Private_info, req: Httpd->Request)
+{	
+	sys = load Sys "$Sys";
+	draw = load Draw "$Draw";
+	
+	daytime = load Daytime Daytime->PATH;
+	if(daytime == nil) badmod(Daytime->PATH);
+
+	pars = load Parser Parser->PATH;
+	if(pars == nil) badmod(Parser->PATH);
+
+	date = load Date Date->PATH;
+	if(date == nil) badmod(Date->PATH);
+
+	date->init();
+	bufio=k.bufio;
+	send(k, req.method, req.version, req.uri, req.search);
+}
+
+send(g: ref Private_info, meth, vers, uri, search : string)
+{
+	if(meth=="");
+	if(uri=="");
+	if(search=="");
+	if(vers != ""){
+		if (g.version == nil)
+			sys->print("stats: version is unknown.\n");
+		g.bout.puts(sys->sprint("%s 200 OK\r\n", g.version));
+		g.bout.puts("Server: Charon\r\n");
+		g.bout.puts("MIME-version: 1.0\r\n");
+		g.bout.puts(sys->sprint("Date: %s\r\n", date->dateconv(daytime->now())));
+		g.bout.puts("Content-type: text/html\r\n");
+		g.bout.puts(sys->sprint("Expires: %s\r\n", date->dateconv(daytime->now())));
+		g.bout.puts("\r\n");
+	}
+	g.bout.puts("<head><title>Cache Information</title></head>\r\n");
+	g.bout.puts("<body><h1>Cache Information</h1>\r\n");
+	g.bout.puts("These are the pages stored in the server cache:<p>\r\n");
+	lis:=(g.cache)->dump();
+	while (lis!=nil){
+		(a,b,d):=hd lis;
+		g.bout.puts(sys->sprint("<a href=\"%s\"> %s</a> \t size %d \t tag %d.<p>\r\n",a,a,b,d));
+		lis = tl lis;
+	}
+	g.bout.flush();
+}
--- /dev/null
+++ b/appl/svc/mkfile
@@ -1,0 +1,24 @@
+<../../mkconfig
+
+DIRS=\
+	httpd\
+	webget\
+
+SHTARG=\
+	auth.sh\
+	net.sh\
+	registry.sh\
+	rstyx.sh\
+	styx.sh\
+
+BIN=$ROOT/dis/svc
+
+<$ROOT/mkfiles/mksubdirs
+
+SHFILES=${SHTARG:%.sh=$BIN/%}
+install:V:	$SHFILES
+%.install:V:	$BIN/%
+%.installall:V:	$BIN/%
+
+$BIN/%:	%.sh
+	cp $stem.sh $target && chmod a+rx $target
--- /dev/null
+++ b/appl/svc/net.sh
@@ -1,0 +1,6 @@
+#!/dis/sh.dis -n
+load std
+or {ftest -e /net/dns} {ftest -e /env/emuhost} {ndb/dns}
+or {ftest -e /net/cs} {ndb/cs}
+svc/registry
+svc/styx
--- /dev/null
+++ b/appl/svc/registry.sh
@@ -1,0 +1,8 @@
+#!/dis/sh.dis -n
+load std
+or {ftest -f /mnt/registry/new} {
+	db=()
+	and {ftest -f /lib/ndb/registry} {db=(-f /lib/ndb/registry)}
+	mount -A -c {ndb/registry $db} /mnt/registry
+}
+listen -v 'tcp!*!registry' {export /mnt/registry&}	# -n?
--- /dev/null
+++ b/appl/svc/rstyx.sh
@@ -1,0 +1,4 @@
+#!/dis/sh.dis -n
+load std
+listen 'tcp!*!rstyx'  {runas $user auxi/rstyxd&}
+#and {ftest -d /net/il} {listen 'il!*!rstyx' {runas $user auxi/rstyxd&}}
--- /dev/null
+++ b/appl/svc/styx.sh
@@ -1,0 +1,4 @@
+#!/dis/sh.dis -n
+load std
+listen -v 'tcp!*!styx' {export /&}	# -n?
+#and {ftest -d /net/il} {listen -v 'il!*!styx' {export /&}}	# -n?
--- /dev/null
+++ b/appl/svc/webget/date.b
@@ -1,0 +1,274 @@
+implement Date;
+
+include "sys.m";
+	sys: Sys;
+
+include "daytime.m";
+	daytime : Daytime;
+
+Tm: import daytime;
+
+include "date.m";
+
+ # print dates in the format
+ # Wkd, DD Mon YYYY HH:MM:SS GMT
+ # parse dates of formats
+ # Wkd, DD Mon YYYY HH:MM:SS GMT
+ # Weekday, DD-Mon-YY HH:MM:SS GMT
+ # Wkd Mon ( D|DD) HH:MM:SS YYYY
+ # plus anything similar
+
+SEC2MIN: con 60;
+SEC2HOUR: con (60*SEC2MIN);
+SEC2DAY: con (24*SEC2HOUR);
+
+#  days per month plus days/year
+
+dmsize := array[] of {
+	365, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+};
+
+ldmsize := array[] of {
+	366, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+};
+
+
+#  return the days/month for the given year
+
+
+weekdayname := array[] of {
+	"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+};
+
+wdayname := array[] of {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+
+monname := array[] of {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+init()
+{
+	daytime = load Daytime Daytime->PATH;
+	sys = load Sys "$Sys";
+	if (daytime==nil)
+		sys->print("daytime load: %r\n");
+}
+
+# internals....
+dateindex : fn(nil: string, nill:array of string): int; 
+gmtm2sec  : fn(tm: Tm): int;
+
+
+yrsize(yr : int): array of int
+{
+	if(yr % 4 == 0 && (yr % 100 != 0 || yr % 400 == 0))
+		return ldmsize;
+	else
+		return dmsize;
+}
+
+tolower(c: int): int
+{
+	if(c >= 'A' && c <= 'Z')
+		return c - 'A' + 'a';
+	return c;
+}
+
+
+isalpha(c: int): int
+{
+	return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
+}
+
+
+isdig(c: int): int
+{
+	return c >= '0' && c <= '9';
+}
+
+
+dateconv(t: int): string
+{
+	tm : ref Tm;
+	tm = daytime->gmt(t);
+	return sys->sprint("%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT",
+		wdayname[tm.wday], tm.mday, monname[tm.mon], tm.year+1900,
+		tm.hour, tm.min, tm.sec);	
+}
+
+
+dateword(date : string): (string,string) {
+	p : string;
+	i:=0;
+	p = "";
+	while((i<len date) && !isalpha(date[i]) && !isdig(date[i]))
+		i++;
+	while((i<len date) && isalpha(date[i])){		
+		p[len p] = tolower(date[i]);
+		i++;
+	}
+	rest := "";
+	if(i < len date)
+		rest = date[i:];
+	return (rest,p);
+}
+
+
+datenum(date : string): (string, int){
+	n, i : int;
+	i=0;
+	while((i<len date) && !isdig(date[i]))
+		i++;
+	if(i == len date)
+		return (nil, -1);
+	n = 0;
+	while((i<len date) && isdig(date[i])){
+		n = n * 10 + date[i] - '0';
+		i++;
+	}
+	return (date[i:], n);
+}
+
+
+ # parse a date and return the seconds since the epoch
+ # return 0 for a failure
+ 
+# could be big?
+date2sec(date : string): int
+{
+	tm : Tm;
+	buf : string;
+
+	 # Weekday|Wday
+	 
+	(date,buf) = dateword(date);
+	tm.wday = dateindex(buf, wdayname);
+	if(tm.wday < 0)
+		tm.wday = dateindex(buf, weekdayname);
+
+	if(tm.wday < 0)
+		return 0;
+
+	 # check for the two major formats
+	
+	(date,buf) = dateword(date);
+	tm.mon = dateindex(buf, monname);
+	if(tm.mon >= 0){
+		 # MM
+		(date, tm.mday) = datenum(date);
+		if(tm.mday < 1 || tm.mday > 31)
+			return 0;
+
+		 # HH:MM:SS
+		(date, tm.hour) = datenum(date);
+		if(tm.hour < 0 || tm.hour >= 24)
+			return 0;
+		(date, tm.min) = datenum(date);
+		if(tm.min < 0 || tm.min >= 60)
+			return 0;
+		(date, tm.sec) = datenum(date);
+		if(tm.sec < 0 || tm.sec >= 60)
+			return 0;
+
+		
+		 # YYYY 
+		(nil, tm.year) = datenum(date);
+		if(tm.year < 70 || tm.year > 99 && tm.year < 1970)
+			return 0;
+		if(tm.year >= 1970)
+			tm.year -= 1900;
+	}else{
+		# MM-Mon-(YY|YYYY)
+		(date, tm.mday) = datenum(date);
+		if(tm.mday < 1 || tm.mday > 31)
+			return 0;
+		(date,buf) = dateword(date);
+		tm.mon = dateindex(buf, monname);
+		if(tm.mon < 0 || tm.mon >= 12)
+			return 0;
+		(date, tm.year) = datenum(date);
+		if(tm.year < 70 || tm.year > 99 && tm.year < 1970)
+			return 0;
+		if(tm.year >= 1970)
+			tm.year -= 1900;
+		
+		 # HH:MM:SS
+		(date, tm.hour) = datenum(date);
+		if(tm.hour < 0 || tm.hour >= 24)
+			return 0;
+		(date, tm.min) = datenum(date);
+		if(tm.min < 0 || tm.min >= 60)
+			return 0;
+		(date, tm.sec) = datenum(date);
+		if(tm.sec < 0 || tm.sec >= 60)
+			return 0;
+
+		 # timezone
+		(date,buf)=dateword(date);
+		if(len buf >= 3 && lowercase(buf[0:3])!="gmt")
+			return 0;
+	}
+
+	tm.zone="GMT";
+	return gmtm2sec(tm);
+}
+
+lowercase(name:string): string
+{
+	p: string;
+	for(i:=0;i<len name;i++)
+		p[i]=tolower(name[i]);
+	return p;
+}
+
+dateindex(d : string, tab : array of string): int
+{
+	for(i := 0; i < len tab; i++)
+		if (lowercase(tab[i]) == d)
+			return i;
+	return -1;
+}
+
+
+# compute seconds since Jan 1 1970 GMT
+
+gmtm2sec(tm:Tm): int 
+{
+	secs,i : int;
+	d2m: array of int;
+	sys = load Sys "$Sys";
+	secs=0;
+
+	#seconds per year
+	tm.year += 1900;
+	if(tm.year < 1970)
+		return 0;
+	for(i = 1970; i < tm.year; i++){
+		d2m = yrsize(i);
+		secs += d2m[0] * SEC2DAY;
+	}
+
+
+	#seconds per month
+	d2m = yrsize(tm.year);
+	for(i = 0; i < tm.mon; i++)
+		secs += d2m[i+1] * SEC2DAY;
+
+	#secs in last month
+	secs += (tm.mday-1) * SEC2DAY;
+
+	#hours, minutes, seconds	
+	secs += tm.hour * SEC2HOUR;
+	secs += tm.min * SEC2MIN;
+	secs += tm.sec;
+
+	return secs;
+}
+
+now(): int
+{
+	return daytime->now();
+}
--- /dev/null
+++ b/appl/svc/webget/date.m
@@ -1,0 +1,12 @@
+
+Date: module{
+	PATH : con "/dis/svc/webget/date.dis";
+
+	dateconv: fn(secs :int): string; # returns an http formatted
+					 # date representing secs.
+	date2sec: fn(foo:string): int;   # parses a date and returns
+					 # number of secs since the 
+					 # epoch that it represents. 
+	now: fn(): int;		# so don't have to load daytime too
+	init: fn();			# to load needed modules
+};
--- /dev/null
+++ b/appl/svc/webget/file.b
@@ -1,0 +1,69 @@
+implement Transport;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	S: String;
+
+include "bufio.m";
+	B : Bufio;
+	Iobuf: import Bufio;
+
+include "message.m";
+	M: Message;
+	Msg, Nameval: import M;
+
+include "url.m";
+	U: Url;
+	ParsedUrl: import U;
+
+include "dial.m";
+
+include "webget.m";
+
+include "wgutils.m";
+	W: WebgetUtils;
+	Fid, Req: import WebgetUtils;
+
+include "transport.m";
+
+init(w: WebgetUtils)
+{
+	sys = load Sys Sys->PATH;
+	W = w;
+	M = W->M;
+	S = W->S;
+	B = W->B;
+	U = W->U;
+}
+
+connect(c: ref Fid, r: ref Req, donec: chan of ref Fid)
+{
+	u := r.url;
+	mrep: ref Msg = nil;
+	if(!(u.host == "" || u.host == "localhost"))
+		mrep = W->usererr(r, "no remote file system to " + u.host);
+	else {
+		f := u.pstart + u.path;
+		io := B->open(f, sys->OREAD);
+		if(io == nil)
+			mrep = W->usererr(r, sys->sprint("can't open %s: %r\n", f));
+		else {
+			mrep = Msg.newmsg();
+			e := W->getdata(io, mrep, W->fixaccept(r.types), u);
+			B->io.close();
+			if(e != "")
+				mrep = W->usererr(r, e);
+			else
+				W->okprefix(r, mrep);
+		}
+	}
+	if(mrep != nil) {
+		W->log(c, "file: reply ready for " + r.reqid + ": " + mrep.prefixline);
+		r.reply = mrep;
+		donec <-= c;
+	}
+}
--- /dev/null
+++ b/appl/svc/webget/ftp.b
@@ -1,0 +1,231 @@
+implement Transport;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	S: String;
+
+include "bufio.m";
+	B : Bufio;
+	Iobuf: import Bufio;
+
+include "message.m";
+	M: Message;
+	Msg, Nameval: import M;
+
+include "url.m";
+	U: Url;
+	ParsedUrl: import U;
+
+include "webget.m";
+
+include "dial.m";
+	DI: Dial;
+
+include "wgutils.m";
+	W: WebgetUtils;
+	Fid, Req: import WebgetUtils;
+
+include "transport.m";
+
+FTPPORT: con "21";
+DEBUG: con 1;
+
+# Return codes
+Extra, Success, Incomplete, TempFail, PermFail : con (1+iota);
+
+init(w: WebgetUtils)
+{
+	sys = load Sys Sys->PATH;
+	W = w;
+	M = W->M;
+	S = W->S;
+	B = W->B;
+	U = W->U;
+	DI = W->DI;
+}
+
+connect(c: ref Fid, r: ref Req, donec: chan of ref Fid)
+{
+	mrep: ref Msg = nil;
+	io, dio: ref Iobuf = nil;
+	err := "";
+	u := r.url;
+	port := u.port;
+	if(port == "")
+		port = FTPPORT;
+	addr := DI->netmkaddr(u.host, "tcp", port);
+
+dummyloop:	# just for breaking out of on error
+	for(;;) {
+		W->log(c, sys->sprint("ftp: dialing %s", addr));
+		net := DI->dial(addr, nil);
+		if(net == nil) {
+			err = sys->sprint("dial error: %r");
+			break dummyloop;
+		}
+		io = B->fopen(net.dfd, sys->ORDWR);
+		if(io == nil) {
+			err = "cannot open network via bufio";
+			break dummyloop;
+		}
+
+		# look for Hello
+		(code, msg) := getreply(c, io);
+		if(code != Success) {
+			err = "instead of hello: " + msg;
+			break dummyloop;
+		}
+		# logon
+		err = sendrequest(c, io, "USER anonymous");
+		if(err != "") 
+			break dummyloop;
+		(code, msg) = getreply(c, io);
+		if(code != Success) {
+			if(code == Incomplete) {
+				# need password
+				err = sendrequest(c, io, "PASS webget@webget.com");
+				(code, msg) = getreply(c, io);
+				if(code != Success) {
+					err = "login failed: " + msg;
+					break dummyloop;
+				}
+			}
+			else {
+				err = "login failed: " + msg;
+				break dummyloop;
+			}
+		}
+		# image type
+		err = sendrequest(c, io, "TYPE I");
+		(code, msg) = getreply(c, io);
+		if(code != Success) {
+			err = "can't set type I: " + msg;
+			break dummyloop;
+		}
+		# passive mode
+		err = sendrequest(c, io, "PASV");
+		(code, msg) = getreply(c, io);
+		if(code != Success) {
+			err = "can't use passive mode: " + msg;
+			break dummyloop;
+		}
+		(paddr, pport) := passvap(msg);
+		if(paddr == "") {
+			err = "passive mode protocol botch: " + msg;
+			break dummyloop;
+		}
+		# dial data port
+		daddr := "tcp!" + paddr + "!" + pport;
+		W->log(c, sys->sprint("ftp: dialing data %s", daddr));
+		(ok2, dnet) := sys->dial(daddr, nil);
+		if(ok2 < 0) {
+			err = sys->sprint("data dial error: %r");
+			break dummyloop;
+		}
+		dio = B->fopen(dnet.dfd, sys->ORDWR);
+		if(dio == nil) {
+			err = "cannot open network via bufio";
+			break dummyloop;
+		}
+		# tell remote to send file
+		err = sendrequest(c, io, "RETR " + u.path);
+		(code, msg) = getreply(c, io);
+		if(code != Extra) {
+			err = "passive mode retrieve failed: " + msg;
+			break dummyloop;
+		}
+
+		mrep = Msg.newmsg();
+W->log(c, "reading from dio now");
+		err = W->getdata(dio, mrep, W->fixaccept(r.types), u);
+W->log(c, "done reading from dio now, err=" + err);
+		B->dio.close();
+		if(err == "")
+			W->okprefix(r, mrep);
+		break dummyloop;
+	}
+	if(io != nil)
+		B->io.close();
+	if(dio != nil)
+		B->dio.close();
+	if(err != "")
+		mrep = W->usererr(r, err);
+	if(mrep != nil) {
+		W->log(c, "ftp: reply ready for " + r.reqid + ": " + mrep.prefixline);
+		r.reply = mrep;
+		donec <-= c;
+	}
+}
+
+getreply(c: ref Fid, io: ref Iobuf) : (int, string)
+{
+	for(;;) {
+		line := B->io.gets('\n');
+		n := len line;
+		if(n == 0)
+			break;
+		if(DEBUG)
+			W->log(c, "ftp: got reply: " + line);
+		if(line[n-1] == '\n') {
+			if(n > 2 && line[n-2] == '\r')
+				line = line[0:n-2];
+			else
+				line = line[0:n-1];
+		}
+		rv := int line;
+		if(rv >= 100 && rv < 600) {
+			# if line is like '123-stuff'
+			# then there will be more lines until
+			# '123 stuff'
+			if(len line<4 || line[3]==' ')
+				return (rv/100, line);
+		}
+	}
+	return (-1, "");
+}
+
+sendrequest(c: ref Fid, io: ref Iobuf, cmd: string) : string
+{
+	if(DEBUG)
+		W->log(c, "ftp: send request: " + cmd);
+	cmd = cmd + "\r\n";
+	buf := array of byte cmd;
+	n := len buf;
+	if(B->io.write(buf, n) != n)
+		return sys->sprint("write error: %r");
+	return "";
+}
+
+passvap(s: string) : (string, string)
+{
+	# Parse reply to PASSV to find address and port numbers.
+	# This is AI
+	addr := "";
+	port := "";
+	(nil, v) := S->splitl(s, "(");
+	if(v != "")
+		s = v[1:];
+	else
+		(nil, s) = S->splitl(s, "0123456789");
+	if(s != "") {
+		(n, l) := sys->tokenize(s, ",");
+		if(n >= 6) {
+			addr = hd l + ".";
+			l = tl l;
+			addr += hd l + ".";
+			l = tl l;
+			addr += hd l + ".";
+			l = tl l;
+			addr += hd l;
+			l = tl l;
+			p1 := int hd l;
+			p2 := int hd tl l;
+			port = string (((p1&255)<<8)|(p2&255));
+		}
+	}
+	return (addr, port);
+}
--- /dev/null
+++ b/appl/svc/webget/http.b
@@ -1,0 +1,627 @@
+implement Transport;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	S: String;
+
+include "bufio.m";
+	B : Bufio;
+	Iobuf: import B;
+
+include "date.m";
+	D: Date;
+
+include "message.m";
+	M: Message;
+	Msg, Nameval: import M;
+
+include "url.m";
+	U: Url;
+	ParsedUrl: import U;
+
+include "dial.m";
+	DI: Dial;
+
+include "webget.m";
+
+include "wgutils.m";
+	W: WebgetUtils;
+	Fid, Req: import WebgetUtils;
+
+include "keyring.m";
+include "asn1.m";
+include "pkcs.m";
+include "sslsession.m";
+include "ssl3.m";
+	ssl3: SSL3;
+	Context: import ssl3;
+# Inferno supported cipher suites: RSA_EXPORT_RC4_40_MD5
+ssl_suites := array [] of {byte 0, byte 16r03};
+ssl_comprs := array [] of {byte 0};
+
+include "transport.m";
+
+HTTPD:		con "80";		# Default IP port
+HTTPSD:		con "443";	# Default IP port for HTTPS
+Version:	con "1.0";	# Client ID
+MAXREDIR:	con 10;
+
+HTTPheader: adt
+{
+	vers:		string;
+	code:		int;
+	length:		int;
+	content:	string;
+};
+
+Resp: adt
+{
+	code:		int;
+	action:		int;
+	cacheable:	int;
+	name:		string;
+};
+
+DODATA, ERROR, REDIR, UNAUTH, HTMLERR: con iota;
+
+usecache := 1;
+cachedir: con "/services/webget/cache";
+
+httpproxy: ref ParsedUrl;
+noproxydoms: list of string;	# domains that don't require proxy
+agent := "Inferno-webget/" + Version;
+
+responses := array[] of {
+	(Resp)(100, DODATA, 0,	"Continue" ),
+	(Resp)(101, ERROR, 0,	"Switching Protocols" ),
+	(Resp)(200, DODATA, 1,	"Ok" ),
+	(Resp)(201, DODATA, 0,	"Created" ),
+	(Resp)(202, DODATA, 0,	"Accepted" ),
+	(Resp)(203, DODATA, 1,	"Non-Authoratative Information" ),
+	(Resp)(204, DODATA, 0,	"No content" ),
+	(Resp)(205, DODATA, 0,	"Reset content" ),
+	(Resp)(206, DODATA, 0,	"Partial content" ),
+	(Resp)(300, ERROR, 1,	"Multiple choices" ),
+	(Resp)(301, REDIR, 1,	"Moved permanently" ),
+	(Resp)(302, REDIR, 0,	"Moved temporarily" ),
+	(Resp)(303, ERROR, 0,	"See other" ),
+	(Resp)(304, ERROR, 0,	"Not modified" ),
+	(Resp)(305, ERROR, 0,	"Use proxy" ),
+	(Resp)(400, HTMLERR, 0,	"Bad request" ),
+	(Resp)(401, UNAUTH, 0,	"Unauthorized" ),
+	(Resp)(402, HTMLERR, 0,	"Payment required" ),
+	(Resp)(403, HTMLERR, 0,	"Forbidden" ),
+	(Resp)(404, HTMLERR, 0,	"Not found" ),
+	(Resp)(405, HTMLERR, 0,	"Method not allowed" ),
+	(Resp)(406, HTMLERR, 0,	"Not Acceptable" ),
+	(Resp)(407, HTMLERR, 0,	"Proxy authentication required" ),
+	(Resp)(408, HTMLERR, 0,	"Request timed-out" ),
+	(Resp)(409, HTMLERR, 0,	"Conflict" ),
+	(Resp)(410, HTMLERR, 1,	"Gone" ),
+	(Resp)(411, HTMLERR, 0,	"Length required" ),
+	(Resp)(412, HTMLERR, 0,	"Precondition failed" ),
+	(Resp)(413, HTMLERR, 0,	"Request entity too large" ),
+	(Resp)(414, HTMLERR, 0,	"Request-URI too large" ),
+	(Resp)(415, HTMLERR, 0,	"Unsupported media type" ),
+	(Resp)(500, ERROR, 0,	"Internal server error"),
+	(Resp)(501, ERROR, 0,	"Not implemented"),
+	(Resp)(502, ERROR, 0,	"Bad gateway"),
+	(Resp)(503, ERROR, 0,	"Service unavailable"),
+	(Resp)(504, ERROR, 0,	"Gateway time-out"),
+	(Resp)(505, ERROR, 0,	"HTTP version not supported"),
+};
+
+init(w: WebgetUtils)
+{
+	sys = load Sys Sys->PATH;
+	D = load Date Date->PATH;
+	D->init();
+	W = w;
+	M = W->M;
+	S = W->S;
+	B = W->B;
+	U = W->U;
+	DI = W->DI;
+	ssl3 = nil;	# load on demand
+	readconfig();
+}
+
+readconfig()
+{
+	cfgio := B->open("/services/webget/config", sys->OREAD);
+	if(cfgio != nil) {
+		for(;;) {
+			line := B->cfgio.gets('\n');
+			if(line == "") {
+				B->cfgio.close();
+				break;
+			}
+			if(line[0]=='#')
+				continue;
+			(key, val) := S->splitl(line, " \t=");
+			val = S->take(S->drop(val, " \t="), "^\r\n");
+			if(val == nil)
+				continue;
+			case key{
+			"httpproxy" =>
+				if(val != "none"){
+					# val should be host or host:port
+					httpproxy = U->makeurl("http://" + val);
+					W->log(nil, "Using http proxy " + httpproxy.tostring());
+					usecache = 0;
+				}
+			"noproxy" =>
+				(nil, noproxydoms) = sys->tokenize(val, ";, \t");
+			"agent" =>
+				agent = val;
+				W->log(nil, sys->sprint("User agent specfied as '%s'\n", agent));
+			}
+		}
+	}
+}
+
+need_proxy(h: string) : int
+{
+	doml := noproxydoms;
+	if(doml == nil)
+		return 1;		# all domains need proxy
+
+	lh := len h;
+	for(dom := hd doml; doml != nil; doml = tl doml) {
+		ld := len dom;
+		if(lh >= ld && h[lh-ld:] == dom)
+			return 0;	# domain is on the noproxy list
+	}
+
+	return 1;
+}
+
+connect(c: ref Fid, r: ref Req, donec: chan of ref Fid)
+{
+	method := r.method;
+	u := r.url;
+	accept := W->fixaccept(r.types);
+	mrep, cachemrep: ref Msg = nil;
+	validate : string;
+	io: ref Iobuf = nil;
+	redir := 1;
+	usessl := 0;
+	sslx : ref Context;
+
+    redirloop:
+	for(nredir := 0; redir && nredir < MAXREDIR; nredir++) {
+		redir = 0;
+		mrep = nil;
+		cachemrep = nil;
+		io = nil;
+		validate = "";
+		if(u.scheme == Url->HTTPS) {
+			usessl = 1;
+			if(ssl3 == nil) {
+				ssl3 = load SSL3 SSL3->PATH;
+				ssl3->init();
+				sslx = ssl3->Context.new();
+			}
+		}
+		cacheit := usecache;
+		if(r.cachectl == "no-cache" || usessl)
+			cacheit = 0;
+		resptime := 0;
+		#
+		# Perhaps try the cache
+		#
+		if(usecache && method == "GET") {
+			(cachemrep, validate) = cacheread(c, u, r);
+			if(cachemrep != nil && validate == "")
+				cacheit = 0;
+		}
+		else
+			cacheit = 0;
+
+		if(cachemrep == nil || validate != "") {
+			#
+			# Find the port and dial the network
+			#
+			dialu := u;
+			if(httpproxy != nil && need_proxy(u.host))
+				dialu = httpproxy;
+			port := dialu.port;
+			if(port == "") {
+				if(usessl)
+					port = HTTPSD;
+				else
+					port = HTTPD;
+			}
+			addr := DI->netmkaddr(dialu.host, "tcp", port);
+
+			W->log(c, sys->sprint("http: dialing %s", addr));
+			net := DI->dial(addr, nil);
+			if(net == nil) {
+				mrep = W->usererr(r, sys->sprint("%r"));
+				break redirloop;
+			}
+			W->log(c, "http: connected");
+			e: string;
+			if(usessl) {
+				vers := 3;
+				info := ref SSL3->Authinfo(ssl_suites, ssl_comprs, nil, 0, nil, nil, nil);
+				(e, vers) = sslx.client(net.dfd, addr, vers, info);
+				if(e != "") {
+					mrep = W->usererr(r, e);
+					break redirloop;
+				}
+				W->log(c, "https: ssl handshake complete");
+			}
+
+			#
+			# Issue the request
+			#
+			m := Msg.newmsg();
+			requ: string;
+			if(httpproxy != nil && need_proxy(u.host))
+				requ = u.tostring();
+			else {
+				requ = u.pstart + u.path;
+				if(u.query != "")
+					requ += "?" + u.query;
+			}
+			m.prefixline = method + " " +  requ + " HTTP/1.0";
+			hdrs := Nameval("Host", u.host) ::
+				Nameval("User-agent", agent) ::
+				Nameval("Accept", accept) :: nil;
+			m.addhdrs(hdrs);
+			if(validate != "")
+				m.addhdrs(Nameval("If-Modified_Since", validate) :: nil);
+			if(r.auth != "") {
+				m.addhdrs(Nameval("Authorization", "Basic " + r.auth) :: nil);
+				cacheit = 0;
+			}
+			if(method == "POST") {
+				m.body = r.body;
+				m.bodylen = len m.body;
+				m.addhdrs(Nameval("Content-Length", string (len r.body)) :: 
+						Nameval("Content-type", "text/xml") ::	# was application/x-www-form-urlencoded
+						nil);
+			}
+			io = B->fopen(net.dfd, sys->ORDWR);
+			if(io == nil) {
+				mrep = W->usererr(r, "cannot open network via bufio");
+				break redirloop;
+			}
+			e = m.writemsg(io);
+			if(e != "") {
+				mrep = W->usererr(r, e);
+				break redirloop;
+			}
+			(mrep, e) = Msg.readhdr(io, 1);
+			if(e!= "") {
+				mrep = W->usererr(r, e);
+				break redirloop;
+			}
+			resptime = D->now();
+		}
+		else
+			mrep = cachemrep;
+		status := mrep.prefixline;
+		W->log(c, "http:  response from network or cache: " + status
+				+ "\n" + mrep.header()
+				);
+	
+		if(!S->prefix("HTTP/", status)) {
+			mrep = W->usererr(r, "expected http got something else");
+			break redirloop;
+		}
+		code := getcode(status);
+
+		if(validate != "" && code == 304) {
+			# response: "Not Modified", so use cached response
+			mrep = cachemrep;
+			B->io.close();
+			io = nil;
+
+			# let caching happen with new response time
+			# (so age will be small next time)
+			status = mrep.prefixline;
+			W->log(c, "http: validate ok, using cache: " + status);
+			code = getcode(status);
+		}
+
+		for(i := 0; i < len responses; i++) {
+			if(responses[i].code == code)
+				break;
+		}
+
+		if(i >= len responses) {
+			mrep = W->usererr(r, "Unrecognized HTTP response code");
+			break redirloop;
+		}
+
+		(nil, conttype) := mrep.fieldval("content-type");
+		cacheit = cacheit && responses[i].cacheable;
+		case responses[i].action {
+		DODATA =>
+			e := W->getdata(io, mrep, accept, u);
+			if(e != "")
+				mrep = W->usererr(r, e);
+			else {
+				if(cacheit)
+					cachewrite(c, mrep, u, resptime);
+				W->okprefix(r, mrep);
+			}
+		ERROR =>
+			mrep = W->usererr(r, responses[i].name);
+		UNAUTH =>
+			(cok, chal) := mrep.fieldval("www-authenticate");
+			if(cok && r.auth == "")
+				mrep = W->usererr(r, "Unauthorized: " + chal);
+			else {
+				if(conttype == "text/html" && htmlok(accept)) {
+					e := W->getdata(io, mrep, accept, u);
+					if(e != "")
+						mrep = W->usererr(r, "Authorization needed");
+					else
+						W->okprefix(r, mrep);
+				}
+				else
+					mrep = W->usererr(r, "Authorization needed");
+			}
+		REDIR =>
+			(nil, newloc) := mrep.fieldval("location");
+			if(newloc == "") {
+				e := W->getdata(io, mrep, accept, u);
+				if(e != "")
+					mrep = W->usererr(r, e);
+				else
+					W->okprefix(r, mrep);
+			}
+			else {
+				if(cacheit)
+					cachewrite(c, mrep, u, resptime);
+				if(method == "POST") {
+					# this is called "erroneous behavior of some
+					# HTTP 1.0 clients" in the HTTP 1.1 spec,
+					# but servers out there rely on this...
+					method = "GET";
+				}
+				oldu := u;
+				u = U->makeurl(newloc);
+				u.frag = "";
+				u.makeabsolute(oldu);
+				W->log(c, "http: redirect to " + u.tostring());
+				if(io != nil) {
+					B->io.close();
+					io = nil;
+				}
+				redir = 1;
+			}
+		HTMLERR =>
+			if(cacheit)
+				cachewrite(c, mrep, u, resptime);
+			if(conttype == "text/html" && htmlok(accept)) {
+				e := W->getdata(io, mrep, accept, u);
+				if(e != "")
+					mrep = W->usererr(r, responses[i].name);
+				else
+					W->okprefix(r, mrep);
+			}
+			else
+				mrep = W->usererr(r, responses[i].name);
+		}
+	}
+	if(io != nil)
+		B->io.close();
+	if(nredir == MAXREDIR)
+		mrep = W->usererr(r, "redirect loop");
+	if(mrep != nil) {
+		W->log(c, "http: reply ready for " + r.reqid + ": " + mrep.prefixline);
+		r.reply = mrep;
+		donec <-= c;
+	}
+}
+
+getcode(status: string) : int
+{
+	(nil, scode) := S->splitl(status, " ");
+	scode = S->drop(scode, " ");
+	return int scode;
+}
+
+htmlok(accept: string) : int
+{
+	(nil,y) := S->splitstrl(accept, "text/html");
+	return (y != "");
+}
+
+mkhtml(msg: string) : ref Msg
+{
+	m := Msg.newmsg();
+	m.body = array of byte sys->sprint("<HTML>\n"+
+			"<BODY>\n"+
+			"<H1>HTTP Reported Error</H1>\n"+
+			"<P>\n"+
+			"The server reported an error: %s\n"+
+			"</BODY>\n"+
+			"</HTML>\n", msg);
+	m.bodylen = len m.body;
+	m.update("content-type", "text/html");
+	m.update("content-location", "webget-internal-message");
+	return m;
+}
+
+cacheread(c: ref Fid, u: ref Url->ParsedUrl, r: ref Req) : (ref Msg, string)
+{
+	ctl := r.cachectl;
+	if(ctl == "no-cache")
+		return (nil, "");
+	uname := u.tostring();
+	hname := hashname(uname);
+	io := B->open(hname, sys->OREAD);
+	if(io == nil)
+		return (nil, "");
+	(mrep, e) := Msg.readhdr(io, 1);
+	if(e != "") {
+		B->io.close();
+		return (nil, "");
+	}
+
+	# see if cache validation is necessary
+	validate := "";
+	cmaxstale := 0;
+	cmaxage := -1;
+	(nl, l) := sys->tokenize(ctl, ",");
+	for(i := 0; i < nl; i++) {
+		s := hd l;
+		if(S->prefix("max-stale=", s))
+			cmaxstale = int s[10:];
+		else if (S->prefix("max-age=", s))
+			cmaxage = int s[8:];
+		l = tl l;
+	}
+	# we wrote x-resp-time and x-url, so they should be there
+	(srst, sresptime) := mrep.fieldval("x-resp-time");
+	(su, surl) := mrep.fieldval("x-url");
+	if(!srst || !su) {
+		cacheremove(hname);
+		B->io.close();
+		return (nil, "");
+	}
+	if(surl != uname) {
+		B->io.close();
+		return (nil, "");
+	}
+	(se, sexpires) := mrep.fieldval("expires");
+	(sd, sdate) := mrep.fieldval("date");
+	(slm, slastmod) := mrep.fieldval("last-modified");
+	(sa, sage) := mrep.fieldval("age");
+
+	# calculate response age (in seconds), as of time received
+	respt := int sresptime;
+	datet := D->date2sec(sdate);
+	nowt := D->now();
+
+	age := nowt - respt;
+	if(sa)
+		age += (int sage);
+	freshness_lifetime := 0;
+	(sma, smaxage) := mrep.fieldval("max-age");
+	if(sma)
+		freshness_lifetime = int smaxage;
+	else if(sd && se) {
+		exp := D->date2sec(sexpires);
+		freshness_lifetime = exp - datet;
+	}
+	else if(slm){
+		# use heuristic: 10% of time since last modified
+		lastmod := D->date2sec(slastmod);
+		if(lastmod == 0)
+			lastmod = respt;
+		freshness_lifetime = (nowt - lastmod) / 10;
+	}
+	if(age - freshness_lifetime > cmaxstale ||
+	   (cmaxage != -1 && age >= cmaxage)) {
+		W->log(c, sys->sprint("must revalidate, age=%d, lifetime=%d, cmaxstale=%d, cmaxage=%d\n",
+				age, freshness_lifetime, cmaxstale, cmaxage));
+		if(slm)
+			validate = slastmod;
+		else
+			return (nil, "");
+	}
+	e = mrep.readbody(io);
+	B->io.close();
+	if(e != "") {
+		cacheremove(hname);
+		return (nil, "");
+	}
+	if(validate == "")
+		W->log(c, "cache hit " + hname);
+	else
+		W->log(c, "cache hit " + hname + " if not modified after " + validate);
+	return (mrep, validate);
+}
+
+cachewrite(c: ref Fid, m: ref Msg, u: ref Url->ParsedUrl, respt: int)
+{
+	(sp, spragma) := m.fieldval("pragma");
+	if(sp && spragma == "no-cache")
+		return;
+	(scc, scachectl) := m.fieldval("cache-control");
+	if(scc) {
+		(snc, nil) := attrval(scachectl, "no-cache");
+		(sns, nil) := attrval(scachectl, "no-store");
+		(smv, nil) := attrval(scachectl, "must-revalidate");
+		if(snc || sns || smv)
+			return;
+	}
+	uname := u.tostring();
+	hname := hashname(uname);
+	m.update("x-resp-time", string respt);
+	m.update("x-url", uname);
+	m.update("content-length", string m.bodylen);
+	io := B->create(hname, sys->OWRITE, 8r666);
+	if(io != nil) {
+		W->log(c, "cache writeback to " + hname);
+		m.writemsg(io);
+		B->io.close();
+	}
+}
+
+cacheremove(hname: string)
+{
+	sys->remove(hname);
+}
+
+attrval(avs, aname: string) : (int, string)
+{
+	(nl, l) := sys->tokenize(avs, ",");
+	for(i := 0; i < nl; i++) {
+		s := hd l;
+		(lh, rh) := S->splitl(s, "=");
+		lh = trim(lh);
+		if(lh == aname) {
+			if(rh != "")
+				rh = trim(rh[1:]);
+			return (1, rh);
+		}
+		l = tl l;
+	}
+	return (0, "");
+}
+
+trim(s: string) : string
+{
+	is := 0;
+	ie := len s;
+	while(is < ie) {
+		if(!S->in(s[is], " \t\n\r"))
+			break;
+		is++;
+	}
+	if(is == ie)
+		return "";
+	if(s[is] == '"')
+		is++;
+	while(ie > is) {
+		if(!S->in(s[ie-1], " \t\n\r"))
+			break;
+		ie--;
+	}
+	if(is >= ie)
+		return "";
+	return s[is:ie];
+}
+
+hashname(uname: string) : string
+{
+	hash := 0;
+	prime: con 8388617;
+	# start after "http:"
+	for(i := 5; i < len uname; i++) {
+		hash = hash % prime;
+		hash = (hash << 7) + uname[i];
+	}
+	return sys->sprint(cachedir + "/%.8ux", hash); 
+}
--- /dev/null
+++ b/appl/svc/webget/image2enc.b
@@ -1,0 +1,1070 @@
+implement Image2enc;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "bufio.m";
+
+include "imagefile.m";
+	Rawimage: import RImagefile;
+
+include "image2enc.m";
+
+closest:= array[16*16*16] of {
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 255,byte 255,byte 250,byte 250,byte 250,byte 220,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 250,byte 250,byte 250,byte 250,byte 249,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 250,byte 250,byte 250,byte 233,byte 233,byte 249,
+	byte 249,byte 232,byte 215,byte 215,byte 248,byte 231,byte 214,byte 197,
+	byte 234,byte 234,byte 250,byte 250,byte 233,byte 233,byte 216,byte 216,
+	byte 249,byte 232,byte 215,byte 198,byte 198,byte 231,byte 214,byte 197,
+	byte 217,byte 217,byte 217,byte 246,byte 233,byte 216,byte 216,byte 199,
+	byte 199,byte 215,byte 215,byte 198,byte 198,byte 198,byte 214,byte 197,
+	byte 200,byte 200,byte 246,byte 246,byte 246,byte 216,byte 199,byte 199,
+	byte 245,byte 245,byte 198,byte 244,byte 244,byte 244,byte 227,byte 197,
+	byte 247,byte 247,byte 246,byte 246,byte 246,byte 246,byte 199,byte 245,
+	byte 245,byte 245,byte 228,byte 244,byte 244,byte 244,byte 227,byte 193,
+	byte 230,byte 230,byte 246,byte 246,byte 229,byte 229,byte 212,byte 245,
+	byte 245,byte 228,byte 228,byte 211,byte 244,byte 227,byte 210,byte 193,
+	byte 213,byte 213,byte 229,byte 229,byte 212,byte 212,byte 212,byte 195,
+	byte 228,byte 228,byte 211,byte 211,byte 194,byte 227,byte 210,byte 193,
+	byte 196,byte 196,byte 242,byte 242,byte 212,byte 195,byte 195,byte 241,
+	byte 241,byte 211,byte 211,byte 194,byte 194,byte 240,byte 210,byte 193,
+	byte 243,byte 243,byte 242,byte 242,byte 242,byte 195,byte 195,byte 241,
+	byte 241,byte 241,byte 194,byte 194,byte 240,byte 240,byte 239,byte 205,
+	byte 226,byte 226,byte 242,byte 242,byte 225,byte 225,byte 195,byte 241,
+	byte 241,byte 224,byte 224,byte 240,byte 240,byte 239,byte 239,byte 205,
+	byte 209,byte 209,byte 225,byte 225,byte 208,byte 208,byte 208,byte 224,
+	byte 224,byte 223,byte 223,byte 223,byte 239,byte 239,byte 222,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 238,byte 221,byte 221,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 221,byte 221,byte 221,byte 204,byte 250,byte 220,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 221,byte 221,byte 204,byte 250,byte 250,byte 249,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 204,byte 250,byte 250,byte 233,byte 233,byte 249,
+	byte 249,byte 232,byte 215,byte 215,byte 248,byte 231,byte 214,byte 197,
+	byte 234,byte 234,byte 250,byte 250,byte 233,byte 233,byte 216,byte 216,
+	byte 249,byte 232,byte 215,byte 198,byte 198,byte 231,byte 214,byte 197,
+	byte 217,byte 217,byte 217,byte 246,byte 233,byte 216,byte 216,byte 199,
+	byte 199,byte 215,byte 215,byte 198,byte 198,byte 198,byte 214,byte 197,
+	byte 200,byte 200,byte 246,byte 246,byte 246,byte 216,byte 199,byte 199,
+	byte 245,byte 245,byte 198,byte 244,byte 244,byte 244,byte 227,byte 197,
+	byte 247,byte 247,byte 246,byte 246,byte 246,byte 246,byte 199,byte 245,
+	byte 245,byte 245,byte 228,byte 244,byte 244,byte 244,byte 227,byte 193,
+	byte 230,byte 230,byte 246,byte 246,byte 229,byte 229,byte 212,byte 245,
+	byte 245,byte 228,byte 228,byte 211,byte 244,byte 227,byte 210,byte 193,
+	byte 213,byte 213,byte 229,byte 229,byte 212,byte 212,byte 212,byte 195,
+	byte 228,byte 228,byte 211,byte 211,byte 194,byte 227,byte 210,byte 193,
+	byte 196,byte 196,byte 242,byte 242,byte 212,byte 195,byte 195,byte 241,
+	byte 241,byte 211,byte 211,byte 194,byte 194,byte 240,byte 210,byte 193,
+	byte 243,byte 243,byte 242,byte 242,byte 242,byte 195,byte 195,byte 241,
+	byte 241,byte 241,byte 194,byte 194,byte 240,byte 240,byte 239,byte 205,
+	byte 226,byte 226,byte 242,byte 242,byte 225,byte 225,byte 195,byte 241,
+	byte 241,byte 224,byte 224,byte 240,byte 240,byte 239,byte 239,byte 205,
+	byte 209,byte 209,byte 225,byte 225,byte 208,byte 208,byte 208,byte 224,
+	byte 224,byte 223,byte 223,byte 223,byte 239,byte 239,byte 222,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 255,byte 255,byte 255,byte 191,byte 191,byte 191,byte 220,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 255,byte 221,byte 221,byte 221,byte 204,byte 191,byte 220,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 255,byte 221,byte 221,byte 204,byte 204,byte 204,byte 186,byte 186,
+	byte 186,byte 186,byte 186,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 221,byte 204,byte 204,byte 204,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 232,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 233,byte 216,byte 186,
+	byte 186,byte 186,byte 215,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 217,byte 217,byte 183,byte 183,byte 183,byte 216,byte 216,byte 199,
+	byte 182,byte 182,byte 215,byte 198,byte 198,byte 181,byte 214,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 199,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 181,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 193,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 228,byte 165,byte 181,byte 181,byte 164,byte 164,byte 193,
+	byte 167,byte 167,byte 183,byte 229,byte 166,byte 212,byte 212,byte 182,
+	byte 182,byte 165,byte 211,byte 211,byte 181,byte 164,byte 210,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 195,byte 178,
+	byte 178,byte 178,byte 211,byte 194,byte 177,byte 177,byte 177,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 195,byte 178,
+	byte 178,byte 178,byte 178,byte 177,byte 177,byte 177,byte 177,byte 205,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 178,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 177,byte 160,byte 205,
+	byte 163,byte 163,byte 162,byte 162,byte 162,byte 162,byte 208,byte 178,
+	byte 161,byte 161,byte 223,byte 177,byte 177,byte 160,byte 160,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 176,byte 176,byte 191,byte 191,byte 191,byte 191,byte 190,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 176,byte 221,byte 221,byte 204,byte 191,byte 191,byte 190,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 188,byte 221,byte 204,byte 204,byte 204,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 173,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 168,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 170,byte 170,byte 182,
+	byte 182,byte 169,byte 152,byte 152,byte 181,byte 168,byte 151,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 165,byte 181,byte 181,byte 181,byte 164,byte 193,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 164,byte 193,
+	byte 167,byte 167,byte 167,byte 166,byte 166,byte 166,byte 149,byte 182,
+	byte 165,byte 165,byte 165,byte 148,byte 181,byte 164,byte 147,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 149,byte 178,
+	byte 178,byte 178,byte 148,byte 177,byte 177,byte 177,byte 147,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 178,byte 178,
+	byte 178,byte 178,byte 178,byte 177,byte 177,byte 177,byte 160,byte 205,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 162,byte 162,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 160,byte 160,byte 205,
+	byte 163,byte 163,byte 162,byte 162,byte 162,byte 162,byte 145,byte 161,
+	byte 161,byte 161,byte 144,byte 144,byte 160,byte 160,byte 160,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 176,byte 176,byte 191,byte 191,byte 191,byte 174,byte 174,byte 190,
+	byte 190,byte 173,byte 156,byte 156,byte 189,byte 172,byte 155,byte 138,
+	byte 176,byte 176,byte 204,byte 191,byte 191,byte 174,byte 174,byte 190,
+	byte 190,byte 173,byte 156,byte 156,byte 189,byte 172,byte 155,byte 138,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 187,byte 187,byte 170,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 152,byte 152,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 183,byte 183,byte 170,byte 170,byte 170,byte 153,
+	byte 182,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 153,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 165,byte 181,byte 181,byte 181,byte 164,byte 130,
+	byte 167,byte 167,byte 183,byte 183,byte 166,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 166,byte 166,byte 166,byte 149,byte 149,byte 182,
+	byte 165,byte 165,byte 148,byte 148,byte 164,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 179,byte 179,byte 179,byte 149,byte 132,byte 178,
+	byte 178,byte 178,byte 148,byte 131,byte 177,byte 177,byte 147,byte 130,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 132,byte 178,
+	byte 178,byte 178,byte 161,byte 177,byte 177,byte 177,byte 160,byte 142,
+	byte 163,byte 163,byte 179,byte 179,byte 162,byte 162,byte 162,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 162,byte 162,byte 145,byte 145,byte 145,byte 161,
+	byte 161,byte 144,byte 144,byte 144,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 175,byte 175,byte 191,byte 191,byte 174,byte 174,byte 157,byte 157,
+	byte 190,byte 173,byte 156,byte 139,byte 139,byte 172,byte 155,byte 138,
+	byte 175,byte 175,byte 191,byte 191,byte 174,byte 174,byte 157,byte 157,
+	byte 190,byte 173,byte 156,byte 139,byte 139,byte 172,byte 155,byte 138,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 157,byte 186,
+	byte 186,byte 186,byte 156,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 168,byte 138,
+	byte 171,byte 171,byte 187,byte 187,byte 170,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 152,byte 152,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 187,byte 170,byte 170,byte 170,byte 170,byte 153,
+	byte 169,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 170,byte 153,byte 153,
+	byte 169,byte 152,byte 152,byte 135,byte 135,byte 135,byte 151,byte 134,
+	byte 154,byte 154,byte 183,byte 183,byte 183,byte 153,byte 153,byte 153,
+	byte 182,byte 182,byte 135,byte 135,byte 181,byte 181,byte 164,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 164,byte 130,
+	byte 167,byte 167,byte 183,byte 166,byte 166,byte 166,byte 149,byte 182,
+	byte 165,byte 165,byte 165,byte 148,byte 181,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 150,byte 166,byte 149,byte 149,byte 149,byte 132,
+	byte 165,byte 165,byte 148,byte 148,byte 131,byte 147,byte 147,byte 130,
+	byte 133,byte 133,byte 179,byte 179,byte 149,byte 132,byte 132,byte 132,
+	byte 178,byte 148,byte 148,byte 131,byte 131,byte 131,byte 130,byte 130,
+	byte 133,byte 133,byte 179,byte 179,byte 179,byte 132,byte 132,byte 178,
+	byte 178,byte 178,byte 131,byte 131,byte 131,byte 177,byte 160,byte 142,
+	byte 163,byte 163,byte 179,byte 162,byte 162,byte 162,byte 132,byte 178,
+	byte 161,byte 161,byte 144,byte 131,byte 177,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 162,byte 162,byte 145,byte 145,byte 145,byte 161,
+	byte 161,byte 144,byte 144,byte 143,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 158,byte 158,byte 158,byte 112,byte 174,byte 157,byte 157,byte 140,
+	byte 140,byte 156,byte 156,byte 139,byte 139,byte 139,byte 155,byte 138,
+	byte 158,byte 158,byte 158,byte 112,byte 174,byte 157,byte 157,byte 140,
+	byte 140,byte 156,byte 156,byte 139,byte 139,byte 139,byte 155,byte 138,
+	byte 158,byte 158,byte 124,byte 124,byte 124,byte 157,byte 157,byte 140,
+	byte 123,byte 123,byte 156,byte 139,byte 139,byte 122,byte 155,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 170,byte 170,byte 123,
+	byte 123,byte 169,byte 152,byte 152,byte 122,byte 168,byte 151,byte 138,
+	byte 171,byte 171,byte 124,byte 124,byte 170,byte 170,byte 170,byte 153,
+	byte 123,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 170,byte 153,byte 153,
+	byte 169,byte 152,byte 152,byte 135,byte 135,byte 135,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 153,byte 153,byte 153,
+	byte 136,byte 152,byte 135,byte 135,byte 135,byte 135,byte 134,byte 134,
+	byte 137,byte 137,byte 137,byte 120,byte 153,byte 153,byte 153,byte 136,
+	byte 136,byte 136,byte 135,byte 135,byte 135,byte 118,byte 164,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 166,byte 136,byte 136,
+	byte 136,byte 165,byte 165,byte 118,byte 118,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 120,byte 166,byte 166,byte 149,byte 149,byte 136,
+	byte 165,byte 165,byte 148,byte 148,byte 118,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 150,byte 149,byte 149,byte 149,byte 132,byte 132,
+	byte 165,byte 148,byte 148,byte 131,byte 131,byte 147,byte 147,byte 130,
+	byte 133,byte 133,byte 133,byte 149,byte 132,byte 132,byte 132,byte 132,
+	byte 115,byte 148,byte 131,byte 131,byte 131,byte 131,byte 130,byte 130,
+	byte 133,byte 133,byte 133,byte 116,byte 132,byte 132,byte 132,byte 132,
+	byte 115,byte 115,byte 131,byte 131,byte 131,byte 131,byte 160,byte 142,
+	byte 133,byte 133,byte 116,byte 162,byte 162,byte 132,byte 132,byte 115,
+	byte 161,byte 161,byte 144,byte 131,byte 131,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 146,byte 145,byte 145,byte 145,byte 128,byte 161,
+	byte 144,byte 144,byte 144,byte 143,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 141,byte 141,byte 112,byte 112,byte 112,byte 157,byte 140,byte 140,
+	byte 140,byte 127,byte 139,byte 126,byte 126,byte 126,byte 109,byte 138,
+	byte 141,byte 141,byte 112,byte 112,byte 112,byte 157,byte 140,byte 140,
+	byte 140,byte 127,byte 139,byte 126,byte 126,byte 126,byte 109,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 140,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 122,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 105,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 153,byte 123,
+	byte 123,byte 123,byte 152,byte 122,byte 122,byte 122,byte 105,byte 134,
+	byte 154,byte 154,byte 124,byte 124,byte 124,byte 153,byte 153,byte 153,
+	byte 123,byte 123,byte 135,byte 135,byte 122,byte 122,byte 105,byte 134,
+	byte 137,byte 137,byte 137,byte 120,byte 153,byte 153,byte 153,byte 136,
+	byte 136,byte 136,byte 135,byte 135,byte 135,byte 118,byte 105,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 153,byte 136,byte 136,
+	byte 136,byte 119,byte 119,byte 118,byte 118,byte 118,byte 118,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 120,byte 136,byte 136,
+	byte 119,byte 119,byte 119,byte 118,byte 118,byte 118,byte 101,byte 130,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte 130,
+	byte 133,byte 133,byte 120,byte 120,byte 149,byte 132,byte 132,byte 119,
+	byte 119,byte 102,byte 148,byte 131,byte 131,byte 101,byte 101,byte 130,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 132,byte 132,byte 115,
+	byte 115,byte 115,byte 131,byte 131,byte 114,byte 114,byte 114,byte 130,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 132,byte 115,
+	byte 115,byte 115,byte 131,byte 114,byte 114,byte 114,byte 114,byte 142,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte  98,byte 114,byte 114,byte 114,byte  97,byte 142,
+	byte 100,byte 100,byte 116,byte  99,byte  99,byte  99,byte  99,byte 115,
+	byte  98,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 113,byte 113,byte 112,byte 112,byte 112,byte 112,byte 140,byte 140,
+	byte 127,byte 127,byte 110,byte 126,byte 126,byte 126,byte 109,byte  75,
+	byte 113,byte 113,byte 112,byte 112,byte 112,byte 112,byte 140,byte 140,
+	byte 127,byte 127,byte 110,byte 126,byte 126,byte 126,byte 109,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 122,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 122,byte 105,byte  71,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte 105,byte  71,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 107,byte 136,byte 136,
+	byte 136,byte 106,byte 106,byte 118,byte 118,byte 105,byte  88,byte  71,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 120,byte 136,byte 136,
+	byte 119,byte 119,byte 119,byte 118,byte 118,byte 118,byte 101,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte  67,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 103,byte 103,byte 119,
+	byte 119,byte 102,byte 102,byte 118,byte 118,byte 101,byte 101,byte  67,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte 115,byte 114,byte 114,byte 114,byte 114,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte 115,byte 114,byte 114,byte 114,byte  97,byte  79,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte  99,byte  99,byte 115,
+	byte 115,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  99,byte  82,byte  98,
+	byte  98,byte  98,byte  81,byte 114,byte  97,byte  97,byte  97,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  96,byte  96,byte 112,byte 112,byte 111,byte 111,byte  94,byte 127,
+	byte 127,byte 110,byte 110,byte  93,byte 126,byte 109,byte  92,byte  75,
+	byte  96,byte  96,byte 112,byte 112,byte 111,byte 111,byte  94,byte 127,
+	byte 127,byte 110,byte 110,byte  93,byte 126,byte 109,byte  92,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 105,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte 105,byte  75,
+	byte 108,byte 108,byte 124,byte 124,byte 107,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte  88,byte  71,
+	byte 108,byte 108,byte 124,byte 107,byte 107,byte 107,byte  90,byte 123,
+	byte 106,byte 106,byte 106,byte  89,byte 122,byte 105,byte  88,byte  71,
+	byte  91,byte  91,byte 120,byte 107,byte 107,byte  90,byte  90,byte 136,
+	byte 106,byte 106,byte  89,byte  89,byte 118,byte 105,byte  88,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 103,byte 103,byte 119,
+	byte 119,byte 102,byte 102,byte 118,byte 118,byte 101,byte 101,byte  67,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 104,byte 104,byte 103,byte 103,byte 103,byte 103,byte  86,byte 102,
+	byte 102,byte 102,byte  85,byte  85,byte 101,byte 101,byte  84,byte  67,
+	byte  87,byte  87,byte 116,byte 116,byte 116,byte  86,byte  86,byte 115,
+	byte 115,byte 115,byte  85,byte  85,byte 114,byte 114,byte  84,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte  98,byte 114,byte 114,byte 114,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  99,byte  99,byte 115,
+	byte  98,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte  79,
+	byte  83,byte  83,byte  99,byte  99,byte  82,byte  82,byte  82,byte  98,
+	byte  98,byte  81,byte  81,byte  81,byte  97,byte  97,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  95,byte  95,byte 111,byte 111,byte  94,byte  94,byte  94,byte  77,
+	byte 110,byte 110,byte  93,byte  93,byte  76,byte 109,byte  92,byte  75,
+	byte  95,byte  95,byte 111,byte 111,byte  94,byte  94,byte  94,byte  77,
+	byte 110,byte 110,byte  93,byte  93,byte  76,byte 109,byte  92,byte  75,
+	byte 108,byte 108,byte 124,byte 111,byte 107,byte  94,byte  94,byte 123,
+	byte 123,byte 106,byte  93,byte  93,byte 122,byte 105,byte  92,byte  75,
+	byte 108,byte 108,byte 108,byte 107,byte 107,byte 107,byte  90,byte 123,
+	byte 106,byte 106,byte 106,byte  89,byte 122,byte 105,byte  88,byte  75,
+	byte  91,byte  91,byte 107,byte 107,byte 107,byte  90,byte  90,byte 123,
+	byte 106,byte 106,byte  89,byte  89,byte 105,byte 105,byte  88,byte  71,
+	byte  91,byte  91,byte  91,byte 107,byte  90,byte  90,byte  90,byte  73,
+	byte 106,byte 106,byte  89,byte  89,byte  72,byte  88,byte  88,byte  71,
+	byte  91,byte  91,byte  91,byte  90,byte  90,byte  90,byte  73,byte  73,
+	byte 106,byte  89,byte  89,byte  72,byte  72,byte  88,byte  88,byte  71,
+	byte  74,byte  74,byte 120,byte 120,byte 120,byte  73,byte  73,byte 119,
+	byte 119,byte 102,byte  89,byte  72,byte  72,byte 101,byte 101,byte  71,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 104,byte 104,byte 103,byte 103,byte 103,byte 103,byte  86,byte 102,
+	byte 102,byte 102,byte  85,byte  85,byte 101,byte 101,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte 103,byte  86,byte  86,byte  86,byte  86,
+	byte 102,byte  85,byte  85,byte  85,byte  85,byte  84,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte  86,byte  86,byte  86,byte  69,byte  69,
+	byte 115,byte  85,byte  85,byte  85,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte 116,byte 116,byte  99,byte  69,byte  69,byte  69,
+	byte 115,byte  98,byte  85,byte  68,byte  68,byte  97,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  82,byte  82,byte  98,
+	byte  98,byte  98,byte  81,byte  68,byte  97,byte  97,byte  97,byte  79,
+	byte  83,byte  83,byte  83,byte  82,byte  82,byte  82,byte  82,byte  98,
+	byte  81,byte  81,byte  81,byte  64,byte  97,byte  97,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  78,byte  78,byte  49,byte  49,byte  94,byte  77,byte  77,byte  48,
+	byte  48,byte  93,byte  93,byte  76,byte  76,byte  63,byte  92,byte  75,
+	byte  78,byte  78,byte  49,byte  49,byte  94,byte  77,byte  77,byte  48,
+	byte  48,byte  93,byte  93,byte  76,byte  76,byte  63,byte  92,byte  75,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  77,byte  60,
+	byte  60,byte  60,byte  93,byte  76,byte  59,byte  59,byte  59,byte  75,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  90,byte  60,
+	byte  60,byte  60,byte  89,byte  59,byte  59,byte  59,byte  88,byte  75,
+	byte  91,byte  91,byte  61,byte  61,byte  61,byte  90,byte  73,byte  60,
+	byte  60,byte  60,byte  89,byte  72,byte  59,byte  59,byte  88,byte  71,
+	byte  74,byte  74,byte  61,byte  61,byte  90,byte  73,byte  73,byte  73,
+	byte  60,byte  89,byte  89,byte  72,byte  72,byte  72,byte  71,byte  71,
+	byte  74,byte  74,byte  74,byte  90,byte  73,byte  73,byte  73,byte  73,
+	byte  56,byte  89,byte  72,byte  72,byte  72,byte  72,byte  71,byte  71,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  73,byte  73,byte  56,
+	byte  56,byte  56,byte  72,byte  72,byte  55,byte  55,byte  55,byte  71,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  56,byte  55,byte  55,byte  55,byte  55,byte  67,
+	byte  87,byte  87,byte  57,byte  57,byte  57,byte  86,byte  86,byte  56,
+	byte  56,byte  56,byte  85,byte  85,byte  55,byte  55,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte  86,byte  86,byte  86,byte  69,byte  69,
+	byte  56,byte  85,byte  85,byte  85,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte  70,byte  53,byte  69,byte  69,byte  69,byte  69,
+	byte  52,byte  85,byte  85,byte  68,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  68,byte  51,byte  51,byte  79,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  80,byte  79,
+	byte  83,byte  83,byte  53,byte  82,byte  82,byte  65,byte  65,byte  52,
+	byte  52,byte  81,byte  64,byte  64,byte  51,byte  80,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  50,byte  50,byte  49,byte  49,byte  49,byte  77,byte  77,byte  48,
+	byte  48,byte  48,byte  76,byte  76,byte  63,byte  63,byte  46,byte  12,
+	byte  50,byte  50,byte  49,byte  49,byte  49,byte  77,byte  77,byte  48,
+	byte  48,byte  48,byte  76,byte  76,byte  63,byte  63,byte  46,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  77,byte  60,
+	byte  60,byte  60,byte  60,byte  59,byte  59,byte  59,byte  59,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  60,byte  60,
+	byte  60,byte  60,byte  60,byte  59,byte  59,byte  59,byte  42,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  73,byte  60,
+	byte  60,byte  60,byte  43,byte  59,byte  59,byte  59,byte  42,byte   8,
+	byte  74,byte  74,byte  61,byte  61,byte  61,byte  73,byte  73,byte  60,
+	byte  60,byte  60,byte  72,byte  72,byte  72,byte  59,byte  42,byte   8,
+	byte  74,byte  74,byte  74,byte  57,byte  73,byte  73,byte  73,byte  73,
+	byte  56,byte  56,byte  72,byte  72,byte  72,byte  72,byte  42,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  73,byte  56,
+	byte  56,byte  56,byte  72,byte  55,byte  55,byte  55,byte  55,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  56,byte  55,byte  55,byte  55,byte  38,byte   4,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  39,byte  55,byte  55,byte  55,byte  38,byte   4,
+	byte  70,byte  70,byte  57,byte  57,byte  40,byte  69,byte  69,byte  69,
+	byte  56,byte  39,byte  85,byte  68,byte  68,byte  38,byte  38,byte   4,
+	byte  70,byte  70,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  68,byte  51,byte  51,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  51,byte   0,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  53,byte  69,byte  52,
+	byte  52,byte  52,byte  35,byte  51,byte  51,byte  51,byte  34,byte   0,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  33,byte  33,byte  49,byte  49,byte  32,byte  32,byte  77,byte  48,
+	byte  48,byte  47,byte  47,byte  63,byte  63,byte  46,byte  46,byte  12,
+	byte  33,byte  33,byte  49,byte  49,byte  32,byte  32,byte  77,byte  48,
+	byte  48,byte  47,byte  47,byte  63,byte  63,byte  46,byte  46,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  60,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  59,byte  42,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  44,byte  44,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  42,byte  42,byte  12,
+	byte  45,byte  45,byte  61,byte  61,byte  44,byte  44,byte  44,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  42,byte  42,byte   8,
+	byte  45,byte  45,byte  61,byte  44,byte  44,byte  44,byte  73,byte  60,
+	byte  43,byte  43,byte  26,byte  72,byte  59,byte  42,byte  42,byte   8,
+	byte  74,byte  74,byte  57,byte  44,byte  44,byte  73,byte  73,byte  56,
+	byte  43,byte  43,byte  26,byte  72,byte  72,byte  42,byte  42,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  39,byte  55,byte  55,byte  55,byte  38,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  40,byte  40,byte  56,
+	byte  56,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   4,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  40,byte  40,byte  56,
+	byte  39,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   4,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  23,byte  23,byte  39,
+	byte  39,byte  39,byte  22,byte  68,byte  38,byte  38,byte  38,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  21,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  53,byte  69,byte  52,
+	byte  52,byte  52,byte  35,byte  51,byte  51,byte  51,byte  34,byte   0,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte  37,byte  37,byte  36,byte  36,byte  36,byte  36,byte  36,byte  35,
+	byte  35,byte  35,byte  35,byte  18,byte  34,byte  34,byte  34,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  16,byte  16,byte  32,byte  32,byte  31,byte  31,byte  31,byte  47,
+	byte  47,byte  30,byte  30,byte  30,byte  46,byte  46,byte  29,byte  12,
+	byte  16,byte  16,byte  32,byte  32,byte  31,byte  31,byte  31,byte  47,
+	byte  47,byte  30,byte  30,byte  30,byte  46,byte  46,byte  29,byte  12,
+	byte  45,byte  45,byte  44,byte  44,byte  44,byte  44,byte  31,byte  60,
+	byte  43,byte  43,byte  30,byte  59,byte  59,byte  42,byte  42,byte  12,
+	byte  45,byte  45,byte  44,byte  44,byte  44,byte  44,byte  27,byte  43,
+	byte  43,byte  43,byte  26,byte  26,byte  42,byte  42,byte  42,byte  12,
+	byte  28,byte  28,byte  44,byte  44,byte  27,byte  27,byte  27,byte  43,
+	byte  43,byte  26,byte  26,byte  26,byte  42,byte  42,byte  25,byte   8,
+	byte  28,byte  28,byte  44,byte  44,byte  27,byte  27,byte  27,byte  43,
+	byte  43,byte  26,byte  26,byte   9,byte  42,byte  42,byte  25,byte   8,
+	byte  28,byte  28,byte  28,byte  27,byte  27,byte  27,byte  10,byte  43,
+	byte  26,byte  26,byte  26,byte   9,byte  42,byte  42,byte  25,byte   8,
+	byte  41,byte  41,byte  57,byte  40,byte  40,byte  40,byte  40,byte  56,
+	byte  39,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   8,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  40,byte  23,byte  39,
+	byte  39,byte  39,byte  22,byte  55,byte  38,byte  38,byte  38,byte   4,
+	byte  24,byte  24,byte  40,byte  40,byte  23,byte  23,byte  23,byte  39,
+	byte  39,byte  22,byte  22,byte  22,byte  38,byte  38,byte  21,byte   4,
+	byte  24,byte  24,byte  24,byte  23,byte  23,byte  23,byte  23,byte  39,
+	byte  22,byte  22,byte  22,byte   5,byte  38,byte  38,byte  21,byte   4,
+	byte  24,byte  24,byte  53,byte  23,byte  23,byte   6,byte   6,byte  52,
+	byte  52,byte  22,byte   5,byte   5,byte  51,byte  21,byte  21,byte   4,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte  37,byte  37,byte  36,byte  36,byte  36,byte  36,byte  36,byte  35,
+	byte  35,byte  35,byte  35,byte  18,byte  34,byte  34,byte  34,byte   0,
+	byte  20,byte  20,byte  36,byte  36,byte  19,byte  19,byte  19,byte  35,
+	byte  35,byte  18,byte  18,byte  18,byte  34,byte  34,byte  17,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+};
+
+rgbvmap := array[3*256] of {
+	byte 255,byte 255,byte 255,   byte 255,byte 255,byte 170,
+	byte 255,byte 255,byte  85,   byte 255,byte 255,byte   0,
+	byte 255,byte 170,byte 255,   byte 255,byte 170,byte 170,
+	byte 255,byte 170,byte  85,   byte 255,byte 170,byte   0,
+	byte 255,byte  85,byte 255,   byte 255,byte  85,byte 170,
+	byte 255,byte  85,byte  85,   byte 255,byte  85,byte   0,
+	byte 255,byte   0,byte 255,   byte 255,byte   0,byte 170,
+	byte 255,byte   0,byte  85,   byte 255,byte   0,byte   0,
+	byte 238,byte   0,byte   0,   byte 238,byte 238,byte 238,
+	byte 238,byte 238,byte 158,   byte 238,byte 238,byte  79,
+	byte 238,byte 238,byte   0,   byte 238,byte 158,byte 238,
+	byte 238,byte 158,byte 158,   byte 238,byte 158,byte  79,
+	byte 238,byte 158,byte   0,   byte 238,byte  79,byte 238,
+	byte 238,byte  79,byte 158,   byte 238,byte  79,byte  79,
+	byte 238,byte  79,byte   0,   byte 238,byte   0,byte 238,
+	byte 238,byte   0,byte 158,   byte 238,byte   0,byte  79,
+	byte 221,byte   0,byte  73,   byte 221,byte   0,byte   0,
+	byte 221,byte 221,byte 221,   byte 221,byte 221,byte 147,
+	byte 221,byte 221,byte  73,   byte 221,byte 221,byte   0,
+	byte 221,byte 147,byte 221,   byte 221,byte 147,byte 147,
+	byte 221,byte 147,byte  73,   byte 221,byte 147,byte   0,
+	byte 221,byte  73,byte 221,   byte 221,byte  73,byte 147,
+	byte 221,byte  73,byte  73,   byte 221,byte  73,byte   0,
+	byte 221,byte   0,byte 221,   byte 221,byte   0,byte 147,
+	byte 204,byte   0,byte 136,   byte 204,byte   0,byte  68,
+	byte 204,byte   0,byte   0,   byte 204,byte 204,byte 204,
+	byte 204,byte 204,byte 136,   byte 204,byte 204,byte  68,
+	byte 204,byte 204,byte   0,   byte 204,byte 136,byte 204,
+	byte 204,byte 136,byte 136,   byte 204,byte 136,byte  68,
+	byte 204,byte 136,byte   0,   byte 204,byte  68,byte 204,
+	byte 204,byte  68,byte 136,   byte 204,byte  68,byte  68,
+	byte 204,byte  68,byte   0,   byte 204,byte   0,byte 204,
+	byte 170,byte 255,byte 170,   byte 170,byte 255,byte  85,
+	byte 170,byte 255,byte   0,   byte 170,byte 170,byte 255,
+	byte 187,byte 187,byte 187,   byte 187,byte 187,byte  93,
+	byte 187,byte 187,byte   0,   byte 170,byte  85,byte 255,
+	byte 187,byte  93,byte 187,   byte 187,byte  93,byte  93,
+	byte 187,byte  93,byte   0,   byte 170,byte   0,byte 255,
+	byte 187,byte   0,byte 187,   byte 187,byte   0,byte  93,
+	byte 187,byte   0,byte   0,   byte 170,byte 255,byte 255,
+	byte 158,byte 238,byte 238,   byte 158,byte 238,byte 158,
+	byte 158,byte 238,byte  79,   byte 158,byte 238,byte   0,
+	byte 158,byte 158,byte 238,   byte 170,byte 170,byte 170,
+	byte 170,byte 170,byte  85,   byte 170,byte 170,byte   0,
+	byte 158,byte  79,byte 238,   byte 170,byte  85,byte 170,
+	byte 170,byte  85,byte  85,   byte 170,byte  85,byte   0,
+	byte 158,byte   0,byte 238,   byte 170,byte   0,byte 170,
+	byte 170,byte   0,byte  85,   byte 170,byte   0,byte   0,
+	byte 153,byte   0,byte   0,   byte 147,byte 221,byte 221,
+	byte 147,byte 221,byte 147,   byte 147,byte 221,byte  73,
+	byte 147,byte 221,byte   0,   byte 147,byte 147,byte 221,
+	byte 153,byte 153,byte 153,   byte 153,byte 153,byte  76,
+	byte 153,byte 153,byte   0,   byte 147,byte  73,byte 221,
+	byte 153,byte  76,byte 153,   byte 153,byte  76,byte  76,
+	byte 153,byte  76,byte   0,   byte 147,byte   0,byte 221,
+	byte 153,byte   0,byte 153,   byte 153,byte   0,byte  76,
+	byte 136,byte   0,byte  68,   byte 136,byte   0,byte   0,
+	byte 136,byte 204,byte 204,   byte 136,byte 204,byte 136,
+	byte 136,byte 204,byte  68,   byte 136,byte 204,byte   0,
+	byte 136,byte 136,byte 204,   byte 136,byte 136,byte 136,
+	byte 136,byte 136,byte  68,   byte 136,byte 136,byte   0,
+	byte 136,byte  68,byte 204,   byte 136,byte  68,byte 136,
+	byte 136,byte  68,byte  68,   byte 136,byte  68,byte   0,
+	byte 136,byte   0,byte 204,   byte 136,byte   0,byte 136,
+	byte  85,byte 255,byte  85,   byte  85,byte 255,byte   0,
+	byte  85,byte 170,byte 255,   byte  93,byte 187,byte 187,
+	byte  93,byte 187,byte  93,   byte  93,byte 187,byte   0,
+	byte  85,byte  85,byte 255,   byte  93,byte  93,byte 187,
+	byte 119,byte 119,byte 119,   byte 119,byte 119,byte   0,
+	byte  85,byte   0,byte 255,   byte  93,byte   0,byte 187,
+	byte 119,byte   0,byte 119,   byte 119,byte   0,byte   0,
+	byte  85,byte 255,byte 255,   byte  85,byte 255,byte 170,
+	byte  79,byte 238,byte 158,   byte  79,byte 238,byte  79,
+	byte  79,byte 238,byte   0,   byte  79,byte 158,byte 238,
+	byte  85,byte 170,byte 170,   byte  85,byte 170,byte  85,
+	byte  85,byte 170,byte   0,   byte  79,byte  79,byte 238,
+	byte  85,byte  85,byte 170,   byte 102,byte 102,byte 102,
+	byte 102,byte 102,byte   0,   byte  79,byte   0,byte 238,
+	byte  85,byte   0,byte 170,   byte 102,byte   0,byte 102,
+	byte 102,byte   0,byte   0,   byte  79,byte 238,byte 238,
+	byte  73,byte 221,byte 221,   byte  73,byte 221,byte 147,
+	byte  73,byte 221,byte  73,   byte  73,byte 221,byte   0,
+	byte  73,byte 147,byte 221,   byte  76,byte 153,byte 153,
+	byte  76,byte 153,byte  76,   byte  76,byte 153,byte   0,
+	byte  73,byte  73,byte 221,   byte  76,byte  76,byte 153,
+	byte  85,byte  85,byte  85,   byte  85,byte  85,byte   0,
+	byte  73,byte   0,byte 221,   byte  76,byte   0,byte 153,
+	byte  85,byte   0,byte  85,   byte  85,byte   0,byte   0,
+	byte  68,byte   0,byte   0,   byte  68,byte 204,byte 204,
+	byte  68,byte 204,byte 136,   byte  68,byte 204,byte  68,
+	byte  68,byte 204,byte   0,   byte  68,byte 136,byte 204,
+	byte  68,byte 136,byte 136,   byte  68,byte 136,byte  68,
+	byte  68,byte 136,byte   0,   byte  68,byte  68,byte 204,
+	byte  68,byte  68,byte 136,   byte  68,byte  68,byte  68,
+	byte  68,byte  68,byte   0,   byte  68,byte   0,byte 204,
+	byte  68,byte   0,byte 136,   byte  68,byte   0,byte  68,
+	byte   0,byte 255,byte   0,   byte   0,byte 170,byte 255,
+	byte   0,byte 187,byte 187,   byte   0,byte 187,byte  93,
+	byte   0,byte 187,byte   0,   byte   0,byte  85,byte 255,
+	byte   0,byte  93,byte 187,   byte   0,byte 119,byte 119,
+	byte   0,byte 119,byte   0,   byte   0,byte   0,byte 255,
+	byte   0,byte   0,byte 187,   byte   0,byte   0,byte 119,
+	byte  51,byte  51,byte  51,   byte   0,byte 255,byte 255,
+	byte   0,byte 255,byte 170,   byte   0,byte 255,byte  85,
+	byte   0,byte 238,byte  79,   byte   0,byte 238,byte   0,
+	byte   0,byte 158,byte 238,   byte   0,byte 170,byte 170,
+	byte   0,byte 170,byte  85,   byte   0,byte 170,byte   0,
+	byte   0,byte  79,byte 238,   byte   0,byte  85,byte 170,
+	byte   0,byte 102,byte 102,   byte   0,byte 102,byte   0,
+	byte   0,byte   0,byte 238,   byte   0,byte   0,byte 170,
+	byte   0,byte   0,byte 102,   byte  34,byte  34,byte  34,
+	byte   0,byte 238,byte 238,   byte   0,byte 238,byte 158,
+	byte   0,byte 221,byte 147,   byte   0,byte 221,byte  73,
+	byte   0,byte 221,byte   0,   byte   0,byte 147,byte 221,
+	byte   0,byte 153,byte 153,   byte   0,byte 153,byte  76,
+	byte   0,byte 153,byte   0,   byte   0,byte  73,byte 221,
+	byte   0,byte  76,byte 153,   byte   0,byte  85,byte  85,
+	byte   0,byte  85,byte   0,   byte   0,byte   0,byte 221,
+	byte   0,byte   0,byte 153,   byte   0,byte   0,byte  85,
+	byte  17,byte  17,byte  17,   byte   0,byte 221,byte 221,
+	byte   0,byte 204,byte 204,   byte   0,byte 204,byte 136,
+	byte   0,byte 204,byte  68,   byte   0,byte 204,byte   0,
+	byte   0,byte 136,byte 204,   byte   0,byte 136,byte 136,
+	byte   0,byte 136,byte  68,   byte   0,byte 136,byte   0,
+	byte   0,byte  68,byte 204,   byte   0,byte  68,byte 136,
+	byte   0,byte  68,byte  68,   byte   0,byte  68,byte   0,
+	byte   0,byte   0,byte 204,   byte   0,byte   0,byte 136,
+	byte   0,byte   0,byte  68,   byte   0,byte   0,byte   0,
+};
+
+clamp: array of int;
+
+# Remap pixels according to standard Inferno colormap,
+# then convert to Inferno compressed image encoding.
+# If second component of return is non-nil, it is a compressed mask.
+image2enc(i: ref Rawimage, errdiff: int): (array of byte, array of byte, string)
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+
+	j: int;
+	dx := i.r.max.x-i.r.min.x;
+	dy := i.r.max.y-i.r.min.y;
+	cmap := i.cmap;
+
+	if(clamp == nil){
+		clamp = array[64+256+64] of int;
+		for(j=0; j<64; j++)
+			clamp[j] = 0;
+		for(j=0; j<256; j++)
+			clamp[64+j] = (j>>4);
+		for(j=0; j<64; j++)
+			clamp[64+256+j] = (255>>4);
+	}
+
+	pic := i.chans[0];
+	npic := len pic;
+
+	maski : ref Rawimage = nil;
+	if(i.transp) {
+		mpic := array[npic] of byte;
+		maski = ref Rawimage ( i.r, nil, 0, byte 0, 1, array[1] of {mpic}, 0, 0 );
+		for(j = 0; j < npic; j++)
+			if(pic[j] == i.trindex)
+				mpic[j] = byte 0;
+			else
+				mpic[j] = byte 1;
+	}
+
+
+	case i.chandesc{
+	RImagefile->CRGB1 =>
+		if(cmap == nil)
+			return (nil, nil, "image has no color map");
+		if(i.nchans != 1)
+			return (nil, nil, sys->sprint("can't handle nchans %d", i.nchans));
+		for(j=1; j<=8; j++)
+			if(len cmap == 3*(1<<j))
+				break;
+		if(j > 8)
+			return (nil, nil, sys->sprint("can't understand colormap size 3*%d", len cmap/3));
+		if(len cmap != 3*256){
+			# to avoid a range check in inner loop below, make a full-size cmap
+			cmap1 := array[3*256] of byte;
+			cmap1[0:] = cmap[0:];
+			cmap = cmap1;
+			errdiff = 0;	# why not?
+		}
+		if(errdiff == 0){
+			map := array[256] of byte;
+			k := 0;
+			for(j=0; j<256; j++){
+				r := int cmap[k]>>4;
+				g := int cmap[k+1]>>4;
+				b := int cmap[k+2]>>4;
+				k += 3;
+				map[j] = byte closest[b+16*(g+16*r)];
+			}
+			for(j=0; j<npic; j++)
+				pic[j] = map[int pic[j]];
+		}else{
+			# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+			ered := array[dx+1] of int;
+			egrn := array[dx+1] of int;
+			eblu := array[dx+1] of int;
+			for(j=0; j<=dx; j++)
+				ered[j] = 0;
+			egrn[0:] = ered[0:];
+			eblu[0:] = ered[0:];
+			p := 0;
+			for(y:=0; y<dy; y++){
+				er := 0;
+				eg := 0;
+				eb := 0;
+				for(x:=0; x<dx; x++){
+					in := 3*int pic[p];
+					r := int cmap[in+0]+ered[x];
+					g := int cmap[in+1]+egrn[x];
+					b := int cmap[in+2]+eblu[x];
+					r1 := clamp[r+64];
+					g1 := clamp[g+64];
+					b1 := clamp[b+64];
+					col := int closest[b1+16*(g1+16*r1)];
+					pic[p++] = byte col;
+
+					col *= 3;
+					r -= int rgbvmap[col+0];
+					t := (3*r)>>4;
+					ered[x] = t+er;
+					ered[x+1] += t;
+					er = r-3*t;
+
+					g -= int rgbvmap[col+1];
+					t = (3*g)>>4;
+					egrn[x] = t+eg;
+					egrn[x+1] += t;
+					eg = g-3*t;
+
+					b -= int rgbvmap[col+2];
+					t = (3*b)>>4;
+					eblu[x] = t+eb;
+					eblu[x+1] += t;
+					eb = b-3*t;
+				}
+			}
+		}
+	RImagefile->CRGB =>
+		if(i.nchans != 3)
+			return (nil, nil, sys->sprint("RGB image has %d channels", i.nchans));
+		rpic := i.chans[0];
+		gpic := i.chans[1];
+		bpic := i.chans[2];
+		if(errdiff == 0){
+			for(j=0; j<len rpic; j++){
+				r := int rpic[j]>>4;
+				g := int gpic[j]>>4;
+				b := int bpic[j]>>4;
+				pic[j] = byte closest[b+16*(g+16*r)];
+			}
+		}else{
+			# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+			ered := array[dx+1] of int;
+			egrn := array[dx+1] of int;
+			eblu := array[dx+1] of int;
+			for(j=0; j<=dx; j++)
+				ered[j] = 0;
+			egrn[0:] = ered[0:];
+			eblu[0:] = ered[0:];
+			p := 0;
+			for(y:=0; y<dy; y++){
+				er := 0;
+				eg := 0;
+				eb := 0;
+				for(x:=0; x<dx; x++){
+					r := int rpic[p]+ered[x];
+					g := int gpic[p]+egrn[x];
+					b := int bpic[p]+eblu[x];
+					r1 := clamp[r+64];
+					g1 := clamp[g+64];
+					b1 := clamp[b+64];
+					col := int closest[b1+16*(g1+16*r1)];
+					pic[p++] = byte col;
+
+					col *= 3;
+					r -= int rgbvmap[col+0];
+					t := (3*r)>>4;
+					ered[x] = t+er;
+					ered[x+1] += t;
+					er = r-3*t;
+
+					g -= int rgbvmap[col+1];
+					t = (3*g)>>4;
+					egrn[x] = t+eg;
+					egrn[x+1] += t;
+					eg = g-3*t;
+
+					b -= int rgbvmap[col+2];
+					t = (3*b)>>4;
+					eblu[x] = t+eb;
+					eblu[x+1] += t;
+					eb = b-3*t;
+				}
+			}
+		}
+	RImagefile->CY =>
+		if(i.nchans != 1)
+			return (nil, nil, sys->sprint("Y image has %d chans", i.nchans));
+		rpic := i.chans[0];
+		if(errdiff == 0){
+			for(j=0; j<npic; j++){
+				r := int rpic[j]>>4;
+				pic[j] = byte closest[r+16*(r+16*r)];
+			}
+		}else{
+			# modified floyd steinberg, coefficients (1 0) 3/16, (0, 1) 3/16, (1, 1) 7/16
+			ered := array[dx+1] of int;
+			for(j=0; j<=dx; j++)
+				ered[j] = 0;
+			p := 0;
+			for(y:=0; y<dy; y++){
+				er := 0;
+				for(x:=0; x<dx; x++){
+					r := int rpic[p]+ered[x];
+					r1 := clamp[r+64];
+					col := int closest[r1+16*(r1+16*r1)];
+					pic[p++] = byte col;
+
+					col *= 3;
+					r -= int rgbvmap[col+0];
+					t := (3*r)>>4;
+					ered[x] = t+er;
+					ered[x+1] += t;
+					er = r-3*t;
+				}
+			}
+		}
+	}
+
+	(encim, estr) := i2e(i);
+	encmask : array of byte = nil;
+	if(i.transp && estr == "")
+		(encmask, estr) = i2e(maski);
+	return (encim, encmask, estr);
+}
+
+# Some defs from /usr/inferno/include/image.h
+NMATCH:		con 3;				# shortest match possible
+NRUN:		con (NMATCH+31);	# longest match possible
+NMEM:		con 1024;			# window size
+NDUMP:		con 128;			# maximum length of dump
+NCBLOCK:	con	6000;			# size of compressed blocks
+MAXCB:		con 200;				# maximum number of blocks
+
+HSHIFT:		con 3;
+NHASH:		con (1<<(HSHIFT*NMATCH));
+HMASK:		con (NHASH-1);
+
+Hlist: adt {
+	index: int;
+	next: cyclic ref Hlist;
+	prev: cyclic ref Hlist;
+};
+
+Outbuf: adt {
+	hdr: array of byte;
+	buf: array of byte;
+	buflen: int;
+};
+
+i2e(im: ref Rawimage): (array of byte, string)
+{
+	i : int;
+	pic := im.chans[0];
+	bpl := im.r.max.x - im.r.min.x;
+	nl := im.r.max.y - im.r.min.y;
+	edata := bpl * nl;
+	linei := 0;
+	hash := array[NHASH] of ref Hlist;
+	chain := array[NMEM] of ref Hlist;
+	dumpbuf := array[NDUMP] of byte;
+	blocks := array[MAXCB] of ref Outbuf;
+	hdr := array of byte sys->sprint("compressed\n%11d %11d %11d %11d %11d ",
+		3, im.r.min.x, im.r.min.y, im.r.max.x, im.r.max.y);
+	blocks[0] = ref Outbuf(hdr, nil, 0);
+	outbnum := 1;
+	y := im.r.min.y;
+	for(i = 0; i < NHASH; i++)
+		hash[i] = ref Hlist(0, nil, nil);
+	for(i = 0; i < NMEM; i++)
+		chain[i] = ref Hlist(0, nil, nil);
+
+	# if a line is too narrow, we don't compress at all
+	nmatch := NMATCH;
+	if(nmatch >= bpl)
+		nmatch = 0;
+
+	while(linei < edata) {
+		curblock := ref Outbuf(nil, array[NCBLOCK] of byte, 0);
+		blocks[outbnum] = curblock;
+		outbuf := curblock.buf;
+		cp := 0;
+		h := 0;
+		for(i = 0; i < NHASH; i++) {
+			hi := hash[i];
+			hi.next = nil;
+			hi.prev = nil;
+		}
+		for(i = 0; i < NMEM; i++) {
+			ci := chain[i];
+			ci.next = nil;
+			ci.prev = nil;
+		}
+
+		outp := 0;
+		for(i = 0; i <= nmatch; i++)
+			h = (((h<<HSHIFT)^(int pic[linei+i]))&HMASK);
+		loutp := 0;
+
+	    blockloop:
+		while(linei < edata) {
+			ndump := 0;
+			eline := linei + bpl;
+			for(p := linei; p < eline;) {
+				es : int;
+				if(eline - p < NRUN)
+					es = eline;
+				else
+					es = p + NRUN;
+				q := 0;
+				runlen := 2;
+				hp := hash[h];
+			matchloop:
+				for(hp=hp.next; hp != nil; hp = hp.next) {
+					s := p + runlen;
+					if(s >= es)
+						continue matchloop;
+					t := hp.index + runlen;
+					for(; s >= p; s--){
+						if(pic[s] != pic[t])
+							continue matchloop;
+						t--;
+					}
+					t += runlen+2;
+					s += runlen+2;
+					for(; s < es; s++) {
+						if(pic[s] != pic[t])
+							break;
+						t++;
+					}
+					n := s - p;
+					if(n > runlen) {
+						runlen = n;
+						q = hp.index;
+						if(n == NRUN)
+							break;
+					} 
+				}
+				if(runlen < NMATCH) {
+					if(ndump == NDUMP) {
+						if(NCBLOCK-outp < NDUMP+1)
+							break blockloop;
+						outbuf[outp++] = byte (NDUMP-1+128);
+						outbuf[outp:] = dumpbuf;
+						outp += NDUMP;
+						ndump = 0;
+					}
+					dumpbuf[ndump++] = pic[p];
+					runlen = 1;
+				}
+				else {
+					if(ndump != 0) {
+						if(NCBLOCK-outp < ndump+1)
+							break blockloop;
+						outbuf[outp++] = byte (ndump-1+128);
+						outbuf[outp:] = dumpbuf[0:ndump];
+						outp += ndump;
+						ndump = 0;
+					}
+					offs := p - q - 1;
+					if(NCBLOCK-outp < 2)
+						break blockloop;
+					outbuf[outp++] = byte (((runlen-NMATCH)<<2)+(offs>>8));
+					outbuf[outp++] = byte offs;
+				}
+				for(q = p+runlen; p < q; p++) {
+					c := chain[cp];
+					if(c.prev != nil)
+						c.prev.next = nil;
+					c.next = hash[h].next;
+					c.prev = hash[h];
+					if(c.next != nil)
+						c.next.prev = c;
+					c.prev.next = c;
+					c.index = p;
+					cp++;
+					if(cp == NMEM)
+						cp = 0;
+					if(edata-p > NMATCH)
+						h = (((h<<HSHIFT)^(int pic[p+NMATCH]))&HMASK);
+				}
+			}
+			if(ndump != 0) {
+				if(NCBLOCK-outp < ndump+1)
+					break blockloop;
+				outbuf[outp++] = byte (ndump-1+128);
+				outbuf[outp:] = dumpbuf[0:ndump];
+				outp += ndump;
+			}
+			linei = eline;
+			loutp = outp;
+			y++;
+		}
+
+		# current block output buffer full
+		if(loutp == 0)
+			return (nil, "buffer too small for line");
+		curblock.hdr = array of byte sys->sprint("%11d %11d ", y, loutp);
+		curblock.buflen = loutp;
+		outbnum++;
+		if(outbnum >= MAXCB)
+			return (nil, "too many output blocks");
+	}
+	ntot := 0;
+	for(i = 0; i < outbnum; i++) {
+		b := blocks[i];
+		ntot += len b.hdr + b.buflen;
+	}
+	a := array[ntot] of byte;
+	ai := 0;
+	for(i = 0; i < outbnum; i++) {
+		b := blocks[i];
+		a[ai:] = b.hdr;
+		ai += len b.hdr;
+		if(i > 0) {
+			a[ai:] = b.buf[0:b.buflen];
+			ai += b.buflen;
+		}
+	}
+	return (a, "");
+}
--- /dev/null
+++ b/appl/svc/webget/image2enc.m
@@ -1,0 +1,7 @@
+Image2enc: module
+{
+	PATH:	con "/dis/svc/webget/image2enc.dis";
+
+	image2enc: fn(i: ref RImagefile->Rawimage, errdiff: int): (array of byte, array of byte, string);
+};
+
--- /dev/null
+++ b/appl/svc/webget/message.b
@@ -1,0 +1,249 @@
+implement Message;
+
+include "sys.m";
+	sys: Sys;
+
+include "string.m";
+	S : String;
+
+include "bufio.m";
+	B : Bufio;
+	Iobuf: import B;
+
+include "message.m";
+	msg: Message;
+
+msglog: ref Sys->FD;
+
+init(bufio: Bufio, smod: String)
+{
+	sys = load Sys Sys->PATH;
+	S = smod;
+	B = bufio;
+}
+
+sptab : con " \t";
+crlf : con "\r\n";
+
+Msg.newmsg() : ref Msg
+{
+	return ref Msg("", nil, nil, nil, 0);
+}
+
+# Read a message header from fd and return a Msg
+# the header fields.
+# If withprefix is true, read one line first and put it
+# in the prefixline field of the Msg (without terminating \r\n)
+# Return nil if there is a read error or eof before the
+# header is completely read.
+Msg.readhdr(io: ref Iobuf, withprefix: int) : (ref Msg, string)
+{
+	m := Msg.newmsg();
+	l : list of Nameval = nil;
+	needprefix := withprefix;
+	for(;;) {
+		line := getline(io);
+		n := len line;
+		if(n == 0) {
+			if(withprefix && m.prefixline != "")
+				break;
+			return(nil, "msg read hdr error: no header");
+		}
+		if(line[n-1] != '\n')
+			return (m, "msg read hdr error: incomplete header");
+		if(n >= 2 && line[n-2] == '\r')
+			line = line[0:n-2];
+		else
+			line = line[0:n-1];
+		if(needprefix) {
+			m.prefixline = line;
+			needprefix = 0;
+		}
+		else {
+			if(line == "")
+				break;
+			if(S->in(line[0], sptab)) {
+				if(l == nil)
+					continue;
+				nv := hd l;
+				l = Nameval(nv.name, nv.value + " " + S->drop(line, sptab)) :: tl l;
+			}
+			else {
+				(nam, val) := S->splitl(line, ":");
+				if(val == nil)
+					continue;  # no colon
+				l = Nameval(S->tolower(nam), S->drop(val[1:], sptab)) :: l;
+			}
+		}
+	}
+	nh := len l;
+	if(nh > 0) {
+		m.fields = array[nh] of Nameval;
+		for(i := nh-1; i >= 0; i--) {
+			m.fields[i] = hd l;
+			l = tl l;
+		}
+	}
+	return (m, "");
+}
+
+glbuf := array[300] of byte;
+
+# Like io.gets('\n'), but assume Latin-1 instead of UTF encoding
+getline(io: ref Iobuf): string
+{
+	imax := len glbuf - 1;
+	for(i := 0; i < imax; ) {
+		c := io.getb();
+		if(c < 0)
+			break;
+		if(c < 128)
+			glbuf[i++] = byte c;
+		else
+			i += sys->char2byte(c, glbuf, i);
+		if(c == '\n')
+			break;
+		if(i == imax) {
+			imax += 100;
+			if(imax > 1000)
+				break;	# Header lines aren't supposed to be > 1000
+			newglbuf := array[imax] of byte;
+			newglbuf[0:] = glbuf[0:i];
+			glbuf = newglbuf;
+		}
+	}
+	ans := string glbuf[0:i];
+	return ans;
+}
+
+Bbufsize: con 8000;
+
+# Read the body of the message, assuming the header has been processed.
+# If content-length has been specified, read exactly that many bytes
+# or until eof; else read until done.
+# Return "" if all is OK, else return an error string.
+Msg.readbody(m: self ref Msg, io: ref Iobuf) : string
+{
+	(clfnd, cl) := m.fieldval("content-length");
+	if(clfnd) {
+		clen := int cl;
+		if(clen > 0) {
+			m.body = array[clen] of byte;
+			n := B->io.read(m.body, clen);
+			m.bodylen = n;
+			if(n != clen)
+				return "short read";
+		}
+	}
+	else {
+		m.body = array[Bbufsize] of byte;
+		curlen := 0;
+		for(;;) {
+			avail := len m.body - curlen;
+			if(avail <= 0) {
+				newa := array[len m.body + Bbufsize] of byte;
+				if(curlen > 0)
+					newa[0:] = m.body[0:curlen];
+				m.body = newa;
+				avail = Bbufsize;
+			}
+			n := B->io.read(m.body[curlen:], avail);
+			if(n < 0)
+				return sys->sprint("readbody error %r");
+			if(n == 0)
+				break;
+			else
+				curlen += n;
+		}
+		m.bodylen = curlen;
+	}
+	return "";
+}
+
+# Look for name (lowercase) in the fields of m
+# and (1, field value) if found, or (0,"") if not.
+# If multiple fields with the same name exist,
+# the value is defined as the comma separated list
+# of all such values.
+Msg.fieldval(m: self ref Msg, name: string) : (int, string)
+{
+	n := len m.fields;
+	ans := "";
+	found := 0;
+	for(i := 0; i < n; i++) {
+		if(m.fields[i].name == name) {
+			v := m.fields[i].value;
+			if(found)
+				ans = ans + ", " + v;
+			else
+				ans = v;
+			found = 1;
+		}
+	}
+	return (found, ans);
+}
+
+Msg.addhdrs(m: self ref Msg, hdrs: list of Nameval)
+{
+	nh := len hdrs;
+	if(nh == 0)
+		return;
+	onh := len m.fields;
+	newa := array[nh + onh] of Nameval;
+	newa[0:] = m.fields;
+	i := onh;
+	while(hdrs != nil) {
+		newa[i++] = hd hdrs;
+		hdrs = tl hdrs;
+	}
+	m.fields = newa;
+}
+
+Msg.update(m: self ref Msg, name, value: string)
+{
+	for(i := 0; i < len m.fields; i++)
+		if(m.fields[i].name == name) {
+			m.fields[i] = Nameval(name, value);
+			return;
+		}
+	m.addhdrs(Nameval(name, value) :: nil);
+}
+
+Msg.header(m: self ref Msg) : string
+{
+	s := "";
+	for(i := 0; i < len m.fields; i++) {
+		nv := m.fields[i];
+		s += nv.name + ": " + nv.value + "\n";
+	}
+	return s;
+}
+
+Msg.writemsg(m: self ref Msg, io: ref Iobuf) : string
+{
+	n := 0;
+	if(m.prefixline != nil) {
+		n = B->io.puts(m.prefixline);
+		if(n >= 0)
+			n = B->io.puts(crlf);
+	}
+	for(i := 0; i < len m.fields; i++) {
+		nv := m.fields[i];
+		if(n >= 0)
+			n = B->io.puts(nv.name);
+		if(n >= 0)
+			n = B->io.puts(": ");
+		if(n >= 0)
+			n = B->io.puts(nv.value);
+		if(n >= 0)
+			n = B->io.puts(crlf);
+	}
+	if(n >= 0)
+		n = B->io.puts(crlf);
+	if(n >= 0 && m.bodylen > 0)
+		n = B->io.write(m.body, m.bodylen);
+	if(n < 0)
+		return sys->sprint("msg write error: %r");
+	B->io.flush();
+	return "";
+}
--- /dev/null
+++ b/appl/svc/webget/message.m
@@ -1,0 +1,28 @@
+Message: module
+{
+	PATH:	con "/dis/svc/webget/message.dis";
+
+	init: fn(bufio: Bufio, smod: String);
+
+	Nameval: adt {
+		name: string;
+		value: string;
+	};
+
+	Msg: adt {
+		prefixline: string;
+		prefixbytes: array of byte;
+		fields: array of Nameval;
+		body: array of byte;
+		bodylen: int;
+
+		readhdr: fn(io: ref Bufio->Iobuf, withprefix: int) : (ref Msg, string);
+		readbody: fn(m: self ref Msg, io: ref Bufio->Iobuf) : string;
+		writemsg: fn(m: self ref Msg, io: ref Bufio->Iobuf) : string;
+		header: fn(m: self ref Msg) : string;
+		addhdrs: fn(m: self ref Msg, hdrs: list of Nameval);
+		newmsg: fn() : ref Msg;
+		fieldval: fn(m: self ref Msg, name: string) : (int, string);
+		update: fn(m: self ref Msg, name, value: string);
+	};
+};
--- /dev/null
+++ b/appl/svc/webget/mkfile
@@ -1,0 +1,40 @@
+<../../../mkconfig
+
+TARG=\
+	date.dis\
+	file.dis\
+	ftp.dis\
+	http.dis\
+	image2enc.dis\
+	message.dis\
+	webget.dis\
+	wgutils.dis\
+
+MODULES=\
+	date.m\
+	image2enc.m\
+	message.m\
+	transport.m\
+	wgutils.m\
+
+SYSMODULES=\
+	bufio.m\
+	daytime.m\
+	draw.m\
+	imagefile.m\
+	ssl3.m\
+	string.m\
+	strinttab.m\
+	sys.m\
+	url.m\
+	webget.m\
+
+DISBIN=$ROOT/dis/svc/webget
+
+<$ROOT/mkfiles/mkdis
+
+install:V:	install-logs
+
+install-logs:V:
+	rm -f $ROOT/services/webget/webget.log && cp webget.log $ROOT/services/webget/webget.log
+	# chmod 644 $ROOT/services/webget/webget.log
--- /dev/null
+++ b/appl/svc/webget/transport.m
@@ -1,0 +1,5 @@
+Transport: module
+{
+	init:		fn(w: WebgetUtils);
+	connect:	fn(c: ref Fid, r: ref Req, donec: chan of ref Fid);
+};
--- /dev/null
+++ b/appl/svc/webget/webget.b
@@ -1,0 +1,467 @@
+implement Webget;
+
+# Protocol
+#
+# Client opens /chan/webget and writes one of
+#		GET  0 reqid url types cachectl authcookie\n
+#	    or
+#		POST bodylength reqid url types cachectl authcookie\n
+#		body
+#
+# The possibilities for cachectl are
+#		max-stale=seconds
+#			client is willing to accept a response whose age exceeds
+#			its freshness lifetime (by at most specified seconds)
+#			without revalidation
+#		max-age=seconds
+#			client is unwilling to accept a response whose age
+#			(now - generation time) exceeds specified seconds
+#			without revalidiation
+#		no-cache
+#			unconditional reload
+# Both max-stale and max-age may be specified (separated by comma),
+# but no-cache must appear by itself.
+#
+# Authcookie is optional.  If present, it goes in an Authorization: header.
+#
+# The appropriate transport mechanism gets the entity and
+# responds with one of
+#		OK bodylength reqid type url\n
+#		body
+#	    or
+#		ERROR reqid message\n
+#
+# (In the ERROR case, the message might be "Unauthorized: challenge\n",
+# where challenge is of the form "BASIC realm=xxx (param, param, ...)\n".
+# The user can be prompted for a name:password, and the request repeated
+# with authcookie containing the base64 encoding of name:password).
+
+include	"sys.m";
+	sys: Sys;
+	FD: import sys;
+
+include "draw.m";
+
+include "string.m";
+	S: String;
+
+include "bufio.m";
+	B: Bufio;
+
+include "dial.m";
+	DI: Dial;
+
+include "message.m";
+	M: Message;
+	Msg: import M;
+
+include "url.m";
+	U: Url;
+	ParsedUrl: import U;
+
+include "webget.m";
+
+include "wgutils.m";
+	W: WebgetUtils;
+	Fid, Req: import W;
+
+include "transport.m";
+	
+fhash := array[128] of ref Fid;
+
+Transports: adt
+{
+	scheme:		int;
+	m:		Transport;
+};
+transports: array of ref Transports;
+
+transtab := array[] of {
+	(Url->HTTP,	"/dis/svc/webget/http.dis"),
+	(Url->HTTPS,	nil),	# nil means: same as previous
+	(Url->FILE,	"/dis/svc/webget/file.dis"),
+	(Url->FTP,	"/dis/svc/webget/ftp.dis")
+};
+
+Transpreq: adt
+{
+	index: int;
+	fid: ref Fid;
+	r: ref Req;
+	next: cyclic ref Transpreq;
+};
+
+Rchunk: con 30;
+# Transpmax: con 5;	# max number of simultaneously spawned transports
+Transpmax: con 1;	# max number of simultaneously spawned transports
+
+logfile: con "/services/webget/webget.log";
+DO_LOG: con 1;
+
+stderr: ref FD;
+
+# to start ever-present webget
+init(nil: ref Draw->Context, nil: list of string)
+{
+	dummyctl := chan of int;
+	spawn start(dummyctl);
+	<- dummyctl;
+	<- dummyctl;
+}
+
+# sends a 1 on ctl when ready to serve,
+# 0 if there was some problem starting.
+start(ctl: chan of int)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	ok := 1;
+	ntransp := 0;
+	tqueuehd: ref Transpreq = nil;
+	tqueuetl: ref Transpreq = nil;
+
+	log : ref Sys->FD;
+	if(DO_LOG)
+		log = sys->create(logfile, Sys->OWRITE, 8r666);
+
+	io := sys->file2chan("/chan", "webget");
+	if(io == nil) {
+		sys->fprint(stderr, "webget: failed to post: %r\n");
+		ok = 0;
+	}
+
+	B = load Bufio Bufio->PATH;
+	if(B == nil) {
+		sys->fprint(stderr, "webget: failed to load Bufio: %r\n");
+		ok = 0;
+	}
+	S = load String String->PATH;
+	if(S == nil) {
+		sys->fprint(stderr, "webget: failed to load String: %r\n");
+		ok = 0;
+	}
+	M = load Message Message->PATH;
+	if(M == nil) {
+		sys->fprint(stderr, "webget: failed to load Message: %r\n");
+		ok = 0;
+	}
+	M->init(B, S);
+	U = load Url Url->PATH;
+	if(U == nil) {
+		sys->fprint(stderr, "webget: failed to load Url: %r\n");
+		ok = 0;
+	}
+	U->init();
+	W = load WebgetUtils WebgetUtils->PATH;
+	if(W == nil) {
+		sys->fprint(stderr, "webget: failed to load WebgetUtils: %r\n");
+		ok = 0;
+	}
+	if(!ok) {
+		ctl <-= 0;
+		return;
+	}
+	W->init(M, S, B, U, DI, log);
+
+	loadtransmod();
+
+	donec := chan of ref Fid;
+	ctl <-= 1;
+	
+
+    altloop:
+	for(;;) alt {
+	(nil, data, fid, wc) := <-io.write =>
+		if(wc == nil) {
+			finish(fid);
+			continue altloop;
+		}
+		ndata := len data;
+		c := lookup(fid);
+		W->log(c, "\nREQUEST: " + string data);
+		iw := c.writer;
+		n := len c.reqs;
+		if(iw == n) {
+			newrs := array[n + Rchunk] of ref Req;
+			newrs[0:] = c.reqs[0:n];
+			c.reqs = newrs;
+		}
+		r := c.reqs[iw];
+		err := "";
+		if(r == nil) {
+			# initial part (or all) of a request
+			r = ref Req(iw, "", 0, "", "", "", "", "", nil, nil, nil);
+			c.reqs[iw] = r;
+
+			# expect at least the prefix line to be in data
+			prefix := "";
+			for(i := 0; i < ndata; i++) {
+				if(int data[i] == '\n') {
+					prefix = string data[0:i];
+					if(i+1 < ndata) {
+						r.body = array[ndata-i-1] of byte;
+						r.body[0:] = data[i+1:ndata];
+					}
+					break;
+				}
+			}
+			if(prefix == "")
+				err = "no prefix line";
+			else if(prefix == "FINISH") {
+				writereply(wc, len data, "");
+				finish(fid);
+				continue altloop;
+			}
+			else {
+				(nl, l) := sys->tokenize(prefix, "∎");
+				if(nl != 6 && nl != 7)
+					err = "wrong number of fields in " + prefix;
+				else {
+					r.method = hd l;
+					l = tl l;
+					r.bodylen = int hd(l);
+					l = tl l;
+					r.reqid = hd l;
+					l = tl l;
+					r.loc = hd l;
+					l = tl l;
+					r.types = hd l;
+					l = tl l;
+					r.cachectl = hd l;
+					l = tl l;
+					if(l != nil)
+						r.auth = hd l;
+					locurl := U->makeurl(r.loc);
+					if(locurl.scheme == U->MAILTO)
+						err = "webget shouldn't get mailto";
+					else if(locurl.scheme == U->NOSCHEME || 
+					   (locurl.scheme != U->FILE && (locurl.host == "" || locurl.pstart != "/")))
+						err = "url not absolute: " + r.loc;
+					r.url = locurl;
+				}
+			}
+			if(err != "")
+				err = "webget protocol error: " + err;
+		}
+		else {
+			# continuation of request: more body
+			olen := len r.body;
+			newa := array[olen + ndata] of byte;
+			newa[0:] = r.body[0:olen];
+			newa[olen:] = data[0:ndata];
+			r.body = newa;
+		}
+		if(err == "" && len r.body == r.bodylen) {
+			# request complete: spawn off transport handler
+			c.writer++;
+			scheme := r.url.scheme;
+			found := 0;
+			for(i := 0; i < len transports; i++) {
+				if(transports[i].scheme == scheme) {
+					found = 1;
+					break;
+				}
+			}
+			if(found == 0)
+				err = "don't know how to fetch " + r.loc;
+			else {
+				if(ntransp < Transpmax) {
+					W->log(c, "transport " + string scheme + ":  get " + r.loc);
+					spawn transports[i].m->connect(c, r, donec);
+					ntransp++;
+				}
+				else {
+					# too many active transports: queue this until later
+					tr := ref Transpreq(i, c, r, nil);
+					if(tqueuetl == nil)
+						tqueuehd = tqueuetl = tr;
+					else {
+						tqueuetl.next = tr;
+						tqueuetl = tr;
+					}
+				}
+			}
+		}
+		if(err != "") {
+			writereply(wc, -1, err);
+			W->log(c, err);
+			c.reqs[iw] = nil;
+		}
+		else
+			writereply(wc, ndata, "");
+
+	(nil, nbyte, fid, rc) := <-io.read =>
+		if(rc == nil) {
+			finish(fid);
+			continue altloop;
+		}
+		c := lookup(fid);
+		c.nbyte = nbyte;
+		c.rc = rc;
+		readans(c);
+	c := <- donec =>
+		ntransp--;
+		if(tqueuehd != nil) {
+			tr := tqueuehd;
+			tqueuehd = tr.next;
+			if(tqueuehd == nil)
+				tqueuetl = nil;
+			W->log(c, "transport:  get " + tr.r.loc);
+			spawn transports[tr.index].m->connect(tr.fid, tr.r, donec);
+			ntransp++;
+		}
+		readans(c);
+		c = nil;
+	}
+}
+
+loadtransmod()
+{
+	transports = array[len transtab] of ref Transports;
+	j := 0;
+	prevt : ref Transports = nil;
+	for(i := 0; i < len transtab; i++) {
+		(scheme, path) := transtab[i];
+		if(path == nil) {
+			if(prevt != nil)
+				transports[j++] = ref Transports(scheme, prevt.m);
+		}
+		else {
+			t := load Transport path;
+			if(t == nil) {
+				sys->fprint(stderr, "failed to load %s: %r\n", path);
+				continue;
+			}
+	
+			t->init(W);
+
+			ts := ref Transports(scheme, t);
+			transports[j++] = ts;
+			prevt = ts;
+		}
+	}
+}
+
+# Answer a read request c.nbyte bytes, reply to go to c.rc.
+# If c.readr is not -1, it is the index of a req with the currently
+# being consumed reply.
+# c.nread contains the number of bytes of this message read so far.
+readans(c: ref Fid)
+{
+	n := c.nbyte;
+	if(n <= 0)
+		return;
+	ir := c.readr;
+	if(ir == -1) {
+		# look for ready reply
+		for(i := 0; i < c.writer; i++) {
+			r := c.reqs[i];
+			if(r != nil && r.reply != nil)
+				break;
+		}
+		if(i == c.writer) {
+			return;
+		}
+		ir = i;
+	}
+	r := c.reqs[ir];
+	m := r.reply;
+	if(m == nil) {
+		W->log(c, "readans bad state: nil reply");
+		readreply(c, nil, "");
+		return;
+	}
+	if(m.prefixbytes == nil && m.prefixline != "")
+		m.prefixbytes = array of byte m.prefixline;
+	plen := len m.prefixbytes;
+	blen := m.bodylen;	
+	ntot := plen + blen;
+	nread := c.nread;
+	if(nread == 0)
+		W->log(c, "\nREPLY: " + m.prefixline);
+	nrest := ntot - nread;
+	if(nrest <= 0) {
+		W->log(c, "readans bad state: 0 left");
+		readreply(c, nil, "");
+		return;
+	}
+	if(n > nrest)
+		n = nrest;
+	n1 := plen - nread;
+	if(n1 > 0) {
+		if(n1 > n)
+			n1 = n;
+		readreply(c, m.prefixbytes[nread:nread + n1], "");
+		c.nread += n1;
+	}
+	else {
+		bpos := nread - plen;
+		n2 := blen - bpos;
+		if(n > n2)
+			n = n2;
+		readreply(c, m.body[bpos:bpos+n], "");
+		c.nread += n;
+	}
+	if(c.nread >= ntot) {
+		c.reqs[ir] = nil;
+		c.readr = -1;
+		c.nbyte = 0;
+		c.nread = 0;
+		c.rc = nil;
+		# move back write pointer as far as possible
+		if(c.writer == ir+1) {
+			while(ir >= 0 && c.reqs[ir] == nil)
+				ir--;
+			c.writer = ir+1;
+		}
+	}
+	else
+		c.readr = ir;
+}
+
+# Reply to a write command.
+writereply(wc: Sys->Rwrite, n: int, err: string)
+{
+	wc <-= (n, err);
+}
+
+readreply(c: ref Fid, a: array of byte, err: string)
+{
+	rc := c.rc;
+	if(rc != nil)
+		rc <-= (a, err);
+	c.nbyte = 0;
+}
+
+lookup(fid: int): ref Fid
+{
+	h := fid%len fhash;
+	for(f := fhash[h]; f != nil; f = f.link)
+		if(f.fid == fid)
+			return f;
+	f = ref Fid(fid, fhash[h], array[Rchunk] of ref Req, 0, -1, 0, 0, nil);
+	fhash[h] = f;
+
+	W->log(f, "\nNEW CLIENT");
+
+	return f;	
+}
+
+finish(fid: int)
+{
+	W->log(nil, "finish");
+	h := fid%len fhash;
+
+	prev: ref Fid;
+	for(f := fhash[h]; f != nil; f = f.link) {
+		if(f.fid == fid) {
+			f.rc = nil;
+			W->log(f, "client finished");
+			if(prev == nil)
+				fhash[h] = f.link;
+			else
+				prev.link = f.link;
+			return;
+		}
+	}
+}
--- /dev/null
+++ b/appl/svc/webget/wgutils.b
@@ -1,0 +1,305 @@
+implement WebgetUtils;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+
+include "bufio.m";
+
+include "dial.m";
+
+include "imagefile.m";	
+	readgif, readjpg, readxbitmap: RImagefile;
+
+include "image2enc.m";
+	image2enc: Image2enc;
+
+include "message.m";
+
+include "url.m";
+
+include "wgutils.m";
+	Iobuf: import B;
+
+include "strinttab.m";
+	T: StringIntTab;
+
+Msg, Nameval: import M;
+ParsedUrl: import U;
+
+logfd: ref Sys->FD;
+
+# return from acceptmatch; and conv arg to readbody
+BadConv, NoConv, Gif2xcompressed, Jpeg2xcompressed, Xbm2xcompressed: con iota;
+
+# Both extensions and Content-Types can be in same table.
+# This array should be kept sorted
+mtypes := array[] of { T->StringInt
+	("ai", ApplPostscript),
+	("application/html", TextHtml),
+	("application/pdf", ApplPdf),
+	("application/postscript", ApplPostscript),
+	("application/rtf", ApplRtf),
+	("application/soap+xml", TextPlain),
+	("application/x-html", TextHtml),
+	("au", AudioBasic),
+	("audio/au", AudioBasic),
+	("audio/basic", AudioBasic),
+	("bit", ImageXCompressed),
+	("bit2", ImageXCompressed2),
+	("eps", ApplPostscript),
+	("gif", ImageGif),
+	("htm", TextHtml),
+	("html", TextHtml),
+	("image/gif", ImageGif),
+	("image/ief", ImageIef),
+	("image/jpeg", ImageJpeg),
+	("image/tiff", ImageTiff),
+	("image/x-compressed", ImageXCompressed),
+	("image/x-compressed2", ImageXCompressed2),
+	("image/x-xbitmap", ImageXXBitmap),
+	("jpe", ImageJpeg),
+	("jpeg", ImageJpeg),
+	("jpg", ImageJpeg),
+	("pdf", ApplPdf),
+	("ps", ApplPostscript),
+	("text", TextPlain),
+	("text/html", TextHtml),
+	("text/plain", TextPlain),
+	("text/x-html", TextHtml),
+	("text/xml", TextXml),
+	("tif", ImageTiff),
+	("tiff", ImageTiff),
+	("txt", TextPlain),
+	("video/mpeg", VideoMpeg),
+	("video/quicktime", VideoQuicktime),
+};
+
+# following array must track media type def in wgutils.m
+mnames := array[] of {
+	"application/x-unknown",
+	"text/plain",
+	"text/html",
+	"application/postscript",
+	"application/rtf",
+	"application/pdf",
+	"image/jpeg",
+	"image/gif",
+	"image/ief",
+	"image/tiff",
+	"image/x-compressed",
+	"image/x-compressed2",
+	"image/x-xbitmap",
+	"audio/basic",
+	"video/mpeg",
+	"video/quicktime",
+	"application/soap+xml",
+	"text/xml"
+};
+
+init(m: Message, s: String, b: Bufio, u: Url, di: Dial, lfd: ref Sys->FD)
+{
+	sys = load Sys Sys->PATH;
+
+	M = m;
+	S = s;
+	B = b;
+	U = u;
+	DI = di;
+	logfd = lfd;
+	T = load StringIntTab StringIntTab->PATH;
+	readgif = load RImagefile RImagefile->READGIFPATH;
+	readjpg = load RImagefile RImagefile->READJPGPATH;
+	readxbitmap = load RImagefile RImagefile->READXBMPATH;
+	image2enc = load Image2enc Image2enc->PATH;
+	if(T == nil || readgif == nil || readjpg == nil || readxbitmap == nil || image2enc == nil) {
+		sys->fprint(sys->fildes(2), "webget: failed to load T, readgif, readjpg, readxbitmap, or imageremap: %r\n");
+		return;
+	}
+	readgif->init(B);
+	readjpg->init(B);
+	readxbitmap->init(B);
+}
+
+# Return msg with error provoked by bad user action
+usererr(r: ref Req, msg: string) : ref Msg
+{
+	m := Msg.newmsg();
+	m.prefixline = sys->sprint("ERROR %s %s\n", r.reqid, msg);
+	m.bodylen = 0;
+	return m;
+}
+
+okprefix(r: ref Req, mrep: ref Msg)
+{
+	(nil, sctype) := mrep.fieldval("content-type");
+	if(sctype == "")
+		sctype = "text/html";
+	else
+		sctype = canon_mtype(sctype);
+	(nil, cloc) := mrep.fieldval("content-location");
+	if(cloc == "")
+		cloc = "unknown";
+	mrep.prefixline = "OK " + string mrep.bodylen + " " + r.reqid + " " + sctype + " " + cloc +"\n";
+}
+
+canon_mtype(s: string) : string
+{
+	# lowercase, and remove possible parameter
+	ls := S->tolower(s);
+	(ts, nil) := S->splitl(ls, "; ");
+	return ts;
+}
+
+mediatype(s: string, dflt: int) : int
+{
+	(fnd, val) := T->lookup(mtypes, canon_mtype(s));
+	if(!fnd)
+		val = dflt;
+	return val;
+}
+
+acceptmatch(ctype: int, accept: string) : int
+{
+	conv := BadConv;
+	(nil,l) := sys->tokenize(accept, ",");
+	while(l != nil) {
+		a := S->drop(hd l, " \t");
+		mt := mediatype(a, -1);
+		match := (ctype == mt) || (a == "*/*")
+			|| ((ctype == ImageXCompressed || ctype == ImageXCompressed2)
+			   && (mt == ImageJpeg || mt == ImageGif || mt == ImageXXBitmap));
+		if(match) {
+			if(ctype == ImageGif)
+				conv = Gif2xcompressed;
+			else if(ctype == ImageJpeg)
+				conv = Jpeg2xcompressed;
+			else if(ctype == ImageXXBitmap)
+				conv = Xbm2xcompressed;
+			else
+				conv = NoConv;
+			break;
+		}
+		l = tl l;
+	}
+	return conv;
+}
+
+# Get the body of the message whose header is in mrep,
+# if io != nil.
+# First check that the content type is acceptable.
+# Image types will get converted into Inferno compressed format.
+# If there is an error, return error string, else "".
+# If no error, mrep will contain content-encoding, content-location,
+# and content-type fields (guessed if they weren't orignally there).
+ 
+getdata(io: ref Iobuf, m: ref Msg, accept: string, url: ref ParsedUrl) : string
+{
+	(efnd, etype) := m.fieldval("content-encoding");
+	if(efnd)
+		return "content is encoded: " + etype;
+	ctype := UnknownType;
+	(tfnd, sctype) := m.fieldval("content-type");
+	if(tfnd)
+		ctype = mediatype(sctype, UnknownType);
+	else {
+		# try to guess type from extension
+		sctype = "(unknown)";
+		(nil, name) := S->splitr(url.path, "/");
+		if(name != "") {
+			(f, ext) := S->splitr(name, ".");
+			if(f != "" && ext != "") {
+				ctype = mediatype(ext, UnknownType);
+				if(ctype != UnknownType) {
+					sctype = mnames[ctype];
+					m.update("content-type", sctype);
+				}
+			}
+		}
+	}
+	transform := acceptmatch(ctype, accept);
+	if(transform == BadConv)
+		return "Unacceptable media type: " + sctype;
+	(clfnd, cloc) := m.fieldval("content-location");
+	if(!clfnd) {
+		cloc = url.tostring();
+		m.update("content-location", cloc);
+	}
+	if(transform != NoConv) {
+		rawimg: ref RImagefile->Rawimage;
+		err: string;
+		if(transform == Gif2xcompressed)
+			(rawimg, err) = readgif->read(io);
+		else if(transform == Jpeg2xcompressed)
+			(rawimg, err) = readjpg->read(io);
+		else if(transform == Xbm2xcompressed)
+			(rawimg, err) = readxbitmap->read(io);
+		# if gif file has multiple images, we are supposed to animate,
+		# but the first one should be there
+		if(err != "" && err != "ReadGIF: can't handle multiple images in file")
+			return "error converting image file: " + err;
+		(data, mask, e) := image2enc->image2enc(rawimg, 1);
+		if(e != "")
+			return "error remapping and encoding image file: " + e;
+		if(mask == nil)
+			sctype = "image/x-compressed";
+		else {
+			sctype = "image/x-compressed2";
+			newdata := array[len data + len mask] of byte;
+			newdata[0:] = data[0:];
+			newdata[len data:] = mask[0:];
+			data = newdata;
+		}
+		m.body = data;
+		m.bodylen = len data;
+		m.update("content-type", sctype);
+		m.update("content-length", string m.bodylen);
+	}
+	else {
+		# io will be nil if m came from cache
+		if(io != nil) {
+			e := m.readbody(io);
+			if(e != "")
+				return "reading body: " + e;
+		}
+	}
+	return "";
+}
+
+# Change an accept spec from webget client into one we can send
+# to http server.  This means image/x-compressed must be
+# changed into image formats we can handle: i.e., gif or jpeg
+fixaccept(a: string) : string
+{
+	(nil,l) := sys->tokenize(a, ",");
+	ans := "";
+	sep := "";
+	while(l != nil) {
+		s := S->drop(hd l, " \t");
+		if(s == "image/x-compressed")
+			ans += sep + "image/gif,image/jpeg,image/x-xbitmap";
+		else
+			ans += sep + s;
+		sep = ",";
+		l = tl l;
+	}
+	if(ans == "")
+		ans = "*/*";
+	return ans;
+}
+
+log(c: ref Fid, msg: string)
+{
+	if(logfd != nil) {
+		# don't use print in case msg is longer than buf
+		s := "";
+		if(c != nil)
+			s += (string c.fid) + ": ";
+		s += msg + "\n";
+		b := array of byte s;
+		sys->write(logfd, b, len b);
+	}
+}
--- /dev/null
+++ b/appl/svc/webget/wgutils.m
@@ -1,0 +1,54 @@
+WebgetUtils: module
+{
+	PATH: con "/dis/svc/webget/wgutils.dis";
+
+	Req: adt
+	{
+		index:	int;
+		method:	string;
+		bodylen:	int;
+		reqid:	string;
+		loc:		string;
+		types:	string;
+		cachectl:	string;
+		auth:		string;
+		body:	array of byte;
+		url:		ref Url->ParsedUrl;
+		reply:	ref Message->Msg;
+	};
+
+	Fid: adt
+	{
+		fid:		int;
+		link:		cyclic ref Fid;
+		reqs:		array of ref Req;
+		writer:	int;
+		readr:	int;
+		nbyte:	int;
+		nread:	int;
+		rc:		Sys->Rread;
+	};
+
+	M: Message;
+	B: Bufio;
+	S: String;
+	U: Url;
+	DI: Dial;
+
+	# media types (must track mnames array in wgutils.b)
+	UnknownType,
+	TextPlain, TextHtml,
+	ApplPostscript, ApplRtf, ApplPdf,
+	ImageJpeg, ImageGif, ImageIef, ImageTiff,
+	ImageXCompressed, ImageXCompressed2, ImageXXBitmap,
+	AudioBasic,
+	VideoMpeg, VideoQuicktime, Soap, TextXml: con iota;
+
+	init : fn(m: Message, s: String, b: Bufio, u: Url, di: Dial, logfd: ref Sys->FD);
+	usererr: fn(r: ref Req, msg: string) : ref Message->Msg;
+	okprefix: fn(r: ref Req, mrep: ref Message->Msg);
+	getdata: fn(io: ref Bufio->Iobuf, m: ref Message->Msg,
+					accept: string, url: ref Url->ParsedUrl) : string;
+	fixaccept: fn(a: string) : string;
+	log: fn(c: ref Fid, msg: string);
+};
--- /dev/null
+++ b/appl/tiny/broke.b
@@ -1,0 +1,70 @@
+implement Broke;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+Broke: module
+{
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+init(nil: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	fd := sys->open("/prog", Sys->OREAD);
+	if(fd == nil)
+		err(sys->sprint("can't open /prog: %r"));
+	killed := "";
+	for(;;){
+		(n, dir) := sys->dirread(fd);
+		if(n <= 0){
+			if(n < 0)
+				err(sys->sprint("error reading /prog: %r"));
+			break;
+		}
+		for(i := 0; i < n; i++)
+			if(isbroken(dir[i].name) && kill(dir[i].name))
+				killed += sys->sprint(" %s", dir[i].name);
+	}
+	if(killed != nil)
+		sys->print("%s\n", killed);
+}
+
+isbroken(pid: string): int
+{
+	statf := "/prog/" + pid + "/status";
+	fd := sys->open(statf, Sys->OREAD);
+	if (fd == nil)
+		return 0;
+	buf := array[256] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n < 0) {	# process died or is exiting
+		# sys->fprint(stderr(), "broke: can't read %s: %r\n", statf);
+		return 0;
+	}
+	(nf, l) := sys->tokenize(string buf[0:n], " ");
+	return nf >= 5 && hd tl tl tl tl l == "broken";
+}
+
+kill(pid: string): int
+{
+	ctl := "/prog/" + pid + "/ctl";
+	fd := sys->open(ctl, sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "kill") < 0){
+		sys->fprint(stderr(), "broke: can't kill %s: %r\n", pid);	# but press on
+		return 0;
+	}
+	return 1;
+}
+
+err(s: string)
+{
+	sys->fprint(sys->fildes(2), "broke: %s\n", s);
+	raise "fail:error";
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/tiny/kill.b
@@ -1,0 +1,146 @@
+implement Kill;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "arg.m";
+
+Kill: module {
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+stderr: ref Sys->FD;
+
+usage()
+{
+	sys->fprint(stderr, "usage: kill [-g] pid|module [...]\n");
+	raise "fail: usage";
+}
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil){
+		sys->fprint(stderr, "kill: cannot load %s: %r\n", Arg->PATH);
+		raise "fail:load";
+	}
+
+	msg := array of byte "kill";
+	arg->init(args);
+	while((o := arg->opt()) != 0)
+		case o {
+		'g' =>
+			msg = array of byte "killgrp";
+		* =>
+			usage();
+		}
+
+	argv := arg->argv();
+	arg = nil;
+	if(argv == nil)
+		usage();
+	n := 0;
+	for(v := argv; v != nil; v = tl v) {
+		s := hd v;
+		if (s == nil)
+			usage();
+		if(s[0] >= '0' && s[0] <= '9')
+			n += killpid(s, msg, 1);
+		else
+			n += killmod(s, msg);
+	}
+	if (n == 0 && argv != nil)
+		raise "fail:nothing killed";
+}
+
+killpid(pid: string, msg: array of byte, sbok: int): int
+{
+	fd := sys->open("/prog/"+pid+"/ctl", sys->OWRITE);
+	if(fd == nil) {
+		err := sys->sprint("%r");
+		elen := len err;
+		if(sbok || err != "thread exited" && elen >= 14 && err[elen-14:] != "does not exist")
+			sys->fprint(stderr, "kill: cannot open /prog/%s/ctl: %r\n", pid);
+		return 0;
+	}
+
+	n := sys->write(fd, msg, len msg);
+	if(n < 0) {
+		err := sys->sprint("%r");
+		elen := len err;
+		if(sbok || err != "thread exited")
+			sys->fprint(stderr, "kill: cannot kill %s: %r\n", pid);
+		return 0;
+	}
+	return 1;
+}
+
+killmod(mod: string, msg: array of byte): int
+{
+	fd := sys->open("/prog", sys->OREAD);
+	if(fd == nil) {
+		sys->fprint(stderr, "kill: open /prog: %r\n");
+		return 0;
+	}
+
+	pids: list of string;
+	for(;;) {
+		(n, d) := sys->dirread(fd);
+		if(n <= 0) {
+			if (n < 0)
+				sys->fprint(stderr, "kill: read /prog: %r\n");
+			break;
+		}
+
+		for(i := 0; i < n; i++)
+			if (killmatch(d[i].name, mod))
+				pids = d[i].name :: pids;		
+	}
+	if (pids == nil) {
+		sys->fprint(stderr, "kill: cannot find %s\n", mod);
+		return 0;
+	}
+	n := 0;
+	for (; pids != nil; pids = tl pids)
+		if (killpid(hd pids, msg, 0)) {
+			sys->print("%s ", hd pids);
+			n++;
+		}
+	if (n > 0)
+		sys->print("\n");
+	return n;
+}
+
+killmatch(dir, mod: string): int
+{
+	status := "/prog/"+dir+"/status";
+	fd := sys->open(status, sys->OREAD);
+	if(fd == nil)
+		return 0;
+	buf := array[512] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) {
+		err := sys->sprint("%r");
+		if(err != "thread exited")
+			sys->fprint(stderr, "kill: cannot read %s: %s\n", status, err);
+		return 0;
+	}
+
+	# module name is last field
+	(nil, fields) := sys->tokenize(string buf[0:n], " ");
+	for(s := ""; fields != nil; fields = tl fields)
+		s = hd fields;
+
+	# strip builtin module, e.g. Sh[$Sys]
+	for(i := 0; i < len s; i++) {
+		if(s[i] == '[') {
+			s = s[0:i];
+			break;
+		}
+	}
+
+	return s == mod;
+}
--- /dev/null
+++ b/appl/tiny/mkfile
@@ -1,0 +1,20 @@
+<../../mkconfig
+
+TARG=\
+	broke.dis\
+	kill.dis\
+	rm.dis\
+	sh.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	sys.m\
+	draw.m\
+	env.m\
+	filepat.m\
+	bufio.m\
+
+DISBIN=$ROOT/dis/tiny
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/tiny/rm.b
@@ -1,0 +1,23 @@
+implement Rm;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+
+Rm: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(nil: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr := sys->fildes(2);
+
+	argv = tl argv;
+	while(argv != nil) {
+		if(sys->remove(hd argv) < 0)
+			sys->fprint(stderr, "rm: %s: %r\n", hd argv);
+		argv = tl argv;
+	}
+}
--- /dev/null
+++ b/appl/tiny/sh.b
@@ -1,0 +1,628 @@
+implement Sh;
+
+include "sys.m";
+	sys: Sys;
+	FD: import Sys;
+
+include "draw.m";
+	Context: import Draw;
+
+include "filepat.m";
+	filepat: Filepat;
+	nofilepat := 0;			# true if load Filepat has failed.
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "env.m";
+	env: Env;
+
+stdin: ref FD;
+stderr: ref FD;
+waitfd: ref FD;
+
+Quoted: con '\uFFF0';
+stringQuoted: con "\uFFF0";
+
+Sh: module
+{
+	init: fn(ctxt: ref Context, argv: list of string);
+};
+
+Command: adt
+{
+	args: list of string;
+	inf, outf: string;
+	append: int;
+};
+
+Async, Seq: con iota;
+
+Pipeline: adt
+{
+	cmds: list of ref Command;
+	term: int;
+};
+
+usage()
+{
+	sys->fprint(stderr, "Usage: sh [-n] [-c cmd] [file]\n");
+}
+
+init(ctxt: ref Context, argv: list of string)
+{
+	n: int;
+	arg: list of string;
+	buf := array[1024] of byte;
+
+	sys = load Sys Sys->PATH;
+	env = load Env Env->PATH;
+
+	stderr = sys->fildes(2);
+
+	waitfd = sys->open("#p/"+string sys->pctl(0, nil)+"/wait", sys->OREAD);
+	if(waitfd == nil){
+		sys->fprint(stderr, "sh: open wait: %r\n");
+		return;
+	}
+
+	eflag := nflag := lflag := 0;
+	cmd: string;
+	if(argv != nil)
+		argv = tl argv;
+	for(; argv != nil && len hd argv && (hd argv)[0]=='-'; argv = tl argv)
+		case hd argv {
+		"-e" =>
+			eflag = 1;
+		"-n" =>
+			nflag = 1;
+		"-l" =>
+			lflag = 1;
+		"-c" =>
+			argv = tl argv;
+			if(len argv != 1){
+				usage();
+				return;
+			}
+			cmd = hd argv;
+		* =>
+			usage();
+			return;
+		}
+	
+	if (lflag)
+		startup(ctxt);
+
+	if(eflag == 0)
+		sys->pctl(sys->FORKENV, nil);
+	if(nflag == 0)
+		sys->pctl(sys->FORKNS, nil);
+	if(cmd != nil){
+		arg = tokenize(cmd+"\n");
+		if(arg != nil)
+			runit(ctxt, parseit(arg));
+		return;
+	}
+	if(argv != nil){
+		script(ctxt, hd argv);
+		return;
+	}
+
+	stdin = sys->fildes(0);
+
+	prompt := sysname() + "$ ";
+	for(;;){
+		sys->print("%s", prompt);
+		n = sys->read(stdin, buf, len buf);
+		if(n <= 0)
+			break;
+		arg = tokenize(string buf[0:n]);
+		if(arg != nil)
+			runit(ctxt, parseit(arg));
+	}
+}
+
+rev(arg: list of string): list of string
+{
+	ret: list of string;
+
+	while(arg != nil){
+		ret = hd arg :: ret;
+		arg = tl arg;
+	}
+	return ret;
+}
+
+waitfor(pid: int)
+{
+	if(pid <= 0)
+		return;
+	buf := array[sys->WAITLEN] of byte;
+	status := "";
+	for(;;){
+		n := sys->read(waitfd, buf, len buf);
+		if(n < 0){
+			sys->fprint(stderr, "sh: read wait: %r\n");
+			return;
+		}
+		status = string buf[0:n];
+		if(status[len status-1] != ':')
+			sys->fprint(stderr, "%s\n", status);
+		who := int status;
+		if(who != 0){
+			if(who == pid)
+				return;
+		}
+	}
+}
+
+mkprog(ctxt: ref Context, arg: list of string, infd, outfd: ref Sys->FD, waitpid: chan of int)
+{
+	fds := list of {0, 1, 2};
+	if(infd != nil)
+		fds = infd.fd :: fds;
+	if(outfd != nil)
+		fds = outfd.fd :: fds;
+	pid := sys->pctl(sys->NEWFD, fds);
+	console := sys->fildes(2);
+
+	if(infd != nil){
+		sys->dup(infd.fd, 0);
+		infd = nil;
+	}
+	if(outfd != nil){
+		sys->dup(outfd.fd, 1);
+		outfd = nil;
+	}
+
+	waitpid <-= pid;
+
+	if(pid < 0 || arg == nil)
+		return;
+
+	{
+		exec(ctxt, arg, console);
+	}exception{
+	"fail:*" =>
+		#sys->fprint(console, "%s:%s\n", hd arg, e.name[5:]);
+		exit;
+	"write on closed pipe" =>
+		#sys->fprint(console, "%s: %s\n", hd arg, e.name);
+		exit;
+	}
+}
+
+exec(ctxt: ref Context, args: list of string, console: ref Sys->FD)
+{
+	if (args == nil)
+		return;
+	cmd := hd args;
+	file := cmd;
+		
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Sh file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(err != "permission denied" && err != "access permission denied" && file[0]!='/' && file[0:2]!="./"){
+			c = load Sh "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sys->fprint(console, "%s: %s\n", cmd, err);
+			return;
+		}
+	}
+
+	c->init(ctxt, args);
+}
+
+script(ctxt: ref Context, src: string)
+{
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil){
+		sys->fprint(stderr, "sh: load bufio: %r\n");
+		return;
+	}
+
+	f := bufio->open(src, Bufio->OREAD);
+	if(f == nil){
+		sys->fprint(stderr, "sh: open %s: %r\n", src);
+		return;
+	}
+	for(;;){
+		s := f.gets('\n');
+		if(s == nil)
+			break;
+		arg := tokenize(s);
+		if(arg != nil)
+			runit(ctxt, parseit(arg));
+	}
+}
+
+sysname(): string
+{
+	fd := sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return "anon";
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return "anon";
+	return string buf[0:n];
+}
+
+# Lexer.
+
+tokenize(s: string): list of string
+{
+	tok: list of string;
+	token := "";
+	instring := 0;
+
+loop:
+	for(i:=0; i<len s; i++) {
+		if(instring) {
+			if(s[i] != '\'')
+				token = addchar(token, s[i]);
+			else if(i == len s-1 || s[i+1] != '\'') {
+				if(i == len s-1 || s[i+1] == ' ' || s[i+1] == '\t' || s[i+1] == '\n'){
+					tok = token :: tok;
+					token = "";
+				}
+				instring = 0;
+			} else {
+				token[len token] = '\'';
+				i++;
+			}
+			continue;
+		}
+		case s[i] {
+		' ' or '\t' or '\n' or '#' or
+		'\'' or '|' or '&' or ';' or
+		'>' or '<' or '\r' =>
+			if(token != "" && s[i]!='\''){
+				tok = token :: tok;
+				token = "";
+			}
+			case s[i] {
+			'#' =>
+				break loop;
+			'\'' =>
+				instring = 1;
+			'>' =>
+				ss := "";
+				ss[0] = s[i];
+				if(i<len s-1 && s[i+1]==s[i])
+					ss[1] = s[i++];
+				tok = ss :: tok;
+			'|' or '&' or ';' or '<' =>
+				ss := "";
+				ss[0] = s[i];
+				tok = ss :: tok;
+			}
+		* =>
+			token[len token] = s[i];
+		}
+	}
+	if(instring){
+		sys->fprint(stderr, "sh: unmatched quote\n");
+		return nil;
+	}
+	return rev(tok);
+}
+
+ismeta(char: int): int
+{
+	case char {
+	'*'  or '[' or '?' or
+	'#'  or '\'' or '|' or
+	'&' or ';' or '>' or
+	'<'  =>
+		return 1;
+	}
+	return 0;
+}
+
+addchar(token: string, char: int): string
+{
+	if(ismeta(char) && (len token==0 || token[0]!=Quoted))
+		token = stringQuoted + token;
+	token[len token] = char;
+	return token;
+}
+
+# Parser.
+
+getcommand(words: list of string): (ref Command, list of string)
+{
+	args: list of string;
+	word: string;
+	si, so: string;
+	append := 0;
+
+gather:
+	do {
+		word = hd words;
+
+		case word {
+		">" or ">>" =>
+			if(so != nil)
+				return (nil, nil);
+
+			words = tl words;
+
+			if(words == nil)
+				return (nil, nil);
+
+			so = hd words;
+			if(len so>0 && so[0]==Quoted)
+				so = so[1:];
+			if(word == ">>")
+				append = 1;
+		"<" =>
+			if(si != nil)
+				return (nil, nil);
+
+			words = tl words;
+
+			if(words == nil)
+				return (nil, nil);
+
+			si = hd words;
+			if(len si>0 && si[0]==Quoted)
+				si = si[1:];
+		"|" or ";" or "&" =>
+			break gather;
+		* =>
+			files := doexpand(word);
+			while(files != nil){
+				args = hd files :: args;
+				files = tl files;
+			}
+		}
+
+		words = tl words;
+	} while (words != nil);
+
+	return (ref Command(rev(args), si, so, append), words);
+}
+
+doexpand(file: string): list of string
+{
+	if(file == nil)
+		return file :: nil;
+	if(len file>0 && file[0]==Quoted)
+		return file[1:] :: nil;
+	if (nofilepat)
+		return file :: nil;
+	for(i:=0; i<len file; i++)
+		if(file[i]=='*' || file[i]=='[' || file[i]=='?'){
+			if(filepat == nil) {
+				if ((filepat = load Filepat Filepat->PATH) == nil) {
+					sys->fprint(stderr, "sh: warning: cannot load %s: %r\n",
+							Filepat->PATH);
+					nofilepat = 1;
+					return file :: nil;
+				}
+			}
+			files := filepat->expand(file);
+			if(files != nil)
+				return files;
+			break;
+		}
+	return file :: nil;
+}
+
+revc(arg: list of ref Command): list of ref Command
+{
+	ret: list of ref Command;
+	while(arg != nil) {
+		ret = hd arg :: ret;
+		arg = tl arg;
+	}
+	return ret;
+}
+
+getpipe(words: list of string): (ref Pipeline, list of string)
+{
+	cmds: list of ref Command;
+	cur: ref Command;
+	word: string;
+
+	term := Seq;
+gather:
+	while(words != nil) {
+		word = hd words;
+
+		if(word == "|")
+			return (nil, nil);
+
+		(cur, words) = getcommand(words);
+
+		if(cur == nil)
+			return (nil, nil);
+
+		cmds = cur :: cmds;
+
+		if(words == nil)
+			break gather;
+
+		word = hd words;
+		words = tl words;
+
+		case word {
+		";" =>
+			break gather;
+		"&" =>
+			term = Async;
+			break gather;
+		"|" =>
+			continue gather;
+		}
+		return (nil, nil);
+	}
+
+	if(word == "|")
+		return (nil, nil);
+
+	return (ref Pipeline(revc(cmds), term), words);
+}
+
+revp(arg: list of ref Pipeline): list of ref Pipeline
+{
+	ret: list of ref Pipeline;
+
+	while(arg != nil) {
+		ret = hd arg :: ret;
+		arg = tl arg;
+	}
+	return ret;
+}
+
+parseit(words: list of string): list of ref Pipeline
+{
+	ret: list of ref Pipeline;
+	cur: ref Pipeline;
+
+	while(words != nil) {
+		(cur, words) = getpipe(words);
+		if(cur == nil){
+			sys->fprint(stderr, "sh: syntax error\n");
+			return nil;
+		}
+		ret = cur :: ret;
+	}
+	return revp(ret);
+}
+
+# Runner.
+
+runpipeline(ctx: ref Context, pipeline: ref Pipeline)
+{
+	if(pipeline.term == Async)
+		sys->pctl(sys->NEWPGRP, nil);
+	pid := startpipeline(ctx, pipeline);
+	if(pid < 0)
+		return;
+	if(pipeline.term == Seq)
+		waitfor(pid);
+}
+
+startpipeline(ctx: ref Context, pipeline: ref Pipeline): int
+{
+	pid := 0;
+	cmds := pipeline.cmds;
+	first := 1;
+	inpipe, outpipe: ref Sys->FD;
+	while(cmds != nil) {
+		last := tl cmds == nil;
+		cmd := hd cmds;
+
+		infd: ref Sys->FD;
+		if(!first)
+			infd = inpipe;
+		else if(cmd.inf != nil){
+			infd = sys->open(cmd.inf, Sys->OREAD);
+			if(infd == nil){
+				sys->fprint(stderr, "sh: can't open %s: %r\n", cmd.inf);
+				return -1;
+			}
+		}
+
+		outfd: ref Sys->FD;
+		if(!last){
+			fds := array[2] of ref Sys->FD;
+			if(sys->pipe(fds) < 0){
+				sys->fprint(stderr, "sh: can't make pipe: %r\n");
+				return -1;
+			}
+			outpipe = fds[0];
+			outfd = fds[1];
+			fds = nil;
+		}else if(cmd.outf != nil){
+			if(cmd.append){
+				outfd = sys->open(cmd.outf, Sys->OWRITE);
+				if(outfd != nil)
+					sys->seek(outfd, big 0, Sys->SEEKEND);
+			}
+			if(outfd == nil)
+				outfd = sys->create(cmd.outf, Sys->OWRITE, 8r666);
+			if(outfd == nil){
+				sys->fprint(stderr, "sh: can't open %s: %r\n", cmd.outf);
+				return -1;
+			}
+		}
+
+		rpid := chan of int;
+		spawn mkprog(ctx, cmd.args, infd, outfd, rpid);
+		pid = <-rpid;
+		infd = nil;
+		outfd = nil;
+
+		inpipe = outpipe;
+		outpipe = nil;
+
+		first = 0;
+		cmds = tl cmds;
+	}
+	return pid;
+}
+
+runit(ctx: ref Context, pipes: list of ref Pipeline)
+{
+	while(pipes != nil) {
+		pipeline := hd pipes;
+		pipes = tl pipes;
+		if(pipeline.term == Seq)
+			runpipeline(ctx, pipeline);
+		else
+			spawn runpipeline(ctx, pipeline);
+	}
+}
+
+strchr(s: string, c: int): int
+{
+	ln := len s;
+	for (i := 0; i < ln; i++)
+		if (s[i] == c)
+			return i;
+	return -1;
+}
+
+# PROFILE: con "/lib/profile";
+PROFILE: con "/lib/infernoinit";
+
+startup(ctxt: ref Context)
+{
+	if (env == nil)
+		return;
+	# if (env->getenv("home") != nil)
+	#	return;
+	home := gethome();
+	env->setenv("home", home);
+	escript(ctxt, PROFILE);
+	escript(ctxt, home + PROFILE);
+}
+
+escript(ctxt: ref Context, file: string)
+{
+	fd := sys->open(file, Sys->OREAD);
+	if (fd != nil)
+		script(ctxt, file);
+}
+
+gethome(): string
+{
+	fd := sys->open("/dev/user", sys->OREAD);
+  	if(fd == nil)
+    		return "/";
+  	buf := array[128] of byte;
+  	n := sys->read(fd, buf, len buf);
+  	if(n < 0)
+    		return "/";
+  	return "/usr/" + string buf[0:n];
+}
--- /dev/null
+++ b/appl/wm/about.b
@@ -1,0 +1,72 @@
+implement WmAbout;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Display, Image: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+WmAbout: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+tkcfg(version: string): array of string
+{
+	return  array[] of {
+	"frame .f -bg black -borderwidth 2 -relief ridge",
+	"label .b -bg black -bitmap @/icons/inferno.bit",
+	"label .l1 -bg black -fg #ff5500  -text {Inferno "+ version + "}",
+	"pack .b .l1 -in .f",
+	"pack .f -ipadx 4 -ipady 2",
+	"pack propagate . 0",
+	"update",
+	};
+}
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys  = load Sys  Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "about: no window context\n");
+		raise "fail:bad context";
+	}
+
+	draw = load Draw Draw->PATH;
+	tk   = load Tk   Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+
+	tkclient->init();
+	(t, menubut) := tkclient->toplevel(ctxt, "", "About Inferno", 0);
+
+	tkcmds := tkcfg(rf("/dev/sysctl"));
+	for (i := 0; i < len tkcmds; i++)
+		tk->cmd(t,tkcmds[i]);
+
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "ptr"::nil);
+	stop := chan of int;
+	spawn tkclient->handler(t, stop);
+	while((menu := <-menubut) != "exit")
+		tkclient->wmctl(t, menu);
+	stop <-= 1;
+}
+
+rf(name: string): string
+{
+	fd := sys->open(name, Sys->OREAD);
+	if(fd == nil)
+		return nil;
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		n = 0;
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/wm/avi.b
@@ -1,0 +1,384 @@
+implement WmAVI;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Rect, Display, Image: import draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+	ctxt: ref Draw->Context;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "riff.m";
+	avi: Riff;
+	AVIhdr, AVIstream, RD: import avi;
+	video: ref AVIstream;
+
+WmAVI: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Stopped, Playing, Paused: con iota;
+state	:= Stopped;
+
+
+cmap: array of byte;
+codedbuf: array of byte;
+pixelbuf: array of byte;
+pixelrec: Draw->Rect;
+
+task_cfg := array[] of {
+	"canvas .c",
+	"frame .b",
+	"button .b.File -text File -command {send cmd file}",
+	"button .b.Stop -text Stop -command {send cmd stop}",
+	"button .b.Pause -text Pause -command {send cmd pause}",
+	"button .b.Play -text Play -command {send cmd play}",
+	"frame .f",
+	"label .f.file -text {File:}",
+	"label .f.name",
+	"pack .f.file .f.name -side left",
+	"pack .b.File .b.Stop .b.Pause .b.Play -side left",
+	"pack .f -fill x",
+	"pack .b -anchor w",
+	"pack .c -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+init(xctxt: ref Draw->Context, nil: list of string)
+{
+	sys  = load Sys  Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk   = load Tk   Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+
+	ctxt = xctxt;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	tkclient->init();
+	dialog->init();
+	selectfile->init();
+
+	(t, wmctl) := tkclient->toplevel(ctxt, "", "AVI Player", 0);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	for (c:=0; c<len task_cfg; c++)
+		tk->cmd(t, task_cfg[c]);
+
+	tk->cmd(t, "bind . <Configure> {send cmd resize}");
+	tk->cmd(t, "update");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	avi = load Riff Riff->PATH;
+	if(avi == nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Loading Interfaces",
+			"Failed to load the RIFF/AVI\ninterface:"+sys->sprint("%r"),
+			0, "Exit"::nil);
+		return;
+	}
+	avi->init();
+
+	fname := "";
+	state = Stopped;
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-wmctl =>
+		if(s == "exit") {
+			state = Stopped;
+			return;
+		}
+		tkclient->wmctl(t, s);
+	press := <-cmd =>
+		case press {
+		"file" =>
+			state = Stopped;
+			patterns := list of {
+				"*.avi (Microsoft movie files)",
+				"* (All Files)"
+			};
+			fname = selectfile->filename(ctxt, t.image, "Locate AVI files",
+				patterns, nil);
+			if(fname != nil) {
+				tk->cmd(t, ".f.name configure -text {"+fname+"}");
+				tk->cmd(t, "update");
+			}
+		"play" =>
+			if (state != Stopped) {
+				state = Playing;
+				continue;
+			}
+			if(fname != nil) {
+				state = Playing;
+				spawn play(t, fname);
+			}
+		"pause" =>
+			if(state == Playing)
+				state = Paused;
+		"stop" =>
+			state = Stopped;
+		}
+	}
+}
+
+play(t: ref Toplevel, file: string)
+{
+	sp := list of { "Stop Play" };
+
+	(r, err) := avi->open(file);
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Open AVI file", err, 0, sp);
+		return;
+	}
+
+	err = avi->r.check4("AVI ");
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Read AVI format", err, 0, sp);
+		return;
+	}
+
+	(code, l) := avi->r.gethdr();
+	if(code != "LIST") {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Parse AVI headers",
+				"no list under AVI section header", 0, sp);
+		return;
+	}
+
+	err = avi->r.check4("hdrl");
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Read AVI header", err, 0, sp);
+		return;
+	}
+
+	avihdr: ref AVIhdr;
+	(avihdr, err) = avi->r.avihdr();
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Read AVI header", err, 0, sp);
+		return;
+	}
+
+	#
+	# read the stream info & format structures
+	#
+	stream := array[avihdr.streams] of ref AVIstream;
+	for(i := 0; i < avihdr.streams; i++) {
+		(stream[i], err) = avi->r.streaminfo();
+		if(err != nil) {
+			dialog->prompt(ctxt, t.image, "error -fg red", "Parse AVI headers",
+				"Failed to parse stream headers\n"+err, 0, sp);
+			return;
+		}
+		if(stream[i].stype == "vids") {
+			video = stream[i];
+			err = video.fmt2binfo();
+			if(err != nil) {
+				dialog->prompt(ctxt, t.image, "error -fg red",
+					"Parse AVI Video format",
+					"Invalid stream headers\n"+err, 0, sp);
+				return;
+			}
+		}
+	}
+
+	img: ref Draw->Image;
+	if(video != nil) {
+		case video.binfo.compression {
+		* =>
+			dialog->prompt(ctxt, t.image, "error -fg red",
+					"Parse AVI Compression method",
+					"unknown compression/encoding method", 0, sp);
+			return;
+		avi->BI_RLE8 =>
+			cmap = array[len video.binfo.cmap] of byte;
+			for(i = 0; i < len video.binfo.cmap; i++) {
+				e := video.binfo.cmap[i];
+				cmap[i] = byte ctxt.display.rgb2cmap(e.r, e.g, e.b);
+			}
+			break;
+		}
+		chans: draw->Chans;
+		case video.binfo.bitcount {
+		* =>
+			dialog->prompt(ctxt, t.image, "error -fg red",
+					"Check AVI Video format",
+					string video.binfo.bitcount+
+					" bits per pixel not supported", 0, sp);
+			return;
+		8 =>
+			chans = Draw->CMAP8;
+			mem := video.binfo.width*video.binfo.height;
+			pixelbuf = array[mem] of byte;
+		};
+		pixelrec.min = (0, 0);
+		pixelrec.max = (video.binfo.width, video.binfo.height);
+		img = ctxt.display.newimage(pixelrec, chans, 0, Draw->White);
+		if (img == nil) {
+				sys->fprint(sys->fildes(2), "coffee: failed to allocate image\n");	
+		exit;
+		}
+	}
+
+	#
+	# Parse out the junk headers we don't understand
+	#
+	parse: for(;;) {
+		(code, l) = avi->r.gethdr();
+		if(l < 0)
+			break;
+
+		case code {
+		* =>
+#			sys->print("%s %d\n", code, l);
+			avi->r.skip(l);
+		"LIST" =>
+			err = avi->r.check4("movi");
+			if(err != nil) {
+				dialog->prompt(ctxt, t.image, "error -fg red",
+					"Strip AVI headers",
+					"no movi chunk", 0, sp);
+				return;
+			}
+			break parse;
+		}
+	}
+
+	canvr := canvsize(t);
+	p := (Draw->Point)(0, 0);
+	dx := canvr.dx();
+	if(dx > video.binfo.width)
+		p.x = (dx - video.binfo.width)/2;
+
+	dy := canvr.dy();
+	if(dy > video.binfo.height)
+		p.y = (dy - video.binfo.height)/2;
+
+	canvr = canvr.addpt(p);
+
+	chunk: for(;;) {
+		while(state == Paused)
+			sys->sleep(0);
+		if(state == Stopped)
+			break chunk;
+		(code, l) = avi->r.gethdr();
+		if(l <= 0)
+			break;
+		if(l & 1)
+			l++;
+		case code {
+		* =>
+			avi->r.skip(l);
+		"00db" =>			# Stream 0 Video DIB
+			dib(r, img, l);
+		"00dc" =>			# Stream 0 Video DIB compressed
+			dibc(r, img, l);
+			t.image.draw(canvr, img, nil, img.r.min);
+		"idx1" =>
+			break chunk;
+		}
+	}
+	state = Stopped;
+}
+
+dib(r: ref RD, i: ref Draw->Image, l: int): int
+{
+	if(len codedbuf < l)
+		codedbuf = array[l] of byte;
+
+	if(r.readn(codedbuf, l) != l)
+		return -1;
+
+	case video.binfo.bitcount {
+	8 =>
+		for(k := 0; k < l; k++)
+			codedbuf[k] = cmap[int codedbuf[k]];
+	
+		i.writepixels(pixelrec, codedbuf);
+	}
+	return 0;
+}
+
+dibc(r: ref RD, i: ref Draw->Image, l: int): int
+{
+	if(len codedbuf < l)
+		codedbuf = array[l] of byte;
+
+	if(r.readn(codedbuf, l) != l)
+		return -1;
+
+	case video.binfo.compression {
+	avi->BI_RLE8 =>
+		p := 0;
+		posn := 0;
+		x := 0;
+		y := video.binfo.height-1;
+		w := video.binfo.width;
+		decomp: while(p < l) {
+			n := int codedbuf[p++];
+			if(n == 0) {
+				esc := int codedbuf[p++];
+				case esc {
+				0 =>			# end of line
+					x = 0;
+					y--;
+				1 =>			# end of image
+					break decomp;
+				2 =>			# Delta dx,dy
+					x += int codedbuf[p++];
+					y -= int codedbuf[p++];
+				* =>
+					posn = x+y*w;
+					for(k := 0; k < esc; k++)
+						pixelbuf[posn++] = cmap[int codedbuf[p++]];
+					x += esc;
+					if(p & 1)
+						p++;
+				};
+			}
+			else {
+				posn = x+y*w;
+				v := cmap[int codedbuf[p++]];
+				for(k := 0; k < n; k++)
+					pixelbuf[posn++] = v;
+				x += n;
+			}
+		}
+		i.writepixels(pixelrec, pixelbuf);
+	}
+	return 0;
+}
+
+canvsize(t: ref Toplevel): Rect
+{
+	r: Rect;
+
+	r.min.x = int tk->cmd(t, ".c cget -actx");
+	r.min.y = int tk->cmd(t, ".c cget -acty");
+	r.max.x = r.min.x + int tk->cmd(t, ".c cget -width");
+	r.max.y = r.min.y + int tk->cmd(t, ".c cget -height");
+
+	return r;
+}
--- /dev/null
+++ b/appl/wm/bounce.b
@@ -1,0 +1,356 @@
+implement Bounce;
+
+# bouncing balls demo.  it uses tk and multiple processes to animate a
+# number of balls bouncing around the screen.  each ball has its own
+# process; CPU time is doled out fairly to each process by using
+# a central monitor loop.
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "math.m";
+	math: Math;
+include "rand.m";
+
+Bounce: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+BALLSIZE: con 4;
+ZERO: con 1e-6;
+π: con Math->Pi;
+
+Line: adt {
+	p1, p2: Point;
+};
+
+Realpoint: adt {
+	x, y: real;
+};
+
+gamecmds := array[] of {
+"canvas .c",
+"bind .c <ButtonRelease-1> {send cmd 0 %x %y}",
+"bind .c <ButtonRelease-2> {send cmd 0 %x %y}",
+"bind .c <Button-1> {send cmd 1 %x %y}",
+"bind .c <Button-2> {send cmd 2 %x %y}",
+"frame .f",
+"button .f.left -bitmap small_color_left.bit -bd 0 -command {send cmd k -1}",
+"button .f.right -bitmap small_color_right.bit -bd 0 -command {send cmd k 1}",
+"label .f.l -text {8 balls}",
+"pack .f.left .f.right -side left",
+"pack .f.l -side left",
+"pack .f -fill x",
+"pack .c -fill both -expand 1",
+};
+
+randch: chan of int;
+lines: list of (int, Line);
+lineid := 0;
+lineversion := 0;
+
+addline(win: ref Tk->Toplevel, v: Line)
+{
+	lines = (++lineid, v) :: lines;
+	cmd(win, ".c create line " + pt2s(v.p1) + " " + pt2s(v.p2) + " -width 3 -fill black" +
+			" -tags l" + string lineid);
+	lineversion++;
+}
+
+nomod(s: string)
+{
+	sys->fprint(sys->fildes(2), "bounce: cannot load %s: %r\n", s);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	math = load Math Math->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		nomod(Tkclient->PATH);
+	tkclient->init();
+	nballs := 8;
+	if (argv != nil && tl argv != nil)
+		nballs = int hd tl argv;
+	if (nballs < 0) {
+		sys->fprint(sys->fildes(2), "usage: bounce [nballs]\n");
+		raise "fail:usage";
+	}
+	sys->pctl(Sys->NEWPGRP, nil);
+	if(ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+	(win, wmctl) := tkclient->toplevel(ctxt, nil, "Bounce", 0);
+	cmdch := chan of string;
+	tk->namechan(win, cmdch, "cmd");
+	for (i := 0; i < len gamecmds; i++)
+		cmd(win, gamecmds[i]);
+	cmd(win, ".c configure -width 400 -height 400");
+	cmd(win, "pack propagate . 0");
+	cmd(win, ".f.l configure -text '" + string nballs + " balls");
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+	mch := chan of (int, Point);
+	randch = chan of int;
+	spawn randgenproc(randch);
+	csz := Point(int cmd(win, ".c cget -actwidth"), int cmd(win, ".c cget -actheight"));
+
+	# add edges of window
+	addline(win, ((-1, -1), (csz.x, -1)));
+	addline(win, ((csz.x, -1), csz));
+	addline(win, (csz, (-1, csz.y)));
+	addline(win, ((-1, csz.y), (-1, -1)));
+
+	spawn makelinesproc(win, mch);
+	mkball := chan of (int, Realpoint, Realpoint);
+	spawn monitor(win, mkball);
+	for (i = 0; i < nballs; i++)
+		mkball <-= (1, randpoint(csz), makeunit(randpoint(csz)));
+	for (;;) alt {
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-wmctl =>
+		tkclient->wmctl(win, s);
+	c := <-cmdch =>
+		(nil, toks) := sys->tokenize(c, " ");
+		if (hd toks != "k") {
+			mch <-= (int hd toks, Point(int hd tl toks, int hd tl tl toks));
+			continue;
+		}
+		n := nballs + int hd tl toks;
+		if (n < 0)
+			n = 0;
+		dn := 1;
+		if (n < nballs)
+			dn = -1;
+		for (; nballs != n; nballs += dn)
+			mkball <-= (dn, randpoint(csz), makeunit(randpoint(csz)));
+		cmd(win, ".f.l configure -text '" + string nballs + " balls");
+		cmd(win, "update");
+	}
+}
+
+randpoint(size: Point): Realpoint
+{
+	return (randreal(size.x), randreal(size.y));
+}
+
+# return randomish real number between 1 and x-1
+randreal(x: int): real
+{
+	return real (<-randch % ((x - 1) * 100)) / 100.0 + 1.0;
+}
+
+# make sure cpu time is handed to all ball processes fairly
+# by passing a "token" around to each process in turn.
+# each process does its work when it *hasn't* got its
+# token but it can't go through two iterations without
+# waiting its turn.
+#
+# new processes can be created and destroyed by
+# sending on mkball. processes are arranged in a stack-like
+# order: new processes are added to the top of the stack, and
+# processes are destroyed from the top of the stack downwards.
+monitor(win: ref Tk->Toplevel, mkball: chan of (int, Realpoint, Realpoint))
+{
+	procl := proc := chan of int :: nil;
+	spawn nullproc(hd proc);	# always there to avoid deadlock when no balls.
+	hd proc <-= 1;			# hand token to dummy proc
+	for (;;) {
+		procc := hd proc;
+		alt {
+		(n, p, v) := <-mkball =>
+			if (n > 0) {					# start new ball proc going.
+				procl = chan of int :: procl;
+				spawn animproc(hd procl, win, p, v);
+			} else if (tl procl != nil) {		# stop a ball proc.
+				<-hd proc;			# get token.
+				hd procl <-= 0;			# stop proc.
+				proc = procl = tl procl;	# remove proc.
+				hd proc <-= 1;			# hand out token.
+			}
+		<-procc =>					# got token.
+			if ((proc = tl proc) == nil)
+				proc = procl;
+			hd proc <-= 1;				# hand token to next process.
+		}
+	}
+}
+
+nullproc(c: chan of int)
+{
+	for (;;)
+		c <-= <-c;
+}
+
+# animate one ball. initial position and unit-velocity are
+# given by p and v.
+animproc(c: chan of int, win: ref Tk->Toplevel, p, v: Realpoint)
+{
+	speed := 0.1 + real (<-randch % 40) / 100.0;
+	ballid := cmd(win, sys->sprint(".c create oval 0 0 1 1 -fill #%.6x", <-randch & 16rffffff));
+	hitlineid := -1;
+	smallcount := 0;
+	version := lineversion;
+loop:	for (;;) {
+		hitline: Line;
+		hitp: Realpoint;
+
+		dist := 1000000.0;
+		oldid := hitlineid;
+		for (l := lines; l != nil; l = tl l) {
+			(id, line) := hd l;
+			(ok, hp, hdist) := intersect(p, v, line);
+			if (ok && hdist < dist && id != oldid && (smallcount < 10 || hdist > 1.5)) {
+				(hitp, hitline, hitlineid, dist) = (hp, line, id, hdist);
+			}
+		}
+		if (dist > 10000.0) {
+			sys->print("no intersection!\n");
+#			sys->print("p: [%f, %f], v: [%f, %f]\n", p.x, p.y, v.x, v.y);
+#			for (l := lines; l != nil; l = tl l) {
+#				(id, line) := hd l;
+#				(ok, hp, hdist) := intersect(p, v, line);
+#				sys->print("line: [%d %d]->[%d %d] -> %d, [%f, %f], %f\n", line.p1.x, line.p1.y, line.p2.x, line.p2.y,
+#						ok, hp.x, hp.y, hdist);
+#			}
+			cmd(win, ".c delete " + ballid + ";update");
+			while (c <-= <-c)
+				;
+			exit;
+		}
+		if (dist < 0.0001)
+			smallcount++;
+		else
+			smallcount = 0;
+		bouncev := boing(v, hitline);
+		t0 := sys->millisec();
+		dt := int (dist / speed);
+		t := 0;
+		do {
+			s := real t * speed;
+			currp := Realpoint(p.x + s * v.x,  p.y + s * v.y);
+			bp := Point(int currp.x, int currp.y);
+			cmd(win, ".c coords " + ballid + " " +
+				string (bp.x-BALLSIZE)+" "+string (bp.y-BALLSIZE)+" "+
+				string (bp.x+BALLSIZE)+" "+string (bp.y+BALLSIZE));
+			cmd(win, "update");
+			if (lineversion > version) {
+				(p, hitlineid, version) = (currp, oldid, lineversion);
+				continue loop;
+			}
+			# pass the token back to the monitor.
+			if (<-c == 0) {
+				cmd(win, ".c delete " + ballid + ";update");
+				exit;
+			}
+			c <-= 1;
+			t = sys->millisec() - t0;
+		} while (t < dt);
+		p = hitp;
+		v = bouncev;
+	}
+}
+
+# thread-safe access to the Rand module
+randgenproc(ch: chan of int)
+{
+	rand := load Rand Rand->PATH;
+	for (;;)
+		ch <-= rand->rand(16r7fffffff);
+}
+
+makelinesproc(win: ref Tk->Toplevel, mch: chan of (int, Point))
+{
+	for (;;) {
+		(down, p1) := <-mch;
+		addline(win, (p1, p1));
+		(id, nil) := hd lines;
+		p2 := p1;
+		do {
+			(down, p2) = <-mch;
+			cmd(win, ".c coords l" + string id + " " + pt2s(p1) + " " + pt2s(p2));
+			cmd(win, "update");
+			lines = (id, (p1, p2)) :: tl lines;
+			lineversion++;
+			if (down > 1) {
+				dp := p2.sub(p1);
+				if (dp.x*dp.x + dp.y*dp.y > 5) {
+					p1 = p2;
+					addline(win, (p2, p2));
+					(id, nil) = hd lines;
+				}
+			}
+		} while (down);
+	}
+}
+
+# make a vector of unit-length, parallel to v.
+makeunit(v: Realpoint): Realpoint
+{
+	mag := math->sqrt(v.x * v.x + v.y * v.y);
+	return (v.x / mag, v.y / mag);
+}
+
+# bounce ball travelling in direction av off line b.
+# return the new unit vector.
+boing(av: Realpoint, b: Line): Realpoint
+{
+	f := b.p2.sub(b.p1);
+	d := math->atan2(real f.y, real f.x) * 2.0 - math->atan2(av.y, av.x);
+	return (math->cos(d), math->sin(d));
+}
+
+# compute the intersection of lines a and b.
+# b is assumed to be fixed, and a is indefinitely long
+# but doesn't extend backwards from its starting point.
+# a is defined by the starting point p and the unit vector v.
+intersect(p, v: Realpoint, b: Line): (int, Realpoint, real)
+{
+	w := Realpoint(real (b.p2.x - b.p1.x), real (b.p2.y - b.p1.y));
+	det := w.x * v.y - v.x * w.y;
+	if (det > -ZERO && det < ZERO)
+		return (0, (0.0, 0.0), 0.0);
+
+	y21 := real b.p1.y - p.y;
+	x21 := real b.p1.x - p.x;
+	s := (w.x * y21 - w.y * x21) / det;
+	if (s < 0.0)
+		return (0, (0.0, 0.0), 0.0);
+
+	hp := Realpoint(p.x+v.x*s, p.y+v.y*s);
+	if (b.p1.x > b.p2.x)
+		(b.p1.x, b.p2.x) = (b.p2.x, b.p1.x);
+	if (b.p1.y > b.p2.y)
+		(b.p1.y, b.p2.y) = (b.p2.y, b.p1.y);
+
+	return (int hp.x >= b.p1.x && int hp.x <= b.p2.x
+			&& int hp.y >= b.p1.y && int hp.y <= int b.p2.y, hp, s);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->print("tk error %s on '%s'\n", e, s);
+	return e;
+}
+
+pt2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
--- /dev/null
+++ b/appl/wm/brutus.b
@@ -1,0 +1,2031 @@
+implement Brutus;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context: import draw;
+	ctxt: ref Context;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include	"bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include	"workdir.m";
+
+include	"plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg: import plumbmsg;
+
+include	"brutus.m";
+include	"brutusext.m";
+
+EXTDIR:	con "/dis/wm/brutus";
+NEXTRA:	con NTAG-NFONTTAG;
+DEFFONT:	con "/fonts/lucidasans/unicode.8.font";
+DEFFONTNAME:	con "Roman";
+DEFSIZE:	con 10;
+DEFTAG:	con "Roman.10";
+SETFONT:	con " -font "+DEFFONT+" ";
+FOCUS:	con "focus .ft.t";
+NOSEL:	con ".ft.t tag remove sel sel.first sel.last";
+UPDATE:	con "update";
+
+#
+# Foreign keyboards and languages
+#
+Remaptab: adt
+{
+	in, out:	int;
+};
+include	"hebrew.m";
+
+BS:		con 8;		# ^h backspace character
+BSW:		con 23;		# ^w bacspace word
+BSL:		con 21;		# ^u backspace line
+ESC:		con 27;		# ^[ cut selection
+
+Name:	con "Brutus";
+
+# build menu
+menu_cfg := array[] of {
+	# menu
+	"menu .m",
+	".m add command -text Cut -command {send edit cut}",
+	".m add command -text Paste -command {send edit paste}",
+	".m add command -text Snarf -command {send edit snarf}",
+	".m add command -text Look -command {send edit look}",
+};
+
+brutus_cfg := array[] of {
+	# buttons
+	"button .b.Tag -text Tag -command {send cmd tag} -state disabled",
+	"menubutton .b.Font -text Roman -menu .b.Font.menu -underline -1 -state disabled",
+	"menu .b.Font.menu",
+	".b.Font.menu add command -label Roman -command {send cmd font Roman}",
+	".b.Font.menu add command -label Italic -command {send cmd font Italic}",
+	".b.Font.menu add command -label Bold -command {send cmd font Bold}",
+	".b.Font.menu add command -label Type -command {send cmd font Type}",
+	"checkbutton .b.Applyfont -variable Applyfont -command {send cmd applyfont}} -state disabled",
+	"button .b.Applyfontnow -text Font -command {send cmd applyfontnow} -state disabled",
+	"button .b.Applysizenow -text Size -command {send cmd applysizenow} -state disabled",
+	"button .b.Applyfontsizenow -text F&S -command {send cmd applyfontsizenow} -state disabled",
+	"menubutton .b.Size -text 10pt -menu .b.Size.menu -underline -1 -state disabled",
+	"menu .b.Size.menu",
+	".b.Size.menu add command -label 6pt -command {send cmd size 6}",
+	".b.Size.menu add command -label 8pt -command {send cmd size 8}",
+	".b.Size.menu add command -label 10pt -command {send cmd size 10}",
+	".b.Size.menu add command -label 12pt -command {send cmd size 12}",
+	".b.Size.menu add command -label 16pt -command {send cmd size 16}",
+	"button .b.Put -text Put -command {send cmd put} -state disabled",
+
+	# text
+	"frame .ft",
+	"scrollbar .ft.scroll -command {.ft.t yview}",
+	"text .ft.t -height 7c -tabs {1c} -wrap word -yscrollcommand {.ft.scroll set}",
+	FOCUS,
+
+	# pack
+	"pack .b.File .b.Ext .b.Tag .b.Applyfontnow .b.Applysizenow .b.Applyfontsizenow .b.Applyfont .b.Font .b.Size .b.Put -side left",
+	"pack .b -anchor w",
+	"pack .ft.scroll -side left -fill y",
+	"pack .ft.t -fill both -expand 1",
+	"pack .ft -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+control_cfg := array[] of {
+	# text
+	"frame .ft",
+	"scrollbar .ft.scroll -command {.ft.t yview}",
+	"text .ft.t -height 4c -wrap word -yscrollcommand {.ft.scroll set}",
+	"pack .b.File",
+	"pack .b -anchor w",
+	"pack .ft.scroll -side left -fill y",
+	"pack .ft.t -fill both -expand 1",
+	"pack .ft -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+# bindings to build nice controls in text widget
+input_cfg := array[] of {
+	# input
+	"bind .ft.t <Key> {send keys {%A}}",
+	"bind .ft.t <Control-h> {send keys {%A}}",
+	"bind .ft.t <Control-w> {send keys {%A}}",
+	"bind .ft.t <Control-u> {send keys {%A}}",
+	"bind .ft.t <Button-1> +{grab set .ft.t; send but1 pressed}",
+	"bind .ft.t <Double-Button-1> +{grab set .ft.t; send but1 pressed}",
+	"bind .ft.t <ButtonRelease-1> +{grab release .ft.t; send but1 released}",
+	"bind .ft.t <Button-2> {send but2 %X %Y}",
+	"bind .ft.t <Motion-Button-2-Button-1> {}",
+	"bind .ft.t <Motion-Button-2> {}",
+	"bind .ft.t <ButtonPress-3> {send but3 pressed}",
+	"bind .ft.t <ButtonRelease-3> {send but3 released %x %y}",
+	"bind .ft.t <Motion-Button-3> {}",
+	"bind .ft.t <Motion-Button-3-Button-1> {}",
+	"bind .ft.t <Double-Button-3> {}",
+	"bind .ft.t <Double-ButtonRelease-3> {}",
+	"bind .ft.t <FocusIn> +{send cmd focus}",
+	UPDATE
+};
+
+fontbuts := array[] of {
+	".b.Ext",
+	".b.Tag",
+	".b.Applyfontnow",
+	".b.Applysizenow",
+	".b.Applyfontsizenow",
+	".b.Applyfont",
+	".b.Font",
+	".b.Size",
+};
+
+fontname = array[NFONT] of {
+	"Roman",
+	"Italic",
+	"Bold",
+	"Type",
+};
+
+sizename = array[NSIZE] of {
+	"6",
+	"8",
+	"10",
+	"12",
+	"16",
+};
+
+tagname = array[NTAG] of {
+	# first NFONT*NSIZE are font/size names
+	"Roman.6",
+	"Roman.8",
+	"Roman.10",
+	"Roman.12",
+	"Roman.16",
+	"Italic.6",
+	"Italic.8",
+	"Italic.10",
+	"Italic.12",
+	"Italic.16",
+	"Bold.6",
+	"Bold.8",
+	"Bold.10",
+	"Bold.12",
+	"Bold.16",
+	"Type.6",
+	"Type.8",
+	"Type.10",
+	"Type.12",
+	"Type.16",
+	"Example",
+	"Caption",
+	"List",
+	"List-elem",
+	"Label",
+	"Label-ref",
+	"Exercise",
+	"Heading",
+	"No-fill",
+	"Author",
+	"Title",
+	"Index",
+	"Index-topic",
+};
+
+tagconfig = array[NTAG] of {
+	"-font /fonts/lucidasans/unicode.6.font",
+	"-font /fonts/lucidasans/unicode.7.font",
+	"-font /fonts/lucidasans/unicode.8.font",
+	"-font /fonts/lucidasans/unicode.10.font",
+	"-font /fonts/lucidasans/unicode.13.font",
+	"-font /fonts/lucidasans/italiclatin1.6.font",
+	"-font /fonts/lucidasans/italiclatin1.7.font",
+	"-font /fonts/lucidasans/italiclatin1.8.font",
+	"-font /fonts/lucidasans/italiclatin1.10.font",
+	"-font /fonts/lucidasans/italiclatin1.13.font",
+	"-font /fonts/lucidasans/boldlatin1.6.font",
+	"-font /fonts/lucidasans/boldlatin1.7.font",
+	"-font /fonts/lucidasans/boldlatin1.8.font",
+	"-font /fonts/lucidasans/boldlatin1.10.font",
+	"-font /fonts/lucidasans/boldlatin1.13.font",
+	"-font /fonts/lucidasans/typelatin1.6.font",
+	"-font /fonts/lucidasans/typelatin1.7.font",
+	"-font /fonts/pelm/latin1.9.font",
+	"-font /fonts/pelm/ascii.12.font",
+	"-font /fonts/pelm/ascii.16.font",
+	"-foreground #444444 -lmargin1 1c -lmargin2 1c; .ft.t tag lower Example",
+	"-foreground #444444; .ft.t tag lower Caption",
+	"-foreground #444444 -lmargin1 1c -lmargin2 1c; .ft.t tag lower List",
+	"-foreground #0000A0; .ft.t tag lower List-elem",
+	"-foreground #444444; .ft.t tag lower Label",
+	"-foreground #444444; .ft.t tag lower Label-ref",
+	"-foreground #444444; .ft.t tag lower Exercise",
+	"-foreground #444444; .ft.t tag lower Heading",
+	"-foreground #444444; .ft.t tag lower No-fill",
+	"-foreground #444444; .ft.t tag lower Author",
+	"-foreground #444444; .ft.t tag lower Title",
+	"-foreground #444444; .ft.t tag lower Index",
+	"-foreground #444444; .ft.t tag lower Index-topic",
+};
+
+enabled := array[] of {"disabled", "normal"};
+
+File: adt
+{
+	tk:			ref Tk->Toplevel;
+	isctl:			int;
+	applyfont:		int;
+	fontsused:	int;
+	name:		string;
+	dirty:		int;
+	font:			string;	# set by the buttons, not nec. by the text
+	size:			int;		# set by the buttons, not nec. by the text
+	fonttag:		string;	# set by the buttons, not nec. by the text
+	configed:		array of int;
+	button1:		int;
+	button3:		int;
+	fontsok:		int;		# fonts and tags can be set
+	extensions:	list of ref Ext;
+};
+
+Ext: adt
+{
+	tkname:		string;
+	modname:	string;
+	mod:		Brutusext;
+	args:			string;
+};
+
+menuindex := "0";
+snarftext := "";
+snarfsgml := "";
+central: chan of (ref File, string);
+files:	array of ref File;	# global but modified only by control thread
+plumbed := 0;
+curdir := "";
+lang := "";
+
+init(c: ref Context, argv: list of string)
+{
+	ctxt = c;
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "brutus: no window context\n");
+		raise "fail:bad context";
+	}
+ 	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+	bufio = load Bufio Bufio->PATH;
+	plumbmsg = load Plumbmsg Plumbmsg->PATH;
+
+	if(plumbmsg->init(1, "edit", 1000) >= 0){
+		plumbed = 1;
+		workdir := load Workdir Workdir->PATH;
+		curdir = workdir->init();
+		workdir = nil;
+	}
+
+	tkclient->init();
+	dialog->init();
+	selectfile->init();
+	sys->pctl(Sys->NEWPGRP, nil);	# so we can pass "exit" command to tkclient
+
+	file := "";
+	if(argv != nil)
+		argv = tl argv;
+	if(argv != nil)
+		file = hd argv;
+	central = chan of (ref File, string);
+	spawn control(ctxt);
+	<-central;
+	spawn brutus(ctxt, file);
+}
+
+# build menu button for dynamically generated menu
+buttoncfg(label, enable: string): string
+{
+	return "label .b."+label+" -text "+label + " " + enable +
+		";bind .b."+label+" <Button-1> {send cmd "+label+"}" +
+		";bind .b."+label+" <ButtonRelease-1> {}" +
+		";bind .b."+label+" <Motion-Button-1> {}" +
+		";bind .b."+label+" <Double-Button-1> {}" +
+		";bind .b."+label+" <Double-ButtonRelease-1> {}" +
+		";bind .b."+label+" <Enter> {.b."+label+" configure -background #EEEEEE}" +
+		";bind .b."+label+" <Leave> {.b."+label+" configure -background #DDDDDD}";
+}
+
+tkchans(t: ref Tk->Toplevel): (chan of string, chan of string, chan of string, chan of string, chan of string, chan of string, chan of string)
+{
+	keys := chan of string;
+	tk->namechan(t, keys, "keys");
+	edit := chan of string;
+	tk->namechan(t, edit, "edit");
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	but1 := chan of string;
+	tk->namechan(t, but1, "but1");
+	but2 := chan of string;
+	tk->namechan(t, but2, "but2");
+	but3 := chan of string;
+	tk->namechan(t, but3, "but3");
+	drag := chan of string;
+	tk->namechan(t, drag, "Wm_drag");
+	return (keys, edit, cmd, but1, but2, but3, drag);
+}
+
+control(ctxt: ref Context)
+{
+	(t, titlectl) := tkclient->toplevel(ctxt, SETFONT, Name, Tkclient->Appl);
+
+	# f is not used to store anything, just to simplify interfaces
+	# shared by control and brutus
+	f := ref File (t, 1, 0, 0, "", 0, DEFFONTNAME, DEFSIZE, DEFTAG, nil, 0, 0, 0, nil);
+
+	tkcmds(t, menu_cfg);
+	tkcmd(t, "frame .b");
+	tkcmd(t, buttoncfg("File", ""));
+	tkcmds(t, control_cfg);
+	tkcmds(t, input_cfg);
+	files = array[1] of ref File;
+	files[0] = f;
+
+	(keys, edit, cmd, but1, but2, but3, drag) := tkchans(t);
+
+	tkcmd(t, ".ft.t mark set typingstart 1.0; .ft.t mark gravity typingstart left");
+	central <-= (nil, "");	# signal readiness
+#	spawn tkclient->wmctl(t, "task");
+	curfile: ref File;
+
+	plumbc := chan of (string, string);
+	spawn plumbproc(plumbc);
+
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+	tkclient->onscreen(t, nil);
+	tkclient->wmctl(t, "task");
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+
+	menu := <-t.ctxt.ctl or
+	menu = <-t.wreq or
+	menu = <-titlectl =>
+		if(menu == "exit"){
+			if(shutdown(ctxt, t)){
+				killplumb();
+				tkclient->wmctl(t, menu);
+			}
+			break;
+		}
+		# spawn tkclient->wmctl(t, menu);
+		tkclient->wmctl(t, menu);
+
+	ecmd := <-edit =>
+		editor(f, ecmd);
+		tkcmd(t, FOCUS);
+
+	c := <-cmd =>
+		(nil, s) := sys->tokenize(c, " ");
+		case hd s {
+		* =>
+			sys->print("unknown control cmd %s\n",c );
+		"File" =>
+			filemenu(t, 0, 0);
+		"new" =>
+			(name, ok, nil) := getfilename(ctxt, t, "file for new window", f.name, 1, 0, 0);
+			if(ok)
+				spawn brutus(ctxt, name);
+		"select" =>
+			n := int hd tl s;
+			if(n > len files)
+				break;
+			if(n > 0)
+				curfile = files[n];
+			tkcmd(files[n].tk, ". map; raise .; focus .ft.t");
+		"focus" =>
+			;
+		}
+
+	(file, action) := <-central =>
+		(nil, s) := sys->tokenize(action, " ");
+		case hd s {
+		* =>
+			sys->print("control unknown central command %s\n", action);
+		"new" =>
+			curfile = file;
+			nfiles := array[len files+1] of ref File;
+			nfiles[0:] = files;
+			files = nfiles;
+			nfiles = nil;	# make sure references don't linger
+			files[len files-1] = file;
+		"name" =>
+			name := nameof(file);
+			index := 0;
+			for(i:=1; i<len files; i++)
+				if(files[i] == file){
+					index = i;
+					break;
+				}
+			if(index == 0)
+				sys->print("can't find file\n");
+		"focus" =>
+			if(file != f)
+				curfile = file;
+		"select" =>
+			n := int hd tl s;
+			if(n >= len files)
+				break;
+			if(n > 0)
+				curfile = files[n];
+			tkcmd(files[n].tk, ". map; raise .; focus .ft.t; update");
+		"exiting" =>
+			if(file == nil)
+				break;
+			if(file == curfile)
+				curfile = nil;
+			index := 0;
+			for(i:=1; i<len files; i++)
+				if(files[i] == file){
+					index = i;
+					break;
+				}
+			if(index == 0)
+				sys->print("can't find file\n");
+			else{
+				# make a new one rather than slice, to clean up references
+				nfiles := array[len files-1] of ref File;
+				for(i=0; i<index; i++)
+					nfiles[i] = files[i];
+				for(; i<len nfiles; i++)
+					nfiles[i] = files[i+1];
+				files = nfiles;
+			}
+			file = nil;
+		}
+	c := <-keys =>
+		char := typing(f, c);
+		if(curfile!=nil && char=='\n' && insat(t, "end"))
+			execute(t, curfile, tkcmd(t, ".ft.t get insert-1line insert"));
+
+	c := <-but1 =>
+		mousebut1(f, c);
+
+	c := <-but2 =>
+		mousebut2(f, c);
+
+	c := <-but3 =>
+		mousebut3(f, c);
+
+	c := <-drag =>
+		if(len c < 6 || c[0:5] != "path=")
+			break;
+		spawn brutus(ctxt, c[5:]);
+
+	(fname, addr) := <-plumbc =>
+		for(i:=1; i<len files; i++)
+			if(files[i].name == fname){
+				tkcmd(files[i].tk, ". map; raise .; focus .ft.t");
+				showaddr(files[i], addr);
+				break;
+			}
+		if(i == len files){
+			if(addr != "")
+				spawn brutus(ctxt, fname+":"+addr);
+			else
+				spawn brutus(ctxt, fname);
+		}
+	}
+}
+
+brutus(ctxt: ref Context, filename: string)
+{
+	addr := "";
+	for(i:=len filename; --i>0; ){
+		if(filename[i] == ':'){
+			(ok, dir) := sys->stat(filename[0:i]);
+			if(ok >= 0){
+				addr = filename[i+1:];
+				filename = filename[0:i];
+				break;
+			}
+		}
+	}
+
+	(t, titlectl)  := tkclient->toplevel(ctxt, SETFONT, Name, Tkclient->Appl);
+
+	f := ref File (t, 0, 0, 0, filename, 0, DEFFONTNAME, DEFSIZE, DEFTAG, nil, 0, 0, 0, nil);
+	f.configed = array[NTAG] of {* => 0};
+
+	tkcmds(t, menu_cfg);
+	tkcmd(t, "frame .b");
+	tkcmd(t, buttoncfg("File", ""));
+	tkcmd(t, buttoncfg("Ext", "-state disabled"));
+
+	tkcmds(t, brutus_cfg);
+	tkcmds(t, input_cfg);
+
+	# buttons work better when they grab the mouse
+	a := array[] of {".b.Tag", ".b.Applyfontnow", ".b.Applysizenow", ".b.Applyfontsizenow"};
+	for(i=0; i<len a; i++){
+		tkcmd(t, "bind "+a[i]+" <Button-1> +{grab set "+a[i]+"}");
+		tkcmd(t, "bind "+a[i]+" <ButtonRelease-1> +{grab release "+a[i]+"}");
+	}
+
+	(keys, edit, cmd, but1, but2, but3, drag) := tkchans(t);
+
+	configfont(f, "Heading");
+	configfont(f, "Title");
+	configfont(f, f.fonttag);
+	tkcmd(t, ".ft.t mark set typingstart 1.0; .ft.t mark gravity typingstart left");
+	tkcmd(t, "image create bitmap waiting -file cursor.wait");
+
+	central <-= (f, "new");
+	setfilename(f, filename);
+
+	if(filename != "")
+		if(loadfile(f, filename) < 0)
+			dialog->prompt(ctxt, t.image, "error -fg red",
+				"Open file",
+				sys->sprint("Can't read %s:\n%r", filename),
+				0, "Continue" :: nil);
+		else
+			showaddr(f, addr);
+
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+
+	menu := <-t.ctxt.ctl or
+	menu = <-t.wreq or
+	menu = <-titlectl =>
+		case menu {
+		"exit" =>
+			if(f.dirty){
+				action := confirm(ctxt, t, nameof(f)+" is dirty", 1);
+				case action {
+				"cancel" =>
+					continue;
+				"exitclean" =>
+					if(dumpfile(f, f.name, f.fontsused) < 0)
+						continue;
+					break;
+				"exitdirty" =>
+					break;
+				}
+			}
+			central <-= (f, "exiting");
+			# this one tears down temporaries holding references to f
+			central <-= (nil, "exiting");
+			return;
+		"task" =>
+			tkcmd(t, ". unmap");
+		* =>
+			tkclient->wmctl(t, menu);
+		}
+
+	ecmd := <-edit =>
+		editor(f, ecmd);
+		tkcmd(t, FOCUS);
+
+	command := <-cmd =>
+		(nil, c) := sys->tokenize(command, " ");
+		case hd c {
+		* =>
+			sys->print("unknown command %s\n", command);
+		"File" =>
+			filemenu(t, 1, f.fontsok);
+		"Ext" =>
+			extmenu(t);
+		"new" =>
+			(name, ok, nil) := getfilename(ctxt, t, "file for new window", f.name, 1, 0, 0);
+			if(ok)
+				spawn brutus(ctxt, name);
+		"open" =>
+			if(f.dirty){
+				action := confirm(ctxt, t, nameof(f)+" is dirty", 1);
+				case action {
+				"cancel" =>
+					continue;
+				"exitclean" =>
+					if(dumpfile(f, f.name, f.fontsused) < 0)
+						continue;
+					break;
+				"exitdirty" =>
+					break;
+				}
+			}
+			(name, ok, nil) := getfilename(ctxt, t, "file for this window", f.name, 1, 0, 0);
+			if(ok && name!=""){
+				setfilename(f, name);
+				if(loadfile(f, name) < 0){
+					tkcmd(t, ".ft.t delete 1.0 end");
+					dialog->prompt(ctxt, t.image, "error -fg red",
+						"Open file",
+						sys->sprint("Can't open %s:\n%r", name),
+						0, "Continue"::nil);
+				}
+			}
+		"name" =>
+			(name, ok, nil) := getfilename(ctxt, t, "remembered file name", f.name, 1, 0, 0);
+			if(ok){
+				if(name != f.name){
+					setfilename(f, name);
+					dirty(f, 1);
+				}
+			}
+		"write" =>
+			(name, ok, sgml) := getfilename(ctxt, t, "file to write", f.name, 1, 1, f.fontsused);
+			if(ok && name!=""){
+				if(f.name == ""){
+					setfilename(f, name);
+					dirty(f, 1);
+				}
+				dumpfile(f, name, sgml);
+			}
+		"fonts" =>
+			if(f.fontsok==0 && f.fontsused==0){
+				action := confirm(ctxt, t, "Converting "+nameof(f)+" to SGML", 0);
+				case action {
+				"cancel" =>
+					continue;
+				"exitdirty" =>
+					usingfonts(f);
+					dirty(f, 1);
+				}
+			}
+			enablefonts(f, !f.fontsok);
+		"language" =>
+			if(lang == "")
+				lang = "Hebrew";
+			else
+				lang = "";
+		"addext" =>
+			ext := hd tl c;
+			(args, ok, nil) := getfilename(ctxt, t, "parameters for "+ext, "", 0, 0, 0);
+			if(ok){
+				tkcmd(t, "cursor -image waiting; update");
+				addextension(f, ext+" "+args, nil);
+				usingfonts(f);
+				dirty(f, 1);
+				tkcmd(t, "cursor -default; update");
+			}
+		"select" =>
+			central <-= (f, command);
+		"tag" =>
+			tageditor(ctxt, f);
+			tkcmd(t, FOCUS);
+		"font" =>
+			f.font = hd tl c;
+			tkcmd(t, ".b.Font configure -text "+f.font+";"+UPDATE);
+			f.fonttag = f.font+"."+string f.size;
+			configfont(f, f.fonttag);
+			if(changefont(f, f.font))
+				dirty(f, 1);
+		"size" =>
+			sz := hd tl c;
+			tkcmd(t, ".b.Size configure -text "+sz+"pt; update");
+			f.size = int sz;
+			f.fonttag = f.font+"."+string f.size;
+			configfont(f, f.fonttag);
+			if(changesize(f, string f.size))
+				dirty(f, 1);
+		"applyfont" =>
+			f.applyfont = int tkcmd(t, "variable Applyfont");
+			if(f.applyfont)
+				configfont(f, f.fonttag);
+		"applyfontnow" =>
+			if(changefont(f, f.font))
+				dirty(f, 1);
+		"applysizenow" =>
+			if(changesize(f, string f.size))
+				dirty(f, 1);
+		"applyfontsizenow" =>
+			if(changefontsize(f, f.fonttag))
+				dirty(f, 1);
+		"put" =>
+			dumpfile(f, f.name, f.fontsused);
+		"focus" =>
+			central <-= (f, "focus");
+		}
+
+	c := <-keys =>
+		typing(f, c);
+
+	c := <-but1 =>
+		mousebut1(f, c);
+
+	c := <-but2 =>
+		mousebut2(f, c);
+
+	c := <-but3 =>
+		mousebut3(f, c);
+
+	c := <-drag =>
+		if(len c < 6 || c[0:5] != "path=")
+			break;
+		spawn brutus(ctxt, c[5:]);
+	}
+}
+
+kbdremap(c: int) : (int, int)
+{
+	tab: array of Remaptab;
+
+	dir := 1;
+	case lang{
+	"" =>
+		return (c, dir);
+	"Hebrew" =>
+		tab = hebrewtab;
+		dir = -1;
+	* =>
+		sys->print("unknown language %s\n", lang);
+		return (c, dir);
+	}
+	for(i:=0; i<len tab; i++)
+		if(c == tab[i].in)
+			return (tab[i].out, dir);
+	return (c, 1);
+}
+
+typing(f: ref File, c: string): int
+{
+	t := f.tk;
+	char := c[1];
+	if(char == '\\')
+		char = c[2];
+	update := ";.ft.t see insert;"+UPDATE;
+	if(char != ESC)
+		cut(f, 1);
+	case char {
+	* =>
+		dir := 1;
+		if(c[1] != '\\')	# safe character; remap it
+			(c[1], dir) = kbdremap(char);
+		s := ".ft.t insert insert "+c;
+		if(dir < 0)
+			s += ";.ft.t mark set insert insert-1c";
+		if(f.applyfont){
+			usingfonts(f);
+			s += f.fonttag;
+		}
+		tkcmd(t, s+update);
+		if(f.fontsused && f.applyfont==0){
+			# nasty goo to make sure we don't insert text without a font tag;
+			# must ask after the fact if default rules set a tag.
+			names := tkcmd(t, ".ft.t tag names insert-1chars");
+			if(!somefont(names))
+				tkcmd(t, ".ft.t tag add "+DEFTAG+" insert-1chars");
+		}
+		dirty(f, 1);
+	ESC =>
+		if(nullsel(t))
+			tkcmd(t, ".ft.t tag add sel typingstart insert;"+
+					".ft.t mark set typingstart insert");
+		else
+			cut(f, 1);
+		tkcmd(t, UPDATE);
+	BS =>
+		bs(f, "c");
+	BSL =>
+		bs(f, "l");
+	BSW =>
+		bs(f, "w");
+	}
+	return char;
+}
+
+bs(f: ref File, c: string)
+{
+	if(!insat(f.tk, "1.0")){
+		tkcmd(f.tk, ".ft.t tkTextDelIns -"+c+";.ft.t see insert;"+UPDATE);
+		dirty(f, 1);
+	}
+}
+
+mousebut1(f: ref File, c: string)
+{
+	f.button1 = (c == "pressed");
+	f.button3 = 0;	# abort any pending button 3 action
+	tkcmd(f.tk, ".ft.t mark set typingstart insert");
+}
+
+mousebut2(f: ref File, c: string)
+{
+	if(f.button1){
+		cut(f, 1);
+		tk->cmd(f.tk, UPDATE);
+	}else{
+		(nil, l) := sys->tokenize(c, " ");
+		x := int hd l - 50;
+		y := int hd tl l - int tk->cmd(f.tk, ".m yposition "+menuindex) - 10;
+#		tkcmd(f.tk, "focus .ft.t");
+		tkcmd(f.tk, ".m activate "+menuindex+"; .m post "+string x+" "+string y+
+			"; update");
+	}
+}
+
+mousebut3(f: ref File, c: string)
+{
+	t := f.tk;
+	if(c == "pressed"){
+		f.button3 = 1;
+		if(f.button1){
+			paste(f);
+			tk->cmd(t, "update");
+		}
+		return;
+	}
+	if(!plumbed || f.button3==0 || f.button1!=0)
+		return;
+	f.button3 = 0;
+	# Plumb message triggered by release of button 3
+	(nil, l) := sys->tokenize(c, " ");
+	x := int hd tl l;
+	y := int hd tl tl l;
+	index := tk->cmd(t, ".ft.t index @"+string x+","+string y);
+	selindex := tk->cmd(t, ".ft.t tag ranges sel");
+	if(selindex != "")
+		insel := tk->cmd(t, ".ft.t compare sel.first <= "+index)=="1" &&
+			tk->cmd(t, ".ft.t compare sel.last >= "+index)=="1";
+	else
+		insel = 0;
+	attr := "";
+	if(insel)
+		text := tk->cmd(t, ".ft.t get sel.first sel.last");
+	else{
+		# have line with text in it
+		# now extract whitespace-bounded string around click
+		(nil, w) := sys->tokenize(index, ".");
+		charno := int hd tl w;
+		left := tk->cmd(t, ".ft.t index {"+index+" linestart}");
+		right := tk->cmd(t, ".ft.t index {"+index+" lineend}");
+		line := tk->cmd(t, ".ft.t get "+left+" "+right);
+		for(i:=charno; i>0; --i)
+			if(line[i-1]==' ' || line[i-1]=='\t')
+				break;
+		for(j:=charno; j<len line; j++)
+			if(line[j]==' ' || line[j]=='\t')
+				break;
+		text = line[i:j];
+		attr = "click="+string (charno-i);
+	}
+	msg := ref Msg(
+		"Brutus",
+		"",
+		directory(f),
+		"text",
+		attr,
+		array of byte text);
+	if(msg.send() < 0)
+		sys->fprint(sys->fildes(2), "brutus: plumbing write error: %r\n");
+}
+
+directory(f: ref File): string
+{
+	for(i:=len f.name; --i>=0;)
+		if(f.name[i] == '/'){
+			if(i == 0)
+				i++;
+			return f.name[0:i];
+		}
+	return curdir;
+}
+
+enablefonts(f: ref File, enable: int)
+{
+	for(i:=0; i<len fontbuts; i++)
+		tkcmd(f.tk, fontbuts[i] + " configure -state "+enabled[enable]);
+	tkcmd(f.tk, "update");
+	f.fontsok = enable;
+}
+
+filemenu(t: ref tk->Toplevel, buttons, fontsok: int)
+{
+	tkcmd(t, "menu .b.Filemenu");
+	tkcmd(t, ".b.Filemenu add command -label New -command {send cmd new}");
+	if(buttons){
+		tkcmd(t, ".b.Filemenu add command -label Open -command {send cmd open}");
+		tkcmd(t, ".b.Filemenu add command -label Name -command {send cmd name}");
+		tkcmd(t, ".b.Filemenu add command -label Write -command {send cmd write}");
+		if(fontsok)
+			pre := "Dis";
+		else
+			pre = "En";
+		tkcmd(t, ".b.Filemenu add command -label {"
+			+pre+"able Fonts} -command {send cmd fonts}");
+		if(lang == "")
+			pre = "En";
+		else
+			pre = "Dis";
+		tkcmd(t, ".b.Filemenu add command -label {"
+			+pre+"able Hebrew} -command {send cmd language}");
+	}
+	tkcmd(t, ".b.Filemenu add command -label {["+Name+"]} -command {send cmd select 0}");
+	if(files != nil)
+		for(i:=1; i<len files; i++){
+			name := nameof(files[i]);
+			if(files[i].dirty)
+				name = "{' "+name+"}";
+			else
+				name = "{  "+name+"}";
+			tkcmd(t, ".b.Filemenu add command -label "+name+
+				" -command {send cmd select "+string i+"}");
+		}
+	tkcmd(t, "bind .b.Filemenu <Unmap> {destroy .b.Filemenu}");
+	x := tk->cmd(t, ".ft.scroll cget actx");
+	y := tk->cmd(t, ".ft.scroll cget acty");
+	tkcmd(t, ".b.Filemenu post "+x+" "+y+"; grab set .b.Filemenu; update");
+}
+
+extmenu(t: ref tk->Toplevel)
+{
+	fd := sys->open(EXTDIR, Sys->OREAD);
+	if(fd == nil || ((n,dir):=sys->dirread(fd)).t0<=0){
+		sys->print("%s: can't find extension directory %s: %r\n", Name, EXTDIR);
+		return;
+	}
+
+	tkcmd(t, "menu .b.Extmenu");
+	for(i:=0; i<n; i++){
+		name := dir[i].name;
+		if(len name>4 && name[len name-4:]==".dis"){
+			name = name[0:len name-4];
+			tkcmd(t, ".b.Extmenu add command -label {Add "+name+
+				"} -command {send cmd addext "+name+"}");
+		}
+	}
+
+	tkcmd(t, "bind .b.Extmenu <Unmap> {destroy .b.Extmenu}");
+	x := tk->cmd(t, ".ft.scroll cget actx");
+	y := tk->cmd(t, ".ft.scroll cget acty");
+	tkcmd(t, ".b.Extmenu post "+x+" "+y+"; grab set .b.Extmenu; update");
+}
+
+basepath(file: string): (string, string)
+{
+	for(i := len file-1; i >= 0; i--) {
+		if(file[i] == '/')
+			return (file[0:i], file[i+1:]);
+	}
+	return (".", file);
+}
+
+putbut(f: ref File)
+{
+	state := enabled[f.dirty];
+	if(f.name != "")
+		tkcmd(f.tk, ".b.Put configure -state "+state+"; update");
+}
+
+dirty(f: ref File, nowdirty: int)
+{
+	if(f.isctl)
+		return;
+	old := f.dirty;
+	f.dirty = nowdirty;
+	if(old != nowdirty){
+		setfilename(f, f.name);
+		putbut(f);
+	}
+}
+
+setfilename(f: ref File, name: string)
+{
+	oldname := f.name;
+	f.name = name;
+	if(oldname=="" && name!="")
+		putbut(f);
+	name = Name + ": \"" +nameof(f)+ "\"";
+	if(f.dirty)
+		name += " (dirty)";
+	tkclient->settitle(f.tk, name);
+	tkcmd(f.tk, UPDATE);
+	central <-= (f, "name");
+}
+
+configfont(f: ref File, tag: string)
+{
+	for(i:=0; i<NTAG; i++)
+		if(tag == tagname[i]){
+			if(f.configed[i] == 0){
+				tkcmd(f.tk, ".ft.t tag configure "+tag+" "+tagconfig[i]);
+				f.configed[i] = 1;
+			}
+			return;
+		}
+	sys->print("Brutus: can't configure font %s\n", tag);
+}
+
+insat(t: ref Tk->Toplevel, mark: string): int
+{
+	return tkcmd(t, ".ft.t compare insert == "+mark) == "1";
+}
+
+isalnum(s: string): int
+{
+	if(s == "")
+		return 0;
+	c := s[0];
+	if('a' <= c && c <= 'z')
+		return 1;
+	if('A' <= c && c <= 'Z')
+		return 1;
+	if('0' <= c && c <= '9')
+		return 1;
+	if(c == '_')
+		return 1;
+	if(c > 16rA0)
+		return 1;
+	return 0;
+}
+
+editor(f: ref File, ecmd: string)
+{
+
+	case ecmd {
+	"cut" =>
+		menuindex = "0";
+		cut(f, 1);
+	
+	"paste" =>
+		menuindex = "1";
+		paste(f);
+
+	"snarf" =>
+		menuindex = "2";
+		if(nullsel(f.tk))
+			return;
+		snarf(f);
+
+	"look" =>
+		menuindex = "3";
+		look(f);
+	}
+	tkcmd(f.tk, UPDATE);
+}
+
+nullsel(t: ref Tk->Toplevel): int
+{
+	return tkcmd(t, ".ft.t tag ranges sel") == "";
+}
+
+cut(f: ref File, snarfit: int)
+{
+	if(nullsel(f.tk))
+		return;
+	dirty(f, 1);
+	if(snarfit)
+		snarf(f);
+	# sometimes when clicking fast, selection and insert point can
+	# separate.  the only time this really matters is when typing into
+	# a double-clicked selection.  it's easy to fix here.
+	tkcmd(f.tk, ".ft.t mark set insert sel.first;.ft.t delete sel.first sel.last");
+}
+
+snarf(f: ref File)
+{
+	# convert sel.first and sel.last to numeric forms because sgml()
+	# must clear selection to avoid <sel> tags in result.
+	(nil, sel) := sys->tokenize(tkcmd(f.tk, ".ft.t tag ranges sel"), " ");
+	snarftext = tkcmd(f.tk, ".ft.t get "+hd sel+" "+hd tl sel);
+	snarfsgml = sgml(f.tk, "-sgml", hd sel, hd tl sel);
+	tkclient->snarfput(snarftext);
+}
+
+paste(f: ref File)
+{
+#	good question
+	snarftext = tkclient->snarfget();
+	if(snarftext == "" && (f.fontsused == 0 || snarfsgml == nil))
+		return;
+	cut(f, 0);
+	dirty(f, 1);
+
+	t := f.tk;
+	start := tkcmd(t, ".ft.t index insert");
+	if(f.fontsused == 0)
+		tkcmd(t, ".ft.t insert insert '"+snarftext);
+	else if(f.applyfont)
+		tkcmd(t, ".ft.t insert insert "+tk->quote(snarftext)+" "+f.fonttag);
+	else
+		insert(f, snarfsgml);
+	tkcmd(t, ".ft.t tag add sel "+start+" insert");
+}
+
+look(f: ref File)
+{
+	t := f.tk;
+	(sel0, sel1) := word(t);
+	if(sel0 == nil)
+		return;
+	text := tkcmd(t, ".ft.t get "+sel0+" "+sel1);
+	if(text == nil)
+		return;
+	tkcmd(t, "cursor -image waiting; update");
+	search(nil, f, text, 0, 0);
+	tkcmd(t, "cursor -default; update");
+}
+
+# First time fonts are used explicitly, establish font tags for all extant text.
+usingfonts(f: ref File)
+{
+	if(f.fontsused)
+		return;
+	tkcmd(f.tk, ".ft.t tag add "+DEFTAG+" 1.0 end");
+	f.fontsused = 1;
+}
+
+word(t: ref Tk->Toplevel): (string, string)
+{
+	start := "sel.first";
+	end := "sel.last";
+	if(nullsel(t)){
+		insert := tkcmd(t, ".ft.t index insert");
+		start = tkcmd(t, ".ft.t index {insert wordstart}");
+		if(insert == start){	# tk's definition of 'wordstart' is bogus
+			# if at beginning, tk->cmd will return !error and a0 will be false.
+			a0 := isalnum(tk->cmd(t, ".ft.t get insert-1chars"));
+			a1 := isalnum(tk->cmd(t, ".ft.t get insert"));
+			if(a0==0 && a1==0)
+				return (nil, nil);
+			if(a1 == 0)
+				start = tkcmd(t, ".ft.t index {insert-1chars wordstart}");
+		}
+		end = tkcmd(t, ".ft.t index {"+start+" wordend}");
+		if(start == end)
+			return (nil, nil);
+	}
+	return (start, end);
+}
+
+# Change the font associated with the selection
+changefont(f: ref File, font: string): int
+{
+	t := f.tk;
+	(sel0, sel1) := word(f.tk);
+	mod := 0;
+	if(sel0 == nil)
+		return mod;
+	usingfonts(f);
+	for(i:=0; i<NFONT; i++){
+		if(fontname[i] == font)
+			continue;
+		for(j:=0; j<NSIZE; j++){
+			tag := fontname[i]+"."+sizename[j];
+			start := sel0;
+			for(;;){
+				range := tkcmd(t, ".ft.t tag nextrange "+tag+" "+start+" "+sel1);
+				if(len range > 0 && range[0] == '!')
+					break;
+				(nil, tt) := sys->tokenize(range, " ");
+				if(tt == nil)
+					break;
+				tkcmd(t, ".ft.t tag remove "+tag+" "+hd tt+" "+hd tl tt);
+				fs := font+"."+sizename[j];
+				tkcmd(t, ".ft.t tag add "+fs+" "+hd tt+" "+hd tl tt);
+				configfont(f, fs);
+				start = hd tl tt;
+				mod = 1;
+			}
+		}
+	}
+	tkcmd(t, UPDATE);
+	return mod;
+}
+
+# See if tag list includes a font name
+somefont(tag: string): int
+{
+	(nil, tt) := sys->tokenize(tag, " ");
+	for(; tt!=nil; tt=tl tt)
+		for(i:=0; i<NFONT*NSIZE; i++){
+			if(tagname[i] == hd tt)
+				return 1;
+		}
+	return 0;
+}
+
+# Change the size associated with the selection
+changesize(f: ref File, size: string): int
+{
+	t := f.tk;
+	(sel0, sel1) := word(f.tk);
+	mod := 0;
+	if(sel0 == nil)
+		return mod;
+	usingfonts(f);
+	for(i:=0; i<NFONT; i++){
+		for(j:=0; j<NSIZE; j++){
+			if(sizename[j] == size)
+				continue;
+			tag := fontname[i]+"."+sizename[j];
+			start := sel0;
+			for(;;){
+				range := tkcmd(t, ".ft.t tag nextrange "+tag+" "+start+" "+sel1);
+				if(len range > 0 && range[0] == '!')
+					break;
+				(nil, tt) := sys->tokenize(range, " ");
+				if(tt == nil)
+					break;
+				tkcmd(t, ".ft.t tag remove "+tag+" "+hd tt+" "+hd tl tt);
+				fs := fontname[i]+"."+size;
+				tkcmd(t, ".ft.t tag add "+fs+" "+hd tt+" "+hd tl tt);
+				configfont(f, fs);
+				start = hd tl tt;
+				mod = 1;
+			}
+		}
+	}
+	tkcmd(t, UPDATE);
+	return mod;
+}
+
+# Change the font and size associated with the selection
+changefontsize(f: ref File, newfontsize: string): int
+{
+	t := f.tk;
+	(sel0, sel1) := word(f.tk);
+	if(sel0 == nil)
+		return 0;
+	usingfonts(f);
+	(nil, names) := sys->tokenize(tkcmd(t, ".ft.t tag names"), " ");
+	# clear old tags
+	tags := tagname[0:NFONT*NSIZE];
+	for(l:=names; l!=nil; l=tl l)
+		for(i:=0; i<len tags; i++)
+			if(tags[i] == hd l)
+				tkcmd(t, ".ft.t tag remove "+hd l+" "+sel0+" "+sel1);
+	tkcmd(t, ".ft.t tag add "+newfontsize+" "+sel0+" "+sel1+"; update");
+	return 1;
+}
+
+listtostring(l: list of string): string
+{
+	s := "{";
+	while(l != nil){
+		if(len s == 1)
+			s += hd l;
+		else
+			s += " " + hd l;
+		l = tl l;
+	}
+	s += "}";
+	return s;
+}
+
+# splitl based on indices rather than slices.  this version returns char
+# position of the matching character.
+splitl(str: string, i, j: int, pat: string): int
+{
+	while(i < j){
+		c := str[i];
+		for(k:=len pat-1; k>=0; k--)
+			if(c == pat[k])
+				return i;
+		i++;
+	}
+	return i;
+}
+
+# splitstrl based on indices rather than slices. this version returns char
+# position of the beginning of the matching string.
+splitstrl(str: string, i, j: int, pat: string): int
+{
+	l := len pat;
+	if(l == 0)	# shouldn't happen, but be safe
+		return j;
+	first := pat[0];
+	while(i <= j-l){
+		# check first char for speed
+		if(str[i] == first){
+			for(k:=1; k<l && str[i+k]==pat[k]; k++)
+				;
+			if(k == l)
+				return i;
+		}
+		i++;
+	}
+	return j;
+}
+
+# place the text, as annotated by SGML tags, into document
+# where indicated by insert mark
+insert(f: ref File, sgml: string)
+{
+	taglist: list of string;
+
+	t := f.tk;
+	usingfonts(f);
+	if(f.applyfont)
+		taglist = f.fonttag :: taglist;
+	tag := listtostring(taglist);
+	end := len sgml;
+	j: int;
+	for(i:=0; i<end; i=j){
+		j = splitl(sgml, i, end, "<&");
+		tt := tag;
+		if(tt=="" || tt=="{}")
+			tt = DEFTAG;	# can happen e.g. when pasting plain text
+		if(j > i)
+			tkcmd(t, ".ft.t insert insert "+tk->quote(sgml[i:j])+" "+tt);
+		if(j < end)
+			case sgml[j] {
+			'&' =>
+				if(j+4<=end && sgml[j:j+4]=="&lt;"){
+					tkcmd(t, ".ft.t insert insert "+"{<} "+tt);
+					j += 4;
+				}else{
+					tkcmd(t, ".ft.t insert insert {&} "+tt);
+					j += 1;
+				}
+			'<' =>
+				(nc, newtag, on) := tagstring(sgml, j, end);
+				if(nc < 0){
+					tkcmd(t, ".ft.t insert insert "+"{<} "+tt);
+					j += 1;
+				}else if(len newtag>9 && newtag[0:10]=="Extension "){
+					addextension(f, newtag[10:], taglist);
+					j += nc;
+				}else if(len newtag>9 && newtag[0:7]=="Window "){
+					repostextension(f, newtag[7:], taglist);
+					j += nc;
+				}else{
+					if(on){
+						taglist = newtag :: taglist;
+						configfont(f, newtag);
+					}else{
+						taglist = drop(taglist, newtag);
+						if(f.applyfont && hasfonts(taglist)==0)
+							taglist = f.fonttag :: taglist;
+					}
+					j += nc;
+					tag = listtostring(taglist);
+				}
+			}
+	}
+}
+
+drop(l: list of string, s: string): list of string
+{
+	n: list of string;
+	while(l != nil){
+		if(s != hd l)
+			n = hd l :: n;
+		l = tl l;
+	}
+	return n;
+}
+
+extid := 0;
+addextension(f: ref File, s: string, taglist: list of string)
+{
+	for(i:=0; i<len s; i++)
+		if(s[i] == ' ')
+			break;
+	if(i == 0 || i == len s){
+		sys->print("Brutus: badly formed extension %s\n", s);
+		return;
+	}
+	modname := s[0:i];
+	s = s[i+1:];
+
+	mod: Brutusext;
+	for(el:=f.extensions; el!=nil; el=tl el)
+		if(modname == (hd el).modname){
+			mod = (hd el).mod;
+			break;
+		}
+
+	if(mod == nil){
+		file := modname;
+		if(i < 4 || file[i-4:i] != ".dis")
+			file += ".dis";
+		if(file[0] != '/')
+			file = "/dis/wm/brutus/" + file;
+		mod = load Brutusext file;
+		if(mod == nil){
+			sys->print("%s: can't load module %s: %r\n", Name, file);
+			return;
+		}
+	}
+	mkextension(f, mod, modname, s, taglist);
+}
+
+repostextension(f: ref File, tkname: string, taglist: list of string)
+{
+	mod: Brutusext;
+	for(el:=f.extensions; el!=nil; el=tl el)
+		if(tkname == (hd el).tkname){
+			mod = (hd el).mod;
+			break;
+		}
+	if(mod == nil){
+		sys->print("Brutus: can't find extension widget %s: %r\n", tkname);
+		return;
+	}
+
+	mkextension(f, mod, (hd el).modname, (hd el).args, taglist);
+}
+
+mkextension(f: ref File, mod: Brutusext, modname, args: string, taglist: list of string)
+{
+	t := f.tk;
+
+	name := ".ext"+string extid++;
+	mod->init(sys, draw, bufio, tk, tkclient);
+	err := mod->create(f.name, t, name, args);
+	if(err != ""){
+		sys->print("%s: can't create extension widget %s: %s\n", Name, modname, err);
+		return;
+	}
+	tkcmd(t, ".ft.t window create insert -window "+name);
+	while(taglist != nil){
+		tkcmd(t, ".ft.t tag add "+hd taglist+" "+name);
+		taglist = tl taglist;
+	}
+	f.extensions = ref Ext(name, modname, mod, args) :: f.extensions;
+}
+
+# rewrite <window .ext1> tags into <Extension module args>
+extrewrite(f: ref File, sgml: string): string
+{
+	if(f.extensions == nil)
+		return sgml;
+
+	new := "";
+
+	end := len sgml;
+	j: int;
+	for(i:=0; i<end; i=j){
+		j = splitstrl(sgml, i, end, "<Window ");
+		if(j > i)
+			new += sgml[i:j];
+		if(j < end){
+			j += 8;
+			for(k:=j; sgml[k]!='>' && k<end; k++)
+				;
+			tkname := sgml[j:k];
+			for(el:=f.extensions; el!=nil; el=tl el)
+				if((hd el).tkname == tkname)
+					break;
+			if(el == nil)
+				sys->print("%s: unrecognized extension %s\n", Name, tkname);
+			else{
+				e := hd el;
+				new += "<Extension "+e.modname+" "+e.args+">";
+			}
+			j = k+1;	# skip '>'
+		}
+	}
+	return new;
+}
+
+hasfonts(l: list of string): int
+{
+	for(i:=0; i<NFONT*NSIZE; i++)
+		for(ll:=l; ll!=nil; ll=tl ll)
+			if(hd ll == tagname[i])
+				return 1;
+	return 0;
+}
+
+# s[i] is known to be a less-than sign
+tagstring(s: string, i, end: int): (int, string, int)
+{
+	tag: string;
+
+	j := splitl(s, i+1, end, ">");
+	if(j==end || s[j]!='>')
+		return (-1, "", 0);
+	nc := (j-i)+1;
+	on := 1;
+	if(s[i+1] == '/'){
+		on = 0;
+		i++;
+	}
+	tag = s[i+1:j];
+# NEED TO CHECK VALIDITY OF TAG
+	return (nc, tag, on);
+}
+
+sgml(t: ref Tk->Toplevel, flag, start, end: string): string
+{
+	# turn off selection, to avoid getting that in output
+	sel := tkcmd(t, ".ft.t tag ranges sel");
+	if(sel != "")
+		tkcmd(t, ".ft.t tag remove sel "+sel);
+	s := tkcmd(t, ".ft.t dump "+flag+" "+start+" "+end);
+	if(sel != "")
+		tkcmd(t, ".ft.t tag add sel "+sel);
+	return s;
+}
+
+loadfile(f: ref File, file: string): int
+{
+	f.size = DEFSIZE;
+	f.font = DEFFONTNAME;
+	f.fonttag = DEFTAG;
+	f.fontsused = 0;
+	enablefonts(f, 0);
+	t := f.tk;
+	tkcmd(t, ".b.Font configure -text "+f.font);
+	tkcmd(t, ".b.Size configure -text "+string f.size+"pt");
+	tkcmd(t, "cursor -image waiting; update");
+	r := loadfile1(f, file);
+	tkcmd(t, "cursor -default");
+	return r;
+}
+
+loadfile1(f: ref File, file: string): int
+{
+	fd := bufio->open(file, Sys->OREAD);
+	if(fd == nil)
+		return -1;
+	(ok, dir) := sys->fstat(fd.fd);
+	if(ok < 0){
+		fd.close();
+		return -1;
+	}
+	l := int dir.length;
+	a := array[l] of byte;
+	n := fd.read(a, len a);
+	fd.close();
+	if(n != len a)
+		return -1;
+	t := f.tk;
+	tkcmd(t, ".ft.t delete 1.0 end");
+	if(len a>=7 && string a[0:7]=="<SGML>\n")
+		insert(f, string a[7:n]);
+	else
+		tkcmd(t, ".ft.t insert 1.0 '"+string a[0:n]);
+	dirty(f, 0);
+	tkcmd(t, ".ft.t mark set insert 1.0; update");
+	return 1;
+}
+
+dumpfile(f: ref File, file: string, sgml: int): int
+{
+	tkcmd(f.tk, "cursor -image waiting");
+	r := dumpfile1(f, file, sgml);
+	tkcmd(f.tk, "cursor -default");
+	return r;
+}
+
+dumpfile1(f: ref File, file: string, sgml: int): int
+{
+	if(writefile(f, file, sgml) < 0){
+		dialog->prompt(ctxt, f.tk.image, "error -fg red",
+			"Write file",
+			sys->sprint("Can't write %s:\n%r", file),
+			0, "Continue"::nil);
+		tkcmd(f.tk, FOCUS);
+		return -1;
+	}
+	return 1;
+}
+
+writefile(f: ref File, file: string, sgmlfmt: int): int
+{
+	if(file == "")
+		return -1;
+	fd := bufio->create(file, Sys->OWRITE, 8r666);
+	if(fd == nil)
+		return -1;
+
+	t := f.tk;
+	flag := "";
+	if(sgmlfmt){
+		flag = "-sgml";
+		prefix := "<SGML>\n";
+		if(f.fontsused == 0)
+			prefix += "<"+DEFTAG+">";
+		x := array of byte prefix;
+		if(fd.write(x, len x) != len x){
+			fd.close();
+			return -1;
+		}
+	}
+	sgmltext := sgml(t, flag, "1.0", "end");
+	if(sgmlfmt)
+		sgmltext = extrewrite(f, sgmltext);
+	a := array of byte sgmltext;
+	if(fd.write(a, len a) != len a){
+		fd.close();
+		return -1;
+	}
+	if(sgmlfmt && f.fontsused==0){
+		suffix := array of byte ("</"+DEFTAG+">");
+		if(fd.write(suffix, len suffix) != len suffix){
+			fd.close();
+			return -1;
+		}
+	}
+	if(fd.flush() < 0){
+		fd.close();
+		return -1;
+	}
+	fd.close();
+	if(file == f.name){
+		dirty(f, sgmlfmt!=f.fontsused);
+		tkcmd(t, UPDATE);
+	}
+	return 1;
+}
+
+shutdown(s: ref Draw->Context, t: ref Tk->Toplevel): int
+{
+	for(i:=1; i<len files; i++){
+		f := files[i];
+		if(f.dirty){
+			action := confirm(s, t, "file "+nameof(f)+" is dirty", 1);
+			case action {
+			"cancel" =>
+				return 0;
+			"exitclean" =>
+				if(dumpfile(f, f.name, f.fontsused) < 0)
+					return 0;
+			"exitdirty" =>
+				break;
+			}
+		}
+	}
+	return 1;
+}
+
+nameof(f: ref File): string
+{
+	s := f.name;
+	if(s == "")
+		s = "(unnamed)";
+	return s;
+}
+
+tkcmd(t: ref Tk->Toplevel, s: string): string
+{
+	res := tk->cmd(t, s);
+	if(len res > 0 && res[0] == '!')
+		sys->print("%s: tk error executing '%s': %s\n", Name, s, res);
+	return res;
+}
+
+confirm_cfg := array[] of {
+	"frame .f -borderwidth 2 -relief groove -padx 3 -pady 3",
+	"frame .f.f",
+#	"label .f.f.l -bitmap error -foreground red",
+	"label .f.f.l -text Warning:",
+	"label .f.f.m",
+	"button .f.exitclean -text {  Write and Proceed  } -width 17w -command {send cmd exitclean}",
+	"button .f.exitdirty -text {  Proceed  } -width 17w -command {send cmd exitdirty}",
+	"button .f.cancel -text {  Cancel  } -width 17w -command {send cmd cancel}",
+	"pack .f.f.l .f.f.m -side left",
+	"pack .f.f .f.exitclean .f.exitdirty .f.cancel -padx 10 -pady 10",
+	"pack .f",
+};
+
+widget(parent: ref Tk->Toplevel, ctxt: ref Draw->Context, cfg: array of string): ref Tk->Toplevel
+{
+	x := int tk->cmd(parent, ". cget -x");
+	y := int tk->cmd(parent, ". cget -y");
+	where := sys->sprint("-x %d -y %d ", x+45, y+25);
+	(t,nil) := tkclient->toplevel(ctxt, where+SETFONT+" -borderwidth 2 -relief raised", "", tkclient->Plain);
+	tkcmds(t, cfg);
+	return t;
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	for(i := 0; i < len a; i++)
+		v := tkcmd(top, a[i]);
+}
+
+confirm(ctxt: ref Draw->Context, parent: ref Tk->Toplevel, message: string, write: int): string
+{
+	s := confirm1(ctxt, parent, message, write);
+	tkcmd(parent, FOCUS);
+	return s;
+}
+
+confirm1(ctxt: ref Draw->Context, parent: ref Tk->Toplevel, message: string, write: int): string
+{
+	t := widget(parent, ctxt, confirm_cfg);
+	tkcmd(t, ".f.f.m configure -text '"+message);
+	if(write == 0)
+		tkcmd(t, "destroy .f.exitclean");
+	tkcmd(t, UPDATE);
+	tkclient->onscreen(t, "onscreen");
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	tkclient->onscreen(t, "exact");
+	tkclient->startinput(t, "ptr"::nil);
+	for(;;) alt {
+		s := <-t.ctxt.ptr =>
+			tk->pointer(t, *s);
+		c := <-cmd =>
+			return c;
+	}
+	return <-cmd;
+}
+
+getfilename_cfg := array[] of {
+	"frame .f",
+	"label .f.Message",
+	"entry .f.Name -width 25w",
+	"checkbutton .f.SGML -text { Write SGML } -variable SGML",
+	"button .f.Ok -text {  OK  } -width 14w -command {send cmd ok}",
+	"button .f.Browse -text {  Browse  } -width 14w -command {send cmd browse}",
+	"button .f.Cancel -text {  Cancel  } -width 14w -command {send cmd cancel}",
+	"bind .f.Name <Control-j> {send cmd ok}",
+	"pack .f.Message .f.Name .f.SGML .f.Ok .f.Browse .f.Cancel -padx 10 -pady 10",
+	"pack .f",
+	"focus .f.Name",
+};
+
+getfilename(ctxt: ref Draw->Context, parent: ref Tk->Toplevel, message, name: string, browse, sgml, nowsgml: int): (string, int, int)
+{
+	(s, i, issgml) := getfilename1(ctxt, parent, message, name, browse, sgml, nowsgml);
+	tkcmd(parent, FOCUS);
+	return (s, i, issgml);
+}
+
+getfilename1(ctxt: ref Draw->Context, parent: ref Tk->Toplevel, message, name: string, browse, sgml, nowsgml: int): (string, int, int)
+{
+	t := widget(parent, ctxt, getfilename_cfg);
+	tkcmds(t, getfilename_cfg);
+
+	tkcmd(t, ".f.Message configure -text '"+message);
+	tk->cmd(t, ".f.Name insert 0 "+name);
+	if(browse == 0)
+		tkcmd(t, "destroy .f.Browse");
+	if(sgml == 0)
+		tkcmd(t, "destroy .f.SGML");
+	else if(nowsgml)
+		tkcmd(t, ".f.SGML select");
+	tkcmd(t, UPDATE);
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	tkclient->onscreen(t, "exact");
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+	for(;;) alt {
+		s := <-t.ctxt.kbd =>
+			tk->keyboard(t, s);
+		s := <-t.ctxt.ptr =>
+			tk->pointer(t, *s);
+		c := <-cmd =>
+			case c {
+			"ok" =>
+				return (tkcmd(t, ".f.Name get"), 1, int tkcmd(t, "variable SGML"));
+			"cancel" =>
+				return ("", 0, 0);
+			"browse" =>
+				name = tkcmd(t, ".f.Name get");
+				(dir, path) := basepath(name);
+	
+				pat := list of {
+					"* (All files)",
+					"*.sgml (SGML dump files)",
+					"*.html (Web source files)",
+					"*.tex (Latex source files)",
+					"*.[bm] (Limbo source files)"
+				};
+	
+				path = selectfile->filename(ctxt, parent.image, message, pat, dir);
+				if(path != "")
+					name = path;
+				tk->cmd(t, ".f.Name delete 0 end; .f.Name insert 0 "+name+";focus .f.Name; update");
+				if(path != "")
+					return (name, 1, int tkcmd(t, "variable SGML"));
+		}
+	}
+}
+
+tageditor(ctxt: ref Draw->Context, f: ref File)
+{
+	(start, end) := word(f.tk);
+	if(start == nil)
+		return;
+	cfg := array[100] of string;
+	i := 0;
+	cfg[i++] = "frame .f";
+	(nil, names) := sys->tokenize(tkcmd(f.tk, ".ft.t tag names "+start), " ");
+	pack := "pack";
+	set := array[NEXTRA] of int;
+	for(j:=0; j<NEXTRA; j++){
+		n := tagname[j+NFONT*NSIZE];
+		cfg[i++] = "checkbutton .f.c"+string j+" -variable c"+string j+
+			" -text {"+n+"} -command {send cmd "+string j+"} -anchor w";
+		pack += " .f.c"+string j;
+		set[j] = 0;
+		for(l:=names; l!=nil; l=tl l)
+			if(hd l == n){
+				cfg[i++] = ".f.c"+string j+" select";
+				set[j] = 1;
+			}
+	}
+	cfg[i++] = "button .f.Ok -text {  OK  } -width 6w -command {send cmd ok}";
+	cfg[i++] = "button .f.Cancel -text {  Cancel  } -width 6w -command {send cmd cancel}";
+	cfg[i++] = pack + " -padx 3 -pady 0 -fill x";
+	cfg[i++] = "pack .f.Ok .f.Cancel -padx 2 -pady 2 -side left";
+	cfg[i++] = "pack .f; grab set .f; update";
+	t := widget(f.tk, ctxt, cfg[0:i]);
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	tkclient->onscreen(t, "exact");
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+    loop:
+	for(;;){
+		alt{
+		s := <-t.ctxt.kbd =>
+			tk->keyboard(t, s);
+		s := <-t.ctxt.ptr =>
+			tk->pointer(t, *s);
+		c := <-cmd =>
+			case c {
+			"ok" =>
+				break loop;
+			"cancel" =>
+				return;
+			* =>
+				j = int c;
+				set[j] = (tkcmd(t, "variable c"+c) == "1");
+			}
+		}
+	}
+	for(j=0; j<NEXTRA; j++){
+		s := tagname[j+NFONT*NSIZE];
+		if(set[j]){
+			configfont(f, s);
+			tkcmd(f.tk, ".ft.t tag add "+s+" "+start+" "+end);
+		}else
+			tkcmd(f.tk, ".ft.t tag remove "+s+" "+start+" "+end);
+	}
+	dirty(f, 1);
+	usingfonts(f);
+	tkcmd(f.tk, UPDATE);
+}
+
+plumbpid: int;
+plumbproc(plumbc: chan of (string, string))
+{
+	plumbpid = sys->pctl(0, nil);
+
+	for(;;){
+		msg := Msg.recv();
+		if(msg == nil){
+			sys->print("Brutus: can't read /chan/plumb.edit: %r\n");
+			plumbpid = 0;
+			return;
+		}
+		if(msg.kind != "text"){
+			sys->print("Brutus: can't interpret '%s' kind of message\n", msg.kind);
+			continue;
+		}
+		text := string msg.data;
+		n := len text;
+		addr := "";
+		for(j:=0; j<n; j++)
+			if(text[j] == ':'){
+				addr = text[j+1:];
+				break;
+			}
+		file := text[0:j];
+		if(len file>0 && file[0]!='/' && len msg.dir>0){
+			if(msg.dir[len msg.dir-1] == '/')
+				file = msg.dir+file;
+			else
+				file = msg.dir+"/"+file;
+		}
+		plumbc <-= (file, addr);
+	}
+}
+
+killplumb()
+{
+	if(plumbed == 0)
+		return;
+	plumbmsg->shutdown();
+	if(plumbpid <= 0)
+		return;
+	fname := sys->sprint("#p/%d/ctl", plumbpid);
+	fd := sys->open(fname, sys->OWRITE);
+	if(fd != nil)
+		sys->write(fd, array of byte "kill\n", 8);
+}
+
+lastpat: string;
+
+execute(cmdwin: ref Tk->Toplevel, f: ref File, cmd: string)
+{
+	if(len cmd>1 && cmd[len cmd-1]=='\n')
+		cmd = cmd[0:len cmd-1];
+	if(cmd == "")
+		return;
+	if(cmd[0] == '/' || cmd[0]=='?'){
+		search(cmdwin, f, cmd[1:], cmd[0]=='?', 1);
+		return;
+	}
+	for(i:=0; i<len cmd; i++)
+		if(cmd[i]<'0' || '9'<cmd[i]){
+			sys->print("bad command %s\n", cmd);
+			return;
+		}
+	t := f.tk;
+	line := int cmd;
+	if(!nullsel(t))
+		tkcmd(t, NOSEL);
+	tkcmd(t, ".ft.t tag add sel "+string line+".0 {"+string line+".0 lineend+1char}");
+	tkcmd(t, ".ft.t mark set insert "+string line+".0; .ft.t see insert;update");
+}
+
+search(cmdwin: ref Tk->Toplevel, f: ref File, pat: string, backwards, uselast: int)
+{
+	t := f.tk;
+	if(pat == nil)
+		pat = lastpat;
+	else if(uselast)
+		lastpat = pat;
+	if(pat == nil){
+		error(cmdwin, "no pattern");
+		return;
+	}
+	cmd := ".ft.t search ";
+	if(backwards)
+		cmd += "-backwards ";
+	p := "";
+	for(i:=0; i<len pat; i++){
+		if(pat[i]== '\\' || pat[i]=='{')
+			p[len p] = '\\';
+		p[len p] = pat[i];
+	}
+	cmd += "{"+p+"} ";
+	null := nullsel(t);
+	if(null)
+		cmd += "insert";
+	else if(backwards)
+		cmd += "sel.first";
+	else
+		cmd += "sel.last";
+	s := tk->cmd(t, cmd);
+	if(s == "")
+		error(cmdwin, "not found");
+	else{
+		if(!null)
+			tkcmd(t, NOSEL);
+		tkcmd(t, ".ft.t tag add sel "+s+" "+s+"+"+string len pat+"chars");
+		tkcmd(t, ".ft.t mark set insert "+s+";.ft.t see insert; update");
+	}
+}
+
+showaddr(f: ref File, addr: string)
+{
+	 if(addr=="")
+		return;
+	t := f.tk;
+	if(addr[0]=='#' || ('0'<=addr[0] && addr[0]<='9')){
+		# UGLY! just do line and character numbers until we get a
+		# decent command/address interface set up.
+		if(!nullsel(t))
+			tkcmd(t, NOSEL);
+		if(addr[0] == '#'){
+			addr = addr[1:];
+			tkcmd(t, ".ft.t mark set insert {1.0+"+addr+"char}; .ft.t see insert;update");
+		}else{
+			tkcmd(t, ".ft.t tag add sel "+addr+".0 {"+addr+".0 lineend+1char}");
+			tkcmd(t, ".ft.t mark set insert "+addr+".0; .ft.t see insert;update");
+		}
+	}
+}
+
+error(cmdwin: ref Tk->Toplevel, err: string)
+{
+	if(cmdwin == nil)
+		return;
+	tkcmd(cmdwin, ".ft.t insert end '?"+err+"\n");
+	if(!nullsel(cmdwin))
+		tkcmd(cmdwin, NOSEL);
+	tkcmd(cmdwin, ".ft.t mark set insert end");
+	tkcmd(cmdwin, ".ft.t mark set typingstart end; update");
+}
--- /dev/null
+++ b/appl/wm/brutus/excerpt.b
@@ -1,0 +1,264 @@
+implement Brutusext;
+
+# <Extension excerpt file [start [end]]>
+
+Name:	con "Brutus entry";
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context: import draw;
+
+include	"bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include	"regex.m";
+	regex: Regex;
+
+include "tk.m";
+	tk: Tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include	"brutus.m";
+include	"brutusext.m";
+
+init(s: Sys, d: Draw, b: Bufio, t: Tk, w: Tkclient)
+{
+	sys = s;
+	draw = d;
+	bufio = b;
+	tk = t;
+	tkclient = w;
+	regex = load Regex Regex->PATH;
+}
+
+create(parent: string, t: ref Tk->Toplevel, name, args: string): string
+{
+	(text, err) := gather(parent, args);
+	if(err != nil)
+		return err;
+	err = tk->cmd(t, "text "+name+" -tabs {1c} -wrap none -font /fonts/pelm/latin1.9.font");
+	if(len err > 0 && err[0] == '!')
+		return err;
+	(n, maxw) := nlines(text);
+	if(maxw < 40)
+		maxw = 40;
+	if(maxw > 70)
+		maxw = 70;
+	tk->cmd(t, name+" configure -height "+string n+".01h -width "+string maxw+"w");
+	return tk->cmd(t, name+" insert end '"+text);
+}
+
+gather(parent, args: string): (string, string)
+{
+	argl := tokenize(args);
+	nargs := len argl;
+	if(nargs == 0)
+		return (nil, "usage: excerpt [start] [end] file");
+	file := hd argl;
+	argl = tl argl;
+	b := bufio->open(fullname(parent, file), Bufio->OREAD);
+	if(b == nil)
+		return (nil, sys->sprint("can't open %s: %r", file));
+	start := "";
+	end := "";
+	if(argl != nil){
+		start = hd argl;
+		if(tl argl != nil)
+			end = hd tl argl;
+	}
+	(text, err) := readall(b, start, end);
+	return (text, err);
+}
+
+tokenize(s: string): list of string
+{
+	l: list of string;
+	i := 0;
+	a := "";
+	first := 1;
+	while(i < len s){
+		(a, i) = arg(first, s, i);
+		if(a != "")
+			l = a :: l;
+		first = 0;
+	}
+	rl: list of string;
+	while(l != nil){
+		rl = hd l :: rl;
+		l = tl l;
+	}
+	return rl;
+}
+
+arg(first: int, s: string, i: int): (string, int)
+{
+	while(i<len s && (s[i]==' ' || s[i]=='\t'))
+		i++;
+	if(i == len s)
+		return ("", i);
+	j := i+1;
+	if(first || s[i] != '/'){
+		while(j<len s && (s[j]!=' ' && s[j]!='\t'))
+			j++;
+		return (s[i:j], j);
+	}
+	while(j<len s && s[j]!='/')
+		if(s[j++] == '\\')
+			j++;
+	if(j == len s)
+		return (s[i:j], j);
+	return (s[i:j+1], j+1);
+}
+
+readall(b: ref Iobuf, start, end: string): (string, string)
+{
+	revlines : list of string = nil;
+	appending := 0;
+	lineno := 0;
+	for(;;){
+		line := b.gets('\n');
+		if(line == nil)
+			break;
+		lineno++;
+		if(!appending){
+			m := match(start, line, lineno);
+			if(m < 0)
+				return (nil, "error in pattern");
+			if(m)
+				appending = 1;
+		}
+		if(appending){
+			revlines = line :: revlines;
+			if(start != ""){
+				m := match(end, line, lineno);
+				if(m < 0)
+					return (nil, "error in pattern");
+				if(m)
+					break;
+			}
+		}
+	}
+	return (prep(revlines), "");
+}
+
+prep(revlines: list of string) : string
+{
+	tabstrip := -1;
+	for(l:=revlines; l != nil; l = tl l) {
+		s := hd l;
+		if(len s > 1) {
+			n := nleadtab(hd l);
+			if(tabstrip == -1 || n < tabstrip)
+				tabstrip = n;
+		}
+	}
+	# remove tabstrip tabs from each line
+	# and concatenate in reverse order
+	ans := "";
+	for(l=revlines; l != nil; l = tl l) {
+		s := hd l;
+		if(tabstrip > 0 && len s > 1)
+			s = s[tabstrip:];
+		ans = s + ans;
+	}
+	return ans;
+}
+
+nleadtab(s: string) : int
+{
+	slen := len s;
+	for(i:=0; i<slen; i++)
+		if(s[i] != '\t')
+			break;
+	return i;
+}
+
+nlines(s: string): (int, int)
+{
+	n := 0;
+	maxw := 0;
+	w := 0;
+	for(i:=0; i<len s; i++) {
+		if(s[i] == '\n') {
+			n++;
+			if(w > maxw)
+				maxw = w;
+			w = 0;
+		}
+		else if(s[i] == '\t')
+			w += 5;
+		else
+			w++;
+	}
+	if(len s>0 && s[len s-1]!='\n') {
+		n++;
+		if(w > maxw)
+			maxw = w;
+	}
+	return (n, maxw);
+}
+
+match(pat, line: string, lineno: int): int
+{
+	if(pat == "")
+		return 1;
+	case pat[0] {
+	'0' to '9' =>
+		return int pat <= lineno;
+	'/' =>
+		if(len pat < 3 || pat[len pat-1]!='/')
+			return -1;
+		re := compile(pat[1:len pat-1]);
+		if(re == nil)
+			return -1;
+		match := regex->execute(re, line);
+		return match != nil;
+	}
+	return -1;
+}
+
+pats: list of (string, Regex->Re);
+
+compile(pat: string): Regex->Re
+{
+	l := pats;
+	while(l != nil){
+		(p, r) := hd l;
+		if(p == pat)
+			return r;
+		l = tl l;
+	}
+	(re, nil) := regex->compile(pat, 0);
+	pats = (pat, re) :: pats;
+	return re;
+}
+
+cook(parent: string, nil: int, args: string): (ref Brutusext->Celem, string)
+{
+	(text, err) := gather(parent, args);
+	if(err != nil)
+		return (nil, err);
+	el1 := ref Brutusext->Celem(Brutusext->Text, text, nil, nil, nil, nil);
+	el2 := ref Brutusext->Celem(Brutus->Type*Brutus->NSIZE+Brutus->Size10, "", el1, nil, nil, nil);
+	el1.parent = el2;
+	ans := ref Brutusext->Celem(Brutus->Example, "", el2, nil, nil, nil);
+	el2.parent = ans;
+	return (ans, "");
+}
+
+fullname(parent, file: string): string
+{
+	if(len parent==0 || (len file>0 && (file[0]=='/' || file[0]=='#')))
+		return file;
+
+	for(i:=len parent-1; i>=0; i--)
+		if(parent[i] == '/')
+			return parent[0:i+1] + file;
+	return file;
+}
--- /dev/null
+++ b/appl/wm/brutus/image.b
@@ -1,0 +1,259 @@
+implement Brutusext;
+
+# <Extension image imagefile>
+
+Name:	con "Brutus image";
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context, Image, Display, Rect: import draw;
+
+include	"bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "imagefile.m";
+	imageremap: Imageremap;
+	readgif: RImagefile;
+	readjpg: RImagefile;
+
+include "tk.m";
+	tk: Tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "pslib.m";
+	pslib: Pslib;
+
+include	"brutus.m";
+include	"brutusext.m";
+
+stderr: ref Sys->FD;
+
+Cache: adt
+{
+	args:		string;
+	name:	string;
+	r:		Rect;
+};
+
+init(s: Sys, d: Draw, b: Bufio, t: Tk, w: Tkclient)
+{
+	sys = s;
+	draw = d;
+	bufio = b;
+	tk = t;
+	tkclient = w;
+	imageremap = load Imageremap Imageremap->PATH;
+	stderr = sys->fildes(2);
+}
+
+cache: list of ref Cache;
+
+create(parent: string, t: ref Tk->Toplevel, name, args: string): string
+{
+	if(imageremap == nil)
+		return sys->sprint(Name + ": can't load remap: %r");
+	display := t.image.display;
+	file := args;
+
+	for(cl:=cache; cl!=nil; cl=tl cl)
+		if((hd cl).args == args)
+			break;
+
+	c: ref Cache;
+	if(cl != nil)
+		c = hd cl;
+	else{
+		(im, mask, err) := loadimage(display, parent, file);
+		if(err != "")
+			return err;
+		imagename := name+file;
+		err = tk->cmd(t, "image create bitmap "+imagename);
+		if(len err > 0 && err[0] == '!')
+			return err;
+		err = tk->putimage(t, imagename, im, mask);
+		if(len err > 0 && err[0] == '!')
+			return err;
+		c = ref Cache(args, imagename, im.r);
+		cache = c :: cache;
+	}
+
+	err := tk->cmd(t, "canvas "+name+" -height "+string c.r.dy()+" -width "+string c.r.dx());
+	if(len err > 0 && err[0] == '!')
+		return err;
+	err = tk->cmd(t, name+" create image 0 0 -anchor nw -image "+c.name);
+
+	return "";
+}
+
+loadimage(display: ref Display, parent, file: string) : (ref Image, ref Image, string)
+{
+	im := display.open(fullname(parent, file));
+	mask: ref Image;
+
+	if(im == nil){
+		fd := bufio->open(fullname(parent, file), Bufio->OREAD);
+		if(fd == nil)
+			return (nil, nil, sys->sprint(Name + ": can't open %s: %r", file));
+
+		mod := filetype(file, fd);
+		if(mod == nil)
+			return (nil, nil, sys->sprint(Name + ": can't find decoder module for %s: %r", file));
+
+		(ri, err) := mod->read(fd);
+		if(ri == nil)
+			return (nil, nil, sys->sprint(Name + ": %s: %s", file, err));
+		if(err != "")
+			sys->fprint(stderr, Name + ": %s: %s", file, err);
+		mask = transparency(display, ri);
+
+		# if transparency is enabled, errdiff==1 is probably a mistake,
+		# but there's no easy solution.
+		(im, err) = imageremap->remap(ri, display, 1);
+		if(im == nil)
+			return (nil, nil, sys->sprint(Name+": remap %s: %s\n", file, err));
+		if(err != "")
+			sys->fprint(stderr, Name+": remap %s: %s\n", file, err);
+		ri = nil;
+	}
+	return(im, mask, "");
+}
+
+cook(parent: string, fmt: int, args: string): (ref Brutusext->Celem, string)
+{
+	file := args;
+	ans : ref Brutusext->Celem = nil;
+	if(fmt == Brutusext->FHtml) {
+		s := "<IMG SRC=\"" + file + "\">";
+		ans = ref Brutusext->Celem(Brutusext->Special, s, nil, nil, nil, nil);
+	}
+	else {
+		(rc, dir) := sys->stat(file);
+		if(rc < 0)
+			return (nil, "can't find " + file);
+		mtime := dir.mtime;
+
+		# psfile name: in dir of file, with .ps suffix
+		psfile := file;
+		for(i := (len psfile)-1; i >= 0; i--) {
+			if(psfile[i] == '.') {
+				psfile = psfile[0:i];
+				break;
+			}
+		}
+		psfile = psfile + ".ps";
+		(rc, dir) = sys->stat(psfile);
+		if(rc < 0 || dir.mtime < mtime) {
+			iob := bufio->create(psfile, Bufio->OWRITE, 8r664);
+			if(iob == nil)
+				return (nil, "can't create " + psfile);
+
+			display := draw->Display.allocate("");
+			(im, mask, err) := loadimage(display, parent, file);
+			if(err != "")
+				return (nil, err);
+			pslib = load Pslib Pslib->PATH;
+			if(pslib == nil)
+				return (nil, "can't load Pslib");
+			pslib->init(bufio);
+			pslib->writeimage(iob, im, 100);
+			iob.close();
+		}
+		s := "\\epsfbox{" + psfile + "}\n";
+		ans = ref Brutusext->Celem(Brutusext->Special, s, nil, nil, nil, nil);
+	}
+	return (ans, "");
+}
+
+fullname(parent, file: string): string
+{
+	if(len parent==0 || (len file>0 && (file[0]=='/' || file[0]=='#')))
+		return file;
+
+	for(i:=len parent-1; i>=0; i--)
+		if(parent[i] == '/')
+			return parent[0:i+1] + file;
+	return file;
+}
+
+#
+# rest of this is all borrowed from wm/view.
+# should probably be packaged - perhaps in RImagefile?
+#
+filetype(file: string, fd: ref Iobuf): RImagefile
+{
+	if(len file>4 && file[len file-4:]==".gif")
+		return loadgif();
+	if(len file>4 && file[len file-4:]==".jpg")
+		return loadjpg();
+
+	# sniff the header looking for a magic number
+	buf := array[20] of byte;
+	if(fd.read(buf, len buf) != len buf){
+		sys->fprint(stderr, "View: can't read %s: %r\n", file);
+		return nil;
+	}
+	fd.seek(big 0, 0);
+	if(string buf[0:6]=="GIF87a" || string buf[0:6]=="GIF89a")
+		return loadgif();
+	jpmagic := array[] of {byte 16rFF, byte 16rD8, byte 16rFF, byte 16rE0,
+		byte 0, byte 0, byte 'J', byte 'F', byte 'I', byte 'F', byte 0};
+	for(i:=0; i<len jpmagic; i++)
+		if(jpmagic[i]>byte 0 && buf[i]!=jpmagic[i])
+			break;
+	if(i == len jpmagic)
+		return loadjpg();
+	return nil;
+}
+
+loadgif(): RImagefile
+{
+	if(readgif == nil){
+		readgif = load RImagefile RImagefile->READGIFPATH;
+		if(readgif == nil)
+			sys->fprint(stderr, "Brutus image: can't load readgif: %r\n");
+		else
+			readgif->init(bufio);
+	}
+	return readgif;
+}
+
+loadjpg(): RImagefile
+{
+	if(readjpg == nil){
+		readjpg = load RImagefile RImagefile->READJPGPATH;
+		if(readjpg == nil)
+			sys->fprint(stderr, "Brutus image: can't load readjpg: %r\n");
+		else
+			readjpg->init(bufio);
+	}
+	return readjpg;
+}
+
+transparency(display: ref Display, r: ref RImagefile->Rawimage): ref Image
+{
+	if(r.transp == 0)
+		return nil;
+	if(r.nchans != 1)
+		return nil;
+	i := display.newimage(r.r, display.image.chans, 0, 0);
+	if(i == nil){
+		return nil;
+	}
+	pic := r.chans[0];
+	npic := len pic;
+	mpic := array[npic] of byte;
+	index := r.trindex;
+	for(j:=0; j<npic; j++)
+		if(pic[j] == index)
+			mpic[j] = byte 0;
+		else
+			mpic[j] = byte 16rFF;
+	i.writepixels(i.r, mpic);
+	return i;
+}
--- /dev/null
+++ b/appl/wm/brutus/mkfile
@@ -1,0 +1,24 @@
+<../../../mkconfig
+
+TARG=\
+	excerpt.dis\
+	image.dis\
+	mod.dis\
+	table.dis\
+
+MODULES=\
+
+SYSMODULES=\
+	brutus.m\
+	brutusext.m\
+	bufio.m\
+	draw.m\
+	html.m\
+	imagefile.m\
+	pslib.m\
+	regex.m\
+	string.m\
+
+DISBIN=$ROOT/dis/wm/brutus
+
+<$ROOT/mkfiles/mkdis
--- /dev/null
+++ b/appl/wm/brutus/mod.b
@@ -1,0 +1,335 @@
+implement Brutusext;
+
+# <Extension mod file>
+# For module descriptions (in book)
+
+Name:	con "Brutus mod";
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context, Font: import draw;
+
+include	"bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "string.m";
+	S : String;
+
+include	"brutus.m";
+	Size8, Index, Roman, Italic, Bold, Type, NFONT, NSIZE: import Brutus;
+
+include	"brutusext.m";
+
+Mstring: adt
+{
+	s: string;
+	style: int;
+	indexed: int;
+	width: int;
+	next: cyclic ref Mstring;
+};
+
+fontname :=  array[NFONT] of {
+	"/fonts/lucidasans/unicode.7.font",
+	"/fonts/lucidasans/italiclatin1.7.font",
+	"/fonts/lucidasans/boldlatin1.7.font",
+	"/fonts/lucidasans/typelatin1.7.font",
+	};
+
+fontswitch :=  array[NFONT] of {
+	"\\fontseries{m}\\rmfamily ",
+	"\\itshape ",
+	"\\fontseries{b}\\rmfamily ",
+	"\\fontseries{mc}\\ttfamily ",
+	};
+
+fontref := array[NFONT] of ref Font;
+
+LEFTCHARS: con 45;
+LEFTPIX: con LEFTCHARS*7;	# 7 is width of lucidasans/typelatin1.7 chars
+
+init(s: Sys, d: Draw, b: Bufio, t: Tk, w: Tkclient)
+{
+	sys = s;
+	draw = d;
+	bufio = b;
+	tk = t;
+	tkclient = w;
+	S = load String String->PATH;
+}
+
+create(parent: string, t: ref Tk->Toplevel, name, args: string): string
+{
+	(spec, err) := getspec(parent, args);
+	if(err != nil)
+		return err;
+	n := len spec;
+	if(n == 0)
+		return "empty spec";
+	d := t.image.display;
+	for(i:=0; i < NFONT; i++) {
+		if(i == Bold || fontref[i] != nil)
+			continue;
+		fontref[i] = Font.open(d, fontname[i]);
+		if(fontref[i] == nil)
+			return sys->sprint("can't open font %s: %r\n", fontname[i]);
+	}
+	(nil, nil, rw, nil) := measure(spec, 1);
+	lw := LEFTPIX;
+	wd := lw + rw;
+	fnt := fontref[Roman];
+	ht := n * fnt.height;
+	err = tk->cmd(t, "canvas " + name + " -width " + string wd
+			+ " -height " + string ht
+			+ " -font " + fontname[Type]);
+	if(len err > 0 && err[0] == '!')
+		return "problem creating canvas";
+	y := 0;
+	xl := 0;
+	xr := lw;
+	for(l := spec; l != nil; l = tl l) {
+		(lm, rm) := hd l;
+		canvmstring(t, name, lm, xl, y);
+		canvmstring(t, name, rm, xr, y);
+		y += fnt.height;
+	}
+	tk->cmd(t, "update");
+	return "";
+}
+
+canvmstring(t: ref Tk->Toplevel, canv: string, m: ref Mstring, x, y: int)
+{
+	# assume fonts all have same ascent
+	while(m != nil) {
+		pos := string x + " " + string y;
+		font := "";
+		if(m.style != Type)
+			font = " -font " + fontname[m.style];
+		e := tk->cmd(t, canv + " create text " + pos + " -anchor nw "
+			+ font + " -text '" + m.s);
+		x += m.width;
+		m = m.next;
+	}
+}
+
+getspec(parent, args: string) : (list of (ref Mstring, ref Mstring), string)
+{
+	(n, argl) := sys->tokenize(args, " ");
+	if(n != 1)
+		return (nil, "usage: " + Name + " file");
+	b := bufio->open(fullname(parent, hd argl), Sys->OREAD);
+	if(b == nil)
+		return (nil, sys->sprint("can't open %s, the error was: %r", hd argl));
+	mm : list of (ref Mstring, ref Mstring) = nil;
+	for(;;) {
+		s := b.gets('\n');
+		if(s == "")
+			break;
+		(nf, fl) := sys->tokenize(s, "	");
+		if(nf == 0)
+			mm = (nil, nil) :: mm;
+		else {
+			sleft := "";
+			sright := "";
+			if(nf == 1) {
+				f := hd fl;
+				if(s[0] == '\t')
+					sright = f;
+				else
+					sleft = f;
+			}
+			else {
+				sleft = hd fl;
+				sright = hd tl fl;
+			}
+			mm = (tom(sleft, Type, Roman, 1), tom(sright, Italic, Type, 0)) :: mm;
+		}
+	}
+	ans : list of (ref Mstring, ref Mstring) = nil;
+	while(mm != nil) {
+		ans = hd mm :: ans;
+		mm = tl mm;
+	}
+	return (ans, "");
+}
+
+tom(str: string, defstyle, altstyle, doindex: int) : ref Mstring
+{
+	if(str == "")
+		return nil;
+	if(str[len str - 1] == '\n')
+		str = str[0: len str - 1];
+	if(str == "")
+		return nil;
+	style := defstyle;
+	if(str[0] == '|')
+		style = altstyle;
+	(nil, l) := sys->tokenize(str, "|");
+	dummy := ref Mstring;
+	last := dummy;
+	if(doindex && l != nil && S->prefix("  ", hd l))
+		doindex = 0;	# continuation line
+	while(l != nil) {
+		s := hd l;
+		m : ref Mstring;
+		if(doindex && style == defstyle) {
+			# index 'words' in defstyle, but not past : or (
+			(sl,sr) := S->splitl(s, ":(");
+			while(sl != nil) {
+				a : string;
+				(a,sl) = S->splitl(sl, "a-zA-Z");
+				if(a != "") {
+					m = ref Mstring(a, style, 0, 0, nil);
+					last.next = m;
+					last = m;
+				}
+				if(sl != "") {
+					b : string;
+					(b,sl) = S->splitl(sl, "^a-zA-Z0-9_");
+					if(b != "") {
+						m = ref Mstring(b, style, 1, 0, nil);
+						last.next = m;
+						last = m;
+					}
+				}
+			}
+			if(sr != "") {
+				m = ref Mstring(sr, style, 0, 0, nil);
+				last.next = m;
+				last = m;
+				doindex = 0;
+			}
+		}
+		else {
+			m = ref Mstring(s, style, 0, 0, nil);
+			last.next = m;
+			last = m;
+		}
+		l = tl l;
+		if(style == defstyle)
+			style = altstyle;
+		else
+			style = defstyle;
+	}
+	return dummy.next;
+}
+
+measure(spec: list of (ref Mstring, ref Mstring), pixels: int) : (int, ref Mstring,  int, ref Mstring)
+{
+	maxl := 0;
+	maxr := 0;
+	maxlm : ref Mstring = nil;
+	maxrm : ref Mstring = nil;
+	while(spec != nil) {
+		(lm, rm) := hd spec;
+		spec = tl spec;
+		(maxl, maxlm) = measuremax(lm, maxl, maxlm, pixels);
+		(maxr, maxrm) = measuremax(rm, maxr, maxrm, pixels);
+	}
+	return (maxl, maxlm, maxr, maxrm);
+}
+
+measuremax(m: ref Mstring, maxw: int, maxm: ref Mstring, pixels: int) : (int, ref Mstring)
+{
+	w := 0;
+	for(mm := m; mm != nil; mm = mm.next) {
+		if(pixels)
+			mm.width = fontref[mm.style].width(mm.s);
+		else
+			mm.width = len mm.s;
+		w += mm.width;
+	}
+	if(w > maxw) {
+		maxw = w;
+		maxm = m;
+	}
+	return (maxw, maxm);
+}
+
+cook(parent: string, nil: int, args: string): (ref Celem, string)
+{
+	(spec, err) := getspec(parent, args);
+	if(err != nil)
+		return (nil, err);
+	(nil, maxlm, nil, nil) := measure(spec, 0);
+	ans := fontce(Roman);
+	tail := specialce("\\begin{tabbing}\\hspace{3in}\\=\\kill\n");
+	tail = add(ans, nil, tail);
+	for(l := spec; l != nil; l = tl l) {
+		(lm, rm) := hd l;
+		tail = cookmstring(ans, tail, lm, 1);
+		tail = add(ans, tail, specialce("\\>"));
+		tail = cookmstring(ans, tail, rm, 0);
+		tail = add(ans, tail, specialce("\\\\\n"));
+	}
+	add(ans, tail, specialce("\\end{tabbing}"));
+	return (ans, "");
+}
+
+cookmstring(par, tail: ref Celem, m: ref Mstring, doindex: int) : ref Celem
+{
+	s := "";
+	if(m == nil)
+		return tail;
+	while(m != nil) {
+		e := fontce(m.style);
+		te := textce(m.s);
+		add(e, nil, te);
+		if(doindex && m.indexed) {
+			ie := ref Celem(Index, nil, nil, nil, nil, nil);
+			add(ie, nil, e);
+			e = ie;
+		}
+		tail = add(par, tail, e);
+		m = m.next;
+	}
+	return tail;
+}
+
+specialce(s: string) : ref Celem
+{
+	return ref Celem(Special, s, nil, nil, nil, nil);
+}
+
+textce(s: string) : ref Celem
+{
+	return ref Celem(Text, s, nil, nil, nil, nil);
+}
+
+fontce(sty: int) : ref Celem
+{
+	return ref Celem(sty*NSIZE+Size8, nil, nil, nil, nil, nil);
+}
+
+add(par, tail: ref Celem, e: ref Celem) : ref Celem
+{
+	if(tail == nil) {
+		par.contents = e;
+		e.parent = par;
+	}
+	else
+		tail.next = e;
+	e.prev = tail;
+	return e;
+}
+
+fullname(parent, file: string): string
+{
+	if(len parent==0 || (len file>0 && (file[0]=='/' || file[0]=='#')))
+		return file;
+
+	for(i:=len parent-1; i>=0; i--)
+		if(parent[i] == '/')
+			return parent[0:i+1] + file;
+	return file;
+}
--- /dev/null
+++ b/appl/wm/brutus/table.b
@@ -1,0 +1,1478 @@
+implement Brutusext;
+
+# <Extension table tablefile>
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Point, Font, Rect: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "bufio.m";
+
+include "string.m";
+	S: String;
+
+include "html.m";
+	html: HTML;
+	Lex, Attr, RBRA, Data, Ttable, Tcaption, Tcol, Ttr, Ttd: import html;
+
+include "brutus.m";
+	Size6, Size8, Size10, Size12, Size16, NSIZE,
+	Roman, Italic, Bold, Type, NFONT, NFONTTAG,
+	Example, List, Listelem, Heading, Nofill, Author, Title,
+	DefFont, DefSize, TitleFont, TitleSize, HeadingFont, HeadingSize: import Brutus;
+
+include "brutusext.m";
+
+Name: con "Table";
+
+# alignment types
+Anone, Aleft, Acenter, Aright, Ajustify, Atop, Amiddle, Abottom, Abaseline: con iota;
+
+# A cell has a number of Lines, each of which has a number of Items.
+# Each Item is a string in one font.
+Item: adt
+{
+	itemid: int;	# canvas text item id
+	s: string;
+	fontnum: int;	# (style*NumSizes + size)
+	pos: Point;	# nw corner of text item, relative to line origin
+	width: int;		# of s, in pixels,  when displayed in font
+	line: cyclic ref Line;   # containing line
+	prev: cyclic ref Item;
+	next: cyclic ref Item;
+};
+
+Line: adt
+{
+	items: cyclic ref Item;
+	pos: Point;	# nw corner of Line relative to containing cell;
+	height: int;
+	ascent: int;
+	width: int;
+	cell: cyclic ref Tablecell;  # containing cell
+	next: cyclic ref Line;
+};
+
+Align: adt
+{
+	halign: int;
+	valign: int;
+};
+
+Tablecell: adt
+{
+	cellid: int;
+	content: array of ref Lex;
+	lines: cyclic ref Line;
+	rowspan: int;
+	colspan: int;
+	nowrap: int;
+	align: Align;
+	width: int;
+	height: int;
+	ascent: int;
+	row: int;
+	col: int;
+	pos: Point;	# nw corner of cell, in canvas coords
+};
+
+Tablegcell: adt
+{
+	cell: ref Tablecell;
+	drawnhere: int;
+};
+
+Tablerow: adt
+{
+	cells: list of ref Tablecell;
+	height: int;
+	ascent: int;
+	align: Align;
+	pos: Point;
+	rule: int;			# width of rule below row, if > 0
+	ruleids: list of int;	# canvas ids of lines used to draw rule
+};
+
+Tablecol: adt
+{
+	width: int;
+	align: Align;
+	pos: Point;
+	rule: int;			# width of rule to right of col, if > 0
+	ruleids: list of int;	# canvas ids of lines used to draw rule
+};
+
+Table: adt
+{
+	nrow: int;
+	ncol: int;
+	ncell: int;
+	width: int;
+	height: int;
+	capcell: ref Tablecell;
+	border: int;
+	brectid: int;
+	cols: array of ref Tablecol;
+	rows: array of ref Tablerow;
+	cells: list of ref Tablecell;
+	grid: array of array of ref Tablegcell;
+	colw: array of int;
+	rowh: array of int;
+};
+
+# Font stuff
+
+DefaultFnum: con (DefFont*NSIZE + Size10);
+
+fontnames := array[NFONTTAG] of {
+	"/fonts/lucidasans/unicode.6.font",
+	"/fonts/lucidasans/unicode.7.font",
+	"/fonts/lucidasans/unicode.8.font",
+	"/fonts/lucidasans/unicode.10.font",
+	"/fonts/lucidasans/unicode.13.font",
+	"/fonts/lucidasans/italiclatin1.6.font",
+	"/fonts/lucidasans/italiclatin1.7.font",
+	"/fonts/lucidasans/italiclatin1.8.font",
+	"/fonts/lucidasans/italiclatin1.10.font",
+	"/fonts/lucidasans/italiclatin1.13.font",
+	"/fonts/lucidasans/boldlatin1.6.font",
+	"/fonts/lucidasans/boldlatin1.7.font",
+	"/fonts/lucidasans/boldlatin1.8.font",
+	"/fonts/lucidasans/boldlatin1.10.font",
+	"/fonts/lucidasans/boldlatin1.13.font",
+	"/fonts/lucidasans/typelatin1.6.font",
+	"/fonts/lucidasans/typelatin1.7.font",
+	"/fonts/pelm/latin1.9.font",
+	"/fonts/pelm/ascii.12.font",
+	"/fonts/pelm/ascii.16.font"
+};
+
+fontrefs := array[NFONTTAG] of ref Font;
+fontused := array[NFONTTAG] of { DefaultFnum => 1, * => 0};
+
+# TABHPAD, TABVPAD are extra space between columns, rows
+TABHPAD: con 10;
+TABVPAD: con 4;
+
+tab: ref Table;
+top: ref Tk->Toplevel;
+display: ref Draw->Display;
+canv: string;
+
+init(asys: Sys, adraw: Draw, nil: Bufio, atk: Tk, aw: Tkclient)
+{
+	sys = asys;
+	draw = adraw;
+	tk = atk;
+	tkclient = aw;
+	html = load HTML HTML->PATH;
+	S = load String String->PATH;
+}
+
+create(parent: string, t: ref Tk->Toplevel, name, args: string): string
+{
+	if(html == nil)
+		return "can't load HTML module";
+	top = t;
+	display = t.image.display;
+	canv = name;
+	err := tk->cmd(t, "canvas " + canv);
+	if(len err > 0 && err[0] == '!')
+		return err_ret(err);
+
+	spec: array of ref Lex;
+	(spec, err) = getspec(parent, args);
+	if(err != "")
+		return err_ret(err);
+
+	err = parsetab(spec);
+	if(err != "")
+		return err_ret(err);
+
+	err = build();
+	if(err != "")
+		return err_ret(err);
+	return "";
+}
+
+err_ret(s: string) : string
+{
+	return Name + ": " + s;
+}
+
+getspec(parent, args: string) : (array of ref Lex, string)
+{
+	(n, argl) := sys->tokenize(args, " ");
+	if(n != 1)
+		return (nil, "usage: " + Name + " file");
+	(filebytes, err) := readfile(fullname(parent, hd argl));
+	if(err != "")
+		return (nil, err);
+	return(html->lex(filebytes, HTML->UTF8, 1), "");
+}
+
+readfile(path: string): (array of byte, string)
+{
+	fd := sys->open(path, sys->OREAD);
+	if(fd == nil)
+		return (nil, sys->sprint("can't open %s, the error was: %r", path));
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0)
+		return (nil, sys->sprint("can't stat %s, the error was: %r", path));
+	if(d.mode & Sys->DMDIR)
+		return (nil, sys->sprint("%s is a directory", path));
+
+	l := int d.length;
+	buf := array[l] of byte;
+	tot := 0;
+	while(tot < l) {
+		need := l - tot;
+		n := sys->read(fd, buf[tot:], need);
+		if(n <= 0)
+			return (nil, sys->sprint("error reading %s, the error was: %r", path));
+		tot += n;
+	}
+	return (buf, "");
+}
+
+# Use HTML 3.2 table spec as external representation
+# (But no th cells, width specs; and extra "rule" attribute
+# for col and tr meaning that a rule of given width is to
+# follow the given column or row).
+# DTD elements:
+#	table: - O (caption?, col*, tr*)
+#	caption: - - (%text+)
+#	col: - O empty
+#	tr: - O td*
+#	td: - O (%body.content)
+parsetab(toks: array of ref Lex) : string
+{
+	tabletlex := toks[0];
+	n := len toks;
+	(tlex, i) := nexttok(toks, n, 0);
+
+	# caption
+	capcell: ref Tablecell = nil;
+	if(tlex != nil && tlex.tag == Tcaption) {
+		for(j := i+1; j < n; j++) {
+			tlex = toks[j];
+			if(tlex.tag == Tcaption + RBRA)
+				break;
+		}
+		if(j >= n)
+			return syntax_err(tlex, j);
+		if(j > i+1) {
+			captoks := toks[i+1:j];
+			(caplines, e) := lexes2lines(captoks);
+			if(e != nil)
+				return e;
+			# we ignore caption now
+#			capcell = ref Tablecell(0, captoks, caplines, 1, 1, 1, Align(Anone, Anone),
+#						0, 0, 0, 0, 0, Point(0,0));
+		}
+		(tlex, i) = nexttok(toks, n, j);
+	}
+
+	# col*
+	cols: list of ref Tablecol = nil;
+	while(tlex != nil && tlex.tag == Tcol) {
+		col := makecol(tlex);
+		if(col.align.halign == Anone)
+			col.align.halign = Aleft;
+		cols = col :: cols;
+		(tlex, i) = nexttok(toks, n, i);
+	}
+	cols = revcols(cols);
+
+	body : list of ref Tablerow = nil;
+	cells : list of ref Tablecell = nil;
+	cellid := 0;
+	rows: list of ref Tablerow = nil;
+
+	# tr*
+	while(tlex != nil && tlex.tag == Ttr) {
+		currow := ref Tablerow(nil, 0, 0, makealign(tlex), Point(0,0), makelinew(tlex, "rule"), nil);
+		rows = currow :: rows;
+
+		# td*
+		(tlex, i) = nexttok(toks, n, i);
+		while(tlex != nil && tlex.tag == Ttd) {
+			rowspan := 1;
+			(rsfnd, rs) := html->attrvalue(tlex.attr, "rowspan");
+			if(rsfnd && rs != "")
+				rowspan = int rs;
+			colspan := 1;
+			(csfnd, cs) := html->attrvalue(tlex.attr, "colspan");
+			if(csfnd && cs != "")
+				colspan = int cs;
+			nowrap := 0;
+			(nwfnd, nil) := html->attrvalue(tlex.attr, "nowrap");
+			if(nwfnd)
+				nowrap = 1;
+			align := makealign(tlex);
+			for(j := i+1; j < n; j++) {
+				tlex = toks[j];
+				tg := tlex.tag;
+				if(tg == Ttd + RBRA || tg == Ttd || tg == Ttr + RBRA || tg == Ttr)
+					break;
+			}
+			if(j == n)
+				tlex = nil;
+			content: array of ref Lex = nil;
+			if(j > i+1)
+				content = toks[i+1:j];
+			(lines, err) := lexes2lines(content);
+			if(err != "")
+				return err;
+			curcell := ref Tablecell(cellid, content, lines, rowspan, colspan, nowrap, align, 0, 0, 0, 0, 0, Point(0,0));
+			currow.cells = curcell :: currow.cells;
+			cells = curcell :: cells;
+			cellid++;
+			if(tlex != nil && tlex.tag == Ttd + RBRA)
+				(tlex, i) = nexttok(toks, n, j);
+			else
+				i = j;
+		}
+		if(tlex != nil && tlex.tag == Ttr + RBRA)
+			(tlex, i) = nexttok(toks, n, i);
+	}
+	if(tlex == nil || tlex.tag != Ttable + RBRA)
+		return syntax_err(tlex, i);
+
+	# now reverse all the lists that were built in reverse order
+	# and calculate nrow, ncol
+
+	rows = revrowl(rows);
+	nrow := len rows;
+	rowa := array[nrow] of ref Tablerow;
+	ncol := 0;
+	r := 0;
+	for(rl := rows; rl != nil; rl = tl rl) {
+		row := hd rl;
+		rowa[r++] = row;
+		rcols := 0;
+		cl := row.cells;
+		row.cells = nil;
+		while(cl != nil) {
+			c := hd cl;
+			row.cells = c :: row.cells;
+			rcols += c.colspan;
+			cl = tl cl;
+		}
+		if(rcols > ncol)
+			ncol = rcols;
+	}
+	cells = revcelll(cells);
+
+	cola := array[ncol] of ref Tablecol;
+	for(c := 0; c < ncol; c++) {
+		if(cols != nil) {
+			cola[c] = hd cols;
+			cols = tl cols;
+		}
+		else
+			cola[c] = ref Tablecol(0, Align(Anone, Anone), Point(0,0), 0, nil);
+	}
+
+	if(tabletlex.tag != Ttable)
+		return syntax_err(tabletlex, 0);
+	border := makelinew(tabletlex, "border");
+	tab = ref Table(nrow, ncol, cellid, 0, 0, capcell, border, 0, cola, rowa, cells, nil, nil, nil);
+
+	return "";
+}
+
+syntax_err(tlex: ref Lex, i: int) : string
+{
+	if(tlex == nil)
+		return "syntax error in table: premature end";
+	else
+		return "syntax error in table at token " + string i + ": " + html->lex2string(tlex);
+}
+
+# next token after toks[i], skipping whitespace
+nexttok(toks: array of ref Lex, ntoks, i: int) : (ref Lex, int)
+{
+	i++;
+	if(i >= ntoks)
+		return (nil, i);
+	t := toks[i];
+	while(t.tag == Data) {
+		if(S->drop(t.text, " \t\n\r") != "")
+			break;
+		i++;
+		if(i >= ntoks)
+			return (nil, i);
+		t = toks[i];
+	}
+# sys->print("nexttok returning (%s,%d)\n", html->lex2string(t), i);
+	return(t, i);
+}
+
+makecol(tlex: ref Lex) : ref Tablecol
+{
+	return ref Tablecol(0, makealign(tlex), Point(0,0), makelinew(tlex, "rule"), nil);
+}
+
+makelinew(tlex: ref Lex, aname: string) : int
+{
+	ans := 0;
+	(fnd, val) := html->attrvalue(tlex.attr, aname);
+	if(fnd) {
+		if(val == "")
+			ans = 1;
+		else
+			ans = int val;
+	}
+	return ans;
+}
+
+makealign(tlex: ref Lex) : Align
+{
+	(nil,h) := html->attrvalue(tlex.attr, "align");
+	(nil,v) := html->attrvalue(tlex.attr, "valign");
+	hal := align_val(h, Anone);
+	val := align_val(v, Anone);
+	return Align(hal, val);
+}
+
+align_val(sal: string, dflt: int) : int
+{
+	ans := dflt;
+	case sal {
+		"left" => ans = Aleft;
+		"center" => ans = Acenter;
+		"right" => ans = Aright;
+		"justify" => ans = Ajustify;
+		"top" => ans = Atop;
+		"middle" => ans = Amiddle;
+		"bottom" => ans = Abottom;
+		"baseline" => ans = Abaseline;
+	}
+	return ans;
+}
+
+revcols(l : list of ref Tablecol) : list of ref Tablecol
+{
+	ans : list of ref Tablecol = nil;
+	while(l != nil) {
+		ans = hd l :: ans;
+		l = tl l;
+	}
+	return ans;
+}
+
+revrowl(l : list of ref Tablerow) : list of ref Tablerow
+{
+	ans : list of ref Tablerow = nil;
+	while(l != nil) {
+		ans = hd l :: ans;
+		l = tl l;
+	}
+	return ans;
+}
+
+revcelll(l : list of ref Tablecell) : list of ref Tablecell
+{
+	ans : list of ref Tablecell = nil;
+	while(l != nil) {
+		ans = hd l :: ans;
+		l = tl l;
+	}
+	return ans;
+}
+
+revintl(l : list of int) : list of int
+{
+	ans : list of int = nil;
+	while(l != nil) {
+		ans = hd l :: ans;
+		l = tl l;
+	}
+	return ans;
+}
+
+# toks should contain only Font (i.e., size) and style changes, along with text.
+lexes2lines(toks: array of ref Lex) : (ref Line, string)
+{
+	n := len toks;
+	(tlex, i) := nexttok(toks, n, -1);
+	ans: ref Line = nil;
+	if(tlex == nil)
+		return(ans, "");
+	curline : ref Line = nil;
+	curitem : ref Item = nil;
+	stylestk := DefFont :: nil;
+	sizestk := DefSize :: nil;
+	f := DefaultFnum;
+	fontstk:= f :: nil;
+	for(;;) {
+		if(i >= n)
+			break;
+		tlex = toks[i++];
+		case tlex.tag {
+		Data =>
+			text := tlex.text;
+			while(text != "") {
+				if(curline == nil) {
+					curline = ref Line(nil, Point(0,0), 0, 0, 0, nil, nil);
+					ans = curline;
+				}
+				s : string;
+				(s, text) = S->splitl(text, "\n");
+				if(s != "") {
+					f = hd fontstk;
+					it := ref Item(0, s, f, Point(0,0), 0, curline, curitem, nil);
+					if(curitem == nil)
+						curline.items = it;
+					else
+						curitem.next = it;
+					curitem = it;
+				}
+				if(text != "") {
+					text = text[1:];
+					curline.next = ref Line(nil, Point(0,0), 0, 0, 0, nil, nil);
+					curline = curline.next;
+					curitem = nil;
+				}
+			}
+		HTML->Tfont =>
+			(fnd, ssize) := html->attrvalue(tlex.attr, "size");
+			if(fnd && len ssize > 0) {
+				# HTML size 3 == our Size10
+				sz := (int ssize) + (Size10 - 3);
+				if(sz < 0 || sz >= NSIZE)
+					return (nil, "bad font size " + ssize);
+				sizestk = sz :: sizestk;
+				fontstk = fnum(hd stylestk, sz) :: fontstk;
+			}
+			else
+				return (nil, "bad font command: no size");
+		HTML->Tfont + RBRA =>
+			fontstk = tl fontstk;
+			sizestk = tl sizestk;
+			if(sizestk == nil)
+				return (nil, "unmatched </FONT>");
+		HTML->Tb =>
+			stylestk = Bold :: stylestk;
+			fontstk = fnum(Bold, hd sizestk) :: fontstk;
+		HTML->Ti =>
+			stylestk = Italic :: stylestk;
+			fontstk = fnum(Italic, hd sizestk) :: fontstk;
+		HTML->Ttt =>
+			stylestk = Type :: stylestk;
+			fontstk = fnum(Type, hd sizestk) :: fontstk;
+		HTML->Tb + RBRA or HTML->Ti + RBRA or HTML->Ttt + RBRA =>
+			fontstk = tl fontstk;
+			stylestk = tl stylestk;
+			if(stylestk == nil)
+				return (nil, "unmatched </B>, </I>, or </TT>");
+		}
+	}
+	return (ans, "");
+}
+
+fnum(fstyle, fsize: int) : int
+{
+	ans := fstyle*NSIZE + fsize;
+	fontused[ans] = 1;
+	return ans;
+}
+
+loadfonts() : string
+{
+	for(i := 0; i < NFONTTAG; i++) {
+		if(fontused[i] && fontrefs[i] == nil) {
+			fname := fontnames[i];
+			f := Font.open(display, fname);
+			if(f == nil)
+				return sys->sprint("can't open font %s: %r", fname);
+			fontrefs[i] = f;
+		}
+	}
+	return "";
+}
+
+# Find where each cell goes in nrow x ncol grid
+setgrid()
+{
+	gcells := array[tab.nrow] of { * => array[tab.ncol] of { * => ref Tablegcell(nil, 1)} };
+
+	# The following arrays keep track of cells that are spanning
+	# multiple rows;  rowspancnt[i] is the number of rows left
+	# to be spanned in column i.
+	# When done, cell's (row,col) is upper left grid point.
+	rowspancnt := array[tab.ncol] of { * => 0};
+	rowspancell := array[tab.ncol] of ref Tablecell;
+
+	ri := 0;
+	ci := 0;
+	for(ri = 0; ri < tab.nrow; ri++) {
+		row := tab.rows[ri];
+		cl := row.cells;
+		for(ci = 0; ci < tab.ncol; ) {
+			if(rowspancnt[ci] > 0) {
+				gcells[ri][ci].cell = rowspancell[ci];
+				gcells[ri][ci].drawnhere = 0;
+				rowspancnt[ci]--;
+				ci++;
+			}
+			else {
+				if(cl == nil) {
+					ci++;
+					continue;
+				}
+				c := hd cl;
+				cl = tl cl;
+				cspan := c.colspan;
+				if(cspan == 0) {
+					cspan = tab.ncol - ci;
+					c.colspan = cspan;
+				}
+				rspan := c.rowspan;
+				if(rspan == 0) {
+					rspan = tab.nrow - ri;
+					c.rowspan = rspan;
+				}
+				c.row = ri;
+				c.col = ci;
+				for(i := 0; i < cspan && ci < tab.ncol; i++) {
+					gcells[ri][ci].cell = c;
+					if(i > 0)
+						gcells[ri][ci].drawnhere = 0;
+					if(rspan > 1) {
+						rowspancnt[ci] = rspan-1;
+						rowspancell[ci] = c;
+					}
+					ci++;
+				}
+			}
+		}
+	}
+	tab.grid = gcells;
+}
+
+build() : string
+{
+	ri, ci: int;
+
+#	sys->print("\n\ninitial table\n"); printtable();
+	if(tab.ncol == 0 || tab.nrow == 0)
+		return "";
+
+	setgrid();
+
+	err := loadfonts();
+	if(err != "")
+		return err;
+
+	for(cl := tab.cells; cl != nil; cl = tl cl)
+		cell_geom(hd cl);
+
+	for(ci = 0; ci < tab.ncol; ci++)
+		col_geom(ci);
+
+	for(ri = 0; ri < tab.nrow; ri++)
+		row_geom(ri);
+
+	caption_geom();
+
+	table_geom();
+#	sys->print("\n\ntable after geometry set\n"); printtable();
+
+	h := tab.height;
+	w := tab.width;
+	if(tab.capcell != nil) {
+		h += tab.capcell.height;
+		if(tab.capcell.width > w)
+			w = tab.capcell.width;
+	}
+
+	err = tk->cmd(top, canv + " configure -width " + string w
+		+ " -height " + string h);
+	if(len err > 0 && err[0] == '!')
+		return err;
+	err = create_cells();
+	if(err != "")
+		return err;
+	err = create_border();
+	if(err != "")
+		return err;
+	err = create_rules();
+	if(err != "")
+		return err;
+	err = create_caption();
+	if(err != "")
+		return err;
+	tk->cmd(top, "update");
+
+	return "";
+}
+
+create_cells() : string
+{
+	for(cl := tab.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		cpos := c.pos;
+		for(l := c.lines; l != nil; l = l.next) {
+			lpos := l.pos;
+			for(it := l.items; it != nil; it = it.next) {
+				ipos := it.pos;
+				pos := ipos.add(lpos.add(cpos));
+				fnt := fontrefs[it.fontnum];
+				v := tk->cmd(top, canv + " create text " + string pos.x + " "
+					+ string pos.y + " -anchor nw -font " + fnt.name
+					+ " -text '" + it.s);
+				if(len v > 0 && v[0] == '!')
+					return v;
+				it.itemid = int v;
+			}
+		}
+	}
+	return "";
+}
+
+create_border() : string
+{
+	bd := tab.border;
+	if(bd > 0) {
+		x1 := string (bd / 2);
+		y1 := x1;
+		x2 := string (tab.width - bd/2 -1);
+		y2 := string (tab.height - bd/2 -1);
+		v := tk->cmd(top, canv + " create rectangle "
+			+ x1 + " " + y1 + " " + x2 + " " + y2 + " -width " + string bd);
+		if(len v > 0 && v[0] == '!')
+			return v;
+		tab.brectid = int v;
+	}
+	return "";
+}
+
+create_rules() : string
+{
+	ci, ri, i: int;
+	err : string;
+	c : ref Tablecell;
+	for(ci = 0; ci < tab.ncol; ci++) {
+		col := tab.cols[ci];
+		rw := col.rule;
+		if(rw > 0) {
+			x := col.pos.x + col.width + TABHPAD/2 - rw/2;
+			ids: list of int = nil;
+			startri := 0;
+			for(ri = 0; ri < tab.nrow; ri++) {
+				c = tab.grid[ri][ci].cell;
+				if(c.col+c.colspan-1 > ci) {
+					# rule would cross a spanning cell at this column
+					if(ri > startri) {
+						(err, i) = create_col_rule(startri, ri-1, x, rw);
+						if(err != "")
+							return err;
+						ids = i :: ids;
+					}
+					startri = ri+1;
+				}
+			}
+			if(ri > startri)
+				(err, i) = create_col_rule(startri, ri-1, x, rw);
+			ids = i :: ids;
+			col.ruleids = revintl(ids);
+		}
+	}
+	for(ri = 0; ri < tab.nrow; ri++) {
+		row := tab.rows[ri];
+		rw := row.rule;
+		if(rw > 0) {
+			y := row.pos.y + row.height + TABVPAD/2 - rw/2;
+			ids: list of int = nil;
+			startci := 0;
+			for(ci = 0; ci < tab.ncol; ci++) {
+				c = tab.grid[ri][ci].cell;
+				if(c.row+c.rowspan-1 > ri) {
+					# rule would cross a spanning cell at this row
+					if(ci > startci) {
+						(err, i) = create_row_rule(startci, ci-1, y, rw);
+						if(err != "")
+							return err;
+						ids = i :: ids;
+					}
+					startci = ci+1;
+				}
+			}
+			if(ci > startci)
+				(err, i) = create_row_rule(startci, ci-1, y, rw);
+			ids = i :: ids;
+			row.ruleids = revintl(ids);
+		}
+	}
+	return "";
+}
+
+create_col_rule(topri, botri, x, rw: int) : (string, int)
+{
+	y1, y2: int;
+	if(topri == 0)
+		y1 = 0;
+	else
+		y1 = tab.rows[topri].pos.y - TABVPAD/2;
+	if(botri == tab.nrow-1)
+		y2 = tab.height;
+	else
+		y2 = tab.rows[botri].pos.y + tab.rows[botri].height + TABVPAD/2;
+	sx := string x;
+	v := tk->cmd(top, canv + " create line " + sx + " "
+		+ string y1 + " " + sx + " " + string y2 + " -width " + string rw);
+	if(len v > 0 && v[0] == '!')
+		return (v, 0);
+	return ("", int v);
+}
+
+create_row_rule(leftci, rightci, y, rw: int) : (string, int)
+{
+	x1, x2: int;
+	if(leftci == 0)
+		x1 = 0;
+	else
+		x1 = tab.cols[leftci].pos.x - TABHPAD/2;
+	if(rightci == tab.ncol-1)
+		x2 = tab.width;
+	else
+		x2 = tab.cols[rightci].pos.x + tab.cols[rightci].width + TABHPAD/2;
+	sy := string y;
+	v := tk->cmd(top, canv + " create line " + string x1 + " "
+		+ sy + " " + string x2 + " " + sy + " -width " + string rw);
+	if(len v > 0 && v[0] == '!')
+		return (v, 0);
+	return ("", int v);
+}
+
+create_caption() : string
+{
+	if(tab.capcell == nil)
+		return "";
+	cpos := Point(0, tab.height + 2*TABVPAD);
+	for(l := tab.capcell.lines; l != nil; l = l.next) {
+		lpos := l.pos;
+		for(it := l.items; it != nil; it = it.next) {
+			ipos := it.pos;
+			pos := ipos.add(lpos.add(cpos));
+			fnt := fontrefs[it.fontnum];
+			v := tk->cmd(top, canv + " create text " + string pos.x + " "
+				+ string pos.y + " -anchor nw -font " + fnt.name
+				+ " -text '" + it.s);
+			if(len v > 0 && v[0] == '!')
+				return v;
+			it.itemid = int v;
+		}
+	}
+	return "";
+}
+
+# Assuming row and col geoms correct, set row, col, and cell origins
+table_geom()
+{
+	row: ref Tablerow;
+	col: ref Tablecol;
+	orig := Point(0,0);
+	bd := tab.border;
+	if(bd > 0)
+		orig = orig.add(Point(TABHPAD+bd, TABVPAD+bd));
+	o := orig;
+	for(ci := 0; ci < tab.ncol; ci++) {
+		col = tab.cols[ci];
+		col.pos = o;
+		o.x += col.width + col.rule;
+		if(ci < tab.ncol-1)
+			o.x += TABHPAD;
+	}
+	if(bd > 0)
+		o.x += TABHPAD + bd;
+	tab.width = o.x;
+
+	o = orig;
+	for(ri := 0; ri < tab.nrow; ri++) {
+		row = tab.rows[ri];
+		row.pos = o;
+		o.y += row.height + row.rule;
+		if(ri < tab.nrow-1)
+			o.y += TABVPAD;
+	}
+	if(bd > 0)
+		o.y += TABVPAD + bd;
+	tab.height = o.y;
+
+	if(tab.capcell != nil) {
+		tabw := tab.width;
+		if(tab.capcell.width > tabw)
+			tabw = tab.capcell.width;
+		for(l := tab.capcell.lines; l != nil; l = l.next)
+			l.pos.x += (tabw - l.width)/2;
+	}
+
+	for(cl := tab.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		row = tab.rows[c.row];
+		col = tab.cols[c.col];
+		x := col.pos.x;
+		y := row.pos.y;
+		w := spanned_col_width(c.col, c.col+c.colspan-1);
+		case (cellhalign(c)) {
+		Aright =>
+			x += w - c.width;
+		Acenter =>
+			x += (w - c.width) / 2;
+		}
+		h := spanned_row_height(c.row, c.row+c.rowspan-1);
+		case (cellvalign(c)) {
+		Abottom =>
+			y += h - c.height;
+		Anone or Amiddle =>
+			y += (h - c.height) / 2;
+		Abaseline =>
+			y += row.ascent - c.ascent;
+		}
+		c.pos = Point(x,y);
+	}
+}
+
+spanned_col_width(firstci, lastci: int) : int
+{
+	firstcol := tab.cols[firstci];
+	if(firstci == lastci)
+		return firstcol.width;
+	lastcol := tab.cols[lastci];
+	return (lastcol.pos.x + lastcol.width - firstcol.pos.x);
+}
+
+spanned_row_height(firstri, lastri: int) : int
+{
+	firstrow := tab.rows[firstri];
+	if(firstri == lastri)
+		return firstrow.height;
+	lastrow := tab.rows[lastri];
+	return (lastrow.pos.y + lastrow.height - firstrow.pos.y);
+}
+
+# Assuming cell geoms are correct, set col widths.
+# This code is sloppy for spanned columns;
+# it will allocate too much space for them because
+# inter-column pad is ignored, and it may make
+# narrow columns wider than they have to be.
+col_geom(ci: int)
+{
+	col := tab.cols[ci];
+	col.width = 0;
+	for(ri := 0; ri < tab.nrow; ri++) {
+		c := tab.grid[ri][ci].cell;
+		if(c == nil)
+			continue;
+		cwd := c.width / c.colspan;
+		if(cwd > col.width)
+			col.width = cwd;
+	}
+}
+
+# Assuming cell geoms are correct, set row heights
+row_geom(ri: int)
+{
+	row := tab.rows[ri];
+	# find rows's global height and ascent
+	h := 0;
+	a := 0;
+	n : int;
+	for(cl := row.cells; cl != nil; cl = tl cl) {
+		c := hd cl;
+		al := cellvalign(c);
+		if(al == Abaseline) {
+			n = c.ascent;
+			if(n > a) {
+				h += (n - a);
+				a = n;
+			}
+			n = c.height - c.ascent;
+			if(n > h-a)
+				h = a + n;
+		}
+		else {
+			n = c.height;
+			if(n > h)
+				h = n;
+		}
+	}
+	row.height = h;
+	row.ascent = a;
+}
+
+cell_geom(c: ref Tablecell)
+{
+	width := 0;
+	o := Point(0,0);
+	for(l := c.lines; l != nil; l = l.next) {
+		line_geom(l, o);
+		o.y += l.height;
+		if(l.width > width)
+			width = l.width;
+	}
+	c.width = width;
+	c.height = o.y;
+	if(c.lines != nil)
+		c.ascent = c.lines.ascent;
+	else
+		c.ascent = 0;
+
+	al := cellhalign(c);
+	if(al == Acenter || al == Aright) {
+		for(l = c.lines; l != nil; l = l.next) {
+			xdelta := c.width - l.width;
+			if(al == Acenter)
+				xdelta /= 2;
+			l.pos.x += xdelta;
+		}
+	}
+}
+
+caption_geom()
+{
+	if(tab.capcell != nil) {
+		o := Point(0,TABVPAD);
+		width := 0;
+		for(l := tab.capcell.lines; l != nil; l = l.next) {
+			line_geom(l, o);
+			o.y += l.height;
+			if(l.width > width)
+				width = l.width;
+		}
+		tab.capcell.width = width;
+		tab.capcell.height = o.y + 4*TABVPAD;
+	}
+}
+
+line_geom(l: ref Line, o: Point)
+{
+	# find line's global height and ascent
+	h := 0;
+	a := 0;
+	for(it := l.items; it != nil; it = it.next) {
+		fnt := fontrefs[it.fontnum];
+		n := fnt.ascent;
+		if(n > a) {
+			h += (n - a);
+			a = n;
+		}
+		n = fnt.height - fnt.ascent;
+		if(n > h-a)
+			h = a + n;
+	}
+	l.height = h;
+	l.ascent = a;
+	# set positions
+	l.pos = o;
+	for(it = l.items; it != nil; it = it.next) {
+		fnt := fontrefs[it.fontnum];
+		it.width = fnt.width(it.s);
+		it.pos.x = o.x;
+		o.x += it.width;
+		it.pos.y = a - fnt.ascent;
+	}
+	l.width = o.x;
+}
+
+cellhalign(c: ref Tablecell) : int
+{
+	a := c.align.halign;
+	if(a == Anone)
+		a = tab.cols[c.col].align.halign;
+	return a;
+}
+
+cellvalign(c: ref Tablecell) : int
+{
+	a := c.align.valign;
+	if(a == Anone)
+		a = tab.rows[c.row].align.valign;
+	return a;
+}
+
+# table debugging
+printtable()
+{
+	if(tab == nil) {
+		sys->print("no table\n");
+		return;
+	}
+	sys->print("Table %d rows, %d cols width %d height %d\n",
+			tab.nrow, tab.ncol, tab.width, tab.height);
+	if(tab.capcell != nil)
+		sys->print("  caption: "); printlexes(tab.capcell.content, "    ");
+	sys->print("  cols:\n"); printcols(tab.cols);
+	sys->print("  rows:\n"); printrows(tab.rows);
+}
+
+align2string(al: int) : string
+{
+	s := "";
+	case al {
+		Anone => s = "none";
+		Aleft => s = "left";
+		Acenter => s = "center";
+		Aright => s = "right";
+		Ajustify => s = "justify";
+		Atop => s = "top";
+		Amiddle => s = "middle";
+		Abottom => s = "bottom";
+		Abaseline => s = "baseline";
+	}
+	return s;
+}
+
+printcols(cols: array of ref Tablecol)
+{
+	n := len cols;
+	for(i := 0 ; i < n; i++) {
+		c := cols[i];
+		sys->print(" width %d align = %s,%s pos (%d,%d) rule %d\n", c.width,
+			align2string(c.align.halign), align2string(c.align.valign), c.pos.x, c.pos.y, c.rule);
+	}
+}
+
+printrows(rows: array of ref Tablerow)
+{
+	n := len rows;
+	for(i := 0; i < n; i++) {
+		tr := rows[i];
+		sys->print("      row height %d ascent %d align=%s,%s pos (%d,%d) rule %d\n", tr.height, tr.ascent,
+			align2string(tr.align.halign), align2string(tr.align.valign), tr.pos.x, tr.pos.y, tr.rule);
+		for(cl := tr.cells; cl != nil; cl = tl cl) {
+			c := hd cl;
+			sys->print("        cell %d width %d height %d ascent %d align=%s,%s\n",
+				c.cellid, c.width, c.height, c.ascent,
+				align2string(c.align.halign), align2string(c.align.valign));
+			sys->print("             pos (%d,%d) rowspan=%d colspan=%d nowrap=%d\n",
+				c.pos.x, c.pos.y, c.rowspan, c.colspan, c.nowrap);
+			printlexes(c.content, "        ");
+			printlines(c.lines);
+		}
+	}
+}
+
+printlexes(lexes: array of ref Lex, indent: string)
+{
+	for(i := 0; i < len lexes; i++)
+		sys->print("%s%s\n", indent, html->lex2string(lexes[i]));
+}
+
+printlines(l: ref Line)
+{
+	if(l == nil)
+		return;
+	sys->print("lines: \n");
+	while(l != nil) {
+		sys->print("          Line: pos (%d,%d), height %d ascent %d\n", l.pos.x, l.pos.y, l.height, l.ascent);
+		printitems(l.items);
+		l = l.next;
+	}
+}
+
+printitems(i: ref Item)
+{
+	while(i != nil) {
+		sys->print("            '%s' id %d fontnum %d w %d, pos (%d,%d)\n", i.s, i.itemid, i.fontnum,
+			i.width, i.pos.x, i.pos.y);
+		i = i.next;
+	}
+}
+
+printgrid(g: array of array of ref Tablegcell)
+{
+	nr := len g;
+	nc := len g[0];
+	for(r := 0; r < nr; r++) {
+		for(c := 0; c < nc; c++) {
+			x := g[r][c];
+			cell := x.cell;
+			suf := " ";
+			if(x.drawnhere == 0)
+				suf = "*";
+			if(cell == nil)
+				sys->print("     %s", suf);
+			else
+				sys->print("%5d%s", cell.cellid, suf);
+		}
+		sys->print("\n");
+	}
+}
+
+# Return (table in correct format, error string)
+cook(parent: string, fmt: int, args: string) : (ref Celem, string)
+{
+	(spec, err) := getspec(parent, args);
+	if(err != "")
+		return (nil, err);
+	if(fmt == FHtml)
+		return cookhtml(spec);
+	else
+		return cooklatex(spec);
+}
+
+# Return (table as latex, error string)
+# BUG: cells spanning multiple rows not handled correctly
+# (all their contents go in the first row of span, though hrules properly broken)
+cooklatex(spec: array of ref Lex) : (ref Celem, string)
+{
+	s : string;
+	ci, ri: int;
+	err := parsetab(spec);
+	if(err != "")
+		return (nil, err_ret(err));
+
+	setgrid();
+
+	ans := ref Celem(SGML, "", nil, nil, nil, nil);
+	cur : ref Celem = nil;
+	cur = add(ans, cur, specialce("\\begin{tabular}[t]{" + lcolspec() + "}\n"));
+	if(tab.border) {
+		if(tab.border == 1)
+			s = "\\hline\n";
+		else
+			s = "\\hline\\hline\n";
+		cur = add(ans, cur, specialce(s));
+	}
+	for(ri = 0; ri < tab.nrow; ri++) {
+		row := tab.rows[ri];
+		ci = 0;
+		anyrowspan := 0;
+		for(cl := row.cells; cl != nil; cl = tl cl) {
+			c := hd cl;
+			while(ci < c.col) {
+				cur = add(ans, cur, specialce("&"));
+				ci++;
+			}
+			mcol := 0;
+			if(c.colspan > 1) {
+				cur = add(ans, cur, specialce("\\multicolumn{" + string c.colspan + "}{" +
+						lnthcolspec(ci, ci+c.colspan-1, c.align.halign) + "}{"));
+				mcol = 1;
+			}
+			else if(c.align.halign != Anone) {
+				cur = add(ans, cur, specialce("\\multicolumn{1}{" +
+						lnthcolspec(ci, ci, c.align.halign) + "}{"));
+				mcol = 1;
+			}
+			if(c.rowspan > 1)
+				anyrowspan = 1;
+			cur = addlconvlines(ans, cur, c);
+			if(mcol) {
+				cur = add(ans, cur, specialce("}"));
+				ci += c.colspan-1;
+			}
+		}
+		while(ci++ < tab.ncol-1)
+			cur = add(ans, cur, specialce("&"));
+		if(ri < tab.nrow-1 || row.rule > 0 || tab.border > 0)
+			cur = add(ans, cur, specialce("\\\\\n"));
+		if(row.rule) {
+			if(anyrowspan) {
+				startci := 0;
+				for(ci = 0; ci < tab.ncol; ci++) {
+					c := tab.grid[ri][ci].cell;
+					if(c.row+c.rowspan-1 > ri) {
+						# rule would cross a spanning cell at this row
+						if(ci > startci)
+							cur = add(ans, cur, specialce("\\cline{" +
+								string (startci+1) + "-" + string ci + "}"));
+						startci = ci+1;
+					}
+				}
+				if(ci > startci)
+					cur = add(ans, cur, specialce("\\cline{" +
+						string (startci+1) + "-" + string ci + "}"));
+			}
+			else
+				cur = add(ans, cur, specialce("\\hline\n"));
+		}
+	}
+	if(tab.border) {
+		if(tab.border == 1)
+			s = "\\hline\n";
+		else
+			s = "\\hline\\hline\n";
+		cur = add(ans, cur, specialce(s));
+	}
+	cur = add(ans, cur, specialce("\\end{tabular}\n"));
+
+	if(ans != nil)
+		ans = ans.contents;
+	return (ans, "");
+}
+
+lcolspec() : string
+{
+	ans := "";
+	for(ci := 0; ci < tab.ncol; ci++)
+		ans += lnthcolspec(ci, ci, Anone);
+	return ans;
+}
+
+lnthcolspec(ci, cie, al: int) : string
+{
+	ans := "";
+	if(ci == 0) {
+		if(tab.border == 1)
+			ans = "|";
+		else if(tab.border > 1)
+			ans = "||";
+	}
+	col := tab.cols[ci];
+	if(al == Anone)
+		al = col.align.halign;
+	case al {
+	Acenter =>
+		ans += "c";
+	Aright =>
+		ans += "r";
+	* =>
+		ans += "l";
+	}
+	if(ci == cie) {
+		if(col.rule == 1)
+			ans += "|";
+		else if(col.rule > 1)
+			ans += "||";
+	}
+	if(cie == tab.ncol - 1) {
+		if(tab.border == 1)
+			ans += "|";
+		else if(tab.border > 1)
+			ans += "||";
+	}
+	return ans;
+}
+
+addlconvlines(par, tail: ref Celem, c: ref Tablecell) : ref Celem
+{
+	line := c.lines;
+	if(line == nil)
+		return tail;
+	multiline := 0;
+	if(line.next != nil) {
+		multiline = 1;
+		val := "";
+		case cellvalign(c) {
+		Abaseline or Atop => val = "[t]";
+		Abottom => val = "[b]";
+		}
+		hal := "l";
+		case cellhalign(c) {
+		Aright => hal = "r";
+		Acenter => hal = "c";
+		}
+		# The @{}'s in the colspec eliminate extra space before and after result
+		tail = add(par, tail, specialce("\\begin{tabular}" + val + "{@{}" + hal + "@{}}\n"));
+	}
+	while(line != nil) {
+		for(it := line.items; it != nil; it = it.next) {
+			fnum := it.fontnum;
+			f := fnum / NSIZE;
+			sz := fnum % NSIZE;
+			grouped := 0;
+			if((f != DefFont || sz != DefSize) && (it.prev!=nil || it.next!=nil)) {
+				tail = add(par, tail, specialce("{"));
+				grouped = 1;
+			}
+			if(f != DefFont) {
+				fcmd := "";
+				case f {
+				Roman => fcmd = "\\rmfamily ";
+				Italic => fcmd = "\\itshape ";
+				Bold => fcmd = "\\bfseries ";
+				Type => fcmd = "\\ttfamily ";
+				}
+				tail = add(par, tail, specialce(fcmd));
+			}
+			if(sz != DefSize) {
+				szcmd := "";
+				case sz {
+				Size6 => szcmd = "\\footnotesize ";
+				Size8 => szcmd = "\\small ";
+				Size10 => szcmd = "\\normalsize ";
+				Size12 => szcmd = "\\large ";
+				Size16 => szcmd = "\\Large ";
+				}
+				tail = add(par, tail, specialce(szcmd));
+			}
+			tail = add(par, tail, textce(it.s));
+			if(grouped)
+				tail = add(par, tail, specialce("}"));
+		}
+		ln := line.next;
+		if(multiline && ln != nil)
+			tail = add(par, tail, specialce("\\\\\n"));
+		line = line.next;
+	}
+	if(multiline)
+		tail = add(par, tail, specialce("\\end{tabular}\n"));
+	return tail;
+}
+
+# Return (table as html, error string)
+cookhtml(spec: array of ref Lex) : (ref Celem, string)
+{
+	n := len spec;
+	ans := ref Celem(SGML, "", nil, nil, nil, nil);
+	cur : ref Celem = nil;
+	for(i := 0; i < n; i++) {
+		tok := spec[i];
+		if(tok.tag == Data)
+			cur = add(ans, cur, textce(tok.text));
+		else {
+			s := html->lex2string(spec[i]);
+			cur = add(ans, cur, specialce(s));
+		}
+	}
+	if(ans != nil)
+		ans = ans.contents;
+	return (ans, "");
+}
+
+textce(s: string) : ref Celem
+{
+	return ref Celem(Text, s, nil, nil, nil, nil);
+}
+
+specialce(s: string) : ref Celem
+{
+	return ref Celem(Special, s, nil, nil, nil, nil);
+}
+
+add(par, tail: ref Celem, e: ref Celem) : ref Celem
+{
+	if(tail == nil) {
+		par.contents = e;
+		e.parent = par;
+	}
+	else
+		tail.next = e;
+	e.prev = tail;
+	return e;
+}
+
+fullname(parent, file: string): string
+{
+	if(len parent==0 || (len file>0 && (file[0]=='/' || file[0]=='#')))
+		return file;
+
+	for(i:=len parent-1; i>=0; i--)
+		if(parent[i] == '/')
+			return parent[0:i+1] + file;
+	return file;
+}
--- /dev/null
+++ b/appl/wm/c4.b
@@ -1,0 +1,718 @@
+implement Connect;
+
+#
+# Copyright © 2000 Vita Nuova Limited. All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Image, Font, Context, Screen, Display: import draw;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "daytime.m";
+	daytime: Daytime;
+include "rand.m";
+	rand: Rand;
+
+# adtize and modularize
+
+stderr: ref Sys->FD;
+
+Connect: module 
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+nosleep, printout, auto: int;
+display: ref Draw->Display;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	daytime = load Daytime Daytime->PATH;
+	rand = load Rand Rand->PATH;
+
+	argv = tl argv;
+	while(argv != nil){
+		s := hd argv;
+		if(s != nil && s[0] == '-'){
+			for(i := 1; i < len s; i++){
+				case s[i]{
+					'a' => auto = 1;
+					'p' => printout = 1;
+					's' => nosleep = 1;
+				}
+			}
+		}
+		argv = tl argv;
+	}
+	stderr = sys->fildes(2);
+	rand->init(daytime->now());
+	daytime = nil;
+
+	if(ctxt == nil)
+		fatal("wm not running");
+	display = ctxt.display;
+	tkclient->init();
+	(win, wmcmd) := tkclient->toplevel(ctxt, "", "Connect", Tkclient->Resize | Tkclient->Hide);
+	mainwin = win;
+	sys->pctl(Sys->NEWPGRP, nil);
+	cmdch := chan of string;
+	tk->namechan(win, cmdch, "cmd");
+	for(i := 0; i < len win_config; i++)
+		cmd(win, win_config[i]);
+	pid := -1;
+	sync := chan of int;
+	mvch := chan of (int, int);
+	initboard();
+	setimage();
+	spawn game(sync, mvch);
+	pid = <- sync;
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+	for(;;){
+		alt{
+			s := <-win.ctxt.kbd =>
+				tk->keyboard(win, s);
+			s := <-win.ctxt.ptr =>
+				tk->pointer(win, *s);
+			c := <-win.ctxt.ctl or
+			c = <-win.wreq or
+			c = <-wmcmd =>
+				case c{
+					"exit" =>
+						if(pid != -1)
+							kill(pid);
+						exit;
+					* =>
+						e := tkclient->wmctl(win, c);
+						if(e == nil && c[0] == '!'){
+							setimage();
+							drawboard();
+						}
+					}
+			c := <- cmdch =>
+				(nil, toks) := sys->tokenize(c, " ");
+				case hd toks{
+					"b1" or "b2" or "b3" =>
+						alt{
+							mvch <-= (int hd tl toks, int hd tl tl toks) => ;
+							* => ;
+						}
+					"bh" or "bm" or "wh" or "wm" =>
+						colour := BLACK;
+						knd := HUMAN;
+						if((hd toks)[0] == 'w')
+							colour = WHITE;
+						if((hd toks)[1] == 'm')
+							knd = MACHINE;
+						kind[colour] = knd;
+					"blev" or "wlev" =>
+						colour := BLACK;
+						e := "be";
+						if((hd toks)[0] == 'w'){
+							colour = WHITE;
+							e = "we";
+						}
+						sk := int cmd(win, ".f0." + e + " get");
+						if(sk > MAXPLIES)
+							sk = MAXPLIES;
+						if(sk >= 0)
+							skill[colour] = sk;
+					* =>
+						;
+				}
+			<- sync =>
+				pid = -1;
+				# exit;
+				spawn game(sync, mvch);
+				pid = <- sync;
+		}
+	}
+}
+
+WIDTH: con 400;
+HEIGHT: con 400;
+
+SZW: con 7;
+SZH: con 6;
+SZC: con 4;
+SZS: con 1024;
+PIECES: con SZW*SZH;
+
+BLACK, WHITE, EMPTY: con iota;
+MACHINE, HUMAN: con iota;
+SKILLB : con 8;
+SKILLW : con 0;
+MAXPLIES: con 10;
+
+board: array of array of int;	# for display
+brd: array of array of int;		# for calculations
+col: array of int;
+pieces: array of int;
+val: array of int;
+kind: array of int;
+skill: array of int;
+name: array of string;
+lines: array of array of int;
+line: array of array of list of int;
+
+mainwin: ref Toplevel;
+brdimg: ref Image;
+brdr: Rect;
+brdx, brdy: int;
+
+black, white, bg: ref Image;
+
+movech: chan of (int, int);
+
+setimage()
+{
+	brdw := int tk->cmd(mainwin, ".p cget -actwidth");
+	brdh := int tk->cmd(mainwin, ".p cget -actheight");
+	brdr = Rect((0,0), (brdw, brdh));
+	brdimg = display.newimage(brdr, display.image.chans, 0, Draw->White);
+	if(brdimg == nil)
+		fatal("not enough image memory");
+	tk->putimage(mainwin, ".p", brdimg, nil);
+}
+
+game(sync: chan of int, mvch: chan of (int, int))
+{
+	sync <-= sys->pctl(0, nil);
+	movech = mvch;
+	initbrd();
+	play();
+	sync <-= 0;
+}
+
+initboard()
+{
+	i, j, k: int;
+
+	board = array[SZW] of array of int;
+	brd = array[SZW] of array of int;
+	line = array[SZW] of array of list of int;
+	col = array[SZW] of int;
+	for(i = 0; i < SZW; i++){
+		board[i] = array[SZH] of int;
+		brd[i] = array[SZH] of int;
+		line[i] = array[SZH] of list of int;
+	}
+	pieces = array[2] of int;
+	val = array[2] of int;
+	kind = array[2] of int;
+	kind[BLACK] = MACHINE;
+	if(auto)
+		kind[WHITE] = MACHINE;
+	else
+		kind[WHITE] = HUMAN;
+	skill = array[2] of int;
+	skill[BLACK] = SKILLB;
+	skill[WHITE] = SKILLW;
+	name = array[2] of string;
+	name[BLACK] = "black";
+	name[WHITE] = "white";
+	black = display.color(Draw->Black);
+	white = display.color(Draw->White);
+	bg = display.color(Draw->Yellow);
+	n := SZW*(SZH-SZC+1)+SZH*(SZW-SZC+1)+2*(SZH-SZC+1)*(SZW-SZC+1);
+	lines = array[n] of array of int;
+	for(i = 0; i < n; i++)
+		lines[i] = array[2] of int;
+	m := 0;
+	for(i = 0; i < SZW; i++){
+		for(j = 0; j <= SZH-SZC; j++){
+			for(k = 0; k < SZC; k++){
+				line[i][j+k] = m :: line[i][j+k];
+			}
+			m++;
+		}
+	}
+	for(i = 0; i < SZH; i++){
+		for(j = 0; j <= SZW-SZC; j++){
+			for(k = 0; k < SZC; k++){
+				line[j+k][i] = m :: line[j+k][i];
+			}
+			m++;
+		}
+	}
+	for(i = 0; i <= SZW-SZC; i++){
+		for(j = 0; j <= SZH-SZC; j++){
+			for(k = 0; k < SZC; k++){
+				line[i+k][j+k] = m :: line[i+k][j+k];
+			}
+			m++;
+		}
+	}
+	for(i = 0; i <= SZW-SZC; i++){
+		for(j = 0; j <= SZH-SZC; j++){
+			for(k = 0; k < SZC; k++){
+				line[SZW-1-i-k][j+k] = m :: line[SZW-1-i-k][j+k];
+			}
+			m++;
+		}
+	}
+	if(m != n)
+		fatal(sys->sprint("%d != %d\n", m, n));		
+}
+
+initbrd()
+{
+	i, j: int;
+
+	for(i = 0; i < SZW; i++){
+		col[i] = 0;
+		for(j = 0; j < SZH; j++)
+			board[i][j] = brd[i][j] = EMPTY;
+	}
+	pieces[BLACK] = pieces[WHITE] = 0;
+	val[BLACK] = val[WHITE] = 0;
+	drawboard();
+	n := len lines;
+	for(i = 0; i < n; i++)
+		lines[i][0] = lines[i][1] = 0;
+}
+
+plays := 0;
+bwins := 0;
+wwins := 0;
+
+play()
+{
+	if(plays&1)
+		(first, second) := (WHITE, BLACK);
+	else
+		(first, second) = (BLACK, WHITE);
+	for(;;){
+		if(pieces[BLACK]+pieces[WHITE] == PIECES)
+			break;
+		m1 := move(first, second);
+		if(printout)
+			sys->print("%s: %d %d %d\n", name[first], m1, val[BLACK], val[WHITE]);
+		if(win(first))
+			break;
+		if(pieces[BLACK]+pieces[WHITE] == PIECES)
+			break;
+		m2 := move(second, first);
+		if(printout)
+			sys->print("%s: %d %d %d\n", name[second], m2, val[BLACK], val[WHITE]);
+		if(win(second))
+			break;
+	}
+	if(win(BLACK)){
+		bwins++;
+		puts("black wins");
+		highlight(BLACK);
+	}
+	else if(win(WHITE)){
+		wwins++;
+		puts("white wins");
+		highlight(WHITE);
+	}
+	else
+		puts("draw");
+	sleep(2500);
+	plays++;
+	puts(sys->sprint("black %d:%d white", bwins, wwins));
+	sleep(2500);
+	if(printout)
+		sys->print("\n");
+}
+
+move(me: int, you: int): int
+{
+	if(kind[me] == MACHINE){
+		puts("machine " + name[me] + " move");
+		return genmove(me, you);
+	}
+	else{
+		m, n: int;
+
+		# mvs := findmoves();
+		for(;;){
+			puts("human " + name[me] + " move");
+			m = getmove();
+			if(m < 0 || m >= SZW)
+				continue;
+			n = col[m];
+			valid := n >= 0 && n < SZH;
+			if(valid && brd[m][n] != EMPTY)
+				fatal("! EMPTY");
+			if(valid)
+				break;
+			puts("illegal move");
+			sleep(2500);
+		}
+		makemove(m, n, me, you, 0);
+		return m*SZS+n;
+	}
+}
+
+genmove(me: int, you: int): int
+{
+	m, n, v: int;
+
+	mvs := findmoves();
+	if(skill[me] == 0){
+		l := len mvs;
+		r := rand->rand(l);
+		# r = 0;
+		while(--r >= 0)
+			mvs = tl mvs;
+		(m, n) = hd mvs;
+	}
+	else{
+		plies := skill[me];
+		left := PIECES-(pieces[BLACK]+pieces[WHITE]);
+		if(left < plies)		# limit search
+			plies = left;
+		else if(left < 2*plies)	# expand search to end
+			plies = left;
+		else{				# expand search nearer end of game
+			k := left/plies;
+			if(k < 3)
+				plies = ((k+2)*plies)/(k+1);
+		}
+		visits = leaves = 0;
+		(v, (m, n)) = minimax(me, you, plies, ∞);
+		if(0){
+			while(mvs != nil){
+				v0: int;
+				(a, b) := hd mvs;
+				makemove(a, b, me, you, 1);
+				(v0, (m, n)) = minimax(you, me, plies-1, ∞);
+				sys->print("	(%d, %d): %d\n", a, b, -v0);
+				undomove(a, b, me, you);
+				mvs = tl mvs;
+			}
+			sys->print("best move is %d, %d\n", m, n);
+			kind[WHITE] = HUMAN;
+		}
+		if(auto)		
+			sys->print("eval = %d plies=%d goes=%d visits=%d\n", v, plies, len mvs, leaves);
+	}
+	makemove(m, n, me, you, 0);
+	return m*SZS+n;
+}
+
+findmoves(): list of (int, int)
+{
+	mvs: list of (int, int);
+
+	for(i := 0; i < SZW; i++){
+		if((j := col[i]) < SZH)
+			mvs = (i, j) :: mvs;
+	}
+	return mvs;
+}
+
+makemove(m: int, n: int, me: int, you: int, gen: int)
+{
+	pieces[me]++;
+	brd[m][n] = me;
+	col[m]++;
+	for(l := line[m][n]; l != nil; l = tl l){
+		i := hd l;
+		a := lines[i][me];
+		b := lines[i][you];
+		lines[i][me]++;
+		if(a+b >= SZC)
+			fatal("makemove a+b");
+		if(b == 0){
+			val[me] += 2*a+1;
+			if(a == SZC-1)
+				val[me] += WIN;
+		}
+		else if(a == 0)
+			val[you] -= b*b;
+	}
+	if(!gen){
+		board[m][n] = me;
+		drawpiece(m, n, me);
+		panelupdate();
+		# sleep(1000);
+	}
+}
+
+undomove(m: int, n: int, me: int, you: int)
+{
+	brd[m][n] = EMPTY;
+	pieces[me]--;
+	col[m]--;
+	for(l := line[m][n]; l != nil; l = tl l){
+		i := hd l;
+		a := lines[i][me];
+		b := lines[i][you];
+		lines[i][me]--;
+		if(a == 0 || a+b > SZC)
+			fatal("undomove a+b");
+		if(b == 0){
+			val[me] -= 2*a-1;
+			if(a == SZC)
+				val[me] -= WIN;
+		}
+		else if(a == 1)
+			val[you] += b*b;
+	}
+}
+
+win(me: int): int
+{
+	return val[me] > WIN/2;
+}
+
+highlight(me: int)
+{
+	n := len lines;
+	for(i := 0; i < n; i++){
+		if(lines[i][me] == SZC){
+			for(j := 0; j < SZW; j++){
+				for(k := 0; k < SZH; k++){
+					for(l := line[j][k]; l != nil; l = tl l){
+						if(i == hd l)
+							highpiece(j, k, board[j][k]);
+					}
+				}
+			}
+		}
+	}
+}
+
+getmove(): int
+{
+	(x, nil) := <- movech;
+	return x/brdx;
+}
+
+drawboard()
+{
+	brdx = brdr.dx()/SZW;
+	brdy = brdr.dy()/SZH;
+	brdimg.draw(brdr, bg, nil, (0, 0));
+	for(i := 1; i < SZW; i++)
+		drawline(lmap(i, 0), lmap(i, SZH), nil);
+	for(j := 1; j < SZH; j++)
+		drawline(lmap(0, j), lmap(SZW, j), nil);
+	for(i = 0; i < SZW; i++){
+		for(j = 0; j < SZH; j++){
+			if (board[i][j] == BLACK || board[i][j] == WHITE)
+				drawpiece(i, j, board[i][j]);
+		}
+	}
+	panelupdate();
+}
+
+drawpiece(m, n, p: int)
+{
+	if(p == BLACK)
+		src := black;
+	else if(p == WHITE)
+		src = white;
+	else
+		src = bg;
+	brdimg.fillellipse(cmap(m, n), 3*brdx/8, 3*brdy/8, src, (0, 0));
+}
+
+highpiece(m, n, p: int)
+{
+	if(p == BLACK)
+		src := white;
+	else if(p == WHITE)
+		src = black;
+	else
+		src = bg;
+	pt := cmap(m, n);
+	rx := (3*brdx/8, 0);
+	ry := (0, 3*brdy/8);
+	drawline(pt.add(rx), pt.sub(rx), src);
+	drawline(pt.add(ry), pt.sub(ry), src);
+}
+
+panelupdate()
+{
+	tk->cmd(mainwin, sys->sprint(".p dirty %d %d %d %d", brdr.min.x, brdr.min.y, brdr.max.x, brdr.max.y));
+	tk->cmd(mainwin, "update");
+}
+
+drawline(p0, p1: Point, c: ref Image)
+{
+	if(c == nil)
+		c = black;
+	brdimg.line(p0, p1, Draw->Endsquare, Draw->Endsquare, 0, c, (0, 0));
+}
+
+cmap(m, n: int): Point
+{
+	return brdr.min.add((m*brdx+brdx/2, (SZH-1-n)*brdy+brdy/2));
+}
+
+lmap(m, n: int): Point
+{
+	return brdr.min.add((m*brdx, n*brdy));
+}
+
+∞: con (1<<30);
+WIN: con (1<<20);
+MAXVISITS: con 1024;
+
+visits, leaves : int;
+
+minimax(me: int, you: int, plies: int, αβ: int): (int, (int, int))
+{
+	v: int;
+
+	if(plies == 0){
+		visits++;
+		leaves++;
+		if(visits == MAXVISITS){
+			visits = 0;
+			sys->sleep(0);
+		}
+		return (eval(me, you), (0, 0));
+	}
+	mvs := findmoves();
+	if(mvs == nil){
+		fatal("mvs==nil");
+		# if(mv)
+		# 	(v, nil) := minimax(you, me, plies, ∞);
+		# else
+		#	(v, nil) = minimax(you, me, plies-1, ∞);
+		# return (-v, (0, 0));
+	}
+	bestv := -∞;
+	bestm := (0, 0);
+	e := 0;
+	for(; mvs != nil; mvs = tl mvs){
+		(m, n) := hd mvs;
+		makemove(m, n, me, you, 1);
+		if(win(me))
+			v = eval(me, you);
+		else{
+			(v, nil) = minimax(you, me, plies-1, -bestv);
+			v = -v;
+		}
+		undomove(m, n, me, you);
+		if(v > bestv || (v == bestv && rand->rand(++e) == 0)){
+			if(v > bestv)
+				e = 1;
+			bestv = v;
+			bestm = (m, n);
+			if(bestv >= αβ)
+				return (∞, (0, 0));
+		}
+	}
+	return (bestv, bestm);
+}
+	
+eval(me: int, you: int): int
+{
+	return val[me]-val[you];
+}
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "%s\n", s);
+	exit;
+}
+
+sleep(t: int)
+{
+	if(nosleep)
+		sys->sleep(0);
+	else
+		sys->sleep(t);
+}
+
+kill(pid: int): int
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil)
+		return -1;
+	if(sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+cmd(top: ref Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr, "connect: tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+# swidth: int;
+# sfont: ref Font;
+
+# gettxtattrs()
+# {
+#	swidth = int cmd(mainwin, ".f1.txt cget -width");	# always initial value ?
+#	f := cmd(mainwin, ".f1.txt cget -font");
+#	sfont = Font.open(brdimg.display, f);
+# }
+	
+puts(s: string)
+{
+	# while(sfont.width(s) > swidth)
+	#	s = s[0: len s -1];
+	cmd(mainwin, ".f1.txt configure -text {" + s + "}");
+	cmd(mainwin, "update");
+}
+					
+win_config := array[] of {
+	"frame .f",
+	"menubutton .f.bk -text Black -menu .f.bk.bm",
+	"menubutton .f.wk -text White -menu .f.wk.wm",
+	"menu .f.bk.bm",
+	".f.bk.bm add command -label Human -command { send cmd bh }",
+	".f.bk.bm add command -label Machine -command { send cmd bm }",
+	"menu .f.wk.wm",
+	".f.wk.wm add command -label Human -command { send cmd wh }",
+	".f.wk.wm add command -label Machine -command { send cmd wm }",
+	"pack .f.bk -side left",
+	"pack .f.wk -side right",
+
+	"frame .f0",
+	"label .f0.bl -text {Black level}",
+	"label .f0.wl -text {White level}",
+	"entry .f0.be -width 32",
+	"entry .f0.we -width 32",
+	".f0.be insert 0 {" + string SKILLB+"}",
+	".f0.we insert 0 {" + string SKILLW+"}",
+	"pack .f0.bl -side left",
+	"pack .f0.be -side left",
+	"pack .f0.wl -side right",
+	"pack .f0.we -side right",
+
+	"frame .f1",
+	"label .f1.txt -text { } -width " + string WIDTH,
+	"pack .f1.txt -side top -fill x",
+
+	"panel .p -width " + string WIDTH + " -height " + string HEIGHT,
+
+	"pack .f -side top -fill x",
+	"pack .f0 -side top -fill x",
+	"pack .f1 -side top -fill x",
+	"pack .p -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+
+	"bind .p <Button-1> {send cmd b1 %x %y}",
+	"bind .p <Button-2> {send cmd b2 %x %y}",
+	"bind .p <Button-3> {send cmd b3 %x %y}",
+	# "bind .c <ButtonRelease-1> {send cmd b1r %x %y}",
+	# "bind .c <ButtonRelease-2> {send cmd b2r %x %y}",
+	# "bind .c <ButtonRelease-3> {send cmd b3r %x %y}",
+	"bind .f0.be <Key-\n> {send cmd blev}",
+	"bind .f0.we <Key-\n> {send cmd wlev}",
+	"update",
+};
--- /dev/null
+++ b/appl/wm/calendar.b
@@ -1,0 +1,1064 @@
+implement Calendar;
+
+#
+# Copyright © 2000 Vita Nuova Limited. All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Font, Point, Rect: import draw;
+include "daytime.m";
+	daytime: Daytime;
+	Tm: import Daytime;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "dialog.m";
+	dialog: Dialog;
+include "readdir.m";
+include "translate.m";
+	translate: Translate;
+	Dict: import translate;
+include "arg.m";
+	arg: Arg;
+include "sh.m";
+
+Calendar: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Cal: adt {
+	w: string;
+	dx, dy: int;
+	onepos: int;
+	top: ref Tk->Toplevel;
+	sched: ref Schedule;
+	date: int;
+	marked: array of int;
+	make: fn(top: ref Tk->Toplevel, sched: ref Schedule, w: string): (ref Cal, chan of string);
+	show: fn(cal: self ref Cal, date: int);
+	mark: fn(cal: self ref Cal, ent: Entry);
+};
+
+Entry: adt {
+	date: int;		# YYYYMMDD
+	mark: int;
+};
+
+Sentry: adt {
+	ent: Entry;
+	file: int;
+};
+
+Schedule: adt {
+	dir: string;
+	entries: array of Sentry;
+	new: fn(dir: string): (ref Schedule, string);
+	getentry: fn(sched: self ref Schedule, date: int): (int, Entry);
+	readentry: fn(sched: self ref Schedule, date: int): (Entry, string);
+	setentry: fn(sched: self ref Schedule, ent: Entry, data: string): (int, string);
+};
+
+Markset: adt {
+	new: fn(top: ref Tk->Toplevel, cal: ref Cal, w: string): (ref Markset, chan of string);
+	set: fn(m: self ref Markset, kind: int);
+	get: fn(m: self ref Markset): int;
+	ctl: fn(m: self ref Markset, c: string);
+
+	top: ref Tk->Toplevel;
+	cal: ref Cal;
+	w: string;
+	curr: int;
+};
+
+DBFSPATH: con "/dis/rawdbfs.dis";
+SCHEDDIR: con "/mnt/schedule";
+
+stderr: ref Sys->FD;
+dict: ref Dict;
+font := "/fonts/lucidasans/unicode.7.font";
+days, months: array of string;
+
+packcmds := array[] of {
+"pack .ctf.show .ctf.set .ctf.date -side right",
+"pack .ctf -side top -fill x",
+
+"pack .cf.head.fwd .cf.head.bwd .cf.head.date -side right",
+"pack .cf.head -side top -fill x",
+"pack .cf.cal -side top",
+"pack .cf -side top",
+
+"pack .schedf.head.fwd .schedf.head.bwd .schedf.head.date .schedf.head.markset"
+	+ " .schedf.head.save .schedf.head.del -side right",
+"pack .schedf.head -side top -fill x",
+"pack .schedf.tf.scroll -side left -fill y",
+"pack .schedf.tf.t -side top -fill both -expand 1",
+"pack .schedf.tf -side top -fill both -expand 1",
+"pack .schedf -side top -fill both -expand 1",
+};
+
+Savebut: con ".schedf.head.save";
+Delbut: con ".schedf.head.del";
+
+usage()
+{
+	sys->fprint(stderr, "usage: calendar [-f font] [/mnt/schedule | schedfile]\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	loadmods();
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "calendar: no window context\n");
+		raise "fail:bad context";
+	}
+	days = Xa(array[] of {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri",  "Sat"});
+	months = Xa(array[] of {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"});
+	arg->init(argv);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'f' =>
+			if ((font = arg->arg()) == nil)
+				usage();
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	scheddir := SCHEDDIR;
+	if (argv != nil)
+		scheddir = hd argv;
+	(top, wmctl) := tkclient->toplevel(ctxt, "", X("Calendar"), Tkclient->Appl);
+	if (top == nil) {
+		sys->fprint(stderr, "cal: cannot make window: %r\n");
+		raise "fail:cannot make window";
+	}
+	(sched, err) := Schedule.new(scheddir);
+	if (sched == nil)
+		sys->fprint(stderr, "cal: cannot load schedule: %s\n", err);
+	currtime := daytime->local(daytime->now());
+	if (currtime == nil) {
+		sys->fprint(stderr, "cannot get local time: %r\n");
+		raise "fail:failed to get local time";
+	}
+	date := tm2date(currtime);
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+
+	cmdch := chan of string;
+	tk->namechan(top, cmdch, "cmd");
+	wincmds := array[] of {
+	"frame .ctf",
+	"button .ctf.set -text {"+X("Set")+"} -command {send cmd settime}",
+	"button .ctf.show -text {"+X("Show")+"} -command {send cmd showtime}",
+	
+	"frame .cf -bd 2 -relief raised",
+	"frame .cf.head",
+	"button .cf.head.bwd -text {<<} -command {send cmd bwdmonth}",
+	"button .cf.head.fwd -text {>>} -command {send cmd fwdmonth}",
+	"label .cf.head.date -text {XXX 0000}",
+	
+	"frame .schedf -bd 2 -relief raised",
+	"frame .schedf.head",
+	"button .schedf.head.save -text {"+X("Save")+"} -command {send cmd save}",
+	"button .schedf.head.del -text {"+X("Del")+"} -command {send cmd del}",
+	"label .schedf.head.date -text {0000/00/00}",
+	"canvas .schedf.head.markset",
+	"button .schedf.head.bwd -text {<<} -command {send cmd bwdday}",
+	"button .schedf.head.fwd -text {>>} -command {send cmd fwdday}",
+	"frame .schedf.tf",
+	"scrollbar .schedf.tf.scroll -command {.schedf.tf.t yview}",
+	"text .schedf.tf.t -wrap word -yscrollcommand {.schedf.tf.scroll set} -height 7h -width 20w",
+	"bind .schedf.tf.t <Key> +{send cmd dirty}",
+	};
+	tkcmds(top, wincmds);
+	(cal, calch) := Cal.make(top, sched, ".cf.cal");
+	sync := chan of int;
+	spawn clock(top, ".ctf.date", sync);
+	clockpid := <-sync;
+	(ms, msch) := Markset.new(top, cal, ".schedf.head.markset");
+	tkcmds(top, packcmds);
+	if (sched == nil)
+		cmd(top, "pack forget .schedf");
+
+	showdate(top, cal, ms, date);
+	cmd(top, "pack propagate . 0");
+	cmd(top, "update");
+	if (date < 19700002)
+		raisesettime(ctxt, top);
+
+	setting := 0;
+	dirty := 0;
+	empty := scheduleempty(top);
+	currsched := 0;
+
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+
+	for (;;) {
+		enable(top, Savebut, dirty);
+		enable(top, Delbut, !empty);
+		cmd(top, "update");
+		ndate := date;
+		alt {
+		c := <-calch =>
+			(y,m,d) := date2ymd(date);
+			d = int c;
+			ndate = ymd2date(y,m,d);
+		c := <-msch =>
+			ms.ctl(c);
+			cal.mark(Entry(date, ms.get()));
+			dirty = 1;
+		c := <-cmdch =>
+			case c {
+			"dirty" =>
+				dirty = 1;
+				nowempty := scheduleempty(top);
+				if (nowempty != empty) {
+					if (nowempty) {
+						ms.set(0);
+						cal.mark(Entry(date, 0));
+					} else {
+						ms.set(1);
+						cal.mark(Entry(date, ms.get()));
+					}
+					empty = nowempty;
+				}
+			"bwdmonth" =>
+				ndate = decmonth(date);
+			"fwdmonth" =>
+				ndate = incmonth(date);
+			"bwdday" =>
+				ndate = adddays(date, -1);
+			"fwdday" =>
+				ndate = adddays(date, 1);
+			"del" =>
+				if (!empty) {
+					cmd(top, ".schedf.tf.t delete 1.0 end");
+					empty = 1;
+					dirty = 1;
+					cal.mark(Entry(date, 0));
+				}
+			"save" =>
+				if (dirty && save(ctxt, top, cal, ms, date) != -1)
+					dirty = 0;
+			"settime" =>
+				raisesettime(ctxt, top);
+			"showtime" =>
+				ndate = tm2date(daytime->local(daytime->now()));
+			* =>
+				sys->fprint(stderr, "cal: unknown command '%s'\n", c);
+			}
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		c := <-top.ctxt.ctl or
+		c = <-top.wreq or
+		c = <-wmctl =>
+			if (c == "exit" && dirty)
+				save(ctxt, top, cal, ms, date);
+			tkclient->wmctl(top, c);
+		}
+		if (ndate != date) {
+			e := 0;
+			if (dirty)
+				e = save(ctxt, top, cal, ms, date);
+			if (e != -1) {
+				dirty = 0;
+				showdate(top, cal, ms, ndate);
+				empty = scheduleempty(top);
+				date = ndate;
+				cmd(top, "update");
+			}
+		}
+	}
+}
+
+Markset.new(top: ref Tk->Toplevel, cal: ref Cal, w: string): (ref Markset, chan of string)
+{
+	cmd(top, w+" configure -width "+string (cal.dx * 2 + 6) +
+				" -height "+string (cal.dy + 4));
+	ch := chan of string;
+	tk->namechan(top, ch, "markcmd");
+	return (ref Markset(top, cal, w, 0), ch);
+}
+
+Markset.set(m: self ref Markset, kind: int)
+{
+	cmd(m.top, m.w + " delete x");
+	if (kind > 0) {
+		(shape, col) := kind2shapecol(kind);
+		id := cmd(m.top, m.w + " create " +
+			shapestr(m.cal, (m.cal.dx/2+2, m.cal.dy/2+2), Square) +
+			" -fill " + colours[col] + " -tags x");
+		cmd(m.top, m.w + " bind " + id + " <ButtonRelease-1> {send markcmd col}");
+		id = cmd(m.top, m.w + " create " +
+			shapestr(m.cal, (m.cal.dx * 3 / 2+4, m.cal.dy/2+2), shape) +
+			" -tags x -width 2");
+		cmd(m.top, m.w + " bind " + id + " <ButtonRelease-1> {send markcmd shape}");
+	}
+	m.curr = kind;
+}
+
+Markset.get(m: self ref Markset): int
+{
+	return m.curr;
+}
+
+Markset.ctl(m: self ref Markset, c: string)
+{
+	(shape, col) := kind2shapecol(m.curr);
+	case c {
+	"col" => col = (col + 1) % len colours;
+	"shape" => shape = (shape + 1) % Numshapes;
+	}
+	m.set(shapecol2kind((shape, col)));
+}
+
+scheduleempty(top: ref Tk->Toplevel): int
+{
+	return int cmd(top, ".schedf.tf.t compare 1.0 == end");
+}
+
+enable(top: ref Tk->Toplevel, but: string, enable: int)
+{
+	cmd(top, but + " configure -state " +
+		(array[] of {"disabled", "normal"})[!!enable]);
+}
+
+save(ctxt: ref Draw->Context, top: ref Tk->Toplevel, cal: ref Cal, ms: ref Markset, date: int): int
+{
+	s := cmd(top, ".schedf.tf.t get 1.0 end");
+	empty := scheduleempty(top);
+	mark := ms.get();
+	if (empty)
+		mark = 0;
+	ent := Entry(date, mark);
+	cal.mark(ent);
+	(ok, err) := cal.sched.setentry(ent, s);
+	if (ok == -1) {
+		notice(ctxt, top, "Cannot save entry: " + err);
+		return -1;
+	}
+	return 0;
+}
+
+notice(ctxt: ref Draw->Context, top: ref Tk->Toplevel, s: string)
+{
+	dialog->prompt(ctxt, top.image, nil, "Notice", s, 0, "OK"::nil);
+}
+
+showdate(top: ref Tk->Toplevel, cal: ref Cal, ms: ref Markset, date: int)
+{
+	(y,m,d) := date2ymd(date);
+ 	cal.show(date);
+	cmd(top, ".cf.head.date configure -text {" + sys->sprint("%.4d/%.2d", y, m+1) + "}");
+	cmd(top, ".schedf.head.date configure -text {" + sys->sprint("%.4d/%.2d/%.2d", y, m+1, d) + "}");
+	(ent, s) := cal.sched.readentry(date);
+	ms.set(ent.mark);
+	cmd(top, ".schedf.tf.t delete 1.0 end; .schedf.tf.t insert 1.0 '" + s);
+}
+
+nomod(s: string)
+{
+	sys->fprint(stderr, "cal: cannot load %s: %r\n", s);
+	raise "fail:bad module";
+}
+
+loadmods()
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	daytime = load Daytime Daytime->PATH;
+	if (daytime == nil)
+		nomod(Daytime->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		nomod(Tkclient->PATH);
+	translate = load Translate Translate->PATH;
+	if(translate != nil){
+		translate->init();
+		(dict, nil) = translate->opendict(translate->mkdictname("", "calendar"));
+	}
+	tkclient->init();
+	arg = load Arg Arg->PATH;
+	if (arg == nil)
+		nomod(Arg->PATH);
+	dialog = load Dialog Dialog->PATH;
+	if(dialog == nil)
+		nomod(Dialog->PATH);
+	dialog->init();
+}
+
+s2a(s: string, min, max: int, sep: string): array of int
+{
+	(ntoks, toks) := sys->tokenize(s, sep);
+	if (ntoks < min || ntoks > max)
+		return nil;
+	a := array[max] of int;
+	for (i := 0; toks != nil; toks = tl toks) {
+		if (!isnum(hd toks))
+			return nil;
+		a[i++] = int hd toks;
+	}
+	return a[0:i];
+}
+
+validtm(t: ref Daytime->Tm): int
+{
+	if (t.hour < 0 || t.hour > 23
+			|| t.min < 0 || t.min > 59
+			|| t.sec < 0 || t.sec > 59
+			|| t.mday < 1 || t.mday > 31
+			|| t.mon < 0 || t.mon > 11
+			|| t.year < 70 || t.year > 137)
+		return 0;
+	if (t.mon == 1 && dysize(t.year+1900) > 365)
+		return t.mday <= 29;
+	return t.mday <= dmsize[t.mon];
+}
+
+clock(top: ref Tk->Toplevel, w: string, sync: chan of int)
+{
+	cmd(top, "label " + w);	
+	fd := sys->open("/dev/time", Sys->OREAD);
+	if (fd == nil) {
+		sync <-= -1;
+		return;
+	}
+	buf := array[128] of byte;
+	for (;;) {
+		sys->seek(fd, big 0, Sys->SEEKSTART);
+		n := sys->read(fd, buf, len buf);
+		if (n < 0) {
+			sys->fprint(stderr, "cal: could not read time: %r\n");
+			if (sync != nil)
+				sync <-= -1;
+			break;
+		}
+		ms := big string buf[0:n] / big 1000;
+		ct := ms / big 1000;
+		t := daytime->local(int ct);
+
+		s := sys->sprint("%s %s %d %.2d:%.2d.%.2d",
+			days[t.wday], months[t.mon], t.mday, t.hour, t.min, t.sec);
+		cmd(top, w + " configure -text {" + s + "}");
+		cmd(top, "update");
+		if (sync != nil) {
+			sync <-= sys->pctl(0, nil);
+			sync = nil;
+		}
+		sys->sleep(int ((ct + big 1) * big 1000 - ms));
+	}
+}
+
+# "the world is the lord's and all it contains,
+# save the highlands and islands, which belong to macbraynes"
+Cal.make(top: ref Tk->Toplevel, sched: ref Schedule, w: string): (ref Cal, chan of string)
+{
+	f := Font.open(top.display, font);
+	if (f == nil) {
+		sys->fprint(stderr, "cal: could not open font %s: %r\n", font);
+		font = cmd(top, ". cget -font");
+		f = Font.open(top.display, font);
+	}
+	if (f == nil)
+		return (nil, nil);
+	maxw := 0;
+	for (i := 0; i < 7; i++) {
+		if ((dw := f.width(days[i] + " ")) > maxw)
+			maxw = dw;
+	}
+	for (i = 10; i < 32; i++) {
+		if ((dw := f.width(string i + " ")) > maxw)
+			maxw = dw;
+	}
+	cal := ref Cal;
+	cal.w = w;
+	cal.dx = maxw;
+	cal.dy = f.height;
+	cal.onepos = 0;
+	cal.top = top;
+	cal.sched = sched;
+	cal.marked = array[31] of {* => 0};
+	cmd(top, "canvas " + w + " -width " + string (cal.dx * 7) + " -height " + string (cal.dy * 7));
+	for (i = 0; i < 7; i++)
+		cmd(top, w + " create text " + posstr(daypos(cal, i, 0))
+				+ " -text " + days[i] + " -font " + font);
+	ch := chan of string;
+	tk->namechan(top, ch, "ch" + w);
+	return (cal, ch);
+}
+
+Cal.show(cal: self ref Cal, date: int)
+{
+	if (date == cal.date)
+		return;
+	mon := (date / 100) % 100;
+	year := date / 10000;
+	cmd(cal.top, cal.w + " delete curr");
+	if (cal.date / 100 != date / 100) {
+		cmd(cal.top, cal.w + " delete date");
+		cmd(cal.top, cal.w + " delete mark");
+		for (i := 0; i < len cal.marked; i++)
+			cal.marked[i] = 0;
+		(md, wd) := monthinfo(mon, year);
+		base := year * 10000 + mon * 100;
+		cal.onepos = wd;
+		for (i = 0; i < 6; i++) {
+			for (j := 0; j < 7; j++) {
+				d := i * 7 + j - wd;
+				if (d >= 0 && d < md) {
+					id := cmd(cal.top, cal.w + " create text " + posstr(daypos(cal, j, i+1))
+						+ " -tags date -text " + string (d+1)
+						+ " -font " + font);
+					cmd(cal.top, cal.w + " bind " + id +
+						" <ButtonRelease-1> {send ch" + cal.w + " " + string (d+1) + "}");
+					(ok, ent) := cal.sched.getentry(base + d + 1);
+					if (ok != -1)
+						cal.mark(ent);
+				}
+			}
+		}
+	}
+	if (cal.sched != nil) {
+		e := date % 100 - 1 + cal.onepos;
+		p := daypos(cal, e % 7, e / 7 + 1);
+		cmd(cal.top, cal.w + " create " + shapestr(cal, p, Square) +
+				" -tags curr -width 3");
+	}
+	cal.date = date;
+}
+
+Cal.mark(cal: self ref Cal, ent: Entry)
+{
+	if (ent.date / 100 != ent.date / 100)
+		return;
+	(nil, nil, d) := date2ymd(ent.date);
+	d--;
+	cmd(cal.top, cal.w + " delete m" + string d);
+	if (ent.mark) {
+		e := d + cal.onepos;
+		p := daypos(cal, e % 7, e / 7 + 1);
+		id := cmd(cal.top, cal.w + " create " + itemshape(cal, p, ent.mark) +
+				" -tags {mark m"+string d + "}");
+		cmd(cal.top, cal.w + " bind " + id +
+				" <ButtonRelease-1> {send ch" + cal.w + " " + string (d+1) + "}");
+		cmd(cal.top, cal.w + " lower " + id);
+	}
+	cal.marked[d] = ent.mark;
+}
+
+Oval, Diamond, Square, Numshapes: con iota;
+
+colours := array[] of {
+	"red",
+	"yellow",
+	"#00eeee",
+	"white"
+};
+
+kind2shapecol(kind: int): (int, int)
+{
+	kind = (kind - 1) & 16rffff;
+	return ((kind & 16rff) % Numshapes, (kind >> 8) % len colours);
+}
+
+shapecol2kind(shapecol: (int, int)): int
+{
+	(shape, colour) := shapecol;
+	return (shape + (colour << 8)) + 1;
+}
+
+itemshape(cal: ref Cal, centre: Point, kind: int): string
+{
+	(shape, colour) := kind2shapecol(kind);
+	return shapestr(cal, centre, shape) + " -fill " + colours[colour];
+}
+
+shapestr(cal: ref Cal, p: Point, kind: int): string
+{
+	(hdx, hdy) := (cal.dx / 2, cal.dy / 2);
+	case kind {
+	Oval =>
+		r := Rect((p.x - hdx, p.y - hdy), (p.x + hdx, p.y + hdy));
+		return "oval " + rectstr(r);
+	Diamond =>
+		return "polygon " + string (p.x - hdx) + " " + string p.y + " " +
+					string p.x + " " + string (p.y - hdy) + " " +
+					string (p.x + hdx) + " " + string p.y + " " +
+					string p.x + " " + string (p.y + hdy) +
+				" -outline black";
+	Square =>
+		r := Rect((p.x - hdx, p.y - hdy), (p.x + hdx, p.y + hdy));
+		return "rectangle " + rectstr(r);
+	* =>
+		sys->fprint(stderr, "cal: unknown shape %d\n", kind);
+		return nil;
+	}
+}
+		
+rectstr(r: Rect): string
+{
+	return string r.min.x + " " + string r.min.y + " " +
+			string r.max.x + " " + string r.max.y;
+}
+
+posstr(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+# return centre point of position for day.
+daypos(cal: ref Cal, d, w: int): Point
+{
+	return Point(d * cal.dx + cal.dx / 2, w * cal.dy + cal.dy / 2);
+}
+
+body2entry(body: string): (int, Entry, string)
+{
+	for (i := 0; i < len body; i++)
+		if (body[i] == '\n')
+			break;
+	if (i == len body)
+		return (-1, (-1, -1), "invalid schedule header (no newline)");
+	(n, toks) := sys->tokenize(body[0:i], " \t\n");
+	if (n < 2)
+		return (-1, (-1, -1), "invalid schedule header (too few fields)");
+	date := int hd toks;
+	(y, m, d) := (date / 10000, (date / 100) % 100, date%100);
+	if (y < 1970 || y > 2037 || m > 12 || m < 1 || d > 31 || d < 1)
+		return (-1, (-1,-1), sys->sprint("invalid date (%.8d) in schedule header", date));
+	e := Entry(ymd2date(y, m-1, d), int hd tl toks);
+	return (0, e, body[i+1:]);
+}
+
+startdbfs(f: string): (string, string)
+{
+	dbfs := load Command DBFSPATH;
+	if (dbfs == nil)
+		return (nil, sys->sprint("cannot load %s: %r", DBFSPATH));
+	sync := chan of string;
+	spawn rundbfs(sync, dbfs, f, SCHEDDIR);
+	e := <-sync;
+	if (e != nil)
+		return (nil, e);
+	return (SCHEDDIR, nil);
+}
+
+rundbfs(sync: chan of string, dbfs: Command, f, d: string)
+{
+	sys->pctl(Sys->FORKFD, nil);
+	{
+		dbfs->init(nil, "dbfs" :: "-r" :: f :: d :: nil);
+		sync <-= nil;
+	}exception e{
+	"fail:*" =>
+		sync <-= "dbfs failed: " + e[5:];
+		exit;
+	}
+}
+
+Schedule.new(d: string): (ref Schedule, string)
+{
+	(rc, info) := sys->stat(d);
+	if (rc == -1)
+		return (nil, sys->sprint("cannot find %s: %r", d));
+	if ((info.mode & Sys->DMDIR) == 0) {
+		err: string;
+		(d, err) = startdbfs(d);
+		if (d == nil)
+			return (nil, err);
+	}
+	(rc, nil) = sys->stat(d + "/new");
+	if (rc == -1)
+		return (nil, "no dbfs mounted on " + d);
+		
+	readdir := load Readdir Readdir->PATH;
+	if (readdir == nil)
+		return (nil, sys->sprint("cannot load %s: %r", Readdir->PATH));
+	sched := ref Schedule;
+	sched.dir = d;
+	(de, nil) := readdir->init(d, Readdir->NONE);
+	if (de == nil)
+		return (nil, "could not read schedule directory");
+	buf := array[Sys->ATOMICIO] of byte;
+	sched.entries = array[len de] of Sentry;
+	ne := 0;
+	for (i := 0; i < len de; i++) {
+		if (!isnum(de[i].name))
+			continue;
+		f := d + "/" + de[i].name;
+		fd := sys->open(f, Sys->OREAD);
+		if (fd == nil) {
+			sys->fprint(stderr, "cal: cannot open %s: %r\n", f);
+		} else {
+			n := sys->read(fd, buf, len buf);
+			if (n == -1) {
+				sys->fprint(stderr, "cal: error reading %s: %r\n", f);
+			} else {
+				(ok, e, err) := body2entry(string buf[0:n]);
+				if (ok == -1)
+					sys->fprint(stderr, "cal: error on entry %s: %s\n", f, err);
+				else
+					sched.entries[ne++] = (e, int de[i].name);
+				err = nil;
+			}
+		}
+	}
+	sched.entries = sched.entries[0:ne];
+	sortentries(sched.entries);
+	return (sched, nil);
+}
+
+Schedule.getentry(sched: self ref Schedule, date: int): (int, Entry)
+{
+	if (sched == nil)
+		return (-1, (-1, -1));
+	ent := search(sched, date);
+	if (ent == -1)
+		return (-1, (-1,-1));
+	return (0, sched.entries[ent].ent);
+}
+
+Schedule.readentry(sched: self ref Schedule, date: int): (Entry, string)
+{
+	if (sched == nil)
+		return ((-1, -1), nil);
+	ent := search(sched, date);
+	if (ent == -1)
+		return ((-1, -1), nil);
+	(nil, fno) := sched.entries[ent];
+
+	f := sched.dir + "/" + string fno;
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil) {
+		sys->fprint(stderr, "cal: cannot open %s: %r", f);
+		return ((-1, -1), nil);
+	}
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n == -1) {
+		sys->fprint(stderr, "cal: cannot read %s: %r", f);
+		return ((-1, -1), nil);
+	}
+	(ok, e, body) := body2entry(string buf[0:n]);
+	if (ok == -1) {
+		sys->fprint(stderr, "cal: couldn't get body in file %s: %s\n", f, body);
+		return ((-1, -1), nil);
+	}
+	return (e, body);
+}	
+
+writeentry(fd: ref Sys->FD, ent: Entry, data: string): (int, string)
+{
+	ent.date += 100;
+	b := array of byte (sys->sprint("%d %d\n", ent.date, ent.mark) + data);
+	if (len b > Sys->ATOMICIO)
+		return (-1, "entry is too long");
+	if (sys->write(fd, b, len b) != len b)
+		return (-1, sys->sprint("cannot write entry: %r"));
+	return (0, nil);
+}
+	
+Schedule.setentry(sched: self ref Schedule, ent: Entry, data: string): (int, string)
+{
+	if (sched == nil)
+		return (-1, "no schedule");
+	idx := search(sched, ent.date);
+	if (idx == -1) {
+		if (data == nil)
+			return (0, nil);
+		fd := sys->open(sched.dir + "/new", Sys->OWRITE);
+		if (fd == nil)
+			return (-1, sys->sprint("cannot open new: %r"));
+		(ok, info) := sys->fstat(fd);
+		if (ok == -1)
+			return (-1, sys->sprint("cannot stat new: %r"));
+		if (!isnum(info.name))
+			return (-1, "new dbfs entry is not numeric");
+		err: string;
+		(ok, err) = writeentry(fd, ent, data);
+		if (ok == -1)
+			return (ok, err);
+		(fd, data) = (nil, nil);
+		e := sched.entries;
+		for (i := 0; i < len e; i++)
+			if (ent.date < e[i].ent.date)
+				break;
+		ne := array[len e + 1] of Sentry;
+		(ne[0:],  ne[i], ne[i+1:]) = (e[0:i], (ent, int info.name), e[i:]);
+		sched.entries = ne;
+		return (0, nil);
+	} else {
+		fno := sched.entries[idx].file;
+		f := sched.dir + "/" + string fno;
+		if (data == nil) {
+			sys->remove(f);
+			sched.entries[idx:] = sched.entries[idx+1:];
+			sched.entries = sched.entries[0:len sched.entries - 1];
+			return (0, nil);
+		} else {
+			sched.entries[idx] = (ent, fno);
+			fd := sys->open(f, Sys->OWRITE);
+			if (fd == nil)
+				return (-1, sys->sprint("cannot open %s: %r", sched.dir + "/" + string fno));
+			return writeentry(fd, ent, data);
+		}
+	}
+}
+
+search(sched: ref Schedule, date: int): int
+{
+	e := sched.entries;
+	lo := 0;
+	hi := len e - 1;
+	while (lo <= hi) {
+		mid := (lo + hi) / 2;
+		if (date < e[mid].ent.date)
+			hi = mid - 1;
+		else if (date > e[mid].ent.date)
+			lo = mid + 1;
+		else
+			return mid;
+	}
+	return -1;
+}
+
+sortentries(a: array of Sentry)
+{
+	m: int;
+	n := len a;
+	for(m = n; m > 1; ) {
+		if(m < 5)
+			m = 1;
+		else
+			m = (5*m-1)/11;
+		for(i := n-m-1; i >= 0; i--) {
+			tmp := a[i];
+			for(j := i+m; j <= n-1 && tmp.ent.date > a[j].ent.date; j += m)
+				a[j-m] = a[j];
+			a[j-m] = tmp;
+		}
+	}
+}
+
+raisesettime(ctxt: ref Draw->Context, top: ref Tk->Toplevel)
+{
+	panelcmds := array[] of {
+	"frame .d",
+	"label .d.title -text {"+X("Date (YYYY/MM/DD):")+"}",
+	"entry .d.de -width 11w}",
+	"frame .t",
+	"label .t.title -text {"+X("Time (HH:MM.SS):")+"}",
+	"entry .t.te -width 11w}",
+	"frame .b",
+	"button .b.set -text Set -command {send cmd set}",
+	"button .b.cancel -text Cancel -command {send cmd cancel}",
+	"pack .d .t .b -side top -fill x",
+	"pack .d.de .d.title -side right",
+	"pack .t.te .t.title -side right",
+	"pack .b.set .b.cancel -side right",
+	};
+	fd := sys->open("/dev/time", Sys->OWRITE);
+	if (fd == nil) {
+		notice(ctxt, top, X("Cannot set time: ") + sys->sprint("%r"));
+		return;
+	}
+	(panel, wmctl) := tkclient->toplevel(ctxt, "",	X("Set Time"), 0);
+	tkcmds(panel, panelcmds);
+	cmdch := chan of string;
+	tk->namechan(panel, cmdch, "cmd");
+	t := daytime->local(daytime->now());
+	if (t.year < 71)
+		(t.year, t.mon, t.mday) = (100, 0, 1);
+	cmd(panel, ".d.de insert 0 " + sys->sprint("%.4d/%.2d/%.2d",
+				t.year+1900, t.mon+1, t.mday));
+	cmd(panel, ".t.te insert 0 " + sys->sprint("%.2d:%.2d.%.2d", t.hour, t.min, t.sec));
+	#cmd(panel, "grab set ."); XXX should, but not a good idea with global tk.
+	# wouldn't work with current dialog->prompt() either...
+	cmd(panel, "update");
+	tkclient->onscreen(panel, nil);
+	tkclient->startinput(panel, "kbd"::"ptr"::nil);
+
+loop: for (;;) alt {
+	s := <-panel.ctxt.kbd =>
+		tk->keyboard(panel, s);
+	s := <-panel.ctxt.ptr =>
+		tk->pointer(panel, *s);
+	c := <-cmdch =>
+		case c {
+		"set" =>
+			err := settime(fd, cmd(panel, ".d.de get"), cmd(panel, ".t.te get"));
+			if (err == nil)
+				break loop;
+			notice(ctxt, panel, X("Cannot set time: ") + err);
+		"cancel" =>
+			break loop;
+		* =>;
+		}
+	c := <-wmctl =>
+		case c {
+		"exit" =>
+			break loop;
+		* =>
+			tkclient->wmctl(panel, c);
+		}
+	}
+}
+
+settime(tfd: ref Sys->FD, date, time: string): string
+{
+	da := s2a(date, 3, 3, "/");
+	if (da == nil)
+		return X("Invalid date syntax");
+	ta := s2a(time, 2, 3, ":.");
+	if (ta == nil)
+		return X("Invalid time syntax");
+	t := ref blanktm;
+	if (da[2] > 1000)
+		(da[0], da[1], da[2]) = (da[2], da[1], da[0]);
+	(t.year, t.mon, t.mday) = (da[0]-1900, da[1]-1, da[2]);
+	if (len ta == 3)
+		(t.hour, t.min, t.sec) = (ta[0], ta[1], ta[2]);
+	else
+		(t.hour, t.min, t.sec) = (ta[0], ta[1], 0);
+	if (!validtm(t))
+		return X("Invalid time or date given");
+	s := string daytime->tm2epoch(t) + "000000";
+	if (sys->fprint(tfd, "%s", s) == -1)
+		return X("write failed:") + sys->sprint(" %r");
+	return nil;
+}
+	
+
+cmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr, "cal: tk error on '%s': %s\n", cmd, e);
+	return e;
+}
+
+tkcmds(top: ref Tk->Toplevel, a: array of string)
+{
+	for (i := 0; i < len a; i++)
+		cmd(top, a[i]);
+}
+
+isnum(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] < '0' || s[i] > '9')
+			return 0;
+	return 1;
+}
+
+tm2date(t: ref Tm): int
+{
+	if (t == nil)
+		return 19700001;
+	return ymd2date(t.year+1900, t.mon, t.mday);
+}
+
+date2ymd(date: int): (int, int, int)
+{
+	return (date / 10000, (date / 100) % 100, date%100);
+}
+
+ymd2date(y, m, d: int): int
+{
+	return d + m* 100 + y * 10000;
+}
+
+adddays(date, delta: int): int
+{
+	t := ref blanktm;
+	t.mday = date % 100;
+	t.mon = (date / 100) % 100;
+	t.year = (date / 10000) - 1900;
+	t.hour = 12;
+	e := daytime->tm2epoch(t);
+	e += delta * 24 * 60 * 60;
+	t = daytime->gmt(e);
+	if (!validtm(t))
+		return date;
+	return tm2date(t);
+}
+
+incmonth(date: int): int
+{
+	(y,m,d) := date2ymd(date);
+	if (m < 11)
+		m++;
+	else if (y < 2037)
+		(y, m) = (y+1, 0);
+	(n, nil) := monthinfo(m, y);
+	if (d > n)
+		d = n;
+	return ymd2date(y,m,d);
+}
+
+decmonth(date: int): int
+{
+	(y,m,d) := date2ymd(date);
+	if (m > 0)
+		m--;
+	else if (y > 1970)
+		(y, m) = (y-1, 11);
+	(n, nil) := monthinfo(m, y);
+	if (d > n)
+		d = n;
+	return ymd2date(y,m,d);
+}
+
+dmsize := array[] of {
+	31, 28, 31, 30, 31, 30,
+	31, 31, 30, 31, 30, 31
+};
+
+dysize(y: int): int
+{
+	if( (y%4) == 0 && (y % 100 != 0 || y % 400 == 0) )
+		return 366;
+	return 365;
+}
+
+blanktm: Tm;
+
+# return number of days in month and
+# starting day of month/year.
+monthinfo(mon, year: int): (int, int)
+{
+	t  := ref blanktm;
+	t.mday = 1;
+	t.mon = mon;
+	t.year = year - 1900;
+	t = daytime->gmt(daytime->tm2epoch(t));
+	md := dmsize[mon];
+	if (dysize(year) == 366 && t.mon == 1)
+		md++;
+	return (md, t.wday);
+}
+
+X(s: string): string
+{
+	#sys->print("\"%s\"\n", s);
+	if (dict == nil)
+		return s;
+	return dict.xlate(s);
+}
+
+Xa(a: array of string): array of string
+{
+	for (i := 0; i < len a; i++)
+		a[i] = X(a[i]);
+	return a;
+}
+
--- /dev/null
+++ b/appl/wm/clock.b
@@ -1,0 +1,123 @@
+implement Clock;
+
+#
+# Subject to the Lucent Public License 1.02
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Display, Image, Point, Rect: import draw;
+
+include "math.m";
+	math: Math;
+
+include "tk.m";
+include "wmclient.m";
+	wmclient: Wmclient;
+	Window: import wmclient;
+
+include "daytime.m";
+	daytime: Daytime;
+	Tm: import daytime;
+
+Clock: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+hrhand: ref Image;
+minhand: ref Image;
+dots: ref Image;
+back: ref Image;
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	math = load Math Math->PATH;
+	daytime = load Daytime Daytime->PATH;
+	wmclient = load Wmclient Wmclient->PATH;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	wmclient->init();
+
+	w := wmclient->window(ctxt, "clock", Wmclient->Appl);	# Plain?
+	display := w.display;
+	back = display.colormix(Draw->Palebluegreen, Draw->White);
+
+	hrhand = display.newimage(Rect((0,0),(1,1)), Draw->CMAP8, 1, Draw->Darkblue);
+	minhand = display.newimage(Rect((0,0),(1,1)), Draw->CMAP8, 1, Draw->Paleblue);
+	dots = display.newimage(Rect((0,0),(1,1)), Draw->CMAP8, 1, Draw->Blue);
+
+	w.reshape(Rect((0, 0), (100, 100)));
+	w.startinput("ptr" :: nil);
+
+	now := daytime->now();
+	w.onscreen(nil);
+	drawclock(w.image, now);
+
+	ticks := chan of int;
+	spawn timer(ticks, 30*1000);
+	for(;;) alt{
+	ctl := <-w.ctl or
+	ctl = <-w.ctxt.ctl =>
+		w.wmctl(ctl);
+		if(ctl != nil && ctl[0] == '!')
+			drawclock(w.image, now);
+	p := <-w.ctxt.ptr =>
+		w.pointer(*p);
+	<-ticks =>
+		t := daytime->now();
+		if(t != now){
+			now = t;
+			drawclock(w.image, now);
+		}
+	}
+}
+
+ZP := Point(0, 0);
+
+drawclock(screen: ref Image, t: int)
+{
+	if(screen == nil)
+		return;
+	tms := daytime->local(t);
+	anghr := 90-(tms.hour*5 + tms.min/10)*6;
+	angmin := 90-tms.min*6;
+	r := screen.r;
+	c := r.min.add(r.max).div(2);
+	if(r.dx() < r.dy())
+		rad := r.dx();
+	else
+		rad = r.dy();
+	rad /= 2;
+	rad -= 8;
+
+	screen.draw(screen.r, back, nil, ZP);
+	for(i:=0; i<12; i++)
+		screen.fillellipse(circlept(c, rad, i*(360/12)), 2, 2, dots, ZP);
+
+	screen.line(c, circlept(c, (rad*3)/4, angmin), 0, 0, 1, minhand, ZP);
+	screen.line(c, circlept(c, rad/2, anghr), 0, 0, 1, hrhand, ZP);
+
+	screen.flush(Draw->Flushnow);
+}
+
+circlept(c: Point, r: int, degrees: int): Point
+{
+	rad := real degrees * Math->Pi/180.0;
+	c.x += int (math->cos(rad)*real r);
+	c.y -= int (math->sin(rad)*real r);
+	return c;
+}
+
+timer(c: chan of int, ms: int)
+{
+	for(;;){
+		sys->sleep(ms);
+		c <-= 1;
+	}
+}
--- /dev/null
+++ b/appl/wm/coffee.b
@@ -1,0 +1,227 @@
+implement Coffee;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context, Display, Point, Rect, Image, Screen: import draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+Coffee: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+display: ref Display;
+t: ref Toplevel;
+
+NC: con 6;
+
+task_cfg := array[] of {
+	"frame .f",
+	"frame .b",
+	"button .b.Stop -text Stop -command {send cmd stop}",
+	"scale .b.Rate -from 1 -to 10 -orient horizontal"+
+		" -showvalue 0 -command {send cmd rate}",
+	"scale .b.Jitter -from 0 -to 5 -orient horizontal"+
+		" -showvalue 0 -command {send cmd jitter}",
+	"scale .b.Skip -from 0 -to 25 -orient horizontal"+
+		" -showvalue 0 -command {send cmd skip}",
+	".b.Rate set 3",
+	".b.Jitter set 2",
+	".b.Skip set 5",
+	"pack .b.Stop .b.Rate .b.Jitter .b.Skip -side left",
+	"pack .b -anchor w",
+	"pack .f -side bottom -fill both -expand 1",
+};
+
+init(ctxt: ref Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	tkclient->init();
+	if(ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+	display = ctxt.display;
+
+	menubut: chan of string;
+	(t, menubut) = tkclient->toplevel(ctxt, "", "Infernal Coffee", 0);
+
+	cmdch := chan of string;
+	tk->namechan(t, cmdch, "cmd");
+
+	for (i := 0; i < len task_cfg; i++)
+		cmd(t, task_cfg[i]);
+
+	tk->cmd(t, "update");
+	tkclient->startinput(t, "ptr"::"kbd"::nil);
+	tkclient->onscreen(t, nil);
+
+	ctl := chan of (string, int, int);
+	spawn animate(ctl);
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-menubut =>
+		tkclient->wmctl(t, s);
+	press := <-cmdch =>
+		(nil, word) := sys->tokenize(press, " ");
+		case hd word {
+		"stop" or "go" =>
+			ctl <-= (hd word, 0, 0);
+		"rate" or "jitter" or "skip" =>
+			ctl <-= (hd word, int hd tl word, 0);
+		}
+	}
+
+}
+
+animate(ctl: chan of (string, int, int))
+{
+	stopped := 0;
+
+	fill := display.open("/icons/bigdelight.bit");
+	if (fill == nil) {
+		sys->fprint(sys->fildes(2), "coffee: failed to allocate image\n");	
+		exit;
+	}
+
+	c := array[NC] of ref Image;
+	m := array[NC] of ref Image;
+
+	for(i:=0; i<NC; i++){
+		c[i] = display.open("/icons/coffee"+string i+".bit");
+		m[i] = display.open("/icons/coffee"+string i+".mask");
+	if (c[i] == nil || m[i] == nil) {
+		sys->fprint(sys->fildes(2), "coffee: failed to allocate image\n");	
+		exit;
+	}
+	}
+
+	r := Rect((0, 0), (400, 300));
+	buffer := display.newimage(r, t.image.chans, 0, Draw->Black);
+	if (buffer == nil) {
+		sys->fprint(sys->fildes(2), "coffee: failed to allocate image\n");	
+		exit;
+	}
+	cmd(t, "panel .f.p -bd 3 -relief flat");
+	cmd(t, "pack .f.p -fill both -expand 1");
+	cmd(t, "update");
+	# org := buffer.r.min;
+	tk->putimage(t, ".f.p", buffer, nil);
+
+	rate := 3;
+	jitter := 2;
+	skip := 5;
+
+	i = 0;
+	for(k:=0; ; k++){
+		sys->sleep(1);
+		if(k%25 > 25-skip)
+			i -= rate;
+		else
+			i += rate;
+		buffer.draw(buffer.clipr, fill, nil, fill.r.min);
+		center := buffer.r.max.div(2);
+		for(j:=0; j<NC; j++){
+			(sin, cos) := sincos(i+j*(360/NC));
+			x := (sin*150)/1000 + jitter*(k%5);
+			y := (cos*100)/1000 + jitter*(k%5);
+			p0 := center.add((x-c[j].r.dx()/2, y-c[j].r.dy()/2));
+			buffer.draw(c[j].r.addpt(p0), c[j], m[j], (0,0));
+			if(j & 1)	# be nice from time to time
+				sys->sleep(0);
+		}
+		tk->cmd(t, ".f.p dirty; update");
+		sys->sleep(5);
+		alt{
+		(cmd, i0, i1) := <-ctl =>
+	Pause:
+			for(;;){
+				case cmd{
+				"go" =>
+					if(stopped){
+						tk->cmd(t, ".b.Stop configure -text Stop -command {send cmd stop}");
+						tk->cmd(t, "update");
+						stopped = 0;
+					}
+					break Pause;
+				"stop" =>
+					if(!stopped){
+						tk->cmd(t, ".b.Stop configure -text { Go } -command {send cmd go}");
+						tk->cmd(t, "update");
+						stopped = 1;
+					}
+				"rate" =>
+					rate = i0;
+					if(stopped == 0)
+						break Pause;
+				"jitter" =>
+					jitter = i0;
+					if(stopped == 0)
+						break Pause;
+				"skip" =>
+					skip = i0;
+					if(stopped == 0)
+						break Pause;
+				}
+				(cmd, i0, i1) = <-ctl;
+			}
+		* =>
+			;
+		}
+	}
+}
+
+sintab := array[] of {
+	0000, 0017, 0035, 0052, 0070, 0087, 0105, 0122, 0139, 0156,
+	0174, 0191, 0208, 0225, 0242, 0259, 0276, 0292, 0309, 0326,
+	0342, 0358, 0375, 0391, 0407, 0423, 0438, 0454, 0469, 0485,
+	0500, 0515, 0530, 0545, 0559, 0574, 0588, 0602, 0616, 0629,
+	0643, 0656, 0669, 0682, 0695, 0707, 0719, 0731, 0743, 0755,
+	0766, 0777, 0788, 0799, 0809, 0819, 0829, 0839, 0848, 0857,
+	0866, 0875, 0883, 0891, 0899, 0906, 0914, 0921, 0927, 0934,
+	0940, 0946, 0951, 0956, 0961, 0966, 0970, 0974, 0978, 0982,
+	0985, 0988, 0990, 0993, 0995, 0996, 0998, 0999, 0999, 1000,
+	1000, };
+
+sincos(a: int): (int, int)
+{
+	a %= 360;
+	if(a < 0)
+		a += 360;
+
+	if(a <= 90)
+		return (sintab[a], sintab[90-a]);
+	if(a <= 180)
+		return (sintab[180-a], -sintab[a-90]);
+	if(a <= 270)
+		return (-sintab[a-180], -sintab[270-a]);
+	return (-sintab[360-a], sintab[a-270]);
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+	r := tk->cmd(win, s);
+	if (len r > 0 && r[0] == '!') {
+		sys->print("error executing '%s': %s\n", s, r[1:]);
+	}
+	return r;
+}
--- /dev/null
+++ b/appl/wm/collide.b
@@ -1,0 +1,2180 @@
+#
+#	initially generated by c2l
+#
+
+implement Collide;
+
+include "draw.m";
+	draw: Draw;
+	Display, Image: import draw;
+
+Collide: module
+{
+	init: fn(nil: ref Draw->Context, argl: list of string);
+};
+
+include "sys.m";
+	sys: Sys;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "math.m";
+	maths: Math;
+include "rand.m";
+	rand: Rand;
+include "daytime.m";
+	daytime: Daytime;
+include "bufio.m";
+include "arg.m";
+	arg: Arg;
+include "math/polyhedra.m";
+	polyhedra: Polyhedra;
+
+init(ctxt: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	maths = load Math Math->PATH;
+	rand = load Rand Rand->PATH;
+	arg = load Arg Arg->PATH;
+	daytime = load Daytime Daytime->PATH;
+	main(ctxt, argl);
+}
+
+π: con Math->Pi;
+∞: con real (1<<30);
+ε: con 0.001;
+√2: con 1.4142135623730950488016887242096980785696718753769486732;
+
+M1: con 1.0;
+M2: con 1.0;
+E: con 1.0;	# coefficient of restitution/elasticity
+
+COLLIDE, REFLECT: con 1<<iota;
+
+MAXX, MAXY: con 512;
+
+RDisp: ref Draw->Image;
+black, white, red: ref Draw->Image;
+display: ref Draw->Display;
+toplev: ref Toplevel;
+
+Vector: adt{
+	x: real;
+	y: real;
+	z: real;
+};
+
+Line: adt{
+	a: Vector;
+	d: Vector;		# normalized
+};
+
+Plane: adt{
+	id: int;
+	n: Vector;		# normalized
+	d: real;
+	min: Vector;
+	max: Vector;
+	far: Vector;
+	v: array of Vector;
+};
+
+Object: adt{
+	id: int;
+	poly: ref Polyhedra->Polyhedron;	# if any
+	c: ref Draw->Image;		#  colour 
+	cb: ref Draw->Image;	#  border colour
+	l: ref Line;				#  initial point and direction 
+	p: Vector;				#  current position
+	rp: Vector;			# position after reflection
+	cp: Vector;			# any collision point
+	rt: real;				# time to reflection
+	ct: real;				# time to collision
+	plane: ref Plane;		# reflecting off
+	pmask: int;			# plane mask
+	obj: cyclic ref Object;	# colliding with
+	v: real;				#  speed
+	ω: real;				# speed of rotation
+	roll: real;				# roll
+	pitch: real;			# pitch
+	yaw: real;				# yaw
+	todo: int;				# work to do
+};
+
+planes: list of ref Plane;
+
+V0: con Vector(real 0, real 0, real 0);
+VZ: con Vector(0.0, 0.0, 1.0);
+
+far: Vector;
+
+DOCIRCLE: con 1;
+POLY, FILLPOLY, CIRCLE, FILLCIRCLE, ELLIPSE, FILLELLIPSE: con iota;
+
+#
+#  final object is centred on (0, 0, -objd)
+#  viewer is at origin looking along (0 0 -1)
+#  
+maxx, maxy: int;
+
+SCRW: con 320;	#  screen width
+SCRH: con 240;		# screen height
+
+frac := 0.5;	# % of screen for cube
+front := 0.5;	# % of cube in front of screen
+hpar := 0.0;	# horizontal parallax
+fov := -1.0;	# field of view : 0 for parallel projection, -1 for unspecified
+objd := 500.0;	# eye to middle of cube
+cubd := 100.0;	# half side of cube
+icubd: real;	# half side of inner cube
+icubd2: real;	# square of above
+eyed := 32.0;	# half eye to eye
+trkd := 5.0;	# side/diameter of object
+trkd2: real;	# square of above
+rpy := 0;
+roll := 0.0;		# z
+pitch := 0.0;	# y
+yaw := 0.0;	# x
+
+scrd, objD, scrD: real;	# screen distance
+left := 0;		# left or right eye
+sx, sy, sz: real;	#  screen scale factors 
+sf: real;		#  perspective scale factor 
+fbpar: real;	#  -1 for front of cube, 1 for back
+vf := 1.0;		# current velocity factor
+
+cmin, cmax: Vector;	# cube extents
+
+#  special transformation matrix without roll, pitch, yaw 
+#  this is needed so that spheres can be drawn as circles 
+mod0 := array[4] of array of real;
+
+stereo := 0;	# stereopsis
+
+SPHERE, ELLIPSOID, CUBE, POLYHEDRON: con iota;
+surr := CUBE;	# surround
+
+poly := 0;		# show polyhedra
+flat: int;		# objects in one plane
+projx: int;		# strange projection
+
+# ellipse parameters
+ef: Vector = (1.0, 0.8, 1.0);
+e2: Vector;
+
+# objects
+nobjs: int;
+objs: array of ref Object;
+me: ref Object;
+
+# circle drawing
+NC: con 72;
+cost, sint: array of real;
+
+# polyhedra
+polys: ref Polyhedra->Polyhedron;
+npolys: int;
+polyh: ref Polyhedra->Polyhedron;
+
+rgba(r: int, g: int, b: int, α: int): ref Image
+{
+	c := draw->setalpha((r<<24)|(g<<16)|(b<<8), α);
+	return display.newimage(((0, 0), (1, 1)), display.image.chans, 1, c);
+}
+
+random(a: int, b: int): int
+{
+	return a+rand->rand(b-a+1);
+}
+
+urand(): real
+{
+	M: con 1000;
+	return real random(0, M)/real M;
+}
+
+randomr(a: real, b: real): real
+{
+	return a+urand()*(b-a);
+}
+
+randomc(): ref Image
+{
+	r, g, b: int;
+
+	do{
+		r = random(0, 255);
+		g = random(0, 255);
+		b = random(0, 255);
+	}while(r+g+b < 384);
+	return rgba(r, g, b, 255);
+}
+
+randomv(a: real, b: real): Vector
+{
+	x := randomr(a, b);
+	y := randomr(a, b);
+	if(flat)
+		return (x, y, (a+b)/2.0);
+	return (x, y, randomr(a, b));
+}
+
+randomd(): Vector
+{
+	M: con 1000.0;
+	v := randomv(-M, M);
+	while(vlen(v) == 0.0)
+		v = randomv(-M, M);
+	return vnorm(v);
+}
+
+randomp(min: real, max: real): Vector
+{
+	case(surr){
+		SPHERE =>
+			return vmul(randomd(), max*maths->sqrt(urand()));
+		ELLIPSOID =>
+			return vmul(randomd(), max*vmin(ef)*maths->sqrt(urand()));
+		CUBE =>
+			return randomv(min, max);
+		* =>
+			v := randomv(min, max);
+			while(outside3(v, cmin, cmax))
+				v = randomv(min, max);
+			return v;
+	}
+}
+
+det(a: real, b: real, c: real, d: real): real
+{
+	return a*d-b*c;
+}
+
+simeq(a: real, b: real, c: real, d: real, e: real, f: real): (real, real)
+{
+	de := det(a, b, c, d);
+	return (det(e, b, f, d)/de, det(a, e, c, f)/de);
+}
+
+cksimeq(a: real, b: real, c: real, d: real, e: real, f: real): (int, real, real)
+{
+	ade := de := det(a, b, c, d);
+	if(ade < 0.0)
+		ade = -ade;
+	if(ade < ε)
+		return (0, 0.0, 0.0);
+	return (1, det(e, b, f, d)/de, det(a, e, c, f)/de);
+}
+
+ostring(o: ref Object): string
+{
+	return lstring(o.l) + "+" + vstring(o.p) + "+" + string o.v;
+}
+
+pstring(p: ref Plane): string
+{
+	return vstring(p.n) + "=" + string p.d;
+}
+
+lstring(l: ref Line): string
+{
+	return vstring(l.a) + "->" + vstring(l.d);
+}
+
+vstring(v: Vector): string
+{
+	return "(" + string v.x + " " + string v.y + " " + string v.z + ")";
+}
+
+vpt(x: real, y: real, z: real): Vector
+{
+	p: Vector;
+
+	p.x = x;
+	p.y = y;
+	p.z = z;
+	return p;
+}
+
+vdot(v1: Vector, v2: Vector): real
+{
+	return v1.x*v2.x+v1.y*v2.y+v1.z*v2.z;
+}
+
+vcross(v1: Vector, v2: Vector): Vector
+{
+	v: Vector;
+
+	v.x = v1.y*v2.z-v1.z*v2.y;
+	v.y = v1.z*v2.x-v1.x*v2.z;
+	v.z = v1.x*v2.y-v1.y*v2.x;
+	return v;
+}
+
+vadd(v1: Vector, v2: Vector): Vector
+{
+	v: Vector;
+
+	v.x = v1.x+v2.x;
+	v.y = v1.y+v2.y;
+	v.z = v1.z+v2.z;
+	return v;
+}
+
+vsub(v1: Vector, v2: Vector): Vector
+{
+	v: Vector;
+
+	v.x = v1.x-v2.x;
+	v.y = v1.y-v2.y;
+	v.z = v1.z-v2.z;
+	return v;
+}
+
+vmul(v1: Vector, s: real): Vector
+{
+	v: Vector;
+
+	v.x = s*v1.x;
+	v.y = s*v1.y;
+	v.z = s*v1.z;
+	return v;
+}
+
+vdiv(v1: Vector, s: real): Vector
+{
+	v: Vector;
+
+	v.x = v1.x/s;
+	v.y = v1.y/s;
+	v.z = v1.z/s;
+	return v;
+}
+
+vlen(v: Vector): real
+{
+	return maths->sqrt(vdot(v, v));
+}
+
+vlen2(v: Vector): (real, real)
+{
+	d2 := vdot(v, v);
+	d := maths->sqrt(d2);
+	return (d, d2);
+}
+
+vnorm(v: Vector): Vector
+{
+	d := maths->sqrt(vdot(v, v));
+	if(d == 0.0)
+		return v;
+	return vmul(v, real 1/d);
+}
+
+vnorm2(v: Vector): (real, Vector)
+{
+	d := maths->sqrt(vdot(v, v));
+	if(d == 0.0)
+		return (0.0, VZ);
+	return (d, vmul(v, real 1/d));
+}
+
+clip(x: real, d: real): real
+{
+	if(x < -d)
+		x = -d;
+	if(x > d)
+		x = d;
+	return x;
+}
+
+vclip(v: Vector, d: real): Vector
+{
+	c: Vector;
+
+	c.x = clip(v.x, d);
+	c.y = clip(v.y, d);
+	c.z = clip(v.z, d);
+	return c;
+}
+
+vout(v1: Vector, v2: Vector): int
+{
+	v := vsub(v2, v1);
+	return v.x < 0.0 || v.y < 0.0 || v.z < 0.0;
+}
+
+vmin(v: Vector): real
+{
+	m := v.x;
+	if(v.y < m)
+		m = v.y;
+	if(v.z < m)
+		m = v.z;
+	return m;
+}
+
+vvmul(v1: Vector, v2: Vector): Vector
+{
+	v: Vector;
+
+	v.x = v1.x*v2.x;
+	v.y = v1.y*v2.y;
+	v.z = v1.z*v2.z;
+	return v;
+}
+
+vvdiv(v1: Vector, v2: Vector): Vector
+{
+	v: Vector;
+
+	v.x = v1.x/v2.x;
+	v.y = v1.y/v2.y;
+	v.z = v1.z/v2.z;
+	return v;
+}
+
+vmuldiv(v1: Vector, v2: Vector, v3: Vector): real
+{
+	return vdot(vvdiv(v1, v3), v2);
+}
+
+newp(id: int, a: real, b: real, c: real, d: real, m: real, v: array of Vector)
+{
+	p := ref Plane;
+	p.id = id;
+	p.n = (a, b, c);
+	p.d = d;
+	m += ε;
+	p.min = (-m, -m, -m);
+	p.max = (+m, +m, +m);
+	p.v = v;
+	if(v != nil){
+		p.min = (∞, ∞, ∞);
+		p.max = (-∞, -∞, -∞);
+		for(i := 0; i < len v; i++){
+			vtx := v[i];
+			if(vtx.x < p.min.x)
+				p.min.x = vtx.x;
+			if(vtx.y < p.min.y)
+				p.min.y = vtx.y;
+			if(vtx.z < p.min.z)
+				p.min.z = vtx.z;
+			if(vtx.x > p.max.x)
+				p.max.x = vtx.x;
+			if(vtx.y > p.max.y)
+				p.max.y = vtx.y;
+			if(vtx.z > p.max.z)
+				p.max.z = vtx.z;
+		}
+		(x, y, z) := p.far = vmul(p.max, 2.0);
+		if(a != 0.0)
+			p.far.x = (d-b*y-c*z)/a;
+		else if(b != 0.0)
+			p.far.y = (d-c*z-a*x)/b;
+		else if(c != 0.0)
+			p.far.z = (d-a*x-b*y)/c;
+		else
+			fatal("null plane");
+	}
+	planes = p :: planes;
+}
+
+pinit()
+{
+	case(surr){
+		SPHERE or
+		ELLIPSOID =>
+			newp(0, 0.0, 0.0, 1.0, ∞, ∞, nil);
+		CUBE =>
+			newp(0, 1.0, 0.0, 0.0, -icubd, icubd, nil);
+			newp(1, 1.0, 0.0, 0.0, +icubd, icubd, nil);
+			newp(2, 0.0, 1.0, 0.0, -icubd, icubd, nil);
+			newp(3, 0.0, 1.0, 0.0, +icubd, icubd, nil);
+			newp(4, 0.0, 0.0, 1.0, -icubd, icubd, nil);
+			newp(5, 0.0, 0.0, 1.0, +icubd, icubd, nil);
+		* =>
+			p := polyh;
+			F := p.F;
+			v := p.v;
+			f := p.f;
+			fv := p.fv;
+			d := 0.0;
+			for(i := 0; i < F; i++){
+				n := vnorm(f[i]);
+				dn := vmul(n, cubd-icubd);
+				fvi := fv[i];
+				m := fvi[0];
+				av := array[m] of Vector;
+				for(j := 0; j < m; j++){
+					av[j] = vtx := vsub(vmul(v[fvi[j+1]], 2.0*cubd), dn);
+					d += vdot(n, vtx);
+				}
+				d /= real m;
+				newp(i, n.x, n.y, n.z, d, 0.0, av);
+			}
+	}
+}
+
+inside(v: Vector, vmin: Vector, vmax: Vector): int
+{
+	return !vout(vmin, v) && !vout(v, vmax);
+}
+
+inside2(v: Vector, p: ref Plane): int
+{
+	h := 0;
+	pt := p.far;
+	vs := p.v;
+	n := len p.v;
+	j := n-1;
+	for(i := 0; i < n; i++){
+		(ok, λ, μ) := cksimeq(vs[j].x-vs[i].x, v.x-pt.x, vs[j].y-vs[i].y, v.y-pt.y, v.x-vs[i].x, v.y-vs[i].y);
+		if(!ok)
+		(ok, λ, μ) = cksimeq(vs[j].y-vs[i].y, v.y-pt.y, vs[j].z-vs[i].z, v.z-pt.z, v.y-vs[i].y, v.z-vs[i].z);
+		if(!ok)
+		(ok, λ, μ) = cksimeq(vs[j].z-vs[i].z, v.z-pt.z, vs[j].x-vs[i].x, v.x-pt.x, v.z-vs[i].z, v.x-vs[i].x);
+		if(ok && μ >= 0.0 && λ >= 0.0 && λ < 1.0)
+			h++;
+		j = i;
+	}
+	return h&1;
+}
+
+inside3(v: Vector, lp: list of ref Plane): int
+{
+	h := 0;
+	l := ref Line;
+	l.a = v;
+	l.d = vnorm(vsub(far, v));
+	for( ; lp != nil; lp = tl lp){
+		(ok, nil, nil) := intersect(l, hd lp);
+		if(ok)
+			h++;
+	}
+	return h&1;
+}
+
+# outside of a face
+outside2(v: Vector, p: ref Plane): int
+{
+	if(surr == CUBE)
+		return vout(p.min, v) || vout(v, p.max);
+	else
+		return !inside2(v, p);
+}
+
+# outside of a polyhedron
+outside3(v: Vector, vmin: Vector, vmax: Vector): int
+{
+	case(surr){
+		SPHERE =>
+			return vout(vmin, v) || vout(v, vmax) || vdot(v, v) > icubd2 ;
+		ELLIPSOID =>
+			return vout(vmin, v) || vout(v, vmax) || vmuldiv(v, v, e2) > 1.0;
+		CUBE =>
+			return vout(vmin, v) || vout(v, vmax);
+		* =>
+			return !inside3(v, planes);
+	}
+}
+
+intersect(l: ref Line, p: ref Plane): (int, real, Vector)
+{
+	m := vdot(p.n, l.d);
+	if(m == real 0)
+		return (0, real 0, V0);
+	c := vdot(p.n, l.a);
+	λ := (p.d-c)/m;
+	if(λ < real 0)
+		return (0, λ, V0);
+	pt := vadd(l.a, vmul(l.d, λ));
+	if(outside2(pt, p))
+		return (0, λ, pt);
+	return (1, λ, pt);
+}
+
+reflection(tr: ref Object, lp: list of ref Plane)
+{
+	ok: int;
+	λ: real;
+
+	l := tr.l;
+	if(surr == SPHERE){
+		(ok, λ) = quadratic(1.0, 2.0*vdot(l.a, l.d), vdot(l.a, l.a)-icubd2);
+		if(!ok || λ < 0.0)
+			fatal("no sphere intersections");
+		tr.rp = vadd(l.a, vmul(l.d, λ));
+		tr.plane = hd lp;	# anything
+	}
+	else if(surr == ELLIPSOID){
+		(ok, λ) = quadratic(vmuldiv(l.d, l.d, e2), 2.0*vmuldiv(l.a, l.d, e2), vmuldiv(l.a, l.a, e2)-1.0);
+		if(!ok || λ < 0.0)
+			fatal("no ellipsoid intersections");
+		tr.rp = vadd(l.a, vmul(l.d, λ));
+		tr.plane = hd lp;	# anything
+	}
+	else{
+		p: ref Plane;
+		pt := V0;
+		λ = ∞;
+		for( ; lp != nil; lp = tl lp){
+			p0 := hd lp;
+			if((1<<p0.id)&tr.pmask)
+				continue;
+			(ok0, λ0, pt0) := intersect(l, p0);
+			if(ok0 && λ0 < λ){
+				λ = λ0;
+				p = p0;
+				pt = pt0;
+			}
+		}
+		if(λ == ∞)
+			fatal("no intersections");
+		tr.rp = pt;
+		tr.plane = p;
+	}
+	if(tr.v == 0.0)
+		tr.rt = ∞;
+	else
+		tr.rt = λ/tr.v;
+}
+
+reflect(tr: ref Object)
+{
+	l := tr.l;
+	if(surr == SPHERE)
+		n := vdiv(tr.rp, -icubd);
+	else if(surr == ELLIPSOID)
+		n = vnorm(vdiv(vvdiv(tr.rp, e2), -1.0));
+	else
+		n = tr.plane.n;
+	tr.l.a = tr.rp;
+	tr.l.d = vnorm(vsub(l.d, vmul(n, 2.0*vdot(n, l.d))));
+}
+
+impact(u2: real): (real, real)
+{
+	# u1 == 0
+	return simeq(M1, M2, -1.0, 1.0, M2*u2, -E*u2);
+}
+
+collision(t1: ref Object, t2: ref Object): (int, real, Vector, Vector)
+{
+	# stop t2
+	(v3, f) := vnorm2(vsub(vmul(t1.l.d, t1.v), vmul(t2.l.d, t2.v)));
+	if(v3 == 0.0)
+		return (0, 0.0, V0, V0);
+	ab := vsub(t2.p, t1.p);
+	(d, d2) := vlen2(ab);
+	cos := vdot(f, ab)/d;
+	cos2 := cos*cos;
+	if(cos < 0.0 || (disc := trkd2 - d2*(1.0-cos2)) < 0.0)
+		return (0, 0.0, V0, V0);
+	s := d*cos - maths->sqrt(disc);
+	t := s/v3;
+	s1 := t1.v*t;
+	s2 := t2.v*t;
+	cp1 := vadd(t1.p, vmul(t1.l.d, s1));
+	if(outside3(cp1, cmin, cmax))
+		return (0, 0.0, V0, V0);
+	cp2 := vadd(t2.p, vmul(t2.l.d, s2));
+	if(outside3(cp2, cmin, cmax))
+		return (0, 0.0, V0, V0);
+	return (1, t, cp1, cp2);
+}
+
+collisions(tr: ref Object)
+{
+	mincp1, mincp2: Vector;
+
+	n := nobjs;
+	t := objs;
+	tr0 := tr.obj;
+	mint := tr.ct;
+	tr1: ref Object;
+	for(i := 0; i < n; i++){
+		if((tr3 := t[i]) == tr || tr3 == tr0)
+			continue;
+		(c, tm, cp1, cp2) := collision(tr, tr3);
+		if(c && tm < mint && tm < tr3.ct){
+			mint = tm;
+			tr1 = tr3;
+			mincp1 = cp1;
+			mincp2 = cp2;
+		}
+	}
+	if(tr1 != nil){
+		tr.ct = mint;
+		tr1.ct = mint;
+		tr.obj = tr1;
+		tr2 := tr1.obj;
+		tr1.obj  = tr;
+		tr.cp = mincp1;
+		tr1.cp = mincp2;
+		zerot(tr0, COLLIDE, 0);
+		zerot(tr2, COLLIDE, 0);
+		if(tr0 != nil && tr0 != tr2)
+			collisions(tr0);
+		if(tr2 != nil)
+			collisions(tr2);
+	}
+}
+
+collide(t1: ref Object, t2: ref Object)
+{
+	# stop t2
+	ov := vmul(t2.l.d, t2.v);
+	(v3, f) := vnorm2(vsub(vmul(t1.l.d, t1.v), ov));
+	ab := vsub(t2.cp, t1.cp);
+	α := vdot(f, ab)/vdot(ab, ab);
+	abr := vsub(f, vmul(ab, α));
+	(v2, v1) := impact(α*v3);
+	t1.l.a = t1.cp;
+	t2.l.a = t2.cp;
+	dir1 := vadd(vmul(ab, v1), vmul(abr, v3));
+	dir2 := vmul(ab, v2);
+	# start t2
+	(t1.v, t1.l.d) = vnorm2(vadd(dir1, ov));
+	(t2.v, t2.l.d) = vnorm2(vadd(dir2, ov));
+}
+
+deg2rad(d: real): real
+{
+	return π*d/180.0;
+}
+
+rad2deg(r: real): real
+{
+	return 180.0*r/π;
+}
+
+rp2d(r: real, p: real): Vector
+{
+	r = deg2rad(r);
+	cr := maths->cos(r);
+	sr := maths->sin(r);
+	p = deg2rad(p);
+	cp := maths->cos(p);
+	sp := maths->sin(p);
+	return (cr*cp, sr*cp, sp);
+}
+
+d2rp(v: Vector): (real, real)
+{
+	r := maths->atan2(v.y, v.x);
+	p := maths->asin(v.z);
+	return (rad2deg(r), rad2deg(p));
+}
+
+collideω(t1: ref Object, t2: ref Object)
+{
+	d1 := rp2d(t1.roll, t1.pitch);
+	d2 := rp2d(t2.roll, t2.pitch);
+	oω := vmul(d2, t2.ω);
+	(ω3, f) := vnorm2(vsub(vmul(d1, t1.ω), oω));
+	ab := vsub(t2.cp, t1.cp);
+	α := vdot(f, ab)/vdot(ab, ab);
+	abr := vsub(f, vmul(ab, α));
+	(ω2, ω1) := impact(α*ω3);
+	dir1 := vadd(vmul(ab, ω1), vmul(abr, ω3));
+	dir2 := vmul(ab, ω2);
+	(t1.ω, d1) = vnorm2(vadd(dir1, oω));
+	(t2.ω, d2) = vnorm2(vadd(dir2, oω));
+	(t1.roll, t1.pitch) = d2rp(d1);
+	(t2.roll, t2.pitch) = d2rp(d2);
+}
+
+plane(p1: Vector, p2: Vector, p3: Vector): (Vector, real)
+{
+	n := vnorm(vcross(vsub(p2, p1), vsub(p3, p1)));
+	d := vdot(n, p1);
+	return (n, d);
+}
+
+#  angle subtended by the eyes at p in minutes 
+angle(p: Vector): real
+{
+	l, r: Vector;
+
+	#  left  eye at (-eyed, 0, 0)
+	#  right eye at (+eyed, 0, 0)
+	#      
+	l = p;
+	l.x += eyed;
+	r = p;
+	r.x -= eyed;
+	return real 60*(real 180*maths->acos(vdot(l, r)/(maths->sqrt(vdot(l, l))*maths->sqrt(vdot(r, r))))/π);
+}
+
+#  given coordinates relative to centre of cube 
+disparity(p: Vector, b: Vector): real
+{
+	p.z -= objd;
+	b.z -= objd;
+	return angle(p)-angle(b);
+}
+
+#  rotation about any of the axes 
+#  rotate(theta, &x, &y, &z) for x-axis
+#    rotate(theta, &y, &z, &x) for y-axis
+#    rotate(theta, &z, &x, &y) for z-axis
+#  
+rotate(theta: int, x: real, y: real, z: real): (real, real, real)
+{
+	a := π*real theta/real 180;
+	c := maths->cos(a);
+	s := maths->sin(a);
+	oy := y;
+	oz := z;
+	y = c*oy-s*oz;
+	z = c*oz+s*oy;
+	return (x, y, z);
+}
+
+#  solve the quadratic ax^2 + bx + c = 0, returning the larger root
+#  * (a > 0)
+#  
+quadratic(a: real, b: real, c: real): (int, real)
+{
+	d := b*b-real 4*a*c;
+	if(d < real 0)
+		return (0, 0.0);	#  no real roots 
+	x := (maths->sqrt(d)-b)/(real 2*a);
+	return (1, x);
+}
+
+#  calculate the values of objD, scrD given the required parallax 
+dopar()
+{
+	a := real 1;
+	b, c: real;
+	f := real 2*front-real 1;
+	x: real;
+	s: int;
+	w := sx*real SCRW;
+	ok: int;
+
+	if(hpar == 0.0){	#  natural parallax 
+		objD = objd;
+		scrD = scrd;
+		return;
+	}
+	if(fbpar < f)
+		s = -1;
+	else
+		s = 1;
+	if(fbpar == f)
+		fatal("parallax value is zero at screen distance");
+	b = (fbpar+f)*cubd-(fbpar-f)*w*eyed*real s*frac/hpar;
+	c = fbpar*f*cubd*cubd;
+	(ok, x) = quadratic(a, b, c);
+	if(ok){
+		objD = x;
+		scrD = x+f*cubd;
+		if(objD > real 0 && scrD > real 0)
+			return;
+	}
+	fatal("unachievable parallax value");
+}
+
+#  update graphics parameters 
+update(init: int)
+{
+	if(fov != real 0){
+		if(objd == real 0)
+			fov = 180.0;
+		else
+			fov = real 2*(real 180*maths->atan(cubd/(frac*objd))/π);
+	}
+	scrd = objd+(real 2*front-real 1)*cubd;
+	if(init){
+		if(objd < ε)
+			objd = ε;
+		if(fov != real 0)
+			sf = real (1<<2)*cubd/(objd*frac);
+		else
+			sf = cubd/frac;
+	}
+	# dopar();
+	domats();
+}
+
+fovtodist()
+{
+	if(fov != real 0)
+		objd = cubd/(frac*maths->tan(π*(fov/real 2)/real 180));
+}
+
+getpolys()
+{
+	(n, p, b) := polyhedra->scanpolyhedra("/lib/polyhedra");
+	polyhedra->getpolyhedra(p, b);
+	polys = p;
+	npolys = n;
+	do{
+		for(i := 0; i < p.V; i++)
+			p.v[i] = vmul(p.v[i], 0.5);
+		for(i = 0; i < p.F; i++)
+			p.f[i] = vmul(p.f[i], 0.5);
+		p = p.nxt;
+	}while(p != polys);
+}
+
+randpoly(p: ref Polyhedra->Polyhedron, n: int): ref Polyhedra->Polyhedron
+{
+	r := random(0, n-1);
+	for( ; --r >= 0; p = p.nxt)
+		;
+	return p;
+}
+
+drawpoly(p: ref Polyhedra->Polyhedron, typex: int)
+{
+	# V := p.V;
+	F := p.F;
+	v := p.v;
+	# f := p.f;
+	fv := p.fv;
+	for(i := 0; i < F; i++){
+		fvi := fv[i];
+		n := fvi[0];
+		m_begin(typex, n);
+		for(j := 0; j < n; j++){
+			vtx := v[fvi[j+1]];
+			m_vertex(vtx.x, vtx.y, vtx.z);
+		}
+		m_end();
+	}
+}
+
+#  objects with unit sides/diameter 
+H: con 0.5;
+
+square(typex: int)
+{
+	m_begin(typex, 4);
+	m_vertex(-H, -H, 0.0);
+	m_vertex(-H, +H, 0.0);
+	m_vertex(+H, +H, 0.0);
+	m_vertex(+H, -H, 0.0);
+	m_end();
+}
+
+diamond(typex: int)
+{
+	m_pushmatrix();
+	m_rotatez(45.0);
+	square(typex);
+	m_popmatrix();
+}
+
+circleinit()
+{
+	i: int;
+	a := 0.0;
+	cost = array[NC] of real;
+	sint = array[NC] of real;
+	for(i = 0; i < NC; i++){
+		cost[i] = H*maths->cos(a);
+		sint[i] = H*maths->sin(a);
+		a += (2.0*π)/real NC;
+	}
+}
+
+circle(typex: int)
+{
+	i: int;
+
+	if(DOCIRCLE){
+		m_begin(typex, 2);
+		m_circle(0.0, 0.0, 0.0, 0.5);
+		m_end();
+		return;
+	}
+	else{
+		m_begin(typex, NC);
+		for(i = 0; i < NC; i++)
+			m_vertex(cost[i], sint[i], 0.0);
+		m_end();
+	}
+}
+
+ellipse(typex: int)
+{
+	m_begin(typex, 4);
+	m_ellipse(0.0, 0.0, 0.0, vmul(ef, 0.5));
+	m_end();
+}
+
+hexahedron(typex: int)
+{
+	i, j, k: int;
+	V := array[8] of {
+		array[3] of {
+			-H, -H, -H,
+		},
+		array[3] of {
+			-H, -H, +H,
+		},
+		array[3] of {
+			-H, +H, -H,
+		},
+		array[3] of {
+			-H, +H, +H,
+		},
+		array[3] of {
+			+H, -H, -H,
+		},
+		array[3] of {
+			+H, -H, +H,
+		},
+		array[3] of {
+			+H, +H, -H,
+		},
+		array[3] of {
+			+H, +H, +H,
+		},
+	};
+	F := array[6] of {
+		array[4] of {
+			0, 4, 6, 2,
+		},
+		array[4] of {
+			0, 4, 5, 1,
+		},
+		array[4] of {
+			0, 1, 3, 2,
+		},
+		array[4] of {
+			1, 5, 7, 3,
+		},
+		array[4] of {
+			2, 6, 7, 3,
+		},
+		array[4] of {
+			4, 5, 7, 6,
+		},
+	};
+
+	for(i = 0; i < 6; i++){
+		m_begin(typex, 4);
+		for(j = 0; j < 4; j++){
+			k = F[i][j];
+			m_vertex(V[k][0], V[k][1], V[k][2]);
+		}
+		m_end();
+	}
+}
+
+zerot(tr: ref Object, zero: int, note: int)
+{
+	if(tr != nil){
+		if(zero&REFLECT){
+			tr.rt = ∞;
+			tr.plane = nil;
+		}
+		if(zero&COLLIDE){
+			tr.ct = ∞;
+			tr.obj  = nil;
+		}
+		if(note)
+			tr.todo = zero;
+	}
+}
+
+newobj(t: array of ref Object, n: int, vel: int, velf: real): ref Object
+{
+	tr: ref Object;
+	p1: Vector;
+	again: int;
+
+	d := icubd-1.0;
+	cnt := 1024;
+	do{
+		p1 = randomp(-d, d);
+		again = 0;
+		for(i := 0; i < n; i++){
+			(nil, d2) := vlen2(vsub(t[i].p, p1));
+			if(d2 <= trkd2){
+				again = 1;
+				break;
+			}
+		}
+		cnt--;
+	}while(again && cnt > 0);
+	if(again)
+		return nil;
+	# p2 := randomp(-d, d);
+	p21 := randomd();
+	tr = ref Object;
+	tr.id = n;
+	tr.poly = nil;
+	if(poly){
+		if(n == 0)
+			tr.poly = randpoly(polys, npolys);
+		else
+			tr.poly = t[0].poly;
+	}
+	tr.c = randomc();
+	tr.cb = tr.c;	# randomc();
+	if(vel)
+		tr.v = vf*velf*randomr(0.5, 2.0);
+	else
+		tr.v = 0.0;
+	tr.ω = vf*randomr(1.0, 10.0);
+	tr.roll = randomr(0.0, 360.0);
+	tr.pitch = randomr(0.0, 360.0);
+	tr.yaw = randomr(0.0, 360.0);
+	tr.l = ref Line(p1, vnorm(p21));
+	tr.p = p1;
+	tr.todo = 0;
+	zerot(tr, REFLECT|COLLIDE, 0);
+	tr.pmask = 0;
+	reflection(tr, planes);
+	return tr;
+}
+
+objinit(m: int, v: int)
+{
+	velf := real m/real v;
+	p := nobjs;
+	n := p+m;
+	v += p;
+	t := array[n] of ref Object;
+	for(i := 0; i < p; i++)
+		t[i] = objs[i];
+	for(i = p; i < n; i++){
+		t[i] = newobj(t, i, i < v, velf);
+		if(t[i] == nil)
+			return;
+	}
+	sort(t, n);
+	nobjs = n;
+	objs = t;
+	for(i = p; i < n; i++)
+		collisions(t[i]);
+}
+
+zobj: Object;
+
+newo(n: int, p: Vector, c: ref Draw->Image): ref Object
+{
+	o := ref Object;
+	*o = zobj;
+	o.id = n;
+	o.c = o.cb = c;
+	o.l = ref Line(p, VZ);
+	o.p = p;
+	zerot(o, REFLECT|COLLIDE, 0);
+	reflection(o, planes);
+	return o;
+}
+
+objinits(nil: int, nil: int)
+{
+	n := 16;
+	t := array[n] of ref Object;
+	r := trkd/2.0;
+	i := 0;
+	yc := 0.0;
+	for(y := 0; y < 5; y++){
+		xc := -real y*r;
+		for(x := 0; x <= y; x++){
+			t[i] = newo(i, (xc, yc, 0.0), red);
+			xc += trkd;
+			i++;
+		}
+		yc += trkd;
+	}
+	t[i] = newo(i, (0.0, -50.0, 0.0), white);
+	t[i].l.d = (0.0, 1.0, 0.0);
+	t[i].v = 1.0;
+	sort(t, n);
+	nobjs = n;
+	objs = t;
+	for(i = 0; i < n; i++)
+		collisions(t[i]);
+}
+
+initme(): ref Object
+{
+	t := newobj(nil, 0, 1, 1.0);
+	t.roll = t.pitch = t.yaw = 0.0;
+	t.v = t.ω = 0.0;
+	t.l.a = (0.0, 0.0, objd);	# origin when translated
+	t.l.d = (0.0, 0.0, -1.0);
+	t.p = t.l.a;
+	zerot(t, REFLECT|COLLIDE, 0);
+	return t;
+}
+
+retime(s: real)
+{
+	r := 1.0/s;
+	n := nobjs;
+	t := objs;
+	for(i := 0; i < n; i++){
+		tr := t[i];
+		tr.v *= s;
+		tr.ω *= s;
+		tr.rt *= r;
+		tr.ct *= r;
+	}
+	me.v *= s;
+	me.ω *= s;
+	me.rt *= r;
+	me.ct *= r;
+}
+
+drawobjs()
+{
+	tr: ref Object;
+	p: Vector;
+
+	n := nobjs;
+	t := objs;
+
+	for(i := 0; i < n; i++){
+		tr = t[i];
+		tr.pmask = 0;
+		p = tr.p;
+		m_pushmatrix();
+		if(rpy && tr.poly ==  nil){
+			m_loadmatrix(mod0);
+			(p.x, p.y, p.z) = rotate(int yaw, p.x, p.y, p.z);
+			(p.y, p.z, p.x) = rotate(int pitch, p.y, p.z, p.x);
+			(p.z, p.x, p.y) = rotate(int roll, p.z, p.x, p.y);
+		}
+		m_translate(p.x, p.y, p.z);
+		m_scale(trkd, trkd, trkd);
+		if(tr.poly != nil){
+			m_rotatez(tr.roll);
+			m_rotatey(tr.pitch);
+			m_rotatex(tr.yaw);
+			tr.yaw += tr.ω;
+		}
+		m_matmul();
+		if(tr.cb != tr.c){
+			m_colour(tr.cb);
+			if(tr.poly != nil)
+				drawpoly(tr.poly, POLY);
+			else if(DOCIRCLE)
+				circle(CIRCLE);
+			else
+				circle(POLY);
+		}
+		m_colour(tr.c);
+		if(tr.poly != nil)
+			drawpoly(tr.poly, FILLPOLY);
+		else if(DOCIRCLE)
+			circle(FILLCIRCLE);
+		else
+			circle(FILLPOLY);
+		m_popmatrix();
+	}
+
+	tm := 1.0;
+	do{
+		δt := ∞;
+
+		for(i = 0; i < n; i++){
+			tr = t[i];
+			if(tr.rt < δt)
+				δt = tr.rt;
+			if(tr.ct < δt)
+				δt = tr.ct;
+		}
+
+		if(δt > tm)
+			δt = tm;
+
+		for(i = 0; i < n; i++){
+			tr = t[i];
+			if(tr.rt == δt){
+				tr1 := tr.obj;
+				reflect(tr);
+				tr.p = tr.rp;
+				if(δt > 0.0)
+					tr.pmask = (1<<tr.plane.id);
+				else
+					tr.pmask |= (1<<tr.plane.id);
+				zerot(tr, REFLECT|COLLIDE, 1);
+				zerot(tr1, COLLIDE, 1);
+			}
+			else if(tr.ct == δt){
+				tr1 := tr.obj ;
+				collide(tr, tr1);
+				if(0 && poly)
+					collideω(tr, tr1);
+				tr.p = tr.cp;
+				tr1.p = tr1.cp;
+				tr.pmask = tr1.pmask = 0;
+				zerot(tr, REFLECT|COLLIDE, 1);
+				zerot(tr1, REFLECT|COLLIDE, 1);
+			}
+			else if(tr.todo != (REFLECT|COLLIDE)){
+				tr.p = vclip(vadd(tr.p, vmul(tr.l.d, tr.v*δt)), icubd);
+				tr.rt -= δt;
+				tr.ct -= δt;
+			}
+		}
+
+		for(i = 0; i < n; i++){
+			tr = t[i];
+			if(tr.todo){
+				if(tr.todo&REFLECT)
+					reflection(tr, planes);
+				if(tr.todo&COLLIDE)
+					collisions(tr);
+				tr.todo = 0;
+			}
+		}
+
+		tm -= δt;
+
+	}while(tm > 0.0);
+
+	sort(t, n);
+}
+
+drawscene()
+{
+	m_pushmatrix();
+	m_scale(real 2*cubd, real 2*cubd, real 2*cubd);
+	m_colour(white);
+	m_matmul();
+	case(surr){
+		SPHERE =>
+			if(DOCIRCLE)
+				circle(CIRCLE);
+			else
+				circle(POLY);
+		ELLIPSOID =>
+			ellipse(ELLIPSE);
+		CUBE =>
+			if(flat)
+				square(POLY);
+			else
+				hexahedron(POLY);
+		* =>
+			drawpoly(polyh, POLY);
+	}
+	m_popmatrix();
+	drawobjs();
+}
+
+#  ensure parallax doesn't alter between images 
+adjpar(x: array of real, y: array of real, z: array of real)
+{
+	zed, incr: real;
+
+	y = nil;
+	if(z[0] < real 0)
+		zed = -z[0];
+	else
+		zed = z[0];
+	incr = eyed*zed*(real 1-scrD/(zed+objD-objd))/scrd;
+	if(!stereo || fov == real 0)
+		return;
+	if(left)
+		x[0] -= incr;
+	else
+		x[0] += incr;
+}
+
+view()
+{
+	m_mode(PROJ);
+	m_loadidentity();
+	m_scale(sx, sy, sz);
+	if(fov != real 0)
+		m_frustum(sf, real (1<<2), real (1<<20));
+	else
+		m_ortho(sf, real (1<<2), real (1<<20));
+	# m_print();
+	m_mode(MODEL);
+}
+
+model(rot: int)
+{
+	m_loadidentity();
+	m_translate(0.0, 0.0, -objd);
+	if(rpy && rot){
+		m_rotatez(roll);
+		m_rotatey(pitch);
+		m_rotatex(yaw);
+	}
+}
+
+#  store projection and modelview matrices 
+domats()
+{
+	model(0);
+	m_storematrix(mod0);
+	model(1);
+	view();
+}
+
+scale()
+{
+	if(maxx > maxy)
+		sx = real maxy/real maxx;
+	else
+		sx = 1.0;
+	if(maxy > maxx)
+		sy = real maxx/real maxy;
+	else
+		sy = 1.0;
+	sz = 1.0;
+}
+
+rescale(w: int, h: int)
+{
+	maxx = w;
+	maxy = h;
+	scale();
+	m_viewport(0, 0, maxx, maxy);
+}
+
+grinit()
+{
+	for(i := 0; i < 4; i++)
+		mod0[i] = array[4] of real;
+	far = (2.0*cubd, 2.0*cubd, 2.0*cubd);
+	icubd = cubd-trkd/2.0;
+	icubd2 = icubd*icubd;
+	trkd2 = trkd*trkd;
+	cmin = vpt(-icubd, -icubd, -icubd);
+	cmax = vpt(+icubd, +icubd, +icubd);
+	maxx = MAXX;
+	maxy = MAXY;
+	e2 = vmul(vvmul(ef, ef), icubd2);
+
+	m_init();
+	pinit();
+	circleinit();
+
+	m_viewport(0, 0, maxx, maxy);
+
+	scale();
+	if(fov > real 0)
+		fovtodist();
+	update(1);
+}
+
+newimage(win: ref Toplevel, init: int)
+{
+	maxx = int cmd(win, ".p cget -actwidth");
+	maxy = int cmd(win, ".p cget -actheight");
+	RDisp = display.newimage(((0, 0), (maxx, maxy)), win.image.chans, 0, Draw->Black);
+	tk->putimage(win, ".p", RDisp, nil);
+	RDisp.draw(RDisp.r, black, nil, (0, 0));
+	reveal();
+	rescale(maxx, maxy);
+	update(init);
+}
+
+reveal()
+{
+	cmd(toplev, ".p dirty; update");
+}
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "usage: collide [-cse] [-f] [-op] [-b num] [-v num]\n");
+	exit;
+}
+
+main(ctxt: ref Draw->Context, args: list of string)
+{
+	rand->init(daytime->now());
+	daytime = nil;
+
+	b := v := random(16, 32);
+
+	arg->init(args);
+	while((o := arg->opt()) != 0){
+		case(o){
+			* =>
+				usage();
+			's' =>
+				surr = SPHERE;
+			'e' =>
+				surr = ELLIPSOID;
+			'c' =>
+				surr = CUBE;
+			'o' =>
+				fov = 0.0;
+			'p' =>
+				fov = -1.0;
+			'f' =>
+				flat = 1;
+			'b' =>
+				b = v = int arg->arg();
+			'v' =>
+				v = int arg->arg();
+		}
+	}
+	if(arg->argv() != nil)
+		usage();
+
+	if(b <= 0)
+		b = 1;
+	if(b > 100)
+		b = 100;
+	if(v <= 0)
+		v = 1;
+	if(v > b)
+		v = b;
+
+	if(poly || surr == POLYHEDRON){
+		polyhedra = load Polyhedra Polyhedra->PATH;
+		getpolys();
+	}
+	if(surr == POLYHEDRON)
+		polyh = randpoly(polys, npolys);
+
+	grinit();
+	
+	tkclient->init();
+	(win, wmch) := tkclient->toplevel(ctxt, "", "Collide", Tkclient->Resize | Tkclient->Hide);
+	toplev = win;
+	sys->pctl(Sys->NEWPGRP, nil);
+	cmdch := chan of string;
+	tk->namechan(win, cmdch, "cmd");
+	for(i := 0; i < len winconfig; i++)
+		cmd(win, winconfig[i]);
+	cmd(win, "update");
+
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+	display = win.image.display;
+	newimage(win, 1);
+
+	black = display.color(Draw->Black);
+	white = display.color(Draw->White);
+	red = display.color(Draw->Red);
+
+	objinit(b, v);
+	me = initme();
+
+	pid := -1;
+	sync := chan of int;
+	cmdc := chan of int;
+	spawn animate(sync, cmdc);
+	pid = <- sync;
+
+	for(;;){
+		alt{
+			c := <-win.ctxt.kbd =>
+				tk->keyboard(win, c);
+			c := <-win.ctxt.ptr =>
+				tk->pointer(win, *c);
+			c := <-win.ctxt.ctl or
+			c = <-win.wreq =>
+				tkclient->wmctl(win, c);
+			c := <- wmch =>
+				case c{
+					"exit" =>
+						if(pid != -1)
+							kill(pid);
+						exit;
+					* =>
+						sync <-= 0;
+						tkclient->wmctl(win, c);
+						if(c[0] == '!')
+							newimage(win, 0);
+						sync <-= 1;
+				}
+			c := <- cmdch =>
+				case c{
+					"stop" =>
+						cmdc <-= 's';
+					"zoomin" =>
+						cmdc <-= 'z';
+					"zoomout" =>
+						cmdc <-= 'o';
+					"slow" =>
+						cmdc <-= '-';
+					"fast" =>
+						cmdc <-= '+';
+					"objs" =>
+						sync <-= 0;
+						b >>= 1;
+						if(b == 0)
+							b = 1;
+						objinit(b, b);
+						sync <-= 1;
+				}
+		}
+	}
+}
+
+sign(r: real): real
+{
+	if(r < 0.0)
+		return -1.0;
+	return 1.0;
+}
+
+abs(r: real): real
+{
+	if(r < 0.0)
+		return -r;
+	return r;
+}
+
+animate(sync: chan of int, cmd: chan of int)
+{
+	zd := objd/250.0;
+	δ := θ := 0.0;
+	f := 8;
+
+	sync <- = sys->pctl(0, nil);
+	for(;;){
+		σ := 1.0;
+		alt{
+			<- sync =>
+				<- sync;
+			c := <- cmd =>
+				case(c){
+					's' =>
+						δ = θ = 0.0;
+					'z' =>
+						δ = zd;
+					'o' =>
+						δ = -zd;
+					'r' =>
+						θ = 1.0;
+					'+' =>
+						σ = 1.25;
+						f++;
+						if(f > 16){
+							f--;
+							σ = 1.0;
+						}
+						else
+							vf *= σ;
+					'-' =>
+						σ = 0.8;
+						f--;
+						if(f < 0){
+							f++;
+							σ = 1.0;
+						}
+						else
+							vf *= σ;	
+				}
+			* =>
+				sys->sleep(0);
+		}
+
+		RDisp.draw(RDisp.r, black, nil, (0, 0));
+		drawscene();
+		reveal();
+
+		if(δ != 0.0 || θ != 0.0){
+			objd -= δ;
+			me.l.a.z -= δ;
+			if(θ != 0.0){
+				roll += θ;
+				pitch += θ;
+				yaw += θ;
+				rpy = 1;
+			}
+			update(projx);
+		}
+		if(σ != 1.0)
+			retime(σ);
+	}
+}
+
+# usually almost sorted
+sort(ts: array of ref Object, n: int)
+{
+	done: int;
+	t: ref Object;
+	q, p: int;
+
+	q = n;
+	do{
+		done = 1;
+		q--;
+		for(p = 0; p < q; p++){
+			if(ts[p].p.z > ts[p+1].p.z){
+				t = ts[p];
+				ts[p] = ts[p+1];
+				ts[p+1] = t;
+				done = 0;
+			}
+		}
+	}while(!done);
+}
+
+kill(pid: int): int
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil)
+		return -1;
+	if(sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+fatal(e: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", e);
+	raise "fatal";
+}
+
+cmd(top: ref Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		fatal(sys->sprint("tk error on '%s': %s", s, e));
+	return e;
+}
+
+winconfig := array[] of {
+	"frame .f",
+	"button .f.zoomin -text {zoomin} -command {send cmd zoomin}",
+	"button .f.zoomout -text {zoomout} -command {send cmd zoomout}",
+	"button .f.stop -text {stop} -command {send cmd stop}",
+	"pack .f.zoomin -side left",
+	"pack .f.zoomout -side right",
+	"pack .f.stop -side top",
+
+	"frame .f2",
+	"button .f2.slow -text {slow} -command {send cmd slow}",
+	"button .f2.fast -text {fast} -command {send cmd fast}",
+	"button .f2.objs -text {objects} -command {send cmd objs}",
+	"pack .f2.slow -side left",
+	"pack .f2.fast -side right",
+	"pack .f2.objs -side top",
+
+	"panel .p -width " + string MAXX + " -height " + string MAXY,
+
+	"pack .f -side top -fill x",
+	"pack .f2 -side top -fill x",
+	"pack .p -side bottom -fill both -expand 1",
+
+	"pack propagate . 0",
+	"update"
+};
+
+############################################################
+
+# gl.b
+
+#
+#	initially generated by c2l
+#
+
+MODEL, PROJ: con iota;
+
+Matrix: type array of array of real;
+
+Mstate: adt{
+	matl: list of Matrix;
+	modl: list of Matrix;
+	prjl: list of Matrix;
+	mull: Matrix;
+	freel: list of Matrix;
+	vk: int;
+	vr: int;
+	vrr: int;
+	vc: ref Draw->Image;
+	ap: array of Draw->Point;
+	apn: int;
+	mx, cx, my, cy: real;
+	ignore: int;
+};
+
+ms: Mstate;
+
+m_new(): Matrix
+{
+	if(ms.freel != nil){
+		m := hd ms.freel;
+		ms.freel = tl ms.freel;
+		return m;
+	}
+	m := array[4] of array of real;
+	for(i := 0; i < 4; i++)
+		m[i] = array[4] of real;
+	return m;
+}
+
+m_free(m: Matrix)
+{
+	ms.freel = m :: ms.freel;
+}
+
+m_init()
+{
+	ms.modl = m_new() :: nil;
+	ms.prjl = m_new() :: nil;
+	ms.matl = ms.modl;
+	ms.mull = m_new();
+	ms.vk = 0;
+	ms.apn = 0;
+	ms.mx = ms.cx = ms.my = ms.cy = 0.0;
+	ms.ignore = 0;
+}
+
+m_print()
+{
+	m := hd ms.matl;
+
+	for(i := 0; i < 4; i++){
+		for(j := 0; j < 4; j++)
+			sys->print("%f	", m[i][j]);
+		sys->print("\n");
+	}
+	sys->print("\n");
+}
+
+m_mode(m: int)
+{
+	if(m == PROJ)
+		ms.matl = ms.prjl;
+	else
+		ms.matl = ms.modl;
+}
+
+m_pushmatrix()
+{
+	if(ms.matl == ms.modl){
+		ms.modl = m_new() :: ms.modl;
+		ms.matl = ms.modl;
+	}
+	else{
+		ms.prjl = m_new() :: ms.prjl;
+		ms.matl = ms.prjl;
+	}
+	s := hd tl ms.matl;
+	d := hd ms.matl;
+	for(i := 0; i < 4; i++)
+		for(j := 0; j < 4; j++)
+			d[i][j] = s[i][j];
+}
+
+m_popmatrix()
+{
+	if(ms.matl == ms.modl){
+		m_free(hd ms.modl);
+		ms.modl = tl ms.modl;
+		ms.matl = ms.modl;
+	}
+	else{
+		m_free(hd ms.prjl);
+		ms.prjl = tl ms.prjl;
+		ms.matl = ms.prjl;
+	}
+}
+
+m_loadidentity()
+{
+	i, j: int;
+	m := hd ms.matl;
+
+	for(i = 0; i < 4; i++){
+		for(j = 0; j < 4; j++)
+			m[i][j] = real 0;
+		m[i][i] = real 1;
+	}	
+}
+
+m_translate(x: real, y: real, z: real)
+{
+	i: int;
+	m := hd ms.matl;
+
+	for(i = 0; i < 4; i++)
+		m[i][3] = x*m[i][0]+y*m[i][1]+z*m[i][2]+m[i][3];
+}
+
+m_scale(x: real, y: real, z: real)
+{
+	i: int;
+	m := hd ms.matl;
+
+	for(i = 0; i < 4; i++){
+		m[i][0] *= x;
+		m[i][1] *= y;
+		m[i][2] *= z;
+	}
+}
+
+#  rotate about x, y or z axes 
+rot(deg: real, j: int, k: int)
+{
+	i: int;
+	m := hd ms.matl;
+	rad := Math->Pi*deg/real 180;
+	s := maths->sin(rad);
+	c := maths->cos(rad);
+	a, b: real;
+
+	for(i = 0; i < 4; i++){
+		a = m[i][j];
+		b = m[i][k];
+		m[i][j] = c*a+s*b;
+		m[i][k] = c*b-s*a;
+	}
+}
+
+m_rotatex(a: real)
+{
+	rot(a, 1, 2);
+}
+
+m_rotatey(a: real)
+{
+	rot(a, 2, 0);
+}
+
+m_rotatez(a: real)
+{
+	rot(a, 0, 1);
+}
+
+# (l m n) normalized
+m_rotate(deg: real, l: real, m: real, n:real)
+{
+	i: int;
+	mx := hd ms.matl;
+	rad := Math->Pi*deg/real 180;
+	s := maths->sin(rad);
+	c := maths->cos(rad);
+	f := 1.0-c;
+	m0, m1, m2: real;
+
+	for(i = 0; i < 4; i++){
+		m0 = mx[i][0];
+		m1 = mx[i][1];
+		m2 = mx[i][2];
+		mx[i][0] = m0*(l*l*f+c)+m1*(l*m*f+n*s)+m2*(l*n*f-m*s);
+		mx[i][1] = m0*(l*m*f-n*s)+m1*(m*m*f+c)+m2*(m*n*f+l*s);
+		mx[i][2] = m0*(l*n*f+m*s)+m1*(m*n*f-l*s)+m2*(n*n*f+c);
+	}
+}
+
+#  Frustum(-l, l, -l, l, n, f) 
+m_frustum(l: real, n: real, f: real)
+{
+	i: int;
+	m := hd ms.matl;
+	r := n/l;
+	a, b: real;
+
+	f = ∞;
+	for(i = 0; i < 4; i++){
+		a = m[i][2];
+		b = m[i][3];
+		m[i][0] *= r;
+		m[i][1] *= r;
+		m[i][2] = a+b;
+		m[i][3] = 0.0;
+		# m[i][2] = -(a*(f+n)/(f-n)+b);
+		# m[i][3] = real -2*f*n*a/(f-n);
+	}
+}
+
+#  Ortho(-l, l, -l, l, n, f) 
+m_ortho(l: real, n: real, f: real)
+{
+	i: int;
+	m := hd ms.matl;
+	r := real 1/l;
+	# a: real;
+
+	n = 0.0;
+	f = ∞;
+	for(i = 0; i < 4; i++){
+		# a = m[i][2];
+		m[i][0] *= r;
+		m[i][1] *= r;
+		# m[i][2] *= real -2/(f-n);
+		# m[i][3] -= a*(f+n)/(f-n);
+	}
+}
+
+m_loadmatrix(u: array of array of real)
+{
+	m := hd ms.matl;
+
+	for(i := 0; i < 4; i++)
+		for(j := 0; j < 4; j++)
+			m[i][j] = u[i][j];
+}
+
+m_storematrix(u: array of array of real)
+{
+	m := hd ms.matl;
+
+	for(i := 0; i < 4; i++)
+		for(j := 0; j < 4; j++)
+			u[i][j] = m[i][j];
+}
+
+m_matmul()
+{
+	m, p, r: Matrix;
+
+	m = hd ms.modl;
+	p = hd ms.prjl;
+	r = ms.mull;
+	for(i := 0; i < 4; i++){
+		pr := p[i];
+		rr := r[i];
+		for(j := 0; j < 4; j++)
+			rr[j] = pr[0]*m[0][j]+pr[1]*m[1][j]+pr[2]*m[2][j]+pr[3]*m[3][j];
+	}
+}
+
+m_vertexo(x: real, y: real, z: real)
+{
+	m: Matrix;
+	mr: array of real;
+	w, x1, y1, z1: real;
+
+	m = hd ms.modl;
+	mr = m[0]; x1 = x*mr[0]+y*mr[1]+z*mr[2]+mr[3];
+	mr = m[1]; y1 = x*mr[0]+y*mr[1]+z*mr[2]+mr[3];
+	mr = m[2]; z1 = x*mr[0]+y*mr[1]+z*mr[2]+mr[3];
+	mr = m[3]; w = x*mr[0]+y*mr[1]+z*mr[2]+mr[3];
+	if(w != real 1){
+		x1 /= w;
+		y1 /= w;
+		z1 /= w;
+	}
+	if(z1 >= 0.0){
+		ms.ignore = 1;
+		return;
+	}
+	m = hd ms.prjl;
+	mr = m[0]; x = x1*mr[0]+y1*mr[1]+z1*mr[2]+mr[3];
+	mr = m[1]; y = x1*mr[0]+y1*mr[1]+z1*mr[2]+mr[3];
+	mr = m[2]; z = x1*mr[0]+y1*mr[1]+z1*mr[2]+mr[3];
+	mr = m[3]; w = x1*mr[0]+y1*mr[1]+z1*mr[2]+mr[3];
+	if(w == real 0){
+		ms.ignore = 1;
+		return;
+	}
+	if(w != real 1){
+		x /= w;
+		y /= w;
+		# z /= w;
+	}
+	ms.ap[ms.apn++] = (int (ms.mx*x+ms.cx), int (ms.my*y+ms.cy));
+}
+
+m_vertex(x: real, y: real, z: real): (real, real)
+{
+	m: Matrix;
+	mr: array of real;
+	w, x1, y1, z1: real;
+
+	m = ms.mull;
+	mr = m[0]; x1 = x*mr[0]+y*mr[1]+z*mr[2]+mr[3];
+	mr = m[1]; y1 = x*mr[0]+y*mr[1]+z*mr[2]+mr[3];
+	mr = m[2]; z1 = x*mr[0]+y*mr[1]+z*mr[2]+mr[3];
+	mr = m[3]; w = x*mr[0]+y*mr[1]+z*mr[2]+mr[3];
+	if(w == real 0){
+		ms.ignore = 1;
+		return (x1, y1);
+	}
+	if(w != real 1){
+		x1 /= w;
+		y1 /= w;
+		# z1 /= w;
+	}
+	if(z1 >= 0.0){
+		ms.ignore = 1;
+		return (x1, y1);
+	}
+	ms.ap[ms.apn++] = (int (ms.mx*x1+ms.cx), int (ms.my*y1+ms.cy));
+	return (x1, y1);
+}
+
+m_circle(x: real, y: real, z: real, r: real)
+{
+	(d, nil) := m_vertex(x, y, z);
+	(e, nil) := m_vertex(x+r, y, z);
+	d -= e;
+	if(d < 0.0)
+		d = -d;
+	ms.vr = int (ms.mx*d);
+}
+
+m_ellipse(x: real, y: real, z: real, v: Vector)
+{
+	m_circle(x, y, z, v.x);
+	(nil, d) := m_vertex(x, y, z);
+	(nil, e) := m_vertex(x, y+v.y, z);
+	d -= e;
+	if(d < 0.0)
+		d = -d;
+	ms.vrr = int (ms.my*d);
+}
+
+m_begin(k: int, n: int)
+{
+	ms.ignore = 0;
+	ms.vk = k;
+	ms.ap = array[n+1] of Draw->Point;
+	ms.apn = 0;
+}
+
+m_end()
+{
+	if(ms.ignore)
+		return;
+	case(ms.vk){
+		CIRCLE =>
+			RDisp.ellipse(ms.ap[0], ms.vr, ms.vr, 0, ms.vc, (0, 0));
+		FILLCIRCLE =>
+			RDisp.fillellipse(ms.ap[0], ms.vr, ms.vr, ms.vc, (0, 0));
+		ELLIPSE =>
+			RDisp.ellipse(ms.ap[0], ms.vr, ms.vrr, 0, ms.vc, (0, 0));
+		FILLELLIPSE =>
+			RDisp.fillellipse(ms.ap[0], ms.vr, ms.vrr, ms.vc, (0, 0));
+		POLY =>
+			ms.ap[len ms.ap-1] = ms.ap[0];
+			RDisp.poly(ms.ap, Draw->Endsquare, Draw->Endsquare, 0, ms.vc, (0, 0));
+		FILLPOLY =>
+			ms.ap[len ms.ap-1] = ms.ap[0];
+			RDisp.fillpoly(ms.ap, ~0, ms.vc, (0, 0));
+	}
+}
+
+m_colour(i: ref Draw->Image)
+{
+	ms.vc = i;
+}
+
+m_viewport(x1: int, y1: int, x2: int, y2: int)
+{
+	ms.mx = real (x2-x1)/2.0;
+	ms.cx = real (x2+x1)/2.0;
+	ms.my = real (y2-y1)/2.0;
+	ms.cy = real (y2+y1)/2.0;
+}
+
+# sys->print("%d %f (%f %f %f) %s\n", ok, λ, 1.0, 2.0*vdot(l.a, l.d), vdot(l.a, l.a)-icubd2, lstring(l));
+
+# sys->print("%d %f (%f %f %f) %s\n", ok, λ, vmuldiv(l.d, l.d, e2), 2.0*vmuldiv(l.a, l.d, e2), vmuldiv(l.a, l.a, e2)-1.0, lstring(l));
+
+# 			for(lp = lp0 ; lp != nil; lp = tl lp){
+# 				p := hd lp;
+# 				(ok, λ, pt) := intersect(l, p);
+# 				sys->print("%d	%x	%d	%f	%s	%s	%s\n", p.id, tr.pmask, ok, λ, vstring(pt), pstring(p), lstring(l));
+# 			}
--- /dev/null
+++ b/appl/wm/colors.b
@@ -1,0 +1,153 @@
+implement Colors;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Point, Rect, Image: import draw;
+include "tk.m";
+	tk: Tk;
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+Colors: module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+display: ref Display;
+top: ref Tk->Toplevel;
+tmpi: ref Image;
+
+task_cfg := array[] of {
+	"panel .c",
+	"label .l -anchor w -text {col:}",
+	"pack .l -fill x",
+	"pack .c -fill both -expand 1",
+	"bind .c <Button-1> {grab set .c; send cmd %X %Y}",
+	"bind .c <ButtonRelease-1> {grab release .c}",
+};
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	spawn init1(ctxt);
+}
+
+init1(ctxt: ref Draw->Context)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+
+	tkclient->init();
+	display = ctxt.display;
+	tmpi = display.newimage(((0,0), (1, 1)), Draw->RGB24, 0, 0);
+
+	titlectl: chan of string;
+	(top, titlectl) = tkclient->toplevel(ctxt, "", "Colors", Tkclient->Appl);
+
+	cmdch := chan of string;
+	tk->namechan(top, cmdch, "cmd");
+
+	for (i := 0; i < len task_cfg; i++)
+		cmd(top, task_cfg[i]);
+	tk->putimage(top, ".c", cmap((256, 256)), nil);
+	cmd(top, "pack propagate . 0");
+	cmd(top, "update");
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+	s := <-top.ctxt.kbd =>
+		tk->keyboard(top, s);
+	s := <-top.ctxt.ptr =>
+		tk->pointer(top, *s);
+	c := <-top.ctxt.ctl or
+	c = <-top.wreq or
+	c = <-titlectl =>
+		if(c == "exit")
+			return;
+		e := tkclient->wmctl(top, c);
+		if(e == nil && c[0] == '!'){
+			tk->putimage(top, ".c", cmap(actr(".c").size()), nil);
+			cmd(top, "update");
+		}
+
+	press := <-cmdch =>
+		(nil, toks) := sys->tokenize(press, " ");
+		color((int hd toks, int hd tl toks));
+	}
+}
+
+color(p: Point)
+{
+	r, g, b: int;
+	col: string;
+
+	cr := actr(".c");
+	if(p.in(cr)){
+		p = p.sub(cr.min);
+		p.x = (16*p.x)/cr.dx();
+		p.y = (16*p.y)/cr.dy();
+		(r, g, b) = display.cmap2rgb(16*p.y+p.x);
+		col = string (16*p.y+p.x);
+	}else{
+		tmpi.draw(tmpi.r, display.image, nil, p);
+		data := array[3] of byte;
+		ok := tmpi.readpixels(tmpi.r, data);
+		if(ok != len data)
+			return;
+		(r, g, b) = (int data[2], int data[1], int data[0]);
+		c := display.rgb2cmap(r, g, b);
+		(r1, g1, b1) := display.cmap2rgb(c);
+		if (r == r1 && g == g1 && b == b1)
+			col = string c;
+		else
+			col = "~" + string c;
+	}
+
+	cmd(top, ".l configure -text " +
+		sys->sprint("{col:%s #%.6X r%d g%d b%d}", col, (r<<16)|(g<<8)|b, r, g, b));
+	cmd(top, "update");
+}
+
+cmap(size: Point): ref Image
+{
+	# use writepixels because it's much faster than allocating all those colors.
+	img := display.newimage(((0, 0), size), Draw->CMAP8, 0, 0);
+	if (img == nil){
+		sys->print("colors: cannot make new image: %r\n");
+		return nil;
+	}
+
+	dy := (size.y / 16 + 1);
+	buf := array[size.x * dy] of byte;
+
+	for(y:=0; y<16; y++){
+		for (i := 0; i < size.x; i++)
+			buf[i] = byte (16*y + (16*i)/size.x);
+		for (i = 1; i < dy; i++)
+			buf[size.x*i:] = buf[0:size.x];
+		img.writepixels(((0, (y*size.y)/16), (size.x, ((y+1)*size.y) / 16)), buf);
+	}
+	return img;
+}
+
+actr(w: string): Rect
+{
+	r: Rect;
+	bd := int cmd(top, w + " cget -bd");
+	r.min.x = int cmd(top, w + " cget -actx") + bd;
+	r.min.y = int cmd(top, w + " cget -acty") + bd;
+	r.max.x = r.min.x + int cmd(top, w + " cget -actwidth");
+	r.max.y = r.min.y + int cmd(top, w + " cget -actheight");
+	return r;
+}
+
+cmd(top: ref Tk->Toplevel, cmd: string): string
+{
+	e := tk->cmd(top, cmd);
+	if (e != nil && e[0] == '!')
+		sys->print("colors: tk error on '%s': %s\n", cmd, e);
+	return e;
+}
--- /dev/null
+++ b/appl/wm/cprof.b
@@ -1,0 +1,360 @@
+implement Wmcprof;
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "draw.m";
+	draw: Draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "arg.m";
+	arg: Arg;
+include "profile.m";
+
+Prof: module{
+	init0: fn(ctxt: ref Draw->Context, argv: list of string): Profile->Coverage;
+};
+
+prof: Prof;
+
+Wmcprof: module{
+	init: fn(ctxt: ref Draw->Context, argl: list of string);
+};
+
+usage(s: string)
+{
+	sys->fprint(sys->fildes(2), "wm/cprof: %s\n", s);
+	sys->fprint(sys->fildes(2), "usage: wm/cprof [-efr] [-m modname]... cmd [arg ... ]\n");
+	exit;
+}
+
+TXTBEGIN: con 3;
+
+freq: int;
+
+init(ctxt: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	arg = load Arg Arg->PATH;
+	
+	if(ctxt == nil)
+		fatal("wm not running");
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	arg->init(argl);
+	while((o := arg->opt()) != 0){
+		case(o){
+			'e' or 'r' =>
+				;
+			'f' =>
+				freq = 1;
+			'm' =>
+				if(arg->arg() == nil)
+					usage("missing module/file");
+			* => 
+				usage(sys->sprint("unknown option -%c", o));
+		}
+	}
+
+	cover := execprof(ctxt, argl);
+
+	tkclient->init();
+	(win, wmc) := tkclient->toplevel(ctxt, nil, hd argl, Tkclient->Resize|Tkclient->Hide);
+	tkc := chan of string;
+	tk->namechan(win, tkc, "tkc");
+	for(i := 0; i < len wincfg; i++)
+		cmd(win, wincfg[i]);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	createmenu(win, cover);
+	curc := 0;
+	curm := newprint(win, cover, curc);
+	
+	for(;;){
+		alt{
+			c := <-win.ctxt.kbd =>
+				tk->keyboard(win, c);
+			c := <-win.ctxt.ptr =>
+				tk->pointer(win, *c);
+			c := <-win.ctxt.ctl or
+			c = <-win.wreq or
+			c = <-wmc =>
+				tkclient->wmctl(win, c);
+			c := <- tkc =>
+				(nil, toks) := sys->tokenize(c, " ");
+				case(hd toks){
+					"b" =>
+						if(curc > 0)
+							curm = newprint(win, cover, --curc);
+					"f" =>
+						if(curc < len cover - 1)
+							curm = newprint(win, cover, ++curc);
+					"s" =>
+						if(curm != nil)
+							scroll(win, curm);
+					"m" =>
+						x := cmd(win, ".f cget actx");
+						y := cmd(win, ".f cget acty");
+						cmd(win, ".f.menu post " + x + " " + y);
+					* =>
+						curc = int hd toks;
+						curm = newprint(win, cover, curc);
+				}
+		}
+	}
+}
+
+execprof(ctxt: ref Draw->Context, argl: list of string): Profile->Coverage
+{
+	{
+		prof = load Prof "/dis/cprof.dis";
+		if(prof == nil)
+			fatal("cannot load profiler");
+		return prof->init0(ctxt, hd argl :: "-g" :: tl argl);
+	}
+	exception{
+		"fail:*" =>
+			return nil;
+	}
+	return nil;
+}
+
+maxf(rs: list of (int, int, int)): int
+{
+	fmax := 0;
+	for(r := rs; r != nil; r = tl r){
+		(nil, nil, f) := hd r;
+		if(f > fmax)
+			fmax = f;
+	}
+	return fmax;
+}
+
+print(win: ref Tk->Toplevel, cvr: Profile->Coverage, i: int, c: chan of Profile->Coverage)
+{
+	cmd(win, ".f.t delete 1.0 end");
+	cmd(win, "update");
+	m0, m1: Profile->Coverage;
+	for(m := cvr; m != nil && --i >= 0; m = tl m)
+		m0 = m;
+	if(m == nil){
+		c <- = nil;
+		return;
+	}
+	m1 = tl m;	
+	(name, cvd, ls) := hd m;
+	name0 := name1 := "nil";
+	if(m0 != nil)
+		(name0, nil, nil) = hd m0;
+	if(m1 != nil)
+		(name1, nil, nil) = hd m1;
+	if(freq){
+		cvd = 0;
+		for(l := ls; l != nil; l = tl l){
+			(rs, nil) := hd l;
+			cvd += maxf(rs);
+		}
+	}
+	else
+		name += sys->sprint(" (%d%% coverage) ", cvd);
+	cmd(win, ".f.t insert end {" + name + "        <- " + name0 + "        -> " + name1 + "}");
+	cmd(win, ".f.t insert end \n\n");
+	cmd(win, "update");
+	line := TXTBEGIN;
+	for(l := ls; l != nil; l = tl l){
+		tab := 0;
+		(rs, s) := hd l;
+		if(freq){
+			fmax := maxf(rs);
+			s = string fmax + "\t" + s;
+			tab = len string fmax + 1;
+		}
+		cmd(win, ".f.t insert end " + tk->quote(s));
+		for(r := rs; r != nil; r = tl r){
+			tag: string;
+			(a, b, e) := hd r;
+			if(freq){
+				tag = gettag(win, e, cvd);
+				a += tab;
+				b += tab;
+			}
+			else{
+				if(int e)	# partly executed
+					tag = "halfexec";
+				else
+					tag = "notexec";
+			}
+			cmd(win, ".f.t tag add " + tag + " " + string line + "." + string a + " " + string line + "." + string b);
+		}
+		cmd(win, "update");
+		line++;
+	}
+	c <- = m;
+}
+
+newprint(win: ref Tk->Toplevel, cvr: Profile->Coverage, i: int): Profile->Coverage
+{
+	c := chan of Profile->Coverage;
+	spawn print(win, cvr, i, c);
+	return <- c;
+}
+
+index(win: ref Tk->Toplevel, x: int, y: int): int
+{
+	t := cmd(win, ".f.t index @" + string x + "," + string y);
+	(nil, l) := sys->tokenize(t, ".");
+# sys->print("%d,%d -> %s\n", x, y, t);
+	return int hd l;
+}
+
+winextent(win: ref Tk->Toplevel): (int, int)
+{
+	w := int cmd(win, ".f.t cget -actwidth");
+	h := int cmd(win, ".f.t cget -actheight");
+	lw := index(win, 0, 0);
+	uw := index(win, w-1, h-1);
+	return (lw, uw);
+}
+
+see(win: ref Tk->Toplevel, line: int)
+{
+	cmd(win, ".f.t see " + string line + ".0");
+	cmd(win, "update");	
+}
+	
+scroll(win: ref Tk->Toplevel, m: Profile->Coverage)
+{
+	(nil, cvd, ls) := hd m;
+	if(freq)
+		cvd = 0;
+	(nil, uw) := winextent(win);
+	line := TXTBEGIN;
+	for(l := ls; l != nil; l = tl l){
+		(rs, nil) := hd l;
+		if(rs != nil && line > uw){
+			see(win, line);
+			return;
+		}
+		line++;
+	}
+	if(cvd < 100){
+		line = TXTBEGIN;
+		for(l = ls; l != nil; l = tl l){
+			(rs, nil) := hd l;
+			if(rs != nil){
+				see(win, line);
+				return;
+			}
+			line++;
+		}
+	}
+	return;
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	# sys->print("%s\n", s);
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+	exit;
+}
+
+MENUMAX: con 20;
+
+createmenu(top: ref Tk->Toplevel, cvr: Profile->Coverage )
+{
+	mn := ".f.menu";
+	cmd(top, "menu " + mn);
+	i := j := 0;
+	for(m := cvr; m != nil; m = tl m){
+		(name, nil, nil) := hd m;
+		cmd(top, mn + " add command -label " + name + " -command {send tkc " + string i + "}");
+		i++;
+		j++;
+		if(j == MENUMAX && tl m != nil){
+			cmd(top, mn + " add cascade -label MORE -menu " + mn + ".menu");
+			mn += ".menu";
+			cmd(top, "menu " + mn);
+			j = 0;
+		}
+	}
+}
+
+SNT: con 16;
+NT: con SNT*SNT;
+NTF: con 256/SNT;
+
+tags := array[NT]  of { * => byte 0 };
+
+gettag(win: ref Tk->Toplevel, n: int, d: int): string
+{
+	i := int ((real n/real d) * real (NT-1));
+	if(i < 0 || i > NT-1)
+		i = 0;
+	s := "tag" + string i;
+	if(tags[i] == byte 0){
+		rgb := "#" + hex2(255-NTF*0)+hex2(255-NTF*(i/SNT))+hex2(255-NTF*(i%SNT));
+		cmd(win, ".f.t tag configure " + s + " -fg black -bg " + rgb);
+		tags[i] = byte 1;
+	}
+	return s;
+}
+
+hex(i: int): int
+{
+	if(i < 10)
+		return i+'0';
+	else
+		return i-10+'A';
+}
+
+hex2(i: int): string
+{
+	s := "00";
+	s[0] = hex(i/16);
+	s[1] = hex(i%16);
+	return s;
+}
+
+wincfg := array[] of {
+	"frame .f",
+	"text .f.t -width 809 -height 500 -state disabled -wrap char -bg white -yscrollcommand {.f.s set}",
+	"scrollbar .f.s -orient vertical -command {.f.t yview}",
+	"frame .i",
+	"button .i.b -bitmap small_color_left.bit -command {send tkc b}",
+	"button .i.f -bitmap small_color_right.bit -command {send tkc f}",
+	"button .i.s -bitmap small_find.bit -command {send tkc s}",
+	"button .i.m -bitmap small_reload.bit -command {send tkc m}",
+
+	"pack .i.b -side left",
+	"pack .i.f -side left",
+	"pack .i.s -side left",
+	"pack .i.m -side left",
+
+	"pack .f.s -fill y -side left",
+	"pack .f.t -fill both -expand 1",
+
+	"pack .i -fill x",
+	"pack .f -fill both -expand 1",
+	"pack propagate . 0",
+
+	".f.t tag configure notexec -fg white -bg red",
+	".f.t tag configure halfexec -fg red -bg white",
+
+	"update",
+};
--- /dev/null
+++ b/appl/wm/date.b
@@ -1,0 +1,78 @@
+implement WmDate;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "daytime.m";
+	daytime: Daytime;
+
+
+WmDate: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+tpid: int;
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys  = load Sys  Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "date: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk   = load Tk   Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+	(t, wmctl) := tkclient->toplevel(ctxt, "", "Date", 0);
+
+	st := daytime->time()[0:19];
+	tk->cmd(t, "label .d -label {"+st+"}");
+	tk->cmd(t, "pack .d; pack propagate . 0");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "ptr"::nil);
+	tick := chan of int;
+	spawn timer(tick);
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-wmctl =>
+		tkclient->wmctl(t, s);
+	<-tick =>
+		tk->cmd(t, ".d configure -label {"+daytime->time()[0:19]+"};update");
+	}
+}
+
+timer(c: chan of int)
+{
+	tpid = sys->pctl(0, nil);
+	for(;;) {
+		c <-= 1;
+		sys->sleep(1000);
+	}
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
--- /dev/null
+++ b/appl/wm/deb.b
@@ -1,0 +1,1444 @@
+implement WmDebugger;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+	arg: Arg;
+
+include "readdir.m";
+	readdir: Readdir;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include "tabs.m";
+	tabs: Tabs;
+
+include "debug.m";
+	debug: Debug;
+	Prog, Exp, Module, Src, Sym: import debug;
+
+include "wmdeb.m";
+	debdata: DebData;
+	Vars: import debdata;
+	debsrc: DebSrc;
+	opendir, Mod: import debsrc;
+
+WmDebugger: module
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+icondir :	con "debug/";
+
+tkconfig := array[] of {
+	"frame .m -relief raised -bd 1",
+	"frame .p -padx 2",
+	"frame .ctls -padx 2",
+	"frame .body",
+
+	# menu bar
+	"menubutton .m.file -text File -menu .m.file.menu",
+	"menubutton .m.search -text Search -menu .m.search.menu",
+	"button .m.stack -text Stack -command {send m stack}",
+	"pack .m.file .m.search .m.stack -side left",
+
+	# file menu
+	"menu .m.file.menu",
+	".m.file.menu add command -label Open... -command {send m open}",
+	".m.file.menu add command -label Thread... -command {send m pickup}",
+	".m.file.menu add command -label Options... -command {send m options}",
+	".m.file.menu add separator",
+
+	# search menu
+	"menu .m.search.menu",
+	".m.search.menu add command -state disabled"+
+		" -label Look -command {send m look}",
+	".m.search.menu add command -state disabled"+
+		" -label {Search For} -command {send m search}",
+
+	# program control
+	"image create bitmap Detach -file "+icondir+
+			"detach.bit -maskfile "+icondir+"detach.mask",
+	"image create bitmap Kill -file "+icondir+
+			"kill.bit -maskfile "+icondir+"kill.mask",
+	"image create bitmap Run -file "+icondir+
+			"run.bit -maskfile "+icondir+"run.mask",
+	"image create bitmap Stop -file "+icondir+
+			"stop.bit -maskfile "+icondir+"stop.mask",
+	"image create bitmap Bpt -file "+icondir+
+			"break.bit -maskfile "+icondir+"break.mask",
+	"image create bitmap Stepop -file "+icondir+
+			"stepop.bit -maskfile "+icondir+"stepop.mask",
+	"image create bitmap Stepin -file "+icondir+
+			"stepin.bit -maskfile "+icondir+"stepin.mask",
+	"image create bitmap Stepout -file "+icondir+
+			"stepout.bit -maskfile "+icondir+"stepout.mask",
+	"image create bitmap Stepover -file "+icondir+
+			"stepover.bit -maskfile "+icondir+"stepover.mask",
+	"button .p.kill -image Kill -command {send m killall}"+
+			" -state disabled -relief sunken",
+	"bind .p.kill <Enter> +{.p.status configure -text {kill current process}}",
+	"bind .p.kill <Leave> +{.p.status configure -text {}}",
+	"button .p.detach -image Detach -command {send m detach}"+
+			" -state disabled -relief sunken",
+	"bind .p.detach <Enter> +{.p.status configure -text {stop debugging current process}}",
+	"bind .p.detach <Leave> +{.p.status configure -text {}}",
+	"button .p.run -image Run -command {send m run}"+
+			" -state disabled -relief sunken",
+	"bind .p.run <Enter> +{.p.status configure -text {run to breakpoint}}",
+	"bind .p.run <Leave> +{.p.status configure -text {}}",
+	"button .p.step -image Stepop -command {send m step}"+
+			" -state disabled -relief sunken",
+	"bind .p.step <Enter> +{.p.status configure -text {step one operation}}",
+	"bind .p.step <Leave> +{.p.status configure -text {}}",
+	"button .p.stmt -image Stepin -command {send m stmt}"+
+			" -state disabled -relief sunken",
+	"bind .p.stmt <Enter> +{.p.status configure -text {step one statement}}",
+	"bind .p.stmt <Leave> +{.p.status configure -text {}}",
+	"button .p.over -image Stepover -command {send m over}"+
+			" -state disabled -relief sunken",
+	"bind .p.over <Enter> +{.p.status configure -text {step over calls}}",
+	"bind .p.over <Leave> +{.p.status configure -text {}}",
+	"button .p.out -image Stepout -command {send m out}"+
+			" -state disabled -relief sunken",
+	"bind .p.out <Enter> +{.p.status configure -text {step out of fn}}",
+	"bind .p.out <Leave> +{.p.status configure -text {}}",
+	"button .p.bpt -image Bpt -command {send m setbpt}"+
+			" -state disabled -relief sunken",
+	"bind .p.bpt <Enter> +{.p.status configure -text {set/clear breakpoint}}",
+	"bind .p.bpt <Leave> +{.p.status configure -text {}}",
+	"frame .p.steps",
+	"label .p.status -anchor w",
+	"pack .p.step .p.stmt .p.over .p.out -in .p.steps -side left -fill y",
+	"pack .p.kill .p.detach .p.run .p.steps .p.bpt -side left -padx 5 -fill y",
+	"pack .p.status -side left -expand 1 -fill x",
+
+	# progs
+	"frame .prog",
+	"label .prog.l -text Threads",
+	"canvas .prog.d -height 1 -width 1 -relief sunken -bd 2",
+	"frame .prog.v",
+	".prog.d create window 0 0 -window .prog.v -anchor nw",
+	"pack .prog.l -side top -anchor w",
+	"pack .prog.d -side left -fill both -expand 1",
+
+	# breakpoints
+	"frame .bpt",
+	"label .bpt.l -text Break",
+	"canvas .bpt.d -height 1 -width 1 -relief sunken -bd 2",
+	"frame .bpt.v",
+	".bpt.d create window 0 0 -window .bpt.v -anchor nw",
+	"pack .bpt.l -side top -anchor w",
+	"pack .bpt.d -side left -fill both -expand 1",
+
+	"pack .prog .bpt -side top -fill both -expand 1 -in .ctls",
+
+	# test body
+	"frame .body.ft -bd 1 -relief sunken -width 60w -height 20h",
+	"scrollbar .body.scy",
+	"pack .body.scy -side right -fill y",
+
+	"pack .body.ft -side top -expand 1 -fill both",
+	"pack propagate .body.ft 0",
+
+	"pack .m .p -side top -fill x",
+	"pack .ctls -side left -fill y",
+
+	"scrollbar .body.scx -orient horizontal",
+	"pack .body.scx -side bottom -fill x",
+
+	"pack .body -expand 1 -fill both",
+
+	"pack propagate . 0",
+
+	"raise .; update; cursor -default"
+};
+
+# commands for disabling or enabling buttons
+searchoff := array[] of {
+	".m.search.menu entryconfigure 0 -state disabled",
+	".m.search.menu entryconfigure 1 -state disabled",
+	".m.search.menu entryconfigure 2 -state disabled",
+};
+searchon := array[] of {
+	".m.search.menu entryconfigure 0 -state normal",
+	".m.search.menu entryconfigure 1 -state normal",
+	".m.search.menu entryconfigure 2 -state normal",
+};
+tkstopped := array[] of {
+	".p.bpt configure -state normal -relief raised",
+	".p.detach configure -state normal -relief raised",
+	".p.kill configure -state normal -relief raised",
+	".p.out configure -state normal -relief raised",
+	".p.over configure -state normal -relief raised",
+	".p.run configure -state normal -relief raised -image Run -command {send m run}",
+	".p.step configure -state normal -relief raised",
+	".p.stmt configure -state normal -relief raised",
+};
+tkrunning := array[] of {
+	".p.bpt configure -state normal -relief raised",
+	".p.detach configure -state normal -relief raised",
+	".p.kill configure -state normal -relief raised",
+	".p.out configure -state disabled -relief sunken",
+	".p.over configure -state disabled -relief sunken",
+	".p.run configure -state normal -relief raised -image Stop -command {send m stop}",
+	".p.step configure -state disabled -relief sunken",
+	".p.stmt configure -state disabled -relief sunken",
+};
+tkexited := array[] of {
+	".p.bpt configure -state normal -relief raised",
+	".p.detach configure -state normal -relief raised",
+	".p.kill configure -state normal -relief raised",
+	".p.out configure -state disabled -relief sunken",
+	".p.over configure -state disabled -relief sunken",
+	".p.run configure -state disabled -relief sunken -image Run -command {send m run}",
+	".p.step configure -state disabled -relief sunken",
+	".p.stmt configure -state disabled -relief sunken",
+	".p.stop configure -state disabled -relief sunken",
+};
+tkloaded := array[] of {
+	".p.bpt configure -state normal -relief raised",
+	".p.detach configure -state disabled -relief sunken",
+	".p.kill configure -state disabled -relief sunken",
+	".p.out configure -state disabled -relief sunken",
+	".p.over configure -state disabled -relief sunken",
+	".p.run configure -state normal -relief raised -image Run -command {send m run}",
+	".p.step configure -state disabled -relief sunken",
+	".p.stmt configure -state disabled -relief sunken",
+};
+tknobody := array[] of {
+	".p.bpt configure -state disabled -relief sunken",
+	".p.detach configure -state disabled -relief sunken",
+	".p.kill configure -state disabled -relief sunken",
+	".p.out configure -state disabled -relief sunken",
+	".p.over configure -state disabled -relief sunken",
+	".p.run configure -state disabled -relief sunken -image Run -command {send m run}",
+	".p.step configure -state disabled -relief sunken",
+	".p.stmt configure -state disabled -relief sunken",
+};
+
+#tk option dialog
+tkoptpack := array[] of {
+	"frame .buts",
+
+	"pack .opts -side left -padx 10 -pady 5",
+};
+
+tkoptions := array[] of {
+	# general options
+	"frame .gen",
+	"frame .mod",
+	"label .modlab -text 'Source of executable module",
+	"entry .modent",
+	"pack .modlab -in .mod -anchor w",
+	"pack .modent -in .mod -fill x",
+
+	"frame .arg",
+	"label .arglab -text 'Program Arguments",
+	"entry .argent -width 300",
+	"pack .arglab -in .arg -anchor w",
+	"pack .argent -in .arg -fill x",
+
+	"frame .wd",
+	"label .wdlab -text 'Working Directory",
+	"entry .wdent",
+	"pack .wdlab -in .wd -anchor w",
+	"pack .wdent -in .wd -fill x",
+
+	"pack .mod .arg .wd -fill x -anchor w -pady 10 -in .gen",
+
+	# thread control options
+	"frame .prog",
+	"frame .new",
+	"radiobutton .new.run -variable new -value r -text 'Run new threads",
+	"radiobutton .new.block -variable new -value b  -text 'Block new threads",
+	"pack .new.block .new.run -anchor w",
+	"frame .x",
+	"radiobutton .x.kill -variable exit -value k -text 'Kill threads on exit",
+	"radiobutton .x.detach -variable exit -value d -text 'Detach threads on exit",
+	"pack .x.kill .x.detach -anchor w",
+	"pack .new .x -expand 1 -anchor w -in .prog",
+
+	# layout options
+	"frame .layout",
+	"frame .line",
+	"radiobutton .line.wrap -variable wrap -value w -text 'Wrap lines",
+	"radiobutton .line.scroll -variable wrap -value s -text 'Horizontal scroll",
+	"pack .line.wrap .line.scroll -anchor w",
+	"frame .crlf",
+	"radiobutton .crlf.no -variable crlf -value n -text 'CR/LF as is",
+	"radiobutton .crlf.yes -variable crlf -value y -text 'CR/LF -> LF",
+	"pack .crlf.no .crlf.yes -anchor w",
+	"pack .line .crlf -expand 1 -anchor w -in .layout",
+};
+
+tkopttabs := array[] of {
+	("General",	".gen"),
+	("Thread",	".prog"),
+	("Layout",	".layout"),
+};
+
+# prog listing dialog box
+tkpicktab := array[] of {
+	"frame .progs",
+	"scrollbar .progs.s -command '.progs.p yview",
+	"listbox .progs.p -width 35w -yscrollcommand '.progs.s set",
+	"bind .progs.p <Double-Button-1> 'send cmd prog",
+	"pack .progs.s -side right -fill y",
+	"pack .progs.p -fill both -expand 1",
+
+	"frame .buts",
+	"button .buts.prog -text {Add Thread} -command 'send cmd prog",
+	"button .buts.grp -text {Add Group} -command 'send cmd group",
+	"pack .buts.prog .buts.grp -expand 1 -side left -fill x -padx 4 -pady 4",
+
+	"pack .progs -fill both -expand 1",
+	"pack .buts -fill x",
+	"pack propagate . 0",
+};
+
+Bpt: adt
+{
+	id:	int;
+	m:	ref Mod;
+	pc:	int;
+};
+
+Recv, Send, Alt, Running, Stopped, Exited, Broken, Killing, Killed: con iota;
+status := array[] of
+{
+	Running =>	"Running",
+	Recv =>		"Receive",
+	Send =>		"Send",
+	Alt =>		"Alt",
+	Stopped =>	"Stopped",
+	Exited =>	"Exited",
+	Broken =>	"Broken",
+	Killing =>	"Killed",
+	Killed =>	"Killed",
+};
+
+tktools : array of array of string;
+toolstate : array of string;
+
+KidGrab, KidStep, KidStmt, KidOver, KidOut, KidKill, KidRun: con iota;
+Kid: adt
+{
+	state:	int;
+	prog:	ref Prog;
+	watch:	int;		# pid of watching prog
+	run:	int;		# pid of stepping prog
+	pickup:	int;		# picking up this kid?
+	cmd:	chan of int;
+	stack:	ref Vars;
+};
+
+Options: adt
+{
+	start:	string;		# src of module to start
+	mod:	ref Mod;	# module to start
+	wm:	int;		# program is a wm program?
+	path:	array of string;# search path for .src and .sbl
+	args:	list of string;	# argument for starting a kid
+	dir:	string;		# . for kid
+	tabs:	int;		# options to show
+	nrun:	int;		# run new kids?
+	xkill:	int;		# kill kids on exit?
+	xscroll: int;	# horizontal scrolling
+	remcr: int;	# CR/LF -> LF
+};
+
+tktop:		ref Tk->Toplevel;
+kids:		list of ref Kid;
+kid:		ref Kid;
+kidctxt:	ref Draw->Context;
+kidack:		chan of (ref Kid, string);
+kidevent:	chan of (ref Kid, string);
+bpts:		list of ref Bpt;
+bptid:=		1;
+title:		string;
+runok :=	0;
+context:	ref Draw->Context;
+opts:		ref Options;
+dbpid:		int;
+searchfor:	string;
+initsrc:	string;
+
+badmodule(p: string)
+{
+	sys->fprint(sys->fildes(2), "deb: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "deb: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if(tkclient == nil)
+		badmodule(Tkclient->PATH);
+	selectfile = load Selectfile Selectfile->PATH;
+	if(selectfile == nil)
+		badmodule(Selectfile->PATH);
+	dialog = load Dialog Dialog->PATH;
+	if(dialog == nil)
+		badmodule(Dialog->PATH);
+	tabs = load Tabs Tabs->PATH;
+	if(tabs == nil)
+		badmodule(Tabs->PATH);
+	str = load String String->PATH;
+	if(str == nil)
+		badmodule(String->PATH);
+	readdir = load Readdir Readdir->PATH;
+	if(readdir == nil)
+		badmodule(Readdir->PATH);
+	debug = load Debug Debug->PATH;
+	if(debug == nil)
+		badmodule(Debug->PATH);
+	debdata = load DebData DebData->PATH;
+	if(debdata == nil)
+		badmodule(DebData->PATH);
+	debsrc = load DebSrc DebSrc->PATH;
+	if(debsrc == nil)
+		badmodule(DebSrc->PATH);
+	arg = load Arg Arg->PATH;
+	if(arg == nil)
+		badmodule(Arg->PATH);
+	dbpid = sys->pctl(Sys->NEWPGRP, nil);
+	opts = ref Options;
+	opts.tabs = 0;
+	opts.nrun = 0;
+	opts.xkill = 1;
+	opts.xscroll = 0;
+	opts.remcr = 0;
+	readopts(opts);
+	sysnam := sysname();
+	context = ctxt;
+
+	grabpids: list of int;
+	arg->init(argv);
+	arg->setusage("wmdeb [-p pid]");
+	while((opt := arg->opt()) != 0){
+		case opt {
+		'f' =>
+			initsrc = arg->earg();
+		'p' =>
+			grabpids = int arg->earg() :: grabpids;
+		* =>
+			arg->usage();
+		}
+	}
+	for(argv = arg->argv(); argv != nil; argv = tl argv)
+		grabpids = int hd argv :: grabpids;
+	arg = nil;
+
+	pickdummy := chan of int;
+	pickchan := pickdummy;
+	optdummy := chan of ref Options;
+	optchan := optdummy;
+
+	tktools = array[] of {
+		Running =>	tkrunning,
+		Recv =>		tkrunning,
+		Send =>		tkrunning,
+		Alt =>		tkrunning,
+		Stopped =>	tkstopped,
+		Exited =>	tkexited,
+		Broken =>	tkexited,
+		Killing =>	tkexited,
+		Killed =>	tkexited,
+	};
+
+
+	tkclient->init();
+	selectfile->init();
+	dialog->init();
+	tabs->init();
+
+	title = sysnam+":Wmdeb";
+	titlebut := chan of string;
+	(tktop, titlebut) = tkclient->toplevel(context, nil, title, Tkclient->Appl);
+	tkcmd("cursor -bitmap cursor.wait");
+
+	debug->init();
+	kidctxt = ctxt;
+
+	stderr = sys->fildes(2);
+
+	debsrc->init(context, tktop, tkclient, selectfile, dialog, str, debug, opts.xscroll, opts.remcr);
+	(datatop, datactl, datatitle) := debdata->init(context, nil, debsrc, str, debug);
+
+	m := chan of string;
+	tk->namechan(tktop, m, "m");
+	toolstate = tknobody;
+	tkcmds(tktop, tkconfig);
+	if(!opts.xscroll){
+		tkcmd("pack forget .body.scx");
+		tkcmd("pack .body -expand 1 -fill both; update");
+	}
+
+	tkcmd("cursor -default");
+	tkclient->onscreen(tktop, nil);
+	tkclient->startinput(tktop, "kbd" :: "ptr" :: nil);
+
+	kids = nil;
+	kid = nil;
+	kidack = chan of (ref Kid, string);
+	kidevent = chan of (ref Kid, string);
+
+	# pick up a src file, a kid?
+	if(initsrc != nil)
+		open1(initsrc);
+	else if(grabpids != nil)
+		for(; grabpids != nil; grabpids = tl grabpids)
+			pickup(hd grabpids);
+
+	for(exiting := 0; !exiting || kids != nil; ){
+		tkcmd("update");
+		alt {
+		c := <-tktop.ctxt.kbd =>
+			tk->keyboard(tktop, c);
+		p := <-tktop.ctxt.ptr =>
+			tk->pointer(tktop, *p);
+		s := <-tktop.ctxt.ctl or
+		s = <-tktop.wreq or
+		s = <-titlebut =>
+			case s{
+			"exit" =>
+				if(!exiting){
+					if(opts.xkill)
+						killkids();
+					else
+						detachkids();
+					tkcmd("destroy .");
+				}
+				exiting = 1;
+				break;
+			"task" =>
+				spawn task(tktop);
+			* =>
+				tkclient->wmctl(tktop, s);
+			}
+		c := <-datatop.ctxt.kbd =>
+			tk->keyboard(datatop, c);
+		p := <-datatop.ctxt.ptr =>
+			tk->pointer(datatop, *p);
+		s := <-datactl =>
+			debdata->ctl(s);
+		s := <-datatop.wreq or
+		s = <-datatop.ctxt.ctl or
+		s = <-datatitle =>
+			case s{
+			"task" =>
+				spawn debdata->wmctl(s);
+			* =>
+				debdata->wmctl(s);
+			}
+		o := <-optchan =>
+			if(o != nil && checkopts(o))
+				opts = o;
+			optchan = optdummy;
+		p := <-pickchan =>
+			if(p < 0){
+				pickchan = pickdummy;
+				break;
+			}
+			k := pickup(p);
+			if(k != nil && k != kid){
+				kid = k;
+				refresh(k);
+			}
+		s := <-m =>
+			case s {
+			"open" =>
+				open();
+			"pickup" =>
+				if(pickchan == pickdummy){
+					pickchan = chan of int;
+					spawn pickprog(pickchan);
+				}
+			"options" =>
+				if(optchan == optdummy){
+					optchan = chan of ref Options;
+					spawn options(opts, optchan);
+				}
+			"step" =>
+				step(kid, KidStep);
+			"over" =>
+				step(kid, KidOver);
+			"out" =>
+				step(kid, KidOut);
+			"stmt" =>
+				step(kid, KidStmt);
+			"run" =>
+				step(kid, KidRun);
+			"stop" =>
+				if(kid != nil)
+					kid.prog.stop();
+			"killall" =>
+				killkids();
+			"kill" =>
+				killkid(kid);
+			"detach" =>
+				detachkid(kid);
+			"setbpt" =>
+				setbpt();
+			"look" =>
+				debsrc->search(debsrc->snarf());
+			"search" =>
+				s = dialog->getstring(context, tktop.image, "Search For");
+				if(s == ""){
+					tkcmd(".m.search.menu delete 2");
+				}else{
+					if(searchfor == "")
+						tkcmd(".m.search.menu add command -command {send m research}");
+					tkcmd(".m.search.menu entryconfigure 2 -label '/"+s);
+					debsrc->search(s);
+				}
+				searchfor = s;
+			"research" =>
+				debsrc->search(searchfor);
+			"stack" =>
+				if(debdata != nil)
+					debdata->raisex();
+			* =>
+				if(str->prefix("open ", s))
+					debsrc->showstrsrc(s[len "open ":]);
+				else if(str->prefix("seeprog ", s))
+					seekid(int s[len "seeprog ":]);
+				else if(str->prefix("seebpt ", s))
+					seebpt(int s[len "seebpt ":]);
+			}
+		(k, s) := <-kidevent =>
+			case s{
+			"recv" =>
+				if(k.state == Running)
+					k.state = Recv;
+			"send" =>
+				if(k.state == Running)
+					k.state = Send;
+			"alt" =>
+				if(k.state == Running)
+					k.state = Alt;
+			"run" =>
+				if(k.state == Recv || k.state == Send || k.state == Alt)
+					k.state = Running;
+			"exited" =>
+				k.state = Exited;
+			"interrupted" or
+			"killed" =>
+				alert("Thread "+string k.prog.id+" "+s);
+				k.state = Exited;
+			* =>
+				if(str->prefix("new ", s)){
+					nk := newkid(int s[len "new ":]);
+					if(opts.nrun)
+						step(nk, KidRun);
+					break;
+				}
+				if(str->prefix("load ", s)){
+					s = s[len "load ":];
+					if(s != nil && s[0] != '$')
+						loaded(s);
+					break;
+				}
+				if(str->prefix("child: ", s))
+					s = s[len "child: ":];
+
+				if(str->prefix("broken: ", s))
+					k.state = Broken;
+				alert("Thread "+string k.prog.id+" "+s);
+			}
+			if(k == kid && k.state != Running)
+				refresh(k);
+			k = nil;
+		(k, s) := <-kidack =>
+			if(k.state == Killing){
+				k.state = Killed;
+				k.cmd <-= KidKill;
+				k = nil;
+				break;
+			}
+			if(k.state == Killed){
+				delkid(k);
+				k = nil;
+				break;
+			}
+			case s{
+			"" or "child: breakpoint" or "child: stopped" =>
+				k.state = Stopped;
+				k.prog.unstop();
+			"prog broken" =>
+				k.state = Broken;
+			* =>
+				if(!str->prefix("child: ", s))
+					alert("Debugger error "+status[k.state]+" "+string k.prog.id+" '"+s+"'");
+			}
+			if(k == kid)
+				refresh(k);
+			if(k.pickup && opts.nrun){
+				k.pickup = 0;
+				if(k.state == Stopped)
+					step(k, KidRun);
+			}
+			k = nil;
+		}
+	}
+	exitdb();
+}
+
+task(top: ref Tk->Toplevel)
+{
+	tkclient->wmctl(top, "task");
+}
+
+open()
+{
+	pattern := list of {
+		"*.b (Limbo source files)",
+		"* (All files)"
+	};
+
+	file := selectfile->filename(context, tktop.image, "Open source file", pattern, opendir);
+	if(file != nil)
+		open1(file);
+}
+
+open1(file: string)
+{
+	(opendir, nil) = str->splitr(file, "/");
+	if(opendir == "")
+		opendir = ".";
+	m := debsrc->loadsrc(file, 1);
+	if(m == nil){
+		alert("Can't open "+file);
+		return;
+	}
+	debsrc->showmodsrc(m, ref Src((file, 1, 0), (file, 1, 0)));
+	kidstate();
+	if(opts.start == nil){
+		opts.start = file;
+		opts.mod = m;
+	}
+	if(opts.dir == "")
+		opts.dir = opendir;
+}
+
+options(oo: ref Options, r: chan of ref Options)
+{
+	(t, titlebut) := tkclient->toplevel(context, nil, "Wmdeb Options", tkclient->OK);
+
+	tkcmds(t, tkoptions);
+	tabsctl := tabs->mktabs(t, ".opts", tkopttabs, oo.tabs);
+	tkcmds(t, tkoptpack);
+
+	o := ref *oo;
+	if(o.start != nil)
+		tk->cmd(t, ".modent insert end '"+o.start);
+	args := "";
+	for(oa := o.args; oa != nil; oa = tl oa){
+		if(args == "")
+			args = hd oa;
+		else
+			args += " " + hd oa;
+	}
+	tk->cmd(t, ".argent insert end '"+args);
+	tk->cmd(t, ".wdent insert end '"+o.dir);
+	if(o.xkill)
+		tk->cmd(t, ".x.kill invoke");
+	else
+		tk->cmd(t, ".x.detach invoke");
+	if(o.nrun)
+		tk->cmd(t, ".new.run invoke");
+	else
+		tk->cmd(t, ".new.block invoke");
+	if(o.xscroll)
+		tk->cmd(t, ".line.scroll invoke");
+	else
+		tk->cmd(t, ".line.wrap invoke");
+	if(o.remcr)
+		tk->cmd(t, ".crlf.yes invoke");
+	else
+		tk->cmd(t, ".crlf.no invoke");
+
+	tk->cmd(t, ".killkids configure -command 'send cmd kill");
+	tk->cmd(t, ".runkids configure -command 'send cmd run");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "ptr" :: "kbd" :: nil);
+
+out:	for(;;){
+		tk->cmd(t, "update");
+		alt{
+		c := <-t.ctxt.kbd =>
+			tk->keyboard(t, c);
+		m := <-t.ctxt.ptr =>
+			tk->pointer(t, *m);
+		s := <-tabsctl =>
+			o.tabs = tabs->tabsctl(t, ".opts", tkopttabs, o.tabs, s);
+		s := <-t.ctxt.ctl or
+		s = <-t.wreq or
+		s = <-titlebut =>
+			case s{
+			"exit" =>
+				r <-= nil;
+				exit;
+			"ok" =>
+				break out;
+			}
+			tkclient->wmctl(t, s);
+		}
+	}
+	xscroll := o.xscroll;
+	o.start = tk->cmd(t, ".modent get");
+	(nil, o.args) = sys->tokenize(tk->cmd(t, ".argent get"), " \t\n");
+	o.dir = tk->cmd(t, ".wdent get");
+	case tk->cmd(t, "variable new"){
+	"r" => o.nrun = 1;
+	"b" => o.nrun = 0;
+	}
+	case tk->cmd(t, "variable exit"){
+	"k" => o.xkill = 1;
+	"d" => o.xkill = 0;
+	}
+	case tk->cmd(t, "variable wrap"){
+	"s" => o.xscroll = 1;
+	"w" => o.xscroll = 0;
+	}
+	case tk->cmd(t, "variable crlf"){
+	"y" => o.remcr = 1;
+	"n" => o.remcr = 0;
+	}
+	if(o.xscroll != xscroll){
+		if(o.xscroll)
+			tkcmd("pack .body.scx -side bottom -fill x");
+		else
+			tkcmd("pack forget .body.scx");
+		tkcmd("pack .body -expand 1 -fill both; update");
+	}
+	debsrc->reinit(o.xscroll, o.remcr);
+	writeopts(o);
+	r <-= o;
+}
+
+checkopts(o: ref Options): int
+{
+	if(o.start != ""){
+		o.mod = debsrc->loadsrc(o.start, 1);
+		if(o.mod == nil)
+			o.start = "";
+	}
+	return 1;
+}
+
+pickprog(c: chan of int)
+{
+	(t, titlebut) := tkclient->toplevel(context, nil, "Wmdeb Thread List", 0);
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	tkcmds(t, tkpicktab);
+	tk->cmd(t, "update");
+	ids := addpickprogs(t);
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "ptr" :: "kbd" :: nil);
+
+	for(;;){
+		tk->cmd(t, "update");
+		alt{
+		key := <-t.ctxt.kbd =>
+			tk->keyboard(t, key);
+		m := <-t.ctxt.ptr =>
+			tk->pointer(t, *m);
+		s := <-t.ctxt.ctl or
+		s = <-t.wreq or
+		s = <-titlebut =>
+			if(s == "exit"){
+				c <-= -1;
+				exit;
+			}
+			tkclient->wmctl(t, s);
+		s := <-cmd =>
+			case s{
+			"ok" =>
+				c <-= -1;
+				exit;
+			"prog" =>
+				sel := tk->cmd(t, ".progs.p curselection");
+				if(sel == "")
+					break;
+				pid := int tk->cmd(t, ".progs.p get "+sel);
+				c <-= pid;
+			"group" =>
+				sel := tk->cmd(t, ".progs.p curselection");
+				if(sel == "")
+					break;
+				nid := int sel;
+				if(nid > len ids || nid < 0)
+					break;
+				(nil, gid) := ids[nid];
+				nid = len ids;
+				for(i := 0; i < nid; i++){
+					(p, g) := ids[i];
+					if(g == gid)
+						c <-= p;
+				}
+			}
+		}
+	}
+}
+
+addpickprogs(t: ref Tk->Toplevel): array of (int, int)
+{
+	(d, n) := readdir->init("/prog", Readdir->NONE);
+	if(n <= 0)
+		return nil;
+	a := array[n] of { * => (-1, -1) };
+	for(i := 0; i < n; i++){
+		(p, nil) := debug->prog(int d[i].name);
+		if(p == nil)
+			continue;
+		(grp, nil, st, code) := debug->p.status();
+		if(grp < 0)
+			continue;
+		a[i] = (p.id, grp);
+		tk->cmd(t, ".progs.p insert end '"+
+				sys->sprint("%4d %4d %8s %s", p.id, grp, st, code));
+	}
+	return a;
+}
+
+step(k: ref Kid, cmd: int)
+{
+	if(k == nil){
+		if(kids != nil){
+			alert("No current thread");
+			return;
+		}
+		k = spawnkid(opts);
+		kid = k;
+		if(k != nil)
+			refresh(k);
+		return;
+	}
+	case k.state{
+	Stopped =>
+		k.cmd <-= cmd;
+		k.state = Running;
+		if(k == kid)
+			kidstate();
+	Running or Send or Recv or Alt or Exited or Broken =>
+		;
+	* =>
+		sys->print("bad debug step state %d\n", k.state);
+	}
+}
+
+setbpt()
+{
+	(m, pc) := debsrc->getsel();
+	if(m == nil)
+		return;
+	s := m.sym.pctosrc(pc);
+	if(s == nil){
+		alert("No pc is appropriate");
+		return;
+	}
+
+	# if the breakpoint is already there, delete it
+	for(bl := bpts; bl != nil; bl = tl bl){
+		b := hd bl;
+		if(b.m == m && b.pc == pc){
+			bpts = delbpt(b, bpts);
+			return;
+		}
+	}
+
+	b := ref Bpt(bptid++, m, pc);
+	bpts = b :: bpts;
+	debsrc->attachdis(m);
+	for(kl := kids; kl != nil; kl = tl kl){
+		k := hd kl;
+		k.prog.setbpt(m.dis, pc);
+	}
+
+	# mark the breakpoint text
+	tkcmd(m.tk+" tag add bpt "+string s.start.line+"."+string s.start.pos+" "+string s.stop.line+"."+string s.stop.pos);
+
+	# add the kid to the breakpoint window
+	me := ".bpt.v."+string b.id;
+	tkcmd("label "+me+" -text "+string b.id);
+	tkcmd("pack "+me+" -side top -fill x");
+	tkcmd("bind "+me+" <ButtonRelease-1> {send m seebpt "+string b.id+"}");
+	updatebpts();
+}
+
+seebpt(bpt: int)
+{
+	for(bl := bpts; bl != nil; bl = tl bl){
+		b := hd bl;
+		if(b.id == bpt){
+			s := b.m.sym.pctosrc(b.pc);
+			debsrc->showmodsrc(b.m, s);
+			return;
+		}
+	}
+}
+
+delbpt(b: ref Bpt, bpts: list of ref Bpt): list of ref Bpt
+{
+	if(bpts == nil)
+		return nil;
+	hb := hd bpts;
+	tb := tl bpts;
+	if(b == hb){
+		# remove mark from breakpoint text
+		s := b.m.sym.pctosrc(b.pc);
+		tkcmd(b.m.tk+" tag remove bpt "+string s.start.line+"."+string s.start.pos+" "+string s.stop.line+"."+string s.stop.pos);
+	
+		# remove the breakpoint window
+		tkcmd("destroy .bpt.v."+string b.id);
+
+		# remove from kids
+		disablebpt(b);
+		return tb;
+	}
+	return hb :: delbpt(b, tb);
+
+}
+
+disablebpt(b: ref Bpt)
+{
+	for(kl := kids; kl != nil; kl = tl kl){
+		k := hd kl;
+		k.prog.delbpt(b.m.dis, b.pc);
+	}
+}
+
+updatebpts()
+{
+tkcmd("update");
+	tkcmd(".bpt.d configure -scrollregion {0 0 [.bpt.v cget -width] [.bpt.v cget -height]}");
+}
+
+seekid(pid: int)
+{
+	for(kl := kids; kl != nil; kl = tl kl){
+		k := hd kl;
+		if(k.prog.id == pid){
+			kid = k;
+			kid.stack.show();
+			refresh(kid);
+			return;
+		}
+	}
+}
+
+delkid(k: ref Kid)
+{
+	kids = rdelkid(k, kids);
+	if(kid == k){
+		if(kids == nil){
+			kid = nil;
+			kidstate();
+		}else{
+			kid = hd kids;
+			refresh(kid);
+		}
+	}
+}
+
+rdelkid(k: ref Kid, kids: list of ref Kid): list of ref Kid
+{
+	if(kids == nil)
+		return nil;
+	hk := hd kids;
+	t := tl kids;
+	if(k == hk){
+		# remove kid from display
+		k.stack.delete();
+		tkcmd("destroy .prog.v."+string k.prog.id);
+		updatekids();
+		return t;
+	}
+	return hk :: rdelkid(k, t);
+}
+
+updatekids()
+{
+tkcmd("update");
+	tkcmd(".prog.d configure -scrollregion {0 0 [.prog.v cget -width] [.prog.v cget -height]}");
+}
+
+killkids()
+{
+	for(kl := kids; kl != nil; kl = tl kl)
+		killkid(hd kl);
+}
+
+killkid(k: ref Kid)
+{
+	if(k.watch >= 0){
+		killpid(k.watch);
+		k.watch = -1;
+	}
+	case k.state{
+	Exited or Broken or Stopped =>
+		k.cmd <-= KidKill;
+		k.state = Killed;
+	Running or Send or Recv or Alt or Killing =>
+		k.prog.kill();
+		k.state = Killing;
+	* =>
+		sys->print("unknown state %d in killkid\n", k.state);
+	}
+}
+
+freekids(): int
+{
+	r := 0;
+	for(kl := kids; kl != nil; kl = tl kl){
+		k := hd kl;
+		if(k.state == Exited || k.state == Killing || k.state == Killed){
+			r ++;
+			detachkid(k);
+		}
+	}
+	return r;
+}
+
+detachkids()
+{
+	for(kl := kids; kl != nil; kl = tl kl)
+		detachkid(hd kl);
+}
+
+detachkid(k: ref Kid)
+{
+	if(k == nil){
+		alert("No current thread");
+		return;
+	}
+	if(k.state == Exited){
+		killkid(k);
+		return;
+	}
+
+	# kill off the debugger progs
+	killpid(k.watch);
+	killpid(k.run);
+	err := k.prog.start();
+	if(err != "")
+		alert("Detaching thread: "+err);
+
+	delkid(k);
+}
+
+kidstate()
+{
+	ts : array of string;
+	if(kid == nil){
+		tkcmd(".Wm_t.title configure -text '"+title);
+		if(debsrc->packed == nil){
+			tkcmds(tktop, searchoff);
+			ts = tknobody;
+		}else{
+			ts = tkloaded;
+			tkcmds(tktop, searchon);
+		}
+	}else{
+		tkcmd(".Wm_t.title configure -text '"+title+" "+string kid.prog.id+" "+status[kid.state]);
+		ts = tktools[kid.state];
+		tkcmds(tktop, searchon);
+	}
+	if(ts != toolstate){
+		toolstate = ts;
+		tkcmds(tktop, ts);
+	}
+}
+
+#
+# update the stack an src displays
+# to reflect the current state of k
+#
+refresh(k: ref Kid)
+{
+	if(k.state == Killing || k.state == Killed){
+		kidstate();
+		return;
+	}
+	(s, err) := k.prog.stack();
+	if(s == nil && err == "")
+		err = "No stack";
+	if(err != ""){
+		kidstate();
+		return;
+	}
+	for(i := 0; i < len s; i++){
+		debsrc->findmod(s[i].m);
+		s[i].findsym();
+	}
+	err = s[0].findsym();
+	src := s[0].src();
+	kidstate();
+	m := s[0].m;
+	if(src == nil && len s > 1){
+		dis := s[0].m.dis();
+		if(len dis > 0 && dis[0] == '$'){
+			m = s[1].m;
+			s[1].findsym();
+			src = s[1].src();
+		}
+	}
+	debsrc->showmodsrc(debsrc->findmod(m), src);
+	k.stack.refresh(s);
+	k.stack.show();
+}
+
+pickup(pid: int): ref Kid
+{
+	for(kl := kids; kl != nil; kl = tl kl)
+		if((hd kl).prog.id == pid)
+			return hd kl;
+	k := newkid(pid);
+	if(k == nil)
+		return nil;
+	k.cmd <-= KidGrab;
+	k.state = Running;
+	k.pickup = 1;
+	if(kid == nil){
+		kid = k;
+		refresh(kid);
+	}
+	return k;
+}
+
+loaded(s: string)
+{
+	for(bl := bpts; bl != nil; bl = tl bl){
+		b := hd bl;
+		debsrc->attachdis(b.m);
+		if(s == b.m.dis){
+			for(kl := kids; kl != nil; kl = tl kl)
+				(hd kl).prog.setbpt(s, b.pc);
+		}
+	}
+}
+
+Enofd: con "no free file descriptors\n";
+
+newkid(pid: int): ref Kid
+{
+	(p, err) := debug->prog(pid);
+	if(err != ""){
+		n := len err - len Enofd;
+		if(n >= 0 && err[n: ] == Enofd && freekids()){
+			(p, err) = debug->prog(pid);
+			if(err == "")
+				return mkkid(p);
+		}
+		alert("Can't pick up thread "+err);
+		return nil;
+	}
+	return mkkid(p);
+}
+
+mkkid(p: ref Prog): ref Kid
+{
+	for(bl := bpts; bl != nil; bl = tl bl){
+		b := hd bl;
+		debsrc->attachdis(b.m);
+		p.setbpt(b.m.dis, b.pc);
+	}
+	k := ref Kid(Stopped, p, -1, -1, 0, chan of int, Vars.create());
+	kids = k :: kids;
+	c := chan of int;
+	spawn kidslave(k, c);
+	k.run = <- c;
+	spawn kidwatch(k, c);
+	k.watch = <-c;
+	me := ".prog.v."+string p.id;
+	tkcmd("label "+me+" -text "+string p.id);
+	tkcmd("pack "+me+" -side top -fill x");
+	tkcmd("bind "+me+" <ButtonRelease-1> {send m seeprog "+string p.id+"}");
+	tkcmd(".prog.d configure -scrollregion {0 0 [.prog.v cget -width] [.prog.v cget -height]}");
+	return k;
+}
+
+spawnkid(o: ref Options): ref Kid
+{
+	m := o.mod;
+	if(m == nil){
+		alert("No module to run");
+		return nil;
+	}
+
+	if(!debsrc->attachdis(m)){
+		alert("Can't load Dis file "+m.dis);
+		return nil;
+	}
+
+	(p, err) := debug->startprog(m.dis, o.dir, kidctxt, m.dis :: o.args);
+	if(err != nil){
+		alert(m.dis+" is not a debuggable Dis command module: "+err);
+		return nil;
+	}
+
+	return mkkid(p);
+}
+
+xlate := array[] of {
+	KidStep => Debug->StepExp,
+	KidStmt => Debug->StepStmt,
+	KidOver => Debug->StepOver,
+	KidOut => Debug->StepOut,
+};
+
+kidslave(k: ref Kid, me: chan of int)
+{
+	me <-= sys->pctl(0, nil);
+	me = nil;
+	for(;;){
+		c := <-k.cmd;
+		case c{
+		KidGrab =>
+			err := k.prog.grab();
+			kidack <-= (k, err);
+		KidStep or KidStmt or KidOver or KidOut =>
+			err := k.prog.step(xlate[c]);
+			kidack <-= (k, err);
+		KidKill =>
+			err := "kill "+k.prog.kill();
+			k.prog.kill();			# kill again to slay blocked progs
+			kidack <-= (k, err);
+			exit;
+		KidRun =>
+			err := k.prog.cont();
+			kidack <-= (k, err);
+		* =>
+			sys->print("kidslave: bad command %d\n", c);
+			exit;
+		}
+	}
+}
+
+kidwatch(k: ref Kid, me: chan of int)
+{
+	me <-= sys->pctl(0, nil);
+	me = nil;
+	for(;;)
+		kidevent <-= (k, k.prog.event());
+}
+
+alert(m: string)
+{
+	dialog->prompt(context, tktop.image, "warning -fg yellow",
+		"Debugger Alert", m, 0, "Dismiss"::nil);
+}
+
+tkcmd(cmd: string): string
+{
+	s := tk->cmd(tktop, cmd);
+#	if(len s != 0 && s[0] == '!')
+#		sys->print("%s '%s'\n", s, cmd);
+	return s;
+}
+
+sysname(): string
+{
+	fd := sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return "Anon";
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return "Anon";
+	return string buf[:n];
+}
+
+tkcmds(top: ref Tk->Toplevel, cmds: array of string)
+{
+	for(i := 0; i < len cmds; i++)
+		tk->cmd(top, cmds[i]);
+}
+
+exitdb()
+{
+	fd := sys->open("#p/"+string dbpid+"/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+	exit;
+}
+
+killpid(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+getuser(): string
+{
+  	fd := sys->open("/dev/user", Sys->OREAD);
+  	if(fd == nil)
+    		return "";
+  	buf := array[128] of byte;
+  	n := sys->read(fd, buf, len buf);
+  	if(n < 0)
+    		return "";
+  	return string buf[0:n];	
+}
+
+debconf(): string
+{
+	return "/usr/" + getuser() + "/lib/deb";
+}
+
+readopts(o: ref Options)
+{
+	fd := sys->open(debconf(), Sys->OREAD);
+	if(fd == nil)
+		return;
+	b := array[4] of byte;
+	if(sys->read(fd, b, 4) != 4)
+		return;
+	o.nrun = int b[0]-'0';
+	o.xkill = int b[1]-'0';
+	o.xscroll = int b[2]-'0';
+	o.remcr = int b[3]-'0';
+}
+
+writeopts(o: ref Options)
+{
+	fd := sys->create(debconf(), Sys->OWRITE, 8r660);
+	if(fd == nil)
+		return;
+	b := array[4] of byte;
+	b[0] = byte (o.nrun+'0');
+	b[1] = byte (o.xkill+'0');
+	b[2] = byte (o.xscroll+'0');
+	b[3] = byte (o.remcr+'0');
+	sys->write(fd, b, 4);
+}
--- /dev/null
+++ b/appl/wm/debdata.b
@@ -1,0 +1,418 @@
+implement DebData;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+
+include "selectfile.m";
+
+include "debug.m";
+	debug: Debug;
+	Sym, Src, Exp, Module: import debug;
+
+include "wmdeb.m";
+	debsrc: DebSrc;
+
+DatumSize:	con 32;
+WalkWidth:	con "20";
+
+context:		ref Draw->Context;
+tktop:		ref Tk->Toplevel;
+var:		ref Vars;
+vid:		int;
+tkids :=	1;	# increasing id of tk pieces
+
+icondir :	con "debug/";
+
+tkconfig := array[] of {
+	"frame .body -width 400 -height 400",
+	"pack .Wm_t -side top -fill x",
+	"pack .body -expand 1 -fill both",
+	"pack propagate . 0",
+	"update",
+	"image create bitmap Itemopen -file "+icondir+
+			"open.bit -maskfile "+icondir+"open.mask",
+	"image create bitmap Itemclosed -file "+icondir+
+			"closed.bit -maskfile "+icondir+"closed.mask",
+};
+
+init(acontext: ref Draw->Context,
+	geom: string,
+	adebsrc: DebSrc,
+	astr: String,
+	adebug: Debug): (ref Tk->Toplevel, chan of string, chan of string)
+{
+	context = acontext;
+	debsrc = adebsrc;
+	sys = load Sys Sys->PATH;
+	tk = load Tk Tk->PATH;
+	str = astr;
+	debug = adebug;
+
+	tkclient = load Tkclient Tkclient->PATH;
+
+	tkclient->init();
+	titlebut: chan of string;
+	(tktop, titlebut) = tkclient->toplevel(context, geom, "Stack", Tkclient->Resize);
+	buts := chan of string;
+	tk->namechan(tktop, buts, "buts");
+
+	for(i := 0; i < len tkconfig; i++)
+		tk->cmd(tktop, tkconfig[i]);
+
+	tkcmd("update");
+	tkclient->onscreen(tktop, nil);
+	tkclient->startinput(tktop, "kbd" :: "ptr" :: nil);
+	return (tktop, buts, titlebut);
+}
+
+ctl(s: string)
+{
+	if(var == nil)
+		return;
+	arg := s[1:];
+	case s[0]{
+	'o' =>
+		var.expand(arg);
+		var.update();
+	'c' =>
+		var.contract(arg);
+		var.update();
+	'y' =>
+		var.scrolly(arg);
+	's' =>
+		var.showsrc(arg);
+	}
+	tkcmd("update");
+}
+
+wmctl(s: string)
+{
+	if(s == "exit"){
+		tkcmd(". unmap");
+		return;
+	}
+	tkclient->wmctl(tktop, s);
+	tkcmd("update");
+}
+
+Vars.create(): ref Vars
+{
+	t := ".body.v"+string vid++;
+
+	tkcmd("frame "+t);
+	tkcmd("canvas "+t+".cvar -width 2 -height 2 -yscrollcommand {"+t+".sy set} -xscrollcommand {"+t+".sxvar set}");
+	tkcmd("frame "+t+".f0");
+
+	tkcmd(t+".cvar create window 0 0 -window "+t+".f0 -anchor nw");
+	tkcmd("scrollbar "+t+".sxvar -orient horizontal -command {"+t+".cvar xview}");
+
+	tkcmd("scrollbar "+t+".sy -command {send buts y}");
+	tkcmd("pack "+t+".sy -side right -fill y -in "+t);
+	tkcmd("pack "+t+".sxvar -fill x -side bottom -in "+t);
+	tkcmd("pack "+t+".cvar -expand 1 -fill both -in "+t);
+
+	return ref Vars(t, 0, nil);
+}
+
+Vars.show(v: self ref Vars)
+{
+	if(v == var)
+		return;
+	if(var != nil)
+		tkcmd("pack forget "+var.tk);
+	var = v;
+	tkcmd("pack "+var.tk+" -expand 1 -fill both");
+	v.update();
+}
+
+Vars.delete(v: self ref Vars)
+{
+	if(var == v)
+		var = nil;
+	tkcmd("destroy "+v.tk);
+	tkcmd("update");
+}
+
+Vars.refresh(v: self ref Vars, ea: array of ref Exp)
+{
+	nea := len ea;
+	newd := array[nea] of ref Datum;
+	da := v.d;
+	nd := len da;
+	n := nea;
+	if(n > nd)
+		n = nd;
+	for(i := 0; i < n; i++){
+		d := da[nd-i-1];
+		if(!sameexp(ea[nea-i-1], d.e, 1))
+			break;
+		newd[nea-i-1] = d;
+	}
+	n = nea-i;
+	for(; i < nd; i++)
+		da[nd-i-1].destroy();
+	v.d = nil;
+	for(i = 0; i < n; i++){
+		debsrc->findmod(ea[i].m);
+		ea[i].findsym();
+		newd[i] = mkkid(ea[i], v.tk, "0", string tkids++, nil, nil, -1, "");
+	}
+	for(; i < nea; i++){
+		debsrc->findmod(ea[i].m);
+		ea[i].findsym();
+		d := newd[i];
+		newd[i] = mkkid(ea[i], v.tk, "0", d.tkid, d.kids, d.val, d.canwalk, "");
+	}
+	v.d = newd;
+	v.update();
+}
+
+Vars.update(v: self ref Vars)
+{
+	tkcmd("update");
+	tkcmd(v.tk+".cvar configure -scrollregion {0 0 ["+v.tk+".f0 cget -width] ["+v.tk+".f0 cget -height]}");
+	tkcmd("update");
+}
+
+Vars.scrolly(v: self ref Vars, pos: string)
+{
+	tkcmd(v.tk+".cvar yview"+pos);
+}
+
+Vars.showsrc(v: self ref Vars, who: string)
+{
+	(sid, kids) := str->splitl(who[1:], ".");
+	showsrc(v.d, sid, kids);
+}
+
+showsrc(da: array of ref Datum, id, kids: string)
+{
+	if(da == nil)
+		return;
+	for(i := 0; i < len da; i++){
+		d := da[i];
+		if(d.tkid != id)
+			continue;
+		if(kids == "")
+			d.showsrc();
+		else{
+			sid : string;
+			(sid, kids) = str->splitl(kids[1:], ".");
+			showsrc(d.kids, sid, kids);
+		}
+		break;
+	}
+}
+
+Vars.expand(v: self ref Vars, who: string)
+{
+	(sid, kids) := str->splitl(who[1:], ".");
+	v.d = expandkid(v.d, sid, kids, who);
+}
+
+expandkid(da: array of ref Datum, id, kids, who: string): array of ref Datum
+{
+	if(da == nil)
+		return nil;
+	for(i := 0; i < len da; i++){
+		d := da[i];
+		if(d.tkid != id)
+			continue;
+		if(kids == "")
+			da[i] = d.expand(nil, who);
+		else{
+			sid : string;
+			(sid, kids) = str->splitl(kids[1:], ".");
+			d.kids = expandkid(d.kids, sid, kids, who);
+		}
+		break;
+	}
+	return da;
+}
+
+Vars.contract(v: self ref Vars, who: string)
+{
+	(sid, kids) := str->splitl(who[1:], ".");
+	v.d = contractkid(v.d, sid, kids, who);
+}
+
+contractkid(da: array of ref Datum, id, kids, who: string): array of ref Datum
+{
+	if(da == nil)
+		return nil;
+	for(i := 0; i < len da; i++){
+		d := da[i];
+		if(d.tkid != id)
+			continue;
+		if(kids == "")
+			da[i] = d.contract(who);
+		else{
+			sid : string;
+			(sid, kids) = str->splitl(kids[1:], ".");
+			d.kids = contractkid(d.kids, sid, kids, who);
+		}
+		break;
+	}
+	return da;
+}
+
+Datum.contract(d: self ref Datum, who: string): ref Datum
+{
+	vtk := d.vtk;
+	tkid := d.tkid;
+	if(tkid == "")
+		return d;
+	kids := d.kids;
+	if(kids == nil){
+		tkcmd(vtk+".v"+tkid+".b configure -image Itemclosed -command {send buts o"+who+"}");
+		return d;
+	}
+
+	for(i := 0; i < len kids; i++)
+		kids[i].destroy();
+	d.kids = nil;
+	tkcmd("destroy "+vtk+".f"+tkid);
+	tkcmd(vtk+".v"+tkid+".b configure -image Itemclosed -command {send buts o"+who+"}");
+
+	return d;
+}
+
+Datum.showsrc(d: self ref Datum)
+{
+	debsrc->showmodsrc(debsrc->findmod(d.e.m), d.e.src());
+}
+
+Datum.destroy(d: self ref Datum)
+{
+	kids := d.kids;
+	for(i := 0; i < len kids; i++)
+		kids[i].destroy();
+	vtk := d.vtk;
+	tkid := string d.tkid;
+	if(d.kids != nil){
+		tkcmd("destroy "+vtk+".f"+tkid);
+	}
+	d.kids = nil;
+	tkcmd("destroy "+vtk+".v"+tkid);
+}
+
+mkkid(e: ref Exp, vtk, parent, me: string, okids: array of ref Datum, oval:string, owalk: int, who: string): ref Datum
+{
+	(val, walk) := e.val();
+
+	who = who+"."+me;
+
+	# make the tk goo
+	if(walk != owalk){
+		if(owalk == -1){
+			tkcmd("frame "+vtk+".v"+me);
+			tkcmd("label "+vtk+".v"+me+".l -text '"+e.name);
+			tkcmd("bind "+vtk+".v"+me+".l <ButtonRelease-1> 'send buts s"+who);
+		}else{
+			tkcmd("destroy "+vtk+".v"+me+".b");
+		}
+		if(walk)
+			tkcmd("button "+vtk+".v"+me+".b -image Itemclosed -command 'send buts o"+who);
+		else
+			tkcmd("frame "+vtk+".v"+me+".b -width "+WalkWidth);
+	}
+
+	n := 16 - len e.name;
+	if(n < 4)
+		n = 4;
+	pad := "                "[:n];
+
+	# tk value goo
+	if(val == "")
+		val = " ";
+	if(oval != ""){
+		if(val != oval)
+			tkcmd(vtk+".v"+me+".val configure -text '"+pad+val);
+	}else
+		tkcmd("label "+vtk+".v"+me+".val -text '"+pad+val);
+
+	tkcmd("pack "+vtk+".v"+me+".b "+vtk+".v"+me+".l "+vtk+".v"+me+".val -side left");
+	tkcmd("pack "+vtk+".v"+me+" -side top -anchor w -in "+vtk+".f"+parent);
+
+	d := ref Datum(me, parent, vtk, e, val, walk, nil);
+	if(okids != nil){
+		if(walk)
+			return d.expand(okids, who);
+		for(i := 0; i < len okids; i++)
+			okids[i].destroy();
+	}
+	return d;
+}
+
+Datum.expand(d: self ref Datum, okids: array of ref Datum, who: string): ref Datum
+{
+	e := d.e.expand();
+	if(e == nil)
+		return d;
+
+	vtk := d.vtk;
+
+	me := d.tkid;
+
+	# make the tk goo for holding kids
+	needtk := okids == nil;
+	if(needtk){
+		tkcmd("frame "+vtk+".f"+me);
+		tkcmd("frame "+vtk+".f"+me+".x -width "+WalkWidth);
+		tkcmd("frame "+vtk+".f"+me+".v");
+		tkcmd("pack "+vtk+".f"+me+".x "+vtk+".f"+me+".v -side left -fill y -expand 1");
+	}
+
+	kids := array[len e] of ref Datum;
+	for(i := 0; i < len e; i++){
+		if(i >= len okids)
+			break;
+		ok := okids[i];
+		if(!sameexp(e[i], ok.e, 0))
+			break;
+		kids[i] = mkkid(e[i], vtk, me, ok.tkid, ok.kids, ok.val, ok.canwalk, who);
+	}
+	for(oi := i; oi < len okids; oi++)
+		okids[oi].destroy();
+	for(; i < len e; i++)
+		kids[i] = mkkid(e[i], vtk, me, string tkids++, nil, nil, -1, who);
+
+	tkcmd("pack "+vtk+".f"+me+" -side top -anchor w -after "+vtk+".v"+me);
+	tkcmd(vtk+".v"+me+".b configure -image Itemopen -command {send buts c"+who+"}");
+
+	d.kids = kids;
+	return d;
+}
+
+sameexp(e, f: ref Exp, offmatch: int): int
+{
+	if(e.m != f.m || e.p != f.p || e.name != f.name)
+		return 0;
+	return !offmatch || e.offset == f.offset;
+}
+
+tkcmd(cmd: string): string
+{
+	s := tk->cmd(tktop, cmd);
+#	if(len s != 0 && s[0] == '!')
+#		sys->print("%s '%s'\n", s, cmd);
+	return s;
+}
+
+raisex()
+{
+	tkcmd(". map; raise .; update");
+}
--- /dev/null
+++ b/appl/wm/debsrc.b
@@ -1,0 +1,633 @@
+implement DebSrc;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+
+include "string.m";
+	str: String;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include "debug.m";
+	debug: Debug;
+	Sym, Src, Exp, Module: import debug;
+
+include "wmdeb.m";
+
+include	"plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg: import plumbmsg;
+
+include "workdir.m";
+	workdir: Workdir;
+
+include "dis.m";
+	dism: Dis;
+
+mods:		list of ref Mod;
+tktop:		ref Tk->Toplevel;
+context:		ref Draw->Context;
+opendir =	".";
+srcid:		int;
+xscroll, remcr:	int;
+
+sblpath :=	array[] of
+{
+	("/dis/",	"/appl/"),
+	("/dis/",	"/appl/cmd/"),
+	# ("/dis/mux/",	"/appl/mux/"),
+	# ("/dis/lib/",	"/appl/lib/"),
+	# ("/dis/wm/",	"/appl/wm/"),
+	("/dis/sh.",	"/appl/cmd/sh/sh."),
+};
+
+plumbed := 0;
+but3: chan of string;
+
+plumbbind := array[] of
+{
+	"<ButtonPress-3> {send but3 pressed}",
+	"<ButtonRelease-3> {send but3 released %x %y}",
+	"<Motion-Button-3> {}",
+	"<Double-Button-3> {}",
+	"<Double-ButtonRelease-3> {}",
+};
+
+init(acontext: ref Draw->Context,
+	atktop: ref Tk->Toplevel,
+	atkclient: Tkclient,
+	aselectfile: Selectfile,
+	adialog: Dialog,
+	astr: String,
+	adebug: Debug,
+	xscr: int,
+	rcr: int)
+{
+	context = acontext;
+	tktop = atktop;
+	sys = load Sys Sys->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = atkclient;
+	selectfile = aselectfile;
+	dialog = adialog;
+	str = astr;
+	debug = adebug;
+	xscroll = xscr;
+	remcr = rcr;
+
+	plumbmsg = load Plumbmsg Plumbmsg->PATH;
+	if(plumbmsg->init(1, nil, 0) >= 0){
+		plumbed = 1;
+		workdir = load Workdir Workdir->PATH;
+	}
+}
+
+reinit(xscr: int, rcr: int)
+{
+	if(xscroll == xscr && remcr == rcr)
+		return;
+	xscroll = xscr;
+	remcr = rcr;
+	for(ml := mods; ml != nil; ml = tl ml){
+		m := hd ml;
+		if(xscroll)
+			tkcmd(m.tk+" configure  -wrap none");
+		else
+			tkcmd(m.tk+" configure -wrap char");
+		tkcmd("update");
+		fd := sys->open(m.src, sys->OREAD);
+		if(fd != nil)
+			loadfile(m.tk, fd);
+	}
+}
+
+#
+# make a Mod with a text widget for source file src
+#
+loadsrc(src: string, addpath: int): ref Mod
+{
+	if(src == "")
+		return nil;
+
+	m : ref Mod = nil;
+	for(ml := mods; ml != nil; ml = tl ml){
+		m = hd ml;
+		if(m.src == src || filesuffix(src, m.src))
+			break;
+	}
+
+	if(ml == nil || m.tk == nil){
+		if(ml == nil)
+			m = ref Mod(src, nil, nil, nil, 0, 1);
+		fd := sys->open(src, sys->OREAD);
+		if(fd == nil)
+			return nil;
+		(dir, file) := str->splitr(src, "/");
+		m.tk = ".t."+tk->quote(file)+string srcid++;
+		if(xscroll)
+			tkcmd("text "+m.tk+" -bd 0 -state disabled -wrap none");
+		else
+			tkcmd("text "+m.tk+" -bd 0 -state disabled");
+		if (but3 == nil) {
+			but3 = chan of string;
+			spawn but3proc();
+		}
+		tk->namechan(tktop, but3, "but3");
+		for (i := 0; i < len plumbbind; i++)
+			tkcmd("bind "+m.tk+" "+plumbbind[i]);
+		tkcmd(m.tk+" configure -insertwidth 2");
+		opack := packed;
+		packm(m);
+		if(!loadfile(m.tk, fd)){
+			fd = nil;
+			packm(opack);
+			tkcmd("destroy "+m.tk);
+			return nil;
+		}
+		fd = nil;
+		tkcmd(m.tk+" tag configure bpt -foreground #c00");
+		tkcmd(".m.file.menu add command -label "+src+" -command {send m open "+src+"}");
+		if(ml == nil)
+			mods = m :: mods;
+
+		if(addpath)
+			addsearch(dir);
+	}
+	return m;
+}
+
+addsearch(dir: string)
+{
+	for(i := 0; i < len searchpath; i++)
+		if(searchpath[i] == dir)
+			return;
+	s := array[i+1] of string;
+	s[0:] = searchpath;
+	s[i] = dir;
+	searchpath = s;
+}
+
+#
+# bring up the widget for src, if it exists
+#
+showstrsrc(src: string)
+{
+	m : ref Mod = nil;
+	for(ml := mods; ml != nil; ml = tl ml){
+		m = hd ml;
+		if(m.src == src)
+			break;
+	}
+	if(ml == nil)
+		return;
+
+	packm(m);
+}
+
+#
+# bring up the widget for module
+# at position s
+#
+showmodsrc(m: ref Mod, s: ref Src)
+{
+	if(s == nil)
+		return;
+
+	src := s.start.file;
+	if(src != s.stop.file)
+		s.stop = s.start;
+
+	if(m == nil || m.tk == nil || m.src != src){
+		m1 := findsrc(src);
+		if(m1 == nil)
+			return;
+		if(m1.dis == nil)
+			m1.dis = m.dis;
+		if(m1.sym == nil)
+			m1.sym = m.sym;
+		m = m1;
+	}
+
+	tkcmd(m.tk+" mark set insert "+string s.start.line+"."+string s.start.pos);
+	tkcmd(m.tk+" tag remove sel 0.0 end");
+	tkcmd(m.tk+" tag add sel insert "+string s.stop.line+"."+string s.stop.pos);
+	tkcmd(m.tk+" see insert");
+
+	packm(m);
+}
+
+packm(m: ref Mod)
+{
+	if(packed != m && packed != nil){
+		tkcmd(packed.tk+" configure -xscrollcommand {}");
+		tkcmd(packed.tk+" configure -yscrollcommand {}");
+		tkcmd(".body.scx configure -command {}");
+		tkcmd(".body.scy configure -command {}");
+		tkcmd("pack forget "+packed.tk);
+	}
+
+	if(packed != m && m != nil){
+		tkcmd(m.tk+" configure -xscrollcommand {.body.scx set}");
+		tkcmd(m.tk+" configure -yscrollcommand {.body.scy set}");
+		tkcmd(".body.scx configure -command {"+m.tk+" xview}");
+		tkcmd(".body.scy configure -command {"+m.tk+" yview}");
+		tkcmd("pack "+m.tk+" -expand 1 -fill both -in .body.ft");
+	}
+	packed = m;
+}
+
+#
+# find the dis file associated with m
+# we know that m has a valid src
+#
+attachdis(m: ref Mod): int
+{
+	c := load Diss m.dis;
+	if(c == nil){
+		m.dis = repsuff(m.src, ".b", ".dis");
+		c = load Diss m.dis;
+	}
+	if(c == nil && m.sym != nil){
+		m.dis = repsuff(m.sym.path, ".sbl", ".dis");
+		c = load Diss m.dis;
+	}
+	if(c != nil){
+		# if m.dis in /appl, prefer one in /dis if it exists (!)
+		nd := len m.dis;
+		for(i := 0; i < len sblpath; i++){
+			(disd, srcd) := sblpath[i];
+			ns := len srcd;
+			if(nd > ns && m.dis[:ns] == srcd){
+				dis := disd + m.dis[ns:];
+				d := load Diss dis;
+				if(d != nil)
+					m.dis = dis;
+					break;
+			}	
+		}
+	}
+	if(c == nil){
+		(dir, file) := str->splitr(repsuff(m.src, ".b", ".dis"), "/");
+		pat := list of {
+			file+" (Dis VM module)",
+			"*.dis (Dis VM module)"
+		};
+		m.dis = selectfile->filename(context, tktop.image, "Locate Dis file", pat, dir);
+		c = load Diss m.dis;
+	}
+	return c != nil;
+}
+
+#
+# load the symbol file for m
+# works best if m has an associated source file
+#
+attachsym(m: ref Mod)
+{
+	if(m.sym != nil)
+		return;
+	sbl := repsuff(m.src, ".b", ".sbl");
+	err : string;
+	tk->cmd(tktop, "cursor -bitmap cursor.wait");
+	(m.sym, err) = debug->sym(sbl);
+	tk->cmd(tktop, "cursor -default");
+	if(m.sym != nil)
+		return;
+	if(!str->prefix("Can't open", err)){
+		alert(err);
+		return;
+	}
+	(dir, file) := str->splitr(sbl, "/");
+
+	pat := list of {
+		file+" (Symbol table file)",
+		"*.sbl (Symbol table file)"
+	};
+	sbl = selectfile->filename(context, tktop.image, "Locate Symbol file", pat, dir);
+	tk->cmd(tktop, "cursor -bitmap cursor.wait");
+	(m.sym, err) = debug->sym(sbl);
+	tk->cmd(tktop, "cursor -default");
+	if(m.sym != nil)
+		return;
+	if(!str->prefix("Can't open", err)){
+		alert(err);
+		return;
+	}
+}
+
+#
+# get the current selection
+#
+getsel(): (ref Mod, int)
+{
+	m := packed;
+	if(m == nil || m.src == nil)
+		return (nil, 0);
+	attachsym(m);
+	if(m.sym == nil){
+		alert("No symbol file for "+m.src);
+		return (nil, 0);
+	}
+	index := tkcmd(m.tk+" index insert");
+	if(len index == 0 || index[0] == '!')
+		return (nil, 0);
+	(sline, spos) := str->splitl(index, ".");
+	line := int sline;
+	pos := int spos[1:];
+	pc := m.sym.srctopc(ref Src((m.src, line, pos), (m.src, line, pos)));
+	s := m.sym.pctosrc(pc);
+	if(s == nil){
+		alert("No pc is appropriate");
+		return (nil, 0);
+	}
+	return (m, pc);
+}
+
+#
+# return the selected string
+#
+snarf(): string
+{
+	if(packed == nil)
+		return "";
+	s := tk->cmd(tktop, packed.tk+" get sel.first sel.last");
+	if(len s > 0 && s[0] == '!')
+		s = "";
+	return s;
+}
+
+plumbit(x, y: string)
+{
+	if (packed == nil)
+		return;
+	s := tk->cmd(tktop, packed.tk+" index @"+x+","+y);
+	if (s == nil || s[0] == '!')
+		return;
+	(nil, l) := sys->tokenize(s, ".");
+	msg := ref Msg(
+		"WmDeb",
+		"",
+		workdir->init(),
+		"text",
+		nil,
+		array of byte (packed.src+":"+hd l));
+	if(msg.send() < 0)
+		sys->fprint(sys->fildes(2), "deb: plumbing write error: %r\n");
+}
+
+but3proc()
+{
+	button3 := 0;
+	for (;;) {
+		s := <-but3;
+		if(s == "pressed"){
+			button3 = 1;
+			continue;
+		}
+		if(plumbed == 0 || button3 == 0)
+			continue;
+		button3 = 0;
+		(nil, l) := sys->tokenize(s, " ");
+		plumbit(hd tl l, hd tl tl l);
+	}
+}
+
+#
+# search for another occurance of s;
+# return if s was found
+#
+search(s: string): int
+{
+	if(packed == nil || s == "")
+		return 0;
+	pos := " sel.last";
+	sel := tk->cmd(tktop, packed.tk+" get sel.last");
+	if(len sel > 0 && sel[0] == '!')
+		pos = " insert";
+	pos = tk->cmd(tktop, packed.tk+" search -- "+tk->quote(s)+pos);
+	if((len pos > 0 && pos[0] == '1') || pos == "")
+		return 0;
+	tkcmd(packed.tk+" mark set insert "+pos);
+	tkcmd(packed.tk+" tag remove sel 0.0 end");
+	tkcmd(packed.tk+" tag add sel insert "+pos+"+"+string len s+"c");
+	tkcmd(packed.tk+" see insert");
+	return 1;
+}
+
+#
+# make a Mod for debugger module mod
+#
+findmod(mod: ref Module): ref Mod
+{
+	dis := mod.dis();
+	if(dis == "")
+		return nil;
+	m: ref Mod;
+	for(ml := mods; ml != nil; ml = tl ml){
+		m = hd ml;
+		if(m.dis == dis || filesuffix(dis, m.dis))
+			break;
+	}
+	if(ml == nil){
+		if(len dis > 0 && dis[0] != '$')
+			m = findsrc(repsuff(dis, ".dis", ".b"));
+		if(m == nil)
+			mods = ref Mod("", "", dis, nil, 0, 0) :: mods;
+	}
+	if(m != nil){
+		m.srcask = 0;
+		m.dis = dis;
+		if(m.symask){
+			attachsym(m);
+			m.symask = 0;
+		}
+		mod.addsym(m.sym);
+	}
+	return m;
+}
+
+# log(s: string)
+# {
+#	fd := sys->open("/usr/jrf/debug", Sys->OWRITE);
+#	sys->seek(fd, 0, Sys->SEEKEND);
+#	sys->fprint(fd, "%s\n", s);
+#	fd = nil;
+# }
+
+findbm(dis: string): ref Mod
+{
+	if(dism == nil){
+		dism = load Dis Dis->PATH;
+		if(dism != nil)
+			dism->init();
+	}
+	if(dism != nil && (b := dism->src(dis)) != nil)
+		return loadsrc(b, 1);
+	return nil;	
+}
+
+findsrc(src: string): ref Mod
+{
+	m := loadsrc(src, 1);
+	if(m != nil)
+		return m;
+	m = findbm(repsuff(src, ".b", ".dis"));
+	if(m != nil)
+		return m;
+	(dir, file) := str->splitr(src, "/");
+	for(i := 0; i < len searchpath; i++){
+		if(dir != "" && dir[0] != '/')
+			m = loadsrc(searchpath[i] + src, 0);
+		if(m != nil)
+			return m;
+		m = loadsrc(searchpath[i] + file, 0);
+		if(m != nil)
+			return m;
+	}
+
+	ns := len src;
+	for(i = 0; i < len sblpath; i++){
+		(disd, srcd) := sblpath[i];
+		nd := len disd;
+		if(ns > nd && src[:nd] == disd){
+			m = loadsrc(srcd + src[nd:], 0);
+			if(m != nil)
+				return m;
+		}
+	}
+
+	(dir, file) = str->splitr(src, "/");
+	opdir := dir;
+	if(opdir == "" || opdir[0] != '/')
+		opdir = opendir;
+
+	pat := list of {
+		file+" (Limbo source)",
+		"*.b (Limbo source)"
+	};
+
+	src = selectfile->filename(context, tktop.image, "Locate Limbo Source", pat, opdir);
+	if(src == nil)
+		return nil;
+	(opendir, nil) = str->splitr(src, "/");
+	if(opendir == "")
+		opendir = ".";
+	m = loadsrc(src, 1);
+	if(m != nil
+	&& dir != "" && dir[0] != '/'
+	&& suffix(dir, opendir))
+		addsearch(opendir[:len opendir - len dir]);
+	else if(m != nil)	# remember anyway
+		addsearch(opendir);
+	return m;
+}
+
+suffix(suff, s: string): int
+{
+	if(len suff > len s)
+		return 0;
+	return suff == s[len s - len suff:];
+}
+
+#
+# load the contents of fd into tkt
+#
+loadfile(tkt: string, fd: ref Sys->FD): int
+{
+	buf := array[512] of byte;
+	i := 0;
+
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0)
+		return 0;
+	tk->cmd(tktop, "cursor -bitmap cursor.wait");
+	length := int d.length;
+	whole := array[length] of byte;
+	cr := 0;
+	for(;;){
+		if(cr){
+			buf[0] = byte '\r';
+			n := sys->read(fd, buf[1:], len buf - 1);
+			n++;
+		}
+		else
+			n := sys->read(fd, buf, len buf);
+		if(n <= 0)
+			break;
+		if(remcr){
+			for(k := 0; k < n-1; ){
+				if(buf[k] == byte '\r' && buf[k+1] == byte '\n')
+					buf[k:] = buf[k+1:n--];
+				else
+					k++;
+			}
+			if(buf[n-1] == byte '\r'){
+				n--;
+				cr = 1;
+			}
+		}
+		j := i+n;
+		if(j > length)
+			break;
+		whole[i:] = buf[:n];
+		i += n;
+	}
+	tk->cmd(tktop, tkt+" delete 1.0 end;"+tkt+" insert end '"+string whole[:i]);
+	tk->cmd(tktop, "update; cursor -default");
+	return 1;
+}
+
+delmod(mods: list of ref Mod, m: ref Mod): list of ref Mod
+{
+	if(mods == nil)
+		return nil;
+	mh := hd mods;
+	if(mh == m)
+		return tl mods;
+	return mh :: delmod(tl mods, m);
+}
+
+#
+# replace an occurance in name of suffix old with new
+#
+repsuff(name, old, new: string): string
+{
+	no := len old;
+	nn := len name;
+	if(nn >= no && name[nn-no:] == old)
+		return name[:nn-no] + new;
+	return name;
+}
+
+filesuffix(suf, s: string): int
+{
+	nsuf := len suf;
+	ns := len s;
+	return ns > nsuf
+		&& suf[0] != '/'
+		&& s[ns-nsuf-1] == '/'
+		&& s[ns-nsuf:] == suf;
+}
+
+alert(m: string)
+{
+	dialog->prompt(context, tktop.image, "warning -fg yellow",
+		"Debugger Alert", m, 0, "Dismiss"::nil);
+}
+
+tkcmd(s: string): string
+{
+	return tk->cmd(tktop, s);
+}
--- /dev/null
+++ b/appl/wm/dir.b
@@ -1,0 +1,511 @@
+implement WmDir;
+
+include "sys.m";
+	sys: Sys;
+	Dir: import sys;
+
+include "draw.m";
+	draw: Draw;
+	ctxt: ref Draw->Context;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "readdir.m";
+	readdir: Readdir;
+
+include "daytime.m";
+	daytime: Daytime;
+
+include "plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg: import plumbmsg;
+
+Fontwidth: 	con 8;
+Xwidth:		con 50;
+
+WmDir: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Wm: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Ft: adt
+{
+	ext:	string;
+	cmd:	string;
+	tkname:	string;
+	icon:	string;
+	loaded:	int;
+	givearg:	int;
+};
+
+dirwin_cfg := array[] of {
+	# Lay out the screen
+	"frame .fc",
+	"scrollbar .fc.scroll -command {.fc.c yview}",
+	"canvas .fc.c -relief sunken -yscrollincrement 25"+
+		" -borderwidth 2 -width 10c -height 300"+
+		" -yscrollcommand {.fc.scroll set}"+
+		" -font /fonts/misc/latin1.8x13.font",
+	"frame .mbar",
+	"menubutton .mbar.opt -text {Options} -menu .opt",
+	"pack .mbar.opt -side left",
+	"pack .fc.scroll -side right -fill y",
+	"pack .fc.c -fill both -expand 1",
+	"pack .mbar -fill x",
+	"pack .fc -fill both -expand 1",
+	"pack propagate . 0",
+
+	# prepare cursor
+	"image create bitmap waiting -file cursor.wait",
+
+	# Build the options menu
+	"menu .opt",
+	".opt add radiobutton -text {by name}"+
+		" -variable sort -value n -command {send opt sort}",
+	".opt add radiobutton -text {by access}"+
+		" -variable sort -value a -command {send opt sort}",
+	".opt add radiobutton -text {by modify}"+
+		" -variable sort -value m -command {send opt sort}",
+	".opt add radiobutton -text {by size}"+
+		" -variable sort -value s -command {send opt sort}",
+	".opt add separator",
+	".opt add radiobutton -text {use icons}"+
+		" -variable show -value i -command {send opt icon}",
+	".opt add radiobutton -text {use text}"
+		+" -variable show -value t -command {send opt text}",
+	".opt add separator",
+	".opt add checkbutton -text {Walk} -command {send opt walk}",
+};
+
+key := Readdir->NAME;
+walk: int;
+path: string;
+usetext: int;
+cmdname: string;
+sysnam: string;
+nde: int;
+now: int;
+plumbed := 0;
+de: array of ref Sys->Dir;
+
+filetypes: array of ref Ft;
+deftype: ref Ft;
+dirtype: ref Ft;
+
+inittypes()
+{
+	deftype = ref Ft("", "/dis/wm/edit.dis", "WmDir_Dis", "file", 0, 1);
+	dirtype = ref Ft("", nil, "WmDir_Dir", "dir", 0, 1);
+	filetypes = array[] of {
+		ref Ft("dis", nil, "WmDis_Pic", "dis", 0, 0),
+		ref Ft("bit", "/dis/wm/view.dis", "WmDir_Pic", "pic", 0, 1),
+		ref Ft("gif", "/dis/wm/view.dis", "WmDir_Pic", "pic", 0, 1),
+		ref Ft("jpg", "/dis/wm/view.dis", "WmDir_Pic", "pic", 0, 1),
+		ref Ft("jpeg", "/dis/wm/view.dis", "WmDir_Pic", "pic", 0, 1),
+		ref Ft("mask", "/dis/wm/view.dis", "WmDir_Pic", "pic", 0, 1),
+	};
+}
+
+init(env: ref Draw->Context, argv: list of string)
+{
+	ctxt = env;
+
+	sys  = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "dir: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk   = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	readdir = load Readdir Readdir->PATH;
+	plumbmsg = load Plumbmsg Plumbmsg->PATH;
+	if(plumbmsg != nil && plumbmsg->init(1, nil, 0) >= 0)
+		plumbed = 1;
+
+	tkclient->init();
+	dialog->init();
+	inittypes();
+
+	cmdname = hd argv;
+	sysnam = sysname()+":";
+
+	(t, wmctl) := tkclient->toplevel(ctxt, "", "", Tkclient->Appl);
+
+	tk->cmd(t, "cursor -image waiting");
+
+	filecmd := chan of string;
+	tk->namechan(t, filecmd, "fc");
+	conf := chan of string;
+	tk->namechan(t, conf, "cf");
+	opt := chan of string;
+	tk->namechan(t, opt, "opt");
+
+	argv = tl argv;
+	if(argv == nil)
+		getdir(t, "");
+	else
+		getdir(t, hd argv);
+	for (c:=0; c<len dirwin_cfg; c++)
+		tk->cmd(t, dirwin_cfg[c]);
+	drawdir(t);
+	tk->cmd(t, "update; cursor -default");
+	tk->cmd(t, "bind . <Configure> {send cf conf}");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	menu := "";
+
+f:	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-wmctl =>
+		if (s == "exit")
+			exit;
+		tkclient->wmctl(t, s);
+	<-conf =>
+		#
+		# Only recompute contents if the size changed
+		#
+		if(menu[0] != 's')
+			break;
+		tk->cmd(t, ".fc.c delete all");
+		drawdir(t);
+		tk->cmd(t, ".fc.c yview moveto 0; update");
+	mopt := <-opt =>
+		case mopt {
+		"sort" =>
+			case tk->cmd(t, "variable sort") {
+			"n" => key = readdir->NAME;
+			"a" => key = readdir->ATIME;
+			"m" => key = readdir->MTIME;
+			"s" => key = readdir->SIZE;
+			}
+			(de, nde) = readdir->sortdir(de, key);
+		"walk" =>
+			walk = !walk;
+			continue f;
+		"text" =>
+			usetext = 1;
+		"icon" =>
+			usetext = 0;
+		}
+		tk->cmd(t, ".fc.c delete all");
+		drawdir(t);
+		tk->cmd(t, ".fc.c yview moveto 0; update");
+	action := <-filecmd =>
+		nd := int action[1:];
+		if(nd > len de)
+			break;
+		case action[0] {
+		'1' =>
+			button1(t, de[nd]);
+		'3' =>
+			button3(t, de[nd]);
+		}
+	}
+}
+
+getdir(t: ref Toplevel, dir: string)
+{
+	if(dir == "")
+		dir = "/";
+
+	path = dir;
+	if (path[len path - 1] != '/')
+		path[len path] = '/';
+
+	(de, nde) = readdir->init(path, key);
+	if(nde < 0) {
+		dialog->prompt(ctxt, t.image, "error -fg red",
+				"Read directory",
+				sys->sprint("Error reading \"%s\"\n%r", path),
+				0, "Exit"::nil);
+		exit;
+	}
+
+	if(path != "/") {
+		(ok, d) := sys->stat("..");
+		if(ok >= 0) {
+			dot := array[nde+1] of ref Dir;
+			dot[0] = ref d;
+			dot[0].name = "..";
+			dot[1:] = de;
+			de = dot;
+			nde++;
+		}
+	}
+
+	for(i := 0; i < nde; i++) {
+		s := de[i].name;
+		l := len s;
+		if(l > 4 && s[l-4:] == ".dis")
+			de[i].mode |= 8r111;
+	}
+	tkclient->settitle(t, sysnam+path);
+}
+
+defcursor(t: ref Toplevel)
+{
+	tk->cmd(t, "cursor -default");
+}
+
+button1(t: ref Toplevel, item: ref Dir)
+{
+	mod: Wm;
+
+	tk->cmd(t, "cursor -image waiting");
+	npath := path;
+	name := item.name + "/";
+	if(item.name == "..") {
+		i := len path - 2;
+		while(i > 0 && path[i] != '/')
+			i--;
+		npath = path[0:i];
+		name = "/";
+	}
+
+	exec := npath+name[0:len name-1];
+	ft := filetype(t, item, exec);
+
+	if(item.mode & Sys->DMDIR) {
+		if(walk != 0) {
+			path = npath;
+			getdir(t, npath+name);
+			tk->cmd(t, ".fc.c delete all");
+			drawdir(t);
+			tk->cmd(t, ".fc.c yview moveto 0; update");
+			defcursor(t);
+			return;
+		}
+		mod = load Wm "/dis/wm/dir.dis";
+		defcursor(t);
+		if(mod == nil) {
+			dialog->prompt(ctxt, t.image, "error -fg red", "Load Dir module",
+				sys->sprint("Error: %r"),
+				0, "Continue"::nil);
+			return;
+		}
+		args := npath+name :: nil;
+		args = cmdname :: args;
+		spawn mod->init(ctxt,  args);
+		return;
+	}
+
+	cmd := ft.cmd;
+	if(cmd == nil)
+		cmd = npath+name;
+
+	mod = load Wm cmd;
+	defcursor(t);
+	if(mod == nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Load Module",
+			sys->sprint("Trying to load \"%s\"\n%r", cmd),
+			0, "Continue"::nil);
+		return;
+	}
+	if(ft.givearg)
+		spawn applinit(mod, ctxt, item.name :: exec :: nil);
+	else
+		spawn applinit(mod, ctxt, item.name :: nil);
+}
+
+applinit(mod: Wm, ctxt: ref Draw->Context, args: list of string)
+{
+	sys->pctl(sys->NEWPGRP|sys->FORKFD, nil);
+	spawn mod->init(ctxt, args);
+}
+
+
+button3(nil: ref Toplevel, stat: ref Sys->Dir)
+{
+	if(!plumbed)
+		return;
+	msg := ref Msg(
+		"WmDir",
+		"",
+		path,
+		"text",
+		"",
+		array of byte stat.name);
+
+	msg.send();
+}
+
+filetype(t: ref Toplevel, d: ref Dir, path: string): ref Ft
+{
+	if(d.mode & Sys->DMDIR)
+		return loadtype(t, dirtype);
+
+	suffix := "";
+	for(j := len path-2; j >= 0; j--) {
+		if(path[j] == '.') {
+			suffix = path[j+1:];
+			break;
+		}
+	}
+
+	if(suffix == "")
+		return loadtype(t, deftype);
+
+	if(suffix[0] >= 'A' && suffix[0] <= 'Z') {
+		for(j = 0; j < len suffix; j++)
+			suffix[j] += ('A' - 'a');
+	}
+
+	for(i := 0; i<len filetypes; i++) {
+		if(suffix == filetypes[i].ext)
+			return loadtype(t, filetypes[i]);
+	}
+
+	return loadtype(t, deftype);
+}
+
+loadtype(t: ref Toplevel, ft: ref Ft): ref Ft
+{
+	if(ft.loaded)
+		return ft;
+
+	s := sys->sprint("image create bitmap %s -file %s.bit -maskfile %s.mask",
+				ft.tkname, ft.icon, ft.icon);	
+	tk->cmd(t, s);
+
+	ft.loaded = 1;
+	return ft;
+}
+
+drawdir(t: ref Toplevel)
+{
+	if(usetext)
+		drawdirtxt(t);
+	else
+		drawdirico(t);
+}
+
+drawdirtxt(t: ref Toplevel)
+{
+	if(daytime == nil) {
+		daytime = load Daytime Daytime->PATH;
+		if(daytime == nil) {
+			dialog->prompt(ctxt, t.image, "error -fg red", "Load Module",
+				sys->sprint("Trying to load \"%s\"\n%r", Daytime->PATH),
+				0, "Continue"::nil);
+			return;
+		}
+		now = daytime->now();
+	}
+
+	y := 10;
+	for(i := 0; i < nde; i++) {
+		tp := "file";
+		if(de[i].mode & Sys->DMDIR)
+			tp = "dir ";
+		else
+		if(de[i].mode & 8r111)
+			tp = "exe ";
+		s := sys->sprint("%s %7bd %s %s",
+			tp,
+			de[i].length,
+			daytime->filet(now, de[i].mtime),
+			de[i].name);
+		id := tk->cmd(t, ".fc.c create text 10 "+string y+
+				" -anchor w -text {"+s+"}");
+
+		base := ".fc.c bind "+id;
+		tk->cmd(t, base+" <Double-Button-1> {send fc %b "+string i+"}");
+		tk->cmd(t, base+" <Button-3> {send fc %b "+string i+"}");
+		tk->cmd(t, base+" <Motion-Button-3> {}");
+		y += 15;
+	}
+
+	x := int tk->cmd(t, ".fc.c cget actwidth");
+	tk->cmd(t, ".fc.c configure -scrollregion { 0 0 "+string x+" "+string y+"}");
+}
+
+drawdirico(t: ref Toplevel)
+{
+	w := int tk->cmd(t, ".fc.c cget actwidth");
+
+	longest := 0;
+	for(i := 0; i < nde; i++) {
+		l := len de[i].name;
+		if(l > longest)
+			longest = l;
+	}
+	longest += 2;
+
+	minw := (longest*Fontwidth);
+	if( w < minw ){
+		w = minw + int tk->cmd(t, ".fc.scroll cget actwidth");
+		tk->cmd(t, ". configure -width "+string w);
+		w = minw;
+	}
+
+	xwid := Xwidth;
+	x := w/minw;
+	x = w/x;
+	if(x > xwid)
+		xwid = x;
+
+	x = xwid/2;
+	y := 20;
+
+	for(i = 0; i < nde; i++) {
+		sx := string x;
+		ft := filetype(t, de[i], de[i].name);
+		img := ft.tkname;
+		
+		id := tk->cmd(t, ".fc.c create image "+sx+" "+
+				string y+" -image "+img);
+		tk->cmd(t, ".fc.c create text "+sx+
+				" "+string (y+25)+" -text "+de[i].name);
+
+		base := ".fc.c bind "+id;
+		tk->cmd(t, base+" <Double-Button-1> {send fc %b "+string i+"}");
+		tk->cmd(t, base+" <Button-2> {send fc %b "+string i+"}");
+		tk->cmd(t, base+" <Motion-Button-2> {}");
+		tk->cmd(t, base+" <Button-3> {send fc %b "+string i+"}");
+		tk->cmd(t, base+" <Motion-Button-3> {}");
+		x += xwid;
+		if(x > w) {
+			x = xwid/2;
+			y += 50;
+		}
+	}
+	y += 50;
+	x = int tk->cmd(t, ".fc.c cget actwidth");
+	tk->cmd(t, ".fc.c configure -scrollregion { 0 0 "+string x+" "+string y+"}");
+}
+
+sysname(): string
+{
+	syspath := "#c";
+	if ( cmdname == "wmdir" )
+		syspath = "/n/dev";
+	fd := sys->open(syspath+"/sysname", sys->OREAD);
+	if(fd == nil)
+		return "Anon";
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return "Anon";
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/wm/drawmux/dmview.b
@@ -1,0 +1,163 @@
+implement DMView;
+
+include "sys.m";
+include "draw.m";
+include "tk.m";
+include "tkclient.m";
+
+DMView : module {
+	init : fn (ctxt : ref Draw->Context, args : list of string);
+};
+
+DMPORT : con 9998;
+
+sys : Sys;
+draw : Draw;
+tk : Tk;
+tkclient : Tkclient;
+
+Display, Image, Screen, Point, Rect, Chans : import draw;
+
+display : ref Display;
+screen : ref Screen;
+
+
+init(ctxt : ref Draw->Context, args : list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		fail(sys->sprint("cannot load %s: %r", Tk->PATH), "init");
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		fail(sys->sprint("cannot load %s: %r", Tkclient->PATH), "init");
+
+	args = tl args;
+	if (args == nil)
+		fail("usage: dmview netaddr", "usage");
+	addr := hd args;
+	args = tl args;
+
+	display = ctxt.display;
+	screen = ctxt.screen;
+
+	tkclient ->init();
+
+	(ok, nc) := sys->dial("tcp!"+addr+"!" + string DMPORT, nil);
+	if (ok < 0)
+		fail(sys->sprint("could not connect: %r"), "init");
+
+	info := array [2 * 12] of byte;
+	if (sys->read(nc.dfd, info, len info) != len info) {
+		sys->print("protocol error\n");
+		return;
+	}
+	dispw := int string info[0:12];
+	disph := int string info[12:24];
+	info = nil;
+
+	(tktop, wmctl) := tkclient->toplevel(ctxt, "", "dmview: "+addr, Tkclient->Hide);
+	if (tktop == nil)
+		fail("cannot create window", "init");
+
+	cpos := mkframe(tktop, dispw, disph);
+	winr := Rect((0, 0), (dispw, disph));
+	newwin := display.newimage(winr, display.image.chans, 0, Draw->White);
+	# newwin := screen.newwindow(winr, Draw->Refbackup, Draw->White);
+	if (newwin == nil) {
+		sys->print("failed to create window: %r\n");
+		return;
+	}
+	tk->putimage(tktop, ".c", newwin, nil);
+	tk->cmd(tktop, ".c dirty");
+	tk->cmd(tktop, "update");
+	winr = winr.addpt(cpos);
+	newwin.origin(Point(0,0), winr.min);
+
+	pubscr := Screen.allocate(newwin, ctxt.display.black, 1);
+	if (pubscr == nil) {
+		sys->print("failed to create public screen: %r\n");
+		return;
+	}
+	
+	msg := array of byte sys->sprint("%11d %11s ", pubscr.id, newwin.chans.text());
+	sys->write(nc.dfd, msg, len msg);
+	msg = nil;
+
+	pidc := chan of int;
+	spawn srv(nc.dfd, wmctl, pidc);
+	srvpid := <- pidc;
+
+	tkclient->onscreen(tktop, nil);
+	tkclient->startinput(tktop, nil);
+
+	for (;;) {
+		cmd := <- wmctl;
+		case cmd {
+		"srvexit" =>
+sys->print("srv exit: %r\n");
+			srvpid = -1;
+		"exit" =>
+			if (srvpid != -1)
+				kill(srvpid);
+			return;
+		"move" =>
+			newwin.origin(Point(0,0), display.image.r.max);
+			tkclient->wmctl(tktop, cmd);
+			x := int tk->cmd(tktop, ".c cget -actx");
+			y := int tk->cmd(tktop, ".c cget -acty");
+			newwin.origin(Point(0,0), Point(x, y));
+		"task" =>
+			newwin.origin(Point(0,0), display.image.r.max);
+			tkclient->wmctl(tktop, cmd);
+			x := int tk->cmd(tktop, ".c cget -actx");
+			y := int tk->cmd(tktop, ".c cget -acty");
+			newwin.origin(Point(0,0), Point(x, y));
+		* =>
+			tkclient->wmctl(tktop, cmd);
+		}
+	}
+}
+
+srv(fd : ref Sys->FD, done : chan of string, pidc : chan of int)
+{
+	pidc <-= sys->pctl(Sys->FORKNS, nil);
+	sys->bind("/dev/draw", "/", Sys->MREPL);
+	sys->export(fd, "/", Sys->EXPWAIT);
+	done <-= "srvexit";
+}
+
+fail(msg, exc : string)
+{
+	sys->print("%s\n", msg);
+	raise "fail:"+exc;
+}
+
+mkframe(t : ref Tk->Toplevel, w, h : int) : Point
+{
+	tk->cmd(t, "panel .c -width " + string w + " -height " + string h);
+	tk->cmd(t, "frame .f -borderwidth 3 -relief groove");
+	tk->cmd(t, "pack .c -in .f");
+	tk->cmd(t, "pack .f");
+	tk->cmd(t, "update");
+
+	x := int tk->cmd(t, ".c cget -actx");
+	y := int tk->cmd(t, ".c cget -acty");
+
+	return Point(x, y);
+}
+
+kill(pid: int)
+{
+	if ((pctl  := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE)) != nil)
+		sys->fprint(pctl, "kill");
+}
+
+tkcmd(t : ref Tk->Toplevel, c : string)
+{
+	s := tk->cmd(t, c);
+	if (s != nil)
+		sys->print("%s ERROR: %s\n", c, s);
+}
--- /dev/null
+++ b/appl/wm/drawmux/dmwm.b
@@ -1,0 +1,207 @@
+implement Dmwm;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Screen, Display, Image, Rect, Point, Wmcontext, Pointer: import draw;
+include "drawmux.m";
+	dmux : Drawmux;
+include "wmsrv.m";
+	wmsrv: Wmsrv;
+	Window, Client: import wmsrv;
+include "tk.m";
+include "wmclient.m";
+	wmclient: Wmclient;
+include "string.m";
+	str: String;
+include "dialog.m";
+	dialog: Dialog;
+include "arg.m";
+
+Wm: module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Dmwm: module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Background: con int 16r777777FF;
+
+screen: ref Screen;
+display: ref Display;
+
+badmodule(p: string)
+{
+	sys->fprint(sys->fildes(2), "wm: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys  = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	if(draw == nil)
+		badmodule(Draw->PATH);
+
+	str = load String String->PATH;
+	if(str == nil)
+		badmodule(String->PATH);
+
+	wmsrv = load Wmsrv Wmsrv->PATH;
+	if(wmsrv == nil)
+		badmodule(Wmsrv->PATH);
+
+	wmclient = load Wmclient Wmclient->PATH;
+	if(wmclient == nil)
+		badmodule(Wmclient->PATH);
+	wmclient->init();
+
+	dialog = load Dialog Dialog->PATH;
+	if (dialog == nil) badmodule(Dialog->PATH);
+	dialog->init();
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+	if (ctxt == nil)
+		ctxt = wmclient->makedrawcontext();
+	display = ctxt.display;
+
+	dmux = load Drawmux Drawmux->PATH;
+	if (dmux != nil) {
+		(err, disp) := dmux->init();
+		if (err != nil) {
+			dmux = nil;
+			sys->fprint(stderr(), "wm: cannot start drawmux: %s\n", err);
+		}
+		else
+			display = disp;
+	}
+
+	buts := Wmclient->Appl;
+	if(ctxt.wm == nil)
+		buts = Wmclient->Plain;
+	# win := wmclient->window(ctxt, "Wm", buts);
+	# wmclient->win.onscreen("place");
+	# wmclient->win.startinput("kbd" :: "ptr" :: nil);
+
+	# screen = makescreen(win.image);
+
+	(clientwm, join, req) := wmsrv->init();
+	clientctxt := ref Draw->Context(display, nil, nil);
+
+	sync := chan of string;
+	argv = tl argv;
+	if(argv == nil)
+		argv = "wm/toolbar" :: nil;
+	argv = "wm/wm" :: argv;
+	spawn command(clientctxt, argv, sync);
+	if((e := <-sync) != nil)
+		fatal("cannot run command: " + e);
+
+	dmuxrequest := chan of (string, ref Sys->FD);
+	if (dmux != nil)
+		spawn dmuxlistener(dmuxrequest);
+
+	for(;;) alt {
+	(name, fd) := <- dmuxrequest =>
+		spawn dmuxask(ctxt, name, fd);
+	}
+}
+
+makescreen(img: ref Image): ref Screen
+{
+	screen = Screen.allocate(img, img.display.color(Background), 0);
+	img.draw(img.r, screen.fill, nil, screen.fill.r.min);
+	return screen;
+}
+
+kill(pid: int, note: string): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", note) < 0)
+		return -1;
+	return 0;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "wm: %s\n", s);
+	kill(sys->pctl(0, nil), "killgrp");
+	raise "fail:error";
+}
+
+command(ctxt: ref Draw->Context, args: list of string, sync: chan of string)
+{
+	fds := list of {0, 1, 2};
+	pid := sys->pctl(sys->NEWFD, fds);
+
+	cmd := hd args;
+	file := cmd;
+
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Wm file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(err != "permission denied" && err != "access permission denied" && file[0]!='/' && file[0:2]!="./"){
+			c = load Wm "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sync <-= sys->sprint("%s: %s\n", cmd, err);
+			exit;
+		}
+	}
+	sync <-= nil;
+	c->init(ctxt, args);
+}
+
+dmuxlistener(newclient : chan of (string, ref Sys->FD))
+{
+	(aok, c) := sys->announce("tcp!*!9998");
+	if (aok < 0) {
+		sys->print("cannot announce drawmux port: %r\n");
+		return;
+	}
+	buf := array [Sys->ATOMICIO] of byte;
+	for (;;) {
+		(ok, nc) := sys->listen(c);
+		if (ok < 0) {
+			sys->fprint(stderr(), "wm: dmux listen failed: %r\n");
+			return;
+		}
+		fd := sys->open(nc.dir+"/remote", Sys->OREAD);
+		name := "unknown";
+		if (fd == nil)
+			sys->fprint(stderr(), "wm: dmux cannot access remote address: %r\n");
+		else {
+			n := sys->read(fd, buf, len buf);
+			if (n > 0) {
+				name = string buf[0:n];
+				for (i := len name -1; i > 0; i--)
+					if (name[i] == '!')
+						break;
+				if (i != 0)
+					name = name[0:i];
+			}
+		}
+		fd = sys->open(nc.dir+"/data", Sys->ORDWR);
+		if (fd != nil)
+			newclient <-= (name, fd);
+	}
+}
+
+dmuxask(ctxt: ref Draw->Context, name : string, fd : ref Sys->FD)
+{
+	msg := sys->sprint("Screen snoop request\nAddress: %s\n\nProceed?", name);
+	labs := "Ok" :: "No way!" :: nil;
+	if (1 || dialog->prompt(ctxt, nil, nil, "Snoop!", msg, 1, labs) == 0)
+		dmux->newviewer(fd);
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
--- /dev/null
+++ b/appl/wm/drawmux/drawmux.b
@@ -1,0 +1,1827 @@
+implement Drawmux;
+
+include "sys.m";
+include "draw.m";
+include "drawmux.m";
+
+include "drawoffs.m";
+
+sys : Sys;
+draw : Draw;
+
+Display, Point, Rect, Chans : import draw;
+
+Ehungup : con "Hangup";
+
+drawR: Draw->Rect;
+drawchans: Draw->Chans;
+drawop := Draw->SoverD;
+drawfd: ref Sys->FD;
+images: ref Imageset;
+screens: ref Screenset;
+viewers: list of ref Viewer;
+drawlock: chan of chan of int;
+readdata: array of byte;
+nhangups := 0;
+prevnhangups := 0;
+
+init() : (string, ref Draw->Display)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+
+	if (draw == nil)
+		return (sys->sprint("cannot load %s: %r", Draw->PATH), nil);
+	drawlock = chan of chan of int;
+	images = Imageset.new();
+	screens = Screenset. new();
+	res := chan of (string, ref Draw->Display);
+	spawn getdisp(res);
+	r := <- res;
+	return r;
+}
+
+newviewer(fd : ref Sys->FD)
+{
+	reply := array of byte sys->sprint("%.11d %.11d ", drawR.max.x - drawR.min.x, drawR.max.y - drawR.min.y);
+	if (sys->write(fd, reply, len reply) != len reply) {
+#		sys->print("viewer hangup\n");
+		return;
+	}
+
+	buf := array [Sys->ATOMICIO] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n < 24)
+		return;
+	pubscr := int string buf[0:12];
+	chans := Chans.mk(string buf[12:24]);
+
+	sys->pctl(Sys->FORKNS, nil);
+	sys->mount(fd, nil, "/", Sys->MREPL, nil);
+	cfd := sys->open("/new", Sys->OREAD);
+	sys->read(cfd, buf, len buf);
+	cnum := int string buf[0:12];
+	cdata := sys->sprint("/%d/data", cnum);
+	datafd := sys->open(cdata, Sys->ORDWR);
+
+	if (datafd == nil) {
+#		sys->print("cannot open viewer data file: %r\n");
+		return;
+	}
+	Viewer.new(datafd, pubscr, chans);
+}
+
+getdisp(result : chan of (string, ref Draw->Display))
+{	
+	sys->pctl(Sys->FORKNS, nil);
+	sys->bind("#i", "/dev", Sys->MREPL);
+	sys->bind("#s", "/dev/draw", Sys->MBEFORE);
+	newio := sys->file2chan("/dev/draw", "new");
+	if (newio == nil) {
+		result <- = ("cannot create /dev/new file2chan", nil);
+		return;
+	}
+	spawn srvnew(newio);
+	disp := Display.allocate(nil);
+	if (disp == nil) {
+		result <-= (sys->sprint("%r"), nil);
+		return;
+	}
+	
+	draw->disp.image.draw(disp.image.r, disp.rgb(0,0,0), nil, Point(0,0));
+	result <- = (nil, disp);
+}
+
+srvnew(newio : ref Sys->FileIO)
+{
+	for (;;) alt {
+	(offset, count, fid, rc) := <- newio.read =>
+		if (rc != nil) {
+			c := chan of (string, ref Sys->FD);
+			fd := sys->open("#i/draw/new", Sys->OREAD);
+			# +1 because of a sprint() nasty in devdraw.c
+			buf := array [(12 * 12)+1] of byte;
+			nn := sys->read(fd, buf, len buf);
+			cnum := int string buf[0:12];
+			drawchans = Chans.mk(string buf[24:36]);
+			# repl is at [36:48]
+			drawR.min.x = int string buf[48:60];
+			drawR.min.y = int string buf[60:72];
+			drawR.max.x = int string buf[72:84];
+			drawR.max.y = int string buf[84:96];
+
+			bwidth := bytesperline(drawR, drawchans);
+			img := ref Image (0, 0, 0, 0, drawchans, 0, drawR, drawR, Draw->Black, nil, drawR.min, bwidth, 0, "");
+			images.add(0, img);
+
+			cdir := sys->sprint("/dev/draw/%d", cnum);
+			dpath := sys->sprint("#i/draw/%d/data", cnum);
+			drawfd = sys->open(dpath, Sys->ORDWR);
+			fd = nil;
+			if (drawfd == nil) {
+				rc <-= (nil, sys->sprint("%r"));
+				return;
+			}
+			sys->bind("#s", cdir, Sys->MBEFORE);
+			drawio := sys->file2chan(cdir, "data");
+			spawn drawclient(drawio);
+			rc <- = (buf, nil);
+			return;
+		}
+	(offset, data, fid, wc) := <- newio.write =>
+		if (wc != nil)
+			writereply(wc, (0, "permission denied"));
+	}
+}
+
+# for simplicity make the file 'exclusive use'
+drawclient(drawio : ref Sys->FileIO)
+{
+	activefid := -1;
+	closecount := 2;
+
+	for (;closecount;) {
+		alt {
+		unlock := <- drawlock =>
+			<- unlock;
+	
+		(offset, count, fid, rc) := <- drawio.read =>
+				if (activefid == -1)
+					activefid = fid;
+	
+				if (rc == nil) {
+					closecount--;
+					continue;
+				}
+				if (fid != activefid) {
+					rc <-= (nil, "file busy");
+					continue;
+				}
+				if (readdata == nil) {
+					rc <-= (nil, nil);
+					continue;
+				}
+				if (count > len readdata)
+					count = len readdata;
+				rc <- = (readdata[0:count], nil);
+				readdata = nil;
+	
+		(offset, data, fid, wc) := <- drawio.write =>
+			if (wc == nil) {
+				closecount--;
+				continue;
+			}
+			writereply(wc, process(data));
+		}
+		if (nhangups != prevnhangups) {
+			ok : list of ref Viewer;
+			for (ok = nil; viewers != nil; viewers = tl viewers) {
+				v := hd viewers;
+				if (!v.hungup)
+					ok = v :: ok;
+				else {
+#					sys->print("shutting down Viewer\n");
+					v.output <- = (nil, nil);
+				}
+			}
+			viewers = ok;
+			prevnhangups = nhangups;
+		}
+	}
+#	sys->print("DRAWIO DONE!\n");
+}
+
+writereply(wc : chan of (int, string), val : (int, string))
+{
+	alt {
+	wc <-= val =>
+		;
+	* =>
+		;
+	}
+}
+
+Image: adt {
+	id: int;
+	refc: int;
+	screenid: int;
+	refresh: int;
+	chans: Draw->Chans;
+	repl: int;
+	R: Draw->Rect;
+	clipR: Draw->Rect;
+	rrggbbaa: int;
+	font: ref Font;
+	lorigin: Draw->Point;
+	bwidth: int;
+	dirty: int;
+	name: string;
+};
+
+Screen: adt {
+	id: int;
+	imageid: int;
+	fillid: int;
+	windows: array of int;
+
+	setz: fn (s: self ref Screen, z: array of int, top: int);
+	addwin: fn (s: self ref Screen, wid: int);
+	delwin: fn (s: self ref Screen, wid: int);
+};
+
+Font: adt {
+	ascent: int;
+	chars: array of ref Fontchar;
+};
+
+Fontchar: adt {
+	srcid: int;
+	R: Draw->Rect;
+	P: Draw->Point;
+	left: int;
+	width: int;
+};
+
+Idpair: adt {
+	key: int;
+	val: int;
+	next: cyclic ref Idpair;
+};
+
+Idmap: adt {
+	buckets: array of ref Idpair;
+
+	new: fn (): ref Idmap;
+	add: fn (m: self ref Idmap, key, val: int);
+	del: fn (m: self ref Idmap, key: int);
+	lookup: fn (m: self ref Idmap, key: int): int;
+};
+
+Imageset: adt {
+	images: array of ref Image;
+	ixmap: ref Idmap;
+	freelist: list of int;
+	new: fn (): ref Imageset;
+	add: fn (s: self ref Imageset, id: int, img: ref Image);
+	del: fn (s: self ref Imageset, id: int);
+	lookup: fn (s: self ref Imageset, id: int): ref Image;
+	findname: fn(s: self ref Imageset, name: string): ref Image;
+};
+
+Screenset: adt {
+	screens: array of ref Screen;
+	ixmap: ref Idmap;
+	freelist: list of int;
+	new: fn (): ref Screenset;
+	add: fn (s: self ref Screenset, scr: ref Screen);
+	del: fn (s: self ref Screenset, id: int);
+	lookup: fn (s: self ref Screenset, id: int): ref Screen;
+};
+
+
+Drawreq: adt {
+	data: array of byte;
+	pick {
+#	a =>	# allocate image
+#		id: int;
+#		screenid: int;
+#		refresh: int;
+#		ldepth: int;
+#		repl: int;
+#		R: Draw->Rect;
+#		clipR: Draw->Rect;
+#		value: int;
+	b =>	# new allocate image
+		id: int;
+		screenid: int;
+		refresh: int;
+		chans: Draw->Chans;
+		repl: int;
+		R: 	Draw->Rect;
+		clipR: Draw->Rect;
+		rrggbbaa: int;
+	A => # allocate screen
+		id: int;
+		imageid: int;
+		fillid: int;
+	c => # set clipr and repl
+		dstid: int;
+		repl: int;
+		clipR: Draw->Rect;
+#	x => # move cursor
+#	C => # set cursor image and hotspot
+#		_: int;
+	d => # general draw op
+		dstid: int;
+		srcid: int;
+		maskid: int;
+	D => # debug mode
+		_: int;
+	e => # draw ellipse
+		dstid: int;
+		srcid: int;
+	f => # free image
+		id: int;
+		img: ref Image;	# helper for Viewers
+	F =>	 # free screen
+		id: int;
+	i => # convert image to font
+		fontid: int;
+		nchars: int;
+		ascent: int;
+	l => # load a char into font
+		fontid: int;
+		srcid: int;
+		index: int;
+		R: Draw->Rect;
+		P: Draw->Point;
+		left: int;
+		width: int;
+	L => # draw line
+		dstid: int;
+		srcid: int;
+	n =>	# attach to named image
+		dstid: int;
+		name: string;
+	N => # name image
+		dstid: int;
+		in: int;
+		name: string;
+	o =>	# set window origins
+		id: int;
+		rmin: Draw->Point;
+		screenrmin: Draw->Point;
+	O => # set next compositing op
+		op: int;
+	p =>	# draw polygon
+		dstid: int;
+		srcid: int;
+	r =>	# read pixels
+		id: int;
+		R: Draw->Rect;
+	s =>	# draw text
+		dstid: int;
+		srcid: int;
+		fontid: int;
+	x => # draw text with bg
+		dstid: int;
+		srcid: int;
+		fontid: int;
+		bgid: int;
+	S =>	# import public screen
+	t =>	# adjust window z order
+		top: int;
+		ids: array of int;
+	v =>	 # flush updates to display
+	y =>	# write pixels
+		id: int;
+		R: Draw->Rect;
+	}
+};
+
+getreq(data : array of byte, ix : int) : (ref Drawreq, string)
+{
+	mlen := 0;
+	err := "short draw message";
+	req : ref Drawreq;
+
+	case int data[ix] {
+	'b' => # alloc image
+		mlen = 1+4+4+1+4+1+(4*4)+(4*4)+4;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.b;
+			r.data = data;
+			r.id = get4(data, OPb_id);
+			r.screenid = get4(data, OPb_screenid);
+			r.refresh = get1(data, OPb_refresh);
+			r.chans = Draw->Chans(get4(data, OPb_chans));
+			r.repl = get1(data, OPb_repl);
+			r.R = getR(data, OPb_R);
+			r.clipR = getR(data, OPb_clipR);
+			r.rrggbbaa = get4(data, OPb_rrggbbaa);
+			req = r;
+		}
+	'A' => # alloc screen
+		mlen = 1+4+4+4+1;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.A;
+			r.data = data;
+			r.id = get4(data, OPA_id);
+			r.imageid = get4(data, OPA_imageid);
+			r.fillid = get4(data, OPA_fillid);
+			req = r;
+		}
+	'c' => # set clipR
+		mlen = 1+4+1+(4*4);
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.c;
+			r.data = data;
+			r.dstid = get4(data, OPc_dstid);
+			r.repl = get1(data, OPc_repl);
+			r.clipR = getR(data, OPc_clipR);
+			req = r;
+		}
+	'd' => # draw
+		mlen = 1+4+4+4+(4*4)+(2*4)+(2*4);
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.d;
+			r.data = data;
+			r.dstid = get4(data, OPd_dstid);
+			r.srcid = get4(data, OPd_srcid);
+			r.maskid = get4(data, OPd_maskid);
+			req = r;
+		}
+	'D' =>
+		# debug mode
+		mlen = 1+1;
+		if (mlen+ix <= len data) {
+			req = ref Drawreq.v;
+			req.data = data[ix:ix+mlen];
+		}
+	'e' or
+	'E' => # ellipse
+		mlen = 1+4+4+(2*4)+4+4+4+(2*4)+4+4;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.e;
+			r.data = data;
+			r.dstid = get4(data, OPe_dstid);
+			r.srcid = get4(data, OPe_srcid);
+			req = r;
+		}
+	'f' => # free image
+		mlen = 1+4;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.f;
+			r.data = data;
+			r.id = get4(data, OPf_id);
+			req = r;
+		}
+	'F' => # free screen
+		mlen = 1+4;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.f;
+			r.data = data;
+			r.id = get4(data, OPF_id);
+			req = r;
+		}
+	'i' =>	 # alloc font
+		mlen = 1+4+4+1;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.i;
+			r.data = data;
+			r.fontid = get4(data, OPi_fontid);
+			r.nchars = get4(data, OPi_nchars);
+			r.ascent = get1(data, OPi_ascent);
+			req = r;
+		}
+	'l' =>	 # load font char
+		mlen = 1+4+4+2+(4*4)+(2*4)+1+1;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.l;
+			r.data = data;
+			r.fontid = get4(data, OPl_fontid);
+			r.srcid = get4(data, OPl_srcid);
+			r.index = get2(data, OPl_index);
+			r.R = getR(data, OPl_R);
+			r.P = getP(data, OPl_P);
+			r.left = get1(data, OPl_left);
+			r.width = get1(data, OPl_width);
+			req = r;
+		}
+	'L' => # line
+		mlen = 1+4+(2*4)+(2*4)+4+4+4+4+(2*4);
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.L;
+			r.data = data;
+			r.dstid = get4(data, OPL_dstid);
+			r.srcid = get4(data, OPL_srcid);
+			req = r;
+		}
+	'n' => # attach to named image
+		mlen = 1+4+1;
+		if (mlen+ix < len data) {
+			mlen += get1(data, ix+OPn_j);
+			if (mlen+ix <= len data) {
+				data = data[ix:ix+mlen];
+				r := ref Drawreq.n;
+				r.data = data;
+				r.dstid = get4(data, OPn_dstid);
+				r.name = string data[OPn_name:];
+				req = r;
+			}
+		}
+	'N' => # name image
+		mlen = 1+4+1+1;
+		if (mlen+ix < len data) {
+			mlen += get1(data, ix+OPN_j);
+			if (mlen+ix <= len data) {
+				data = data[ix:ix+mlen];
+				r := ref Drawreq.N;
+				r.data = data;
+				r.dstid = get4(data, OPN_dstid);
+				r.in = get1(data, OPN_in);
+				r.name = string data[OPN_name:];
+				req = r;
+			}
+		}
+	'o' => # set origins
+		mlen = 1+4+(2*4)+(2*4);
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.o;
+			r.data = data;
+			r.id = get4(data, OPo_id);
+			r.rmin = getP(data, OPo_rmin);
+			r.screenrmin = getP(data, OPo_screenrmin);
+			req = r;
+		}
+	'O' => # set next compop
+		mlen = 1+1;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.O;
+			r.data = data;
+			r.op = get1(data, OPO_op);
+			req = r;
+		}
+	'p' or
+	'P' => # polygon
+		mlen = 1+4+2+4+4+4+4+(2*4);
+		if (mlen + ix <= len data) {
+			n := get2(data, ix+OPp_n);
+			nb := coordslen(data, ix+OPp_P0, 2*(n+1));
+			if (nb == -1)
+				err = "bad coords";
+			else {
+				mlen += nb;
+				if (mlen+ix <= len data) {
+					data = data[ix:ix+mlen];
+					r := ref Drawreq.p;
+					r.data = data;
+					r.dstid = get4(data, OPp_dstid);
+					r.srcid = get4(data, OPp_srcid);
+					req = r;
+				}
+			}
+		}
+	'r' =>	 # read pixels
+		mlen = 1+4+(4*4);
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			r := ref Drawreq.r;
+			r.data = data;
+			r.id = get4(data, OPr_id);
+			r.R = getR(data, OPr_R);
+			req = r;
+		}
+	's' => # text
+		mlen = 1+4+4+4+(2*4)+(4*4)+(2*4)+2;
+		if (ix+mlen <= len data) {
+			ni := get2(data, ix+OPs_ni);
+			mlen += (2*ni);
+			if (mlen+ix <= len data) {
+				data = data[ix:ix+mlen];
+				r := ref Drawreq.s;
+				r.data = data;
+				r.dstid = get4(data, OPs_dstid);
+				r.srcid = get4(data, OPs_srcid);
+				r.fontid = get4(data, OPs_fontid);
+				req = r;
+			}
+		}
+	'x' => # text with bg img
+		mlen = 1+4+4+4+(2*4)+(4*4)+(2*4)+2+4+(2*4);
+		if (ix+mlen <= len data) {
+			ni := get2(data, ix+OPx_ni);
+			mlen += (2*ni);
+			if (mlen+ix <= len data) {
+				data = data[ix:ix+mlen];
+				r := ref Drawreq.x;
+				r.data = data;
+				r.dstid = get4(data, OPx_dstid);
+				r.srcid = get4(data, OPx_srcid);
+				r.fontid = get4(data, OPx_fontid);
+				r.bgid = get4(data, OPx_bgid);
+				req = r;
+			}
+		}
+	'S' => # import public screen
+		mlen = 1+4+4;
+		if (mlen+ix <= len data) {
+			data = data[ix:ix+mlen];
+			req = ref Drawreq.S;
+			req.data = data;
+		}
+	't' => # adjust window z order
+		mlen = 1+1+2;
+		if (ix+mlen<= len data) {
+			nw := get2(data, ix+OPt_nw);
+			mlen += (4*nw);
+			if (mlen+ix <= len data) {
+				data = data[ix:ix+mlen];
+				r := ref Drawreq.t;
+				r.data = data;
+				r.top = get1(data, OPt_top);
+				r.ids = array [nw] of int;
+				for (n := 0; n < nw; n++)
+					r.ids[n] = get4(data, OPt_id + 4*n);
+				req = r;
+			}
+		}
+	'v' => # flush
+		req = ref Drawreq.v;
+		req.data = data[ix:ix+1];
+	'y' or
+	'Y' =>	# write pixels
+		mlen = 1+4+(4*4);
+		if (ix+mlen <= len data) {
+			imgid := get4(data, ix+OPy_id);
+			img := images.lookup(imgid);
+			compd := data[ix] == byte 'Y';
+			r := getR(data, ix+OPy_R);
+			n := imglen(img, data, ix+mlen, r, compd);
+			if (n == -1)
+				err ="bad image data";
+			mlen += n;
+			if (mlen+ix <= len data)
+				req = ref Drawreq.y (data[ix:ix+mlen], imgid, r);
+		}
+	* =>
+		err = "bad draw command";
+	}
+
+	if (req == nil)
+		return (nil, err);
+	return (req, nil);
+}
+
+process(data : array of byte) : (int, string)
+{
+	offset := 0;
+	while (offset < len data) {
+		(req, err) := getreq(data, offset);
+		if (err != nil)
+			return (0, err);
+		offset += len req.data;
+		n := sys->write(drawfd, req.data, len req.data);
+		if (n <= 0)
+			return (n, sys->sprint("[%c] %r", int req.data[0]));
+
+		readn := 0;
+		sendtoviews := 1;
+
+		# actions that must be done before sending to Viewers
+		pick r := req {
+		b =>	# allocate image
+			bwidth := bytesperline(r.R, r.chans);
+			img := ref Image (r.id, 0, r.screenid, r.refresh, r.chans, r.repl, r.R, r.clipR, r.rrggbbaa, nil, r.R.min, bwidth, 0, "");
+			images.add(r.id, img);
+			if (r.screenid != 0) {
+				scr := screens.lookup(r.screenid);
+				scr.addwin(r.id);
+			}
+
+		A =>	# allocate screen
+			scr := ref Screen (r.id, r.imageid, r.fillid, nil);
+			screens.add(scr);
+			# we never allocate public screens on our Viewers
+			put1(r.data, OPA_public, 0);
+			dirty(r.imageid, 0);
+
+		c =>	# set clipr and repl
+			img := images.lookup(r.dstid);
+			img.repl = r.repl;
+			img.clipR = r.clipR;
+
+		d =>	# general draw op
+			dirty(r.dstid, 1);
+			drawop = Draw->SoverD;
+
+		e =>	# draw ellipse
+			dirty(r.dstid, 1);
+			drawop = Draw->SoverD;
+
+		f => # free image
+			# help out Viewers, real work is done later
+			r.img = images.lookup(r.id);
+
+		L =>	# draw line
+			dirty(r.dstid, 1);
+			drawop = Draw->SoverD;
+
+		n =>	# attach to named image
+			img := images.findname(r.name);
+			images.add(r.dstid, img);
+			
+		N => # name image
+			img := images.lookup(r.dstid);
+			if (r.in)
+				img.name = r.name;
+			else
+				img.name = nil;
+
+		o => # set image origins
+			img := images.lookup(r.id);
+			deltax := img.lorigin.x - r.rmin.x;
+			deltay := img.lorigin.y - r.rmin.y;
+			w := img.R.max.x - img.R.min.x;
+			h := img.R.max.y - img.R.min.y;
+			
+			img.R = Draw->Rect(r.screenrmin, (r.screenrmin.x + w, r.screenrmin.y + h));
+			img.clipR = Draw->Rect((img.clipR.min.x - deltax, img.clipR.min.y - deltay), (img.clipR.max.x - deltax, img.clipR.max.y - deltay));
+			img.lorigin = r.rmin;
+
+		O =>	# set compositing op
+			drawop = r.op;
+
+		p =>	# draw polygon
+			dirty(r.dstid, 1);
+			drawop = Draw->SoverD;
+
+		r => # read pixels
+			img := images.lookup(r.id);
+			bpl := bytesperline(r.R, img.chans);
+			readn = bpl * (r.R.max.y - r.R.min.y);
+
+		s =>	# draw text
+			dirty(r.dstid, 1);
+			drawop = Draw->SoverD;
+
+		x => # draw text with bg
+			dirty(r.dstid, 1);
+			drawop = Draw->SoverD;
+
+		t =>	# adjust window z order
+			if (r.ids != nil) {
+				img := images.lookup(r.ids[0]);
+				scr := screens.lookup(img.screenid);
+				scr.setz(r.ids, r.top);
+			}
+
+		y =>	# write pixels
+			dirty(r.id, 1);
+		}
+
+		if (readn) {
+			rdata := array [readn] of byte;
+			if (sys->read(drawfd, rdata, readn) == readn)
+				readdata = rdata;
+		}
+
+		for (vs := viewers; vs != nil; vs = tl vs) {
+			v := hd vs;
+			v.process(req);
+		}
+
+		# actions that must only be done after sending to Viewers
+		pick r := req {
+		f => # free image
+			img := images.lookup(r.id);
+			if (img.screenid != 0) {
+				scr := screens.lookup(img.screenid);
+				scr.delwin(img.id);
+			}
+			images.del(r.id);
+
+		F =>	# free screen
+			scr := screens.lookup(r.id);
+			for (i := 0; i < len scr.windows; i++) {
+				img := images.lookup(scr.windows[i]);
+				img.screenid = 0;
+			}
+			screens.del(r.id);
+
+		i => # convert image to font
+			img := images.lookup(r.fontid);
+			font := ref Font;
+			font.ascent = r.ascent;
+			font.chars = array[r.nchars] of ref Fontchar;
+			img.font = font;
+
+		l =>	# load a char into font
+			img := images.lookup(r.fontid);
+			font := img.font;
+			fc := ref Fontchar(r.srcid, r.R, r.P, r.left, r.width);
+			font.chars[r.index] = fc;
+		}
+	}
+	return (offset, nil);
+}
+
+coordslen(data : array of byte, ix, n : int) : int
+{
+	start := ix;
+	dlen := len data;
+	if (ix == dlen)
+		return -1;
+	while (ix < dlen && n) {
+		n--;
+		if ((int data[ix++]) & 16r80)
+			ix += 2;
+	}
+	if (n)
+		return -1;
+	return ix - start;
+}
+
+
+imglen(i : ref Image, data : array of byte, ix : int, r : Draw->Rect, comp : int) : int
+{
+	bpl := bytesperline(r, i.chans);
+	if (!comp)
+		return (r.max.y - r.min.y) * bpl;
+	y := r.min.y;
+	lineix := byteaddr(i, r.min);
+	elineix := lineix+bpl;
+	start := ix;
+	eix := len data;
+	for (;;) {
+		if (lineix == elineix) {
+			if (++y == r.max.y)
+				break;
+			lineix = byteaddr(i, Point(r.min.x, y));
+			elineix = lineix+bpl;
+		}
+		if (ix == eix)	# buffer too small
+			return -1;
+		c := int data[ix++];
+		if (c >= 128) {
+			for (cnt := c-128+1; cnt != 0; --cnt) {
+				if (ix == eix)	# buffer too small
+					return -1;
+				if (lineix == elineix)	# phase error
+					return -1;
+				lineix++;
+				ix++;
+			}
+		} else {
+			if (ix == eix)	# short buffer
+				return -1;
+			ix++;
+			for (cnt := (c >> 2)+3; cnt != 0; --cnt) {
+				if (lineix == elineix) # phase error
+					return -1;
+				lineix++;
+			}
+		}
+	}
+	return ix-start;
+}
+
+byteaddr(i: ref Image, p: Point): int
+{
+	x := p.x - i.lorigin.x;
+	y := p.y - i.lorigin.y;
+	bits := i.chans.depth();
+	if (bits == 0)
+		# invalid chans
+		return 0;
+	return (y*i.bwidth)+(x<<3)/bits;
+}
+
+bytesperline(r: Draw->Rect, chans: Draw->Chans): int
+{
+	d := chans.depth();
+	l, t: int;
+
+	if(r.min.x >= 0){
+		l = (r.max.x*d+8-1)/8;
+		l -= (r.min.x*d)/8;
+	}else{			# make positive before divide
+		t = (-r.min.x*d+8-1)/8;
+		l = t+(r.max.x*d+8-1)/8;
+	}
+	return l;
+}
+
+get1(data : array of byte, ix : int) : int
+{
+	return int data[ix];
+}
+
+put1(data : array of byte, ix, val : int)
+{
+	data[ix] = byte val;
+}
+
+get2(data : array of byte, ix : int) : int
+{
+	return int data[ix] | ((int data[ix+1]) << 8);
+}
+
+put2(data : array of byte, ix, val : int)
+{
+	data[ix] = byte val;
+	data[ix+1] = byte (val >> 8);
+}
+
+get4(data : array of byte, ix : int) : int
+{
+	return int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
+}
+
+put4(data : array of byte, ix, val : int)
+{
+	data[ix] = byte val;
+	data[ix+1] = byte (val >> 8);
+	data[ix+2] = byte (val >> 16);
+	data[ix+3] = byte (val >> 24);
+}
+
+getP(data : array of byte, ix : int) : Draw->Point
+{
+	x := int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
+	ix += 4;
+	y := int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
+	return Draw->Point(x, y);
+}
+
+putP(data : array of byte, ix : int, P : Draw->Point)
+{
+	val := P.x;
+	data[ix] = byte val;
+	data[ix+1] = byte (val >> 8);
+	data[ix+2] = byte (val >> 16);
+	data[ix+3] = byte (val >> 24);
+	val = P.y;
+	ix += 4;
+	data[ix] = byte val;
+	data[ix+1] = byte (val >> 8);
+	data[ix+2] = byte (val >> 16);
+	data[ix+3] = byte (val >> 24);
+}
+
+getR(data : array of byte, ix : int) : Draw->Rect
+{
+	minx := int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
+	ix += 4;
+	miny := int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
+	ix += 4;
+	maxx :=  int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
+	ix += 4;
+	maxy :=  int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
+	
+	return Draw->Rect(Draw->Point(minx, miny), Draw->Point(maxx, maxy));
+}
+
+putR(data : array of byte, ix : int , R : Draw->Rect)
+{
+	val := R.min.x;
+	data[ix] = byte val;
+	data[ix+1] = byte (val >> 8);
+	data[ix+2] = byte (val >> 16);
+	data[ix+3] = byte (val >> 24);
+	val = R.min.y;
+	ix += 4;
+	data[ix] = byte val;
+	data[ix+1] = byte (val >> 8);
+	data[ix+2] = byte (val >> 16);
+	data[ix+3] = byte (val >> 24);
+	val = R.max.x;
+	ix += 4;
+	data[ix] = byte val;
+	data[ix+1] = byte (val >> 8);
+	data[ix+2] = byte (val >> 16);
+	data[ix+3] = byte (val >> 24);
+	val = R.max.y;
+	ix += 4;
+	data[ix] = byte val;
+	data[ix+1] = byte (val >> 8);
+	data[ix+2] = byte (val >> 16);
+	data[ix+3] = byte (val >> 24);
+}
+
+dirty(id, v : int)
+{
+	img := images.lookup(id);
+	img.dirty = v;
+}
+
+Screen.setz(s : self ref Screen, z : array of int, top : int)
+{
+	old := s.windows;
+	nw := array [len old] of int;
+	# use a dummy idmap to ensure uniqueness;
+	ids := Idmap.new();
+	ix := 0;
+	if (top) {
+		for (i := 0; i < len z; i++) {
+			if (ids.lookup(z[i]) == -1) {
+				ids.add(z[i], 0);
+				nw[ix++] = z[i];
+			}
+		}
+	}
+	for (i := 0; i < len old; i++) {
+		if (ids.lookup(old[i]) == -1) {
+			ids.add(old[i], 0);
+			nw[ix++] = old[i];
+		}
+	}
+	if (!top) {
+		for (i = 0; i < len z; i++) {
+			if (ids.lookup(z[i]) == -1) {
+				ids.add(z[i], 0);
+				nw[ix++] = z[i];
+			}
+		}
+	}
+	s.windows = nw;
+}
+
+Screen.addwin(s : self ref Screen, wid : int)
+{
+	nw :=  array [len s.windows + 1] of int;
+	nw[0] = wid;
+	nw[1:] = s.windows;
+	s.windows = nw;
+}
+
+Screen.delwin(s : self ref Screen, wid : int)
+{
+	if (len s.windows == 1) {
+		# assert s.windows[0] == wid
+		s.windows = nil;
+		return;
+	}
+	nw := array [len s.windows - 1] of int;
+	ix := 0;
+	for (i := 0; i < len s.windows; i++) {
+		if (s.windows[i] == wid)
+			continue;
+		nw[ix++] = s.windows[i];
+	}
+	s.windows = nw;
+}
+
+Idmap.new() : ref Idmap
+{
+	m := ref Idmap;
+	m.buckets = array[256] of ref Idpair;
+	return m;
+}
+
+Idmap.add(m : self ref Idmap, key, val : int)
+{
+	h := key & 16rff;
+	m.buckets[h] = ref Idpair (key, val, m.buckets[h]);
+}
+
+Idmap.del(m : self ref Idmap, key : int)
+{
+	h := key &16rff;
+	prev := m.buckets[h];
+	if (prev == nil)
+		return;
+	if (prev.key == key) {
+		m.buckets[h] = m.buckets[h].next;
+		return;
+	}
+	for (idp := prev.next; idp != nil; idp = idp.next) {
+		if (idp.key == key)
+			break;
+		prev = idp;
+	}
+	if (idp != nil)
+		prev.next = idp.next;
+}
+
+Idmap.lookup(m :self ref Idmap, key : int) : int
+{
+	h := key &16rff;
+	for (idp := m.buckets[h]; idp != nil; idp = idp.next) {
+		if (idp.key == key)
+			return idp.val;
+	}
+	return -1;
+}
+
+Imageset.new() : ref Imageset
+{
+	s := ref Imageset;
+	s.images = array [32] of ref Image;
+	s.ixmap = Idmap.new();
+	for (i := 0; i < len s.images; i++)
+		s.freelist = i :: s.freelist;
+	return s;
+}
+
+Imageset.add(s: self ref Imageset, id: int, img: ref Image)
+{
+	if (s.freelist == nil) {
+		n := 2 * len s.images;
+		ni := array [n] of ref Image;
+		ni[:] = s.images;
+		for (i := len s.images; i < n; i++)
+			s.freelist = i :: s.freelist;
+		s.images = ni;
+	}
+	ix := hd s.freelist;
+	s.freelist = tl s.freelist;
+	s.images[ix] = img;
+	s.ixmap.add(id, ix);
+	img.refc++;
+}
+
+Imageset.del(s: self ref Imageset, id: int)
+{
+	ix := s.ixmap.lookup(id);
+	if (ix == -1)
+		return;
+	img := s.images[ix];
+	if (img != nil)
+		img.refc--;
+	s.images[ix] = nil;
+	s.freelist = ix :: s.freelist;
+	s.ixmap.del(id);
+}
+
+Imageset.lookup(s : self ref Imageset, id : int ) : ref Image
+{
+	ix := s.ixmap.lookup(id);
+	if (ix == -1)
+		return nil;
+	return s.images[ix];
+}
+
+Imageset.findname(s: self ref Imageset, name: string): ref Image
+{
+	for (ix := 0; ix < len s.images; ix++) {
+		img := s.images[ix];
+		if (img != nil && img.name == name)
+			return img;
+	}
+	return nil;
+}
+
+Screenset.new() : ref Screenset
+{
+	s := ref Screenset;
+	s.screens = array [32] of ref Screen;
+	s.ixmap = Idmap.new();
+	for (i := 0; i < len s.screens; i++)
+		s.freelist = i :: s.freelist;
+	return s;
+}
+
+Screenset.add(s : self ref Screenset, scr : ref Screen)
+{
+	if (s.freelist == nil) {
+		n := 2 * len s.screens;
+		ns := array [n] of ref Screen;
+		ns[:] = s.screens;
+		for (i := len s.screens; i < n; i++)
+			s.freelist = i :: s.freelist;
+		s.screens = ns;
+	}
+	ix := hd s.freelist;
+	s.freelist = tl s.freelist;
+	s.screens[ix] = scr;
+	s.ixmap.add(scr.id, ix);
+}
+
+Screenset.del(s : self ref Screenset, id : int)
+{
+	ix := s.ixmap.lookup(id);
+	if (ix == -1)
+		return;
+	s.screens[ix] = nil;
+	s.freelist = ix :: s.freelist;
+	s.ixmap.del(id);
+}
+
+Screenset.lookup(s : self ref Screenset, id : int ) : ref Screen
+{
+	ix := s.ixmap.lookup(id);
+	if (ix == -1)
+		return nil;
+	return s.screens[ix];
+}
+
+
+Viewer : adt {
+	imgmap:	ref Idmap;
+	scrmap:	ref Idmap;
+	chanmap:	ref Idmap;	# maps to 1 for images that require chan conversion
+
+	imageid:	int;
+	screenid:	int;
+	whiteid:	int;
+	hungup:	int;
+	dchans:	Draw->Chans;	# chans.desc of remote display img
+
+	# temporary image for chan conversion
+	tmpid:	int;
+	tmpR:	Draw->Rect;
+
+	output:	chan of (array of byte, chan of string);
+
+	new:		fn(fd: ref Sys->FD, pubscr: int, chans: Draw->Chans): string;
+	process:	fn(v: self ref Viewer, req: ref Drawreq);
+	getimg:	fn(v: self ref Viewer, id: int): int;
+	getscr:	fn(v: self ref Viewer, id, win: int): (int, int);
+	copyimg:	fn(v: self ref Viewer, img: ref Image, id: int);
+	chanconv:	fn(v: self ref Viewer, img: ref Image, id: int, r: Rect, ymsg: array of byte);
+};
+
+vwriter(fd : ref Sys->FD, datac : chan of array of byte, nc : chan of string)
+{
+	for (;;) {
+		data := <- datac;
+		if (data == nil)
+			return;
+		n := sys->write(fd, data, len data);
+		if (n != len data) {
+#			sys->print("[%c]: %r\n", int data[0]);
+#			sys->print("[%c] datalen %d got %d error: %r\n", int data[0], len data, n);
+			nc <-= sys->sprint("%r");
+		} else {
+#			sys->print("[%c]", int data[0]);
+			nc <-= nil;
+		}
+	}
+}
+
+vbmsg : adt {
+	data : array of byte;
+	rc : chan of string;
+	next : cyclic ref vbmsg;
+};
+
+vbuffer(v : ref Viewer, fd : ref Sys->FD)
+{
+	ioc := v.output;
+	datac := chan of array of byte;
+	errc := chan of string;
+	spawn vwriter(fd, datac, errc);
+	fd = nil;
+
+	msghd : ref vbmsg;
+	msgtl : ref vbmsg;
+
+Loop:
+	for (;;) alt {
+	(data, rc) := <- ioc =>
+		if (data == nil)
+			break Loop;
+		if (msgtl != nil) {
+			if (msgtl != msghd && msgtl.rc == nil && (len msgtl.data + len data) <= Sys->ATOMICIO) {
+				ndata := array [len msgtl.data + len data] of byte;
+				ndata[:] = msgtl.data;
+				ndata[len msgtl.data:] = data;
+				msgtl.data = ndata;
+				msgtl.rc = rc;
+			} else {
+				msgtl.next = ref vbmsg (data, rc, nil);
+				msgtl = msgtl.next;
+			}
+		} else {
+			msghd = ref vbmsg (data, rc, nil);
+			msgtl = msghd;
+			datac <-= data;
+		}
+	err := <- errc =>
+		if (msghd.rc != nil)
+			msghd.rc <- = err;
+		msghd = msghd.next;
+		if (msghd != nil)
+			datac <-= msghd.data;
+		else
+			msgtl = nil;
+		if (err == Ehungup) {
+			nhangups++;
+			v.hungup = 1;
+		}
+	}
+	# shutdown vwriter (may be blocked sending on errc)
+	for (;;) alt {
+	<- errc =>
+		;
+	datac <- = nil =>
+		return;
+	}
+}
+
+Viewer.new(fd: ref Sys->FD, pubscr: int, chans: Draw->Chans): string
+{
+	v := ref Viewer;
+	v.output = chan of (array of byte, chan of string);
+	spawn vbuffer(v, fd);
+
+	v.imgmap = Idmap.new();
+	v.scrmap = Idmap.new();
+	v.chanmap = Idmap.new();
+	v.imageid = 0;
+	v.screenid = pubscr;
+	v.hungup = 0;
+	v.dchans = chans;
+	v.tmpid = 0;
+	v.tmpR = Rect((0,0), (0,0));
+
+#D := array[1+1] of byte;
+#D[0] = byte 'D';
+#D[1] = byte 1;
+#v.output <-= (D, nil);
+
+	reply := chan of string;
+	# import remote public screen into our remote draw client
+	S := array [1+4+4] of byte;
+	S[0] = byte 'S';
+	put4(S, OPS_id, pubscr);
+	put4(S, OPS_chans, chans.desc);
+	v.output <-= (S, reply);
+	err := <- reply;
+	if (err != nil) {
+		v.output <-= (nil, nil);
+		return err;
+	}
+
+	# create remote window
+	dispid := ++v.imageid;
+	b := array [1+4+4+1+4+1+(4*4)+(4*4)+4] of byte;
+	b[0] = byte 'b';
+	put4(b, OPb_id, dispid);
+	put4(b, OPb_screenid, pubscr);
+	put1(b, OPb_refresh, 0);
+	put4(b, OPb_chans, chans.desc);
+	put1(b, OPb_repl, 0);
+	putR(b, OPb_R, drawR);
+	putR(b, OPb_clipR, drawR);
+	put4(b, OPb_rrggbbaa, Draw->White);
+	v.output <-= (b, reply);
+	err = <- reply;
+	if (err != nil) {
+		v.output <-= (nil, nil);
+		return err;
+	}
+
+	# map local display image id to remote window image id
+	v.imgmap.add(0, dispid);
+	if (!drawchans.eq(chans))
+		# writepixels on this image must be chan converted
+		v.chanmap.add(0, 1);
+
+	# create 'white' repl image for use as mask
+	v.whiteid = ++v.imageid;
+	put4(b, OPb_id, v.whiteid);
+	put4(b, OPb_screenid, 0);
+	put1(b, OPb_refresh, 0);
+	put4(b, OPb_chans, (Draw->RGBA32).desc);
+	put1(b, OPb_repl, 1);
+	putR(b, OPb_R, Rect((0,0), (1,1)));
+	putR(b, OPb_clipR, Rect((-16r3FFFFFFF, -16r3FFFFFFF), (16r3FFFFFFF, 16r3FFFFFFF)));
+	put4(b, OPb_rrggbbaa, Draw->White);
+	v.output <-= (b, reply);
+	err = <- reply;
+	if (err != nil) {
+		v.output <-= (nil, nil);
+		return err;
+	}
+
+	img := images.lookup(0);
+	key := chan of int;
+	drawlock <- = key;
+	v.copyimg(img, dispid);
+
+	O := array [1+1] of byte;
+	O[0] = byte 'O';
+	O[1] = byte drawop;
+	v.output <-= (O, nil);
+
+	flush := array [1] of byte;
+	flush[0] = byte 'v';
+	v.output <- = (flush, nil);
+	viewers = v :: viewers;
+	key <-= 1;
+	return nil;
+}
+
+Viewer.process(v : self ref Viewer, req : ref Drawreq)
+{
+	data := req.data;
+	pick r := req {
+	b => # allocate image
+		imgid := ++v.imageid;
+		if (r.screenid != 0) {
+			(scrid, mapchans) := v.getscr(r.screenid, 0);
+			put4(data, OPb_screenid, scrid);
+			if (mapchans) {
+				put4(data, OPb_chans, v.dchans.desc);
+				v.chanmap.add(r.id, 1);
+			}
+		}
+		v.imgmap.add(r.id, imgid);
+		put4(data, OPb_id, imgid);
+
+	A => # allocate screen
+		imgid := v.getimg(r.imageid);
+		put4(data, OPA_fillid, v.getimg(r.fillid));
+		put4(data, OPA_imageid, imgid);
+		reply := chan of string;
+		for (i := 0; i < 25; i++) {
+			put4(data, OPA_id, ++v.screenid);
+			v.output <-= (data, reply);
+			if (<-reply == nil) {
+				v.scrmap.add(r.id, v.screenid);
+				return;
+			}
+		}
+		return;
+
+	c => # set clipr and repl
+		put4(data, OPc_dstid, v.getimg(r.dstid));
+
+	d =>	 # general draw op
+		dstid := v.imgmap.lookup(r.dstid);
+		if (dstid == -1) {
+			# don't do draw op as getimg() will do a writepixels
+			v.getimg(r.dstid);
+			return;
+		}
+		put4(data, OPd_maskid, v.getimg(r.maskid));
+		put4(data, OPd_srcid, v.getimg(r.srcid));
+		put4(data, OPd_dstid, dstid);
+
+	e =>	 # draw ellipse
+		dstid := v.imgmap.lookup(r.dstid);
+		if (dstid == -1) {
+			# don't do draw op as getimg() will do a writepixels
+			v.getimg(r.dstid);
+			return;
+		}
+		put4(data, OPe_srcid, v.getimg(r.srcid));
+		put4(data, OPe_dstid, dstid);
+
+	f => # free image
+		id := v.imgmap.lookup(r.img.id);
+		if (id == -1)
+			# Viewer has never seen this image - ignore
+			return;
+		v.imgmap.del(r.id);
+		# Viewers alias named images - only delete if last reference
+		if (r.img.refc > 1)
+			return;
+		v.chanmap.del(r.img.id);
+		put4(data, OPf_id, id);
+
+	F => # free screen
+		id := v.scrmap.lookup(r.id);
+		scr := screens.lookup(r.id);
+		# image and fill are free'd separately
+		#v.imgmap.del(scr.imageid);
+		#v.imgmap.del(scr.fillid);
+		if (id == -1)
+			return;
+		put4(data, OPF_id, id);
+
+	i => # convert image to font
+		put4(data, OPi_fontid, v.getimg(r.fontid));
+
+	l => # load a char into font
+		put4(data, OPl_srcid, v.getimg(r.srcid));
+		put4(data, OPl_fontid, v.getimg(r.fontid));
+
+	L => # draw line
+		dstid := v.imgmap.lookup(r.dstid);
+		if (dstid == -1) {
+			# don't do draw op as getimg() will do a writepixels
+			v.getimg(r.dstid);
+			return;
+		}
+		put4(data, OPL_srcid, v.getimg(r.srcid));
+		put4(data, OPL_dstid, dstid);
+
+#	n =>	# attach to named image
+#	N =>	# name
+#		Handled by id remapping to avoid clashes in namespace of remote viewers.
+#		If it is a name we know then the id is remapped within the images Imageset
+#		Otherwise, there is nothing we can do other than ignore all ops related to the id.
+
+	o =>	 # set image origins
+		id := v.imgmap.lookup(r.id);
+		if (id == -1)
+			# Viewer has never seen this image - ignore
+			return;
+		put4(data, OPo_id, id);
+
+	O =>	# set next compositing op
+		;
+
+	p =>	 # draw polygon
+		dstid := v.imgmap.lookup(r.dstid);
+		if (dstid == -1) {
+			# don't do draw op as getimg() will do a writepixels
+			v.getimg(r.dstid);
+			return;
+		}
+		put4(data, OPp_srcid, v.getimg(r.srcid));
+		put4(data, OPp_dstid, dstid);
+
+	s => # draw text
+		dstid := v.imgmap.lookup(r.dstid);
+		if (dstid == -1) {
+			# don't do draw op as getimg() will do a writepixels
+			v.getimg(r.dstid);
+			return;
+		}
+		put4(data, OPs_fontid, v.getimg(r.fontid));
+		put4(data, OPs_srcid, v.getimg(r.srcid));
+		put4(data, OPs_dstid, dstid);
+
+	x =>	# draw text with bg
+		dstid := v.imgmap.lookup(r.dstid);
+		if (dstid == -1) {
+			# don't do draw op as getimg() will do a writepixels
+			v.getimg(r.dstid);
+			return;
+		}
+		put4(data, OPx_fontid, v.getimg(r.fontid));
+		put4(data, OPx_srcid, v.getimg(r.srcid));
+		put4(data, OPx_bgid, v.getimg(r.bgid));
+		put4(data, OPx_dstid, dstid);
+
+	t => # adjust window z order
+		for (i := 0; i < len r.ids; i++)
+			put4(data, OPt_id + 4*i, v.getimg(r.ids[i]));
+
+	v => # flush updates to display
+			;
+
+	y => # write pixels
+		id := v.imgmap.lookup(r.id);
+		if (id == -1) {
+			# don't do draw op as getimg() will do a writepixels
+			v.getimg(r.id);
+			return;
+		}
+		if (!drawchans.eq(v.dchans) && v.chanmap.lookup(r.id) != -1) {
+			# chans clash
+			img := images.lookup(r.id);
+			# copy data as other Viewers may alter contents
+			copy := (array [len data] of byte)[:] = data;
+			v.chanconv(img, id, r.R, copy);
+			return;
+		}
+		put4(data, OPy_id, id);
+
+	* =>
+		return;
+	}
+	# send out a copy of the data as other Viewers may alter contents
+	copy := array [len data] of byte;
+	copy[:] = data;
+	v.output <-= (copy, nil);
+}
+
+Viewer.getimg(v: self ref Viewer, localid: int) : int
+{
+	remid := v.imgmap.lookup(localid);
+	if (remid != -1)
+		return remid;
+
+	img := images.lookup(localid);
+	if (img.id != localid) {
+		# attached via name, see if we have the aliased image
+		remid = v.imgmap.lookup(img.id);
+		if (remid != -1) {
+			# we have it, add mapping to save us this trouble next time
+			v.imgmap.add(localid, remid);
+			return remid;
+		}
+	}
+	# is the image a window?
+	scrid := 0;
+	mapchans := 0;
+	if (img.screenid != 0)
+		(scrid, mapchans) = v.getscr(img.screenid, img.id);
+
+	vid := ++v.imageid;
+	# create the image
+	# note: clipr for image creation has to be based on screen co-ords
+	clipR := img.clipR.subpt(img.lorigin);
+	clipR = clipR.addpt(img.R.min);
+	b := array [1+4+4+1+4+1+(4*4)+(4*4)+4] of byte;
+	b[0] = byte 'b';
+	put4(b, OPb_id, vid);
+	put4(b, OPb_screenid, scrid);
+	put1(b, OPb_refresh, 0);
+	if (mapchans)
+		put4(b, OPb_chans, v.dchans.desc);
+	else
+		put4(b, OPb_chans, img.chans.desc);
+	put1(b, OPb_repl, img.repl);
+	putR(b, OPb_R, img.R);
+	putR(b, OPb_clipR, clipR);
+	put4(b, OPb_rrggbbaa, img.rrggbbaa);
+	v.output <-= (b, nil);
+
+	v.imgmap.add(img.id, vid);
+	if (mapchans)
+		v.chanmap.add(img.id, 1);
+
+	# set the origin
+	if (img.lorigin.x != img.R.min.x || img.lorigin.y != img.R.min.y) {
+		o := array [1+4+(2*4)+(2*4)] of byte;
+		o[0] = byte 'o';
+		put4(o, OPo_id, vid);
+		putP(o, OPo_rmin, img.lorigin);
+		putP(o, OPo_screenrmin, img.R.min);
+		v.output <-= (o, nil);
+	}
+
+	# is the image a font?
+	if (img.font != nil) {
+		f := img.font;
+		i := array [1+4+4+1] of byte;
+		i[0] = byte 'i';
+		put4(i, OPi_fontid, vid);
+		put4(i, OPi_nchars, len f.chars);
+		put1(i, OPi_ascent, f.ascent);
+		v.output <-= (i, nil);
+	
+		for (index := 0; index < len f.chars; index++) {
+			ch := f.chars[index];
+			if (ch == nil)
+				continue;
+			l := array [1+4+4+2+(4*4)+(2*4)+1+1] of byte;
+			l[0] = byte 'l';
+			put4(l, OPl_fontid, vid);
+			put4(l, OPl_srcid, v.getimg(ch.srcid));
+			put2(l, OPl_index, index);
+			putR(l, OPl_R, ch.R);
+			putP(l, OPl_P, ch.P);
+			put1(l, OPl_left, ch.left);
+			put1(l, OPl_width, ch.width);
+			v.output <-= (l, nil);
+		}
+	}
+
+	# if 'dirty' then writepixels
+	if (img.dirty)
+		v.copyimg(img, vid);
+
+	return vid;
+}
+
+Viewer.copyimg(v : self ref Viewer, img : ref Image, id : int)
+{
+	dx := img.R.max.x - img.R.min.x;
+	dy := img.R.max.y - img.R.min.y;
+	srcR := Rect (img.lorigin, (img.lorigin.x + dx, img.lorigin.y + dy));
+	bpl := bytesperline(srcR, img.chans);
+	rlen : con 1+4+(4*4);
+	ystep := (Sys->ATOMICIO - rlen)/ bpl;
+	minx := srcR.min.x;
+	maxx := srcR.max.x;
+	maxy := srcR.max.y;
+
+	chanconv := 0;
+	if (!drawchans.eq(v.dchans) && v.chanmap.lookup(img.id) != -1)
+		chanconv = 1;
+
+	for (y := img.lorigin.y; y < maxy; y += ystep) {
+		if (y + ystep > maxy)
+			ystep = (maxy - y);
+		R := Draw->Rect((minx, y), (maxx, y+ystep));
+		r := array [rlen] of byte;
+		r[0] = byte 'r';
+		put4(r, OPr_id, img.id);
+		putR(r, OPr_R, R);
+		if (sys->write(drawfd, r, len r) != len r)
+			break;
+
+		nb := bpl * ystep;
+		ymsg := array [1+4+(4*4)+nb] of byte;
+		ymsg[0] = byte 'y';
+#		put4(ymsg, OPy_id, id);
+		putR(ymsg, OPy_R, R);
+		n := sys->read(drawfd, ymsg[OPy_data:], nb);
+		if (n != nb)
+			break;
+		if (chanconv)
+			v.chanconv(img, id, R, ymsg);
+		else {
+			put4(ymsg, OPy_id, id);
+			v.output <-= (ymsg, nil);
+		}
+	}
+}
+
+Viewer.chanconv(v: self ref Viewer, img: ref Image, id: int, r: Rect, ymsg: array of byte)
+{
+	# check origin matches and enough space in conversion image
+	if (!(img.lorigin.eq(v.tmpR.min) && r.inrect(v.tmpR))) {
+		# create new tmp image
+		if (v.tmpid != 0) {
+			f := array [1+4] of byte;
+			f[0] = byte 'f';
+			put4(f, OPf_id, v.tmpid);
+			v.output <-= (f, nil);
+		}
+		v.tmpR = Rect((0,0), (img.R.dx(), img.R.dy())).addpt(img.lorigin);
+		v.tmpid = ++v.imageid;
+		b := array [1+4+4+1+4+1+(4*4)+(4*4)+4] of byte;
+		b[0] = byte 'b';
+		put4(b, OPb_id, v.tmpid);
+		put4(b, OPb_screenid, 0);
+		put1(b, OPb_refresh, 0);
+		put4(b, OPb_chans, drawchans.desc);
+		put1(b, OPb_repl, 0);
+		putR(b, OPb_R, v.tmpR);
+		putR(b, OPb_clipR, v.tmpR);
+		put4(b, OPb_rrggbbaa, Draw->Nofill);
+		v.output <-= (b, nil);
+	}
+	# writepixels to conversion image
+	put4(ymsg, OPy_id, v.tmpid);
+	v.output <-= (ymsg, nil);
+
+	# ensure that drawop is Draw->S
+	if (drawop != Draw->S) {
+		O := array [1+1] of byte;
+		O[0] = byte 'O';
+		put1(O, OPO_op, Draw->S);
+		v.output <-= (O, nil);
+	}
+	# blit across to real target
+	d := array [1+4+4+4+(4*4)+(2*4)+(2*4)] of byte;
+	d[0] = byte 'd';
+	put4(d, OPd_dstid, id);
+	put4(d, OPd_srcid, v.tmpid);
+	put4(d, OPd_maskid, v.whiteid);
+	putR(d, OPd_R, r);
+	putP(d, OPd_P0, r.min);
+	putP(d, OPd_P1, r.min);
+	v.output <-= (d, nil);
+
+	# restore drawop if necessary
+	if (drawop != Draw->S) {
+		O := array [1+1] of byte;
+		O[0] = byte 'O';
+		put1(O, OPO_op, drawop);
+		v.output <-= (O, nil);
+	}
+}
+
+# returns (rid, map)
+# rid == remote screen id
+# map indicates that chan mapping is required for windows on this screen
+
+Viewer.getscr(v : self ref Viewer, localid, winid : int) : (int, int)
+{
+	remid := v.scrmap.lookup(localid);
+	if (remid != -1) {
+		if (drawchans.eq(v.dchans))
+			return (remid, 0);
+		scr := screens.lookup(localid);
+		if (v.chanmap.lookup(scr.imageid) == -1)
+			return (remid, 0);
+		return (remid, 1);
+	}
+
+	scr := screens.lookup(localid);
+	imgid := v.getimg(scr.imageid);
+	fillid := v.getimg(scr.fillid);
+	A := array [1+4+4+4+1] of byte;
+	A[0] = byte 'A';
+	put4(A, OPA_imageid, imgid);
+	put4(A, OPA_fillid, fillid);
+	put1(A, OPA_public, 0);
+
+	reply := chan of string;
+	for (i := 0; i < 25; i++) {
+		put4(A, OPA_id, ++v.screenid);
+		v.output <-= (A, reply);
+		if (<-reply != nil)
+			continue;
+		v.scrmap.add(localid, v.screenid);
+		break;
+	}
+	# if i == 25 then we have a problem
+	# ...
+	if (i == 25) {
+#		sys->print("failed to create remote screen\n");
+		return (0, 0);
+	}
+
+	# pre-construct the windows on this screen
+	for (ix := len scr.windows -1; ix >=0; ix--)
+		if (scr.windows[ix] != winid)
+			v.getimg(scr.windows[ix]);
+
+	if (drawchans.eq(v.dchans))
+		return (v.screenid, 0);
+	if (v.chanmap.lookup(scr.imageid) == -1)
+		return (v.screenid, 0);
+	return (v.screenid, 1);
+}
--- /dev/null
+++ b/appl/wm/drawmux/drawmux.m
@@ -1,0 +1,6 @@
+Drawmux: module {
+	PATH: con "/dis/lib/drawmux.dis";
+
+	init: fn(): (string, ref Draw->Display);
+	newviewer: fn(fd: ref Sys->FD);
+};
--- /dev/null
+++ b/appl/wm/drawmux/drawoffs.m
@@ -1,0 +1,185 @@
+# allocate image (old)
+#OPa_id		: con 1;
+#OPa_screenid	: con 5;
+#OPa_refresh	: con 9;
+#OPa_ldepth	: con 10;
+#OPa_repl		: con 12;
+#OPa_R		: con 13;
+#OPa_clipR		: con 29;
+#OPa_value	: con 45;
+
+# allocate image (new)
+OPb_id		: con 1;
+OPb_screenid	: con 5;
+OPb_refresh	: con 9;
+OPb_chans	: con 10;
+OPb_repl		: con 14;
+OPb_R		: con 15;
+OPb_clipR	: con 31;
+OPb_rrggbbaa	: con 47;
+
+# allocate screen
+OPA_id		: con 1;
+OPA_imageid	: con 5;
+OPA_fillid		: con 9;
+OPA_public	: con 13;
+
+# set repl & clipr
+OPc_dstid		: con 1;
+OPc_repl		: con 5;
+OPc_clipR		: con 6;
+
+# set cursor image and hotspot
+#OPC_id		: con 1;
+#OPC_hotspot	: con 5;
+
+# the primitive draw op
+OPd_dstid	: con 1;
+OPd_srcid		: con 5;
+OPd_maskid	: con 9;
+OPd_R		: con 13;
+OPd_P0		: con 29;
+OPd_P1		: con 37;
+
+# enable debug messages
+OPD_val		: con 1;
+
+# ellipse
+OPe_dstid	: con 1;
+OPe_srcid		: con 5;
+OPe_center	: con 9;
+OPe_a		: con 17;
+OPe_b		: con 21;
+OPe_thick		: con 25;
+OPe_sp		: con 29;
+OPe_alpha	: con 37;
+OPe_phi		: con 41;
+
+# filled ellipse
+OPE_dstid	: con 1;
+OPE_srcid		: con 5;
+OPE_center	: con 9;
+OPE_a		: con 17;
+OPE_b		: con 21;
+OPE_thick		: con 25;
+OPE_sp		: con 29;
+OPE_alpha	: con 37;
+OPE_phi		: con 41;
+
+# free image
+OPf_id		: con 1;
+
+# free screen
+OPF_id		: con 1;
+
+# init font
+OPi_fontid	: con 1;
+OPi_nchars	: con 5;
+OPi_ascent	: con 9;
+
+# load font char
+OPl_fontid	: con 1;
+OPl_srcid		: con 5;
+OPl_index	: con 9;
+OPl_R		: con 11;
+OPl_P		: con 27;
+OPl_left		: con 35;
+OPl_width	: con 36;
+
+# line
+OPL_dstid		: con 1;
+OPL_P0		: con 5;
+OPL_P1		: con 13;
+OPL_end0		: con 21;
+OPL_end1		: con 25;
+OPL_radius	: con 29;
+OPL_srcid		: con 33;
+OPL_sp		: con 37;
+
+# attach to named image
+OPn_dstid	: con 1;
+OPn_j		: con 5;
+OPn_name	: con 6;
+
+# name image
+OPN_dstid	: con 1;
+OPN_in		: con 5;
+OPN_j		: con 6;
+OPN_name	: con 7;
+
+# set window origins
+OPo_id		: con 1;
+OPo_rmin		: con 5;
+OPo_screenrmin	: con 13;
+
+# set next compositing operator
+OPO_op		: con 1;
+
+# polygon
+OPp_dstid	: con 1;
+OPp_n		: con 5;
+OPp_end0	: con 7;
+OPp_end1	: con 11;
+OPp_radius	: con 15;
+OPp_srcid		: con 19;
+OPp_sp		: con 23;
+OPp_P0		: con 31;
+OPp_dp		: con 39;
+
+# filled polygon
+OPP_dstid	: con 1;
+OPP_n		: con 5;
+OPP_wind	: con 7;
+OPP_ignore	: con 11;
+OPP_srcid		: con 19;
+OPP_sp		: con 23;
+OPP_P0		: con 31;
+OPP_dp		: con 39;
+
+# read
+OPr_id		: con 1;
+OPr_R		: con 5;
+
+# string
+OPs_dstid		: con 1;
+OPs_srcid		: con 5;
+OPs_fontid	: con 9;
+OPs_P		: con 13;
+OPs_clipR		: con 21;
+OPs_sp		: con 37;
+OPs_ni		: con 45;
+OPs_index	: con 47;
+
+# stringbg
+OPx_dstid	: con 1;
+OPx_srcid		: con 5;
+OPx_fontid	: con 9;
+OPx_P		: con 13;
+OPx_clipR		: con 21;
+OPx_sp		: con 37;
+OPx_ni		: con 45;
+OPx_bgid		: con 47;
+OPx_bgpt		: con 51;
+OPx_index	: con 59;
+
+# attach to public screen
+OPS_id		: con 1;
+OPS_chans		: con 5;
+
+# visible
+# top or bottom windows
+OPt_top		: con 1;
+OPt_nw		: con 2;
+OPt_id		: con 4;
+
+#OPv		no fields
+
+# write
+OPy_id		: con 1;
+OPy_R		: con 5;
+OPy_data		: con 21;
+
+# write compressed
+OPY_id		: con 1;
+OPY_R		: con 5;
+OPY_data		: con 21;
--- /dev/null
+++ b/appl/wm/drawmux/mkfile
@@ -1,0 +1,37 @@
+<../../../mkconfig
+
+TARG=\
+	dmview.dis\
+	dmwm.dis\
+
+LIBTARG=\
+	drawmux.dis\
+
+MODULES=\
+	drawmux.m\
+	drawoffs.m\
+
+SYSMODULES=\
+	arg.m\
+	draw.m\
+	sh.m\
+	sys.m\
+	tk.m\
+	wmlib.m\
+
+DISBIN=$ROOT/dis/wm
+DISLIB=$ROOT/dis/lib
+
+all:V:	$TARG $LIBTARG
+
+install:V:	$DISBIN/dmview.dis $DISBIN/dmwm.dis $DISLIB/drawmux.dis
+
+<$ROOT/mkfiles/mkdis
+
+nuke:V:	nuke-lib
+
+nuke-lib:V:
+	cd $DISLIB; rm -f $LIBTARG
+
+$DISLIB/%.dis:	%.dis
+	rm -f $DISLIB/$stem.dis && cp $stem.dis $DISLIB/$stem.dis
--- /dev/null
+++ b/appl/wm/edit.b
@@ -1,0 +1,730 @@
+#
+# Copyright © 1996-1999 Lucent Technologies Inc.  All rights reserved.
+#	Modified version of edit
+#	D.B.Knudsen
+# Revisions Copyright © 2000-2002 Vita Nuova Holdings Limited.  All rights reserved.
+#
+implement WmEdit;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Rect, Screen: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+WmEdit: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+ErrIco: con "error -fg red";
+
+ed: ref Tk->Toplevel;
+dirty := 0;
+
+BLUE : con "#0000ff";
+GREEN : con "#008800";
+
+SEARCH,
+SEARCHFOR,
+REPLACE,
+REPLACEWITH,
+REPLACEALL,
+NOSEE : con iota;
+
+ed_config := array[] of {
+	"frame .m -relief raised",
+	"frame .b",
+	"menubutton .m.file -text File -menu .m.file.menu",
+	"menubutton .m.edit -text Edit -menu .m.edit.menu",
+	"menubutton .m.search -text Search -menu .m.search.menu",
+	"menubutton .m.options -text Options -menu .m.options.menu",
+#	"label .m.filename",
+	"pack .m.file .m.edit .m.search .m.options -side left",
+#	"pack .m.filename -padx 10 -side left",
+	"menu .m.file.menu",
+	".m.file.menu add command -label New -command {send c new}",
+	".m.file.menu add command -label Open... -command {send c open}",
+	".m.file.menu add separator",
+	".m.file.menu add command -label Save -command {send c save}",
+	".m.file.menu add command -label {Save As...} -command {send c saveas}",
+	".m.file.menu add separator",
+	".m.file.menu add command -label {Exit} -command {send c exit}",
+	"menu .m.edit.menu",
+	".m.edit.menu add command -label Cut -command {send c cut}",
+	".m.edit.menu add command -label Copy -command {send c copy}",
+	".m.edit.menu add command -label Paste -command {send c paste}",
+	"menu .m.search.menu",
+	".m.search.menu add command -label {Find ...} " +
+					"-command {send c searchf}",
+	".m.search.menu add command -label {Replace with...} " +
+					"-command {send c replacew}",
+	".m.search.menu add command -label {Find Again} -command {send c search}",
+	".m.search.menu add command -label {Find and Replace} " +
+					"-command {send c replace}",
+	".m.search.menu add command -label {Find and Replace All} " +
+					"-command {send c replaceall}",
+	"menu .m.options.menu",
+	".m.options.menu add checkbutton -text Limbo -command {send c limbo}",
+	".m.options.menu add command -label Indent -command {send c indent}",
+	"text .b.t  -yscrollcommand {.b.s set} -bg white",
+	"bind .b.t <Button-2> {.m.edit.menu post %X %Y}",
+	"bind .b.t <Key> +{send c dirtied {%A}}",
+	"bind .b.t <ButtonRelease-1> +{send c reindent}",
+	"scrollbar .b.s -command {.b.t yview}",
+	"pack .m -fill x",
+	"pack .b.s -fill y -side left",
+	"pack .b.t -fill both -expand 1",
+	"pack .b -fill both -expand 1",
+	"focus .b.t",
+	"pack propagate . 0",
+	".b.t tag configure keyword -fg " + BLUE,
+	".b.t tag configure comment -fg " + GREEN,
+	"update",
+};
+
+context : ref Draw->Context;
+curfile := "(New)";
+snarf := "";
+searchfor := "";
+replacewith := "";
+path := ".";
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	wmctl: chan of string;
+
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+	dialog = load Dialog Dialog->PATH;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+	selectfile->init();
+	dialog->init();
+
+	context = ctxt;
+
+	(ed, wmctl) = tkclient->toplevel(context, "", "Edit", Tkclient->Appl);
+
+	argv = tl argv;
+
+	c := chan of string;
+	tk->namechan(ed, c, "c");
+	for (i := 0; i < len ed_config; i++)
+		cmd(ed, ed_config[i]);
+
+	if (argv != nil) {
+		e := loadtfile(hd argv);
+		if(e != nil)
+			dialog->prompt(ctxt, ed.image, ErrIco, "Open file", e, 0, "Ok"::nil);
+	}
+
+	tkclient->settitle(ed, "Edit " + curfile);
+	tkclient->onscreen(ed, nil);
+	tkclient->startinput(ed, "ptr" :: "kbd" :: nil);
+	cmd(ed, "update");
+
+	e := cmd(ed, "variable lasterror");
+	if(e != "") {
+		sys->print("edit error: %s\n", e);
+		return;
+	}
+
+	cmdloop: for(;;) {
+		alt {
+		key := <-ed.ctxt.kbd =>
+			tk->keyboard(ed, key);
+		m := <-ed.ctxt.ptr =>
+			tk->pointer(ed, *m);
+		s := <-ed.ctxt.ctl or
+		s = <-ed.wreq or
+		s = <-wmctl =>
+			if(s == "exit") {
+				if (check_dirty())
+					break cmdloop;
+				else
+					break;
+			}
+			task_title: string;
+			if (s == "task") {
+				if (curfile == "(New)")
+					task_title = tkclient->settitle(ed, "Edit");
+				else
+					task_title = tkclient->settitle(ed, "Edit " + curfile);
+				cmd(ed, "update");
+			}
+			tkclient->wmctl(ed, s);
+			if (s == "task")
+				tkclient->settitle(ed, task_title);
+		s := <-c =>
+			if ( len s > 7 && s[:7] == "dirtied" ) {
+				set_dirty(); do_limbo_check(s);
+			}
+			else
+			case s {
+			"exit" =>	if ( check_dirty() ){ set_clean(); break cmdloop; }
+			"dirtied" =>	set_dirty(); do_limbo_check(s);
+			"new" =>	if ( check_dirty()) {set_clean(); do_new();}
+			"open" =>	if ( check_dirty() && do_open()) set_clean();
+			"save" =>	do_save(0);
+			"saveas" =>	do_save(1);
+			"cut" =>	do_snarf(1); set_dirty();
+			"copy" =>	do_snarf(0);
+			"paste" =>	do_paste(); set_dirty();
+			"search" =>	do_search(SEARCH);
+			"searchf" =>	do_search(SEARCHFOR);
+			"replace" =>	do_replace(REPLACE);
+			"replacew" =>	do_replace(REPLACEWITH);
+			"replaceall" =>	do_replaceall();
+			"limbo" =>	do_limbo();
+			"indent" =>	do_indent();
+			"reindent" =>	re_indent();
+			}
+			cmd(ed, "focus .b.t");
+		}
+		cmd(ed, "update");
+		e = cmd(ed, "variable lasterror");
+		if(e != "") {
+			sys->print("edit error: %s\n", e);
+			break cmdloop;
+		}
+	}
+}
+
+check_dirty() : int
+{
+	if ( dirty == 0 )
+		return 1;
+	if (dialog->prompt(context, ed.image, ErrIco, "Confirm",
+					"File was changed.\nDiscard changes?",
+					0, "Yes" :: "No" :: nil) == 0 ) {
+		return 1;
+	}
+	return 0;
+}
+
+set_dirty()
+{
+	if(!dirty){
+		dirty = 1;
+		tkclient->settitle(ed, "Edit " + curfile + " (dirty)");
+		cmd(ed, "update");
+	}
+#	We want to just remove the binding, but Inferno's tk does not
+#	recognize the - in front of the command.  To make it do so would
+#	require changes to utils.c and ebind.c in /tk
+#	cmd(ed, "bind .b.t <Key> -{send c dirtied}");
+}
+
+set_clean()
+{
+	if(dirty){
+		dirty = 0;
+		tkclient->settitle(ed, "Edit " + curfile);
+		cmd(ed, "update");
+		#cmd(ed, "bind .b.t <Key> +{send c dirtied}");
+	}
+}
+
+BLOCK, TEMP : con iota;
+is_limbo	:= 0;		# initially not limbo
+this_word := "";
+last_keyword := "";
+in_comment	:= 0;
+first_char	:= 1;
+indent		: list of int;
+last_kw_is_block := 0;
+tab		:= "\t";
+tabs		:= array[] of {
+	"", "\t", "\t\t", "\t\t\t", "\t\t\t\t", "\t\t\t\t\t",
+	"\t\t\t\t\t\t", "\t\t\t\t\t\t\t", "\t\t\t\t\t\t\t\t"
+};
+
+keywords := array[] of {
+	"adt", "alt", "array", "big", "break",
+	"byte", "case", "chan", "con", "continue",
+	"cyclic", "do", "else", "exit", "fn",
+	"for", "hd", "if", "implement", "import",
+	"include", "int", "len", "list", "load",
+	"module", "nil", "of", "or", "pick",
+	"real", "ref", "return", "self", "spawn",
+	"string", "tagof", "tl", "to", "type",
+	"while"
+};
+block_keyword := (big 1 << 40 ) | big (1 << 17) | big (1 << 15) |
+					big (1 << 12) | big (1 << 11);
+
+do_limbo()
+{
+	is_limbo = !is_limbo;
+	if ( is_limbo )
+		mark_keyw_comm();
+	else {
+		cmd(ed, ".b.t tag remove comment 1.0 end");
+		cmd(ed, ".b.t tag remove keyword 1.0 end");
+	}
+}
+
+do_limbo_check(s : string)
+{
+	if ( ! is_limbo )
+		return;
+	if ( len s < 11 )
+		return;
+#
+#   Maybe we should actually remember where the insert point is.
+#   In general we can get it via .b.t index insert, but for most
+#   characters, we could maintain the position with simple arithmetic.
+#
+#   Also, we need to insert code in cut and paste operations to keep
+#   track of various things when in limbo mode.  Also need to catch
+#   text deletions via typeover of selection.
+#
+	char := s[9];
+	if ( char == '\\' && len s > 10 )
+		char = s[10];
+	case char {
+	    ' ' or '\t' =>
+		if ( ! in_comment )
+			look_keyword(this_word);
+		this_word = "" ;
+	    '\n' =>
+		if ( in_comment ) {
+			# terminate current tag
+			cmd(ed, ".b.t tag remove comment insert-1chars");
+			in_comment = 0;
+		}
+		else
+			look_keyword(this_word);
+		this_word = "" ;
+		if ( last_kw_is_block )
+			indent = TEMP :: indent;
+		else while ( indent != nil && hd indent == TEMP )
+			indent = tl indent;
+		last_kw_is_block = 0;
+		add_indent();
+		first_char = 1;
+		return;
+	    '{' =>
+		indent = BLOCK :: indent;
+		last_kw_is_block = 0;
+	    '}' =>
+		if ( indent != nil )
+			indent = tl indent;
+		last_kw_is_block = 0;
+	# If the line is just indentation plus '}', rewrite it
+	# to have one less indent.
+		if ( first_char ) {
+			current := int cmd(ed, ".b.t index insert");
+			cmd(ed, ".b.t delete " +
+						string current + ".0 insert");
+			add_indent();
+			cmd(ed, ".b.t insert insert '}");
+		}
+#	    ';' =>
+#		last_kw_is_block = 0;
+#	    '\b' =>	# By the time we see this, the character has
+#			# already been wiped out, probably.
+#			# To know what it was we'd need a lastchar,
+#			# reset for each mouse button up and \b
+#	    '\u007f' =>	# Here, we have to know what used to be ahead of the
+#			# insert point.
+	    '#' =>
+		# if ( ! in_quote ) {
+		#	cmd(ed, ".b.t tag add comment insert-1chars");
+			in_comment = 1;
+		# }
+	    'A' to 'Z' or 'a' to 'z' or '0' to '9' or '_' =>
+		if ( ! in_comment )
+			this_word[len this_word] = char;
+	    * =>
+		if ( ! in_comment )
+			look_keyword(this_word);
+		this_word = "";
+	}
+	if ( in_comment )
+		cmd(ed, ".b.t tag add comment insert-1chars");
+	first_char = 0;
+}
+
+look_keyword(word : string)
+{
+	# compare this_word to all keywords
+	if ( is_keyword(word) ) {
+		cmd(ed, ".b.t tag add keyword insert-" +
+			string (len this_word + 1) + "chars insert-1chars");
+	}
+}
+
+is_keyword(word : string) : int
+{
+	l := len keywords;
+	for ( i := 0; i < l; i++ )
+		if ( word == keywords[i] ) {
+			if ( i != 26 )	# don't set for 'nil'
+				last_kw_is_block = int (block_keyword >> i) & 1;
+			return 1;
+		}
+	return 0;
+}
+
+do_new()
+{
+	cmd(ed, ".b.t delete 1.0 end");
+	curfile = "(New)";
+	tkclient->settitle(ed, "Edit " + curfile);
+}
+
+do_open(): int
+{
+	for(;;) {
+		fname := selectfile->filename(context, ed.image, "", nil, path);
+		if(fname == "")
+			break;
+		cmd(ed, ".b.t delete 1.0 end");
+		e := loadtfile(fname);
+		if(e == nil) {
+			basepath(fname);
+			return 1;
+		}
+
+		options := list of {
+			"Cancel",
+			"Open another file"
+		};
+
+		if(dialog->prompt(context, ed.image, ErrIco, "Open file", e, 0,  options) == 0)
+			break;
+	}
+	return 0;
+}
+
+basepath(file: string)
+{
+	for(i := len file-1; i >= 0; i--)
+		if(file[i] == '/') {
+			path = file[0:i];
+			break;
+		}
+}
+
+do_save(prompt: int)
+{
+	fname := curfile;
+
+	contents := tk->cmd(ed, ".b.t get 1.0 end");
+	for(;;) {
+		if(prompt || curfile == "(New)") {
+			fname = dialog->getstring(context, ed.image, "File");
+			if ( len fname > 0 && fname[0] != '/' && path != "" )
+				fname = path + "/" + fname;
+		}
+
+		if(savetfile(fname, contents)) {
+			set_clean();
+			break;
+		}
+
+		options := list of {
+			"Cancel",
+			"Try another file"
+		};
+
+		msg := sys->sprint("Trying to write file \"%s\"\n%r", fname);
+		if(dialog->prompt(context, ed.image, ErrIco, "Save file", msg, 0, options) == 0)
+			break;
+
+		prompt = 1;
+	}
+}
+
+do_snarf(del: int)
+{
+	range := cmd(ed, ".b.t tag nextrange sel 1.0");
+	if(range == "" || (len range > 0 && range[0] == '!'))
+		return;
+	snarf = tk->cmd(ed, ".b.t get " + range);
+	if(del)
+		cmd(ed, ".b.t delete " + range);
+	tkclient->snarfput(snarf);
+}
+
+do_paste()
+{
+	snarf = tkclient->snarfget();
+	if(snarf == "")
+		return;
+	cmd(ed, ".b.t insert insert '" + snarf);
+}
+
+do_search(prompt: int) : int
+{
+	if(prompt == SEARCHFOR)
+		searchfor = dialog->getstring(context, ed.image, "Search For");
+	if(searchfor == "")
+		return 0;
+	cmd(ed, "cursor -bitmap cursor.wait");
+	ix := cmd(ed, ".b.t search -- " + tk->quote(searchfor) + " insert+1c");
+	if(ix != "" && len ix > 1 && ix[0] != '!') {
+		cmd(ed, ".b.t tag remove sel 0.0 end");
+		cmd(ed, ".b.t mark set anchor " + ix);
+		cmd(ed, ".b.t mark set insert " + ix);
+		cmd(ed, ".b.t tag add sel " + ix + " " + ix + "+" +
+						string(len searchfor) + "c");
+		if ( prompt != NOSEE )
+			cmd(ed, ".b.t see " + ix);
+		cmd(ed, "cursor -default");
+		return 1;
+	}
+	cmd(ed, "cursor -default");
+	return 0;
+}
+
+do_replace(prompt : int)
+{
+	range := "";
+	if ( prompt == REPLACEWITH ) {
+		replacewith = dialog->getstring(context, ed.image, "Replacement String");
+
+		range = cmd(ed, ".b.t tag nextrange sel 1.0");
+		if(range == "" || (len range > 0 && range[0] == '!'))
+			return;			# nothing currently selected
+	}
+	if ( range != "" ) {		# there's something selected
+		cmd(ed, ".b.t mark set insert sel.first");
+	}
+	else {				# have to find a string
+		if ( searchfor == "" ) {	# no search string!
+			if ( do_search(SEARCHFOR) == 0 )
+				return;
+		}
+		else if ( do_search(SEARCH) == 0 )
+			return;
+	}
+	cmd(ed, ".b.t delete sel.first sel.last");
+	cmd(ed, ".b.t insert insert " + tk->quote(replacewith));
+}
+
+do_replaceall()
+{
+	cur := cmd(ed, ".b.t index insert");
+	if ( cur == "" || cur[0] == '!' )
+		return;
+	dirt := 0;
+	if ( searchfor == "" )		# no search string
+		searchfor = dialog->getstring(context, ed.image, "Search For");
+	if ( searchfor == "" )		# still no search string
+		return;
+	srch := tk->quote(searchfor);
+	repl := tk->quote(replacewith);
+	for ( ix := "1.0"; len ix > 0 && ix[0] != '!'; ) {
+		ix = cmd(ed, ".b.t search -- " + srch + " " + ix + " end");
+		if ( ix == "" || len ix <= 1 || ix[0] == '!')
+			break;
+		cmd(ed, ".b.t delete " + ix + " " + ix + "+" +
+						string(len searchfor) + "c");
+		if ( replacewith != "" ) {
+			cmd(ed, ".b.t insert " + ix + " " + repl);
+			ix = cmd(ed, ".b.t index " + ix + "+" +
+					string(len replacewith) + "c");
+		}
+		dirt++;
+	}
+	cmd(ed, ".b.t mark set insert " + cur);
+	if ( dirt > 0 )
+		set_dirty();
+}
+	
+
+loadtfile(path: string): string
+{
+	if ( path != nil && path[0] == '/' )
+		basepath(path);
+	fd := sys->open(path, sys->OREAD);
+	if(fd == nil)
+		return "Can't open "+path+", the error was:\n"+sys->sprint("%r");
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0)
+		return "Can't stat "+path+", the error was:\n"+sys->sprint("%r");
+	if(d.mode & Sys->DMDIR)
+		return path+" is a directory";
+
+	cmd(ed, "cursor -bitmap cursor.wait");
+	BLEN: con 8192;
+	buf := array[BLEN+Sys->UTFmax] of byte;
+	inset := 0;
+	for(;;) {
+		n := sys->read(fd, buf[inset:], BLEN);
+		if(n <= 0)
+			break;
+		n += inset;
+		nutf := sys->utfbytes(buf, n);
+		s := string buf[0:nutf];
+		# move any partial rune to beginning of buffer
+		inset = n-nutf;
+		buf[0:] = buf[nutf:n];
+		cmd(ed, ".b.t insert end '" + s);
+	}
+	if ( is_limbo )
+		mark_keyw_comm();
+	curfile = path;
+	tkclient->settitle(ed, "Edit " + curfile);
+	cmd(ed, "cursor -default");
+	cmd(ed, "update");
+	return "";
+}
+
+savetfile(path: string, contents: string): int
+{
+	buf := array of byte contents;
+	n := len buf;
+
+	fd := sys->create(path, sys->OWRITE, 8r664);
+	if(fd == nil)
+		return 0;
+	i := sys->write(fd, buf, n);
+	if(i != n) {
+		sys->print("savetfile only wrote %d of %d: %r\n", i, n);
+		return 0;
+	}
+	curfile = path;
+#	cmd(ed, ".m.filename configure -text '" + curfile);
+	tkclient->settitle(ed, "Edit " + curfile);
+
+	return 1;
+}
+
+mark_keyw_comm()
+{
+	quote := 0;
+	start : int;
+	notkey := 0;
+	word : string;
+
+	last := int cmd(ed, ".b.t index end");
+	for ( i := 1; i <= last; i++ ) {
+		quote = 0;
+		word = "";
+		line := tk->cmd(ed, ".b.t get " + string i + ".0 " +
+						string (i+1) + ".0");
+		l := len line;
+ll :		for ( j := 0; j < l; j++ ) {
+			c := line[j];
+			if ( quote && (c = line[j]) != quote )
+				continue;
+			case c {
+			    '#' =>
+				cmd(ed, sys->sprint(".b.t tag add comment" +
+					" %d.%d %d.%d", i, j, i, l));
+				break ll;
+			    '\'' or '\"' =>
+				if ( j != 0 && line[j-1] == '\\' )
+					break;
+				if ( c == quote )
+					quote = 0;
+				else
+					quote = line[j];
+				word = "";
+			    'a' to 'z' =>
+				if ( word == "" )
+					start = j;
+				word[len word] = c;
+			    'A' to 'Z' or '_' =>
+				notkey = 1;
+				continue;
+			    * =>
+				if ( ! notkey && is_keyword(word) )
+					cmd(ed, ".b.t tag add keyword " +
+						sys->sprint("%d.%d %d.%d",
+							i, start, i, j));
+				word = "";
+				notkey = 0;
+			}
+		}
+	}
+}
+
+do_indent()
+{
+	for ( ; ; ) {
+		tab = dialog->getstring(context, ed.image, "single indent");
+		break;
+	}
+	for ( i := 1; i <= 8; i++ ) {
+		s := "";
+		for ( j := i; j > 0; j-- )
+			s += tab;
+		tabs[i] = collapse(s);
+	}	
+}
+
+collapse(s : string) : string
+{
+	if ( len s >= 8 && s[0:8] == "        " )
+		return "\t" + collapse(s[8:]);
+	return s;
+}
+
+add_indent()
+{
+	for ( i := len indent; i >= 8; i -= 8 )
+		cmd(ed, ".b.t insert insert '" + tabs[8]);
+	cmd(ed, ".b.t insert insert '" + tabs[i]);
+}
+#
+#	We should also look at the previous line, maybe.
+#	And the line after.  That may be too much.
+#
+#	This is also the logical place to check if we are in a keyword,
+#	reinitialize this_word (which presents problems if we are in the
+#	middle of a word, etc.)  Also check if we are in a comment or not.  
+#
+re_indent()
+{
+	pos := cmd(ed, ".b.t index insert");
+	(n, lc) := sys->tokenize(pos, ".");
+	if ( n < 2 )
+		return;
+	init := tk->cmd(ed, ".b.t get " + hd lc + ".0 insert");
+	l := len init;
+	for ( i := 8; i > 0; i-- ) {
+		lt := len tabs[i];
+		if ( l >= lt && init[:lt] == tabs[i] )
+			break;
+	}
+	for ( indent = nil; len indent < i; indent = 0 :: indent) ;
+	
+	in_comment = 0;		# Are we in a comment?
+	for ( i = len tabs[i]; i < l; i++ )
+		if ( init[i] == '#' ) {
+			in_comment = 1;
+			break;
+		}
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+#	sys->print("%s\n", s);
+	r := tk->cmd(win, s);
+	if (r != nil && r[0] == '!') {
+		sys->print("wm/edit: error executing '%s': %s\n", s, r);
+	}
+	return r;
+}
--- /dev/null
+++ b/appl/wm/filename.b
@@ -1,0 +1,74 @@
+implement Filename;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+	draw: Draw;
+	Rect: import draw;
+include "tk.m";
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include "arg.m";
+
+Filename: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+usage()
+{
+	sys->fprint(stderr, "usage: filename [-g geom] [-d startdir] [pattern...]\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+	if (selectfile == nil) {
+		sys->fprint(stderr, "selectfile: cannot load %s: %r\n", Selectfile->PATH);
+		raise "fail:bad module";
+	}
+	arg := load Arg Arg->PATH;
+	if (arg == nil) {
+		sys->fprint(stderr, "filename: cannot load %s: %r\n", Arg->PATH);
+		raise "fail:bad module";
+	}
+
+	if (ctxt == nil) {
+		sys->fprint(stderr, "filename: no window context\n");
+		raise "fail:bad context";
+	}
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	selectfile->init();
+
+	startdir := ".";
+#	geom := "-x " + string (ctxt.screen.image.r.dx() / 5) +
+#			" -y " + string (ctxt.screen.image.r.dy() / 5);
+	title := "Select a file";
+	arg->init(argv);
+	while (opt := arg->opt()) {
+		case opt {
+#		'g' =>
+#			geom = arg->arg();
+		'd' =>
+			startdir = arg->arg();
+		't' =>
+			title = arg->arg();
+		* =>
+			sys->fprint(stderr, "filename: unknown option -%c\n", opt);
+			usage();
+		}
+	}
+	if (startdir == nil || title == nil)
+		usage();
+#	top := tk->toplevel(ctxt.screen, geom);
+	argv = arg->argv();
+	arg = nil;
+	sys->print("%s\n", selectfile->filename(ctxt, nil, title, argv, startdir));
+}
--- /dev/null
+++ b/appl/wm/ftree/cptree.b
@@ -1,0 +1,136 @@
+implement Cptree;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "readdir.m";
+	readdir: Readdir;
+include "cptree.m";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	readdir = load Readdir Readdir->PATH;
+}
+
+Context: adt {
+	progressch: chan of string;
+	warningch: chan of (string, chan of int);
+	finishedch: chan of string;
+};
+
+# recursively copy file/directory f into directory d;
+# the name remains the same.
+copyproc(f, d: string, progressch: chan of string,
+		warningch: chan of (string, chan of int),
+		finishedch: chan of string)
+{
+	ctxt := ref Context(progressch, warningch, finishedch);
+	(fok, fstat) := sys->stat(f);
+	if (fok == -1)
+		error(ctxt, sys->sprint("cannot stat '%s': %r", f));
+	(dok, dstat) := sys->stat(d);
+	if (dok == -1)
+		error(ctxt, sys->sprint("cannot stat '%s': %r", d));
+	if ((dstat.mode & Sys->DMDIR) == 0)
+		error(ctxt, sys->sprint("'%s' is not a directory", d));
+	if (fstat.qid.path == dstat.qid.path)
+		error(ctxt, sys->sprint("'%s' and '%s' are identical", f, d));
+
+	c := d + "/" + fname(f);
+	(cok, cstat) := sys->stat(c);
+	if (cok == 0)
+		error(ctxt, sys->sprint("'%s' already exists", c));
+	rcopy(ctxt, f, ref fstat, c);
+	finishedch <-= nil;
+}
+
+rcopy(ctxt: ref Context, src: string, srcstat: ref Sys->Dir, dst: string)
+{
+	omode := Sys->OWRITE;
+	perm := srcstat.mode;
+	if (perm & Sys->DMDIR) {
+		omode = Sys->OREAD;
+		perm |= 8r300;
+	}
+
+	dstfd := sys->create(dst, omode, perm);
+	if (dstfd == nil) {
+		warning(ctxt, sys->sprint("cannot create '%s': %r", dst));
+		return;
+	}
+	if (srcstat.mode & Sys->DMDIR) {
+		(entries, n) := readdir->init(src, Readdir->NAME | Readdir->COMPACT);
+		if (n == -1)
+			warning(ctxt, sys->sprint("cannot read dir '%s': %r", src));
+		for (i := 0; i < len entries; i++) {
+			e := entries[i];
+			rcopy(ctxt, src + "/" + e.name, e, dst + "/" + e.name);
+		}
+		if (perm != srcstat.mode) {
+			(ok, nil) := sys->fstat(dstfd);
+			if (ok != -1) {
+				dststat := sys->nulldir;
+				dststat.mode = srcstat.mode;
+				sys->fwstat(dstfd, dststat);
+			}
+		}
+	} else {
+		srcfd := sys->open(src, Sys->OREAD);
+		if (srcfd == nil) {
+			sys->remove(dst);
+			warning(ctxt, sys->sprint("cannot open '%s': %r", src));
+			return;
+		}
+		ctxt.progressch <-= "copying " + src;
+		buf := array[Sys->ATOMICIO] of byte;
+		while ((n := sys->read(srcfd, buf, len buf)) > 0) {
+			if (sys->write(dstfd, buf, n) != n) {
+				sys->remove(dst);
+				warning(ctxt, sys->sprint("error writing '%s': %r", dst));
+				return;
+			}
+		}
+		if (n == -1) {
+			sys->remove(dst);
+			warning(ctxt, sys->sprint("error reading '%s': %r", src));
+			return;
+		}
+	}
+}
+
+warning(ctxt: ref Context, msg: string)
+{
+	r := chan of int;
+	ctxt.warningch <-= (msg, r);
+	if (!<-r)
+		exit;
+}
+
+error(ctxt: ref Context, msg: string)
+{
+	ctxt.finishedch <-= msg;
+	exit;
+}
+
+fname(f: string): string
+{
+	f = cleanname(f);
+	for (i := len f - 1; i >= 0; i--)
+		if (f[i] == '/')
+			break;
+	return f[i+1:];
+}
+
+cleanname(s: string): string
+{
+	t := "";
+	i := 0;
+	while (i < len s)
+		if ((t[len t] = s[i++]) == '/')
+			while (i < len s && s[i] == '/')
+				i++;
+	if (len t > 1 && t[len t - 1] == '/')
+		t = t[0:len t - 1];
+	return t;
+}
--- /dev/null
+++ b/appl/wm/ftree/cptree.m
@@ -1,0 +1,8 @@
+Cptree: module {
+	PATH: con "/dis/lib/ftree/cptree.dis";
+	init: fn();
+	copyproc: fn(f, d: string, progressch: chan of string,
+		warningch: chan of (string, chan of int),
+		finishedch: chan of string);
+};
+
--- /dev/null
+++ b/appl/wm/ftree/ftree.b
@@ -1,0 +1,873 @@
+implement Ftree;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "readdir.m";
+	readdir: Readdir;
+include "items.m";
+	items: Items;
+	Item, Expander: import items;
+include "plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg: import plumbmsg;
+include "sh.m";
+	sh: Sh;
+include "popup.m";
+	popup: Popup;
+include "cptree.m";
+	cptree: Cptree;
+include "string.m";
+	str: String;
+include "arg.m";
+	arg: Arg;
+
+stderr: ref Sys->FD;
+
+Ftree: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Tree: adt {
+	fname: string;
+	pick {
+	L =>
+	N =>
+		e: ref Expander;
+		sub: cyclic array of ref Tree;
+	}
+};
+
+tkcmds := array[] of {
+	"frame .top",
+	"label .top.l -text |",
+	"pack .top.l -side left -expand 1 -fill x",
+	"frame .f",
+	"canvas .c -yscrollcommand {.f.s set}",
+	"scrollbar .f.s -command {.c yview}",
+	"pack .f.s -side left -fill y",
+	"pack .c -side top -in .f -fill both -expand 1",
+	"pack .top -anchor w",
+	"pack .f -fill both -expand 1",
+	"pack propagate . 0",
+	".top.l configure -text {}",
+};
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "ftree: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+tkwin: ref Tk->Toplevel;
+root := "/";
+
+cpfile := "";
+
+usage()
+{
+	sys->fprint(stderr, "usage: ftree [-e] [-E] [-p] [-d] [root]\n");
+	raise "fail:usage";
+}
+
+plumbinprogress := 0;
+disallow := 1;
+plumbed: chan of int;
+roottree: ref Tree.N;
+rootitem: Item;
+runplumb := 1;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	loadmods();
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "ftree: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	noexit := 0;
+	winopts := Tkclient->Resize | Tkclient->Hide;
+	arg->init(argv);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'e' =>
+			(noexit, winopts) = (1, Tkclient->Resize);
+		'E' =>
+			(noexit, winopts) = (1, 0);
+		'p' =>
+			(noexit, winopts) = (0, 0);
+		'd' =>
+			disallow = 0;
+		'P' =>
+			runplumb = 1;
+		* =>
+			usage();
+		}
+	}
+	argv = arg->argv();
+	if (argv != nil && tl argv != nil)
+		usage();
+	if (argv != nil) {
+		root = hd argv;
+		(ok, s) := sys->stat(root);
+		if (ok == -1) {
+			sys->fprint(stderr, "ftree: %s: %r\n", root);
+			raise "fail:bad root";
+		} else if ((s.mode & Sys->DMDIR) == 0) {
+			sys->fprint(stderr, "ftree: %s is not a directory\n", root);
+			raise "fail:bad root";
+		}
+	}
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+
+	(win, wmctl) := tkclient->toplevel(ctxt, nil, "Ftree", winopts);
+	tkwin = win;
+	for (i := 0; i < len tkcmds; i++)
+		cmd(win, tkcmds[i]);
+	fittoscreen(win);
+	cmd(win, "update");
+
+	event := chan of string;
+	tk->namechan(win, event, "event");
+
+	clickfile := chan of string;
+	tk->namechan(win, clickfile, "clickfile");
+
+	sys->bind("#s", "/chan", Sys->MBEFORE);
+	fio := sys->file2chan("/chan", "plumbstart");
+	if (fio == nil) {
+		sys->fprint(stderr, "ftree: cannot make /chan/plumbstart: %r\n");
+		raise "fail:error";
+	}
+	nsfio := sys->file2chan("/chan", "nsupdate");
+	if (nsfio == nil)  {
+		sys->fprint(stderr, "ftree: cannot make /chan/nsupdate: %r\n");
+		raise "fail:error";
+	}
+
+	if (runplumb){
+		if((err := sh->run(ctxt, "plumber" :: "-n" :: "-w" :: "-c/chan/plumbstart" :: nil)) != nil)
+			sys->fprint(stderr, "ftree: can't start plumber: %s\n", err);
+	}
+
+	plumbmsg = load Plumbmsg Plumbmsg->PATH;
+	if (plumbmsg != nil && plumbmsg->init(1, nil, 0) == -1) {
+		sys->fprint(stderr, "ftree: no plumber\n");
+		plumbmsg = nil;
+	}
+
+	nschanged := chan of string;
+	roottree = ref Tree.N("/", Expander.new(win, ".c"), nil);
+	rootitem = roottree.e.make(items->maketext(win, ".c", "/", "/"));
+	cmd(win, ".c configure -width " + string rootitem.r.dx() + " -height " + string rootitem.r.dy() +
+		" -scrollregion {" + r2s(rootitem.r) + "}");
+	sendevent("/", "expand");
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "ptr"::nil);
+	cmd(win, "update");
+
+	plumbed = chan of int;
+	for (;;) alt {
+	key := <-win.ctxt.kbd =>
+		tk->keyboard(win, key);
+	m := <-win.ctxt.ptr =>
+		tk->pointer(win, *m);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-wmctl =>
+		if (noexit && s == "exit")
+			s = "task";
+		tkclient->wmctl(win, s);
+	s := <-event =>
+		(target, ev) := eventtarget(s);
+		sendevent(target, ev);
+	m := <-clickfile =>
+		(n, toks) := sys->tokenize(m, " ");
+		(b, s) := (hd toks, hd tl toks);
+		if (b == "menu") {
+			c := chan of (ref Tree, Item, chan of Item);
+			nsu := chan of string;
+			spawn menuproc(c, nsu);
+			found := operate(s, c);
+			if (found) {
+				if ((upd := <-nsu) != nil)
+					updatens(upd);
+			}
+		} else if (b == "plumb")
+			plumbit(s);
+	ok := <-plumbed =>
+		colour := "#00ff00";
+		if (!ok)
+			colour = "red";
+		cmd(tkwin, ".c itemconfigure highlight -fill " + colour);
+		cmd(tkwin, "update");
+		plumbinprogress = 0;
+	s := <-nschanged =>
+		sys->print("got nschanged: %s\n", s);
+		updatens(s);
+	(nil, nil, nil, rc) := <-nsfio.read =>
+		if (rc != nil)
+			readreply(rc, nil, "permission denied");
+	(nil, data, nil, wc) := <-nsfio.write =>
+		if (wc == nil)
+			break;
+		s := cleanname(string data);
+		if (len s >= len root && s[0:len root] == root) {
+			s = s[len root:];
+			if (s == nil)
+				s = "/";
+			if (s[0] == '/')
+				updatens(s);
+		}
+		writereply(wc, len data, nil);
+	(nil, nil, nil, rc) := <-fio.read =>
+		if (rc != nil)
+			readreply(rc, nil, "permission denied");
+	(nil, data, nil, wc) := <-fio.write =>
+		if (wc == nil)
+			break;
+		s := string data;
+		if (len s == 0 || s[0] != 's')
+			writereply(wc, 0, "invalid write");
+		cmd := str->unquoted(s);
+		if (cmd == nil || tl cmd == nil || tl tl cmd == nil) {
+			writereply(wc, 0, "invalid write");
+		} else {
+			if (hd tl tl cmd == "+ftree")
+				runsubftree(ctxt, tl tl tl cmd);
+			else
+				sh->run(ctxt, "{$* &}" :: tl tl cmd);
+			writereply(wc, len data, nil);
+		}
+	}
+}
+
+runsubftree(ctxt: ref Draw->Context, c: list of string)
+{
+	if (len c < 2) {
+		return;
+	}
+	cmd(tkwin, ". unmap");
+	sh->run(ctxt, c);
+	cmd(tkwin, ". map");
+}
+
+sendevent(target, ev: string)
+{
+	c := chan of (ref Tree, Item, chan of Item);
+	spawn sendeventproc(ev, c);
+	operate(target, c);
+	cmd(tkwin, "update");
+}
+
+# non-blocking reply to read request, in case client has gone away.
+readreply(reply: Sys->Rread, data: array of byte, err: string)
+{
+	alt {
+	reply <-= (data, err) =>;
+	* =>;
+	}
+}
+
+# non-blocking reply to write request, in case client has gone away.
+writereply(reply: Sys->Rwrite, count: int, err: string)
+{
+	alt {
+	reply <-= (count, err) =>;
+	* =>;
+	}
+}
+
+plumbit(f: string)
+{
+	if (!plumbinprogress) {
+		highlight(f, "yellow", 2000);
+		spawn plumbproc(root + f, plumbed);
+		plumbinprogress = 1;
+	}
+}
+
+plumbproc(f: string, plumbed: chan of int)
+{
+	if (plumbmsg == nil || (ref Msg("browser", nil, nil, "text", nil, array of byte f)).send() == -1) {
+		sys->fprint(stderr, "ftree: cannot plumb %s\n", f);
+		plumbed <-= 0;
+	} else
+		plumbed <-= 1;
+}
+
+loadmods()
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmodule(Tkclient->PATH);
+	tkclient->init();
+
+	readdir = load Readdir Readdir->PATH;
+	if (readdir == nil)
+		badmodule(Readdir->PATH);
+
+	str = load String String->PATH;
+	if (str == nil)
+		badmodule(String->PATH);
+
+	items = load Items Items->PATH;
+	if (items == nil)
+		badmodule(Items->PATH);
+	items->init();
+
+	sh = load Sh Sh->PATH;
+	if (sh == nil)
+		badmodule(Sh->PATH);
+
+	popup = load Popup Popup->PATH;
+	if (popup == nil)
+		badmodule(Popup->PATH);
+	popup->init();
+
+	cptree = load Cptree Cptree->PATH;
+	if (cptree == nil)
+		badmodule(Cptree->PATH);
+	cptree->init();
+
+	arg = load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+}
+
+updatens(s: string)
+{
+	sys->print("updatens(%s)\n", s);
+	(target, ev) := eventtarget(s);
+	spawn rereadproc(c := chan of (ref Tree, Item, chan of Item));
+	operate(target, c);
+	cmd(tkwin, "update");
+}
+
+nsupdatereaderproc(fd: ref Sys->FD, path: string, nschanged: chan of string)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	while ((n := sys->read(fd, buf, len buf)) > 0) {
+		s := string buf[0:n];
+		nschanged <-= path + string buf[0:n-1];
+	}
+	sys->print("nsupdate gave eof: (%r)\n");
+}
+
+sendeventproc(ev: string, c: chan of (ref Tree, Item, chan of Item))
+{
+	(tree, it, replyc) := <-c;
+	if (replyc == nil)
+		return;
+	pick t := tree {
+	N =>
+		if (ev == "expand")
+			expand(t, it);
+		else if (ev == "contract")
+			t.sub = nil;
+		it = t.e.event(it, ev);
+	}
+	replyc <-= it;
+}
+
+Open, Copy, Paste, Remove: con iota;
+
+menu := array[] of {
+Open => "Open",
+Copy => "Copy",
+Paste => "Paste into",
+Remove => "Remove",
+};
+
+screenx(cvs: string, x: int): int
+{
+	return x - int cmd(tkwin, cvs + " canvasx 0");
+}
+
+screeny(cvs: string, y: int): int
+{
+	return y - int cmd(tkwin, cvs + " canvasy 0");
+}
+
+menuproc(c: chan of (ref Tree, Item, chan of Item), nsu: chan of string)
+{
+	(tree, it, replyc) := <-c;
+	if (replyc == nil)
+		return;
+
+	p := Point(screenx(".c", it.r.min.x), screeny(".c", it.r.min.y));
+	m := array[len menu] of string;
+	for (i := 0; i < len m; i++)
+		m[i] = menu[i] + " " + tree.fname;
+	n := post(tkwin, p, m, 0);
+	upd: string;
+	if (n >= 0) {
+		case n {
+		Copy =>
+			cpfile = it.name;
+		Paste =>
+			if (cpfile == nil)
+				notice("no file in snarf buffer");
+			else {
+				cp(cpfile, it.name);
+				upd = it.name;
+			}
+		Remove =>
+			if ((e := rm(it.name)) != nil)
+				notice(e);
+			upd = parent(it.name);
+		Open =>
+			plumbit(it.name);
+		}
+	}
+
+#	id := cmd(tkwin, ".c create rectangle " + r2s(it.r) + " -fill yellow");
+	replyc <-= it;
+	nsu <-= upd;
+}
+
+post(win: ref Tk->Toplevel, p: Point, a: array of string, n: int): int
+{
+	rc := popup->post(win, p, a, n);
+	for(;;)alt{
+	r := <-rc =>
+		return r;
+	key := <-win.ctxt.kbd =>
+		tk->keyboard(win, key);
+	m := <-win.ctxt.ptr =>
+		tk->pointer(win, *m);
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq =>
+		tkclient->wmctl(win, s);
+	}
+}
+
+highlight(f: string, colour: string, time: int)
+{
+	spawn highlightproc(c := chan of (ref Tree, Item, chan of Item), colour, time);
+	operate(f, c);
+	tk->cmd(tkwin, "update");
+}
+
+unhighlight()
+{
+	cmd(tkwin, ".c delete highlight");
+	tk->cmd(tkwin, "update");
+}
+
+hpid := -1;
+highlightproc(c: chan of (ref Tree, Item, chan of Item), colour: string, time: int)
+{
+	(tree, it, replyc) := <-c;
+	if (replyc == nil)
+		return;
+	r: Rect;
+	pick t  := tree {
+	N =>
+		r = t.e.titleitem.r.addpt(it.r.min);
+	L =>
+		r = it.r;
+	}
+	id := cmd(tkwin, ".c create rectangle " + r2s(r) + " -fill " + colour + " -tags highlight");
+	cmd(tkwin, ".c lower " + id);
+	kill(hpid);
+	sync := chan of int;
+	spawn highlightsleepproc(sync, time);
+	hpid = <-sync;
+	replyc <-= it;
+}
+
+highlightsleepproc(sync: chan of int, time: int)
+{
+	sync <-= sys->pctl(0, nil);
+	sys->sleep(time);
+	cmd(tkwin, ".c delete highlight");
+	cmd(tkwin, "update");
+}
+
+operate(towhom: string, c: chan of (ref Tree, Item, chan of Item)): int
+{
+	towhom = cleanname(towhom);
+	(ok, it) := operate1(roottree, rootitem, towhom, towhom, c);
+	if (!it.eq(rootitem)) {
+		cmd(tkwin, ".c configure -width " + string it.r.dx() + " -height " + string it.r.dy() +
+			" -scrollregion {" + r2s(it.r) + "}");
+		rootitem = it;
+	}
+	if (!ok)
+		c <-= (nil, it, nil);
+	return ok;
+}
+
+blankitem: Item;
+operate1(tree: ref Tree, it: Item, towhom, below: string,
+		c: chan of (ref Tree, Item, chan of Item)): (int, Item)	
+{
+#	sys->print("operate on %s, towhom: %s, below: %s\n", it.name, towhom, below);
+	n: ref Tree.N;
+	replyc := chan of Item;
+	if (it.name != towhom) {
+		pick t := tree {
+		L =>
+			return (0, it);
+		N =>
+			n = t;
+		}
+		below = dropelem(below);
+		if (below == nil)
+			return (0, it);
+		path := pathcat(it.name, below);
+		if (len n.e.children != len n.sub) {
+			sys->fprint(stderr, "inconsistent children in %s (%d vs sub %d)\n", it.name, len n.e.children, len n.sub);
+			return (0, it);
+		}
+		for (i := 0; i < len n.e.children; i++) {
+			f := n.e.children[i].name;
+#			sys->print("checking %s against child %s\n", path, f);
+			if (len path >= len f && path[0:len f] == f &&
+					(len path == len f || path[len f] == '/')) {
+				break;
+			}
+		}
+		if (i == len n.e.children)
+			return (0, it);
+		oldit := n.e.children[i].addpt(it.r.min);
+		(ok, nit) := operate1(n.sub[i], oldit, towhom, below, c);
+		if (nit.eq(oldit))
+			return (ok, it);
+#		sys->print("childchanged({%s, [%s]}, %d, {%s, [%s]})\n",
+#				it.name, r2s(it.r), i, nit.name, r2s(nit.r));
+		n.e.children[i] = nit.subpt(it.r.min);
+		return (ok, n.e.childrenchanged(it));
+	}
+	c <-= (tree, it, replyc);
+	return (1, <-replyc);
+}
+
+
+dropelem(below: string): string
+{
+	if (below[0] == '/')
+		return below[1:];
+	for (i := 1; i < len below; i++)
+		if (below[i] == '/')
+			break;
+	if (i == len below)
+		return nil;
+	return below[i+1:];
+}
+
+cleanname(s: string): string
+{
+	t := "";
+	i := 0;
+	while (i < len s)
+		if ((t[len t] = s[i++]) == '/')
+			while (i < len s && s[i] == '/')
+				i++;
+	if (len t > 1 && t[len t - 1] == '/')
+		t = t[0:len t - 1];
+	return t;
+}
+
+pathcat(s1, s2: string): string
+{
+	if (s1 == nil || s2 == nil)
+		return s1 + s2;
+	if (s1[len s1 - 1] != '/' && s2[0] != '/')
+		return s1 + "/" + s2;
+	return s1 + s2;
+}
+
+# read the directory referred to by t.
+expand(t: ref Tree.N, it: Item)
+{
+	(d, n) := readdir->init(root + it.name, Readdir->NAME|Readdir->COMPACT);
+	if (d == nil) {
+		sys->print("readdir failed: %r\n");
+		d = array[0] of ref Sys->Dir;
+	}
+	sortit(d);
+	t.sub = array[len d] of ref Tree;
+	t.e.children = array[len d] of Item;
+	for (i := 0; i < len d; i++) {
+		tagname := pathcat(it.name, d[i].name);
+		(t.sub[i], t.e.children[i]) = makenode(d[i].mode & Sys->DMDIR, d[i].name, tagname);
+		# make coords relative to parent
+		t.e.children[i] = t.e.children[i].subpt(it.r.min);
+	}
+}
+
+makenode(isdir: int, title, tagname: string): (ref Tree, Item)
+{
+	tree: ref Tree;
+	it: Item;
+	if (isdir) {
+		e := Expander.new(tkwin, ".c");
+		tree = ref Tree.N(title, e, nil);
+		it = e.make(items->maketext(tkwin, ".c", tagname, title));
+		cmd(tkwin, ".c bind " + e.titleitem.name +
+			" <Button-1> {send clickfile menu " + tagname + "}");
+	} else {
+		tree = ref Tree.L(title);
+		it = items->maketext(tkwin, ".c", tagname, title);
+		cmd(tkwin, ".c bind " + tagname +
+			" <ButtonRelease-2> {send clickfile plumb " + tagname + "}");
+		cmd(tkwin, ".c bind " + tagname +
+			" <Button-1> {send clickfile menu " + tagname + "}");
+	}
+	return (tree, it);
+}
+
+rereadproc(c: chan of (ref Tree, Item, chan of Item))
+{
+	(tree, it, replyc) := <-c;
+	if (replyc == nil)
+		return;
+	pick t := tree {
+	L =>
+		replyc <-= it;
+	N =>
+		replyc <-= reread(t, it);
+	}
+}
+
+# re-read tree & update recursively as necessary.
+# _it_ is the tree's Item, in absolute coords.
+reread(tree: ref Tree.N, it: Item): Item
+{
+	(d, n) := readdir->init(root + it.name, Readdir->NAME|Readdir->COMPACT);
+	sortit(d);
+	sys->print("re-reading %s (was %d, now %d)\n", it.name, len tree.sub, len d);
+
+	sub := tree.sub;
+	newsub := array[len d] of ref Tree;
+	newchildren := array[len d] of Item;
+	i := j := 0;
+	while (i < len sub || j < len d) {
+		cmp: int;
+		if (i >= len sub)
+			cmp = 1;
+		else if (j >= len d)
+			cmp = -1;
+		else {
+			cmp = entrycmp(sub[i].fname, tagof(sub[i]) == tagof(Tree.N),
+					d[j].name, d[j].mode & Sys->DMDIR);
+		}
+		if (cmp == 0) {
+			# entry remains the same, but maybe it's changed type.
+			if ((tagof(sub[i]) == tagof(Tree.N)) != ((d[j].mode & Sys->DMDIR) != 0)) {
+				# delete old item and make new one...
+				tagname := tree.e.children[i].name;
+				cmd(tkwin, ".c delete " + tagname);
+				(newsub[j], newchildren[j]) =
+					makenode(d[j].mode & Sys->DMDIR, d[j].name, tagname);
+				newchildren[j] = newchildren[j].subpt(it.r.min);
+			} else {
+				nit := tree.e.children[i];
+				pick t := sub[i] {
+				N =>
+					if (t.e.expanded)
+						nit = reread(t, nit.addpt(it.r.min)).subpt(it.r.min);
+				}
+				(newsub[j], newchildren[j]) = (sub[i], nit);
+			}
+			i++;
+			j++;
+		} else if (cmp > 0) {
+			# new entry, d[j]
+			tagname := pathcat(it.name, d[j].name);
+			(newsub[j], newchildren[j]) =
+				makenode(d[j].mode & Sys->DMDIR, d[j].name, tagname);
+			newchildren[j] = newchildren[j].subpt(it.r.min);
+			j++;
+		} else {
+			# entry has been deleted, sub[i]
+			cmd(tkwin, ".c delete " + tree.e.children[i].name);
+			i++;
+		}
+	}
+	(tree.sub, tree.e.children) = (newsub, newchildren);
+	return tree.e.childrenchanged(it);
+}
+
+entrycmp(s1: string, isdir1: int, s2: string, isdir2: int): int
+{
+	if (!isdir1 == !isdir2) {
+		if (s1 > s2)
+			return 1;
+		else if (s1 < s2)
+			return -1;
+		else
+			return 0;
+	} else if (isdir1)
+		return -1;
+	else
+		return 1;
+}
+
+sortit(d: array of ref Sys->Dir)
+{
+	da := array[len d] of ref Sys->Dir;
+	fa := array[len d] of ref Sys->Dir;
+	nd := nf := 0;
+	for (i := 0; i < len d; i++) {
+		if (d[i].mode & Sys->DMDIR)
+			da[nd++] = d[i];
+		else
+			fa[nf++] = d[i];
+	}
+	d[0:] = da[0:nd];
+	d[nd:] = fa[0:nf];
+}
+
+eventtarget(s: string): (string, string)
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] == ' ')
+			return (s[0:i], s[i+1:]);
+	return (s, nil);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "ftree: tk error %s on '%s'\n", e, s);
+	return e;
+}
+
+r2s(r: Rect): string
+{
+	return string r.min.x + " " + string r.min.y + " " +
+			string r.max.x + " " + string r.max.y;
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	Point: import draw;
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+	bd := int cmd(win, ". cget -bd");
+	winsize := Point(int cmd(win, ". cget -actwidth") + bd * 2, int cmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		cmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		cmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int cmd(win, ". cget -actx"), int cmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int cmd(win, ". cget -actwidth") + bd*2,
+				int cmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	cmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
+
+cp(src, dst: string)
+{
+	if(disallow){
+		notice("permission denied");
+		return;
+	}
+	progressch := chan of string;
+	warningch := chan of (string, chan of int);
+	finishedch := chan of string;
+	spawn cptree->copyproc(root + src, root + dst, progressch, warningch, finishedch);
+loop: for (;;) alt {
+	m := <-progressch =>
+		status(m);
+	(m, r) := <-warningch =>
+		notice("warning: " + m);
+		sys->sleep(1000);
+		r <-= 1;
+	m := <-finishedch =>
+		status(m);
+		break loop;
+	}
+}
+
+parent(f: string): string
+{
+	f = cleanname(f);
+	for (i := len f - 1; i >= 0; i--)
+		if (f[i] == '/')
+			break;
+	if (i > 0)
+		f = f[0:i];
+	return f;
+}
+
+notice(s: string)
+{
+	status(s);
+}
+
+status(s: string)
+{
+	cmd(tkwin, ".top.l configure -text '" + s);
+	cmd(tkwin, "update");
+}
+
+rm(name: string): string
+{
+	if(disallow)
+		return "permission denied";
+	name = root + name;
+	if(sys->remove(name) < 0) {
+		e := sys->sprint("%r");
+		(ok, d) := sys->stat(name);
+		if(ok >= 0 && (d.mode & Sys->DMDIR) != 0)
+			return rmdir(name);
+		return e;
+	}
+	return nil;
+}
+
+rmdir(name: string): string
+{
+	(d, n) := readdir->init(name, Readdir->NONE|Readdir->COMPACT);
+	for(i := 0; i < n; i++) {
+		path := name+"/"+d[i].name;
+		e: string;
+		if(d[i].mode & Sys->DMDIR)
+			e = rmdir(path);
+		else if (sys->remove(path) == -1)
+			e = sys->sprint("cannot remove %s: %r", path);
+		if (e != nil)
+			return e;
+	}
+	if (sys->remove(name) == -1)
+		return sys->sprint("cannot remove %s: %r", name);
+	return nil;
+}
+
+kill(pid: int)
+{
+	if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+		sys->write(fd, array of byte "kill", 4);
+}
--- /dev/null
+++ b/appl/wm/ftree/items.b
@@ -1,0 +1,326 @@
+implement Items;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "items.m";
+
+Taglen: con 5;
+Titletaglen: con 10;
+Spotdiam: con 10;
+Lineopts: con " -width 1 -fill gray";
+Ovalopts: con " -outline gray";
+Crossopts: con " -fill red";
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+}
+
+blankexpander: Expander;
+Expander.new(win: ref Tk->Toplevel, cvs: string): ref Expander
+{
+	e := ref blankexpander;
+	e.win = win;
+	e.cvs = cvs;
+	return e;
+}
+
+moveto(win: ref Tk->Toplevel, cvs: string, tag: string, bbox: Rect, p: Point)
+{
+	if (!bbox.min.eq(p))
+		cmd(win, cvs + " move " + tag + " " + p2s(p.sub(bbox.min)));
+}
+
+bbox(win: ref Tk->Toplevel, cvs, w: string): Rect
+{
+	return s2r(cmd(win, cvs + " bbox " + w));
+}
+
+rename(win: ref Tk->Toplevel, it: Item, newname: string): Item
+{
+	(nil, itl) := sys->tokenize(cmd(win, ".c find withtag " + it.name), " ");
+	cmd(win, ".c dtag " + it.name + " " + it.name);
+	for (; itl != nil; itl = tl itl)
+		cmd(win, ".c addtag " + newname + " withtag " + hd itl);
+	it.name = newname;
+	return it;
+}
+
+Expander.make(e: self ref Expander, titleitem: Item): Item
+{
+	name := titleitem.name;
+	tag := " -tags " + name;
+
+	e.titleitem = rename(e.win, titleitem, "!!." + name);
+	cmd(e.win, e.cvs + " addtag " + name + " withtag !!." + name);
+	sc := spotcentre((0, 0), dxy(e.titleitem.r));
+	spotr := Rect(sc, sc).inset(-Spotdiam/2);
+
+	p := (spotr.max.x + Titletaglen, 0);
+	moveto(e.win, e.cvs, e.titleitem.name, e.titleitem.r, p);
+	e.titleitem.r = rmoveto(e.titleitem.r, p);
+	it := Item(name,  ((0, 0), (spotr.max.x + Titletaglen + titleitem.r.dx(), titleitem.r.dy())), (0, 0));
+
+	# make line to the right of spot
+	cmd(e.win, e.cvs + " create line " +
+		p2s((spotr.max.x, sc.y)) + " " + p2s((spotr.max.x+Titletaglen, sc.y)) + tag + Lineopts);
+
+	# make spot
+	spotid := cmd(e.win, e.cvs + " create oval " +
+		r2s(spotr) + Ovalopts + tag);
+	if (e.expanded)
+		cmd(e.win, e.cvs + " bind " + spotid + " <ButtonRelease-1>"
+			+ " {send event " + name + " contract}");
+	else
+		cmd(e.win, e.cvs + " bind " + spotid + " <ButtonRelease-1>"
+			+ " {send event " + name + " expand}");
+
+	cmd(e.win, e.cvs + " raise " + spotid);
+	e.spotid = int spotid;
+
+	it.attach = (0, sc.y);
+	it.r.max = (e.titleitem.r.dx() + spotr.max.x + Titletaglen, e.titleitem.r.dy());
+
+	if (!e.expanded) {
+		addcross(e, it, name);
+		return it;
+	}
+
+	it.r = placechildren(e, it, name);
+	return it;
+}
+
+rmoveto(r: Rect, p: Point): Rect
+{
+	return r.addpt(p.sub(r.min));
+}
+
+# place all children of e appropriately.
+# assumes that the canvas items of all children are already made.
+# return bbox rectangle of whole thing.
+placechildren(e: ref Expander, it: Item, tags: string): Rect
+{
+	ltag := " -tags {"+ tags + " !." + it.name + "}";
+	titlesize := dxy(e.titleitem.r);
+	sc := spotcentre(it.r.min, titlesize);
+	maxwidth := 0;
+	y := it.r.min.y + titlesize.y;
+	lasty := 0;
+	for (i := 0; i < len e.children; i++) {
+		c := e.children[i];
+		if (c.r.dx() > maxwidth)
+			maxwidth = c.r.dx();
+		c.r = c.r.addpt(it.r.min);
+		r: Rect;
+		r.min = (sc.x + Taglen, y);
+		r.max = r.min.add(dxy(c.r));
+		moveto(e.win, e.cvs, c.name, c.r, r.min);
+
+		# make item coords relative to parent
+		e.children[i].r = r.subpt(it.r.min);
+		cmd(e.win, e.cvs + " addtag " + it.name + " withtag " + c.name);
+
+		# horizontal attachment
+		cmd(e.win, e.cvs + " create line " +
+			p2s((sc.x, y + c.attach.y)) + " " +
+			p2s((sc.x + Taglen + c.attach.x, y + c.attach.y)) +
+			ltag + Lineopts);
+		lasty = y + c.attach.y;
+		y += r.dy();
+	}
+
+	# vertical attachment (if there were any children)
+	if (i > 0) {
+		id := cmd(e.win, e.cvs + " create line " +
+			p2s((sc.x, sc.y + Spotdiam/2)) + " " + p2s((sc.x, lasty)) + ltag + Lineopts);
+		cmd(e.win, e.cvs + " bind " + id + " <Button-1>"+
+				" {send event " + it.name + " see}");
+	}
+	r := Rect(it.r.min,
+			(max(sc.x+Spotdiam/2+Titletaglen+titlesize.x, sc.x+Taglen+maxwidth),
+			y));
+	return r;
+}
+
+Expander.event(e: self ref Expander, it: Item, ev: string): Item
+{
+	case ev {
+	"expand" =>
+		if (e.expanded) {
+			sys->print("item %s is already expanded\n", it.name);
+			return it;
+		}
+		e.expanded = 1;
+		tags := gettags(e.win, e.cvs, string e.spotid);
+		cmd(e.win, e.cvs + " delete !." + it.name);
+		cmd(e.win, e.cvs + " bind " + string e.spotid + " <ButtonRelease-1>" +
+			+ " {send event " + it.name + " contract}");
+		it.r = placechildren(e, it, tags);
+	"contract" =>
+		if (!e.expanded) {
+			sys->print("item %s is already contracted\n", it.name);
+			return it;
+		}
+		e.expanded = 0;
+		cmd(e.win, e.cvs + " delete !." + it.name);
+		for (i := 0; i < len e.children; i++)
+			cmd(e.win, e.cvs + " delete " + e.children[i].name);
+		cmd(e.win, e.cvs + " bind " + string e.spotid + " <ButtonRelease-1>" +
+			+ " {send event " + it.name + " expand}");
+		tags := gettags(e.win, e.cvs, string e.spotid);
+		addcross(e, it, tags);
+		titlesize := dxy(e.titleitem.r);
+		it.r.max = it.r.min.add((Taglen * 2 + Spotdiam + titlesize.x, titlesize.y));
+		e.children = nil;
+	"see" =>
+		cmd(e.win, e.cvs + " see " + p2s(it.r.min));
+	* =>
+		sys->print("unknown event '%s' on item %s\n", ev, it.name);
+	}
+	return it;
+}
+
+Expander.childrenchanged(e: self ref Expander, it: Item): Item
+{
+	cmd(e.win, e.cvs + " delete !." + it.name);
+	tags := gettags(e.win, e.cvs, string e.spotid);
+	it.r = placechildren(e, it, tags);
+	return it;
+}
+
+gettags(win: ref Tk->Toplevel, cvs: string, name: string): string
+{
+	tags := cmd(win, cvs + " gettags " + name);
+	(n, tagl) := sys->tokenize(tags, " ");
+	ntags := "";
+	for (; tagl != nil; tagl = tl tagl) {
+		t := hd tagl;
+		if (t[0] != '!' && (t[0] < '0' || t[0] > '9'))
+			ntags += " " + t;
+	}
+	return ntags;
+}
+
+spotcentre(origin, titlesize: Point): Point
+{
+	return (origin.x + Spotdiam / 2, origin.y + titlesize.y / 2);
+}
+
+addcross(e: ref Expander, it: Item, tags: string)
+{
+	p := spotcentre(it.r.min, dxy(e.titleitem.r));
+	crosstags := " -tags {" + tags + " !." + it.name + "}";
+
+	id1 := cmd(e.win, e.cvs + " create line " +
+		p2s((p.x-Spotdiam/2, p.y)) + " " +
+		p2s((p.x+Spotdiam/2, p.y)) + crosstags + Crossopts);
+	id2 := cmd(e.win, e.cvs + " create line " +
+		p2s((p.x, p.y-Spotdiam/2)) + " " +
+		p2s((p.x, p.y+Spotdiam/2)) + crosstags + Crossopts);
+	cmd(e.win, e.cvs + " lower " + id1 + ";" + e.cvs + " lower " + id2);
+}
+
+knownfont: string;
+knownfontheight: int;
+fontheight(win: ref Tk->Toplevel, font: string): int
+{
+	Font: import draw;
+	if (font == knownfont)
+		return knownfontheight;
+	if (win.image == nil)			# can happen if we run out of image memory
+		return -1;
+	f := Font.open(win.image.display, font);
+	if (f == nil)
+		return -1;
+	knownfont = font;
+	knownfontheight = f.height;
+	return f.height;
+}
+
+maketext(win: ref Tk->Toplevel, cvs: string, name: string, text: string): Item
+{
+	tag := " -tags " + name;
+	it := Item(name, ((0, 0), (0, 0)), (0, 0));
+	ttid := cmd(win, cvs + " create text 0 0 " +
+		" -anchor nw" + tag +
+		" -text '" + text);
+	it.r = bbox(win, cvs, ttid);
+	h := fontheight(win, cmd(win, cvs + " itemcget " + ttid + " -font"));
+	if (h != -1) {
+		dh := it.r.dy() - h;
+		it.r.min.y += dh / 2;
+		it.r.max.y -= dh / 2;
+	}
+	it.attach = (0, it.r.dy() / 2);
+	return it;
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "items: tk error %s on '%s'\n", e, s);
+	return e;
+}
+
+r2s(r: Rect): string
+{
+	return string r.min.x + " " + string r.min.y + " " +
+			string r.max.x + " " + string r.max.y;
+}
+
+s2r(s: string): Rect
+{
+	(n, toks) := sys->tokenize(s, " ");
+	if (n != 4) {
+		sys->print("'%s' is not a rectangle!\n", s);
+		raise "bad conversion";
+	}
+	r: Rect;
+	(r.min.x, toks) = (int hd toks, tl toks);
+	(r.min.y, toks) = (int hd toks, tl toks);
+	(r.max.x, toks) = (int hd toks, tl toks);
+	(r.max.y, toks) = (int hd toks, tl toks);
+	return r;
+}
+
+Item.eq(i: self Item, j: Item): int
+{
+	return i.r.eq(j.r) && i.attach.eq(j.attach) && i.name == j.name;
+}
+
+Item.addpt(i: self Item, p: Point): Item
+{
+	i.r = i.r.addpt(p);
+	return i;
+}
+
+Item.subpt(i: self Item, p: Point): Item
+{
+	i.r = i.r.subpt(p);
+	return i;
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+dxy(r: Rect): Point
+{
+	return r.max.sub(r.min);
+}
+
+max(a, b: int): int
+{
+	if (a > b)
+		return a;
+	return b;
+}
--- /dev/null
+++ b/appl/wm/ftree/items.m
@@ -1,0 +1,30 @@
+Items: module {
+	PATH: con "/dis/lib/ftree/items.dis";
+
+	Item: adt {
+		name:	string;	# tag held in common by all canvas items in this Item.
+		r:		Rect;		# relative to parent's Item when stored in children
+		attach:	Point;	# attachment point relative to r.min
+		
+		eq:		fn(i: self Item, j: Item): int;
+		addpt:	fn(i: self Item, p: Point): Item;
+		subpt:	fn(i: self Item, p: Point): Item;
+	};
+
+	Expander: adt {
+		titleitem:		Item;
+		expanded: 	int;
+		children: 		array of Item;
+		win: 			ref Tk->Toplevel;
+		cvs: 			string;
+		spotid:		int;
+	
+		new:			fn(win: ref Tk->Toplevel, cvs: string): ref Expander;
+		make:		fn(e: self ref Expander, it: Item): Item;
+		event:		fn(e: self ref Expander, it: Item, ev: string): Item;
+		childrenchanged:	fn(e: self ref Expander, it: Item): Item;
+	};
+
+	init: 		fn();
+	maketext:	fn(win: ref Tk->Toplevel, cvs: string, name: string, text: string): Item;
+};
--- /dev/null
+++ b/appl/wm/ftree/mkfile
@@ -1,0 +1,36 @@
+<../../../mkconfig
+
+TARG=\
+	items.dis\
+	cptree.dis\
+	ftree.dis
+
+MODULES=\
+	items.m\
+	cptree.m\
+
+SYSMODULES=\
+	arg.m\
+	draw.m\
+	plumbmsg.m\
+	popup.m\
+	readdir.m\
+	sh.m\
+	string.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+
+DISBIN=$ROOT/dis/lib/ftree
+
+all:V:	ftree.dis $TARG
+
+$ROOT/dis/wm/ftree.dis:	ftree.dis
+	rm -f $ROOT/dis/wm/ftree.dis && cp ftree.dis $ROOT/dis/wm/ftree.dis
+
+<$ROOT/mkfiles/mkdis
+
+install:V:	$ROOT/dis/wm/ftree.dis
+
+nuke:V:	nuke-std
+	cd $ROOT/dis/wm; rm -f ftree.dis
--- /dev/null
+++ b/appl/wm/ftree/wmsetup
@@ -1,0 +1,48 @@
+# /dis/sh script
+# wm defines "menu" and "delmenu" builtins
+load std
+prompt='% ' ''
+fn % {$*}
+autoload=std
+home=/usr/^"{cat /dev/user}
+
+if {! {~ wm ${loaded}}} {
+	echo wmsetup must run under wm >[1=2]
+	raise usage
+}
+
+fn wmrun {
+	args := $*
+	{
+		pctl newpgrp
+		fn wmrun
+		$args
+	} >[2] /chan/wmstderr &
+}
+
+fn cd {
+	builtin cd $*; echo cwd `{pwd} > /chan/shctl
+}
+
+menu Shell			{wmrun wm/sh}
+menu Acme			{wmrun acme}
+menu Edit				{wmrun wm/edit}
+menu Charon			{wmrun charon}
+menu Manual			{wmrun wm/man}
+menu Files			{if {ftest -d $home} {wmrun wm/dir $home} {wmrun wm/dir /}}
+menu ''	''
+menu System			'Debugger'		{wmrun wm/deb}
+menu System			'Module manager'	{wmrun wm/rt}
+menu System			'Task manager'		{wmrun wm/task}
+menu System			'Memory monitor'	{wmrun wm/memory}
+menu System			'About'			{wmrun wm/about}
+menu Misc			'Tetris'			{wmrun wm/tetris}
+menu Misc			'Coffee'			{wmrun wm/coffee}
+menu Misc			'Colours'			{wmrun wm/colors}
+menu Misc			'Winctl'			{wmrun wm/winctl}
+menu Misc			'Clock'			{wmrun wm/date}
+
+if {ftest -f $home/lib/wmsetup} {run $home/lib/wmsetup} {}
+
+builtin cd /usr/rog/limbo/browser
+wmrun ftree
--- /dev/null
+++ b/appl/wm/getauthinfo.b
@@ -1,0 +1,291 @@
+implement WmGetauthinfo;
+
+include "sys.m";
+	sys: Sys;
+
+include "security.m";
+	login: Login;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "keyring.m";
+	kr: Keyring;
+
+include "string.m";
+
+include "sh.m";
+
+#
+# Tk version of getauthinfo command
+#
+WmGetauthinfo: module 
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Wm: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+cfg := array[] of {
+	"frame .all -borderwidth 2 -relief raised",
+
+	"frame .u",
+	"label .u.l -text {User    } -anchor w",
+	"entry .u.e",
+	"pack .u.l .u.e -side left -in .u -expand 1",
+	"bind .u.e <Key-\n> {send cmd u}",
+	"focus .u.e",
+
+	"frame .p",
+	"label .p.l -text {Password} -anchor w",
+	"entry .p.e -show *",
+	"pack .p.l .p.e -side left -in .p -expand 1",
+	"bind .p.e <Key-\n> {send cmd p}",
+
+	"frame .s",
+	"label .s.l -text {Signer  } -anchor w",
+	"entry .s.e",
+	"pack .s.l .s.e -side left -in .s -expand 1",
+	"bind .s.e <Key-\n> {send cmd s}",
+
+	"frame .f",
+	"label .f.l -text {Save key} -anchor w",
+	"entry .f.e",
+	"pack .f.l .f.e -side left -in .f -expand 1",
+	"bind .f.e <Key-\n> {send cmd f}",
+
+	"frame .b",
+	"radiobutton .b.p -variable save -value p -anchor w -text '" + "Permanent",
+	"radiobutton .b.t -variable save -value t -anchor w -text '" + "Temporary",
+	"pack .b.p .b.t -side right -in .b -expand 1",
+	".b.p invoke",
+	"pack .u .p .s .f .b -in .all",
+	"pack .Wm_t .all -fill x -expand 1",
+	"update"
+};
+
+about : con "Generate keys and\n" + 
+	    "request certificate for\n" +
+	    "mounting remote server";
+
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "getauthinfo: no window context\n");
+		raise "fail:bad context";
+	}
+	kr = load Keyring Keyring->PATH;
+	str := load String String->PATH;
+
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	tkclient->init();
+	dialog->init();
+
+	(top, wmctl) := tkclient->toplevel(ctxt, "",
+		"Obtain Certificate for Server", Tkclient->Help);
+	for (c:=0; c<len cfg; c++)
+		tk->cmd(top, cfg[c]);
+	cmd := chan of string;
+	tk->namechan(top, cmd, "cmd");
+
+	login = load Login Login->PATH;
+	if(login == nil){
+		dialog->prompt(ctxt, top.image, "error -fg red", "Error", 
+			"Cannot load " + Login->PATH, 0, "Exit"::nil);
+		exit;
+	}
+
+	# start interactive
+	usr := user();
+	passwd := "";
+	signer := defaultsigner();
+	dir:= "";
+	file := "net!";
+	path := "";
+	tk->cmd(top, ".u.e insert end '" + usr);
+	tk->cmd(top, ".s.e insert end '" + signer);
+	tk->cmd(top, "update");
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	info : ref Keyring->Authinfo;
+	for(;;){
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		s := <-top.ctxt.ctl or
+		s = <-top.wreq =>
+			tkclient->wmctl(top, s);
+		menu := <-wmctl =>
+			case menu {
+			"exit" =>
+				exit;
+			"help" =>
+				dialog->prompt(ctxt, top.image, "info -fg green", "About", 
+				  about, 0, "OK"::nil);
+			}
+			tkclient->wmctl(top, menu);
+		rdy := <-cmd =>
+			case (rdy[0]) {
+			'u' =>
+				usr = tk->cmd(top, ".u.e get");
+				if(usr == "")
+					tk->cmd(top, "focus .u.e; update");
+				else {
+					dir = "/usr/" + usr + "/keyring/";
+					path = dir + file;
+					tk->cmd(top, ".f.e delete 0 end");
+					tk->cmd(top, ".f.e insert end '" + path);
+					tk->cmd(top, "focus .p.e; update");
+				}
+				continue;
+			'p' =>
+				passwd = tk->cmd(top, ".p.e get");	
+				if(passwd == "")
+					tk->cmd(top, "focus .p.e; update");
+				else
+					tk->cmd(top, "focus .s.e; update");
+				continue;
+			's' =>
+				signer = tk->cmd(top, ".s.e get");
+				if(signer == "")
+					tk->cmd(top, "focus .s.e");
+				else {
+					file = "net!" + signer;
+					path = dir + file;
+					tk->cmd(top, ".f.e delete 0 end");
+					tk->cmd(top, ".f.e insert end " + path);
+					tk->cmd(top, "focus .f.e; update");
+				}
+				continue;
+			'f' =>
+				path = tk->cmd(top, ".f.e get");
+				if(path == "") {
+					tk->cmd(top, "focus .f.e; update");
+					continue;
+				}
+
+				# start encrypt key exchange
+				addr := "net!"+signer+"!inflogin";
+				tk->cmd(top, "cursor -bitmap cursor.wait");
+				err: string;	
+				(err, info) = login->login(usr, passwd, addr);
+				tk->cmd(top, "cursor -default");
+				if(info == nil){
+					dialog->prompt(ctxt, top.image, "warning -fg yellow", "Warning", 
+						err, 0, "Continue"::nil);
+					tk->cmd(top, ".p.e delete 0 end");
+					tk->cmd(top, "focus .p.e");
+					continue;
+				}
+
+				# save the info for later access
+				save := tk->cmd(top, "variable save");
+				(dir, file) = str->splitr(path, "/");
+				if(save[0] == 't')
+					spawn save2file(dir, file);
+
+				tk->cmd(top, "cursor -default");			
+				if(kr->writeauthinfo(path, info) < 0){
+					dialog->prompt(ctxt, top.image, "error -fg red", "Error", 
+						"Can't write to " + path, 0, "Exit"::nil);
+					exit;
+				}	
+				if(save[0] == 'p')
+					dialog->prompt(ctxt, top.image, "info -fg green", "Notice", 
+						"Authentication information is\nsaved in file:\n" 
+						+ path, 0, "OK"::nil);
+				else
+					dialog->prompt(ctxt, top.image, "info -fg green", "Notice", 
+						"Authentication information is\nheld in a temporary file:\n" 
+						+ path, 0, "OK"::nil);
+
+				return;
+
+			}
+		}
+	}
+}
+
+
+user(): string
+{
+	sys = load Sys Sys->PATH;
+
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n];	
+}
+
+save2file(dir, file: string)
+{
+	if(sys->bind("#s", dir, Sys->MBEFORE) < 0)
+		exit;
+	fileio := sys->file2chan(dir, file);
+	if(fileio != nil)
+		exit;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	infodata := array[0] of byte;
+
+	for(;;) alt {
+	(off, nbytes, fid, rc) := <-fileio.read =>
+		if(rc == nil)
+			break;
+		if(off > len infodata){
+			rc <-= (infodata[off:off], nil);
+		} else {
+			if(off + nbytes > len infodata)
+				nbytes = len infodata - off;
+			rc <-= (infodata[off:off+nbytes], nil);
+		}
+
+	(off, data, fid, wc) := <-fileio.write =>
+		if(wc == nil)
+			break;
+
+		if(off != len infodata){
+			wc <-= (0, "cannot be rewritten");
+		} else {
+			nid := array[len infodata+len data] of byte;
+			nid[0:] = infodata;
+			nid[len infodata:] = data;
+			infodata = nid;
+			wc <-= (len data, nil);
+		}
+		data = nil;
+	}
+}
+
+# get default signer server name
+defaultsigner(): string
+{
+	return "$SIGNER";
+}
--- /dev/null
+++ b/appl/wm/hebrew.m
@@ -1,0 +1,30 @@
+hebrewtab := array[] of {
+	Remaptab(' ', ' '),
+	Remaptab('t', 16r5d0+0),
+	Remaptab('c', 16r5d0+1),
+	Remaptab('d', 16r5d0+2),
+	Remaptab('s', 16r5d0+3),
+	Remaptab('v', 16r5d0+4),
+	Remaptab('u', 16r5d0+5),
+	Remaptab('z', 16r5d0+6),
+	Remaptab('j', 16r5d0+7),
+	Remaptab('y', 16r5d0+8),
+	Remaptab('h', 16r5d0+9),
+	Remaptab('l', 16r5d0+10),
+	Remaptab('f', 16r5d0+11),
+	Remaptab('k', 16r5d0+12),
+	Remaptab('o', 16r5d0+13),
+	Remaptab('n', 16r5d0+14),
+	Remaptab('i', 16r5d0+15),
+	Remaptab('b', 16r5d0+16),
+	Remaptab('x', 16r5d0+17),
+	Remaptab('g', 16r5d0+18),
+	Remaptab(';', 16r5d0+19),
+	Remaptab('p', 16r5d0+20),
+	Remaptab('.', 16r5d0+21),
+	Remaptab('m', 16r5d0+22),
+	Remaptab('e', 16r5d0+23),
+	Remaptab('r', 16r5d0+24),
+	Remaptab('a', 16r5d0+25),
+	Remaptab(',', 16r5d0+26)
+};
--- /dev/null
+++ b/appl/wm/keyboard.b
@@ -1,0 +1,511 @@
+implement Keybd;
+
+#
+# extensive revision of code originally by N. W. Knauft
+#
+# Copyright © 1997 Lucent Technologies Inc.  All rights reserved.
+# Revisions Copyright © 1998 Vita Nuova Limited.  All rights reserved.
+# Rewritten code Copyright © 2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+# To do:
+#	input from file
+#	calculate size
+
+include "sys.m";
+        sys: Sys;
+
+include "draw.m";
+        draw: Draw;
+	Rect, Point: import draw;
+
+include "tk.m";
+        tk: Tk;
+
+include "tkclient.m";
+        tkclient: Tkclient;
+
+include "arg.m";
+
+include "keyboard.m";
+
+Keybd: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+FONT: con "/fonts/lucidasans/boldlatin1.6.font";
+SPECFONT: con "/fonts/lucidasans/unicode.6.font";
+
+# size in pixels
+#KEYSIZE: con 16;
+KEYSIZE: con 13;
+KEYSPACE: con 2;
+KEYBORDER: con 1;
+KEYGAP: con KEYSPACE - (2 * KEYBORDER);
+#ENDGAP: con 2 - KEYBORDER;
+ENDGAP: con 0;
+
+Key: adt {
+	name: string;
+	val:	int;
+	size:	int;
+	x:	list of int;
+	on:	int;
+};
+
+background: con "#dddddd";
+
+Backspace, Tab, Backslash, CapsLock, Return, Shift, Ctrl, Esc, Alt, Space: con iota;
+
+specials := array[] of {
+Backspace =>		Key("<-", '\b', 28, nil, 0),
+Tab =>			Key("Tab", '\t', 26, nil, 0),
+Backslash =>		Key("\\\\", '\\', KEYSIZE, nil, 0),
+CapsLock =>		Key("Caps", Keyboard->Caps, 40, nil, 0),
+Return =>			Key("Enter", '\n', 36, nil, 0),
+Shift =>			Key("Shift", Keyboard->LShift, 45, nil, 0),
+Esc =>			Key("Esc", 8r33, 21, nil, 0),
+Ctrl =>			Key("Ctrl", Keyboard->LCtrl, 36, nil, 0),
+Alt =>			Key("Alt", Keyboard->LAlt, 22, nil, 0),
+Space =>			Key(" ", ' ', 140, nil, 0),
+Space+1 =>		Key("Return", '\n', 36, nil, 0),
+};
+
+keys:= array[] of {
+	# unshifted
+	array[] of {
+		"Esc", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "\\\\", "`", nil,
+		"Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "<-", nil,
+		"Ctrl", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "Enter", nil,
+		"Shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "Shift", nil,
+		"Caps", "Alt", " ", "Alt", nil,
+	},
+
+	# shifted
+	array[] of {
+		"Esc", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+", "|", "~", nil,
+		"Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\\{", "\\}", "<-", nil,
+		"Ctrl", "A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "\"", "Return", nil,
+		"Shift", "Z", "X", "C", "V", "B", "N", "M", "<", ">", "?", "Shift", nil,
+		"Caps", "Alt", " ", "Alt", nil,
+	},
+};
+
+keyvals: array of array of int;
+noexit := 0;
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "keyboard: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	arg := load Arg Arg->PATH;
+
+	taskbar := 0;
+	winopts := Tkclient->Hide;
+	arg->init(args);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		't' =>
+			taskbar = 1;
+		'e' =>
+			noexit = 1;
+			winopts = 0;
+		* =>
+			sys->fprint(sys->fildes(2), "usage: keyboard [-et]\n");
+			raise "fail:usage";
+		}
+	}
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+
+	keyvals = array[] of {
+		array[len keys[0]] of int,
+		array[len keys[1]] of int,
+	};
+	setindex(keys[0], keyvals[0], specials);
+	setindex(keys[1], keyvals[1], specials);
+
+
+	(t, wcmd) := tkclient->toplevel(ctxt, "", "Kbd", winopts);
+	cmd(t, ". configure -bd 0 -relief flat");
+
+	for(i := 0; i < len keys[0]; i++)
+		if(keys[0][i] != nil)
+			cmd(t, sys->sprint("button .b%d -takefocus 0 -font %s -width %d -height %d -bd %d -activebackground %s -text {%s} -command 'send keypress %d",
+				i, FONT, KEYSIZE, KEYSIZE, KEYBORDER, background, keys[0][i], keyvals[0][i]));
+
+	for(i = 0; i < len specials; i++) {
+		k := specials[i];
+		for(xl := k.x; xl != nil; xl = tl xl)
+			cmd(t, sys->sprint(".b%d configure -font %s -width %d", hd xl, SPECFONT, k.size));
+	}
+
+	# pack buttons in rows
+	i = 0;
+	for(j:=0; i < len keys[0]; j++){
+		rowf := sys->sprint(".f%d", j);
+		cmd(t, "frame "+rowf);
+		cmd(t, sys->sprint("frame .pad%d -height %d", j, KEYGAP));
+		if(ENDGAP){
+			cmd(t, rowf + ".pad -width " + string ENDGAP);
+			cmd(t, "pack " + rowf + ".pad -side left");
+		}
+		for(; keys[0][i] != nil; i++){
+			label := keys[0][i];
+			expand := label != "\\\\" && len label > 1;
+			cmd(t, "pack .b" + string i + " -in "+ rowf + " -side left -fill x -expand "+string expand);
+			if(keys[0][i+1] != nil && KEYGAP > 0){
+				padf := sys->sprint("%s.pad%d", rowf, i);
+				cmd(t, "frame " + padf + " -width " + string KEYGAP);
+				cmd(t, "pack " + padf + " -side left");
+			}
+		}
+		if(ENDGAP){
+			padf := sys->sprint("%s.pad%d", rowf, i);
+			cmd(t, "frame " + padf + " -width " + string ENDGAP);
+			cmd(t, "pack " + padf + " -side left");
+		}
+		i++;
+	}
+	nrow := j;
+
+	# pack rows in frame
+	for(j = 0; j < nrow; j++)
+		cmd(t, sys->sprint("pack .f%d .pad%d -fill x -in .", j, j));
+
+	(w, h) := (int cmd(t, ". cget -width"), int cmd(t, ". cget -height"));
+	r := t.screenr;
+	off := (r.dx()-w)/2;
+	cmd(t, sys->sprint(". configure -x %d -y %d", r.min.x+off, r.max.y-h));
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "ptr" :: nil);
+
+	spawn handle_keyclicks(t, wcmd, taskbar);
+}
+
+setindex(keys: array of string, keyvals: array of int, spec: array of Key)
+{
+	for(i := 0; i < len keys; i++){
+		if(keys[i] == nil)
+			continue;
+		val := keys[i][0];
+		if(len keys[i] > 1 && val == '\\')
+			val = keys[i][1];
+		for(j := 0; j < len spec; j++)
+			if(spec[j].name == keys[i]){
+				if(!inlist(i, spec[j].x))
+					spec[j].x = i :: spec[j].x;
+				val = spec[j].val;
+				break;
+			}
+		keyvals[i] = val;
+	}
+}
+
+inlist(i: int, l: list of int): int
+{
+	for(; l != nil; l = tl l)
+		if(hd l == i)
+			return 1;
+	return 0;
+}
+
+handle_keyclicks(t: ref Tk->Toplevel, wcmd: chan of string, taskbar: int)
+{
+	keypress := chan of string;
+	tk->namechan(t, keypress, "keypress");
+
+	if(taskbar)
+		tkclient->wmctl(t, "task");
+
+	cmd(t,"update");
+
+	collecting := 0;
+	collected := "";
+	for(;;)alt {
+	k := <-keypress =>
+		c := int k;
+		case c {
+		Keyboard->Caps =>
+			active(t, Ctrl, 0);
+			active(t, Shift, 0);
+			active(t, Alt, 0);
+			active(t, CapsLock, -1);
+			redraw(t);
+		Keyboard->LShift =>
+			active(t, Shift, -1);
+			redraw(t);
+		Keyboard->LCtrl =>
+			active(t, Alt, 0);
+			active(t, Ctrl, -1);
+			active(t, Shift, 0);
+			redraw(t);
+		Keyboard->LAlt =>
+			active(t, Alt, -1);
+			active(t, Ctrl, 0);
+			active(t, Shift, 0);
+			redraw(t);
+			if(specials[Alt].on){
+				collecting = 1;
+				collected = "";
+			}else
+				collecting = 0;
+		* =>
+			if(collecting){
+				collected[len collected] = c;
+				c = latin1(collected);
+				if(c < -1)
+					continue;
+				collecting = 0;
+				if(c == -1){
+					for(i := 0; i < len collected; i++)
+						sendkey(t, collected[i]);
+					continue;
+				}
+			}
+			show := specials[Ctrl].on | specials[Alt].on | specials[Shift].on;
+			if(specials[Ctrl].on)
+				c &= 16r1F;
+			active(t, Ctrl, 0);
+			active(t, Alt, 0);
+			active(t, Shift, 0);
+			if(show)
+				redraw(t);
+			sendkey(t, c);
+		}
+	m := <-t.ctxt.ptr =>
+		tk->pointer(t, *m);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-wcmd =>
+		if (s == "exit" && noexit)
+			s = "task";
+		tkclient->wmctl(t, s);
+	}
+}
+
+sendkey(t: ref Tk->Toplevel, c: int)
+{
+	sys->fprint(t.ctxt.connfd, "key %d", c);
+}
+
+active(t: ref Tk->Toplevel, keyno: int, on: int)
+{
+	key := specials[keyno:];
+	if(on < 0)
+		key[0].on ^= 1;
+	else
+		key[0].on = on;
+	for(xl := key[0].x; xl != nil; xl = tl xl){
+		col := background;
+		if(key[0].on)
+			col = "white";
+		cmd(t, ".b"+string hd xl+" configure -bg "+col+ " -activebackground "+col);
+	}
+}
+
+redraw(t: ref Tk->Toplevel)
+{
+	shifted := specials[Shift].on;
+	bank := keys[shifted];
+	vals := keyvals[shifted];
+	for(i:=0; i<len bank; i++) {
+		key := bank[i];
+		val := vals[i];
+		if(key != nil){
+			if(specials[CapsLock].on && len key == 1){
+				if(key[0]>='A' && key[0]<='Z')	# true if also shifted
+					key[0] += 'a'-'A';
+				else if(key[0] >= 'a' && key[0]<='z')
+					key[0] += 'A'-'a';
+				val = key[0];
+			}
+			cmd(t, ".b" + string i + " configure -text {" + key + "} -command 'send keypress " + string val);
+		}
+  	}
+	cmd(t, "update");
+}
+
+#
+# The code makes two assumptions: strlen(ld) is 1 or 2; latintab[i].ld can be a
+# prefix of latintab[j].ld only when j<i.
+#
+Cvlist: adt
+{
+	ld:	string;	# must be seen before using this conversion
+	si:	string;	#  options for last input characters
+	so:	string;	# the corresponding Rune for each si entry
+};
+latintab: array of Cvlist = array[] of {
+	(" ", " i",	"␣ı"),
+	("!~", "-=~",	"≄≇≉"),
+	("!", "!<=>?bmp",	"¡≮≠≯‽⊄∉⊅"),
+	("\"*", "IUiu",	"ΪΫϊϋ"),
+	("\"", "\"AEIOUYaeiouy",	"¨ÄËÏÖÜŸäëïöüÿ"),
+	("$*", "fhk",	"ϕϑϰ"),
+	("$", "BEFHILMRVaefglopv",	"ℬℰℱℋℐℒℳℛƲɑℯƒℊℓℴ℘ʋ"),
+	("\'\"", "Uu",	"Ǘǘ"),
+	("\'", "\'ACEILNORSUYZacegilnorsuyz",	"´ÁĆÉÍĹŃÓŔŚÚÝŹáćéģíĺńóŕśúýź"),
+	("*", "*ABCDEFGHIKLMNOPQRSTUWXYZabcdefghiklmnopqrstuwxyz",	"∗ΑΒΞΔΕΦΓΘΙΚΛΜΝΟΠΨΡΣΤΥΩΧΗΖαβξδεφγθικλμνοπψρστυωχηζ"),
+	("+", "-O",	"±⊕"),
+	(",", ",ACEGIKLNORSTUacegiklnorstu",	"¸ĄÇĘĢĮĶĻŅǪŖŞŢŲąçęģįķļņǫŗşţų"),
+	("-*", "l",	"ƛ"),
+	("-", "+-2:>DGHILOTZbdghiltuz~",	"∓­ƻ÷→ÐǤĦƗŁ⊖ŦƵƀðǥℏɨłŧʉƶ≂"),
+	(".", ".CEGILOZceglz",	"·ĊĖĠİĿ⊙Żċėġŀż"),
+	("/", "Oo",	"Øø"),
+	("1", "234568",	"½⅓¼⅕⅙⅛"),
+	("2", "-35",	"ƻ⅔⅖"),
+	("3", "458",	"¾⅗⅜"),
+	("4", "5",	"⅘"),
+	("5", "68",	"⅚⅝"),
+	("7", "8",	"⅞"),
+	(":", ")-=",	"☺÷≔"),
+	("<!", "=~",	"≨⋦"),
+	("<", "-<=>~",	"←«≤≶≲"),
+	("=", ":<=>OV",	"≕⋜≡⋝⊜⇒"),
+	(">!", "=~",	"≩⋧"),
+	(">", "<=>~",	"≷≥»≳"),
+	("?", "!?",	"‽¿"),
+	("@\'", "\'",	"ъ"),
+	("@@", "\'EKSTYZekstyz",	"ьЕКСТЫЗекстыз"),
+	("@C", "Hh",	"ЧЧ"),
+	("@E", "Hh",	"ЭЭ"),
+	("@K", "Hh",	"ХХ"),
+	("@S", "CHch",	"ЩШЩШ"),
+	("@T", "Ss",	"ЦЦ"),
+	("@Y", "AEOUaeou",	"ЯЕЁЮЯЕЁЮ"),
+	("@Z", "Hh",	"ЖЖ"),
+	("@c", "h",	"ч"),
+	("@e", "h",	"э"),
+	("@k", "h",	"х"),
+	("@s", "ch",	"щш"),
+	("@t", "s",	"ц"),
+	("@y", "aeou",	"яеёю"),
+	("@z", "h",	"ж"),
+	("@", "ABDFGIJLMNOPRUVXabdfgijlmnopruvx",	"АБДФГИЙЛМНОПРУВХабдфгийлмнопрувх"),
+	("A", "E",	"Æ"),
+	("C", "ACU",	"⋂ℂ⋃"),
+	("Dv", "Zz",	"DŽDž"),
+	("D", "-e",	"Ð∆"),
+	("G", "-",	"Ǥ"),
+	("H", "-H",	"Ħℍ"),
+	("I", "-J",	"ƗIJ"),
+	("L", "&-Jj|",	"⋀ŁLJLj⋁"),
+	("N", "JNj",	"NJℕNj"),
+	("O", "*+-./=EIcoprx",	"⊛⊕⊖⊙⊘⊜ŒƢ©⊚℗®⊗"),
+	("P", "P",	"ℙ"),
+	("Q", "Q",	"ℚ"),
+	("R", "R",	"ℝ"),
+	("S", "123S",	"¹²³§"),
+	("T", "-u",	"Ŧ⊨"),
+	("V", "=",	"⇐"),
+	("Y", "R",	"Ʀ"),
+	("Z", "-ACSZ",	"Ƶℤ"),
+	("^", "ACEGHIJOSUWYaceghijosuwy",	"ÂĈÊĜĤÎĴÔŜÛŴŶâĉêĝĥîĵôŝûŵŷ"),
+	("_\"", "AUau",	"ǞǕǟǖ"),
+	("_,", "Oo",	"Ǭǭ"),
+	("_.", "Aa",	"Ǡǡ"),
+	("_", "AEIOU_aeiou",	"ĀĒĪŌŪ¯āēīōū"),
+	("`\"", "Uu",	"Ǜǜ"),
+	("`", "AEIOUaeiou",	"ÀÈÌÒÙàèìòù"),
+	("a", "ben",	"↔æ∠"),
+	("b", "()+-0123456789=bknpqru",	"₍₎₊₋₀₁₂₃₄₅₆₇₈₉₌♝♚♞♟♛♜•"),
+	("c", "$Oagu",	"¢©∩≅∪"),
+	("dv", "z",	"dž"),
+	("d", "-adegz",	"ð↓‡°†ʣ"),
+	("e", "$lmns",	"€⋯—–∅"),
+	("f", "a",	"∀"),
+	("g", "$-r",	"¤ǥ∇"),
+	("h", "-v",	"ℏƕ"),
+	("i", "-bfjps",	"ɨ⊆∞ij⊇∫"),
+	("l", "\"$&\'-jz|",	"“£∧‘łlj⋄∨"),
+	("m", "iou",	"µ∈×"),
+	("n", "jo",	"nj¬"),
+	("o", "AOUaeiu",	"Å⊚Ůåœƣů"),
+	("p", "Odgrt",	"℗∂¶∏∝"),
+	("r", "\"\'O",	"”’®"),
+	("s", "()+-0123456789=abnoprstu",	"⁽⁾⁺⁻⁰ⁱ⁲⁳⁴⁵⁶⁷⁸⁹⁼ª⊂ⁿº⊃√ß∍∑"),
+	("t", "-efmsu",	"ŧ∃∴™ς⊢"),
+	("u", "-AEGIOUaegiou",	"ʉĂĔĞĬŎŬ↑ĕğĭŏŭ"),
+	("v\"", "Uu",	"Ǚǚ"),
+	("v", "ACDEGIKLNORSTUZacdegijklnorstuz",	"ǍČĎĚǦǏǨĽŇǑŘŠŤǓŽǎčďěǧǐǰǩľňǒřšťǔž"),
+	("w", "bknpqr",	"♗♔♘♙♕♖"),
+	("x", "O",	"⊗"),
+	("y", "$",	"¥"),
+	("z", "-",	"ƶ"),
+	("|", "Pp|",	"Þþ¦"),
+	("~!", "=",	"≆"),
+	("~", "-=AINOUainou~",	"≃≅ÃĨÑÕŨãĩñõũ≈"),
+};
+
+#
+# Given 5 characters k[0]..k[4], find the rune or return -1 for failure.
+#
+unicode(k: string): int
+{
+	c := 0;
+	for(i:=1; i<5; i++){
+		r := k[i];
+		c <<= 4;
+		if('0'<=r && r<='9')
+			c += r-'0';
+		else if('a'<=r && r<='f')
+			c += 10 + r-'a';
+		else if('A'<=r && r<='F')
+			c += 10 + r-'A';
+		else
+			return -1;
+	}
+	return c;
+}
+
+#
+# Given n characters k[0]..k[n-1], find the corresponding rune or return -1 for
+# failure, or something < -1 if n is too small.  In the latter case, the result
+# is minus the required n.
+#
+latin1(k: string): int
+{
+	n := len k;
+	if(k[0] == 'X' || n>1 && k[0] == 'x' && k[1]!='O')	# 'x' to avoid having to Shift as well
+		if(n>=5)
+			return unicode(k);
+		else
+			return -5;
+	for(i := 0; i < len latintab; i++){
+		l := latintab[i];
+		if(k[0] == l.ld[0]){
+			if(n == 1)
+				return -2;
+			c := 0;
+			if(len l.ld == 1)
+				c = k[1];
+			else if(l.ld[1] != k[1])
+				continue;
+			else if(n == 2)
+				return -3;
+			else
+				c = k[2];
+			for(p:=0; p < len l.si; p++)
+				if(l.si[p] == c && p < len l.so)
+					return l.so[p];
+			return -1;
+		}
+	}
+	return -1;
+}
+
+cmd(top: ref Tk->Toplevel, c: string): string
+{
+	e := tk->cmd(top, c);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "keyboard: tk error on '%s': %s\n", c, e);
+	return e;
+}
--- /dev/null
+++ b/appl/wm/logon.b
@@ -1,0 +1,339 @@
+implement WmLogon;
+#
+# Logon program for Wm environment
+#
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Screen, Display, Image, Context, Point, Rect: import draw;
+	ctxt: ref Context;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "readdir.m";
+
+include "arg.m";
+include "sh.m";
+include "newns.m";
+include "keyring.m";
+include "security.m";
+
+WmLogon: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+cfg := array[] of {
+	"label .p -bitmap @/icons/inferno.bit -borderwidth 2 -relief raised",
+	"frame .l -bg red",
+	"label .l.u -fg black -bg silver -text {User Name:} -anchor w",
+	"pack .l.u -fill x",
+	"frame .e",
+	"entry .e.u -bg white",
+	"pack .e.u -fill x",
+	"frame .f -borderwidth 2 -relief raised",
+	"pack .l .e -side left -in .f",
+	"pack .p .f -fill x",
+	"bind .e.u <Key-\n> {send cmd ok}",
+	"focus .e.u"
+};
+
+listcfg := array[] of {
+	"frame .f",
+	"listbox .f.lb -yscrollcommand {.f.sb set}",
+	"scrollbar .f.sb -orient vertical -command {.f.lb yview}",
+	"button .login -text {Login} -command {send cmd login}",
+	"pack .f.sb .f.lb -in .f -side left -fill both -expand 1",
+	"pack .f -side top -anchor center -fill y -expand 1",
+	"pack .login -side top",
+#	"pack propagate . 0",
+};
+
+init(actxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if(tkclient == nil){
+		sys->fprint(stderr(), "logon: cannot load %s: %r\n", Tkclient->PATH);
+		raise "fail:bad module";
+	}
+	sys->pctl(Sys->NEWPGRP|Sys->FORKFD, nil);
+	tkclient->init();
+	ctxt = actxt;
+
+	dolist := 0;
+	usr := "";
+	nsfile := "namespace";
+	arg := load Arg Arg->PATH;
+	if(arg != nil){
+		arg->init(args);
+		arg->setusage("logon [-l] [-n namespace] [-u user]");
+		while((opt := arg->opt()) != 0){
+			case opt{
+			'u' =>
+				usr = arg->earg();
+			'l' =>
+				dolist = 1;
+			'n' =>
+				nsfile = arg->earg();
+			* =>
+				arg->usage();
+			}
+		}
+		args = arg->argv();
+		arg = nil;
+	} else
+		args = nil;
+	if(ctxt == nil)
+		sys->fprint(stderr(), "logon: must run under a window manager\n");
+
+	(ctlwin, nil) := tkclient->toplevel(ctxt, nil, nil, Tkclient->Plain);
+	if(sys->fprint(ctlwin.ctxt.connfd, "request") == -1){
+		sys->fprint(stderr(), "logon: must be run as principal wm application\n");
+		raise "fail:lack of control";
+	}
+
+	if(dolist)
+		usr = chooseuser(ctxt);
+
+	if (usr == nil || !logon(usr)) {
+		(panel, cmd) := makepanel(ctxt, cfg);
+		stop := chan of int;
+		spawn tkclient->handler(panel, stop);
+		for(;;) {
+			tk->cmd(panel, "focus .e.u; update");
+			<-cmd;
+			usr = tk->cmd(panel, ".e.u get");
+			if(usr == "") {
+				notice("You must supply a user name to login");
+				continue;
+			}
+			if(logon(usr)) {
+				panel = nil;
+				stop <-= 1;
+				break;
+			}
+			tk->cmd(panel, ".e.u delete 0 end");
+		}
+	}
+	ok: int;
+	if(nsfile != nil){
+		(ok, nil) = sys->stat(nsfile);
+		if(ok < 0){
+			nsfile = nil;
+			(ok, nil) = sys->stat("namespace");
+		}
+	}else
+		(ok, nil) = sys->stat("namespace");
+	if(ok >= 0) {
+		ns := load Newns Newns->PATH;
+		if(ns == nil)
+			notice("failed to load namespace builder");
+		else if ((nserr := ns->newns(nil, nsfile)) != nil)
+			notice("namespace error:\n"+nserr);
+	}
+	tkclient->wmctl(ctlwin, "endcontrol");
+	errch := chan of string;
+	spawn exec(ctxt, args, errch);
+	err := <-errch;
+	if (err != nil) {
+		sys->fprint(stderr(), "logon: %s\n", err);
+		raise "fail:exec failed";
+	}
+}
+
+makepanel(ctxt: ref Draw->Context, cmds: array of string): (ref Tk->Toplevel, chan of string)
+{
+	(t, nil) := tkclient->toplevel(ctxt, "-bg silver", nil, Tkclient->Plain);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	for(i := 0; i < len cmds; i++)
+		tk->cmd(t, cmds[i]);
+	err := tk->cmd(t, "variable lasterr");
+	if(err != nil) {
+		sys->fprint(stderr(), "logon: tk error: %s\n", err);
+		raise "fail:config error";
+	}
+	tk->cmd(t, "update");
+	centre(t);
+	tkclient->startinput(t, "kbd" :: "ptr" :: nil);
+	tkclient->onscreen(t, "onscreen");
+	return (t, cmd);
+}
+
+exec(ctxt: ref Draw->Context, argv: list of string, errch: chan of string)
+{
+	sys->pctl(sys->NEWFD, 0 :: 1 :: 2 :: nil);
+	{
+		argv = "/dis/wm/toolbar.dis" :: nil;
+		cmd := load Command hd argv;
+		if (cmd == nil) {
+			errch <-= sys->sprint("cannot load %s: %r", hd argv);
+		} else {
+			errch <-= nil;
+			spawn cmd->init(ctxt, argv);
+		}
+	}exception{
+	"fail:*" =>
+		exit;
+	}
+}
+
+logon(user: string): int
+{
+	userdir := "/usr/"+user;
+	if(sys->chdir(userdir) < 0) {
+		notice("There is no home directory for \""+
+			user+"\"\nmounted on this machine");
+		return 0;
+	}
+
+	chmod("/chan", Sys->DMDIR|8r777);
+	chmod("/chan/wmrect", 8r666);
+	chmod("/chan/wmctl", 8r666);
+
+	#
+	# Set the user id
+	#
+	fd := sys->open("/dev/user", sys->OWRITE);
+	if(fd == nil) {
+		notice(sys->sprint("failed to open /dev/user: %r"));
+		return 0;
+	}
+	b := array of byte user;
+	if(sys->write(fd, b, len b) < 0) {
+		notice("failed to write /dev/user\nwith error "+sys->sprint("%r"));
+		return 0;
+	}
+
+	return 1;
+}
+
+chmod(file: string, mode: int): int
+{
+	d := sys->nulldir;
+	d.mode = mode;
+	if(sys->wstat(file, d) < 0){
+		notice(sys->sprint("failed to chmod %s: %r", file));
+		return -1;
+	}
+	return 0;
+}
+
+chooseuser(ctxt: ref Draw->Context): string
+{
+	(t, cmd) := makepanel(ctxt, listcfg);
+	usrlist := getusers();
+	if(usrlist == nil)
+		usrlist = "inferno" :: nil;
+	for(; usrlist != nil; usrlist = tl usrlist)
+		tkcmd(t, ".f.lb insert end '" + hd usrlist);
+	tkcmd(t, "update");
+	stop := chan of int;
+	spawn tkclient->handler(t, stop);
+	u := "";
+	for(;;){
+		<-cmd;
+		sel := tkcmd(t, ".f.lb curselection");
+		if(sel == nil)
+			continue;
+		u = tkcmd(t, ".f.lb get " + sel);
+		if(u != nil)
+			break;
+	}
+	stop <-= 1;
+	return u;
+}
+
+getusers(): list of string
+{
+	readdir := load Readdir Readdir->PATH;
+	if(readdir == nil)
+		return nil;
+	(dirs, nil) := readdir->init("/usr", Readdir->NAME);
+	n: list of string;
+	for (i := len dirs -1; i >=0; i--)
+		if (dirs[i].qid.qtype & Sys->QTDIR)
+			n = dirs[i].name :: n;
+	return n;
+}
+
+notecmd := array[] of {
+	"frame .f",
+	"label .f.l -bitmap error -foreground red",
+	"button .b -text Continue -command {send cmd done}",
+	"focus .f",
+	"bind .f <Key-\n> {send cmd done}",
+	"pack .f.l .f.m -side left -expand 1",
+	"pack .f .b",
+	"pack propagate . 0",
+};
+
+centre(t: ref Tk->Toplevel)
+{
+	org: Point;
+	ir := tk->rect(t, ".", Tk->Border|Tk->Required);
+	org.x = t.screenr.dx() / 2 - ir.dx() / 2;
+	org.y = t.screenr.dy() / 3 - ir.dy() / 2;
+#sys->print("ir: %d %d %d %d\n", ir.min.x, ir.min.y, ir.max.x, ir.max.y);
+	if (org.y < 0)
+		org.y = 0;
+	tk->cmd(t, ". configure -x " + string org.x + " -y " + string org.y);
+}
+
+notice(message: string)
+{
+	(t, nil) := tkclient->toplevel(ctxt, "-borderwidth 2 -relief raised", nil, Tkclient->Plain);
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	tk->cmd(t, "label .f.m -anchor nw -text '"+message);
+	for(i := 0; i < len notecmd; i++)
+		tk->cmd(t, notecmd[i]);
+	centre(t);
+	tkclient->onscreen(t, "onscreen");
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+	stop := chan of int;
+	spawn tkclient->handler(t, stop);
+	tk->cmd(t, "update; cursor -default");
+	<-cmd;
+	stop <-= 1;
+}
+
+tkcmd(t: ref Tk->Toplevel, cmd: string): string
+{
+	s := tk->cmd(t, cmd);
+	if (s != nil && s[0] == '!') {
+		sys->print("%s\n", cmd);
+		sys->print("tk error: %s\n", s);
+	}
+	return s;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+rf(path: string) : string
+{
+	fd := sys->open(path, sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[512] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/wm/logwindow.b
@@ -1,0 +1,187 @@
+implement Logwindow;
+
+#
+# Copyright © 1999 Vita Nuova Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+	draw: Draw;
+include "tk.m";
+	tk: Tk;
+	cmd: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "arg.m";
+
+Logwindow: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+cfg := array[] of {
+	"frame .bf",
+	"checkbutton .bf.scroll -text Scroll -variable scroll -command {send cmd scroll}",
+	".bf.scroll select",
+	"checkbutton .bf.popup -text {Pop up} -variable popup -command {send cmd popup}",
+	".bf.popup select",
+	"pack .bf.scroll .bf.popup -side left",
+	"frame .t",
+	"scrollbar .t.scroll -command {.t.t yview}",
+	"text .t.t -height 7c -yscrollcommand {.t.scroll set}",
+	"pack .t.scroll -side left -fill y",
+	"pack .t.t -fill both -expand 1",
+	"pack .Wm_t -fill x",
+	"pack .bf -anchor w",
+	"pack .t -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+eflag := 0;
+
+badmodule(p: string)
+{
+	sys->fprint(stderr, "logwindow: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmodule(Tkclient->PATH);
+	tkclient->init();
+
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmodule(Tk->PATH);
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+
+	if (ctxt == nil) {
+		sys->fprint(stderr, "logwindow: nil Draw->Context\n");
+		raise "fail:no draw context";
+	}
+	gflag := 0;
+	title := "Log Window";
+	arg->init(argv);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'e' =>
+			eflag = 1;
+		'g' =>
+			gflag = 1;
+		* =>
+			sys->fprint(stderr, "usage: logwindow [-ge] [title]\n");
+			raise "fail:usage";
+		}
+	}
+	argv = arg->argv();
+	if (argv != nil)
+		title = hd argv;
+
+	if (!gflag)
+		sys->pctl(Sys->NEWPGRP, nil);
+
+	(top, wmchan) := tkclient->toplevel(ctxt, "", title, Tkclient->Hide|Tkclient->Resize);
+	if (top == nil) {
+		sys->fprint(stderr, "logwindow: couldn't make window\n");
+		raise "fail: no window";
+	}
+	cmd(top, ". unmap");
+
+	for (c:=0; c<len cfg; c++)
+		tk->cmd(top, cfg[c]);
+	if ((err := tk->cmd(top, "variable lasterror")) != nil) {
+		sys->fprint(stderr, "logwindow: tk error: %s\n", err);
+		raise "fail: tk error";
+	}
+
+	logwin(sys->fildes(0), top, wmchan);
+}
+
+scrolling := 1;
+popup := 1;
+
+logwin(fd: ref Sys->FD, top: ref Tk->Toplevel, wmchan: chan of string)
+{
+	cmd := chan of string;
+	tk->namechan(top, cmd, "cmd");
+	raised := 0;
+	ichan := chan of int;
+	spawn inputmon(fd, top, ichan);
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	tkclient->wmctl(top, "task");
+	for (;;) alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		s := <-top.ctxt.ctl or
+		s = <-top.wreq or
+		s = <-wmchan =>
+		case s {
+		"task" =>
+			raised = 0;
+		"untask" =>
+			raised = 1;
+		}
+		tkclient->wmctl(top, s);
+	e := <-ichan =>
+		if (e == 0 && eflag) {
+			tkclient->wmctl(top, "exit");
+			exit;
+		}
+		if (!raised && popup)
+			tkclient->wmctl(top, "untask");
+	msg := <-cmd =>
+		case msg {
+		"scroll" =>
+			scrolling = int tk->cmd(top, "variable scroll");
+		"popup" =>
+			popup = int tk->cmd(top, "variable popup");
+		}
+	}
+}
+
+inputmon(fd: ref Sys->FD, top: ref Tk->Toplevel, ichan: chan of int)
+{
+	buf := array[Sys->ATOMICIO] of byte;
+	t := 0;
+	while ((n := sys->read(fd, buf[t:], len buf-t)) > 0) {
+		t += n;
+		cl := 0;
+		for (i := t - 1; i >= 0; i--) {
+			(nil, cl, nil) = sys->byte2char(buf, i);
+			if (cl > 0)
+				break;
+		}
+		if (cl == 0)
+			continue;
+		logmsg(top, ichan, string buf[0:i+cl]);
+		buf[0:] = buf[i+cl:t];
+		t -= i + cl;
+	}
+	if (n < 0)
+		logmsg(top, ichan, sys->sprint("Input error: %r\n"));
+	else
+		logmsg(top, ichan, "Got EOF\n");
+	if (eflag)
+		ichan <-= 0;
+}
+
+logmsg(top: ref Tk->Toplevel, ichan: chan of int, m: string)
+{
+	tk->cmd(top, ".t.t insert end '"+m);
+	if (scrolling)
+		tk->cmd(top, ".t.t see end");
+	tk->cmd(top, "update");
+	ichan <-= 1;
+}
--- /dev/null
+++ b/appl/wm/man.b
@@ -1,0 +1,780 @@
+implement WmMan;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Font: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "plumbmsg.m";
+include "man.m";
+	man: Man;
+
+WmMan: module {
+	init: fn (ctxt: ref Draw->Context, argv: list of string);
+};
+
+window: ref Tk->Toplevel;
+
+W: adt {
+	textwidth: fn(nil: self ref W, text: Text): int;
+};
+
+ROMAN: con "/fonts/lucidasans/unicode.7.font";
+BOLD: con "/fonts/lucidasans/typelatin1.7.font";
+ITALIC: con "/fonts/lucidasans/italiclatin1.7.font";
+HEADING1: con "/fonts/lucidasans/boldlatin1.7.font";
+HEADING2: con "/fonts/lucidasans/italiclatin1.7.font";
+rfont, bfont, ifont, h1font, h2font: ref Font;
+
+GOATTR: con Parseman->ATTR_LAST << iota;
+MANPATH: con "/man/1/man";
+INDENT: con 40;
+
+metrics: Parseman->Metrics;
+parser: Parseman;
+Text: import parser;
+
+
+tkconfig := array [] of {
+	"frame .input",
+	"frame .view",
+	"text .view.t -state disabled -width 0 -height 0 -bg white -yscrollcommand {.view.yscroll set} -xscrollcommand {.view.xscroll set}",
+	"scrollbar .view.yscroll -orient vertical -command {.view.t yview}",
+	"scrollbar .view.xscroll -orient horizontal -command {.view.t xview}",
+	"entry .input.e -bg white",
+	"button .input.back -state disabled -bitmap small_color_left.bit -command {send nav b}",
+	"button .input.forward -state disabled -bitmap small_color_right.bit -command {send nav f}",
+
+	"pack .input.back .input.forward -side left -anchor w",
+	"pack .input.e -expand 1 -fill x",
+
+ 	"pack .view.yscroll -fill y -side left",
+ 	"pack .view.t -expand 1 -fill both",
+	
+	"bind .input.e <Key-\n> {send nav e}",
+	"bind .input.e <Button-1> +{grab set .input.e}",
+	"bind .input.e <ButtonRelease-1> +{grab release .input.e}",
+	"bind .view.t <Button-1> +{grab set .view.t}",
+	"bind .view.t <ButtonRelease-1> +{grab release .view.t}",
+	"bind .view.t <ButtonRelease-3> {send plumb %x %y}",
+
+	"pack .input -fill x",
+	"pack .view -expand 1 -fill both",
+	"pack propagate . 0",
+	". configure -width 500 -height 500",
+	"focus .input.e",
+};
+
+History: adt {
+	prev: cyclic ref History;
+	next: cyclic ref History;
+	topline: string;
+	searchstart: string;
+	searchend: string;
+	pick {
+	Search =>
+		search: list of string;
+	Go =>
+		path: string;
+	}
+};
+
+history: ref History;
+
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	doplumb := 0;
+
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "man: no window context\n");
+		raise "fail:bad context";
+	}
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	draw = load Draw Draw->PATH;
+	if (draw == nil)
+		loaderr("Draw");
+
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		loaderr(Tk->PATH);
+
+	man = load Man Man->PATH;
+	if (man == nil)
+		loaderr(Man->PATH);
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		loaderr(Tkclient->PATH);
+
+	parser = load Parseman Parseman->PATH;
+	if (parser == nil)
+		loaderr(Parseman->PATH);
+	parser->init();
+
+	plumber := load Plumbmsg Plumbmsg->PATH;
+	if (plumber != nil) {
+		if (plumber->init(1, nil, 0) >= 0)
+			doplumb = 1;
+	}
+
+	argv = tl argv;
+
+	rfont = Font.open(ctxt.display, ROMAN);
+	bfont = Font.open(ctxt.display, BOLD);
+	ifont = Font.open(ctxt.display, ITALIC);
+	h1font = Font.open(ctxt.display, HEADING1);
+	h2font = Font.open(ctxt.display, HEADING2);
+
+	em := rfont.width("m");
+	en := rfont.width("n");
+	metrics = Parseman->Metrics(490, 80, em, en, 14, 40, 20);
+
+	tkclient->init();
+	buts := Tkclient->Resize | Tkclient->Hide;
+	winctl: chan of string;
+	(window, winctl) = tkclient->toplevel(ctxt, nil, "Man", buts);
+	nav := chan of string;
+	plumb := chan of string;
+	tk->namechan(window, nav, "nav");
+	tk->namechan(window, plumb, "plumb");
+	for(tc:=0; tc<len tkconfig; tc++)
+		tkcmd(window, tkconfig[tc]);
+	if ((err := tkcmd(window, "variable lasterror")) != nil) {
+		sys->fprint(sys->fildes(2), "man: tk initialization failed: %s\n", err);
+		raise "fail:tk";
+	}
+	fittoscreen(window);
+	tkcmd(window, "update");
+	mktags();
+
+	vw := int tkcmd(window, ".view.t cget -actwidth") - 10;
+	if (vw <= 0)
+		vw = 1;
+	metrics.pagew = vw;
+
+	linechan := chan of list of (int, Text);
+	man->loadsections(nil);
+
+	pidc := chan of int;
+
+	if (argv != nil) {
+		if (hd argv == "-f") {
+			first: ref History;
+			for (argv = tl argv; argv != nil; argv = tl argv) {
+				hnode := ref History.Go(history, nil, "", "", "", hd argv);
+				if (history != nil)
+					history.next = hnode;
+				history = hnode;
+				if (first == nil)
+					first = history;
+			}
+			history = first;
+		} else
+			history = ref History.Search(nil, nil, "", "", "", argv);
+	}
+
+	if (history == nil)
+		history = ref History.Go(nil, nil, "", "", "", MANPATH);
+
+	setbuttons();
+	spawn printman(pidc, linechan, history);
+	layoutpid := <- pidc;
+	tkclient->onscreen(window, nil);
+	tkclient->startinput(window, "kbd"::"ptr"::nil);
+	for (;;) alt {
+	s := <-window.ctxt.kbd =>
+		tk->keyboard(window, s);
+	s := <-window.ctxt.ptr =>
+		tk->pointer(window, *s);
+	s := <-window.ctxt.ctl or
+	s = <-window.wreq or
+	s = <-winctl =>
+		e := tkclient->wmctl(window, s);
+		if (e == nil && s[0] == '!') {
+			topline := tkcmd(window, ".view.t yview");
+			(nil, toptoks) := sys->tokenize(topline, " ");
+			if (toptoks != nil)
+				history.topline = hd toptoks;
+			vw = int tkcmd(window, ".view.t cget -actwidth") - 10;
+			if (vw <= 0)
+				vw = 1;
+			if (vw != metrics.pagew) {
+				if (layoutpid != -1)
+					kill(layoutpid);
+				metrics.pagew = vw;
+				tkcmd(window, ".view.t delete 1.0 end");
+				tkcmd(window, "update");
+				spawn printman(pidc, linechan, history);
+				layoutpid = <- pidc;
+			}
+		}
+	line := <- linechan =>
+		if (line == nil) {
+			# layout done
+			if (history.topline != "") {
+				topline := tkcmd(window, ".view.t yview");
+				(nil, toptoks) := sys->tokenize(topline, " ");
+				if (toptoks != nil)
+					if (hd toptoks == "0")
+						tkcmd(window, ".view.t yview moveto " + history.topline);
+			}
+			tkcmd(window, "update");
+		} else
+			setline(line);
+	go := <- nav =>
+		topline := tkcmd(window, ".view.t yview");
+		(nil, toptoks) := sys->tokenize(topline, " ");
+		if (toptoks != nil)
+			history.topline = hd toptoks;
+		case go[0] {
+		'f' =>
+			# forward
+			history = history.next;
+			setbuttons();
+			if (layoutpid != -1)
+				kill(layoutpid);
+			tkcmd(window, ".view.t delete 1.0 end");
+			tkcmd(window, "update");
+			spawn printman(pidc, linechan, history);
+			layoutpid = <- pidc;
+		'b' =>
+			# back
+			history = history.prev;
+			setbuttons();
+			if (layoutpid != -1)
+				kill(layoutpid);
+			tkcmd(window, ".view.t delete 1.0 end");
+			tkcmd(window, "update");
+			spawn printman(pidc, linechan, history);
+			layoutpid = <- pidc;
+		'e' or 'l' =>
+			t := "";
+			if (go[0] == 'l') {
+				# link
+				t = go[1:];
+			} else {
+				# entry
+				t = tkcmd(window, ".input.e get");
+				for (i := 0; i < len t; i++)
+					if (!(t[i] == ' ' || t[i] == '\t'))
+						break;
+				if (i == len t)
+					break;
+				t = t[i:];
+				if (t[0] == '/' || t[0] == '?') {
+					search(t);
+					break;
+				}
+			}
+			(n, toks) := sys->tokenize(t, " \t");
+			if (n == 0)
+				continue;
+			h := ref History.Search(history, nil, "", "", "", toks);
+			history.next = h;
+			history = h;
+			setbuttons();
+			if (layoutpid != -1)
+				kill(layoutpid);
+			tkcmd(window, ".view.t delete 1.0 end");
+			tkcmd(window, "update");
+			spawn printman(pidc, linechan, history);
+			layoutpid = <- pidc;
+		'g' =>
+			# goto file
+			h := ref History.Go(history, nil, "", "", "", go[1:]);
+			history.next = h;
+			history = h;
+			setbuttons();
+			if (layoutpid != 0)
+				kill(layoutpid);
+			tkcmd(window, ".view.t delete 1.0 end");
+			tkcmd(window, "update");
+			spawn printman(pidc, linechan, history);
+			layoutpid = <- pidc;
+		}
+	p := <- plumb =>
+		if (!doplumb)
+			break;
+		(nil, l) := sys->tokenize(p, " ");
+		x := int hd l;
+		y := int hd tl l;
+		index := tkcmd(window, ".view.t index @"+string x+","+string y);		
+		selindex := tkcmd(window, ".view.t tag ranges sel");
+		insel := 0;
+		if(selindex != "")
+			insel = tkcmd(window, ".view.t compare sel.first <= "+index)=="1" &&
+				tkcmd(window, ".view.t compare sel.last >= "+index)=="1";
+		text := "";
+		attr := "";
+		if (insel)
+			text = tkcmd(window, ".view.t get sel.first sel.last");
+		else{
+			# have line with text in it
+			# now extract whitespace-bounded string around click
+			(nil, w) := sys->tokenize(index, ".");
+			charno := int hd tl w;
+			left := tkcmd(window, ".view.t index {"+index+" linestart}");
+			right := tkcmd(window, ".view.t index {"+index+" lineend}");
+			line := tkcmd(window, ".view.t get "+left+" "+right);
+			for(i:=charno; i>0; --i)
+				if(line[i-1]==' ' || line[i-1]=='\t')
+					break;
+			for(j:=charno; j<len line; j++)
+				if(line[j]==' ' || line[j]=='\t')
+					break;
+			text = line[i:j];
+			attr = "click="+string (charno-i);
+		}
+		msg := ref Plumbmsg->Msg(
+			"WmMan",
+			"",
+			"",
+			"text",
+			attr,
+			array of byte text);
+		plumber->msg.send();
+
+	layoutpid = <- pidc =>
+		;
+	}
+}
+
+search(pat: string)
+{
+	dir: string;
+	start: string;
+	if (pat[0] == '/') {
+		dir = "-forwards";
+		start = history.searchend;
+	} else {
+		dir = "-backwards";
+		start = history.searchstart;
+	}
+	pat = pat[1:];
+	if (start == "")
+		start = "1.0";
+	r := tkcmd(window, ".view.t search " + dir + " -- " + tk->quote(pat) + " " + start);
+	if (r != nil) {
+		history.searchstart = r;
+		history.searchend = r + "+" + string len pat + "c";
+		tkcmd(window, ".view.t tag remove sel 1.0 end");
+		tkcmd(window, ".view.t tag add sel " + history.searchstart + " " + history.searchend);
+		tkcmd(window, ".view.t see " + r);
+		tkcmd(window, "update");
+	}
+}
+
+setbuttons()
+{
+	if (history.prev == nil)
+		tkcmd(window, ".input.back configure -state disabled");
+	else
+		tkcmd(window, ".input.back configure -state normal");
+	if (history.next == nil)
+		tkcmd(window, ".input.forward configure -state disabled");
+	else
+		tkcmd(window, ".input.forward configure -state normal");
+}
+
+dolayout(linechan: chan of list of (int, Text), path: string)
+{
+	fd := sys->open(path, Sys->OREAD);
+	if (fd == nil) {
+		layouterror(linechan, sys->sprint("cannot open file %s: %r", path));
+		return;
+	}
+	w: ref W;
+	parser->parseman(fd, metrics, 0, w, linechan);
+}
+
+printman(pidc: chan of int, linechan: chan of list of (int, Text), h: ref History)
+{
+	pidc <-= sys->pctl(0, nil);
+	args: list of string;
+	pick hp := h {
+		Search =>
+			args = hp.search;
+		Go =>
+			dolayout(linechan, hp.path);
+			pidc <-= -1;
+			return;
+	}
+	sections: list of string;
+	argstext := "";
+	addsections := 1;
+	keywords: list of string;
+	for (; args != nil; args = tl args) {
+		arg := hd args;
+		if (arg == nil)
+			continue;
+		if (addsections && !isint(trimdot(arg))) {
+			addsections = 0;
+			keywords = args;
+		}
+		if (addsections)
+			sections = arg :: sections;
+		argstext = argstext + " " + arg;
+	}
+	manpages := man->getfiles(sections, keywords);
+	pagelist := sortpages(manpages);
+	if (len pagelist == 1) {
+		(nil, path, nil) := hd pagelist;
+		dolayout(linechan, path);
+		pidc <-= -1;
+		return;
+	}
+
+	tt := Text(Parseman->FONT_ROMAN, 0, "Search:", 1, nil);
+	at := Text(Parseman->FONT_BOLD, 0, argstext, 0, nil);
+	linechan <-= (0, tt)::(0, at)::nil;
+	tt.text = "";
+	linechan <-= (0, tt)::nil;
+
+	if (pagelist == nil) {
+		donet := Text(Parseman->FONT_ROMAN, 0, "No matches", 0, nil);
+		linechan <-= (INDENT, donet) :: nil;
+		linechan <-= nil;
+		pidc <-= -1;
+		return;
+	}
+
+	linelist: list of list of Text;
+	pathlist: list of Text;
+	
+	maxkwlen := 0;
+	comma := Text(Parseman->FONT_ROMAN, 0, ", ", 0, "");
+	for (; pagelist != nil; pagelist = tl pagelist) {
+		(n, p, kwl) := hd pagelist;
+		l := 0;
+		keywords: list of Text = nil;
+		for (; kwl != nil; kwl = tl kwl) {
+			kw := hd kwl;
+			kwt := Text(Parseman->FONT_ITALIC, GOATTR, kw, 0, p);
+			nt := Text(Parseman->FONT_ROMAN, GOATTR, "(" + string n + ")", 0, p);
+			l += textwidth(kwt) + textwidth(nt);
+			if (keywords != nil) {
+				l += textwidth(comma);
+				keywords = nt :: kwt :: comma :: keywords;
+			} else
+				keywords = nt :: kwt :: nil;
+		}
+		if (l > maxkwlen)
+			maxkwlen = l;
+		linelist = keywords :: linelist;
+		ptext := Text(Parseman->FONT_ROMAN, GOATTR, p, 0, "");
+		pathlist = ptext :: pathlist;
+	}
+
+	for (; pathlist != nil; (pathlist, linelist) = (tl pathlist, tl linelist)) {
+		line := (10 + INDENT + maxkwlen, hd pathlist) :: nil;
+		for (ll := hd linelist; ll != nil; ll = tl ll) {
+			litem := hd ll;
+			if (tl ll == nil)
+				line = (INDENT, litem) :: line;
+			else
+				line = (0, litem) :: line;
+		}
+		linechan <-= line;
+	}
+	linechan <-= nil;
+	pidc <-= -1;
+}
+
+layouterror(linechan: chan of list of (int, Text), msg: string)
+{
+	text := "ERROR: " + msg;
+	t := Text(Parseman->FONT_ROMAN, 0, text, 0, nil);
+	linechan <-= (0, t)::nil;
+	linechan <-= nil;
+}
+
+loaderr(modname: string)
+{
+	sys->print("cannot load %s module: %r\n", modname);
+	raise "fail:init";
+}
+
+W.textwidth(nil: self ref W, text: Text): int
+{
+	return textwidth(text);
+}
+
+textwidth(text: Text): int
+{
+	f: ref Font;
+	if (text.heading == 1)
+		f = h1font;
+	else if (text.heading == 2)
+		f = h2font;
+	else {
+		case text.font {
+		Parseman->FONT_ROMAN =>
+			f = rfont;
+		Parseman->FONT_BOLD =>
+			f = bfont;
+		Parseman->FONT_ITALIC =>
+			f = ifont;
+		* =>
+			return 8 * len text.text;
+		}
+	}
+	return draw->f.width(text.text);
+}
+
+lnum := 0;
+
+setline(line: list of (int, Text))
+{
+	tabstr := "";
+	linestr := "";
+	lastoff := 0;
+	curfont := Parseman->FONT_ROMAN;
+	curlink := "";
+	curgtag := "";
+	curheading := 0;
+	fonttext := "";
+
+	for (l := line; l != nil; l = tl l) {
+		(offset, nil) := hd l;
+		if (offset != 0) {
+			lastoff = offset;
+			if (tabstr != "")
+				tabstr[len tabstr] = ' ';
+			tabstr = tabstr + string offset;
+		}
+	}
+	# fudge up tabs for rest of line
+	if (lastoff != 0)
+		tabstr = tabstr + " " + string lastoff + " " + string (lastoff + INDENT);
+	ttag := "";
+	gtag := "";
+	if (tabstr != nil)
+		ttag = tabtag(tabstr) + " ";
+
+	for (l = line; l != nil; l = tl l) {
+		(offset, text) := hd l;
+		gtag = "";
+		if (text.link != nil) {
+			if (text.attr & GOATTR)
+				gtag = gotag(text.link) + " ";
+			else {
+				gtag = linktag(text.link) + " ";
+			}
+		}
+		if (offset != 0)
+			fonttext[len fonttext] = '\t';
+		if (text.font != curfont || text.link != curlink || text.heading != curheading || gtag != curgtag) {
+			# need to change tags
+			linestr = linestr + " " + tk->quote(fonttext) + " {" + ttag + curgtag + fonttag(curfont, curheading) + "}";
+			ttag = "";
+			curgtag = gtag;
+			fonttext = "";
+			curfont = text.font;
+			curlink = text.link;
+			curheading = text.heading;
+		}
+		fonttext = fonttext + text.text;
+	}
+	if (fonttext != nil)
+		linestr = linestr + " " + tk->quote(fonttext) + " {" + ttag + curgtag + fonttag(curfont, curheading) + "}";
+	tkcmd(window, ".view.t insert end " + linestr);
+	tkcmd(window, ".view.t insert end {\n}");
+	# only update on every other line
+	if (lnum++ & 1)
+		tkcmd(window, "update");
+}
+
+mktags()
+{
+	tkcmd(window, ".view.t tag configure ROMAN -font " + ROMAN);
+	tkcmd(window, ".view.t tag configure BOLD -font " + BOLD);
+	tkcmd(window, ".view.t tag configure ITALIC -font " + ITALIC);
+	tkcmd(window, ".view.t tag configure H1 -font " + HEADING1);
+	tkcmd(window, ".view.t tag configure H2 -font " + HEADING2);
+}
+
+fonttag(font, heading: int): string
+{
+	if (heading == 1)
+		return "H1";
+	if (heading == 2)
+		return "H2";
+	case font {
+	Parseman->FONT_ROMAN =>
+		return "ROMAN";
+	Parseman->FONT_BOLD =>
+		return "BOLD";
+	Parseman->FONT_ITALIC =>
+		return "ITALIC";
+	}
+	return nil;
+}
+
+nexttag := 0;
+lasttabstr := "";
+lasttagname := "";
+
+tabtag(tabstr: string): string
+{
+	if (tabstr == lasttabstr)
+		return lasttagname;
+	lasttagname = "TAB" + string nexttag++;
+	lasttabstr = tabstr;
+	tkcmd(window, ".view.t tag configure " + lasttagname + " -tabs " + tk->quote(tabstr));
+	return lasttagname;
+}
+
+# optimise this!
+gotag(path: string): string
+{
+	cmd := "{send nav g" + path + "}";
+	name := "GO" + string nexttag++;
+	tkcmd(window, ".view.t tag bind " + name + " <ButtonRelease-1> +" + cmd);
+	tkcmd(window, ".view.t tag configure " + name + " -fg green");
+	return name;
+}
+
+# and this!
+linktag(search: string): string
+{
+	cmd := tk->quote("send nav l" + search);
+	name := "LN" + string nexttag++;
+	tkcmd(window, ".view.t tag bind " + name + " <ButtonRelease-1> +" + cmd);
+	tkcmd(window, ".view.t tag configure " + name + " -fg green");
+	return name;
+}
+
+isint(s: string): int
+{
+	for (i := 0; i < len s; i++)
+		if (s[i] < '0' || s[i] > '9')
+			return 0;
+	return 1;
+}
+
+kill(pid: int)
+{
+	fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
+	if (fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+revsortuniq(strlist: list of string): list of string
+{
+	strs := array [len strlist] of string;
+	for (i := 0; strlist != nil; (i, strlist) = (i+1, tl strlist))
+		strs[i] = hd strlist;
+
+	# simple sort (ascending)
+	for (i = 0; i < len strs - 1; i++) {
+		for (j := i+1; j < len strs; j++)
+			if (strs[i] < strs[j])
+				(strs[i], strs[j]) = (strs[j], strs[i]);
+	}
+
+	# construct list (result is descending)
+	r: list of string;
+	prev := "";
+	for (i = 0; i < len strs; i++) {
+		if (strs[i] != prev) {
+			r = strs[i] :: r;
+			prev = strs[i];
+		}
+	}
+	return r;
+}
+
+sortpages(pagelist: list of (int, string, string)): list of (int, string, list of string)
+{
+	pages := array [len pagelist] of (int, string, string);
+	for (i := 0; pagelist != nil; (i, pagelist) = (i+1, tl pagelist))
+		pages[i] = hd pagelist;
+
+	for (i = 0; i < len pages - 1; i++) {
+		for (j := i+1; j < len pages; j++) {
+			(nil, nil, ipath) := pages[i];
+			(nil, nil, jpath) := pages[j];
+			if (ipath > jpath)
+				(pages[i], pages[j]) = (pages[j], pages[i]);
+		}
+	}
+
+	r: list of (int, string, list of string);
+	filecmds: list of string;
+	lastfile := "";
+	lastsect := 0;
+	for (i = 0; i < len pages; i++) {
+		(section, cmd, file) := pages[i];
+		if (lastfile == "") {
+			lastfile = file;
+			lastsect = section;
+		}
+
+		if (file != lastfile) {
+			r = (lastsect, lastfile, filecmds) :: r;
+			lastfile = file;
+			lastsect = section;
+			filecmds = nil;
+		}
+		filecmds = cmd :: filecmds;
+	}
+	if (filecmds != nil)
+		r = (lastsect, lastfile, revsortuniq(filecmds)) :: r;
+	return r;
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	Point, Rect: import draw;
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+	bd := int tkcmd(win, ". cget -bd");
+	winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2,
+				int tkcmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.max.x - dx, r.max.x);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.max.y - dy, r.max.y);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
+
+tkcmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->print("tk error %s on '%s'\n", e, s);
+	return e;
+}
+
+trimdot(s: string): string
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == '.')
+			return s[0: i];
+	return s;
+}
--- /dev/null
+++ b/appl/wm/mand.b
@@ -1,0 +1,860 @@
+implement Mand;
+
+#
+# Copyright © 2000 Vita Nuova Limited. All rights reserved.
+#
+
+# mandelbrot/julia fractal browser:
+# button 1 - drag a rectangle to zoom into
+# button 2 - (from mandel only) show julia at point
+# button 3 - zoom out
+
+include "sys.m";
+	sys : Sys;
+include "draw.m";
+	draw : Draw;
+	Point, Rect, Image, Context, Screen, Display : import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+
+Mand : module
+{
+	init : fn(nil : ref Context, argv : list of string);
+};
+
+colours: array of ref Image;
+stderr : ref Sys->FD;
+
+FIX: type big;
+
+Calc: adt {
+	xr, yr: array of FIX;
+	parx, pary: FIX;
+	# column order
+	dispbase: array of COL;		# auxiliary display and border
+	imgch: chan of (ref Image, Rect);
+	img: ref Image;
+	maxx, maxy, supx, supy: int;
+	disp: int;					# origin of auxiliary display
+	morj : int;
+	winr: Rect;
+	kdivisor: int;
+	pointsdone: int;
+};
+
+# BASE, LIMIT, MAXCOUNT, MINDELTA may be varied
+
+#
+#	calls with 256X128 on initial set
+#	---------------------------------
+#	crawl		58	(5% of time)
+#	fillline	894	(6% of time)
+#	isblank		5012	(0% of time)
+#	mcount		6928	(55% of time)
+#	getcolour	52942	(11% of time)
+#	displayset	1	(15% of time)
+#
+WHITE : con 16r0;
+BLACK : con 16rff;
+
+COL : type byte;
+
+BASE	: con 60;		# 28
+HBASE : con (BASE/2);
+SCALE : con (big 1<<BASE);
+TWO	: con (big 1<<(BASE+1));
+FOUR : con (big 1<<(BASE+2));
+NEG	: con (~((big 1<<(32-HBASE))-big 1));
+MINDELTA : con (big 1<<(HBASE-1));		# (1<<(HBASE-2))
+
+SCHEDCOUNT: con 100;
+
+BLANK : con 0;		# blank pixel
+BORDER : con 255;	# border pixel
+LIMIT : con 4;		# 4 or 5
+
+# pointcolour() returns values in the range 1..MAXCOUNT+1
+# these must not clash with 0 or 255
+# hence 0 <= MAXCOUNT <= 253
+#
+MAXCOUNT : con 253;		# 92  64
+
+# colour cube
+R, G, B : int;
+
+# initial width and height
+WIDTH: con 400;
+HEIGHT: con 400;
+
+Fracpoint: adt {
+	x, y: real;
+};
+
+Fracrect: adt {
+	min, max: Fracpoint;
+	dx:	fn(r: self Fracrect): real;
+	dy:	fn(r: self Fracrect): real;
+};
+
+Params: adt {
+	r: Fracrect;
+	p: Fracpoint;
+	m: int;
+	kdivisor: int;
+	fill: int;
+};
+
+Usercmd: adt {
+	pick {
+	Zoomin =>
+		r: Rect;
+	Julia =>
+		p: Point;
+	Zoomout or
+	Restart =>
+		# nothing
+	}
+};
+
+badmod(mod: string)
+{
+	sys->fprint(stderr, "mand: cannot load %s: %r\n", mod);
+	raise "fail:bad module";
+}
+
+win_config := array[] of {
+	"frame .f",
+	"label .f.dl -text Depth",
+	"entry .f.depth",
+	".f.depth insert 0 1",
+	"checkbutton .f.fill -text {Fill} -command {send cmd fillchanged} -variable fill",
+	".f.fill select",
+	"pack .f.dl -side left",
+	"pack .f.fill -side right",
+	"pack .f.depth -side top -fill x",
+	"frame .c -bd 3 -relief sunken -width " + string WIDTH + " -height " + string HEIGHT,
+	"pack .f -side top -fill x",
+	"pack .c -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+	"bind .c <Button-1> {send cmd b1 %x %y}",
+	"bind .c <ButtonRelease-2> {send cmd b2 %x %y}",
+	"bind .c <ButtonRelease-1> {send cmd b1r %x %y}",
+	"bind .c <ButtonRelease-3> {send cmd b3 %x %y}",
+
+	"bind .f.depth <Key-\n> {send cmd setkdivisor}",
+	"update",
+};
+
+mouseproc(win: ref Tk->Toplevel)
+{
+	for(;;)
+		tk->pointer(win, *<-win.ctxt.ptr);
+}
+
+init(ctxt: ref Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil) badmod(Tkclient->PATH);
+
+	tkclient->init();
+	if (ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+	(win, wmcmd) := tkclient->toplevel(ctxt, "", "Fractals", Tkclient->Appl);
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	cmdch := chan of string;
+	tk->namechan(win, cmdch, "cmd");
+	for (i := 0; i < len win_config; i++)
+		cmd(win, win_config[i]);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	fittoscreen(win);
+	cmd(win, "update");
+	spawn mouseproc(win);
+
+	R = G = B = 6;
+	argv = tl argv;
+	if (argv != nil) { (R, argv) = (int hd argv, tl argv); if (R <= 0) R = 1; }
+	if (argv != nil) { (G, argv) = (int hd argv, tl argv); if (G <= 0) G = 1; }
+	if (argv != nil) { (B, argv) = (int hd argv, tl argv); if (B <= 0) B = 1; }
+	colours = array[256] of ref Image;
+	for (i = 0; i < len colours; i++)
+		# colours[i] = ctxt.display.color(i);
+		colours[i] = ctxt.display.rgb(col(i/(G*B), R),
+							    col(i/(1*B), G),
+							    col(i/(1*1), B));
+	canvr := canvposn(win);
+	specr := Fracrect((-2.0, -1.5), (1.0, 1.5));
+	p := Params(
+			correctratio(specr, canvr),
+			(0.0, 0.0),
+			1,			# m
+			1,			# kdivisor
+			int cmd(win, "variable fill")
+		);
+	pid := -1;
+	sync := chan of int;
+	imgch := chan of (ref Image, Rect);
+	spawn docalculate(sync, p, imgch);
+	pid = <-sync;
+	imgch <-= (win.image, canvr);
+
+	stack: list of (Fracrect, Params);
+	for(;;){
+		restart := 0;
+		alt {
+		s := <-win.ctxt.kbd =>
+			tk->keyboard(win, s);
+		c := <-win.ctxt.ctl or
+		c = <-win.wreq or
+		c = <-wmcmd =>
+			if(c[0] == '!'){
+				if(pid != -1)
+					restart = winreq(win, c, imgch, sync);
+				else
+					restart = winreq(win, c, nil, nil);
+			}else{
+				tkclient->wmctl(win, c);
+				if(c == "task" && pid != -1){
+					kill(pid);
+					pid = -1;
+				}
+			}
+		press := <-cmdch =>
+			(nil, toks) := sys->tokenize(press, " ");
+			ucmd: ref Usercmd = nil;
+			case hd toks {
+			"start" =>
+				ucmd = ref Usercmd.Restart;
+			"b1" or "b2" or "b3" =>
+				#cmd(win, "grab set .c");
+				#fiximage(win);
+				ucmd = trackmouse(win, cmdch, hd toks, Point(int hd tl toks, int hd tl tl toks));
+				#cmd(win, "grab release .c");
+			"fillchanged" =>
+				p.fill = int cmd(win, "variable fill");
+				ucmd = ref Usercmd.Restart;
+			"setkdivisor" =>
+				p.kdivisor = int cmd(win, ".f.depth get");
+				if (p.kdivisor < 1)
+					p.kdivisor = 1;
+				ucmd = ref Usercmd.Restart;
+			}
+			if (ucmd != nil) {
+				pick u := ucmd {
+				Zoomin =>
+					# sys->print("zoomin to %s\n", r2s(u.r));
+					if (u.r.dx() > 0 && u.r.dy() > 0) {
+						stack = (specr, p) :: stack;
+						specr.min = pt2real(u.r.min, win, p.r);
+						specr.max = pt2real(u.r.max, win, p.r);
+						(specr.min.y, specr.max.y) = (specr.max.y, specr.min.y);	# canonicalise
+						restart = 1;
+					}
+				Zoomout =>
+					if (stack != nil) {
+						((specr, p), stack) = (hd stack, tl stack);
+						cmd(win, ".f.depth delete 0 end");
+						cmd(win, ".f.depth insert 0 " + string p.kdivisor);
+						if (p.fill)
+							cmd(win, ".f.fill select");
+						else
+							cmd(win, ".f.fill deselect");
+						cmd(win, "update");
+						restart = 1;
+					}
+				Julia =>
+					# pt := pt2real(u.p, win, p.r);
+					if (p.m) {
+						stack = (specr, p) :: stack;
+						p.p = pt2real(u.p, win, p.r);
+						specr = ((-2.0, -1.5), (1.0, 1.5));
+						p.m = 0;
+						restart = 1;
+					}
+				Restart =>
+					restart = 1;
+				}
+			}
+		<-sync =>
+			win.image.flush(Draw->Flushon);
+			pid = -1;
+		}
+		if (restart) {
+			if (pid != -1)
+				kill(pid);
+			win.image.flush(Draw->Flushoff);
+			wr := canvposn(win);
+			if(!isempty(wr)){
+				p.r = correctratio(specr, wr);
+				sync = chan of int;
+				spawn docalculate(sync, p, imgch);
+				pid = <-sync;
+				imgch <-= (win.image, wr);
+			}
+		}
+	}
+}
+
+winreq(win: ref Tk->Toplevel, c: string, imgch: chan of (ref Image, Rect), terminated: chan of int): int
+{
+	oldimage := win.image;
+	if (imgch != nil) {
+		# halt calculation process
+		alt {
+		imgch <-= (nil, ((0,0), (0,0))) =>;
+		<-terminated =>
+			imgch = nil;
+		}
+	}
+	tkclient->wmctl(win, c);
+	if(win.image != oldimage)
+		return 1;
+	if(imgch != nil)
+		imgch <-= (win.image, canvposn(win));
+	return 0;
+}
+
+correctratio(r: Fracrect, wr: Rect): Fracrect
+{
+	# make sure calculation rectangle is in
+	# the same ratio as bitmap (also make sure that
+	# calculated area always includes desired area)
+	if(isempty(wr))
+		return ((0.0,0.0), (0.0,0.0));
+	(btall, atall) := (real wr.dy() / real wr.dx(), r.dy() / r.dx());
+	if (btall > atall) {
+		# bitmap is taller than area, so expand area vertically
+		excess := r.dx()*btall - r.dy();
+		r.min.y -= excess / 2.0;
+		r.max.y += excess / 2.0;
+	} else {
+		# area is taller than bitmap, so expand area horizontally
+		excess := r.dy()/btall - r.dx();
+		r.min.x -= excess / 2.0;
+		r.max.x += excess / 2.0;
+	}
+	return r;
+}
+
+pt2real(pt: Point, win: ref Tk->Toplevel, r: Fracrect): Fracpoint
+{
+	sz := Point(int cmd(win, ".c cget -actwidth"), int cmd(win, ".c cget -actheight"));
+	return (real pt.x / real sz.x * (r.max.x- r.min.x) + r.min.x,
+			real (sz.y - pt.y) / real sz.y * (r.max.y - r.min.y) + r.min.y);
+}
+
+pt2s(pt: Point): string
+{
+	return string pt.x + " " + string pt.y;
+}
+
+r2s(r: Rect): string
+{
+	return pt2s(r.min) + " " + pt2s(r.max);
+}
+
+trackmouse(win: ref Tk->Toplevel, cmdch: chan of string, but: string, p: Point): ref Usercmd
+{
+	case but {
+	"b1" =>
+		cr := canvposn(win);
+		display := win.image.display;
+		save := display.newimage(cr, win.image.chans, 0, Draw->Nofill);
+		save.draw(cr, win.image, nil, cr.min);
+		oclip := win.image.clipr;
+		win.image.clipr = cr;
+
+		p = p.add(cr.min);
+		r := Rect(p, p);
+		win.image.border(r, 1, display.white, (0, 0));
+		win.image.flush(Draw->Flushnow);
+		do {
+			but = <-cmdch;
+			(nil, toks) := sys->tokenize(but, " ");
+			but = hd toks;
+			if(but == "b1"){
+				xr := r.canon();
+				win.image.draw(xr, save, nil, xr.min);
+				(r.max.x, r.max.y) = (int hd tl toks + cr.min.x, int hd tl tl toks + cr.min.y);
+				win.image.border(r.canon(), 1, display.white, (0, 0));
+				win.image.flush(Draw->Flushnow);
+			}
+		} while (but != "b1r");
+		r = r.canon();
+		win.image.draw(r, save, nil, r.min);
+		win.image.clipr = oclip;
+		r = r.subpt(cr.min);
+		return ref Usercmd.Zoomin(r);
+	"b2" =>
+		return ref Usercmd.Julia(p);
+	"b3" =>
+		return ref Usercmd.Zoomout;
+	}
+	return nil;
+}
+
+poll(calc: ref Calc)
+{
+	calc.img.flush(Draw->Flushnow);
+	alt {
+	<-calc.imgch =>
+		calc.img = nil;
+		(calc.img, calc.winr) = <-calc.imgch;
+	* =>;
+	}
+}
+
+docalculate(sync: chan of int, p: Params, imgch: chan of (ref Image, Rect))
+{
+	if (p.m)
+		; # sys->print("mandel [[%g,%g],[%g,%g]]\n", r.min.x, r.min.y, r.max.x, r.max.y);
+	else
+		; # sys->print("julia  [[%g,%g],[%g,%g]] [%g,%g]\n", r.min.x, r.min.y, r.max.x, r.max.y, p.p.x, p.p.y);
+	sync <-= sys->pctl(0, nil);
+	calculate(p, imgch);
+	sync <-= 0;
+}
+
+canvposn(win: ref Tk->Toplevel): Rect
+{
+	return tk->rect(win, ".c", Tk->Local);
+}
+
+isempty(r: Rect): int
+{
+	return r.dx() <= 0 || r.dy() <= 0;
+}
+
+calculate(p: Params, imgch: chan of (ref Image, Rect))
+{
+	calc := ref Calc;
+	(calc.img, calc.winr) = <-imgch;
+	r := calc.winr;
+	calc.maxx = r.dx();
+	calc.maxy = r.dy();
+	calc.supx = calc.maxx + 2;
+	calc.supy = calc.maxy + 2;
+	calc.imgch = imgch;
+	calc.xr = array[calc.maxx] of FIX;
+	calc.yr = array[calc.maxy] of FIX;
+	calc.morj = p.m;
+	initr(calc, p);
+	calc.img.drawop(r, calc.img.display.white, nil, (0,0), Draw->S);
+
+	if (p.fill) {
+		calc.dispbase = array[calc.supx*calc.supy] of COL;		# auxiliary display and border
+		calc.disp = calc.maxy + 3;
+		setdisp(calc);
+		displayset(calc);
+	} else {
+		for (x := 0; x < calc.maxx; x++) {
+			for (y := 0; y < calc.maxy; y++)
+				point(calc, calc.img, (x, y), pointcolour(calc, x, y));
+		}
+	}
+}
+ 
+setdisp(calc: ref Calc)
+{
+	d : int;
+	i : int;
+
+	for (i = 0; i < calc.supx*calc.supy; i++)
+		calc.dispbase[i] = byte BLANK;
+
+	i = 0;
+	for (d = 0; i < calc.supx; d += calc.supy) {
+		calc.dispbase[d] = byte BORDER;
+		i++;
+	}
+	i = 0;
+	for (d = 0; i < calc.supy; d++) {
+		calc.dispbase[d] = byte BORDER;
+		i++;
+	}
+	i = 0;
+	for (d = 0+calc.supx*calc.supy-1; i < calc.supx; d -= calc.supy) {
+		calc.dispbase[d] = byte BORDER;
+		i++;
+	}
+	i = 0;
+	for (d = 0+calc.supx*calc.supy-1; i < calc.supy; d--) {
+		calc.dispbase[d] = byte BORDER;
+		i++;
+	}
+}
+
+initr(calc: ref Calc, p: Params): int
+{
+	r := p.r;
+	dp := real2fix((r.max.x-r.min.x)/(real calc.maxx));
+	dq := real2fix((r.max.y-r.min.y)/(real calc.maxy));
+	calc.xr[0] = real2fix(r.min.x)-(big calc.maxx*dp-(real2fix(r.max.x)-real2fix(r.min.x)))/big 2;
+	for (x := 1; x < calc.maxx; x++)
+		calc.xr[x] = calc.xr[x-1] + dp;
+	calc.yr[0] = real2fix(r.max.y)+(big calc.maxy*dq-(real2fix(r.max.y)-real2fix(r.min.y)))/big 2;
+	for (y := 1; y < calc.maxy; y++)
+		calc.yr[y] = calc.yr[y-1] - dq;
+	calc.parx = real2fix(p.p.x);
+	calc.pary = real2fix(p.p.y);
+	calc.kdivisor = p.kdivisor;
+	calc.pointsdone = 0;
+	return dp >= MINDELTA && dq >= MINDELTA;
+}
+
+fillline(calc: ref Calc, x, y, d, dir, dird, col: int)
+{
+	x0 := x;
+
+	while (calc.dispbase[d] == byte BLANK) {
+		calc.dispbase[d] = byte col;
+		x -= dir;
+		d -= dird;
+	}
+	if (0 && pointcolour(calc, (x0+x+dir)/2, y) != col) {		# midpoint of line (island code)
+		# island - undo colouring or do properly
+		do {
+			d += dird;
+			x += dir;
+			# *d = BLANK;
+			calc.dispbase[d] = byte pointcolour(calc, x, y);
+			point(calc, calc.img, (x, y), int calc.dispbase[d]);
+		} while (x != x0);
+		return;				# abort crawl ?
+	}
+	horizline(calc, calc.img, x0, x, y, col);
+}
+ 
+crawlt(calc: ref Calc, x, y, d, col: int)
+{
+	yinc, dyinc : int;
+ 
+	firstd := d;
+	xinc := 1;
+	dxinc := calc.supy;
+ 
+	for (;;) {
+		if (getcolour(calc, x+xinc, y, d+dxinc) == col) {
+			x += xinc;
+			d += dxinc;
+			yinc = -xinc;
+			dyinc = -dxinc;
+			# if (isblank(x+xinc, y, d+dxinc))
+			if (calc.dispbase[d+dxinc] == byte BLANK)
+				fillline(calc, x+xinc, y, d+dxinc, yinc, dyinc, col);
+			if (d == firstd)
+				break;
+		}
+		else { 
+			yinc = xinc;
+			dyinc = dxinc;
+		}
+		if (getcolour(calc, x, y+yinc, d+yinc) == col) {
+			y += yinc;
+			d += yinc;
+			xinc = yinc;
+			dxinc = dyinc;
+			# if (isblank(x-xinc, y, d-dxinc))
+			if (calc.dispbase[d-dxinc] == byte BLANK)
+				fillline(calc, x-xinc, y, d-dxinc, yinc, dyinc, col);
+			if (d == firstd)
+				break;
+		}
+		else { 
+			xinc = -yinc;
+			dxinc = -dyinc;
+		}
+	}
+}
+
+# spurious lines problem - disallow all acw paths
+#
+#	43--------->
+#	12--------->
+#
+#	654------------>
+#	7 3------------>
+#	812------------>
+#
+
+# Given a closed curve completely described by unit movements LRUD (left,
+# right, up, and down), calculate the enclosed area.  The description
+# may be cw or acw and of arbitrary shape.
+#
+# Based on Green's Theorem :-  area = integral  ydx
+#					    C
+# area = 0;
+# count = ARBITRARY_VALUE;
+# while( moves_are_left() ){
+#     move = next_move();
+#    switch(move){
+#        case L:
+#            area -= count;
+#            break;
+#        case R:
+#            area += count;
+#            break;
+#        case U:
+#            count++;
+#            break;
+#        case D:
+#            count--;
+#            break;
+#    }
+#    area = abs(area);
+
+crawlf(calc: ref Calc, x, y, d, col: int)
+{
+	xinc, yinc, dxinc, dyinc : int;
+	firstx, firsty : int;
+	firstd : int;
+	area := 0;
+	count := 0;
+ 
+	firstx = x;
+	firsty = y;
+	firstd = d;
+	xinc = 1;
+	dxinc = calc.supy;
+ 
+	# acw on success, cw on failure
+	for (;;) {
+		if (getcolour(calc, x+xinc, y, d+dxinc) == col) {
+			x += xinc;
+			d += dxinc;
+			yinc = -xinc;
+			dyinc = -dxinc;
+			area += xinc*count;
+			if (d == firstd)
+				break;
+		} else { 
+			yinc = xinc;
+			dyinc = dxinc;
+		}
+		if (getcolour(calc, x, y+yinc, d+yinc) == col) {
+			y += yinc;
+			d += yinc;
+			xinc = yinc;
+			dxinc = dyinc;
+			count -= yinc;
+			if (d == firstd)
+				break;
+		} else { 
+			xinc = -yinc;
+			dxinc = -dyinc;
+		}
+	}
+	if (area > 0)	# cw
+		crawlt(calc, firstx, firsty, firstd, col);
+}
+
+displayset(calc: ref Calc)
+{
+	edge : int;
+	last := BLANK;
+	d := calc.disp;
+ 
+	for (x := 0; x < calc.maxx; x++) {
+		for (y := 0; y < calc.maxy; y++) {
+			col := calc.dispbase[d];
+			if (col == byte BLANK) {
+				col = calc.dispbase[d] = byte pointcolour(calc, x, y);
+				point(calc, calc.img, (x, y), int col);
+				if (col == byte last)
+					edge++;
+				else {
+					last = int col;
+					edge = 0;
+				}
+				if (edge >= LIMIT) {
+					crawlf(calc, x, y-edge, d-edge, last);
+					# prevent further crawlf()
+					last = BLANK;
+				}
+			}
+			else {
+				if (col == byte last)
+					edge++;
+				else {
+					last = int col;
+					edge = 0;
+				}
+			}
+			d++;
+		}
+		last = BLANK;
+		d += 2;
+	}
+}
+
+pointcolour(calc: ref Calc, x, y: int) : int
+{
+	if (++calc.pointsdone >= SCHEDCOUNT) {
+		calc.pointsdone = 0;
+		sys->sleep(0);
+		poll(calc);
+	}
+	if (calc.morj)
+		return mcount(calc, x, y) + 1;
+	else
+		return jcount(calc, x, y) + 1;
+}
+
+mcount(calc: ref Calc, x_coord, y_coord: int): int
+{
+	(p, q) := (calc.xr[x_coord], calc.yr[y_coord]);
+	(x, y) := (calc.parx, calc.pary);
+	k := 0;
+	maxcount := MAXCOUNT * calc.kdivisor;
+	while (k < maxcount) {
+		if (x >= TWO || y >= TWO || x <= -TWO || y <= -TWO)
+			break;
+
+		if (0) {
+			# x = (x < 0) ? (x>>HBASE)|NEG : x>>HBASE;
+			# y = (y < 0) ? (y>>HBASE)|NEG : y>>HBASE;
+		}
+
+		x >>= HBASE;
+		y >>= HBASE;
+		t := y*y;
+		y = big 2*x*y+q;	# possible unserious overflow when BASE == 28
+		x *= x;
+		if (x+t >= FOUR) 
+			break;
+		x -= t-p;
+		k++;
+	}
+	return k / calc.kdivisor;
+}
+
+jcount(calc: ref Calc, x_coord, y_coord: int): int
+{
+	(x, y) := (calc.xr[x_coord], calc.yr[y_coord]);
+	(p, q) := (calc.parx, calc.pary);
+	k := 0;
+	maxcount := MAXCOUNT * calc.kdivisor;
+	while (k < maxcount) {
+		if (x >= TWO || y >= TWO || x <= -TWO || y <= -TWO)
+			break;
+
+		if (0) {
+			# x = (x < 0) ? (x>>HBASE)|NEG : x>>HBASE;
+			# y = (y < 0) ? (y>>HBASE)|NEG : y>>HBASE;
+		}
+
+		x >>= HBASE;
+		y >>= HBASE;
+		t := y*y;
+		y = big 2*x*y+q;	# possible unserious overflow when BASE == 28
+		x *= x;
+		if (x+t >= FOUR) 
+			break;
+		x -= t-p;
+		k++;
+	}
+	return k / calc.kdivisor;
+}
+
+getcolour(calc: ref Calc, x, y, d: int): int
+{
+	if (calc.dispbase[d] == byte BLANK) {
+		calc.dispbase[d] = byte pointcolour(calc, x, y);
+		point(calc, calc.img, (x, y), int calc.dispbase[d]);
+	}
+	return int calc.dispbase[d];
+}
+
+point(calc: ref Calc, d: ref Image, p: Point, col: int)
+{
+	d.draw(Rect(p, p.add((1,1))).addpt(calc.winr.min), colours[col], nil, (0,0));
+}
+
+horizline(calc: ref Calc, d: ref Image, x0, x1, y: int, col: int)
+{
+	if (x0 < x1)
+		r := Rect((x0, y), (x1, y+1));
+	else
+		r = Rect((x1+1, y), (x0+1, y+1));
+	d.draw(r.addpt(calc.winr.min), colours[col], nil, (0, 0));
+	# r := Rect((x0, y), (x1, y)).canon();
+	# r.max = r.max.add((1, 1));
+}
+
+Fracrect.dx(r: self Fracrect): real
+{
+	return r.max.x - r.min.x;
+}
+
+Fracrect.dy(r: self Fracrect): real
+{
+	return r.max.y - r.min.y;
+}
+
+real2fix(x: real): FIX
+{
+	return big (x * real SCALE);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr, "mand: tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+kill(pid: int): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if (fd == nil)
+		return -1;
+	if (sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+col(i, r : int) : int
+{
+	if (r == 1)
+		return 0;
+	return (255*(i%r))/(r-1);
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	Point: import draw;
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+	bd := int cmd(win, ". cget -bd");
+	winsize := Point(int cmd(win, ". cget -actwidth") + bd * 2, int cmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		cmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		cmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int cmd(win, ". cget -actx"), int cmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int cmd(win, ". cget -actwidth") + bd*2,
+				int cmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.max.x - dx, r.max.x);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.max.y - dy, r.max.y);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	cmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
--- /dev/null
+++ b/appl/wm/mash.b
@@ -1,0 +1,577 @@
+implement WmMash;
+
+include "sys.m";
+	sys: Sys;
+	FileIO: import sys;
+
+include "draw.m";
+	draw: Draw;
+	Context: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include	"plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg: import plumbmsg;
+
+include "workdir.m";
+	workdir: Workdir;
+
+WmMash: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+Command: module
+{
+	tkinit:	fn(ctxt: ref Draw->Context, t: ref Tk->Toplevel, args: list of string);
+};
+
+BS:		con 8;		# ^h backspace character
+BSW:		con 23;		# ^w bacspace word
+BSL:		con 21;		# ^u backspace line
+EOT:		con 4;		# ^d end of file
+ESC:		con 27;		# hold mode
+
+HIWAT:	con 2000;	# maximum number of lines in transcript
+LOWAT:	con 1500;	# amount to reduce to after high water
+
+Name:	con "Mash";
+
+Rdreq: adt
+{
+	off:	int;
+	nbytes:	int;
+	fid:	int;
+	rc:	chan of (array of byte, string);
+};
+
+shwin_cfg := array[] of {
+	"menu .m",
+	".m add command -text Cut -command {send edit cut}",
+	".m add command -text Paste -command {send edit paste}",
+	".m add command -text Snarf -command {send edit snarf}",
+	".m add command -text Send -command {send edit send}",
+	"frame .b -bd 1 -relief ridge",
+	"frame .ft -bd 0",
+	"scrollbar .ft.scroll -width 14 -bd 0 -relief ridge -command {.ft.t yview}",
+	"text .ft.t -bd 1 -relief flat -width 520 -height 7c -yscrollcommand {.ft.scroll set}",
+	"pack .ft.scroll -side left -fill y",
+	"pack .ft.t -fill both -expand 1",
+	"pack .Wm_t -fill x",
+	"pack .b -anchor w -fill x",
+	"pack .ft -fill both -expand 1",
+	"pack propagate . 0",
+	"focus .ft.t",
+	"bind .ft.t <Key> {send keys {%A}}",
+	"bind .ft.t <Control-d> {send keys {%A}}",
+	"bind .ft.t <Control-h> {send keys {%A}}",
+	"bind .ft.t <Button-1> +{grab set .ft.t; send but1 pressed}",
+	"bind .ft.t <Double-Button-1> +{grab set .ft.t; send but1 pressed}",
+	"bind .ft.t <ButtonRelease-1> +{grab release .ft.t; send but1 released}",
+	"bind .ft.t <ButtonPress-2> {send but2 %X %Y}",
+	"bind .ft.t <Motion-Button-2-Button-1> {}",
+	"bind .ft.t <Motion-ButtonPress-2> {}",
+	"bind .ft.t <ButtonPress-3> {send but3 pressed}",
+	"bind .ft.t <ButtonRelease-3> {send but3 released %x %y}",
+	"bind .ft.t <Motion-Button-3> {}",
+	"bind .ft.t <Motion-Button-3-Button-1> {}",
+	"bind .ft.t <Double-Button-3> {}",
+	"bind .ft.t <Double-ButtonRelease-3> {}",
+	"update"
+};
+
+rdreq: list of Rdreq;
+menuindex := "0";
+holding := 0;
+plumbed := 0;
+rawon := 0;
+rawinput := "";
+
+init(ctxt: ref Context, argv: list of string)
+{
+	s: string;
+
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "mash: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	plumbmsg = load Plumbmsg Plumbmsg->PATH;
+
+	sys->pctl(Sys->FORKNS | Sys->NEWPGRP, nil);
+
+	tkclient->init();
+
+	if(plumbmsg->init(1, nil, 0) >= 0){
+		plumbed = 1;
+		workdir = load Workdir Workdir->PATH;
+	}
+
+	argv = tl argv;		# strip off command name
+	(t, titlectl) := tkclient->toplevel(ctxt, "", Name, Tkclient->Appl);
+
+	edit := chan of string;
+	tk->namechan(t, edit, "edit");
+#	mash := chan of string;
+#	tk->namechan(t, mash, "mash");
+
+	tkcmds(t, shwin_cfg);
+
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	ioc := chan of (int, ref FileIO, ref FileIO, string);
+	spawn newsh(ctxt, t, ioc, argv);
+
+	(pid, file, filectl, consfile) := <-ioc;
+	if(file == nil || filectl == nil) {
+		sys->print("newsh: %r\n");
+		return;
+	}
+
+	keys := chan of string;
+	tk->namechan(t, keys, "keys");
+
+	but1 := chan of string;
+	tk->namechan(t, but1, "but1");
+	but2 := chan of string;
+	tk->namechan(t, but2, "but2");
+	but3 := chan of string;
+	tk->namechan(t, but3, "but3");
+	button1 := 0;
+	button3 := 0;
+
+	rdrpc: Rdreq;
+
+	# outpoint is place in text to insert characters printed by programs
+	tk->cmd(t, ".ft.t mark set outpoint end; .ft.t mark gravity outpoint left");
+
+	for(;;) alt {
+	c := <-t.ctxt.kbd =>
+		tk->keyboard(t, c);
+	c := <-t.ctxt.ptr =>
+		tk->pointer(t, *c);
+	c := <-t.ctxt.ctl or
+	c = <-t.wreq =>
+		tkclient->wmctl(t, c);
+	menu := <-titlectl =>
+		if(menu == "exit") {
+			kill(pid);
+			return;
+		}
+		tkclient->wmctl(t, menu);
+		tk->cmd(t, "focus .ft.t");
+
+	ecmd := <-edit =>
+		editor(t, ecmd);
+		sendinput(t);
+		tk->cmd(t, "focus .ft.t");
+
+	c := <-keys =>
+		cut(t, 1);
+		if(rawon) {
+			rawinput += c[1:2];
+			rawinput = sendraw(rawinput);
+			break;
+		}
+		char := c[1];
+		if(char == '\\')
+			char = c[2];
+		update := ";.ft.t see insert;update";
+		case char {
+		* =>
+			tk->cmd(t, ".ft.t insert insert "+c+update);
+		'\n' or EOT =>
+			tk->cmd(t, ".ft.t insert insert "+c+update);
+			sendinput(t);
+		BS =>
+			tk->cmd(t, ".ft.t tkTextDelIns -c"+update);
+		BSL =>
+			tk->cmd(t, ".ft.t tkTextDelIns -l"+update);
+		BSW =>
+			tk->cmd(t, ".ft.t tkTextDelIns -w"+update);
+		ESC =>
+			holding ^= 1;
+			color := "blue";
+			if(!holding){
+				color = "black";
+				tkclient->settitle(t, Name);
+				sendinput(t);
+			}else
+				tkclient->settitle(t, Name+" (holding)");
+			tk->cmd(t, ".ft.t configure -foreground "+color+update);
+		}
+
+	c := <-but1 =>
+		button1 = (c == "pressed");
+		button3 = 0;	# abort any pending button 3 action
+
+	c := <-but2 =>
+		if(button1){
+			cut(t, 1);
+			tk->cmd(t, "update");
+			break;
+		}
+		(nil, l) := sys->tokenize(c, " ");
+		x := int hd l - 50;
+		y := int hd tl l - int tk->cmd(t, ".m yposition "+menuindex) - 10;
+		tk->cmd(t, ".m activate "+menuindex+"; .m post "+string x+" "+string y+
+			"; grab set .m; update");
+		button3 = 0;	# abort any pending button 3 action
+
+	c := <-but3 =>
+		if(c == "pressed"){
+			button3 = 1;
+			if(button1){
+				paste(t);
+				tk->cmd(t, "update");
+			}
+			break;
+		}
+		if(plumbed == 0 || button3 == 0 || button1 != 0)
+			break;
+		button3 = 0;
+		# Plumb message triggered by release of button 3
+		(nil, l) := sys->tokenize(c, " ");
+		x := int hd tl l;
+		y := int hd tl tl l;
+		index := tk->cmd(t, ".ft.t index @"+string x+","+string y);
+		selindex := tk->cmd(t, ".ft.t tag ranges sel");
+		if(selindex != "")
+			insel := tk->cmd(t, ".ft.t compare sel.first <= "+index)=="1" &&
+				tk->cmd(t, ".ft.t compare sel.last >= "+index)=="1";
+		else
+			insel = 0;
+		attr := "";
+		if(insel)
+			text := tk->cmd(t, ".ft.t get sel.first sel.last");
+		else{
+			# have line with text in it
+			# now extract whitespace-bounded string around click
+			(nil, w) := sys->tokenize(index, ".");
+			charno := int hd tl w;
+			left := tk->cmd(t, ".ft.t index {"+index+" linestart}");
+			right := tk->cmd(t, ".ft.t index {"+index+" lineend}");
+			line := tk->cmd(t, ".ft.t get "+left+" "+right);
+			for(i:=charno; i>0; --i)
+				if(line[i-1]==' ' || line[i-1]=='\t')
+					break;
+			for(j:=charno; j<len line; j++)
+				if(line[j]==' ' || line[j]=='\t')
+					break;
+			text = line[i:j];
+			attr = "click="+string (charno-i);
+		}
+		msg := ref Msg(
+			"WmSh",
+			"",
+			workdir->init(),
+			"text",
+			attr,
+			array of byte text);
+		if(msg.send() < 0)
+			sys->fprint(sys->fildes(2), "sh: plumbing write error: %r\n");
+
+	rdrpc = <-filectl.read =>
+		if(rdrpc.rc == nil)
+			continue;
+		rdrpc.rc <-= ( nil, "not allowed" );
+
+	(nil, data, nil, wc) := <-filectl.write =>
+		if(wc == nil) {
+			# consctl closed - revert to cooked mode
+			rawon = 0;
+			continue;
+		}
+		(nc, cmdlst) := sys->tokenize(string data, " \n");
+		if(nc == 1) {
+			case hd cmdlst {
+			"rawon" =>
+				rawon = 1;
+				rawinput = "";
+				# discard previous input
+				advance := string (len tk->cmd(t, ".ft.t get outpoint end") +1);
+				tk->cmd(t, ".ft.t mark set outpoint outpoint+" + advance + "chars");
+			"rawoff" =>
+				rawon = 0;
+			* =>
+				wc <-= (0, "unknown consctl request");
+				continue;
+			}
+			wc <-= (len data, nil);
+			continue;
+		}
+		wc <-= (0, "unknown consctl request");
+
+	rdrpc = <-file.read =>
+		if(rdrpc.rc == nil) {
+			(ok, nil) := sys->stat(consfile);
+			if (ok < 0)
+				return;
+			continue;
+		}
+		append(rdrpc);
+		sendinput(t);
+
+	(off, data, fid, wc) := <-file.write =>
+		if(wc == nil) {
+			(ok, nil) := sys->stat(consfile);
+			if (ok < 0)
+				return;
+			continue;
+		}
+		cdata := stripbs(t, string data);
+		ncdata := string len cdata + "chars;";
+		moveins := insat(t, "outpoint");
+		tk->cmd(t, ".ft.t insert outpoint '"+ cdata);
+		wc <-= (len data, nil);
+		data = nil;
+		s = ".ft.t mark set outpoint outpoint+" + ncdata;
+		s += ".ft.t see outpoint;";
+		if(moveins)
+			s += ".ft.t mark set insert insert+" + ncdata;
+		s += "update";
+		tk->cmd(t, s);
+		nlines := int tk->cmd(t, ".ft.t index end");
+		if(nlines > HIWAT){
+			s = ".ft.t delete 1.0 "+ string (nlines-LOWAT) +".0;update";
+			tk->cmd(t, s);
+		}
+	}
+}
+
+RPCread: type (int, int, int, chan of (array of byte, string));
+
+append(r: RPCread)
+{
+	t := r :: nil;
+	while(rdreq != nil) {
+		t = hd rdreq :: t;
+		rdreq = tl rdreq;
+	}
+	rdreq = t;
+}
+
+insat(t: ref Tk->Toplevel, mark: string): int
+{
+	return tk->cmd(t, ".ft.t compare insert == "+mark) == "1";
+}
+
+insininput(t: ref Tk->Toplevel): int
+{
+	if(tk->cmd(t, ".ft.t compare insert >= outpoint") != "1")
+		return 0;
+	return tk->cmd(t, ".ft.t compare {insert linestart} == {outpoint linestart}") == "1";
+}
+
+isalnum(s: string): int
+{
+	if(s == "")
+		return 0;
+	c := s[0];
+	if('a' <= c && c <= 'z')
+		return 1;
+	if('A' <= c && c <= 'Z')
+		return 1;
+	if('0' <= c && c <= '9')
+		return 1;
+	if(c == '_')
+		return 1;
+	if(c > 16rA0)
+		return 1;
+	return 0;
+}
+
+stripbs(t: ref Tk->Toplevel, s: string): string
+{
+	l := len s;
+	for(i := 0; i < l; i++)
+		if(s[i] == '\b') {
+			pre := "";
+			rem := "";
+			if(i + 1 < l)
+				rem = s[i+1:];
+			if(i == 0) {	# erase existing character in line
+				if(tk->cmd(t, ".ft.t get " +
+					"{outpoint linestart} outpoint") != "")
+				    tk->cmd(t, ".ft.t delete outpoint-1char");
+			} else {
+				if(s[i-1] != '\n')	# don't erase newlines
+					i--;
+				if(i)
+					pre = s[:i];
+			}
+			s = pre + rem;
+			l = len s;
+			i = len pre - 1;
+		}
+	return s;
+}
+
+editor(t: ref Tk->Toplevel, ecmd: string)
+{
+	s, snarf: string;
+
+	case ecmd {
+	"cut" =>
+		menuindex = "0";
+		cut(t, 1);
+	
+	"paste" =>
+		menuindex = "1";
+		paste(t);
+
+	"snarf" =>
+		menuindex = "2";
+		if(tk->cmd(t, ".ft.t tag ranges sel") == "")
+			break;
+		snarf = tk->cmd(t, ".ft.t get sel.first sel.last");
+		tkclient->snarfput(snarf);
+
+	"send" =>
+		menuindex = "3";
+		if(tk->cmd(t, ".ft.t tag ranges sel") != ""){
+			snarf = tk->cmd(t, ".ft.t get sel.first sel.last");
+			tkclient->snarfput(snarf);
+		}else
+			snarf = tkclient->snarfget();
+		if(snarf != "")
+			s = snarf;
+		else
+			return;
+		if(s[len s-1] != '\n' && s[len s-1] != EOT)
+			s[len s] = '\n';
+		tk->cmd(t, ".ft.t see end; .ft.t insert end '"+s);
+		tk->cmd(t, ".ft.t mark set insert end");
+		tk->cmd(t, ".ft.t tag remove sel sel.first sel.last");
+	}
+	tk->cmd(t, "update");
+}
+
+cut(t: ref Tk->Toplevel, snarfit: int)
+{
+	if(tk->cmd(t, ".ft.t tag ranges sel") == "")
+		return;
+	if(snarfit)
+		tkclient->snarfput(tk->cmd(t, ".ft.t get sel.first sel.last"));
+	tk->cmd(t, ".ft.t delete sel.first sel.last");
+}
+
+paste(t: ref Tk->Toplevel)
+{
+	snarf := tkclient->snarfget();
+	if(snarf == "")
+		return;
+	cut(t, 0);
+	tk->cmd(t, ".ft.t insert insert '"+snarf);
+	tk->cmd(t, ".ft.t tag add sel insert-"+string len snarf+"chars insert");
+	sendinput(t);
+}
+
+sendinput(t: ref Tk->Toplevel)
+{
+	if(holding)
+		return;
+	input := tk->cmd(t, ".ft.t get outpoint end");
+	slen := len input;
+	if(slen == 0 || rdreq == nil)
+		return;
+
+	r := hd rdreq;
+	for(i := 0; i < slen; i++)
+		if(input[i] == '\n' || input[i] == EOT)
+			break;
+
+	if(i >= slen && slen < r.nbytes)
+		return;
+
+	if(i >= r.nbytes)
+		i = r.nbytes-1;
+	advance := string (i+1);
+	if(input[i] == EOT)
+		input = input[0:i];
+	else
+		input = input[0:i+1];
+
+	rdreq = tl rdreq;
+
+	alt {
+	r.rc <-= (array of byte input, "") =>
+		tk->cmd(t, ".ft.t mark set outpoint outpoint+" + advance + "chars");
+	* =>
+		# requester has disappeared; ignore his request and try again
+		sendinput(t);
+	}
+}
+
+sendraw(input : string) : string
+{
+	i := len input;
+	if(i == 0 || rdreq == nil)
+		return input;
+
+	r := hd rdreq;
+	rdreq = tl rdreq;
+
+	if(i > r.nbytes)
+		i = r.nbytes;
+
+	alt {
+	r.rc <-= (array of byte input[0:i], "") =>
+		input = input[i:];
+	* =>
+		;# requester has disappeared; ignore his request and try again
+	}
+	return input;
+}
+
+newsh(ctxt: ref Context, t: ref Tk->Toplevel, ioc: chan of (int, ref FileIO, ref FileIO, string), args: list of string)
+{
+	pid := sys->pctl(sys->NEWFD, nil);
+
+	sh := load Command "/dis/mash.dis";
+	if(sh == nil) {
+		ioc <-= (0, nil, nil, nil);
+		return;
+	}
+
+	tty := "cons."+string pid;
+
+	sys->bind("#s","/chan",sys->MBEFORE);
+	fio := sys->file2chan("/chan", tty);
+	fioctl := sys->file2chan("/chan", tty + "ctl");
+
+	ioc <-= (pid, fio, fioctl, "/chan/"+tty);
+	if(fio == nil || fioctl == nil)
+		return;
+
+	sys->bind("/chan/"+tty, "/dev/cons", sys->MREPL);
+	sys->bind("/chan/"+tty+"ctl", "/dev/consctl", sys->MREPL);
+
+	fd0 := sys->open("/dev/cons", sys->OREAD|sys->ORCLOSE);
+	fd1 := sys->open("/dev/cons", sys->OWRITE);
+	fd2 := sys->open("/dev/cons", sys->OWRITE);
+
+	sh->tkinit(ctxt, t, "mash" :: args);
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+}
+
+tkcmds(t: ref Tk->Toplevel, cfg: array of string)
+{
+	for(i := 0; i < len cfg; i++)
+		tk->cmd(t, cfg[i]);
+}
--- /dev/null
+++ b/appl/wm/memory.b
@@ -1,0 +1,246 @@
+implement WmMemory;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Display, Image, Rect: import draw;
+
+include "tk.m";
+	tk: Tk;
+	t: ref Tk->Toplevel;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+WmMemory: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Arena: adt
+{
+	name:	string;
+	limit:	int;
+	size:	int;
+	hw:	int;
+	allocs: int;
+	frees: int;
+	exts: int;
+	chunk: int;
+	y:	int;
+	tag:	string;
+	tagsz: string;
+	taghw:	string;
+	tagiu:	string;
+};
+a := array[10] of Arena;
+
+mem_cfg := array[] of {
+	"canvas .c -width 240 -height 45",
+	"pack .c",
+	"update",
+};
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	spawn realinit(ctxt);
+}
+
+realinit(ctxt: ref Draw->Context)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "memory: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+
+	menubut := chan of string;
+	(t, menubut) = tkclient->toplevel(ctxt, "", "Memory", 0);
+	for(j := 0; j < len mem_cfg; j++)
+		cmd(t, mem_cfg[j]);
+	tkclient->startinput(t, "ptr"::nil);
+	tkclient->onscreen(t, nil);
+
+	tick := chan of int;
+	spawn ticker(tick);
+
+	mfd := sys->open("/dev/memory", sys->OREAD);
+
+	n := getmem(mfd);
+	maxx := initdraw(n);
+
+	pid: int;
+	for(;;) alt {
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-menubut =>
+		if(s == "exit"){
+			kill(pid);
+			return;
+		}
+		tkclient->wmctl(t, s);
+	pid = <-tick =>
+		update(mfd);
+		for(i := 0; i < n; i++) {
+			if(a[i].limit <= 0)
+				continue;
+			x := int ((big a[i].size * big (230-maxx)) / big a[i].limit);
+			s := sys->sprint(".c coords %s %d %d %d %d",
+				a[i].tag,
+				maxx,
+				a[i].y + 4,
+				maxx + x,
+				a[i].y + 8);
+			cmd(t, s);
+			x = int ((big a[i].hw * big (230-maxx)) / big a[i].limit);
+			s = sys->sprint(".c coords %s %d %d %d %d",
+				a[i].taghw,
+				maxx,
+				a[i].y + 4,
+				maxx+x,
+				a[i].y + 8);
+			cmd(t, s);
+			s = sys->sprint(".c itemconfigure %s -text '%s", a[i].tagsz, string a[i].size);
+			cmd(t, s);
+			s = sys->sprint(".c itemconfigure %s -text '%d", a[i].tagiu, a[i].allocs-a[i].frees);
+			cmd(t, s);
+		}
+		cmd(t, "update");
+	}
+}
+
+ticker(c: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	for(;;) {
+		c <-= pid;
+		sys->sleep(1000);
+	}
+}
+
+initdraw(n: int): int
+{
+	y := 15;
+	maxx := 0;
+	for (i := 0; i < n; i++) {
+		id := cmd(t, ".c create text 5 "+string y+" -anchor w -text "+a[i].name);
+		r := s2r(cmd(t, ".c bbox " + id));
+		if (r.max.x > maxx)
+			maxx = r.max.x;
+		y += 20;
+	}
+	maxx += 5;
+	y = 15;
+	for(i = 0; i < n; i++) {
+		s := sys->sprint(".c create rectangle %d %d 230 %d -fill white", maxx, y+4, y+8);
+		cmd(t, s);
+		s = sys->sprint(".c create rectangle %d %d 230 %d -fill white", maxx, y+4, y+8);
+		a[i].taghw = cmd(t, s);
+		s = sys->sprint(".c create rectangle %d %d 230 %d -fill red", maxx, y+4, y+8);
+		a[i].tag = cmd(t, s);
+		s = sys->sprint(".c create text 230 %d -anchor e -text '%s", y - 2, sizestr(a[i].limit));
+		cmd(t, s);
+		s = sys->sprint(".c create text %d %d -anchor w -text '%s", maxx, y - 2, string a[i].size);
+		a[i].tagsz = cmd(t, s);
+		s = sys->sprint(".c create text 120 %d -fill red -anchor w -text '%d", y - 2, a[i].allocs-a[i].frees);
+		a[i].tagiu = cmd(t, s);
+		a[i].y = y;
+		y += 20;
+	}
+	cmd(t, ".c configure -height "+string y);
+	cmd(t, "update");
+	return maxx;
+}
+
+sizestr(n: int): string
+{
+	if ((n / 1024) % 1024 == 0)
+		return string (n / (1024 * 1024)) + "M";
+	return string (n / 1024) + "K";
+}
+
+buf := array[8192] of byte;
+
+update(mfd: ref Sys->FD): int
+{
+	sys->seek(mfd, big 0, Sys->SEEKSTART);
+	n := sys->read(mfd, buf, len buf);
+	if(n <= 0)
+		exit;
+	(nil, l) := sys->tokenize(string buf[0:n], "\n");
+	i := 0;
+	while(l != nil) {
+		s := hd l;
+		a[i].size = int s[0:];
+		a[i].hw = int s[24:];
+		a[i].allocs = int s[3*12:];
+		a[i].frees = int s[4*12:];
+		a[i].exts = int s[5*12:];
+		a[i++].chunk = int s[6*12:];
+		l = tl l;
+	}
+	return i;
+}
+
+getmem(mfd: ref Sys->FD): int
+{
+	n := sys->read(mfd, buf, len buf);
+	if(n <= 0)
+		exit;
+	(nil, l) := sys->tokenize(string buf[0:n], "\n");
+	i := 0;
+	while(l != nil) {
+		s := hd l;
+		a[i].size = int s[0:];
+		a[i].limit = int s[12:];
+		a[i].hw = int s[2*12:];
+		a[i].allocs = int s[3*12:];
+		a[i].frees = int s[4*12:];
+		a[i].exts = int s[5*12:];
+		a[i].chunk = int s[6*12:];
+		a[i].name = s[7*12:];
+		i++;
+		l = tl l;
+	}
+	return i;
+}
+
+s2r(s: string): Rect
+{
+	(n, toks) := sys->tokenize(s, " ");
+	if (n != 4) {
+		sys->print("'%s' is not a rectangle!\n", s);
+		raise "bad conversion";
+	}
+	r: Rect;
+	(r.min.x, toks) = (int hd toks, tl toks);
+	(r.min.y, toks) = (int hd toks, tl toks);
+	(r.max.x, toks) = (int hd toks, tl toks);
+	(r.max.y, toks) = (int hd toks, tl toks);
+	return r;
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->print("memory: tk error on '%s': %s\n", s, e);
+	return e;
+}
--- /dev/null
+++ b/appl/wm/mkfile
@@ -1,0 +1,102 @@
+<../../mkconfig
+
+DIRS=\
+	brutus\
+	camera\
+#	diary\
+	drawmux\
+	ftree\
+	mailtool\
+	mpeg\
+
+TARG=\
+	about.dis\
+	avi.dis\
+	bounce.dis\
+	brutus.dis\
+	c4.dis\
+	calendar.dis\
+	clock.dis\
+	coffee.dis\
+	collide.dis\
+	colors.dis\
+	cprof.dis\
+	date.dis\
+	deb.dis\
+	debdata.dis\
+	debsrc.dis\
+	dir.dis\
+	edit.dis\
+	filename.dis\
+	getauthinfo.dis\
+	keyboard.dis\
+	logon.dis\
+	logwindow.dis\
+	man.dis\
+	mand.dis\
+	mash.dis\
+	memory.dis\
+##	mpeg.dis\
+	mprof.dis\
+	pen.dis\
+	polyhedra.dis\
+	prof.dis\
+##	qt.dis\
+	readmail.dis\
+	remotelogon.dis\
+	reversi.dis\
+	rmtdir.dis\
+	rt.dis\
+	sendmail.dis\
+	sh.dis\
+	smenu.dis\
+	snake.dis\
+	stopwatch.dis\
+	sweeper.dis\
+	task.dis\
+	telnet.dis\
+	tetris.dis\
+	toolbar.dis\
+	unibrowse.dis\
+	view.dis\
+	vt.dis\
+	wish.dis\
+	wm.dis\
+	wmplay.dis\
+
+MODULES=\
+	wmdeb.m\
+
+SYSMODULES=\
+	bufio.m\
+	cci.m\
+	daytime.m\
+	debug.m\
+	draw.m\
+	filepat.m\
+	html.m\
+	keyring.m\
+	man.m\
+	mpeg.m\
+	newns.m\
+	plumbmsg.m\
+	quicktime.m\
+	rand.m\
+	readdir.m\
+	riff.m\
+	security.m\
+	sh.m\
+	string.m\
+	sys.m\
+	tk.m\
+	tkclient.m\
+	url.m\
+	webget.m\
+	wmclient.m\
+	wmsrv.m\
+	workdir.m\
+
+DISBIN=$ROOT/dis/wm
+
+<$ROOT/mkfiles/mkdis
+<$ROOT/mkfiles/mksubdirs
--- /dev/null
+++ b/appl/wm/mpeg.b
@@ -1,0 +1,185 @@
+implement WmMpeg;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Rect, Display, Image: import draw;
+	ctxt: ref Draw->Context;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include	"mpeg.m";
+	mpeg: Mpeg;
+
+WmMpeg: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Stopped, Playing: con iota;
+
+dx, dy: int;
+dw, dh: int;
+adjust: int;
+
+task_cfg := array[] of {
+	"canvas .c -background =5",
+	"frame .b",
+	"button .b.File -text File -command {send cmd file}",
+	"button .b.Stop -text Stop -command {send cmd stop}",
+	"button .b.Pause -text Pause -command {send cmd pause}",
+	"button .b.Play -text Play -command {send cmd play}",
+	"button .b.Picture -text Picture -command {send cmd pict}",
+	"frame .f",
+	"label .f.file -text {File:}",
+	"label .f.name",
+	"pack .f.file .f.name -side left",
+	"pack .b.File .b.Stop .b.Pause .b.Play .b.Picture -side left",
+	"pack .f -fill x",
+	"pack .b -anchor w",
+	"pack .c -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+init(xctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys  Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	mpeg = load Mpeg  Mpeg->PATH;
+
+	ctxt = xctxt;
+
+	tkclient->init();
+
+	(t, menubut)  := tkclient->toplevel(ctxt.screen, "", "Mpeg Player", Tkclient->Appl);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	tkclient->tkcmds(t, task_cfg);
+
+	tk->cmd(t, "bind . <Configure> {send cmd resize}");
+	tk->cmd(t, "update");
+
+	fname := "";
+	ctl := chan of string;
+	state := Stopped;
+
+	for(;;) alt {
+	menu := <-menubut =>
+		if(menu == "exit") {
+			if(state == Playing) {
+				mpeg->ctl("stop");
+				<-ctl;
+			}
+			return;
+		}
+		tkclient->wmctl(t, menu);
+	press := <-cmd =>
+		case press {
+		"file" =>
+			pat := list of {
+				"*.mpg (MPEG movie file)"
+			};
+			fname = tkclient->filename(ctxt.screen, t, "Locate MPEG clip", pat, "");
+			if(fname != nil) {
+				tk->cmd(t, ".f.name configure -text {"+fname+"}");
+				tk->cmd(t, "update");
+			}
+		"play" =>
+			s := mpeg->play(ctxt.display, nil, 0, canvsize(t), fname, ctl);
+			if(s != nil) {
+				tkclient->dialog(t, "error -fg red", "Play MPEG",
+					"Media player error:\n"+s,
+					0, "Stop Play"::nil);
+				break;
+			}
+			state = Playing;
+		"resize" =>
+			if(state != Playing)
+				break;
+			r := canvsize(t);
+			s := sys->sprint("window %d %d %d %d",
+					r.min.x, r.min.y, r.max.x, r.max.y);
+			mpeg->ctl(s);
+		"pict" =>
+			if(adjust)
+				break;
+			adjust = 1;
+			spawn pict(t);
+		* =>
+			# Stop & Pause
+			mpeg->ctl(press);
+		}
+	done := <-ctl =>
+		state = Stopped;
+	}
+}
+
+canvsize(t: ref Toplevel): Rect
+{
+	r: Rect;
+
+	r.min.x = int tk->cmd(t, ".c cget -actx") + dx;
+	r.min.y = int tk->cmd(t, ".c cget -acty") + dy;
+	r.max.x = r.min.x + int tk->cmd(t, ".c cget -width") + dw;
+	r.max.y = r.min.y + int tk->cmd(t, ".c cget -height") + dh;
+
+	return r;
+}
+
+pict_cfg := array[] of {
+	"scale .dx -orient horizontal -from -5 -to 5 -label {Origin X}"+
+		" -command { send c dx}",
+	"scale .dy -orient horizontal -from -5 -to 5 -label {Origin Y}"+
+		" -command { send c dy}",
+	"scale .dw -orient horizontal -from -5 -to 5 -label {Width}"+
+		" -command {send c dw}",
+	"scale .dh -orient horizontal -from -5 -to 5 -label {Height}"+
+		" -command {send c dh}",
+	"pack .Wm_t -fill x",
+	"pack .dx .dy .dw .dh -fill x",
+	"pack propagate . 0",
+	"update",
+};
+
+pict(parent: ref Toplevel)
+{
+	targ := +" -borderwidth 2 -relief raised";
+
+	(t, menubut) := tkclient->toplevel(ctxt.screen, tkclient->geom(parent), "Mpeg Picture", 0);
+
+	pchan := chan of string;
+	tk->namechan(t, pchan, "c");
+
+	tkclient->tkcmds(t, pict_cfg);
+
+	for(;;) alt {
+	menu := <-menubut =>
+		if(menu == "exit") {
+			adjust = 0;
+			return;
+		}
+		tkclient->wmctl(t, menu);
+	tcip := <-pchan =>
+		case tcip {
+		"dx" =>	dx = int tk->cmd(t, ".dx get");
+		"dy" => dy = int tk->cmd(t, ".dy get");
+		"dw" => dw = int tk->cmd(t, ".dw get");
+		"dh" => dh = int tk->cmd(t, ".dh get");
+		}
+		r := canvsize(parent);
+		s := sys->sprint("window %d %d %d %d",
+				r.min.x, r.min.y, r.max.x, r.max.y);
+		mpeg->ctl(s);
+	}
+}
--- /dev/null
+++ b/appl/wm/mpeg/c0.tab
@@ -1,0 +1,261 @@
+# vlc -uUNDEF,UNDEF c0
+c0_size: con 256;
+c0_bits: con 8;
+c0_table:= array[] of {
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(0, UNDEF,UNDEF),
+	(8, 18,1),
+	(8, -18,1),
+	(8, 17,1),
+	(8, -17,1),
+	(8, 16,1),
+	(8, -16,1),
+	(8, 15,1),
+	(8, -15,1),
+	(8, 3,6),
+	(8, -3,6),
+	(8, 2,16),
+	(8, -2,16),
+	(8, 2,15),
+	(8, -2,15),
+	(8, 2,14),
+	(8, -2,14),
+	(8, 2,13),
+	(8, -2,13),
+	(8, 2,12),
+	(8, -2,12),
+	(8, 2,11),
+	(8, -2,11),
+	(8, 1,31),
+	(8, -1,31),
+	(8, 1,30),
+	(8, -1,30),
+	(8, 1,29),
+	(8, -1,29),
+	(8, 1,28),
+	(8, -1,28),
+	(8, 1,27),
+	(8, -1,27),
+	(7, 40,0),
+	(7, 40,0),
+	(7, -40,0),
+	(7, -40,0),
+	(7, 39,0),
+	(7, 39,0),
+	(7, -39,0),
+	(7, -39,0),
+	(7, 38,0),
+	(7, 38,0),
+	(7, -38,0),
+	(7, -38,0),
+	(7, 37,0),
+	(7, 37,0),
+	(7, -37,0),
+	(7, -37,0),
+	(7, 36,0),
+	(7, 36,0),
+	(7, -36,0),
+	(7, -36,0),
+	(7, 35,0),
+	(7, 35,0),
+	(7, -35,0),
+	(7, -35,0),
+	(7, 34,0),
+	(7, 34,0),
+	(7, -34,0),
+	(7, -34,0),
+	(7, 33,0),
+	(7, 33,0),
+	(7, -33,0),
+	(7, -33,0),
+	(7, 32,0),
+	(7, 32,0),
+	(7, -32,0),
+	(7, -32,0),
+	(7, 14,1),
+	(7, 14,1),
+	(7, -14,1),
+	(7, -14,1),
+	(7, 13,1),
+	(7, 13,1),
+	(7, -13,1),
+	(7, -13,1),
+	(7, 12,1),
+	(7, 12,1),
+	(7, -12,1),
+	(7, -12,1),
+	(7, 11,1),
+	(7, 11,1),
+	(7, -11,1),
+	(7, -11,1),
+	(7, 10,1),
+	(7, 10,1),
+	(7, -10,1),
+	(7, -10,1),
+	(7, 9,1),
+	(7, 9,1),
+	(7, -9,1),
+	(7, -9,1),
+	(7, 8,1),
+	(7, 8,1),
+	(7, -8,1),
+	(7, -8,1),
+	(6, 31,0),
+	(6, 31,0),
+	(6, 31,0),
+	(6, 31,0),
+	(6, -31,0),
+	(6, -31,0),
+	(6, -31,0),
+	(6, -31,0),
+	(6, 30,0),
+	(6, 30,0),
+	(6, 30,0),
+	(6, 30,0),
+	(6, -30,0),
+	(6, -30,0),
+	(6, -30,0),
+	(6, -30,0),
+	(6, 29,0),
+	(6, 29,0),
+	(6, 29,0),
+	(6, 29,0),
+	(6, -29,0),
+	(6, -29,0),
+	(6, -29,0),
+	(6, -29,0),
+	(6, 28,0),
+	(6, 28,0),
+	(6, 28,0),
+	(6, 28,0),
+	(6, -28,0),
+	(6, -28,0),
+	(6, -28,0),
+	(6, -28,0),
+	(6, 27,0),
+	(6, 27,0),
+	(6, 27,0),
+	(6, 27,0),
+	(6, -27,0),
+	(6, -27,0),
+	(6, -27,0),
+	(6, -27,0),
+	(6, 26,0),
+	(6, 26,0),
+	(6, 26,0),
+	(6, 26,0),
+	(6, -26,0),
+	(6, -26,0),
+	(6, -26,0),
+	(6, -26,0),
+	(6, 25,0),
+	(6, 25,0),
+	(6, 25,0),
+	(6, 25,0),
+	(6, -25,0),
+	(6, -25,0),
+	(6, -25,0),
+	(6, -25,0),
+	(6, 24,0),
+	(6, 24,0),
+	(6, 24,0),
+	(6, 24,0),
+	(6, -24,0),
+	(6, -24,0),
+	(6, -24,0),
+	(6, -24,0),
+	(6, 23,0),
+	(6, 23,0),
+	(6, 23,0),
+	(6, 23,0),
+	(6, -23,0),
+	(6, -23,0),
+	(6, -23,0),
+	(6, -23,0),
+	(6, 22,0),
+	(6, 22,0),
+	(6, 22,0),
+	(6, 22,0),
+	(6, -22,0),
+	(6, -22,0),
+	(6, -22,0),
+	(6, -22,0),
+	(6, 21,0),
+	(6, 21,0),
+	(6, 21,0),
+	(6, 21,0),
+	(6, -21,0),
+	(6, -21,0),
+	(6, -21,0),
+	(6, -21,0),
+	(6, 20,0),
+	(6, 20,0),
+	(6, 20,0),
+	(6, 20,0),
+	(6, -20,0),
+	(6, -20,0),
+	(6, -20,0),
+	(6, -20,0),
+	(6, 19,0),
+	(6, 19,0),
+	(6, 19,0),
+	(6, 19,0),
+	(6, -19,0),
+	(6, -19,0),
+	(6, -19,0),
+	(6, -19,0),
+	(6, 18,0),
+	(6, 18,0),
+	(6, 18,0),
+	(6, 18,0),
+	(6, -18,0),
+	(6, -18,0),
+	(6, -18,0),
+	(6, -18,0),
+	(6, 17,0),
+	(6, 17,0),
+	(6, 17,0),
+	(6, 17,0),
+	(6, -17,0),
+	(6, -17,0),
+	(6, -17,0),
+	(6, -17,0),
+	(6, 16,0),
+	(6, 16,0),
+	(6, 16,0),
+	(6, 16,0),
+	(6, -16,0),
+	(6, -16,0),
+	(6, -16,0),
+	(6, -16,0),
+};
--- /dev/null
+++ b/appl/wm/mpeg/c0.vlc
@@ -1,0 +1,50 @@
+# Run/Level continuation 0
+# vlc -uUNDEF,UNDEF c0 < c0.vlc > c0.tab
+11111s	16,0
+11110s	17,0
+11101s	18,0
+11100s	19,0
+11011s	20,0
+11010s	21,0
+11001s	22,0
+11000s	23,0
+10111s	24,0
+10110s	25,0
+10101s	26,0
+10100s	27,0
+10011s	28,0
+10010s	29,0
+10001s	30,0
+10000s	31,0
+011000s	32,0
+010111s	33,0
+010110s	34,0
+010101s	35,0
+010100s	36,0
+010011s	37,0
+010010s	38,0
+010001s	39,0
+010000s	40,0
+011111s	8,1
+011110s	9,1
+011101s	10,1
+011100s	11,1
+011011s	12,1
+011010s	13,1
+011001s	14,1
+0010011s	15,1
+0010010s	16,1
+0010001s	17,1
+0010000s	18,1
+0010100s	3,6
+0011010s	2,11
+0011001s	2,12
+0011000s	2,13
+0010111s	2,14
+0010110s	2,15
+0010101s	2,16
+0011111s	1,27
+0011110s	1,28
+0011101s	1,29
+0011100s	1,30
+0011011s	1,31
--- /dev/null
+++ b/appl/wm/mpeg/c1.tab
@@ -1,0 +1,37 @@
+# vlc -cfp c1
+c1_size: con 32;
+c1_bits: con 5;
+c1_table:= array[] of {
+	(2,10),
+	(-2,10),
+	(2,9),
+	(-2,9),
+	(3,5),
+	(-3,5),
+	(4,3),
+	(-4,3),
+	(5,2),
+	(-5,2),
+	(7,1),
+	(-7,1),
+	(6,1),
+	(-6,1),
+	(15,0),
+	(-15,0),
+	(14,0),
+	(-14,0),
+	(13,0),
+	(-13,0),
+	(12,0),
+	(-12,0),
+	(1,26),
+	(-1,26),
+	(1,25),
+	(-1,25),
+	(1,24),
+	(-1,24),
+	(1,23),
+	(-1,23),
+	(1,22),
+	(-1,22),
+};
--- /dev/null
+++ b/appl/wm/mpeg/c1.vlc
@@ -1,0 +1,18 @@
+# Run/Level continuation 1
+# vlc -cfp c1 < c1.vlc > c1.tab
+1010s	12,0
+1001s	13,0
+1000s	14,0
+0111s	15,0
+0110s	6,1
+0101s	7,1
+0100s	5,2
+0011s	4,3
+0010s	3,5
+0001s	2,9
+0000s	2,10
+1111s	1,22
+1110s	1,23
+1101s	1,24
+1100s	1,25
+1011s	1,26
--- /dev/null
+++ b/appl/wm/mpeg/c2.tab
@@ -1,0 +1,21 @@
+# vlc -cfp c2
+c2_size: con 16;
+c2_bits: con 4;
+c2_table:= array[] of {
+	(11,0),
+	(-11,0),
+	(2,8),
+	(-2,8),
+	(3,4),
+	(-3,4),
+	(10,0),
+	(-10,0),
+	(4,2),
+	(-4,2),
+	(2,7),
+	(-2,7),
+	(1,21),
+	(-1,21),
+	(1,20),
+	(-1,20),
+};
--- /dev/null
+++ b/appl/wm/mpeg/c2.vlc
@@ -1,0 +1,10 @@
+# Run/Level continuation 2
+# vlc -cfp c2 < c2.vlc > c2.tab
+011s	10,0
+000s	11,0
+100s	4,2
+010s	3,4
+101s	2,7
+001s	2,8
+111s	1,20
+110s	1,21
--- /dev/null
+++ b/appl/wm/mpeg/c3.tab
@@ -1,0 +1,21 @@
+# vlc -cfp c3
+c3_size: con 16;
+c3_bits: con 4;
+c3_table:= array[] of {
+	(9,0),
+	(-9,0),
+	(1,19),
+	(-1,19),
+	(1,18),
+	(-1,18),
+	(5,1),
+	(-5,1),
+	(3,3),
+	(-3,3),
+	(8,0),
+	(-8,0),
+	(2,6),
+	(-2,6),
+	(1,17),
+	(-1,17),
+};
--- /dev/null
+++ b/appl/wm/mpeg/c3.vlc
@@ -1,0 +1,10 @@
+# Run/Level continuation 3
+# vlc -cfp c3 < c3.vlc > c3.tab
+101s	8,0
+000s	9,0
+011s	5,1
+100s	3,3
+110s	2,6
+111s	1,17
+010s	1,18
+001s	1,19
--- /dev/null
+++ b/appl/wm/mpeg/c4.tab
@@ -1,0 +1,9 @@
+# vlc -cfp c4
+c4_size: con 4;
+c4_bits: con 2;
+c4_table:= array[] of {
+	(1,16),
+	(-1,16),
+	(2,5),
+	(-2,5),
+};
--- /dev/null
+++ b/appl/wm/mpeg/c4.vlc
@@ -1,0 +1,4 @@
+# Run/Level continuation 4
+# vlc -cfp c4 < c4.vlc > c4.tab
+0s	1,16
+1s	2,5
--- /dev/null
+++ b/appl/wm/mpeg/c5.tab
@@ -1,0 +1,9 @@
+# vlc -cfp c5
+c5_size: con 4;
+c5_bits: con 2;
+c5_table:= array[] of {
+	(7,0),
+	(-7,0),
+	(3,2),
+	(-3,2),
+};
--- /dev/null
+++ b/appl/wm/mpeg/c5.vlc
@@ -1,0 +1,4 @@
+# Run/Level continuation 5
+# vlc -cfp c5 < c5.vlc > c5.tab
+0s	7,0
+1s	3,2
--- /dev/null
+++ b/appl/wm/mpeg/c6.tab
@@ -1,0 +1,9 @@
+# vlc -cfp c6
+c6_size: con 4;
+c6_bits: con 2;
+c6_table:= array[] of {
+	(4,1),
+	(-4,1),
+	(1,15),
+	(-1,15),
+};
--- /dev/null
+++ b/appl/wm/mpeg/c6.vlc
@@ -1,0 +1,4 @@
+# Run/Level continuation 6
+# vlc -cfp c6 < c6.vlc > c6.tab
+0s	4,1
+1s	1,15
--- /dev/null
+++ b/appl/wm/mpeg/c7.tab
@@ -1,0 +1,9 @@
+# vlc -cfp c7
+c7_size: con 4;
+c7_bits: con 2;
+c7_table:= array[] of {
+	(1,14),
+	(-1,14),
+	(2,4),
+	(-2,4),
+};
--- /dev/null
+++ b/appl/wm/mpeg/c7.vlc
@@ -1,0 +1,4 @@
+# Run/Level continuation 7
+# vlc -cfp c7 < c7.vlc > c7.tab
+0s	1,14
+1s	2,4
--- /dev/null
+++ b/appl/wm/mpeg/cbp.tab
@@ -1,0 +1,517 @@
+# vlc cbp
+cbp_size: con 512;
+cbp_bits: con 9;
+cbp_table:= array[] of {
+	(0, UNDEF),
+	(0, UNDEF),
+	(9, 39),
+	(9, 27),
+	(9, 59),
+	(9, 55),
+	(9, 47),
+	(9, 31),
+	(8, 58),
+	(8, 58),
+	(8, 54),
+	(8, 54),
+	(8, 46),
+	(8, 46),
+	(8, 30),
+	(8, 30),
+	(8, 57),
+	(8, 57),
+	(8, 53),
+	(8, 53),
+	(8, 45),
+	(8, 45),
+	(8, 29),
+	(8, 29),
+	(8, 38),
+	(8, 38),
+	(8, 26),
+	(8, 26),
+	(8, 37),
+	(8, 37),
+	(8, 25),
+	(8, 25),
+	(8, 43),
+	(8, 43),
+	(8, 23),
+	(8, 23),
+	(8, 51),
+	(8, 51),
+	(8, 15),
+	(8, 15),
+	(8, 42),
+	(8, 42),
+	(8, 22),
+	(8, 22),
+	(8, 50),
+	(8, 50),
+	(8, 14),
+	(8, 14),
+	(8, 41),
+	(8, 41),
+	(8, 21),
+	(8, 21),
+	(8, 49),
+	(8, 49),
+	(8, 13),
+	(8, 13),
+	(8, 35),
+	(8, 35),
+	(8, 19),
+	(8, 19),
+	(8, 11),
+	(8, 11),
+	(8, 7),
+	(8, 7),
+	(7, 34),
+	(7, 34),
+	(7, 34),
+	(7, 34),
+	(7, 18),
+	(7, 18),
+	(7, 18),
+	(7, 18),
+	(7, 10),
+	(7, 10),
+	(7, 10),
+	(7, 10),
+	(7, 6),
+	(7, 6),
+	(7, 6),
+	(7, 6),
+	(7, 33),
+	(7, 33),
+	(7, 33),
+	(7, 33),
+	(7, 17),
+	(7, 17),
+	(7, 17),
+	(7, 17),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 5),
+	(7, 5),
+	(7, 5),
+	(7, 5),
+	(6, 63),
+	(6, 63),
+	(6, 63),
+	(6, 63),
+	(6, 63),
+	(6, 63),
+	(6, 63),
+	(6, 63),
+	(6, 3),
+	(6, 3),
+	(6, 3),
+	(6, 3),
+	(6, 3),
+	(6, 3),
+	(6, 3),
+	(6, 3),
+	(6, 36),
+	(6, 36),
+	(6, 36),
+	(6, 36),
+	(6, 36),
+	(6, 36),
+	(6, 36),
+	(6, 36),
+	(6, 24),
+	(6, 24),
+	(6, 24),
+	(6, 24),
+	(6, 24),
+	(6, 24),
+	(6, 24),
+	(6, 24),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 62),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 2),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 61),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 1),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 56),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 52),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 44),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 28),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 40),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 20),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 48),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(5, 12),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 32),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 16),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 8),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+	(3, 60),
+};
--- /dev/null
+++ b/appl/wm/mpeg/cbp.vlc
@@ -1,0 +1,65 @@
+# Coded Block Pattern
+# vlc cbp < cbp.vlc > cbp.tab
+01011	1
+01001	2
+001101	3
+1101	4
+0010111	5
+0010011	6
+00011111	7
+1100	8
+0010110	9
+0010010	10
+00011110	11
+10011	12
+00011011	13
+00010111	14
+00010011	15
+1011	16
+0010101	17
+0010001	18
+00011101	19
+10001	20
+00011001	21
+00010101	22
+00010001	23
+001111	24
+00001111	25
+00001101	26
+000000011	27
+01111	28
+00001011	29
+00000111	30
+000000111	31
+1010	32
+0010100	33
+0010000	34
+00011100	35
+001110	36
+00001110	37
+00001100	38
+000000010	39
+10000	40
+00011000	41
+00010100	42
+00010000	43
+01110	44
+00001010	45
+00000110	46
+000000110	47
+10010	48
+00011010	49
+00010110	50
+00010010	51
+01101	52
+00001001	53
+00000101	54
+000000101	55
+01100	56
+00001000	57
+00000100	58
+000000100	59
+111	60
+01010	61
+01000	62
+001100	63
--- /dev/null
+++ b/appl/wm/mpeg/cdc.tab
@@ -1,0 +1,261 @@
+# vlc cdc
+cdc_size: con 256;
+cdc_bits: con 8;
+cdc_table:= array[] of {
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 0),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(5, 5),
+	(5, 5),
+	(5, 5),
+	(5, 5),
+	(5, 5),
+	(5, 5),
+	(5, 5),
+	(5, 5),
+	(6, 6),
+	(6, 6),
+	(6, 6),
+	(6, 6),
+	(7, 7),
+	(7, 7),
+	(8, 8),
+	(0, UNDEF),
+};
--- /dev/null
+++ b/appl/wm/mpeg/cdc.vlc
@@ -1,0 +1,11 @@
+# Chrominance DC
+# vlc cdc < cdc.vlc > cdc.tab
+00	0
+01	1
+10	2
+110	3
+1110	4
+11110	5
+111110	6
+1111110	7
+11111110	8
--- /dev/null
+++ b/appl/wm/mpeg/closest.m
@@ -1,0 +1,514 @@
+closest := array[16*16*16] of {
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 255,byte 255,byte 250,byte 250,byte 250,byte 220,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 250,byte 250,byte 250,byte 250,byte 249,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 250,byte 250,byte 250,byte 233,byte 233,byte 249,
+	byte 249,byte 232,byte 215,byte 215,byte 248,byte 231,byte 214,byte 197,
+	byte 234,byte 234,byte 250,byte 250,byte 233,byte 233,byte 216,byte 216,
+	byte 249,byte 232,byte 215,byte 198,byte 198,byte 231,byte 214,byte 197,
+	byte 217,byte 217,byte 217,byte 246,byte 233,byte 216,byte 216,byte 199,
+	byte 199,byte 215,byte 215,byte 198,byte 198,byte 198,byte 214,byte 197,
+	byte 200,byte 200,byte 246,byte 246,byte 246,byte 216,byte 199,byte 199,
+	byte 245,byte 245,byte 198,byte 244,byte 244,byte 244,byte 227,byte 197,
+	byte 247,byte 247,byte 246,byte 246,byte 246,byte 246,byte 199,byte 245,
+	byte 245,byte 245,byte 228,byte 244,byte 244,byte 244,byte 227,byte 193,
+	byte 230,byte 230,byte 246,byte 246,byte 229,byte 229,byte 212,byte 245,
+	byte 245,byte 228,byte 228,byte 211,byte 244,byte 227,byte 210,byte 193,
+	byte 213,byte 213,byte 229,byte 229,byte 212,byte 212,byte 212,byte 195,
+	byte 228,byte 228,byte 211,byte 211,byte 194,byte 227,byte 210,byte 193,
+	byte 196,byte 196,byte 242,byte 242,byte 212,byte 195,byte 195,byte 241,
+	byte 241,byte 211,byte 211,byte 194,byte 194,byte 240,byte 210,byte 193,
+	byte 243,byte 243,byte 242,byte 242,byte 242,byte 195,byte 195,byte 241,
+	byte 241,byte 241,byte 194,byte 194,byte 240,byte 240,byte 239,byte 205,
+	byte 226,byte 226,byte 242,byte 242,byte 225,byte 225,byte 195,byte 241,
+	byte 241,byte 224,byte 224,byte 240,byte 240,byte 239,byte 239,byte 205,
+	byte 209,byte 209,byte 225,byte 225,byte 208,byte 208,byte 208,byte 224,
+	byte 224,byte 223,byte 223,byte 223,byte 239,byte 239,byte 222,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 255,byte 255,byte 255,byte 254,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 238,byte 221,byte 221,byte 254,byte 237,byte 220,byte 203,
+	byte 253,byte 236,byte 219,byte 202,byte 252,byte 235,byte 218,byte 201,
+	byte 255,byte 221,byte 221,byte 221,byte 204,byte 250,byte 220,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 221,byte 221,byte 204,byte 250,byte 250,byte 249,byte 249,
+	byte 249,byte 249,byte 232,byte 248,byte 248,byte 248,byte 231,byte 201,
+	byte 251,byte 251,byte 204,byte 250,byte 250,byte 233,byte 233,byte 249,
+	byte 249,byte 232,byte 215,byte 215,byte 248,byte 231,byte 214,byte 197,
+	byte 234,byte 234,byte 250,byte 250,byte 233,byte 233,byte 216,byte 216,
+	byte 249,byte 232,byte 215,byte 198,byte 198,byte 231,byte 214,byte 197,
+	byte 217,byte 217,byte 217,byte 246,byte 233,byte 216,byte 216,byte 199,
+	byte 199,byte 215,byte 215,byte 198,byte 198,byte 198,byte 214,byte 197,
+	byte 200,byte 200,byte 246,byte 246,byte 246,byte 216,byte 199,byte 199,
+	byte 245,byte 245,byte 198,byte 244,byte 244,byte 244,byte 227,byte 197,
+	byte 247,byte 247,byte 246,byte 246,byte 246,byte 246,byte 199,byte 245,
+	byte 245,byte 245,byte 228,byte 244,byte 244,byte 244,byte 227,byte 193,
+	byte 230,byte 230,byte 246,byte 246,byte 229,byte 229,byte 212,byte 245,
+	byte 245,byte 228,byte 228,byte 211,byte 244,byte 227,byte 210,byte 193,
+	byte 213,byte 213,byte 229,byte 229,byte 212,byte 212,byte 212,byte 195,
+	byte 228,byte 228,byte 211,byte 211,byte 194,byte 227,byte 210,byte 193,
+	byte 196,byte 196,byte 242,byte 242,byte 212,byte 195,byte 195,byte 241,
+	byte 241,byte 211,byte 211,byte 194,byte 194,byte 240,byte 210,byte 193,
+	byte 243,byte 243,byte 242,byte 242,byte 242,byte 195,byte 195,byte 241,
+	byte 241,byte 241,byte 194,byte 194,byte 240,byte 240,byte 239,byte 205,
+	byte 226,byte 226,byte 242,byte 242,byte 225,byte 225,byte 195,byte 241,
+	byte 241,byte 224,byte 224,byte 240,byte 240,byte 239,byte 239,byte 205,
+	byte 209,byte 209,byte 225,byte 225,byte 208,byte 208,byte 208,byte 224,
+	byte 224,byte 223,byte 223,byte 223,byte 239,byte 239,byte 222,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 255,byte 255,byte 255,byte 191,byte 191,byte 191,byte 220,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 255,byte 221,byte 221,byte 221,byte 204,byte 191,byte 220,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 255,byte 221,byte 221,byte 204,byte 204,byte 204,byte 186,byte 186,
+	byte 186,byte 186,byte 186,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 221,byte 204,byte 204,byte 204,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 232,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 233,byte 216,byte 186,
+	byte 186,byte 186,byte 215,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 217,byte 217,byte 183,byte 183,byte 183,byte 216,byte 216,byte 199,
+	byte 182,byte 182,byte 215,byte 198,byte 198,byte 181,byte 214,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 199,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 181,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 193,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 228,byte 165,byte 181,byte 181,byte 164,byte 164,byte 193,
+	byte 167,byte 167,byte 183,byte 229,byte 166,byte 212,byte 212,byte 182,
+	byte 182,byte 165,byte 211,byte 211,byte 181,byte 164,byte 210,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 195,byte 178,
+	byte 178,byte 178,byte 211,byte 194,byte 177,byte 177,byte 177,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 195,byte 178,
+	byte 178,byte 178,byte 178,byte 177,byte 177,byte 177,byte 177,byte 205,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 178,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 177,byte 160,byte 205,
+	byte 163,byte 163,byte 162,byte 162,byte 162,byte 162,byte 208,byte 178,
+	byte 161,byte 161,byte 223,byte 177,byte 177,byte 160,byte 160,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 176,byte 176,byte 191,byte 191,byte 191,byte 191,byte 190,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 176,byte 221,byte 221,byte 204,byte 191,byte 191,byte 190,byte 190,
+	byte 190,byte 190,byte 173,byte 189,byte 189,byte 189,byte 172,byte 201,
+	byte 188,byte 221,byte 204,byte 204,byte 204,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 173,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 201,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 197,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 168,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 170,byte 170,byte 182,
+	byte 182,byte 169,byte 152,byte 152,byte 181,byte 168,byte 151,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 197,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 165,byte 181,byte 181,byte 181,byte 164,byte 193,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 164,byte 193,
+	byte 167,byte 167,byte 167,byte 166,byte 166,byte 166,byte 149,byte 182,
+	byte 165,byte 165,byte 165,byte 148,byte 181,byte 164,byte 147,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 149,byte 178,
+	byte 178,byte 178,byte 148,byte 177,byte 177,byte 177,byte 147,byte 193,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 178,byte 178,
+	byte 178,byte 178,byte 178,byte 177,byte 177,byte 177,byte 160,byte 205,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 162,byte 162,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 160,byte 160,byte 205,
+	byte 163,byte 163,byte 162,byte 162,byte 162,byte 162,byte 145,byte 161,
+	byte 161,byte 161,byte 144,byte 144,byte 160,byte 160,byte 160,byte 205,
+	byte 192,byte 192,byte 192,byte 192,byte 207,byte 207,byte 207,byte 207,
+	byte 206,byte 206,byte 206,byte 206,byte 205,byte 205,byte 205,byte 205,
+	byte 176,byte 176,byte 191,byte 191,byte 191,byte 174,byte 174,byte 190,
+	byte 190,byte 173,byte 156,byte 156,byte 189,byte 172,byte 155,byte 138,
+	byte 176,byte 176,byte 204,byte 191,byte 191,byte 174,byte 174,byte 190,
+	byte 190,byte 173,byte 156,byte 156,byte 189,byte 172,byte 155,byte 138,
+	byte 188,byte 204,byte 204,byte 204,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 186,byte 186,
+	byte 186,byte 186,byte 169,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 187,byte 187,byte 170,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 152,byte 152,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 183,byte 183,byte 170,byte 170,byte 170,byte 153,
+	byte 182,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 153,byte 182,
+	byte 182,byte 182,byte 182,byte 181,byte 181,byte 181,byte 164,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 183,byte 182,byte 182,
+	byte 182,byte 182,byte 165,byte 181,byte 181,byte 181,byte 164,byte 130,
+	byte 167,byte 167,byte 183,byte 183,byte 166,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 166,byte 166,byte 166,byte 149,byte 149,byte 182,
+	byte 165,byte 165,byte 148,byte 148,byte 164,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 179,byte 179,byte 179,byte 149,byte 132,byte 178,
+	byte 178,byte 178,byte 148,byte 131,byte 177,byte 177,byte 147,byte 130,
+	byte 180,byte 180,byte 179,byte 179,byte 179,byte 179,byte 132,byte 178,
+	byte 178,byte 178,byte 161,byte 177,byte 177,byte 177,byte 160,byte 142,
+	byte 163,byte 163,byte 179,byte 179,byte 162,byte 162,byte 162,byte 178,
+	byte 178,byte 161,byte 161,byte 177,byte 177,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 162,byte 162,byte 145,byte 145,byte 145,byte 161,
+	byte 161,byte 144,byte 144,byte 144,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 175,byte 175,byte 191,byte 191,byte 174,byte 174,byte 157,byte 157,
+	byte 190,byte 173,byte 156,byte 139,byte 139,byte 172,byte 155,byte 138,
+	byte 175,byte 175,byte 191,byte 191,byte 174,byte 174,byte 157,byte 157,
+	byte 190,byte 173,byte 156,byte 139,byte 139,byte 172,byte 155,byte 138,
+	byte 188,byte 188,byte 204,byte 187,byte 187,byte 187,byte 157,byte 186,
+	byte 186,byte 186,byte 156,byte 185,byte 185,byte 185,byte 168,byte 138,
+	byte 188,byte 188,byte 187,byte 187,byte 187,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 169,byte 185,byte 185,byte 168,byte 168,byte 138,
+	byte 171,byte 171,byte 187,byte 187,byte 170,byte 170,byte 170,byte 186,
+	byte 186,byte 169,byte 152,byte 152,byte 185,byte 168,byte 151,byte 134,
+	byte 171,byte 171,byte 187,byte 170,byte 170,byte 170,byte 170,byte 153,
+	byte 169,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 170,byte 153,byte 153,
+	byte 169,byte 152,byte 152,byte 135,byte 135,byte 135,byte 151,byte 134,
+	byte 154,byte 154,byte 183,byte 183,byte 183,byte 153,byte 153,byte 153,
+	byte 182,byte 182,byte 135,byte 135,byte 181,byte 181,byte 164,byte 134,
+	byte 184,byte 184,byte 183,byte 183,byte 183,byte 166,byte 166,byte 182,
+	byte 182,byte 165,byte 165,byte 181,byte 181,byte 164,byte 164,byte 130,
+	byte 167,byte 167,byte 183,byte 166,byte 166,byte 166,byte 149,byte 182,
+	byte 165,byte 165,byte 165,byte 148,byte 181,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 150,byte 166,byte 149,byte 149,byte 149,byte 132,
+	byte 165,byte 165,byte 148,byte 148,byte 131,byte 147,byte 147,byte 130,
+	byte 133,byte 133,byte 179,byte 179,byte 149,byte 132,byte 132,byte 132,
+	byte 178,byte 148,byte 148,byte 131,byte 131,byte 131,byte 130,byte 130,
+	byte 133,byte 133,byte 179,byte 179,byte 179,byte 132,byte 132,byte 178,
+	byte 178,byte 178,byte 131,byte 131,byte 131,byte 177,byte 160,byte 142,
+	byte 163,byte 163,byte 179,byte 162,byte 162,byte 162,byte 132,byte 178,
+	byte 161,byte 161,byte 144,byte 131,byte 177,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 162,byte 162,byte 145,byte 145,byte 145,byte 161,
+	byte 161,byte 144,byte 144,byte 143,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 158,byte 158,byte 158,byte 112,byte 174,byte 157,byte 157,byte 140,
+	byte 140,byte 156,byte 156,byte 139,byte 139,byte 139,byte 155,byte 138,
+	byte 158,byte 158,byte 158,byte 112,byte 174,byte 157,byte 157,byte 140,
+	byte 140,byte 156,byte 156,byte 139,byte 139,byte 139,byte 155,byte 138,
+	byte 158,byte 158,byte 124,byte 124,byte 124,byte 157,byte 157,byte 140,
+	byte 123,byte 123,byte 156,byte 139,byte 139,byte 122,byte 155,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 170,byte 170,byte 123,
+	byte 123,byte 169,byte 152,byte 152,byte 122,byte 168,byte 151,byte 138,
+	byte 171,byte 171,byte 124,byte 124,byte 170,byte 170,byte 170,byte 153,
+	byte 123,byte 169,byte 152,byte 135,byte 135,byte 168,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 170,byte 153,byte 153,
+	byte 169,byte 152,byte 152,byte 135,byte 135,byte 135,byte 151,byte 134,
+	byte 154,byte 154,byte 154,byte 170,byte 170,byte 153,byte 153,byte 153,
+	byte 136,byte 152,byte 135,byte 135,byte 135,byte 135,byte 134,byte 134,
+	byte 137,byte 137,byte 137,byte 120,byte 153,byte 153,byte 153,byte 136,
+	byte 136,byte 136,byte 135,byte 135,byte 135,byte 118,byte 164,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 166,byte 136,byte 136,
+	byte 136,byte 165,byte 165,byte 118,byte 118,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 120,byte 166,byte 166,byte 149,byte 149,byte 136,
+	byte 165,byte 165,byte 148,byte 148,byte 118,byte 164,byte 147,byte 130,
+	byte 150,byte 150,byte 150,byte 149,byte 149,byte 149,byte 132,byte 132,
+	byte 165,byte 148,byte 148,byte 131,byte 131,byte 147,byte 147,byte 130,
+	byte 133,byte 133,byte 133,byte 149,byte 132,byte 132,byte 132,byte 132,
+	byte 115,byte 148,byte 131,byte 131,byte 131,byte 131,byte 130,byte 130,
+	byte 133,byte 133,byte 133,byte 116,byte 132,byte 132,byte 132,byte 132,
+	byte 115,byte 115,byte 131,byte 131,byte 131,byte 131,byte 160,byte 142,
+	byte 133,byte 133,byte 116,byte 162,byte 162,byte 132,byte 132,byte 115,
+	byte 161,byte 161,byte 144,byte 131,byte 131,byte 160,byte 160,byte 142,
+	byte 146,byte 146,byte 146,byte 145,byte 145,byte 145,byte 128,byte 161,
+	byte 144,byte 144,byte 144,byte 143,byte 160,byte 160,byte 159,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 141,byte 141,byte 112,byte 112,byte 112,byte 157,byte 140,byte 140,
+	byte 140,byte 127,byte 139,byte 126,byte 126,byte 126,byte 109,byte 138,
+	byte 141,byte 141,byte 112,byte 112,byte 112,byte 157,byte 140,byte 140,
+	byte 140,byte 127,byte 139,byte 126,byte 126,byte 126,byte 109,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 140,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 122,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 105,byte 138,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 153,byte 123,
+	byte 123,byte 123,byte 152,byte 122,byte 122,byte 122,byte 105,byte 134,
+	byte 154,byte 154,byte 124,byte 124,byte 124,byte 153,byte 153,byte 153,
+	byte 123,byte 123,byte 135,byte 135,byte 122,byte 122,byte 105,byte 134,
+	byte 137,byte 137,byte 137,byte 120,byte 153,byte 153,byte 153,byte 136,
+	byte 136,byte 136,byte 135,byte 135,byte 135,byte 118,byte 105,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 153,byte 136,byte 136,
+	byte 136,byte 119,byte 119,byte 118,byte 118,byte 118,byte 118,byte 134,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 120,byte 136,byte 136,
+	byte 119,byte 119,byte 119,byte 118,byte 118,byte 118,byte 101,byte 130,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte 130,
+	byte 133,byte 133,byte 120,byte 120,byte 149,byte 132,byte 132,byte 119,
+	byte 119,byte 102,byte 148,byte 131,byte 131,byte 101,byte 101,byte 130,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 132,byte 132,byte 115,
+	byte 115,byte 115,byte 131,byte 131,byte 114,byte 114,byte 114,byte 130,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 132,byte 115,
+	byte 115,byte 115,byte 131,byte 114,byte 114,byte 114,byte 114,byte 142,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte  98,byte 114,byte 114,byte 114,byte  97,byte 142,
+	byte 100,byte 100,byte 116,byte  99,byte  99,byte  99,byte  99,byte 115,
+	byte  98,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte 142,
+	byte 129,byte 129,byte 129,byte 129,byte 128,byte 128,byte 128,byte 128,
+	byte 143,byte 143,byte 143,byte 143,byte 142,byte 142,byte 142,byte 142,
+	byte 113,byte 113,byte 112,byte 112,byte 112,byte 112,byte 140,byte 140,
+	byte 127,byte 127,byte 110,byte 126,byte 126,byte 126,byte 109,byte  75,
+	byte 113,byte 113,byte 112,byte 112,byte 112,byte 112,byte 140,byte 140,
+	byte 127,byte 127,byte 110,byte 126,byte 126,byte 126,byte 109,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 123,byte 122,byte 122,byte 122,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 122,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 122,byte 105,byte  71,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte 105,byte  71,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 107,byte 136,byte 136,
+	byte 136,byte 106,byte 106,byte 118,byte 118,byte 105,byte  88,byte  71,
+	byte 137,byte 137,byte 120,byte 120,byte 120,byte 120,byte 136,byte 136,
+	byte 119,byte 119,byte 119,byte 118,byte 118,byte 118,byte 101,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte  67,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 103,byte 103,byte 119,
+	byte 119,byte 102,byte 102,byte 118,byte 118,byte 101,byte 101,byte  67,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte 115,byte 114,byte 114,byte 114,byte 114,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte 115,byte 114,byte 114,byte 114,byte  97,byte  79,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte  99,byte  99,byte 115,
+	byte 115,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  99,byte  82,byte  98,
+	byte  98,byte  98,byte  81,byte 114,byte  97,byte  97,byte  97,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  96,byte  96,byte 112,byte 112,byte 111,byte 111,byte  94,byte 127,
+	byte 127,byte 110,byte 110,byte  93,byte 126,byte 109,byte  92,byte  75,
+	byte  96,byte  96,byte 112,byte 112,byte 111,byte 111,byte  94,byte 127,
+	byte 127,byte 110,byte 110,byte  93,byte 126,byte 109,byte  92,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 124,byte 123,byte 123,
+	byte 123,byte 123,byte 106,byte 122,byte 122,byte 105,byte 105,byte  75,
+	byte 125,byte 125,byte 124,byte 124,byte 124,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte 105,byte  75,
+	byte 108,byte 108,byte 124,byte 124,byte 107,byte 107,byte 107,byte 123,
+	byte 123,byte 106,byte 106,byte 122,byte 122,byte 105,byte  88,byte  71,
+	byte 108,byte 108,byte 124,byte 107,byte 107,byte 107,byte  90,byte 123,
+	byte 106,byte 106,byte 106,byte  89,byte 122,byte 105,byte  88,byte  71,
+	byte  91,byte  91,byte 120,byte 107,byte 107,byte  90,byte  90,byte 136,
+	byte 106,byte 106,byte  89,byte  89,byte 118,byte 105,byte  88,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 120,byte 136,byte 119,
+	byte 119,byte 119,byte 102,byte 118,byte 118,byte 118,byte 101,byte  71,
+	byte 121,byte 121,byte 120,byte 120,byte 120,byte 103,byte 103,byte 119,
+	byte 119,byte 102,byte 102,byte 118,byte 118,byte 101,byte 101,byte  67,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 104,byte 104,byte 103,byte 103,byte 103,byte 103,byte  86,byte 102,
+	byte 102,byte 102,byte  85,byte  85,byte 101,byte 101,byte  84,byte  67,
+	byte  87,byte  87,byte 116,byte 116,byte 116,byte  86,byte  86,byte 115,
+	byte 115,byte 115,byte  85,byte  85,byte 114,byte 114,byte  84,byte  67,
+	byte 117,byte 117,byte 116,byte 116,byte 116,byte 116,byte 115,byte 115,
+	byte 115,byte 115,byte  98,byte 114,byte 114,byte 114,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  99,byte  99,byte 115,
+	byte  98,byte  98,byte  98,byte 114,byte 114,byte  97,byte  97,byte  79,
+	byte  83,byte  83,byte  99,byte  99,byte  82,byte  82,byte  82,byte  98,
+	byte  98,byte  81,byte  81,byte  81,byte  97,byte  97,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  95,byte  95,byte 111,byte 111,byte  94,byte  94,byte  94,byte  77,
+	byte 110,byte 110,byte  93,byte  93,byte  76,byte 109,byte  92,byte  75,
+	byte  95,byte  95,byte 111,byte 111,byte  94,byte  94,byte  94,byte  77,
+	byte 110,byte 110,byte  93,byte  93,byte  76,byte 109,byte  92,byte  75,
+	byte 108,byte 108,byte 124,byte 111,byte 107,byte  94,byte  94,byte 123,
+	byte 123,byte 106,byte  93,byte  93,byte 122,byte 105,byte  92,byte  75,
+	byte 108,byte 108,byte 108,byte 107,byte 107,byte 107,byte  90,byte 123,
+	byte 106,byte 106,byte 106,byte  89,byte 122,byte 105,byte  88,byte  75,
+	byte  91,byte  91,byte 107,byte 107,byte 107,byte  90,byte  90,byte 123,
+	byte 106,byte 106,byte  89,byte  89,byte 105,byte 105,byte  88,byte  71,
+	byte  91,byte  91,byte  91,byte 107,byte  90,byte  90,byte  90,byte  73,
+	byte 106,byte 106,byte  89,byte  89,byte  72,byte  88,byte  88,byte  71,
+	byte  91,byte  91,byte  91,byte  90,byte  90,byte  90,byte  73,byte  73,
+	byte 106,byte  89,byte  89,byte  72,byte  72,byte  88,byte  88,byte  71,
+	byte  74,byte  74,byte 120,byte 120,byte 120,byte  73,byte  73,byte 119,
+	byte 119,byte 102,byte  89,byte  72,byte  72,byte 101,byte 101,byte  71,
+	byte 104,byte 104,byte 120,byte 103,byte 103,byte 103,byte 103,byte 119,
+	byte 102,byte 102,byte 102,byte 118,byte 118,byte 101,byte  84,byte  67,
+	byte 104,byte 104,byte 103,byte 103,byte 103,byte 103,byte  86,byte 102,
+	byte 102,byte 102,byte  85,byte  85,byte 101,byte 101,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte 103,byte  86,byte  86,byte  86,byte  86,
+	byte 102,byte  85,byte  85,byte  85,byte  85,byte  84,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte  86,byte  86,byte  86,byte  69,byte  69,
+	byte 115,byte  85,byte  85,byte  85,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte 116,byte 116,byte  99,byte  69,byte  69,byte  69,
+	byte 115,byte  98,byte  85,byte  68,byte  68,byte  97,byte  97,byte  79,
+	byte 100,byte 100,byte  99,byte  99,byte  99,byte  82,byte  82,byte  98,
+	byte  98,byte  98,byte  81,byte  68,byte  97,byte  97,byte  97,byte  79,
+	byte  83,byte  83,byte  83,byte  82,byte  82,byte  82,byte  82,byte  98,
+	byte  81,byte  81,byte  81,byte  64,byte  97,byte  97,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  78,byte  78,byte  49,byte  49,byte  94,byte  77,byte  77,byte  48,
+	byte  48,byte  93,byte  93,byte  76,byte  76,byte  63,byte  92,byte  75,
+	byte  78,byte  78,byte  49,byte  49,byte  94,byte  77,byte  77,byte  48,
+	byte  48,byte  93,byte  93,byte  76,byte  76,byte  63,byte  92,byte  75,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  77,byte  60,
+	byte  60,byte  60,byte  93,byte  76,byte  59,byte  59,byte  59,byte  75,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  90,byte  60,
+	byte  60,byte  60,byte  89,byte  59,byte  59,byte  59,byte  88,byte  75,
+	byte  91,byte  91,byte  61,byte  61,byte  61,byte  90,byte  73,byte  60,
+	byte  60,byte  60,byte  89,byte  72,byte  59,byte  59,byte  88,byte  71,
+	byte  74,byte  74,byte  61,byte  61,byte  90,byte  73,byte  73,byte  73,
+	byte  60,byte  89,byte  89,byte  72,byte  72,byte  72,byte  71,byte  71,
+	byte  74,byte  74,byte  74,byte  90,byte  73,byte  73,byte  73,byte  73,
+	byte  56,byte  89,byte  72,byte  72,byte  72,byte  72,byte  71,byte  71,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  73,byte  73,byte  56,
+	byte  56,byte  56,byte  72,byte  72,byte  55,byte  55,byte  55,byte  71,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  56,byte  55,byte  55,byte  55,byte  55,byte  67,
+	byte  87,byte  87,byte  57,byte  57,byte  57,byte  86,byte  86,byte  56,
+	byte  56,byte  56,byte  85,byte  85,byte  55,byte  55,byte  84,byte  67,
+	byte  87,byte  87,byte  87,byte  86,byte  86,byte  86,byte  69,byte  69,
+	byte  56,byte  85,byte  85,byte  85,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte  70,byte  53,byte  69,byte  69,byte  69,byte  69,
+	byte  52,byte  85,byte  85,byte  68,byte  68,byte  68,byte  67,byte  67,
+	byte  70,byte  70,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  68,byte  51,byte  51,byte  79,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  80,byte  79,
+	byte  83,byte  83,byte  53,byte  82,byte  82,byte  65,byte  65,byte  52,
+	byte  52,byte  81,byte  64,byte  64,byte  51,byte  80,byte  80,byte  79,
+	byte  66,byte  66,byte  66,byte  66,byte  65,byte  65,byte  65,byte  65,
+	byte  64,byte  64,byte  64,byte  64,byte  79,byte  79,byte  79,byte  79,
+	byte  50,byte  50,byte  49,byte  49,byte  49,byte  77,byte  77,byte  48,
+	byte  48,byte  48,byte  76,byte  76,byte  63,byte  63,byte  46,byte  12,
+	byte  50,byte  50,byte  49,byte  49,byte  49,byte  77,byte  77,byte  48,
+	byte  48,byte  48,byte  76,byte  76,byte  63,byte  63,byte  46,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  77,byte  60,
+	byte  60,byte  60,byte  60,byte  59,byte  59,byte  59,byte  59,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  60,byte  60,
+	byte  60,byte  60,byte  60,byte  59,byte  59,byte  59,byte  42,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  73,byte  60,
+	byte  60,byte  60,byte  43,byte  59,byte  59,byte  59,byte  42,byte   8,
+	byte  74,byte  74,byte  61,byte  61,byte  61,byte  73,byte  73,byte  60,
+	byte  60,byte  60,byte  72,byte  72,byte  72,byte  59,byte  42,byte   8,
+	byte  74,byte  74,byte  74,byte  57,byte  73,byte  73,byte  73,byte  73,
+	byte  56,byte  56,byte  72,byte  72,byte  72,byte  72,byte  42,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  73,byte  56,
+	byte  56,byte  56,byte  72,byte  55,byte  55,byte  55,byte  55,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  56,byte  55,byte  55,byte  55,byte  38,byte   4,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  39,byte  55,byte  55,byte  55,byte  38,byte   4,
+	byte  70,byte  70,byte  57,byte  57,byte  40,byte  69,byte  69,byte  69,
+	byte  56,byte  39,byte  85,byte  68,byte  68,byte  38,byte  38,byte   4,
+	byte  70,byte  70,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  68,byte  51,byte  51,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  51,byte   0,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  53,byte  69,byte  52,
+	byte  52,byte  52,byte  35,byte  51,byte  51,byte  51,byte  34,byte   0,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  33,byte  33,byte  49,byte  49,byte  32,byte  32,byte  77,byte  48,
+	byte  48,byte  47,byte  47,byte  63,byte  63,byte  46,byte  46,byte  12,
+	byte  33,byte  33,byte  49,byte  49,byte  32,byte  32,byte  77,byte  48,
+	byte  48,byte  47,byte  47,byte  63,byte  63,byte  46,byte  46,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  61,byte  60,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  59,byte  42,byte  12,
+	byte  62,byte  62,byte  61,byte  61,byte  61,byte  44,byte  44,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  42,byte  42,byte  12,
+	byte  45,byte  45,byte  61,byte  61,byte  44,byte  44,byte  44,byte  60,
+	byte  60,byte  43,byte  43,byte  59,byte  59,byte  42,byte  42,byte   8,
+	byte  45,byte  45,byte  61,byte  44,byte  44,byte  44,byte  73,byte  60,
+	byte  43,byte  43,byte  26,byte  72,byte  59,byte  42,byte  42,byte   8,
+	byte  74,byte  74,byte  57,byte  44,byte  44,byte  73,byte  73,byte  56,
+	byte  43,byte  43,byte  26,byte  72,byte  72,byte  42,byte  42,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  57,byte  56,byte  56,
+	byte  56,byte  56,byte  39,byte  55,byte  55,byte  55,byte  38,byte   8,
+	byte  58,byte  58,byte  57,byte  57,byte  57,byte  40,byte  40,byte  56,
+	byte  56,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   4,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  40,byte  40,byte  56,
+	byte  39,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   4,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  23,byte  23,byte  39,
+	byte  39,byte  39,byte  22,byte  68,byte  38,byte  38,byte  38,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  69,byte  69,byte  52,
+	byte  52,byte  52,byte  68,byte  68,byte  51,byte  51,byte  21,byte   4,
+	byte  54,byte  54,byte  53,byte  53,byte  53,byte  53,byte  69,byte  52,
+	byte  52,byte  52,byte  35,byte  51,byte  51,byte  51,byte  34,byte   0,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte  37,byte  37,byte  36,byte  36,byte  36,byte  36,byte  36,byte  35,
+	byte  35,byte  35,byte  35,byte  18,byte  34,byte  34,byte  34,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  16,byte  16,byte  32,byte  32,byte  31,byte  31,byte  31,byte  47,
+	byte  47,byte  30,byte  30,byte  30,byte  46,byte  46,byte  29,byte  12,
+	byte  16,byte  16,byte  32,byte  32,byte  31,byte  31,byte  31,byte  47,
+	byte  47,byte  30,byte  30,byte  30,byte  46,byte  46,byte  29,byte  12,
+	byte  45,byte  45,byte  44,byte  44,byte  44,byte  44,byte  31,byte  60,
+	byte  43,byte  43,byte  30,byte  59,byte  59,byte  42,byte  42,byte  12,
+	byte  45,byte  45,byte  44,byte  44,byte  44,byte  44,byte  27,byte  43,
+	byte  43,byte  43,byte  26,byte  26,byte  42,byte  42,byte  42,byte  12,
+	byte  28,byte  28,byte  44,byte  44,byte  27,byte  27,byte  27,byte  43,
+	byte  43,byte  26,byte  26,byte  26,byte  42,byte  42,byte  25,byte   8,
+	byte  28,byte  28,byte  44,byte  44,byte  27,byte  27,byte  27,byte  43,
+	byte  43,byte  26,byte  26,byte   9,byte  42,byte  42,byte  25,byte   8,
+	byte  28,byte  28,byte  28,byte  27,byte  27,byte  27,byte  10,byte  43,
+	byte  26,byte  26,byte  26,byte   9,byte  42,byte  42,byte  25,byte   8,
+	byte  41,byte  41,byte  57,byte  40,byte  40,byte  40,byte  40,byte  56,
+	byte  39,byte  39,byte  39,byte  55,byte  55,byte  38,byte  38,byte   8,
+	byte  41,byte  41,byte  40,byte  40,byte  40,byte  40,byte  23,byte  39,
+	byte  39,byte  39,byte  22,byte  55,byte  38,byte  38,byte  38,byte   4,
+	byte  24,byte  24,byte  40,byte  40,byte  23,byte  23,byte  23,byte  39,
+	byte  39,byte  22,byte  22,byte  22,byte  38,byte  38,byte  21,byte   4,
+	byte  24,byte  24,byte  24,byte  23,byte  23,byte  23,byte  23,byte  39,
+	byte  22,byte  22,byte  22,byte   5,byte  38,byte  38,byte  21,byte   4,
+	byte  24,byte  24,byte  53,byte  23,byte  23,byte   6,byte   6,byte  52,
+	byte  52,byte  22,byte   5,byte   5,byte  51,byte  21,byte  21,byte   4,
+	byte  37,byte  37,byte  53,byte  36,byte  36,byte  36,byte  36,byte  52,
+	byte  35,byte  35,byte  35,byte  51,byte  51,byte  34,byte  34,byte   0,
+	byte  37,byte  37,byte  36,byte  36,byte  36,byte  36,byte  36,byte  35,
+	byte  35,byte  35,byte  35,byte  18,byte  34,byte  34,byte  34,byte   0,
+	byte  20,byte  20,byte  36,byte  36,byte  19,byte  19,byte  19,byte  35,
+	byte  35,byte  18,byte  18,byte  18,byte  34,byte  34,byte  17,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  15,byte  15,byte  15,byte  15,byte  14,byte  14,byte  14,byte  14,
+	byte  13,byte  13,byte  13,byte  13,byte  12,byte  12,byte  12,byte  12,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte  11,byte  11,byte  11,byte  11,byte  10,byte  10,byte  10,byte  10,
+	byte   9,byte   9,byte   9,byte   9,byte   8,byte   8,byte   8,byte   8,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   7,byte   7,byte   7,byte   7,byte   6,byte   6,byte   6,byte   6,
+	byte   5,byte   5,byte   5,byte   5,byte   4,byte   4,byte   4,byte   4,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+	byte   3,byte   3,byte   3,byte   3,byte   2,byte   2,byte   2,byte   2,
+	byte   1,byte   1,byte   1,byte   1,byte   0,byte   0,byte   0,byte   0,
+};
--- /dev/null
+++ b/appl/wm/mpeg/decode.b
@@ -1,0 +1,831 @@
+implement Mpegd;
+
+include "sys.m";
+include "mpegio.m";
+
+sys: Sys;
+idct: IDCT;
+
+Mpegi, Picture, Slice, MacroBlock, YCbCr, Pair: import Mpegio;
+
+intra_tab := array[64] of {
+	8, 16, 19, 22, 26, 27, 29, 34,
+	16, 16, 22, 24, 27, 29, 34, 37,
+	19, 22, 26, 27, 29, 34, 34, 38,
+	22, 22, 26, 27, 29, 34, 37, 40,
+	22, 26, 27, 29, 32, 35, 40, 48,
+	26, 27, 29, 32, 35, 40, 48, 58,
+	26, 27, 29, 34, 38, 46, 56, 69,
+	27, 29, 35, 38, 46, 56, 69, 83,
+};
+
+nintra_tab := array[64] of { * => 16 };
+
+CLOFF: con 256;
+
+intraQ, nintraQ: array of int;
+rtmp: array of array of int;
+rflag := array[6] of int;
+rforw, dforw, rback, dback: int;
+rforw2, dforw2, rback2, dback2: int;
+ydb, ydf, cdb, cdf: int;
+vflags: int;
+past := array[3] of int;
+pinit := array[3] of { * => 128 * 8 };
+zeros := array[64] of { * => 0 };
+zeros1: array of int;
+clamp := array[CLOFF + 256 + CLOFF] of byte;
+width, height, w2, h2: int;
+mpi, mps, yadj, cadj, yskip: int;
+I, B0: ref YCbCr;
+Ps := array[2] of ref YCbCr;
+Rs := array[2] of ref YCbCr;
+P, B, R, M, N: ref YCbCr;
+pn: int = 0;
+rn: int = 0;
+
+zig := array[64] of {
+	0, 1, 8, 16, 9, 2, 3, 10, 17,
+	24, 32, 25, 18, 11, 4, 5,
+	12, 19, 26, 33, 40, 48, 41, 34,
+	27, 20, 13, 6, 7, 14, 21, 28, 
+	35, 42, 49, 56, 57, 50, 43, 36,
+	29, 22, 15, 23, 30, 37, 44, 51,
+	58, 59, 52, 45, 38, 31, 39, 46,
+	53, 60, 61, 54, 47, 55, 62, 63,
+};
+
+init(m: ref Mpegi)
+{
+	sys = load Sys Sys->PATH;
+	idct = load IDCT IDCT->PATH;
+	if (idct == nil) {
+		sys->print("could not open %s: %r\n", IDCT->PATH);
+		exit;
+	}
+	idct->init();
+	width = m.width;
+	height = m.height;
+	w2 = width >> 1;
+	h2 = height >> 1;
+	mps = width >> 4;
+	mpi = mps * height >> 4;
+	yskip = 8 * width;
+	yadj = 16 * width - (width - 16);
+	cadj = 8 * w2 - (w2 - 8);
+	I = frame();
+	Ps[0] = frame();
+	Ps[1] = frame();
+	Rs[0] = Ps[0];
+	Rs[1] = Ps[1];
+	B0 = frame();
+	for (i := 0; i < CLOFF; i++)
+		clamp[i] = byte 0;
+	for (i = 0; i < 256; i++)
+		clamp[i + CLOFF] = byte i;
+	for (i = CLOFF + 256; i < CLOFF + 256 + CLOFF; i++)
+		clamp[i] = byte 255;
+	if (m.intra == nil)
+		intraQ = intra_tab;
+	else
+		intraQ = zigof(m.intra);
+	if (m.nintra == nil)
+		nintraQ = nintra_tab;
+	else
+		nintraQ = zigof(m.nintra);
+	rtmp = array[6] of array of int;
+	for (i = 0; i < 6; i++)
+		rtmp[i] = array[64] of int;
+	zeros1 = zeros[1:];
+}
+
+zarray(n: int, v: byte): array of byte
+{
+	return array[n] of { * => v };
+}
+
+frame(): ref YCbCr
+{
+	y := zarray(width * height, byte 0);
+	b := zarray(w2 * h2, byte 128);
+	r := zarray(w2 * h2, byte 128);
+	return ref YCbCr(y, b, r);
+}
+
+zigof(a: array of int): array of int
+{
+	z := array[64] of int;
+	for (i := 0; i < 64; i++)
+		z[zig[i]] = a[i];
+	return z;
+}
+
+invQ_intra(a: array of Pair, q: int, b: array of int)
+{
+	(nil, t) := a[0];
+	b[0] = t * 8;
+	b[1:] = zeros1;
+	n := 1;
+	i := 1;
+	while (n < len a) {
+		(r, l) := a[n++];
+		i += r;
+		x := zig[i++];
+		if (l > 0) {
+			v := l * q * intraQ[x] >> 3;
+			if (v > 2047)
+				b[x] = 2047;
+			else
+				b[x] = (v - 1) | 1;
+		} else {
+			v := (l * q * intraQ[x] + 7) >> 3;
+			if (v < -2048)
+				b[x] = -2048;
+			else
+				b[x] = v | 1;
+		}
+		#sys->print("%d %d %d %d\n", x, r, l, b[x]);
+	}
+}
+
+invQ_nintra(a: array of Pair, q: int, b: array of int)
+{
+	b[0:] = zeros;
+	i := 0;
+	for (n := 0; n < len a; n++) {
+		(r, l) := a[n];
+		i += r;
+		if (l == 0) {
+			raisex("zero level");
+			i++;
+			continue;
+		}
+		x := zig[i++];
+		if (l > 0) {
+			v := ((l << 1) + 1) * q * nintraQ[x] >> 4;
+			if (v > 2047)
+				b[x] = 2047;
+			else
+				b[x] = (v - 1) | 1;
+		} else {
+			v := (((l << 1) - 1) * q * nintraQ[x] + 15) >> 4;
+			if (v < -2048)
+				b[x] = -2048;
+			else
+				b[x] = v | 1;
+		}
+		#sys->print("%d %d %d %d\n", x, r, l, b[x]);
+	}
+}
+
+yzero(v: array of byte, base: int)
+{
+	x := 0;
+	i := 8;
+	do {
+		n := base;
+		j := 8;
+		do
+			v[n++] = byte 0;
+		while (--j > 0);
+		base += width;
+	} while (--i > 0);
+}
+
+czero(v: array of byte, base: int)
+{
+	x := 0;
+	i := 8;
+	do {
+		n := base;
+		j := 8;
+		do
+			v[n++] = byte 128;
+		while (--j > 0);
+		base += w2;
+	} while (--i > 0);
+
+}
+
+blockzero(d: ref YCbCr)
+{
+	yzero(d.Y, ybase);
+	yzero(d.Y, ybase + 8);
+	yzero(d.Y, ybase + yskip);
+	yzero(d.Y, ybase + 8 + yskip);
+	czero(d.Cb, cbase);
+	czero(d.Cr, cbase);
+}
+
+ydistr(a: array of int, v: array of byte, base: int)
+{
+	x := 0;
+	i := 8;
+	do {
+		n := base;
+		j := 8;
+		do
+			v[n++] = clamp[a[x++] + CLOFF];
+		while (--j > 0);
+		base += width;
+	} while (--i > 0);
+}
+
+cdistr(a: array of int, v: array of byte, base: int)
+{
+	x := 0;
+	i := 8;
+	do {
+		n := base;
+		j := 8;
+		do
+			v[n++] = clamp[a[x++] + CLOFF];
+		while (--j > 0);
+		base += w2;
+	} while (--i > 0);
+
+}
+
+invQ_intra_block(b: array of array of Pair, q: int, pred: int, d: ref YCbCr)
+{
+	a, dc: array of int;
+	if (pred)
+		dc = past;
+	else
+		dc = pinit;
+	p := dc[0];
+	for (i := 0; i < 4; i++) {
+		a = rtmp[i];
+		#sys->print("%d\n", i);
+		invQ_intra(b[i], q, a);
+		p += a[0];
+		a[0] = p;
+		#sys->print("%d\n", a[0]);
+		idct->idct(a);
+	}
+	past[0] = p;
+	for (i = 4; i < 6; i++) {
+		p = dc[i - 3];
+		a = rtmp[i];
+		#sys->print("%d\n", i);
+		invQ_intra(b[i], q, a);
+		p += a[0];
+		a[0] = p;
+		#sys->print("%d\n", a[0]);
+		past[i - 3] = p;
+		idct->idct(a);
+	}
+	ydistr(rtmp[0], d.Y, ybase);
+	ydistr(rtmp[1], d.Y, ybase + 8);
+	ydistr(rtmp[2], d.Y, ybase + yskip);
+	ydistr(rtmp[3], d.Y, ybase + 8 + yskip);
+	cdistr(rtmp[4], d.Cb, cbase);
+	cdistr(rtmp[5], d.Cr, cbase);
+}
+
+invQ_nintra_block(b: array of array of Pair, q: int)
+{
+	for (i := 0; i < 6; i++) {
+		p := b[i];
+		if (p != nil) {
+			a := rtmp[i];
+			#sys->print("%d\n", i);
+			invQ_nintra(p, q, a);
+			idct->idct(a);
+			rflag[i] = 1;
+		} else
+			rflag[i] = 0;
+	}
+}
+
+mbr, ybase, cbase: int;
+
+nextmb()
+{
+	if (--mbr == 0) {
+		ybase += yadj;
+		cbase += cadj;
+		mbr = mps;
+	} else {
+		ybase += 16;
+		cbase += 8;
+	}
+}
+
+copyblock(s, d: array of byte, b, n, w: int)
+{
+	i := 8;
+	do {
+		d[b:] = s[b:b+n];
+		b += w;
+	} while (--i > 0);
+}
+
+copyblockdisp(s, d: array of byte, b, n, w, p: int)
+{
+	i := 8;
+	p += b;
+	do {
+		d[b:] = s[p:p+n];
+		b += w;
+		p += w;
+	} while (--i > 0);
+}
+
+interpblock(s0, s1, d: array of byte, b, n, w, p0, p1: int)
+{
+	i := 8;
+	do {
+		dx := b;
+		s0x := b + p0;
+		s1x := b + p1;
+		j := n;
+		do
+			d[dx++] = byte ((int s0[s0x++] + int s1[s1x++] + 1) >> 1);
+		while (--j > 0);
+		b += w;
+	} while (--i > 0);
+}
+
+deltablock(s: array of byte, r: array of int, d: array of byte, b, w, o: int)
+{
+	rx := 0;
+	i := 8;
+	do {
+		dx := b;
+		sx := b + o;
+		j := 8;
+		do
+			d[dx++] = clamp[CLOFF + int s[sx++] + r[rx++]];
+		while (--j > 0);
+		b += w;
+	} while (--i > 0);
+}
+
+deltainterpblock(s0, s1: array of byte, r: array of int, d: array of byte, b, w, o0, o1: int)
+{
+	rx := 0;
+	i := 8;
+	do {
+		dx := b;
+		s0x := b + o0;
+		s1x := b + o1;
+		j := 8;
+		do
+			d[dx++] = clamp[CLOFF + ((int s0[s0x++] + int s1[s1x++] + 1) >> 1) + r[rx++]];
+		while (--j > 0);
+		b += w;
+	} while (--i > 0);
+}
+
+dispblock(s, d: array of byte, n, b, w, o: int)
+{
+	if (rflag[n])
+		deltablock(s, rtmp[n], d, b, w, o);
+	else
+		copyblockdisp(s, d, b, 8, w, o);
+}
+
+genblock(s0, s1, d: array of byte, n, b, w, o0, o1: int)
+{
+	if (rflag[n])
+		deltainterpblock(s0, s1, rtmp[n], d, b, w, o0, o1);
+	else
+		interpblock(s0, s1, d, b, 8, w, o0, o1);
+}
+
+copymb()
+{
+	copyblock(R.Y, P.Y, ybase, 16, width);
+	copyblock(R.Y, P.Y, ybase + yskip, 16, width);
+	copyblock(R.Cb, P.Cb, cbase, 8, w2);
+	copyblock(R.Cr, P.Cr, cbase, 8, w2);
+}
+
+deltamb()
+{
+	dispblock(R.Y, P.Y, 0, ybase, width, 0);
+	dispblock(R.Y, P.Y, 1, ybase + 8, width, 0);
+	dispblock(R.Y, P.Y, 2, ybase + yskip, width, 0);
+	dispblock(R.Y, P.Y, 3, ybase + 8 + yskip, width, 0);
+	dispblock(R.Cb, P.Cb, 4, cbase, w2, 0);
+	dispblock(R.Cr, P.Cr, 5, cbase, w2, 0);
+}
+
+copymbforw()
+{
+	copyblockdisp(N.Y, B.Y, ybase, 16, width, ydf);
+	copyblockdisp(N.Y, B.Y, ybase + yskip, 16, width, ydf);
+	copyblockdisp(N.Cb, B.Cb, cbase, 8, w2, cdf);
+	copyblockdisp(N.Cr, B.Cr, cbase, 8, w2, cdf);
+}
+
+copymbback()
+{
+	copyblockdisp(M.Y, B.Y, ybase, 16, width, ydb);
+	copyblockdisp(M.Y, B.Y, ybase + yskip, 16, width, ydb);
+	copyblockdisp(M.Cb, B.Cb, cbase, 8, w2, cdb);
+	copyblockdisp(M.Cr, B.Cr, cbase, 8, w2, cdb);
+}
+
+copymbbackforw()
+{
+	interpblock(M.Y, N.Y, B.Y, ybase, 16, width, ydb, ydf);
+	interpblock(M.Y, N.Y, B.Y, ybase + yskip, 16, width, ydb, ydf);
+	interpblock(M.Cb, N.Cb, B.Cb, cbase, 8, w2, cdb, cdf);
+	interpblock(M.Cr, N.Cr, B.Cr, cbase, 8, w2, cdb, cdf);
+}
+
+deltambforw()
+{
+	dispblock(N.Y, B.Y, 0, ybase, width, ydf);
+	dispblock(N.Y, B.Y, 1, ybase + 8, width, ydf);
+	dispblock(N.Y, B.Y, 2, ybase + yskip, width, ydf);
+	dispblock(N.Y, B.Y, 3, ybase + 8 + yskip, width, ydf);
+	dispblock(N.Cb, B.Cb, 4, cbase, w2, cdf);
+	dispblock(N.Cr, B.Cr, 5, cbase, w2, cdf);
+}
+
+deltambback()
+{
+	dispblock(M.Y, B.Y, 0, ybase, width, ydb);
+	dispblock(M.Y, B.Y, 1, ybase + 8, width, ydb);
+	dispblock(M.Y, B.Y, 2, ybase + yskip, width, ydb);
+	dispblock(M.Y, B.Y, 3, ybase + 8 + yskip, width, ydb);
+	dispblock(M.Cb, B.Cb, 4, cbase, w2, cdb);
+	dispblock(M.Cr, B.Cr, 5, cbase, w2, cdb);
+}
+
+deltambbackforw()
+{
+	genblock(M.Y, N.Y, B.Y, 0, ybase, width, ydb, ydf);
+	genblock(M.Y, N.Y, B.Y, 1, ybase + 8, width, ydb, ydf);
+	genblock(M.Y, N.Y, B.Y, 2, ybase + yskip, width, ydb, ydf);
+	genblock(M.Y, N.Y, B.Y, 3, ybase + 8 + yskip, width, ydb, ydf);
+	genblock(M.Cb, N.Cb, B.Cb, 4, cbase, w2, cdb, cdf);
+	genblock(M.Cr, N.Cr, B.Cr, 5, cbase, w2, cdb, cdf);
+}
+
+deltambinterp()
+{
+	case vflags & (Mpegio->MB_MF | Mpegio->MB_MB) {
+	Mpegio->MB_MF =>
+		deltambforw();
+	Mpegio->MB_MB =>
+		deltambback();
+	Mpegio->MB_MF | Mpegio->MB_MB =>
+		deltambbackforw();
+	* =>
+		raisex("bad vflags");
+	}
+}
+
+interpmb()
+{
+	case vflags & (Mpegio->MB_MF | Mpegio->MB_MB) {
+	Mpegio->MB_MF =>
+		copymbforw();
+	Mpegio->MB_MB =>
+		copymbback();
+	Mpegio->MB_MF | Mpegio->MB_MB =>
+		copymbbackforw();
+	* =>
+		raisex("bad vflags");
+	}
+}
+
+Idecode(p: ref Picture): ref YCbCr
+{
+	sa := p.slices;
+	n := 0;
+	mbr = mps;
+	ybase = 0;
+	cbase = 0;
+	for (i := 0; i < len sa; i++) {
+		pred := 0;
+		ba := sa[i].blocks;
+		for (j := 0; j < len ba; j++) {
+			invQ_intra_block(ba[j].rls, ba[j].qscale, pred, I);
+			nextmb();
+			n++;
+			pred = 1;
+		}
+	}
+	if (n != mpi)
+		raisex("I mb count");
+	R = I;
+	Rs[rn] = I;
+	rn ^= 1;
+	return I;
+}
+
+Pdecode(p: ref Picture): ref YCbCr
+{
+	rforwp, dforwp: int;
+	md, c: int;
+	P = Ps[pn];
+	N = R;
+	B = P;
+	pn ^= 1;
+	fs := 1 << p.forwfc;
+	fsr := fs << 5;
+	fsmin := -(fs << 4);
+	fsmax := (fs << 4) - 1;
+	sa := p.slices;
+	n := 0;
+	mbr = mps;
+	ybase = 0;
+	cbase = 0;
+	for (i := 0; i < len sa; i++) {
+		pred := 0;
+		ipred := 0;
+		ba := sa[i].blocks;
+		for (j := 0; j < len ba; j++) {
+			mb := ba[j];
+			while (n < mb.addr) {
+				copymb();
+				ipred = 0;
+				pred = 0;
+				nextmb();
+				n++;
+			}
+			if (mb.flags & Mpegio->MB_I) {
+				invQ_intra_block(mb.rls, mb.qscale, ipred, P);
+				#blockzero(P);
+				ipred = 1;
+				pred = 0;
+			} else {
+				if (mb.flags & Mpegio->MB_MF) {
+					if (fs == 1 || mb.mhfc == 0)
+						md = mb.mhfc;
+					else if ((c = mb.mhfc) < 0)
+						md = (c + 1) * fs - mb.mhfr - 1;
+					else
+						md = (c - 1) * fs + mb.mhfr + 1;
+					if (pred)
+						md += rforwp;
+					if (md > fsmax)
+						rforw = md - fsr;
+					else if (md < fsmin)
+						rforw = md + fsr;
+					else
+						rforw = md;
+					rforwp = rforw;
+					if (fs == 1 || mb.mvfc == 0)
+						md = mb.mvfc;
+					else if ((c = mb.mvfc) < 0)
+						md = (c + 1) * fs - mb.mvfr - 1;
+					else
+						md = (c - 1) * fs + mb.mvfr + 1;
+					if (pred)
+						md += dforwp;
+					if (md > fsmax)
+						dforw = md - fsr;
+					else if (md < fsmin)
+						dforw = md + fsr;
+					else
+						dforw = md;
+					dforwp = dforw;
+					if (p.flags & Mpegio->FPFV) {
+						rforw2 = rforw;
+						dforw2 = dforw;
+						rforw <<= 1;
+						dforw <<= 1;
+						ydf = rforw2 + dforw2 * width;
+						cdf = (rforw2 >> 1) + (dforw2 >> 1) * w2;
+					} else {
+						if (rforw < 0)
+							rforw2 = (rforw + 1) >> 1;
+						else
+							rforw2 = rforw >> 1;
+						if (dforw < 0)
+							dforw2 = (dforw + 1) >> 1;
+						else
+							dforw2 = dforw >> 1;
+						ydf = (rforw >> 1) + (dforw >> 1) * width;
+						cdf = (rforw2 >> 1) + (dforw2 >> 1) * w2;
+					}
+					pred = 1;
+					if (mb.rls != nil) {
+						invQ_nintra_block(mb.rls, mb.qscale);
+						deltambforw();
+					} else
+						copymbforw();
+				} else {
+					if (mb.rls == nil)
+						raisex("empty delta");
+					invQ_nintra_block(mb.rls, mb.qscale);
+					deltamb();
+					pred = 0;
+				}
+				ipred = 0;
+			}
+			nextmb();
+			n++;
+		}
+	}
+	while (n < mpi) {
+		copymb();
+		nextmb();
+		n++;
+	}
+	R = P;
+	Rs[rn] = P;
+	rn ^= 1;
+	return P;
+}
+
+Bdecode(p: ref Mpegio->Picture): ref Mpegio->YCbCr
+{
+	return Bdecode2(p, Rs[rn ^ 1], Rs[rn]);
+}
+
+Bdecode2(p: ref Mpegio->Picture, f0, f1: ref Mpegio->YCbCr): ref Mpegio->YCbCr
+{
+	rforwp, dforwp, rbackp, dbackp: int;
+	md, c: int;
+	M = f0;
+	N = f1;
+	B = B0;
+	fs := 1 << p.forwfc;
+	fsr := fs << 5;
+	fsmin := -(fs << 4);
+	fsmax := (fs << 4) - 1;
+	bs := 1 << p.backfc;
+	bsr := bs << 5;
+	bsmin := -(bs << 4);
+	bsmax := (bs << 4) - 1;
+	sa := p.slices;
+	n := 0;
+	mbr = mps;
+	ybase = 0;
+	cbase = 0;
+	for (i := 0; i < len sa; i++) {
+		ipred := 0;
+		rback = 0;
+		rforw = 0;
+		dback = 0;
+		dforw = 0;
+		rbackp = 0;
+		rforwp = 0;
+		dbackp = 0;
+		dforwp = 0;
+		rback2 = 0;
+		rforw2 = 0;
+		dback2 = 0;
+		dforw2 = 0;
+		ydb = 0;
+		ydf = 0;
+		cdb = 0;
+		cdf = 0;
+		ba := sa[i].blocks;
+		for (j := 0; j < len ba; j++) {
+			mb := ba[j];
+			while (n < mb.addr) {
+				interpmb();
+				nextmb();
+				ipred = 0;
+				n++;
+			}
+			if (mb.flags & Mpegio->MB_I) {
+				invQ_intra_block(mb.rls, mb.qscale, ipred, B);
+				ipred = 1;
+				rback = 0;
+				rforw = 0;
+				dback = 0;
+				dforw = 0;
+				rbackp = 0;
+				rforwp = 0;
+				dbackp = 0;
+				dforwp = 0;
+				rback2 = 0;
+				rforw2 = 0;
+				dback2 = 0;
+				dforw2 = 0;
+				ydb = 0;
+				ydf = 0;
+				cdb = 0;
+				cdf = 0;
+			} else {
+				if (mb.flags & Mpegio->MB_MF) {
+					if (fs == 1 || mb.mhfc == 0)
+						md = mb.mhfc;
+					else if ((c = mb.mhfc) < 0)
+						md = (c + 1) * fs - mb.mhfr - 1;
+					else
+						md = (c - 1) * fs + mb.mhfr + 1;
+					md += rforwp;
+					if (md > fsmax)
+						rforw = md - fsr;
+					else if (md < fsmin)
+						rforw = md + fsr;
+					else
+						rforw = md;
+					rforwp = rforw;
+					if (fs == 1 || mb.mvfc == 0)
+						md = mb.mvfc;
+					else if ((c = mb.mvfc) < 0)
+						md = (c + 1) * fs - mb.mvfr - 1;
+					else
+						md = (c - 1) * fs + mb.mvfr + 1;
+					md += dforwp;
+					if (md > fsmax)
+						dforw = md - fsr;
+					else if (md < fsmin)
+						dforw = md + fsr;
+					else
+						dforw = md;
+					dforwp = dforw;
+					if (p.flags & Mpegio->FPFV) {
+						rforw2 = rforw;
+						dforw2 = dforw;
+						rforw <<= 1;
+						dforw <<= 1;
+						ydf = rforw2 + dforw2 * width;
+						cdf = (rforw2 >> 1) + (dforw2 >> 1) * w2;
+					} else {
+						if (rforw < 0)
+							rforw2 = (rforw + 1) >> 1;
+						else
+							rforw2 = rforw >> 1;
+						if (dforw < 0)
+							dforw2 = (dforw + 1) >> 1;
+						else
+							dforw2 = dforw >> 1;
+						ydf = (rforw >> 1) + (dforw >> 1) * width;
+						cdf = (rforw2 >> 1) + (dforw2 >> 1) * w2;
+					}
+				}
+				if (mb.flags & Mpegio->MB_MB) {
+					if (bs == 1 || mb.mhbc == 0)
+						md = mb.mhbc;
+					else if ((c = mb.mhbc) < 0)
+						md = (c + 1) * bs - mb.mhbr - 1;
+					else
+						md = (c - 1) * bs + mb.mhbr + 1;
+					md += rbackp;
+					if (md > bsmax)
+						rback = md - bsr;
+					else if (md < bsmin)
+						rback = md + bsr;
+					else
+						rback = md;
+					rbackp = rback;
+					if (bs == 1 || mb.mvbc == 0)
+						md = mb.mvbc;
+					else if ((c = mb.mvbc) < 0)
+						md = (c + 1) * bs - mb.mvbr - 1;
+					else
+						md = (c - 1) * bs + mb.mvbr + 1;
+					md += dbackp;
+					if (md > bsmax)
+						dback = md - bsr;
+					else if (md < bsmin)
+						dback = md + bsr;
+					else
+						dback = md;
+					dbackp = dback;
+					if (p.flags & Mpegio->FPBV) {
+						rback2 = rback;
+						dback2 = dback;
+						rback <<= 1;
+						dback <<= 1;
+						ydb = rback2 + dback2 * width;
+						cdb = (rback2 >> 1) + (dback2 >> 1) * w2;
+					} else {
+						if (rback < 0)
+							rback2 = (rback + 1) >> 1;
+						else
+							rback2 = rback >> 1;
+						if (dback < 0)
+							dback2 = (dback + 1) >> 1;
+						else
+							dback2 = dback >> 1;
+						ydb = (rback >> 1) + (dback >> 1) * width;
+						cdb = (rback2 >> 1) + (dback2 >> 1) * w2;
+					}
+				}
+				vflags = mb.flags;
+				if (mb.rls != nil) {
+					invQ_nintra_block(mb.rls, mb.qscale);
+					deltambinterp();
+				} else
+					interpmb();
+				ipred = 0;
+			}
+			nextmb();
+			n++;
+		}
+	}
+	while (n < mpi) {
+		interpmb();
+		nextmb();
+		n++;
+	}
+	return B;
+}
+
+raisex(nil: string)
+{
+	raise "decode error";
+}
--- /dev/null
+++ b/appl/wm/mpeg/decode4.b
@@ -1,0 +1,709 @@
+implement Mpegd;
+
+include "sys.m";
+include "mpegio.m";
+
+sys: Sys;
+idct: IDCT;
+
+Mpegi, Picture, Slice, MacroBlock, YCbCr, Pair: import Mpegio;
+
+intra_tab := array[64] of {
+	8, 16, 19, 22, 26, 27, 29, 34,
+	16, 16, 22, 24, 27, 29, 34, 37,
+	19, 22, 26, 27, 29, 34, 34, 38,
+	22, 22, 26, 27, 29, 34, 37, 40,
+	22, 26, 27, 29, 32, 35, 40, 48,
+	26, 27, 29, 32, 35, 40, 48, 58,
+	26, 27, 29, 34, 38, 46, 56, 69,
+	27, 29, 35, 38, 46, 56, 69, 83,
+};
+
+nintra_tab := array[64] of { * => 16 };
+
+CLOFF: con 256;
+
+intraQ, nintraQ: array of int;
+rtmp: array of array of int;
+rflag := array[6] of int;
+rforw, dforw, rback, dback: int;
+ydb, ydf: int;
+vflags: int;
+past := array[3] of int;
+pinit := array[3] of { * => 128 * 8 };
+zeros := array[64] of { * => 0 };
+zeros1: array of int;
+clamp := array[CLOFF + 256 + CLOFF] of byte;
+width, height, w2, h2: int;
+mpi, mps, yadj, yskip: int;
+I, B0: ref YCbCr;
+Ps := array[2] of ref YCbCr;
+Rs := array[2] of ref YCbCr;
+P, B, R, M, N: ref YCbCr;
+pn: int = 0;
+rn: int = 0;
+
+zig := array[64] of {
+	0, 1, 8, 16, 9, 2, 3, 10, 17,
+	24, 32, 25, 18, 11, 4, 5,
+	12, 19, 26, 33, 40, 48, 41, 34,
+	27, 20, 13, 6, 7, 14, 21, 28, 
+	35, 42, 49, 56, 57, 50, 43, 36,
+	29, 22, 15, 23, 30, 37, 44, 51,
+	58, 59, 52, 45, 38, 31, 39, 46,
+	53, 60, 61, 54, 47, 55, 62, 63,
+};
+
+init(m: ref Mpegi)
+{
+	sys = load Sys Sys->PATH;
+	idct = load IDCT IDCT->SPATH;
+	if (idct == nil) {
+		sys->print("could not open %s: %r\n", IDCT->PATH);
+		exit;
+	}
+	idct->init();
+	width = m.width;
+	height = m.height;
+	w2 = width >> 1;
+	h2 = height >> 1;
+	mps = width >> 4;
+	mpi = mps * height >> 4;
+	yskip = 8 * width;
+	yadj = 16 * width - (width - 16);
+	I = frame();
+	Ps[0] = frame();
+	Ps[1] = frame();
+	Rs[0] = Ps[0];
+	Rs[1] = Ps[1];
+	B0 = frame();
+	for (i := 0; i < CLOFF; i++)
+		clamp[i] = byte 0;
+	for (i = 0; i < 256; i++)
+		clamp[i + CLOFF] = byte i;
+	for (i = CLOFF + 256; i < CLOFF + 256 + CLOFF; i++)
+		clamp[i] = byte 255;
+	if (m.intra == nil)
+		intraQ = intra_tab;
+	else
+		intraQ = zigof(m.intra);
+	if (m.nintra == nil)
+		nintraQ = nintra_tab;
+	else
+		nintraQ = zigof(m.nintra);
+	rtmp = array[6] of array of int;
+	for (i = 0; i < 6; i++)
+		rtmp[i] = array[64] of int;
+	zeros1 = zeros[1:];
+}
+
+zarray(n: int, v: byte): array of byte
+{
+	return array[n] of { * => v };
+}
+
+frame(): ref YCbCr
+{
+	y := zarray(width * height, byte 0);
+	return ref YCbCr(y, nil, nil);
+}
+
+zigof(a: array of int): array of int
+{
+	z := array[64] of int;
+	for (i := 0; i < 64; i++)
+		z[zig[i]] = a[i];
+	return z;
+}
+
+invQ_intra(a: array of Pair, q: int, b: array of int)
+{
+	(nil, t) := a[0];
+	b[0] = t * 8;
+	b[1:] = zeros1;
+	n := 1;
+	i := 1;
+	while (n < len a) {
+		(r, l) := a[n++];
+		i += r;
+		x := zig[i++];
+		if (l > 0) {
+			v := l * q * intraQ[x] >> 3;
+			if (v > 2047)
+				b[x] = 2047;
+			else
+				b[x] = (v - 1) | 1;
+		} else {
+			v := (l * q * intraQ[x] + 7) >> 3;
+			if (v < -2048)
+				b[x] = -2048;
+			else
+				b[x] = v | 1;
+		}
+		#sys->print("%d %d %d %d\n", x, r, l, b[x]);
+	}
+}
+
+invQ_nintra(a: array of Pair, q: int, b: array of int)
+{
+	b[0:] = zeros;
+	i := 0;
+	for (n := 0; n < len a; n++) {
+		(r, l) := a[n];
+		i += r;
+		if (l == 0) {
+			raisex("zero level");
+			i++;
+			continue;
+		}
+		x := zig[i++];
+		if (l > 0) {
+			v := ((l << 1) + 1) * q * nintraQ[x] >> 4;
+			if (v > 2047)
+				b[x] = 2047;
+			else
+				b[x] = (v - 1) | 1;
+		} else {
+			v := (((l << 1) - 1) * q * nintraQ[x] + 15) >> 4;
+			if (v < -2048)
+				b[x] = -2048;
+			else
+				b[x] = v | 1;
+		}
+		#sys->print("%d %d %d %d\n", x, r, l, b[x]);
+	}
+}
+
+yzero(v: array of byte, base: int)
+{
+	x := 0;
+	i := 8;
+	do {
+		n := base;
+		j := 8;
+		do
+			v[n++] = byte 0;
+		while (--j > 0);
+		base += width;
+	} while (--i > 0);
+}
+
+blockzero(d: ref YCbCr)
+{
+	yzero(d.Y, ybase);
+	yzero(d.Y, ybase + 8);
+	yzero(d.Y, ybase + yskip);
+	yzero(d.Y, ybase + 8 + yskip);
+}
+
+ydistr(a: array of int, v: array of byte, base: int)
+{
+	x := 0;
+	i := 8;
+	do {
+		n := base;
+		j := 8;
+		do
+			v[n++] = clamp[a[x++] + CLOFF];
+		while (--j > 0);
+		base += width;
+	} while (--i > 0);
+}
+
+invQ_intra_block(b: array of array of Pair, q: int, pred: int, d: ref YCbCr)
+{
+	a, dc: array of int;
+	if (pred)
+		dc = past;
+	else
+		dc = pinit;
+	p := dc[0];
+	for (i := 0; i < 4; i++) {
+		a = rtmp[i];
+		#sys->print("%d\n", i);
+		invQ_intra(b[i], q, a);
+		p += a[0];
+		a[0] = p;
+		#sys->print("%d\n", a[0]);
+		idct->idct(a);
+	}
+	past[0] = p;
+	ydistr(rtmp[0], d.Y, ybase);
+	ydistr(rtmp[1], d.Y, ybase + 8);
+	ydistr(rtmp[2], d.Y, ybase + yskip);
+	ydistr(rtmp[3], d.Y, ybase + 8 + yskip);
+}
+
+invQ_nintra_block(b: array of array of Pair, q: int)
+{
+	for (i := 0; i < 4; i++) {
+		p := b[i];
+		if (p != nil) {
+			a := rtmp[i];
+			#sys->print("%d\n", i);
+			invQ_nintra(p, q, a);
+			idct->idct(a);
+			rflag[i] = 1;
+		} else
+			rflag[i] = 0;
+	}
+}
+
+mbr, ybase: int;
+
+nextmb()
+{
+	if (--mbr == 0) {
+		ybase += yadj;
+		mbr = mps;
+	} else
+		ybase += 16;
+}
+
+copyblock(s, d: array of byte, b, n, w: int)
+{
+	i := 8;
+	do {
+		d[b:] = s[b:b+n];
+		b += w;
+	} while (--i > 0);
+}
+
+copyblockdisp(s, d: array of byte, b, n, w, p: int)
+{
+	i := 8;
+	p += b;
+	do {
+		d[b:] = s[p:p+n];
+		b += w;
+		p += w;
+	} while (--i > 0);
+}
+
+interpblock(s0, s1, d: array of byte, b, n, w, p0, p1: int)
+{
+	i := 8;
+	do {
+		dx := b;
+		s0x := b + p0;
+		s1x := b + p1;
+		j := n;
+		do
+			d[dx++] = byte ((int s0[s0x++] + int s1[s1x++] + 1) >> 1);
+		while (--j > 0);
+		b += w;
+	} while (--i > 0);
+}
+
+deltablock(s: array of byte, r: array of int, d: array of byte, b, w, o: int)
+{
+	rx := 0;
+	i := 8;
+	do {
+		dx := b;
+		sx := b + o;
+		j := 8;
+		do
+			d[dx++] = clamp[CLOFF + int s[sx++] + r[rx++]];
+		while (--j > 0);
+		b += w;
+	} while (--i > 0);
+}
+
+deltainterpblock(s0, s1: array of byte, r: array of int, d: array of byte, b, w, o0, o1: int)
+{
+	rx := 0;
+	i := 8;
+	do {
+		dx := b;
+		s0x := b + o0;
+		s1x := b + o1;
+		j := 8;
+		do
+			d[dx++] = clamp[CLOFF + ((int s0[s0x++] + int s1[s1x++] + 1) >> 1) + r[rx++]];
+		while (--j > 0);
+		b += w;
+	} while (--i > 0);
+}
+
+dispblock(s, d: array of byte, n, b, w, o: int)
+{
+	if (rflag[n])
+		deltablock(s, rtmp[n], d, b, w, o);
+	else
+		copyblockdisp(s, d, b, 8, w, o);
+}
+
+genblock(s0, s1, d: array of byte, n, b, w, o0, o1: int)
+{
+	if (rflag[n])
+		deltainterpblock(s0, s1, rtmp[n], d, b, w, o0, o1);
+	else
+		interpblock(s0, s1, d, b, 8, w, o0, o1);
+}
+
+copymb()
+{
+	copyblock(R.Y, P.Y, ybase, 16, width);
+	copyblock(R.Y, P.Y, ybase + yskip, 16, width);
+}
+
+deltamb()
+{
+	dispblock(R.Y, P.Y, 0, ybase, width, 0);
+	dispblock(R.Y, P.Y, 1, ybase + 8, width, 0);
+	dispblock(R.Y, P.Y, 2, ybase + yskip, width, 0);
+	dispblock(R.Y, P.Y, 3, ybase + 8 + yskip, width, 0);
+}
+
+copymbforw()
+{
+	copyblockdisp(N.Y, B.Y, ybase, 16, width, ydf);
+	copyblockdisp(N.Y, B.Y, ybase + yskip, 16, width, ydf);
+}
+
+copymbback()
+{
+	copyblockdisp(M.Y, B.Y, ybase, 16, width, ydb);
+	copyblockdisp(M.Y, B.Y, ybase + yskip, 16, width, ydb);
+}
+
+copymbbackforw()
+{
+	interpblock(M.Y, N.Y, B.Y, ybase, 16, width, ydb, ydf);
+	interpblock(M.Y, N.Y, B.Y, ybase + yskip, 16, width, ydb, ydf);
+}
+
+deltambforw()
+{
+	dispblock(N.Y, B.Y, 0, ybase, width, ydf);
+	dispblock(N.Y, B.Y, 1, ybase + 8, width, ydf);
+	dispblock(N.Y, B.Y, 2, ybase + yskip, width, ydf);
+	dispblock(N.Y, B.Y, 3, ybase + 8 + yskip, width, ydf);
+}
+
+deltambback()
+{
+	dispblock(M.Y, B.Y, 0, ybase, width, ydb);
+	dispblock(M.Y, B.Y, 1, ybase + 8, width, ydb);
+	dispblock(M.Y, B.Y, 2, ybase + yskip, width, ydb);
+	dispblock(M.Y, B.Y, 3, ybase + 8 + yskip, width, ydb);
+}
+
+deltambbackforw()
+{
+	genblock(M.Y, N.Y, B.Y, 0, ybase, width, ydb, ydf);
+	genblock(M.Y, N.Y, B.Y, 1, ybase + 8, width, ydb, ydf);
+	genblock(M.Y, N.Y, B.Y, 2, ybase + yskip, width, ydb, ydf);
+	genblock(M.Y, N.Y, B.Y, 3, ybase + 8 + yskip, width, ydb, ydf);
+}
+
+deltambinterp()
+{
+	case vflags & (Mpegio->MB_MF | Mpegio->MB_MB) {
+	Mpegio->MB_MF =>
+		deltambforw();
+	Mpegio->MB_MB =>
+		deltambback();
+	Mpegio->MB_MF | Mpegio->MB_MB =>
+		deltambbackforw();
+	* =>
+		raisex("bad vflags");
+	}
+}
+
+interpmb()
+{
+	case vflags & (Mpegio->MB_MF | Mpegio->MB_MB) {
+	Mpegio->MB_MF =>
+		copymbforw();
+	Mpegio->MB_MB =>
+		copymbback();
+	Mpegio->MB_MF | Mpegio->MB_MB =>
+		copymbbackforw();
+	* =>
+		raisex("bad vflags");
+	}
+}
+
+Idecode(p: ref Picture): ref YCbCr
+{
+	sa := p.slices;
+	n := 0;
+	mbr = mps;
+	ybase = 0;
+	for (i := 0; i < len sa; i++) {
+		pred := 0;
+		ba := sa[i].blocks;
+		for (j := 0; j < len ba; j++) {
+			invQ_intra_block(ba[j].rls, ba[j].qscale, pred, I);
+			nextmb();
+			n++;
+			pred = 1;
+		}
+	}
+	if (n != mpi)
+		raisex("I mb count");
+	R = I;
+	Rs[rn] = I;
+	rn ^= 1;
+	return I;
+}
+
+Pdecode(p: ref Picture): ref YCbCr
+{
+	rforwp, dforwp: int;
+	md, c: int;
+	P = Ps[pn];
+	N = R;
+	B = P;
+	pn ^= 1;
+	fs := 1 << p.forwfc;
+	fsr := fs << 5;
+	fsmin := -(fs << 4);
+	fsmax := (fs << 4) - 1;
+	sa := p.slices;
+	n := 0;
+	mbr = mps;
+	ybase = 0;
+	for (i := 0; i < len sa; i++) {
+		pred := 0;
+		ipred := 0;
+		ba := sa[i].blocks;
+		for (j := 0; j < len ba; j++) {
+			mb := ba[j];
+			while (n < mb.addr) {
+				copymb();
+				ipred = 0;
+				pred = 0;
+				nextmb();
+				n++;
+			}
+			if (mb.flags & Mpegio->MB_I) {
+				invQ_intra_block(mb.rls, mb.qscale, ipred, P);
+				#blockzero(P);
+				ipred = 1;
+				pred = 0;
+			} else {
+				if (mb.flags & Mpegio->MB_MF) {
+					if (fs == 1 || mb.mhfc == 0)
+						md = mb.mhfc;
+					else if ((c = mb.mhfc) < 0)
+						md = (c + 1) * fs - mb.mhfr - 1;
+					else
+						md = (c - 1) * fs + mb.mhfr + 1;
+					if (pred)
+						md += rforwp;
+					if (md > fsmax)
+						rforw = md - fsr;
+					else if (md < fsmin)
+						rforw = md + fsr;
+					else
+						rforw = md;
+					rforwp = rforw;
+					if (fs == 1 || mb.mvfc == 0)
+						md = mb.mvfc;
+					else if ((c = mb.mvfc) < 0)
+						md = (c + 1) * fs - mb.mvfr - 1;
+					else
+						md = (c - 1) * fs + mb.mvfr + 1;
+					if (pred)
+						md += dforwp;
+					if (md > fsmax)
+						dforw = md - fsr;
+					else if (md < fsmin)
+						dforw = md + fsr;
+					else
+						dforw = md;
+					dforwp = dforw;
+					if (p.flags & Mpegio->FPFV) {
+						ydf = rforw + dforw * width;
+						rforw <<= 1;
+						dforw <<= 1;
+					} else
+						ydf = (rforw >> 1) + (dforw >> 1) * width;
+					pred = 1;
+					if (mb.rls != nil) {
+						invQ_nintra_block(mb.rls, mb.qscale);
+						deltambforw();
+					} else
+						copymbforw();
+				} else {
+					if (mb.rls == nil)
+						raisex("empty delta");
+					invQ_nintra_block(mb.rls, mb.qscale);
+					deltamb();
+					pred = 0;
+				}
+				ipred = 0;
+			}
+			nextmb();
+			n++;
+		}
+	}
+	while (n < mpi) {
+		copymb();
+		nextmb();
+		n++;
+	}
+	R = P;
+	Rs[rn] = P;
+	rn ^= 1;
+	return P;
+}
+
+Bdecode(p: ref Mpegio->Picture): ref Mpegio->YCbCr
+{
+	return Bdecode2(p, Rs[rn ^ 1], Rs[rn]);
+}
+
+Bdecode2(p: ref Mpegio->Picture, f0, f1: ref Mpegio->YCbCr): ref Mpegio->YCbCr
+{
+	rforwp, dforwp, rbackp, dbackp: int;
+	md, c: int;
+	M = f0;
+	N = f1;
+	B = B0;
+	fs := 1 << p.forwfc;
+	fsr := fs << 5;
+	fsmin := -(fs << 4);
+	fsmax := (fs << 4) - 1;
+	bs := 1 << p.backfc;
+	bsr := bs << 5;
+	bsmin := -(bs << 4);
+	bsmax := (bs << 4) - 1;
+	sa := p.slices;
+	n := 0;
+	mbr = mps;
+	ybase = 0;
+	for (i := 0; i < len sa; i++) {
+		ipred := 0;
+		rback = 0;
+		rforw = 0;
+		dback = 0;
+		dforw = 0;
+		rbackp = 0;
+		rforwp = 0;
+		dbackp = 0;
+		dforwp = 0;
+		ydb = 0;
+		ydf = 0;
+		ba := sa[i].blocks;
+		for (j := 0; j < len ba; j++) {
+			mb := ba[j];
+			while (n < mb.addr) {
+				interpmb();
+				nextmb();
+				ipred = 0;
+				n++;
+			}
+			if (mb.flags & Mpegio->MB_I) {
+				invQ_intra_block(mb.rls, mb.qscale, ipred, B);
+				ipred = 1;
+				rback = 0;
+				rforw = 0;
+				dback = 0;
+				dforw = 0;
+				rbackp = 0;
+				rforwp = 0;
+				dbackp = 0;
+				dforwp = 0;
+				ydb = 0;
+				ydf = 0;
+			} else {
+				if (mb.flags & Mpegio->MB_MF) {
+					if (fs == 1 || mb.mhfc == 0)
+						md = mb.mhfc;
+					else if ((c = mb.mhfc) < 0)
+						md = (c + 1) * fs - mb.mhfr - 1;
+					else
+						md = (c - 1) * fs + mb.mhfr + 1;
+					md += rforwp;
+					if (md > fsmax)
+						rforw = md - fsr;
+					else if (md < fsmin)
+						rforw = md + fsr;
+					else
+						rforw = md;
+					rforwp = rforw;
+					if (fs == 1 || mb.mvfc == 0)
+						md = mb.mvfc;
+					else if ((c = mb.mvfc) < 0)
+						md = (c + 1) * fs - mb.mvfr - 1;
+					else
+						md = (c - 1) * fs + mb.mvfr + 1;
+					md += dforwp;
+					if (md > fsmax)
+						dforw = md - fsr;
+					else if (md < fsmin)
+						dforw = md + fsr;
+					else
+						dforw = md;
+					dforwp = dforw;
+					if (p.flags & Mpegio->FPFV) {
+						ydf = rforw + dforw * width;
+						rforw <<= 1;
+						dforw <<= 1;
+					} else
+						ydf = (rforw >> 1) + (dforw >> 1) * width;
+				}
+				if (mb.flags & Mpegio->MB_MB) {
+					if (bs == 1 || mb.mhbc == 0)
+						md = mb.mhbc;
+					else if ((c = mb.mhbc) < 0)
+						md = (c + 1) * bs - mb.mhbr - 1;
+					else
+						md = (c - 1) * bs + mb.mhbr + 1;
+					md += rbackp;
+					if (md > bsmax)
+						rback = md - bsr;
+					else if (md < bsmin)
+						rback = md + bsr;
+					else
+						rback = md;
+					rbackp = rback;
+					if (bs == 1 || mb.mvbc == 0)
+						md = mb.mvbc;
+					else if ((c = mb.mvbc) < 0)
+						md = (c + 1) * bs - mb.mvbr - 1;
+					else
+						md = (c - 1) * bs + mb.mvbr + 1;
+					md += dbackp;
+					if (md > bsmax)
+						dback = md - bsr;
+					else if (md < bsmin)
+						dback = md + bsr;
+					else
+						dback = md;
+					dbackp = dback;
+					if (p.flags & Mpegio->FPBV) {
+						ydb = rback + dback * width;
+						rback <<= 1;
+						dback <<= 1;
+					} else
+						ydb = (rback >> 1) + (dback >> 1) * width;
+				}
+				vflags = mb.flags;
+				if (mb.rls != nil) {
+					invQ_nintra_block(mb.rls, mb.qscale);
+					deltambinterp();
+				} else
+					interpmb();
+				ipred = 0;
+			}
+			nextmb();
+			n++;
+		}
+	}
+	while (n < mpi) {
+		interpmb();
+		nextmb();
+		n++;
+	}
+	return B;
+}
+
+raisex(nil: string)
+{
+	raise "decode error";
+}
--- /dev/null
+++ b/appl/wm/mpeg/fixidct.b
@@ -1,0 +1,188 @@
+implement IDCT;
+
+include "sys.m";
+include "mpegio.m";
+
+init()
+{
+}
+
+# IDCT based on Arai, Agui, and Nakajima, using flow chart Figure 4.8
+# of Pennebaker & Mitchell, JPEG: Still Image Data Compression Standard.
+# Remember IDCT is reverse of flow of DCT.
+# Nasty truncated integer version (not compliant).
+
+B0: con 16;
+B1: con 16;
+M: con (1 << B0);
+N: con (1 << B1);
+
+a0: con 1.414;
+a1: con 0.707;
+a2: con 0.541;
+a3: con 0.707;
+a4: con 1.307;
+a5: con -0.383;
+
+A0: con int (a0 * real N);
+A1: con int (a1 * real M);
+A2: con int (a2 * real M);
+A3: con int (a3 * real M);
+A4: con int (a4 * real M);
+A5: con int (a5 * real M);
+
+# scaling factors from eqn 4-35 of P&M
+s1: con 1.0196;
+s2: con 1.0823;
+s3: con 1.2026;
+s4: con 1.4142;
+s5: con 1.8000;
+s6: con 2.6131;
+s7: con 5.1258;
+
+S1: con int (s1 * real N);
+S2: con int (s2 * real N);
+S3: con int (s3 * real N);
+S4: con int (s4 * real N);
+S5: con int (s5 * real N);
+S6: con int (s6 * real N);
+S7: con int (s7 * real N);
+
+# overall normalization of 1/16, folded into premultiplication on vertical pass
+S: con 4;
+scale: con 0.0625;
+
+idct(b: array of int)
+{
+	x, y: int;
+
+	r := array[8*8] of int;
+
+	# transform horizontally
+	for(y=0; y<8; y++){
+		eighty := y<<3;
+		# if all non-DC components are zero, just propagate the DC term
+		if(b[eighty+1]==0)
+		if(b[eighty+2]==0 && b[eighty+3]==0)
+		if(b[eighty+4]==0 && b[eighty+5]==0)
+		if(b[eighty+6]==0 && b[eighty+7]==0){
+			v := b[eighty]*A0;
+			r[eighty+0] = v;
+			r[eighty+1] = v;
+			r[eighty+2] = v;
+			r[eighty+3] = v;
+			r[eighty+4] = v;
+			r[eighty+5] = v;
+			r[eighty+6] = v;
+			r[eighty+7] = v;
+			continue;
+		}
+
+		# step 5
+		in1 := S1*b[eighty+1];
+		in3 := S3*b[eighty+3];
+		in5 := S5*b[eighty+5];
+		in7 := S7*b[eighty+7];
+		f2 := S2*b[eighty+2];
+		f3 := S6*b[eighty+6];
+		f5 := (in1+in7);
+		f7 := (in5+in3);
+
+		# step 4
+		g2 := f2-f3;
+		g4 := (in5-in3);
+		g6 := (in1-in7);
+		g7 := f5+f7;
+
+		# step 3.5
+		t := ((g4+g6)>>B0)*A5;
+
+		# step 3
+		f0 := A0*b[eighty+0];
+		f1 := S4*b[eighty+4];
+		f3 += f2;
+		f2 = A1*(g2>>B0);
+
+		# step 2
+		g0 := f0+f1;
+		g1 := f0-f1;
+		g3 := f2+f3;
+		g4 = t-A2*(g4>>B0);
+		g5 := A3*((f5-f7)>>B0);
+		g6 = A4*(g6>>B0)+t;
+
+		# step 1
+		f0 = g0+g3;
+		f1 = g1+f2;
+		f2 = g1-f2;
+		f3 = g0-g3;
+		f5 = g5-g4;
+		f6 := g5+g6;
+		f7 = g6+g7;
+
+		# step 6
+		r[eighty+0] = (f0+f7);
+		r[eighty+1] = (f1+f6);
+		r[eighty+2] = (f2+f5);
+		r[eighty+3] = (f3-g4);
+		r[eighty+4] = (f3+g4);
+		r[eighty+5] = (f2-f5);
+		r[eighty+6] = (f1-f6);
+		r[eighty+7] = (f0-f7);
+	}
+
+	# transform vertically
+	for(x=0; x<8; x++){
+		# step 5
+		in1 := S1*(r[x+8]>>(B1+S));
+		in3 := S3*(r[x+24]>>(B1+S));
+		in5 := S5*(r[x+40]>>(B1+S));
+		in7 := S7*(r[x+56]>>(B1+S));
+		f2 := S2*(r[x+16]>>(B1+S));
+		f3 := S6*(r[x+48]>>(B1+S));
+		f5 := (in1+in7);
+		f7 := (in5+in3);
+
+		# step 4
+		g2 := f2-f3;
+		g4 := (in5-in3);
+		g6 := (in1-in7);
+		g7 := f5+f7;
+
+		# step 3.5
+		t := ((g4+g6)>>B0)*A5;
+
+		# step 3
+		f0 := A0*(r[x]>>(B1+S));
+		f1 := S4*(r[x+32]>>(B1+S));
+		f3 += f2;
+		f2 = A1*(g2>>B0);
+
+		# step 2
+		g0 := f0+f1;
+		g1 := f0-f1;
+		g3 := f2+f3;
+		g4 = t-A2*(g4>>B0);
+		g5 := A3*((f5-f7)>>B0);
+		g6 = A4*(g6>>B0)+t;
+
+		# step 1
+		f0 = g0+g3;
+		f1 = g1+f2;
+		f2 = g1-f2;
+		f3 = g0-g3;
+		f5 = g5-g4;
+		f6 := g5+g6;
+		f7 = g6+g7;
+
+		# step 6
+		b[x] = (f0+f7)>>B1;
+		b[x+8] = (f1+f6)>>B1;
+		b[x+16] = (f2+f5)>>B1;
+		b[x+24] = (f3-g4)>>B1;
+		b[x+32] = (f3+g4)>>B1;
+		b[x+40] = (f2-f5)>>B1;
+		b[x+48] = (f1-f6)>>B1;
+		b[x+56] = (f0-f7)>>B1;
+	}
+}
--- /dev/null
+++ b/appl/wm/mpeg/fltidct.b
@@ -1,0 +1,177 @@
+implement IDCT;
+
+include "sys.m";
+include "mpegio.m";
+
+init()
+{
+}
+
+# IDCT based on Arai, Agui, and Nakajima, using flow chart Figure 4.8
+# of Pennebaker & Mitchell, JPEG: Still Image Data Compression Standard.
+# Remember IDCT is reverse of flow of DCT.
+# Based on rob's readjpeg.b
+
+a0: con 1.414;
+a1: con 0.707;
+a2: con 0.541;
+a3: con 0.707;
+a4: con 1.307;
+a5: con -0.383;
+
+# scaling factors from eqn 4-35 of P&M
+s1: con 1.0196;
+s2: con 1.0823;
+s3: con 1.2026;
+s4: con 1.4142;
+s5: con 1.8000;
+s6: con 2.6131;
+s7: con 5.1258;
+
+# overall normalization of 1/16, folded into premultiplication on vertical pass
+scale: con 0.0625;
+
+ridct(zin: array of real, zout: array of real)
+{
+	x, y: int;
+
+	r := array[8*8] of real;
+
+	# transform horizontally
+	for(y=0; y<8; y++){
+		eighty := y<<3;
+		# if all non-DC components are zero, just propagate the DC term
+		if(zin[eighty+1]==0.)
+		if(zin[eighty+2]==0. && zin[eighty+3]==0.)
+		if(zin[eighty+4]==0. && zin[eighty+5]==0.)
+		if(zin[eighty+6]==0. && zin[eighty+7]==0.){
+			v := zin[eighty]*a0;
+			r[eighty+0] = v;
+			r[eighty+1] = v;
+			r[eighty+2] = v;
+			r[eighty+3] = v;
+			r[eighty+4] = v;
+			r[eighty+5] = v;
+			r[eighty+6] = v;
+			r[eighty+7] = v;
+			continue;
+		}
+
+		# step 5
+		in1 := s1*zin[eighty+1];
+		in3 := s3*zin[eighty+3];
+		in5 := s5*zin[eighty+5];
+		in7 := s7*zin[eighty+7];
+		f2 := s2*zin[eighty+2];
+		f3 := s6*zin[eighty+6];
+		f5 := (in1+in7);
+		f7 := (in5+in3);
+
+		# step 4
+		g2 := f2-f3;
+		g4 := (in5-in3);
+		g6 := (in1-in7);
+		g7 := f5+f7;
+
+		# step 3.5
+		t := (g4+g6)*a5;
+
+		# step 3
+		f0 := a0*zin[eighty+0];
+		f1 := s4*zin[eighty+4];
+		f3 += f2;
+		f2 = a1*g2;
+
+		# step 2
+		g0 := f0+f1;
+		g1 := f0-f1;
+		g3 := f2+f3;
+		g4 = t-a2*g4;
+		g5 := a3*(f5-f7);
+		g6 = a4*g6+t;
+
+		# step 1
+		f0 = g0+g3;
+		f1 = g1+f2;
+		f2 = g1-f2;
+		f3 = g0-g3;
+		f5 = g5-g4;
+		f6 := g5+g6;
+		f7 = g6+g7;
+
+		# step 6
+		r[eighty+0] = (f0+f7);
+		r[eighty+1] = (f1+f6);
+		r[eighty+2] = (f2+f5);
+		r[eighty+3] = (f3-g4);
+		r[eighty+4] = (f3+g4);
+		r[eighty+5] = (f2-f5);
+		r[eighty+6] = (f1-f6);
+		r[eighty+7] = (f0-f7);
+	}
+
+	# transform vertically
+	for(x=0; x<8; x++){
+		# step 5
+		in1 := scale*s1*r[x+8];
+		in3 := scale*s3*r[x+24];
+		in5 := scale*s5*r[x+40];
+		in7 := scale*s7*r[x+56];
+		f2 := scale*s2*r[x+16];
+		f3 := scale*s6*r[x+48];
+		f5 := (in1+in7);
+		f7 := (in5+in3);
+
+		# step 4
+		g2 := f2-f3;
+		g4 := (in5-in3);
+		g6 := (in1-in7);
+		g7 := f5+f7;
+
+		# step 3.5
+		t := (g4+g6)*a5;
+
+		# step 3
+		f0 := scale*a0*r[x];
+		f1 := scale*s4*r[x+32];
+		f3 += f2;
+		f2 = a1*g2;
+
+		# step 2
+		g0 := f0+f1;
+		g1 := f0-f1;
+		g3 := f2+f3;
+		g4 = t-a2*g4;
+		g5 := a3*(f5-f7);
+		g6 = a4*g6+t;
+
+		# step 1
+		f0 = g0+g3;
+		f1 = g1+f2;
+		f2 = g1-f2;
+		f3 = g0-g3;
+		f5 = g5-g4;
+		f6 := g5+g6;
+		f7 = g6+g7;
+
+		# step 6
+		zout[x] = (f0+f7);
+		zout[x+8] = (f1+f6);
+		zout[x+16] = (f2+f5);
+		zout[x+24] = (f3-g4);
+		zout[x+32] = (f3+g4);
+		zout[x+40] = (f2-f5);
+		zout[x+48] = (f1-f6);
+		zout[x+56] = (f0-f7);
+	}
+}
+
+idct(b: array of int)
+{
+	tmp := array[64] of real;
+	for (i := 0; i < 64; i++)
+		tmp[i] = real b[i];
+	ridct(tmp, tmp);
+	for (i = 0; i < 64; i++)
+		b[i] = int tmp[i];
+}
--- /dev/null
+++ b/appl/wm/mpeg/mai.tab
@@ -1,0 +1,2053 @@
+# vlc mai
+mai_size: con 2048;
+mai_bits: con 11;
+mai_table:= array[] of {
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(11, 33),
+	(11, 32),
+	(11, 31),
+	(11, 30),
+	(11, 29),
+	(11, 28),
+	(11, 27),
+	(11, 26),
+	(11, 25),
+	(11, 24),
+	(11, 23),
+	(11, 22),
+	(10, 21),
+	(10, 21),
+	(10, 20),
+	(10, 20),
+	(10, 19),
+	(10, 19),
+	(10, 18),
+	(10, 18),
+	(10, 17),
+	(10, 17),
+	(10, 16),
+	(10, 16),
+	(8, 15),
+	(8, 15),
+	(8, 15),
+	(8, 15),
+	(8, 15),
+	(8, 15),
+	(8, 15),
+	(8, 15),
+	(8, 14),
+	(8, 14),
+	(8, 14),
+	(8, 14),
+	(8, 14),
+	(8, 14),
+	(8, 14),
+	(8, 14),
+	(8, 13),
+	(8, 13),
+	(8, 13),
+	(8, 13),
+	(8, 13),
+	(8, 13),
+	(8, 13),
+	(8, 13),
+	(8, 12),
+	(8, 12),
+	(8, 12),
+	(8, 12),
+	(8, 12),
+	(8, 12),
+	(8, 12),
+	(8, 12),
+	(8, 11),
+	(8, 11),
+	(8, 11),
+	(8, 11),
+	(8, 11),
+	(8, 11),
+	(8, 11),
+	(8, 11),
+	(8, 10),
+	(8, 10),
+	(8, 10),
+	(8, 10),
+	(8, 10),
+	(8, 10),
+	(8, 10),
+	(8, 10),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 9),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(7, 8),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 7),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(4, 4),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(3, 2),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+	(1, 1),
+};
--- /dev/null
+++ b/appl/wm/mpeg/mai.vlc
@@ -1,0 +1,35 @@
+# Macroblock Address Increment
+# vlc mai < mai.vlc > mai.tab
+1	1
+011	2
+010	3
+0011	4
+0010	5
+00011	6
+00010	7
+0000111	8
+0000110	9
+00001011	10
+00001010	11
+00001001	12
+00001000	13
+00000111	14
+00000110	15
+0000010111	16
+0000010110	17
+0000010101	18
+0000010100	19
+0000010011	20
+0000010010	21
+00000100011	22
+00000100010	23
+00000100001	24
+00000100000	25
+00000011111	26
+00000011110	27
+00000011101	28
+00000011100	29
+00000011011	30
+00000011010	31
+00000011001	32
+00000011000	33
--- /dev/null
+++ b/appl/wm/mpeg/makergbvmap.b
@@ -1,0 +1,31 @@
+implement MakeRGBVMap;
+
+include "sys.m";
+include "draw.m";
+
+draw: Draw;
+sys: Sys;
+
+Display: import draw;
+
+MakeRGBVMap: module
+{
+	init:	fn(ctxt: ref Draw->Context, nil: list of string);
+};
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	if (draw == nil) {
+		sys->print("could not load %s: %r\n", Draw->PATH);
+		exit;
+	}
+	d := ctxt.display;
+	sys->print("rgbvmap := array[3*256] of {\n");
+	for (i := 0; i < 256; i++) {
+		(r, g, b) := d.cmap2rgb(i);
+		sys->print("\tbyte\t%d,byte\t%d,byte\t%d,\n", r, g, b);
+	}
+	sys->print("};\n");
+}
--- /dev/null
+++ b/appl/wm/mpeg/maketables
@@ -1,0 +1,36 @@
+echo motion:
+vlc motion < motion.vlc > motion.tab
+echo rl0f:
+vlc -c rl0f < rl0f.vlc > rl0f.tab
+echo rl0n:
+vlc -c rl0n < rl0n.vlc > rl0n.tab
+echo c0:
+vlc -uUNDEF,UNDEF c0 < c0.vlc > c0.tab
+echo c1:
+vlc -cfp c1 < c1.vlc > c1.tab
+echo c2:
+vlc -cfp c2 < c2.vlc > c2.tab
+echo c3:
+vlc -cfp c3 < c3.vlc > c3.tab
+echo c4:
+vlc -cfp c4 < c4.vlc > c4.tab
+echo c5:
+vlc -cfp c5 < c5.vlc > c5.tab
+echo c6:
+vlc -cfp c6 < c6.vlc > c6.tab
+echo c7:
+vlc -cfp c7 < c7.vlc > c7.tab
+echo mai:
+vlc mai < mai.vlc > mai.tab
+echo mbi:
+vlc mbi < mbi.vlc > mbi.tab
+echo mbp:
+vlc mbp < mbp.vlc > mbp.tab
+echo mbb:
+vlc mbb < mbb.vlc > mbb.tab
+echo cbp:
+vlc cbp < cbp.vlc > cbp.tab
+echo cdc:
+vlc cdc < cdc.vlc > cdc.tab
+echo ydc:
+vlc ydc < ydc.vlc > ydc.tab
--- /dev/null
+++ b/appl/wm/mpeg/mbb.tab
@@ -1,0 +1,69 @@
+# vlc mbb
+mbb_size: con 64;
+mbb_bits: con 6;
+mbb_table:= array[] of {
+	(0, UNDEF),
+	(6, 10),
+	(6, 6),
+	(6, 4),
+	(5, 8),
+	(5, 8),
+	(5, 9),
+	(5, 9),
+	(4, 0),
+	(4, 0),
+	(4, 0),
+	(4, 0),
+	(4, 3),
+	(4, 3),
+	(4, 3),
+	(4, 3),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 5),
+	(3, 5),
+	(3, 5),
+	(3, 5),
+	(3, 5),
+	(3, 5),
+	(3, 5),
+	(3, 5),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+	(2, 7),
+};
--- /dev/null
+++ b/appl/wm/mpeg/mbb.vlc
@@ -1,0 +1,13 @@
+# Macroblock Type-B
+# vlc mbb < mbb.vlc > mbb.tab
+0010	0
+010	1
+10	2
+0011	3
+000011	4
+011	5
+000010	6
+11	7
+00010	8
+00011	9
+000001	10
--- /dev/null
+++ b/appl/wm/mpeg/mbi.tab
@@ -1,0 +1,9 @@
+# vlc mbi
+mbi_size: con 4;
+mbi_bits: con 2;
+mbi_table:= array[] of {
+	(0, UNDEF),
+	(2, 1),
+	(1, 0),
+	(1, 0),
+};
--- /dev/null
+++ b/appl/wm/mpeg/mbi.vlc
@@ -1,0 +1,4 @@
+# Macroblock Type-I
+# vlc mbi < mbi.vlc > mbi.tab
+1	0
+01	1
--- /dev/null
+++ b/appl/wm/mpeg/mbp.tab
@@ -1,0 +1,69 @@
+# vlc mbp
+mbp_size: con 64;
+mbp_bits: con 6;
+mbp_table:= array[] of {
+	(0, UNDEF),
+	(6, 6),
+	(5, 2),
+	(5, 2),
+	(5, 4),
+	(5, 4),
+	(5, 5),
+	(5, 5),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+	(1, 3),
+};
--- /dev/null
+++ b/appl/wm/mpeg/mbp.vlc
@@ -1,0 +1,9 @@
+# Macroblock Type-P
+# vlc mbp < mbp.vlc > mbp.tab
+001	0
+01	1
+00001	2
+1	3
+00010	4
+00011	5
+000001	6
--- /dev/null
+++ b/appl/wm/mpeg/mkfile
@@ -1,0 +1,47 @@
+<../../../mkconfig
+
+TARG=\
+	decode.dis\
+	decode4.dis\
+	fixidct.dis\
+	fltidct.dis\
+	makergbvmap.dis\
+	mpegio.dis\
+	refidct.dis\
+	remap.dis\
+	remap1.dis\
+	remap2.dis\
+	remap4.dis\
+	remap24.dis\
+	remap8.dis\
+	scidct.dis\
+	vlc.dis\
+
+MODULES=\
+	closest.m\
+	mpegio.m\
+	rgbvmap.m\
+
+SYSMODULES=\
+	bufio.m\
+	draw.m\
+	math.m\
+	sys.m\
+	tk.m\
+	wmlib.m\
+
+DISBIN=$ROOT/dis/mpeg
+
+<$ROOT/mkfiles/mkdis
+
+all:V:		mpeg.dis
+
+install:V:	$ROOT/dis/mpeg/mpeg.dis
+
+$ROOT/dis/mpeg/mpeg.dis:	mpeg.dis
+	rm -f $target && cp mpeg.dis $target
+
+mpeg.dis:	$MODULES $SYS_MODULE
+
+nuke:V:
+	rm -f $ROOT/dis/mpeg/mpeg.dis
--- /dev/null
+++ b/appl/wm/mpeg/motion.tab
@@ -1,0 +1,2053 @@
+# vlc motion
+motion_size: con 2048;
+motion_bits: con 11;
+motion_table:= array[] of {
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(0, UNDEF),
+	(11, 16),
+	(11, -16),
+	(11, 15),
+	(11, -15),
+	(11, 14),
+	(11, -14),
+	(11, 13),
+	(11, -13),
+	(11, 12),
+	(11, -12),
+	(11, 11),
+	(11, -11),
+	(10, 10),
+	(10, 10),
+	(10, -10),
+	(10, -10),
+	(10, 9),
+	(10, 9),
+	(10, -9),
+	(10, -9),
+	(10, 8),
+	(10, 8),
+	(10, -8),
+	(10, -8),
+	(8, 7),
+	(8, 7),
+	(8, 7),
+	(8, 7),
+	(8, 7),
+	(8, 7),
+	(8, 7),
+	(8, 7),
+	(8, -7),
+	(8, -7),
+	(8, -7),
+	(8, -7),
+	(8, -7),
+	(8, -7),
+	(8, -7),
+	(8, -7),
+	(8, 6),
+	(8, 6),
+	(8, 6),
+	(8, 6),
+	(8, 6),
+	(8, 6),
+	(8, 6),
+	(8, 6),
+	(8, -6),
+	(8, -6),
+	(8, -6),
+	(8, -6),
+	(8, -6),
+	(8, -6),
+	(8, -6),
+	(8, -6),
+	(8, 5),
+	(8, 5),
+	(8, 5),
+	(8, 5),
+	(8, 5),
+	(8, 5),
+	(8, 5),
+	(8, 5),
+	(8, -5),
+	(8, -5),
+	(8, -5),
+	(8, -5),
+	(8, -5),
+	(8, -5),
+	(8, -5),
+	(8, -5),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, 4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(7, -4),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, 3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(5, -3),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, 2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(4, -2),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, 1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(3, -1),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+	(1, 0),
+};
--- /dev/null
+++ b/appl/wm/mpeg/motion.vlc
@@ -1,0 +1,19 @@
+# Motion Codes
+# vlc motion < motion.vlc > motion.tab
+1	0
+01s	1
+001s	2
+0001s	3
+000011s	4
+0000101s	5
+0000100s	6
+0000011s	7
+000001011s	8
+000001010s	9
+000001001s	10
+0000010001s	11
+0000010000s	12
+0000001111s	13
+0000001110s	14
+0000001101s	15
+0000001100s	16
--- /dev/null
+++ b/appl/wm/mpeg/mpeg.b
@@ -1,0 +1,285 @@
+implement WmMpeg;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Display, Image: import draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+	ctxt: ref Draw->Context;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include "mpegio.m";
+
+include "arg.m";
+
+mio: Mpegio;
+decode: Mpegd;
+remap: Remap;
+Mpegi: import mio;
+
+WmMpeg: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Stopped, Playing, Stepping, Paused: con iota;
+state	:= Stopped;
+depth := -1;
+sdepth: int;
+cvt: ref Image;
+
+pixelrec: Draw->Rect;
+
+decoders := array[] of {
+1=>	Mpegd->PATH4,
+2=>	Mpegd->PATH4,
+4=>	Mpegd->PATH4,
+8 or 16 or 24 or 32 =>	Mpegd->PATH,
+};
+
+remappers := array[] of {
+1=>	Remap->PATH1,
+2=>	Remap->PATH2,
+4=>	Remap->PATH4,
+8 or 16 or 24 or 32 =>	Remap->PATH,
+};
+
+task_cfg := array[] of {
+	"canvas .c",
+	"frame .b",
+	"button .b.File -text File -command {send cmd file}",
+	"button .b.Stop -text Stop -command {send cmd stop}",
+	"button .b.Pause -text Pause -command {send cmd pause}",
+	"button .b.Step -text Step -command {send cmd step}",
+	"button .b.Play -text Play -command {send cmd play}",
+	"frame .f",
+	"label .f.file -text {File:}",
+	"label .f.name",
+	"pack .f.file .f.name -side left",
+	"pack .b.File .b.Stop .b.Pause .b.Step .b.Play -side left",
+	"pack .f -fill x",
+	"pack .b -anchor w",
+	"pack .c -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+init(xctxt: ref Draw->Context, argv: list of string)
+{
+	sys  = load Sys  Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk   = load Tk   Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	selectfile= load Selectfile Selectfile->PATH;
+
+	ctxt = xctxt;
+	tkclient->init();
+	dialog->init();
+	selectfile->init();
+
+	darg, tkarg: string;
+	arg := load Arg Arg->PATH;
+	arg->init(argv);
+	while((c := arg->opt()) != 0)
+		case c {
+		'x' =>
+			tkarg = arg->arg();
+		'd' =>
+			darg = arg->arg();
+		}
+	args := arg->argv();
+	arg = nil;
+	if(darg != nil)
+		depth = int darg;
+	sdepth = ctxt.display.image.depth;
+	if (depth < 0 || depth > sdepth)
+		depth = sdepth;
+	(t, menubut) := tkclient->toplevel(ctxt, tkarg, "MPEG Player", 0);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	for(i:=0; i<len task_cfg; i++)
+		tk->cmd(t, task_cfg[i]);
+
+	tk->cmd(t, "bind . <Configure> {send cmd resize}");
+	tk->cmd(t, "update");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	mio = load Mpegio Mpegio->PATH;
+	decode = load Mpegd decoders[depth];
+	remap = load Remap remappers[depth];
+	if(mio == nil || decode == nil || remap == nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Loading Interfaces",
+			"Failed to load the MPEG\ninterface: "+sys->sprint("%r"),
+			0, "Exit"::nil);
+		return;
+	}
+	mio->init();
+
+	fname := "";
+	ctl := chan of string;
+	state = Stopped;
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq =>
+		tkclient->wmctl(t, s);
+	s := <-menubut =>
+		if(s == "exit"){
+			state = Stopped;
+			return;
+		}
+		tkclient->wmctl(t, s);
+	press := <-cmd =>
+		case press {
+		"file" =>
+			state = Stopped;
+			patterns := list of {
+				"*.mpg (MPEG movie files)",
+				"* (All Files)"
+			};
+			fname = selectfile->filename(ctxt, t.image, "Locate MPEG files",
+				patterns, nil);
+			if(fname != nil) {
+				tk->cmd(t, ".f.name configure -text {"+fname+"}");
+				tk->cmd(t, "update");
+			}
+		"play" =>
+			if (state != Stopped) {
+				state = Playing;
+				continue;
+			}
+			if(fname != nil) {
+				state = Playing;
+				spawn play(t, fname);
+			}
+		"step" =>
+			if (state != Stopped) {
+				state = Stepping;
+				continue;
+			}
+			if(fname != nil) {
+				state = Stepping;
+				spawn play(t, fname);
+			}
+		"pause" =>
+			if(state == Playing)
+				state = Paused;
+		"stop" =>
+			state = Stopped;
+		}
+	}
+}
+
+play(t: ref Toplevel, file: string)
+{
+	sp := list of { "Stop Play" };
+
+	fd := sys->open(file, Sys->OREAD);
+	if(fd == nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Open MPEG file", sys->sprint("%r"), 0, sp);
+		return;
+	}
+	m := mio->prepare(fd, file);
+	m.streaminit(Mpegio->VIDEO_STR0);
+	p := m.getpicture(1);
+	decode->init(m);
+	remap->init(m);
+
+	canvr := canvsize(t);
+	o := Point(0, 0);
+	dx := canvr.dx();
+	if(dx > m.width)
+		o.x = (dx - m.width)/2;
+	dy := canvr.dy();
+	if(dy > m.height)
+		o.y = (dy - m.height)/2;
+	canvr.min = canvr.min.add(o);
+	canvr.max = canvr.min.add(Point(m.width, m.height));
+
+	if (depth != sdepth){
+		chans := Draw->CMAP8;
+		case depth {
+		0 =>	chans = Draw->GREY1;
+		1 =>	chans = Draw->GREY2;
+		2 =>	chans = Draw->GREY4;
+		3 =>	chans = Draw->CMAP8;
+		4 =>	chans = Draw->RGB16;
+		5 =>	chans = Draw->RGB24;	# ?
+		}
+		cvt = ctxt.display.newimage(Rect((0, 0), (m.width, m.height)), chans, 0, 0);
+	}
+
+	f, pf: ref Mpegio->YCbCr;
+	for(;;) {
+		if(state == Stopped)
+			break;
+		case p.ptype {
+		Mpegio->IPIC =>
+			f = decode->Idecode(p);
+		Mpegio->PPIC =>
+			f = decode->Pdecode(p);
+		Mpegio->BPIC =>
+			f = decode->Bdecode(p);
+		}
+		while(state == Paused)
+			sys->sleep(0);
+		if (p.ptype == Mpegio->BPIC) {
+			writepixels(t, canvr, remap->remap(f));
+			if(state == Stepping)
+				state = Paused;
+		} else {
+			if (pf != nil) {
+				writepixels(t, canvr, remap->remap(pf));
+				if(state == Stepping)
+					state = Paused;
+			}
+			pf = f;
+		}
+		if ((p = m.getpicture(1)) == nil) {
+			writepixels(t, canvr, remap->remap(pf));
+			break;
+		}
+	}
+	state = Stopped;
+}
+
+writepixels(t: ref Toplevel, r: Rect, b: array of byte)
+{
+	if (cvt != nil) {
+		cvt.writepixels(cvt.r, b);
+		t.image.draw(r, cvt, nil, (0, 0));
+	} else
+		t.image.writepixels(r, b);
+}
+
+canvsize(t: ref Toplevel): Rect
+{
+	r: Rect;
+
+	r.min.x = int tk->cmd(t, ".c cget -actx");
+	r.min.y = int tk->cmd(t, ".c cget -acty");
+	r.max.x = r.min.x + int tk->cmd(t, ".c cget -width");
+	r.max.y = r.min.y + int tk->cmd(t, ".c cget -height");
+
+	return r;
+}
--- /dev/null
+++ b/appl/wm/mpeg/mpegio.b
@@ -1,0 +1,870 @@
+implement Mpegio;
+
+#
+#	MPEG ISO 11172 IO module.
+#
+
+include "sys.m";
+include "mpegio.m";
+
+sys: Sys;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+}
+
+raisex(s: string)
+{
+	raise MEXCEPT + s;
+}
+
+prepare(fd: ref Sys->FD, name: string): ref Mpegi
+{
+	m := ref Mpegi;
+	m.fd = fd;
+	m.name = name;
+	m.seek = 0;
+	m.looked = 0;
+	m.index = 0;
+	m.size = 0;
+	m.buff = array[MBSZ] of byte;
+	return m;
+}
+
+Mpegi.startsys(m: self ref Mpegi)
+{
+	# 2.4.3.2
+	m.xnextsc(PACK_SC);
+	m.packhdr();
+	m.xnextsc(SYSHD_SC);
+	m.syssz = m.getw();
+	m.boundmr = m.get22("boundmr");
+	m.syspar = m.getw();
+	if ((m.syspar & 16r20) == 0 || m.getb() != 16rFF)
+		m.fmterr("syspar");
+	t := m.syssz - 6;
+	if (t <= 0 || (t % 3) != 0)
+		m.fmterr("syssz");
+	t /= 3;
+	m.nstream = t;
+	m.streams = array[t] of Stream;
+	for (i := 0; i < t; i++) {
+		v := m.getb();
+		if ((v & 16r80) == 0)
+			m.fmterr("streamid");
+		w := m.getb();
+		if ((w & 16rC0) != 16rC0)
+			m.fmterr("stream mark");
+		m.streams[i] = (byte v, byte ((w >> 5) & 1), ((w & 16r1F) << 8) | m.getb(), nil);
+	}
+}
+
+Mpegi.packetcp(m: self ref Mpegi): int
+{
+	while ((c := m.nextsc()) != STREAM_EC) {
+		case c {
+		PACK_SC =>
+			m.packhdr();
+		SYSHD_SC =>
+			m.syshdr();
+		* =>
+			if (c < STREAM_BASE)
+				m.fmterr(sys->sprint("stream code %x", c));
+			# 2.4.3.3
+			l := m.getw();
+			fd := m.getfd(c);
+			if (fd != nil) {
+				if (c != PRIVSTREAM2)
+					l -= m.stamps();
+				if (m.log != nil)
+					sys->fprint(m.log, "%x %d %d\n", c & 16rFF, m.tell(), l);
+				m.cpn(fd, l);
+			} else
+				m.skipn(l);
+			return 1;
+		}
+	}
+	return 0;
+}
+
+Mpegi.getfd(m: self ref Mpegi, c: int): ref Sys->FD
+{
+	id := byte c;
+	n := m.nstream;
+	for (i := 0; i < n; i++) {
+		if (m.streams[i].id == id)
+			return m.streams[i].fd;
+	}
+	return nil;
+}
+
+Mpegi.packhdr(m: self ref Mpegi)
+{
+	# 2.4.3.2
+	t := m.getb();
+	if ((t & 16rF1) != 16r21)
+		m.fmterr("pack tag");
+	m.packt0 = (t >> 1) & 7;
+	v := m.getb() << 22;
+	t = m.getb();
+	if ((t & 1) == 0)
+		m.fmterr("packt mark 1");
+	v |= ((t & ~1) << 15) | (m.getb() << 7);
+	t = m.getb();
+	if ((t & 1) == 0)
+		m.fmterr("packt mark 2");
+	m.packt1 = v | (t >> 1);
+	m.packmr = m.get22("packmr");
+}
+
+Mpegi.syshdr(m: self ref Mpegi)
+{
+	l := m.getw();
+	if (l != m.syssz)
+		m.fmterr("syshdr size mismatch");
+	m.skipn(l);
+}
+
+Mpegi.stamps(m: self ref Mpegi): int
+{
+	# 2.4.3.3
+	n := 1;
+	while ((c := m.getb()) == 16rFF)
+		n++;
+	if ((c >> 6) == 1) {
+		m.getb();
+		c = m.getb();
+		n += 2;
+	}
+	case c >> 4 {
+	2 =>
+		m.skipn(4);
+		n += 4;
+	3 =>
+		m.skipn(9);
+		n += 9;
+	* =>
+		if (c != 16rF)
+			m.fmterr("stamps");
+	}
+	return n;
+}
+
+Mpegi.streaminit(m: self ref Mpegi, c: int)
+{
+	m.inittables();
+	m.sid = c;
+	s := m.peeksc();
+	if (s == PACK_SC) {
+		m.startsys();
+		f := 0;
+		id := byte m.sid;
+		for (i := 0; i < m.nstream; i++) {
+			if (m.streams[i].id == id) {
+				f = 1;
+				break;
+			}
+		}
+		if (!f)
+			m.fmterr(sys->sprint("%x: stream not found", c));
+		m.sseek();
+	} else if (s == SEQUENCE_SC) {
+		m.sresid = -1;
+		m.slim = m.size;
+	} else
+		m.fmterr(sys->sprint("start code = %x", s));
+	m.sbits = 0;
+}
+
+Mpegi.sseek(m: self ref Mpegi)
+{
+	while ((c := m.nextsc()) != STREAM_EC) {
+		case c {
+		PACK_SC =>
+			m.packhdr();
+		SYSHD_SC =>
+			m.syshdr();
+		* =>
+			if (c < STREAM_BASE)
+				m.fmterr(sys->sprint("stream code %x", c));
+			# 2.4.3.3
+			l := m.getw();
+			if (c == m.sid) {
+				if (c != PRIVSTREAM2)
+					l -= m.stamps();
+				n := m.size - m.index;
+				if (l <= n) {
+					m.slim = m.index + l;
+					m.sresid = 0;
+				} else {
+					m.slim = m.size;
+					m.sresid = l - n;
+				}
+				return;
+			} else
+				m.skipn(l);
+		}
+	}
+	m.fmterr("end of stream");
+}
+
+Mpegi.getpicture(m: self ref Mpegi, detail: int): ref Picture
+{
+	g := 0;
+	for (;;) {
+		case c := m.snextsc() {
+		SEQUENCE_SC =>
+			m.seqhdr();
+		GROUP_SC =>
+			m.grphdr();
+			g = 1;
+		PICTURE_SC =>
+			p := m.picture(detail);
+			if (g)
+				p.flags |= GSTART;
+			return p;
+		SEQUENCE_EC =>
+			return nil;
+		* =>
+			m.fmterr(sys->sprint("start code %x", c));
+		}
+	}
+}
+
+Mpegi.seqhdr(m: self ref Mpegi)
+{
+	# 2.4.2.3
+	c := m.sgetb();
+	d := m.sgetb();
+	m.width = (c << 4) | (d >> 4);
+	m.height = ((d & 16rF) << 8) | m.sgetb();
+	c = m.sgetb();
+	m.aspect = c >> 4;
+	m.frames = c & 16rF;
+	m.rate = m.sgetn(18);
+	m.smarker();
+	m.vbv = m.sgetn(10);
+	m.flags = 0;
+	if (m.sgetn(1))
+		m.flags |= CONSTRAINED;
+	if (m.sgetn(1))
+		m.intra = m.getquant();
+	if (m.sgetn(1))
+		m.nintra = m.getquant();
+	if (m.speeksc() == EXTENSION_SC)
+		m.sseeksc();
+	if (m.speeksc() == USER_SC)
+		m.sseeksc();
+}
+
+Mpegi.grphdr(m: self ref Mpegi)
+{
+	# 2.4.2.4
+	v := m.sgetb() << 17;
+	v |= m.sgetb() << 9;
+	v |= m.sgetb() << 1;
+	c := m.sgetb();
+	m.smpte = v | (c >> 7);
+	if (c & (1 << 6))
+		m.flags |= CLOSED;
+	else
+		m.flags &= ~CLOSED;
+	if (c & (1 << 5))
+		m.flags |= BROKEN;
+	else
+		m.flags &= ~BROKEN;
+	if (m.speeksc() == EXTENSION_SC)
+		m.sseeksc();
+	if (m.speeksc() == USER_SC)
+		m.sseeksc();
+}
+
+Mpegi.getquant(m: self ref Mpegi): array of int
+{
+	a := array[64] of int;
+	for (i := 0; i < 64; i++)
+		a[i] = m.sgetn(8);
+	return a;
+}
+
+Mpegi.picture(m: self ref Mpegi, detail: int): ref Picture
+{
+	# 2.4.2.5
+	p := ref Picture;
+	p.temporal = m.sgetn(10);
+	p.ptype = m.sgetn(3);
+	p.vbvdelay = m.sgetn(16);
+	p.flags = 0;
+	if (p.ptype == PPIC || p.ptype == BPIC) {
+		if (m.sgetn(1))
+			p.flags |= FPFV;
+		p.forwfc = m.sgetn(3);
+		if (p.forwfc == 0)
+			m.fmterr("forwfc");
+		p.forwfc--;
+		if (p.ptype == BPIC) {
+			if (m.sgetn(1))
+				p.flags |= FPBV;
+			p.backfc = m.sgetn(3);
+			if (p.backfc == 0)
+				m.fmterr("backfc");
+			p.backfc--;
+		} else
+			p.backfc = 0;
+	} else {
+		p.forwfc = 0;
+		p.backfc = 0;
+	}
+	while (m.sgetn(1))
+		m.sgetn(8);
+	if (m.speeksc() == EXTENSION_SC)
+		m.sseeksc();
+	if (m.speeksc() == USER_SC)
+		m.sseeksc();
+	p.seek = m.tell() - 3;
+	if (m.sresid < 0)
+		p.eos = -1;
+	else
+		p.eos = m.seek - m.size + m.slim + m.sresid;
+	if (detail)
+		m.detail(p);
+	else
+		m.skipdetail();
+	return p;
+}
+
+Mpegi.detail(m: self ref Mpegi, p: ref Picture)
+{
+	l: list of ref Slice;
+	p.addr = -1;
+	while ((c := m.speeksc()) >= SLICE1_SC && c <= SLICEN_SC)
+		l = m.slice(p) :: l;
+	if (l == nil)
+		m.fmterr("slice sc");
+	n := len l;
+	a := array[n] of ref Slice;
+	while (--n >= 0) {
+		a[n] = hd l;
+		l = tl l;
+	}
+	p.slices = a;
+}
+
+Mpegi.skipdetail(m: self ref Mpegi)
+{
+	while ((c := m.speeksc()) >= SLICE1_SC && c <= SLICEN_SC) {
+		m.looked = 0;
+		m.sseeksc();
+	}
+}
+
+ESC, EOB, C0, C1, C2, C3, C4, C5, C6, C7:	con -(iota + 1);
+
+include	"mai.tab";
+include	"mbi.tab";
+include	"mbp.tab";
+include	"mbb.tab";
+include	"motion.tab";
+include	"cbp.tab";
+include	"cdc.tab";
+include	"ydc.tab";
+include	"rl0f.tab";
+include	"rl0n.tab";
+include	"c0.tab";
+include	"c1.tab";
+include	"c2.tab";
+include	"c3.tab";
+include	"c4.tab";
+include	"c5.tab";
+include	"c6.tab";
+include	"c7.tab";
+
+mbif := array[] of {
+	MB_I,
+	MB_I | MB_Q,
+};
+
+mbpf := array[] of {
+	MB_MF,
+	MB_P,
+	MB_P | MB_Q,
+	MB_P | MB_MF,
+	MB_P | MB_MF | MB_Q,
+	MB_I,
+	MB_I | MB_Q,
+};
+
+mbbf := array[] of {
+	MB_MF,
+	MB_MB,
+	MB_MB | MB_MF,
+	MB_P | MB_MF,
+	MB_P | MB_MF | MB_Q,
+	MB_P | MB_MB,
+	MB_P | MB_MB | MB_Q,
+	MB_P | MB_MB | MB_MF,
+	MB_P | MB_MB | MB_MF | MB_Q,
+	MB_I,
+	MB_I | MB_Q,
+};
+
+c_bits := array[] of {
+	c1_bits, 
+	c2_bits, 
+	c3_bits, 
+	c4_bits, 
+	c5_bits, 
+	c6_bits, 
+	c7_bits, 
+};
+
+c_tables: array of array of Pair;
+
+patcode := array[] of {
+	1<<5, 1<<4, 1<<3, 1<<2, 1<<1, 1<<0,
+};
+
+Mpegi.inittables()
+{
+	if (c_tables == nil) {
+		c_tables = array[] of {
+			c1_table, 
+			c2_table, 
+			c3_table, 
+			c4_table, 
+			c5_table, 
+			c6_table, 
+			c7_table, 
+		};
+	}
+}
+
+Mpegi.slice(m: self ref Mpegi, p: ref Picture): ref Slice
+{
+	m.snextsc();
+	s := ref Slice;
+	q := m.sgetn(5);
+	while (m.sgetn(1))
+		m.sgetn(8);
+	x := p.addr;
+	l: list of ref MacroBlock;
+	while (m.speekn(23) != 0) {
+		while (m.speekn(11) == 16rF)
+			m.sbits -= 11;
+		while (m.speekn(11) == 16r8) {
+			x += 33;
+			m.sbits -= 11;
+		}
+		i := m.svlc(mai_table, mai_bits, "mai");
+		x += i;
+		b := ref MacroBlock;
+		b.addr = x;
+		case p.ptype {
+		IPIC =>
+			b.flags = mbif[m.svlc(mbi_table, mbi_bits, "mbi")];
+		PPIC =>
+			b.flags = mbpf[m.svlc(mbp_table, mbp_bits, "mbp")];
+		BPIC =>
+			b.flags = mbbf[m.svlc(mbb_table, mbb_bits, "mbb")];
+		DPIC =>
+			if (!m.sgetn(1))
+				m.fmterr("mbd flags");
+			b.flags = MB_I;
+		* =>
+			m.fmterr("ptype");
+		}
+		if (b.flags & MB_Q)
+			q = m.sgetn(5);
+		b.qscale = q;
+		if (b.flags & MB_MF) {
+			i = m.svlc(motion_table, motion_bits, "mhfc");
+			b.mhfc = i;
+			if (i != 0 && p.forwfc != 0)
+				b.mhfr = m.sgetn(p.forwfc);
+			i = m.svlc(motion_table, motion_bits, "mvfc");
+			b.mvfc = i;
+			if (i != 0 && p.forwfc != 0)
+				b.mvfr = m.sgetn(p.forwfc);
+		}
+		if (b.flags & MB_MB) {
+			i = m.svlc(motion_table, motion_bits, "mhbc");
+			b.mhbc = i;
+			if (i != 0 && p.backfc != 0)
+				b.mhbr = m.sgetn(p.backfc);
+			i = m.svlc(motion_table, motion_bits, "mvbc");
+			b.mvbc = i;
+			if (i != 0 && p.backfc != 0)
+				b.mvbr = m.sgetn(p.backfc);
+		}
+		if (b.flags & MB_I)
+			i = 16r3F;
+		else if (b.flags & MB_P)
+			i = m.svlc(cbp_table, cbp_bits, "cbp");
+		else
+			i = 0;
+		b.pcode = i;
+		if (i != 0) {
+			b.rls = array[6] of array of Pair;
+			for (j := 0; j < 6; j++) {
+				if (i & patcode[j]) {
+					rl: list of Pair;
+					R, L: int;
+					if (b.flags & MB_I) {
+						if (j < 4)
+							L = m.svlc(ydc_table, ydc_bits, "ydc");
+						else
+							L = m.svlc(cdc_table, cdc_bits, "cdc");
+						if (L != 0)
+							L = m.sdiffn(L);
+						rl = (0, L) :: nil;
+					} else
+						rl = m.sdct(rl0f_table, "rl0f") :: nil;
+					if (p.ptype != DPIC) {
+						for (;;) {
+							(R, L) = m.sdct(rl0n_table, "rl0n");
+							if (R == EOB)
+								break;
+							rl = (R, L) :: rl;
+						}
+					}
+					mn := len rl;
+					ma := array[mn] of Pair;
+					while (--mn >= 0) {
+						ma[mn] = hd rl;
+						rl = tl rl;
+					}
+					b.rls[j] = ma;
+				}
+			}
+		}
+		l = b :: l;
+	}
+	p.addr = x;
+	if (l == nil)
+		m.fmterr("macroblock");
+	n := len l;
+	a := array[n] of ref MacroBlock;
+	while (--n >= 0) {
+		a[n] = hd l;
+		l = tl l;
+	}
+	s.blocks = a;
+	return s;
+}
+
+Mpegi.cpn(m: self ref Mpegi, fd: ref Sys->FD, n: int)
+{
+	for (;;) {
+		r := m.size - m.index;
+		if (r >= n) {
+			if (sys->write(fd, m.buff[m.index:], n) < 0)
+				raisex(X_WRITE);
+			m.index += n;
+			return;
+		}
+		if (sys->write(fd, m.buff[m.index:], r) < 0)
+			raisex(X_WRITE);
+		m.fill();
+		n -= r;
+	}
+}
+
+Mpegi.fill(m: self ref Mpegi)
+{
+	n := sys->read(m.fd, m.buff, MBSZ);
+	if (n < 0) {
+		m.error = sys->sprint("%r");
+		raisex(X_READ);
+	}
+	if (n == 0)
+		raisex(X_EOF);
+	m.seek += n;
+	m.index = 0;
+	m.size = n;
+}
+
+Mpegi.tell(m: self ref Mpegi): int
+{
+	return m.seek - m.size + m.index;
+}
+
+Mpegi.skipn(m: self ref Mpegi, n: int)
+{
+	for (;;) {
+		r := m.size - m.index;
+		if (r >= n) {
+			m.index += n;
+			return;
+		}
+		n -= r;
+		m.fill();
+	}
+}
+
+Mpegi.getb(m: self ref Mpegi): int
+{
+	if (m.index == m.size)
+		m.fill();
+	return int m.buff[m.index++];
+}
+
+Mpegi.getw(m: self ref Mpegi): int
+{
+	t := m.getb();
+	return (t << 8) | m.getb();
+}
+
+Mpegi.get22(m: self ref Mpegi, s: string): int
+{
+	u := m.getb();
+	if ((u & 16r80) == 0)
+		m.fmterr(s + " mark 0");
+	v := m.getb();
+	w := m.getb();
+	if ((w & 1) == 0)
+		m.fmterr(s + " mark 1");
+	return ((u & 16r7F)  << 15) | (v << 7) | (w >> 1);
+}
+
+Mpegi.getsc(m: self ref Mpegi): int
+{
+	if (m.getb() != 0 || m.getb() != 0)
+		m.fmterr("start code 0s");
+	while ((c := m.getb()) == 0)
+		;
+	if (c != 1)
+		m.fmterr("start code 1");
+	return 16r100 | m.getb();
+}
+
+Mpegi.nextsc(m: self ref Mpegi): int
+{
+	if (m.looked) {
+		m.looked = 0;
+		return m.value;
+	} else
+		return m.getsc();
+}
+
+Mpegi.peeksc(m: self ref Mpegi): int
+{
+	if (!m.looked) {
+		m.value = m.getsc();
+		m.looked = 1;
+	}
+	return m.value;
+}
+
+Mpegi.xnextsc(m: self ref Mpegi, x: int)
+{
+	c := m.nextsc();
+	if (c != x)
+		m.fmterr(sys->sprint("startcode %x, got %x", x, c));
+}
+
+Mpegi.sfill(m: self ref Mpegi)
+{
+	r := m.sresid;
+	if (r < 0) {
+		m.fill();
+		m.slim = m.size;
+	} else if (r > 0) {
+		m.fill();
+		if (r <= m.size) {
+			m.slim = r;
+			m.sresid = 0;
+		} else {
+			m.slim = m.size;
+			m.sresid = r - m.size;
+		}
+	} else
+		m.sseek();
+}
+
+bits := array[] of {
+	0,
+	16r1, 16r3, 16r7, 16rF,
+	16r1F, 16r3F, 16r7F, 16rFF,
+	16r1FF, 16r3FF, 16r7FF, 16rFFF,
+	16r1FFF, 16r3FFF, 16r7FFF, 16rFFFF,
+	16r1FFFF, 16r3FFFF, 16r7FFFF, 16rFFFFF,
+	16r1FFFFF, 16r3FFFFF, 16r7FFFFF, 16rFFFFFF,
+	16r1FFFFFF, 16r3FFFFFF, 16r7FFFFFF, 16rFFFFFFF,
+	16r1FFFFFFF, 16r3FFFFFFF, 16r7FFFFFFF, int 16rFFFFFFFF,
+};
+
+sign := array[] of {
+	0,
+	16r1, 16r2, 16r4, 16r8,
+	16r10, 16r20, 16r40, 16r80,
+};
+
+Mpegi.sgetn(m: self ref Mpegi, n: int): int
+{
+	b := m.sbits;
+	v := m.svalue;
+	if (b < n) {
+		do {
+			v = (v << 8) | m.sgetb();
+			b += 8;
+		} while (b < n);
+		m.svalue = v;
+	}
+	b -= n;
+	m.sbits = b;
+	return (v >> b) & bits[n];
+}
+
+Mpegi.sdiffn(m: self ref Mpegi, n: int): int
+{
+	i := m.sgetn(n);
+	if (i & sign[n])
+		return i;
+	else
+		return i - bits[n];
+}
+
+Mpegi.speekn(m: self ref Mpegi, n: int): int
+{
+	b := m.sbits;
+	v := m.svalue;
+	if (b < n) {
+		do {
+			v = (v << 8) | m.sgetb();
+			b += 8;
+		} while (b < n);
+		m.sbits = b;
+		m.svalue = v;
+	}
+	return (v >> (b - n)) & bits[n];
+}
+
+Mpegi.sgetb(m: self ref Mpegi): int
+{
+	if (m.index == m.slim)
+		m.sfill();
+	return int m.buff[m.index++];
+}
+
+Mpegi.smarker(m: self ref Mpegi)
+{
+	if (!m.sgetn(1))
+		m.fmterr("marker");
+}
+
+Mpegi.sgetsc(m: self ref Mpegi): int
+{
+	b := m.sbits;
+	if (b >= 8) {
+		if (b >= 16) {
+			if (b >= 24) {
+				case m.svalue & 16rFFFFFF {
+				0 =>
+					break;
+				1 =>
+					m.sbits = 0;
+					return 16r100 | m.sgetb();
+				* =>
+					m.fmterr("start code 0s - 3");
+				}
+			} else if ((m.svalue & 16rFFFF) != 0)
+				m.fmterr("start code 0s - 2");
+		} else if ((m.svalue & 16rFF) != 0 || m.sgetb() != 0)
+			m.fmterr("start code 0s - 1");
+	} else if (m.sgetb() != 0 || m.sgetb() != 0)
+		m.fmterr("start code 0s");
+	m.sbits = 0;
+	while ((c := m.sgetb()) == 0)
+		;
+	if (c != 1)
+		m.fmterr("start code 1");
+	return 16r100 | m.sgetb();
+}
+
+Mpegi.snextsc(m: self ref Mpegi): int
+{
+	if (m.looked) {
+		m.looked = 0;
+		return m.value;
+	} else
+		return m.sgetsc();
+}
+
+Mpegi.speeksc(m: self ref Mpegi): int
+{
+	if (!m.looked) {
+		m.value = m.sgetsc();
+		m.looked = 1;
+	}
+	return m.value;
+}
+
+Mpegi.sseeksc(m: self ref Mpegi)
+{
+	n := 0;
+	for (;;) {
+		case m.sgetb() {
+		0 =>
+			n++;
+		1 =>
+			if (n >= 2) {
+				m.value = 16r100 | m.sgetb();
+				m.looked = 1;
+				return;
+			}
+			n = 0;
+		* =>
+			n = 0;
+		}
+	}
+}
+
+Mpegi.svlc(m: self ref Mpegi, a: array of Pair, n: int, s: string): int
+{
+	(b, v) := a[m.speekn(n)];
+	if (v == UNDEF)
+		m.fmterr(s + " vlc");
+	m.sbits -= b;
+	return v;
+}
+
+Mpegi.sdct(m: self ref Mpegi, a: array of Triple, s: string): Pair
+{
+	(b, l, r) := a[m.speekn(rl0f_bits)];
+	m.sbits -= b;
+	if (r < 0) {
+		case r {
+		EOB =>
+			break;
+		ESC =>
+			r = m.sgetn(6);
+			l = m.sgetn(8);
+			if (l == 0) {
+				l = m.sgetn(8);
+				if (l < 128)
+					m.fmterr(s + " esc +7");
+			} else if (l == 128) {
+				l = m.sgetn(8) - 256;
+				if (l > -128)
+					m.fmterr(s + " esc -7");
+			} else
+				l = (l << 24) >> 24;
+		C0 =>
+			(b, l, r) = c0_table[m.speekn(c0_bits)];
+			if (r == UNDEF)
+				m.fmterr(s + " c0 vlc");
+			m.sbits -= b;
+		* =>
+			r = C1 - r;
+			(l, r) = c_tables[r][m.sgetn(c_bits[r])];
+		}
+	}
+	return (r, l);
+}
+
+Mpegi.fmterr(m: self ref Mpegi, s: string)
+{
+	m.error = s;
+	raisex(X_FORMAT);
+}
--- /dev/null
+++ b/appl/wm/mpeg/mpegio.m
@@ -1,0 +1,218 @@
+#
+#	MPEG ISO 11172 IO module.
+#
+Mpegio: module
+{
+	PATH:	con "/dis/mpeg/mpegio.dis";
+
+	MBSZ:	con Sys->ATOMICIO;
+
+	PICTURE_SC:	con 16r100;
+	SLICE1_SC:	con 16r101;
+	SLICEN_SC:	con 16r1AF;
+	USER_SC:		con 16r1B2;
+	SEQUENCE_SC:	con 16r1B3;
+	EXTENSION_SC:	con 16r1B5;
+	SEQUENCE_EC:	con 16r1B7;
+	GROUP_SC:	con 16r1B8;
+	STREAM_EC:	con 16r1B9;
+	PACK_SC:		con 16r1BA;
+	SYSHD_SC:	con 16r1BB;
+	STREAM_BASE:	con 16r1BC;
+	PRIVSTREAM2:	con 16r1BF;
+	AUDIO_STR0:	con 16r1C0;
+	VIDEO_STR0:	con 16r1E0;
+
+	MEXCEPT:		con "mpeg: ";
+	X_FORMAT:	con "fmt error";
+	X_READ:		con "read error";
+	X_WRITE:		con "write error";
+	X_EOF:		con "premature eof";
+
+	UNDEF:		con 100;
+
+	CONSTRAINED, CLOSED, BROKEN:	con 1 << iota;
+	FPFV, FPBV, GSTART:	con 1 << iota;
+
+	IPIC:		con 1;
+	PPIC:		con 2;
+	BPIC:		con 3;
+	DPIC:		con 4;
+
+	ptypes:	con "0IPBD";
+
+	MB_Q, MB_MF, MB_MB, MB_P, MB_I:	con 1 << iota;
+
+	Stream: adt
+	{
+		id:		byte;
+		scale:	byte;
+		bound:	int;
+		fd:		ref Sys->FD;
+	};
+
+	Picture: adt
+	{
+		seek:		int;
+		eos:		int;
+		temporal:	int;
+		ptype:	int;
+		vbvdelay:	int;
+		flags:		int;
+		forwfc:	int;
+		backfc:	int;
+		slices:	array of ref Slice;
+		addr:		int;
+	};
+
+	Slice: adt
+	{
+		blocks:	array of ref MacroBlock;
+	};
+
+	MacroBlock: adt
+	{
+		flags:		int;
+		qscale:	int;
+		mhfc, mhfr, mvfc, mvfr: int;
+		mhbc, mhbr, mvbc, mvbr: int;
+		pcode:	int;
+		rls:		array of array of Pair;
+		addr:		int;
+	};
+
+	YCbCr: adt
+	{
+		Y, Cb, Cr: array of byte;
+	};
+
+	Pair:		type (int, int);
+	Triple:	type (int, int, int);
+
+	Mpegi: adt
+	{
+		fd:		ref Sys->FD;
+		name:	string;
+		error:	string;
+		looked:	int;
+		value:	int;
+		# info
+		width:	int;
+		height:	int;
+		aspect:	int;
+		frames:	int;
+		rate:		int;
+		vbv:		int;
+		flags:		int;
+		intra:		array of int;
+		nintra:	array of int;
+		smpte:	int;
+		# real buffer
+		seek:		int;
+		index:	int;
+		size:		int;
+		buff:		array of byte;
+		# stream buffer
+		sid:		int;	# stream id
+		slim:		int;	# stream limit <= size
+		sresid:	int;	# stream residual (-1 entire file)
+		sbits:		int;	# bits remaining
+		svalue:	int;	# current value
+
+		packt0:	int;
+		packt1:	int;
+		packmr:	int;
+		syssz:	int;
+		boundmr:	int;
+		syspar:	int;
+		nstream:	int;
+		streams:	array of Stream;
+		log:		ref Sys->FD;
+
+		startsys:	fn(m: self ref Mpegi);
+		packhdr:	fn(m: self ref Mpegi);
+		syshdr:	fn(m: self ref Mpegi);
+		packetcp:	fn(m: self ref Mpegi): int;
+		getfd:	fn(m: self ref Mpegi, c: int): ref Sys->FD;
+		stamps:	fn(m: self ref Mpegi): int;
+
+		streaminit:	fn(m: self ref Mpegi, c: int);
+		inittables:	fn();
+		sseek:	fn(m: self ref Mpegi);
+		seqhdr:	fn(m: self ref Mpegi);
+		grphdr:	fn(m: self ref Mpegi);
+		getquant:	fn(m: self ref Mpegi): array of int;
+		getpicture:	fn(m: self ref Mpegi, detail: int): ref Picture;
+		picture:	fn(m: self ref Mpegi, detail: int): ref Picture;
+		detail:	fn(m: self ref Mpegi, p: ref Picture);
+		skipdetail:	fn(m: self ref Mpegi);
+		slice:		fn(m: self ref Mpegi, p: ref Picture): ref Slice;
+
+		cpn:		fn(m: self ref Mpegi, fd: ref Sys->FD, n: int);
+		fill:		fn(m: self ref Mpegi);
+		tell:		fn(m: self ref Mpegi): int;
+		skipn:	fn(m: self ref Mpegi, n: int);
+		getb:		fn(m: self ref Mpegi): int;
+		getw:		fn(m: self ref Mpegi): int;
+		get22:	fn(m: self ref Mpegi, s: string): int;
+		getsc:	fn(m: self ref Mpegi): int;
+		nextsc:	fn(m: self ref Mpegi): int;
+		peeksc:	fn(m: self ref Mpegi): int;
+		xnextsc:	fn(m: self ref Mpegi, code: int);
+
+		sfill:		fn(m: self ref Mpegi);
+		sgetb:	fn(m: self ref Mpegi): int;
+		sgetn:	fn(m: self ref Mpegi, n: int): int;
+		sdiffn:	fn(m: self ref Mpegi, n: int): int;
+		sdct:		fn(m: self ref Mpegi, a: array of Triple, s: string): Pair;
+		speekn:	fn(m: self ref Mpegi, n: int): int;
+		smarker:	fn(m: self ref Mpegi);
+		sgetsc:	fn(m: self ref Mpegi): int;
+		snextsc:	fn(m: self ref Mpegi): int;
+		speeksc:	fn(m: self ref Mpegi): int;
+		sseeksc:	fn(m: self ref Mpegi);
+		svlc:		fn(m: self ref Mpegi, a: array of Pair, n: int, s: string): int;
+
+		fmterr:	fn(m: self ref Mpegi, s: string);
+	};
+
+	init:		fn();
+	prepare:	fn(fd: ref Sys->FD, name: string): ref Mpegi;
+	raisex:		fn(s: string);
+};
+
+Mpegd: module
+{
+	PATH:	con "/dis/mpeg/decode.dis";
+	PATH4:	con "/dis/mpeg/decode4.dis";
+
+	init:		fn(m: ref Mpegio->Mpegi);
+	Idecode:	fn(p: ref Mpegio->Picture): ref Mpegio->YCbCr;
+	Pdecode:	fn(p: ref Mpegio->Picture): ref Mpegio->YCbCr;
+	Bdecode:	fn(p: ref Mpegio->Picture): ref Mpegio->YCbCr;
+	Bdecode2:	fn(p: ref Mpegio->Picture, f0, f1: ref Mpegio->YCbCr): ref Mpegio->YCbCr;
+};
+
+IDCT: module
+{
+	FPATH:	con "/dis/mpeg/fltidct.dis";		# based on rob's jpeg
+	RPATH:	con "/dis/mpeg/refidct.dis";	# reference (full idct)
+	SPATH:	con "/dis/mpeg/scidct.dis";	# scaled integer implementation
+	XPATH:	con "/dis/mpeg/fixidct.dis";	# nasty fixed point
+	PATH:	con SPATH;
+
+	init:		fn();
+	idct:		fn(block: array of int);
+};
+
+Remap: module
+{
+	PATH:	con "/dis/mpeg/remap.dis";
+	PATH1:	con "/dis/mpeg/remap1.dis";
+	PATH2:	con "/dis/mpeg/remap2.dis";
+	PATH4:	con "/dis/mpeg/remap4.dis";
+	PATH24:	con "/dis/mpeg/remap24.dis";
+
+	init:		fn(m: ref Mpegio->Mpegi);
+	remap:	fn(p: ref Mpegio->YCbCr): array of byte;
+};
--- /dev/null
+++ b/appl/wm/mpeg/refidct.b
@@ -1,0 +1,58 @@
+implement IDCT;
+
+include "sys.m";
+include "math.m";
+include "mpegio.m";
+
+sys: Sys;
+math: Math;
+
+#
+#	Reference IDCT.  Full expanded 2-d IDCT.
+#
+
+coeff: array of array of real;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	math = load Math Math->PATH;
+	if (math == nil) {
+		sys->fprint(sys->fildes(2), "could not load %s: %r\n", Math->PATH);
+		exit;
+	}
+	init_idct();
+}
+
+init_idct()
+{
+	coeff = array[8] of array of real;
+	for (f := 0; f < 8; f++) {
+		coeff[f] = array[8] of real;
+		s := 0.5;
+		if (f == 0)
+			s = math->sqrt(0.125);
+		a := real f * (Math->Pi / 8.0);
+		for (t := 0; t < 8; t++) 
+			coeff[f][t] = s * math->cos(a * (real t + 0.5));
+	}
+}
+
+idct(block: array of int)
+{
+	tmp := array[64] of real;
+	for (i := 0; i < 8; i++)
+		for (j := 0; j < 8; j++) {
+			p := 0.0;
+			for (k := 0; k < 8; k++)
+				p += coeff[k][j] * real block[8 * i + k];
+			tmp[8 * i + j] = p;
+		}
+	for (j = 0; j < 8; j++)
+		for (i = 0; i < 8; i++) {
+			p := 0.0;
+			for (k := 0; k < 8; k++)
+				p += coeff[k][i] * tmp[8 * k + j];
+			block[8 * i + j] = int p;
+		}
+}
--- /dev/null
+++ b/appl/wm/mpeg/remap.b
@@ -1,0 +1,128 @@
+implement Remap;
+
+include "sys.m";
+include "mpegio.m";
+
+Mpegi, YCbCr: import Mpegio;
+
+CLOFF: con 255;
+
+width, height, w2, h2: int;
+out: array of byte;
+ered, egrn, eblu: array of int;
+b0r1, b1, r0: array of int;
+clamp := array[CLOFF + 256 + CLOFF] of int;
+clamp16 := array[CLOFF + 256 + CLOFF] of int;
+
+init(m: ref Mpegi)
+{
+	width = m.width;
+	height = m.height;
+	w2 = width >> 1;
+	h2 = height >> 1;
+	out = array[width * height] of byte;
+	b0r1 = array[w2] of int;
+	b1 = array[w2] of int;
+	r0 = array[w2] of int;
+	ered = array[width + 1] of int;
+	egrn = array[width + 1] of int;
+	eblu = array[width + 1] of int;
+	for (i := 0; i < CLOFF; i++) {
+		clamp[i] = 0;
+		clamp16[i] = 0;
+	}
+	for (i = 0; i < 256; i++) {
+		clamp[i + CLOFF] = i;
+		clamp16[i + CLOFF] = i >> 4;
+	}
+	for (i = CLOFF + 256; i < CLOFF + 256 + CLOFF; i++) {
+		clamp[i] = 255;
+		clamp16[i] = 255 >> 4;
+	}
+}
+
+include "closest.m";
+include "rgbvmap.m";
+
+#	rgb(y, cb, cr: int): (int, int, int)
+#	{
+#		Y := real y;
+#		Cb := real (cb - 128);
+#		Cr := real (cr - 128);
+#		r := int (Y+1.402*Cr);
+#		g := int (Y-0.34414*Cb-0.71414*Cr);
+#		b := int (Y+1.772*Cb);
+#		return (r, g, b);
+#	}
+
+B: con 16;
+M: con (1 << B);
+B0: con int (-0.34414 * real M);
+B1: con int (1.772 * real M);
+R0: con int (1.402 * real M);
+R1: con int (-0.71414 * real M);
+
+remap(p: ref Mpegio->YCbCr): array of byte
+{
+	Y := p.Y;
+	Cb := p.Cb;
+	Cr := p.Cr;
+	for (e := 0; e <= width; e++)
+		ered[e] = 0;
+	egrn[0:] = ered[0:];
+	eblu[0:] = ered[0:];
+	m := 0;
+	n := 0;
+	for (i := 0; i < h2; i++) {
+		for (j := 0; j < w2; j++) {
+			cb := int Cb[m] - 128;
+			cr := int Cr[m] - 128;
+			b0r1[j] = B0 * cb + R1 * cr;
+			b1[j] = B1 * cb;
+			r0[j] = R0 * cr;
+			m++;
+		}
+		j = 2;
+		do {
+			ex := 0;
+			er := 0;
+			eg := 0;
+			eb := 0;
+			for (k := 0; k < w2; k++) {
+				l := 2;
+				do {
+					y := int Y[n] << B;
+					r := clamp[((y + r0[k]) >> B) + CLOFF] + ered[ex];
+					g := clamp[((y + b0r1[k]) >> B) + CLOFF] + egrn[ex];
+					b := clamp[((y + b1[k]) >> B) + CLOFF] + eblu[ex];
+					rc := clamp16[r + CLOFF];
+					gc := clamp16[g + CLOFF];
+					bc := clamp16[b + CLOFF];
+					col := int closest[bc + 16 * (gc + 16 * rc)];
+					out[n++] = byte col;
+
+					col *= 3;
+					r -= int rgbvmap[col + 0];
+					t := (3 * r) >> 4;
+					ered[ex] = t + er;
+					ered[ex + 1] += t;
+					er = r - 3 * t;
+
+					g -= int rgbvmap[col + 1];
+					t = (3 * g) >> 4;
+					egrn[ex] = t + eg;
+					egrn[ex + 1] += t;
+					eg = g - 3 * t;
+
+					b -= int rgbvmap[col + 2];
+					t = (3 * b) >> 4;
+					eblu[ex] = t + eb;
+					eblu[ex + 1] += t;
+					eb = b - 3 * t;
+					ex++;
+				} while (--l > 0);
+			}
+		} while (--j > 0);
+	}
+	return out;
+}
--- /dev/null
+++ b/appl/wm/mpeg/remap1.b
@@ -1,0 +1,116 @@
+implement Remap;
+
+include "sys.m";
+include "mpegio.m";
+
+Mpegi, YCbCr: import Mpegio;
+
+CLOFF: con 511;
+
+width, height, w8: int;
+out: array of byte;
+elum: array of int;
+clamp2 := array[CLOFF + 256 + CLOFF] of int;
+
+init(m: ref Mpegi)
+{
+	width = m.width;
+	height = m.height;
+	w8 = width >> 3;
+	out = array[w8 * height] of byte;
+	elum = array[width + 1] of int;
+	for (i := 0; i < CLOFF; i++)
+		clamp2[i] = 0;
+	for (i = 0; i < 256; i++)
+		clamp2[i + CLOFF] = i >> 7;
+	for (i = CLOFF + 256; i < CLOFF + 256 + CLOFF; i++)
+		clamp2[i] = 255 >> 7;
+}
+
+remap(p: ref Mpegio->YCbCr): array of byte
+{
+	Y := p.Y;
+	for (e := 0; e <= width; e++)
+		elum[e] = 0;
+	m := 0;
+	n := 0;
+	for (i := 0; i < height; i++) {
+		el := 0;
+		ex := 0;
+		for (k := 0; k < w8; k++) {
+			y := (256 - int Y[n++]) + elum[ex];
+			l := clamp2[y + CLOFF] << 7;
+			b := l;
+			y -= l;
+			t := (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp2[y + CLOFF];
+			b |= l << 6;
+			y -= l << 7;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp2[y + CLOFF];
+			b |= l << 5;
+			y -= l << 7;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp2[y + CLOFF];
+			b |= l << 4;
+			y -= l << 7;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp2[y + CLOFF];
+			b |= l << 3;
+			y -= l << 7;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp2[y + CLOFF];
+			b |= l << 2;
+			y -= l << 7;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp2[y + CLOFF];
+			b |= l << 1;
+			y -= l << 7;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp2[y + CLOFF];
+			out[m++] = byte (b | l);
+			y -= l << 7;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+		}
+	}
+	return out;
+}
--- /dev/null
+++ b/appl/wm/mpeg/remap2.b
@@ -1,0 +1,80 @@
+implement Remap;
+
+include "sys.m";
+include "mpegio.m";
+
+Mpegi, YCbCr: import Mpegio;
+
+CLOFF: con 255;
+
+width, height, w4: int;
+out: array of byte;
+elum: array of int;
+clamp4 := array[CLOFF + 256 + CLOFF] of int;
+
+init(m: ref Mpegi)
+{
+	width = m.width;
+	height = m.height;
+	w4 = width >> 2;
+	out = array[w4 * height] of byte;
+	elum = array[width + 1] of int;
+	for (i := 0; i < CLOFF; i++)
+		clamp4[i] = 0;
+	for (i = 0; i < 256; i++)
+		clamp4[i + CLOFF] = i >> 6;
+	for (i = CLOFF + 256; i < CLOFF + 256 + CLOFF; i++)
+		clamp4[i] = 255 >> 6;
+}
+
+remap(p: ref Mpegio->YCbCr): array of byte
+{
+	Y := p.Y;
+	for (e := 0; e <= width; e++)
+		elum[e] = 0;
+	m := 0;
+	n := 0;
+	for (i := 0; i < height; i++) {
+		el := 0;
+		ex := 0;
+		for (k := 0; k < w4; k++) {
+			y := (256 - int Y[n++]) + elum[ex];
+			l := clamp4[y + CLOFF] << 6;
+			b := l;
+			y -= l;
+			t := (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp4[y + CLOFF];
+			b |= l << 4;
+			y -= l << 6;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp4[y + CLOFF];
+			b |= l << 2;
+			y -= l << 6;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp4[y + CLOFF];
+			out[m++] = byte (b | l);
+			y -= l << 6;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+		}
+	}
+	return out;
+}
--- /dev/null
+++ b/appl/wm/mpeg/remap24.b
@@ -1,0 +1,82 @@
+implement Remap;
+
+include "sys.m";
+include "mpegio.m";
+
+Mpegi, YCbCr: import Mpegio;
+
+CLOFF: con 255;
+
+width, height, w2, h2: int;
+out: array of byte;
+b0r1, b1, r0: array of int;
+clamp := array[CLOFF + 256 + CLOFF] of byte;
+
+init(m: ref Mpegi)
+{
+	width = m.width;
+	height = m.height;
+	w2 = width >> 1;
+	h2 = height >> 1;
+	out = array[3 * width * height] of byte;
+	b0r1 = array[w2] of int;
+	b1 = array[w2] of int;
+	r0 = array[w2] of int;
+	for (i := 0; i < CLOFF; i++)
+		clamp[i] = byte 0;
+	for (i = 0; i < 256; i++)
+		clamp[i + CLOFF] = byte i;
+	for (i = CLOFF + 256; i < CLOFF + 256 + CLOFF; i++)
+		clamp[i] = byte 255;
+}
+
+#	rgb(y, cb, cr: int): (int, int, int)
+#	{
+#		Y := real y;
+#		Cb := real (cb - 128);
+#		Cr := real (cr - 128);
+#		r := int (Y+1.402*Cr);
+#		g := int (Y-0.34414*Cb-0.71414*Cr);
+#		b := int (Y+1.772*Cb);
+#		return (r, g, b);
+#	}
+
+B: con 16;
+M: con (1 << B);
+B0: con int (-0.34414 * real M);
+B1: con int (1.772 * real M);
+R0: con int (1.402 * real M);
+R1: con int (-0.71414 * real M);
+
+remap(p: ref Mpegio->YCbCr): array of byte
+{
+	Y := p.Y;
+	Cb := p.Cb;
+	Cr := p.Cr;
+	m := 0;
+	n := 0;
+	x := 0;
+	for (i := 0; i < h2; i++) {
+		for (j := 0; j < w2; j++) {
+			cb := int Cb[m] - 128;
+			cr := int Cr[m] - 128;
+			b0r1[j] = B0 * cb + R1 * cr;
+			b1[j] = B1 * cb;
+			r0[j] = R0 * cr;
+			m++;
+		}
+		j = 2;
+		do {
+			for (k := 0; k < w2; k++) {
+				l := 2;
+				do {
+					y := int Y[n++] << B;
+					out[x++] = clamp[((y + r0[k]) >> B) + CLOFF];
+					out[x++] = clamp[((y + b0r1[k]) >> B) + CLOFF];
+					out[x++] = clamp[((y + b1[k]) >> B) + CLOFF];
+				} while (--l > 0);
+			}
+		} while (--j > 0);
+	}
+	return out;
+}
--- /dev/null
+++ b/appl/wm/mpeg/remap4.b
@@ -1,0 +1,62 @@
+implement Remap;
+
+include "sys.m";
+include "mpegio.m";
+
+Mpegi, YCbCr: import Mpegio;
+
+CLOFF: con 255;
+
+width, height, w2: int;
+out: array of byte;
+elum: array of int;
+clamp16 := array[CLOFF + 256 + CLOFF] of int;
+
+init(m: ref Mpegi)
+{
+	width = m.width;
+	height = m.height;
+	w2 = width >> 1;
+	out = array[w2 * height] of byte;
+	elum = array[width + 1] of int;
+	for (i := 0; i < CLOFF; i++)
+		clamp16[i] = 0;
+	for (i = 0; i < 256; i++)
+		clamp16[i + CLOFF] = i >> 4;
+	for (i = CLOFF + 256; i < CLOFF + 256 + CLOFF; i++)
+		clamp16[i] = 255 >> 4;
+}
+
+remap(p: ref Mpegio->YCbCr): array of byte
+{
+	Y := p.Y;
+	for (e := 0; e <= width; e++)
+		elum[e] = 0;
+	m := 0;
+	n := 0;
+	for (i := 0; i < height; i++) {
+		el := 0;
+		ex := 0;
+		for (k := 0; k < w2; k++) {
+			y := (256 - int Y[n++]) + elum[ex];
+			l := clamp16[y + CLOFF] << 4;
+			b := l;
+			y -= l;
+			t := (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+			y = (256 - int Y[n++]) + elum[ex];
+			l = clamp16[y + CLOFF];
+			out[m++] = byte (b | l);
+			y -= l << 4;
+			t = (3 * y) >> 4;
+			elum[ex] = t + el;
+			elum[ex + 1] += t;
+			el = y - 3 * t;
+			ex++;
+		}
+	}
+	return out;
+}
--- /dev/null
+++ b/appl/wm/mpeg/remap8.b
@@ -1,0 +1,84 @@
+implement Remap;
+
+include "sys.m";
+include "mpegio.m";
+
+Mpegi, YCbCr: import Mpegio;
+
+CLOFF: con 255;
+
+width, height, w2, h2: int;
+out: array of byte;
+b0r1, b1, r0: array of int;
+clamp16 := array[CLOFF + 256 + CLOFF] of int;
+
+init(m: ref Mpegi)
+{
+	width = m.width;
+	height = m.height;
+	w2 = width >> 1;
+	h2 = height >> 1;
+	out = array[width * height] of byte;
+	b0r1 = array[w2] of int;
+	b1 = array[w2] of int;
+	r0 = array[w2] of int;
+	for (i := 0; i < CLOFF; i++)
+		clamp16[i] = 0;
+	for (i = 0; i < 256; i++)
+		clamp16[i + CLOFF] = i >> 4;
+	for (i = CLOFF + 256; i < CLOFF + 256 + CLOFF; i++) 
+		clamp16[i] = 255 >> 4;
+}
+
+include "closest.m";
+
+#	rgb(y, cb, cr: int): (int, int, int)
+#	{
+#		Y := real y;
+#		Cb := real (cb - 128);
+#		Cr := real (cr - 128);
+#		r := int (Y+1.402*Cr);
+#		g := int (Y-0.34414*Cb-0.71414*Cr);
+#		b := int (Y+1.772*Cb);
+#		return (r, g, b);
+#	}
+
+B: con 16;
+M: con (1 << B);
+B0: con int (-0.34414 * real M);
+B1: con int (1.772 * real M);
+R0: con int (1.402 * real M);
+R1: con int (-0.71414 * real M);
+
+remap(p: ref Mpegio->YCbCr): array of byte
+{
+	Y := p.Y;
+	Cb := p.Cb;
+	Cr := p.Cr;
+	m := 0;
+	n := 0;
+	for (i := 0; i < h2; i++) {
+		for (j := 0; j < w2; j++) {
+			cb := int Cb[m] - 128;
+			cr := int Cr[m] - 128;
+			b0r1[j] = B0 * cb + R1 * cr;
+			b1[j] = B1 * cb;
+			r0[j] = R0 * cr;
+			m++;
+		}
+		j = 2;
+		do {
+			for (k := 0; k < w2; k++) {
+				l := 2;
+				do {
+					y := int Y[n] << B;
+					rc := clamp16[((y + r0[k]) >> B) + CLOFF];
+					gc := clamp16[((y + b0r1[k]) >> B) + CLOFF];
+					bc := clamp16[((y + b1[k]) >> B) + CLOFF];
+					out[n++] = closest[bc + 16 * (gc + 16 * rc)];
+				} while (--l > 0);
+			}
+		} while (--j > 0);
+	}
+	return out;
+}
--- /dev/null
+++ b/appl/wm/mpeg/rgbvmap.m
@@ -1,0 +1,258 @@
+rgbvmap := array[3*256] of {
+	byte	255,byte	255,byte	255,
+	byte	255,byte	255,byte	170,
+	byte	255,byte	255,byte	85,
+	byte	255,byte	255,byte	0,
+	byte	255,byte	170,byte	255,
+	byte	255,byte	170,byte	170,
+	byte	255,byte	170,byte	85,
+	byte	255,byte	170,byte	0,
+	byte	255,byte	85,byte	255,
+	byte	255,byte	85,byte	170,
+	byte	255,byte	85,byte	85,
+	byte	255,byte	85,byte	0,
+	byte	255,byte	0,byte	255,
+	byte	255,byte	0,byte	170,
+	byte	255,byte	0,byte	85,
+	byte	255,byte	0,byte	0,
+	byte	238,byte	0,byte	0,
+	byte	238,byte	238,byte	238,
+	byte	238,byte	238,byte	158,
+	byte	238,byte	238,byte	79,
+	byte	238,byte	238,byte	0,
+	byte	238,byte	158,byte	238,
+	byte	238,byte	158,byte	158,
+	byte	238,byte	158,byte	79,
+	byte	238,byte	158,byte	0,
+	byte	238,byte	79,byte	238,
+	byte	238,byte	79,byte	158,
+	byte	238,byte	79,byte	79,
+	byte	238,byte	79,byte	0,
+	byte	238,byte	0,byte	238,
+	byte	238,byte	0,byte	158,
+	byte	238,byte	0,byte	79,
+	byte	221,byte	0,byte	73,
+	byte	221,byte	0,byte	0,
+	byte	221,byte	221,byte	221,
+	byte	221,byte	221,byte	147,
+	byte	221,byte	221,byte	73,
+	byte	221,byte	221,byte	0,
+	byte	221,byte	147,byte	221,
+	byte	221,byte	147,byte	147,
+	byte	221,byte	147,byte	73,
+	byte	221,byte	147,byte	0,
+	byte	221,byte	73,byte	221,
+	byte	221,byte	73,byte	147,
+	byte	221,byte	73,byte	73,
+	byte	221,byte	73,byte	0,
+	byte	221,byte	0,byte	221,
+	byte	221,byte	0,byte	147,
+	byte	204,byte	0,byte	136,
+	byte	204,byte	0,byte	68,
+	byte	204,byte	0,byte	0,
+	byte	204,byte	204,byte	204,
+	byte	204,byte	204,byte	136,
+	byte	204,byte	204,byte	68,
+	byte	204,byte	204,byte	0,
+	byte	204,byte	136,byte	204,
+	byte	204,byte	136,byte	136,
+	byte	204,byte	136,byte	68,
+	byte	204,byte	136,byte	0,
+	byte	204,byte	68,byte	204,
+	byte	204,byte	68,byte	136,
+	byte	204,byte	68,byte	68,
+	byte	204,byte	68,byte	0,
+	byte	204,byte	0,byte	204,
+	byte	170,byte	255,byte	170,
+	byte	170,byte	255,byte	85,
+	byte	170,byte	255,byte	0,
+	byte	170,byte	170,byte	255,
+	byte	187,byte	187,byte	187,
+	byte	187,byte	187,byte	93,
+	byte	187,byte	187,byte	0,
+	byte	170,byte	85,byte	255,
+	byte	187,byte	93,byte	187,
+	byte	187,byte	93,byte	93,
+	byte	187,byte	93,byte	0,
+	byte	170,byte	0,byte	255,
+	byte	187,byte	0,byte	187,
+	byte	187,byte	0,byte	93,
+	byte	187,byte	0,byte	0,
+	byte	170,byte	255,byte	255,
+	byte	158,byte	238,byte	238,
+	byte	158,byte	238,byte	158,
+	byte	158,byte	238,byte	79,
+	byte	158,byte	238,byte	0,
+	byte	158,byte	158,byte	238,
+	byte	170,byte	170,byte	170,
+	byte	170,byte	170,byte	85,
+	byte	170,byte	170,byte	0,
+	byte	158,byte	79,byte	238,
+	byte	170,byte	85,byte	170,
+	byte	170,byte	85,byte	85,
+	byte	170,byte	85,byte	0,
+	byte	158,byte	0,byte	238,
+	byte	170,byte	0,byte	170,
+	byte	170,byte	0,byte	85,
+	byte	170,byte	0,byte	0,
+	byte	153,byte	0,byte	0,
+	byte	147,byte	221,byte	221,
+	byte	147,byte	221,byte	147,
+	byte	147,byte	221,byte	73,
+	byte	147,byte	221,byte	0,
+	byte	147,byte	147,byte	221,
+	byte	153,byte	153,byte	153,
+	byte	153,byte	153,byte	76,
+	byte	153,byte	153,byte	0,
+	byte	147,byte	73,byte	221,
+	byte	153,byte	76,byte	153,
+	byte	153,byte	76,byte	76,
+	byte	153,byte	76,byte	0,
+	byte	147,byte	0,byte	221,
+	byte	153,byte	0,byte	153,
+	byte	153,byte	0,byte	76,
+	byte	136,byte	0,byte	68,
+	byte	136,byte	0,byte	0,
+	byte	136,byte	204,byte	204,
+	byte	136,byte	204,byte	136,
+	byte	136,byte	204,byte	68,
+	byte	136,byte	204,byte	0,
+	byte	136,byte	136,byte	204,
+	byte	136,byte	136,byte	136,
+	byte	136,byte	136,byte	68,
+	byte	136,byte	136,byte	0,
+	byte	136,byte	68,byte	204,
+	byte	136,byte	68,byte	136,
+	byte	136,byte	68,byte	68,
+	byte	136,byte	68,byte	0,
+	byte	136,byte	0,byte	204,
+	byte	136,byte	0,byte	136,
+	byte	85,byte	255,byte	85,
+	byte	85,byte	255,byte	0,
+	byte	85,byte	170,byte	255,
+	byte	93,byte	187,byte	187,
+	byte	93,byte	187,byte	93,
+	byte	93,byte	187,byte	0,
+	byte	85,byte	85,byte	255,
+	byte	93,byte	93,byte	187,
+	byte	119,byte	119,byte	119,
+	byte	119,byte	119,byte	0,
+	byte	85,byte	0,byte	255,
+	byte	93,byte	0,byte	187,
+	byte	119,byte	0,byte	119,
+	byte	119,byte	0,byte	0,
+	byte	85,byte	255,byte	255,
+	byte	85,byte	255,byte	170,
+	byte	79,byte	238,byte	158,
+	byte	79,byte	238,byte	79,
+	byte	79,byte	238,byte	0,
+	byte	79,byte	158,byte	238,
+	byte	85,byte	170,byte	170,
+	byte	85,byte	170,byte	85,
+	byte	85,byte	170,byte	0,
+	byte	79,byte	79,byte	238,
+	byte	85,byte	85,byte	170,
+	byte	102,byte	102,byte	102,
+	byte	102,byte	102,byte	0,
+	byte	79,byte	0,byte	238,
+	byte	85,byte	0,byte	170,
+	byte	102,byte	0,byte	102,
+	byte	102,byte	0,byte	0,
+	byte	79,byte	238,byte	238,
+	byte	73,byte	221,byte	221,
+	byte	73,byte	221,byte	147,
+	byte	73,byte	221,byte	73,
+	byte	73,byte	221,byte	0,
+	byte	73,byte	147,byte	221,
+	byte	76,byte	153,byte	153,
+	byte	76,byte	153,byte	76,
+	byte	76,byte	153,byte	0,
+	byte	73,byte	73,byte	221,
+	byte	76,byte	76,byte	153,
+	byte	85,byte	85,byte	85,
+	byte	85,byte	85,byte	0,
+	byte	73,byte	0,byte	221,
+	byte	76,byte	0,byte	153,
+	byte	85,byte	0,byte	85,
+	byte	85,byte	0,byte	0,
+	byte	68,byte	0,byte	0,
+	byte	68,byte	204,byte	204,
+	byte	68,byte	204,byte	136,
+	byte	68,byte	204,byte	68,
+	byte	68,byte	204,byte	0,
+	byte	68,byte	136,byte	204,
+	byte	68,byte	136,byte	136,
+	byte	68,byte	136,byte	68,
+	byte	68,byte	136,byte	0,
+	byte	68,byte	68,byte	204,
+	byte	68,byte	68,byte	136,
+	byte	68,byte	68,byte	68,
+	byte	68,byte	68,byte	0,
+	byte	68,byte	0,byte	204,
+	byte	68,byte	0,byte	136,
+	byte	68,byte	0,byte	68,
+	byte	0,byte	255,byte	0,
+	byte	0,byte	170,byte	255,
+	byte	0,byte	187,byte	187,
+	byte	0,byte	187,byte	93,
+	byte	0,byte	187,byte	0,
+	byte	0,byte	85,byte	255,
+	byte	0,byte	93,byte	187,
+	byte	0,byte	119,byte	119,
+	byte	0,byte	119,byte	0,
+	byte	0,byte	0,byte	255,
+	byte	0,byte	0,byte	187,
+	byte	0,byte	0,byte	119,
+	byte	51,byte	51,byte	51,
+	byte	0,byte	255,byte	255,
+	byte	0,byte	255,byte	170,
+	byte	0,byte	255,byte	85,
+	byte	0,byte	238,byte	79,
+	byte	0,byte	238,byte	0,
+	byte	0,byte	158,byte	238,
+	byte	0,byte	170,byte	170,
+	byte	0,byte	170,byte	85,
+	byte	0,byte	170,byte	0,
+	byte	0,byte	79,byte	238,
+	byte	0,byte	85,byte	170,
+	byte	0,byte	102,byte	102,
+	byte	0,byte	102,byte	0,
+	byte	0,byte	0,byte	238,
+	byte	0,byte	0,byte	170,
+	byte	0,byte	0,byte	102,
+	byte	34,byte	34,byte	34,
+	byte	0,byte	238,byte	238,
+	byte	0,byte	238,byte	158,
+	byte	0,byte	221,byte	147,
+	byte	0,byte	221,byte	73,
+	byte	0,byte	221,byte	0,
+	byte	0,byte	147,byte	221,
+	byte	0,byte	153,byte	153,
+	byte	0,byte	153,byte	76,
+	byte	0,byte	153,byte	0,
+	byte	0,byte	73,byte	221,
+	byte	0,byte	76,byte	153,
+	byte	0,byte	85,byte	85,
+	byte	0,byte	85,byte	0,
+	byte	0,byte	0,byte	221,
+	byte	0,byte	0,byte	153,
+	byte	0,byte	0,byte	85,
+	byte	17,byte	17,byte	17,
+	byte	0,byte	221,byte	221,
+	byte	0,byte	204,byte	204,
+	byte	0,byte	204,byte	136,
+	byte	0,byte	204,byte	68,
+	byte	0,byte	204,byte	0,
+	byte	0,byte	136,byte	204,
+	byte	0,byte	136,byte	136,
+	byte	0,byte	136,byte	68,
+	byte	0,byte	136,byte	0,
+	byte	0,byte	68,byte	204,
+	byte	0,byte	68,byte	136,
+	byte	0,byte	68,byte	68,
+	byte	0,byte	68,byte	0,
+	byte	0,byte	0,byte	204,
+	byte	0,byte	0,byte	136,
+	byte	0,byte	0,byte	68,
+	byte	0,byte	0,byte	0,
+};
--- /dev/null
+++ b/appl/wm/mpeg/rl0f.tab
@@ -1,0 +1,517 @@
+# vlc -c rl0f
+rl0f_size: con 512;
+rl0f_bits: con 9;
+rl0f_table:= array[] of {
+	(9, 0,C0),
+	(9, 0,C1),
+	(9, 0,C2),
+	(9, 0,C3),
+	(9, 0,C4),
+	(9, 0,C5),
+	(9, 0,C6),
+	(9, 0,C7),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(8, 2,2),
+	(8, 2,2),
+	(8, -2,2),
+	(8, -2,2),
+	(8, 1,9),
+	(8, 1,9),
+	(8, -1,9),
+	(8, -1,9),
+	(8, 4,0),
+	(8, 4,0),
+	(8, -4,0),
+	(8, -4,0),
+	(8, 1,8),
+	(8, 1,8),
+	(8, -1,8),
+	(8, -1,8),
+	(7, 1,7),
+	(7, 1,7),
+	(7, 1,7),
+	(7, 1,7),
+	(7, -1,7),
+	(7, -1,7),
+	(7, -1,7),
+	(7, -1,7),
+	(7, 1,6),
+	(7, 1,6),
+	(7, 1,6),
+	(7, 1,6),
+	(7, -1,6),
+	(7, -1,6),
+	(7, -1,6),
+	(7, -1,6),
+	(7, 2,1),
+	(7, 2,1),
+	(7, 2,1),
+	(7, 2,1),
+	(7, -2,1),
+	(7, -2,1),
+	(7, -2,1),
+	(7, -2,1),
+	(7, 1,5),
+	(7, 1,5),
+	(7, 1,5),
+	(7, 1,5),
+	(7, -1,5),
+	(7, -1,5),
+	(7, -1,5),
+	(7, -1,5),
+	(9, 1,13),
+	(9, -1,13),
+	(9, 6,0),
+	(9, -6,0),
+	(9, 1,12),
+	(9, -1,12),
+	(9, 1,11),
+	(9, -1,11),
+	(9, 2,3),
+	(9, -2,3),
+	(9, 3,1),
+	(9, -3,1),
+	(9, 5,0),
+	(9, -5,0),
+	(9, 1,10),
+	(9, -1,10),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, 1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+	(2, -1,0),
+};
--- /dev/null
+++ b/appl/wm/mpeg/rl0f.vlc
@@ -1,0 +1,34 @@
+# Run/Level First base (first 9 bits)
+# vlc -c rl0f < rl0f.vlc > rl0f.tab
+1s	1,0
+0100s	2,0
+00101s	3,0
+0000110s	4,0
+00100110s	5,0
+00100001s	6,0
+000000101	0,C5
+000000011	0,C3
+000000010	0,C2
+000000001	0,C1
+000000000	0,C0
+011s	1,1
+000110s	2,1
+00100101s	3,1
+000000110	0,C6
+0101s	1,2
+0000100s	2,2
+00111s	1,3
+00100100s	2,3
+00110s	1,4
+000000111	0,C7
+000111s	1,5
+000000100	0,C4
+000101s	1,6
+000100s	1,7
+0000111s	1,8
+0000101s	1,9
+00100111s	1,10
+00100011s	1,11
+00100010s	1,12
+00100000s	1,13
+000001	0,ESC
--- /dev/null
+++ b/appl/wm/mpeg/rl0n.tab
@@ -1,0 +1,517 @@
+# vlc -c rl0n
+rl0n_size: con 512;
+rl0n_bits: con 9;
+rl0n_table:= array[] of {
+	(9, 0,C0),
+	(9, 0,C1),
+	(9, 0,C2),
+	(9, 0,C3),
+	(9, 0,C4),
+	(9, 0,C5),
+	(9, 0,C6),
+	(9, 0,C7),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(6, 0,ESC),
+	(8, 2,2),
+	(8, 2,2),
+	(8, -2,2),
+	(8, -2,2),
+	(8, 1,9),
+	(8, 1,9),
+	(8, -1,9),
+	(8, -1,9),
+	(8, 4,0),
+	(8, 4,0),
+	(8, -4,0),
+	(8, -4,0),
+	(8, 1,8),
+	(8, 1,8),
+	(8, -1,8),
+	(8, -1,8),
+	(7, 1,7),
+	(7, 1,7),
+	(7, 1,7),
+	(7, 1,7),
+	(7, -1,7),
+	(7, -1,7),
+	(7, -1,7),
+	(7, -1,7),
+	(7, 1,6),
+	(7, 1,6),
+	(7, 1,6),
+	(7, 1,6),
+	(7, -1,6),
+	(7, -1,6),
+	(7, -1,6),
+	(7, -1,6),
+	(7, 2,1),
+	(7, 2,1),
+	(7, 2,1),
+	(7, 2,1),
+	(7, -2,1),
+	(7, -2,1),
+	(7, -2,1),
+	(7, -2,1),
+	(7, 1,5),
+	(7, 1,5),
+	(7, 1,5),
+	(7, 1,5),
+	(7, -1,5),
+	(7, -1,5),
+	(7, -1,5),
+	(7, -1,5),
+	(9, 1,13),
+	(9, -1,13),
+	(9, 6,0),
+	(9, -6,0),
+	(9, 1,12),
+	(9, -1,12),
+	(9, 1,11),
+	(9, -1,11),
+	(9, 2,3),
+	(9, -2,3),
+	(9, 3,1),
+	(9, -3,1),
+	(9, 5,0),
+	(9, -5,0),
+	(9, 1,10),
+	(9, -1,10),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, 3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, -3,0),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, 1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, -1,4),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, 1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(6, -1,3),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, 2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, -2,0),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, 1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(5, -1,2),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, 1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(4, -1,1),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(2, 0,EOB),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, 1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+	(3, -1,0),
+};
--- /dev/null
+++ b/appl/wm/mpeg/rl0n.vlc
@@ -1,0 +1,35 @@
+# Run/Level Next base (first 9 bits)
+# vlc -c rl0n < rl0n.vlc > rl0n.tab
+11s	1,0
+0100s	2,0
+00101s	3,0
+0000110s	4,0
+00100110s	5,0
+00100001s	6,0
+000000101	0,C5
+000000011	0,C3
+000000010	0,C2
+000000001	0,C1
+000000000	0,C0
+011s	1,1
+000110s	2,1
+00100101s	3,1
+000000110	0,C6
+0101s	1,2
+0000100s	2,2
+00111s	1,3
+00100100s	2,3
+00110s	1,4
+000000111	0,C7
+000111s	1,5
+000000100	0,C4
+000101s	1,6
+000100s	1,7
+0000111s	1,8
+0000101s	1,9
+00100111s	1,10
+00100011s	1,11
+00100010s	1,12
+00100000s	1,13
+10	0,EOB
+000001	0,ESC
--- /dev/null
+++ b/appl/wm/mpeg/scidct.b
@@ -1,0 +1,160 @@
+implement IDCT;
+
+include "sys.m";
+include "mpegio.m";
+
+init()
+{
+}
+
+# Scaled integer implementation.
+# inverse two dimensional DCT, Chen-Wang algorithm
+# (IEEE ASSP-32, pp. 803-816, Aug. 1984)
+# 32-bit integer arithmetic (8 bit coefficients)
+# 11 mults, 29 adds per DCT
+#
+# coefficients extended to 12 bit for IEEE1180-1990
+# compliance
+
+W1:	con 2841;	# 2048*sqrt(2)*cos(1*pi/16)
+W2:	con 2676;	# 2048*sqrt(2)*cos(2*pi/16)
+W3:	con 2408;	# 2048*sqrt(2)*cos(3*pi/16)
+W5:	con 1609;	# 2048*sqrt(2)*cos(5*pi/16)
+W6:	con 1108;	# 2048*sqrt(2)*cos(6*pi/16)
+W7:	con 565;	# 2048*sqrt(2)*cos(7*pi/16)
+
+W1pW7:	con 3406;	# W1+W7
+W1mW7:	con 2276;	# W1-W7
+W3pW5:	con 4017;	# W3+W5
+W3mW5:	con 799;	# W3-W5
+W2pW6:	con 3784;	# W2+W6
+W2mW6:	con 1567;	# W2-W6
+
+R2:	con 181;	# 256/sqrt(2)
+
+idct(b: array of int)
+{
+	# transform horizontally
+	for(y:=0; y<8; y++){
+		eighty := y<<3;
+		# if all non-DC components are zero, just propagate the DC term
+		if(b[eighty+1]==0)
+		if(b[eighty+2]==0 && b[eighty+3]==0)
+		if(b[eighty+4]==0 && b[eighty+5]==0)
+		if(b[eighty+6]==0 && b[eighty+7]==0){
+			v := b[eighty]<<3;
+			b[eighty+0] = v;
+			b[eighty+1] = v;
+			b[eighty+2] = v;
+			b[eighty+3] = v;
+			b[eighty+4] = v;
+			b[eighty+5] = v;
+			b[eighty+6] = v;
+			b[eighty+7] = v;
+			continue;
+		}
+		# prescale
+		x0 := (b[eighty+0]<<11)+128;
+		x1 := b[eighty+4]<<11;
+		x2 := b[eighty+6];
+		x3 := b[eighty+2];
+		x4 := b[eighty+1];
+		x5 := b[eighty+7];
+		x6 := b[eighty+5];
+		x7 := b[eighty+3];
+		# first stage
+		x8 := W7*(x4+x5);
+		x4 = x8 + W1mW7*x4;
+		x5 = x8 - W1pW7*x5;
+		x8 = W3*(x6+x7);
+		x6 = x8 - W3mW5*x6;
+		x7 = x8 - W3pW5*x7;
+		# second stage
+		x8 = x0 + x1;
+		x0 -= x1;
+		x1 = W6*(x3+x2);
+		x2 = x1 - W2pW6*x2;
+		x3 = x1 + W2mW6*x3;
+		x1 = x4 + x6;
+		x4 -= x6;
+		x6 = x5 + x7;
+		x5 -= x7;
+		# third stage
+		x7 = x8 + x3;
+		x8 -= x3;
+		x3 = x0 + x2;
+		x0 -= x2;
+		x2 = (R2*(x4+x5)+128)>>8;
+		x4 = (R2*(x4-x5)+128)>>8;
+		# fourth stage
+		b[eighty+0] = (x7+x1)>>8;
+		b[eighty+1] = (x3+x2)>>8;
+		b[eighty+2] = (x0+x4)>>8;
+		b[eighty+3] = (x8+x6)>>8;
+		b[eighty+4] = (x8-x6)>>8;
+		b[eighty+5] = (x0-x4)>>8;
+		b[eighty+6] = (x3-x2)>>8;
+		b[eighty+7] = (x7-x1)>>8;
+	}
+	# transform vertically
+	for(x:=0; x<8; x++){
+		# if all non-DC components are zero, just propagate the DC term
+		if(b[x+8*1]==0)
+		if(b[x+8*2]==0 && b[x+8*3]==0)
+		if(b[x+8*4]==0 && b[x+8*5]==0)
+		if(b[x+8*6]==0 && b[x+8*7]==0){
+			v := (b[x+8*0]+32)>>6;
+			b[x+8*0] = v;
+			b[x+8*1] = v;
+			b[x+8*2] = v;
+			b[x+8*3] = v;
+			b[x+8*4] = v;
+			b[x+8*5] = v;
+			b[x+8*6] = v;
+			b[x+8*7] = v;
+			continue;
+		}
+		# prescale
+		x0 := (b[x+8*0]<<8)+8192;
+		x1 := b[x+8*4]<<8;
+		x2 := b[x+8*6];
+		x3 := b[x+8*2];
+		x4 := b[x+8*1];
+		x5 := b[x+8*7];
+		x6 := b[x+8*5];
+		x7 := b[x+8*3];
+		# first stage
+		x8 := W7*(x4+x5) + 4;
+		x4 = (x8+W1mW7*x4)>>3;
+		x5 = (x8-W1pW7*x5)>>3;
+		x8 = W3*(x6+x7) + 4;
+		x6 = (x8-W3mW5*x6)>>3;
+		x7 = (x8-W3pW5*x7)>>3;
+		# second stage
+		x8 = x0 + x1;
+		x0 -= x1;
+		x1 = W6*(x3+x2) + 4;
+		x2 = (x1-W2pW6*x2)>>3;
+		x3 = (x1+W2mW6*x3)>>3;
+		x1 = x4 + x6;
+		x4 -= x6;
+		x6 = x5 + x7;
+		x5 -= x7;
+		# third stage
+		x7 = x8 + x3;
+		x8 -= x3;
+		x3 = x0 + x2;
+		x0 -= x2;
+		x2 = (R2*(x4+x5)+128)>>8;
+		x4 = (R2*(x4-x5)+128)>>8;
+		# fourth stage
+		b[x+8*0] = (x7+x1)>>14;
+		b[x+8*1] = (x3+x2)>>14;
+		b[x+8*2] = (x0+x4)>>14;
+		b[x+8*3] = (x8+x6)>>14;
+		b[x+8*4] = (x8-x6)>>14;
+		b[x+8*5] = (x0-x4)>>14;
+		b[x+8*6] = (x3-x2)>>14;
+		b[x+8*7] = (x7-x1)>>14;
+	}
+}
--- /dev/null
+++ b/appl/wm/mpeg/vlc.b
@@ -1,0 +1,213 @@
+implement Vlc;
+
+include "sys.m";
+include "draw.m";
+include "bufio.m";
+
+#
+#	Construct expanded Vlc (variable length code) tables
+#	from vlc description files.
+#
+
+sys: Sys;
+bufio: Bufio;
+Iobuf: import bufio;
+
+stderr: ref Sys->FD;
+
+sv: adt
+{
+	s:	int;
+	v:	string;
+};
+
+s2list: type list of (string, string);
+bits, size: int;
+table: array of sv;
+prog: string;
+undef: string = "UNDEF";
+xfixed: int = 0;
+complete: int = 0;
+paren: int = 0;
+
+Vlc: module
+{
+	init:	fn(nil: ref Draw->Context, args: list of string);
+};
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	sargs := makestr(args);
+	prog = hd args;
+	args = tl args;
+	bufio = load Bufio Bufio->PATH;
+	if (bufio == nil) {
+		sys->fprint(stderr, "%s: could not load %s: %r\n", prog, Bufio->PATH);
+		return;
+	}
+	inf := bufio->fopen(sys->fildes(0), Bufio->OREAD);
+	if (inf == nil) {
+		sys->fprint(stderr, "%s: fopen stdin failed: %r\n", prog);
+		return;
+	}
+	while (args != nil && len hd args && (a := hd args)[0] == '-') {
+	flag:
+		for (x := 1; x < len a; x++) {
+			case a[x] {
+			'c' =>
+				complete = 1;
+			'f' =>
+				xfixed = 1;
+			'p' =>
+				paren = 1;
+			'u' =>
+				if (++x == len a) {
+					args = tl args;
+					if (args == nil)
+						usage();
+					undef = hd args;
+				} else
+					undef = a[x:];
+				break flag;
+			* =>
+				usage();
+				return;
+			}
+		}
+		args = tl args;
+	}
+	vlc := "vlc";
+	if (args != nil) {
+		if (tl args != nil) {
+			usage();
+			return;
+		}
+		vlc = hd args;
+	}
+	il: s2list;
+	while ((l := inf.gets('\n')) != nil) {
+		if (l[0] == '#')
+			continue;
+		(n, t) := sys->tokenize(l, " \t\n");
+		if (n != 2) {
+			sys->fprint(stderr, "%s: bad input: %s", prog, l);
+			return;
+		}
+		il = (hd t, hd tl t) :: il;
+	}
+	(n, nl) := expand(il);
+	bits = n;
+	size = 1 << bits;
+	table = array[size] of sv;
+	maketable(nl);
+	printtable(vlc, sargs);
+}
+
+usage()
+{
+	sys->fprint(stderr, "usage: %s [-cfp] [-u undef] [stem]\n", prog);
+}
+
+makestr(l: list of string): string
+{
+	s, t: string;
+	while (l != nil) {
+		s = s + t + hd l;
+		t = " ";
+		l = tl l;
+	}
+	return s;
+}
+
+expand(l: s2list): (int, s2list)
+{
+	nl: s2list;
+	max := 0;
+	while (l != nil) {
+		(bs, val) := hd l;
+		n := len bs;
+		if (n > max)
+			max = n;
+		if (bs[n - 1] == 's') {
+			t := bs[:n - 1];
+			nl = (t + "0", val) :: (t + "1", "-" + val) :: nl;
+		} else
+			nl = (bs, val) :: nl;
+		l = tl l;
+	}
+	return (max, nl);
+}
+
+maketable(l: s2list)
+{
+	while (l != nil) {
+		(bs, val) := hd l;
+		z := len bs;
+		if (xfixed && z != bits)
+			error(sys->sprint("string %s too short", bs));
+		s := bits - z;
+		v := value(bs) << s;
+		n := 1 << s;
+		for (i := 0; i < n; i++) {
+			if (table[v].v != nil)
+				error(sys->sprint("repeat match for %x", v));
+			table[v] = (z, val);
+			v++;
+		}
+		l = tl l;
+	}
+}
+
+value(s: string): int
+{
+	n := len s;
+	v := 0;
+	for (i := 0; i < n; i++) {
+		case s[i] {
+		'0' =>
+			v <<= 1;
+		'1'=>
+			v = (v << 1) | 1;
+		* =>
+			error("bad bitstream: " + s);
+		}
+	}
+	return v;
+}
+
+printtable(s, a: string)
+{
+	sys->print("# %s\n", a);
+	sys->print("%s_size: con %d;\n", s, size);
+	sys->print("%s_bits: con %d;\n", s, bits);
+	sys->print("%s_table:= array[] of {\n", s);
+	for (i := 0; i < size; i++) {
+		if (table[i].v != nil) {
+			if (xfixed) {
+				if (paren)
+					sys->print("\t(%s),\n", table[i].v);
+				else
+					sys->print("\t%s,\n", table[i].v);
+			} else
+				sys->print("\t(%d, %s),\n", table[i].s, table[i].v);
+		} else if (!complete) {
+			if (xfixed) {
+				if (paren)
+					sys->print("\t(%s),\n", undef);
+				else
+					sys->print("\t%s,\n", undef);
+			} else
+				sys->print("\t(0, %s),\n", undef);
+		} else
+			error(sys->sprint("no match for %x", i));
+	}
+	sys->print("};\n");
+}
+
+error(s: string)
+{
+	sys->fprint(stderr, "%s: error: %s\n", prog, s);
+	exit;
+}
--- /dev/null
+++ b/appl/wm/mpeg/ydc.tab
@@ -1,0 +1,133 @@
+# vlc ydc
+ydc_size: con 128;
+ydc_bits: con 7;
+ydc_table:= array[] of {
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 1),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(2, 2),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 0),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 3),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(3, 4),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(4, 5),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(5, 6),
+	(6, 7),
+	(6, 7),
+	(7, 8),
+	(0, UNDEF),
+};
--- /dev/null
+++ b/appl/wm/mpeg/ydc.vlc
@@ -1,0 +1,11 @@
+# Luminance DC
+# vlc ydc < ydc.vlc > ydc.tab
+100	0
+00	1
+01	2
+101	3
+110	4
+1110	5
+11110	6
+111110	7
+1111110	8
--- /dev/null
+++ b/appl/wm/mprof.b
@@ -1,0 +1,314 @@
+implement Wmmprof;
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "draw.m";
+	draw: Draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "arg.m";
+	arg: Arg;
+include "profile.m";
+
+Prof: module{
+	init0: fn(ctxt: ref Draw->Context, argv: list of string): Profile->Prof;
+};
+
+prof: Prof;
+
+Wmmprof: module{
+	init: fn(ctxt: ref Draw->Context, argl: list of string);
+};
+
+usage(s: string)
+{
+	sys->fprint(sys->fildes(2), "wm/mprof: %s\n", s);
+	sys->fprint(sys->fildes(2), "usage: wm/mprof [-e] [-m modname]... cmd [arg ... ]\n");
+	exit;
+}
+
+TXTBEGIN: con 3;
+
+init(ctxt: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	arg = load Arg Arg->PATH;
+	
+	if(ctxt == nil)
+		fatal("wm not running");
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	arg->init(argl);
+	while((o := arg->opt()) != 0){
+		case(o){
+			'1' or '2' or '3' or 'e' => ;
+			'm' =>
+				if(arg->arg() == nil)
+					usage("missing module/file");
+			* => 
+				usage(sys->sprint("unknown option -%c", o));
+		}
+	}
+
+	stats := execprof(ctxt, argl);
+	if(stats.mods == nil)
+		exit;
+
+	tkclient->init();
+	(win, wmc) := tkclient->toplevel(ctxt, nil, hd argl, Tkclient->Resize|Tkclient->Hide);
+	tkc := chan of string;
+	tk->namechan(win, tkc, "tkc");
+	for(i := 0; i < len wincfg; i++)
+		cmd(win, wincfg[i]);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	createmenu(win, stats);
+	curc := 0;
+	cura := newprint(win, stats, curc);
+	
+	for(;;){
+		alt{
+			c := <-win.ctxt.kbd =>
+				tk->keyboard(win, c);
+			c := <-win.ctxt.ptr =>
+				tk->pointer(win, *c);
+			c := <-win.ctxt.ctl or
+			c = <-win.wreq or
+			c = <-wmc =>
+				tkclient->wmctl(win, c);
+			c := <- tkc =>
+				(nil, toks) := sys->tokenize(c, " ");
+				case(hd toks){
+					"b" =>
+						if(curc > 0)
+							cura = newprint(win, stats, --curc);
+					"f" =>
+						if(curc < len stats.mods - 1)
+							cura = newprint(win, stats, ++curc);
+					"s" =>
+						if(cura  != nil)
+							scroll(win, cura);
+					"m" =>
+						x := cmd(win, ".f cget actx");
+						y := cmd(win, ".f cget acty");
+						cmd(win, ".f.menu post " + x + " " + y);
+					* =>
+						curc = int hd toks;
+						cura = newprint(win, stats, curc);
+				}
+		}
+	}
+}
+
+execprof(ctxt: ref Draw->Context, argl: list of string): Profile->Prof
+{
+	{
+		prof = load Prof "/dis/mprof.dis";
+		if(prof == nil)
+			fatal("cannot load profiler");
+		return prof->init0(ctxt, hd argl :: "-g" :: tl argl);
+	}
+	exception{
+		"fail:*" =>
+			return (nil, 0, nil);
+	}
+	return (nil, 0, nil);
+}
+
+newprint(win: ref Tk->Toplevel, p: Profile->Prof, i: int): array of int
+{
+	cmd(win, ".f.t delete 1.0 end");
+	cmd(win, "update");
+	m0, m1: list of Profile->Modprof;
+	for(m := p.mods; m != nil && --i >= 0; m = tl m)
+		m0 = m;
+	if(m == nil)
+		return nil;
+	m1 = tl m;	
+	(name, nil, spath, nil, line, nil, nil, tot, tots, nil) := hd m;
+	name0 := name1 := "nil";
+	if(m0 != nil)
+		name0 = (hd m0).name;
+	if(m1 != nil)
+		name1 = (hd m1).name;
+	a := len name;
+	name += sys->sprint(" (%d %d) ", tot, tots[0]);
+	cmd(win, ".f.t insert end {" + name + "        <- " + name0 + "        -> " + name1 + "}");
+	tag := gettag(win, tot+tots[0], p.total+p.totals[0]);
+	cmd(win, ".f.t tag add " + tag + " " + "1.0" + " " + "1." + string a);
+	cmd(win, ".f.t insert end \n\n");
+	cmd(win, "update");
+	lineno := TXTBEGIN;
+	bio := bufio->open(spath, Bufio->OREAD);
+	if(bio == nil)
+		return nil;
+	i = 1;
+	ll := len line/2;
+	while((s := bio.gets('\n')) != nil){
+		f := g := 0;
+		if(i < ll){
+			f = line[2*i];
+			g = line[2*i+1];
+		}
+		a = len s;
+		s = sys->sprint("%d\t%d\t%s", f, g, s);
+		b := len s;
+		cmd(win, ".f.t insert end " + tk->quote(s));
+		tag = gettag(win, f+g, tot+tots[0]);
+		cmd(win, ".f.t tag add " + tag + " " + string lineno + "." + string (b-a) + " " + string lineno + "." + string (b-1));
+		cmd(win, "update");
+		lineno++;
+		i++;
+	}
+	return line;
+}
+
+index(win: ref Tk->Toplevel, x: int, y: int): int
+{
+	t := cmd(win, ".f.t index @" + string x + "," + string y);
+	(nil, l) := sys->tokenize(t, ".");
+# sys->print("%d,%d -> %s\n", x, y, t);
+	return int hd l;
+}
+
+winextent(win: ref Tk->Toplevel): (int, int)
+{
+	w := int cmd(win, ".f.t cget -actwidth");
+	h := int cmd(win, ".f.t cget -actheight");
+	lw := index(win, 0, 0);
+	uw := index(win, w-1, h-1);
+	return (lw, uw);
+}
+
+see(win: ref Tk->Toplevel, line: int)
+{
+	cmd(win, ".f.t see " + string line + ".0");
+	cmd(win, "update");	
+}
+
+scroll(win: ref Tk->Toplevel, line: array of int)
+{
+	(nil, uw) := winextent(win);
+	lno := TXTBEGIN;
+	ll := len line/2;
+	for(i := 1; i < ll; i++){
+		n := line[2*i]+line[2*i+1];
+		if(n > 0 && lno > uw){
+			see(win, lno);
+			return;
+		}
+		lno++;
+	}
+	lno = TXTBEGIN;
+	ll = len line/2;
+	for(i = 1; i < ll; i++){
+		n := line[2*i]+line[2*i+1];
+		if(n > 0){
+			see(win, lno);
+			return;
+		}
+		lno++;
+	}
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	# sys->print("%s\n", s);
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+	exit;
+}
+
+MENUMAX: con 20;
+
+createmenu(top: ref Tk->Toplevel, p: Profile->Prof )
+{
+	mn := ".f.menu";
+	cmd(top, "menu " + mn);
+	i := j := 0;
+	for(m := p.mods; m != nil; m = tl m){
+		name := (hd m).name;
+		cmd(top, mn + " add command -label " + name + " -command {send tkc " + string i + "}");
+		i++;
+		j++;
+		if(j == MENUMAX && tl m != nil){
+			cmd(top, mn + " add cascade -label MORE -menu " + mn + ".menu");
+			mn += ".menu";
+			cmd(top, "menu " + mn);
+			j = 0;
+		}
+	}
+}
+
+tags := array[256]  of { * => byte 0 };
+
+gettag(win: ref Tk->Toplevel, n: int, d: int): string
+{
+	i := int ((real n/real d) * real 15);
+	if(i < 0 || i > 15)
+		i = 0;
+	s := "tag" + string i;
+	if(tags[i] == byte 0){
+		rgb := "#" + hex2(255-64*0)+hex2(255-64*(i/4))+hex2(255-64*(i%4));
+		cmd(win, ".f.t tag configure " + s + " -fg black -bg " + rgb);
+		tags[i] = byte 1;
+	}
+	return s;
+}
+
+hex(i: int): int
+{
+	if(i < 10)
+		return i+'0';
+	else
+		return i-10+'A';
+}
+
+hex2(i: int): string
+{
+	s := "00";
+	s[0] = hex(i/16);
+	s[1] = hex(i%16);
+	return s;
+}
+
+wincfg := array[] of {
+	"frame .f",
+	"text .f.t -width 809 -height 500 -state disabled -wrap char -bg white -yscrollcommand {.f.s set}",
+	"scrollbar .f.s -orient vertical -command {.f.t yview}",
+	"frame .i",
+	"button .i.b -bitmap small_color_left.bit -command {send tkc b}",
+	"button .i.f -bitmap small_color_right.bit -command {send tkc f}",
+	"button .i.s -bitmap small_find.bit -command {send tkc s}",
+	"button .i.m -bitmap small_reload.bit -command {send tkc m}",
+
+	"pack .i.b -side left",
+	"pack .i.f -side left",
+	"pack .i.s -side left",
+	"pack .i.m -side left",
+
+	"pack .f.s -fill y -side left",
+	"pack .f.t -fill both -expand 1",
+
+	"pack .i -fill x",
+	"pack .f -fill both -expand 1",
+	"pack propagate . 0",
+
+	"update",
+};
--- /dev/null
+++ b/appl/wm/pen.b
@@ -1,0 +1,447 @@
+implement Pen;
+
+#
+# pen input on touch screen
+#
+#	Copyright © 2001,2002 Vita Nuova Holdings Limited.  All rights reserved.
+#
+#	This may be used or modified by anyone for any purpose.
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "strokes.m";
+	strokes: Strokes;
+	Classifier, Penpoint, Stroke: import strokes;
+	readstrokes: Readstrokes;
+
+include "arg.m";
+
+Pen: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+debug := 0;
+stderr: ref Sys->FD;
+
+tkconfig := array[] of{
+	"canvas .c -borderwidth 0 -bg white -height 80 -width 80",
+	".c create text 0 0 -anchor nw -width 5w -fill gray -tags mode",
+	".c create text 30 0 -anchor nw -width 3w -fill blue -tags char",
+	"bind .c <Button-1> {grab set .c; send cmd push %x %y}",
+	"bind .c <Motion-Button-1> {send cmd move %x %y}",
+	"bind .c <ButtonRelease-1> {grab release .c; send cmd release %x %y}",
+	"bind .c <Enter> {send cmd move %x %y}",	# does nothing if not previously down
+#	"bind .c <Leave> {send cmd leave %x %y}",	# ditto
+	"pack .c -expand 1 -fill both -padx 5 -pady 5",
+};
+
+usage()
+{
+	sys->fprint(sys->fildes(2), "Usage: pen [-t] [-e] [classifier ...]\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "pen: no window context\n");
+		raise "fail:bad context";
+	}
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	bufio = load Bufio Bufio->PATH;
+	tk = load Tk Tk->PATH;
+	if(tk == nil)
+		nomod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if(tkclient == nil)
+		nomod(Tkclient->PATH);
+	strokes = load Strokes Strokes->PATH;
+	if(strokes == nil)
+		nomod(Strokes->PATH);
+	strokes->init();
+	readstrokes = load Readstrokes Readstrokes->PATH;
+	if(readstrokes == nil)
+		nomod(Readstrokes->PATH);
+	readstrokes->init(strokes);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		nomod(Arg->PATH);
+	arg->init(args);
+	taskbar := 0;
+	noexit := 0;
+	winopts := Tkclient->Appl;
+	corner := 1;
+	while((opt := arg->opt()) != 0)
+		case opt {
+		't' =>
+			taskbar = 1;
+		'e' =>
+			noexit = 1;
+		'r' =>
+			winopts &= ~Tkclient->Resize;
+		'c' =>
+			corner = 0;
+		* =>
+			usage();
+		}
+	args = arg->argv();
+	arg = nil;
+
+	if(args == nil)
+		args = "/lib/strokes/letters.clx" :: "/lib/strokes/digits.clx" :: "/lib/strokes/punc.clx" :: nil;
+	csets := array[len args] of ref Classifier;
+	cs := 0;
+	for(; args != nil; args = tl args){
+		file := hd args;
+		(err, rc) := readstrokes->read_classifier(file, 1, 0);
+		if(rc == nil)
+			error(sys->sprint("can't read classifier %s: %s", file, err));
+		csets[cs++] = rc;
+	}
+	readstrokes = nil;
+
+	rec := csets[0];
+	digits: ref Classifier;
+	if(len csets > 1)
+		digits = csets[1];	# need not actually be digits
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+	(top, ctl) := tkclient->toplevel(ctxt, nil, "Pen", winopts);
+	cmd := chan of string;
+	tk->namechan(top, cmd, "cmd");
+	for (i1 := 0; i1 < len tkconfig; i1++)
+		tkcmd(top, tkconfig[i1]);
+	if(winopts & Tkclient->Resize)
+		tkcmd(top, "pack propagate . 0");
+
+
+	if(corner){
+		(w, h) := (int tk->cmd(top, ". cget -width"), int tk->cmd(top, ". cget -height"));
+		r := ctxt.display.image.r;
+		tkcmd(top, sys->sprint(". configure -x %d -y %d", r.max.x-w, r.max.y-h));
+	}
+
+
+	shift := 0;
+	punct := 0;
+	points := array[1000] of Penpoint;
+	npoint := 0;
+
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "ptr"::nil);
+	if(taskbar)
+		tkclient->wmctl(top, "task");
+	tk->cmd(top, "update");
+
+	for(;;){
+		if(punct)
+			drawmode(top, "#&*");
+		else if(rec == digits)
+			drawmode(top, "123");
+		else if(shift == 1)
+			drawmode(top, "Abc");
+		else if(shift == 2)
+			drawmode(top, "ABC");
+		else if(shift)
+			drawmode(top, "S "+string shift);
+		else
+			drawmode(top, "abc");
+		tk->cmd(top, "update");
+		alt{
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+
+		s := <-top.ctxt.ctl or
+		s = <-top.wreq or
+		s = <-ctl =>
+			if(s == "exit" && noexit)
+				s = "task";
+			tkclient->wmctl(top, s);
+
+		s := <-cmd =>
+			(nf, flds) := sys->tokenize(s, " \t");
+			if(nf < 3)
+				break;
+			p := Penpoint(int hd tl flds, int hd tl tl flds, 0);
+			case hd flds {
+			"push" =>
+				tkcmd(top, "raise .");
+				tk->cmd(top, "update");
+				npoint = 0;
+				points[npoint++] = p;
+			"leave" =>
+				npoint = 0;
+				tkcmd(top, ".c delete stuff");
+			"release" =>
+				if(npoint == 0)
+					break;
+				points[npoint++] = p;
+				(n, tap) := recognize_stroke(top, rec, ref Stroke(npoint, points[0:npoint], 0, 0), debug);
+				drawchars(top, "");
+				name: string = nil;
+				if(n >= 0){
+					name = rec.cnames[n];
+					if(debug > 1){
+						ex: ref Stroke = nil;
+						if(rec.canonex != nil)
+							ex = rec.canonex[n];
+						drawshape(top, "stuff", ex, "blue", rec.dompts[n], "yellow");
+						sys->fprint(stderr, "match: %s\n", name);
+					}
+					case c := name[0] {
+					'S' =>
+						shift = (shift+1)%3;
+						name = nil;
+					'A' =>
+						name = " ";
+					'B' =>
+						name = "\b";
+					'R' =>
+						name = "\n";
+					'T' =>
+						name = "\t";
+					'N' =>
+						# num lock
+						if(rec == digits)
+							rec = csets[0];
+						else
+							rec = digits;
+						name = nil;
+					* =>
+						if(c >= 'A' && c <= 'Z'){	# other gestures, not yet implemented
+							shift = 0;
+							punct = 0;
+							rec = csets[0];
+							name = nil;
+							unknown(top);
+							break;
+						}
+						if(punct){
+							rec = csets[0];
+							punct = 0;
+						}
+						if(shift){
+							for(i := 0; i < len name; i++)
+								if((c = name[i]) >= 'a' && c <= 'z')
+									name[i] += 'A'-'a';
+							if(shift < 2)
+								shift = 0;
+						}
+					}
+				}else if(tap != nil){
+					if(punct == 0){
+						if(len csets > 2){
+							rec = csets[2];
+							punct = 1;
+						}
+						name = nil;
+					}else{
+						rec = csets[0];
+						punct = 0;
+						name = ".";
+					}
+				}else
+					unknown(top);
+				if(name != nil){
+					drawchars(top, name);
+					for(i := 0; i < len name; i++)
+						sys->fprint(top.ctxt.connfd, "key %d", name[i]);
+					#	tk->keyboard(top, name[i]);
+				}
+				tkcmd(top, ".c delete stuff");
+				npoint = 0;
+			* =>
+				if(npoint){
+					q := points[npoint-1];
+					points[npoint++] = p;
+					tkcmd(top, sys->sprint(".c create line %d %d %d %d -tags stuff; update", q.x, q.y, p.x, p.y));
+				}
+			}
+		}
+	}
+}
+
+unknown(top: ref Tk->Toplevel)
+{
+	drawquery(top, (10, 10), 3);
+	tk->cmd(top, "update");
+	sys->sleep(300);
+	tkcmd(top, ".c delete query");
+	tk->cmd(top, "update");
+}
+
+drawchars(top: ref Tk->Toplevel, s: string)
+{
+	t := "";
+	for(i := 0; i < len s; i++){
+		c := s[i];
+		case c {
+		'\n' =>	t += "\\n";
+		'\b' =>	t += "\\b";
+		'\t' =>	t += "\\t";
+		4 =>		t += "eot";
+		* =>
+			if(c < ' ')
+				t += sys->sprint("\\%3.3o", c);
+			else
+				t[len t] = c;
+		}
+	}
+	tkcmd(top, ".c itemconfigure char -text '"+t);
+}
+		
+drawmode(top: ref Tk->Toplevel, mode: string)
+{
+	tkcmd(top, ".c itemconfigure mode -text '"+mode);
+}
+
+drawquery(top: ref Tk->Toplevel, p: Point, scale: int)
+{
+	width := 2;
+	size := 1<<scale;
+	if(size < 4)
+		width = 1;
+	o := Point(p.x-size/2, p.x+size/2);
+	if(o.x < 0)
+		o.x = 0;
+	if(o.y < 0)
+		o.y = 0;
+	c := o.add((size, size));
+	m := o.add(c).div(2);
+	b := c.add((0, size));
+	tkcmd(top, sys->sprint(".c create arc %d %d %d %d -start 150 -extent -240 -style arc -tags query -width %d -outline red", o.x, o.y, c.x, c.y, width));
+	tkcmd(top, sys->sprint(".c create line %d %d %d %d -fill red -width %d -tags query", m.x, c.y, m.x, b.y, width));
+	tkcmd(top, sys->sprint(".c create arc %d %d %d %d -start 0 -extent 360 -fill red -width %d -tags query -style arc -outline red", m.x-width, b.y+2*width, m.x+width, b.y+3*width, width));
+}
+
+tkcmd(top: ref Tk->Toplevel, s: string)
+{
+	e := tk->cmd(top, s);
+	if(e != nil && e[0]=='!')
+		sys->fprint(sys->fildes(2), "pen: tk error: %s in [%s]\n", e, s);
+}
+
+drawshape(top: ref Tk->Toplevel, tag: string, stroke: ref Stroke, colour: string, dompts: ref Stroke, domcol: string)
+{
+	if(top == nil)
+		return;
+	if(stroke != nil)
+		for(i := 1; i < stroke.npts; i++){
+			p := stroke.pts[i-1];
+			q := stroke.pts[i];
+			tkcmd(top, sys->sprint(".c create line %d %d %d %d -fill %s -tags %s", p.x, p.y, q.x, q.y, colour, tag));
+		}
+	if(dompts != nil)
+		for(i = 0; i < dompts.npts; i++){
+			p := dompts.pts[i];
+			tkcmd(top, sys->sprint(".c create oval %d %d %d %d -fill %s -tags %s", p.x-1, p.y-1, p.x+1, p.y+1, domcol, tag));
+		}
+	tk->cmd(top, "update");
+}
+
+#
+# duplicate function of strokes module temporarily
+# to allow for experiment
+#
+
+#DIST_THLD: con 3200;	# x100
+DIST_THLD: con 3300;	# x100
+
+#  Tap-handling parameters
+TAP_TIME_THLD: con 150;	# msec
+TAP_DIST_THLD: con 75;		# dx*dx + dy*dy
+TAP_PATHLEN: con 10*100;		# x100
+
+recognize_stroke(top: ref Tk->Toplevel, rec: ref Classifier, stroke: ref Stroke, debug: int): (int, string)
+{
+
+	if(stroke.npts < 1)
+		return (-1, nil);
+
+	stroke = stroke.filter();	 # filter out close points
+
+	if(stroke.npts == 1 || stroke.length() < TAP_PATHLEN)
+		return (-1, ".");		# considered a tap regardless of elapsed time
+
+	strokes->preprocess_stroke(stroke);
+
+	#  Compute its dominant points.
+	dompts := stroke.interpolate().dominant();
+
+	if(debug)
+		drawshape(top, "stuff", stroke, "green", dompts, "red");
+
+	if(rec == nil)
+		return (-1, nil);
+
+	best_dist := Strokes->MAXDIST;
+	best_i := -1;
+
+	#  Score input stroke against every class in classifier.
+	for(i := 0; i < rec.nclasses; i++){
+		name := rec.cnames[i];
+		(sim, dist) := strokes->score_stroke(dompts, rec.dompts[i]);
+		if(debug > 1 && dist < Strokes->MAXDIST)
+			sys->fprint(stderr, "(%s, %d, %d) ", name, sim, dist);
+		if(dist < DIST_THLD){
+			if(debug > 1)
+				sys->fprint(stderr, "(%s, %d, %d) ", name, sim, dist);
+			#  Is it the best so far?
+			if(dist < best_dist){
+				best_dist = dist;
+				best_i = i;
+			}
+		}
+	}
+
+	if(debug > 1)
+		sys->fprint(stderr, "\n");
+
+	return (best_i, nil);
+}
+
+objrect(t: ref Tk->Toplevel, path: string, addbd: int): Rect
+{
+	r: Rect;
+	r.min.x = int tk->cmd(t, path+" cget -actx");
+	if(addbd)
+		r.min.x += int tk->cmd(t, path+" cget -bd");
+	r.min.y = int tk->cmd(t, ".f cget -acty");	
+	if(addbd)
+		r.min.y += int tk->cmd(t, path+" cget -bd");
+	r.max.x = r.min.x + int tk->cmd(t, path+" cget -actwidth");
+	r.max.y = r.min.y + int tk->cmd(t, path+" cget -actheight");
+	return r;
+}
+
+nomod(s: string)
+{
+	error(sys->sprint("can't load %s: %r", s));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "scribble: %s\n", s);
+	raise "fail:error";
+}
--- /dev/null
+++ b/appl/wm/polyhedra.b
@@ -1,0 +1,800 @@
+implement WmPolyhedra;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Pointer, Image, Screen, Display: import draw;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "math.m";
+	math: Math;
+	sin, cos, tan, sqrt: import math;
+include "rand.m";
+	rand: Rand;
+include "daytime.m";
+	daytime: Daytime;
+include "math/polyhedra.m";
+	polyhedra: Polyhedra;
+	Polyhedron: import Polyhedra;
+	scanpolyhedra, getpolyhedron: import polyhedra;
+include "math/polyfill.m";
+	polyfill: Polyfill;
+	initzbuf, clearzbuf, fillpoly: import polyfill;
+include "smenu.m";
+	smenu: Smenu;
+	Scrollmenu: import smenu;
+
+WmPolyhedra : module
+{
+	init : fn(nil : ref Draw->Context, argv : list of string);
+};
+
+WIDTH, HEIGHT: con 400;
+
+mainwin: ref Toplevel;
+Disp, black, white, opaque: ref Image;
+Dispr: Rect;
+pinit := 40;
+
+init(ctxt : ref Draw->Context, argv : list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	bufio = load Bufio Bufio->PATH;
+	math = load Math Math->PATH;
+	rand = load Rand Rand->PATH;
+	daytime = load Daytime Daytime->PATH;
+	polyhedra = load Polyhedra Polyhedra->PATH;
+	polyfill = load Polyfill Polyfill->PATH;
+	smenu = load Smenu Smenu->PATH;
+	rand->init(daytime->now());
+	daytime = nil;
+	polyfill->init();
+	√2 = sqrt(2.0);
+	√3 = sqrt(3.0);
+	cursor := "";
+
+	tkclient->init();
+	if(ctxt == nil){
+		ctxt = tkclient->makedrawcontext();
+		# sys->fprint(sys->fildes(2), "wm not running\n");
+		# exit;
+	}
+	argv = tl argv;
+	while(argv != nil){
+		case hd argv{
+			"-p" =>
+				argv = tl argv;
+				if(argv != nil)
+					pinit = int hd argv;
+			"-r" =>
+				pinit = -1;
+			"-c" =>
+				argv = tl argv;
+				if(argv != nil)
+					cursor = hd argv;
+		}
+		if(argv != nil)
+			argv = tl argv;
+	}
+	(win, wmcmd) := tkclient->toplevel(ctxt, "", "Polyhedra", Tkclient->Resize | Tkclient->Hide);
+	mainwin = win;
+	sys->pctl(Sys->NEWPGRP, nil);
+	cmdch := chan of string;
+	tk->namechan(win, cmdch, "cmd");
+	for(i := 0; i < len win_config; i++)
+		cmd(win, win_config[i]);
+	if(cursor != nil)
+		cmd(win, "cursor -bitmap " + cursor);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	fittoscreen(win);
+	pid := -1;
+	sync := chan of int;
+	chanθ := chan of real;
+	geo := newgeom();
+	setimage(win, geo);
+	cmd(win, "update");
+	display := win.image.display;
+	white = display.color(Draw->White);
+	black = display.color(Draw->Black);
+	opaque = display.opaque;
+	shade = array[NSHADES] of ref Image;
+	for(i = 0; i < NSHADES; i++){
+		# v := (255*i)/(NSHADES-1);		# NSHADES=17
+		v := (192*i)/(NSHADES-1)+32;		# NSHADES=13
+		# v := (128*i)/(NSHADES-1)+64;	# NSHADES=9
+		shade[i] = display.rgb(v, v, v);
+		# shade[i] = rgba(display, v, v, v, 16r7f);
+	}
+	(geo.npolyhedra, geo.polyhedra, geo.b) = scanpolyhedra("/lib/polyhedra.all");
+	if(geo.npolyhedra == 0){
+		sys->fprint(sys->fildes(2), "cannot open polyhedra database\n");
+		exit;
+	}
+	yieldc := chan of int;
+	# spawn yieldproc(yieldc);
+	# ypid := <- yieldc;
+	initgeom(geo);
+	sm := array[2] of ref Scrollmenu;
+	sm[0] = scrollmenu(win, ".f.menu", geo.polyhedra, geo.npolyhedra, 0);
+	sm[1] = scrollmenu(win, ".f.menud", geo.polyhedra, geo.npolyhedra, 1);
+	# createmenu(win, geo.polyhedra);
+	spawn drawpolyhedron(geo, sync, chanθ, yieldc);
+	pid = <- sync;
+	newproc := 0;
+
+	for(;;){
+		alt{
+		c := <-win.ctxt.kbd =>
+			tk->keyboard(win, c);
+		c := <-win.ctxt.ptr =>
+			tk->pointer(win, *c);
+		c := <-win.ctxt.ctl or
+		c = <-win.wreq =>
+			tkclient->wmctl(win, c);
+		c := <- wmcmd =>
+			case c{
+			"exit" =>
+				exits(pid, sm);
+			* =>
+				sync <-= 0;
+				tkclient->wmctl(win, c);
+				if(c[0] == '!'){
+					if(setimage(win, geo) <= 0)
+						exits(pid, sm);
+				}
+				sync <-= 1;
+			}
+		c := <- cmdch =>
+			(nil, toks) := sys->tokenize(c, " ");
+			case hd toks{
+			"prev" =>
+				geo.curpolyhedron = geo.curpolyhedron.prv;
+				getpoly(geo, -1);
+				newproc = 1;
+			"next" =>
+				geo.curpolyhedron = geo.curpolyhedron.nxt;
+				getpoly(geo, 1);
+				newproc = 1;
+			"dual" =>
+				geo.dual = !geo.dual;
+				newproc = 1;
+			"edges" =>
+				edges = !edges;
+			"faces" =>
+				faces = !faces;
+			"clear" =>
+				clear = !clear;
+			"slow" =>
+				if(geo.θ > ε){
+					if(geo.θ < 2.)
+						chanθ <-= geo.θ/2.;
+					else
+						chanθ <-= geo.θ-1.;
+				}
+			"fast" =>
+				if(geo.θ < 45.){
+					if(geo.θ < 1.)
+						chanθ <-= 2.*geo.θ;
+					else
+						chanθ <-= geo.θ+1.;
+				}
+			"axis" =>
+				setaxis(geo);
+				initmatrix(geo);
+				newproc = 1;
+			"menu" =>
+				x := int cmd(win, ".p cget actx");
+				y := int cmd(win, ".p cget acty");
+				w := int cmd(win, ".p cget -actwidth");
+				h := int cmd(win, ".p cget -actheight");
+				sm[geo.dual].post(x+w/8, y+h/8, cmdch, "");
+				# cmd(win, ".f.menu post " + x + " " + y);
+			* =>
+				i = int hd toks;
+				fp := geo.polyhedra;
+				for(p := fp; p != nil; p = p.nxt){
+					if(p.indx == i){
+						geo.curpolyhedron = p;
+						getpoly(geo, 1);
+						newproc = 1;
+						break;
+					}
+					if(p.nxt == fp)
+						break;
+				}
+			}
+		}
+		if(newproc){
+			sync <-= 0;	# stop it first
+			kill(pid);
+			spawn drawpolyhedron(geo, sync, chanθ, yieldc);
+			pid = <- sync;
+			newproc = 0;
+		}
+	}
+}
+
+setimage(win: ref Toplevel, geo: ref Geom): int
+{
+	panelw := int tk->cmd(win, ".p cget -actwidth");
+	panelh := int tk->cmd(win, ".p cget -actheight");
+	if(panelw < 3)
+		panelw = 3;
+	if(panelh < 3)
+		panelh = 3;
+	Dispr = Rect((0,0), (panelw, panelh));
+	Disp = win.image.display.newimage(Dispr, win.image.chans, 0, Draw->Black);
+	if(Disp == nil){
+		sys->fprint(sys->fildes(2), "not enough image memory\n");
+		return 0;
+	}
+	tk->putimage(win, ".p", Disp, nil);
+	if(Dispr.dx() > Dispr.dy())
+		h := Dispr.dy();
+	else
+		h = Dispr.dx();
+	rr: Rect = ((0, 0), (h, h));
+	corner := ((Dispr.min.x+Dispr.max.x-rr.max.x)/2, (Dispr.min.y+Dispr.max.y-rr.max.y)/2);
+	geo.r = (rr.min.add(corner), rr.max.add(corner));
+	geo.h = h;
+	geo.sx = real ((3*h)/8);
+	geo.sy = - real ((3*h)/8);
+	geo.tx = h/2+geo.r.min.x;
+	geo.ty = h/2+geo.r.min.y;
+	geo.zstate = initzbuf(geo.r);
+	return 1;
+}
+
+# yieldcpu(c: chan of int)
+# {
+# 	c <-= 1;
+# 	<-c;
+# }
+
+# yieldproc(c: chan of int)
+# {
+# 	c <-= sys->pctl(0, nil);
+# 	for (;;) {
+# 		<-c;
+# 		c <-= 1;
+# 	}
+# }
+
+π: con Math->Pi;
+√2, √3: real;
+∞: con 1<<30;
+ε: con 0.001;
+
+Axis: adt{
+	λ, μ, ν: int;
+};
+
+Vector: adt{
+	x, y, z: real;
+};
+	
+Geom: adt{
+	h: int;					# length, breadth of r below
+	r: Rect;					# area on screen to update
+	sx, sy: real;				# x, y scale
+	tx, ty: int;					# x, y translation
+	θ: real;					# angle of rotation
+	TM: array of array of real;		# rotation matrix
+	axis: Axis;					# direction cosines of rotation
+	view: Vector;
+	light: Vector;
+	npolyhedra: int;
+	polyhedra: ref Polyhedron;
+	curpolyhedron: ref Polyhedron;
+	b: ref Iobuf;				# of polyhedra file
+	dual: int;
+	zstate: ref Polyfill->Zstate;
+};
+
+NSHADES: con 13;	# odd
+shade: array of ref Image;
+
+clear, faces: int = 1;
+edges: int = 0;
+
+setview(geo: ref Geom)
+{
+	geo.view = (0.0, 0.0, 1.0);
+	geo.light = (0.0, -1.0, 0.0);
+}
+
+map(v: Vector, geo: ref Geom): Point
+{
+	return (int (geo.sx*v.x)+geo.tx, int (geo.sy*v.y)+geo.ty);
+}
+
+minus(v1: Vector): Vector
+{
+	return (-v1.x, -v1.y, -v1.z);
+}
+
+add(v1, v2: Vector): Vector
+{
+	return (v1.x+v2.x, v1.y+v2.y, v1.z+v2.z);
+}
+
+sub(v1, v2: Vector): Vector
+{
+	return (v1.x-v2.x, v1.y-v2.y, v1.z-v2.z);
+}
+
+mul(v1: Vector, l: real): Vector
+{
+	return (l*v1.x, l*v1.y, l*v1.z);
+}
+
+div(v1: Vector, l: real): Vector
+{
+	return (v1.x/l, v1.y/l, v1.z/l);
+}
+
+normalize(v1: Vector): Vector
+{
+	return div(v1, sqrt(dot(v1, v1)));
+}
+
+dot(v1, v2: Vector): real
+{
+	return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
+}
+
+cross(v1, v2: Vector): Vector
+{
+	return (v1.y*v2.z-v2.y*v1.z, v1.z*v2.x-v2.z*v1.x, v1.x*v2.y-v2.x*v1.y);
+}
+
+drawpolyhedron(geo: ref Geom, sync: chan of int, chanθ: chan of real, yieldc: chan of int)
+{
+	s: string;
+
+	sync <-= sys->pctl(0, nil);
+	p := geo.curpolyhedron;
+	if(!geo.dual || p.anti){
+		s = p.name;
+		s += " (" + string p.indx + ")";
+		puts(s);
+		drawpolyhedron0(p.V, p.F, p.concave, p.allf || p.anti, p.v, p.f, p.fv, p.inc, geo, sync, chanθ, yieldc);
+	}
+	else{
+		s = p.dname;
+		s += " (" + string p.indx + ")";
+		puts(s);
+		drawpolyhedron0(p.F, p.V, p.concave, p.anti, p.f, p.v, p.vf, 0.0, geo, sync, chanθ, yieldc);
+	}
+}
+
+drawpolyhedron0(V, F, concave, allf: int, v, f: array of Vector, fv: array of array of int, inc: real, geo: ref Geom, sync: chan of int, chanθ: chan of real, yieldc: chan of int)
+{
+	norm : array of array of Vector;
+	newn, oldn : array of Vector;
+
+	yieldc = nil;	# not used now
+	θ := geo.θ;
+	totθ := 0.;
+	if(θ != 0.)
+		n := int ((360.+θ/2.)/θ);
+	else
+		n = ∞;
+	p := n;
+	t := 0;
+	vec := array[2] of array of Vector;
+	vec[0] = array[V] of Vector;
+	vec[1] = array[V] of Vector;
+	if(concave){
+		norm = array[2] of array of Vector;
+		norm[0] = array[F] of Vector;
+		norm[1] = array[F] of Vector;
+	}
+	Disp.draw(geo.r, black, opaque, (0, 0));
+	reveal(geo.r);
+	for(i := 0; ; i = (i+1)%p){
+		alt{
+			<- sync =>
+				<- sync;
+			θ = <- chanθ =>
+				geo.θ = θ;
+				initmatrix(geo);
+				if(θ != 0.){
+					n = int ((360.+θ/2.)/θ);
+					p = int ((360.-totθ+θ/2.)/θ);
+				}
+				else
+					n = p = ∞;
+				if(p == 0)
+					i = 0;
+				else
+					i = 1;
+			* =>
+				# yieldcpu(yieldc);
+				sys->sleep(0);
+		}
+		if(concave)
+			clearzbuf(geo.zstate);
+		new := vec[t];
+		old := vec[!t];
+		if(concave){
+			newn = norm[t];
+			oldn = norm[!t];
+		}
+		t = !t;
+		if(i == 0){
+			for(j := 0; j < V; j++)
+				new[j] = v[j];
+			if(concave){
+				for(j = 0; j < F; j++)
+					newn[j] = f[j];
+			}
+			setview(geo);
+			totθ = 0.;
+			p = n;
+		}
+		else{
+			for(j := 0; j < V; j++)
+				new[j] = mulm(geo.TM, old[j]);
+			if(concave){
+				for(j = 0; j < F; j++)
+					newn[j] = mulm(geo.TM, oldn[j]);
+			}
+			else{
+				geo.view = mulmi(geo.TM, geo.view);
+				geo.light = mulmi(geo.TM, geo.light);
+			}
+			totθ += θ;
+		}
+		if(clear)
+			Disp.draw(geo.r, black, opaque, (0, 0));
+		for(j := 0; j < F; j++){
+			if(concave){
+				if(allf || dot(geo.view, newn[j]) < 0.0)
+					polyfilla(fv[j], new, newn[j], dot(geo.light, newn[j]), geo, concave, inc);
+			}
+			else{
+				 if(dot(geo.view, f[j]) < 0.0)
+					polyfilla(fv[j], new, f[j], dot(geo.light, f[j]), geo, concave, 0.0);
+			}
+		}
+		reveal(geo.r);
+	}	
+}
+
+ZSCALE: con real (1<<20);
+LIMIT: con real (1<<11);
+
+polyfilla(fv: array of int, v: array of Vector, f: Vector, ill: real, geo: ref Geom, concave: int, inc: real)
+{
+	dc, dx, dy: int;
+
+	d := 0.0;
+	n := fv[0];
+	ap := array[n+1] of Point;
+	for(j := 0; j < n; j++){
+		vtx := v[fv[j+1]];
+		# vtx = add(vtx, mul(f, 0.1));	# interesting effects with -/larger factors
+		ap[j] = map(vtx, geo);
+		d += dot(f, vtx);
+	}
+	ap[n] = ap[0];
+	d /= real n;
+	if(concave){
+		if(fv[n+1] != 1)
+			d += inc;
+		if(f.z > -ε && f.z < ε)
+			return;
+		α := geo.sx;
+		β := real geo.tx;
+		γ := geo.sy;
+		δ := real geo.ty;
+		c := f.z;
+		a := -f.x/(c*α);
+		if(a <= -LIMIT || a >= LIMIT)
+			return;
+		b := -f.y/(c*γ);
+		if(b <= -LIMIT || b >= LIMIT)
+			return;
+		d = d/c-β*a-δ*b;
+		if(d <= -LIMIT || d >= LIMIT)
+			return;
+		dx = int (a*ZSCALE);
+		dy = int (b*ZSCALE);
+		dc = int (d*ZSCALE);
+	}
+	edge := white;
+	face := shade[int ((real ((NSHADES-1)/2))*(1.0-ill))];
+	if(concave){
+		if(!faces)
+			face = black;
+		if(!edges)
+			edge = nil;
+		fillpoly(Disp, ap, ~0, face, (0, 0), geo.zstate, dc, dx, dy);
+	}
+	else{
+		if(faces)
+			Disp.fillpoly(ap, ~0, face, (0, 0));
+		if(edges)
+			Disp.poly(ap, Draw->Endsquare, Draw->Endsquare, 0, edge, (0, 0));
+	}
+}
+
+getpoly(geo: ref Geom, dir: int)
+{
+	p := geo.curpolyhedron;
+	if(0){
+		while(p.anti){
+			if(dir > 0)
+				p = p.nxt;
+			else
+				p = p.prv;
+		}
+	}
+	geo.curpolyhedron = p;
+	getpolyhedron(p, geo.b);
+}
+	
+degtorad(α: real): real
+{
+	return α*π/180.0;
+}
+
+initmatrix(geo: ref Geom)
+{
+	TM := geo.TM;
+	φ := degtorad(geo.θ);
+	sinθ := sin(φ);
+	cosθ := cos(φ);
+	(l, m, n) := normalize((real geo.axis.λ, real geo.axis.μ, real geo.axis.ν));
+	f := 1.0-cosθ;
+	TM[1][1] = (1.0-l*l)*cosθ + l*l;
+	TM[1][2] = l*m*f-n*sinθ;
+	TM[1][3] = l*n*f+m*sinθ;
+	TM[2][1] = l*m*f+n*sinθ;
+	TM[2][2] = (1.0-m*m)*cosθ + m*m;
+	TM[2][3] = m*n*f-l*sinθ;
+	TM[3][1] = l*n*f-m*sinθ;
+	TM[3][2] = m*n*f+l*sinθ;
+	TM[3][3] = (1.0-n*n)*cosθ + n*n;
+}
+
+mulm(TM: array of array of real, v: Vector): Vector
+{
+	x := v.x;
+	y := v.y;
+	z := v.z;
+	v.x = TM[1][1]*x + TM[1][2]*y + TM[1][3]*z;
+	v.y = TM[2][1]*x + TM[2][2]*y + TM[2][3]*z;
+	v.z = TM[3][1]*x + TM[3][2]*y + TM[3][3]*z;
+	return v;
+}
+
+mulmi(TM: array of array of real, v: Vector): Vector
+{
+	x := v.x;
+	y := v.y;
+	z := v.z;
+	v.x = TM[1][1]*x + TM[2][1]*y + TM[3][1]*z;
+	v.y = TM[1][2]*x + TM[2][2]*y + TM[3][2]*z;
+	v.z = TM[1][3]*x + TM[2][3]*y + TM[3][3]*z;
+	return v;
+}
+
+reveal(r: Rect)
+{
+	cmd := sys->sprint(".p dirty %d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+	tk->cmd(mainwin, cmd);
+	tk->cmd(mainwin, "update");
+}
+
+newgeom(): ref Geom
+{
+	geo := ref Geom;
+	TM := array[4] of array of real;
+	for(i := 0; i < 4; i++)
+		TM[i] = array[4] of real;
+	geo.θ = 10.;
+	geo.TM = TM;
+	geo.axis = (1, 1, 1);
+	geo.view = (1., 1., 1.);
+	geo.light = (1., 1., 1.);
+	geo.dual = 0;
+	return geo;
+}
+
+setaxis(geo: ref Geom)
+{
+	oaxis := geo.axis;
+	# while(geo.axis == Axis (0, 0, 0) || geo.axis = oaxis) not allowed
+	while((geo.axis.λ == 0 && geo.axis.μ == 0 && geo.axis.ν == 0) || (geo.axis.λ == oaxis.λ && geo.axis.μ == oaxis.μ && geo.axis.ν == oaxis.ν))
+		geo.axis = (rand->rand(5) - 2, rand->rand(5) - 2, rand->rand(5) - 2);
+}
+
+initgeom(geo: ref Geom)
+{
+	if(pinit < 0)
+		pn := rand->rand(geo.npolyhedra);
+	else
+		pn = pinit;
+	for(p := geo.polyhedra; --pn >= 0; p = p.nxt)
+		;
+	geo.curpolyhedron = p;
+	getpoly(geo, 1);
+	setaxis(geo);
+  	geo.θ = real (rand->rand(5)+1);
+	geo.dual = 0;
+  	initmatrix(geo);
+	setview(geo);
+  	Disp.draw(geo.r, black, opaque, (0, 0));
+	reveal(geo.r);
+}
+
+kill(pid: int): int
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil)
+		return -1;
+	if(sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+exits(pid: int, sm: array of ref Scrollmenu)
+{
+	if(pid != -1)
+		kill(pid);
+	# kill(ypid);
+	sm[0].destroy();
+	sm[1].destroy();
+	exit;
+}
+
+cmd(top: ref Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "polyhedra: tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+puts(s: string)
+{
+	cmd(mainwin, ".f1.txt configure -text {" + s + "}");
+	cmd(mainwin, "update");
+}
+
+MENUMAX: con 10;
+
+scrollmenu(top: ref Tk->Toplevel, mname: string, p: ref Polyhedron, n: int, dual: int): ref Scrollmenu
+{
+	labs := array[n] of string;
+	i := 0;
+	for(q := p; q != nil && i < n; q = q.nxt){
+		if(dual)
+			name := q.dname;
+		else
+			name = q.name;
+		labs[i++] = string q.indx + " " + name;
+	}
+	sm := Scrollmenu.new(top, mname, labs, MENUMAX, (n-MENUMAX)/2);
+	cmd(top, mname + " configure -borderwidth 3");
+	return sm;
+}
+
+createmenu(top: ref Tk->Toplevel, p: ref Polyhedron)
+{
+	mn := ".f.menu";
+	cmd(top, "menu " + mn);
+	i := j := 0;
+	for(q := p ; q != nil; q = q.nxt){
+		cmd(top, mn + " add command -label {" + string q.indx + " " + q.name + "} -command {send cmd " + string q.indx + "}");
+		if(q.nxt == p)
+			break;
+		i++;
+		j++;
+		if(j == MENUMAX && q.nxt != nil){
+			cmd(top, mn + " add cascade -label MORE -menu " + mn + ".menu");
+			mn += ".menu";
+			cmd(top, "menu " + mn);
+			j = 0;
+		}
+	}
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	Point: import draw;
+	if (win.image == nil || win.image.screen == nil)
+		return;
+	r := win.image.screen.image.r;
+	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+	bd := int cmd(win, ". cget -bd");
+	winsize := Point(int cmd(win, ". cget -actwidth") + bd * 2, int cmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		cmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		cmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int cmd(win, ". cget -actx"), int cmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int cmd(win, ". cget -actwidth") + bd*2,
+				int cmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.max.x - dx, r.max.x);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.max.y - dy, r.max.y);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	cmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
+
+win_config := array[] of {
+	"frame .f",
+	"button .f.prev -text {prev} -command {send cmd prev}",
+	"button .f.next -text {next} -command {send cmd next}",
+	"checkbutton .f.dual -text {dual} -command {send cmd dual} -variable dual",
+	".f.dual deselect",
+	"pack .f.prev -side left",
+	"pack .f.next -side right",
+	"pack .f.dual -side top",
+
+	"frame .f0",
+	"checkbutton .f0.edges -text {edges} -command {send cmd edges} -variable edges",
+	".f0.edges deselect",
+	"checkbutton .f0.faces -text {faces} -command {send cmd faces} -variable faces",
+	".f0.faces select",
+	"checkbutton .f0.clear -text {clear} -command {send cmd clear} -variable clear",
+	".f0.clear select",
+	"pack .f0.edges -side left",
+	"pack .f0.faces -side right",
+	"pack .f0.clear -side top",
+
+	"frame .f2",
+	"button .f2.slow -text {slow} -command {send cmd slow}",
+	"button .f2.fast -text {fast} -command {send cmd fast}",
+	"button .f2.axis -text {axis} -command {send cmd axis}",
+	"pack .f2.slow -side left",
+	"pack .f2.fast -side right",
+	"pack .f2.axis -side top",
+
+	"frame .f1",
+	"label .f1.txt -text { } -width " + string WIDTH,
+	"pack .f1.txt -side top -fill x",
+
+	"frame .f3",
+	"button .f3.menu -text {menu} -command {send cmd menu}",
+	"pack .f3.menu -side left",
+
+	"frame .pbd -bd 3",
+	"panel .p -width " + string WIDTH + " -height " + string HEIGHT,
+
+	"pack .f -side top -fill x",
+	"pack .f0 -side top -fill x",
+	"pack .f2 -side top -fill x",
+	"pack .f1 -side top -fill x",
+	"pack .f3 -side top -fill x",
+	"pack .p -in .pbd -fill both -expand 1",
+	"pack .pbd -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+
+};
+
+rgba(d: ref Display, r: int, g: int, b: int, α: int): ref Image
+{
+	c := draw->setalpha((r<<24)|(g<<16)|(b<<8), α);
+	return d.newimage(((0, 0), (1, 1)), d.image.chans, 1, c);
+}
--- /dev/null
+++ b/appl/wm/prof.b
@@ -1,0 +1,323 @@
+implement Wmprof;
+
+include "sys.m";
+	sys: Sys;
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "draw.m";
+	draw: Draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "arg.m";
+	arg: Arg;
+include "profile.m";
+
+Prof: module{
+	init0: fn(ctxt: ref Draw->Context, argv: list of string): Profile->Prof;
+};
+
+prof: Prof;
+
+Wmprof: module{
+	init: fn(ctxt: ref Draw->Context, argl: list of string);
+};
+
+usage(s: string)
+{
+	sys->fprint(sys->fildes(2), "wm/prof: %s\n", s);
+	sys->fprint(sys->fildes(2), "usage: wm/prof [-e] [-m modname]... cmd [arg ... ]\n");
+	exit;
+}
+
+TXTBEGIN: con 3;
+
+init(ctxt: ref Draw->Context, argl: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	arg = load Arg Arg->PATH;
+	
+	if(ctxt == nil)
+		fatal("wm not running");
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	arg->init(argl);
+	while((o := arg->opt()) != 0){
+		case(o){
+			'e' => ;
+			'm' =>
+				if(arg->arg() == nil)
+					usage("missing module/file");
+			's' =>
+				if(arg->arg() == nil)
+					usage("missing sample rate");
+			* => 
+				usage(sys->sprint("unknown option -%c", o));
+		}
+	}
+
+	stats := execprof(ctxt, argl);
+	if(stats.mods == nil)
+		exit;
+
+	tkclient->init();
+	(win, wmc) := tkclient->toplevel(ctxt, nil, hd argl, Tkclient->Resize|Tkclient->Hide);
+	tkc := chan of string;
+	tk->namechan(win, tkc, "tkc");
+	for(i := 0; i < len wincfg; i++)
+		cmd(win, wincfg[i]);
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	createmenu(win, stats);
+	curc := 0;
+	cura := newprint(win, stats, curc);
+	
+	for(;;){
+		alt{
+			c := <-win.ctxt.kbd =>
+				tk->keyboard(win, c);
+			c := <-win.ctxt.ptr =>
+				tk->pointer(win, *c);
+			c := <-win.ctxt.ctl or
+			c = <-win.wreq or
+			c = <-wmc =>
+				tkclient->wmctl(win, c);
+			c := <- tkc =>
+				(nil, toks) := sys->tokenize(c, " ");
+				case(hd toks){
+					"b" =>
+						if(curc > 0)
+							cura = newprint(win, stats, --curc);
+					"f" =>
+						if(curc < len stats.mods - 1)
+							cura = newprint(win, stats, ++curc);
+					"s" =>
+						if(cura  != nil)
+							scroll(win, cura);
+					"m" =>
+						x := cmd(win, ".f cget actx");
+						y := cmd(win, ".f cget acty");
+						cmd(win, ".f.menu post " + x + " " + y);
+					* =>
+						curc = int hd toks;
+						cura = newprint(win, stats, curc);
+				}
+		}
+	}
+}
+
+execprof(ctxt: ref Draw->Context, argl: list of string): Profile->Prof
+{
+	{
+		prof = load Prof "/dis/prof.dis";
+		if(prof == nil)
+			fatal("cannot load profiler");
+		return prof->init0(ctxt, hd argl :: "-g" :: tl argl);
+	}
+	exception{
+		"fail:*" =>
+			return (nil, 0, nil);
+	}
+	return (nil, 0, nil);
+}
+
+newprint(win: ref Tk->Toplevel, p: Profile->Prof, i: int): array of int
+{
+	cmd(win, ".f.t delete 1.0 end");
+	cmd(win, "update");
+	m0, m1: list of Profile->Modprof;
+	for(m := p.mods; m != nil && --i >= 0; m = tl m)
+		m0 = m;
+	if(m == nil)
+		return nil;
+	m1 = tl m;	
+	(name, nil, spath, nil, line, nil, nil, tot, nil, nil) := hd m;
+	name0 := name1 := "nil";
+	if(m0 != nil)
+		name0 = (hd m0).name;
+	if(m1 != nil)
+		name1 = (hd m1).name;
+	a := len name;
+	name += sys->sprint(" (%d%%) ", percent(tot, p.total));
+	cmd(win, ".f.t insert end {" + name + "        <- " + name0 + "        -> " + name1 + "}");
+	tag := gettag(win, tot, p.total);
+	cmd(win, ".f.t tag add " + tag + " " + "1.0" + " " + "1." + string a);
+	cmd(win, ".f.t insert end \n\n");
+	cmd(win, "update");
+	lineno := TXTBEGIN;
+	bio := bufio->open(spath, Bufio->OREAD);
+	if(bio == nil)
+		return nil;
+	i = 1;
+	ll := len line;
+	while((s := bio.gets('\n')) != nil){
+		f := 0;
+		if(i < ll)
+			f = line[i];
+		a = len s;
+		if(f > 0)
+			s = sys->sprint("%d%%\t%s", percent(f, tot), s);
+		else
+			s = sys->sprint("- \t%s", s);
+		b := len s;
+		cmd(win, ".f.t insert end " + tk->quote(s));
+		tag = gettag(win, f, tot);
+		cmd(win, ".f.t tag add " + tag + " " + string lineno + "." + string (b-a) + " " + string lineno + "." + string (b-1));
+		cmd(win, "update");
+		lineno++;
+		i++;
+	}
+	return line;
+}
+
+index(win: ref Tk->Toplevel, x: int, y: int): int
+{
+	t := cmd(win, ".f.t index @" + string x + "," + string y);
+	(nil, l) := sys->tokenize(t, ".");
+# sys->print("%d,%d -> %s\n", x, y, t);
+	return int hd l;
+}
+
+winextent(win: ref Tk->Toplevel): (int, int)
+{
+	w := int cmd(win, ".f.t cget -actwidth");
+	h := int cmd(win, ".f.t cget -actheight");
+	lw := index(win, 0, 0);
+	uw := index(win, w-1, h-1);
+	return (lw, uw);
+}
+
+see(win: ref Tk->Toplevel, line: int)
+{
+	cmd(win, ".f.t see " + string line + ".0");
+	cmd(win, "update");	
+}
+
+scroll(win: ref Tk->Toplevel, line: array of int)
+{
+	(nil, uw) := winextent(win);
+	lno := TXTBEGIN;
+	ll := len line;
+	for(i := 1; i < ll; i++){
+		n := line[i];
+		if(n > 0 && lno > uw){
+			see(win, lno);
+			return;
+		}
+		lno++;
+	}
+	lno = TXTBEGIN;
+	ll = len line;
+	for(i = 1; i < ll; i++){
+		n := line[i];
+		if(n > 0){
+			see(win, lno);
+			return;
+		}
+		lno++;
+	}
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	# sys->print("%s\n", s);
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+	exit;
+}
+
+MENUMAX: con 20;
+
+createmenu(top: ref Tk->Toplevel, p: Profile->Prof )
+{
+	mn := ".f.menu";
+	cmd(top, "menu " + mn);
+	i := j := 0;
+	for(m := p.mods; m != nil; m = tl m){
+		name := (hd m).name;
+		cmd(top, mn + " add command -label " + name + " -command {send tkc " + string i + "}");
+		i++;
+		j++;
+		if(j == MENUMAX && tl m != nil){
+			cmd(top, mn + " add cascade -label MORE -menu " + mn + ".menu");
+			mn += ".menu";
+			cmd(top, "menu " + mn);
+			j = 0;
+		}
+	}
+}
+
+tags := array[256]  of { * => byte 0 };
+
+gettag(win: ref Tk->Toplevel, n: int, d: int): string
+{
+	i := int ((real n/real d) * real 15);
+	if(i < 0 || i > 15)
+		i = 0;
+	s := "tag" + string i;
+	if(tags[i] == byte 0){
+		rgb := "#" + hex2(255-64*0)+hex2(255-64*(i/4))+hex2(255-64*(i%4));
+		cmd(win, ".f.t tag configure " + s + " -fg black -bg " + rgb);
+		tags[i] = byte 1;
+	}
+	return s;
+}
+
+percent(n: int, d: int): int
+{
+	return int ((real n/real d) * real 100);
+}
+
+hex(i: int): int
+{
+	if(i < 10)
+		return i+'0';
+	else
+		return i-10+'A';
+}
+
+hex2(i: int): string
+{
+	s := "00";
+	s[0] = hex(i/16);
+	s[1] = hex(i%16);
+	return s;
+}
+
+wincfg := array[] of {
+	"frame .f",
+	"text .f.t -width 809 -height 500 -state disabled -wrap char -bg white -yscrollcommand {.f.s set}",
+	"scrollbar .f.s -orient vertical -command {.f.t yview}",
+	"frame .i",
+	"button .i.b -bitmap small_color_left.bit -command {send tkc b}",
+	"button .i.f -bitmap small_color_right.bit -command {send tkc f}",
+	"button .i.s -bitmap small_find.bit -command {send tkc s}",
+	"button .i.m -bitmap small_reload.bit -command {send tkc m}",
+
+	"pack .i.b -side left",
+	"pack .i.f -side left",
+	"pack .i.s -side left",
+	"pack .i.m -side left",
+
+	"pack .f.s -fill y -side left",
+	"pack .f.t -fill both -expand 1",
+
+	"pack .i -fill x",
+	"pack .f -fill both -expand 1",
+	"pack propagate . 0",
+
+	"update",
+};
--- /dev/null
+++ b/appl/wm/qt.b
@@ -1,0 +1,161 @@
+implement WmQt;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+	ctxt: ref Draw->Context;
+
+include "quicktime.m";
+	qt: QuickTime;
+
+WmQt: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Stopped, Playing: con iota;
+
+task_cfg := array[] of {
+	"canvas .c",
+	"frame .b",
+	"button .b.File -text File -command {send cmd file}",
+	"button .b.Stop -text Stop -command {send cmd stop}",
+	"button .b.Pause -text Pause -command {send cmd pause}",
+	"button .b.Play -text Play -command {send cmd play}",
+	"frame .f",
+	"label .f.file -text {File:}",
+	"label .f.name",
+	"pack .f.file .f.name -side left",
+	"pack .b.File .b.Stop .b.Pause .b.Play -side left",
+	"pack .f -fill x",
+	"pack .b -anchor w",
+	"pack .c -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+init(xctxt: ref Draw->Context, nil: list of string)
+{
+	sys  = load Sys  Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "qt: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk   = load Tk   Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+
+	ctxt = xctxt;
+
+	tkclient->init();
+	(t, menubut) := tkclient->toplevel(ctxt.screen, "", "QuickTime Player", 0);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	tkclient->tkcmds(t, task_cfg);
+
+	tk->cmd(t, "bind . <Configure> {send cmd resize}");
+	tk->cmd(t, "update");
+
+	qt = load QuickTime QuickTime->PATH;
+	if(qt == nil) {
+		tkclient->dialog(t, "error -fg red", "Load Module",
+				"Failed to load the QuickTime interface:\n"+
+					sys->sprint("%r"),
+				0, "Exit"::nil);
+		return;
+	}
+	qt->init();
+
+	fname := "";
+	ctl := chan of string;
+	state := Stopped;
+
+	for(;;) alt {
+	menu := <-menubut =>
+		if(menu == "exit")
+			return;
+		tkclient->wmctl(t, menu);
+	press := <-cmd =>
+		case press {
+		"file" =>
+			pat := list of {
+				"*.mov (Apple QuickTime Movie)"
+			};
+			fname = tkclient->filename(ctxt.screen, t, "Locate Movie", pat, "");
+			if(fname != nil) {
+				s := fname;
+				if(len s > 25)
+					s = "..."+fname[len s - 25:];
+				tk->cmd(t, ".f.name configure -text {"+s+"}");
+				tk->cmd(t, "update");
+			}
+		"play" =>
+			if(fname != nil)
+				spawn play(t, fname);
+		}
+	}
+}
+
+#
+# Parse the atoms describing a movie
+#
+moov(t: ref Toplevel, q: ref QuickTime->QD)
+{
+	for(;;) {
+		(h, l) := qt->q.atomhdr();
+		if(l < 0)
+			break;
+		case h {
+		* =>
+			qt->q.skipatom(l);
+		"mvhd" =>
+			err := qt->q.mvhd(l);
+			if(err == nil)
+				break;
+			tkclient->dialog(t, "error -fg red", "Parse Headers",
+					err,
+					0, "Exit"::nil);
+			exit;
+		"trak" =>
+			err := qt->q.trak(l);
+			if(err == nil)
+				break;
+			tkclient->dialog(t, "error -fg red", "Parse Track",
+					err,
+					0, "Exit"::nil);
+			exit;
+		}
+	}
+}
+
+play(t: ref Toplevel, file: string)
+{
+	(q, err) := qt->open(file);
+	if(err != nil) {
+		tkclient->dialog(t, "error -fg red", "Open Movie",
+					"Failed to open \""+file+"\"\n"+err,
+					0, "Continue"::nil);
+		return;
+	}
+	for(;;) {
+		(h, l) := qt->q.atomhdr();
+		if(l < 0)
+			break;
+		case h {
+		* =>
+			qt->q.skipatom(l);
+		"moov" =>
+			moov(t, q);
+		}
+	}
+}
--- /dev/null
+++ b/appl/wm/readmail.b
@@ -1,0 +1,885 @@
+implement WmReadmail;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context: import draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include "string.m";
+	str: String;
+
+include "keyring.m";
+	kr: Keyring;
+
+WmReadmail: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+WmSendmail: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+srv: Sys->Connection;
+main: ref Toplevel;
+ctxt: ref Context;
+nmesg: int;
+cmesg: int;
+map: array of byte;
+Ok, Deleted: con iota;
+username: string;
+
+mail_cfg := array[] of {
+	"frame .top",
+	"label .top.l -bitmap email.bit",
+	"frame .top.con",
+	"frame .top.con.b",
+	"button .top.con.b.con -bitmap mailcon -command {send msg connect}",
+	"bind .top.con.b.con <Enter> +{.top.status configure -text {connect/disconnect to mail server}}",
+	"button .top.con.b.next -bitmap mailnext -command {send msg next}",
+	"bind .top.con.b.next <Enter> +{.top.status configure -text {next message}}",
+	"button .top.con.b.prev -bitmap mailprev -command {send msg prev}",
+	"bind .top.con.b.prev <Enter> +{.top.status configure -text {previous message}}",
+	"button .top.con.b.del -bitmap maildel -command {send msg dele}",
+	"bind .top.con.b.del <Enter> +{.top.status configure -text {delete message}}",
+	"button .top.con.b.reply -bitmap mailreply -command {send msg reply}",
+	"bind .top.con.b.reply <Enter> +{.top.status configure -text {reply to message}}",
+	"button .top.con.b.fwd -bitmap mailforward",
+	"bind .top.con.b.fwd <Enter> +{.top.status configure -text {forward message}}",
+	"button .top.con.b.hdr -bitmap mailhdr -command {send msg hdrs}",
+	"bind .top.con.b.hdr <Enter> +{.top.status configure -text {fetch message headers}}",
+	"button .top.con.b.save -bitmap mailsave -command {send msg save}",
+	"bind .top.con.b.save <Enter> +{.top.status configure -text {save message}}",
+	"pack .top.con.b.con .top.con.b.prev .top.con.b.next .top.con.b.del .top.con.b.reply .top.con.b.fwd .top.con.b.hdr .top.con.b.save -padx 2 -side left",
+	"label .top.status -text {not connected ...} -anchor w",
+	"pack .top.l -side left",
+	"pack .top.con -side left -padx 10",
+	"pack .top.con.b .top.status -in .top.con -fill x -expand 1",
+	"frame .hdr",
+	"scrollbar .hdr.scroll -command {.hdr.t yview}",
+	"text .hdr.t -height 3c -yscrollcommand {.hdr.scroll set} -bg white",
+	"frame .hdr.pad -width 2c",
+	"pack .hdr.t -side left -fill x -expand 1",
+	"pack .hdr.scroll -side left -fill y",
+	"pack .hdr.pad",
+	"frame .body",
+	"scrollbar .body.scroll -command {.body.t yview}",
+	"text .body.t -width 15c -height 7c -yscrollcommand {.body.scroll set} -bg white",
+	"pack .body.t -side left -expand 1 -fill both",
+	"pack .body.scroll -side left -fill y",
+	"pack .top -anchor w -padx 5",
+	"pack .hdr -fill x -anchor w -padx 5 -pady 5",
+	"pack .body -expand 1 -fill both -padx 5 -pady 5",
+	"pack .b -padx 5 -pady 5 -fill x",
+	"pack propagate . 0",
+	"update"
+};
+
+con_cfg := array[] of {
+	"frame .b",
+	"button .b.ok -text {Connect} -command {send cmd ok}",
+	"button .b.can -text {Cancel} -command {send cmd can}",
+	"pack .b.ok .b.can -side left -fill x -padx 10 -pady 10 -expand 1",
+	"frame .l",
+	"label .l.h -text {Mail Server:} -anchor w",
+	"label .l.u -text {User Name:} -anchor w",
+	"label .l.s -text {Secret:} -anchor w",
+	"pack .l.h .l.u .l.s -fill both -expand 1",
+	"frame .e",
+	"entry .e.h",
+	"entry .e.u",
+	"entry .e.s -show •",
+	"pack .e.h .e.u .e.s -fill x",
+	"frame .f -borderwidth 2 -relief raised",
+	"pack .l .e -fill both -expand 1 -side left -in .f",
+	"pack .f",
+	"pack .b -fill x -expand 1",
+	"bind .e.h <Key-\n> {send cmd ok}",
+	"bind .e.u <Key-\n> {send cmd ok}",
+	"bind .e.s <Key-\n> {send cmd ok}",
+	"focus .e.s",
+};
+
+hdr_cfg := array[] of {
+	"scrollbar .sh -orient horizontal -command {.f.l xview}",
+	"scrollbar .f.sv -command {.f.l yview}",
+	"frame .f",
+	"listbox .f.l -width 80w -height 20h -yscrollcommand { .f.sv set} -xscrollcommand { .sh set}",
+	"pack .f.l -side left -fill both -expand 1",
+	"pack .f.sv -side left -fill y",
+	"pack .f -fill both -expand 1",
+	"pack .sh -fill x",
+	"pack propagate . 0",
+	"bind .f.l <Double-Button> { send tomain [.f.l get [.f.l curselection]] }",
+	"update",
+};
+
+init(xctxt: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (xctxt == nil) {
+		sys->fprint(sys->fildes(2), "readmail: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+	str = load String String->PATH;
+	kr = load Keyring Keyring->PATH;
+
+	ctxt = xctxt;
+
+	tkclient->init();
+	dialog->init();
+	selectfile->init();
+
+	tkargs := "";
+	argv = tl argv;
+	if(argv != nil) {
+		tkargs = hd argv;
+		argv = tl argv;
+	}
+
+	titlectl := chan of string;
+	(main, titlectl) = tkclient->toplevel(ctxt, tkargs, "Readmail: Reader", Tkclient->Appl);
+
+	msg := chan of string;
+	tk->namechan(main, msg, "msg");
+	hdr := chan of string;
+
+	for (c:=0; c<len mail_cfg; c++)
+		tk->cmd(main, mail_cfg[c]);
+	tkclient->onscreen(main, nil);
+	tkclient->startinput(main, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+		s := <-main.ctxt.kbd =>
+			tk->keyboard(main, s);
+		s := <-main.ctxt.ptr =>
+			tk->pointer(main, *s);
+		s := <-main.ctxt.ctl or
+		s = <-main.wreq or
+		s = <-titlectl =>
+			if(s == "exit") {
+				if(srv.dfd != nil) {
+					status("Updating mail box...");
+					pop3cmd("QUIT");
+				}
+				return;
+			}
+			tkclient->wmctl(main, s);
+	cmd := <-msg =>
+		case cmd {
+		"connect" =>
+			if(srv.dfd == nil) {
+				connect(main);
+				if(srv.dfd != nil)
+					initialize();
+				break;
+			}
+			disconnect();
+		"prev" =>
+			if(cmesg > nmesg) {
+				status("no more messages.");
+				break;
+			}
+			for(new := cmesg+1; new <= nmesg; new++) {
+				if(map[new] == byte Ok) {
+					cmesg = new;
+					loadmesg();
+					break;
+				}
+			}
+		"next" =>
+			for(new := cmesg-1; new >= 1; new--) {
+				if(map[new] == byte Ok) {
+					cmesg = new;
+					loadmesg();
+					break;
+				}
+			}
+		"dele" =>
+			delete();
+			if(cmesg > 0) {
+				cmesg--;
+				loadmesg();
+			}
+		"hdrs" =>
+			headers(hdr);
+		"save" =>
+			save();
+		"reply" =>
+			reply();
+		}
+	get := <-hdr =>
+		new := int get;
+		if(new < 1 || new > nmesg || map[new] != byte Ok)
+			break;		
+		cmesg = new;
+		loadmesg();
+	}
+}
+
+headers(tomain: chan of string)
+{
+	(hdr, hdrctl) := tkclient->toplevel(ctxt, nil,
+				"Readmail: Headers", Tkclient->Appl);
+
+	tk->namechan(hdr, tomain, "tomain");
+
+	for (c:=0; c<len hdr_cfg; c++)
+		tk->cmd(hdr, hdr_cfg[c]);
+
+	for(i := 1; i <= nmesg; i++) {
+		if(map[i] == byte Deleted) {
+			info := sys->sprint("%4d ...Deleted...\n", i);
+			tk->cmd(hdr, ".f.l insert 0 '"+info);
+			continue;
+		}
+		if(topit(hdr, i) == 0)
+			break;
+		alt {
+		s := <-hdrctl =>
+			if(s == "exit")
+				return;
+			tkclient->wmctl(hdr, s);
+		* =>
+			;
+		}
+		if((i%10) == 9)
+			tk->cmd(hdr, "update");
+	}
+	tk->cmd(hdr, "update");
+	tkclient->onscreen(hdr, nil);
+	tkclient->startinput(hdr, "kbd"::"ptr"::nil);
+
+	spawn hproc(hdrctl, hdr);
+}
+
+trunc(name: string): string
+{
+	for(i := 0; i < len name; i++)
+		if(name[i] == '<')
+			break;
+	i++;
+	if(i >= len name)
+		return name;
+	for(j := i; j < len name; j++)
+		if(name[j] == '>')
+			break;
+	return name[i:j];
+}
+
+topit(hdr: ref Toplevel, msg: int): int
+{
+	(err, s) := pop3cmd("TOP "+string msg+" 0");
+	if(err != nil) {
+		dialog->prompt(ctxt, hdr.image, "error -fg red", "POP3 Error",
+				"Ecountered a problem fetching headers\n"+err,
+				0, "Dismiss"::nil);
+		return 0;
+	}
+
+	size := int s;
+	b := pop3body(size);
+	if(b == nil)
+		return 0;
+
+	from := getfield("from", b);
+	from = trunc(from);
+	date := getfield("date", b);
+	subj := getfield("subject", b);
+	if(len subj > 20)
+		subj = subj[0:19];
+
+	if(len subj > 0)
+		info := sys->sprint("%4d %5d %s \"%s\" %s", msg, size, from, subj, date);
+	else
+		info = sys->sprint("%4d %5d %s %s", msg, size, from, date);
+
+	tk->cmd(hdr, ".f.l insert 0 '"+info);
+	return 1;
+}
+
+mapdown(b: array of byte): string
+{
+	lb := len b;
+	l := array[lb] of byte;
+	for(i := 0; i < lb; i++) {
+		c := b[i];
+		if(c >= byte 'A' && c <= byte 'Z')
+			c += byte('a' - 'A');
+		l[i] = c;
+	}
+	return string l;	
+}
+
+getfield(key: string, text: array of byte): string
+{
+	key[len key] = ':';
+	lk := len key;
+	cl := byte key[0];
+	cu := cl - byte ('a' - 'A');
+
+	lc: byte;
+	for(i := 0; i < len text - lk; i++) {
+		t := text[i];
+		if(t == byte '\n' && lc == byte '\n')		# end header
+			break;
+		lc = t;
+		if(t != cu && t != cl)
+			continue;
+		if(key == mapdown(text[i:i+lk])) {
+			i += lk+1;
+			for(j := i+1; j < len text; j++) {
+				c := text[j];
+				if(c == byte '\r' || c == byte '\n')
+					break;
+			}
+			return string text[i:j];
+		}
+	}
+	return "";
+}
+
+hproc(wmctl: chan of string, top: ref Toplevel)
+{
+	for(;;) {
+		alt {
+		s := <-top.ctxt.kbd =>
+			tk->keyboard(top, s);
+		s := <-top.ctxt.ptr =>
+			tk->pointer(top, *s);
+		s := <-top.ctxt.ctl or
+		s = <-top.wreq or
+		s = <-wmctl =>
+			if(s == "exit")
+				return;
+			tkclient->wmctl(top, s);
+		}
+	}
+}
+
+reply()
+{
+	if(cmesg == 0) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Reply",
+					"No message to reply to",
+					0, "Abort"::nil);
+		return;
+	}
+
+	hdr := tk->cmd(main, ".hdr.t get 1.0 end");
+	if(hdr == "") {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Reply",
+					"Mail has no header to reply to",
+					0, "Abort"::nil);
+		return;
+	}
+
+	wmsender := load WmSendmail "/dis/wm/sendmail.dis";
+	if(wmsender == nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Reply",
+				"Failed to load mail sender:\n"+sys->sprint("%r"),
+				0, "Abort"::nil);
+		return;
+	}
+
+	spawn wmsender->init(ctxt, "sendmail" :: hdr :: nil);
+}
+
+save()
+{
+	if(cmesg == 0) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Save",
+				"No current message",
+				0, "Continue"::nil);
+		return;
+	}
+	pat := list of {
+		"*.let (Saved mail)",
+		"* (All files)"
+	};
+
+	fd: ref Sys->FD;
+	fname: string;
+	for(;;) {
+		fname = selectfile->filename(ctxt, main.image, "Save in Mailbox",
+					pat, "/usr/"+username+"/mail");
+		if(fname == nil)
+			return;
+
+		fd = sys->create(fname, sys->OWRITE, 8r660);
+		if(fd != nil)
+			break;
+
+		labs := list of {
+			"New name",
+			"Abort"
+		};
+
+		r := dialog->prompt(ctxt, main.image, "error -fg red", "Save",
+				"Failed to create "+sys->sprint("%s\n%r", fname),
+				0, labs);
+		if(r == 1)
+			return;
+	}
+	s := tk->cmd(main, ".hdr.t get 1.0 end");
+	b := array of byte s;
+	r := sys->write(fd, b, len b);
+	if(r < 0) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Save",
+				"Error writing file"+sys->sprint("%s\n%r", fname),
+				0, "Continue (not saved)":: nil);
+		return;
+	}
+	s = tk->cmd(main, ".body.t get 1.0 end");
+	b = array of byte s;
+	n := sys->write(fd, b, len b);
+	if(n < 0) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Save",
+				"Error writing file"+sys->sprint("%s\n%r", fname),
+				0, "Continue (not saved)":: nil);
+		return;
+	}
+	status("wrote "+string(n+r)+" bytes.");
+}
+
+delete()
+{
+	if(srv.dfd == nil) {
+		dialog->prompt(ctxt, main.image, "warning -fg yellow", "Delete",
+				"You must be connected to delete messages",
+				0, "Continue"::nil);
+		return;
+	}
+	(err, s) := pop3cmd("DELE "+string cmesg);
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Delete",
+				"Encountered POP3 problem during delete\n"+err,
+				0, "Continue"::nil);
+		return;
+	}
+	map[cmesg] = byte Deleted;
+	status(s);
+}
+
+status(msg: string)
+{
+	tk->cmd(main, ".top.status configure -text {"+msg+"}; update");
+}
+
+disconnect()
+{
+	(err, s) := pop3cmd("QUIT");
+	srv.dfd = nil;
+	tk->cmd(main, ".top.con configure -text Connect");
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Disconnect",
+				"POP3 protocol problem\n"+err,
+				0, "Proceed"::nil);
+		return;
+	}
+	status(s);
+}
+
+connect(parent: ref Toplevel)
+{
+	(t, conctl) := tkclient->toplevel(ctxt, postposn(parent),
+				"Connection Parameters", 0);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	for (c:=0; c<len con_cfg; c++)
+		tk->cmd(t, con_cfg[c]);
+
+	username = rf("/dev/user");
+	sv := rf("/usr/"+username+"/mail/popserver");
+	if(sv != "")
+		tk->cmd(t, ".e.h insert 0 '"+sv);
+
+	u := tk->cmd(t, ".e.u get");
+	if(u == "")
+		tk->cmd(t, ".e.u insert 0 '"+username);
+
+	tk->cmd(t, "update");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-conctl =>
+		if(s == "exit")
+			return;
+		tkclient->wmctl(t, s);
+	s := <-cmd =>
+		if(s == "can")
+			return;
+		server := tk->cmd(t, ".e.h get");
+		if(server == "") {
+			dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+					"You must supply a server address",
+					0, "Proceed"::nil);
+			break;
+		}
+		user := tk->cmd(t, ".e.u get");
+		if(user == "") {
+			dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+					"You must supply a user name",
+					0, "Proceed"::nil);
+			break;
+		}
+		pass := tk->cmd(t, ".e.s get");
+		if(pass == "") {
+			dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+					"You must give a secret or password",
+					0, "Proceed"::nil);
+			break;
+		}
+		if(dialer(t, server, user, pass) != 0)
+			return;
+		status("not connected");
+	}
+	srv.dfd = nil;
+}
+
+initialize()
+{
+	(err, s) := pop3cmd("STAT");
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Mailbox Status",
+				"The following error occurred while "+
+				    "checking your mailbox:\n"+err,
+				0, "Dismiss"::nil);
+		srv.dfd = nil;
+		status("not connected");
+		return;
+	}
+
+	tk->cmd(main, ".top.con configure -text Disconnect; update");
+	nmesg = int s;
+	if(nmesg == 0) {
+		status("There are no messages.");
+		return;
+	}
+
+	map = array[nmesg+1] of byte;
+	for(i := 0; i <= nmesg; i++)
+		map[i] = byte Ok;
+
+	s = "";
+	if(nmesg > 1)
+		s = "s";
+	status("You have "+string nmesg+" message"+s);
+	cmesg = nmesg;
+	loadmesg();
+}
+
+loadmesg()
+{
+	if(srv.dfd == nil) {
+		dialog->prompt(ctxt, main.image, "warning -fg yellow", "Read",
+				"You must be connected to read messages",
+				0, "Continue"::nil);
+		return;
+	}
+	(err, s) := pop3cmd("RETR "+sys->sprint("%d", cmesg));
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Read",
+				"Error retrieving message:\n"+err,
+				0, "Continue"::nil);
+		return;
+	}
+
+	tk->cmd(main, ".hdr.t delete 1.0 end; .body.t delete 1.0 end");
+	size := int s;
+
+	status("reading "+string size+" bytes ...");
+
+	b := pop3body(size);
+
+	(headr, body) := split(string b);
+	b = nil;
+	tk->cmd(main, ".hdr.t insert end '"+headr);
+	tk->cmd(main, ".body.t insert end '"+body);
+	tk->cmd(main, ".hdr.t see 1.0; .body.t see 1.0");
+	status("read message "+string cmesg+" of "+string nmesg+" , ready...");
+}
+
+split(text: string): (string, string)
+{
+	c, lc: int;
+	hdr, body: string;
+
+	hp := 0;
+	for(i := 0; i < len text; i++) {
+		c = text[i];
+		if(c == '\r')
+			continue;
+		hdr[hp++] = c;
+		if(lc == '\n' && c == '\n')
+			break;
+		lc = c;
+	}
+	bp := 0;
+	while(i < len text) {
+		c = text[i++];
+		if(c != '\r')
+			body[bp++] = c;
+	}
+	return (hdr, body);
+}
+
+dialer(t: ref Toplevel, server, user, pass: string): int
+{
+	ok: int;
+
+	for(;;) {
+		status("dialing server...");
+		(ok, srv) = sys->dial(netmkaddr(server, nil, "110"), nil);
+		if(ok >= 0)
+			break;
+
+			labs := list of {
+				"Retry",
+				"Cancel"
+			};
+			ok = dialog->prompt(ctxt, t.image, "error -fg", "Connect",
+					"The following error occurred while\n"+
+					 "dialing the server: "+sys->sprint("%r"),
+					0, labs);
+			if(ok != 0)
+				return 0;
+	}
+	status("connected...");
+	(err, s) := pop3resp();
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				 "An error occurred during sign on.\n"+err,
+				0, "Proceed"::nil);
+		return 0;
+	}
+	status(s);
+	(nil, s) = str->splitl(s, "<");
+	(chal, nil) := str->splitr(s, ">");
+	if(chal != nil){
+		ca := array of byte chal;
+		digest := array[kr->MD5dlen] of byte;
+		md5state := kr->md5(ca, len ca, nil, nil);
+		pa := array of byte pass;
+		kr->md5(pa, len pa, digest, md5state);
+		s = nil;
+		for(i := 0; i < kr->MD5dlen; i++)
+			s  += sys->sprint("%2.2ux", int digest[i]);
+		(err, s) = pop3cmd("APOP "+user+" "+s);
+		if(err == nil) {
+			status("ready to serve...");
+			return 1;
+		} else {
+			dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				 "Challenge/response failed.\n"+err,
+				0, "Proceed"::nil);
+			return 0;
+		}
+	}
+	(err, s) = pop3cmd("USER "+user);
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				 "An error occurred during login.\n"+err,
+				0, "Proceed"::nil);
+		return 0;
+	}
+	(err, s) = pop3cmd("PASS "+pass);
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				 "An error occurred during login.\n"+err,
+				0, "Proceed"::nil);
+		return 0;
+	}
+	status("ready to serve...");
+	return 1;
+}
+
+rf(file: string): string
+{
+	fd := sys->open(file, sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[Sys->NAMEMAX] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n];	
+}
+
+postposn(parent: ref Toplevel): string
+{
+	x := int tk->cmd(parent, ".top.con cget -actx");
+	y := int tk->cmd(parent, ".top.con cget -acty");
+	h := int tk->cmd(parent, ".top.con cget -height");
+
+	return "-x "+string(x-2)+" -y "+string(y+h+2);
+}
+
+#
+# Talk POP3
+#
+pop3cmd(cmd: string): (string, string)
+{
+	cmd += "\r\n";
+#	sys->print("->%s", cmd);
+	b := array of byte cmd;
+	l := len b;
+	n := sys->write(srv.dfd, b, l);
+	if(n != l)
+		return ("send to server:"+sys->sprint("%r"), nil);
+
+	return pop3resp();
+}
+
+pop3resp(): (string, string)
+{
+	s := "";
+	i := 0;
+	lastc := 0;
+	for(;;) {
+		c := pop3getc();
+		if(c == -1)
+			return ("read from server:"+sys->sprint("%r"), nil);
+		if(lastc == '\r' && c == '\n')
+			break;
+		s[i++] = c;
+		lastc = c;
+	}
+#	sys->print("<-%s\n", s);
+	if(i < 3)
+		return ("short read from server", nil);
+	s = s[0:i-1];
+	if(s[0:3] == "+OK") {
+		i = 3;
+		while(i < len s && s[i] == ' ')
+			i++;
+		return (nil, s[i:]);
+	}
+	if(s[0:4] == "-ERR") {
+		i = 4;
+		while(s[i] == ' ' && i < len s)
+			i++;
+		return (s[i:], nil);
+	}
+	return ("invalid server response", nil);
+}
+
+pop3body(size: int): array of byte
+{
+	size += 512;
+	b := array[size] of byte;
+
+	cnt := emptypopbuf(b);
+	size -= cnt;
+
+	for(;;) {
+
+		if(cnt > 5 && string b[cnt-5:cnt] == "\r\n.\r\n") {
+			b = b[0:cnt-5];
+			break;
+		}
+		# resize buffer
+		if(size == 0) {
+			nb := array[len b + 4096] of byte;
+			nb[0:] = b;
+			size = len nb - len b;
+			b = nb;
+			nb = nil;
+		}
+		n := sys->read(srv.dfd, b[cnt:], len b - cnt);
+		if(n <= 0) {
+			dialog->prompt(ctxt, main.image, "error -fg red", "Read",
+				sys->sprint("Error retrieving message: %r"),
+					0, "Continue"::nil);
+			return nil;
+		}
+		size -= n;
+		cnt += n;
+	}
+	return b;
+}
+
+Iob: adt
+{
+	nbyte:	int;
+	posn:	int;
+	buf:	array of byte;
+};
+popbuf: Iob;
+
+pop3getc(): int
+{
+	if(popbuf.nbyte > 0) {
+		popbuf.nbyte--;
+		return int popbuf.buf[popbuf.posn++];
+	}
+	if(popbuf.buf == nil)
+		popbuf.buf = array[512] of byte;
+
+	popbuf.posn = 0;
+	n := sys->read(srv.dfd, popbuf.buf, len popbuf.buf);
+	if(n < 0)
+		return -1;
+
+	popbuf.nbyte = n-1;
+	return int popbuf.buf[popbuf.posn++];
+}
+
+emptypopbuf(a: array of byte) : int
+{
+	i := popbuf.nbyte;
+
+	if (i) {
+		a[0:] = popbuf.buf[popbuf.posn:(popbuf.posn+popbuf.nbyte)];
+		popbuf.nbyte = 0;
+	}
+	
+	return i;
+}
+
+netmkaddr(addr, net, svc: string): string
+{
+	if(net == nil)
+		net = "net";
+	(n, l) := sys->tokenize(addr, "!");
+	if(n <= 1){
+		if(svc== nil)
+			return sys->sprint("%s!%s", net, addr);
+		return sys->sprint("%s!%s!%s", net, addr, svc);
+	}
+	if(svc == nil || n > 2)
+		return addr;
+	return sys->sprint("%s!%s", addr, svc);
+}
--- /dev/null
+++ b/appl/wm/remotelogon.b
@@ -1,0 +1,314 @@
+implement WmLogon;
+#
+# get a certificate to enable remote access.
+#
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Screen, Display, Image, Context, Point, Rect: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "arg.m";
+include "sh.m";
+include "newns.m";
+include "keyring.m";
+	keyring: Keyring;
+include "security.m";
+	login: Login;
+
+# XXX where to put the certificate: is the username already set to
+# something appropriate, with a home directory and keyring directory in that?
+
+# how do we find out the signer; presumably from the registry?
+# should do that before signing on; if we can't get it, then prompt for it.
+WmLogon: module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+cfg := array[] of {
+	"label .p -bitmap @/icons/inferno.bit -borderwidth 2 -relief raised",
+	"label .ul -text {User Name:} -anchor w",
+	"entry .ue -bg white",
+	"label .pl -text {Password:} -anchor w",
+	"entry .pe -bg white -show *",
+	"frame .f -borderwidth 2 -relief raised",
+	"grid .ul .ue -in .f",
+	"grid .pl .pe -in .f",
+	"pack .p .f -fill x",
+	"bind .ue <Key-\n> {focus next}",
+	"bind .ue {<Key-\t>} {focus next}",
+	"bind .pe <Key-\n> {send cmd ok}",
+	"bind .pe {<Key-\t>} {focus next}",
+	"focus .e",
+};
+
+init(ctxt: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if(tkclient == nil){
+		sys->fprint(stderr(), "logon: cannot load %s: %r\n", Tkclient->PATH);
+		raise "fail:bad module";
+	}
+	login = load Login Login->PATH;
+	if(login == nil){
+		sys->fprint(stderr(), "logon: cannot load %s: %r\n", Login->PATH);
+		raise "fail:bad module";
+	}
+	keyring = load Keyring Keyring->PATH;
+	if(keyring == nil){
+		sys->fprint(stderr(), "logon: cannot load %s: %r\n", Keyring->PATH);
+		raise "fail:bad module";
+	}
+	sys->pctl(sys->NEWPGRP, nil);
+	tkclient->init();
+
+	(ctlwin, nil) := tkclient->toplevel(ctxt, nil, nil, Tkclient->Plain);
+	if(sys->fprint(ctlwin.ctxt.connfd, "request") == -1){
+		sys->fprint(stderr(), "logon: must be run as principal wm application\n");
+		raise "fail:lack of control";
+	}
+	addr: con "tcp!127.0.0.1!inflogin";
+	usr := "";
+	passwd := "";
+	arg := load Arg Arg->PATH;
+	if(arg != nil){
+		arg->init(args);
+		arg->setusage("usage: logon [-u user] [-p passwd] command [arg...]]\n");
+		while((opt := arg->opt()) != 0){
+			case opt{
+			'u' =>
+				usr = arg->earg();
+			'p' =>
+				passwd = arg->earg();
+			* =>
+				arg->usage();
+			}
+		}
+		args = arg->argv();
+		arg = nil;
+	} else
+		args = nil;
+	if(ctxt == nil)
+		sys->fprint(stderr(), "logon: must run under a window manager\n");
+
+	if (usr == nil || !logon(ctxt, usr, passwd, addr)) {
+		(panel, cmd) := makepanel(ctxt);
+		stop := chan of int;
+		spawn tkclient->handler(panel, stop);
+		for(;;) {
+			tk->cmd(panel, "focus .ue; update");
+			<-cmd;
+			usr = tk->cmd(panel, ".ue get");
+			if(usr == nil) {
+				notice(ctxt, "You must supply a user name to login");
+				continue;
+			}
+			passwd = tk->cmd(panel, ".pe get");
+
+			if(logon(ctxt, usr, passwd, addr)) {
+				panel = nil;
+				stop <-= 1;
+				break;
+			}
+			tk->cmd(panel, ".ue delete 0 end");
+			tk->cmd(panel, ".pe delete 0 end");
+		}
+	}
+	(ok, nil) := sys->stat("namespace");
+	if(ok >= 0) {
+		ns := load Newns Newns->PATH;
+		if(ns == nil)
+			notice(ctxt, "failed to load namespace builder");
+		else if ((nserr := ns->newns(nil, nil)) != nil)
+			notice(ctxt, "namespace error:\n"+nserr);
+	}
+	tkclient->wmctl(ctlwin, "endcontrol");
+	errch := chan of string;
+	spawn exec(ctxt, args, errch);
+	err := <-errch;
+	if (err != nil) {
+		sys->fprint(stderr(), "logon: %s\n", err);
+		raise "fail:exec failed";
+	}
+}
+
+makepanel(ctxt: ref Draw->Context): (ref Tk->Toplevel, chan of string)
+{
+	(t, nil) := tkclient->toplevel(ctxt, "-bg silver", nil, Tkclient->Plain);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	for(i := 0; i < len cfg; i++)
+		tk->cmd(t, cfg[i]);
+	err := tk->cmd(t, "variable lasterr");
+	if(err != nil) {
+		sys->fprint(stderr(), "logon: tk error: %s\n", err);
+		raise "fail:config error";
+	}
+	tk->cmd(t, "update");
+	org: Point;
+	ir := tk->rect(t, ".", Tk->Border|Tk->Required);
+	org.x = t.screenr.dx() / 2 - ir.dx() / 2;
+	org.y = t.screenr.dy() / 3 - ir.dy() / 2;
+	if (org.y < 0)
+		org.y = 0;
+	tk->cmd(t, ". configure -x " + string org.x + " -y " + string org.y);
+	tkclient->startinput(t, "kbd" :: "ptr" :: nil);
+	tkclient->onscreen(t, "onscreen");
+	return (t, cmd);
+}
+
+exec(ctxt: ref Draw->Context, argv: list of string, errch: chan of string)
+{
+	sys->pctl(sys->NEWFD, 0 :: 1 :: 2 :: nil);
+	if(argv == nil)
+		argv = "/dis/wm/toolbar.dis" :: nil;
+	else {
+		sh := load Sh Sh->PATH;
+		if(sh != nil){
+			sh->run(ctxt, "{$* &}" :: argv);
+			errch <-= nil;
+			exit;
+		}
+	}
+	{
+		cmd := load Command hd argv;
+		if (cmd == nil) {
+			errch <-= sys->sprint("cannot load %s: %r", hd argv);
+		} else {
+			errch <-= nil;
+			spawn cmd->init(ctxt, argv);
+		}
+	}exception{
+	"fail:*" =>
+		exit;
+	}
+}
+
+logon(ctxt: ref Draw->Context, uname, passwd, addr: string): int
+{
+	(err, info) := login->login(uname, passwd, addr);
+	if(err != nil){
+		notice(ctxt, "Login failed:\n" + err);
+		return 0;
+	}
+
+	keys := "/usr/" + user() + "/keyring";
+	if(sys->bind("#s", keys, Sys->MBEFORE) == -1){
+		notice(ctxt, sys->sprint("Cannot access keyring: %r"));
+		return 0;
+	}
+	fio := sys->file2chan(keys, "default");
+	if(fio == nil){
+		notice(ctxt, sys->sprint("Cannot create key file: %r"));
+		return 0;
+	}
+	sync := chan of int;
+	spawn infofile(fio, sync);
+	<-sync;
+
+	if(keyring->writeauthinfo(keys + "/default", info) == -1){
+		notice(ctxt, sys->sprint("Cannot write key file: %r"));
+		return 0;
+	}
+
+	return 1;
+}
+
+notecmd := array[] of {
+	"frame .f",
+	"label .f.l -bitmap error -foreground red",
+	"button .b -text Continue -command {send cmd done}",
+	"focus .f",
+	"bind .f <Key-\n> {send cmd done}",
+	"pack .f.l .f.m -side left -expand 1",
+	"pack .f .b",
+	"pack propagate . 0",
+};
+
+centre(t: ref Tk->Toplevel)
+{
+	sz := Point(int tk->cmd(t, ". cget -width"), int tk->cmd(t, ". cget -height"));
+	r := t.screenr;
+	if (sz.x > r.dx())
+		tk->cmd(t, ". configure -width " + string r.dx());
+	org: Point;
+	org.x = r.dx() / 2 - tk->rect(t, ".", 0).dx() / 2;
+	org.y = r.dy() / 3 - tk->rect(t, ".", 0).dy() / 2;
+	if (org.y < 0)
+		org.y = 0;
+	tk->cmd(t, ". configure -x " + string org.x + " -y " + string org.y);
+}
+
+notice(ctxt: ref Draw->Context, message: string)
+{
+	(t, nil) := tkclient->toplevel(ctxt, "-borderwidth 2 -relief raised", nil, Tkclient->Plain);
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	tk->cmd(t, "label .f.m -anchor nw -text '"+message);
+	for(i := 0; i < len notecmd; i++)
+		tk->cmd(t, notecmd[i]);
+	centre(t);
+	tkclient->onscreen(t, "onscreen");
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+	stop := chan of int;
+	spawn tkclient->handler(t, stop);
+	tk->cmd(t, "update; cursor -default");
+	<-cmd;
+	stop <-= 1;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+user(): string
+{
+	fd := sys->open("/dev/user", Sys->OREAD);
+	buf := array[8192] of byte;
+	if((n := sys->read(fd, buf, len buf)) > 0)
+		return string buf[0:n];
+	return "none";
+}
+
+infofile(fileio: ref Sys->FileIO, sync: chan of int)
+{
+	sys->pctl(Sys->NEWPGRP|Sys->NEWFD|Sys->NEWNS, nil);
+	sync <-= 1;
+
+	infodata: array of byte;
+	for(;;) alt {
+	(off, nbytes, fid, rc) := <-fileio.read =>
+		if(rc == nil)
+			break;
+		if(off > len infodata)
+			off = len infodata;
+		rc <-= (infodata[off:], nil);
+
+	(off, data, fid, wc) := <-fileio.write =>
+		if(wc == nil)
+			break;
+
+		if(off != len infodata){
+			wc <-= (0, "cannot be rewritten");
+		} else {
+			nid := array[len infodata+len data] of byte;
+			nid[0:] = infodata;
+			nid[len infodata:] = data;
+			infodata = nid;
+			wc <-= (len data, nil);
+		}
+	}
+}
--- /dev/null
+++ b/appl/wm/reversi.b
@@ -1,0 +1,903 @@
+implement Reversi;
+
+#
+# Copyright © 2000 Vita Nuova Limited. All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Image, Font, Context, Screen, Display: import draw;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "daytime.m";
+	daytime: Daytime;
+include "rand.m";
+	rand: Rand;
+
+# adtize and modularize
+
+stderr: ref Sys->FD;
+
+Reversi: module 
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+nosleep, printout, auto: int;
+display: ref Draw->Display;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	tkclient->init();
+	daytime = load Daytime Daytime->PATH;
+	rand = load Rand Rand->PATH;
+
+	argv = tl argv;
+	while(argv != nil){
+		s := hd argv;
+		if(s != nil && s[0] == '-'){
+			for(i := 1; i < len s; i++){
+				case s[i]{
+					'a' => auto = 1;
+					'p' => printout = 1;
+					's' => nosleep = 1;
+				}
+			}
+		}
+		argv = tl argv;
+	}
+	stderr = sys->fildes(2);
+	rand->init(daytime->now());
+	daytime = nil;
+
+	if(ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+	display = ctxt.display;
+	(win, wmctl) := tkclient->toplevel(ctxt, "", "Reversi", Tkclient->Resize | Tkclient->Hide);
+	mainwin = win;
+	sys->pctl(Sys->NEWPGRP, nil);
+	cmdch := chan of string;
+	tk->namechan(win, cmdch, "cmd");
+	for(i := 0; i < len win_config; i++)
+		cmd(win, win_config[i]);
+	fittoscreen(win);
+	pid := -1;
+	sync := chan of int;
+	mvch := chan of (int, int, int);
+	initboard();
+	setimage();
+	spawn game(sync, mvch, 0);
+	pid = <- sync;
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	lasts := 1;
+	for(;;){
+		alt{
+			s := <-win.ctxt.kbd =>
+				tk->keyboard(win, s);
+			s := <-win.ctxt.ptr =>
+				tk->pointer(win, *s);
+			s := <-win.ctxt.ctl or
+			s = <-win.wreq =>
+				tkclient->wmctl(win, s);
+			c := <- wmctl =>
+				case c{
+					"exit" =>
+						if(pid != -1)
+							kill(pid);
+						exit;
+					* =>
+						e := tkclient->wmctl(win, c);
+						if(e == nil && c[0] == '!'){
+							setimage();
+							drawboard();
+						}
+				}
+			c := <- cmdch =>
+				(nil, toks) := sys->tokenize(c, " ");
+				case hd toks{
+					"b1" or "b2" or "b3" =>
+						alt{
+							mvch <-= (SQUARE, int hd tl toks, int hd tl tl toks) => lasts = 1;
+							* => ;
+						}
+					"bh" or "bm" or "wh" or "wm" =>
+						col := BLACK;
+						knd := HUMAN;
+						if((hd toks)[0] == 'w')
+							col = WHITE;
+						if((hd toks)[1] == 'm')
+							knd = MACHINE;
+						kind[col] = knd;
+					"blev" or "wlev" =>
+						col := BLACK;
+						e := "be";
+						if((hd toks)[0] == 'w'){
+							col = WHITE;
+							e = "we";
+						}
+						sk := int cmd(win, ".f0." + e + " get");
+						if(sk > MAXPLIES)
+							sk = MAXPLIES;
+						if(sk >= 0)
+							skill[col] = sk;
+					"last" =>
+						alt{
+							mvch <-= (REPLAY, lasts, 0) => lasts++;
+							* => ;
+						}
+					* =>
+						;
+				}
+			<- sync =>
+				pid = -1;
+				# exit;
+				spawn game(sync, mvch, 0);
+				pid = <- sync;
+		}
+	}
+}
+
+SQUARE, REPLAY: con iota;
+
+WIDTH: con 400;
+HEIGHT: con 400;
+
+SZB: con 8;		# must be even
+SZF: con SZB+2;
+MC1: con SZB/2;
+MC2: con MC1+1;
+PIECES: con SZB*SZB;
+SQUARES: con PIECES-4;
+MAXMOVES: con 3*PIECES/2;
+NOMOVE: con SZF*SZF - 1;
+
+BLACK, WHITE, EMPTY, BORDER: con iota;
+MACHINE, HUMAN: con iota;
+SKILLB : con 6;
+SKILLW : con 0;
+MAXPLIES: con 6;
+
+moves: array of int;
+board: array of array of int;	# for display
+brd: array of array of int;		# for calculations
+val: array of array of int;
+order: array of (int, int);
+pieces: array of int;
+value: array of int;
+kind: array of int;
+skill: array of int;
+name: array of string;
+
+mainwin: ref Toplevel;
+brdimg: ref Image;
+brdr: Rect;
+brdx, brdy: int;
+
+black, white, green: ref Image;
+
+movech: chan  of (int, int, int);
+
+setimage()
+{
+	brdw := int tk->cmd(mainwin, ".p cget -actwidth");
+	brdh := int tk->cmd(mainwin, ".p cget -actheight");
+#	if (brdw > display.image.r.dx())
+#		brdw = display.image.r.dx() - 4;
+#	if (brdh > display.image.r.dy())
+#		brdh = display.image.r.dy() - 40;
+
+	brdr = Rect((0,0), (brdw, brdh));
+	brdimg = display.newimage(brdr, display.image.chans, 0, Draw->White);
+	if(brdimg == nil)
+		fatal("not enough image memory");
+	tk->putimage(mainwin, ".p", brdimg, nil);
+}
+
+game(sync: chan of int, mvch: chan of (int, int, int), again: int)
+{
+	sync <-= sys->pctl(0, nil);
+	movech = mvch;
+	initbrd();
+	drawboard();
+	if(again)
+		replay(moves);
+	else
+		play();
+	sync <-= 0;
+}
+
+ordrect()
+{
+	i, j : int;
+
+	n := 0;
+	for(i = 1; i <= SZB; i++){
+		for(j = 1; j <= SZB; j++){
+			if(i < SZB/2 || j < SZB/2 || i > SZB/2+1 || j > SZB/2+1)
+				order[n++] = (i, j);
+		}
+	}
+	for(k := 0; k < SQUARES-1; k++){
+		for(l := k+1; l < SQUARES; l++){
+			(i, j) = order[k];
+			(a, b) := order[l];
+			if(val[i][j] > val[a][b])
+				(order[k], order[l]) = (order[l], order[k]);
+		}
+	}
+}
+
+initboard()
+{
+	i, j, k: int;
+
+	moves = array[MAXMOVES+1] of int;
+	board = array[SZF] of array of int;
+	brd = array[SZF] of array of int;
+	for(i = 0; i < SZF; i++){
+		board[i] = array[SZF] of int;
+		brd[i] = array[SZF] of int;
+	}
+	val = array[SZF] of array of int;
+	s := -pow(-1, SZB/2);
+	for(i = 0; i < SZF; i++){
+		val[i] = array[SZF] of int;
+		val[i][0] = val[i][SZF-1] = 0;
+		for(j = 1; j <= SZB; j++){
+			for(k = SZB/2; k > 0; k--){
+				if(i == k || i == SZB+1-k || j == k || j == SZB+1-k){
+					val[i][j] = s*pow(-7, SZB/2-k);
+					break;
+				}
+			}
+		}
+	}
+	order = array[SQUARES] of (int, int);
+	ordrect();
+	pieces = array[2] of int;
+	value = array[2] of int;
+	kind = array[2] of int;
+	kind[BLACK] = MACHINE;
+	if(auto)
+		kind[WHITE] = MACHINE;
+	else
+		kind[WHITE] = HUMAN;
+	skill = array[2] of int;
+	skill[BLACK] = SKILLB;
+	skill[WHITE] = SKILLW;
+	name = array[2] of string;
+	name[BLACK] = "black";
+	name[WHITE] = "white";
+	black = display.color(Draw->Black);
+	white = display.color(Draw->White);
+	green = display.color(Draw->Green);
+}
+
+initbrd()
+{
+	i, j: int;
+
+	for(i = 0; i < SZF; i++)
+		for(j = 0; j < SZF; j++)
+			brd[i][j] = EMPTY;
+	for(i = 0; i < SZF; i++)
+		brd[i][0] = brd[i][SZF-1] = BORDER;
+	for(j = 0; j< SZF; j++)
+		brd[0][j] = brd[SZF-1][j] = BORDER;
+	brd[MC1][MC1] = brd[MC2][MC2] = BLACK;
+	brd[MC1][MC2] = brd[MC2][MC1] = WHITE;
+	for(i = 0; i < SZF; i++)
+		for(j = 0; j < SZF; j++)
+			board[i][j] = brd[i][j];
+	pieces[BLACK] = pieces[WHITE] = 2;
+	value[BLACK] = value[WHITE] = -2;
+}
+
+plays := 0;
+bscore := 0;
+wscore := 0;
+bwins := 0;
+wwins := 0;
+
+play()
+{
+	n := 0;
+	for(i := 0; i <= MAXMOVES; i++)
+		moves[i] = NOMOVE;
+	if(plays&1)
+		(first, second) := (WHITE, BLACK);
+	else
+		(first, second) = (BLACK, WHITE);
+	if(printout)
+		sys->print("%d\n", first);
+	moves[n++] = first;
+	m1 := m2 := 1;
+	for(;;){
+		if(pieces[BLACK]+pieces[WHITE] == PIECES)
+			break;
+		m2 = m1;
+		m1 = move(first, second);
+		if(printout)
+			sys->print("%d\n", m1);
+		moves[n++] = m1;
+		if(!m1 && !m2)
+			break;
+		(first, second) = (second, first);
+	}
+	if(auto)
+		sys->print("score: %d-%d\n", pieces[BLACK], pieces[WHITE]);
+	bscore += pieces[BLACK];
+	wscore += pieces[WHITE];
+	if(pieces[BLACK] > pieces[WHITE])
+		bwins++;
+	else if(pieces[BLACK] < pieces[WHITE])
+		wwins++;
+	plays++;
+	if(auto)
+		sys->print("	black: %d white: %d draw: %d total: (%d-%d)\n", bwins, wwins, plays-bwins-wwins, bscore, wscore);
+	puts(sys->sprint("black %d:%d white", pieces[BLACK], pieces[WHITE]));
+	sleep(2000);
+	puts(sys->sprint("black %d:%d white", bwins, wwins));
+	sleep(2000);
+}
+
+replay(moves: array of int)
+{
+	n := 0;
+	first := moves[n++];
+	second := BLACK+WHITE-first;
+	m1 := m2 := 1;
+	while (pieces[BLACK]+pieces[WHITE] < PIECES){
+		m2 = m1;
+		m1 = moves[n++];
+		if(m1 == NOMOVE)
+			break;
+		if(m1 != 0)
+			makemove(m1/SZF, m1%SZF, first, second, 1, 0);
+		if(!m1 && !m2)
+			break;
+		(first, second) = (second, first);
+	}
+	# sys->print("score: %d-%d\n", pieces[BLACK], pieces[WHITE]);
+}
+
+lastmoves(p: int, moves: array of int)
+{
+	initbrd();
+	k := MAXMOVES+1;
+	for(i := 0; i <= MAXMOVES; i++){
+		if(moves[i] == NOMOVE){
+			k = i;
+			break;
+		}
+	}
+	if(k-p < 1)
+		p = k-1;
+	for(i = k-p; i < k; i++)
+		if(moves[i] == 0)
+			p++;
+	if(k-p < 1)
+		p = k-1;
+	n := 0;
+	me := moves[n++];
+	you := BLACK+WHITE-me;
+	while(n < k-p){
+		m := moves[n++];
+		if(m != 0)
+			makemove(m/SZF, m%SZF, me, you, 1, 1);
+		(me, you) = (you, me);
+	}
+	for(i = 0; i < SZF; i++)
+		for(j := 0; j < SZF; j++)
+			board[i][j] = brd[i][j];
+	drawboard();
+	sleep(1000);
+	while(n < k){
+		m := moves[n++];
+		if(m != 0)
+			makemove(m/SZF, m%SZF, me, you, 1, 0);
+		if(n < k)
+			sleep(500);
+		(me, you) = (you, me);
+	}
+}
+
+move(me: int, you: int): int
+{
+	if(kind[me] == MACHINE){
+		puts("machine " + name[me] + " move");
+		m := genmove(me, you);
+		if(!m){
+			puts("machine " + name[me] + " cannot go");
+			sleep(2000);
+		}
+		return m;
+	}
+	else{
+		m, n: int;
+
+		mvs := findmoves(me, you);
+		if(mvs == nil){
+			puts("human " + name[me] + " cannot go");
+			sleep(2000);
+			return 0;
+		}
+		for(;;){
+			puts("human " + name[me] + " move");
+			(m, n) = getmove();
+			if(m < 1 || n < 1 || m > SZB || n > SZB)
+				continue;
+			if(brd[m][n] == EMPTY)
+				(valid, nil) := makemove(m, n, me, you, 0, 0);
+			else
+				valid = 0;
+			if(valid)
+				break;
+			puts("illegal move");
+			sleep(2000);
+		}
+		makemove(m, n, me, you, 1, 0);
+		return m*SZF+n;
+	}
+}
+
+fullsrch: int;
+
+genmove(me: int, you: int): int
+{
+	m, n, v: int;
+
+	mvs := findmoves(me, you);
+	if(mvs == nil)
+		return 0;
+	if(skill[me] == 0){
+		l := len mvs;
+		r := rand->rand(l);
+		# r = 0;
+		while(--r >= 0)
+			mvs = tl mvs;
+		(m, n) = hd mvs;
+	}
+	else{
+		plies := skill[me];
+		left := PIECES-(pieces[BLACK]+pieces[WHITE]);
+		if(left < plies)		# limit search
+			plies = left;
+		else if(left < 2*plies)	# expand search to end
+			plies = left;
+		else{				# expand search nearer end of game
+			k := left/plies;
+			if(k < 3)
+				plies = ((k+2)*plies)/(k+1);
+		}
+		fullsrch = plies == left;
+		visits = leaves = 0;
+		(v, (m, n)) = minimax(me, you, plies, ∞, 1);
+		if(0){
+		# if((m==2&&n==2&&brd[1][1]!=BLACK) ||
+		#    (m==2&&n==7&&brd[1][8]!=BLACK) ||
+		#    (m==7&&n==2&&brd[8][1]!=BLACK) ||
+		#    (m==7&&n==7&&brd[8][8]!=BLACK)){
+			while(mvs != nil){
+				(a, b) := hd mvs;
+				(nil, sqs) := makemove(a, b, me, you, 1, 1);
+				(v0, nil) := minimax(you, me, plies-1, ∞, 1);
+				sys->print("	(%d, %d): %d\n", a, b, v0);
+				undomove(a, b, me, you, sqs);
+				mvs = tl mvs;
+			}
+			if(!fullsrch){
+				sys->print("best move is %d, %d\n", m, n);
+				kind[WHITE] = HUMAN;
+			}
+		}
+		if(auto)		
+			sys->print("eval = %d plies=%d goes=%d visits=%d\n", v, plies, len mvs, leaves);
+	}
+	makemove(m, n, me, you, 1, 0);
+	return m*SZF+n;
+}
+
+findmoves(me: int, you: int): list of (int, int)
+{
+	mvs: list of (int, int);
+
+	for(k := 0; k < SQUARES; k++){
+		(i, j) := order[k];
+		if(brd[i][j] == EMPTY){
+			(valid, nil) := makemove(i, j, me, you, 0, 0);
+			if(valid)
+				mvs = (i, j) :: mvs;
+		}
+	}
+	return mvs;
+}
+
+makemove(m: int, n: int, me: int, you: int, move: int, gen: int): (int, list of (int, int))
+{
+	sqs: list of (int, int);
+
+	if(move){
+		pieces[me]++;
+		value[me] += val[m][n];
+		brd[m][n] = me;
+		if(!gen){
+			board[m][n] = me;
+			drawpiece(m, n, me, 1);
+			panelupdate();
+			sleep(1000);
+		}
+	}
+	valid := 0;
+	for(i := -1; i < 2; i++){
+		for(j := -1; j < 2; j++){
+			if(i != 0 || j != 0){
+				v: int;
+
+				(v, sqs) = dirmove(m, n, i, j, me, you, move, gen, sqs);
+				valid |= v;
+				if (valid && !move)
+					return (1, sqs);
+			}
+		}
+	}
+	if(!valid && move)
+		fatal(sys->sprint("bad makemove call (%d, %d)", m, n));
+	return (valid, sqs);
+}
+
+dirmove(m: int, n: int, dx: int, dy: int, me: int, you: int, move: int, gen: int, sqs: list of (int, int)): (int, list of (int, int))
+{
+	p := 0;
+	m += dx;
+	n += dy;
+	while(brd[m][n] == you){
+		m += dx;
+		n += dy;
+		p++;
+	}
+	if(p > 0 && brd[m][n] == me){
+		if(move){
+			pieces[me] += p;
+			pieces[you] -= p;
+			m -= p*dx;
+			n -= p*dy;
+			while(--p >= 0){
+				brd[m][n] = me;
+				value[me] += val[m][n];
+				value[you] -= val[m][n];
+				if(gen)
+					sqs = (m, n) :: sqs;
+				else{
+					board[m][n] = me;
+					drawpiece(m, n, me, 0);
+					# sleep(500);
+					panelupdate();
+				}
+				m += dx;
+				n += dy;
+			}
+		}
+		return (1, sqs);
+	}
+	return (0, sqs);
+}			
+
+undomove(m: int, n: int, me: int, you: int, sqs: list of (int, int))
+{
+	brd[m][n] = EMPTY;
+	pieces[me]--;
+	value[me] -= val[m][n];
+	for(; sqs != nil; sqs = tl sqs){
+		(x, y) := hd sqs;
+		brd[x][y] = you;
+		pieces[me]--;
+		pieces[you]++;
+		value[me] -= val[x][y];
+		value[you] += val[x][y];
+	}
+}
+
+getmove(): (int, int)
+{
+	k, x, y: int;
+
+	(k, x, y) = <- movech;
+	if(k == REPLAY){
+		lastmoves(x, moves);
+		return getmove();
+	}
+	return (x/brdx+1, y/brdy+1);
+}
+
+drawboard()
+{
+	brdx = brdr.dx()/SZB;
+	brdy = brdr.dy()/SZB;
+	brdimg.draw(brdr, green, nil, (0, 0));
+	for(i := 1; i < SZB; i++)
+		drawline(lmap(i, 0), lmap(i, SZB));
+	for(j := 1; j < SZB; j++)
+		drawline(lmap(0, j), lmap(SZB, j));
+	for(i = 1; i <= SZB; i++){
+		for(j = 1; j <= SZB; j++){
+			if (board[i][j] == BLACK || board[i][j] == WHITE)
+				drawpiece(i, j, board[i][j], 0);
+		}
+	}
+	panelupdate();
+}
+
+drawpiece(m, n, p, flash: int)
+{
+	if(p == BLACK)
+		src := black;
+	else
+		src = white;
+	if(0 && flash && kind[p] == MACHINE){
+		for(i := 0; i < 4; i++){
+			brdimg.fillellipse(cmap(m, n), 3*brdx/8, 3*brdy/8, src, (0, 0));
+			panelupdate();
+			sys->sleep(250);
+			brdimg.fillellipse(cmap(m, n), 3*brdx/8, 3*brdy/8, green, (0, 0));
+			panelupdate();
+			sys->sleep(250);
+		}
+	}
+	brdimg.fillellipse(cmap(m, n), 3*brdx/8, 3*brdy/8, src, (0, 0));
+}
+
+panelupdate()
+{
+	tk->cmd(mainwin, sys->sprint(".p dirty %d %d %d %d", brdr.min.x, brdr.min.y, brdr.max.x, brdr.max.y));
+	tk->cmd(mainwin, "update");
+}
+
+drawline(p0, p1: Point)
+{
+	brdimg.line(p0, p1, Draw->Endsquare, Draw->Endsquare, 0, brdimg.display.black, (0, 0));
+}
+
+cmap(m, n: int): Point
+{
+	return brdr.min.add((m*brdx-brdx/2, n*brdy-brdy/2));
+}
+
+lmap(m, n: int): Point
+{
+	return brdr.min.add((m*brdx, n*brdy));
+}
+
+∞: con (1<<30);
+MAXVISITS: con 1024;
+
+visits, leaves : int;
+
+minimax(me: int, you: int, plies: int, αβ: int, mv: int): (int, (int, int))
+{
+	if(plies == 0){
+		visits++;
+		leaves++;
+		if(visits == MAXVISITS){
+			visits = 0;
+			sys->sleep(0);
+		}
+		return (eval(me, you), (0, 0));
+	}
+	mvs := findmoves(me, you);
+	if(mvs == nil){
+		if(mv)
+			(v, nil) := minimax(you, me, plies, ∞, 0);
+		else
+			(v, nil) = minimax(you, me, plies-1, ∞, 0);
+		return (-v, (0, 0));
+	}
+	bestv := -∞;
+	bestm := (0, 0);
+	e := 0;
+	for(; mvs != nil; mvs = tl mvs){
+		(m, n) := hd mvs;
+		(nil, sqs) := makemove(m, n, me, you, 1, 1);
+		(v, nil) := minimax(you, me, plies-1, -bestv, 1);
+		v = -v;
+		undomove(m, n, me, you, sqs);
+		if(v > bestv || (v == bestv && rand->rand(++e) == 0)){
+			if(v > bestv)
+				e = 1;
+			bestv = v;
+			bestm = (m, n);
+			if(bestv >= αβ)
+				return (∞, (0, 0));
+		}
+	}
+	return (bestv, bestm);
+}
+	
+eval(me: int, you: int): int
+{
+	d := pieces[me]-pieces[you];
+	if(fullsrch)
+		return d;
+	n := pieces[me]+pieces[you];
+	v := 0;
+	for(i := 1; i <= SZB; i += SZB-1)
+		for(j := 1; j <= SZB; j += SZB-1)
+			v += line(i, j, me, you);
+	return (PIECES-n)*(value[me]-value[you]+v) + n*d;
+}
+
+line(m: int, n: int, me: int, you: int): int
+{
+	if(brd[m][n] == EMPTY)
+		return 0;
+	dx := dy := -1;
+	if(m == 1)
+		dx = 1;
+	if(n == 1)
+		dy = 1;
+	return line0(m, n, 0, dy, me, you) +
+		   line0(m, n, dx, 0, me, you) +
+		   line0(m, n, dx, dy, me, you);
+}
+
+line0(m: int, n: int, dx: int, dy: int, me: int, you: int): int
+{
+	v := 0;
+	p := brd[m][n];
+	i := val[1][1];
+	while(brd[m][n] == p){
+		v += i;
+		m += dx;
+		n += dy;
+	}
+	if(p == you)
+		return -v;
+	if(p == me)
+		return v;
+	return v;
+}
+
+pow(n: int, m: int): int
+{
+	p := 1;
+	while(--m >= 0)
+		p *= n;
+	return p;
+}
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "%s\n", s);
+	exit;
+}
+
+sleep(t: int)
+{
+	if(nosleep)
+		sys->sleep(0);
+	else
+		sys->sleep(t);
+}
+
+kill(pid: int): int
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil)
+		return -1;
+	if(sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+cmd(top: ref Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr, "reversi: tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+# swidth: int;
+# sfont: ref Font;
+
+# gettxtattrs()
+# {
+#	swidth = int cmd(mainwin, ".f1.txt cget -width");	# always initial value ?
+#	f := cmd(mainwin, ".f1.txt cget -font");
+#	sfont = Font.open(brdimg.display, f);
+# }
+	
+puts(s: string)
+{
+	# while(sfont.width(s) > swidth)
+	#	s = s[0: len s -1];
+	cmd(mainwin, ".f1.txt configure -text {" + s + "}");
+	cmd(mainwin, "update");
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+	Point: import draw;
+	if (display.image == nil)
+		return;
+	r :=  display.image.r;
+	scrsize := Point(r.dx(), r.dy());
+	bd := int cmd(win, ". cget -bd");
+	winsize := Point(int cmd(win, ". cget -actwidth") + bd * 2, int cmd(win, ". cget -actheight") + bd * 2);
+	if (winsize.x > scrsize.x)
+		cmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+	if (winsize.y > scrsize.y)
+		cmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+	actr: Rect;
+	actr.min = Point(int cmd(win, ". cget -actx"), int cmd(win, ". cget -acty"));
+	actr.max = actr.min.add((int cmd(win, ". cget -actwidth") + bd*2,
+				int cmd(win, ". cget -actheight") + bd*2));
+	(dx, dy) := (actr.dx(), actr.dy());
+	if (actr.max.x > r.max.x)
+		(actr.min.x, actr.max.x) = (r.max.x - dx, r.max.x);
+	if (actr.max.y > r.max.y)
+		(actr.min.y, actr.max.y) = (r.max.y - dy, r.max.y);
+	if (actr.min.x < r.min.x)
+		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+	if (actr.min.y < r.min.y)
+		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+	cmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+	cmd(win, "update");
+}
+					
+win_config := array[] of {
+	"frame .f",
+	"button .f.last -text {last move} -command {send cmd last}",
+	"menubutton .f.bk -text Black -menu .f.bk.bm",
+	"menubutton .f.wk -text White -menu .f.wk.wm",
+	"menu .f.bk.bm",
+	".f.bk.bm add command -label Human -command { send cmd bh }",
+	".f.bk.bm add command -label Machine -command { send cmd bm }",
+	"menu .f.wk.wm",
+	".f.wk.wm add command -label Human -command { send cmd wh }",
+	".f.wk.wm add command -label Machine -command { send cmd wm }",
+	"pack .f.bk -side left",
+	"pack .f.wk -side right",
+	"pack .f.last -side top",
+
+	"frame .f0",
+	"label .f0.bl -text {Black level}",
+	"label .f0.wl -text {White level}",
+	"entry .f0.be -width 32",
+	"entry .f0.we -width 32",
+	".f0.be insert 0 " + string SKILLB,
+	".f0.we insert 0 " + string SKILLW,
+	"pack .f0.bl -side left",
+	"pack .f0.be -side left",
+	"pack .f0.wl -side right",
+	"pack .f0.we -side right",
+
+	"frame .f1",
+	"label .f1.txt -text { } -width " + string WIDTH,
+	"pack .f1.txt -side top -fill x",
+
+	"panel .p -width " + string WIDTH + " -height " + string HEIGHT,
+
+	"pack .f -side top -fill x",
+	"pack .f0 -side top -fill x",
+	"pack .f1 -side top -fill x",
+	"pack .p -side bottom -fill both -expand 1",
+	"pack propagate . 0",
+
+	"bind .p <Button-1> {send cmd b1 %x %y}",
+	"bind .p <Button-2> {send cmd b2 %x %y}",
+	"bind .p <Button-3> {send cmd b3 %x %y}",
+	"bind .f0.be <Key-\n> {send cmd blev}",
+	"bind .f0.we <Key-\n> {send cmd wlev}",
+	"update",
+};
--- /dev/null
+++ b/appl/wm/rmtdir.b
@@ -1,0 +1,215 @@
+implement WmRmtdir;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "keyring.m";
+include "security.m";
+
+t: ref Toplevel;
+
+WmRmtdir: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Wm: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+rmt_config := array[] of {
+	"frame .f",
+	"label .f.l -text Address:",
+	"entry .f.e",
+	"pack .f.l .f.e -side left",
+	"label .status -text {Enter net!machine ...} -anchor w",
+	"pack .Wm_t .status .f -fill x",
+	"bind .f.e <Key-\n> {send cmd dial}",
+	"frame .b",
+	"radiobutton .b.none -variable alg -value none -anchor w -text '"+
+			"Authentication without SSL",
+	"radiobutton .b.clear -variable alg -value clear -anchor w -text '"+
+			"Authentication with SSL clear",
+	"radiobutton .b.sha -variable alg -value sha  -anchor w -text '"+
+			"Authentication with SHA hash",
+	"radiobutton .b.md5 -variable alg -value md5  -anchor w -text '"+
+			"Authentication with MD5 hash",
+	"radiobutton .b.rc4 -variable alg -value rc4 -anchor w -text '"+
+			"Authentication with RC4 encryption",
+	"radiobutton .b.sharc4 -variable alg -value sha/rc4 -anchor w -text '"+
+			"Authentication with SHA and RC4",
+	"radiobutton .b.md5rc4 -variable alg -value md5/rc4 -anchor w -text '"+
+			"Authentication with MD5 and RC4",
+	"pack .b.none .b.clear .b.sha .b.md5 .b.rc4 .b.sharc4 .b.md5rc4 -fill x",
+	"pack .b -fill x",
+	".b.none invoke",
+	"update",
+};
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	menubut : chan of string;
+
+	sys  = load Sys  Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "rmtdir: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk   = load Tk   Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+
+	tkclient->init();
+
+	(t, menubut) = tkclient->toplevel(ctxt, "", sysname()+": Remote Connection", 0);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	for (i:=0; i<len rmt_config; i++)
+		tk->cmd(t, rmt_config[i]);
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-menubut =>
+		tkclient->wmctl(t, s);
+	<-cmd =>
+		addr := tk->cmd(t, ".f.e get");
+		status("Dialing");
+		(ok, c) := sys->dial(netmkaddr(addr, "tcp", "styx"), nil);
+		if(ok < 0) {
+			tk->cmd(t, ".status configure -text {Failed: "+
+					sys->sprint("%r")+"}; update");
+			break;
+		}
+		status("Authenticate");
+		alg := tk->cmd(t, "variable alg");
+
+		kr := load Keyring Keyring->PATH;
+		if(kr == nil){
+			tk->cmd(t, ".status configure -text {Error: can't load module Keyring "+
+					sys->sprint("%r")+"}; update");
+			break;
+		}
+
+		user := user();
+		kd := "/usr/" + user + "/keyring/";
+		cert := kd + netmkaddr(addr, "tcp", "");
+		(ok, nil) = sys->stat(cert);
+		if(ok < 0)
+			cert = kd + "default";
+
+		ai := kr->readauthinfo(cert);
+		if(ai == nil){
+			tk->cmd(t, ".status configure -text {Error: certificate for "+
+					sys->sprint("%s",addr)+" not found}; update");
+			wmgetauthinfo := load Wm "/dis/wm/wmgetauthinfo.dis";
+			if(wmgetauthinfo == nil){
+				tk->cmd(t, ".status configure -text {Error: can't load module wmgetauthinfo.dis}; update");
+				exit;
+			}
+			spawn wmgetauthinfo->init(ctxt, nil); 
+			break;
+		}
+
+		au := load Auth Auth->PATH;
+		if(au == nil){
+			tk->cmd(t, ".status configure -text {Error: can't load module Auth "+
+					sys->sprint("%r")+"; update");
+			break;
+		}
+
+		err := au->init();
+		if(err != nil){
+			tk->cmd(t, ".status configure -text {Error: "+
+					sys->sprint("%s", err)+"; update");
+			break;
+		}
+
+		fd: ref Sys->FD;
+		(fd, err) = au->client(alg, ai, c.dfd);
+		if(fd == nil){
+			tk->cmd(t, ".status configure -text {Error: authentication failed: "+
+					sys->sprint("%s",err)+"; update");
+			break;
+		}
+
+		status("Mount");
+		sys->pctl(sys->FORKNS, nil);	# don't fork before authentication
+		n := sys->mount(fd, nil, "/n/remote", sys->MREPL, "");
+		if(n < 0) {
+			tk->cmd(t, ".status configure -text {Mount failed: "+
+					sys->sprint("%r")+"}; update");
+			break;
+		}
+		wmdir := load Wm "/dis/wm/dir.dis";
+		spawn wmdir->init(ctxt, "wm/dir" :: "/n/remote" :: nil);
+		return;
+	}
+}
+
+status(s: string)
+{
+	tk->cmd(t, ".status configure -text {"+s+"}; update");
+}
+
+sysname(): string
+{
+	fd := sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return "Anon";
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return "Anon";
+	return string buf[0:n];
+}
+
+user(): string
+{
+	sys = load Sys Sys->PATH;
+
+	fd := sys->open("/dev/user", sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n];	
+}
+
+netmkaddr(addr, net, svc: string): string
+{
+	if(net == nil)
+		net = "net";
+	(n, l) := sys->tokenize(addr, "!");
+	if(n <= 1){
+		if(svc== nil)
+			return sys->sprint("%s!%s", net, addr);
+		return sys->sprint("%s!%s!%s", net, addr, svc);
+	}
+	if(svc == nil || n > 2)
+		return addr;
+	return sys->sprint("%s!%s", addr, svc);
+}
--- /dev/null
+++ b/appl/wm/rt.b
@@ -1,0 +1,701 @@
+implement WmRt;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "draw.m";
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include "dis.m";
+	dis: Dis;
+	Inst, Type, Data, Link, Mod: import dis;
+	XMAGIC: import Dis;
+	MUSTCOMPILE, DONTCOMPILE: import Dis;
+	AMP, AFP, AIMM, AXXX, AIND, AMASK: import Dis;
+	ARM, AXNON, AXIMM, AXINF, AXINM: import Dis;
+	DEFB, DEFW, DEFS, DEFF, DEFA, DIND, DAPOP, DEFL: import Dis;
+
+WmRt: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+gctxt: ref Draw->Context;
+t: ref Toplevel;
+disfile: string;
+
+TK:	con 1;
+
+m: ref Mod;
+rt := 0;
+ss := -1;
+
+rt_cfg := array[] of {
+	"frame .m",
+	"menubutton .m.open -text File -menu .file",
+	"menubutton .m.prop -text Properties -menu .prop",
+	"menubutton .m.view -text View -menu .view",
+	"label .m.l",
+	"pack .m.open .m.view .m.prop -side left",
+	"pack .m.l -side right",
+	"frame .b",
+	"text .b.t -width 12c -height 7c -yscrollcommand {.b.s set} -bg white",
+	"scrollbar .b.s -command {.b.t yview}",
+	"pack .b.s -fill y -side left",
+	"pack .b.t -fill both -expand 1",
+	"pack .m -anchor w -fill x",
+	"pack .b -fill both -expand 1",
+	"pack propagate . 0",
+	"update",
+
+	"menu .prop",
+	".prop add checkbutton -text {Must compile} -command {send cmd must}",
+	".prop add checkbutton -text {Don't compile} -command {send cmd dont}",
+	".prop add separator",
+	".prop add command -text {Set stack extent} -command {send cmd stack}",
+	".prop add command -text {Sign module} -command {send cmd sign}",
+
+	"menu .view",
+	".view add command -text {Header} -command {send cmd hdr}",
+	".view add command -text {Code segment} -command {send cmd code}",
+	".view add command -text {Data segment} -command {send cmd data}",
+	".view add command -text {Type descriptors} -command {send cmd type}",
+	".view add command -text {Link descriptors} -command {send cmd link}",
+	".view add command -text {Import descriptors} -command {send cmd imports}",
+	".view add command -text {Exception handlers} -command {send cmd handlers}",
+
+	"menu .file",
+	".file add command -text {Open module} -command {send cmd open}",
+	".file add separator",
+	".file add command -text {Write .dis module} -command {send cmd save}",
+	".file add command -text {Write .s file} -command {send cmd list}",
+};
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "rt: no window context\n");
+		raise "fail:bad context";
+	}
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	tkclient->init();
+	dialog->init();
+	selectfile->init();
+
+	gctxt = ctxt;
+
+	menubut: chan of string;
+	(t, menubut) = tkclient->toplevel(ctxt, "", "Dis Module Manager", Tkclient->Appl);
+
+	cmd := chan of string;
+
+	tk->namechan(t, cmd, "cmd");
+	tkcmds(t, rt_cfg);
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	dis = load Dis Dis->PATH;
+	if(dis == nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Load Module",
+				"wmrt requires Dis",
+				0, "Exit"::nil);
+		return;
+	}
+	dis->init();
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq =>
+		tkclient->wmctl(t, s);
+	menu := <-menubut =>
+		if(menu == "exit")
+			return;
+		tkclient->wmctl(t, menu);
+	s := <-cmd =>
+		case s {
+		"open" =>
+			openfile(ctxt);
+		"save" =>
+			writedis();
+		"list" =>
+			writeasm();
+		"hdr" =>
+			hdr();
+		"code" =>
+			das(TK);
+		"data" =>
+			dat(TK);
+		"type" =>
+			desc(TK);
+		"link" =>
+			link(TK);
+		"imports" =>
+			imports(TK);
+		"handlers" =>
+			handlers(TK);
+		"must" =>
+			rt ^= MUSTCOMPILE;
+		"dont" =>
+			rt ^= DONTCOMPILE;
+		"stack" =>
+			spawn stack(ctxt);
+		"sign" =>
+			dialog->prompt(ctxt, t.image, "error -fg red", "Signed Modules",
+				"not implemented",
+				0, "Continue"::nil);
+		}
+	}
+}
+
+stack_cfg := array[] of {
+	"scale .s -length 200 -to 32768 -resolution 128 -orient horizontal",
+	"frame .f",
+	"pack .s .f -pady 5 -fill x -expand 1",
+};
+
+stack(ctxt: ref Draw->Context)
+{
+	# (s, sbut) := tkclient->toplevel(ctxt, tkclient->geom(t), "Dis Stack", 0);
+	(s, sbut) := tkclient->toplevel(ctxt, "", "Dis Stack", 0);
+
+	cmd := chan of string;
+	tk->namechan(s, cmd, "cmd");
+	tkcmds(s, stack_cfg);
+	tk->cmd(s, ".s set " + string ss);
+	tk->cmd(s, "update");
+	tkclient->onscreen(s, nil);
+	tkclient->startinput(s, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+	c := <-s.ctxt.kbd =>
+		tk->keyboard(s, c);
+	c := <-s.ctxt.ptr =>
+		tk->pointer(s, *c);
+	c := <-s.ctxt.ctl or
+	c = <-s.wreq =>
+		tkclient->wmctl(s, c);
+	wmctl := <-sbut =>
+		if(wmctl == "exit") {
+			ss = int tk->cmd(s, ".s get");
+			return;
+		}
+		tkclient->wmctl(s, wmctl);
+	}	
+}
+
+openfile(ctxt: ref Draw->Context)
+{
+	pattern := list of {
+		"*.dis (Dis VM module)",
+		"* (All files)"
+	};
+
+	for(;;) {
+		disfile = selectfile->filename(ctxt, t.image, "Dis file", pattern, nil);
+		if(disfile == "")
+			break;
+
+		s: string;
+		(m, s) = dis->loadobj(disfile);
+		if(s == nil) {
+			ss = m.ssize;
+			rt = m.rt;
+			tk->cmd(t, ".m.l configure -text {"+m.name+"}");
+			das(TK);
+			return;
+		}
+
+		r := dialog->prompt(ctxt, t.image, "error -fg red", "Open Dis File",
+				s,
+				0, "Retry" :: "Abort" :: nil);
+		if(r == 1)
+			return;
+	}
+}
+
+writedis()
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Write .dis",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+	if(rt < 0)
+		rt = m.rt;
+	if(ss < 0)
+		ss = m.ssize;
+	if(rt == m.rt && ss == m.ssize)
+		return;
+	while((fd := sys->open(disfile, Sys->OREAD)) == nil){
+		if(dialog->prompt(gctxt, t.image, "error -fg red", "Open Dis File", "open failed: "+sprint("%r"),
+		     0, "Retry" :: "Abort" :: nil))
+			return;
+	}
+	if(len discona(rt) == len discona(m.rt) && len discona(ss) == len discona(m.ssize)){
+		sys->seek(fd, big 4, Sys->SEEKSTART);	# skip magic
+		discon(fd, rt);
+		discon(fd, ss);
+		m.rt = rt;
+		m.ssize = ss;
+		return;
+	}
+	# rt and ss representations changed in length: read the file in,
+	# make a copy and update rt and ss when copying
+	(ok, d) := sys->fstat(fd);
+	if(ok < 0){
+		ioerror("Reading Dis file "+disfile, "can't find file length: "+sprint("%r"));
+		return;
+	}
+	length := int d.length;
+	disbuf := array[length] of byte;
+	if(sys->read(fd, disbuf, length) != length){
+		ioerror("Reading Dis file "+disfile, "read error: "+sprint("%r"));
+		return;
+	}
+	outbuf := array[length+2*4] of byte;	# could avoid this buffer if required, by writing portions of disbuf
+	(magic, i) := operand(disbuf, 0);
+	o := putoperand(outbuf, magic);
+	if(magic == Dis->SMAGIC){
+		ns: int;
+		(ns, i) = operand(disbuf, i);
+		o += putoperand(outbuf[o:], ns);
+		sign := disbuf[i:i+ns];
+		i += ns;
+		outbuf[o:] = sign;
+		o += ns;
+	}
+	(nil, i) = operand(disbuf, i);
+	(nil, i) = operand(disbuf, i);
+	if(i < 0){
+		ioerror("Reading Dis file "+disfile, "Dis header too short");
+		return;
+	}
+	o += putoperand(outbuf[o:], rt);
+	o += putoperand(outbuf[o:], ss);
+	outbuf[o:] = disbuf[i:];
+	o += len disbuf - i;
+	fd = sys->create(disfile, Sys->OWRITE, 8r666);
+	if(fd == nil){
+		ioerror("Rewriting "+disfile, sys->sprint("can't create %s: %r",disfile));
+		return;
+	}
+	if(sys->write(fd, outbuf, o) != o)
+		ioerror("Rewriting "+disfile, "write error: "+sprint("%r"));
+	m.rt = rt;
+	m.ssize = ss;
+}
+
+ioerror(title: string, err: string)
+{
+	dialog->prompt(gctxt, t.image, "error -fg red", title, err, 0, "Dismiss" :: nil);
+}
+
+putoperand(out: array of byte, v: int): int
+{
+	a := discona(v);
+	out[0:] = a;
+	return len a;
+}
+
+discona(val: int): array of byte
+{
+	if(val >= -64 && val <= 63)
+		return array[] of { byte(val & ~16r80) };
+	else if(val >= -8192 && val <= 8191)
+		return array[] of { byte((val>>8) & ~16rC0 | 16r80), byte val };
+	else
+		return array[] of { byte(val>>24 | 16rC0), byte(val>>16), byte(val>>8), byte val };
+}
+
+discon(fd: ref Sys->FD, val: int)
+{
+	a := discona(val);
+	sys->write(fd, a, len a);
+}
+
+operand(disobj: array of byte, o: int): (int, int)
+{
+	if(o >= len disobj)
+		return (-1, -1);
+	b := int disobj[o++];
+	case b & 16rC0 {
+	16r00 =>
+		return (b, o);
+	16r40 =>
+		return (b | ~16r7F, o);
+	16r80 =>
+		if(o >= len disobj)
+			return (-1, -1);
+		if(b & 16r20)
+			b |= ~16r3F;
+		else
+			b &= 16r3F;
+		b = (b<<8) | int disobj[o++];
+		return (b, o);
+	16rC0 =>
+		if(o+2 >= len disobj)
+			return (-1, -1);
+		if(b & 16r20)
+			b |= ~16r3F;
+		else
+			b &= 16r3F;
+		b = b<<24 |
+			(int disobj[o]<<16) |
+		    	(int disobj[o+1]<<8)|
+		    	int disobj[o+2];
+		o += 3;
+		return (b, o);
+	}
+	return (0, -1);	# can't happen
+}
+
+fasm: ref Iobuf;
+
+writeasm()
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Write .s",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Write .s",
+				"Bufio load failed: "+sprint("%r"),
+				0, "Exit"::nil);
+		return;
+	}
+
+	for(;;) {
+		asmfile: string;
+		if(len disfile > 4 && disfile[len disfile-4:] == ".dis")
+			asmfile = disfile[0:len disfile-3] + "s";
+		else
+			asmfile = disfile + ".s";
+		fasm = bufio->create(asmfile, Sys->OWRITE|Sys->OTRUNC, 8r666);
+		if(fasm != nil)
+			break;
+		r := dialog->prompt(gctxt, t.image, "error -fg red", "Create .s file",
+			"open failed: "+sprint("%r"),
+			0, "Retry" :: "Abort" :: nil);
+		if(r == 0)
+			continue;
+		else
+			return;
+	}
+	das(!TK);
+	fasm.puts("\tentry\t" + string m.entry + "," + string m.entryt + "\n");
+	desc(!TK);
+	dat(!TK);
+	fasm.puts("\tmodule\t" + m.name + "\n");
+	link(!TK);
+	imports(!TK);
+	handlers(!TK);
+	fasm.close();
+}
+
+link(flag: int)
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Link Descriptors",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+
+	if(flag == TK)
+		tk->cmd(t, ".b.t delete 1.0 end");
+
+	for(i := 0; i < m.lsize; i++) {
+		l := m.links[i];
+		s := sprint("	link %d,%d, 0x%ux, \"%s\"\n",
+					l.desc, l.pc, l.sig, l.name);
+		if(flag == TK)
+			tk->cmd(t, ".b.t insert end '"+s);
+		else
+			fasm.puts(s);
+	}
+	if(flag == TK)
+		tk->cmd(t, ".b.t see 1.0; update");
+}
+
+imports(flag: int)
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Import Descriptors",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+
+	if(flag == TK)
+		tk->cmd(t, ".b.t delete 1.0 end");
+
+	mi := m.imports;
+	for(i := 0; i < len mi; i++) {
+		a := mi[i];
+		for(j := 0; j < len a; j++) {
+			ai := a[j];
+			s := sprint("	import 0x%ux, \"%s\"\n", ai.sig, ai.name);
+			if(flag == TK)
+				tk->cmd(t, ".b.t insert end '"+s);
+			else
+				fasm.puts(s);
+		}
+	}
+	if(flag == TK)
+		tk->cmd(t, ".b.t see 1.0; update");
+}
+
+handlers(flag: int)
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Exception Handlers",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+
+	if(flag == TK)
+		tk->cmd(t, ".b.t delete 1.0 end");
+
+	hs := m.handlers;
+	for(i := 0; i < len hs; i++) {
+		h := hs[i];
+		tt := -1;
+		for(j := 0; j < len m.types; j++) {
+			if(h.t == m.types[j]) {
+				tt = j;
+				break;
+			}
+		}
+		s := sprint("	%d-%d, o=%d, e=%d t=%d\n", h.pc1, h.pc2, h.eoff, h.ne, tt);
+		if(flag == TK)
+			tk->cmd(t, ".b.t insert end '"+s);
+		else
+			fasm.puts(s);
+		et := h.etab;
+		for(j = 0; j < len et; j++) {
+			e := et[j];
+			if(e.s == nil)
+				s = sprint("		%d	*\n", e.pc);
+			else
+				s = sprint("		%d	\"%s\"\n", e.pc, e.s);
+			if(flag == TK)
+				tk->cmd(t, ".b.t insert end '"+s);
+			else
+				fasm.puts(s);
+		}
+	}
+	if(flag == TK)
+		tk->cmd(t, ".b.t see 1.0; update");
+}
+
+desc(flag: int)
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Type Descriptors",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+
+	if(flag == TK)
+		tk->cmd(t, ".b.t delete 1.0 end");
+
+	for(i := 0; i < m.tsize; i++) {
+		h := m.types[i];
+		s := sprint("	desc $%d, %d, \"", i, h.size);
+		for(j := 0; j < h.np; j++)
+			s += sprint("%.2ux", int h.map[j]);
+		s += "\"\n";
+		if(flag == TK)
+			tk->cmd(t, ".b.t insert end '"+s);
+		else
+			fasm.puts(s);
+	}
+	if(flag == TK)
+		tk->cmd(t, ".b.t see 1.0; update");
+}
+
+hdr()
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Header",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+
+	tk->cmd(t, ".b.t delete 1.0 end");
+
+	s := sprint("%.8ux Version %d Dis VM\n", m.magic, m.magic - XMAGIC + 1);
+	s += sprint("%.8ux Runtime flags %s\n", m.rt, rtflag(m.rt));
+	s += sprint("%8d bytes per stack extent\n\n", m.ssize);
+
+
+	s += sprint("%8d instructions\n", m.isize);
+	s += sprint("%8d data size\n", m.dsize);
+	s += sprint("%8d heap type descriptors\n", m.tsize);
+	s += sprint("%8d link directives\n", m.lsize);
+	s += sprint("%8d entry pc\n", m.entry);
+	s += sprint("%8d entry type descriptor\n\n", m.entryt);
+
+	if(m.sign == nil)
+		s += "Module is Insecure\n";
+
+	tk->cmd(t, ".b.t insert end '"+s);
+	tk->cmd(t, ".b.t see 1.0; update");
+}
+
+rtflag(flag: int): string
+{
+	if(flag == 0)
+		return "";
+
+	s := "[";
+
+	if(flag & MUSTCOMPILE)
+		s += "MustCompile";
+	if(flag & DONTCOMPILE) {
+		if(flag & MUSTCOMPILE)
+			s += "|";
+		s += "DontCompile";
+	}
+	s[len s] = ']';
+
+	return s;
+}
+
+das(flag: int)
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Assembly",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+
+	if(flag == TK)
+		tk->cmd(t, ".b.t delete 1.0 end");
+
+	for(i := 0; i < m.isize; i++) {
+		prefix := "";
+		if(flag == TK)
+			prefix = sprint(".b.t insert end '%4d   ", i);
+		else {
+			if(i % 10 == 0)
+				fasm.puts("#" + string i + "\n");
+			prefix = sprint("\t");
+		}
+		s := prefix + dis->inst2s(m.inst[i]) + "\n";
+
+		if(flag == TK)
+			tk->cmd(t, s);
+		else
+			fasm.puts(s);
+	}
+	if(flag == TK)
+		tk->cmd(t, ".b.t see 1.0; update");
+}
+
+dat(flag: int)
+{
+	if(m == nil || m.magic == 0) {
+		dialog->prompt(gctxt, t.image, "error -fg red", "Module Data",
+				"no module loaded",
+				0, "Continue"::nil);
+		return;
+	}
+	s := sprint("	var @mp, %d\n", m.types[0].size);
+	if(flag == TK) {
+		tk->cmd(t, ".b.t delete 1.0 end");
+		tk->cmd(t, ".b.t insert end '"+s);
+	} else
+		fasm.puts(s);
+
+	s = "";
+	for(d := m.data; d != nil; d = tl d) {
+		pick dat := hd d {
+		Bytes =>
+			s = sprint("\tbyte @mp+%d", dat.off);
+			for(n := 0; n < dat.n; n++)
+				s += sprint(",%d", int dat.bytes[n]);
+		Words =>
+			s = sprint("\tword @mp+%d", dat.off);
+			for(n := 0; n < dat.n; n++)
+				s += sprint(",%d", dat.words[n]);
+		String =>
+			s = sprint("\tstring @mp+%d, \"%s\"", dat.off, mapstr(dat.str));
+		Reals =>
+			s = sprint("\treal @mp+%d", dat.off);
+			for(n := 0; n < dat.n; n++)
+				s += sprint(", %g", dat.reals[n]);
+			break;
+		Array =>
+			s = sprint("\tarray @mp+%d,$%d,%d", dat.off, dat.typex, dat.length);
+		Aindex =>
+			s = sprint("\tindir @mp+%d,%d", dat.off, dat.index);
+		Arestore =>
+			s = "\tapop";
+			break;
+		Bigs =>
+			s = sprint("\tlong @mp+%d", dat.off);
+			for(n := 0; n < dat.n; n++)
+				s += sprint(", %bd", dat.bigs[n]);
+		}
+		if(flag == TK)
+			tk->cmd(t, ".b.t insert end '"+s+"\n");
+		else
+			fasm.puts(s+"\n");
+	}
+
+	if(flag == TK)
+		tk->cmd(t, ".b.t see 1.0; update");
+}
+
+mapstr(s: string): string
+{
+	for(i := 0; i < len s; i++) {
+		if(s[i] == '\n')
+			s = s[0:i] + "\\n" + s[i+1:];
+	}
+	return s;
+}
+
+tkcmds(t: ref Toplevel, cfg: array of string)
+{
+	for(i := 0; i < len cfg; i++)
+		tk->cmd(t, cfg[i]);
+}
--- /dev/null
+++ b/appl/wm/sam.b
@@ -1,0 +1,230 @@
+implement Samterm;
+
+include "sys.m";
+sys: Sys;
+fprint, sprint, FD: import sys;
+stderr, logfd: ref FD;
+
+include "draw.m";
+draw:	Draw;
+
+include "samterm.m";
+
+include "samtk.m";
+samtk: Samtk;
+
+include "samstub.m";
+samstub: Samstub;
+Samio, Sammsg: import samstub;
+
+samio: ref Samio;
+
+ctxt: ref Context;
+
+init(context: ref draw->Context, nil: list of string)
+{
+	recvsam: chan of ref Sammsg;
+
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	stderr = sys->fildes(2);
+
+	logfd = sys->create("samterm.log", sys->OWRITE, 8r666);
+	if (logfd == nil) {
+		fprint(stderr, "Can't create samterm.log\n");
+		logfd = stderr;
+	}
+
+	fprint(logfd, "Samterm started\n");
+
+	pgrp := sys->pctl(sys->NEWPGRP, nil);
+
+	ctxt = ref Context(
+		context,
+		1000,		# initial tag
+
+		0,		# lock
+
+		nil,		# keysel
+		nil,		# scrollsel
+		nil,		# buttonsel
+		nil,		# menu2sel
+		nil,		# menu3sel
+		nil,		# titlesel
+		nil,		# tags
+
+		nil,		# menus
+		nil,		# texts
+
+		nil,		# cmd
+		nil,		# which
+		nil,		# work
+		pgrp,		# pgrp
+		logfd		# logging file descriptor
+	);
+
+	samtk = load Samtk Samtk->PATH;
+	if (samtk == nil) {
+		fprint(stderr, "Can't load %s\n", Samtk->PATH);
+		return;
+	}
+	samtk->init(ctxt);
+
+	samstub = load Samstub Samstub->PATH;
+	if (samstub == nil) {
+		fprint(stderr, "Can't load %s\n", Samstub->PATH);
+		return;
+	}
+	samstub->init(ctxt);
+
+	(samio, recvsam) = samstub->start();
+	if (samio == nil) {
+		fprint(stderr, "couldn't start samstub\n");
+		return;
+	}
+	samstub->outTs(samstub->Tversion, samstub->VERSION);
+
+	samstub->startcmdfile();
+
+	samstub->setlock();
+
+	for(;;) if (ctxt.lock == 0) alt {
+	(win, menu) := <-ctxt.titlesel =>
+		samstub->cleanout();
+		fl := ctxt.flayers[win];
+		tag := fl.tag;
+		if ((i := samtk->whichtext(tag)) < 0)
+			samtk->panic("samterm: whichtext");
+		t := ctxt.texts[i];
+		samtk->newcur(t, fl);
+		case menu {
+		"exit" =>
+			if (ctxt.flayers[win].tag == 0) {
+				samstub->outT0(samstub->Texit);
+				f := sprint("#p/%d/ctl", pgrp);
+				if ((fd := sys->open(f, sys->OWRITE)) != nil)
+					sys->write(fd, array of byte "killgrp\n", 8);
+				return;
+			}
+			samstub->close(win, tag);
+		"resize" =>
+			samtk->resize(fl);
+			samstub->scrollto(fl, fl.scope.first);
+		"task" =>
+			spawn samtk->titlectl(win, menu);
+		* =>
+			samtk->titlectl(win, menu);
+		}
+
+
+	(win, m1) := <-ctxt.buttonsel =>
+		samstub->cleanout();
+		fl := ctxt.flayers[win];
+		tag := fl.tag;
+		if (samtk->buttonselect(fl, m1)) {
+			samstub->outTsl(samstub->Tdclick, tag, fl.dot.first);
+			samstub->setlock();
+		}
+	(win, m2) := <-ctxt.menu2sel =>
+		samstub->cleanout();
+		fl := ctxt.flayers[win];
+		tag := fl.tag;
+		if ((i := samtk->whichtext(tag)) < 0)
+			samtk->panic("samterm: whichtext");
+		t := ctxt.texts[i];
+		samtk->newcur(t, fl);
+		case m2 {
+		"cut" =>
+			samstub->cut(t, fl);
+		"paste" =>
+			samstub->paste(t, fl);
+		"snarf" =>
+			samstub->snarf(t, fl);
+		"look" =>
+			samstub->look(t, fl);
+		"exch" =>
+			fprint(ctxt.logfd, "debug -- exch: %d, %s\n", win, m2);
+		"send" =>
+			samstub->send(t, fl);
+		"search" =>
+			samstub->search(t, fl);
+		* =>
+			samtk->panic("samterm: editmenu");
+		}
+	(win, m3) := <-ctxt.menu3sel =>
+		samstub->cleanout();
+		fl := ctxt.flayers[win];
+		tag := fl.tag;
+		if ((i := samtk->whichtext(tag)) < 0)
+			samtk->panic("samterm: whichtext");
+		t := ctxt.texts[i];
+		samtk->newcur(t, fl);
+		case m3 {
+		"new" =>
+			samstub->startnewfile();
+		"zerox" =>
+			samstub->zerox(t);
+		"close" =>
+			if (win != 0) {
+				samstub->close(win, tag);
+			}
+		"write" =>
+			samstub->outTs(samstub->Twrite, tag);
+			samstub->setlock();
+		* =>
+			for (i = 0; i < len ctxt.menus; i++) {
+				if (ctxt.menus[i].name == m3) {
+					break;
+				}
+			}
+			if (i == len ctxt.menus)
+				samtk->panic("init: can't find m3");
+			t = ctxt.menus[i].text;
+			t.flayers = samtk->append(tl t.flayers, hd t.flayers);
+			samtk->newcur(t, hd t.flayers);
+			
+		}
+	(win, c) := <-ctxt.keysel =>
+		if (ctxt.which != ctxt.flayers[win]) {
+			fprint(ctxt.logfd, "probably can't happen\n");
+			samstub->cleanout();
+			tag := ctxt.flayers[win].tag;
+			if ((i := samtk->whichtext(tag)) < 0)
+				samtk->panic("samterm: whichtext");
+			samtk->newcur(ctxt.texts[i], ctxt.flayers[win]);
+		}
+		samstub->keypress(c[1:len c -1]);
+	(win, c) := <-ctxt.scrollsel =>
+		if (ctxt.which != ctxt.flayers[win]) {
+			samstub->cleanout();
+			tag := ctxt.flayers[win].tag;
+			if ((i := samtk->whichtext(tag)) < 0)
+				samtk->panic("samterm: whichtext");
+			samtk->newcur(ctxt.texts[i], ctxt.flayers[win]);
+		}
+		(pos, lines) := samtk->scroll(ctxt.which, c);
+		if (lines > 0) {
+			samstub->outTsll(samstub->Torigin,
+				ctxt.which.tag, pos, lines);
+			samstub->setlock();
+		} else if (pos != -1)
+			samstub->scrollto(ctxt.which, pos);
+	h := <-recvsam =>
+		if (samstub->inmesg(h)) {
+			samstub->outT0(samstub->Texit);
+			fname := sprint("#p/%d/ctl", pgrp);
+			if ((fdesc := sys->open(fname, sys->OWRITE)) != nil)
+				sys->write(fdesc, array of byte "killgrp\n", 8);
+			return;
+		}
+	} else {
+		h := <-recvsam;
+		if (samstub->inmesg(h)) {
+			samstub->outT0(samstub->Texit);
+			fname := sprint("#p/%d/ctl", pgrp);
+			if ((fdesc := sys->open(fname, sys->OWRITE)) != nil)
+				sys->write(fdesc, array of byte "killgrp\n", 8);
+			return;
+		}
+	}
+}
--- /dev/null
+++ b/appl/wm/samstub.b
@@ -1,0 +1,1338 @@
+implement Samstub;
+
+include "sys.m";
+sys: Sys;
+fprint, FD, fildes: import sys;
+
+stderr: ref FD;
+
+include "draw.m";
+draw: Draw;
+
+include "samterm.m";
+samterm: Samterm;
+Text, Menu, Context, Flayer, Section: import samterm;
+
+include "samtk.m";
+samtk: Samtk;
+panic, whichtext, whichmenu: import samtk;
+
+include "samstub.m";
+
+sendsam:	chan of ref Sammsg;
+recvsam:	chan of ref Sammsg;
+
+snarflen:	int;
+
+ctxt: ref Context;
+
+requested: list of (int, int);
+
+tname := array [] of {
+	"Tversion",
+	"Tstartcmdfile",
+	"Tcheck",
+	"Trequest",
+	"Torigin",
+	"Tstartfile",
+	"Tworkfile",
+	"Ttype",
+	"Tcut",
+	"Tpaste",
+	"Tsnarf",
+	"Tstartnewfile",
+	"Twrite",
+	"Tclose",
+	"Tlook",
+	"Tsearch",
+	"Tsend",
+	"Tdclick",
+	"Tstartsnarf",
+	"Tsetsnarf",
+	"Tack",
+	"Texit",
+};
+
+hname := array [] of {
+	"Hversion",
+	"Hbindname",
+	"Hcurrent",
+	"Hnewname",
+	"Hmovname",
+	"Hgrow",
+	"Hcheck0",
+	"Hcheck",
+	"Hunlock",
+	"Hdata",
+	"Horigin",
+	"Hunlockfile",
+	"Hsetdot",
+	"Hgrowdata",
+	"Hmoveto",
+	"Hclean",
+	"Hdirty",
+	"Hcut",
+	"Hsetpat",
+	"Hdelname",
+	"Hclose",
+	"Hsetsnarf",
+	"Hsnarflen",
+	"Hack",
+	"Hexit",
+};
+
+init(c: ref Context)
+{
+	ctxt = c;
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+
+	stderr = fildes(2);
+
+	samterm = load Samterm Samterm->PATH;
+
+	samtk = load Samtk Samtk->PATH;
+	samtk->init(ctxt);
+
+	requested = nil;
+}
+
+start(): (ref Samio, chan of ref Sammsg)
+{
+	sys = load Sys Sys->PATH;
+
+	sys->bind("#C", "/", sys->MAFTER);
+
+	# Allocate a cmd device
+	ctl := sys->open("/cmd/clone", sys->ORDWR);
+	if(ctl == nil) {
+		fprint(stderr, "can't open /cmd/clone\n");
+		return (nil, nil);
+	}
+
+	# Find out which one
+	buf := array[32] of byte;
+	n := sys->read(ctl, buf, len buf);
+	if(n <= 0) {
+		fprint(stderr, "can't read cmd device\n");
+		return (nil, nil);
+	}
+
+	dir := "/cmd/"+string buf[0:n];
+
+	# Start the Command
+	n = sys->fprint(ctl, "exec "+ SAM);
+	if(n <= 0) {
+		fprint(stderr, "can't exec %s\n", SAM);
+		return (nil, nil);
+	}
+
+	data := sys->open(dir+"/data", sys->ORDWR);
+	if(data == nil) {
+		fprint(stderr, "can't open cmd data file\n");
+		return (nil, nil);
+	}
+
+	sendsam = chan of ref Sammsg;
+	recvsam = chan of ref Sammsg;
+
+	samio := ref Samio(ctl, data, array[1] of byte, 0, 0);
+
+	spawn sender(samio, sendsam);
+	spawn receiver(samio, recvsam);
+
+	return (samio, recvsam);
+}
+
+sender(samio: ref Samio, c: chan of ref Sammsg)
+{
+	fprint(ctxt.logfd, "sender started\n");
+	for (;;) {
+		h := <- c;
+		if (h == nil) return;
+		buf := array[3 + len h.mdata] of byte;
+		buf[0] = byte h.mtype;
+		buf[1] = byte h.mcount;
+		buf[2] = byte (h.mcount >> 8);
+		buf[3:] = h.mdata;
+		sys->write(samio.data, buf, len buf);
+	}
+}
+
+receiver(samio: ref Samio, msgchan: chan of ref Sammsg)
+{
+	c: int;
+
+	fprint(ctxt.logfd, "receiver started\n");
+
+	state := 0;
+	i := 0;
+	errs := 0;
+
+	h: ref Sammsg;
+
+	for (;;) {
+		if (samio.count == 0) {
+			n := sys->read(samio.data, samio.buffer, len samio.buffer);
+			if (n <= 0) {
+				fprint(stderr, "Read error on sam's pipe\n");
+				return;
+			}
+			samio.index = 0;
+			samio.count = n;
+		}
+		samio.count--;
+
+		c = int samio.buffer[samio.index++];
+
+		case state {
+		0 =>
+			h = ref Sammsg(c, 0, nil);
+			state++;
+			continue;
+		1 =>
+			h.mcount = c;
+			state++;
+			continue;
+		2 =>
+			h.mcount = h.mcount|(c<<8);
+			if (h.mcount > DATASIZE || h.mcount < 0)
+				panic("receiver: count>DATASIZE");
+			if(h.mcount != 0) {
+				h.mdata = array[h.mcount] of byte;
+				i = 0;
+				state++;
+				continue;
+			}
+		3 =>
+			h.mdata[i++] = byte c;
+			if(i < h.mcount){
+				continue;
+			}
+		}
+		msgchan <- = h;
+		h = nil;
+		state = 0;
+	}
+}
+
+inmesg(h: ref Sammsg): int
+{
+
+	case h.mtype {
+
+	Hversion =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hversion: %d\n", m);
+
+	Hbindname =>
+		m := h.inshort(0);
+		vl := h.invlong(2);
+		fprint(ctxt.logfd, "Hbindname: %ux, %bux\n", m, vl);
+		bindname(m, int vl);
+
+	Hcurrent =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hcurrent: %d\n", m);
+		hcurrent(m);
+
+	Hmovname =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hmovname: %d, %s\n", m, string h.mdata[2:]);
+		movename(m, string h.mdata[2:]);
+
+	Hgrow =>
+		m := h.inshort(0);
+		l1 := h.inlong(2);
+		l2 := h.inlong(6);
+		fprint(ctxt.logfd, "Hgrow: %d, %d, %d\n", m, l1, l2);
+		hgrow(m, l1, l2);
+
+	Hnewname =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hnewname: %d\n", m);
+		newname(m);
+
+	Hcheck0 =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hcheck0: %d\n", m);
+		i := whichmenu(m);
+		if (i >= 0) {
+			t := ctxt.menus[i].text;
+			if (t != nil)
+				t.lock++;
+			outTs(Tcheck, m);
+		}
+
+	Hcheck =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hcheck: %d\n", m);
+		i := whichmenu(m);
+		if (i >= 0) {
+			t := ctxt.menus[i].text;
+			if (t != nil && t.lock)
+				t.lock--;
+			hcheck(t);
+		}
+
+	Hunlock =>
+		fprint(ctxt.logfd, "Hunlock\n");
+		clrlock();
+
+	Hdata =>
+		m := h.inshort(0);
+		l := h.inlong(2);
+		fprint(ctxt.logfd, "Hdata: %d, %d, %s\n",
+			m, l, contract(string h.mdata[6:]));
+		hdata(m, l, string h.mdata[6:]);
+
+	Horigin =>
+		m := h.inshort(0);
+		l := h.inlong(2);
+		fprint(ctxt.logfd, "Horigin: %d, %d\n", m, l);
+		horigin(m, l);
+
+	Hunlockfile =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hunlockfile: %d\n", m);
+		clrlock();
+
+	Hsetdot =>
+		m := h.inshort(0);
+		l1 := h.inlong(2);
+		l2 := h.inlong(6);
+		fprint(ctxt.logfd, "Hsetdot: %d, %d, %d\n", m, l1, l2);
+		hsetdot(m, l1, l2);
+
+	Hgrowdata =>
+		m := h.inshort(0);
+		l1 := h.inlong(2);
+		l2 := h.inlong(6);
+		fprint(ctxt.logfd, "Hgrowdata: %d, %d, %d, %s\n",
+			m, l1, l2, contract(string h.mdata[10:]));
+		hgrowdata(m, l1, l2, string h.mdata[10:]);
+
+	Hmoveto =>
+		m := h.inshort(0);
+		l := h.inlong(2);
+		fprint(ctxt.logfd, "Hmoveto: %d, %d\n", m, l);
+		hmoveto(m, l);
+
+	Hclean =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hclean: %d\n", m);
+		hclean(m);
+
+	Hdirty =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hdirty: %d\n", m);
+		hdirty(m);
+
+	Hdelname =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hdelname: %d\n", m);
+		hdelname(m);
+
+	Hcut =>
+		m := h.inshort(0);
+		l1 := h.inlong(2);
+		l2 := h.inlong(6);
+		fprint(ctxt.logfd, "Hcut: %d, %d, %d\n",
+			m, l1, l2);
+		hcut(m, l1, l2);
+
+	Hclose =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hclose: %d\n", m);
+		hclose(m);
+
+	Hsetpat =>
+		fprint(ctxt.logfd, "Hsetpat: %s\n", string h.mdata);
+		samtk->hsetpat(string h.mdata);
+
+	Hsetsnarf =>
+		m := h.inshort(0);
+		fprint(ctxt.logfd, "Hsetsnarf: %d\n", m);
+
+	Hsnarflen =>
+		snarflen = h.inlong(0);
+		fprint(ctxt.logfd, "Hsnarflen: %d\n", snarflen);
+
+	Hack =>
+		fprint(ctxt.logfd, "Hack\n");
+		outT0(Tack);
+
+	Hexit =>
+		fprint(ctxt.logfd, "Hexit\n");
+		return 1;
+
+	-1 =>
+		panic("rcv error");
+
+	* =>
+		fprint(ctxt.logfd, "type %d\n", h.mtype);
+		panic("rcv unknown");
+	}
+	return 0;
+}
+
+Sammsg.inshort(h: self ref Sammsg, n: int): int
+{
+	return	((int h.mdata[n+1])<<8) |
+		((int h.mdata[n]));
+}
+
+Sammsg.inlong(h: self ref Sammsg, n: int): int
+{
+	return	((int h.mdata[n+3])<<24) |
+		((int h.mdata[n+2])<<16) |
+		((int h.mdata[n+1])<< 8) |
+		((int h.mdata[n]));
+}
+
+Sammsg.invlong(h: self ref Sammsg, n: int): big
+{
+	return	((big h.mdata[n+7])<<56) |
+		((big h.mdata[n+6])<<48) |
+		((big h.mdata[n+5])<<40) |
+		((big h.mdata[n+4])<<32) |
+		((big h.mdata[n+3])<<24) |
+		((big h.mdata[n+2])<<16) |
+		((big h.mdata[n+1])<< 8) |
+		((big h.mdata[n]));
+}
+
+Sammsg.outcopy(h: self ref Sammsg, pos: int, data: array of byte)
+{
+	h.mdata[pos:] = data;
+}
+
+Sammsg.outshort(h: self ref Sammsg, pos: int, s: int)
+{
+	h.mdata[pos++]	= byte s;
+	h.mdata[pos]	= byte (s >> 8);
+}
+
+Sammsg.outlong(h: self ref Sammsg, pos: int, s: int)
+{
+	h.mdata[pos++]	= byte s;
+	h.mdata[pos++]	= byte (s >> 8);
+	h.mdata[pos++]	= byte (s >> 16);
+	h.mdata[pos]	= byte (s >> 24);
+}
+
+Sammsg.outvlong(h: self ref Sammsg, pos: int, s: big)
+{
+	h.mdata[pos++]	= byte s;
+	h.mdata[pos++]	= byte (s >> 8);
+	h.mdata[pos++]	= byte (s >> 16);
+	h.mdata[pos++]	= byte (s >> 24);
+	h.mdata[pos++]	= byte (s >> 32);
+	h.mdata[pos++]	= byte (s >> 40);
+	h.mdata[pos++]	= byte (s >> 48);
+	h.mdata[pos]	= byte (s >> 56);
+}
+
+outT0(t: int)
+{
+	fprint(ctxt.logfd, "\t\t\t\t\t%s\n", tname[t]);
+	h := ref Sammsg(t, 0, nil);
+	sendsam <- = h;
+}
+
+outTs(t, s: int)
+{
+	fprint(ctxt.logfd, "\t\t\t\t\t%s %ux\n", tname[t], s);
+	a := array[2] of byte;
+	h := ref Sammsg(t, 2, a);
+	h.outshort(0, s);
+	sendsam <- = h;
+}
+	
+outTv(t: int, i: big)
+{
+	fprint(ctxt.logfd, "\t\t\t\t\t%s %bux\n", tname[t], i);
+	a := array[8] of byte;
+	h := ref Sammsg(t, 8, a);
+	h.outvlong(0, i);
+	sendsam <- = h;
+}
+
+outTsll(t, m, l1, l2: int)
+{	fprint(ctxt.logfd, "\t\t\t\t\t%s %d %d %d\n", tname[t], m, l1, l2);
+	a := array[10] of byte;
+	h := ref Sammsg(t, 10, a);
+	h.outshort(0, m);
+	h.outlong(2, l1);
+	h.outlong(6, l2);
+	sendsam <- = h;
+}
+
+outTsl(t, m, l: int)
+{	fprint(ctxt.logfd, "\t\t\t\t\t%s %d %d\n", tname[t], m, l);
+	a := array[6] of byte;
+	h := ref Sammsg(t, 6, a);
+	h.outshort(0, m);
+	h.outlong(2, l);
+	sendsam <- = h;
+}
+
+outTsls(t, m, l1, l2: int)
+{	fprint(ctxt.logfd, "\t\t\t\t\t%s %d %d %d\n", tname[t], m, l1, l2);
+	a := array[8] of byte;
+	h := ref Sammsg(t, 8, a);
+	h.outshort(0, m);
+	h.outlong(2, l1);
+	h.outshort(6, l2);
+	sendsam <- = h;
+}
+	
+outTslS(t, s1, l1: int, s: string)
+{
+	fprint(ctxt.logfd, "\t\t\t\t\t%s %d %d %s\n", tname[t], s1, l1, s);
+	a := array[6 + len array of byte s] of byte;
+	h := ref Sammsg(t, len a, a);
+	h.outshort(0, s1);
+	h.outlong(2, l1);
+	h.outcopy(6, array of byte s);
+	sendsam <- = h;
+}
+
+newname(tag: int)
+{
+	menuins(0, "dummy", nil, tag);
+}
+
+bindname(tag, l: int)
+{
+	if ((m := whichmenu(tag)) < 0) panic("bindname: whichmenu");
+	if ((l = whichtext(l)) < 0) panic("bindname: whichtext");
+	if (ctxt.menus[m].text != nil)
+		return;		# Already bound
+	t := ctxt.texts[l];
+	t.tag = tag;
+	for (fls := t.flayers; fls != nil; fls = tl fls) (hd fls).tag = tag;
+	ctxt.menus[m].text = t;
+}
+
+menuins(m: int, s: string, t: ref Text, tag: int)
+{
+	newmenus := array [len ctxt.menus+1] of ref Menu;
+	menu := ref Menu(
+		tag,	# tag
+		s,	# name
+		t	# text
+	);
+	if (m > 0)
+		newmenus[0:] = ctxt.menus[0:m];
+	newmenus[m] = menu;
+	if (m < len ctxt.menus)
+		newmenus[m+1:] = ctxt.menus[m:];	
+	ctxt.menus = newmenus;
+
+	samtk->menuins(m, s);
+}
+
+menudel(m: int)
+{
+	if (len ctxt.menus == 0 || m >= len ctxt.menus || ctxt.menus[m].text != nil)
+		panic("menudel");
+	newmenus := array [len ctxt.menus - 1] of ref Menu;
+	newmenus[0:] = ctxt.menus[0:m];
+	newmenus[m:] = ctxt.menus[m+1:];
+	ctxt.menus = newmenus;
+	samtk->menudel(m);
+}
+
+outcmd() {
+	if(ctxt.work != nil) {
+		fl := ctxt.work;
+		outTsll(Tworkfile, fl.tag, fl.dot.first, fl.dot.last);
+	}
+}
+
+hclose(m: int)
+{
+	i: int;
+
+	# close LAST window of a file
+	if((m = whichmenu(m)) < 0) panic("hclose: whichmenu");
+	t := ctxt.menus[m].text;
+	if (tl t.flayers != nil) panic("hclose: flayers");
+	fl := hd t.flayers;
+	fl.t = nil;
+	for (i = 0; i< len ctxt.flayers; i++)
+		if (ctxt.flayers[i] == fl) break;
+	if (i == len ctxt.flayers) panic("hclose: ctxt.flayers");
+	samtk->chandel(i);
+	t.flayers = nil;
+	for (i = 0; i< len ctxt.texts; i++)
+		if (ctxt.texts[i] == ctxt.menus[m].text) break;
+	if (i == len ctxt.texts) panic("hclose: ctxt.texts");
+	ctxt.texts[i:] = ctxt.texts[i+1:];
+	ctxt.texts = ctxt.texts[:len ctxt.texts - 1];
+	ctxt.menus[m].text = nil;
+	ctxt.which = nil;
+	samtk->focus(hd ctxt.cmd.flayers);
+}
+
+close(win, tag: int)
+{
+	nfls: list of ref Flayer;
+
+	if ((m := whichtext(tag)) < 0) panic("close: text");
+	t := ctxt.texts[m];
+	if ((m = whichmenu(tag)) < 0) panic("close: menu");
+	if (len t.flayers == 1) {
+		outTs(Tclose, tag);
+		setlock();
+		return;
+	}
+	fl := ctxt.flayers[win];
+	nfls = nil;
+	for (fls := t.flayers; fls != nil; fls = tl fls)
+		if (hd fls != fl) nfls = hd fls :: nfls;
+	t.flayers = nfls;
+	samtk->chandel(win);
+	fl.t = nil;
+	samtk->settitle(t, ctxt.menus[m].name);
+	ctxt.which = nil;
+}
+
+hdelname(m: int)
+{
+	# close LAST window of a file
+	if((m = whichmenu(m)) < 0) panic("hdelname: whichmenu");
+	if (ctxt.menus[m].text != nil) panic("hdelname: text");
+	ctxt.menus[m:] = ctxt.menus[m+1:];
+	ctxt.menus = ctxt.menus[:len ctxt.menus - 1];
+	samtk->menudel(m);
+	ctxt.which = nil;
+}
+
+hdirty(m: int)
+{
+	if((m = whichmenu(m)) < 0) panic("hdirty: whichmenu");
+	if (ctxt.menus[m].text == nil) panic("hdirty: text");
+	ctxt.menus[m].text.state |= Samterm->Dirty;
+	samtk->settitle(ctxt.menus[m].text, ctxt.menus[m].name);
+}
+
+hclean(m: int)
+{
+	if((m = whichmenu(m)) < 0) panic("hclean: whichmenu");
+	if (ctxt.menus[m].text == nil) panic("hclean: text");
+	ctxt.menus[m].text.state &= ~Samterm->Dirty;
+	samtk->settitle(ctxt.menus[m].text, ctxt.menus[m].name);
+}
+
+movename(tag: int, s: string)
+{
+	i := whichmenu(tag);
+	if (i < 0) panic("movename: whichmenu");
+
+	t := ctxt.menus[i].text;
+
+	ctxt.menus[i].text = nil;	# suppress panic in menudel
+	menudel(i);
+
+	if(t == ctxt.cmd)
+		i = 0;
+	else {
+		if (len ctxt.menus > 0 && ctxt.menus[0].text == ctxt.cmd)
+			i = 1;
+		else
+			i = 0;
+		for(; i < len ctxt.menus; i++) {
+			if (s < ctxt.menus[i].name)
+				break;
+		}
+	}
+	if (t != nil) samtk->settitle(t, s);
+	menuins(i, s, t, tag);
+}
+
+hcheck(t: ref Text)
+{
+	if (t == nil) {
+		fprint(ctxt.logfd, "hcheck: no text in menu entry\n");
+		return;
+	}
+	for (fls := t.flayers; fls != nil; fls = tl fls) {
+		fl := hd fls;
+		scrollto(fl, fl.scope.first);
+	}
+}
+
+setlock()
+{
+	ctxt.lock++;
+	samtk->allflayers("cursor -bitmap cursor.wait");
+}
+
+clrlock()
+{
+	if (ctxt.lock > 0)
+		ctxt.lock--;
+	else
+		fprint(ctxt.logfd, "lock: wasn't locked\n");
+	if (ctxt.lock == 0)
+		samtk->allflayers("cursor -default; update");
+}
+
+hcut(m, where, howmuch: int)
+{
+	if((m = whichmenu(m)) < 0) panic("hcut: whichmenu");
+	t := ctxt.menus[m].text;
+	if (t == nil) panic("hcut -- no text");
+
+#	sctdump(t.sects, "Hcut, before");
+	t.nrunes -= howmuch;
+	t.sects = sctdelete(t.sects, where, howmuch);
+#	sctdump(t.sects, "Hcut, after");
+	for (fls := t.flayers; fls != nil; fls = tl fls) {
+		fl := hd fls;
+		if (where < fl.scope.first) {
+			if (where + howmuch <= fl.scope.first)
+				fl.scope.first -= howmuch;
+			else
+				fl.scope.first = where;
+		}
+		if (where < fl.scope.last) {
+			if (where + howmuch <= fl.scope.last)
+				fl.scope.last -= howmuch;
+			else
+				fl.scope.last = where;
+		}
+	}
+}
+
+hgrow(tag, l1, l2: int)
+{
+	if((m := whichmenu(tag)) < 0) panic("hgrow: whichmenu");
+	t := ctxt.menus[m].text;
+	grow(t, l1, l2);
+}
+
+hdata(m, l: int, s: string)
+{
+	nr: list of (int, int);
+
+	if((m = whichmenu(m)) < 0) panic("hdata: whichmenu");
+	t := ctxt.menus[m].text;
+	if (t == nil) panic("hdata -- no text");
+	if (s != "") {
+		t.sects = sctput(t.sects, l, s);
+		updatefls(t, l, s);
+	}
+	for (nr = nil; requested != nil; requested = tl requested) {
+		(r1, r2) := hd requested;
+		if (r1 != m || r2 != l)
+			nr = (r1, r2) :: nr;
+	}
+	requested = nr;
+	clrlock();
+}
+
+hgrowdata(tag, l1, l2: int, s: string)
+{
+	if((m := whichmenu(tag)) < 0) panic("hgrow: whichmenu");
+	t := ctxt.menus[m].text;
+	if (t == nil) panic("hdata -- no text");
+	grow(t, l1, l2);
+	t.sects = sctput(t.sects, l1, s);
+	updatefls(t, l1, s);
+}
+
+hsetdot(m, l1, l2: int)
+{
+	if((m = whichmenu(m)) < 0) panic("hsetdot: whichmenu");
+	t := ctxt.menus[m].text;
+	if (t == nil || t.flayers == nil) panic("hsetdot -- no text");
+	samtk->setdot(hd t.flayers, l1, l2);
+}
+
+hcurrent(tag: int)
+{
+	if ((i := whichmenu(tag)) < 0) panic("hcurrent: whichmenu");
+	if (ctxt.menus[i].text == nil) {
+		n := startfile(tag);
+		ctxt.menus[i].text = ctxt.texts[n];
+		if (ctxt.menus[i].name != nil)
+			samtk->settitle(ctxt.texts[n], ctxt.menus[i].name);
+	}
+	ctxt.work = hd ctxt.menus[i].text.flayers;
+}
+
+hmoveto(m, l: int)
+{
+	if((m = whichmenu(m)) < 0) panic("hmoveto: whichmenu");
+	t := ctxt.menus[m].text;
+	fl := hd t.flayers;
+	if (fl.scope.first <= l &&
+	   (l < fl.scope.last || fl.scope.last == fl.scope.first))
+		return;
+	(n, p) := sctrevcnt(t.sects, l, fl.lines/2);
+#	fprint(ctxt.logfd, "hmoveto: (n, p) = (%d, %d)\n", n, p);
+	if (n < 0) {
+		outTsll(Torigin, t.tag, l, fl.lines/2);
+		setlock();
+		return;
+	}
+	scrollto(fl, p);
+}
+
+startcmdfile()
+{
+	t := ctxt.tag++;
+	n := newtext(t, 1);
+	ctxt.cmd = ctxt.texts[n];
+	outTv(Tstartcmdfile, big t);
+}
+
+startnewfile()
+{
+	t := ctxt.tag++;
+	n := newtext(t, 0);
+	outTv(Tstartnewfile, big t);
+}
+
+startfile(tag: int): int
+{
+	n := newtext(tag, 0);
+	outTv(Tstartfile, big tag);
+	setlock();
+	return n;
+}
+
+horigin(m, l: int)
+{
+	if((m = whichmenu(m)) < 0) panic("hmoveto: whichmenu");
+	t := ctxt.menus[m].text;
+	fl := hd t.flayers;
+	scrollto(fl, l);
+	clrlock();
+}
+
+scrollto(fl: ref Flayer, where: int)
+{
+	s: string;
+	n: int;
+
+	tag := fl.tag;
+	if ((i := whichtext(tag)) < 0) panic("scrollto: whichtext");
+	t := ctxt.texts[i];
+	
+	samtk->flclear(fl);
+	(n, s) = sctgetlines(t.sects, where, fl.lines);
+	fl.scope.first = where;
+	fl.scope.last = where + len s;
+	if (s != "")
+		samtk->flinsert(fl, where, s);
+	if (n == 0) {
+		samtk->setscrollbar(t, fl);
+	} else {
+		(h, l) := scthole(t, fl.scope.last);
+		fl.scope.last = h;
+		if (l > 0)
+			outrequest(tag, h, l);
+		else
+			if (fl.scope.first > t.nrunes) {
+				fl.scope.first = t.nrunes;
+				fl.scope.last = t.nrunes;
+				samtk->setscrollbar(t, fl);
+			}
+	}
+}
+
+scthole(t: ref Text, f: int): (int, int)
+{
+	p := 0;
+	h := -1;
+	l := 0;
+	for (scts := t.sects; scts != nil; scts = tl scts) {
+		sct := hd scts;
+		nr := sct.nrunes;
+		nt := len sct.text;
+		if (h >= 0) {
+			if (sct.text == "") {
+				l += nr;
+				if (l >= 512) return (h,512);
+			} else
+				return (h,l);
+		}
+		if (h < 0 && f < nr) {
+			if (nt < nr) {
+				if (f < nt) {
+					h = p + nt;
+					l = nr - nt;
+				} else {
+					h = p + f;
+					l = nr - f;
+				}
+				if (l >= 512) return (h,512);
+			}
+		}
+		p += sct.nrunes;
+		f -= sct.nrunes;
+	}
+	if (h == -1) return (p, 0);
+	return (h, l);
+}
+
+# return (x, p): x = -1: p -> hole; x = 0: p -> line n; x > 0: p -> eof
+sctlinecount(t: ref Text, pos, n: int): (int, int)
+{
+	i: int;
+
+	p := 0;
+	for (scts := t.sects; scts != nil; scts = tl scts) {
+		sct := hd scts;
+		nr := sct.nrunes;
+		nt := len sct.text;
+		if (pos < nr) {
+			if (pos > 0) i = pos; else i = 0;
+			while (i < nt) {
+				if (sct.text[i++] == '\n') n--;
+				if (n == 0) return (0, p + i);
+			}
+			if (nt < nr) return (-1, p + nt);
+		}
+		p += sct.nrunes;
+		pos -= sct.nrunes;
+	}
+	return (n, p);
+}
+
+sctrevcnt(scts: list of ref Section, pos, n: int): (int, int)
+{
+	if (scts == nil) return (n, 0);
+	sct := hd scts;
+	scts = tl scts;
+	nt := len sct.text;
+	nr := sct.nrunes;
+	if (pos >= nr) {
+		(n, pos) = sctrevcnt(scts, pos - nr, n);
+		pos += nr;
+	}
+	if (n > 0) {
+		if (nt < nr && pos > nt)
+			return(-1, pos);
+		for (i := pos-1; i >= 0; i--) {
+			if (sct.text[i] == '\n') n--;
+			if (n == 0) break;
+		}
+		return (n, i + 1);	
+	}
+	return (n, pos);
+}
+
+insertfls(t: ref Text, l: int, s: string)
+{
+	for (fls := t.flayers; fls != nil; fls = tl fls) {
+		fl := hd fls;
+		if (l < fl.scope.first || l > fl.scope.last) continue;
+		samtk->flinsert(fl, l, s);
+		samtk->setscrollbar(t, fl);
+		fl.scope.last += len s;
+	}
+}
+
+updatefls(t: ref Text, l: int, s: string)
+{
+	for (fls := t.flayers; fls != nil; fls = tl fls) {
+		fl := hd fls;
+		if (l < fl.scope.first || l > fl.scope.last) continue;
+		samtk->flinsert(fl, l, s);
+		(x, p) := sctlinecount(t, fl.scope.first, fl.lines);
+		fl.scope.last = p;
+		if (x >= 0) {
+			if (p > l + len s) {
+				samtk->flinsert(fl, l + len s,
+					sctget(t.sects, l + len s, p));
+			}
+			if (x == 0)
+				samtk->fldelexcess(fl);
+		} else {
+			(h1, h2) := scthole(t, l);
+			fl.scope.last = h1;
+			if (h2 > 0) {
+				outrequest(t.tag, h1, h2);
+				continue;
+			} else {
+				panic("Can't happen ??");
+			}
+		}
+		samtk->setscrollbar(t, fl);
+	}
+}
+
+outrequest(tag, h1, h2: int) {
+	for (l := requested; l != nil; l = tl l) {
+		(r1, r2) := hd l;
+		if (r1 == tag && r2 == h1) return;
+	}
+	outTsls(Trequest, tag, h1, h2);
+	requested = (tag, h1) :: requested;
+	setlock();
+}
+
+deletefls(t: ref Text, pos, nbytes: int)
+{
+	for (fls := t.flayers; fls != nil; fls = tl fls) {
+		fl := hd fls;
+		if (pos >= fl.scope.last) continue;
+		if (pos + nbytes <= fl.scope.first || pos >= fl.scope.last) {
+			fl.scope.first -= nbytes;
+			fl.scope.last -= nbytes;
+			continue;
+		}
+		samtk->fldelete(fl, pos, pos + nbytes);
+		(x, p) := sctlinecount(t, fl.scope.first, fl.lines);
+		if (x >= 0 && p > fl.scope.last) {
+			samtk->flinsert(fl, fl.scope.last,
+				sctget(t.sects, fl.scope.last, p));
+			fl.scope.last = p;
+		} else {
+			fl.scope.last = p;
+			(h1, h2) := scthole(t, fl.scope.last);
+			if (h2 > 0)
+				outrequest(t.tag, h1, h2);
+		}
+		samtk->setscrollbar(t, fl);
+	}
+}
+
+contract(s: string): string
+{
+	if (len s < 32)
+		cs := s;
+	else
+		cs = s[0:16] + " ... " + s[len s - 16:];
+	for (i := 0; i < len cs; i++)
+		if (cs[i] == '\n') cs[i] = '\u008a';
+	return cs;
+}
+
+cleanout()
+{
+	if ((fl := ctxt.which) == nil) return;
+	if ((i := whichtext(fl.tag)) < 0) panic("cleanout: whichtext");
+	t := ctxt.texts[i];
+
+	if (fl.typepoint >= 0 && fl.dot.first > fl.typepoint) {
+		s := sctget(t.sects, fl.typepoint, fl.dot.first);
+		outTslS(Samstub->Ttype, fl.tag, fl.typepoint, s);
+		t.state &= ~Samterm->LDirty;
+	}
+	fl.typepoint = -1;
+}
+
+newtext(tag, tp: int): int
+{
+	n := len ctxt.texts;
+	t := ref Text(
+		tag,					# tag
+		0,					# lock
+		samtk->newflayer(tag, tp) :: nil,	# flayers
+		0,					# nrunes
+		nil,					# sects
+		0					# state
+	);
+	texts := array [n + 1] of ref Text;
+	texts[0:] = ctxt.texts;
+	texts[n] = t;
+	ctxt.texts = texts;
+	samtk->newcur(t, hd t.flayers);
+	return n;
+}
+
+keypress(key: string)
+{
+	# Find text and flayer
+	fl := ctxt.which;
+	tag := fl.tag;
+	if ((i := whichtext(tag)) < 0) panic("keypress: whichtext");
+	t := ctxt.texts[i];
+
+	if (fl.dot.last != fl.dot.first) {
+		cut(t, fl);
+	}
+
+	case (key) {
+	"\b" =>
+		if (t.nrunes == 0 || fl.dot.first == 0)
+			return;
+		fl.dot.first--;
+		if (fl.typepoint >= 0 && fl.dot.first >= fl.typepoint) {
+			t.nrunes -= fl.dot.last - fl.dot.first;
+			t.sects = sctdelete(t.sects, fl.dot.first, fl.dot.last - fl.dot.first);
+			deletefls(t, fl.dot.first, fl.dot.last - fl.dot.first);
+			if (fl.dot.first == fl.typepoint) {
+				fl.typepoint = -1;
+				t.state &= ~Samterm->LDirty;
+				if ((i = whichmenu(tag)) < 0)
+					panic("keypress: whichmenu");
+				samtk->settitle(t, ctxt.menus[i].name);
+			}
+		} else {
+			cut(t, fl);
+		}
+	* =>
+		if (fl.typepoint < 0) {
+			fl.typepoint = fl.dot.first;
+			t.state |= Samterm->LDirty;
+			if ((i = whichmenu(tag)) < 0)
+				panic("keypress: whichmenu");
+			samtk->settitle(t, ctxt.menus[i].name);
+		}
+		if (fl.dot.first > t.nrunes)
+			panic("keypress -- cursor > file len");
+		t.sects = sctmakeroom(t.sects, fl.dot.first, len key);
+		t.nrunes += len key;
+		t.sects = sctput(t.sects, fl.dot.first, key);
+		insertfls(t, fl.dot.first, key);
+		f := fl.dot.first + len key;
+		samtk->setdot(fl, f, f);
+		if (key == "\n") {
+			if (f >= fl.scope.last) {
+				(n, p) := sctrevcnt(t.sects, f-1, 2*fl.lines/3);
+				if (n < 0) {
+					outTsll(Torigin, t.tag, f-1, 2*fl.lines/3);
+					setlock();
+				} else {
+					scrollto(fl, p);
+				}
+			}
+			if (t == ctxt.cmd && fl.dot.last == t.nrunes) {
+				outcmd();
+				setlock();
+			}
+			cleanout();
+		}
+	}
+	return;
+}
+
+cut(t: ref Text, fl: ref Flayer)
+{
+	if (fl.typepoint >= 0) panic("cut: typepoint");
+	outTsll(Tcut, fl.tag, fl.dot.first, fl.dot.last);
+	t.nrunes -= fl.dot.last - fl.dot.first;
+	t.sects = sctdelete(t.sects, fl.dot.first, fl.dot.last - fl.dot.first);
+	deletefls(t, fl.dot.first, fl.dot.last - fl.dot.first);
+}
+
+paste(t: ref Text, fl: ref Flayer)
+{
+	if (fl.typepoint >= 0) panic("paste: typepoint");
+	if (snarflen == 0) return;
+	if (fl.dot.first < fl.dot.last) cut(t, fl);
+	outTsl(Tpaste, fl.tag, fl.dot.first);
+}
+
+snarf(nil: ref Text, fl: ref Flayer)
+{
+	if (fl.typepoint >= 0) panic("snarf: typepoint");
+	if (fl.dot.first == fl.dot.last) return;
+	snarflen = fl.dot.last - fl.dot.first;
+	outTsll(Tsnarf, fl.tag, fl.dot.first, fl.dot.last);
+}
+
+look(nil: ref Text, fl: ref Flayer)
+{
+	if (fl.typepoint >= 0) panic("look: typepoint");
+	outTsll(Tlook, fl.tag, fl.dot.first, fl.dot.last);
+	setlock();
+}
+
+send(nil: ref Text, fl: ref Flayer)
+{
+	if (fl.typepoint >= 0) panic("send: typepoint");
+	outcmd();
+	outTsll(Tsend, fl.tag, fl.dot.first, fl.dot.last);
+	setlock();
+}
+
+search(nil: ref Text, fl: ref Flayer)
+{
+	if (fl.typepoint >= 0) panic("search: typepoint");
+	outcmd();
+	outT0(Tsearch);
+	setlock();
+}
+
+zerox(t: ref Text)
+{
+	fl := samtk->newflayer(t.tag, ctxt.cmd == t);
+	t.flayers = fl :: t.flayers;
+	m := whichmenu(t.tag);
+	samtk->settitle(t, ctxt.menus[m].name);
+	samtk->newcur(t, fl);
+	scrollto(fl, 0);
+}
+
+sctget(scts: list of ref Section, p1, p2: int): string
+{
+	while (scts != nil) {
+		sct := hd scts; scts = tl scts;
+		ln := len sct.text;
+		if (p1 < sct.nrunes) {
+			if (ln < sct.nrunes && p2 > ln) {
+				sctdump(scts, "panic");
+				panic("sctget - asking for a hole");
+			}
+			if (p2 > sct.nrunes) {
+				s := sct.text[p1:];
+				return s + sctget(scts, 0, p2 - ln);
+			}
+			return sct.text[p1:p2];
+		}
+		p1 -= sct.nrunes;
+		p2 -= sct.nrunes;
+	}
+	return "";
+}
+
+sctgetlines(scts: list of ref Section, p, n: int): (int, string)
+{
+	s := "";
+	while (scts != nil) {
+		sct := hd scts; scts = tl scts;
+		ln := len sct.text;
+		if (p < sct.nrunes) {
+			if (p > ln) return (n, s);
+			if (p > 0) b := p; else b = 0;
+			for (i := b; i < ln && n > 0;   ) {
+				if (sct.text[i++] == '\n') n--;
+			}
+			if ( i > b)
+				s = s + sct.text[b:i];
+			if (n == 0 || ln < sct.nrunes) return (n, s);
+		}
+		p -= sct.nrunes;
+	}
+	return (n, s);
+}
+
+sctput(scts: list of ref Section, pos: int, s: string): list of ref Section
+{
+	# There should be a hole to receive text
+	if (scts == nil  && s != "") panic("sctput: scts is nil\n");
+	sct := hd scts;
+	l := len sct.text;
+	if (sct.nrunes <= pos) {
+		return sct :: sctput(tl scts, pos-sct.nrunes, s);
+	}
+	if (pos < l) {
+		sctdump(scts, "panic");
+		panic("sctput: overwriting");
+	}
+	if (pos == l) {
+		if (sct.nrunes < l + len s) {
+			sct.text += s[:sct.nrunes-l];
+			return sct :: sctput(tl scts, 0, s[sct.nrunes-l:]);
+		} 
+		sct.text += s;	
+		return sct :: tl scts;
+	}
+	nrunes := sct.nrunes;
+	sct.nrunes = pos;
+	if (nrunes < pos + len s)
+		return	sct ::
+			ref Section(nrunes-pos, s[:nrunes-pos]) ::
+			sctput(tl scts, 0, s[nrunes-pos:]);
+	return sct :: ref Section(nrunes-pos, s) :: tl scts;
+}
+
+sctmakeroom(scts: list of ref Section, pos: int, l: int): list of ref Section
+{
+	if (scts == nil) {
+		if (pos) panic("sctmakeroom: beyond end of sections");
+		return ref Section(l, nil) :: nil;
+	}
+	sct := hd scts;
+	if (sct.nrunes < pos)
+		return sct :: sctmakeroom(tl scts, pos-sct.nrunes, l);
+	if (len sct.text <= pos) {
+		# just add to the hole at end of section
+		sct.nrunes += l;
+		return sct :: tl scts;
+	}
+	if (pos == 0) {
+		# text is non-nil!
+		bsct := ref Section(l, nil);
+		return bsct :: scts;
+	}
+	bsct := ref Section(pos + l, sct.text[0:pos]);
+	esct := ref Section(sct.nrunes-pos, sct.text[pos:]);
+	return bsct :: esct :: tl scts;
+}
+
+sctdelete(scts: list of ref Section, start, nbytes: int): list of ref Section
+{
+	if (nbytes == 0) return scts;
+	if (scts == nil) panic("sctdelete: at eof");
+	sct := hd scts;
+	scts = tl scts;
+	nrunes := sct.nrunes;
+	if (start + nbytes < len sct.text) {
+		sct.text = sct.text[0:start] + sct.text[start+nbytes:];
+		sct.nrunes -= nbytes;
+		return sct :: scts;
+	}
+	if (start < nrunes) {
+		if (start > 0) {
+			if (start < len sct.text)
+				sct.text = sct.text[0:start];
+			if (start + nbytes <= nrunes) {
+				sct.nrunes -= nbytes;
+				return sct :: scts;
+			}
+			sct.nrunes = start;
+			return sct :: sctdelete(scts, 0, nbytes-nrunes+start);
+		}
+		if (nbytes < nrunes) {
+			sct.text = "";
+			sct.nrunes -= nbytes;
+			return sct :: scts;
+		}
+		return sctdelete(scts, 0, nbytes - nrunes);
+	}
+	return sct :: sctdelete(scts, start - nrunes, nbytes);
+}
+
+grow(t: ref Text, at, l: int)
+{
+#	sctdump(t.sects, "grow, before");
+	t.sects = sctmakeroom(t.sects, at, l);
+	t.nrunes += l;
+#	sctdump(t.sects, "grow, after");
+	for (fls := t.flayers; fls != nil; fls = tl fls) {
+		fl := hd fls;
+		if (at < fl.scope.first) fl.scope.first += l;
+		if (at < fl.scope.last) fl.scope.last += l;
+	}
+}
+
+findhole(t: ref Text): (int, int)
+{
+	for (fls := t.flayers; fls != nil; fls = tl fls) {
+		(h, l) := scthole(t, (hd fls).scope.first);
+		if (l > 0) return (h, l);
+	}
+	return (0, 0);
+}
+
+sctdump(scts: list of ref Section, s: string)
+{
+	fprint(ctxt.logfd, "Sctdump: %s\n", s);
+	p := 0;
+	while (scts != nil) {
+		sct := hd scts; scts = tl scts;
+		fprint(ctxt.logfd, "\tsct@%4d len=%4d len txt=%4d: %s\n",
+			p, sct.nrunes, len sct.text, contract(sct.text));
+		p += sct.nrunes;
+	}
+	fprint(ctxt.logfd, "\tend@%4d\n", p);
+}
--- /dev/null
+++ b/appl/wm/samstub.m
@@ -1,0 +1,132 @@
+Samstub: module
+{
+	PATH:		con "/dis/wm/samstub.dis";
+	SAM:		con "sam -R";
+
+	VERSION:	con 0;
+	UTFmax:		con 3;
+
+	TBLOCKSIZE:	con 512;  # largest piece of text sent to terminal ...
+	DATASIZE:	con (UTFmax*TBLOCKSIZE+30);
+				  # ... including protocol header stuff
+	SNARFSIZE:	con 4096; # maximum length of exchanged snarf buffer
+
+	# Message types
+	Error, Status, Debug: con iota;
+
+	Sammsg: adt {
+		mtype:	int;
+		mcount:	int;
+		mdata:	array of byte;
+
+		inshort:	fn(h: self ref Sammsg, n: int): int;
+		inlong:		fn(h: self ref Sammsg, n: int): int;
+		invlong:	fn(h: self ref Sammsg, n: int): big;
+		outcopy:	fn(h: self ref Sammsg, pos: int, data: array of byte);
+		outshort:	fn(h: self ref Sammsg, pos: int, s: int);
+		outlong:	fn(h: self ref Sammsg, pos: int, s: int);
+		outvlong:	fn(h: self ref Sammsg, pos: int, s: big);
+	};
+
+	Samio: adt {
+		ctl:		ref Sys->FD;	# /cmd/nnn/ctl
+		data:		ref Sys->FD;	# /cmd/nnn/data
+		buffer:		array of byte;	# buffered data read from sam
+		index:		int;
+		count:		int;		# pointers into buffer
+
+	};
+
+	init:		fn(ctxt: ref Context);
+
+	start:		fn(): (ref Samio, chan of ref Sammsg);
+	sender:		fn(s: ref Samio, c: chan of ref Sammsg);
+	receiver:	fn(s: ref Samio, c: chan of ref Sammsg);
+
+	outTs:		fn(t, s: int);
+	outTv:		fn(t: int, i: big);
+	outT0:		fn(t: int);
+	outTsl:		fn(t, m, l: int);
+	outTslS:	fn(t, s1, l1: int, s: string);
+	outTsll:	fn(t, m, l1, l2: int);
+
+	cleanout:	fn();
+	close:		fn(win, tag: int);
+	cut:		fn(t: ref Text, fl: ref Flayer);
+	findhole:	fn(t: ref Text): (int, int);
+	grow:		fn(t: ref Text, l1, l2: int);
+	horigin:	fn(m, l: int);
+	inmesg:		fn(h: ref Sammsg): int;
+	keypress:	fn(key: string);
+	look:		fn(t: ref Text, fl: ref Flayer);
+	menuins:	fn(p: int, s: string, t: ref Text, tg: int);
+	newtext:	fn(tag, tp: int): int;
+	paste:		fn(t: ref Text, fl: ref Flayer);
+	scrollto:	fn(fl: ref Flayer, where: int);
+	sctget:		fn(scts: list of ref Section, p1, p2: int): string;
+	sctgetlines:	fn(scts: list of ref Section, p, n: int):
+				 (int, string);
+	scthole:	fn(t: ref Text, f: int): (int, int);
+	sctput:		fn(scts: list of ref Section, pos: int, s: string):
+				 list of ref Section;
+	search:		fn(t: ref Text, fl: ref Flayer);
+	send:		fn(t: ref Text, fl: ref Flayer);
+	setlock:	fn();
+	snarf:		fn(t: ref Text, fl: ref Flayer);
+	startcmdfile:	fn();
+	startfile:	fn(tag: int): int;
+	startnewfile:	fn();
+	updatefls:	fn(t: ref Text, l: int, s: string);
+	zerox:		fn(t: ref Text);
+
+	Tversion,	# version
+	Tstartcmdfile,	# terminal just opened command frame
+	Tcheck,		# ask host to poke with Hcheck
+	Trequest,	# request data to fill a hole
+	Torigin,	# gimme an Horigin near here
+	Tstartfile,	# terminal just opened a file's frame
+	Tworkfile,	# set file to which commands apply
+	Ttype,		# add some characters, but terminal already knows
+	Tcut,
+	Tpaste,
+	Tsnarf,
+	Tstartnewfile,	# terminal just opened a new frame
+	Twrite,		# write file
+	Tclose,		# terminal requests file close; check mod. status
+	Tlook,		# search for literal current text
+	Tsearch,	# search for last regular expression
+	Tsend,		# pretend he typed stuff
+	Tdclick,	# double click
+	Tstartsnarf,	# initiate snarf buffer exchange
+	Tsetsnarf,	# remember string in snarf buffer
+	Tack,		# acknowledge Hack
+	Texit,		# exit
+	TMAX: con iota;
+
+	Hversion,	# version
+	Hbindname,	# attach name[0] to text in terminal
+	Hcurrent,	# make named file the typing file
+	Hnewname,	# create "" name in menu
+	Hmovname,	# move file name in menu
+	Hgrow,		# insert space in rasp
+	Hcheck0,	# see below
+	Hcheck,		# ask terminal to check whether it needs more data
+	Hunlock,	# command is finished; user can do things
+	Hdata,		# store this data in previously allocated space
+	Horigin,	# set origin of file/frame in terminal
+	Hunlockfile,	# unlock file in terminal
+	Hsetdot,	# set dot in terminal
+	Hgrowdata,	# Hgrow + Hdata folded together
+	Hmoveto,	# scrolling, context search, etc.
+	Hclean,		# named file is now 'clean'
+	Hdirty,		# named file is now 'dirty'
+	Hcut,		# remove space from rasp
+	Hsetpat,	# set remembered regular expression
+	Hdelname,	# delete file name from menu
+	Hclose,		# close file and remove from menu
+	Hsetsnarf,	# remember string in snarf buffer
+	Hsnarflen,	# report length of implicit snarf
+	Hack,		# request acknowledgement
+	Hexit,
+	HMAX: con iota;
+};
--- /dev/null
+++ b/appl/wm/samterm.m
@@ -1,0 +1,75 @@
+include "tk.m";
+include "wmlib.m";
+
+Samterm: module
+{
+
+	PATH:		con "/dis/wm/sam.dis";
+
+	Section: adt
+	{
+		nrunes:	int;
+		text:	string;		# if null, we haven't got it
+	};
+
+	Range: adt {
+		first, last: int;
+	};
+
+	Flayer: adt {
+		tag:		int;
+		t:		ref Tk->Toplevel;
+		tkwin:		string;	# tk window name
+		scope:		Range;	# part of file in range
+		dot:		Range;	# cursor position wrt file, not scope
+		width:		int;	# window width (not used yet)
+		lineheigth:	int;	# height of a single line (for resize)
+		lines:		int;	# window height in lines
+		scrollbar:	Range;	# current position of scrollbar
+		typepoint:	int;	# -1, or pos of first unsent char typed
+	};
+
+	Text: adt {
+		tag:		int;
+		lock:		int;
+		flayers:	list of ref Flayer;	# hd flayers is current
+		nrunes:		int;
+		sects:		list of ref Section;
+		state:		int;
+	};
+
+	Dirty:	con 1;
+	LDirty:	con 2;
+
+	Menu: adt {
+		tag:		int;
+		name:		string;
+		text:		ref Text;
+	};
+
+	Context: adt {
+		ctxt:		ref Draw->Context;
+		tag:		int;	# globally unique tag generator
+		lock:		int;	# global lock
+
+		keysel:		array of chan of string;
+		scrollsel:	array of chan of string;
+		buttonsel:	array of chan of string;
+		menu2sel:	array of chan of string;
+		menu3sel:	array of chan of string;
+		titlesel:	array of chan of string;
+		flayers:	array of ref Flayer;
+
+		menus:		array of ref Menu;
+		texts:		array of ref Text;
+
+		cmd:		ref Text;	# sam command window
+		which:		ref Flayer;	# current flayer (sam or work)
+		work:		ref Flayer;	# current work flayer
+
+		pgrp:		int;		# process group
+		logfd:		ref FD;
+	};
+
+	init:		fn(ctxt: ref Draw->Context, args: list of string);
+};
--- /dev/null
+++ b/appl/wm/samtk.b
@@ -1,0 +1,688 @@
+implement Samtk;
+
+include "sys.m";
+sys: Sys;
+sprint, FD: import sys;
+
+include "draw.m";
+draw:	Draw;
+
+include "samterm.m";
+Context, Flayer, Text, Section: import Samterm;
+
+include "tkclient.m";
+
+include "samtk.m";
+
+ctxt: ref Context;
+
+tk:	Tk;
+tkclient:	Tkclient;
+
+tksam1 := array[] of {
+	"frame .w",
+	"scrollbar .w.s -command {send scroll}",
+	"text .w.t -width 80w -height 8h",
+	"pack .w.s -side left -fill y",
+	"pack .w.t -fill both -expand 1",
+	"pack .Wm_t -fill x",
+	"pack .w -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+tkwork1 := array[] of {
+	"frame .w",
+	"scrollbar .w.s -command {send scroll}",
+	"text .w.t -width 80w -height 20h",
+	"pack .w.s -side left -fill y",
+	"pack .w.t -fill both -expand 1",
+	"pack .Wm_t -fill x",
+	"pack .w -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+tkcmdlist := array[] of {
+	"bind .w.t <Key> {send keys {%A}}",
+	"bind .w.t <Key-\b> {send keys {%A}}",
+	"bind .w.s <ButtonRelease-1> +{send scroll %s %b %y}",
+	"bind .w.t <ButtonPress-1> +{send button1 %s %b %x %y}",
+	"bind .w.t <ButtonRelease-1> +{send button1 %s %b %x %y}",
+	"bind .w.t <Double-ButtonPress-1> {send button1 2 %b %x %y}",
+	"bind .w.t <Double-ButtonRelease-1> {send button1 3 %b %x %y}",
+	"bind .w.t <ButtonPress-2> {.m2 post %x %y; grab set .m2}",
+	"bind .w.t <ButtonPress-3> {.m3 post %x %y; grab set .m3}",
+	"bind . <Configure> {send titlesel resize}",
+	"focus .w.t",
+	"update"
+};
+
+menuidx := array[2] of {"0","0"};
+
+init(c: ref Context)
+{
+	ctxt = c;
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+
+	tkclient = load Tkclient Tkclient->PATH;
+	tkclient->init();
+
+	scrollpos = scrolllines = 0;
+}
+
+x := 10;
+y := 10;
+
+newflayer(tag, tp: int): ref Flayer
+{
+	if (ctxt.which != nil) {
+		tk->cmd(ctxt.which.t,
+			".Wm_t.title configure -background blue; update");
+	}
+	(t, cmdc) := tkclient->toplevel(ctxt.ctxt.screen, "-borderwidth 1 -relief raised", "SamTerm", Tkclient->Appl);
+	tk->cmd(t, ". configure -x "+string x+" -y "+string y+"; update");
+
+	if (x == 10 && y == 10) {
+		y = 200;
+	} else {
+		x += 40;
+		y += 40;
+	}
+
+	n := chanadd();
+	ctxt.titlesel[n] = cmdc;
+	tk->namechan(t, ctxt.menu3sel[n], "menu3");
+	tk->namechan(t, ctxt.menu2sel[n], "menu2");
+	tk->namechan(t, ctxt.buttonsel[n], "button1");
+	tk->namechan(t, ctxt.keysel[n], "keys");
+	tk->namechan(t, ctxt.scrollsel[n], "scroll");
+	tk->namechan(t, ctxt.titlesel[n], "titlesel");
+
+	lines: int;
+	if (tp) {
+		lines = 8;
+		tkclient->tkcmds(t, tksam1);
+		mkmenu2c(t);
+	} else {
+		lines = 20;
+		tkclient->tkcmds(t, tkwork1);
+		mkmenu2(t);
+	}
+	mkmenu3(t);
+	tkclient->tkcmds(t, tkcmdlist);
+
+	f := ref Flayer(
+		tag,		# tag
+		t,		# t
+		"SamTerm",	# tkwin
+		(0, 0),		# scope
+		(0, 0),		# dot
+		int tk->cmd(t, ".w.t cget actwidth"),		# screen width
+		int tk->cmd(t, ".w.t cget actheight") / lines,	# lineheigth
+		lines,		# lines
+		(0, 1),		# scrollbar
+		-1		# typepoint
+	);
+	ctxt.flayers[n] = f;
+	return f;
+}
+
+menu2str := array [] of {
+	"cut",
+	"paste",
+	"snarf",
+	"look",
+#	"exch",
+	"send",		# storage for last pattern
+};
+
+menu3str := array [] of {
+	"new",
+	"zerox",
+	"close",
+	"write",
+};
+
+mkmenu2c(t: ref Tk->Toplevel)
+{
+	menus := array [NMENU2+1] of string;
+
+	menus[0] = "menu .m2";
+	for (i := 0; i < NMENU2; i++) {
+		menus[i+1] = addmenuitem(2, "menu2", menu2str[i]);
+	}
+	tkclient->tkcmds(t, menus);
+}
+
+mkmenu2(t: ref Tk->Toplevel)
+{
+	menus := array [NMENU2+1] of string;
+
+	menus[0] = "menu .m2";
+	for (i := 0; i < NMENU2-1; i++) {
+		menus[i+1] = addmenuitem(2, "menu2", menu2str[i]);
+	}
+	menus[NMENU2] = addmenuitem(2, "edit", "/");
+	tkclient->tkcmds(t, menus);
+}
+
+mkmenu3(t: ref Tk->Toplevel)
+{
+	menus := array [NMENU3+len ctxt.menus+1] of string;
+
+	menus[0] = "menu .m3";
+	for (i := 0; i < NMENU3; i++) {
+		menus[i+1] = addmenuitem(3, "menu3", menu3str[i]);
+	}
+	for (i = 0; i < len ctxt.menus; i++) {
+		menus[i+NMENU3+1] = addmenuitem(3, "menu3", ctxt.menus[i].name);
+	}
+	tkclient->tkcmds(t, menus);
+}
+
+addmenuitem(d: int, m, s: string): string
+{
+	return sprint(".m%d add command -text %s -command {send %s %s}",
+		d, s, m, s);
+}
+
+menuins(pos: int, s: string)
+{
+	for (i := 0; i < len ctxt.flayers; i++)
+	   tk->cmd(ctxt.flayers[i].t,
+	      sprint(".m3 insert %d command -text %s -command {send menu3 %s}",
+		pos + NMENU3, s, s));
+}
+
+menudel(pos: int)
+{
+	for (i := 0; i < len ctxt.flayers; i++)
+	    tk->cmd(ctxt.flayers[i].t, sprint(".m3 delete %d", pos + NMENU3));
+}
+
+hsetpat(s: string)
+{
+	for (i := 0; i < len ctxt.flayers; i++) {
+	    fl := ctxt.flayers[i];
+	    if (fl.tag != ctxt.cmd.tag) {
+		tk->cmd(fl.t, ".m2 entryconfigure "
+		        + string Search
+		        + " -command {send menu2 search} -text '/" + s);
+	    }
+	}
+}
+
+lastsearchstring := "//";
+
+setmenu(num : int,c : string){
+		fl := ctxt.flayers[num];
+		(nil, l) := sys->tokenize(c, " ");
+		x1 := int hd l - 50;
+		y1 := int hd tl l - int tk->cmd(fl.t, ".m"+string num+" yposition "+menuidx[num-2]) 
+								- 10;
+		tk->cmd(fl.t, ".m"+string num+" activate "+menuidx[num-2]+
+			"; .m"+string num+" post "+string x1+" "+string y1+
+			"; grab set .m"+string num+"; update");
+}
+
+titlectl(win: int, menu: string)
+{
+	tkclient->wmctl(ctxt.flayers[win].t, menu);
+}
+
+flraise(t: ref Text, fl: ref Flayer)
+{
+	nfls: list of ref Flayer;
+
+	nfls = nil;
+	t.flayers = fl :: dellist(t.flayers, fl);
+	tk->cmd(fl.t, "raise .; focus .w.t; update");
+}
+
+dellist(fls: list of ref Flayer, fl: ref Flayer): list of ref Flayer
+{
+	if (fls == nil) return nil;
+	if (hd fls == fl) return dellist(tl fls, fl);
+	return hd fls :: dellist(tl fls, fl);
+}
+
+append(fls: list of ref Flayer, fl: ref Flayer): list of ref Flayer
+{
+	if (fls == nil) return fl :: nil;
+	return hd fls :: append(tl fls, fl);
+}
+
+focus(fl: ref Flayer)
+{
+	tk->cmd(fl.t, "focus .w.t; update");
+}
+
+newcur(t: ref Text, fl: ref Flayer)
+{
+	if (ctxt.which == fl) return;
+	flraise(t, fl);
+	ctxt.which = fl;
+	if (t != ctxt.cmd)
+		ctxt.work = fl;
+}
+
+settitle(t: ref Text, s: string)
+{
+	sd := "";
+	sz := "";
+	if (t.state & Samterm->Dirty) sd = " (Dirty)";
+	if (t != ctxt.cmd && (t.state & Samterm->LDirty)) sd = " (Modified)";
+	if (len t.flayers > 1) sz = " (Zeroxed)";
+	for (fls := t.flayers; fls != nil; fls = tl fls) {
+		fl := hd fls;
+		fl.tkwin = s;
+		tkclient->settitle(fl.t, s + sd + sz);
+		tk->cmd(fl.t, "update");
+	}
+}
+
+resize(fl: ref Flayer)
+{
+	fl.lines = int tk->cmd(fl.t, ".w.t cget actheight") / fl.lineheigth;
+}
+
+allflayers(s: string)
+{
+	for (i := 0; i < len ctxt.texts; i++)
+		for (fls := ctxt.texts[i].flayers; fls != nil; fls = tl fls) {
+			fl := hd fls;
+			tk->cmd(fl.t, s);
+		}
+}
+
+setdot(fl: ref Flayer, l1, l2: int)
+{
+	tk->cmd(fl.t, ".w.t tag remove sel 0.0 end");
+
+	fl.dot.first = l1;
+	fl.dot.last = l2;
+	if (l2 <= fl.scope.first)
+		tk->cmd(fl.t, ".w.t mark set insert 0.0");
+	else if (fl.scope.last <= l1)
+		tk->cmd(fl.t, ".w.t mark set insert end");
+	else {
+		tk->cmd(fl.t, sprint(".w.t mark set insert 0.0+%dchars",
+				l1-fl.scope.first));
+		if (l1 != l2)
+			tk->cmd(fl.t, sprint(".w.t tag add sel 0.0+%dchars 0.0+%dchars",
+				l1-fl.scope.first,
+				l2-fl.scope.first));
+	}
+	tk->cmd(fl.t, "update");
+}
+
+panic(s: string)
+{
+	stderr := sys->fildes(2);
+	sys->fprint(stderr, "Panic: %s\n", s);
+	f := sys->sprint("#p/%d/ctl", ctxt.pgrp);
+	if ((fd := sys->open(f, sys->OWRITE)) != nil)
+		sys->write(fd, array of byte "killgrp\n", 8);
+	exit;
+}
+
+whichmenu(tag: int): int
+{
+	for (i := 0; i < len ctxt.menus; i++)
+		if (ctxt.menus[i].tag == tag)
+			return i;
+	return -1;
+}
+
+whichtext(tag: int): int
+{
+	for (i := 0; i < len ctxt.texts; i++)
+		if (ctxt.texts[i].tag == tag)
+			return i;
+	return -1;
+}
+
+setscrollbar(t: ref Text, fl: ref Flayer)
+{
+	ll := real t.nrunes;
+	f1 := 0.0; f2 := 1.0;
+	if (ll != 0.0) {
+		f1 = real fl.scope.first / ll;
+		if (fl.scope.last > t.nrunes)
+			f2 = 1.0;
+		else
+			f2 = real fl.scope.last / ll;
+	}
+	fl.scrollbar = fl.scope;
+	tk->cmd(fl.t, sprint(".w.s set %f %f; update", f1, f2));
+}
+
+buttonselect(fl: ref Flayer, s: string): int
+{
+	tag := fl.tag;
+	if ((i := whichtext(tag)) < 0) panic("buttonselect: whichtext");
+	t := ctxt.texts[i];
+
+	(n, l) := sys->tokenize(s, " ");
+	if (n != 4) panic("buttonselect");
+
+	# ignore mouse down -- wait for mouse up
+	if (hd l == "1" || hd l == "3") return 0;
+
+	if (ctxt.which != fl) {
+		if (ctxt.menus[i].text != ctxt.cmd)
+			ctxt.work = fl;
+		newcur(t, fl);
+#		setdot(fl, fl.dot.first, fl.dot.first);
+		return 0;
+	}
+
+	if (hd l == "2") {
+		# Double click
+		l = tl tl l;
+		s = tk->cmd(fl.t, ".w.t index @" + hd l + "," + hd tl l);
+		fl.dot.first = fl.dot.last = coord2pos(t, fl, s);
+		return 1;
+	}
+
+	rg := tk->cmd(fl.t, ".w.t tag ranges sel");
+	if (rg == "") {
+		# Nothing selected, find insertion point
+		l = tl tl l;
+		s = tk->cmd(fl.t, ".w.t index @" + hd l + "," + hd tl l);
+		fl.dot.first = fl.dot.last = coord2pos(t, fl, s);
+	} else {
+		(n, l) = sys->tokenize(rg, " ");
+		#if (n == 4 && hd tl l == hd tl tl l)
+		#	lst := hd tl tl tl l;
+		#else if (n != 2) panic("buttonselect: tag ranges");
+		#else lst = hd tl l;
+		# We only have one contiguous selection, so, take the
+		# first as dot.first and the last as dot.last
+		fst:=hd l;
+		lst:=fst;
+		while(l!=nil){
+			lst=hd l;
+			l = tl l;
+		}
+		fl.dot.first = coord2pos(t, fl, fst);
+		fl.dot.last = coord2pos(t, fl, lst);
+		tk->cmd(fl.t, ".w.t mark set insert " + fst);
+		tk->cmd(fl.t, "update");
+	}
+	return 0;
+}
+
+coord2pos(t: ref Text, fl: ref Flayer, s: string): int
+{
+	x, y: int;
+
+	(n, l) := sys->tokenize(s, ".");
+	if (n != 2) panic("coord2pos");
+	y = (int hd l) - 1;
+	x = int hd tl l;
+	if (x == 0 && y == 0) return fl.scope.first;
+	first := fl.scope.first;
+	for (scts := t.sects; scts != nil; scts = tl scts) {
+		sct := hd scts;
+		if (first >= sct.nrunes) {
+			first -= sct.nrunes;
+			continue;
+		}
+		if (first > 0) i := first; else i = 0;
+		while (i < len sct.text) {
+			if (y) {
+				if (sct.text[i++] == '\n') y--;
+			} else {
+				if (x <= 1)
+					return fl.scope.first - first + i + x;
+				if (sct.text[i++] == '\n') panic("coord2pos");
+				x--;
+			}
+		}
+		if (len sct.text < sct.nrunes) panic("coord2pos: hole");
+		first -= sct.nrunes;
+	}
+	if (x <= 0 && y == 0) return t.nrunes;
+	panic("coord2pos: can't find");
+	return(-1);
+}
+
+scrollpos, scrolllines: int;
+
+scroll(fl: ref Flayer, s: string): (int, int)
+{
+	tag := fl.tag;
+	if ((i := whichtext(tag)) < 0) panic("scroll: whichtext");
+	t := ctxt.texts[i];
+	(n, l) := sys->tokenize(s, " ");
+	height := fl.scrollbar.last - fl.scrollbar.first;
+	length := t.nrunes;
+	case (hd l) {
+	"0" =>
+		if (n != 3) panic("scroll: format");
+		return (scrollpos, scrolllines);
+	"moveto" =>
+		if (n != 2) panic("scroll: format");
+		f := real hd tl l;
+		if (f < 0.0) f = 0.0;
+		if (f > 1.0) f = 1.0;
+		scrollpos = int (f * real length) - height/2;
+		scrolllines = 1;
+	"scroll" =>
+		if (n != 3) panic("scroll: format");
+		l = tl l;
+		n = int hd l;
+		case(hd tl l) {
+		"page" =>
+			if (n < 0) {
+				scrollpos = fl.scrollbar.first;
+				scrolllines = fl.lines;
+				break;
+			}
+			scrollpos = fl.scrollbar.last;
+			scrolllines = 0;
+		"unit" =>
+			if (n < 0) {
+				scrollpos = fl.scrollbar.first - 1;
+				scrolllines = 1;
+				break;
+			}
+			(p, q) := rasplines(t.sects, fl.scrollbar.first, 1);
+			if (p > 0) {
+				scrollpos = p;
+				scrolllines = 0;
+			} else {
+				scrollpos = fl.scrollbar.first;
+				scrolllines = 0;
+			}
+		}
+	* =>
+		panic("scroll: input");
+	}
+	if (scrollpos > length)
+		scrollpos = length;
+	if (scrollpos < 0) {
+		scrollpos = 0;
+		scrolllines = 0;
+	}
+	if (length != 0)
+		tk->cmd(fl.t, sprint(".w.s set %f %f",
+			real scrollpos / real length,
+			real (scrollpos + height) / real length));
+	else
+		tk->cmd(fl.t, ".w.s set 0.0 1.0");
+	tk->cmd(fl.t, "update");
+	return (-1, -1);
+}
+
+flclear(fl: ref Flayer)
+{
+	tk->cmd(fl.t, ".w.t delete 0.0 end");
+	tk->cmd(fl.t, "update");
+}
+
+flinsert(fl: ref Flayer, l: int, s: string)
+{
+	offset := l-fl.scope.first;
+	tk->cmd(fl.t, ".w.t insert 0.0+" + string offset + "chars '" + s);
+	setdot(fl, fl.dot.first, fl.dot.last);
+}
+
+fldelexcess(fl: ref Flayer)
+{
+	tk->cmd(fl.t, ".w.t delete " + string (fl.lines+1) + ".0 end");
+}
+
+fldelete(fl: ref Flayer, l1, l2: int)
+{
+	s: string;
+	if (l1 <= fl.scope.first) {
+		if (l2 >= fl.scope.last) {
+			s = sprint(".w.t delete 0.0 end");
+			fl.scope.first = fl.scope.last = l1;
+		} else {
+			s = sprint(".w.t delete 0.0 0.0+%dchars",
+				l2 - fl.scope.first);
+			fl.scope.last -= l2 - l1;
+			fl.scope.first = l1;
+		}
+	} else {
+		if (l2 >= fl.scope.last) {
+			s = sprint(".w.t delete 0.0+%dchars end",
+				l1 - fl.scope.first);
+			fl.scope.last = l1;
+		} else {
+			s = sprint(".w.t delete 0.0+%dchars 0.0+%dchars",
+				l1 - fl.scope.first, l2 - fl.scope.first);
+			fl.scope.last -= l2 - l1;	
+		}
+	}
+	if (fl.dot.first >= l2) fl.dot.first -= l2-l1;
+	else if (fl.dot.first > l1) fl.dot.first = l1;
+	if (fl.dot.last >= l2) fl.dot.last -= l2-l1;
+	else if (fl.dot.last > l1) fl.dot.last = l1;
+	tk->cmd(fl.t, s);
+	setdot(fl, fl.dot.first, fl.dot.last);
+	tk->cmd(fl.t, "update");
+}
+
+# Calculate position forward or backward nlines lines from pos.
+# If lines > 0 count forward, if lines < 0 count backward.\
+# Returns a pair, (position, nlines).  Nlines is the remaining
+# number of lines to be found.  If non-zero, beginning or end of
+# rasp was encountered while still counting, or a hole was
+# encountered.  In the former case, position will be 0 or nrunes,
+# in the latter case, position will be set to -1.
+# To search to the beginning of the current line, set nlines to -1;
+
+rasplines(scts: list of ref Section, pos, nlines: int): (int, int)
+{
+	p, i: int;
+	if (nlines < 0) {
+		if (scts != nil) {
+			sct := hd scts; scts = tl scts;
+			if (pos > sct.nrunes) {
+				(p, nlines) =
+				    rasplines(scts, pos - sct.nrunes, nlines);
+				if (p < 0) return (p, nlines);
+				pos = p + sct.nrunes;
+				if (nlines == 0) return (pos, 0);
+			}
+			if (pos > len sct.text) return (-1, nlines);
+			for (p = pos-1; p >= 0; p--) {
+				if (sct.text[p] == '\n') nlines++;
+				if (nlines == 0) return (p+1, 0);
+			}
+		}
+		return (0, nlines);
+	} else {
+		p = 0;
+		while (scts != nil) {
+			sct := hd scts; scts = tl scts;
+			if (pos < sct.nrunes) {
+				for (i = pos; i < len sct.text; i++) {
+					if (sct.text[i] == '\n') nlines--;
+					if (nlines == 0) return (p+i+1, 0);
+				}
+				if (i < sct.nrunes) return (-1, nlines);
+			}
+			pos -= sct.nrunes;
+			if (pos < 0) pos = 0;
+			p += sct.nrunes;
+		}
+		return (p, nlines);
+	}
+}
+
+chanadd(): int
+{
+	l := len ctxt.flayers;
+
+	keysel := array [l+1] of chan of string;
+	keysel[0:] = ctxt.keysel;
+	keysel[l] = chan of string;
+	ctxt.keysel = keysel;
+	scrollsel := array [l+1] of chan of string;
+	scrollsel[0:] = ctxt.scrollsel;
+	scrollsel[l] = chan of string;
+	ctxt.scrollsel = scrollsel;
+	buttonsel := array [l+1] of chan of string;
+	buttonsel[0:] = ctxt.buttonsel;
+	buttonsel[l] = chan of string;
+	ctxt.buttonsel = buttonsel;
+	menu2sel := array [l+1] of chan of string;
+	menu2sel[0:] = ctxt.menu2sel;
+	menu2sel[l] = chan of string;
+	ctxt.menu2sel = menu2sel;
+	menu3sel := array [l+1] of chan of string;
+	menu3sel[0:] = ctxt.menu3sel;
+	menu3sel[l] = chan of string;
+	ctxt.menu3sel = menu3sel;
+	titlesel := array [l+1] of chan of string;
+	titlesel[0:] = ctxt.titlesel;
+	titlesel[l] = chan of string;
+	ctxt.titlesel = titlesel;
+	flayers := array [l+1] of ref Flayer;
+	flayers[0:] = ctxt.flayers;
+	flayers[l] = nil;
+	ctxt.flayers = flayers;
+	return l;
+}
+
+chandel(n: int)
+{
+	l := len ctxt.flayers;
+	if (n >= l)
+		panic("chandel");
+
+	keysel := array [l-1] of chan of string;
+	keysel[0:] = ctxt.keysel[0:n];
+	keysel[n:] = ctxt.keysel[n+1:];
+	ctxt.keysel = keysel;
+	scrollsel := array [l-1] of chan of string;
+	scrollsel[0:] = ctxt.scrollsel[0:n];
+	scrollsel[n:] = ctxt.scrollsel[n+1:];
+	ctxt.scrollsel = scrollsel;
+	buttonsel := array [l-1] of chan of string;
+	buttonsel[0:] = ctxt.buttonsel[0:n];
+	buttonsel[n:] = ctxt.buttonsel[n+1:];
+	ctxt.buttonsel = buttonsel;
+	menu2sel := array [l-1] of chan of string;
+	menu2sel[0:] = ctxt.menu2sel[0:n];
+	menu2sel[n:] = ctxt.menu2sel[n+1:];
+	ctxt.menu2sel = menu2sel;
+	menu3sel := array [l-1] of chan of string;
+	menu3sel[0:] = ctxt.menu3sel[0:n];
+	menu3sel[n:] = ctxt.menu3sel[n+1:];
+	ctxt.menu3sel = menu3sel;
+	titlesel := array [l-1] of chan of string;
+	titlesel[0:] = ctxt.titlesel[0:n];
+	titlesel[n:] = ctxt.titlesel[n+1:];
+	ctxt.titlesel = titlesel;
+	flayers := array [l-1] of ref Flayer;
+	flayers[0:] = ctxt.flayers[0:n];
+	flayers[n:] = ctxt.flayers[n+1:];
+	ctxt.flayers = flayers;
+}
--- /dev/null
+++ b/appl/wm/samtk.m
@@ -1,0 +1,54 @@
+Samtk: module
+{
+
+	PATH:		con "/dis/wm/samtk.dis";
+
+	Cut,
+	Paste,
+	Snarf,
+	Look,
+#	Exch,
+	Send,
+	NMENU2: con iota;
+	Search: con Send;
+
+	New,
+	Zerox,
+	Close,
+	Write,
+	NMENU3: con iota;
+
+	None,
+	Some,
+	All: con iota;	# visibility in flayer (`some' may not be used)
+
+	init:		fn(ctxt: ref Context);
+
+	allflayers:	fn(s: string);
+	append:		fn(fls: list of ref Flayer, fl: ref Flayer):
+				list of ref Flayer;
+	buttonselect:	fn(fl: ref Flayer, s: string): int;
+	chanadd:	fn(): int;
+	chandel:	fn(n: int);
+	coord2pos:	fn(t: ref Text, fl: ref Flayer, s: string): int;
+	flclear:	fn(fl: ref Flayer);
+	fldelete:	fn(fl: ref Flayer, l1, l2: int);
+	fldelexcess:	fn(fl: ref Flayer);
+	flinsert:	fn(fl: ref Flayer, l: int, s: string);
+	flraise:	fn(t: ref Text, fl: ref Flayer);
+	focus:		fn(fl: ref Flayer);
+	hsetpat:	fn(s: string);
+	menudel:	fn(pos: int);
+	menuins:	fn(pos: int, s: string);
+	newcur:		fn(t: ref Text, fl: ref Flayer);
+	newflayer:	fn(tag, tp: int): ref Flayer;
+	panic:		fn(s: string);
+	resize:		fn(fl: ref Flayer);
+	scroll:		fn(fl: ref Flayer, s: string): (int, int);
+	setdot:		fn(fl: ref Flayer, l1, l2: int);
+	setscrollbar:	fn(t: ref Text, fl: ref Flayer);
+	settitle:	fn(t: ref Text, s: string);
+	titlectl:	fn(win: int, menu: string);
+	whichmenu:	fn(tag: int): int;
+	whichtext:	fn(tag: int): int;
+};
--- /dev/null
+++ b/appl/wm/sendmail.b
@@ -1,0 +1,652 @@
+implement WmSendmail;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context: import draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+WmSendmail: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+srv: Sys->Connection;
+main: ref Toplevel;
+ctxt: ref Context;
+username: string;
+
+mail_cfg := array[] of {
+	"frame .top",
+	"label .top.l -bitmap email.bit",
+	"frame .top.con",
+	"frame .top.con.b",
+	"button .top.con.b.con -bitmap mailcon -command {send msg connect}",
+	"bind .top.con.b.con <Enter> +{.top.status configure -text {connect/disconnect to mail server}}",
+	"button .top.con.b.send -bitmap maildeliver -command {send msg send}",
+	"bind .top.con.b.send <Enter> +{.top.status configure -text {deliver mail}}",
+
+	"button .top.con.b.nocc -bitmap mailnocc -command {.hdr.e.cc delete 0 end}",
+	"bind .top.con.b.nocc <Enter> +{.top.status configure -text {no carbon copy}}",
+
+	"button .top.con.b.new -bitmap mailnew -command {send msg new}",
+	"bind .top.con.b.new <Enter> +{.top.status configure -text {start a new message}}",
+	"button .top.con.b.save -bitmap mailsave -command {send msg save}",
+	"bind .top.con.b.save <Enter> +{.top.status configure -text {save message}}",
+	"pack .top.con.b.con .top.con.b.send .top.con.b.nocc .top.con.b.new .top.con.b.save -padx 2 -side left",
+	"label .top.status -text {not connected ...} -anchor w",
+	"pack .top.l -side left",
+	"pack .top.con -side left -padx 10",
+	"pack .top.con.b .top.status -in .top.con -fill x -expand 1",
+	"frame .hdr",
+	"frame .hdr.l",
+	"frame .hdr.e",
+	"label .hdr.l.mt -text {Mail To:}",
+	"label .hdr.l.cc -text {Mail CC:}",
+	"label .hdr.l.sb -text {Subject:}",
+	"pack .hdr.l.mt .hdr.l.cc .hdr.l.sb -fill y -expand 1",
+	"entry .hdr.e.mt -bg white",
+	"entry .hdr.e.cc -bg white",
+	"entry .hdr.e.sb -bg white",
+	"bind .hdr.e.mt <Key-\n> {}",
+	"bind .hdr.e.cc <Key-\n> {}",
+	"bind .hdr.e.sb <Key-\n> {}",
+	"pack .hdr.e.mt .hdr.e.cc .hdr.e.sb -fill x -expand 1",
+	"pack .hdr.l -side left -fill y",
+	"pack .hdr.e -side left -fill x -expand 1",
+	"frame .body",
+	"scrollbar .body.scroll -command {.body.t yview}",
+	"text .body.t -width 15c -height 7c -yscrollcommand {.body.scroll set} -bg white",
+	"pack .body.t -side left -expand 1 -fill both",
+	"pack .body.scroll -side left -fill y",
+	"pack .top -anchor w -padx 5",
+	"pack .hdr -fill x -anchor w -padx 5 -pady 5",
+	"pack .body -expand 1 -fill both -padx 5 -pady 5",
+	"pack .b -padx 5 -pady 5 -fill x",
+	"pack propagate . 0",
+	"update"
+};
+
+con_cfg := array[] of {
+	"frame .b",
+	"button .b.ok -text {Connect} -command {send cmd ok}",
+	"button .b.can -text {Cancel} -command {send cmd can}",
+	"pack .b.ok .b.can -side left -fill x -padx 10 -pady 10 -expand 1",
+	"frame .l",
+	"label .l.h -text {Mail Server:} -anchor w",
+	"label .l.u -text {User Name:} -anchor w",
+	"pack .l.h .l.u -fill both -expand 1",
+	"frame .e",
+	"entry .e.h -width 30w",
+	"entry .e.u -width 30w",
+	"pack .e.h .e.u -fill x",
+	"frame .f -borderwidth 2 -relief raised",
+	"pack .l .e -fill both -expand 1 -side left -in .f",
+	"bind .e.h <Key-\n> {send cmd ok}",
+	"bind .e.u <Key-\n> {send cmd ok}",
+};
+
+con_pack := array[] of {
+	"pack .f",
+	"pack .b -fill x -expand 1",
+	"focus .e.u",
+	"update",
+};
+
+new_cmd := array[] of {
+	".hdr.e.mt delete 0 end",
+	".hdr.e.cc delete 0 end",
+	".hdr.e.sb delete 0 end",
+	".body.t delete 1.0 end",
+	".body.t see 1.0",
+	"update"
+};
+
+init(xctxt: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (xctxt == nil) {
+		sys->fprint(sys->fildes(2), "sendmail: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+
+	ctxt = xctxt;
+
+	tkclient->init();
+	dialog->init();
+	selectfile->init();
+
+	tkargs := "";
+	argv = tl argv;
+	if(argv != nil) {
+		tkargs = hd argv;
+		argv = tl argv;
+	}
+
+	titlectl: chan of string;
+	(main, titlectl) = tkclient->toplevel(ctxt, tkargs,
+				"MailStop: Sender", Tkclient->Appl);
+
+	msg := chan of string;
+	tk->namechan(main, msg, "msg");
+
+	for (c:=0; c<len mail_cfg; c++)
+		tk->cmd(main, mail_cfg[c]);
+	tkclient->onscreen(main, nil);
+	tkclient->startinput(main, "kbd"::"ptr"::nil);
+
+	if(argv != nil)
+		fromreadmail(hd argv);
+
+	for(;;) alt {
+		s := <-main.ctxt.kbd =>
+			tk->keyboard(main, s);
+		s := <-main.ctxt.ptr =>
+			tk->pointer(main, *s);
+		s := <-main.ctxt.ctl or
+		s = <-main.wreq or
+		s = <-titlectl =>
+		if(s == "exit") {
+			if(srv.dfd == nil)
+				return;
+			status("Closing connection...");
+			smtpcmd("QUIT");
+			return;
+		}
+		tkclient->wmctl(main, s);
+	cmd := <-msg =>
+		case cmd {
+		"connect" =>
+			if(srv.dfd == nil) {
+				connect(main, 1);
+				fixbutton();
+				break;
+			}
+			disconnect();
+		"save" =>
+			save();
+		"send" =>
+			sendmail();
+		"new" =>
+			for (c=0; c<len new_cmd; c++)
+				tk->cmd(main, new_cmd[c]);
+		}
+	}
+}
+
+fixbutton()
+{
+	s := "Connect";
+	if(srv.dfd != nil)
+		s = "Disconnect";
+
+	tk->cmd(main, ".top.con configure -text "+s+"; update");
+}
+
+sendmail()
+{
+	if(srv.dfd == nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Send",
+				"You must be connected to deliver mail",
+				0, "Continue"::nil);
+		return;
+	}
+
+	mto := tk->cmd(main, ".hdr.e.mt get");
+	if(mto == "") {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Send",
+				"You must fill in the \"Mail To\" entry",
+				0, "Continue (nothing sent)"::nil);
+		return;
+	}
+
+	if(tk->cmd(main, ".body.t index end") == "1.0") {
+		opt := "Cancel" :: "Send anyway" :: nil;
+		if(dialog->prompt(ctxt, main.image, "warning -fg yellow", "Send",
+				"The body of the mail is empty", 0, opt) == 0)
+			return;
+	}
+
+	(err, s) := smtpcmd("MAIL FROM:<"+username+">");
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Send",
+				"Failed to specify FROM correctly:\n"+err,
+				0, "Continue (nothing sent)"::nil);
+		return;
+	}
+	status(s);
+	(err, s) = smtpcmd("RCPT TO:<"+mto+">");
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Send",
+				"Failed to specify TO correctly:\n"+err,
+				0, "Continue (nothing sent)"::nil);
+		return;
+	}
+	status(s);
+	cc := tk->cmd(main, ".hdr.e.cc get");
+	if(cc != nil) {
+		(nil, l) := sys->tokenize(cc, "\t ,");
+		while(l != nil) {
+			copy := hd l;
+			(err, s) = smtpcmd("RCPT TO:<"+copy+">");
+			if(err != nil) {
+				dialog->prompt(ctxt, main.image, "error -fg red", "Send",
+					"Carbon copy to "+copy+"failed:\n"+err,
+					0, "Continue (nothing sent)"::nil);
+			}
+		}
+	}
+	(err, s) = smtpcmd("DATA");
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Send",
+				"Failed to enter DATA mode:\n"+err,
+				0, "Continue (nothing sent)"::nil);
+		return;
+	}
+
+	sub := tk->cmd(main, ".hdr.e.sb get");
+	if(sub != nil)
+		sys->fprint(srv.dfd, "Subject: %s\n", sub);
+
+	b := array of byte tk->cmd(main, ".body.t get 1.0 end");
+	n := sys->write(srv.dfd, b, len b);
+	b = nil;
+	if(n < 0) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Send",
+				"Error writing server:\n"+sys->sprint("%r"),
+				0, "Abort (partial send)"::nil);
+		return;
+	}
+	(err, s) = smtpcmd("\r\n.");
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Send",
+				"Failed to terminate message:\n"+err,
+				0, "Abort (partial send)"::nil);
+		return;
+	}
+	status(s);
+}
+
+save()
+{
+	mto := tk->cmd(main, ".hdr.e.to get");
+	if(mto == "") {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Save",
+				"No message to save",
+				0, "Dismiss"::nil);
+		return;
+	}
+
+	pat := list of {
+		"*.letter (Saved mail)",
+		"* (All files)"
+	};
+
+	fname: string;
+	fd: ref Sys->FD;
+
+	for(;;) {
+		fname = selectfile->filename(ctxt, main.image, "Save in Mailbox", pat,
+					  "/usr/"+rf("/dev/user")+"/mail");
+		if(fname == nil)
+			return;
+
+		fd = sys->create(fname, sys->OWRITE, 8r660);
+		if(fd != nil)
+			break;
+		r := dialog->prompt(ctxt, main.image, "error -fg red", "Save",
+			"Failed to create "+sys->sprint("%s\n%r", fname),
+			0, "Retry"::"Cancel"::nil);
+		if(r > 0)
+			return;
+	}
+
+	r := sys->fprint(srv.dfd, "Mail To: %s\n", mto);
+	cc := tk->cmd(main, ".hdr.e.cc get");
+	if(cc != nil)
+		r += sys->fprint(srv.dfd, "Mail CC: %s\n", cc);
+	sb := tk->cmd(main, ".hdr.e.sb get");
+	if(sb != nil)
+		r += sys->fprint(srv.dfd, "Subject: %s\n\n", sb);
+
+	s := tk->cmd(main, ".body.t get 1.0 end");
+	b := array of byte s;
+	n := sys->write(fd, b, len b);
+	if(n < 0) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Save",
+			"Error writing file "+sys->sprint("%s\n%r", fname),
+			0, "Continue"::nil);
+		return;
+	}
+	status("wrote "+string(n+r)+" bytes.");
+}
+
+status(msg: string)
+{
+	tk->cmd(main, ".top.status configure -text {"+msg+"}; update");
+}
+
+disconnect()
+{
+	(err, s) := smtpcmd("QUIT");
+	srv.dfd = nil;
+	fixbutton();
+	if(err != nil) {
+		dialog->prompt(ctxt, main.image, "error -fg red", "Disconnect",
+					"Server problem:\n"+err,
+				0, "Dismiss"::nil);
+		return;
+	}
+	status(s);
+}
+
+connect(parent: ref Toplevel, interactive: int)
+{
+	(t, conctl) := tkclient->toplevel(ctxt, postposn(parent),
+					"Connection Parameters", 0);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	for (c:=0; c<len con_cfg; c++)
+		tk->cmd(t, con_cfg[c]);
+
+	username = rf("/dev/user");
+	s := rf("/usr/"+username+"/mail/smtpserver");
+	if(s != "")
+		tk->cmd(t, ".e.h insert 0 '"+s);
+
+	s = rf("/usr/"+username+"/mail/domain");
+	if(s != nil)
+		username += "@"+s;
+
+	u := tk->cmd(t, ".e.u get");
+	if(u == "")
+		tk->cmd(t, ".e.u insert 0 '"+username);
+
+	if(interactive == 0 && checkthendial(t) != 0)
+		return;
+
+	for (c=0; c<len con_pack; c++)
+		tk->cmd(t, con_pack[c]);
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+		ss := <-t.ctxt.kbd =>
+			tk->keyboard(t, ss);
+		ss := <-t.ctxt.ptr =>
+			tk->pointer(t, *ss);
+		ss := <-t.ctxt.ctl or
+		ss = <-t.wreq or
+		ss = <-conctl =>
+			if (ss == "exit")
+				return;
+			tkclient->wmctl(t, ss);
+	s = <-cmd =>
+		if(s == "can")
+			return;
+		if(checkthendial(t) != 0)
+			return;
+		status("not connected");
+	}
+	srv.dfd = nil;
+}
+
+checkthendial(t: ref Toplevel): int
+{
+	server := tk->cmd(t, ".e.h get");
+	if(server == "") {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				"You must supply a server address",
+				0, "Continue"::nil);
+		return 0;
+	}
+	user := tk->cmd(t, ".e.u get");
+	if(user == "") {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				"You must supply a user name",
+				0, "Continue"::nil);
+		return 0;
+	}
+	if(dom(user) == "") {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				"The user name must contain an '@'",
+				0, "Continue"::nil);
+		return 0;
+	}
+	return dialer(t, server, user);
+}
+
+dialer(t: ref Toplevel, server, user: string): int
+{
+	ok: int;
+
+	status("dialing server...");
+	(ok, srv) = sys->dial(netmkaddr(server, nil, "25"), nil);
+	if(ok < 0) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				"The following error occurred while\n"+
+				 "dialing the server: "+sys->sprint("%r"),
+				0, "Continue"::nil);
+		return 0;
+	}
+	status("connected...");
+	(err, s) := smtpresp();
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				"An error occurred during sign on.\n"+err,
+				0, "Continue"::nil);
+		return 0;
+	}
+	status(s);
+	(err, s) = smtpcmd("HELO "+dom(user));
+	if(err != nil) {
+		dialog->prompt(ctxt, t.image, "error -fg red", "Connect",
+				"An error occurred during login.\n"+err,
+				0, "Continue"::nil);
+		return 0;
+	}
+	status("ready to send...");
+	return 1;
+}
+
+rf(file: string): string
+{
+	fd := sys->open(file, sys->OREAD);
+	if(fd == nil)
+		return "";
+
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return "";
+
+	return string buf[0:n];	
+}
+
+postposn(parent: ref Toplevel): string
+{
+	x := int tk->cmd(parent, ".top.con cget -actx");
+	y := int tk->cmd(parent, ".top.con cget -acty");
+	h := int tk->cmd(parent, ".top.con cget -height");
+
+	return "-x "+string(x-2)+" -y "+string(y+h+2);
+}
+
+dom(name: string): string
+{
+	for(i := 0; i < len name; i++)
+		if(name[i] == '@')
+			return name[i+1:];
+	return nil;
+}
+
+fromreadmail(hdr: string)
+{
+	(nil, l) := sys->tokenize(hdr, "\n");
+	while(l != nil) {
+		s := hd l;
+		l = tl l;
+		n := match(s, "subject: ");
+		if(n != nil) {
+			tk->cmd(main, ".hdr.e.sb insert end '"+n);
+			continue;
+		}
+		n = match(s, "cc: ");
+		if(n != nil) {
+			tk->cmd(main, ".hdr.e.cc insert end '"+n);
+			continue;
+		}
+		n = match(s, "from: ");
+		if(n != nil) {
+			n = extract(n);
+			tk->cmd(main, ".hdr.e.mt insert end '"+n);
+		}
+	}
+	connect(main, 0);
+}
+
+extract(name: string): string
+{
+	for(i := 0; i < len name; i++) {
+		if(name[i] == '<') {
+			for(j := i+1; j < len name; j++)
+				if(name[j] == '>')
+					break;
+			return name[i+1:j];
+		}
+	}
+	for(i = 0; i < len name; i++)
+		if(name[i] == ' ')
+			break;
+	return name[0:i];
+}
+
+lower(c: int): int
+{
+	if(c >= 'A' && c <= 'Z')
+		c = 'a' + (c - 'A');
+	return c;
+}
+
+match(text, pat: string): string
+{
+	for(i := 0; i < len pat; i++) {
+		c := text[i];
+		p := pat[i];
+		if(c != p && lower(c) != p)
+			return "";
+	}
+	return text[i:];
+}
+
+#
+# Talk SMTP
+#
+smtpcmd(cmd: string): (string, string)
+{
+	cmd += "\r\n";
+#	sys->print("->%s", cmd);
+	b := array of byte cmd;
+	l := len b;
+	n := sys->write(srv.dfd, b, l);
+	if(n != l)
+		return ("send to server:"+sys->sprint("%r"), nil);
+
+	return smtpresp();
+}
+
+smtpresp(): (string, string)
+{
+	s := "";
+	i := 0;
+	lastc := 0;
+	for(;;) {
+		c := smtpgetc();
+		if(c == -1)
+			return ("read from server:"+sys->sprint("%r"), nil);
+		if(lastc == '\r' && c == '\n')
+			break;
+		s[i++] = c;
+		lastc = c;
+	}
+#	sys->print("<-%s\n", s);
+	if(i < 3)
+		return ("short read from server", nil);
+	s = s[0:i-1];
+	case s[0] {
+	'1' or '2' or '3' =>
+		i = 3;
+		while(s[i] == ' ' && i < len s)
+			i++;
+		return (nil, s[i:]);
+	'4'or '5' =>
+		i = 3;
+		while(s[i] == ' ' && i < len s)
+			i++;
+		return (s[i:], nil);
+	 * =>
+		return ("invalid server response", nil);
+	}
+}
+
+Iob: adt
+{
+	nbyte:	int;
+	posn:	int;
+	buf:	array of byte;
+};
+smtpbuf: Iob;
+
+smtpgetc(): int
+{
+	if(smtpbuf.nbyte > 0) {
+		smtpbuf.nbyte--;
+		return int smtpbuf.buf[smtpbuf.posn++];
+	}
+	if(smtpbuf.buf == nil)
+		smtpbuf.buf = array[512] of byte;
+
+	smtpbuf.posn = 0;
+	n := sys->read(srv.dfd, smtpbuf.buf, len smtpbuf.buf);
+	if(n < 0)
+		return -1;
+
+	smtpbuf.nbyte = n-1;
+	return int smtpbuf.buf[smtpbuf.posn++];
+}
+
+netmkaddr(addr, net, svc: string): string
+{
+	if(net == nil)
+		net = "net";
+	(n, l) := sys->tokenize(addr, "!");
+	if(n <= 1){
+		if(svc== nil)
+			return sys->sprint("%s!%s", net, addr);
+		return sys->sprint("%s!%s!%s", net, addr, svc);
+	}
+	if(svc == nil || n > 2)
+		return addr;
+	return sys->sprint("%s!%s", addr, svc);
+}
--- /dev/null
+++ b/appl/wm/sh.b
@@ -1,0 +1,874 @@
+implement WmSh;
+
+include "sys.m";
+	sys: Sys;
+	FileIO: import sys;
+
+include "draw.m";
+	draw: Draw;
+	Context, Rect: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include	"plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg: import plumbmsg;
+
+include "workdir.m";
+
+include "string.m";
+	str: String;
+
+include "arg.m";
+
+WmSh: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+Command: type WmSh;
+
+BSW:		con 23;		# ^w bacspace word
+BSL:		con 21;		# ^u backspace line
+EOT:		con 4;		# ^d end of file
+ESC:		con 27;		# hold mode
+
+# XXX line-based limits are inadequate - memory is still
+# blown if a client writes a very long line.
+HIWAT:	con 2000;	# maximum number of lines in transcript
+LOWAT:	con 1500;	# amount to reduce to after high water
+
+Name:	con "Shell";
+
+Rdreq: adt
+{
+	off:	int;
+	nbytes:	int;
+	fid:	int;
+	rc:	chan of (array of byte, string);
+};
+
+shwin_cfg := array[] of {
+	"menu .m",
+	".m add command -text noscroll -command {send edit noscroll}",
+	".m add command -text cut -command {send edit cut}",
+	".m add command -text paste -command {send edit paste}",
+	".m add command -text snarf -command {send edit snarf}",
+	".m add command -text send -command {send edit send}",
+	"frame .b -bd 1 -relief ridge",
+	"frame .ft -bd 0",
+	"scrollbar .ft.scroll -command {send scroll t}",
+	"text .ft.t -bd 1 -relief flat -yscrollcommand {send scroll s} -bg white -selectforeground black -selectbackground #CCCCCC",
+	".ft.t tag configure sel -relief flat",
+	"pack .ft.scroll -side left -fill y",
+	"pack .ft.t -fill both -expand 1",
+	"pack .Wm_t -fill x",
+	"pack .b -anchor w -fill x",
+	"pack .ft -fill both -expand 1",
+	"focus .ft.t",
+	"bind .ft.t <Key> {send keys {%A}}",
+	"bind .ft.t <Control-d> {send keys {%A}}",
+	"bind .ft.t <Control-h> {send keys {%A}}",
+	"bind .ft.t <Control-w> {send keys {%A}}",
+	"bind .ft.t <Control-u> {send keys {%A}}",
+	"bind .ft.t <Button-1> +{send but1 pressed}",
+	"bind .ft.t <Double-Button-1> +{send but1 pressed}",
+	"bind .ft.t <ButtonRelease-1> +{send but1 released}",
+	"bind .ft.t <ButtonPress-2> {send but2 %X %Y}",
+	"bind .ft.t <Motion-Button-2-Button-1> {}",
+	"bind .ft.t <Motion-ButtonPress-2> {}",
+	"bind .ft.t <ButtonPress-3> {send but3 pressed}",
+	"bind .ft.t <ButtonRelease-3> {send but3 released %x %y}",
+	"bind .ft.t <Motion-Button-3> {}",
+	"bind .ft.t <Motion-Button-3-Button-1> {}",
+	"bind .ft.t <Double-Button-3> {}",
+	"bind .ft.t <Double-ButtonRelease-3> {}",
+};
+
+rdreq: list of Rdreq;
+menuindex := "0";
+holding := 0;
+haskbdfocus := 0;
+plumbed := 0;
+rawon := 0;
+rawinput := "";
+scrolling := 1;
+partialread: array of byte;
+cwd := "";
+width, height, font: string;
+
+events: list of string;
+evrdreq: list of Rdreq;
+winname: string;
+
+badmod(p: string)
+{
+	sys->print("wm/sh: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+
+	str = load String String->PATH;
+	if (str == nil)
+		badmod(String->PATH);
+
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmod(Arg->PATH);
+	arg->init(argv);
+
+	plumbmsg = load Plumbmsg Plumbmsg->PATH;
+
+	sys->pctl(Sys->FORKNS | Sys->NEWPGRP | Sys->FORKENV, nil);
+
+	tkclient->init();
+	if (ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+	if(ctxt == nil){
+		sys->fprint(sys->fildes(2), "sh: no window context\n");
+		raise "fail:bad context";
+	}
+
+	if(plumbmsg != nil && plumbmsg->init(1, nil, 0) >= 0){
+		plumbed = 1;
+		workdir := load Workdir Workdir->PATH;
+		cwd = workdir->init();
+	}
+
+	shargs: list of string;
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'w' =>
+			width = arg->arg();
+		'h' =>
+			height = arg->arg();
+		'f' =>
+			font = arg->arg();
+		'c' =>
+			a := arg->arg();
+			if (a == nil) {
+				sys->print("usage: wm/sh [-ilxvn] [-w width] [-h height] [-f font] [-c command] [file [args...]\n");
+				raise "fail:usage";
+			}
+			shargs = a :: "-c" :: shargs;
+		'i' or 'l' or 'x' or 'v' or 'n' =>
+			shargs = sys->sprint("-%c", opt) :: shargs;
+		}
+	}
+	argv = arg->argv();
+	for (; shargs != nil; shargs = tl shargs)
+		argv = hd shargs :: argv;
+
+	winname = Name + " " + cwd;
+
+	spawn main(ctxt, argv);
+}
+
+task(t: ref Tk->Toplevel)
+{
+	tkclient->wmctl(t, "task");
+}
+
+atend(t: ref Tk->Toplevel, w: string): int
+{
+	s := cmd(t, w+" yview");
+	for(i := 0; i < len s; i++)
+		if(s[i] == ' ')
+			break;
+	return i == len s - 2 && s[i+1] == '1';
+}
+
+main(ctxt: ref Draw->Context, argv: list of string)
+{
+	(t, titlectl) := tkclient->toplevel(ctxt, "", winname, Tkclient->Appl);
+	wm := t.ctxt;
+
+	edit := chan of string;
+	tk->namechan(t, edit, "edit");
+
+	keys := chan of string;
+	tk->namechan(t, keys, "keys");
+
+	butcmd := chan of string;
+	tk->namechan(t, butcmd, "button");
+
+	event := chan of string;
+	tk->namechan(t, event, "action");
+
+	scroll := chan of string;
+	tk->namechan(t, scroll, "scroll");
+
+	but1 := chan of string;
+	tk->namechan(t, but1, "but1");
+	but2 := chan of string;
+	tk->namechan(t, but2, "but2");
+	but3 := chan of string;
+	tk->namechan(t, but3, "but3");
+	button1 := 0;
+	button3 := 0;
+
+	for (i := 0; i < len shwin_cfg; i++)
+		cmd(t, shwin_cfg[i]);
+	(menuw, nil) := itemsize(t, ".m");
+	if (font != nil) {
+		if (font[0] != '/' && (len font == 1 || font[0:2] != "./"))
+			font = "/fonts/" + font;
+		cmd(t, ".ft.t configure -font " + font);
+	}
+	cmd(t, ".ft.t configure -width 65w -height 20h");
+	cmd(t, "pack propagate . 0");
+	if(width != nil)
+		cmd(t, ". configure -width " + width);
+	if(height != nil)
+		cmd(t, ". configure -height " + height);
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "ptr" :: "kbd" :: nil);
+
+	ioc := chan of (int, ref FileIO, ref FileIO, string, ref FileIO);
+	spawn newsh(ctxt, ioc, argv);
+
+	(nil, file, filectl, consfile, shctl) := <-ioc;
+	if(file == nil || filectl == nil || shctl == nil) {
+		sys->print("newsh: shell cons creation failed\n");
+		return;
+	}
+	dummyfwrite := chan of (int, array of byte, int, Sys->Rwrite);
+	fwrite := file.write;
+
+	rdrpc: Rdreq;
+
+	# outpoint is place in text to insert characters printed by programs
+	cmd(t, ".ft.t mark set outpoint 1.0; .ft.t mark gravity outpoint left");
+
+	for(;;) alt {
+	c := <-wm.kbd =>
+		tk->keyboard(t, c);
+	m := <-wm.ptr =>
+		tk->pointer(t, *m);
+	c := <-wm.ctl or
+	c = <-t.wreq or
+	c = <-titlectl =>
+		(nil, flds) := sys->tokenize(c, " \t");
+		if(flds != nil && hd flds == "haskbdfocus" && tl flds != nil){
+			haskbdfocus = int hd tl flds;
+			setcols(t);
+		}
+		tkclient->wmctl(t, c);
+	ecmd := <-edit =>
+		editor(t, ecmd);
+		sendinput(t);
+
+	c := <-keys =>
+		char := c[1];
+		if(char == '\\')
+			char = c[2];
+		if(char != ESC)
+			cut(t, 1);
+		if(rawon){
+			if(int cmd(t, ".ft.t compare insert >= outpoint")){
+				rawinput[len rawinput] = char;
+				sendinput(t);
+				break;
+			}
+		}
+		case char {
+		* =>
+			cmd(t, ".ft.t insert insert "+c);
+		'\n' or
+		EOT =>
+			cmd(t, ".ft.t insert insert "+c);
+			sendinput(t);
+		'\b' =>
+			cmd(t, ".ft.t tkTextDelIns -c");
+		BSL =>
+			cmd(t, ".ft.t tkTextDelIns -l");
+		BSW =>
+			cmd(t, ".ft.t tkTextDelIns -w");
+		ESC =>
+			setholding(t, !holding);
+		}
+		cmd(t, ".ft.t see insert;update");
+
+	c := <-but1 =>
+		button1 = (c == "pressed");
+		button3 = 0;	# abort any pending button 3 action
+
+	c := <-but2 =>
+		if(button1){
+			cut(t, 1);
+			cmd(t, "update");
+			break;
+		}
+		(nil, l) := sys->tokenize(c, " ");
+		x := int hd l - menuw/2;
+		y := int hd tl l - int cmd(t, ".m yposition "+menuindex) - 10;
+		cmd(t, ".m activate "+menuindex+"; .m post "+string x+" "+string y+
+			"; update");
+		button3 = 0;	# abort any pending button 3 action
+
+	c := <-but3 =>
+		if(c == "pressed"){
+			button3 = 1;
+			if(button1){
+				paste(t);
+				sendinput(t);
+				cmd(t, "update");
+			}
+			break;
+		}
+		if(plumbed == 0 || button3 == 0 || button1 != 0)
+			break;
+		button3 = 0;
+		# plumb message triggered by release of button 3
+		(nil, l) := sys->tokenize(c, " ");
+		x := int hd tl l;
+		y := int hd tl tl l;
+		index := cmd(t, ".ft.t index @"+string x+","+string y);
+		selindex := cmd(t, ".ft.t tag ranges sel");
+		if(selindex != "")
+			insel := cmd(t, ".ft.t compare sel.first <= "+index)=="1" &&
+				cmd(t, ".ft.t compare sel.last >= "+index)=="1";
+		else
+			insel = 0;
+		attr := "";
+		if(insel)
+			text := tk->cmd(t, ".ft.t get sel.first sel.last");
+		else{
+			# have line with text in it
+			# now extract whitespace-bounded string around click
+			(nil, w) := sys->tokenize(index, ".");
+			charno := int hd tl w;
+			left := cmd(t, ".ft.t index {"+index+" linestart}");
+			right := cmd(t, ".ft.t index {"+index+" lineend}");
+			line := tk->cmd(t, ".ft.t get "+left+" "+right);
+			for(i=charno; i>0; --i)
+				if(line[i-1]==' ' || line[i-1]=='\t')
+					break;
+			for(j:=charno; j<len line; j++)
+				if(line[j]==' ' || line[j]=='\t')
+					break;
+			text = line[i:j];
+			attr = "click="+string (charno-i);
+		}
+		msg := ref Msg(
+			"WmSh",
+			"",
+			cwd,
+			"text",
+			attr,
+			array of byte text);
+		if(msg.send() < 0)
+			sys->fprint(sys->fildes(2), "sh: plumbing write error: %r\n");
+	c := <-butcmd =>
+		simulatetype(t, tkunquote(c));
+		sendinput(t);
+		cmd(t, "update");
+	c := <-event =>
+		events = str->append(tkunquote(c), events);
+		if (evrdreq != nil) {
+			rc := (hd evrdreq).rc;
+			rc <-= (array of byte hd events, nil);
+			evrdreq = tl evrdreq;
+			events = tl events;
+		}
+	rdrpc = <-shctl.read =>
+		if(rdrpc.rc == nil)
+			continue;
+		if (events != nil) {
+			rdrpc.rc <-= (array of byte hd events, nil);
+			events = tl events;
+		} else
+			evrdreq = rdrpc :: evrdreq;
+	(nil, data, nil, wc) := <-shctl.write =>
+		if (wc == nil)
+			break;
+		if ((err := shctlcmd(t, string data)) != nil)
+			wc <-= (0, err);
+		else
+			wc <-= (len data, nil);
+	rdrpc = <-filectl.read =>
+		if(rdrpc.rc == nil)
+			continue;
+		rdrpc.rc <-= (nil, "not allowed");
+	(nil, data, nil, wc) := <-filectl.write =>
+		if(wc == nil) {
+			# consctl closed - revert to cooked mode
+			# XXX should revert only on *last* close?
+			rawon = 0;
+			continue;
+		}
+		(nc, cmdlst) := sys->tokenize(string data, " \n");
+		if(nc == 1) {
+			case hd cmdlst {
+			"rawon" =>
+				rawon = 1;
+				rawinput = "";
+				# discard previous input
+				advance := string (len tk->cmd(t, ".ft.t get outpoint end") +1);
+				cmd(t, ".ft.t mark set outpoint outpoint+" + advance + "chars");
+				partialread = nil;
+			"rawoff" =>
+				rawon = 0;
+				partialread = nil;
+			"holdon" =>
+				setholding(t, 1);
+				cmd(t, "update");
+			"holdoff" =>
+				setholding(t, 0);
+				cmd(t, "update");
+			* =>
+				wc <-= (0, "unknown consctl request");
+				continue;
+			}
+			wc <-= (len data, nil);
+			continue;
+		}
+		wc <-= (0, "unknown consctl request");
+
+	rdrpc = <-file.read =>
+		if(rdrpc.rc == nil) {
+			(ok, nil) := sys->stat(consfile);
+			if (ok < 0)
+				return;
+			continue;
+		}
+		append(rdrpc);
+		sendinput(t);
+
+	c := <-scroll =>
+		if(c[0] == 't'){
+			cmd(t, ".ft.t yview "+c[1:]+";update");
+			if(scrolling)
+				fwrite = file.write;
+			else if(atend(t, ".ft.t"))
+				fwrite = file.write;
+			else
+				fwrite = dummyfwrite;
+		}else{
+			cmd(t, ".ft.scroll set "+c[1:]+";update");
+			if(atend(t, ".ft.t") && fwrite == dummyfwrite)
+				fwrite = file.write;
+		}
+	(nil, data, nil, wc) := <-fwrite =>
+		if(wc == nil) {
+			(ok, nil) := sys->stat(consfile);
+			if (ok < 0)
+				return;
+			continue;
+		}
+		needscroll := atend(t, ".ft.t");
+		cdata := cursorcontrol(t, string data);
+		ncdata := string len cdata + "chars;";
+		cmd(t, ".ft.t insert outpoint '"+ cdata);
+		wc <-= (len data, nil);
+		data = nil;
+		s := ".ft.t mark set outpoint outpoint+" + ncdata;
+		if(!atend(t, ".ft.t") && scrolling == 0)
+			fwrite = dummyfwrite;
+		else if(needscroll)
+			s += ".ft.t see outpoint;";
+		s += "update";
+		cmd(t, s);
+		nlines := int cmd(t, ".ft.t index end");
+		if(nlines > HIWAT){
+			s = ".ft.t delete 1.0 "+ string (nlines-LOWAT) +".0;update";
+			cmd(t, s);
+		}
+	}
+}
+
+setholding(t: ref Tk->Toplevel, hold: int)
+{
+	if(hold == holding)
+		return;
+	holding = hold;
+	if(!holding){
+		tkclient->settitle(t, winname);
+		sendinput(t);
+	}else
+		tkclient->settitle(t, winname+" (holding)");
+	setcols(t);
+}
+
+setcols(t: ref Tk->Toplevel)
+{
+	fgcol := "black";
+	if(holding){
+		if(haskbdfocus)
+			fgcol = "#000099FF";	# DMedblue
+		else
+			fgcol = "#005DBBFF";	# DGreyblue
+	}else{
+		if(haskbdfocus)
+			fgcol = "black";
+		else
+			fgcol = "#666666FF";	# dark grey
+	}
+	cmd(t, ".ft.t configure -foreground "+fgcol+" -selectforeground "+fgcol);
+	cmd(t, ".ft.t tag configure sel -foreground "+fgcol);
+}
+
+tkunquote(s: string): string
+{
+	if (s == nil)
+		return nil;
+	t: string;
+	if (s[0] != '{' || s[len s - 1] != '}')
+		return s;
+	for (i := 1; i < len s - 1; i++) {
+		if (s[i] == '\\')
+			i++;
+		t[len t] = s[i];
+	}
+	return t;
+}
+
+buttonid := 0;
+shctlcmd(win: ref Tk->Toplevel, c: string): string
+{
+	toks := str->unquoted(c);
+	if (toks == nil)
+		return "null command";
+	n := len toks;
+	case hd toks {
+	"button" or
+	"action"=>
+		# (button|action) title sendtext
+		if (n != 3)
+			return "bad usage";
+		id := ".b.b" + string buttonid++;
+		cmd(win, "button " + id + " -text " + tk->quote(hd tl toks) +
+				" -command 'send " + hd toks + " " + tk->quote(hd tl tl toks));
+		cmd(win, "pack " + id + " -side left");
+		cmd(win, "pack propagate .b 0");
+	"clear" =>
+		cmd(win, "pack propagate .b 1");
+		for (i := 0; i < buttonid; i++)
+			cmd(win, "destroy .b.b" + string i);
+		buttonid = 0;
+	"cwd" =>
+		if (n != 2)
+			return "bad usage";
+		cwd = hd tl toks;
+		winname = Name + " " + cwd;
+		tkclient->settitle(win, winname);
+	* =>
+		return "bad command";
+	}
+	cmd(win, "update");
+	return nil;
+}
+
+
+RPCread: type (int, int, int, chan of (array of byte, string));
+
+append(r: RPCread)
+{
+	t := r :: nil;
+	while(rdreq != nil) {
+		t = hd rdreq :: t;
+		rdreq = tl rdreq;
+	}
+	rdreq = t;
+}
+
+insat(t: ref Tk->Toplevel, mark: string): int
+{
+	return cmd(t, ".ft.t compare insert == "+mark) == "1";
+}
+
+insininput(t: ref Tk->Toplevel): int
+{
+	if(cmd(t, ".ft.t compare insert >= outpoint") != "1")
+		return 0;
+	return cmd(t, ".ft.t compare {insert linestart} == {outpoint linestart}") == "1";
+}
+
+isalnum(s: string): int
+{
+	if(s == "")
+		return 0;
+	c := s[0];
+	if('a' <= c && c <= 'z')
+		return 1;
+	if('A' <= c && c <= 'Z')
+		return 1;
+	if('0' <= c && c <= '9')
+		return 1;
+	if(c == '_')
+		return 1;
+	if(c > 16rA0)
+		return 1;
+	return 0;
+}
+
+cursorcontrol(t: ref Tk->Toplevel, s: string): string
+{
+	l := len s;
+	for(i := 0; i < l; i++) {
+		case s[i] {
+		    '\b' =>
+			pre := "";
+			rem := "";
+			if(i + 1 < l)
+				rem = s[i+1:];
+			if(i == 0) {	# erase existing character in line
+				if(tk->cmd(t, ".ft.t get " +
+					"{outpoint linestart} outpoint") != "")
+				    cmd(t, ".ft.t delete outpoint-1char");
+			} else {
+				if(s[i-1] != '\n')	# don't erase newlines
+					i--;
+				if(i)
+					pre = s[:i];
+			}
+			s = pre + rem;
+			l = len s;
+			i = len pre - 1;
+		    '\r' =>
+			s[i] = '\n';
+			if(i + 1 < l && s[i+1] == '\n')	# \r\n
+				s = s[:i] + s[i+1:];
+			else if(i > 0 && s[i-1] == '\n')	# \n\r
+				s = s[:i-1] + s[i:];
+			l = len s;
+		    '\0' =>
+			s[i] = Sys->UTFerror;
+		}
+	}
+	return s;
+}
+
+editor(t: ref Tk->Toplevel, ecmd: string)
+{
+	s, snarf: string;
+
+	case ecmd {
+	"scroll" =>
+		menuindex = "0";
+		scrolling = 1;
+		cmd(t, ".m entryconfigure 0 -text noscroll -command {send edit noscroll}");
+	"noscroll" =>
+		menuindex = "0";
+		scrolling = 0;
+		cmd(t, ".m entryconfigure 0 -text scroll -command {send edit scroll}");
+	"cut" =>
+		menuindex = "1";
+		cut(t, 1);
+	"paste" =>
+		menuindex = "2";
+		paste(t);
+	"snarf" =>
+		menuindex = "3";
+		if(cmd(t, ".ft.t tag ranges sel") == "")
+			break;
+		snarf = tk->cmd(t, ".ft.t get sel.first sel.last");
+		tkclient->snarfput(snarf);
+	"send" =>
+		menuindex = "4";
+		if(cmd(t, ".ft.t tag ranges sel") != ""){
+			snarf = tk->cmd(t, ".ft.t get sel.first sel.last");
+			tkclient->snarfput(snarf);
+		}else{
+			snarf = tkclient->snarfget();
+		}
+		if(snarf != "")
+			s = snarf;
+		else
+			return;
+		if(s[len s-1] != '\n' && s[len s-1] != EOT)
+			s[len s] = '\n';
+		simulatetype(t, s);
+	}
+	cmd(t, "update");
+}
+
+simulatetype(t: ref Tk->Toplevel, s: string)
+{
+	if(rawon){
+		rawinput += s;
+	}else{
+		cmd(t, ".ft.t see end; .ft.t insert end '"+s);
+		cmd(t, ".ft.t mark set insert end");
+		tk->cmd(t, ".ft.t tag remove sel sel.first sel.last");
+	}
+}
+
+cut(t: ref Tk->Toplevel, snarfit: int)
+{
+	if(cmd(t, ".ft.t tag ranges sel") == "")
+		return;
+	if(snarfit)
+		tkclient->snarfput(tk->cmd(t, ".ft.t get sel.first sel.last"));
+	cmd(t, ".ft.t delete sel.first sel.last");
+}
+
+paste(t: ref Tk->Toplevel)
+{
+	snarf := tkclient->snarfget();
+	if(snarf == "")
+		return;
+	cut(t, 0);
+	if(rawon && int cmd(t, ".ft.t compare insert >= outpoint")){
+		rawinput += snarf;
+	}else{
+		cmd(t, ".ft.t insert insert '"+snarf);
+		cmd(t, ".ft.t tag add sel insert-"+string len snarf+"chars insert");
+	}
+}
+
+sendinput(t: ref Tk->Toplevel)
+{
+	input: string;
+	if(rawon)
+		input = rawinput;
+	else
+		input = tk->cmd(t, ".ft.t get outpoint end");
+	if(rdreq == nil || (input == nil && len partialread == 0))
+		return;
+	r := hd rdreq;
+	(chars, bytes, partial) := triminput(r.nbytes, input, partialread);
+	if(bytes == nil)
+		return;	# no terminator yet
+	rdreq = tl rdreq;
+
+	alt {
+	r.rc <-= (bytes, nil) =>
+		# check that it really was sent
+		alt {
+		r.rc <-= (nil, nil) =>
+			;
+		* =>
+			return;
+		}
+	* =>
+		return;	# requester has disappeared; ignore his request and try another
+	}
+	if(rawon)
+		rawinput = rawinput[chars:];
+	else
+		cmd(t, ".ft.t mark set outpoint outpoint+" + string chars + "chars");
+	partialread = partial;
+}
+
+# read at most nr bytes from the input string, returning the number of characters
+# consumed, the bytes to be read, and any remaining bytes from a partially
+# read multibyte UTF character.
+triminput(nr: int, input: string, partial: array of byte): (int, array of byte, array of byte)
+{
+	if(nr <= len partial)
+		return (0, partial[0:nr], partial[nr:]);
+	if(holding)
+		return (0, nil, partial);
+
+	# keep the array bounds within sensible limits
+	if(nr > len input*Sys->UTFmax)
+		nr = len input*Sys->UTFmax;
+	buf := array[nr+Sys->UTFmax] of byte;
+	t := len partial;
+	buf[0:] = partial;
+
+	hold := !rawon;
+	i := 0;
+	while(i < len input){
+		c := input[i++];
+		# special case for ^D - don't read the actual ^D character
+		if(!rawon && c == EOT){
+			hold = 0;
+			break;
+		}
+
+		t += sys->char2byte(c, buf, t);
+		if(c == '\n' && !rawon){
+			hold = 0;
+			break;
+		}
+		if(t >= nr)
+			break;
+	}
+	if(hold){
+		for(j := i; j < len input; j++){
+			c := input[j];
+			if(c == '\n' || c == EOT)
+				break;
+		}
+		if(j == len input)
+			return (0, nil, partial);
+		# strip ^D when next read would read it, otherwise
+		# we'll give premature EOF.
+		if(i == j && input[i] == EOT)
+			i++;
+	}
+	partial = nil;
+	if(t > nr){
+		partial = buf[nr:t];
+		t = nr;
+	}
+	return (i, buf[0:t], partial);
+}
+
+newsh(ctxt: ref Context, ioc: chan of (int, ref FileIO, ref FileIO, string, ref FileIO),
+			args: list of string)
+{
+	pid := sys->pctl(sys->NEWFD, nil);
+
+	sh := load Command "/dis/sh.dis";
+	if(sh == nil) {
+		ioc <-= (0, nil, nil, nil, nil);
+		return;
+	}
+
+	tty := "cons."+string pid;
+
+	sys->bind("#s","/chan",sys->MBEFORE);
+	fio := sys->file2chan("/chan", tty);
+	fioctl := sys->file2chan("/chan", tty + "ctl");
+	shctl := sys->file2chan("/chan", "shctl");
+	ioc <-= (pid, fio, fioctl, "/chan/"+tty, shctl);
+	if(fio == nil || fioctl == nil || shctl == nil)
+		return;
+
+	sys->bind("/chan/"+tty, "/dev/cons", sys->MREPL);
+	sys->bind("/chan/"+tty+"ctl", "/dev/consctl", sys->MREPL);
+
+	fd0 := sys->open("/dev/cons", sys->OREAD|sys->ORCLOSE);
+	fd1 := sys->open("/dev/cons", sys->OWRITE);
+	fd2 := sys->open("/dev/cons", sys->OWRITE);
+
+	{
+		sh->init(ctxt, "sh" :: "-n" :: args);
+	}exception{
+	"fail:*" =>
+		exit;
+	}
+}
+
+cmd(top: ref Tk->Toplevel, c: string): string
+{
+	s:= tk->cmd(top, c);
+#	sys->print("* %s\n", c);
+	if (s != nil && s[0] == '!')
+		sys->fprint(sys->fildes(2), "wmsh: tk error on '%s': %s\n", c, s);
+	return s;
+}
+
+itemsize(top: ref Tk->Toplevel, item: string): (int, int)
+{
+	w := int tk->cmd(top, item + " cget -actwidth");
+	h := int tk->cmd(top, item + " cget -actheight");
+	b := int tk->cmd(top, item + " cget -borderwidth");
+	return (w+b, h+b);
+}
--- /dev/null
+++ b/appl/wm/smenu.b
@@ -1,0 +1,204 @@
+implement Smenu;
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+include "tk.m";
+	tk: Tk;
+include "smenu.m";
+
+Scrollmenu.new(t: ref Tk->Toplevel, name: string, labs: array of string, e: int, o: int): ref Scrollmenu
+{
+	if(sys == nil)
+		sys = load Sys Sys->PATH;
+	if(tk == nil)
+		tk = load Tk Tk->PATH;
+	m := ref Scrollmenu;
+	n := len labs;
+	if(n < e)
+		e = n;
+	if(o > n-e)
+		o = n-e;
+	l := 0;
+	for(i := 0; i < n; i++){
+		if(len labs[i] > l)
+			l = len labs[i];
+		i++;
+	}
+	nlabs := array[n] of string;
+	sp := string array[l] of { * => byte ' ' };
+	for(i = 0; i < n; i++)
+		nlabs[i] = labs[i] + sp[0: l - len labs[i]];
+	sch := cname(name);
+	cmd(t, "menu " + name);
+	for(i = 0; i < e; i++){
+		cmd(t, name + " add command -label {" + nlabs[o+i] + "} -command {send " + sch + " " + string i + "}");
+	}
+	# cmd(t, "bind " + name + " <ButtonPress-1> +{send " + sch + " b}");
+	# cmd(t, "bind " + name + " <ButtonRelease-1> +{send " + sch + " b}");
+	cmd(t, "bind " + name + " <Motion> +{send " + sch + " M %x %y}");
+	cmd(t, "bind " + name + " <Map> +{send " + sch + " m}");
+	cmd(t, "bind " + name + " <Unmap> +{send " + sch + " u}");
+	cmd(t, "update");
+	m.name = name;
+	m.labs = nlabs;
+	m.c = nil;
+	m.t = t;
+	m.m = e;
+	m.n = n;
+	m.o = o;
+	m.timer = 1;
+	return m;
+}
+
+Scrollmenu.post(m: self ref Scrollmenu, x: int, y: int, resc: chan of string, prefix: string)
+{
+	sync := chan of int;
+	spawn listen(m, sync, resc, prefix);
+	<- sync;
+	cmd(m.t, m.name + " post " + string x + " " + string y);
+	cmd(m.t, "update");
+}
+
+Scrollmenu.destroy(m: self ref Scrollmenu)
+{
+	if(m.c != nil){
+		m.c <-= "u";	# fake unmap message
+		m.c = nil;
+	}
+	m.name = nil;
+	m.labs = nil;
+	m.t = nil;
+}
+
+timer(t: int, sync: chan of int, c: chan of int)
+{
+	sync <-= 0;
+	for(;;){
+		alt{
+			c <-= 0 =>
+				sys->sleep(t);
+			<- sync =>
+				exit;
+		}
+	}
+}
+
+TINT: con 100;
+SEC: con 1000/TINT;
+		
+listen(m: ref Scrollmenu, sync: chan of int, resc: chan of string, prefix: string)
+{
+	timerc := chan of int;
+	cmdc := chan of string;
+	m.c = cmdc;
+	tk->namechan(m.t, cmdc, cname(m.name));
+	sync <-= 0;
+	x := y := ly := w := h := -1;
+	for(;;){
+		alt{
+			<- timerc =>
+				if(x > 0 && x < w){
+					if(y < 0 && y > -h/m.m)
+						menudir(m, -1);
+					else if(y > 0+h && y < h+h/m.m)
+						menudir(m, 1);
+				}
+			s := <- cmdc =>
+				(nil, toks) := sys->tokenize(s, " ");
+				case hd toks{
+					"M" =>
+						x = int hd tl toks;
+						y = int hd tl tl toks;
+						if(!m.timer && x > 0 && x < w){
+							mv := 0;
+							if(y < ly && y < 0)
+								mv = y/(h/m.m)-1;
+							else if(y > ly && y > h)
+								mv = (y-h)/(h/m.m)+1;
+							if(mv != 0)
+								menudirs(m, mv);
+							ly = y;
+						}
+					"m" =>
+						w = int cmd(m.t, m.name + " cget -actwidth");
+						h = int cmd(m.t, m.name + " cget -actheight");
+						ly = -1;
+						if(m.timer){
+							spawn timer(TINT, sync, timerc);
+							<- sync;
+						}
+					"u" =>
+						if(m.timer)
+							sync <-= 0;
+						m.c = nil;
+						exit;
+					* =>
+						# do not block
+						res := prefix + string (int hd toks + m.o);
+						for(t := 0; t < SEC; ){
+							if(m.timer)
+								alt{
+									resc <-=  res =>
+										t = SEC;
+									<- timerc =>
+										t++;
+								}
+							else
+								alt{
+									resc <-= res =>
+										t = SEC;
+									* =>
+										sys->sleep(TINT);
+										t++;
+								}
+						}
+				}
+		}
+	}
+}
+
+menudirs(sm: ref Scrollmenu, n: int)
+{
+	if(n < 0)
+		(a, d) := (-n, -1);
+	else
+		(a, d) = (n, 1);
+	for(i := 0; i < a; i++)
+		menudir(sm, d);
+}
+
+menudir(sm: ref Scrollmenu, d: int)
+{
+	o := sm.o;
+	n := sm.n;
+	m := sm.m;
+	if(d == -1){
+		if(o == 0)
+			return;
+		for(i := 0; i < m; i++)
+			cmd(sm.t, sm.name + " entryconfigure " + string i + " -label {" + sm.labs[o-1+i] + "}");
+		sm.o = o-1;
+	}
+	else{
+		if(o+m == n)
+			return;
+		for(i := 0; i < m; i++)
+			cmd(sm.t, sm.name + " entryconfigure " + string i + " -label {" + sm.labs[o+1+i] + "}");
+		sm.o = o+1;
+	}
+	cmd(sm.t, "update");	
+}
+
+cname(s: string): string
+{
+	return "sm_" + s + "_sm";
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "Smenu: tk error on '%s': %s\n", s, e);
+	return e;
+}
--- /dev/null
+++ b/appl/wm/smenu.m
@@ -1,0 +1,18 @@
+Smenu: module
+{
+	PATH: con "/dis/wm/smenu.dis";
+
+	Scrollmenu: adt{
+		# private data
+		m, n, o: int;
+		timer: int;
+		name: string;
+		labs: array of string;
+		c: chan of string;
+		t: ref Tk->Toplevel;
+
+		new: fn(t: ref Tk->Toplevel, name: string, labs: array of string, entries: int, origin: int): ref Scrollmenu;
+		post: fn(m: self ref Scrollmenu, x: int, y: int, resc: chan of string, prefix: string);
+		destroy: fn(m: self ref Scrollmenu);
+	};
+};
--- /dev/null
+++ b/appl/wm/snake.b
@@ -1,0 +1,373 @@
+implement Snake;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Display, Point, Screen, Image, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "keyboard.m";
+include "rand.m";
+	rand: Rand;
+include "scoretable.m";
+	scoretable: Scoretable;
+
+Snake: module{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Tick: adt{
+	dt: int;
+};
+
+DX: con 30;
+DY: con 30;
+Size: int;
+
+EMPTY, SNAKE, FOOD, CRASH: con iota;
+HIGHSCOREFILE: con "/lib/scores/snake";
+
+board: array of array of int;
+win: ref Tk->Toplevel;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "snake: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	if(tkclient == nil){
+		sys->print("sys->fildes(2), couldn't load %s: %r\n", Tkclient->PATH);
+		raise "fail:bad module";
+	}
+	tkclient->init();
+	tk = load Tk Tk->PATH;
+	rand = load Rand Rand->PATH;
+	if(rand == nil){
+		sys->fprint(sys->fildes(2), "snake: cannot load %s: %r\n", Rand->PATH);
+		raise "fail:bad module";
+	}
+	scoretable = load Scoretable Scoretable->PATH;
+	if (scoretable != nil) {
+		(ok, err) := scoretable->init(-1, readfile("/dev/user"), "snake", HIGHSCOREFILE);
+		if (ok == -1) {
+			sys->fprint(sys->fildes(2), "snake: cannot init scoretable: %s\n", err);
+			scoretable = nil;
+		}
+	}
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	ctlchan: chan of string;
+	(win, ctlchan) = tkclient->toplevel(ctxt, nil, "Snake", Tkclient->Hide);
+
+	tk->namechan(win, kch := chan of string, "kch");
+
+	cmd(win, "canvas .c -bd 2 -relief ridge");
+	cmd(win, "label .scoret -text Score:");
+	cmd(win, "label .score -text 0");
+	cmd(win, "frame .f");
+	if (scoretable != nil) {
+		cmd(win, "label .hight -text High:");
+		cmd(win, "label .high -text 0");
+		cmd(win, "pack .hight .high -in .f -side left");
+	}
+	cmd(win, "pack .score .scoret -in .f -side right");
+	cmd(win, "pack .f -side top -fill x");
+	cmd(win, "pack .c");
+	cmd(win, "bind .c <Key> {send kch %s}");
+	cmd(win, "bind . <ButtonRelease-1> {focus .c}");
+	cmd(win, "bind .Wm_t <ButtonRelease-1> +{focus .c}");
+	cmd(win, "focus .c");
+
+	Size = int cmd(win, ".c cget -actheight") / DY;
+	cmd(win, ".c configure -width " + string (Size * DX) + " -height " + string (Size * DY));
+
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+	spawn winctl(ctlchan);
+	if (len argv > 1)
+		game(kch, hd tl argv);
+
+	for(;;){
+		game(kch, nil);
+		cmd(win, ".c delete all");
+	}
+}
+
+winctl(ctlchan: chan of string)
+{
+	for(;;) alt {
+		s := <-win.ctxt.kbd =>
+			tk->keyboard(win, s);
+		s := <-win.ctxt.ptr =>
+			tk->pointer(win, *s);
+		s := <-win.ctxt.ctl or
+		s = <-win.wreq or
+		s = <-ctlchan =>
+			tkclient->wmctl(win, s);
+	}
+}
+
+board2s(board: array of array of int): string
+{
+	s := string DX + "." + string DY + ".";
+	for (y := 0; y < DY; y++)
+		for (x := 0; x < DX; x++)
+			s[len s] = board[x][y] + '0';
+	return s;
+}
+
+replayproc(replay: string, kch: chan of string, tick: chan of int, nil: ref Tick)
+{
+	i := 0;
+	while(i < len replay){
+		n := 0;
+		while(i < len replay && replay[i] >= '0' && replay[i] <= '9') {
+			n = n*10 + replay[i] - '0';
+			i++;
+		}
+		for (t := 0; t < n; t++) {
+			tick <-= 1;
+			sys->sleep(0);
+		}
+		if (i == len replay)
+			break;
+		kch <-= string replay[i];
+		i++;
+	}
+	tick <-= 1;
+	tick <-= 0;
+}
+
+game(realkch: chan of string, replay: string)
+{
+	scores := scoretable->scores();
+	if (scores != nil)
+		cmd(win, ".high configure -text " + string (hd scores).score);
+	cmd(win, ".score configure -text {0}");
+	board = array[DX] of { * => array[DY] of{* => EMPTY}};
+
+	seed := rand->rand(16r7fffffff);
+	if (replay != nil) {
+		seed = int replay;
+		for (i := 0; i < len replay; i++)
+			if (replay[i] == '.')
+				break;
+		if (i<len replay)
+			replay = replay[i+1:];
+	}
+	rand->init(seed);
+	p := Point(DX/2, DY/2);
+	dir := Point(1, 0);
+	lkey := 'r';
+	snake := array[5] of Point;
+	for(i := 0; i < len snake; i++){
+		snake[i] = p.add(dir.mul(i));
+		make(snake[i]);
+	}
+	placefood();
+	p = p.add(dir.mul(i));
+	ticki := ref Tick(100);
+	realtick := chan of int;
+
+	userkch: chan of string;
+	if(replay != nil) {
+		(userkch, realkch) = (realkch, chan of string);
+		spawn replayproc(replay, realkch, realtick, ticki);
+	} else {
+		userkch = chan of string;
+		spawn ticker(realtick, ticki);
+	}
+	cmd(win, "update");
+
+	score := 0;
+	leaveit := 0;
+	paused := 0;
+
+	log := "";
+	nticks := 0;
+	odir := dir;
+
+	dummykch := chan of string;
+	kch := realkch;
+
+	dummytick := chan of int;
+	tick := realtick;
+	for(;;){
+		alt{
+		c := <-kch =>
+			if(paused){
+				paused = 0;
+				tick = realtick;
+			}
+			kch = dummykch;
+			ndir := dir;
+			case int c{
+			Keyboard->Up =>
+				ndir = (0, -1);
+			Keyboard->Down =>
+				ndir = (0, 1);
+			Keyboard->Left =>
+				ndir = (-1, 0);
+			Keyboard->Right =>
+				ndir = (1, 0);
+			'q' =>
+				tkclient->wmctl(win, "exit");
+			'p' =>
+				paused = 1;
+				tick = dummytick;
+				kch = realkch;
+			}
+			if (!ndir.eq(dir) && !ndir.eq(dir.mul(-1))) {		# don't allow 180° turn.
+				lkey = int c;
+				dir = ndir;
+			}
+		<-tick =>
+			if(!odir.eq(dir)) {
+				log += string nticks;
+				log[len log] = lkey;
+				nticks = 0;
+				odir = dir;
+			}
+			nticks++;
+			if(leaveit){
+				ns := array[len snake + 1] of Point;
+				ns[0:] = snake;
+				snake = ns;
+				leaveit = 0;
+			} else{
+				destroy(snake[0]);
+				snake[0:] = snake[1:];
+			}
+			np := snake[len snake - 2].add(dir);
+			np.x = (np.x + DX) % DX;
+			np.y = (np.y + DY) % DY;
+			snake[len snake - 1] = np;
+			wasfood := board[np.x][np.y] == FOOD;
+			if(!make(np)){
+				cmd(win, ".c create oval " + r2s(square(np).inset(-5)) + " -fill yellow");
+				cmd(win, "update");
+				if (scoretable != nil && replay == nil) {
+					board[np.x][np.y] = CRASH;
+					log += string nticks;
+					sys->print("%d.%s\n", seed, log);
+					scoretable->setscore(score, string seed + "." + log + " " + board2s(board));
+				}
+				ticki.dt = -1;
+				while(<-tick)
+					;
+				sys->sleep(750);
+				absorb(realkch);
+				if(int <-realkch == 'q')
+					tkclient->wmctl(win, "exit");
+				return;
+			}
+			if(wasfood){
+				score++;
+				#if(score % 10 == 0){
+				#	if(ticki.dt > 0)
+				#		ticki.dt -= 5;
+				#}
+				cmd(win, ".score configure -text " + string score);
+				leaveit = 1;
+				placefood();
+			}
+			cmd(win, "update");
+			kch = realkch;
+		}
+	}
+}
+
+placefood()
+{
+	for(;;)
+		if(makefood((rand->rand(DX), rand->rand(DY))))
+			return;
+}
+
+make(p: Point): int
+{
+	# b := board[p.x][p.y];
+	if(board[p.x][p.y] == SNAKE)
+		return 0;
+	cmd(win, ".c create rectangle " + r2s(square(p)) +
+			" -fill blue -outline {} -tags b." + string p.x + "." + string p.y);
+	board[p.x][p.y] = SNAKE;
+	return 1;
+}
+
+makefood(p: Point): int
+{
+	b := board[p.x][p.y];
+	if(b == SNAKE)
+		return 0;
+	cmd(win, ".c create oval " + r2s(square(p).inset(-2)) +
+			" -fill red -tags b." + string p.x + "." + string p.y);
+	board[p.x][p.y] = FOOD;
+	return 1;
+}
+
+destroy(p: Point)
+{
+	board[p.x][p.y] = 0;
+	cmd(win, ".c delete b." + string p.x + "." + string p.y);
+}
+
+square(p: Point): Rect
+{
+	p = p.mul(Size);
+	return (p, p.add((Size, Size)));
+}
+
+ticker(tick: chan of int, ticki: ref Tick)
+{
+	while((dt := ticki.dt) >= 0){
+		sys->sleep(dt);
+		tick <-= 1;
+	}
+	tick <-= 0;
+}
+
+absorb(c: chan of string)
+{
+	for(;;){
+		alt{
+		<-c =>
+			;
+		* =>
+			return;
+		}
+	}
+}
+
+r2s(r: Rect): string
+{
+	return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil)
+		return nil;
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return nil;
+	return string buf[0:n];
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+	r := tk->cmd(win, s);
+	if(len r > 0 && r[0] == '!'){
+		sys->print("error executing '%s': %s\n", s, r[1:]);
+	}
+	return r;
+}
--- /dev/null
+++ b/appl/wm/stopwatch.b
@@ -1,0 +1,184 @@
+implement WmStopWatch;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "daytime.m";
+	daytime: Daytime;
+
+
+WmStopWatch: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+t: ref Tk->Toplevel;
+cmd: chan of string;
+
+tpid: int;
+
+hr,
+min,
+sec: int;
+
+sw_cfg := array[] of {
+	"frame .f",
+	"button .f.b1 -text Start -command {send cmd start}",
+	"button .f.b2 -text Stop -command {send cmd stop}",
+	"button .f.b3 -text Reset -command {send cmd reset}",
+	"pack .f.b1 .f.b2 .f.b3 -side left -fill x -expand 1",
+
+	"frame .ft",
+	"label .ft.d -label {0:00:00}",
+	"pack .ft.d -expand 1",
+
+	"frame .fs1",
+	"button .fs1.s -text Time1 -command {send cmd s1}",
+	"label .fs1.l -label {0:00:00}",
+	"pack .fs1.s .fs1.l -side left -expand 1",
+
+	"frame .fs2",
+	"button .fs2.s -text Time2 -command {send cmd s2}",
+	"label .fs2.l -label {0:00:00}",
+	"pack .fs2.s .fs2.l -side left -expand 1",
+
+	"frame .fs3",
+	"button .fs3.s -text Time3 -command {send cmd s3}",
+	"label .fs3.l -label {0:00:00}",
+	"pack .fs3.s .fs3.l -side left -expand 1",
+
+	"pack .Wm_t -fill x",
+	"pack .f .ft .fs1 .fs2 .fs3",
+	"pack propagate . 0",
+	"update",
+};
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys  = load Sys  Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "stopwatch: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk   = load Tk  Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	if(draw==nil || tk==nil || tkclient==nil || daytime==nil){
+		sys->fprint(sys->fildes(2), "stopwatch: couldn't load modules\n");
+		return;
+	}
+
+	tkclient->init();
+
+	menubut := chan of string;
+	(t, menubut) = tkclient->toplevel(ctxt, "-borderwidth 2 -relief raised", "StopWatch", Tkclient->Appl);
+
+	hr = 0;
+	min = 0;
+	sec = 0;
+
+	cmd = chan of string;
+	tk->namechan(t, cmd, "cmd");
+	for (c:=0; c<len sw_cfg; c++)
+		tk->cmd(t, sw_cfg[c]);
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+	tpid = 0;
+
+	# keep the timerloop in a separate thread,
+	# so that wm events don't hold up the ticker
+	# i.e., titlebar click&hold would otherwise 
+	# 'pause' the timer since the tick would not
+	# be processed.
+
+	pid := chan of int;
+	spawn timerloop(pid);
+	looppid := <- pid;
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq =>
+		tkclient->wmctl(t, s);
+	menu := <-menubut =>
+		if(menu == "exit") {
+			if(tpid)
+				kill(tpid);
+			kill(looppid);
+			return;
+		}
+		tkclient->wmctl(t, menu);
+	}
+}
+
+timerloop(pid: chan of int)
+{
+	pid <- = sys->pctl(0, nil);
+
+	tick := chan of int;
+	s: string;
+
+	for(;;) alt {
+	c := <-cmd =>
+		if(c == "stop"){
+			if(tpid != 0){
+				kill(tpid);
+				tpid = 0;
+			}
+		} else if(c == "reset"){
+			hr = min = sec = 0;
+			s = sys->sprint("%d:%2.2d:%2.2d", hr, min, sec);
+			tk->cmd(t, ".ft.d configure -label {"+s+"};update");
+		} else if(c == "start"){
+			if(tpid == 0){
+				spawn timer(tick);
+				tpid = <- tick;
+			}
+		} else if(c == "s1" || c == "s2" || c == "s3"){
+			s = sys->sprint("%d:%2.2d:%2.2d", hr, min, sec);
+			tk->cmd(t, ".f"+c+".l configure -label {"+s+"};update");
+		}
+	<-tick =>
+		sec++;
+		if(sec>=60){
+			sec = 0;
+			min++;
+			if(min>=60){
+				min = 0;
+				hr++;
+			}
+		}
+		s = sys->sprint("%d:%2.2d:%2.2d", hr, min, sec);
+		tk->cmd(t, ".ft.d configure -label {"+s+"};update");
+	}
+}
+
+timer(c: chan of int)
+{
+	pid := sys->pctl(0, nil);
+	for(;;) {
+		c <-= pid;
+		sys->sleep(1000);
+	}
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
--- /dev/null
+++ b/appl/wm/sweeper.b
@@ -1,0 +1,330 @@
+implement Sweeper;
+
+#
+# michael@vitanuova.com
+#
+# Copyright © 2000 Vita Nuova Limited.  All rights reserved.
+# Copyright © 2001 Vita Nuova Holdings Limited.  All rights reserved.
+#
+
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Point, Rect, Image, Font, Context, Screen, Display: import draw;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "daytime.m";
+	daytime: Daytime;
+include "rand.m";
+	rand: Rand;
+
+stderr: ref Sys->FD;
+
+Sweeper: module 
+{
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+mainwin: ref Toplevel;
+score: int;
+mines: int;
+
+WIDTH: con 220;
+HEIGHT: con 220;
+
+EASY: con 20;
+SZB: con 10;
+SZI: con SZB+2;			# internal board is 2 larger than visible board
+
+Cell: adt {
+	mine, state: int;
+};
+
+board: array of array of Cell;
+
+UNSELECTED, SELECTED, MARKED: con (1<<iota);
+
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	daytime = load Daytime Daytime->PATH;
+	rand = load Rand Rand->PATH;
+
+	stderr = sys->fildes(2);
+	rand->init(daytime->now());
+	daytime = nil;
+
+	tkclient->init();
+	if(ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+
+	(win, wmcmd) := tkclient->toplevel(ctxt, "", "Mine Sweeper", Tkclient->Hide);
+	mainwin = win;
+	sys->pctl(Sys->NEWPGRP, nil);
+	cmdch := chan of string;
+	tk->namechan(win, cmdch, "cmd");
+	display_board();
+	pid := -1;
+	finished := 0;
+	init_board();
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	for (;;) {
+		alt {
+			s := <-win.ctxt.kbd =>
+				tk->keyboard(win, s);
+			s := <-win.ctxt.ptr =>
+				tk->pointer(win, *s);
+			c := <-win.ctxt.ctl or
+			c = <-win.wreq or
+			c = <- wmcmd =>	# wm commands
+				case c {
+					"exit" =>
+						if(pid != -1)
+							kill(pid);
+						exit;
+					* =>
+						tkclient->wmctl(win, c);
+				}
+			c := <- cmdch =>	# tk commands
+				(nil, toks) := sys->tokenize(c, " ");
+				case hd toks {
+					"b" =>
+						x := int hd tl toks;
+						y := int hd tl tl toks;
+						i := board_check(x, y);
+						case i {
+							-1 =>
+								display_mines();
+								display_lost();
+								finished = 1;
+							0 to 8 =>
+								if (finished)
+									break;
+								score++;
+								board[x][y].state = SELECTED;
+								display_square(x, y, sys->sprint("%d", i), "olive");
+								if (i == 0) {  # check all adjacent zeros
+									display_zeros(x, y);
+								}
+								display_score();
+								if (score+mines == SZB*SZB) {
+									display_mines();
+									display_win();
+									finished = 1;
+								}
+							* =>
+								;
+						}
+						cmd(mainwin, "update");
+					"b3" =>
+						x := int hd tl toks;
+						y := int hd tl tl toks;
+						mark_square(x, y);
+						cmd(mainwin, "update");
+					"restart" =>
+						init_board();
+						display_score();
+						reset_display();
+						finished = 0;
+					* =>
+						sys->fprint(stderr, "%s\n", c);
+				}
+			}
+	}
+}
+
+display_board() {
+	i, j: int;
+	pack: string;
+
+	for(i = 0; i < len win_config; i++)
+		cmd(mainwin, win_config[i]);
+
+	for (i = 1; i <= SZB; i++) {
+		cmd(mainwin,  sys->sprint("frame .f%d", i));
+		pack = "";
+		for (j = 1; j <= SZB; j++) {
+			pack += sys->sprint(" .f%d.b%dx%d", i, i, j);
+			cmd(mainwin, sys->sprint("button .f%d.b%dx%d -text { } -width 14 -command {send cmd b %d %d}", i, i, j, i, j));
+			cmd(mainwin, sys->sprint("bind .f%d.b%dx%d <ButtonRelease-3> {send cmd b3 %d %d}", i, i, j, i, j));
+		}
+		cmd(mainwin, sys->sprint("pack %s -side left", pack));
+		cmd(mainwin, sys->sprint("pack .f%d -side top -fill x", i));
+	}
+
+	for (i = 0; i < len win_config2; i++)
+		cmd (mainwin, win_config2[i]);
+}
+
+reset_display()
+{
+	for (i := 1; i <= SZB; i++) {
+		for (j := 1; j <= SZB; j++) {
+			s := sys->sprint(".f%d.b%dx%d configure -text { } -bg #dddddd -activebackground #eeeeee", i, i, j);
+			cmd(mainwin, s);
+		}
+	}
+	cmd(mainwin, "update");
+}
+
+
+init_board()
+{
+	i, j: int;
+
+	score = 0;
+	mines = 0;
+	board = array[SZI] of array of Cell;
+	for (i = 0; i < SZI; i++)
+		board[i] = array[SZI] of Cell;
+
+	# initialize board
+	for (i = 0; i < SZI; i++)
+		for (j =0; j < SZI; j++) {
+			board[i][j].mine = 0;
+			board[i][j].state = UNSELECTED;
+		}
+
+	# place mines
+	for (i = 0; i < EASY; i++) {
+		j = rand->rand(SZB*SZB);
+		if (board[(j/SZB)+1][(j%SZB)+1].mine == 0) { 	# rand could yield same result twice
+			board[(j/SZB)+1][(j%SZB)+1].mine = 1;
+			mines++;
+		}
+	}
+	cmd(mainwin, "update");
+}
+
+display_score()
+{
+	cmd(mainwin, ".f.l configure -text {Score: "+ sys->sprint("%d", score)+ "}");
+}
+
+display_win()
+{
+	cmd(mainwin, ".f.l configure -text {You have Won}");
+}
+
+display_lost()
+{
+	cmd(mainwin, ".f.l configure -text {You have Lost}");
+}
+
+display_mines()
+{
+	for (i := 1; i <= SZB; i++)
+		for (j := 1; j <= SZB; j++)
+			if (board[i][j].mine == 1)
+				display_square(i, j, "M", "red");
+}
+
+display_square(i, j: int, v: string, c: string) {
+	cmd(mainwin, sys->sprint(".f%d.b%dx%d configure -text {%s} -bg %s -activebackground %s", i, i, j, v, c, c));
+	cmd(mainwin, "update");
+}
+
+mark_square(i, j: int) {
+	case board[i][j].state {
+		UNSELECTED =>
+			board[i][j].state = MARKED;
+			display_square(i, j, "?", "orange");
+		MARKED =>
+			board[i][j].state = UNSELECTED;
+			display_square(i, j, " ", "#dddddd");
+	}
+}
+
+board_check(i, j: int) : int 
+{
+	if (board[i][j].mine == 1)
+		return -1;
+	if (board[i][j].state&(SELECTED|MARKED))
+		return -2;
+	c := 0;
+	for (x := i-1; x <= i+1; x++)
+		for (y := j-1; y <= j+1; y++)
+			if (board[x][y].mine == 1)
+				c++;
+	return c;
+}
+
+display_zeros(i, j: int)
+{
+	for (x := i-1; x <= i+1; x++) {
+		for (y := j-1; y <= j+1; y++) {
+			if (x <1 || x>SZB || y<1 || y>SZB)
+				continue;
+			if (board_check(x, y) == 0) {
+				score++;
+				board[x][y].state = SELECTED;
+				display_square(x, y, "0", "olive");
+				display_zeros(x, y);
+			}
+		}
+	}		
+}
+
+fatal(s: string)
+{
+	sys->fprint(stderr, "%s\n", s);
+	exit;
+}
+
+sleep(t: int)
+{
+	sys->sleep(t);
+}
+
+kill(pid: int): int
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil)
+		return -1;
+	if(sys->write(fd, array of byte "kill", 4) != 4)
+		return -1;
+	return 0;
+}
+
+cmd(top: ref Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr, "sweeper: tk error on '%s': %s\n", s, e);
+	return e;
+}
+				
+win_config := array[] of {
+	"frame .f -width 220 -height 220",
+
+	"menubutton .f.sz -text Options -menu .f.sz.sm",
+	"menu .f.sz.sm",
+	".f.sz.sm add command -label restart -command { send cmd restart }",
+	"pack .f.sz -side left",
+
+	"label .f.l -text {Score:  }",
+	"pack .f.l  -side right",
+
+	"frame .ft",
+	"label .ft.l -text {  }",
+	"pack .ft.l -side left",
+
+	"pack .f -side top -fill x",
+	"pack .ft -side top -fill x",
+
+};
+
+win_config2 := array[] of {
+
+	"pack propagate . 0",
+	"update",
+};
--- /dev/null
+++ b/appl/wm/task.b
@@ -1,0 +1,240 @@
+implement WmTask;
+
+include "sys.m";
+	sys: Sys;
+	Dir: import sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+Prog: adt
+{
+	pid:	int;
+	pgrp: int;
+	size:	int;
+	state:	string;
+	mod:	string;
+};
+
+WmTask: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Wm: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+task_cfg := array[] of {
+	"frame .fl",
+	"scrollbar .fl.scroll -command {.fl.l yview}",
+	"listbox .fl.l -width 40w -yscrollcommand {.fl.scroll set}",
+	"frame .b",
+	"button .b.ref -text Refresh -command {send cmd r}",
+	"button .b.deb -text Debug -command {send cmd d}",
+	"button .b.files -text Files -command {send cmd f}",
+	"button .b.kill -text Kill -command {send cmd k}",
+	"button .b.killg -text {Kill Group} -command {send cmd kg}",
+	"pack .b.ref .b.deb .b.files .b.kill .b.killg -side left -padx 2 -pady 2",
+	"pack .b -fill x",
+	"pack .fl.scroll -side left -fill y",
+	"pack .fl.l -fill both -expand 1",
+	"pack .fl -fill both -expand 1",
+	"pack propagate . 0",
+};
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys  = load Sys  Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "task: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk   = load Tk   Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+
+	tkclient->init();
+	dialog->init();
+
+	sysnam := sysname();
+
+	(t, wmctl) := tkclient->toplevel(ctxt, "", sysnam, Tkclient->Appl);
+	if(t == nil)
+		return;
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+
+	for (c:=0; c<len task_cfg; c++)
+		tk->cmd(t, task_cfg[c]);
+
+	readprog(t);
+
+	tk->cmd(t, ".fl.l see end;update");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq =>
+		tkclient->wmctl(t, s);
+	menu := <-wmctl =>
+		case menu {
+		"exit" =>
+			return;
+		"task" =>
+			tkclient->wmctl(t, menu);
+			tk->cmd(t, ".fl.l delete 0 end");
+			readprog(t);
+			tk->cmd(t, ".fl.l see end;update");
+		* =>
+			tkclient->wmctl(t, menu);
+		}
+	bcmd := <-cmd =>
+		case bcmd {
+		"d" =>
+			sel := tk->cmd(t, ".fl.l curselection");
+			if(sel == "")
+				break;
+			pid := int tk->cmd(t, ".fl.l get "+sel);
+			stk := load Wm "/dis/wm/deb.dis";
+			if(stk == nil)
+				break;
+			spawn stk->init(ctxt, "wm/deb" :: "-p "+string pid :: nil);
+			stk = nil;
+		"k" or "kg" =>
+			sel := tk->cmd(t, ".fl.l curselection");
+			if(sel == "")
+				break;
+			pid := int tk->cmd(t, ".fl.l get "+sel);
+			what := "opening ctl file";
+			cfile := "/prog/"+string pid+"/ctl";
+			cfd := sys->open(cfile, sys->OWRITE);
+			if(cfd != nil) {
+				if(bcmd == "kg"){
+					if(sys->fprint(cfd, "killgrp") > 0){
+						cfd = nil;
+						refresh(t);
+						break;
+					}
+				}else if(sys->fprint(cfd, "kill") > 0){
+					tk->cmd(t, ".fl.l delete "+sel);
+					cfd = nil;
+					break;
+				}
+				cfd = nil;
+				what = "sending kill request";
+			}
+			if(bcmd == "k" && sys->sprint("%r") == "file does not exist") {
+				refresh(t);
+				break;
+			}
+			dialog->prompt(ctxt, t.image, "error -fg red", "Kill",
+					"Error "+what+"\n"+
+					 "System: "+sys->sprint("%r"),
+					0, "OK" :: nil);
+		"r" =>
+			refresh(t);
+		"f" =>
+			sel := tk->cmd(t, ".fl.l curselection");
+			if(sel == "")
+				break;
+			pid := int tk->cmd(t, ".fl.l get "+sel);
+			fi := load Wm "/dis/wm/edit.dis";
+			if(fi == nil)
+				break;
+			spawn fi->init(ctxt,
+				"edit" ::
+				"/prog/"+string pid+"/fd" :: nil);
+			fi = nil;
+		}
+	}
+}
+
+refresh(t: ref Tk->Toplevel)
+{
+	tk->cmd(t, ".fl.l delete 0 end");
+	readprog(t);
+	tk->cmd(t, ".fl.l see end;update");
+}
+
+mkprog(file: string): ref Prog
+{
+	fd := sys->open("/prog/"+file+"/status", sys->OREAD);
+	if(fd == nil)
+		return nil;
+
+	buf := array[256] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+
+	(v, l) := sys->tokenize(string buf[0:n], " ");
+	if(v < 6)
+		return nil;
+
+	prg := ref Prog;
+	prg.pid = int hd l;
+	l = tl l;
+	prg.pgrp = int hd l;
+	l = tl l;
+	l = tl l;
+	# eat blanks in user name
+	while(len l > 3)
+		l = tl l;
+	prg.state = hd l;
+	l = tl l;
+	prg.size = int hd l;
+	l = tl l;
+	prg.mod = hd l;
+
+	return prg;
+}
+
+readprog(t: ref Toplevel)
+{
+	fd := sys->open("/prog", sys->OREAD);
+	if(fd == nil)
+		return;
+	for(;;) {
+		(n, d) := sys->dirread(fd);
+		if(n <= 0)
+			break;
+		for(i := 0; i < n; i++) {
+			p := mkprog(d[i].name);
+			if(p != nil){
+				l := sys->sprint("%4d %4d %3dK %-7s  %s", p.pid, p.pgrp, p.size, p.state, p.mod);
+				tk->cmd(t, ".fl.l insert end '"+l);
+			}
+		}
+	}
+}
+
+sysname(): string
+{
+	fd := sys->open("#c/sysname", sys->OREAD);
+	if(fd == nil)
+		return "Anon";
+	buf := array[128] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0) 
+		return "Anon";
+	return string buf[0:n];
+}
--- /dev/null
+++ b/appl/wm/telnet.b
@@ -1,0 +1,820 @@
+implement WmTelnet;
+
+include "sys.m";
+	sys: Sys;
+	Connection: import sys;
+
+include "draw.m";
+	draw: Draw;
+	Context: import draw;
+
+include "tk.m";
+	tk: Tk;
+
+include "tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+WmTelnet: module
+{
+	init:	fn(ctxt: ref Draw->Context, args: list of string);
+};
+
+Iob: adt
+{
+	fd:	ref Sys->FD;
+	t:	ref Tk->Toplevel;
+	out:	cyclic ref Iob;
+	buf:	array of byte;
+	ptr:	int;
+	nbyte:	int;
+};
+
+BS:		con 8;		# ^h backspace character
+BSW:		con 23;		# ^w bacspace word
+BSL:		con 21;		# ^u backspace line
+EOT:		con 4;		# ^d end of file
+ESC:		con 27;		# hold mode
+
+HIWAT:	con 2000;	# maximum number of lines in transcript
+LOWAT:	con 1500;	# amount to reduce to after high water
+
+Name:	con "Telnet";
+ctxt:	ref Context;
+cmds:	chan of string;
+net:	Connection;
+stderr: ref Sys->FD;
+mcrlf:	int;
+netinp:	ref Iob;
+
+# control characters
+Se:		con 240;	# end subnegotiation
+NOP:		con 241;
+Mark:		con 242;	# data mark
+Break:		con 243;
+Interrupt:	con 244;
+Abort:		con 245;	# TENEX ^O
+AreYouThere:	con 246;
+Erasechar:	con 247;	# erase last character
+Eraseline:	con 248;	# erase line
+GoAhead:	con 249;	# half duplex clear to send
+Sb:		con 250;	# start subnegotiation
+Will:		con 251;
+Wont:		con 252;
+Do:		con 253;
+Dont:		con 254;
+Iac:		con 255;
+
+# options
+Binary,	Echo,	SGA,	Stat,	Timing,
+Det,	Term,	EOR,	Uid,	Outmark,
+Ttyloc,	M3270,	Padx3,	Window,	Speed,
+Flow,	Line,	Xloc,	Extend: con iota;
+
+Opt: adt
+{
+	name:	string;
+	code:	int;
+	noway:	int;	
+	remote:	int;		# remote value
+	local:	int;		# local value
+};
+
+opt := array[] of
+{
+	Binary	=> Opt("binary",			0,	0,	0, 	0),
+	Echo		=> Opt("echo",				1,  	0, 	0,	0),
+	SGA		=> Opt("suppress Go Ahead",	3,  	0, 	0,	0),
+	Stat		=> Opt("status",			5,  	1, 	0,	0),
+	Timing	=> Opt("timing",			6,  	1, 	0,	0),
+	Det		=> Opt("det",				20, 	1, 	0,	0),
+	Term	=> Opt("terminal",			24, 	0, 	0,	0),
+	EOR		=> Opt("end of record",		25, 	1, 	0,	0),
+	Uid		=> Opt("uid",				26, 	1, 	0,	0),
+	Outmark	=> Opt("outmark",			27, 	1, 	0,	0),
+	Ttyloc	=> Opt("ttyloc",				28, 	1, 	0,	0),
+	M3270	=> Opt("3270 mode",		29, 	1, 	0,	0),
+	Padx3	=> Opt("pad x.3",			30, 	1, 	0,	0),
+	Window	=> Opt("window size",		31, 	1, 	0,	0),
+	Speed	=> Opt("speed",			32, 	1, 	0,	0),
+	Flow		=> Opt("flow control",		33, 	1, 	0,	0),
+	Line		=> Opt("line mode",			34, 	0, 	0,	0),
+	Xloc		=> Opt("X display loc",		35, 	1, 	0,	0),
+	Extend	=> Opt("Extended",			255, 	1, 	0,	0),
+};
+
+shwin_cfg := array[] of {
+	"menu .m",
+	".m add command -text Cut -command {send edit cut}",
+	".m add command -text Paste -command {send edit paste}",
+	".m add command -text Snarf -command {send edit snarf}",
+	".m add command -text Send -command {send edit send}",
+	"frame .ft",
+	"scrollbar .ft.scroll -command {.ft.t yview}",
+	"text .ft.t -width 70w -height 25h -yscrollcommand {.ft.scroll set}",
+	"frame .mb",
+	"menubutton .mb.c -text Connect -menu .mbc",
+	"menubutton .mb.t -text Terminal -menu .mbt",
+	"menu .mbc",
+	".mbc add command -text {Remote System} -command {send cmd con}",
+	".mbc add command -text {Disconnect} -state disabled -command {send cmd dis}",
+	".mbc add command -text {Exit} -command {send cmd exit}",
+	".mbc add separator",
+	"menu .mbt",
+	".mbt add checkbutton -text {Line Mode} -command {send cmd line}",
+	".mbt add checkbutton -text {Map CR to LF} -command {send cmd crlf}",
+	"pack .mb.c .mb.t -side left",
+	"pack .ft.scroll -side left -fill y",
+	"pack .ft.t -fill both -expand 1",
+	"pack .mb -fill x",
+	"pack .ft -fill both -expand 1",
+	"pack propagate . 0",
+	"focus .ft.t",
+	"bind .ft.t <Key> {send keys {%A}}",
+	"bind .ft.t <Control-d> {send keys {%A}}",
+	"bind .ft.t <Control-h> {send keys {%A}}",
+	"bind .ft.t <ButtonPress-3> {send but3 %X %Y}",
+	"bind .ft.t <ButtonRelease-3> {}",
+	"bind .ft.t <DoubleButton-3> {}",
+	"bind .ft.t <Double-ButtonRelease-3> {}",
+	"bind .ft.t <ButtonPress-2> {}",
+	"bind .ft.t <ButtonRelease-2> {}",
+	"update"
+};
+
+connect_cfg := array[] of {
+	"frame .fl",
+	"label .fl.h -text Host",
+	"label .fl.p -text Port",
+	"pack .fl.h .fl.p",
+	"frame .el",
+	"entry .el.h",
+	"entry .el.p",
+	".el.p insert end 'telnet",
+	"pack .el.h .el.p",
+	"pack .Wm_t -fill x",
+	"pack .fl .el -side left",
+	"focus .el.h",
+	"bind .el.h <Key-\n> {send cmd ok}",
+	"bind .el.p <key-\n> {send cmd ok}",
+	"update"
+};
+
+connected_cfg := array[] of {
+	"focus .ft.t",
+	".mbc entryconfigure 0 -state disabled",
+	".mbc entryconfigure 1 -state normal"
+};
+
+menuindex := "0";
+holding := 0;
+
+init(C: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (C == nil) {
+		sys->fprint(sys->fildes(2), "telnet: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+
+	ctxt = C;
+	tkclient->init();
+	dialog->init();
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+
+	tkargs := "";
+	argv = tl argv;
+	if(argv != nil) {
+		tkargs = hd argv;
+		argv = tl argv;
+	}
+	(t, titlectl) := tkclient->toplevel(ctxt, tkargs, Name, Tkclient->Appl);
+
+	edit := chan of string;
+	tk->namechan(t, edit, "edit");
+	for (cc:=0; cc<len shwin_cfg; cc++)
+		tk->cmd(t, shwin_cfg[cc]);
+
+	keys := chan of string;
+	tk->namechan(t, keys, "keys");
+
+	but3 := chan of string;
+	tk->namechan(t, but3, "but3");
+
+	cmds = chan of string;
+	tk->namechan(t, cmds, "cmd");
+
+	# outpoint is place in text to insert characters printed by programs
+	tk->cmd(t, ".ft.t mark set outpoint end; .ft.t mark gravity outpoint left");
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-titlectl =>
+		if(s == "exit") {
+			kill();
+			return;
+		}
+		tkclient->wmctl(t, s);
+	ecmd := <-edit =>
+		editor(t, ecmd);
+		sendinput(t);
+
+	c := <-keys =>
+		if(opt[Echo].local == 0) {
+			sys->fprint(net.dfd, "%c", c[1]);
+			break;
+		}
+		cut(t, 1);
+		char := c[1];
+		if(char == '\\')
+			char = c[2];
+		update := ";.ft.t see insert;update";
+		case char{
+		* =>
+			tk->cmd(t, ".ft.t insert insert "+c+update);
+		'\n' or EOT =>
+			tk->cmd(t, ".ft.t insert insert "+c+update);
+			sendinput(t);
+		BS =>
+			if(!insat(t, "outpoint"))
+				tk->cmd(t, ".ft.t delete insert-1chars"+update);
+		ESC =>
+			holding ^= 1;
+			color := "blue";
+			if(!holding){
+				color = "black";
+				tkclient->settitle(t, Name);
+				sendinput(t);
+			}else
+				tkclient->settitle(t, Name+" (holding)");
+			tk->cmd(t, ".ft.t configure -foreground "+color+update);
+		BSL =>
+			if(insininput(t))
+				tk->cmd(t, ".ft.t delete outpoint insert"+update);
+			else
+				tk->cmd(t, ".ft.t delete {insert linestart} insert"+update);
+		BSW =>
+			if(insat(t, "outpoint"))
+				break;
+			a0 := isalnum(tk->cmd(t, ".ft.t get insert-1chars"));
+			a1 := isalnum(tk->cmd(t, ".ft.t get insert"));
+			start: string;
+			if(a0 && a1)	# middle of word
+				start = "{insert wordstart}";
+			else if(a0)		# end of word
+				start = "{insert-1chars wordstart}";
+			else{	# beginning or not in word; must search
+				s: string;
+				for(n:=1; ;){
+					s = tk->cmd(t, ".ft.t get insert-"+ string n +"chars");
+					if(s=="" || s=="\n"){
+						start = "insert-"+ string n+"chars";
+						break;
+					}
+					n++;
+					if(isalnum(s)){
+						start = "{insert-"+ string n+"chars wordstart}";
+						break;
+					}
+				}
+				
+			}
+			# don't ^w across outpoint
+			if(tk->cmd(t, ".ft.t compare insert >= outpoint") == "1"
+			&& tk->cmd(t, ".ft.t compare "+start+" < outpoint") == "1")
+				start = "outpoint";
+			tk->cmd(t, ".ft.t delete " + start + " insert"+update);
+		}
+
+	c := <-but3 =>
+		(nil, l) := sys->tokenize(c, " ");
+		x := int hd l - 50;
+		y := int hd tl l - int tk->cmd(t, ".m yposition "+menuindex) - 10;
+		tk->cmd(t, ".m activate "+menuindex+"; .m post "+string x+" "+string y+
+			"; grab set .m; update");
+
+	c := <-cmds =>
+		case c {
+		"con" =>
+			tk->cmd(t, ".mb.c configure -state disabled");
+			connect(t);
+			tk->cmd(t, ".mb.c configure -state normal; update");
+		"dis" =>
+			tkclient->settitle(t, "Telnet");
+			tk->cmd(t, ".mbc entryconfigure 0 -state normal");
+			tk->cmd(t, ".mbc entryconfigure 1 -state disabled");
+			net.cfd = nil;
+			net.dfd = nil;
+			kill();
+		"exit" =>
+			kill();
+			return;
+		"crlf" =>
+			mcrlf = !mcrlf;
+			break;
+		"line" =>
+			if(opt[Line].local == 0)
+				send3(netinp, Iac, Will, opt[Line].code);
+			else
+				send3(netinp, Iac, Wont, opt[Line].code);
+		}
+	}
+}
+
+insat(t: ref Tk->Toplevel, mark: string): int
+{
+	return tk->cmd(t, ".ft.t compare insert == "+mark) == "1";
+}
+
+insininput(t: ref Tk->Toplevel): int
+{
+	if(tk->cmd(t, ".ft.t compare insert >= outpoint") != "1")
+		return 0;
+	return tk->cmd(t, ".ft.t compare {insert linestart} == {outpoint linestart}") == "1";
+}
+
+isalnum(s: string): int
+{
+	if(s == "")
+		return 0;
+	c := s[0];
+	if('a' <= c && c <= 'z')
+		return 1;
+	if('A' <= c && c <= 'Z')
+		return 1;
+	if('0' <= c && c <= '9')
+		return 1;
+	if(c == '_')
+		return 1;
+	if(c > 16rA0)
+		return 1;
+	return 0;
+}
+
+editor(t: ref Tk->Toplevel, ecmd: string)
+{
+	s, snarf: string;
+
+	case ecmd {
+	"cut" =>
+		menuindex = "0";
+		cut(t, 1);
+	
+	"paste" =>
+		menuindex = "1";
+		snarf = tkclient->snarfget();
+		if(snarf == "")
+			break;
+		cut(t, 0);
+		tk->cmd(t, ".ft.t insert insert '"+snarf);
+		sendinput(t);
+
+	"snarf" =>
+		menuindex = "2";
+		if(tk->cmd(t, ".ft.t tag ranges sel") == "")
+			break;
+		snarf = tk->cmd(t, ".ft.t get sel.first sel.last");
+		tkclient->snarfput(snarf);
+
+	"send" =>
+		menuindex = "3";
+		if(tk->cmd(t, ".ft.t tag ranges sel") != ""){
+			snarf = tk->cmd(t, ".ft.t get sel.first sel.last");
+			tkclient->snarfput(snarf);
+		}else
+			snarf = tkclient->snarfget();
+		if(snarf != "")
+			s = snarf;
+		else
+			return;
+		if(s[len s-1] != '\n' && s[len s-1] != EOT)
+			s[len s] = '\n';
+		tk->cmd(t, ".ft.t see end; .ft.t insert end '"+s);
+		tk->cmd(t, ".ft.t mark set insert end");
+		tk->cmd(t, ".ft.t tag remove sel sel.first sel.last");
+	}
+	tk->cmd(t, "update");
+}
+
+cut(t: ref Tk->Toplevel, snarfit: int)
+{
+	if(tk->cmd(t, ".ft.t tag ranges sel") == "")
+		return;
+	if(snarfit)
+		tkclient->snarfput(tk->cmd(t, ".ft.t get sel.first sel.last"));
+	tk->cmd(t, ".ft.t delete sel.first sel.last");
+}
+
+sendinput(t: ref Tk->Toplevel)
+{
+	if(holding)
+		return;
+	input := tk->cmd(t, ".ft.t get outpoint end");
+	slen := len input;
+	if(slen == 0)
+		return;
+
+	for(i := 0; i < slen; i++)
+		if(input[i] == '\n' || input[i] == EOT)
+			break;
+
+	if(i >= slen)
+		return;
+
+	advance := string (i+1);
+	if(input[i] == EOT)
+		input = input[0:i];
+	else
+		input = input[0:i+1];
+
+	sys->fprint(net.dfd, "%s", input);
+	tk->cmd(t, ".ft.t mark set outpoint outpoint+" + advance + "chars");
+}
+
+kill()
+{
+	path := sys->sprint("#p/%d/ctl", sys->pctl(0, nil));
+	fd := sys->open(path, sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+}
+
+connect(t: ref Tk->Toplevel)
+{
+	(b, titlectl) := tkclient->toplevel(ctxt, nil, "Connect", 0);
+	for (c:=0; c<len connect_cfg; c++)
+		tk->cmd(b, connect_cfg[c]);
+
+	cmd := chan of string;
+	tk->namechan(b, cmd, "cmd");
+	tkclient->onscreen(b, nil);
+	tkclient->startinput(b, "kbd"::"ptr"::nil);
+
+loop:	for(;;) alt {
+		s := <-b.ctxt.kbd =>
+			tk->keyboard(b, s);
+		s := <-b.ctxt.ptr =>
+			tk->pointer(b, *s);
+		s := <-b.ctxt.ctl or
+		s = <-b.wreq or
+		s = <-titlectl =>
+			if(s == "exit")
+				return;
+			tkclient->wmctl(b, s);
+	<-cmd =>
+		break loop;		
+	}
+
+	addr := sys->sprint("tcp!%s!%s",
+			tk->cmd(b, ".el.h get"),
+			tk->cmd(b, ".el.p get"));
+
+	tkclient->settitle(b, "Dialing");
+	tk->cmd(b, "update");
+
+	ok: int;
+	(ok, net) = sys->dial(addr, nil);
+	if(ok < 0) {
+		dialog->prompt(ctxt, b.image, "error -fg red",
+			"Connect", "Connection to host failed\n"+sys->sprint("%r"),
+			0, "Stop connect" :: nil);
+		return;
+	}
+
+	tkclient->settitle(t, "Telnet - "+addr);
+	for (c=0; c<len connected_cfg; c++)
+		tk->cmd(b, connected_cfg[c]);
+
+	spawn fromnet(t);
+}
+
+flush(t: ref Tk->Toplevel, data: array of byte)
+{
+	cdata := string data;
+	ncdata := string len cdata + "chars;";
+	moveins := insat(t, "outpoint");
+	tk->cmd(t, ".ft.t insert outpoint '"+ cdata);
+	s := ".ft.t mark set outpoint outpoint+" + ncdata;
+	s += ".ft.t see outpoint;";
+	if(moveins)
+		s += ".ft.t mark set insert insert+" + ncdata;
+	s += "update";
+	tk->cmd(t, s);
+	nlines := int tk->cmd(t, ".ft.t index end");
+	if(nlines > HIWAT){
+		s = ".ft.t delete 1.0 "+ string (nlines-LOWAT) +".0;update";
+		tk->cmd(t, s);
+	}
+}
+
+iobnew(fd: ref Sys->FD, t: ref Tk->Toplevel, out: ref Iob, size: int): ref Iob
+{
+	iob := ref Iob;
+	iob.fd = fd;
+	iob.t = t;
+	iob.out = out;
+	iob.buf = array[size] of byte;
+	iob.nbyte = 0;
+	iob.ptr = 0;
+	return iob;
+}
+
+iobget(iob: ref Iob): int
+{
+	if(iob.nbyte == 0) {
+		if(iob.out != nil)
+			iobflush(iob.out);
+		iob.nbyte = sys->read(iob.fd, iob.buf, len iob.buf);
+		if(iob.nbyte <= 0)
+			return iob.nbyte;
+		iob.ptr = 0;
+	}
+	iob.nbyte--;
+	return int iob.buf[iob.ptr++];
+}
+
+iobput(iob: ref Iob, c: int)
+{
+	iob.buf[iob.ptr++] = byte c;
+	if(iob.ptr == len iob.buf)
+		iobflush(iob);
+}
+
+iobflush(iob: ref Iob)
+{
+	if(iob.fd == nil) {
+		flush(iob.t, iob.buf[0:iob.ptr]);
+		iob.ptr = 0;
+	}
+}
+
+fromnet(t: ref Tk->Toplevel)
+{
+	conout := iobnew(nil, t, nil, 2048);
+	netinp = iobnew(net.dfd, nil, conout, 2048);
+
+	crnls := 0;
+	freenl := 0;
+
+loop:	for(;;) {
+		c := iobget(netinp);
+		case c {
+		-1 =>
+			cmds <-= "dis";
+			return;
+		'\n' =>				# skip nl after string of cr's */
+			if(!opt[Binary].local && !mcrlf) {
+				crnls++;
+				if(freenl == 0)
+					break;
+				freenl = 0;
+				continue loop;
+			}
+		'\r' =>
+			if(!opt[Binary].local && !mcrlf) {
+				if(crnls++ == 0){
+					freenl = 1;
+					c = '\n';
+					break;
+				}
+				continue loop;
+			}
+		Iac  =>
+			c = iobget(netinp);
+			if(c == Iac)
+				break;
+			iobflush(conout);
+			if(control(netinp, c) < 0)
+				return;
+
+			continue loop;	
+		}
+		iobput(conout, c);
+	}
+}
+
+control(bp: ref Iob, c: int): int
+{
+	case c {
+	AreYouThere =>
+		sys->fprint(net.dfd, "Inferno telnet V1.0\r\n");
+	Sb =>
+		return sub(bp);
+	Will =>
+		return will(bp);
+	Wont =>
+		return wont(bp);
+	Do =>
+		return doit(bp);
+	Dont =>
+		return dont(bp);
+	Se =>
+		sys->fprint(stderr, "telnet: SE without an SB\n");
+	-1 =>
+		return -1;
+	*  =>
+		break;
+	}
+	return 0;
+}
+
+sub(bp: ref Iob): int
+{
+	subneg: string;
+	i := 0;
+	for(;;){
+		c := iobget(bp);
+		if(c == Iac) {
+			c = iobget(bp);
+			if(c == Se)
+				break;
+			subneg[i++] = Iac;
+		}
+		if(c < 0)
+			return -1;
+		subneg[i++] = c;
+	}
+	if(i == 0)
+		return 0;
+
+	sys->fprint(stderr, "sub %d %d n = %d\n", subneg[0], subneg[1], i);
+
+	for(i = 0; i < len opt; i++)
+		if(opt[i].code == subneg[0])
+			break;
+
+	if(i >= len opt)
+		return 0;
+
+	case i {
+	Term =>
+		sbsend(opt[Term].code, array of byte "dumb");	
+	}
+
+	return 0;
+}
+
+sbsend(code: int, data: array of byte): int
+{
+	buf := array[4+len data+2] of byte;
+	o := 4+len data;
+
+	buf[0] = byte Iac;
+	buf[1] = byte Sb;
+	buf[2] = byte code;
+	buf[3] = byte 0;
+	buf[4:] = data;
+	buf[o] = byte Iac;
+	o++;
+	buf[o] = byte Se;
+
+	return sys->write(net.dfd, buf, len buf);
+}
+
+will(bp: ref Iob): int
+{
+	c := iobget(bp);
+	if(c < 0)
+		return -1;
+
+	sys->fprint(stderr, "will %d\n", c);
+
+	for(i := 0; i < len opt; i++)
+		if(opt[i].code == c)
+			break;
+
+	if(i >= len opt) {
+		send3(bp, Iac, Dont, c);
+		return 0;
+	}
+
+	rv := 0;
+	if(opt[i].noway)
+		send3(bp, Iac, Dont, c);
+	else
+	if(opt[i].remote == 0)
+		rv |= send3(bp, Iac, Do, c);
+
+	if(opt[i].remote == 0)
+		rv |= change(bp, i, Will);
+	opt[i].remote = 1;
+	return rv;
+}
+
+wont(bp: ref Iob): int
+{
+	c := iobget(bp);
+	if(c < 0)
+		return -1;
+
+	sys->fprint(stderr, "wont %d\n", c);
+
+	for(i := 0; i < len opt; i++)
+		if(opt[i].code == c)
+			break;
+
+	if(i >= len opt)
+		return 0;
+
+	rv := 0;
+	if(opt[i].remote) {
+		rv |= change(bp, i, Wont);
+		rv |= send3(bp, Iac, Dont, c);
+	}
+	opt[i].remote = 0;
+	return rv;
+}
+
+doit(bp: ref Iob): int
+{
+	c := iobget(bp);
+	if(c < 0)
+		return -1;
+
+	sys->fprint(stderr, "do %d\n", c);
+
+	for(i := 0; i < len opt; i++)
+		if(opt[i].code == c)
+			break;
+
+	if(i >= len opt || opt[i].noway) {
+		send3(bp, Iac, Wont, c);
+		return 0;
+	}
+	rv := 0;
+	if(opt[i].local == 0) {
+		rv |= change(bp, i, Do);
+		rv |= send3(bp, Iac, Will, c);
+	}
+	opt[i].local = 1;
+	return rv;
+}
+
+dont(bp: ref Iob): int
+{
+	c := iobget(bp);
+	if(c < 0)
+		return -1;
+
+	sys->fprint(stderr, "dont %d\n", c);
+
+	for(i := 0; i < len opt; i++)
+		if(opt[i].code == c)
+			break;
+
+	if(i >= len opt || opt[i].noway)
+		return 0;
+
+	rv := 0;
+	if(opt[i].local){
+		opt[i].local = 0;
+		rv |= change(bp, i, Dont);
+		rv |= send3(bp, Iac, Wont, c);
+	}
+	opt[i].local = 0;
+	return rv;
+}
+
+change(nil: ref Iob, nil: int, nil: int): int
+{
+	return 0;
+}
+
+send3(bp: ref Iob, c0: int, c1: int, c2: int): int
+{
+	buf := array[3] of byte;
+
+	buf[0] = byte c0;
+	buf[1] = byte c1;
+	buf[2] = byte c2;
+
+	t: string;
+	case c0 {
+	Will => t = "Will";
+	Wont => t = "Wont";
+	Do =>	t = "Do";
+	Dont => t = "Dont";
+	}
+	if(t != nil)
+		sys->fprint(stderr, "r %s %d\n", t, c1);
+
+	r := sys->write(bp.fd, buf, 3);
+	if(r != 3)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/appl/wm/tetris.b
@@ -1,0 +1,804 @@
+# Copyright  © 1999 Roger Peppe.  All rights reserved.
+implement Tetris;
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+	draw: Draw;
+	Point, Rect: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "rand.m";
+	rand: Rand;
+include "scoretable.m";
+	scoretab: Scoretable;
+include "arg.m";
+include "keyboard.m";
+	Up, Down, Right, Left: import Keyboard;
+
+include "keyring.m";
+include "security.m";	# for random seed
+
+Tetris: module {
+	init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+SCORETABLE: con "/lib/scores/tetris";
+LOCKPORT: con 18343;
+
+# number of pieces across and down board.
+BOARDWIDTH: con 10;
+BOARDHEIGHT: con 22;
+
+awaitingscore := 1;
+	
+Row: adt {
+	tag:		string;
+	delete:	int;
+};
+
+Board: adt {
+	new:			fn(top: ref Tk->Toplevel, w: string,
+					blocksize: int, maxsize: Point): ref Board;
+	makeblock:	fn(bd: self ref Board, colour: string, p: Point): string;
+	moveblock:	fn(bd: self ref Board, b: string, p: Point);
+	movecurr:	fn(bd: self ref Board, delta: Point);
+	delrows:		fn(bd: self ref Board, rows: list of int);
+	landedblock:	fn(bd: self ref Board, b: string, p: Point);
+	setnextshape:	fn(bd: self ref Board, colour: string, spec: array of Point);
+	setscore:		fn(bd: self ref Board, score: int);
+	setlevel:		fn(bd: self ref Board, level: int);
+	setnrows:		fn(bd: self ref Board, level: int);
+	gameover:	fn(bd: self ref Board);
+	update:		fn(bd: self ref Board);
+
+	state:		array of array of byte;
+	w:			string;
+	dx:			int;
+	win:			ref Tk->Toplevel;
+	rows:		array of Row;
+	maxid:		int;
+};
+
+Piece: adt {
+	shape:	int;
+	rot:		int;
+};
+
+Shape: adt {
+	coords:	array of array of Point;
+	colour:	string;
+	score:	array of int;
+};
+
+Game: adt {
+	new:		fn(bd: ref Board): ref Game;
+	move:	fn(g: self ref Game, dx: int);
+	rotate:	fn(g: self ref Game, clockwise: int);
+	tick:		fn(g: self ref Game): int;
+	drop:	fn(g: self ref Game);
+
+	bd:		ref Board;
+	level:	int;
+	delay:	int;
+	score:	int;
+	nrows:	int;
+	pieceids:	array of string;
+	pos:		Point;
+	next,
+	curr:		Piece;
+};
+
+badmod(path: string)
+{
+	sys->fprint(stderr, "tetris: cannot load %s: %r\n", path);
+	raise "fail: bad module";
+}
+
+usage()
+{
+	sys->fprint(stderr, "usage: tetris [-b blocksize]\n");
+	raise "fail:usage";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	stderr = sys->fildes(2);
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	if (tk == nil)
+		badmod(Tk->PATH);
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil)
+		badmod(Tkclient->PATH);
+	tkclient->init();
+	rand = load Rand Rand->PATH;
+	if (rand == nil)
+		badmod(Rand->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmod(Arg->PATH);
+	if (ctxt == nil)
+		ctxt = tkclient->makedrawcontext();
+	blocksize := 17;			# preferred block size
+	arg->init(argv);
+	while ((opt := arg->opt()) != 0) {
+		case opt {
+		'b' =>
+			if ((b := arg->arg()) == nil || int b <= 0)
+				usage();
+			blocksize = int b;
+		* =>
+			usage();
+		}
+	}
+	if (arg->argv() != nil)
+		usage();
+	
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+	scoretab = load Scoretable Scoretable->PATH;
+	scorech := chan of int;
+	spawn scoresrvwait(scorech);
+	(win, winctl) := tkclient->toplevel(ctxt, "", "Tetris",Tkclient->Hide);
+	seedrand();
+	fromuser := chan of string;
+	tk->namechan(win, fromuser, "user");
+	cmd(win, "bind . <Key> {send user k %s}");
+	cmd(win, "bind . <ButtonRelease-1> {focus .}");
+	cmd(win, "bind .Wm_t <ButtonRelease-1> +{focus .}");
+	cmd(win, "focus .");
+
+	maxsize := Point(10000, 10000);
+	if (ctxt.display.image != nil) {
+		img := ctxt.display.image;
+		wsz := wsize(win, ".");
+		maxsize.y = img.r.dy() - wsz.y;
+		maxsize.x = img.r.dx();
+	}
+		
+	tkclient->onscreen(win, nil);
+	tkclient->startinput(win, "kbd"::"ptr"::nil);
+	for (;;) {
+		bd := Board.new(win, ".f", blocksize, maxsize);
+		if (bd == nil) {
+			sys->fprint(stderr, "tetris: couldn't make board\n");
+			return;
+		}
+		cmd(win, "bind .f.c <ButtonRelease-1> {send user m %x %y}");
+		cmd(win, "pack .f -side top");
+		cmd(win, "update");
+		g := Game.new(bd);
+		(finished, rank) := rungame(g, win, fromuser, winctl, scorech);
+		if (finished)
+			break;
+		cmd(win, "pack propagate . 0");
+		if (scoretab != nil) {
+			cmd(win, "destroy .f");
+			if (showhighscores(win, fromuser, winctl, rank) == 0)
+				break;
+		} else
+			cmd(win, "destroy .f");
+	}
+}
+
+wsize(win: ref Tk->Toplevel, w: string): Point
+{
+	bd := int cmd(win, w + " cget -bd");
+	return (int cmd(win, w + " cget -width") + bd * 2,
+		int cmd(win, w + " cget -height") + bd * 2);
+}
+
+rungame(g: ref Game, win: ref Tk->Toplevel, fromuser: chan of string, winctl: chan of string, scorech: chan of int): (int, int)
+{	
+	tickchan := chan of int;
+	spawn ticker(g, tickchan);
+	paused := 0;
+	tch := chan of int;
+
+	gameover := 0;
+	rank := -1;
+	bdsize := wsize(win, ".f.c");
+	boundy := bdsize.y * 2 / 3;
+	id := cmd(win, ".f.c create line " + p2s((0, boundy)) + " " + p2s((bdsize.x, boundy)) +
+			" -fill white");
+	cmd(win, ".f.c lower " + id);
+	for (;;) alt {
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	s := <-fromuser =>
+		key: int;
+		if (s[0] == 'm') {
+			(nil, toks) := sys->tokenize(s, " ");
+			p := Point(int hd tl toks, int hd tl tl toks);
+			if (p.y > boundy)
+				key = ' ';
+			else {
+				x := p.x / (bdsize.x / 3);
+				case x {
+				0 =>
+					key = '7';
+				1 =>
+					key = '8';
+				2 =>
+					key = '9';
+				* =>
+					break;
+				}
+			}
+		} else if (s[0] == 'k')
+			key = int s[1:];
+		else
+			sys->print("oops (%s)\n", s);
+		if (gameover)
+			return (key == 'q', rank);
+		if (paused) {
+			paused = 0;
+			(tickchan, tch) = (tch, tickchan);
+			if (key != 'q')
+				continue;
+		}
+		case key {
+		'9'  or 'c' or Right =>
+			g.move(1);
+		'7' or 'z' or Left =>
+			g.move(-1);
+		'8' or 'x' or Up =>
+			g.rotate(0);
+		' ' or Down =>
+			g.drop();
+		'p' =>
+			paused = 1;
+			(tickchan, tch) = (tch, tickchan);
+		'q' =>
+			g.delay = -1;
+			while (<-tickchan)
+				;
+			return (1, rank);
+		}
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-winctl =>
+		tkclient->wmctl(win, s);
+	n := <-tickchan =>
+		if (g.tick() == -1) {
+			while (n)
+				n = <-tickchan;
+			if (awaitingscore && !<-scorech) {
+				awaitingscore = 0;
+				scoretab = nil;
+			}
+			if (scoretab != nil)
+				rank = scoretab->setscore(g.score, sys->sprint("%d %d %bd", g.nrows, g.level,
+						big readfile("/dev/time") / big 1000000));
+			gameover = 1;
+		}
+	ok := <-scorech =>
+		awaitingscore = 0;
+		if (!ok)
+			scoretab = nil;
+	}
+}
+
+tablerow(win: ref Tk->Toplevel, w, bg: string, relief: string, vals: array of string, widths: array of string)
+{
+	cmd(win, "frame " + w + " -bd 2 -relief " + relief);
+	for (i := 0; i < len vals; i++) {
+		cw := cmd(win, "label " + w + "." + string i + " -text " + tk->quote(vals[i]) + " -width " + widths[i] + bg);
+		cmd(win, "pack " + cw + " -side left -anchor w");
+	}
+	cmd(win, "pack " + w + " -side top");
+}
+
+showhighscores(win: ref Tk->Toplevel, fromuser: chan of string, winctl: chan of string, rank: int): int
+{
+	widths := array[] of {"10w", "7w", "7w", "5w"};	# user, score, level, rows
+	cmd(win, "frame .f -bd 4 -relief raised");
+	cmd(win, "label .f.title -text {High Scores}");
+	cmd(win, "pack .f.title -side top -anchor n");
+	tablerow(win, ".f.h", nil, "raised", array[] of {"User", "Score", "Level", "Rows"}, widths);
+	sl := scoretab->scores();
+	n := 0;
+	while (sl != nil) {
+		s := hd sl;
+		bg := "";
+		if (n == rank)
+			bg = " -bg white";
+		f := ".f.f" + string n++;
+		nrows := level := "";
+		(nil, toks) := sys->tokenize(s.other, " ");
+		if (toks != nil)
+			(nrows, toks) = (hd toks, tl toks);
+		if (toks != nil)
+			level = hd toks;
+		tablerow(win, f, bg, "sunken", array[] of {s.user, string s.score, level, nrows}, widths);
+		sl = tl sl;
+	}
+	cmd(win, "button .f.b -text {New game} -command {send user s}");
+	cmd(win, "pack .f.b -side top");
+	cmd(win, "pack .f -side top");
+	cmd(win, "update");
+	for (;;) alt {
+	s := <-win.ctxt.kbd =>
+		tk->keyboard(win, s);
+	s := <-win.ctxt.ptr =>
+		tk->pointer(win, *s);
+	s := <-fromuser =>
+		if (s[0] == 'k') {
+			cmd(win, "destroy .f");
+			return int s[1:] != 'q';
+		} else if (s[0] == 's') {
+			cmd(win, "destroy .f");
+			return 1;
+		}
+	s := <-win.ctxt.ctl or
+	s = <-win.wreq or
+	s = <-winctl =>
+		tkclient->wmctl(win, s);
+	}
+}
+
+scoresrvwait(ch: chan of int)
+{
+	if (scoretab == nil) {
+		ch <-= 0;
+		return;
+	}
+	(ok, err) := scoretab->init(LOCKPORT, readfile("/dev/user"), "tetris", SCORETABLE);
+	if (ok != -1)
+		ch <-= 1;
+	else {
+		if (err != "timeout")
+			sys->fprint(stderr, "tetris: scoretable error: %s\n", err);
+		else
+			sys->fprint(stderr, "tetris: timed out trying to connect to score server\n");
+		ch <-= 0;
+	}
+}
+
+readfile(f: string): string
+{
+	fd := sys->open(f, Sys->OREAD);
+	if (fd == nil)
+		return nil;
+	buf := array[Sys->ATOMICIO] of byte;
+	n := sys->read(fd, buf, len buf);
+	if (n <= 0)
+		return nil;
+	return string buf[0:n];
+}
+
+ticker(g: ref Game, c: chan of int)
+{
+	c <-= 1;
+	while (g.delay >= 0) {
+		sys->sleep(g.delay);
+		c <-= 1;
+	}
+	c <-= 0;
+}
+
+seedrand()
+{
+	random := load Random Random->PATH;
+	if (random == nil) {
+		sys->fprint(stderr, "tetris: cannot load %s: %r\n", Random->PATH);
+		return;
+	}
+	seed := random->randomint(Random->ReallyRandom);
+	rand->init(seed);
+}
+
+Game.new(bd: ref Board): ref Game
+{
+	g := ref Game;
+	g.bd = bd;
+	g.level = 0;
+	g.pieceids = array[4] of string;
+	g.score = 0;
+	g.delay = delays[g.level];
+	g.nrows = 0;
+	g.next = randompiece();
+	newpiece(g);
+	bd.update();
+	return g;
+}
+
+randompiece(): Piece
+{
+	p: Piece;
+	p.shape = rand->rand(len shapes);
+	p.rot = rand->rand(len shapes[p.shape].coords);
+	return p;
+}
+
+Game.move(g: self ref Game, dx: int)
+{
+	np := g.pos.add((dx, 0));
+	if (canmove(g, g.curr, np)) {
+		g.bd.movecurr((dx, 0));
+		g.bd.update();
+		g.pos = np;
+	}
+}
+
+Game.rotate(g: self ref Game, clockwise: int)
+{
+	inc := 1;
+	if (!clockwise)
+		inc = -1;
+	npiece := g.curr;
+	coords := shapes[npiece.shape].coords;
+	nrots := len coords;
+	npiece.rot = (npiece.rot + inc + nrots) % nrots;
+	if (canmove(g, npiece, g.pos)) {
+		c := coords[npiece.rot];
+		for (i := 0; i < len c; i++)
+			g.bd.moveblock(g.pieceids[i], g.pos.add(c[i]));
+		g.curr = npiece;
+		g.bd.update();
+	}
+}
+		
+Game.tick(g: self ref Game): int
+{
+	if (canmove(g, g.curr, g.pos.add((0, 1)))) {
+		g.bd.movecurr((0, 1));
+		g.pos.y++;
+	} else {
+		c := shapes[g.curr.shape].coords[g.curr.rot];
+		max := g.pos.y;
+		min := g.pos.y + 4;
+		for (i := 0; i < len c; i++) {
+			p := g.pos.add(c[i]);
+			if (p.y < 0) {
+				g.delay = -1;
+				g.bd.gameover();
+				g.bd.update();
+				return -1;
+			}
+			if (p.y > max)
+				max = p.y;
+			if (p.y < min)
+				min = p.y;
+			g.bd.landedblock(g.pieceids[i], p);
+		}
+		full: list of int;
+		for (i = min; i <= max; i++) {
+			for (x := 0; x < BOARDWIDTH; x++)
+				if (g.bd.state[i][x] == byte 0)
+					break;
+			if (x == BOARDWIDTH)
+				full = i :: full;
+		}
+		if (full != nil) {
+			g.bd.delrows(full);
+			g.nrows += len full;
+			g.bd.setnrows(g.nrows);
+			level := g.nrows / 10;
+			if (level != g.level) {
+				g.bd.setlevel(level);
+				g.level  = level;
+				if (level >= len delays)
+					level = len delays - 1;
+				g.delay = delays[level];
+			}
+		}
+		g.score += shapes[g.curr.shape].score[g.curr.rot];
+		g.bd.setscore(g.score);
+		newpiece(g);
+	}
+	g.bd.update();
+	return 0;
+}
+
+Game.drop(g: self ref Game)
+{
+	p := g.pos.add((0, 1));
+	while (canmove(g, g.curr, p))
+		p.y++;
+	p.y--;
+	g.bd.movecurr((0, p.y - g.pos.y));
+	g.pos = p;
+	g.bd.update();
+}
+
+canmove(g: ref Game, piece: Piece, p: Point): int
+{
+	c := shapes[piece.shape].coords[piece.rot];
+	for (i := 0; i < len c; i++) {
+		q := p.add(c[i]);
+		if (q.x < 0 || q.x >= BOARDWIDTH || q.y >= BOARDHEIGHT)
+			return 0;
+		if (q.y >= 0 && int g.bd.state[q.y][q.x])
+			return 0;
+	}
+	return 1;
+}
+
+newpiece(g: ref Game)
+{
+	g.curr = g.next;
+	g.next = randompiece();
+	g.bd.setnextshape(shapes[g.next.shape].colour, shapes[g.next.shape].coords[g.next.rot]);
+	shape := shapes[g.curr.shape];
+	coords := shape.coords[g.curr.rot];
+	g.pos = (3, -4);
+	for (i := 0; i < len coords; i++)
+		g.pieceids[i] = g.bd.makeblock(shape.colour, g.pos.add(coords[i]));
+}
+
+p2s(p: Point): string
+{
+	return string p.x + " " + string p.y;
+}
+
+Board.new(top: ref Tk->Toplevel, w: string, blocksize: int, maxsize: Point): ref Board
+{
+	cmd(top, "frame " + w);
+	cmd(top, "canvas " + w + ".c -borderwidth 2 -relief sunken -width 1 -height 1");
+	cmd(top, "frame " + w + ".f");
+	cmd(top, "canvas " + w + ".f.ns -width 1 -height 1");
+	makescorewidget(top, w + ".f.scoref", "Score");
+	makescorewidget(top, w + ".f.levelf", "Level");
+	makescorewidget(top, w + ".f.rowsf", "Rows");
+	cmd(top, "pack " + w + ".c -side left");
+	cmd(top, "pack " + w + ".f -side top");
+	cmd(top, "pack " + w + ".f.ns -side top");
+	cmd(top, "pack " + w + ".f.scoref -side top -fill x");
+	cmd(top, "pack " + w + ".f.levelf -side top -fill x");
+	cmd(top, "pack " + w + ".f.rowsf -side top -fill x");
+
+	sz := wsize(top, w);
+	avail := Point(maxsize.x - sz.x, maxsize.y);
+	avail.x /= BOARDWIDTH;
+	avail.y /= BOARDHEIGHT;
+	dx := avail.x;
+	if (avail.y < avail.x)
+		dx = avail.y;
+	if (dx <= 0)
+		return nil;
+	if (dx > blocksize)
+		dx = blocksize;
+	cmd(top, w + ".f.ns configure -width " + string(4 * dx + 1 - 2*2) +
+			" -height " + string(4 * dx + 1 - 2*2));
+	cmd(top, w + ".c configure -width " + string(dx * BOARDWIDTH + 1) +
+		" -height " + string(dx * BOARDHEIGHT + 1));
+	bd := ref Board(array[BOARDHEIGHT]
+					of {* => array[BOARDWIDTH] of {* => byte 0}},
+			w, dx, top, array[BOARDHEIGHT]  of {* => Row(nil, 0)}, 1);
+	return bd;
+}
+
+makescorewidget(top: ref Tk->Toplevel, w, title: string)
+{
+	cmd(top, "frame " + w);
+	cmd(top, "label " + w + ".title -text " + tk->quote(title));
+	cmd(top, "label " + w +
+		".val -bd 2 -relief sunken -width 5w -text 0 -anchor e");
+	cmd(top, "pack " + w + ".title -side left -anchor w");
+	cmd(top, "pack " + w + ".val -side right -anchor e");
+}
+
+blockrect(bd: ref Board, p: Point): string
+{
+	p = p.mul(bd.dx);
+	q := p.add((bd.dx, bd.dx));
+	return string p.x + " " + string p.y + " " + string q.x + " " + string q.y;
+}
+
+Board.makeblock(bd: self ref Board, colour: string, p: Point): string
+{
+	tag := cmd(bd.win, bd.w + ".c create rectangle " + blockrect(bd, p) + " -fill " + colour + " -tags curr");
+	if (tag != nil && tag[0] == '!')
+		return nil;
+	return tag;
+}
+
+Board.moveblock(bd: self ref Board, b: string, p: Point)
+{
+	cmd(bd.win, bd.w + ".c coords " + b + " " + blockrect(bd, p));
+}
+
+Board.movecurr(bd: self ref Board, delta: Point)
+{
+	delta = delta.mul(bd.dx);
+	cmd(bd.win, bd.w + ".c move curr " + string delta.x + " " + string delta.y);
+}
+
+Board.landedblock(bd: self ref Board, b: string, p: Point)
+{
+	cmd(bd.win, bd.w + ".c dtag " + b + " curr");
+	rs := cmd(bd.win, bd.w + ".c coords " + b);
+	if (rs != nil && rs[0] == '!')
+		return;
+	(nil, toks) := sys->tokenize(rs, " ");
+	if (len toks != 4) {
+		sys->fprint(stderr, "bad coords for block %s\n", b);
+		return;
+	}
+	y := int hd tl toks / bd.dx;
+	if (y < 0)
+		return;
+	if (y >= BOARDHEIGHT) {
+		sys->fprint(stderr, "block '%s' too far down (coords %s)\n", b, rs);
+		return;
+	}
+	rtag := bd.rows[y].tag;
+	if (rtag == nil)
+		rtag = bd.rows[y].tag = "r" + string bd.maxid++;
+	cmd(bd.win, bd.w + ".c addtag " + rtag + " withtag " + b);
+	if (p.y >= 0)
+		bd.state[p.y][p.x] = byte 1;
+}
+	
+Board.delrows(bd: self ref Board, rows: list of int)
+{
+	while (rows != nil) {
+		r := hd rows;
+		bd.rows[r].delete = 1;
+		rows = tl rows;
+	}
+	j := BOARDHEIGHT - 1;
+	for (i := BOARDHEIGHT - 1; i >= 0; i--) {
+		if (bd.rows[i].delete) {
+			cmd(bd.win, bd.w + ".c delete " + bd.rows[i].tag);
+			bd.rows[i] = (nil, 0);
+			bd.state[i] = nil;
+		} else {
+			if (i != j && bd.rows[i].tag != nil) {
+				dy := (j - i) * bd.dx;
+				cmd(bd.win, bd.w + ".c move " + bd.rows[i].tag + " 0 " + string dy);
+				bd.rows[j] = bd.rows[i];
+				bd.rows[i] = (nil, 0);
+				bd.state[j] = bd.state[i];
+				bd.state[i] = nil;
+			}
+			j--;
+		}
+	}
+	for (i = 0; i < BOARDHEIGHT; i++)
+		if (bd.state[i] == nil)
+			bd.state[i] = array[BOARDWIDTH] of {* => byte 0};
+}
+
+Board.update(bd: self ref Board)
+{
+	cmd(bd.win, "update");
+}
+
+Board.setnextshape(bd: self ref Board, colour: string, spec: array of Point)
+{
+	cmd(bd.win, bd.w + ".f.ns delete all");
+	min := Point(4,4);
+	max := Point(0,0);
+	for (i := 0; i < len spec; i++) {
+		if (spec[i].x > max.x) max.x = spec[i].x;
+		if (spec[i].x < min.x) min.x = spec[i].x;
+		if (spec[i].y > max.y) max.y = spec[i].y;
+		if (spec[i].y < min.y) min.y = spec[i].y;
+	}
+	o: Point;
+	o.x = (4 - (max.x - min.x + 1)) * bd.dx / 2 - min.x * bd.dx;
+	o.y = (4 - (max.y - min.y + 1)) * bd.dx / 2 - min.y * bd.dx;
+	for (i = 0; i < len spec; i++) {
+		br := Rect(o.add(spec[i].mul(bd.dx)), o.add(spec[i].add((1,1)).mul(bd.dx)));
+		cmd(bd.win, bd.w + ".f.ns create rectangle " +
+			string br.min.x + " " + string br.min.y + " " + string br.max.x + " " + string br.max.y +
+			" -fill " + colour);
+	}
+}
+
+Board.setscore(bd: self ref Board, score: int)
+{
+	cmd(bd.win, bd.w + ".f.scoref.val configure -text " + string score);
+}
+
+Board.setlevel(bd: self ref Board, level: int)
+{
+	cmd(bd.win, bd.w + ".f.levelf.val configure -text " + string level);
+}
+
+Board.setnrows(bd: self ref Board, nrows: int)
+{
+	cmd(bd.win, bd.w + ".f.rowsf.val configure -text " + string nrows);
+}
+
+Board.gameover(bd: self ref Board)
+{
+	cmd(bd.win, "label " + bd.w + ".gameover -text {Game over} -bd 4 -relief ridge");
+	p := Point(BOARDWIDTH * bd.dx / 2, BOARDHEIGHT * bd.dx / 3);
+	cmd(bd.win, bd.w + ".c create window " + string p.x + " " + string p.y + " -window " + bd.w + ".gameover");
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	e := tk->cmd(top, s);
+#	sys->print("%s\n", s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(stderr, "tetris: tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+VIOLET: con "#ffaaff";
+CYAN: con "#93ddf1";
+
+delays := array[] of {300, 250, 200, 150, 100, 80};
+
+shapes := array[] of {
+Shape(
+	# ####
+	array[] of {
+		array[] of {Point(0,1), Point(1,1), Point(2,1), Point(3,1)},
+		array[] of {Point(1,0), Point(1,1), Point(1,2), Point(1,3)},
+	},
+	"red",
+	array[] of {5, 8}),
+Shape(
+	# ##
+	# ##
+	array[] of {
+		array[] of {Point(0,0), Point(0,1), Point(1,0), Point(1,1)},
+	},
+	"orange",
+	array[] of {6}),
+Shape(
+	# #
+	# ##
+	# #
+	array[] of {
+		array[] of {Point(1,0), Point(0,1), Point(1,1), Point(2,1)},
+		array[] of {Point(1,0), Point(1,1), Point(2,1), Point(1,2)},
+		array[] of {Point(0,1), Point(1,1), Point(2,1), Point(1,2)},
+		array[] of {Point(1,0), Point(0,1), Point(1,1), Point(1,2)},
+	},
+	"yellow",
+	array[] of {5,5,6,5}),
+Shape(
+	# ##
+	#  ##
+	array[] of {
+		array[] of {Point(0,0), Point(1,0), Point(1,1), Point(2,1)},
+		array[] of {Point(1,0), Point(0,1), Point(1,1), Point(0,2)},
+	},
+	"green",
+	array[] of {6,7}),
+Shape(
+	#  ##
+	# ##
+	array[] of {
+		array[] of {Point(1,0), Point(2,0), Point(0,1), Point(1,1)},
+		array[] of {Point(0,0), Point(0,1), Point(1,1), Point(1,2)},
+	},
+	"blue",
+	array[] of {6,7}),
+Shape(
+	# ###
+	# #
+	array[] of {
+		array[] of {Point(2,0), Point(0,1), Point(1,1), Point(2,1)},
+		array[] of {Point(0,0), Point(0,1), Point(0,2), Point(1,2)},
+		array[] of {Point(0,0), Point(1,0), Point(2,0), Point(0,1)},
+		array[] of {Point(0,0), Point(1,0), Point(1,1), Point(1,2)},
+	},
+	CYAN,
+	array[] of {6,7,6,7}),
+Shape(
+	# #
+	# ###
+	array[] of {
+		array[] of {Point(0,0), Point(1,0), Point(2,0), Point(2,1)},
+		array[] of {Point(1,0), Point(1,1), Point(0,2), Point(1,2)},
+		array[] of {Point(0,0), Point(0,1), Point(1,1), Point(2,1)},
+		array[] of {Point(0,0), Point(1,0), Point(0,1), Point(0,2)},
+	},
+	VIOLET,
+	array[] of {6,7,6,7}
+),
+};
+
--- /dev/null
+++ b/appl/wm/toolbar.b
@@ -1,0 +1,568 @@
+implement Toolbar;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Screen, Display, Image, Rect, Point, Wmcontext, Pointer: import draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "sh.m";
+	shell: Sh;
+	Listnode, Context: import shell;
+include "string.m";
+	str: String;
+include "arg.m";
+
+myselfbuiltin: Shellbuiltin;
+
+Toolbar: module 
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+	initbuiltin: fn(c: ref Context, sh: Sh): string;
+	runbuiltin: fn(c: ref Context, sh: Sh,
+			cmd: list of ref Listnode, last: int): string;
+	runsbuiltin: fn(c: ref Context, sh: Sh,
+			cmd: list of ref Listnode): list of ref Listnode;
+	whatis: fn(c: ref Sh->Context, sh: Sh, name: string, wtype: int): string;
+	getself: fn(): Shellbuiltin;
+};
+
+MAXCONSOLELINES:	con 1024;
+
+# execute this if no menu items have been created
+# by the init script.
+defaultscript :=
+	"{menu shell " +
+		"{{autoload=std; load $autoload; pctl newpgrp; wm/sh}&}}";
+
+tbtop: ref Tk->Toplevel;
+screenr: Rect;
+
+badmodule(p: string)
+{
+	sys->fprint(stderr(), "toolbar: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys  = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	if(draw == nil)
+		badmodule(Draw->PATH);
+	tk   = load Tk Tk->PATH;
+	if(tk == nil)
+		badmodule(Tk->PATH);
+
+	str = load String String->PATH;
+	if(str == nil)
+		badmodule(String->PATH);
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if(tkclient == nil)
+		badmodule(Tkclient->PATH);
+	tkclient->init();
+
+	shell = load Sh Sh->PATH;
+	if (shell == nil)
+		badmodule(Sh->PATH);
+	arg := load Arg Arg->PATH;
+	if (arg == nil)
+		badmodule(Arg->PATH);
+
+	myselfbuiltin = load Shellbuiltin "$self";
+	if (myselfbuiltin == nil)
+		badmodule("$self(Shellbuiltin)");
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+
+	sys->bind("#p", "/prog", sys->MREPL);
+	sys->bind("#s", "/chan", sys->MBEFORE);
+
+	arg->init(argv);
+	arg->setusage("toolbar [-s] [-p]");
+	startmenu := 1;
+#	ownsnarf := (sys->open("/chan/snarf", Sys->ORDWR) == nil);
+	ownsnarf := sys->stat("/chan/snarf").t0 < 0;
+	while((c := arg->opt()) != 0){
+		case c {
+		's' =>
+			startmenu = 0;
+		'p' =>
+			ownsnarf = 1;
+		* =>
+			arg->usage();
+		}
+	}
+	argv = arg->argv();
+	arg = nil;
+
+	if (ctxt == nil){
+		sys->fprint(sys->fildes(2), "toolbar: must run under a window manager\n");
+		raise "fail:no wm";
+	}
+
+	exec := chan of string;
+	task := chan of string;
+
+	tbtop = toolbar(ctxt, startmenu, exec, task);
+	tkclient->startinput(tbtop, "ptr" :: "control" :: nil);
+	layout(tbtop);
+
+	shctxt := Context.new(ctxt);
+	shctxt.addmodule("wm", myselfbuiltin);
+
+	snarfIO: ref Sys->FileIO;
+	if(ownsnarf){
+		snarfIO = sys->file2chan("/chan", "snarf");
+		if(snarfIO == nil)
+			fatal(sys->sprint("cannot make /chan/snarf: %r"));
+	}else
+		snarfIO = ref Sys->FileIO(chan of (int, int, int, Sys->Rread), chan of (int, array of byte, int, Sys->Rwrite));
+	sync := chan of string;
+	spawn consoleproc(ctxt, sync);
+	if ((err := <-sync) != nil)
+		fatal(err);
+
+	setupfinished := chan of int;
+	donesetup := 0;
+	spawn setup(shctxt, setupfinished);
+
+	snarf: array of byte;
+#	write("/prog/"+string sys->pctl(0, nil)+"/ctl", "restricted"); # for testing
+	for(;;) alt{
+	k := <-tbtop.ctxt.kbd =>
+		tk->keyboard(tbtop, k);
+	m := <-tbtop.ctxt.ptr =>
+		tk->pointer(tbtop, *m);
+	s := <-tbtop.ctxt.ctl or
+	s = <-tbtop.wreq =>
+		wmctl(tbtop, s);
+	s := <-exec =>
+		# guard against parallel access to the shctxt environment
+		if (donesetup){
+			{
+ 				shctxt.run(ref Listnode(nil, s) :: nil, 0);
+			} exception {
+			"fail:*" =>	;
+			}
+		}
+	detask := <-task =>
+		deiconify(detask);
+	(off, data, nil, wc) := <-snarfIO.write =>
+		if(wc == nil)
+			break;
+		if (off == 0)			# write at zero truncates
+			snarf = data;
+		else {
+			if (off + len data > len snarf) {
+				nsnarf := array[off + len data] of byte;
+				nsnarf[0:] = snarf;
+				snarf = nsnarf;
+			}
+			snarf[off:] = data;
+		}
+		wc <-= (len data, "");
+	(off, nbytes, nil, rc) := <-snarfIO.read =>
+		if(rc == nil)
+			break;
+		if (off >= len snarf) {
+			rc <-= (nil, "");		# XXX alt
+			break;
+		}
+		e := off + nbytes;
+		if (e > len snarf)
+			e = len snarf;
+		rc <-= (snarf[off:e], "");	# XXX alt
+	donesetup = <-setupfinished =>
+		;	
+	}
+}
+
+wmctl(top: ref Tk->Toplevel, c: string)
+{
+	args := str->unquoted(c);
+	if(args == nil)
+		return;
+	n := len args;
+
+	case hd args{
+	"request" =>
+		# request clientid args...
+		if(n < 3)
+			return;
+		args = tl args;
+		clientid := hd args;
+		args = tl args;
+		err := handlerequest(clientid, args);
+		if(err != nil)
+			sys->fprint(sys->fildes(2), "toolbar: bad wmctl request %#q: %s\n", c, err);
+	"newclient" =>
+		# newclient id
+		;
+	"delclient" =>
+		# delclient id
+		deiconify(hd tl args);
+	"rect" =>
+		tkclient->wmctl(top, c);
+		layout(top);
+	* =>
+		tkclient->wmctl(top, c);
+	}
+}
+
+handlerequest(clientid: string, args: list of string): string
+{
+	n := len args;
+	case hd args {
+	"task" =>
+		# task name
+		if(n != 2)
+			return "no task label given";
+		iconify(clientid, hd tl args);
+	"untask" or
+	"unhide" =>
+		deiconify(clientid);
+	* =>
+		return "unknown request";
+	}
+	return nil;
+}
+
+iconify(id, label: string)
+{
+	label = condenselabel(label);
+	e := tk->cmd(tbtop, "button .toolbar." +id+" -command {send task "+id+"} -takefocus 0");
+	cmd(tbtop, ".toolbar." +id+" configure -text '" + label);
+	if(e[0] != '!')
+		cmd(tbtop, "pack .toolbar."+id+" -side left -fill y");
+	cmd(tbtop, "update");
+}
+
+deiconify(id: string)
+{
+	e := tk->cmd(tbtop, "destroy .toolbar."+id);
+	if(e == nil){
+		tkclient->wmctl(tbtop, sys->sprint("ctl %q untask", id));
+		tkclient->wmctl(tbtop, sys->sprint("ctl %q kbdfocus 1", id));
+	}
+	cmd(tbtop, "update");
+}
+
+layout(top: ref Tk->Toplevel)
+{
+	r := top.screenr;
+	h := 32;
+	if(r.dy() < 480)
+		h = tk->rect(top, ".b", Tk->Border|Tk->Required).dy();
+	cmd(top, ". configure -x " + string r.min.x +
+			" -y " + string (r.max.y - h) +
+			" -width " + string r.dx() +
+			" -height " + string h);
+	cmd(top, "update");
+	tkclient->onscreen(tbtop, "exact");
+}
+
+toolbar(ctxt: ref Draw->Context, startmenu: int,
+		exec, task: chan of string): ref Tk->Toplevel
+{
+	(tbtop, nil) = tkclient->toplevel(ctxt, nil, nil, Tkclient->Plain);
+	screenr = tbtop.screenr;
+
+	cmd(tbtop, "button .b -text {XXX}");
+	cmd(tbtop, "pack propagate . 0");
+
+	tk->namechan(tbtop, exec, "exec");
+	tk->namechan(tbtop, task, "task");
+	cmd(tbtop, "frame .toolbar");
+	if (startmenu) {
+		cmd(tbtop, "menubutton .toolbar.start -menu .m -borderwidth 0 -bitmap vitasmall.bit");
+		cmd(tbtop, "pack .toolbar.start -side left");
+	}
+	cmd(tbtop, "pack .toolbar -fill x");
+	cmd(tbtop, "menu .m");
+	return tbtop;
+}
+
+setup(shctxt: ref Context, finished: chan of int)
+{
+	ctxt := shctxt.copy(0);
+	ctxt.run(shell->stringlist2list("run"::"/lib/wmsetup"::nil), 0);
+	# if no items in menu, then create some.
+	if (tk->cmd(tbtop, ".m type 0")[0] == '!')
+		ctxt.run(shell->stringlist2list(defaultscript::nil), 0);
+	cmd(tbtop, "update");
+	finished <-= 1;
+}
+
+condenselabel(label: string): string
+{
+	if(len label > 15){
+		new := "";
+		l := 0;
+		while(len label > 15 && l < 3) {
+			new += label[0:15]+"\n";
+			label = label[15:];
+			for(v := 0; v < len label; v++)
+				if(label[v] != ' ')
+					break;
+			label = label[v:];
+			l++;
+		}
+		label = new + label;
+	}
+	return label;
+}
+
+initbuiltin(ctxt: ref Context, nil: Sh): string
+{
+	if (tbtop == nil) {
+		sys = load Sys Sys->PATH;
+		sys->fprint(sys->fildes(2), "wm: cannot load wm as a builtin\n");
+		raise "fail:usage";
+	}
+	ctxt.addbuiltin("menu", myselfbuiltin);
+	ctxt.addbuiltin("delmenu", myselfbuiltin);
+	ctxt.addbuiltin("error", myselfbuiltin);
+	return nil;
+}
+
+whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
+{
+	return nil;
+}
+
+runbuiltin(c: ref Context, sh: Sh,
+			cmd: list of ref Listnode, nil: int): string
+{
+	case (hd cmd).word {
+	"menu" =>	return builtin_menu(c, sh, cmd);
+	"delmenu" =>	return builtin_delmenu(c, sh, cmd);
+	}
+	return nil;
+}
+
+runsbuiltin(nil: ref Context, nil: Sh,
+			nil: list of ref Listnode): list of ref Listnode
+{
+	return nil;
+}
+
+stderr(): ref Sys->FD
+{
+	return sys->fildes(2);
+}
+
+word(ln: ref Listnode): string
+{
+	if (ln.word != nil)
+		return ln.word;
+	if (ln.cmd != nil)
+		return shell->cmd2string(ln.cmd);
+	return nil;
+}
+
+menupath(title: string): string
+{
+	mpath := ".m."+title;
+	for(j := 0; j < len mpath; j++)
+		if(mpath[j] == ' ')
+			mpath[j] = '_';
+	return mpath;
+}
+
+builtin_menu(nil: ref Context, nil: Sh, argv: list of ref Listnode): string
+{
+	n := len argv;
+	if (n < 3 || n > 4) {
+		sys->fprint(stderr(), "usage: menu topmenu [ secondmenu ] command\n");
+		raise "fail:usage";
+	}
+	primary := (hd tl argv).word;
+	argv = tl tl argv;
+
+	if (n == 3) {
+		w := word(hd argv);
+		if (len w == 0)
+			cmd(tbtop, ".m insert 0 separator");
+		else
+			cmd(tbtop, ".m insert 0 command -label " + tk->quote(primary) +
+				" -command {send exec " + w + "}");
+	} else {
+		secondary := (hd argv).word;
+		argv = tl argv;
+
+		mpath := menupath(primary);
+		e := tk->cmd(tbtop, mpath+" cget -width");
+		if(e[0] == '!') {
+			cmd(tbtop, "menu "+mpath);
+			cmd(tbtop, ".m insert 0 cascade -label "+tk->quote(primary)+" -menu "+mpath);
+		}
+		w := word(hd argv);
+		if (len w == 0)
+			cmd(tbtop, mpath + " insert 0 separator");
+		else
+			cmd(tbtop, mpath+" insert 0 command -label "+tk->quote(secondary)+
+				" -command {send exec "+w+"}");
+	}
+	return nil;
+}
+
+builtin_delmenu(nil: ref Context, nil: Sh, nil: list of ref Listnode): string
+{
+	delmenu(".m");
+	cmd(tbtop, "menu .m");
+	return nil;
+}
+
+delmenu(m: string)
+{
+	for (i := int cmd(tbtop, m + " index end"); i >= 0; i--)
+		if (cmd(tbtop, m + " type " + string i) == "cascade")
+			delmenu(cmd(tbtop, m + " entrycget " + string i + " -menu"));
+	cmd(tbtop, "destroy " + m);
+}
+
+getself(): Shellbuiltin
+{
+	return myselfbuiltin;
+}
+
+cmd(top: ref Tk->Toplevel, c: string): string
+{
+	s := tk->cmd(top, c);
+	if (s != nil && s[0] == '!')
+		sys->fprint(stderr(), "tk error on %#q: %s\n", c, s);
+	return s;
+}
+
+kill(pid: int, note: string): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", note) < 0)
+		return -1;
+	return 0;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "wm: %s\n", s);
+	kill(sys->pctl(0, nil), "killgrp");
+	raise "fail:error";
+}
+
+bufferproc(in, out: chan of string)
+{
+	h, t: list of string;
+	dummyout := chan of string;
+	for(;;){
+		outc := dummyout;
+		s: string;
+		if(h != nil || t != nil){
+			outc = out;
+			if(h == nil)
+				for(; t != nil; t = tl t)
+					h = hd t :: h;
+			s = hd h;
+		}
+		alt{
+		x := <-in =>
+			t = x :: t;
+		outc <-= s =>
+			h = tl h;
+		}
+	}
+}
+
+con_cfg := array[] of
+{
+	"frame .cons",
+	"scrollbar .cons.scroll -command {.cons.t yview}",
+	"text .cons.t -width 60w -height 15w -bg white "+
+		"-fg black -font /fonts/misc/latin1.6x13.font "+
+		"-yscrollcommand {.cons.scroll set}",
+	"pack .cons.scroll -side left -fill y",
+	"pack .cons.t -fill both -expand 1",
+	"pack .cons -expand 1 -fill both",
+	"pack propagate . 0",
+	"update"
+};
+nlines := 0;		# transcript length
+
+consoleproc(ctxt: ref Draw->Context, sync: chan of string)
+{
+	iostdout := sys->file2chan("/chan", "wmstdout");
+	if(iostdout == nil){
+		sync <-= sys->sprint("cannot make /chan/wmstdout: %r");
+		return;
+	}
+	iostderr := sys->file2chan("/chan", "wmstderr");
+	if(iostderr == nil){
+		sync <-= sys->sprint("cannot make /chan/wmstdout: %r");
+		return;
+	}
+
+	sync <-= nil;
+
+	(top, titlectl) := tkclient->toplevel(ctxt, "", "Log", tkclient->Appl); 
+	for(i := 0; i < len con_cfg; i++)
+		cmd(top, con_cfg[i]);
+
+	r := tk->rect(top, ".", Tk->Border|Tk->Required);
+	cmd(top, ". configure -x " + string ((top.screenr.dx() - r.dx()) / 2 + top.screenr.min.x) +
+				" -y " + string (r.dy() / 3 + top.screenr.min.y));
+
+	tkclient->startinput(top, "ptr"::"kbd"::nil);
+	tkclient->onscreen(top, "onscreen");
+	tkclient->wmctl(top, "task");
+
+	for(;;) alt {
+	c := <-titlectl or
+	c = <-top.wreq or
+	c = <-top.ctxt.ctl =>
+		if(c == "exit")
+			c = "task";
+		tkclient->wmctl(top, c);
+	c := <-top.ctxt.kbd =>
+		tk->keyboard(top, c);
+	p := <-top.ctxt.ptr =>
+		tk->pointer(top, *p);
+	(nil, nil, nil, rc) := <-iostdout.read =>
+		if(rc != nil)
+			rc <-= (nil, "inappropriate use of file");
+	(nil, nil, nil, rc) := <-iostderr.read =>
+		if(rc != nil)
+			rc <-= (nil, "inappropriate use of file");
+	(nil, data, nil, wc) := <-iostdout.write =>
+		conout(top, data, wc);
+	(nil, data, nil, wc) := <-iostderr.write =>
+		conout(top, data, wc);
+		if(wc != nil)
+			tkclient->wmctl(top, "untask");
+	}
+}
+
+conout(top: ref Tk->Toplevel, data: array of byte, wc: Sys->Rwrite)
+{
+	if(wc == nil)
+		return;
+
+	s := string data;
+	tk->cmd(top, ".cons.t insert end '"+ s);
+	alt{
+	wc <-= (len data, nil) =>;
+	* =>;
+	}
+
+	for(i := 0; i < len s; i++)
+		if(s[i] == '\n')
+			nlines++;
+	if(nlines > MAXCONSOLELINES){
+		cmd(top, ".cons.t delete 1.0 " + string (nlines/4) + ".0; update");
+		nlines -= nlines / 4;
+	}
+
+	tk->cmd(top, ".cons.t see end; update");
+}
--- /dev/null
+++ b/appl/wm/unibrowse.b
@@ -1,0 +1,972 @@
+implement Unibrowse;
+
+# unicode browser for inferno.
+# roger peppe (rog@ohm.york.ac.uk)
+
+include "sys.m";
+	sys: Sys;
+	stderr: ref Sys->FD;
+include "draw.m";
+	draw: Draw;
+include "tk.m";
+	tk: Tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "dialog.m";
+	dialog: Dialog;
+include "selectfile.m";
+	selectfile: Selectfile;
+include "string.m";
+	str: String;
+include "bufio.m";
+	bio: Bufio;
+
+Unibrowse: module
+{
+	init: fn(ctxt: ref Draw->Context, nil: list of string);
+};
+
+Widgetstack: adt {
+	stk: list of string;	# list of widget names; bottom of list is left-most widget
+	name: string;
+
+	# init returns the widget name for the widgetstack;
+	# wn is the name of the frame holding the widget stack
+	new: fn(wn: string): ref Widgetstack;
+
+	push: fn(ws: self ref Widgetstack, w: string);
+	pop: fn(ws: self ref Widgetstack): string;
+	top: fn(ws: self ref Widgetstack): string;
+};
+
+Defaultwidth: con 30;
+Defaultheight: con 1;
+
+Tablerows: con 3;
+Tablecols: con 8;
+
+Element: adt {
+	name: string;
+	cmd: chan of string;
+	cmdname: string;
+	config: array of string;
+	doneinit: int;
+};
+
+# columns in unidata file
+ud_VAL, ud_CHARNAME, ud_CATEG, ud_COMBINE, ud_BIDIRECT,
+ud_DECOMP, ud_DECDIGIT, ud_DIGIT, ud_NUMERICVAL, ud_MIRRORED,
+ud_OLDNAME, ud_COMMENT, ud_UPCASE, ud_LOWCASE, ud_TITLECASE: con iota;
+
+# default font configurations within the application
+DEFAULTFONT:	con "";
+UNICODEFONT:	con "lucm/unicode.9";
+TITLEFONT:	con "misc/latin1.8x13";
+DATAFONT:	con "misc/latin1.8x13";
+BUTTONFONT:	con "misc/latin1.8x13";
+
+currfont := "/fonts/" + UNICODEFONT + ".font";
+
+MAINMENU, BYSEARCH, BYNUMBER, BYCATEGORY, BYFONT, TABLE: con iota;
+elements := array[] of {
+MAINMENU => Element(".main", nil, "maincmd", array[] of {
+	"frame .main",
+	"$listbox data .main.menu -height 6h",
+	"$button button .main.insp -text {Inspector} -command {send maincmd inspect}",
+	"$button button .main.font -text {Font} -command {send maincmd font}",
+	"$label unicode .fontlabel",	# .fontlabel's font is currently chosen font
+	"pack .main.menu -side top",
+	"pack .main.insp .main.font -side left",
+	"bind .main.menu <ButtonRelease-1> +{send maincmd newselect}"
+	}, 0),
+BYNUMBER => Element(".numfield", nil, "numcmd", array[] of {
+	"frame .numfield",
+	"$entry data .numfield.f -width 8w",
+	"bind .numfield.f <Key-\n> {send numcmd shownum}",
+	"$label title .numfield.l -text 'Hex unicode value",
+	"pack .numfield.l .numfield.f -side left"
+	}, 0),
+TABLE => Element(".tbl", nil, "tblcmd", array[] of {
+	"frame .tbl",
+	"frame .tbl.tf",
+	"frame .tbl.buts",
+	"$button button .tbl.buts.forw -text {Next} -command {send tblcmd forw}",
+	"$button button .tbl.buts.backw -text {Prev} -command {send tblcmd backw}",
+	"pack .tbl.buts.forw .tbl.buts.backw -side left",
+	"pack .tbl.tf -side top",
+	"pack .tbl.buts -side left"
+	}, 0),
+BYCATEGORY => Element(".cat", nil, "catcmd", array[] of {
+	"frame .cat",
+	"$listbox data .cat.menu -width 43w -height 130 -yscrollcommand {.cat.yscroll set}",
+	"scrollbar .cat.yscroll -width 18 -command {.cat.menu yview}",
+	"pack .cat.yscroll .cat.menu -side left -fill y", 
+	"bind .cat.menu <ButtonRelease-1> +{send catcmd newselect}"
+	}, 0),
+BYSEARCH => Element(".srch", nil, "searchcmd", array[] of {
+	"frame .srch",
+	"$listbox data .srch.menu -width 43w -height 130 -yscrollcommand {.srch.yscroll set}",
+	"scrollbar .srch.yscroll -width 18 -command {.srch.menu yview}",
+	"pack .srch.yscroll .srch.menu -side left -fill y", 
+	"bind .srch.menu <ButtonRelease-1> +{send searchcmd search}"
+	}, 0),
+BYFONT => Element(".font", nil, "fontcmd", array[] of {
+	"frame .font",
+	"$listbox data .font.menu -width 43w -height 130 -yscrollcommand {.font.yscroll set}",
+	"scrollbar .font.yscroll -width 18 -command {.font.menu yview}",
+	"pack .font.yscroll .font.menu -side left -fill y", 
+	"bind .font.menu <ButtonRelease-1> +{send fontcmd newselect}"
+	}, 0),
+};
+
+entries := array[] of {
+("By Category", BYCATEGORY),
+("By number", BYNUMBER),
+("Symbol wordsearch", BYSEARCH),
+("Font information", BYFONT)
+};
+
+toplevelconfig := array[] of {
+"pack .Wm_t .display -side top -fill x",
+"image create bitmap waiting -file cursor.wait"
+};
+
+wmchan:		chan of string;	# from main window
+inspchan:	chan of string;	# to inspector
+
+ctxt:		ref Draw->Context;
+displ:	ref Widgetstack;
+top:		ref Tk->Toplevel;
+unidata:	ref bio->Iobuf;
+
+UNIDATA:	con "/lib/unidata/unidata2.txt";
+UNIINDEX:	con "/lib/unidata/index2.txt";
+UNIBLOCKS:	con "/lib/unidata/blocks.txt";
+
+notice(msg: string)
+{
+	dialog->prompt(ctxt, top.image, "bomb.bit", "Notice", msg, 0, "OK"::nil);
+}
+
+init(drawctxt: ref Draw->Context, nil: list of string)
+{
+	entrychan := chan of string;
+
+	ctxt = drawctxt;
+	config();
+	if ((unidata = bio->open(UNIDATA, bio->OREAD)) == nil) {
+		notice("Couldn't open unicode data file");
+		inspchan <-= "exit";
+		exit;
+	}
+
+	push(MAINMENU);
+	tkclient->onscreen(top, nil);
+	tkclient->startinput(top, "kbd"::"ptr"::nil);
+	currpos := 0;
+
+	for (;;) alt {
+	c := <-top.ctxt.kbd =>
+		tk->keyboard(top, c);
+	p := <-top.ctxt.ptr =>
+		tk->pointer(top, *p);
+	c := <-top.ctxt.ctl or
+	c = <-top.wreq or
+	c = <-wmchan =>
+		tkclient->wmctl(top, c);
+	c := <-elements[MAINMENU].cmd =>
+		case c {
+		"font" =>
+			font := choosefont(ctxt);
+			if (font != nil) {
+				currfont = font;
+				updatefont();
+				update(top);
+			}
+		"newselect" =>
+			sel := int cmd(top, ".main.menu curselection");
+			(nil, el) := entries[sel];
+			if (el == BYSEARCH) {
+				spawn sendentry(top, "Enter search string", entrychan);
+				break;
+			}
+			pop(MAINMENU);
+			push(el);
+			update(top);
+
+		"inspect" =>
+			inspchan <-= "raise";
+		}
+	c := <-entrychan =>
+		if (c != nil) {
+			pop(MAINMENU);
+			push(BYSEARCH);
+			update(top);
+			keywordsearch(c);
+		}
+
+	<-elements[BYNUMBER].cmd =>
+		txt := cmd(top, ".numfield.f get");
+		(n, nil) := str->toint(txt, 16);
+
+		pop(BYNUMBER);
+		push(TABLE);
+		setchar(0, n);
+		currpos = filltable(n);
+		update(top);
+
+	<-elements[BYCATEGORY].cmd =>
+		sel := cmd(top, ".cat.menu curselection");
+		(currpos, nil) = str->toint(cmd(top, ".cat.menu get "+sel), 16);
+		pop(BYCATEGORY);
+		push(TABLE);
+		currpos = filltable(currpos);
+		update(top);
+
+	c := <-elements[TABLE].cmd =>
+		case c {
+		"forw" =>	currpos = filltable(currpos + Tablerows * Tablecols);
+				update(top);
+
+		"backw" =>	currpos = filltable(currpos - Tablerows * Tablecols);
+				update(top);
+
+		* =>		# must be set <col> <row> <raise>
+				(nil, args) := sys->tokenize(c, " ");
+				setchar(int hd tl tl tl args, currpos + int hd tl args
+						+ int hd tl tl args * Tablecols);
+		}
+
+	<-elements[BYSEARCH].cmd =>
+		sel := cmd(top, ".srch.menu curselection");
+		(n, nil) := str->toint(cmd(top, ".srch.menu get "+sel), 16);
+
+		pop(BYSEARCH);
+		push(TABLE);
+		setchar(0, n);
+		currpos = filltable(n);
+		update(top);
+
+	<-elements[BYFONT].cmd =>
+		sel := cmd(top, ".font.menu curselection");
+		(currpos, nil) = str->toint(cmd(top, ".font.menu get "+sel), 16);
+		pop(BYFONT);
+		push(TABLE);
+		currpos = filltable(currpos);
+		update(top);
+	}
+	inspchan <-= "exit";
+}
+
+sendentry(t: ref Tk->Toplevel, msg: string, where: chan of string)
+{
+	where <-= dialog->getstring(ctxt, t.image, msg);
+	exit;
+}
+
+setchar(raisei: int, c: int)
+{
+	s := ""; s[0] = c;
+	if(raisei)
+		inspchan <-= "raise";
+	inspchan <-= s;
+}
+
+
+charconfig := array[] of {
+"frame .chdata -borderwidth 5 -relief ridge",
+"frame .chdata.f1",
+"frame .chdata.f2",
+"frame .chdata.chf -borderwidth 4 -relief raised",
+"frame .chdata.chcf -borderwidth 3 -relief ridge",
+"$label title .chdata.chf.title -text 'Glyph: ",
+"$label unicode .chdata.ch",
+"$label data .chdata.val -anchor e",
+"$label title .chdata.name -anchor w",
+"$label data .chdata.cat -anchor w",
+"$label data .chdata.comm -anchor w",
+"$button button .chdata.snarfbut -text {Snarf} -command {send charcmd snarf}",
+"$button button .chdata.pastebut -text {Paste} -command {send charcmd paste}",
+"pack .chdata.chf.title .chdata.chcf -in .chdata.chf -side left",
+"pack .chdata.ch -in .chdata.chcf",
+"pack .chdata.chf -in .chdata.f1 -side left -padx 1 -pady 1",
+"pack .chdata.val -in .chdata.f1 -side right",
+"pack .chdata.snarfbut .chdata.pastebut -in .chdata.f2 -side right",
+"pack .chdata.f1 .chdata.name .chdata.cat .chdata.comm .chdata.f2 -fill x -side top",
+"pack .Wm_t .chdata -side top -fill x",
+};
+
+inspector(ctxt: ref Draw->Context, cmdch: chan of string)
+{
+	chtop: ref Tk->Toplevel;
+
+	kbd := chan of int;
+	ptr := chan of ref Draw->Pointer;
+	wreq := chan of string;
+	iwmchan := chan of string;
+	ctl := chan of string; 
+
+	charcmd := chan of string;
+	currc := 'A';
+
+	for (;;) alt {
+	c := <-kbd =>
+		tk->keyboard(chtop, c);
+	p := <-ptr =>
+		tk->pointer(chtop, *p);
+	c := <-ctl or
+	c = <-wreq or
+	c = <-iwmchan =>
+		if (c != "exit" && chtop != nil)
+			tkclient->wmctl(chtop, c);
+		else
+			chtop = nil;
+	c := <-cmdch =>
+		case c {
+		"raise" =>
+			if (chtop != nil) {
+				cmd(chtop, "raise .");
+				break;
+			}
+			org := winorg(top);
+			org.y += int cmd(top, ". cget -actheight");
+			(chtop, iwmchan) = tkclient->toplevel(ctxt,
+					"-x "+string org.x+" -y "+string org.y,
+					"Character inspector", 0);
+			tk->namechan(chtop, charcmd, "charcmd");
+
+			runconfig(chtop, charconfig);
+			inspector_setchar(chtop, currc);
+			tkclient->onscreen(chtop, "onscreen");
+			tkclient->startinput(chtop, "ptr"::nil);
+			kbd = chtop.ctxt.kbd;
+			ptr = chtop.ctxt.ptr;
+			ctl = chtop.ctxt.ctl;
+			wreq = chtop.wreq;
+		"font" =>
+			if (chtop != nil) {
+				cmd(chtop, ".chdata.ch configure -font "+currfont);
+				update(chtop);
+			}
+		"exit" =>
+			exit;
+		* =>
+			if (len c == 1) {
+				currc = c[0];
+				inspector_setchar(chtop, currc);
+			} else {
+				sys->fprint(stderr, "unknown inspector cmd: '%s'\n", c);
+			}
+		}
+	c := <-charcmd =>
+		case c {
+		"snarf" =>
+			tkclient->snarfput(cmd(chtop, ".chdata.ch cget -text"));
+		"paste" =>
+			buf := tkclient->snarfget();
+			if (len buf > 0)
+				inspector_setchar(chtop, buf[0]);
+		}
+	}
+}
+
+inspector_setchar(t: ref Tk->Toplevel, c: int)
+{
+	if(t == nil)
+		return;
+	line := look(unidata, ';', sys->sprint("%4.4X", c));
+	labelset(t, ".chdata.ch", sys->sprint("%c", c));
+	labelset(t, ".chdata.val", sys->sprint("%4.4X", c));
+	if (line == nil) {
+		labelset(t, ".chdata.name", "No entry found in unicode table");
+		labelset(t, ".chdata.cat", "");
+		labelset(t, ".chdata.comm", "");
+	} else {
+		flds := fields(line, ';');
+		labelset(t, ".chdata.name", fieldindex(flds, ud_CHARNAME));
+		labelset(t, ".chdata.cat", categname(fieldindex(flds, ud_CATEG)));
+		labelset(t, ".chdata.comm", fieldindex(flds, ud_OLDNAME));
+	}
+	update(t);
+}
+
+keywordsearch(key: string): int
+{
+
+	data := bio->open(UNIINDEX, Sys->OREAD);
+
+	key = str->tolower(key);
+	
+	busy();
+	cmd(top, ".srch.menu delete 0 end");
+	count := 0;
+	while ((l := bio->data.gets('\n')) != nil) {
+		l = str->tolower(l);
+		if (str->prefix(key, l)) {
+			if (len l > 1 && l[len l - 2] == '\r')
+				l = l[0:len l - 2];
+			else
+				l = l[0:len l - 1];
+			flds := fields(l, '\t');
+			cmd(top, ".srch.menu insert end '"
+				+fieldindex(flds, 1)+": "+fieldindex(flds, 0));
+			update(top);
+			count++;
+		}
+	}
+	notbusy();
+	if (count == 0) {
+		notice("No match");
+		return 0;
+	}
+	return 1;
+}
+
+nomodule(s: string)
+{
+	sys->fprint(stderr, "couldn't load modules %s: %r\n", s);
+	raise "could not load modules";
+}
+
+config()
+{
+	sys = load Sys Sys->PATH;
+	if(ctxt == nil){
+		sys->fprint(stderr, "unibrowse: window manager required\n");
+		raise "no wm";
+	}
+	sys->pctl(Sys->NEWPGRP, nil);
+	stderr = sys->fildes(2);
+
+	draw = load Draw Draw->PATH;
+	if (draw == nil) nomodule(Draw->PATH);
+
+	tk = load Tk Tk->PATH;
+	if (tk == nil) nomodule(Tk->PATH);
+
+	tkclient = load Tkclient Tkclient->PATH;
+	if (tkclient == nil) nomodule(Tkclient->PATH);
+
+	dialog = load Dialog Dialog->PATH;
+	if (dialog == nil) nomodule(Dialog->PATH);
+
+	selectfile = load Selectfile Selectfile->PATH;
+	if (selectfile == nil) nomodule(Selectfile->PATH);
+
+	str = load String String->PATH;
+	if (str == nil) nomodule(String->PATH);
+
+	bio = load Bufio Bufio->PATH;
+	if (bio == nil) nomodule(Bufio->PATH);
+
+	tkclient->init();
+	dialog->init();
+	selectfile->init();
+
+	ctxt = ctxt;
+
+	(top, wmchan) = tkclient->toplevel(ctxt, nil, "Unicode browser", Tkclient->Hide);
+
+	displ = Widgetstack.new(".display");
+	cmd(top, "pack .display");
+
+	for (i := 0; i < len elements; i++) {
+		elements[i].cmd = tkchan(elements[i].cmdname);
+		runconfig(top, elements[i].config);
+	}
+
+	runconfig(top, toplevelconfig);
+
+	inspchan = chan of string;
+	spawn inspector(ctxt, inspchan);
+}
+
+runconfig(top: ref Tk->Toplevel, cmds: array of string)
+{
+	for (i := 0; i < len cmds; i++) {
+		ent := tkexpand(cmds[i]);
+		if (ent != nil) {
+			err := cmd(top, ent);
+			if (len err > 0 && err[0] == '!')
+				sys->fprint(stderr, "config err: %s on '%s'\n", err, ent);
+		}
+	}
+}
+
+update(top: ref Tk->Toplevel)
+{ cmd(top, "update"); }
+
+busy()
+{ cmd(top, "cursor -image waiting"); }
+
+notbusy()
+{ cmd(top, "cursor -default"); }
+
+initelement(el: int): int
+# returns non-zero on success
+{
+	if (!elements[el].doneinit) {
+		elements[el].doneinit = 1;
+		case el {
+		MAINMENU =>
+			for (e := entries; len e > 0; e = e[1:]) {
+				(text, nil) := e[0];
+				cmd(top, ".main.menu insert end '" + text);
+			}
+
+		BYCATEGORY =>
+			cats := getcategories();
+			if (cats == nil) {
+				notice("No categories found");
+				elements[el].doneinit = 0;
+				return 0;
+			}
+			while (cats != nil) {
+				cmd(top, ".cat.menu insert 0 '" + hd cats);
+				cats = tl cats;
+			}
+		BYFONT =>
+			elements[el].doneinit = 0;	# do it each time
+			fonts := getfonts(currfont);
+			if (fonts == nil) {
+				notice("Can't find font information file");
+				return 0;
+			}
+
+			cmd(top, ".font.menu delete 0 end");
+			while (fonts != nil) {
+				cmd(top, ".font.menu insert 0 '" + hd fonts);
+				fonts = tl fonts;
+			}
+		TABLE =>
+			inittable();
+		}
+
+	}
+	return 1;
+}
+
+tablecharpath(col, row: int): string
+{
+	return ".tbl.tf.c"+string row+"_"+string col;
+}
+
+inittable()
+{
+	i: int;
+	for (i = 0; i < Tablerows; i++) {
+		cmd(top, tkexpand("$label title .tbl.tf.num" + string i));
+		cmd(top, sys->sprint("grid .tbl.tf.num%d -row %d", i, i));
+
+		# >>> could put entry here
+		for (j := 0; j < Tablecols; j++) {
+			cname := ".tbl.tf.c" + string i +"_" +string j;
+			cmd(top, tkexpand("$label unicode "+cname
+					+" -borderwidth 1 -relief raised"));
+			cmd(top, "bind "+cname+" <ButtonRelease-1>"
+					+" {send tblcmd set "+string j+" "+string i+" 0}");
+			cmd(top, "bind "+cname+" <Double-Button-1>"
+					+" {send tblcmd set "+string j+" "+string i+" 1}");
+			cmd(top, "grid "+cname+" -row "+string i+" -column "+string (j+1) +
+						" -sticky ews");
+		}
+	}
+}
+
+# fill table starting at n.
+# return actual starting value.
+filltable(n: int): int
+{
+	if (n < 0)
+		n = 0;
+	if (n + Tablerows * Tablecols > 16rffff)
+		n = 16rffff - Tablerows * Tablecols;
+	n -= n % Tablecols;
+	for (i := 0; i < Tablerows; i++) {
+		cmd(top, ".tbl.tf.num" + string i +" configure -text '"
+				+ sys->sprint("%4.4X",n+i*Tablecols));
+		for (j := 0; j < Tablecols; j++) {
+			cname := tablecharpath(j, i);
+			cmd(top, cname + " configure -text '"
+					+sys->sprint("%c", n + i * Tablecols + j));
+		}
+	}
+	return n;
+}
+
+cnumtoint(s: string): int
+{
+	if (len s == 0)
+		return 0;
+	if (s[0] == '0' && len s > 1) {
+		n: int;
+		if (s[1] == 'x' || s[1] == 'X') {
+			if (len s < 3)
+				return 0;
+			(n, nil) = str->toint(s[2:], 16);
+		} else
+			(n, nil) = str->toint(s, 8);
+		return n;
+	}
+	return int s;
+}
+
+getfonts(font: string): list of string
+{
+	f := bio->open(font, bio->OREAD);
+	if (f == nil)
+		return nil;
+
+	# ignore header
+	if (bio->f.gets('\n') == nil)
+		return nil;
+
+	ret: list of string;
+	while ((s := bio->f.gets('\n')) != nil) {
+		(count, wds) := sys->tokenize(s, " \t");
+		if (count < 3 || count > 4)
+			continue;	# ignore malformed lines
+		first := cnumtoint(hd wds);
+		wds = tl wds;
+		last := cnumtoint(hd wds);
+		wds = tl wds;
+		if (tl wds != nil) 		# if optional third field exists
+			wds = tl wds;	# ignore it
+		name := hd wds;
+		if (name != "" && name[len name - 1] == '\n')
+				name = name[0:len name - 1];
+		ret = sys->sprint("%.4X-%.4X: %s", first, last, name) :: ret;
+	}
+	return ret;
+}
+
+getcategories(): list of string
+{
+	f := bio->open(UNIBLOCKS, bio->OREAD);
+	if (f == nil)
+		return nil;
+
+	ret: list of string;
+	while ((s := bio->f.gets('\n')) != nil) {
+		if (s[0] == '#')
+			continue;
+		(s, nil) = str->splitr(s, "^\n\r");
+		if (len s > 0) {
+			start, end: string;
+			(start, s) = str->splitl(s, ";");
+			s = str->drop(s, "; ");
+			(end, s) = str->splitl(s, ";");
+			s = str->drop(s, "; ");
+
+			ret = start+"-"+end+": "+s :: ret;
+		}
+	}
+	return ret;
+}
+
+
+tkexpand(s: string): string
+{
+	if (len s == 0 || s[0] != '$')
+		return s;
+
+	cmd, tp, name: string;
+	(cmd, s) = str->splitl(s, " \t");
+	cmd = cmd[1:];
+
+	s = str->drop(s, " \t");
+	(tp, s) = str->splitl(s, " \t");
+	s = str->drop(s, " \t");
+
+	(name, s) = str->splitl(s, " \t");
+	s = str->drop(s, " \t");
+
+	font := "";
+	case tp {
+		"deflt" =>	font = DEFAULTFONT;
+		"title" =>	font = TITLEFONT;
+		"data" =>	font = DATAFONT;
+		"button" =>	font = BUTTONFONT;
+		"unicode" =>	font = currfont;
+	}
+	if (font != nil) {
+		if (font[0] != '/')
+			font = "/fonts/"+font+".font";
+		font = "-font "+font;
+	}
+
+
+	ret := cmd+" "+name+" "+font+" "+s;
+	return ret;
+}
+
+categname(s: string): string
+{
+	r := "Unknown category";
+	case s {
+	"Mn" => r = "Mark, Non-Spacing ";
+	"Mc" => r = "Mark, Combining";
+	"Nd" => r = "Number, Decimal Digit";
+	"No" => r = "Number, Other";
+	"Zs" => r = "Separator, Space";
+	"Zl" => r = "Separator, Line";
+	"Zp" => r = "Separator, Paragraph";
+	"Cc" => r = "Other, Control or Format";
+	"Co" => r = "Other, Private Use";
+	"Cn" => r = "Other, Not Assigned";
+	"Lu" => r = "Letter, Uppercase";
+	"Ll" => r = "Letter, Lowercase";
+	"Lt" => r = "Letter, Titlecase ";
+	"Lm" => r = "Letter, Modifier";
+	"Lo" => r = "Letter, Other ";
+	"Pd" => r = "Punctuation, Dash";
+	"Ps" => r = "Punctuation, Open";
+	"Pe" => r = "Punctuation, Close";
+	"Po" => r = "Punctuation, Other";
+	"Sm" => r = "Symbol, Math";
+	"Sc" => r = "Symbol, Currency";
+	"So" => r = "Symbol, Other";
+	}
+	return r;
+}
+
+
+fields(s: string, sep: int): list of string
+# seperator can't be '^' (see string(2))
+{
+	cl := ""; cl[0] = sep;
+	ret: list of string;
+	do {
+		(l, r) := str->splitr(s, cl);
+		ret = r :: ret;
+		if (len l > 0)
+			s = l[0:len l - 1];
+		else
+			s = nil;
+	} while (s != nil);
+	return ret;
+}
+
+fieldindex(sl: list of string, n: int): string
+{
+	for (; sl != nil; sl = tl sl) {
+		if (n == 0)
+			return hd sl;
+		n--;
+	}
+	return nil;
+}
+
+push(el: int)
+{
+	if (initelement(el)) {
+		displ.push(elements[el].name);
+	}
+}
+
+pop(el: int)
+# pop elements until we encounter one matching el.
+{
+	while (displ.top() != elements[el].name)
+		displ.pop();
+}
+
+tkchan(nm: string): chan of string
+{
+	c := chan of string;
+	tk->namechan(top, c, nm);
+	return c;
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+	# sys->print("%s\n", s);
+	e := tk->cmd(top, s);
+	if (e != nil && e[0] == '!')
+		sys->fprint(sys->fildes(2), "tk error on '%s': %s\n", s, e);
+	return e;
+}
+
+labelset(t: ref Tk->Toplevel, name: string, val: string)
+{
+	cmd(t, name+" configure -text '"+val);
+}
+
+
+choosefont(ctxt: ref Draw->Context): string
+{
+	font := selectfile->filename(ctxt, top.image, "Select a font", "*.font" :: nil, "/fonts");
+	if (font != nil) { 
+		ret := cmd(top, ".fontlabel configure"+" -font "+font);
+		if (len ret > 0 && ret[0] == '!') {
+			font = nil;
+			notice("Bad font: "+ret[1:]);
+		}
+	}
+	return font;
+}
+
+updatefont()
+{
+	if (elements[TABLE].doneinit)	# only if table is being displayed
+		for (i := 0; i < Tablerows; i++)
+			for (j := 0; j < Tablecols; j++)
+				cmd(top, tablecharpath(j, i) + " configure -font "+currfont);
+	# update the font display table if it's being displayed
+	for (el := displ.stk; el != nil; el = tl el) {
+		if (hd el == elements[BYFONT].name) {
+			initelement(BYFONT);
+		}
+	}
+	inspchan <-= "font";
+}
+
+
+winorg(t: ref Tk->Toplevel): Draw->Point
+{
+	return Draw->Point(int cmd(t, ". cget -x"), int cmd(t, ". cget -y"));
+}
+	
+Widgetstack.new(wn: string): ref Widgetstack
+{
+	cmd(top, "frame "+wn+" -borderwidth 4 -relief ridge");
+
+	return ref Widgetstack(nil, wn);
+}
+
+Widgetstack.push(ws: self ref Widgetstack, w: string)
+{
+	if (w == nil)
+		return;
+	opts: con " -fill y -side left";
+
+	if (ws.stk == nil) {
+		cmd(top, "pack "+w+" -in "+ws.name+" "+opts);
+	} else {
+		cmd(top, "pack "+w+" -after "+hd ws.stk+" "+opts);
+	}
+
+	ws.stk = w :: ws.stk;
+}
+
+Widgetstack.pop(ws: self ref Widgetstack): string
+{
+	if (ws.stk == nil) {
+		sys->fprint(stderr, "widget stack underflow!\n");
+		exit;
+	}
+	old := hd ws.stk;
+	ws.stk = tl ws.stk;
+	cmd(top, "pack forget "+old);
+	return old;
+}
+
+Widgetstack.top(ws: self ref Widgetstack): string
+{
+	if (ws.stk == nil)
+		return nil;
+	return hd ws.stk;
+}
+
+# binary search for key in f.
+# code converted from bsd source without permission.
+look(f: ref bio->Iobuf, sep: int, key: string): string
+{
+	bot := mid := big 0;
+	ktop := bio->f.seek(big 0, Sys->SEEKEND);
+	key = canon(key, sep);
+
+	for (;;) {
+		mid = (ktop + bot) / big 2;
+		bio->f.seek(mid, Sys->SEEKSTART);
+		c: int;
+		do {
+			c = bio->f.getb();
+			mid++;
+		} while (c != bio->EOF && c != bio->ERROR && c != '\n');
+		(entry, eof) := getword(f);
+		if (entry == nil && eof)
+			break;
+		entry = canon(entry, sep);
+		case comparewords(key, entry) {
+		-2 or -1 or 0 =>
+			if (ktop <= mid)
+				break;
+			ktop = mid;
+			continue;
+		1 or 2 =>
+			bot = mid;
+			continue;
+		}
+		break;
+	}
+	bio->f.seek(bot, Sys->SEEKSTART);
+	while (bio->f.seek(big 0, Sys->SEEKRELA) < ktop) {
+		(entry, eof) := getword(f);
+		if (entry == nil && eof)
+			return nil;
+		word := canon(entry, sep);
+		case comparewords(key, word) {
+		-2 =>
+			return nil;
+		-1 or 0 =>
+			return entry;
+		1 or 2 =>
+			continue;
+		}
+		break;
+	}
+	for (;;) {
+		(entry, eof) := getword(f);
+		if (entry == nil && eof)
+			return nil;
+		word := canon(entry, sep);
+		case comparewords(key, word) {
+		-1 or 0 =>
+			return entry;
+		}
+		break;
+	}
+	return nil;
+}
+
+comparewords(s, t: string): int
+{
+	if (s == t)
+		return 0;
+	i := 0;
+	for (; i < len s && i < len t && s[i] == t[i]; i++)
+		;
+	if (i >= len s)
+		return -1;
+	if (i >= len t)
+		return 1;
+	if (s[i] < t[i])
+		return -2;
+	return 2;
+}
+
+getword(f: ref bio->Iobuf): (string, int)
+{
+	ret := "";
+	for (;;) {
+		c := bio->f.getc();
+		if (c == bio->EOF || c == bio->ERROR)
+			return (ret, 0);
+		if (c == '\n')
+			break;
+		ret[len ret] = c;
+	}
+	return (ret, 1);
+}
+
+canon(s: string, sep: int): string
+{
+	if (sep < 0)
+		return s;
+	i := 0;
+	for (; i < len s; i++)
+		if (s[i] == sep)
+			break;
+	return s[0:i];
+}
--- /dev/null
+++ b/appl/wm/view.b
@@ -1,0 +1,484 @@
+implement View;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context, Rect, Point, Display, Screen, Image: import draw;
+
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+
+include "imagefile.m";
+	imageremap: Imageremap;
+	readgif: RImagefile;
+	readjpg: RImagefile;
+	readxbitmap: RImagefile;
+	readpng: RImagefile;
+
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+include	"arg.m";
+
+include	"plumbmsg.m";
+	plumbmsg: Plumbmsg;
+	Msg: import plumbmsg;
+
+stderr: ref Sys->FD;
+display: ref Display;
+x := 25;
+y := 25;
+img_patterns: list of string;
+plumbed := 0;
+background: ref Image;
+
+View: module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	spawn realinit(ctxt, argv);
+}
+
+realinit(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "view: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+	selectfile->init();
+
+	stderr = sys->fildes(2);
+	display = ctxt.display;
+	background = display.color(16r222222ff);
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		badload(Arg->PATH);
+
+	img_patterns = list of {
+		"*.bit (Compressed image files)",
+		"*.gif (GIF image files)",
+		"*.jpg (JPEG image files)",
+		"*.jpeg (JPEG image files)",
+		"*.png (PNG image files)",
+		"*.xbm (X Bitmap image files)"
+		};
+
+	imageremap = load Imageremap Imageremap->PATH;
+	if(imageremap == nil)
+		badload(Imageremap->PATH);
+
+	bufio = load Bufio Bufio->PATH;
+	if(bufio == nil)
+		badload(Bufio->PATH);
+
+
+	arg->init(argv);
+	errdiff := 1;
+	while((c := arg->opt()) != 0)
+		case c {
+		'f' =>
+			errdiff = 0;
+		'i' =>
+			if(!plumbed){
+				plumbmsg = load Plumbmsg Plumbmsg->PATH;
+				if(plumbmsg != nil && plumbmsg->init(1, "view", 1000) >= 0)
+					plumbed = 1;
+			}
+		}
+	argv = arg->argv();
+	arg = nil;
+	if(argv == nil && !plumbed){
+		f := selectfile->filename(ctxt, nil, "View file name", img_patterns, nil);
+		if(f == "") {
+			#spawn view(nil, nil, "");
+			return;
+		}
+		argv = f :: nil;
+	}
+
+
+	for(;;){
+		file: string;
+		if(argv != nil){
+			file = hd argv;
+			argv = tl argv;
+			if(file == "-f"){
+				errdiff = 0;
+				continue;
+			}
+		}else if(plumbed){
+			file = plumbfile();
+			if(file == nil)
+				break;
+			errdiff = 1;	# set this from attributes?
+		}else
+			break;
+
+		(ims, masks, err) := readimages(file, errdiff);
+
+		if(ims == nil)
+			sys->fprint(stderr, "view: can't read %s: %s\n", file, err);
+		else
+			spawn view(ctxt, ims, masks, file);
+	}
+}
+
+badload(s: string)
+{
+	sys->fprint(stderr, "view: can't load %s: %r\n", s);
+	raise "fail:load";
+}
+
+readimages(file: string, errdiff: int) : (array of ref Image, array of ref Image, string)
+{
+	im := display.open(file);
+
+	if(im != nil)
+		return (array[1] of {im}, array[1] of ref Image, nil);
+
+	fd := bufio->open(file, Sys->OREAD);
+	if(fd == nil)
+		return (nil, nil, sys->sprint("%r"));
+
+	(mod, err1) := filetype(file, fd);
+	if(mod == nil)
+		return (nil, nil, err1);
+
+	(ai, err2) := mod->readmulti(fd);
+	if(ai == nil)
+		return (nil, nil, err2);
+	if(err2 != "")
+		sys->fprint(stderr, "view: %s: %s\n", file, err2);
+	ims := array[len ai] of ref Image;
+	masks := array[len ai] of ref Image;
+	for(i := 0; i < len ai; i++){
+		masks[i] = transparency(ai[i], file);
+
+		# if transparency is enabled, errdiff==1 is probably a mistake,
+		# but there's no easy solution.
+		(ims[i], err2) = imageremap->remap(ai[i], display, errdiff);
+		if(ims[i] == nil)
+			return(nil, nil, err2);
+	}
+	return (ims, masks, nil);
+}
+
+viewcfg := array[] of {
+	"panel .p",
+	"menu .m",
+	".m add command -label Open -command {send cmd open}",
+	".m add command -label Grab -command {send cmd grab} -state disabled",
+	".m add command -label Save -command {send cmd save}",
+	"pack .p -side bottom -fill both -expand 1",
+	"bind .p <Button-3> {send cmd but3 %X %Y}",
+	"bind .p <Motion-Button-3> {}",
+	"bind .p <ButtonRelease-3> {}",
+	"bind .p <Button-1> {send but1 %X %Y}",
+};
+
+DT: con 250;
+
+timer(dt: int, ticks, pidc: chan of int)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		sys->sleep(dt);
+		ticks <-= 1;
+	}
+}
+
+view(ctxt: ref Context, ims, masks: array of ref Image, file: string)
+{
+	file = lastcomponent(file);
+	(t, titlechan) := tkclient->toplevel(ctxt, "", "view: "+file, Tkclient->Hide);
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	but1 := chan of string;
+	tk->namechan(t, but1, "but1");
+
+	for (c:=0; c<len viewcfg; c++)
+		tk->cmd(t, viewcfg[c]);
+	tk->cmd(t, "update");
+
+	image := display.newimage(ims[0].r, ims[0].chans, 0, Draw->White);
+	if (image == nil) {
+		sys->fprint(stderr, "view: can't create image: %r\n");
+		return;
+	}
+	imconfig(t, image);
+	image.draw(image.r, ims[0], masks[0], ims[0].r.min);
+	tk->putimage(t, ".p", image, nil);
+	tk->cmd(t, "update");
+
+	pid := -1;
+	ticks := chan of int;
+	if(len ims > 1){
+		pidc := chan of int;
+		spawn timer(DT, ticks, pidc);
+		pid = <-pidc;
+	}
+	imno := 0;
+	grabbing := 0;
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+
+
+	for(;;) alt{
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq or
+	s = <-titlechan =>
+		tkclient->wmctl(t, s);
+
+	<-ticks =>
+		if(masks[imno] != nil)
+			paneldraw(t, image, image.r, background, nil, image.r.min);
+		++imno;
+		if(imno >= len ims)
+			imno = 0;
+		paneldraw(t, image, ims[imno].r, ims[imno], masks[imno], ims[imno].r.min);
+		tk->cmd(t, "update");
+
+	s := <-cmd =>
+		(nil, l) := sys->tokenize(s, " ");
+		case (hd l) {
+		"open" =>
+			spawn open(ctxt, t);
+		"grab" =>
+			tk->cmd(t, "cursor -bitmap cursor.drag; grab set .p");
+			grabbing = 1;
+		"save" =>
+			patterns := list of {
+				"*.bit (Inferno image files)",
+				"*.gif (GIF image files)",
+				"*.jpg (JPEG image files)",
+				"* (All files)"
+			};
+			f := selectfile->filename(ctxt, t.image, "Save file name",
+				patterns, nil);
+			if(f != "") {
+				fd := sys->create(f, Sys->OWRITE, 8r664);
+				if(fd != nil) 
+					display.writeimage(fd, ims[0]);
+			}
+		"but3" =>
+			if(!grabbing) {
+				xx := int hd tl l - 50;
+				yy := int hd tl tl l - int tk->cmd(t, ".m yposition 0") - 10;
+				tk->cmd(t, ".m activate 0; .m post "+string xx+" "+string yy+
+					"; grab set .m; update");
+			}
+		}
+	s := <- but1 =>
+			if(grabbing) {
+				(nil, l) := sys->tokenize(s, " ");
+				xx := int hd l;
+				yy := int hd tl l;
+#				grabtop := tk->intop(ctxt.screen, xx, yy);
+#				if(grabtop != nil) {
+#					cim := grabtop.image;
+#					imr := Rect((0,0), (cim.r.dx(), cim.r.dy()));
+#					image = display.newimage(imr, cim.chans, 0, draw->White);
+#					if(image == nil){
+#						sys->fprint(stderr, "view: can't allocate image\n");
+#						exit;
+#					}
+#					image.draw(imr, cim, nil, cim.r.min);
+#					tk->cmd(t, ".Wm_t.title configure -text {View: grabbed}");
+#					imconfig(t, image);
+#					tk->putimage(t, ".p", image, nil);
+#					tk->cmd(t, "update");
+#					# Would be nicer if this could be spun off cleanly
+#					ims = array[1] of {image};
+#					masks = array[1] of ref Image;
+#					imno = 0;
+#					grabtop = nil;
+#					cim = nil;
+#				}
+				tk->cmd(t, "cursor -default; grab release .p");
+				grabbing = 0;
+			}
+	}
+}
+
+open(ctxt: ref Context, t: ref tk->Toplevel)
+{
+	f := selectfile->filename(ctxt, t.image, "View file name", img_patterns, nil);
+	t = nil;
+	if(f != "") {
+		(ims, masks, err) := readimages(f, 1);
+		if(ims == nil)
+			sys->fprint(stderr, "view: can't read %s: %s\n", f, err);
+		else
+			view(ctxt, ims, masks, f);
+	}
+}
+
+lastcomponent(path: string) : string
+{
+	for(k:=len path-2; k>=0; k--)
+		if(path[k] == '/'){
+			path = path[k+1:];
+			break;
+		}
+	return path;
+}
+
+imconfig(t: ref Toplevel, im: ref Draw->Image)
+{
+	width := im.r.dx();
+	height := im.r.dy();
+	tk->cmd(t, ".p configure -width " + string width
+		+ " -height " + string height + "; update");
+}
+
+plumbfile(): string
+{
+	if(!plumbed)
+		return nil;
+	for(;;){
+		msg := Msg.recv();
+		if(msg == nil){
+			sys->print("view: can't read /chan/plumb.view: %r\n");
+			return nil;
+		}
+		if(msg.kind != "text"){
+			sys->print("view: can't interpret '%s' kind of message\n", msg.kind);
+			continue;
+		}
+		file := string msg.data;
+		if(len file>0 && file[0]!='/' && len msg.dir>0){
+			if(msg.dir[len msg.dir-1] == '/')
+				file = msg.dir+file;
+			else
+				file = msg.dir+"/"+file;
+		}
+		return file;
+	}
+}
+
+Tab: adt
+{
+	suf:	string;
+	path:	string;
+	mod:	RImagefile;
+};
+
+GIF, JPG, PIC, PNG, XBM: con iota;
+
+tab := array[] of
+{
+	GIF => Tab(".gif",	RImagefile->READGIFPATH,	nil),
+	JPG => Tab(".jpg",	RImagefile->READJPGPATH,	nil),
+	PIC => Tab(".pic",	RImagefile->READPICPATH,	nil),
+	XBM => Tab(".xbm",	RImagefile->READXBMPATH,	nil),
+	PNG => Tab(".png",	RImagefile->READPNGPATH,	nil),
+};
+
+filetype(file: string, fd: ref Iobuf): (RImagefile, string)
+{
+	for(i:=0; i<len tab; i++){
+		n := len tab[i].suf;
+		if(len file>n && file[len file-n:]==tab[i].suf)
+			return loadmod(i);
+	}
+
+	# sniff the header looking for a magic number
+	buf := array[20] of byte;
+	if(fd.read(buf, len buf) != len buf)
+		return (nil, sys->sprint("%r"));
+	fd.seek(big 0, 0);
+	if(string buf[0:6]=="GIF87a" || string buf[0:6]=="GIF89a")
+		return loadmod(GIF);
+	if(string buf[0:5] == "TYPE=")
+		return loadmod(PIC);
+	jpmagic := array[] of {byte 16rFF, byte 16rD8, byte 16rFF, byte 16rE0,
+		byte 0, byte 0, byte 'J', byte 'F', byte 'I', byte 'F', byte 0};
+	if(eqbytes(buf, jpmagic))
+		return loadmod(JPG);
+	pngmagic := array[] of {byte 137, byte 80, byte 78, byte 71, byte 13, byte 10, byte 26, byte 10};
+	if(eqbytes(buf, pngmagic))
+		return loadmod(PNG);
+	if(string buf[0:7] == "#define")
+		return loadmod(XBM);
+	return (nil, "can't recognize file type");
+}
+
+eqbytes(buf, magic: array of byte): int
+{
+	for(i:=0; i<len magic; i++)
+		if(magic[i]>byte 0 && buf[i]!=magic[i])
+			return 0;
+	return i == len magic;
+}
+
+loadmod(i: int): (RImagefile, string)
+{
+	if(tab[i].mod == nil){
+		tab[i].mod = load RImagefile tab[i].path;
+		if(tab[i].mod == nil)
+			sys->fprint(stderr, "view: can't find %s reader: %r\n", tab[i].suf);
+		else
+			tab[i].mod->init(bufio);
+	}
+	return (tab[i].mod, nil);
+}
+
+transparency(r: ref RImagefile->Rawimage, file: string): ref Image
+{
+	if(r.transp == 0)
+		return nil;
+	if(r.nchans != 1){
+		sys->fprint(stderr, "view: can't do transparency for multi-channel image %s\n", file);
+		return nil;
+	}
+	i := display.newimage(r.r, display.image.chans, 0, 0);
+	if(i == nil){
+		sys->fprint(stderr, "view: can't allocate mask for %s: %r\n", file);
+		exit;
+	}
+	pic := r.chans[0];
+	npic := len pic;
+	mpic := array[npic] of byte;
+	index := r.trindex;
+	for(j:=0; j<npic; j++)
+		if(pic[j] == index)
+			mpic[j] = byte 0;
+		else
+			mpic[j] = byte 16rFF;
+	i.writepixels(i.r, mpic);
+	return i;
+}
+
+paneldraw(t: ref Tk->Toplevel, dst: ref Image, r: Rect, src, mask: ref Image, p: Point)
+{
+	dst.draw(r, src, mask, p);
+	s := sys->sprint(".p dirty %d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+	tk->cmd(t, s);
+}
--- /dev/null
+++ b/appl/wm/vt.b
@@ -1,0 +1,1007 @@
+implement WmVt;
+
+# note: this code was hacked together in a hurry from some decade-old C code
+# of mine, so don't expect it to be pretty...
+# Also, don't expect it to be finished... I had to rush to check this
+# in... it's just been worked on as a side-project from time to time
+# But it's good enough to be useful most of the time
+ 
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+	draw: Draw;
+	Display, Font, Black, Rect, Image, Point, Endsquare, Enddisc: import draw;
+include "tk.m";
+	tk: Tk;
+	Toplevel: import tk;
+include "tkclient.m";
+	tkclient: Tkclient;
+include "sh.m";
+
+CON_Maxnpts:	con 1000;
+Maxnhits:	con 5;
+
+ 
+WmVt: module {
+	init:   fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+
+
+VT_MAXPARAM: con 8;
+
+
+Vt: adt {
+	y1, y2: int;
+	mode: int;	# misc mode parameters 
+	qmode: int;	# extended mode parameters
+	attr: int; 	# display attributes 
+	fg: int;	# foreground color 
+	bg: int;	# background color 
+
+	# saved values:
+	save_x, save_y: int;
+	save_attr: int;
+	save_fg, save_bg: int;
+	save_mode: int;
+	save_qmode: int;
+
+	# escape code parsing:
+	esc: int;	# escape mode 
+	pcount: int;	# parameter count
+	etype: int;	# escape code type
+	ptype: int;	# current parameter type
+	value: int;	# current value
+	param: array of int;
+
+	# display info:
+	wid, hgt: int;
+	x, y: int;
+	dx, dy: int;
+	nlcr: int;
+	ccc: int;
+	scr: array of string;
+	cc: array of string;
+};
+
+
+display: ref Display;
+t: ref Toplevel;
+canvas: ref Image;
+canvrect: Rect;
+org: Point;
+font: ref Font;
+stderr: ref Sys->FD;
+vt: ref Vt;
+pad: string;
+vtc := array[16] of ref Image;
+raw := 0;
+echo := 1;
+reverse := 0;
+sq := "";
+
+inpchan: chan of string;
+
+
+shwin_cfg := array[] of {
+	"frame .f",
+	"pack .c .f -side top -fill x",
+	"pack propagate . 0",
+	"focus .f",
+	"bind .f <Key> {send keys {%A}}",
+	"bind . <Configure> {send cmd resize}",
+	"update"
+};
+
+
+titlebar()
+{
+	tk->cmd(t, "destroy .Wm_t.S");
+	tk->cmd(t, "button .Wm_t.S -bg #aaaaaa -fg white -text {" +
+		sprint("%d x %d", vt.wid, vt.hgt) + "}; " +
+		"pack .Wm_t.S -side right");
+	c := "green";
+	if(raw)
+		c = "red";
+	tk->cmd(t, "destroy .Wm_t.k");
+	tk->cmd(t, "button .Wm_t.k -bitmap keyboard.bit"+
+		" -background "+c+" -command {send wm_title raw}; " +
+		"pack .Wm_t.k -side right");
+	c = "red";
+	if(echo)
+		c = "green";
+	tk->cmd(t, "destroy .Wm_t.d");
+	tk->cmd(t, "button .Wm_t.d -bitmap display.bit"+
+		" -background "+c+" -command {send wm_title echo}; " +
+				"pack .Wm_t.d -side right");
+	c = "white";
+	if(reverse)
+		c = "black";
+	tk->cmd(t, "destroy .Wm_t.r");
+	tk->cmd(t, "button .Wm_t.r -width 24 -height 24 "+
+		" -background "+c+" -command {send wm_title reverse}; " +
+				"pack .Wm_t.r -side right");
+	tk->cmd(t, "update");
+}
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "vt: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+
+	stderr = sys->fildes(2);
+
+	sys->pctl(Sys->FORKNS, nil);
+	sys->pctl(Sys->NEWPGRP, nil);
+
+	menubut: chan of string;
+	tkclient->init();
+	(t, menubut) = tkclient->toplevel(ctxt, "", "WmVt", Tkclient->Appl);
+
+	display = ctxt.display;	
+	font = Font.open(display, "*default*");
+
+	vt = ref Vt;
+	vt.hgt = 24;
+	vt.wid = 80;
+	vt.scr = array[vt.hgt] of string;
+	vt.cc = array[vt.hgt] of string;
+	vt_init(vt);
+
+	pad = "";
+	for(i:=0; i<vt.wid; i++) 
+		pad[i] = ' ';
+
+	cmd := chan of string;
+	tk->namechan(t, cmd, "cmd");
+	tk->cmd(t, "canvas .c -height "
+		+ string (vt.hgt*font.height) +
+		+ " -width " + string (vt.wid*font.width("0")) +
+		" -background red");
+	tkcmds(t, shwin_cfg);
+	tkclient->onscreen(t, nil);
+	tkclient->startinput(t, "kbd"::"ptr"::nil);
+	titlebar();
+
+	keys := chan of string;
+	tk->namechan(t, keys, "keys");
+ 
+	canvas = t.image;
+	canvrect = canvposn(t);
+	org = canvrect.min;
+	
+	npts := 0;
+	WasUp := 1;
+
+	for(i=0; i<16; i++) {
+		r := 0;
+		g := 0;
+		b := 0;
+		v := 192;
+		if(i&8)
+			v = 255;
+		if(i&1)
+			r = v;
+		if(i&2)
+			g = v;
+		if(i&4)
+			b = v;
+		vtc[i] = display.newimage(((0,0),(1,1)), t.image.chans,
+				1, display.rgb2cmap(r, g, b));
+		if (vtc[i] == nil) {
+			sys->fprint(sys->fildes(2), "Failed to allocate image\n");
+			exit;
+		}
+	}
+
+	vt_write(vt, "\u001b[2J");
+
+	ioc := chan of (int, ref Sys->FileIO, ref Sys->FileIO);
+	spawn newsh(ctxt, ioc);
+	
+	(pid, file, filectl) := <- ioc;
+	if((file == nil) || (filectl == nil)) {
+		sys->print("newsh: %r\n");
+		return;
+	}
+
+	# XXX - need to kill this later
+	ic := chan of string;
+	spawn consinp(ic, file.read);
+
+	inpchan = ic;	# hack
+
+	for(;;) alt {
+	s := <-t.ctxt.kbd =>
+		tk->keyboard(t, s);
+	s := <-t.ctxt.ptr =>
+		tk->pointer(t, *s);
+	s := <-t.ctxt.ctl or
+	s = <-t.wreq =>
+		tkclient->wmctl(t, s);
+	menu := <- menubut =>
+		if(menu == "exit") {
+			kill(pid);
+			return;
+		}
+		else if(menu == "raw") {
+			raw = !raw;
+			titlebar();
+			redraw();
+		}
+		else if(menu == "echo") {
+			echo = !echo;
+			titlebar();
+			redraw();
+		}
+		else if(menu == "reverse") {
+			reverse = !reverse;
+			tmp := vtc[0];
+			vtc[0] = vtc[7];
+			vtc[7] = tmp;
+			titlebar();
+			redraw();
+		} else
+			tkclient->wmctl(t, menu);
+		tk->cmd(t, "focus .f");
+
+	s := <- cmd =>
+		(n, cmdstr) := sys->tokenize(s, " \t\n");
+		case hd cmdstr {
+		"quit" =>
+			exit;
+		"resize" =>
+			# sys->print("resize\n");
+			canvas = t.image;
+			canvrect = canvposn(t);
+			org = canvrect.min;
+			# sys->print("%d,%d %d,%d\n", canvrect.max.x, canvrect.min.x,
+			#	canvas.r.max.x, canvas.r.min.x);
+			resize((canvrect.max.x-canvrect.min.x)/font.width("0"),
+				(canvrect.max.y-canvrect.min.y)/font.height);
+			titlebar();
+			redraw();
+		}
+
+	c := <- keys =>
+		ic <-= c[1:2];
+		if(echo)
+			scwrite(c[1:2]);
+
+	(off, data, fid, wc) := <- file.write =>
+		if(wc == nil)
+			return;
+		if(echo && !raw && sq != "") {
+			s := "";
+			for(i=0; i<len sq; i++)
+				s += "\b \b";
+			scwrite(s);
+		}
+		scwrite(string data);
+		if(echo && !raw && sq != "")
+			scwrite(sq);
+		wc <-= (len data, nil);
+	(off, data, fid, wc) := <- filectl.write =>
+		if(string data == "rawon") {
+			raw = 1;
+			echo = 0;
+			titlebar();
+			redraw();
+		}
+		if(string data == "rawoff") {
+			raw = 0;
+			echo = 1;
+			titlebar();
+			redraw();
+		}
+		wc <-= (len data, nil);
+	}
+}
+
+resize(wid,hgt: int)
+{
+	scr := array[hgt] of string;
+	cc := array[hgt] of string;
+	for(y :=0; y<hgt; y++) {
+		oy := y + hgt - vt.hgt;
+		if(oy < vt.hgt && oy >= 0) {
+			scr[y] = vt.scr[oy];
+			cc[y] = vt.cc[oy];
+		} else {
+			scr[y] = "";
+			cc[y] = "";
+		}
+	}
+	vt.x += wid - vt.wid;
+	vt.y += hgt - vt.hgt;
+	if(vt.x < 0)
+		vt.x = 0;
+	if(vt.x >= wid)
+		vt.x = wid;
+	if(vt.y < 0)
+		vt.y = 0;
+	if(vt.y >= hgt)
+		vt.y = hgt;
+	vt.wid = wid;
+	vt.hgt = hgt;
+	vt.scr = scr;
+	vt.cc = cc;
+}
+
+
+fixdx := 0;
+fixdy := 0;
+
+canvposn(t: ref Toplevel): Rect
+{
+	r: Rect;
+
+	r.min.x = int tk->cmd(t, ".c cget -actx") + int tk->cmd(t, ".dx get");
+	r.min.y = int tk->cmd(t, ".c cget -acty") + int tk->cmd(t, ".dy get");
+	r.max.x = r.min.x + int tk->cmd(t, ".c cget -width") + int tk->cmd(t, ".dw get");
+	r.max.y = r.min.y + int tk->cmd(t, ".c cget -height") + int tk->cmd(t, ".dh get");
+
+	# correction for Tk bug (width/height not correct):
+	dx := (t.image.r.max.x - t.image.r.min.x) - (r.max.x - r.min.x);
+	dy := (t.image.r.max.y - t.image.r.min.y) - (r.max.y - r.min.y);
+	if(fixdx == 0) {
+		fixdx = dx;
+		fixdy = dy;
+	} else {
+		r.max.x += dx-fixdx;
+		r.max.y += dy-fixdy;
+	}
+	return r;
+}
+
+
+redraw()
+{
+	# sys->print("redraw\n");
+	for(y:=0; y<vt.hgt; y++) {
+		xp := canvrect.min.x;
+		yp := canvrect.max.y-(vt.hgt-y)*font.height;
+		f := 0;
+		for(x:=0; x<=len vt.cc[y]; x++) {
+			if(x == len vt.cc[y] || (vt.cc[y][x]>>4) != (vt.cc[y][f]>>4)) {
+				if(x == len vt.cc[y])
+					w := canvrect.max.x-xp;
+				else
+					w = font.width(vt.scr[y][f:x]);
+				if(len vt.cc[y] == 0)
+					ccc := 7;
+				else
+					ccc = vt.cc[y][f];
+				canvas.draw(((xp,yp),(xp+w,yp+font.height)),
+					vtc[ccc>>4], nil, (0, 0));
+				xp += w;
+				f = x;
+			}
+		}
+		xp = canvrect.min.x;
+		f = 0;
+		for(x=1; x<=len vt.scr[y]; x++) {
+			if(x == len vt.scr[y] || (vt.cc[y][x]&15) != (vt.cc[y][f]&15)) {
+				canvas.text((xp,yp), vtc[vt.cc[y][f]&15],
+					(0, 0), font, vt.scr[y][f:x]);
+				xp += font.width(vt.scr[y][f:x]);
+				f = x;
+			}
+		}
+	}
+}
+
+
+
+scwrite(s: string)
+{
+	putchar(vt.x, vt.y, vtscr(vt.y, vt.x), vtcc(vt.y, vt.x));
+	vt_write(vt, s);
+	putchar(vt.x, vt.y, vtscr(vt.y, vt.x), vtcc(vt.y, vt.x) ^ 16rff);
+}
+
+putchar(x,y: int, ch: int, ccc: int)
+{
+	if(len vt.scr[y] < x) {
+		vt.scr[y] += pad[0:x-len vt.scr[y]];
+		vt.cc[y] += pad[0:x-len vt.cc[y]];
+	}
+	xp := canvrect.min.x+font.width(vt.scr[y][0:x]);
+	yp := canvrect.max.y-(vt.hgt-y)*font.height;
+	s: string;
+	s[0] = ch;
+	canvas.draw(((xp,yp),(xp+font.width(s),yp+font.height)),
+				vtc[ccc>>4], nil, (0, 0));
+	canvas.text((xp,yp), vtc[ccc&15], (0, 0), font, s);
+}
+
+VT_PUTCHAR(vt: ref Vt, x,y: int, ch: int)
+{
+	if(len vt.scr[y] < x) {
+		vt.scr[y] += pad[0:x-len vt.scr[y]];
+		vt.cc[y] += pad[0:x-len vt.cc[y]];
+	}
+	vt.scr[y][x] = ch;
+	vt.cc[y][x] = vt.ccc;
+	putchar(x, y, ch, int vt.ccc);
+}
+
+VT_SCROLL_UP(vt: ref Vt, x1,y1,x2,y2,n: int)
+{
+	# XXX - needs to handle vertical slices
+	for(i:=y1; i<=y2-n; i++) {
+		vt.scr[i] = vt.scr[i+n];
+		vt.cc[i] = vt.cc[i+n];
+	}
+	r: Rect;
+	r.min.x = canvrect.min.x;
+	r.max.x = r.min.x+(x2-x1+1)*font.width(" ");
+	r.min.y = canvrect.max.y-(vt.hgt-y1)*font.height;
+	r.max.y = r.min.y+(y2-y1-n+1)*font.height;
+	canvas.draw(r, canvas, nil, (r.min.x, r.min.y+font.height*n));
+	VT_CLEAR(vt, x1,y2-n+1,x2,y2);
+}
+
+VT_SCROLL_DOWN(vt: ref Vt, x1,y1,x2,y2,n: int)
+{
+	# XXX - needs to handle vertical slices
+	for(i:=y2; i>=y1+n; i--) {
+		vt.scr[i] = vt.scr[i-n];
+		vt.cc[i] = vt.cc[i-n];
+	}
+	VT_CLEAR(vt, x1,y1,x2,y1+n-1);
+	redraw();
+}
+
+VT_SCROLL_LEFT(vt: ref Vt, x1,y1,x2,y2,n: int)
+{
+	# XXX - shouldn't always scroll whole line
+	for(y:=y1; y<=y2; y++) {
+		if(len vt.scr[y] > n) {
+			vt.scr[y] = vt.scr[y][n:];
+			vt.cc[y] = vt.cc[y][n:];
+		} else {
+			vt.scr[y] = "";
+			vt.cc[y] = "";
+		}
+	}
+	redraw();
+}
+
+VT_SCROLL_RIGHT(vt: ref Vt, x1,y1,x2,y2,n: int)
+{
+	# XXX - shouldn't always scroll whole line
+	for(y:=y1; y<=y2; y++) {
+		vt.scr[y] = pad[0:n] + vt.scr[y];
+		vt.cc[y] = pad[0:n] + vt.cc[y];
+	}
+	redraw();
+}
+
+VT_CLEAR(vt: ref Vt, x1,y1,x2,y2: int)
+{
+	# XXX - needs to handle vertical slices
+	for(y:=y1; y<=y2; y++) {
+		vt.scr[y] = "";
+		vt.cc[y] = "";
+	}
+	r: Rect;
+	r.min.x = canvrect.min.x;
+	r.max.x = r.min.x + (x2-x1+1)*font.width(" ");
+	r.min.y = canvrect.max.y-(vt.hgt-y1)*font.height;
+	r.max.y = r.min.y + (y2-y1+1)*font.height;
+	canvas.draw(r, vtc[vt.ccc>>4], nil, (0, 0));
+}
+
+VT_SET_COLOR(vt: ref Vt)
+{
+	if(vt.attr & (1<<7))
+		vt.ccc = ((vt.fg<<4) | vt.bg);
+	else
+		vt.ccc = ((vt.bg<<4) | vt.fg);
+	if(vt.attr & (1<<1))
+		vt.ccc ^= (1<<3);
+}
+
+vtscr(y,x: int): int
+{
+	if(vt.scr[y] == nil)
+		return ' ';
+	if(x >= len vt.scr[y])
+		return ' ';
+	return vt.scr[y][x];
+}
+
+vtcc(y,x: int): int
+{
+	if(vt.cc[y] == nil)
+		return 7;
+	if(x >= len vt.cc[y])
+		return 7;
+	return vt.cc[y][x];
+}
+
+VT_SET_CURSOR(nil: ref Vt, x,y: int)
+{
+}
+
+VT_BEEP(nil: ref Vt)
+{
+	redraw();
+}
+
+# function for simulated typing (for returning status)
+VT_TYPE(vt: ref Vt, b: string)
+{
+	inpchan <-= b;
+}
+
+
+#############################################################################
+
+
+vt_save_state(vt: ref Vt)
+{
+	vt.save_x = vt.x;
+	vt.save_y = vt.y;
+	vt.save_attr = vt.attr;
+	vt.save_fg = vt.fg;
+	vt.save_bg = vt.bg;
+	vt.save_mode = vt.mode;
+	vt.save_qmode = vt.qmode;
+}
+
+vt_restore_state(vt: ref Vt)
+{
+	vt.x = vt.save_x;
+	vt.y = vt.save_y;
+	vt.attr = vt.save_attr;
+	vt.fg = vt.save_fg;
+	vt.bg = vt.save_bg;
+	vt.mode = vt.save_mode;
+	vt.qmode = vt.save_qmode;
+	VT_SET_COLOR(vt);
+}
+
+
+
+# expects vt.wid, vt.hgt and implementation
+# variables to be initialized first: 
+
+vt_init(vt: ref Vt)
+{
+	vt.fg = 7;
+	vt.bg = 0;
+	vt.attr = 0;
+	vt.mode = 0;
+	vt.qmode = (1<<7);
+	vt.y1 = 0;
+	vt.y2 = vt.hgt-1;
+	vt.x = 0;
+	vt.y = 0;
+	vt.dx = 1;
+	vt.dy = 1;
+	vt.esc = 0;
+	vt.pcount = 0;
+	vt.param = array[VT_MAXPARAM] of int;
+	vt_save_state(vt);
+	VT_SET_COLOR(vt);
+}
+
+
+vt_checkscroll(vt: ref Vt, s: string)
+{
+	i := 0;
+	n: int;
+	if (vt.y == vt.y2+1 || vt.y >= vt.hgt) {
+		n = 1;
+		while(i < len s && n < (vt.y2-vt.y1)) {
+			c := s[i++];
+			if(c == 27 || c > 126 || c < 0)
+				break;
+			if(c == '\n')
+				n++;
+		}
+              	vt.y = vt.y2-n+1;
+		VT_SCROLL_UP(vt,0,vt.y1,vt.wid-1,vt.y2,n);
+       	} else if (vt.y == vt.y1-1) {
+		vt.y = vt.y1;
+		VT_SCROLL_DOWN(vt,0,vt.y1,vt.wid-1,vt.y2,1);
+	} else if (vt.y < 0)
+		vt.y = 0;
+}
+
+vt_write(vt: ref Vt, s: string)
+{
+	ch: int;
+	check_scroll: int;
+	n: int;
+	i := 0;
+
+        while(i < len s) {
+	    check_scroll = 0;
+            ch = s[i++];
+	    case vt.esc {
+	    1 =>
+		if(ch == '[') {
+			vt.etype = ch;
+			vt.esc++;
+			vt.value = 0;
+			vt.pcount = 0;
+			vt.ptype = 1;
+			for(n=0; n<VT_MAXPARAM; n++)
+				vt.param[n] = 0;
+		} else {
+			check_scroll = vt_call_ncsi(vt, ch);
+			vt.esc = 0;
+		}	
+	    2 =>
+		if(ch >= '0' && ch <= '9') 
+			vt.value=(vt.value)*10+(ch-'0');
+		else if(ch == '?')
+			vt.ptype = -1;
+		else {
+			vt.param[vt.pcount++] = vt.value*vt.ptype;
+			if(ch == ';') {
+				if(vt.pcount >= VT_MAXPARAM)
+					vt.pcount = VT_MAXPARAM-1;
+				vt.value = 0;
+			} else {
+				check_scroll = vt_call_csi(vt, ch);
+				vt.esc = 0;
+			}
+		}
+	    * =>
+		case ch {
+                '\n' =>
+                        vt.y += vt.dy;
+			check_scroll = 1;
+			if(vt.nlcr)
+                        	vt.x = 0;
+                '\r' =>
+                        vt.x = 0;
+                '\b' =>
+                        if (vt.x > 0)
+                                vt.x -= vt.dx;
+                '\t' =>
+			n = (vt.x & ~7)+8;
+			if(vt.mode & (1<<4))
+				VT_SCROLL_RIGHT(vt, vt.x,vt.y,
+				  vt.wid-1,vt.y, n - vt.x);
+                        vt.x = n;
+			if(vt.x > vt.wid) {
+				vt.x = 0; 
+				vt.y++;
+				check_scroll = 1;
+			}
+		7 =>
+			VT_BEEP(vt);
+		11 =>
+			vt.x = 0;
+			vt.y = vt.y1;
+		12 =>
+			VT_CLEAR(vt,0,vt.y1,vt.wid-1,vt.y2);
+		27 =>
+			vt.esc++;
+		133 =>
+			vt.x = 0;
+			vt.y++;
+			check_scroll = 1;
+		132 =>
+			vt.y++;
+			check_scroll = 1;
+		136 =>	# XXX - set a tabstop 
+			;
+		141 =>
+			vt.y--;
+			check_scroll = 1;
+		142 =>	# XXX -- map G2 into GL for next char only
+			;
+		143 =>	# XXX -- map G3 into GL for next char 
+			;
+		144 =>	# XXX -- device control string 
+			;
+		145 =>	# XXX -- start of string - ignored 
+			;
+		146 =>	# XXX -- device attribute request 
+			;
+		147 =>
+			vt.esc = 2;
+			vt.etype = '[';
+			vt.esc++;
+			vt.value = 0;
+			vt.pcount = 0;
+			vt.ptype = 1;
+			for(n=0; n<VT_MAXPARAM; n++)
+				vt.param[n] = 0;
+                * =>
+			if(vt.mode & (1<<4))
+				VT_SCROLL_RIGHT(vt,vt.x,vt.y,
+				  vt.wid-1,vt.y,1);	
+			if(ch>=32 || ch <=126) {
+				if(vt.qmode & (1<<15)) {
+					if(vt.x >= vt.wid-1 && (vt.qmode & (1<<7))) {
+						vt.x = 0;
+						vt.y += vt.dy;
+						vt_checkscroll(vt, s[i:]);
+					}
+					vt.qmode &= ~(1<<15);
+				}
+				VT_PUTCHAR(vt,vt.x,vt.y,ch);
+                       		if((vt.x += vt.dx) >= vt.wid) {
+					vt.x = vt.wid-1; 
+					vt.qmode |= (1<<15);
+                        	}
+			}
+                }
+	    }
+	    if(check_scroll)
+		vt_checkscroll(vt, s[i:]); 
+	    if(vt.x < 0)
+		vt.x = 0;
+	    else if(vt.x >= vt.wid)
+		vt.x = vt.wid-1;
+	    if(vt.y < 0)
+		vt.y = 0;
+	    else if(vt.y >= vt.hgt)
+		vt.y = vt.hgt-1;
+	}
+	VT_SET_CURSOR(vt, vt.x, vt.y);
+}
+
+
+
+
+vt_call_csi(vt: ref Vt, ch: int): int
+{
+	i, n: int;
+	case ch {
+	'A' =>
+		vt.y -= vt_param(vt, 1,1,1,vt.hgt);
+	'B' =>
+		vt.y += vt_param(vt, 1,1,1,vt.hgt);
+	'C' =>
+		vt.x += vt_param(vt, 1,1,1,vt.wid);
+	'D' =>
+		vt.x -= vt_param(vt, 1,1,1,vt.wid);
+	'f' or 'H' =>
+		vt.y = vt_param(vt, 0,1,1,vt.hgt)-1;
+		vt.x = vt_param(vt, 1,1,1,vt.wid)-1;
+	'J' =>
+		case vt.param[0] {
+		0 => VT_CLEAR(vt,vt.x,vt.y,vt.wid-1,vt.y);
+			VT_CLEAR(vt,0,vt.y+1,vt.wid-1,vt.y2); 
+		1 => VT_CLEAR(vt,0,0,vt.wid-1,vt.y-1); 
+			VT_CLEAR(vt,0,vt.y,vt.x,vt.y); 
+		2 => VT_CLEAR(vt,0,vt.y1,vt.wid-1,vt.y2);	
+		}
+	'K' =>
+		case vt.param[0] {
+		0 => VT_CLEAR(vt,vt.x,vt.y,vt.wid-1,vt.y);
+		1 => VT_CLEAR(vt,0,vt.y,vt.x,vt.y);
+		2 => VT_CLEAR(vt,0,vt.y,vt.wid-1,vt.y); 
+		}
+	'L' =>
+		n = vt_param(vt, 0,1,1,vt.hgt);
+		VT_SCROLL_DOWN(vt,0,vt.y,vt.wid-1,vt.y2,n);	
+	'M' =>
+		n = vt_param(vt,0,1,1,vt.hgt);
+		VT_SCROLL_UP(vt,0,vt.y,vt.wid-1,vt.y2,n);	
+	'@' =>
+		n = vt_param(vt,0,1,1,vt.wid-1-vt.x);
+		VT_SCROLL_RIGHT(vt,vt.x,vt.y,vt.wid-1,vt.y,n);	
+	'P' =>
+		n = vt_param(vt,0,1,1,vt.wid-1-vt.x);
+		VT_SCROLL_LEFT(vt,vt.x,vt.y,vt.wid-1,vt.y,n);	
+	'X' =>
+		n = vt_param(vt,0,1,1,vt.wid-1-vt.x);
+		VT_CLEAR(vt,vt.x,vt.y,vt.x+n-1,vt.y);
+	'm' =>
+		if(vt.pcount == 0)
+			vt.pcount++;
+		for(i=0; i<vt.pcount; i++) {
+			n = vt.param[i];
+			if(!n) {
+				vt.attr = 0; 
+				vt.fg = 7;
+				vt.bg = 0;
+			} else if (n < 16)
+				vt.attr |= (1<<n);
+			else if (n < 28)
+				vt.attr &= ~(1<<(n-20));
+			else if (n < 38)
+				vt.fg = n-30;
+			else if (n < 48)
+				vt.bg = n-40;
+			else if (n < 58)
+				vt.fg = n-50+8;
+			else if (n < 68)
+				vt.bg = n-60+8;
+		}
+		VT_SET_COLOR(vt);
+	'c' =>
+		if(vt.wid >= 132)
+			VT_TYPE(vt, "\u001b[?61;1;6c");
+		else
+			VT_TYPE(vt, "\u001b[?61;6c");
+	'n' => 
+		n = vt_param(vt, 0,0,0,9);
+		if(n == 5)
+			VT_TYPE(vt, "\u001b[0n");
+		if(n == 5 || n == 6) 
+			VT_TYPE(vt, sprint("\u001b[%d;%dR",vt.y+1,vt.x+1));
+	'r' =>
+		vt.y1 = vt_param(vt, 0,1,1,vt.hgt)-1;
+		vt.y2 = vt_param(vt, 1,vt.hgt,1,vt.hgt)-1;
+	's' =>
+		vt_save_state(vt);
+	'u' =>
+		vt_restore_state(vt);
+	'h' =>
+		for(i=0; i<vt.pcount; i++) {
+			n = vt.param[i];
+			if(n >= 0)
+				vt.mode |= (1<<n);
+			else
+				vt.qmode |= (1<<(-n));
+		}
+	'l' =>
+		for(i=0; i<vt.pcount; i++) {
+			n = vt.param[i];
+			if(n >= 0)
+				vt.mode &= ~(1<<n);
+			else
+				vt.qmode &= ~(1<<(-n));
+		}
+	}
+
+	if(vt.y < 0)
+		vt.y = 0;
+	if(vt.y >= vt.hgt)
+		vt.y = vt.hgt-1;
+	if(vt.x < 0)
+		vt.x = 0;
+	if(vt.x >= vt.wid)
+		vt.x = vt.wid-1;
+	return 0;
+}
+
+vt_call_ncsi(vt: ref Vt, ch: int): int
+{
+	case ch {
+	'E' =>
+		vt.x = 0;
+	'9' =>
+		;
+	'D' =>
+		vt.y++;
+		return 1;
+	'H' =>	# XXX -- horizontal tab set
+		;
+	'6' =>
+		;
+	'M' =>
+		vt.y--;
+		return 1;
+	'7' =>
+		vt_save_state(vt);
+	'8' =>
+		vt_restore_state(vt);
+	'=' =>
+		;
+	'>' =>
+		;
+	'#' =>
+		;
+	'(' =>
+		;
+	')' =>
+		;
+	}
+	return 0;
+}
+
+
+vt_param(vt: ref Vt, n: int, def: int, min, max: int): int
+{
+	param := vt.param[n];
+	if(param == 0)
+		param = def;
+	if(param < min)
+		param = min;
+	if(param > max)
+		param = max;
+	return param;
+}
+
+#############################################################################
+
+
+consinp(cs: chan of string, cr: chan of (int, int, int, Sys->Rread))
+{
+	for(;;) {
+		alt {
+		sq += <- cs => ;
+
+		(nil, nbytes, nil, rc) := <- cr =>
+			p := 0;
+			for(;;) {
+				if(raw)
+					p = len sq;
+				else
+					forloop:
+					for(i := 0; i < len sq; i++) {
+						case sq[i] {
+						'\b' =>
+							if(i > 0) {
+								sq = sq[0:i-1] + sq[i+1:];
+								--i;
+							}
+						'\n' =>
+							p = i+1;
+							break forloop;
+						}
+					}
+				if(p > 0)
+					break;
+				sq += <- cs;
+			}
+			if(nbytes > p)
+				nbytes = p;
+			alt {
+			rc <-= (array of byte sq[0:nbytes], "") =>
+				sq = sq[nbytes:];
+			* => ;
+			}
+		}
+	}
+}
+
+newsh(ctxt: ref Draw->Context, ioc: chan of (int, ref Sys->FileIO, ref Sys->FileIO))
+{
+	pid := sys->pctl(sys->NEWFD, nil);
+
+	sh := load Command "/dis/sh.dis";
+	if(sh == nil) {
+		ioc <-= (0, nil, nil);
+		return;
+	}
+
+	tty := "cons."+string pid;
+
+	sys->bind("#s","/chan",sys->MBEFORE);
+	fio := sys->file2chan("/chan", tty);
+	fioctl := sys->file2chan("/chan", tty + "ctl");
+	ioc <-= (pid, fio, fioctl);
+	if ((fio == nil) || (fioctl == nil))
+		return;
+
+	sys->bind("/chan/"+tty, "/dev/cons", sys->MREPL);
+	sys->bind("/chan/"+tty+"ctl", "/dev/consctl", sys->MREPL);
+
+	fd0 := sys->open("/dev/cons", sys->OREAD|sys->ORCLOSE);
+	fd1 := sys->open("/dev/cons", sys->OWRITE);
+	fd2 := sys->open("/dev/cons", sys->OWRITE);
+
+	sh->init(ctxt, "sh" :: "-n" :: nil);
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "killgrp");
+}
+
+tkcmds(t: ref Tk->Toplevel, cfg: array of string)
+{
+	for(i := 0; i < len cfg; i++)
+		tk->cmd(t, cfg[i]);
+}
--- /dev/null
+++ b/appl/wm/wish.b
@@ -1,0 +1,165 @@
+implement wish;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+
+include "tk.m";
+	tk: Tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "bufio.m";
+	bufmod : Bufio;
+Iobuf :	import bufmod;
+
+include "../lib/tcl.m";
+	tcl : Tcl_Core;
+
+wish : module
+{
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+
+
+menubut : chan of string;
+keyboard,mypid : int;
+
+Wwsh : ref Tk->Toplevel;
+
+init(ctxt: ref Draw->Context, argv: list of string) {
+	sys  = load Sys  Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "wish: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk   = load Tk   Tk->PATH;
+	tkclient= load Tkclient Tkclient->PATH;
+	bufmod = load Bufio Bufio->PATH;
+	if (tk==nil || tkclient==nil || bufmod==nil){
+		sys->print("Load Error: %r\n");
+		exit;
+	}
+	tcl=load Tcl_Core Tcl_Core->PATH;
+	if (tcl==nil){
+		sys->print("Cannot load Tcl (%r)\n");
+		exit;
+	}
+	keyboard=1;
+	argv = tl argv;
+	if (argv!=nil)
+		file:=parse_args(argv);
+	geom:="";
+	mypid=sys->pctl(sys->NEWPGRP, nil);
+	tkclient->init();
+	Wshinit(ctxt, geom);
+	tcl->init(ctxt,argv);
+	tcl->set_top(Wwsh);
+	shellit(file);
+}
+
+
+
+
+
+parse_args(argv : list of string) : string {
+	while (argv!=nil){
+		case (hd argv){
+			"-k" =>
+				keyboard=0;
+			"-f" =>
+				argv = tl argv;
+				return hd argv;
+			* =>
+				return nil;
+		}
+		argv = tl argv;
+	}
+	return nil;
+}
+
+shellit(file:string){
+	drag:=chan of string;
+	tk->namechan(Wwsh, drag, "Wm_drag");
+	lines:=chan of string;
+	Tcl_Chan:=chan of string;
+	tk->namechan(Wwsh, lines, "lines");
+	tk->namechan(Wwsh, Tcl_Chan, "Tcl_Chan");
+	new_inp:="wish%";
+	unfin:="wish>";
+	line : string;
+	loadfile(file);
+	quiet:=0;
+	if (keyboard)
+		spawn tcl->grab_lines(new_inp,unfin,lines);
+	for(;;){
+		alt{
+			s := <-drag =>
+				if(len s < 6 || s[0:5] != "path=")
+					break;
+				loadfile(s[5:]);
+				sys->print("%s ",new_inp);
+			line = <-lines =>
+				line = tcl->prepass(line);
+				msg:= tcl->evalcmd(line,0);
+				if (msg!=nil)
+					sys->print("%s\n",msg);
+				sys->print("%s ", new_inp);
+				tcl->clear_error();
+			rline := <-Tcl_Chan  =>
+				rline = tcl->prepass(rline);
+				msg:= tcl->evalcmd(rline,0);
+				if (msg!=nil)
+					sys->print("%s\n",msg);
+				tcl->clear_error();
+			menu := <-menubut =>
+				if(menu == "exit"){
+					kfd := sys->open("#p/"+string mypid+"/ctl", sys->OWRITE);
+					if(kfd == nil) 
+						sys->print("error opening pid %d (%r)\n",mypid);
+						sys->fprint(kfd, "killgrp");
+						exit;				
+				}
+				tkclient->wmctl(Wwsh, menu);
+		}
+	}
+}
+
+
+
+loadfile(file :string) {
+	iob : ref Iobuf;
+	line,input : string;
+	line = "";
+	if (file==nil)
+		return;	
+	iob = bufmod->open(file,bufmod->OREAD);
+	if (iob==nil){
+		sys->print("File %s cannot be opened for reading",file);
+		return;
+	}
+	while((input=iob.gets('\n'))!=nil){
+		line+=input;
+		if (tcl->finished(line,0)){
+			line = tcl->prepass(line);
+			msg:= tcl->evalcmd(line,0);
+			if (msg!=nil)
+				sys->print("%s\n",msg);
+			tcl->clear_error();
+			line=nil;
+		}
+	}
+}
+
+Wshinit(ctxt: ref Draw->Context, geom: string) {
+	(Wwsh, menubut) = tkclient->toplevel(ctxt, geom,
+		"WishPad",Tkclient->Appl);
+	cmd := chan of string;
+	tk->namechan(Wwsh, cmd, "wsh");
+	tk->cmd(Wwsh, "update");
+}
--- /dev/null
+++ b/appl/wm/wm.b
@@ -1,0 +1,691 @@
+implement Wm;
+include "sys.m";
+	sys: Sys;
+include "draw.m";
+	draw: Draw;
+	Screen, Display, Image, Rect, Point, Wmcontext, Pointer: import draw;
+include "wmsrv.m";
+	wmsrv: Wmsrv;
+	Window, Client: import wmsrv;
+include "tk.m";
+include "wmclient.m";
+	wmclient: Wmclient;
+include "string.m";
+	str: String;
+include "sh.m";
+include "winplace.m";
+	winplace: Winplace;
+
+Wm: module {
+	init:	fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Ptrstarted, Kbdstarted, Controlstarted, Controller, Fixedorigin: con 1<<iota;
+Bdwidth: con 3;
+Sminx, Sminy, Smaxx, Smaxy: con iota;
+Minx, Miny, Maxx, Maxy: con 1<<iota;
+Background: con int 16r777777FF;
+
+screen: ref Screen;
+display: ref Display;
+ptrfocus: ref Client;
+kbdfocus: ref Client;
+controller: ref Client;
+allowcontrol := 1;
+fakekbd: chan of string;
+fakekbdin: chan of string;
+buttons := 0;
+
+badmodule(p: string)
+{
+	sys->fprint(sys->fildes(2), "wm: cannot load %s: %r\n", p);
+	raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+	sys  = load Sys Sys->PATH;
+	draw = load Draw Draw->PATH;
+	if(draw == nil)
+		badmodule(Draw->PATH);
+
+	str = load String String->PATH;
+	if(str == nil)
+		badmodule(String->PATH);
+
+	wmsrv = load Wmsrv Wmsrv->PATH;
+	if(wmsrv == nil)
+		badmodule(Wmsrv->PATH);
+
+	wmclient = load Wmclient Wmclient->PATH;
+	if(wmclient == nil)
+		badmodule(Wmclient->PATH);
+	wmclient->init();
+
+	winplace = load Winplace Winplace->PATH;
+	if(winplace == nil)
+		badmodule(Winplace->PATH);
+	winplace->init();
+
+	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+	if (ctxt == nil)
+		ctxt = wmclient->makedrawcontext();
+	display = ctxt.display;
+
+	buts := Wmclient->Appl;
+	if(ctxt.wm == nil)
+		buts = Wmclient->Plain;
+	win := wmclient->window(ctxt, "Wm", buts);
+	wmclient->win.reshape(((0, 0), (100, 100)));
+	wmclient->win.onscreen("place");
+	if(win.image == nil){
+		sys->fprint(sys->fildes(2), "wm: cannot get image to draw on\n");
+		raise "fail:no image";
+	}
+	wmclient->win.startinput("kbd" :: "ptr" :: nil);
+
+	wmctxt := win.ctxt;
+	screen = makescreen(win.image);
+
+	(clientwm, join, req) := wmsrv->init();
+	clientctxt := ref Draw->Context(ctxt.display, nil, clientwm);
+
+	wmrectIO := sys->file2chan("/chan", "wmrect");
+	if(wmrectIO == nil)
+		fatal(sys->sprint("cannot make /chan/wmrect: %r"));
+
+	sync := chan of string;
+	argv = tl argv;
+	if(argv == nil)
+		argv = "wm/toolbar" :: nil;
+	spawn command(clientctxt, argv, sync);
+	if((e := <-sync) != nil)
+		fatal("cannot run command: " + e);
+
+	fakekbd = chan of string;
+	for(;;) alt {
+	c := <-win.ctl or
+	c = <-wmctxt.ctl =>
+		# XXX could implement "pleaseexit" in order that
+		# applications can raise a warning message before
+		# they're unceremoniously dumped.
+		if(c == "exit")
+			for(z := wmsrv->top(); z != nil; z = z.znext)
+				z.ctl <-= "exit";
+
+		wmclient->win.wmctl(c);
+		if(win.image != screen.image)
+			reshaped(win);
+	c := <-wmctxt.kbd or
+	c = int <-fakekbd =>
+		if(kbdfocus != nil)
+			kbdfocus.kbd <-= c;
+	p := <-wmctxt.ptr =>
+		if(wmclient->win.pointer(*p))
+			break;
+		if(p.buttons && (ptrfocus == nil || buttons == 0)){
+			c := wmsrv->find(p.xy);
+			if(c != nil){
+				ptrfocus = c;
+				c.ctl <-= "raise";
+				setfocus(win, c);
+			}
+		}
+		if(ptrfocus != nil && (ptrfocus.flags & Ptrstarted) != 0){
+			# inside currently selected client or it had button down last time (might have come up)
+			buttons = p.buttons;
+			ptrfocus.ptr <-= p;
+			break;
+		}
+		buttons = 0;
+	(c, rc) := <-join =>
+		rc <-= nil;
+		# new client; inform it of the available screen rectangle.
+		# XXX do we need to do this now we've got wmrect?
+		c.ctl <-= "rect " + r2s(screen.image.r);
+		if(allowcontrol){
+			controller = c;
+			c.flags |= Controller;
+			allowcontrol = 0;
+		}else
+			controlevent("newclient " + string c.id);
+		c.cursor = "cursor";
+	(c, data, rc) := <-req =>
+		# if client leaving
+		if(rc == nil){
+			c.remove();
+			if(c.stop == nil)
+				break;
+			if(c == ptrfocus)
+				ptrfocus = nil;
+			if(c == kbdfocus)
+				kbdfocus = nil;
+			if(c == controller)
+				controller = nil;
+			controlevent("delclient " + string c.id);
+			for(z := wmsrv->top(); z != nil; z = z.znext)
+				if(z.flags & Kbdstarted)
+					break;
+			setfocus(win, z);
+			c.stop <-= 1;
+			break;
+		}
+		err := handlerequest(win, wmctxt, c, string data);
+		n := len data;
+		if(err != nil)
+			n = -1;
+		alt{
+		rc <-= (n, err) =>;
+		* =>;
+		}
+	(nil, nil, nil, wc) := <-wmrectIO.write =>
+		if(wc == nil)
+			break;
+		alt{
+		wc <-= (0, "cannot write") =>;
+		* =>;
+		}
+	(off, nil, nil, rc) := <-wmrectIO.read =>
+		if(rc == nil)
+			break;
+		d := array of byte r2s(screen.image.r);
+		if(off > len d)
+			off = len d;
+		alt{
+		rc <-= (d[off:], nil) =>;
+		* =>;
+		}
+	}
+}
+
+handlerequest(win: ref Wmclient->Window, wmctxt: ref Wmcontext, c: ref Client, req: string): string
+{
+#sys->print("%d: %s\n", c.id, req);
+	args := str->unquoted(req);
+	if(args == nil)
+		return "no request";
+	n := len args;
+	if(req[0] == '!' && n < 3)
+		return "bad arg count";
+	case hd args {
+	"key" =>
+		# XXX should we restrict this capability to certain clients only?
+		if(n != 2)
+			return "bad arg count";
+		if(fakekbdin == nil){
+			fakekbdin = chan of string;
+			spawn bufferproc(fakekbdin, fakekbd);
+		}
+		fakekbdin <-= hd tl args;
+	"ptr" =>
+		# ptr x y
+		if(n != 3)
+			return "bad arg count";
+		if(ptrfocus != c)
+			return "cannot move pointer";
+		e := wmclient->win.wmctl(req);
+		if(e == nil){
+			c.ptr <-= nil;		# flush queue
+			c.ptr <-= ref Pointer(buttons, (int hd tl args, int hd tl tl args), sys->millisec());
+		}
+	"cursor" =>
+		# cursor hotx hoty dx dy data
+		if(n != 6 && n != 1)
+			return "bad arg count";
+		c.cursor = req;
+		if(ptrfocus == c || kbdfocus == c)
+			return wmclient->win.wmctl(c.cursor);
+	"start" =>
+		if(n != 2)
+			return "bad arg count";
+		case hd tl args {
+		"mouse" or
+		"ptr" =>
+			c.flags |= Ptrstarted;
+		"kbd" =>
+			c.flags |= Kbdstarted;
+			# XXX this means that any new window grabs the focus from the current
+			# application, but usually you want this to happen... how can we distinguish
+			# the two cases?
+			setfocus(win, c);
+		"control" =>
+			if((c.flags & Controller) == 0)
+				return "control not available";
+			c.flags |= Controlstarted;
+		* =>
+			return "unknown input source";
+		}
+	"!reshape" =>
+		# reshape tag reqid rect [how]
+		# XXX allow "how" to specify that the origin of the window is never
+		# changed - a new window will be created instead.
+		if(n < 7)
+			return "bad arg count";
+		args = tl args;
+		tag := hd args; args = tl args;
+		args = tl args;		# skip reqid
+		r: Rect;
+		r.min.x = int hd args; args = tl args;
+		r.min.y = int hd args; args = tl args;
+		r.max.x = int hd args; args = tl args;
+		r.max.y = int hd args; args = tl args;
+		if(args != nil){
+			case hd args{
+			"onscreen" =>
+				r = fitrect(r, screen.image.r);
+			"place" =>
+				r = fitrect(r, screen.image.r);
+				r = newrect(r, screen.image.r);
+			"exact" =>
+				;
+			"max" =>
+				r = screen.image.r;			# XXX don't obscure toolbar?
+			* =>
+				return "unkown placement method";
+			}
+		}
+		return reshape(c, tag, r);
+	"delete" =>
+		# delete tag
+		if(tl args == nil)
+			return "tag required";
+		c.setimage(hd tl args, nil);
+		if(c.wins == nil && c == kbdfocus)
+			setfocus(win, nil);
+	"raise" =>
+		c.top();
+	"lower" =>
+		c.bottom();
+	"!move" or
+	"!size" =>
+		# !move tag reqid startx starty
+		# !size tag reqid mindx mindy
+		ismove := hd args == "!move";
+		if(n < 3)
+			return "bad arg count";
+		args = tl args;
+		tag := hd args; args = tl args;
+		args = tl args;			# skip reqid
+		w := c.window(tag);
+		if(w == nil)
+			return "no such tag";
+		if(ismove){
+			if(n != 5)
+				return "bad arg count";
+			return dragwin(wmctxt.ptr, c, w, Point(int hd args, int hd tl args).sub(w.r.min));
+		}else{
+			if(n != 5)
+				return "bad arg count";
+			sizewin(wmctxt.ptr, c, w, Point(int hd args, int hd tl args));
+		}
+	"fixedorigin" =>
+		c.flags |= Fixedorigin;
+	"rect" =>
+		;
+	"kbdfocus" =>
+		if(n != 2)
+			return "bad arg count";
+		if(int hd tl args)
+			setfocus(win, c);
+		else if(c == kbdfocus)
+			setfocus(win, nil);
+	# controller specific messages:
+	"request" =>		# can be used to test for control.
+		if((c.flags & Controller) == 0)
+			return "you are not in control";
+	"ctl" =>
+		# ctl id msg
+		if((c.flags & Controlstarted) == 0)
+			return "invalid request";
+		if(n < 3)
+			return "bad arg count";
+		id := int hd tl args;
+		for(z := wmsrv->top(); z != nil; z = z.znext)
+			if(z.id == id)
+				break;
+		if(z == nil)
+			return "no such client";
+		z.ctl <-= str->quoted(tl tl args);
+	"endcontrol" =>
+		if(c != controller)
+			return "invalid request";
+		controller = nil;
+		allowcontrol = 1;
+		c.flags &= ~(Controlstarted | Controller);
+	* =>
+		if(c == controller || controller == nil || (controller.flags & Controlstarted) == 0)
+			return "unknown control request";
+		controller.ctl <-= "request " + string c.id + " " + req;
+	}
+	return nil;
+}
+
+Fix: con 1000;
+# the window manager window has been reshaped;
+# allocate a new screen, and move all the 
+reshaped(win: ref Wmclient->Window)
+{
+	oldr := screen.image.r;
+	newr := win.image.r;
+	mx := Fix;
+	if(oldr.dx() > 0)
+		mx = newr.dx() * Fix / oldr.dx();
+	my := Fix;
+	if(oldr.dy() > 0)
+		my = newr.dy() * Fix / oldr.dy();
+	screen = makescreen(win.image);
+	for(z := wmsrv->top(); z != nil; z = z.znext){
+		for(wl := z.wins; wl != nil; wl = tl wl){
+			w := hd wl;
+			w.img = nil;
+			nr := w.r.subpt(oldr.min);
+			nr.min.x = nr.min.x * mx / Fix;
+			nr.min.y = nr.min.y * my / Fix;
+			nr.max.x = nr.max.x * mx / Fix;
+			nr.max.y = nr.max.y * my / Fix;
+			nr = nr.addpt(newr.min);
+			w.img = screen.newwindow(nr, Draw->Refbackup, Draw->Nofill);
+			# XXX check for creation failure
+			w.r = nr;
+			z.ctl <-= sys->sprint("!reshape %q -1 %s", w.tag, r2s(nr));
+			z.ctl <-= "rect " + r2s(newr);
+		}
+	}
+}
+
+controlevent(e: string)
+{
+	if(controller != nil && (controller.flags & Controlstarted))
+		controller.ctl <-= e;
+}
+
+dragwin(ptr: chan of ref Pointer, c: ref Client, w: ref Window, off: Point): string
+{
+	if(buttons == 0)
+		return "too late";
+	p: ref Pointer;
+	scr := screen.image.r;
+	Margin: con 10;
+	do{
+		p = <-ptr;
+		org := p.xy.sub(off);
+		if(org.y < scr.min.y)
+			org.y = scr.min.y;
+		else if(org.y > scr.max.y - Margin)
+			org.y = scr.max.y - Margin;
+		if(org.x < scr.min.x && org.x + w.r.dx() < scr.min.x + Margin)
+			org.x = scr.min.x + Margin - w.r.dx();
+		else if(org.x > scr.max.x - Margin)
+			org.x = scr.max.x - Margin;
+		w.img.origin(w.img.r.min, org);
+	} while (p.buttons != 0);
+	c.ptr <-= p;
+	buttons = 0;
+	r: Rect;
+	r.min = p.xy.sub(off);
+	r.max = r.min.add(w.r.size());
+	if(r.eq(w.r))
+		return "not moved";
+	reshape(c, w.tag, r);
+	return nil;
+}
+
+sizewin(ptrc: chan of ref Pointer, c: ref Client, w: ref Window, minsize: Point): string
+{
+	borders := array[4] of ref Image;
+	showborders(borders, w.r, Minx|Maxx|Miny|Maxy);
+	screen.image.flush(Draw->Flushnow);
+	while((ptr := <-ptrc).buttons == 0)
+		;
+	xy := ptr.xy;
+	move, show: int;
+	offset := Point(0, 0);
+	r := w.r;
+	show = Minx|Miny|Maxx|Maxy;
+	if(xy.in(w.r) == 0){
+		r = (xy, xy);
+		move = Maxx|Maxy;
+	}else {
+		if(xy.x < (r.min.x+r.max.x)/2){
+			move=Minx;
+			offset.x = xy.x - r.min.x;
+		}else{
+			move=Maxx;
+			offset.x = xy.x - r.max.x;
+		}
+		if(xy.y < (r.min.y+r.max.y)/2){
+			move |= Miny;
+			offset.y = xy.y - r.min.y;
+		}else{
+			move |= Maxy;
+			offset.y = xy.y - r.max.y;
+		}
+	}
+	return reshape(c, w.tag, sweep(ptrc, r, offset, borders, move, show, minsize));
+}
+
+reshape(c: ref Client, tag: string, r: Rect): string
+{
+	w := c.window(tag);
+	# if window hasn't changed size, then just change its origin and use the same image.
+	if((c.flags & Fixedorigin) == 0 && w != nil && w.r.size().eq(r.size())){
+		c.setorigin(tag, r.min);
+	} else {
+		img := screen.newwindow(r, Draw->Refbackup, Draw->Nofill);
+		if(img == nil)
+			return sys->sprint("window creation failed: %r");
+		if(c.setimage(tag, img) == -1)
+			return "can't do two at once";
+	}
+	c.top();
+	return nil;
+}
+
+sweep(ptr: chan of ref Pointer, r: Rect, offset: Point, borders: array of ref Image, move, show: int, min: Point): Rect
+{
+	while((p := <-ptr).buttons != 0){
+		xy := p.xy.sub(offset);
+		if(move&Minx)
+			r.min.x = xy.x;
+		if(move&Miny)
+			r.min.y = xy.y;
+		if(move&Maxx)
+			r.max.x = xy.x;
+		if(move&Maxy)
+			r.max.y = xy.y;
+		showborders(borders, r, show);
+	}
+	r = r.canon();
+	if(r.min.y < screen.image.r.min.y){
+		r.min.y = screen.image.r.min.y;
+		r = r.canon();
+	}
+	if(r.dx() < min.x){
+		if(move & Maxx)
+			r.max.x = r.min.x + min.x;
+		else
+			r.min.x = r.max.x - min.x;
+	}
+	if(r.dy() < min.y){
+		if(move & Maxy)
+			r.max.y = r.min.y + min.y;
+		else {
+			r.min.y = r.max.y - min.y;
+			if(r.min.y < screen.image.r.min.y){
+				r.min.y = screen.image.r.min.y;
+				r.max.y = r.min.y + min.y;
+			}
+		}
+	}
+	return r;
+}
+
+showborders(b: array of ref Image, r: Rect, show: int)
+{
+	r = r.canon();
+	b[Sminx] = showborder(b[Sminx], show&Minx,
+		(r.min, (r.min.x+Bdwidth, r.max.y)));
+	b[Sminy] = showborder(b[Sminy], show&Miny,
+		((r.min.x+Bdwidth, r.min.y), (r.max.x-Bdwidth, r.min.y+Bdwidth)));
+	b[Smaxx] = showborder(b[Smaxx], show&Maxx,
+		((r.max.x-Bdwidth, r.min.y), (r.max.x, r.max.y)));
+	b[Smaxy] = showborder(b[Smaxy], show&Maxy,
+		((r.min.x+Bdwidth, r.max.y-Bdwidth), (r.max.x-Bdwidth, r.max.y)));
+}
+
+showborder(b: ref Image, show: int, r: Rect): ref Image
+{
+	if(!show)
+		return nil;
+	if(b != nil && b.r.size().eq(r.size()))
+		b.origin(r.min, r.min);
+	else
+		b = screen.newwindow(r, Draw->Refbackup, Draw->Red);
+	return b;
+}
+
+r2s(r: Rect): string
+{
+	return string r.min.x + " " + string r.min.y + " " +
+			string r.max.x + " " + string r.max.y;
+}
+
+# XXX for consideration:
+# do not allow applications to grab the keyboard focus
+# unless there is currently no keyboard focus...
+# but what about launching a new app from the taskbar:
+# surely we should allow that to grab the focus?
+setfocus(win: ref Wmclient->Window, new: ref Client)
+{
+	old := kbdfocus;
+	if(old == new)
+		return;
+	if(new == nil)
+		wmclient->win.wmctl("cursor");
+	else if(old == nil || old.cursor != new.cursor)
+		wmclient->win.wmctl(new.cursor);
+	if(new != nil && (new.flags & Kbdstarted) == 0)
+		return;
+	if(old != nil)
+		old.ctl <-= "haskbdfocus 0";
+	
+	if(new != nil){
+		new.ctl <-= "raise";
+		new.ctl <-= "haskbdfocus 1";
+		kbdfocus = new;
+	} else
+		kbdfocus = nil;
+}
+
+makescreen(img: ref Image): ref Screen
+{
+	screen = Screen.allocate(img, img.display.color(Background), 0);
+	img.draw(img.r, screen.fill, nil, screen.fill.r.min);
+	return screen;
+}
+
+kill(pid: int, note: string): int
+{
+	fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd == nil || sys->fprint(fd, "%s", note) < 0)
+		return -1;
+	return 0;
+}
+
+fatal(s: string)
+{
+	sys->fprint(sys->fildes(2), "wm: %s\n", s);
+	kill(sys->pctl(0, nil), "killgrp");
+	raise "fail:error";
+}
+
+# fit a window rectangle to the available space.
+# try to preserve requested location if possible.
+# make sure that the window is no bigger than
+# the screen, and that its top and left-hand edges
+# will be visible at least.
+fitrect(w, r: Rect): Rect
+{
+	if(w.dx() > r.dx())
+		w.max.x = w.min.x + r.dx();
+	if(w.dy() > r.dy())
+		w.max.y = w.min.y + r.dy();
+	size := w.size();
+	if (w.max.x > r.max.x)
+		(w.min.x, w.max.x) = (r.min.x - size.x, r.max.x - size.x);
+	if (w.max.y > r.max.y)
+		(w.min.y, w.max.y) = (r.min.y - size.y, r.max.y - size.y);
+	if (w.min.x < r.min.x)
+		(w.min.x, w.max.x) = (r.min.x, r.min.x + size.x);
+	if (w.min.y < r.min.y)
+		(w.min.y, w.max.y) = (r.min.y, r.min.y + size.y);
+	return w;
+}
+
+lastrect: Rect;
+# find an suitable area for a window
+newrect(w, r: Rect): Rect
+{
+	rl: list of Rect;
+	for(z := wmsrv->top(); z != nil; z = z.znext)
+		for(wl := z.wins; wl != nil; wl = tl wl)
+			rl = (hd wl).r :: rl;
+	lastrect = winplace->place(rl, r, lastrect, w.size());
+	return lastrect;
+}
+
+bufferproc(in, out: chan of string)
+{
+	h, t: list of string;
+	dummyout := chan of string;
+	for(;;){
+		outc := dummyout;
+		s: string;
+		if(h != nil || t != nil){
+			outc = out;
+			if(h == nil)
+				for(; t != nil; t = tl t)
+					h = hd t :: h;
+			s = hd h;
+		}
+		alt{
+		x := <-in =>
+			t = x :: t;
+		outc <-= s =>
+			h = tl h;
+		}
+	}
+}
+
+command(ctxt: ref Draw->Context, args: list of string, sync: chan of string)
+{
+	if((sh := load Sh Sh->PATH) != nil){
+		sh->run(ctxt, "{$*&}" :: args);
+		sync <-= nil;
+		return;
+	}
+	fds := list of {0, 1, 2};
+	sys->pctl(sys->NEWFD, fds);
+
+	cmd := hd args;
+	file := cmd;
+
+	if(len file<4 || file[len file-4:]!=".dis")
+		file += ".dis";
+
+	c := load Wm file;
+	if(c == nil) {
+		err := sys->sprint("%r");
+		if(err != "permission denied" && err != "access permission denied" && file[0]!='/' && file[0:2]!="./"){
+			c = load Wm "/dis/"+file;
+			if(c == nil)
+				err = sys->sprint("%r");
+		}
+		if(c == nil){
+			sync <-= sys->sprint("%s: %s\n", cmd, err);
+			exit;
+		}
+	}
+	sync <-= nil;
+	c->init(ctxt, args);
+}
--- /dev/null
+++ b/appl/wm/wmdeb.m
@@ -1,0 +1,82 @@
+Diss: module {};
+
+DebSrc: module
+{
+	PATH:	con "/dis/wm/debsrc.dis";
+
+	Mod: adt
+	{
+		src:	string;		# .b path
+		tk:	string;		# text widget
+		dis:	string;		# .dis path
+		sym:	ref Sym;	# debugger symbol table
+		srcask:	int;		# look for src file?
+		symask:	int;		# look for symbol file?
+	};
+
+	loadsrc:	fn(src: string, addpath: int): ref Mod;
+	showstrsrc:	fn(src: string);
+	search:		fn(s: string): int;
+	snarf:		fn(): string;
+	getsel:		fn(): (ref Mod, int);
+	attachdis:	fn(m: ref Mod): int;
+	attachsym:	fn(m: ref Mod);
+	showmodsrc:	fn(m: ref Mod, src: ref Src);
+	findmod:	fn(m: ref Module): ref Mod;
+
+	init:		fn(ctxt: ref Draw->Context, t: ref Tk->Toplevel,
+				tkclient: Tkclient, selectfile: Selectfile, dialog: Dialog,
+				str: String, debug: Debug, xscroll: int, remcr: int);
+	reinit:	fn(xscroll: int, remcr: int);
+
+	packed:		ref Mod;
+	searchpath:	array of string;
+	opendir:	string;
+};
+
+DebData: module
+{
+	PATH:	con "/dis/wm/debdata.dis";
+
+	Datum: adt
+	{
+		tkid:		string;
+		parent:		string;				# tkid of parent
+		vtk:		string;				# root tk name
+		e:		ref Exp;
+		val:		string;				# value displayed on screen
+		canwalk:	int;				# can the variable be expanded?
+		kids:		cyclic array of ref Datum;	# list of expanded kids
+
+		expand:		fn(d: self ref Datum, okids: array of ref Datum, who: string): ref Datum;
+		contract:	fn(d: self ref Datum, who: string): ref Datum;
+		destroy:	fn(d: self ref Datum);
+		showsrc:	fn(d: self ref Datum);
+	};
+
+	Vars: adt
+	{
+		tk:		string;				# root tk widget
+		xbar:		int;				# x coord of var/val dividing line
+		d:		array of ref Datum;		# displayed variables
+
+		create:		fn(): ref Vars;
+		delete:		fn(v: self ref Vars);
+		show:		fn(v: self ref Vars);
+		refresh:	fn(v: self ref Vars, e: array of ref Debug->Exp);
+
+		expand:		fn(v: self ref Vars, kid: string);
+		contract:	fn(v: self ref Vars, kid: string);
+		showsrc:	fn(v: self ref Vars, kid: string);
+		update:		fn(v: self ref Vars);
+		scrolly:	fn(v: self ref Vars, s: string);
+	};
+
+	ctl:		fn(s: string);
+	wmctl:	fn(s: string);
+	init:		fn(ctxt: ref Draw->Context, geom: string,
+				debsrc: DebSrc,
+				str: String, debug: Debug):
+			(ref Tk->Toplevel, chan of string, chan of string);
+	raisex:	fn();
+};
--- /dev/null
+++ b/appl/wm/wmplay.b
@@ -1,0 +1,176 @@
+implement WmPlay;
+
+include "sys.m";
+	sys: Sys;
+
+include "draw.m";
+	draw: Draw;
+	Context: import draw;
+	gctxt: ref Context;
+
+include "tk.m";
+	tk: Tk;
+
+include	"tkclient.m";
+	tkclient: Tkclient;
+
+include "dialog.m";
+	dialog: Dialog;
+
+include "selectfile.m";
+	selectfile: Selectfile;
+
+tpid:	int;
+ppid:	int;
+Magic:	con "rate";
+data:	con "/dev/audio";
+ctl:	con "/dev/audioctl";
+buffz:	con Sys->ATOMICIO;
+top: ref Tk->Toplevel;
+
+WmPlay: module
+{
+	init:	fn(ctxt: ref Context, argv: list of string);
+};
+
+notecmd := array[] of {
+	"frame .f",
+	"label .f.l -bitmap error -foreground red",
+	"button .b -text Continue -command {send cmd done}",
+	"focus .f",
+	"bind .f <Key-\n> {send cmd done}",
+	"pack .f.l .f.m -side left -expand 1 -padx 10 -pady 10",
+	"pack .f .b -padx 10 -pady 10",
+	"update; cursor -default"
+};
+
+notice(message: string)
+{
+	dialog->prompt(gctxt, top.image, "error -fg red", "Error", message, 0, "OK"::nil);
+}
+
+play(f: string)
+{
+	ppid = sys->pctl(0, nil);
+	buff := array[buffz] of byte;
+	inf := sys->open(f, Sys->OREAD);
+	if (inf == nil) {
+		notice(sys->sprint("could not open %s: %r", f));
+		return;
+	}
+	n := sys->read(inf, buff, buffz);
+	if (n < 0) {
+		notice(sys->sprint("could not read %s: %r", f));
+		return;
+	}
+	if (n < 10 || string buff[0:4] != Magic) {
+		notice(sys->sprint("%s: not an audio file", f));
+		return;
+	}
+	i := 0;
+	for (;;) {
+		if (i == n) {
+			notice(sys->sprint("%s: bad header", f));
+			return;
+		}
+		if (buff[i] == byte '\n') {
+			i++;
+			if (i == n) {
+				notice(sys->sprint("%s: bad header", f));
+				return;
+			}
+			if (buff[i] == byte '\n') {
+				i++;
+				if ((i % 4) != 0) {
+					notice(sys->sprint("%s: unpadded header", f));
+					return;
+				}
+				break;
+			}
+		}
+		else
+			i++;
+	}
+	df := sys->open(data, Sys->OWRITE);
+	if (df == nil) {
+		notice(sys->sprint("could not open %s: %r", data));
+		return;
+	}
+	cf := sys->open(ctl, Sys->OWRITE);
+	if (cf == nil) {
+		notice(sys->sprint("could not open %s: %r", ctl));
+		return;
+	}
+	if (sys->write(cf, buff, i - 1) < 0) {
+		notice(sys->sprint("could not write %s: %r", ctl));
+		return;
+	}
+	if (n > i && sys->write(df, buff[i:n], n - i) < 0) {
+		notice(sys->sprint("could not write %s: %r", data));
+		return;
+	}
+	if (sys->stream(inf, df, Sys->ATOMICIO) < 0) {
+		notice(sys->sprint("could not stream %s: %r", data));
+		return;
+	}
+}
+
+doplay(f: string)
+{
+	play(f);
+	kill(tpid);
+}
+
+init(ctxt: ref Context, argv: list of string)
+{
+	sys = load Sys Sys->PATH;
+	if (ctxt == nil) {
+		sys->fprint(sys->fildes(2), "wmplay: no window context\n");
+		raise "fail:bad context";
+	}
+	draw = load Draw Draw->PATH;
+	tk = load Tk Tk->PATH;
+	tkclient = load Tkclient Tkclient->PATH;
+	dialog = load Dialog Dialog->PATH;
+	selectfile = load Selectfile Selectfile->PATH;
+
+	gctxt = ctxt;
+	sys->pctl(Sys->NEWPGRP, nil);
+	tkclient->init();
+	dialog->init();
+	selectfile->init();
+
+	file: string;
+	argv = tl argv;
+	if (argv != nil)
+		file = hd argv;
+	else {
+		file = selectfile->filename(ctxt, nil, "Locate Audio File", "*.iaf"::"*.wav"::nil, "");
+		if (file == "")
+			exit;
+	}
+
+	(t, menubut) := tkclient->toplevel(ctxt, "-borderwidth 2 -relief raised", "Play", 0);
+	tk->cmd(t, "label .d -label {" + file + "}");
+	tk->cmd(t, "pack .Wm_t -fill x; pack .d; pack propagate . 0");
+	tk->cmd(t, "update");
+	top = t;
+	tpid = sys->pctl(0, nil);
+	spawn doplay(file);
+
+	for(;;) {
+		menu := <- menubut;
+		if(menu == "exit") {
+			kill(ppid);
+			return;
+		}
+		tkclient->wmctl(t, menu);
+	}
+}
+
+kill(pid: int)
+{
+	fd := sys->open("/prog/" + string pid + "/ctl", sys->OWRITE);
+	if (fd != nil)
+		sys->fprint(fd, "kill");
+}